Merge branch 'master' into feature/sqs-table-cleanup

This commit is contained in:
Michael Drury 2024-05-15 12:34:12 +01:00 committed by GitHub
commit 1c22c7d2d1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
18 changed files with 650 additions and 231 deletions

View File

@ -1,5 +1,5 @@
{ {
"version": "2.26.2", "version": "2.26.3",
"npmClient": "yarn", "npmClient": "yarn",
"packages": [ "packages": [
"packages/*", "packages/*",

View File

@ -398,44 +398,50 @@
if (!externalTable) { if (!externalTable) {
return [ return [
FIELDS.STRING, FIELDS.STRING,
FIELDS.BARCODEQR, FIELDS.NUMBER,
FIELDS.LONGFORM,
FIELDS.OPTIONS, FIELDS.OPTIONS,
FIELDS.ARRAY, FIELDS.ARRAY,
FIELDS.NUMBER,
FIELDS.BIGINT,
FIELDS.BOOLEAN, FIELDS.BOOLEAN,
FIELDS.DATETIME, FIELDS.DATETIME,
FIELDS.ATTACHMENT_SINGLE,
FIELDS.ATTACHMENTS,
FIELDS.LINK, FIELDS.LINK,
FIELDS.FORMULA, FIELDS.LONGFORM,
FIELDS.JSON,
FIELDS.USER, FIELDS.USER,
FIELDS.USERS, FIELDS.USERS,
FIELDS.ATTACHMENT_SINGLE,
FIELDS.ATTACHMENTS,
FIELDS.FORMULA,
FIELDS.JSON,
FIELDS.BARCODEQR,
FIELDS.BIGINT,
FIELDS.AUTO, FIELDS.AUTO,
] ]
} else { } else {
let fields = [ let fields = [
FIELDS.STRING, FIELDS.STRING,
FIELDS.BARCODEQR,
FIELDS.LONGFORM,
FIELDS.OPTIONS,
FIELDS.DATETIME,
FIELDS.NUMBER, FIELDS.NUMBER,
FIELDS.OPTIONS,
FIELDS.ARRAY,
FIELDS.BOOLEAN, FIELDS.BOOLEAN,
FIELDS.FORMULA, FIELDS.DATETIME,
FIELDS.BIGINT, FIELDS.LINK,
FIELDS.LONGFORM,
FIELDS.USER, FIELDS.USER,
FIELDS.USERS,
FIELDS.FORMULA,
FIELDS.BARCODEQR,
FIELDS.BIGINT,
] ]
if (datasource && datasource.source !== SourceName.GOOGLE_SHEETS) { // Filter out multiple users for google sheets
fields.push(FIELDS.USERS) if (datasource?.source === SourceName.GOOGLE_SHEETS) {
fields = fields.filter(x => x !== FIELDS.USERS)
} }
// no-sql or a spreadsheet
if (!externalTable || table.sql) { // Filter out SQL-specific types for non-SQL datasources
fields = [...fields, FIELDS.LINK, FIELDS.ARRAY] if (!table.sql) {
fields = fields.filter(x => x !== FIELDS.LINK && x !== FIELDS.ARRAY)
} }
return fields return fields
} }
} }

View File

@ -2,21 +2,21 @@
import { Modal, ModalContent } from "@budibase/bbui" import { Modal, ModalContent } from "@budibase/bbui"
import FreeTrial from "../../../../assets/FreeTrial.svelte" import FreeTrial from "../../../../assets/FreeTrial.svelte"
import { get } from "svelte/store" import { get } from "svelte/store"
import { auth, licensing } from "stores/portal" import { auth, licensing, admin } from "stores/portal"
import { API } from "api" import { API } from "api"
import { PlanType } from "@budibase/types" import { PlanType } from "@budibase/types"
import { sdk } from "@budibase/shared-core"
let freeTrialModal let freeTrialModal
$: planType = $licensing?.license?.plan?.type $: planType = $licensing?.license?.plan?.type
$: showFreeTrialModal(planType, freeTrialModal) $: showFreeTrialModal(planType, freeTrialModal)
$: isOwner = $auth.accountPortalAccess && $admin.cloud
const showFreeTrialModal = (planType, freeTrialModal) => { const showFreeTrialModal = (planType, freeTrialModal) => {
if ( if (
planType === PlanType.ENTERPRISE_BASIC_TRIAL && planType === PlanType.ENTERPRISE_BASIC_TRIAL &&
!$auth.user?.freeTrialConfirmedAt && !$auth.user?.freeTrialConfirmedAt &&
sdk.users.isAdmin($auth.user) isOwner
) { ) {
freeTrialModal?.show() freeTrialModal?.show()
} }

View File

@ -33,7 +33,7 @@ export const FIELDS = {
}, },
}, },
BARCODEQR: { BARCODEQR: {
name: "Barcode/QR", name: "Barcode / QR",
type: FieldType.BARCODEQR, type: FieldType.BARCODEQR,
icon: TypeIconMap[FieldType.BARCODEQR], icon: TypeIconMap[FieldType.BARCODEQR],
constraints: { constraints: {
@ -43,7 +43,7 @@ export const FIELDS = {
}, },
}, },
LONGFORM: { LONGFORM: {
name: "Long Form Text", name: "Long form text",
type: FieldType.LONGFORM, type: FieldType.LONGFORM,
icon: TypeIconMap[FieldType.LONGFORM], icon: TypeIconMap[FieldType.LONGFORM],
constraints: { constraints: {
@ -53,7 +53,7 @@ export const FIELDS = {
}, },
}, },
OPTIONS: { OPTIONS: {
name: "Options", name: "Single select",
type: FieldType.OPTIONS, type: FieldType.OPTIONS,
icon: TypeIconMap[FieldType.OPTIONS], icon: TypeIconMap[FieldType.OPTIONS],
constraints: { constraints: {
@ -63,7 +63,7 @@ export const FIELDS = {
}, },
}, },
ARRAY: { ARRAY: {
name: "Multi-select", name: "Multi select",
type: FieldType.ARRAY, type: FieldType.ARRAY,
icon: TypeIconMap[FieldType.ARRAY], icon: TypeIconMap[FieldType.ARRAY],
constraints: { constraints: {
@ -83,7 +83,7 @@ export const FIELDS = {
}, },
}, },
BIGINT: { BIGINT: {
name: "BigInt", name: "Big integer",
type: FieldType.BIGINT, type: FieldType.BIGINT,
icon: TypeIconMap[FieldType.BIGINT], icon: TypeIconMap[FieldType.BIGINT],
}, },
@ -97,7 +97,7 @@ export const FIELDS = {
}, },
}, },
DATETIME: { DATETIME: {
name: "Date/Time", name: "Date / time",
type: FieldType.DATETIME, type: FieldType.DATETIME,
icon: TypeIconMap[FieldType.DATETIME], icon: TypeIconMap[FieldType.DATETIME],
constraints: { constraints: {
@ -111,7 +111,7 @@ export const FIELDS = {
}, },
}, },
ATTACHMENT_SINGLE: { ATTACHMENT_SINGLE: {
name: "Attachment", name: "Single attachment",
type: FieldType.ATTACHMENT_SINGLE, type: FieldType.ATTACHMENT_SINGLE,
icon: TypeIconMap[FieldType.ATTACHMENT_SINGLE], icon: TypeIconMap[FieldType.ATTACHMENT_SINGLE],
constraints: { constraints: {
@ -119,7 +119,7 @@ export const FIELDS = {
}, },
}, },
ATTACHMENTS: { ATTACHMENTS: {
name: "Attachment List", name: "Multi attachment",
type: FieldType.ATTACHMENTS, type: FieldType.ATTACHMENTS,
icon: TypeIconMap[FieldType.ATTACHMENTS], icon: TypeIconMap[FieldType.ATTACHMENTS],
constraints: { constraints: {
@ -137,7 +137,7 @@ export const FIELDS = {
}, },
}, },
AUTO: { AUTO: {
name: "Auto Column", name: "Auto column",
type: FieldType.AUTO, type: FieldType.AUTO,
icon: TypeIconMap[FieldType.AUTO], icon: TypeIconMap[FieldType.AUTO],
constraints: {}, constraints: {},
@ -158,7 +158,7 @@ export const FIELDS = {
}, },
}, },
USER: { USER: {
name: "User", name: "Single user",
type: FieldType.BB_REFERENCE_SINGLE, type: FieldType.BB_REFERENCE_SINGLE,
subtype: BBReferenceFieldSubType.USER, subtype: BBReferenceFieldSubType.USER,
icon: TypeIconMap[FieldType.BB_REFERENCE_SINGLE][ icon: TypeIconMap[FieldType.BB_REFERENCE_SINGLE][
@ -166,7 +166,7 @@ export const FIELDS = {
], ],
}, },
USERS: { USERS: {
name: "User List", name: "Multi user",
type: FieldType.BB_REFERENCE, type: FieldType.BB_REFERENCE,
subtype: BBReferenceFieldSubType.USER, subtype: BBReferenceFieldSubType.USER,
icon: TypeIconMap[FieldType.BB_REFERENCE][BBReferenceFieldSubType.USER], icon: TypeIconMap[FieldType.BB_REFERENCE][BBReferenceFieldSubType.USER],

View File

@ -830,7 +830,7 @@ export const getActionBindings = (actions, actionId) => {
* @return {{schema: Object, table: Object}} * @return {{schema: Object, table: Object}}
*/ */
export const getSchemaForDatasourcePlus = (resourceId, options) => { export const getSchemaForDatasourcePlus = (resourceId, options) => {
const isViewV2 = resourceId?.includes("view_") const isViewV2 = resourceId?.startsWith("view_")
const datasource = isViewV2 const datasource = isViewV2
? { ? {
type: "viewV2", type: "viewV2",

View File

@ -1,7 +1,14 @@
<script> <script>
import { isActive, redirect, goto, url } from "@roxi/routify" import { isActive, redirect, goto, url } from "@roxi/routify"
import { Icon, notifications, Tabs, Tab } from "@budibase/bbui" import { Icon, notifications, Tabs, Tab } from "@budibase/bbui"
import { organisation, auth, menu, appsStore, licensing } from "stores/portal" import {
organisation,
auth,
menu,
appsStore,
licensing,
admin,
} from "stores/portal"
import { onMount } from "svelte" import { onMount } from "svelte"
import UpgradeButton from "./_components/UpgradeButton.svelte" import UpgradeButton from "./_components/UpgradeButton.svelte"
import MobileMenu from "./_components/MobileMenu.svelte" import MobileMenu from "./_components/MobileMenu.svelte"
@ -20,6 +27,7 @@
$: $url(), updateActiveTab($menu) $: $url(), updateActiveTab($menu)
$: isOnboarding = $: isOnboarding =
!$appsStore.apps.length && sdk.users.hasBuilderPermissions($auth.user) !$appsStore.apps.length && sdk.users.hasBuilderPermissions($auth.user)
$: isOwner = $auth.accountPortalAccess && $admin.cloud
const updateActiveTab = menu => { const updateActiveTab = menu => {
for (let entry of menu) { for (let entry of menu) {
@ -38,8 +46,7 @@
const showFreeTrialBanner = () => { const showFreeTrialBanner = () => {
return ( return (
$licensing.license?.plan?.type === $licensing.license?.plan?.type ===
Constants.PlanType.ENTERPRISE_BASIC_TRIAL && Constants.PlanType.ENTERPRISE_BASIC_TRIAL && isOwner
sdk.users.isAdmin($auth.user)
) )
} }

View File

@ -386,7 +386,7 @@
> >
Hide column Hide column
</MenuItem> </MenuItem>
{#if $config.canEditColumns && column.schema.type === "link" && column.schema.tableId === TableNames.USERS} {#if $config.canEditColumns && column.schema.type === "link" && column.schema.tableId === TableNames.USERS && !column.schema.autocolumn}
<MenuItem icon="User" on:click={openMigrationModal}> <MenuItem icon="User" on:click={openMigrationModal}>
Migrate to user column Migrate to user column
</MenuItem> </MenuItem>

View File

@ -265,7 +265,10 @@ export class ExternalRequest<T extends Operation> {
} }
} }
inputProcessing(row: Row | undefined, table: Table) { inputProcessing<T extends Row | undefined>(
row: T,
table: Table
): { row: T; manyRelationships: ManyRelationship[] } {
if (!row) { if (!row) {
return { row, manyRelationships: [] } return { row, manyRelationships: [] }
} }
@ -346,7 +349,7 @@ export class ExternalRequest<T extends Operation> {
// we return the relationships that may need to be created in the through table // we return the relationships that may need to be created in the through table
// we do this so that if the ID is generated by the DB it can be inserted // we do this so that if the ID is generated by the DB it can be inserted
// after the fact // after the fact
return { row: newRow, manyRelationships } return { row: newRow as T, manyRelationships }
} }
/** /**
@ -598,6 +601,18 @@ export class ExternalRequest<T extends Operation> {
// clean up row on ingress using schema // clean up row on ingress using schema
const processed = this.inputProcessing(row, table) const processed = this.inputProcessing(row, table)
row = processed.row row = processed.row
let manyRelationships = processed.manyRelationships
if (!row && rows) {
manyRelationships = []
for (let i = 0; i < rows.length; i++) {
const processed = this.inputProcessing(rows[i], table)
rows[i] = processed.row
if (processed.manyRelationships.length) {
manyRelationships.push(...processed.manyRelationships)
}
}
}
if ( if (
operation === Operation.DELETE && operation === Operation.DELETE &&
(filters == null || Object.keys(filters).length === 0) (filters == null || Object.keys(filters).length === 0)

View File

@ -15,6 +15,7 @@ import {
} from "@budibase/types" } from "@budibase/types"
import sdk from "../../../sdk" import sdk from "../../../sdk"
import { builderSocket } from "../../../websockets" import { builderSocket } from "../../../websockets"
import { inputProcessing } from "../../../utilities/rowProcessor"
function getDatasourceId(table: Table) { function getDatasourceId(table: Table) {
if (!table) { if (!table) {
@ -80,7 +81,7 @@ export async function destroy(ctx: UserCtx) {
export async function bulkImport( export async function bulkImport(
ctx: UserCtx<BulkImportRequest, BulkImportResponse> ctx: UserCtx<BulkImportRequest, BulkImportResponse>
) { ) {
const table = await sdk.tables.getTable(ctx.params.tableId) let table = await sdk.tables.getTable(ctx.params.tableId)
const { rows } = ctx.request.body const { rows } = ctx.request.body
const schema = table.schema const schema = table.schema
@ -88,7 +89,15 @@ export async function bulkImport(
ctx.throw(400, "Provided data import information is invalid.") ctx.throw(400, "Provided data import information is invalid.")
} }
const parsedRows = parse(rows, schema) const parsedRows = []
for (const row of parse(rows, schema)) {
const processed = await inputProcessing(ctx.user?._id, table, row, {
noAutoRelationships: true,
})
parsedRows.push(processed.row)
table = processed.table
}
await handleRequest(Operation.BULK_CREATE, table._id!, { await handleRequest(Operation.BULK_CREATE, table._id!, {
rows: parsedRows, rows: parsedRows,
}) })

View File

@ -88,7 +88,7 @@ describe.each(
let res = await setup.runStep(setup.actions.EXECUTE_QUERY.stepId, { let res = await setup.runStep(setup.actions.EXECUTE_QUERY.stepId, {
query: { queryId: "wrong_id" }, query: { queryId: "wrong_id" },
}) })
expect(res.response).toEqual("Error: CouchDB error: missing") expect(res.response).toBeDefined()
expect(res.success).toEqual(false) expect(res.success).toEqual(false)
}) })
}) })

View File

@ -79,9 +79,7 @@ export async function search(
} }
const table = await sdk.tables.getTable(options.tableId) const table = await sdk.tables.getTable(options.tableId)
options = searchInputMapping(table, options, { options = searchInputMapping(table, options)
isSql: !!table.sql || !!env.SQS_SEARCH_ENABLE,
})
if (isExternalTable) { if (isExternalTable) {
return external.search(options, table) return external.search(options, table)

View File

@ -11,7 +11,7 @@ import {
RowSearchParams, RowSearchParams,
} from "@budibase/types" } from "@budibase/types"
import { db as dbCore, context } from "@budibase/backend-core" import { db as dbCore, context } from "@budibase/backend-core"
import { helpers, utils } from "@budibase/shared-core" import { utils } from "@budibase/shared-core"
export async function paginatedSearch( export async function paginatedSearch(
query: SearchFilters, query: SearchFilters,
@ -49,12 +49,7 @@ function findColumnInQueries(
} }
} }
function userColumnMapping( function userColumnMapping(column: string, options: RowSearchParams) {
column: string,
options: RowSearchParams,
isDeprecatedSingleUserColumn: boolean = false,
isSql: boolean = false
) {
findColumnInQueries(column, options, (filterValue: any): any => { findColumnInQueries(column, options, (filterValue: any): any => {
const isArray = Array.isArray(filterValue), const isArray = Array.isArray(filterValue),
isString = typeof filterValue === "string" isString = typeof filterValue === "string"
@ -71,33 +66,23 @@ function userColumnMapping(
} }
} }
let wrapper = (s: string) => s
if (isDeprecatedSingleUserColumn && filterValue && isSql) {
// Decreated single users are stored as stringified arrays of a single value
wrapper = (s: string) => JSON.stringify([s])
}
if (isArray) { if (isArray) {
return filterValue.map(el => { return filterValue.map(el => {
if (typeof el === "string") { if (typeof el === "string") {
return wrapper(processString(el)) return processString(el)
} else { } else {
return el return el
} }
}) })
} else { } else {
return wrapper(processString(filterValue)) return processString(filterValue)
} }
}) })
} }
// maps through the search parameters to check if any of the inputs are invalid // 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. // based on the table schema, converts them to something that is valid.
export function searchInputMapping( export function searchInputMapping(table: Table, options: RowSearchParams) {
table: Table,
options: RowSearchParams,
datasourceOptions: { isSql?: boolean } = {}
) {
if (!table?.schema) { if (!table?.schema) {
return options return options
} }
@ -116,12 +101,7 @@ export function searchInputMapping(
break break
} }
case FieldType.BB_REFERENCE: { case FieldType.BB_REFERENCE: {
userColumnMapping( userColumnMapping(key, options)
key,
options,
helpers.schema.isDeprecatedSingleUserColumn(column),
datasourceOptions.isSql
)
break break
} }
} }

View File

@ -14,7 +14,13 @@ export async function processInputBBReference(
subtype: BBReferenceFieldSubType.USER subtype: BBReferenceFieldSubType.USER
): Promise<string | null> { ): Promise<string | null> {
if (value && Array.isArray(value)) { if (value && Array.isArray(value)) {
throw "BB_REFERENCE_SINGLE cannot be an array" if (value.length > 1) {
throw new InvalidBBRefError(
JSON.stringify(value),
BBReferenceFieldSubType.USER
)
}
value = value[0]
} }
let id = typeof value === "string" ? value : value?._id let id = typeof value === "string" ? value : value?._id

View File

@ -18,6 +18,7 @@ import {
processOutputBBReferences, processOutputBBReferences,
} from "./bbReferenceProcessor" } from "./bbReferenceProcessor"
import { isExternalTableID } from "../../integrations/utils" import { isExternalTableID } from "../../integrations/utils"
import { helpers } from "@budibase/shared-core"
export * from "./utils" export * from "./utils"
export * from "./attachments" export * from "./attachments"
@ -162,10 +163,14 @@ export async function inputProcessing(
if (attachment?.url) { if (attachment?.url) {
delete clonedRow[key].url delete clonedRow[key].url
} }
} else if (field.type === FieldType.BB_REFERENCE && value) { } else if (
clonedRow[key] = await processInputBBReferences(value, field.subtype) value &&
} else if (field.type === FieldType.BB_REFERENCE_SINGLE && value) { (field.type === FieldType.BB_REFERENCE_SINGLE ||
helpers.schema.isDeprecatedSingleUserColumn(field))
) {
clonedRow[key] = await processInputBBReference(value, field.subtype) clonedRow[key] = await processInputBBReference(value, field.subtype)
} else if (value && field.type === FieldType.BB_REFERENCE) {
clonedRow[key] = await processInputBBReferences(value, field.subtype)
} }
} }

View File

@ -102,7 +102,7 @@ describe("rowProcessor - inputProcessing", () => {
name: "user", name: "user",
constraints: { constraints: {
presence: true, presence: true,
type: "string", type: "array",
}, },
}, },
}, },
@ -154,7 +154,7 @@ describe("rowProcessor - inputProcessing", () => {
name: "user", name: "user",
constraints: { constraints: {
presence: false, presence: false,
type: "string", type: "array",
}, },
}, },
}, },
@ -196,7 +196,7 @@ describe("rowProcessor - inputProcessing", () => {
name: "user", name: "user",
constraints: { constraints: {
presence: false, presence: false,
type: "string", type: "array",
}, },
}, },
}, },

View File

@ -6,7 +6,10 @@ import {
export function isDeprecatedSingleUserColumn( export function isDeprecatedSingleUserColumn(
schema: Pick<FieldSchema, "type" | "subtype" | "constraints"> schema: Pick<FieldSchema, "type" | "subtype" | "constraints">
) { ): schema is {
type: FieldType.BB_REFERENCE
subtype: BBReferenceFieldSubType.USER
} {
const result = const result =
schema.type === FieldType.BB_REFERENCE && schema.type === FieldType.BB_REFERENCE &&
schema.subtype === BBReferenceFieldSubType.USER && schema.subtype === BBReferenceFieldSubType.USER &&

View File

@ -459,10 +459,11 @@ describe("scim", () => {
it("should return 404 when requesting unexisting user id", async () => { it("should return 404 when requesting unexisting user id", async () => {
const response = await findScimUser(structures.uuid(), { expect: 404 }) const response = await findScimUser(structures.uuid(), { expect: 404 })
expect(response).toEqual({ expect(response).toEqual(
message: "missing", expect.objectContaining({
status: 404, status: 404,
}) })
)
}) })
}) })
@ -861,10 +862,11 @@ describe("scim", () => {
it("should return 404 when requesting unexisting group id", async () => { it("should return 404 when requesting unexisting group id", async () => {
const response = await findScimGroup(structures.uuid(), { expect: 404 }) const response = await findScimGroup(structures.uuid(), { expect: 404 })
expect(response).toEqual({ expect(response).toEqual(
message: "missing", expect.objectContaining({
status: 404, status: 404,
}) })
)
}) })
it("should allow excluding members", async () => { it("should allow excluding members", async () => {

672
yarn.lock

File diff suppressed because it is too large Load Diff