Merge branch 'develop' of github.com:Budibase/budibase into views-v2-frontend
This commit is contained in:
commit
cb78788bdb
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"version": "2.8.29-alpha.8",
|
||||
"version": "2.8.29-alpha.9",
|
||||
"npmClient": "yarn",
|
||||
"packages": [
|
||||
"packages/*"
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
|
|
|
@ -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 },
|
||||
},
|
||||
|
|
|
@ -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", () => {
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -13,7 +13,7 @@ jest.unmock("mysql2/promise")
|
|||
|
||||
jest.setTimeout(30000)
|
||||
|
||||
describe("external", () => {
|
||||
describe.skip("external", () => {
|
||||
const config = new TestConfiguration()
|
||||
|
||||
let externalDatasource: Datasource
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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",
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
)
|
||||
|
|
|
@ -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?: {
|
||||
|
|
|
@ -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>
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
export * from "./documents"
|
||||
export * from "./sdk"
|
||||
export * from "./api"
|
||||
export * from "./shared"
|
||||
|
|
|
@ -3,3 +3,7 @@ export type DeepPartial<T> = {
|
|||
}
|
||||
|
||||
export type ISO8601 = string
|
||||
|
||||
export type RequiredKeys<T> = {
|
||||
[K in keyof Required<T>]: T[K]
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue