Merge branch 'master' into borderless-grid
This commit is contained in:
commit
9a1fd59e25
|
@ -1,5 +1,5 @@
|
||||||
{
|
{
|
||||||
"version": "2.22.17",
|
"version": "2.22.18",
|
||||||
"npmClient": "yarn",
|
"npmClient": "yarn",
|
||||||
"packages": [
|
"packages": [
|
||||||
"packages/*",
|
"packages/*",
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
Subproject commit 532c4db35cecd346b5c24f0b89ab7b397a122a36
|
Subproject commit a0ee9cad8cefb8f9f40228705711be174f018fa9
|
|
@ -72,7 +72,7 @@
|
||||||
"fast-json-patch": "^3.1.1",
|
"fast-json-patch": "^3.1.1",
|
||||||
"json-format-highlight": "^1.0.4",
|
"json-format-highlight": "^1.0.4",
|
||||||
"lodash": "4.17.21",
|
"lodash": "4.17.21",
|
||||||
"posthog-js": "^1.116.6",
|
"posthog-js": "^1.118.0",
|
||||||
"remixicon": "2.5.0",
|
"remixicon": "2.5.0",
|
||||||
"sanitize-html": "^2.7.0",
|
"sanitize-html": "^2.7.0",
|
||||||
"shortid": "2.2.15",
|
"shortid": "2.2.15",
|
||||||
|
|
|
@ -38,6 +38,10 @@ class AnalyticsHub {
|
||||||
intercom.show(user)
|
intercom.show(user)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
initPosthog() {
|
||||||
|
posthog.init()
|
||||||
|
}
|
||||||
|
|
||||||
async logout() {
|
async logout() {
|
||||||
posthog.logout()
|
posthog.logout()
|
||||||
intercom.logout()
|
intercom.logout()
|
||||||
|
|
|
@ -33,13 +33,10 @@
|
||||||
import { TOUR_STEP_KEYS } from "components/portal/onboarding/tours.js"
|
import { TOUR_STEP_KEYS } from "components/portal/onboarding/tours.js"
|
||||||
import { goto } from "@roxi/routify"
|
import { goto } from "@roxi/routify"
|
||||||
import { onMount } from "svelte"
|
import { onMount } from "svelte"
|
||||||
import PosthogClient from "../../analytics/PosthogClient"
|
|
||||||
|
|
||||||
export let application
|
export let application
|
||||||
export let loaded
|
export let loaded
|
||||||
|
|
||||||
const posthog = new PosthogClient(process.env.POSTHOG_TOKEN)
|
|
||||||
|
|
||||||
let unpublishModal
|
let unpublishModal
|
||||||
let updateAppModal
|
let updateAppModal
|
||||||
let revertModal
|
let revertModal
|
||||||
|
@ -156,7 +153,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
posthog.init()
|
analytics.initPosthog()
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
@ -5,29 +5,29 @@
|
||||||
import Provider from "./context/Provider.svelte"
|
import Provider from "./context/Provider.svelte"
|
||||||
import { onMount, getContext } from "svelte"
|
import { onMount, getContext } from "svelte"
|
||||||
import { enrichButtonActions } from "../utils/buttonActions.js"
|
import { enrichButtonActions } from "../utils/buttonActions.js"
|
||||||
|
import { memo } from "@budibase/frontend-core"
|
||||||
|
|
||||||
export let params = {}
|
export let params = {}
|
||||||
|
|
||||||
const context = getContext("context")
|
const context = getContext("context")
|
||||||
|
const onLoadActions = memo()
|
||||||
|
|
||||||
// Get the screen definition for the current route
|
// Get the screen definition for the current route
|
||||||
$: screenDefinition = $screenStore.activeScreen?.props
|
$: screenDefinition = $screenStore.activeScreen?.props
|
||||||
|
$: onLoadActions.set($screenStore.activeScreen?.onLoad)
|
||||||
$: runOnLoadActions(params)
|
$: runOnLoadActions($onLoadActions, params)
|
||||||
|
|
||||||
// Enrich and execute any on load actions.
|
// Enrich and execute any on load actions.
|
||||||
// We manually construct the full context here as this component is the
|
// We manually construct the full context here as this component is the
|
||||||
// one that provides the url context, so it is not available in $context yet
|
// one that provides the url context, so it is not available in $context yet
|
||||||
const runOnLoadActions = params => {
|
const runOnLoadActions = (actions, params) => {
|
||||||
const screenState = get(screenStore)
|
if (actions?.length && !get(builderStore).inBuilder) {
|
||||||
|
const enrichedActions = enrichButtonActions(actions, {
|
||||||
if (screenState.activeScreen?.onLoad && !get(builderStore).inBuilder) {
|
|
||||||
const actions = enrichButtonActions(screenState.activeScreen.onLoad, {
|
|
||||||
...get(context),
|
...get(context),
|
||||||
url: params,
|
url: params,
|
||||||
})
|
})
|
||||||
if (actions != null) {
|
if (enrichedActions != null) {
|
||||||
actions()
|
enrichedActions()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -84,8 +84,8 @@ export async function save(ctx: UserCtx<SaveTableRequest, SaveTableResponse>) {
|
||||||
}
|
}
|
||||||
let savedTable = await api.save(ctx, renaming)
|
let savedTable = await api.save(ctx, renaming)
|
||||||
if (!table._id) {
|
if (!table._id) {
|
||||||
await events.table.created(savedTable)
|
|
||||||
savedTable = sdk.tables.enrichViewSchemas(savedTable)
|
savedTable = sdk.tables.enrichViewSchemas(savedTable)
|
||||||
|
await events.table.created(savedTable)
|
||||||
} else {
|
} else {
|
||||||
await events.table.updated(savedTable)
|
await events.table.updated(savedTable)
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,6 +6,7 @@ import {
|
||||||
UIFieldMetadata,
|
UIFieldMetadata,
|
||||||
UpdateViewRequest,
|
UpdateViewRequest,
|
||||||
ViewResponse,
|
ViewResponse,
|
||||||
|
ViewResponseEnriched,
|
||||||
ViewV2,
|
ViewV2,
|
||||||
} from "@budibase/types"
|
} from "@budibase/types"
|
||||||
import { builderSocket, gridSocket } from "../../../websockets"
|
import { builderSocket, gridSocket } from "../../../websockets"
|
||||||
|
@ -39,9 +40,9 @@ async function parseSchema(view: CreateViewRequest) {
|
||||||
return finalViewSchema
|
return finalViewSchema
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function get(ctx: Ctx<void, ViewResponse>) {
|
export async function get(ctx: Ctx<void, ViewResponseEnriched>) {
|
||||||
ctx.body = {
|
ctx.body = {
|
||||||
data: await sdk.views.get(ctx.params.viewId, { enriched: true }),
|
data: await sdk.views.getEnriched(ctx.params.viewId),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -722,6 +722,39 @@ describe.each([
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe("bulkImport", () => {
|
||||||
|
isInternal &&
|
||||||
|
it("should update Auto ID field after bulk import", async () => {
|
||||||
|
const table = await config.api.table.save(
|
||||||
|
saveTableRequest({
|
||||||
|
primary: ["autoId"],
|
||||||
|
schema: {
|
||||||
|
autoId: {
|
||||||
|
name: "autoId",
|
||||||
|
type: FieldType.NUMBER,
|
||||||
|
subtype: AutoFieldSubType.AUTO_ID,
|
||||||
|
autocolumn: true,
|
||||||
|
constraints: {
|
||||||
|
type: "number",
|
||||||
|
presence: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
let row = await config.api.row.save(table._id!, {})
|
||||||
|
expect(row.autoId).toEqual(1)
|
||||||
|
|
||||||
|
await config.api.row.bulkImport(table._id!, {
|
||||||
|
rows: [{ autoId: 2 }],
|
||||||
|
})
|
||||||
|
|
||||||
|
row = await config.api.row.save(table._id!, {})
|
||||||
|
expect(row.autoId).toEqual(3)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
describe("enrich", () => {
|
describe("enrich", () => {
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
table = await config.api.table.save(defaultTable())
|
table = await config.api.table.save(defaultTable())
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
import { context, events } from "@budibase/backend-core"
|
import { context, events } from "@budibase/backend-core"
|
||||||
import {
|
import {
|
||||||
AutoFieldSubType,
|
AutoFieldSubType,
|
||||||
|
Datasource,
|
||||||
FieldSubtype,
|
FieldSubtype,
|
||||||
FieldType,
|
FieldType,
|
||||||
INTERNAL_TABLE_SOURCE_ID,
|
INTERNAL_TABLE_SOURCE_ID,
|
||||||
InternalTable,
|
InternalTable,
|
||||||
NumberFieldMetadata,
|
|
||||||
RelationshipType,
|
RelationshipType,
|
||||||
Row,
|
Row,
|
||||||
SaveTableRequest,
|
SaveTableRequest,
|
||||||
|
@ -13,31 +13,41 @@ import {
|
||||||
TableSourceType,
|
TableSourceType,
|
||||||
User,
|
User,
|
||||||
ViewCalculation,
|
ViewCalculation,
|
||||||
|
ViewV2Enriched,
|
||||||
} from "@budibase/types"
|
} from "@budibase/types"
|
||||||
import { checkBuilderEndpoint } from "./utilities/TestFunctions"
|
import { checkBuilderEndpoint } from "./utilities/TestFunctions"
|
||||||
import * as setup from "./utilities"
|
import * as setup from "./utilities"
|
||||||
import sdk from "../../../sdk"
|
|
||||||
import * as uuid from "uuid"
|
import * as uuid from "uuid"
|
||||||
|
|
||||||
import tk from "timekeeper"
|
import { generator } from "@budibase/backend-core/tests"
|
||||||
import { generator, mocks } from "@budibase/backend-core/tests"
|
import { DatabaseName, getDatasource } from "../../../integrations/tests/utils"
|
||||||
import { TableToBuild } from "../../../tests/utilities/TestConfiguration"
|
import { tableForDatasource } from "../../../tests/utilities/structures"
|
||||||
|
import timekeeper from "timekeeper"
|
||||||
tk.freeze(mocks.date.MOCK_DATE)
|
|
||||||
|
|
||||||
const { basicTable } = setup.structures
|
const { basicTable } = setup.structures
|
||||||
const ISO_REGEX_PATTERN = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/
|
const ISO_REGEX_PATTERN = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/
|
||||||
|
|
||||||
describe("/tables", () => {
|
describe.each([
|
||||||
let request = setup.getRequest()
|
["internal", undefined],
|
||||||
|
[DatabaseName.POSTGRES, getDatasource(DatabaseName.POSTGRES)],
|
||||||
|
[DatabaseName.MYSQL, getDatasource(DatabaseName.MYSQL)],
|
||||||
|
[DatabaseName.SQL_SERVER, getDatasource(DatabaseName.SQL_SERVER)],
|
||||||
|
[DatabaseName.MARIADB, getDatasource(DatabaseName.MARIADB)],
|
||||||
|
])("/tables (%s)", (_, dsProvider) => {
|
||||||
|
let isInternal: boolean
|
||||||
|
let datasource: Datasource | undefined
|
||||||
let config = setup.getConfig()
|
let config = setup.getConfig()
|
||||||
let appId: string
|
|
||||||
|
|
||||||
afterAll(setup.afterAll)
|
afterAll(setup.afterAll)
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
const app = await config.init()
|
await config.init()
|
||||||
appId = app.appId
|
if (dsProvider) {
|
||||||
|
datasource = await config.api.datasource.create(await dsProvider)
|
||||||
|
isInternal = false
|
||||||
|
} else {
|
||||||
|
isInternal = true
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
describe("create", () => {
|
describe("create", () => {
|
||||||
|
@ -45,102 +55,28 @@ describe("/tables", () => {
|
||||||
jest.clearAllMocks()
|
jest.clearAllMocks()
|
||||||
})
|
})
|
||||||
|
|
||||||
const createTable = (table?: Table) => {
|
it("creates a table successfully", async () => {
|
||||||
if (!table) {
|
const name = generator.guid()
|
||||||
table = basicTable()
|
const table = await config.api.table.save(
|
||||||
}
|
tableForDatasource(datasource, { name })
|
||||||
return request
|
|
||||||
.post(`/api/tables`)
|
|
||||||
.send(table)
|
|
||||||
.set(config.defaultHeaders())
|
|
||||||
.expect("Content-Type", /json/)
|
|
||||||
.expect(200)
|
|
||||||
}
|
|
||||||
|
|
||||||
it("returns a success message when the table is successfully created", async () => {
|
|
||||||
const res = await createTable()
|
|
||||||
|
|
||||||
expect((res as any).res.statusMessage).toEqual(
|
|
||||||
"Table TestTable saved successfully."
|
|
||||||
)
|
)
|
||||||
expect(res.body.name).toEqual("TestTable")
|
expect(table.name).toEqual(name)
|
||||||
expect(events.table.created).toHaveBeenCalledTimes(1)
|
expect(events.table.created).toHaveBeenCalledTimes(1)
|
||||||
expect(events.table.created).toHaveBeenCalledWith(res.body)
|
expect(events.table.created).toHaveBeenCalledWith(table)
|
||||||
})
|
|
||||||
|
|
||||||
it("creates all the passed fields", async () => {
|
|
||||||
const tableData: TableToBuild = {
|
|
||||||
name: "TestTable",
|
|
||||||
type: "table",
|
|
||||||
schema: {
|
|
||||||
autoId: {
|
|
||||||
name: "id",
|
|
||||||
type: FieldType.NUMBER,
|
|
||||||
subtype: AutoFieldSubType.AUTO_ID,
|
|
||||||
autocolumn: true,
|
|
||||||
constraints: {
|
|
||||||
type: "number",
|
|
||||||
presence: false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
views: {
|
|
||||||
"table view": {
|
|
||||||
id: "viewId",
|
|
||||||
version: 2,
|
|
||||||
name: "table view",
|
|
||||||
tableId: "tableId",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
const testTable = await config.createTable(tableData)
|
|
||||||
|
|
||||||
const expected: Table = {
|
|
||||||
...tableData,
|
|
||||||
type: "table",
|
|
||||||
views: {
|
|
||||||
"table view": {
|
|
||||||
...tableData.views!["table view"],
|
|
||||||
schema: {
|
|
||||||
autoId: {
|
|
||||||
autocolumn: true,
|
|
||||||
constraints: {
|
|
||||||
presence: false,
|
|
||||||
type: "number",
|
|
||||||
},
|
|
||||||
name: "id",
|
|
||||||
type: FieldType.NUMBER,
|
|
||||||
subtype: AutoFieldSubType.AUTO_ID,
|
|
||||||
visible: false,
|
|
||||||
} as NumberFieldMetadata,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
sourceType: TableSourceType.INTERNAL,
|
|
||||||
sourceId: expect.any(String),
|
|
||||||
_rev: expect.stringMatching(/^1-.+/),
|
|
||||||
_id: expect.any(String),
|
|
||||||
createdAt: mocks.date.MOCK_DATE.toISOString(),
|
|
||||||
updatedAt: mocks.date.MOCK_DATE.toISOString(),
|
|
||||||
}
|
|
||||||
expect(testTable).toEqual(expected)
|
|
||||||
|
|
||||||
const persistedTable = await config.api.table.get(testTable._id!)
|
|
||||||
expect(persistedTable).toEqual(expected)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it("creates a table via data import", async () => {
|
it("creates a table via data import", async () => {
|
||||||
const table: SaveTableRequest = basicTable()
|
const table: SaveTableRequest = 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 config.api.table.save(table)
|
||||||
|
|
||||||
expect(events.table.created).toHaveBeenCalledTimes(1)
|
expect(events.table.created).toHaveBeenCalledTimes(1)
|
||||||
expect(events.table.created).toHaveBeenCalledWith(res.body)
|
expect(events.table.created).toHaveBeenCalledWith(res)
|
||||||
expect(events.table.imported).toHaveBeenCalledTimes(1)
|
expect(events.table.imported).toHaveBeenCalledTimes(1)
|
||||||
expect(events.table.imported).toHaveBeenCalledWith(res.body)
|
expect(events.table.imported).toHaveBeenCalledWith(res)
|
||||||
expect(events.rows.imported).toHaveBeenCalledTimes(1)
|
expect(events.rows.imported).toHaveBeenCalledTimes(1)
|
||||||
expect(events.rows.imported).toHaveBeenCalledWith(res.body, 1)
|
expect(events.rows.imported).toHaveBeenCalledWith(res, 1)
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should apply authorization to endpoint", async () => {
|
it("should apply authorization to endpoint", async () => {
|
||||||
|
@ -155,21 +91,31 @@ describe("/tables", () => {
|
||||||
|
|
||||||
describe("update", () => {
|
describe("update", () => {
|
||||||
it("updates a table", async () => {
|
it("updates a table", async () => {
|
||||||
const testTable = await config.createTable()
|
const table = await config.api.table.save(
|
||||||
|
tableForDatasource(datasource, {
|
||||||
|
schema: {
|
||||||
|
name: {
|
||||||
|
type: FieldType.STRING,
|
||||||
|
name: "name",
|
||||||
|
constraints: {
|
||||||
|
type: "string",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
const res = await request
|
const updatedTable = await config.api.table.save({
|
||||||
.post(`/api/tables`)
|
...table,
|
||||||
.send(testTable)
|
name: generator.guid(),
|
||||||
.set(config.defaultHeaders())
|
})
|
||||||
.expect("Content-Type", /json/)
|
|
||||||
.expect(200)
|
|
||||||
|
|
||||||
expect(events.table.updated).toHaveBeenCalledTimes(1)
|
expect(events.table.updated).toHaveBeenCalledTimes(1)
|
||||||
expect(events.table.updated).toHaveBeenCalledWith(res.body)
|
expect(events.table.updated).toHaveBeenCalledWith(updatedTable)
|
||||||
})
|
})
|
||||||
|
|
||||||
it("updates all the row fields for a table when a schema key is renamed", async () => {
|
it("updates all the row fields for a table when a schema key is renamed", async () => {
|
||||||
const testTable = await config.createTable()
|
const testTable = await config.api.table.save(basicTable(datasource))
|
||||||
await config.createLegacyView({
|
await config.createLegacyView({
|
||||||
name: "TestView",
|
name: "TestView",
|
||||||
field: "Price",
|
field: "Price",
|
||||||
|
@ -179,112 +125,96 @@ describe("/tables", () => {
|
||||||
filters: [],
|
filters: [],
|
||||||
})
|
})
|
||||||
|
|
||||||
const testRow = await request
|
const testRow = await config.api.row.save(testTable._id!, {
|
||||||
.post(`/api/${testTable._id}/rows`)
|
name: "test",
|
||||||
.send({
|
})
|
||||||
name: "test",
|
|
||||||
})
|
|
||||||
.set(config.defaultHeaders())
|
|
||||||
.expect("Content-Type", /json/)
|
|
||||||
.expect(200)
|
|
||||||
|
|
||||||
const updatedTable = await request
|
const { name, ...otherColumns } = testTable.schema
|
||||||
.post(`/api/tables`)
|
const updatedTable = await config.api.table.save({
|
||||||
.send({
|
...testTable,
|
||||||
_id: testTable._id,
|
_rename: {
|
||||||
_rev: testTable._rev,
|
old: "name",
|
||||||
name: "TestTable",
|
updated: "updatedName",
|
||||||
key: "name",
|
},
|
||||||
_rename: {
|
schema: {
|
||||||
old: "name",
|
...otherColumns,
|
||||||
updated: "updatedName",
|
updatedName: {
|
||||||
|
...name,
|
||||||
|
name: "updatedName",
|
||||||
},
|
},
|
||||||
schema: {
|
},
|
||||||
updatedName: { type: "string" },
|
})
|
||||||
},
|
|
||||||
})
|
|
||||||
.set(config.defaultHeaders())
|
|
||||||
.expect("Content-Type", /json/)
|
|
||||||
.expect(200)
|
|
||||||
expect((updatedTable as any).res.statusMessage).toEqual(
|
|
||||||
"Table TestTable saved successfully."
|
|
||||||
)
|
|
||||||
expect(updatedTable.body.name).toEqual("TestTable")
|
|
||||||
|
|
||||||
const res = await request
|
expect(updatedTable.name).toEqual(testTable.name)
|
||||||
.get(`/api/${testTable._id}/rows/${testRow.body._id}`)
|
|
||||||
.set(config.defaultHeaders())
|
|
||||||
.expect("Content-Type", /json/)
|
|
||||||
.expect(200)
|
|
||||||
|
|
||||||
expect(res.body.updatedName).toEqual("test")
|
const res = await config.api.row.get(testTable._id!, testRow._id!)
|
||||||
expect(res.body.name).toBeUndefined()
|
expect(res.updatedName).toEqual("test")
|
||||||
|
expect(res.name).toBeUndefined()
|
||||||
})
|
})
|
||||||
|
|
||||||
it("updates only the passed fields", async () => {
|
it("updates only the passed fields", async () => {
|
||||||
const testTable = await config.createTable({
|
await timekeeper.withFreeze(new Date(2021, 1, 1), async () => {
|
||||||
name: "TestTable",
|
const table = await config.api.table.save(
|
||||||
type: "table",
|
tableForDatasource(datasource, {
|
||||||
schema: {
|
schema: {
|
||||||
autoId: {
|
autoId: {
|
||||||
name: "id",
|
name: "id",
|
||||||
type: FieldType.NUMBER,
|
type: FieldType.NUMBER,
|
||||||
subtype: AutoFieldSubType.AUTO_ID,
|
subtype: AutoFieldSubType.AUTO_ID,
|
||||||
autocolumn: true,
|
autocolumn: true,
|
||||||
constraints: {
|
constraints: {
|
||||||
type: "number",
|
type: "number",
|
||||||
presence: false,
|
presence: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
})
|
||||||
},
|
)
|
||||||
views: {
|
|
||||||
view1: {
|
|
||||||
id: "viewId",
|
|
||||||
version: 2,
|
|
||||||
name: "table view",
|
|
||||||
tableId: "tableId",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
const response = await request
|
const newName = generator.guid()
|
||||||
.post(`/api/tables`)
|
|
||||||
.send({
|
const updatedTable = await config.api.table.save({
|
||||||
...testTable,
|
...table,
|
||||||
name: "UpdatedName",
|
name: newName,
|
||||||
})
|
})
|
||||||
.set(config.defaultHeaders())
|
|
||||||
.expect("Content-Type", /json/)
|
|
||||||
.expect(200)
|
|
||||||
|
|
||||||
expect(response.body).toEqual({
|
let expected: Table = {
|
||||||
...testTable,
|
...table,
|
||||||
name: "UpdatedName",
|
name: newName,
|
||||||
_rev: expect.stringMatching(/^2-.+/),
|
_id: expect.any(String),
|
||||||
})
|
}
|
||||||
|
if (isInternal) {
|
||||||
|
expected._rev = expect.stringMatching(/^2-.+/)
|
||||||
|
}
|
||||||
|
|
||||||
const persistedTable = await config.api.table.get(testTable._id!)
|
expect(updatedTable).toEqual(expected)
|
||||||
expect(persistedTable).toEqual({
|
|
||||||
...testTable,
|
const persistedTable = await config.api.table.get(updatedTable._id!)
|
||||||
name: "UpdatedName",
|
expected = {
|
||||||
_rev: expect.stringMatching(/^2-.+/),
|
...table,
|
||||||
|
name: newName,
|
||||||
|
_id: updatedTable._id,
|
||||||
|
}
|
||||||
|
if (datasource?.isSQL) {
|
||||||
|
expected.sql = true
|
||||||
|
}
|
||||||
|
if (isInternal) {
|
||||||
|
expected._rev = expect.stringMatching(/^2-.+/)
|
||||||
|
}
|
||||||
|
expect(persistedTable).toEqual(expected)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe("user table", () => {
|
describe("user table", () => {
|
||||||
it("should add roleId and email field when adjusting user table schema", async () => {
|
isInternal &&
|
||||||
const res = await request
|
it("should add roleId and email field when adjusting user table schema", async () => {
|
||||||
.post(`/api/tables`)
|
const table = await config.api.table.save({
|
||||||
.send({
|
...basicTable(datasource),
|
||||||
...basicTable(),
|
|
||||||
_id: "ta_users",
|
_id: "ta_users",
|
||||||
})
|
})
|
||||||
.set(config.defaultHeaders())
|
expect(table.schema.email).toBeDefined()
|
||||||
.expect("Content-Type", /json/)
|
expect(table.schema.roleId).toBeDefined()
|
||||||
.expect(200)
|
})
|
||||||
expect(res.body.schema.email).toBeDefined()
|
|
||||||
expect(res.body.schema.roleId).toBeDefined()
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should add a new column for an internal DB table", async () => {
|
it("should add a new column for an internal DB table", async () => {
|
||||||
|
@ -295,12 +225,7 @@ describe("/tables", () => {
|
||||||
...basicTable(),
|
...basicTable(),
|
||||||
}
|
}
|
||||||
|
|
||||||
const response = await request
|
const response = await config.api.table.save(saveTableRequest)
|
||||||
.post(`/api/tables`)
|
|
||||||
.send(saveTableRequest)
|
|
||||||
.set(config.defaultHeaders())
|
|
||||||
.expect("Content-Type", /json/)
|
|
||||||
.expect(200)
|
|
||||||
|
|
||||||
const expectedResponse = {
|
const expectedResponse = {
|
||||||
...saveTableRequest,
|
...saveTableRequest,
|
||||||
|
@ -311,15 +236,16 @@ describe("/tables", () => {
|
||||||
views: {},
|
views: {},
|
||||||
}
|
}
|
||||||
delete expectedResponse._add
|
delete expectedResponse._add
|
||||||
|
expect(response).toEqual(expectedResponse)
|
||||||
expect(response.status).toBe(200)
|
|
||||||
expect(response.body).toEqual(expectedResponse)
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe("import", () => {
|
describe("import", () => {
|
||||||
it("imports rows successfully", async () => {
|
it("imports rows successfully", async () => {
|
||||||
const table = await config.createTable()
|
const name = generator.guid()
|
||||||
|
const table = await config.api.table.save(
|
||||||
|
basicTable(datasource, { name })
|
||||||
|
)
|
||||||
const importRequest = {
|
const importRequest = {
|
||||||
schema: table.schema,
|
schema: table.schema,
|
||||||
rows: [{ name: "test-name", description: "test-desc" }],
|
rows: [{ name: "test-name", description: "test-desc" }],
|
||||||
|
@ -327,83 +253,36 @@ describe("/tables", () => {
|
||||||
|
|
||||||
jest.clearAllMocks()
|
jest.clearAllMocks()
|
||||||
|
|
||||||
await request
|
await config.api.table.import(table._id!, importRequest)
|
||||||
.post(`/api/tables/${table._id}/import`)
|
|
||||||
.send(importRequest)
|
|
||||||
.set(config.defaultHeaders())
|
|
||||||
.expect("Content-Type", /json/)
|
|
||||||
.expect(200)
|
|
||||||
|
|
||||||
expect(events.table.created).not.toHaveBeenCalled()
|
expect(events.table.created).not.toHaveBeenCalled()
|
||||||
expect(events.rows.imported).toHaveBeenCalledTimes(1)
|
expect(events.rows.imported).toHaveBeenCalledTimes(1)
|
||||||
expect(events.rows.imported).toHaveBeenCalledWith(
|
expect(events.rows.imported).toHaveBeenCalledWith(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
name: "TestTable",
|
name,
|
||||||
_id: table._id,
|
_id: table._id,
|
||||||
}),
|
}),
|
||||||
1
|
1
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should update Auto ID field after bulk import", async () => {
|
|
||||||
const table = await config.createTable({
|
|
||||||
name: "TestTable",
|
|
||||||
type: "table",
|
|
||||||
schema: {
|
|
||||||
autoId: {
|
|
||||||
name: "id",
|
|
||||||
type: FieldType.NUMBER,
|
|
||||||
subtype: AutoFieldSubType.AUTO_ID,
|
|
||||||
autocolumn: true,
|
|
||||||
constraints: {
|
|
||||||
type: "number",
|
|
||||||
presence: false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
let row = await config.api.row.save(table._id!, {})
|
|
||||||
expect(row.autoId).toEqual(1)
|
|
||||||
|
|
||||||
await config.api.row.bulkImport(table._id!, {
|
|
||||||
rows: [{ autoId: 2 }],
|
|
||||||
identifierFields: [],
|
|
||||||
})
|
|
||||||
|
|
||||||
row = await config.api.row.save(table._id!, {})
|
|
||||||
expect(row.autoId).toEqual(3)
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
|
||||||
describe("fetch", () => {
|
describe("fetch", () => {
|
||||||
let testTable: Table
|
let testTable: Table
|
||||||
const enrichViewSchemasMock = jest.spyOn(sdk.tables, "enrichViewSchemas")
|
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
testTable = await config.createTable(testTable)
|
testTable = await config.api.table.save(
|
||||||
|
basicTable(datasource, { name: generator.guid() })
|
||||||
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
afterEach(() => {
|
it("returns all tables", async () => {
|
||||||
delete testTable._rev
|
const res = await config.api.table.fetch()
|
||||||
})
|
const table = res.find(t => t._id === testTable._id)
|
||||||
|
|
||||||
afterAll(() => {
|
|
||||||
enrichViewSchemasMock.mockRestore()
|
|
||||||
})
|
|
||||||
|
|
||||||
it("returns all the tables for that instance in the response body", async () => {
|
|
||||||
const res = await request
|
|
||||||
.get(`/api/tables`)
|
|
||||||
.set(config.defaultHeaders())
|
|
||||||
.expect("Content-Type", /json/)
|
|
||||||
.expect(200)
|
|
||||||
|
|
||||||
const table = res.body.find((t: Table) => t._id === testTable._id)
|
|
||||||
expect(table).toBeDefined()
|
expect(table).toBeDefined()
|
||||||
expect(table.name).toEqual(testTable.name)
|
expect(table!.name).toEqual(testTable.name)
|
||||||
expect(table.type).toEqual("table")
|
expect(table!.type).toEqual("table")
|
||||||
expect(table.sourceType).toEqual("internal")
|
expect(table!.sourceType).toEqual(testTable.sourceType)
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should apply authorization to endpoint", async () => {
|
it("should apply authorization to endpoint", async () => {
|
||||||
|
@ -414,99 +293,110 @@ describe("/tables", () => {
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should fetch views", async () => {
|
it("should enrich the view schemas", async () => {
|
||||||
const tableId = config.table!._id!
|
const viewV2 = await config.api.viewV2.create({
|
||||||
const views = [
|
tableId: testTable._id!,
|
||||||
await config.api.viewV2.create({ tableId, name: generator.guid() }),
|
name: generator.guid(),
|
||||||
await config.api.viewV2.create({ tableId, name: generator.guid() }),
|
})
|
||||||
]
|
const legacyView = await config.api.legacyView.save({
|
||||||
|
tableId: testTable._id!,
|
||||||
const res = await request
|
name: generator.guid(),
|
||||||
.get(`/api/tables`)
|
filters: [],
|
||||||
.set(config.defaultHeaders())
|
schema: {},
|
||||||
.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!
|
|
||||||
enrichViewSchemasMock.mockImplementation(t => ({
|
|
||||||
...t,
|
|
||||||
views: {
|
|
||||||
view1: {
|
|
||||||
version: 2,
|
|
||||||
name: "view1",
|
|
||||||
schema: {},
|
|
||||||
id: "new_view_id",
|
|
||||||
tableId: t._id!,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}))
|
|
||||||
|
|
||||||
await config.api.viewV2.create({ tableId, name: generator.guid() })
|
|
||||||
await config.createLegacyView()
|
|
||||||
|
|
||||||
const res = await config.api.table.fetch()
|
const res = await config.api.table.fetch()
|
||||||
|
|
||||||
expect(res).toEqual(
|
const table = res.find(t => t._id === testTable._id)
|
||||||
expect.arrayContaining([
|
expect(table).toBeDefined()
|
||||||
expect.objectContaining({
|
expect(table!.views![viewV2.name]).toBeDefined()
|
||||||
_id: tableId,
|
|
||||||
views: {
|
const expectedViewV2: ViewV2Enriched = {
|
||||||
view1: {
|
...viewV2,
|
||||||
version: 2,
|
schema: {
|
||||||
name: "view1",
|
description: {
|
||||||
schema: {},
|
constraints: {
|
||||||
id: "new_view_id",
|
type: "string",
|
||||||
tableId,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
}),
|
name: "description",
|
||||||
])
|
type: FieldType.STRING,
|
||||||
|
visible: false,
|
||||||
|
},
|
||||||
|
name: {
|
||||||
|
constraints: {
|
||||||
|
type: "string",
|
||||||
|
},
|
||||||
|
name: "name",
|
||||||
|
type: FieldType.STRING,
|
||||||
|
visible: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isInternal) {
|
||||||
|
expectedViewV2.schema!.id = {
|
||||||
|
name: "id",
|
||||||
|
type: FieldType.NUMBER,
|
||||||
|
visible: false,
|
||||||
|
autocolumn: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(table!.views![viewV2.name!]).toEqual(expectedViewV2)
|
||||||
|
|
||||||
|
if (isInternal) {
|
||||||
|
expect(table!.views![legacyView.name!]).toBeDefined()
|
||||||
|
expect(table!.views![legacyView.name!]).toEqual({
|
||||||
|
...legacyView,
|
||||||
|
schema: {
|
||||||
|
description: {
|
||||||
|
constraints: {
|
||||||
|
type: "string",
|
||||||
|
},
|
||||||
|
name: "description",
|
||||||
|
type: "string",
|
||||||
|
},
|
||||||
|
name: {
|
||||||
|
constraints: {
|
||||||
|
type: "string",
|
||||||
|
},
|
||||||
|
name: "name",
|
||||||
|
type: "string",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("get", () => {
|
||||||
|
it("returns a table", async () => {
|
||||||
|
const table = await config.api.table.save(
|
||||||
|
basicTable(datasource, { name: generator.guid() })
|
||||||
)
|
)
|
||||||
|
const res = await config.api.table.get(table._id!)
|
||||||
|
expect(res).toEqual(table)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe("indexing", () => {
|
describe("indexing", () => {
|
||||||
it("should be able to create a table with indexes", async () => {
|
it("should be able to create a table with indexes", async () => {
|
||||||
await context.doInAppContext(appId, async () => {
|
await context.doInAppContext(config.getAppId(), async () => {
|
||||||
const db = context.getAppDB()
|
const db = context.getAppDB()
|
||||||
const indexCount = (await db.getIndexes()).total_rows
|
const indexCount = (await db.getIndexes()).total_rows
|
||||||
const table = basicTable()
|
const table = basicTable()
|
||||||
table.indexes = ["name"]
|
table.indexes = ["name"]
|
||||||
const res = await request
|
const res = await config.api.table.save(table)
|
||||||
.post(`/api/tables`)
|
expect(res._id).toBeDefined()
|
||||||
.send(table)
|
expect(res._rev).toBeDefined()
|
||||||
.set(config.defaultHeaders())
|
|
||||||
.expect("Content-Type", /json/)
|
|
||||||
.expect(200)
|
|
||||||
expect(res.body._id).toBeDefined()
|
|
||||||
expect(res.body._rev).toBeDefined()
|
|
||||||
expect((await db.getIndexes()).total_rows).toEqual(indexCount + 1)
|
expect((await db.getIndexes()).total_rows).toEqual(indexCount + 1)
|
||||||
// update index to see what happens
|
// update index to see what happens
|
||||||
table.indexes = ["name", "description"]
|
table.indexes = ["name", "description"]
|
||||||
await request
|
await config.api.table.save({
|
||||||
.post(`/api/tables`)
|
...table,
|
||||||
.send({
|
_id: res._id,
|
||||||
...table,
|
_rev: res._rev,
|
||||||
_id: res.body._id,
|
})
|
||||||
_rev: res.body._rev,
|
|
||||||
})
|
|
||||||
.set(config.defaultHeaders())
|
|
||||||
.expect("Content-Type", /json/)
|
|
||||||
.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)
|
||||||
})
|
})
|
||||||
|
@ -521,12 +411,9 @@ describe("/tables", () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
it("returns a success response when a table is deleted.", async () => {
|
it("returns a success response when a table is deleted.", async () => {
|
||||||
const res = await request
|
await config.api.table.destroy(testTable._id!, testTable._rev!, {
|
||||||
.delete(`/api/tables/${testTable._id}/${testTable._rev}`)
|
body: { message: `Table ${testTable._id} deleted.` },
|
||||||
.set(config.defaultHeaders())
|
})
|
||||||
.expect("Content-Type", /json/)
|
|
||||||
.expect(200)
|
|
||||||
expect(res.body.message).toEqual(`Table ${testTable._id} deleted.`)
|
|
||||||
expect(events.table.deleted).toHaveBeenCalledTimes(1)
|
expect(events.table.deleted).toHaveBeenCalledTimes(1)
|
||||||
expect(events.table.deleted).toHaveBeenCalledWith({
|
expect(events.table.deleted).toHaveBeenCalledWith({
|
||||||
...testTable,
|
...testTable,
|
||||||
|
@ -559,12 +446,9 @@ describe("/tables", () => {
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const res = await request
|
await config.api.table.destroy(testTable._id!, testTable._rev!, {
|
||||||
.delete(`/api/tables/${testTable._id}/${testTable._rev}`)
|
body: { message: `Table ${testTable._id} deleted.` },
|
||||||
.set(config.defaultHeaders())
|
})
|
||||||
.expect("Content-Type", /json/)
|
|
||||||
.expect(200)
|
|
||||||
expect(res.body.message).toEqual(`Table ${testTable._id} deleted.`)
|
|
||||||
const dependentTable = await config.api.table.get(linkedTable._id!)
|
const dependentTable = await config.api.table.get(linkedTable._id!)
|
||||||
expect(dependentTable.schema.TestTable).not.toBeDefined()
|
expect(dependentTable.schema.TestTable).not.toBeDefined()
|
||||||
})
|
})
|
||||||
|
@ -816,33 +700,31 @@ describe("/tables", () => {
|
||||||
describe("unhappy paths", () => {
|
describe("unhappy paths", () => {
|
||||||
let table: Table
|
let table: Table
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
table = await config.api.table.save({
|
table = await config.api.table.save(
|
||||||
name: "table",
|
tableForDatasource(datasource, {
|
||||||
type: "table",
|
schema: {
|
||||||
sourceId: INTERNAL_TABLE_SOURCE_ID,
|
"user relationship": {
|
||||||
sourceType: TableSourceType.INTERNAL,
|
type: FieldType.LINK,
|
||||||
schema: {
|
fieldName: "test",
|
||||||
"user relationship": {
|
name: "user relationship",
|
||||||
type: FieldType.LINK,
|
constraints: {
|
||||||
fieldName: "test",
|
type: "array",
|
||||||
name: "user relationship",
|
presence: false,
|
||||||
constraints: {
|
},
|
||||||
type: "array",
|
relationshipType: RelationshipType.MANY_TO_ONE,
|
||||||
presence: false,
|
tableId: InternalTable.USER_METADATA,
|
||||||
},
|
},
|
||||||
relationshipType: RelationshipType.MANY_TO_ONE,
|
num: {
|
||||||
tableId: InternalTable.USER_METADATA,
|
type: FieldType.NUMBER,
|
||||||
},
|
name: "num",
|
||||||
num: {
|
constraints: {
|
||||||
type: FieldType.NUMBER,
|
type: "number",
|
||||||
name: "num",
|
presence: false,
|
||||||
constraints: {
|
},
|
||||||
type: "number",
|
|
||||||
presence: false,
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
})
|
||||||
})
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should fail if the new column name is blank", async () => {
|
it("should fail if the new column name is blank", async () => {
|
||||||
|
|
|
@ -181,7 +181,7 @@ describe.each([
|
||||||
|
|
||||||
const createdView = await config.api.viewV2.create(newView)
|
const createdView = await config.api.viewV2.create(newView)
|
||||||
|
|
||||||
expect(await config.api.viewV2.get(createdView.id)).toEqual({
|
expect(createdView).toEqual({
|
||||||
...newView,
|
...newView,
|
||||||
schema: {
|
schema: {
|
||||||
Price: {
|
Price: {
|
||||||
|
@ -398,7 +398,7 @@ describe.each([
|
||||||
})
|
})
|
||||||
|
|
||||||
it("updates only UI schema overrides", async () => {
|
it("updates only UI schema overrides", async () => {
|
||||||
await config.api.viewV2.update({
|
const updatedView = await config.api.viewV2.update({
|
||||||
...view,
|
...view,
|
||||||
schema: {
|
schema: {
|
||||||
Price: {
|
Price: {
|
||||||
|
@ -417,7 +417,7 @@ describe.each([
|
||||||
} as Record<string, FieldSchema>,
|
} as Record<string, FieldSchema>,
|
||||||
})
|
})
|
||||||
|
|
||||||
expect(await config.api.viewV2.get(view.id)).toEqual({
|
expect(updatedView).toEqual({
|
||||||
...view,
|
...view,
|
||||||
schema: {
|
schema: {
|
||||||
Price: {
|
Price: {
|
||||||
|
@ -479,17 +479,17 @@ describe.each([
|
||||||
|
|
||||||
describe("fetch view (through table)", () => {
|
describe("fetch view (through table)", () => {
|
||||||
it("should be able to fetch a view V2", async () => {
|
it("should be able to fetch a view V2", async () => {
|
||||||
const newView: CreateViewRequest = {
|
const res = await config.api.viewV2.create({
|
||||||
name: generator.name(),
|
name: generator.name(),
|
||||||
tableId: table._id!,
|
tableId: table._id!,
|
||||||
schema: {
|
schema: {
|
||||||
Price: { visible: false },
|
Price: { visible: false },
|
||||||
Category: { visible: true },
|
Category: { visible: true },
|
||||||
},
|
},
|
||||||
}
|
})
|
||||||
const res = await config.api.viewV2.create(newView)
|
expect(res.schema?.Price).toBeUndefined()
|
||||||
|
|
||||||
const view = await config.api.viewV2.get(res.id)
|
const view = await config.api.viewV2.get(res.id)
|
||||||
expect(view!.schema?.Price).toBeUndefined()
|
|
||||||
const updatedTable = await config.api.table.get(table._id!)
|
const updatedTable = await config.api.table.get(table._id!)
|
||||||
const viewSchema = updatedTable.views![view!.name!].schema as Record<
|
const viewSchema = updatedTable.views![view!.name!].schema as Record<
|
||||||
string,
|
string,
|
||||||
|
|
|
@ -224,12 +224,12 @@ class SqlTableQueryBuilder {
|
||||||
const tableName = schemaName
|
const tableName = schemaName
|
||||||
? `\`${schemaName}\`.\`${json.table.name}\``
|
? `\`${schemaName}\`.\`${json.table.name}\``
|
||||||
: `\`${json.table.name}\``
|
: `\`${json.table.name}\``
|
||||||
const externalType = json.table.schema[updatedColumn].externalType!
|
|
||||||
return {
|
return {
|
||||||
sql: `alter table ${tableName} change column \`${json.meta.renamed.old}\` \`${updatedColumn}\` ${externalType};`,
|
sql: `alter table ${tableName} rename column \`${json.meta.renamed.old}\` to \`${updatedColumn}\`;`,
|
||||||
bindings: [],
|
bindings: [],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
query = buildUpdateTable(
|
query = buildUpdateTable(
|
||||||
client,
|
client,
|
||||||
json.table,
|
json.table,
|
||||||
|
@ -237,6 +237,27 @@ class SqlTableQueryBuilder {
|
||||||
json.meta.table,
|
json.meta.table,
|
||||||
json.meta.renamed!
|
json.meta.renamed!
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// renameColumn for SQL Server returns a parameterised `sp_rename` query,
|
||||||
|
// which is not supported by SQL Server and gives a syntax error.
|
||||||
|
if (this.sqlClient === SqlClient.MS_SQL && json.meta.renamed) {
|
||||||
|
const oldColumn = json.meta.renamed.old
|
||||||
|
const updatedColumn = json.meta.renamed.updated
|
||||||
|
const tableName = schemaName
|
||||||
|
? `${schemaName}.${json.table.name}`
|
||||||
|
: `${json.table.name}`
|
||||||
|
const sql = query.toSQL()
|
||||||
|
if (Array.isArray(sql)) {
|
||||||
|
for (const query of sql) {
|
||||||
|
if (query.sql.startsWith("exec sp_rename")) {
|
||||||
|
query.sql = `exec sp_rename '${tableName}.${oldColumn}', '${updatedColumn}', 'COLUMN'`
|
||||||
|
query.bindings = []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return sql
|
||||||
|
}
|
||||||
break
|
break
|
||||||
case Operation.DELETE_TABLE:
|
case Operation.DELETE_TABLE:
|
||||||
query = buildDeleteTable(client, json.table)
|
query = buildDeleteTable(client, json.table)
|
||||||
|
|
|
@ -722,7 +722,7 @@ describe("SQL query builder", () => {
|
||||||
})
|
})
|
||||||
expect(query).toEqual({
|
expect(query).toEqual({
|
||||||
bindings: [],
|
bindings: [],
|
||||||
sql: `alter table \`${TABLE_NAME}\` change column \`name\` \`first_name\` varchar(45);`,
|
sql: `alter table \`${TABLE_NAME}\` rename column \`name\` to \`first_name\`;`,
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
@ -48,6 +48,18 @@ export async function save(
|
||||||
oldTable = await getTable(tableId)
|
oldTable = await getTable(tableId)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
!oldTable &&
|
||||||
|
(tableToSave.primary == null || tableToSave.primary.length === 0)
|
||||||
|
) {
|
||||||
|
tableToSave.primary = ["id"]
|
||||||
|
tableToSave.schema.id = {
|
||||||
|
type: FieldType.NUMBER,
|
||||||
|
autocolumn: true,
|
||||||
|
name: "id",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (hasTypeChanged(tableToSave, oldTable)) {
|
if (hasTypeChanged(tableToSave, oldTable)) {
|
||||||
throw new Error("A column type has changed.")
|
throw new Error("A column type has changed.")
|
||||||
}
|
}
|
||||||
|
@ -183,6 +195,10 @@ export async function save(
|
||||||
// that the datasource definition changed
|
// that the datasource definition changed
|
||||||
const updatedDatasource = await datasourceSdk.get(datasource._id!)
|
const updatedDatasource = await datasourceSdk.get(datasource._id!)
|
||||||
|
|
||||||
|
if (updatedDatasource.isSQL) {
|
||||||
|
tableToSave.sql = true
|
||||||
|
}
|
||||||
|
|
||||||
return { datasource: updatedDatasource, table: tableToSave }
|
return { datasource: updatedDatasource, table: tableToSave }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -142,7 +142,9 @@ export function enrichViewSchemas(table: Table): TableResponse {
|
||||||
return {
|
return {
|
||||||
...table,
|
...table,
|
||||||
views: Object.values(table.views ?? [])
|
views: Object.values(table.views ?? [])
|
||||||
.map(v => sdk.views.enrichSchema(v, table.schema))
|
.map(v =>
|
||||||
|
sdk.views.isV2(v) ? sdk.views.enrichSchema(v, table.schema) : v
|
||||||
|
)
|
||||||
.reduce((p, v) => {
|
.reduce((p, v) => {
|
||||||
p[v.name!] = v
|
p[v.name!] = v
|
||||||
return p
|
return p
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { ViewV2 } from "@budibase/types"
|
import { ViewV2, ViewV2Enriched } from "@budibase/types"
|
||||||
import { context, HTTPError } from "@budibase/backend-core"
|
import { context, HTTPError } from "@budibase/backend-core"
|
||||||
|
|
||||||
import sdk from "../../../sdk"
|
import sdk from "../../../sdk"
|
||||||
|
@ -6,26 +6,34 @@ import * as utils from "../../../db/utils"
|
||||||
import { enrichSchema, isV2 } from "."
|
import { enrichSchema, isV2 } from "."
|
||||||
import { breakExternalTableId } from "../../../integrations/utils"
|
import { breakExternalTableId } from "../../../integrations/utils"
|
||||||
|
|
||||||
export async function get(
|
export async function get(viewId: string): Promise<ViewV2> {
|
||||||
viewId: string,
|
|
||||||
opts?: { enriched: boolean }
|
|
||||||
): Promise<ViewV2> {
|
|
||||||
const { tableId } = utils.extractViewInfoFromID(viewId)
|
const { tableId } = utils.extractViewInfoFromID(viewId)
|
||||||
|
|
||||||
const { datasourceId, tableName } = breakExternalTableId(tableId)
|
const { datasourceId, tableName } = breakExternalTableId(tableId)
|
||||||
const ds = await sdk.datasources.get(datasourceId!)
|
const ds = await sdk.datasources.get(datasourceId!)
|
||||||
|
|
||||||
const table = ds.entities![tableName!]
|
const table = ds.entities![tableName!]
|
||||||
const views = Object.values(table.views!)
|
const views = Object.values(table.views!).filter(isV2)
|
||||||
const found = views.find(v => isV2(v) && v.id === viewId)
|
const found = views.find(v => v.id === viewId)
|
||||||
if (!found) {
|
if (!found) {
|
||||||
throw new Error("No view found")
|
throw new Error("No view found")
|
||||||
}
|
}
|
||||||
if (opts?.enriched) {
|
return found
|
||||||
return enrichSchema(found, table.schema) as ViewV2
|
}
|
||||||
} else {
|
|
||||||
return found as ViewV2
|
export async function getEnriched(viewId: string): Promise<ViewV2Enriched> {
|
||||||
|
const { tableId } = utils.extractViewInfoFromID(viewId)
|
||||||
|
|
||||||
|
const { datasourceId, tableName } = breakExternalTableId(tableId)
|
||||||
|
const ds = await sdk.datasources.get(datasourceId!)
|
||||||
|
|
||||||
|
const table = ds.entities![tableName!]
|
||||||
|
const views = Object.values(table.views!).filter(isV2)
|
||||||
|
const found = views.find(v => v.id === viewId)
|
||||||
|
if (!found) {
|
||||||
|
throw new Error("No view found")
|
||||||
}
|
}
|
||||||
|
return enrichSchema(found, table.schema)
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function create(
|
export async function create(
|
||||||
|
|
|
@ -1,8 +1,13 @@
|
||||||
import { RenameColumn, TableSchema, View, ViewV2 } from "@budibase/types"
|
import {
|
||||||
|
RenameColumn,
|
||||||
|
TableSchema,
|
||||||
|
View,
|
||||||
|
ViewV2,
|
||||||
|
ViewV2Enriched,
|
||||||
|
} from "@budibase/types"
|
||||||
import { db as dbCore } from "@budibase/backend-core"
|
import { db as dbCore } from "@budibase/backend-core"
|
||||||
import { cloneDeep } from "lodash"
|
import { cloneDeep } from "lodash"
|
||||||
|
|
||||||
import sdk from "../../../sdk"
|
|
||||||
import * as utils from "../../../db/utils"
|
import * as utils from "../../../db/utils"
|
||||||
import { isExternalTableID } from "../../../integrations/utils"
|
import { isExternalTableID } from "../../../integrations/utils"
|
||||||
|
|
||||||
|
@ -16,12 +21,14 @@ function pickApi(tableId: any) {
|
||||||
return internal
|
return internal
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function get(
|
export async function get(viewId: string): Promise<ViewV2> {
|
||||||
viewId: string,
|
|
||||||
opts?: { enriched: boolean }
|
|
||||||
): Promise<ViewV2> {
|
|
||||||
const { tableId } = utils.extractViewInfoFromID(viewId)
|
const { tableId } = utils.extractViewInfoFromID(viewId)
|
||||||
return pickApi(tableId).get(viewId, opts)
|
return pickApi(tableId).get(viewId)
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getEnriched(viewId: string): Promise<ViewV2Enriched> {
|
||||||
|
const { tableId } = utils.extractViewInfoFromID(viewId)
|
||||||
|
return pickApi(tableId).getEnriched(viewId)
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function create(
|
export async function create(
|
||||||
|
@ -52,11 +59,10 @@ export function allowedFields(view: View | ViewV2) {
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
export function enrichSchema(view: View | ViewV2, tableSchema: TableSchema) {
|
export function enrichSchema(
|
||||||
if (!sdk.views.isV2(view)) {
|
view: ViewV2,
|
||||||
return view
|
tableSchema: TableSchema
|
||||||
}
|
): ViewV2Enriched {
|
||||||
|
|
||||||
let schema = cloneDeep(tableSchema)
|
let schema = cloneDeep(tableSchema)
|
||||||
const anyViewOrder = Object.values(view.schema || {}).some(
|
const anyViewOrder = Object.values(view.schema || {}).some(
|
||||||
ui => ui.order != null
|
ui => ui.order != null
|
||||||
|
|
|
@ -1,26 +1,30 @@
|
||||||
import { ViewV2 } from "@budibase/types"
|
import { ViewV2, ViewV2Enriched } from "@budibase/types"
|
||||||
import { context, HTTPError } from "@budibase/backend-core"
|
import { context, HTTPError } from "@budibase/backend-core"
|
||||||
|
|
||||||
import sdk from "../../../sdk"
|
import sdk from "../../../sdk"
|
||||||
import * as utils from "../../../db/utils"
|
import * as utils from "../../../db/utils"
|
||||||
import { enrichSchema, isV2 } from "."
|
import { enrichSchema, isV2 } from "."
|
||||||
|
|
||||||
export async function get(
|
export async function get(viewId: string): Promise<ViewV2> {
|
||||||
viewId: string,
|
|
||||||
opts?: { enriched: boolean }
|
|
||||||
): Promise<ViewV2> {
|
|
||||||
const { tableId } = utils.extractViewInfoFromID(viewId)
|
const { tableId } = utils.extractViewInfoFromID(viewId)
|
||||||
const table = await sdk.tables.getTable(tableId)
|
const table = await sdk.tables.getTable(tableId)
|
||||||
const views = Object.values(table.views!)
|
const views = Object.values(table.views!).filter(isV2)
|
||||||
const found = views.find(v => isV2(v) && v.id === viewId)
|
const found = views.find(v => v.id === viewId)
|
||||||
if (!found) {
|
if (!found) {
|
||||||
throw new Error("No view found")
|
throw new Error("No view found")
|
||||||
}
|
}
|
||||||
if (opts?.enriched) {
|
return found
|
||||||
return enrichSchema(found, table.schema) as ViewV2
|
}
|
||||||
} else {
|
|
||||||
return found as ViewV2
|
export async function getEnriched(viewId: string): Promise<ViewV2Enriched> {
|
||||||
|
const { tableId } = utils.extractViewInfoFromID(viewId)
|
||||||
|
const table = await sdk.tables.getTable(tableId)
|
||||||
|
const views = Object.values(table.views!).filter(isV2)
|
||||||
|
const found = views.find(v => v.id === viewId)
|
||||||
|
if (!found) {
|
||||||
|
throw new Error("No view found")
|
||||||
}
|
}
|
||||||
|
return enrichSchema(found, table.schema)
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function create(
|
export async function create(
|
||||||
|
|
|
@ -1,4 +1,6 @@
|
||||||
import {
|
import {
|
||||||
|
BulkImportRequest,
|
||||||
|
BulkImportResponse,
|
||||||
MigrateRequest,
|
MigrateRequest,
|
||||||
MigrateResponse,
|
MigrateResponse,
|
||||||
SaveTableRequest,
|
SaveTableRequest,
|
||||||
|
@ -39,4 +41,28 @@ export class TableAPI extends TestAPI {
|
||||||
expectations,
|
expectations,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
import = async (
|
||||||
|
tableId: string,
|
||||||
|
data: BulkImportRequest,
|
||||||
|
expectations?: Expectations
|
||||||
|
): Promise<BulkImportResponse> => {
|
||||||
|
return await this._post<BulkImportResponse>(
|
||||||
|
`/api/tables/${tableId}/import`,
|
||||||
|
{
|
||||||
|
body: data,
|
||||||
|
expectations,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
destroy = async (
|
||||||
|
tableId: string,
|
||||||
|
revId: string,
|
||||||
|
expectations?: Expectations
|
||||||
|
): Promise<void> => {
|
||||||
|
return await this._delete<void>(`/api/tables/${tableId}/${revId}`, {
|
||||||
|
expectations,
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,9 +4,9 @@ import {
|
||||||
ViewV2,
|
ViewV2,
|
||||||
SearchViewRowRequest,
|
SearchViewRowRequest,
|
||||||
PaginatedSearchRowResponse,
|
PaginatedSearchRowResponse,
|
||||||
|
ViewResponseEnriched,
|
||||||
} from "@budibase/types"
|
} from "@budibase/types"
|
||||||
import { Expectations, TestAPI } from "./base"
|
import { Expectations, TestAPI } from "./base"
|
||||||
import sdk from "../../../sdk"
|
|
||||||
|
|
||||||
export class ViewV2API extends TestAPI {
|
export class ViewV2API extends TestAPI {
|
||||||
create = async (
|
create = async (
|
||||||
|
@ -45,9 +45,8 @@ export class ViewV2API extends TestAPI {
|
||||||
}
|
}
|
||||||
|
|
||||||
get = async (viewId: string) => {
|
get = async (viewId: string) => {
|
||||||
return await this.config.doInContext(this.config.appId, () =>
|
return (await this._get<ViewResponseEnriched>(`/api/v2/views/${viewId}`))
|
||||||
sdk.views.get(viewId)
|
.data
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
search = async (
|
search = async (
|
||||||
|
|
|
@ -26,32 +26,56 @@ import {
|
||||||
WebhookActionType,
|
WebhookActionType,
|
||||||
} from "@budibase/types"
|
} from "@budibase/types"
|
||||||
import { LoopInput, LoopStepType } from "../../definitions/automations"
|
import { LoopInput, LoopStepType } from "../../definitions/automations"
|
||||||
|
import { merge } from "lodash"
|
||||||
|
import { generator } from "@budibase/backend-core/tests"
|
||||||
|
|
||||||
const { BUILTIN_ROLE_IDS } = roles
|
const { BUILTIN_ROLE_IDS } = roles
|
||||||
|
|
||||||
export function basicTable(): Table {
|
export function tableForDatasource(
|
||||||
return {
|
datasource?: Datasource,
|
||||||
name: "TestTable",
|
...extra: Partial<Table>[]
|
||||||
type: "table",
|
): Table {
|
||||||
sourceId: INTERNAL_TABLE_SOURCE_ID,
|
return merge(
|
||||||
sourceType: TableSourceType.INTERNAL,
|
{
|
||||||
schema: {
|
name: generator.guid(),
|
||||||
name: {
|
type: "table",
|
||||||
type: FieldType.STRING,
|
sourceType: datasource
|
||||||
name: "name",
|
? TableSourceType.EXTERNAL
|
||||||
constraints: {
|
: TableSourceType.INTERNAL,
|
||||||
type: "string",
|
sourceId: datasource ? datasource._id! : INTERNAL_TABLE_SOURCE_ID,
|
||||||
|
schema: {},
|
||||||
|
},
|
||||||
|
...extra
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function basicTable(
|
||||||
|
datasource?: Datasource,
|
||||||
|
...extra: Partial<Table>[]
|
||||||
|
): Table {
|
||||||
|
return tableForDatasource(
|
||||||
|
datasource,
|
||||||
|
{
|
||||||
|
name: "TestTable",
|
||||||
|
schema: {
|
||||||
|
name: {
|
||||||
|
type: FieldType.STRING,
|
||||||
|
name: "name",
|
||||||
|
constraints: {
|
||||||
|
type: "string",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
description: {
|
||||||
description: {
|
type: FieldType.STRING,
|
||||||
type: FieldType.STRING,
|
name: "description",
|
||||||
name: "description",
|
constraints: {
|
||||||
constraints: {
|
type: "string",
|
||||||
type: "string",
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
...extra
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function basicView(tableId: string) {
|
export function basicView(tableId: string) {
|
||||||
|
|
|
@ -3,16 +3,11 @@ import {
|
||||||
Row,
|
Row,
|
||||||
Table,
|
Table,
|
||||||
TableRequest,
|
TableRequest,
|
||||||
TableSchema,
|
|
||||||
View,
|
View,
|
||||||
ViewV2,
|
ViewV2Enriched,
|
||||||
} from "../../../documents"
|
} from "../../../documents"
|
||||||
|
|
||||||
interface ViewV2Response extends ViewV2 {
|
export type TableViewsResponse = { [key: string]: View | ViewV2Enriched }
|
||||||
schema: TableSchema
|
|
||||||
}
|
|
||||||
|
|
||||||
export type TableViewsResponse = { [key: string]: View | ViewV2Response }
|
|
||||||
|
|
||||||
export interface TableResponse extends Table {
|
export interface TableResponse extends Table {
|
||||||
views?: TableViewsResponse
|
views?: TableViewsResponse
|
||||||
|
|
|
@ -1,14 +1,13 @@
|
||||||
import { ViewV2, UIFieldMetadata } from "../../../documents"
|
import { ViewV2, ViewV2Enriched } from "../../../documents"
|
||||||
|
|
||||||
export interface ViewResponse {
|
export interface ViewResponse {
|
||||||
data: ViewV2
|
data: ViewV2
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CreateViewRequest
|
export interface ViewResponseEnriched {
|
||||||
extends Omit<ViewV2, "version" | "id" | "schema"> {
|
data: ViewV2Enriched
|
||||||
schema?: Record<string, UIFieldMetadata>
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface UpdateViewRequest extends Omit<ViewV2, "schema"> {
|
export interface CreateViewRequest extends Omit<ViewV2, "version" | "id"> {}
|
||||||
schema?: Record<string, UIFieldMetadata>
|
|
||||||
}
|
export interface UpdateViewRequest extends ViewV2 {}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { SearchFilter, SortOrder, SortType } from "../../api"
|
import { SearchFilter, SortOrder, SortType } from "../../api"
|
||||||
import { UIFieldMetadata } from "./table"
|
import { TableSchema, UIFieldMetadata } from "./table"
|
||||||
import { Document } from "../document"
|
import { Document } from "../document"
|
||||||
import { DBView } from "../../sdk"
|
import { DBView } from "../../sdk"
|
||||||
|
|
||||||
|
@ -48,6 +48,10 @@ export interface ViewV2 {
|
||||||
schema?: Record<string, UIFieldMetadata>
|
schema?: Record<string, UIFieldMetadata>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ViewV2Enriched extends ViewV2 {
|
||||||
|
schema?: TableSchema
|
||||||
|
}
|
||||||
|
|
||||||
export type ViewSchema = ViewCountOrSumSchema | ViewStatisticsSchema
|
export type ViewSchema = ViewCountOrSumSchema | ViewStatisticsSchema
|
||||||
|
|
||||||
export interface ViewCountOrSumSchema {
|
export interface ViewCountOrSumSchema {
|
||||||
|
|
Loading…
Reference in New Issue