Merge pull request #13604 from Budibase/feat/budi-8123-single-user
[Feat] Single user column
This commit is contained in:
commit
fc4f271e4b
54 changed files with 1140 additions and 423 deletions
|
@ -54,7 +54,8 @@
|
||||||
"ignoreRestSiblings": true
|
"ignoreRestSiblings": true
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"local-rules/no-budibase-imports": "error"
|
"no-redeclare": "off",
|
||||||
|
"@typescript-eslint/no-redeclare": "error"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
4
packages/backend-core/src/cache/user.ts
vendored
4
packages/backend-core/src/cache/user.ts
vendored
|
@ -69,7 +69,7 @@ async function populateUsersFromDB(
|
||||||
export async function getUser(
|
export async function getUser(
|
||||||
userId: string,
|
userId: string,
|
||||||
tenantId?: string,
|
tenantId?: string,
|
||||||
populateUser?: any
|
populateUser?: (userId: string, tenantId: string) => Promise<User>
|
||||||
) {
|
) {
|
||||||
if (!populateUser) {
|
if (!populateUser) {
|
||||||
populateUser = populateFromDB
|
populateUser = populateFromDB
|
||||||
|
@ -83,7 +83,7 @@ export async function getUser(
|
||||||
}
|
}
|
||||||
const client = await redis.getUserClient()
|
const client = await redis.getUserClient()
|
||||||
// try cache
|
// try cache
|
||||||
let user = await client.get(userId)
|
let user: User = await client.get(userId)
|
||||||
if (!user) {
|
if (!user) {
|
||||||
user = await populateUser(userId, tenantId)
|
user = await populateUser(userId, tenantId)
|
||||||
await client.store(userId, user, EXPIRY_SECONDS)
|
await client.store(userId, user, EXPIRY_SECONDS)
|
||||||
|
|
|
@ -13,7 +13,7 @@ import { getGlobalDB, doInTenant } from "../context"
|
||||||
import { decrypt } from "../security/encryption"
|
import { decrypt } from "../security/encryption"
|
||||||
import * as identity from "../context/identity"
|
import * as identity from "../context/identity"
|
||||||
import env from "../environment"
|
import env from "../environment"
|
||||||
import { Ctx, EndpointMatcher, SessionCookie } from "@budibase/types"
|
import { Ctx, EndpointMatcher, SessionCookie, User } from "@budibase/types"
|
||||||
import { InvalidAPIKeyError, ErrorCode } from "../errors"
|
import { InvalidAPIKeyError, ErrorCode } from "../errors"
|
||||||
import tracer from "dd-trace"
|
import tracer from "dd-trace"
|
||||||
|
|
||||||
|
@ -41,7 +41,10 @@ function finalise(ctx: any, opts: FinaliseOpts = {}) {
|
||||||
ctx.version = opts.version
|
ctx.version = opts.version
|
||||||
}
|
}
|
||||||
|
|
||||||
async function checkApiKey(apiKey: string, populateUser?: Function) {
|
async function checkApiKey(
|
||||||
|
apiKey: string,
|
||||||
|
populateUser?: (userId: string, tenantId: string) => Promise<User>
|
||||||
|
) {
|
||||||
// check both the primary and the fallback internal api keys
|
// check both the primary and the fallback internal api keys
|
||||||
// this allows for rotation
|
// this allows for rotation
|
||||||
if (isValidInternalAPIKey(apiKey)) {
|
if (isValidInternalAPIKey(apiKey)) {
|
||||||
|
@ -128,6 +131,7 @@ export default function (
|
||||||
} else {
|
} else {
|
||||||
user = await getUser(userId, session.tenantId)
|
user = await getUser(userId, session.tenantId)
|
||||||
}
|
}
|
||||||
|
// @ts-ignore
|
||||||
user.csrfToken = session.csrfToken
|
user.csrfToken = session.csrfToken
|
||||||
|
|
||||||
if (session?.lastAccessedAt < timeMinusOneMinute()) {
|
if (session?.lastAccessedAt < timeMinusOneMinute()) {
|
||||||
|
@ -167,19 +171,25 @@ export default function (
|
||||||
authenticated = false
|
authenticated = false
|
||||||
}
|
}
|
||||||
|
|
||||||
if (user) {
|
const isUser = (
|
||||||
|
user: any
|
||||||
|
): user is User & { budibaseAccess?: string } => {
|
||||||
|
return user && user.email
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isUser(user)) {
|
||||||
tracer.setUser({
|
tracer.setUser({
|
||||||
id: user?._id,
|
id: user._id!,
|
||||||
tenantId: user?.tenantId,
|
tenantId: user.tenantId,
|
||||||
budibaseAccess: user?.budibaseAccess,
|
budibaseAccess: user.budibaseAccess,
|
||||||
status: user?.status,
|
status: user.status,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// isAuthenticated is a function, so use a variable to be able to check authed state
|
// isAuthenticated is a function, so use a variable to be able to check authed state
|
||||||
finalise(ctx, { authenticated, user, internal, version, publicEndpoint })
|
finalise(ctx, { authenticated, user, internal, version, publicEndpoint })
|
||||||
|
|
||||||
if (user && user.email) {
|
if (isUser(user)) {
|
||||||
return identity.doInUserContext(user, ctx, next)
|
return identity.doInUserContext(user, ctx, next)
|
||||||
} else {
|
} else {
|
||||||
return next()
|
return next()
|
||||||
|
|
|
@ -48,6 +48,7 @@
|
||||||
import { TriggerStepID, ActionStepID } from "constants/backend/automations"
|
import { TriggerStepID, ActionStepID } from "constants/backend/automations"
|
||||||
import { onMount } from "svelte"
|
import { onMount } from "svelte"
|
||||||
import { cloneDeep } from "lodash/fp"
|
import { cloneDeep } from "lodash/fp"
|
||||||
|
import { FIELDS } from "constants/backend"
|
||||||
|
|
||||||
export let block
|
export let block
|
||||||
export let testData
|
export let testData
|
||||||
|
@ -228,6 +229,10 @@
|
||||||
categoryName,
|
categoryName,
|
||||||
bindingName
|
bindingName
|
||||||
) => {
|
) => {
|
||||||
|
const field = Object.values(FIELDS).find(
|
||||||
|
field => field.type === value.type && field.subtype === value.subtype
|
||||||
|
)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
readableBinding: bindingName
|
readableBinding: bindingName
|
||||||
? `${bindingName}.${name}`
|
? `${bindingName}.${name}`
|
||||||
|
@ -238,7 +243,7 @@
|
||||||
icon,
|
icon,
|
||||||
category: categoryName,
|
category: categoryName,
|
||||||
display: {
|
display: {
|
||||||
type: value.type,
|
type: field?.name || value.type,
|
||||||
name,
|
name,
|
||||||
rank: isLoopBlock ? idx + 1 : idx - loopBlockCount,
|
rank: isLoopBlock ? idx + 1 : idx - loopBlockCount,
|
||||||
},
|
},
|
||||||
|
@ -282,6 +287,7 @@
|
||||||
for (const key in table?.schema) {
|
for (const key in table?.schema) {
|
||||||
schema[key] = {
|
schema[key] = {
|
||||||
type: table.schema[key].type,
|
type: table.schema[key].type,
|
||||||
|
subtype: table.schema[key].subtype,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// remove the original binding
|
// remove the original binding
|
||||||
|
|
|
@ -55,7 +55,7 @@ export function getBindings({
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
const field = Object.values(FIELDS).find(
|
const field = Object.values(FIELDS).find(
|
||||||
field => field.type === schema.type
|
field => field.type === schema.type && field.subtype === schema.subtype
|
||||||
)
|
)
|
||||||
|
|
||||||
const label = path == null ? column : `${path}.0.${column}`
|
const label = path == null ? column : `${path}.0.${column}`
|
||||||
|
|
|
@ -14,7 +14,11 @@
|
||||||
AbsTooltip,
|
AbsTooltip,
|
||||||
ProgressCircle,
|
ProgressCircle,
|
||||||
} from "@budibase/bbui"
|
} from "@budibase/bbui"
|
||||||
import { SWITCHABLE_TYPES, ValidColumnNameRegex } from "@budibase/shared-core"
|
import {
|
||||||
|
SWITCHABLE_TYPES,
|
||||||
|
ValidColumnNameRegex,
|
||||||
|
helpers,
|
||||||
|
} from "@budibase/shared-core"
|
||||||
import { createEventDispatcher, getContext, onMount } from "svelte"
|
import { createEventDispatcher, getContext, onMount } from "svelte"
|
||||||
import { cloneDeep } from "lodash/fp"
|
import { cloneDeep } from "lodash/fp"
|
||||||
import { tables, datasources } from "stores/builder"
|
import { tables, datasources } from "stores/builder"
|
||||||
|
@ -31,8 +35,8 @@
|
||||||
import { getBindings } from "components/backend/DataTable/formula"
|
import { getBindings } from "components/backend/DataTable/formula"
|
||||||
import JSONSchemaModal from "./JSONSchemaModal.svelte"
|
import JSONSchemaModal from "./JSONSchemaModal.svelte"
|
||||||
import {
|
import {
|
||||||
FieldType,
|
|
||||||
BBReferenceFieldSubType,
|
BBReferenceFieldSubType,
|
||||||
|
FieldType,
|
||||||
SourceName,
|
SourceName,
|
||||||
} from "@budibase/types"
|
} from "@budibase/types"
|
||||||
import RelationshipSelector from "components/common/RelationshipSelector.svelte"
|
import RelationshipSelector from "components/common/RelationshipSelector.svelte"
|
||||||
|
@ -68,7 +72,6 @@
|
||||||
let savingColumn
|
let savingColumn
|
||||||
let deleteColName
|
let deleteColName
|
||||||
let jsonSchemaModal
|
let jsonSchemaModal
|
||||||
let allowedTypes = []
|
|
||||||
let editableColumn = {
|
let editableColumn = {
|
||||||
type: FIELDS.STRING.type,
|
type: FIELDS.STRING.type,
|
||||||
constraints: FIELDS.STRING.constraints,
|
constraints: FIELDS.STRING.constraints,
|
||||||
|
@ -176,6 +179,11 @@
|
||||||
SWITCHABLE_TYPES[field.type] &&
|
SWITCHABLE_TYPES[field.type] &&
|
||||||
!editableColumn?.autocolumn)
|
!editableColumn?.autocolumn)
|
||||||
|
|
||||||
|
$: allowedTypes = getAllowedTypes(datasource).map(t => ({
|
||||||
|
fieldId: makeFieldId(t.type, t.subtype),
|
||||||
|
...t,
|
||||||
|
}))
|
||||||
|
|
||||||
const fieldDefinitions = Object.values(FIELDS).reduce(
|
const fieldDefinitions = Object.values(FIELDS).reduce(
|
||||||
// Storing the fields by complex field id
|
// Storing the fields by complex field id
|
||||||
(acc, field) => ({
|
(acc, field) => ({
|
||||||
|
@ -189,7 +197,10 @@
|
||||||
// don't make field IDs for auto types
|
// don't make field IDs for auto types
|
||||||
if (type === AUTO_TYPE || autocolumn) {
|
if (type === AUTO_TYPE || autocolumn) {
|
||||||
return type.toUpperCase()
|
return type.toUpperCase()
|
||||||
} else if (type === FieldType.BB_REFERENCE) {
|
} else if (
|
||||||
|
type === FieldType.BB_REFERENCE ||
|
||||||
|
type === FieldType.BB_REFERENCE_SINGLE
|
||||||
|
) {
|
||||||
return `${type}${subtype || ""}`.toUpperCase()
|
return `${type}${subtype || ""}`.toUpperCase()
|
||||||
} else {
|
} else {
|
||||||
return type.toUpperCase()
|
return type.toUpperCase()
|
||||||
|
@ -227,11 +238,6 @@
|
||||||
editableColumn.subtype,
|
editableColumn.subtype,
|
||||||
editableColumn.autocolumn
|
editableColumn.autocolumn
|
||||||
)
|
)
|
||||||
|
|
||||||
allowedTypes = getAllowedTypes().map(t => ({
|
|
||||||
fieldId: makeFieldId(t.type, t.subtype),
|
|
||||||
...t,
|
|
||||||
}))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -265,13 +271,6 @@
|
||||||
if (saveColumn.type !== LINK_TYPE) {
|
if (saveColumn.type !== LINK_TYPE) {
|
||||||
delete saveColumn.fieldName
|
delete saveColumn.fieldName
|
||||||
}
|
}
|
||||||
if (isUsersColumn(saveColumn)) {
|
|
||||||
if (saveColumn.subtype === BBReferenceFieldSubType.USER) {
|
|
||||||
saveColumn.relationshipType = RelationshipType.ONE_TO_MANY
|
|
||||||
} else if (saveColumn.subtype === BBReferenceFieldSubType.USERS) {
|
|
||||||
saveColumn.relationshipType = RelationshipType.MANY_TO_MANY
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await tables.saveField({
|
await tables.saveField({
|
||||||
|
@ -366,20 +365,36 @@
|
||||||
deleteColName = ""
|
deleteColName = ""
|
||||||
}
|
}
|
||||||
|
|
||||||
function getAllowedTypes() {
|
function getAllowedTypes(datasource) {
|
||||||
if (originalName) {
|
if (originalName) {
|
||||||
const possibleTypes = SWITCHABLE_TYPES[field.type] || [
|
let possibleTypes = SWITCHABLE_TYPES[field.type] || [editableColumn.type]
|
||||||
editableColumn.type,
|
if (helpers.schema.isDeprecatedSingleUserColumn(editableColumn)) {
|
||||||
]
|
// This will handle old single users columns
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
...FIELDS.USER,
|
||||||
|
type: FieldType.BB_REFERENCE,
|
||||||
|
subtype: BBReferenceFieldSubType.USER,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
} else if (
|
||||||
|
editableColumn.type === FieldType.BB_REFERENCE &&
|
||||||
|
editableColumn.subtype === BBReferenceFieldSubType.USERS
|
||||||
|
) {
|
||||||
|
// This will handle old multi users columns
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
...FIELDS.USERS,
|
||||||
|
subtype: BBReferenceFieldSubType.USERS,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
return Object.entries(FIELDS)
|
return Object.entries(FIELDS)
|
||||||
.filter(([_, field]) => possibleTypes.includes(field.type))
|
.filter(([_, field]) => possibleTypes.includes(field.type))
|
||||||
.map(([_, fieldDefinition]) => fieldDefinition)
|
.map(([_, fieldDefinition]) => fieldDefinition)
|
||||||
}
|
}
|
||||||
|
|
||||||
const isUsers =
|
|
||||||
editableColumn.type === FieldType.BB_REFERENCE &&
|
|
||||||
editableColumn.subtype === BBReferenceFieldSubType.USERS
|
|
||||||
|
|
||||||
if (!externalTable) {
|
if (!externalTable) {
|
||||||
return [
|
return [
|
||||||
FIELDS.STRING,
|
FIELDS.STRING,
|
||||||
|
@ -396,7 +411,8 @@
|
||||||
FIELDS.LINK,
|
FIELDS.LINK,
|
||||||
FIELDS.FORMULA,
|
FIELDS.FORMULA,
|
||||||
FIELDS.JSON,
|
FIELDS.JSON,
|
||||||
isUsers ? FIELDS.USERS : FIELDS.USER,
|
FIELDS.USER,
|
||||||
|
FIELDS.USERS,
|
||||||
FIELDS.AUTO,
|
FIELDS.AUTO,
|
||||||
]
|
]
|
||||||
} else {
|
} else {
|
||||||
|
@ -410,8 +426,12 @@
|
||||||
FIELDS.BOOLEAN,
|
FIELDS.BOOLEAN,
|
||||||
FIELDS.FORMULA,
|
FIELDS.FORMULA,
|
||||||
FIELDS.BIGINT,
|
FIELDS.BIGINT,
|
||||||
isUsers ? FIELDS.USERS : FIELDS.USER,
|
FIELDS.USER,
|
||||||
]
|
]
|
||||||
|
|
||||||
|
if (datasource && datasource.source !== SourceName.GOOGLE_SHEETS) {
|
||||||
|
fields.push(FIELDS.USERS)
|
||||||
|
}
|
||||||
// no-sql or a spreadsheet
|
// no-sql or a spreadsheet
|
||||||
if (!externalTable || table.sql) {
|
if (!externalTable || table.sql) {
|
||||||
fields = [...fields, FIELDS.LINK, FIELDS.ARRAY]
|
fields = [...fields, FIELDS.LINK, FIELDS.ARRAY]
|
||||||
|
@ -485,15 +505,6 @@
|
||||||
return newError
|
return newError
|
||||||
}
|
}
|
||||||
|
|
||||||
function isUsersColumn(column) {
|
|
||||||
return (
|
|
||||||
column.type === FieldType.BB_REFERENCE &&
|
|
||||||
[BBReferenceFieldSubType.USER, BBReferenceFieldSubType.USERS].includes(
|
|
||||||
column.subtype
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
mounted = true
|
mounted = true
|
||||||
})
|
})
|
||||||
|
@ -692,22 +703,6 @@
|
||||||
<Button primary text on:click={openJsonSchemaEditor}
|
<Button primary text on:click={openJsonSchemaEditor}
|
||||||
>Open schema editor</Button
|
>Open schema editor</Button
|
||||||
>
|
>
|
||||||
{:else if isUsersColumn(editableColumn) && datasource?.source !== SourceName.GOOGLE_SHEETS}
|
|
||||||
<Toggle
|
|
||||||
value={editableColumn.subtype === BBReferenceFieldSubType.USERS}
|
|
||||||
on:change={e =>
|
|
||||||
handleTypeChange(
|
|
||||||
makeFieldId(
|
|
||||||
FieldType.BB_REFERENCE,
|
|
||||||
e.detail
|
|
||||||
? BBReferenceFieldSubType.USERS
|
|
||||||
: BBReferenceFieldSubType.USER
|
|
||||||
)
|
|
||||||
)}
|
|
||||||
disabled={!isCreating}
|
|
||||||
thin
|
|
||||||
text="Allow multiple users"
|
|
||||||
/>
|
|
||||||
{/if}
|
{/if}
|
||||||
{#if editableColumn.type === AUTO_TYPE || editableColumn.autocolumn}
|
{#if editableColumn.type === AUTO_TYPE || editableColumn.autocolumn}
|
||||||
<Select
|
<Select
|
||||||
|
|
|
@ -13,7 +13,9 @@
|
||||||
onMount(() => subscribe("edit-column", editColumn))
|
onMount(() => subscribe("edit-column", editColumn))
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<CreateEditColumn
|
{#if editableColumn}
|
||||||
field={editableColumn}
|
<CreateEditColumn
|
||||||
on:updatecolumns={rows.actions.refreshData}
|
field={editableColumn}
|
||||||
/>
|
on:updatecolumns={rows.actions.refreshData}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
|
|
@ -59,13 +59,17 @@
|
||||||
value: FieldType.ATTACHMENTS,
|
value: FieldType.ATTACHMENTS,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "User",
|
label: "Users",
|
||||||
value: `${FieldType.BB_REFERENCE}${BBReferenceFieldSubType.USER}`,
|
value: `${FieldType.BB_REFERENCE}${BBReferenceFieldSubType.USER}`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "Users",
|
label: "Users",
|
||||||
value: `${FieldType.BB_REFERENCE}${BBReferenceFieldSubType.USERS}`,
|
value: `${FieldType.BB_REFERENCE}${BBReferenceFieldSubType.USERS}`,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
label: "User",
|
||||||
|
value: `${FieldType.BB_REFERENCE_SINGLE}${BBReferenceFieldSubType.USER}`,
|
||||||
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
$: {
|
$: {
|
||||||
|
|
|
@ -47,4 +47,5 @@ export const FieldTypeToComponentMap = {
|
||||||
[FieldType.JSON]: "jsonfield",
|
[FieldType.JSON]: "jsonfield",
|
||||||
[FieldType.BARCODEQR]: "codescanner",
|
[FieldType.BARCODEQR]: "codescanner",
|
||||||
[FieldType.BB_REFERENCE]: "bbreferencefield",
|
[FieldType.BB_REFERENCE]: "bbreferencefield",
|
||||||
|
[FieldType.BB_REFERENCE_SINGLE]: "bbreferencesinglefield",
|
||||||
}
|
}
|
||||||
|
|
|
@ -159,15 +159,17 @@ export const FIELDS = {
|
||||||
},
|
},
|
||||||
USER: {
|
USER: {
|
||||||
name: "User",
|
name: "User",
|
||||||
type: FieldType.BB_REFERENCE,
|
type: FieldType.BB_REFERENCE_SINGLE,
|
||||||
subtype: BBReferenceFieldSubType.USER,
|
subtype: BBReferenceFieldSubType.USER,
|
||||||
icon: TypeIconMap[FieldType.USER],
|
icon: TypeIconMap[FieldType.BB_REFERENCE_SINGLE][
|
||||||
|
BBReferenceFieldSubType.USER
|
||||||
|
],
|
||||||
},
|
},
|
||||||
USERS: {
|
USERS: {
|
||||||
name: "Users",
|
name: "User List",
|
||||||
type: FieldType.BB_REFERENCE,
|
type: FieldType.BB_REFERENCE,
|
||||||
subtype: BBReferenceFieldSubType.USERS,
|
subtype: BBReferenceFieldSubType.USER,
|
||||||
icon: TypeIconMap[FieldType.USERS],
|
icon: TypeIconMap[FieldType.BB_REFERENCE][BBReferenceFieldSubType.USER],
|
||||||
constraints: {
|
constraints: {
|
||||||
type: "array",
|
type: "array",
|
||||||
},
|
},
|
||||||
|
|
|
@ -29,6 +29,7 @@ import { JSONUtils, Constants } from "@budibase/frontend-core"
|
||||||
import ActionDefinitions from "components/design/settings/controls/ButtonActionEditor/manifest.json"
|
import ActionDefinitions from "components/design/settings/controls/ButtonActionEditor/manifest.json"
|
||||||
import { environment, licensing } from "stores/portal"
|
import { environment, licensing } from "stores/portal"
|
||||||
import { convertOldFieldFormat } from "components/design/settings/controls/FieldConfiguration/utils"
|
import { convertOldFieldFormat } from "components/design/settings/controls/FieldConfiguration/utils"
|
||||||
|
import { FIELDS } from "constants/backend"
|
||||||
|
|
||||||
const { ContextScopes } = Constants
|
const { ContextScopes } = Constants
|
||||||
|
|
||||||
|
@ -491,7 +492,7 @@ const generateComponentContextBindings = (asset, componentContext) => {
|
||||||
icon: bindingCategory.icon,
|
icon: bindingCategory.icon,
|
||||||
display: {
|
display: {
|
||||||
name: `${fieldSchema.name || key}`,
|
name: `${fieldSchema.name || key}`,
|
||||||
type: fieldSchema.type,
|
type: fieldSchema.display?.type || fieldSchema.type,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
@ -1019,15 +1020,23 @@ export const getSchemaForDatasource = (asset, datasource, options) => {
|
||||||
// are objects
|
// are objects
|
||||||
let fixedSchema = {}
|
let fixedSchema = {}
|
||||||
Object.entries(schema || {}).forEach(([fieldName, fieldSchema]) => {
|
Object.entries(schema || {}).forEach(([fieldName, fieldSchema]) => {
|
||||||
|
const field = Object.values(FIELDS).find(
|
||||||
|
field =>
|
||||||
|
field.type === fieldSchema.type &&
|
||||||
|
field.subtype === fieldSchema.subtype
|
||||||
|
)
|
||||||
|
|
||||||
if (typeof fieldSchema === "string") {
|
if (typeof fieldSchema === "string") {
|
||||||
fixedSchema[fieldName] = {
|
fixedSchema[fieldName] = {
|
||||||
type: fieldSchema,
|
type: fieldSchema,
|
||||||
name: fieldName,
|
name: fieldName,
|
||||||
|
display: { type: fieldSchema },
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
fixedSchema[fieldName] = {
|
fixedSchema[fieldName] = {
|
||||||
...fieldSchema,
|
...fieldSchema,
|
||||||
name: fieldName,
|
name: fieldName,
|
||||||
|
display: { type: field?.name || fieldSchema.type },
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
|
@ -71,6 +71,7 @@
|
||||||
"multifieldselect",
|
"multifieldselect",
|
||||||
"s3upload",
|
"s3upload",
|
||||||
"codescanner",
|
"codescanner",
|
||||||
|
"bbreferencesinglefield",
|
||||||
"bbreferencefield"
|
"bbreferencefield"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|
|
@ -7025,8 +7025,8 @@
|
||||||
},
|
},
|
||||||
"bbreferencefield": {
|
"bbreferencefield": {
|
||||||
"devComment": "As bb reference is only used for user subtype for now, we are using user for icon and labels",
|
"devComment": "As bb reference is only used for user subtype for now, we are using user for icon and labels",
|
||||||
"name": "User Field",
|
"name": "User List Field",
|
||||||
"icon": "User",
|
"icon": "UserGroup",
|
||||||
"styles": ["size"],
|
"styles": ["size"],
|
||||||
"requiredAncestors": ["form"],
|
"requiredAncestors": ["form"],
|
||||||
"editable": true,
|
"editable": true,
|
||||||
|
@ -7130,5 +7130,113 @@
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
"bbreferencesinglefield": {
|
||||||
|
"devComment": "As bb reference is only used for user subtype for now, we are using user for icon and labels",
|
||||||
|
"name": "User Field",
|
||||||
|
"icon": "User",
|
||||||
|
"styles": ["size"],
|
||||||
|
"requiredAncestors": ["form"],
|
||||||
|
"editable": true,
|
||||||
|
"size": {
|
||||||
|
"width": 400,
|
||||||
|
"height": 50
|
||||||
|
},
|
||||||
|
"settings": [
|
||||||
|
{
|
||||||
|
"type": "field/bb_reference_single",
|
||||||
|
"label": "Field",
|
||||||
|
"key": "field",
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "text",
|
||||||
|
"label": "Label",
|
||||||
|
"key": "label"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "text",
|
||||||
|
"label": "Placeholder",
|
||||||
|
"key": "placeholder"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "text",
|
||||||
|
"label": "Default value",
|
||||||
|
"key": "defaultValue"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "text",
|
||||||
|
"label": "Help text",
|
||||||
|
"key": "helpText"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "event",
|
||||||
|
"label": "On change",
|
||||||
|
"key": "onChange",
|
||||||
|
"context": [
|
||||||
|
{
|
||||||
|
"label": "Field Value",
|
||||||
|
"key": "value"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "validation/link",
|
||||||
|
"label": "Validation",
|
||||||
|
"key": "validation"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "boolean",
|
||||||
|
"label": "Search",
|
||||||
|
"key": "autocomplete",
|
||||||
|
"defaultValue": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "boolean",
|
||||||
|
"label": "Disabled",
|
||||||
|
"key": "disabled",
|
||||||
|
"defaultValue": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "boolean",
|
||||||
|
"label": "Read only",
|
||||||
|
"key": "readonly",
|
||||||
|
"defaultValue": false,
|
||||||
|
"dependsOn": {
|
||||||
|
"setting": "disabled",
|
||||||
|
"value": true,
|
||||||
|
"invert": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "select",
|
||||||
|
"label": "Layout",
|
||||||
|
"key": "span",
|
||||||
|
"defaultValue": 6,
|
||||||
|
"hidden": true,
|
||||||
|
"showInBar": true,
|
||||||
|
"barStyle": "buttons",
|
||||||
|
"options": [
|
||||||
|
{
|
||||||
|
"label": "1 column",
|
||||||
|
"value": 6,
|
||||||
|
"barIcon": "Stop",
|
||||||
|
"barTitle": "1 column"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "2 columns",
|
||||||
|
"value": 3,
|
||||||
|
"barIcon": "ColumnTwoA",
|
||||||
|
"barTitle": "2 columns"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "3 columns",
|
||||||
|
"value": 2,
|
||||||
|
"barIcon": "ViewColumn",
|
||||||
|
"barTitle": "3 columns"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,6 +21,7 @@
|
||||||
[FieldType.JSON]: "jsonfield",
|
[FieldType.JSON]: "jsonfield",
|
||||||
[FieldType.BARCODEQR]: "codescanner",
|
[FieldType.BARCODEQR]: "codescanner",
|
||||||
[FieldType.BB_REFERENCE]: "bbreferencefield",
|
[FieldType.BB_REFERENCE]: "bbreferencefield",
|
||||||
|
[FieldType.BB_REFERENCE_SINGLE]: "bbreferencesinglefield",
|
||||||
}
|
}
|
||||||
|
|
||||||
const getFieldSchema = field => {
|
const getFieldSchema = field => {
|
||||||
|
|
|
@ -1,8 +1,10 @@
|
||||||
<script>
|
<script>
|
||||||
import RelationshipField from "./RelationshipField.svelte"
|
|
||||||
import { sdk } from "@budibase/shared-core"
|
import { sdk } from "@budibase/shared-core"
|
||||||
|
import { FieldType } from "@budibase/types"
|
||||||
|
import RelationshipField from "./RelationshipField.svelte"
|
||||||
|
|
||||||
export let defaultValue
|
export let defaultValue
|
||||||
|
export let type = FieldType.BB_REFERENCE
|
||||||
|
|
||||||
function updateUserIDs(value) {
|
function updateUserIDs(value) {
|
||||||
if (Array.isArray(value)) {
|
if (Array.isArray(value)) {
|
||||||
|
@ -22,6 +24,7 @@
|
||||||
|
|
||||||
<RelationshipField
|
<RelationshipField
|
||||||
{...$$props}
|
{...$$props}
|
||||||
|
{type}
|
||||||
datasourceType={"user"}
|
datasourceType={"user"}
|
||||||
primaryDisplay={"email"}
|
primaryDisplay={"email"}
|
||||||
defaultValue={updateReferences(defaultValue)}
|
defaultValue={updateReferences(defaultValue)}
|
||||||
|
|
|
@ -0,0 +1,6 @@
|
||||||
|
<script>
|
||||||
|
import { FieldType } from "@budibase/types"
|
||||||
|
import BBReferenceField from "./BBReferenceField.svelte"
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<BBReferenceField {...$$restProps} type={FieldType.BB_REFERENCE_SINGLE} />
|
|
@ -1,9 +1,9 @@
|
||||||
<script>
|
<script>
|
||||||
import { CoreSelect, CoreMultiselect } from "@budibase/bbui"
|
import { CoreSelect, CoreMultiselect } from "@budibase/bbui"
|
||||||
|
import { FieldType } from "@budibase/types"
|
||||||
import { fetchData, Utils } from "@budibase/frontend-core"
|
import { fetchData, Utils } from "@budibase/frontend-core"
|
||||||
import { getContext, onMount } from "svelte"
|
import { getContext } from "svelte"
|
||||||
import Field from "./Field.svelte"
|
import Field from "./Field.svelte"
|
||||||
import { FieldTypes } from "../../../constants"
|
|
||||||
|
|
||||||
const { API } = getContext("sdk")
|
const { API } = getContext("sdk")
|
||||||
|
|
||||||
|
@ -21,6 +21,7 @@
|
||||||
export let primaryDisplay
|
export let primaryDisplay
|
||||||
export let span
|
export let span
|
||||||
export let helpText = null
|
export let helpText = null
|
||||||
|
export let type = FieldType.LINK
|
||||||
|
|
||||||
let fieldState
|
let fieldState
|
||||||
let fieldApi
|
let fieldApi
|
||||||
|
@ -28,12 +29,10 @@
|
||||||
let tableDefinition
|
let tableDefinition
|
||||||
let searchTerm
|
let searchTerm
|
||||||
let open
|
let open
|
||||||
let initialValue
|
|
||||||
|
|
||||||
$: type =
|
$: multiselect =
|
||||||
datasourceType === "table" ? FieldTypes.LINK : FieldTypes.BB_REFERENCE
|
[FieldType.LINK, FieldType.BB_REFERENCE].includes(type) &&
|
||||||
|
fieldSchema?.relationshipType !== "one-to-many"
|
||||||
$: multiselect = fieldSchema?.relationshipType !== "one-to-many"
|
|
||||||
$: linkedTableId = fieldSchema?.tableId
|
$: linkedTableId = fieldSchema?.tableId
|
||||||
$: fetch = fetchData({
|
$: fetch = fetchData({
|
||||||
API,
|
API,
|
||||||
|
@ -52,18 +51,19 @@
|
||||||
? flatten(fieldState?.value) ?? []
|
? flatten(fieldState?.value) ?? []
|
||||||
: flatten(fieldState?.value)?.[0]
|
: flatten(fieldState?.value)?.[0]
|
||||||
$: component = multiselect ? CoreMultiselect : CoreSelect
|
$: component = multiselect ? CoreMultiselect : CoreSelect
|
||||||
$: expandedDefaultValue = expand(defaultValue)
|
|
||||||
$: primaryDisplay = primaryDisplay || tableDefinition?.primaryDisplay
|
$: primaryDisplay = primaryDisplay || tableDefinition?.primaryDisplay
|
||||||
|
|
||||||
let optionsObj = {}
|
let optionsObj
|
||||||
let initialValuesProcessed
|
|
||||||
|
|
||||||
$: {
|
$: {
|
||||||
if (!initialValuesProcessed && primaryDisplay) {
|
if (primaryDisplay && fieldState && !optionsObj) {
|
||||||
// Persist the initial values as options, allowing them to be present in the dropdown,
|
// Persist the initial values as options, allowing them to be present in the dropdown,
|
||||||
// even if they are not in the inital fetch results
|
// even if they are not in the inital fetch results
|
||||||
initialValuesProcessed = true
|
let valueAsSafeArray = fieldState.value || []
|
||||||
optionsObj = (fieldState?.value || []).reduce((accumulator, value) => {
|
if (!Array.isArray(valueAsSafeArray)) {
|
||||||
|
valueAsSafeArray = [fieldState.value]
|
||||||
|
}
|
||||||
|
optionsObj = valueAsSafeArray.reduce((accumulator, value) => {
|
||||||
// fieldState has to be an array of strings to be valid for an update
|
// fieldState has to be an array of strings to be valid for an update
|
||||||
// therefore we cannot guarantee value will be an object
|
// therefore we cannot guarantee value will be an object
|
||||||
// https://linear.app/budibase/issue/BUDI-7577/refactor-the-relationshipfield-component-to-have-better-support-for
|
// https://linear.app/budibase/issue/BUDI-7577/refactor-the-relationshipfield-component-to-have-better-support-for
|
||||||
|
@ -75,7 +75,7 @@
|
||||||
[primaryDisplay]: value.primaryDisplay,
|
[primaryDisplay]: value.primaryDisplay,
|
||||||
}
|
}
|
||||||
return accumulator
|
return accumulator
|
||||||
}, optionsObj)
|
}, {})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -86,7 +86,7 @@
|
||||||
accumulator[row._id] = row
|
accumulator[row._id] = row
|
||||||
}
|
}
|
||||||
return accumulator
|
return accumulator
|
||||||
}, optionsObj)
|
}, optionsObj || {})
|
||||||
|
|
||||||
return Object.values(result)
|
return Object.values(result)
|
||||||
}
|
}
|
||||||
|
@ -110,17 +110,10 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
$: forceFetchRows(filter)
|
$: forceFetchRows(filter)
|
||||||
$: debouncedFetchRows(
|
$: debouncedFetchRows(searchTerm, primaryDisplay, defaultValue)
|
||||||
searchTerm,
|
|
||||||
primaryDisplay,
|
|
||||||
initialValue || defaultValue
|
|
||||||
)
|
|
||||||
|
|
||||||
const forceFetchRows = async () => {
|
const forceFetchRows = async () => {
|
||||||
// if the filter has changed, then we need to reset the options, clear the selection, and re-fetch
|
|
||||||
optionsObj = {}
|
|
||||||
fieldApi?.setValue([])
|
fieldApi?.setValue([])
|
||||||
selectedValue = []
|
|
||||||
debouncedFetchRows(searchTerm, primaryDisplay, defaultValue)
|
debouncedFetchRows(searchTerm, primaryDisplay, defaultValue)
|
||||||
}
|
}
|
||||||
const fetchRows = async (searchTerm, primaryDisplay, defaultVal) => {
|
const fetchRows = async (searchTerm, primaryDisplay, defaultVal) => {
|
||||||
|
@ -136,7 +129,7 @@
|
||||||
if (defaultVal && !Array.isArray(defaultVal)) {
|
if (defaultVal && !Array.isArray(defaultVal)) {
|
||||||
defaultVal = defaultVal.split(",")
|
defaultVal = defaultVal.split(",")
|
||||||
}
|
}
|
||||||
if (defaultVal && defaultVal.some(val => !optionsObj[val])) {
|
if (defaultVal && optionsObj && defaultVal.some(val => !optionsObj[val])) {
|
||||||
await fetch.update({
|
await fetch.update({
|
||||||
query: { oneOf: { _id: defaultVal } },
|
query: { oneOf: { _id: defaultVal } },
|
||||||
})
|
})
|
||||||
|
@ -162,16 +155,13 @@
|
||||||
if (!values) {
|
if (!values) {
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!Array.isArray(values)) {
|
if (!Array.isArray(values)) {
|
||||||
values = [values]
|
values = [values]
|
||||||
}
|
}
|
||||||
values = values.map(value =>
|
values = values.map(value =>
|
||||||
typeof value === "object" ? value._id : value
|
typeof value === "object" ? value._id : value
|
||||||
)
|
)
|
||||||
// Make sure field state is valid
|
|
||||||
if (values?.length > 0) {
|
|
||||||
fieldApi.setValue(values)
|
|
||||||
}
|
|
||||||
return values
|
return values
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -179,25 +169,20 @@
|
||||||
return row?.[primaryDisplay] || "-"
|
return row?.[primaryDisplay] || "-"
|
||||||
}
|
}
|
||||||
|
|
||||||
const singleHandler = e => {
|
const handleChange = e => {
|
||||||
handleChange(e.detail == null ? [] : [e.detail])
|
let value = e.detail
|
||||||
}
|
if (!multiselect) {
|
||||||
|
value = value == null ? [] : [value]
|
||||||
const multiHandler = e => {
|
}
|
||||||
handleChange(e.detail)
|
|
||||||
}
|
if (
|
||||||
|
type === FieldType.BB_REFERENCE_SINGLE &&
|
||||||
const expand = values => {
|
value &&
|
||||||
if (!values) {
|
Array.isArray(value)
|
||||||
return []
|
) {
|
||||||
|
value = value[0] || null
|
||||||
}
|
}
|
||||||
if (Array.isArray(values)) {
|
|
||||||
return values
|
|
||||||
}
|
|
||||||
return values.split(",").map(value => value.trim())
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleChange = value => {
|
|
||||||
const changed = fieldApi.setValue(value)
|
const changed = fieldApi.setValue(value)
|
||||||
if (onChange && changed) {
|
if (onChange && changed) {
|
||||||
onChange({
|
onChange({
|
||||||
|
@ -211,16 +196,6 @@
|
||||||
fetch.nextPage()
|
fetch.nextPage()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onMount(() => {
|
|
||||||
// if the form is in 'Update' mode, then we need to fetch the matching row so that the value is correctly set
|
|
||||||
if (fieldState?.value) {
|
|
||||||
initialValue =
|
|
||||||
fieldSchema?.relationshipType !== "one-to-many"
|
|
||||||
? flatten(fieldState?.value) ?? []
|
|
||||||
: flatten(fieldState?.value)?.[0]
|
|
||||||
}
|
|
||||||
})
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Field
|
<Field
|
||||||
|
@ -229,7 +204,7 @@
|
||||||
{disabled}
|
{disabled}
|
||||||
{readonly}
|
{readonly}
|
||||||
{validation}
|
{validation}
|
||||||
defaultValue={expandedDefaultValue}
|
{defaultValue}
|
||||||
{type}
|
{type}
|
||||||
{span}
|
{span}
|
||||||
{helpText}
|
{helpText}
|
||||||
|
@ -243,7 +218,7 @@
|
||||||
options={enrichedOptions}
|
options={enrichedOptions}
|
||||||
{autocomplete}
|
{autocomplete}
|
||||||
value={selectedValue}
|
value={selectedValue}
|
||||||
on:change={multiselect ? multiHandler : singleHandler}
|
on:change={handleChange}
|
||||||
on:loadMore={loadMore}
|
on:loadMore={loadMore}
|
||||||
id={fieldState.fieldId}
|
id={fieldState.fieldId}
|
||||||
disabled={fieldState.disabled}
|
disabled={fieldState.disabled}
|
||||||
|
|
|
@ -17,3 +17,4 @@ export { default as jsonfield } from "./JSONField.svelte"
|
||||||
export { default as s3upload } from "./S3Upload.svelte"
|
export { default as s3upload } from "./S3Upload.svelte"
|
||||||
export { default as codescanner } from "./CodeScannerField.svelte"
|
export { default as codescanner } from "./CodeScannerField.svelte"
|
||||||
export { default as bbreferencefield } from "./BBReferenceField.svelte"
|
export { default as bbreferencefield } from "./BBReferenceField.svelte"
|
||||||
|
export { default as bbreferencesinglefield } from "./BBReferenceSingleField.svelte"
|
||||||
|
|
|
@ -125,6 +125,7 @@
|
||||||
filter.type = fieldSchema?.type
|
filter.type = fieldSchema?.type
|
||||||
filter.subtype = fieldSchema?.subtype
|
filter.subtype = fieldSchema?.subtype
|
||||||
filter.formulaType = fieldSchema?.formulaType
|
filter.formulaType = fieldSchema?.formulaType
|
||||||
|
filter.constraints = fieldSchema?.constraints
|
||||||
|
|
||||||
// Update external type based on field
|
// Update external type based on field
|
||||||
filter.externalType = getSchema(filter)?.externalType
|
filter.externalType = getSchema(filter)?.externalType
|
||||||
|
@ -281,7 +282,7 @@
|
||||||
timeOnly={getSchema(filter)?.timeOnly}
|
timeOnly={getSchema(filter)?.timeOnly}
|
||||||
bind:value={filter.value}
|
bind:value={filter.value}
|
||||||
/>
|
/>
|
||||||
{:else if filter.type === FieldType.BB_REFERENCE}
|
{:else if [FieldType.BB_REFERENCE, FieldType.BB_REFERENCE_SINGLE].includes(filter.type)}
|
||||||
<FilterUsers
|
<FilterUsers
|
||||||
bind:value={filter.value}
|
bind:value={filter.value}
|
||||||
multiselect={[
|
multiselect={[
|
||||||
|
|
|
@ -1,19 +1,27 @@
|
||||||
<script>
|
<script>
|
||||||
import { getContext } from "svelte"
|
import { getContext } from "svelte"
|
||||||
|
import { helpers } from "@budibase/shared-core"
|
||||||
import RelationshipCell from "./RelationshipCell.svelte"
|
import RelationshipCell from "./RelationshipCell.svelte"
|
||||||
import { BBReferenceFieldSubType, RelationshipType } from "@budibase/types"
|
import {
|
||||||
|
BBReferenceFieldSubType,
|
||||||
|
FieldType,
|
||||||
|
RelationshipType,
|
||||||
|
} from "@budibase/types"
|
||||||
|
|
||||||
export let api
|
export let api
|
||||||
|
export let hideCounter = false
|
||||||
|
export let schema
|
||||||
|
|
||||||
const { API } = getContext("grid")
|
const { API } = getContext("grid")
|
||||||
const { subtype } = $$props.schema
|
const { type, subtype } = schema
|
||||||
|
|
||||||
const schema = {
|
$: schema = {
|
||||||
...$$props.schema,
|
...$$props.schema,
|
||||||
// This is not really used, just adding some content to be able to render the relationship cell
|
// This is not really used, just adding some content to be able to render the relationship cell
|
||||||
tableId: "external",
|
tableId: "external",
|
||||||
relationshipType:
|
relationshipType:
|
||||||
subtype === BBReferenceFieldSubType.USER
|
type === FieldType.BB_REFERENCE_SINGLE ||
|
||||||
|
helpers.schema.isDeprecatedSingleUserColumn(schema)
|
||||||
? RelationshipType.ONE_TO_MANY
|
? RelationshipType.ONE_TO_MANY
|
||||||
: RelationshipType.MANY_TO_MANY,
|
: RelationshipType.MANY_TO_MANY,
|
||||||
}
|
}
|
||||||
|
@ -44,8 +52,9 @@
|
||||||
|
|
||||||
<RelationshipCell
|
<RelationshipCell
|
||||||
bind:api
|
bind:api
|
||||||
{...$$props}
|
{...$$restProps}
|
||||||
{schema}
|
{schema}
|
||||||
{searchFunction}
|
{searchFunction}
|
||||||
primaryDisplay={"email"}
|
primaryDisplay={"email"}
|
||||||
|
{hideCounter}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -0,0 +1,22 @@
|
||||||
|
<script>
|
||||||
|
import BbReferenceCell from "./BBReferenceCell.svelte"
|
||||||
|
|
||||||
|
export let value
|
||||||
|
export let onChange
|
||||||
|
export let api
|
||||||
|
|
||||||
|
$: arrayValue = (!Array.isArray(value) && value ? [value] : value) || []
|
||||||
|
|
||||||
|
$: onValueChange = value => {
|
||||||
|
value = value[0] || null
|
||||||
|
onChange(value)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<BbReferenceCell
|
||||||
|
bind:api
|
||||||
|
{...$$restProps}
|
||||||
|
value={arrayValue}
|
||||||
|
onChange={onValueChange}
|
||||||
|
hideCounter={true}
|
||||||
|
/>
|
|
@ -17,6 +17,7 @@
|
||||||
export let contentLines = 1
|
export let contentLines = 1
|
||||||
export let searchFunction = API.searchTable
|
export let searchFunction = API.searchTable
|
||||||
export let primaryDisplay
|
export let primaryDisplay
|
||||||
|
export let hideCounter = false
|
||||||
|
|
||||||
const color = getColor(0)
|
const color = getColor(0)
|
||||||
|
|
||||||
|
@ -263,7 +264,7 @@
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{#if value?.length}
|
{#if !hideCounter && value?.length}
|
||||||
<div class="count">
|
<div class="count">
|
||||||
{value?.length || 0}
|
{value?.length || 0}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -7,11 +7,6 @@
|
||||||
} from "@budibase/bbui"
|
} from "@budibase/bbui"
|
||||||
import { getContext } from "svelte"
|
import { getContext } from "svelte"
|
||||||
import { ValidColumnNameRegex } from "@budibase/shared-core"
|
import { ValidColumnNameRegex } from "@budibase/shared-core"
|
||||||
import {
|
|
||||||
BBReferenceFieldSubType,
|
|
||||||
FieldType,
|
|
||||||
RelationshipType,
|
|
||||||
} from "@budibase/types"
|
|
||||||
|
|
||||||
const { API, definition, rows } = getContext("grid")
|
const { API, definition, rows } = getContext("grid")
|
||||||
|
|
||||||
|
@ -33,20 +28,11 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
const migrateUserColumn = async () => {
|
const migrateUserColumn = async () => {
|
||||||
let subtype = BBReferenceFieldSubType.USERS
|
|
||||||
if (column.schema.relationshipType === RelationshipType.ONE_TO_MANY) {
|
|
||||||
subtype = BBReferenceFieldSubType.USER
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await API.migrateColumn({
|
await API.migrateColumn({
|
||||||
tableId: $definition._id,
|
tableId: $definition._id,
|
||||||
oldColumn: column.schema,
|
oldColumn: column.schema.name,
|
||||||
newColumn: {
|
newColumn: newColumnName,
|
||||||
name: newColumnName,
|
|
||||||
type: FieldType.BB_REFERENCE,
|
|
||||||
subtype,
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
notifications.success("Column migrated")
|
notifications.success("Column migrated")
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|
|
@ -13,6 +13,7 @@ import JSONCell from "../cells/JSONCell.svelte"
|
||||||
import AttachmentCell from "../cells/AttachmentCell.svelte"
|
import AttachmentCell from "../cells/AttachmentCell.svelte"
|
||||||
import AttachmentSingleCell from "../cells/AttachmentSingleCell.svelte"
|
import AttachmentSingleCell from "../cells/AttachmentSingleCell.svelte"
|
||||||
import BBReferenceCell from "../cells/BBReferenceCell.svelte"
|
import BBReferenceCell from "../cells/BBReferenceCell.svelte"
|
||||||
|
import BBReferenceSingleCell from "../cells/BBReferenceSingleCell.svelte"
|
||||||
|
|
||||||
const TypeComponentMap = {
|
const TypeComponentMap = {
|
||||||
[FieldType.STRING]: TextCell,
|
[FieldType.STRING]: TextCell,
|
||||||
|
@ -29,6 +30,7 @@ const TypeComponentMap = {
|
||||||
[FieldType.FORMULA]: FormulaCell,
|
[FieldType.FORMULA]: FormulaCell,
|
||||||
[FieldType.JSON]: JSONCell,
|
[FieldType.JSON]: JSONCell,
|
||||||
[FieldType.BB_REFERENCE]: BBReferenceCell,
|
[FieldType.BB_REFERENCE]: BBReferenceCell,
|
||||||
|
[FieldType.BB_REFERENCE_SINGLE]: BBReferenceSingleCell,
|
||||||
}
|
}
|
||||||
export const getCellRenderer = column => {
|
export const getCellRenderer = column => {
|
||||||
return TypeComponentMap[column?.schema?.type] || TextCell
|
return TypeComponentMap[column?.schema?.type] || TextCell
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { helpers } from "@budibase/shared-core"
|
||||||
import { TypeIconMap } from "../../../constants"
|
import { TypeIconMap } from "../../../constants"
|
||||||
|
|
||||||
// we can't use "-" for joining the ID/field, as this can be present in the ID or column name
|
// we can't use "-" for joining the ID/field, as this can be present in the ID or column name
|
||||||
|
@ -28,8 +29,12 @@ export const getColumnIcon = column => {
|
||||||
if (column.schema.autocolumn) {
|
if (column.schema.autocolumn) {
|
||||||
return "MagicWand"
|
return "MagicWand"
|
||||||
}
|
}
|
||||||
const { type, subtype } = column.schema
|
|
||||||
|
|
||||||
|
if (helpers.schema.isDeprecatedSingleUserColumn(column.schema)) {
|
||||||
|
return "User"
|
||||||
|
}
|
||||||
|
|
||||||
|
const { type, subtype } = column.schema
|
||||||
const result =
|
const result =
|
||||||
typeof TypeIconMap[type] === "object" && subtype
|
typeof TypeIconMap[type] === "object" && subtype
|
||||||
? TypeIconMap[type][subtype]
|
? TypeIconMap[type][subtype]
|
||||||
|
|
|
@ -132,10 +132,11 @@ export const TypeIconMap = {
|
||||||
[FieldType.JSON]: "Brackets",
|
[FieldType.JSON]: "Brackets",
|
||||||
[FieldType.BIGINT]: "TagBold",
|
[FieldType.BIGINT]: "TagBold",
|
||||||
[FieldType.AUTO]: "MagicWand",
|
[FieldType.AUTO]: "MagicWand",
|
||||||
[FieldType.USER]: "User",
|
|
||||||
[FieldType.USERS]: "UserGroup",
|
|
||||||
[FieldType.BB_REFERENCE]: {
|
[FieldType.BB_REFERENCE]: {
|
||||||
[BBReferenceFieldSubType.USER]: "User",
|
[BBReferenceFieldSubType.USER]: "UserGroup",
|
||||||
[BBReferenceFieldSubType.USERS]: "UserGroup",
|
[BBReferenceFieldSubType.USERS]: "UserGroup",
|
||||||
},
|
},
|
||||||
|
[FieldType.BB_REFERENCE_SINGLE]: {
|
||||||
|
[BBReferenceFieldSubType.USER]: "User",
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
// need to handle table name + field or just field, depending on if relationships used
|
// need to handle table name + field or just field, depending on if relationships used
|
||||||
import { FieldType, Row, Table } from "@budibase/types"
|
import { FieldType, Row, Table } from "@budibase/types"
|
||||||
|
import { helpers } from "@budibase/shared-core"
|
||||||
import { generateRowIdField } from "../../../../integrations/utils"
|
import { generateRowIdField } from "../../../../integrations/utils"
|
||||||
import { CONSTANT_INTERNAL_ROW_COLS } from "../../../../db/utils"
|
import { CONSTANT_INTERNAL_ROW_COLS } from "../../../../db/utils"
|
||||||
|
|
||||||
|
@ -107,12 +108,17 @@ export function basicProcessing({
|
||||||
|
|
||||||
export function fixArrayTypes(row: Row, table: Table) {
|
export function fixArrayTypes(row: Row, table: Table) {
|
||||||
for (let [fieldName, schema] of Object.entries(table.schema)) {
|
for (let [fieldName, schema] of Object.entries(table.schema)) {
|
||||||
if (schema.type === FieldType.ARRAY && typeof row[fieldName] === "string") {
|
if (
|
||||||
|
[FieldType.ARRAY, FieldType.BB_REFERENCE].includes(schema.type) &&
|
||||||
|
typeof row[fieldName] === "string"
|
||||||
|
) {
|
||||||
try {
|
try {
|
||||||
row[fieldName] = JSON.parse(row[fieldName])
|
row[fieldName] = JSON.parse(row[fieldName])
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
// couldn't convert back to array, ignore
|
if (!helpers.schema.isDeprecatedSingleUserColumn(schema)) {
|
||||||
delete row[fieldName]
|
// couldn't convert back to array, ignore
|
||||||
|
delete row[fieldName]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -180,5 +180,5 @@ export async function migrate(ctx: UserCtx<MigrateRequest, MigrateResponse>) {
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx.status = 200
|
ctx.status = 200
|
||||||
ctx.body = { message: `Column ${oldColumn.name} migrated.` }
|
ctx.body = { message: `Column ${oldColumn} migrated.` }
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,6 +16,7 @@ import {
|
||||||
SourceName,
|
SourceName,
|
||||||
Table,
|
Table,
|
||||||
TableSchema,
|
TableSchema,
|
||||||
|
SupportedSqlTypes,
|
||||||
} from "@budibase/types"
|
} from "@budibase/types"
|
||||||
import { DatabaseName, getDatasource } from "../../../integrations/tests/utils"
|
import { DatabaseName, getDatasource } from "../../../integrations/tests/utils"
|
||||||
import { tableForDatasource } from "../../../tests/utilities/structures"
|
import { tableForDatasource } from "../../../tests/utilities/structures"
|
||||||
|
@ -261,20 +262,6 @@ describe("/datasources", () => {
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
|
||||||
type SupportedSqlTypes =
|
|
||||||
| FieldType.STRING
|
|
||||||
| FieldType.BARCODEQR
|
|
||||||
| FieldType.LONGFORM
|
|
||||||
| FieldType.OPTIONS
|
|
||||||
| FieldType.DATETIME
|
|
||||||
| FieldType.NUMBER
|
|
||||||
| FieldType.BOOLEAN
|
|
||||||
| FieldType.FORMULA
|
|
||||||
| FieldType.BIGINT
|
|
||||||
| FieldType.BB_REFERENCE
|
|
||||||
| FieldType.LINK
|
|
||||||
| FieldType.ARRAY
|
|
||||||
|
|
||||||
const fullSchema: {
|
const fullSchema: {
|
||||||
[type in SupportedSqlTypes]: FieldSchema & { type: type }
|
[type in SupportedSqlTypes]: FieldSchema & { type: type }
|
||||||
} = {
|
} = {
|
||||||
|
@ -337,7 +324,12 @@ describe("/datasources", () => {
|
||||||
[FieldType.BB_REFERENCE]: {
|
[FieldType.BB_REFERENCE]: {
|
||||||
name: "bb_reference",
|
name: "bb_reference",
|
||||||
type: FieldType.BB_REFERENCE,
|
type: FieldType.BB_REFERENCE,
|
||||||
subtype: BBReferenceFieldSubType.USERS,
|
subtype: BBReferenceFieldSubType.USER,
|
||||||
|
},
|
||||||
|
[FieldType.BB_REFERENCE_SINGLE]: {
|
||||||
|
name: "bb_reference_single",
|
||||||
|
type: FieldType.BB_REFERENCE_SINGLE,
|
||||||
|
subtype: BBReferenceFieldSubType.USER,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -262,11 +262,19 @@ describe.each([
|
||||||
{ name: "serverDate", appointment: serverTime.toISOString() },
|
{ name: "serverDate", appointment: serverTime.toISOString() },
|
||||||
{
|
{
|
||||||
name: "single user, session user",
|
name: "single user, session user",
|
||||||
single_user: JSON.stringify([currentUser]),
|
single_user: JSON.stringify(currentUser),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "single user",
|
name: "single user",
|
||||||
single_user: JSON.stringify([globalUsers[0]]),
|
single_user: JSON.stringify(globalUsers[0]),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "deprecated single user, session user",
|
||||||
|
deprecated_single_user: JSON.stringify([currentUser]),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "deprecated single user",
|
||||||
|
deprecated_single_user: JSON.stringify([globalUsers[0]]),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "multi user",
|
name: "multi user",
|
||||||
|
@ -276,6 +284,14 @@ describe.each([
|
||||||
name: "multi user with session user",
|
name: "multi user with session user",
|
||||||
multi_user: JSON.stringify([...globalUsers, currentUser]),
|
multi_user: JSON.stringify([...globalUsers, currentUser]),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "deprecated multi user",
|
||||||
|
deprecated_multi_user: JSON.stringify(globalUsers),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "deprecated multi user with session user",
|
||||||
|
deprecated_multi_user: JSON.stringify([...globalUsers, currentUser]),
|
||||||
|
},
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -301,13 +317,29 @@ describe.each([
|
||||||
appointment: { name: "appointment", type: FieldType.DATETIME },
|
appointment: { name: "appointment", type: FieldType.DATETIME },
|
||||||
single_user: {
|
single_user: {
|
||||||
name: "single_user",
|
name: "single_user",
|
||||||
|
type: FieldType.BB_REFERENCE_SINGLE,
|
||||||
|
subtype: BBReferenceFieldSubType.USER,
|
||||||
|
},
|
||||||
|
deprecated_single_user: {
|
||||||
|
name: "deprecated_single_user",
|
||||||
type: FieldType.BB_REFERENCE,
|
type: FieldType.BB_REFERENCE,
|
||||||
subtype: BBReferenceFieldSubType.USER,
|
subtype: BBReferenceFieldSubType.USER,
|
||||||
},
|
},
|
||||||
multi_user: {
|
multi_user: {
|
||||||
name: "multi_user",
|
name: "multi_user",
|
||||||
type: FieldType.BB_REFERENCE,
|
type: FieldType.BB_REFERENCE,
|
||||||
|
subtype: BBReferenceFieldSubType.USER,
|
||||||
|
constraints: {
|
||||||
|
type: "array",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
deprecated_multi_user: {
|
||||||
|
name: "deprecated_multi_user",
|
||||||
|
type: FieldType.BB_REFERENCE,
|
||||||
subtype: BBReferenceFieldSubType.USERS,
|
subtype: BBReferenceFieldSubType.USERS,
|
||||||
|
constraints: {
|
||||||
|
type: "array",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
await createRows(rows(config.getUser()))
|
await createRows(rows(config.getUser()))
|
||||||
|
@ -398,7 +430,18 @@ describe.each([
|
||||||
}).toContainExactly([
|
}).toContainExactly([
|
||||||
{
|
{
|
||||||
name: "single user, session user",
|
name: "single user, session user",
|
||||||
single_user: [{ _id: config.getUser()._id }],
|
single_user: { _id: config.getUser()._id },
|
||||||
|
},
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should match a deprecated single user row by the session user id", async () => {
|
||||||
|
await expectQuery({
|
||||||
|
equal: { deprecated_single_user: "{{ [user]._id }}" },
|
||||||
|
}).toContainExactly([
|
||||||
|
{
|
||||||
|
name: "deprecated single user, session user",
|
||||||
|
deprecated_single_user: [{ _id: config.getUser()._id }],
|
||||||
},
|
},
|
||||||
])
|
])
|
||||||
})
|
})
|
||||||
|
@ -420,6 +463,23 @@ describe.each([
|
||||||
])
|
])
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// TODO(samwho): fix for SQS
|
||||||
|
!isSqs &&
|
||||||
|
it("should match the session user id in a deprecated multi user field", async () => {
|
||||||
|
const allUsers = [...globalUsers, config.getUser()].map((user: any) => {
|
||||||
|
return { _id: user._id }
|
||||||
|
})
|
||||||
|
|
||||||
|
await expectQuery({
|
||||||
|
contains: { deprecated_multi_user: ["{{ [user]._id }}"] },
|
||||||
|
}).toContainExactly([
|
||||||
|
{
|
||||||
|
name: "deprecated multi user with session user",
|
||||||
|
deprecated_multi_user: allUsers,
|
||||||
|
},
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
// TODO(samwho): fix for SQS
|
// TODO(samwho): fix for SQS
|
||||||
!isSqs &&
|
!isSqs &&
|
||||||
it("should not match the session user id in a multi user field", async () => {
|
it("should not match the session user id in a multi user field", async () => {
|
||||||
|
@ -436,6 +496,22 @@ describe.each([
|
||||||
])
|
])
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// TODO(samwho): fix for SQS
|
||||||
|
!isSqs &&
|
||||||
|
it("should not match the session user id in a deprecated multi user field", async () => {
|
||||||
|
await expectQuery({
|
||||||
|
notContains: { deprecated_multi_user: ["{{ [user]._id }}"] },
|
||||||
|
notEmpty: { deprecated_multi_user: true },
|
||||||
|
}).toContainExactly([
|
||||||
|
{
|
||||||
|
name: "deprecated multi user",
|
||||||
|
deprecated_multi_user: globalUsers.map((user: any) => {
|
||||||
|
return { _id: user._id }
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
it("should match the session user id and a user table row id using helpers, user binding and a static user id.", async () => {
|
it("should match the session user id and a user table row id using helpers, user binding and a static user id.", async () => {
|
||||||
await expectQuery({
|
await expectQuery({
|
||||||
oneOf: {
|
oneOf: {
|
||||||
|
@ -447,11 +523,31 @@ describe.each([
|
||||||
}).toContainExactly([
|
}).toContainExactly([
|
||||||
{
|
{
|
||||||
name: "single user, session user",
|
name: "single user, session user",
|
||||||
single_user: [{ _id: config.getUser()._id }],
|
single_user: { _id: config.getUser()._id },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "single user",
|
name: "single user",
|
||||||
single_user: [{ _id: globalUsers[0]._id }],
|
single_user: { _id: globalUsers[0]._id },
|
||||||
|
},
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should match the session user id and a user table row id using helpers, user binding and a static user id. (deprecated single user)", async () => {
|
||||||
|
await expectQuery({
|
||||||
|
oneOf: {
|
||||||
|
deprecated_single_user: [
|
||||||
|
"{{ default [user]._id '_empty_' }}",
|
||||||
|
globalUsers[0]._id,
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}).toContainExactly([
|
||||||
|
{
|
||||||
|
name: "deprecated single user, session user",
|
||||||
|
deprecated_single_user: [{ _id: config.getUser()._id }],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "deprecated single user",
|
||||||
|
deprecated_single_user: [{ _id: globalUsers[0]._id }],
|
||||||
},
|
},
|
||||||
])
|
])
|
||||||
})
|
})
|
||||||
|
@ -467,7 +563,23 @@ describe.each([
|
||||||
}).toContainExactly([
|
}).toContainExactly([
|
||||||
{
|
{
|
||||||
name: "single user",
|
name: "single user",
|
||||||
single_user: [{ _id: globalUsers[0]._id }],
|
single_user: { _id: globalUsers[0]._id },
|
||||||
|
},
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should resolve 'default' helper to '_empty_' when binding resolves to nothing (deprecated single user)", async () => {
|
||||||
|
await expectQuery({
|
||||||
|
oneOf: {
|
||||||
|
deprecated_single_user: [
|
||||||
|
"{{ default [user]._idx '_empty_' }}",
|
||||||
|
globalUsers[0]._id,
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}).toContainExactly([
|
||||||
|
{
|
||||||
|
name: "deprecated single user",
|
||||||
|
deprecated_single_user: [{ _id: globalUsers[0]._id }],
|
||||||
},
|
},
|
||||||
])
|
])
|
||||||
})
|
})
|
||||||
|
|
|
@ -545,16 +545,16 @@ describe.each([
|
||||||
)
|
)
|
||||||
|
|
||||||
await config.api.table.migrate(table._id!, {
|
await config.api.table.migrate(table._id!, {
|
||||||
oldColumn: table.schema["user relationship"],
|
oldColumn: "user relationship",
|
||||||
newColumn: {
|
newColumn: "user column",
|
||||||
name: "user column",
|
|
||||||
type: FieldType.BB_REFERENCE,
|
|
||||||
subtype: BBReferenceFieldSubType.USER,
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const migratedTable = await config.api.table.get(table._id!)
|
const migratedTable = await config.api.table.get(table._id!)
|
||||||
expect(migratedTable.schema["user column"]).toBeDefined()
|
expect(migratedTable.schema["user column"]).toEqual({
|
||||||
|
name: "user column",
|
||||||
|
type: FieldType.BB_REFERENCE_SINGLE,
|
||||||
|
subtype: BBReferenceFieldSubType.USER,
|
||||||
|
})
|
||||||
expect(migratedTable.schema["user relationship"]).not.toBeDefined()
|
expect(migratedTable.schema["user relationship"]).not.toBeDefined()
|
||||||
|
|
||||||
const migratedRows = await config.api.row.fetch(table._id!)
|
const migratedRows = await config.api.row.fetch(table._id!)
|
||||||
|
@ -567,7 +567,7 @@ describe.each([
|
||||||
expect(migratedRow["user column"]).toBeDefined()
|
expect(migratedRow["user column"]).toBeDefined()
|
||||||
expect(migratedRow["user relationship"]).not.toBeDefined()
|
expect(migratedRow["user relationship"]).not.toBeDefined()
|
||||||
expect(row["user relationship"][0]._id).toEqual(
|
expect(row["user relationship"][0]._id).toEqual(
|
||||||
migratedRow["user column"][0]._id
|
migratedRow["user column"]._id
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -610,16 +610,19 @@ describe.each([
|
||||||
)
|
)
|
||||||
|
|
||||||
await config.api.table.migrate(table._id!, {
|
await config.api.table.migrate(table._id!, {
|
||||||
oldColumn: table.schema["user relationship"],
|
oldColumn: "user relationship",
|
||||||
newColumn: {
|
newColumn: "user column",
|
||||||
name: "user column",
|
|
||||||
type: FieldType.BB_REFERENCE,
|
|
||||||
subtype: BBReferenceFieldSubType.USERS,
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const migratedTable = await config.api.table.get(table._id!)
|
const migratedTable = await config.api.table.get(table._id!)
|
||||||
expect(migratedTable.schema["user column"]).toBeDefined()
|
expect(migratedTable.schema["user column"]).toEqual({
|
||||||
|
name: "user column",
|
||||||
|
type: FieldType.BB_REFERENCE,
|
||||||
|
subtype: BBReferenceFieldSubType.USER,
|
||||||
|
constraints: {
|
||||||
|
type: "array",
|
||||||
|
},
|
||||||
|
})
|
||||||
expect(migratedTable.schema["user relationship"]).not.toBeDefined()
|
expect(migratedTable.schema["user relationship"]).not.toBeDefined()
|
||||||
|
|
||||||
const migratedRow = await config.api.row.get(table._id!, testRow._id!)
|
const migratedRow = await config.api.row.get(table._id!, testRow._id!)
|
||||||
|
@ -662,16 +665,19 @@ describe.each([
|
||||||
})
|
})
|
||||||
|
|
||||||
await config.api.table.migrate(table._id!, {
|
await config.api.table.migrate(table._id!, {
|
||||||
oldColumn: table.schema["user relationship"],
|
oldColumn: "user relationship",
|
||||||
newColumn: {
|
newColumn: "user column",
|
||||||
name: "user column",
|
|
||||||
type: FieldType.BB_REFERENCE,
|
|
||||||
subtype: BBReferenceFieldSubType.USERS,
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const migratedTable = await config.api.table.get(table._id!)
|
const migratedTable = await config.api.table.get(table._id!)
|
||||||
expect(migratedTable.schema["user column"]).toBeDefined()
|
expect(migratedTable.schema["user column"]).toEqual({
|
||||||
|
name: "user column",
|
||||||
|
type: FieldType.BB_REFERENCE,
|
||||||
|
subtype: BBReferenceFieldSubType.USER,
|
||||||
|
constraints: {
|
||||||
|
type: "array",
|
||||||
|
},
|
||||||
|
})
|
||||||
expect(migratedTable.schema["user relationship"]).not.toBeDefined()
|
expect(migratedTable.schema["user relationship"]).not.toBeDefined()
|
||||||
|
|
||||||
const row1Migrated = await config.api.row.get(table._id!, row1._id!)
|
const row1Migrated = await config.api.row.get(table._id!, row1._id!)
|
||||||
|
@ -717,16 +723,19 @@ describe.each([
|
||||||
})
|
})
|
||||||
|
|
||||||
await config.api.table.migrate(table._id!, {
|
await config.api.table.migrate(table._id!, {
|
||||||
oldColumn: table.schema["user relationship"],
|
oldColumn: "user relationship",
|
||||||
newColumn: {
|
newColumn: "user column",
|
||||||
name: "user column",
|
|
||||||
type: FieldType.BB_REFERENCE,
|
|
||||||
subtype: BBReferenceFieldSubType.USERS,
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const migratedTable = await config.api.table.get(table._id!)
|
const migratedTable = await config.api.table.get(table._id!)
|
||||||
expect(migratedTable.schema["user column"]).toBeDefined()
|
expect(migratedTable.schema["user column"]).toEqual({
|
||||||
|
name: "user column",
|
||||||
|
type: FieldType.BB_REFERENCE,
|
||||||
|
subtype: BBReferenceFieldSubType.USER,
|
||||||
|
constraints: {
|
||||||
|
type: "array",
|
||||||
|
},
|
||||||
|
})
|
||||||
expect(migratedTable.schema["user relationship"]).not.toBeDefined()
|
expect(migratedTable.schema["user relationship"]).not.toBeDefined()
|
||||||
|
|
||||||
const row1Migrated = await config.api.row.get(table._id!, row1._id!)
|
const row1Migrated = await config.api.row.get(table._id!, row1._id!)
|
||||||
|
@ -776,12 +785,8 @@ describe.each([
|
||||||
await config.api.table.migrate(
|
await config.api.table.migrate(
|
||||||
table._id!,
|
table._id!,
|
||||||
{
|
{
|
||||||
oldColumn: table.schema["user relationship"],
|
oldColumn: "user relationship",
|
||||||
newColumn: {
|
newColumn: "",
|
||||||
name: "",
|
|
||||||
type: FieldType.BB_REFERENCE,
|
|
||||||
subtype: BBReferenceFieldSubType.USERS,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
{ status: 400 }
|
{ status: 400 }
|
||||||
)
|
)
|
||||||
|
@ -791,12 +796,8 @@ describe.each([
|
||||||
await config.api.table.migrate(
|
await config.api.table.migrate(
|
||||||
table._id!,
|
table._id!,
|
||||||
{
|
{
|
||||||
oldColumn: table.schema["user relationship"],
|
oldColumn: "user relationship",
|
||||||
newColumn: {
|
newColumn: "_id",
|
||||||
name: "_id",
|
|
||||||
type: FieldType.BB_REFERENCE,
|
|
||||||
subtype: BBReferenceFieldSubType.USERS,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
{ status: 400 }
|
{ status: 400 }
|
||||||
)
|
)
|
||||||
|
@ -806,12 +807,8 @@ describe.each([
|
||||||
await config.api.table.migrate(
|
await config.api.table.migrate(
|
||||||
table._id!,
|
table._id!,
|
||||||
{
|
{
|
||||||
oldColumn: table.schema["user relationship"],
|
oldColumn: "user relationship",
|
||||||
newColumn: {
|
newColumn: "num",
|
||||||
name: "num",
|
|
||||||
type: FieldType.BB_REFERENCE,
|
|
||||||
subtype: BBReferenceFieldSubType.USERS,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
{ status: 400 }
|
{ status: 400 }
|
||||||
)
|
)
|
||||||
|
@ -821,16 +818,8 @@ describe.each([
|
||||||
await config.api.table.migrate(
|
await config.api.table.migrate(
|
||||||
table._id!,
|
table._id!,
|
||||||
{
|
{
|
||||||
oldColumn: {
|
oldColumn: "not a column",
|
||||||
name: "not a column",
|
newColumn: "new column",
|
||||||
type: FieldType.BB_REFERENCE,
|
|
||||||
subtype: BBReferenceFieldSubType.USERS,
|
|
||||||
},
|
|
||||||
newColumn: {
|
|
||||||
name: "new column",
|
|
||||||
type: FieldType.BB_REFERENCE,
|
|
||||||
subtype: BBReferenceFieldSubType.USERS,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
{ status: 400 }
|
{ status: 400 }
|
||||||
)
|
)
|
||||||
|
|
|
@ -12,7 +12,6 @@ import SqlTableQueryBuilder from "./sqlTable"
|
||||||
import {
|
import {
|
||||||
BBReferenceFieldMetadata,
|
BBReferenceFieldMetadata,
|
||||||
FieldSchema,
|
FieldSchema,
|
||||||
BBReferenceFieldSubType,
|
|
||||||
FieldType,
|
FieldType,
|
||||||
JsonFieldMetadata,
|
JsonFieldMetadata,
|
||||||
Operation,
|
Operation,
|
||||||
|
@ -27,6 +26,7 @@ import {
|
||||||
INTERNAL_TABLE_SOURCE_ID,
|
INTERNAL_TABLE_SOURCE_ID,
|
||||||
} from "@budibase/types"
|
} from "@budibase/types"
|
||||||
import environment from "../../environment"
|
import environment from "../../environment"
|
||||||
|
import { helpers } from "@budibase/shared-core"
|
||||||
|
|
||||||
type QueryFunction = (query: SqlQuery | SqlQuery[], operation: Operation) => any
|
type QueryFunction = (query: SqlQuery | SqlQuery[], operation: Operation) => any
|
||||||
|
|
||||||
|
@ -787,7 +787,7 @@ class SqlQueryBuilder extends SqlTableQueryBuilder {
|
||||||
return (
|
return (
|
||||||
field.type === FieldType.JSON ||
|
field.type === FieldType.JSON ||
|
||||||
(field.type === FieldType.BB_REFERENCE &&
|
(field.type === FieldType.BB_REFERENCE &&
|
||||||
field.subtype === BBReferenceFieldSubType.USERS)
|
!helpers.schema.isDeprecatedSingleUserColumn(field))
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
import { Knex, knex } from "knex"
|
import { Knex, knex } from "knex"
|
||||||
import {
|
import {
|
||||||
BBReferenceFieldSubType,
|
|
||||||
FieldType,
|
FieldType,
|
||||||
NumberFieldMetadata,
|
NumberFieldMetadata,
|
||||||
Operation,
|
Operation,
|
||||||
|
@ -12,7 +11,7 @@ import {
|
||||||
TableSourceType,
|
TableSourceType,
|
||||||
} from "@budibase/types"
|
} from "@budibase/types"
|
||||||
import { breakExternalTableId, getNativeSql, SqlClient } from "../utils"
|
import { breakExternalTableId, getNativeSql, SqlClient } from "../utils"
|
||||||
import { utils } from "@budibase/shared-core"
|
import { helpers, utils } from "@budibase/shared-core"
|
||||||
import SchemaBuilder = Knex.SchemaBuilder
|
import SchemaBuilder = Knex.SchemaBuilder
|
||||||
import CreateTableBuilder = Knex.CreateTableBuilder
|
import CreateTableBuilder = Knex.CreateTableBuilder
|
||||||
|
|
||||||
|
@ -54,27 +53,15 @@ function generateSchema(
|
||||||
) {
|
) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
switch (column.type) {
|
const columnType = column.type
|
||||||
|
switch (columnType) {
|
||||||
case FieldType.STRING:
|
case FieldType.STRING:
|
||||||
case FieldType.OPTIONS:
|
case FieldType.OPTIONS:
|
||||||
case FieldType.LONGFORM:
|
case FieldType.LONGFORM:
|
||||||
case FieldType.BARCODEQR:
|
case FieldType.BARCODEQR:
|
||||||
|
case FieldType.BB_REFERENCE_SINGLE:
|
||||||
schema.text(key)
|
schema.text(key)
|
||||||
break
|
break
|
||||||
case FieldType.BB_REFERENCE: {
|
|
||||||
const subtype = column.subtype
|
|
||||||
switch (subtype) {
|
|
||||||
case BBReferenceFieldSubType.USER:
|
|
||||||
schema.text(key)
|
|
||||||
break
|
|
||||||
case BBReferenceFieldSubType.USERS:
|
|
||||||
schema.json(key)
|
|
||||||
break
|
|
||||||
default:
|
|
||||||
throw utils.unreachable(subtype)
|
|
||||||
}
|
|
||||||
break
|
|
||||||
}
|
|
||||||
case FieldType.NUMBER:
|
case FieldType.NUMBER:
|
||||||
// if meta is specified then this is a junction table entry
|
// if meta is specified then this is a junction table entry
|
||||||
if (column.meta && column.meta.toKey && column.meta.toTable) {
|
if (column.meta && column.meta.toKey && column.meta.toTable) {
|
||||||
|
@ -97,7 +84,13 @@ function generateSchema(
|
||||||
})
|
})
|
||||||
break
|
break
|
||||||
case FieldType.ARRAY:
|
case FieldType.ARRAY:
|
||||||
schema.json(key)
|
case FieldType.BB_REFERENCE:
|
||||||
|
if (helpers.schema.isDeprecatedSingleUserColumn(column)) {
|
||||||
|
// This is still required for unit testing, in order to create "deprecated" schemas
|
||||||
|
schema.text(key)
|
||||||
|
} else {
|
||||||
|
schema.json(key)
|
||||||
|
}
|
||||||
break
|
break
|
||||||
case FieldType.LINK:
|
case FieldType.LINK:
|
||||||
// this side of the relationship doesn't need any SQL work
|
// this side of the relationship doesn't need any SQL work
|
||||||
|
@ -127,6 +120,18 @@ function generateSchema(
|
||||||
.references(`${tableName}.${relatedPrimary}`)
|
.references(`${tableName}.${relatedPrimary}`)
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
|
case FieldType.FORMULA:
|
||||||
|
// This is allowed, but nothing to do on the external datasource
|
||||||
|
break
|
||||||
|
case FieldType.ATTACHMENTS:
|
||||||
|
case FieldType.ATTACHMENT_SINGLE:
|
||||||
|
case FieldType.AUTO:
|
||||||
|
case FieldType.JSON:
|
||||||
|
case FieldType.INTERNAL:
|
||||||
|
throw `${column.type} is not a valid SQL type`
|
||||||
|
|
||||||
|
default:
|
||||||
|
utils.unreachable(columnType)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -17,6 +17,7 @@ import {
|
||||||
TableRequest,
|
TableRequest,
|
||||||
TableSourceType,
|
TableSourceType,
|
||||||
DatasourcePlusQueryResponse,
|
DatasourcePlusQueryResponse,
|
||||||
|
BBReferenceFieldSubType,
|
||||||
} from "@budibase/types"
|
} from "@budibase/types"
|
||||||
import { OAuth2Client } from "google-auth-library"
|
import { OAuth2Client } from "google-auth-library"
|
||||||
import {
|
import {
|
||||||
|
@ -52,17 +53,30 @@ interface AuthTokenResponse {
|
||||||
access_token: string
|
access_token: string
|
||||||
}
|
}
|
||||||
|
|
||||||
const ALLOWED_TYPES = [
|
const isTypeAllowed: Record<FieldType, boolean> = {
|
||||||
FieldType.STRING,
|
[FieldType.STRING]: true,
|
||||||
FieldType.FORMULA,
|
[FieldType.FORMULA]: true,
|
||||||
FieldType.NUMBER,
|
[FieldType.NUMBER]: true,
|
||||||
FieldType.LONGFORM,
|
[FieldType.LONGFORM]: true,
|
||||||
FieldType.DATETIME,
|
[FieldType.DATETIME]: true,
|
||||||
FieldType.OPTIONS,
|
[FieldType.OPTIONS]: true,
|
||||||
FieldType.BOOLEAN,
|
[FieldType.BOOLEAN]: true,
|
||||||
FieldType.BARCODEQR,
|
[FieldType.BARCODEQR]: true,
|
||||||
FieldType.BB_REFERENCE,
|
[FieldType.BB_REFERENCE]: true,
|
||||||
]
|
[FieldType.BB_REFERENCE_SINGLE]: true,
|
||||||
|
[FieldType.ARRAY]: false,
|
||||||
|
[FieldType.ATTACHMENTS]: false,
|
||||||
|
[FieldType.ATTACHMENT_SINGLE]: false,
|
||||||
|
[FieldType.LINK]: false,
|
||||||
|
[FieldType.AUTO]: false,
|
||||||
|
[FieldType.JSON]: false,
|
||||||
|
[FieldType.INTERNAL]: false,
|
||||||
|
[FieldType.BIGINT]: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
const ALLOWED_TYPES = Object.entries(isTypeAllowed)
|
||||||
|
.filter(([_, allowed]) => allowed)
|
||||||
|
.map(([type]) => type as FieldType)
|
||||||
|
|
||||||
const SCHEMA: Integration = {
|
const SCHEMA: Integration = {
|
||||||
plus: true,
|
plus: true,
|
||||||
|
@ -350,6 +364,7 @@ class GoogleSheetsIntegration implements DatasourcePlus {
|
||||||
rowIndex: json.extra?.idFilter?.equal?.rowNumber,
|
rowIndex: json.extra?.idFilter?.equal?.rowNumber,
|
||||||
sheet,
|
sheet,
|
||||||
row: json.body,
|
row: json.body,
|
||||||
|
table: json.meta.table,
|
||||||
})
|
})
|
||||||
case Operation.DELETE:
|
case Operation.DELETE:
|
||||||
return this.delete({
|
return this.delete({
|
||||||
|
@ -567,7 +582,12 @@ class GoogleSheetsIntegration implements DatasourcePlus {
|
||||||
return { sheet, row }
|
return { sheet, row }
|
||||||
}
|
}
|
||||||
|
|
||||||
async update(query: { sheet: string; rowIndex: number; row: any }) {
|
async update(query: {
|
||||||
|
sheet: string
|
||||||
|
rowIndex: number
|
||||||
|
row: any
|
||||||
|
table: Table
|
||||||
|
}) {
|
||||||
try {
|
try {
|
||||||
await this.connect()
|
await this.connect()
|
||||||
const { sheet, row } = await this.getRowByIndex(
|
const { sheet, row } = await this.getRowByIndex(
|
||||||
|
@ -583,6 +603,15 @@ class GoogleSheetsIntegration implements DatasourcePlus {
|
||||||
if (row[key] === null) {
|
if (row[key] === null) {
|
||||||
row[key] = ""
|
row[key] = ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const { type, subtype, constraints } = query.table.schema[key]
|
||||||
|
const isDeprecatedSingleUser =
|
||||||
|
type === FieldType.BB_REFERENCE &&
|
||||||
|
subtype === BBReferenceFieldSubType.USER &&
|
||||||
|
constraints?.type !== "array"
|
||||||
|
if (isDeprecatedSingleUser && Array.isArray(row[key])) {
|
||||||
|
row[key] = row[key][0]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
await row.save()
|
await row.save()
|
||||||
return [
|
return [
|
||||||
|
|
|
@ -383,6 +383,7 @@ function copyExistingPropsOver(
|
||||||
case FieldType.ATTACHMENT_SINGLE:
|
case FieldType.ATTACHMENT_SINGLE:
|
||||||
case FieldType.JSON:
|
case FieldType.JSON:
|
||||||
case FieldType.BB_REFERENCE:
|
case FieldType.BB_REFERENCE:
|
||||||
|
case FieldType.BB_REFERENCE_SINGLE:
|
||||||
shouldKeepSchema = keepIfType(FieldType.JSON, FieldType.STRING)
|
shouldKeepSchema = keepIfType(FieldType.JSON, FieldType.STRING)
|
||||||
break
|
break
|
||||||
|
|
||||||
|
|
|
@ -79,7 +79,9 @@ 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)
|
||||||
|
|
|
@ -19,7 +19,7 @@ const tableWithUserCol: Table = {
|
||||||
schema: {
|
schema: {
|
||||||
user: {
|
user: {
|
||||||
name: "user",
|
name: "user",
|
||||||
type: FieldType.BB_REFERENCE,
|
type: FieldType.BB_REFERENCE_SINGLE,
|
||||||
subtype: BBReferenceFieldSubType.USER,
|
subtype: BBReferenceFieldSubType.USER,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -35,7 +35,7 @@ const tableWithUsersCol: Table = {
|
||||||
user: {
|
user: {
|
||||||
name: "user",
|
name: "user",
|
||||||
type: FieldType.BB_REFERENCE,
|
type: FieldType.BB_REFERENCE,
|
||||||
subtype: BBReferenceFieldSubType.USERS,
|
subtype: BBReferenceFieldSubType.USER,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 { utils } from "@budibase/shared-core"
|
import { helpers, utils } from "@budibase/shared-core"
|
||||||
|
|
||||||
export async function paginatedSearch(
|
export async function paginatedSearch(
|
||||||
query: SearchFilters,
|
query: SearchFilters,
|
||||||
|
@ -49,13 +49,19 @@ function findColumnInQueries(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function userColumnMapping(column: string, options: RowSearchParams) {
|
function userColumnMapping(
|
||||||
|
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"
|
||||||
if (!isString && !isArray) {
|
if (!isString && !isArray) {
|
||||||
return filterValue
|
return filterValue
|
||||||
}
|
}
|
||||||
|
|
||||||
const processString = (input: string) => {
|
const processString = (input: string) => {
|
||||||
const rowPrefix = DocumentType.ROW + SEPARATOR
|
const rowPrefix = DocumentType.ROW + SEPARATOR
|
||||||
if (input.startsWith(rowPrefix)) {
|
if (input.startsWith(rowPrefix)) {
|
||||||
|
@ -64,40 +70,60 @@ function userColumnMapping(column: string, options: RowSearchParams) {
|
||||||
return input
|
return input
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 processString(el)
|
return wrapper(processString(el))
|
||||||
} else {
|
} else {
|
||||||
return el
|
return el
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
return processString(filterValue)
|
return wrapper(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(table: Table, options: RowSearchParams) {
|
export function searchInputMapping(
|
||||||
|
table: Table,
|
||||||
|
options: RowSearchParams,
|
||||||
|
datasourceOptions: { isSql?: boolean } = {}
|
||||||
|
) {
|
||||||
if (!table?.schema) {
|
if (!table?.schema) {
|
||||||
return options
|
return options
|
||||||
}
|
}
|
||||||
for (let [key, column] of Object.entries(table.schema)) {
|
for (let [key, column] of Object.entries(table.schema)) {
|
||||||
switch (column.type) {
|
switch (column.type) {
|
||||||
case FieldType.BB_REFERENCE: {
|
case FieldType.BB_REFERENCE_SINGLE: {
|
||||||
const subtype = column.subtype
|
const subtype = column.subtype
|
||||||
switch (subtype) {
|
switch (subtype) {
|
||||||
case BBReferenceFieldSubType.USER:
|
case BBReferenceFieldSubType.USER:
|
||||||
case BBReferenceFieldSubType.USERS:
|
|
||||||
userColumnMapping(key, options)
|
userColumnMapping(key, options)
|
||||||
break
|
break
|
||||||
|
|
||||||
default:
|
default:
|
||||||
utils.unreachable(subtype)
|
utils.unreachable(subtype)
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
case FieldType.BB_REFERENCE: {
|
||||||
|
userColumnMapping(
|
||||||
|
key,
|
||||||
|
options,
|
||||||
|
helpers.schema.isDeprecatedSingleUserColumn(column),
|
||||||
|
datasourceOptions.isSql
|
||||||
|
)
|
||||||
|
break
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return options
|
return options
|
||||||
|
|
|
@ -45,6 +45,7 @@ const FieldTypeMap: Record<FieldType, SQLiteType> = {
|
||||||
[FieldType.BIGINT]: SQLiteType.TEXT,
|
[FieldType.BIGINT]: SQLiteType.TEXT,
|
||||||
// TODO: consider the difference between multi-user and single user types (subtyping)
|
// TODO: consider the difference between multi-user and single user types (subtyping)
|
||||||
[FieldType.BB_REFERENCE]: SQLiteType.TEXT,
|
[FieldType.BB_REFERENCE]: SQLiteType.TEXT,
|
||||||
|
[FieldType.BB_REFERENCE_SINGLE]: SQLiteType.TEXT,
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildRelationshipDefinitions(
|
function buildRelationshipDefinitions(
|
||||||
|
|
|
@ -4,7 +4,6 @@ import {
|
||||||
FieldSchema,
|
FieldSchema,
|
||||||
BBReferenceFieldSubType,
|
BBReferenceFieldSubType,
|
||||||
InternalTable,
|
InternalTable,
|
||||||
isBBReferenceField,
|
|
||||||
isRelationshipField,
|
isRelationshipField,
|
||||||
LinkDocument,
|
LinkDocument,
|
||||||
LinkInfo,
|
LinkInfo,
|
||||||
|
@ -12,6 +11,8 @@ import {
|
||||||
RelationshipType,
|
RelationshipType,
|
||||||
Row,
|
Row,
|
||||||
Table,
|
Table,
|
||||||
|
FieldType,
|
||||||
|
BBReferenceSingleFieldMetadata,
|
||||||
} from "@budibase/types"
|
} from "@budibase/types"
|
||||||
import sdk from "../../../sdk"
|
import sdk from "../../../sdk"
|
||||||
import { isExternalTableID } from "../../../integrations/utils"
|
import { isExternalTableID } from "../../../integrations/utils"
|
||||||
|
@ -24,25 +25,58 @@ export interface MigrationResult {
|
||||||
|
|
||||||
export async function migrate(
|
export async function migrate(
|
||||||
table: Table,
|
table: Table,
|
||||||
oldColumn: FieldSchema,
|
oldColumnName: string,
|
||||||
newColumn: FieldSchema
|
newColumnName: string
|
||||||
): Promise<MigrationResult> {
|
): Promise<MigrationResult> {
|
||||||
if (newColumn.name in table.schema) {
|
if (newColumnName in table.schema) {
|
||||||
throw new BadRequestError(`Column "${newColumn.name}" already exists`)
|
throw new BadRequestError(`Column "${newColumnName}" already exists`)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (newColumn.name === "") {
|
if (newColumnName === "") {
|
||||||
throw new BadRequestError(`Column name cannot be empty`)
|
throw new BadRequestError(`Column name cannot be empty`)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (dbCore.isInternalColumnName(newColumn.name)) {
|
if (dbCore.isInternalColumnName(newColumnName)) {
|
||||||
throw new BadRequestError(`Column name cannot be a reserved column name`)
|
throw new BadRequestError(`Column name cannot be a reserved column name`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const oldColumn = table.schema[oldColumnName]
|
||||||
|
|
||||||
|
if (!oldColumn) {
|
||||||
|
throw new BadRequestError(
|
||||||
|
`Column "${oldColumnName}" does not exist on table "${table.name}"`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
oldColumn.type !== FieldType.LINK ||
|
||||||
|
oldColumn.tableId !== InternalTable.USER_METADATA
|
||||||
|
) {
|
||||||
|
throw new BadRequestError(
|
||||||
|
`Only user relationship migration columns is currently supported`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const type =
|
||||||
|
oldColumn.relationshipType === RelationshipType.ONE_TO_MANY
|
||||||
|
? FieldType.BB_REFERENCE_SINGLE
|
||||||
|
: FieldType.BB_REFERENCE
|
||||||
|
const newColumn: FieldSchema = {
|
||||||
|
name: newColumnName,
|
||||||
|
type,
|
||||||
|
subtype: BBReferenceFieldSubType.USER,
|
||||||
|
}
|
||||||
|
|
||||||
|
if (newColumn.type === FieldType.BB_REFERENCE) {
|
||||||
|
newColumn.constraints = {
|
||||||
|
type: "array",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
table.schema[newColumn.name] = newColumn
|
table.schema[newColumn.name] = newColumn
|
||||||
table = await sdk.tables.saveTable(table)
|
table = await sdk.tables.saveTable(table)
|
||||||
|
|
||||||
let migrator = getColumnMigrator(table, oldColumn, newColumn)
|
const migrator = getColumnMigrator(table, oldColumn, newColumn)
|
||||||
try {
|
try {
|
||||||
return await migrator.doMigration()
|
return await migrator.doMigration()
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
@ -75,11 +109,14 @@ function getColumnMigrator(
|
||||||
throw new BadRequestError(`Column "${oldColumn.name}" does not exist`)
|
throw new BadRequestError(`Column "${oldColumn.name}" does not exist`)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isBBReferenceField(newColumn)) {
|
if (
|
||||||
|
newColumn.type !== FieldType.BB_REFERENCE_SINGLE &&
|
||||||
|
newColumn.type !== FieldType.BB_REFERENCE
|
||||||
|
) {
|
||||||
throw new BadRequestError(`Column "${newColumn.name}" is not a user column`)
|
throw new BadRequestError(`Column "${newColumn.name}" is not a user column`)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (newColumn.subtype !== "user" && newColumn.subtype !== "users") {
|
if (newColumn.subtype !== BBReferenceFieldSubType.USER) {
|
||||||
throw new BadRequestError(`Column "${newColumn.name}" is not a user column`)
|
throw new BadRequestError(`Column "${newColumn.name}" is not a user column`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -96,7 +133,7 @@ function getColumnMigrator(
|
||||||
}
|
}
|
||||||
|
|
||||||
if (oldColumn.relationshipType === RelationshipType.ONE_TO_MANY) {
|
if (oldColumn.relationshipType === RelationshipType.ONE_TO_MANY) {
|
||||||
if (newColumn.subtype !== BBReferenceFieldSubType.USER) {
|
if (newColumn.type !== FieldType.BB_REFERENCE_SINGLE) {
|
||||||
throw new BadRequestError(
|
throw new BadRequestError(
|
||||||
`Column "${oldColumn.name}" is a one-to-many column but "${newColumn.name}" is not a single user column`
|
`Column "${oldColumn.name}" is a one-to-many column but "${newColumn.name}" is not a single user column`
|
||||||
)
|
)
|
||||||
|
@ -107,22 +144,23 @@ function getColumnMigrator(
|
||||||
oldColumn.relationshipType === RelationshipType.MANY_TO_MANY ||
|
oldColumn.relationshipType === RelationshipType.MANY_TO_MANY ||
|
||||||
oldColumn.relationshipType === RelationshipType.MANY_TO_ONE
|
oldColumn.relationshipType === RelationshipType.MANY_TO_ONE
|
||||||
) {
|
) {
|
||||||
if (newColumn.subtype !== BBReferenceFieldSubType.USERS) {
|
if (newColumn.type !== FieldType.BB_REFERENCE) {
|
||||||
throw new BadRequestError(
|
throw new BadRequestError(
|
||||||
`Column "${oldColumn.name}" is a ${oldColumn.relationshipType} column but "${newColumn.name}" is not a multi user column`
|
`Column "${oldColumn.name}" is a ${oldColumn.relationshipType} column but "${newColumn.name}" is not a multi user column`
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return new MultiUserColumnMigrator(table, oldColumn, newColumn)
|
return new MultiUserColumnMigrator(table, oldColumn, newColumn)
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new BadRequestError(`Unknown migration type`)
|
throw new BadRequestError(`Unknown migration type`)
|
||||||
}
|
}
|
||||||
|
|
||||||
abstract class UserColumnMigrator implements ColumnMigrator {
|
abstract class UserColumnMigrator<T> implements ColumnMigrator {
|
||||||
constructor(
|
constructor(
|
||||||
protected table: Table,
|
protected table: Table,
|
||||||
protected oldColumn: RelationshipFieldMetadata,
|
protected oldColumn: RelationshipFieldMetadata,
|
||||||
protected newColumn: BBReferenceFieldMetadata
|
protected newColumn: T
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
abstract updateRow(row: Row, linkInfo: LinkInfo): void
|
abstract updateRow(row: Row, linkInfo: LinkInfo): void
|
||||||
|
@ -192,7 +230,7 @@ abstract class UserColumnMigrator implements ColumnMigrator {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class SingleUserColumnMigrator extends UserColumnMigrator {
|
class SingleUserColumnMigrator extends UserColumnMigrator<BBReferenceSingleFieldMetadata> {
|
||||||
updateRow(row: Row, linkInfo: LinkInfo): void {
|
updateRow(row: Row, linkInfo: LinkInfo): void {
|
||||||
row[this.newColumn.name] = dbCore.getGlobalIDFromUserMetadataID(
|
row[this.newColumn.name] = dbCore.getGlobalIDFromUserMetadataID(
|
||||||
linkInfo.rowId
|
linkInfo.rowId
|
||||||
|
@ -200,7 +238,7 @@ class SingleUserColumnMigrator extends UserColumnMigrator {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class MultiUserColumnMigrator extends UserColumnMigrator {
|
class MultiUserColumnMigrator extends UserColumnMigrator<BBReferenceFieldMetadata> {
|
||||||
updateRow(row: Row, linkInfo: LinkInfo): void {
|
updateRow(row: Row, linkInfo: LinkInfo): void {
|
||||||
if (!row[this.newColumn.name]) {
|
if (!row[this.newColumn.name]) {
|
||||||
row[this.newColumn.name] = []
|
row[this.newColumn.name] = []
|
||||||
|
|
|
@ -9,26 +9,57 @@ import { InvalidBBRefError } from "./errors"
|
||||||
|
|
||||||
const ROW_PREFIX = DocumentType.ROW + SEPARATOR
|
const ROW_PREFIX = DocumentType.ROW + SEPARATOR
|
||||||
|
|
||||||
export async function processInputBBReferences(
|
export async function processInputBBReference(
|
||||||
value: string | string[] | { _id: string } | { _id: string }[],
|
value: string | { _id: string },
|
||||||
subtype: BBReferenceFieldSubType.USER | BBReferenceFieldSubType.USERS
|
subtype: BBReferenceFieldSubType.USER
|
||||||
): Promise<string | string[] | null> {
|
): Promise<string | null> {
|
||||||
let referenceIds: string[] = []
|
if (value && Array.isArray(value)) {
|
||||||
|
throw "BB_REFERENCE_SINGLE cannot be an array"
|
||||||
|
}
|
||||||
|
let id = typeof value === "string" ? value : value?._id
|
||||||
|
|
||||||
if (Array.isArray(value)) {
|
if (!id) {
|
||||||
referenceIds.push(
|
return null
|
||||||
...value.map(idOrDoc =>
|
}
|
||||||
typeof idOrDoc === "string" ? idOrDoc : idOrDoc._id
|
|
||||||
)
|
switch (subtype) {
|
||||||
)
|
case BBReferenceFieldSubType.USER: {
|
||||||
} else if (typeof value !== "string") {
|
if (id.startsWith(ROW_PREFIX)) {
|
||||||
referenceIds.push(value._id)
|
id = dbCore.getGlobalIDFromUserMetadataID(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await cache.user.getUser(id)
|
||||||
|
return id
|
||||||
|
} catch (e: any) {
|
||||||
|
if (e.statusCode === 404) {
|
||||||
|
throw new InvalidBBRefError(id, BBReferenceFieldSubType.USER)
|
||||||
|
}
|
||||||
|
throw e
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
throw utils.unreachable(subtype)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export async function processInputBBReferences(
|
||||||
|
value: string | string[] | { _id: string }[],
|
||||||
|
subtype: BBReferenceFieldSubType
|
||||||
|
): Promise<string[] | null> {
|
||||||
|
if (!value || !value[0]) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
let referenceIds
|
||||||
|
if (typeof value === "string") {
|
||||||
|
referenceIds = value
|
||||||
|
.split(",")
|
||||||
|
.map(u => u.trim())
|
||||||
|
.filter(u => !!u)
|
||||||
} else {
|
} else {
|
||||||
referenceIds.push(
|
referenceIds = value.map(idOrDoc =>
|
||||||
...value
|
typeof idOrDoc === "string" ? idOrDoc : idOrDoc._id
|
||||||
.split(",")
|
|
||||||
.filter(x => x)
|
|
||||||
.map((id: string) => id.trim())
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -44,6 +75,8 @@ export async function processInputBBReferences(
|
||||||
})
|
})
|
||||||
|
|
||||||
switch (subtype) {
|
switch (subtype) {
|
||||||
|
case undefined:
|
||||||
|
throw "Subtype must be defined"
|
||||||
case BBReferenceFieldSubType.USER:
|
case BBReferenceFieldSubType.USER:
|
||||||
case BBReferenceFieldSubType.USERS: {
|
case BBReferenceFieldSubType.USERS: {
|
||||||
const { notFoundIds } = await cache.user.getUsers(referenceIds)
|
const { notFoundIds } = await cache.user.getUsers(referenceIds)
|
||||||
|
@ -55,11 +88,54 @@ export async function processInputBBReferences(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (subtype === BBReferenceFieldSubType.USERS) {
|
if (!referenceIds?.length) {
|
||||||
return referenceIds
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
return referenceIds.join(",") || null
|
return referenceIds
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
throw utils.unreachable(subtype)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface UserReferenceInfo {
|
||||||
|
_id: string
|
||||||
|
primaryDisplay: string
|
||||||
|
email: string
|
||||||
|
firstName?: string
|
||||||
|
lastName?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function processOutputBBReference(
|
||||||
|
value: string | null | undefined,
|
||||||
|
subtype: BBReferenceFieldSubType.USER
|
||||||
|
): Promise<UserReferenceInfo | undefined> {
|
||||||
|
if (!value) {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (subtype) {
|
||||||
|
case BBReferenceFieldSubType.USER: {
|
||||||
|
let user
|
||||||
|
try {
|
||||||
|
user = await cache.user.getUser(value as string)
|
||||||
|
} catch (err: any) {
|
||||||
|
if (err.statusCode !== 404) {
|
||||||
|
throw err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!user) {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
_id: user._id!,
|
||||||
|
primaryDisplay: user.email,
|
||||||
|
email: user.email,
|
||||||
|
firstName: user.firstName,
|
||||||
|
lastName: user.lastName,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
throw utils.unreachable(subtype)
|
throw utils.unreachable(subtype)
|
||||||
|
@ -67,14 +143,12 @@ export async function processInputBBReferences(
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function processOutputBBReferences(
|
export async function processOutputBBReferences(
|
||||||
value: string | string[],
|
value: string | null | undefined,
|
||||||
subtype: BBReferenceFieldSubType.USER | BBReferenceFieldSubType.USERS
|
subtype: BBReferenceFieldSubType
|
||||||
) {
|
): Promise<UserReferenceInfo[] | undefined> {
|
||||||
if (value === null || value === undefined) {
|
if (!value) {
|
||||||
// Already processed or nothing to process
|
return undefined
|
||||||
return value || undefined
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const ids =
|
const ids =
|
||||||
typeof value === "string" ? value.split(",").filter(id => !!id) : value
|
typeof value === "string" ? value.split(",").filter(id => !!id) : value
|
||||||
|
|
||||||
|
@ -87,7 +161,7 @@ export async function processOutputBBReferences(
|
||||||
}
|
}
|
||||||
|
|
||||||
return users.map(u => ({
|
return users.map(u => ({
|
||||||
_id: u._id,
|
_id: u._id!,
|
||||||
primaryDisplay: u.email,
|
primaryDisplay: u.email,
|
||||||
email: u.email,
|
email: u.email,
|
||||||
firstName: u.firstName,
|
firstName: u.firstName,
|
||||||
|
|
|
@ -12,7 +12,9 @@ import {
|
||||||
} from "@budibase/types"
|
} from "@budibase/types"
|
||||||
import { cloneDeep } from "lodash/fp"
|
import { cloneDeep } from "lodash/fp"
|
||||||
import {
|
import {
|
||||||
|
processInputBBReference,
|
||||||
processInputBBReferences,
|
processInputBBReferences,
|
||||||
|
processOutputBBReference,
|
||||||
processOutputBBReferences,
|
processOutputBBReferences,
|
||||||
} from "./bbReferenceProcessor"
|
} from "./bbReferenceProcessor"
|
||||||
import { isExternalTableID } from "../../integrations/utils"
|
import { isExternalTableID } from "../../integrations/utils"
|
||||||
|
@ -160,10 +162,10 @@ 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) {
|
||||||
|
|
||||||
if (field.type === FieldType.BB_REFERENCE && value) {
|
|
||||||
clonedRow[key] = await processInputBBReferences(value, field.subtype)
|
clonedRow[key] = await processInputBBReferences(value, field.subtype)
|
||||||
|
} else if (field.type === FieldType.BB_REFERENCE_SINGLE && value) {
|
||||||
|
clonedRow[key] = await processInputBBReference(value, field.subtype)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -252,6 +254,16 @@ export async function outputProcessing<T extends Row[] | Row>(
|
||||||
column.subtype
|
column.subtype
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
} else if (
|
||||||
|
!opts.skipBBReferences &&
|
||||||
|
column.type == FieldType.BB_REFERENCE_SINGLE
|
||||||
|
) {
|
||||||
|
for (let row of enriched) {
|
||||||
|
row[property] = await processOutputBBReference(
|
||||||
|
row[property],
|
||||||
|
column.subtype
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -2,7 +2,9 @@ import _ from "lodash"
|
||||||
import * as backendCore from "@budibase/backend-core"
|
import * as backendCore from "@budibase/backend-core"
|
||||||
import { BBReferenceFieldSubType, User } from "@budibase/types"
|
import { BBReferenceFieldSubType, User } from "@budibase/types"
|
||||||
import {
|
import {
|
||||||
|
processInputBBReference,
|
||||||
processInputBBReferences,
|
processInputBBReferences,
|
||||||
|
processOutputBBReference,
|
||||||
processOutputBBReferences,
|
processOutputBBReferences,
|
||||||
} from "../bbReferenceProcessor"
|
} from "../bbReferenceProcessor"
|
||||||
import {
|
import {
|
||||||
|
@ -22,6 +24,7 @@ jest.mock("@budibase/backend-core", (): typeof backendCore => {
|
||||||
...actual.cache,
|
...actual.cache,
|
||||||
user: {
|
user: {
|
||||||
...actual.cache.user,
|
...actual.cache.user,
|
||||||
|
getUser: jest.fn(actual.cache.user.getUser),
|
||||||
getUsers: jest.fn(actual.cache.user.getUsers),
|
getUsers: jest.fn(actual.cache.user.getUsers),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -31,6 +34,9 @@ jest.mock("@budibase/backend-core", (): typeof backendCore => {
|
||||||
const config = new DBTestConfiguration()
|
const config = new DBTestConfiguration()
|
||||||
|
|
||||||
describe("bbReferenceProcessor", () => {
|
describe("bbReferenceProcessor", () => {
|
||||||
|
const cacheGetUserSpy = backendCore.cache.user.getUser as jest.MockedFunction<
|
||||||
|
typeof backendCore.cache.user.getUser
|
||||||
|
>
|
||||||
const cacheGetUsersSpy = backendCore.cache.user
|
const cacheGetUsersSpy = backendCore.cache.user
|
||||||
.getUsers as jest.MockedFunction<typeof backendCore.cache.user.getUsers>
|
.getUsers as jest.MockedFunction<typeof backendCore.cache.user.getUsers>
|
||||||
|
|
||||||
|
@ -56,6 +62,64 @@ describe("bbReferenceProcessor", () => {
|
||||||
jest.clearAllMocks()
|
jest.clearAllMocks()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe("processInputBBReference", () => {
|
||||||
|
describe("subtype user", () => {
|
||||||
|
it("validate valid string id", async () => {
|
||||||
|
const user = _.sample(users)
|
||||||
|
const userId = user!._id!
|
||||||
|
|
||||||
|
const result = await config.doInTenant(() =>
|
||||||
|
processInputBBReference(userId, BBReferenceFieldSubType.USER)
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(result).toEqual(userId)
|
||||||
|
expect(cacheGetUserSpy).toHaveBeenCalledTimes(1)
|
||||||
|
expect(cacheGetUserSpy).toHaveBeenCalledWith(userId)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("throws an error given an invalid id", async () => {
|
||||||
|
const userId = generator.guid()
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
config.doInTenant(() =>
|
||||||
|
processInputBBReference(userId, BBReferenceFieldSubType.USER)
|
||||||
|
)
|
||||||
|
).rejects.toThrow(
|
||||||
|
new InvalidBBRefError(userId, BBReferenceFieldSubType.USER)
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("validate valid user object", async () => {
|
||||||
|
const userId = _.sample(users)!._id!
|
||||||
|
|
||||||
|
const result = await config.doInTenant(() =>
|
||||||
|
processInputBBReference({ _id: userId }, BBReferenceFieldSubType.USER)
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(result).toEqual(userId)
|
||||||
|
expect(cacheGetUserSpy).toHaveBeenCalledTimes(1)
|
||||||
|
expect(cacheGetUserSpy).toHaveBeenCalledWith(userId)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("empty strings will return null", async () => {
|
||||||
|
const result = await config.doInTenant(() =>
|
||||||
|
processInputBBReference("", BBReferenceFieldSubType.USER)
|
||||||
|
)
|
||||||
|
|
||||||
|
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(() =>
|
||||||
|
processInputBBReference(userMetadataId, BBReferenceFieldSubType.USER)
|
||||||
|
)
|
||||||
|
expect(result).toBe(userId)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
describe("processInputBBReferences", () => {
|
describe("processInputBBReferences", () => {
|
||||||
describe("subtype user", () => {
|
describe("subtype user", () => {
|
||||||
it("validate valid string id", async () => {
|
it("validate valid string id", async () => {
|
||||||
|
@ -66,7 +130,7 @@ describe("bbReferenceProcessor", () => {
|
||||||
processInputBBReferences(userId, BBReferenceFieldSubType.USER)
|
processInputBBReferences(userId, BBReferenceFieldSubType.USER)
|
||||||
)
|
)
|
||||||
|
|
||||||
expect(result).toEqual(userId)
|
expect(result).toEqual([userId])
|
||||||
expect(cacheGetUsersSpy).toHaveBeenCalledTimes(1)
|
expect(cacheGetUsersSpy).toHaveBeenCalledTimes(1)
|
||||||
expect(cacheGetUsersSpy).toHaveBeenCalledWith([userId])
|
expect(cacheGetUsersSpy).toHaveBeenCalledWith([userId])
|
||||||
})
|
})
|
||||||
|
@ -93,7 +157,7 @@ describe("bbReferenceProcessor", () => {
|
||||||
processInputBBReferences(userIdCsv, BBReferenceFieldSubType.USER)
|
processInputBBReferences(userIdCsv, BBReferenceFieldSubType.USER)
|
||||||
)
|
)
|
||||||
|
|
||||||
expect(result).toEqual(userIds.join(","))
|
expect(result).toEqual(userIds)
|
||||||
expect(cacheGetUsersSpy).toHaveBeenCalledTimes(1)
|
expect(cacheGetUsersSpy).toHaveBeenCalledTimes(1)
|
||||||
expect(cacheGetUsersSpy).toHaveBeenCalledWith(userIds)
|
expect(cacheGetUsersSpy).toHaveBeenCalledWith(userIds)
|
||||||
})
|
})
|
||||||
|
@ -117,36 +181,26 @@ describe("bbReferenceProcessor", () => {
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
it("validate valid user object", async () => {
|
|
||||||
const userId = _.sample(users)!._id!
|
|
||||||
|
|
||||||
const result = await config.doInTenant(() =>
|
|
||||||
processInputBBReferences(
|
|
||||||
{ _id: userId },
|
|
||||||
BBReferenceFieldSubType.USER
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
expect(result).toEqual(userId)
|
|
||||||
expect(cacheGetUsersSpy).toHaveBeenCalledTimes(1)
|
|
||||||
expect(cacheGetUsersSpy).toHaveBeenCalledWith([userId])
|
|
||||||
})
|
|
||||||
|
|
||||||
it("validate valid user object array", async () => {
|
it("validate valid user object array", async () => {
|
||||||
const userIds = _.sampleSize(users, 3).map(x => x._id!)
|
const inputUsers = _.sampleSize(users, 3).map(u => ({ _id: u._id! }))
|
||||||
|
const userIds = inputUsers.map(u => u._id)
|
||||||
|
|
||||||
const result = await config.doInTenant(() =>
|
const result = await config.doInTenant(() =>
|
||||||
processInputBBReferences(userIds, BBReferenceFieldSubType.USER)
|
processInputBBReferences(inputUsers, BBReferenceFieldSubType.USER)
|
||||||
)
|
)
|
||||||
|
|
||||||
expect(result).toEqual(userIds.join(","))
|
expect(result).toEqual(userIds)
|
||||||
expect(cacheGetUsersSpy).toHaveBeenCalledTimes(1)
|
expect(cacheGetUsersSpy).toHaveBeenCalledTimes(1)
|
||||||
expect(cacheGetUsersSpy).toHaveBeenCalledWith(userIds)
|
expect(cacheGetUsersSpy).toHaveBeenCalledWith(userIds)
|
||||||
})
|
})
|
||||||
|
|
||||||
it("empty strings will return null", async () => {
|
it("empty strings will return null", async () => {
|
||||||
const result = await config.doInTenant(() =>
|
const result = await config.doInTenant(() =>
|
||||||
processInputBBReferences("", BBReferenceFieldSubType.USER)
|
processInputBBReferences(
|
||||||
|
"",
|
||||||
|
|
||||||
|
BBReferenceFieldSubType.USER
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
expect(result).toEqual(null)
|
expect(result).toEqual(null)
|
||||||
|
@ -166,7 +220,42 @@ describe("bbReferenceProcessor", () => {
|
||||||
const result = await config.doInTenant(() =>
|
const result = await config.doInTenant(() =>
|
||||||
processInputBBReferences(userMetadataId, BBReferenceFieldSubType.USER)
|
processInputBBReferences(userMetadataId, BBReferenceFieldSubType.USER)
|
||||||
)
|
)
|
||||||
expect(result).toBe(userId)
|
expect(result).toEqual([userId])
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("processOutputBBReference", () => {
|
||||||
|
describe("subtype user", () => {
|
||||||
|
it("fetches user given a valid string id", async () => {
|
||||||
|
const user = _.sample(users)!
|
||||||
|
const userId = user._id!
|
||||||
|
|
||||||
|
const result = await config.doInTenant(() =>
|
||||||
|
processOutputBBReference(userId, BBReferenceFieldSubType.USER)
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
_id: user._id,
|
||||||
|
primaryDisplay: user.email,
|
||||||
|
email: user.email,
|
||||||
|
firstName: user.firstName,
|
||||||
|
lastName: user.lastName,
|
||||||
|
})
|
||||||
|
expect(cacheGetUserSpy).toHaveBeenCalledTimes(1)
|
||||||
|
expect(cacheGetUserSpy).toHaveBeenCalledWith(userId)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("returns undefined given an unexisting user", async () => {
|
||||||
|
const userId = generator.guid()
|
||||||
|
|
||||||
|
const result = await config.doInTenant(() =>
|
||||||
|
processOutputBBReference(userId, BBReferenceFieldSubType.USER)
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(result).toBeUndefined()
|
||||||
|
expect(cacheGetUserSpy).toHaveBeenCalledTimes(1)
|
||||||
|
expect(cacheGetUserSpy).toHaveBeenCalledWith(userId)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
@ -221,6 +310,46 @@ describe("bbReferenceProcessor", () => {
|
||||||
expect(cacheGetUsersSpy).toHaveBeenCalledTimes(1)
|
expect(cacheGetUsersSpy).toHaveBeenCalledTimes(1)
|
||||||
expect(cacheGetUsersSpy).toHaveBeenCalledWith([userId1, userId2])
|
expect(cacheGetUsersSpy).toHaveBeenCalledWith([userId1, userId2])
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it("trims unexisting users user given a valid string id csv", async () => {
|
||||||
|
const [user1, user2] = _.sampleSize(users, 2)
|
||||||
|
const userId1 = user1._id!
|
||||||
|
const userId2 = user2._id!
|
||||||
|
|
||||||
|
const unexistingUserId1 = generator.guid()
|
||||||
|
const unexistingUserId2 = generator.guid()
|
||||||
|
|
||||||
|
const input = [
|
||||||
|
unexistingUserId1,
|
||||||
|
userId1,
|
||||||
|
unexistingUserId2,
|
||||||
|
userId2,
|
||||||
|
].join(",")
|
||||||
|
|
||||||
|
const result = await config.doInTenant(() =>
|
||||||
|
processOutputBBReferences(input, BBReferenceFieldSubType.USER)
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(result).toHaveLength(2)
|
||||||
|
expect(result).toEqual(
|
||||||
|
expect.arrayContaining(
|
||||||
|
[user1, user2].map(u => ({
|
||||||
|
_id: u._id,
|
||||||
|
primaryDisplay: u.email,
|
||||||
|
email: u.email,
|
||||||
|
firstName: u.firstName,
|
||||||
|
lastName: u.lastName,
|
||||||
|
}))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
expect(cacheGetUsersSpy).toHaveBeenCalledTimes(1)
|
||||||
|
expect(cacheGetUsersSpy).toHaveBeenCalledWith([
|
||||||
|
unexistingUserId1,
|
||||||
|
userId1,
|
||||||
|
unexistingUserId2,
|
||||||
|
userId2,
|
||||||
|
])
|
||||||
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -10,7 +10,9 @@ import {
|
||||||
import * as bbReferenceProcessor from "../bbReferenceProcessor"
|
import * as bbReferenceProcessor from "../bbReferenceProcessor"
|
||||||
|
|
||||||
jest.mock("../bbReferenceProcessor", (): typeof bbReferenceProcessor => ({
|
jest.mock("../bbReferenceProcessor", (): typeof bbReferenceProcessor => ({
|
||||||
|
processInputBBReference: jest.fn(),
|
||||||
processInputBBReferences: jest.fn(),
|
processInputBBReferences: jest.fn(),
|
||||||
|
processOutputBBReference: jest.fn(),
|
||||||
processOutputBBReferences: jest.fn(),
|
processOutputBBReferences: jest.fn(),
|
||||||
}))
|
}))
|
||||||
|
|
||||||
|
@ -19,7 +21,64 @@ describe("rowProcessor - inputProcessing", () => {
|
||||||
jest.resetAllMocks()
|
jest.resetAllMocks()
|
||||||
})
|
})
|
||||||
|
|
||||||
it("processes BB references if on the schema and it's populated", async () => {
|
const processInputBBReferenceMock =
|
||||||
|
bbReferenceProcessor.processInputBBReference as jest.Mock
|
||||||
|
const processInputBBReferencesMock =
|
||||||
|
bbReferenceProcessor.processInputBBReferences as jest.Mock
|
||||||
|
|
||||||
|
it("processes single BB references if on the schema and it's populated", async () => {
|
||||||
|
const userId = generator.guid()
|
||||||
|
|
||||||
|
const table: Table = {
|
||||||
|
_id: generator.guid(),
|
||||||
|
name: "TestTable",
|
||||||
|
type: "table",
|
||||||
|
sourceId: INTERNAL_TABLE_SOURCE_ID,
|
||||||
|
sourceType: TableSourceType.INTERNAL,
|
||||||
|
schema: {
|
||||||
|
name: {
|
||||||
|
type: FieldType.STRING,
|
||||||
|
name: "name",
|
||||||
|
constraints: {
|
||||||
|
presence: true,
|
||||||
|
type: "string",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
user: {
|
||||||
|
type: FieldType.BB_REFERENCE_SINGLE,
|
||||||
|
subtype: BBReferenceFieldSubType.USER,
|
||||||
|
name: "user",
|
||||||
|
constraints: {
|
||||||
|
presence: true,
|
||||||
|
type: "string",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
const newRow = {
|
||||||
|
name: "Jack",
|
||||||
|
user: "123",
|
||||||
|
}
|
||||||
|
|
||||||
|
const user = structures.users.user()
|
||||||
|
|
||||||
|
processInputBBReferenceMock.mockResolvedValue(user)
|
||||||
|
|
||||||
|
const { row } = await inputProcessing(userId, table, newRow)
|
||||||
|
|
||||||
|
expect(bbReferenceProcessor.processInputBBReference).toHaveBeenCalledTimes(
|
||||||
|
1
|
||||||
|
)
|
||||||
|
expect(bbReferenceProcessor.processInputBBReference).toHaveBeenCalledWith(
|
||||||
|
"123",
|
||||||
|
"user"
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(row).toEqual({ ...newRow, user })
|
||||||
|
})
|
||||||
|
|
||||||
|
it("processes multiple BB references if on the schema and it's populated", async () => {
|
||||||
const userId = generator.guid()
|
const userId = generator.guid()
|
||||||
|
|
||||||
const table: Table = {
|
const table: Table = {
|
||||||
|
@ -56,9 +115,7 @@ describe("rowProcessor - inputProcessing", () => {
|
||||||
|
|
||||||
const user = structures.users.user()
|
const user = structures.users.user()
|
||||||
|
|
||||||
;(
|
processInputBBReferencesMock.mockResolvedValue(user)
|
||||||
bbReferenceProcessor.processInputBBReferences as jest.Mock
|
|
||||||
).mockResolvedValue(user)
|
|
||||||
|
|
||||||
const { row } = await inputProcessing(userId, table, newRow)
|
const { row } = await inputProcessing(userId, table, newRow)
|
||||||
|
|
||||||
|
|
|
@ -11,7 +11,9 @@ import { generator, structures } from "@budibase/backend-core/tests"
|
||||||
import * as bbReferenceProcessor from "../bbReferenceProcessor"
|
import * as bbReferenceProcessor from "../bbReferenceProcessor"
|
||||||
|
|
||||||
jest.mock("../bbReferenceProcessor", (): typeof bbReferenceProcessor => ({
|
jest.mock("../bbReferenceProcessor", (): typeof bbReferenceProcessor => ({
|
||||||
|
processInputBBReference: jest.fn(),
|
||||||
processInputBBReferences: jest.fn(),
|
processInputBBReferences: jest.fn(),
|
||||||
|
processOutputBBReference: jest.fn(),
|
||||||
processOutputBBReferences: jest.fn(),
|
processOutputBBReferences: jest.fn(),
|
||||||
}))
|
}))
|
||||||
|
|
||||||
|
@ -20,10 +22,12 @@ describe("rowProcessor - outputProcessing", () => {
|
||||||
jest.resetAllMocks()
|
jest.resetAllMocks()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const processOutputBBReferenceMock =
|
||||||
|
bbReferenceProcessor.processOutputBBReference as jest.Mock
|
||||||
const processOutputBBReferencesMock =
|
const processOutputBBReferencesMock =
|
||||||
bbReferenceProcessor.processOutputBBReferences as jest.Mock
|
bbReferenceProcessor.processOutputBBReferences as jest.Mock
|
||||||
|
|
||||||
it("fetches bb user references given a populated field", async () => {
|
it("fetches single user references given a populated field", async () => {
|
||||||
const table: Table = {
|
const table: Table = {
|
||||||
_id: generator.guid(),
|
_id: generator.guid(),
|
||||||
name: "TestTable",
|
name: "TestTable",
|
||||||
|
@ -40,7 +44,7 @@ describe("rowProcessor - outputProcessing", () => {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
user: {
|
user: {
|
||||||
type: FieldType.BB_REFERENCE,
|
type: FieldType.BB_REFERENCE_SINGLE,
|
||||||
subtype: BBReferenceFieldSubType.USER,
|
subtype: BBReferenceFieldSubType.USER,
|
||||||
name: "user",
|
name: "user",
|
||||||
constraints: {
|
constraints: {
|
||||||
|
@ -57,12 +61,61 @@ describe("rowProcessor - outputProcessing", () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const user = structures.users.user()
|
const user = structures.users.user()
|
||||||
processOutputBBReferencesMock.mockResolvedValue(user)
|
processOutputBBReferenceMock.mockResolvedValue(user)
|
||||||
|
|
||||||
const result = await outputProcessing(table, row, { squash: false })
|
const result = await outputProcessing(table, row, { squash: false })
|
||||||
|
|
||||||
expect(result).toEqual({ name: "Jack", user })
|
expect(result).toEqual({ name: "Jack", user })
|
||||||
|
|
||||||
|
expect(bbReferenceProcessor.processOutputBBReference).toHaveBeenCalledTimes(
|
||||||
|
1
|
||||||
|
)
|
||||||
|
expect(bbReferenceProcessor.processOutputBBReference).toHaveBeenCalledWith(
|
||||||
|
"123",
|
||||||
|
BBReferenceFieldSubType.USER
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("fetches users references given a populated field", async () => {
|
||||||
|
const table: Table = {
|
||||||
|
_id: generator.guid(),
|
||||||
|
name: "TestTable",
|
||||||
|
type: "table",
|
||||||
|
sourceId: INTERNAL_TABLE_SOURCE_ID,
|
||||||
|
sourceType: TableSourceType.INTERNAL,
|
||||||
|
schema: {
|
||||||
|
name: {
|
||||||
|
type: FieldType.STRING,
|
||||||
|
name: "name",
|
||||||
|
constraints: {
|
||||||
|
presence: true,
|
||||||
|
type: "string",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
users: {
|
||||||
|
type: FieldType.BB_REFERENCE,
|
||||||
|
subtype: BBReferenceFieldSubType.USER,
|
||||||
|
name: "users",
|
||||||
|
constraints: {
|
||||||
|
presence: false,
|
||||||
|
type: "string",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
const row = {
|
||||||
|
name: "Jack",
|
||||||
|
users: "123",
|
||||||
|
}
|
||||||
|
|
||||||
|
const users = [structures.users.user()]
|
||||||
|
processOutputBBReferencesMock.mockResolvedValue(users)
|
||||||
|
|
||||||
|
const result = await outputProcessing(table, row, { squash: false })
|
||||||
|
|
||||||
|
expect(result).toEqual({ name: "Jack", users })
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
bbReferenceProcessor.processOutputBBReferences
|
bbReferenceProcessor.processOutputBBReferences
|
||||||
).toHaveBeenCalledTimes(1)
|
).toHaveBeenCalledTimes(1)
|
||||||
|
|
|
@ -54,6 +54,7 @@ export function validate(rows: Rows, schema: TableSchema): ValidationResults {
|
||||||
type: columnType,
|
type: columnType,
|
||||||
subtype: columnSubtype,
|
subtype: columnSubtype,
|
||||||
autocolumn: isAutoColumn,
|
autocolumn: isAutoColumn,
|
||||||
|
constraints,
|
||||||
} = schema[columnName] || {}
|
} = schema[columnName] || {}
|
||||||
|
|
||||||
// If the column had an invalid value we don't want to override it
|
// If the column had an invalid value we don't want to override it
|
||||||
|
@ -61,6 +62,12 @@ export function validate(rows: Rows, schema: TableSchema): ValidationResults {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const isRequired =
|
||||||
|
!!constraints &&
|
||||||
|
((typeof constraints.presence !== "boolean" &&
|
||||||
|
!constraints.presence?.allowEmpty) ||
|
||||||
|
constraints.presence === true)
|
||||||
|
|
||||||
// If the columnType is not a string, then it's not present in the schema, and should be added to the invalid columns array
|
// If the columnType is not a string, then it's not present in the schema, and should be added to the invalid columns array
|
||||||
if (typeof columnType !== "string") {
|
if (typeof columnType !== "string") {
|
||||||
results.invalidColumns.push(columnName)
|
results.invalidColumns.push(columnName)
|
||||||
|
@ -92,8 +99,9 @@ export function validate(rows: Rows, schema: TableSchema): ValidationResults {
|
||||||
) {
|
) {
|
||||||
results.schemaValidation[columnName] = false
|
results.schemaValidation[columnName] = false
|
||||||
} else if (
|
} else if (
|
||||||
columnType === FieldType.BB_REFERENCE &&
|
(columnType === FieldType.BB_REFERENCE ||
|
||||||
!isValidBBReference(columnData, columnSubtype)
|
columnType === FieldType.BB_REFERENCE_SINGLE) &&
|
||||||
|
!isValidBBReference(columnData, columnType, columnSubtype, isRequired)
|
||||||
) {
|
) {
|
||||||
results.schemaValidation[columnName] = false
|
results.schemaValidation[columnName] = false
|
||||||
} else {
|
} else {
|
||||||
|
@ -121,7 +129,7 @@ export function parse(rows: Rows, schema: TableSchema): Rows {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const { type: columnType, subtype: columnSubtype } = schema[columnName]
|
const { type: columnType } = schema[columnName]
|
||||||
if (columnType === FieldType.NUMBER) {
|
if (columnType === FieldType.NUMBER) {
|
||||||
// If provided must be a valid number
|
// If provided must be a valid number
|
||||||
parsedRow[columnName] = columnData ? Number(columnData) : columnData
|
parsedRow[columnName] = columnData ? Number(columnData) : columnData
|
||||||
|
@ -131,22 +139,16 @@ export function parse(rows: Rows, schema: TableSchema): Rows {
|
||||||
? new Date(columnData).toISOString()
|
? new Date(columnData).toISOString()
|
||||||
: columnData
|
: columnData
|
||||||
} else if (columnType === FieldType.BB_REFERENCE) {
|
} else if (columnType === FieldType.BB_REFERENCE) {
|
||||||
const parsedValues =
|
let parsedValues: { _id: string }[] = columnData || []
|
||||||
!!columnData && parseCsvExport<{ _id: string }[]>(columnData)
|
if (columnData) {
|
||||||
if (!parsedValues) {
|
parsedValues = parseCsvExport<{ _id: string }[]>(columnData)
|
||||||
parsedRow[columnName] = undefined
|
|
||||||
} else {
|
|
||||||
switch (columnSubtype) {
|
|
||||||
case BBReferenceFieldSubType.USER:
|
|
||||||
parsedRow[columnName] = parsedValues[0]?._id
|
|
||||||
break
|
|
||||||
case BBReferenceFieldSubType.USERS:
|
|
||||||
parsedRow[columnName] = parsedValues.map(u => u._id)
|
|
||||||
break
|
|
||||||
default:
|
|
||||||
utils.unreachable(columnSubtype)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
parsedRow[columnName] = parsedValues?.map(u => u._id)
|
||||||
|
} else if (columnType === FieldType.BB_REFERENCE_SINGLE) {
|
||||||
|
const parsedValue =
|
||||||
|
columnData && parseCsvExport<{ _id: string }>(columnData)
|
||||||
|
parsedRow[columnName] = parsedValue?._id
|
||||||
} else if (
|
} else if (
|
||||||
(columnType === FieldType.ATTACHMENTS ||
|
(columnType === FieldType.ATTACHMENTS ||
|
||||||
columnType === FieldType.ATTACHMENT_SINGLE) &&
|
columnType === FieldType.ATTACHMENT_SINGLE) &&
|
||||||
|
@ -163,33 +165,37 @@ export function parse(rows: Rows, schema: TableSchema): Rows {
|
||||||
}
|
}
|
||||||
|
|
||||||
function isValidBBReference(
|
function isValidBBReference(
|
||||||
columnData: any,
|
data: any,
|
||||||
columnSubtype: BBReferenceFieldSubType.USER | BBReferenceFieldSubType.USERS
|
type: FieldType.BB_REFERENCE | FieldType.BB_REFERENCE_SINGLE,
|
||||||
|
subtype: BBReferenceFieldSubType,
|
||||||
|
isRequired: boolean
|
||||||
): boolean {
|
): boolean {
|
||||||
switch (columnSubtype) {
|
if (typeof data !== "string") {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (type === FieldType.BB_REFERENCE_SINGLE) {
|
||||||
|
if (!data) {
|
||||||
|
return !isRequired
|
||||||
|
}
|
||||||
|
const user = parseCsvExport<{ _id: string }>(data)
|
||||||
|
return db.isGlobalUserID(user._id)
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (subtype) {
|
||||||
case BBReferenceFieldSubType.USER:
|
case BBReferenceFieldSubType.USER:
|
||||||
case BBReferenceFieldSubType.USERS: {
|
case BBReferenceFieldSubType.USERS: {
|
||||||
if (typeof columnData !== "string") {
|
const userArray = parseCsvExport<{ _id: string }[]>(data)
|
||||||
return false
|
|
||||||
}
|
|
||||||
const userArray = parseCsvExport<{ _id: string }[]>(columnData)
|
|
||||||
if (!Array.isArray(userArray)) {
|
if (!Array.isArray(userArray)) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
|
||||||
columnSubtype === BBReferenceFieldSubType.USER &&
|
|
||||||
userArray.length > 1
|
|
||||||
) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
const constainsWrongId = userArray.find(
|
const constainsWrongId = userArray.find(
|
||||||
user => !db.isGlobalUserID(user._id)
|
user => !db.isGlobalUserID(user._id)
|
||||||
)
|
)
|
||||||
return !constainsWrongId
|
return !constainsWrongId
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
throw utils.unreachable(columnSubtype)
|
throw utils.unreachable(subtype)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,10 +9,11 @@ import {
|
||||||
SearchFilterOperator,
|
SearchFilterOperator,
|
||||||
SortDirection,
|
SortDirection,
|
||||||
SortType,
|
SortType,
|
||||||
|
FieldConstraints,
|
||||||
} from "@budibase/types"
|
} from "@budibase/types"
|
||||||
import dayjs from "dayjs"
|
import dayjs from "dayjs"
|
||||||
import { OperatorOptions, SqlNumberTypeRangeMap } from "./constants"
|
import { OperatorOptions, SqlNumberTypeRangeMap } from "./constants"
|
||||||
import { deepGet } from "./helpers"
|
import { deepGet, schema } from "./helpers"
|
||||||
|
|
||||||
const HBS_REGEX = /{{([^{].*?)}}/g
|
const HBS_REGEX = /{{([^{].*?)}}/g
|
||||||
|
|
||||||
|
@ -24,9 +25,10 @@ export const getValidOperatorsForType = (
|
||||||
type: FieldType
|
type: FieldType
|
||||||
subtype?: BBReferenceFieldSubType
|
subtype?: BBReferenceFieldSubType
|
||||||
formulaType?: FormulaType
|
formulaType?: FormulaType
|
||||||
|
constraints?: FieldConstraints
|
||||||
},
|
},
|
||||||
field: string,
|
field?: string,
|
||||||
datasource: Datasource & { tableId: any }
|
datasource?: Datasource & { tableId: any }
|
||||||
) => {
|
) => {
|
||||||
const Op = OperatorOptions
|
const Op = OperatorOptions
|
||||||
const stringOps = [
|
const stringOps = [
|
||||||
|
@ -51,7 +53,7 @@ export const getValidOperatorsForType = (
|
||||||
value: string
|
value: string
|
||||||
label: string
|
label: string
|
||||||
}[] = []
|
}[] = []
|
||||||
const { type, subtype, formulaType } = fieldType
|
const { type, formulaType } = fieldType
|
||||||
if (type === FieldType.STRING) {
|
if (type === FieldType.STRING) {
|
||||||
ops = stringOps
|
ops = stringOps
|
||||||
} else if (type === FieldType.NUMBER || type === FieldType.BIGINT) {
|
} else if (type === FieldType.NUMBER || type === FieldType.BIGINT) {
|
||||||
|
@ -69,14 +71,11 @@ export const getValidOperatorsForType = (
|
||||||
} else if (type === FieldType.FORMULA && formulaType === FormulaType.STATIC) {
|
} else if (type === FieldType.FORMULA && formulaType === FormulaType.STATIC) {
|
||||||
ops = stringOps.concat([Op.MoreThan, Op.LessThan])
|
ops = stringOps.concat([Op.MoreThan, Op.LessThan])
|
||||||
} else if (
|
} else if (
|
||||||
type === FieldType.BB_REFERENCE &&
|
type === FieldType.BB_REFERENCE_SINGLE ||
|
||||||
subtype == BBReferenceFieldSubType.USER
|
schema.isDeprecatedSingleUserColumn(fieldType)
|
||||||
) {
|
) {
|
||||||
ops = [Op.Equals, Op.NotEquals, Op.Empty, Op.NotEmpty, Op.In]
|
ops = [Op.Equals, Op.NotEquals, Op.Empty, Op.NotEmpty, Op.In]
|
||||||
} else if (
|
} else if (type === FieldType.BB_REFERENCE) {
|
||||||
type === FieldType.BB_REFERENCE &&
|
|
||||||
subtype == BBReferenceFieldSubType.USERS
|
|
||||||
) {
|
|
||||||
ops = [Op.Contains, Op.NotContains, Op.ContainsAny, Op.Empty, Op.NotEmpty]
|
ops = [Op.Contains, Op.NotContains, Op.ContainsAny, Op.Empty, Op.NotEmpty]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
export * from "./helpers"
|
export * from "./helpers"
|
||||||
export * from "./integrations"
|
export * from "./integrations"
|
||||||
export * as cron from "./cron"
|
export * as cron from "./cron"
|
||||||
|
export * as schema from "./schema"
|
||||||
|
|
15
packages/shared-core/src/helpers/schema.ts
Normal file
15
packages/shared-core/src/helpers/schema.ts
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
import {
|
||||||
|
BBReferenceFieldSubType,
|
||||||
|
FieldSchema,
|
||||||
|
FieldType,
|
||||||
|
} from "@budibase/types"
|
||||||
|
|
||||||
|
export function isDeprecatedSingleUserColumn(
|
||||||
|
schema: Pick<FieldSchema, "type" | "subtype" | "constraints">
|
||||||
|
) {
|
||||||
|
const result =
|
||||||
|
schema.type === FieldType.BB_REFERENCE &&
|
||||||
|
schema.subtype === BBReferenceFieldSubType.USER &&
|
||||||
|
schema.constraints?.type !== "array"
|
||||||
|
return result
|
||||||
|
}
|
|
@ -18,6 +18,7 @@ const allowDisplayColumnByType: Record<FieldType, boolean> = {
|
||||||
[FieldType.LINK]: false,
|
[FieldType.LINK]: false,
|
||||||
[FieldType.JSON]: false,
|
[FieldType.JSON]: false,
|
||||||
[FieldType.BB_REFERENCE]: false,
|
[FieldType.BB_REFERENCE]: false,
|
||||||
|
[FieldType.BB_REFERENCE_SINGLE]: false,
|
||||||
}
|
}
|
||||||
|
|
||||||
const allowSortColumnByType: Record<FieldType, boolean> = {
|
const allowSortColumnByType: Record<FieldType, boolean> = {
|
||||||
|
@ -39,6 +40,7 @@ const allowSortColumnByType: Record<FieldType, boolean> = {
|
||||||
[FieldType.ARRAY]: false,
|
[FieldType.ARRAY]: false,
|
||||||
[FieldType.LINK]: false,
|
[FieldType.LINK]: false,
|
||||||
[FieldType.BB_REFERENCE]: false,
|
[FieldType.BB_REFERENCE]: false,
|
||||||
|
[FieldType.BB_REFERENCE_SINGLE]: false,
|
||||||
}
|
}
|
||||||
|
|
||||||
export function canBeDisplayColumn(type: FieldType): boolean {
|
export function canBeDisplayColumn(type: FieldType): boolean {
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
import {
|
import {
|
||||||
FieldSchema,
|
|
||||||
Row,
|
Row,
|
||||||
Table,
|
Table,
|
||||||
TableRequest,
|
TableRequest,
|
||||||
|
@ -31,8 +30,8 @@ export interface BulkImportResponse {
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface MigrateRequest {
|
export interface MigrateRequest {
|
||||||
oldColumn: FieldSchema
|
oldColumn: string
|
||||||
newColumn: FieldSchema
|
newColumn: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface MigrateResponse {
|
export interface MigrateResponse {
|
||||||
|
|
|
@ -100,13 +100,17 @@ export enum FieldType {
|
||||||
*/
|
*/
|
||||||
BIGINT = "bigint",
|
BIGINT = "bigint",
|
||||||
/**
|
/**
|
||||||
* a JSON type, called User within Budibase. This type is used to represent a link to an internal Budibase
|
* a JSON type, called Users within Budibase. It will hold an array of strings. This type is used to represent a link to an internal Budibase
|
||||||
* resource, like a user or group, today only users are supported. This type will be represented as an
|
* resource, like a user or group, today only users are supported. This type will be represented as an
|
||||||
* array of internal resource IDs (e.g. user IDs) within the row - this ID list will be enriched with
|
* array of internal resource IDs (e.g. user IDs) within the row - this ID list will be enriched with
|
||||||
* the full resources when rows are returned from the API. The full resources can be input to the API, or
|
* the full resources when rows are returned from the API. The full resources can be input to the API, or
|
||||||
* an array of resource IDs, the API will squash these down and validate them before saving the row.
|
* an array of resource IDs, the API will squash these down and validate them before saving the row.
|
||||||
*/
|
*/
|
||||||
BB_REFERENCE = "bb_reference",
|
BB_REFERENCE = "bb_reference",
|
||||||
|
/**
|
||||||
|
* a string type, called User within Budibase. Same logic as `bb_reference`, storing a single id as string instead of an array
|
||||||
|
*/
|
||||||
|
BB_REFERENCE_SINGLE = "bb_reference_single",
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface RowAttachment {
|
export interface RowAttachment {
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
import { FieldType } from "../row"
|
||||||
|
|
||||||
export enum RelationshipType {
|
export enum RelationshipType {
|
||||||
ONE_TO_MANY = "one-to-many",
|
ONE_TO_MANY = "one-to-many",
|
||||||
MANY_TO_ONE = "many-to-one",
|
MANY_TO_ONE = "many-to-one",
|
||||||
|
@ -27,5 +29,21 @@ export enum FormulaType {
|
||||||
|
|
||||||
export enum BBReferenceFieldSubType {
|
export enum BBReferenceFieldSubType {
|
||||||
USER = "user",
|
USER = "user",
|
||||||
|
/** @deprecated this should not be used anymore, left here in order to support the existing usages */
|
||||||
USERS = "users",
|
USERS = "users",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type SupportedSqlTypes =
|
||||||
|
| FieldType.STRING
|
||||||
|
| FieldType.BARCODEQR
|
||||||
|
| FieldType.LONGFORM
|
||||||
|
| FieldType.OPTIONS
|
||||||
|
| FieldType.DATETIME
|
||||||
|
| FieldType.NUMBER
|
||||||
|
| FieldType.BOOLEAN
|
||||||
|
| FieldType.FORMULA
|
||||||
|
| FieldType.BIGINT
|
||||||
|
| FieldType.BB_REFERENCE
|
||||||
|
| FieldType.BB_REFERENCE_SINGLE
|
||||||
|
| FieldType.LINK
|
||||||
|
| FieldType.ARRAY
|
||||||
|
|
|
@ -110,9 +110,14 @@ export interface FormulaFieldMetadata extends BaseFieldSchema {
|
||||||
export interface BBReferenceFieldMetadata
|
export interface BBReferenceFieldMetadata
|
||||||
extends Omit<BaseFieldSchema, "subtype"> {
|
extends Omit<BaseFieldSchema, "subtype"> {
|
||||||
type: FieldType.BB_REFERENCE
|
type: FieldType.BB_REFERENCE
|
||||||
subtype: BBReferenceFieldSubType.USER | BBReferenceFieldSubType.USERS
|
subtype: BBReferenceFieldSubType
|
||||||
relationshipType?: RelationshipType
|
relationshipType?: RelationshipType
|
||||||
}
|
}
|
||||||
|
export interface BBReferenceSingleFieldMetadata
|
||||||
|
extends Omit<BaseFieldSchema, "subtype"> {
|
||||||
|
type: FieldType.BB_REFERENCE_SINGLE
|
||||||
|
subtype: Exclude<BBReferenceFieldSubType, BBReferenceFieldSubType.USERS>
|
||||||
|
}
|
||||||
|
|
||||||
export interface AttachmentFieldMetadata extends BaseFieldSchema {
|
export interface AttachmentFieldMetadata extends BaseFieldSchema {
|
||||||
type: FieldType.ATTACHMENTS
|
type: FieldType.ATTACHMENTS
|
||||||
|
@ -164,6 +169,7 @@ interface OtherFieldMetadata extends BaseFieldSchema {
|
||||||
| FieldType.NUMBER
|
| FieldType.NUMBER
|
||||||
| FieldType.LONGFORM
|
| FieldType.LONGFORM
|
||||||
| FieldType.BB_REFERENCE
|
| FieldType.BB_REFERENCE
|
||||||
|
| FieldType.BB_REFERENCE_SINGLE
|
||||||
| FieldType.ATTACHMENTS
|
| FieldType.ATTACHMENTS
|
||||||
>
|
>
|
||||||
}
|
}
|
||||||
|
@ -179,6 +185,7 @@ export type FieldSchema =
|
||||||
| BBReferenceFieldMetadata
|
| BBReferenceFieldMetadata
|
||||||
| JsonFieldMetadata
|
| JsonFieldMetadata
|
||||||
| AttachmentFieldMetadata
|
| AttachmentFieldMetadata
|
||||||
|
| BBReferenceSingleFieldMetadata
|
||||||
|
|
||||||
export interface TableSchema {
|
export interface TableSchema {
|
||||||
[key: string]: FieldSchema
|
[key: string]: FieldSchema
|
||||||
|
@ -207,15 +214,3 @@ export function isManyToOne(
|
||||||
): field is ManyToOneRelationshipFieldMetadata {
|
): field is ManyToOneRelationshipFieldMetadata {
|
||||||
return field.relationshipType === RelationshipType.MANY_TO_ONE
|
return field.relationshipType === RelationshipType.MANY_TO_ONE
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isBBReferenceField(
|
|
||||||
field: FieldSchema
|
|
||||||
): field is BBReferenceFieldMetadata {
|
|
||||||
return field.type === FieldType.BB_REFERENCE
|
|
||||||
}
|
|
||||||
|
|
||||||
export function isAttachmentField(
|
|
||||||
field: FieldSchema
|
|
||||||
): field is AttachmentFieldMetadata {
|
|
||||||
return field.type === FieldType.ATTACHMENTS
|
|
||||||
}
|
|
||||||
|
|
Loading…
Reference in a new issue