Merge branch 'master' into feature/sqs-table-cleanup
This commit is contained in:
commit
1c22c7d2d1
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"version": "2.26.2",
|
||||
"version": "2.26.3",
|
||||
"npmClient": "yarn",
|
||||
"packages": [
|
||||
"packages/*",
|
||||
|
|
|
@ -398,44 +398,50 @@
|
|||
if (!externalTable) {
|
||||
return [
|
||||
FIELDS.STRING,
|
||||
FIELDS.BARCODEQR,
|
||||
FIELDS.LONGFORM,
|
||||
FIELDS.NUMBER,
|
||||
FIELDS.OPTIONS,
|
||||
FIELDS.ARRAY,
|
||||
FIELDS.NUMBER,
|
||||
FIELDS.BIGINT,
|
||||
FIELDS.BOOLEAN,
|
||||
FIELDS.DATETIME,
|
||||
FIELDS.ATTACHMENT_SINGLE,
|
||||
FIELDS.ATTACHMENTS,
|
||||
FIELDS.LINK,
|
||||
FIELDS.FORMULA,
|
||||
FIELDS.JSON,
|
||||
FIELDS.LONGFORM,
|
||||
FIELDS.USER,
|
||||
FIELDS.USERS,
|
||||
FIELDS.ATTACHMENT_SINGLE,
|
||||
FIELDS.ATTACHMENTS,
|
||||
FIELDS.FORMULA,
|
||||
FIELDS.JSON,
|
||||
FIELDS.BARCODEQR,
|
||||
FIELDS.BIGINT,
|
||||
FIELDS.AUTO,
|
||||
]
|
||||
} else {
|
||||
let fields = [
|
||||
FIELDS.STRING,
|
||||
FIELDS.BARCODEQR,
|
||||
FIELDS.LONGFORM,
|
||||
FIELDS.OPTIONS,
|
||||
FIELDS.DATETIME,
|
||||
FIELDS.NUMBER,
|
||||
FIELDS.OPTIONS,
|
||||
FIELDS.ARRAY,
|
||||
FIELDS.BOOLEAN,
|
||||
FIELDS.FORMULA,
|
||||
FIELDS.BIGINT,
|
||||
FIELDS.DATETIME,
|
||||
FIELDS.LINK,
|
||||
FIELDS.LONGFORM,
|
||||
FIELDS.USER,
|
||||
FIELDS.USERS,
|
||||
FIELDS.FORMULA,
|
||||
FIELDS.BARCODEQR,
|
||||
FIELDS.BIGINT,
|
||||
]
|
||||
|
||||
if (datasource && datasource.source !== SourceName.GOOGLE_SHEETS) {
|
||||
fields.push(FIELDS.USERS)
|
||||
// Filter out multiple users for google sheets
|
||||
if (datasource?.source === SourceName.GOOGLE_SHEETS) {
|
||||
fields = fields.filter(x => x !== FIELDS.USERS)
|
||||
}
|
||||
// no-sql or a spreadsheet
|
||||
if (!externalTable || table.sql) {
|
||||
fields = [...fields, FIELDS.LINK, FIELDS.ARRAY]
|
||||
|
||||
// Filter out SQL-specific types for non-SQL datasources
|
||||
if (!table.sql) {
|
||||
fields = fields.filter(x => x !== FIELDS.LINK && x !== FIELDS.ARRAY)
|
||||
}
|
||||
|
||||
return fields
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,21 +2,21 @@
|
|||
import { Modal, ModalContent } from "@budibase/bbui"
|
||||
import FreeTrial from "../../../../assets/FreeTrial.svelte"
|
||||
import { get } from "svelte/store"
|
||||
import { auth, licensing } from "stores/portal"
|
||||
import { auth, licensing, admin } from "stores/portal"
|
||||
import { API } from "api"
|
||||
import { PlanType } from "@budibase/types"
|
||||
import { sdk } from "@budibase/shared-core"
|
||||
|
||||
let freeTrialModal
|
||||
|
||||
$: planType = $licensing?.license?.plan?.type
|
||||
$: showFreeTrialModal(planType, freeTrialModal)
|
||||
$: isOwner = $auth.accountPortalAccess && $admin.cloud
|
||||
|
||||
const showFreeTrialModal = (planType, freeTrialModal) => {
|
||||
if (
|
||||
planType === PlanType.ENTERPRISE_BASIC_TRIAL &&
|
||||
!$auth.user?.freeTrialConfirmedAt &&
|
||||
sdk.users.isAdmin($auth.user)
|
||||
isOwner
|
||||
) {
|
||||
freeTrialModal?.show()
|
||||
}
|
||||
|
|
|
@ -33,7 +33,7 @@ export const FIELDS = {
|
|||
},
|
||||
},
|
||||
BARCODEQR: {
|
||||
name: "Barcode/QR",
|
||||
name: "Barcode / QR",
|
||||
type: FieldType.BARCODEQR,
|
||||
icon: TypeIconMap[FieldType.BARCODEQR],
|
||||
constraints: {
|
||||
|
@ -43,7 +43,7 @@ export const FIELDS = {
|
|||
},
|
||||
},
|
||||
LONGFORM: {
|
||||
name: "Long Form Text",
|
||||
name: "Long form text",
|
||||
type: FieldType.LONGFORM,
|
||||
icon: TypeIconMap[FieldType.LONGFORM],
|
||||
constraints: {
|
||||
|
@ -53,7 +53,7 @@ export const FIELDS = {
|
|||
},
|
||||
},
|
||||
OPTIONS: {
|
||||
name: "Options",
|
||||
name: "Single select",
|
||||
type: FieldType.OPTIONS,
|
||||
icon: TypeIconMap[FieldType.OPTIONS],
|
||||
constraints: {
|
||||
|
@ -63,7 +63,7 @@ export const FIELDS = {
|
|||
},
|
||||
},
|
||||
ARRAY: {
|
||||
name: "Multi-select",
|
||||
name: "Multi select",
|
||||
type: FieldType.ARRAY,
|
||||
icon: TypeIconMap[FieldType.ARRAY],
|
||||
constraints: {
|
||||
|
@ -83,7 +83,7 @@ export const FIELDS = {
|
|||
},
|
||||
},
|
||||
BIGINT: {
|
||||
name: "BigInt",
|
||||
name: "Big integer",
|
||||
type: FieldType.BIGINT,
|
||||
icon: TypeIconMap[FieldType.BIGINT],
|
||||
},
|
||||
|
@ -97,7 +97,7 @@ export const FIELDS = {
|
|||
},
|
||||
},
|
||||
DATETIME: {
|
||||
name: "Date/Time",
|
||||
name: "Date / time",
|
||||
type: FieldType.DATETIME,
|
||||
icon: TypeIconMap[FieldType.DATETIME],
|
||||
constraints: {
|
||||
|
@ -111,7 +111,7 @@ export const FIELDS = {
|
|||
},
|
||||
},
|
||||
ATTACHMENT_SINGLE: {
|
||||
name: "Attachment",
|
||||
name: "Single attachment",
|
||||
type: FieldType.ATTACHMENT_SINGLE,
|
||||
icon: TypeIconMap[FieldType.ATTACHMENT_SINGLE],
|
||||
constraints: {
|
||||
|
@ -119,7 +119,7 @@ export const FIELDS = {
|
|||
},
|
||||
},
|
||||
ATTACHMENTS: {
|
||||
name: "Attachment List",
|
||||
name: "Multi attachment",
|
||||
type: FieldType.ATTACHMENTS,
|
||||
icon: TypeIconMap[FieldType.ATTACHMENTS],
|
||||
constraints: {
|
||||
|
@ -137,7 +137,7 @@ export const FIELDS = {
|
|||
},
|
||||
},
|
||||
AUTO: {
|
||||
name: "Auto Column",
|
||||
name: "Auto column",
|
||||
type: FieldType.AUTO,
|
||||
icon: TypeIconMap[FieldType.AUTO],
|
||||
constraints: {},
|
||||
|
@ -158,7 +158,7 @@ export const FIELDS = {
|
|||
},
|
||||
},
|
||||
USER: {
|
||||
name: "User",
|
||||
name: "Single user",
|
||||
type: FieldType.BB_REFERENCE_SINGLE,
|
||||
subtype: BBReferenceFieldSubType.USER,
|
||||
icon: TypeIconMap[FieldType.BB_REFERENCE_SINGLE][
|
||||
|
@ -166,7 +166,7 @@ export const FIELDS = {
|
|||
],
|
||||
},
|
||||
USERS: {
|
||||
name: "User List",
|
||||
name: "Multi user",
|
||||
type: FieldType.BB_REFERENCE,
|
||||
subtype: BBReferenceFieldSubType.USER,
|
||||
icon: TypeIconMap[FieldType.BB_REFERENCE][BBReferenceFieldSubType.USER],
|
||||
|
|
|
@ -830,7 +830,7 @@ export const getActionBindings = (actions, actionId) => {
|
|||
* @return {{schema: Object, table: Object}}
|
||||
*/
|
||||
export const getSchemaForDatasourcePlus = (resourceId, options) => {
|
||||
const isViewV2 = resourceId?.includes("view_")
|
||||
const isViewV2 = resourceId?.startsWith("view_")
|
||||
const datasource = isViewV2
|
||||
? {
|
||||
type: "viewV2",
|
||||
|
|
|
@ -1,7 +1,14 @@
|
|||
<script>
|
||||
import { isActive, redirect, goto, url } from "@roxi/routify"
|
||||
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 UpgradeButton from "./_components/UpgradeButton.svelte"
|
||||
import MobileMenu from "./_components/MobileMenu.svelte"
|
||||
|
@ -20,6 +27,7 @@
|
|||
$: $url(), updateActiveTab($menu)
|
||||
$: isOnboarding =
|
||||
!$appsStore.apps.length && sdk.users.hasBuilderPermissions($auth.user)
|
||||
$: isOwner = $auth.accountPortalAccess && $admin.cloud
|
||||
|
||||
const updateActiveTab = menu => {
|
||||
for (let entry of menu) {
|
||||
|
@ -38,8 +46,7 @@
|
|||
const showFreeTrialBanner = () => {
|
||||
return (
|
||||
$licensing.license?.plan?.type ===
|
||||
Constants.PlanType.ENTERPRISE_BASIC_TRIAL &&
|
||||
sdk.users.isAdmin($auth.user)
|
||||
Constants.PlanType.ENTERPRISE_BASIC_TRIAL && isOwner
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -386,7 +386,7 @@
|
|||
>
|
||||
Hide column
|
||||
</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}>
|
||||
Migrate to user column
|
||||
</MenuItem>
|
||||
|
|
|
@ -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) {
|
||||
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 do this so that if the ID is generated by the DB it can be inserted
|
||||
// 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
|
||||
const processed = this.inputProcessing(row, table)
|
||||
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 (
|
||||
operation === Operation.DELETE &&
|
||||
(filters == null || Object.keys(filters).length === 0)
|
||||
|
|
|
@ -15,6 +15,7 @@ import {
|
|||
} from "@budibase/types"
|
||||
import sdk from "../../../sdk"
|
||||
import { builderSocket } from "../../../websockets"
|
||||
import { inputProcessing } from "../../../utilities/rowProcessor"
|
||||
|
||||
function getDatasourceId(table: Table) {
|
||||
if (!table) {
|
||||
|
@ -80,7 +81,7 @@ export async function destroy(ctx: UserCtx) {
|
|||
export async function bulkImport(
|
||||
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 schema = table.schema
|
||||
|
||||
|
@ -88,7 +89,15 @@ export async function bulkImport(
|
|||
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!, {
|
||||
rows: parsedRows,
|
||||
})
|
||||
|
|
|
@ -88,7 +88,7 @@ describe.each(
|
|||
let res = await setup.runStep(setup.actions.EXECUTE_QUERY.stepId, {
|
||||
query: { queryId: "wrong_id" },
|
||||
})
|
||||
expect(res.response).toEqual("Error: CouchDB error: missing")
|
||||
expect(res.response).toBeDefined()
|
||||
expect(res.success).toEqual(false)
|
||||
})
|
||||
})
|
||||
|
|
|
@ -79,9 +79,7 @@ export async function search(
|
|||
}
|
||||
|
||||
const table = await sdk.tables.getTable(options.tableId)
|
||||
options = searchInputMapping(table, options, {
|
||||
isSql: !!table.sql || !!env.SQS_SEARCH_ENABLE,
|
||||
})
|
||||
options = searchInputMapping(table, options)
|
||||
|
||||
if (isExternalTable) {
|
||||
return external.search(options, table)
|
||||
|
|
|
@ -11,7 +11,7 @@ import {
|
|||
RowSearchParams,
|
||||
} from "@budibase/types"
|
||||
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(
|
||||
query: SearchFilters,
|
||||
|
@ -49,12 +49,7 @@ function findColumnInQueries(
|
|||
}
|
||||
}
|
||||
|
||||
function userColumnMapping(
|
||||
column: string,
|
||||
options: RowSearchParams,
|
||||
isDeprecatedSingleUserColumn: boolean = false,
|
||||
isSql: boolean = false
|
||||
) {
|
||||
function userColumnMapping(column: string, options: RowSearchParams) {
|
||||
findColumnInQueries(column, options, (filterValue: any): any => {
|
||||
const isArray = Array.isArray(filterValue),
|
||||
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) {
|
||||
return filterValue.map(el => {
|
||||
if (typeof el === "string") {
|
||||
return wrapper(processString(el))
|
||||
return processString(el)
|
||||
} else {
|
||||
return el
|
||||
}
|
||||
})
|
||||
} else {
|
||||
return wrapper(processString(filterValue))
|
||||
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: RowSearchParams,
|
||||
datasourceOptions: { isSql?: boolean } = {}
|
||||
) {
|
||||
export function searchInputMapping(table: Table, options: RowSearchParams) {
|
||||
if (!table?.schema) {
|
||||
return options
|
||||
}
|
||||
|
@ -116,12 +101,7 @@ export function searchInputMapping(
|
|||
break
|
||||
}
|
||||
case FieldType.BB_REFERENCE: {
|
||||
userColumnMapping(
|
||||
key,
|
||||
options,
|
||||
helpers.schema.isDeprecatedSingleUserColumn(column),
|
||||
datasourceOptions.isSql
|
||||
)
|
||||
userColumnMapping(key, options)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
|
|
@ -14,7 +14,13 @@ export async function processInputBBReference(
|
|||
subtype: BBReferenceFieldSubType.USER
|
||||
): Promise<string | null> {
|
||||
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
|
||||
|
||||
|
|
|
@ -18,6 +18,7 @@ import {
|
|||
processOutputBBReferences,
|
||||
} from "./bbReferenceProcessor"
|
||||
import { isExternalTableID } from "../../integrations/utils"
|
||||
import { helpers } from "@budibase/shared-core"
|
||||
|
||||
export * from "./utils"
|
||||
export * from "./attachments"
|
||||
|
@ -162,10 +163,14 @@ export async function inputProcessing(
|
|||
if (attachment?.url) {
|
||||
delete clonedRow[key].url
|
||||
}
|
||||
} else if (field.type === FieldType.BB_REFERENCE && value) {
|
||||
clonedRow[key] = await processInputBBReferences(value, field.subtype)
|
||||
} else if (field.type === FieldType.BB_REFERENCE_SINGLE && value) {
|
||||
} else if (
|
||||
value &&
|
||||
(field.type === FieldType.BB_REFERENCE_SINGLE ||
|
||||
helpers.schema.isDeprecatedSingleUserColumn(field))
|
||||
) {
|
||||
clonedRow[key] = await processInputBBReference(value, field.subtype)
|
||||
} else if (value && field.type === FieldType.BB_REFERENCE) {
|
||||
clonedRow[key] = await processInputBBReferences(value, field.subtype)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -102,7 +102,7 @@ describe("rowProcessor - inputProcessing", () => {
|
|||
name: "user",
|
||||
constraints: {
|
||||
presence: true,
|
||||
type: "string",
|
||||
type: "array",
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@ -154,7 +154,7 @@ describe("rowProcessor - inputProcessing", () => {
|
|||
name: "user",
|
||||
constraints: {
|
||||
presence: false,
|
||||
type: "string",
|
||||
type: "array",
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@ -196,7 +196,7 @@ describe("rowProcessor - inputProcessing", () => {
|
|||
name: "user",
|
||||
constraints: {
|
||||
presence: false,
|
||||
type: "string",
|
||||
type: "array",
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
|
@ -6,7 +6,10 @@ import {
|
|||
|
||||
export function isDeprecatedSingleUserColumn(
|
||||
schema: Pick<FieldSchema, "type" | "subtype" | "constraints">
|
||||
) {
|
||||
): schema is {
|
||||
type: FieldType.BB_REFERENCE
|
||||
subtype: BBReferenceFieldSubType.USER
|
||||
} {
|
||||
const result =
|
||||
schema.type === FieldType.BB_REFERENCE &&
|
||||
schema.subtype === BBReferenceFieldSubType.USER &&
|
||||
|
|
|
@ -459,10 +459,11 @@ describe("scim", () => {
|
|||
it("should return 404 when requesting unexisting user id", async () => {
|
||||
const response = await findScimUser(structures.uuid(), { expect: 404 })
|
||||
|
||||
expect(response).toEqual({
|
||||
message: "missing",
|
||||
status: 404,
|
||||
})
|
||||
expect(response).toEqual(
|
||||
expect.objectContaining({
|
||||
status: 404,
|
||||
})
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
|
@ -861,10 +862,11 @@ describe("scim", () => {
|
|||
it("should return 404 when requesting unexisting group id", async () => {
|
||||
const response = await findScimGroup(structures.uuid(), { expect: 404 })
|
||||
|
||||
expect(response).toEqual({
|
||||
message: "missing",
|
||||
status: 404,
|
||||
})
|
||||
expect(response).toEqual(
|
||||
expect.objectContaining({
|
||||
status: 404,
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
it("should allow excluding members", async () => {
|
||||
|
|
Loading…
Reference in New Issue