diff --git a/i18n/README.kr.md b/i18n/README.kr.md
new file mode 100644
index 0000000000..09fc83569b
--- /dev/null
+++ b/i18n/README.kr.md
@@ -0,0 +1,221 @@
+
+
+
+
+
+
+ Budibase
+
+
+ 자체 인프라에서 몇 분 만에 맞춤형 비즈니스 도구를 구축하세요.
+
+
+ Budibase는 개발자와 IT 전문가가 몇 분 만에 맞춤형 애플리케이션을 구축하고 자동화할 수 있는 오픈 소스 로우코드 플랫폼입니다.
+
+
+
+ 🤖 🎨 🚀
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 소개
+ ·
+ 문서
+ ·
+ 기능 요청
+ ·
+ 버그 보고
+ ·
+ 지원: 토론
+
+
+
+## ✨ 특징
+
+### "실제" 소프트웨어를 구축할 수 있습니다.
+Budibase를 사용하면 고성능 단일 페이지 애플리케이션을 구축할 수 있습니다. 또한 반응형 디자인으로 제작하여 사용자에게 멋진 경험을 제공할 수 있습니다.
+
+
+### 오픈 소스 및 확장성
+Budibase는 오픈소스이며, GPL v3 라이선스에 따라 공개되어 있습니다. 이는 Budibase가 항상 당신 곁에 있다는 안도감을 줄 것입니다. 그리고 우리는 개발자 친화적인 환경을 제공하고 있기 때문에, 당신은 원하는 만큼 소스 코드를 포크하여 수정하거나 Budibase에 직접 기여할 수 있습니다.
+
+
+### 기존 데이터 또는 처음부터 시작
+Budibase를 사용하면 다음과 같은 여러 소스에서 데이터를 가져올 수 있습니다: MondoDB, CouchDB, PostgreSQL, MySQL, Airtable, S3, DynamoDB 또는 REST API.
+
+또는 원하는 경우 외부 도구 없이도 Budibase를 사용하여 처음부터 시작하여 자체 애플리케이션을 구축할 수 있습니다.[데이터 소스 제안](https://github.com/Budibase/budibase/discussions?discussions_q=category%3AIdeas).
+
+
+
+
+
+
+### 강력한 내장 구성 요소로 애플리케이션을 설계하고 구축할 수 있습니다.
+
+Budibase에는 아름답게 디자인된 강력한 컴포넌트들이 제공되며, 이를 사용하여 UI를 쉽게 구축할 수 있습니다. 또한, CSS를 통한 스타일링 옵션도 풍부하게 제공되어 보다 창의적인 표현도 가능하다.
+ [Request new component](https://github.com/Budibase/budibase/discussions?discussions_q=category%3AIdeas).
+
+
+
+
+
+
+### 프로세스를 자동화하고, 다른 도구와 연동하고, 웹훅으로 연결하세요!
+워크플로우와 수동 프로세스를 자동화하여 시간을 절약하세요. 웹훅 이벤트 연결부터 이메일 자동화까지, Budibase에 수행할 작업을 지시하기만 하면 자동으로 처리됩니다. [새로운 자동화 만들기](https://github.com/Budibase/automations)또는[새로운 자동화를 요청할 수 있습니다](https://github.com/Budibase/budibase/discussions?discussions_q=category%3AIdeas).
+
+
+
+
+
+
+### 선호하는 도구
+Budibase는 사용자의 선호도에 따라 애플리케이션을 구축할 수 있는 다양한 도구를 통합하고 있습니다.
+
+
+
+
+
+
+### 관리자의 천국
+Budibase는 어떤 규모의 프로젝트에도 유연하게 대응할 수 있으며, Budibase를 사용하면 개인 또는 조직의 서버에서 자체 호스팅하고 사용자, 온보딩, SMTP, 앱, 그룹, 테마 등을 한꺼번에 관리할 수 있습니다. 또한, 사용자나 그룹에 앱 포털을 제공하고 그룹 관리자에게 사용자 관리를 맡길 수도 있다.
+- 프로모션 비디오: https://youtu.be/xoljVpty_Kw
+
+
+
+## 🏁 시작
+
+Docker, Kubernetes 또는 Digital Ocean을 사용하여 자체 인프라에서 Budibase를 호스팅하거나, 걱정 없이 빠르게 애플리케이션을 구축하려는 경우 클라우드에서 Budibase를 사용할 수 있습니다.
+
+### [Budibase 셀프 호스팅으로 시작하기](https://docs.budibase.com/docs/hosting-methods)
+
+- [Docker - single ARM compatible image](https://docs.budibase.com/docs/docker)
+- [Docker Compose](https://docs.budibase.com/docs/docker-compose)
+- [Kubernetes](https://docs.budibase.com/docs/kubernetes-k8s)
+- [Digital Ocean](https://docs.budibase.com/docs/digitalocean)
+- [Portainer](https://docs.budibase.com/docs/portainer)
+
+
+### [클라우드에서 Budibase 시작하기](https://budibase.com)
+
+
+
+## 🎓 Budibase 알아보기
+
+문서 [documentacion de Budibase](https://docs.budibase.com/docs).
+
+
+
+
+
+## 💬 커뮤니티
+
+질문하고, 다른 사람을 돕고, 다른 Budibase 사용자와 즐거운 대화를 나눌 수 있는 Budibase 커뮤니티에 여러분을 초대합니다.
+[깃허브 토론](https://github.com/Budibase/budibase/discussions)
+
+
+
+## ❗ 행동강령
+
+Budibase 는 모든 계층의 사람들을 환영하고 상호 존중하는 환경을 제공하는 데 특별한 주의를 기울이고 있습니다. 저희는 커뮤니티에도 같은 기대를 가지고 있습니다.
+[**행동 강령**](https://github.com/Budibase/budibase/blob/HEAD/.github/CODE_OF_CONDUCT.md).
+
+
+
+
+
+## 🙌 Contribuir en Budibase
+
+버그 신고부터 코드의 버그 수정에 이르기까지 모든 기여를 감사하고 환영합니다. 새로운 기능을 구현하거나 API를 변경할 계획이 있다면 [여기에 새 메시지](https://github.com/Budibase/budibase/issues),
+이렇게 하면 여러분의 노력이 헛되지 않도록 보장할 수 있습니다.
+
+여기에는 다음을 위해 Budibase 환경을 설정하는 방법에 대한 지침이 나와 있습니다. [여기를 클릭하세요](https://github.com/Budibase/budibase/tree/HEAD/docs/CONTRIBUTING.md).
+
+### 어디서부터 시작해야 할지 혼란스러우신가요?
+이곳은 기여를 시작하기에 최적의 장소입니다! [First time issues project](https://github.com/Budibase/budibase/projects/22).
+
+### 리포지토리 구성
+
+Budibase는 Lerna에서 관리하는 단일 리포지토리입니다. Lerna는 변경 사항이 있을 때마다 이를 동기화하여 Budibase 패키지를 빌드하고 게시합니다. 크게 보면 이러한 패키지가 Budibase를 구성하는 패키지입니다:
+
+- [packages/builder](https://github.com/Budibase/budibase/tree/HEAD/packages/builder) - budibase builder 클라이언트 측의 svelte 애플리케이션 코드가 포함되어 있습니다.
+
+- [packages/client](https://github.com/Budibase/budibase/tree/HEAD/packages/client) - budibase builder 클라이언트 측의 svelte 애플리케이션 코드가 포함되어 있습니다.
+
+- [packages/server](https://github.com/Budibase/budibase/tree/HEAD/packages/server) - Budibase의 서버 부분입니다. 이 Koa 애플리케이션은 빌더에게 Budibase 애플리케이션을 생성하는 데 필요한 것을 제공하는 역할을 합니다. 또한 데이터베이스 및 파일 저장소와 상호 작용할 수 있는 API를 제공합니다.
+
+자세한 내용은 다음 문서를 참조하세요. [CONTRIBUTING.md](https://github.com/Budibase/budibase/blob/HEAD/docs/CONTRIBUTING.md)
+
+
+
+
+## 📝 라이선스
+
+Budibase는 오픈 소스이며, 라이선스는 다음과 같습니다 [GPL v3](https://www.gnu.org/licenses/gpl-3.0.en.html). 클라이언트 및 컴포넌트 라이브러리는 다음과 같이 라이선스가 부여됩니다. [MPL](https://directory.fsf.org/wiki/License:MPL-2.0) - 이렇게 하면 빌드한 애플리케이션에 원하는 대로 라이선스를 부여할 수 있습니다.
+
+
+
+## ⭐ 스타 수의 역사
+
+[![Stargazers over time](https://starchart.cc/Budibase/budibase.svg)](https://starchart.cc/Budibase/budibase)
+
+빌더 업데이트 중 문제가 발생하는 경우 [여기](https://github.com/Budibase/budibase/blob/HEAD/docs/CONTRIBUTING.md#troubleshooting) 를 참고하여 환경을 정리해 주세요.
+
+
+
+## Contributors ✨
+
+훌륭한 여러분께 감사할 따름입니다. ([emoji key](https://allcontributors.org/docs/en/emoji-key)):
+
+
+
+
+
+
+
+
+
+
+
+이 프로젝트는 다음 사양을 따릅니다. [all-contributors](https://github.com/all-contributors/all-contributors).
+모든 종류의 기여를 환영합니다!
diff --git a/lerna.json b/lerna.json
index a62c15997d..a77a16a24e 100644
--- a/lerna.json
+++ b/lerna.json
@@ -1,5 +1,5 @@
{
- "version": "2.20.5",
+ "version": "2.21.3",
"npmClient": "yarn",
"packages": [
"packages/*",
diff --git a/package.json b/package.json
index 4407fd33f3..0a20f01d52 100644
--- a/package.json
+++ b/package.json
@@ -22,7 +22,7 @@
"nx-cloud": "16.0.5",
"prettier": "2.8.8",
"prettier-plugin-svelte": "^2.3.0",
- "svelte": "3.49.0",
+ "svelte": "^4.2.10",
"svelte-eslint-parser": "^0.33.1",
"typescript": "5.2.2",
"yargs": "^17.7.2"
diff --git a/packages/backend-core/package.json b/packages/backend-core/package.json
index 3f8c34f823..fe56780982 100644
--- a/packages/backend-core/package.json
+++ b/packages/backend-core/package.json
@@ -67,7 +67,7 @@
"@types/lodash": "4.14.200",
"@types/node-fetch": "2.6.4",
"@types/pouchdb": "6.4.0",
- "@types/redlock": "4.0.3",
+ "@types/redlock": "4.0.7",
"@types/semver": "7.3.7",
"@types/tar-fs": "2.0.1",
"@types/uuid": "8.3.4",
@@ -78,6 +78,7 @@
"jest-serial-runner": "1.2.1",
"pino-pretty": "10.0.0",
"pouchdb-adapter-memory": "7.2.2",
+ "testcontainers": "^10.7.2",
"timekeeper": "2.2.0",
"typescript": "5.2.2"
},
diff --git a/packages/backend-core/src/db/couch/DatabaseImpl.ts b/packages/backend-core/src/db/couch/DatabaseImpl.ts
index 0e2b4173b0..7e7c997cbe 100644
--- a/packages/backend-core/src/db/couch/DatabaseImpl.ts
+++ b/packages/backend-core/src/db/couch/DatabaseImpl.ts
@@ -11,6 +11,7 @@ import {
Document,
isDocument,
RowResponse,
+ RowValue,
} from "@budibase/types"
import { getCouchInfo } from "./connections"
import { directCouchUrlCall } from "./utils"
@@ -221,7 +222,7 @@ export class DatabaseImpl implements Database {
})
}
- async allDocs(
+ async allDocs(
params: DatabaseQueryOpts
): Promise> {
return this.performCall(db => {
diff --git a/packages/backend-core/src/db/instrumentation.ts b/packages/backend-core/src/db/instrumentation.ts
index aa2ac424ae..03010d4c92 100644
--- a/packages/backend-core/src/db/instrumentation.ts
+++ b/packages/backend-core/src/db/instrumentation.ts
@@ -1,5 +1,4 @@
import {
- DocumentScope,
DocumentDestroyResponse,
DocumentInsertResponse,
DocumentBulkResponse,
@@ -13,6 +12,7 @@ import {
DatabasePutOpts,
DatabaseQueryOpts,
Document,
+ RowValue,
} from "@budibase/types"
import tracer from "dd-trace"
import { Writable } from "stream"
@@ -79,7 +79,7 @@ export class DDInstrumentedDatabase implements Database {
})
}
- allDocs(
+ allDocs(
params: DatabaseQueryOpts
): Promise> {
return tracer.trace("db.allDocs", span => {
diff --git a/packages/backend-core/src/docIds/ids.ts b/packages/backend-core/src/docIds/ids.ts
index 02176109da..9627b2b94c 100644
--- a/packages/backend-core/src/docIds/ids.ts
+++ b/packages/backend-core/src/docIds/ids.ts
@@ -74,7 +74,7 @@ export function getGlobalIDFromUserMetadataID(id: string) {
* Generates a template ID.
* @param ownerId The owner/user of the template, this could be global or a workspace level.
*/
-export function generateTemplateID(ownerId: any) {
+export function generateTemplateID(ownerId: string) {
return `${DocumentType.TEMPLATE}${SEPARATOR}${ownerId}${SEPARATOR}${newid()}`
}
@@ -105,7 +105,7 @@ export function prefixRoleID(name: string) {
* Generates a new dev info document ID - this is scoped to a user.
* @returns The new dev info ID which info for dev (like api key) can be stored under.
*/
-export const generateDevInfoID = (userId: any) => {
+export const generateDevInfoID = (userId: string) => {
return `${DocumentType.DEV_INFO}${SEPARATOR}${userId}`
}
diff --git a/packages/backend-core/src/middleware/errorHandling.ts b/packages/backend-core/src/middleware/errorHandling.ts
index ebdd4107e9..2b8f7195ed 100644
--- a/packages/backend-core/src/middleware/errorHandling.ts
+++ b/packages/backend-core/src/middleware/errorHandling.ts
@@ -1,5 +1,6 @@
import { APIError } from "@budibase/types"
import * as errors from "../errors"
+import environment from "../environment"
export async function errorHandling(ctx: any, next: any) {
try {
@@ -14,15 +15,19 @@ export async function errorHandling(ctx: any, next: any) {
console.error(err)
}
- const error = errors.getPublicError(err)
- const body: APIError = {
+ let error: APIError = {
message: err.message,
status: status,
validationErrors: err.validation,
- error,
+ error: errors.getPublicError(err),
}
- ctx.body = body
+ if (environment.isTest() && ctx.headers["x-budibase-include-stacktrace"]) {
+ // @ts-ignore
+ error.stack = err.stack
+ }
+
+ ctx.body = error
}
}
diff --git a/packages/backend-core/src/middleware/joi-validator.ts b/packages/backend-core/src/middleware/joi-validator.ts
index fcc8316886..ac8064a512 100644
--- a/packages/backend-core/src/middleware/joi-validator.ts
+++ b/packages/backend-core/src/middleware/joi-validator.ts
@@ -1,12 +1,12 @@
-import Joi, { ObjectSchema } from "joi"
-import { BBContext } from "@budibase/types"
+import Joi from "joi"
+import { Ctx } from "@budibase/types"
function validate(
schema: Joi.ObjectSchema | Joi.ArraySchema,
property: string
) {
// Return a Koa middleware function
- return (ctx: BBContext, next: any) => {
+ return (ctx: Ctx, next: any) => {
if (!schema) {
return next()
}
@@ -30,7 +30,6 @@ function validate(
const { error } = schema.validate(params)
if (error) {
ctx.throw(400, `Invalid ${property} - ${error.message}`)
- return
}
return next()
}
diff --git a/packages/backend-core/src/objectStore/buckets/plugins.ts b/packages/backend-core/src/objectStore/buckets/plugins.ts
index 6f1b7116ae..02be9345ab 100644
--- a/packages/backend-core/src/objectStore/buckets/plugins.ts
+++ b/packages/backend-core/src/objectStore/buckets/plugins.ts
@@ -6,7 +6,7 @@ import { Plugin } from "@budibase/types"
// URLS
-export function enrichPluginURLs(plugins: Plugin[]) {
+export function enrichPluginURLs(plugins?: Plugin[]): Plugin[] {
if (!plugins || !plugins.length) {
return []
}
diff --git a/packages/backend-core/src/redis/redis.ts b/packages/backend-core/src/redis/redis.ts
index d15453ba62..beb59ee3fa 100644
--- a/packages/backend-core/src/redis/redis.ts
+++ b/packages/backend-core/src/redis/redis.ts
@@ -1,5 +1,5 @@
import env from "../environment"
-import Redis from "ioredis"
+import Redis, { Cluster } from "ioredis"
// mock-redis doesn't have any typing
let MockRedis: any | undefined
if (env.MOCK_REDIS) {
@@ -28,7 +28,7 @@ const DEFAULT_SELECT_DB = SelectableDatabase.DEFAULT
// for testing just generate the client once
let CLOSED = false
-let CLIENTS: { [key: number]: any } = {}
+const CLIENTS: Record = {}
let CONNECTED = false
// mock redis always connected
@@ -36,7 +36,7 @@ if (env.MOCK_REDIS) {
CONNECTED = true
}
-function pickClient(selectDb: number): any {
+function pickClient(selectDb: number) {
return CLIENTS[selectDb]
}
@@ -201,12 +201,15 @@ class RedisWrapper {
key = `${db}${SEPARATOR}${key}`
let stream
if (CLUSTERED) {
- let node = this.getClient().nodes("master")
+ let node = (this.getClient() as never as Cluster).nodes("master")
stream = node[0].scanStream({ match: key + "*", count: 100 })
} else {
- stream = this.getClient().scanStream({ match: key + "*", count: 100 })
+ stream = (this.getClient() as Redis).scanStream({
+ match: key + "*",
+ count: 100,
+ })
}
- return promisifyStream(stream, this.getClient())
+ return promisifyStream(stream, this.getClient() as any)
}
async keys(pattern: string) {
@@ -221,14 +224,16 @@ class RedisWrapper {
async get(key: string) {
const db = this._db
- let response = await this.getClient().get(addDbPrefix(db, key))
+ const response = await this.getClient().get(addDbPrefix(db, key))
// overwrite the prefixed key
+ // @ts-ignore
if (response != null && response.key) {
+ // @ts-ignore
response.key = key
}
// if its not an object just return the response
try {
- return JSON.parse(response)
+ return JSON.parse(response!)
} catch (err) {
return response
}
@@ -274,13 +279,37 @@ class RedisWrapper {
}
}
+ async bulkStore(
+ data: Record,
+ expirySeconds: number | null = null
+ ) {
+ const client = this.getClient()
+
+ const dataToStore = Object.entries(data).reduce((acc, [key, value]) => {
+ acc[addDbPrefix(this._db, key)] =
+ typeof value === "object" ? JSON.stringify(value) : value
+ return acc
+ }, {} as Record)
+
+ const pipeline = client.pipeline()
+ pipeline.mset(dataToStore)
+
+ if (expirySeconds !== null) {
+ for (const key of Object.keys(dataToStore)) {
+ pipeline.expire(key, expirySeconds)
+ }
+ }
+
+ await pipeline.exec()
+ }
+
async getTTL(key: string) {
const db = this._db
const prefixedKey = addDbPrefix(db, key)
return this.getClient().ttl(prefixedKey)
}
- async setExpiry(key: string, expirySeconds: number | null) {
+ async setExpiry(key: string, expirySeconds: number) {
const db = this._db
const prefixedKey = addDbPrefix(db, key)
await this.getClient().expire(prefixedKey, expirySeconds)
@@ -295,6 +324,26 @@ class RedisWrapper {
let items = await this.scan()
await Promise.all(items.map((obj: any) => this.delete(obj.key)))
}
+
+ async increment(key: string) {
+ const result = await this.getClient().incr(addDbPrefix(this._db, key))
+ if (isNaN(result)) {
+ throw new Error(`Redis ${key} does not contain a number`)
+ }
+ return result
+ }
+
+ async deleteIfValue(key: string, value: any) {
+ const client = this.getClient()
+
+ const luaScript = `
+ if redis.call('GET', KEYS[1]) == ARGV[1] then
+ redis.call('DEL', KEYS[1])
+ end
+ `
+
+ await client.eval(luaScript, 1, addDbPrefix(this._db, key), value)
+ }
}
export default RedisWrapper
diff --git a/packages/backend-core/src/redis/redlockImpl.ts b/packages/backend-core/src/redis/redlockImpl.ts
index 7009dc6f55..adeb5b12ec 100644
--- a/packages/backend-core/src/redis/redlockImpl.ts
+++ b/packages/backend-core/src/redis/redlockImpl.ts
@@ -72,7 +72,7 @@ const OPTIONS: Record = {
export async function newRedlock(opts: Redlock.Options = {}) {
const options = { ...OPTIONS.DEFAULT, ...opts }
const redisWrapper = await getLockClient()
- const client = redisWrapper.getClient()
+ const client = redisWrapper.getClient() as any
return new Redlock([client], options)
}
diff --git a/packages/backend-core/src/redis/tests/redis.spec.ts b/packages/backend-core/src/redis/tests/redis.spec.ts
new file mode 100644
index 0000000000..c2c9e4a14e
--- /dev/null
+++ b/packages/backend-core/src/redis/tests/redis.spec.ts
@@ -0,0 +1,214 @@
+import { GenericContainer, StartedTestContainer } from "testcontainers"
+import { generator, structures } from "../../../tests"
+import RedisWrapper from "../redis"
+import { env } from "../.."
+
+jest.setTimeout(30000)
+
+describe("redis", () => {
+ let redis: RedisWrapper
+ let container: StartedTestContainer
+
+ beforeAll(async () => {
+ const container = await new GenericContainer("redis")
+ .withExposedPorts(6379)
+ .start()
+
+ env._set(
+ "REDIS_URL",
+ `${container.getHost()}:${container.getMappedPort(6379)}`
+ )
+ env._set("MOCK_REDIS", 0)
+ env._set("REDIS_PASSWORD", 0)
+ })
+
+ afterAll(() => container?.stop())
+
+ beforeEach(async () => {
+ redis = new RedisWrapper(structures.db.id())
+ await redis.init()
+ })
+
+ describe("store", () => {
+ it("a basic value can be persisted", async () => {
+ const key = structures.uuid()
+ const value = generator.word()
+
+ await redis.store(key, value)
+
+ expect(await redis.get(key)).toEqual(value)
+ })
+
+ it("objects can be persisted", async () => {
+ const key = structures.uuid()
+ const value = { [generator.word()]: generator.word() }
+
+ await redis.store(key, value)
+
+ expect(await redis.get(key)).toEqual(value)
+ })
+ })
+
+ describe("bulkStore", () => {
+ function createRandomObject(
+ keyLength: number,
+ valueGenerator: () => any = () => generator.word()
+ ) {
+ return generator
+ .unique(() => generator.word(), keyLength)
+ .reduce((acc, key) => {
+ acc[key] = valueGenerator()
+ return acc
+ }, {} as Record)
+ }
+
+ it("a basic object can be persisted", async () => {
+ const data = createRandomObject(10)
+
+ await redis.bulkStore(data)
+
+ for (const [key, value] of Object.entries(data)) {
+ expect(await redis.get(key)).toEqual(value)
+ }
+
+ expect(await redis.keys("*")).toHaveLength(10)
+ })
+
+ it("a complex object can be persisted", async () => {
+ const data = {
+ ...createRandomObject(10, () => createRandomObject(5)),
+ ...createRandomObject(5),
+ }
+
+ await redis.bulkStore(data)
+
+ for (const [key, value] of Object.entries(data)) {
+ expect(await redis.get(key)).toEqual(value)
+ }
+
+ expect(await redis.keys("*")).toHaveLength(15)
+ })
+
+ it("no TTL is set by default", async () => {
+ const data = createRandomObject(10)
+
+ await redis.bulkStore(data)
+
+ for (const [key, value] of Object.entries(data)) {
+ expect(await redis.get(key)).toEqual(value)
+ expect(await redis.getTTL(key)).toEqual(-1)
+ }
+ })
+
+ it("a bulk store can be persisted with TTL", async () => {
+ const ttl = 500
+ const data = createRandomObject(8)
+
+ await redis.bulkStore(data, ttl)
+
+ for (const [key, value] of Object.entries(data)) {
+ expect(await redis.get(key)).toEqual(value)
+ expect(await redis.getTTL(key)).toEqual(ttl)
+ }
+
+ expect(await redis.keys("*")).toHaveLength(8)
+ })
+
+ it("setting a TTL of -1 will not persist the key", async () => {
+ const ttl = -1
+ const data = createRandomObject(5)
+
+ await redis.bulkStore(data, ttl)
+
+ for (const [key, value] of Object.entries(data)) {
+ expect(await redis.get(key)).toBe(null)
+ }
+
+ expect(await redis.keys("*")).toHaveLength(0)
+ })
+ })
+
+ describe("increment", () => {
+ it("can increment on a new key", async () => {
+ const key = structures.uuid()
+ const result = await redis.increment(key)
+ expect(result).toBe(1)
+ })
+
+ it("can increment multiple times", async () => {
+ const key = structures.uuid()
+ const results = [
+ await redis.increment(key),
+ await redis.increment(key),
+ await redis.increment(key),
+ await redis.increment(key),
+ await redis.increment(key),
+ ]
+ expect(results).toEqual([1, 2, 3, 4, 5])
+ })
+
+ it("can increment on a new key", async () => {
+ const key1 = structures.uuid()
+ const key2 = structures.uuid()
+
+ const result1 = await redis.increment(key1)
+ expect(result1).toBe(1)
+
+ const result2 = await redis.increment(key2)
+ expect(result2).toBe(1)
+ })
+
+ it("can increment multiple times in parallel", async () => {
+ const key = structures.uuid()
+ const results = await Promise.all(
+ Array.from({ length: 100 }).map(() => redis.increment(key))
+ )
+ expect(results).toHaveLength(100)
+ expect(results).toEqual(Array.from({ length: 100 }).map((_, i) => i + 1))
+ })
+
+ it("can increment existing set keys", async () => {
+ const key = structures.uuid()
+ await redis.store(key, 70)
+ await redis.increment(key)
+
+ const result = await redis.increment(key)
+ expect(result).toBe(72)
+ })
+
+ it.each([
+ generator.word(),
+ generator.bool(),
+ { [generator.word()]: generator.word() },
+ ])("cannot increment if the store value is not a number", async value => {
+ const key = structures.uuid()
+ await redis.store(key, value)
+
+ await expect(redis.increment(key)).rejects.toThrowError(
+ "ERR value is not an integer or out of range"
+ )
+ })
+ })
+
+ describe("deleteIfValue", () => {
+ it("can delete if the value matches", async () => {
+ const key = structures.uuid()
+ const value = generator.word()
+ await redis.store(key, value)
+
+ await redis.deleteIfValue(key, value)
+
+ expect(await redis.get(key)).toBeNull()
+ })
+
+ it("will not delete if the value does not matches", async () => {
+ const key = structures.uuid()
+ const value = generator.word()
+ await redis.store(key, value)
+
+ await redis.deleteIfValue(key, generator.word())
+
+ expect(await redis.get(key)).toEqual(value)
+ })
+ })
+})
diff --git a/packages/backend-core/src/redis/utils.ts b/packages/backend-core/src/redis/utils.ts
index 4d8b1bb9a4..7b93458b52 100644
--- a/packages/backend-core/src/redis/utils.ts
+++ b/packages/backend-core/src/redis/utils.ts
@@ -29,6 +29,7 @@ export enum Databases {
WRITE_THROUGH = "writeThrough",
LOCKS = "locks",
SOCKET_IO = "socket_io",
+ BPM_EVENTS = "bpmEvents",
}
/**
diff --git a/packages/backend-core/src/security/roles.ts b/packages/backend-core/src/security/roles.ts
index 4f048c0a11..01473ad991 100644
--- a/packages/backend-core/src/security/roles.ts
+++ b/packages/backend-core/src/security/roles.ts
@@ -84,16 +84,18 @@ export function getBuiltinRoles(): { [key: string]: RoleDoc } {
return cloneDeep(BUILTIN_ROLES)
}
-export const BUILTIN_ROLE_ID_ARRAY = Object.values(BUILTIN_ROLES).map(
- role => role._id
-)
+export function isBuiltin(role: string) {
+ return getBuiltinRole(role) !== undefined
+}
-export const BUILTIN_ROLE_NAME_ARRAY = Object.values(BUILTIN_ROLES).map(
- role => role.name
-)
-
-export function isBuiltin(role?: string) {
- return BUILTIN_ROLE_ID_ARRAY.some(builtin => role?.includes(builtin))
+export function getBuiltinRole(roleId: string): Role | undefined {
+ const role = Object.values(BUILTIN_ROLES).find(role =>
+ roleId.includes(role._id)
+ )
+ if (!role) {
+ return undefined
+ }
+ return cloneDeep(role)
}
/**
@@ -123,7 +125,7 @@ export function builtinRoleToNumber(id?: string) {
/**
* Converts any role to a number, but has to be async to get the roles from db.
*/
-export async function roleToNumber(id?: string) {
+export async function roleToNumber(id: string) {
if (isBuiltin(id)) {
return builtinRoleToNumber(id)
}
@@ -131,7 +133,7 @@ export async function roleToNumber(id?: string) {
defaultPublic: true,
})) as RoleDoc[]
for (let role of hierarchy) {
- if (isBuiltin(role?.inherits)) {
+ if (role?.inherits && isBuiltin(role.inherits)) {
return builtinRoleToNumber(role.inherits) + 1
}
}
@@ -161,35 +163,28 @@ export function lowerBuiltinRoleID(roleId1?: string, roleId2?: string): string {
* @returns The role object, which may contain an "inherits" property.
*/
export async function getRole(
- roleId?: string,
+ roleId: string,
opts?: { defaultPublic?: boolean }
-): Promise {
- if (!roleId) {
- return undefined
- }
- let role: any = {}
+): Promise {
// built in roles mostly come from the in-code implementation,
// but can be extended by a doc stored about them (e.g. permissions)
- if (isBuiltin(roleId)) {
- role = cloneDeep(
- Object.values(BUILTIN_ROLES).find(role => role._id === roleId)
- )
- } else {
+ let role: RoleDoc | undefined = getBuiltinRole(roleId)
+ if (!role) {
// make sure has the prefix (if it has it then it won't be added)
roleId = prefixRoleID(roleId)
}
try {
const db = getAppDB()
- const dbRole = await db.get(getDBRoleID(roleId))
- role = Object.assign(role, dbRole)
+ const dbRole = await db.get(getDBRoleID(roleId))
+ role = Object.assign(role || {}, dbRole)
// finalise the ID
- role._id = getExternalRoleID(role._id, role.version)
+ role._id = getExternalRoleID(role._id!, role.version)
} catch (err) {
if (!isBuiltin(roleId) && opts?.defaultPublic) {
return cloneDeep(BUILTIN_ROLES.PUBLIC)
}
// only throw an error if there is no role at all
- if (Object.keys(role).length === 0) {
+ if (!role || Object.keys(role).length === 0) {
throw err
}
}
@@ -200,7 +195,7 @@ export async function getRole(
* Simple function to get all the roles based on the top level user role ID.
*/
async function getAllUserRoles(
- userRoleId?: string,
+ userRoleId: string,
opts?: { defaultPublic?: boolean }
): Promise {
// admins have access to all roles
@@ -226,7 +221,7 @@ async function getAllUserRoles(
}
export async function getUserRoleIdHierarchy(
- userRoleId?: string
+ userRoleId: string
): Promise {
const roles = await getUserRoleHierarchy(userRoleId)
return roles.map(role => role._id!)
@@ -241,7 +236,7 @@ export async function getUserRoleIdHierarchy(
* highest level of access and the last being the lowest level.
*/
export async function getUserRoleHierarchy(
- userRoleId?: string,
+ userRoleId: string,
opts?: { defaultPublic?: boolean }
) {
// special case, if they don't have a role then they are a public user
@@ -265,9 +260,9 @@ export function checkForRoleResourceArray(
return rolePerms
}
-export async function getAllRoleIds(appId?: string) {
+export async function getAllRoleIds(appId: string): Promise {
const roles = await getAllRoles(appId)
- return roles.map(role => role._id)
+ return roles.map(role => role._id!)
}
/**
diff --git a/packages/backend-core/tests/core/utilities/mocks/licenses.ts b/packages/backend-core/tests/core/utilities/mocks/licenses.ts
index 758fd6bf9a..1cbc282575 100644
--- a/packages/backend-core/tests/core/utilities/mocks/licenses.ts
+++ b/packages/backend-core/tests/core/utilities/mocks/licenses.ts
@@ -58,7 +58,7 @@ export const useCloudFree = () => {
// FEATURES
const useFeature = (feature: Feature) => {
- const license = cloneDeep(UNLIMITED_LICENSE)
+ const license = cloneDeep(getCachedLicense() || UNLIMITED_LICENSE)
const opts: UseLicenseOpts = {
features: [feature],
}
diff --git a/packages/bbui/package.json b/packages/bbui/package.json
index 78eed2b608..a1baa2a38b 100644
--- a/packages/bbui/package.json
+++ b/packages/bbui/package.json
@@ -24,8 +24,7 @@
"rollup": "^2.45.2",
"rollup-plugin-postcss": "^4.0.0",
"rollup-plugin-svelte": "^7.1.0",
- "rollup-plugin-terser": "^7.0.2",
- "svelte": "3.49.0"
+ "rollup-plugin-terser": "^7.0.2"
},
"keywords": [
"svelte"
diff --git a/packages/bbui/src/ActionButton/ActionButton.svelte b/packages/bbui/src/ActionButton/ActionButton.svelte
index 0e6ec3d155..c346e34d54 100644
--- a/packages/bbui/src/ActionButton/ActionButton.svelte
+++ b/packages/bbui/src/ActionButton/ActionButton.svelte
@@ -41,6 +41,7 @@
}
+
(showTooltip = true)}
diff --git a/packages/bbui/src/ActionMenu/ActionMenu.svelte b/packages/bbui/src/ActionMenu/ActionMenu.svelte
index 08425e8f59..642ec4932a 100644
--- a/packages/bbui/src/ActionMenu/ActionMenu.svelte
+++ b/packages/bbui/src/ActionMenu/ActionMenu.svelte
@@ -33,6 +33,8 @@
setContext("actionMenu", { show, hide })
+
+
diff --git a/packages/bbui/src/Actions/position_dropdown.js b/packages/bbui/src/Actions/position_dropdown.js
index cc169eac09..d259b9197a 100644
--- a/packages/bbui/src/Actions/position_dropdown.js
+++ b/packages/bbui/src/Actions/position_dropdown.js
@@ -35,7 +35,10 @@ export default function positionDropdown(element, opts) {
}
if (typeof customUpdate === "function") {
- styles = customUpdate(anchorBounds, elementBounds, styles)
+ styles = customUpdate(anchorBounds, elementBounds, {
+ ...styles,
+ offset: opts.offset,
+ })
} else {
// Determine vertical styles
if (align === "right-outside") {
diff --git a/packages/bbui/src/Badge/Badge.svelte b/packages/bbui/src/Badge/Badge.svelte
index 8b54045297..e4ec7d4f33 100644
--- a/packages/bbui/src/Badge/Badge.svelte
+++ b/packages/bbui/src/Badge/Badge.svelte
@@ -13,6 +13,8 @@
export let hoverable = false
+
+
+
+
+
+
diff --git a/packages/bbui/src/DetailSummary/DetailSummary.svelte b/packages/bbui/src/DetailSummary/DetailSummary.svelte
index 2cbb6796f3..cbfdcbec9b 100644
--- a/packages/bbui/src/DetailSummary/DetailSummary.svelte
+++ b/packages/bbui/src/DetailSummary/DetailSummary.svelte
@@ -15,6 +15,8 @@
}
+
+
{#if name}
diff --git a/packages/bbui/src/FancyForm/FancyField.svelte b/packages/bbui/src/FancyForm/FancyField.svelte
index 455f4b38fb..798f486187 100644
--- a/packages/bbui/src/FancyForm/FancyField.svelte
+++ b/packages/bbui/src/FancyForm/FancyField.svelte
@@ -36,6 +36,8 @@
})
+
+