diff --git a/lerna.json b/lerna.json index 647c9f202d..d0f0bd23c5 100644 --- a/lerna.json +++ b/lerna.json @@ -1,6 +1,6 @@ { "$schema": "node_modules/lerna/schemas/lerna-schema.json", - "version": "3.2.37", + "version": "3.2.42", "npmClient": "yarn", "concurrency": 20, "command": { diff --git a/packages/backend-core/package.json b/packages/backend-core/package.json index 3e1b5f324b..1ab05cd14b 100644 --- a/packages/backend-core/package.json +++ b/packages/backend-core/package.json @@ -21,7 +21,7 @@ "scripts": { "prebuild": "rimraf dist/", "prepack": "cp package.json dist", - "build": "node ./scripts/build.js && tsc -p tsconfig.build.json --emitDeclarationOnly --paths null", + "build": "node ./scripts/build.js && tsc -p tsconfig.build.json --emitDeclarationOnly --paths null && tsc -p tsconfig.test.json --paths null", "build:dev": "yarn prebuild && tsc --build --watch --preserveWatchOutput", "build:oss": "node ./scripts/build.js", "check:types": "tsc -p tsconfig.json --noEmit --paths null", diff --git a/packages/backend-core/src/middleware/errorHandling.ts b/packages/backend-core/src/middleware/errorHandling.ts index 6ceda9cd3a..5a5a25b461 100644 --- a/packages/backend-core/src/middleware/errorHandling.ts +++ b/packages/backend-core/src/middleware/errorHandling.ts @@ -32,8 +32,12 @@ export async function errorHandling(ctx: any, next: any) { } if (environment.isTest() && ctx.headers["x-budibase-include-stacktrace"]) { + let rootErr = err + while (rootErr.cause) { + rootErr = rootErr.cause + } // @ts-ignore - error.stack = err.stack + error.stack = rootErr.stack } ctx.body = error diff --git a/packages/backend-core/src/sql/sql.ts b/packages/backend-core/src/sql/sql.ts index 5f462ee144..5a6907faa0 100644 --- a/packages/backend-core/src/sql/sql.ts +++ b/packages/backend-core/src/sql/sql.ts @@ -272,17 +272,6 @@ class InternalBuilder { return parts.join(".") } - private isFullSelectStatementRequired(): boolean { - for (let column of Object.values(this.table.schema)) { - if (this.SPECIAL_SELECT_CASES.POSTGRES_MONEY(column)) { - return true - } else if (this.SPECIAL_SELECT_CASES.MSSQL_DATES(column)) { - return true - } - } - return false - } - private generateSelectStatement(): (string | Knex.Raw)[] | "*" { const { table, resource } = this.query @@ -292,11 +281,9 @@ class InternalBuilder { const alias = this.getTableName(table) const schema = this.table.schema - if (!this.isFullSelectStatementRequired()) { - return [this.knex.raw("??", [`${alias}.*`])] - } + // get just the fields for this table - return resource.fields + const tableFields = resource.fields .map(field => { const parts = field.split(/\./g) let table: string | undefined = undefined @@ -311,34 +298,33 @@ class InternalBuilder { return { table, column, field } }) .filter(({ table }) => !table || table === alias) - .map(({ table, column, field }) => { - const columnSchema = schema[column] - if (this.SPECIAL_SELECT_CASES.POSTGRES_MONEY(columnSchema)) { - return this.knex.raw(`??::money::numeric as ??`, [ - this.rawQuotedIdentifier([table, column].join(".")), - this.knex.raw(this.quote(field)), - ]) - } + return tableFields.map(({ table, column, field }) => { + const columnSchema = schema[column] - if (this.SPECIAL_SELECT_CASES.MSSQL_DATES(columnSchema)) { - // Time gets returned as timestamp from mssql, not matching the expected - // HH:mm format + if (this.SPECIAL_SELECT_CASES.POSTGRES_MONEY(columnSchema)) { + return this.knex.raw(`??::money::numeric as ??`, [ + this.rawQuotedIdentifier([table, column].join(".")), + this.knex.raw(this.quote(field)), + ]) + } - // TODO: figure out how to express this safely without string - // interpolation. - return this.knex.raw(`CONVERT(varchar, ??, 108) as ??`, [ - this.rawQuotedIdentifier(field), - this.knex.raw(this.quote(field)), - ]) - } + if (this.SPECIAL_SELECT_CASES.MSSQL_DATES(columnSchema)) { + // Time gets returned as timestamp from mssql, not matching the expected + // HH:mm format - if (table) { - return this.rawQuotedIdentifier(`${table}.${column}`) - } else { - return this.rawQuotedIdentifier(field) - } - }) + return this.knex.raw(`CONVERT(varchar, ??, 108) as ??`, [ + this.rawQuotedIdentifier(field), + this.knex.raw(this.quote(field)), + ]) + } + + if (table) { + return this.rawQuotedIdentifier(`${table}.${column}`) + } else { + return this.rawQuotedIdentifier(field) + } + }) } // OracleDB can't use character-large-objects (CLOBs) in WHERE clauses, @@ -816,14 +802,29 @@ class InternalBuilder { filters.oneOf, ArrayOperator.ONE_OF, (q, key: string, array) => { + const schema = this.getFieldSchema(key) + const values = Array.isArray(array) ? array : [array] if (shouldOr) { q = q.or } if (this.client === SqlClient.ORACLE) { // @ts-ignore key = this.convertClobs(key) + } else if ( + this.client === SqlClient.SQL_LITE && + schema?.type === FieldType.DATETIME && + schema.dateOnly + ) { + for (const value of values) { + if (value != null) { + q = q.or.whereLike(key, `${value.toISOString().slice(0, 10)}%`) + } else { + q = q.or.whereNull(key) + } + } + return q } - return q.whereIn(key, Array.isArray(array) ? array : [array]) + return q.whereIn(key, values) }, (q, key: string[], array) => { if (shouldOr) { @@ -882,6 +883,19 @@ class InternalBuilder { let high = value.high let low = value.low + if ( + this.client === SqlClient.SQL_LITE && + schema?.type === FieldType.DATETIME && + schema.dateOnly + ) { + if (high != null) { + high = `${high.toISOString().slice(0, 10)}T23:59:59.999Z` + } + if (low != null) { + low = low.toISOString().slice(0, 10) + } + } + if (this.client === SqlClient.ORACLE) { rawKey = this.convertClobs(key) } else if ( @@ -914,6 +928,7 @@ class InternalBuilder { } if (filters.equal) { iterate(filters.equal, BasicOperator.EQUAL, (q, key, value) => { + const schema = this.getFieldSchema(key) if (shouldOr) { q = q.or } @@ -928,6 +943,16 @@ class InternalBuilder { // @ts-expect-error knex types are wrong, raw is fine here subq.whereNotNull(identifier).andWhere(identifier, value) ) + } else if ( + this.client === SqlClient.SQL_LITE && + schema?.type === FieldType.DATETIME && + schema.dateOnly + ) { + if (value != null) { + return q.whereLike(key, `${value.toISOString().slice(0, 10)}%`) + } else { + return q.whereNull(key) + } } else { return q.whereRaw(`COALESCE(?? = ?, FALSE)`, [ this.rawQuotedIdentifier(key), @@ -938,6 +963,7 @@ class InternalBuilder { } if (filters.notEqual) { iterate(filters.notEqual, BasicOperator.NOT_EQUAL, (q, key, value) => { + const schema = this.getFieldSchema(key) if (shouldOr) { q = q.or } @@ -959,6 +985,18 @@ class InternalBuilder { // @ts-expect-error knex types are wrong, raw is fine here .or.whereNull(identifier) ) + } else if ( + this.client === SqlClient.SQL_LITE && + schema?.type === FieldType.DATETIME && + schema.dateOnly + ) { + if (value != null) { + return q.not + .whereLike(key, `${value.toISOString().slice(0, 10)}%`) + .or.whereNull(key) + } else { + return q.not.whereNull(key) + } } else { return q.whereRaw(`COALESCE(?? != ?, TRUE)`, [ this.rawQuotedIdentifier(key), @@ -1239,6 +1277,7 @@ class InternalBuilder { if (!toTable || !fromTable) { continue } + const relatedTable = tables[toTable] if (!relatedTable) { throw new Error(`related table "${toTable}" not found in datasource`) @@ -1267,6 +1306,10 @@ class InternalBuilder { const fieldList = relationshipFields.map(field => this.buildJsonField(relatedTable, field) ) + if (!fieldList.length) { + continue + } + const fieldListFormatted = fieldList .map(f => { const separator = this.client === SqlClient.ORACLE ? " VALUE " : "," @@ -1307,7 +1350,9 @@ class InternalBuilder { ) const standardWrap = (select: Knex.Raw): Knex.QueryBuilder => { - subQuery = subQuery.select(`${toAlias}.*`).limit(getRelationshipLimit()) + subQuery = subQuery + .select(relationshipFields) + .limit(getRelationshipLimit()) // @ts-ignore - the from alias syntax isn't in Knex typing return knex.select(select).from({ [toAlias]: subQuery, @@ -1537,11 +1582,12 @@ class InternalBuilder { limits?: { base: number; query: number } } = {} ): Knex.QueryBuilder { - let { operation, filters, paginate, relationships, table } = this.query + const { operation, filters, paginate, relationships, table } = this.query const { limits } = opts // start building the query let query = this.qualifiedKnex() + // handle pagination let foundOffset: number | null = null let foundLimit = limits?.query || limits?.base @@ -1590,7 +1636,7 @@ class InternalBuilder { const mainTable = this.query.tableAliases?.[table.name] || table.name const cte = this.addSorting( this.knex - .with("paginated", query) + .with("paginated", query.clone().clearSelect().select("*")) .select(this.generateSelectStatement()) .from({ [mainTable]: "paginated", diff --git a/packages/backend-core/src/sql/utils.ts b/packages/backend-core/src/sql/utils.ts index 16b352995b..b07854b2a0 100644 --- a/packages/backend-core/src/sql/utils.ts +++ b/packages/backend-core/src/sql/utils.ts @@ -14,7 +14,7 @@ import environment from "../environment" const DOUBLE_SEPARATOR = `${SEPARATOR}${SEPARATOR}` const ROW_ID_REGEX = /^\[.*]$/g const ENCODED_SPACE = encodeURIComponent(" ") -const ISO_DATE_REGEX = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z$/ +const ISO_DATE_REGEX = /^\d{4}-\d{2}-\d{2}(?:T\d{2}:\d{2}:\d{2}.\d{3}Z)?$/ const TIME_REGEX = /^(?:\d{2}:)?(?:\d{2}:)(?:\d{2})$/ export function isExternalTableID(tableId: string) { @@ -149,15 +149,7 @@ export function isInvalidISODateString(str: string) { } export function isValidISODateString(str: string) { - const trimmedValue = str.trim() - if (!ISO_DATE_REGEX.test(trimmedValue)) { - return false - } - let d = new Date(trimmedValue) - if (isNaN(d.getTime())) { - return false - } - return d.toISOString() === trimmedValue + return ISO_DATE_REGEX.test(str.trim()) } export function isValidFilter(value: any) { diff --git a/packages/backend-core/tsconfig.test.json b/packages/backend-core/tsconfig.test.json new file mode 100644 index 0000000000..0e3e7a79b2 --- /dev/null +++ b/packages/backend-core/tsconfig.test.json @@ -0,0 +1,9 @@ +{ + "extends": "../../tsconfig.build.json", + "compilerOptions": { + "outDir": "dist", + "sourceMap": true + }, + "include": ["tests/**/*.js", "tests/**/*.ts"], + "exclude": ["node_modules", "dist"] +} diff --git a/packages/builder/src/components/backend/DataTable/modals/CreateEditColumn.svelte b/packages/builder/src/components/backend/DataTable/modals/CreateEditColumn.svelte index db5b8a7d49..55ac8474a6 100644 --- a/packages/builder/src/components/backend/DataTable/modals/CreateEditColumn.svelte +++ b/packages/builder/src/components/backend/DataTable/modals/CreateEditColumn.svelte @@ -49,7 +49,6 @@ import { RowUtils, canBeDisplayColumn } from "@budibase/frontend-core" import ServerBindingPanel from "@/components/common/bindings/ServerBindingPanel.svelte" import OptionsEditor from "./OptionsEditor.svelte" - import { isEnabled } from "@/helpers/featureFlags" import { getUserBindings } from "@/dataBinding" export let field @@ -168,7 +167,6 @@ // used to select what different options can be displayed for column type $: canBeDisplay = canBeDisplayColumn(editableColumn) && !editableColumn.autocolumn - $: defaultValuesEnabled = isEnabled("DEFAULT_VALUES") $: canHaveDefault = !required && canHaveDefaultColumn(editableColumn.type) $: canBeRequired = editableColumn?.type !== FieldType.LINK && @@ -300,7 +298,7 @@ } // Ensure we don't have a default value if we can't have one - if (!canHaveDefault || !defaultValuesEnabled) { + if (!canHaveDefault) { delete saveColumn.default } @@ -848,51 +846,49 @@ {/if} - {#if defaultValuesEnabled} - {#if editableColumn.type === FieldType.OPTIONS} - (editableColumn.default = e.detail)} + placeholder="None" + /> + {:else if editableColumn.type === FieldType.ARRAY} + + (editableColumn.default = e.detail?.length ? e.detail : undefined)} + placeholder="None" + /> + {:else if editableColumn.subtype === BBReferenceFieldSubType.USER} + {@const defaultValue = + editableColumn.type === FieldType.BB_REFERENCE_SINGLE + ? SingleUserDefault + : MultiUserDefault} + + (editableColumn.default = e.detail ? defaultValue : undefined)} + /> + {:else} + (editableColumn.default = e.detail)} + bindings={defaultValueBindings} + allowJS + /> {/if} diff --git a/packages/builder/src/components/common/bindings/SnippetDrawer.svelte b/packages/builder/src/components/common/bindings/SnippetDrawer.svelte index 90ca6ffd9f..f10c44f81f 100644 --- a/packages/builder/src/components/common/bindings/SnippetDrawer.svelte +++ b/packages/builder/src/components/common/bindings/SnippetDrawer.svelte @@ -28,7 +28,9 @@ let loading = false let deleteConfirmationDialog - $: defaultName = getSequentialName($snippets, "MySnippet", x => x.name) + $: defaultName = getSequentialName($snippets, "MySnippet", { + getName: x => x.name, + }) $: key = snippet?.name $: name = snippet?.name || defaultName $: code = snippet?.code ? encodeJSBinding(snippet.code) : "" diff --git a/packages/builder/src/constants/backend/automations.js b/packages/builder/src/constants/backend/automations.ts similarity index 100% rename from packages/builder/src/constants/backend/automations.js rename to packages/builder/src/constants/backend/automations.ts diff --git a/packages/builder/src/constants/backend/backups.js b/packages/builder/src/constants/backend/backups.ts similarity index 100% rename from packages/builder/src/constants/backend/backups.js rename to packages/builder/src/constants/backend/backups.ts diff --git a/packages/builder/src/constants/backend/index.js b/packages/builder/src/constants/backend/index.ts similarity index 97% rename from packages/builder/src/constants/backend/index.js rename to packages/builder/src/constants/backend/index.ts index 6ddf4c2138..b7d3f584be 100644 --- a/packages/builder/src/constants/backend/index.js +++ b/packages/builder/src/constants/backend/index.ts @@ -16,7 +16,10 @@ export { export const AUTO_COLUMN_SUB_TYPES = AutoFieldSubType -export const AUTO_COLUMN_DISPLAY_NAMES = { +export const AUTO_COLUMN_DISPLAY_NAMES: Record< + keyof typeof AUTO_COLUMN_SUB_TYPES, + string +> = { AUTO_ID: "Auto ID", CREATED_BY: "Created By", CREATED_AT: "Created At", @@ -209,13 +212,6 @@ export const Roles = { BUILDER: "BUILDER", } -export function isAutoColumnUserRelationship(subtype) { - return ( - subtype === AUTO_COLUMN_SUB_TYPES.CREATED_BY || - subtype === AUTO_COLUMN_SUB_TYPES.UPDATED_BY - ) -} - export const PrettyRelationshipDefinitions = { MANY: "Many rows", ONE: "One row", diff --git a/packages/builder/src/constants/index.js b/packages/builder/src/constants/index.ts similarity index 100% rename from packages/builder/src/constants/index.js rename to packages/builder/src/constants/index.ts diff --git a/packages/builder/src/helpers/duplicate.js b/packages/builder/src/helpers/duplicate.ts similarity index 86% rename from packages/builder/src/helpers/duplicate.js rename to packages/builder/src/helpers/duplicate.ts index 361e1faa25..b4740b3e52 100644 --- a/packages/builder/src/helpers/duplicate.js +++ b/packages/builder/src/helpers/duplicate.ts @@ -10,13 +10,13 @@ * * Repl */ -export const duplicateName = (name, allNames) => { +export const duplicateName = (name: string, allNames: string[]) => { const duplicatePattern = new RegExp(`\\s(\\d+)$`) const baseName = name.split(duplicatePattern)[0] const isDuplicate = new RegExp(`${baseName}\\s(\\d+)$`) // get the sequence from matched names - const sequence = [] + const sequence: number[] = [] allNames.filter(n => { if (n === baseName) { return true @@ -70,12 +70,18 @@ export const duplicateName = (name, allNames) => { * @param getName optional function to extract the name for an item, if not a * flat array of strings */ -export const getSequentialName = ( - items, - prefix, - { getName = x => x, numberFirstItem = false } = {} +export const getSequentialName = ( + items: T[] | null, + prefix: string | null, + { + getName, + numberFirstItem, + }: { + getName?: (item: T) => string + numberFirstItem?: boolean + } = {} ) => { - if (!prefix?.length || !getName) { + if (!prefix?.length) { return null } const trimmedPrefix = prefix.trim() @@ -85,7 +91,7 @@ export const getSequentialName = ( } let max = 0 items.forEach(item => { - const name = getName(item) + const name = getName?.(item) ?? item if (typeof name !== "string" || !name.startsWith(trimmedPrefix)) { return } diff --git a/packages/builder/src/helpers/featureFlags.js b/packages/builder/src/helpers/featureFlags.ts similarity index 54% rename from packages/builder/src/helpers/featureFlags.js rename to packages/builder/src/helpers/featureFlags.ts index e9054e8a9c..faa57892e8 100644 --- a/packages/builder/src/helpers/featureFlags.js +++ b/packages/builder/src/helpers/featureFlags.ts @@ -1,7 +1,8 @@ +import { FeatureFlag } from "@budibase/types" import { auth } from "../stores/portal" import { get } from "svelte/store" -export const isEnabled = featureFlag => { +export const isEnabled = (featureFlag: FeatureFlag | `${FeatureFlag}`) => { const user = get(auth).user return !!user?.flags?.[featureFlag] } diff --git a/packages/builder/src/helpers/fetchData.js b/packages/builder/src/helpers/fetchData.ts similarity index 57% rename from packages/builder/src/helpers/fetchData.js rename to packages/builder/src/helpers/fetchData.ts index 085d0ae5b4..cffb8f4d7d 100644 --- a/packages/builder/src/helpers/fetchData.js +++ b/packages/builder/src/helpers/fetchData.ts @@ -1,13 +1,21 @@ import { writable } from "svelte/store" import { API } from "@/api" -export default function (url) { - const store = writable({ status: "LOADING", data: {}, error: {} }) +export default function (url: string) { + const store = writable<{ + status: "LOADING" | "SUCCESS" | "ERROR" + data: object + error?: unknown + }>({ + status: "LOADING", + data: {}, + error: {}, + }) async function get() { store.update(u => ({ ...u, status: "LOADING" })) try { - const data = await API.get({ url }) + const data = await API.get({ url }) store.set({ data, status: "SUCCESS" }) } catch (e) { store.set({ data: {}, error: e, status: "ERROR" }) diff --git a/packages/builder/src/helpers/helpers.js b/packages/builder/src/helpers/helpers.js deleted file mode 100644 index 99483d40e2..0000000000 --- a/packages/builder/src/helpers/helpers.js +++ /dev/null @@ -1,46 +0,0 @@ -import { last, flow } from "lodash/fp" - -export const buildStyle = styles => { - let str = "" - for (let s in styles) { - if (styles[s]) { - let key = convertCamel(s) - str += `${key}: ${styles[s]}; ` - } - } - return str -} - -export const convertCamel = str => { - return str.replace(/[A-Z]/g, match => `-${match.toLowerCase()}`) -} - -export const pipe = (arg, funcs) => flow(funcs)(arg) - -export const capitalise = s => { - if (!s) { - return s - } - return s.substring(0, 1).toUpperCase() + s.substring(1) -} - -export const lowercase = s => s.substring(0, 1).toLowerCase() + s.substring(1) - -export const lowercaseExceptFirst = s => - s.charAt(0) + s.substring(1).toLowerCase() - -export const get_name = s => (!s ? "" : last(s.split("/"))) - -export const get_capitalised_name = name => pipe(name, [get_name, capitalise]) - -export const isBuilderInputFocused = e => { - const activeTag = document.activeElement?.tagName.toLowerCase() - const inCodeEditor = document.activeElement?.classList?.contains("cm-content") - if ( - (inCodeEditor || ["input", "textarea"].indexOf(activeTag) !== -1) && - e.key !== "Escape" - ) { - return true - } - return false -} diff --git a/packages/builder/src/helpers/helpers.ts b/packages/builder/src/helpers/helpers.ts new file mode 100644 index 0000000000..f767e6f59f --- /dev/null +++ b/packages/builder/src/helpers/helpers.ts @@ -0,0 +1,50 @@ +import type { Many } from "lodash" +import { last, flow } from "lodash/fp" + +export const buildStyle = (styles: Record) => { + let str = "" + for (let s in styles) { + if (styles[s]) { + let key = convertCamel(s) + str += `${key}: ${styles[s]}; ` + } + } + return str +} + +export const convertCamel = (str: string) => { + return str.replace(/[A-Z]/g, match => `-${match.toLowerCase()}`) +} + +export const pipe = (arg: string, funcs: Many<(...args: any[]) => any>) => + flow(funcs)(arg) + +export const capitalise = (s: string) => { + if (!s) { + return s + } + return s.substring(0, 1).toUpperCase() + s.substring(1) +} + +export const lowercase = (s: string) => + s.substring(0, 1).toLowerCase() + s.substring(1) + +export const lowercaseExceptFirst = (s: string) => + s.charAt(0) + s.substring(1).toLowerCase() + +export const get_name = (s: string) => (!s ? "" : last(s.split("/"))) + +export const get_capitalised_name = (name: string) => + pipe(name, [get_name, capitalise]) + +export const isBuilderInputFocused = (e: KeyboardEvent) => { + const activeTag = document.activeElement?.tagName.toLowerCase() + const inCodeEditor = document.activeElement?.classList?.contains("cm-content") + if ( + (inCodeEditor || ["input", "textarea"].indexOf(activeTag!) !== -1) && + e.key !== "Escape" + ) { + return true + } + return false +} diff --git a/packages/builder/src/helpers/index.js b/packages/builder/src/helpers/index.ts similarity index 100% rename from packages/builder/src/helpers/index.js rename to packages/builder/src/helpers/index.ts diff --git a/packages/builder/src/helpers/keyUtils.js b/packages/builder/src/helpers/keyUtils.js deleted file mode 100644 index 8d6dfb06dc..0000000000 --- a/packages/builder/src/helpers/keyUtils.js +++ /dev/null @@ -1,7 +0,0 @@ -function handleEnter(fnc) { - return e => e.key === "Enter" && fnc() -} - -export const keyUtils = { - handleEnter, -} diff --git a/packages/builder/src/helpers/keyUtils.ts b/packages/builder/src/helpers/keyUtils.ts new file mode 100644 index 0000000000..f78b05ec2b --- /dev/null +++ b/packages/builder/src/helpers/keyUtils.ts @@ -0,0 +1,7 @@ +function handleEnter(fnc: () => void) { + return (e: KeyboardEvent) => e.key === "Enter" && fnc() +} + +export const keyUtils = { + handleEnter, +} diff --git a/packages/builder/src/helpers/pagination.js b/packages/builder/src/helpers/pagination.ts similarity index 76% rename from packages/builder/src/helpers/pagination.js rename to packages/builder/src/helpers/pagination.ts index 122973f1a1..0280cd1052 100644 --- a/packages/builder/src/helpers/pagination.js +++ b/packages/builder/src/helpers/pagination.ts @@ -1,6 +1,16 @@ import { writable } from "svelte/store" -function defaultValue() { +interface PaginationStore { + nextPage: string | null | undefined + page: string | null | undefined + hasPrevPage: boolean + hasNextPage: boolean + loading: boolean + pageNumber: number + pages: string[] +} + +function defaultValue(): PaginationStore { return { nextPage: null, page: undefined, @@ -29,13 +39,13 @@ export function createPaginationStore() { update(state => { state.pageNumber++ state.page = state.nextPage - state.pages.push(state.page) + state.pages.push(state.page!) state.hasPrevPage = state.pageNumber > 1 return state }) } - function fetched(hasNextPage, nextPage) { + function fetched(hasNextPage: boolean, nextPage: string) { update(state => { state.hasNextPage = hasNextPage state.nextPage = nextPage diff --git a/packages/builder/src/helpers/planTitle.js b/packages/builder/src/helpers/planTitle.ts similarity index 86% rename from packages/builder/src/helpers/planTitle.js rename to packages/builder/src/helpers/planTitle.ts index c08b8bf3fe..ab342b4d93 100644 --- a/packages/builder/src/helpers/planTitle.js +++ b/packages/builder/src/helpers/planTitle.ts @@ -1,6 +1,6 @@ import { PlanType } from "@budibase/types" -export function getFormattedPlanName(userPlanType) { +export function getFormattedPlanName(userPlanType: PlanType) { let planName switch (userPlanType) { case PlanType.PRO: @@ -29,6 +29,6 @@ export function getFormattedPlanName(userPlanType) { return `${planName} Plan` } -export function isPremiumOrAbove(userPlanType) { +export function isPremiumOrAbove(userPlanType: PlanType) { return ![PlanType.PRO, PlanType.TEAM, PlanType.FREE].includes(userPlanType) } diff --git a/packages/builder/src/helpers/sanitizeUrl.js b/packages/builder/src/helpers/sanitizeUrl.ts similarity index 88% rename from packages/builder/src/helpers/sanitizeUrl.js rename to packages/builder/src/helpers/sanitizeUrl.ts index 4d00c503fb..5b76930037 100644 --- a/packages/builder/src/helpers/sanitizeUrl.js +++ b/packages/builder/src/helpers/sanitizeUrl.ts @@ -1,4 +1,4 @@ -export default function (url) { +export default function (url: string) { return url .split("/") .map(part => { diff --git a/packages/builder/src/helpers/tests/duplicate.test.js b/packages/builder/src/helpers/tests/duplicate.test.ts similarity index 100% rename from packages/builder/src/helpers/tests/duplicate.test.js rename to packages/builder/src/helpers/tests/duplicate.test.ts diff --git a/packages/builder/src/helpers/tests/nameHelpers.spec.js b/packages/builder/src/helpers/tests/nameHelpers.spec.ts similarity index 100% rename from packages/builder/src/helpers/tests/nameHelpers.spec.js rename to packages/builder/src/helpers/tests/nameHelpers.spec.ts diff --git a/packages/builder/src/helpers/utils.js b/packages/builder/src/helpers/utils.js deleted file mode 100644 index 27f459d6f2..0000000000 --- a/packages/builder/src/helpers/utils.js +++ /dev/null @@ -1,75 +0,0 @@ -import { FieldType } from "@budibase/types" -import { ActionStepID } from "@/constants/backend/automations" -import { TableNames } from "@/constants" -import { - AUTO_COLUMN_DISPLAY_NAMES, - AUTO_COLUMN_SUB_TYPES, - FIELDS, - isAutoColumnUserRelationship, -} from "@/constants/backend" -import { isEnabled } from "@/helpers/featureFlags" - -export function getAutoColumnInformation(enabled = true) { - let info = {} - for (const [key, subtype] of Object.entries(AUTO_COLUMN_SUB_TYPES)) { - // Because it's possible to replicate the functionality of CREATED_AT and - // CREATED_BY columns, we disable their creation when the DEFAULT_VALUES - // feature flag is enabled. - if (isEnabled("DEFAULT_VALUES")) { - if ( - subtype === AUTO_COLUMN_SUB_TYPES.CREATED_AT || - subtype === AUTO_COLUMN_SUB_TYPES.CREATED_BY - ) { - continue - } - } - info[subtype] = { enabled, name: AUTO_COLUMN_DISPLAY_NAMES[key] } - } - return info -} - -export function buildAutoColumn(tableName, name, subtype) { - let type, constraints - switch (subtype) { - case AUTO_COLUMN_SUB_TYPES.UPDATED_BY: - case AUTO_COLUMN_SUB_TYPES.CREATED_BY: - type = FieldType.LINK - constraints = FIELDS.LINK.constraints - break - case AUTO_COLUMN_SUB_TYPES.AUTO_ID: - type = FieldType.NUMBER - constraints = FIELDS.NUMBER.constraints - break - case AUTO_COLUMN_SUB_TYPES.UPDATED_AT: - case AUTO_COLUMN_SUB_TYPES.CREATED_AT: - type = FieldType.DATETIME - constraints = FIELDS.DATETIME.constraints - break - default: - type = FieldType.STRING - constraints = FIELDS.STRING.constraints - break - } - if (Object.values(AUTO_COLUMN_SUB_TYPES).indexOf(subtype) === -1) { - throw "Cannot build auto column with supplied subtype" - } - const base = { - name, - type, - subtype, - icon: "ri-magic-line", - autocolumn: true, - constraints, - } - if (isAutoColumnUserRelationship(subtype)) { - base.tableId = TableNames.USERS - base.fieldName = `${tableName}-${name}` - } - return base -} - -export function checkForCollectStep(automation) { - return automation.definition.steps.some( - step => step.stepId === ActionStepID.COLLECT - ) -} diff --git a/packages/builder/src/helpers/utils.ts b/packages/builder/src/helpers/utils.ts new file mode 100644 index 0000000000..140daaef3b --- /dev/null +++ b/packages/builder/src/helpers/utils.ts @@ -0,0 +1,96 @@ +import { + AutoFieldSubType, + Automation, + DateFieldMetadata, + FieldType, + NumberFieldMetadata, + RelationshipFieldMetadata, + RelationshipType, +} from "@budibase/types" +import { ActionStepID } from "@/constants/backend/automations" +import { TableNames } from "@/constants" +import { + AUTO_COLUMN_DISPLAY_NAMES, + AUTO_COLUMN_SUB_TYPES, + FIELDS, +} from "@/constants/backend" +import { utils } from "@budibase/shared-core" + +type AutoColumnInformation = Partial< + Record +> + +export function getAutoColumnInformation( + enabled = true +): AutoColumnInformation { + const info: AutoColumnInformation = {} + for (const [key, subtype] of Object.entries(AUTO_COLUMN_SUB_TYPES)) { + // Because it's possible to replicate the functionality of CREATED_AT and + // CREATED_BY columns with user column default values, we disable their creation + if ( + subtype === AUTO_COLUMN_SUB_TYPES.CREATED_AT || + subtype === AUTO_COLUMN_SUB_TYPES.CREATED_BY + ) { + continue + } + const typedKey = key as keyof typeof AUTO_COLUMN_SUB_TYPES + info[subtype] = { + enabled, + name: AUTO_COLUMN_DISPLAY_NAMES[typedKey], + } + } + return info +} + +export function buildAutoColumn( + tableName: string, + name: string, + subtype: AutoFieldSubType +): RelationshipFieldMetadata | NumberFieldMetadata | DateFieldMetadata { + const base = { + name, + icon: "ri-magic-line", + autocolumn: true, + } + + switch (subtype) { + case AUTO_COLUMN_SUB_TYPES.UPDATED_BY: + case AUTO_COLUMN_SUB_TYPES.CREATED_BY: + return { + ...base, + type: FieldType.LINK, + subtype, + constraints: FIELDS.LINK.constraints, + tableId: TableNames.USERS, + fieldName: `${tableName}-${name}`, + relationshipType: RelationshipType.MANY_TO_ONE, + } + + case AUTO_COLUMN_SUB_TYPES.AUTO_ID: + return { + ...base, + type: FieldType.NUMBER, + subtype, + constraints: FIELDS.NUMBER.constraints, + } + case AUTO_COLUMN_SUB_TYPES.UPDATED_AT: + case AUTO_COLUMN_SUB_TYPES.CREATED_AT: + return { + ...base, + type: FieldType.DATETIME, + subtype, + constraints: FIELDS.DATETIME.constraints, + } + + default: + throw utils.unreachable(subtype, { + message: "Cannot build auto column with supplied subtype", + }) + } +} + +export function checkForCollectStep(automation: Automation) { + return automation.definition.steps.some( + step => step.stepId === ActionStepID.COLLECT + ) +} diff --git a/packages/builder/src/helpers/warnings.js b/packages/builder/src/helpers/warnings.ts similarity index 85% rename from packages/builder/src/helpers/warnings.js rename to packages/builder/src/helpers/warnings.ts index ad943a8578..ae6c65666c 100644 --- a/packages/builder/src/helpers/warnings.js +++ b/packages/builder/src/helpers/warnings.ts @@ -1,4 +1,4 @@ -export const suppressWarnings = warnings => { +export const suppressWarnings = (warnings: string[]) => { if (!warnings?.length) { return } diff --git a/packages/builder/src/pages/builder/app/[application]/_components/BuilderSidePanel.svelte b/packages/builder/src/pages/builder/app/[application]/_components/BuilderSidePanel.svelte index 37abd7f1eb..2260892913 100644 --- a/packages/builder/src/pages/builder/app/[application]/_components/BuilderSidePanel.svelte +++ b/packages/builder/src/pages/builder/app/[application]/_components/BuilderSidePanel.svelte @@ -442,13 +442,11 @@ const onUpdateUserInvite = async (invite, role) => { let updateBody = { - code: invite.code, apps: { ...invite.apps, [prodAppId]: role, }, } - if (role === Constants.Roles.CREATOR) { updateBody.builder = updateBody.builder || {} updateBody.builder.apps = [...(updateBody.builder.apps ?? []), prodAppId] @@ -456,7 +454,7 @@ } else if (role !== Constants.Roles.CREATOR && invite?.builder?.apps) { invite.builder.apps = [] } - await users.updateInvite(updateBody) + await users.updateInvite(invite.code, updateBody) await filterInvites(query) } @@ -470,8 +468,7 @@ let updated = { ...invite } delete updated.info.apps[prodAppId] - return await users.updateInvite({ - code: updated.code, + return await users.updateInvite(updated.code, { apps: updated.apps, }) } diff --git a/packages/builder/src/pages/builder/portal/apps/index.svelte b/packages/builder/src/pages/builder/portal/apps/index.svelte index f94bad2147..bcd59cd948 100644 --- a/packages/builder/src/pages/builder/portal/apps/index.svelte +++ b/packages/builder/src/pages/builder/portal/apps/index.svelte @@ -191,8 +191,14 @@ ? "View errors" : "View error"} on:dismiss={async () => { - await automationStore.actions.clearLogErrors({ appId }) - await appsStore.load() + const automationId = Object.keys(automationErrors[appId] || {})[0] + if (automationId) { + await automationStore.actions.clearLogErrors({ + appId, + automationId, + }) + await appsStore.load() + } }} message={automationErrorMessage(appId)} /> diff --git a/packages/builder/src/pages/builder/portal/users/groups/_components/GroupUsers.svelte b/packages/builder/src/pages/builder/portal/users/groups/_components/GroupUsers.svelte index 8d99d406fd..71fd4c0be3 100644 --- a/packages/builder/src/pages/builder/portal/users/groups/_components/GroupUsers.svelte +++ b/packages/builder/src/pages/builder/portal/users/groups/_components/GroupUsers.svelte @@ -52,7 +52,7 @@ ] const removeUser = async id => { - await groups.actions.removeUser(groupId, id) + await groups.removeUser(groupId, id) fetchGroupUsers.refresh() } diff --git a/packages/builder/src/pages/builder/portal/users/users/index.svelte b/packages/builder/src/pages/builder/portal/users/users/index.svelte index 97120c55d4..c77e40c964 100644 --- a/packages/builder/src/pages/builder/portal/users/users/index.svelte +++ b/packages/builder/src/pages/builder/portal/users/users/index.svelte @@ -251,6 +251,7 @@ passwordModal.show() await fetch.refresh() } catch (error) { + console.error(error) notifications.error("Error creating user") } } diff --git a/packages/builder/src/stores/builder/queries.js b/packages/builder/src/stores/builder/queries.js deleted file mode 100644 index 7aeb9ff8fd..0000000000 --- a/packages/builder/src/stores/builder/queries.js +++ /dev/null @@ -1,130 +0,0 @@ -import { writable, get, derived } from "svelte/store" -import { datasources } from "./datasources" -import { integrations } from "./integrations" -import { API } from "@/api" -import { duplicateName } from "@/helpers/duplicate" - -const sortQueries = queryList => { - queryList.sort((q1, q2) => { - return q1.name.localeCompare(q2.name) - }) -} - -export function createQueriesStore() { - const store = writable({ - list: [], - selectedQueryId: null, - }) - const derivedStore = derived(store, $store => ({ - ...$store, - selected: $store.list?.find(q => q._id === $store.selectedQueryId), - })) - - const fetch = async () => { - const queries = await API.getQueries() - sortQueries(queries) - store.update(state => ({ - ...state, - list: queries, - })) - } - - const save = async (datasourceId, query) => { - const _integrations = get(integrations) - const dataSource = get(datasources).list.filter( - ds => ds._id === datasourceId - ) - // Check if readable attribute is found - if (dataSource.length !== 0) { - const integration = _integrations[dataSource[0].source] - const readable = integration.query[query.queryVerb].readable - if (readable) { - query.readable = readable - } - } - query.datasourceId = datasourceId - const savedQuery = await API.saveQuery(query) - store.update(state => { - const idx = state.list.findIndex(query => query._id === savedQuery._id) - const queries = state.list - if (idx >= 0) { - queries.splice(idx, 1, savedQuery) - } else { - queries.push(savedQuery) - } - sortQueries(queries) - return { - list: queries, - selectedQueryId: savedQuery._id, - } - }) - return savedQuery - } - - const importQueries = async ({ data, datasourceId }) => { - return await API.importQueries(datasourceId, data) - } - - const select = id => { - store.update(state => ({ - ...state, - selectedQueryId: id, - })) - } - - const preview = async query => { - const result = await API.previewQuery(query) - // Assume all the fields are strings and create a basic schema from the - // unique fields returned by the server - const schema = {} - for (let [field, metadata] of Object.entries(result.schema)) { - schema[field] = metadata || { type: "string" } - } - return { ...result, schema, rows: result.rows || [] } - } - - const deleteQuery = async query => { - await API.deleteQuery(query._id, query._rev) - store.update(state => { - state.list = state.list.filter(existing => existing._id !== query._id) - return state - }) - } - - const duplicate = async query => { - let list = get(store).list - const newQuery = { ...query } - const datasourceId = query.datasourceId - - delete newQuery._id - delete newQuery._rev - newQuery.name = duplicateName( - query.name, - list.map(q => q.name) - ) - - return await save(datasourceId, newQuery) - } - - const removeDatasourceQueries = datasourceId => { - store.update(state => ({ - ...state, - list: state.list.filter(table => table.datasourceId !== datasourceId), - })) - } - - return { - subscribe: derivedStore.subscribe, - fetch, - init: fetch, - select, - save, - import: importQueries, - delete: deleteQuery, - preview, - duplicate, - removeDatasourceQueries, - } -} - -export const queries = createQueriesStore() diff --git a/packages/builder/src/stores/builder/queries.ts b/packages/builder/src/stores/builder/queries.ts new file mode 100644 index 0000000000..c6511dc346 --- /dev/null +++ b/packages/builder/src/stores/builder/queries.ts @@ -0,0 +1,156 @@ +import { derived, get, Writable } from "svelte/store" +import { datasources } from "./datasources" +import { integrations } from "./integrations" +import { API } from "@/api" +import { duplicateName } from "@/helpers/duplicate" +import { DerivedBudiStore } from "@/stores/BudiStore" +import { + Query, + QueryPreview, + PreviewQueryResponse, + SaveQueryRequest, + ImportRestQueryRequest, + QuerySchema, +} from "@budibase/types" + +const sortQueries = (queryList: Query[]) => { + queryList.sort((q1, q2) => { + return q1.name.localeCompare(q2.name) + }) +} + +interface BuilderQueryStore { + list: Query[] + selectedQueryId: string | null +} + +interface DerivedQueryStore extends BuilderQueryStore { + selected?: Query +} + +export class QueryStore extends DerivedBudiStore< + BuilderQueryStore, + DerivedQueryStore +> { + constructor() { + const makeDerivedStore = (store: Writable) => { + return derived(store, ($store): DerivedQueryStore => { + return { + list: $store.list, + selectedQueryId: $store.selectedQueryId, + selected: $store.list?.find(q => q._id === $store.selectedQueryId), + } + }) + } + + super( + { + list: [], + selectedQueryId: null, + }, + makeDerivedStore + ) + + this.select = this.select.bind(this) + } + + async fetch() { + const queries = await API.getQueries() + sortQueries(queries) + this.store.update(state => ({ + ...state, + list: queries, + })) + } + + async save(datasourceId: string, query: SaveQueryRequest) { + const _integrations = get(integrations) + const dataSource = get(datasources).list.filter( + ds => ds._id === datasourceId + ) + // Check if readable attribute is found + if (dataSource.length !== 0) { + const integration = _integrations[dataSource[0].source] + const readable = integration.query[query.queryVerb].readable + if (readable) { + query.readable = readable + } + } + query.datasourceId = datasourceId + const savedQuery = await API.saveQuery(query) + this.store.update(state => { + const idx = state.list.findIndex(query => query._id === savedQuery._id) + const queries = state.list + if (idx >= 0) { + queries.splice(idx, 1, savedQuery) + } else { + queries.push(savedQuery) + } + sortQueries(queries) + return { + list: queries, + selectedQueryId: savedQuery._id || null, + } + }) + return savedQuery + } + + async importQueries(data: ImportRestQueryRequest) { + return await API.importQueries(data) + } + + select(id: string | null) { + this.store.update(state => ({ + ...state, + selectedQueryId: id, + })) + } + + async preview(query: QueryPreview): Promise { + const result = await API.previewQuery(query) + // Assume all the fields are strings and create a basic schema from the + // unique fields returned by the server + const schema: Record = {} + for (let [field, metadata] of Object.entries(result.schema)) { + schema[field] = (metadata as QuerySchema) || { type: "string" } + } + return { ...result, schema, rows: result.rows || [] } + } + + async delete(query: Query) { + if (!query._id || !query._rev) { + throw new Error("Query ID or Revision is missing") + } + await API.deleteQuery(query._id, query._rev) + this.store.update(state => ({ + ...state, + list: state.list.filter(existing => existing._id !== query._id), + })) + } + + async duplicate(query: Query) { + let list = get(this.store).list + const newQuery = { ...query } + const datasourceId = query.datasourceId + + delete newQuery._id + delete newQuery._rev + newQuery.name = duplicateName( + query.name, + list.map(q => q.name) + ) + + return await this.save(datasourceId, newQuery) + } + + removeDatasourceQueries(datasourceId: string) { + this.store.update(state => ({ + ...state, + list: state.list.filter(table => table.datasourceId !== datasourceId), + })) + } + + init = this.fetch +} + +export const queries = new QueryStore() diff --git a/packages/builder/src/stores/builder/rowActions.ts b/packages/builder/src/stores/builder/rowActions.ts index 9576eccd1b..2b3077926e 100644 --- a/packages/builder/src/stores/builder/rowActions.ts +++ b/packages/builder/src/stores/builder/rowActions.ts @@ -62,7 +62,7 @@ export class RowActionStore extends BudiStore { const existingRowActions = get(this)[tableId] || [] name = getSequentialName(existingRowActions, "New row action ", { getName: x => x.name, - }) + })! } if (!name) { diff --git a/packages/builder/src/stores/portal/menu.ts b/packages/builder/src/stores/portal/menu.ts index 3b1ece9156..5cd619d4a9 100644 --- a/packages/builder/src/stores/portal/menu.ts +++ b/packages/builder/src/stores/portal/menu.ts @@ -1,9 +1,7 @@ import { derived, Readable } from "svelte/store" import { admin } from "./admin" import { auth } from "./auth" -import { isEnabled } from "@/helpers/featureFlags" import { sdk } from "@budibase/shared-core" -import { FeatureFlag } from "@budibase/types" interface MenuItem { title: string @@ -73,13 +71,11 @@ export const menu: Readable = derived( title: "Environment", href: "/builder/portal/settings/environment", }, - ] - if (isEnabled(FeatureFlag.AI_CUSTOM_CONFIGS)) { - settingsSubPages.push({ + { title: "AI", href: "/builder/portal/settings/ai", - }) - } + }, + ] if (!cloud) { settingsSubPages.push({ diff --git a/packages/builder/src/stores/portal/users.js b/packages/builder/src/stores/portal/users.ts similarity index 52% rename from packages/builder/src/stores/portal/users.js rename to packages/builder/src/stores/portal/users.ts index 99ead22317..605f8612aa 100644 --- a/packages/builder/src/stores/portal/users.js +++ b/packages/builder/src/stores/portal/users.ts @@ -1,41 +1,71 @@ -import { writable } from "svelte/store" import { API } from "@/api" -import { update } from "lodash" import { licensing } from "." import { sdk } from "@budibase/shared-core" import { Constants } from "@budibase/frontend-core" +import { + DeleteInviteUsersRequest, + InviteUsersRequest, + SearchUsersRequest, + SearchUsersResponse, + UpdateInviteRequest, + User, + UserIdentifier, + UnsavedUser, +} from "@budibase/types" +import { BudiStore } from "../BudiStore" -export function createUsersStore() { - const { subscribe, set } = writable({}) +interface UserInfo { + email: string + password: string + forceResetPassword?: boolean + role: keyof typeof Constants.BudibaseRoles +} - // opts can contain page and search params - async function search(opts = {}) { +type UserState = SearchUsersResponse & SearchUsersRequest + +class UserStore extends BudiStore { + constructor() { + super({ + data: [], + }) + } + + async search(opts: SearchUsersRequest = {}) { const paged = await API.searchUsers(opts) - set({ + this.set({ ...paged, ...opts, }) return paged } - async function get(userId) { + async get(userId: string) { try { return await API.getUser(userId) } catch (err) { return null } } - const fetch = async () => { + + async fetch() { return await API.getUsers() } - // One or more users. - async function onboard(payload) { + async onboard(payload: InviteUsersRequest) { return await API.onboardUsers(payload) } - async function invite(payload) { - const users = payload.map(user => { + async invite( + payload: { + admin?: boolean + builder?: boolean + creator?: boolean + email: string + apps?: any[] + groups?: any[] + }[] + ) { + const users: InviteUsersRequest = payload.map(user => { let builder = undefined if (user.admin || user.builder) { builder = { global: true } @@ -55,11 +85,16 @@ export function createUsersStore() { return API.inviteUsers(users) } - async function removeInvites(payload) { + async removeInvites(payload: DeleteInviteUsersRequest) { return API.removeUserInvites(payload) } - async function acceptInvite(inviteCode, password, firstName, lastName) { + async acceptInvite( + inviteCode: string, + password: string, + firstName: string, + lastName?: string + ) { return API.acceptInvite({ inviteCode, password, @@ -68,21 +103,25 @@ export function createUsersStore() { }) } - async function fetchInvite(inviteCode) { + async fetchInvite(inviteCode: string) { return API.getUserInvite(inviteCode) } - async function getInvites() { + async getInvites() { return API.getUserInvites() } - async function updateInvite(invite) { - return API.updateUserInvite(invite.code, invite) + async updateInvite(code: string, invite: UpdateInviteRequest) { + return API.updateUserInvite(code, invite) } - async function create(data) { - let mappedUsers = data.users.map(user => { - const body = { + async getUserCountByApp(appId: string) { + return await API.getUserCountByApp(appId) + } + + async create(data: { users: UserInfo[]; groups: any[] }) { + let mappedUsers: UnsavedUser[] = data.users.map((user: any) => { + const body: UnsavedUser = { email: user.email, password: user.password, roles: {}, @@ -92,17 +131,17 @@ export function createUsersStore() { } switch (user.role) { - case "appUser": + case Constants.BudibaseRoles.AppUser: body.builder = { global: false } body.admin = { global: false } break - case "developer": + case Constants.BudibaseRoles.Developer: body.builder = { global: true } break - case "creator": + case Constants.BudibaseRoles.Creator: body.builder = { creator: true, global: false } break - case "admin": + case Constants.BudibaseRoles.Admin: body.admin = { global: true } body.builder = { global: true } break @@ -111,43 +150,47 @@ export function createUsersStore() { return body }) const response = await API.createUsers(mappedUsers, data.groups) + licensing.setQuotaUsage() // re-search from first page - await search() + await this.search() return response } - async function del(id) { + async delete(id: string) { await API.deleteUser(id) - update(users => users.filter(user => user._id !== id)) + licensing.setQuotaUsage() } - async function getUserCountByApp(appId) { - return await API.getUserCountByApp(appId) + async bulkDelete(users: UserIdentifier[]) { + const res = API.deleteUsers(users) + licensing.setQuotaUsage() + return res } - async function bulkDelete(users) { - return API.deleteUsers(users) + async save(user: User) { + const res = await API.saveUser(user) + licensing.setQuotaUsage() + return res } - async function save(user) { - return await API.saveUser(user) - } - - async function addAppBuilder(userId, appId) { + async addAppBuilder(userId: string, appId: string) { return await API.addAppBuilder(userId, appId) } - async function removeAppBuilder(userId, appId) { + async removeAppBuilder(userId: string, appId: string) { return await API.removeAppBuilder(userId, appId) } - async function getAccountHolder() { + async getAccountHolder() { return await API.getAccountHolder() } - const getUserRole = user => { - if (user && user.email === user.tenantOwnerEmail) { + getUserRole(user?: User & { tenantOwnerEmail?: string }) { + if (!user) { + return Constants.BudibaseRoles.AppUser + } + if (user.email === user.tenantOwnerEmail) { return Constants.BudibaseRoles.Owner } else if (sdk.users.isAdmin(user)) { return Constants.BudibaseRoles.Admin @@ -159,38 +202,6 @@ export function createUsersStore() { return Constants.BudibaseRoles.AppUser } } - - const refreshUsage = - fn => - async (...args) => { - const response = await fn(...args) - await licensing.setQuotaUsage() - return response - } - - return { - subscribe, - search, - get, - getUserRole, - fetch, - invite, - onboard, - fetchInvite, - getInvites, - removeInvites, - updateInvite, - getUserCountByApp, - addAppBuilder, - removeAppBuilder, - // any operation that adds or deletes users - acceptInvite, - create: refreshUsage(create), - save: refreshUsage(save), - bulkDelete: refreshUsage(bulkDelete), - delete: refreshUsage(del), - getAccountHolder, - } } -export const users = createUsersStore() +export const users = new UserStore() diff --git a/packages/client/manifest.json b/packages/client/manifest.json index 6dd1b71b75..c236dd1ad9 100644 --- a/packages/client/manifest.json +++ b/packages/client/manifest.json @@ -5139,7 +5139,8 @@ { "type": "text", "label": "File name", - "key": "key" + "key": "key", + "nested": true }, { "type": "event", diff --git a/packages/client/src/api/api.js b/packages/client/src/api/api.ts similarity index 94% rename from packages/client/src/api/api.js rename to packages/client/src/api/api.ts index d4c8faa4d2..b944f7bd7c 100644 --- a/packages/client/src/api/api.js +++ b/packages/client/src/api/api.ts @@ -1,6 +1,10 @@ import { createAPIClient } from "@budibase/frontend-core" -import { authStore } from "../stores/auth.js" -import { notificationStore, devToolsEnabled, devToolsStore } from "../stores/" +import { authStore } from "../stores/auth" +import { + notificationStore, + devToolsEnabled, + devToolsStore, +} from "../stores/index" import { get } from "svelte/store" export const API = createAPIClient({ diff --git a/packages/client/src/api/index.js b/packages/client/src/api/index.ts similarity index 73% rename from packages/client/src/api/index.js rename to packages/client/src/api/index.ts index 5eb6b2b6f4..3c53045cfd 100644 --- a/packages/client/src/api/index.js +++ b/packages/client/src/api/index.ts @@ -1,5 +1,5 @@ -import { API } from "./api.js" -import { patchAPI } from "./patches.js" +import { API } from "./api" +import { patchAPI } from "./patches" // Certain endpoints which return rows need patched so that they transform // and enrich the row docs, so that they can be correctly handled by the diff --git a/packages/client/src/api/patches.js b/packages/client/src/api/patches.ts similarity index 88% rename from packages/client/src/api/patches.js rename to packages/client/src/api/patches.ts index 722167e16d..9482644fa7 100644 --- a/packages/client/src/api/patches.js +++ b/packages/client/src/api/patches.ts @@ -1,19 +1,20 @@ -import { Constants } from "@budibase/frontend-core" +import { Constants, APIClient } from "@budibase/frontend-core" import { FieldTypes } from "../constants" +import { Row, Table } from "@budibase/types" -export const patchAPI = API => { +export const patchAPI = (API: APIClient) => { /** * Enriches rows which contain certain field types so that they can * be properly displayed. * The ability to create these bindings has been removed, but they will still * exist in client apps to support backwards compatibility. */ - const enrichRows = async (rows, tableId) => { + const enrichRows = async (rows: Row[], tableId: string) => { if (!Array.isArray(rows)) { return [] } if (rows.length) { - const tables = {} + const tables: Record = {} for (let row of rows) { // Fall back to passed in tableId if row doesn't have it specified let rowTableId = row.tableId || tableId @@ -54,7 +55,7 @@ export const patchAPI = API => { const fetchSelf = API.fetchSelf API.fetchSelf = async () => { const user = await fetchSelf() - if (user && user._id) { + if (user && "_id" in user && user._id) { if (user.roleId === "PUBLIC") { // Don't try to enrich a public user as it will 403 return user @@ -90,13 +91,14 @@ export const patchAPI = API => { return await enrichRows(rows, tableId) } - // Wipe any HBS formulae from table definitions, as these interfere with + // Wipe any HBS formulas from table definitions, as these interfere with // handlebars enrichment const fetchTableDefinition = API.fetchTableDefinition API.fetchTableDefinition = async tableId => { const definition = await fetchTableDefinition(tableId) Object.keys(definition?.schema || {}).forEach(field => { if (definition.schema[field]?.type === "formula") { + // @ts-expect-error TODO check what use case removing that would break delete definition.schema[field].formula } }) diff --git a/packages/client/src/components/Block.svelte b/packages/client/src/components/Block.svelte index a739065015..48c11f152a 100644 --- a/packages/client/src/components/Block.svelte +++ b/packages/client/src/components/Block.svelte @@ -1,7 +1,7 @@