Merge pull request #11286 from Budibase/BUDI-7189/create_view_schema_endpoint
Return schema from table endpoint
This commit is contained in:
commit
76d32b40fd
|
@ -6,9 +6,13 @@ import {
|
||||||
isRows,
|
isRows,
|
||||||
} from "../../../utilities/schema"
|
} from "../../../utilities/schema"
|
||||||
import { isExternalTable, isSQL } from "../../../integrations/utils"
|
import { isExternalTable, isSQL } from "../../../integrations/utils"
|
||||||
import { getDatasourceParams } from "../../../db/utils"
|
import { events } from "@budibase/backend-core"
|
||||||
import { context, events } from "@budibase/backend-core"
|
import {
|
||||||
import { Table, UserCtx } from "@budibase/types"
|
FetchTablesResponse,
|
||||||
|
Table,
|
||||||
|
TableResponse,
|
||||||
|
UserCtx,
|
||||||
|
} from "@budibase/types"
|
||||||
import sdk from "../../../sdk"
|
import sdk from "../../../sdk"
|
||||||
import { jsonFromCsvString } from "../../../utilities/csv"
|
import { jsonFromCsvString } from "../../../utilities/csv"
|
||||||
import { builderSocket } from "../../../websockets"
|
import { builderSocket } from "../../../websockets"
|
||||||
|
@ -26,37 +30,34 @@ function pickApi({ tableId, table }: { tableId?: string; table?: Table }) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// covers both internal and external
|
// covers both internal and external
|
||||||
export async function fetch(ctx: UserCtx) {
|
export async function fetch(ctx: UserCtx<void, FetchTablesResponse>) {
|
||||||
const db = context.getAppDB()
|
|
||||||
|
|
||||||
const internal = await sdk.tables.getAllInternalTables()
|
const internal = await sdk.tables.getAllInternalTables()
|
||||||
|
|
||||||
const externalTables = await db.allDocs(
|
const externalTables = await sdk.datasources.getExternalDatasources()
|
||||||
getDatasourceParams("plus", {
|
|
||||||
include_docs: true,
|
|
||||||
})
|
|
||||||
)
|
|
||||||
|
|
||||||
const external = externalTables.rows.flatMap(tableDoc => {
|
const external = externalTables.flatMap(table => {
|
||||||
let entities = tableDoc.doc.entities
|
let entities = table.entities
|
||||||
if (entities) {
|
if (entities) {
|
||||||
return Object.values(entities).map((entity: any) => ({
|
return Object.values(entities).map<Table>((entity: Table) => ({
|
||||||
...entity,
|
...entity,
|
||||||
type: "external",
|
type: "external",
|
||||||
sourceId: tableDoc.doc._id,
|
sourceId: table._id,
|
||||||
sql: isSQL(tableDoc.doc),
|
sql: isSQL(table),
|
||||||
}))
|
}))
|
||||||
} else {
|
} else {
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
ctx.body = [...internal, ...external]
|
const response = [...internal, ...external].map(sdk.tables.enrichViewSchemas)
|
||||||
|
ctx.body = response
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function find(ctx: UserCtx) {
|
export async function find(ctx: UserCtx<void, TableResponse>) {
|
||||||
const tableId = ctx.params.tableId
|
const tableId = ctx.params.tableId
|
||||||
ctx.body = await sdk.tables.getTable(tableId)
|
const table = await sdk.tables.getTable(tableId)
|
||||||
|
|
||||||
|
ctx.body = sdk.tables.enrichViewSchemas(table)
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function save(ctx: UserCtx) {
|
export async function save(ctx: UserCtx) {
|
||||||
|
|
|
@ -1,12 +1,15 @@
|
||||||
const { checkBuilderEndpoint } = require("./utilities/TestFunctions")
|
import { generator } from "@budibase/backend-core/tests"
|
||||||
const setup = require("./utilities")
|
import { events, context } from "@budibase/backend-core"
|
||||||
|
import { FieldType, Table } from "@budibase/types"
|
||||||
|
import { checkBuilderEndpoint } from "./utilities/TestFunctions"
|
||||||
|
import * as setup from "./utilities"
|
||||||
const { basicTable } = setup.structures
|
const { basicTable } = setup.structures
|
||||||
const { events, context } = require("@budibase/backend-core")
|
import sdk from "../../../sdk"
|
||||||
|
|
||||||
describe("/tables", () => {
|
describe("/tables", () => {
|
||||||
let request = setup.getRequest()
|
let request = setup.getRequest()
|
||||||
let config = setup.getConfig()
|
let config = setup.getConfig()
|
||||||
let appId
|
let appId: string
|
||||||
|
|
||||||
afterAll(setup.afterAll)
|
afterAll(setup.afterAll)
|
||||||
|
|
||||||
|
@ -16,12 +19,11 @@ describe("/tables", () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
describe("create", () => {
|
describe("create", () => {
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
jest.clearAllMocks()
|
jest.clearAllMocks()
|
||||||
})
|
})
|
||||||
|
|
||||||
const createTable = (table) => {
|
const createTable = (table?: Table) => {
|
||||||
if (!table) {
|
if (!table) {
|
||||||
table = basicTable()
|
table = basicTable()
|
||||||
}
|
}
|
||||||
|
@ -29,15 +31,16 @@ describe("/tables", () => {
|
||||||
.post(`/api/tables`)
|
.post(`/api/tables`)
|
||||||
.send(table)
|
.send(table)
|
||||||
.set(config.defaultHeaders())
|
.set(config.defaultHeaders())
|
||||||
.expect('Content-Type', /json/)
|
.expect("Content-Type", /json/)
|
||||||
.expect(200)
|
.expect(200)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
it("returns a success message when the table is successfully created", async () => {
|
it("returns a success message when the table is successfully created", async () => {
|
||||||
const res = await createTable()
|
const res = await createTable()
|
||||||
|
|
||||||
expect(res.res.statusMessage).toEqual("Table TestTable saved successfully.")
|
expect((res as any).res.statusMessage).toEqual(
|
||||||
|
"Table TestTable saved successfully."
|
||||||
|
)
|
||||||
expect(res.body.name).toEqual("TestTable")
|
expect(res.body.name).toEqual("TestTable")
|
||||||
expect(events.table.created).toBeCalledTimes(1)
|
expect(events.table.created).toBeCalledTimes(1)
|
||||||
expect(events.table.created).toBeCalledWith(res.body)
|
expect(events.table.created).toBeCalledWith(res.body)
|
||||||
|
@ -45,7 +48,7 @@ describe("/tables", () => {
|
||||||
|
|
||||||
it("creates a table via data import", async () => {
|
it("creates a table via data import", async () => {
|
||||||
const table = basicTable()
|
const table = basicTable()
|
||||||
table.rows = [{ name: 'test-name', description: 'test-desc' }]
|
table.rows = [{ name: "test-name", description: "test-desc" }]
|
||||||
|
|
||||||
const res = await createTable(table)
|
const res = await createTable(table)
|
||||||
|
|
||||||
|
@ -62,7 +65,7 @@ describe("/tables", () => {
|
||||||
config,
|
config,
|
||||||
method: "POST",
|
method: "POST",
|
||||||
url: `/api/tables`,
|
url: `/api/tables`,
|
||||||
body: basicTable()
|
body: basicTable(),
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
@ -75,7 +78,7 @@ describe("/tables", () => {
|
||||||
.post(`/api/tables`)
|
.post(`/api/tables`)
|
||||||
.send(testTable)
|
.send(testTable)
|
||||||
.set(config.defaultHeaders())
|
.set(config.defaultHeaders())
|
||||||
.expect('Content-Type', /json/)
|
.expect("Content-Type", /json/)
|
||||||
.expect(200)
|
.expect(200)
|
||||||
|
|
||||||
expect(events.table.updated).toBeCalledTimes(1)
|
expect(events.table.updated).toBeCalledTimes(1)
|
||||||
|
@ -94,10 +97,10 @@ describe("/tables", () => {
|
||||||
const testRow = await request
|
const testRow = await request
|
||||||
.post(`/api/${testTable._id}/rows`)
|
.post(`/api/${testTable._id}/rows`)
|
||||||
.send({
|
.send({
|
||||||
name: "test"
|
name: "test",
|
||||||
})
|
})
|
||||||
.set(config.defaultHeaders())
|
.set(config.defaultHeaders())
|
||||||
.expect('Content-Type', /json/)
|
.expect("Content-Type", /json/)
|
||||||
.expect(200)
|
.expect(200)
|
||||||
|
|
||||||
const updatedTable = await request
|
const updatedTable = await request
|
||||||
|
@ -109,22 +112,24 @@ describe("/tables", () => {
|
||||||
key: "name",
|
key: "name",
|
||||||
_rename: {
|
_rename: {
|
||||||
old: "name",
|
old: "name",
|
||||||
updated: "updatedName"
|
updated: "updatedName",
|
||||||
},
|
},
|
||||||
schema: {
|
schema: {
|
||||||
updatedName: { type: "string" }
|
updatedName: { type: "string" },
|
||||||
}
|
},
|
||||||
})
|
})
|
||||||
.set(config.defaultHeaders())
|
.set(config.defaultHeaders())
|
||||||
.expect('Content-Type', /json/)
|
.expect("Content-Type", /json/)
|
||||||
.expect(200)
|
.expect(200)
|
||||||
expect(updatedTable.res.statusMessage).toEqual("Table TestTable saved successfully.")
|
expect((updatedTable as any).res.statusMessage).toEqual(
|
||||||
|
"Table TestTable saved successfully."
|
||||||
|
)
|
||||||
expect(updatedTable.body.name).toEqual("TestTable")
|
expect(updatedTable.body.name).toEqual("TestTable")
|
||||||
|
|
||||||
const res = await request
|
const res = await request
|
||||||
.get(`/api/${testTable._id}/rows/${testRow.body._id}`)
|
.get(`/api/${testTable._id}/rows/${testRow.body._id}`)
|
||||||
.set(config.defaultHeaders())
|
.set(config.defaultHeaders())
|
||||||
.expect('Content-Type', /json/)
|
.expect("Content-Type", /json/)
|
||||||
.expect(200)
|
.expect(200)
|
||||||
|
|
||||||
expect(res.body.updatedName).toEqual("test")
|
expect(res.body.updatedName).toEqual("test")
|
||||||
|
@ -140,7 +145,7 @@ describe("/tables", () => {
|
||||||
_id: "ta_users",
|
_id: "ta_users",
|
||||||
})
|
})
|
||||||
.set(config.defaultHeaders())
|
.set(config.defaultHeaders())
|
||||||
.expect('Content-Type', /json/)
|
.expect("Content-Type", /json/)
|
||||||
.expect(200)
|
.expect(200)
|
||||||
expect(res.body.schema.email).toBeDefined()
|
expect(res.body.schema.email).toBeDefined()
|
||||||
expect(res.body.schema.roleId).toBeDefined()
|
expect(res.body.schema.roleId).toBeDefined()
|
||||||
|
@ -153,7 +158,7 @@ describe("/tables", () => {
|
||||||
const table = await config.createTable()
|
const table = await config.createTable()
|
||||||
const importRequest = {
|
const importRequest = {
|
||||||
schema: table.schema,
|
schema: table.schema,
|
||||||
rows: [{ name: 'test-name', description: 'test-desc' }]
|
rows: [{ name: "test-name", description: "test-desc" }],
|
||||||
}
|
}
|
||||||
|
|
||||||
jest.clearAllMocks()
|
jest.clearAllMocks()
|
||||||
|
@ -162,20 +167,23 @@ describe("/tables", () => {
|
||||||
.post(`/api/tables/${table._id}/import`)
|
.post(`/api/tables/${table._id}/import`)
|
||||||
.send(importRequest)
|
.send(importRequest)
|
||||||
.set(config.defaultHeaders())
|
.set(config.defaultHeaders())
|
||||||
.expect('Content-Type', /json/)
|
.expect("Content-Type", /json/)
|
||||||
.expect(200)
|
.expect(200)
|
||||||
|
|
||||||
expect(events.table.created).not.toHaveBeenCalled()
|
expect(events.table.created).not.toHaveBeenCalled()
|
||||||
expect(events.rows.imported).toBeCalledTimes(1)
|
expect(events.rows.imported).toBeCalledTimes(1)
|
||||||
expect(events.rows.imported).toBeCalledWith(expect.objectContaining({
|
expect(events.rows.imported).toBeCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
name: "TestTable",
|
name: "TestTable",
|
||||||
_id: table._id
|
_id: table._id,
|
||||||
}), 1)
|
}),
|
||||||
|
1
|
||||||
|
)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe("fetch", () => {
|
describe("fetch", () => {
|
||||||
let testTable
|
let testTable: Table
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
testTable = await config.createTable(testTable)
|
testTable = await config.createTable(testTable)
|
||||||
|
@ -189,7 +197,7 @@ describe("/tables", () => {
|
||||||
const res = await request
|
const res = await request
|
||||||
.get(`/api/tables`)
|
.get(`/api/tables`)
|
||||||
.set(config.defaultHeaders())
|
.set(config.defaultHeaders())
|
||||||
.expect('Content-Type', /json/)
|
.expect("Content-Type", /json/)
|
||||||
.expect(200)
|
.expect(200)
|
||||||
const fetchedTable = res.body[0]
|
const fetchedTable = res.body[0]
|
||||||
expect(fetchedTable.name).toEqual(testTable.name)
|
expect(fetchedTable.name).toEqual(testTable.name)
|
||||||
|
@ -203,6 +211,70 @@ describe("/tables", () => {
|
||||||
url: `/api/tables`,
|
url: `/api/tables`,
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it("should fetch views", async () => {
|
||||||
|
const tableId = config.table!._id!
|
||||||
|
const views = [
|
||||||
|
await config.api.viewV2.create({ tableId }),
|
||||||
|
await config.api.viewV2.create({ tableId }),
|
||||||
|
]
|
||||||
|
|
||||||
|
const res = await request
|
||||||
|
.get(`/api/tables`)
|
||||||
|
.set(config.defaultHeaders())
|
||||||
|
.expect("Content-Type", /json/)
|
||||||
|
.expect(200)
|
||||||
|
|
||||||
|
expect(res.body).toEqual(
|
||||||
|
expect.arrayContaining([
|
||||||
|
expect.objectContaining({
|
||||||
|
_id: tableId,
|
||||||
|
views: views.reduce((p, c) => {
|
||||||
|
p[c.name] = { ...c, schema: expect.anything() }
|
||||||
|
return p
|
||||||
|
}, {} as any),
|
||||||
|
}),
|
||||||
|
])
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should enrich the view schemas for viewsV2", async () => {
|
||||||
|
const tableId = config.table!._id!
|
||||||
|
jest.spyOn(sdk.tables, "enrichViewSchemas").mockImplementation(t => ({
|
||||||
|
...t,
|
||||||
|
views: {
|
||||||
|
view1: {
|
||||||
|
version: 2,
|
||||||
|
name: "view1",
|
||||||
|
schema: {},
|
||||||
|
id: "new_view_id",
|
||||||
|
tableId,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
|
||||||
|
await config.api.viewV2.create({ tableId })
|
||||||
|
await config.createView({ tableId, name: generator.guid() })
|
||||||
|
|
||||||
|
const res = await config.api.table.fetch()
|
||||||
|
|
||||||
|
expect(res).toEqual(
|
||||||
|
expect.arrayContaining([
|
||||||
|
expect.objectContaining({
|
||||||
|
_id: tableId,
|
||||||
|
views: {
|
||||||
|
view1: {
|
||||||
|
version: 2,
|
||||||
|
name: "view1",
|
||||||
|
schema: {},
|
||||||
|
id: "new_view_id",
|
||||||
|
tableId,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
])
|
||||||
|
)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe("indexing", () => {
|
describe("indexing", () => {
|
||||||
|
@ -216,7 +288,7 @@ describe("/tables", () => {
|
||||||
.post(`/api/tables`)
|
.post(`/api/tables`)
|
||||||
.send(table)
|
.send(table)
|
||||||
.set(config.defaultHeaders())
|
.set(config.defaultHeaders())
|
||||||
.expect('Content-Type', /json/)
|
.expect("Content-Type", /json/)
|
||||||
.expect(200)
|
.expect(200)
|
||||||
expect(res.body._id).toBeDefined()
|
expect(res.body._id).toBeDefined()
|
||||||
expect(res.body._rev).toBeDefined()
|
expect(res.body._rev).toBeDefined()
|
||||||
|
@ -231,7 +303,7 @@ describe("/tables", () => {
|
||||||
_rev: res.body._rev,
|
_rev: res.body._rev,
|
||||||
})
|
})
|
||||||
.set(config.defaultHeaders())
|
.set(config.defaultHeaders())
|
||||||
.expect('Content-Type', /json/)
|
.expect("Content-Type", /json/)
|
||||||
.expect(200)
|
.expect(200)
|
||||||
// shouldn't have created a new index
|
// shouldn't have created a new index
|
||||||
expect((await db.getIndexes()).total_rows).toEqual(indexCount + 1)
|
expect((await db.getIndexes()).total_rows).toEqual(indexCount + 1)
|
||||||
|
@ -240,7 +312,7 @@ describe("/tables", () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
describe("destroy", () => {
|
describe("destroy", () => {
|
||||||
let testTable
|
let testTable: Table
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
testTable = await config.createTable(testTable)
|
testTable = await config.createTable(testTable)
|
||||||
|
@ -254,40 +326,44 @@ describe("/tables", () => {
|
||||||
const res = await request
|
const res = await request
|
||||||
.delete(`/api/tables/${testTable._id}/${testTable._rev}`)
|
.delete(`/api/tables/${testTable._id}/${testTable._rev}`)
|
||||||
.set(config.defaultHeaders())
|
.set(config.defaultHeaders())
|
||||||
.expect('Content-Type', /json/)
|
.expect("Content-Type", /json/)
|
||||||
.expect(200)
|
.expect(200)
|
||||||
expect(res.body.message).toEqual(`Table ${testTable._id} deleted.`)
|
expect(res.body.message).toEqual(`Table ${testTable._id} deleted.`)
|
||||||
expect(events.table.deleted).toBeCalledTimes(1)
|
expect(events.table.deleted).toBeCalledTimes(1)
|
||||||
expect(events.table.deleted).toBeCalledWith({ ...testTable, tableId: testTable._id })
|
expect(events.table.deleted).toBeCalledWith({
|
||||||
|
...testTable,
|
||||||
|
tableId: testTable._id,
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
it("deletes linked references to the table after deletion", async () => {
|
it("deletes linked references to the table after deletion", async () => {
|
||||||
const linkedTable = await config.createTable({
|
const linkedTable = await config.createTable({
|
||||||
name: "LinkedTable",
|
name: "LinkedTable",
|
||||||
type: "table",
|
type: "table",
|
||||||
key: "name",
|
|
||||||
schema: {
|
schema: {
|
||||||
name: {
|
name: {
|
||||||
type: "string",
|
type: FieldType.STRING,
|
||||||
|
name: "name",
|
||||||
constraints: {
|
constraints: {
|
||||||
type: "string",
|
type: "string",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
TestTable: {
|
TestTable: {
|
||||||
type: "link",
|
type: FieldType.LINK,
|
||||||
|
name: "TestTable",
|
||||||
fieldName: "TestTable",
|
fieldName: "TestTable",
|
||||||
tableId: testTable._id,
|
tableId: testTable._id,
|
||||||
constraints: {
|
constraints: {
|
||||||
type: "array"
|
type: "array",
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const res = await request
|
const res = await request
|
||||||
.delete(`/api/tables/${testTable._id}/${testTable._rev}`)
|
.delete(`/api/tables/${testTable._id}/${testTable._rev}`)
|
||||||
.set(config.defaultHeaders())
|
.set(config.defaultHeaders())
|
||||||
.expect('Content-Type', /json/)
|
.expect("Content-Type", /json/)
|
||||||
.expect(200)
|
.expect(200)
|
||||||
expect(res.body.message).toEqual(`Table ${testTable._id} deleted.`)
|
expect(res.body.message).toEqual(`Table ${testTable._id} deleted.`)
|
||||||
const dependentTable = await config.getTable(linkedTable._id)
|
const dependentTable = await config.getTable(linkedTable._id)
|
|
@ -186,6 +186,13 @@ export function getDatasourceParams(
|
||||||
return getDocParams(DocumentType.DATASOURCE, datasourceId, otherProps)
|
return getDocParams(DocumentType.DATASOURCE, datasourceId, otherProps)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getDatasourcePlusParams(
|
||||||
|
datasourceId?: Optional,
|
||||||
|
otherProps?: { include_docs: boolean }
|
||||||
|
) {
|
||||||
|
return getDocParams(DocumentType.DATASOURCE_PLUS, datasourceId, otherProps)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generates a new query ID.
|
* Generates a new query ID.
|
||||||
* @returns {string} The new query ID which the query doc can be stored under.
|
* @returns {string} The new query ID which the query doc can be stored under.
|
||||||
|
|
|
@ -19,6 +19,7 @@ import _ from "lodash"
|
||||||
import {
|
import {
|
||||||
BudibaseInternalDB,
|
BudibaseInternalDB,
|
||||||
getDatasourceParams,
|
getDatasourceParams,
|
||||||
|
getDatasourcePlusParams,
|
||||||
getTableParams,
|
getTableParams,
|
||||||
} from "../../../db/utils"
|
} from "../../../db/utils"
|
||||||
import sdk from "../../index"
|
import sdk from "../../index"
|
||||||
|
@ -243,3 +244,15 @@ export function mergeConfigs(update: Datasource, old: Datasource) {
|
||||||
|
|
||||||
return update
|
return update
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function getExternalDatasources(): Promise<Datasource[]> {
|
||||||
|
const db = context.getAppDB()
|
||||||
|
|
||||||
|
const externalDatasources = await db.allDocs<Datasource>(
|
||||||
|
getDatasourcePlusParams(undefined, {
|
||||||
|
include_docs: true,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
return externalDatasources.rows.map(r => r.doc)
|
||||||
|
}
|
||||||
|
|
|
@ -5,9 +5,16 @@ import {
|
||||||
isExternalTable,
|
isExternalTable,
|
||||||
isSQL,
|
isSQL,
|
||||||
} from "../../../integrations/utils"
|
} from "../../../integrations/utils"
|
||||||
import { Table, Database } from "@budibase/types"
|
import {
|
||||||
|
Table,
|
||||||
|
Database,
|
||||||
|
TableResponse,
|
||||||
|
TableViewsResponse,
|
||||||
|
} from "@budibase/types"
|
||||||
import datasources from "../datasources"
|
import datasources from "../datasources"
|
||||||
import { populateExternalTableSchemas, isEditableColumn } from "./validation"
|
import { populateExternalTableSchemas, isEditableColumn } from "./validation"
|
||||||
|
import sdk from "../../../sdk"
|
||||||
|
import _ from "lodash"
|
||||||
|
|
||||||
async function getAllInternalTables(db?: Database): Promise<Table[]> {
|
async function getAllInternalTables(db?: Database): Promise<Table[]> {
|
||||||
if (!db) {
|
if (!db) {
|
||||||
|
@ -55,6 +62,20 @@ async function getTable(tableId: any): Promise<Table> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function enrichViewSchemas(table: Table): TableResponse {
|
||||||
|
const result: TableResponse = {
|
||||||
|
...table,
|
||||||
|
views: Object.values(table.views ?? [])
|
||||||
|
.map(v => sdk.views.enrichSchema(v, table.schema))
|
||||||
|
.reduce((p, v) => {
|
||||||
|
p[v.name] = v
|
||||||
|
return p
|
||||||
|
}, {} as TableViewsResponse),
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
getAllInternalTables,
|
getAllInternalTables,
|
||||||
getAllExternalTables,
|
getAllExternalTables,
|
||||||
|
@ -62,4 +83,5 @@ export default {
|
||||||
getTable,
|
getTable,
|
||||||
populateExternalTableSchemas,
|
populateExternalTableSchemas,
|
||||||
isEditableColumn,
|
isEditableColumn,
|
||||||
|
enrichViewSchemas,
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,100 @@
|
||||||
|
import { FieldType, Table, ViewV2 } from "@budibase/types"
|
||||||
|
import { generator } from "@budibase/backend-core/tests"
|
||||||
|
import sdk from "../../.."
|
||||||
|
|
||||||
|
jest.mock("../../views", () => ({
|
||||||
|
...jest.requireActual("../../views"),
|
||||||
|
enrichSchema: jest.fn().mockImplementation(v => ({ ...v, mocked: true })),
|
||||||
|
}))
|
||||||
|
|
||||||
|
describe("table sdk", () => {
|
||||||
|
describe("enrichViewSchemas", () => {
|
||||||
|
const basicTable: Table = {
|
||||||
|
_id: generator.guid(),
|
||||||
|
name: "TestTable",
|
||||||
|
type: "table",
|
||||||
|
schema: {
|
||||||
|
name: {
|
||||||
|
type: FieldType.STRING,
|
||||||
|
name: "name",
|
||||||
|
visible: true,
|
||||||
|
width: 80,
|
||||||
|
order: 2,
|
||||||
|
constraints: {
|
||||||
|
type: "string",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
description: {
|
||||||
|
type: FieldType.STRING,
|
||||||
|
name: "description",
|
||||||
|
visible: true,
|
||||||
|
width: 200,
|
||||||
|
constraints: {
|
||||||
|
type: "string",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
id: {
|
||||||
|
type: FieldType.NUMBER,
|
||||||
|
name: "id",
|
||||||
|
visible: true,
|
||||||
|
order: 1,
|
||||||
|
constraints: {
|
||||||
|
type: "number",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
hiddenField: {
|
||||||
|
type: FieldType.STRING,
|
||||||
|
name: "hiddenField",
|
||||||
|
visible: false,
|
||||||
|
constraints: {
|
||||||
|
type: "string",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
it("should fetch the default schema if not overriden", async () => {
|
||||||
|
const tableId = basicTable._id!
|
||||||
|
function getTable() {
|
||||||
|
const view: ViewV2 = {
|
||||||
|
version: 2,
|
||||||
|
id: generator.guid(),
|
||||||
|
name: generator.guid(),
|
||||||
|
tableId,
|
||||||
|
}
|
||||||
|
return view
|
||||||
|
}
|
||||||
|
const view1 = getTable()
|
||||||
|
const view2 = getTable()
|
||||||
|
const view3 = getTable()
|
||||||
|
const res = sdk.tables.enrichViewSchemas({
|
||||||
|
...basicTable,
|
||||||
|
views: {
|
||||||
|
[view1.name]: view1,
|
||||||
|
[view2.name]: view2,
|
||||||
|
[view3.name]: view3,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(sdk.views.enrichSchema).toBeCalledTimes(3)
|
||||||
|
|
||||||
|
expect(res).toEqual({
|
||||||
|
...basicTable,
|
||||||
|
views: {
|
||||||
|
[view1.name]: {
|
||||||
|
...view1,
|
||||||
|
mocked: true,
|
||||||
|
},
|
||||||
|
[view2.name]: {
|
||||||
|
...view2,
|
||||||
|
mocked: true,
|
||||||
|
},
|
||||||
|
[view3.name]: {
|
||||||
|
...view3,
|
||||||
|
mocked: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
|
@ -1,8 +1,9 @@
|
||||||
import { HTTPError, context } from "@budibase/backend-core"
|
import { HTTPError, context } from "@budibase/backend-core"
|
||||||
import { View, ViewV2 } from "@budibase/types"
|
import { TableSchema, UIFieldMetadata, View, ViewV2 } from "@budibase/types"
|
||||||
|
|
||||||
import sdk from "../../../sdk"
|
import sdk from "../../../sdk"
|
||||||
import * as utils from "../../../db/utils"
|
import * as utils from "../../../db/utils"
|
||||||
|
import _ from "lodash"
|
||||||
|
|
||||||
export async function get(viewId: string): Promise<ViewV2 | undefined> {
|
export async function get(viewId: string): Promise<ViewV2 | undefined> {
|
||||||
const { tableId } = utils.extractViewInfoFromID(viewId)
|
const { tableId } = utils.extractViewInfoFromID(viewId)
|
||||||
|
@ -48,3 +49,43 @@ export async function remove(viewId: string): Promise<void> {
|
||||||
delete table.views![view?.name]
|
delete table.views![view?.name]
|
||||||
await db.put(table)
|
await db.put(table)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function enrichSchema(view: View | ViewV2, tableSchema: TableSchema) {
|
||||||
|
if (!sdk.views.isV2(view)) {
|
||||||
|
return view
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...view,
|
||||||
|
schema:
|
||||||
|
!view?.columns || !Object.entries(view?.columns).length
|
||||||
|
? tableSchema
|
||||||
|
: enrichViewV2Schema(tableSchema, view.columns),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,265 @@
|
||||||
|
import { FieldType, Table, ViewV2 } from "@budibase/types"
|
||||||
|
import { generator } from "@budibase/backend-core/tests"
|
||||||
|
import { enrichSchema } from ".."
|
||||||
|
|
||||||
|
describe("table sdk", () => {
|
||||||
|
describe("enrichViewSchemas", () => {
|
||||||
|
const basicTable: Table = {
|
||||||
|
_id: generator.guid(),
|
||||||
|
name: "TestTable",
|
||||||
|
type: "table",
|
||||||
|
schema: {
|
||||||
|
name: {
|
||||||
|
type: FieldType.STRING,
|
||||||
|
name: "name",
|
||||||
|
visible: true,
|
||||||
|
width: 80,
|
||||||
|
order: 2,
|
||||||
|
constraints: {
|
||||||
|
type: "string",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
description: {
|
||||||
|
type: FieldType.STRING,
|
||||||
|
name: "description",
|
||||||
|
visible: true,
|
||||||
|
width: 200,
|
||||||
|
constraints: {
|
||||||
|
type: "string",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
id: {
|
||||||
|
type: FieldType.NUMBER,
|
||||||
|
name: "id",
|
||||||
|
visible: true,
|
||||||
|
order: 1,
|
||||||
|
constraints: {
|
||||||
|
type: "number",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
hiddenField: {
|
||||||
|
type: FieldType.STRING,
|
||||||
|
name: "hiddenField",
|
||||||
|
visible: false,
|
||||||
|
constraints: {
|
||||||
|
type: "string",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
it("should fetch the default schema if not overriden", async () => {
|
||||||
|
const tableId = basicTable._id!
|
||||||
|
const view: ViewV2 = {
|
||||||
|
version: 2,
|
||||||
|
id: generator.guid(),
|
||||||
|
name: generator.guid(),
|
||||||
|
tableId,
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = enrichSchema(view, basicTable.schema)
|
||||||
|
|
||||||
|
expect(res).toEqual({
|
||||||
|
...view,
|
||||||
|
schema: {
|
||||||
|
name: {
|
||||||
|
type: "string",
|
||||||
|
name: "name",
|
||||||
|
visible: true,
|
||||||
|
order: 2,
|
||||||
|
width: 80,
|
||||||
|
constraints: {
|
||||||
|
type: "string",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
description: {
|
||||||
|
type: "string",
|
||||||
|
name: "description",
|
||||||
|
visible: true,
|
||||||
|
width: 200,
|
||||||
|
constraints: {
|
||||||
|
type: "string",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
id: {
|
||||||
|
type: "number",
|
||||||
|
name: "id",
|
||||||
|
visible: true,
|
||||||
|
order: 1,
|
||||||
|
constraints: {
|
||||||
|
type: "number",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
hiddenField: {
|
||||||
|
type: "string",
|
||||||
|
name: "hiddenField",
|
||||||
|
visible: false,
|
||||||
|
constraints: {
|
||||||
|
type: "string",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it("if view schema only defines visiblility, 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 },
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = enrichSchema(view, basicTable.schema)
|
||||||
|
|
||||||
|
expect(res).toEqual({
|
||||||
|
...view,
|
||||||
|
schema: {
|
||||||
|
name: {
|
||||||
|
type: "string",
|
||||||
|
name: "name",
|
||||||
|
visible: true,
|
||||||
|
order: 2,
|
||||||
|
width: 80,
|
||||||
|
constraints: {
|
||||||
|
type: "string",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
id: {
|
||||||
|
type: "number",
|
||||||
|
name: "id",
|
||||||
|
visible: true,
|
||||||
|
order: 1,
|
||||||
|
constraints: {
|
||||||
|
type: "number",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it("schema does not break if the view has corrupted columns", async () => {
|
||||||
|
const tableId = basicTable._id!
|
||||||
|
const view: ViewV2 = {
|
||||||
|
version: 2,
|
||||||
|
id: generator.guid(),
|
||||||
|
name: generator.guid(),
|
||||||
|
tableId,
|
||||||
|
columns: { unnexisting: { visible: true }, name: { visible: true } },
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = enrichSchema(view, basicTable.schema)
|
||||||
|
|
||||||
|
expect(res).toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
...view,
|
||||||
|
schema: {
|
||||||
|
name: {
|
||||||
|
type: "string",
|
||||||
|
name: "name",
|
||||||
|
order: 2,
|
||||||
|
visible: true,
|
||||||
|
width: 80,
|
||||||
|
constraints: {
|
||||||
|
type: "string",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("if view schema only defines visiblility, 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 },
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = enrichSchema(view, basicTable.schema)
|
||||||
|
|
||||||
|
expect(res).toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
...view,
|
||||||
|
schema: {
|
||||||
|
name: {
|
||||||
|
type: "string",
|
||||||
|
name: "name",
|
||||||
|
order: 2,
|
||||||
|
visible: true,
|
||||||
|
width: 80,
|
||||||
|
constraints: {
|
||||||
|
type: "string",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
id: {
|
||||||
|
type: "number",
|
||||||
|
name: "id",
|
||||||
|
order: 1,
|
||||||
|
visible: true,
|
||||||
|
constraints: {
|
||||||
|
type: "number",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("if 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: {
|
||||||
|
name: { visible: true, order: 1 },
|
||||||
|
id: { visible: true },
|
||||||
|
description: { visible: false, order: 2 },
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = enrichSchema(view, basicTable.schema)
|
||||||
|
|
||||||
|
expect(res).toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
...view,
|
||||||
|
schema: {
|
||||||
|
name: {
|
||||||
|
type: "string",
|
||||||
|
name: "name",
|
||||||
|
order: 1,
|
||||||
|
visible: true,
|
||||||
|
width: 80,
|
||||||
|
constraints: {
|
||||||
|
type: "string",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
id: {
|
||||||
|
type: "number",
|
||||||
|
name: "id",
|
||||||
|
visible: true,
|
||||||
|
constraints: {
|
||||||
|
type: "number",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
|
@ -7,6 +7,17 @@ export class TableAPI extends TestAPI {
|
||||||
super(config)
|
super(config)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fetch = async (
|
||||||
|
{ expectStatus } = { expectStatus: 200 }
|
||||||
|
): Promise<Table[]> => {
|
||||||
|
const res = await this.request
|
||||||
|
.get(`/api/tables`)
|
||||||
|
.set(this.config.defaultHeaders())
|
||||||
|
.expect("Content-Type", /json/)
|
||||||
|
.expect(expectStatus)
|
||||||
|
return res.body
|
||||||
|
}
|
||||||
|
|
||||||
get = async (
|
get = async (
|
||||||
tableId: string,
|
tableId: string,
|
||||||
{ expectStatus } = { expectStatus: 200 }
|
{ expectStatus } = { expectStatus: 200 }
|
||||||
|
|
|
@ -16,23 +16,26 @@ import {
|
||||||
AutomationTrigger,
|
AutomationTrigger,
|
||||||
AutomationTriggerStepId,
|
AutomationTriggerStepId,
|
||||||
Datasource,
|
Datasource,
|
||||||
|
FieldType,
|
||||||
SourceName,
|
SourceName,
|
||||||
|
Table,
|
||||||
} from "@budibase/types"
|
} from "@budibase/types"
|
||||||
|
|
||||||
export function basicTable() {
|
export function basicTable(): Table {
|
||||||
return {
|
return {
|
||||||
name: "TestTable",
|
name: "TestTable",
|
||||||
type: "table",
|
type: "table",
|
||||||
key: "name",
|
|
||||||
schema: {
|
schema: {
|
||||||
name: {
|
name: {
|
||||||
type: "string",
|
type: FieldType.STRING,
|
||||||
|
name: "name",
|
||||||
constraints: {
|
constraints: {
|
||||||
type: "string",
|
type: "string",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
description: {
|
description: {
|
||||||
type: "string",
|
type: FieldType.STRING,
|
||||||
|
name: "description",
|
||||||
constraints: {
|
constraints: {
|
||||||
type: "string",
|
type: "string",
|
||||||
},
|
},
|
||||||
|
|
|
@ -2,3 +2,4 @@ export * from "./backup"
|
||||||
export * from "./datasource"
|
export * from "./datasource"
|
||||||
export * from "./view"
|
export * from "./view"
|
||||||
export * from "./rows"
|
export * from "./rows"
|
||||||
|
export * from "./table"
|
||||||
|
|
|
@ -0,0 +1,13 @@
|
||||||
|
import { Table, TableSchema, View, ViewV2 } from "../../../documents"
|
||||||
|
|
||||||
|
interface ViewV2Response extends ViewV2 {
|
||||||
|
schema: TableSchema
|
||||||
|
}
|
||||||
|
|
||||||
|
export type TableViewsResponse = { [key: string]: View | ViewV2Response }
|
||||||
|
|
||||||
|
export interface TableResponse extends Table {
|
||||||
|
views?: TableViewsResponse
|
||||||
|
}
|
||||||
|
|
||||||
|
export type FetchTablesResponse = TableResponse[]
|
|
@ -1,4 +1,4 @@
|
||||||
import { ViewV2 } from "../../../documents"
|
import { TableSchema, ViewV2 } from "../../../documents"
|
||||||
|
|
||||||
export interface ViewResponse {
|
export interface ViewResponse {
|
||||||
data: ViewV2
|
data: ViewV2
|
||||||
|
|
Loading…
Reference in New Issue