Merge branch 'develop' of github.com:Budibase/budibase into views-v2-frontend

This commit is contained in:
Andrew Kingston 2023-08-01 14:12:03 +01:00
commit cb78788bdb
13 changed files with 359 additions and 65 deletions

View File

@ -1,5 +1,5 @@
{
"version": "2.8.29-alpha.8",
"version": "2.8.29-alpha.9",
"npmClient": "yarn",
"packages": [
"packages/*"

View File

@ -2,15 +2,79 @@ import sdk from "../../../sdk"
import {
CreateViewRequest,
Ctx,
UIFieldMetadata,
UpdateViewRequest,
ViewResponse,
ViewV2,
RequiredKeys,
} from "@budibase/types"
async function parseSchemaUI(ctx: Ctx, view: CreateViewRequest) {
if (!view.schema) {
return
}
function hasOverrides(
newObj: Record<string, any>,
existingObj: Record<string, any>
) {
const result = Object.entries(newObj).some(([key, value]) => {
const isObject = typeof value === "object"
const existing = existingObj[key]
if (isObject && hasOverrides(value, existing || {})) {
return true
}
if (!isObject && value !== existing) {
return true
}
})
return result
}
const table = await sdk.tables.getTable(view.tableId)
for (const [
fieldName,
{ order, width, visible, icon, ...schemaNonUI },
] of Object.entries(view.schema)) {
const overrides = hasOverrides(schemaNonUI, table.schema[fieldName])
if (overrides) {
ctx.throw(
400,
"This endpoint does not support overriding non UI fields in the schema"
)
}
}
const schemaUI =
view.schema &&
Object.entries(view.schema).reduce((p, [fieldName, schemaValue]) => {
p[fieldName] = {
order: schemaValue.order,
width: schemaValue.width,
visible: schemaValue.visible,
icon: schemaValue.icon,
}
return p
}, {} as Record<string, RequiredKeys<UIFieldMetadata>>)
return schemaUI
}
export async function create(ctx: Ctx<CreateViewRequest, ViewResponse>) {
const view = ctx.request.body
const { tableId } = view
const result = await sdk.views.create(tableId, view)
const schemaUI = await parseSchemaUI(ctx, view)
const parsedView: Omit<ViewV2, "id" | "version"> = {
name: view.name,
tableId: view.tableId,
query: view.query,
sort: view.sort,
columns: view.schema && Object.keys(view.schema),
schemaUI,
}
const result = await sdk.views.create(tableId, parsedView)
ctx.status = 201
ctx.body = {
data: result,
@ -30,7 +94,19 @@ export async function update(ctx: Ctx<UpdateViewRequest, ViewResponse>) {
const { tableId } = view
const result = await sdk.views.update(tableId, view)
const schemaUI = await parseSchemaUI(ctx, view)
const parsedView: ViewV2 = {
id: view.id,
name: view.name,
version: view.version,
tableId: view.tableId,
query: view.query,
sort: view.sort,
columns: view.schema && Object.keys(view.schema),
schemaUI,
}
const result = await sdk.views.update(tableId, parsedView)
ctx.body = {
data: result,
}

View File

@ -1033,7 +1033,7 @@ describe("/rows", () => {
}
const view = await config.api.viewV2.create({
columns: { name: { visible: true } },
schema: { name: {} },
})
const response = await config.api.viewV2.search(view.id)
@ -1102,7 +1102,7 @@ describe("/rows", () => {
const table = await config.createTable(userTable())
const view = await config.api.viewV2.create({
tableId: table._id!,
columns: {
schema: {
name: { visible: true },
surname: { visible: true },
address: { visible: true },
@ -1150,7 +1150,7 @@ describe("/rows", () => {
const tableId = table._id!
const view = await config.api.viewV2.create({
tableId,
columns: {
schema: {
name: { visible: true },
address: { visible: true },
},
@ -1203,7 +1203,7 @@ describe("/rows", () => {
const tableId = table._id!
const view = await config.api.viewV2.create({
tableId,
columns: {
schema: {
name: { visible: true },
address: { visible: true },
},
@ -1231,7 +1231,7 @@ describe("/rows", () => {
const tableId = table._id!
const view = await config.api.viewV2.create({
tableId,
columns: {
schema: {
name: { visible: true },
address: { visible: true },
},

View File

@ -1,6 +1,7 @@
import * as setup from "./utilities"
import {
CreateViewRequest,
FieldSchema,
FieldType,
SortOrder,
SortType,
@ -40,7 +41,7 @@ describe("/v2/views", () => {
order: SortOrder.DESCENDING,
type: SortType.STRING,
},
columns: {
schema: {
name: {
visible: true,
},
@ -73,17 +74,111 @@ describe("/v2/views", () => {
const newView: CreateViewRequest = {
name: generator.name(),
tableId: config.table!._id!,
...viewFilters,
query: viewFilters.query,
sort: viewFilters.sort,
}
delete newView.schema
const res = await config.api.viewV2.create(newView)
expect(res).toEqual({
...newView,
...viewFilters,
query: viewFilters.query,
sort: viewFilters.sort,
id: expect.any(String),
version: 2,
})
})
it("persist only UI schema overrides", async () => {
const newView: CreateViewRequest = {
name: generator.name(),
tableId: config.table!._id!,
schema: {
Price: {
name: "Price",
type: FieldType.NUMBER,
visible: true,
order: 1,
width: 100,
},
Category: {
name: "Category",
type: FieldType.STRING,
visible: false,
icon: "ic",
},
} as Record<string, FieldSchema>,
}
const createdView = await config.api.viewV2.create(newView)
expect(await config.api.viewV2.get(createdView.id)).toEqual({
...newView,
schema: undefined,
columns: ["Price", "Category"],
schemaUI: {
Price: {
visible: true,
order: 1,
width: 100,
},
Category: {
visible: false,
icon: "ic",
},
},
id: createdView.id,
version: 2,
})
})
it("throw an exception if the schema overrides a non UI field", async () => {
const newView: CreateViewRequest = {
name: generator.name(),
tableId: config.table!._id!,
schema: {
Price: {
name: "Price",
type: FieldType.NUMBER,
visible: true,
},
Category: {
name: "Category",
type: FieldType.STRING,
constraints: {
type: "string",
presence: true,
},
},
} as Record<string, FieldSchema>,
}
await config.api.viewV2.create(newView, {
expectStatus: 400,
})
})
it("will not throw an exception if the schema is 'deleting' non UI fields", async () => {
const newView: CreateViewRequest = {
name: generator.name(),
tableId: config.table!._id!,
schema: {
Price: {
name: "Price",
type: FieldType.NUMBER,
visible: true,
},
Category: {
name: "Category",
type: FieldType.STRING,
},
} as Record<string, FieldSchema>,
}
await config.api.viewV2.create(newView, {
expectStatus: 201,
})
})
})
describe("update", () => {
@ -202,6 +297,94 @@ describe("/v2/views", () => {
status: 400,
})
})
it("updates only UI schema overrides", async () => {
await config.api.viewV2.update({
...view,
schema: {
Price: {
name: "Price",
type: FieldType.NUMBER,
visible: true,
order: 1,
width: 100,
},
Category: {
name: "Category",
type: FieldType.STRING,
visible: false,
icon: "ic",
},
} as Record<string, FieldSchema>,
})
expect(await config.api.viewV2.get(view.id)).toEqual({
...view,
schema: undefined,
columns: ["Price", "Category"],
schemaUI: {
Price: {
visible: true,
order: 1,
width: 100,
},
Category: {
visible: false,
icon: "ic",
},
},
id: view.id,
version: 2,
})
})
it("throw an exception if the schema overrides a non UI field", async () => {
await config.api.viewV2.update(
{
...view,
schema: {
Price: {
name: "Price",
type: FieldType.NUMBER,
visible: true,
},
Category: {
name: "Category",
type: FieldType.STRING,
constraints: {
type: "string",
presence: true,
},
},
} as Record<string, FieldSchema>,
},
{
expectStatus: 400,
}
)
})
it("will not throw an exception if the schema is 'deleting' non UI fields", async () => {
await config.api.viewV2.update(
{
...view,
schema: {
Price: {
name: "Price",
type: FieldType.NUMBER,
visible: true,
},
Category: {
name: "Category",
type: FieldType.STRING,
},
} as Record<string, FieldSchema>,
},
{
expectStatus: 200,
}
)
})
})
describe("delete", () => {

View File

@ -129,7 +129,7 @@ describe("trimViewRowInfo middleware", () => {
id: viewId,
name: generator.guid(),
tableId: table._id!,
columns: { name: { visible: true }, address: { visible: true } },
columns: ["name", "address"],
})
const data = getRandomData()

View File

@ -13,7 +13,7 @@ jest.unmock("mysql2/promise")
jest.setTimeout(30000)
describe("external", () => {
describe.skip("external", () => {
const config = new TestConfiguration()
let externalDatasource: Datasource

View File

@ -1,5 +1,5 @@
import { HTTPError, context } from "@budibase/backend-core"
import { TableSchema, UIFieldMetadata, View, ViewV2 } from "@budibase/types"
import { FieldSchema, TableSchema, View, ViewV2 } from "@budibase/types"
import sdk from "../../../sdk"
import * as utils from "../../../db/utils"
@ -74,37 +74,34 @@ export function enrichSchema(view: View | ViewV2, tableSchema: TableSchema) {
return view
}
let schema = { ...tableSchema }
if (view.schemaUI) {
const viewOverridesEntries = Object.entries(view.schemaUI)
const viewSetsOrder = viewOverridesEntries.some(([_, v]) => v.order)
for (const [fieldName, schemaUI] of viewOverridesEntries) {
schema[fieldName] = {
...schema[fieldName],
...schemaUI,
order: viewSetsOrder
? schemaUI.order || undefined
: schema[fieldName].order,
}
}
}
if (view?.columns?.length) {
const pickedSchema: Record<string, FieldSchema> = {}
for (const fieldName of view.columns) {
if (!schema[fieldName]) {
continue
}
pickedSchema[fieldName] = { ...schema[fieldName] }
}
schema = pickedSchema
}
return {
...view,
schema:
!view?.columns || !Object.entries(view?.columns).length
? tableSchema
: enrichViewV2Schema(tableSchema, view.columns),
schema: schema,
}
}
function enrichViewV2Schema(
tableSchema: TableSchema,
viewOverrides: Record<string, UIFieldMetadata>
) {
const result: TableSchema = {}
const viewOverridesEntries = Object.entries(viewOverrides)
const viewSetsOrder = viewOverridesEntries.some(([_, v]) => v.order)
for (const [columnName, columnUIMetadata] of viewOverridesEntries) {
if (!columnUIMetadata.visible) {
continue
}
if (!tableSchema[columnName]) {
continue
}
const tableFieldSchema = tableSchema[columnName]
if (viewSetsOrder) {
delete tableFieldSchema.order
}
result[columnName] = merge(tableFieldSchema, columnUIMetadata)
}
return result
}

View File

@ -102,18 +102,14 @@ describe("table sdk", () => {
})
})
it("if view schema only defines visiblility, should only fetch the selected fields", async () => {
it("if view schema only defines columns, should only fetch the selected fields", async () => {
const tableId = basicTable._id!
const view: ViewV2 = {
version: 2,
id: generator.guid(),
name: generator.guid(),
tableId,
columns: {
name: { visible: true },
id: { visible: true },
description: { visible: false },
},
columns: ["name", "id"],
}
const res = enrichSchema(view, basicTable.schema)
@ -151,7 +147,7 @@ describe("table sdk", () => {
id: generator.guid(),
name: generator.guid(),
tableId,
columns: { unnexisting: { visible: true }, name: { visible: true } },
columns: ["unnexisting", "name"],
}
const res = enrichSchema(view, basicTable.schema)
@ -175,16 +171,17 @@ describe("table sdk", () => {
)
})
it("if view schema only defines visiblility, should only fetch the selected fields", async () => {
it("if the view schema overrides the schema UI, the table schema should be overridden", async () => {
const tableId = basicTable._id!
const view: ViewV2 = {
version: 2,
id: generator.guid(),
name: generator.guid(),
tableId,
columns: {
name: { visible: true },
id: { visible: true },
columns: ["name", "id", "description"],
schemaUI: {
name: { visible: true, width: 100 },
id: { visible: true, width: 20 },
description: { visible: false },
},
}
@ -200,7 +197,7 @@ describe("table sdk", () => {
name: "name",
order: 2,
visible: true,
width: 80,
width: 100,
constraints: {
type: "string",
},
@ -210,23 +207,34 @@ describe("table sdk", () => {
name: "id",
order: 1,
visible: true,
width: 20,
constraints: {
type: "number",
},
},
description: {
type: "string",
name: "description",
visible: false,
width: 200,
constraints: {
type: "string",
},
},
},
})
)
})
it("if view defines order, the table schema order should be ignored", async () => {
it("if the view defines order, the table schema order should be ignored", async () => {
const tableId = basicTable._id!
const view: ViewV2 = {
version: 2,
id: generator.guid(),
name: generator.guid(),
tableId,
columns: {
columns: ["name", "id", "description"],
schemaUI: {
name: { visible: true, order: 1 },
id: { visible: true },
description: { visible: false, order: 2 },
@ -257,6 +265,16 @@ describe("table sdk", () => {
type: "number",
},
},
description: {
type: "string",
name: "description",
order: 2,
visible: false,
width: 200,
constraints: {
type: "string",
},
},
},
})
)

View File

@ -1,17 +1,19 @@
import {
CreateViewRequest,
SortOrder,
SortType,
UpdateViewRequest,
DeleteRowRequest,
PatchRowRequest,
PatchRowResponse,
Row,
SortOrder,
SortType,
ViewV2,
} from "@budibase/types"
import TestConfiguration from "../TestConfiguration"
import { TestAPI } from "./base"
import { generator } from "@budibase/backend-core/tests"
import { Response } from "superagent"
import sdk from "../../../sdk"
export class ViewV2API extends TestAPI {
constructor(config: TestConfiguration) {
@ -42,7 +44,7 @@ export class ViewV2API extends TestAPI {
}
update = async (
view: ViewV2,
view: UpdateViewRequest,
{
expectStatus,
handleResponse,
@ -71,6 +73,12 @@ export class ViewV2API extends TestAPI {
.expect(expectStatus)
}
get = async (viewId: string) => {
return await this.config.doInContext(this.config.appId, () =>
sdk.views.get(viewId)
)
}
search = async (
viewId: string,
options?: {

View File

@ -1,9 +1,15 @@
import { TableSchema, ViewV2 } from "../../../documents"
import { ViewV2, UIFieldMetadata } from "../../../documents"
export interface ViewResponse {
data: ViewV2
}
export type CreateViewRequest = Omit<ViewV2, "version" | "id">
export interface CreateViewRequest
extends Omit<ViewV2, "version" | "id" | "columns" | "schemaUI"> {
schema?: Record<string, UIFieldMetadata>
}
export type UpdateViewRequest = ViewV2
export interface UpdateViewRequest
extends Omit<ViewV2, "columns" | "schemaUI"> {
schema?: Record<string, UIFieldMetadata>
}

View File

@ -25,7 +25,8 @@ export interface ViewV2 {
order?: SortOrder
type?: SortType
}
columns?: Record<string, UIFieldMetadata>
columns?: string[]
schemaUI?: Record<string, UIFieldMetadata>
}
export type ViewSchema = ViewCountOrSumSchema | ViewStatisticsSchema

View File

@ -1,3 +1,4 @@
export * from "./documents"
export * from "./sdk"
export * from "./api"
export * from "./shared"

View File

@ -3,3 +3,7 @@ export type DeepPartial<T> = {
}
export type ISO8601 = string
export type RequiredKeys<T> = {
[K in keyof Required<T>]: T[K]
}