Merge branch 'develop' into tests/qa-22-improve-qa-core-boilerplate

This commit is contained in:
Pedro Silva 2023-05-17 21:34:44 +01:00
commit 06e7430ab8
100 changed files with 2390 additions and 442 deletions

View File

@ -55,7 +55,7 @@ http {
set $csp_style "style-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net https://fonts.googleapis.com https://rsms.me https://maxcdn.bootstrapcdn.com"; set $csp_style "style-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net https://fonts.googleapis.com https://rsms.me https://maxcdn.bootstrapcdn.com";
set $csp_object "object-src 'none'"; set $csp_object "object-src 'none'";
set $csp_base_uri "base-uri 'self'"; set $csp_base_uri "base-uri 'self'";
set $csp_connect "connect-src 'self' https://*.budibase.net https://api-iam.intercom.io https://api-iam.intercom.io https://api-ping.intercom.io https://app.posthog.com wss://nexus-websocket-a.intercom.io wss://nexus-websocket-b.intercom.io https://nexus-websocket-a.intercom.io https://nexus-websocket-b.intercom.io https://uploads.intercomcdn.com https://uploads.intercomusercontent.com https://*.s3.amazonaws.com https://*.s3.us-east-2.amazonaws.com https://*.s3.us-east-1.amazonaws.com https://*.s3.us-west-1.amazonaws.com https://*.s3.us-west-2.amazonaws.com https://*.s3.af-south-1.amazonaws.com https://*.s3.ap-east-1.amazonaws.com https://*.s3.ap-southeast-3.amazonaws.com https://*.s3.ap-south-1.amazonaws.com https://*.s3.ap-northeast-3.amazonaws.com https://*.s3.ap-northeast-2.amazonaws.com https://*.s3.ap-southeast-1.amazonaws.com https://*.s3.ap-southeast-2.amazonaws.com https://*.s3.ap-northeast-1.amazonaws.com https://*.s3.ca-central-1.amazonaws.com https://*.s3.cn-north-1.amazonaws.com https://*.s3.cn-northwest-1.amazonaws.com https://*.s3.eu-central-1.amazonaws.com https://*.s3.eu-west-1.amazonaws.com https://*.s3.eu-west-2.amazonaws.com https://*.s3.eu-south-1.amazonaws.com https://*.s3.eu-west-3.amazonaws.com https://*.s3.eu-north-1.amazonaws.com https://*.s3.sa-east-1.amazonaws.com https://*.s3.me-south-1.amazonaws.com https://*.s3.us-gov-east-1.amazonaws.com https://*.s3.us-gov-west-1.amazonaws.com https://api.github.com"; set $csp_connect "connect-src 'self' https://*.budibase.net https://api-iam.intercom.io https://api-iam.intercom.io https://api-ping.intercom.io https://app.posthog.com wss://nexus-websocket-a.intercom.io wss://nexus-websocket-b.intercom.io https://nexus-websocket-a.intercom.io https://nexus-websocket-b.intercom.io https://uploads.intercomcdn.com https://uploads.intercomusercontent.com https://*.amazonaws.com https://*.s3.amazonaws.com https://*.s3.us-east-2.amazonaws.com https://*.s3.us-east-1.amazonaws.com https://*.s3.us-west-1.amazonaws.com https://*.s3.us-west-2.amazonaws.com https://*.s3.af-south-1.amazonaws.com https://*.s3.ap-east-1.amazonaws.com https://*.s3.ap-southeast-3.amazonaws.com https://*.s3.ap-south-1.amazonaws.com https://*.s3.ap-northeast-3.amazonaws.com https://*.s3.ap-northeast-2.amazonaws.com https://*.s3.ap-southeast-1.amazonaws.com https://*.s3.ap-southeast-2.amazonaws.com https://*.s3.ap-northeast-1.amazonaws.com https://*.s3.ca-central-1.amazonaws.com https://*.s3.cn-north-1.amazonaws.com https://*.s3.cn-northwest-1.amazonaws.com https://*.s3.eu-central-1.amazonaws.com https://*.s3.eu-west-1.amazonaws.com https://*.s3.eu-west-2.amazonaws.com https://*.s3.eu-south-1.amazonaws.com https://*.s3.eu-west-3.amazonaws.com https://*.s3.eu-north-1.amazonaws.com https://*.s3.sa-east-1.amazonaws.com https://*.s3.me-south-1.amazonaws.com https://*.s3.us-gov-east-1.amazonaws.com https://*.s3.us-gov-west-1.amazonaws.com https://api.github.com";
set $csp_font "font-src 'self' data: https://cdn.jsdelivr.net https://fonts.gstatic.com https://rsms.me https://maxcdn.bootstrapcdn.com https://js.intercomcdn.com https://fonts.intercomcdn.com"; set $csp_font "font-src 'self' data: https://cdn.jsdelivr.net https://fonts.gstatic.com https://rsms.me https://maxcdn.bootstrapcdn.com https://js.intercomcdn.com https://fonts.intercomcdn.com";
set $csp_frame "frame-src 'self' https:"; set $csp_frame "frame-src 'self' https:";
set $csp_img "img-src http: https: data: blob:"; set $csp_img "img-src http: https: data: blob:";
@ -82,6 +82,12 @@ http {
set $couchdb ${COUCHDB_UPSTREAM_URL}; set $couchdb ${COUCHDB_UPSTREAM_URL};
set $watchtower ${WATCHTOWER_UPSTREAM_URL}; set $watchtower ${WATCHTOWER_UPSTREAM_URL};
location /health {
access_log off;
add_header 'Content-Type' 'application/json';
return 200 '{ "status": "OK" }';
}
location /app { location /app {
proxy_pass $apps; proxy_pass $apps;
} }

View File

@ -1,5 +1,5 @@
{ {
"version": "2.6.8-alpha.9", "version": "2.6.16-alpha.5",
"npmClient": "yarn", "npmClient": "yarn",
"packages": [ "packages": [
"packages/backend-core", "packages/backend-core",

View File

@ -21,7 +21,7 @@ export enum ViewName {
AUTOMATION_LOGS = "automation_logs", AUTOMATION_LOGS = "automation_logs",
ACCOUNT_BY_EMAIL = "account_by_email", ACCOUNT_BY_EMAIL = "account_by_email",
PLATFORM_USERS_LOWERCASE = "platform_users_lowercase", PLATFORM_USERS_LOWERCASE = "platform_users_lowercase",
USER_BY_GROUP = "by_group_user", USER_BY_GROUP = "user_by_group",
APP_BACKUP_BY_TRIGGER = "by_trigger", APP_BACKUP_BY_TRIGGER = "by_trigger",
} }

View File

@ -12,7 +12,7 @@ import {
isDocument, isDocument,
} from "@budibase/types" } from "@budibase/types"
import { getCouchInfo } from "./connections" import { getCouchInfo } from "./connections"
import { directCouchCall } from "./utils" import { directCouchUrlCall } from "./utils"
import { getPouchDB } from "./pouchDB" import { getPouchDB } from "./pouchDB"
import { WriteStream, ReadStream } from "fs" import { WriteStream, ReadStream } from "fs"
import { newid } from "../../docIds/newid" import { newid } from "../../docIds/newid"
@ -46,6 +46,8 @@ export class DatabaseImpl implements Database {
private readonly instanceNano?: Nano.ServerScope private readonly instanceNano?: Nano.ServerScope
private readonly pouchOpts: DatabaseOpts private readonly pouchOpts: DatabaseOpts
private readonly couchInfo = getCouchInfo()
constructor(dbName?: string, opts?: DatabaseOpts, connection?: string) { constructor(dbName?: string, opts?: DatabaseOpts, connection?: string) {
if (dbName == null) { if (dbName == null) {
throw new Error("Database name cannot be undefined.") throw new Error("Database name cannot be undefined.")
@ -53,8 +55,8 @@ export class DatabaseImpl implements Database {
this.name = dbName this.name = dbName
this.pouchOpts = opts || {} this.pouchOpts = opts || {}
if (connection) { if (connection) {
const couchInfo = getCouchInfo(connection) this.couchInfo = getCouchInfo(connection)
this.instanceNano = buildNano(couchInfo) this.instanceNano = buildNano(this.couchInfo)
} }
if (!DatabaseImpl.nano) { if (!DatabaseImpl.nano) {
DatabaseImpl.init() DatabaseImpl.init()
@ -67,7 +69,11 @@ export class DatabaseImpl implements Database {
} }
async exists() { async exists() {
let response = await directCouchCall(`/${this.name}`, "HEAD") const response = await directCouchUrlCall({
url: `${this.couchInfo.url}/${this.name}`,
method: "HEAD",
cookie: this.couchInfo.cookie,
})
return response.status === 200 return response.status === 200
} }

View File

@ -4,21 +4,21 @@ export const getCouchInfo = (connection?: string) => {
const urlInfo = getUrlInfo(connection) const urlInfo = getUrlInfo(connection)
let username let username
let password let password
if (env.COUCH_DB_USERNAME) { if (urlInfo.auth?.username) {
// set from env
username = env.COUCH_DB_USERNAME
} else if (urlInfo.auth.username) {
// set from url // set from url
username = urlInfo.auth.username username = urlInfo.auth.username
} else if (env.COUCH_DB_USERNAME) {
// set from env
username = env.COUCH_DB_USERNAME
} else if (!env.isTest()) { } else if (!env.isTest()) {
throw new Error("CouchDB username not set") throw new Error("CouchDB username not set")
} }
if (env.COUCH_DB_PASSWORD) { if (urlInfo.auth?.password) {
// set from env
password = env.COUCH_DB_PASSWORD
} else if (urlInfo.auth.password) {
// set from url // set from url
password = urlInfo.auth.password password = urlInfo.auth.password
} else if (env.COUCH_DB_PASSWORD) {
// set from env
password = env.COUCH_DB_PASSWORD
} else if (!env.isTest()) { } else if (!env.isTest()) {
throw new Error("CouchDB password not set") throw new Error("CouchDB password not set")
} }

View File

@ -9,6 +9,20 @@ export async function directCouchCall(
) { ) {
let { url, cookie } = getCouchInfo() let { url, cookie } = getCouchInfo()
const couchUrl = `${url}/${path}` const couchUrl = `${url}/${path}`
return await directCouchUrlCall({ url: couchUrl, cookie, method, body })
}
export async function directCouchUrlCall({
url,
cookie,
method,
body,
}: {
url: string
cookie: string
method: string
body?: any
}) {
const params: any = { const params: any = {
method: method, method: method,
headers: { headers: {
@ -19,7 +33,7 @@ export async function directCouchCall(
params.body = JSON.stringify(body) params.body = JSON.stringify(body)
params.headers["Content-Type"] = "application/json" params.headers["Content-Type"] = "application/json"
} }
return await fetch(checkSlashesInUrl(encodeURI(couchUrl)), params) return await fetch(checkSlashesInUrl(encodeURI(url)), params)
} }
export async function directCouchQuery( export async function directCouchQuery(

View File

@ -69,10 +69,10 @@ function findVersion() {
try { try {
const packageJsonFile = findFileInAncestors("package.json", process.cwd()) const packageJsonFile = findFileInAncestors("package.json", process.cwd())
const content = readFileSync(packageJsonFile!, "utf-8") const content = readFileSync(packageJsonFile!, "utf-8")
const version = JSON.parse(content).version return JSON.parse(content).version
return version
} catch { } catch {
throw new Error("Cannot find a valid version in its package.json") // throwing an error here is confusing/causes backend-core to be hard to import
return undefined
} }
} }
@ -95,7 +95,7 @@ const environment = {
GOOGLE_CLIENT_SECRET: process.env.GOOGLE_CLIENT_SECRET, GOOGLE_CLIENT_SECRET: process.env.GOOGLE_CLIENT_SECRET,
SALT_ROUNDS: process.env.SALT_ROUNDS, SALT_ROUNDS: process.env.SALT_ROUNDS,
REDIS_URL: process.env.REDIS_URL || "localhost:6379", REDIS_URL: process.env.REDIS_URL || "localhost:6379",
REDIS_PASSWORD: process.env.REDIS_PASSWORD || "budibase", REDIS_PASSWORD: process.env.REDIS_PASSWORD,
REDIS_CLUSTERED: process.env.REDIS_CLUSTERED, REDIS_CLUSTERED: process.env.REDIS_CLUSTERED,
MOCK_REDIS: process.env.MOCK_REDIS, MOCK_REDIS: process.env.MOCK_REDIS,
MINIO_ACCESS_KEY: process.env.MINIO_ACCESS_KEY, MINIO_ACCESS_KEY: process.env.MINIO_ACCESS_KEY,

View File

@ -5,6 +5,7 @@ import * as db from "../../db"
import { Header } from "../../constants" import { Header } from "../../constants"
import { newid } from "../../utils" import { newid } from "../../utils"
import env from "../../environment" import env from "../../environment"
import { BBContext } from "@budibase/types"
describe("utils", () => { describe("utils", () => {
const config = new DBTestConfiguration() const config = new DBTestConfiguration()
@ -106,4 +107,85 @@ describe("utils", () => {
expect(actual).toBe(undefined) expect(actual).toBe(undefined)
}) })
}) })
describe("isServingBuilder", () => {
let ctx: BBContext
const expectResult = (result: boolean) =>
expect(utils.isServingBuilder(ctx)).toBe(result)
beforeEach(() => {
ctx = structures.koa.newContext()
})
it("returns true if current path is in builder", async () => {
ctx.path = "/builder/app/app_"
expectResult(true)
})
it("returns false if current path doesn't have '/' suffix", async () => {
ctx.path = "/builder/app"
expectResult(false)
ctx.path = "/xx"
expectResult(false)
})
})
describe("isServingBuilderPreview", () => {
let ctx: BBContext
const expectResult = (result: boolean) =>
expect(utils.isServingBuilderPreview(ctx)).toBe(result)
beforeEach(() => {
ctx = structures.koa.newContext()
})
it("returns true if current path is in builder preview", async () => {
ctx.path = "/app/preview/xx"
expectResult(true)
})
it("returns false if current path is not in builder preview", async () => {
ctx.path = "/builder"
expectResult(false)
ctx.path = "/xx"
expectResult(false)
})
})
describe("isPublicAPIRequest", () => {
let ctx: BBContext
const expectResult = (result: boolean) =>
expect(utils.isPublicApiRequest(ctx)).toBe(result)
beforeEach(() => {
ctx = structures.koa.newContext()
})
it("returns true if current path remains to public API", async () => {
ctx.path = "/api/public/v1/invoices"
expectResult(true)
ctx.path = "/api/public/v1"
expectResult(true)
ctx.path = "/api/public/v2"
expectResult(true)
ctx.path = "/api/public/v21"
expectResult(true)
})
it("returns false if current path doesn't remain to public API", async () => {
ctx.path = "/api/public"
expectResult(false)
ctx.path = "/xx"
expectResult(false)
})
})
}) })

View File

@ -1,11 +1,5 @@
import { getAllApps, queryGlobalView } from "../db" import { getAllApps } from "../db"
import { import { Header, MAX_VALID_DATE, DocumentType, SEPARATOR } from "../constants"
Header,
MAX_VALID_DATE,
DocumentType,
SEPARATOR,
ViewName,
} from "../constants"
import env from "../environment" import env from "../environment"
import * as tenancy from "../tenancy" import * as tenancy from "../tenancy"
import * as context from "../context" import * as context from "../context"
@ -23,7 +17,9 @@ const APP_PREFIX = DocumentType.APP + SEPARATOR
const PROD_APP_PREFIX = "/app/" const PROD_APP_PREFIX = "/app/"
const BUILDER_PREVIEW_PATH = "/app/preview" const BUILDER_PREVIEW_PATH = "/app/preview"
const BUILDER_REFERER_PREFIX = "/builder/app/" const BUILDER_PREFIX = "/builder"
const BUILDER_APP_PREFIX = `${BUILDER_PREFIX}/app/`
const PUBLIC_API_PREFIX = "/api/public/v"
function confirmAppId(possibleAppId: string | undefined) { function confirmAppId(possibleAppId: string | undefined) {
return possibleAppId && possibleAppId.startsWith(APP_PREFIX) return possibleAppId && possibleAppId.startsWith(APP_PREFIX)
@ -69,6 +65,18 @@ export function isServingApp(ctx: Ctx) {
return false return false
} }
export function isServingBuilder(ctx: Ctx): boolean {
return ctx.path.startsWith(BUILDER_APP_PREFIX)
}
export function isServingBuilderPreview(ctx: Ctx): boolean {
return ctx.path.startsWith(BUILDER_PREVIEW_PATH)
}
export function isPublicApiRequest(ctx: Ctx): boolean {
return ctx.path.startsWith(PUBLIC_API_PREFIX)
}
/** /**
* Given a request tries to find the appId, which can be located in various places * Given a request tries to find the appId, which can be located in various places
* @param {object} ctx The main request body to look through. * @param {object} ctx The main request body to look through.
@ -110,7 +118,7 @@ export async function getAppIdFromCtx(ctx: Ctx) {
// make sure this is performed after prod app url resolution, in case the // make sure this is performed after prod app url resolution, in case the
// referer header is present from a builder redirect // referer header is present from a builder redirect
const referer = ctx.request.headers.referer const referer = ctx.request.headers.referer
if (!appId && referer?.includes(BUILDER_REFERER_PREFIX)) { if (!appId && referer?.includes(BUILDER_APP_PREFIX)) {
const refererId = parseAppIdFromUrl(ctx.request.headers.referer) const refererId = parseAppIdFromUrl(ctx.request.headers.referer)
appId = confirmAppId(refererId) appId = confirmAppId(refererId)
} }

View File

@ -84,7 +84,7 @@
"@spectrum-css/vars": "3.0.1", "@spectrum-css/vars": "3.0.1",
"dayjs": "^1.10.4", "dayjs": "^1.10.4",
"easymde": "^2.16.1", "easymde": "^2.16.1",
"svelte-flatpickr": "^3.3.2", "svelte-flatpickr": "3.2.3",
"svelte-portal": "^1.0.0" "svelte-portal": "^1.0.0"
}, },
"resolutions": { "resolutions": {

View File

@ -2,6 +2,7 @@
import "@spectrum-css/button/dist/index-vars.css" import "@spectrum-css/button/dist/index-vars.css"
import Tooltip from "../Tooltip/Tooltip.svelte" import Tooltip from "../Tooltip/Tooltip.svelte"
export let type
export let disabled = false export let disabled = false
export let size = "M" export let size = "M"
export let cta = false export let cta = false
@ -21,6 +22,7 @@
<button <button
{id} {id}
{type}
class:spectrum-Button--cta={cta} class:spectrum-Button--cta={cta}
class:spectrum-Button--primary={primary} class:spectrum-Button--primary={primary}
class:spectrum-Button--secondary={secondary} class:spectrum-Button--secondary={secondary}
@ -73,6 +75,7 @@
button { button {
position: relative; position: relative;
} }
.spectrum-Button-label { .spectrum-Button-label {
white-space: nowrap; white-space: nowrap;
overflow: hidden; overflow: hidden;

View File

@ -0,0 +1,19 @@
<script>
import { slide } from "svelte/transition"
export let error = null
</script>
<div transition:slide|local={{ duration: 130 }} class="error-message">
{error}
</div>
<style>
.error-message {
background: var(--spectrum-global-color-red-400);
color: white;
font-size: 14px;
padding: 6px 16px;
font-weight: 500;
}
</style>

View File

@ -1,7 +1,7 @@
<script> <script>
import Icon from "../Icon/Icon.svelte" import Icon from "../Icon/Icon.svelte"
import { getContext, onMount } from "svelte" import { getContext, onMount } from "svelte"
import { slide } from "svelte/transition" import ErrorMessage from "./ErrorMessage.svelte"
export let disabled = false export let disabled = false
export let error = null export let error = null
@ -55,9 +55,7 @@
{/if} {/if}
</div> </div>
{#if error} {#if error}
<div transition:slide|local={{ duration: 130 }} class="error-message"> <ErrorMessage {error} />
{error}
</div>
{/if} {/if}
</div> </div>
@ -110,13 +108,6 @@
.field { .field {
flex: 1 1 auto; flex: 1 1 auto;
} }
.error-message {
background: var(--spectrum-global-color-red-400);
color: white;
font-size: 14px;
padding: 6px 16px;
font-weight: 500;
}
.error-icon { .error-icon {
flex: 0 0 auto; flex: 0 0 auto;
} }

View File

@ -4,3 +4,4 @@ export { default as FancySelect } from "./FancySelect.svelte"
export { default as FancyButton } from "./FancyButton.svelte" export { default as FancyButton } from "./FancyButton.svelte"
export { default as FancyForm } from "./FancyForm.svelte" export { default as FancyForm } from "./FancyForm.svelte"
export { default as FancyButtonRadio } from "./FancyButtonRadio.svelte" export { default as FancyButtonRadio } from "./FancyButtonRadio.svelte"
export { default as ErrorMessage } from "./ErrorMessage.svelte"

View File

@ -62,6 +62,7 @@
"@budibase/frontend-core": "0.0.1", "@budibase/frontend-core": "0.0.1",
"@budibase/shared-core": "0.0.1", "@budibase/shared-core": "0.0.1",
"@budibase/string-templates": "0.0.1", "@budibase/string-templates": "0.0.1",
"@budibase/types": "0.0.1",
"@fortawesome/fontawesome-svg-core": "^6.2.1", "@fortawesome/fontawesome-svg-core": "^6.2.1",
"@fortawesome/free-brands-svg-icons": "^6.2.1", "@fortawesome/free-brands-svg-icons": "^6.2.1",
"@fortawesome/free-solid-svg-icons": "^6.2.1", "@fortawesome/free-solid-svg-icons": "^6.2.1",

View File

@ -22,6 +22,8 @@
export let rowCount export let rowCount
export let disableSorting = false export let disableSorting = false
export let customPlaceholder = false export let customPlaceholder = false
export let allowClickRows
export let allowEditing = true
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
@ -109,7 +111,9 @@
{rowCount} {rowCount}
{disableSorting} {disableSorting}
{customPlaceholder} {customPlaceholder}
allowEditRows={allowEditing}
showAutoColumns={!hideAutocolumns} showAutoColumns={!hideAutocolumns}
{allowClickRows}
on:clickrelationship={e => selectRelationship(e.detail)} on:clickrelationship={e => selectRelationship(e.detail)}
on:sort on:sort
> >

View File

@ -58,6 +58,7 @@
{loading} {loading}
{type} {type}
rowCount={10} rowCount={10}
allowEditing={false}
bind:hideAutocolumns bind:hideAutocolumns
> >
<ViewFilterButton {view} /> <ViewFilterButton {view} />

View File

@ -6,7 +6,8 @@
import NavItem from "components/common/NavItem.svelte" import NavItem from "components/common/NavItem.svelte"
import { goto, isActive } from "@roxi/routify" import { goto, isActive } from "@roxi/routify"
const alphabetical = (a, b) => a.name?.toLowerCase() > b.name?.toLowerCase() const alphabetical = (a, b) =>
a.name?.toLowerCase() > b.name?.toLowerCase() ? 1 : -1
export let sourceId export let sourceId

View File

@ -17,6 +17,7 @@
export let highlighted = false export let highlighted = false
export let rightAlignIcon = false export let rightAlignIcon = false
export let id export let id
export let showTooltip = false
const scrollApi = getContext("scroll") const scrollApi = getContext("scroll")
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
@ -84,7 +85,7 @@
<Icon color={iconColor} size="S" name={icon} /> <Icon color={iconColor} size="S" name={icon} />
</div> </div>
{/if} {/if}
<div class="text">{text}</div> <div class="text" title={showTooltip ? text : null}>{text}</div>
{#if withActions} {#if withActions}
<div class="actions"> <div class="actions">
<slot /> <slot />

View File

@ -0,0 +1,31 @@
<script>
import { Modal, ModalContent, Body } from "@budibase/bbui"
let modal
export let onConfirm
export function show() {
modal.show()
}
export function hide() {
modal.hide()
}
</script>
<Modal bind:this={modal} on:hide={modal}>
<ModalContent
title="Your account is currently de-activated"
size="S"
showCancelButton={true}
showCloseIcon={false}
confirmText={"View plans"}
{onConfirm}
>
<Body size="S"
>Due to the free plan user limit being exceeded, your account has been
de-activated. Upgrade your plan to re-activate your account.</Body
>
</ModalContent>
</Modal>

View File

@ -3,7 +3,6 @@ import { temporalStore } from "builderStore"
import { admin, auth, licensing } from "stores/portal" import { admin, auth, licensing } from "stores/portal"
import { get } from "svelte/store" import { get } from "svelte/store"
import { BANNER_TYPES } from "@budibase/bbui" import { BANNER_TYPES } from "@budibase/bbui"
import { capitalise } from "helpers"
const oneDayInSeconds = 86400 const oneDayInSeconds = 86400
@ -146,23 +145,19 @@ const buildUsersAboveLimitBanner = EXPIRY_KEY => {
const userLicensing = get(licensing) const userLicensing = get(licensing)
return { return {
key: EXPIRY_KEY, key: EXPIRY_KEY,
type: BANNER_TYPES.WARNING, type: BANNER_TYPES.NEGATIVE,
onChange: () => { onChange: () => {
defaultCacheFn(EXPIRY_KEY) defaultCacheFn(EXPIRY_KEY)
}, },
criteria: () => { criteria: () => {
return userLicensing.warnUserLimit return userLicensing.errUserLimit
}, },
message: `${capitalise( message: "Your Budibase account is de-activated. Upgrade your plan",
userLicensing.license.plan.type
)} plan changes - Users will be limited to ${
userLicensing.userLimit
} users in ${userLicensing.userLimitDays}`,
...{ ...{
extraButtonText: "Find out more", extraButtonText: "View plans",
extraButtonAction: () => { extraButtonAction: () => {
defaultCacheFn(ExpiringKeys.LICENSING_USERS_ABOVE_LIMIT_BANNER) defaultCacheFn(ExpiringKeys.LICENSING_USERS_ABOVE_LIMIT_BANNER)
window.location.href = "/builder/portal/users/users" window.location.href = "https://budibase.com/pricing/"
}, },
}, },
showCloseButton: true, showCloseButton: true,

View File

@ -6,6 +6,8 @@
export let app export let app
export let lockedAction
const handleDefaultClick = () => { const handleDefaultClick = () => {
if (window.innerWidth < 640) { if (window.innerWidth < 640) {
goToOverview() goToOverview()
@ -29,7 +31,7 @@
} }
</script> </script>
<div class="app-row" on:click={handleDefaultClick}> <div class="app-row" on:click={lockedAction || handleDefaultClick}>
<div class="title"> <div class="title">
<div class="app-icon"> <div class="app-icon">
<Icon size="L" name={app.icon?.name || "Apps"} color={app.icon?.color} /> <Icon size="L" name={app.icon?.name || "Apps"} color={app.icon?.color} />
@ -58,8 +60,11 @@
<div class="app-row-actions"> <div class="app-row-actions">
<AppLockModal {app} buttonSize="M" /> <AppLockModal {app} buttonSize="M" />
<Button size="S" secondary on:click={goToOverview}>Manage</Button> <Button size="S" secondary on:click={lockedAction || goToOverview}
<Button size="S" primary on:click={goToBuilder}>Edit</Button> >Manage</Button
>
<Button size="S" primary on:click={lockedAction || goToBuilder}>Edit</Button
>
</div> </div>
</div> </div>

View File

@ -28,13 +28,16 @@
let inviting = false let inviting = false
let searchFocus = false let searchFocus = false
// Initially filter entities without app access
// Show all when false
let filterByAppAccess = true
let appInvites = [] let appInvites = []
let filteredInvites = [] let filteredInvites = []
let filteredUsers = [] let filteredUsers = []
let filteredGroups = [] let filteredGroups = []
let selectedGroup let selectedGroup
let userOnboardResponse = null let userOnboardResponse = null
let userLimitReachedModal let userLimitReachedModal
$: queryIsEmail = emailValidator(query) === true $: queryIsEmail = emailValidator(query) === true
@ -52,15 +55,32 @@
} }
const filterInvites = async query => { const filterInvites = async query => {
appInvites = await getInvites() if (!prodAppId) {
if (!query || query == "") {
filteredInvites = appInvites
return return
} }
filteredInvites = appInvites.filter(invite => invite.email.includes(query))
appInvites = await getInvites()
//On Focus behaviour
if (!filterByAppAccess && !query) {
filteredInvites =
appInvites.length > 100 ? appInvites.slice(0, 100) : [...appInvites]
return
}
filteredInvites = appInvites.filter(invite => {
const inviteInfo = invite.info?.apps
if (!query && inviteInfo && prodAppId) {
return Object.keys(inviteInfo).includes(prodAppId)
}
return invite.email.includes(query)
})
} }
$: filterInvites(query) $: filterByAppAccess, prodAppId, filterInvites(query)
$: if (searchFocus === true) {
filterByAppAccess = false
}
const usersFetch = fetchData({ const usersFetch = fetchData({
API, API,
@ -79,9 +99,9 @@
} }
await usersFetch.update({ await usersFetch.update({
query: { query: {
appId: query ? null : prodAppId, appId: query || !filterByAppAccess ? null : prodAppId,
email: query, email: query,
paginated: query ? null : false, paginated: query || !filterByAppAccess ? null : false,
}, },
}) })
await usersFetch.refresh() await usersFetch.refresh()
@ -107,7 +127,12 @@
} }
const debouncedUpdateFetch = Utils.debounce(searchUsers, 250) const debouncedUpdateFetch = Utils.debounce(searchUsers, 250)
$: debouncedUpdateFetch(query, $store.builderSidePanel, loaded) $: debouncedUpdateFetch(
query,
$store.builderSidePanel,
loaded,
filterByAppAccess
)
const updateAppUser = async (user, role) => { const updateAppUser = async (user, role) => {
if (!prodAppId) { if (!prodAppId) {
@ -182,9 +207,10 @@
} }
const searchGroups = (userGroups, query) => { const searchGroups = (userGroups, query) => {
let filterGroups = query?.length let filterGroups =
? userGroups query?.length || !filterByAppAccess
: getAppGroups(userGroups, prodAppId) ? userGroups
: getAppGroups(userGroups, prodAppId)
return filterGroups return filterGroups
.filter(group => { .filter(group => {
if (!query?.length) { if (!query?.length) {
@ -214,7 +240,7 @@
} }
// Adds the 'role' attribute and sets it to the current app. // Adds the 'role' attribute and sets it to the current app.
$: enrichedGroups = getEnrichedGroups($groups) $: enrichedGroups = getEnrichedGroups($groups, filterByAppAccess)
$: filteredGroups = searchGroups(enrichedGroups, query) $: filteredGroups = searchGroups(enrichedGroups, query)
$: groupUsers = buildGroupUsers(filteredGroups, filteredUsers) $: groupUsers = buildGroupUsers(filteredGroups, filteredUsers)
$: allUsers = [...filteredUsers, ...groupUsers] $: allUsers = [...filteredUsers, ...groupUsers]
@ -226,7 +252,7 @@
specific roles for the app. specific roles for the app.
*/ */
const buildGroupUsers = (userGroups, filteredUsers) => { const buildGroupUsers = (userGroups, filteredUsers) => {
if (query) { if (query || !filterByAppAccess) {
return [] return []
} }
// Must exclude users who have explicit privileges // Must exclude users who have explicit privileges
@ -321,12 +347,12 @@
[prodAppId]: role, [prodAppId]: role,
}, },
}) })
await filterInvites() await filterInvites(query)
} }
const onUninviteAppUser = async invite => { const onUninviteAppUser = async invite => {
await uninviteAppUser(invite) await uninviteAppUser(invite)
await filterInvites() await filterInvites(query)
} }
// Purge only the app from the invite or recind the invite if only 1 app remains? // Purge only the app from the invite or recind the invite if only 1 app remains?
@ -351,7 +377,6 @@
onMount(() => { onMount(() => {
rendered = true rendered = true
searchFocus = true
}) })
function handleKeyDown(evt) { function handleKeyDown(evt) {
@ -417,7 +442,6 @@
autocomplete="off" autocomplete="off"
disabled={inviting} disabled={inviting}
value={query} value={query}
autofocus
on:input={e => { on:input={e => {
query = e.target.value.trim() query = e.target.value.trim()
}} }}
@ -428,16 +452,20 @@
<span <span
class="search-input-icon" class="search-input-icon"
class:searching={query} class:searching={query || !filterByAppAccess}
on:click={() => { on:click={() => {
if (!filterByAppAccess) {
filterByAppAccess = true
}
if (!query) { if (!query) {
return return
} }
query = null query = null
userOnboardResponse = null userOnboardResponse = null
filterByAppAccess = true
}} }}
> >
<Icon name={query ? "Close" : "Search"} /> <Icon name={!filterByAppAccess || query ? "Close" : "Search"} />
</span> </span>
</div> </div>
@ -555,7 +583,7 @@
{#if filteredUsers?.length} {#if filteredUsers?.length}
<div class="auth-entity-section"> <div class="auth-entity-section">
<div class="auth-entity-header "> <div class="auth-entity-header">
<div class="auth-entity-title">Users</div> <div class="auth-entity-title">Users</div>
<div class="auth-entity-access-title">Access</div> <div class="auth-entity-access-title">Access</div>
</div> </div>
@ -696,7 +724,7 @@
max-width: calc(100vw - 40px); max-width: calc(100vw - 40px);
background: var(--background); background: var(--background);
border-left: var(--border-light); border-left: var(--border-light);
z-index: 3; z-index: 999;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
overflow-y: auto; overflow-y: auto;

View File

@ -59,6 +59,7 @@
text={screen.routing.route} text={screen.routing.route}
on:click={() => store.actions.screens.select(screen._id)} on:click={() => store.actions.screens.select(screen._id)}
rightAlignIcon rightAlignIcon
showTooltip
> >
<ScreenDropdownMenu screenId={screen._id} /> <ScreenDropdownMenu screenId={screen._id} />
<RoleIndicator slot="right" roleId={screen.routing.roleId} /> <RoleIndicator slot="right" roleId={screen.routing.roleId} />

View File

@ -133,7 +133,7 @@
</Body> </Body>
</Layout> </Layout>
<Divider /> <Divider />
{#if $licensing.usageMetrics?.dayPasses >= 100} {#if $licensing.usageMetrics?.dayPasses >= 100 || $licensing.errUserLimit}
<div> <div>
<Layout gap="S" justifyItems="center"> <Layout gap="S" justifyItems="center">
<img class="spaceman" alt="spaceman" src={Spaceman} /> <img class="spaceman" alt="spaceman" src={Spaceman} />

View File

@ -14,6 +14,7 @@
import Spinner from "components/common/Spinner.svelte" import Spinner from "components/common/Spinner.svelte"
import CreateAppModal from "components/start/CreateAppModal.svelte" import CreateAppModal from "components/start/CreateAppModal.svelte"
import AppLimitModal from "components/portal/licensing/AppLimitModal.svelte" import AppLimitModal from "components/portal/licensing/AppLimitModal.svelte"
import AccountLockedModal from "components/portal/licensing/AccountLockedModal.svelte"
import { store, automationStore } from "builderStore" import { store, automationStore } from "builderStore"
import { API } from "api" import { API } from "api"
@ -28,6 +29,7 @@
let template let template
let creationModal let creationModal
let appLimitModal let appLimitModal
let accountLockedModal
let creatingApp = false let creatingApp = false
let searchTerm = "" let searchTerm = ""
let creatingFromTemplate = false let creatingFromTemplate = false
@ -48,6 +50,11 @@
: true) : true)
) )
$: automationErrors = getAutomationErrors(enrichedApps) $: automationErrors = getAutomationErrors(enrichedApps)
$: isOwner = $auth.accountPortalAccess && $admin.cloud
const usersLimitLockAction = $licensing?.errUserLimit
? () => accountLockedModal.show()
: null
const enrichApps = (apps, user, sortBy) => { const enrichApps = (apps, user, sortBy) => {
const enrichedApps = apps.map(app => ({ const enrichedApps = apps.map(app => ({
@ -189,6 +196,9 @@
creatingFromTemplate = true creatingFromTemplate = true
createAppFromTemplateUrl(initInfo.init_template) createAppFromTemplateUrl(initInfo.init_template)
} }
if (usersLimitLockAction) {
usersLimitLockAction()
}
} catch (error) { } catch (error) {
notifications.error("Error getting init info") notifications.error("Error getting init info")
} }
@ -230,20 +240,30 @@
<Layout noPadding gap="L"> <Layout noPadding gap="L">
<div class="title"> <div class="title">
<div class="buttons"> <div class="buttons">
<Button size="M" cta on:click={initiateAppCreation}> <Button
size="M"
cta
on:click={usersLimitLockAction || initiateAppCreation}
>
Create new app Create new app
</Button> </Button>
{#if $apps?.length > 0} {#if $apps?.length > 0}
<Button <Button
size="M" size="M"
secondary secondary
on:click={$goto("/builder/portal/apps/templates")} on:click={usersLimitLockAction ||
$goto("/builder/portal/apps/templates")}
> >
View templates View templates
</Button> </Button>
{/if} {/if}
{#if !$apps?.length} {#if !$apps?.length}
<Button size="L" quiet secondary on:click={initiateAppImport}> <Button
size="L"
quiet
secondary
on:click={usersLimitLockAction || initiateAppImport}
>
Import app Import app
</Button> </Button>
{/if} {/if}
@ -267,7 +287,7 @@
<div class="app-table"> <div class="app-table">
{#each filteredApps as app (app.appId)} {#each filteredApps as app (app.appId)}
<AppRow {app} /> <AppRow {app} lockedAction={usersLimitLockAction} />
{/each} {/each}
</div> </div>
</Layout> </Layout>
@ -294,6 +314,11 @@
</Modal> </Modal>
<AppLimitModal bind:this={appLimitModal} /> <AppLimitModal bind:this={appLimitModal} />
<AccountLockedModal
bind:this={accountLockedModal}
onConfirm={() =>
isOwner ? $licensing.goToUpgradePage() : $licensing.goToPricingPage()}
/>
<style> <style>
.title { .title {

View File

@ -107,8 +107,9 @@
useSampleData, useSampleData,
isGoogle, isGoogle,
}) => { }) => {
let app
try { try {
const app = await createApp(useSampleData) app = await createApp(useSampleData)
let datasource let datasource
if (datasourceConfig) { if (datasourceConfig) {
@ -134,6 +135,17 @@
console.log(e) console.log(e)
creationLoading = false creationLoading = false
notifications.error("There was a problem creating your app") notifications.error("There was a problem creating your app")
// Reset the store so that we don't send up stale headers
store.actions.reset()
// If we successfully created an app, delete it again so that we
// can try again once the error has been corrected.
// This also ensures onboarding can't be skipped by entering invalid
// data credentials.
if (app?.appId) {
await API.deleteApp(app.appId)
}
} }
} }
</script> </script>
@ -146,80 +158,87 @@
/> />
</Modal> </Modal>
<SplitPage> <div class="full-width">
{#if stage === "name"} <SplitPage>
<NamePanel bind:name bind:url onNext={() => (stage = "data")} /> {#if stage === "name"}
{:else if googleComplete} <NamePanel bind:name bind:url onNext={() => (stage = "data")} />
<div class="centered"> {:else if googleComplete}
<Body <div class="centered">
>Please login to your Google account in the new tab which as opened to <Body
continue.</Body >Please login to your Google account in the new tab which as opened to
> continue.</Body
</div> >
{:else if integrationsLoading || creationLoading}
<div class="centered">
<Spinner />
</div>
{:else if stage === "data"}
<DataPanel onBack={() => (stage = "name")}>
<div class="dataButton">
<FancyButton on:click={() => handleCreateApp({ useSampleData: true })}>
<div class="dataButtonContent">
<div class="dataButtonIcon">
<img
alt="Budibase Logo"
class="budibaseLogo"
src={"https://i.imgur.com/Xhdt1YP.png"}
/>
</div>
Budibase Sample data
</div>
</FancyButton>
</div> </div>
<div class="dataButton"> {:else if integrationsLoading || creationLoading}
<FancyButton on:click={uploadModal.show}> <div class="centered">
<div class="dataButtonContent"> <Spinner />
<div class="dataButtonIcon">
<FontAwesomeIcon name="fa-solid fa-file-arrow-up" />
</div>
Upload data (CSV or JSON)
</div>
</FancyButton>
</div> </div>
{#each Object.entries(plusIntegrations) as [integrationType, schema]} {:else if stage === "data"}
<DataPanel onBack={() => (stage = "name")}>
<div class="dataButton"> <div class="dataButton">
<FancyButton on:click={() => (stage = integrationType)}> <FancyButton
on:click={() => handleCreateApp({ useSampleData: true })}
>
<div class="dataButtonContent"> <div class="dataButtonContent">
<div class="dataButtonIcon"> <div class="dataButtonIcon">
<IntegrationIcon {integrationType} {schema} /> <img
alt="Budibase Logo"
class="budibaseLogo"
src={"https://i.imgur.com/Xhdt1YP.png"}
/>
</div> </div>
{schema.friendlyName} Budibase Sample data
</div> </div>
</FancyButton> </FancyButton>
</div> </div>
{/each} <div class="dataButton">
</DataPanel> <FancyButton on:click={uploadModal.show}>
{:else if stage in plusIntegrations} <div class="dataButtonContent">
<DatasourceConfigPanel <div class="dataButtonIcon">
title={plusIntegrations[stage].friendlyName} <FontAwesomeIcon name="fa-solid fa-file-arrow-up" />
fields={plusIntegrations[stage].datasource} </div>
type={stage} Upload data (CSV or JSON)
onBack={() => (stage = "data")} </div>
onNext={data => { </FancyButton>
const isGoogle = data.isGoogle </div>
delete data.isGoogle {#each Object.entries(plusIntegrations) as [integrationType, schema]}
return handleCreateApp({ datasourceConfig: data, isGoogle }) <div class="dataButton">
}} <FancyButton on:click={() => (stage = integrationType)}>
/> <div class="dataButtonContent">
{:else} <div class="dataButtonIcon">
<p>There was an problem. Please refresh the page and try again.</p> <IntegrationIcon {integrationType} {schema} />
{/if} </div>
<div slot="right"> {schema.friendlyName}
<ExampleApp {name} showData={stage !== "name"} /> </div>
</div> </FancyButton>
</SplitPage> </div>
{/each}
</DataPanel>
{:else if stage in plusIntegrations}
<DatasourceConfigPanel
title={plusIntegrations[stage].friendlyName}
fields={plusIntegrations[stage].datasource}
type={stage}
onBack={() => (stage = "data")}
onNext={data => {
const isGoogle = data.isGoogle
delete data.isGoogle
return handleCreateApp({ datasourceConfig: data, isGoogle })
}}
/>
{:else}
<p>There was an problem. Please refresh the page and try again.</p>
{/if}
<div slot="right">
<ExampleApp {name} showData={stage !== "name"} />
</div>
</SplitPage>
</div>
<style> <style>
.full-width {
width: 100%;
}
.centered { .centered {
display: flex; display: flex;
justify-content: center; justify-content: center;

View File

@ -1,7 +1,7 @@
<script> <script>
import EditUserPicker from "./EditUserPicker.svelte" import EditUserPicker from "./EditUserPicker.svelte"
import { Heading, Pagination, Table } from "@budibase/bbui" import { Heading, Pagination, Table, Search } from "@budibase/bbui"
import { fetchData } from "@budibase/frontend-core" import { fetchData } from "@budibase/frontend-core"
import { goto } from "@roxi/routify" import { goto } from "@roxi/routify"
import { API } from "api" import { API } from "api"
@ -12,7 +12,9 @@
export let groupId export let groupId
const fetchGroupUsers = fetchData({ let emailSearch
let fetchGroupUsers
$: fetchGroupUsers = fetchData({
API, API,
datasource: { datasource: {
type: "groupUser", type: "groupUser",
@ -20,6 +22,7 @@
options: { options: {
query: { query: {
groupId, groupId,
emailSearch,
}, },
}, },
}) })
@ -59,24 +62,31 @@
</script> </script>
<div class="header"> <div class="header">
<Heading size="S">Users</Heading>
{#if !scimEnabled} {#if !scimEnabled}
<EditUserPicker {groupId} onUsersUpdated={fetchGroupUsers.getInitialData} /> <EditUserPicker {groupId} onUsersUpdated={fetchGroupUsers.getInitialData} />
{:else} {:else}
<ScimBanner /> <ScimBanner />
{/if} {/if}
</div>
<div class="controls-right">
<Search bind:value={emailSearch} placeholder="Search email" />
</div>
</div>
<Table <Table
schema={userSchema} schema={userSchema}
data={$fetchGroupUsers?.rows} data={$fetchGroupUsers?.rows}
loading={$fetchGroupUsers.loading}
allowEditRows={false} allowEditRows={false}
customPlaceholder customPlaceholder
customRenderers={customUserTableRenderers} customRenderers={customUserTableRenderers}
on:click={e => $goto(`../users/${e.detail._id}`)} on:click={e => $goto(`../users/${e.detail._id}`)}
> >
<div class="placeholder" slot="placeholder"> <div class="placeholder" slot="placeholder">
<Heading size="S">This user group doesn't have any users</Heading> <Heading size="S"
>{emailSearch
? `No users found matching the email "${emailSearch}"`
: "This user group doesn't have any users"}</Heading
>
</div> </div>
</Table> </Table>
@ -98,7 +108,7 @@
.header { .header {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
justify-content: flex-start; justify-content: space-between;
align-items: center; align-items: center;
gap: var(--spacing-l); gap: var(--spacing-l);
} }
@ -109,4 +119,15 @@
width: 100%; width: 100%;
text-align: center; text-align: center;
} }
.controls-right {
display: flex;
flex-direction: row;
justify-content: flex-end;
align-items: center;
gap: var(--spacing-xl);
}
.controls-right :global(.spectrum-Search) {
width: 200px;
}
</style> </style>

View File

@ -30,8 +30,8 @@
$: hasError = userData.find(x => x.error != null) $: hasError = userData.find(x => x.error != null)
$: userCount = $licensing.userCount + userData.length $: userCount = $licensing.userCount + userData.length
$: willReach = licensing.willReachUserLimit(userCount) $: reached = licensing.usersLimitReached(userCount)
$: willExceed = licensing.willExceedUserLimit(userCount) $: exceeded = licensing.usersLimitExceeded(userCount)
function removeInput(idx) { function removeInput(idx) {
userData = userData.filter((e, i) => i !== idx) userData = userData.filter((e, i) => i !== idx)
@ -87,7 +87,7 @@
confirmDisabled={disabled} confirmDisabled={disabled}
cancelText="Cancel" cancelText="Cancel"
showCloseIcon={false} showCloseIcon={false}
disabled={hasError || !userData.length || willExceed} disabled={hasError || !userData.length || exceeded}
> >
<Layout noPadding gap="XS"> <Layout noPadding gap="XS">
<Label>Email address</Label> <Label>Email address</Label>
@ -118,7 +118,7 @@
</div> </div>
{/each} {/each}
{#if willReach} {#if reached}
<div class="user-notification"> <div class="user-notification">
<Icon name="Info" /> <Icon name="Info" />
<span> <span>

View File

@ -25,10 +25,10 @@
$: invalidEmails = [] $: invalidEmails = []
$: userCount = $licensing.userCount + userEmails.length $: userCount = $licensing.userCount + userEmails.length
$: willExceed = licensing.willExceedUserLimit(userCount) $: exceed = licensing.usersLimitExceeded(userCount)
$: importDisabled = $: importDisabled =
!userEmails.length || !validEmails(userEmails) || !usersRole || willExceed !userEmails.length || !validEmails(userEmails) || !usersRole || exceed
const validEmails = userEmails => { const validEmails = userEmails => {
if ($admin.cloud && userEmails.length > MAX_USERS_UPLOAD_LIMIT) { if ($admin.cloud && userEmails.length > MAX_USERS_UPLOAD_LIMIT) {
@ -93,7 +93,7 @@
</label> </label>
</div> </div>
{#if willExceed} {#if exceed}
<div class="user-notification"> <div class="user-notification">
<Icon name="Info" /> <Icon name="Info" />
{capitalise($licensing.license.plan.type)} plan is limited to {$licensing.userLimit} {capitalise($licensing.license.plan.type)} plan is limited to {$licensing.userLimit}

View File

@ -88,6 +88,16 @@
}, },
} }
const getPendingSchema = tblSchema => {
if (!tblSchema) {
return {}
}
let pendingSchema = JSON.parse(JSON.stringify(tblSchema))
pendingSchema.email.displayName = "Pending Invites"
return pendingSchema
}
$: pendingSchema = getPendingSchema(schema)
$: userData = [] $: userData = []
$: inviteUsersResponse = { successful: [], unsuccessful: [] } $: inviteUsersResponse = { successful: [], unsuccessful: [] }
$: { $: {
@ -110,6 +120,24 @@
} }
}) })
} }
let invitesLoaded = false
let pendingInvites = []
let parsedInvites = []
const invitesToSchema = invites => {
return invites.map(invite => {
const { admin, builder, userGroups, apps } = invite.info
return {
email: invite.email,
builder,
admin,
userGroups: userGroups,
apps: apps ? [...new Set(Object.keys(apps))] : undefined,
}
})
}
$: parsedInvites = invitesToSchema(pendingInvites)
const updateFetch = email => { const updateFetch = email => {
fetch.update({ fetch.update({
@ -144,6 +172,7 @@
})) }))
try { try {
inviteUsersResponse = await users.invite(payload) inviteUsersResponse = await users.invite(payload)
pendingInvites = await users.getInvites()
inviteConfirmationModal.show() inviteConfirmationModal.show()
} catch (error) { } catch (error) {
notifications.error("Error inviting user") notifications.error("Error inviting user")
@ -232,12 +261,13 @@
try { try {
await groups.actions.init() await groups.actions.init()
groupsLoaded = true groupsLoaded = true
pendingInvites = await users.getInvites()
invitesLoaded = true
} catch (error) { } catch (error) {
notifications.error("Error fetching user group data") notifications.error("Error fetching user group data")
} }
}) })
let staticUserLimit = $licensing.license.quotas.usage.static.users.value
</script> </script>
<Layout noPadding gap="M"> <Layout noPadding gap="M">
@ -246,7 +276,7 @@
<Body>Add users and control who gets access to your published apps</Body> <Body>Add users and control who gets access to your published apps</Body>
</Layout> </Layout>
<Divider /> <Divider />
{#if $licensing.warnUserLimit} {#if $licensing.errUserLimit}
<InlineAlert <InlineAlert
type="error" type="error"
onConfirm={() => { onConfirm={() => {
@ -258,13 +288,9 @@
}} }}
buttonText={isOwner ? "Upgrade" : "View plans"} buttonText={isOwner ? "Upgrade" : "View plans"}
cta cta
header={`Users will soon be limited to ${staticUserLimit}`} header="Account de-activated"
message={`Our free plan is going to be limited to ${staticUserLimit} users in ${$licensing.userLimitDays}. message="Due to the free plan user limit being exceeded, your account has been de-activated.
Upgrade your plan to re-activate your account."
This means any users exceeding the limit will be de-activated.
De-activated users will not able to access the builder or any published apps until you upgrade to one of our paid plans.
`}
/> />
{/if} {/if}
<div class="controls"> <div class="controls">
@ -324,6 +350,15 @@
goToNextPage={fetch.nextPage} goToNextPage={fetch.nextPage}
/> />
</div> </div>
<Table
schema={pendingSchema}
data={parsedInvites}
allowEditColumns={false}
allowEditRows={false}
{customRenderers}
loading={!invitesLoaded}
allowClickRows={false}
/>
</Layout> </Layout>
<Modal bind:this={createUserModal}> <Modal bind:this={createUserModal}>

View File

@ -4,7 +4,7 @@ import { auth, admin } from "stores/portal"
import { Constants } from "@budibase/frontend-core" import { Constants } from "@budibase/frontend-core"
import { StripeStatus } from "components/portal/licensing/constants" import { StripeStatus } from "components/portal/licensing/constants"
import { TENANT_FEATURE_FLAGS, isEnabled } from "helpers/featureFlags" import { TENANT_FEATURE_FLAGS, isEnabled } from "helpers/featureFlags"
import dayjs from "dayjs" import { PlanModel } from "@budibase/types"
const UNLIMITED = -1 const UNLIMITED = -1
@ -12,6 +12,7 @@ export const createLicensingStore = () => {
const DEFAULT = { const DEFAULT = {
// navigation // navigation
goToUpgradePage: () => {}, goToUpgradePage: () => {},
goToPricingPage: () => {},
// the top level license // the top level license
license: undefined, license: undefined,
isFreePlan: true, isFreePlan: true,
@ -37,29 +38,37 @@ export const createLicensingStore = () => {
// user limits // user limits
userCount: undefined, userCount: undefined,
userLimit: undefined, userLimit: undefined,
userLimitDays: undefined,
userLimitReached: false, userLimitReached: false,
warnUserLimit: false, errUserLimit: false,
} }
const oneDayInMilliseconds = 86400000 const oneDayInMilliseconds = 86400000
const store = writable(DEFAULT) const store = writable(DEFAULT)
function willReachUserLimit(userCount, userLimit) { function usersLimitReached(userCount, userLimit) {
if (userLimit === UNLIMITED) { if (userLimit === UNLIMITED) {
return false return false
} }
return userCount >= userLimit return userCount >= userLimit
} }
function willExceedUserLimit(userCount, userLimit) { function usersLimitExceeded(userCount, userLimit) {
if (userLimit === UNLIMITED) { if (userLimit === UNLIMITED) {
return false return false
} }
return userCount > userLimit return userCount > userLimit
} }
async function isCloud() {
let adminStore = get(admin)
if (!adminStore.loaded) {
await admin.init()
adminStore = get(admin)
}
return adminStore.cloud
}
const actions = { const actions = {
init: async () => { init: async () => {
actions.setNavigation() actions.setNavigation()
@ -71,10 +80,14 @@ export const createLicensingStore = () => {
const goToUpgradePage = () => { const goToUpgradePage = () => {
window.location.href = upgradeUrl window.location.href = upgradeUrl
} }
const goToPricingPage = () => {
window.open("https://budibase.com/pricing/", "_blank")
}
store.update(state => { store.update(state => {
return { return {
...state, ...state,
goToUpgradePage, goToUpgradePage,
goToPricingPage,
} }
}) })
}, },
@ -128,15 +141,15 @@ export const createLicensingStore = () => {
quotaUsage, quotaUsage,
} }
}) })
actions.setUsageMetrics() await actions.setUsageMetrics()
}, },
willReachUserLimit: userCount => { usersLimitReached: userCount => {
return willReachUserLimit(userCount, get(store).userLimit) return usersLimitReached(userCount, get(store).userLimit)
}, },
willExceedUserLimit(userCount) { usersLimitExceeded(userCount) {
return willExceedUserLimit(userCount, get(store).userLimit) return usersLimitExceeded(userCount, get(store).userLimit)
}, },
setUsageMetrics: () => { setUsageMetrics: async () => {
if (isEnabled(TENANT_FEATURE_FLAGS.LICENSING)) { if (isEnabled(TENANT_FEATURE_FLAGS.LICENSING)) {
const usage = get(store).quotaUsage const usage = get(store).quotaUsage
const license = get(auth).user.license const license = get(auth).user.license
@ -198,11 +211,13 @@ export const createLicensingStore = () => {
const userQuota = license.quotas.usage.static.users const userQuota = license.quotas.usage.static.users
const userLimit = userQuota?.value const userLimit = userQuota?.value
const userCount = usage.usageQuota.users const userCount = usage.usageQuota.users
const userLimitReached = willReachUserLimit(userCount, userLimit) const userLimitReached = usersLimitReached(userCount, userLimit)
const userLimitExceeded = willExceedUserLimit(userCount, userLimit) const userLimitExceeded = usersLimitExceeded(userCount, userLimit)
const days = dayjs(userQuota?.startDate).diff(dayjs(), "day") const isCloudAccount = await isCloud()
const userLimitDays = days > 1 ? `${days} days` : "1 day" const errUserLimit =
const warnUserLimit = userQuota?.startDate && userLimitExceeded isCloudAccount &&
license.plan.model === PlanModel.PER_USER &&
userLimitExceeded
store.update(state => { store.update(state => {
return { return {
@ -217,9 +232,8 @@ export const createLicensingStore = () => {
// user limits // user limits
userCount, userCount,
userLimit, userLimit,
userLimitDays,
userLimitReached, userLimitReached,
warnUserLimit, errUserLimit,
} }
}) })
} }

View File

@ -2,9 +2,9 @@
"name": "@budibase/cli", "name": "@budibase/cli",
"version": "0.0.1", "version": "0.0.1",
"description": "Budibase CLI, for developers, self hosting and migrations.", "description": "Budibase CLI, for developers, self hosting and migrations.",
"main": "dist/index.js", "main": "dist/src/index.js",
"bin": { "bin": {
"budi": "dist/index.js" "budi": "dist/src/index.js"
}, },
"author": "Budibase", "author": "Budibase",
"license": "GPL-3.0", "license": "GPL-3.0",

View File

@ -2,17 +2,17 @@
process.env.DISABLE_PINO_LOGGER = "1" process.env.DISABLE_PINO_LOGGER = "1"
import "./prebuilds" import "./prebuilds"
import "./environment" import "./environment"
import { env } from "@budibase/backend-core"
import { getCommands } from "./options" import { getCommands } from "./options"
import { Command } from "commander" import { Command } from "commander"
import { getHelpDescription } from "./utils" import { getHelpDescription } from "./utils"
import { version } from "../package.json"
// add hosting config // add hosting config
async function init() { async function init() {
const program = new Command() const program = new Command()
.addHelpCommand("help", getHelpDescription("Help with Budibase commands.")) .addHelpCommand("help", getHelpDescription("Help with Budibase commands."))
.helpOption(false) .helpOption(false)
.version(env.VERSION) .version(version)
// add commands // add commands
for (let command of getCommands()) { for (let command of getCommands()) {
command.configure(program) command.configure(program)

View File

@ -13,7 +13,7 @@ if (!process.argv[0].includes("node")) {
} }
function checkForBinaries() { function checkForBinaries() {
const readDir = join(__filename, "..", "..", PREBUILDS, ARCH) const readDir = join(__filename, "..", "..", "..", PREBUILDS, ARCH)
if (fs.existsSync(PREBUILD_DIR) || !fs.existsSync(readDir)) { if (fs.existsSync(PREBUILD_DIR) || !fs.existsSync(readDir)) {
return return
} }

View File

@ -5,6 +5,7 @@
"declaration": true, "declaration": true,
"sourceMap": true, "sourceMap": true,
"baseUrl": ".", "baseUrl": ".",
"resolveJsonModule": true,
"paths": { "paths": {
"@budibase/types": ["../types/src"], "@budibase/types": ["../types/src"],
"@budibase/backend-core": ["../backend-core/src"], "@budibase/backend-core": ["../backend-core/src"],
@ -16,6 +17,6 @@
"swc": true "swc": true
}, },
"references": [{ "path": "../types" }, { "path": "../backend-core" }], "references": [{ "path": "../types" }, { "path": "../backend-core" }],
"include": ["src/**/*"], "include": ["src/**/*", "package.json"],
"exclude": ["node_modules", "dist"] "exclude": ["node_modules", "dist"]
} }

View File

@ -55,10 +55,13 @@ export const buildGroupsEndpoints = API => {
/** /**
* Gets a group users by the group id * Gets a group users by the group id
*/ */
getGroupUsers: async ({ id, bookmark }) => { getGroupUsers: async ({ id, bookmark, emailSearch }) => {
let url = `/api/global/groups/${id}/users?` let url = `/api/global/groups/${id}/users?`
if (bookmark) { if (bookmark) {
url += `bookmark=${bookmark}` url += `bookmark=${bookmark}&`
}
if (emailSearch) {
url += `emailSearch=${emailSearch}&`
} }
return await API.get({ return await API.get({

View File

@ -0,0 +1,67 @@
<script>
import { Layout } from "@budibase/bbui"
import Bulgaria from "../../assets/bulgaria.png"
import Covanta from "../../assets/covanta.png"
import Schnellecke from "../../assets/schnellecke.png"
const testimonials = [
{
text: "Budibase was the only solution that checked all the boxes for Covanta. Covanta expects to realize $3.2MM in savings due to the elimination of redundant data entry.",
name: "Charles Link",
role: "Senior Director, Data and Analytics",
image: Covanta,
imageSize: 105,
},
{
text: "Budibase was mission-critical for us and went a long way in preventing what could have become a humanitarian crisis here in Bulgaria.",
name: "Bozhidar Bozhanov",
role: "Government of Bulgaria",
image: Bulgaria,
imageSize: 49,
},
{
text: "Centralization of authentication, quick turnaround time for requests, integration with different database systems has given it the edge and its now used daily for internal development for those apps that you know you need but dont feel value in losing days of development to reinvent the wheel.",
name: "Davide Lenzarini",
role: "IT manager",
image: Schnellecke,
imageSize: 141,
},
]
const testimonial = testimonials[Math.floor(Math.random() * 3)]
</script>
<div class="testimonial">
<Layout noPadding gap="S">
<img
width={testimonial.imageSize}
alt="a-happy-budibase-user"
src={testimonial.image}
/>
<div class="text">
"{testimonial.text}"
</div>
<div class="author">
<div class="name">{testimonial.name}</div>
<div class="company">{testimonial.role}</div>
</div>
</Layout>
</div>
<style>
.testimonial {
width: 380px;
padding: 40px;
}
.text {
font-size: var(--font-size-l);
font-style: italic;
}
.name {
font-weight: bold;
color: var(--spectrum-global-color-gray-900);
font-size: var(--font-size-l);
}
.company {
color: var(--spectrum-global-color-gray-700);
}
</style>

View File

@ -1,58 +1,15 @@
<script> <script>
import SplitPage from "./SplitPage.svelte" import SplitPage from "./SplitPage.svelte"
import { Layout } from "@budibase/bbui" import Testimonial from "./Testimonial.svelte"
import Bulgaria from "../../assets/bulgaria.png"
import Covanta from "../../assets/covanta.png"
import Schnellecke from "../../assets/schnellecke.png"
export let enabled = true export let enabled = true
const testimonials = [
{
text: "Budibase was the only solution that checked all the boxes for Covanta. Covanta expects to realize $3.2MM in savings due to the elimination of redundant data entry.",
name: "Charles Link",
role: "Senior Director, Data and Analytics",
image: Covanta,
imageSize: 105,
},
{
text: "Budibase was mission-critical for us and went a long way in preventing what could have become a humanitarian crisis here in Bulgaria.",
name: "Bozhidar Bozhanov",
role: "Government of Bulgaria",
image: Bulgaria,
imageSize: 49,
},
{
text: "Centralization of authentication, quick turnaround time for requests, integration with different database systems has given it the edge and its now used daily for internal development for those apps that you know you need but dont feel value in losing days of development to reinvent the wheel.",
name: "Davide Lenzarini",
role: "IT manager",
image: Schnellecke,
imageSize: 141,
},
]
const testimonial = testimonials[Math.floor(Math.random() * 3)]
</script> </script>
<SplitPage> <SplitPage>
<slot /> <slot />
<div class:wrapper={enabled} slot="right"> <div class:wrapper={enabled} slot="right">
{#if enabled} {#if enabled}
<div class="testimonial"> <Testimonial />
<Layout noPadding gap="S">
<img
width={testimonial.imageSize}
alt="a-happy-budibase-user"
src={testimonial.image}
/>
<div class="text">
"{testimonial.text}"
</div>
<div class="author">
<div class="name">{testimonial.name}</div>
<div class="company">{testimonial.role}</div>
</div>
</Layout>
</div>
{/if} {/if}
</div> </div>
</SplitPage> </SplitPage>
@ -64,20 +21,4 @@
display: grid; display: grid;
place-items: center; place-items: center;
} }
.testimonial {
width: 380px;
padding: 40px;
}
.text {
font-size: var(--font-size-l);
font-style: italic;
}
.name {
font-weight: bold;
color: var(--spectrum-global-color-gray-900);
font-size: var(--font-size-l);
}
.company {
color: var(--spectrum-global-color-gray-700);
}
</style> </style>

View File

@ -13,10 +13,10 @@
let flatpickr let flatpickr
let isOpen let isOpen
// adding the 0- will turn a string like 00:00:00 into a valid ISO // Adding the 0- will turn a string like 00:00:00 into a valid ISO
// date, but will make actual ISO dates invalid // date, but will make actual ISO dates invalid
$: time = new Date(`0-${value}`) $: isTimeValue = !isNaN(new Date(`0-${value}`))
$: timeOnly = !isNaN(time) || schema?.timeOnly $: timeOnly = isTimeValue || schema?.timeOnly
$: dateOnly = schema?.dateOnly $: dateOnly = schema?.dateOnly
$: format = timeOnly $: format = timeOnly
? "HH:mm:ss" ? "HH:mm:ss"
@ -24,6 +24,19 @@
? "MMM D YYYY" ? "MMM D YYYY"
: "MMM D YYYY, HH:mm" : "MMM D YYYY, HH:mm"
$: editable = focused && !readonly $: editable = focused && !readonly
$: displayValue = getDisplayValue(value, format, timeOnly, isTimeValue)
const getDisplayValue = (value, format, timeOnly, isTimeValue) => {
if (!value) {
return ""
}
// Parse full date strings
if (!timeOnly || !isTimeValue) {
return dayjs(value).format(format)
}
// Otherwise must be a time string
return dayjs(`0-${value}`).format(format)
}
// Ensure we close flatpickr when unselected // Ensure we close flatpickr when unselected
$: { $: {
@ -49,7 +62,7 @@
<div class="container"> <div class="container">
<div class="value"> <div class="value">
{#if value} {#if value}
{dayjs(timeOnly ? time : value).format(format)} {displayValue}
{/if} {/if}
</div> </div>
{#if editable} {#if editable}

View File

@ -1,3 +1,4 @@
export { default as SplitPage } from "./SplitPage.svelte" export { default as SplitPage } from "./SplitPage.svelte"
export { default as TestimonialPage } from "./TestimonialPage.svelte" export { default as TestimonialPage } from "./TestimonialPage.svelte"
export { default as Testimonial } from "./Testimonial.svelte"
export { Grid } from "./grid" export { Grid } from "./grid"

View File

@ -31,6 +31,7 @@ export default class GroupUserFetch extends DataFetch {
try { try {
const res = await this.API.getGroupUsers({ const res = await this.API.getGroupUsers({
id: query.groupId, id: query.groupId,
emailSearch: query.emailSearch,
bookmark: cursor, bookmark: cursor,
}) })

@ -1 +1 @@
Subproject commit c57a98d246a50a43905d8572a88c901ec598390c Subproject commit 64a2025727c25d5813832c92eb360de3947b7aa6

View File

@ -99,7 +99,7 @@
"mysql2": "2.3.3", "mysql2": "2.3.3",
"node-fetch": "2.6.7", "node-fetch": "2.6.7",
"open": "8.4.0", "open": "8.4.0",
"pg": "8.5.1", "pg": "8.10.0",
"posthog-node": "1.3.0", "posthog-node": "1.3.0",
"pouchdb": "7.3.0", "pouchdb": "7.3.0",
"pouchdb-adapter-memory": "7.2.2", "pouchdb-adapter-memory": "7.2.2",
@ -141,6 +141,7 @@
"@types/node": "14.18.20", "@types/node": "14.18.20",
"@types/node-fetch": "2.6.1", "@types/node-fetch": "2.6.1",
"@types/oracledb": "5.2.2", "@types/oracledb": "5.2.2",
"@types/pg": "8.6.6",
"@types/pouchdb": "6.4.0", "@types/pouchdb": "6.4.0",
"@types/redis": "4.0.11", "@types/redis": "4.0.11",
"@types/server-destroy": "1.0.1", "@types/server-destroy": "1.0.1",

View File

@ -18,11 +18,71 @@ import {
Row, Row,
CreateDatasourceResponse, CreateDatasourceResponse,
UpdateDatasourceResponse, UpdateDatasourceResponse,
UpdateDatasourceRequest,
CreateDatasourceRequest, CreateDatasourceRequest,
VerifyDatasourceRequest,
VerifyDatasourceResponse,
IntegrationBase,
DatasourcePlus,
} from "@budibase/types" } from "@budibase/types"
import sdk from "../../sdk" import sdk from "../../sdk"
function getErrorTables(errors: any, errorType: string) {
return Object.entries(errors)
.filter(entry => entry[1] === errorType)
.map(([name]) => name)
}
function updateError(error: any, newError: any, tables: string[]) {
if (!error) {
error = ""
}
if (error.length > 0) {
error += "\n"
}
error += `${newError} ${tables.join(", ")}`
return error
}
async function getConnector(
datasource: Datasource
): Promise<IntegrationBase | DatasourcePlus> {
const Connector = await getIntegration(datasource.source)
// can't enrich if it doesn't have an ID yet
if (datasource._id) {
datasource = await sdk.datasources.enrich(datasource)
}
// Connect to the DB and build the schema
return new Connector(datasource.config)
}
async function buildSchemaHelper(datasource: Datasource) {
const connector = (await getConnector(datasource)) as DatasourcePlus
await connector.buildSchema(datasource._id!, datasource.entities!)
const errors = connector.schemaErrors
let error = null
if (errors && Object.keys(errors).length > 0) {
const noKey = getErrorTables(errors, BuildSchemaErrors.NO_KEY)
const invalidCol = getErrorTables(errors, BuildSchemaErrors.INVALID_COLUMN)
if (noKey.length) {
error = updateError(
error,
"No primary key constraint found for the following:",
noKey
)
}
if (invalidCol.length) {
const invalidCols = Object.values(InvalidColumns).join(", ")
error = updateError(
error,
`Cannot use columns ${invalidCols} found in following:`,
invalidCol
)
}
}
return { tables: connector.tables, error }
}
export async function fetch(ctx: UserCtx) { export async function fetch(ctx: UserCtx) {
// Get internal tables // Get internal tables
const db = context.getAppDB() const db = context.getAppDB()
@ -66,6 +126,33 @@ export async function fetch(ctx: UserCtx) {
ctx.body = [bbInternalDb, ...datasources] ctx.body = [bbInternalDb, ...datasources]
} }
export async function verify(
ctx: UserCtx<VerifyDatasourceRequest, VerifyDatasourceResponse>
) {
const { datasource } = ctx.request.body
let existingDatasource: undefined | Datasource
if (datasource._id) {
existingDatasource = await sdk.datasources.get(datasource._id)
}
let enrichedDatasource = datasource
if (existingDatasource) {
enrichedDatasource = sdk.datasources.mergeConfigs(
datasource,
existingDatasource
)
}
const connector = await getConnector(enrichedDatasource)
if (!connector.testConnection) {
ctx.throw(400, "Connection information verification not supported")
}
const response = await connector.testConnection()
ctx.body = {
connected: response.connected,
error: response.error,
}
}
export async function buildSchemaFromDb(ctx: UserCtx) { export async function buildSchemaFromDb(ctx: UserCtx) {
const db = context.getAppDB() const db = context.getAppDB()
const datasource = await sdk.datasources.get(ctx.params.datasourceId) const datasource = await sdk.datasources.get(ctx.params.datasourceId)
@ -311,51 +398,3 @@ export async function query(ctx: UserCtx) {
ctx.throw(400, err) ctx.throw(400, err)
} }
} }
function getErrorTables(errors: any, errorType: string) {
return Object.entries(errors)
.filter(entry => entry[1] === errorType)
.map(([name]) => name)
}
function updateError(error: any, newError: any, tables: string[]) {
if (!error) {
error = ""
}
if (error.length > 0) {
error += "\n"
}
error += `${newError} ${tables.join(", ")}`
return error
}
async function buildSchemaHelper(datasource: Datasource) {
const Connector = await getIntegration(datasource.source)
datasource = await sdk.datasources.enrich(datasource)
// Connect to the DB and build the schema
const connector = new Connector(datasource.config)
await connector.buildSchema(datasource._id, datasource.entities)
const errors = connector.schemaErrors
let error = null
if (errors && Object.keys(errors).length > 0) {
const noKey = getErrorTables(errors, BuildSchemaErrors.NO_KEY)
const invalidCol = getErrorTables(errors, BuildSchemaErrors.INVALID_COLUMN)
if (noKey.length) {
error = updateError(
error,
"No primary key constraint found for the following:",
noKey
)
}
if (invalidCol.length) {
const invalidCols = Object.values(InvalidColumns).join(", ")
error = updateError(
error,
`Cannot use columns ${invalidCols} found in following:`,
invalidCol
)
}
}
return { tables: connector.tables, error }
}

View File

@ -1,4 +1,4 @@
import { getDefinitions } from "../../integrations" import { getDefinition, getDefinitions } from "../../integrations"
import { BBContext } from "@budibase/types" import { BBContext } from "@budibase/types"
export async function fetch(ctx: BBContext) { export async function fetch(ctx: BBContext) {
@ -7,7 +7,7 @@ export async function fetch(ctx: BBContext) {
} }
export async function find(ctx: BBContext) { export async function find(ctx: BBContext) {
const defs = await getDefinitions() const def = await getDefinition(ctx.params.type)
ctx.body = def
ctx.status = 200 ctx.status = 200
ctx.body = defs[ctx.params.type]
} }

View File

@ -118,8 +118,11 @@ export async function patch(ctx: UserCtx) {
combinedRow[key] = inputs[key] combinedRow[key] = inputs[key]
} }
// need to copy the table so it can be differenced on way out
const tableClone = cloneDeep(dbTable)
// this returns the table and row incase they have been updated // this returns the table and row incase they have been updated
let { table, row } = inputProcessing(ctx.user, dbTable, combinedRow) let { table, row } = inputProcessing(ctx.user, tableClone, combinedRow)
const validateResult = await utils.validate({ const validateResult = await utils.validate({
row, row,
table, table,
@ -163,7 +166,12 @@ export async function save(ctx: UserCtx) {
// this returns the table and row incase they have been updated // this returns the table and row incase they have been updated
const dbTable = await db.get(inputs.tableId) const dbTable = await db.get(inputs.tableId)
let { table, row } = inputProcessing(ctx.user, dbTable, inputs)
// need to copy the table so it can be differenced on way out
const tableClone = cloneDeep(dbTable)
let { table, row } = inputProcessing(ctx.user, tableClone, inputs)
const validateResult = await utils.validate({ const validateResult = await utils.validate({
row, row,
table, table,

View File

@ -97,6 +97,7 @@ export async function bulkImport(ctx: UserCtx) {
// right now we don't trigger anything for bulk import because it // right now we don't trigger anything for bulk import because it
// can only be done in the builder, but in the future we may need to // can only be done in the builder, but in the future we may need to
// think about events for bulk items // think about events for bulk items
ctx.status = 200 ctx.status = 200
ctx.body = { message: `Bulk rows created.` } ctx.body = { message: `Bulk rows created.` }
} }

View File

@ -184,8 +184,13 @@ export async function destroy(ctx: any) {
} }
export async function bulkImport(ctx: any) { export async function bulkImport(ctx: any) {
const db = context.getAppDB()
const table = await sdk.tables.getTable(ctx.params.tableId) const table = await sdk.tables.getTable(ctx.params.tableId)
const { rows } = ctx.request.body const { rows } = ctx.request.body
await handleDataImport(ctx.user, table, rows) await handleDataImport(ctx.user, table, rows)
// Ensure auto id and other table updates are persisted
await db.put(table)
return table return table
} }

View File

@ -129,17 +129,17 @@ export function importToRows(
// the real schema of the table passed in, not the clone used for // the real schema of the table passed in, not the clone used for
// incrementing auto IDs // incrementing auto IDs
for (const [fieldName, schema] of Object.entries(originalTable.schema)) { for (const [fieldName, schema] of Object.entries(originalTable.schema)) {
const rowVal = Array.isArray(row[fieldName])
? row[fieldName]
: [row[fieldName]]
if ( if (
(schema.type === FieldTypes.OPTIONS || (schema.type === FieldTypes.OPTIONS ||
schema.type === FieldTypes.ARRAY) && schema.type === FieldTypes.ARRAY) &&
row[fieldName] && row[fieldName]
(!schema.constraints!.inclusion ||
schema.constraints!.inclusion.indexOf(row[fieldName]) === -1)
) { ) {
schema.constraints!.inclusion = [ let merged = [...schema.constraints!.inclusion!, ...rowVal]
...schema.constraints!.inclusion!, let superSet = new Set(merged)
row[fieldName], schema.constraints!.inclusion = Array.from(superSet)
]
schema.constraints!.inclusion.sort() schema.constraints!.inclusion.sort()
} }
} }

View File

@ -15,6 +15,11 @@ router
authorized(permissions.BUILDER), authorized(permissions.BUILDER),
datasourceController.fetch datasourceController.fetch
) )
.post(
"/api/datasources/verify",
authorized(permissions.BUILDER),
datasourceController.verify
)
.get( .get(
"/api/datasources/:datasourceId", "/api/datasources/:datasourceId",
authorized( authorized(

View File

@ -42,13 +42,17 @@ if (!env.isTest()) {
host: REDIS_OPTS.host, host: REDIS_OPTS.host,
port: REDIS_OPTS.port, port: REDIS_OPTS.port,
}, },
password: }
REDIS_OPTS.opts.password || REDIS_OPTS.opts.redisOptions.password,
if (REDIS_OPTS.opts?.password || REDIS_OPTS.opts.redisOptions?.password) {
// @ts-ignore
options.password =
REDIS_OPTS.opts.password || REDIS_OPTS.opts.redisOptions.password
} }
if (!env.REDIS_CLUSTERED) { if (!env.REDIS_CLUSTERED) {
// Can't set direct redis db in clustered env
// @ts-ignore // @ts-ignore
// Can't set direct redis db in clustered env
options.database = 1 options.database = 1
} }
} }

View File

@ -73,18 +73,97 @@ describe("run misc tests", () => {
type: "string", type: "string",
}, },
}, },
e: {
name: "Auto ID",
type: "number",
subtype: "autoID",
icon: "ri-magic-line",
autocolumn: true,
constraints: {
type: "number",
presence: false,
numericality: {
greaterThanOrEqualTo: "",
lessThanOrEqualTo: "",
},
},
},
f: {
type: "array",
constraints: {
type: "array",
presence: {
"allowEmpty": true
},
inclusion: [
"One",
"Two",
"Three",
]
},
name: "Sample Tags",
sortable: false
},
g: {
type: "options",
constraints: {
type: "string",
presence: false,
inclusion: [
"Alpha",
"Beta",
"Gamma"
]
},
name: "Sample Opts"
}
}, },
}) })
// Shift specific row tests to the row spec
await tableUtils.handleDataImport( await tableUtils.handleDataImport(
{ userId: "test" }, { userId: "test" },
table, table,
[{ a: '1', b: '2', c: '3', d: '4'}] [
{ a: '1', b: '2', c: '3', d: '4', f: "['One']", g: "Alpha" },
{ a: '5', b: '6', c: '7', d: '8', f: "[]", g: undefined},
{ a: '9', b: '10', c: '11', d: '12', f: "['Two','Four']", g: ""},
{ a: '13', b: '14', c: '15', d: '16', g: "Omega"}
]
) )
// 4 rows imported, the auto ID starts at 1
// We expect the handleDataImport function to update the lastID
expect(table.schema.e.lastID).toEqual(4);
// Array/Multi - should have added a new value to the inclusion.
expect(table.schema.f.constraints.inclusion).toEqual(['Four','One','Three','Two']);
// Options - should have a new value in the inclusion
expect(table.schema.g.constraints.inclusion).toEqual(['Alpha','Beta','Gamma','Omega']);
const rows = await config.getRows() const rows = await config.getRows()
expect(rows[0].a).toEqual("1") expect(rows.length).toEqual(4);
expect(rows[0].b).toEqual("2")
expect(rows[0].c).toEqual("3") const rowOne = rows.find(row => row.e === 1)
expect(rowOne.a).toEqual("1")
expect(rowOne.f).toEqual(['One'])
expect(rowOne.g).toEqual('Alpha')
const rowTwo = rows.find(row => row.e === 2)
expect(rowTwo.a).toEqual("5")
expect(rowTwo.f).toEqual([])
expect(rowTwo.g).toEqual(undefined)
const rowThree = rows.find(row => row.e === 3)
expect(rowThree.a).toEqual("9")
expect(rowThree.f).toEqual(['Two','Four'])
expect(rowThree.g).toEqual(null)
const rowFour = rows.find(row => row.e === 4)
expect(rowFour.a).toEqual("13")
expect(rowFour.f).toEqual(undefined)
expect(rowFour.g).toEqual('Omega')
}) })
}) })
}) })

View File

@ -34,9 +34,9 @@ describe("/rows", () => {
row = basicRow(table._id) row = basicRow(table._id)
}) })
const loadRow = async (id, status = 200) => const loadRow = async (id, tbl_Id, status = 200) =>
await request await request
.get(`/api/${table._id}/rows/${id}`) .get(`/api/${tbl_Id}/rows/${id}`)
.set(config.defaultHeaders()) .set(config.defaultHeaders())
.expect("Content-Type", /json/) .expect("Content-Type", /json/)
.expect(status) .expect(status)
@ -79,6 +79,60 @@ describe("/rows", () => {
await assertQueryUsage(queryUsage + 1) await assertQueryUsage(queryUsage + 1)
}) })
it("Increment row autoId per create row request", async () => {
const rowUsage = await getRowUsage()
const queryUsage = await getQueryUsage()
const newTable = await config.createTable({
name: "TestTableAuto",
type: "table",
key: "name",
schema: {
...table.schema,
"Row ID": {
name: "Row ID",
type: "number",
subtype: "autoID",
icon: "ri-magic-line",
autocolumn: true,
constraints: {
type: "number",
presence: false,
numericality: {
greaterThanOrEqualTo: "",
lessThanOrEqualTo: "",
},
},
},
}
})
const ids = [1,2,3]
// Performing several create row requests should increment the autoID fields accordingly
const createRow = async (id) => {
const res = await request
.post(`/api/${newTable._id}/rows`)
.send({
name: "row_" + id
})
.set(config.defaultHeaders())
.expect('Content-Type', /json/)
.expect(200)
expect(res.res.statusMessage).toEqual(`${newTable.name} saved successfully`)
expect(res.body.name).toEqual("row_" + id)
expect(res.body._rev).toBeDefined()
expect(res.body["Row ID"]).toEqual(id)
}
for (let i=0; i<ids.length; i++ ){
await createRow(ids[i])
}
await assertRowUsage(rowUsage + ids.length)
await assertQueryUsage(queryUsage + ids.length)
})
it("updates a row successfully", async () => { it("updates a row successfully", async () => {
const existing = await config.createRow() const existing = await config.createRow()
const rowUsage = await getRowUsage() const rowUsage = await getRowUsage()
@ -182,8 +236,32 @@ describe("/rows", () => {
type: "string", type: "string",
presence: false, presence: false,
datetime: { earliest: "", latest: "" }, datetime: { earliest: "", latest: "" },
}, }
} }
const arrayField = {
type: "array",
constraints: {
type: "array",
presence: false,
inclusion: [
"One",
"Two",
"Three",
]
},
name: "Sample Tags",
sortable: false
}
const optsField = {
fieldName: "Sample Opts",
name: "Sample Opts",
type: "options",
constraints: {
type: "string",
presence: false,
inclusion: [ "Alpha", "Beta", "Gamma" ]
},
},
table = await config.createTable({ table = await config.createTable({
name: "TestTable2", name: "TestTable2",
@ -212,7 +290,15 @@ describe("/rows", () => {
attachmentNull: attachment, attachmentNull: attachment,
attachmentUndefined: attachment, attachmentUndefined: attachment,
attachmentEmpty: attachment, attachmentEmpty: attachment,
attachmentEmptyArrayStr: attachment attachmentEmptyArrayStr: attachment,
arrayFieldEmptyArrayStr: arrayField,
arrayFieldArrayStrKnown: arrayField,
arrayFieldNull: arrayField,
arrayFieldUndefined: arrayField,
optsFieldEmptyStr: optsField,
optsFieldUndefined: optsField,
optsFieldNull: optsField,
optsFieldStrKnown: optsField
}, },
}) })
@ -241,11 +327,20 @@ describe("/rows", () => {
attachmentUndefined: undefined, attachmentUndefined: undefined,
attachmentEmpty: "", attachmentEmpty: "",
attachmentEmptyArrayStr: "[]", attachmentEmptyArrayStr: "[]",
arrayFieldEmptyArrayStr: "[]",
arrayFieldUndefined: undefined,
arrayFieldNull: null,
arrayFieldArrayStrKnown: "['One']",
optsFieldEmptyStr: "",
optsFieldUndefined: undefined,
optsFieldNull: null,
optsFieldStrKnown: 'Alpha'
} }
const id = (await config.createRow(row))._id const createdRow = await config.createRow(row);
const id = createdRow._id
const saved = (await loadRow(id)).body const saved = (await loadRow(id, table._id)).body
expect(saved.stringUndefined).toBe(undefined) expect(saved.stringUndefined).toBe(undefined)
expect(saved.stringNull).toBe("") expect(saved.stringNull).toBe("")
@ -270,7 +365,15 @@ describe("/rows", () => {
expect(saved.attachmentNull).toEqual([]) expect(saved.attachmentNull).toEqual([])
expect(saved.attachmentUndefined).toBe(undefined) expect(saved.attachmentUndefined).toBe(undefined)
expect(saved.attachmentEmpty).toEqual([]) expect(saved.attachmentEmpty).toEqual([])
expect(saved.attachmentEmptyArrayStr).toEqual([]) expect(saved.attachmentEmptyArrayStr).toEqual([])
expect(saved.arrayFieldEmptyArrayStr).toEqual([])
expect(saved.arrayFieldNull).toEqual([])
expect(saved.arrayFieldUndefined).toEqual(undefined)
expect(saved.optsFieldEmptyStr).toEqual(null)
expect(saved.optsFieldUndefined).toEqual(undefined)
expect(saved.optsFieldNull).toEqual(null)
expect(saved.arrayFieldArrayStrKnown).toEqual(['One'])
expect(saved.optsFieldStrKnown).toEqual('Alpha')
}) })
}) })
@ -299,7 +402,7 @@ describe("/rows", () => {
expect(res.body.name).toEqual("Updated Name") expect(res.body.name).toEqual("Updated Name")
expect(res.body.description).toEqual(existing.description) expect(res.body.description).toEqual(existing.description)
const savedRow = await loadRow(res.body._id) const savedRow = await loadRow(res.body._id, table._id)
expect(savedRow.body.description).toEqual(existing.description) expect(savedRow.body.description).toEqual(existing.description)
expect(savedRow.body.name).toEqual("Updated Name") expect(savedRow.body.name).toEqual("Updated Name")
@ -401,7 +504,7 @@ describe("/rows", () => {
.expect(200) .expect(200)
expect(res.body.length).toEqual(2) expect(res.body.length).toEqual(2)
await loadRow(row1._id, 404) await loadRow(row1._id, table._id, 404)
await assertRowUsage(rowUsage - 2) await assertRowUsage(rowUsage - 2)
await assertQueryUsage(queryUsage + 1) await assertQueryUsage(queryUsage + 1)
}) })

View File

@ -167,7 +167,10 @@ describe("/tables", () => {
expect(events.table.created).not.toHaveBeenCalled() expect(events.table.created).not.toHaveBeenCalled()
expect(events.rows.imported).toBeCalledTimes(1) expect(events.rows.imported).toBeCalledTimes(1)
expect(events.rows.imported).toBeCalledWith(table, 1) expect(events.rows.imported).toBeCalledWith(expect.objectContaining({
name: "TestTable",
_id: table._id
}), 1)
}) })
}) })

View File

@ -140,7 +140,7 @@ export function init(endpoint: string) {
docClient = new AWS.DynamoDB.DocumentClient(docClientParams) docClient = new AWS.DynamoDB.DocumentClient(docClientParams)
} }
if (!env.isProd()) { if (!env.isProd() && !env.isJest()) {
env._set("AWS_ACCESS_KEY_ID", "KEY_ID") env._set("AWS_ACCESS_KEY_ID", "KEY_ID")
env._set("AWS_SECRET_ACCESS_KEY", "SECRET_KEY") env._set("AWS_SECRET_ACCESS_KEY", "SECRET_KEY")
init("http://localhost:8333") init("http://localhost:8333")

View File

@ -19,7 +19,6 @@ import _ from "lodash"
import { generator } from "@budibase/backend-core/tests" import { generator } from "@budibase/backend-core/tests"
import { utils } from "@budibase/backend-core" import { utils } from "@budibase/backend-core"
import { GenericContainer } from "testcontainers" import { GenericContainer } from "testcontainers"
import { generateRowIdField } from "../integrations/utils"
const config = setup.getConfig()! const config = setup.getConfig()!

View File

@ -1,11 +1,13 @@
import { import {
Integration, ConnectionInfo,
DatasourceFeature,
DatasourceFieldType, DatasourceFieldType,
QueryType, Integration,
IntegrationBase, IntegrationBase,
QueryType,
} from "@budibase/types" } from "@budibase/types"
const Airtable = require("airtable") import Airtable from "airtable"
interface AirtableConfig { interface AirtableConfig {
apiKey: string apiKey: string
@ -18,6 +20,7 @@ const SCHEMA: Integration = {
"Airtable is a spreadsheet-database hybrid, with the features of a database but applied to a spreadsheet.", "Airtable is a spreadsheet-database hybrid, with the features of a database but applied to a spreadsheet.",
friendlyName: "Airtable", friendlyName: "Airtable",
type: "Spreadsheet", type: "Spreadsheet",
features: [DatasourceFeature.CONNECTION_CHECKING],
datasource: { datasource: {
apiKey: { apiKey: {
type: DatasourceFieldType.PASSWORD, type: DatasourceFieldType.PASSWORD,
@ -81,13 +84,37 @@ const SCHEMA: Integration = {
class AirtableIntegration implements IntegrationBase { class AirtableIntegration implements IntegrationBase {
private config: AirtableConfig private config: AirtableConfig
private client: any private client
constructor(config: AirtableConfig) { constructor(config: AirtableConfig) {
this.config = config this.config = config
this.client = new Airtable(config).base(config.base) this.client = new Airtable(config).base(config.base)
} }
async testConnection(): Promise<ConnectionInfo> {
const mockTable = Date.now().toString()
try {
await this.client.makeRequest({
path: `/${mockTable}`,
})
return { connected: true }
} catch (e: any) {
if (
e.message ===
`Could not find table ${mockTable} in application ${this.config.base}`
) {
// The request managed to check the application, so the credentials are valid
return { connected: true }
}
return {
connected: false,
error: e.message as string,
}
}
}
async create(query: { table: any; json: any }) { async create(query: { table: any; json: any }) {
const { table, json } = query const { table, json } = query

View File

@ -3,9 +3,11 @@ import {
DatasourceFieldType, DatasourceFieldType,
QueryType, QueryType,
IntegrationBase, IntegrationBase,
DatasourceFeature,
ConnectionInfo,
} from "@budibase/types" } from "@budibase/types"
const { Database, aql } = require("arangojs") import { Database, aql } from "arangojs"
interface ArangodbConfig { interface ArangodbConfig {
url: string url: string
@ -21,6 +23,7 @@ const SCHEMA: Integration = {
type: "Non-relational", type: "Non-relational",
description: description:
"ArangoDB is a scalable open-source multi-model database natively supporting graph, document and search. All supported data models & access patterns can be combined in queries allowing for maximal flexibility. ", "ArangoDB is a scalable open-source multi-model database natively supporting graph, document and search. All supported data models & access patterns can be combined in queries allowing for maximal flexibility. ",
features: [DatasourceFeature.CONNECTION_CHECKING],
datasource: { datasource: {
url: { url: {
type: DatasourceFieldType.STRING, type: DatasourceFieldType.STRING,
@ -58,7 +61,7 @@ const SCHEMA: Integration = {
class ArangoDBIntegration implements IntegrationBase { class ArangoDBIntegration implements IntegrationBase {
private config: ArangodbConfig private config: ArangodbConfig
private client: any private client
constructor(config: ArangodbConfig) { constructor(config: ArangodbConfig) {
const newConfig = { const newConfig = {
@ -74,6 +77,19 @@ class ArangoDBIntegration implements IntegrationBase {
this.client = new Database(newConfig) this.client = new Database(newConfig)
} }
async testConnection() {
const response: ConnectionInfo = {
connected: false,
}
try {
await this.client.get()
response.connected = true
} catch (e: any) {
response.error = e.message as string
}
return response
}
async read(query: { sql: any }) { async read(query: { sql: any }) {
try { try {
const result = await this.client.query(query.sql) const result = await this.client.query(query.sql)

View File

@ -1,4 +1,6 @@
import { import {
ConnectionInfo,
DatasourceFeature,
DatasourceFieldType, DatasourceFieldType,
Document, Document,
Integration, Integration,
@ -18,6 +20,7 @@ const SCHEMA: Integration = {
type: "Non-relational", type: "Non-relational",
description: description:
"Apache CouchDB is an open-source document-oriented NoSQL database, implemented in Erlang.", "Apache CouchDB is an open-source document-oriented NoSQL database, implemented in Erlang.",
features: [DatasourceFeature.CONNECTION_CHECKING],
datasource: { datasource: {
url: { url: {
type: DatasourceFieldType.STRING, type: DatasourceFieldType.STRING,
@ -61,21 +64,32 @@ const SCHEMA: Integration = {
} }
class CouchDBIntegration implements IntegrationBase { class CouchDBIntegration implements IntegrationBase {
private config: CouchDBConfig private readonly client: dbCore.DatabaseImpl
private readonly client: any
constructor(config: CouchDBConfig) { constructor(config: CouchDBConfig) {
this.config = config
this.client = dbCore.DatabaseWithConnection(config.database, config.url) this.client = dbCore.DatabaseWithConnection(config.database, config.url)
} }
async testConnection() {
const response: ConnectionInfo = {
connected: false,
}
try {
const result = await this.query("exists", "validation error", {})
response.connected = result === true
} catch (e: any) {
response.error = e.message as string
}
return response
}
async query( async query(
command: string, command: string,
errorMsg: string, errorMsg: string,
query: { json?: object; id?: string } query: { json?: object; id?: string }
) { ) {
try { try {
return await this.client[command](query.id || query.json) return await (this.client as any)[command](query.id || query.json)
} catch (err) { } catch (err) {
console.error(errorMsg, err) console.error(errorMsg, err)
throw err throw err

View File

@ -3,10 +3,13 @@ import {
DatasourceFieldType, DatasourceFieldType,
QueryType, QueryType,
IntegrationBase, IntegrationBase,
DatasourceFeature,
ConnectionInfo,
} from "@budibase/types" } from "@budibase/types"
import AWS from "aws-sdk" import AWS from "aws-sdk"
import { AWS_REGION } from "../db/dynamoClient" import { AWS_REGION } from "../db/dynamoClient"
import { DocumentClient } from "aws-sdk/clients/dynamodb"
interface DynamoDBConfig { interface DynamoDBConfig {
region: string region: string
@ -22,6 +25,7 @@ const SCHEMA: Integration = {
"Amazon DynamoDB is a key-value and document database that delivers single-digit millisecond performance at any scale.", "Amazon DynamoDB is a key-value and document database that delivers single-digit millisecond performance at any scale.",
friendlyName: "DynamoDB", friendlyName: "DynamoDB",
type: "Non-relational", type: "Non-relational",
features: [DatasourceFeature.CONNECTION_CHECKING],
datasource: { datasource: {
region: { region: {
type: DatasourceFieldType.STRING, type: DatasourceFieldType.STRING,
@ -128,7 +132,7 @@ const SCHEMA: Integration = {
class DynamoDBIntegration implements IntegrationBase { class DynamoDBIntegration implements IntegrationBase {
private config: DynamoDBConfig private config: DynamoDBConfig
private client: any private client
constructor(config: DynamoDBConfig) { constructor(config: DynamoDBConfig) {
this.config = config this.config = config
@ -148,7 +152,23 @@ class DynamoDBIntegration implements IntegrationBase {
this.client = new AWS.DynamoDB.DocumentClient(this.config) this.client = new AWS.DynamoDB.DocumentClient(this.config)
} }
async create(query: { table: string; json: object }) { async testConnection() {
const response: ConnectionInfo = {
connected: false,
}
try {
const scanRes = await new AWS.DynamoDB(this.config).listTables().promise()
response.connected = !!scanRes.$response
} catch (e: any) {
response.error = e.message as string
}
return response
}
async create(query: {
table: string
json: Omit<DocumentClient.PutItemInput, "TableName">
}) {
const params = { const params = {
TableName: query.table, TableName: query.table,
...query.json, ...query.json,
@ -189,7 +209,10 @@ class DynamoDBIntegration implements IntegrationBase {
return new AWS.DynamoDB(this.config).describeTable(params).promise() return new AWS.DynamoDB(this.config).describeTable(params).promise()
} }
async get(query: { table: string; json: object }) { async get(query: {
table: string
json: Omit<DocumentClient.GetItemInput, "TableName">
}) {
const params = { const params = {
TableName: query.table, TableName: query.table,
...query.json, ...query.json,
@ -197,7 +220,10 @@ class DynamoDBIntegration implements IntegrationBase {
return this.client.get(params).promise() return this.client.get(params).promise()
} }
async update(query: { table: string; json: object }) { async update(query: {
table: string
json: Omit<DocumentClient.UpdateItemInput, "TableName">
}) {
const params = { const params = {
TableName: query.table, TableName: query.table,
...query.json, ...query.json,
@ -205,7 +231,10 @@ class DynamoDBIntegration implements IntegrationBase {
return this.client.update(params).promise() return this.client.update(params).promise()
} }
async delete(query: { table: string; json: object }) { async delete(query: {
table: string
json: Omit<DocumentClient.DeleteItemInput, "TableName">
}) {
const params = { const params = {
TableName: query.table, TableName: query.table,
...query.json, ...query.json,

View File

@ -3,6 +3,8 @@ import {
DatasourceFieldType, DatasourceFieldType,
QueryType, QueryType,
IntegrationBase, IntegrationBase,
DatasourceFeature,
ConnectionInfo,
} from "@budibase/types" } from "@budibase/types"
import { Client, ClientOptions } from "@elastic/elasticsearch" import { Client, ClientOptions } from "@elastic/elasticsearch"
@ -20,6 +22,7 @@ const SCHEMA: Integration = {
"Elasticsearch is a search engine based on the Lucene library. It provides a distributed, multitenant-capable full-text search engine with an HTTP web interface and schema-free JSON documents.", "Elasticsearch is a search engine based on the Lucene library. It provides a distributed, multitenant-capable full-text search engine with an HTTP web interface and schema-free JSON documents.",
friendlyName: "ElasticSearch", friendlyName: "ElasticSearch",
type: "Non-relational", type: "Non-relational",
features: [DatasourceFeature.CONNECTION_CHECKING],
datasource: { datasource: {
url: { url: {
type: DatasourceFieldType.STRING, type: DatasourceFieldType.STRING,
@ -95,7 +98,7 @@ const SCHEMA: Integration = {
class ElasticSearchIntegration implements IntegrationBase { class ElasticSearchIntegration implements IntegrationBase {
private config: ElasticsearchConfig private config: ElasticsearchConfig
private client: any private client
constructor(config: ElasticsearchConfig) { constructor(config: ElasticsearchConfig) {
this.config = config this.config = config
@ -114,6 +117,18 @@ class ElasticSearchIntegration implements IntegrationBase {
this.client = new Client(clientConfig) this.client = new Client(clientConfig)
} }
async testConnection(): Promise<ConnectionInfo> {
try {
await this.client.info()
return { connected: true }
} catch (e: any) {
return {
connected: false,
error: e.message as string,
}
}
}
async create(query: { index: string; json: object }) { async create(query: { index: string; json: object }) {
const { index, json } = query const { index, json } = query

View File

@ -3,6 +3,8 @@ import {
Integration, Integration,
QueryType, QueryType,
IntegrationBase, IntegrationBase,
DatasourceFeature,
ConnectionInfo,
} from "@budibase/types" } from "@budibase/types"
import { Firestore, WhereFilterOp } from "@google-cloud/firestore" import { Firestore, WhereFilterOp } from "@google-cloud/firestore"
@ -18,6 +20,7 @@ const SCHEMA: Integration = {
type: "Non-relational", type: "Non-relational",
description: description:
"Cloud Firestore is a flexible, scalable database for mobile, web, and server development from Firebase and Google Cloud.", "Cloud Firestore is a flexible, scalable database for mobile, web, and server development from Firebase and Google Cloud.",
features: [DatasourceFeature.CONNECTION_CHECKING],
datasource: { datasource: {
email: { email: {
type: DatasourceFieldType.STRING, type: DatasourceFieldType.STRING,
@ -99,6 +102,18 @@ class FirebaseIntegration implements IntegrationBase {
}) })
} }
async testConnection(): Promise<ConnectionInfo> {
try {
await this.client.listCollections()
return { connected: true }
} catch (e: any) {
return {
connected: false,
error: e.message as string,
}
}
}
async create(query: { json: object; extra: { [key: string]: string } }) { async create(query: { json: object; extra: { [key: string]: string } }) {
try { try {
const documentReference = this.client const documentReference = this.client

View File

@ -1,4 +1,6 @@
import { import {
ConnectionInfo,
DatasourceFeature,
DatasourceFieldType, DatasourceFieldType,
DatasourcePlus, DatasourcePlus,
FieldType, FieldType,
@ -64,6 +66,7 @@ const SCHEMA: Integration = {
"Create and collaborate on online spreadsheets in real-time and from any device. ", "Create and collaborate on online spreadsheets in real-time and from any device. ",
friendlyName: "Google Sheets", friendlyName: "Google Sheets",
type: "Spreadsheet", type: "Spreadsheet",
features: [DatasourceFeature.CONNECTION_CHECKING],
datasource: { datasource: {
spreadsheetId: { spreadsheetId: {
display: "Google Sheet URL", display: "Google Sheet URL",
@ -139,6 +142,19 @@ class GoogleSheetsIntegration implements DatasourcePlus {
this.client = new GoogleSpreadsheet(spreadsheetId) this.client = new GoogleSpreadsheet(spreadsheetId)
} }
async testConnection(): Promise<ConnectionInfo> {
try {
await this.connect()
await this.client.loadInfo()
return { connected: true }
} catch (e: any) {
return {
connected: false,
error: e.message as string,
}
}
}
getBindingIdentifier() { getBindingIdentifier() {
return "" return ""
} }

View File

@ -20,7 +20,7 @@ import env from "../environment"
import { cloneDeep } from "lodash" import { cloneDeep } from "lodash"
import sdk from "../sdk" import sdk from "../sdk"
const DEFINITIONS: { [key: string]: Integration } = { const DEFINITIONS: Record<SourceName, Integration | undefined> = {
[SourceName.POSTGRES]: postgres.schema, [SourceName.POSTGRES]: postgres.schema,
[SourceName.DYNAMODB]: dynamodb.schema, [SourceName.DYNAMODB]: dynamodb.schema,
[SourceName.MONGODB]: mongodb.schema, [SourceName.MONGODB]: mongodb.schema,
@ -36,9 +36,10 @@ const DEFINITIONS: { [key: string]: Integration } = {
[SourceName.GOOGLE_SHEETS]: googlesheets.schema, [SourceName.GOOGLE_SHEETS]: googlesheets.schema,
[SourceName.REDIS]: redis.schema, [SourceName.REDIS]: redis.schema,
[SourceName.SNOWFLAKE]: snowflake.schema, [SourceName.SNOWFLAKE]: snowflake.schema,
[SourceName.ORACLE]: undefined,
} }
const INTEGRATIONS: { [key: string]: any } = { const INTEGRATIONS: Record<SourceName, any> = {
[SourceName.POSTGRES]: postgres.integration, [SourceName.POSTGRES]: postgres.integration,
[SourceName.DYNAMODB]: dynamodb.integration, [SourceName.DYNAMODB]: dynamodb.integration,
[SourceName.MONGODB]: mongodb.integration, [SourceName.MONGODB]: mongodb.integration,
@ -55,6 +56,7 @@ const INTEGRATIONS: { [key: string]: any } = {
[SourceName.REDIS]: redis.integration, [SourceName.REDIS]: redis.integration,
[SourceName.FIRESTORE]: firebase.integration, [SourceName.FIRESTORE]: firebase.integration,
[SourceName.SNOWFLAKE]: snowflake.integration, [SourceName.SNOWFLAKE]: snowflake.integration,
[SourceName.ORACLE]: undefined,
} }
// optionally add oracle integration if the oracle binary can be installed // optionally add oracle integration if the oracle binary can be installed
@ -67,10 +69,13 @@ if (
INTEGRATIONS[SourceName.ORACLE] = oracle.integration INTEGRATIONS[SourceName.ORACLE] = oracle.integration
} }
export async function getDefinition(source: SourceName): Promise<Integration> { export async function getDefinition(
source: SourceName
): Promise<Integration | undefined> {
// check if its integrated, faster // check if its integrated, faster
if (DEFINITIONS[source]) { const definition = DEFINITIONS[source]
return DEFINITIONS[source] if (definition) {
return definition
} }
const allDefinitions = await getDefinitions() const allDefinitions = await getDefinitions()
return allDefinitions[source] return allDefinitions[source]
@ -98,7 +103,7 @@ export async function getDefinitions() {
} }
} }
export async function getIntegration(integration: string) { export async function getIntegration(integration: SourceName) {
if (INTEGRATIONS[integration]) { if (INTEGRATIONS[integration]) {
return INTEGRATIONS[integration] return INTEGRATIONS[integration]
} }
@ -107,7 +112,7 @@ export async function getIntegration(integration: string) {
for (let plugin of plugins) { for (let plugin of plugins) {
if (plugin.name === integration) { if (plugin.name === integration) {
// need to use commonJS require due to its dynamic runtime nature // need to use commonJS require due to its dynamic runtime nature
const retrieved: any = await getDatasourcePlugin(plugin) const retrieved = await getDatasourcePlugin(plugin)
if (retrieved.integration) { if (retrieved.integration) {
return retrieved.integration return retrieved.integration
} else { } else {

View File

@ -8,6 +8,8 @@ import {
QueryType, QueryType,
SqlQuery, SqlQuery,
DatasourcePlus, DatasourcePlus,
DatasourceFeature,
ConnectionInfo,
} from "@budibase/types" } from "@budibase/types"
import { import {
getSqlQuery, getSqlQuery,
@ -39,6 +41,7 @@ const SCHEMA: Integration = {
"Microsoft SQL Server is a relational database management system developed by Microsoft. ", "Microsoft SQL Server is a relational database management system developed by Microsoft. ",
friendlyName: "MS SQL Server", friendlyName: "MS SQL Server",
type: "Relational", type: "Relational",
features: [DatasourceFeature.CONNECTION_CHECKING],
datasource: { datasource: {
user: { user: {
type: DatasourceFieldType.STRING, type: DatasourceFieldType.STRING,
@ -121,6 +124,19 @@ class SqlServerIntegration extends Sql implements DatasourcePlus {
} }
} }
async testConnection() {
const response: ConnectionInfo = {
connected: false,
}
try {
await this.connect()
response.connected = true
} catch (e: any) {
response.error = e.message as string
}
return response
}
getBindingIdentifier(): string { getBindingIdentifier(): string {
return `@p${this.index++}` return `@p${this.index++}`
} }

View File

@ -3,6 +3,8 @@ import {
DatasourceFieldType, DatasourceFieldType,
QueryType, QueryType,
IntegrationBase, IntegrationBase,
DatasourceFeature,
ConnectionInfo,
} from "@budibase/types" } from "@budibase/types"
import { import {
MongoClient, MongoClient,
@ -38,6 +40,7 @@ const getSchema = () => {
type: "Non-relational", type: "Non-relational",
description: description:
"MongoDB is a general purpose, document-based, distributed database built for modern application developers and for the cloud era.", "MongoDB is a general purpose, document-based, distributed database built for modern application developers and for the cloud era.",
features: [DatasourceFeature.CONNECTION_CHECKING],
datasource: { datasource: {
connectionString: { connectionString: {
type: DatasourceFieldType.STRING, type: DatasourceFieldType.STRING,
@ -358,6 +361,19 @@ class MongoIntegration implements IntegrationBase {
this.client = new MongoClient(config.connectionString, options) this.client = new MongoClient(config.connectionString, options)
} }
async testConnection() {
const response: ConnectionInfo = {
connected: false,
}
try {
await this.connect()
response.connected = true
} catch (e: any) {
response.error = e.message as string
}
return response
}
async connect() { async connect() {
return this.client.connect() return this.client.connect()
} }

View File

@ -7,6 +7,8 @@ import {
Table, Table,
TableSchema, TableSchema,
DatasourcePlus, DatasourcePlus,
DatasourceFeature,
ConnectionInfo,
} from "@budibase/types" } from "@budibase/types"
import { import {
getSqlQuery, getSqlQuery,
@ -20,18 +22,11 @@ import { NUMBER_REGEX } from "../utilities"
import Sql from "./base/sql" import Sql from "./base/sql"
import { MySQLColumn } from "./base/types" import { MySQLColumn } from "./base/types"
const mysql = require("mysql2/promise") import mysql from "mysql2/promise"
interface MySQLConfig { interface MySQLConfig extends mysql.ConnectionOptions {
host: string
port: number
user: string
password: string
database: string database: string
ssl?: { [key: string]: any }
rejectUnauthorized: boolean rejectUnauthorized: boolean
typeCast: Function
multipleStatements: boolean
} }
const SCHEMA: Integration = { const SCHEMA: Integration = {
@ -41,6 +36,7 @@ const SCHEMA: Integration = {
type: "Relational", type: "Relational",
description: description:
"MySQL Database Service is a fully managed database service to deploy cloud-native applications. ", "MySQL Database Service is a fully managed database service to deploy cloud-native applications. ",
features: [DatasourceFeature.CONNECTION_CHECKING],
datasource: { datasource: {
host: { host: {
type: DatasourceFieldType.STRING, type: DatasourceFieldType.STRING,
@ -92,8 +88,6 @@ const SCHEMA: Integration = {
}, },
} }
const TimezoneAwareDateTypes = ["timestamp"]
function bindingTypeCoerce(bindings: any[]) { function bindingTypeCoerce(bindings: any[]) {
for (let i = 0; i < bindings.length; i++) { for (let i = 0; i < bindings.length; i++) {
const binding = bindings[i] const binding = bindings[i]
@ -120,7 +114,7 @@ function bindingTypeCoerce(bindings: any[]) {
class MySQLIntegration extends Sql implements DatasourcePlus { class MySQLIntegration extends Sql implements DatasourcePlus {
private config: MySQLConfig private config: MySQLConfig
private client: any private client?: mysql.Connection
public tables: Record<string, Table> = {} public tables: Record<string, Table> = {}
public schemaErrors: Record<string, string> = {} public schemaErrors: Record<string, string> = {}
@ -134,7 +128,8 @@ class MySQLIntegration extends Sql implements DatasourcePlus {
if ( if (
config.rejectUnauthorized != null && config.rejectUnauthorized != null &&
!config.rejectUnauthorized && !config.rejectUnauthorized &&
config.ssl config.ssl &&
typeof config.ssl !== "string"
) { ) {
config.ssl.rejectUnauthorized = config.rejectUnauthorized config.ssl.rejectUnauthorized = config.rejectUnauthorized
} }
@ -160,6 +155,22 @@ class MySQLIntegration extends Sql implements DatasourcePlus {
} }
} }
async testConnection() {
const response: ConnectionInfo = {
connected: false,
}
try {
const [result] = await this.internalQuery(
{ sql: "SELECT 1+1 AS checkRes" },
{ connect: true }
)
response.connected = result?.checkRes == 2
} catch (e: any) {
response.error = e.message as string
}
return response
}
getBindingIdentifier(): string { getBindingIdentifier(): string {
return "?" return "?"
} }
@ -173,7 +184,7 @@ class MySQLIntegration extends Sql implements DatasourcePlus {
} }
async disconnect() { async disconnect() {
await this.client.end() await this.client!.end()
} }
async internalQuery( async internalQuery(
@ -192,10 +203,10 @@ class MySQLIntegration extends Sql implements DatasourcePlus {
? baseBindings ? baseBindings
: bindingTypeCoerce(baseBindings) : bindingTypeCoerce(baseBindings)
// Node MySQL is callback based, so we must wrap our call in a promise // Node MySQL is callback based, so we must wrap our call in a promise
const response = await this.client.query(query.sql, bindings) const response = await this.client!.query(query.sql, bindings)
return response[0] return response[0]
} finally { } finally {
if (opts?.connect) { if (opts?.connect && this.client) {
await this.disconnect() await this.disconnect()
} }
} }

View File

@ -7,6 +7,8 @@ import {
SqlQuery, SqlQuery,
Table, Table,
DatasourcePlus, DatasourcePlus,
DatasourceFeature,
ConnectionInfo,
} from "@budibase/types" } from "@budibase/types"
import { import {
buildExternalTableId, buildExternalTableId,
@ -24,12 +26,7 @@ import {
ExecuteOptions, ExecuteOptions,
Result, Result,
} from "oracledb" } from "oracledb"
import { import { OracleTable, OracleColumn, OracleColumnsResponse } from "./base/types"
OracleTable,
OracleColumn,
OracleColumnsResponse,
OracleConstraint,
} from "./base/types"
let oracledb: any let oracledb: any
try { try {
oracledb = require("oracledb") oracledb = require("oracledb")
@ -53,6 +50,7 @@ const SCHEMA: Integration = {
type: "Relational", type: "Relational",
description: description:
"Oracle Database is an object-relational database management system developed by Oracle Corporation", "Oracle Database is an object-relational database management system developed by Oracle Corporation",
features: [DatasourceFeature.CONNECTION_CHECKING],
datasource: { datasource: {
host: { host: {
type: DatasourceFieldType.STRING, type: DatasourceFieldType.STRING,
@ -325,6 +323,30 @@ class OracleIntegration extends Sql implements DatasourcePlus {
this.schemaErrors = final.errors this.schemaErrors = final.errors
} }
async testConnection() {
const response: ConnectionInfo = {
connected: false,
}
let connection
try {
connection = await this.getConnection()
response.connected = true
} catch (err: any) {
response.connected = false
response.error = err.message
} finally {
if (connection) {
try {
await connection.close()
} catch (err: any) {
response.connected = false
response.error = err.message
}
}
}
return response
}
private async internalQuery<T>(query: SqlQuery): Promise<Result<T>> { private async internalQuery<T>(query: SqlQuery): Promise<Result<T>> {
let connection let connection
try { try {

View File

@ -6,6 +6,8 @@ import {
SqlQuery, SqlQuery,
Table, Table,
DatasourcePlus, DatasourcePlus,
DatasourceFeature,
ConnectionInfo,
} from "@budibase/types" } from "@budibase/types"
import { import {
getSqlQuery, getSqlQuery,
@ -18,7 +20,7 @@ import Sql from "./base/sql"
import { PostgresColumn } from "./base/types" import { PostgresColumn } from "./base/types"
import { escapeDangerousCharacters } from "../utilities" import { escapeDangerousCharacters } from "../utilities"
const { Client, types } = require("pg") import { Client, types } from "pg"
// Return "date" and "timestamp" types as plain strings. // Return "date" and "timestamp" types as plain strings.
// This lets us reference the original stored timezone. // This lets us reference the original stored timezone.
@ -50,6 +52,7 @@ const SCHEMA: Integration = {
type: "Relational", type: "Relational",
description: description:
"PostgreSQL, also known as Postgres, is a free and open-source relational database management system emphasizing extensibility and SQL compliance.", "PostgreSQL, also known as Postgres, is a free and open-source relational database management system emphasizing extensibility and SQL compliance.",
features: [DatasourceFeature.CONNECTION_CHECKING],
datasource: { datasource: {
host: { host: {
type: DatasourceFieldType.STRING, type: DatasourceFieldType.STRING,
@ -114,7 +117,7 @@ const SCHEMA: Integration = {
} }
class PostgresIntegration extends Sql implements DatasourcePlus { class PostgresIntegration extends Sql implements DatasourcePlus {
private readonly client: any private readonly client: Client
private readonly config: PostgresConfig private readonly config: PostgresConfig
private index: number = 1 private index: number = 1
private open: boolean private open: boolean
@ -150,6 +153,21 @@ class PostgresIntegration extends Sql implements DatasourcePlus {
this.open = false this.open = false
} }
async testConnection() {
const response: ConnectionInfo = {
connected: false,
}
try {
await this.openConnection()
response.connected = true
} catch (e: any) {
response.error = e.message as string
} finally {
await this.closeConnection()
}
return response
}
getBindingIdentifier(): string { getBindingIdentifier(): string {
return `$${this.index++}` return `$${this.index++}`
} }
@ -163,7 +181,7 @@ class PostgresIntegration extends Sql implements DatasourcePlus {
if (!this.config.schema) { if (!this.config.schema) {
this.config.schema = "public" this.config.schema = "public"
} }
this.client.query(`SET search_path TO ${this.config.schema}`) await this.client.query(`SET search_path TO ${this.config.schema}`)
this.COLUMNS_SQL = `select * from information_schema.columns where table_schema = '${this.config.schema}'` this.COLUMNS_SQL = `select * from information_schema.columns where table_schema = '${this.config.schema}'`
this.open = true this.open = true
} }

View File

@ -1,4 +1,10 @@
import { DatasourceFieldType, Integration, QueryType } from "@budibase/types" import {
ConnectionInfo,
DatasourceFeature,
DatasourceFieldType,
Integration,
QueryType,
} from "@budibase/types"
import Redis from "ioredis" import Redis from "ioredis"
interface RedisConfig { interface RedisConfig {
@ -11,9 +17,11 @@ interface RedisConfig {
const SCHEMA: Integration = { const SCHEMA: Integration = {
docs: "https://redis.io/docs/", docs: "https://redis.io/docs/",
description: "", description:
"Redis is a caching tool, providing powerful key-value store capabilities.",
friendlyName: "Redis", friendlyName: "Redis",
type: "Non-relational", type: "Non-relational",
features: [DatasourceFeature.CONNECTION_CHECKING],
datasource: { datasource: {
host: { host: {
type: "string", type: "string",
@ -86,7 +94,7 @@ const SCHEMA: Integration = {
class RedisIntegration { class RedisIntegration {
private readonly config: RedisConfig private readonly config: RedisConfig
private client: any private client
constructor(config: RedisConfig) { constructor(config: RedisConfig) {
this.config = config this.config = config
@ -99,6 +107,21 @@ class RedisIntegration {
}) })
} }
async testConnection() {
const response: ConnectionInfo = {
connected: false,
}
try {
await this.client.ping()
response.connected = true
} catch (e: any) {
response.error = e.message as string
} finally {
await this.disconnect()
}
return response
}
async disconnect() { async disconnect() {
return this.client.quit() return this.client.quit()
} }

View File

@ -3,10 +3,12 @@ import {
QueryType, QueryType,
IntegrationBase, IntegrationBase,
DatasourceFieldType, DatasourceFieldType,
DatasourceFeature,
ConnectionInfo,
} from "@budibase/types" } from "@budibase/types"
const AWS = require("aws-sdk") import AWS from "aws-sdk"
const csv = require("csvtojson") import csv from "csvtojson"
interface S3Config { interface S3Config {
region: string region: string
@ -22,6 +24,7 @@ const SCHEMA: Integration = {
"Amazon Simple Storage Service (Amazon S3) is an object storage service that offers industry-leading scalability, data availability, security, and performance.", "Amazon Simple Storage Service (Amazon S3) is an object storage service that offers industry-leading scalability, data availability, security, and performance.",
friendlyName: "Amazon S3", friendlyName: "Amazon S3",
type: "Object store", type: "Object store",
features: [DatasourceFeature.CONNECTION_CHECKING],
datasource: { datasource: {
region: { region: {
type: "string", type: "string",
@ -152,7 +155,7 @@ const SCHEMA: Integration = {
class S3Integration implements IntegrationBase { class S3Integration implements IntegrationBase {
private readonly config: S3Config private readonly config: S3Config
private client: any private client
constructor(config: S3Config) { constructor(config: S3Config) {
this.config = config this.config = config
@ -165,6 +168,19 @@ class S3Integration implements IntegrationBase {
this.client = new AWS.S3(this.config) this.client = new AWS.S3(this.config)
} }
async testConnection() {
const response: ConnectionInfo = {
connected: false,
}
try {
await this.client.listBuckets().promise()
response.connected = true
} catch (e: any) {
response.error = e.message as string
}
return response
}
async create(query: { async create(query: {
bucket: string bucket: string
location: string location: string

View File

@ -1,4 +1,10 @@
import { Integration, QueryType, SqlQuery } from "@budibase/types" import {
ConnectionInfo,
DatasourceFeature,
Integration,
QueryType,
SqlQuery,
} from "@budibase/types"
import { Snowflake } from "snowflake-promise" import { Snowflake } from "snowflake-promise"
interface SnowflakeConfig { interface SnowflakeConfig {
@ -16,6 +22,7 @@ const SCHEMA: Integration = {
"Snowflake is a solution for data warehousing, data lakes, data engineering, data science, data application development, and securely sharing and consuming shared data.", "Snowflake is a solution for data warehousing, data lakes, data engineering, data science, data application development, and securely sharing and consuming shared data.",
friendlyName: "Snowflake", friendlyName: "Snowflake",
type: "Relational", type: "Relational",
features: [DatasourceFeature.CONNECTION_CHECKING],
datasource: { datasource: {
account: { account: {
type: "string", type: "string",
@ -65,6 +72,18 @@ class SnowflakeIntegration {
this.client = new Snowflake(config) this.client = new Snowflake(config)
} }
async testConnection(): Promise<ConnectionInfo> {
try {
await this.client.connect()
return { connected: true }
} catch (e: any) {
return {
connected: false,
error: e.message as string,
}
}
}
async internalQuery(query: SqlQuery) { async internalQuery(query: SqlQuery) {
await this.client.connect() await this.client.connect()
try { try {

View File

@ -33,7 +33,7 @@ export const backfill = async (appDb: any, timestamp: string | number) => {
datasource = { datasource = {
type: "unknown", type: "unknown",
_id: query.datasourceId, _id: query.datasourceId,
source: SourceName.UNKNOWN, source: "unknown" as SourceName,
} }
} else { } else {
throw e throw e

View File

@ -13,6 +13,7 @@ import {
import { cloneDeep } from "lodash/fp" import { cloneDeep } from "lodash/fp"
import { getEnvironmentVariables } from "../../utils" import { getEnvironmentVariables } from "../../utils"
import { getDefinitions, getDefinition } from "../../../integrations" import { getDefinitions, getDefinition } from "../../../integrations"
import _ from "lodash"
const ENV_VAR_PREFIX = "env." const ENV_VAR_PREFIX = "env."
@ -41,7 +42,7 @@ async function enrichDatasourceWithValues(datasource: Datasource) {
{ onlyFound: true } { onlyFound: true }
) as Datasource ) as Datasource
const definition = await getDefinition(processed.source) const definition = await getDefinition(processed.source)
processed.config = checkDatasourceTypes(definition, processed.config) processed.config = checkDatasourceTypes(definition!, processed.config)
return { return {
datasource: processed, datasource: processed,
envVars: env as Record<string, string>, envVars: env as Record<string, string>,
@ -147,6 +148,11 @@ export function mergeConfigs(update: Datasource, old: Datasource) {
} }
} }
} }
if (old.config?.auth) {
update.config = _.merge(old.config, update.config)
}
// update back to actual passwords for everything else // update back to actual passwords for everything else
for (let [key, value] of Object.entries(update.config)) { for (let [key, value] of Object.entries(update.config)) {
if (value !== PASSWORD_REPLACEMENT) { if (value !== PASSWORD_REPLACEMENT) {

View File

@ -137,8 +137,7 @@ export function inputProcessing(
opts?: AutoColumnProcessingOpts opts?: AutoColumnProcessingOpts
) { ) {
let clonedRow = cloneDeep(row) let clonedRow = cloneDeep(row)
// need to copy the table so it can be differenced on way out
const copiedTable = cloneDeep(table)
const dontCleanseKeys = ["type", "_id", "_rev", "tableId"] const dontCleanseKeys = ["type", "_id", "_rev", "tableId"]
for (let [key, value] of Object.entries(clonedRow)) { for (let [key, value] of Object.entries(clonedRow)) {
const field = table.schema[key] const field = table.schema[key]
@ -175,7 +174,7 @@ export function inputProcessing(
} }
// handle auto columns - this returns an object like {table, row} // handle auto columns - this returns an object like {table, row}
return processAutoColumn(user, copiedTable, clonedRow, opts) return processAutoColumn(user, table, clonedRow, opts)
} }
/** /**

View File

@ -2,6 +2,22 @@
import { FieldTypes } from "../../constants" import { FieldTypes } from "../../constants"
import { logging } from "@budibase/backend-core" import { logging } from "@budibase/backend-core"
const parseArrayString = value => {
if (typeof value === "string") {
if (value === "") {
return []
}
let result
try {
result = JSON.parse(value.replace(/'/g, '"'))
return result
} catch (e) {
logging.logAlert("Could not parse row value", e)
}
}
return value
}
/** /**
* A map of how we convert various properties in rows to each other based on the row type. * A map of how we convert various properties in rows to each other based on the row type.
*/ */
@ -26,9 +42,9 @@ export const TYPE_TRANSFORM_MAP: any = {
[undefined]: undefined, [undefined]: undefined,
}, },
[FieldTypes.ARRAY]: { [FieldTypes.ARRAY]: {
"": [],
[null]: [], [null]: [],
[undefined]: undefined, [undefined]: undefined,
parse: parseArrayString,
}, },
[FieldTypes.STRING]: { [FieldTypes.STRING]: {
"": "", "": "",
@ -70,21 +86,7 @@ export const TYPE_TRANSFORM_MAP: any = {
[FieldTypes.ATTACHMENT]: { [FieldTypes.ATTACHMENT]: {
[null]: [], [null]: [],
[undefined]: undefined, [undefined]: undefined,
parse: attachments => { parse: parseArrayString,
if (typeof attachments === "string") {
if (attachments === "") {
return []
}
let result
try {
result = JSON.parse(attachments)
} catch (e) {
logging.logAlert("Could not parse attachments", e)
}
return result
}
return attachments
},
}, },
[FieldTypes.BOOLEAN]: { [FieldTypes.BOOLEAN]: {
"": null, "": null,

View File

@ -14,6 +14,15 @@ export interface CreateDatasourceRequest {
fetchSchema?: boolean fetchSchema?: boolean
} }
export interface VerifyDatasourceRequest {
datasource: Datasource
}
export interface VerifyDatasourceResponse {
connected: boolean
error?: string
}
export interface UpdateDatasourceRequest extends Datasource { export interface UpdateDatasourceRequest extends Datasource {
datasource: Datasource datasource: Datasource
} }

View File

@ -55,7 +55,6 @@ export enum SourceName {
FIRESTORE = "FIRESTORE", FIRESTORE = "FIRESTORE",
REDIS = "REDIS", REDIS = "REDIS",
SNOWFLAKE = "SNOWFLAKE", SNOWFLAKE = "SNOWFLAKE",
UNKNOWN = "unknown",
} }
export enum IncludeRelationship { export enum IncludeRelationship {
@ -74,6 +73,10 @@ export enum FilterType {
ONE_OF = "oneOf", ONE_OF = "oneOf",
} }
export enum DatasourceFeature {
CONNECTION_CHECKING = "connection",
}
export interface StepDefinition { export interface StepDefinition {
key: string key: string
template: string template: string
@ -112,6 +115,7 @@ export interface Integration {
docs: string docs: string
plus?: boolean plus?: boolean
auth?: { type: string } auth?: { type: string }
features?: DatasourceFeature[]
relationships?: boolean relationships?: boolean
description: string description: string
friendlyName: string friendlyName: string
@ -124,11 +128,17 @@ export interface Integration {
extra?: ExtraQueryConfig extra?: ExtraQueryConfig
} }
export type ConnectionInfo = {
connected: boolean
error?: string
}
export interface IntegrationBase { export interface IntegrationBase {
create?(query: any): Promise<any[] | any> create?(query: any): Promise<any[] | any>
read?(query: any): Promise<any[] | any> read?(query: any): Promise<any[] | any>
update?(query: any): Promise<any[] | any> update?(query: any): Promise<any[] | any>
delete?(query: any): Promise<any[] | any> delete?(query: any): Promise<any[] | any>
testConnection?(): Promise<ConnectionInfo>
} }
export interface DatasourcePlus extends IntegrationBase { export interface DatasourcePlus extends IntegrationBase {

View File

@ -1,5 +1,9 @@
import { events } from "@budibase/backend-core" import { events } from "@budibase/backend-core"
import { generator } from "@budibase/backend-core/tests"
import { structures, TestConfiguration, mocks } from "../../../../tests" import { structures, TestConfiguration, mocks } from "../../../../tests"
import { UserGroup } from "@budibase/types"
mocks.licenses.useGroups()
describe("/api/global/groups", () => { describe("/api/global/groups", () => {
const config = new TestConfiguration() const config = new TestConfiguration()
@ -113,4 +117,118 @@ describe("/api/global/groups", () => {
}) })
}) })
}) })
describe("find users", () => {
describe("without users", () => {
let group: UserGroup
beforeAll(async () => {
group = structures.groups.UserGroup()
await config.api.groups.saveGroup(group)
})
it("should return empty", async () => {
const result = await config.api.groups.searchUsers(group._id!)
expect(result.body).toEqual({
users: [],
bookmark: undefined,
hasNextPage: false,
})
})
})
describe("existing users", () => {
let groupId: string
let users: { _id: string; email: string }[] = []
beforeAll(async () => {
groupId = (
await config.api.groups.saveGroup(structures.groups.UserGroup())
).body._id
await Promise.all(
Array.from({ length: 30 }).map(async (_, i) => {
const email = `user${i}@${generator.domain()}`
const user = await config.api.users.saveUser({
...structures.users.user(),
email,
})
users.push({ _id: user.body._id, email })
})
)
users = users.sort((a, b) => a._id.localeCompare(b._id))
await config.api.groups.updateGroupUsers(groupId, {
add: users.map(u => u._id),
remove: [],
})
})
describe("pagination", () => {
it("should return first page", async () => {
const result = await config.api.groups.searchUsers(groupId)
expect(result.body).toEqual({
users: users.slice(0, 10),
bookmark: users[10]._id,
hasNextPage: true,
})
})
it("given a bookmark, should return skip items", async () => {
const result = await config.api.groups.searchUsers(groupId, {
bookmark: users[7]._id,
})
expect(result.body).toEqual({
users: users.slice(7, 17),
bookmark: users[17]._id,
hasNextPage: true,
})
})
it("bookmarking the last page, should return last page info", async () => {
const result = await config.api.groups.searchUsers(groupId, {
bookmark: users[20]._id,
})
expect(result.body).toEqual({
users: users.slice(20),
bookmark: undefined,
hasNextPage: false,
})
})
})
describe("search by email", () => {
it('should be able to search "starting" by email', async () => {
const result = await config.api.groups.searchUsers(groupId, {
emailSearch: `user1`,
})
const matchedUsers = users
.filter(u => u.email.startsWith("user1"))
.sort((a, b) => a.email.localeCompare(b.email))
expect(result.body).toEqual({
users: matchedUsers.slice(0, 10),
bookmark: matchedUsers[10].email,
hasNextPage: true,
})
})
it("should be able to bookmark when searching by email", async () => {
const matchedUsers = users
.filter(u => u.email.startsWith("user1"))
.sort((a, b) => a.email.localeCompare(b.email))
const result = await config.api.groups.searchUsers(groupId, {
emailSearch: `user1`,
bookmark: matchedUsers[4].email,
})
expect(result.body).toEqual({
users: matchedUsers.slice(4),
bookmark: undefined,
hasNextPage: false,
})
})
})
})
})
}) })

View File

@ -23,4 +23,34 @@ export class GroupsAPI extends TestAPI {
.expect("Content-Type", /json/) .expect("Content-Type", /json/)
.expect(200) .expect(200)
} }
searchUsers = (
id: string,
params?: { bookmark?: string; emailSearch?: string }
) => {
let url = `/api/global/groups/${id}/users?`
if (params?.bookmark) {
url += `bookmark=${params.bookmark}&`
}
if (params?.emailSearch) {
url += `emailSearch=${params.emailSearch}&`
}
return this.request
.get(url)
.set(this.config.defaultHeaders())
.expect("Content-Type", /json/)
.expect(200)
}
updateGroupUsers = (
id: string,
body: { add: string[]; remove: string[] }
) => {
return this.request
.post(`/api/global/groups/${id}/users`)
.send(body)
.set(this.config.defaultHeaders())
.expect("Content-Type", /json/)
.expect(200)
}
} }

View File

@ -7,7 +7,7 @@ function getExpirySecondsForDB(db: string) {
// a hour // a hour
return 3600 return 3600
case redis.utils.Databases.INVITATIONS: case redis.utils.Databases.INVITATIONS:
// a day // a week
return 604800 return 604800
} }
} }

View File

@ -14,12 +14,14 @@
"test:watch": "yarn run test --watch", "test:watch": "yarn run test --watch",
"test:debug": "DEBUG=1 yarn run test", "test:debug": "DEBUG=1 yarn run test",
"test:notify": "node scripts/testResultsWebhook", "test:notify": "node scripts/testResultsWebhook",
"test:smoke": "yarn run test --testPathIgnorePatterns=\\\"\\/dataSources\\/\\\"", "test:smoke": "yarn run test --testPathIgnorePatterns=/.+\\.integration\\.spec\\.ts",
"test:ci": "start-server-and-test dev:built http://localhost:4001/health test:smoke", "test:ci": "start-server-and-test dev:built http://localhost:4001/health test:smoke",
"serve": "start-server-and-test dev:built http://localhost:4001/health",
"dev:built": "cd ../ && yarn dev:built" "dev:built": "cd ../ && yarn dev:built"
}, },
"devDependencies": { "devDependencies": {
"@budibase/types": "^2.3.17", "@budibase/types": "^2.3.17",
"@trendyol/jest-testcontainers": "^2.1.1",
"@types/jest": "29.0.0", "@types/jest": "29.0.0",
"@types/node-fetch": "2.6.2", "@types/node-fetch": "2.6.2",
"chance": "1.1.8", "chance": "1.1.8",

View File

@ -0,0 +1,77 @@
import { GenericContainer, Wait } from "testcontainers"
import arangodb from "../../../../packages/server/src/integrations/arangodb"
import { generator } from "../../shared"
jest.unmock("arangojs")
describe("datasource validators", () => {
describe("arangodb", () => {
let connectionSettings: {
user: string
password: string
url: string
}
beforeAll(async () => {
const user = "root"
const password = generator.hash()
const container = await new GenericContainer("arangodb")
.withExposedPorts(8529)
.withEnv("ARANGO_ROOT_PASSWORD", password)
.withWaitStrategy(
Wait.forLogMessage("is ready for business. Have fun!")
)
.start()
connectionSettings = {
user,
password,
url: `http://${container.getContainerIpAddress()}:${container.getMappedPort(
8529
)}`,
}
})
it("test valid connection string", async () => {
const integration = new arangodb.integration({
url: connectionSettings.url,
username: connectionSettings.user,
password: connectionSettings.password,
databaseName: "",
collection: "",
})
const result = await integration.testConnection()
expect(result).toEqual({ connected: true })
})
it("test wrong password", async () => {
const integration = new arangodb.integration({
url: connectionSettings.url,
username: connectionSettings.user,
password: "wrong",
databaseName: "",
collection: "",
})
const result = await integration.testConnection()
expect(result).toEqual({
connected: false,
error: "not authorized to execute this request",
})
})
it("test wrong url", async () => {
const integration = new arangodb.integration({
url: "http://not.here",
username: connectionSettings.user,
password: connectionSettings.password,
databaseName: "",
collection: "",
})
const result = await integration.testConnection()
expect(result).toEqual({
connected: false,
error: "getaddrinfo ENOTFOUND not.here",
})
})
})
})

View File

@ -0,0 +1,67 @@
import { GenericContainer } from "testcontainers"
import couchdb from "../../../../packages/server/src/integrations/couchdb"
import { generator } from "../../shared"
describe("datasource validators", () => {
describe("couchdb", () => {
let url: string
beforeAll(async () => {
const user = generator.first()
const password = generator.hash()
const container = await new GenericContainer("budibase/couchdb")
.withExposedPorts(5984)
.withEnv("COUCHDB_USER", user)
.withEnv("COUCHDB_PASSWORD", password)
.start()
const host = container.getContainerIpAddress()
const port = container.getMappedPort(5984)
await container.exec([
`curl`,
`-u`,
`${user}:${password}`,
`-X`,
`PUT`,
`localhost:5984/db`,
])
url = `http://${user}:${password}@${host}:${port}`
})
it("test valid connection string", async () => {
const integration = new couchdb.integration({
url,
database: "db",
})
const result = await integration.testConnection()
expect(result).toEqual({ connected: true })
})
it("test invalid database", async () => {
const integration = new couchdb.integration({
url,
database: "random_db",
})
const result = await integration.testConnection()
expect(result).toEqual({
connected: false,
})
})
it("test invalid url", async () => {
const integration = new couchdb.integration({
url: "http://invalid:123",
database: "any",
})
const result = await integration.testConnection()
expect(result).toEqual({
connected: false,
error:
"request to http://invalid:123/any failed, reason: getaddrinfo ENOTFOUND invalid",
})
})
})
})

View File

@ -0,0 +1,63 @@
import { GenericContainer } from "testcontainers"
import { env } from "@budibase/backend-core"
import dynamodb from "../../../../packages/server/src/integrations/dynamodb"
import { generator } from "../../shared"
jest.unmock("aws-sdk")
describe("datasource validators", () => {
describe("dynamodb", () => {
let connectionSettings: {
user: string
password: string
url: string
}
beforeAll(async () => {
const user = "root"
const password = generator.hash()
const container = await new GenericContainer("amazon/dynamodb-local")
.withExposedPorts(8000)
.start()
connectionSettings = {
user,
password,
url: `http://${container.getContainerIpAddress()}:${container.getMappedPort(
8000
)}`,
}
env._set("AWS_ACCESS_KEY_ID", "mocked_key")
env._set("AWS_SECRET_ACCESS_KEY", "mocked_secret")
})
it("test valid connection string", async () => {
const integration = new dynamodb.integration({
endpoint: connectionSettings.url,
region: "",
accessKeyId: "",
secretAccessKey: "",
})
const result = await integration.testConnection()
expect(result).toEqual({ connected: true })
})
it("test wrong endpoint", async () => {
const integration = new dynamodb.integration({
endpoint: "http://wrong.url:2880",
region: "",
accessKeyId: "",
secretAccessKey: "",
})
const result = await integration.testConnection()
expect(result).toEqual({
connected: false,
error:
"Inaccessible host: `wrong.url' at port `undefined'. This service may not be available in the `eu-west-1' region.",
})
})
})
})

View File

@ -0,0 +1,34 @@
import { ElasticsearchContainer } from "testcontainers"
import elastic from "../../../../packages/server/src/integrations/elasticsearch"
jest.unmock("@elastic/elasticsearch")
describe("datasource validators", () => {
describe("elastic search", () => {
let url: string
beforeAll(async () => {
const container = await new ElasticsearchContainer().start()
url = container.getHttpUrl()
})
it("test valid connection string", async () => {
const integration = new elastic.integration({
url,
})
const result = await integration.testConnection()
expect(result).toEqual({ connected: true })
})
it("test wrong connection string", async () => {
const integration = new elastic.integration({
url: `http://localhost:5656`,
})
const result = await integration.testConnection()
expect(result).toEqual({
connected: false,
error: "connect ECONNREFUSED 127.0.0.1:5656",
})
})
})
})

View File

@ -0,0 +1,100 @@
import { GenericContainer } from "testcontainers"
import mongo from "../../../../packages/server/src/integrations/mongodb"
import { generator } from "../../shared"
jest.unmock("mongodb")
describe("datasource validators", () => {
describe("mongo", () => {
let connectionSettings: {
user: string
password: string
host: string
port: number
}
function getConnectionString(
settings: Partial<typeof connectionSettings> = {}
) {
const { user, password, host, port } = {
...connectionSettings,
...settings,
}
return `mongodb://${user}:${password}@${host}:${port}`
}
beforeAll(async () => {
const user = generator.name()
const password = generator.hash()
const container = await new GenericContainer("mongo")
.withExposedPorts(27017)
.withEnv("MONGO_INITDB_ROOT_USERNAME", user)
.withEnv("MONGO_INITDB_ROOT_PASSWORD", password)
.start()
connectionSettings = {
user,
password,
host: container.getContainerIpAddress(),
port: container.getMappedPort(27017),
}
})
it("test valid connection string", async () => {
const integration = new mongo.integration({
connectionString: getConnectionString(),
db: "",
tlsCertificateFile: "",
tlsCertificateKeyFile: "",
tlsCAFile: "",
})
const result = await integration.testConnection()
expect(result).toEqual({ connected: true })
})
it("test invalid password", async () => {
const integration = new mongo.integration({
connectionString: getConnectionString({ password: "wrong" }),
db: "",
tlsCertificateFile: "",
tlsCertificateKeyFile: "",
tlsCAFile: "",
})
const result = await integration.testConnection()
expect(result).toEqual({
connected: false,
error: "Authentication failed.",
})
})
it("test invalid username", async () => {
const integration = new mongo.integration({
connectionString: getConnectionString({ user: "wrong" }),
db: "",
tlsCertificateFile: "",
tlsCertificateKeyFile: "",
tlsCAFile: "",
})
const result = await integration.testConnection()
expect(result).toEqual({
connected: false,
error: "Authentication failed.",
})
})
it("test invalid connection", async () => {
const integration = new mongo.integration({
connectionString: getConnectionString({ host: "http://nothinghere" }),
db: "",
tlsCertificateFile: "",
tlsCertificateKeyFile: "",
tlsCAFile: "",
})
const result = await integration.testConnection()
expect(result).toEqual({
connected: false,
error: "Error: getaddrinfo ENOTFOUND http",
})
})
})
})

View File

@ -0,0 +1,65 @@
import { GenericContainer, Wait } from "testcontainers"
import { Duration, TemporalUnit } from "node-duration"
import mssql from "../../../../packages/server/src/integrations/microsoftSqlServer"
jest.unmock("mssql")
describe("datasource validators", () => {
describe("mssql", () => {
let host: string, port: number
const password = "Str0Ng_p@ssW0rd!"
beforeAll(async () => {
const container = await new GenericContainer(
"mcr.microsoft.com/mssql/server"
)
.withExposedPorts(1433)
.withEnv("ACCEPT_EULA", "Y")
.withEnv("MSSQL_SA_PASSWORD", password)
.withEnv("MSSQL_PID", "Developer")
.withWaitStrategy(Wait.forHealthCheck())
.withHealthCheck({
test: `/opt/mssql-tools/bin/sqlcmd -S localhost -U sa -P "${password}" -Q "SELECT 1" -b -o /dev/null`,
interval: new Duration(1000, TemporalUnit.MILLISECONDS),
timeout: new Duration(3, TemporalUnit.SECONDS),
retries: 20,
startPeriod: new Duration(100, TemporalUnit.MILLISECONDS),
})
.start()
host = container.getContainerIpAddress()
port = container.getMappedPort(1433)
})
it("test valid connection string", async () => {
const integration = new mssql.integration({
user: "sa",
password,
server: host,
port: port,
database: "master",
schema: "dbo",
})
const result = await integration.testConnection()
expect(result).toEqual({ connected: true })
})
it("test invalid password", async () => {
const integration = new mssql.integration({
user: "sa",
password: "wrong_pwd",
server: host,
port: port,
database: "master",
schema: "dbo",
})
const result = await integration.testConnection()
expect(result).toEqual({
connected: false,
error: "ConnectionError: Login failed for user 'sa'.",
})
})
})
})

View File

@ -0,0 +1,70 @@
import { GenericContainer } from "testcontainers"
import mysql from "../../../../packages/server/src/integrations/mysql"
jest.unmock("mysql2/promise")
describe("datasource validators", () => {
describe("mysql", () => {
let host: string
let port: number
beforeAll(async () => {
const container = await new GenericContainer("mysql")
.withExposedPorts(3306)
.withEnv("MYSQL_ROOT_PASSWORD", "admin")
.withEnv("MYSQL_DATABASE", "db")
.withEnv("MYSQL_USER", "user")
.withEnv("MYSQL_PASSWORD", "password")
.start()
host = container.getContainerIpAddress()
port = container.getMappedPort(3306)
})
it("test valid connection string", async () => {
const integration = new mysql.integration({
host,
port,
user: "user",
database: "db",
password: "password",
rejectUnauthorized: true,
})
const result = await integration.testConnection()
expect(result).toEqual({ connected: true })
})
it("test invalid database", async () => {
const integration = new mysql.integration({
host,
port,
user: "user",
database: "test",
password: "password",
rejectUnauthorized: true,
})
const result = await integration.testConnection()
expect(result).toEqual({
connected: false,
error: "Access denied for user 'user'@'%' to database 'test'",
})
})
it("test invalid password", async () => {
const integration = new mysql.integration({
host,
port,
user: "root",
database: "test",
password: "wrong",
rejectUnauthorized: true,
})
const result = await integration.testConnection()
expect(result).toEqual({
connected: false,
error:
"Access denied for user 'root'@'172.17.0.1' (using password: YES)",
})
})
})
})

View File

@ -0,0 +1,53 @@
import { GenericContainer } from "testcontainers"
jest.unmock("pg")
describe("datasource validators", () => {
describe("postgres", () => {
let host: string
let port: number
beforeAll(async () => {
const container = await new GenericContainer("postgres")
.withExposedPorts(5432)
.withEnv("POSTGRES_PASSWORD", "password")
.start()
host = container.getContainerIpAddress()
port = container.getMappedPort(5432)
})
it("test valid connection string", async () => {
const integration = new postgres.integration({
host,
port,
database: "postgres",
user: "postgres",
password: "password",
schema: "public",
ssl: false,
rejectUnauthorized: false,
})
const result = await integration.testConnection()
expect(result).toEqual({ connected: true })
})
it("test invalid connection string", async () => {
const integration = new postgres.integration({
host,
port,
database: "postgres",
user: "wrong",
password: "password",
schema: "public",
ssl: false,
rejectUnauthorized: false,
})
const result = await integration.testConnection()
expect(result).toEqual({
connected: false,
error: 'password authentication failed for user "wrong"',
})
})
})
})

View File

@ -0,0 +1,72 @@
import redis from "../../../../packages/server/src/integrations/redis"
import { GenericContainer } from "testcontainers"
import { generator } from "../../shared"
describe("datasource validators", () => {
describe("redis", () => {
describe("unsecured", () => {
let host: string
let port: number
beforeAll(async () => {
const container = await new GenericContainer("redis")
.withExposedPorts(6379)
.start()
host = container.getContainerIpAddress()
port = container.getMappedPort(6379)
})
it("test valid connection", async () => {
const integration = new redis.integration({
host,
port,
username: "",
})
const result = await integration.testConnection()
expect(result).toEqual({ connected: true })
})
it("test invalid connection even with wrong user/password", async () => {
const integration = new redis.integration({
host,
port,
username: generator.name(),
password: generator.hash(),
})
const result = await integration.testConnection()
expect(result).toEqual({
connected: false,
error:
"WRONGPASS invalid username-password pair or user is disabled.",
})
})
})
describe("secured", () => {
let host: string
let port: number
beforeAll(async () => {
const container = await new GenericContainer("redis")
.withExposedPorts(6379)
.withCmd(["redis-server", "--requirepass", "P@ssW0rd!"])
.start()
host = container.getContainerIpAddress()
port = container.getMappedPort(6379)
})
it("test valid connection", async () => {
const integration = new redis.integration({
host,
port,
username: "",
password: "P@ssW0rd!",
})
const result = await integration.testConnection()
expect(result).toEqual({ connected: true })
})
})
})
})

View File

@ -0,0 +1,52 @@
import s3 from "../../../../packages/server/src/integrations/s3"
import { GenericContainer } from "testcontainers"
jest.unmock("aws-sdk")
describe("datasource validators", () => {
describe("s3", () => {
let host: string
let port: number
beforeAll(async () => {
const container = await new GenericContainer("localstack/localstack")
.withExposedPorts(4566)
.withEnv("SERVICES", "s3")
.withEnv("DEFAULT_REGION", "eu-west-1")
.withEnv("AWS_ACCESS_KEY_ID", "testkey")
.withEnv("AWS_SECRET_ACCESS_KEY", "testsecret")
.start()
host = container.getContainerIpAddress()
port = container.getMappedPort(4566)
})
it("test valid connection", async () => {
const integration = new s3.integration({
region: "eu-west-1",
accessKeyId: "testkey",
secretAccessKey: "testsecret",
s3ForcePathStyle: false,
endpoint: `http://${host}:${port}`,
})
const result = await integration.testConnection()
expect(result).toEqual({ connected: true })
})
it("test wrong endpoint", async () => {
const integration = new s3.integration({
region: "eu-west-2",
accessKeyId: "testkey",
secretAccessKey: "testsecret",
s3ForcePathStyle: false,
endpoint: `http://wrong:123`,
})
const result = await integration.testConnection()
expect(result).toEqual({
connected: false,
error:
"Inaccessible host: `wrong' at port `undefined'. This service may not be available in the `eu-west-2' region.",
})
})
})
})

View File

@ -1,3 +1,3 @@
const Chance = require("chance") import Chance from "chance"
export default new Chance() export default new Chance()

View File

@ -304,6 +304,11 @@
"@babel/helper-validator-identifier" "^7.18.6" "@babel/helper-validator-identifier" "^7.18.6"
to-fast-properties "^2.0.0" to-fast-properties "^2.0.0"
"@balena/dockerignore@^1.0.2":
version "1.0.2"
resolved "https://registry.yarnpkg.com/@balena/dockerignore/-/dockerignore-1.0.2.tgz#9ffe4726915251e8eb69f44ef3547e0da2c03e0d"
integrity sha512-wMue2Sy4GAVTk6Ic4tJVcnfdau+gx2EnG7S+uAEe+TWJFqE4YoWN4/H8MSLj4eYJKxGg26lZwboEniNiNwZQ6Q==
"@bcoe/v8-coverage@^0.2.3": "@bcoe/v8-coverage@^0.2.3":
version "0.2.3" version "0.2.3"
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"
@ -779,6 +784,15 @@
request "^2.88.0" request "^2.88.0"
webfinger "^0.4.2" webfinger "^0.4.2"
"@trendyol/jest-testcontainers@^2.1.1":
version "2.1.1"
resolved "https://registry.yarnpkg.com/@trendyol/jest-testcontainers/-/jest-testcontainers-2.1.1.tgz#dced95cf9c37b75efe0a65db9b75ae8912f2f14a"
integrity sha512-4iAc2pMsev4BTUzoA7jO1VvbTOU2N3juQUYa8TwiSPXPuQtxKwV9WB9ZEP+JQ+Pj15YqfGOXp5H0WNMPtapjiA==
dependencies:
cwd "^0.10.0"
node-duration "^1.0.4"
testcontainers "4.7.0"
"@tsconfig/node10@^1.0.7": "@tsconfig/node10@^1.0.7":
version "1.0.9" version "1.0.9"
resolved "https://registry.yarnpkg.com/@tsconfig/node10/-/node10-1.0.9.tgz#df4907fc07a886922637b15e02d4cebc4c0021b2" resolved "https://registry.yarnpkg.com/@tsconfig/node10/-/node10-1.0.9.tgz#df4907fc07a886922637b15e02d4cebc4c0021b2"
@ -832,6 +846,13 @@
dependencies: dependencies:
"@babel/types" "^7.3.0" "@babel/types" "^7.3.0"
"@types/dockerode@^2.5.34":
version "2.5.34"
resolved "https://registry.yarnpkg.com/@types/dockerode/-/dockerode-2.5.34.tgz#9adb884f7cc6c012a6eb4b2ad794cc5d01439959"
integrity sha512-LcbLGcvcBwBAvjH9UrUI+4qotY+A5WCer5r43DR5XHv2ZIEByNXFdPLo1XxR+v/BjkGjlggW8qUiXuVEhqfkpA==
dependencies:
"@types/node" "*"
"@types/graceful-fs@^4.1.3": "@types/graceful-fs@^4.1.3":
version "4.1.5" version "4.1.5"
resolved "https://registry.yarnpkg.com/@types/graceful-fs/-/graceful-fs-4.1.5.tgz#21ffba0d98da4350db64891f92a9e5db3cdb4e15" resolved "https://registry.yarnpkg.com/@types/graceful-fs/-/graceful-fs-4.1.5.tgz#21ffba0d98da4350db64891f92a9e5db3cdb4e15"
@ -1006,6 +1027,11 @@ ansi-styles@^5.0.0:
resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-5.2.0.tgz#07449690ad45777d1924ac2abb2fc8895dba836b" resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-5.2.0.tgz#07449690ad45777d1924ac2abb2fc8895dba836b"
integrity sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA== integrity sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==
any-promise@^1.1.0:
version "1.3.0"
resolved "https://registry.yarnpkg.com/any-promise/-/any-promise-1.3.0.tgz#abc6afeedcea52e809cdc0376aed3ce39635d17f"
integrity sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==
anymatch@^3.0.3: anymatch@^3.0.3:
version "3.1.2" version "3.1.2"
resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.2.tgz#c0557c096af32f106198f4f4e2a383537e378716" resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.2.tgz#c0557c096af32f106198f4f4e2a383537e378716"
@ -1044,7 +1070,7 @@ argsarray@0.0.1:
resolved "https://registry.yarnpkg.com/argsarray/-/argsarray-0.0.1.tgz#6e7207b4ecdb39b0af88303fa5ae22bda8df61cb" resolved "https://registry.yarnpkg.com/argsarray/-/argsarray-0.0.1.tgz#6e7207b4ecdb39b0af88303fa5ae22bda8df61cb"
integrity sha512-u96dg2GcAKtpTrBdDoFIM7PjcBA+6rSP0OR94MOReNRyUECL6MtQt5XXmRr4qrftYaef9+l5hcpO5te7sML1Cg== integrity sha512-u96dg2GcAKtpTrBdDoFIM7PjcBA+6rSP0OR94MOReNRyUECL6MtQt5XXmRr4qrftYaef9+l5hcpO5te7sML1Cg==
asn1@~0.2.3: asn1@^0.2.6, asn1@~0.2.3:
version "0.2.6" version "0.2.6"
resolved "https://registry.yarnpkg.com/asn1/-/asn1-0.2.6.tgz#0d3a7bb6e64e02a90c0303b31f292868ea09a08d" resolved "https://registry.yarnpkg.com/asn1/-/asn1-0.2.6.tgz#0d3a7bb6e64e02a90c0303b31f292868ea09a08d"
integrity sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ== integrity sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==
@ -1199,7 +1225,7 @@ base64url@3.x.x, base64url@^3.0.1:
resolved "https://registry.yarnpkg.com/base64url/-/base64url-3.0.1.tgz#6399d572e2bc3f90a9a8b22d5dbb0a32d33f788d" resolved "https://registry.yarnpkg.com/base64url/-/base64url-3.0.1.tgz#6399d572e2bc3f90a9a8b22d5dbb0a32d33f788d"
integrity sha512-ir1UPr3dkwexU7FdV8qBBbNDRUhMmIekYMFZfi+C/sLNnRESKPl23nB9b2pltqfOQNnGzsDdId90AEtG5tCx4A== integrity sha512-ir1UPr3dkwexU7FdV8qBBbNDRUhMmIekYMFZfi+C/sLNnRESKPl23nB9b2pltqfOQNnGzsDdId90AEtG5tCx4A==
bcrypt-pbkdf@^1.0.0: bcrypt-pbkdf@^1.0.0, bcrypt-pbkdf@^1.0.2:
version "1.0.2" version "1.0.2"
resolved "https://registry.yarnpkg.com/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz#a4301d389b6a43f9b67ff3ca11a3f6637e360e9e" resolved "https://registry.yarnpkg.com/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz#a4301d389b6a43f9b67ff3ca11a3f6637e360e9e"
integrity sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w== integrity sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==
@ -1304,6 +1330,11 @@ buffer@^5.5.0, buffer@^5.6.0:
base64-js "^1.3.1" base64-js "^1.3.1"
ieee754 "^1.1.13" ieee754 "^1.1.13"
buildcheck@~0.0.6:
version "0.0.6"
resolved "https://registry.yarnpkg.com/buildcheck/-/buildcheck-0.0.6.tgz#89aa6e417cfd1e2196e3f8fe915eb709d2fe4238"
integrity sha512-8f9ZJCUXyT1M35Jx7MkBgmBMo3oHTTBIPLiY9xyL0pl3T5RwcPEY8cUHr5LBNfu/fk6c2T4DJZuVM/8ZZT2D2A==
bull@4.10.1: bull@4.10.1:
version "4.10.1" version "4.10.1"
resolved "https://registry.yarnpkg.com/bull/-/bull-4.10.1.tgz#f14974b6089358b62b495a2cbf838aadc098e43f" resolved "https://registry.yarnpkg.com/bull/-/bull-4.10.1.tgz#f14974b6089358b62b495a2cbf838aadc098e43f"
@ -1319,6 +1350,11 @@ bull@4.10.1:
semver "^7.3.2" semver "^7.3.2"
uuid "^8.3.0" uuid "^8.3.0"
byline@^5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/byline/-/byline-5.0.0.tgz#741c5216468eadc457b03410118ad77de8c1ddb1"
integrity sha512-s6webAy+R4SR8XVuJWt2V2rGvhnrhxN+9S15GNuTK3wKPOXFF6RNc+8ug2XhH+2s4f+uudG4kUVYmYOQWL2g0Q==
cache-content-type@^1.0.0: cache-content-type@^1.0.0:
version "1.0.1" version "1.0.1"
resolved "https://registry.yarnpkg.com/cache-content-type/-/cache-content-type-1.0.1.tgz#035cde2b08ee2129f4a8315ea8f00a00dba1453c" resolved "https://registry.yarnpkg.com/cache-content-type/-/cache-content-type-1.0.1.tgz#035cde2b08ee2129f4a8315ea8f00a00dba1453c"
@ -1546,6 +1582,14 @@ correlation-id@4.0.0:
dependencies: dependencies:
uuid "^8.3.1" uuid "^8.3.1"
cpu-features@~0.0.7:
version "0.0.7"
resolved "https://registry.yarnpkg.com/cpu-features/-/cpu-features-0.0.7.tgz#81ba93e1d0a729fd25132a54c3ff689c37b542f7"
integrity sha512-fjzFmsUKKCrC9GrM1eQTvQx18e+kjXFzjRLvJPNEDjk31+bJ6ZiV6uchv/hzbzXVIgbWdrEyyX1IFKwse65+8w==
dependencies:
buildcheck "~0.0.6"
nan "^2.17.0"
create-require@^1.1.0: create-require@^1.1.0:
version "1.1.1" version "1.1.1"
resolved "https://registry.yarnpkg.com/create-require/-/create-require-1.1.1.tgz#c1d7e8f1e5f6cfc9ff65f9cd352d37348756c333" resolved "https://registry.yarnpkg.com/create-require/-/create-require-1.1.1.tgz#c1d7e8f1e5f6cfc9ff65f9cd352d37348756c333"
@ -1572,6 +1616,14 @@ crypt@0.0.2:
resolved "https://registry.yarnpkg.com/crypt/-/crypt-0.0.2.tgz#88d7ff7ec0dfb86f713dc87bbb42d044d3e6c41b" resolved "https://registry.yarnpkg.com/crypt/-/crypt-0.0.2.tgz#88d7ff7ec0dfb86f713dc87bbb42d044d3e6c41b"
integrity sha512-mCxBlsHFYh9C+HVpiEacem8FEBnMXgU9gy4zmNC+SXAZNB/1idgp/aulFJ4FgCi7GPEVbfyng092GqL2k2rmow== integrity sha512-mCxBlsHFYh9C+HVpiEacem8FEBnMXgU9gy4zmNC+SXAZNB/1idgp/aulFJ4FgCi7GPEVbfyng092GqL2k2rmow==
cwd@^0.10.0:
version "0.10.0"
resolved "https://registry.yarnpkg.com/cwd/-/cwd-0.10.0.tgz#172400694057c22a13b0cf16162c7e4b7a7fe567"
integrity sha512-YGZxdTTL9lmLkCUTpg4j0zQ7IhRB5ZmqNBbGCl3Tg6MP/d5/6sY7L5mmTjzbc6JKgVZYiqTQTNhPFsbXNGlRaA==
dependencies:
find-pkg "^0.1.2"
fs-exists-sync "^0.1.0"
dashdash@^1.12.0: dashdash@^1.12.0:
version "1.14.1" version "1.14.1"
resolved "https://registry.yarnpkg.com/dashdash/-/dashdash-1.14.1.tgz#853cfa0f7cbe2fed5de20326b8dd581035f6e2f0" resolved "https://registry.yarnpkg.com/dashdash/-/dashdash-1.14.1.tgz#853cfa0f7cbe2fed5de20326b8dd581035f6e2f0"
@ -1676,6 +1728,32 @@ diff@^4.0.1:
resolved "https://registry.yarnpkg.com/diff/-/diff-4.0.2.tgz#60f3aecb89d5fae520c11aa19efc2bb982aade7d" resolved "https://registry.yarnpkg.com/diff/-/diff-4.0.2.tgz#60f3aecb89d5fae520c11aa19efc2bb982aade7d"
integrity sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A== integrity sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==
docker-compose@^0.23.5:
version "0.23.19"
resolved "https://registry.yarnpkg.com/docker-compose/-/docker-compose-0.23.19.tgz#9947726e2fe67bdfa9e8efe1ff15aa0de2e10eb8"
integrity sha512-v5vNLIdUqwj4my80wxFDkNH+4S85zsRuH29SO7dCWVWPCMt/ohZBsGN6g6KXWifT0pzQ7uOxqEKCYCDPJ8Vz4g==
dependencies:
yaml "^1.10.2"
docker-modem@^3.0.0:
version "3.0.8"
resolved "https://registry.yarnpkg.com/docker-modem/-/docker-modem-3.0.8.tgz#ef62c8bdff6e8a7d12f0160988c295ea8705e77a"
integrity sha512-f0ReSURdM3pcKPNS30mxOHSbaFLcknGmQjwSfmbcdOw1XWKXVhukM3NJHhr7NpY9BIyyWQb0EBo3KQvvuU5egQ==
dependencies:
debug "^4.1.1"
readable-stream "^3.5.0"
split-ca "^1.0.1"
ssh2 "^1.11.0"
dockerode@^3.2.1:
version "3.3.5"
resolved "https://registry.yarnpkg.com/dockerode/-/dockerode-3.3.5.tgz#7ae3f40f2bec53ae5e9a741ce655fff459745629"
integrity sha512-/0YNa3ZDNeLr/tSckmD69+Gq+qVNhvKfAHNeZJBnp7EOP6RGKV8ORrJHkUn20So5wU+xxT7+1n5u8PjHbfjbSA==
dependencies:
"@balena/dockerignore" "^1.0.2"
docker-modem "^3.0.0"
tar-fs "~2.0.1"
dotenv@16.0.1: dotenv@16.0.1:
version "16.0.1" version "16.0.1"
resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.0.1.tgz#8f8f9d94876c35dac989876a5d3a82a267fdce1d" resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.0.1.tgz#8f8f9d94876c35dac989876a5d3a82a267fdce1d"
@ -1844,6 +1922,13 @@ exit@^0.1.2:
resolved "https://registry.yarnpkg.com/exit/-/exit-0.1.2.tgz#0632638f8d877cc82107d30a0fff1a17cba1cd0c" resolved "https://registry.yarnpkg.com/exit/-/exit-0.1.2.tgz#0632638f8d877cc82107d30a0fff1a17cba1cd0c"
integrity sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ== integrity sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==
expand-tilde@^1.2.2:
version "1.2.2"
resolved "https://registry.yarnpkg.com/expand-tilde/-/expand-tilde-1.2.2.tgz#0b81eba897e5a3d31d1c3d102f8f01441e559449"
integrity sha512-rtmc+cjLZqnu9dSYosX9EWmSJhTwpACgJQTfj4hgg2JjOD/6SIQalZrt4a3aQeh++oNxkazcaxrhPUj6+g5G/Q==
dependencies:
os-homedir "^1.0.1"
expect@^29.0.0: expect@^29.0.0:
version "29.0.2" version "29.0.2"
resolved "https://registry.yarnpkg.com/expect/-/expect-29.0.2.tgz#22c7132400f60444b427211f1d6bb604a9ab2420" resolved "https://registry.yarnpkg.com/expect/-/expect-29.0.2.tgz#22c7132400f60444b427211f1d6bb604a9ab2420"
@ -1919,6 +2004,21 @@ fill-range@^7.0.1:
dependencies: dependencies:
to-regex-range "^5.0.1" to-regex-range "^5.0.1"
find-file-up@^0.1.2:
version "0.1.3"
resolved "https://registry.yarnpkg.com/find-file-up/-/find-file-up-0.1.3.tgz#cf68091bcf9f300a40da411b37da5cce5a2fbea0"
integrity sha512-mBxmNbVyjg1LQIIpgO8hN+ybWBgDQK8qjht+EbrTCGmmPV/sc7RF1i9stPTD6bpvXZywBdrwRYxhSdJv867L6A==
dependencies:
fs-exists-sync "^0.1.0"
resolve-dir "^0.1.0"
find-pkg@^0.1.2:
version "0.1.2"
resolved "https://registry.yarnpkg.com/find-pkg/-/find-pkg-0.1.2.tgz#1bdc22c06e36365532e2a248046854b9788da557"
integrity sha512-0rnQWcFwZr7eO0513HahrWafsc3CTFioEB7DRiEYCUM/70QXSY8f3mCST17HXLcPvEhzH/Ty/Bxd72ZZsr/yvw==
dependencies:
find-file-up "^0.1.2"
find-up@^4.0.0, find-up@^4.1.0: find-up@^4.0.0, find-up@^4.1.0:
version "4.1.0" version "4.1.0"
resolved "https://registry.yarnpkg.com/find-up/-/find-up-4.1.0.tgz#97afe7d6cdc0bc5928584b7c8d7b16e8a9aa5d19" resolved "https://registry.yarnpkg.com/find-up/-/find-up-4.1.0.tgz#97afe7d6cdc0bc5928584b7c8d7b16e8a9aa5d19"
@ -1984,6 +2084,11 @@ fs-constants@^1.0.0:
resolved "https://registry.yarnpkg.com/fs-constants/-/fs-constants-1.0.0.tgz#6be0de9be998ce16af8afc24497b9ee9b7ccd9ad" resolved "https://registry.yarnpkg.com/fs-constants/-/fs-constants-1.0.0.tgz#6be0de9be998ce16af8afc24497b9ee9b7ccd9ad"
integrity sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow== integrity sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==
fs-exists-sync@^0.1.0:
version "0.1.0"
resolved "https://registry.yarnpkg.com/fs-exists-sync/-/fs-exists-sync-0.1.0.tgz#982d6893af918e72d08dec9e8673ff2b5a8d6add"
integrity sha512-cR/vflFyPZtrN6b38ZyWxpWdhlXrzZEBawlpBQMq7033xVY7/kg0GDMBK5jg8lDYQckdJ5x/YC88lM3C7VMsLg==
fs-minipass@^2.0.0: fs-minipass@^2.0.0:
version "2.1.0" version "2.1.0"
resolved "https://registry.yarnpkg.com/fs-minipass/-/fs-minipass-2.1.0.tgz#7f5036fdbf12c63c169190cbe4199c852271f9fb" resolved "https://registry.yarnpkg.com/fs-minipass/-/fs-minipass-2.1.0.tgz#7f5036fdbf12c63c169190cbe4199c852271f9fb"
@ -2062,7 +2167,7 @@ getpass@^0.1.1:
dependencies: dependencies:
assert-plus "^1.0.0" assert-plus "^1.0.0"
glob@^7.1.3, glob@^7.1.4: glob@^7.1.3, glob@^7.1.4, glob@^7.1.6:
version "7.2.3" version "7.2.3"
resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.3.tgz#b8df0fb802bbfa8e89bd1d938b4e16578ed44f2b" resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.3.tgz#b8df0fb802bbfa8e89bd1d938b4e16578ed44f2b"
integrity sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q== integrity sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==
@ -2074,6 +2179,24 @@ glob@^7.1.3, glob@^7.1.4:
once "^1.3.0" once "^1.3.0"
path-is-absolute "^1.0.0" path-is-absolute "^1.0.0"
global-modules@^0.2.3:
version "0.2.3"
resolved "https://registry.yarnpkg.com/global-modules/-/global-modules-0.2.3.tgz#ea5a3bed42c6d6ce995a4f8a1269b5dae223828d"
integrity sha512-JeXuCbvYzYXcwE6acL9V2bAOeSIGl4dD+iwLY9iUx2VBJJ80R18HCn+JCwHM9Oegdfya3lEkGCdaRkSyc10hDA==
dependencies:
global-prefix "^0.1.4"
is-windows "^0.2.0"
global-prefix@^0.1.4:
version "0.1.5"
resolved "https://registry.yarnpkg.com/global-prefix/-/global-prefix-0.1.5.tgz#8d3bc6b8da3ca8112a160d8d496ff0462bfef78f"
integrity sha512-gOPiyxcD9dJGCEArAhF4Hd0BAqvAe/JzERP7tYumE4yIkmIedPUVXcJFWbV3/p/ovIIvKjkrTk+f1UVkq7vvbw==
dependencies:
homedir-polyfill "^1.0.0"
ini "^1.3.4"
is-windows "^0.2.0"
which "^1.2.12"
globals@^11.1.0: globals@^11.1.0:
version "11.12.0" version "11.12.0"
resolved "https://registry.yarnpkg.com/globals/-/globals-11.12.0.tgz#ab8795338868a0babd8525758018c2a7eb95c42e" resolved "https://registry.yarnpkg.com/globals/-/globals-11.12.0.tgz#ab8795338868a0babd8525758018c2a7eb95c42e"
@ -2131,6 +2254,13 @@ has@^1.0.3:
dependencies: dependencies:
function-bind "^1.1.1" function-bind "^1.1.1"
homedir-polyfill@^1.0.0:
version "1.0.3"
resolved "https://registry.yarnpkg.com/homedir-polyfill/-/homedir-polyfill-1.0.3.tgz#743298cef4e5af3e194161fbadcc2151d3a058e8"
integrity sha512-eSmmWE5bZTK2Nou4g0AI3zZ9rswp7GRKoKXS1BLUkvPviOqs4YTN1djQIqrXy9k5gEtdLPy86JjRwsNM9tnDcA==
dependencies:
parse-passwd "^1.0.0"
html-escaper@^2.0.0: html-escaper@^2.0.0:
version "2.0.2" version "2.0.2"
resolved "https://registry.yarnpkg.com/html-escaper/-/html-escaper-2.0.2.tgz#dfd60027da36a36dfcbe236262c00a5822681453" resolved "https://registry.yarnpkg.com/html-escaper/-/html-escaper-2.0.2.tgz#dfd60027da36a36dfcbe236262c00a5822681453"
@ -2230,6 +2360,11 @@ inherits@2, inherits@2.0.4, inherits@^2.0.1, inherits@^2.0.3, inherits@^2.0.4, i
resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c"
integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==
ini@^1.3.4:
version "1.3.8"
resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.8.tgz#a29da425b48806f34767a4efce397269af28432c"
integrity sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==
ioredis@4.28.0: ioredis@4.28.0:
version "4.28.0" version "4.28.0"
resolved "https://registry.yarnpkg.com/ioredis/-/ioredis-4.28.0.tgz#5a2be3f37ff2075e2332f280eaeb02ab4d9ff0d3" resolved "https://registry.yarnpkg.com/ioredis/-/ioredis-4.28.0.tgz#5a2be3f37ff2075e2332f280eaeb02ab4d9ff0d3"
@ -2318,6 +2453,11 @@ is-typedarray@~1.0.0:
resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a" resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a"
integrity sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA== integrity sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==
is-windows@^0.2.0:
version "0.2.0"
resolved "https://registry.yarnpkg.com/is-windows/-/is-windows-0.2.0.tgz#de1aa6d63ea29dd248737b69f1ff8b8002d2108c"
integrity sha512-n67eJYmXbniZB7RF4I/FTjK1s6RPOCTxhYrVYLRaCt3lF0mpWZPKr3T2LSZAqyjQsxR2qMmGYXXzK0YWwcPM1Q==
isarray@0.0.1: isarray@0.0.1:
version "0.0.1" version "0.0.1"
resolved "https://registry.yarnpkg.com/isarray/-/isarray-0.0.1.tgz#8a18acfca9a8f4177e09abfc6038939b05d1eedf" resolved "https://registry.yarnpkg.com/isarray/-/isarray-0.0.1.tgz#8a18acfca9a8f4177e09abfc6038939b05d1eedf"
@ -3332,6 +3472,11 @@ msgpackr@^1.5.2:
optionalDependencies: optionalDependencies:
msgpackr-extract "^3.0.0" msgpackr-extract "^3.0.0"
nan@^2.17.0:
version "2.17.0"
resolved "https://registry.yarnpkg.com/nan/-/nan-2.17.0.tgz#c0150a2368a182f033e9aa5195ec76ea41a199cb"
integrity sha512-2ZTgtl0nJsO0KQCjEpxcIr5D+Yv90plTitZt9JBfQvVJDS5seMl3FOvsh3+9CoYWXf/1l5OaZzzF6nDm4cagaQ==
napi-macros@~2.0.0: napi-macros@~2.0.0:
version "2.0.0" version "2.0.0"
resolved "https://registry.yarnpkg.com/napi-macros/-/napi-macros-2.0.0.tgz#2b6bae421e7b96eb687aa6c77a7858640670001b" resolved "https://registry.yarnpkg.com/napi-macros/-/napi-macros-2.0.0.tgz#2b6bae421e7b96eb687aa6c77a7858640670001b"
@ -3367,6 +3512,11 @@ node-addon-api@^3.1.0:
resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-3.2.1.tgz#81325e0a2117789c0128dab65e7e38f07ceba161" resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-3.2.1.tgz#81325e0a2117789c0128dab65e7e38f07ceba161"
integrity sha512-mmcei9JghVNDYydghQmeDX8KoAm0FAiYyIcUt/N4nhyAipB17pllZQDOJD2fotxABnt4Mdz+dKTO7eftLg4d0A== integrity sha512-mmcei9JghVNDYydghQmeDX8KoAm0FAiYyIcUt/N4nhyAipB17pllZQDOJD2fotxABnt4Mdz+dKTO7eftLg4d0A==
node-duration@^1.0.4:
version "1.0.4"
resolved "https://registry.yarnpkg.com/node-duration/-/node-duration-1.0.4.tgz#3e94ecc0e473691c89c4560074503362071cecac"
integrity sha512-eUXYNSY7DL53vqfTosggWkvyIW3bhAcqBDIlolgNYlZhianXTrCL50rlUJWD1eRqkIxMppXTfiFbp+9SjpPrgA==
node-fetch@2, node-fetch@2.6.7, node-fetch@^2.6.7: node-fetch@2, node-fetch@2.6.7, node-fetch@^2.6.7:
version "2.6.7" version "2.6.7"
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.7.tgz#24de9fba827e3b4ae44dc8b20256a379160052ad" resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.7.tgz#24de9fba827e3b4ae44dc8b20256a379160052ad"
@ -3490,6 +3640,11 @@ only@~0.0.2:
resolved "https://registry.yarnpkg.com/only/-/only-0.0.2.tgz#2afde84d03e50b9a8edc444e30610a70295edfb4" resolved "https://registry.yarnpkg.com/only/-/only-0.0.2.tgz#2afde84d03e50b9a8edc444e30610a70295edfb4"
integrity sha512-Fvw+Jemq5fjjyWz6CpKx6w9s7xxqo3+JCyM0WXWeCSOboZ8ABkyvP8ID4CZuChA/wxSx+XSJmdOm8rGVyJ1hdQ== integrity sha512-Fvw+Jemq5fjjyWz6CpKx6w9s7xxqo3+JCyM0WXWeCSOboZ8ABkyvP8ID4CZuChA/wxSx+XSJmdOm8rGVyJ1hdQ==
os-homedir@^1.0.1:
version "1.0.2"
resolved "https://registry.yarnpkg.com/os-homedir/-/os-homedir-1.0.2.tgz#ffbc4988336e0e833de0c168c7ef152121aa7fb3"
integrity sha512-B5JU3cabzk8c67mRRd3ECmROafjYMXbuzlwtqdM8IbS8ktlTix8aFGb2bAGKrSRIlnfKwovGUUr72JUPyOb6kQ==
p-finally@^1.0.0: p-finally@^1.0.0:
version "1.0.0" version "1.0.0"
resolved "https://registry.yarnpkg.com/p-finally/-/p-finally-1.0.0.tgz#3fbcfb15b899a44123b34b6dcc18b724336a2cae" resolved "https://registry.yarnpkg.com/p-finally/-/p-finally-1.0.0.tgz#3fbcfb15b899a44123b34b6dcc18b724336a2cae"
@ -3543,6 +3698,11 @@ parse-json@^5.2.0:
json-parse-even-better-errors "^2.3.0" json-parse-even-better-errors "^2.3.0"
lines-and-columns "^1.1.6" lines-and-columns "^1.1.6"
parse-passwd@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/parse-passwd/-/parse-passwd-1.0.0.tgz#6d5b934a456993b23d37f40a382d6f1666a8e5c6"
integrity sha512-1Y1A//QUXEZK7YKz+rD9WydcE1+EuPr6ZBgKecAB8tmoW6UFv0NREVJe1p+jRxtThkcbbKkfwIbWJe/IeE6m2Q==
parseurl@^1.3.2, parseurl@^1.3.3: parseurl@^1.3.2, parseurl@^1.3.3:
version "1.3.3" version "1.3.3"
resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4" resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4"
@ -3982,6 +4142,15 @@ readable-stream@1.1.14, readable-stream@^1.0.27-1:
string_decoder "^1.1.1" string_decoder "^1.1.1"
util-deprecate "^1.0.1" util-deprecate "^1.0.1"
readable-stream@^3.5.0:
version "3.6.2"
resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.2.tgz#56a9b36ea965c00c5a93ef31eb111a0f11056967"
integrity sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==
dependencies:
inherits "^2.0.3"
string_decoder "^1.1.1"
util-deprecate "^1.0.1"
readable-stream@~0.0.2: readable-stream@~0.0.2:
version "0.0.4" version "0.0.4"
resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-0.0.4.tgz#f32d76e3fb863344a548d79923007173665b3b8d" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-0.0.4.tgz#f32d76e3fb863344a548d79923007173665b3b8d"
@ -4077,6 +4246,14 @@ resolve-cwd@^3.0.0:
dependencies: dependencies:
resolve-from "^5.0.0" resolve-from "^5.0.0"
resolve-dir@^0.1.0:
version "0.1.1"
resolved "https://registry.yarnpkg.com/resolve-dir/-/resolve-dir-0.1.1.tgz#b219259a5602fac5c5c496ad894a6e8cc430261e"
integrity sha512-QxMPqI6le2u0dCLyiGzgy92kjkkL6zO0XyvHzjdTNH3zM6e5Hz3BwG6+aEyNgiQ5Xz6PwTwgQEj3U50dByPKIA==
dependencies:
expand-tilde "^1.2.2"
global-modules "^0.2.3"
resolve-from@^5.0.0: resolve-from@^5.0.0:
version "5.0.0" version "5.0.0"
resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-5.0.0.tgz#c35225843df8f776df21c57557bc087e9dfdfc69" resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-5.0.0.tgz#c35225843df8f776df21c57557bc087e9dfdfc69"
@ -4238,6 +4415,11 @@ spark-md5@3.0.2:
resolved "https://registry.yarnpkg.com/spark-md5/-/spark-md5-3.0.2.tgz#7952c4a30784347abcee73268e473b9c0167e3fc" resolved "https://registry.yarnpkg.com/spark-md5/-/spark-md5-3.0.2.tgz#7952c4a30784347abcee73268e473b9c0167e3fc"
integrity sha512-wcFzz9cDfbuqe0FZzfi2or1sgyIrsDwmPwfZC4hiNidPdPINjeUwNfv5kldczoEAcjl9Y1L3SM7Uz2PUEQzxQw== integrity sha512-wcFzz9cDfbuqe0FZzfi2or1sgyIrsDwmPwfZC4hiNidPdPINjeUwNfv5kldczoEAcjl9Y1L3SM7Uz2PUEQzxQw==
split-ca@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/split-ca/-/split-ca-1.0.1.tgz#6c83aff3692fa61256e0cd197e05e9de157691a6"
integrity sha512-Q5thBSxp5t8WPTTJQS59LrGqOZqOsrhDGDVm8azCqIBjSBd7nd9o2PM+mDulQQkh8h//4U6hFZnc/mul8t5pWQ==
split2@^2.1.0: split2@^2.1.0:
version "2.2.0" version "2.2.0"
resolved "https://registry.yarnpkg.com/split2/-/split2-2.2.0.tgz#186b2575bcf83e85b7d18465756238ee4ee42493" resolved "https://registry.yarnpkg.com/split2/-/split2-2.2.0.tgz#186b2575bcf83e85b7d18465756238ee4ee42493"
@ -4257,6 +4439,17 @@ sprintf-js@~1.0.2:
resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c"
integrity sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g== integrity sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==
ssh2@^1.11.0:
version "1.13.0"
resolved "https://registry.yarnpkg.com/ssh2/-/ssh2-1.13.0.tgz#9b53a07534fa72283ada471b82395a3b3c875934"
integrity sha512-CIZBFRRY1y9mAZSqBGFE4EB4dNJad2ysT2PqO8OpkiI3UTB/gUZwE5EaN16qVyQ6s/M7EgC/iaV/MnjdlvnuzA==
dependencies:
asn1 "^0.2.6"
bcrypt-pbkdf "^1.0.2"
optionalDependencies:
cpu-features "~0.0.7"
nan "^2.17.0"
sshpk@^1.7.0: sshpk@^1.7.0:
version "1.17.0" version "1.17.0"
resolved "https://registry.yarnpkg.com/sshpk/-/sshpk-1.17.0.tgz#578082d92d4fe612b13007496e543fa0fbcbe4c5" resolved "https://registry.yarnpkg.com/sshpk/-/sshpk-1.17.0.tgz#578082d92d4fe612b13007496e543fa0fbcbe4c5"
@ -4314,6 +4507,13 @@ stream-combiner@~0.0.4:
dependencies: dependencies:
duplexer "~0.1.1" duplexer "~0.1.1"
stream-to-array@^2.3.0:
version "2.3.0"
resolved "https://registry.yarnpkg.com/stream-to-array/-/stream-to-array-2.3.0.tgz#bbf6b39f5f43ec30bc71babcb37557acecf34353"
integrity sha512-UsZtOYEn4tWU2RGLOXr/o/xjRBftZRlG3dEWoaHr8j4GuypJ3isitGbVyjQKAuMu+xbiop8q224TjiZWc4XTZA==
dependencies:
any-promise "^1.1.0"
string-length@^4.0.1: string-length@^4.0.1:
version "4.0.2" version "4.0.2"
resolved "https://registry.yarnpkg.com/string-length/-/string-length-4.0.2.tgz#a8a8dc7bd5c1a82b9b3c8b87e125f66871b6e57a" resolved "https://registry.yarnpkg.com/string-length/-/string-length-4.0.2.tgz#a8a8dc7bd5c1a82b9b3c8b87e125f66871b6e57a"
@ -4403,7 +4603,7 @@ supports-preserve-symlinks-flag@^1.0.0:
resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09" resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09"
integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w== integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==
tar-fs@2.1.1: tar-fs@2.1.1, tar-fs@^2.1.0:
version "2.1.1" version "2.1.1"
resolved "https://registry.yarnpkg.com/tar-fs/-/tar-fs-2.1.1.tgz#489a15ab85f1f0befabb370b7de4f9eb5cbe8784" resolved "https://registry.yarnpkg.com/tar-fs/-/tar-fs-2.1.1.tgz#489a15ab85f1f0befabb370b7de4f9eb5cbe8784"
integrity sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng== integrity sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng==
@ -4413,7 +4613,17 @@ tar-fs@2.1.1:
pump "^3.0.0" pump "^3.0.0"
tar-stream "^2.1.4" tar-stream "^2.1.4"
tar-stream@^2.1.4: tar-fs@~2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/tar-fs/-/tar-fs-2.0.1.tgz#e44086c1c60d31a4f0cf893b1c4e155dabfae9e2"
integrity sha512-6tzWDMeroL87uF/+lin46k+Q+46rAJ0SyPGz7OW7wTgblI273hsBqk2C1j0/xNadNLKDTUL9BukSjB7cwgmlPA==
dependencies:
chownr "^1.1.1"
mkdirp-classic "^0.5.2"
pump "^3.0.0"
tar-stream "^2.0.0"
tar-stream@^2.0.0, tar-stream@^2.1.4:
version "2.2.0" version "2.2.0"
resolved "https://registry.yarnpkg.com/tar-stream/-/tar-stream-2.2.0.tgz#acad84c284136b060dc3faa64474aa9aebd77287" resolved "https://registry.yarnpkg.com/tar-stream/-/tar-stream-2.2.0.tgz#acad84c284136b060dc3faa64474aa9aebd77287"
integrity sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ== integrity sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==
@ -4445,6 +4655,23 @@ test-exclude@^6.0.0:
glob "^7.1.4" glob "^7.1.4"
minimatch "^3.0.4" minimatch "^3.0.4"
testcontainers@4.7.0:
version "4.7.0"
resolved "https://registry.yarnpkg.com/testcontainers/-/testcontainers-4.7.0.tgz#5a9a864b1b0cc86984086dcc737c2f5e73490cf3"
integrity sha512-5SrG9RMfDRRZig34fDZeMcGD5i3lHCOJzn0kjouyK4TiEWjZB3h7kCk8524lwNRHROFE1j6DGjceonv/5hl5ag==
dependencies:
"@types/dockerode" "^2.5.34"
byline "^5.0.0"
debug "^4.1.1"
docker-compose "^0.23.5"
dockerode "^3.2.1"
get-port "^5.1.1"
glob "^7.1.6"
node-duration "^1.0.4"
slash "^3.0.0"
stream-to-array "^2.3.0"
tar-fs "^2.1.0"
through2@3.0.2: through2@3.0.2:
version "3.0.2" version "3.0.2"
resolved "https://registry.yarnpkg.com/through2/-/through2-3.0.2.tgz#99f88931cfc761ec7678b41d5d7336b5b6a07bf4" resolved "https://registry.yarnpkg.com/through2/-/through2-3.0.2.tgz#99f88931cfc761ec7678b41d5d7336b5b6a07bf4"
@ -4746,6 +4973,13 @@ whatwg-url@^5.0.0:
tr46 "~0.0.3" tr46 "~0.0.3"
webidl-conversions "^3.0.0" webidl-conversions "^3.0.0"
which@^1.2.12:
version "1.3.1"
resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a"
integrity sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==
dependencies:
isexe "^2.0.0"
which@^2.0.1: which@^2.0.1:
version "2.0.2" version "2.0.2"
resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1" resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1"
@ -4824,6 +5058,11 @@ yallist@^4.0.0:
resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72" resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72"
integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A== integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==
yaml@^1.10.2:
version "1.10.2"
resolved "https://registry.yarnpkg.com/yaml/-/yaml-1.10.2.tgz#2301c5ffbf12b467de8da2333a459e29e7920e4b"
integrity sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==
yargs-parser@^21.0.0, yargs-parser@^21.0.1: yargs-parser@^21.0.0, yargs-parser@^21.0.1:
version "21.1.1" version "21.1.1"
resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-21.1.1.tgz#9096bceebf990d21bb31fa9516e0ede294a77d35" resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-21.1.1.tgz#9096bceebf990d21bb31fa9516e0ede294a77d35"

View File

@ -5211,6 +5211,15 @@
resolved "https://registry.yarnpkg.com/@types/parse-json/-/parse-json-4.0.0.tgz#2f8bb441434d163b35fb8ffdccd7138927ffb8c0" resolved "https://registry.yarnpkg.com/@types/parse-json/-/parse-json-4.0.0.tgz#2f8bb441434d163b35fb8ffdccd7138927ffb8c0"
integrity sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA== integrity sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA==
"@types/pg@8.6.6":
version "8.6.6"
resolved "https://registry.yarnpkg.com/@types/pg/-/pg-8.6.6.tgz#21cdf873a3e345a6e78f394677e3b3b1b543cb80"
integrity sha512-O2xNmXebtwVekJDD+02udOncjVcMZQuTEQEMpKJ0ZRf5E7/9JJX3izhKUcUifBkyKpljyUM6BTgy2trmviKlpw==
dependencies:
"@types/node" "*"
pg-protocol "*"
pg-types "^2.2.0"
"@types/pouchdb-adapter-cordova-sqlite@*": "@types/pouchdb-adapter-cordova-sqlite@*":
version "1.0.1" version "1.0.1"
resolved "https://registry.yarnpkg.com/@types/pouchdb-adapter-cordova-sqlite/-/pouchdb-adapter-cordova-sqlite-1.0.1.tgz#49e5ee6df7cc0c23196fcb340f43a560e74eb1d6" resolved "https://registry.yarnpkg.com/@types/pouchdb-adapter-cordova-sqlite/-/pouchdb-adapter-cordova-sqlite-1.0.1.tgz#49e5ee6df7cc0c23196fcb340f43a560e74eb1d6"
@ -19085,7 +19094,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 sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow== integrity sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==
pg-connection-string@2.5.0, pg-connection-string@^2.4.0: pg-connection-string@2.5.0, pg-connection-string@^2.5.0:
version "2.5.0" version "2.5.0"
resolved "https://registry.yarnpkg.com/pg-connection-string/-/pg-connection-string-2.5.0.tgz#538cadd0f7e603fc09a12590f3b8a452c2c0cf34" resolved "https://registry.yarnpkg.com/pg-connection-string/-/pg-connection-string-2.5.0.tgz#538cadd0f7e603fc09a12590f3b8a452c2c0cf34"
integrity sha512-r5o/V/ORTA6TmUnyWZR9nCj1klXCO2CEKNRlVuJptZe85QuhFayC7WeMic7ndayT5IRIR0S0xFxFi2ousartlQ== integrity sha512-r5o/V/ORTA6TmUnyWZR9nCj1klXCO2CEKNRlVuJptZe85QuhFayC7WeMic7ndayT5IRIR0S0xFxFi2ousartlQ==
@ -19095,17 +19104,17 @@ pg-int8@1.0.1:
resolved "https://registry.yarnpkg.com/pg-int8/-/pg-int8-1.0.1.tgz#943bd463bf5b71b4170115f80f8efc9a0c0eb78c" resolved "https://registry.yarnpkg.com/pg-int8/-/pg-int8-1.0.1.tgz#943bd463bf5b71b4170115f80f8efc9a0c0eb78c"
integrity sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw== integrity sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==
pg-pool@^3.2.2: pg-pool@^3.6.0:
version "3.6.0" version "3.6.0"
resolved "https://registry.yarnpkg.com/pg-pool/-/pg-pool-3.6.0.tgz#3190df3e4747a0d23e5e9e8045bcd99bda0a712e" resolved "https://registry.yarnpkg.com/pg-pool/-/pg-pool-3.6.0.tgz#3190df3e4747a0d23e5e9e8045bcd99bda0a712e"
integrity sha512-clFRf2ksqd+F497kWFyM21tMjeikn60oGDmqMT8UBrynEwVEX/5R5xd2sdvdo1cZCFlguORNpVuqxIj+aK4cfQ== integrity sha512-clFRf2ksqd+F497kWFyM21tMjeikn60oGDmqMT8UBrynEwVEX/5R5xd2sdvdo1cZCFlguORNpVuqxIj+aK4cfQ==
pg-protocol@^1.4.0: pg-protocol@*, pg-protocol@^1.6.0:
version "1.6.0" version "1.6.0"
resolved "https://registry.yarnpkg.com/pg-protocol/-/pg-protocol-1.6.0.tgz#4c91613c0315349363af2084608db843502f8833" resolved "https://registry.yarnpkg.com/pg-protocol/-/pg-protocol-1.6.0.tgz#4c91613c0315349363af2084608db843502f8833"
integrity sha512-M+PDm637OY5WM307051+bsDia5Xej6d9IR4GwJse1qA1DIhiKlksvrneZOYQq42OM+spubpcNYEo2FcKQrDk+Q== integrity sha512-M+PDm637OY5WM307051+bsDia5Xej6d9IR4GwJse1qA1DIhiKlksvrneZOYQq42OM+spubpcNYEo2FcKQrDk+Q==
pg-types@^2.1.0: pg-types@^2.1.0, pg-types@^2.2.0:
version "2.2.0" version "2.2.0"
resolved "https://registry.yarnpkg.com/pg-types/-/pg-types-2.2.0.tgz#2d0250d636454f7cfa3b6ae0382fdfa8063254a3" resolved "https://registry.yarnpkg.com/pg-types/-/pg-types-2.2.0.tgz#2d0250d636454f7cfa3b6ae0382fdfa8063254a3"
integrity sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA== integrity sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==
@ -19116,16 +19125,16 @@ pg-types@^2.1.0:
postgres-date "~1.0.4" postgres-date "~1.0.4"
postgres-interval "^1.1.0" postgres-interval "^1.1.0"
pg@8.5.1: pg@8.10.0:
version "8.5.1" version "8.10.0"
resolved "https://registry.yarnpkg.com/pg/-/pg-8.5.1.tgz#34dcb15f6db4a29c702bf5031ef2e1e25a06a120" resolved "https://registry.yarnpkg.com/pg/-/pg-8.10.0.tgz#5b8379c9b4a36451d110fc8cd98fc325fe62ad24"
integrity sha512-9wm3yX9lCfjvA98ybCyw2pADUivyNWT/yIP4ZcDVpMN0og70BUWYEGXPCTAQdGTAqnytfRADb7NERrY1qxhIqw== integrity sha512-ke7o7qSTMb47iwzOSaZMfeR7xToFdkE71ifIipOAAaLIM0DYzfOAXlgFFmYUIE2BcJtvnVlGCID84ZzCegE8CQ==
dependencies: dependencies:
buffer-writer "2.0.0" buffer-writer "2.0.0"
packet-reader "1.0.0" packet-reader "1.0.0"
pg-connection-string "^2.4.0" pg-connection-string "^2.5.0"
pg-pool "^3.2.2" pg-pool "^3.6.0"
pg-protocol "^1.4.0" pg-protocol "^1.6.0"
pg-types "^2.1.0" pg-types "^2.1.0"
pgpass "1.x" pgpass "1.x"
@ -22951,7 +22960,14 @@ svelte-dnd-action@^0.9.8:
resolved "https://registry.yarnpkg.com/svelte-dnd-action/-/svelte-dnd-action-0.9.22.tgz#003eee9dddb31d8c782f6832aec8b1507fff194d" resolved "https://registry.yarnpkg.com/svelte-dnd-action/-/svelte-dnd-action-0.9.22.tgz#003eee9dddb31d8c782f6832aec8b1507fff194d"
integrity sha512-lOQJsNLM1QWv5mdxIkCVtk6k4lHCtLgfE59y8rs7iOM6erchbLC9hMEFYSveZz7biJV0mpg7yDSs4bj/RT/YkA== integrity sha512-lOQJsNLM1QWv5mdxIkCVtk6k4lHCtLgfE59y8rs7iOM6erchbLC9hMEFYSveZz7biJV0mpg7yDSs4bj/RT/YkA==
svelte-flatpickr@^3.1.0, svelte-flatpickr@^3.2.3, svelte-flatpickr@^3.3.2: svelte-flatpickr@3.2.3:
version "3.2.3"
resolved "https://registry.yarnpkg.com/svelte-flatpickr/-/svelte-flatpickr-3.2.3.tgz#db5dd7ad832ef83262b45e09737955ad3d591fc8"
integrity sha512-PNkqK4Napx8nTvCwkaUXdnKo8dISThaxEOK+szTUXcY6H0dQM0TSyuoMaVWY2yX7pM+PN5cpCQCcVe8YvTRFSw==
dependencies:
flatpickr "^4.5.2"
svelte-flatpickr@^3.1.0, svelte-flatpickr@^3.2.3:
version "3.3.2" version "3.3.2"
resolved "https://registry.yarnpkg.com/svelte-flatpickr/-/svelte-flatpickr-3.3.2.tgz#f08bcde83d439cb30df6fd07b974d87371f130c1" resolved "https://registry.yarnpkg.com/svelte-flatpickr/-/svelte-flatpickr-3.3.2.tgz#f08bcde83d439cb30df6fd07b974d87371f130c1"
integrity sha512-VNJLYyLRDplI63oWX5hJylzAJc2VhTh3z9SNecfjtuPZmP6FZPpg9Fw7rXpkEV2DPovIWj2PtaVxB6Kp9r423w== integrity sha512-VNJLYyLRDplI63oWX5hJylzAJc2VhTh3z9SNecfjtuPZmP6FZPpg9Fw7rXpkEV2DPovIWj2PtaVxB6Kp9r423w==