Merge branch 'master' into fix/internal-db-enrich-perf
This commit is contained in:
commit
33bab95117
|
@ -1,5 +1,5 @@
|
||||||
{
|
{
|
||||||
"version": "2.11.12",
|
"version": "2.11.17",
|
||||||
"npmClient": "yarn",
|
"npmClient": "yarn",
|
||||||
"packages": [
|
"packages": [
|
||||||
"packages/*"
|
"packages/*"
|
||||||
|
|
|
@ -948,12 +948,15 @@ export const buildFormSchema = (component, asset) => {
|
||||||
|
|
||||||
if (component._component.endsWith("formblock")) {
|
if (component._component.endsWith("formblock")) {
|
||||||
let schema = {}
|
let schema = {}
|
||||||
|
|
||||||
const datasource = getDatasourceForProvider(asset, component)
|
const datasource = getDatasourceForProvider(asset, component)
|
||||||
const info = getSchemaForDatasource(component, datasource)
|
const info = getSchemaForDatasource(component, datasource)
|
||||||
|
|
||||||
|
if (!info?.schema) {
|
||||||
|
return schema
|
||||||
|
}
|
||||||
|
|
||||||
if (!component.fields) {
|
if (!component.fields) {
|
||||||
Object.values(info?.schema)
|
Object.values(info.schema)
|
||||||
.filter(
|
.filter(
|
||||||
({ autocolumn, name }) =>
|
({ autocolumn, name }) =>
|
||||||
!autocolumn && !["_rev", "_id"].includes(name)
|
!autocolumn && !["_rev", "_id"].includes(name)
|
||||||
|
|
|
@ -64,6 +64,7 @@ const INITIAL_FRONTEND_STATE = {
|
||||||
},
|
},
|
||||||
features: {
|
features: {
|
||||||
componentValidation: false,
|
componentValidation: false,
|
||||||
|
disableUserMetadata: false,
|
||||||
},
|
},
|
||||||
errors: [],
|
errors: [],
|
||||||
hasAppPackage: false,
|
hasAppPackage: false,
|
||||||
|
|
|
@ -4,6 +4,7 @@
|
||||||
import { TableNames } from "constants"
|
import { TableNames } from "constants"
|
||||||
import { Grid } from "@budibase/frontend-core"
|
import { Grid } from "@budibase/frontend-core"
|
||||||
import { API } from "api"
|
import { API } from "api"
|
||||||
|
import { store } from "builderStore"
|
||||||
import GridAddColumnModal from "components/backend/DataTable/modals/grid/GridCreateColumnModal.svelte"
|
import GridAddColumnModal from "components/backend/DataTable/modals/grid/GridCreateColumnModal.svelte"
|
||||||
import GridCreateEditRowModal from "components/backend/DataTable/modals/grid/GridCreateEditRowModal.svelte"
|
import GridCreateEditRowModal from "components/backend/DataTable/modals/grid/GridCreateEditRowModal.svelte"
|
||||||
import GridEditUserModal from "components/backend/DataTable/modals/grid/GridEditUserModal.svelte"
|
import GridEditUserModal from "components/backend/DataTable/modals/grid/GridEditUserModal.svelte"
|
||||||
|
@ -17,11 +18,11 @@
|
||||||
import GridUsersTableButton from "components/backend/DataTable/modals/grid/GridUsersTableButton.svelte"
|
import GridUsersTableButton from "components/backend/DataTable/modals/grid/GridUsersTableButton.svelte"
|
||||||
|
|
||||||
const userSchemaOverrides = {
|
const userSchemaOverrides = {
|
||||||
firstName: { displayName: "First name" },
|
firstName: { displayName: "First name", disabled: true },
|
||||||
lastName: { displayName: "Last name" },
|
lastName: { displayName: "Last name", disabled: true },
|
||||||
email: { displayName: "Email" },
|
email: { displayName: "Email", disabled: true },
|
||||||
roleId: { displayName: "Role" },
|
roleId: { displayName: "Role", disabled: true },
|
||||||
status: { displayName: "Status" },
|
status: { displayName: "Status", disabled: true },
|
||||||
}
|
}
|
||||||
|
|
||||||
$: id = $tables.selected?._id
|
$: id = $tables.selected?._id
|
||||||
|
@ -60,14 +61,14 @@
|
||||||
datasource={gridDatasource}
|
datasource={gridDatasource}
|
||||||
canAddRows={!isUsersTable}
|
canAddRows={!isUsersTable}
|
||||||
canDeleteRows={!isUsersTable}
|
canDeleteRows={!isUsersTable}
|
||||||
canEditRows={!isUsersTable}
|
canEditRows={!isUsersTable || !$store.features.disableUserMetadata}
|
||||||
canEditColumns={!isUsersTable}
|
canEditColumns={!isUsersTable || !$store.features.disableUserMetadata}
|
||||||
schemaOverrides={isUsersTable ? userSchemaOverrides : null}
|
schemaOverrides={isUsersTable ? userSchemaOverrides : null}
|
||||||
showAvatars={false}
|
showAvatars={false}
|
||||||
on:updatedatasource={handleGridTableUpdate}
|
on:updatedatasource={handleGridTableUpdate}
|
||||||
>
|
>
|
||||||
<svelte:fragment slot="filter">
|
<svelte:fragment slot="filter">
|
||||||
{#if isUsersTable}
|
{#if isUsersTable && $store.features.disableUserMetadata}
|
||||||
<GridUsersTableButton />
|
<GridUsersTableButton />
|
||||||
{/if}
|
{/if}
|
||||||
<GridFilterButton />
|
<GridFilterButton />
|
||||||
|
|
|
@ -37,7 +37,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
$: datasource = getDatasourceForProvider($currentAsset, componentInstance)
|
$: datasource = getDatasourceForProvider($currentAsset, componentInstance)
|
||||||
$: resourceId = datasource.resourceId || datasource.tableId
|
$: resourceId = datasource?.resourceId || datasource?.tableId
|
||||||
|
|
||||||
$: if (!isEqual(value, cachedValue)) {
|
$: if (!isEqual(value, cachedValue)) {
|
||||||
cachedValue = cloneDeep(value)
|
cachedValue = cloneDeep(value)
|
||||||
|
|
|
@ -191,7 +191,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
const getValidOperatorsForType = filter => {
|
const getValidOperatorsForType = filter => {
|
||||||
if (!filter) {
|
if (!filter?.field) {
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
Subproject commit 7040ae5282cc23d7ae56ac1be8a369d1c32aab2f
|
Subproject commit 044bec6447066b215932d6726c437e7ec5a9e42e
|
|
@ -289,6 +289,7 @@ async function performAppCreate(ctx: UserCtx) {
|
||||||
},
|
},
|
||||||
features: {
|
features: {
|
||||||
componentValidation: true,
|
componentValidation: true,
|
||||||
|
disableUserMetadata: true,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -310,10 +311,13 @@ async function performAppCreate(ctx: UserCtx) {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
// Keep existing validation setting
|
// Keep existing feature flags
|
||||||
if (!existing.features?.componentValidation) {
|
if (!existing.features?.componentValidation) {
|
||||||
newApplication.features!.componentValidation = false
|
newApplication.features!.componentValidation = false
|
||||||
}
|
}
|
||||||
|
if (!existing.features?.disableUserMetadata) {
|
||||||
|
newApplication.features!.disableUserMetadata = false
|
||||||
|
}
|
||||||
|
|
||||||
// Migrate navigation settings and screens if required
|
// Migrate navigation settings and screens if required
|
||||||
if (existing) {
|
if (existing) {
|
||||||
|
|
|
@ -308,12 +308,19 @@ class LinkController {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
// remove schema from other table
|
try {
|
||||||
let linkedTable = await this._db.get<Table>(field.tableId)
|
// remove schema from other table, if it exists
|
||||||
if (field.fieldName) {
|
let linkedTable = await this._db.get<Table>(field.tableId)
|
||||||
delete linkedTable.schema[field.fieldName]
|
if (field.fieldName) {
|
||||||
|
delete linkedTable.schema[field.fieldName]
|
||||||
|
}
|
||||||
|
await this._db.put(linkedTable)
|
||||||
|
} catch (error: any) {
|
||||||
|
// ignore missing to ensure broken relationship columns can be deleted
|
||||||
|
if (error.statusCode !== 404) {
|
||||||
|
throw error
|
||||||
|
}
|
||||||
}
|
}
|
||||||
await this._db.put(linkedTable)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -233,4 +233,19 @@ describe("test the link controller", () => {
|
||||||
}
|
}
|
||||||
await config.updateTable(table)
|
await config.updateTable(table)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it("should be able to remove a linked field from a table, even if the linked table does not exist", async () => {
|
||||||
|
await createLinkedRow()
|
||||||
|
await createLinkedRow("link2")
|
||||||
|
table1.schema["link"].tableId = "not_found"
|
||||||
|
const controller = await createLinkController(table1, null, table1)
|
||||||
|
await context.doInAppContext(appId, async () => {
|
||||||
|
let before = await controller.getTableLinkDocs()
|
||||||
|
await controller.removeFieldFromTable("link")
|
||||||
|
let after = await controller.getTableLinkDocs()
|
||||||
|
expect(before.length).toEqual(2)
|
||||||
|
// shouldn't delete the other field
|
||||||
|
expect(after.length).toEqual(1)
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -16,6 +16,7 @@ import { cleanExportRows } from "../utils"
|
||||||
import { utils } from "@budibase/shared-core"
|
import { utils } from "@budibase/shared-core"
|
||||||
import { ExportRowsParams, ExportRowsResult } from "../search"
|
import { ExportRowsParams, ExportRowsResult } from "../search"
|
||||||
import { HTTPError, db } from "@budibase/backend-core"
|
import { HTTPError, db } from "@budibase/backend-core"
|
||||||
|
import { searchInputMapping } from "./utils"
|
||||||
import pick from "lodash/pick"
|
import pick from "lodash/pick"
|
||||||
import { outputProcessing } from "../../../../utilities/rowProcessor"
|
import { outputProcessing } from "../../../../utilities/rowProcessor"
|
||||||
|
|
||||||
|
@ -50,7 +51,10 @@ export async function search(options: SearchParams) {
|
||||||
[params.sort]: { direction },
|
[params.sort]: { direction },
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
const table = await sdk.tables.getTable(tableId)
|
||||||
|
options = searchInputMapping(table, options)
|
||||||
let rows = (await handleRequest(Operation.READ, tableId, {
|
let rows = (await handleRequest(Operation.READ, tableId, {
|
||||||
filters: query,
|
filters: query,
|
||||||
sort,
|
sort,
|
||||||
|
@ -76,7 +80,6 @@ export async function search(options: SearchParams) {
|
||||||
rows = rows.map((r: any) => pick(r, fields))
|
rows = rows.map((r: any) => pick(r, fields))
|
||||||
}
|
}
|
||||||
|
|
||||||
const table = await sdk.tables.getTable(tableId)
|
|
||||||
rows = await outputProcessing(table, rows, { preserveLinks: true })
|
rows = await outputProcessing(table, rows, { preserveLinks: true })
|
||||||
|
|
||||||
// need wrapper object for bookmarks etc when paginating
|
// need wrapper object for bookmarks etc when paginating
|
||||||
|
|
|
@ -29,6 +29,7 @@ import {
|
||||||
} from "../../../../api/controllers/view/utils"
|
} from "../../../../api/controllers/view/utils"
|
||||||
import sdk from "../../../../sdk"
|
import sdk from "../../../../sdk"
|
||||||
import { ExportRowsParams, ExportRowsResult } from "../search"
|
import { ExportRowsParams, ExportRowsResult } from "../search"
|
||||||
|
import { searchInputMapping } from "./utils"
|
||||||
import pick from "lodash/pick"
|
import pick from "lodash/pick"
|
||||||
|
|
||||||
export async function search(options: SearchParams) {
|
export async function search(options: SearchParams) {
|
||||||
|
@ -47,9 +48,9 @@ export async function search(options: SearchParams) {
|
||||||
disableEscaping: options.disableEscaping,
|
disableEscaping: options.disableEscaping,
|
||||||
}
|
}
|
||||||
|
|
||||||
let table
|
let table = await sdk.tables.getTable(tableId)
|
||||||
|
options = searchInputMapping(table, options)
|
||||||
if (params.sort && !params.sortType) {
|
if (params.sort && !params.sortType) {
|
||||||
table = await sdk.tables.getTable(tableId)
|
|
||||||
const schema = table.schema
|
const schema = table.schema
|
||||||
const sortField = schema[params.sort]
|
const sortField = schema[params.sort]
|
||||||
params.sortType = sortField.type === "number" ? "number" : "string"
|
params.sortType = sortField.type === "number" ? "number" : "string"
|
||||||
|
@ -68,7 +69,6 @@ export async function search(options: SearchParams) {
|
||||||
if (tableId === InternalTables.USER_METADATA) {
|
if (tableId === InternalTables.USER_METADATA) {
|
||||||
response.rows = await getGlobalUsersFromMetadata(response.rows)
|
response.rows = await getGlobalUsersFromMetadata(response.rows)
|
||||||
}
|
}
|
||||||
table = table || (await sdk.tables.getTable(tableId))
|
|
||||||
|
|
||||||
if (options.fields) {
|
if (options.fields) {
|
||||||
const fields = [...options.fields, ...db.CONSTANT_INTERNAL_ROW_COLS]
|
const fields = [...options.fields, ...db.CONSTANT_INTERNAL_ROW_COLS]
|
||||||
|
|
|
@ -0,0 +1,77 @@
|
||||||
|
import { searchInputMapping } from "../utils"
|
||||||
|
import { db as dbCore } from "@budibase/backend-core"
|
||||||
|
import {
|
||||||
|
FieldType,
|
||||||
|
FieldTypeSubtypes,
|
||||||
|
Table,
|
||||||
|
SearchParams,
|
||||||
|
} from "@budibase/types"
|
||||||
|
|
||||||
|
const tableId = "ta_a"
|
||||||
|
const tableWithUserCol: Table = {
|
||||||
|
_id: tableId,
|
||||||
|
name: "table",
|
||||||
|
schema: {
|
||||||
|
user: {
|
||||||
|
name: "user",
|
||||||
|
type: FieldType.BB_REFERENCE,
|
||||||
|
subtype: FieldTypeSubtypes.BB_REFERENCE.USER,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("searchInputMapping", () => {
|
||||||
|
const globalUserId = dbCore.generateGlobalUserID()
|
||||||
|
const userMedataId = dbCore.generateUserMetadataID(globalUserId)
|
||||||
|
|
||||||
|
it("should be able to map ro_ to global user IDs", () => {
|
||||||
|
const params: SearchParams = {
|
||||||
|
tableId,
|
||||||
|
query: {
|
||||||
|
equal: {
|
||||||
|
"1:user": userMedataId,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
const output = searchInputMapping(tableWithUserCol, params)
|
||||||
|
expect(output.query.equal!["1:user"]).toBe(globalUserId)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should handle array of user IDs", () => {
|
||||||
|
const params: SearchParams = {
|
||||||
|
tableId,
|
||||||
|
query: {
|
||||||
|
oneOf: {
|
||||||
|
"1:user": [userMedataId, globalUserId],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
const output = searchInputMapping(tableWithUserCol, params)
|
||||||
|
expect(output.query.oneOf!["1:user"]).toStrictEqual([
|
||||||
|
globalUserId,
|
||||||
|
globalUserId,
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
it("shouldn't change any other input", () => {
|
||||||
|
const email = "test@test.com"
|
||||||
|
const params: SearchParams = {
|
||||||
|
tableId,
|
||||||
|
query: {
|
||||||
|
equal: {
|
||||||
|
"1:user": email,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
const output = searchInputMapping(tableWithUserCol, params)
|
||||||
|
expect(output.query.equal!["1:user"]).toBe(email)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("shouldn't error if no query supplied", () => {
|
||||||
|
const params: any = {
|
||||||
|
tableId,
|
||||||
|
}
|
||||||
|
const output = searchInputMapping(tableWithUserCol, params)
|
||||||
|
expect(output.query).toBeUndefined()
|
||||||
|
})
|
||||||
|
})
|
|
@ -0,0 +1,76 @@
|
||||||
|
import {
|
||||||
|
FieldType,
|
||||||
|
FieldTypeSubtypes,
|
||||||
|
SearchParams,
|
||||||
|
Table,
|
||||||
|
DocumentType,
|
||||||
|
SEPARATOR,
|
||||||
|
} from "@budibase/types"
|
||||||
|
import { db as dbCore } from "@budibase/backend-core"
|
||||||
|
|
||||||
|
function findColumnInQueries(
|
||||||
|
column: string,
|
||||||
|
options: SearchParams,
|
||||||
|
callback: (filter: any) => any
|
||||||
|
) {
|
||||||
|
if (!options.query) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for (let filterBlock of Object.values(options.query)) {
|
||||||
|
if (typeof filterBlock !== "object") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
for (let [key, filter] of Object.entries(filterBlock)) {
|
||||||
|
if (key.endsWith(column)) {
|
||||||
|
filterBlock[key] = callback(filter)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function userColumnMapping(column: string, options: SearchParams) {
|
||||||
|
findColumnInQueries(column, options, (filterValue: any): any => {
|
||||||
|
const isArray = Array.isArray(filterValue),
|
||||||
|
isString = typeof filterValue === "string"
|
||||||
|
if (!isString && !isArray) {
|
||||||
|
return filterValue
|
||||||
|
}
|
||||||
|
const processString = (input: string) => {
|
||||||
|
const rowPrefix = DocumentType.ROW + SEPARATOR
|
||||||
|
if (input.startsWith(rowPrefix)) {
|
||||||
|
return dbCore.getGlobalIDFromUserMetadataID(input)
|
||||||
|
} else {
|
||||||
|
return input
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (isArray) {
|
||||||
|
return filterValue.map(el => {
|
||||||
|
if (typeof el === "string") {
|
||||||
|
return processString(el)
|
||||||
|
} else {
|
||||||
|
return el
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
return processString(filterValue)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// maps through the search parameters to check if any of the inputs are invalid
|
||||||
|
// based on the table schema, converts them to something that is valid.
|
||||||
|
export function searchInputMapping(table: Table, options: SearchParams) {
|
||||||
|
if (!table?.schema) {
|
||||||
|
return options
|
||||||
|
}
|
||||||
|
for (let [key, column] of Object.entries(table.schema)) {
|
||||||
|
switch (column.type) {
|
||||||
|
case FieldType.BB_REFERENCE:
|
||||||
|
if (column.subtype === FieldTypeSubtypes.BB_REFERENCE.USER) {
|
||||||
|
userColumnMapping(key, options)
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return options
|
||||||
|
}
|
|
@ -1,13 +1,15 @@
|
||||||
import { cache } from "@budibase/backend-core"
|
import { cache, db as dbCore } from "@budibase/backend-core"
|
||||||
import { utils } from "@budibase/shared-core"
|
import { utils } from "@budibase/shared-core"
|
||||||
import { FieldSubtype } from "@budibase/types"
|
import { FieldSubtype, DocumentType, SEPARATOR } from "@budibase/types"
|
||||||
import { InvalidBBRefError } from "./errors"
|
import { InvalidBBRefError } from "./errors"
|
||||||
|
|
||||||
|
const ROW_PREFIX = DocumentType.ROW + SEPARATOR
|
||||||
|
|
||||||
export async function processInputBBReferences(
|
export async function processInputBBReferences(
|
||||||
value: string | string[] | { _id: string } | { _id: string }[],
|
value: string | string[] | { _id: string } | { _id: string }[],
|
||||||
subtype: FieldSubtype
|
subtype: FieldSubtype
|
||||||
): Promise<string | null> {
|
): Promise<string | null> {
|
||||||
const referenceIds: string[] = []
|
let referenceIds: string[] = []
|
||||||
|
|
||||||
if (Array.isArray(value)) {
|
if (Array.isArray(value)) {
|
||||||
referenceIds.push(
|
referenceIds.push(
|
||||||
|
@ -26,6 +28,17 @@ export async function processInputBBReferences(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// make sure all reference IDs are correct global user IDs
|
||||||
|
// they may be user metadata references (start with row prefix)
|
||||||
|
// and these need to be converted to global IDs
|
||||||
|
referenceIds = referenceIds.map(id => {
|
||||||
|
if (id?.startsWith(ROW_PREFIX)) {
|
||||||
|
return dbCore.getGlobalIDFromUserMetadataID(id)
|
||||||
|
} else {
|
||||||
|
return id
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
switch (subtype) {
|
switch (subtype) {
|
||||||
case FieldSubtype.USER:
|
case FieldSubtype.USER:
|
||||||
const { notFoundIds } = await cache.user.getUsers(referenceIds)
|
const { notFoundIds } = await cache.user.getUsers(referenceIds)
|
||||||
|
|
|
@ -154,6 +154,15 @@ describe("bbReferenceProcessor", () => {
|
||||||
|
|
||||||
expect(result).toEqual(null)
|
expect(result).toEqual(null)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it("should convert user medata IDs to global IDs", async () => {
|
||||||
|
const userId = _.sample(users)!._id!
|
||||||
|
const userMetadataId = backendCore.db.generateUserMetadataID(userId)
|
||||||
|
const result = await config.doInTenant(() =>
|
||||||
|
processInputBBReferences(userMetadataId, FieldSubtype.USER)
|
||||||
|
)
|
||||||
|
expect(result).toBe(userId)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
@ -14,7 +14,6 @@ const HBS_REGEX = /{{([^{].*?)}}/g
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the valid operator options for a certain data type
|
* Returns the valid operator options for a certain data type
|
||||||
* @param type the data type
|
|
||||||
*/
|
*/
|
||||||
export const getValidOperatorsForType = (
|
export const getValidOperatorsForType = (
|
||||||
type: FieldType,
|
type: FieldType,
|
||||||
|
|
|
@ -66,4 +66,5 @@ export interface AppIcon {
|
||||||
|
|
||||||
export interface AppFeatures {
|
export interface AppFeatures {
|
||||||
componentValidation?: boolean
|
componentValidation?: boolean
|
||||||
|
disableUserMetadata?: boolean
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue