Merge remote-tracking branch 'origin/master' into feature/form-screen-template

This commit is contained in:
Dean 2024-03-01 12:23:12 +00:00
commit d9b0b1c1e3
94 changed files with 3138 additions and 1141 deletions

221
i18n/README.kr.md Normal file
View File

@ -0,0 +1,221 @@
<p align="center">
<a href="https://www.budibase.com">
<img alt="Budibase" src="https://res.cloudinary.com/daog6scxm/image/upload/v1696515725/Branding/Assets/Symbol/RGB/Full%20Colour/Budibase_Symbol_RGB_FullColour_cbqvha_1_z5cwq2.svg" width="60" />
</a>
</p>
<h1 align="center">
Budibase
</h1>
<h3 align="center">
자체 인프라에서 몇 분 만에 맞춤형 비즈니스 도구를 구축하세요.
</h3>
<p align="center">
Budibase는 개발자와 IT 전문가가 몇 분 만에 맞춤형 애플리케이션을 구축하고 자동화할 수 있는 오픈 소스 로우코드 플랫폼입니다.
</p>
<h3 align="center">
🤖 🎨 🚀
</h3>
<p align="center">
<img alt="Budibase design ui" src="https://res.cloudinary.com/daog6scxm/image/upload/v1633524049/ui/design-ui-wide-mobile_gdaveq.jpg">
</p>
<p align="center">
<a href="https://github.com/Budibase/budibase/releases">
<img alt="GitHub all releases" src="https://img.shields.io/github/downloads/Budibase/budibase/total">
</a>
<a href="https://github.com/Budibase/budibase/releases">
<img alt="GitHub release (latest by date)" src="https://img.shields.io/github/v/release/Budibase/budibase">
</a>
<a href="https://twitter.com/intent/follow?screen_name=budibase">
<img src="https://img.shields.io/twitter/follow/budibase?style=social" alt="Follow @budibase" />
</a>
<img src="https://img.shields.io/badge/Contributor%20Covenant-v2.0%20adopted-ff69b4.svg" alt="Code of conduct" />
<a href="https://codecov.io/gh/Budibase/budibase">
<img src="https://codecov.io/gh/Budibase/budibase/graph/badge.svg?token=E8W2ZFXQOH"/>
</a>
</p>
<h3 align="center">
<a href="https://docs.budibase.com/getting-started">소개</a>
<span> · </span>
<a href="https://docs.budibase.com">문서</a>
<span> · </span>
<a href="https://github.com/Budibase/budibase/discussions?discussions_q=category%3AIdeas">기능 요청</a>
<span> · </span>
<a href="https://github.com/Budibase/budibase/issues">버그 보고</a>
<span> · </span>
지원: <a href="https://github.com/Budibase/budibase/discussions">토론</a>
</h3>
<br /><br />
## ✨ 특징
### "실제" 소프트웨어를 구축할 수 있습니다.
Budibase를 사용하면 고성능 단일 페이지 애플리케이션을 구축할 수 있습니다. 또한 반응형 디자인으로 제작하여 사용자에게 멋진 경험을 제공할 수 있습니다.
<br /><br />
### 오픈 소스 및 확장성
Budibase는 오픈소스이며, GPL v3 라이선스에 따라 공개되어 있습니다. 이는 Budibase가 항상 당신 곁에 있다는 안도감을 줄 것입니다. 그리고 우리는 개발자 친화적인 환경을 제공하고 있기 때문에, 당신은 원하는 만큼 소스 코드를 포크하여 수정하거나 Budibase에 직접 기여할 수 있습니다.
<br /><br />
### 기존 데이터 또는 처음부터 시작
Budibase를 사용하면 다음과 같은 여러 소스에서 데이터를 가져올 수 있습니다: MondoDB, CouchDB, PostgreSQL, MySQL, Airtable, S3, DynamoDB 또는 REST API.
또는 원하는 경우 외부 도구 없이도 Budibase를 사용하여 처음부터 시작하여 자체 애플리케이션을 구축할 수 있습니다.[데이터 소스 제안](https://github.com/Budibase/budibase/discussions?discussions_q=category%3AIdeas).
<p align="center">
<img alt="Budibase data" src="https://res.cloudinary.com/daog6scxm/image/upload/v1636970242/Out%20of%20beta%20launch/data_n1tlhf.png">
</p>
<br /><br />
### 강력한 내장 구성 요소로 애플리케이션을 설계하고 구축할 수 있습니다.
Budibase에는 아름답게 디자인된 강력한 컴포넌트들이 제공되며, 이를 사용하여 UI를 쉽게 구축할 수 있습니다. 또한, CSS를 통한 스타일링 옵션도 풍부하게 제공되어 보다 창의적인 표현도 가능하다.
[Request new component](https://github.com/Budibase/budibase/discussions?discussions_q=category%3AIdeas).
<p align="center">
<img alt="Budibase design" src="https://res.cloudinary.com/daog6scxm/image/upload/v1636970243/Out%20of%20beta%20launch/design-like-a-pro_qhlfeu.gif">
</p>
<br /><br />
### 프로세스를 자동화하고, 다른 도구와 연동하고, 웹훅으로 연결하세요!
워크플로우와 수동 프로세스를 자동화하여 시간을 절약하세요. 웹훅 이벤트 연결부터 이메일 자동화까지, Budibase에 수행할 작업을 지시하기만 하면 자동으로 처리됩니다. [새로운 자동화 만들기](https://github.com/Budibase/automations)또는[새로운 자동화를 요청할 수 있습니다](https://github.com/Budibase/budibase/discussions?discussions_q=category%3AIdeas).
<p align="center">
<img alt="Budibase automations" src="https://res.cloudinary.com/daog6scxm/image/upload/v1636970486/Out%20of%20beta%20launch/automation_riro7u.png">
</p>
<br /><br />
### 선호하는 도구
Budibase는 사용자의 선호도에 따라 애플리케이션을 구축할 수 있는 다양한 도구를 통합하고 있습니다.
<p align="center">
<img alt="Budibase integrations" src="https://res.cloudinary.com/daog6scxm/image/upload/v1636970242/Out%20of%20beta%20launch/integrations_kc7dqt.png">
</p>
<br /><br />
### 관리자의 천국
Budibase는 어떤 규모의 프로젝트에도 유연하게 대응할 수 있으며, Budibase를 사용하면 개인 또는 조직의 서버에서 자체 호스팅하고 사용자, 온보딩, SMTP, 앱, 그룹, 테마 등을 한꺼번에 관리할 수 있습니다. 또한, 사용자나 그룹에 앱 포털을 제공하고 그룹 관리자에게 사용자 관리를 맡길 수도 있다.
- 프로모션 비디오: https://youtu.be/xoljVpty_Kw
<br /><br /><br />
## 🏁 시작
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)
<br /><br />
## 🎓 Budibase 알아보기
문서 [documentacion de Budibase](https://docs.budibase.com/docs).
<br />
<br /><br />
## 💬 커뮤니티
질문하고, 다른 사람을 돕고, 다른 Budibase 사용자와 즐거운 대화를 나눌 수 있는 Budibase 커뮤니티에 여러분을 초대합니다.
[깃허브 토론](https://github.com/Budibase/budibase/discussions)
<br /><br /><br />
## ❗ 행동강령
Budibase 는 모든 계층의 사람들을 환영하고 상호 존중하는 환경을 제공하는 데 특별한 주의를 기울이고 있습니다. 저희는 커뮤니티에도 같은 기대를 가지고 있습니다.
[**행동 강령**](https://github.com/Budibase/budibase/blob/HEAD/.github/CODE_OF_CONDUCT.md).
<br />
<br /><br />
## 🙌 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)
<br /><br />
## 📝 라이선스
Budibase는 오픈 소스이며, 라이선스는 다음과 같습니다 [GPL v3](https://www.gnu.org/licenses/gpl-3.0.en.html). 클라이언트 및 컴포넌트 라이브러리는 다음과 같이 라이선스가 부여됩니다. [MPL](https://directory.fsf.org/wiki/License:MPL-2.0) - 이렇게 하면 빌드한 애플리케이션에 원하는 대로 라이선스를 부여할 수 있습니다.
<br /><br />
## ⭐ 스타 수의 역사
[![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) 를 참고하여 환경을 정리해 주세요.
<br /><br />
## Contributors ✨
훌륭한 여러분께 감사할 따름입니다. ([emoji key](https://allcontributors.org/docs/en/emoji-key)):
<!-- ALL-CONTRIBUTORS-LIST:START - Do not remove or modify this section -->
<!-- prettier-ignore-start -->
<!-- markdownlint-disable -->
<table>
<tr>
<td align="center"><a href="http://martinmck.com"><img src="https://avatars1.githubusercontent.com/u/11256663?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Martin McKeaveney</b></sub></a><br /><a href="https://github.com/Budibase/budibase/commits?author=shogunpurple" title="Code">💻</a> <a href="https://github.com/Budibase/budibase/commits?author=shogunpurple" title="Documentation">📖</a> <a href="https://github.com/Budibase/budibase/commits?author=shogunpurple" title="Tests">⚠️</a> <a href="#infra-shogunpurple" title="Infrastructure (Hosting, Build-Tools, etc)">🚇</a></td>
<td align="center"><a href="http://www.michaeldrury.co.uk/"><img src="https://avatars2.githubusercontent.com/u/4407001?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Michael Drury</b></sub></a><br /><a href="https://github.com/Budibase/budibase/commits?author=mike12345567" title="Documentation">📖</a> <a href="https://github.com/Budibase/budibase/commits?author=mike12345567" title="Code">💻</a> <a href="https://github.com/Budibase/budibase/commits?author=mike12345567" title="Tests">⚠️</a> <a href="#infra-mike12345567" title="Infrastructure (Hosting, Build-Tools, etc)">🚇</a></td>
<td align="center"><a href="https://github.com/aptkingston"><img src="https://avatars3.githubusercontent.com/u/9075550?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Andrew Kingston</b></sub></a><br /><a href="https://github.com/Budibase/budibase/commits?author=aptkingston" title="Documentation">📖</a> <a href="https://github.com/Budibase/budibase/commits?author=aptkingston" title="Code">💻</a> <a href="https://github.com/Budibase/budibase/commits?author=aptkingston" title="Tests">⚠️</a> <a href="#design-aptkingston" title="Design">🎨</a></td>
<td align="center"><a href="https://budibase.com/"><img src="https://avatars3.githubusercontent.com/u/3524181?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Michael Shanks</b></sub></a><br /><a href="https://github.com/Budibase/budibase/commits?author=mjashanks" title="Documentation">📖</a> <a href="https://github.com/Budibase/budibase/commits?author=mjashanks" title="Code">💻</a> <a href="https://github.com/Budibase/budibase/commits?author=mjashanks" title="Tests">⚠️</a></td>
<td align="center"><a href="https://github.com/kevmodrome"><img src="https://avatars3.githubusercontent.com/u/534488?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Kevin Åberg Kultalahti</b></sub></a><br /><a href="https://github.com/Budibase/budibase/commits?author=kevmodrome" title="Documentation">📖</a> <a href="https://github.com/Budibase/budibase/commits?author=kevmodrome" title="Code">💻</a> <a href="https://github.com/Budibase/budibase/commits?author=kevmodrome" title="Tests">⚠️</a></td>
<td align="center"><a href="https://www.budibase.com/"><img src="https://avatars2.githubusercontent.com/u/49767913?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Joe</b></sub></a><br /><a href="https://github.com/Budibase/budibase/commits?author=joebudi" title="Documentation">📖</a> <a href="https://github.com/Budibase/budibase/commits?author=joebudi" title="Code">💻</a> <a href="#content-joebudi" title="Content">🖋</a> <a href="#design-joebudi" title="Design">🎨</a></td>
<td align="center"><a href="https://github.com/Rory-Powell"><img src="https://avatars.githubusercontent.com/u/8755148?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Rory Powell</b></sub></a><br /><a href="https://github.com/Budibase/budibase/commits?author=Rory-Powell" title="Code">💻</a> <a href="https://github.com/Budibase/budibase/commits?author=Rory-Powell" title="Documentation">📖</a> <a href="https://github.com/Budibase/budibase/commits?author=Rory-Powell" title="Tests">⚠️</a></td>
</tr>
<tr>
<td align="center"><a href="https://github.com/PClmnt"><img src="https://avatars.githubusercontent.com/u/5665926?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Peter Clement</b></sub></a><br /><a href="https://github.com/Budibase/budibase/commits?author=PClmnt" title="Code">💻</a> <a href="https://github.com/Budibase/budibase/commits?author=PClmnt" title="Documentation">📖</a> <a href="https://github.com/Budibase/budibase/commits?author=PClmnt" title="Tests">⚠️</a></td>
<td align="center"><a href="https://github.com/Conor-Mack"><img src="https://avatars1.githubusercontent.com/u/36074859?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Conor_Mack</b></sub></a><br /><a href="https://github.com/Budibase/budibase/commits?author=Conor-Mack" title="Code">💻</a> <a href="https://github.com/Budibase/budibase/commits?author=Conor-Mack" title="Tests">⚠️</a></td>
<td align="center"><a href="https://github.com/pngwn"><img src="https://avatars1.githubusercontent.com/u/12937446?v=4?s=100" width="100px;" alt=""/><br /><sub><b>pngwn</b></sub></a><br /><a href="https://github.com/Budibase/budibase/commits?author=pngwn" title="Code">💻</a> <a href="https://github.com/Budibase/budibase/commits?author=pngwn" title="Tests">⚠️</a></td>
<td align="center"><a href="https://github.com/HugoLd"><img src="https://avatars0.githubusercontent.com/u/26521848?v=4?s=100" width="100px;" alt=""/><br /><sub><b>HugoLd</b></sub></a><br /><a href="https://github.com/Budibase/budibase/commits?author=HugoLd" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/victoriasloan"><img src="https://avatars.githubusercontent.com/u/9913651?v=4?s=100" width="100px;" alt=""/><br /><sub><b>victoriasloan</b></sub></a><br /><a href="https://github.com/Budibase/budibase/commits?author=victoriasloan" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/yashank09"><img src="https://avatars.githubusercontent.com/u/37672190?v=4?s=100" width="100px;" alt=""/><br /><sub><b>yashank09</b></sub></a><br /><a href="https://github.com/Budibase/budibase/commits?author=yashank09" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/SOVLOOKUP"><img src="https://avatars.githubusercontent.com/u/53158137?v=4?s=100" width="100px;" alt=""/><br /><sub><b>SOVLOOKUP</b></sub></a><br /><a href="https://github.com/Budibase/budibase/commits?author=SOVLOOKUP" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/seoulaja"><img src="https://avatars.githubusercontent.com/u/15101654?v=4?s=100" width="100px;" alt=""/><br /><sub><b>seoulaja</b></sub></a><br /><a href="#translation-seoulaja" title="Translation">🌍</a></td>
<td align="center"><a href="https://github.com/mslourens"><img src="https://avatars.githubusercontent.com/u/1907152?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Maurits Lourens</b></sub></a><br /><a href="https://github.com/Budibase/budibase/commits?author=mslourens" title="Tests">⚠️</a> <a href="https://github.com/Budibase/budibase/commits?author=mslourens" title="Code">💻</a></td>
</tr>
</table>
<!-- markdownlint-restore -->
<!-- prettier-ignore-end -->
<!-- ALL-CONTRIBUTORS-LIST:END -->
이 프로젝트는 다음 사양을 따릅니다. [all-contributors](https://github.com/all-contributors/all-contributors).
모든 종류의 기여를 환영합니다!

View File

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

@ -1 +1 @@
Subproject commit de6d44c372a7f48ca0ce8c6c0c19311d4bc21646 Subproject commit 19f7a5829f4d23cbc694136e45d94482a59a475a

View File

@ -11,6 +11,7 @@ import {
Document, Document,
isDocument, isDocument,
RowResponse, RowResponse,
RowValue,
} from "@budibase/types" } from "@budibase/types"
import { getCouchInfo } from "./connections" import { getCouchInfo } from "./connections"
import { directCouchUrlCall } from "./utils" import { directCouchUrlCall } from "./utils"
@ -221,7 +222,7 @@ export class DatabaseImpl implements Database {
}) })
} }
async allDocs<T extends Document>( async allDocs<T extends Document | RowValue>(
params: DatabaseQueryOpts params: DatabaseQueryOpts
): Promise<AllDocsResponse<T>> { ): Promise<AllDocsResponse<T>> {
return this.performCall(db => { return this.performCall(db => {

View File

@ -1,5 +1,4 @@
import { import {
DocumentScope,
DocumentDestroyResponse, DocumentDestroyResponse,
DocumentInsertResponse, DocumentInsertResponse,
DocumentBulkResponse, DocumentBulkResponse,
@ -13,6 +12,7 @@ import {
DatabasePutOpts, DatabasePutOpts,
DatabaseQueryOpts, DatabaseQueryOpts,
Document, Document,
RowValue,
} from "@budibase/types" } from "@budibase/types"
import tracer from "dd-trace" import tracer from "dd-trace"
import { Writable } from "stream" import { Writable } from "stream"
@ -79,7 +79,7 @@ export class DDInstrumentedDatabase implements Database {
}) })
} }
allDocs<T extends Document>( allDocs<T extends Document | RowValue>(
params: DatabaseQueryOpts params: DatabaseQueryOpts
): Promise<AllDocsResponse<T>> { ): Promise<AllDocsResponse<T>> {
return tracer.trace("db.allDocs", span => { return tracer.trace("db.allDocs", span => {

View File

@ -74,7 +74,7 @@ export function getGlobalIDFromUserMetadataID(id: string) {
* Generates a template ID. * Generates a template ID.
* @param ownerId The owner/user of the template, this could be global or a workspace level. * @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()}` 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. * 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. * @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}` return `${DocumentType.DEV_INFO}${SEPARATOR}${userId}`
} }

View File

@ -116,7 +116,6 @@
$: pagerText = `Page ${currentPage} of ${totalPages}` $: pagerText = `Page ${currentPage} of ${totalPages}`
</script> </script>
a11y-click-events-have-key-events
<div bind:this={buttonAnchor}> <div bind:this={buttonAnchor}>
<ActionButton on:click={dropdown.show}> <ActionButton on:click={dropdown.show}>
{displayValue} {displayValue}

View File

@ -69,11 +69,12 @@
// brought back to the same screen. // brought back to the same screen.
const topItemNavigate = path => () => { const topItemNavigate = path => () => {
const activeTopNav = $layout.children.find(c => $isActive(c.path)) const activeTopNav = $layout.children.find(c => $isActive(c.path))
if (!activeTopNav) return if (activeTopNav) {
builderStore.setPreviousTopNavPath( builderStore.setPreviousTopNavPath(
activeTopNav.path, activeTopNav.path,
window.location.pathname window.location.pathname
) )
}
$goto($builderStore.previousTopNavPath[path] || path) $goto($builderStore.previousTopNavPath[path] || path)
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -20,6 +20,7 @@ import {
AutomationActionStepId, AutomationActionStepId,
AutomationResults, AutomationResults,
UserCtx, UserCtx,
DeleteAutomationResponse,
} from "@budibase/types" } from "@budibase/types"
import { getActionDefinitions as actionDefs } from "../../automations/actions" import { getActionDefinitions as actionDefs } from "../../automations/actions"
import sdk from "../../sdk" import sdk from "../../sdk"
@ -72,7 +73,9 @@ function cleanAutomationInputs(automation: Automation) {
return automation return automation
} }
export async function create(ctx: UserCtx) { export async function create(
ctx: UserCtx<Automation, { message: string; automation: Automation }>
) {
const db = context.getAppDB() const db = context.getAppDB()
let automation = ctx.request.body let automation = ctx.request.body
automation.appId = ctx.appId automation.appId = ctx.appId
@ -207,7 +210,7 @@ export async function find(ctx: UserCtx) {
ctx.body = await db.get(ctx.params.id) ctx.body = await db.get(ctx.params.id)
} }
export async function destroy(ctx: UserCtx) { export async function destroy(ctx: UserCtx<void, DeleteAutomationResponse>) {
const db = context.getAppDB() const db = context.getAppDB()
const automationId = ctx.params.id const automationId = ctx.params.id
const oldAutomation = await db.get<Automation>(automationId) const oldAutomation = await db.get<Automation>(automationId)

View File

@ -15,10 +15,14 @@ import {
FieldType, FieldType,
RelationshipFieldMetadata, RelationshipFieldMetadata,
SourceName, SourceName,
UpdateDatasourceRequest,
UpdateDatasourceResponse, UpdateDatasourceResponse,
UserCtx, UserCtx,
VerifyDatasourceRequest, VerifyDatasourceRequest,
VerifyDatasourceResponse, VerifyDatasourceResponse,
Table,
RowValue,
DynamicVariable,
} from "@budibase/types" } from "@budibase/types"
import sdk from "../../sdk" import sdk from "../../sdk"
import { builderSocket } from "../../websockets" import { builderSocket } from "../../websockets"
@ -90,8 +94,10 @@ async function invalidateVariables(
existingDatasource: Datasource, existingDatasource: Datasource,
updatedDatasource: Datasource updatedDatasource: Datasource
) { ) {
const existingVariables: any = existingDatasource.config?.dynamicVariables const existingVariables: DynamicVariable[] =
const updatedVariables: any = updatedDatasource.config?.dynamicVariables existingDatasource.config?.dynamicVariables || []
const updatedVariables: DynamicVariable[] =
updatedDatasource.config?.dynamicVariables || []
const toInvalidate = [] const toInvalidate = []
if (!existingVariables) { if (!existingVariables) {
@ -103,9 +109,9 @@ async function invalidateVariables(
toInvalidate.push(...existingVariables) toInvalidate.push(...existingVariables)
} else { } else {
// invaldate changed / removed // invaldate changed / removed
existingVariables.forEach((existing: any) => { existingVariables.forEach(existing => {
const unchanged = updatedVariables.find( const unchanged = updatedVariables.find(
(updated: any) => updated =>
existing.name === updated.name && existing.name === updated.name &&
existing.queryId === updated.queryId && existing.queryId === updated.queryId &&
existing.value === updated.value existing.value === updated.value
@ -118,24 +124,32 @@ async function invalidateVariables(
await invalidateDynamicVariables(toInvalidate) await invalidateDynamicVariables(toInvalidate)
} }
export async function update(ctx: UserCtx<any, UpdateDatasourceResponse>) { export async function update(
ctx: UserCtx<UpdateDatasourceRequest, UpdateDatasourceResponse>
) {
const db = context.getAppDB() const db = context.getAppDB()
const datasourceId = ctx.params.datasourceId const datasourceId = ctx.params.datasourceId
const baseDatasource = await sdk.datasources.get(datasourceId) const baseDatasource = await sdk.datasources.get(datasourceId)
const auth = baseDatasource.config?.auth
await invalidateVariables(baseDatasource, ctx.request.body) await invalidateVariables(baseDatasource, ctx.request.body)
const isBudibaseSource = const isBudibaseSource =
baseDatasource.type === dbCore.BUDIBASE_DATASOURCE_TYPE baseDatasource.type === dbCore.BUDIBASE_DATASOURCE_TYPE
const dataSourceBody = isBudibaseSource const dataSourceBody: Datasource = isBudibaseSource
? { name: ctx.request.body?.name } ? {
name: ctx.request.body?.name,
type: dbCore.BUDIBASE_DATASOURCE_TYPE,
source: SourceName.BUDIBASE,
}
: ctx.request.body : ctx.request.body
let datasource: Datasource = { let datasource: Datasource = {
...baseDatasource, ...baseDatasource,
...sdk.datasources.mergeConfigs(dataSourceBody, 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) { if (auth && !ctx.request.body.auth) {
// don't strip auth config from DB // don't strip auth config from DB
datasource.config!.auth = auth datasource.config!.auth = auth
@ -204,7 +218,7 @@ async function destroyInternalTablesBySourceId(datasourceId: string) {
const db = context.getAppDB() const db = context.getAppDB()
// Get all internal tables // Get all internal tables
const internalTables = await db.allDocs( const internalTables = await db.allDocs<Table>(
getTableParams(null, { getTableParams(null, {
include_docs: true, include_docs: true,
}) })
@ -212,8 +226,8 @@ async function destroyInternalTablesBySourceId(datasourceId: string) {
// Filter by datasource and return the docs. // Filter by datasource and return the docs.
const datasourceTableDocs = internalTables.rows.reduce( const datasourceTableDocs = internalTables.rows.reduce(
(acc: any, table: any) => { (acc: Table[], table) => {
if (table.doc.sourceId == datasourceId) { if (table.doc?.sourceId == datasourceId) {
acc.push(table.doc) acc.push(table.doc)
} }
return acc return acc
@ -254,9 +268,9 @@ export async function destroy(ctx: UserCtx) {
if (datasource.type === dbCore.BUDIBASE_DATASOURCE_TYPE) { if (datasource.type === dbCore.BUDIBASE_DATASOURCE_TYPE) {
await destroyInternalTablesBySourceId(datasourceId) await destroyInternalTablesBySourceId(datasourceId)
} else { } else {
const queries = await db.allDocs(getQueryParams(datasourceId)) const queries = await db.allDocs<RowValue>(getQueryParams(datasourceId))
await db.bulkDocs( await db.bulkDocs(
queries.rows.map((row: any) => ({ queries.rows.map(row => ({
_id: row.id, _id: row.id,
_rev: row.value.rev, _rev: row.value.rev,
_deleted: true, _deleted: true,

View File

@ -1,7 +1,10 @@
import { getDefinition, getDefinitions } from "../../integrations" import { getDefinition, getDefinitions } from "../../integrations"
import { SourceName, UserCtx } from "@budibase/types" 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) { export async function fetch(ctx: UserCtx) {
const definitions = await getDefinitions() const definitions = await getDefinitions()

View File

@ -1,9 +1,17 @@
import { EMPTY_LAYOUT } from "../../constants/layouts" import { EMPTY_LAYOUT } from "../../constants/layouts"
import { generateLayoutID, getScreenParams } from "../../db/utils" import { generateLayoutID, getScreenParams } from "../../db/utils"
import { events, context } from "@budibase/backend-core" 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<SaveLayoutRequest, SaveLayoutResponse>
) {
const db = context.getAppDB() const db = context.getAppDB()
let layout = ctx.request.body let layout = ctx.request.body

View File

@ -73,7 +73,7 @@ const _import = async (ctx: UserCtx) => {
} }
export { _import as import } export { _import as import }
export async function save(ctx: UserCtx) { export async function save(ctx: UserCtx<Query, Query>) {
const db = context.getAppDB() const db = context.getAppDB()
const query: Query = ctx.request.body const query: Query = ctx.request.body

View File

@ -7,6 +7,7 @@ import {
FilterType, FilterType,
IncludeRelationship, IncludeRelationship,
ManyToManyRelationshipFieldMetadata, ManyToManyRelationshipFieldMetadata,
ManyToOneRelationshipFieldMetadata,
OneToManyRelationshipFieldMetadata, OneToManyRelationshipFieldMetadata,
Operation, Operation,
PaginationJson, PaginationJson,
@ -18,6 +19,7 @@ import {
SortJson, SortJson,
SortType, SortType,
Table, Table,
isManyToOne,
} from "@budibase/types" } from "@budibase/types"
import { import {
breakExternalTableId, breakExternalTableId,
@ -32,7 +34,9 @@ import { processObjectSync } from "@budibase/string-templates"
import { cloneDeep } from "lodash/fp" import { cloneDeep } from "lodash/fp"
import { processDates, processFormulas } from "../../../utilities/rowProcessor" import { processDates, processFormulas } from "../../../utilities/rowProcessor"
import { db as dbCore } from "@budibase/backend-core" import { db as dbCore } from "@budibase/backend-core"
import AliasTables from "./alias"
import sdk from "../../../sdk" import sdk from "../../../sdk"
import env from "../../../environment"
export interface ManyRelationship { export interface ManyRelationship {
tableId?: string 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 * 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 * 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) { function getEndpoint(tableId: string | undefined, operation: string) {
if (!tableId) { if (!tableId) {
return {} throw new Error("Cannot get endpoint information - no table ID specified")
} }
const { datasourceId, tableName } = breakExternalTableId(tableId) const { datasourceId, tableName } = breakExternalTableId(tableId)
return { return {
datasourceId, datasourceId: datasourceId!,
entityId: tableName, entityId: tableName!,
operation, operation: operation as Operation,
} }
} }
@ -304,6 +341,18 @@ export class ExternalRequest<T extends Operation> {
} }
} }
async getRow(table: Table, rowId: string): Promise<Row> {
const response = await getDatasourceAndQuery({
endpoint: getEndpoint(table._id!, Operation.READ),
filters: buildFilters(rowId, {}, table),
})
if (Array.isArray(response) && response.length > 0) {
return response[0]
} else {
throw new Error(`Cannot fetch row by ID "${rowId}"`)
}
}
inputProcessing(row: Row | undefined, table: Table) { inputProcessing(row: Row | undefined, table: Table) {
if (!row) { if (!row) {
return { row, manyRelationships: [] } return { row, manyRelationships: [] }
@ -571,7 +620,9 @@ export class ExternalRequest<T extends Operation> {
* information. * information.
*/ */
async lookupRelations(tableId: string, row: Row) { 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) const { tableName } = breakExternalTableId(tableId)
if (!tableName) { if (!tableName) {
return related return related
@ -589,14 +640,26 @@ export class ExternalRequest<T extends Operation> {
) { ) {
continue continue
} }
const isMany = field.relationshipType === RelationshipType.MANY_TO_MANY let tableId: string | undefined,
const tableId = isMany ? field.through : field.tableId 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) const { tableName: relatedTableName } = breakExternalTableId(tableId)
// @ts-ignore // @ts-ignore
const linkPrimaryKey = this.tables[relatedTableName].primary[0] 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]) { if (!lookupField || !row[lookupField]) {
continue continue
} }
@ -609,9 +672,12 @@ export class ExternalRequest<T extends Operation> {
}, },
}) })
// this is the response from knex if no rows found // this is the response from knex if no rows found
const rows = !response[0].read ? response : [] const rows: Row[] =
const storeTo = isMany ? field.throughFrom || linkPrimaryKey : fieldName !Array.isArray(response) || response?.[0].read ? [] : response
related[storeTo] = { rows, isMany, tableId } const storeTo = isManyToMany(field)
? field.throughFrom || linkPrimaryKey
: fieldName
related[storeTo] = { rows, isMany: isManyToMany(field), tableId }
} }
return related return related
} }
@ -697,24 +763,43 @@ export class ExternalRequest<T extends Operation> {
continue continue
} }
for (let row of rows) { for (let row of rows) {
const filters = buildFilters(generateIdForRow(row, table), {}, table) const rowId = generateIdForRow(row, table)
// safety check, if there are no filters on deletion bad things happen const promise: Promise<any> = isMany
if (Object.keys(filters).length !== 0) { ? removeManyToManyRelationships(rowId, table, colName)
const op = isMany ? Operation.DELETE : Operation.UPDATE : removeOneToManyRelationships(rowId, table)
const body = isMany ? null : { [colName]: null } if (promise) {
promises.push( promises.push(promise)
getDatasourceAndQuery({
endpoint: getEndpoint(tableId, op),
body,
filters,
})
)
} }
} }
} }
await Promise.all(promises) 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 * 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 * 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<T extends Operation> {
} }
let json = { let json = {
endpoint: { endpoint: {
datasourceId, datasourceId: datasourceId!,
entityId: tableName, entityId: tableName,
operation, operation,
}, },
@ -826,17 +911,30 @@ export class ExternalRequest<T extends Operation> {
}, },
} }
// can't really use response right now // remove any relationships that could block deletion
const response = await getDatasourceAndQuery(json) if (operation === Operation.DELETE && id) {
// handle many to many relationships now if we know the ID (could be auto increment) 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) { if (operation !== Operation.READ) {
await this.handleManyRelationships( await this.handleManyRelationships(
table._id || "", table._id || "",
response[0], responseRows[0],
processed.manyRelationships 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 reading it'll just be an array of rows, return whole thing
if (operation === Operation.READ) { if (operation === Operation.READ) {
return ( return (

View File

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

View File

@ -189,11 +189,12 @@ export async function fetchEnrichedRow(ctx: UserCtx) {
const tableId = utils.getTableId(ctx) const tableId = utils.getTableId(ctx)
const rowId = ctx.params.rowId as string const rowId = ctx.params.rowId as string
// need table to work out where links go in row, as well as the link docs // 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), sdk.tables.getTable(tableId),
utils.findRow(ctx, tableId, rowId),
linkRows.getLinkDocuments({ tableId, rowId, fieldName }), linkRows.getLinkDocuments({ tableId, rowId, fieldName }),
]) ])
let row = await utils.findRow(ctx, tableId, rowId)
row = await outputProcessing(table, row)
const linkVals = links as LinkDocumentValue[] const linkVals = links as LinkDocumentValue[]
// look up the actual rows based on the ids // look up the actual rows based on the ids

View File

@ -7,7 +7,13 @@ import {
roles, roles,
} from "@budibase/backend-core" } from "@budibase/backend-core"
import { updateAppPackage } from "./application" 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" import { builderSocket } from "../../websockets"
export async function fetch(ctx: BBContext) { 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<Screen, Screen>) {
const db = context.getAppDB() const db = context.getAppDB()
let screen = ctx.request.body let screen = ctx.request.body

View File

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

View File

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

View File

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

View File

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

View File

@ -394,7 +394,7 @@ describe("/automations", () => {
it("deletes a automation by its ID", async () => { it("deletes a automation by its ID", async () => {
const automation = await config.createAutomation() const automation = await config.createAutomation()
const res = await request const res = await request
.delete(`/api/automations/${automation.id}/${automation.rev}`) .delete(`/api/automations/${automation._id}/${automation._rev}`)
.set(config.defaultHeaders()) .set(config.defaultHeaders())
.expect("Content-Type", /json/) .expect("Content-Type", /json/)
.expect(200) .expect(200)
@ -408,7 +408,7 @@ describe("/automations", () => {
await checkBuilderEndpoint({ await checkBuilderEndpoint({
config, config,
method: "DELETE", method: "DELETE",
url: `/api/automations/${automation.id}/${automation._rev}`, url: `/api/automations/${automation._id}/${automation._rev}`,
}) })
}) })
}) })

View File

@ -44,7 +44,7 @@ describe("/backups", () => {
expect(headers["content-disposition"]).toEqual( expect(headers["content-disposition"]).toEqual(
`attachment; filename="${ `attachment; filename="${
config.getApp()!.name config.getApp().name
}-export-${mocks.date.MOCK_DATE.getTime()}.tar.gz"` }-export-${mocks.date.MOCK_DATE.getTime()}.tar.gz"`
) )
}) })

View File

@ -86,7 +86,7 @@ describe("/datasources", () => {
}) })
// check variables in cache // check variables in cache
let contents = await checkCacheForDynamicVariable( let contents = await checkCacheForDynamicVariable(
query._id, query._id!,
"variable3" "variable3"
) )
expect(contents.rows.length).toEqual(1) expect(contents.rows.length).toEqual(1)
@ -102,7 +102,7 @@ describe("/datasources", () => {
expect(res.body.errors).toBeUndefined() expect(res.body.errors).toBeUndefined()
// check variables no longer in cache // check variables no longer in cache
contents = await checkCacheForDynamicVariable(query._id, "variable3") contents = await checkCacheForDynamicVariable(query._id!, "variable3")
expect(contents).toBe(null) expect(contents).toBe(null)
}) })
}) })

View File

@ -467,7 +467,10 @@ describe("/queries", () => {
queryString: "test={{ variable3 }}", queryString: "test={{ variable3 }}",
}) })
// check its in cache // check its in cache
const contents = await checkCacheForDynamicVariable(base._id, "variable3") const contents = await checkCacheForDynamicVariable(
base._id!,
"variable3"
)
expect(contents.rows.length).toEqual(1) expect(contents.rows.length).toEqual(1)
const responseBody = await preview(datasource, { const responseBody = await preview(datasource, {
path: "www.failonce.com", path: "www.failonce.com",
@ -490,7 +493,7 @@ describe("/queries", () => {
queryString: "test={{ variable3 }}", queryString: "test={{ variable3 }}",
}) })
// check its in cache // check its in cache
let contents = await checkCacheForDynamicVariable(base._id, "variable3") let contents = await checkCacheForDynamicVariable(base._id!, "variable3")
expect(contents.rows.length).toEqual(1) expect(contents.rows.length).toEqual(1)
// delete the query // delete the query
@ -500,7 +503,7 @@ describe("/queries", () => {
.expect(200) .expect(200)
// check variables no longer in cache // check variables no longer in cache
contents = await checkCacheForDynamicVariable(base._id, "variable3") contents = await checkCacheForDynamicVariable(base._id!, "variable3")
expect(contents).toBe(null) expect(contents).toBe(null)
}) })
}) })

View File

@ -110,7 +110,7 @@ describe.each([
config.api.row.get(tbl_Id, id, { expectStatus: status }) config.api.row.get(tbl_Id, id, { expectStatus: status })
const getRowUsage = async () => { const getRowUsage = async () => {
const { total } = await config.doInContext(null, () => const { total } = await config.doInContext(undefined, () =>
quotas.getCurrentUsageValues(QuotaUsageType.STATIC, StaticQuotaName.ROWS) quotas.getCurrentUsageValues(QuotaUsageType.STATIC, StaticQuotaName.ROWS)
) )
return total return total

View File

@ -27,15 +27,17 @@ describe("/users", () => {
describe("fetch", () => { describe("fetch", () => {
it("returns a list of users from an instance db", async () => { it("returns a list of users from an instance db", async () => {
await config.createUser({ id: "uuidx" }) const id1 = `us_${utils.newid()}`
await config.createUser({ id: "uuidy" }) const id2 = `us_${utils.newid()}`
await config.createUser({ _id: id1 })
await config.createUser({ _id: id2 })
const res = await config.api.user.fetch() const res = await config.api.user.fetch()
expect(res.length).toBe(3) expect(res.length).toBe(3)
const ids = res.map(u => u._id) const ids = res.map(u => u._id)
expect(ids).toContain(`ro_ta_users_us_uuidx`) expect(ids).toContain(`ro_ta_users_${id1}`)
expect(ids).toContain(`ro_ta_users_us_uuidy`) expect(ids).toContain(`ro_ta_users_${id2}`)
}) })
it("should apply authorization to endpoint", async () => { it("should apply authorization to endpoint", async () => {
@ -54,7 +56,7 @@ describe("/users", () => {
describe("update", () => { describe("update", () => {
it("should be able to update the user", async () => { it("should be able to update the user", async () => {
const user: UserMetadata = await config.createUser({ const user: UserMetadata = await config.createUser({
id: `us_update${utils.newid()}`, _id: `us_update${utils.newid()}`,
}) })
user.roleId = roles.BUILTIN_ROLE_IDS.BASIC user.roleId = roles.BUILTIN_ROLE_IDS.BASIC
delete user._rev delete user._rev

View File

@ -4,6 +4,7 @@ import { AppStatus } from "../../../../db/utils"
import { roles, tenancy, context, db } from "@budibase/backend-core" import { roles, tenancy, context, db } from "@budibase/backend-core"
import env from "../../../../environment" import env from "../../../../environment"
import Nano from "@budibase/nano" import Nano from "@budibase/nano"
import TestConfiguration from "src/tests/utilities/TestConfiguration"
class Request { class Request {
appId: any 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() const automations = await config.getAllAutomations()
for (let auto of automations) { for (let auto of automations) {
await context.doInAppContext(config.appId, async () => { await context.doInAppContext(config.getAppId(), async () => {
await config.deleteAutomation(auto) await config.deleteAutomation(auto)
}) })
} }
@ -101,7 +102,12 @@ export const checkBuilderEndpoint = async ({
method, method,
url, url,
body, body,
}: any) => { }: {
config: TestConfiguration
method: string
url: string
body?: any
}) => {
const headers = await config.login({ const headers = await config.login({
userId: "us_fail", userId: "us_fail",
builder: false, builder: false,

View File

@ -36,7 +36,7 @@ describe("/webhooks", () => {
const automation = await config.createAutomation() const automation = await config.createAutomation()
const res = await request const res = await request
.put(`/api/webhooks`) .put(`/api/webhooks`)
.send(basicWebhook(automation._id)) .send(basicWebhook(automation._id!))
.set(config.defaultHeaders()) .set(config.defaultHeaders())
.expect("Content-Type", /json/) .expect("Content-Type", /json/)
.expect(200) .expect(200)
@ -145,7 +145,7 @@ describe("/webhooks", () => {
let automation = collectAutomation() let automation = collectAutomation()
let newAutomation = await config.createAutomation(automation) let newAutomation = await config.createAutomation(automation)
let syncWebhook = await config.createWebhook( let syncWebhook = await config.createWebhook(
basicWebhook(newAutomation._id) basicWebhook(newAutomation._id!)
) )
// replicate changes before checking webhook // replicate changes before checking webhook

View File

@ -29,6 +29,6 @@ start().catch(err => {
throw err throw err
}) })
export function getServer() { export function getServer(): Server {
return server return server
} }

View File

@ -1,9 +1,11 @@
import { Layout } from "@budibase/types"
export const BASE_LAYOUT_PROP_IDS = { export const BASE_LAYOUT_PROP_IDS = {
PRIVATE: "layout_private_master", PRIVATE: "layout_private_master",
PUBLIC: "layout_public_master", PUBLIC: "layout_public_master",
} }
export const EMPTY_LAYOUT = { export const EMPTY_LAYOUT: Layout = {
componentLibraries: ["@budibase/standard-components"], componentLibraries: ["@budibase/standard-components"],
title: "{{ name }}", title: "{{ name }}",
favicon: "./_shared/favicon.png", favicon: "./_shared/favicon.png",

View File

@ -1,5 +1,6 @@
import { roles } from "@budibase/backend-core" import { roles } from "@budibase/backend-core"
import { BASE_LAYOUT_PROP_IDS } from "./layouts" import { BASE_LAYOUT_PROP_IDS } from "./layouts"
import { Screen } from "@budibase/types"
export function createHomeScreen( export function createHomeScreen(
config: { config: {
@ -9,10 +10,8 @@ export function createHomeScreen(
roleId: roles.BUILTIN_ROLE_IDS.BASIC, roleId: roles.BUILTIN_ROLE_IDS.BASIC,
route: "/", route: "/",
} }
) { ): Screen {
return { return {
description: "",
url: "",
layoutId: BASE_LAYOUT_PROP_IDS.PRIVATE, layoutId: BASE_LAYOUT_PROP_IDS.PRIVATE,
props: { props: {
_id: "d834fea2-1b3e-4320-ab34-f9009f5ecc59", _id: "d834fea2-1b3e-4320-ab34-f9009f5ecc59",

View File

@ -1,8 +1,8 @@
import { import {
DEFAULT_BB_DATASOURCE_ID, DEFAULT_BB_DATASOURCE_ID,
DEFAULT_INVENTORY_TABLE_ID,
DEFAULT_EMPLOYEE_TABLE_ID, DEFAULT_EMPLOYEE_TABLE_ID,
DEFAULT_EXPENSES_TABLE_ID, DEFAULT_EXPENSES_TABLE_ID,
DEFAULT_INVENTORY_TABLE_ID,
DEFAULT_JOBS_TABLE_ID, DEFAULT_JOBS_TABLE_ID,
} from "../../constants" } from "../../constants"
import { importToRows } from "../../api/controllers/table/utils" import { importToRows } from "../../api/controllers/table/utils"
@ -15,19 +15,21 @@ import { expensesImport } from "./expensesImport"
import { db as dbCore } from "@budibase/backend-core" import { db as dbCore } from "@budibase/backend-core"
import { import {
AutoFieldSubType, AutoFieldSubType,
Datasource,
FieldType, FieldType,
RelationshipType, RelationshipType,
Row, Row,
SourceName,
Table, Table,
TableSchema, TableSchema,
TableSourceType, TableSourceType,
} from "@budibase/types" } from "@budibase/types"
const defaultDatasource = { const defaultDatasource: Datasource = {
_id: DEFAULT_BB_DATASOURCE_ID, _id: DEFAULT_BB_DATASOURCE_ID,
type: dbCore.BUDIBASE_DATASOURCE_TYPE, type: dbCore.BUDIBASE_DATASOURCE_TYPE,
name: "Sample Data", name: "Sample Data",
source: "BUDIBASE", source: SourceName.BUDIBASE,
config: {}, config: {},
} }

View File

@ -1,13 +1,15 @@
import newid from "./newid" import newid from "./newid"
import { db as dbCore } from "@budibase/backend-core" import { db as dbCore } from "@budibase/backend-core"
import { import {
FieldType, DatabaseQueryOpts,
Datasource,
DocumentType, DocumentType,
FieldSchema, FieldSchema,
RelationshipFieldMetadata, FieldType,
VirtualDocumentType,
INTERNAL_TABLE_SOURCE_ID, INTERNAL_TABLE_SOURCE_ID,
DatabaseQueryOpts, RelationshipFieldMetadata,
SourceName,
VirtualDocumentType,
} from "@budibase/types" } from "@budibase/types"
export { DocumentType, VirtualDocumentType } from "@budibase/types" export { DocumentType, VirtualDocumentType } from "@budibase/types"
@ -20,11 +22,11 @@ export const enum AppStatus {
DEPLOYED = "published", DEPLOYED = "published",
} }
export const BudibaseInternalDB = { export const BudibaseInternalDB: Datasource = {
_id: INTERNAL_TABLE_SOURCE_ID, _id: INTERNAL_TABLE_SOURCE_ID,
type: dbCore.BUDIBASE_DATASOURCE_TYPE, type: dbCore.BUDIBASE_DATASOURCE_TYPE,
name: "Budibase DB", name: "Budibase DB",
source: "BUDIBASE", source: SourceName.BUDIBASE,
config: {}, config: {},
} }

View File

@ -76,13 +76,16 @@ const environment = {
DEFAULTS.AUTOMATION_THREAD_TIMEOUT > QUERY_THREAD_TIMEOUT DEFAULTS.AUTOMATION_THREAD_TIMEOUT > QUERY_THREAD_TIMEOUT
? DEFAULTS.AUTOMATION_THREAD_TIMEOUT ? DEFAULTS.AUTOMATION_THREAD_TIMEOUT
: QUERY_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_EMAIL: process.env.BB_ADMIN_USER_EMAIL,
BB_ADMIN_USER_PASSWORD: process.env.BB_ADMIN_USER_PASSWORD, BB_ADMIN_USER_PASSWORD: process.env.BB_ADMIN_USER_PASSWORD,
PLUGINS_DIR: process.env.PLUGINS_DIR || DEFAULTS.PLUGINS_DIR, PLUGINS_DIR: process.env.PLUGINS_DIR || DEFAULTS.PLUGINS_DIR,
OPENAI_API_KEY: process.env.OPENAI_API_KEY, OPENAI_API_KEY: process.env.OPENAI_API_KEY,
MAX_IMPORT_SIZE_MB: process.env.MAX_IMPORT_SIZE_MB, MAX_IMPORT_SIZE_MB: process.env.MAX_IMPORT_SIZE_MB,
SESSION_EXPIRY_SECONDS: process.env.SESSION_EXPIRY_SECONDS, 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 // flags
ALLOW_DEV_AUTOMATIONS: process.env.ALLOW_DEV_AUTOMATIONS, ALLOW_DEV_AUTOMATIONS: process.env.ALLOW_DEV_AUTOMATIONS,
DISABLE_THREADING: process.env.DISABLE_THREADING, DISABLE_THREADING: process.env.DISABLE_THREADING,

View File

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

View File

@ -17,7 +17,6 @@ const envLimit = environment.SQL_MAX_ROWS
: null : null
const BASE_LIMIT = envLimit || 5000 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 // 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 MIN_ISO_DATE = "0000-00-00T00:00:00.000Z"
const MAX_ISO_DATE = "9999-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 // right now we only do filters on the specific table being queried
addFilters( addFilters(
query: KnexQuery, query: Knex.QueryBuilder,
filters: SearchFilters | undefined, filters: SearchFilters | undefined,
opts: { relationship?: boolean; tableName?: string } tableName: string,
): KnexQuery { opts: { aliases?: Record<string, string>; relationship?: boolean }
): Knex.QueryBuilder {
function getTableName(name: string) {
const alias = opts.aliases?.[name]
return alias || name
}
function iterate( function iterate(
structure: { [key: string]: any }, structure: { [key: string]: any },
fn: (key: string, value: any) => void fn: (key: string, value: any) => void
@ -139,10 +143,11 @@ class InternalBuilder {
const updatedKey = dbCore.removeKeyNumbering(key) const updatedKey = dbCore.removeKeyNumbering(key)
const isRelationshipField = updatedKey.includes(".") const isRelationshipField = updatedKey.includes(".")
if (!opts.relationship && !isRelationshipField) { if (!opts.relationship && !isRelationshipField) {
fn(`${opts.tableName}.${updatedKey}`, value) fn(`${getTableName(tableName)}.${updatedKey}`, value)
} }
if (opts.relationship && isRelationshipField) { 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 return query
} }
addSorting(query: KnexQuery, json: QueryJson): KnexQuery { addSorting(query: Knex.QueryBuilder, json: QueryJson): Knex.QueryBuilder {
let { sort, paginate } = json let { sort, paginate } = json
const table = json.meta?.table const table = json.meta?.table
if (sort && Object.keys(sort || {}).length > 0) { if (sort && Object.keys(sort || {}).length > 0) {
@ -330,16 +335,28 @@ class InternalBuilder {
return query 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( addRelationships(
query: KnexQuery, query: Knex.QueryBuilder,
fromTable: string, fromTable: string,
relationships: RelationshipsJson[] | undefined, relationships: RelationshipsJson[] | undefined,
schema: string | undefined schema: string | undefined,
): KnexQuery { aliases?: Record<string, string>
): Knex.QueryBuilder {
if (!relationships) { if (!relationships) {
return query return query
} }
const tableSets: Record<string, [any]> = {} const tableSets: Record<string, [RelationshipsJson]> = {}
// aggregate into table sets (all the same to tables) // aggregate into table sets (all the same to tables)
for (let relationship of relationships) { for (let relationship of relationships) {
const keyObj: { toTable: string; throughTable: string | undefined } = { const keyObj: { toTable: string; throughTable: string | undefined } = {
@ -358,10 +375,17 @@ class InternalBuilder {
} }
for (let [key, relationships] of Object.entries(tableSets)) { for (let [key, relationships] of Object.entries(tableSets)) {
const { toTable, throughTable } = JSON.parse(key) const { toTable, throughTable } = JSON.parse(key)
const toTableWithSchema = schema ? `${schema}.${toTable}` : toTable const toAlias = aliases?.[toTable] || toTable,
const throughTableWithSchema = schema throughAlias = aliases?.[throughTable] || throughTable,
? `${schema}.${throughTable}` fromAlias = aliases?.[fromTable] || fromTable
: throughTable let toTableWithSchema = this.tableNameWithSchema(toTable, {
alias: toAlias,
schema,
})
let throughTableWithSchema = this.tableNameWithSchema(throughTable, {
alias: throughAlias,
schema,
})
if (!throughTable) { if (!throughTable) {
// @ts-ignore // @ts-ignore
query = query.leftJoin(toTableWithSchema, function () { query = query.leftJoin(toTableWithSchema, function () {
@ -369,7 +393,7 @@ class InternalBuilder {
const from = relationship.from, const from = relationship.from,
to = relationship.to to = relationship.to
// @ts-ignore // @ts-ignore
this.orOn(`${fromTable}.${from}`, "=", `${toTable}.${to}`) this.orOn(`${fromAlias}.${from}`, "=", `${toAlias}.${to}`)
} }
}) })
} else { } else {
@ -381,9 +405,9 @@ class InternalBuilder {
const from = relationship.from const from = relationship.from
// @ts-ignore // @ts-ignore
this.orOn( this.orOn(
`${fromTable}.${fromPrimary}`, `${fromAlias}.${fromPrimary}`,
"=", "=",
`${throughTable}.${from}` `${throughAlias}.${from}`
) )
} }
}) })
@ -392,7 +416,7 @@ class InternalBuilder {
const toPrimary = relationship.toPrimary const toPrimary = relationship.toPrimary
const to = relationship.to const to = relationship.to
// @ts-ignore // @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) return query.limit(BASE_LIMIT)
} }
create(knex: Knex, json: QueryJson, opts: QueryOptions): KnexQuery { knexWithAlias(
const { endpoint, body } = json knex: Knex,
let query: KnexQuery = knex(endpoint.entityId) 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) { if (endpoint.schema) {
query = query.withSchema(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) const parsedBody = parseBody(body)
// make sure no null values in body for creation // make sure no null values in body for creation
for (let [key, value] of Object.entries(parsedBody)) { 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 const { endpoint, body } = json
let query: KnexQuery = knex(endpoint.entityId) let query = this.knexWithAlias(knex, endpoint)
if (endpoint.schema) {
query = query.withSchema(endpoint.schema)
}
if (!Array.isArray(body)) { if (!Array.isArray(body)) {
return query return query
} }
@ -435,8 +469,10 @@ class InternalBuilder {
return query.insert(parsedBody) return query.insert(parsedBody)
} }
read(knex: Knex, json: QueryJson, limit: number): KnexQuery { read(knex: Knex, json: QueryJson, limit: number): Knex.QueryBuilder {
let { endpoint, resource, filters, paginate, relationships } = json let { endpoint, resource, filters, paginate, relationships, tableAliases } =
json
const tableName = endpoint.entityId const tableName = endpoint.entityId
// select all if not specified // select all if not specified
if (!resource) { if (!resource) {
@ -462,21 +498,20 @@ class InternalBuilder {
foundLimit = paginate.limit foundLimit = paginate.limit
} }
// start building the query // start building the query
let query: KnexQuery = knex(tableName).limit(foundLimit) let query = this.knexWithAlias(knex, endpoint, tableAliases)
if (endpoint.schema) { query = query.limit(foundLimit)
query = query.withSchema(endpoint.schema)
}
if (foundOffset) { if (foundOffset) {
query = query.offset(foundOffset) query = query.offset(foundOffset)
} }
query = this.addFilters(query, filters, { tableName }) query = this.addFilters(query, filters, tableName, {
aliases: tableAliases,
})
// add sorting to pre-query // add sorting to pre-query
query = this.addSorting(query, json) query = this.addSorting(query, json)
// @ts-ignore const alias = tableAliases?.[tableName] || tableName
let preQuery: KnexQuery = knex({ let preQuery = knex({
// @ts-ignore [alias]: query,
[tableName]: query, } as any).select(selectStatement) as any
}).select(selectStatement)
// have to add after as well (this breaks MS-SQL) // have to add after as well (this breaks MS-SQL)
if (this.client !== SqlClient.MS_SQL) { if (this.client !== SqlClient.MS_SQL) {
preQuery = this.addSorting(preQuery, json) preQuery = this.addSorting(preQuery, json)
@ -486,19 +521,22 @@ class InternalBuilder {
preQuery, preQuery,
tableName, tableName,
relationships, 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 { update(knex: Knex, json: QueryJson, opts: QueryOptions): Knex.QueryBuilder {
const { endpoint, body, filters } = json const { endpoint, body, filters, tableAliases } = json
let query: KnexQuery = knex(endpoint.entityId) let query = this.knexWithAlias(knex, endpoint, tableAliases)
if (endpoint.schema) {
query = query.withSchema(endpoint.schema)
}
const parsedBody = parseBody(body) 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 // mysql can't use returning
if (opts.disableReturning) { if (opts.disableReturning) {
return query.update(parsedBody) return query.update(parsedBody)
@ -507,13 +545,12 @@ class InternalBuilder {
} }
} }
delete(knex: Knex, json: QueryJson, opts: QueryOptions): KnexQuery { delete(knex: Knex, json: QueryJson, opts: QueryOptions): Knex.QueryBuilder {
const { endpoint, filters } = json const { endpoint, filters, tableAliases } = json
let query: KnexQuery = knex(endpoint.entityId) let query = this.knexWithAlias(knex, endpoint, tableAliases)
if (endpoint.schema) { query = this.addFilters(query, filters, endpoint.entityId, {
query = query.withSchema(endpoint.schema) aliases: tableAliases,
} })
query = this.addFilters(query, filters, { tableName: endpoint.entityId })
// mysql can't use returning // mysql can't use returning
if (opts.disableReturning) { if (opts.disableReturning) {
return query.delete() 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. * 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. * @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 sqlClient = this.getSqlClient()
const client = knex({ client: sqlClient }) const client = knex({ client: sqlClient })
let query let query: Knex.QueryBuilder
const builder = new InternalBuilder(sqlClient) const builder = new InternalBuilder(sqlClient)
switch (this._operation(json)) { switch (this._operation(json)) {
case Operation.CREATE: case Operation.CREATE:
@ -565,8 +602,6 @@ class SqlQueryBuilder extends SqlTableQueryBuilder {
default: default:
throw `Operation type is not supported by SQL query builder` throw `Operation type is not supported by SQL query builder`
} }
// @ts-ignore
return query.toSQL().toNative() return query.toSQL().toNative()
} }
@ -648,6 +683,18 @@ class SqlQueryBuilder extends SqlTableQueryBuilder {
} }
return results.length ? results : [{ [operation.toLowerCase()]: true }] 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 export default SqlQueryBuilder

View File

@ -9,7 +9,7 @@ import {
Table, Table,
FieldType, FieldType,
} from "@budibase/types" } from "@budibase/types"
import { breakExternalTableId } from "../utils" import { breakExternalTableId, SqlClient } from "../utils"
import SchemaBuilder = Knex.SchemaBuilder import SchemaBuilder = Knex.SchemaBuilder
import CreateTableBuilder = Knex.CreateTableBuilder import CreateTableBuilder = Knex.CreateTableBuilder
import { utils } from "@budibase/shared-core" import { utils } from "@budibase/shared-core"
@ -135,7 +135,8 @@ function generateSchema(
// need to check if any columns have been deleted // need to check if any columns have been deleted
if (oldTable) { if (oldTable) {
const deletedColumns = Object.entries(oldTable.schema).filter( 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]) => { deletedColumns.forEach(([key, column]) => {
if (renamed?.old === key || isIgnoredType(column.type)) { if (renamed?.old === key || isIgnoredType(column.type)) {
@ -197,13 +198,14 @@ class SqlTableQueryBuilder {
return json.endpoint.operation return json.endpoint.operation
} }
_tableQuery(json: QueryJson): any { _tableQuery(json: QueryJson): Knex.Sql | Knex.SqlNative {
let client = knex({ client: this.sqlClient }).schema let client = knex({ client: this.sqlClient }).schema
if (json?.endpoint?.schema) { let schemaName = json?.endpoint?.schema
client = client.withSchema(json.endpoint.schema) if (schemaName) {
client = client.withSchema(schemaName)
} }
let query let query: Knex.SchemaBuilder
if (!json.table || !json.meta || !json.meta.tables) { if (!json.table || !json.meta || !json.meta.tables) {
throw "Cannot execute without table being specified" throw "Cannot execute without table being specified"
} }
@ -215,6 +217,18 @@ class SqlTableQueryBuilder {
if (!json.meta || !json.meta.table) { if (!json.meta || !json.meta.table) {
throw "Must specify old table for update" 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( query = buildUpdateTable(
client, client,
json.table, json.table,

View File

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

View File

@ -37,6 +37,7 @@ const DEFINITIONS: Record<SourceName, Integration | undefined> = {
[SourceName.REDIS]: redis.schema, [SourceName.REDIS]: redis.schema,
[SourceName.SNOWFLAKE]: snowflake.schema, [SourceName.SNOWFLAKE]: snowflake.schema,
[SourceName.ORACLE]: undefined, [SourceName.ORACLE]: undefined,
[SourceName.BUDIBASE]: undefined,
} }
const INTEGRATIONS: Record<SourceName, any> = { const INTEGRATIONS: Record<SourceName, any> = {
@ -56,6 +57,7 @@ const INTEGRATIONS: Record<SourceName, any> = {
[SourceName.REDIS]: redis.integration, [SourceName.REDIS]: redis.integration,
[SourceName.SNOWFLAKE]: snowflake.integration, [SourceName.SNOWFLAKE]: snowflake.integration,
[SourceName.ORACLE]: undefined, [SourceName.ORACLE]: undefined,
[SourceName.BUDIBASE]: undefined,
} }
// optionally add oracle integration if the oracle binary can be installed // optionally add oracle integration if the oracle binary can be installed

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -13,7 +13,7 @@ describe("syncApps", () => {
afterAll(config.end) afterAll(config.end)
it("runs successfully", async () => { it("runs successfully", async () => {
return config.doInContext(null, async () => { return config.doInContext(undefined, async () => {
// create the usage quota doc and mock usages // create the usage quota doc and mock usages
await quotas.getQuotaUsage() await quotas.getQuotaUsage()
await quotas.setUsage(3, StaticQuotaName.APPS, QuotaUsageType.STATIC) await quotas.setUsage(3, StaticQuotaName.APPS, QuotaUsageType.STATIC)

View File

@ -12,8 +12,8 @@ describe("syncCreators", () => {
afterAll(config.end) afterAll(config.end)
it("syncs creators", async () => { it("syncs creators", async () => {
return config.doInContext(null, async () => { return config.doInContext(undefined, async () => {
await config.createUser({ admin: true }) await config.createUser({ admin: { global: true } })
await syncCreators.run() await syncCreators.run()

View File

@ -14,7 +14,7 @@ describe("syncRows", () => {
afterAll(config.end) afterAll(config.end)
it("runs successfully", async () => { it("runs successfully", async () => {
return config.doInContext(null, async () => { return config.doInContext(undefined, async () => {
// create the usage quota doc and mock usages // create the usage quota doc and mock usages
await quotas.getQuotaUsage() await quotas.getQuotaUsage()
await quotas.setUsage(300, StaticQuotaName.ROWS, QuotaUsageType.STATIC) await quotas.setUsage(300, StaticQuotaName.ROWS, QuotaUsageType.STATIC)

View File

@ -12,7 +12,7 @@ describe("syncUsers", () => {
afterAll(config.end) afterAll(config.end)
it("syncs users", async () => { it("syncs users", async () => {
return config.doInContext(null, async () => { return config.doInContext(undefined, async () => {
await config.createUser() await config.createUser()
await syncUsers.run() await syncUsers.run()

View File

@ -40,7 +40,7 @@ describe("migrations", () => {
describe("backfill", () => { describe("backfill", () => {
it("runs app db migration", async () => { it("runs app db migration", async () => {
await config.doInContext(null, async () => { await config.doInContext(undefined, async () => {
await clearMigrations() await clearMigrations()
await config.createAutomation() await config.createAutomation()
await config.createAutomation(structures.newAutomation()) await config.createAutomation(structures.newAutomation())
@ -93,18 +93,18 @@ describe("migrations", () => {
}) })
it("runs global db migration", async () => { it("runs global db migration", async () => {
await config.doInContext(null, async () => { await config.doInContext(undefined, async () => {
await clearMigrations() await clearMigrations()
const appId = config.prodAppId const appId = config.getProdAppId()
const roles = { [appId]: "role_12345" } const roles = { [appId]: "role_12345" }
await config.createUser({ await config.createUser({
builder: false, builder: { global: false },
admin: true, admin: { global: true },
roles, roles,
}) // admin only }) // admin only
await config.createUser({ await config.createUser({
builder: false, builder: { global: false },
admin: false, admin: { global: false },
roles, roles,
}) // non admin non builder }) // non admin non builder
await config.createTable() await config.createTable()

View File

@ -85,7 +85,9 @@ async function getImportableDocuments(db: Database) {
const docPromises = [] const docPromises = []
for (let docType of DocumentTypesToImport) { for (let docType of DocumentTypesToImport) {
docPromises.push( docPromises.push(
db.allDocs(dbCore.getDocParams(docType, null, { include_docs: true })) db.allDocs<Document>(
dbCore.getDocParams(docType, null, { include_docs: true })
)
) )
} }
// map the responses to the document itself // map the responses to the document itself

View File

@ -43,8 +43,8 @@ async function createUser(email: string, roles: UserRoles, builder?: boolean) {
const user = await config.createUser({ const user = await config.createUser({
email, email,
roles, roles,
builder: builder || false, builder: { global: builder || false },
admin: false, admin: { global: false },
}) })
await context.doInContext(config.appId!, async () => { await context.doInContext(config.appId!, async () => {
await events.user.created(user) await events.user.created(user)
@ -55,10 +55,10 @@ async function createUser(email: string, roles: UserRoles, builder?: boolean) {
async function removeUserRole(user: User) { async function removeUserRole(user: User) {
const final = await config.globalUser({ const final = await config.globalUser({
...user, ...user,
id: user._id, _id: user._id,
roles: {}, roles: {},
builder: false, builder: { global: false },
admin: false, admin: { global: false },
}) })
await context.doInContext(config.appId!, async () => { await context.doInContext(config.appId!, async () => {
await events.user.updated(final) await events.user.updated(final)
@ -69,8 +69,8 @@ async function createGroupAndUser(email: string) {
groupUser = await config.createUser({ groupUser = await config.createUser({
email, email,
roles: {}, roles: {},
builder: false, builder: { global: false },
admin: false, admin: { global: false },
}) })
group = await config.createGroup() group = await config.createGroup()
await config.addUserToGroup(group._id!, groupUser._id!) await config.addUserToGroup(group._id!, groupUser._id!)

View File

@ -229,7 +229,7 @@ export async function removeSecretSingle(datasource: Datasource) {
} }
export function mergeConfigs(update: Datasource, old: Datasource) { export function mergeConfigs(update: Datasource, old: Datasource) {
if (!update.config) { if (!update.config || !old.config) {
return update return update
} }
// specific to REST datasources, fix the auth configs again if required // specific to REST datasources, fix the auth configs again if required

View File

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

View File

@ -81,7 +81,7 @@ describe("sdk >> rows >> internal", () => {
const response = await internalSdk.save( const response = await internalSdk.save(
table._id!, table._id!,
row, row,
config.user._id config.getUser()._id
) )
expect(response).toEqual({ expect(response).toEqual({
@ -129,7 +129,7 @@ describe("sdk >> rows >> internal", () => {
const response = await internalSdk.save( const response = await internalSdk.save(
table._id!, table._id!,
row, row,
config.user._id config.getUser()._id
) )
expect(response).toEqual({ expect(response).toEqual({
@ -190,15 +190,15 @@ describe("sdk >> rows >> internal", () => {
await config.doInContext(config.appId, async () => { await config.doInContext(config.appId, async () => {
for (const row of makeRows(5)) { 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( await Promise.all(
makeRows(10).map(row => 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)) { for (const row of makeRows(5)) {
await internalSdk.save(table._id!, row, config.user._id) await internalSdk.save(table._id!, row, config.getUser()._id)
} }
}) })

View File

@ -1,12 +1,21 @@
import cloneDeep from "lodash/cloneDeep" import cloneDeep from "lodash/cloneDeep"
import validateJs from "validate.js" 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 { makeExternalQuery } from "../../../integrations/base/query"
import { Format } from "../../../api/controllers/view/exporters" import { Format } from "../../../api/controllers/view/exporters"
import sdk from "../.." import sdk from "../.."
import { isRelationshipColumn } from "../../../db/utils" import { isRelationshipColumn } from "../../../db/utils"
export async function getDatasourceAndQuery(json: any) { export async function getDatasourceAndQuery(
json: QueryJson
): DatasourcePlusQueryResponse {
const datasourceId = json.endpoint.datasourceId const datasourceId = json.endpoint.datasourceId
const datasource = await sdk.datasources.get(datasourceId) const datasource = await sdk.datasources.get(datasourceId)
return makeExternalQuery(datasource, json) return makeExternalQuery(datasource, json)

View File

@ -22,15 +22,18 @@ describe("syncGlobalUsers", () => {
expect(metadata).toHaveLength(1) expect(metadata).toHaveLength(1)
expect(metadata).toEqual([ expect(metadata).toEqual([
expect.objectContaining({ expect.objectContaining({
_id: db.generateUserMetadataID(config.user._id), _id: db.generateUserMetadataID(config.getUser()._id!),
}), }),
]) ])
}) })
}) })
it("admin and builders users are synced", async () => { it("admin and builders users are synced", async () => {
const user1 = await config.createUser({ admin: true }) const user1 = await config.createUser({ admin: { global: true } })
const user2 = await config.createUser({ admin: false, builder: true }) const user2 = await config.createUser({
admin: { global: false },
builder: { global: true },
})
await config.doInContext(config.appId, async () => { await config.doInContext(config.appId, async () => {
expect(await rawUserMetadata()).toHaveLength(1) expect(await rawUserMetadata()).toHaveLength(1)
await syncGlobalUsers() await syncGlobalUsers()
@ -51,7 +54,10 @@ describe("syncGlobalUsers", () => {
}) })
it("app users are not synced if not specified", async () => { 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 config.doInContext(config.appId, async () => {
await syncGlobalUsers() await syncGlobalUsers()
@ -68,8 +74,14 @@ describe("syncGlobalUsers", () => {
it("app users are added when group is assigned to app", async () => { it("app users are added when group is assigned to app", async () => {
await config.doInTenant(async () => { await config.doInTenant(async () => {
const group = await proSdk.groups.save(structures.userGroups.userGroup()) const group = await proSdk.groups.save(structures.userGroups.userGroup())
const user1 = await config.createUser({ admin: false, builder: false }) const user1 = await config.createUser({
const user2 = await config.createUser({ admin: false, builder: false }) 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 proSdk.groups.addUsers(group.id, [user1._id!, user2._id!])
await config.doInContext(config.appId, async () => { 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 () => { it("app users are removed when app is removed from user group", async () => {
await config.doInTenant(async () => { await config.doInTenant(async () => {
const group = await proSdk.groups.save(structures.userGroups.userGroup()) const group = await proSdk.groups.save(structures.userGroups.userGroup())
const user1 = await config.createUser({ admin: false, builder: false }) const user1 = await config.createUser({
const user2 = await config.createUser({ admin: false, builder: false }) admin: { global: false },
builder: { global: false },
})
const user2 = await config.createUser({
admin: { global: false },
builder: { global: false },
})
await proSdk.groups.updateGroupApps(group.id, { await proSdk.groups.updateGroupApps(group.id, {
appsToAdd: [ appsToAdd: [
{ appId: config.prodAppId!, roleId: roles.BUILTIN_ROLE_IDS.BASIC }, { appId: config.prodAppId!, roleId: roles.BUILTIN_ROLE_IDS.BASIC },

View File

@ -38,6 +38,7 @@ async function initRoutes(app: Koa) {
// api routes // api routes
app.use(api.router.routes()) app.use(api.router.routes())
app.use(api.router.allowedMethods())
} }
async function initPro() { async function initPro() {

View File

@ -49,25 +49,31 @@ import {
AuthToken, AuthToken,
Automation, Automation,
CreateViewRequest, CreateViewRequest,
Ctx,
Datasource, Datasource,
FieldType, FieldType,
INTERNAL_TABLE_SOURCE_ID, INTERNAL_TABLE_SOURCE_ID,
Layout,
Query,
RelationshipFieldMetadata, RelationshipFieldMetadata,
RelationshipType, RelationshipType,
Row, Row,
Screen,
SearchParams, SearchParams,
SourceName, SourceName,
Table, Table,
TableSourceType, TableSourceType,
User, User,
UserRoles, UserCtx,
View, View,
Webhook,
WithRequired, WithRequired,
} from "@budibase/types" } from "@budibase/types"
import API from "./api" import API from "./api"
import { cloneDeep } from "lodash" import { cloneDeep } from "lodash"
import jwt, { Secret } from "jsonwebtoken" import jwt, { Secret } from "jsonwebtoken"
import { Server } from "http"
mocks.licenses.init(pro) mocks.licenses.init(pro)
@ -82,27 +88,23 @@ export interface TableToBuild extends Omit<Table, "sourceId" | "sourceType"> {
} }
export default class TestConfiguration { export default class TestConfiguration {
server: any server?: Server
request: supertest.SuperTest<supertest.Test> | undefined request?: supertest.SuperTest<supertest.Test>
started: boolean started: boolean
appId: string | null appId?: string
allApps: any[] allApps: App[]
app?: App app?: App
prodApp: any prodApp?: App
prodAppId: any prodAppId?: string
user: any user?: User
userMetadataId: any userMetadataId?: string
table?: Table table?: Table
automation: any automation?: Automation
datasource?: Datasource datasource?: Datasource
tenantId?: string tenantId?: string
api: API api: API
csrfToken?: string csrfToken?: string
private get globalUserId() {
return this.user._id
}
constructor(openServer = true) { constructor(openServer = true) {
if (openServer) { if (openServer) {
// use a random port because it doesn't matter // use a random port because it doesn't matter
@ -114,7 +116,7 @@ export default class TestConfiguration {
} else { } else {
this.started = false this.started = false
} }
this.appId = null this.appId = undefined
this.allApps = [] this.allApps = []
this.api = new API(this) this.api = new API(this)
@ -125,46 +127,86 @@ export default class TestConfiguration {
} }
getApp() { getApp() {
if (!this.app) {
throw new Error("app has not been initialised, call config.init() first")
}
return this.app return this.app
} }
getProdApp() { getProdApp() {
if (!this.prodApp) {
throw new Error(
"prodApp has not been initialised, call config.init() first"
)
}
return this.prodApp return this.prodApp
} }
getAppId() { getAppId() {
if (!this.appId) { 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 return this.appId
} }
getProdAppId() { getProdAppId() {
if (!this.prodAppId) {
throw new Error(
"prodAppId has not been initialised, call config.init() first"
)
}
return this.prodAppId return this.prodAppId
} }
getUser(): User {
if (!this.user) {
throw new Error("User has not been initialised, call config.init() first")
}
return this.user
}
getUserDetails() { getUserDetails() {
const user = this.getUser()
return { return {
globalId: this.globalUserId, globalId: user._id!,
email: this.user.email, email: user.email,
firstName: this.user.firstName, firstName: user.firstName,
lastName: this.user.lastName, 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<T>( async doInContext<T>(
appId: string | null, appId: string | undefined,
task: () => Promise<T> task: () => Promise<T>
): Promise<T> { ): Promise<T> {
if (!appId) {
appId = this.appId
}
const tenant = this.getTenantId() const tenant = this.getTenantId()
return tenancy.doInTenant(tenant, () => { return tenancy.doInTenant(tenant, () => {
if (!appId) {
appId = this.appId
}
// check if already in a context // check if already in a context
if (context.getAppId() == null && appId !== null) { if (context.getAppId() == null && appId) {
return context.doInAppContext(appId, async () => { return context.doInAppContext(appId, async () => {
return task() return task()
}) })
@ -259,7 +301,11 @@ export default class TestConfiguration {
// UTILS // UTILS
_req(body: any, params: any, controlFunc: any) { _req<Req extends Record<string, any> | void, Res>(
handler: (ctx: UserCtx<Req, Res>) => Promise<void>,
body?: Req,
params?: Record<string, string | undefined>
): Promise<Res> {
// create a fake request ctx // create a fake request ctx
const request: any = {} const request: any = {}
const appId = this.appId const appId = this.appId
@ -278,63 +324,48 @@ export default class TestConfiguration {
throw new Error(`Error ${status} - ${message}`) throw new Error(`Error ${status} - ${message}`)
} }
return this.doInContext(appId, async () => { return this.doInContext(appId, async () => {
await controlFunc(request) await handler(request)
return request.body return request.body
}) })
} }
// USER / AUTH // USER / AUTH
async globalUser( async globalUser(config: Partial<User> = {}): Promise<User> {
config: {
id?: string
firstName?: string
lastName?: string
builder?: boolean
admin?: boolean
email?: string
roles?: any
} = {}
): Promise<User> {
const { const {
id = `us_${newid()}`, _id = `us_${newid()}`,
firstName = generator.first(), firstName = generator.first(),
lastName = generator.last(), lastName = generator.last(),
builder = true, builder = { global: true },
admin = false, admin = { global: false },
email = generator.email(), email = generator.email(),
roles, tenantId = this.getTenantId(),
roles = {},
} = config } = config
const db = tenancy.getTenantDB(this.getTenantId()) const db = tenancy.getTenantDB(this.getTenantId())
let existing let existing: Partial<User> = {}
try { try {
existing = await db.get<any>(id) existing = await db.get<User>(_id)
} catch (err) { } catch (err) {
existing = { email } // ignore
} }
const user: User = { const user: User = {
_id: id, _id,
...existing, ...existing,
roles: roles || {}, ...config,
tenantId: this.getTenantId(), email,
roles,
tenantId,
firstName, firstName,
lastName, lastName,
builder,
admin,
} }
await sessions.createASession(id, { await sessions.createASession(_id, {
sessionId: "sessionid", sessionId: "sessionid",
tenantId: this.getTenantId(), tenantId: this.getTenantId(),
csrfToken: this.csrfToken, 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) const resp = await db.put(user)
return { return {
_rev: resp.rev, _rev: resp.rev,
@ -342,38 +373,9 @@ export default class TestConfiguration {
} }
} }
async createUser( async createUser(user: Partial<User> = {}): Promise<User> {
user: { const resp = await this.globalUser(user)
id?: string await cache.user.invalidateUser(resp._id!)
firstName?: string
lastName?: string
email?: string
builder?: boolean
admin?: boolean
roles?: UserRoles
} = {}
): Promise<User> {
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)
return resp return resp
} }
@ -381,7 +383,7 @@ export default class TestConfiguration {
return context.doInTenant(this.tenantId!, async () => { return context.doInTenant(this.tenantId!, async () => {
const baseGroup = structures.userGroups.userGroup() const baseGroup = structures.userGroups.userGroup()
baseGroup.roles = { baseGroup.roles = {
[this.prodAppId]: roleId, [this.getProdAppId()]: roleId,
} }
const { id, rev } = await pro.sdk.groups.save(baseGroup) const { id, rev } = await pro.sdk.groups.save(baseGroup)
return { return {
@ -404,8 +406,18 @@ export default class TestConfiguration {
}) })
} }
async login({ roleId, userId, builder, prodApp = false }: any = {}) { async login({
const appId = prodApp ? this.prodAppId : this.appId roleId,
userId,
builder,
prodApp,
}: {
roleId?: string
userId: string
builder: boolean
prodApp: boolean
}) {
const appId = prodApp ? this.getProdAppId() : this.getAppId()
return context.doInAppContext(appId, async () => { return context.doInAppContext(appId, async () => {
userId = !userId ? `us_uuid1` : userId userId = !userId ? `us_uuid1` : userId
if (!this.request) { if (!this.request) {
@ -414,9 +426,9 @@ export default class TestConfiguration {
// make sure the user exists in the global DB // make sure the user exists in the global DB
if (roleId !== roles.BUILTIN_ROLE_IDS.PUBLIC) { if (roleId !== roles.BUILTIN_ROLE_IDS.PUBLIC) {
await this.globalUser({ await this.globalUser({
id: userId, _id: userId,
builder, builder: { global: builder },
roles: { [this.prodAppId]: roleId }, roles: { [appId]: roleId || roles.BUILTIN_ROLE_IDS.BASIC },
}) })
} }
await sessions.createASession(userId, { await sessions.createASession(userId, {
@ -445,8 +457,9 @@ export default class TestConfiguration {
defaultHeaders(extras = {}, prodApp = false) { defaultHeaders(extras = {}, prodApp = false) {
const tenantId = this.getTenantId() const tenantId = this.getTenantId()
const user = this.getUser()
const authObj: AuthToken = { const authObj: AuthToken = {
userId: this.globalUserId, userId: user._id!,
sessionId: "sessionid", sessionId: "sessionid",
tenantId, tenantId,
} }
@ -498,7 +511,7 @@ export default class TestConfiguration {
builder = false, builder = false,
prodApp = true, prodApp = true,
} = {}) { } = {}) {
return this.login({ email, roleId, builder, prodApp }) return this.login({ userId: email, roleId, builder, prodApp })
} }
// TENANCY // TENANCY
@ -521,18 +534,22 @@ export default class TestConfiguration {
this.tenantId = structures.tenant.id() this.tenantId = structures.tenant.id()
this.user = await this.globalUser() this.user = await this.globalUser()
this.userMetadataId = generateUserMetadataID(this.user._id) this.userMetadataId = generateUserMetadataID(this.user._id!)
return this.createApp(appName) return this.createApp(appName)
} }
doInTenant(task: any) { doInTenant<T>(task: () => T) {
return context.doInTenant(this.getTenantId(), task) return context.doInTenant(this.getTenantId(), task)
} }
// API // 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 db = tenancy.getTenantDB(this.getTenantId())
const id = dbCore.generateDevInfoID(userId) const id = dbCore.generateDevInfoID(userId)
let devInfo: any let devInfo: any
@ -552,25 +569,28 @@ export default class TestConfiguration {
async createApp(appName: string): Promise<App> { async createApp(appName: string): Promise<App> {
// create dev app // create dev app
// clear any old app // clear any old app
this.appId = null this.appId = undefined
this.app = await context.doInTenant(this.tenantId!, async () => { this.app = await context.doInTenant(
const app = await this._req({ name: appName }, null, appController.create) this.tenantId!,
this.appId = app.appId! async () =>
return app (await this._req(appController.create, {
}) name: appName,
return await context.doInAppContext(this.getAppId(), async () => { })) as App
)
this.appId = this.app.appId
return await context.doInAppContext(this.app.appId!, async () => {
// create production app // create production app
this.prodApp = await this.publish() this.prodApp = await this.publish()
this.allApps.push(this.prodApp) this.allApps.push(this.prodApp)
this.allApps.push(this.app) this.allApps.push(this.app!)
return this.app! return this.app!
}) })
} }
async publish() { async publish() {
await this._req(null, null, deployController.publishApp) await this._req(deployController.publishApp)
// @ts-ignore // @ts-ignore
const prodAppId = this.getAppId().replace("_dev", "") const prodAppId = this.getAppId().replace("_dev", "")
this.prodAppId = prodAppId this.prodAppId = prodAppId
@ -582,13 +602,11 @@ export default class TestConfiguration {
} }
async unpublish() { async unpublish() {
const response = await this._req( const response = await this._req(appController.unpublish, {
null, appId: this.appId,
{ appId: this.appId }, })
appController.unpublish this.prodAppId = undefined
) this.prodApp = undefined
this.prodAppId = null
this.prodApp = null
return response return response
} }
@ -716,8 +734,7 @@ export default class TestConfiguration {
// ROLE // ROLE
async createRole(config?: any) { async createRole(config?: any) {
config = config || basicRole() return this._req(roleController.save, config || basicRole())
return this._req(config, null, roleController.save)
} }
// VIEW // VIEW
@ -730,7 +747,7 @@ export default class TestConfiguration {
tableId: this.table!._id, tableId: this.table!._id,
name: generator.guid(), name: generator.guid(),
} }
return this._req(view, null, viewController.v1.save) return this._req(viewController.v1.save, view)
} }
async createView( async createView(
@ -754,40 +771,38 @@ export default class TestConfiguration {
// AUTOMATION // AUTOMATION
async createAutomation(config?: any) { async createAutomation(config?: Automation) {
config = config || basicAutomation() config = config || basicAutomation()
if (config._rev) { if (config._rev) {
delete config._rev delete config._rev
} }
this.automation = ( const res = await this._req(automationController.create, config)
await this._req(config, null, automationController.create) this.automation = res.automation
).automation
return this.automation return this.automation
} }
async getAllAutomations() { 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 automation = automation || this.automation
if (!automation) { if (!automation) {
return return
} }
return this._req( return this._req(automationController.destroy, undefined, {
null, id: automation._id,
{ id: automation._id, rev: automation._rev }, rev: automation._rev,
automationController.destroy })
)
} }
async createWebhook(config?: any) { async createWebhook(config?: Webhook) {
if (!this.automation) { if (!this.automation) {
throw "Must create an automation before creating webhook." 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 // DATASOURCE
@ -809,7 +824,7 @@ export default class TestConfiguration {
return { ...this.datasource, _id: this.datasource!._id! } return { ...this.datasource, _id: this.datasource!._id! }
} }
async restDatasource(cfg?: any) { async restDatasource(cfg?: Record<string, any>) {
return this.createDatasource({ return this.createDatasource({
datasource: { datasource: {
...basicDatasource().datasource, ...basicDatasource().datasource,
@ -866,26 +881,25 @@ export default class TestConfiguration {
// QUERY // QUERY
async createQuery(config?: any) { async createQuery(config?: Query) {
if (!this.datasource && !config) { return this._req(
throw "No datasource created for query." queryController.save,
} config || basicQuery(this.getDatasource()._id!)
config = config || basicQuery(this.datasource!._id!) )
return this._req(config, null, queryController.save)
} }
// SCREEN // SCREEN
async createScreen(config?: any) { async createScreen(config?: Screen) {
config = config || basicScreen() config = config || basicScreen()
return this._req(config, null, screenController.save) return this._req(screenController.save, config)
} }
// LAYOUT // LAYOUT
async createLayout(config?: any) { async createLayout(config?: Layout) {
config = config || basicLayout() config = config || basicLayout()
return await this._req(config, null, layoutController.save) return await this._req(layoutController.save, config)
} }
} }

View File

@ -22,6 +22,8 @@ import {
INTERNAL_TABLE_SOURCE_ID, INTERNAL_TABLE_SOURCE_ID,
TableSourceType, TableSourceType,
Query, Query,
Webhook,
WebhookActionType,
} from "@budibase/types" } from "@budibase/types"
import { LoopInput, LoopStepType } from "../../definitions/automations" import { LoopInput, LoopStepType } from "../../definitions/automations"
@ -407,12 +409,12 @@ export function basicLayout() {
return cloneDeep(EMPTY_LAYOUT) return cloneDeep(EMPTY_LAYOUT)
} }
export function basicWebhook(automationId: string) { export function basicWebhook(automationId: string): Webhook {
return { return {
live: true, live: true,
name: "webhook", name: "webhook",
action: { action: {
type: "automation", type: WebhookActionType.AUTOMATION,
target: automationId, target: automationId,
}, },
} }

View File

@ -32,9 +32,7 @@ export interface FetchDatasourceInfoResponse {
tableNames: string[] tableNames: string[]
} }
export interface UpdateDatasourceRequest extends Datasource { export interface UpdateDatasourceRequest extends Datasource {}
datasource: Datasource
}
export interface BuildSchemaFromSourceRequest { export interface BuildSchemaFromSourceRequest {
tablesFilter?: string[] tablesFilter?: string[]

View File

@ -0,0 +1,3 @@
import { DocumentDestroyResponse } from "@budibase/nano"
export interface DeleteAutomationResponse extends DocumentDestroyResponse {}

View File

@ -11,3 +11,5 @@ export * from "./global"
export * from "./pagination" export * from "./pagination"
export * from "./searchFilter" export * from "./searchFilter"
export * from "./cookies" export * from "./cookies"
export * from "./automation"
export * from "./layout"

View File

@ -0,0 +1,5 @@
import { Layout } from "../../documents"
export interface SaveLayoutRequest extends Layout {}
export interface SaveLayoutResponse extends Layout {}

View File

@ -6,6 +6,9 @@ export interface Datasource extends Document {
type: string type: string
name?: string name?: string
source: SourceName 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 // the config is defined by the schema
config?: Record<string, any> config?: Record<string, any>
plus?: boolean plus?: boolean
@ -36,6 +39,12 @@ export interface RestAuthConfig {
config: RestBasicAuthConfig | RestBearerAuthConfig config: RestBasicAuthConfig | RestBearerAuthConfig
} }
export interface DynamicVariable {
name: string
queryId: string
value: string
}
export interface RestConfig { export interface RestConfig {
url: string url: string
rejectUnauthorized: boolean rejectUnauthorized: boolean
@ -47,11 +56,5 @@ export interface RestConfig {
staticVariables: { staticVariables: {
[key: string]: string [key: string]: string
} }
dynamicVariables: [ dynamicVariables: DynamicVariable[]
{
name: string
queryId: string
value: string
}
]
} }

View File

@ -1,6 +1,11 @@
import { Document } from "../document" import { Document } from "../document"
export interface Layout extends Document { export interface Layout extends Document {
componentLibraries: string[]
title: string
favicon: string
stylesheets: string[]
props: any props: any
layoutId?: string layoutId?: string
name?: string
} }

View File

@ -22,4 +22,5 @@ export interface Screen extends Document {
routing: ScreenRouting routing: ScreenRouting
props: ScreenProps props: ScreenProps
name?: string name?: string
pluginAdded?: boolean
} }

View File

@ -5,15 +5,15 @@ export interface RowValue {
deleted: boolean deleted: boolean
} }
export interface RowResponse<T extends Document> { export interface RowResponse<T extends Document | RowValue> {
id: string id: string
key: string key: string
error: string error: string
value: T | RowValue value: T
doc?: T doc?: T
} }
export interface AllDocsResponse<T extends Document> { export interface AllDocsResponse<T extends Document | RowValue> {
offset: number offset: number
total_rows: number total_rows: number
rows: RowResponse<T>[] rows: RowResponse<T>[]

View File

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

View File

@ -1,5 +1,11 @@
import type Nano from "@budibase/nano" import type Nano from "@budibase/nano"
import { AllDocsResponse, AnyDocument, Document, ViewTemplateOpts } from "../" import {
AllDocsResponse,
AnyDocument,
Document,
RowValue,
ViewTemplateOpts,
} from "../"
import { Writable } from "stream" import { Writable } from "stream"
export enum SearchIndex { export enum SearchIndex {
@ -135,7 +141,7 @@ export interface Database {
opts?: DatabasePutOpts opts?: DatabasePutOpts
): Promise<Nano.DocumentInsertResponse> ): Promise<Nano.DocumentInsertResponse>
bulkDocs(documents: AnyDocument[]): Promise<Nano.DocumentBulkResponse[]> bulkDocs(documents: AnyDocument[]): Promise<Nano.DocumentBulkResponse[]>
allDocs<T extends Document>( allDocs<T extends Document | RowValue>(
params: DatabaseQueryOpts params: DatabaseQueryOpts
): Promise<AllDocsResponse<T>> ): Promise<AllDocsResponse<T>>
query<T extends Document>( query<T extends Document>(

View File

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

View File

@ -280,7 +280,7 @@ class TestConfiguration {
const db = context.getGlobalDB() const db = context.getGlobalDB()
const id = dbCore.generateDevInfoID(this.user!._id) const id = dbCore.generateDevInfoID(this.user!._id!)
// TODO: dry // TODO: dry
this.apiKey = encryption.encrypt( this.apiKey = encryption.encrypt(
`${this.tenantId}${dbCore.SEPARATOR}${utils.newid()}` `${this.tenantId}${dbCore.SEPARATOR}${utils.newid()}`

View File

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

384
yarn.lock
View File

@ -1097,7 +1097,7 @@
"@babel/highlight@^7.23.4": "@babel/highlight@^7.23.4":
version "7.23.4" version "7.23.4"
resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.23.4.tgz#edaadf4d8232e1a961432db785091207ead0621b" 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: dependencies:
"@babel/helper-validator-identifier" "^7.22.20" "@babel/helper-validator-identifier" "^7.22.20"
chalk "^2.4.2" chalk "^2.4.2"
@ -1988,14 +1988,14 @@
resolved "https://registry.yarnpkg.com/@babel/regjsgen/-/regjsgen-0.8.0.tgz#f0ba69b075e1f05fb2825b7fad991e7adbb18310" resolved "https://registry.yarnpkg.com/@babel/regjsgen/-/regjsgen-0.8.0.tgz#f0ba69b075e1f05fb2825b7fad991e7adbb18310"
integrity sha512-x/rqGMdzj+fWZvCOYForTghzbtqPDZ5gPwaoNGHdgDfF2QA/XZbCBp4Moo5scrkAMPhB7z26XM/AaHuIJdgauA== 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" version "7.23.9"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.23.9.tgz#47791a15e4603bb5f905bc0753801cf21d6345f7" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.23.9.tgz#47791a15e4603bb5f905bc0753801cf21d6345f7"
integrity sha512-0CX6F+BI2s9dkUqr08KFrAIZgNFj75rdBU/DjCyYLIaV/quFjkk6T+EJ2LkZHyZTbEV4L5p97mNkUsHl2wLFAw== integrity sha512-0CX6F+BI2s9dkUqr08KFrAIZgNFj75rdBU/DjCyYLIaV/quFjkk6T+EJ2LkZHyZTbEV4L5p97mNkUsHl2wLFAw==
dependencies: dependencies:
regenerator-runtime "^0.14.0" 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" version "7.23.8"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.23.8.tgz#8ee6fe1ac47add7122902f257b8ddf55c898f650" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.23.8.tgz#8ee6fe1ac47add7122902f257b8ddf55c898f650"
integrity sha512-Y7KbAP984rn1VGMbGqKmBLio9V7y5Je9GvU4rQPCPinCyNfUcToxIXl06d59URp/F3LwinvODxab5N/G6qggkw== integrity sha512-Y7KbAP984rn1VGMbGqKmBLio9V7y5Je9GvU4rQPCPinCyNfUcToxIXl06d59URp/F3LwinvODxab5N/G6qggkw==
@ -3419,9 +3419,9 @@
tar "^6.1.11" tar "^6.1.11"
"@mongodb-js/saslprep@^1.1.0": "@mongodb-js/saslprep@^1.1.0":
version "1.1.1" version "1.1.4"
resolved "https://registry.yarnpkg.com/@mongodb-js/saslprep/-/saslprep-1.1.1.tgz#9a6c2516bc9188672c4d953ec99760ba49970da7" resolved "https://registry.yarnpkg.com/@mongodb-js/saslprep/-/saslprep-1.1.4.tgz#24ec1c4915a65f5c506bb88c081731450d91bb1c"
integrity sha512-t7c5K033joZZMspnHg/gWPE4kandgc2OxE74aYOtGKfgB9VPuVJPix0H6fhmm2erj5PBJ21mqcx34lpIGtUCsQ== integrity sha512-8zJ8N1x51xo9hwPh6AWnKdLGEC5N3lDa6kms1YHmFBoRhTpJR6HG8wWk0td1MVCu9cD4YBrvjZEtd5Obw0Fbnw==
dependencies: dependencies:
sparse-bitfield "^3.0.3" sparse-bitfield "^3.0.3"
@ -4012,70 +4012,70 @@
estree-walker "^2.0.2" estree-walker "^2.0.2"
picomatch "^2.3.1" picomatch "^2.3.1"
"@rollup/rollup-android-arm-eabi@4.10.0": "@rollup/rollup-android-arm-eabi@4.12.0":
version "4.10.0" version "4.12.0"
resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.10.0.tgz#786eaf6372be2fc209cc957c14aa9d3ff8fefe6a" resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.12.0.tgz#38c3abd1955a3c21d492af6b1a1dca4bb1d894d6"
integrity sha512-/MeDQmcD96nVoRumKUljsYOLqfv1YFJps+0pTrb2Z9Nl/w5qNUysMaWQsrd1mvAlNT4yza1iVyIu4Q4AgF6V3A== integrity sha512-+ac02NL/2TCKRrJu2wffk1kZ+RyqxVUlbjSagNgPm94frxtr+XDL12E5Ll1enWskLrtrZ2r8L3wED1orIibV/w==
"@rollup/rollup-android-arm64@4.10.0": "@rollup/rollup-android-arm64@4.12.0":
version "4.10.0" version "4.12.0"
resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.10.0.tgz#0114a042fd6396f4f3233e6171fd5b61a36ed539" resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.12.0.tgz#3822e929f415627609e53b11cec9a4be806de0e2"
integrity sha512-lvu0jK97mZDJdpZKDnZI93I0Om8lSDaiPx3OiCk0RXn3E8CMPJNS/wxjAvSJJzhhZpfjXsjLWL8LnS6qET4VNQ== integrity sha512-OBqcX2BMe6nvjQ0Nyp7cC90cnumt8PXmO7Dp3gfAju/6YwG0Tj74z1vKrfRz7qAv23nBcYM8BCbhrsWqO7PzQQ==
"@rollup/rollup-darwin-arm64@4.10.0": "@rollup/rollup-darwin-arm64@4.12.0":
version "4.10.0" version "4.12.0"
resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.10.0.tgz#944d007c1dc71a8c9174d11671c0c34bd74a2c81" resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.12.0.tgz#6c082de71f481f57df6cfa3701ab2a7afde96f69"
integrity sha512-uFpayx8I8tyOvDkD7X6n0PriDRWxcqEjqgtlxnUA/G9oS93ur9aZ8c8BEpzFmsed1TH5WZNG5IONB8IiW90TQg== integrity sha512-X64tZd8dRE/QTrBIEs63kaOBG0b5GVEd3ccoLtyf6IdXtHdh8h+I56C2yC3PtC9Ucnv0CpNFJLqKFVgCYe0lOQ==
"@rollup/rollup-darwin-x64@4.10.0": "@rollup/rollup-darwin-x64@4.12.0":
version "4.10.0" version "4.12.0"
resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.10.0.tgz#1d08cb4521a058d7736ab1c7fe988daf034a2598" resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.12.0.tgz#c34ca0d31f3c46a22c9afa0e944403eea0edcfd8"
integrity sha512-nIdCX03qFKoR/MwQegQBK+qZoSpO3LESurVAC6s6jazLA1Mpmgzo3Nj3H1vydXp/JM29bkCiuF7tDuToj4+U9Q== integrity sha512-cc71KUZoVbUJmGP2cOuiZ9HSOP14AzBAThn3OU+9LcA1+IUqswJyR1cAJj3Mg55HbjZP6OLAIscbQsQLrpgTOg==
"@rollup/rollup-linux-arm-gnueabihf@4.10.0": "@rollup/rollup-linux-arm-gnueabihf@4.12.0":
version "4.10.0" version "4.12.0"
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.10.0.tgz#4763eec1591bf0e99a54ad3d1ef39cb268ed7b19" resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.12.0.tgz#48e899c1e438629c072889b824a98787a7c2362d"
integrity sha512-Fz7a+y5sYhYZMQFRkOyCs4PLhICAnxRX/GnWYReaAoruUzuRtcf+Qnw+T0CoAWbHCuz2gBUwmWnUgQ67fb3FYw== integrity sha512-a6w/Y3hyyO6GlpKL2xJ4IOh/7d+APaqLYdMf86xnczU3nurFTaVN9s9jOXQg97BE4nYm/7Ga51rjec5nfRdrvA==
"@rollup/rollup-linux-arm64-gnu@4.10.0": "@rollup/rollup-linux-arm64-gnu@4.12.0":
version "4.10.0" version "4.12.0"
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.10.0.tgz#e6dae70c53ace836973526c41803b877cffc6f7b" resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.12.0.tgz#788c2698a119dc229062d40da6ada8a090a73a68"
integrity sha512-yPtF9jIix88orwfTi0lJiqINnlWo6p93MtZEoaehZnmCzEmLL0eqjA3eGVeyQhMtxdV+Mlsgfwhh0+M/k1/V7Q== integrity sha512-0fZBq27b+D7Ar5CQMofVN8sggOVhEtzFUwOwPppQt0k+VR+7UHMZZY4y+64WJ06XOhBTKXtQB/Sv0NwQMXyNAA==
"@rollup/rollup-linux-arm64-musl@4.10.0": "@rollup/rollup-linux-arm64-musl@4.12.0":
version "4.10.0" version "4.12.0"
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.10.0.tgz#5692e1a0feba0cc4a933864961afc3211177d242" resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.12.0.tgz#3882a4e3a564af9e55804beeb67076857b035ab7"
integrity sha512-9GW9yA30ib+vfFiwjX+N7PnjTnCMiUffhWj4vkG4ukYv1kJ4T9gHNg8zw+ChsOccM27G9yXrEtMScf1LaCuoWQ== integrity sha512-eTvzUS3hhhlgeAv6bfigekzWZjaEX9xP9HhxB0Dvrdbkk5w/b+1Sxct2ZuDxNJKzsRStSq1EaEkVSEe7A7ipgQ==
"@rollup/rollup-linux-riscv64-gnu@4.10.0": "@rollup/rollup-linux-riscv64-gnu@4.12.0":
version "4.10.0" version "4.12.0"
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.10.0.tgz#fbe3d80f7a7ac54a8847f5bddd1bc6f7b9ccb65f" resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.12.0.tgz#0c6ad792e1195c12bfae634425a3d2aa0fe93ab7"
integrity sha512-X1ES+V4bMq2ws5fF4zHornxebNxMXye0ZZjUrzOrf7UMx1d6wMQtfcchZ8SqUnQPPHdOyOLW6fTcUiFgHFadRA== integrity sha512-ix+qAB9qmrCRiaO71VFfY8rkiAZJL8zQRXveS27HS+pKdjwUfEhqo2+YF2oI+H/22Xsiski+qqwIBxVewLK7sw==
"@rollup/rollup-linux-x64-gnu@4.10.0": "@rollup/rollup-linux-x64-gnu@4.12.0":
version "4.10.0" version "4.12.0"
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.10.0.tgz#3f06b55ccf173446d390d0306643dff62ec99807" resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.12.0.tgz#9d62485ea0f18d8674033b57aa14fb758f6ec6e3"
integrity sha512-w/5OpT2EnI/Xvypw4FIhV34jmNqU5PZjZue2l2Y3ty1Ootm3SqhI+AmfhlUYGBTd9JnpneZCDnt3uNOiOBkMyw== integrity sha512-TenQhZVOtw/3qKOPa7d+QgkeM6xY0LtwzR8OplmyL5LrgTWIXpTQg2Q2ycBf8jm+SFW2Wt/DTn1gf7nFp3ssVA==
"@rollup/rollup-linux-x64-musl@4.10.0": "@rollup/rollup-linux-x64-musl@4.12.0":
version "4.10.0" version "4.12.0"
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.10.0.tgz#e4ac9b27041c83d7faab6205f62763103eb317ba" resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.12.0.tgz#50e8167e28b33c977c1f813def2b2074d1435e05"
integrity sha512-q/meftEe3QlwQiGYxD9rWwB21DoKQ9Q8wA40of/of6yGHhZuGfZO0c3WYkN9dNlopHlNT3mf5BPsUSxoPuVQaw== integrity sha512-LfFdRhNnW0zdMvdCb5FNuWlls2WbbSridJvxOvYWgSBOYZtgBfW9UGNJG//rwMqTX1xQE9BAodvMH9tAusKDUw==
"@rollup/rollup-win32-arm64-msvc@4.10.0": "@rollup/rollup-win32-arm64-msvc@4.12.0":
version "4.10.0" version "4.12.0"
resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.10.0.tgz#6ad0d4fb0066f240778ee3f61eecf7aa0357f883" resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.12.0.tgz#68d233272a2004429124494121a42c4aebdc5b8e"
integrity sha512-NrR6667wlUfP0BHaEIKgYM/2va+Oj+RjZSASbBMnszM9k+1AmliRjHc3lJIiOehtSSjqYiO7R6KLNrWOX+YNSQ== integrity sha512-JPDxovheWNp6d7AHCgsUlkuCKvtu3RB55iNEkaQcf0ttsDU/JZF+iQnYcQJSk/7PtT4mjjVG8N1kpwnI9SLYaw==
"@rollup/rollup-win32-ia32-msvc@4.10.0": "@rollup/rollup-win32-ia32-msvc@4.12.0":
version "4.10.0" version "4.12.0"
resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.10.0.tgz#29d50292381311cc8d3623e73b427b7e2e40a653" resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.12.0.tgz#366ca62221d1689e3b55a03f4ae12ae9ba595d40"
integrity sha512-FV0Tpt84LPYDduIDcXvEC7HKtyXxdvhdAOvOeWMWbQNulxViH2O07QXkT/FffX4FqEI02jEbCJbr+YcuKdyyMg== integrity sha512-fjtuvMWRGJn1oZacG8IPnzIV6GF2/XG+h71FKn76OYFqySXInJtseAqdprVTDTyqPxQOG9Exak5/E9Z3+EJ8ZA==
"@rollup/rollup-win32-x64-msvc@4.10.0": "@rollup/rollup-win32-x64-msvc@4.12.0":
version "4.10.0" version "4.12.0"
resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.10.0.tgz#4eedd01af3a82c1acb0fe6d837ebf339c4cbf839" resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.12.0.tgz#9ffdf9ed133a7464f4ae187eb9e1294413fab235"
integrity sha512-OZoJd+o5TaTSQeFFQ6WjFCiltiYVjIdsXxwu/XZ8qRpsvMQr4UsVrE5UyT9RIvsnuF47DqkJKhhVZ2Q9YW9IpQ== integrity sha512-ZYmr5mS2wd4Dew/JjT0Fqi2NPB/ZhZ2VvPp7SmvPZb4Y1CG/LRcS6tcRo2cYU7zLK5A7cdbhWnnWmUjoI4qapg==
"@roxi/routify@2.18.0": "@roxi/routify@2.18.0":
version "2.18.0" version "2.18.0"
@ -5219,16 +5219,16 @@
integrity sha512-6ckxMjBBD8URvjB6J3NcnuAn5Pkl7t3TizAg+xdlzzQGSPSmBcXf8KoIH0ua/i+tio+ZRUHEXp0HEmvaR4kt0w== integrity sha512-6ckxMjBBD8URvjB6J3NcnuAn5Pkl7t3TizAg+xdlzzQGSPSmBcXf8KoIH0ua/i+tio+ZRUHEXp0HEmvaR4kt0w==
"@types/chai-subset@^1.3.3": "@types/chai-subset@^1.3.3":
version "1.3.5" version "1.3.3"
resolved "https://registry.yarnpkg.com/@types/chai-subset/-/chai-subset-1.3.5.tgz#3fc044451f26985f45625230a7f22284808b0a9a" resolved "https://registry.yarnpkg.com/@types/chai-subset/-/chai-subset-1.3.3.tgz#97893814e92abd2c534de422cb377e0e0bdaac94"
integrity sha512-c2mPnw+xHtXDoHmdtcCXGwyLMiauiAyxWMzhGpqHC4nqI/Y5G2XhTampslK2rb59kpcuHon03UH8W6iYUzw88A== integrity sha512-frBecisrNGz+F4T6bcc+NLeolfiojh5FxW2klu669+8BARtyQv2C/GkNW6FUodVe4BroGMP/wER/YDGc7rEllw==
dependencies: dependencies:
"@types/chai" "*" "@types/chai" "*"
"@types/chai@*", "@types/chai@^4.3.4": "@types/chai@*", "@types/chai@^4.3.4":
version "4.3.11" version "4.3.9"
resolved "https://registry.yarnpkg.com/@types/chai/-/chai-4.3.11.tgz#e95050bf79a932cb7305dd130254ccdf9bde671c" resolved "https://registry.yarnpkg.com/@types/chai/-/chai-4.3.9.tgz#144d762491967db8c6dea38e03d2206c2623feec"
integrity sha512-qQR1dr2rGIHYlJulmr8Ioq3De0Le9E4MJ5AiaeAETJJpndT1uUNHsGFK3L/UIu+rbkQSdj8J/w2bCsBZc/Y5fQ== integrity sha512-69TtiDzu0bcmKQv3yg1Zx409/Kd7r0b5F1PfpYJfSHzLGtB53547V4u+9iqKYsTu/O2ai6KTb0TInNpvuQ3qmg==
"@types/chance@1.1.3": "@types/chance@1.1.3":
version "1.1.3" version "1.1.3"
@ -5623,10 +5623,10 @@
"@types/node" "*" "@types/node" "*"
form-data "^3.0.0" 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": "@types/node@*", "@types/node@>=10.0.0", "@types/node@>=12.12.47", "@types/node@>=13.13.4", "@types/node@>=13.7.0":
version "20.11.2" version "20.10.7"
resolved "https://registry.yarnpkg.com/@types/node/-/node-20.11.2.tgz#39cea3fe02fbbc2f80ed283e94e1d24f2d3856fb" resolved "https://registry.yarnpkg.com/@types/node/-/node-20.10.7.tgz#40fe8faf25418a75de9fe68a8775546732a3a901"
integrity sha512-cZShBaVa+UO1LjWWBPmWRR4+/eY/JR/UIEcDlVsw3okjWEu+rB7/mH6X3B/L+qJVHDLjk9QW/y2upp9wp1yDXA== integrity sha512-fRbIKb8C/Y2lXxB5eVMj4IU7xpdox0Lh8bUPEdtLysaylsml1hOOx1+STloRs/B9nf7C6kPRmmg/V7aQW7usNg==
dependencies: dependencies:
undici-types "~5.26.4" undici-types "~5.26.4"
@ -5652,10 +5652,17 @@
resolved "https://registry.yarnpkg.com/@types/node/-/node-14.18.37.tgz#0bfcd173e8e1e328337473a8317e37b3b14fd30d" resolved "https://registry.yarnpkg.com/@types/node/-/node-14.18.37.tgz#0bfcd173e8e1e328337473a8317e37b3b14fd30d"
integrity sha512-7GgtHCs/QZrBrDzgIJnQtuSvhFSwhyYSI2uafSwZoNt1iOGhEN5fwNrQMjtONyHm9+/LoA4453jH0CMYcr06Pg== 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": "@types/node@^18.11.18":
version "18.19.13" version "18.19.10"
resolved "https://registry.yarnpkg.com/@types/node/-/node-18.19.13.tgz#c3e989ca967b862a1f6c8c4148fe31865eedaf1a" resolved "https://registry.yarnpkg.com/@types/node/-/node-18.19.10.tgz#4de314ab66faf6bc8ba691021a091ddcdf13a158"
integrity sha512-kgnbRDj8ioDyGxoiaXsiu1Ybm/K14ajCgMOkwiqpHrnF7d7QiYRoRqHIpglMMs3DwXinlK4qJ8TZGlj4hfleJg== integrity sha512-IZD8kAM02AW1HRDTPOlz3npFava678pr8Ie9Vp8uRhBROXAv8MXT2pCnGZZAKYdromsNQLHQcfWQ6EOatVLtqA==
dependencies: dependencies:
undici-types "~5.26.4" undici-types "~5.26.4"
@ -6075,9 +6082,9 @@
integrity sha512-xTE1E+YF4aWPJJeUzaZI5DRntlkY3+BCVJi0axFptnjGmAoWxkyREIh/XMrfxVLejwQxMCfDXdICo0VLxThrog== integrity sha512-xTE1E+YF4aWPJJeUzaZI5DRntlkY3+BCVJi0axFptnjGmAoWxkyREIh/XMrfxVLejwQxMCfDXdICo0VLxThrog==
"@types/whatwg-url@^11.0.2": "@types/whatwg-url@^11.0.2":
version "11.0.3" version "11.0.4"
resolved "https://registry.yarnpkg.com/@types/whatwg-url/-/whatwg-url-11.0.3.tgz#9f584c9a9421f0971029ee504dd62a831cb8f3aa" resolved "https://registry.yarnpkg.com/@types/whatwg-url/-/whatwg-url-11.0.4.tgz#ffed0dc8d89d91f62e3f368fcbda222a487c4f63"
integrity sha512-z1ELvMijRL1QmU7QuzDkeYXSF2+dXI0ITKoQsIoVKcNBOiK5RMmWy+pYYxJTHFt8vkpZe7UsvRErQwcxZkjoUw== integrity sha512-lXCmTWSHJvf0TRSO58nm978b8HJ/EdsSsEKLd3ODHFjo+3VGAyyTp4v50nWvwtzBxSMQrVOK7tcuN0zGPLICMw==
dependencies: dependencies:
"@types/webidl-conversions" "*" "@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" resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-7.2.0.tgz#0de889a601203909b0fbe07b8938dc21d2e967bc"
integrity sha512-OPdCF6GsMIP+Az+aWfAAOEt2/+iVDKE7oy6lJ098aoe59oAmK76qV6Gw60SbZ8jHuG2wH058GF4pLFbYamYrVA== 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" version "8.2.0"
resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-8.2.0.tgz#741210f2e2426454508853a2f44d0ab83b7f69c1" resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-8.2.0.tgz#741210f2e2426454508853a2f44d0ab83b7f69c1"
integrity sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA== 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: acorn@^5.2.1, acorn@^5.7.3:
version "5.7.4" version "5.7.4"
resolved "https://registry.yarnpkg.com/acorn/-/acorn-5.7.4.tgz#3e8d8a9947d0599a1796d10225d7432f4a4acf5e" 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" resolved "https://registry.yarnpkg.com/acorn/-/acorn-7.4.1.tgz#feaed255973d2e77555b83dbc08851a6c63520fa"
integrity sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A== 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: 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.3" version "8.11.2"
resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.11.3.tgz#71e0b14e13a4ec160724b38fb7b0f233b1b81d7a" resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.11.2.tgz#ca0d78b51895be5390a5903c5b3bdcdaf78ae40b"
integrity sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg== integrity sha512-nc0Axzp/0FILLEVsm4fNwLCwMttvhEI263QtVPQcbpfZZ3ts0hLsZGOpE6czNlid7CJ9MlyH8reXkpsf3YUY4w==
add-stream@^1.0.0: add-stream@^1.0.0:
version "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" minimalistic-assert "^1.0.0"
safer-buffer "^2.1.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" version "0.2.6"
resolved "https://registry.yarnpkg.com/asn1/-/asn1-0.2.6.tgz#0d3a7bb6e64e02a90c0303b31f292868ea09a08d" resolved "https://registry.yarnpkg.com/asn1/-/asn1-0.2.6.tgz#0d3a7bb6e64e02a90c0303b31f292868ea09a08d"
integrity sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ== integrity sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==
@ -7043,7 +7045,12 @@ async@^2.6.3:
dependencies: dependencies:
lodash "^4.17.14" 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" version "3.2.5"
resolved "https://registry.yarnpkg.com/async/-/async-3.2.5.tgz#ebd52a8fdaf7a2289a24df399f8d8485c8a46b66" resolved "https://registry.yarnpkg.com/async/-/async-3.2.5.tgz#ebd52a8fdaf7a2289a24df399f8d8485c8a46b66"
integrity sha512-baNZyqaaLhyLVKm/DlvdW051MSgO6b8eVfIezl9E5PqWxFgzLm/wQntEW4zOytVburDEr0JlALEpdOFwvErLsg== integrity sha512-baNZyqaaLhyLVKm/DlvdW051MSgO6b8eVfIezl9E5PqWxFgzLm/wQntEW4zOytVburDEr0JlALEpdOFwvErLsg==
@ -7646,6 +7653,11 @@ bufferutil@^4.0.1:
dependencies: dependencies:
node-gyp-build "^4.3.0" 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: buildcheck@~0.0.6:
version "0.0.6" version "0.0.6"
resolved "https://registry.yarnpkg.com/buildcheck/-/buildcheck-0.0.6.tgz#89aa6e417cfd1e2196e3f8fe915eb709d2fe4238" resolved "https://registry.yarnpkg.com/buildcheck/-/buildcheck-0.0.6.tgz#89aa6e417cfd1e2196e3f8fe915eb709d2fe4238"
@ -7910,9 +7922,9 @@ catharsis@^0.9.0:
lodash "^4.17.15" lodash "^4.17.15"
chai@^4.3.7: chai@^4.3.7:
version "4.4.1" version "4.3.10"
resolved "https://registry.yarnpkg.com/chai/-/chai-4.4.1.tgz#3603fa6eba35425b0f2ac91a009fe924106e50d1" resolved "https://registry.yarnpkg.com/chai/-/chai-4.3.10.tgz#d784cec635e3b7e2ffb66446a63b4e33bd390384"
integrity sha512-13sOfMv2+DWduEU+/xbun3LScLoqN17nBeTLUsmDfKdoiC1fr0n9PU4guu4AhRcOVFk/sW8LyZWHuhWtQZiF+g== integrity sha512-0UXG04VuVbruMUYbJ6JctvH0YnC/4q3/AkT18q4NaITo91CUm0liMS9VqzT9vZhVQ/1eqPanMWjBM+Juhfb/9g==
dependencies: dependencies:
assertion-error "^1.1.0" assertion-error "^1.1.0"
check-error "^1.0.3" check-error "^1.0.3"
@ -8654,6 +8666,14 @@ cosmiconfig@^8.2.0:
parse-json "^5.0.0" parse-json "^5.0.0"
path-type "^4.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: cpu-features@~0.0.9:
version "0.0.9" version "0.0.9"
resolved "https://registry.yarnpkg.com/cpu-features/-/cpu-features-0.0.9.tgz#5226b92f0f1c63122b0a3eb84cb8335a4de499fc" 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== integrity sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==
diff@^5.1.0: diff@^5.1.0:
version "5.2.0" version "5.1.0"
resolved "https://registry.yarnpkg.com/diff/-/diff-5.2.0.tgz#26ded047cd1179b78b9537d5ef725503ce1ae531" resolved "https://registry.yarnpkg.com/diff/-/diff-5.1.0.tgz#bc52d298c5ea8df9194800224445ed43ffc87e40"
integrity sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A== integrity sha512-D+mk+qE8VC/PAUrlAU34N+VfXev0ghe5ywmpqrawphmVZc1bEfn56uo9qpyGp1p4xpzOHkSW4ztBd6L7Xx4ACw==
diffie-hellman@^5.0.0: diffie-hellman@^5.0.0:
version "5.0.3" version "5.0.3"
@ -9611,7 +9631,16 @@ docker-modem@^3.0.0:
split-ca "^1.0.1" split-ca "^1.0.1"
ssh2 "^1.11.0" 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" version "3.3.5"
resolved "https://registry.yarnpkg.com/dockerode/-/dockerode-3.3.5.tgz#7ae3f40f2bec53ae5e9a741ce655fff459745629" resolved "https://registry.yarnpkg.com/dockerode/-/dockerode-3.3.5.tgz#7ae3f40f2bec53ae5e9a741ce655fff459745629"
integrity sha512-/0YNa3ZDNeLr/tSckmD69+Gq+qVNhvKfAHNeZJBnp7EOP6RGKV8ORrJHkUn20So5wU+xxT7+1n5u8PjHbfjbSA== 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== integrity sha512-IrPdXQsk2BbzvCBGBOTmmSH5SodmqZNt4ERAZDmW4CT+tL8VtvinqywuANaFu4bOMWki16nqf0e4oC0QIaDr/g==
dotenv@^16.3.1: dotenv@^16.3.1:
version "16.3.1" version "16.4.1"
resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.3.1.tgz#369034de7d7e5b120972693352a3bf112172cc3e" resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.4.1.tgz#1d9931f1d3e5d2959350d1250efab299561f7f11"
integrity sha512-IPzF4w4/Rd94bA9imS68tZBaYyBWSCE47V1RGuMrB94iyTOIEwRmVL2x/4An+6mETpLrKJ5hQkB8W4kFAadeIQ== integrity sha512-CjA3y+Dr3FyFDOAMnxZEGtnW9KBR2M0JvvUtXNW+dYJL5ROWxP9DUHCwgFqpMk0OXCc0ljhaNTr2w/kutYIcHQ==
dotenv@~10.0.0: dotenv@~10.0.0:
version "10.0.0" version "10.0.0"
@ -10791,13 +10820,20 @@ fast-xml-parser@4.2.5:
dependencies: dependencies:
strnum "^1.0.5" 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" version "4.3.3"
resolved "https://registry.yarnpkg.com/fast-xml-parser/-/fast-xml-parser-4.3.3.tgz#aeaf5778392329f17168c40c51bcbfec8ff965be" resolved "https://registry.yarnpkg.com/fast-xml-parser/-/fast-xml-parser-4.3.3.tgz#aeaf5778392329f17168c40c51bcbfec8ff965be"
integrity sha512-coV/D1MhrShMvU6D0I+VAK3umz6hUaxxhL0yp/9RjfiYUfAv14rDhGQL+PLForhMdr0wq3PiV07WtkkNjJjNHg== integrity sha512-coV/D1MhrShMvU6D0I+VAK3umz6hUaxxhL0yp/9RjfiYUfAv14rDhGQL+PLForhMdr0wq3PiV07WtkkNjJjNHg==
dependencies: dependencies:
strnum "^1.0.5" 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: fastest-levenshtein@^1.0.12:
version "1.0.16" version "1.0.16"
resolved "https://registry.yarnpkg.com/fastest-levenshtein/-/fastest-levenshtein-1.0.16.tgz#210e61b6ff181de91ea9b3d1b84fdedd47e034e5" resolved "https://registry.yarnpkg.com/fastest-levenshtein/-/fastest-levenshtein-1.0.16.tgz#210e61b6ff181de91ea9b3d1b84fdedd47e034e5"
@ -10857,7 +10893,7 @@ fetch-cookie@0.11.0:
dependencies: dependencies:
tough-cookie "^2.3.3 || ^3.0.1 || ^4.0.0" 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" version "0.4.8"
resolved "https://registry.yarnpkg.com/fflate/-/fflate-0.4.8.tgz#f90b82aefbd8ac174213abb338bd7ef848f0f5ae" resolved "https://registry.yarnpkg.com/fflate/-/fflate-0.4.8.tgz#f90b82aefbd8ac174213abb338bd7ef848f0f5ae"
integrity sha512-FJqqoDBR00Mdj9ppamLa/Y7vxm+PRmNWA67N846RvsoYVMKB4q3y/de5PA7gUmRMYK/8CMz2GDZQmCRN1wBcWA== 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" resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e"
integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw== integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==
mlly@^1.1.0: mlly@^1.1.0, mlly@^1.2.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:
version "1.4.2" version "1.4.2"
resolved "https://registry.yarnpkg.com/mlly/-/mlly-1.4.2.tgz#7cf406aa319ff6563d25da6b36610a93f2a8007e" resolved "https://registry.yarnpkg.com/mlly/-/mlly-1.4.2.tgz#7cf406aa319ff6563d25da6b36610a93f2a8007e"
integrity sha512-i/Ykufi2t1EZ6NaPLdfnZk2AX8cs0d+mTzVKuPfqPKPatxLApaBoxJQ9x1/uckXtrS/U5oisPMDkNs0yQTaBRg== integrity sha512-i/Ykufi2t1EZ6NaPLdfnZk2AX8cs0d+mTzVKuPfqPKPatxLApaBoxJQ9x1/uckXtrS/U5oisPMDkNs0yQTaBRg==
@ -15809,6 +15835,11 @@ named-placeholders@^1.1.3:
dependencies: dependencies:
lru-cache "^7.14.1" 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: nan@^2.17.0, nan@^2.18.0:
version "2.18.0" version "2.18.0"
resolved "https://registry.yarnpkg.com/nan/-/nan-2.18.0.tgz#26a6faae7ffbeb293a39660e88a76b82e30b7554" 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" resolved "https://registry.yarnpkg.com/pathe/-/pathe-1.1.1.tgz#1dd31d382b974ba69809adc9a7a347e65d84829a"
integrity sha512-d+RQGp0MAYTIaDBIMmOfMwz3E+LOZnxx1HZd5R18mmCZY0QBlK0LDZfPc8FW8Ed2DlvsuE6PRjroDY+wg4+j/Q== 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: pathval@^1.1.1:
version "1.1.1" version "1.1.1"
resolved "https://registry.yarnpkg.com/pathval/-/pathval-1.1.1.tgz#8534e77a77ce7ac5a2512ea21e0fdb8fcf6c3d8d" resolved "https://registry.yarnpkg.com/pathval/-/pathval-1.1.1.tgz#8534e77a77ce7ac5a2512ea21e0fdb8fcf6c3d8d"
@ -17800,10 +17826,18 @@ postgres-interval@^1.1.0:
dependencies: dependencies:
xtend "^4.0.0" xtend "^4.0.0"
posthog-js@^1.13.4, posthog-js@^1.36.0: posthog-js@^1.13.4:
version "1.100.0" version "1.103.1"
resolved "https://registry.yarnpkg.com/posthog-js/-/posthog-js-1.100.0.tgz#687b9a6e4ed226aa6572f4040b418ea0c8b3d353" resolved "https://registry.yarnpkg.com/posthog-js/-/posthog-js-1.103.1.tgz#f846c413c28aca204dc1527f49d39f651348f3c4"
integrity sha512-r2XZEiHQ9mBK7D1G9k57I8uYZ2kZTAJ0OCX6K/OOdCWN8jKPhw3h5F9No5weilP6eVAn+hrsy7NvPV7SCX7gMg== 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: dependencies:
fflate "^0.4.1" 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" resolved "https://registry.yarnpkg.com/pprof-format/-/pprof-format-2.0.7.tgz#526e4361f8b37d16b2ec4bb0696b5292de5046a4"
integrity sha512-1qWaGAzwMpaXJP9opRa23nPnt2Egi7RMNoNBptEE/XwHbcn4fC2b/4U4bKc5arkGkIh2ZabpF2bEb+c5GNHEKA== 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: prebuild-install@^7.1.1:
version "7.1.1" version "7.1.1"
resolved "https://registry.yarnpkg.com/prebuild-install/-/prebuild-install-7.1.1.tgz#de97d5b34a70a0c81334fd24641f2a1702352e45" 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" fsevents "~2.3.2"
rollup@^4.9.6: rollup@^4.9.6:
version "4.10.0" version "4.12.0"
resolved "https://registry.yarnpkg.com/rollup/-/rollup-4.10.0.tgz#244c2cb54a8de004a949fe6036a0801be9060456" resolved "https://registry.yarnpkg.com/rollup/-/rollup-4.12.0.tgz#0b6d1e5f3d46bbcf244deec41a7421dc54cc45b5"
integrity sha512-t2v9G2AKxcQ8yrG+WGxctBes1AomT0M4ND7jTFBCVPXQ/WFTvNSefIrNSmLKhIKBrvN8SG+CZslimJcT3W2u2g== integrity sha512-wz66wn4t1OHIJw3+XU7mJJQV/2NAfw5OAk6G6Hoo3zcvz/XOfQ52Vgi+AN4Uxoxi0KBBwk2g8zPrTDA4btSB/Q==
dependencies: dependencies:
"@types/estree" "1.0.5" "@types/estree" "1.0.5"
optionalDependencies: optionalDependencies:
"@rollup/rollup-android-arm-eabi" "4.10.0" "@rollup/rollup-android-arm-eabi" "4.12.0"
"@rollup/rollup-android-arm64" "4.10.0" "@rollup/rollup-android-arm64" "4.12.0"
"@rollup/rollup-darwin-arm64" "4.10.0" "@rollup/rollup-darwin-arm64" "4.12.0"
"@rollup/rollup-darwin-x64" "4.10.0" "@rollup/rollup-darwin-x64" "4.12.0"
"@rollup/rollup-linux-arm-gnueabihf" "4.10.0" "@rollup/rollup-linux-arm-gnueabihf" "4.12.0"
"@rollup/rollup-linux-arm64-gnu" "4.10.0" "@rollup/rollup-linux-arm64-gnu" "4.12.0"
"@rollup/rollup-linux-arm64-musl" "4.10.0" "@rollup/rollup-linux-arm64-musl" "4.12.0"
"@rollup/rollup-linux-riscv64-gnu" "4.10.0" "@rollup/rollup-linux-riscv64-gnu" "4.12.0"
"@rollup/rollup-linux-x64-gnu" "4.10.0" "@rollup/rollup-linux-x64-gnu" "4.12.0"
"@rollup/rollup-linux-x64-musl" "4.10.0" "@rollup/rollup-linux-x64-musl" "4.12.0"
"@rollup/rollup-win32-arm64-msvc" "4.10.0" "@rollup/rollup-win32-arm64-msvc" "4.12.0"
"@rollup/rollup-win32-ia32-msvc" "4.10.0" "@rollup/rollup-win32-ia32-msvc" "4.12.0"
"@rollup/rollup-win32-x64-msvc" "4.10.0" "@rollup/rollup-win32-x64-msvc" "4.12.0"
fsevents "~2.3.2" fsevents "~2.3.2"
rotating-file-stream@3.1.0: rotating-file-stream@3.1.0:
@ -19309,7 +19348,14 @@ rxjs@^6.6.6:
dependencies: dependencies:
tslib "^1.9.0" 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" version "7.8.1"
resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-7.8.1.tgz#6f6f3d99ea8044291efd92e7c7fcf562c4057543" resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-7.8.1.tgz#6f6f3d99ea8044291efd92e7c7fcf562c4057543"
integrity sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg== integrity sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==
@ -20003,7 +20049,18 @@ ssh-remote-port-forward@^1.0.4:
"@types/ssh2" "^0.5.48" "@types/ssh2" "^0.5.48"
ssh2 "^1.4.0" 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" version "1.15.0"
resolved "https://registry.yarnpkg.com/ssh2/-/ssh2-1.15.0.tgz#2f998455036a7f89e0df5847efb5421748d9871b" resolved "https://registry.yarnpkg.com/ssh2/-/ssh2-1.15.0.tgz#2f998455036a7f89e0df5847efb5421748d9871b"
integrity sha512-C0PHgX4h6lBxYx7hcXwu3QWdh4tg6tZZsTfXcdvc5caW/EMxaB4H9dWsl7qk+F7LAW762hp8VbXOX7x4xUYvEw== integrity sha512-C0PHgX4h6lBxYx7hcXwu3QWdh4tg6tZZsTfXcdvc5caW/EMxaB4H9dWsl7qk+F7LAW762hp8VbXOX7x4xUYvEw==
@ -20081,9 +20138,9 @@ statuses@2.0.1, statuses@^2.0.0:
integrity sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA== integrity sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==
std-env@^3.3.1: std-env@^3.3.1:
version "3.7.0" version "3.4.3"
resolved "https://registry.yarnpkg.com/std-env/-/std-env-3.7.0.tgz#c9f7386ced6ecf13360b6c6c55b8aaa4ef7481d2" resolved "https://registry.yarnpkg.com/std-env/-/std-env-3.4.3.tgz#326f11db518db751c83fd58574f449b7c3060910"
integrity sha512-JPbdCEQLj1w5GilpiHAx3qJvFndqybBysA3qUOnznweH4QbNYUsW/ea8QzSrnh0vNsezMMw5bcVool8lM0gwzg== integrity sha512-f9aPhy8fYBuMN+sNfakZV18U39PbalgjXG3lLB9WkaYTxijru61wb57V9wxxNthXM5Sd88ETBWi29qLAsHO52Q==
step@0.0.x: step@0.0.x:
version "0.0.6" version "0.0.6"
@ -20546,9 +20603,9 @@ svelte-spa-router@^4.0.1:
regexparam "2.0.2" regexparam "2.0.2"
svelte@^4.2.10: svelte@^4.2.10:
version "4.2.10" version "4.2.12"
resolved "https://registry.yarnpkg.com/svelte/-/svelte-4.2.10.tgz#3bef8d79ca75eb53cc4d03f9fac1546e60393f77" resolved "https://registry.yarnpkg.com/svelte/-/svelte-4.2.12.tgz#13d98d2274d24d3ad216c8fdc801511171c70bb1"
integrity sha512-Ep06yCaCdgG1Mafb/Rx8sJ1QS3RW2I2BxGp2Ui9LBHSZ2/tO/aGLc5WqPjgiAP6KAnLJGaIr/zzwQlOo1b8MxA== integrity sha512-d8+wsh5TfPwqVzbm4/HCXC783/KPHV60NvwitJnyTA5lWn1elhXMNWhXGCJ7PwPa8qFUnyJNIyuIRt2mT0WMug==
dependencies: dependencies:
"@ampproject/remapping" "^2.2.1" "@ampproject/remapping" "^2.2.1"
"@jridgewell/sourcemap-codec" "^1.4.15" "@jridgewell/sourcemap-codec" "^1.4.15"
@ -20958,9 +21015,9 @@ tiny-queue@^0.2.0:
integrity sha512-EijGsv7kzd9I9g0ByCl6h42BWNGUZrlCSejfrb3AKeHC33SGbASu1VDf5O3rRiiUOhAC9CHdZxFPbZu0HmR70A== integrity sha512-EijGsv7kzd9I9g0ByCl6h42BWNGUZrlCSejfrb3AKeHC33SGbASu1VDf5O3rRiiUOhAC9CHdZxFPbZu0HmR70A==
tinybench@^2.3.1: tinybench@^2.3.1:
version "2.6.0" version "2.5.1"
resolved "https://registry.yarnpkg.com/tinybench/-/tinybench-2.6.0.tgz#1423284ee22de07c91b3752c048d2764714b341b" resolved "https://registry.yarnpkg.com/tinybench/-/tinybench-2.5.1.tgz#3408f6552125e53a5a48adee31261686fd71587e"
integrity sha512-N8hW3PG/3aOoZAN5V/NSAEDz0ZixDSSt5b/a05iqtpgfLWMSVuCo7w0k2vVvEjdrIoeGqZzweX2WlyioNIHchA== integrity sha512-65NKvSuAVDP/n4CqH+a9w2kTlLReS9vhsAP06MWx+/89nMinJyB2icyl58RIcqCmIggpojIGeuJGhjU1aGMBSg==
tinycolor2@^1.6.0: tinycolor2@^1.6.0:
version "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" resolved "https://registry.yarnpkg.com/ufo/-/ufo-1.3.1.tgz#e085842f4627c41d4c1b60ebea1f75cdab4ce86b"
integrity sha512-uY/99gMLIOlJPwATcMVYfqDSxUR9//AUcgZMzwfSTJPDKzA1S8mX4VLqa+fiAtveraQUBCz4FFcwVZBGbwBXIw== 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: uglify-js@^3.1.4, uglify-js@^3.7.7:
version "3.17.4" version "3.17.4"
resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.17.4.tgz#61678cf5fa3f5b7eb789bb345df29afb8257c22c" 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== integrity sha512-+A5Sja4HP1M08MaXya7p5LvjuM7K6q/2EaC0+iovj/wOcMsTzMvDFbasi/oSapiwOlt252IqsKqPjCl7huKS0A==
undici-types@^6.0.1: undici-types@^6.0.1:
version "6.0.1" version "6.6.2"
resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-6.0.1.tgz#62e2af9fcd3ce359634175658de39df8d0f37197" resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-6.6.2.tgz#48c65d30bfcae492c3c89b1d147fed9d43a16b79"
integrity sha512-i9dNdkCziyqGpFxhatR9LITcInbFWh+ExlWkrZQpZHje8FfCcJKgps0IbmMd7D1o8c8syG4pIOV+aKIoC9JEyA== integrity sha512-acoBcoBobgsg3YUEO/Oht8JJCuFYpzWLFKbqEbcEZcXdkQrTzkF/yWj9JoLaFDa6ArI31dFEmNZkCjQZ7mlf7w==
undici-types@~5.26.4: undici-types@~5.26.4:
version "5.26.5" version "5.26.5"
@ -21464,9 +21516,9 @@ undici@^4.14.1:
integrity sha512-tkZSECUYi+/T1i4u+4+lwZmQgLXd4BLGlrc7KZPcLIW7Jpq99+Xpc30ONv7nS6F5UNOxp/HBZSSL9MafUrvJbw== integrity sha512-tkZSECUYi+/T1i4u+4+lwZmQgLXd4BLGlrc7KZPcLIW7Jpq99+Xpc30ONv7nS6F5UNOxp/HBZSSL9MafUrvJbw==
undici@^6.0.1: undici@^6.0.1:
version "6.0.1" version "6.6.2"
resolved "https://registry.yarnpkg.com/undici/-/undici-6.0.1.tgz#385572addca36d1c2b280629cb694b726170027e" resolved "https://registry.yarnpkg.com/undici/-/undici-6.6.2.tgz#8dce5ae54e8a3bc7140c2b2a0972b5fde9a88efb"
integrity sha512-eZFYQLeS9BiXpsU0cuFhCwfeda2MnC48EVmmOz/eCjsTgmyTdaHdVsPSC/kwC2GtW2e0uH0HIPbadf3/bRWSxw== integrity sha512-vSqvUE5skSxQJ5sztTZ/CdeJb1Wq0Hf44hlYMciqHghvz+K88U0l7D6u1VsndoFgskDcnU+nG3gYmMzJVzd9Qg==
dependencies: dependencies:
"@fastify/busboy" "^2.0.0" "@fastify/busboy" "^2.0.0"
@ -21790,18 +21842,7 @@ vite-plugin-static-copy@^0.17.0:
fs-extra "^11.1.0" fs-extra "^11.1.0"
picocolors "^1.0.0" picocolors "^1.0.0"
"vite@^3.0.0 || ^4.0.0": "vite@^3.0.0 || ^4.0.0", vite@^4.5.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:
version "4.5.0" version "4.5.0"
resolved "https://registry.yarnpkg.com/vite/-/vite-4.5.0.tgz#ec406295b4167ac3bc23e26f9c8ff559287cff26" resolved "https://registry.yarnpkg.com/vite/-/vite-4.5.0.tgz#ec406295b4167ac3bc23e26f9c8ff559287cff26"
integrity sha512-ulr8rNLA6rkyFAlVWw2q5YJ91v098AFQ2R0PRFwPzREXOUJQPtFUG0t+/ZikhaOCDqFoDhN6/v8Sq0o4araFAw== 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" resolved "https://registry.yarnpkg.com/yaml/-/yaml-1.10.2.tgz#2301c5ffbf12b467de8da2333a459e29e7920e4b"
integrity sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg== 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" version "2.3.4"
resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.3.4.tgz#53fc1d514be80aabf386dc6001eb29bf3b7523b2" resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.3.4.tgz#53fc1d514be80aabf386dc6001eb29bf3b7523b2"
integrity sha512-8aAvwVUSHpfEqTQ4w/KMlf3HcRdt50E5ODIQJBw1fQ5RL34xabzxtUlzTXVqc4rkZsPbvrXKWnABCD7kWSmocA== integrity sha512-8aAvwVUSHpfEqTQ4w/KMlf3HcRdt50E5ODIQJBw1fQ5RL34xabzxtUlzTXVqc4rkZsPbvrXKWnABCD7kWSmocA==