diff --git a/README.md b/README.md index 7d11ea570f..aa368d29fd 100644 --- a/README.md +++ b/README.md @@ -104,12 +104,14 @@ Budibase is made to scale. With Budibase, you can self-host on your own infrastr ## 🏁 Get started - + -Deploy Budibase self-Hosted in your existing infrastructure, using Docker, Kubernetes, and Digital Ocean. +Deploy Budibase self-hosted in your existing infrastructure, using Docker, Kubernetes, and Digital Ocean. Or use Budibase Cloud if you don't need to self-host, and would like to get started quickly. -### [Get started with Budibase](https://budibase.com) +### [Get started with self-hosting Budibase](https://docs.budibase.com/self-hosting/self-host) + +### [Get started with Budibase Cloud](https://budibase.com)

diff --git a/i18n/README.jp.md b/i18n/README.jp.md new file mode 100644 index 0000000000..6fea497d53 --- /dev/null +++ b/i18n/README.jp.md @@ -0,0 +1,214 @@ +

+ + Budibase + +

+

+ Budibase +

+ +

+ 使って楽しいローコードプラットフォーム +

+

+ Budibaseはオープンソースのローコードプラットフォームで、生産性を向上させるツールを簡単に構築することができます。 +

+ +

+ 🤖 🎨 🚀 +

+
+ +

+ Budibase design ui +

+ +

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

+ +

+ はじめに + · + ドキュメント + · + 機能リクエスト + · + バグ報告 + · + サポート: ディスカッション +

+ +

+## ✨ 特徴 + +### "本物"のソフトウェアを構築できます +ほかのプラットフォームとは違い、Budibaseだけでシングルページのアプリケーションを制作し完成させることができます。Budibaseで作られたアプリケーションは素晴らしいパフォーマンスを持っており、レスポンシブデザインにも対応しています。ユーザー達にいい印象を与えること間違いなしでしょう! +

+ +### 拡張性が高くオープンソース +Budibaseはオープンソースで、GPL v3ライセンスの下に公開されています。このことは、Budibaseが常にあなたのそばにいるという安心感を与えてくれることでしょう。そして、私たちは開発者に優しい環境を提供しているので、あなたは好きなだけにソースコードをフォークして改造、もしくは直接Budibaseにコントリビュートすることができます。 +

+ +### 既存のデータ、もしくは一から始める +Budibaseはいろんなツールから既存のデータを使用できます。たとえばMongoDB、CouchDB、 PostgreSQL、MySQL、Airtable、S3、DynamoDB、REST APIなど。ほかのプラットフォームにない特徴として、Budibaseはデータなしの状態でビジネスアプリケーションの構築を一から始めることができます。 [新しいデータリソースをリクエスト](https://github.com/Budibase/budibase/discussions?discussions_q=category%3AIdeas)。 + +

+ Budibase data +

+

+ +### パワフルな内蔵コンポーネントでアプリケーションを設計し構築 + +Budibaseには、美しくデザインされた強力なコンポーネントが付属しており、それら使用しUIを簡単に構築することができます。また、CSSによるスタイリングオプションも豊富に用意されているので、よりクリエイティブな表現もも可能です。 + [Request new component](https://github.com/Budibase/budibase/discussions?discussions_q=category%3AIdeas)。 + +

+ Budibase design +

+

+ +### プロセスを自動化し、ほかのツールと連携し、Webhookをでつながる! +定型化した作業を自動化して時間を節約しましょう。Webhookに接続、Eメールの自動送信など、すべてBudibaseに任せましょう。 こちらで簡単に [新しいオートメーションを作る](https://github.com/Budibase/automations)または[新しいオートメーションをリクエストすることができます](https://github.com/Budibase/budibase/discussions?discussions_q=category%3AIdeas)。 + +

+ Budibase automations +

+

+ +### 使い親しんだツールとの統合 +Budibaseは多くの人気ツールと統合されており、あなたのニーズに合わせたパーフェクトなアプリケーションを構築することができます。 + +

+ Budibase integrations +

+

+ +### 管理者のパラダイス +Budibaseはどんな規模のプロジェクトにも柔軟に対応できます。Budibaseを使えば、個人または組織のサーバーでセルフホスティングし、ユーザー、オンボーディング、SMTP、アプリ、グループ、テーマなどをひとまとめに管理することが可能です。また、ユーザーやグループにアプリポータルを提供し、グループ管理者にユーザー管理を委ねることも可能です。 +- プロモーションビデオを視聴する: https://youtu.be/xoljVpty_Kw + +


+ +## 🏁 始めましょう + + + +Docker、KubernetesもしくはDegital Oceanを使用しセルフホスティングするか、セルフホスティングに困難がある、もしくは今すぐ開始したい場合はBudibase Cloudを使用しすぐに始めましょう。 + +### [Budibaseをセルフホスティングする](https://docs.budibase.com/self-hosting/self-host) + +### [Budibase Cloudを使用する](https://budibase.com) + + +

+ +## 🎓 Budibaseを学ぶ + +Budibaseのドキュメント[はここです](https://docs.budibase.com)。 +
+ + +

+ +## 💬 コミュニティ + +もし何か問題がある、もしくはBudibaseコミュニティのほかのユーザーと交流したいのであれば私たちの[Github discussions](https://github.com/Budibase/budibase/discussions)までお越しください。 + +


+ + +## ❗ 行動規範 + +Budibase は、すべての人を歓迎し、多様で、ハラスメントのない環境を提供することに尽力しています。Budibase コミュニティに参加するすべての人たちが私たちの[**行動規範**](https://github.com/Budibase/budibase/blob/HEAD/.github/CODE_OF_CONDUCT.md)を遵守していただくことお願いします。必ず読んでください。 +
+ + +

+ + +## 🙌 Budibaseにコントリビュート + + +バグレポートからプルリクエストの作成まで、すべての貢献は感謝、そして歓迎されております。新しい新機能の実装やAPIの変更を計画している場合は、まずIssueを作成してください。これであなたの貴重な考えは私たちにも伝わり、無駄とはなりません。 + +### どこから始めるか混乱していますか? +ここはコントリビュートをはじめるための最適な場所です! [First time issues project](https://github.com/Budibase/budibase/projects/22). + +### リポジトリの構成 +Budibaseは、lernaによってmonorepo方式で管理されています。budibase パッケージのビルドと公開はlernaによって管理されています。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) - ブラウザ上で動作するモジュールで、JSONの定義を読み取り、そこから"生きている"Webアプリケーションを作成します。 + +- [packages/server](https://github.com/Budibase/budibase/tree/HEAD/packages/server) - budibaseのサーバーです。この Koa アプリは、builder アプリと budibase アプリの JS を提供し、データベースとファイル システムと対話するための API を提供する役割を担っています。 + +詳しくは[CONTRIBUTING.md](https://github.com/Budibase/budibase/blob/HEAD/.github/CONTRIBUTING.md)をご覧ください。 + +

+ + +## 📝 ライセンス + +Budibase はオープンソースであり、[GPL v3](https://www.gnu.org/licenses/gpl-3.0.en.html)ライセンスの下に公開されています。クライアントとコンポーネントライブラリは [MPL](https://directory.fsf.org/wiki/License:MPL-2.0)で公開されています - ですから、あなたが制作したアプリケーションはどのようなライセンスでも公開することができます。 + +

+ +## ⭐ スター数の履歴 + +[![Stargazers over time](https://starchart.cc/Budibase/budibase.svg)](https://starchart.cc/Budibase/budibase) + +ビルダーのアップデートの間に問題が発生する場合は[ここ](https://github.com/Budibase/budibase/blob/HEAD/.github/CONTRIBUTING.md#troubleshooting)を参考に環境をクリアにしてください。 + +

+ +## Contributors ✨ + +すばらしい皆さまに感謝しかありません。([emoji key](https://allcontributors.org/docs/en/emoji-key)): + + + + + + + + + + + + + + + + + + + + + + + + + +

Martin McKeaveney

💻 📖 ⚠️ 🚇

Michael Drury

📖 💻 ⚠️ 🚇

Andrew Kingston

📖 💻 ⚠️ 🎨

Michael Shanks

📖 💻 ⚠️

Kevin Åberg Kultalahti

📖 💻 ⚠️

Joe

📖 💻 🖋 🎨

Rory Powell

💻 📖 ⚠️

Peter Clement

💻 📖 ⚠️

Conor_Mack

💻 ⚠️

pngwn

💻 ⚠️

HugoLd

💻

victoriasloan

💻

yashank09

💻

SOVLOOKUP

💻

seoulaja

🌍

Maurits Lourens

