1
0
Fork 0
mirror of synced 2024-07-05 22:40:39 +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",
"packages": [
"packages/*",

View file

@ -11,6 +11,7 @@ import {
Document,
isDocument,
RowResponse,
RowValue,
} from "@budibase/types"
import { getCouchInfo } from "./connections"
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
): Promise<AllDocsResponse<T>> {
return this.performCall(db => {

View file

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

View file

@ -74,7 +74,7 @@ export function getGlobalIDFromUserMetadataID(id: string) {
* Generates a template ID.
* @param ownerId The owner/user of the template, this could be global or a workspace level.
*/
export function generateTemplateID(ownerId: any) {
export function generateTemplateID(ownerId: string) {
return `${DocumentType.TEMPLATE}${SEPARATOR}${ownerId}${SEPARATOR}${newid()}`
}
@ -105,7 +105,7 @@ export function prefixRoleID(name: string) {
* Generates a new dev info document ID - this is scoped to a user.
* @returns The new dev info ID which info for dev (like api key) can be stored under.
*/
export const generateDevInfoID = (userId: any) => {
export const generateDevInfoID = (userId: string) => {
return `${DocumentType.DEV_INFO}${SEPARATOR}${userId}`
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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 Updating } from "./Updating.svelte"
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);
--drop-shadow: rgba(0, 0, 0, 0.25) !important;
--spectrum-global-color-blue-100: rgba(35, 40, 50) !important;
--spectrum-alias-background-color-secondary: var(--spectrum-global-color-gray-75);
--spectrum-alias-background-color-primary: var(--spectrum-global-color-gray-100);
}

View file

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

View file

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

View file

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

View file

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

View file

@ -1,7 +1,10 @@
import { getDefinition, getDefinitions } from "../../integrations"
import { SourceName, UserCtx } from "@budibase/types"
const DISABLED_EXTERNAL_INTEGRATIONS = [SourceName.AIRTABLE]
const DISABLED_EXTERNAL_INTEGRATIONS = [
SourceName.AIRTABLE,
SourceName.BUDIBASE,
]
export async function fetch(ctx: UserCtx) {
const definitions = await getDefinitions()

View file

@ -1,9 +1,17 @@
import { EMPTY_LAYOUT } from "../../constants/layouts"
import { generateLayoutID, getScreenParams } from "../../db/utils"
import { events, context } from "@budibase/backend-core"
import { BBContext, Layout } from "@budibase/types"
import {
BBContext,
Layout,
SaveLayoutRequest,
SaveLayoutResponse,
UserCtx,
} from "@budibase/types"
export async function save(ctx: BBContext) {
export async function save(
ctx: UserCtx<SaveLayoutRequest, SaveLayoutResponse>
) {
const db = context.getAppDB()
let layout = ctx.request.body

View file

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

View file

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

View file

@ -7,7 +7,13 @@ import {
roles,
} from "@budibase/backend-core"
import { updateAppPackage } from "./application"
import { Plugin, ScreenProps, BBContext, Screen } from "@budibase/types"
import {
Plugin,
ScreenProps,
BBContext,
Screen,
UserCtx,
} from "@budibase/types"
import { builderSocket } from "../../websockets"
export async function fetch(ctx: BBContext) {
@ -31,7 +37,7 @@ export async function fetch(ctx: BBContext) {
)
}
export async function save(ctx: BBContext) {
export async function save(ctx: UserCtx<Screen, Screen>) {
const db = context.getAppDB()
let screen = ctx.request.body

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -85,7 +85,9 @@ async function getImportableDocuments(db: Database) {
const docPromises = []
for (let docType of DocumentTypesToImport) {
docPromises.push(
db.allDocs(dbCore.getDocParams(docType, null, { include_docs: true }))
db.allDocs<Document>(
dbCore.getDocParams(docType, null, { include_docs: true })
)
)
}
// 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({
email,
roles,
builder: builder || false,
admin: false,
builder: { global: builder || false },
admin: { global: false },
})
await context.doInContext(config.appId!, async () => {
await events.user.created(user)
@ -55,10 +55,10 @@ async function createUser(email: string, roles: UserRoles, builder?: boolean) {
async function removeUserRole(user: User) {
const final = await config.globalUser({
...user,
id: user._id,
_id: user._id,
roles: {},
builder: false,
admin: false,
builder: { global: false },
admin: { global: false },
})
await context.doInContext(config.appId!, async () => {
await events.user.updated(final)
@ -69,8 +69,8 @@ async function createGroupAndUser(email: string) {
groupUser = await config.createUser({
email,
roles: {},
builder: false,
admin: false,
builder: { global: false },
admin: { global: false },
})
group = await config.createGroup()
await config.addUserToGroup(group._id!, groupUser._id!)

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

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 "./searchFilter"
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
name?: string
source: SourceName
// this is a googlesheets specific property which
// can be found in the GSheets schema - pertains to SSO creds
auth?: { type: string }
// the config is defined by the schema
config?: Record<string, any>
plus?: boolean
@ -36,6 +39,12 @@ export interface RestAuthConfig {
config: RestBasicAuthConfig | RestBearerAuthConfig
}
export interface DynamicVariable {
name: string
queryId: string
value: string
}
export interface RestConfig {
url: string
rejectUnauthorized: boolean
@ -47,11 +56,5 @@ export interface RestConfig {
staticVariables: {
[key: string]: string
}
dynamicVariables: [
{
name: string
queryId: string
value: string
}
]
dynamicVariables: DynamicVariable[]
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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