diff --git a/i18n/README.kr.md b/i18n/README.kr.md
new file mode 100644
index 0000000000..09fc83569b
--- /dev/null
+++ b/i18n/README.kr.md
@@ -0,0 +1,221 @@
+
+
+
+
+
+
+ Budibase
+
+
+ 자체 인프라에서 몇 분 만에 맞춤형 비즈니스 도구를 구축하세요.
+
+
+ Budibase는 개발자와 IT 전문가가 몇 분 만에 맞춤형 애플리케이션을 구축하고 자동화할 수 있는 오픈 소스 로우코드 플랫폼입니다.
+
+
+
+ 🤖 🎨 🚀
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 소개
+ ·
+ 문서
+ ·
+ 기능 요청
+ ·
+ 버그 보고
+ ·
+ 지원: 토론
+
+
+
+## ✨ 특징
+
+### "실제" 소프트웨어를 구축할 수 있습니다.
+Budibase를 사용하면 고성능 단일 페이지 애플리케이션을 구축할 수 있습니다. 또한 반응형 디자인으로 제작하여 사용자에게 멋진 경험을 제공할 수 있습니다.
+
+
+### 오픈 소스 및 확장성
+Budibase는 오픈소스이며, GPL v3 라이선스에 따라 공개되어 있습니다. 이는 Budibase가 항상 당신 곁에 있다는 안도감을 줄 것입니다. 그리고 우리는 개발자 친화적인 환경을 제공하고 있기 때문에, 당신은 원하는 만큼 소스 코드를 포크하여 수정하거나 Budibase에 직접 기여할 수 있습니다.
+
+
+### 기존 데이터 또는 처음부터 시작
+Budibase를 사용하면 다음과 같은 여러 소스에서 데이터를 가져올 수 있습니다: MondoDB, CouchDB, PostgreSQL, MySQL, Airtable, S3, DynamoDB 또는 REST API.
+
+또는 원하는 경우 외부 도구 없이도 Budibase를 사용하여 처음부터 시작하여 자체 애플리케이션을 구축할 수 있습니다.[데이터 소스 제안](https://github.com/Budibase/budibase/discussions?discussions_q=category%3AIdeas).
+
+
+
+
+
+
+### 강력한 내장 구성 요소로 애플리케이션을 설계하고 구축할 수 있습니다.
+
+Budibase에는 아름답게 디자인된 강력한 컴포넌트들이 제공되며, 이를 사용하여 UI를 쉽게 구축할 수 있습니다. 또한, CSS를 통한 스타일링 옵션도 풍부하게 제공되어 보다 창의적인 표현도 가능하다.
+ [Request new component](https://github.com/Budibase/budibase/discussions?discussions_q=category%3AIdeas).
+
+
+
+
+
+
+### 프로세스를 자동화하고, 다른 도구와 연동하고, 웹훅으로 연결하세요!
+워크플로우와 수동 프로세스를 자동화하여 시간을 절약하세요. 웹훅 이벤트 연결부터 이메일 자동화까지, Budibase에 수행할 작업을 지시하기만 하면 자동으로 처리됩니다. [새로운 자동화 만들기](https://github.com/Budibase/automations)또는[새로운 자동화를 요청할 수 있습니다](https://github.com/Budibase/budibase/discussions?discussions_q=category%3AIdeas).
+
+
+
+
+
+
+### 선호하는 도구
+Budibase는 사용자의 선호도에 따라 애플리케이션을 구축할 수 있는 다양한 도구를 통합하고 있습니다.
+
+
+
+
+
+
+### 관리자의 천국
+Budibase는 어떤 규모의 프로젝트에도 유연하게 대응할 수 있으며, Budibase를 사용하면 개인 또는 조직의 서버에서 자체 호스팅하고 사용자, 온보딩, SMTP, 앱, 그룹, 테마 등을 한꺼번에 관리할 수 있습니다. 또한, 사용자나 그룹에 앱 포털을 제공하고 그룹 관리자에게 사용자 관리를 맡길 수도 있다.
+- 프로모션 비디오: https://youtu.be/xoljVpty_Kw
+
+
+
+## 🏁 시작
+
+Docker, Kubernetes 또는 Digital Ocean을 사용하여 자체 인프라에서 Budibase를 호스팅하거나, 걱정 없이 빠르게 애플리케이션을 구축하려는 경우 클라우드에서 Budibase를 사용할 수 있습니다.
+
+### [Budibase 셀프 호스팅으로 시작하기](https://docs.budibase.com/docs/hosting-methods)
+
+- [Docker - single ARM compatible image](https://docs.budibase.com/docs/docker)
+- [Docker Compose](https://docs.budibase.com/docs/docker-compose)
+- [Kubernetes](https://docs.budibase.com/docs/kubernetes-k8s)
+- [Digital Ocean](https://docs.budibase.com/docs/digitalocean)
+- [Portainer](https://docs.budibase.com/docs/portainer)
+
+
+### [클라우드에서 Budibase 시작하기](https://budibase.com)
+
+
+
+## 🎓 Budibase 알아보기
+
+문서 [documentacion de Budibase](https://docs.budibase.com/docs).
+
+
+
+
+
+## 💬 커뮤니티
+
+질문하고, 다른 사람을 돕고, 다른 Budibase 사용자와 즐거운 대화를 나눌 수 있는 Budibase 커뮤니티에 여러분을 초대합니다.
+[깃허브 토론](https://github.com/Budibase/budibase/discussions)
+
+
+
+## ❗ 행동강령
+
+Budibase 는 모든 계층의 사람들을 환영하고 상호 존중하는 환경을 제공하는 데 특별한 주의를 기울이고 있습니다. 저희는 커뮤니티에도 같은 기대를 가지고 있습니다.
+[**행동 강령**](https://github.com/Budibase/budibase/blob/HEAD/.github/CODE_OF_CONDUCT.md).
+
+
+
+
+
+## 🙌 Contribuir en Budibase
+
+버그 신고부터 코드의 버그 수정에 이르기까지 모든 기여를 감사하고 환영합니다. 새로운 기능을 구현하거나 API를 변경할 계획이 있다면 [여기에 새 메시지](https://github.com/Budibase/budibase/issues),
+이렇게 하면 여러분의 노력이 헛되지 않도록 보장할 수 있습니다.
+
+여기에는 다음을 위해 Budibase 환경을 설정하는 방법에 대한 지침이 나와 있습니다. [여기를 클릭하세요](https://github.com/Budibase/budibase/tree/HEAD/docs/CONTRIBUTING.md).
+
+### 어디서부터 시작해야 할지 혼란스러우신가요?
+이곳은 기여를 시작하기에 최적의 장소입니다! [First time issues project](https://github.com/Budibase/budibase/projects/22).
+
+### 리포지토리 구성
+
+Budibase는 Lerna에서 관리하는 단일 리포지토리입니다. Lerna는 변경 사항이 있을 때마다 이를 동기화하여 Budibase 패키지를 빌드하고 게시합니다. 크게 보면 이러한 패키지가 Budibase를 구성하는 패키지입니다:
+
+- [packages/builder](https://github.com/Budibase/budibase/tree/HEAD/packages/builder) - budibase builder 클라이언트 측의 svelte 애플리케이션 코드가 포함되어 있습니다.
+
+- [packages/client](https://github.com/Budibase/budibase/tree/HEAD/packages/client) - budibase builder 클라이언트 측의 svelte 애플리케이션 코드가 포함되어 있습니다.
+
+- [packages/server](https://github.com/Budibase/budibase/tree/HEAD/packages/server) - Budibase의 서버 부분입니다. 이 Koa 애플리케이션은 빌더에게 Budibase 애플리케이션을 생성하는 데 필요한 것을 제공하는 역할을 합니다. 또한 데이터베이스 및 파일 저장소와 상호 작용할 수 있는 API를 제공합니다.
+
+자세한 내용은 다음 문서를 참조하세요. [CONTRIBUTING.md](https://github.com/Budibase/budibase/blob/HEAD/docs/CONTRIBUTING.md)
+
+
+
+
+## 📝 라이선스
+
+Budibase는 오픈 소스이며, 라이선스는 다음과 같습니다 [GPL v3](https://www.gnu.org/licenses/gpl-3.0.en.html). 클라이언트 및 컴포넌트 라이브러리는 다음과 같이 라이선스가 부여됩니다. [MPL](https://directory.fsf.org/wiki/License:MPL-2.0) - 이렇게 하면 빌드한 애플리케이션에 원하는 대로 라이선스를 부여할 수 있습니다.
+
+
+
+## ⭐ 스타 수의 역사
+
+[![Stargazers over time](https://starchart.cc/Budibase/budibase.svg)](https://starchart.cc/Budibase/budibase)
+
+빌더 업데이트 중 문제가 발생하는 경우 [여기](https://github.com/Budibase/budibase/blob/HEAD/docs/CONTRIBUTING.md#troubleshooting) 를 참고하여 환경을 정리해 주세요.
+
+
+
+## Contributors ✨
+
+훌륭한 여러분께 감사할 따름입니다. ([emoji key](https://allcontributors.org/docs/en/emoji-key)):
+
+
+
+
+
+
+
+
+
+
+
+이 프로젝트는 다음 사양을 따릅니다. [all-contributors](https://github.com/all-contributors/all-contributors).
+모든 종류의 기여를 환영합니다!
diff --git a/lerna.json b/lerna.json
index 54e106cd5a..c06173fe04 100644
--- a/lerna.json
+++ b/lerna.json
@@ -1,5 +1,5 @@
{
- "version": "2.20.10",
+ "version": "2.21.0",
"npmClient": "yarn",
"packages": [
"packages/*",
diff --git a/packages/account-portal b/packages/account-portal
index ab324e35d8..19f7a5829f 160000
--- a/packages/account-portal
+++ b/packages/account-portal
@@ -1 +1 @@
-Subproject commit ab324e35d855012bd0f49caa53c6dd765223c6fa
+Subproject commit 19f7a5829f4d23cbc694136e45d94482a59a475a
diff --git a/packages/backend-core/src/db/couch/DatabaseImpl.ts b/packages/backend-core/src/db/couch/DatabaseImpl.ts
index 0e2b4173b0..7e7c997cbe 100644
--- a/packages/backend-core/src/db/couch/DatabaseImpl.ts
+++ b/packages/backend-core/src/db/couch/DatabaseImpl.ts
@@ -11,6 +11,7 @@ import {
Document,
isDocument,
RowResponse,
+ RowValue,
} from "@budibase/types"
import { getCouchInfo } from "./connections"
import { directCouchUrlCall } from "./utils"
@@ -221,7 +222,7 @@ export class DatabaseImpl implements Database {
})
}
- async allDocs(
+ async allDocs(
params: DatabaseQueryOpts
): Promise> {
return this.performCall(db => {
diff --git a/packages/backend-core/src/db/instrumentation.ts b/packages/backend-core/src/db/instrumentation.ts
index aa2ac424ae..03010d4c92 100644
--- a/packages/backend-core/src/db/instrumentation.ts
+++ b/packages/backend-core/src/db/instrumentation.ts
@@ -1,5 +1,4 @@
import {
- DocumentScope,
DocumentDestroyResponse,
DocumentInsertResponse,
DocumentBulkResponse,
@@ -13,6 +12,7 @@ import {
DatabasePutOpts,
DatabaseQueryOpts,
Document,
+ RowValue,
} from "@budibase/types"
import tracer from "dd-trace"
import { Writable } from "stream"
@@ -79,7 +79,7 @@ export class DDInstrumentedDatabase implements Database {
})
}
- allDocs(
+ allDocs(
params: DatabaseQueryOpts
): Promise> {
return tracer.trace("db.allDocs", span => {
diff --git a/packages/backend-core/src/docIds/ids.ts b/packages/backend-core/src/docIds/ids.ts
index 02176109da..9627b2b94c 100644
--- a/packages/backend-core/src/docIds/ids.ts
+++ b/packages/backend-core/src/docIds/ids.ts
@@ -74,7 +74,7 @@ export function getGlobalIDFromUserMetadataID(id: string) {
* Generates a template ID.
* @param ownerId The owner/user of the template, this could be global or a workspace level.
*/
-export function generateTemplateID(ownerId: any) {
+export function generateTemplateID(ownerId: string) {
return `${DocumentType.TEMPLATE}${SEPARATOR}${ownerId}${SEPARATOR}${newid()}`
}
@@ -105,7 +105,7 @@ export function prefixRoleID(name: string) {
* Generates a new dev info document ID - this is scoped to a user.
* @returns The new dev info ID which info for dev (like api key) can be stored under.
*/
-export const generateDevInfoID = (userId: any) => {
+export const generateDevInfoID = (userId: string) => {
return `${DocumentType.DEV_INFO}${SEPARATOR}${userId}`
}
diff --git a/packages/backend-core/src/objectStore/buckets/plugins.ts b/packages/backend-core/src/objectStore/buckets/plugins.ts
index 6f1b7116ae..02be9345ab 100644
--- a/packages/backend-core/src/objectStore/buckets/plugins.ts
+++ b/packages/backend-core/src/objectStore/buckets/plugins.ts
@@ -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 []
}
diff --git a/packages/backend-core/src/redis/utils.ts b/packages/backend-core/src/redis/utils.ts
index 4d8b1bb9a4..7b93458b52 100644
--- a/packages/backend-core/src/redis/utils.ts
+++ b/packages/backend-core/src/redis/utils.ts
@@ -29,6 +29,7 @@ export enum Databases {
WRITE_THROUGH = "writeThrough",
LOCKS = "locks",
SOCKET_IO = "socket_io",
+ BPM_EVENTS = "bpmEvents",
}
/**
diff --git a/packages/builder/src/components/common/HelpMenu.svelte b/packages/builder/src/components/common/HelpMenu.svelte
index f6e2f42c98..baff9a5a27 100644
--- a/packages/builder/src/components/common/HelpMenu.svelte
+++ b/packages/builder/src/components/common/HelpMenu.svelte
@@ -1,11 +1,11 @@
@@ -67,13 +95,30 @@
options={componentOptions}
on:change={() => (parameters.columns = [])}
/>
+
Export as
+
Export columns
- {
+ const columns = e.detail
+ parameters.columns = columns
+ parameters.customHeaders = columns.reduce((headerMap, column) => {
+ return {
+ [column.name]: column.displayName,
+ ...headerMap,
+ }
+ }, {})
+ }}
/>
@@ -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;
}
diff --git a/packages/builder/src/components/design/settings/controls/ColumnEditor/ColumnEditor.svelte b/packages/builder/src/components/design/settings/controls/ColumnEditor/ColumnEditor.svelte
index 2b9fa573c2..742ab785a1 100644
--- a/packages/builder/src/components/design/settings/controls/ColumnEditor/ColumnEditor.svelte
+++ b/packages/builder/src/components/design/settings/controls/ColumnEditor/ColumnEditor.svelte
@@ -29,6 +29,12 @@
allowLinks: true,
})
+ $: {
+ value = (value || []).filter(
+ column => (schema || {})[column.name || column] !== undefined
+ )
+ }
+
const getText = value => {
if (!value?.length) {
return "All columns"
diff --git a/packages/builder/src/components/design/settings/controls/IconSelect/IconSelect.svelte b/packages/builder/src/components/design/settings/controls/IconSelect/IconSelect.svelte
index 766fb30c49..0c68c3c3e6 100644
--- a/packages/builder/src/components/design/settings/controls/IconSelect/IconSelect.svelte
+++ b/packages/builder/src/components/design/settings/controls/IconSelect/IconSelect.svelte
@@ -116,7 +116,6 @@
$: pagerText = `Page ${currentPage} of ${totalPages}`
-a11y-click-events-have-key-events
{displayValue}
diff --git a/packages/builder/src/helpers/data/utils.js b/packages/builder/src/helpers/data/utils.js
index a29ce8db6d..a592b57a26 100644
--- a/packages/builder/src/helpers/data/utils.js
+++ b/packages/builder/src/helpers/data/utils.js
@@ -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)
})
diff --git a/packages/builder/src/helpers/planTitle.js b/packages/builder/src/helpers/planTitle.js
index 098bfb4529..79f2bc2382 100644
--- a/packages/builder/src/helpers/planTitle.js
+++ b/packages/builder/src/helpers/planTitle.js
@@ -25,3 +25,7 @@ export function getFormattedPlanName(userPlanType) {
}
return `${planName} Plan`
}
+
+export function isPremiumOrAbove(userPlanType) {
+ return ![PlanType.PRO, PlanType.TEAM, PlanType.FREE].includes(userPlanType)
+}
diff --git a/packages/builder/src/helpers/tests/dataUtils.test.js b/packages/builder/src/helpers/tests/dataUtils.test.js
index 8fc2d706d7..bd207ea339 100644
--- a/packages/builder/src/helpers/tests/dataUtils.test.js
+++ b/packages/builder/src/helpers/tests/dataUtils.test.js
@@ -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")
+ })
})
diff --git a/packages/builder/src/pages/builder/app/[application]/_layout.svelte b/packages/builder/src/pages/builder/app/[application]/_layout.svelte
index c7f8c98e73..dd66f5bc34 100644
--- a/packages/builder/src/pages/builder/app/[application]/_layout.svelte
+++ b/packages/builder/src/pages/builder/app/[application]/_layout.svelte
@@ -69,11 +69,12 @@
// brought back to the same screen.
const topItemNavigate = path => () => {
const activeTopNav = $layout.children.find(c => $isActive(c.path))
- if (!activeTopNav) return
- builderStore.setPreviousTopNavPath(
- activeTopNav.path,
- window.location.pathname
- )
+ if (activeTopNav) {
+ builderStore.setPreviousTopNavPath(
+ activeTopNav.path,
+ window.location.pathname
+ )
+ }
$goto($builderStore.previousTopNavPath[path] || path)
}
diff --git a/packages/client/src/utils/buttonActions.js b/packages/client/src/utils/buttonActions.js
index b2068ad152..68478b76ac 100644
--- a/packages/client/src/utils/buttonActions.js
+++ b/packages/client/src/utils/buttonActions.js
@@ -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" }),
diff --git a/packages/frontend-core/src/api/rows.js b/packages/frontend-core/src/api/rows.js
index 79f837e864..0a0d48da43 100644
--- a/packages/frontend-core/src/api/rows.js
+++ b/packages/frontend-core/src/api/rows.js
@@ -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 => {
diff --git a/packages/sdk/package.json b/packages/sdk/package.json
index 0fc14b03c4..ed625e10fa 100644
--- a/packages/sdk/package.json
+++ b/packages/sdk/package.json
@@ -11,9 +11,10 @@
"build:sdk": "yarn run generate && rollup -c"
},
"devDependencies": {
- "@rollup/plugin-commonjs": "^18.0.0",
- "@rollup/plugin-node-resolve": "^11.2.1",
- "rollup": "^2.44.0",
- "rollup-plugin-terser": "^7.0.2"
+ "@rollup/plugin-commonjs": "^25.0.7",
+ "@rollup/plugin-node-resolve": "^15.2.3",
+ "rollup": "^4.9.6",
+ "rollup-plugin-terser": "^7.0.2",
+ "rollup-plugin-polyfill-node": "^0.13.0"
}
}
diff --git a/packages/server/scripts/integrations/postgres/reset.sh b/packages/server/scripts/integrations/postgres/reset.sh
index 32778bd11f..8deb01cdf8 100755
--- a/packages/server/scripts/integrations/postgres/reset.sh
+++ b/packages/server/scripts/integrations/postgres/reset.sh
@@ -1,3 +1,3 @@
#!/bin/bash
-docker-compose down
+docker-compose down -v
docker volume prune -f
diff --git a/packages/server/scripts/test.sh b/packages/server/scripts/test.sh
index 9efef05526..3ecf8bb794 100644
--- a/packages/server/scripts/test.sh
+++ b/packages/server/scripts/test.sh
@@ -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
\ No newline at end of file
diff --git a/packages/server/src/api/controllers/application.ts b/packages/server/src/api/controllers/application.ts
index 33582cf656..0bc93888ae 100644
--- a/packages/server/src/api/controllers/application.ts
+++ b/packages/server/src/api/controllers/application.ts
@@ -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(
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(
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) {
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
+) {
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
+) {
const db = context.getAppDB()
const appId = context.getAppId()
- let application = await db.get(DocumentType.APP_METADATA)
+ let application = await db.get(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) {
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,
+ appId: string
+) {
return context.doInAppContext(appId, async () => {
const db = context.getAppDB()
const application = await db.get(DocumentType.APP_METADATA)
- const newAppPackage = { ...application, ...appPackage }
+ const newAppPackage: App = { ...application, ...appPackage }
if (appPackage._rev !== application._rev) {
newAppPackage._rev = application._rev
}
diff --git a/packages/server/src/api/controllers/automation.ts b/packages/server/src/api/controllers/automation.ts
index 186b68f3b7..b986b5232b 100644
--- a/packages/server/src/api/controllers/automation.ts
+++ b/packages/server/src/api/controllers/automation.ts
@@ -20,6 +20,7 @@ import {
AutomationActionStepId,
AutomationResults,
UserCtx,
+ DeleteAutomationResponse,
} from "@budibase/types"
import { getActionDefinitions as actionDefs } from "../../automations/actions"
import sdk from "../../sdk"
@@ -72,7 +73,9 @@ function cleanAutomationInputs(automation: Automation) {
return automation
}
-export async function create(ctx: UserCtx) {
+export async function create(
+ ctx: UserCtx
+) {
const db = context.getAppDB()
let automation = ctx.request.body
automation.appId = ctx.appId
@@ -207,7 +210,7 @@ export async function find(ctx: UserCtx) {
ctx.body = await db.get(ctx.params.id)
}
-export async function destroy(ctx: UserCtx) {
+export async function destroy(ctx: UserCtx) {
const db = context.getAppDB()
const automationId = ctx.params.id
const oldAutomation = await db.get(automationId)
diff --git a/packages/server/src/api/controllers/datasource.ts b/packages/server/src/api/controllers/datasource.ts
index d70c13f800..0f17c5a2f5 100644
--- a/packages/server/src/api/controllers/datasource.ts
+++ b/packages/server/src/api/controllers/datasource.ts
@@ -15,10 +15,14 @@ import {
FieldType,
RelationshipFieldMetadata,
SourceName,
+ UpdateDatasourceRequest,
UpdateDatasourceResponse,
UserCtx,
VerifyDatasourceRequest,
VerifyDatasourceResponse,
+ Table,
+ RowValue,
+ DynamicVariable,
} from "@budibase/types"
import sdk from "../../sdk"
import { builderSocket } from "../../websockets"
@@ -90,8 +94,10 @@ async function invalidateVariables(
existingDatasource: Datasource,
updatedDatasource: Datasource
) {
- const existingVariables: any = existingDatasource.config?.dynamicVariables
- const updatedVariables: any = updatedDatasource.config?.dynamicVariables
+ const existingVariables: DynamicVariable[] =
+ existingDatasource.config?.dynamicVariables || []
+ const updatedVariables: DynamicVariable[] =
+ updatedDatasource.config?.dynamicVariables || []
const toInvalidate = []
if (!existingVariables) {
@@ -103,9 +109,9 @@ async function invalidateVariables(
toInvalidate.push(...existingVariables)
} else {
// invaldate changed / removed
- existingVariables.forEach((existing: any) => {
+ existingVariables.forEach(existing => {
const unchanged = updatedVariables.find(
- (updated: any) =>
+ updated =>
existing.name === updated.name &&
existing.queryId === updated.queryId &&
existing.value === updated.value
@@ -118,24 +124,32 @@ async function invalidateVariables(
await invalidateDynamicVariables(toInvalidate)
}
-export async function update(ctx: UserCtx) {
+export async function update(
+ ctx: UserCtx
+) {
const db = context.getAppDB()
const datasourceId = ctx.params.datasourceId
const baseDatasource = await sdk.datasources.get(datasourceId)
- const auth = baseDatasource.config?.auth
await invalidateVariables(baseDatasource, ctx.request.body)
const isBudibaseSource =
baseDatasource.type === dbCore.BUDIBASE_DATASOURCE_TYPE
- const dataSourceBody = isBudibaseSource
- ? { name: ctx.request.body?.name }
+ const dataSourceBody: Datasource = isBudibaseSource
+ ? {
+ name: ctx.request.body?.name,
+ type: dbCore.BUDIBASE_DATASOURCE_TYPE,
+ source: SourceName.BUDIBASE,
+ }
: ctx.request.body
let datasource: Datasource = {
...baseDatasource,
...sdk.datasources.mergeConfigs(dataSourceBody, baseDatasource),
}
+
+ // this block is specific to GSheets, if no auth set, set it back
+ const auth = baseDatasource.config?.auth
if (auth && !ctx.request.body.auth) {
// don't strip auth config from DB
datasource.config!.auth = auth
@@ -204,7 +218,7 @@ async function destroyInternalTablesBySourceId(datasourceId: string) {
const db = context.getAppDB()
// Get all internal tables
- const internalTables = await db.allDocs(
+ const internalTables = await db.allDocs(
getTableParams(null, {
include_docs: true,
})
@@ -212,8 +226,8 @@ async function destroyInternalTablesBySourceId(datasourceId: string) {
// Filter by datasource and return the docs.
const datasourceTableDocs = internalTables.rows.reduce(
- (acc: any, table: any) => {
- if (table.doc.sourceId == datasourceId) {
+ (acc: Table[], table) => {
+ if (table.doc?.sourceId == datasourceId) {
acc.push(table.doc)
}
return acc
@@ -254,9 +268,9 @@ export async function destroy(ctx: UserCtx) {
if (datasource.type === dbCore.BUDIBASE_DATASOURCE_TYPE) {
await destroyInternalTablesBySourceId(datasourceId)
} else {
- const queries = await db.allDocs(getQueryParams(datasourceId))
+ const queries = await db.allDocs(getQueryParams(datasourceId))
await db.bulkDocs(
- queries.rows.map((row: any) => ({
+ queries.rows.map(row => ({
_id: row.id,
_rev: row.value.rev,
_deleted: true,
diff --git a/packages/server/src/api/controllers/integration.ts b/packages/server/src/api/controllers/integration.ts
index 9cfde31e4c..57038f8401 100644
--- a/packages/server/src/api/controllers/integration.ts
+++ b/packages/server/src/api/controllers/integration.ts
@@ -1,7 +1,10 @@
import { getDefinition, getDefinitions } from "../../integrations"
import { SourceName, UserCtx } from "@budibase/types"
-const DISABLED_EXTERNAL_INTEGRATIONS = [SourceName.AIRTABLE]
+const DISABLED_EXTERNAL_INTEGRATIONS = [
+ SourceName.AIRTABLE,
+ SourceName.BUDIBASE,
+]
export async function fetch(ctx: UserCtx) {
const definitions = await getDefinitions()
diff --git a/packages/server/src/api/controllers/layout.ts b/packages/server/src/api/controllers/layout.ts
index 69e4ad91ed..c0406f50ac 100644
--- a/packages/server/src/api/controllers/layout.ts
+++ b/packages/server/src/api/controllers/layout.ts
@@ -1,9 +1,17 @@
import { EMPTY_LAYOUT } from "../../constants/layouts"
import { generateLayoutID, getScreenParams } from "../../db/utils"
import { events, context } from "@budibase/backend-core"
-import { BBContext, Layout } from "@budibase/types"
+import {
+ BBContext,
+ Layout,
+ SaveLayoutRequest,
+ SaveLayoutResponse,
+ UserCtx,
+} from "@budibase/types"
-export async function save(ctx: BBContext) {
+export async function save(
+ ctx: UserCtx
+) {
const db = context.getAppDB()
let layout = ctx.request.body
diff --git a/packages/server/src/api/controllers/query/index.ts b/packages/server/src/api/controllers/query/index.ts
index 768c921150..973718ba48 100644
--- a/packages/server/src/api/controllers/query/index.ts
+++ b/packages/server/src/api/controllers/query/index.ts
@@ -73,7 +73,7 @@ const _import = async (ctx: UserCtx) => {
}
export { _import as import }
-export async function save(ctx: UserCtx) {
+export async function save(ctx: UserCtx) {
const db = context.getAppDB()
const query: Query = ctx.request.body
diff --git a/packages/server/src/api/controllers/row/ExternalRequest.ts b/packages/server/src/api/controllers/row/ExternalRequest.ts
index b7dc02c0db..685af4e98e 100644
--- a/packages/server/src/api/controllers/row/ExternalRequest.ts
+++ b/packages/server/src/api/controllers/row/ExternalRequest.ts
@@ -7,6 +7,7 @@ import {
FilterType,
IncludeRelationship,
ManyToManyRelationshipFieldMetadata,
+ ManyToOneRelationshipFieldMetadata,
OneToManyRelationshipFieldMetadata,
Operation,
PaginationJson,
@@ -18,6 +19,7 @@ import {
SortJson,
SortType,
Table,
+ isManyToOne,
} from "@budibase/types"
import {
breakExternalTableId,
@@ -32,7 +34,9 @@ import { processObjectSync } from "@budibase/string-templates"
import { cloneDeep } from "lodash/fp"
import { processDates, processFormulas } from "../../../utilities/rowProcessor"
import { db as dbCore } from "@budibase/backend-core"
+import AliasTables from "./alias"
import sdk from "../../../sdk"
+import env from "../../../environment"
export interface ManyRelationship {
tableId?: string
@@ -101,6 +105,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
@@ -178,13 +215,13 @@ function generateIdForRow(
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,
}
}
@@ -304,6 +341,18 @@ export class ExternalRequest {
}
}
+ async getRow(table: Table, rowId: string): Promise {
+ 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: [] }
@@ -571,7 +620,9 @@ export class ExternalRequest {
* 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
@@ -589,14 +640,26 @@ export class ExternalRequest {
) {
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
}
@@ -609,9 +672,12 @@ export class ExternalRequest {
},
})
// 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
}
@@ -697,24 +763,43 @@ export class ExternalRequest {
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 = 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)
+ })
+ )
+ }
+ }
+
/**
* This function is a bit crazy, but the exact purpose of it is to protect against the scenario in which
* you have column overlap in relationships, e.g. we join a few different tables and they all have the
@@ -804,7 +889,7 @@ export class ExternalRequest {
}
let json = {
endpoint: {
- datasourceId,
+ datasourceId: datasourceId!,
entityId: tableName,
operation,
},
@@ -826,17 +911,30 @@ export class ExternalRequest {
},
}
- // 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
)
}
- const output = this.outputProcessing(response, table, relationships)
+ const output = this.outputProcessing(responseRows, table, relationships)
// if reading it'll just be an array of rows, return whole thing
if (operation === Operation.READ) {
return (
diff --git a/packages/server/src/api/controllers/row/alias.ts b/packages/server/src/api/controllers/row/alias.ts
new file mode 100644
index 0000000000..9658a0d638
--- /dev/null
+++ b/packages/server/src/api/controllers/row/alias.ts
@@ -0,0 +1,166 @@
+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
+ tableAliases: Record
+ 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(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 = {}
+ for (let tableName of tableNames) {
+ if (tableName) {
+ map[tableName] = this.getAlias(tableName)
+ }
+ }
+ return map
+ }
+
+ async queryWithAliasing(json: QueryJson): 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 = {}
+ 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 = {}
+ 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
+ }
+ }
+}
diff --git a/packages/server/src/api/controllers/row/index.ts b/packages/server/src/api/controllers/row/index.ts
index 1ad8a2a695..ec56919d12 100644
--- a/packages/server/src/api/controllers/row/index.ts
+++ b/packages/server/src/api/controllers/row/index.ts
@@ -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)
diff --git a/packages/server/src/api/controllers/row/internal.ts b/packages/server/src/api/controllers/row/internal.ts
index 3ee08fff2e..cc903bd74a 100644
--- a/packages/server/src/api/controllers/row/internal.ts
+++ b/packages/server/src/api/controllers/row/internal.ts
@@ -189,11 +189,12 @@ export async function fetchEnrichedRow(ctx: UserCtx) {
const tableId = utils.getTableId(ctx)
const rowId = ctx.params.rowId as string
// need table to work out where links go in row, as well as the link docs
- const [table, row, links] = await Promise.all([
+ const [table, links] = await Promise.all([
sdk.tables.getTable(tableId),
- utils.findRow(ctx, tableId, rowId),
linkRows.getLinkDocuments({ tableId, rowId, fieldName }),
])
+ let row = await utils.findRow(ctx, tableId, rowId)
+ row = await outputProcessing(table, row)
const linkVals = links as LinkDocumentValue[]
// look up the actual rows based on the ids
diff --git a/packages/server/src/api/controllers/screen.ts b/packages/server/src/api/controllers/screen.ts
index 446fe2e5fa..ee8e0ff892 100644
--- a/packages/server/src/api/controllers/screen.ts
+++ b/packages/server/src/api/controllers/screen.ts
@@ -7,7 +7,13 @@ import {
roles,
} from "@budibase/backend-core"
import { updateAppPackage } from "./application"
-import { Plugin, ScreenProps, BBContext, Screen } from "@budibase/types"
+import {
+ Plugin,
+ ScreenProps,
+ BBContext,
+ Screen,
+ UserCtx,
+} from "@budibase/types"
import { builderSocket } from "../../websockets"
export async function fetch(ctx: BBContext) {
@@ -31,7 +37,7 @@ export async function fetch(ctx: BBContext) {
)
}
-export async function save(ctx: BBContext) {
+export async function save(ctx: UserCtx) {
const db = context.getAppDB()
let screen = ctx.request.body
diff --git a/packages/server/src/api/controllers/static/index.ts b/packages/server/src/api/controllers/static/index.ts
index 5a3803e6d5..c718d5f704 100644
--- a/packages/server/src/api/controllers/static/index.ts
+++ b/packages/server/src/api/controllers/static/index.ts
@@ -170,6 +170,7 @@ export const serveApp = async function (ctx: Ctx) {
if (!env.isJest()) {
const plugins = objectStore.enrichPluginURLs(appInfo.usedPlugins)
const { head, html, css } = AppComponent.render({
+ title: branding?.platformTitle || `${appInfo.name}`,
metaImage:
branding?.metaImageUrl ||
"https://res.cloudinary.com/daog6scxm/image/upload/v1698759482/meta-images/plain-branded-meta-image-coral_ocxmgu.png",
diff --git a/packages/server/src/api/controllers/view/exporters.ts b/packages/server/src/api/controllers/view/exporters.ts
index d6caff6035..3b5f951dca 100644
--- a/packages/server/src/api/controllers/view/exporters.ts
+++ b/packages/server/src/api/controllers/view/exporters.ts
@@ -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
}
diff --git a/packages/server/src/api/routes/application.ts b/packages/server/src/api/routes/application.ts
index babcb1b44b..7e01a3c2ef 100644
--- a/packages/server/src/api/routes/application.ts
+++ b/packages/server/src/api/routes/application.ts
@@ -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()
diff --git a/packages/server/src/api/routes/tests/application.spec.ts b/packages/server/src/api/routes/tests/application.spec.ts
index fa5cb0a983..dbe4eb51ae 100644
--- a/packages/server/src/api/routes/tests/application.spec.ts
+++ b/packages/server/src/api/routes/tests/application.spec.ts
@@ -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()
diff --git a/packages/server/src/api/routes/tests/automation.spec.ts b/packages/server/src/api/routes/tests/automation.spec.ts
index 178189555d..ee8fc7d544 100644
--- a/packages/server/src/api/routes/tests/automation.spec.ts
+++ b/packages/server/src/api/routes/tests/automation.spec.ts
@@ -394,7 +394,7 @@ describe("/automations", () => {
it("deletes a automation by its ID", async () => {
const automation = await config.createAutomation()
const res = await request
- .delete(`/api/automations/${automation.id}/${automation.rev}`)
+ .delete(`/api/automations/${automation._id}/${automation._rev}`)
.set(config.defaultHeaders())
.expect("Content-Type", /json/)
.expect(200)
@@ -408,7 +408,7 @@ describe("/automations", () => {
await checkBuilderEndpoint({
config,
method: "DELETE",
- url: `/api/automations/${automation.id}/${automation._rev}`,
+ url: `/api/automations/${automation._id}/${automation._rev}`,
})
})
})
diff --git a/packages/server/src/api/routes/tests/backup.spec.ts b/packages/server/src/api/routes/tests/backup.spec.ts
index acfac783db..becbeb5480 100644
--- a/packages/server/src/api/routes/tests/backup.spec.ts
+++ b/packages/server/src/api/routes/tests/backup.spec.ts
@@ -44,7 +44,7 @@ describe("/backups", () => {
expect(headers["content-disposition"]).toEqual(
`attachment; filename="${
- config.getApp()!.name
+ config.getApp().name
}-export-${mocks.date.MOCK_DATE.getTime()}.tar.gz"`
)
})
diff --git a/packages/server/src/api/routes/tests/datasource.spec.ts b/packages/server/src/api/routes/tests/datasource.spec.ts
index 41229b0a2a..032da71b80 100644
--- a/packages/server/src/api/routes/tests/datasource.spec.ts
+++ b/packages/server/src/api/routes/tests/datasource.spec.ts
@@ -86,7 +86,7 @@ describe("/datasources", () => {
})
// check variables in cache
let contents = await checkCacheForDynamicVariable(
- query._id,
+ query._id!,
"variable3"
)
expect(contents.rows.length).toEqual(1)
@@ -102,7 +102,7 @@ describe("/datasources", () => {
expect(res.body.errors).toBeUndefined()
// check variables no longer in cache
- contents = await checkCacheForDynamicVariable(query._id, "variable3")
+ contents = await checkCacheForDynamicVariable(query._id!, "variable3")
expect(contents).toBe(null)
})
})
diff --git a/packages/server/src/api/routes/tests/queries/query.seq.spec.ts b/packages/server/src/api/routes/tests/queries/query.seq.spec.ts
index 52d35fa782..2bbc8366ea 100644
--- a/packages/server/src/api/routes/tests/queries/query.seq.spec.ts
+++ b/packages/server/src/api/routes/tests/queries/query.seq.spec.ts
@@ -467,7 +467,10 @@ describe("/queries", () => {
queryString: "test={{ variable3 }}",
})
// check its in cache
- const contents = await checkCacheForDynamicVariable(base._id, "variable3")
+ const contents = await checkCacheForDynamicVariable(
+ base._id!,
+ "variable3"
+ )
expect(contents.rows.length).toEqual(1)
const responseBody = await preview(datasource, {
path: "www.failonce.com",
@@ -490,7 +493,7 @@ describe("/queries", () => {
queryString: "test={{ variable3 }}",
})
// check its in cache
- let contents = await checkCacheForDynamicVariable(base._id, "variable3")
+ let contents = await checkCacheForDynamicVariable(base._id!, "variable3")
expect(contents.rows.length).toEqual(1)
// delete the query
@@ -500,7 +503,7 @@ describe("/queries", () => {
.expect(200)
// check variables no longer in cache
- contents = await checkCacheForDynamicVariable(base._id, "variable3")
+ contents = await checkCacheForDynamicVariable(base._id!, "variable3")
expect(contents).toBe(null)
})
})
diff --git a/packages/server/src/api/routes/tests/row.spec.ts b/packages/server/src/api/routes/tests/row.spec.ts
index 239da36351..726e493b2d 100644
--- a/packages/server/src/api/routes/tests/row.spec.ts
+++ b/packages/server/src/api/routes/tests/row.spec.ts
@@ -110,7 +110,7 @@ describe.each([
config.api.row.get(tbl_Id, id, { expectStatus: status })
const getRowUsage = async () => {
- const { total } = await config.doInContext(null, () =>
+ const { total } = await config.doInContext(undefined, () =>
quotas.getCurrentUsageValues(QuotaUsageType.STATIC, StaticQuotaName.ROWS)
)
return total
diff --git a/packages/server/src/api/routes/tests/user.spec.ts b/packages/server/src/api/routes/tests/user.spec.ts
index e6349099d7..076ee064dc 100644
--- a/packages/server/src/api/routes/tests/user.spec.ts
+++ b/packages/server/src/api/routes/tests/user.spec.ts
@@ -27,15 +27,17 @@ describe("/users", () => {
describe("fetch", () => {
it("returns a list of users from an instance db", async () => {
- await config.createUser({ id: "uuidx" })
- await config.createUser({ id: "uuidy" })
+ const id1 = `us_${utils.newid()}`
+ const id2 = `us_${utils.newid()}`
+ await config.createUser({ _id: id1 })
+ await config.createUser({ _id: id2 })
const res = await config.api.user.fetch()
expect(res.length).toBe(3)
const ids = res.map(u => u._id)
- expect(ids).toContain(`ro_ta_users_us_uuidx`)
- expect(ids).toContain(`ro_ta_users_us_uuidy`)
+ expect(ids).toContain(`ro_ta_users_${id1}`)
+ expect(ids).toContain(`ro_ta_users_${id2}`)
})
it("should apply authorization to endpoint", async () => {
@@ -54,7 +56,7 @@ describe("/users", () => {
describe("update", () => {
it("should be able to update the user", async () => {
const user: UserMetadata = await config.createUser({
- id: `us_update${utils.newid()}`,
+ _id: `us_update${utils.newid()}`,
})
user.roleId = roles.BUILTIN_ROLE_IDS.BASIC
delete user._rev
diff --git a/packages/server/src/api/routes/tests/utilities/TestFunctions.ts b/packages/server/src/api/routes/tests/utilities/TestFunctions.ts
index 53e90396aa..8a843551ac 100644
--- a/packages/server/src/api/routes/tests/utilities/TestFunctions.ts
+++ b/packages/server/src/api/routes/tests/utilities/TestFunctions.ts
@@ -4,6 +4,7 @@ import { AppStatus } from "../../../../db/utils"
import { roles, tenancy, context, db } from "@budibase/backend-core"
import env from "../../../../environment"
import Nano from "@budibase/nano"
+import TestConfiguration from "src/tests/utilities/TestConfiguration"
class Request {
appId: any
@@ -52,10 +53,10 @@ export const clearAllApps = async (
})
}
-export const clearAllAutomations = async (config: any) => {
+export const clearAllAutomations = async (config: TestConfiguration) => {
const automations = await config.getAllAutomations()
for (let auto of automations) {
- await context.doInAppContext(config.appId, async () => {
+ await context.doInAppContext(config.getAppId(), async () => {
await config.deleteAutomation(auto)
})
}
@@ -101,7 +102,12 @@ export const checkBuilderEndpoint = async ({
method,
url,
body,
-}: any) => {
+}: {
+ config: TestConfiguration
+ method: string
+ url: string
+ body?: any
+}) => {
const headers = await config.login({
userId: "us_fail",
builder: false,
diff --git a/packages/server/src/api/routes/tests/webhook.spec.ts b/packages/server/src/api/routes/tests/webhook.spec.ts
index 38f84852b4..48a6da38bf 100644
--- a/packages/server/src/api/routes/tests/webhook.spec.ts
+++ b/packages/server/src/api/routes/tests/webhook.spec.ts
@@ -36,7 +36,7 @@ describe("/webhooks", () => {
const automation = await config.createAutomation()
const res = await request
.put(`/api/webhooks`)
- .send(basicWebhook(automation._id))
+ .send(basicWebhook(automation._id!))
.set(config.defaultHeaders())
.expect("Content-Type", /json/)
.expect(200)
@@ -145,7 +145,7 @@ describe("/webhooks", () => {
let automation = collectAutomation()
let newAutomation = await config.createAutomation(automation)
let syncWebhook = await config.createWebhook(
- basicWebhook(newAutomation._id)
+ basicWebhook(newAutomation._id!)
)
// replicate changes before checking webhook
diff --git a/packages/server/src/app.ts b/packages/server/src/app.ts
index 4e84422dec..aa96a30b00 100644
--- a/packages/server/src/app.ts
+++ b/packages/server/src/app.ts
@@ -29,6 +29,6 @@ start().catch(err => {
throw err
})
-export function getServer() {
+export function getServer(): Server {
return server
}
diff --git a/packages/server/src/constants/layouts.ts b/packages/server/src/constants/layouts.ts
index 835a5d2e15..f4eb337c2d 100644
--- a/packages/server/src/constants/layouts.ts
+++ b/packages/server/src/constants/layouts.ts
@@ -1,9 +1,11 @@
+import { Layout } from "@budibase/types"
+
export const BASE_LAYOUT_PROP_IDS = {
PRIVATE: "layout_private_master",
PUBLIC: "layout_public_master",
}
-export const EMPTY_LAYOUT = {
+export const EMPTY_LAYOUT: Layout = {
componentLibraries: ["@budibase/standard-components"],
title: "{{ name }}",
favicon: "./_shared/favicon.png",
diff --git a/packages/server/src/constants/screens.ts b/packages/server/src/constants/screens.ts
index 6c88b0f957..1107289ea0 100644
--- a/packages/server/src/constants/screens.ts
+++ b/packages/server/src/constants/screens.ts
@@ -1,5 +1,6 @@
import { roles } from "@budibase/backend-core"
import { BASE_LAYOUT_PROP_IDS } from "./layouts"
+import { Screen } from "@budibase/types"
export function createHomeScreen(
config: {
@@ -9,10 +10,8 @@ export function createHomeScreen(
roleId: roles.BUILTIN_ROLE_IDS.BASIC,
route: "/",
}
-) {
+): Screen {
return {
- description: "",
- url: "",
layoutId: BASE_LAYOUT_PROP_IDS.PRIVATE,
props: {
_id: "d834fea2-1b3e-4320-ab34-f9009f5ecc59",
diff --git a/packages/server/src/db/defaultData/datasource_bb_default.ts b/packages/server/src/db/defaultData/datasource_bb_default.ts
index ac540cd8fc..03aed3c118 100644
--- a/packages/server/src/db/defaultData/datasource_bb_default.ts
+++ b/packages/server/src/db/defaultData/datasource_bb_default.ts
@@ -1,8 +1,8 @@
import {
DEFAULT_BB_DATASOURCE_ID,
- DEFAULT_INVENTORY_TABLE_ID,
DEFAULT_EMPLOYEE_TABLE_ID,
DEFAULT_EXPENSES_TABLE_ID,
+ DEFAULT_INVENTORY_TABLE_ID,
DEFAULT_JOBS_TABLE_ID,
} from "../../constants"
import { importToRows } from "../../api/controllers/table/utils"
@@ -15,19 +15,21 @@ import { expensesImport } from "./expensesImport"
import { db as dbCore } from "@budibase/backend-core"
import {
AutoFieldSubType,
+ Datasource,
FieldType,
RelationshipType,
Row,
+ SourceName,
Table,
TableSchema,
TableSourceType,
} from "@budibase/types"
-const defaultDatasource = {
+const defaultDatasource: Datasource = {
_id: DEFAULT_BB_DATASOURCE_ID,
type: dbCore.BUDIBASE_DATASOURCE_TYPE,
name: "Sample Data",
- source: "BUDIBASE",
+ source: SourceName.BUDIBASE,
config: {},
}
diff --git a/packages/server/src/db/utils.ts b/packages/server/src/db/utils.ts
index 35d9b69e96..983cbf423c 100644
--- a/packages/server/src/db/utils.ts
+++ b/packages/server/src/db/utils.ts
@@ -1,13 +1,15 @@
import newid from "./newid"
import { db as dbCore } from "@budibase/backend-core"
import {
- FieldType,
+ DatabaseQueryOpts,
+ Datasource,
DocumentType,
FieldSchema,
- RelationshipFieldMetadata,
- VirtualDocumentType,
+ FieldType,
INTERNAL_TABLE_SOURCE_ID,
- DatabaseQueryOpts,
+ RelationshipFieldMetadata,
+ SourceName,
+ VirtualDocumentType,
} from "@budibase/types"
export { DocumentType, VirtualDocumentType } from "@budibase/types"
@@ -20,11 +22,11 @@ export const enum AppStatus {
DEPLOYED = "published",
}
-export const BudibaseInternalDB = {
+export const BudibaseInternalDB: Datasource = {
_id: INTERNAL_TABLE_SOURCE_ID,
type: dbCore.BUDIBASE_DATASOURCE_TYPE,
name: "Budibase DB",
- source: "BUDIBASE",
+ source: SourceName.BUDIBASE,
config: {},
}
diff --git a/packages/server/src/environment.ts b/packages/server/src/environment.ts
index d0b7e91401..a7c6df29ea 100644
--- a/packages/server/src/environment.ts
+++ b/packages/server/src/environment.ts
@@ -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,
diff --git a/packages/server/src/integrations/base/query.ts b/packages/server/src/integrations/base/query.ts
index 4f31e37744..b906ecbb1b 100644
--- a/packages/server/src/integrations/base/query.ts
+++ b/packages/server/src/integrations/base/query.ts
@@ -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
-) {
+): DatasourcePlusQueryResponse {
datasource = await sdk.datasources.enrich(datasource)
const Integration = await getIntegration(datasource.source)
// query is the opinionated function
diff --git a/packages/server/src/integrations/base/sql.ts b/packages/server/src/integrations/base/sql.ts
index 29c8416b34..6605052598 100644
--- a/packages/server/src/integrations/base/sql.ts
+++ b/packages/server/src/integrations/base/sql.ts
@@ -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"
@@ -127,10 +126,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; 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
@@ -139,10 +143,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)
}
}
}
@@ -314,7 +319,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) {
@@ -330,16 +335,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
+ ): Knex.QueryBuilder {
if (!relationships) {
return query
}
- const tableSets: Record = {}
+ const tableSets: Record = {}
// aggregate into table sets (all the same to tables)
for (let relationship of relationships) {
const keyObj: { toTable: string; throughTable: string | undefined } = {
@@ -358,10 +375,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 () {
@@ -369,7 +393,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 {
@@ -381,9 +405,9 @@ class InternalBuilder {
const from = relationship.from
// @ts-ignore
this.orOn(
- `${fromTable}.${fromPrimary}`,
+ `${fromAlias}.${fromPrimary}`,
"=",
- `${throughTable}.${from}`
+ `${throughAlias}.${from}`
)
}
})
@@ -392,7 +416,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}`)
}
})
}
@@ -400,12 +424,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)) {
@@ -422,12 +459,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
}
@@ -435,8 +469,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) {
@@ -462,21 +498,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)
@@ -486,19 +521,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)
@@ -507,13 +545,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()
@@ -537,10 +574,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 {
const sqlClient = this.getSqlClient()
const client = knex({ client: sqlClient })
- let query
+ let query: Knex.QueryBuilder
const builder = new InternalBuilder(sqlClient)
switch (this._operation(json)) {
case Operation.CREATE:
@@ -565,8 +602,6 @@ class SqlQueryBuilder extends SqlTableQueryBuilder {
default:
throw `Operation type is not supported by SQL query builder`
}
-
- // @ts-ignore
return query.toSQL().toNative()
}
@@ -648,6 +683,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
diff --git a/packages/server/src/integrations/base/sqlTable.ts b/packages/server/src/integrations/base/sqlTable.ts
index f553690b23..f3560791e6 100644
--- a/packages/server/src/integrations/base/sqlTable.ts
+++ b/packages/server/src/integrations/base/sqlTable.ts
@@ -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,
diff --git a/packages/server/src/integrations/googlesheets.ts b/packages/server/src/integrations/googlesheets.ts
index 58c867ea0b..32398bde41 100644
--- a/packages/server/src/integrations/googlesheets.ts
+++ b/packages/server/src/integrations/googlesheets.ts
@@ -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): 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
diff --git a/packages/server/src/integrations/index.ts b/packages/server/src/integrations/index.ts
index 49761bac85..ee2bb23f23 100644
--- a/packages/server/src/integrations/index.ts
+++ b/packages/server/src/integrations/index.ts
@@ -37,6 +37,7 @@ const DEFINITIONS: Record = {
[SourceName.REDIS]: redis.schema,
[SourceName.SNOWFLAKE]: snowflake.schema,
[SourceName.ORACLE]: undefined,
+ [SourceName.BUDIBASE]: undefined,
}
const INTEGRATIONS: Record = {
@@ -56,6 +57,7 @@ const INTEGRATIONS: Record = {
[SourceName.REDIS]: redis.integration,
[SourceName.SNOWFLAKE]: snowflake.integration,
[SourceName.ORACLE]: undefined,
+ [SourceName.BUDIBASE]: undefined,
}
// optionally add oracle integration if the oracle binary can be installed
diff --git a/packages/server/src/integrations/microsoftSqlServer.ts b/packages/server/src/integrations/microsoftSqlServer.ts
index d0a06d4476..f87e248ac0 100644
--- a/packages/server/src/integrations/microsoftSqlServer.ts
+++ b/packages/server/src/integrations/microsoftSqlServer.ts
@@ -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): DatasourcePlusQueryResponse {
const schema = this.config.schema
await this.connect()
if (schema && schema !== DEFAULT_SCHEMA && json?.endpoint) {
diff --git a/packages/server/src/integrations/mysql.ts b/packages/server/src/integrations/mysql.ts
index 8ec73307f4..f629381807 100644
--- a/packages/server/src/integrations/mysql.ts
+++ b/packages/server/src/integrations/mysql.ts
@@ -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): DatasourcePlusQueryResponse {
await this.connect()
try {
const queryFn = (query: any) =>
diff --git a/packages/server/src/integrations/oracle.ts b/packages/server/src/integrations/oracle.ts
index cf76622581..08f3058d63 100644
--- a/packages/server/src/integrations/oracle.ts
+++ b/packages/server/src/integrations/oracle.ts
@@ -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(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): 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 }]
}
diff --git a/packages/server/src/integrations/postgres.ts b/packages/server/src/integrations/postgres.ts
index 9949dee6bb..635d834761 100644
--- a/packages/server/src/integrations/postgres.ts
+++ b/packages/server/src/integrations/postgres.ts
@@ -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): 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) {
diff --git a/packages/server/src/integrations/tests/sql.spec.ts b/packages/server/src/integrations/tests/sql.spec.ts
index 5cc4849d03..f4eaf2859c 100644
--- a/packages/server/src/integrations/tests/sql.spec.ts
+++ b/packages/server/src/integrations/tests/sql.spec.ts
@@ -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"`,
+ },
+ ])
+ })
})
diff --git a/packages/server/src/integrations/tests/sqlAlias.spec.ts b/packages/server/src/integrations/tests/sqlAlias.spec.ts
new file mode 100644
index 0000000000..9b3f6a1b38
--- /dev/null
+++ b/packages/server/src/integrations/tests/sqlAlias.spec.ts
@@ -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`")
+ })
+ })
+})
diff --git a/packages/server/src/integrations/tests/sqlQueryJson/basicFetchWithRelationships.json b/packages/server/src/integrations/tests/sqlQueryJson/basicFetchWithRelationships.json
new file mode 100644
index 0000000000..3445f5fe67
--- /dev/null
+++ b/packages/server/src/integrations/tests/sqlQueryJson/basicFetchWithRelationships.json
@@ -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"
+ }
+}
\ No newline at end of file
diff --git a/packages/server/src/integrations/tests/sqlQueryJson/createWithRelationships.json b/packages/server/src/integrations/tests/sqlQueryJson/createWithRelationships.json
new file mode 100644
index 0000000000..20331b949a
--- /dev/null
+++ b/packages/server/src/integrations/tests/sqlQueryJson/createWithRelationships.json
@@ -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"
+ }
+}
\ No newline at end of file
diff --git a/packages/server/src/integrations/tests/sqlQueryJson/deleteSimple.json b/packages/server/src/integrations/tests/sqlQueryJson/deleteSimple.json
new file mode 100644
index 0000000000..2266b8c8be
--- /dev/null
+++ b/packages/server/src/integrations/tests/sqlQueryJson/deleteSimple.json
@@ -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"
+ }
+}
\ No newline at end of file
diff --git a/packages/server/src/integrations/tests/sqlQueryJson/enrichRelationship.json b/packages/server/src/integrations/tests/sqlQueryJson/enrichRelationship.json
new file mode 100644
index 0000000000..ee658aed18
--- /dev/null
+++ b/packages/server/src/integrations/tests/sqlQueryJson/enrichRelationship.json
@@ -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"
+ }
+}
\ No newline at end of file
diff --git a/packages/server/src/integrations/tests/sqlQueryJson/fetchManyToMany.json b/packages/server/src/integrations/tests/sqlQueryJson/fetchManyToMany.json
new file mode 100644
index 0000000000..682ebaab2d
--- /dev/null
+++ b/packages/server/src/integrations/tests/sqlQueryJson/fetchManyToMany.json
@@ -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"
+ }
+}
\ No newline at end of file
diff --git a/packages/server/src/integrations/tests/sqlQueryJson/filterByRelationship.json b/packages/server/src/integrations/tests/sqlQueryJson/filterByRelationship.json
new file mode 100644
index 0000000000..eb1025f382
--- /dev/null
+++ b/packages/server/src/integrations/tests/sqlQueryJson/filterByRelationship.json
@@ -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"
+ }
+ }
+}
\ No newline at end of file
diff --git a/packages/server/src/integrations/tests/sqlQueryJson/manyRelationshipFilters.json b/packages/server/src/integrations/tests/sqlQueryJson/manyRelationshipFilters.json
new file mode 100644
index 0000000000..afa0889450
--- /dev/null
+++ b/packages/server/src/integrations/tests/sqlQueryJson/manyRelationshipFilters.json
@@ -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"
+ }
+}
\ No newline at end of file
diff --git a/packages/server/src/integrations/tests/sqlQueryJson/updateRelationship.json b/packages/server/src/integrations/tests/sqlQueryJson/updateRelationship.json
new file mode 100644
index 0000000000..01e795bd6c
--- /dev/null
+++ b/packages/server/src/integrations/tests/sqlQueryJson/updateRelationship.json
@@ -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"
+ }
+}
\ No newline at end of file
diff --git a/packages/server/src/integrations/tests/sqlQueryJson/updateSimple.json b/packages/server/src/integrations/tests/sqlQueryJson/updateSimple.json
new file mode 100644
index 0000000000..01e795bd6c
--- /dev/null
+++ b/packages/server/src/integrations/tests/sqlQueryJson/updateSimple.json
@@ -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"
+ }
+}
\ No newline at end of file
diff --git a/packages/server/src/migrations/functions/usageQuotas/tests/syncApps.spec.ts b/packages/server/src/migrations/functions/usageQuotas/tests/syncApps.spec.ts
index d0d50395b2..1d4d4d0f71 100644
--- a/packages/server/src/migrations/functions/usageQuotas/tests/syncApps.spec.ts
+++ b/packages/server/src/migrations/functions/usageQuotas/tests/syncApps.spec.ts
@@ -13,7 +13,7 @@ describe("syncApps", () => {
afterAll(config.end)
it("runs successfully", async () => {
- return config.doInContext(null, async () => {
+ return config.doInContext(undefined, async () => {
// create the usage quota doc and mock usages
await quotas.getQuotaUsage()
await quotas.setUsage(3, StaticQuotaName.APPS, QuotaUsageType.STATIC)
diff --git a/packages/server/src/migrations/functions/usageQuotas/tests/syncCreators.spec.ts b/packages/server/src/migrations/functions/usageQuotas/tests/syncCreators.spec.ts
index 75fa9f217e..93b7d4949b 100644
--- a/packages/server/src/migrations/functions/usageQuotas/tests/syncCreators.spec.ts
+++ b/packages/server/src/migrations/functions/usageQuotas/tests/syncCreators.spec.ts
@@ -12,8 +12,8 @@ describe("syncCreators", () => {
afterAll(config.end)
it("syncs creators", async () => {
- return config.doInContext(null, async () => {
- await config.createUser({ admin: true })
+ return config.doInContext(undefined, async () => {
+ await config.createUser({ admin: { global: true } })
await syncCreators.run()
diff --git a/packages/server/src/migrations/functions/usageQuotas/tests/syncRows.spec.ts b/packages/server/src/migrations/functions/usageQuotas/tests/syncRows.spec.ts
index e644d605b6..730278683c 100644
--- a/packages/server/src/migrations/functions/usageQuotas/tests/syncRows.spec.ts
+++ b/packages/server/src/migrations/functions/usageQuotas/tests/syncRows.spec.ts
@@ -14,7 +14,7 @@ describe("syncRows", () => {
afterAll(config.end)
it("runs successfully", async () => {
- return config.doInContext(null, async () => {
+ return config.doInContext(undefined, async () => {
// create the usage quota doc and mock usages
await quotas.getQuotaUsage()
await quotas.setUsage(300, StaticQuotaName.ROWS, QuotaUsageType.STATIC)
diff --git a/packages/server/src/migrations/functions/usageQuotas/tests/syncUsers.spec.ts b/packages/server/src/migrations/functions/usageQuotas/tests/syncUsers.spec.ts
index f7500c8b4d..2731cc041d 100644
--- a/packages/server/src/migrations/functions/usageQuotas/tests/syncUsers.spec.ts
+++ b/packages/server/src/migrations/functions/usageQuotas/tests/syncUsers.spec.ts
@@ -12,7 +12,7 @@ describe("syncUsers", () => {
afterAll(config.end)
it("syncs users", async () => {
- return config.doInContext(null, async () => {
+ return config.doInContext(undefined, async () => {
await config.createUser()
await syncUsers.run()
diff --git a/packages/server/src/migrations/tests/index.spec.ts b/packages/server/src/migrations/tests/index.spec.ts
index c01040593a..236776cd3f 100644
--- a/packages/server/src/migrations/tests/index.spec.ts
+++ b/packages/server/src/migrations/tests/index.spec.ts
@@ -40,7 +40,7 @@ describe("migrations", () => {
describe("backfill", () => {
it("runs app db migration", async () => {
- await config.doInContext(null, async () => {
+ await config.doInContext(undefined, async () => {
await clearMigrations()
await config.createAutomation()
await config.createAutomation(structures.newAutomation())
@@ -93,18 +93,18 @@ describe("migrations", () => {
})
it("runs global db migration", async () => {
- await config.doInContext(null, async () => {
+ await config.doInContext(undefined, async () => {
await clearMigrations()
- const appId = config.prodAppId
+ const appId = config.getProdAppId()
const roles = { [appId]: "role_12345" }
await config.createUser({
- builder: false,
- admin: true,
+ builder: { global: false },
+ admin: { global: true },
roles,
}) // admin only
await config.createUser({
- builder: false,
- admin: false,
+ builder: { global: false },
+ admin: { global: false },
roles,
}) // non admin non builder
await config.createTable()
diff --git a/packages/server/src/sdk/app/applications/import.ts b/packages/server/src/sdk/app/applications/import.ts
index c3415bdb36..f712548fcb 100644
--- a/packages/server/src/sdk/app/applications/import.ts
+++ b/packages/server/src/sdk/app/applications/import.ts
@@ -85,7 +85,9 @@ async function getImportableDocuments(db: Database) {
const docPromises = []
for (let docType of DocumentTypesToImport) {
docPromises.push(
- db.allDocs(dbCore.getDocParams(docType, null, { include_docs: true }))
+ db.allDocs(
+ dbCore.getDocParams(docType, null, { include_docs: true })
+ )
)
}
// map the responses to the document itself
diff --git a/packages/server/src/sdk/app/applications/tests/sync.spec.ts b/packages/server/src/sdk/app/applications/tests/sync.spec.ts
index 1d28ed977c..a53bdb0bd7 100644
--- a/packages/server/src/sdk/app/applications/tests/sync.spec.ts
+++ b/packages/server/src/sdk/app/applications/tests/sync.spec.ts
@@ -43,8 +43,8 @@ async function createUser(email: string, roles: UserRoles, builder?: boolean) {
const user = await config.createUser({
email,
roles,
- builder: builder || false,
- admin: false,
+ builder: { global: builder || false },
+ admin: { global: false },
})
await context.doInContext(config.appId!, async () => {
await events.user.created(user)
@@ -55,10 +55,10 @@ async function createUser(email: string, roles: UserRoles, builder?: boolean) {
async function removeUserRole(user: User) {
const final = await config.globalUser({
...user,
- id: user._id,
+ _id: user._id,
roles: {},
- builder: false,
- admin: false,
+ builder: { global: false },
+ admin: { global: false },
})
await context.doInContext(config.appId!, async () => {
await events.user.updated(final)
@@ -69,8 +69,8 @@ async function createGroupAndUser(email: string) {
groupUser = await config.createUser({
email,
roles: {},
- builder: false,
- admin: false,
+ builder: { global: false },
+ admin: { global: false },
})
group = await config.createGroup()
await config.addUserToGroup(group._id!, groupUser._id!)
diff --git a/packages/server/src/sdk/app/datasources/datasources.ts b/packages/server/src/sdk/app/datasources/datasources.ts
index c71c3f1b31..fd0d291d91 100644
--- a/packages/server/src/sdk/app/datasources/datasources.ts
+++ b/packages/server/src/sdk/app/datasources/datasources.ts
@@ -229,7 +229,7 @@ export async function removeSecretSingle(datasource: Datasource) {
}
export function mergeConfigs(update: Datasource, old: Datasource) {
- if (!update.config) {
+ if (!update.config || !old.config) {
return update
}
// specific to REST datasources, fix the auth configs again if required
diff --git a/packages/server/src/sdk/app/datasources/plus.ts b/packages/server/src/sdk/app/datasources/plus.ts
index 04cd508863..31ec51c728 100644
--- a/packages/server/src/sdk/app/datasources/plus.ts
+++ b/packages/server/src/sdk/app/datasources/plus.ts
@@ -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) {
+ const errors: Record = {}
+ 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 {
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(
diff --git a/packages/server/src/sdk/app/rows/search.ts b/packages/server/src/sdk/app/rows/search.ts
index 4b71179839..8b24f9bc5f 100644
--- a/packages/server/src/sdk/app/rows/search.ts
+++ b/packages/server/src/sdk/app/rows/search.ts
@@ -36,11 +36,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 {
diff --git a/packages/server/src/sdk/app/rows/search/external.ts b/packages/server/src/sdk/app/rows/search/external.ts
index 8465f997e3..e2d1a1b32c 100644
--- a/packages/server/src/sdk/app/rows/search/external.ts
+++ b/packages/server/src/sdk/app/rows/search/external.ts
@@ -101,7 +101,17 @@ export async function search(options: SearchParams) {
export async function exportRows(
options: ExportRowsParams
): Promise {
- 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 +163,17 @@ export async function exportRows(
rows = result.rows
}
- let exportRows = cleanExportRows(rows, schema, format, columns)
+ let exportRows = 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)
diff --git a/packages/server/src/sdk/app/rows/search/internal.ts b/packages/server/src/sdk/app/rows/search/internal.ts
index 22cb3985b7..2d3c32e02e 100644
--- a/packages/server/src/sdk/app/rows/search/internal.ts
+++ b/packages/server/src/sdk/app/rows/search/internal.ts
@@ -84,7 +84,17 @@ export async function search(options: SearchParams) {
export async function exportRows(
options: ExportRowsParams
): Promise {
- 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)
@@ -124,11 +134,16 @@ export async function exportRows(
rows = result
}
- let exportRows = 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 {
diff --git a/packages/server/src/sdk/app/rows/tests/internal.spec.ts b/packages/server/src/sdk/app/rows/tests/internal.spec.ts
index dda41d5720..3908ef83ed 100644
--- a/packages/server/src/sdk/app/rows/tests/internal.spec.ts
+++ b/packages/server/src/sdk/app/rows/tests/internal.spec.ts
@@ -81,7 +81,7 @@ describe("sdk >> rows >> internal", () => {
const response = await internalSdk.save(
table._id!,
row,
- config.user._id
+ config.getUser()._id
)
expect(response).toEqual({
@@ -129,7 +129,7 @@ describe("sdk >> rows >> internal", () => {
const response = await internalSdk.save(
table._id!,
row,
- config.user._id
+ config.getUser()._id
)
expect(response).toEqual({
@@ -190,15 +190,15 @@ describe("sdk >> rows >> internal", () => {
await config.doInContext(config.appId, async () => {
for (const row of makeRows(5)) {
- await internalSdk.save(table._id!, row, config.user._id)
+ await internalSdk.save(table._id!, row, config.getUser()._id)
}
await Promise.all(
makeRows(10).map(row =>
- internalSdk.save(table._id!, row, config.user._id)
+ internalSdk.save(table._id!, row, config.getUser()._id)
)
)
for (const row of makeRows(5)) {
- await internalSdk.save(table._id!, row, config.user._id)
+ await internalSdk.save(table._id!, row, config.getUser()._id)
}
})
diff --git a/packages/server/src/sdk/app/rows/utils.ts b/packages/server/src/sdk/app/rows/utils.ts
index 14868a4013..a8052462a9 100644
--- a/packages/server/src/sdk/app/rows/utils.ts
+++ b/packages/server/src/sdk/app/rows/utils.ts
@@ -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
+): 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(
diff --git a/packages/server/src/sdk/users/tests/utils.spec.ts b/packages/server/src/sdk/users/tests/utils.spec.ts
index efe790d49b..6f1c5afd3d 100644
--- a/packages/server/src/sdk/users/tests/utils.spec.ts
+++ b/packages/server/src/sdk/users/tests/utils.spec.ts
@@ -22,15 +22,18 @@ describe("syncGlobalUsers", () => {
expect(metadata).toHaveLength(1)
expect(metadata).toEqual([
expect.objectContaining({
- _id: db.generateUserMetadataID(config.user._id),
+ _id: db.generateUserMetadataID(config.getUser()._id!),
}),
])
})
})
it("admin and builders users are synced", async () => {
- const user1 = await config.createUser({ admin: true })
- const user2 = await config.createUser({ admin: false, builder: true })
+ const user1 = await config.createUser({ admin: { global: true } })
+ const user2 = await config.createUser({
+ admin: { global: false },
+ builder: { global: true },
+ })
await config.doInContext(config.appId, async () => {
expect(await rawUserMetadata()).toHaveLength(1)
await syncGlobalUsers()
@@ -51,7 +54,10 @@ describe("syncGlobalUsers", () => {
})
it("app users are not synced if not specified", async () => {
- const user = await config.createUser({ admin: false, builder: false })
+ const user = await config.createUser({
+ admin: { global: false },
+ builder: { global: false },
+ })
await config.doInContext(config.appId, async () => {
await syncGlobalUsers()
@@ -68,8 +74,14 @@ describe("syncGlobalUsers", () => {
it("app users are added when group is assigned to app", async () => {
await config.doInTenant(async () => {
const group = await proSdk.groups.save(structures.userGroups.userGroup())
- const user1 = await config.createUser({ admin: false, builder: false })
- const user2 = await config.createUser({ admin: false, builder: false })
+ const user1 = await config.createUser({
+ admin: { global: false },
+ builder: { global: false },
+ })
+ const user2 = await config.createUser({
+ admin: { global: false },
+ builder: { global: false },
+ })
await proSdk.groups.addUsers(group.id, [user1._id!, user2._id!])
await config.doInContext(config.appId, async () => {
@@ -103,8 +115,14 @@ describe("syncGlobalUsers", () => {
it("app users are removed when app is removed from user group", async () => {
await config.doInTenant(async () => {
const group = await proSdk.groups.save(structures.userGroups.userGroup())
- const user1 = await config.createUser({ admin: false, builder: false })
- const user2 = await config.createUser({ admin: false, builder: false })
+ const user1 = await config.createUser({
+ admin: { global: false },
+ builder: { global: false },
+ })
+ const user2 = await config.createUser({
+ admin: { global: false },
+ builder: { global: false },
+ })
await proSdk.groups.updateGroupApps(group.id, {
appsToAdd: [
{ appId: config.prodAppId!, roleId: roles.BUILTIN_ROLE_IDS.BASIC },
diff --git a/packages/server/src/startup.ts b/packages/server/src/startup.ts
index f9b5974eb2..abe931f503 100644
--- a/packages/server/src/startup.ts
+++ b/packages/server/src/startup.ts
@@ -38,6 +38,7 @@ async function initRoutes(app: Koa) {
// api routes
app.use(api.router.routes())
+ app.use(api.router.allowedMethods())
}
async function initPro() {
diff --git a/packages/server/src/tests/utilities/TestConfiguration.ts b/packages/server/src/tests/utilities/TestConfiguration.ts
index 22bb66b130..21605b7a5e 100644
--- a/packages/server/src/tests/utilities/TestConfiguration.ts
+++ b/packages/server/src/tests/utilities/TestConfiguration.ts
@@ -49,25 +49,31 @@ import {
AuthToken,
Automation,
CreateViewRequest,
+ Ctx,
Datasource,
FieldType,
INTERNAL_TABLE_SOURCE_ID,
+ Layout,
+ Query,
RelationshipFieldMetadata,
RelationshipType,
Row,
+ Screen,
SearchParams,
SourceName,
Table,
TableSourceType,
User,
- UserRoles,
+ UserCtx,
View,
+ Webhook,
WithRequired,
} from "@budibase/types"
import API from "./api"
import { cloneDeep } from "lodash"
import jwt, { Secret } from "jsonwebtoken"
+import { Server } from "http"
mocks.licenses.init(pro)
@@ -82,27 +88,23 @@ export interface TableToBuild extends Omit {
}
export default class TestConfiguration {
- server: any
- request: supertest.SuperTest | undefined
+ server?: Server
+ request?: supertest.SuperTest
started: boolean
- appId: string | null
- allApps: any[]
+ appId?: string
+ allApps: App[]
app?: App
- prodApp: any
- prodAppId: any
- user: any
- userMetadataId: any
+ prodApp?: App
+ prodAppId?: string
+ user?: User
+ userMetadataId?: string
table?: Table
- automation: any
+ automation?: Automation
datasource?: Datasource
tenantId?: string
api: API
csrfToken?: string
- private get globalUserId() {
- return this.user._id
- }
-
constructor(openServer = true) {
if (openServer) {
// use a random port because it doesn't matter
@@ -114,7 +116,7 @@ export default class TestConfiguration {
} else {
this.started = false
}
- this.appId = null
+ this.appId = undefined
this.allApps = []
this.api = new API(this)
@@ -125,46 +127,86 @@ export default class TestConfiguration {
}
getApp() {
+ if (!this.app) {
+ throw new Error("app has not been initialised, call config.init() first")
+ }
return this.app
}
getProdApp() {
+ if (!this.prodApp) {
+ throw new Error(
+ "prodApp has not been initialised, call config.init() first"
+ )
+ }
return this.prodApp
}
getAppId() {
if (!this.appId) {
- throw "appId has not been initialised properly"
+ throw new Error(
+ "appId has not been initialised, call config.init() first"
+ )
}
-
return this.appId
}
getProdAppId() {
+ if (!this.prodAppId) {
+ throw new Error(
+ "prodAppId has not been initialised, call config.init() first"
+ )
+ }
return this.prodAppId
}
+ getUser(): User {
+ if (!this.user) {
+ throw new Error("User has not been initialised, call config.init() first")
+ }
+ return this.user
+ }
+
getUserDetails() {
+ const user = this.getUser()
return {
- globalId: this.globalUserId,
- email: this.user.email,
- firstName: this.user.firstName,
- lastName: this.user.lastName,
+ globalId: user._id!,
+ email: user.email,
+ firstName: user.firstName,
+ lastName: user.lastName,
}
}
+ getAutomation() {
+ if (!this.automation) {
+ throw new Error(
+ "automation has not been initialised, call config.init() first"
+ )
+ }
+ return this.automation
+ }
+
+ getDatasource() {
+ if (!this.datasource) {
+ throw new Error(
+ "datasource has not been initialised, call config.init() first"
+ )
+ }
+ return this.datasource
+ }
+
async doInContext(
- appId: string | null,
+ appId: string | undefined,
task: () => Promise
): Promise {
- if (!appId) {
- appId = this.appId
- }
-
const tenant = this.getTenantId()
return tenancy.doInTenant(tenant, () => {
+ if (!appId) {
+ appId = this.appId
+ }
+
// check if already in a context
- if (context.getAppId() == null && appId !== null) {
+ if (context.getAppId() == null && appId) {
return context.doInAppContext(appId, async () => {
return task()
})
@@ -259,7 +301,11 @@ export default class TestConfiguration {
// UTILS
- _req(body: any, params: any, controlFunc: any) {
+ _req | void, Res>(
+ handler: (ctx: UserCtx) => Promise,
+ body?: Req,
+ params?: Record
+ ): Promise {
// create a fake request ctx
const request: any = {}
const appId = this.appId
@@ -278,63 +324,48 @@ export default class TestConfiguration {
throw new Error(`Error ${status} - ${message}`)
}
return this.doInContext(appId, async () => {
- await controlFunc(request)
+ await handler(request)
return request.body
})
}
// USER / AUTH
- async globalUser(
- config: {
- id?: string
- firstName?: string
- lastName?: string
- builder?: boolean
- admin?: boolean
- email?: string
- roles?: any
- } = {}
- ): Promise {
+ async globalUser(config: Partial = {}): Promise {
const {
- id = `us_${newid()}`,
+ _id = `us_${newid()}`,
firstName = generator.first(),
lastName = generator.last(),
- builder = true,
- admin = false,
+ builder = { global: true },
+ admin = { global: false },
email = generator.email(),
- roles,
+ tenantId = this.getTenantId(),
+ roles = {},
} = config
const db = tenancy.getTenantDB(this.getTenantId())
- let existing
+ let existing: Partial = {}
try {
- existing = await db.get(id)
+ existing = await db.get(_id)
} catch (err) {
- existing = { email }
+ // ignore
}
const user: User = {
- _id: id,
+ _id,
...existing,
- roles: roles || {},
- tenantId: this.getTenantId(),
+ ...config,
+ email,
+ roles,
+ tenantId,
firstName,
lastName,
+ builder,
+ admin,
}
- await sessions.createASession(id, {
+ await sessions.createASession(_id, {
sessionId: "sessionid",
tenantId: this.getTenantId(),
csrfToken: this.csrfToken,
})
- if (builder) {
- user.builder = { global: true }
- } else {
- user.builder = { global: false }
- }
- if (admin) {
- user.admin = { global: true }
- } else {
- user.admin = { global: false }
- }
const resp = await db.put(user)
return {
_rev: resp.rev,
@@ -342,38 +373,9 @@ export default class TestConfiguration {
}
}
- async createUser(
- user: {
- id?: string
- firstName?: string
- lastName?: string
- email?: string
- builder?: boolean
- admin?: boolean
- roles?: UserRoles
- } = {}
- ): Promise {
- const {
- id,
- firstName = generator.first(),
- lastName = generator.last(),
- email = generator.email(),
- builder = true,
- admin,
- roles,
- } = user
-
- const globalId = !id ? `us_${Math.random()}` : `us_${id}`
- const resp = await this.globalUser({
- id: globalId,
- firstName,
- lastName,
- email,
- builder,
- admin,
- roles: roles || {},
- })
- await cache.user.invalidateUser(globalId)
+ async createUser(user: Partial = {}): Promise {
+ const resp = await this.globalUser(user)
+ await cache.user.invalidateUser(resp._id!)
return resp
}
@@ -381,7 +383,7 @@ export default class TestConfiguration {
return context.doInTenant(this.tenantId!, async () => {
const baseGroup = structures.userGroups.userGroup()
baseGroup.roles = {
- [this.prodAppId]: roleId,
+ [this.getProdAppId()]: roleId,
}
const { id, rev } = await pro.sdk.groups.save(baseGroup)
return {
@@ -404,8 +406,18 @@ export default class TestConfiguration {
})
}
- async login({ roleId, userId, builder, prodApp = false }: any = {}) {
- const appId = prodApp ? this.prodAppId : this.appId
+ async login({
+ roleId,
+ userId,
+ builder,
+ prodApp,
+ }: {
+ roleId?: string
+ userId: string
+ builder: boolean
+ prodApp: boolean
+ }) {
+ const appId = prodApp ? this.getProdAppId() : this.getAppId()
return context.doInAppContext(appId, async () => {
userId = !userId ? `us_uuid1` : userId
if (!this.request) {
@@ -414,9 +426,9 @@ export default class TestConfiguration {
// make sure the user exists in the global DB
if (roleId !== roles.BUILTIN_ROLE_IDS.PUBLIC) {
await this.globalUser({
- id: userId,
- builder,
- roles: { [this.prodAppId]: roleId },
+ _id: userId,
+ builder: { global: builder },
+ roles: { [appId]: roleId || roles.BUILTIN_ROLE_IDS.BASIC },
})
}
await sessions.createASession(userId, {
@@ -445,8 +457,9 @@ export default class TestConfiguration {
defaultHeaders(extras = {}, prodApp = false) {
const tenantId = this.getTenantId()
+ const user = this.getUser()
const authObj: AuthToken = {
- userId: this.globalUserId,
+ userId: user._id!,
sessionId: "sessionid",
tenantId,
}
@@ -498,7 +511,7 @@ export default class TestConfiguration {
builder = false,
prodApp = true,
} = {}) {
- return this.login({ email, roleId, builder, prodApp })
+ return this.login({ userId: email, roleId, builder, prodApp })
}
// TENANCY
@@ -521,18 +534,22 @@ export default class TestConfiguration {
this.tenantId = structures.tenant.id()
this.user = await this.globalUser()
- this.userMetadataId = generateUserMetadataID(this.user._id)
+ this.userMetadataId = generateUserMetadataID(this.user._id!)
return this.createApp(appName)
}
- doInTenant(task: any) {
+ doInTenant(task: () => T) {
return context.doInTenant(this.getTenantId(), task)
}
// API
- async generateApiKey(userId = this.user._id) {
+ async generateApiKey(userId?: string) {
+ const user = this.getUser()
+ if (!userId) {
+ userId = user._id!
+ }
const db = tenancy.getTenantDB(this.getTenantId())
const id = dbCore.generateDevInfoID(userId)
let devInfo: any
@@ -552,25 +569,28 @@ export default class TestConfiguration {
async createApp(appName: string): Promise {
// create dev app
// clear any old app
- this.appId = null
- this.app = await context.doInTenant(this.tenantId!, async () => {
- const app = await this._req({ name: appName }, null, appController.create)
- this.appId = app.appId!
- return app
- })
- return await context.doInAppContext(this.getAppId(), async () => {
+ this.appId = undefined
+ this.app = await context.doInTenant(
+ this.tenantId!,
+ async () =>
+ (await this._req(appController.create, {
+ name: appName,
+ })) as App
+ )
+ this.appId = this.app.appId
+ return await context.doInAppContext(this.app.appId!, async () => {
// create production app
this.prodApp = await this.publish()
this.allApps.push(this.prodApp)
- this.allApps.push(this.app)
+ this.allApps.push(this.app!)
return this.app!
})
}
async publish() {
- await this._req(null, null, deployController.publishApp)
+ await this._req(deployController.publishApp)
// @ts-ignore
const prodAppId = this.getAppId().replace("_dev", "")
this.prodAppId = prodAppId
@@ -582,13 +602,11 @@ export default class TestConfiguration {
}
async unpublish() {
- const response = await this._req(
- null,
- { appId: this.appId },
- appController.unpublish
- )
- this.prodAppId = null
- this.prodApp = null
+ const response = await this._req(appController.unpublish, {
+ appId: this.appId,
+ })
+ this.prodAppId = undefined
+ this.prodApp = undefined
return response
}
@@ -716,8 +734,7 @@ export default class TestConfiguration {
// ROLE
async createRole(config?: any) {
- config = config || basicRole()
- return this._req(config, null, roleController.save)
+ return this._req(roleController.save, config || basicRole())
}
// VIEW
@@ -730,7 +747,7 @@ export default class TestConfiguration {
tableId: this.table!._id,
name: generator.guid(),
}
- return this._req(view, null, viewController.v1.save)
+ return this._req(viewController.v1.save, view)
}
async createView(
@@ -754,40 +771,38 @@ export default class TestConfiguration {
// AUTOMATION
- async createAutomation(config?: any) {
+ async createAutomation(config?: Automation) {
config = config || basicAutomation()
if (config._rev) {
delete config._rev
}
- this.automation = (
- await this._req(config, null, automationController.create)
- ).automation
+ const res = await this._req(automationController.create, config)
+ this.automation = res.automation
return this.automation
}
async getAllAutomations() {
- return this._req(null, null, automationController.fetch)
+ return this._req(automationController.fetch)
}
- async deleteAutomation(automation?: any) {
+ async deleteAutomation(automation?: Automation) {
automation = automation || this.automation
if (!automation) {
return
}
- return this._req(
- null,
- { id: automation._id, rev: automation._rev },
- automationController.destroy
- )
+ return this._req(automationController.destroy, undefined, {
+ id: automation._id,
+ rev: automation._rev,
+ })
}
- async createWebhook(config?: any) {
+ async createWebhook(config?: Webhook) {
if (!this.automation) {
throw "Must create an automation before creating webhook."
}
- config = config || basicWebhook(this.automation._id)
+ config = config || basicWebhook(this.automation._id!)
- return (await this._req(config, null, webhookController.save)).webhook
+ return (await this._req(webhookController.save, config)).webhook
}
// DATASOURCE
@@ -809,7 +824,7 @@ export default class TestConfiguration {
return { ...this.datasource, _id: this.datasource!._id! }
}
- async restDatasource(cfg?: any) {
+ async restDatasource(cfg?: Record) {
return this.createDatasource({
datasource: {
...basicDatasource().datasource,
@@ -866,26 +881,25 @@ export default class TestConfiguration {
// QUERY
- async createQuery(config?: any) {
- if (!this.datasource && !config) {
- throw "No datasource created for query."
- }
- config = config || basicQuery(this.datasource!._id!)
- return this._req(config, null, queryController.save)
+ async createQuery(config?: Query) {
+ return this._req(
+ queryController.save,
+ config || basicQuery(this.getDatasource()._id!)
+ )
}
// SCREEN
- async createScreen(config?: any) {
+ async createScreen(config?: Screen) {
config = config || basicScreen()
- return this._req(config, null, screenController.save)
+ return this._req(screenController.save, config)
}
// LAYOUT
- async createLayout(config?: any) {
+ async createLayout(config?: Layout) {
config = config || basicLayout()
- return await this._req(config, null, layoutController.save)
+ return await this._req(layoutController.save, config)
}
}
diff --git a/packages/server/src/tests/utilities/api/application.ts b/packages/server/src/tests/utilities/api/application.ts
index 9c784bade1..3951bba667 100644
--- a/packages/server/src/tests/utilities/api/application.ts
+++ b/packages/server/src/tests/utilities/api/application.ts
@@ -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 => {
+ 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 => {
+ 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 => {
+ 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 => {
+ // 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 => {
+ 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 => {
+ 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 => {
+ 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 => {
+ // 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 => {
+ // 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 => {
+ 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[]
+ }
}
diff --git a/packages/server/src/tests/utilities/structures.ts b/packages/server/src/tests/utilities/structures.ts
index 2fecf15fd6..5b50bd1175 100644
--- a/packages/server/src/tests/utilities/structures.ts
+++ b/packages/server/src/tests/utilities/structures.ts
@@ -22,6 +22,8 @@ import {
INTERNAL_TABLE_SOURCE_ID,
TableSourceType,
Query,
+ Webhook,
+ WebhookActionType,
} from "@budibase/types"
import { LoopInput, LoopStepType } from "../../definitions/automations"
@@ -407,12 +409,12 @@ export function basicLayout() {
return cloneDeep(EMPTY_LAYOUT)
}
-export function basicWebhook(automationId: string) {
+export function basicWebhook(automationId: string): Webhook {
return {
live: true,
name: "webhook",
action: {
- type: "automation",
+ type: WebhookActionType.AUTOMATION,
target: automationId,
},
}
diff --git a/packages/types/src/api/web/app/datasource.ts b/packages/types/src/api/web/app/datasource.ts
index 4a3d07a952..f931665917 100644
--- a/packages/types/src/api/web/app/datasource.ts
+++ b/packages/types/src/api/web/app/datasource.ts
@@ -32,9 +32,7 @@ export interface FetchDatasourceInfoResponse {
tableNames: string[]
}
-export interface UpdateDatasourceRequest extends Datasource {
- datasource: Datasource
-}
+export interface UpdateDatasourceRequest extends Datasource {}
export interface BuildSchemaFromSourceRequest {
tablesFilter?: string[]
diff --git a/packages/types/src/api/web/app/rows.ts b/packages/types/src/api/web/app/rows.ts
index dad3286754..14e28e4a01 100644
--- a/packages/types/src/api/web/app/rows.ts
+++ b/packages/types/src/api/web/app/rows.ts
@@ -37,6 +37,8 @@ export interface ExportRowsRequest {
query?: SearchFilters
sort?: string
sortOrder?: SortOrder
+ delimiter?: string
+ customHeaders?: { [key: string]: string }
}
export type ExportRowsResponse = ReadStream
diff --git a/packages/types/src/api/web/application.ts b/packages/types/src/api/web/application.ts
new file mode 100644
index 0000000000..87a0bd6ef9
--- /dev/null
+++ b/packages/types/src/api/web/application.ts
@@ -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
+}
diff --git a/packages/types/src/api/web/automation.ts b/packages/types/src/api/web/automation.ts
new file mode 100644
index 0000000000..c1f3d01b2f
--- /dev/null
+++ b/packages/types/src/api/web/automation.ts
@@ -0,0 +1,3 @@
+import { DocumentDestroyResponse } from "@budibase/nano"
+
+export interface DeleteAutomationResponse extends DocumentDestroyResponse {}
diff --git a/packages/types/src/api/web/index.ts b/packages/types/src/api/web/index.ts
index 75c246ab9b..62d8ce8280 100644
--- a/packages/types/src/api/web/index.ts
+++ b/packages/types/src/api/web/index.ts
@@ -1,3 +1,4 @@
+export * from "./application"
export * from "./analytics"
export * from "./auth"
export * from "./user"
@@ -10,3 +11,5 @@ export * from "./global"
export * from "./pagination"
export * from "./searchFilter"
export * from "./cookies"
+export * from "./automation"
+export * from "./layout"
diff --git a/packages/types/src/api/web/layout.ts b/packages/types/src/api/web/layout.ts
new file mode 100644
index 0000000000..50512777ef
--- /dev/null
+++ b/packages/types/src/api/web/layout.ts
@@ -0,0 +1,5 @@
+import { Layout } from "../../documents"
+
+export interface SaveLayoutRequest extends Layout {}
+
+export interface SaveLayoutResponse extends Layout {}
diff --git a/packages/types/src/documents/app/app.ts b/packages/types/src/documents/app/app.ts
index 08aafc6527..ae4f3fa6da 100644
--- a/packages/types/src/documents/app/app.ts
+++ b/packages/types/src/documents/app/app.ts
@@ -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 {
diff --git a/packages/types/src/documents/app/datasource.ts b/packages/types/src/documents/app/datasource.ts
index 67035a2e72..8976e1cae3 100644
--- a/packages/types/src/documents/app/datasource.ts
+++ b/packages/types/src/documents/app/datasource.ts
@@ -6,6 +6,9 @@ export interface Datasource extends Document {
type: string
name?: string
source: SourceName
+ // this is a googlesheets specific property which
+ // can be found in the GSheets schema - pertains to SSO creds
+ auth?: { type: string }
// the config is defined by the schema
config?: Record
plus?: boolean
@@ -36,6 +39,12 @@ export interface RestAuthConfig {
config: RestBasicAuthConfig | RestBearerAuthConfig
}
+export interface DynamicVariable {
+ name: string
+ queryId: string
+ value: string
+}
+
export interface RestConfig {
url: string
rejectUnauthorized: boolean
@@ -47,11 +56,5 @@ export interface RestConfig {
staticVariables: {
[key: string]: string
}
- dynamicVariables: [
- {
- name: string
- queryId: string
- value: string
- }
- ]
+ dynamicVariables: DynamicVariable[]
}
diff --git a/packages/types/src/documents/app/layout.ts b/packages/types/src/documents/app/layout.ts
index 06542f680d..51ce511712 100644
--- a/packages/types/src/documents/app/layout.ts
+++ b/packages/types/src/documents/app/layout.ts
@@ -1,6 +1,11 @@
import { Document } from "../document"
export interface Layout extends Document {
+ componentLibraries: string[]
+ title: string
+ favicon: string
+ stylesheets: string[]
props: any
layoutId?: string
+ name?: string
}
diff --git a/packages/types/src/documents/app/screen.ts b/packages/types/src/documents/app/screen.ts
index 58c00ef3d6..4977c79b0b 100644
--- a/packages/types/src/documents/app/screen.ts
+++ b/packages/types/src/documents/app/screen.ts
@@ -22,4 +22,5 @@ export interface Screen extends Document {
routing: ScreenRouting
props: ScreenProps
name?: string
+ pluginAdded?: boolean
}
diff --git a/packages/types/src/documents/pouch.ts b/packages/types/src/documents/pouch.ts
index 11efc502be..6ff851a515 100644
--- a/packages/types/src/documents/pouch.ts
+++ b/packages/types/src/documents/pouch.ts
@@ -5,15 +5,15 @@ export interface RowValue {
deleted: boolean
}
-export interface RowResponse {
+export interface RowResponse {
id: string
key: string
error: string
- value: T | RowValue
+ value: T
doc?: T
}
-export interface AllDocsResponse {
+export interface AllDocsResponse {
offset: number
total_rows: number
rows: RowResponse[]
diff --git a/packages/types/src/sdk/datasources.ts b/packages/types/src/sdk/datasources.ts
index 7a335eb3b9..6b09959b6c 100644
--- a/packages/types/src/sdk/datasources.ts
+++ b/packages/types/src/sdk/datasources.ts
@@ -1,4 +1,5 @@
-import { Table } from "../documents"
+import { Table, Row } from "../documents"
+import { QueryJson } from "./search"
export const PASSWORD_REPLACEMENT = "--secret-value--"
@@ -56,6 +57,7 @@ export enum SourceName {
FIRESTORE = "FIRESTORE",
REDIS = "REDIS",
SNOWFLAKE = "SNOWFLAKE",
+ BUDIBASE = "BUDIBASE",
}
export enum IncludeRelationship {
@@ -180,11 +182,24 @@ export interface Schema {
errors: Record
}
+// return these when an operation occurred but we got no response
+enum DSPlusOperation {
+ CREATE = "create",
+ READ = "read",
+ UPDATE = "update",
+ DELETE = "delete",
+}
+
+export type DatasourcePlusQueryResponse = Promise<
+ Row[] | Record[] | 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): DatasourcePlusQueryResponse
buildSchema(
datasourceId: string,
entities: Record
diff --git a/packages/types/src/sdk/db.ts b/packages/types/src/sdk/db.ts
index 9e44a4827f..c4e4a4f02f 100644
--- a/packages/types/src/sdk/db.ts
+++ b/packages/types/src/sdk/db.ts
@@ -1,5 +1,11 @@
import type Nano from "@budibase/nano"
-import { AllDocsResponse, AnyDocument, Document, ViewTemplateOpts } from "../"
+import {
+ AllDocsResponse,
+ AnyDocument,
+ Document,
+ RowValue,
+ ViewTemplateOpts,
+} from "../"
import { Writable } from "stream"
export enum SearchIndex {
@@ -135,7 +141,7 @@ export interface Database {
opts?: DatabasePutOpts
): Promise
bulkDocs(documents: AnyDocument[]): Promise
- allDocs(
+ allDocs(
params: DatabaseQueryOpts
): Promise>
query(
diff --git a/packages/types/src/sdk/search.ts b/packages/types/src/sdk/search.ts
index 35fd148c05..67c344d845 100644
--- a/packages/types/src/sdk/search.ts
+++ b/packages/types/src/sdk/search.ts
@@ -94,6 +94,7 @@ export interface QueryJson {
idFilter?: SearchFilters
}
relationships?: RelationshipsJson[]
+ tableAliases?: Record
}
export interface SqlQuery {
diff --git a/packages/worker/scripts/test.sh b/packages/worker/scripts/test.sh
index eba95c4916..17b3ee17f4 100644
--- a/packages/worker/scripts/test.sh
+++ b/packages/worker/scripts/test.sh
@@ -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
\ No newline at end of file
diff --git a/packages/worker/src/tests/TestConfiguration.ts b/packages/worker/src/tests/TestConfiguration.ts
index df6726eed1..3ebfb5f020 100644
--- a/packages/worker/src/tests/TestConfiguration.ts
+++ b/packages/worker/src/tests/TestConfiguration.ts
@@ -280,7 +280,7 @@ class TestConfiguration {
const db = context.getGlobalDB()
- const id = dbCore.generateDevInfoID(this.user!._id)
+ const id = dbCore.generateDevInfoID(this.user!._id!)
// TODO: dry
this.apiKey = encryption.encrypt(
`${this.tenantId}${dbCore.SEPARATOR}${utils.newid()}`
diff --git a/qa-core/src/internal-api/api/apis/AppAPI.ts b/qa-core/src/internal-api/api/apis/AppAPI.ts
index a9f9a6a841..8b291a628e 100644
--- a/qa-core/src/internal-api/api/apis/AppAPI.ts
+++ b/qa-core/src/internal-api/api/apis/AppAPI.ts
@@ -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"
diff --git a/qa-core/src/internal-api/fixtures/applications.ts b/qa-core/src/internal-api/fixtures/applications.ts
index 01dd18fc6a..59f73ba863 100644
--- a/qa-core/src/internal-api/fixtures/applications.ts
+++ b/qa-core/src/internal-api/fixtures/applications.ts
@@ -1,5 +1,5 @@
import { generator } from "../../shared"
-import { CreateAppRequest } from "../../types"
+import { CreateAppRequest } from "@budibase/types"
function uniqueWord() {
return generator.word() + generator.hash()
diff --git a/qa-core/src/internal-api/tests/tables/tables.spec.ts b/qa-core/src/internal-api/tests/tables/tables.spec.ts
index 09d8f68e86..a38b8e6059 100644
--- a/qa-core/src/internal-api/tests/tables/tables.spec.ts
+++ b/qa-core/src/internal-api/tests/tables/tables.spec.ts
@@ -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())
diff --git a/qa-core/src/shared/BudibaseTestConfiguration.ts b/qa-core/src/shared/BudibaseTestConfiguration.ts
index 18b7c89ec8..9a12f3e65d 100644
--- a/qa-core/src/shared/BudibaseTestConfiguration.ts
+++ b/qa-core/src/shared/BudibaseTestConfiguration.ts
@@ -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
diff --git a/qa-core/src/types/app.ts b/qa-core/src/types/app.ts
deleted file mode 100644
index 7159112024..0000000000
--- a/qa-core/src/types/app.ts
+++ /dev/null
@@ -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
-}
diff --git a/qa-core/src/types/index.ts b/qa-core/src/types/index.ts
index 9bde46c66e..a44df4ef3c 100644
--- a/qa-core/src/types/index.ts
+++ b/qa-core/src/types/index.ts
@@ -1,6 +1,5 @@
export * from "./api"
export * from "./apiKeyResponse"
-export * from "./app"
export * from "./appPackage"
export * from "./deploy"
export * from "./newAccount"
diff --git a/yarn.lock b/yarn.lock
index 21d3c5a76b..260ae3870a 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -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==
@@ -3419,9 +3419,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"
@@ -3902,19 +3902,6 @@
magic-string "^0.25.7"
resolve "^1.17.0"
-"@rollup/plugin-commonjs@^18.0.0":
- version "18.1.0"
- resolved "https://registry.yarnpkg.com/@rollup/plugin-commonjs/-/plugin-commonjs-18.1.0.tgz#5a760d757af168a50727c0ae080251fbfcc5eb02"
- integrity sha512-h3e6T9rUxVMAQswpDIobfUHn/doMzM9sgkMrsMWCFLmB84PSoC8mV8tOloAJjSRwdqhXBqstlX2BwBpHJvbhxg==
- dependencies:
- "@rollup/pluginutils" "^3.1.0"
- commondir "^1.0.1"
- estree-walker "^2.0.1"
- glob "^7.1.6"
- is-reference "^1.2.1"
- magic-string "^0.25.7"
- resolve "^1.17.0"
-
"@rollup/plugin-commonjs@^25.0.7":
version "25.0.7"
resolved "https://registry.yarnpkg.com/@rollup/plugin-commonjs/-/plugin-commonjs-25.0.7.tgz#145cec7589ad952171aeb6a585bbeabd0fd3b4cf"
@@ -4025,70 +4012,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"
@@ -5232,16 +5219,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"
@@ -5636,10 +5623,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"
@@ -5665,10 +5652,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"
@@ -6088,9 +6082,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" "*"
@@ -6540,16 +6534,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"
@@ -6560,10 +6549,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"
@@ -7005,7 +6994,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==
@@ -7056,7 +7045,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==
@@ -7659,6 +7653,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"
@@ -7923,9 +7922,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"
@@ -8667,6 +8666,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"
@@ -9566,9 +9573,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"
@@ -9624,7 +9631,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==
@@ -9754,9 +9770,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"
@@ -10804,13 +10820,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"
@@ -10870,7 +10893,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==
@@ -15619,17 +15642,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==
@@ -15822,6 +15835,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"
@@ -17184,11 +17202,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"
@@ -17813,10 +17826,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"
@@ -18057,6 +18078,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"
@@ -19257,7 +19283,7 @@ rollup@2.45.2:
optionalDependencies:
fsevents "~2.3.1"
-rollup@^2.36.2, rollup@^2.44.0, rollup@^2.45.2:
+rollup@^2.36.2, rollup@^2.45.2:
version "2.79.1"
resolved "https://registry.yarnpkg.com/rollup/-/rollup-2.79.1.tgz#bedee8faef7c9f93a2647ac0108748f497f081c7"
integrity sha512-uKxbd0IhMZOhjAiD5oAFp7BqvkA4Dv47qpOCtaNvng4HBwdbWtdOh8f5nZNuk2rp51PMGk3bzfWu5oayNEuYnw==
@@ -19272,25 +19298,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:
@@ -19322,7 +19348,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==
@@ -20016,7 +20049,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==
@@ -20094,9 +20138,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"
@@ -20559,9 +20603,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"
@@ -20971,9 +21015,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"
@@ -21413,11 +21457,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"
@@ -21462,9 +21501,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"
@@ -21477,9 +21516,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"
@@ -21803,18 +21842,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==
@@ -22448,7 +22476,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==