Merge branch 'feature/sql-query-aliasing' of github.com:Budibase/budibase into labday/sqs

This commit is contained in:
mike12345567 2024-02-28 17:03:59 +00:00
commit 2d75e1e85c
72 changed files with 3537 additions and 808 deletions

View File

@ -1,5 +1,5 @@
{
"version": "2.20.10",
"version": "2.20.12",
"npmClient": "yarn",
"packages": [
"packages/*",

View File

@ -6,7 +6,7 @@ import { Plugin } from "@budibase/types"
// URLS
export function enrichPluginURLs(plugins: Plugin[]) {
export function enrichPluginURLs(plugins?: Plugin[]): Plugin[] {
if (!plugins || !plugins.length) {
return []
}

View File

@ -1,9 +1,9 @@
<script>
import { Label, Select, Body, Multiselect } from "@budibase/bbui"
import { findAllMatchingComponents, findComponent } from "helpers/components"
import { selectedScreen } from "stores/builder"
import { Label, Select, Body } from "@budibase/bbui"
import { onMount } from "svelte"
import { getDatasourceForProvider, getSchemaForDatasource } from "dataBinding"
import ColumnEditor from "../../ColumnEditor/ColumnEditor.svelte"
import { findAllMatchingComponents } from "helpers/components"
import { selectedScreen } from "stores/builder"
export let parameters
@ -18,37 +18,65 @@
},
]
const DELIMITERS = [
{
label: ",",
value: ",",
},
{
label: ";",
value: ";",
},
{
label: ":",
value: ":",
},
{
label: "|",
value: "|",
},
{
label: "~",
value: "~",
},
{
label: "[tab]",
value: "\t",
},
{
label: "[space]",
value: " ",
},
]
$: tables = findAllMatchingComponents($selectedScreen?.props, component =>
component._component.endsWith("table")
).map(table => ({
label: table._instanceName,
value: table._id,
}))
)
$: tableBlocks = findAllMatchingComponents(
$selectedScreen?.props,
component => component._component.endsWith("tableblock")
).map(block => ({
label: block._instanceName,
value: `${block._id}-table`,
)
$: components = tables.concat(tableBlocks)
$: componentOptions = components.map(table => ({
label: table._instanceName,
value: table._component.includes("tableblock")
? `${table._id}-table`
: table._id,
}))
$: componentOptions = tables.concat(tableBlocks)
$: columnOptions = getColumnOptions(parameters.tableComponentId)
const getColumnOptions = tableId => {
// Strip block suffix if block component
if (tableId?.includes("-")) {
tableId = tableId.split("-")[0]
}
const selectedTable = findComponent($selectedScreen?.props, tableId)
const datasource = getDatasourceForProvider($selectedScreen, selectedTable)
const { schema } = getSchemaForDatasource($selectedScreen, datasource)
return Object.keys(schema || {})
}
$: selectedTableId = parameters.tableComponentId?.includes("-")
? parameters.tableComponentId.split("-")[0]
: parameters.tableComponentId
$: selectedTable = components.find(
component => component._id === selectedTableId
)
onMount(() => {
if (!parameters.type) {
parameters.type = "csv"
}
if (!parameters.delimiter) {
parameters.delimiter = ","
}
})
</script>
@ -67,13 +95,30 @@
options={componentOptions}
on:change={() => (parameters.columns = [])}
/>
<span />
<Label small>Export as</Label>
<Select bind:value={parameters.type} options={FORMATS} />
<Select
bind:value={parameters.delimiter}
placeholder={null}
options={DELIMITERS}
disabled={parameters.type !== "csv"}
/>
<Label small>Export columns</Label>
<Multiselect
placeholder="All columns"
bind:value={parameters.columns}
options={columnOptions}
<ColumnEditor
value={parameters.columns}
allowCellEditing={false}
componentInstance={selectedTable}
on:change={e => {
const columns = e.detail
parameters.columns = columns
parameters.customHeaders = columns.reduce((headerMap, column) => {
return {
[column.name]: column.displayName,
...headerMap,
}
}, {})
}}
/>
</div>
</div>
@ -97,8 +142,8 @@
.params {
display: grid;
column-gap: var(--spacing-xs);
row-gap: var(--spacing-s);
grid-template-columns: 90px 1fr;
row-gap: var(--spacing-m);
grid-template-columns: 90px 1fr 90px;
align-items: center;
}
</style>

View File

@ -29,6 +29,12 @@
allowLinks: true,
})
$: {
value = (value || []).filter(
column => (schema || {})[column.name || column] !== undefined
)
}
const getText = value => {
if (!value?.length) {
return "All columns"

View File

@ -17,6 +17,10 @@ export function breakQueryString(qs) {
return paramObj
}
function isEncoded(str) {
return typeof str == "string" && decodeURIComponent(str) !== str
}
export function buildQueryString(obj) {
let str = ""
if (obj) {
@ -35,7 +39,7 @@ export function buildQueryString(obj) {
value = value.replace(binding, marker)
bindingMarkers[marker] = binding
})
let encoded = encodeURIComponent(value || "")
let encoded = isEncoded(value) ? value : encodeURIComponent(value || "")
Object.entries(bindingMarkers).forEach(([marker, binding]) => {
encoded = encoded.replace(marker, binding)
})

View File

@ -39,4 +39,11 @@ describe("check query string utils", () => {
expect(broken.key1).toBe(obj2.key1)
expect(broken.key2).toBe(obj2.key2)
})
it("should not encode a URL more than once when building the query string", () => {
const queryString = buildQueryString({
values: "a%2Cb%2Cc",
})
expect(queryString).toBe("values=a%2Cb%2Cc")
})
})

View File

@ -12,17 +12,11 @@
hoverStore,
} from "stores/builder"
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
import {
ProgressCircle,
Layout,
Heading,
Body,
Icon,
notifications,
} from "@budibase/bbui"
import { Layout, Heading, Body, Icon, notifications } from "@budibase/bbui"
import ErrorSVG from "@budibase/frontend-core/assets/error.svg?raw"
import { findComponent, findComponentPath } from "helpers/components"
import { isActive, goto } from "@roxi/routify"
import { ClientAppSkeleton } from "@budibase/frontend-core"
let iframe
let layout
@ -240,8 +234,16 @@
<!-- svelte-ignore a11y-click-events-have-key-events -->
<div class="component-container">
{#if loading}
<div class="center">
<ProgressCircle />
<div
class={`loading ${$builderStore.theme}`}
class:tablet={$previewStore.previewDevice === "tablet"}
class:mobile={$previewStore.previewDevice === "mobile"}
>
<ClientAppSkeleton
sideNav={$builderStore.navigation?.navigation === "Left"}
hideFooter
hideDevTools
/>
</div>
{:else if error}
<div class="center error">
@ -258,8 +260,6 @@
bind:this={iframe}
src="/app/preview"
class:hidden={loading || error}
class:tablet={$previewStore.previewDevice === "tablet"}
class:mobile={$previewStore.previewDevice === "mobile"}
/>
<div
class="add-component"
@ -279,6 +279,25 @@
/>
<style>
.loading {
position: absolute;
container-type: inline-size;
width: 100%;
height: 100%;
border: 2px solid transparent;
box-sizing: border-box;
}
.loading.tablet {
width: calc(1024px + 6px);
max-height: calc(768px + 6px);
}
.loading.mobile {
width: calc(390px + 6px);
max-height: calc(844px + 6px);
}
.component-container {
grid-row-start: middle;
grid-column-start: middle;

View File

@ -1,16 +1,22 @@
<script>
import { onMount, onDestroy } from "svelte"
import { params, goto } from "@roxi/routify"
import { apps, auth, sideBarCollapsed } from "stores/portal"
import { licensing, apps, auth, sideBarCollapsed } from "stores/portal"
import { Link, Body, ActionButton } from "@budibase/bbui"
import { sdk } from "@budibase/shared-core"
import { API } from "api"
import ErrorSVG from "./ErrorSVG.svelte"
import { ClientAppSkeleton } from "@budibase/frontend-core"
$: app = $apps.find(app => app.appId === $params.appId)
$: iframeUrl = getIframeURL(app)
$: isBuilder = sdk.users.isBuilder($auth.user, app?.devId)
let loading = true
const getIframeURL = app => {
loading = true
if (app.status === "published") {
return `/app${app.url}`
}
@ -28,6 +34,20 @@
}
$: fetchScreens(app?.devId)
const receiveMessage = async message => {
if (message.data.type === "docLoaded") {
loading = false
}
}
onMount(() => {
window.addEventListener("message", receiveMessage)
})
onDestroy(() => {
window.removeEventListener("message", receiveMessage)
})
</script>
<div class="container">
@ -78,7 +98,17 @@
</Body>
</div>
{:else}
<iframe src={iframeUrl} title={app.name} />
<div class:hide={!loading} class="loading">
<div class={`loadingThemeWrapper ${app.theme}`}>
<ClientAppSkeleton
noAnimation
hideDevTools={app?.status === "published"}
sideNav={app?.navigation.navigation === "Left"}
hideFooter={$licensing.brandingEnabled}
/>
</div>
</div>
<iframe class:hide={loading} src={iframeUrl} title={app.name} />
{/if}
</div>
@ -100,6 +130,23 @@
flex: 0 0 50px;
}
.loading {
height: 100%;
border: 1px solid var(--spectrum-global-color-gray-300);
border-radius: var(--spacing-s);
overflow: hidden;
}
.loadingThemeWrapper {
height: 100%;
container-type: inline-size;
}
.hide {
visibility: hidden;
height: 0;
border: none;
}
iframe {
flex: 1 1 auto;
border-radius: var(--spacing-s);

View File

@ -80,11 +80,18 @@
}
}
let fontsLoaded = false
// Load app config
onMount(async () => {
document.fonts.ready.then(() => {
fontsLoaded = true
})
await initialise()
await authStore.actions.fetchUser()
dataLoaded = true
if (get(builderStore).inBuilder) {
builderStore.actions.notifyLoaded()
} else {
@ -93,6 +100,12 @@
})
}
})
$: {
if (dataLoaded && fontsLoaded) {
document.getElementById("clientAppSkeletonLoader")?.remove()
}
}
</script>
<svelte:head>
@ -103,140 +116,140 @@
{/if}
</svelte:head>
{#if dataLoaded}
<div
id="spectrum-root"
lang="en"
dir="ltr"
class="spectrum spectrum--medium {$themeStore.baseTheme} {$themeStore.theme}"
class:builder={$builderStore.inBuilder}
>
<DeviceBindingsProvider>
<UserBindingsProvider>
<StateBindingsProvider>
<RowSelectionProvider>
<QueryParamsProvider>
<!-- Settings bar can be rendered outside of device preview -->
<!-- Key block needs to be outside the if statement or it breaks -->
{#key $builderStore.selectedComponentId}
{#if $builderStore.inBuilder}
<SettingsBar />
{/if}
{/key}
<div
id="spectrum-root"
lang="en"
dir="ltr"
class="spectrum spectrum--medium {$themeStore.baseTheme} {$themeStore.theme}"
class:builder={$builderStore.inBuilder}
class:show={fontsLoaded && dataLoaded}
>
<DeviceBindingsProvider>
<UserBindingsProvider>
<StateBindingsProvider>
<RowSelectionProvider>
<QueryParamsProvider>
<!-- Settings bar can be rendered outside of device preview -->
<!-- Key block needs to be outside the if statement or it breaks -->
{#key $builderStore.selectedComponentId}
{#if $builderStore.inBuilder}
<SettingsBar />
{/if}
{/key}
<!-- Clip boundary for selection indicators -->
<div
id="clip-root"
class:preview={$builderStore.inBuilder}
class:tablet-preview={$builderStore.previewDevice === "tablet"}
class:mobile-preview={$builderStore.previewDevice === "mobile"}
>
<!-- Actual app -->
<div id="app-root">
{#if showDevTools}
<DevToolsHeader />
<!-- Clip boundary for selection indicators -->
<div
id="clip-root"
class:preview={$builderStore.inBuilder}
class:tablet-preview={$builderStore.previewDevice === "tablet"}
class:mobile-preview={$builderStore.previewDevice === "mobile"}
>
<!-- Actual app -->
<div id="app-root">
{#if showDevTools}
<DevToolsHeader />
{/if}
<div id="app-body">
{#if permissionError}
<div class="error">
<Layout justifyItems="center" gap="S">
<!-- eslint-disable-next-line svelte/no-at-html-tags -->
{@html ErrorSVG}
<Heading size="L">
You don't have permission to use this app
</Heading>
<Body size="S">
Ask your administrator to grant you access
</Body>
</Layout>
</div>
{:else if !$screenStore.activeLayout}
<div class="error">
<Layout justifyItems="center" gap="S">
<!-- eslint-disable-next-line svelte/no-at-html-tags -->
{@html ErrorSVG}
<Heading size="L">
Something went wrong rendering your app
</Heading>
<Body size="S">
Get in touch with support if this issue persists
</Body>
</Layout>
</div>
{:else if embedNoScreens}
<div class="error">
<Layout justifyItems="center" gap="S">
<!-- eslint-disable-next-line svelte/no-at-html-tags -->
{@html ErrorSVG}
<Heading size="L">
This Budibase app is not publicly accessible
</Heading>
</Layout>
</div>
{:else}
<CustomThemeWrapper>
{#key $screenStore.activeLayout._id}
<Component
isLayout
instance={$screenStore.activeLayout.props}
/>
{/key}
<!--
Flatpickr needs to be inside the theme wrapper.
It also needs its own container because otherwise it hijacks
key events on the whole page. It is painful to work with.
-->
<div id="flatpickr-root" />
<!-- Modal container to ensure they sit on top -->
<div class="modal-container" />
<!-- Layers on top of app -->
<NotificationDisplay />
<ConfirmationDisplay />
<PeekScreenDisplay />
</CustomThemeWrapper>
{/if}
<div id="app-body">
{#if permissionError}
<div class="error">
<Layout justifyItems="center" gap="S">
<!-- eslint-disable-next-line svelte/no-at-html-tags -->
{@html ErrorSVG}
<Heading size="L">
You don't have permission to use this app
</Heading>
<Body size="S">
Ask your administrator to grant you access
</Body>
</Layout>
</div>
{:else if !$screenStore.activeLayout}
<div class="error">
<Layout justifyItems="center" gap="S">
<!-- eslint-disable-next-line svelte/no-at-html-tags -->
{@html ErrorSVG}
<Heading size="L">
Something went wrong rendering your app
</Heading>
<Body size="S">
Get in touch with support if this issue persists
</Body>
</Layout>
</div>
{:else if embedNoScreens}
<div class="error">
<Layout justifyItems="center" gap="S">
<!-- eslint-disable-next-line svelte/no-at-html-tags -->
{@html ErrorSVG}
<Heading size="L">
This Budibase app is not publicly accessible
</Heading>
</Layout>
</div>
{:else}
<CustomThemeWrapper>
{#key $screenStore.activeLayout._id}
<Component
isLayout
instance={$screenStore.activeLayout.props}
/>
{/key}
<!--
Flatpickr needs to be inside the theme wrapper.
It also needs its own container because otherwise it hijacks
key events on the whole page. It is painful to work with.
-->
<div id="flatpickr-root" />
<!-- Modal container to ensure they sit on top -->
<div class="modal-container" />
<!-- Layers on top of app -->
<NotificationDisplay />
<ConfirmationDisplay />
<PeekScreenDisplay />
</CustomThemeWrapper>
{/if}
{#if showDevTools}
<DevTools />
{/if}
</div>
{#if !$builderStore.inBuilder && licensing.logoEnabled()}
<FreeFooter />
{#if showDevTools}
<DevTools />
{/if}
</div>
<!-- Preview and dev tools utilities -->
{#if $appStore.isDevApp}
<SelectionIndicator />
{/if}
{#if $builderStore.inBuilder || $devToolsStore.allowSelection}
<HoverIndicator />
{/if}
{#if $builderStore.inBuilder}
<DNDHandler />
<GridDNDHandler />
{#if !$builderStore.inBuilder && licensing.logoEnabled()}
<FreeFooter />
{/if}
</div>
</QueryParamsProvider>
</RowSelectionProvider>
</StateBindingsProvider>
</UserBindingsProvider>
</DeviceBindingsProvider>
</div>
<KeyboardManager />
{/if}
<!-- Preview and dev tools utilities -->
{#if $appStore.isDevApp}
<SelectionIndicator />
{/if}
{#if $builderStore.inBuilder || $devToolsStore.allowSelection}
<HoverIndicator />
{/if}
{#if $builderStore.inBuilder}
<DNDHandler />
<GridDNDHandler />
{/if}
</div>
</QueryParamsProvider>
</RowSelectionProvider>
</StateBindingsProvider>
</UserBindingsProvider>
</DeviceBindingsProvider>
</div>
<KeyboardManager />
<style>
#spectrum-root {
height: 0;
visibility: hidden;
padding: 0;
margin: 0;
overflow: hidden;
height: 100%;
width: 100%;
display: flex;
flex-direction: row;
@ -257,6 +270,11 @@
background-color: transparent;
}
#spectrum-root.show {
height: 100%;
visibility: visible;
}
#app-root {
overflow: hidden;
height: 100%;

View File

@ -13,6 +13,7 @@
<style>
.free-footer {
min-height: 51px;
flex: 0 0 auto;
padding: 16px 20px;
border-top: 1px solid var(--spectrum-global-color-gray-300);

View File

@ -341,7 +341,11 @@ const exportDataHandler = async action => {
tableId: selection.tableId,
rows: selection.selectedRows,
format: action.parameters.type,
columns: action.parameters.columns,
columns: action.parameters.columns?.map(
column => column.name || column
),
delimiter: action.parameters.delimiter,
customHeaders: action.parameters.customHeaders,
})
download(
new Blob([data], { type: "text/plain" }),

View File

@ -89,13 +89,24 @@ export const buildRowEndpoints = API => ({
* @param rows the array of rows to export
* @param format the format to export (csv or json)
* @param columns which columns to export (all if undefined)
* @param delimiter how values should be separated in a CSV (default is comma)
*/
exportRows: async ({ tableId, rows, format, columns, search }) => {
exportRows: async ({
tableId,
rows,
format,
columns,
search,
delimiter,
customHeaders,
}) => {
return await API.post({
url: `/api/${tableId}/rows/exportRows?format=${format}`,
body: {
rows,
columns,
delimiter,
customHeaders,
...search,
},
parseResponse: async response => {

View File

@ -0,0 +1,244 @@
<script>
export let sideNav = false
export let hideDevTools = false
export let hideFooter = false
export let noAnimation = false
</script>
<div class:sideNav id="clientAppSkeletonLoader" class="skeleton">
<div class="animation" class:noAnimation />
{#if !hideDevTools}
<div class="devTools" />
{/if}
<div class="main">
<div class="nav" />
<div class="body">
<div class="bodyVerticalPadding" />
<div class="bodyHorizontal">
<div class="bodyHorizontalPadding" />
<svg
class="svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
width="240"
height="256"
>
<mask id="mask">
<rect x="0" y="0" width="240" height="256" fill="white" />
<rect x="0" y="0" width="240" height="32" rx="6" fill="black" />
<rect x="0" y="56" width="240" height="32" rx="6" fill="black" />
<rect x="0" y="112" width="240" height="32" rx="6" fill="black" />
<rect x="0" y="168" width="240" height="32" rx="6" fill="black" />
<rect x="71" y="224" width="98" height="32" rx="6" fill="black" />
</mask>
<rect
x="0"
y="0"
width="240"
height="256"
fill="black"
mask="url(#mask)"
/>
</svg>
<div class="bodyHorizontalPadding" />
</div>
<div class="bodyVerticalPadding" />
</div>
</div>
{#if !hideFooter}
<div class="footer" />
{/if}
</div>
<style>
.skeleton {
position: relative;
height: 100%;
display: flex;
flex-direction: column;
border-radius: 4px;
overflow: hidden;
background-color: var(--spectrum-global-color-gray-200);
}
.animation {
position: absolute;
height: 100%;
width: 100%;
background: linear-gradient(
to right,
transparent 0%,
var(--spectrum-global-color-gray-300) 20%,
transparent 40%,
transparent 100%
);
animation-duration: 1.3s;
animation-fill-mode: forwards;
animation-iteration-count: infinite;
animation-name: shimmer;
animation-timing-function: linear;
}
.noAnimation {
animation-name: none;
background: transparent;
}
.devTools {
display: flex;
box-sizing: border-box;
background-color: black;
height: 60px;
padding: 1px 24px 1px 20px;
display: flex;
align-items: center;
z-index: 1;
flex-shrink: 0;
color: white;
mix-blend-mode: multiply;
background: rgb(0 0 0);
font-size: 30px;
font-family: Source Sans Pro;
-webkit-font-smoothing: antialiased;
}
.main {
height: 100%;
display: flex;
flex-direction: column;
}
@media (max-width: 720px) {
#clientAppSkeletonLoader .main {
flex-direction: column;
width: initial;
}
}
@container (max-width: 720px) {
#clientAppSkeletonLoader .main {
flex-direction: column;
width: initial;
}
}
.sideNav .main {
flex-direction: row;
width: 100%;
}
.nav {
flex-shrink: 0;
width: 100%;
height: 141px;
background-color: transparent;
}
@media (max-width: 720px) {
#clientAppSkeletonLoader .nav {
height: 61px;
width: initial;
}
}
@container (max-width: 720px) {
#clientAppSkeletonLoader .nav {
height: 61px;
width: initial;
}
}
.sideNav .nav {
height: 100%;
width: 251px;
}
.body {
z-index: 2;
display: flex;
flex-direction: column;
height: 100%;
position: relative;
}
@media (max-width: 720px) {
#clientAppSkeletonLoader .body {
width: initial;
height: 100%;
}
}
@container (max-width: 720px) {
#clientAppSkeletonLoader .body {
width: initial;
height: 100%;
}
}
.sideNav .body {
width: 100%;
height: initial;
}
.body :global(svg > rect) {
fill: var(--spectrum-alias-background-color-primary);
}
.body :global(svg) {
flex-shrink: 0;
}
.bodyHorizontal {
display: flex;
flex-shrink: 0;
}
.bodyHorizontalPadding {
height: 100%;
flex-grow: 1;
background-color: var(--spectrum-alias-background-color-primary);
}
.bodyVerticalPadding {
width: 100%;
flex-grow: 1;
background-color: var(--spectrum-alias-background-color-primary);
}
.footer {
flex-shrink: 0;
box-sizing: border-box;
z-index: 1;
height: 52px;
width: 100%;
}
@media (max-width: 720px) {
#clientAppSkeletonLoader .footer {
border-top: none;
}
}
@container (max-width: 720px) {
#clientAppSkeletonLoader .footer {
border-top: none;
}
}
.sideNav .footer {
border-top: 3px solid var(--spectrum-alias-background-color-primary);
}
@keyframes shimmer {
0% {
left: -170%;
}
100% {
left: 170%;
}
}
</style>

View File

@ -5,3 +5,4 @@ export { default as UserAvatar } from "./UserAvatar.svelte"
export { default as UserAvatars } from "./UserAvatars.svelte"
export { default as Updating } from "./Updating.svelte"
export { Grid } from "./grid"
export { default as ClientAppSkeleton } from "./ClientAppSkeleton.svelte"

View File

@ -17,5 +17,8 @@
--modal-background: var(--spectrum-global-color-gray-50);
--drop-shadow: rgba(0, 0, 0, 0.25) !important;
--spectrum-global-color-blue-100: rgba(35, 40, 50) !important;
--spectrum-alias-background-color-secondary: var(--spectrum-global-color-gray-75);
--spectrum-alias-background-color-primary: var(--spectrum-global-color-gray-100);
}

View File

@ -50,4 +50,7 @@
--modal-background: var(--spectrum-global-color-gray-50);
--drop-shadow: rgba(0, 0, 0, 0.15) !important;
--spectrum-global-color-blue-100: rgb(56, 65, 84) !important;
--spectrum-alias-background-color-secondary: var(--spectrum-global-color-gray-75);
--spectrum-alias-background-color-primary: var(--spectrum-global-color-gray-100);
}

View File

@ -52,6 +52,7 @@
"@budibase/pro": "0.0.0",
"@budibase/shared-core": "0.0.0",
"@budibase/string-templates": "0.0.0",
"@budibase/frontend-core": "0.0.0",
"@budibase/types": "0.0.0",
"@bull-board/api": "5.10.2",
"@bull-board/koa": "5.10.2",

View File

@ -1,3 +1,3 @@
#!/bin/bash
docker-compose down
docker-compose down -v
docker volume prune -f

View File

@ -3,12 +3,12 @@ set -e
if [[ -n $CI ]]
then
export NODE_OPTIONS="--max-old-space-size=4096 --no-node-snapshot"
export NODE_OPTIONS="--max-old-space-size=4096 --no-node-snapshot $NODE_OPTIONS"
echo "jest --coverage --maxWorkers=2 --forceExit --workerIdleMemoryLimit=2000MB --bail $@"
jest --coverage --maxWorkers=2 --forceExit --workerIdleMemoryLimit=2000MB --bail $@
else
# --maxWorkers performs better in development
export NODE_OPTIONS="--no-node-snapshot"
export NODE_OPTIONS="--no-node-snapshot $NODE_OPTIONS"
echo "jest --coverage --maxWorkers=2 --forceExit $@"
jest --coverage --maxWorkers=2 --forceExit $@
fi

View File

@ -47,6 +47,9 @@ import {
PlanType,
Screen,
UserCtx,
CreateAppRequest,
FetchAppDefinitionResponse,
FetchAppPackageResponse,
} from "@budibase/types"
import { BASE_LAYOUT_PROP_IDS } from "../../constants/layouts"
import sdk from "../../sdk"
@ -58,23 +61,23 @@ import * as appMigrations from "../../appMigrations"
async function getLayouts() {
const db = context.getAppDB()
return (
await db.allDocs(
await db.allDocs<Layout>(
getLayoutParams(null, {
include_docs: true,
})
)
).rows.map((row: any) => row.doc)
).rows.map(row => row.doc!)
}
async function getScreens() {
const db = context.getAppDB()
return (
await db.allDocs(
await db.allDocs<Screen>(
getScreenParams(null, {
include_docs: true,
})
)
).rows.map((row: any) => row.doc)
).rows.map(row => row.doc!)
}
function getUserRoleId(ctx: UserCtx) {
@ -116,8 +119,8 @@ function checkAppName(
}
interface AppTemplate {
templateString: string
useTemplate: string
templateString?: string
useTemplate?: string
file?: {
type: string
path: string
@ -174,14 +177,16 @@ export const addSampleData = async (ctx: UserCtx) => {
ctx.status = 200
}
export async function fetch(ctx: UserCtx) {
export async function fetch(ctx: UserCtx<void, App[]>) {
ctx.body = await sdk.applications.fetch(
ctx.query.status as AppStatus,
ctx.user
)
}
export async function fetchAppDefinition(ctx: UserCtx) {
export async function fetchAppDefinition(
ctx: UserCtx<void, FetchAppDefinitionResponse>
) {
const layouts = await getLayouts()
const userRoleId = getUserRoleId(ctx)
const accessController = new roles.AccessController()
@ -196,10 +201,12 @@ export async function fetchAppDefinition(ctx: UserCtx) {
}
}
export async function fetchAppPackage(ctx: UserCtx) {
export async function fetchAppPackage(
ctx: UserCtx<void, FetchAppPackageResponse>
) {
const db = context.getAppDB()
const appId = context.getAppId()
let application = await db.get<any>(DocumentType.APP_METADATA)
let application = await db.get<App>(DocumentType.APP_METADATA)
const layouts = await getLayouts()
let screens = await getScreens()
const license = await licensing.cache.getCachedLicense()
@ -231,17 +238,21 @@ export async function fetchAppPackage(ctx: UserCtx) {
}
}
async function performAppCreate(ctx: UserCtx) {
async function performAppCreate(ctx: UserCtx<CreateAppRequest, App>) {
const apps = (await dbCore.getAllApps({ dev: true })) as App[]
const name = ctx.request.body.name,
possibleUrl = ctx.request.body.url,
encryptionPassword = ctx.request.body.encryptionPassword
const {
name,
url,
encryptionPassword,
useTemplate,
templateKey,
templateString,
} = ctx.request.body
checkAppName(ctx, apps, name)
const url = sdk.applications.getAppUrl({ name, url: possibleUrl })
checkAppUrl(ctx, apps, url)
const appUrl = sdk.applications.getAppUrl({ name, url })
checkAppUrl(ctx, apps, appUrl)
const { useTemplate, templateKey, templateString } = ctx.request.body
const instanceConfig: AppTemplate = {
useTemplate,
key: templateKey,
@ -268,7 +279,7 @@ async function performAppCreate(ctx: UserCtx) {
version: envCore.VERSION,
componentLibraries: ["@budibase/standard-components"],
name: name,
url: url,
url: appUrl,
template: templateKey,
instance,
tenantId: tenancy.getTenantId(),
@ -420,7 +431,9 @@ export async function create(ctx: UserCtx) {
// This endpoint currently operates as a PATCH rather than a PUT
// Thus name and url fields are handled only if present
export async function update(ctx: UserCtx) {
export async function update(
ctx: UserCtx<{ name?: string; url?: string }, App>
) {
const apps = (await dbCore.getAllApps({ dev: true })) as App[]
// validation
const name = ctx.request.body.name,
@ -493,7 +506,7 @@ export async function revertClient(ctx: UserCtx) {
const revertedToVersion = application.revertableVersion
const appPackageUpdates = {
version: revertedToVersion,
revertableVersion: null,
revertableVersion: undefined,
}
const app = await updateAppPackage(appPackageUpdates, ctx.params.appId)
await events.app.versionReverted(app, currentVersion, revertedToVersion)
@ -613,12 +626,15 @@ export async function importToApp(ctx: UserCtx) {
ctx.body = { message: "app updated" }
}
export async function updateAppPackage(appPackage: any, appId: any) {
export async function updateAppPackage(
appPackage: Partial<App>,
appId: string
) {
return context.doInAppContext(appId, async () => {
const db = context.getAppDB()
const application = await db.get<App>(DocumentType.APP_METADATA)
const newAppPackage = { ...application, ...appPackage }
const newAppPackage: App = { ...application, ...appPackage }
if (appPackage._rev !== application._rev) {
newAppPackage._rev = application._rev
}

View File

@ -11,12 +11,12 @@ import {
PaginationJson,
RelationshipFieldMetadata,
RelationshipsJson,
RelationshipType,
Row,
SearchFilters,
SortJson,
SortType,
Table,
isManyToOne,
} from "@budibase/types"
import {
breakExternalTableId,
@ -24,6 +24,7 @@ import {
convertRowId,
isRowId,
isSQL,
generateRowIdField,
} from "../../../integrations/utils"
import {
buildExternalRelationships,
@ -33,13 +34,16 @@ import {
updateRelationshipColumns,
fixArrayTypes,
isManyToMany,
processRelationshipFields,
} from "./utils"
import { getDatasourceAndQuery } from "../../../sdk/app/rows/utils"
import { processObjectSync } from "@budibase/string-templates"
import { cloneDeep } from "lodash/fp"
import { db as dbCore } from "@budibase/backend-core"
import { processDates, processFormulas } from "../../../utilities/rowProcessor"
import { processDates } from "../../../utilities/rowProcessor"
import AliasTables from "./alias"
import sdk from "../../../sdk"
import env from "../../../environment"
export interface ManyRelationship {
tableId?: string
@ -108,6 +112,39 @@ function buildFilters(
}
}
async function removeManyToManyRelationships(
rowId: string,
table: Table,
colName: string
) {
const tableId = table._id!
const filters = buildFilters(rowId, {}, table)
// safety check, if there are no filters on deletion bad things happen
if (Object.keys(filters).length !== 0) {
return getDatasourceAndQuery({
endpoint: getEndpoint(tableId, Operation.DELETE),
body: { [colName]: null },
filters,
})
} else {
return []
}
}
async function removeOneToManyRelationships(rowId: string, table: Table) {
const tableId = table._id!
const filters = buildFilters(rowId, {}, table)
// safety check, if there are no filters on deletion bad things happen
if (Object.keys(filters).length !== 0) {
return getDatasourceAndQuery({
endpoint: getEndpoint(tableId, Operation.UPDATE),
filters,
})
} else {
return []
}
}
/**
* This function checks the incoming parameters to make sure all the inputs are
* valid based on on the table schema. The main thing this is looking for is when a
@ -158,13 +195,13 @@ function cleanupConfig(config: RunConfig, table: Table): RunConfig {
function getEndpoint(tableId: string | undefined, operation: string) {
if (!tableId) {
return {}
throw new Error("Cannot get endpoint information - no table ID specified")
}
const { datasourceId, tableName } = breakExternalTableId(tableId)
return {
datasourceId,
entityId: tableName,
operation,
datasourceId: datasourceId!,
entityId: tableName!,
operation: operation as Operation,
}
}
@ -264,6 +301,18 @@ export class ExternalRequest<T extends Operation> {
}
}
async getRow(table: Table, rowId: string): Promise<Row> {
const response = await getDatasourceAndQuery({
endpoint: getEndpoint(table._id!, Operation.READ),
filters: buildFilters(rowId, {}, table),
})
if (Array.isArray(response) && response.length > 0) {
return response[0]
} else {
throw new Error(`Cannot fetch row by ID "${rowId}"`)
}
}
inputProcessing(row: Row | undefined, table: Table) {
if (!row) {
return { row, manyRelationships: [] }
@ -348,33 +397,6 @@ export class ExternalRequest<T extends Operation> {
return { row: newRow, manyRelationships }
}
processRelationshipFields(
table: Table,
row: Row,
relationships: RelationshipsJson[]
): Row {
for (let relationship of relationships) {
const linkedTable = this.tables[relationship.tableName]
if (!linkedTable || !row[relationship.column]) {
continue
}
for (let key of Object.keys(row[relationship.column])) {
let relatedRow: Row = row[relationship.column][key]
// add this row as context for the relationship
for (let col of Object.values(linkedTable.schema)) {
if (col.type === FieldType.LINK && col.tableId === table._id) {
relatedRow[col.name] = [row]
}
}
// process additional types
relatedRow = processDates(table, relatedRow)
relatedRow = processFormulas(linkedTable, relatedRow)
row[relationship.column][key] = relatedRow
}
}
return row
}
outputProcessing(
rows: Row[] = [],
table: Table,
@ -419,7 +441,7 @@ export class ExternalRequest<T extends Operation> {
// make sure all related rows are correct
let finalRowArray = Object.values(finalRows).map(row =>
this.processRelationshipFields(table, row, relationships)
processRelationshipFields(table, this.tables, row, relationships)
)
// process some additional types
@ -432,7 +454,9 @@ export class ExternalRequest<T extends Operation> {
* information.
*/
async lookupRelations(tableId: string, row: Row) {
const related: { [key: string]: any } = {}
const related: {
[key: string]: { rows: Row[]; isMany: boolean; tableId: string }
} = {}
const { tableName } = breakExternalTableId(tableId)
if (!tableName) {
return related
@ -450,14 +474,26 @@ export class ExternalRequest<T extends Operation> {
) {
continue
}
const isMany = field.relationshipType === RelationshipType.MANY_TO_MANY
const tableId = isMany ? field.through : field.tableId
let tableId: string | undefined,
lookupField: string | undefined,
fieldName: string | undefined
if (isManyToMany(field)) {
tableId = field.through
lookupField = primaryKey
fieldName = field.throughTo || primaryKey
} else if (isManyToOne(field)) {
tableId = field.tableId
lookupField = field.foreignKey
fieldName = field.fieldName
}
if (!tableId || !lookupField || !fieldName) {
throw new Error(
"Unable to lookup relationships - undefined column properties."
)
}
const { tableName: relatedTableName } = breakExternalTableId(tableId)
// @ts-ignore
const linkPrimaryKey = this.tables[relatedTableName].primary[0]
const lookupField = isMany ? primaryKey : field.foreignKey
const fieldName = isMany ? field.throughTo || primaryKey : field.fieldName
if (!lookupField || !row[lookupField]) {
continue
}
@ -470,9 +506,12 @@ export class ExternalRequest<T extends Operation> {
},
})
// this is the response from knex if no rows found
const rows = !response[0].read ? response : []
const storeTo = isMany ? field.throughFrom || linkPrimaryKey : fieldName
related[storeTo] = { rows, isMany, tableId }
const rows: Row[] =
!Array.isArray(response) || response?.[0].read ? [] : response
const storeTo = isManyToMany(field)
? field.throughFrom || linkPrimaryKey
: fieldName
related[storeTo] = { rows, isMany: isManyToMany(field), tableId }
}
return related
}
@ -558,24 +597,43 @@ export class ExternalRequest<T extends Operation> {
continue
}
for (let row of rows) {
const filters = buildFilters(generateIdForRow(row, table), {}, table)
// safety check, if there are no filters on deletion bad things happen
if (Object.keys(filters).length !== 0) {
const op = isMany ? Operation.DELETE : Operation.UPDATE
const body = isMany ? null : { [colName]: null }
promises.push(
getDatasourceAndQuery({
endpoint: getEndpoint(tableId, op),
body,
filters,
})
)
const rowId = generateIdForRow(row, table)
const promise: Promise<any> = isMany
? removeManyToManyRelationships(rowId, table, colName)
: removeOneToManyRelationships(rowId, table)
if (promise) {
promises.push(promise)
}
}
}
await Promise.all(promises)
}
async removeRelationshipsToRow(table: Table, rowId: string) {
const row = await this.getRow(table, rowId)
const related = await this.lookupRelations(table._id!, row)
for (let column of Object.values(table.schema)) {
const relationshipColumn = column as RelationshipFieldMetadata
if (!isManyToOne(relationshipColumn)) {
continue
}
const { rows, isMany, tableId } = related[relationshipColumn.fieldName]
const table = this.getTable(tableId)!
await Promise.all(
rows.map(row => {
const rowId = generateIdForRow(row, table)
return isMany
? removeManyToManyRelationships(
rowId,
table,
relationshipColumn.fieldName
)
: removeOneToManyRelationships(rowId, table)
})
)
}
}
async run(config: RunConfig): Promise<ExternalRequestReturnType<T>> {
const { operation, tableId } = this
let { datasourceId, tableName } = breakExternalTableId(tableId)
@ -632,7 +690,7 @@ export class ExternalRequest<T extends Operation> {
}
let json = {
endpoint: {
datasourceId,
datasourceId: datasourceId!,
entityId: tableName,
operation,
},
@ -658,13 +716,26 @@ export class ExternalRequest<T extends Operation> {
},
}
// can't really use response right now
const response = await getDatasourceAndQuery(json)
// handle many to many relationships now if we know the ID (could be auto increment)
// remove any relationships that could block deletion
if (operation === Operation.DELETE && id) {
await this.removeRelationshipsToRow(table, generateRowIdField(id))
}
// aliasing can be disabled fully if desired
let response
if (env.SQL_ALIASING_DISABLE) {
response = await getDatasourceAndQuery(json)
} else {
const aliasing = new AliasTables(Object.keys(this.tables))
response = await aliasing.queryWithAliasing(json)
}
const responseRows = Array.isArray(response) ? response : []
// handle many-to-many relationships now if we know the ID (could be auto increment)
if (operation !== Operation.READ) {
await this.handleManyRelationships(
table._id || "",
response[0],
responseRows[0],
processed.manyRelationships
)
}

View File

@ -0,0 +1,168 @@
import {
QueryJson,
SearchFilters,
Table,
Row,
DatasourcePlusQueryResponse,
} from "@budibase/types"
import { getDatasourceAndQuery } from "../../../sdk/app/rows/utils"
import { cloneDeep } from "lodash"
class CharSequence {
static alphabet = "abcdefghijklmnopqrstuvwxyz"
counters: number[]
constructor() {
this.counters = [0]
}
getCharacter(): string {
const char = this.counters.map(i => CharSequence.alphabet[i]).join("")
for (let i = this.counters.length - 1; i >= 0; i--) {
if (this.counters[i] < CharSequence.alphabet.length - 1) {
this.counters[i]++
return char
}
this.counters[i] = 0
}
this.counters.unshift(0)
return char
}
}
export default class AliasTables {
aliases: Record<string, string>
tableAliases: Record<string, string>
tableNames: string[]
charSeq: CharSequence
constructor(tableNames: string[]) {
this.tableNames = tableNames
this.aliases = {}
this.tableAliases = {}
this.charSeq = new CharSequence()
}
getAlias(tableName: string) {
if (this.aliases[tableName]) {
return this.aliases[tableName]
}
const char = this.charSeq.getCharacter()
this.aliases[tableName] = char
this.tableAliases[char] = tableName
return char
}
aliasField(field: string) {
const tableNames = this.tableNames
if (field.includes(".")) {
const [tableName, column] = field.split(".")
const foundTableName = tableNames.find(name => {
const idx = tableName.indexOf(name)
if (idx === -1 || idx > 1) {
return
}
return Math.abs(tableName.length - name.length) <= 2
})
if (foundTableName) {
const aliasedTableName = tableName.replace(
foundTableName,
this.getAlias(foundTableName)
)
field = `${aliasedTableName}.${column}`
}
}
return field
}
reverse<T extends Row | Row[]>(rows: T): T {
const process = (row: Row) => {
const final: Row = {}
for (let [key, value] of Object.entries(row)) {
if (!key.includes(".")) {
final[key] = value
} else {
const [alias, column] = key.split(".")
const tableName = this.tableAliases[alias] || alias
final[`${tableName}.${column}`] = value
}
}
return final
}
if (Array.isArray(rows)) {
return rows.map(row => process(row)) as T
} else {
return process(rows) as T
}
}
aliasMap(tableNames: (string | undefined)[]) {
const map: Record<string, string> = {}
for (let tableName of tableNames) {
if (tableName) {
map[tableName] = this.getAlias(tableName)
}
}
return map
}
async queryWithAliasing(
json: QueryJson
): Promise<DatasourcePlusQueryResponse> {
json = cloneDeep(json)
const aliasTable = (table: Table) => ({
...table,
name: this.getAlias(table.name),
})
// run through the query json to update anywhere a table may be used
if (json.resource?.fields) {
json.resource.fields = json.resource.fields.map(field =>
this.aliasField(field)
)
}
if (json.filters) {
for (let [filterKey, filter] of Object.entries(json.filters)) {
if (typeof filter !== "object") {
continue
}
const aliasedFilters: typeof filter = {}
for (let key of Object.keys(filter)) {
aliasedFilters[this.aliasField(key)] = filter[key]
}
json.filters[filterKey as keyof SearchFilters] = aliasedFilters
}
}
if (json.relationships) {
json.relationships = json.relationships.map(relationship => ({
...relationship,
aliases: this.aliasMap([
relationship.through,
relationship.tableName,
json.endpoint.entityId,
]),
}))
}
if (json.meta?.table) {
json.meta.table = aliasTable(json.meta.table)
}
if (json.meta?.tables) {
const aliasedTables: Record<string, Table> = {}
for (let [tableName, table] of Object.entries(json.meta.tables)) {
aliasedTables[this.getAlias(tableName)] = aliasTable(table)
}
json.meta.tables = aliasedTables
}
// invert and return
const invertedTableAliases: Record<string, string> = {}
for (let [key, value] of Object.entries(this.tableAliases)) {
invertedTableAliases[value] = key
}
json.tableAliases = invertedTableAliases
const response = await getDatasourceAndQuery(json)
if (Array.isArray(response)) {
return this.reverse(response)
} else {
return response
}
}
}

View File

@ -223,7 +223,8 @@ export const exportRows = async (
const format = ctx.query.format
const { rows, columns, query, sort, sortOrder } = ctx.request.body
const { rows, columns, query, sort, sortOrder, delimiter, customHeaders } =
ctx.request.body
if (typeof format !== "string" || !exporters.isFormat(format)) {
ctx.throw(
400,
@ -241,6 +242,8 @@ export const exportRows = async (
query,
sort,
sortOrder,
delimiter,
customHeaders,
})
ctx.attachment(fileName)
ctx.body = apiFileReturn(content)

View File

@ -1,7 +1,15 @@
import { InternalTables } from "../../../../db/utils"
import * as userController from "../../user"
import { context } from "@budibase/backend-core"
import { Ctx, RelationshipsJson, Row, Table, UserCtx } from "@budibase/types"
import {
Ctx,
DatasourcePlusQueryResponse,
FieldType,
RelationshipsJson,
Row,
Table,
UserCtx,
} from "@budibase/types"
import {
processDates,
processFormulas,
@ -25,6 +33,34 @@ validateJs.extend(validateJs.validators.datetime, {
},
})
export function processRelationshipFields(
table: Table,
tables: Record<string, Table>,
row: Row,
relationships: RelationshipsJson[]
): Row {
for (let relationship of relationships) {
const linkedTable = tables[relationship.tableName]
if (!linkedTable || !row[relationship.column]) {
continue
}
for (let key of Object.keys(row[relationship.column])) {
let relatedRow: Row = row[relationship.column][key]
// add this row as context for the relationship
for (let col of Object.values(linkedTable.schema)) {
if (col.type === FieldType.LINK && col.tableId === table._id) {
relatedRow[col.name] = [row]
}
}
// process additional types
relatedRow = processDates(table, relatedRow)
relatedRow = processFormulas(linkedTable, relatedRow)
row[relationship.column][key] = relatedRow
}
}
return row
}
export async function findRow(ctx: UserCtx, tableId: string, rowId: string) {
const db = context.getAppDB()
let row: Row
@ -80,17 +116,17 @@ export async function validate(
}
export function sqlOutputProcessing(
rows: Row[] = [],
rows: DatasourcePlusQueryResponse,
table: Table,
tables: Record<string, Table>,
relationships: RelationshipsJson[],
opts?: { internal?: boolean }
) {
if (!rows || rows.length === 0 || rows[0].read === true) {
if (!Array.isArray(rows) || rows.length === 0 || rows[0].read === true) {
return []
}
let finalRows: { [key: string]: Row } = {}
for (let row of rows) {
for (let row of rows as Row[]) {
let rowId = row._id
if (!rowId) {
rowId = generateIdForRow(row, table)
@ -103,7 +139,8 @@ export function sqlOutputProcessing(
tables,
row,
finalRows,
relationships
relationships,
opts
)
continue
}
@ -126,19 +163,18 @@ export function sqlOutputProcessing(
tables,
row,
finalRows,
relationships,
opts
relationships
)
}
// Process some additional data types
let finalRowArray = Object.values(finalRows)
finalRowArray = processDates(table, finalRowArray)
finalRowArray = processFormulas(table, finalRowArray) as Row[]
return finalRowArray.map((row: Row) =>
squashRelationshipColumns(table, tables, row, relationships)
// make sure all related rows are correct
let finalRowArray = Object.values(finalRows).map(row =>
processRelationshipFields(table, tables, row, relationships)
)
// process some additional types
finalRowArray = processDates(table, finalRowArray)
return finalRowArray
}
export function isUserMetadataTable(tableId: string) {

View File

@ -1,7 +1,5 @@
import { InvalidFileExtensions } from "@budibase/shared-core"
import AppComponent from "./templates/BudibaseApp.svelte"
import { join } from "../../../utilities/centralPath"
import * as uuid from "uuid"
import { ObjectStoreBuckets } from "../../../constants"
@ -24,10 +22,12 @@ import fs from "fs"
import sdk from "../../../sdk"
import * as pro from "@budibase/pro"
import {
DocumentType,
UserCtx,
App,
Ctx,
DocumentType,
ProcessAttachmentResponse,
Feature,
} from "@budibase/types"
import {
getAppMigrationVersion,
@ -36,6 +36,61 @@ import {
import send from "koa-send"
const getThemeVariables = (theme: string) => {
if (theme === "spectrum--lightest") {
return `
--spectrum-global-color-gray-50: rgb(255, 255, 255);
--spectrum-global-color-gray-200: rgb(244, 244, 244);
--spectrum-global-color-gray-300: rgb(234, 234, 234);
--spectrum-alias-background-color-primary: var(--spectrum-global-color-gray-50);
`
}
if (theme === "spectrum--light") {
return `
--spectrum-global-color-gray-50: rgb(255, 255, 255);
--spectrum-global-color-gray-200: rgb(234, 234, 234);
--spectrum-global-color-gray-300: rgb(225, 225, 225);
--spectrum-alias-background-color-primary: var(--spectrum-global-color-gray-50);
`
}
if (theme === "spectrum--dark") {
return `
--spectrum-global-color-gray-100: rgb(50, 50, 50);
--spectrum-global-color-gray-200: rgb(62, 62, 62);
--spectrum-global-color-gray-300: rgb(74, 74, 74);
--spectrum-alias-background-color-primary: var(--spectrum-global-color-gray-100);
`
}
if (theme === "spectrum--darkest") {
return `
--spectrum-global-color-gray-100: rgb(30, 30, 30);
--spectrum-global-color-gray-200: rgb(44, 44, 44);
--spectrum-global-color-gray-300: rgb(57, 57, 57);
--spectrum-alias-background-color-primary: var(--spectrum-global-color-gray-100);
`
}
if (theme === "spectrum--nord") {
return `
--spectrum-global-color-gray-100: #3b4252;
--spectrum-global-color-gray-200: #424a5c;
--spectrum-global-color-gray-300: #4c566a;
--spectrum-alias-background-color-primary: var(--spectrum-global-color-gray-100);
`
}
if (theme === "spectrum--midnight") {
return `
--hue: 220;
--sat: 10%;
--spectrum-global-color-gray-100: hsl(var(--hue), var(--sat), 17%);
--spectrum-global-color-gray-200: hsl(var(--hue), var(--sat), 20%);
--spectrum-global-color-gray-300: hsl(var(--hue), var(--sat), 24%);
--spectrum-alias-background-color-primary: var(--spectrum-global-color-gray-100);
`
}
}
export const toggleBetaUiFeature = async function (ctx: Ctx) {
const cookieName = `beta:${ctx.params.feature}`
@ -150,7 +205,7 @@ const requiresMigration = async (ctx: Ctx) => {
return requiresMigrations
}
export const serveApp = async function (ctx: Ctx) {
export const serveApp = async function (ctx: UserCtx) {
const needMigrations = await requiresMigration(ctx)
const bbHeaderEmbed =
@ -171,9 +226,19 @@ export const serveApp = async function (ctx: Ctx) {
const appInfo = await db.get<any>(DocumentType.APP_METADATA)
let appId = context.getAppId()
const hideDevTools = !!ctx.params.appUrl
const sideNav = appInfo.navigation.navigation === "Left"
const hideFooter =
ctx?.user?.license?.features?.includes(Feature.BRANDING) || false
const themeVariables = getThemeVariables(appInfo?.theme)
if (!env.isJest()) {
const plugins = objectStore.enrichPluginURLs(appInfo.usedPlugins)
const { head, html, css } = AppComponent.render({
hideDevTools,
sideNav,
hideFooter,
metaImage:
branding?.metaImageUrl ||
"https://res.cloudinary.com/daog6scxm/image/upload/v1698759482/meta-images/plain-branded-meta-image-coral_ocxmgu.png",
@ -198,7 +263,7 @@ export const serveApp = async function (ctx: Ctx) {
ctx.body = await processString(appHbs, {
head,
body: html,
style: css.code,
css: `:root{${themeVariables}} ${css.code}`,
appId,
embedded: bbHeaderEmbed,
})

View File

@ -1,4 +1,6 @@
<script>
import ClientAppSkeleton from "@budibase/frontend-core/src/components/ClientAppSkeleton.svelte"
export let title = ""
export let favicon = ""
@ -9,6 +11,10 @@
export let clientLibPath
export let usedPlugins
export let appMigrating
export let hideDevTools
export let sideNav
export let hideFooter
</script>
<svelte:head>
@ -96,6 +102,7 @@
</svelte:head>
<body id="app">
<ClientAppSkeleton {hideDevTools} {sideNav} {hideFooter} />
<div id="error">
{#if clientLibPath}
<h1>There was an error loading your app</h1>

View File

@ -1,8 +1,12 @@
<html>
<script>
document.fonts.ready.then(() => {
window.parent.postMessage({ type: "docLoaded" });
})
</script>
<head>
{{{head}}}
<style>{{{style}}}</style>
<style>{{{css}}}</style>
</head>
<script>

View File

@ -1,7 +1,19 @@
import { Row, TableSchema } from "@budibase/types"
export function csv(headers: string[], rows: Row[]) {
let csv = headers.map(key => `"${key}"`).join(",")
function getHeaders(
headers: string[],
customHeaders: { [key: string]: string }
) {
return headers.map(header => `"${customHeaders[header] || header}"`)
}
export function csv(
headers: string[],
rows: Row[],
delimiter: string = ",",
customHeaders: { [key: string]: string } = {}
) {
let csv = getHeaders(headers, customHeaders).join(delimiter)
for (let row of rows) {
csv = `${csv}\n${headers
@ -15,7 +27,7 @@ export function csv(headers: string[], rows: Row[]) {
: ""
return val.trim()
})
.join(",")}`
.join(delimiter)}`
}
return csv
}

View File

@ -4,7 +4,6 @@ import * as deploymentController from "../controllers/deploy"
import authorized from "../../middleware/authorized"
import { permissions } from "@budibase/backend-core"
import { applicationValidator } from "./utils/validators"
import { importToApp } from "../controllers/application"
const router: Router = new Router()

View File

@ -51,8 +51,8 @@ router
controller.deleteObjects
)
.get("/app/preview", authorized(BUILDER), controller.serveBuilderPreview)
.get("/:appId/:path*", controller.serveApp)
.get("/app/:appUrl/:path*", controller.serveApp)
.get("/:appId/:path*", controller.serveApp)
.post(
"/api/attachments/:datasourceId/url",
authorized(PermissionType.TABLE, PermissionLevel.READ),

View File

@ -11,65 +11,54 @@ jest.mock("../../../utilities/redis", () => ({
checkDebounce: jest.fn(),
shutdown: jest.fn(),
}))
import { clearAllApps, checkBuilderEndpoint } from "./utilities/TestFunctions"
import { checkBuilderEndpoint } from "./utilities/TestFunctions"
import * as setup from "./utilities"
import { AppStatus } from "../../../db/utils"
import { events, utils, context } from "@budibase/backend-core"
import env from "../../../environment"
jest.setTimeout(15000)
import type { App } from "@budibase/types"
import tk from "timekeeper"
describe("/applications", () => {
let request = setup.getRequest()
let config = setup.getConfig()
let app: App
afterAll(setup.afterAll)
beforeAll(async () => {
await config.init()
})
beforeAll(async () => await config.init())
beforeEach(async () => {
app = await config.api.application.create({ name: utils.newid() })
const deployment = await config.api.application.publish(app.appId)
expect(deployment.status).toBe("SUCCESS")
jest.clearAllMocks()
})
describe("create", () => {
it("creates empty app", async () => {
const res = await request
.post("/api/applications")
.field("name", utils.newid())
.set(config.defaultHeaders())
.expect("Content-Type", /json/)
.expect(200)
expect(res.body._id).toBeDefined()
const app = await config.api.application.create({ name: utils.newid() })
expect(app._id).toBeDefined()
expect(events.app.created).toBeCalledTimes(1)
})
it("creates app from template", async () => {
const res = await request
.post("/api/applications")
.field("name", utils.newid())
.field("useTemplate", "true")
.field("templateKey", "test")
.field("templateString", "{}") // override the file download
.set(config.defaultHeaders())
.expect("Content-Type", /json/)
.expect(200)
expect(res.body._id).toBeDefined()
const app = await config.api.application.create({
name: utils.newid(),
useTemplate: "true",
templateKey: "test",
templateString: "{}",
})
expect(app._id).toBeDefined()
expect(events.app.created).toBeCalledTimes(1)
expect(events.app.templateImported).toBeCalledTimes(1)
})
it("creates app from file", async () => {
const res = await request
.post("/api/applications")
.field("name", utils.newid())
.field("useTemplate", "true")
.set(config.defaultHeaders())
.attach("templateFile", "src/api/routes/tests/data/export.txt")
.expect("Content-Type", /json/)
.expect(200)
expect(res.body._id).toBeDefined()
const app = await config.api.application.create({
name: utils.newid(),
useTemplate: "true",
templateFile: "src/api/routes/tests/data/export.txt",
})
expect(app._id).toBeDefined()
expect(events.app.created).toBeCalledTimes(1)
expect(events.app.fileImported).toBeCalledTimes(1)
})
@ -84,24 +73,21 @@ describe("/applications", () => {
})
it("migrates navigation settings from old apps", async () => {
const res = await request
.post("/api/applications")
.field("name", "Old App")
.field("useTemplate", "true")
.set(config.defaultHeaders())
.attach("templateFile", "src/api/routes/tests/data/old-app.txt")
.expect("Content-Type", /json/)
.expect(200)
expect(res.body._id).toBeDefined()
expect(res.body.navigation).toBeDefined()
expect(res.body.navigation.hideLogo).toBe(true)
expect(res.body.navigation.title).toBe("Custom Title")
expect(res.body.navigation.hideLogo).toBe(true)
expect(res.body.navigation.navigation).toBe("Left")
expect(res.body.navigation.navBackground).toBe(
const app = await config.api.application.create({
name: utils.newid(),
useTemplate: "true",
templateFile: "src/api/routes/tests/data/old-app.txt",
})
expect(app._id).toBeDefined()
expect(app.navigation).toBeDefined()
expect(app.navigation!.hideLogo).toBe(true)
expect(app.navigation!.title).toBe("Custom Title")
expect(app.navigation!.hideLogo).toBe(true)
expect(app.navigation!.navigation).toBe("Left")
expect(app.navigation!.navBackground).toBe(
"var(--spectrum-global-color-blue-600)"
)
expect(res.body.navigation.navTextColor).toBe(
expect(app.navigation!.navTextColor).toBe(
"var(--spectrum-global-color-gray-50)"
)
expect(events.app.created).toBeCalledTimes(1)
@ -110,164 +96,106 @@ describe("/applications", () => {
})
describe("fetch", () => {
beforeEach(async () => {
// Clean all apps but the onde from config
await clearAllApps(config.getTenantId(), [config.getAppId()!])
})
it("lists all applications", async () => {
await config.createApp("app1")
await config.createApp("app2")
const res = await request
.get(`/api/applications?status=${AppStatus.DEV}`)
.set(config.defaultHeaders())
.expect("Content-Type", /json/)
.expect(200)
// two created apps + the inited app
expect(res.body.length).toBe(3)
const apps = await config.api.application.fetch({ status: AppStatus.DEV })
expect(apps.length).toBeGreaterThan(0)
})
})
describe("fetchAppDefinition", () => {
it("should be able to get an apps definition", async () => {
const res = await request
.get(`/api/applications/${config.getAppId()}/definition`)
.set(config.defaultHeaders())
.expect("Content-Type", /json/)
.expect(200)
expect(res.body.libraries.length).toEqual(1)
const res = await config.api.application.getDefinition(app.appId)
expect(res.libraries.length).toEqual(1)
})
})
describe("fetchAppPackage", () => {
it("should be able to fetch the app package", async () => {
const res = await request
.get(`/api/applications/${config.getAppId()}/appPackage`)
.set(config.defaultHeaders())
.expect("Content-Type", /json/)
.expect(200)
expect(res.body.application).toBeDefined()
expect(res.body.application.appId).toEqual(config.getAppId())
const res = await config.api.application.getAppPackage(app.appId)
expect(res.application).toBeDefined()
expect(res.application.appId).toEqual(config.getAppId())
})
})
describe("update", () => {
it("should be able to update the app package", async () => {
const res = await request
.put(`/api/applications/${config.getAppId()}`)
.send({
name: "TEST_APP",
})
.set(config.defaultHeaders())
.expect("Content-Type", /json/)
.expect(200)
expect(res.body._rev).toBeDefined()
const updatedApp = await config.api.application.update(app.appId, {
name: "TEST_APP",
})
expect(updatedApp._rev).toBeDefined()
expect(events.app.updated).toBeCalledTimes(1)
})
})
describe("publish", () => {
it("should publish app with dev app ID", async () => {
const appId = config.getAppId()
await request
.post(`/api/applications/${appId}/publish`)
.set(config.defaultHeaders())
.expect("Content-Type", /json/)
.expect(200)
await config.api.application.publish(app.appId)
expect(events.app.published).toBeCalledTimes(1)
})
it("should publish app with prod app ID", async () => {
const appId = config.getProdAppId()
await request
.post(`/api/applications/${appId}/publish`)
.set(config.defaultHeaders())
.expect("Content-Type", /json/)
.expect(200)
await config.api.application.publish(app.appId.replace("_dev", ""))
expect(events.app.published).toBeCalledTimes(1)
})
})
describe("manage client library version", () => {
it("should be able to update the app client library version", async () => {
await request
.post(`/api/applications/${config.getAppId()}/client/update`)
.set(config.defaultHeaders())
.expect("Content-Type", /json/)
.expect(200)
await config.api.application.updateClient(app.appId)
expect(events.app.versionUpdated).toBeCalledTimes(1)
})
it("should be able to revert the app client library version", async () => {
// We need to first update the version so that we can then revert
await request
.post(`/api/applications/${config.getAppId()}/client/update`)
.set(config.defaultHeaders())
.expect("Content-Type", /json/)
.expect(200)
await request
.post(`/api/applications/${config.getAppId()}/client/revert`)
.set(config.defaultHeaders())
.expect("Content-Type", /json/)
.expect(200)
await config.api.application.updateClient(app.appId)
await config.api.application.revertClient(app.appId)
expect(events.app.versionReverted).toBeCalledTimes(1)
})
})
describe("edited at", () => {
it("middleware should set edited at", async () => {
const headers = config.defaultHeaders()
headers["referer"] = `/${config.getAppId()}/test`
const res = await request
.put(`/api/applications/${config.getAppId()}`)
.send({
name: "UPDATED_NAME",
})
.set(headers)
.expect("Content-Type", /json/)
.expect(200)
expect(res.body._rev).toBeDefined()
// retrieve the app to check it
const getRes = await request
.get(`/api/applications/${config.getAppId()}/appPackage`)
.set(headers)
.expect("Content-Type", /json/)
.expect(200)
expect(getRes.body.application.updatedAt).toBeDefined()
it("middleware should set updatedAt", async () => {
const app = await tk.withFreeze(
"2021-01-01",
async () => await config.api.application.create({ name: utils.newid() })
)
expect(app.updatedAt).toEqual("2021-01-01T00:00:00.000Z")
const updatedApp = await tk.withFreeze(
"2021-02-01",
async () =>
await config.api.application.update(app.appId, {
name: "UPDATED_NAME",
})
)
expect(updatedApp._rev).toBeDefined()
expect(updatedApp.updatedAt).toEqual("2021-02-01T00:00:00.000Z")
const fetchedApp = await config.api.application.get(app.appId)
expect(fetchedApp.updatedAt).toEqual("2021-02-01T00:00:00.000Z")
})
})
describe("sync", () => {
it("app should sync correctly", async () => {
const res = await request
.post(`/api/applications/${config.getAppId()}/sync`)
.set(config.defaultHeaders())
.expect("Content-Type", /json/)
.expect(200)
expect(res.body.message).toEqual("App sync completed successfully.")
const { message } = await config.api.application.sync(app.appId)
expect(message).toEqual("App sync completed successfully.")
})
it("app should not sync if production", async () => {
const res = await request
.post(`/api/applications/app_123456/sync`)
.set(config.defaultHeaders())
.expect("Content-Type", /json/)
.expect(400)
expect(res.body.message).toEqual(
const { message } = await config.api.application.sync(
app.appId.replace("_dev", ""),
{ statusCode: 400 }
)
expect(message).toEqual(
"This action cannot be performed for production apps"
)
})
it("app should not sync if sync is disabled", async () => {
env._set("DISABLE_AUTO_PROD_APP_SYNC", true)
const res = await request
.post(`/api/applications/${config.getAppId()}/sync`)
.set(config.defaultHeaders())
.expect("Content-Type", /json/)
.expect(200)
expect(res.body.message).toEqual(
const { message } = await config.api.application.sync(app.appId)
expect(message).toEqual(
"App sync disabled. You can reenable with the DISABLE_AUTO_PROD_APP_SYNC environment variable."
)
env._set("DISABLE_AUTO_PROD_APP_SYNC", false)
@ -275,51 +203,26 @@ describe("/applications", () => {
})
describe("unpublish", () => {
beforeEach(async () => {
// We want to republish as the unpublish will delete the prod app
await config.publish()
})
it("should unpublish app with dev app ID", async () => {
const appId = config.getAppId()
await request
.post(`/api/applications/${appId}/unpublish`)
.set(config.defaultHeaders())
.expect(204)
await config.api.application.unpublish(app.appId)
expect(events.app.unpublished).toBeCalledTimes(1)
})
it("should unpublish app with prod app ID", async () => {
const appId = config.getProdAppId()
await request
.post(`/api/applications/${appId}/unpublish`)
.set(config.defaultHeaders())
.expect(204)
await config.api.application.unpublish(app.appId.replace("_dev", ""))
expect(events.app.unpublished).toBeCalledTimes(1)
})
})
describe("delete", () => {
it("should delete published app and dev apps with dev app ID", async () => {
await config.createApp("to-delete")
const appId = config.getAppId()
await request
.delete(`/api/applications/${appId}`)
.set(config.defaultHeaders())
.expect("Content-Type", /json/)
.expect(200)
await config.api.application.delete(app.appId)
expect(events.app.deleted).toBeCalledTimes(1)
expect(events.app.unpublished).toBeCalledTimes(1)
})
it("should delete published app and dev app with prod app ID", async () => {
await config.createApp("to-delete")
const appId = config.getProdAppId()
await request
.delete(`/api/applications/${appId}`)
.set(config.defaultHeaders())
.expect("Content-Type", /json/)
.expect(200)
await config.api.application.delete(app.appId.replace("_dev", ""))
expect(events.app.deleted).toBeCalledTimes(1)
expect(events.app.unpublished).toBeCalledTimes(1)
})
@ -327,28 +230,18 @@ describe("/applications", () => {
describe("POST /api/applications/:appId/sync", () => {
it("should not sync automation logs", async () => {
// setup the apps
await config.createApp("testing-auto-logs")
const automation = await config.createAutomation()
await config.publish()
await context.doInAppContext(config.getProdAppId(), () => {
return config.createAutomationLog(automation)
})
await context.doInAppContext(app.appId, () =>
config.createAutomationLog(automation)
)
// do the sync
const appId = config.getAppId()
await request
.post(`/api/applications/${appId}/sync`)
.set(config.defaultHeaders())
.expect("Content-Type", /json/)
.expect(200)
await config.api.application.sync(app.appId)
// does exist in prod
const prodLogs = await config.getAutomationLogs()
expect(prodLogs.data.length).toBe(1)
// delete prod app so we revert to dev log search
await config.unpublish()
await config.api.application.unpublish(app.appId)
// doesn't exist in dev
const devLogs = await config.getAutomationLogs()

View File

@ -76,13 +76,16 @@ const environment = {
DEFAULTS.AUTOMATION_THREAD_TIMEOUT > QUERY_THREAD_TIMEOUT
? DEFAULTS.AUTOMATION_THREAD_TIMEOUT
: QUERY_THREAD_TIMEOUT,
SQL_MAX_ROWS: process.env.SQL_MAX_ROWS,
BB_ADMIN_USER_EMAIL: process.env.BB_ADMIN_USER_EMAIL,
BB_ADMIN_USER_PASSWORD: process.env.BB_ADMIN_USER_PASSWORD,
PLUGINS_DIR: process.env.PLUGINS_DIR || DEFAULTS.PLUGINS_DIR,
OPENAI_API_KEY: process.env.OPENAI_API_KEY,
MAX_IMPORT_SIZE_MB: process.env.MAX_IMPORT_SIZE_MB,
SESSION_EXPIRY_SECONDS: process.env.SESSION_EXPIRY_SECONDS,
// SQL
SQL_MAX_ROWS: process.env.SQL_MAX_ROWS,
SQL_LOGGING_ENABLE: process.env.SQL_LOGGING_ENABLE,
SQL_ALIASING_DISABLE: process.env.SQL_ALIASING_DISABLE,
// flags
ALLOW_DEV_AUTOMATIONS: process.env.ALLOW_DEV_AUTOMATIONS,
DISABLE_THREADING: process.env.DISABLE_THREADING,

View File

@ -1,11 +1,15 @@
import { QueryJson, Datasource } from "@budibase/types"
import {
QueryJson,
Datasource,
DatasourcePlusQueryResponse,
} from "@budibase/types"
import { getIntegration } from "../index"
import sdk from "../../sdk"
export async function makeExternalQuery(
datasource: Datasource,
json: QueryJson
) {
): Promise<DatasourcePlusQueryResponse> {
datasource = await sdk.datasources.enrich(datasource)
const Integration = await getIntegration(datasource.source)
// query is the opinionated function

View File

@ -17,7 +17,6 @@ const envLimit = environment.SQL_MAX_ROWS
: null
const BASE_LIMIT = envLimit || 5000
type KnexQuery = Knex.QueryBuilder | Knex
// these are invalid dates sent by the client, need to convert them to a real max date
const MIN_ISO_DATE = "0000-00-00T00:00:00.000Z"
const MAX_ISO_DATE = "9999-00-00T00:00:00.000Z"
@ -130,10 +129,15 @@ class InternalBuilder {
// right now we only do filters on the specific table being queried
addFilters(
query: KnexQuery,
query: Knex.QueryBuilder,
filters: SearchFilters | undefined,
opts: { relationship?: boolean; tableName?: string }
): KnexQuery {
tableName: string,
opts: { aliases?: Record<string, string>; relationship?: boolean }
): Knex.QueryBuilder {
function getTableName(name: string) {
const alias = opts.aliases?.[name]
return alias || name
}
function iterate(
structure: { [key: string]: any },
fn: (key: string, value: any) => void
@ -142,10 +146,11 @@ class InternalBuilder {
const updatedKey = dbCore.removeKeyNumbering(key)
const isRelationshipField = updatedKey.includes(".")
if (!opts.relationship && !isRelationshipField) {
fn(`${opts.tableName}.${updatedKey}`, value)
fn(`${getTableName(tableName)}.${updatedKey}`, value)
}
if (opts.relationship && isRelationshipField) {
fn(updatedKey, value)
const [filterTableName, property] = updatedKey.split(".")
fn(`${getTableName(filterTableName)}.${property}`, value)
}
}
}
@ -317,7 +322,7 @@ class InternalBuilder {
return query
}
addSorting(query: KnexQuery, json: QueryJson): KnexQuery {
addSorting(query: Knex.QueryBuilder, json: QueryJson): Knex.QueryBuilder {
let { sort, paginate } = json
const table = json.meta?.table
if (sort && Object.keys(sort || {}).length > 0) {
@ -333,16 +338,28 @@ class InternalBuilder {
return query
}
tableNameWithSchema(
tableName: string,
opts?: { alias?: string; schema?: string }
) {
let withSchema = opts?.schema ? `${opts.schema}.${tableName}` : tableName
if (opts?.alias) {
withSchema += ` as ${opts.alias}`
}
return withSchema
}
addRelationships(
query: KnexQuery,
query: Knex.QueryBuilder,
fromTable: string,
relationships: RelationshipsJson[] | undefined,
schema: string | undefined
): KnexQuery {
schema: string | undefined,
aliases?: Record<string, string>
): Knex.QueryBuilder {
if (!relationships) {
return query
}
const tableSets: Record<string, [any]> = {}
const tableSets: Record<string, [RelationshipsJson]> = {}
// aggregate into table sets (all the same to tables)
for (let relationship of relationships) {
const keyObj: { toTable: string; throughTable: string | undefined } = {
@ -361,10 +378,17 @@ class InternalBuilder {
}
for (let [key, relationships] of Object.entries(tableSets)) {
const { toTable, throughTable } = JSON.parse(key)
const toTableWithSchema = schema ? `${schema}.${toTable}` : toTable
const throughTableWithSchema = schema
? `${schema}.${throughTable}`
: throughTable
const toAlias = aliases?.[toTable] || toTable,
throughAlias = aliases?.[throughTable] || throughTable,
fromAlias = aliases?.[fromTable] || fromTable
let toTableWithSchema = this.tableNameWithSchema(toTable, {
alias: toAlias,
schema,
})
let throughTableWithSchema = this.tableNameWithSchema(throughTable, {
alias: throughAlias,
schema,
})
if (!throughTable) {
// @ts-ignore
query = query.leftJoin(toTableWithSchema, function () {
@ -372,7 +396,7 @@ class InternalBuilder {
const from = relationship.from,
to = relationship.to
// @ts-ignore
this.orOn(`${fromTable}.${from}`, "=", `${toTable}.${to}`)
this.orOn(`${fromAlias}.${from}`, "=", `${toAlias}.${to}`)
}
})
} else {
@ -384,9 +408,9 @@ class InternalBuilder {
const from = relationship.from
// @ts-ignore
this.orOn(
`${fromTable}.${fromPrimary}`,
`${fromAlias}.${fromPrimary}`,
"=",
`${throughTable}.${from}`
`${throughAlias}.${from}`
)
}
})
@ -395,7 +419,7 @@ class InternalBuilder {
const toPrimary = relationship.toPrimary
const to = relationship.to
// @ts-ignore
this.orOn(`${toTable}.${toPrimary}`, `${throughTable}.${to}`)
this.orOn(`${toAlias}.${toPrimary}`, `${throughAlias}.${to}`)
}
})
}
@ -403,12 +427,25 @@ class InternalBuilder {
return query.limit(BASE_LIMIT)
}
create(knex: Knex, json: QueryJson, opts: QueryOptions): KnexQuery {
const { endpoint, body } = json
let query: KnexQuery = knex(endpoint.entityId)
knexWithAlias(
knex: Knex,
endpoint: QueryJson["endpoint"],
aliases?: QueryJson["tableAliases"]
): Knex.QueryBuilder {
const tableName = endpoint.entityId
const tableAliased = aliases?.[tableName]
? `${tableName} as ${aliases?.[tableName]}`
: tableName
let query = knex(tableAliased)
if (endpoint.schema) {
query = query.withSchema(endpoint.schema)
}
return query
}
create(knex: Knex, json: QueryJson, opts: QueryOptions): Knex.QueryBuilder {
const { endpoint, body } = json
let query = this.knexWithAlias(knex, endpoint)
const parsedBody = parseBody(body)
// make sure no null values in body for creation
for (let [key, value] of Object.entries(parsedBody)) {
@ -425,12 +462,9 @@ class InternalBuilder {
}
}
bulkCreate(knex: Knex, json: QueryJson): KnexQuery {
bulkCreate(knex: Knex, json: QueryJson): Knex.QueryBuilder {
const { endpoint, body } = json
let query: KnexQuery = knex(endpoint.entityId)
if (endpoint.schema) {
query = query.withSchema(endpoint.schema)
}
let query = this.knexWithAlias(knex, endpoint)
if (!Array.isArray(body)) {
return query
}
@ -438,8 +472,10 @@ class InternalBuilder {
return query.insert(parsedBody)
}
read(knex: Knex, json: QueryJson, limit: number): KnexQuery {
let { endpoint, resource, filters, paginate, relationships } = json
read(knex: Knex, json: QueryJson, limit: number): Knex.QueryBuilder {
let { endpoint, resource, filters, paginate, relationships, tableAliases } =
json
const tableName = endpoint.entityId
// select all if not specified
if (!resource) {
@ -465,21 +501,20 @@ class InternalBuilder {
foundLimit = paginate.limit
}
// start building the query
let query: KnexQuery = knex(tableName).limit(foundLimit)
if (endpoint.schema) {
query = query.withSchema(endpoint.schema)
}
let query = this.knexWithAlias(knex, endpoint, tableAliases)
query = query.limit(foundLimit)
if (foundOffset) {
query = query.offset(foundOffset)
}
query = this.addFilters(query, filters, { tableName })
query = this.addFilters(query, filters, tableName, {
aliases: tableAliases,
})
// add sorting to pre-query
query = this.addSorting(query, json)
// @ts-ignore
let preQuery: KnexQuery = knex({
// @ts-ignore
[tableName]: query,
}).select(selectStatement)
const alias = tableAliases?.[tableName] || tableName
let preQuery = knex({
[alias]: query,
} as any).select(selectStatement) as any
// have to add after as well (this breaks MS-SQL)
if (this.client !== SqlClient.MS_SQL) {
preQuery = this.addSorting(preQuery, json)
@ -489,19 +524,22 @@ class InternalBuilder {
preQuery,
tableName,
relationships,
endpoint.schema
endpoint.schema,
tableAliases
)
return this.addFilters(query, filters, { relationship: true })
return this.addFilters(query, filters, tableName, {
relationship: true,
aliases: tableAliases,
})
}
update(knex: Knex, json: QueryJson, opts: QueryOptions): KnexQuery {
const { endpoint, body, filters } = json
let query: KnexQuery = knex(endpoint.entityId)
if (endpoint.schema) {
query = query.withSchema(endpoint.schema)
}
update(knex: Knex, json: QueryJson, opts: QueryOptions): Knex.QueryBuilder {
const { endpoint, body, filters, tableAliases } = json
let query = this.knexWithAlias(knex, endpoint, tableAliases)
const parsedBody = parseBody(body)
query = this.addFilters(query, filters, { tableName: endpoint.entityId })
query = this.addFilters(query, filters, endpoint.entityId, {
aliases: tableAliases,
})
// mysql can't use returning
if (opts.disableReturning) {
return query.update(parsedBody)
@ -510,13 +548,12 @@ class InternalBuilder {
}
}
delete(knex: Knex, json: QueryJson, opts: QueryOptions): KnexQuery {
const { endpoint, filters } = json
let query: KnexQuery = knex(endpoint.entityId)
if (endpoint.schema) {
query = query.withSchema(endpoint.schema)
}
query = this.addFilters(query, filters, { tableName: endpoint.entityId })
delete(knex: Knex, json: QueryJson, opts: QueryOptions): Knex.QueryBuilder {
const { endpoint, filters, tableAliases } = json
let query = this.knexWithAlias(knex, endpoint, tableAliases)
query = this.addFilters(query, filters, endpoint.entityId, {
aliases: tableAliases,
})
// mysql can't use returning
if (opts.disableReturning) {
return query.delete()
@ -540,7 +577,10 @@ class SqlQueryBuilder extends SqlTableQueryBuilder {
* which for the sake of mySQL stops adding the returning statement to inserts, updates and deletes.
* @return the query ready to be passed to the driver.
*/
_query(json: QueryJson, opts: QueryOptions = {}) {
_query(
json: QueryJson,
opts: QueryOptions = {}
): Knex.SqlNative | Knex.Sql | string {
const sqlClient = this.getSqlClient()
const config: { client: string; useNullAsDefault?: boolean } = {
client: sqlClient,
@ -549,7 +589,7 @@ class SqlQueryBuilder extends SqlTableQueryBuilder {
config.useNullAsDefault = true
}
const client = knex(config)
let query
let query: Knex.QueryBuilder
const builder = new InternalBuilder(sqlClient)
switch (this._operation(json)) {
case Operation.CREATE:
@ -578,7 +618,6 @@ class SqlQueryBuilder extends SqlTableQueryBuilder {
if (opts?.disablePreparedStatements) {
return query.toString()
} else {
// @ts-ignore
return query.toSQL().toNative()
}
}
@ -661,6 +700,18 @@ class SqlQueryBuilder extends SqlTableQueryBuilder {
}
return results.length ? results : [{ [operation.toLowerCase()]: true }]
}
log(query: string, values?: any[]) {
if (!environment.SQL_LOGGING_ENABLE) {
return
}
const sqlClient = this.getSqlClient()
let string = `[SQL] [${sqlClient.toUpperCase()}] query="${query}"`
if (values) {
string += ` values="${values.join(", ")}"`
}
console.log(string)
}
}
export default SqlQueryBuilder

View File

@ -9,7 +9,7 @@ import {
Table,
FieldType,
} from "@budibase/types"
import { breakExternalTableId } from "../utils"
import { breakExternalTableId, SqlClient } from "../utils"
import SchemaBuilder = Knex.SchemaBuilder
import CreateTableBuilder = Knex.CreateTableBuilder
import { utils } from "@budibase/shared-core"
@ -135,7 +135,8 @@ function generateSchema(
// need to check if any columns have been deleted
if (oldTable) {
const deletedColumns = Object.entries(oldTable.schema).filter(
([key, column]) => isIgnoredType(column.type) && table.schema[key] == null
([key, column]) =>
!isIgnoredType(column.type) && table.schema[key] == null
)
deletedColumns.forEach(([key, column]) => {
if (renamed?.old === key || isIgnoredType(column.type)) {
@ -197,13 +198,14 @@ class SqlTableQueryBuilder {
return json.endpoint.operation
}
_tableQuery(json: QueryJson): any {
_tableQuery(json: QueryJson): Knex.Sql | Knex.SqlNative {
let client = knex({ client: this.sqlClient }).schema
if (json?.endpoint?.schema) {
client = client.withSchema(json.endpoint.schema)
let schemaName = json?.endpoint?.schema
if (schemaName) {
client = client.withSchema(schemaName)
}
let query
let query: Knex.SchemaBuilder
if (!json.table || !json.meta || !json.meta.tables) {
throw "Cannot execute without table being specified"
}
@ -215,6 +217,18 @@ class SqlTableQueryBuilder {
if (!json.meta || !json.meta.table) {
throw "Must specify old table for update"
}
// renameColumn does not work for MySQL, so return a raw query
if (this.sqlClient === SqlClient.MY_SQL && json.meta.renamed) {
const updatedColumn = json.meta.renamed.updated
const tableName = schemaName
? `\`${schemaName}\`.\`${json.table.name}\``
: `\`${json.table.name}\``
const externalType = json.table.schema[updatedColumn].externalType!
return {
sql: `alter table ${tableName} change column \`${json.meta.renamed.old}\` \`${updatedColumn}\` ${externalType};`,
bindings: [],
}
}
query = buildUpdateTable(
client,
json.table,

View File

@ -16,6 +16,7 @@ import {
Table,
TableRequest,
TableSourceType,
DatasourcePlusQueryResponse,
} from "@budibase/types"
import { OAuth2Client } from "google-auth-library"
import {
@ -334,7 +335,7 @@ class GoogleSheetsIntegration implements DatasourcePlus {
return { tables: externalTables, errors }
}
async query(json: QueryJson) {
async query(json: QueryJson): Promise<DatasourcePlusQueryResponse> {
const sheet = json.endpoint.entityId
switch (json.endpoint.operation) {
case Operation.CREATE:
@ -384,7 +385,7 @@ class GoogleSheetsIntegration implements DatasourcePlus {
}
try {
await this.connect()
return await this.client.addSheet({ title: name, headerValues: [name] })
await this.client.addSheet({ title: name, headerValues: [name] })
} catch (err) {
console.error("Error creating new table in google sheets", err)
throw err
@ -450,7 +451,7 @@ class GoogleSheetsIntegration implements DatasourcePlus {
try {
await this.connect()
const sheetToDelete = this.client.sheetsByTitle[sheet]
return await sheetToDelete.delete()
await sheetToDelete.delete()
} catch (err) {
console.error("Error deleting table in google sheets", err)
throw err

View File

@ -13,6 +13,7 @@ import {
SourceName,
Schema,
TableSourceType,
DatasourcePlusQueryResponse,
} from "@budibase/types"
import {
getSqlQuery,
@ -329,6 +330,7 @@ class SqlServerIntegration extends Sql implements DatasourcePlus {
operation === Operation.CREATE
? `${query.sql}; SELECT SCOPE_IDENTITY() AS id;`
: query.sql
this.log(sql, query.bindings)
return await request.query(sql)
} catch (err: any) {
let readableMessage = getReadableErrorMessage(
@ -492,7 +494,7 @@ class SqlServerIntegration extends Sql implements DatasourcePlus {
return response.recordset || [{ deleted: true }]
}
async query(json: QueryJson) {
async query(json: QueryJson): Promise<DatasourcePlusQueryResponse> {
const schema = this.config.schema
await this.connect()
if (schema && schema !== DEFAULT_SCHEMA && json?.endpoint) {

View File

@ -12,7 +12,7 @@ import {
SourceName,
Schema,
TableSourceType,
FieldType,
DatasourcePlusQueryResponse,
} from "@budibase/types"
import {
getSqlQuery,
@ -261,6 +261,7 @@ class MySQLIntegration extends Sql implements DatasourcePlus {
const bindings = opts?.disableCoercion
? baseBindings
: bindingTypeCoerce(baseBindings)
this.log(query.sql, bindings)
// Node MySQL is callback based, so we must wrap our call in a promise
const response = await this.client!.query(query.sql, bindings)
return response[0]
@ -380,7 +381,7 @@ class MySQLIntegration extends Sql implements DatasourcePlus {
return results.length ? results : [{ deleted: true }]
}
async query(json: QueryJson) {
async query(json: QueryJson): Promise<DatasourcePlusQueryResponse> {
await this.connect()
try {
const queryFn = (query: any) =>

View File

@ -12,6 +12,8 @@ import {
ConnectionInfo,
Schema,
TableSourceType,
Row,
DatasourcePlusQueryResponse,
} from "@budibase/types"
import {
buildExternalTableId,
@ -368,6 +370,7 @@ class OracleIntegration extends Sql implements DatasourcePlus {
const options: ExecuteOptions = { autoCommit: true }
const bindings: BindParameters = query.bindings || []
this.log(query.sql, bindings)
return await connection.execute<T>(query.sql, bindings, options)
} finally {
if (connection) {
@ -419,9 +422,9 @@ class OracleIntegration extends Sql implements DatasourcePlus {
: [{ deleted: true }]
}
async query(json: QueryJson) {
async query(json: QueryJson): Promise<DatasourcePlusQueryResponse> {
const operation = this._operation(json)
const input = this._query(json, { disableReturning: true })
const input = this._query(json, { disableReturning: true }) as SqlQuery
if (Array.isArray(input)) {
const responses = []
for (let query of input) {
@ -443,7 +446,7 @@ class OracleIntegration extends Sql implements DatasourcePlus {
if (deletedRows?.rows?.length) {
return deletedRows.rows
} else if (response.rows?.length) {
return response.rows
return response.rows as Row[]
} else {
// get the last row that was updated
if (
@ -454,7 +457,7 @@ class OracleIntegration extends Sql implements DatasourcePlus {
const lastRow = await this.internalQuery({
sql: `SELECT * FROM \"${json.endpoint.entityId}\" WHERE ROWID = '${response.lastRowid}'`,
})
return lastRow.rows
return lastRow.rows as Row[]
} else {
return [{ [operation.toLowerCase()]: true }]
}

View File

@ -12,6 +12,7 @@ import {
SourceName,
Schema,
TableSourceType,
DatasourcePlusQueryResponse,
} from "@budibase/types"
import {
getSqlQuery,
@ -268,7 +269,9 @@ class PostgresIntegration extends Sql implements DatasourcePlus {
}
}
try {
return await client.query(query.sql, query.bindings || [])
const bindings = query.bindings || []
this.log(query.sql, bindings)
return await client.query(query.sql, bindings)
} catch (err: any) {
await this.closeConnection()
let readableMessage = getReadableErrorMessage(
@ -417,9 +420,9 @@ class PostgresIntegration extends Sql implements DatasourcePlus {
return response.rows.length ? response.rows : [{ deleted: true }]
}
async query(json: QueryJson) {
async query(json: QueryJson): Promise<DatasourcePlusQueryResponse> {
const operation = this._operation(json).toLowerCase()
const input = this._query(json)
const input = this._query(json) as SqlQuery
if (Array.isArray(input)) {
const responses = []
for (let query of input) {

View File

@ -1,5 +1,12 @@
const Sql = require("../base/sql").default
const { SqlClient } = require("../utils")
import { SqlClient } from "../utils"
import Sql from "../base/sql"
import {
Operation,
QueryJson,
TableSourceType,
Table,
FieldType,
} from "@budibase/types"
const TABLE_NAME = "test"
@ -17,7 +24,7 @@ function generateReadJson({
filters,
sort,
paginate,
}: any = {}) {
}: any = {}): QueryJson {
return {
endpoint: endpoint(table || TABLE_NAME, "READ"),
resource: {
@ -28,41 +35,51 @@ function generateReadJson({
paginate: paginate || {},
meta: {
table: {
type: "table",
sourceType: TableSourceType.EXTERNAL,
sourceId: "SOURCE_ID",
schema: {},
name: table || TABLE_NAME,
primary: ["id"],
},
} as any,
},
}
}
function generateCreateJson(table = TABLE_NAME, body = {}) {
function generateCreateJson(table = TABLE_NAME, body = {}): QueryJson {
return {
endpoint: endpoint(table, "CREATE"),
body,
}
}
function generateUpdateJson(table = TABLE_NAME, body = {}, filters = {}) {
function generateUpdateJson({
table = TABLE_NAME,
body = {},
filters = {},
meta = {},
}): QueryJson {
return {
endpoint: endpoint(table, "UPDATE"),
filters,
body,
meta,
}
}
function generateDeleteJson(table = TABLE_NAME, filters = {}) {
function generateDeleteJson(table = TABLE_NAME, filters = {}): QueryJson {
return {
endpoint: endpoint(table, "DELETE"),
filters,
}
}
function generateRelationshipJson(config: { schema?: string } = {}) {
function generateRelationshipJson(config: { schema?: string } = {}): QueryJson {
return {
endpoint: {
datasourceId: "Postgres",
entityId: "brands",
operation: "READ",
operation: Operation.READ,
schema: config.schema,
},
resource: {
@ -76,7 +93,6 @@ function generateRelationshipJson(config: { schema?: string } = {}) {
},
filters: {},
sort: {},
paginate: {},
relationships: [
{
from: "brand_id",
@ -240,17 +256,17 @@ describe("SQL query builder", () => {
it("should test an update statement", () => {
const query = sql._query(
generateUpdateJson(
TABLE_NAME,
{
generateUpdateJson({
table: TABLE_NAME,
body: {
name: "John",
},
{
filters: {
equal: {
id: 1001,
},
}
)
},
})
)
expect(query).toEqual({
bindings: ["John", 1001],
@ -502,7 +518,7 @@ describe("SQL query builder", () => {
const query = sql._query(generateRelationshipJson({ schema: "production" }))
expect(query).toEqual({
bindings: [500, 5000],
sql: `select "brands"."brand_id" as "brands.brand_id", "brands"."brand_name" as "brands.brand_name", "products"."product_id" as "products.product_id", "products"."product_name" as "products.product_name", "products"."brand_id" as "products.brand_id" from (select * from "production"."brands" limit $1) as "brands" left join "production"."products" on "brands"."brand_id" = "products"."brand_id" limit $2`,
sql: `select "brands"."brand_id" as "brands.brand_id", "brands"."brand_name" as "brands.brand_name", "products"."product_id" as "products.product_id", "products"."product_name" as "products.product_name", "products"."brand_id" as "products.brand_id" from (select * from "production"."brands" limit $1) as "brands" left join "production"."products" as "products" on "brands"."brand_id" = "products"."brand_id" limit $2`,
})
})
@ -510,7 +526,7 @@ describe("SQL query builder", () => {
const query = sql._query(generateRelationshipJson())
expect(query).toEqual({
bindings: [500, 5000],
sql: `select "brands"."brand_id" as "brands.brand_id", "brands"."brand_name" as "brands.brand_name", "products"."product_id" as "products.product_id", "products"."product_name" as "products.product_name", "products"."brand_id" as "products.brand_id" from (select * from "brands" limit $1) as "brands" left join "products" on "brands"."brand_id" = "products"."brand_id" limit $2`,
sql: `select "brands"."brand_id" as "brands.brand_id", "brands"."brand_name" as "brands.brand_name", "products"."product_id" as "products.product_id", "products"."product_name" as "products.product_name", "products"."brand_id" as "products.brand_id" from (select * from "brands" limit $1) as "brands" left join "products" as "products" on "brands"."brand_id" = "products"."brand_id" limit $2`,
})
})
@ -520,7 +536,7 @@ describe("SQL query builder", () => {
)
expect(query).toEqual({
bindings: [500, 5000],
sql: `select "stores"."store_id" as "stores.store_id", "stores"."store_name" as "stores.store_name", "products"."product_id" as "products.product_id", "products"."product_name" as "products.product_name" from (select * from "production"."stores" limit $1) as "stores" left join "production"."stocks" on "stores"."store_id" = "stocks"."store_id" left join "production"."products" on "products"."product_id" = "stocks"."product_id" limit $2`,
sql: `select "stores"."store_id" as "stores.store_id", "stores"."store_name" as "stores.store_name", "products"."product_id" as "products.product_id", "products"."product_name" as "products.product_name" from (select * from "production"."stores" limit $1) as "stores" left join "production"."stocks" as "stocks" on "stores"."store_id" = "stocks"."store_id" left join "production"."products" as "products" on "products"."product_id" = "stocks"."product_id" limit $2`,
})
})
@ -682,4 +698,99 @@ describe("SQL query builder", () => {
sql: `insert into \"test\" (\"name\") values ($1) returning *`,
})
})
it("should be able to rename column for MySQL", () => {
const table: Table = {
type: "table",
sourceType: TableSourceType.EXTERNAL,
name: TABLE_NAME,
schema: {
first_name: {
type: FieldType.STRING,
name: "first_name",
externalType: "varchar(45)",
},
},
sourceId: "SOURCE_ID",
}
const oldTable: Table = {
...table,
schema: {
name: {
type: FieldType.STRING,
name: "name",
externalType: "varchar(45)",
},
},
}
const query = new Sql(SqlClient.MY_SQL, limit)._query({
table,
endpoint: {
datasourceId: "MySQL",
operation: Operation.UPDATE_TABLE,
entityId: TABLE_NAME,
},
meta: {
table: oldTable,
tables: { [oldTable.name]: oldTable },
renamed: {
old: "name",
updated: "first_name",
},
},
})
expect(query).toEqual({
bindings: [],
sql: `alter table \`${TABLE_NAME}\` change column \`name\` \`first_name\` varchar(45);`,
})
})
it("should be able to delete a column", () => {
const table: Table = {
type: "table",
sourceType: TableSourceType.EXTERNAL,
name: TABLE_NAME,
schema: {
first_name: {
type: FieldType.STRING,
name: "first_name",
externalType: "varchar(45)",
},
},
sourceId: "SOURCE_ID",
}
const oldTable: Table = {
...table,
schema: {
first_name: {
type: FieldType.STRING,
name: "first_name",
externalType: "varchar(45)",
},
last_name: {
type: FieldType.STRING,
name: "last_name",
externalType: "varchar(45)",
},
},
}
const query = sql._query({
table,
endpoint: {
datasourceId: "Postgres",
operation: Operation.UPDATE_TABLE,
entityId: TABLE_NAME,
},
meta: {
table: oldTable,
tables: [oldTable],
},
})
expect(query).toEqual([
{
bindings: [],
sql: `alter table "${TABLE_NAME}" drop column "last_name"`,
},
])
})
})

View File

@ -0,0 +1,204 @@
import { QueryJson } from "@budibase/types"
import { join } from "path"
import Sql from "../base/sql"
import { SqlClient } from "../utils"
import AliasTables from "../../api/controllers/row/alias"
import { generator } from "@budibase/backend-core/tests"
function multiline(sql: string) {
return sql.replace(/\n/g, "").replace(/ +/g, " ")
}
describe("Captures of real examples", () => {
const limit = 5000
const relationshipLimit = 100
function getJson(name: string): QueryJson {
return require(join(__dirname, "sqlQueryJson", name)) as QueryJson
}
describe("create", () => {
it("should create a row with relationships", () => {
const queryJson = getJson("createWithRelationships.json")
let query = new Sql(SqlClient.POSTGRES, limit)._query(queryJson)
expect(query).toEqual({
bindings: ["A Street", 34, "London", "A", "B", "designer", 1990],
sql: multiline(`insert into "persons" ("address", "age", "city", "firstname", "lastname", "type", "year")
values ($1, $2, $3, $4, $5, $6, $7) returning *`),
})
})
})
describe("read", () => {
it("should handle basic retrieval with relationships", () => {
const queryJson = getJson("basicFetchWithRelationships.json")
let query = new Sql(SqlClient.POSTGRES, limit)._query(queryJson)
expect(query).toEqual({
bindings: [relationshipLimit, limit],
sql: multiline(`select "a"."year" as "a.year", "a"."firstname" as "a.firstname", "a"."personid" as "a.personid",
"a"."address" as "a.address", "a"."age" as "a.age", "a"."type" as "a.type", "a"."city" as "a.city",
"a"."lastname" as "a.lastname", "b"."executorid" as "b.executorid", "b"."taskname" as "b.taskname",
"b"."taskid" as "b.taskid", "b"."completed" as "b.completed", "b"."qaid" as "b.qaid",
"b"."executorid" as "b.executorid", "b"."taskname" as "b.taskname", "b"."taskid" as "b.taskid",
"b"."completed" as "b.completed", "b"."qaid" as "b.qaid"
from (select * from "persons" as "a" order by "a"."firstname" asc limit $1) as "a"
left join "tasks" as "b" on "a"."personid" = "b"."qaid" or "a"."personid" = "b"."executorid"
order by "a"."firstname" asc limit $2`),
})
})
it("should handle filtering by relationship", () => {
const queryJson = getJson("filterByRelationship.json")
let query = new Sql(SqlClient.POSTGRES, limit)._query(queryJson)
expect(query).toEqual({
bindings: [relationshipLimit, "assembling", limit],
sql: multiline(`select "a"."productname" as "a.productname", "a"."productid" as "a.productid",
"b"."executorid" as "b.executorid", "b"."taskname" as "b.taskname", "b"."taskid" as "b.taskid",
"b"."completed" as "b.completed", "b"."qaid" as "b.qaid"
from (select * from "products" as "a" order by "a"."productname" asc limit $1) as "a"
left join "products_tasks" as "c" on "a"."productid" = "c"."productid"
left join "tasks" as "b" on "b"."taskid" = "c"."taskid" where "b"."taskname" = $2
order by "a"."productname" asc limit $3`),
})
})
it("should handle fetching many to many relationships", () => {
const queryJson = getJson("fetchManyToMany.json")
let query = new Sql(SqlClient.POSTGRES, limit)._query(queryJson)
expect(query).toEqual({
bindings: [relationshipLimit, limit],
sql: multiline(`select "a"."productname" as "a.productname", "a"."productid" as "a.productid",
"b"."executorid" as "b.executorid", "b"."taskname" as "b.taskname", "b"."taskid" as "b.taskid",
"b"."completed" as "b.completed", "b"."qaid" as "b.qaid"
from (select * from "products" as "a" order by "a"."productname" asc limit $1) as "a"
left join "products_tasks" as "c" on "a"."productid" = "c"."productid"
left join "tasks" as "b" on "b"."taskid" = "c"."taskid"
order by "a"."productname" asc limit $2`),
})
})
it("should handle enrichment of rows", () => {
const queryJson = getJson("enrichRelationship.json")
const filters = queryJson.filters?.oneOf?.taskid as number[]
let query = new Sql(SqlClient.POSTGRES, limit)._query(queryJson)
expect(query).toEqual({
bindings: [...filters, limit, limit],
sql: multiline(`select "a"."executorid" as "a.executorid", "a"."taskname" as "a.taskname",
"a"."taskid" as "a.taskid", "a"."completed" as "a.completed", "a"."qaid" as "a.qaid",
"b"."productname" as "b.productname", "b"."productid" as "b.productid"
from (select * from "tasks" as "a" where "a"."taskid" in ($1, $2) limit $3) as "a"
left join "products_tasks" as "c" on "a"."taskid" = "c"."taskid"
left join "products" as "b" on "b"."productid" = "c"."productid" limit $4`),
})
})
it("should manage query with many relationship filters", () => {
const queryJson = getJson("manyRelationshipFilters.json")
let query = new Sql(SqlClient.POSTGRES, limit)._query(queryJson)
const filters = queryJson.filters
const notEqualsValue = Object.values(filters?.notEqual!)[0]
const rangeValue = Object.values(filters?.range!)[0]
const equalValue = Object.values(filters?.equal!)[0]
expect(query).toEqual({
bindings: [
notEqualsValue,
relationshipLimit,
rangeValue.low,
rangeValue.high,
equalValue,
limit,
],
sql: multiline(`select "a"."executorid" as "a.executorid", "a"."taskname" as "a.taskname", "a"."taskid" as "a.taskid",
"a"."completed" as "a.completed", "a"."qaid" as "a.qaid", "b"."productname" as "b.productname",
"b"."productid" as "b.productid", "c"."year" as "c.year", "c"."firstname" as "c.firstname",
"c"."personid" as "c.personid", "c"."address" as "c.address", "c"."age" as "c.age", "c"."type" as "c.type",
"c"."city" as "c.city", "c"."lastname" as "c.lastname", "c"."year" as "c.year", "c"."firstname" as "c.firstname",
"c"."personid" as "c.personid", "c"."address" as "c.address", "c"."age" as "c.age", "c"."type" as "c.type",
"c"."city" as "c.city", "c"."lastname" as "c.lastname"
from (select * from "tasks" as "a" where not "a"."completed" = $1
order by "a"."taskname" asc limit $2) as "a"
left join "products_tasks" as "d" on "a"."taskid" = "d"."taskid"
left join "products" as "b" on "b"."productid" = "d"."productid"
left join "persons" as "c" on "a"."executorid" = "c"."personid" or "a"."qaid" = "c"."personid"
where "c"."year" between $3 and $4 and "b"."productname" = $5 order by "a"."taskname" asc limit $6`),
})
})
})
describe("update", () => {
it("should handle performing a simple update", () => {
const queryJson = getJson("updateSimple.json")
let query = new Sql(SqlClient.POSTGRES, limit)._query(queryJson)
expect(query).toEqual({
bindings: [1990, "C", "A Street", 34, "designer", "London", "B", 5],
sql: multiline(`update "persons" as "a" set "year" = $1, "firstname" = $2, "address" = $3, "age" = $4,
"type" = $5, "city" = $6, "lastname" = $7 where "a"."personid" = $8 returning *`),
})
})
it("should handle performing an update of relationships", () => {
const queryJson = getJson("updateRelationship.json")
let query = new Sql(SqlClient.POSTGRES, limit)._query(queryJson)
expect(query).toEqual({
bindings: [1990, "C", "A Street", 34, "designer", "London", "B", 5],
sql: multiline(`update "persons" as "a" set "year" = $1, "firstname" = $2, "address" = $3, "age" = $4,
"type" = $5, "city" = $6, "lastname" = $7 where "a"."personid" = $8 returning *`),
})
})
})
describe("delete", () => {
it("should handle deleting with relationships", () => {
const queryJson = getJson("deleteSimple.json")
let query = new Sql(SqlClient.POSTGRES, limit)._query(queryJson)
expect(query).toEqual({
bindings: ["ddd", ""],
sql: multiline(`delete from "compositetable" as "a" where "a"."keypartone" = $1 and "a"."keyparttwo" = $2
returning "a"."keyparttwo" as "a.keyparttwo", "a"."keypartone" as "a.keypartone", "a"."name" as "a.name"`),
})
})
})
describe("check max character aliasing", () => {
it("should handle over 'z' max character alias", () => {
const tableNames = []
for (let i = 0; i < 100; i++) {
tableNames.push(generator.guid())
}
const aliasing = new AliasTables(tableNames)
let alias: string = ""
for (let table of tableNames) {
alias = aliasing.getAlias(table)
}
expect(alias).toEqual("cv")
})
})
describe("check some edge cases", () => {
const tableNames = ["hello", "world"]
it("should handle quoted table names", () => {
const aliasing = new AliasTables(tableNames)
const aliased = aliasing.aliasField(`"hello"."field"`)
expect(aliased).toEqual(`"a"."field"`)
})
it("should handle quoted table names with graves", () => {
const aliasing = new AliasTables(tableNames)
const aliased = aliasing.aliasField("`hello`.`world`")
expect(aliased).toEqual("`a`.`world`")
})
it("should handle table names in table names correctly", () => {
const tableNames = ["he", "hell", "hello"]
const aliasing = new AliasTables(tableNames)
const aliased1 = aliasing.aliasField("`he`.`world`")
const aliased2 = aliasing.aliasField("`hell`.`world`")
const aliased3 = aliasing.aliasField("`hello`.`world`")
expect(aliased1).toEqual("`a`.`world`")
expect(aliased2).toEqual("`b`.`world`")
expect(aliased3).toEqual("`c`.`world`")
})
})
})

View File

@ -0,0 +1,183 @@
{
"endpoint": {
"datasourceId": "datasource_plus_8066e56456784eb2a00129d31be5c3e7",
"entityId": "persons",
"operation": "READ"
},
"resource": {
"fields": [
"a.year",
"a.firstname",
"a.personid",
"a.address",
"a.age",
"a.type",
"a.city",
"a.lastname",
"b.executorid",
"b.taskname",
"b.taskid",
"b.completed",
"b.qaid",
"b.executorid",
"b.taskname",
"b.taskid",
"b.completed",
"b.qaid"
]
},
"filters": {},
"sort": {
"firstname": {
"direction": "ASCENDING"
}
},
"paginate": {
"limit": 100,
"page": 1
},
"relationships": [
{
"tableName": "tasks",
"column": "QA",
"from": "personid",
"to": "qaid",
"aliases": {
"tasks": "b",
"persons": "a"
}
},
{
"tableName": "tasks",
"column": "executor",
"from": "personid",
"to": "executorid",
"aliases": {
"tasks": "b",
"persons": "a"
}
}
],
"extra": {
"idFilter": {}
},
"meta": {
"table": {
"type": "table",
"_id": "datasource_plus_8066e56456784eb2a00129d31be5c3e7__persons",
"primary": [
"personid"
],
"name": "a",
"schema": {
"year": {
"type": "number",
"externalType": "integer",
"autocolumn": false,
"name": "year",
"constraints": {
"presence": false
}
},
"firstname": {
"type": "string",
"externalType": "character varying",
"autocolumn": false,
"name": "firstname",
"constraints": {
"presence": false
}
},
"personid": {
"type": "number",
"externalType": "integer",
"autocolumn": true,
"name": "personid",
"constraints": {
"presence": false
}
},
"address": {
"type": "string",
"externalType": "character varying",
"autocolumn": false,
"name": "address",
"constraints": {
"presence": false
}
},
"age": {
"type": "number",
"externalType": "integer",
"autocolumn": false,
"name": "age",
"constraints": {
"presence": false
}
},
"type": {
"type": "options",
"externalType": "USER-DEFINED",
"autocolumn": false,
"name": "type",
"constraints": {
"presence": false,
"inclusion": [
"support",
"designer",
"programmer",
"qa"
]
}
},
"city": {
"type": "string",
"externalType": "character varying",
"autocolumn": false,
"name": "city",
"constraints": {
"presence": false
}
},
"lastname": {
"type": "string",
"externalType": "character varying",
"autocolumn": false,
"name": "lastname",
"constraints": {
"presence": false
}
},
"QA": {
"tableId": "datasource_plus_8066e56456784eb2a00129d31be5c3e7__tasks",
"name": "QA",
"relationshipType": "many-to-one",
"fieldName": "qaid",
"type": "link",
"main": true,
"_id": "ccb68481c80c34217a4540a2c6c27fe46",
"foreignKey": "personid"
},
"executor": {
"tableId": "datasource_plus_8066e56456784eb2a00129d31be5c3e7__tasks",
"name": "executor",
"relationshipType": "many-to-one",
"fieldName": "executorid",
"type": "link",
"main": true,
"_id": "c89530b9770d94bec851e062b5cff3001",
"foreignKey": "personid",
"tableName": "persons"
}
},
"sourceId": "datasource_plus_8066e56456784eb2a00129d31be5c3e7",
"sourceType": "external",
"primaryDisplay": "firstname",
"views": {}
}
},
"tableAliases": {
"persons": "a",
"tasks": "b"
}
}

View File

@ -0,0 +1,173 @@
{
"endpoint": {
"datasourceId": "datasource_plus_8066e56456784eb2a00129d31be5c3e7",
"entityId": "persons",
"operation": "CREATE"
},
"resource": {
"fields": [
"a.year",
"a.firstname",
"a.personid",
"a.address",
"a.age",
"a.type",
"a.city",
"a.lastname"
]
},
"filters": {},
"relationships": [
{
"tableName": "tasks",
"column": "QA",
"from": "personid",
"to": "qaid",
"aliases": {
"tasks": "b",
"persons": "a"
}
},
{
"tableName": "tasks",
"column": "executor",
"from": "personid",
"to": "executorid",
"aliases": {
"tasks": "b",
"persons": "a"
}
}
],
"body": {
"year": 1990,
"firstname": "A",
"address": "A Street",
"age": 34,
"type": "designer",
"city": "London",
"lastname": "B"
},
"extra": {
"idFilter": {}
},
"meta": {
"table": {
"type": "table",
"_id": "datasource_plus_8066e56456784eb2a00129d31be5c3e7__persons",
"primary": [
"personid"
],
"name": "a",
"schema": {
"year": {
"type": "number",
"externalType": "integer",
"autocolumn": false,
"name": "year",
"constraints": {
"presence": false
}
},
"firstname": {
"type": "string",
"externalType": "character varying",
"autocolumn": false,
"name": "firstname",
"constraints": {
"presence": false
}
},
"personid": {
"type": "number",
"externalType": "integer",
"autocolumn": true,
"name": "personid",
"constraints": {
"presence": false
}
},
"address": {
"type": "string",
"externalType": "character varying",
"autocolumn": false,
"name": "address",
"constraints": {
"presence": false
}
},
"age": {
"type": "number",
"externalType": "integer",
"autocolumn": false,
"name": "age",
"constraints": {
"presence": false
}
},
"type": {
"type": "options",
"externalType": "USER-DEFINED",
"autocolumn": false,
"name": "type",
"constraints": {
"presence": false,
"inclusion": [
"support",
"designer",
"programmer",
"qa"
]
}
},
"city": {
"type": "string",
"externalType": "character varying",
"autocolumn": false,
"name": "city",
"constraints": {
"presence": false
}
},
"lastname": {
"type": "string",
"externalType": "character varying",
"autocolumn": false,
"name": "lastname",
"constraints": {
"presence": false
}
},
"QA": {
"tableId": "datasource_plus_8066e56456784eb2a00129d31be5c3e7__tasks",
"name": "QA",
"relationshipType": "many-to-one",
"fieldName": "qaid",
"type": "link",
"main": true,
"_id": "ccb68481c80c34217a4540a2c6c27fe46",
"foreignKey": "personid"
},
"executor": {
"tableId": "datasource_plus_8066e56456784eb2a00129d31be5c3e7__tasks",
"name": "executor",
"relationshipType": "many-to-one",
"fieldName": "executorid",
"type": "link",
"main": true,
"_id": "c89530b9770d94bec851e062b5cff3001",
"foreignKey": "personid",
"tableName": "persons"
}
},
"sourceId": "datasource_plus_8066e56456784eb2a00129d31be5c3e7",
"sourceType": "external",
"primaryDisplay": "firstname",
"views": {}
}
},
"tableAliases": {
"persons": "a",
"tasks": "b"
}
}

View File

@ -0,0 +1,75 @@
{
"endpoint": {
"datasourceId": "datasource_plus_8066e56456784eb2a00129d31be5c3e7",
"entityId": "compositetable",
"operation": "DELETE"
},
"resource": {
"fields": [
"a.keyparttwo",
"a.keypartone",
"a.name"
]
},
"filters": {
"equal": {
"keypartone": "ddd",
"keyparttwo": ""
}
},
"relationships": [],
"extra": {
"idFilter": {
"equal": {
"keypartone": "ddd",
"keyparttwo": ""
}
}
},
"meta": {
"table": {
"type": "table",
"_id": "datasource_plus_8066e56456784eb2a00129d31be5c3e7__compositetable",
"primary": [
"keypartone",
"keyparttwo"
],
"name": "a",
"schema": {
"keyparttwo": {
"type": "string",
"externalType": "character varying",
"autocolumn": false,
"name": "keyparttwo",
"constraints": {
"presence": true
}
},
"keypartone": {
"type": "string",
"externalType": "character varying",
"autocolumn": false,
"name": "keypartone",
"constraints": {
"presence": true
}
},
"name": {
"type": "string",
"externalType": "character varying",
"autocolumn": false,
"name": "name",
"constraints": {
"presence": false
}
}
},
"sourceId": "datasource_plus_8066e56456784eb2a00129d31be5c3e7",
"sourceType": "external",
"primaryDisplay": "keypartone"
}
},
"tableAliases": {
"compositetable": "a"
}
}

View File

@ -0,0 +1,123 @@
{
"endpoint": {
"datasourceId": "datasource_plus_44a967caf37a435f84fe01cd6dfe8f81",
"entityId": "tasks",
"operation": "READ"
},
"resource": {
"fields": [
"a.executorid",
"a.taskname",
"a.taskid",
"a.completed",
"a.qaid",
"b.productname",
"b.productid"
]
},
"filters": {
"oneOf": {
"taskid": [
1,
2
]
}
},
"relationships": [
{
"tableName": "products",
"column": "products",
"through": "products_tasks",
"from": "taskid",
"to": "productid",
"fromPrimary": "taskid",
"toPrimary": "productid",
"aliases": {
"products_tasks": "c",
"products": "b",
"tasks": "a"
}
}
],
"extra": {
"idFilter": {}
},
"meta": {
"table": {
"type": "table",
"_id": "datasource_plus_44a967caf37a435f84fe01cd6dfe8f81__tasks",
"primary": [
"taskid"
],
"name": "a",
"schema": {
"executorid": {
"type": "number",
"externalType": "integer",
"autocolumn": false,
"name": "executorid",
"constraints": {
"presence": false
}
},
"taskname": {
"type": "string",
"externalType": "character varying",
"autocolumn": false,
"name": "taskname",
"constraints": {
"presence": false
}
},
"taskid": {
"type": "number",
"externalType": "integer",
"autocolumn": true,
"name": "taskid",
"constraints": {
"presence": false
}
},
"completed": {
"type": "boolean",
"externalType": "boolean",
"autocolumn": false,
"name": "completed",
"constraints": {
"presence": false
}
},
"qaid": {
"type": "number",
"externalType": "integer",
"autocolumn": false,
"name": "qaid",
"constraints": {
"presence": false
}
},
"products": {
"tableId": "datasource_plus_44a967caf37a435f84fe01cd6dfe8f81__products",
"name": "products",
"relationshipType": "many-to-many",
"through": "datasource_plus_44a967caf37a435f84fe01cd6dfe8f81__products_tasks",
"type": "link",
"_id": "c3b91d00cd36c4cc1a347794725b9adbd",
"fieldName": "productid",
"throughFrom": "productid",
"throughTo": "taskid"
}
},
"sourceId": "datasource_plus_44a967caf37a435f84fe01cd6dfe8f81",
"sourceType": "external",
"primaryDisplay": "taskname",
"sql": true,
"views": {}
}
},
"tableAliases": {
"tasks": "a",
"products": "b",
"products_tasks": "c"
}
}

View File

@ -0,0 +1,109 @@
{
"endpoint": {
"datasourceId": "datasource_plus_44a967caf37a435f84fe01cd6dfe8f81",
"entityId": "products",
"operation": "READ"
},
"resource": {
"fields": [
"a.productname",
"a.productid",
"b.executorid",
"b.taskname",
"b.taskid",
"b.completed",
"b.qaid"
]
},
"filters": {
"string": {},
"fuzzy": {},
"range": {},
"equal": {},
"notEqual": {},
"empty": {},
"notEmpty": {},
"contains": {},
"notContains": {},
"oneOf": {},
"containsAny": {}
},
"sort": {
"productname": {
"direction": "ASCENDING"
}
},
"paginate": {
"limit": 100,
"page": 1
},
"relationships": [
{
"tableName": "tasks",
"column": "tasks",
"through": "products_tasks",
"from": "productid",
"to": "taskid",
"fromPrimary": "productid",
"toPrimary": "taskid",
"aliases": {
"products_tasks": "c",
"tasks": "b",
"products": "a"
}
}
],
"extra": {
"idFilter": {}
},
"meta": {
"table": {
"type": "table",
"_id": "datasource_plus_44a967caf37a435f84fe01cd6dfe8f81__products",
"primary": [
"productid"
],
"name": "a",
"schema": {
"productname": {
"type": "string",
"externalType": "character varying",
"autocolumn": false,
"name": "productname",
"constraints": {
"presence": false
}
},
"productid": {
"type": "number",
"externalType": "integer",
"autocolumn": true,
"name": "productid",
"constraints": {
"presence": false
}
},
"tasks": {
"tableId": "datasource_plus_44a967caf37a435f84fe01cd6dfe8f81__tasks",
"name": "tasks",
"relationshipType": "many-to-many",
"fieldName": "taskid",
"through": "datasource_plus_44a967caf37a435f84fe01cd6dfe8f81__products_tasks",
"throughFrom": "taskid",
"throughTo": "productid",
"type": "link",
"main": true,
"_id": "c3b91d00cd36c4cc1a347794725b9adbd"
}
},
"sourceId": "datasource_plus_44a967caf37a435f84fe01cd6dfe8f81",
"sourceType": "external",
"primaryDisplay": "productname"
}
},
"tableAliases": {
"products": "a",
"tasks": "b",
"products_tasks": "c"
}
}

View File

@ -0,0 +1,94 @@
{
"endpoint": {
"datasourceId": "datasource_plus_8066e56456784eb2a00129d31be5c3e7",
"entityId": "products",
"operation": "READ"
},
"resource": {
"fields": [
"a.productname",
"a.productid",
"b.executorid",
"b.taskname",
"b.taskid",
"b.completed",
"b.qaid"
]
},
"filters": {
"equal": {
"1:tasks.taskname": "assembling"
},
"onEmptyFilter": "all"
},
"sort": {
"productname": {
"direction": "ASCENDING"
}
},
"paginate": {
"limit": 100,
"page": 1
},
"relationships": [
{
"tableName": "tasks",
"column": "tasks",
"through": "products_tasks",
"from": "productid",
"to": "taskid",
"fromPrimary": "productid",
"toPrimary": "taskid"
}
],
"tableAliases": {
"products_tasks": "c",
"tasks": "b",
"products": "a"
},
"meta": {
"table": {
"type": "table",
"_id": "datasource_plus_8066e56456784eb2a00129d31be5c3e7__products",
"primary": [
"productid"
],
"name": "a",
"schema": {
"productname": {
"type": "string",
"externalType": "character varying",
"autocolumn": false,
"name": "productname",
"constraints": {
"presence": false
}
},
"productid": {
"type": "number",
"externalType": "integer",
"autocolumn": true,
"name": "productid",
"constraints": {
"presence": false
}
},
"tasks": {
"tableId": "datasource_plus_8066e56456784eb2a00129d31be5c3e7__tasks",
"name": "tasks",
"relationshipType": "many-to-many",
"fieldName": "taskid",
"through": "datasource_plus_8066e56456784eb2a00129d31be5c3e7__products_tasks",
"throughFrom": "taskid",
"throughTo": "productid",
"type": "link",
"main": true,
"_id": "ca6862d9ba09146dd8a68e3b5b7055a09"
}
},
"sourceId": "datasource_plus_8066e56456784eb2a00129d31be5c3e7",
"sourceType": "external",
"primaryDisplay": "productname"
}
}
}

View File

@ -0,0 +1,202 @@
{
"endpoint": {
"datasourceId": "datasource_plus_44a967caf37a435f84fe01cd6dfe8f81",
"entityId": "tasks",
"operation": "READ"
},
"resource": {
"fields": [
"a.executorid",
"a.taskname",
"a.taskid",
"a.completed",
"a.qaid",
"b.productname",
"b.productid",
"c.year",
"c.firstname",
"c.personid",
"c.address",
"c.age",
"c.type",
"c.city",
"c.lastname",
"c.year",
"c.firstname",
"c.personid",
"c.address",
"c.age",
"c.type",
"c.city",
"c.lastname"
]
},
"filters": {
"string": {},
"fuzzy": {},
"range": {
"1:persons.year": {
"low": 1990,
"high": 2147483647
}
},
"equal": {
"2:products.productname": "Computers"
},
"notEqual": {
"3:completed": true
},
"empty": {},
"notEmpty": {},
"contains": {},
"notContains": {},
"oneOf": {},
"containsAny": {},
"onEmptyFilter": "all"
},
"sort": {
"taskname": {
"direction": "ASCENDING"
}
},
"paginate": {
"limit": 100,
"page": 1
},
"relationships": [
{
"tableName": "products",
"column": "products",
"through": "products_tasks",
"from": "taskid",
"to": "productid",
"fromPrimary": "taskid",
"toPrimary": "productid",
"aliases": {
"products_tasks": "d",
"products": "b",
"tasks": "a"
}
},
{
"tableName": "persons",
"column": "tasksToExecute",
"from": "executorid",
"to": "personid",
"aliases": {
"persons": "c",
"tasks": "a"
}
},
{
"tableName": "persons",
"column": "tasksToQA",
"from": "qaid",
"to": "personid",
"aliases": {
"persons": "c",
"tasks": "a"
}
}
],
"extra": {
"idFilter": {}
},
"meta": {
"table": {
"type": "table",
"_id": "datasource_plus_44a967caf37a435f84fe01cd6dfe8f81__tasks",
"primary": [
"taskid"
],
"name": "a",
"schema": {
"executorid": {
"type": "number",
"externalType": "integer",
"name": "executorid",
"constraints": {
"presence": false
},
"autocolumn": true,
"autoReason": "foreign_key"
},
"taskname": {
"type": "string",
"externalType": "character varying",
"autocolumn": false,
"name": "taskname",
"constraints": {
"presence": false
}
},
"taskid": {
"type": "number",
"externalType": "integer",
"autocolumn": true,
"name": "taskid",
"constraints": {
"presence": false
}
},
"completed": {
"type": "boolean",
"externalType": "boolean",
"autocolumn": false,
"name": "completed",
"constraints": {
"presence": false
}
},
"qaid": {
"type": "number",
"externalType": "integer",
"name": "qaid",
"constraints": {
"presence": false
}
},
"products": {
"tableId": "datasource_plus_44a967caf37a435f84fe01cd6dfe8f81__products",
"name": "products",
"relationshipType": "many-to-many",
"through": "datasource_plus_44a967caf37a435f84fe01cd6dfe8f81__products_tasks",
"type": "link",
"_id": "c3b91d00cd36c4cc1a347794725b9adbd",
"fieldName": "productid",
"throughFrom": "productid",
"throughTo": "taskid"
},
"tasksToExecute": {
"tableId": "datasource_plus_44a967caf37a435f84fe01cd6dfe8f81__persons",
"name": "tasksToExecute",
"relationshipType": "one-to-many",
"type": "link",
"_id": "c0f440590bda04f28846242156c1dd60b",
"foreignKey": "executorid",
"fieldName": "personid"
},
"tasksToQA": {
"tableId": "datasource_plus_44a967caf37a435f84fe01cd6dfe8f81__persons",
"name": "tasksToQA",
"relationshipType": "one-to-many",
"type": "link",
"_id": "c5fdf453a0ba743d58e29491d174c974b",
"foreignKey": "qaid",
"fieldName": "personid"
}
},
"sourceId": "datasource_plus_44a967caf37a435f84fe01cd6dfe8f81",
"sourceType": "external",
"primaryDisplay": "taskname",
"sql": true,
"views": {}
}
},
"tableAliases": {
"tasks": "a",
"products": "b",
"persons": "c",
"products_tasks": "d"
}
}

View File

@ -0,0 +1,181 @@
{
"endpoint": {
"datasourceId": "datasource_plus_8066e56456784eb2a00129d31be5c3e7",
"entityId": "persons",
"operation": "UPDATE"
},
"resource": {
"fields": [
"a.year",
"a.firstname",
"a.personid",
"a.address",
"a.age",
"a.type",
"a.city",
"a.lastname"
]
},
"filters": {
"equal": {
"personid": 5
}
},
"relationships": [
{
"tableName": "tasks",
"column": "QA",
"from": "personid",
"to": "qaid",
"aliases": {
"tasks": "b",
"persons": "a"
}
},
{
"tableName": "tasks",
"column": "executor",
"from": "personid",
"to": "executorid",
"aliases": {
"tasks": "b",
"persons": "a"
}
}
],
"body": {
"year": 1990,
"firstname": "C",
"address": "A Street",
"age": 34,
"type": "designer",
"city": "London",
"lastname": "B"
},
"extra": {
"idFilter": {
"equal": {
"personid": 5
}
}
},
"meta": {
"table": {
"type": "table",
"_id": "datasource_plus_8066e56456784eb2a00129d31be5c3e7__persons",
"primary": [
"personid"
],
"name": "a",
"schema": {
"year": {
"type": "number",
"externalType": "integer",
"autocolumn": false,
"name": "year",
"constraints": {
"presence": false
}
},
"firstname": {
"type": "string",
"externalType": "character varying",
"autocolumn": false,
"name": "firstname",
"constraints": {
"presence": false
}
},
"personid": {
"type": "number",
"externalType": "integer",
"autocolumn": true,
"name": "personid",
"constraints": {
"presence": false
}
},
"address": {
"type": "string",
"externalType": "character varying",
"autocolumn": false,
"name": "address",
"constraints": {
"presence": false
}
},
"age": {
"type": "number",
"externalType": "integer",
"autocolumn": false,
"name": "age",
"constraints": {
"presence": false
}
},
"type": {
"type": "options",
"externalType": "USER-DEFINED",
"autocolumn": false,
"name": "type",
"constraints": {
"presence": false,
"inclusion": [
"support",
"designer",
"programmer",
"qa"
]
}
},
"city": {
"type": "string",
"externalType": "character varying",
"autocolumn": false,
"name": "city",
"constraints": {
"presence": false
}
},
"lastname": {
"type": "string",
"externalType": "character varying",
"autocolumn": false,
"name": "lastname",
"constraints": {
"presence": false
}
},
"QA": {
"tableId": "datasource_plus_8066e56456784eb2a00129d31be5c3e7__tasks",
"name": "QA",
"relationshipType": "many-to-one",
"fieldName": "qaid",
"type": "link",
"main": true,
"_id": "ccb68481c80c34217a4540a2c6c27fe46",
"foreignKey": "personid"
},
"executor": {
"tableId": "datasource_plus_8066e56456784eb2a00129d31be5c3e7__tasks",
"name": "executor",
"relationshipType": "many-to-one",
"fieldName": "executorid",
"type": "link",
"main": true,
"_id": "c89530b9770d94bec851e062b5cff3001",
"foreignKey": "personid",
"tableName": "persons"
}
},
"sourceId": "datasource_plus_8066e56456784eb2a00129d31be5c3e7",
"sourceType": "external",
"primaryDisplay": "firstname",
"views": {}
}
},
"tableAliases": {
"persons": "a",
"tasks": "b"
}
}

View File

@ -0,0 +1,181 @@
{
"endpoint": {
"datasourceId": "datasource_plus_8066e56456784eb2a00129d31be5c3e7",
"entityId": "persons",
"operation": "UPDATE"
},
"resource": {
"fields": [
"a.year",
"a.firstname",
"a.personid",
"a.address",
"a.age",
"a.type",
"a.city",
"a.lastname"
]
},
"filters": {
"equal": {
"personid": 5
}
},
"relationships": [
{
"tableName": "tasks",
"column": "QA",
"from": "personid",
"to": "qaid",
"aliases": {
"tasks": "b",
"persons": "a"
}
},
{
"tableName": "tasks",
"column": "executor",
"from": "personid",
"to": "executorid",
"aliases": {
"tasks": "b",
"persons": "a"
}
}
],
"body": {
"year": 1990,
"firstname": "C",
"address": "A Street",
"age": 34,
"type": "designer",
"city": "London",
"lastname": "B"
},
"extra": {
"idFilter": {
"equal": {
"personid": 5
}
}
},
"meta": {
"table": {
"type": "table",
"_id": "datasource_plus_8066e56456784eb2a00129d31be5c3e7__persons",
"primary": [
"personid"
],
"name": "a",
"schema": {
"year": {
"type": "number",
"externalType": "integer",
"autocolumn": false,
"name": "year",
"constraints": {
"presence": false
}
},
"firstname": {
"type": "string",
"externalType": "character varying",
"autocolumn": false,
"name": "firstname",
"constraints": {
"presence": false
}
},
"personid": {
"type": "number",
"externalType": "integer",
"autocolumn": true,
"name": "personid",
"constraints": {
"presence": false
}
},
"address": {
"type": "string",
"externalType": "character varying",
"autocolumn": false,
"name": "address",
"constraints": {
"presence": false
}
},
"age": {
"type": "number",
"externalType": "integer",
"autocolumn": false,
"name": "age",
"constraints": {
"presence": false
}
},
"type": {
"type": "options",
"externalType": "USER-DEFINED",
"autocolumn": false,
"name": "type",
"constraints": {
"presence": false,
"inclusion": [
"support",
"designer",
"programmer",
"qa"
]
}
},
"city": {
"type": "string",
"externalType": "character varying",
"autocolumn": false,
"name": "city",
"constraints": {
"presence": false
}
},
"lastname": {
"type": "string",
"externalType": "character varying",
"autocolumn": false,
"name": "lastname",
"constraints": {
"presence": false
}
},
"QA": {
"tableId": "datasource_plus_8066e56456784eb2a00129d31be5c3e7__tasks",
"name": "QA",
"relationshipType": "many-to-one",
"fieldName": "qaid",
"type": "link",
"main": true,
"_id": "ccb68481c80c34217a4540a2c6c27fe46",
"foreignKey": "personid"
},
"executor": {
"tableId": "datasource_plus_8066e56456784eb2a00129d31be5c3e7__tasks",
"name": "executor",
"relationshipType": "many-to-one",
"fieldName": "executorid",
"type": "link",
"main": true,
"_id": "c89530b9770d94bec851e062b5cff3001",
"foreignKey": "personid",
"tableName": "persons"
}
},
"sourceId": "datasource_plus_8066e56456784eb2a00129d31be5c3e7",
"sourceType": "external",
"primaryDisplay": "firstname",
"views": {}
}
},
"tableAliases": {
"persons": "a",
"tasks": "b"
}
}

View File

@ -3,12 +3,33 @@ import {
DatasourcePlus,
IntegrationBase,
Schema,
Table,
} from "@budibase/types"
import * as datasources from "./datasources"
import tableSdk from "../tables"
import { getIntegration } from "../../../integrations"
import { context } from "@budibase/backend-core"
function checkForSchemaErrors(schema: Record<string, Table>) {
const errors: Record<string, string> = {}
for (let [tableName, table] of Object.entries(schema)) {
if (tableName.includes(".")) {
errors[tableName] = "Table names containing dots are not supported."
} else {
const columnNames = Object.keys(table.schema)
const invalidColumnName = columnNames.find(columnName =>
columnName.includes(".")
)
if (invalidColumnName) {
errors[
tableName
] = `Column '${invalidColumnName}' is not supported as it contains a dot.`
}
}
}
return errors
}
export async function buildFilteredSchema(
datasource: Datasource,
filter?: string[]
@ -30,16 +51,19 @@ export async function buildFilteredSchema(
filteredSchema.errors[key] = schema.errors[key]
}
}
return filteredSchema
return {
...filteredSchema,
errors: {
...filteredSchema.errors,
...checkForSchemaErrors(filteredSchema.tables),
},
}
}
async function buildSchemaHelper(datasource: Datasource): Promise<Schema> {
const connector = (await getConnector(datasource)) as DatasourcePlus
const externalSchema = await connector.buildSchema(
datasource._id!,
datasource.entities!
)
return externalSchema
return await connector.buildSchema(datasource._id!, datasource.entities!)
}
export async function getConnector(

View File

@ -66,11 +66,13 @@ export async function search(options: SearchParams): Promise<{
export interface ExportRowsParams {
tableId: string
format: Format
delimiter?: string
rowIds?: string[]
columns?: string[]
query?: SearchFilters
sort?: string
sortOrder?: SortOrder
customHeaders?: { [key: string]: string }
}
export interface ExportRowsResult {

View File

@ -9,10 +9,9 @@ import {
SearchParams,
} from "@budibase/types"
import * as exporters from "../../../../api/controllers/view/exporters"
import sdk from "../../../../sdk"
import { handleRequest } from "../../../../api/controllers/row/external"
import { breakExternalTableId } from "../../../../integrations/utils"
import { cleanExportRows } from "../utils"
import sdk from "../../../../sdk"
import { utils } from "@budibase/shared-core"
import { ExportRowsParams, ExportRowsResult } from "../search"
import { HTTPError, db } from "@budibase/backend-core"
@ -101,7 +100,17 @@ export async function search(options: SearchParams) {
export async function exportRows(
options: ExportRowsParams
): Promise<ExportRowsResult> {
const { tableId, format, columns, rowIds, query, sort, sortOrder } = options
const {
tableId,
format,
columns,
rowIds,
query,
sort,
sortOrder,
delimiter,
customHeaders,
} = options
const { datasourceId, tableName } = breakExternalTableId(tableId)
let requestQuery: SearchFilters = {}
@ -153,12 +162,23 @@ export async function exportRows(
}
const schema = datasource.entities[tableName].schema
let exportRows = cleanExportRows(rows, schema, format, columns)
let exportRows = sdk.rows.utils.cleanExportRows(
rows,
schema,
format,
columns,
customHeaders
)
let content: string
switch (format) {
case exporters.Format.CSV:
content = exporters.csv(headers ?? Object.keys(schema), exportRows)
content = exporters.csv(
headers ?? Object.keys(schema),
exportRows,
delimiter,
customHeaders
)
break
case exporters.Format.JSON:
content = exporters.json(exportRows)

View File

@ -85,7 +85,17 @@ export async function search(options: SearchParams) {
export async function exportRows(
options: ExportRowsParams
): Promise<ExportRowsResult> {
const { tableId, format, rowIds, columns, query, sort, sortOrder } = options
const {
tableId,
format,
rowIds,
columns,
query,
sort,
sortOrder,
delimiter,
customHeaders,
} = options
const db = context.getAppDB()
const table = await sdk.tables.getTable(tableId)
@ -125,11 +135,16 @@ export async function exportRows(
rows = result
}
let exportRows = sdk.rows.utils.cleanExportRows(rows, schema, format, columns)
let exportRows = cleanExportRows(rows, schema, format, columns, customHeaders)
if (format === Format.CSV) {
return {
fileName: "export.csv",
content: csv(headers ?? Object.keys(rows[0]), exportRows),
content: csv(
headers ?? Object.keys(rows[0]),
exportRows,
delimiter,
customHeaders
),
}
} else if (format === Format.JSON) {
return {

View File

@ -1,12 +1,21 @@
import cloneDeep from "lodash/cloneDeep"
import validateJs from "validate.js"
import { FieldType, Row, Table, TableSchema } from "@budibase/types"
import {
FieldType,
QueryJson,
Row,
Table,
TableSchema,
DatasourcePlusQueryResponse,
} from "@budibase/types"
import { makeExternalQuery } from "../../../integrations/base/query"
import { Format } from "../../../api/controllers/view/exporters"
import sdk from "../.."
import { isRelationshipColumn } from "../../../db/utils"
export async function getDatasourceAndQuery(json: any) {
export async function getDatasourceAndQuery(
json: QueryJson
): Promise<DatasourcePlusQueryResponse> {
const datasourceId = json.endpoint.datasourceId
const datasource = await sdk.datasources.get(datasourceId)
return makeExternalQuery(datasource, json)
@ -16,7 +25,8 @@ export function cleanExportRows(
rows: any[],
schema: TableSchema,
format: string,
columns?: string[]
columns?: string[],
customHeaders: { [key: string]: string } = {}
) {
let cleanRows = [...rows]
@ -44,11 +54,27 @@ export function cleanExportRows(
}
}
}
} else if (format === Format.JSON) {
// Replace row keys with custom headers
for (let row of cleanRows) {
renameKeys(customHeaders, row)
}
}
return cleanRows
}
function renameKeys(keysMap: { [key: string]: any }, row: any) {
for (const key in keysMap) {
Object.defineProperty(
row,
keysMap[key],
Object.getOwnPropertyDescriptor(row, key) || {}
)
delete row[key]
}
}
function isForeignKey(key: string, table: Table) {
const relationships = Object.values(table.schema).filter(isRelationshipColumn)
return relationships.some(

View File

@ -1,17 +1,96 @@
import { Response } from "supertest"
import { App } from "@budibase/types"
import {
App,
type CreateAppRequest,
type FetchAppDefinitionResponse,
type FetchAppPackageResponse,
} from "@budibase/types"
import TestConfiguration from "../TestConfiguration"
import { TestAPI } from "./base"
import { AppStatus } from "../../../db/utils"
import { constants } from "@budibase/backend-core"
export class ApplicationAPI extends TestAPI {
constructor(config: TestConfiguration) {
super(config)
}
create = async (app: CreateAppRequest): Promise<App> => {
const request = this.request
.post("/api/applications")
.set(this.config.defaultHeaders())
.expect("Content-Type", /json/)
for (const key of Object.keys(app)) {
request.field(key, (app as any)[key])
}
if (app.templateFile) {
request.attach("templateFile", app.templateFile)
}
const result = await request
if (result.statusCode !== 200) {
throw new Error(JSON.stringify(result.body))
}
return result.body as App
}
delete = async (appId: string): Promise<void> => {
await this.request
.delete(`/api/applications/${appId}`)
.set(this.config.defaultHeaders())
.expect(200)
}
publish = async (
appId: string
): Promise<{ _id: string; status: string; appUrl: string }> => {
// While the publish endpoint does take an :appId parameter, it doesn't
// use it. It uses the appId from the context.
let headers = {
...this.config.defaultHeaders(),
[constants.Header.APP_ID]: appId,
}
const result = await this.request
.post(`/api/applications/${appId}/publish`)
.set(headers)
.expect("Content-Type", /json/)
.expect(200)
return result.body as { _id: string; status: string; appUrl: string }
}
unpublish = async (appId: string): Promise<void> => {
await this.request
.post(`/api/applications/${appId}/unpublish`)
.set(this.config.defaultHeaders())
.expect(204)
}
sync = async (
appId: string,
{ statusCode }: { statusCode: number } = { statusCode: 200 }
): Promise<{ message: string }> => {
const result = await this.request
.post(`/api/applications/${appId}/sync`)
.set(this.config.defaultHeaders())
.expect("Content-Type", /json/)
.expect(statusCode)
return result.body
}
getRaw = async (appId: string): Promise<Response> => {
// While the appPackage endpoint does take an :appId parameter, it doesn't
// use it. It uses the appId from the context.
let headers = {
...this.config.defaultHeaders(),
[constants.Header.APP_ID]: appId,
}
const result = await this.request
.get(`/api/applications/${appId}/appPackage`)
.set(this.config.defaultHeaders())
.set(headers)
.expect("Content-Type", /json/)
.expect(200)
return result
@ -21,4 +100,94 @@ export class ApplicationAPI extends TestAPI {
const result = await this.getRaw(appId)
return result.body.application as App
}
getDefinition = async (
appId: string
): Promise<FetchAppDefinitionResponse> => {
const result = await this.request
.get(`/api/applications/${appId}/definition`)
.set(this.config.defaultHeaders())
.expect("Content-Type", /json/)
.expect(200)
return result.body as FetchAppDefinitionResponse
}
getAppPackage = async (appId: string): Promise<FetchAppPackageResponse> => {
const result = await this.request
.get(`/api/applications/${appId}/appPackage`)
.set(this.config.defaultHeaders())
.expect("Content-Type", /json/)
.expect(200)
return result.body
}
update = async (
appId: string,
app: { name?: string; url?: string }
): Promise<App> => {
const request = this.request
.put(`/api/applications/${appId}`)
.set(this.config.defaultHeaders())
.expect("Content-Type", /json/)
for (const key of Object.keys(app)) {
request.field(key, (app as any)[key])
}
const result = await request
if (result.statusCode !== 200) {
throw new Error(JSON.stringify(result.body))
}
return result.body as App
}
updateClient = async (appId: string): Promise<void> => {
// While the updateClient endpoint does take an :appId parameter, it doesn't
// use it. It uses the appId from the context.
let headers = {
...this.config.defaultHeaders(),
[constants.Header.APP_ID]: appId,
}
const response = await this.request
.post(`/api/applications/${appId}/client/update`)
.set(headers)
.expect("Content-Type", /json/)
if (response.statusCode !== 200) {
throw new Error(JSON.stringify(response.body))
}
}
revertClient = async (appId: string): Promise<void> => {
// While the revertClient endpoint does take an :appId parameter, it doesn't
// use it. It uses the appId from the context.
let headers = {
...this.config.defaultHeaders(),
[constants.Header.APP_ID]: appId,
}
const response = await this.request
.post(`/api/applications/${appId}/client/revert`)
.set(headers)
.expect("Content-Type", /json/)
if (response.statusCode !== 200) {
throw new Error(JSON.stringify(response.body))
}
}
fetch = async ({ status }: { status?: AppStatus } = {}): Promise<App[]> => {
let query = []
if (status) {
query.push(`status=${status}`)
}
const result = await this.request
.get(`/api/applications${query.length ? `?${query.join("&")}` : ""}`)
.set(this.config.defaultHeaders())
.expect("Content-Type", /json/)
.expect(200)
return result.body as App[]
}
}

View File

@ -37,6 +37,8 @@ export interface ExportRowsRequest {
query?: SearchFilters
sort?: string
sortOrder?: SortOrder
delimiter?: string
customHeaders?: { [key: string]: string }
}
export type ExportRowsResponse = ReadStream

View File

@ -0,0 +1,29 @@
import type { PlanType } from "../../sdk"
import type { Layout, App, Screen } from "../../documents"
export interface CreateAppRequest {
name: string
url?: string
useTemplate?: string
templateName?: string
templateKey?: string
templateFile?: string
includeSampleData?: boolean
encryptionPassword?: string
templateString?: string
}
export interface FetchAppDefinitionResponse {
layouts: Layout[]
screens: Screen[]
libraries: string[]
}
export interface FetchAppPackageResponse {
application: App
licenseType: PlanType
screens: Screen[]
layouts: Layout[]
clientLibPath: string
hasLock: boolean
}

View File

@ -1,3 +1,4 @@
export * from "./application"
export * from "./analytics"
export * from "./auth"
export * from "./user"

View File

@ -1,4 +1,4 @@
import { User, Document } from "../"
import { User, Document, Plugin } from "../"
import { SocketSession } from "../../sdk"
export type AppMetadataErrors = { [key: string]: string[] }
@ -24,6 +24,8 @@ export interface App extends Document {
icon?: AppIcon
features?: AppFeatures
automations?: AutomationSettings
usedPlugins?: Plugin[]
upgradableVersion?: string
}
export interface AppInstance {

View File

@ -1,4 +1,5 @@
import { Table } from "../documents"
import { Table, Row } from "../documents"
import { QueryJson } from "./search"
export const PASSWORD_REPLACEMENT = "--secret-value--"
@ -180,11 +181,25 @@ export interface Schema {
errors: Record<string, string>
}
// return these when an operation occurred but we got no response
enum DSPlusOperation {
CREATE = "create",
READ = "read",
UPDATE = "update",
DELETE = "delete",
}
export type DatasourcePlusQueryResponse =
| Row[]
| Record<DSPlusOperation, boolean>[]
| void
export interface DatasourcePlus extends IntegrationBase {
// if the datasource supports the use of bindings directly (to protect against SQL injection)
// this returns the format of the identifier
getBindingIdentifier(): string
getStringConcat(parts: string[]): string
query(json: QueryJson): Promise<DatasourcePlusQueryResponse>
buildSchema(
datasourceId: string,
entities: Record<string, Table>

View File

@ -94,6 +94,7 @@ export interface QueryJson {
idFilter?: SearchFilters
}
relationships?: RelationshipsJson[]
tableAliases?: Record<string, string>
}
export interface SqlQuery {

View File

@ -4,10 +4,10 @@ set -e
if [[ -n $CI ]]
then
# Running in ci, where resources are limited
echo "jest --coverage --maxWorkers=2 --forceExit --bail"
jest --coverage --maxWorkers=2 --forceExit --bail
echo "jest --coverage --maxWorkers=2 --forceExit --bail $@"
jest --coverage --maxWorkers=2 --forceExit --bail $@
else
# --maxWorkers performs better in development
echo "jest --coverage --maxWorkers=2 --forceExit"
jest --coverage --maxWorkers=2 --forceExit
echo "jest --coverage --maxWorkers=2 --forceExit $@"
jest --coverage --maxWorkers=2 --forceExit $@
fi

View File

@ -1,11 +1,10 @@
import { App } from "@budibase/types"
import { App, CreateAppRequest } from "@budibase/types"
import { Response } from "node-fetch"
import {
RouteConfig,
AppPackageResponse,
DeployConfig,
MessageResponse,
CreateAppRequest,
} from "../../../types"
import BudibaseInternalAPIClient from "../BudibaseInternalAPIClient"
import BaseAPI from "./BaseAPI"

View File

@ -1,5 +1,5 @@
import { generator } from "../../shared"
import { CreateAppRequest } from "../../types"
import { CreateAppRequest } from "@budibase/types"
function uniqueWord() {
return generator.word() + generator.hash()

View File

@ -13,17 +13,6 @@ describe("Internal API - Table Operations", () => {
await config.afterAll()
})
async function createAppFromTemplate() {
return config.api.apps.create({
name: generator.word(),
url: `/${generator.word()}`,
useTemplate: "true",
templateName: "Near Miss Register",
templateKey: "app/near-miss-register",
templateFile: undefined,
})
}
it("Create and delete table, columns and rows", async () => {
// create the app
await config.createApp(fixtures.apps.appFromTemplate())

View File

@ -1,8 +1,8 @@
import { BudibaseInternalAPI } from "../internal-api"
import { AccountInternalAPI } from "../account-api"
import { APIRequestOpts, CreateAppRequest, State } from "../types"
import { APIRequestOpts, State } from "../types"
import * as fixtures from "../internal-api/fixtures"
import { CreateAccountRequest } from "@budibase/types"
import { CreateAccountRequest, CreateAppRequest } from "@budibase/types"
export default class BudibaseTestConfiguration {
// apis

View File

@ -1,10 +0,0 @@
// TODO: Integrate with budibase
export interface CreateAppRequest {
name: string
url: string
useTemplate?: string
templateName?: string
templateKey?: string
templateFile?: string
includeSampleData?: boolean
}

View File

@ -1,6 +1,5 @@
export * from "./api"
export * from "./apiKeyResponse"
export * from "./app"
export * from "./appPackage"
export * from "./deploy"
export * from "./newAccount"

View File

@ -17,12 +17,6 @@ const { nodeExternalsPlugin } = require("esbuild-node-externals")
const svelteCompilePlugin = {
name: 'svelteCompile',
setup(build) {
// This resolve handler is necessary to bundle the Svelte runtime into the the final output,
// otherwise the bundled script will attempt to resolve it at runtime
build.onResolve({ filter: /svelte\/internal/ }, async () => {
return { path: `${process.cwd()}/../../node_modules/svelte/src/runtime/internal/ssr.js` }
})
// Compiles `.svelte` files into JS classes so that they can be directly imported into our
// Typescript packages
build.onLoad({ filter: /\.svelte$/ }, async (args) => {
@ -37,7 +31,7 @@ const svelteCompilePlugin = {
contents: js.code,
// The loader this is passed to, basically how the above provided content is "treated",
// the contents provided above will be transpiled and bundled like any other JS file.
loader: 'js',
loader: 'js',
// Where to resolve any imports present in the loaded file
resolveDir: dir
}
@ -80,11 +74,11 @@ async function runBuild(entry, outfile) {
plugins: [
svelteCompilePlugin,
TsconfigPathsPlugin({ tsconfig: tsconfigPathPluginContent }),
nodeExternalsPlugin(),
nodeExternalsPlugin({
allowList: ["@budibase/frontend-core", "svelte"]
}),
],
preserveSymlinks: true,
loader: {
},
metafile: true,
external: [
"deasync",

384
yarn.lock
View File

@ -1097,7 +1097,7 @@
"@babel/highlight@^7.23.4":
version "7.23.4"
resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.23.4.tgz#edaadf4d8232e1a961432db785091207ead0621b"
integrity "sha1-7arfTYIy4alhQy23hQkSB+rQYhs= sha512-acGdbYSfp2WheJoJm/EBBBLh/ID8KDc64ISZ9DYtBmC8/Q204PZJLHyzeB5qMzJ5trcOkybd78M4x2KWsUq++A=="
integrity sha512-acGdbYSfp2WheJoJm/EBBBLh/ID8KDc64ISZ9DYtBmC8/Q204PZJLHyzeB5qMzJ5trcOkybd78M4x2KWsUq++A==
dependencies:
"@babel/helper-validator-identifier" "^7.22.20"
chalk "^2.4.2"
@ -1988,14 +1988,14 @@
resolved "https://registry.yarnpkg.com/@babel/regjsgen/-/regjsgen-0.8.0.tgz#f0ba69b075e1f05fb2825b7fad991e7adbb18310"
integrity sha512-x/rqGMdzj+fWZvCOYForTghzbtqPDZ5gPwaoNGHdgDfF2QA/XZbCBp4Moo5scrkAMPhB7z26XM/AaHuIJdgauA==
"@babel/runtime@^7.10.5":
"@babel/runtime@^7.10.5", "@babel/runtime@^7.13.10", "@babel/runtime@^7.21.0":
version "7.23.9"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.23.9.tgz#47791a15e4603bb5f905bc0753801cf21d6345f7"
integrity sha512-0CX6F+BI2s9dkUqr08KFrAIZgNFj75rdBU/DjCyYLIaV/quFjkk6T+EJ2LkZHyZTbEV4L5p97mNkUsHl2wLFAw==
dependencies:
regenerator-runtime "^0.14.0"
"@babel/runtime@^7.12.5", "@babel/runtime@^7.13.10", "@babel/runtime@^7.15.4", "@babel/runtime@^7.21.0", "@babel/runtime@^7.8.4", "@babel/runtime@^7.9.2":
"@babel/runtime@^7.12.5", "@babel/runtime@^7.15.4", "@babel/runtime@^7.8.4", "@babel/runtime@^7.9.2":
version "7.23.8"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.23.8.tgz#8ee6fe1ac47add7122902f257b8ddf55c898f650"
integrity sha512-Y7KbAP984rn1VGMbGqKmBLio9V7y5Je9GvU4rQPCPinCyNfUcToxIXl06d59URp/F3LwinvODxab5N/G6qggkw==
@ -3434,9 +3434,9 @@
tar "^6.1.11"
"@mongodb-js/saslprep@^1.1.0":
version "1.1.1"
resolved "https://registry.yarnpkg.com/@mongodb-js/saslprep/-/saslprep-1.1.1.tgz#9a6c2516bc9188672c4d953ec99760ba49970da7"
integrity sha512-t7c5K033joZZMspnHg/gWPE4kandgc2OxE74aYOtGKfgB9VPuVJPix0H6fhmm2erj5PBJ21mqcx34lpIGtUCsQ==
version "1.1.4"
resolved "https://registry.yarnpkg.com/@mongodb-js/saslprep/-/saslprep-1.1.4.tgz#24ec1c4915a65f5c506bb88c081731450d91bb1c"
integrity sha512-8zJ8N1x51xo9hwPh6AWnKdLGEC5N3lDa6kms1YHmFBoRhTpJR6HG8wWk0td1MVCu9cD4YBrvjZEtd5Obw0Fbnw==
dependencies:
sparse-bitfield "^3.0.3"
@ -4043,70 +4043,70 @@
estree-walker "^2.0.2"
picomatch "^2.3.1"
"@rollup/rollup-android-arm-eabi@4.10.0":
version "4.10.0"
resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.10.0.tgz#786eaf6372be2fc209cc957c14aa9d3ff8fefe6a"
integrity sha512-/MeDQmcD96nVoRumKUljsYOLqfv1YFJps+0pTrb2Z9Nl/w5qNUysMaWQsrd1mvAlNT4yza1iVyIu4Q4AgF6V3A==
"@rollup/rollup-android-arm-eabi@4.12.0":
version "4.12.0"
resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.12.0.tgz#38c3abd1955a3c21d492af6b1a1dca4bb1d894d6"
integrity sha512-+ac02NL/2TCKRrJu2wffk1kZ+RyqxVUlbjSagNgPm94frxtr+XDL12E5Ll1enWskLrtrZ2r8L3wED1orIibV/w==
"@rollup/rollup-android-arm64@4.10.0":
version "4.10.0"
resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.10.0.tgz#0114a042fd6396f4f3233e6171fd5b61a36ed539"
integrity sha512-lvu0jK97mZDJdpZKDnZI93I0Om8lSDaiPx3OiCk0RXn3E8CMPJNS/wxjAvSJJzhhZpfjXsjLWL8LnS6qET4VNQ==
"@rollup/rollup-android-arm64@4.12.0":
version "4.12.0"
resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.12.0.tgz#3822e929f415627609e53b11cec9a4be806de0e2"
integrity sha512-OBqcX2BMe6nvjQ0Nyp7cC90cnumt8PXmO7Dp3gfAju/6YwG0Tj74z1vKrfRz7qAv23nBcYM8BCbhrsWqO7PzQQ==
"@rollup/rollup-darwin-arm64@4.10.0":
version "4.10.0"
resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.10.0.tgz#944d007c1dc71a8c9174d11671c0c34bd74a2c81"
integrity sha512-uFpayx8I8tyOvDkD7X6n0PriDRWxcqEjqgtlxnUA/G9oS93ur9aZ8c8BEpzFmsed1TH5WZNG5IONB8IiW90TQg==
"@rollup/rollup-darwin-arm64@4.12.0":
version "4.12.0"
resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.12.0.tgz#6c082de71f481f57df6cfa3701ab2a7afde96f69"
integrity sha512-X64tZd8dRE/QTrBIEs63kaOBG0b5GVEd3ccoLtyf6IdXtHdh8h+I56C2yC3PtC9Ucnv0CpNFJLqKFVgCYe0lOQ==
"@rollup/rollup-darwin-x64@4.10.0":
version "4.10.0"
resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.10.0.tgz#1d08cb4521a058d7736ab1c7fe988daf034a2598"
integrity sha512-nIdCX03qFKoR/MwQegQBK+qZoSpO3LESurVAC6s6jazLA1Mpmgzo3Nj3H1vydXp/JM29bkCiuF7tDuToj4+U9Q==
"@rollup/rollup-darwin-x64@4.12.0":
version "4.12.0"
resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.12.0.tgz#c34ca0d31f3c46a22c9afa0e944403eea0edcfd8"
integrity sha512-cc71KUZoVbUJmGP2cOuiZ9HSOP14AzBAThn3OU+9LcA1+IUqswJyR1cAJj3Mg55HbjZP6OLAIscbQsQLrpgTOg==
"@rollup/rollup-linux-arm-gnueabihf@4.10.0":
version "4.10.0"
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.10.0.tgz#4763eec1591bf0e99a54ad3d1ef39cb268ed7b19"
integrity sha512-Fz7a+y5sYhYZMQFRkOyCs4PLhICAnxRX/GnWYReaAoruUzuRtcf+Qnw+T0CoAWbHCuz2gBUwmWnUgQ67fb3FYw==
"@rollup/rollup-linux-arm-gnueabihf@4.12.0":
version "4.12.0"
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.12.0.tgz#48e899c1e438629c072889b824a98787a7c2362d"
integrity sha512-a6w/Y3hyyO6GlpKL2xJ4IOh/7d+APaqLYdMf86xnczU3nurFTaVN9s9jOXQg97BE4nYm/7Ga51rjec5nfRdrvA==
"@rollup/rollup-linux-arm64-gnu@4.10.0":
version "4.10.0"
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.10.0.tgz#e6dae70c53ace836973526c41803b877cffc6f7b"
integrity sha512-yPtF9jIix88orwfTi0lJiqINnlWo6p93MtZEoaehZnmCzEmLL0eqjA3eGVeyQhMtxdV+Mlsgfwhh0+M/k1/V7Q==
"@rollup/rollup-linux-arm64-gnu@4.12.0":
version "4.12.0"
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.12.0.tgz#788c2698a119dc229062d40da6ada8a090a73a68"
integrity sha512-0fZBq27b+D7Ar5CQMofVN8sggOVhEtzFUwOwPppQt0k+VR+7UHMZZY4y+64WJ06XOhBTKXtQB/Sv0NwQMXyNAA==
"@rollup/rollup-linux-arm64-musl@4.10.0":
version "4.10.0"
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.10.0.tgz#5692e1a0feba0cc4a933864961afc3211177d242"
integrity sha512-9GW9yA30ib+vfFiwjX+N7PnjTnCMiUffhWj4vkG4ukYv1kJ4T9gHNg8zw+ChsOccM27G9yXrEtMScf1LaCuoWQ==
"@rollup/rollup-linux-arm64-musl@4.12.0":
version "4.12.0"
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.12.0.tgz#3882a4e3a564af9e55804beeb67076857b035ab7"
integrity sha512-eTvzUS3hhhlgeAv6bfigekzWZjaEX9xP9HhxB0Dvrdbkk5w/b+1Sxct2ZuDxNJKzsRStSq1EaEkVSEe7A7ipgQ==
"@rollup/rollup-linux-riscv64-gnu@4.10.0":
version "4.10.0"
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.10.0.tgz#fbe3d80f7a7ac54a8847f5bddd1bc6f7b9ccb65f"
integrity sha512-X1ES+V4bMq2ws5fF4zHornxebNxMXye0ZZjUrzOrf7UMx1d6wMQtfcchZ8SqUnQPPHdOyOLW6fTcUiFgHFadRA==
"@rollup/rollup-linux-riscv64-gnu@4.12.0":
version "4.12.0"
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.12.0.tgz#0c6ad792e1195c12bfae634425a3d2aa0fe93ab7"
integrity sha512-ix+qAB9qmrCRiaO71VFfY8rkiAZJL8zQRXveS27HS+pKdjwUfEhqo2+YF2oI+H/22Xsiski+qqwIBxVewLK7sw==
"@rollup/rollup-linux-x64-gnu@4.10.0":
version "4.10.0"
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.10.0.tgz#3f06b55ccf173446d390d0306643dff62ec99807"
integrity sha512-w/5OpT2EnI/Xvypw4FIhV34jmNqU5PZjZue2l2Y3ty1Ootm3SqhI+AmfhlUYGBTd9JnpneZCDnt3uNOiOBkMyw==
"@rollup/rollup-linux-x64-gnu@4.12.0":
version "4.12.0"
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.12.0.tgz#9d62485ea0f18d8674033b57aa14fb758f6ec6e3"
integrity sha512-TenQhZVOtw/3qKOPa7d+QgkeM6xY0LtwzR8OplmyL5LrgTWIXpTQg2Q2ycBf8jm+SFW2Wt/DTn1gf7nFp3ssVA==
"@rollup/rollup-linux-x64-musl@4.10.0":
version "4.10.0"
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.10.0.tgz#e4ac9b27041c83d7faab6205f62763103eb317ba"
integrity sha512-q/meftEe3QlwQiGYxD9rWwB21DoKQ9Q8wA40of/of6yGHhZuGfZO0c3WYkN9dNlopHlNT3mf5BPsUSxoPuVQaw==
"@rollup/rollup-linux-x64-musl@4.12.0":
version "4.12.0"
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.12.0.tgz#50e8167e28b33c977c1f813def2b2074d1435e05"
integrity sha512-LfFdRhNnW0zdMvdCb5FNuWlls2WbbSridJvxOvYWgSBOYZtgBfW9UGNJG//rwMqTX1xQE9BAodvMH9tAusKDUw==
"@rollup/rollup-win32-arm64-msvc@4.10.0":
version "4.10.0"
resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.10.0.tgz#6ad0d4fb0066f240778ee3f61eecf7aa0357f883"
integrity sha512-NrR6667wlUfP0BHaEIKgYM/2va+Oj+RjZSASbBMnszM9k+1AmliRjHc3lJIiOehtSSjqYiO7R6KLNrWOX+YNSQ==
"@rollup/rollup-win32-arm64-msvc@4.12.0":
version "4.12.0"
resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.12.0.tgz#68d233272a2004429124494121a42c4aebdc5b8e"
integrity sha512-JPDxovheWNp6d7AHCgsUlkuCKvtu3RB55iNEkaQcf0ttsDU/JZF+iQnYcQJSk/7PtT4mjjVG8N1kpwnI9SLYaw==
"@rollup/rollup-win32-ia32-msvc@4.10.0":
version "4.10.0"
resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.10.0.tgz#29d50292381311cc8d3623e73b427b7e2e40a653"
integrity sha512-FV0Tpt84LPYDduIDcXvEC7HKtyXxdvhdAOvOeWMWbQNulxViH2O07QXkT/FffX4FqEI02jEbCJbr+YcuKdyyMg==
"@rollup/rollup-win32-ia32-msvc@4.12.0":
version "4.12.0"
resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.12.0.tgz#366ca62221d1689e3b55a03f4ae12ae9ba595d40"
integrity sha512-fjtuvMWRGJn1oZacG8IPnzIV6GF2/XG+h71FKn76OYFqySXInJtseAqdprVTDTyqPxQOG9Exak5/E9Z3+EJ8ZA==
"@rollup/rollup-win32-x64-msvc@4.10.0":
version "4.10.0"
resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.10.0.tgz#4eedd01af3a82c1acb0fe6d837ebf339c4cbf839"
integrity sha512-OZoJd+o5TaTSQeFFQ6WjFCiltiYVjIdsXxwu/XZ8qRpsvMQr4UsVrE5UyT9RIvsnuF47DqkJKhhVZ2Q9YW9IpQ==
"@rollup/rollup-win32-x64-msvc@4.12.0":
version "4.12.0"
resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.12.0.tgz#9ffdf9ed133a7464f4ae187eb9e1294413fab235"
integrity sha512-ZYmr5mS2wd4Dew/JjT0Fqi2NPB/ZhZ2VvPp7SmvPZb4Y1CG/LRcS6tcRo2cYU7zLK5A7cdbhWnnWmUjoI4qapg==
"@roxi/routify@2.18.0":
version "2.18.0"
@ -5250,16 +5250,16 @@
integrity sha512-6ckxMjBBD8URvjB6J3NcnuAn5Pkl7t3TizAg+xdlzzQGSPSmBcXf8KoIH0ua/i+tio+ZRUHEXp0HEmvaR4kt0w==
"@types/chai-subset@^1.3.3":
version "1.3.5"
resolved "https://registry.yarnpkg.com/@types/chai-subset/-/chai-subset-1.3.5.tgz#3fc044451f26985f45625230a7f22284808b0a9a"
integrity sha512-c2mPnw+xHtXDoHmdtcCXGwyLMiauiAyxWMzhGpqHC4nqI/Y5G2XhTampslK2rb59kpcuHon03UH8W6iYUzw88A==
version "1.3.3"
resolved "https://registry.yarnpkg.com/@types/chai-subset/-/chai-subset-1.3.3.tgz#97893814e92abd2c534de422cb377e0e0bdaac94"
integrity sha512-frBecisrNGz+F4T6bcc+NLeolfiojh5FxW2klu669+8BARtyQv2C/GkNW6FUodVe4BroGMP/wER/YDGc7rEllw==
dependencies:
"@types/chai" "*"
"@types/chai@*", "@types/chai@^4.3.4":
version "4.3.11"
resolved "https://registry.yarnpkg.com/@types/chai/-/chai-4.3.11.tgz#e95050bf79a932cb7305dd130254ccdf9bde671c"
integrity sha512-qQR1dr2rGIHYlJulmr8Ioq3De0Le9E4MJ5AiaeAETJJpndT1uUNHsGFK3L/UIu+rbkQSdj8J/w2bCsBZc/Y5fQ==
version "4.3.9"
resolved "https://registry.yarnpkg.com/@types/chai/-/chai-4.3.9.tgz#144d762491967db8c6dea38e03d2206c2623feec"
integrity sha512-69TtiDzu0bcmKQv3yg1Zx409/Kd7r0b5F1PfpYJfSHzLGtB53547V4u+9iqKYsTu/O2ai6KTb0TInNpvuQ3qmg==
"@types/chance@1.1.3":
version "1.1.3"
@ -5654,10 +5654,10 @@
"@types/node" "*"
form-data "^3.0.0"
"@types/node@*", "@types/node@>=10.0.0", "@types/node@>=12.12.47", "@types/node@>=13.13.4", "@types/node@>=13.7.0", "@types/node@>=8.1.0":
version "20.11.2"
resolved "https://registry.yarnpkg.com/@types/node/-/node-20.11.2.tgz#39cea3fe02fbbc2f80ed283e94e1d24f2d3856fb"
integrity sha512-cZShBaVa+UO1LjWWBPmWRR4+/eY/JR/UIEcDlVsw3okjWEu+rB7/mH6X3B/L+qJVHDLjk9QW/y2upp9wp1yDXA==
"@types/node@*", "@types/node@>=10.0.0", "@types/node@>=12.12.47", "@types/node@>=13.13.4", "@types/node@>=13.7.0":
version "20.10.7"
resolved "https://registry.yarnpkg.com/@types/node/-/node-20.10.7.tgz#40fe8faf25418a75de9fe68a8775546732a3a901"
integrity sha512-fRbIKb8C/Y2lXxB5eVMj4IU7xpdox0Lh8bUPEdtLysaylsml1hOOx1+STloRs/B9nf7C6kPRmmg/V7aQW7usNg==
dependencies:
undici-types "~5.26.4"
@ -5683,10 +5683,17 @@
resolved "https://registry.yarnpkg.com/@types/node/-/node-14.18.37.tgz#0bfcd173e8e1e328337473a8317e37b3b14fd30d"
integrity sha512-7GgtHCs/QZrBrDzgIJnQtuSvhFSwhyYSI2uafSwZoNt1iOGhEN5fwNrQMjtONyHm9+/LoA4453jH0CMYcr06Pg==
"@types/node@>=8.1.0":
version "20.11.10"
resolved "https://registry.yarnpkg.com/@types/node/-/node-20.11.10.tgz#6c3de8974d65c362f82ee29db6b5adf4205462f9"
integrity sha512-rZEfe/hJSGYmdfX9tvcPMYeYPW2sNl50nsw4jZmRcaG0HIAb0WYEpsB05GOb53vjqpyE9GUhlDQ4jLSoB5q9kg==
dependencies:
undici-types "~5.26.4"
"@types/node@^18.11.18":
version "18.19.13"
resolved "https://registry.yarnpkg.com/@types/node/-/node-18.19.13.tgz#c3e989ca967b862a1f6c8c4148fe31865eedaf1a"
integrity sha512-kgnbRDj8ioDyGxoiaXsiu1Ybm/K14ajCgMOkwiqpHrnF7d7QiYRoRqHIpglMMs3DwXinlK4qJ8TZGlj4hfleJg==
version "18.19.10"
resolved "https://registry.yarnpkg.com/@types/node/-/node-18.19.10.tgz#4de314ab66faf6bc8ba691021a091ddcdf13a158"
integrity sha512-IZD8kAM02AW1HRDTPOlz3npFava678pr8Ie9Vp8uRhBROXAv8MXT2pCnGZZAKYdromsNQLHQcfWQ6EOatVLtqA==
dependencies:
undici-types "~5.26.4"
@ -6106,9 +6113,9 @@
integrity sha512-xTE1E+YF4aWPJJeUzaZI5DRntlkY3+BCVJi0axFptnjGmAoWxkyREIh/XMrfxVLejwQxMCfDXdICo0VLxThrog==
"@types/whatwg-url@^11.0.2":
version "11.0.3"
resolved "https://registry.yarnpkg.com/@types/whatwg-url/-/whatwg-url-11.0.3.tgz#9f584c9a9421f0971029ee504dd62a831cb8f3aa"
integrity sha512-z1ELvMijRL1QmU7QuzDkeYXSF2+dXI0ITKoQsIoVKcNBOiK5RMmWy+pYYxJTHFt8vkpZe7UsvRErQwcxZkjoUw==
version "11.0.4"
resolved "https://registry.yarnpkg.com/@types/whatwg-url/-/whatwg-url-11.0.4.tgz#ffed0dc8d89d91f62e3f368fcbda222a487c4f63"
integrity sha512-lXCmTWSHJvf0TRSO58nm978b8HJ/EdsSsEKLd3ODHFjo+3VGAyyTp4v50nWvwtzBxSMQrVOK7tcuN0zGPLICMw==
dependencies:
"@types/webidl-conversions" "*"
@ -6558,16 +6565,11 @@ acorn-walk@^7.1.1:
resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-7.2.0.tgz#0de889a601203909b0fbe07b8938dc21d2e967bc"
integrity sha512-OPdCF6GsMIP+Az+aWfAAOEt2/+iVDKE7oy6lJ098aoe59oAmK76qV6Gw60SbZ8jHuG2wH058GF4pLFbYamYrVA==
acorn-walk@^8.0.2, acorn-walk@^8.1.1:
acorn-walk@^8.0.2, acorn-walk@^8.1.1, acorn-walk@^8.2.0:
version "8.2.0"
resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-8.2.0.tgz#741210f2e2426454508853a2f44d0ab83b7f69c1"
integrity sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==
acorn-walk@^8.2.0:
version "8.3.2"
resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-8.3.2.tgz#7703af9415f1b6db9315d6895503862e231d34aa"
integrity sha512-cjkyv4OtNCIeqhHrfS81QWXoCBPExR/J62oyEqepVw8WaQeSqpW2uhuLPh1m9eWhDuOo/jUXVTlifvesOWp/4A==
acorn@^5.2.1, acorn@^5.7.3:
version "5.7.4"
resolved "https://registry.yarnpkg.com/acorn/-/acorn-5.7.4.tgz#3e8d8a9947d0599a1796d10225d7432f4a4acf5e"
@ -6578,10 +6580,10 @@ acorn@^7.1.1:
resolved "https://registry.yarnpkg.com/acorn/-/acorn-7.4.1.tgz#feaed255973d2e77555b83dbc08851a6c63520fa"
integrity sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==
acorn@^8.1.0, acorn@^8.10.0, acorn@^8.11.3, acorn@^8.2.4, acorn@^8.4.1, acorn@^8.5.0, acorn@^8.7.1, acorn@^8.8.1, acorn@^8.8.2, acorn@^8.9.0:
version "8.11.3"
resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.11.3.tgz#71e0b14e13a4ec160724b38fb7b0f233b1b81d7a"
integrity sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==
acorn@^8.1.0, acorn@^8.10.0, acorn@^8.2.4, acorn@^8.4.1, acorn@^8.5.0, acorn@^8.7.1, acorn@^8.8.1, acorn@^8.8.2, acorn@^8.9.0:
version "8.11.2"
resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.11.2.tgz#ca0d78b51895be5390a5903c5b3bdcdaf78ae40b"
integrity sha512-nc0Axzp/0FILLEVsm4fNwLCwMttvhEI263QtVPQcbpfZZ3ts0hLsZGOpE6czNlid7CJ9MlyH8reXkpsf3YUY4w==
add-stream@^1.0.0:
version "1.0.0"
@ -7030,7 +7032,7 @@ asn1.js@^5.0.0, asn1.js@^5.2.0, asn1.js@^5.4.1:
minimalistic-assert "^1.0.0"
safer-buffer "^2.1.0"
asn1@^0.2.6, asn1@~0.2.3:
asn1@^0.2.4, 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==
@ -7081,7 +7083,12 @@ async@^2.6.3:
dependencies:
lodash "^4.17.14"
async@^3.2.1, async@^3.2.3, async@^3.2.4:
async@^3.2.1, async@^3.2.3:
version "3.2.4"
resolved "https://registry.yarnpkg.com/async/-/async-3.2.4.tgz#2d22e00f8cddeb5fde5dd33522b56d1cf569a81c"
integrity sha512-iAB+JbDEGXhyIUavoDl9WP/Jj106Kz9DEn1DPgYw5ruDn0e3Wgi3sKFm55sASdGBNOQB8F59d9qQ7deqrHA8wQ==
async@^3.2.4:
version "3.2.5"
resolved "https://registry.yarnpkg.com/async/-/async-3.2.5.tgz#ebd52a8fdaf7a2289a24df399f8d8485c8a46b66"
integrity sha512-baNZyqaaLhyLVKm/DlvdW051MSgO6b8eVfIezl9E5PqWxFgzLm/wQntEW4zOytVburDEr0JlALEpdOFwvErLsg==
@ -7684,6 +7691,11 @@ bufferutil@^4.0.1:
dependencies:
node-gyp-build "^4.3.0"
buildcheck@0.0.3:
version "0.0.3"
resolved "https://registry.yarnpkg.com/buildcheck/-/buildcheck-0.0.3.tgz#70451897a95d80f7807e68fc412eb2e7e35ff4d5"
integrity sha512-pziaA+p/wdVImfcbsZLNF32EiWyujlQLwolMqUQE8xpKNOH7KmZQaY8sXN7DGOEzPAElo9QTaeNRfGnf3iOJbA==
buildcheck@~0.0.6:
version "0.0.6"
resolved "https://registry.yarnpkg.com/buildcheck/-/buildcheck-0.0.6.tgz#89aa6e417cfd1e2196e3f8fe915eb709d2fe4238"
@ -7972,9 +7984,9 @@ catharsis@^0.9.0:
lodash "^4.17.15"
chai@^4.3.7:
version "4.4.1"
resolved "https://registry.yarnpkg.com/chai/-/chai-4.4.1.tgz#3603fa6eba35425b0f2ac91a009fe924106e50d1"
integrity sha512-13sOfMv2+DWduEU+/xbun3LScLoqN17nBeTLUsmDfKdoiC1fr0n9PU4guu4AhRcOVFk/sW8LyZWHuhWtQZiF+g==
version "4.3.10"
resolved "https://registry.yarnpkg.com/chai/-/chai-4.3.10.tgz#d784cec635e3b7e2ffb66446a63b4e33bd390384"
integrity sha512-0UXG04VuVbruMUYbJ6JctvH0YnC/4q3/AkT18q4NaITo91CUm0liMS9VqzT9vZhVQ/1eqPanMWjBM+Juhfb/9g==
dependencies:
assertion-error "^1.1.0"
check-error "^1.0.3"
@ -8716,6 +8728,14 @@ cosmiconfig@^8.2.0:
parse-json "^5.0.0"
path-type "^4.0.0"
cpu-features@~0.0.4:
version "0.0.4"
resolved "https://registry.yarnpkg.com/cpu-features/-/cpu-features-0.0.4.tgz#0023475bb4f4c525869c162e4108099e35bf19d8"
integrity sha512-fKiZ/zp1mUwQbnzb9IghXtHtDoTMtNeb8oYGx6kX2SYfhnG0HNdBEBIzB9b5KlXu5DQPhfy3mInbBxFcgwAr3A==
dependencies:
buildcheck "0.0.3"
nan "^2.15.0"
cpu-features@~0.0.9:
version "0.0.9"
resolved "https://registry.yarnpkg.com/cpu-features/-/cpu-features-0.0.9.tgz#5226b92f0f1c63122b0a3eb84cb8335a4de499fc"
@ -9615,9 +9635,9 @@ diff@^4.0.1:
integrity sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==
diff@^5.1.0:
version "5.2.0"
resolved "https://registry.yarnpkg.com/diff/-/diff-5.2.0.tgz#26ded047cd1179b78b9537d5ef725503ce1ae531"
integrity sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A==
version "5.1.0"
resolved "https://registry.yarnpkg.com/diff/-/diff-5.1.0.tgz#bc52d298c5ea8df9194800224445ed43ffc87e40"
integrity sha512-D+mk+qE8VC/PAUrlAU34N+VfXev0ghe5ywmpqrawphmVZc1bEfn56uo9qpyGp1p4xpzOHkSW4ztBd6L7Xx4ACw==
diffie-hellman@^5.0.0:
version "5.0.3"
@ -9673,7 +9693,16 @@ docker-modem@^3.0.0:
split-ca "^1.0.1"
ssh2 "^1.11.0"
dockerode@^3.2.1, dockerode@^3.3.5:
dockerode@^3.2.1:
version "3.3.4"
resolved "https://registry.yarnpkg.com/dockerode/-/dockerode-3.3.4.tgz#875de614a1be797279caa9fe27e5637cf0e40548"
integrity sha512-3EUwuXnCU+RUlQEheDjmBE0B7q66PV9Rw5NiH1sXwINq0M9c5ERP9fxgkw36ZHOtzf4AGEEYySnkx/sACC9EgQ==
dependencies:
"@balena/dockerignore" "^1.0.2"
docker-modem "^3.0.0"
tar-fs "~2.0.1"
dockerode@^3.3.5:
version "3.3.5"
resolved "https://registry.yarnpkg.com/dockerode/-/dockerode-3.3.5.tgz#7ae3f40f2bec53ae5e9a741ce655fff459745629"
integrity sha512-/0YNa3ZDNeLr/tSckmD69+Gq+qVNhvKfAHNeZJBnp7EOP6RGKV8ORrJHkUn20So5wU+xxT7+1n5u8PjHbfjbSA==
@ -9803,9 +9832,9 @@ dotenv@8.6.0, dotenv@^8.2.0:
integrity sha512-IrPdXQsk2BbzvCBGBOTmmSH5SodmqZNt4ERAZDmW4CT+tL8VtvinqywuANaFu4bOMWki16nqf0e4oC0QIaDr/g==
dotenv@^16.3.1:
version "16.3.1"
resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.3.1.tgz#369034de7d7e5b120972693352a3bf112172cc3e"
integrity sha512-IPzF4w4/Rd94bA9imS68tZBaYyBWSCE47V1RGuMrB94iyTOIEwRmVL2x/4An+6mETpLrKJ5hQkB8W4kFAadeIQ==
version "16.4.1"
resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.4.1.tgz#1d9931f1d3e5d2959350d1250efab299561f7f11"
integrity sha512-CjA3y+Dr3FyFDOAMnxZEGtnW9KBR2M0JvvUtXNW+dYJL5ROWxP9DUHCwgFqpMk0OXCc0ljhaNTr2w/kutYIcHQ==
dotenv@~10.0.0:
version "10.0.0"
@ -10853,13 +10882,20 @@ fast-xml-parser@4.2.5:
dependencies:
strnum "^1.0.5"
fast-xml-parser@^4.1.3, fast-xml-parser@^4.2.2, fast-xml-parser@^4.2.5:
fast-xml-parser@^4.1.3:
version "4.3.3"
resolved "https://registry.yarnpkg.com/fast-xml-parser/-/fast-xml-parser-4.3.3.tgz#aeaf5778392329f17168c40c51bcbfec8ff965be"
integrity sha512-coV/D1MhrShMvU6D0I+VAK3umz6hUaxxhL0yp/9RjfiYUfAv14rDhGQL+PLForhMdr0wq3PiV07WtkkNjJjNHg==
dependencies:
strnum "^1.0.5"
fast-xml-parser@^4.2.2, fast-xml-parser@^4.2.5:
version "4.3.2"
resolved "https://registry.yarnpkg.com/fast-xml-parser/-/fast-xml-parser-4.3.2.tgz#761e641260706d6e13251c4ef8e3f5694d4b0d79"
integrity "sha1-dh5kEmBwbW4TJRxO+OP1aU1LDXk= sha512-rmrXUXwbJedoXkStenj1kkljNF7ugn5ZjR9FJcwmCfcCbtOMDghPajbc+Tck6vE6F5XsDmx+Pr2le9fw8+pXBg=="
dependencies:
strnum "^1.0.5"
fastest-levenshtein@^1.0.12:
version "1.0.16"
resolved "https://registry.yarnpkg.com/fastest-levenshtein/-/fastest-levenshtein-1.0.16.tgz#210e61b6ff181de91ea9b3d1b84fdedd47e034e5"
@ -10919,7 +10955,7 @@ fetch-cookie@0.11.0:
dependencies:
tough-cookie "^2.3.3 || ^3.0.1 || ^4.0.0"
fflate@^0.4.1:
fflate@^0.4.1, fflate@^0.4.8:
version "0.4.8"
resolved "https://registry.yarnpkg.com/fflate/-/fflate-0.4.8.tgz#f90b82aefbd8ac174213abb338bd7ef848f0f5ae"
integrity sha512-FJqqoDBR00Mdj9ppamLa/Y7vxm+PRmNWA67N846RvsoYVMKB4q3y/de5PA7gUmRMYK/8CMz2GDZQmCRN1wBcWA==
@ -15701,17 +15737,7 @@ mkdirp@^1.0.3, mkdirp@^1.0.4:
resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e"
integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==
mlly@^1.1.0:
version "1.6.0"
resolved "https://registry.yarnpkg.com/mlly/-/mlly-1.6.0.tgz#0ecfbddc706857f5e170ccd28c6b0b9c81d3f548"
integrity sha512-YOvg9hfYQmnaB56Yb+KrJE2u0Yzz5zR+sLejEvF4fzwzV1Al6hkf2vyHTwqCRyv0hCi9rVCqVoXpyYevQIRwLQ==
dependencies:
acorn "^8.11.3"
pathe "^1.1.2"
pkg-types "^1.0.3"
ufo "^1.3.2"
mlly@^1.2.0:
mlly@^1.1.0, mlly@^1.2.0:
version "1.4.2"
resolved "https://registry.yarnpkg.com/mlly/-/mlly-1.4.2.tgz#7cf406aa319ff6563d25da6b36610a93f2a8007e"
integrity sha512-i/Ykufi2t1EZ6NaPLdfnZk2AX8cs0d+mTzVKuPfqPKPatxLApaBoxJQ9x1/uckXtrS/U5oisPMDkNs0yQTaBRg==
@ -15904,6 +15930,11 @@ named-placeholders@^1.1.3:
dependencies:
lru-cache "^7.14.1"
nan@^2.15.0, nan@^2.16.0:
version "2.17.0"
resolved "https://registry.yarnpkg.com/nan/-/nan-2.17.0.tgz#c0150a2368a182f033e9aa5195ec76ea41a199cb"
integrity sha512-2ZTgtl0nJsO0KQCjEpxcIr5D+Yv90plTitZt9JBfQvVJDS5seMl3FOvsh3+9CoYWXf/1l5OaZzzF6nDm4cagaQ==
nan@^2.17.0, nan@^2.18.0:
version "2.18.0"
resolved "https://registry.yarnpkg.com/nan/-/nan-2.18.0.tgz#26a6faae7ffbeb293a39660e88a76b82e30b7554"
@ -17287,11 +17318,6 @@ pathe@^1.1.0, pathe@^1.1.1:
resolved "https://registry.yarnpkg.com/pathe/-/pathe-1.1.1.tgz#1dd31d382b974ba69809adc9a7a347e65d84829a"
integrity sha512-d+RQGp0MAYTIaDBIMmOfMwz3E+LOZnxx1HZd5R18mmCZY0QBlK0LDZfPc8FW8Ed2DlvsuE6PRjroDY+wg4+j/Q==
pathe@^1.1.2:
version "1.1.2"
resolved "https://registry.yarnpkg.com/pathe/-/pathe-1.1.2.tgz#6c4cb47a945692e48a1ddd6e4094d170516437ec"
integrity sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==
pathval@^1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/pathval/-/pathval-1.1.1.tgz#8534e77a77ce7ac5a2512ea21e0fdb8fcf6c3d8d"
@ -17916,10 +17942,18 @@ postgres-interval@^1.1.0:
dependencies:
xtend "^4.0.0"
posthog-js@^1.13.4, posthog-js@^1.36.0:
version "1.100.0"
resolved "https://registry.yarnpkg.com/posthog-js/-/posthog-js-1.100.0.tgz#687b9a6e4ed226aa6572f4040b418ea0c8b3d353"
integrity sha512-r2XZEiHQ9mBK7D1G9k57I8uYZ2kZTAJ0OCX6K/OOdCWN8jKPhw3h5F9No5weilP6eVAn+hrsy7NvPV7SCX7gMg==
posthog-js@^1.13.4:
version "1.103.1"
resolved "https://registry.yarnpkg.com/posthog-js/-/posthog-js-1.103.1.tgz#f846c413c28aca204dc1527f49d39f651348f3c4"
integrity sha512-cFXFU4Z4kl/+RUUV4ju1DlfM7dwCGi6H9xWsfhljIhGcBbT8UfS4JGgZGXl9ABQDdgDPb9xciqnysFSsUQshTA==
dependencies:
fflate "^0.4.8"
preact "^10.19.3"
posthog-js@^1.36.0:
version "1.96.1"
resolved "https://registry.yarnpkg.com/posthog-js/-/posthog-js-1.96.1.tgz#4f9719a24e4e14037b0e72d430194d7cdb576447"
integrity sha512-kv1vQqYMt2BV3YHS+wxsbGuP+tz+M3y1AzNhz8TfkpY1HT8W/ONT0i0eQpeRr9Y+d4x/fZ6M4cXG5GMvi9lRCA==
dependencies:
fflate "^0.4.1"
@ -18160,6 +18194,11 @@ pprof-format@^2.0.7:
resolved "https://registry.yarnpkg.com/pprof-format/-/pprof-format-2.0.7.tgz#526e4361f8b37d16b2ec4bb0696b5292de5046a4"
integrity sha512-1qWaGAzwMpaXJP9opRa23nPnt2Egi7RMNoNBptEE/XwHbcn4fC2b/4U4bKc5arkGkIh2ZabpF2bEb+c5GNHEKA==
preact@^10.19.3:
version "10.19.3"
resolved "https://registry.yarnpkg.com/preact/-/preact-10.19.3.tgz#7a7107ed2598a60676c943709ea3efb8aaafa899"
integrity sha512-nHHTeFVBTHRGxJXKkKu5hT8C/YWBkPso4/Gad6xuj5dbptt9iF9NZr9pHbPhBrnT2klheu7mHTxTZ/LjwJiEiQ==
prebuild-install@^7.1.1:
version "7.1.1"
resolved "https://registry.yarnpkg.com/prebuild-install/-/prebuild-install-7.1.1.tgz#de97d5b34a70a0c81334fd24641f2a1702352e45"
@ -19375,25 +19414,25 @@ rollup@^3.27.1:
fsevents "~2.3.2"
rollup@^4.9.6:
version "4.10.0"
resolved "https://registry.yarnpkg.com/rollup/-/rollup-4.10.0.tgz#244c2cb54a8de004a949fe6036a0801be9060456"
integrity sha512-t2v9G2AKxcQ8yrG+WGxctBes1AomT0M4ND7jTFBCVPXQ/WFTvNSefIrNSmLKhIKBrvN8SG+CZslimJcT3W2u2g==
version "4.12.0"
resolved "https://registry.yarnpkg.com/rollup/-/rollup-4.12.0.tgz#0b6d1e5f3d46bbcf244deec41a7421dc54cc45b5"
integrity sha512-wz66wn4t1OHIJw3+XU7mJJQV/2NAfw5OAk6G6Hoo3zcvz/XOfQ52Vgi+AN4Uxoxi0KBBwk2g8zPrTDA4btSB/Q==
dependencies:
"@types/estree" "1.0.5"
optionalDependencies:
"@rollup/rollup-android-arm-eabi" "4.10.0"
"@rollup/rollup-android-arm64" "4.10.0"
"@rollup/rollup-darwin-arm64" "4.10.0"
"@rollup/rollup-darwin-x64" "4.10.0"
"@rollup/rollup-linux-arm-gnueabihf" "4.10.0"
"@rollup/rollup-linux-arm64-gnu" "4.10.0"
"@rollup/rollup-linux-arm64-musl" "4.10.0"
"@rollup/rollup-linux-riscv64-gnu" "4.10.0"
"@rollup/rollup-linux-x64-gnu" "4.10.0"
"@rollup/rollup-linux-x64-musl" "4.10.0"
"@rollup/rollup-win32-arm64-msvc" "4.10.0"
"@rollup/rollup-win32-ia32-msvc" "4.10.0"
"@rollup/rollup-win32-x64-msvc" "4.10.0"
"@rollup/rollup-android-arm-eabi" "4.12.0"
"@rollup/rollup-android-arm64" "4.12.0"
"@rollup/rollup-darwin-arm64" "4.12.0"
"@rollup/rollup-darwin-x64" "4.12.0"
"@rollup/rollup-linux-arm-gnueabihf" "4.12.0"
"@rollup/rollup-linux-arm64-gnu" "4.12.0"
"@rollup/rollup-linux-arm64-musl" "4.12.0"
"@rollup/rollup-linux-riscv64-gnu" "4.12.0"
"@rollup/rollup-linux-x64-gnu" "4.12.0"
"@rollup/rollup-linux-x64-musl" "4.12.0"
"@rollup/rollup-win32-arm64-msvc" "4.12.0"
"@rollup/rollup-win32-ia32-msvc" "4.12.0"
"@rollup/rollup-win32-x64-msvc" "4.12.0"
fsevents "~2.3.2"
rotating-file-stream@3.1.0:
@ -19425,7 +19464,14 @@ rxjs@^6.6.6:
dependencies:
tslib "^1.9.0"
rxjs@^7.5.5, rxjs@^7.8.1:
rxjs@^7.5.5:
version "7.8.0"
resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-7.8.0.tgz#90a938862a82888ff4c7359811a595e14e1e09a4"
integrity sha512-F2+gxDshqmIub1KdvZkaEfGDwLNpPvk9Fs6LD/MyQxNgMds/WH9OdDDXOmxUZpME+iSK3rQCctkL0DYyytUqMg==
dependencies:
tslib "^2.1.0"
rxjs@^7.8.1:
version "7.8.1"
resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-7.8.1.tgz#6f6f3d99ea8044291efd92e7c7fcf562c4057543"
integrity sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==
@ -20139,7 +20185,18 @@ ssh-remote-port-forward@^1.0.4:
"@types/ssh2" "^0.5.48"
ssh2 "^1.4.0"
ssh2@^1.11.0, ssh2@^1.4.0:
ssh2@^1.11.0:
version "1.11.0"
resolved "https://registry.yarnpkg.com/ssh2/-/ssh2-1.11.0.tgz#ce60186216971e12f6deb553dcf82322498fe2e4"
integrity sha512-nfg0wZWGSsfUe/IBJkXVll3PEZ//YH2guww+mP88gTpuSU4FtZN7zu9JoeTGOyCNx2dTDtT9fOpWwlzyj4uOOw==
dependencies:
asn1 "^0.2.4"
bcrypt-pbkdf "^1.0.2"
optionalDependencies:
cpu-features "~0.0.4"
nan "^2.16.0"
ssh2@^1.4.0:
version "1.15.0"
resolved "https://registry.yarnpkg.com/ssh2/-/ssh2-1.15.0.tgz#2f998455036a7f89e0df5847efb5421748d9871b"
integrity sha512-C0PHgX4h6lBxYx7hcXwu3QWdh4tg6tZZsTfXcdvc5caW/EMxaB4H9dWsl7qk+F7LAW762hp8VbXOX7x4xUYvEw==
@ -20224,9 +20281,9 @@ statuses@2.0.1, statuses@^2.0.0:
integrity sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==
std-env@^3.3.1:
version "3.7.0"
resolved "https://registry.yarnpkg.com/std-env/-/std-env-3.7.0.tgz#c9f7386ced6ecf13360b6c6c55b8aaa4ef7481d2"
integrity sha512-JPbdCEQLj1w5GilpiHAx3qJvFndqybBysA3qUOnznweH4QbNYUsW/ea8QzSrnh0vNsezMMw5bcVool8lM0gwzg==
version "3.4.3"
resolved "https://registry.yarnpkg.com/std-env/-/std-env-3.4.3.tgz#326f11db518db751c83fd58574f449b7c3060910"
integrity sha512-f9aPhy8fYBuMN+sNfakZV18U39PbalgjXG3lLB9WkaYTxijru61wb57V9wxxNthXM5Sd88ETBWi29qLAsHO52Q==
step@0.0.x:
version "0.0.6"
@ -20689,9 +20746,9 @@ svelte-spa-router@^4.0.1:
regexparam "2.0.2"
svelte@^4.2.10:
version "4.2.10"
resolved "https://registry.yarnpkg.com/svelte/-/svelte-4.2.10.tgz#3bef8d79ca75eb53cc4d03f9fac1546e60393f77"
integrity sha512-Ep06yCaCdgG1Mafb/Rx8sJ1QS3RW2I2BxGp2Ui9LBHSZ2/tO/aGLc5WqPjgiAP6KAnLJGaIr/zzwQlOo1b8MxA==
version "4.2.12"
resolved "https://registry.yarnpkg.com/svelte/-/svelte-4.2.12.tgz#13d98d2274d24d3ad216c8fdc801511171c70bb1"
integrity sha512-d8+wsh5TfPwqVzbm4/HCXC783/KPHV60NvwitJnyTA5lWn1elhXMNWhXGCJ7PwPa8qFUnyJNIyuIRt2mT0WMug==
dependencies:
"@ampproject/remapping" "^2.2.1"
"@jridgewell/sourcemap-codec" "^1.4.15"
@ -21120,9 +21177,9 @@ tiny-queue@^0.2.0:
integrity sha512-EijGsv7kzd9I9g0ByCl6h42BWNGUZrlCSejfrb3AKeHC33SGbASu1VDf5O3rRiiUOhAC9CHdZxFPbZu0HmR70A==
tinybench@^2.3.1:
version "2.6.0"
resolved "https://registry.yarnpkg.com/tinybench/-/tinybench-2.6.0.tgz#1423284ee22de07c91b3752c048d2764714b341b"
integrity sha512-N8hW3PG/3aOoZAN5V/NSAEDz0ZixDSSt5b/a05iqtpgfLWMSVuCo7w0k2vVvEjdrIoeGqZzweX2WlyioNIHchA==
version "2.5.1"
resolved "https://registry.yarnpkg.com/tinybench/-/tinybench-2.5.1.tgz#3408f6552125e53a5a48adee31261686fd71587e"
integrity sha512-65NKvSuAVDP/n4CqH+a9w2kTlLReS9vhsAP06MWx+/89nMinJyB2icyl58RIcqCmIggpojIGeuJGhjU1aGMBSg==
tinycolor2@^1.6.0:
version "1.6.0"
@ -21562,11 +21619,6 @@ ufo@^1.3.0:
resolved "https://registry.yarnpkg.com/ufo/-/ufo-1.3.1.tgz#e085842f4627c41d4c1b60ebea1f75cdab4ce86b"
integrity sha512-uY/99gMLIOlJPwATcMVYfqDSxUR9//AUcgZMzwfSTJPDKzA1S8mX4VLqa+fiAtveraQUBCz4FFcwVZBGbwBXIw==
ufo@^1.3.2:
version "1.4.0"
resolved "https://registry.yarnpkg.com/ufo/-/ufo-1.4.0.tgz#39845b31be81b4f319ab1d99fd20c56cac528d32"
integrity sha512-Hhy+BhRBleFjpJ2vchUNN40qgkh0366FWJGqVLYBHev0vpHTrXSA0ryT+74UiW6KWsldNurQMKGqCm1M2zBciQ==
uglify-js@^3.1.4, uglify-js@^3.7.7:
version "3.17.4"
resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.17.4.tgz#61678cf5fa3f5b7eb789bb345df29afb8257c22c"
@ -21611,9 +21663,9 @@ underscore@~1.13.2:
integrity sha512-+A5Sja4HP1M08MaXya7p5LvjuM7K6q/2EaC0+iovj/wOcMsTzMvDFbasi/oSapiwOlt252IqsKqPjCl7huKS0A==
undici-types@^6.0.1:
version "6.0.1"
resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-6.0.1.tgz#62e2af9fcd3ce359634175658de39df8d0f37197"
integrity sha512-i9dNdkCziyqGpFxhatR9LITcInbFWh+ExlWkrZQpZHje8FfCcJKgps0IbmMd7D1o8c8syG4pIOV+aKIoC9JEyA==
version "6.6.2"
resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-6.6.2.tgz#48c65d30bfcae492c3c89b1d147fed9d43a16b79"
integrity sha512-acoBcoBobgsg3YUEO/Oht8JJCuFYpzWLFKbqEbcEZcXdkQrTzkF/yWj9JoLaFDa6ArI31dFEmNZkCjQZ7mlf7w==
undici-types@~5.26.4:
version "5.26.5"
@ -21626,9 +21678,9 @@ undici@^4.14.1:
integrity sha512-tkZSECUYi+/T1i4u+4+lwZmQgLXd4BLGlrc7KZPcLIW7Jpq99+Xpc30ONv7nS6F5UNOxp/HBZSSL9MafUrvJbw==
undici@^6.0.1:
version "6.0.1"
resolved "https://registry.yarnpkg.com/undici/-/undici-6.0.1.tgz#385572addca36d1c2b280629cb694b726170027e"
integrity sha512-eZFYQLeS9BiXpsU0cuFhCwfeda2MnC48EVmmOz/eCjsTgmyTdaHdVsPSC/kwC2GtW2e0uH0HIPbadf3/bRWSxw==
version "6.6.2"
resolved "https://registry.yarnpkg.com/undici/-/undici-6.6.2.tgz#8dce5ae54e8a3bc7140c2b2a0972b5fde9a88efb"
integrity sha512-vSqvUE5skSxQJ5sztTZ/CdeJb1Wq0Hf44hlYMciqHghvz+K88U0l7D6u1VsndoFgskDcnU+nG3gYmMzJVzd9Qg==
dependencies:
"@fastify/busboy" "^2.0.0"
@ -21966,18 +22018,7 @@ vite-plugin-static-copy@^0.17.0:
fs-extra "^11.1.0"
picocolors "^1.0.0"
"vite@^3.0.0 || ^4.0.0":
version "4.5.2"
resolved "https://registry.yarnpkg.com/vite/-/vite-4.5.2.tgz#d6ea8610e099851dad8c7371599969e0f8b97e82"
integrity sha512-tBCZBNSBbHQkaGyhGCDUGqeo2ph8Fstyp6FMSvTtsXeZSPpSMGlviAOav2hxVTqFcx8Hj/twtWKsMJXNY0xI8w==
dependencies:
esbuild "^0.18.10"
postcss "^8.4.27"
rollup "^3.27.1"
optionalDependencies:
fsevents "~2.3.2"
vite@^4.5.0:
"vite@^3.0.0 || ^4.0.0", vite@^4.5.0:
version "4.5.0"
resolved "https://registry.yarnpkg.com/vite/-/vite-4.5.0.tgz#ec406295b4167ac3bc23e26f9c8ff559287cff26"
integrity sha512-ulr8rNLA6rkyFAlVWw2q5YJ91v098AFQ2R0PRFwPzREXOUJQPtFUG0t+/ZikhaOCDqFoDhN6/v8Sq0o4araFAw==
@ -22611,7 +22652,12 @@ yaml@^1.10.2:
resolved "https://registry.yarnpkg.com/yaml/-/yaml-1.10.2.tgz#2301c5ffbf12b467de8da2333a459e29e7920e4b"
integrity sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==
yaml@^2.1.1, yaml@^2.2.2:
yaml@^2.1.1:
version "2.3.2"
resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.3.2.tgz#f522db4313c671a0ca963a75670f1c12ea909144"
integrity sha512-N/lyzTPaJasoDmfV7YTrYCI0G/3ivm/9wdG0aHuheKowWQwGTsK0Eoiw6utmzAnI6pkJa0DUVygvp3spqqEKXg==
yaml@^2.2.2:
version "2.3.4"
resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.3.4.tgz#53fc1d514be80aabf386dc6001eb29bf3b7523b2"
integrity sha512-8aAvwVUSHpfEqTQ4w/KMlf3HcRdt50E5ODIQJBw1fQ5RL34xabzxtUlzTXVqc4rkZsPbvrXKWnABCD7kWSmocA==