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 +

+

+ 자체 인프라에서 몇 분 만에 맞춤형 비즈니스 도구를 구축하세요. +

+

+ Budibase는 개발자와 IT 전문가가 몇 분 만에 맞춤형 애플리케이션을 구축하고 자동화할 수 있는 오픈 소스 로우코드 플랫폼입니다. +

+ +

+ 🤖 🎨 🚀 +

+ +

+ Budibase design ui +

+ +

+ + GitHub all releases + + + GitHub release (latest by date) + + + Follow @budibase + + Code of conduct + + + +

+ +

+ 소개 + · + 문서 + · + 기능 요청 + · + 버그 보고 + · + 지원: 토론 +

+ +

+## ✨ 특징 + +### "실제" 소프트웨어를 구축할 수 있습니다. +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 data +

+

+ +### 강력한 내장 구성 요소로 애플리케이션을 설계하고 구축할 수 있습니다. + +Budibase에는 아름답게 디자인된 강력한 컴포넌트들이 제공되며, 이를 사용하여 UI를 쉽게 구축할 수 있습니다. 또한, CSS를 통한 스타일링 옵션도 풍부하게 제공되어 보다 창의적인 표현도 가능하다. + [Request new component](https://github.com/Budibase/budibase/discussions?discussions_q=category%3AIdeas). + +

+ Budibase design +

+

+ +### 프로세스를 자동화하고, 다른 도구와 연동하고, 웹훅으로 연결하세요! +워크플로우와 수동 프로세스를 자동화하여 시간을 절약하세요. 웹훅 이벤트 연결부터 이메일 자동화까지, Budibase에 수행할 작업을 지시하기만 하면 자동으로 처리됩니다. [새로운 자동화 만들기](https://github.com/Budibase/automations)또는[새로운 자동화를 요청할 수 있습니다](https://github.com/Budibase/budibase/discussions?discussions_q=category%3AIdeas). + +

+ Budibase automations +

+

+ +### 선호하는 도구 +Budibase는 사용자의 선호도에 따라 애플리케이션을 구축할 수 있는 다양한 도구를 통합하고 있습니다. + +

+ Budibase integrations +

+

+ +### 관리자의 천국 +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)): + + + + + + + + + + + + + + + + + + + + + + + + + +

Martin McKeaveney

💻 📖 ⚠️ 🚇

Michael Drury

📖 💻 ⚠️ 🚇

Andrew Kingston

📖 💻 ⚠️ 🎨

Michael Shanks

📖 💻 ⚠️

Kevin Åberg Kultalahti

📖 💻 ⚠️

Joe

📖 💻 🖋 🎨

Rory Powell

💻 📖 ⚠️

Peter Clement

💻 📖 ⚠️

Conor_Mack

💻 ⚠️

pngwn

💻 ⚠️

HugoLd

💻

victoriasloan

💻

yashank09

💻

SOVLOOKUP

💻

seoulaja

🌍

Maurits Lourens

⚠️ 💻
+ + + + + + +이 프로젝트는 다음 사양을 따릅니다. [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 de6d44c372..19f7a5829f 160000 --- a/packages/account-portal +++ b/packages/account-portal @@ -1 +1 @@ -Subproject commit de6d44c372a7f48ca0ce8c6c0c19311d4bc21646 +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/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 = [])} /> + - { + 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/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 c838e14744..3c2fb2046f 100644 --- a/packages/server/src/api/controllers/application.ts +++ b/packages/server/src/api/controllers/application.ts @@ -48,6 +48,9 @@ import { PlanType, Screen, UserCtx, + CreateAppRequest, + FetchAppDefinitionResponse, + FetchAppPackageResponse, } from "@budibase/types" import { BASE_LAYOUT_PROP_IDS } from "../../constants/layouts" import sdk from "../../sdk" @@ -59,23 +62,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) { @@ -117,8 +120,8 @@ function checkAppName( } interface AppTemplate { - templateString: string - useTemplate: string + templateString?: string + useTemplate?: string file?: { type?: string path: string @@ -175,14 +178,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() @@ -197,10 +202,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() @@ -232,17 +239,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, @@ -273,7 +284,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(), @@ -435,7 +446,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, @@ -508,7 +521,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) @@ -683,12 +696,15 @@ export async function duplicateApp(ctx: UserCtx) { ctx.status = 200 } -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 f72a8c3c22..a065a22524 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 d202e31d2b..3a69cf34d0 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) @@ -148,164 +134,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) @@ -313,51 +241,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) }) @@ -422,28 +325,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 1e238576e2..f7b8a703bb 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,29 +569,28 @@ export default class TestConfiguration { async createApp(appName: string, url?: 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, url }, - 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 @@ -586,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 } @@ -720,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 @@ -734,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( @@ -758,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 @@ -813,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, @@ -870,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 2c367e469b..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" @@ -4012,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" @@ -5219,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" @@ -5623,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" @@ -5652,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" @@ -6075,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" "*" @@ -6527,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" @@ -6547,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" @@ -6992,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== @@ -7043,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== @@ -7646,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" @@ -7910,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" @@ -8654,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" @@ -9553,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" @@ -9611,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== @@ -9741,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" @@ -10791,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" @@ -10857,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== @@ -15606,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== @@ -15809,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" @@ -17171,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" @@ -17800,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" @@ -18044,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" @@ -19259,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: @@ -19309,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== @@ -20003,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== @@ -20081,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" @@ -20546,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" @@ -20958,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" @@ -21400,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" @@ -21449,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" @@ -21464,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" @@ -21790,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== @@ -22435,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==