Merge branch 'develop' into cypress-testing

This commit is contained in:
Mitch-Budibase 2022-05-17 10:34:54 +01:00
commit 92e6b40b81
44 changed files with 1846 additions and 280 deletions

View File

@ -1,5 +1,5 @@
{ {
"version": "1.0.159-alpha.0", "version": "1.0.164-alpha.0",
"npmClient": "yarn", "npmClient": "yarn",
"packages": [ "packages": [
"packages/*" "packages/*"

View File

@ -3,6 +3,8 @@
"private": true, "private": true,
"devDependencies": { "devDependencies": {
"@rollup/plugin-json": "^4.0.2", "@rollup/plugin-json": "^4.0.2",
"@types/mongodb": "3.6.3",
"@typescript-eslint/parser": "4.28.0",
"babel-eslint": "^10.0.3", "babel-eslint": "^10.0.3",
"eslint": "^7.28.0", "eslint": "^7.28.0",
"eslint-plugin-cypress": "^2.11.3", "eslint-plugin-cypress": "^2.11.3",
@ -16,7 +18,6 @@
"rimraf": "^3.0.2", "rimraf": "^3.0.2",
"rollup-plugin-replace": "^2.2.0", "rollup-plugin-replace": "^2.2.0",
"svelte": "^3.38.2", "svelte": "^3.38.2",
"@typescript-eslint/parser": "4.28.0",
"typescript": "4.5.5" "typescript": "4.5.5"
}, },
"scripts": { "scripts": {

View File

@ -1,6 +1,6 @@
{ {
"name": "@budibase/backend-core", "name": "@budibase/backend-core",
"version": "1.0.159-alpha.0", "version": "1.0.164-alpha.0",
"description": "Budibase backend core libraries used in server and worker", "description": "Budibase backend core libraries used in server and worker",
"main": "src/index.js", "main": "src/index.js",
"author": "Budibase", "author": "Budibase",

View File

@ -1,7 +1,7 @@
const google = require("../google") const google = require("../google")
const { Cookies, Configs } = require("../../../constants") const { Cookies, Configs } = require("../../../constants")
const { clearCookie, getCookie } = require("../../../utils") const { clearCookie, getCookie } = require("../../../utils")
const { getScopedConfig, getPlatformUrl } = require("../../../db/utils") const { getScopedConfig } = require("../../../db/utils")
const { doWithDB } = require("../../../db") const { doWithDB } = require("../../../db")
const environment = require("../../../environment") const environment = require("../../../environment")
const { getGlobalDB } = require("../../../tenancy") const { getGlobalDB } = require("../../../tenancy")
@ -21,20 +21,28 @@ async function fetchGoogleCreds() {
) )
} }
async function platformUrl() { async function getPlatformUrl() {
let platformUrl = environment.PLATFORM_URL || "http://localhost:10000"
const db = getGlobalDB() const db = getGlobalDB()
const publicConfig = await getScopedConfig(db, { const settings = await getScopedConfig(db, {
type: Configs.SETTINGS, type: Configs.SETTINGS,
}) })
return getPlatformUrl(publicConfig)
// self hosted - check for platform url override
if (settings && settings.platformUrl) {
platformUrl = settings.platformUrl
}
return platformUrl
} }
async function preAuth(passport, ctx, next) { async function preAuth(passport, ctx, next) {
// get the relevant config // get the relevant config
const googleConfig = await fetchGoogleCreds() const googleConfig = await fetchGoogleCreds()
const platUrl = await platformUrl() const platformUrl = await getPlatformUrl()
let callbackUrl = `${platUrl}/api/global/auth/datasource/google/callback` let callbackUrl = `${platformUrl}/api/global/auth/datasource/google/callback`
const strategy = await google.strategyFactory(googleConfig, callbackUrl) const strategy = await google.strategyFactory(googleConfig, callbackUrl)
if (!ctx.query.appId || !ctx.query.datasourceId) { if (!ctx.query.appId || !ctx.query.datasourceId) {
@ -51,9 +59,9 @@ async function preAuth(passport, ctx, next) {
async function postAuth(passport, ctx, next) { async function postAuth(passport, ctx, next) {
// get the relevant config // get the relevant config
const config = await fetchGoogleCreds() const config = await fetchGoogleCreds()
const platUrl = await platformUrl() const platformUrl = await getPlatformUrl()
let callbackUrl = `${platUrl}/api/global/auth/datasource/google/callback` let callbackUrl = `${platformUrl}/api/global/auth/datasource/google/callback`
const strategy = await google.strategyFactory( const strategy = await google.strategyFactory(
config, config,
callbackUrl, callbackUrl,

View File

@ -71,7 +71,7 @@ describe("oidc", () => {
describe("authenticate", () => { describe("authenticate", () => {
afterEach(() => { afterEach(() => {
jest.clearAllMocks(); jest.clearAllMocks()
}); });
// mock third party common authentication // mock third party common authentication
@ -80,10 +80,10 @@ describe("oidc", () => {
// mock the passport callback // mock the passport callback
const mockDone = jest.fn() const mockDone = jest.fn()
const mockSaveUserFn = jest.fn()
async function doAuthenticate() { async function doAuthenticate() {
const oidc = require("../oidc") const oidc = require("../oidc")
const mockSaveUserFn = jest.fn()
const authenticate = await oidc.buildVerifyFn(mockSaveUserFn) const authenticate = await oidc.buildVerifyFn(mockSaveUserFn)
await authenticate( await authenticate(
@ -105,11 +105,13 @@ describe("oidc", () => {
expect(authenticateThirdParty).toHaveBeenCalledWith( expect(authenticateThirdParty).toHaveBeenCalledWith(
user, user,
false, false,
mockDone) mockDone,
mockSaveUserFn,
)
} }
it("delegates authentication to third party common", async () => { it("delegates authentication to third party common", async () => {
doTest() await doTest()
}) })
it("uses JWT email to get email", async () => { it("uses JWT email to get email", async () => {
@ -118,7 +120,7 @@ describe("oidc", () => {
email : "mock@budibase.com" email : "mock@budibase.com"
} }
doTest() await doTest()
}) })
it("uses JWT username to get email", async () => { it("uses JWT username to get email", async () => {
@ -127,7 +129,7 @@ describe("oidc", () => {
preferred_username : "mock@budibase.com" preferred_username : "mock@budibase.com"
} }
doTest() await doTest()
}) })
it("uses JWT invalid username to get email", async () => { it("uses JWT invalid username to get email", async () => {

View File

@ -1,7 +1,7 @@
{ {
"name": "@budibase/bbui", "name": "@budibase/bbui",
"description": "A UI solution used in the different Budibase projects.", "description": "A UI solution used in the different Budibase projects.",
"version": "1.0.159-alpha.0", "version": "1.0.164-alpha.0",
"license": "MPL-2.0", "license": "MPL-2.0",
"svelte": "src/index.js", "svelte": "src/index.js",
"module": "dist/bbui.es.js", "module": "dist/bbui.es.js",
@ -38,7 +38,7 @@
], ],
"dependencies": { "dependencies": {
"@adobe/spectrum-css-workflow-icons": "^1.2.1", "@adobe/spectrum-css-workflow-icons": "^1.2.1",
"@budibase/string-templates": "^1.0.159-alpha.0", "@budibase/string-templates": "^1.0.164-alpha.0",
"@spectrum-css/actionbutton": "^1.0.1", "@spectrum-css/actionbutton": "^1.0.1",
"@spectrum-css/actiongroup": "^1.0.1", "@spectrum-css/actiongroup": "^1.0.1",
"@spectrum-css/avatar": "^3.0.2", "@spectrum-css/avatar": "^3.0.2",

View File

@ -0,0 +1,68 @@
<script>
import "@spectrum-css/fieldgroup/dist/index-vars.css"
import "@spectrum-css/radio/dist/index-vars.css"
import { createEventDispatcher } from "svelte"
export let direction = "vertical"
export let value = []
export let options = []
export let error = null
export let disabled = false
export let getOptionLabel = option => option
export let getOptionValue = option => option
const dispatch = createEventDispatcher()
const onChange = e => {
let tempValue = value
let isChecked = e.target.checked
if (!tempValue.includes(e.target.value) && isChecked) {
tempValue.push(e.target.value)
}
value = tempValue
dispatch(
"change",
tempValue.filter(val => val !== e.target.value || isChecked)
)
}
</script>
<div class={`spectrum-FieldGroup spectrum-FieldGroup--${direction}`}>
{#if options && Array.isArray(options)}
{#each options as option}
<div
title={getOptionLabel(option)}
class="spectrum-Checkbox spectrum-FieldGroup-item"
class:is-invalid={!!error}
>
<label
class="spectrum-Checkbox spectrum-Checkbox--sizeM spectrum-FieldGroup-item"
>
<input
on:change={onChange}
value={getOptionValue(option)}
type="checkbox"
class="spectrum-Checkbox-input"
{disabled}
checked={value.includes(getOptionValue(option))}
/>
<span class="spectrum-Checkbox-box">
<svg
class="spectrum-Icon spectrum-UIIcon-Checkmark100 spectrum-Checkbox-checkmark"
focusable="false"
aria-hidden="true"
>
<use xlink:href="#spectrum-css-icon-Checkmark100" />
</svg>
</span>
<span class="spectrum-Checkbox-label">{getOptionLabel(option)}</span>
</label>
</div>
{/each}
{/if}
</div>
<style>
.spectrum-Checkbox-input {
opacity: 0;
}
</style>

View File

@ -58,6 +58,11 @@
if (timeOnly) { if (timeOnly) {
newValue = `2000-01-01T${newValue.split("T")[1]}` newValue = `2000-01-01T${newValue.split("T")[1]}`
} }
// date only, offset for timezone so always right date
else if (!enableTime) {
const offset = dates[0].getTimezoneOffset() * 60000
newValue = new Date(dates[0].getTime() - offset).toISOString()
}
dispatch("change", newValue) dispatch("change", newValue)
} }

View File

@ -43,7 +43,7 @@
return return
} }
searchTerm = null searchTerm = null
open = true open = !open
} }
const getSortedOptions = (options, getLabel, sort) => { const getSortedOptions = (options, getLabel, sort) => {
@ -71,105 +71,73 @@
} }
</script> </script>
<button <div use:clickOutside={() => (open = false)}>
{id} <button
class="spectrum-Picker spectrum-Picker--sizeM" {id}
class:spectrum-Picker--quiet={quiet} class="spectrum-Picker spectrum-Picker--sizeM"
{disabled} class:spectrum-Picker--quiet={quiet}
class:is-invalid={!!error} {disabled}
class:is-open={open} class:is-invalid={!!error}
aria-haspopup="listbox" class:is-open={open}
on:mousedown={onClick} aria-haspopup="listbox"
> on:mousedown={onClick}
{#if fieldIcon}
<span class="icon-Placeholder-Padding">
<img src={fieldIcon} alt="icon" width="20" height="15" />
</span>
{/if}
<span
class="spectrum-Picker-label"
class:is-placeholder={isPlaceholder}
class:auto-width={autoWidth}
> >
{fieldText} {#if fieldIcon}
</span> <span class="icon-Placeholder-Padding">
{#if error} <img src={fieldIcon} alt="icon" width="20" height="15" />
</span>
{/if}
<span
class="spectrum-Picker-label"
class:is-placeholder={isPlaceholder}
class:auto-width={autoWidth}
>
{fieldText}
</span>
{#if error}
<svg
class="spectrum-Icon spectrum-Icon--sizeM spectrum-Picker-validationIcon"
focusable="false"
aria-hidden="true"
aria-label="Folder"
>
<use xlink:href="#spectrum-icon-18-Alert" />
</svg>
{/if}
<svg <svg
class="spectrum-Icon spectrum-Icon--sizeM spectrum-Picker-validationIcon" class="spectrum-Icon spectrum-UIIcon-ChevronDown100 spectrum-Picker-menuIcon"
focusable="false" focusable="false"
aria-hidden="true" aria-hidden="true"
aria-label="Folder"
> >
<use xlink:href="#spectrum-icon-18-Alert" /> <use xlink:href="#spectrum-css-icon-Chevron100" />
</svg> </svg>
{/if} </button>
<svg {#if open}
class="spectrum-Icon spectrum-UIIcon-ChevronDown100 spectrum-Picker-menuIcon" <div
focusable="false" transition:fly|local={{ y: -20, duration: 200 }}
aria-hidden="true" class="spectrum-Popover spectrum-Popover--bottom spectrum-Picker-popover is-open"
> class:auto-width={autoWidth}
<use xlink:href="#spectrum-css-icon-Chevron100" /> >
</svg> {#if autocomplete}
</button> <Search
{#if open} value={searchTerm}
<div on:change={event => (searchTerm = event.detail)}
use:clickOutside={() => (open = false)} {disabled}
transition:fly|local={{ y: -20, duration: 200 }} placeholder="Search"
class="spectrum-Popover spectrum-Popover--bottom spectrum-Picker-popover is-open" />
class:auto-width={autoWidth}
>
{#if autocomplete}
<Search
value={searchTerm}
on:change={event => (searchTerm = event.detail)}
{disabled}
placeholder="Search"
/>
{/if}
<ul class="spectrum-Menu" role="listbox">
{#if placeholderOption}
<li
class="spectrum-Menu-item placeholder"
class:is-selected={isPlaceholder}
role="option"
aria-selected="true"
tabindex="0"
on:click={() => onSelectOption(null)}
>
<span class="spectrum-Menu-itemLabel">{placeholderOption}</span>
<svg
class="spectrum-Icon spectrum-UIIcon-Checkmark100 spectrum-Menu-checkmark spectrum-Menu-itemIcon"
focusable="false"
aria-hidden="true"
>
<use xlink:href="#spectrum-css-icon-Checkmark100" />
</svg>
</li>
{/if} {/if}
{#if filteredOptions.length} <ul class="spectrum-Menu" role="listbox">
{#each filteredOptions as option, idx} {#if placeholderOption}
<li <li
class="spectrum-Menu-item" class="spectrum-Menu-item placeholder"
class:is-selected={isOptionSelected(getOptionValue(option, idx))} class:is-selected={isPlaceholder}
role="option" role="option"
aria-selected="true" aria-selected="true"
tabindex="0" tabindex="0"
on:click={() => onSelectOption(getOptionValue(option, idx))} on:click={() => onSelectOption(null)}
> >
{#if getOptionIcon(option, idx)} <span class="spectrum-Menu-itemLabel">{placeholderOption}</span>
<span class="icon-Padding">
<img
src={getOptionIcon(option, idx)}
alt="icon"
width="20"
height="15"
/>
</span>
{/if}
<span class="spectrum-Menu-itemLabel">
{getOptionLabel(option, idx)}
</span>
<svg <svg
class="spectrum-Icon spectrum-UIIcon-Checkmark100 spectrum-Menu-checkmark spectrum-Menu-itemIcon" class="spectrum-Icon spectrum-UIIcon-Checkmark100 spectrum-Menu-checkmark spectrum-Menu-itemIcon"
focusable="false" focusable="false"
@ -178,11 +146,44 @@
<use xlink:href="#spectrum-css-icon-Checkmark100" /> <use xlink:href="#spectrum-css-icon-Checkmark100" />
</svg> </svg>
</li> </li>
{/each} {/if}
{/if} {#if filteredOptions.length}
</ul> {#each filteredOptions as option, idx}
</div> <li
{/if} class="spectrum-Menu-item"
class:is-selected={isOptionSelected(getOptionValue(option, idx))}
role="option"
aria-selected="true"
tabindex="0"
on:click={() => onSelectOption(getOptionValue(option, idx))}
>
{#if getOptionIcon(option, idx)}
<span class="icon-Padding">
<img
src={getOptionIcon(option, idx)}
alt="icon"
width="20"
height="15"
/>
</span>
{/if}
<span class="spectrum-Menu-itemLabel">
{getOptionLabel(option, idx)}
</span>
<svg
class="spectrum-Icon spectrum-UIIcon-Checkmark100 spectrum-Menu-checkmark spectrum-Menu-itemIcon"
focusable="false"
aria-hidden="true"
>
<use xlink:href="#spectrum-css-icon-Checkmark100" />
</svg>
</li>
{/each}
{/if}
</ul>
</div>
{/if}
</div>
<style> <style>
.spectrum-Popover { .spectrum-Popover {

View File

@ -1,6 +1,6 @@
<script> <script>
import "@spectrum-css/textfield/dist/index-vars.css" import "@spectrum-css/textfield/dist/index-vars.css"
import { createEventDispatcher } from "svelte" import { createEventDispatcher, onMount } from "svelte"
export let value = null export let value = null
export let placeholder = null export let placeholder = null
@ -13,8 +13,11 @@
export let quiet = false export let quiet = false
export let dataCy export let dataCy
export let align export let align
export let autofocus = false
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
let field
let focus = false let focus = false
const updateValue = newValue => { const updateValue = newValue => {
@ -58,6 +61,11 @@
updateValue(event.target.value) updateValue(event.target.value)
} }
} }
onMount(() => {
focus = autofocus
if (focus) field.focus()
})
</script> </script>
<div <div
@ -77,6 +85,7 @@
</svg> </svg>
{/if} {/if}
<input <input
bind:this={field}
{disabled} {disabled}
{readonly} {readonly}
{id} {id}

View File

@ -3,6 +3,7 @@ export { default as CoreSelect } from "./Select.svelte"
export { default as CoreMultiselect } from "./Multiselect.svelte" export { default as CoreMultiselect } from "./Multiselect.svelte"
export { default as CoreCheckbox } from "./Checkbox.svelte" export { default as CoreCheckbox } from "./Checkbox.svelte"
export { default as CoreRadioGroup } from "./RadioGroup.svelte" export { default as CoreRadioGroup } from "./RadioGroup.svelte"
export { default as CoreCheckboxGroup } from "./CheckboxGroup.svelte"
export { default as CoreTextArea } from "./TextArea.svelte" export { default as CoreTextArea } from "./TextArea.svelte"
export { default as CoreCombobox } from "./Combobox.svelte" export { default as CoreCombobox } from "./Combobox.svelte"
export { default as CoreSwitch } from "./Switch.svelte" export { default as CoreSwitch } from "./Switch.svelte"

View File

@ -14,6 +14,7 @@
export let updateOnChange = true export let updateOnChange = true
export let quiet = false export let quiet = false
export let dataCy export let dataCy
export let autofocus
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
const onChange = e => { const onChange = e => {
@ -33,6 +34,7 @@
{placeholder} {placeholder}
{type} {type}
{quiet} {quiet}
{autofocus}
on:change={onChange} on:change={onChange}
on:click on:click
on:input on:input

File diff suppressed because one or more lines are too long

View File

@ -1,4 +1,5 @@
import filterTests from '../support/filterTests' import filterTests from '../support/filterTests'
const interact = require('../support/interact')
filterTests(['smoke', 'all'], () => { filterTests(['smoke', 'all'], () => {
context("Create an Application", () => { context("Create an Application", () => {
@ -10,14 +11,14 @@ filterTests(['smoke', 'all'], () => {
if (!(Cypress.env("TEST_ENV"))) { if (!(Cypress.env("TEST_ENV"))) {
it("should show the new user UI/UX", () => { it("should show the new user UI/UX", () => {
cy.visit(`${Cypress.config().baseUrl}/builder`) cy.visit(`${Cypress.config().baseUrl}/builder/portal/apps/create`) //added /portal/apps/create
cy.get(`[data-cy="create-app-btn"]`).contains('Start from scratch').should("exist") cy.get(interact.CREATE_APP_BUTTON).contains('Start from scratch').should("exist")
cy.get(`[data-cy="import-app-btn"]`).should("exist") cy.get(interact.CREATE_APP_BUTTON).should("exist")
cy.get(".template-category-filters").should("exist") cy.get(interact.TEMPLATE_CATEGORY_FILTER).should("exist")
cy.get(".template-categories").should("exist") cy.get(interact.TEMPLATE_CATEGORY).should("exist")
cy.get(".appTable").should("not.exist") cy.get(interact.APP_TABLE).should("not.exist")
}) })
} }
@ -29,21 +30,21 @@ filterTests(['smoke', 'all'], () => {
.its("body") .its("body")
.then(val => { .then(val => {
if (val.length > 0) { if (val.length > 0) {
cy.get(".spectrum-Button").contains("Templates").click({force: true}) cy.get(interact.SPECTRUM_BUTTON_TEMPLATE).contains("Templates").click({force: true})
} }
}) })
cy.get(".template-category-filters").should("exist") cy.get(interact.TEMPLATE_CATEGORY_FILTER).should("exist")
cy.get(".template-categories").should("exist") cy.get(interact.TEMPLATE_CATEGORY).should("exist")
cy.get(".template-category").its('length').should('be.gt', 1) cy.get(interact.TEMPLATE_CATEGORY_ACTIONGROUP).its('length').should('be.gt', 1)
cy.get(".template-category-filters .spectrum-ActionButton").its('length').should('be.gt', 2) cy.get(interact.TEMPLATE_CATEGORY_FILTER_ACTIONBUTTON).its('length').should('be.gt', 2)
cy.get(".template-category-filters .spectrum-ActionButton").eq(1).click() cy.get(interact.TEMPLATE_CATEGORY_FILTER_ACTIONBUTTON).eq(1).click()
cy.get(".template-category").should('have.length', 1) cy.get(interact.TEMPLATE_CATEGORY_ACTIONGROUP).should('have.length', 1)
cy.get(".template-category-filters .spectrum-ActionButton").eq(0).click() cy.get(interact.TEMPLATE_CATEGORY_FILTER_ACTIONBUTTON).eq(0).click()
cy.get(".template-category").its('length').should('be.gt', 1) cy.get(interact.TEMPLATE_CATEGORY_ACTIONGROUP).its('length').should('be.gt', 1)
}) })
it("should enforce a valid url before submission", () => { it("should enforce a valid url before submission", () => {
@ -51,37 +52,40 @@ filterTests(['smoke', 'all'], () => {
cy.wait(500) cy.wait(500)
// Start create app process. If apps already exist, click second button // Start create app process. If apps already exist, click second button
cy.get(`[data-cy="create-app-btn"]`).click({ force: true }) cy.get(interact.CREATE_APP_BUTTON).click({ force: true })
cy.request(`${Cypress.config().baseUrl}/api/applications?status=all`) cy.request(`${Cypress.config().baseUrl}/api/applications?status=all`)
.its("body") .its("body")
.then(val => { .then(val => {
if (val.length > 0) { if (val.length > 0) {
cy.get(`[data-cy="create-app-btn"]`).click({ force: true }) cy.get(interact.CREATE_APP_BUTTON).click({ force: true })
} }
}) })
const appName = "Cypress Tests" const appName = "Cypress Tests"
cy.get(".spectrum-Modal").within(() => { cy.get(interact.SPECTRUM_MODAL).within(() => {
cy.get(interact.APP_NAME_INPUT).eq(0).should('have.focus')
//Auto fill //Auto fill
cy.get("input").eq(0).type(appName).should("have.value", appName).blur() cy.get(interact.APP_NAME_INPUT).eq(0).clear()
cy.get("input").eq(1).should("have.value", "/cypress-tests") cy.get(interact.APP_NAME_INPUT).eq(0).type(appName).should("have.value", appName).blur()
cy.get(".spectrum-ButtonGroup").contains("Create app").should('not.be.disabled') cy.get(interact.APP_NAME_INPUT).eq(1).should("have.value", "/cypress-tests")
cy.get(interact.SPECTRUM_BUTTON_GROUP).contains("Create app").should('not.be.disabled')
//Empty the app url - disabled create //Empty the app url - disabled create
cy.get("input").eq(1).clear().blur() cy.get(interact.APP_NAME_INPUT).eq(1).clear().blur()
cy.get(".spectrum-ButtonGroup").contains("Create app").should('be.disabled') cy.get(interact.SPECTRUM_BUTTON_GROUP).contains("Create app").should('be.disabled')
//Invalid url //Invalid url
cy.get("input").eq(1).type("/new app-url").blur() cy.get(interact.APP_NAME_INPUT).eq(1).type("/new app-url").blur()
cy.get(".spectrum-ButtonGroup").contains("Create app").should('be.disabled') cy.get(interact.SPECTRUM_BUTTON_GROUP).contains("Create app").should('be.disabled')
//Specifically alter the url //Specifically alter the url
cy.get("input").eq(1).clear() cy.get(interact.APP_NAME_INPUT).eq(1).clear()
cy.get("input").eq(1).type("another-app-name").blur() cy.get(interact.APP_NAME_INPUT).eq(1).type("another-app-name").blur()
cy.get("input").eq(1).should("have.value", "/another-app-name") cy.get(interact.APP_NAME_INPUT).eq(1).should("have.value", "/another-app-name")
cy.get("input").eq(0).should("have.value", appName) cy.get(interact.APP_NAME_INPUT).eq(0).should("have.value", appName)
cy.get(".spectrum-ButtonGroup").contains("Create app").should('not.be.disabled') cy.get(interact.SPECTRUM_BUTTON_GROUP).contains("Create app").should('not.be.disabled')
}) })
}) })
@ -97,6 +101,75 @@ filterTests(['smoke', 'all'], () => {
cy.deleteApp(appName) cy.deleteApp(appName)
}) })
it("should create the first application from scratch with a default name", () => {
cy.createApp()
cy.visit(`${Cypress.config().baseUrl}/builder`)
cy.wait(1000)
cy.applicationInAppTable("My app")
cy.deleteApp("My app")
})
it("should create the first application from scratch, using the users first name as the default app name", () => {
cy.visit(`${Cypress.config().baseUrl}/builder`)
cy.updateUserInformation("Ted", "Userman")
cy.createApp()
cy.visit(`${Cypress.config().baseUrl}/builder`)
cy.wait(1000)
cy.applicationInAppTable("Teds app")
cy.deleteApp("Teds app")
//Accomodate names that end in 'S'
cy.updateUserInformation("Chris", "Userman")
cy.createApp()
cy.visit(`${Cypress.config().baseUrl}/builder`)
cy.wait(1000)
cy.applicationInAppTable("Chris app")
cy.deleteApp("Chris app")
cy.updateUserInformation("", "")
})
it("should create an application from an export", () => {
const exportedApp = 'cypress/fixtures/exported-app.txt'
cy.importApp(exportedApp, "")
cy.visit(`${Cypress.config().baseUrl}/builder`)
cy.applicationInAppTable("My app")
cy.get(".appTable .name").eq(0).click()
cy.deleteApp("My app")
})
it("should create an application from an export, using the users first name as the default app name", () => {
const exportedApp = 'cypress/fixtures/exported-app.txt'
cy.updateUserInformation("Ted", "Userman")
cy.importApp(exportedApp, "")
cy.visit(`${Cypress.config().baseUrl}/builder`)
cy.applicationInAppTable("Teds app")
cy.get(".appTable .name").eq(0).click()
cy.deleteApp("Teds app")
cy.updateUserInformation("", "")
})
it("should generate the first application from a template", () => { it("should generate the first application from a template", () => {
cy.visit(`${Cypress.config().baseUrl}/builder`) cy.visit(`${Cypress.config().baseUrl}/builder`)
cy.wait(500) cy.wait(500)
@ -106,15 +179,15 @@ filterTests(['smoke', 'all'], () => {
.its("body") .its("body")
.then(val => { .then(val => {
if (val.length > 0) { if (val.length > 0) {
cy.get(`[data-cy="create-app-btn"]`).click({ force: true }) cy.get(interact.CREATE_APP_BUTTON).click({ force: true })
} }
}) })
cy.get(".template-category-filters").should("exist") cy.get(interact.TEMPLATE_CATEGORY_FILTER).should("exist")
cy.get(".template-categories").should("exist") cy.get(interact.TEMPLATE_CATEGORY).should("exist")
// Select template // Select template
cy.get('.template-category').eq(0).within(() => { cy.get(interact.TEMPLATE_CATEGORY_ACTIONGROUP).eq(0).within(() => {
const card = cy.get('.template-card').eq(0).should("exist"); const card = cy.get('.template-card').eq(0).should("exist");
const cardOverlay = card.get('.template-thumbnail-action-overlay').should("exist") const cardOverlay = card.get('.template-thumbnail-action-overlay').should("exist")
cardOverlay.invoke("show") cardOverlay.invoke("show")
@ -128,8 +201,8 @@ filterTests(['smoke', 'all'], () => {
templateName.invoke('text') templateName.invoke('text')
.then(templateNameText => { .then(templateNameText => {
const templateNameParsed = "/"+templateNameText.toLowerCase().replace(/\s+/g, "-") const templateNameParsed = "/"+templateNameText.toLowerCase().replace(/\s+/g, "-")
cy.get(".spectrum-Modal input").eq(0).should("have.value", templateNameText) cy.get(interact.SPECTRUM_MODAL_INPUT).eq(0).should("have.value", templateNameText)
cy.get(".spectrum-Modal input").eq(1).should("have.value", templateNameParsed) cy.get(interact.SPECTRUM_MODAL_INPUT).eq(1).should("have.value", templateNameParsed)
cy.get(".spectrum-Modal .spectrum-ButtonGroup").contains("Create app").click() cy.get(".spectrum-Modal .spectrum-ButtonGroup").contains("Create app").click()
cy.wait(5000) cy.wait(5000)

View File

@ -39,6 +39,71 @@ Cypress.Commands.add("closeModal", () => {
}) })
}) })
Cypress.Commands.add("importApp", (exportFilePath, name) => {
cy.visit(`${Cypress.config().baseUrl}/builder`)
cy.request(`${Cypress.config().baseUrl}/api/applications?status=all`)
.its("body")
.then(val => {
if (val.length > 0) {
cy.get(`[data-cy="create-app-btn"]`).click({ force: true })
cy.wait(500)
}
cy.get(`[data-cy="import-app-btn"]`).click({ force: true })
})
cy.get(".spectrum-Modal").within(() => {
cy.get("input").eq(1).should("have.focus")
cy.get(".spectrum-Dropzone").selectFile(exportFilePath, {
action: "drag-drop",
})
cy.get(".gallery .filename").contains("exported-app.txt")
if (name && name != "") {
cy.get("input").eq(0).type(name).should("have.value", name).blur()
}
cy.get(".confirm-wrap button")
.should("not.be.disabled")
.click({ force: true })
cy.wait(5000)
})
})
Cypress.Commands.add("updateUserInformation", (firstName, lastName) => {
cy.get(".user-dropdown .avatar > .icon").click({ force: true })
cy.get(".spectrum-Popover[data-cy='user-menu']").within(() => {
cy.get("li[data-cy='user-info']").click({ force: true })
})
cy.get(".spectrum-Modal.is-open").within(() => {
cy.get("[data-cy='user-first-name']").clear()
if (!firstName || firstName == "") {
cy.get("[data-cy='user-first-name']").invoke("val").should("be.empty")
} else {
cy.get("[data-cy='user-first-name']")
.type(firstName)
.should("have.value", firstName)
.blur()
}
cy.get("[data-cy='user-last-name']").clear()
if (!lastName || lastName == "") {
cy.get("[data-cy='user-last-name']").invoke("val").should("be.empty")
} else {
cy.get("[data-cy='user-last-name']")
.type(lastName)
.should("have.value", lastName)
.blur()
}
cy.get("button").contains("Update information").click({ force: true })
})
})
Cypress.Commands.add("createApp", (name, addDefaultTable) => { Cypress.Commands.add("createApp", (name, addDefaultTable) => {
const shouldCreateDefaultTable = const shouldCreateDefaultTable =
typeof addDefaultTable != "boolean" ? true : addDefaultTable typeof addDefaultTable != "boolean" ? true : addDefaultTable
@ -57,7 +122,11 @@ Cypress.Commands.add("createApp", (name, addDefaultTable) => {
}) })
cy.get(".spectrum-Modal").within(() => { cy.get(".spectrum-Modal").within(() => {
cy.get("input").eq(0).type(name).should("have.value", name).blur() cy.get("input").eq(0).should("have.focus")
if (name && name != "") {
cy.get("input").eq(0).clear()
cy.get("input").eq(0).type(name).should("have.value", name).blur()
}
cy.get(".spectrum-ButtonGroup") cy.get(".spectrum-ButtonGroup")
.contains("Create app") .contains("Create app")
.click({ force: true }) .click({ force: true })

View File

@ -0,0 +1,13 @@
// createApp test
export const CREATE_APP_BUTTON = '[data-cy="create-app-btn"]'
export const TEMPLATE_CATEGORY_FILTER = ".template-category-filters"
export const TEMPLATE_CATEGORY = ".template-categories"
export const APP_TABLE = ".appTable"
export const SPECTRUM_BUTTON_TEMPLATE = ".spectrum-Button"
export const TEMPLATE_CATEGORY_ACTIONGROUP = ".template-category"
export const TEMPLATE_CATEGORY_FILTER_ACTIONBUTTON =
".template-category-filters .spectrum-ActionButton"
export const SPECTRUM_MODAL = ".spectrum-Modal"
export const APP_NAME_INPUT = "input" // we need to update this with atribute cy-data
export const SPECTRUM_BUTTON_GROUP = ".spectrum-ButtonGroup"
export const SPECTRUM_MODAL_INPUT = ".spectrum-Modal input"

View File

@ -1,6 +1,6 @@
{ {
"name": "@budibase/builder", "name": "@budibase/builder",
"version": "1.0.159-alpha.0", "version": "1.0.164-alpha.0",
"license": "GPL-3.0", "license": "GPL-3.0",
"private": true, "private": true,
"scripts": { "scripts": {
@ -67,10 +67,10 @@
} }
}, },
"dependencies": { "dependencies": {
"@budibase/bbui": "^1.0.159-alpha.0", "@budibase/bbui": "^1.0.164-alpha.0",
"@budibase/client": "^1.0.159-alpha.0", "@budibase/client": "^1.0.164-alpha.0",
"@budibase/frontend-core": "^1.0.159-alpha.0", "@budibase/frontend-core": "^1.0.164-alpha.0",
"@budibase/string-templates": "^1.0.159-alpha.0", "@budibase/string-templates": "^1.0.164-alpha.0",
"@sentry/browser": "5.19.1", "@sentry/browser": "5.19.1",
"@spectrum-css/page": "^3.0.1", "@spectrum-css/page": "^3.0.1",
"@spectrum-css/vars": "^3.0.1", "@spectrum-css/vars": "^3.0.1",

View File

@ -12,4 +12,4 @@
} }
</script> </script>
<CopyInput {value} copyValue={fullWebhookURL(value)} /> <CopyInput value={fullWebhookURL(value)} />

View File

@ -3,7 +3,6 @@
export let label = null export let label = null
export let value export let value
export let copyValue
export let dataCy = null export let dataCy = null
const copyToClipboard = val => { const copyToClipboard = val => {
@ -19,7 +18,7 @@
<div data-cy={dataCy}> <div data-cy={dataCy}>
<Input readonly {value} {label} /> <Input readonly {value} {label} />
<div class="icon" on:click={() => copyToClipboard(value || copyValue)}> <div class="icon" on:click={() => copyToClipboard(value)}>
<Icon size="S" name="Copy" /> <Icon size="S" name="Copy" />
</div> </div>
</div> </div>

View File

@ -27,6 +27,14 @@
Personalise the platform by adding your first name and last name. Personalise the platform by adding your first name and last name.
</Body> </Body>
<Input disabled bind:value={$auth.user.email} label="Email" /> <Input disabled bind:value={$auth.user.email} label="Email" />
<Input bind:value={$values.firstName} label="First name" /> <Input
<Input bind:value={$values.lastName} label="Last name" /> bind:value={$values.firstName}
label="First name"
dataCy="user-first-name"
/>
<Input
bind:value={$values.lastName}
label="Last name"
dataCy="user-last-name"
/>
</ModalContent> </ModalContent>

View File

@ -16,13 +16,26 @@
export let template export let template
let creating = false let creating = false
let defaultAppName
const values = writable({ name: "", url: null }) const values = writable({ name: "", url: null })
const validation = createValidationStore() const validation = createValidationStore()
$: validation.check($values) $: validation.check($values)
onMount(async () => { onMount(async () => {
$values.name = resolveAppName(template, $values.name) const lastChar = $auth.user?.firstName
? $auth.user?.firstName[$auth.user?.firstName.length - 1]
: null
defaultAppName =
lastChar && lastChar.toLowerCase() == "s"
? `${$auth.user?.firstName} app`
: `${$auth.user.firstName}s app`
$values.name = resolveAppName(
template,
!$auth.user?.firstName ? "My app" : defaultAppName
)
nameToUrl($values.name) nameToUrl($values.name)
await setupValidation() await setupValidation()
}) })
@ -44,7 +57,7 @@
} }
const resolveAppName = (template, name) => { const resolveAppName = (template, name) => {
if (template && !name) { if (template && !template.fromFile) {
return template.name return template.name
} }
return name ? name.trim() : null return name ? name.trim() : null
@ -83,7 +96,7 @@
} }
data.append("useTemplate", template != null) data.append("useTemplate", template != null)
if (template) { if (template) {
data.append("templateName", template.name) //or here? data.append("templateName", template.name)
data.append("templateKey", template.key) data.append("templateKey", template.key)
data.append("templateFile", $values.file) data.append("templateFile", $values.file)
} }
@ -159,15 +172,14 @@
/> />
{/if} {/if}
<Input <Input
autofocus={true}
bind:value={$values.name} bind:value={$values.name}
disabled={creating} disabled={creating}
error={$validation.touched.name && $validation.errors.name} error={$validation.touched.name && $validation.errors.name}
on:blur={() => ($validation.touched.name = true)} on:blur={() => ($validation.touched.name = true)}
on:change={nameToUrl($values.name)} on:change={nameToUrl($values.name)}
label="Name" label="Name"
placeholder={$auth.user?.firstName placeholder={defaultAppName}
? `${$auth.user.firstName}s app`
: "My app"}
/> />
<span> <span>
<Input <Input

View File

@ -69,7 +69,7 @@
<Layout noPadding> <Layout noPadding>
<div class="header"> <div class="header">
<img alt="logo" src={$organisation.logoUrl || Logo} /> <img alt="logo" src={$organisation.logoUrl || Logo} />
<ActionMenu align="right"> <ActionMenu align="right" dataCy="user-menu">
<div slot="control" class="avatar"> <div slot="control" class="avatar">
<Avatar <Avatar
size="M" size="M"

View File

@ -160,7 +160,7 @@
/> />
</div> </div>
<div class="user-dropdown"> <div class="user-dropdown">
<ActionMenu align="right"> <ActionMenu align="right" dataCy="user-menu">
<div slot="control" class="avatar"> <div slot="control" class="avatar">
<Avatar <Avatar
size="M" size="M"
@ -169,7 +169,11 @@
/> />
<Icon size="XL" name="ChevronDown" /> <Icon size="XL" name="ChevronDown" />
</div> </div>
<MenuItem icon="UserEdit" on:click={() => userInfoModal.show()}> <MenuItem
icon="UserEdit"
on:click={() => userInfoModal.show()}
dataCy={"user-info"}
>
Update user information Update user information
</MenuItem> </MenuItem>
{#if $auth.isBuilder} {#if $auth.isBuilder}

View File

@ -68,6 +68,26 @@
} }
} }
async function deleteSmtp() {
// Delete the SMTP config
try {
await API.deleteConfig({
id: smtpConfig._id,
rev: smtpConfig._rev,
})
smtpConfig = {
config: {},
}
await admin.getChecklist()
notifications.success(`Settings cleared`)
analytics.captureEvent(Events.SMTP.SAVED)
} catch (error) {
notifications.error(
`Failed to clear email settings, reason: ${error?.message || "Unknown"}`
)
}
}
async function fetchSmtp() { async function fetchSmtp() {
loading = true loading = true
try { try {
@ -156,8 +176,15 @@
</div> </div>
{/if} {/if}
</Layout> </Layout>
<div> <div class="spectrum-ButtonGroup spectrum-Settings-buttonGroup">
<Button cta on:click={saveSmtp}>Save</Button> <Button cta on:click={saveSmtp}>Save</Button>
<Button
secondary
on:click={deleteSmtp}
disabled={!$admin.checklist.smtp.checked}
>
Reset
</Button>
</div> </div>
<Divider /> <Divider />
<Layout gap="XS" noPadding> <Layout gap="XS" noPadding>
@ -186,4 +213,8 @@
grid-gap: var(--spacing-l); grid-gap: var(--spacing-l);
align-items: center; align-items: center;
} }
.spectrum-Settings-buttonGroup {
gap: var(--spectrum-global-dimension-static-size-200);
align-items: flex-end;
}
</style> </style>

View File

@ -14,7 +14,10 @@
let options = roles let options = roles
.filter(role => role._id !== "PUBLIC") .filter(role => role._id !== "PUBLIC")
.map(role => ({ value: role._id, label: role.name })) .map(role => ({ value: role._id, label: role.name }))
options.push({ value: NO_ACCESS, label: "No Access" })
if (!user?.builder?.global) {
options.push({ value: NO_ACCESS, label: "No Access" })
}
let selectedRole = user?.roles?.[app?._id] let selectedRole = user?.roles?.[app?._id]
async function updateUserRoles() { async function updateUserRoles() {

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
{ {
"name": "@budibase/cli", "name": "@budibase/cli",
"version": "1.0.159-alpha.0", "version": "1.0.164-alpha.0",
"description": "Budibase CLI, for developers, self hosting and migrations.", "description": "Budibase CLI, for developers, self hosting and migrations.",
"main": "src/index.js", "main": "src/index.js",
"bin": { "bin": {

View File

@ -2338,7 +2338,11 @@
"type": "boolean", "type": "boolean",
"label": "Autocomplete", "label": "Autocomplete",
"key": "autocomplete", "key": "autocomplete",
"defaultValue": false "defaultValue": false,
"dependsOn": {
"setting": "optionsType",
"value": "select"
}
}, },
{ {
"type": "boolean", "type": "boolean",
@ -2346,6 +2350,43 @@
"key": "disabled", "key": "disabled",
"defaultValue": false "defaultValue": false
}, },
{
"type": "select",
"label": "Type",
"key": "optionsType",
"defaultValue": "select",
"placeholder": "Pick an options type",
"options": [
{
"label": "Select",
"value": "select"
},
{
"label": "Checkboxes",
"value": "checkbox"
}
]
},
{
"type": "select",
"label": "Direction",
"key": "direction",
"defaultValue": "vertical",
"options": [
{
"label": "Horizontal",
"value": "horizontal"
},
{
"label": "Vertical",
"value": "vertical"
}
],
"dependsOn": {
"setting": "optionsType",
"value": "checkbox"
}
},
{ {
"type": "select", "type": "select",
"label": "Options source", "label": "Options source",

View File

@ -1,6 +1,6 @@
{ {
"name": "@budibase/client", "name": "@budibase/client",
"version": "1.0.159-alpha.0", "version": "1.0.164-alpha.0",
"license": "MPL-2.0", "license": "MPL-2.0",
"module": "dist/budibase-client.js", "module": "dist/budibase-client.js",
"main": "dist/budibase-client.js", "main": "dist/budibase-client.js",
@ -19,9 +19,9 @@
"dev:builder": "rollup -cw" "dev:builder": "rollup -cw"
}, },
"dependencies": { "dependencies": {
"@budibase/bbui": "^1.0.159-alpha.0", "@budibase/bbui": "^1.0.164-alpha.0",
"@budibase/frontend-core": "^1.0.159-alpha.0", "@budibase/frontend-core": "^1.0.164-alpha.0",
"@budibase/string-templates": "^1.0.159-alpha.0", "@budibase/string-templates": "^1.0.164-alpha.0",
"@spectrum-css/button": "^3.0.3", "@spectrum-css/button": "^3.0.3",
"@spectrum-css/card": "^3.0.3", "@spectrum-css/card": "^3.0.3",
"@spectrum-css/divider": "^1.0.3", "@spectrum-css/divider": "^1.0.3",

View File

@ -1,5 +1,5 @@
<script> <script>
import { CoreMultiselect } from "@budibase/bbui" import { CoreMultiselect, CoreCheckboxGroup } from "@budibase/bbui"
import Field from "./Field.svelte" import Field from "./Field.svelte"
import { getOptions } from "./optionsParser" import { getOptions } from "./optionsParser"
export let field export let field
@ -15,6 +15,8 @@
export let customOptions export let customOptions
export let autocomplete = false export let autocomplete = false
export let onChange export let onChange
export let optionsType = "select"
export let direction = "vertical"
let fieldState let fieldState
let fieldApi let fieldApi
@ -61,17 +63,31 @@
bind:fieldSchema bind:fieldSchema
> >
{#if fieldState} {#if fieldState}
<CoreMultiselect {#if !optionsType || optionsType === "select"}
value={fieldState.value || []} <CoreMultiselect
error={fieldState.error} value={fieldState.value || []}
getOptionLabel={flatOptions ? x => x : x => x.label} error={fieldState.error}
getOptionValue={flatOptions ? x => x : x => x.value} getOptionLabel={flatOptions ? x => x : x => x.label}
id={fieldState.fieldId} getOptionValue={flatOptions ? x => x : x => x.value}
disabled={fieldState.disabled} id={fieldState.fieldId}
on:change={handleChange} disabled={fieldState.disabled}
{placeholder} on:change={handleChange}
{options} {placeholder}
{autocomplete} {options}
/> {autocomplete}
/>
{:else if optionsType === "checkbox"}
<CoreCheckboxGroup
value={fieldState.value || []}
id={fieldState.fieldId}
disabled={fieldState.disabled}
error={fieldState.error}
{options}
{direction}
on:change={handleChange}
getOptionLabel={flatOptions ? x => x : x => x.label}
getOptionValue={flatOptions ? x => x : x => x.value}
/>
{/if}
{/if} {/if}
</Field> </Field>

View File

@ -1,12 +1,12 @@
{ {
"name": "@budibase/frontend-core", "name": "@budibase/frontend-core",
"version": "1.0.159-alpha.0", "version": "1.0.164-alpha.0",
"description": "Budibase frontend core libraries used in builder and client", "description": "Budibase frontend core libraries used in builder and client",
"author": "Budibase", "author": "Budibase",
"license": "MPL-2.0", "license": "MPL-2.0",
"svelte": "src/index.js", "svelte": "src/index.js",
"dependencies": { "dependencies": {
"@budibase/bbui": "^1.0.159-alpha.0", "@budibase/bbui": "^1.0.164-alpha.0",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"svelte": "^3.46.2" "svelte": "^3.46.2"
} }

View File

@ -20,6 +20,17 @@ export const buildConfigEndpoints = API => ({
}) })
}, },
/**
* Deletes a global config
* @param id the id of the config to delete
* @param rev the revision of the config to delete
*/
deleteConfig: async ({ id, rev }) => {
return await API.delete({
url: `/api/global/configs/${id}/${rev}`,
})
},
/** /**
* Gets the config for a certain tenant. * Gets the config for a certain tenant.
* @param tenantId the tenant ID to get the config for * @param tenantId the tenant ID to get the config for

View File

@ -1,7 +1,7 @@
{ {
"name": "@budibase/server", "name": "@budibase/server",
"email": "hi@budibase.com", "email": "hi@budibase.com",
"version": "1.0.159-alpha.0", "version": "1.0.164-alpha.0",
"description": "Budibase Web Server", "description": "Budibase Web Server",
"main": "src/index.ts", "main": "src/index.ts",
"repository": { "repository": {
@ -69,10 +69,10 @@
"license": "GPL-3.0", "license": "GPL-3.0",
"dependencies": { "dependencies": {
"@apidevtools/swagger-parser": "^10.0.3", "@apidevtools/swagger-parser": "^10.0.3",
"@budibase/backend-core": "^1.0.159-alpha.0", "@budibase/backend-core": "^1.0.164-alpha.0",
"@budibase/client": "^1.0.159-alpha.0", "@budibase/client": "^1.0.164-alpha.0",
"@budibase/pro": "1.0.159-alpha.0", "@budibase/pro": "1.0.164-alpha.0",
"@budibase/string-templates": "^1.0.159-alpha.0", "@budibase/string-templates": "^1.0.164-alpha.0",
"@bull-board/api": "^3.7.0", "@bull-board/api": "^3.7.0",
"@bull-board/koa": "^3.7.0", "@bull-board/koa": "^3.7.0",
"@elastic/elasticsearch": "7.10.0", "@elastic/elasticsearch": "7.10.0",

View File

@ -1,9 +1,66 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP // Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`viewBuilder Calculate and filter creates a view with the calculation statistics and filter schema 1`] = `
Object {
"map": "function (doc) {
if ((doc.tableId === \\"14f1c4e94d6a47b682ce89d35d4c78b0\\" && !(
doc[\\"myField\\"] === undefined ||
doc[\\"myField\\"] === null ||
doc[\\"myField\\"] === \\"\\" ||
(Array.isArray(doc[\\"myField\\"]) && doc[\\"myField\\"].length === 0)
)) && (doc[\\"age\\"] > 17)) {
emit(doc[\\"_id\\"], doc[\\"myField\\"]);
}
}",
"meta": Object {
"calculation": "stats",
"field": "myField",
"filters": Array [
Object {
"condition": "MT",
"key": "age",
"value": 17,
},
],
"groupBy": undefined,
"schema": Object {
"avg": Object {
"type": "number",
},
"count": Object {
"type": "number",
},
"field": Object {
"type": "string",
},
"max": Object {
"type": "number",
},
"min": Object {
"type": "number",
},
"sum": Object {
"type": "number",
},
"sumsqr": Object {
"type": "number",
},
},
"tableId": "14f1c4e94d6a47b682ce89d35d4c78b0",
},
"reduce": "_stats",
}
`;
exports[`viewBuilder Calculate creates a view with the calculation statistics schema 1`] = ` exports[`viewBuilder Calculate creates a view with the calculation statistics schema 1`] = `
Object { Object {
"map": "function (doc) { "map": "function (doc) {
if (doc.tableId === \\"14f1c4e94d6a47b682ce89d35d4c78b0\\" ) { if ((doc.tableId === \\"14f1c4e94d6a47b682ce89d35d4c78b0\\" && !(
doc[\\"myField\\"] === undefined ||
doc[\\"myField\\"] === null ||
doc[\\"myField\\"] === \\"\\" ||
(Array.isArray(doc[\\"myField\\"]) && doc[\\"myField\\"].length === 0)
)) ) {
emit(doc[\\"_id\\"], doc[\\"myField\\"]); emit(doc[\\"_id\\"], doc[\\"myField\\"]);
} }
}", }",

View File

@ -44,4 +44,22 @@ describe("viewBuilder", () => {
})).toMatchSnapshot() })).toMatchSnapshot()
}) })
}) })
describe("Calculate and filter", () => {
it("creates a view with the calculation statistics and filter schema", () => {
expect(viewTemplate({
"name": "Calculate View",
"field": "myField",
"calculation": "stats",
"tableId": "14f1c4e94d6a47b682ce89d35d4c78b0",
"filters": [
{
"value": 17,
"condition": "MT",
"key": "age",
}
]
})).toMatchSnapshot()
})
})
}); });

View File

@ -7,6 +7,7 @@ const {
} = require("../../../db/utils") } = require("../../../db/utils")
const env = require("../../../environment") const env = require("../../../environment")
const { getAppDB } = require("@budibase/backend-core/context") const { getAppDB } = require("@budibase/backend-core/context")
const viewBuilder = require("./viewBuilder")
exports.getView = async viewName => { exports.getView = async viewName => {
const db = getAppDB() const db = getAppDB()
@ -114,7 +115,8 @@ exports.deleteView = async viewName => {
exports.migrateToInMemoryView = async (db, viewName) => { exports.migrateToInMemoryView = async (db, viewName) => {
// delete the view initially // delete the view initially
const designDoc = await db.get("_design/database") const designDoc = await db.get("_design/database")
const view = designDoc.views[viewName] // run the view back through the view builder to update it
const view = viewBuilder(designDoc.views[viewName].meta)
delete designDoc.views[viewName] delete designDoc.views[viewName]
await db.put(designDoc) await db.put(designDoc)
await exports.saveView(db, null, viewName, view) await exports.saveView(db, null, viewName, view)
@ -123,7 +125,7 @@ exports.migrateToInMemoryView = async (db, viewName) => {
exports.migrateToDesignView = async (db, viewName) => { exports.migrateToDesignView = async (db, viewName) => {
let view = await db.get(generateMemoryViewID(viewName)) let view = await db.get(generateMemoryViewID(viewName))
const designDoc = await db.get("_design/database") const designDoc = await db.get("_design/database")
designDoc.views[viewName] = view.view designDoc.views[viewName] = viewBuilder(view.view.meta)
await db.put(designDoc) await db.put(designDoc)
await db.remove(view._id, view._rev) await db.remove(view._id, view._rev)
} }

View File

@ -10,6 +10,12 @@ const TOKEN_MAP = {
OR: "||", OR: "||",
} }
const CONDITIONS = {
EMPTY: "EMPTY",
NOT_EMPTY: "NOT_EMPTY",
CONTAINS: "CONTAINS",
}
const isEmptyExpression = key => { const isEmptyExpression = key => {
return `( return `(
doc["${key}"] === undefined || doc["${key}"] === undefined ||
@ -77,13 +83,13 @@ function parseFilterExpression(filters) {
expression.push(TOKEN_MAP[filter.conjunction]) expression.push(TOKEN_MAP[filter.conjunction])
} }
if (filter.condition === "CONTAINS") { if (filter.condition === CONDITIONS.CONTAINS) {
expression.push( expression.push(
`doc["${filter.key}"].${TOKEN_MAP[filter.condition]}("${filter.value}")` `doc["${filter.key}"].${TOKEN_MAP[filter.condition]}("${filter.value}")`
) )
} else if (filter.condition === "EMPTY") { } else if (filter.condition === CONDITIONS.EMPTY) {
expression.push(isEmptyExpression(filter.key)) expression.push(isEmptyExpression(filter.key))
} else if (filter.condition === "NOT_EMPTY") { } else if (filter.condition === CONDITIONS.NOT_EMPTY) {
expression.push(`!${isEmptyExpression(filter.key)}`) expression.push(`!${isEmptyExpression(filter.key)}`)
} else { } else {
const value = const value =
@ -125,22 +131,37 @@ function viewTemplate({ field, tableId, groupBy, filters = [], calculation }) {
if (filters && filters.length > 0 && filters[0].conjunction) { if (filters && filters.length > 0 && filters[0].conjunction) {
delete filters[0].conjunction delete filters[0].conjunction
} }
const parsedFilters = parseFilterExpression(filters)
const filterExpression = parsedFilters ? `&& (${parsedFilters})` : ""
const emitExpression = parseEmitExpression(field, groupBy) let schema = null,
statFilter = null
const reduction = field && calculation ? { reduce: `_${calculation}` } : {}
let schema = null
if (calculation) { if (calculation) {
schema = { schema = {
...(groupBy ? GROUP_PROPERTY : FIELD_PROPERTY), ...(groupBy ? GROUP_PROPERTY : FIELD_PROPERTY),
...SCHEMA_MAP[calculation], ...SCHEMA_MAP[calculation],
} }
if (
!filters.find(
filter =>
filter.key === field && filter.condition === CONDITIONS.NOT_EMPTY
)
) {
statFilter = parseFilterExpression([
{ key: field, condition: CONDITIONS.NOT_EMPTY },
])
}
} }
const parsedFilters = parseFilterExpression(filters)
const filterExpression = parsedFilters ? `&& (${parsedFilters})` : ""
const emitExpression = parseEmitExpression(field, groupBy)
const tableExpression = `doc.tableId === "${tableId}"`
const coreExpression = statFilter
? `(${tableExpression} && ${statFilter})`
: tableExpression
const reduction = field && calculation ? { reduce: `_${calculation}` } : {}
return { return {
meta: { meta: {
field, field,
@ -151,7 +172,7 @@ function viewTemplate({ field, tableId, groupBy, filters = [], calculation }) {
calculation, calculation,
}, },
map: `function (doc) { map: `function (doc) {
if (doc.tableId === "${tableId}" ${filterExpression}) { if (${coreExpression} ${filterExpression}) {
${emitExpression} ${emitExpression}
} }
}`, }`,

View File

@ -4,10 +4,18 @@ import {
QueryTypes, QueryTypes,
} from "../definitions/datasource" } from "../definitions/datasource"
import { IntegrationBase } from "./base/IntegrationBase" import { IntegrationBase } from "./base/IntegrationBase"
import {
MongoClient,
ObjectID,
FilterQuery,
UpdateQuery,
FindOneAndUpdateOption,
UpdateOneOptions,
UpdateManyOptions,
CommonOptions,
} from "mongodb"
module MongoDBModule { module MongoDBModule {
const { MongoClient } = require("mongodb")
interface MongoDBConfig { interface MongoDBConfig {
connectionString: string connectionString: string
db: string db: string
@ -76,20 +84,76 @@ module MongoDBModule {
return this.client.connect() return this.client.connect()
} }
createObjectIds(json: any): object {
const self = this
function interpolateObjectIds(json: any) {
for (let field of Object.keys(json)) {
if (json[field] instanceof Object) {
json[field] = self.createObjectIds(json[field])
}
if (field === "_id" && typeof json[field] === "string") {
const id = json["_id"].match(
/(?<=objectid\(['"]).*(?=['"]\))/gi
)?.[0]
if (id) {
json["_id"] = ObjectID.createFromHexString(id)
}
}
}
return json
}
if (Array.isArray(json)) {
for (let i = 0; i < json.length; i++) {
json[i] = interpolateObjectIds(json[i])
}
return json
}
return interpolateObjectIds(json)
}
parseQueryParams(params: string, mode: string) {
let queryParams = params.split(/(?<=(},)).*{/g)
let group1 = queryParams[0]
let group2 = queryParams[2]
let group3 = queryParams[4]
if (group1) {
group1 = JSON.parse(group1.replace(/,+$/, ""))
}
if (group2) {
group2 = JSON.parse("{" + group2.replace(/,+$/, ""))
}
if (group3) {
group3 = JSON.parse("{" + group3.replace(/,+$/, ""))
}
if (mode === "update") {
return {
filter: group1,
update: group2,
options: group3 ?? {},
}
}
return {
filter: group1,
options: group2 ?? {},
}
}
async create(query: { json: object; extra: { [key: string]: string } }) { async create(query: { json: object; extra: { [key: string]: string } }) {
try { try {
await this.connect() await this.connect()
const db = this.client.db(this.config.db) const db = this.client.db(this.config.db)
const collection = db.collection(query.extra.collection) const collection = db.collection(query.extra.collection)
let json = this.createObjectIds(query.json)
// For mongodb we add an extra actionType to specify // For mongodb we add an extra actionType to specify
// which method we want to call on the collection // which method we want to call on the collection
switch (query.extra.actionTypes) { switch (query.extra.actionTypes) {
case "insertOne": { case "insertOne": {
return await collection.insertOne(query.json) return await collection.insertOne(json)
} }
case "insertMany": { case "insertMany": {
return await collection.insertOne(query.json).toArray() return await collection.insertMany(json)
} }
default: { default: {
throw new Error( throw new Error(
@ -110,22 +174,32 @@ module MongoDBModule {
await this.connect() await this.connect()
const db = this.client.db(this.config.db) const db = this.client.db(this.config.db)
const collection = db.collection(query.extra.collection) const collection = db.collection(query.extra.collection)
let json = this.createObjectIds(query.json)
switch (query.extra.actionTypes) { switch (query.extra.actionTypes) {
case "find": { case "find": {
return await collection.find(query.json).toArray() return await collection.find(json).toArray()
} }
case "findOne": { case "findOne": {
return await collection.findOne(query.json) return await collection.findOne(json)
} }
case "findOneAndUpdate": { case "findOneAndUpdate": {
return await collection.findOneAndUpdate(query.json) let findAndUpdateJson = json as {
filter: FilterQuery<any>
update: UpdateQuery<any>
options: FindOneAndUpdateOption<any>
}
return await collection.findOneAndUpdate(
findAndUpdateJson.filter,
findAndUpdateJson.update,
findAndUpdateJson.options
)
} }
case "count": { case "count": {
return await collection.countDocuments(query.json) return await collection.countDocuments(json)
} }
case "distinct": { case "distinct": {
return await collection.distinct(query.json) return await collection.distinct(json)
} }
default: { default: {
throw new Error( throw new Error(
@ -146,13 +220,30 @@ module MongoDBModule {
await this.connect() await this.connect()
const db = this.client.db(this.config.db) const db = this.client.db(this.config.db)
const collection = db.collection(query.extra.collection) const collection = db.collection(query.extra.collection)
let queryJson = query.json
if (typeof queryJson === "string") {
queryJson = this.parseQueryParams(queryJson, "update")
}
let json = this.createObjectIds(queryJson) as {
filter: FilterQuery<any>
update: UpdateQuery<any>
options: object
}
switch (query.extra.actionTypes) { switch (query.extra.actionTypes) {
case "updateOne": { case "updateOne": {
return await collection.updateOne(query.json) return await collection.updateOne(
json.filter,
json.update,
json.options as UpdateOneOptions
)
} }
case "updateMany": { case "updateMany": {
return await collection.updateMany(query.json).toArray() return await collection.updateMany(
json.filter,
json.update,
json.options as UpdateManyOptions
)
} }
default: { default: {
throw new Error( throw new Error(
@ -173,13 +264,21 @@ module MongoDBModule {
await this.connect() await this.connect()
const db = this.client.db(this.config.db) const db = this.client.db(this.config.db)
const collection = db.collection(query.extra.collection) const collection = db.collection(query.extra.collection)
let queryJson = query.json
if (typeof queryJson === "string") {
queryJson = this.parseQueryParams(queryJson, "delete")
}
let json = this.createObjectIds(queryJson) as {
filter: FilterQuery<any>
options: CommonOptions
}
switch (query.extra.actionTypes) { switch (query.extra.actionTypes) {
case "deleteOne": { case "deleteOne": {
return await collection.deleteOne(query.json) return await collection.deleteOne(json.filter, json.options)
} }
case "deleteMany": { case "deleteMany": {
return await collection.deleteMany(query.json).toArray() return await collection.deleteMany(json.filter, json.options)
} }
default: { default: {
throw new Error( throw new Error(

View File

@ -9,10 +9,10 @@ class TestConfiguration {
} }
function disableConsole() { function disableConsole() {
jest.spyOn(console, 'error'); jest.spyOn(console, "error")
console.error.mockImplementation(() => {}); console.error.mockImplementation(() => {})
return console.error.mockRestore; return console.error.mockRestore
} }
describe("MongoDB Integration", () => { describe("MongoDB Integration", () => {
@ -25,12 +25,12 @@ describe("MongoDB Integration", () => {
it("calls the create method with the correct params", async () => { it("calls the create method with the correct params", async () => {
const body = { const body = {
name: "Hello" name: "Hello",
} }
await config.integration.create({ await config.integration.create({
index: indexName, index: indexName,
json: body, json: body,
extra: { collection: 'testCollection', actionTypes: 'insertOne'} extra: { collection: "testCollection", actionTypes: "insertOne" },
}) })
expect(config.integration.client.insertOne).toHaveBeenCalledWith(body) expect(config.integration.client.insertOne).toHaveBeenCalledWith(body)
}) })
@ -38,9 +38,9 @@ describe("MongoDB Integration", () => {
it("calls the read method with the correct params", async () => { it("calls the read method with the correct params", async () => {
const query = { const query = {
json: { json: {
address: "test" address: "test",
}, },
extra: { collection: 'testCollection', actionTypes: 'find'} extra: { collection: "testCollection", actionTypes: "find" },
} }
const response = await config.integration.read(query) const response = await config.integration.read(query)
expect(config.integration.client.find).toHaveBeenCalledWith(query.json) expect(config.integration.client.find).toHaveBeenCalledWith(query.json)
@ -50,30 +50,47 @@ describe("MongoDB Integration", () => {
it("calls the delete method with the correct params", async () => { it("calls the delete method with the correct params", async () => {
const query = { const query = {
json: { json: {
id: "test" filter: {
id: "test",
},
options: {
opt: "option"
}
}, },
extra: { collection: 'testCollection', actionTypes: 'deleteOne'} extra: { collection: "testCollection", actionTypes: "deleteOne" },
} }
await config.integration.delete(query) await config.integration.delete(query)
expect(config.integration.client.deleteOne).toHaveBeenCalledWith(query.json) expect(config.integration.client.deleteOne).toHaveBeenCalledWith(query.json.filter, query.json.options)
}) })
it("calls the update method with the correct params", async () => { it("calls the update method with the correct params", async () => {
const query = { const query = {
json: { json: {
id: "test" filter: {
id: "test",
},
update: {
name: "TestName",
},
options: {
upsert: false,
},
}, },
extra: { collection: 'testCollection', actionTypes: 'updateOne'} extra: { collection: "testCollection", actionTypes: "updateOne" },
} }
await config.integration.update(query) await config.integration.update(query)
expect(config.integration.client.updateOne).toHaveBeenCalledWith(query.json) expect(config.integration.client.updateOne).toHaveBeenCalledWith(
query.json.filter,
query.json.update,
query.json.options
)
}) })
it("throws an error when an invalid query.extra.actionType is passed for each method", async () => { it("throws an error when an invalid query.extra.actionType is passed for each method", async () => {
const restore = disableConsole() const restore = disableConsole()
const query = { const query = {
extra: { collection: 'testCollection', actionTypes: 'deleteOne'} extra: { collection: "testCollection", actionTypes: "deleteOne" },
} }
let error = null let error = null

View File

@ -1014,10 +1014,10 @@
resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39" resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39"
integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw== integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==
"@budibase/backend-core@1.0.154": "@budibase/backend-core@1.0.160":
version "1.0.154" version "1.0.160"
resolved "https://registry.yarnpkg.com/@budibase/backend-core/-/backend-core-1.0.154.tgz#c310834892e7621778b07579464955487c5c9830" resolved "https://registry.yarnpkg.com/@budibase/backend-core/-/backend-core-1.0.160.tgz#000e3b5a3ed91e73a542b4caa202a6f147d91294"
integrity sha512-mcZxt8XhGgOB4XRHKkWTvBEI4HGp2bo8qyzOJRCvDqlg56S9zqGJDl75Z0N/Wc8N3I53QRcxISerj48odX172A== integrity sha512-XfAFU6sRPrCSEKlm58WeuPw8lUoJK+KwO0tcbT+bB2Nb7XCHplskryEgk/PM9ujRU6SMPDx11zKeqRebHlycbA==
dependencies: dependencies:
"@techpass/passport-openidconnect" "^0.3.0" "@techpass/passport-openidconnect" "^0.3.0"
aws-sdk "^2.901.0" aws-sdk "^2.901.0"
@ -1091,12 +1091,12 @@
svelte-flatpickr "^3.2.3" svelte-flatpickr "^3.2.3"
svelte-portal "^1.0.0" svelte-portal "^1.0.0"
"@budibase/pro@1.0.154": "@budibase/pro@1.0.160":
version "1.0.154" version "1.0.160"
resolved "https://registry.yarnpkg.com/@budibase/pro/-/pro-1.0.154.tgz#f4e31e30376b206159b711224038141d73a1118e" resolved "https://registry.yarnpkg.com/@budibase/pro/-/pro-1.0.160.tgz#921c4e3f65b866d84292644dfd8793c4d0b667c7"
integrity sha512-+O6bemrcgyWG4a+D5dIOoZ+LGjW4aN7tRdFeZqoaIPCc1pA6zNtLUkM1nb+Laafuwq2Aht37vEuaRy7jfzVprA== integrity sha512-p+Jhnk1n98CWCJXydSQSO7a+HDpqGAHekGQbOR7aayuwuoYzyOXxTcHNLdBp+3lkXhLSZq9oIwfEGpgdrrhXPA==
dependencies: dependencies:
"@budibase/backend-core" "1.0.154" "@budibase/backend-core" "1.0.160"
node-fetch "^2.6.1" node-fetch "^2.6.1"
"@budibase/standard-components@^0.9.139": "@budibase/standard-components@^0.9.139":

View File

@ -1,6 +1,6 @@
{ {
"name": "@budibase/string-templates", "name": "@budibase/string-templates",
"version": "1.0.159-alpha.0", "version": "1.0.164-alpha.0",
"description": "Handlebars wrapper for Budibase templating.", "description": "Handlebars wrapper for Budibase templating.",
"main": "src/index.cjs", "main": "src/index.cjs",
"module": "dist/bundle.mjs", "module": "dist/bundle.mjs",

View File

@ -1,7 +1,7 @@
{ {
"name": "@budibase/worker", "name": "@budibase/worker",
"email": "hi@budibase.com", "email": "hi@budibase.com",
"version": "1.0.159-alpha.0", "version": "1.0.164-alpha.0",
"description": "Budibase background service", "description": "Budibase background service",
"main": "src/index.ts", "main": "src/index.ts",
"repository": { "repository": {
@ -31,9 +31,9 @@
"author": "Budibase", "author": "Budibase",
"license": "GPL-3.0", "license": "GPL-3.0",
"dependencies": { "dependencies": {
"@budibase/backend-core": "^1.0.159-alpha.0", "@budibase/backend-core": "^1.0.164-alpha.0",
"@budibase/pro": "1.0.159-alpha.0", "@budibase/pro": "1.0.164-alpha.0",
"@budibase/string-templates": "^1.0.159-alpha.0", "@budibase/string-templates": "^1.0.164-alpha.0",
"@koa/router": "^8.0.0", "@koa/router": "^8.0.0",
"@sentry/node": "6.17.7", "@sentry/node": "6.17.7",
"@techpass/passport-openidconnect": "^0.3.0", "@techpass/passport-openidconnect": "^0.3.0",

View File

@ -293,10 +293,10 @@
resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39" resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39"
integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw== integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==
"@budibase/backend-core@1.0.154": "@budibase/backend-core@1.0.160":
version "1.0.154" version "1.0.160"
resolved "https://registry.yarnpkg.com/@budibase/backend-core/-/backend-core-1.0.154.tgz#c310834892e7621778b07579464955487c5c9830" resolved "https://registry.yarnpkg.com/@budibase/backend-core/-/backend-core-1.0.160.tgz#000e3b5a3ed91e73a542b4caa202a6f147d91294"
integrity sha512-mcZxt8XhGgOB4XRHKkWTvBEI4HGp2bo8qyzOJRCvDqlg56S9zqGJDl75Z0N/Wc8N3I53QRcxISerj48odX172A== integrity sha512-XfAFU6sRPrCSEKlm58WeuPw8lUoJK+KwO0tcbT+bB2Nb7XCHplskryEgk/PM9ujRU6SMPDx11zKeqRebHlycbA==
dependencies: dependencies:
"@techpass/passport-openidconnect" "^0.3.0" "@techpass/passport-openidconnect" "^0.3.0"
aws-sdk "^2.901.0" aws-sdk "^2.901.0"
@ -321,12 +321,12 @@
uuid "^8.3.2" uuid "^8.3.2"
zlib "^1.0.5" zlib "^1.0.5"
"@budibase/pro@1.0.154": "@budibase/pro@1.0.160":
version "1.0.154" version "1.0.160"
resolved "https://registry.yarnpkg.com/@budibase/pro/-/pro-1.0.154.tgz#f4e31e30376b206159b711224038141d73a1118e" resolved "https://registry.yarnpkg.com/@budibase/pro/-/pro-1.0.160.tgz#921c4e3f65b866d84292644dfd8793c4d0b667c7"
integrity sha512-+O6bemrcgyWG4a+D5dIOoZ+LGjW4aN7tRdFeZqoaIPCc1pA6zNtLUkM1nb+Laafuwq2Aht37vEuaRy7jfzVprA== integrity sha512-p+Jhnk1n98CWCJXydSQSO7a+HDpqGAHekGQbOR7aayuwuoYzyOXxTcHNLdBp+3lkXhLSZq9oIwfEGpgdrrhXPA==
dependencies: dependencies:
"@budibase/backend-core" "1.0.154" "@budibase/backend-core" "1.0.160"
node-fetch "^2.6.1" node-fetch "^2.6.1"
"@cspotcode/source-map-consumer@0.8.0": "@cspotcode/source-map-consumer@0.8.0":

View File

@ -972,6 +972,13 @@
estree-walker "^1.0.1" estree-walker "^1.0.1"
picomatch "^2.2.2" picomatch "^2.2.2"
"@types/bson@*":
version "4.2.0"
resolved "https://registry.yarnpkg.com/@types/bson/-/bson-4.2.0.tgz#a2f71e933ff54b2c3bf267b67fa221e295a33337"
integrity sha512-ELCPqAdroMdcuxqwMgUpifQyRoTpyYCNr1V9xKyF40VsBobsj+BbWNRvwGchMgBPGqkw655ypkjj2MEF5ywVwg==
dependencies:
bson "*"
"@types/estree@0.0.39": "@types/estree@0.0.39":
version "0.0.39" version "0.0.39"
resolved "https://registry.yarnpkg.com/@types/estree/-/estree-0.0.39.tgz#e177e699ee1b8c22d23174caaa7422644389509f" resolved "https://registry.yarnpkg.com/@types/estree/-/estree-0.0.39.tgz#e177e699ee1b8c22d23174caaa7422644389509f"
@ -982,6 +989,19 @@
resolved "https://registry.yarnpkg.com/@types/minimist/-/minimist-1.2.2.tgz#ee771e2ba4b3dc5b372935d549fd9617bf345b8c" resolved "https://registry.yarnpkg.com/@types/minimist/-/minimist-1.2.2.tgz#ee771e2ba4b3dc5b372935d549fd9617bf345b8c"
integrity sha512-jhuKLIRrhvCPLqwPcx6INqmKeiA5EWrsCOPhrlFSrbrmU4ZMPjj5Ul/oLCMDO98XRUIwVm78xICz4EPCektzeQ== integrity sha512-jhuKLIRrhvCPLqwPcx6INqmKeiA5EWrsCOPhrlFSrbrmU4ZMPjj5Ul/oLCMDO98XRUIwVm78xICz4EPCektzeQ==
"@types/mongodb@3.6.3":
version "3.6.3"
resolved "https://registry.yarnpkg.com/@types/mongodb/-/mongodb-3.6.3.tgz#5655af409d9e32d5d5ae9a653abf3e5f9c83eb7a"
integrity sha512-6YNqGP1hk5bjUFaim+QoFFuI61WjHiHE1BNeB41TA00Xd2K7zG4lcWyLLq/XtIp36uMavvS5hoAUJ+1u/GcX2Q==
dependencies:
"@types/bson" "*"
"@types/node" "*"
"@types/node@*":
version "17.0.33"
resolved "https://registry.yarnpkg.com/@types/node/-/node-17.0.33.tgz#3c1879b276dc63e73030bb91165e62a4509cd506"
integrity sha512-miWq2m2FiQZmaHfdZNcbpp9PuXg34W5JZ5CrJ/BaS70VuhoJENBEQybeiYSaPBRNq6KQGnjfEnc/F3PN++D+XQ==
"@types/node@>= 8": "@types/node@>= 8":
version "17.0.18" version "17.0.18"
resolved "https://registry.yarnpkg.com/@types/node/-/node-17.0.18.tgz#3b4fed5cfb58010e3a2be4b6e74615e4847f1074" resolved "https://registry.yarnpkg.com/@types/node/-/node-17.0.18.tgz#3b4fed5cfb58010e3a2be4b6e74615e4847f1074"
@ -1300,6 +1320,11 @@ balanced-match@^1.0.0:
resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee"
integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==
base64-js@^1.3.1:
version "1.5.1"
resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a"
integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==
base@^0.11.1: base@^0.11.1:
version "0.11.2" version "0.11.2"
resolved "https://registry.yarnpkg.com/base/-/base-0.11.2.tgz#7bde5ced145b6d551a90db87f83c558b4eb48a8f" resolved "https://registry.yarnpkg.com/base/-/base-0.11.2.tgz#7bde5ced145b6d551a90db87f83c558b4eb48a8f"
@ -1361,6 +1386,13 @@ braces@^3.0.2:
dependencies: dependencies:
fill-range "^7.0.1" fill-range "^7.0.1"
bson@*:
version "4.6.3"
resolved "https://registry.yarnpkg.com/bson/-/bson-4.6.3.tgz#d1a9a0b84b9e84b62390811fc5580f6a8b1d858c"
integrity sha512-rAqP5hcUVJhXP2MCSNVsf0oM2OGU1So6A9pVRDYayvJ5+hygXHQApf87wd5NlhPM1J9RJnbqxIG/f8QTzRoQ4A==
dependencies:
buffer "^5.6.0"
btoa-lite@^1.0.0: btoa-lite@^1.0.0:
version "1.0.0" version "1.0.0"
resolved "https://registry.yarnpkg.com/btoa-lite/-/btoa-lite-1.0.0.tgz#337766da15801210fdd956c22e9c6891ab9d0337" resolved "https://registry.yarnpkg.com/btoa-lite/-/btoa-lite-1.0.0.tgz#337766da15801210fdd956c22e9c6891ab9d0337"
@ -1371,6 +1403,14 @@ buffer-from@^1.0.0:
resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5" resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5"
integrity sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ== integrity sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==
buffer@^5.6.0:
version "5.7.1"
resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.7.1.tgz#ba62e7c13133053582197160851a8f648e99eed0"
integrity sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==
dependencies:
base64-js "^1.3.1"
ieee754 "^1.1.13"
builtins@^1.0.3: builtins@^1.0.3:
version "1.0.3" version "1.0.3"
resolved "https://registry.yarnpkg.com/builtins/-/builtins-1.0.3.tgz#cb94faeb61c8696451db36534e1422f94f0aee88" resolved "https://registry.yarnpkg.com/builtins/-/builtins-1.0.3.tgz#cb94faeb61c8696451db36534e1422f94f0aee88"
@ -2969,6 +3009,11 @@ iconv-lite@^0.6.2:
dependencies: dependencies:
safer-buffer ">= 2.1.2 < 3.0.0" safer-buffer ">= 2.1.2 < 3.0.0"
ieee754@^1.1.13:
version "1.2.1"
resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352"
integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==
iferr@^0.1.5: iferr@^0.1.5:
version "0.1.5" version "0.1.5"
resolved "https://registry.yarnpkg.com/iferr/-/iferr-0.1.5.tgz#c60eed69e6d8fdb6b3104a1fcbca1c192dc5b501" resolved "https://registry.yarnpkg.com/iferr/-/iferr-0.1.5.tgz#c60eed69e6d8fdb6b3104a1fcbca1c192dc5b501"
@ -4663,12 +4708,7 @@ performance-now@^2.1.0:
resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b" resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b"
integrity sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns= integrity sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=
picomatch@^2.2.2: picomatch@^2.2.2, picomatch@^2.3.1:
version "2.3.1"
resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42"
integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==
picomatch@^2.3.1:
version "2.3.1" version "2.3.1"
resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42" resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42"
integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==