diff --git a/.eslintrc.json b/.eslintrc.json index ce4b07ca2f..d475bba8d1 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -54,7 +54,8 @@ "ignoreRestSiblings": true } ], - "local-rules/no-budibase-imports": "error" + "no-redeclare": "off", + "@typescript-eslint/no-redeclare": "error" } }, { diff --git a/lerna.json b/lerna.json index 9c5a6c6bab..df40d0305a 100644 --- a/lerna.json +++ b/lerna.json @@ -1,5 +1,5 @@ { - "version": "2.24.2", + "version": "2.26.0", "npmClient": "yarn", "packages": [ "packages/*", diff --git a/packages/backend-core/src/cache/user.ts b/packages/backend-core/src/cache/user.ts index ecfa20f99e..d319c5dcb6 100644 --- a/packages/backend-core/src/cache/user.ts +++ b/packages/backend-core/src/cache/user.ts @@ -69,7 +69,7 @@ async function populateUsersFromDB( export async function getUser( userId: string, tenantId?: string, - populateUser?: any + populateUser?: (userId: string, tenantId: string) => Promise ) { if (!populateUser) { populateUser = populateFromDB @@ -83,7 +83,7 @@ export async function getUser( } const client = await redis.getUserClient() // try cache - let user = await client.get(userId) + let user: User = await client.get(userId) if (!user) { user = await populateUser(userId, tenantId) await client.store(userId, user, EXPIRY_SECONDS) diff --git a/packages/backend-core/src/db/couch/DatabaseImpl.ts b/packages/backend-core/src/db/couch/DatabaseImpl.ts index 86fc06c24c..7340d10270 100644 --- a/packages/backend-core/src/db/couch/DatabaseImpl.ts +++ b/packages/backend-core/src/db/couch/DatabaseImpl.ts @@ -3,11 +3,11 @@ import { AllDocsResponse, AnyDocument, Database, - DatabaseOpts, - DatabaseQueryOpts, - DatabasePutOpts, DatabaseCreateIndexOpts, DatabaseDeleteIndexOpts, + DatabaseOpts, + DatabasePutOpts, + DatabaseQueryOpts, Document, isDocument, RowResponse, @@ -17,7 +17,7 @@ import { import { getCouchInfo } from "./connections" import { directCouchUrlCall } from "./utils" import { getPouchDB } from "./pouchDB" -import { WriteStream, ReadStream } from "fs" +import { ReadStream, WriteStream } from "fs" import { newid } from "../../docIds/newid" import { SQLITE_DESIGN_DOC_ID } from "../../constants" import { DDInstrumentedDatabase } from "../instrumentation" @@ -39,6 +39,39 @@ function buildNano(couchInfo: { url: string; cookie: string }) { type DBCall = () => Promise +class CouchDBError extends Error { + status: number + statusCode: number + reason: string + name: string + errid: string + error: string + description: string + + constructor( + message: string, + info: { + status: number | undefined + statusCode: number | undefined + name: string + errid: string + description: string + reason: string + error: string + } + ) { + super(message) + const statusCode = info.status || info.statusCode || 500 + this.status = statusCode + this.statusCode = statusCode + this.reason = info.reason + this.name = info.name + this.errid = info.errid + this.description = info.description + this.error = info.error + } +} + export function DatabaseWithConnection( dbName: string, connection: string, @@ -120,7 +153,7 @@ export class DatabaseImpl implements Database { } catch (err: any) { // Handling race conditions if (err.statusCode !== 412) { - throw err + throw new CouchDBError(err.message, err) } } } @@ -139,10 +172,9 @@ export class DatabaseImpl implements Database { if (err.statusCode === 404 && err.reason === DATABASE_NOT_FOUND) { await this.checkAndCreateDb() return await this.performCall(call) - } else if (err.statusCode) { - err.status = err.statusCode } - throw err + // stripping the error down the props which are safe/useful, drop everything else + throw new CouchDBError(`CouchDB error: ${err.message}`, err) } } @@ -323,7 +355,7 @@ export class DatabaseImpl implements Database { if (err.statusCode === 404) { return } else { - throw { ...err, status: err.statusCode } + throw new CouchDBError(err.message, err) } } } diff --git a/packages/backend-core/src/middleware/authenticated.ts b/packages/backend-core/src/middleware/authenticated.ts index 69dba27c43..51cd4ec2af 100644 --- a/packages/backend-core/src/middleware/authenticated.ts +++ b/packages/backend-core/src/middleware/authenticated.ts @@ -13,7 +13,7 @@ import { getGlobalDB, doInTenant } from "../context" import { decrypt } from "../security/encryption" import * as identity from "../context/identity" 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 tracer from "dd-trace" @@ -41,7 +41,10 @@ function finalise(ctx: any, opts: FinaliseOpts = {}) { ctx.version = opts.version } -async function checkApiKey(apiKey: string, populateUser?: Function) { +async function checkApiKey( + apiKey: string, + populateUser?: (userId: string, tenantId: string) => Promise +) { // check both the primary and the fallback internal api keys // this allows for rotation if (isValidInternalAPIKey(apiKey)) { @@ -128,6 +131,7 @@ export default function ( } else { user = await getUser(userId, session.tenantId) } + // @ts-ignore user.csrfToken = session.csrfToken if (session?.lastAccessedAt < timeMinusOneMinute()) { @@ -167,19 +171,25 @@ export default function ( authenticated = false } - if (user) { + const isUser = ( + user: any + ): user is User & { budibaseAccess?: string } => { + return user && user.email + } + + if (isUser(user)) { tracer.setUser({ - id: user?._id, - tenantId: user?.tenantId, - budibaseAccess: user?.budibaseAccess, - status: user?.status, + id: user._id!, + tenantId: user.tenantId, + budibaseAccess: user.budibaseAccess, + status: user.status, }) } // isAuthenticated is a function, so use a variable to be able to check authed state finalise(ctx, { authenticated, user, internal, version, publicEndpoint }) - if (user && user.email) { + if (isUser(user)) { return identity.doInUserContext(user, ctx, next) } else { return next() diff --git a/packages/bbui/src/FancyForm/FancyInput.svelte b/packages/bbui/src/FancyForm/FancyInput.svelte index 0c58b9b045..f665fa5724 100644 --- a/packages/bbui/src/FancyForm/FancyInput.svelte +++ b/packages/bbui/src/FancyForm/FancyInput.svelte @@ -11,6 +11,7 @@ export let error = null export let validate = null export let suffix = null + export let validateOn = "change" const dispatch = createEventDispatcher() @@ -24,7 +25,16 @@ const newValue = e.target.value dispatch("change", newValue) value = newValue - if (validate) { + if (validate && (error || validateOn === "change")) { + error = validate(newValue) + } + } + + const onBlur = e => { + focused = false + const newValue = e.target.value + dispatch("blur", newValue) + if (validate && validateOn === "blur") { error = validate(newValue) } } @@ -61,7 +71,7 @@ type={type || "text"} on:input={onChange} on:focus={() => (focused = true)} - on:blur={() => (focused = false)} + on:blur={onBlur} class:placeholder bind:this={ref} /> diff --git a/packages/builder/src/components/automation/SetupPanel/AutomationBlockSetup.svelte b/packages/builder/src/components/automation/SetupPanel/AutomationBlockSetup.svelte index 0cf0f6c740..85af5bbafd 100644 --- a/packages/builder/src/components/automation/SetupPanel/AutomationBlockSetup.svelte +++ b/packages/builder/src/components/automation/SetupPanel/AutomationBlockSetup.svelte @@ -48,6 +48,7 @@ import { TriggerStepID, ActionStepID } from "constants/backend/automations" import { onMount } from "svelte" import { cloneDeep } from "lodash/fp" + import { FIELDS } from "constants/backend" export let block export let testData @@ -228,6 +229,10 @@ categoryName, bindingName ) => { + const field = Object.values(FIELDS).find( + field => field.type === value.type && field.subtype === value.subtype + ) + return { readableBinding: bindingName ? `${bindingName}.${name}` @@ -238,7 +243,7 @@ icon, category: categoryName, display: { - type: value.type, + type: field?.name || value.type, name, rank: isLoopBlock ? idx + 1 : idx - loopBlockCount, }, @@ -282,6 +287,7 @@ for (const key in table?.schema) { schema[key] = { type: table.schema[key].type, + subtype: table.schema[key].subtype, } } // remove the original binding diff --git a/packages/builder/src/components/backend/DataTable/formula.js b/packages/builder/src/components/backend/DataTable/formula.js index b339729391..c59fb9c536 100644 --- a/packages/builder/src/components/backend/DataTable/formula.js +++ b/packages/builder/src/components/backend/DataTable/formula.js @@ -55,7 +55,7 @@ export function getBindings({ ) } 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}` diff --git a/packages/builder/src/components/backend/DataTable/modals/CreateEditColumn.svelte b/packages/builder/src/components/backend/DataTable/modals/CreateEditColumn.svelte index 81d5545c40..622da2173d 100644 --- a/packages/builder/src/components/backend/DataTable/modals/CreateEditColumn.svelte +++ b/packages/builder/src/components/backend/DataTable/modals/CreateEditColumn.svelte @@ -12,8 +12,13 @@ OptionSelectDnD, Layout, AbsTooltip, + ProgressCircle, } 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 { cloneDeep } from "lodash/fp" import { tables, datasources } from "stores/builder" @@ -30,8 +35,8 @@ import { getBindings } from "components/backend/DataTable/formula" import JSONSchemaModal from "./JSONSchemaModal.svelte" import { - FieldType, BBReferenceFieldSubType, + FieldType, SourceName, } from "@budibase/types" import RelationshipSelector from "components/common/RelationshipSelector.svelte" @@ -67,7 +72,6 @@ let savingColumn let deleteColName let jsonSchemaModal - let allowedTypes = [] let editableColumn = { type: FIELDS.STRING.type, constraints: FIELDS.STRING.constraints, @@ -175,6 +179,11 @@ SWITCHABLE_TYPES[field.type] && !editableColumn?.autocolumn) + $: allowedTypes = getAllowedTypes(datasource).map(t => ({ + fieldId: makeFieldId(t.type, t.subtype), + ...t, + })) + const fieldDefinitions = Object.values(FIELDS).reduce( // Storing the fields by complex field id (acc, field) => ({ @@ -188,7 +197,10 @@ // don't make field IDs for auto types if (type === AUTO_TYPE || autocolumn) { return type.toUpperCase() - } else if (type === FieldType.BB_REFERENCE) { + } else if ( + type === FieldType.BB_REFERENCE || + type === FieldType.BB_REFERENCE_SINGLE + ) { return `${type}${subtype || ""}`.toUpperCase() } else { return type.toUpperCase() @@ -226,11 +238,6 @@ editableColumn.subtype, editableColumn.autocolumn ) - - allowedTypes = getAllowedTypes().map(t => ({ - fieldId: makeFieldId(t.type, t.subtype), - ...t, - })) } } @@ -245,11 +252,11 @@ } async function saveColumn() { - savingColumn = true if (errors?.length) { return } + savingColumn = true let saveColumn = cloneDeep(editableColumn) delete saveColumn.fieldId @@ -264,13 +271,6 @@ if (saveColumn.type !== LINK_TYPE) { 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 { await tables.saveField({ @@ -289,6 +289,8 @@ } } catch (err) { notifications.error(`Error saving column: ${err.message}`) + } finally { + savingColumn = false } } @@ -363,20 +365,36 @@ deleteColName = "" } - function getAllowedTypes() { + function getAllowedTypes(datasource) { if (originalName) { - const possibleTypes = SWITCHABLE_TYPES[field.type] || [ - editableColumn.type, - ] + let possibleTypes = SWITCHABLE_TYPES[field.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) .filter(([_, field]) => possibleTypes.includes(field.type)) .map(([_, fieldDefinition]) => fieldDefinition) } - const isUsers = - editableColumn.type === FieldType.BB_REFERENCE && - editableColumn.subtype === BBReferenceFieldSubType.USERS - if (!externalTable) { return [ FIELDS.STRING, @@ -393,7 +411,8 @@ FIELDS.LINK, FIELDS.FORMULA, FIELDS.JSON, - isUsers ? FIELDS.USERS : FIELDS.USER, + FIELDS.USER, + FIELDS.USERS, FIELDS.AUTO, ] } else { @@ -407,8 +426,12 @@ FIELDS.BOOLEAN, FIELDS.FORMULA, 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 if (!externalTable || table.sql) { fields = [...fields, FIELDS.LINK, FIELDS.ARRAY] @@ -482,15 +505,6 @@ return newError } - function isUsersColumn(column) { - return ( - column.type === FieldType.BB_REFERENCE && - [BBReferenceFieldSubType.USER, BBReferenceFieldSubType.USERS].includes( - column.subtype - ) - ) - } - onMount(() => { mounted = true }) @@ -689,22 +703,6 @@ - {:else if isUsersColumn(editableColumn) && datasource?.source !== SourceName.GOOGLE_SHEETS} - - handleTypeChange( - makeFieldId( - FieldType.BB_REFERENCE, - e.detail - ? BBReferenceFieldSubType.USERS - : BBReferenceFieldSubType.USER - ) - )} - disabled={!isCreating} - thin - text="Allow multiple users" - /> {/if} {#if editableColumn.type === AUTO_TYPE || editableColumn.autocolumn}