1
0
Fork 0
mirror of synced 2024-07-08 15:56:23 +12:00

Merge branch 'master' into BUDI-8064/doc-writethrough

This commit is contained in:
Adria Navarro 2024-03-01 11:08:49 +01:00 committed by GitHub
commit 8611fade5e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
66 changed files with 755 additions and 830 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.20.14",
"npmClient": "yarn", "npmClient": "yarn",
"packages": [ "packages": [
"packages/*", "packages/*",

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"
@ -230,7 +231,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"
@ -86,7 +86,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

@ -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

@ -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

@ -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,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

@ -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

@ -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

@ -56,6 +56,7 @@ export enum SourceName {
FIRESTORE = "FIRESTORE", FIRESTORE = "FIRESTORE",
REDIS = "REDIS", REDIS = "REDIS",
SNOWFLAKE = "SNOWFLAKE", SNOWFLAKE = "SNOWFLAKE",
BUDIBASE = "BUDIBASE",
} }
export enum IncludeRelationship { export enum IncludeRelationship {

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 {
@ -136,7 +142,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

@ -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",