diff --git a/packages/backend-core/src/index.ts b/packages/backend-core/src/index.ts index 7bf26f3688..8001017092 100644 --- a/packages/backend-core/src/index.ts +++ b/packages/backend-core/src/index.ts @@ -2,6 +2,7 @@ export * as configs from "./configs" export * as events from "./events" export * as migrations from "./migrations" export * as users from "./users" +export * as userUtils from "./users/utils" export * as roles from "./security/roles" export * as permissions from "./security/permissions" export * as accounts from "./accounts" diff --git a/packages/backend-core/src/users/db.ts b/packages/backend-core/src/users/db.ts index 4d0d216603..136cb4b8ad 100644 --- a/packages/backend-core/src/users/db.ts +++ b/packages/backend-core/src/users/db.ts @@ -251,7 +251,8 @@ export class UserDB { } const change = dbUser ? 0 : 1 // no change if there is existing user - const creatorsChange = isCreator(dbUser) !== isCreator(user) ? 1 : 0 + const creatorsChange = + (await isCreator(dbUser)) !== (await isCreator(user)) ? 1 : 0 return UserDB.quotas.addUsers(change, creatorsChange, async () => { await validateUniqueUser(email, tenantId) @@ -335,7 +336,7 @@ export class UserDB { } newUser.userGroups = groups || [] newUsers.push(newUser) - if (isCreator(newUser)) { + if (await isCreator(newUser)) { newCreators.push(newUser) } } @@ -432,12 +433,16 @@ export class UserDB { _deleted: true, })) const dbResponse = await usersCore.bulkUpdateGlobalUsers(toDelete) - const creatorsToDelete = usersToDelete.filter(isCreator) + + const creatorsEval = await Promise.all(usersToDelete.map(isCreator)) + const creatorsToDeleteCount = creatorsEval.filter( + creator => !!creator + ).length for (let user of usersToDelete) { await bulkDeleteProcessing(user) } - await UserDB.quotas.removeUsers(toDelete.length, creatorsToDelete.length) + await UserDB.quotas.removeUsers(toDelete.length, creatorsToDeleteCount) // Build Response // index users by id @@ -486,7 +491,7 @@ export class UserDB { await db.remove(userId, dbUser._rev) - const creatorsToDelete = isCreator(dbUser) ? 1 : 0 + const creatorsToDelete = (await isCreator(dbUser)) ? 1 : 0 await UserDB.quotas.removeUsers(1, creatorsToDelete) await eventHelpers.handleDeleteEvents(dbUser) await cache.user.invalidateUser(userId) diff --git a/packages/backend-core/src/users/test/utils.spec.ts b/packages/backend-core/src/users/test/utils.spec.ts new file mode 100644 index 0000000000..0fe27f57a6 --- /dev/null +++ b/packages/backend-core/src/users/test/utils.spec.ts @@ -0,0 +1,67 @@ +import { User, UserGroup } from "@budibase/types" +import { generator, structures } from "../../../tests" +import { DBTestConfiguration } from "../../../tests/extra" +import { getGlobalDB } from "../../context" +import { isCreator } from "../utils" + +const config = new DBTestConfiguration() + +describe("Users", () => { + it("User is a creator if it is configured as a global builder", async () => { + const user: User = structures.users.user({ builder: { global: true } }) + expect(await isCreator(user)).toBe(true) + }) + + it("User is a creator if it is configured as a global admin", async () => { + const user: User = structures.users.user({ admin: { global: true } }) + expect(await isCreator(user)).toBe(true) + }) + + it("User is a creator if it is configured with creator permission", async () => { + const user: User = structures.users.user({ builder: { creator: true } }) + expect(await isCreator(user)).toBe(true) + }) + + it("User is a creator if it is a builder in some application", async () => { + const user: User = structures.users.user({ builder: { apps: ["app1"] } }) + expect(await isCreator(user)).toBe(true) + }) + + it("User is a creator if it has CREATOR permission in some application", async () => { + const user: User = structures.users.user({ roles: { app1: "CREATOR" } }) + expect(await isCreator(user)).toBe(true) + }) + + it("User is a creator if it has ADMIN permission in some application", async () => { + const user: User = structures.users.user({ roles: { app1: "ADMIN" } }) + expect(await isCreator(user)).toBe(true) + }) + + it("User is a creator if it remains to a group with ADMIN permissions", async () => { + const usersInGroup = 10 + const groupId = "gr_17abffe89e0b40268e755b952f101a59" + const group: UserGroup = { + ...structures.userGroups.userGroup(), + ...{ _id: groupId, roles: { app1: "ADMIN" } }, + } + const users: User[] = [] + for (const _ of Array.from({ length: usersInGroup })) { + const userId = `us_${generator.guid()}` + const user: User = structures.users.user({ + _id: userId, + userGroups: [groupId], + }) + users.push(user) + } + + await config.doInTenant(async () => { + const db = getGlobalDB() + await db.put(group) + for (let user of users) { + await db.put(user) + const creator = await isCreator(user) + expect(creator).toBe(true) + } + }) + }) +}) diff --git a/packages/backend-core/src/users/users.ts b/packages/backend-core/src/users/users.ts index cc2b4fc27f..638da4a5b1 100644 --- a/packages/backend-core/src/users/users.ts +++ b/packages/backend-core/src/users/users.ts @@ -309,7 +309,8 @@ export async function getCreatorCount() { let creators = 0 async function iterate(startPage?: string) { const page = await paginatedUsers({ bookmark: startPage }) - creators += page.data.filter(isCreator).length + const creatorsEval = await Promise.all(page.data.map(isCreator)) + creators += creatorsEval.filter(creator => !!creator).length if (page.hasNextPage) { await iterate(page.nextPage) } diff --git a/packages/backend-core/src/users/utils.ts b/packages/backend-core/src/users/utils.ts index 0ef4b77998..348ad1532f 100644 --- a/packages/backend-core/src/users/utils.ts +++ b/packages/backend-core/src/users/utils.ts @@ -1,4 +1,4 @@ -import { CloudAccount } from "@budibase/types" +import { CloudAccount, ContextUser, User, UserGroup } from "@budibase/types" import * as accountSdk from "../accounts" import env from "../environment" import { getPlatformUser } from "./lookup" @@ -6,17 +6,48 @@ import { EmailUnavailableError } from "../errors" import { getTenantId } from "../context" import { sdk } from "@budibase/shared-core" import { getAccountByTenantId } from "../accounts" +import { BUILTIN_ROLE_IDS } from "../security/roles" +import * as context from "../context" // extract from shared-core to make easily accessible from backend-core export const isBuilder = sdk.users.isBuilder export const isAdmin = sdk.users.isAdmin -export const isCreator = sdk.users.isCreator export const isGlobalBuilder = sdk.users.isGlobalBuilder export const isAdminOrBuilder = sdk.users.isAdminOrBuilder export const hasAdminPermissions = sdk.users.hasAdminPermissions export const hasBuilderPermissions = sdk.users.hasBuilderPermissions export const hasAppBuilderPermissions = sdk.users.hasAppBuilderPermissions +export async function isCreator(user?: User | ContextUser) { + const isCreatorByUserDefinition = sdk.users.isCreator(user) + if (!isCreatorByUserDefinition && user) { + return await isCreatorByGroupMembership(user) + } + return isCreatorByUserDefinition +} + +async function isCreatorByGroupMembership(user?: User | ContextUser) { + const userGroups = user?.userGroups || [] + if (userGroups.length > 0) { + const db = context.getGlobalDB() + const groups: UserGroup[] = [] + for (let groupId of userGroups) { + try { + const group = await db.get(groupId) + groups.push(group) + } catch (e: any) { + if (e.error !== "not_found") { + throw e + } + } + } + return groups.some(group => + Object.values(group.roles || {}).includes(BUILTIN_ROLE_IDS.ADMIN) + ) + } + return false +} + export async function validateUniqueUser(email: string, tenantId: string) { // check budibase users in other tenants if (env.MULTI_TENANCY) { diff --git a/packages/bbui/src/Actions/position_dropdown.js b/packages/bbui/src/Actions/position_dropdown.js index f2018272f6..cc169eac09 100644 --- a/packages/bbui/src/Actions/position_dropdown.js +++ b/packages/bbui/src/Actions/position_dropdown.js @@ -18,7 +18,6 @@ export default function positionDropdown(element, opts) { useAnchorWidth, offset = 5, customUpdate, - offsetBelow, } = opts if (!anchor) { return @@ -48,7 +47,7 @@ export default function positionDropdown(element, opts) { styles.top = anchorBounds.top - elementBounds.height - offset styles.maxHeight = maxHeight || 240 } else { - styles.top = anchorBounds.bottom + (offsetBelow || offset) + styles.top = anchorBounds.bottom + offset styles.maxHeight = maxHeight || window.innerHeight - anchorBounds.bottom - 20 } diff --git a/packages/bbui/src/Form/Core/Multiselect.svelte b/packages/bbui/src/Form/Core/Multiselect.svelte index d5d6515d2d..2243570cd5 100644 --- a/packages/bbui/src/Form/Core/Multiselect.svelte +++ b/packages/bbui/src/Form/Core/Multiselect.svelte @@ -15,8 +15,6 @@ export let autoWidth = false export let searchTerm = null export let customPopoverHeight - export let customPopoverOffsetBelow - export let customPopoverMaxHeight export let open = false export let loading @@ -98,7 +96,5 @@ {sort} {autoWidth} {customPopoverHeight} - {customPopoverOffsetBelow} - {customPopoverMaxHeight} {loading} /> diff --git a/packages/bbui/src/Form/Core/Picker.svelte b/packages/bbui/src/Form/Core/Picker.svelte index 94fbe73cf2..cfb1654403 100644 --- a/packages/bbui/src/Form/Core/Picker.svelte +++ b/packages/bbui/src/Form/Core/Picker.svelte @@ -37,8 +37,6 @@ export let sort = false export let searchTerm = null export let customPopoverHeight - export let customPopoverOffsetBelow - export let customPopoverMaxHeight export let align = "left" export let footer = null export let customAnchor = null @@ -156,9 +154,7 @@ on:close={() => (open = false)} useAnchorWidth={!autoWidth} maxWidth={autoWidth ? 400 : null} - maxHeight={customPopoverMaxHeight} customHeight={customPopoverHeight} - offsetBelow={customPopoverOffsetBelow} >
null export let getOptionColour = () => null export let getOptionSubtitle = () => null + export let compare = null export let useOptionIconImage = false export let isOptionEnabled export let readonly = false @@ -23,8 +24,6 @@ export let footer = null export let open = false export let tag = null - export let customPopoverOffsetBelow - export let customPopoverMaxHeight export let searchTerm = null export let loading @@ -34,13 +33,19 @@ $: fieldIcon = getFieldAttribute(getOptionIcon, value, options) $: fieldColour = getFieldAttribute(getOptionColour, value, options) + function compareOptionAndValue(option, value) { + return typeof compare === "function" + ? compare(option, value) + : option === value + } + const getFieldAttribute = (getAttribute, value, options) => { // Wait for options to load if there is a value but no options if (!options?.length) { return "" } - const index = options.findIndex( - (option, idx) => getOptionValue(option, idx) === value + const index = options.findIndex((option, idx) => + compareOptionAndValue(getOptionValue(option, idx), value) ) return index !== -1 ? getAttribute(options[index], index) : null } @@ -90,11 +95,9 @@ {autocomplete} {sort} {tag} - {customPopoverOffsetBelow} - {customPopoverMaxHeight} isPlaceholder={value == null || value === ""} placeholderOption={placeholder === false ? null : placeholder} - isOptionSelected={option => option === value} + isOptionSelected={option => compareOptionAndValue(option, value)} onSelectOption={selectOption} {loading} /> diff --git a/packages/bbui/src/Form/Select.svelte b/packages/bbui/src/Form/Select.svelte index 8235b68faf..2119a37980 100644 --- a/packages/bbui/src/Form/Select.svelte +++ b/packages/bbui/src/Form/Select.svelte @@ -28,6 +28,7 @@ export let footer = null export let tag = null export let helpText = null + export let compare const dispatch = createEventDispatcher() const onChange = e => { value = e.detail @@ -65,6 +66,7 @@ {autocomplete} {customPopoverHeight} {tag} + {compare} on:change={onChange} on:click /> diff --git a/packages/bbui/src/Popover/Popover.svelte b/packages/bbui/src/Popover/Popover.svelte index a68430e973..5066e3aa05 100644 --- a/packages/bbui/src/Popover/Popover.svelte +++ b/packages/bbui/src/Popover/Popover.svelte @@ -18,7 +18,6 @@ export let useAnchorWidth = false export let dismissible = true export let offset = 5 - export let offsetBelow export let customHeight export let animate = true export let customZindex @@ -89,7 +88,6 @@ maxWidth, useAnchorWidth, offset, - offsetBelow, customUpdate: handlePostionUpdate, }} use:clickOutside={{ diff --git a/packages/builder/src/builderStore/index.js b/packages/builder/src/builderStore/index.js index dd54dcf13e..b58d196024 100644 --- a/packages/builder/src/builderStore/index.js +++ b/packages/builder/src/builderStore/index.js @@ -9,6 +9,7 @@ import { findComponent, findComponentPath } from "./componentUtils" import { RoleUtils } from "@budibase/frontend-core" import { createHistoryStore } from "builderStore/store/history" import { cloneDeep } from "lodash/fp" +import { getHoverStore } from "./store/hover" export const store = getFrontendStore() export const automationStore = getAutomationStore() @@ -16,6 +17,7 @@ export const themeStore = getThemeStore() export const temporalStore = getTemporalStore() export const userStore = getUserStore() export const deploymentStore = getDeploymentStore() +export const hoverStore = getHoverStore() // Setup history for screens export const screenHistoryStore = createHistoryStore({ diff --git a/packages/builder/src/builderStore/store/frontend.js b/packages/builder/src/builderStore/store/frontend.js index b05b127b1c..ff7c0d74b8 100644 --- a/packages/builder/src/builderStore/store/frontend.js +++ b/packages/builder/src/builderStore/store/frontend.js @@ -92,9 +92,6 @@ const INITIAL_FRONTEND_STATE = { // Onboarding onboarding: false, tourNodes: null, - - // UI state - hoveredComponentId: null, } export const getFrontendStore = () => { @@ -1415,18 +1412,6 @@ export const getFrontendStore = () => { return state }) }, - hover: (componentId, notifyClient = true) => { - if (componentId === get(store).hoveredComponentId) { - return - } - store.update(state => { - state.hoveredComponentId = componentId - return state - }) - if (notifyClient) { - store.actions.preview.sendEvent("hover-component", componentId) - } - }, }, links: { save: async (url, title) => { diff --git a/packages/builder/src/builderStore/store/hover.js b/packages/builder/src/builderStore/store/hover.js new file mode 100644 index 0000000000..5db9272975 --- /dev/null +++ b/packages/builder/src/builderStore/store/hover.js @@ -0,0 +1,27 @@ +import { get, writable } from "svelte/store" +import { store as builder } from "builderStore" + +export const getHoverStore = () => { + const initialValue = { + componentId: null, + } + + const store = writable(initialValue) + + const update = (componentId, notifyClient = true) => { + if (componentId === get(store).componentId) { + return + } + store.update(state => { + state.componentId = componentId + return state + }) + if (notifyClient) { + builder.actions.preview.sendEvent("hover-component", componentId) + } + } + return { + subscribe: store.subscribe, + actions: { update }, + } +} diff --git a/packages/builder/src/components/design/settings/controls/ButtonConfiguration/ButtonConfiguration.svelte b/packages/builder/src/components/design/settings/controls/ButtonConfiguration/ButtonConfiguration.svelte index 63bfecf386..fade2db761 100644 --- a/packages/builder/src/components/design/settings/controls/ButtonConfiguration/ButtonConfiguration.svelte +++ b/packages/builder/src/components/design/settings/controls/ButtonConfiguration/ButtonConfiguration.svelte @@ -5,6 +5,7 @@ import { store } from "builderStore" import { Helpers } from "@budibase/bbui" import { getEventContextBindings } from "builderStore/dataBinding" + import { cloneDeep, isEqual } from "lodash/fp" export let componentInstance export let componentBindings @@ -17,8 +18,13 @@ const dispatch = createEventDispatcher() let focusItem + let cachedValue - $: buttonList = sanitizeValue(value) || [] + $: if (!isEqual(value, cachedValue)) { + cachedValue = cloneDeep(value) + } + + $: buttonList = sanitizeValue(cachedValue) || [] $: buttonCount = buttonList.length $: eventContextBindings = getEventContextBindings({ componentInstance, diff --git a/packages/builder/src/components/integration/KeyValueBuilder.svelte b/packages/builder/src/components/integration/KeyValueBuilder.svelte index 096d5c0f71..8dac07bcec 100644 --- a/packages/builder/src/components/integration/KeyValueBuilder.svelte +++ b/packages/builder/src/components/integration/KeyValueBuilder.svelte @@ -35,6 +35,7 @@ export let bindingDrawerLeft export let allowHelpers = true export let customButtonText = null + export let compare = (option, value) => option === value let fields = Object.entries(object || {}).map(([name, value]) => ({ name, @@ -112,7 +113,12 @@ on:blur={changed} /> {#if options} - {:else if bindings && bindings.length} import KeyValueBuilder from "../KeyValueBuilder.svelte" - import { SchemaTypeOptions } from "constants/backend" + import { SchemaTypeOptionsExpanded } from "constants/backend" export let schema export let onSchemaChange = () => {} @@ -24,6 +24,7 @@ object={schema} name="field" headings - options={SchemaTypeOptions} + options={SchemaTypeOptionsExpanded} + compare={(option, value) => option.type === value.type} /> {/key} diff --git a/packages/builder/src/components/integration/RestQueryViewer.svelte b/packages/builder/src/components/integration/RestQueryViewer.svelte index 64834c1b0b..d6a8fe6fc3 100644 --- a/packages/builder/src/components/integration/RestQueryViewer.svelte +++ b/packages/builder/src/components/integration/RestQueryViewer.svelte @@ -33,7 +33,7 @@ PaginationTypes, RawRestBodyTypes, RestBodyTypes as bodyTypes, - SchemaTypeOptions, + SchemaTypeOptionsExpanded, } from "constants/backend" import JSONPreview from "components/integration/JSONPreview.svelte" import AccessLevelSelect from "components/integration/AccessLevelSelect.svelte" @@ -97,9 +97,7 @@ $: schemaReadOnly = !responseSuccess $: variablesReadOnly = !responseSuccess $: showVariablesTab = shouldShowVariables(dynamicVariables, variablesReadOnly) - $: hasSchema = - Object.keys(schema || {}).length !== 0 || - Object.keys(query?.schema || {}).length !== 0 + $: hasSchema = Object.keys(schema || {}).length !== 0 $: runtimeUrlQueries = readableToRuntimeMap(mergedBindings, breakQs) @@ -161,7 +159,7 @@ newQuery.fields.queryString = queryString newQuery.fields.authConfigId = authConfigId newQuery.fields.disabledHeaders = restUtils.flipHeaderState(enabledHeaders) - newQuery.schema = restUtils.fieldsToSchema(schema) + newQuery.schema = schema return newQuery } @@ -231,6 +229,14 @@ notifications.info("Request did not return any data") } else { response.info = response.info || { code: 200 } + // if existing schema, copy over what it is + if (schema) { + for (let [name, field] of Object.entries(schema)) { + if (response.schema[name]) { + response.schema[name] = field + } + } + } schema = response.schema notifications.success("Request sent successfully") } @@ -386,6 +392,7 @@ onMount(async () => { query = getSelectedQuery() + schema = query.schema try { // Clear any unsaved changes to the datasource @@ -416,7 +423,6 @@ query.fields.path = `${datasource.config.url}/${path ? path : ""}` } url = buildUrl(query.fields.path, breakQs) - schema = restUtils.schemaToFields(query.schema) requestBindings = restUtils.queryParametersToKeyValue(query.parameters) authConfigId = getAuthConfigId() if (!query.fields.disabledHeaders) { @@ -682,10 +688,11 @@ bind:object={schema} name="schema" headings - options={SchemaTypeOptions} + options={SchemaTypeOptionsExpanded} menuItems={schemaMenuItems} showMenu={!schemaReadOnly} readOnly={schemaReadOnly} + compare={(option, value) => option.type === value.type} /> {/if} diff --git a/packages/builder/src/constants/backend/index.js b/packages/builder/src/constants/backend/index.js index ac4079b69e..eb47ac97fe 100644 --- a/packages/builder/src/constants/backend/index.js +++ b/packages/builder/src/constants/backend/index.js @@ -271,6 +271,11 @@ export const SchemaTypeOptions = [ { label: "Datetime", value: "datetime" }, ] +export const SchemaTypeOptionsExpanded = SchemaTypeOptions.map(el => ({ + ...el, + value: { type: el.value }, +})) + export const RawRestBodyTypes = { NONE: "none", FORM: "form", diff --git a/packages/builder/src/helpers/data/utils.js b/packages/builder/src/helpers/data/utils.js index d1ff4c5f80..a29ce8db6d 100644 --- a/packages/builder/src/helpers/data/utils.js +++ b/packages/builder/src/helpers/data/utils.js @@ -1,26 +1,6 @@ import { IntegrationTypes } from "constants/backend" import { findHBSBlocks } from "@budibase/string-templates" -export function schemaToFields(schema) { - const response = {} - if (schema && typeof schema === "object") { - for (let [field, value] of Object.entries(schema)) { - response[field] = value?.type || "string" - } - } - return response -} - -export function fieldsToSchema(fields) { - const response = {} - if (fields && typeof fields === "object") { - for (let [name, type] of Object.entries(fields)) { - response[name] = { name, type } - } - } - return response -} - export function breakQueryString(qs) { if (!qs) { return {} @@ -184,10 +164,8 @@ export const parseToCsv = (headers, rows) => { export default { breakQueryString, buildQueryString, - fieldsToSchema, flipHeaderState, keyValueToQueryParameters, parseToCsv, queryParametersToKeyValue, - schemaToFields, } diff --git a/packages/builder/src/helpers/planTitle.js b/packages/builder/src/helpers/planTitle.js index dc07e8be2f..098bfb4529 100644 --- a/packages/builder/src/helpers/planTitle.js +++ b/packages/builder/src/helpers/planTitle.js @@ -1,11 +1,27 @@ import { PlanType } from "@budibase/types" export function getFormattedPlanName(userPlanType) { - let planName = "Free" - if (userPlanType === PlanType.PREMIUM_PLUS) { - planName = "Premium" - } else if (userPlanType === PlanType.ENTERPRISE_BASIC) { - planName = "Enterprise" + let planName + switch (userPlanType) { + case PlanType.PRO: + planName = "Pro" + break + case PlanType.TEAM: + planName = "Team" + break + case PlanType.PREMIUM: + case PlanType.PREMIUM_PLUS: + planName = "Premium" + break + case PlanType.BUSINESS: + planName = "Business" + break + case PlanType.ENTERPRISE_BASIC: + case PlanType.ENTERPRISE: + planName = "Enterprise" + break + default: + planName = "Free" // Default to "Free" if the type is not explicitly handled } return `${planName} Plan` } diff --git a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/_components/AppPreview.svelte b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/_components/AppPreview.svelte index 2127392bb9..011980bbe2 100644 --- a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/_components/AppPreview.svelte +++ b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/_components/AppPreview.svelte @@ -1,7 +1,7 @@
    @@ -111,7 +112,7 @@ on:dragover={dragover(component, index)} on:iconClick={() => toggleNodeOpen(component._id)} on:drop={onDrop} - hovering={$store.hoveredComponentId === component._id} + hovering={$hoverStore.componentId === component._id} on:mouseenter={() => hover(component._id)} on:mouseleave={() => hover(null)} text={getComponentText(component)} diff --git a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/_components/ComponentList/index.svelte b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/_components/ComponentList/index.svelte index d2ffc5de74..13f2e73853 100644 --- a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/_components/ComponentList/index.svelte +++ b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/_components/ComponentList/index.svelte @@ -1,7 +1,12 @@
    @@ -60,7 +65,7 @@ icon="WebPage" on:drop={onDrop} on:click={() => ($store.selectedComponentId = screenComponentId)} - hovering={$store.hoveredComponentId === screenComponentId} + hovering={$hoverStore.componentId === screenComponentId} on:mouseenter={() => hover(screenComponentId)} on:mouseleave={() => hover(null)} id="component-screen" @@ -79,7 +84,7 @@ : "VisibilityOff"} on:drop={onDrop} on:click={() => ($store.selectedComponentId = navComponentId)} - hovering={$store.hoveredComponentId === navComponentId} + hovering={$hoverStore.componentId === navComponentId} on:mouseenter={() => hover(navComponentId)} on:mouseleave={() => hover(null)} id="component-nav" diff --git a/packages/builder/src/stores/backend/queries.js b/packages/builder/src/stores/backend/queries.js index 1ec23b300c..7f5f83a792 100644 --- a/packages/builder/src/stores/backend/queries.js +++ b/packages/builder/src/stores/backend/queries.js @@ -89,8 +89,8 @@ export function createQueriesStore() { // Assume all the fields are strings and create a basic schema from the // unique fields returned by the server const schema = {} - for (let [field, type] of Object.entries(result.schemaFields)) { - schema[field] = type || "string" + for (let [field, metadata] of Object.entries(result.schema)) { + schema[field] = metadata || { type: "string" } } return { ...result, schema, rows: result.rows || [] } } diff --git a/packages/client/manifest.json b/packages/client/manifest.json index 2a9bc6f258..83a5b858db 100644 --- a/packages/client/manifest.json +++ b/packages/client/manifest.json @@ -3969,6 +3969,12 @@ "key": "allowManualEntry", "defaultValue": false }, + { + "type": "boolean", + "label": "Auto confirm", + "key": "autoConfirm", + "defaultValue": false + }, { "type": "boolean", "label": "Play sound on scan", diff --git a/packages/client/src/components/app/forms/CodeScanner.svelte b/packages/client/src/components/app/forms/CodeScanner.svelte index ff860d216f..2a546eb64c 100644 --- a/packages/client/src/components/app/forms/CodeScanner.svelte +++ b/packages/client/src/components/app/forms/CodeScanner.svelte @@ -14,11 +14,13 @@ export let value export let disabled = false export let allowManualEntry = false + export let autoConfirm = false export let scanButtonText = "Scan code" export let beepOnScan = false export let beepFrequency = 2637 export let customFrequency = 1046 export let preferredCamera = "environment" + export let validator const dispatch = createEventDispatcher() @@ -41,6 +43,9 @@ beep() } dispatch("change", decodedText) + if (autoConfirm && !validator?.(decodedText)) { + camModal?.hide() + } } } @@ -127,7 +132,11 @@
    {#if value && !manualMode}
    - + {#if validator?.(value)} + + {:else} + + {/if} {value}
    {/if} @@ -183,11 +192,16 @@
    {#if cameraEnabled === true}
    - {#if value} + {#if value && !validator?.(value)}
    {value}
    + {:else if value && validator?.(value)} +
    + + {value} +
    {:else}
    diff --git a/packages/client/src/components/app/forms/CodeScannerField.svelte b/packages/client/src/components/app/forms/CodeScannerField.svelte index be590106c2..7c9948554a 100644 --- a/packages/client/src/components/app/forms/CodeScannerField.svelte +++ b/packages/client/src/components/app/forms/CodeScannerField.svelte @@ -11,6 +11,7 @@ export let defaultValue = "" export let onChange export let allowManualEntry + export let autoConfirm export let scanButtonText export let beepOnScan export let beepFrequency @@ -49,11 +50,13 @@ on:change={handleUpdate} disabled={fieldState.disabled || fieldState.readonly} {allowManualEntry} + {autoConfirm} scanButtonText={scanText} {beepOnScan} {beepFrequency} {customFrequency} {preferredCamera} + validator={fieldState.validator} /> {/if} diff --git a/packages/client/src/components/app/forms/RelationshipField.svelte b/packages/client/src/components/app/forms/RelationshipField.svelte index af48183c20..9d0503be8e 100644 --- a/packages/client/src/components/app/forms/RelationshipField.svelte +++ b/packages/client/src/components/app/forms/RelationshipField.svelte @@ -108,10 +108,13 @@ } } - $: forceFetchRows(filter) + $: forceFetchRows(filter, fieldApi) $: debouncedFetchRows(searchTerm, primaryDisplay, defaultValue) const forceFetchRows = async () => { + if (!fieldApi) { + return + } // if the filter has changed, then we need to reset the options, clear the selection, and re-fetch optionsObj = {} fieldApi.setValue([]) @@ -236,7 +239,6 @@ bind:searchTerm loading={$fetch.loading} bind:open - customPopoverMaxHeight={400} /> {/if} diff --git a/packages/pro b/packages/pro index ce7722ed44..0e4c5f95bd 160000 --- a/packages/pro +++ b/packages/pro @@ -1 +1 @@ -Subproject commit ce7722ed4474718596b465dcfd49bef36cab2e42 +Subproject commit 0e4c5f95bda6af126a5cddeef01b8cf551f236be diff --git a/packages/server/src/api/controllers/query/index.ts b/packages/server/src/api/controllers/query/index.ts index 2aa5526c30..d38df00443 100644 --- a/packages/server/src/api/controllers/query/index.ts +++ b/packages/server/src/api/controllers/query/index.ts @@ -1,15 +1,21 @@ import { generateQueryID } from "../../../db/utils" -import { BaseQueryVerbs, FieldTypes } from "../../../constants" +import { BaseQueryVerbs } from "../../../constants" import { Thread, ThreadType } from "../../../threads" import { save as saveDatasource } from "../datasource" import { RestImporter } from "./import" import { invalidateDynamicVariables } from "../../../threads/utils" import env from "../../../environment" -import { quotas } from "@budibase/pro" import { events, context, utils, constants } from "@budibase/backend-core" import sdk from "../../../sdk" -import { QueryEvent } from "../../../threads/definitions" -import { ConfigType, Query, UserCtx, SessionCookie } from "@budibase/types" +import { QueryEvent, QueryResponse } from "../../../threads/definitions" +import { + ConfigType, + Query, + UserCtx, + SessionCookie, + QuerySchema, + FieldType, +} from "@budibase/types" import { ValidQueryNameRegex } from "@budibase/shared-core" const Runner = new Thread(ThreadType.QUERY, { @@ -162,39 +168,43 @@ export async function preview(ctx: UserCtx) { }, } - const { rows, keys, info, extra } = (await Runner.run(inputs)) as any - const schemaFields: any = {} + const { rows, keys, info, extra } = await Runner.run(inputs) + const previewSchema: Record = {} + const makeQuerySchema = (type: FieldType, name: string): QuerySchema => ({ + type, + name, + }) if (rows?.length > 0) { for (let key of [...new Set(keys)] as string[]) { const field = rows[0][key] let type = typeof field, - fieldType = FieldTypes.STRING + fieldMetadata = makeQuerySchema(FieldType.STRING, key) if (field) switch (type) { case "boolean": - schemaFields[key] = FieldTypes.BOOLEAN + fieldMetadata = makeQuerySchema(FieldType.BOOLEAN, key) break case "object": if (field instanceof Date) { - fieldType = FieldTypes.DATETIME + fieldMetadata = makeQuerySchema(FieldType.DATETIME, key) } else if (Array.isArray(field)) { - fieldType = FieldTypes.ARRAY + fieldMetadata = makeQuerySchema(FieldType.ARRAY, key) } else { - fieldType = FieldTypes.JSON + fieldMetadata = makeQuerySchema(FieldType.JSON, key) } break case "number": - fieldType = FieldTypes.NUMBER + fieldMetadata = makeQuerySchema(FieldType.NUMBER, key) break } - schemaFields[key] = fieldType + previewSchema[key] = fieldMetadata } } // if existing schema, update to include any previous schema keys if (existingSchema) { - for (let key of Object.keys(schemaFields)) { - if (existingSchema[key]?.type) { - schemaFields[key] = existingSchema[key].type + for (let key of Object.keys(previewSchema)) { + if (existingSchema[key]) { + previewSchema[key] = existingSchema[key] } } } @@ -203,7 +213,7 @@ export async function preview(ctx: UserCtx) { await events.query.previewed(datasource, query) ctx.body = { rows, - schemaFields, + schema: previewSchema, info, extra, } @@ -257,7 +267,9 @@ async function execute( schema: query.schema, } - const { rows, pagination, extra, info } = (await Runner.run(inputs)) as any + const { rows, pagination, extra, info } = await Runner.run( + inputs + ) // remove the raw from execution incase transformer being used to hide data if (extra?.raw) { delete extra.raw diff --git a/packages/server/src/api/routes/tests/query.seq.spec.js b/packages/server/src/api/routes/tests/query.seq.spec.js index 05ca5f7cbb..b15a5f4d6d 100644 --- a/packages/server/src/api/routes/tests/query.seq.spec.js +++ b/packages/server/src/api/routes/tests/query.seq.spec.js @@ -235,9 +235,9 @@ describe("/queries", () => { .expect("Content-Type", /json/) .expect(200) // these responses come from the mock - expect(res.body.schemaFields).toEqual({ - a: "string", - b: "number", + expect(res.body.schema).toEqual({ + a: { type: "string", name: "a" }, + b: { type: "number", name: "b" }, }) expect(res.body.rows.length).toEqual(1) expect(events.query.previewed).toBeCalledTimes(1) @@ -300,10 +300,10 @@ describe("/queries", () => { queryString: "test={{ variable2 }}", }) // these responses come from the mock - expect(res.body.schemaFields).toEqual({ - opts: "json", - url: "string", - value: "string", + expect(res.body.schema).toEqual({ + opts: { type: "json", name: "opts" }, + url: { type: "string", name: "url" }, + value: { type: "string", name: "value" }, }) expect(res.body.rows[0].url).toEqual("http://www.google.com?test=1") }) @@ -314,10 +314,10 @@ describe("/queries", () => { path: "www.google.com", queryString: "test={{ variable3 }}", }) - expect(res.body.schemaFields).toEqual({ - opts: "json", - url: "string", - value: "string", + expect(res.body.schema).toEqual({ + opts: { type: "json", name: "opts" }, + url: { type: "string", name: "url" }, + value: { type: "string", name: "value" }, }) expect(res.body.rows[0].url).toContain("doctype%20html") }) @@ -337,10 +337,10 @@ describe("/queries", () => { path: "www.failonce.com", queryString: "test={{ variable3 }}", }) - expect(res.body.schemaFields).toEqual({ - fails: "number", - opts: "json", - url: "string", + expect(res.body.schema).toEqual({ + fails: { type: "number", name: "fails" }, + opts: { type: "json", name: "opts" }, + url: { type: "string", name: "url" }, }) expect(res.body.rows[0].fails).toEqual(1) }) diff --git a/packages/server/src/integrations/utils.ts b/packages/server/src/integrations/utils.ts index 102dfe2935..91966658a6 100644 --- a/packages/server/src/integrations/utils.ts +++ b/packages/server/src/integrations/utils.ts @@ -376,8 +376,8 @@ export function checkExternalTables( errors[name] = "Table must have a primary key." } - const schemaFields = Object.keys(table.schema) - if (schemaFields.find(f => invalidColumns.includes(f))) { + const columnNames = Object.keys(table.schema) + if (columnNames.find(f => invalidColumns.includes(f))) { errors[name] = "Table contains invalid columns." } } diff --git a/packages/server/src/middleware/authorized.ts b/packages/server/src/middleware/authorized.ts index cba765a887..fe7f6bb6fe 100644 --- a/packages/server/src/middleware/authorized.ts +++ b/packages/server/src/middleware/authorized.ts @@ -34,7 +34,7 @@ const checkAuthorized = async ( const isCreatorApi = permType === PermissionType.CREATOR const isBuilderApi = permType === PermissionType.BUILDER const isGlobalBuilder = users.isGlobalBuilder(ctx.user) - const isCreator = users.isCreator(ctx.user) + const isCreator = await users.isCreator(ctx.user) const isBuilder = appId ? users.isBuilder(ctx.user, appId) : users.hasBuilderPermissions(ctx.user) diff --git a/packages/server/src/sdk/app/queries/queries.ts b/packages/server/src/sdk/app/queries/queries.ts index 408e393714..3f967b7198 100644 --- a/packages/server/src/sdk/app/queries/queries.ts +++ b/packages/server/src/sdk/app/queries/queries.ts @@ -3,6 +3,27 @@ import { processStringSync } from "@budibase/string-templates" import { context } from "@budibase/backend-core" import { getQueryParams, isProdAppID } from "../../../db/utils" import { BaseQueryVerbs } from "../../../constants" +import { Query, QuerySchema } from "@budibase/types" + +function updateSchema(query: Query): Query { + if (!query.schema) { + return query + } + const schema: Record = {} + for (let key of Object.keys(query.schema)) { + if (typeof query.schema[key] === "string") { + schema[key] = { type: query.schema[key] as string, name: key } + } else { + schema[key] = query.schema[key] as QuerySchema + } + } + query.schema = schema + return query +} + +function updateSchemas(queries: Query[]): Query[] { + return queries.map(query => updateSchema(query)) +} // simple function to append "readable" to all read queries function enrichQueries(input: any) { @@ -25,7 +46,7 @@ export async function find(queryId: string) { delete query.fields delete query.parameters } - return query + return updateSchema(query) } export async function fetch(opts: { enrich: boolean } = { enrich: true }) { @@ -37,12 +58,11 @@ export async function fetch(opts: { enrich: boolean } = { enrich: true }) { }) ) - const queries = body.rows.map((row: any) => row.doc) + let queries = body.rows.map((row: any) => row.doc) if (opts.enrich) { - return enrichQueries(queries) - } else { - return queries + queries = await enrichQueries(queries) } + return updateSchemas(queries) } export async function enrichContext( diff --git a/packages/server/src/sdk/users/tests/utils.spec.ts b/packages/server/src/sdk/users/tests/utils.spec.ts index f7c9413ebd..86dc411caf 100644 --- a/packages/server/src/sdk/users/tests/utils.spec.ts +++ b/packages/server/src/sdk/users/tests/utils.spec.ts @@ -84,7 +84,7 @@ describe("syncGlobalUsers", () => { await syncGlobalUsers() const metadata = await rawUserMetadata() - expect(metadata).toHaveLength(3) + expect(metadata).toHaveLength(2) expect(metadata).toContainEqual( expect.objectContaining({ _id: db.generateUserMetadataID(user1._id!), @@ -121,7 +121,7 @@ describe("syncGlobalUsers", () => { await syncGlobalUsers() const metadata = await rawUserMetadata() - expect(metadata).toHaveLength(0) + expect(metadata).toHaveLength(1) //ADMIN user created in test bootstrap still in the application }) }) }) diff --git a/packages/server/src/tests/utilities/TestConfiguration.ts b/packages/server/src/tests/utilities/TestConfiguration.ts index b5810b9ba3..4785994da4 100644 --- a/packages/server/src/tests/utilities/TestConfiguration.ts +++ b/packages/server/src/tests/utilities/TestConfiguration.ts @@ -278,6 +278,9 @@ class TestConfiguration { if (params) { request.params = params } + request.throw = (status: number, message: string) => { + throw new Error(`Error ${status} - ${message}`) + } return this.doInContext(appId, async () => { await controlFunc(request) return request.body diff --git a/packages/server/src/threads/definitions.ts b/packages/server/src/threads/definitions.ts index 8915642949..90dc136cb5 100644 --- a/packages/server/src/threads/definitions.ts +++ b/packages/server/src/threads/definitions.ts @@ -1,3 +1,5 @@ +import { QuerySchema, Row } from "@budibase/types" + export type WorkerCallback = (error: any, response?: any) => void export interface QueryEvent { @@ -11,7 +13,15 @@ export interface QueryEvent { queryId: string environmentVariables?: Record ctx?: any - schema?: Record + schema?: Record +} + +export interface QueryResponse { + rows: Row[] + keys: string[] + info: any + extra: any + pagination: any } export interface QueryVariable { diff --git a/packages/server/src/threads/index.ts b/packages/server/src/threads/index.ts index 6afaa9bb4e..64305dff6a 100644 --- a/packages/server/src/threads/index.ts +++ b/packages/server/src/threads/index.ts @@ -74,7 +74,7 @@ export class Thread { ) } - run(job: AutomationJob | QueryEvent) { + run(job: AutomationJob | QueryEvent): Promise { const timeout = this.timeoutMs return new Promise((resolve, reject) => { function fire(worker: any) { diff --git a/packages/server/src/threads/query.ts b/packages/server/src/threads/query.ts index fe85015b41..e31bc70b6a 100644 --- a/packages/server/src/threads/query.ts +++ b/packages/server/src/threads/query.ts @@ -1,7 +1,12 @@ import { default as threadUtils } from "./utils" threadUtils.threadSetup() -import { WorkerCallback, QueryEvent, QueryVariable } from "./definitions" +import { + WorkerCallback, + QueryEvent, + QueryVariable, + QueryResponse, +} from "./definitions" import ScriptRunner from "../utilities/scriptRunner" import { getIntegration } from "../integrations" import { processStringSync } from "@budibase/string-templates" @@ -9,7 +14,7 @@ import { context, cache, auth } from "@budibase/backend-core" import { getGlobalIDFromUserMetadataID } from "../db/utils" import sdk from "../sdk" import { cloneDeep } from "lodash/fp" -import { SourceName, Query } from "@budibase/types" +import { Query } from "@budibase/types" import { isSQL } from "../integrations/utils" import { interpolateSQL } from "../integrations/queries/sql" @@ -53,7 +58,7 @@ class QueryRunner { this.hasDynamicVariables = false } - async execute(): Promise { + async execute(): Promise { let { datasource, fields, queryVerb, transformer, schema } = this let datasourceClone = cloneDeep(datasource) let fieldsClone = cloneDeep(fields) diff --git a/packages/shared-core/src/sdk/documents/users.ts b/packages/shared-core/src/sdk/documents/users.ts index 1aaf44ff7c..11e80dcf29 100644 --- a/packages/shared-core/src/sdk/documents/users.ts +++ b/packages/shared-core/src/sdk/documents/users.ts @@ -70,7 +70,7 @@ export function hasAppCreatorPermissions(user?: User | ContextUser): boolean { return _.flow( _.get("roles"), _.values, - _.find(x => x === "CREATOR"), + _.find(x => ["CREATOR", "ADMIN"].includes(x)), x => !!x )(user) } diff --git a/packages/string-templates/manifest.json b/packages/string-templates/manifest.json index 36db081936..9dd8260350 100644 --- a/packages/string-templates/manifest.json +++ b/packages/string-templates/manifest.json @@ -137,7 +137,7 @@ "n" ], "numArgs": 2, - "example": "{{ after [1, 2, 3] 1}} -> [3]", + "example": "{{ after ['a', 'b', 'c', 'd'] 2}} -> ['c', 'd']", "description": "

    Returns all of the items in an array after the specified index. Opposite of before.

    \n" }, "arrayify": { @@ -154,7 +154,7 @@ "n" ], "numArgs": 2, - "example": "{{ before [1, 2, 3] 2}} -> [1, 2]", + "example": "{{ before ['a', 'b', 'c', 'd'] 3}} -> ['a', 'b']", "description": "

    Return all of the items in the collection before the specified count. Opposite of after.

    \n" }, "eachIndex": { @@ -182,7 +182,7 @@ "n" ], "numArgs": 2, - "example": "{{first [1, 2, 3, 4] 2}} -> [1, 2]", + "example": "{{first [1, 2, 3, 4] 2}} -> 1,2", "description": "

    Returns the first item, or first n items of an array.

    \n" }, "forEach": { @@ -200,7 +200,7 @@ "options" ], "numArgs": 3, - "example": "{{#inArray [1, 2, 3] 2}} 2 exists {{else}} 2 does not exist {{/inArray}} -> 2 exists", + "example": "{{#inArray [1, 2, 3] 2}} 2 exists {{else}} 2 does not exist {{/inArray}} -> ' 2 exists '", "description": "

    Block helper that renders the block if an array has the given value. Optionally specify an inverse block to render when the array does not have the given value.

    \n" }, "isArray": { @@ -226,7 +226,7 @@ "separator" ], "numArgs": 2, - "example": "{{join [1, 2, 3]}} -> '1, 2, 3'", + "example": "{{join [1, 2, 3]}} -> 1, 2, 3", "description": "

    Join all elements of array into a string, optionally using a given separator.

    \n" }, "equalsLength": { @@ -236,7 +236,7 @@ "options" ], "numArgs": 3, - "example": "{{equalsLength '[1,2,3]' 3}} -> true", + "example": "{{equalsLength [1, 2, 3] 3}} -> true", "description": "

    Returns true if the the length of the given value is equal to the given length. Can be used as a block or inline helper.

    \n" }, "last": { @@ -253,7 +253,7 @@ "value" ], "numArgs": 1, - "example": "{{length '[1, 2, 3]'}} -> 3", + "example": "{{length [1, 2, 3]}} -> 3", "description": "

    Returns the length of the given string or array.

    \n" }, "lengthEqual": { @@ -263,7 +263,7 @@ "options" ], "numArgs": 3, - "example": "{{equalsLength '[1,2,3]' 3}} -> true", + "example": "{{equalsLength [1, 2, 3] 3}} -> true", "description": "

    Returns true if the the length of the given value is equal to the given length. Can be used as a block or inline helper.

    \n" }, "map": { @@ -299,7 +299,7 @@ "provided" ], "numArgs": 3, - "example": "{{#some [1, 'b', 3] isString}} string found {{else}} No string found {{/some}} -> string found", + "example": "{{#some [1, \"b\", 3] isString}} string found {{else}} No string found {{/some}} -> ' string found '", "description": "

    Block helper that returns the block if the callback returns true for some value in the given array.

    \n" }, "sort": { @@ -317,7 +317,7 @@ "props" ], "numArgs": 2, - "example": "{{ sortBy [{a: 'zzz'}, {a: 'aaa'}] 'a' }} -> [{'a':'aaa'}, {'a':'zzz'}]", + "example": "{{ sortBy [{'a': 'zzz'}, {'a': 'aaa'}] 'a' }} -> [{'a':'aaa'},{'a':'zzz'}]", "description": "

    Sort an array. If an array of objects is passed, you may optionally pass a key to sort on as the second argument. You may alternatively pass a sorting function as the second argument.

    \n" }, "withAfter": { @@ -347,7 +347,7 @@ "options" ], "numArgs": 3, - "example": "{{ withFirst [1, 2, 3] }} {{this}} {{/withFirst}}", + "example": "{{#withFirst [1, 2, 3] }}{{this}}{{/withFirst}} -> 1", "description": "

    Use the first item in a collection inside a handlebars block expression. Opposite of withLast.

    \n" }, "withGroup": { @@ -357,7 +357,7 @@ "options" ], "numArgs": 3, - "example": "{{#withGroup [1, 2, 3, 4] 2}} {{#each this}} {{.}} {{each}}
    {{/withGroup}} -> 1,2
    3,4
    ", + "example": "{{#withGroup [1, 2, 3, 4] 2}}{{#each this}}{{.}}{{/each}}
    {{/withGroup}} -> 12
    34
    ", "description": "

    Block helper that groups array elements by given group size.

    \n" }, "withLast": { @@ -367,7 +367,7 @@ "options" ], "numArgs": 3, - "example": "{{#withLast [1, 2, 3, 4]}} {{this}} {{/withLast}} -> 4", + "example": "{{#withLast [1, 2, 3, 4]}}{{this}}{{/withLast}} -> 4", "description": "

    Use the last item or n items in an array as context inside a block. Opposite of withFirst.

    \n" }, "withSort": { @@ -377,7 +377,7 @@ "options" ], "numArgs": 3, - "example": "{{#withSort ['b', 'a', 'c']}} {{this}} {{/withSort}} -> abc", + "example": "{{#withSort ['b', 'a', 'c']}}{{this}}{{/withSort}} -> abc", "description": "

    Block helper that sorts a collection and exposes the sorted collection as context inside the block.

    \n" }, "unique": { @@ -386,7 +386,7 @@ "options" ], "numArgs": 2, - "example": "{{#each (unique ['a', 'a', 'c', 'b', 'e', 'e']) }} {{.}} {{/each}} -> acbe", + "example": "{{#each (unique ['a', 'a', 'c', 'b', 'e', 'e']) }}{{.}}{{/each}} -> acbe", "description": "

    Block helper that return an array with all duplicate values removed. Best used along with a each helper.

    \n" } }, @@ -396,7 +396,7 @@ "number" ], "numArgs": 1, - "example": "{{ bytes 1386 }} -> 1.4Kb", + "example": "{{ bytes 1386 1 }} -> 1.4 kB", "description": "

    Format a number to it's equivalent in bytes. If a string is passed, it's length will be formatted and returned. Examples: - 'foo' => 3 B - 13661855 => 13.66 MB - 825399 => 825.39 kB - 1396 => 1.4 kB

    \n" }, "addCommas": { @@ -430,7 +430,7 @@ "fractionDigits" ], "numArgs": 2, - "example": "{{ toExponential 10123 2 }} -> 101e+4", + "example": "{{ toExponential 10123 2 }} -> 1.01e+4", "description": "

    Returns a string representing the given number in exponential notation.

    \n" }, "toFixed": { @@ -472,7 +472,7 @@ "str" ], "numArgs": 1, - "example": "{{ encodeURI 'https://myurl?Hello There' }} -> https://myurl?Hello%20There", + "example": "{{ encodeURI 'https://myurl?Hello There' }} -> https%3A%2F%2Fmyurl%3FHello%20There", "description": "

    Encodes a Uniform Resource Identifier (URI) component by replacing each instance of certain characters by one, two, three, or four escape sequences representing the UTF-8 encoding of the character.

    \n" }, "escape": { @@ -480,7 +480,7 @@ "str" ], "numArgs": 1, - "example": "{{ escape 'https://myurl?Hello+There' }} -> https://myurl?Hello%20There", + "example": "{{ escape 'https://myurl?Hello+There' }} -> https%3A%2F%2Fmyurl%3FHello%2BThere", "description": "

    Escape the given string by replacing characters with escape sequences. Useful for allowing the string to be used in a URL, etc.

    \n" }, "decodeURI": { @@ -488,7 +488,7 @@ "str" ], "numArgs": 1, - "example": "{{ decodeURI 'https://myurl?Hello%20There' }} -> https://myurl?=Hello There", + "example": "{{ decodeURI 'https://myurl?Hello%20There' }} -> https://myurl?Hello There", "description": "

    Decode a Uniform Resource Identifier (URI) component.

    \n" }, "urlResolve": { @@ -513,7 +513,7 @@ "url" ], "numArgs": 1, - "example": "{{ stripQueryString 'https://myurl/api/test?foo=bar' }} -> 'https://myurl/api/test'", + "example": "{{ stripQuerystring 'https://myurl/api/test?foo=bar' }} -> 'https://myurl/api/test'", "description": "

    Strip the query string from the given url.

    \n" }, "stripProtocol": { @@ -521,7 +521,7 @@ "str" ], "numArgs": 1, - "example": "{{ stripProtocol 'https://myurl/api/test' }} -> 'myurl/api/test'", + "example": "{{ stripProtocol 'https://myurl/api/test' }} -> '//myurl/api/test'", "description": "

    Strip protocol from a url. Useful for displaying media that may have an 'http' protocol on secure connections.

    \n" } }, @@ -573,7 +573,7 @@ "string" ], "numArgs": 1, - "example": "{{ chop ' ABC '}} -> 'ABC'", + "example": "{{ chop ' ABC '}} -> ABC", "description": "

    Like trim, but removes both extraneous whitespace and non-word characters from the beginning and end of a string.

    \n" }, "dashcase": { @@ -606,7 +606,7 @@ "length" ], "numArgs": 2, - "example": "{{ellipsis 'foo bar baz', 7}} -> foo bar…", + "example": "{{ellipsis 'foo bar baz' 7}} -> foo bar…", "description": "

    Truncates a string to the specified length, and appends it with an elipsis, .

    \n" }, "hyphenate": { @@ -675,14 +675,6 @@ "example": "{{prepend 'bar' 'foo-'}} -> foo-bar", "description": "

    Prepends the given string with the specified prefix.

    \n" }, - "raw": { - "args": [ - "options" - ], - "numArgs": 1, - "example": "{{{{#raw}}}} {{foo}} {{{{/raw}}}} -> {{foo}}", - "description": "

    Render a block without processing mustache templates inside the block.

    \n" - }, "remove": { "args": [ "str", @@ -698,7 +690,7 @@ "substring" ], "numArgs": 2, - "example": "{{remove 'a b a b a b' 'a'}} -> b a b a b", + "example": "{{removeFirst 'a b a b a b' 'a'}} -> ' b a b a b'", "description": "

    Remove the first occurrence of substring from the given str.

    \n" }, "replace": { @@ -718,7 +710,7 @@ "b" ], "numArgs": 3, - "example": "{{replace 'a b a b a b' 'a' 'z'}} -> z b a b a b", + "example": "{{replaceFirst 'a b a b a b' 'a' 'z'}} -> z b a b a b", "description": "

    Replace the first occurrence of substring a with substring b.

    \n" }, "sentence": { @@ -752,7 +744,7 @@ "options" ], "numArgs": 3, - "example": "{{#startsWith 'Goodbye' 'Hello, world!'}} Yep {{else}} Nope {{/startsWith}} -> Nope", + "example": "{{#startsWith 'Goodbye' 'Hello, world!'}}Yep{{else}}Nope{{/startsWith}} -> Nope", "description": "

    Tests whether a string begins with the given prefix.

    \n" }, "titleize": { @@ -760,7 +752,7 @@ "str" ], "numArgs": 1, - "example": "{{#titleize 'this is title case' }} -> This Is Title Case", + "example": "{{titleize 'this is title case' }} -> This Is Title Case", "description": "

    Title case the given string.

    \n" }, "trim": { @@ -784,7 +776,7 @@ "string" ], "numArgs": 1, - "example": "{{trimRight ' ABC ' }} -> ' ABC '", + "example": "{{trimRight ' ABC ' }} -> ' ABC'", "description": "

    Removes extraneous whitespace from the end of a string.

    \n" }, "truncate": { @@ -804,7 +796,7 @@ "suffix" ], "numArgs": 3, - "example": "{{truncateWords 'foo bar baz' 1 }} -> foo", + "example": "{{truncateWords 'foo bar baz' 1 }} -> foo…", "description": "

    Truncate a string to have the specified number of words. Also see truncate.

    \n" }, "upcase": { @@ -844,7 +836,7 @@ "options" ], "numArgs": 4, - "example": "{{compare 10 '<' 5 }} -> true", + "example": "{{compare 10 '<' 5 }} -> false", "description": "

    Render a block when a comparison of the first and third arguments returns true. The second argument is the [arithemetic operator][operators] to use. You may also optionally specify an inverse block to render when falsy.

    \n" }, "contains": { @@ -874,7 +866,7 @@ "options" ], "numArgs": 3, - "example": "{{#eq 3 3}} equal{{else}} not equal{{/eq}} -> equal", + "example": "{{#eq 3 3}}equal{{else}}not equal{{/eq}} -> equal", "description": "

    Block helper that renders a block if a is equal to b. If an inverse block is specified it will be rendered when falsy. You may optionally use the compare='' hash argument for the second value.

    \n" }, "gt": { @@ -884,7 +876,7 @@ "options" ], "numArgs": 3, - "example": "{{#gt 4 3}} greater than{{else}} not greater than{{/gt}} -> greater than", + "example": "{{#gt 4 3}} greater than{{else}} not greater than{{/gt}} -> ' greater than'", "description": "

    Block helper that renders a block if a is greater than b. If an inverse block is specified it will be rendered when falsy. You may optionally use the compare='' hash argument for the second value.

    \n" }, "gte": { @@ -894,7 +886,7 @@ "options" ], "numArgs": 3, - "example": "{{#gte 4 3}} greater than or equal{{else}} not greater than{{/gte}} -> greater than or equal", + "example": "{{#gte 4 3}} greater than or equal{{else}} not greater than{{/gte}} -> ' greater than or equal'", "description": "

    Block helper that renders a block if a is greater than or equal to b. If an inverse block is specified it will be rendered when falsy. You may optionally use the compare='' hash argument for the second value.

    \n" }, "has": { @@ -904,7 +896,7 @@ "options" ], "numArgs": 3, - "example": "{{#has 'foobar' 'foo'}} has it{{else}} doesn't{{/has}} -> has it", + "example": "{{#has 'foobar' 'foo'}}has it{{else}}doesn't{{/has}} -> has it", "description": "

    Block helper that renders a block if value has pattern. If an inverse block is specified it will be rendered when falsy.

    \n" }, "isFalsey": { @@ -931,7 +923,7 @@ "options" ], "numArgs": 2, - "example": "{{#ifEven 2}} even {{else}} odd {{/ifEven}} -> even", + "example": "{{#ifEven 2}} even {{else}} odd {{/ifEven}} -> ' even '", "description": "

    Return true if the given value is an even number.

    \n" }, "ifNth": { @@ -941,8 +933,8 @@ "options" ], "numArgs": 3, - "example": "{{#ifNth 10 2}} remainder {{else}} no remainder {{/ifNth}} -> remainder", - "description": "

    Conditionally renders a block if the remainder is zero when a operand is divided by b. If an inverse block is specified it will be rendered when the remainder is not zero.

    \n" + "example": "{{#ifNth 2 10}}remainder{{else}}no remainder{{/ifNth}} -> remainder", + "description": "

    Conditionally renders a block if the remainder is zero when b operand is divided by a. If an inverse block is specified it will be rendered when the remainder is not zero.

    \n" }, "ifOdd": { "args": [ @@ -950,7 +942,7 @@ "options" ], "numArgs": 2, - "example": "{{#ifOdd 3}} odd {{else}} even {{/ifOdd}} -> odd", + "example": "{{#ifOdd 3}}odd{{else}}even{{/ifOdd}} -> odd", "description": "

    Block helper that renders a block if value is an odd number. If an inverse block is specified it will be rendered when falsy.

    \n" }, "is": { @@ -960,7 +952,7 @@ "options" ], "numArgs": 3, - "example": "{{#is 3 3}} is {{else}} is not {{/is}} -> is", + "example": "{{#is 3 3}} is {{else}} is not {{/is}} -> ' is '", "description": "

    Block helper that renders a block if a is equal to b. If an inverse block is specified it will be rendered when falsy. Similar to eq but does not do strict equality.

    \n" }, "isnt": { @@ -970,7 +962,7 @@ "options" ], "numArgs": 3, - "example": "{{#isnt 3 3}} isnt {{else}} is {{/isnt}} -> is", + "example": "{{#isnt 3 3}} isnt {{else}} is {{/isnt}} -> ' is '", "description": "

    Block helper that renders a block if a is not equal to b. If an inverse block is specified it will be rendered when falsy. Similar to unlessEq but does not use strict equality for comparisons.

    \n" }, "lt": { @@ -979,7 +971,7 @@ "options" ], "numArgs": 2, - "example": "{{#lt 2 3}} less than {{else}} more than or equal {{/lt}} -> less than", + "example": "{{#lt 2 3}} less than {{else}} more than or equal {{/lt}} -> ' less than '", "description": "

    Block helper that renders a block if a is less than b. If an inverse block is specified it will be rendered when falsy. You may optionally use the compare='' hash argument for the second value.

    \n" }, "lte": { @@ -989,7 +981,7 @@ "options" ], "numArgs": 3, - "example": "{{#lte 2 3}} less than or equal {{else}} more than {{/lte}} -> less than or equal", + "example": "{{#lte 2 3}} less than or equal {{else}} more than {{/lte}} -> ' less than or equal '", "description": "

    Block helper that renders a block if a is less than or equal to b. If an inverse block is specified it will be rendered when falsy. You may optionally use the compare='' hash argument for the second value.

    \n" }, "neither": { @@ -999,7 +991,7 @@ "options" ], "numArgs": 3, - "example": "{{#neither null null}} both falsey {{else}} both not falsey {{/neither}} -> both falsey", + "example": "{{#neither null null}}both falsey{{else}}both not falsey{{/neither}} -> both falsey", "description": "

    Block helper that renders a block if neither of the given values are truthy. If an inverse block is specified it will be rendered when falsy.

    \n" }, "not": { @@ -1008,7 +1000,7 @@ "options" ], "numArgs": 2, - "example": "{{#not undefined }} falsey {{else}} not falsey {{/not}} -> falsey", + "example": "{{#not undefined }}falsey{{else}}not falsey{{/not}} -> falsey", "description": "

    Returns true if val is falsey. Works as a block or inline helper.

    \n" }, "or": { @@ -1017,7 +1009,7 @@ "options" ], "numArgs": 2, - "example": "{{#or 1 2 undefined }} at least one truthy {{else}} all falsey {{/or}} -> at least one truthy", + "example": "{{#or 1 2 undefined }} at least one truthy {{else}} all falsey {{/or}} -> ' at least one truthy '", "description": "

    Block helper that renders a block if any of the given values is truthy. If an inverse block is specified it will be rendered when falsy.

    \n" }, "unlessEq": { @@ -1027,7 +1019,7 @@ "options" ], "numArgs": 3, - "example": "{{#unlessEq 2 1 }} not equal {{else}} equal {{/unlessEq}} -> not equal", + "example": "{{#unlessEq 2 1 }} not equal {{else}} equal {{/unlessEq}} -> ' not equal '", "description": "

    Block helper that always renders the inverse block unless a is equal to b.

    \n" }, "unlessGt": { @@ -1037,7 +1029,7 @@ "options" ], "numArgs": 3, - "example": "{{#unlessGt 20 1 }} not greater than {{else}} greater than {{/unlessGt}} -> greater than", + "example": "{{#unlessGt 20 1 }} not greater than {{else}} greater than {{/unlessGt}} -> ' greater than '", "description": "

    Block helper that always renders the inverse block unless a is greater than b.

    \n" }, "unlessLt": { @@ -1047,7 +1039,7 @@ "options" ], "numArgs": 3, - "example": "{{#unlessLt 20 1 }} greater than or equal {{else}} less than {{/unlessLt}} -> greater than or equal", + "example": "{{#unlessLt 20 1 }}greater than or equal{{else}}less than{{/unlessLt}} -> greater than or equal", "description": "

    Block helper that always renders the inverse block unless a is less than b.

    \n" }, "unlessGteq": { @@ -1057,7 +1049,7 @@ "options" ], "numArgs": 3, - "example": "{{#unlessGteq 20 1 }} less than {{else}} greater than or equal to {{/unlessGteq}} -> greater than or equal to", + "example": "{{#unlessGteq 20 1 }} less than {{else}}greater than or equal to{{/unlessGteq}} -> greater than or equal to", "description": "

    Block helper that always renders the inverse block unless a is greater than or equal to b.

    \n" }, "unlessLteq": { @@ -1067,7 +1059,7 @@ "options" ], "numArgs": 3, - "example": "{{#unlessLteq 20 1 }} greater than {{else}} less than or equal to {{/unlessLteq}} -> greater than", + "example": "{{#unlessLteq 20 1 }} greater than {{else}} less than or equal to {{/unlessLteq}} -> ' greater than '", "description": "

    Block helper that always renders the inverse block unless a is less than or equal to b.

    \n" } }, @@ -1204,7 +1196,7 @@ "durationType" ], "numArgs": 2, - "example": "{{duration timeLeft \"seconds\"}} -> a few seconds", + "example": "{{duration 8 \"seconds\"}} -> a few seconds", "description": "

    Produce a humanized duration left/until given an amount of time and the type of time measurement.

    \n" } } diff --git a/packages/string-templates/package.json b/packages/string-templates/package.json index 1a52adf38c..dfbff1a24b 100644 --- a/packages/string-templates/package.json +++ b/packages/string-templates/package.json @@ -25,7 +25,7 @@ "manifest": "node ./scripts/gen-collection-info.js" }, "dependencies": { - "@budibase/handlebars-helpers": "^0.12.0", + "@budibase/handlebars-helpers": "^0.13.0", "dayjs": "^1.10.8", "handlebars": "^4.7.6", "lodash.clonedeep": "^4.5.0", diff --git a/packages/string-templates/scripts/gen-collection-info.js b/packages/string-templates/scripts/gen-collection-info.js index e42c9ccaf0..b487c4dde4 100644 --- a/packages/string-templates/scripts/gen-collection-info.js +++ b/packages/string-templates/scripts/gen-collection-info.js @@ -36,7 +36,7 @@ const ADDED_HELPERS = { duration: { args: ["time", "durationType"], numArgs: 2, - example: '{{duration timeLeft "seconds"}} -> a few seconds', + example: '{{duration 8 "seconds"}} -> a few seconds', description: "Produce a humanized duration left/until given an amount of time and the type of time measurement.", }, @@ -118,6 +118,8 @@ function getCommentInfo(file, func) { return docs } +const excludeFunctions = { string: ["raw"] } + /** * This script is very specific to purpose, parsing the handlebars-helpers files to attempt to get information about them. */ @@ -136,7 +138,8 @@ function run() { // skip built in functions and ones seen already if ( HelperFunctionBuiltin.indexOf(name) !== -1 || - foundNames.indexOf(name) !== -1 + foundNames.indexOf(name) !== -1 || + excludeFunctions[collection]?.includes(name) ) { continue } diff --git a/packages/string-templates/test/helpers.spec.js b/packages/string-templates/test/helpers.spec.js index 380b0f2833..75de373109 100644 --- a/packages/string-templates/test/helpers.spec.js +++ b/packages/string-templates/test/helpers.spec.js @@ -61,10 +61,10 @@ describe("test the array helpers", () => { }) it("should allow use of the before helper", async () => { - const output = await processString("{{before array 2}}", { + const output = await processString("{{before array 3}}", { array, }) - expect(output).toBe("hi,person,how") + expect(output).toBe("hi,person") }) it("should allow use of the filter helper", async () => { diff --git a/packages/string-templates/test/manifest.spec.js b/packages/string-templates/test/manifest.spec.js new file mode 100644 index 0000000000..506f2eb6f7 --- /dev/null +++ b/packages/string-templates/test/manifest.spec.js @@ -0,0 +1,96 @@ +jest.mock("@budibase/handlebars-helpers/lib/math", () => { + const actual = jest.requireActual("@budibase/handlebars-helpers/lib/math") + + return { + ...actual, + random: () => 10, + } +}) +jest.mock("@budibase/handlebars-helpers/lib/uuid", () => { + const actual = jest.requireActual("@budibase/handlebars-helpers/lib/uuid") + + return { + ...actual, + uuid: () => "f34ebc66-93bd-4f7c-b79b-92b5569138bc", + } +}) + +const fs = require("fs") +const { processString } = require("../src/index.cjs") + +const tk = require("timekeeper") +tk.freeze("2021-01-21T12:00:00") + +const manifest = JSON.parse( + fs.readFileSync(require.resolve("../manifest.json"), "utf8") +) + +const collections = Object.keys(manifest) +const examples = collections.reduce((acc, collection) => { + const functions = Object.keys(manifest[collection]).filter( + fnc => manifest[collection][fnc].example + ) + if (functions.length) { + acc[collection] = functions + } + return acc +}, {}) + +function escapeRegExp(string) { + return string.replace(/[.*+?^${}()|[\]\\]/g, "\\$&") // $& means the whole matched string +} + +function tryParseJson(str) { + if (typeof str !== "string") { + return + } + + try { + return JSON.parse(str.replace(/\'/g, '"')) + } catch (e) { + return + } +} + +describe("manifest", () => { + describe("examples are valid", () => { + describe.each(Object.keys(examples))("%s", collection => { + it.each(examples[collection])("%s", async func => { + const example = manifest[collection][func].example + + let [hbs, js] = example.split("->").map(x => x.trim()) + + const context = { + double: i => i * 2, + isString: x => typeof x === "string", + } + + const arrays = hbs.match(/\[[^/\]]+\]/) + arrays?.forEach((arrayString, i) => { + hbs = hbs.replace(new RegExp(escapeRegExp(arrayString)), `array${i}`) + context[`array${i}`] = JSON.parse(arrayString.replace(/\'/g, '"')) + }) + + if (js === undefined) { + // The function has no return value + return + } + + let result = await processString(hbs, context) + // Trim 's + js = js.replace(/^\'|\'$/g, "") + if ((parsedExpected = tryParseJson(js))) { + if (Array.isArray(parsedExpected)) { + if (typeof parsedExpected[0] === "object") { + js = JSON.stringify(parsedExpected) + } else { + js = parsedExpected.join(",") + } + } + } + result = result.replace(/ /g, " ") + expect(result).toEqual(js) + }) + }) + }) +}) diff --git a/packages/types/src/documents/app/query.ts b/packages/types/src/documents/app/query.ts index c288ed9980..473449bffb 100644 --- a/packages/types/src/documents/app/query.ts +++ b/packages/types/src/documents/app/query.ts @@ -1,12 +1,17 @@ import { Document } from "../document" +export interface QuerySchema { + name?: string + type: string +} + export interface Query extends Document { datasourceId: string name: string parameters: QueryParameter[] fields: RestQueryFields | any transformer: string | null - schema: Record + schema: Record readable: boolean queryVerb: string } diff --git a/packages/worker/src/api/controllers/global/self.ts b/packages/worker/src/api/controllers/global/self.ts index d14aecd1a7..2d7e83a2c3 100644 --- a/packages/worker/src/api/controllers/global/self.ts +++ b/packages/worker/src/api/controllers/global/self.ts @@ -91,6 +91,9 @@ export async function getSelf(ctx: any) { id: userId, } + // Adjust creators quotas (prevents wrong creators count if user has changed the plan) + await groups.adjustGroupCreatorsQuotas() + // get the main body of the user const user = await userSdk.db.getUser(userId) ctx.body = await groups.enrichUserRolesFromGroups(user) diff --git a/scripts/build-single-image.sh b/scripts/build-single-image.sh index ba64d6121b..16b668e033 100755 --- a/scripts/build-single-image.sh +++ b/scripts/build-single-image.sh @@ -1,4 +1,4 @@ #!/bin/bash yarn build --scope @budibase/server --scope @budibase/worker version=$(./scripts/getCurrentVersion.sh) -docker build -f hosting/single/Dockerfile -t budibase:latest --build-arg BUDIBASE_VERSION=$version . +docker build -f hosting/single/Dockerfile -t budibase:latest --build-arg BUDIBASE_VERSION=$version --build-arg TARGETBUILD=single . diff --git a/yarn.lock b/yarn.lock index 7750c8df7b..63684b6278 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2031,10 +2031,10 @@ resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39" integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw== -"@budibase/handlebars-helpers@^0.12.0": - version "0.12.0" - resolved "https://registry.yarnpkg.com/@budibase/handlebars-helpers/-/handlebars-helpers-0.12.0.tgz#dcc4ba8d796a611474e3495b1142c56b470ca67d" - integrity sha512-JjGboau7KMdrVSO8gGJzgo1ACSeD4BxN46vidIx9hvdrEXy+v1x2bfQZMaq/c7Dv+V1vyq7c006XwxR1bpfARg== +"@budibase/handlebars-helpers@^0.13.0": + version "0.13.0" + resolved "https://registry.yarnpkg.com/@budibase/handlebars-helpers/-/handlebars-helpers-0.13.0.tgz#224333d14e3900b7dacf48286af1e624a9fd62ea" + integrity sha512-g8+sFrMNxsIDnK+MmdUICTVGr6ReUFtnPp9hJX0VZwz1pN3Ynolpk/Qbu6rEWAvoU1sEqY1mXr9uo/+kEfeGbQ== dependencies: get-object "^0.2.0" get-value "^3.0.1" @@ -5557,9 +5557,9 @@ integrity sha512-7GgtHCs/QZrBrDzgIJnQtuSvhFSwhyYSI2uafSwZoNt1iOGhEN5fwNrQMjtONyHm9+/LoA4453jH0CMYcr06Pg== "@types/node@>=8.1.0": - version "20.11.2" - resolved "https://registry.yarnpkg.com/@types/node/-/node-20.11.2.tgz#39cea3fe02fbbc2f80ed283e94e1d24f2d3856fb" - integrity sha512-cZShBaVa+UO1LjWWBPmWRR4+/eY/JR/UIEcDlVsw3okjWEu+rB7/mH6X3B/L+qJVHDLjk9QW/y2upp9wp1yDXA== + version "20.11.6" + resolved "https://registry.yarnpkg.com/@types/node/-/node-20.11.6.tgz#6adf4241460e28be53836529c033a41985f85b6e" + integrity sha512-+EOokTnksGVgip2PbYbr3xnR7kZigh4LbybAfBAw5BpnQ+FqBYUsvCEjYd70IXKlbohQ64mzEYmMtlWUY8q//Q== dependencies: undici-types "~5.26.4" @@ -9497,9 +9497,9 @@ dotenv@8.6.0, dotenv@^8.2.0: integrity sha512-IrPdXQsk2BbzvCBGBOTmmSH5SodmqZNt4ERAZDmW4CT+tL8VtvinqywuANaFu4bOMWki16nqf0e4oC0QIaDr/g== dotenv@^16.3.1: - version "16.3.1" - resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.3.1.tgz#369034de7d7e5b120972693352a3bf112172cc3e" - integrity sha512-IPzF4w4/Rd94bA9imS68tZBaYyBWSCE47V1RGuMrB94iyTOIEwRmVL2x/4An+6mETpLrKJ5hQkB8W4kFAadeIQ== + version "16.4.0" + resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.4.0.tgz#ac21c3fcaad2e7832a1cd0c0e4e8e52225ecda0e" + integrity sha512-WvImr5kpN5NGNn7KaDjJnLTh5rDVLZiDf/YLA8T1ZEZEBZNEDOE+mnkS0PVjPax8ZxBP5zC5SLMB3/9VV5de9g== dotenv@~10.0.0: version "10.0.0" @@ -17426,11 +17426,12 @@ postgres-interval@^1.1.0: xtend "^4.0.0" posthog-js@^1.13.4: - version "1.100.0" - resolved "https://registry.yarnpkg.com/posthog-js/-/posthog-js-1.100.0.tgz#687b9a6e4ed226aa6572f4040b418ea0c8b3d353" - integrity sha512-r2XZEiHQ9mBK7D1G9k57I8uYZ2kZTAJ0OCX6K/OOdCWN8jKPhw3h5F9No5weilP6eVAn+hrsy7NvPV7SCX7gMg== + version "1.101.0" + resolved "https://registry.yarnpkg.com/posthog-js/-/posthog-js-1.101.0.tgz#00e0fc6e164addd52b1738f087996bb0d6685943" + integrity sha512-mzwYSSWr9FdEMDeVpc+diLfc85+10r/LgELGtsW/HaYk+0du/GEql6szpqG8YXMMgb2dE4dnj0JICZFIJd7K3w== dependencies: fflate "^0.4.1" + preact "^10.19.3" posthog-js@^1.36.0: version "1.96.1" @@ -17676,6 +17677,11 @@ pprof-format@^2.0.7: resolved "https://registry.yarnpkg.com/pprof-format/-/pprof-format-2.0.7.tgz#526e4361f8b37d16b2ec4bb0696b5292de5046a4" integrity sha512-1qWaGAzwMpaXJP9opRa23nPnt2Egi7RMNoNBptEE/XwHbcn4fC2b/4U4bKc5arkGkIh2ZabpF2bEb+c5GNHEKA== +preact@^10.19.3: + version "10.19.3" + resolved "https://registry.yarnpkg.com/preact/-/preact-10.19.3.tgz#7a7107ed2598a60676c943709ea3efb8aaafa899" + integrity sha512-nHHTeFVBTHRGxJXKkKu5hT8C/YWBkPso4/Gad6xuj5dbptt9iF9NZr9pHbPhBrnT2klheu7mHTxTZ/LjwJiEiQ== + precinct@^8.1.0: version "8.3.1" resolved "https://registry.yarnpkg.com/precinct/-/precinct-8.3.1.tgz#94b99b623df144eed1ce40e0801c86078466f0dc"