Merge branch 'develop' of github.com:Budibase/budibase into data-section-multidev

This commit is contained in:
Andrew Kingston 2023-05-17 16:38:01 +01:00
commit 0bebacc741
68 changed files with 1655 additions and 277 deletions

View File

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

View File

@ -12,7 +12,7 @@ import {
isDocument,
} from "@budibase/types"
import { getCouchInfo } from "./connections"
import { directCouchCall } from "./utils"
import { directCouchUrlCall } from "./utils"
import { getPouchDB } from "./pouchDB"
import { WriteStream, ReadStream } from "fs"
import { newid } from "../../docIds/newid"
@ -46,6 +46,8 @@ export class DatabaseImpl implements Database {
private readonly instanceNano?: Nano.ServerScope
private readonly pouchOpts: DatabaseOpts
private readonly couchInfo = getCouchInfo()
constructor(dbName?: string, opts?: DatabaseOpts, connection?: string) {
if (dbName == null) {
throw new Error("Database name cannot be undefined.")
@ -53,8 +55,8 @@ export class DatabaseImpl implements Database {
this.name = dbName
this.pouchOpts = opts || {}
if (connection) {
const couchInfo = getCouchInfo(connection)
this.instanceNano = buildNano(couchInfo)
this.couchInfo = getCouchInfo(connection)
this.instanceNano = buildNano(this.couchInfo)
}
if (!DatabaseImpl.nano) {
DatabaseImpl.init()
@ -67,7 +69,11 @@ export class DatabaseImpl implements Database {
}
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
}

View File

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

View File

@ -9,6 +9,20 @@ export async function directCouchCall(
) {
let { url, cookie } = getCouchInfo()
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 = {
method: method,
headers: {
@ -19,7 +33,7 @@ export async function directCouchCall(
params.body = JSON.stringify(body)
params.headers["Content-Type"] = "application/json"
}
return await fetch(checkSlashesInUrl(encodeURI(couchUrl)), params)
return await fetch(checkSlashesInUrl(encodeURI(url)), params)
}
export async function directCouchQuery(

View File

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

View File

@ -2,6 +2,7 @@
import "@spectrum-css/button/dist/index-vars.css"
import Tooltip from "../Tooltip/Tooltip.svelte"
export let type
export let disabled = false
export let size = "M"
export let cta = false
@ -21,6 +22,7 @@
<button
{id}
{type}
class:spectrum-Button--cta={cta}
class:spectrum-Button--primary={primary}
class:spectrum-Button--secondary={secondary}
@ -73,6 +75,7 @@
button {
position: relative;
}
.spectrum-Button-label {
white-space: nowrap;
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>
import Icon from "../Icon/Icon.svelte"
import { getContext, onMount } from "svelte"
import { slide } from "svelte/transition"
import ErrorMessage from "./ErrorMessage.svelte"
export let disabled = false
export let error = null
@ -55,9 +55,7 @@
{/if}
</div>
{#if error}
<div transition:slide|local={{ duration: 130 }} class="error-message">
{error}
</div>
<ErrorMessage {error} />
{/if}
</div>
@ -110,13 +108,6 @@
.field {
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 {
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 FancyForm } from "./FancyForm.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/shared-core": "0.0.1",
"@budibase/string-templates": "0.0.1",
"@budibase/types": "0.0.1",
"@fortawesome/fontawesome-svg-core": "^6.2.1",
"@fortawesome/free-brands-svg-icons": "^6.2.1",
"@fortawesome/free-solid-svg-icons": "^6.2.1",

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -268,8 +268,6 @@
notifications.error("Error fetching user group data")
}
})
let staticUserLimit = $licensing.license.quotas.usage.static.users.value
</script>
<Layout noPadding gap="M">
@ -278,7 +276,7 @@
<Body>Add users and control who gets access to your published apps</Body>
</Layout>
<Divider />
{#if $licensing.warnUserLimit}
{#if $licensing.errUserLimit}
<InlineAlert
type="error"
onConfirm={() => {
@ -290,13 +288,9 @@
}}
buttonText={isOwner ? "Upgrade" : "View plans"}
cta
header={`Users will soon be limited to ${staticUserLimit}`}
message={`Our free plan is going to be limited to ${staticUserLimit} users in ${$licensing.userLimitDays}.
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.
`}
header="Account de-activated"
message="Due to the free plan user limit being exceeded, your account has been de-activated.
Upgrade your plan to re-activate your account."
/>
{/if}
<div class="controls">

View File

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

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>
import SplitPage from "./SplitPage.svelte"
import { Layout } from "@budibase/bbui"
import Bulgaria from "../../assets/bulgaria.png"
import Covanta from "../../assets/covanta.png"
import Schnellecke from "../../assets/schnellecke.png"
import Testimonial from "./Testimonial.svelte"
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>
<SplitPage>
<slot />
<div class:wrapper={enabled} slot="right">
{#if enabled}
<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>
<Testimonial />
{/if}
</div>
</SplitPage>
@ -64,20 +21,4 @@
display: grid;
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>

View File

@ -13,10 +13,10 @@
let flatpickr
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
$: time = new Date(`0-${value}`)
$: timeOnly = !isNaN(time) || schema?.timeOnly
$: isTimeValue = !isNaN(new Date(`0-${value}`))
$: timeOnly = isTimeValue || schema?.timeOnly
$: dateOnly = schema?.dateOnly
$: format = timeOnly
? "HH:mm:ss"
@ -24,6 +24,19 @@
? "MMM D YYYY"
: "MMM D YYYY, HH:mm"
$: 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
$: {
@ -49,7 +62,7 @@
<div class="container">
<div class="value">
{#if value}
{dayjs(timeOnly ? time : value).format(format)}
{displayValue}
{/if}
</div>
{#if editable}

View File

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

View File

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

View File

@ -18,11 +18,71 @@ import {
Row,
CreateDatasourceResponse,
UpdateDatasourceResponse,
UpdateDatasourceRequest,
CreateDatasourceRequest,
VerifyDatasourceRequest,
VerifyDatasourceResponse,
IntegrationBase,
DatasourcePlus,
} from "@budibase/types"
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) {
// Get internal tables
const db = context.getAppDB()
@ -66,6 +126,33 @@ export async function fetch(ctx: UserCtx) {
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) {
const db = context.getAppDB()
const datasource = await sdk.datasources.get(ctx.params.datasourceId)
@ -311,51 +398,3 @@ export async function query(ctx: UserCtx) {
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"
export async function fetch(ctx: BBContext) {
@ -7,7 +7,7 @@ export async function fetch(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.body = defs[ctx.params.type]
}

View File

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

View File

@ -140,7 +140,7 @@ export function init(endpoint: string) {
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_SECRET_ACCESS_KEY", "SECRET_KEY")
init("http://localhost:8333")

View File

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

View File

@ -1,11 +1,13 @@
import {
Integration,
ConnectionInfo,
DatasourceFeature,
DatasourceFieldType,
QueryType,
Integration,
IntegrationBase,
QueryType,
} from "@budibase/types"
const Airtable = require("airtable")
import Airtable from "airtable"
interface AirtableConfig {
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.",
friendlyName: "Airtable",
type: "Spreadsheet",
features: [DatasourceFeature.CONNECTION_CHECKING],
datasource: {
apiKey: {
type: DatasourceFieldType.PASSWORD,
@ -81,13 +84,37 @@ const SCHEMA: Integration = {
class AirtableIntegration implements IntegrationBase {
private config: AirtableConfig
private client: any
private client
constructor(config: AirtableConfig) {
this.config = config
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 }) {
const { table, json } = query

View File

@ -3,9 +3,11 @@ import {
DatasourceFieldType,
QueryType,
IntegrationBase,
DatasourceFeature,
ConnectionInfo,
} from "@budibase/types"
const { Database, aql } = require("arangojs")
import { Database, aql } from "arangojs"
interface ArangodbConfig {
url: string
@ -21,6 +23,7 @@ const SCHEMA: Integration = {
type: "Non-relational",
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. ",
features: [DatasourceFeature.CONNECTION_CHECKING],
datasource: {
url: {
type: DatasourceFieldType.STRING,
@ -58,7 +61,7 @@ const SCHEMA: Integration = {
class ArangoDBIntegration implements IntegrationBase {
private config: ArangodbConfig
private client: any
private client
constructor(config: ArangodbConfig) {
const newConfig = {
@ -74,6 +77,19 @@ class ArangoDBIntegration implements IntegrationBase {
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 }) {
try {
const result = await this.client.query(query.sql)

View File

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

View File

@ -3,10 +3,13 @@ import {
DatasourceFieldType,
QueryType,
IntegrationBase,
DatasourceFeature,
ConnectionInfo,
} from "@budibase/types"
import AWS from "aws-sdk"
import { AWS_REGION } from "../db/dynamoClient"
import { DocumentClient } from "aws-sdk/clients/dynamodb"
interface DynamoDBConfig {
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.",
friendlyName: "DynamoDB",
type: "Non-relational",
features: [DatasourceFeature.CONNECTION_CHECKING],
datasource: {
region: {
type: DatasourceFieldType.STRING,
@ -128,7 +132,7 @@ const SCHEMA: Integration = {
class DynamoDBIntegration implements IntegrationBase {
private config: DynamoDBConfig
private client: any
private client
constructor(config: DynamoDBConfig) {
this.config = config
@ -148,7 +152,23 @@ class DynamoDBIntegration implements IntegrationBase {
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 = {
TableName: query.table,
...query.json,
@ -189,7 +209,10 @@ class DynamoDBIntegration implements IntegrationBase {
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 = {
TableName: query.table,
...query.json,
@ -197,7 +220,10 @@ class DynamoDBIntegration implements IntegrationBase {
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 = {
TableName: query.table,
...query.json,
@ -205,7 +231,10 @@ class DynamoDBIntegration implements IntegrationBase {
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 = {
TableName: query.table,
...query.json,

View File

@ -3,6 +3,8 @@ import {
DatasourceFieldType,
QueryType,
IntegrationBase,
DatasourceFeature,
ConnectionInfo,
} from "@budibase/types"
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.",
friendlyName: "ElasticSearch",
type: "Non-relational",
features: [DatasourceFeature.CONNECTION_CHECKING],
datasource: {
url: {
type: DatasourceFieldType.STRING,
@ -95,7 +98,7 @@ const SCHEMA: Integration = {
class ElasticSearchIntegration implements IntegrationBase {
private config: ElasticsearchConfig
private client: any
private client
constructor(config: ElasticsearchConfig) {
this.config = config
@ -114,6 +117,18 @@ class ElasticSearchIntegration implements IntegrationBase {
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 }) {
const { index, json } = query

View File

@ -3,6 +3,8 @@ import {
Integration,
QueryType,
IntegrationBase,
DatasourceFeature,
ConnectionInfo,
} from "@budibase/types"
import { Firestore, WhereFilterOp } from "@google-cloud/firestore"
@ -18,6 +20,7 @@ const SCHEMA: Integration = {
type: "Non-relational",
description:
"Cloud Firestore is a flexible, scalable database for mobile, web, and server development from Firebase and Google Cloud.",
features: [DatasourceFeature.CONNECTION_CHECKING],
datasource: {
email: {
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 } }) {
try {
const documentReference = this.client

View File

@ -1,4 +1,6 @@
import {
ConnectionInfo,
DatasourceFeature,
DatasourceFieldType,
DatasourcePlus,
FieldType,
@ -64,6 +66,7 @@ const SCHEMA: Integration = {
"Create and collaborate on online spreadsheets in real-time and from any device. ",
friendlyName: "Google Sheets",
type: "Spreadsheet",
features: [DatasourceFeature.CONNECTION_CHECKING],
datasource: {
spreadsheetId: {
display: "Google Sheet URL",
@ -139,6 +142,19 @@ class GoogleSheetsIntegration implements DatasourcePlus {
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() {
return ""
}

View File

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

View File

@ -8,6 +8,8 @@ import {
QueryType,
SqlQuery,
DatasourcePlus,
DatasourceFeature,
ConnectionInfo,
} from "@budibase/types"
import {
getSqlQuery,
@ -39,6 +41,7 @@ const SCHEMA: Integration = {
"Microsoft SQL Server is a relational database management system developed by Microsoft. ",
friendlyName: "MS SQL Server",
type: "Relational",
features: [DatasourceFeature.CONNECTION_CHECKING],
datasource: {
user: {
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 {
return `@p${this.index++}`
}

View File

@ -3,6 +3,8 @@ import {
DatasourceFieldType,
QueryType,
IntegrationBase,
DatasourceFeature,
ConnectionInfo,
} from "@budibase/types"
import {
MongoClient,
@ -38,6 +40,7 @@ const getSchema = () => {
type: "Non-relational",
description:
"MongoDB is a general purpose, document-based, distributed database built for modern application developers and for the cloud era.",
features: [DatasourceFeature.CONNECTION_CHECKING],
datasource: {
connectionString: {
type: DatasourceFieldType.STRING,
@ -358,6 +361,19 @@ class MongoIntegration implements IntegrationBase {
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() {
return this.client.connect()
}

View File

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

View File

@ -7,6 +7,8 @@ import {
SqlQuery,
Table,
DatasourcePlus,
DatasourceFeature,
ConnectionInfo,
} from "@budibase/types"
import {
buildExternalTableId,
@ -24,12 +26,7 @@ import {
ExecuteOptions,
Result,
} from "oracledb"
import {
OracleTable,
OracleColumn,
OracleColumnsResponse,
OracleConstraint,
} from "./base/types"
import { OracleTable, OracleColumn, OracleColumnsResponse } from "./base/types"
let oracledb: any
try {
oracledb = require("oracledb")
@ -53,6 +50,7 @@ const SCHEMA: Integration = {
type: "Relational",
description:
"Oracle Database is an object-relational database management system developed by Oracle Corporation",
features: [DatasourceFeature.CONNECTION_CHECKING],
datasource: {
host: {
type: DatasourceFieldType.STRING,
@ -325,6 +323,30 @@ class OracleIntegration extends Sql implements DatasourcePlus {
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>> {
let connection
try {

View File

@ -6,6 +6,8 @@ import {
SqlQuery,
Table,
DatasourcePlus,
DatasourceFeature,
ConnectionInfo,
} from "@budibase/types"
import {
getSqlQuery,
@ -18,7 +20,7 @@ import Sql from "./base/sql"
import { PostgresColumn } from "./base/types"
import { escapeDangerousCharacters } from "../utilities"
const { Client, types } = require("pg")
import { Client, types } from "pg"
// Return "date" and "timestamp" types as plain strings.
// This lets us reference the original stored timezone.
@ -50,6 +52,7 @@ const SCHEMA: Integration = {
type: "Relational",
description:
"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: {
host: {
type: DatasourceFieldType.STRING,
@ -114,7 +117,7 @@ const SCHEMA: Integration = {
}
class PostgresIntegration extends Sql implements DatasourcePlus {
private readonly client: any
private readonly client: Client
private readonly config: PostgresConfig
private index: number = 1
private open: boolean
@ -150,6 +153,21 @@ class PostgresIntegration extends Sql implements DatasourcePlus {
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 {
return `$${this.index++}`
}
@ -163,7 +181,7 @@ class PostgresIntegration extends Sql implements DatasourcePlus {
if (!this.config.schema) {
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.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"
interface RedisConfig {
@ -11,9 +17,11 @@ interface RedisConfig {
const SCHEMA: Integration = {
docs: "https://redis.io/docs/",
description: "",
description:
"Redis is a caching tool, providing powerful key-value store capabilities.",
friendlyName: "Redis",
type: "Non-relational",
features: [DatasourceFeature.CONNECTION_CHECKING],
datasource: {
host: {
type: "string",
@ -86,7 +94,7 @@ const SCHEMA: Integration = {
class RedisIntegration {
private readonly config: RedisConfig
private client: any
private client
constructor(config: RedisConfig) {
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() {
return this.client.quit()
}

View File

@ -3,10 +3,12 @@ import {
QueryType,
IntegrationBase,
DatasourceFieldType,
DatasourceFeature,
ConnectionInfo,
} from "@budibase/types"
const AWS = require("aws-sdk")
const csv = require("csvtojson")
import AWS from "aws-sdk"
import csv from "csvtojson"
interface S3Config {
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.",
friendlyName: "Amazon S3",
type: "Object store",
features: [DatasourceFeature.CONNECTION_CHECKING],
datasource: {
region: {
type: "string",
@ -152,7 +155,7 @@ const SCHEMA: Integration = {
class S3Integration implements IntegrationBase {
private readonly config: S3Config
private client: any
private client
constructor(config: S3Config) {
this.config = config
@ -165,6 +168,19 @@ class S3Integration implements IntegrationBase {
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: {
bucket: 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"
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.",
friendlyName: "Snowflake",
type: "Relational",
features: [DatasourceFeature.CONNECTION_CHECKING],
datasource: {
account: {
type: "string",
@ -65,6 +72,18 @@ class SnowflakeIntegration {
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) {
await this.client.connect()
try {

View File

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

View File

@ -13,6 +13,7 @@ import {
import { cloneDeep } from "lodash/fp"
import { getEnvironmentVariables } from "../../utils"
import { getDefinitions, getDefinition } from "../../../integrations"
import _ from "lodash"
const ENV_VAR_PREFIX = "env."
@ -41,7 +42,7 @@ async function enrichDatasourceWithValues(datasource: Datasource) {
{ onlyFound: true }
) as Datasource
const definition = await getDefinition(processed.source)
processed.config = checkDatasourceTypes(definition, processed.config)
processed.config = checkDatasourceTypes(definition!, processed.config)
return {
datasource: processed,
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
for (let [key, value] of Object.entries(update.config)) {
if (value !== PASSWORD_REPLACEMENT) {

View File

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

View File

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

View File

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

View File

@ -14,12 +14,14 @@
"test:watch": "yarn run test --watch",
"test:debug": "DEBUG=1 yarn run test",
"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",
"serve": "start-server-and-test dev:built http://localhost:4001/health",
"dev:built": "cd ../ && yarn dev:built"
},
"devDependencies": {
"@budibase/types": "^2.3.17",
"@trendyol/jest-testcontainers": "^2.1.1",
"@types/jest": "29.0.0",
"@types/node-fetch": "2.6.2",
"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()

View File

@ -304,6 +304,11 @@
"@babel/helper-validator-identifier" "^7.18.6"
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":
version "0.2.3"
resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39"
@ -779,6 +784,15 @@
request "^2.88.0"
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":
version "1.0.9"
resolved "https://registry.yarnpkg.com/@tsconfig/node10/-/node10-1.0.9.tgz#df4907fc07a886922637b15e02d4cebc4c0021b2"
@ -832,6 +846,13 @@
dependencies:
"@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":
version "4.1.5"
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"
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:
version "3.1.2"
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"
integrity sha512-u96dg2GcAKtpTrBdDoFIM7PjcBA+6rSP0OR94MOReNRyUECL6MtQt5XXmRr4qrftYaef9+l5hcpO5te7sML1Cg==
asn1@~0.2.3:
asn1@^0.2.6, asn1@~0.2.3:
version "0.2.6"
resolved "https://registry.yarnpkg.com/asn1/-/asn1-0.2.6.tgz#0d3a7bb6e64e02a90c0303b31f292868ea09a08d"
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"
integrity sha512-ir1UPr3dkwexU7FdV8qBBbNDRUhMmIekYMFZfi+C/sLNnRESKPl23nB9b2pltqfOQNnGzsDdId90AEtG5tCx4A==
bcrypt-pbkdf@^1.0.0:
bcrypt-pbkdf@^1.0.0, bcrypt-pbkdf@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz#a4301d389b6a43f9b67ff3ca11a3f6637e360e9e"
integrity sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==
@ -1304,6 +1330,11 @@ buffer@^5.5.0, buffer@^5.6.0:
base64-js "^1.3.1"
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:
version "4.10.1"
resolved "https://registry.yarnpkg.com/bull/-/bull-4.10.1.tgz#f14974b6089358b62b495a2cbf838aadc098e43f"
@ -1319,6 +1350,11 @@ bull@4.10.1:
semver "^7.3.2"
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:
version "1.0.1"
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:
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:
version "1.1.1"
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"
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:
version "1.14.1"
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"
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:
version "16.0.1"
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"
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:
version "29.0.2"
resolved "https://registry.yarnpkg.com/expect/-/expect-29.0.2.tgz#22c7132400f60444b427211f1d6bb604a9ab2420"
@ -1919,6 +2004,21 @@ fill-range@^7.0.1:
dependencies:
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:
version "4.1.0"
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"
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:
version "2.1.0"
resolved "https://registry.yarnpkg.com/fs-minipass/-/fs-minipass-2.1.0.tgz#7f5036fdbf12c63c169190cbe4199c852271f9fb"
@ -2062,7 +2167,7 @@ getpass@^0.1.1:
dependencies:
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"
resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.3.tgz#b8df0fb802bbfa8e89bd1d938b4e16578ed44f2b"
integrity sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==
@ -2074,6 +2179,24 @@ glob@^7.1.3, glob@^7.1.4:
once "^1.3.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:
version "11.12.0"
resolved "https://registry.yarnpkg.com/globals/-/globals-11.12.0.tgz#ab8795338868a0babd8525758018c2a7eb95c42e"
@ -2131,6 +2254,13 @@ has@^1.0.3:
dependencies:
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:
version "2.0.2"
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"
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:
version "4.28.0"
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"
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:
version "0.0.1"
resolved "https://registry.yarnpkg.com/isarray/-/isarray-0.0.1.tgz#8a18acfca9a8f4177e09abfc6038939b05d1eedf"
@ -3332,6 +3472,11 @@ msgpackr@^1.5.2:
optionalDependencies:
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:
version "2.0.0"
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"
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:
version "2.6.7"
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"
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:
version "1.0.0"
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"
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:
version "1.3.3"
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"
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:
version "0.0.4"
resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-0.0.4.tgz#f32d76e3fb863344a548d79923007173665b3b8d"
@ -4077,6 +4246,14 @@ resolve-cwd@^3.0.0:
dependencies:
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:
version "5.0.0"
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"
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:
version "2.2.0"
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"
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:
version "1.17.0"
resolved "https://registry.yarnpkg.com/sshpk/-/sshpk-1.17.0.tgz#578082d92d4fe612b13007496e543fa0fbcbe4c5"
@ -4314,6 +4507,13 @@ stream-combiner@~0.0.4:
dependencies:
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:
version "4.0.2"
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"
integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==
tar-fs@2.1.1:
tar-fs@2.1.1, tar-fs@^2.1.0:
version "2.1.1"
resolved "https://registry.yarnpkg.com/tar-fs/-/tar-fs-2.1.1.tgz#489a15ab85f1f0befabb370b7de4f9eb5cbe8784"
integrity sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng==
@ -4413,7 +4613,17 @@ tar-fs@2.1.1:
pump "^3.0.0"
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"
resolved "https://registry.yarnpkg.com/tar-stream/-/tar-stream-2.2.0.tgz#acad84c284136b060dc3faa64474aa9aebd77287"
integrity sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==
@ -4445,6 +4655,23 @@ test-exclude@^6.0.0:
glob "^7.1.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:
version "3.0.2"
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"
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:
version "2.0.2"
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"
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:
version "21.1.1"
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"
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@*":
version "1.0.1"
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"
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"
resolved "https://registry.yarnpkg.com/pg-connection-string/-/pg-connection-string-2.5.0.tgz#538cadd0f7e603fc09a12590f3b8a452c2c0cf34"
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"
integrity sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==
pg-pool@^3.2.2:
pg-pool@^3.6.0:
version "3.6.0"
resolved "https://registry.yarnpkg.com/pg-pool/-/pg-pool-3.6.0.tgz#3190df3e4747a0d23e5e9e8045bcd99bda0a712e"
integrity sha512-clFRf2ksqd+F497kWFyM21tMjeikn60oGDmqMT8UBrynEwVEX/5R5xd2sdvdo1cZCFlguORNpVuqxIj+aK4cfQ==
pg-protocol@^1.4.0:
pg-protocol@*, pg-protocol@^1.6.0:
version "1.6.0"
resolved "https://registry.yarnpkg.com/pg-protocol/-/pg-protocol-1.6.0.tgz#4c91613c0315349363af2084608db843502f8833"
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"
resolved "https://registry.yarnpkg.com/pg-types/-/pg-types-2.2.0.tgz#2d0250d636454f7cfa3b6ae0382fdfa8063254a3"
integrity sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==
@ -19116,16 +19125,16 @@ pg-types@^2.1.0:
postgres-date "~1.0.4"
postgres-interval "^1.1.0"
pg@8.5.1:
version "8.5.1"
resolved "https://registry.yarnpkg.com/pg/-/pg-8.5.1.tgz#34dcb15f6db4a29c702bf5031ef2e1e25a06a120"
integrity sha512-9wm3yX9lCfjvA98ybCyw2pADUivyNWT/yIP4ZcDVpMN0og70BUWYEGXPCTAQdGTAqnytfRADb7NERrY1qxhIqw==
pg@8.10.0:
version "8.10.0"
resolved "https://registry.yarnpkg.com/pg/-/pg-8.10.0.tgz#5b8379c9b4a36451d110fc8cd98fc325fe62ad24"
integrity sha512-ke7o7qSTMb47iwzOSaZMfeR7xToFdkE71ifIipOAAaLIM0DYzfOAXlgFFmYUIE2BcJtvnVlGCID84ZzCegE8CQ==
dependencies:
buffer-writer "2.0.0"
packet-reader "1.0.0"
pg-connection-string "^2.4.0"
pg-pool "^3.2.2"
pg-protocol "^1.4.0"
pg-connection-string "^2.5.0"
pg-pool "^3.6.0"
pg-protocol "^1.6.0"
pg-types "^2.1.0"
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"
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"
resolved "https://registry.yarnpkg.com/svelte-flatpickr/-/svelte-flatpickr-3.3.2.tgz#f08bcde83d439cb30df6fd07b974d87371f130c1"
integrity sha512-VNJLYyLRDplI63oWX5hJylzAJc2VhTh3z9SNecfjtuPZmP6FZPpg9Fw7rXpkEV2DPovIWj2PtaVxB6Kp9r423w==