⚠️ 💻
+ + + + + + +このプロジェクトは、[all-contributors](https://github.com/all-contributors/all-contributors)仕様に準拠しています。どのような貢献でも歓迎します。 + diff --git a/lerna.json b/lerna.json index 9e1abe4e79..b0db0bf381 100644 --- a/lerna.json +++ b/lerna.json @@ -1,5 +1,5 @@ { - "version": "1.0.49-alpha.5", + "version": "1.0.50-alpha.6", "npmClient": "yarn", "packages": [ "packages/*" diff --git a/packages/backend-core/context.js b/packages/backend-core/context.js new file mode 100644 index 0000000000..4bc100687d --- /dev/null +++ b/packages/backend-core/context.js @@ -0,0 +1,17 @@ +const { + getAppDB, + getDevAppDB, + getProdAppDB, + getAppId, + updateAppId, + doInAppContext, +} = require("./src/context") + +module.exports = { + getAppDB, + getDevAppDB, + getProdAppDB, + getAppId, + updateAppId, + doInAppContext, +} diff --git a/packages/backend-core/db.js b/packages/backend-core/db.js index 47854ca9c7..d2adf6c092 100644 --- a/packages/backend-core/db.js +++ b/packages/backend-core/db.js @@ -1,5 +1,6 @@ module.exports = { ...require("./src/db/utils"), ...require("./src/db/constants"), + ...require("./src/db"), ...require("./src/db/views"), } diff --git a/packages/backend-core/deprovision.js b/packages/backend-core/deprovision.js index b4b8dc6110..672da214ff 100644 --- a/packages/backend-core/deprovision.js +++ b/packages/backend-core/deprovision.js @@ -1 +1 @@ -module.exports = require("./src/tenancy/deprovision") +module.exports = require("./src/context/deprovision") diff --git a/packages/backend-core/package.json b/packages/backend-core/package.json index c416c2c4e3..55ca8c39c5 100644 --- a/packages/backend-core/package.json +++ b/packages/backend-core/package.json @@ -1,6 +1,6 @@ { "name": "@budibase/backend-core", - "version": "1.0.49-alpha.5", + "version": "1.0.50-alpha.6", "description": "Budibase backend core libraries used in server and worker", "main": "src/index.js", "author": "Budibase", diff --git a/packages/backend-core/src/tenancy/FunctionContext.js b/packages/backend-core/src/context/FunctionContext.js similarity index 70% rename from packages/backend-core/src/tenancy/FunctionContext.js rename to packages/backend-core/src/context/FunctionContext.js index d97a3a30b4..1a3f65056e 100644 --- a/packages/backend-core/src/tenancy/FunctionContext.js +++ b/packages/backend-core/src/context/FunctionContext.js @@ -4,8 +4,8 @@ const { newid } = require("../hashing") const REQUEST_ID_KEY = "requestId" class FunctionContext { - static getMiddleware(updateCtxFn = null) { - const namespace = this.createNamespace() + static getMiddleware(updateCtxFn = null, contextName = "session") { + const namespace = this.createNamespace(contextName) return async function (ctx, next) { await new Promise( @@ -24,14 +24,14 @@ class FunctionContext { } } - static run(callback) { - const namespace = this.createNamespace() + static run(callback, contextName = "session") { + const namespace = this.createNamespace(contextName) return namespace.runAndReturn(callback) } - static setOnContext(key, value) { - const namespace = this.createNamespace() + static setOnContext(key, value, contextName = "session") { + const namespace = this.createNamespace(contextName) namespace.set(key, value) } @@ -55,16 +55,16 @@ class FunctionContext { } } - static destroyNamespace() { + static destroyNamespace(name = "session") { if (this._namespace) { - cls.destroyNamespace("session") + cls.destroyNamespace(name) this._namespace = null } } - static createNamespace() { + static createNamespace(name = "session") { if (!this._namespace) { - this._namespace = cls.createNamespace("session") + this._namespace = cls.createNamespace(name) } return this._namespace } diff --git a/packages/backend-core/src/tenancy/deprovision.js b/packages/backend-core/src/context/deprovision.js similarity index 98% rename from packages/backend-core/src/tenancy/deprovision.js rename to packages/backend-core/src/context/deprovision.js index 608ca1b84a..1fbc2c8398 100644 --- a/packages/backend-core/src/tenancy/deprovision.js +++ b/packages/backend-core/src/context/deprovision.js @@ -1,6 +1,6 @@ const { getGlobalUserParams, getAllApps } = require("../db/utils") const { getDB, getCouch } = require("../db") -const { getGlobalDB } = require("./tenancy") +const { getGlobalDB } = require("../tenancy") const { StaticDatabases } = require("../db/constants") const TENANT_DOC = StaticDatabases.PLATFORM_INFO.docs.tenants diff --git a/packages/backend-core/src/context/index.js b/packages/backend-core/src/context/index.js new file mode 100644 index 0000000000..1c1238278e --- /dev/null +++ b/packages/backend-core/src/context/index.js @@ -0,0 +1,195 @@ +const env = require("../environment") +const { Headers } = require("../../constants") +const cls = require("./FunctionContext") +const { getCouch } = require("../db") +const { getProdAppID, getDevelopmentAppID } = require("../db/conversions") +const { isEqual } = require("lodash") + +// some test cases call functions directly, need to +// store an app ID to pretend there is a context +let TEST_APP_ID = null + +const ContextKeys = { + TENANT_ID: "tenantId", + APP_ID: "appId", + // whatever the request app DB was + CURRENT_DB: "currentDb", + // get the prod app DB from the request + PROD_DB: "prodDb", + // get the dev app DB from the request + DEV_DB: "devDb", + DB_OPTS: "dbOpts", +} + +exports.DEFAULT_TENANT_ID = "default" + +exports.isDefaultTenant = () => { + return exports.getTenantId() === exports.DEFAULT_TENANT_ID +} + +exports.isMultiTenant = () => { + return env.MULTI_TENANCY +} + +// used for automations, API endpoints should always be in context already +exports.doInTenant = (tenantId, task) => { + return cls.run(() => { + // set the tenant id + cls.setOnContext(ContextKeys.TENANT_ID, tenantId) + + // invoke the task + return task() + }) +} + +exports.doInAppContext = (appId, task) => { + return cls.run(() => { + // set the app ID + cls.setOnContext(ContextKeys.APP_ID, appId) + + // invoke the task + return task() + }) +} + +exports.updateTenantId = tenantId => { + cls.setOnContext(ContextKeys.TENANT_ID, tenantId) +} + +exports.updateAppId = appId => { + try { + cls.setOnContext(ContextKeys.APP_ID, appId) + cls.setOnContext(ContextKeys.PROD_DB, null) + cls.setOnContext(ContextKeys.DEV_DB, null) + cls.setOnContext(ContextKeys.CURRENT_DB, null) + cls.setOnContext(ContextKeys.DB_OPTS, null) + } catch (err) { + if (env.isTest()) { + TEST_APP_ID = appId + } else { + throw err + } + } +} + +exports.setTenantId = ( + ctx, + opts = { allowQs: false, allowNoTenant: false } +) => { + let tenantId + // exit early if not multi-tenant + if (!exports.isMultiTenant()) { + cls.setOnContext(ContextKeys.TENANT_ID, this.DEFAULT_TENANT_ID) + return + } + + const allowQs = opts && opts.allowQs + const allowNoTenant = opts && opts.allowNoTenant + const header = ctx.request.headers[Headers.TENANT_ID] + const user = ctx.user || {} + if (allowQs) { + const query = ctx.request.query || {} + tenantId = query.tenantId + } + // override query string (if allowed) by user, or header + // URL params cannot be used in a middleware, as they are + // processed later in the chain + tenantId = user.tenantId || header || tenantId + + // Set the tenantId from the subdomain + if (!tenantId) { + tenantId = ctx.subdomains && ctx.subdomains[0] + } + + if (!tenantId && !allowNoTenant) { + ctx.throw(403, "Tenant id not set") + } + // check tenant ID just incase no tenant was allowed + if (tenantId) { + cls.setOnContext(ContextKeys.TENANT_ID, tenantId) + } +} + +exports.isTenantIdSet = () => { + const tenantId = cls.getFromContext(ContextKeys.TENANT_ID) + return !!tenantId +} + +exports.getTenantId = () => { + if (!exports.isMultiTenant()) { + return exports.DEFAULT_TENANT_ID + } + const tenantId = cls.getFromContext(ContextKeys.TENANT_ID) + if (!tenantId) { + throw Error("Tenant id not found") + } + return tenantId +} + +exports.getAppId = () => { + const foundId = cls.getFromContext(ContextKeys.APP_ID) + if (!foundId && env.isTest() && TEST_APP_ID) { + return TEST_APP_ID + } else { + return foundId + } +} + +function getDB(key, opts) { + const dbOptsKey = `${key}${ContextKeys.DB_OPTS}` + let storedOpts = cls.getFromContext(dbOptsKey) + let db = cls.getFromContext(key) + if (db && isEqual(opts, storedOpts)) { + return db + } + const appId = exports.getAppId() + const CouchDB = getCouch() + let toUseAppId + switch (key) { + case ContextKeys.CURRENT_DB: + toUseAppId = appId + break + case ContextKeys.PROD_DB: + toUseAppId = getProdAppID(appId) + break + case ContextKeys.DEV_DB: + toUseAppId = getDevelopmentAppID(appId) + break + } + db = new CouchDB(toUseAppId, opts) + try { + cls.setOnContext(key, db) + if (opts) { + cls.setOnContext(dbOptsKey, opts) + } + } catch (err) { + if (!env.isTest()) { + throw err + } + } + return db +} + +/** + * Opens the app database based on whatever the request + * contained, dev or prod. + */ +exports.getAppDB = opts => { + return getDB(ContextKeys.CURRENT_DB, opts) +} + +/** + * This specifically gets the prod app ID, if the request + * contained a development app ID, this will open the prod one. + */ +exports.getProdAppDB = opts => { + return getDB(ContextKeys.PROD_DB, opts) +} + +/** + * This specifically gets the dev app ID, if the request + * contained a prod app ID, this will open the dev one. + */ +exports.getDevAppDB = opts => { + return getDB(ContextKeys.DEV_DB, opts) +} diff --git a/packages/backend-core/src/db/constants.js b/packages/backend-core/src/db/constants.js index 2affb09c7c..b41a9a9c08 100644 --- a/packages/backend-core/src/db/constants.js +++ b/packages/backend-core/src/db/constants.js @@ -32,3 +32,7 @@ exports.StaticDatabases = { }, }, } + +exports.APP_PREFIX = exports.DocumentTypes.APP + exports.SEPARATOR +exports.APP_DEV = exports.APP_DEV_PREFIX = + exports.DocumentTypes.APP_DEV + exports.SEPARATOR diff --git a/packages/backend-core/src/db/conversions.js b/packages/backend-core/src/db/conversions.js new file mode 100644 index 0000000000..50d896322f --- /dev/null +++ b/packages/backend-core/src/db/conversions.js @@ -0,0 +1,46 @@ +const NO_APP_ERROR = "No app provided" +const { APP_DEV_PREFIX, APP_PREFIX } = require("./constants") + +exports.isDevAppID = appId => { + if (!appId) { + throw NO_APP_ERROR + } + return appId.startsWith(APP_DEV_PREFIX) +} + +exports.isProdAppID = appId => { + if (!appId) { + throw NO_APP_ERROR + } + return appId.startsWith(APP_PREFIX) && !exports.isDevAppID(appId) +} + +exports.isDevApp = app => { + if (!app) { + throw NO_APP_ERROR + } + return exports.isDevAppID(app.appId) +} + +/** + * Convert a development app ID to a deployed app ID. + */ +exports.getProdAppID = appId => { + // if dev, convert it + if (appId.startsWith(APP_DEV_PREFIX)) { + const id = appId.split(APP_DEV_PREFIX)[1] + return `${APP_PREFIX}${id}` + } + return appId +} + +/** + * Convert a deployed app ID to a development app ID. + */ +exports.getDevelopmentAppID = appId => { + if (!appId.startsWith(APP_DEV_PREFIX)) { + const id = appId.split(APP_PREFIX)[1] + return `${APP_DEV_PREFIX}${id}` + } + return appId +} diff --git a/packages/backend-core/src/db/utils.js b/packages/backend-core/src/db/utils.js index 2bc5462646..f5ea2f8486 100644 --- a/packages/backend-core/src/db/utils.js +++ b/packages/backend-core/src/db/utils.js @@ -2,7 +2,13 @@ const { newid } = require("../hashing") const Replication = require("./Replication") const { DEFAULT_TENANT_ID, Configs } = require("../constants") const env = require("../environment") -const { StaticDatabases, SEPARATOR, DocumentTypes } = require("./constants") +const { + StaticDatabases, + SEPARATOR, + DocumentTypes, + APP_PREFIX, + APP_DEV, +} = require("./constants") const { getTenantId, getTenantIDFromAppID, @@ -12,8 +18,13 @@ const fetch = require("node-fetch") const { getCouch } = require("./index") const { getAppMetadata } = require("../cache/appMetadata") const { checkSlashesInUrl } = require("../helpers") - -const NO_APP_ERROR = "No app provided" +const { + isDevApp, + isProdAppID, + isDevAppID, + getDevelopmentAppID, + getProdAppID, +} = require("./conversions") const UNICODE_MAX = "\ufff0" @@ -24,10 +35,15 @@ exports.ViewNames = { exports.StaticDatabases = StaticDatabases exports.DocumentTypes = DocumentTypes -exports.APP_PREFIX = DocumentTypes.APP + SEPARATOR -exports.APP_DEV = exports.APP_DEV_PREFIX = DocumentTypes.APP_DEV + SEPARATOR +exports.APP_PREFIX = APP_PREFIX +exports.APP_DEV = exports.APP_DEV_PREFIX = APP_DEV exports.SEPARATOR = SEPARATOR exports.getTenantIDFromAppID = getTenantIDFromAppID +exports.isDevApp = isDevApp +exports.isProdAppID = isProdAppID +exports.isDevAppID = isDevAppID +exports.getDevelopmentAppID = getDevelopmentAppID +exports.getProdAppID = getProdAppID /** * If creating DB allDocs/query params with only a single top level ID this can be used, this @@ -52,27 +68,6 @@ function getDocParams(docType, docId = null, otherProps = {}) { } } -exports.isDevAppID = appId => { - if (!appId) { - throw NO_APP_ERROR - } - return appId.startsWith(exports.APP_DEV_PREFIX) -} - -exports.isProdAppID = appId => { - if (!appId) { - throw NO_APP_ERROR - } - return appId.startsWith(exports.APP_PREFIX) && !exports.isDevAppID(appId) -} - -function isDevApp(app) { - if (!app) { - throw NO_APP_ERROR - } - return exports.isDevAppID(app.appId) -} - /** * Generates a new workspace ID. * @returns {string} The new workspace ID which the workspace doc can be stored under. @@ -157,29 +152,6 @@ exports.getRoleParams = (roleId = null, otherProps = {}) => { return getDocParams(DocumentTypes.ROLE, roleId, otherProps) } -/** - * Convert a development app ID to a deployed app ID. - */ -exports.getDeployedAppID = appId => { - // if dev, convert it - if (appId.startsWith(exports.APP_DEV_PREFIX)) { - const id = appId.split(exports.APP_DEV_PREFIX)[1] - return `${exports.APP_PREFIX}${id}` - } - return appId -} - -/** - * Convert a deployed app ID to a development app ID. - */ -exports.getDevelopmentAppID = appId => { - if (!appId.startsWith(exports.APP_DEV_PREFIX)) { - const id = appId.split(exports.APP_PREFIX)[1] - return `${exports.APP_DEV_PREFIX}${id}` - } - return appId -} - exports.getCouchUrl = () => { if (!env.COUCH_DB_URL) return @@ -225,7 +197,7 @@ exports.getAllDbs = async () => { } let couchUrl = `${exports.getCouchUrl()}/_all_dbs` let tenantId = getTenantId() - if (!env.MULTI_TENANCY || tenantId == DEFAULT_TENANT_ID) { + if (!env.MULTI_TENANCY || tenantId === DEFAULT_TENANT_ID) { // just get all DBs when: // - single tenancy // - default tenant @@ -250,11 +222,11 @@ exports.getAllDbs = async () => { /** * Lots of different points in the system need to find the full list of apps, this will * enumerate the entire CouchDB cluster and get the list of databases (every app). - * NOTE: this operation is fine in self hosting, but cannot be used when hosting many - * different users/companies apps as there is no security around it - all apps are returned. + * * @return {Promise} returns the app information document stored in each app database. */ -exports.getAllApps = async (CouchDB, { dev, all, idsOnly } = {}) => { +exports.getAllApps = async ({ dev, all, idsOnly } = {}) => { + const CouchDB = getCouch() let tenantId = getTenantId() if (!env.MULTI_TENANCY && !tenantId) { tenantId = DEFAULT_TENANT_ID @@ -310,8 +282,8 @@ exports.getAllApps = async (CouchDB, { dev, all, idsOnly } = {}) => { /** * Utility function for getAllApps but filters to production apps only. */ -exports.getDeployedAppIDs = async CouchDB => { - return (await exports.getAllApps(CouchDB, { idsOnly: true })).filter( +exports.getProdAppIDs = async () => { + return (await exports.getAllApps({ idsOnly: true })).filter( id => !exports.isDevAppID(id) ) } @@ -319,13 +291,14 @@ exports.getDeployedAppIDs = async CouchDB => { /** * Utility function for the inverse of above. */ -exports.getDevAppIDs = async CouchDB => { - return (await exports.getAllApps(CouchDB, { idsOnly: true })).filter(id => +exports.getDevAppIDs = async () => { + return (await exports.getAllApps({ idsOnly: true })).filter(id => exports.isDevAppID(id) ) } -exports.dbExists = async (CouchDB, dbName) => { +exports.dbExists = async dbName => { + const CouchDB = getCouch() let exists = false try { const db = CouchDB(dbName, { skip_setup: true }) diff --git a/packages/backend-core/src/middleware/appTenancy.js b/packages/backend-core/src/middleware/appTenancy.js index 30fc4f7453..b0430a0051 100644 --- a/packages/backend-core/src/middleware/appTenancy.js +++ b/packages/backend-core/src/middleware/appTenancy.js @@ -3,8 +3,9 @@ const { updateTenantId, isTenantIdSet, DEFAULT_TENANT_ID, + updateAppId, } = require("../tenancy") -const ContextFactory = require("../tenancy/FunctionContext") +const ContextFactory = require("../context/FunctionContext") const { getTenantIDFromAppID } = require("../db/utils") module.exports = () => { @@ -21,5 +22,6 @@ module.exports = () => { const appId = ctx.appId ? ctx.appId : ctx.user ? ctx.user.appId : null const tenantId = getTenantIDFromAppID(appId) || DEFAULT_TENANT_ID updateTenantId(tenantId) + updateAppId(appId) }) } diff --git a/packages/backend-core/src/middleware/passport/local.js b/packages/backend-core/src/middleware/passport/local.js index f95c3a173e..2149bd3e18 100644 --- a/packages/backend-core/src/middleware/passport/local.js +++ b/packages/backend-core/src/middleware/passport/local.js @@ -8,7 +8,7 @@ const { newid } = require("../../hashing") const { createASession } = require("../../security/sessions") const { getTenantId } = require("../../tenancy") -const INVALID_ERR = "Invalid Credentials" +const INVALID_ERR = "Invalid credentials" const SSO_NO_PASSWORD = "SSO user does not have a password set" const EXPIRED = "This account has expired. Please reset your password" diff --git a/packages/backend-core/src/middleware/tenancy.js b/packages/backend-core/src/middleware/tenancy.js index adfd36a503..5bb81f8824 100644 --- a/packages/backend-core/src/middleware/tenancy.js +++ b/packages/backend-core/src/middleware/tenancy.js @@ -1,5 +1,5 @@ const { setTenantId } = require("../tenancy") -const ContextFactory = require("../tenancy/FunctionContext") +const ContextFactory = require("../context/FunctionContext") const { buildMatcherRegex, matches } = require("./matchers") module.exports = ( diff --git a/packages/backend-core/src/security/roles.js b/packages/backend-core/src/security/roles.js index 8529dde6f4..11abc70bdd 100644 --- a/packages/backend-core/src/security/roles.js +++ b/packages/backend-core/src/security/roles.js @@ -1,4 +1,3 @@ -const { getDB } = require("../db") const { cloneDeep } = require("lodash/fp") const { BUILTIN_PERMISSION_IDS } = require("./permissions") const { @@ -7,6 +6,8 @@ const { DocumentTypes, SEPARATOR, } = require("../db/utils") +const { getAppDB } = require("../context") +const { getDB } = require("../db") const BUILTIN_IDS = { ADMIN: "ADMIN", @@ -111,11 +112,10 @@ exports.lowerBuiltinRoleID = (roleId1, roleId2) => { /** * Gets the role object, this is mainly useful for two purposes, to check if the level exists and * to check if the role inherits any others. - * @param {string} appId The app in which to look for the role. * @param {string|null} roleId The level ID to lookup. * @returns {Promise} The role object, which may contain an "inherits" property. */ -exports.getRole = async (appId, roleId) => { +exports.getRole = async roleId => { if (!roleId) { return null } @@ -128,7 +128,7 @@ exports.getRole = async (appId, roleId) => { ) } try { - const db = getDB(appId) + const db = getAppDB() const dbRole = await db.get(exports.getDBRoleID(roleId)) role = Object.assign(role, dbRole) // finalise the ID @@ -145,11 +145,12 @@ exports.getRole = async (appId, roleId) => { /** * Simple function to get all the roles based on the top level user role ID. */ -async function getAllUserRoles(appId, userRoleId) { - if (!userRoleId) { - return [BUILTIN_IDS.BASIC] +async function getAllUserRoles(userRoleId) { + // admins have access to all roles + if (userRoleId === BUILTIN_IDS.ADMIN) { + return exports.getAllRoles() } - let currentRole = await exports.getRole(appId, userRoleId) + let currentRole = await exports.getRole(userRoleId) let roles = currentRole ? [currentRole] : [] let roleIds = [userRoleId] // get all the inherited roles @@ -159,7 +160,7 @@ async function getAllUserRoles(appId, userRoleId) { roleIds.indexOf(currentRole.inherits) === -1 ) { roleIds.push(currentRole.inherits) - currentRole = await exports.getRole(appId, currentRole.inherits) + currentRole = await exports.getRole(currentRole.inherits) roles.push(currentRole) } return roles @@ -168,29 +169,23 @@ async function getAllUserRoles(appId, userRoleId) { /** * Returns an ordered array of the user's inherited role IDs, this can be used * to determine if a user can access something that requires a specific role. - * @param {string} appId The ID of the application from which roles should be obtained. * @param {string} userRoleId The user's role ID, this can be found in their access token. * @param {object} opts Various options, such as whether to only retrieve the IDs (default true). * @returns {Promise} returns an ordered array of the roles, with the first being their * highest level of access and the last being the lowest level. */ -exports.getUserRoleHierarchy = async ( - appId, - userRoleId, - opts = { idOnly: true } -) => { +exports.getUserRoleHierarchy = async (userRoleId, opts = { idOnly: true }) => { // special case, if they don't have a role then they are a public user - const roles = await getAllUserRoles(appId, userRoleId) + const roles = await getAllUserRoles(userRoleId) return opts.idOnly ? roles.map(role => role._id) : roles } /** * Given an app ID this will retrieve all of the roles that are currently within that app. - * @param {string} appId The ID of the app to retrieve the roles from. * @return {Promise} An array of the role objects that were found. */ exports.getAllRoles = async appId => { - const db = getDB(appId) + const db = appId ? getDB(appId) : getAppDB() const body = await db.allDocs( getRoleParams(null, { include_docs: true, @@ -218,19 +213,17 @@ exports.getAllRoles = async appId => { } /** - * This retrieves the required role/ - * @param appId + * This retrieves the required role * @param permLevel * @param resourceId * @param subResourceId * @return {Promise<{permissions}|Object>} */ exports.getRequiredResourceRole = async ( - appId, permLevel, { resourceId, subResourceId } ) => { - const roles = await exports.getAllRoles(appId) + const roles = await exports.getAllRoles() let main = [], sub = [] for (let role of roles) { @@ -251,8 +244,7 @@ exports.getRequiredResourceRole = async ( } class AccessController { - constructor(appId) { - this.appId = appId + constructor() { this.userHierarchies = {} } @@ -270,7 +262,7 @@ class AccessController { } let roleIds = this.userHierarchies[userRoleId] if (!roleIds) { - roleIds = await exports.getUserRoleHierarchy(this.appId, userRoleId) + roleIds = await exports.getUserRoleHierarchy(userRoleId) this.userHierarchies[userRoleId] = roleIds } diff --git a/packages/backend-core/src/tenancy/context.js b/packages/backend-core/src/tenancy/context.js deleted file mode 100644 index 01d1fdc604..0000000000 --- a/packages/backend-core/src/tenancy/context.js +++ /dev/null @@ -1,84 +0,0 @@ -const env = require("../environment") -const { Headers } = require("../../constants") -const cls = require("./FunctionContext") - -exports.DEFAULT_TENANT_ID = "default" - -exports.isDefaultTenant = () => { - return exports.getTenantId() === exports.DEFAULT_TENANT_ID -} - -exports.isMultiTenant = () => { - return env.MULTI_TENANCY -} - -const TENANT_ID = "tenantId" - -// used for automations, API endpoints should always be in context already -exports.doInTenant = (tenantId, task) => { - return cls.run(() => { - // set the tenant id - cls.setOnContext(TENANT_ID, tenantId) - - // invoke the task - return task() - }) -} - -exports.updateTenantId = tenantId => { - cls.setOnContext(TENANT_ID, tenantId) -} - -exports.setTenantId = ( - ctx, - opts = { allowQs: false, allowNoTenant: false } -) => { - let tenantId - // exit early if not multi-tenant - if (!exports.isMultiTenant()) { - cls.setOnContext(TENANT_ID, this.DEFAULT_TENANT_ID) - return - } - - const allowQs = opts && opts.allowQs - const allowNoTenant = opts && opts.allowNoTenant - const header = ctx.request.headers[Headers.TENANT_ID] - const user = ctx.user || {} - if (allowQs) { - const query = ctx.request.query || {} - tenantId = query.tenantId - } - // override query string (if allowed) by user, or header - // URL params cannot be used in a middleware, as they are - // processed later in the chain - tenantId = user.tenantId || header || tenantId - - // Set the tenantId from the subdomain - if (!tenantId) { - tenantId = ctx.subdomains && ctx.subdomains[0] - } - - if (!tenantId && !allowNoTenant) { - ctx.throw(403, "Tenant id not set") - } - // check tenant ID just incase no tenant was allowed - if (tenantId) { - cls.setOnContext(TENANT_ID, tenantId) - } -} - -exports.isTenantIdSet = () => { - const tenantId = cls.getFromContext(TENANT_ID) - return !!tenantId -} - -exports.getTenantId = () => { - if (!exports.isMultiTenant()) { - return exports.DEFAULT_TENANT_ID - } - const tenantId = cls.getFromContext(TENANT_ID) - if (!tenantId) { - throw Error("Tenant id not found") - } - return tenantId -} diff --git a/packages/backend-core/src/tenancy/index.js b/packages/backend-core/src/tenancy/index.js index 2fe257d885..c847033a12 100644 --- a/packages/backend-core/src/tenancy/index.js +++ b/packages/backend-core/src/tenancy/index.js @@ -1,4 +1,4 @@ module.exports = { - ...require("./context"), + ...require("../context"), ...require("./tenancy"), } diff --git a/packages/backend-core/src/tenancy/tenancy.js b/packages/backend-core/src/tenancy/tenancy.js index de597eac01..8360198b60 100644 --- a/packages/backend-core/src/tenancy/tenancy.js +++ b/packages/backend-core/src/tenancy/tenancy.js @@ -1,6 +1,6 @@ const { getDB } = require("../db") const { SEPARATOR, StaticDatabases, DocumentTypes } = require("../db/constants") -const { getTenantId, DEFAULT_TENANT_ID, isMultiTenant } = require("./context") +const { getTenantId, DEFAULT_TENANT_ID, isMultiTenant } = require("../context") const env = require("../environment") const TENANT_DOC = StaticDatabases.PLATFORM_INFO.docs.tenants diff --git a/packages/backend-core/src/utils.js b/packages/backend-core/src/utils.js index 6c71c51b9d..45fb4acd55 100644 --- a/packages/backend-core/src/utils.js +++ b/packages/backend-core/src/utils.js @@ -256,7 +256,7 @@ exports.saveUser = async ( exports.platformLogout = async ({ ctx, userId, keepActiveSession }) => { if (!ctx) throw new Error("Koa context must be supplied to logout.") - const currentSession = this.getCookie(ctx, Cookies.Auth) + const currentSession = exports.getCookie(ctx, Cookies.Auth) let sessions = await getUserSessions(userId) if (keepActiveSession) { @@ -265,8 +265,8 @@ exports.platformLogout = async ({ ctx, userId, keepActiveSession }) => { ) } else { // clear cookies - this.clearCookie(ctx, Cookies.Auth) - this.clearCookie(ctx, Cookies.CurrentApp) + exports.clearCookie(ctx, Cookies.Auth) + exports.clearCookie(ctx, Cookies.CurrentApp) } await invalidateSessions( diff --git a/packages/bbui/package.json b/packages/bbui/package.json index 0f3ba64508..9fb080e8c7 100644 --- a/packages/bbui/package.json +++ b/packages/bbui/package.json @@ -1,7 +1,7 @@ { "name": "@budibase/bbui", "description": "A UI solution used in the different Budibase projects.", - "version": "1.0.49-alpha.5", + "version": "1.0.50-alpha.6", "license": "MPL-2.0", "svelte": "src/index.js", "module": "dist/bbui.es.js", @@ -79,6 +79,7 @@ "@spectrum-css/underlay": "^2.0.9", "@spectrum-css/vars": "^3.0.1", "dayjs": "^1.10.4", + "easymde": "^2.16.1", "svelte-flatpickr": "^3.2.3", "svelte-portal": "^1.0.0" }, diff --git a/packages/bbui/src/Button/Button.svelte b/packages/bbui/src/Button/Button.svelte index da4d405f02..67930b8030 100644 --- a/packages/bbui/src/Button/Button.svelte +++ b/packages/bbui/src/Button/Button.svelte @@ -1,5 +1,6 @@ - + {#if showTooltip && tooltip} +
+
+ +
+
{/if} - {#if $$slots} - - {/if} - + diff --git a/packages/bbui/src/ColorPicker/ColorPicker.svelte b/packages/bbui/src/ColorPicker/ColorPicker.svelte index ff6a292d1b..1fa950fadc 100644 --- a/packages/bbui/src/ColorPicker/ColorPicker.svelte +++ b/packages/bbui/src/ColorPicker/ColorPicker.svelte @@ -5,7 +5,7 @@ import { fly } from "svelte/transition" import Icon from "../Icon/Icon.svelte" import Input from "../Form/Input.svelte" - import { capitalise } from "../utils/helpers" + import { capitalise } from "../helpers" export let value export let size = "M" diff --git a/packages/bbui/src/Form/Core/DatePicker.svelte b/packages/bbui/src/Form/Core/DatePicker.svelte index 8edb68a38e..c1c4cc866f 100644 --- a/packages/bbui/src/Form/Core/DatePicker.svelte +++ b/packages/bbui/src/Form/Core/DatePicker.svelte @@ -5,7 +5,7 @@ import "@spectrum-css/textfield/dist/index-vars.css" import "@spectrum-css/picker/dist/index-vars.css" import { createEventDispatcher } from "svelte" - import { generateID } from "../../utils/helpers" + import { uuid } from "../../helpers" export let id = null export let disabled = false @@ -14,16 +14,20 @@ export let value = null export let placeholder = null export let appendTo = undefined + export let timeOnly = false const dispatch = createEventDispatcher() - const flatpickrId = `${generateID()}-wrapper` + const flatpickrId = `${uuid()}-wrapper` let open = false - let flatpickr + let flatpickr, flatpickrOptions, isTimeOnly + + $: isTimeOnly = !timeOnly && value ? !isNaN(new Date(`0-${value}`)) : timeOnly $: flatpickrOptions = { element: `#${flatpickrId}`, - enableTime: enableTime || false, + enableTime: isTimeOnly || enableTime || false, + noCalendar: isTimeOnly || false, altInput: true, - altFormat: enableTime ? "F j Y, H:i" : "F j, Y", + altFormat: isTimeOnly ? "H:i" : enableTime ? "F j Y, H:i" : "F j, Y", wrap: true, appendTo, disableMobile: "true", @@ -35,6 +39,11 @@ if (newValue) { newValue = newValue.toISOString() } + // if time only set date component to today + if (timeOnly) { + const todayDate = new Date().toISOString().split("T")[0] + newValue = `${todayDate}T${newValue.split("T")[1]}` + } dispatch("change", newValue) } @@ -67,7 +76,11 @@ return null } let date - if (val instanceof Date) { + let time = new Date(`0-${val}`) + // it is a string like 00:00:00, just time + if (timeOnly || (typeof val === "string" && !isNaN(time))) { + date = time + } else if (val instanceof Date) { // Use real date obj if already parsed date = val } else if (isNaN(val)) { @@ -77,7 +90,7 @@ // Treat as numerical timestamp date = new Date(parseInt(val)) } - const time = date.getTime() + time = date.getTime() if (isNaN(time)) { return null } @@ -88,69 +101,71 @@ } - -
- {#if !!error} +
+ {#if !!error} + + {/if} + +
+
- -
-
+ +{/key} {#if open}
{/if} diff --git a/packages/bbui/src/Form/Core/Dropzone.svelte b/packages/bbui/src/Form/Core/Dropzone.svelte index 6b8022a36c..d739e751c9 100644 --- a/packages/bbui/src/Form/Core/Dropzone.svelte +++ b/packages/bbui/src/Form/Core/Dropzone.svelte @@ -3,7 +3,7 @@ import "@spectrum-css/typography/dist/index-vars.css" import "@spectrum-css/illustratedmessage/dist/index-vars.css" import { createEventDispatcher } from "svelte" - import { generateID } from "../../utils/helpers" + import { uuid } from "../../helpers" import Icon from "../../Icon/Icon.svelte" import Link from "../../Link/Link.svelte" import Tag from "../../Tags/Tag.svelte" @@ -37,7 +37,7 @@ "jfif", ] - const fieldId = id || generateID() + const fieldId = id || uuid() let selectedImageIdx = 0 let fileDragged = false let selectedUrl diff --git a/packages/bbui/src/Form/Core/RichTextField.svelte b/packages/bbui/src/Form/Core/RichTextField.svelte new file mode 100644 index 0000000000..f964405f0d --- /dev/null +++ b/packages/bbui/src/Form/Core/RichTextField.svelte @@ -0,0 +1,42 @@ + + +
+ +
+ + diff --git a/packages/bbui/src/Form/Core/TextArea.svelte b/packages/bbui/src/Form/Core/TextArea.svelte index a022a98e5f..465212cd44 100644 --- a/packages/bbui/src/Form/Core/TextArea.svelte +++ b/packages/bbui/src/Form/Core/TextArea.svelte @@ -13,6 +13,7 @@ start: textarea.selectionStart, end: textarea.selectionEnd, }) + export let align = null let focus = false let textarea @@ -21,11 +22,23 @@ dispatch("change", event.target.value) focus = false } + + const getStyleString = (attribute, value) => { + if (!attribute || value == null) { + return "" + } + if (isNaN(value)) { + return `${attribute}:${value};` + } + return `${attribute}:${value}px;` + } + + $: heightString = getStyleString("height", height) + $: minHeightString = getStyleString("min-height", minHeight)
(focus = true)} diff --git a/packages/bbui/src/Form/Core/TextField.svelte b/packages/bbui/src/Form/Core/TextField.svelte index d2064ddde0..78b698eed2 100644 --- a/packages/bbui/src/Form/Core/TextField.svelte +++ b/packages/bbui/src/Form/Core/TextField.svelte @@ -12,6 +12,7 @@ export let updateOnChange = true export let quiet = false export let dataCy + export let align const dispatch = createEventDispatcher() let focus = false @@ -92,8 +93,9 @@ on:input={onInput} on:keyup={updateValueOnEnter} {type} - inputmode={type === "number" ? "decimal" : "text"} class="spectrum-Textfield-input" + style={align ? `text-align: ${align};` : ""} + inputmode={type === "number" ? "decimal" : "text"} />
diff --git a/packages/bbui/src/Form/Core/index.js b/packages/bbui/src/Form/Core/index.js index 440c4a1b15..3c3f9acb4d 100644 --- a/packages/bbui/src/Form/Core/index.js +++ b/packages/bbui/src/Form/Core/index.js @@ -10,3 +10,4 @@ export { default as CoreSearch } from "./Search.svelte" export { default as CoreDatePicker } from "./DatePicker.svelte" export { default as CoreDropzone } from "./Dropzone.svelte" export { default as CoreStepper } from "./Stepper.svelte" +export { default as CoreRichTextField } from "./RichTextField.svelte" diff --git a/packages/bbui/src/Form/DatePicker.svelte b/packages/bbui/src/Form/DatePicker.svelte index 7d5656a22d..9298c49177 100644 --- a/packages/bbui/src/Form/DatePicker.svelte +++ b/packages/bbui/src/Form/DatePicker.svelte @@ -9,6 +9,7 @@ export let disabled = false export let error = null export let enableTime = true + export let timeOnly = false export let placeholder = null export let appendTo = undefined @@ -27,6 +28,7 @@ {value} {placeholder} {enableTime} + {timeOnly} {appendTo} on:change={onChange} /> diff --git a/packages/bbui/src/Form/RichTextField.svelte b/packages/bbui/src/Form/RichTextField.svelte new file mode 100644 index 0000000000..275242df49 --- /dev/null +++ b/packages/bbui/src/Form/RichTextField.svelte @@ -0,0 +1,36 @@ + + + + + diff --git a/packages/bbui/src/Markdown/MarkdownEditor.svelte b/packages/bbui/src/Markdown/MarkdownEditor.svelte new file mode 100644 index 0000000000..7fb6414ad8 --- /dev/null +++ b/packages/bbui/src/Markdown/MarkdownEditor.svelte @@ -0,0 +1,60 @@ + + +{#key height} + +{/key} diff --git a/packages/bbui/src/Markdown/MarkdownViewer.svelte b/packages/bbui/src/Markdown/MarkdownViewer.svelte new file mode 100644 index 0000000000..5705020f45 --- /dev/null +++ b/packages/bbui/src/Markdown/MarkdownViewer.svelte @@ -0,0 +1,70 @@ + + +
+ +
+ + diff --git a/packages/bbui/src/Markdown/SpectrumMDE.svelte b/packages/bbui/src/Markdown/SpectrumMDE.svelte new file mode 100644 index 0000000000..9b0832c91f --- /dev/null +++ b/packages/bbui/src/Markdown/SpectrumMDE.svelte @@ -0,0 +1,184 @@ + + +
+