Merge branch 'develop' into qa-core-datasource-api-tests
This commit is contained in:
commit
796c50d3e0
|
@ -56,6 +56,7 @@ jobs:
|
|||
run: yarn install:pro $BRANCH $BASE_BRANCH
|
||||
- run: yarn
|
||||
- run: yarn bootstrap
|
||||
- run: yarn build
|
||||
- run: yarn test
|
||||
- uses: codecov/codecov-action@v3
|
||||
with:
|
||||
|
@ -94,4 +95,4 @@ jobs:
|
|||
yarn test:ci
|
||||
env:
|
||||
BB_ADMIN_USER_EMAIL: admin
|
||||
BB_ADMIN_USER_PASSWORD: admin
|
||||
BB_ADMIN_USER_PASSWORD: admin
|
||||
|
|
|
@ -75,7 +75,6 @@ jobs:
|
|||
- name: Build/release Docker images
|
||||
run: |
|
||||
docker login -u $DOCKER_USER -p $DOCKER_PASSWORD
|
||||
yarn build
|
||||
yarn build:docker
|
||||
env:
|
||||
DOCKER_USER: ${{ secrets.DOCKER_USERNAME }}
|
||||
|
|
34
README.md
34
README.md
|
@ -216,35 +216,9 @@ If you are having issues between updates of the builder, please use the guide [h
|
|||
|
||||
Thanks goes to these wonderful people ([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 -->
|
||||
<a href="https://github.com/Budibase/budibase/graphs/contributors">
|
||||
<img src="https://contrib.rocks/image?repo=Budibase/budibase" />
|
||||
</a>
|
||||
|
||||
<!-- ALL-CONTRIBUTORS-LIST:END -->
|
||||
|
||||
This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. Contributions of any kind welcome!
|
||||
Made with [contrib.rocks](https://contrib.rocks).
|
||||
|
|
|
@ -14,6 +14,9 @@ metadata:
|
|||
alb.ingress.kubernetes.io/listen-ports: '[{"HTTP": 80}, {"HTTPS":443}]'
|
||||
alb.ingress.kubernetes.io/certificate-arn: {{ .Values.ingress.certificateArn }}
|
||||
{{- end }}
|
||||
{{- if .Values.ingress.sslPolicy }}
|
||||
alb.ingress.kubernetes.io/actions.ssl-policy: {{ .Values.ingress.sslPolicy }}
|
||||
{{- end }}
|
||||
{{- if .Values.ingress.securityGroups }}
|
||||
alb.ingress.kubernetes.io/security-groups: {{ .Values.ingress.securityGroups }}
|
||||
{{- end }}
|
||||
|
|
|
@ -61,5 +61,18 @@ http://127.0.0.1:10000/builder/admin
|
|||
| **NOTE**: If you are working on a M1 Apple Silicon, you will need to uncomment `# platform: linux/amd64` line in
|
||||
[hosting/docker-compose-dev.yaml](../hosting/docker-compose.dev.yaml)
|
||||
|
||||
### Troubleshooting
|
||||
If there are errors with the `yarn setup` command, you can try installing nvm and node 14. This is the same as the instructions for Debian 11.
|
||||
### Troubleshootings
|
||||
|
||||
#### Yarn setup errors
|
||||
|
||||
If there are errors with the `yarn setup` command, you can try installing nvm and node 14. This is the same as the instructions for Debian 11.
|
||||
|
||||
#### Node 14.20.1 not supported for arm64
|
||||
|
||||
If you are working with M1 or M2 Mac and trying the Node installation via `nvm`, probably you will find the error `curl: (22) The requested URL returned error: 404`.
|
||||
|
||||
Version `v14.20.1` is not supported for arm64; in order to use it, you can switch the CPU architecture for this by the following command:
|
||||
|
||||
```shell
|
||||
arch -x86_64 zsh #Run this before nvm install
|
||||
```
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"version": "2.5.5-alpha.0",
|
||||
"version": "2.5.6-alpha.32",
|
||||
"npmClient": "yarn",
|
||||
"useWorkspaces": true,
|
||||
"packages": ["packages/*"],
|
||||
|
|
|
@ -44,7 +44,7 @@
|
|||
"dev": "yarn run kill-all && lerna link && lerna run --stream --parallel dev:builder --concurrency 1 --stream",
|
||||
"dev:noserver": "yarn run kill-builder && lerna link && lerna run --stream dev:stack:up && lerna run --stream --parallel dev:builder --concurrency 1 --ignore @budibase/backend-core --ignore @budibase/server --ignore @budibase/worker",
|
||||
"dev:server": "yarn run kill-server && lerna run --stream --parallel dev:builder --concurrency 1 --scope @budibase/backend-core --scope @budibase/worker --scope @budibase/server",
|
||||
"dev:built": "cd packages/server && yarn dev:stack:up && cd ../../ && lerna run --stream --parallel dev:built",
|
||||
"dev:built": "yarn run kill-all && cd packages/server && yarn dev:stack:up && cd ../../ && lerna run --stream --parallel dev:built",
|
||||
"test": "lerna run --stream test --stream",
|
||||
"test:pro": "bash scripts/pro/test.sh",
|
||||
"lint:eslint": "eslint packages && eslint qa-core",
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@budibase/backend-core",
|
||||
"version": "2.5.5-alpha.0",
|
||||
"version": "2.5.6-alpha.32",
|
||||
"description": "Budibase backend core libraries used in server and worker",
|
||||
"main": "dist/src/index.js",
|
||||
"types": "dist/src/index.d.ts",
|
||||
|
@ -24,7 +24,7 @@
|
|||
"dependencies": {
|
||||
"@budibase/nano": "10.1.2",
|
||||
"@budibase/pouchdb-replication-stream": "1.2.10",
|
||||
"@budibase/types": "2.5.5-alpha.0",
|
||||
"@budibase/types": "2.5.6-alpha.32",
|
||||
"@shopify/jest-koa-mocks": "5.0.1",
|
||||
"@techpass/passport-openidconnect": "0.3.2",
|
||||
"aws-cloudfront-sign": "2.2.0",
|
||||
|
@ -47,6 +47,8 @@
|
|||
"passport-jwt": "4.0.0",
|
||||
"passport-local": "1.0.0",
|
||||
"passport-oauth2-refresh": "^2.1.0",
|
||||
"pino": "8.11.0",
|
||||
"pino-http": "8.3.3",
|
||||
"posthog-node": "1.3.0",
|
||||
"pouchdb": "7.3.0",
|
||||
"pouchdb-find": "7.2.2",
|
||||
|
@ -54,8 +56,7 @@
|
|||
"sanitize-s3-objectkey": "0.0.1",
|
||||
"semver": "7.3.7",
|
||||
"tar-fs": "2.1.1",
|
||||
"uuid": "8.3.2",
|
||||
"zlib": "1.0.5"
|
||||
"uuid": "8.3.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@jest/test-sequencer": "29.5.0",
|
||||
|
@ -81,7 +82,6 @@
|
|||
"jest-serial-runner": "^1.2.1",
|
||||
"koa": "2.13.4",
|
||||
"nodemon": "2.0.16",
|
||||
"pino": "7.11.0",
|
||||
"pino-pretty": "10.0.0",
|
||||
"pouchdb-adapter-memory": "7.2.2",
|
||||
"timekeeper": "2.2.0",
|
||||
|
|
|
@ -14,6 +14,7 @@ export enum ViewName {
|
|||
USER_BY_APP = "by_app",
|
||||
USER_BY_EMAIL = "by_email2",
|
||||
BY_API_KEY = "by_api_key",
|
||||
/** @deprecated - could be deleted */
|
||||
USER_BY_BUILDERS = "by_builders",
|
||||
LINK = "by_link",
|
||||
ROUTING = "screen_routes",
|
||||
|
|
|
@ -115,10 +115,10 @@ export async function doInContext(appId: string, task: any): Promise<any> {
|
|||
)
|
||||
}
|
||||
|
||||
export async function doInTenant(
|
||||
export async function doInTenant<T>(
|
||||
tenantId: string | null,
|
||||
task: any
|
||||
): Promise<any> {
|
||||
task: () => T
|
||||
): Promise<T> {
|
||||
// make sure default always selected in single tenancy
|
||||
if (!env.MULTI_TENANCY) {
|
||||
tenantId = tenantId || DEFAULT_TENANT_ID
|
||||
|
|
|
@ -243,7 +243,7 @@ export class QueryBuilder<T> {
|
|||
}
|
||||
// Escape characters
|
||||
if (!this.#noEscaping && escape && originalType === "string") {
|
||||
value = `${value}`.replace(/[ #+\-&|!(){}\]^"~*?:\\]/g, "\\$&")
|
||||
value = `${value}`.replace(/[ \/#+\-&|!(){}\]^"~*?:\\]/g, "\\$&")
|
||||
}
|
||||
|
||||
// Wrap in quotes
|
||||
|
@ -320,6 +320,18 @@ export class QueryBuilder<T> {
|
|||
return `${key}:(${statement})`
|
||||
}
|
||||
|
||||
const fuzzy = (key: string, value: any) => {
|
||||
if (!value) {
|
||||
return null
|
||||
}
|
||||
value = builder.preprocess(value, {
|
||||
escape: true,
|
||||
lowercase: true,
|
||||
type: "fuzzy",
|
||||
})
|
||||
return `${key}:/.*${value}.*/`
|
||||
}
|
||||
|
||||
const notContains = (key: string, value: any) => {
|
||||
const allPrefix = allOr ? "*:* AND " : ""
|
||||
const mode = allOr ? "AND" : undefined
|
||||
|
@ -408,17 +420,7 @@ export class QueryBuilder<T> {
|
|||
})
|
||||
}
|
||||
if (this.#query.fuzzy) {
|
||||
build(this.#query.fuzzy, (key: string, value: any) => {
|
||||
if (!value) {
|
||||
return null
|
||||
}
|
||||
value = builder.preprocess(value, {
|
||||
escape: true,
|
||||
lowercase: true,
|
||||
type: "fuzzy",
|
||||
})
|
||||
return `${key}:${value}~`
|
||||
})
|
||||
build(this.#query.fuzzy, fuzzy)
|
||||
}
|
||||
if (this.#query.equal) {
|
||||
build(this.#query.equal, equal)
|
||||
|
|
|
@ -7,7 +7,7 @@ import {
|
|||
} from "../constants"
|
||||
import { getGlobalDB } from "../context"
|
||||
import { doWithDB } from "./"
|
||||
import { Database, DatabaseQueryOpts } from "@budibase/types"
|
||||
import { AllDocsResponse, Database, DatabaseQueryOpts } from "@budibase/types"
|
||||
import env from "../environment"
|
||||
|
||||
const DESIGN_DB = "_design/database"
|
||||
|
@ -42,7 +42,11 @@ async function removeDeprecated(db: Database, viewName: ViewName) {
|
|||
}
|
||||
}
|
||||
|
||||
export async function createView(db: any, viewJs: string, viewName: string) {
|
||||
export async function createView(
|
||||
db: any,
|
||||
viewJs: string,
|
||||
viewName: string
|
||||
): Promise<void> {
|
||||
let designDoc
|
||||
try {
|
||||
designDoc = (await db.get(DESIGN_DB)) as DesignDocument
|
||||
|
@ -57,7 +61,15 @@ export async function createView(db: any, viewJs: string, viewName: string) {
|
|||
...designDoc.views,
|
||||
[viewName]: view,
|
||||
}
|
||||
await db.put(designDoc)
|
||||
try {
|
||||
await db.put(designDoc)
|
||||
} catch (err: any) {
|
||||
if (err.status === 409) {
|
||||
return await createView(db, viewJs, viewName)
|
||||
} else {
|
||||
throw err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const createNewUserEmailView = async () => {
|
||||
|
@ -107,6 +119,34 @@ export interface QueryViewOptions {
|
|||
arrayResponse?: boolean
|
||||
}
|
||||
|
||||
export async function queryViewRaw<T>(
|
||||
viewName: ViewName,
|
||||
params: DatabaseQueryOpts,
|
||||
db: Database,
|
||||
createFunc: any,
|
||||
opts?: QueryViewOptions
|
||||
): Promise<AllDocsResponse<T>> {
|
||||
try {
|
||||
const response = await db.query<T>(`database/${viewName}`, params)
|
||||
// await to catch error
|
||||
return response
|
||||
} catch (err: any) {
|
||||
const pouchNotFound = err && err.name === "not_found"
|
||||
const couchNotFound = err && err.status === 404
|
||||
if (pouchNotFound || couchNotFound) {
|
||||
await removeDeprecated(db, viewName)
|
||||
await createFunc()
|
||||
return queryViewRaw(viewName, params, db, createFunc, opts)
|
||||
} else if (err.status === 409) {
|
||||
// can happen when multiple queries occur at once, view couldn't be created
|
||||
// other design docs being updated, re-run
|
||||
return queryViewRaw(viewName, params, db, createFunc, opts)
|
||||
} else {
|
||||
throw err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const queryView = async <T>(
|
||||
viewName: ViewName,
|
||||
params: DatabaseQueryOpts,
|
||||
|
@ -114,30 +154,18 @@ export const queryView = async <T>(
|
|||
createFunc: any,
|
||||
opts?: QueryViewOptions
|
||||
): Promise<T[] | T | undefined> => {
|
||||
try {
|
||||
let response = await db.query<T>(`database/${viewName}`, params)
|
||||
const rows = response.rows
|
||||
const docs = rows.map((row: any) =>
|
||||
params.include_docs ? row.doc : row.value
|
||||
)
|
||||
const response = await queryViewRaw<T>(viewName, params, db, createFunc, opts)
|
||||
const rows = response.rows
|
||||
const docs = rows.map((row: any) =>
|
||||
params.include_docs ? row.doc : row.value
|
||||
)
|
||||
|
||||
// if arrayResponse has been requested, always return array regardless of length
|
||||
if (opts?.arrayResponse) {
|
||||
return docs as T[]
|
||||
} else {
|
||||
// return the single document if there is only one
|
||||
return docs.length <= 1 ? (docs[0] as T) : (docs as T[])
|
||||
}
|
||||
} catch (err: any) {
|
||||
const pouchNotFound = err && err.name === "not_found"
|
||||
const couchNotFound = err && err.status === 404
|
||||
if (pouchNotFound || couchNotFound) {
|
||||
await removeDeprecated(db, viewName)
|
||||
await createFunc()
|
||||
return queryView(viewName, params, db, createFunc, opts)
|
||||
} else {
|
||||
throw err
|
||||
}
|
||||
// if arrayResponse has been requested, always return array regardless of length
|
||||
if (opts?.arrayResponse) {
|
||||
return docs as T[]
|
||||
} else {
|
||||
// return the single document if there is only one
|
||||
return docs.length <= 1 ? (docs[0] as T) : (docs as T[])
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -192,18 +220,19 @@ export const queryPlatformView = async <T>(
|
|||
})
|
||||
}
|
||||
|
||||
const CreateFuncByName: any = {
|
||||
[ViewName.USER_BY_EMAIL]: createNewUserEmailView,
|
||||
[ViewName.BY_API_KEY]: createApiKeyView,
|
||||
[ViewName.USER_BY_BUILDERS]: createUserBuildersView,
|
||||
[ViewName.USER_BY_APP]: createUserAppView,
|
||||
}
|
||||
|
||||
export const queryGlobalView = async <T>(
|
||||
viewName: ViewName,
|
||||
params: DatabaseQueryOpts,
|
||||
db?: Database,
|
||||
opts?: QueryViewOptions
|
||||
): Promise<T[] | T | undefined> => {
|
||||
const CreateFuncByName: any = {
|
||||
[ViewName.USER_BY_EMAIL]: createNewUserEmailView,
|
||||
[ViewName.BY_API_KEY]: createApiKeyView,
|
||||
[ViewName.USER_BY_BUILDERS]: createUserBuildersView,
|
||||
[ViewName.USER_BY_APP]: createUserAppView,
|
||||
}
|
||||
// can pass DB in if working with something specific
|
||||
if (!db) {
|
||||
db = getGlobalDB()
|
||||
|
@ -211,3 +240,13 @@ export const queryGlobalView = async <T>(
|
|||
const createFn = CreateFuncByName[viewName]
|
||||
return queryView(viewName, params, db!, createFn, opts)
|
||||
}
|
||||
|
||||
export async function queryGlobalViewRaw<T>(
|
||||
viewName: ViewName,
|
||||
params: DatabaseQueryOpts,
|
||||
opts?: QueryViewOptions
|
||||
) {
|
||||
const db = getGlobalDB()
|
||||
const createFn = CreateFuncByName[viewName]
|
||||
return queryViewRaw<T>(viewName, params, db, createFn, opts)
|
||||
}
|
||||
|
|
|
@ -0,0 +1,29 @@
|
|||
import { asyncEventQueue, init as initQueue } from "../events/asyncEvents"
|
||||
import {
|
||||
ProcessorMap,
|
||||
default as DocumentUpdateProcessor,
|
||||
} from "../events/processors/async/DocumentUpdateProcessor"
|
||||
|
||||
let processingPromise: Promise<void>
|
||||
let documentProcessor: DocumentUpdateProcessor
|
||||
|
||||
export function init(processors: ProcessorMap) {
|
||||
if (!asyncEventQueue) {
|
||||
initQueue()
|
||||
}
|
||||
if (!documentProcessor) {
|
||||
documentProcessor = new DocumentUpdateProcessor(processors)
|
||||
}
|
||||
// if not processing in this instance, kick it off
|
||||
if (!processingPromise) {
|
||||
processingPromise = asyncEventQueue.process(async job => {
|
||||
const { event, identity, properties, timestamp } = job.data
|
||||
await documentProcessor.processEvent(
|
||||
event,
|
||||
identity,
|
||||
properties,
|
||||
timestamp
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
|
@ -1,3 +1,5 @@
|
|||
import { existsSync, readFileSync } from "fs"
|
||||
|
||||
function isTest() {
|
||||
return isCypress() || isJest()
|
||||
}
|
||||
|
@ -45,6 +47,35 @@ function httpLogging() {
|
|||
return process.env.HTTP_LOGGING
|
||||
}
|
||||
|
||||
function findVersion() {
|
||||
function findFileInAncestors(
|
||||
fileName: string,
|
||||
currentDir: string
|
||||
): string | null {
|
||||
const filePath = `${currentDir}/${fileName}`
|
||||
if (existsSync(filePath)) {
|
||||
return filePath
|
||||
}
|
||||
|
||||
const parentDir = `${currentDir}/..`
|
||||
if (parentDir === currentDir) {
|
||||
// reached root directory
|
||||
return null
|
||||
}
|
||||
|
||||
return findFileInAncestors(fileName, parentDir)
|
||||
}
|
||||
|
||||
try {
|
||||
const packageJsonFile = findFileInAncestors("package.json", process.cwd())
|
||||
const content = readFileSync(packageJsonFile!, "utf-8")
|
||||
const version = JSON.parse(content).version
|
||||
return version
|
||||
} catch {
|
||||
throw new Error("Cannot find a valid version in its package.json")
|
||||
}
|
||||
}
|
||||
|
||||
const environment = {
|
||||
isTest,
|
||||
isJest,
|
||||
|
@ -122,6 +153,7 @@ const environment = {
|
|||
ENABLE_SSO_MAINTENANCE_MODE: selfHosted
|
||||
? process.env.ENABLE_SSO_MAINTENANCE_MODE
|
||||
: false,
|
||||
VERSION: findVersion(),
|
||||
_set(key: any, value: any) {
|
||||
process.env[key] = value
|
||||
// @ts-ignore
|
||||
|
|
|
@ -0,0 +1,2 @@
|
|||
export * from "./queue"
|
||||
export * from "./publisher"
|
|
@ -0,0 +1,12 @@
|
|||
import { AsyncEvents } from "@budibase/types"
|
||||
import { EventPayload, asyncEventQueue, init } from "./queue"
|
||||
|
||||
export async function publishAsyncEvent(payload: EventPayload) {
|
||||
if (!asyncEventQueue) {
|
||||
init()
|
||||
}
|
||||
const { event, identity } = payload
|
||||
if (AsyncEvents.indexOf(event) !== -1 && identity.tenantId) {
|
||||
await asyncEventQueue.add(payload)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,22 @@
|
|||
import BullQueue from "bull"
|
||||
import { createQueue, JobQueue } from "../../queue"
|
||||
import { Event, Identity } from "@budibase/types"
|
||||
|
||||
export interface EventPayload {
|
||||
event: Event
|
||||
identity: Identity
|
||||
properties: any
|
||||
timestamp?: string | number
|
||||
}
|
||||
|
||||
export let asyncEventQueue: BullQueue.Queue
|
||||
|
||||
export function init() {
|
||||
asyncEventQueue = createQueue<EventPayload>(JobQueue.SYSTEM_EVENT_QUEUE)
|
||||
}
|
||||
|
||||
export async function shutdown() {
|
||||
if (asyncEventQueue) {
|
||||
await asyncEventQueue.close()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,56 @@
|
|||
import {
|
||||
Event,
|
||||
UserCreatedEvent,
|
||||
UserUpdatedEvent,
|
||||
UserDeletedEvent,
|
||||
UserPermissionAssignedEvent,
|
||||
UserPermissionRemovedEvent,
|
||||
GroupCreatedEvent,
|
||||
GroupUpdatedEvent,
|
||||
GroupDeletedEvent,
|
||||
GroupUsersAddedEvent,
|
||||
GroupUsersDeletedEvent,
|
||||
GroupPermissionsEditedEvent,
|
||||
} from "@budibase/types"
|
||||
|
||||
const getEventProperties: Record<
|
||||
string,
|
||||
(properties: any) => string | undefined
|
||||
> = {
|
||||
[Event.USER_CREATED]: (properties: UserCreatedEvent) => properties.userId,
|
||||
[Event.USER_UPDATED]: (properties: UserUpdatedEvent) => properties.userId,
|
||||
[Event.USER_DELETED]: (properties: UserDeletedEvent) => properties.userId,
|
||||
[Event.USER_PERMISSION_ADMIN_ASSIGNED]: (
|
||||
properties: UserPermissionAssignedEvent
|
||||
) => properties.userId,
|
||||
[Event.USER_PERMISSION_ADMIN_REMOVED]: (
|
||||
properties: UserPermissionRemovedEvent
|
||||
) => properties.userId,
|
||||
[Event.USER_PERMISSION_BUILDER_ASSIGNED]: (
|
||||
properties: UserPermissionAssignedEvent
|
||||
) => properties.userId,
|
||||
[Event.USER_PERMISSION_BUILDER_REMOVED]: (
|
||||
properties: UserPermissionRemovedEvent
|
||||
) => properties.userId,
|
||||
[Event.USER_GROUP_CREATED]: (properties: GroupCreatedEvent) =>
|
||||
properties.groupId,
|
||||
[Event.USER_GROUP_UPDATED]: (properties: GroupUpdatedEvent) =>
|
||||
properties.groupId,
|
||||
[Event.USER_GROUP_DELETED]: (properties: GroupDeletedEvent) =>
|
||||
properties.groupId,
|
||||
[Event.USER_GROUP_USERS_ADDED]: (properties: GroupUsersAddedEvent) =>
|
||||
properties.groupId,
|
||||
[Event.USER_GROUP_USERS_REMOVED]: (properties: GroupUsersDeletedEvent) =>
|
||||
properties.groupId,
|
||||
[Event.USER_GROUP_PERMISSIONS_EDITED]: (
|
||||
properties: GroupPermissionsEditedEvent
|
||||
) => properties.groupId,
|
||||
}
|
||||
|
||||
export function getDocumentId(event: Event, properties: any) {
|
||||
const extractor = getEventProperties[event]
|
||||
if (!extractor) {
|
||||
throw new Error("Event does not have a method of document ID extraction")
|
||||
}
|
||||
return extractor(properties)
|
||||
}
|
|
@ -1,7 +1,8 @@
|
|||
import { Event, AuditedEventFriendlyName } from "@budibase/types"
|
||||
import { Event } from "@budibase/types"
|
||||
import { processors } from "./processors"
|
||||
import identification from "./identification"
|
||||
import * as backfill from "./backfill"
|
||||
import { publishAsyncEvent } from "./asyncEvents"
|
||||
|
||||
export const publishEvent = async (
|
||||
event: Event,
|
||||
|
@ -14,6 +15,14 @@ export const publishEvent = async (
|
|||
const backfilling = await backfill.isBackfillingEvent(event)
|
||||
// no backfill - send the event and exit
|
||||
if (!backfilling) {
|
||||
// send off async events if required
|
||||
await publishAsyncEvent({
|
||||
event,
|
||||
identity,
|
||||
properties,
|
||||
timestamp,
|
||||
})
|
||||
// now handle the main sync event processing pipeline
|
||||
await processors.processEvent(event, identity, properties, timestamp)
|
||||
return
|
||||
}
|
||||
|
|
|
@ -23,8 +23,6 @@ import * as installation from "../installation"
|
|||
import * as configs from "../configs"
|
||||
import { withCache, TTL, CacheKey } from "../cache/generic"
|
||||
|
||||
const pkg = require("../../package.json")
|
||||
|
||||
/**
|
||||
* An identity can be:
|
||||
* - account user (Self host)
|
||||
|
@ -65,6 +63,7 @@ const getCurrentIdentity = async (): Promise<Identity> => {
|
|||
hosting,
|
||||
installationId,
|
||||
tenantId,
|
||||
realTenantId: context.getTenantId(),
|
||||
environment,
|
||||
}
|
||||
} else if (identityType === IdentityType.USER) {
|
||||
|
@ -101,7 +100,7 @@ const identifyInstallationGroup = async (
|
|||
const id = installId
|
||||
const type = IdentityType.INSTALLATION
|
||||
const hosting = getHostingFromEnv()
|
||||
const version = pkg.version
|
||||
const version = env.VERSION
|
||||
const environment = getDeploymentEnvironment()
|
||||
|
||||
const group: InstallationGroup = {
|
||||
|
@ -305,4 +304,5 @@ export default {
|
|||
identify,
|
||||
identifyGroup,
|
||||
getInstallationId,
|
||||
getUniqueTenantId,
|
||||
}
|
||||
|
|
|
@ -6,6 +6,8 @@ export * as backfillCache from "./backfill"
|
|||
|
||||
import { processors } from "./processors"
|
||||
|
||||
export function initAsyncEvents() {}
|
||||
|
||||
export const shutdown = () => {
|
||||
processors.shutdown()
|
||||
console.log("Events shutdown")
|
||||
|
|
|
@ -25,7 +25,9 @@ export default class Processor implements EventProcessor {
|
|||
timestamp?: string | number
|
||||
): Promise<void> {
|
||||
for (const eventProcessor of this.processors) {
|
||||
await eventProcessor.identify(identity, timestamp)
|
||||
if (eventProcessor.identify) {
|
||||
await eventProcessor.identify(identity, timestamp)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -34,13 +36,17 @@ export default class Processor implements EventProcessor {
|
|||
timestamp?: string | number
|
||||
): Promise<void> {
|
||||
for (const eventProcessor of this.processors) {
|
||||
await eventProcessor.identifyGroup(identity, timestamp)
|
||||
if (eventProcessor.identifyGroup) {
|
||||
await eventProcessor.identifyGroup(identity, timestamp)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
shutdown() {
|
||||
for (const eventProcessor of this.processors) {
|
||||
eventProcessor.shutdown()
|
||||
if (eventProcessor.shutdown) {
|
||||
eventProcessor.shutdown()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,43 @@
|
|||
import { EventProcessor } from "../types"
|
||||
import { Event, Identity, DocUpdateEvent } from "@budibase/types"
|
||||
import { doInTenant } from "../../../context"
|
||||
import { getDocumentId } from "../../documentId"
|
||||
import { shutdown } from "../../asyncEvents"
|
||||
|
||||
export type Processor = (update: DocUpdateEvent) => Promise<void>
|
||||
export type ProcessorMap = { events: Event[]; processor: Processor }[]
|
||||
|
||||
export default class DocumentUpdateProcessor implements EventProcessor {
|
||||
processors: ProcessorMap = []
|
||||
|
||||
constructor(processors: ProcessorMap) {
|
||||
this.processors = processors
|
||||
}
|
||||
|
||||
async processEvent(
|
||||
event: Event,
|
||||
identity: Identity,
|
||||
properties: any,
|
||||
timestamp?: string | number
|
||||
) {
|
||||
const tenantId = identity.realTenantId
|
||||
const docId = getDocumentId(event, properties)
|
||||
if (!tenantId || !docId) {
|
||||
return
|
||||
}
|
||||
for (let { events, processor } of this.processors) {
|
||||
if (events.includes(event)) {
|
||||
await doInTenant(tenantId, async () => {
|
||||
await processor({
|
||||
id: docId,
|
||||
tenantId,
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
shutdown() {
|
||||
return shutdown()
|
||||
}
|
||||
}
|
|
@ -4,7 +4,6 @@ import { EventProcessor } from "../types"
|
|||
import env from "../../../environment"
|
||||
import * as context from "../../../context"
|
||||
import * as rateLimiting from "./rateLimiting"
|
||||
const pkg = require("../../../../package.json")
|
||||
|
||||
const EXCLUDED_EVENTS: Event[] = [
|
||||
Event.USER_UPDATED,
|
||||
|
@ -49,7 +48,7 @@ export default class PosthogProcessor implements EventProcessor {
|
|||
|
||||
properties = this.clearPIIProperties(properties)
|
||||
|
||||
properties.version = pkg.version
|
||||
properties.version = env.VERSION
|
||||
properties.service = env.SERVICE
|
||||
properties.environment = identity.environment
|
||||
properties.hosting = identity.hosting
|
||||
|
|
|
@ -1,18 +1 @@
|
|||
import { Event, Identity, Group } from "@budibase/types"
|
||||
|
||||
export enum EventProcessorType {
|
||||
POSTHOG = "posthog",
|
||||
LOGGING = "logging",
|
||||
}
|
||||
|
||||
export interface EventProcessor {
|
||||
processEvent(
|
||||
event: Event,
|
||||
identity: Identity,
|
||||
properties: any,
|
||||
timestamp?: string | number
|
||||
): Promise<void>
|
||||
identify(identity: Identity, timestamp?: string | number): Promise<void>
|
||||
identifyGroup(group: Group, timestamp?: string | number): Promise<void>
|
||||
shutdown(): void
|
||||
}
|
||||
export { EventProcessor } from "@budibase/types"
|
||||
|
|
|
@ -27,6 +27,7 @@ export * as errors from "./errors"
|
|||
export * as timers from "./timers"
|
||||
export { default as env } from "./environment"
|
||||
export * as blacklist from "./blacklist"
|
||||
export * as docUpdates from "./docUpdates"
|
||||
export { SearchParams } from "./db"
|
||||
// Add context to tenancy for backwards compatibility
|
||||
// only do this for external usages to prevent internal
|
||||
|
|
|
@ -6,8 +6,7 @@ import { Installation, IdentityType, Database } from "@budibase/types"
|
|||
import * as context from "./context"
|
||||
import semver from "semver"
|
||||
import { bustCache, withCache, TTL, CacheKey } from "./cache/generic"
|
||||
|
||||
const pkg = require("../package.json")
|
||||
import environment from "./environment"
|
||||
|
||||
export const getInstall = async (): Promise<Installation> => {
|
||||
return withCache(CacheKey.INSTALLATION, TTL.ONE_DAY, getInstallFromDB, {
|
||||
|
@ -18,7 +17,7 @@ async function createInstallDoc(platformDb: Database) {
|
|||
const install: Installation = {
|
||||
_id: StaticDatabases.PLATFORM_INFO.docs.install,
|
||||
installId: newid(),
|
||||
version: pkg.version,
|
||||
version: environment.VERSION,
|
||||
}
|
||||
try {
|
||||
const resp = await platformDb.put(install)
|
||||
|
@ -33,7 +32,7 @@ async function createInstallDoc(platformDb: Database) {
|
|||
}
|
||||
}
|
||||
|
||||
const getInstallFromDB = async (): Promise<Installation> => {
|
||||
export const getInstallFromDB = async (): Promise<Installation> => {
|
||||
return doWithDB(
|
||||
StaticDatabases.PLATFORM_INFO.name,
|
||||
async (platformDb: any) => {
|
||||
|
@ -80,7 +79,7 @@ export const checkInstallVersion = async (): Promise<void> => {
|
|||
const install = await getInstall()
|
||||
|
||||
const currentVersion = install.version
|
||||
const newVersion = pkg.version
|
||||
const newVersion = environment.VERSION
|
||||
|
||||
if (currentVersion !== newVersion) {
|
||||
const isUpgrade = semver.gt(newVersion, currentVersion)
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
export * as correlation from "./correlation/correlation"
|
||||
export { default as logger } from "./pino/logger"
|
||||
export { logger, disableLogger } from "./pino/logger"
|
||||
export * from "./alerts"
|
||||
|
||||
// turn off or on context logging i.e. tenantId, appId etc
|
||||
|
|
|
@ -5,6 +5,17 @@ import * as correlation from "../correlation"
|
|||
import { IdentityType } from "@budibase/types"
|
||||
import { LOG_CONTEXT } from "../index"
|
||||
|
||||
// CORE LOGGERS - for disabling
|
||||
|
||||
const BUILT_INS = {
|
||||
log: console.log,
|
||||
error: console.error,
|
||||
info: console.info,
|
||||
warn: console.warn,
|
||||
trace: console.trace,
|
||||
debug: console.debug,
|
||||
}
|
||||
|
||||
// LOGGER
|
||||
|
||||
const pinoOptions: LoggerOptions = {
|
||||
|
@ -31,6 +42,15 @@ if (env.isDev()) {
|
|||
|
||||
export const logger = pino(pinoOptions)
|
||||
|
||||
export function disableLogger() {
|
||||
console.log = BUILT_INS.log
|
||||
console.error = BUILT_INS.error
|
||||
console.info = BUILT_INS.info
|
||||
console.warn = BUILT_INS.warn
|
||||
console.trace = BUILT_INS.trace
|
||||
console.debug = BUILT_INS.debug
|
||||
}
|
||||
|
||||
// CONSOLE OVERRIDES
|
||||
|
||||
interface MergingObject {
|
||||
|
@ -166,5 +186,3 @@ const getIdentity = () => {
|
|||
}
|
||||
return identity
|
||||
}
|
||||
|
||||
export default logger
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import env from "../../environment"
|
||||
import logger from "./logger"
|
||||
import { logger } from "./logger"
|
||||
import { IncomingMessage } from "http"
|
||||
const pino = require("koa-pino-logger")
|
||||
import { Options } from "pino-http"
|
||||
|
|
|
@ -44,7 +44,7 @@ async function checkApiKey(apiKey: string, populateUser?: Function) {
|
|||
// check both the primary and the fallback internal api keys
|
||||
// this allows for rotation
|
||||
if (isValidInternalAPIKey(apiKey)) {
|
||||
return { valid: true }
|
||||
return { valid: true, user: undefined }
|
||||
}
|
||||
const decrypted = decrypt(apiKey)
|
||||
const tenantId = decrypted.split(SEPARATOR)[0]
|
||||
|
|
|
@ -3,7 +3,7 @@ import AWS from "aws-sdk"
|
|||
import stream from "stream"
|
||||
import fetch from "node-fetch"
|
||||
import tar from "tar-fs"
|
||||
const zlib = require("zlib")
|
||||
import zlib from "zlib"
|
||||
import { promisify } from "util"
|
||||
import { join } from "path"
|
||||
import fs from "fs"
|
||||
|
@ -415,7 +415,7 @@ export const downloadTarballDirect = async (
|
|||
throw new Error(`unexpected response ${response.statusText}`)
|
||||
}
|
||||
|
||||
await streamPipeline(response.body, zlib.Unzip(), tar.extract(path))
|
||||
await streamPipeline(response.body, zlib.createUnzip(), tar.extract(path))
|
||||
}
|
||||
|
||||
export const downloadTarball = async (
|
||||
|
@ -431,7 +431,7 @@ export const downloadTarball = async (
|
|||
}
|
||||
|
||||
const tmpPath = join(budibaseTempDir(), path)
|
||||
await streamPipeline(response.body, zlib.Unzip(), tar.extract(tmpPath))
|
||||
await streamPipeline(response.body, zlib.createUnzip(), tar.extract(tmpPath))
|
||||
if (!env.isTest() && env.SELF_HOSTED) {
|
||||
await uploadDirectory(bucketName, tmpPath, path)
|
||||
}
|
||||
|
|
|
@ -0,0 +1,83 @@
|
|||
import { validate } from "../utils"
|
||||
import fetch from "node-fetch"
|
||||
import { PluginType } from "@budibase/types"
|
||||
|
||||
const repoUrl =
|
||||
"https://raw.githubusercontent.com/Budibase/budibase-skeleton/master"
|
||||
const automationLink = `${repoUrl}/automation/schema.json.hbs`
|
||||
const componentLink = `${repoUrl}/component/schema.json.hbs`
|
||||
const datasourceLink = `${repoUrl}/datasource/schema.json.hbs`
|
||||
|
||||
async function getSchema(link: string) {
|
||||
const response = await fetch(link)
|
||||
if (response.status > 300) {
|
||||
return
|
||||
}
|
||||
const text = await response.text()
|
||||
return JSON.parse(text)
|
||||
}
|
||||
|
||||
async function runTest(opts: { link?: string; schema?: any }) {
|
||||
let error
|
||||
try {
|
||||
let schema = opts.schema
|
||||
if (opts.link) {
|
||||
schema = await getSchema(opts.link)
|
||||
}
|
||||
validate(schema)
|
||||
} catch (err) {
|
||||
error = err
|
||||
}
|
||||
return error
|
||||
}
|
||||
|
||||
describe("it should be able to validate an automation schema", () => {
|
||||
it("should return automation skeleton schema is valid", async () => {
|
||||
const error = await runTest({ link: automationLink })
|
||||
expect(error).toBeUndefined()
|
||||
})
|
||||
|
||||
it("should fail given invalid automation schema", async () => {
|
||||
const error = await runTest({
|
||||
schema: {
|
||||
type: PluginType.AUTOMATION,
|
||||
schema: {},
|
||||
},
|
||||
})
|
||||
expect(error).toBeDefined()
|
||||
})
|
||||
})
|
||||
|
||||
describe("it should be able to validate a component schema", () => {
|
||||
it("should return component skeleton schema is valid", async () => {
|
||||
const error = await runTest({ link: componentLink })
|
||||
expect(error).toBeUndefined()
|
||||
})
|
||||
|
||||
it("should fail given invalid component schema", async () => {
|
||||
const error = await runTest({
|
||||
schema: {
|
||||
type: PluginType.COMPONENT,
|
||||
schema: {},
|
||||
},
|
||||
})
|
||||
expect(error).toBeDefined()
|
||||
})
|
||||
})
|
||||
|
||||
describe("it should be able to validate a datasource schema", () => {
|
||||
it("should return datasource skeleton schema is valid", async () => {
|
||||
const error = await runTest({ link: datasourceLink })
|
||||
expect(error).toBeUndefined()
|
||||
})
|
||||
|
||||
it("should fail given invalid datasource schema", async () => {
|
||||
const error = await runTest({
|
||||
schema: {
|
||||
type: PluginType.DATASOURCE,
|
||||
schema: {},
|
||||
},
|
||||
})
|
||||
expect(error).toBeDefined()
|
||||
})
|
||||
})
|
|
@ -1,4 +1,12 @@
|
|||
import { DatasourceFieldType, QueryType, PluginType } from "@budibase/types"
|
||||
import {
|
||||
DatasourceFieldType,
|
||||
QueryType,
|
||||
PluginType,
|
||||
AutomationStepType,
|
||||
AutomationStepIdArray,
|
||||
AutomationIOType,
|
||||
AutomationCustomIOType,
|
||||
} from "@budibase/types"
|
||||
import joi from "joi"
|
||||
|
||||
const DATASOURCE_TYPES = [
|
||||
|
@ -19,7 +27,7 @@ function runJoi(validator: joi.Schema, schema: any) {
|
|||
|
||||
function validateComponent(schema: any) {
|
||||
const validator = joi.object({
|
||||
type: joi.string().allow("component").required(),
|
||||
type: joi.string().allow(PluginType.COMPONENT).required(),
|
||||
metadata: joi.object().unknown(true).required(),
|
||||
hash: joi.string().optional(),
|
||||
version: joi.string().optional(),
|
||||
|
@ -53,7 +61,7 @@ function validateDatasource(schema: any) {
|
|||
.required()
|
||||
|
||||
const validator = joi.object({
|
||||
type: joi.string().allow("datasource").required(),
|
||||
type: joi.string().allow(PluginType.DATASOURCE).required(),
|
||||
metadata: joi.object().unknown(true).required(),
|
||||
hash: joi.string().optional(),
|
||||
version: joi.string().optional(),
|
||||
|
@ -82,6 +90,55 @@ function validateDatasource(schema: any) {
|
|||
runJoi(validator, schema)
|
||||
}
|
||||
|
||||
function validateAutomation(schema: any) {
|
||||
const basePropsValidator = joi.object().pattern(joi.string(), {
|
||||
type: joi
|
||||
.string()
|
||||
.allow(...Object.values(AutomationIOType))
|
||||
.required(),
|
||||
customType: joi.string().allow(...Object.values(AutomationCustomIOType)),
|
||||
title: joi.string(),
|
||||
description: joi.string(),
|
||||
enum: joi.array().items(joi.string()),
|
||||
pretty: joi.array().items(joi.string()),
|
||||
})
|
||||
const stepSchemaValidator = joi
|
||||
.object({
|
||||
properties: basePropsValidator,
|
||||
required: joi.array().items(joi.string()),
|
||||
})
|
||||
.concat(basePropsValidator)
|
||||
.required()
|
||||
const validator = joi.object({
|
||||
type: joi.string().allow(PluginType.AUTOMATION).required(),
|
||||
metadata: joi.object().unknown(true).required(),
|
||||
hash: joi.string().optional(),
|
||||
version: joi.string().optional(),
|
||||
schema: joi.object({
|
||||
name: joi.string().required(),
|
||||
tagline: joi.string().required(),
|
||||
icon: joi.string().required(),
|
||||
description: joi.string().required(),
|
||||
type: joi
|
||||
.string()
|
||||
.allow(AutomationStepType.ACTION, AutomationStepType.LOGIC)
|
||||
.required(),
|
||||
stepId: joi
|
||||
.string()
|
||||
.disallow(...AutomationStepIdArray)
|
||||
.required(),
|
||||
inputs: joi.object().optional(),
|
||||
schema: joi
|
||||
.object({
|
||||
inputs: stepSchemaValidator,
|
||||
outputs: stepSchemaValidator,
|
||||
})
|
||||
.required(),
|
||||
}),
|
||||
})
|
||||
runJoi(validator, schema)
|
||||
}
|
||||
|
||||
export function validate(schema: any) {
|
||||
switch (schema?.type) {
|
||||
case PluginType.COMPONENT:
|
||||
|
@ -90,6 +147,9 @@ export function validate(schema: any) {
|
|||
case PluginType.DATASOURCE:
|
||||
validateDatasource(schema)
|
||||
break
|
||||
case PluginType.AUTOMATION:
|
||||
validateAutomation(schema)
|
||||
break
|
||||
default:
|
||||
throw new Error(`Unknown plugin type - check schema.json: ${schema.type}`)
|
||||
}
|
||||
|
|
|
@ -2,4 +2,5 @@ export enum JobQueue {
|
|||
AUTOMATION = "automationQueue",
|
||||
APP_BACKUP = "appBackupQueue",
|
||||
AUDIT_LOG = "auditLogQueue",
|
||||
SYSTEM_EVENT_QUEUE = "systemEventQueue",
|
||||
}
|
||||
|
|
|
@ -1,10 +1,16 @@
|
|||
import Redlock, { Options } from "redlock"
|
||||
import Redlock from "redlock"
|
||||
import { getLockClient } from "./init"
|
||||
import { LockOptions, LockType } from "@budibase/types"
|
||||
import * as context from "../context"
|
||||
import env from "../environment"
|
||||
|
||||
const getClient = async (type: LockType): Promise<Redlock> => {
|
||||
const getClient = async (
|
||||
type: LockType,
|
||||
opts?: Redlock.Options
|
||||
): Promise<Redlock> => {
|
||||
if (type === LockType.CUSTOM) {
|
||||
return newRedlock(opts)
|
||||
}
|
||||
if (env.isTest() && type !== LockType.TRY_ONCE) {
|
||||
return newRedlock(OPTIONS.TEST)
|
||||
}
|
||||
|
@ -56,7 +62,7 @@ const OPTIONS = {
|
|||
},
|
||||
}
|
||||
|
||||
const newRedlock = async (opts: Options = {}) => {
|
||||
const newRedlock = async (opts: Redlock.Options = {}) => {
|
||||
let options = { ...OPTIONS.DEFAULT, ...opts }
|
||||
const redisWrapper = await getLockClient()
|
||||
const client = redisWrapper.getClient()
|
||||
|
|
|
@ -24,7 +24,7 @@ export enum PermissionType {
|
|||
QUERY = "query",
|
||||
}
|
||||
|
||||
class Permission {
|
||||
export class Permission {
|
||||
type: PermissionType
|
||||
level: PermissionLevel
|
||||
|
||||
|
@ -34,7 +34,7 @@ class Permission {
|
|||
}
|
||||
}
|
||||
|
||||
function levelToNumber(perm: PermissionLevel) {
|
||||
export function levelToNumber(perm: PermissionLevel) {
|
||||
switch (perm) {
|
||||
// not everything has execute privileges
|
||||
case PermissionLevel.EXECUTE:
|
||||
|
@ -55,7 +55,7 @@ function levelToNumber(perm: PermissionLevel) {
|
|||
* @param {string} userPermLevel The permission level of the user.
|
||||
* @return {string[]} All the permission levels this user is allowed to carry out.
|
||||
*/
|
||||
function getAllowedLevels(userPermLevel: PermissionLevel) {
|
||||
export function getAllowedLevels(userPermLevel: PermissionLevel): string[] {
|
||||
switch (userPermLevel) {
|
||||
case PermissionLevel.EXECUTE:
|
||||
return [PermissionLevel.EXECUTE]
|
||||
|
@ -64,9 +64,9 @@ function getAllowedLevels(userPermLevel: PermissionLevel) {
|
|||
case PermissionLevel.WRITE:
|
||||
case PermissionLevel.ADMIN:
|
||||
return [
|
||||
PermissionLevel.EXECUTE,
|
||||
PermissionLevel.READ,
|
||||
PermissionLevel.WRITE,
|
||||
PermissionLevel.EXECUTE,
|
||||
]
|
||||
default:
|
||||
return []
|
||||
|
@ -81,7 +81,7 @@ export enum BuiltinPermissionID {
|
|||
POWER = "power",
|
||||
}
|
||||
|
||||
const BUILTIN_PERMISSIONS = {
|
||||
export const BUILTIN_PERMISSIONS = {
|
||||
PUBLIC: {
|
||||
_id: BuiltinPermissionID.PUBLIC,
|
||||
name: "Public",
|
||||
|
|
|
@ -0,0 +1,145 @@
|
|||
import { cloneDeep } from "lodash"
|
||||
import * as permissions from "../permissions"
|
||||
import { BUILTIN_ROLE_IDS } from "../roles"
|
||||
|
||||
describe("levelToNumber", () => {
|
||||
it("should return 0 for EXECUTE", () => {
|
||||
expect(permissions.levelToNumber(permissions.PermissionLevel.EXECUTE)).toBe(
|
||||
0
|
||||
)
|
||||
})
|
||||
|
||||
it("should return 1 for READ", () => {
|
||||
expect(permissions.levelToNumber(permissions.PermissionLevel.READ)).toBe(1)
|
||||
})
|
||||
|
||||
it("should return 2 for WRITE", () => {
|
||||
expect(permissions.levelToNumber(permissions.PermissionLevel.WRITE)).toBe(2)
|
||||
})
|
||||
|
||||
it("should return 3 for ADMIN", () => {
|
||||
expect(permissions.levelToNumber(permissions.PermissionLevel.ADMIN)).toBe(3)
|
||||
})
|
||||
|
||||
it("should return -1 for an unknown permission level", () => {
|
||||
expect(
|
||||
permissions.levelToNumber("unknown" as permissions.PermissionLevel)
|
||||
).toBe(-1)
|
||||
})
|
||||
})
|
||||
describe("getAllowedLevels", () => {
|
||||
it('should return ["execute"] for EXECUTE', () => {
|
||||
expect(
|
||||
permissions.getAllowedLevels(permissions.PermissionLevel.EXECUTE)
|
||||
).toEqual([permissions.PermissionLevel.EXECUTE])
|
||||
})
|
||||
|
||||
it('should return ["execute", "read"] for READ', () => {
|
||||
expect(
|
||||
permissions.getAllowedLevels(permissions.PermissionLevel.READ)
|
||||
).toEqual([
|
||||
permissions.PermissionLevel.EXECUTE,
|
||||
permissions.PermissionLevel.READ,
|
||||
])
|
||||
})
|
||||
|
||||
it('should return ["execute", "read", "write"] for WRITE', () => {
|
||||
expect(
|
||||
permissions.getAllowedLevels(permissions.PermissionLevel.WRITE)
|
||||
).toEqual([
|
||||
permissions.PermissionLevel.EXECUTE,
|
||||
permissions.PermissionLevel.READ,
|
||||
permissions.PermissionLevel.WRITE,
|
||||
])
|
||||
})
|
||||
|
||||
it('should return ["execute", "read", "write"] for ADMIN', () => {
|
||||
expect(
|
||||
permissions.getAllowedLevels(permissions.PermissionLevel.ADMIN)
|
||||
).toEqual([
|
||||
permissions.PermissionLevel.EXECUTE,
|
||||
permissions.PermissionLevel.READ,
|
||||
permissions.PermissionLevel.WRITE,
|
||||
])
|
||||
})
|
||||
|
||||
it("should return [] for an unknown permission level", () => {
|
||||
expect(
|
||||
permissions.getAllowedLevels("unknown" as permissions.PermissionLevel)
|
||||
).toEqual([])
|
||||
})
|
||||
})
|
||||
|
||||
describe("doesHaveBasePermission", () => {
|
||||
it("should return true if base permission has the required level", () => {
|
||||
const permType = permissions.PermissionType.USER
|
||||
const permLevel = permissions.PermissionLevel.READ
|
||||
const rolesHierarchy = [
|
||||
{
|
||||
roleId: BUILTIN_ROLE_IDS.ADMIN,
|
||||
permissionId: permissions.BuiltinPermissionID.ADMIN,
|
||||
},
|
||||
]
|
||||
expect(
|
||||
permissions.doesHaveBasePermission(permType, permLevel, rolesHierarchy)
|
||||
).toBe(true)
|
||||
})
|
||||
|
||||
it("should return false if base permission does not have the required level", () => {
|
||||
const permType = permissions.PermissionType.APP
|
||||
const permLevel = permissions.PermissionLevel.READ
|
||||
const rolesHierarchy = [
|
||||
{
|
||||
roleId: BUILTIN_ROLE_IDS.PUBLIC,
|
||||
permissionId: permissions.BuiltinPermissionID.PUBLIC,
|
||||
},
|
||||
]
|
||||
expect(
|
||||
permissions.doesHaveBasePermission(permType, permLevel, rolesHierarchy)
|
||||
).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe("isPermissionLevelHigherThanRead", () => {
|
||||
it("should return true if level is higher than read", () => {
|
||||
expect(
|
||||
permissions.isPermissionLevelHigherThanRead(
|
||||
permissions.PermissionLevel.WRITE
|
||||
)
|
||||
).toBe(true)
|
||||
})
|
||||
|
||||
it("should return false if level is read or lower", () => {
|
||||
expect(
|
||||
permissions.isPermissionLevelHigherThanRead(
|
||||
permissions.PermissionLevel.READ
|
||||
)
|
||||
).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe("getBuiltinPermissions", () => {
|
||||
it("returns a clone of the builtin permissions", () => {
|
||||
const builtins = permissions.getBuiltinPermissions()
|
||||
expect(builtins).toEqual(cloneDeep(permissions.BUILTIN_PERMISSIONS))
|
||||
expect(builtins).not.toBe(permissions.BUILTIN_PERMISSIONS)
|
||||
})
|
||||
})
|
||||
|
||||
describe("getBuiltinPermissionByID", () => {
|
||||
it("returns correct permission object for valid ID", () => {
|
||||
const expectedPermission = {
|
||||
_id: permissions.BuiltinPermissionID.PUBLIC,
|
||||
name: "Public",
|
||||
permissions: [
|
||||
new permissions.Permission(
|
||||
permissions.PermissionType.WEBHOOK,
|
||||
permissions.PermissionLevel.EXECUTE
|
||||
),
|
||||
],
|
||||
}
|
||||
expect(permissions.getBuiltinPermissionByID("public")).toEqual(
|
||||
expectedPermission
|
||||
)
|
||||
})
|
||||
})
|
|
@ -1,15 +1,16 @@
|
|||
import {
|
||||
ViewName,
|
||||
getUsersByAppParams,
|
||||
getProdAppID,
|
||||
generateAppUserID,
|
||||
queryGlobalView,
|
||||
UNICODE_MAX,
|
||||
DocumentType,
|
||||
SEPARATOR,
|
||||
directCouchFind,
|
||||
DocumentType,
|
||||
generateAppUserID,
|
||||
getGlobalUserParams,
|
||||
getProdAppID,
|
||||
getUsersByAppParams,
|
||||
pagination,
|
||||
queryGlobalView,
|
||||
queryGlobalViewRaw,
|
||||
SEPARATOR,
|
||||
UNICODE_MAX,
|
||||
ViewName,
|
||||
} from "./db"
|
||||
import { BulkDocsResponse, SearchUsersRequest, User } from "@budibase/types"
|
||||
import { getGlobalDB } from "./context"
|
||||
|
@ -239,3 +240,11 @@ export const paginatedUsers = async ({
|
|||
getKey,
|
||||
})
|
||||
}
|
||||
|
||||
export async function getUserCount() {
|
||||
const response = await queryGlobalViewRaw(ViewName.USER_BY_EMAIL, {
|
||||
limit: 0, // to be as fast as possible - we just want the total rows count
|
||||
include_docs: false,
|
||||
})
|
||||
return response.total_rows
|
||||
}
|
||||
|
|
|
@ -46,8 +46,9 @@ export async function resolveAppUrl(ctx: Ctx) {
|
|||
}
|
||||
|
||||
// search prod apps for a url that matches
|
||||
const apps: App[] = await context.doInTenant(tenantId, () =>
|
||||
getAllApps({ dev: false })
|
||||
const apps: App[] = await context.doInTenant(
|
||||
tenantId,
|
||||
() => getAllApps({ dev: false }) as Promise<App[]>
|
||||
)
|
||||
const app = apps.filter(
|
||||
a => a.url && a.url.toLowerCase() === possibleAppUrl
|
||||
|
@ -221,27 +222,6 @@ export function isClient(ctx: Ctx) {
|
|||
return ctx.headers[Header.TYPE] === "client"
|
||||
}
|
||||
|
||||
async function getBuilders() {
|
||||
const builders = await queryGlobalView(ViewName.USER_BY_BUILDERS, {
|
||||
include_docs: false,
|
||||
})
|
||||
|
||||
if (!builders) {
|
||||
return []
|
||||
}
|
||||
|
||||
if (Array.isArray(builders)) {
|
||||
return builders
|
||||
} else {
|
||||
return [builders]
|
||||
}
|
||||
}
|
||||
|
||||
export async function getBuildersCount() {
|
||||
const builders = await getBuilders()
|
||||
return builders.length
|
||||
}
|
||||
|
||||
export function timeout(timeMs: number) {
|
||||
return new Promise(resolve => setTimeout(resolve, timeMs))
|
||||
}
|
||||
|
|
|
@ -2,5 +2,5 @@ export * as mocks from "./mocks"
|
|||
export * as structures from "./structures"
|
||||
export { generator } from "./structures"
|
||||
export * as testContainerUtils from "./testContainerUtils"
|
||||
|
||||
export * as utils from "./utils"
|
||||
export * from "./jestUtils"
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
import * as events from "../../../../src/events"
|
||||
|
||||
beforeAll(async () => {
|
||||
const processors = await import("../../../../src/events/processors")
|
||||
const events = await import("../../../../src/events")
|
||||
|
@ -120,4 +122,13 @@ beforeAll(async () => {
|
|||
jest.spyOn(events.plugin, "init")
|
||||
jest.spyOn(events.plugin, "imported")
|
||||
jest.spyOn(events.plugin, "deleted")
|
||||
|
||||
jest.spyOn(events.license, "tierChanged")
|
||||
jest.spyOn(events.license, "planChanged")
|
||||
jest.spyOn(events.license, "activated")
|
||||
jest.spyOn(events.license, "checkoutOpened")
|
||||
jest.spyOn(events.license, "checkoutSuccess")
|
||||
jest.spyOn(events.license, "portalOpened")
|
||||
jest.spyOn(events.license, "paymentFailed")
|
||||
jest.spyOn(events.license, "paymentRecovered")
|
||||
})
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
const mockFetch = jest.fn((url: any, opts: any) => {
|
||||
const fetch = jest.requireActual("node-fetch")
|
||||
const env = jest.requireActual("../../../../src/environment").default
|
||||
if (url.includes(env.COUCH_DB_URL)) {
|
||||
if (url.includes(env.COUCH_DB_URL) || url.includes("raw.github")) {
|
||||
return fetch(url, opts)
|
||||
}
|
||||
return undefined
|
||||
|
|
|
@ -0,0 +1,20 @@
|
|||
import Chance from "chance"
|
||||
|
||||
export default class CustomChance extends Chance {
|
||||
arrayOf<T>(
|
||||
generateFn: () => T,
|
||||
opts: { min?: number; max?: number } = {}
|
||||
): T[] {
|
||||
const itemCount = this.integer({
|
||||
min: opts.min != null ? opts.min : 1,
|
||||
max: opts.max != null ? opts.max : 50,
|
||||
})
|
||||
|
||||
const items = []
|
||||
for (let i = 0; i < itemCount; i++) {
|
||||
items.push(generateFn())
|
||||
}
|
||||
|
||||
return items
|
||||
}
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
import { generator, uuid } from "."
|
||||
import { generator, uuid, quotas } from "."
|
||||
import { generateGlobalUserID } from "../../../../src/docIds"
|
||||
import {
|
||||
Account,
|
||||
|
@ -28,6 +28,7 @@ export const account = (): Account => {
|
|||
name: generator.name(),
|
||||
size: "10+",
|
||||
profession: "Software Engineer",
|
||||
quotaUsage: quotas.usage(),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,2 +1,2 @@
|
|||
import Chance from "chance"
|
||||
import Chance from "./Chance"
|
||||
export const generator = new Chance()
|
||||
|
|
|
@ -11,3 +11,4 @@ export * as users from "./users"
|
|||
export * as userGroups from "./userGroups"
|
||||
export { generator } from "./generator"
|
||||
export * as scim from "./scim"
|
||||
export * as quotas from "./quotas"
|
||||
|
|
|
@ -1,18 +1,132 @@
|
|||
import { AccountPlan, License, PlanType, Quotas } from "@budibase/types"
|
||||
import {
|
||||
Billing,
|
||||
Customer,
|
||||
Feature,
|
||||
License,
|
||||
PlanModel,
|
||||
PlanType,
|
||||
PriceDuration,
|
||||
PurchasedPlan,
|
||||
Quotas,
|
||||
Subscription,
|
||||
} from "@budibase/types"
|
||||
|
||||
const newPlan = (type: PlanType = PlanType.FREE): AccountPlan => {
|
||||
export const plan = (type: PlanType = PlanType.FREE): PurchasedPlan => {
|
||||
return {
|
||||
type,
|
||||
usesInvoicing: false,
|
||||
minUsers: 1,
|
||||
model: PlanModel.PER_USER,
|
||||
}
|
||||
}
|
||||
|
||||
export const newLicense = (opts: {
|
||||
quotas: Quotas
|
||||
planType?: PlanType
|
||||
}): License => {
|
||||
export function quotas(): Quotas {
|
||||
return {
|
||||
features: [],
|
||||
quotas: opts.quotas,
|
||||
plan: newPlan(opts.planType),
|
||||
usage: {
|
||||
monthly: {
|
||||
queries: {
|
||||
name: "Queries",
|
||||
value: 1,
|
||||
triggers: [],
|
||||
},
|
||||
automations: {
|
||||
name: "Queries",
|
||||
value: 1,
|
||||
triggers: [],
|
||||
},
|
||||
dayPasses: {
|
||||
name: "Queries",
|
||||
value: 1,
|
||||
triggers: [],
|
||||
},
|
||||
},
|
||||
static: {
|
||||
rows: {
|
||||
name: "Rows",
|
||||
value: 1,
|
||||
triggers: [],
|
||||
},
|
||||
apps: {
|
||||
name: "Apps",
|
||||
value: 1,
|
||||
triggers: [],
|
||||
},
|
||||
users: {
|
||||
name: "Users",
|
||||
value: 1,
|
||||
triggers: [],
|
||||
},
|
||||
userGroups: {
|
||||
name: "User Groups",
|
||||
value: 1,
|
||||
triggers: [],
|
||||
},
|
||||
plugins: {
|
||||
name: "Plugins",
|
||||
value: 1,
|
||||
triggers: [],
|
||||
},
|
||||
},
|
||||
},
|
||||
constant: {
|
||||
automationLogRetentionDays: {
|
||||
name: "Automation Logs",
|
||||
value: 1,
|
||||
triggers: [],
|
||||
},
|
||||
appBackupRetentionDays: {
|
||||
name: "Backups",
|
||||
value: 1,
|
||||
triggers: [],
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export function billing(
|
||||
opts: { customer?: Customer; subscription?: Subscription } = {}
|
||||
): Billing {
|
||||
return {
|
||||
customer: opts.customer || customer(),
|
||||
subscription: opts.subscription || subscription(),
|
||||
}
|
||||
}
|
||||
|
||||
export function customer(): Customer {
|
||||
return {
|
||||
balance: 0,
|
||||
currency: "usd",
|
||||
}
|
||||
}
|
||||
|
||||
export function subscription(): Subscription {
|
||||
return {
|
||||
amount: 10000,
|
||||
cancelAt: undefined,
|
||||
currency: "usd",
|
||||
currentPeriodEnd: 0,
|
||||
currentPeriodStart: 0,
|
||||
downgradeAt: 0,
|
||||
duration: PriceDuration.MONTHLY,
|
||||
pastDueAt: undefined,
|
||||
quantity: 0,
|
||||
status: "active",
|
||||
}
|
||||
}
|
||||
|
||||
export const license = (
|
||||
opts: {
|
||||
quotas?: Quotas
|
||||
plan?: PurchasedPlan
|
||||
planType?: PlanType
|
||||
features?: Feature[]
|
||||
billing?: Billing
|
||||
} = {}
|
||||
): License => {
|
||||
return {
|
||||
features: opts.features || [],
|
||||
quotas: opts.quotas || quotas(),
|
||||
plan: opts.plan || plan(opts.planType),
|
||||
billing: opts.billing || billing(),
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,67 @@
|
|||
import { MonthlyQuotaName, QuotaUsage } from "@budibase/types"
|
||||
|
||||
export const usage = (): QuotaUsage => {
|
||||
return {
|
||||
_id: "usage_quota",
|
||||
quotaReset: new Date().toISOString(),
|
||||
apps: {
|
||||
app_1: {
|
||||
// @ts-ignore - the apps definition doesn't match up to actual usage
|
||||
usageQuota: {
|
||||
rows: 0,
|
||||
},
|
||||
},
|
||||
},
|
||||
monthly: {
|
||||
"01-2023": {
|
||||
automations: 0,
|
||||
dayPasses: 0,
|
||||
queries: 0,
|
||||
triggers: {},
|
||||
breakdown: {
|
||||
rowQueries: {
|
||||
parent: MonthlyQuotaName.QUERIES,
|
||||
values: {
|
||||
row_1: 0,
|
||||
row_2: 0,
|
||||
},
|
||||
},
|
||||
datasourceQueries: {
|
||||
parent: MonthlyQuotaName.QUERIES,
|
||||
values: {
|
||||
ds_1: 0,
|
||||
ds_2: 0,
|
||||
},
|
||||
},
|
||||
automations: {
|
||||
parent: MonthlyQuotaName.AUTOMATIONS,
|
||||
values: {
|
||||
auto_1: 0,
|
||||
auto_2: 0,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"02-2023": {
|
||||
automations: 0,
|
||||
dayPasses: 0,
|
||||
queries: 0,
|
||||
triggers: {},
|
||||
},
|
||||
current: {
|
||||
automations: 0,
|
||||
dayPasses: 0,
|
||||
queries: 0,
|
||||
triggers: {},
|
||||
},
|
||||
},
|
||||
usageQuota: {
|
||||
apps: 0,
|
||||
plugins: 0,
|
||||
users: 0,
|
||||
userGroups: 0,
|
||||
rows: 0,
|
||||
triggers: {},
|
||||
},
|
||||
}
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
export * as time from "./time"
|
|
@ -0,0 +1,3 @@
|
|||
export function addDaysToDate(date: Date, days: number) {
|
||||
return new Date(date.getTime() + days * 24 * 60 * 60 * 1000)
|
||||
}
|
|
@ -10,15 +10,11 @@
|
|||
"incremental": true,
|
||||
"sourceMap": true,
|
||||
"declaration": true,
|
||||
"types": [ "node", "jest" ],
|
||||
"types": ["node", "jest"],
|
||||
"outDir": "dist",
|
||||
"skipLibCheck": true
|
||||
},
|
||||
"include": [
|
||||
"**/*.js",
|
||||
"**/*.ts",
|
||||
"package.json"
|
||||
],
|
||||
"include": ["**/*.js", "**/*.ts"],
|
||||
"exclude": [
|
||||
"node_modules",
|
||||
"dist",
|
||||
|
@ -26,4 +22,4 @@
|
|||
"**/*.spec.js",
|
||||
"__mocks__"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "@budibase/bbui",
|
||||
"description": "A UI solution used in the different Budibase projects.",
|
||||
"version": "2.5.5-alpha.0",
|
||||
"version": "2.5.6-alpha.32",
|
||||
"license": "MPL-2.0",
|
||||
"svelte": "src/index.js",
|
||||
"module": "dist/bbui.es.js",
|
||||
|
@ -38,8 +38,8 @@
|
|||
],
|
||||
"dependencies": {
|
||||
"@adobe/spectrum-css-workflow-icons": "1.2.1",
|
||||
"@budibase/shared-core": "2.5.5-alpha.0",
|
||||
"@budibase/string-templates": "2.5.5-alpha.0",
|
||||
"@budibase/shared-core": "2.5.6-alpha.32",
|
||||
"@budibase/string-templates": "2.5.6-alpha.32",
|
||||
"@spectrum-css/accordion": "3.0.24",
|
||||
"@spectrum-css/actionbutton": "1.0.1",
|
||||
"@spectrum-css/actiongroup": "1.0.1",
|
||||
|
@ -84,7 +84,7 @@
|
|||
"@spectrum-css/vars": "3.0.1",
|
||||
"dayjs": "^1.10.4",
|
||||
"easymde": "^2.16.1",
|
||||
"svelte-flatpickr": "^3.2.3",
|
||||
"svelte-flatpickr": "^3.3.2",
|
||||
"svelte-portal": "^1.0.0"
|
||||
},
|
||||
"resolutions": {
|
||||
|
|
|
@ -71,7 +71,7 @@
|
|||
{/if}
|
||||
{#if icon}
|
||||
<svg
|
||||
class="spectrum-Icon spectrum-Icon--size{size}"
|
||||
class="spectrum-Icon spectrum-Icon--sizeS"
|
||||
focusable="false"
|
||||
aria-hidden="true"
|
||||
aria-label={icon}
|
||||
|
|
|
@ -6,6 +6,9 @@ let clickHandlers = []
|
|||
*/
|
||||
const handleClick = event => {
|
||||
// Ignore click if this is an ignored class
|
||||
if (event.target.closest('[data-ignore-click-outside="true"]')) {
|
||||
return
|
||||
}
|
||||
for (let className of ignoredClasses) {
|
||||
if (event.target.closest(className)) {
|
||||
return
|
||||
|
@ -29,6 +32,7 @@ const handleClick = event => {
|
|||
})
|
||||
}
|
||||
document.documentElement.addEventListener("click", handleClick, true)
|
||||
document.documentElement.addEventListener("contextmenu", handleClick, true)
|
||||
|
||||
/**
|
||||
* Adds or updates a click handler
|
||||
|
|
|
@ -10,7 +10,14 @@ export default function positionDropdown(element, opts) {
|
|||
|
||||
// Updates the position of the dropdown
|
||||
const updatePosition = opts => {
|
||||
const { anchor, align, maxWidth, useAnchorWidth, offset = 5 } = opts
|
||||
const {
|
||||
anchor,
|
||||
align,
|
||||
maxHeight,
|
||||
maxWidth,
|
||||
useAnchorWidth,
|
||||
offset = 5,
|
||||
} = opts
|
||||
if (!anchor) {
|
||||
return
|
||||
}
|
||||
|
@ -31,10 +38,11 @@ export default function positionDropdown(element, opts) {
|
|||
styles.top = anchorBounds.top
|
||||
} else if (window.innerHeight - anchorBounds.bottom < 100) {
|
||||
styles.top = anchorBounds.top - elementBounds.height - offset
|
||||
styles.maxHeight = 240
|
||||
styles.maxHeight = maxHeight || 240
|
||||
} else {
|
||||
styles.top = anchorBounds.bottom + offset
|
||||
styles.maxHeight = window.innerHeight - anchorBounds.bottom - 20
|
||||
styles.maxHeight =
|
||||
maxHeight || window.innerHeight - anchorBounds.bottom - 20
|
||||
}
|
||||
|
||||
// Determine horizontal styles
|
||||
|
|
|
@ -138,7 +138,7 @@
|
|||
}
|
||||
</script>
|
||||
|
||||
<div class="container">
|
||||
<div class="container" class:compact>
|
||||
{#if selectedImage}
|
||||
{#if gallery}
|
||||
<div class="gallery">
|
||||
|
@ -355,6 +355,9 @@
|
|||
input[type="file"] {
|
||||
display: none;
|
||||
}
|
||||
.compact .spectrum-Dropzone {
|
||||
padding: 6px 0 !important;
|
||||
}
|
||||
|
||||
.gallery {
|
||||
display: flex;
|
||||
|
@ -379,6 +382,17 @@
|
|||
object-fit: contain;
|
||||
margin: 20px 30px;
|
||||
}
|
||||
.compact .placeholder,
|
||||
.compact img {
|
||||
margin: 10px 16px;
|
||||
}
|
||||
.compact img {
|
||||
height: 90px;
|
||||
}
|
||||
.compact .gallery {
|
||||
padding: 6px 10px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.title {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
|
@ -447,6 +461,13 @@
|
|||
.disabled .spectrum-Heading--sizeL {
|
||||
color: var(--spectrum-alias-text-color-disabled);
|
||||
}
|
||||
.compact .spectrum-Dropzone {
|
||||
padding-top: 8px;
|
||||
padding-bottom: 8px;
|
||||
}
|
||||
.compact .spectrum-IllustratedMessage-description {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.tags {
|
||||
margin-top: 20px;
|
||||
|
|
|
@ -20,12 +20,13 @@
|
|||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
$: selectedLookupMap = getSelectedLookupMap(value)
|
||||
$: arrayValue = Array.isArray(value) ? value : [value].filter(x => !!x)
|
||||
$: selectedLookupMap = getSelectedLookupMap(arrayValue)
|
||||
$: optionLookupMap = getOptionLookupMap(options)
|
||||
|
||||
$: fieldText = getFieldText(value, optionLookupMap, placeholder)
|
||||
$: fieldText = getFieldText(arrayValue, optionLookupMap, placeholder)
|
||||
$: isOptionSelected = optionValue => selectedLookupMap[optionValue] === true
|
||||
$: toggleOption = makeToggleOption(selectedLookupMap, value)
|
||||
$: toggleOption = makeToggleOption(selectedLookupMap, arrayValue)
|
||||
|
||||
const getFieldText = (value, map, placeholder) => {
|
||||
if (Array.isArray(value) && value.length > 0) {
|
||||
|
@ -84,7 +85,7 @@
|
|||
{readonly}
|
||||
{fieldText}
|
||||
{options}
|
||||
isPlaceholder={!value?.length}
|
||||
isPlaceholder={!arrayValue.length}
|
||||
{autocomplete}
|
||||
bind:fetchTerm
|
||||
{useFetch}
|
||||
|
|
|
@ -16,6 +16,7 @@
|
|||
export let gallery = true
|
||||
export let fileTags = []
|
||||
export let maximum = undefined
|
||||
export let compact = false
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
const onChange = e => {
|
||||
|
@ -37,6 +38,7 @@
|
|||
{gallery}
|
||||
{fileTags}
|
||||
{maximum}
|
||||
{compact}
|
||||
on:change={onChange}
|
||||
/>
|
||||
</Field>
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
export let message = ""
|
||||
export let onConfirm = undefined
|
||||
export let buttonText = ""
|
||||
|
||||
export let cta = false
|
||||
$: icon = selectIcon(type)
|
||||
// if newlines used, convert them to different elements
|
||||
$: split = message.split("\n")
|
||||
|
@ -41,7 +41,9 @@
|
|||
{/each}
|
||||
{#if onConfirm}
|
||||
<div class="spectrum-InLineAlert-footer button">
|
||||
<Button secondary on:click={onConfirm}>{buttonText || "OK"}</Button>
|
||||
<Button {cta} secondary={cta ? false : true} on:click={onConfirm}
|
||||
>{buttonText || "OK"}</Button
|
||||
>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
@ -57,7 +59,6 @@
|
|||
--spectrum-semantic-negative-icon-color: #e34850;
|
||||
min-width: 100px;
|
||||
margin: 0;
|
||||
border-color: var(--spectrum-global-color-gray-400);
|
||||
border-width: 1px;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -9,6 +9,7 @@
|
|||
|
||||
<TooltipWrapper {tooltip} {size}>
|
||||
<label
|
||||
data-testid="label"
|
||||
class:muted
|
||||
for=""
|
||||
class={`spectrum-FieldLabel spectrum-FieldLabel--size${size}`}
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
|
||||
export let wide = false
|
||||
export let narrow = false
|
||||
export let narrower = false
|
||||
export let noPadding = false
|
||||
|
||||
let sidePanelVisble = false
|
||||
|
@ -16,7 +17,7 @@
|
|||
|
||||
<div class="page">
|
||||
<div class="main">
|
||||
<div class="content" class:wide class:noPadding class:narrow>
|
||||
<div class="content" class:wide class:noPadding class:narrow class:narrower>
|
||||
<slot />
|
||||
<div class="fix-scroll-padding" />
|
||||
</div>
|
||||
|
@ -70,6 +71,9 @@
|
|||
.content.narrow {
|
||||
max-width: 840px;
|
||||
}
|
||||
.content.narrower {
|
||||
max-width: 700px;
|
||||
}
|
||||
#side-panel {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
|
|
|
@ -14,11 +14,13 @@
|
|||
export let align = "right"
|
||||
export let portalTarget
|
||||
export let maxWidth
|
||||
export let maxHeight
|
||||
export let open = false
|
||||
export let useAnchorWidth = false
|
||||
export let dismissible = true
|
||||
export let offset = 5
|
||||
export let customHeight
|
||||
export let animate = true
|
||||
|
||||
$: target = portalTarget || getContext(Context.PopoverRoot) || ".spectrum"
|
||||
|
||||
|
@ -64,6 +66,7 @@
|
|||
use:positionDropdown={{
|
||||
anchor,
|
||||
align,
|
||||
maxHeight,
|
||||
maxWidth,
|
||||
useAnchorWidth,
|
||||
offset,
|
||||
|
@ -76,7 +79,7 @@
|
|||
class="spectrum-Popover is-open"
|
||||
role="presentation"
|
||||
style="height: {customHeight}"
|
||||
transition:fly|local={{ y: -20, duration: 200 }}
|
||||
transition:fly|local={{ y: -20, duration: animate ? 200 : 0 }}
|
||||
>
|
||||
<slot />
|
||||
</div>
|
||||
|
|
|
@ -3,6 +3,7 @@ import { writable } from "svelte/store"
|
|||
export const BANNER_TYPES = {
|
||||
INFO: "info",
|
||||
NEGATIVE: "negative",
|
||||
WARNING: "warning",
|
||||
}
|
||||
|
||||
export function createBannerStore() {
|
||||
|
@ -38,7 +39,8 @@ export function createBannerStore() {
|
|||
const queue = async entries => {
|
||||
const priority = {
|
||||
[BANNER_TYPES.NEGATIVE]: 0,
|
||||
[BANNER_TYPES.INFO]: 1,
|
||||
[BANNER_TYPES.WARNING]: 1,
|
||||
[BANNER_TYPES.INFO]: 2,
|
||||
}
|
||||
banner.update(store => {
|
||||
const sorted = [...store.messages, ...entries].sort((a, b) => {
|
||||
|
|
|
@ -5,8 +5,9 @@
|
|||
|
||||
const displayLimit = 5
|
||||
|
||||
$: badges = Array.isArray(value) ? value.slice(0, displayLimit) : []
|
||||
$: leftover = (value?.length ?? 0) - badges.length
|
||||
$: arrayValue = Array.isArray(value) ? value : [value].filter(x => !!x)
|
||||
$: badges = arrayValue.slice(0, displayLimit)
|
||||
$: leftover = arrayValue.length - badges.length
|
||||
</script>
|
||||
|
||||
{#each badges as badge}
|
||||
|
|
|
@ -143,7 +143,7 @@
|
|||
}
|
||||
fields?.forEach(field => {
|
||||
const fieldSchema = schema[field]
|
||||
if (fieldSchema.width) {
|
||||
if (fieldSchema.width && typeof fieldSchema.width === "string") {
|
||||
style += ` ${fieldSchema.width}`
|
||||
} else {
|
||||
style += " minmax(auto, 1fr)"
|
||||
|
|
|
@ -9,6 +9,7 @@
|
|||
</script>
|
||||
|
||||
<p
|
||||
data-testid="typography-body"
|
||||
style={`
|
||||
${weight ? `font-weight:${weight};` : ""}
|
||||
${textAlign ? `text-align:${textAlign};` : ""}
|
||||
|
|
|
@ -3,9 +3,13 @@
|
|||
|
||||
export let size = "M"
|
||||
export let serif = false
|
||||
export let weight = 600
|
||||
</script>
|
||||
|
||||
<p
|
||||
style={`
|
||||
${weight ? `font-weight:${weight};` : ""}
|
||||
`}
|
||||
class="spectrum-Detail spectrum-Detail--size{size}"
|
||||
class:spectrum-Detail--serif={serif}
|
||||
>
|
||||
|
@ -13,7 +17,4 @@
|
|||
</p>
|
||||
|
||||
<style>
|
||||
p {
|
||||
font-weight: 600;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -9,6 +9,7 @@
|
|||
</script>
|
||||
|
||||
<h1
|
||||
data-testid="typography-heading"
|
||||
style={textAlign ? `text-align:${textAlign}` : ``}
|
||||
class:noPadding
|
||||
class="spectrum-Heading spectrum-Heading--size{size} spectrum-Heading--{weight}"
|
||||
|
|
|
@ -97,4 +97,22 @@
|
|||
|
||||
a {
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
|
||||
/* Custom theme additions */
|
||||
.spectrum--darkest {
|
||||
--drop-shadow: rgba(0, 0, 0, 0.6);
|
||||
--spectrum-global-color-blue-100: rgb(28, 33, 43);
|
||||
}
|
||||
.spectrum--dark {
|
||||
--drop-shadow: rgba(0, 0, 0, 0.3);
|
||||
--spectrum-global-color-blue-100: rgb(42, 47, 57);
|
||||
}
|
||||
.spectrum--light {
|
||||
--drop-shadow: rgba(0, 0, 0, 0.075);
|
||||
--spectrum-global-color-blue-100: rgb(240, 245, 255);
|
||||
}
|
||||
.spectrum--lightest {
|
||||
--drop-shadow: rgba(0, 0, 0, 0.05);
|
||||
--spectrum-global-color-blue-100: rgb(240, 244, 255);
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@budibase/builder",
|
||||
"version": "2.5.5-alpha.0",
|
||||
"version": "2.5.6-alpha.32",
|
||||
"license": "GPL-3.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
|
@ -9,7 +9,7 @@
|
|||
"dev:builder": "routify -c dev:vite",
|
||||
"dev:vite": "vite --host 0.0.0.0",
|
||||
"rollup": "rollup -c -w",
|
||||
"test": "jest"
|
||||
"test": "vitest"
|
||||
},
|
||||
"jest": {
|
||||
"globals": {
|
||||
|
@ -58,11 +58,10 @@
|
|||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"@budibase/bbui": "2.5.5-alpha.0",
|
||||
"@budibase/client": "2.5.5-alpha.0",
|
||||
"@budibase/frontend-core": "2.5.5-alpha.0",
|
||||
"@budibase/shared-core": "2.5.5-alpha.0",
|
||||
"@budibase/string-templates": "2.5.5-alpha.0",
|
||||
"@budibase/bbui": "2.5.6-alpha.32",
|
||||
"@budibase/frontend-core": "2.5.6-alpha.32",
|
||||
"@budibase/shared-core": "2.5.6-alpha.32",
|
||||
"@budibase/string-templates": "2.5.6-alpha.32",
|
||||
"@fortawesome/fontawesome-svg-core": "^6.2.1",
|
||||
"@fortawesome/free-brands-svg-icons": "^6.2.1",
|
||||
"@fortawesome/free-solid-svg-icons": "^6.2.1",
|
||||
|
@ -93,13 +92,14 @@
|
|||
"@roxi/routify": "2.18.5",
|
||||
"@sveltejs/vite-plugin-svelte": "1.0.1",
|
||||
"@testing-library/jest-dom": "^5.11.10",
|
||||
"@testing-library/svelte": "^3.0.0",
|
||||
"@testing-library/svelte": "^3.2.2",
|
||||
"babel-jest": "^26.6.3",
|
||||
"cypress": "^9.3.1",
|
||||
"cypress-multi-reporters": "^1.6.0",
|
||||
"cypress-terminal-report": "^1.4.1",
|
||||
"identity-obj-proxy": "^3.0.0",
|
||||
"jest": "^26.6.3",
|
||||
"jsdom": "^21.1.1",
|
||||
"mochawesome": "^7.1.3",
|
||||
"mochawesome-merge": "^4.2.1",
|
||||
"mochawesome-report-generator": "^6.2.0",
|
||||
|
@ -113,7 +113,8 @@
|
|||
"ts-node": "10.8.1",
|
||||
"tsconfig-paths": "4.0.0",
|
||||
"typescript": "4.7.3",
|
||||
"vite": "^3.0.8"
|
||||
"vite": "^3.0.8",
|
||||
"vitest": "^0.29.2"
|
||||
},
|
||||
"gitHead": "115189f72a850bfb52b65ec61d932531bf327072"
|
||||
}
|
||||
|
|
|
@ -10,7 +10,10 @@ import { auth } from "./stores/portal"
|
|||
export const API = createAPIClient({
|
||||
attachHeaders: headers => {
|
||||
// Attach app ID header from store
|
||||
headers["x-budibase-app-id"] = get(store).appId
|
||||
let appId = get(store).appId
|
||||
if (appId) {
|
||||
headers["x-budibase-app-id"] = appId
|
||||
}
|
||||
|
||||
// Add csrf token if authenticated
|
||||
const user = get(auth).user
|
||||
|
|
|
@ -134,6 +134,7 @@ export const getFrontendStore = () => {
|
|||
previousTopNavPath: {},
|
||||
version: application.version,
|
||||
revertableVersion: application.revertableVersion,
|
||||
upgradableVersion: application.upgradableVersion,
|
||||
navigation: application.navigation || {},
|
||||
usedPlugins: application.usedPlugins || [],
|
||||
}))
|
||||
|
|
|
@ -1,13 +1,13 @@
|
|||
import * as jsonpatch from "fast-json-patch/index.mjs"
|
||||
import { writable, derived, get } from "svelte/store"
|
||||
|
||||
const Operations = {
|
||||
export const Operations = {
|
||||
Add: "Add",
|
||||
Delete: "Delete",
|
||||
Change: "Change",
|
||||
}
|
||||
|
||||
const initialState = {
|
||||
export const initialState = {
|
||||
history: [],
|
||||
position: 0,
|
||||
loading: false,
|
||||
|
|
|
@ -0,0 +1,345 @@
|
|||
import { it, expect, describe, beforeEach, vi } from "vitest"
|
||||
import { Operations, initialState, createHistoryStore } from "./history"
|
||||
|
||||
import { writable, derived, get } from "svelte/store"
|
||||
import * as jsonpatch from "fast-json-patch/index.mjs"
|
||||
|
||||
vi.mock("svelte/store", () => {
|
||||
return {
|
||||
writable: vi.fn(),
|
||||
derived: vi.fn(),
|
||||
get: vi.fn(),
|
||||
}
|
||||
})
|
||||
|
||||
vi.mock("fast-json-patch/index.mjs", () => {
|
||||
return {
|
||||
compare: vi.fn(),
|
||||
deepClone: vi.fn(),
|
||||
applyPatch: vi.fn(),
|
||||
}
|
||||
})
|
||||
|
||||
describe("admin store", () => {
|
||||
beforeEach(ctx => {
|
||||
vi.clearAllMocks()
|
||||
|
||||
ctx.writableReturn = { update: vi.fn(), subscribe: vi.fn(), set: vi.fn() }
|
||||
writable.mockReturnValue(ctx.writableReturn)
|
||||
|
||||
ctx.derivedReturn = { subscribe: vi.fn() }
|
||||
derived.mockReturnValue(ctx.derivedReturn)
|
||||
|
||||
ctx.getDoc = vi.fn().mockReturnValue({})
|
||||
ctx.selectDoc = vi.fn().mockReturnValue({})
|
||||
ctx.beforeAction = vi.fn()
|
||||
ctx.afterAction = vi.fn()
|
||||
|
||||
ctx.returnedStore = createHistoryStore({
|
||||
getDoc: ctx.getDoc,
|
||||
selectDoc: ctx.selectDoc,
|
||||
beforeAction: ctx.beforeAction,
|
||||
afterAction: ctx.afterAction,
|
||||
})
|
||||
})
|
||||
|
||||
describe("init", () => {
|
||||
it("inits the writable store with the default config", () => {
|
||||
expect(writable).toHaveBeenCalledTimes(1)
|
||||
expect(writable).toHaveBeenCalledWith(initialState)
|
||||
})
|
||||
|
||||
it("inits the derived store with the initial writable store and an update function", () => {
|
||||
expect(derived).toHaveBeenCalledTimes(1)
|
||||
expect(derived.calls[0][1]({ position: 0, history: [] })).toEqual({
|
||||
position: 0,
|
||||
history: [],
|
||||
canUndo: false,
|
||||
canRedo: false,
|
||||
})
|
||||
})
|
||||
|
||||
it("returns the created store and methods to manipulate it", ctx => {
|
||||
expect(ctx.returnedStore).toEqual({
|
||||
subscribe: expect.toBe(ctx.derivedReturn.subscribe),
|
||||
wrapSaveDoc: expect.toBeFunc(),
|
||||
wrapDeleteDoc: expect.toBeFunc(),
|
||||
reset: expect.toBeFunc(),
|
||||
undo: expect.toBeFunc(),
|
||||
redo: expect.toBeFunc(),
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe("wrapSaveDoc", () => {
|
||||
beforeEach(async ctx => {
|
||||
ctx.saveFn = vi.fn().mockResolvedValue("fn")
|
||||
|
||||
ctx.doc = { _id: "id" }
|
||||
ctx.oldDoc = { _id: "oldDoc" }
|
||||
ctx.newDoc = { _id: "newDoc" }
|
||||
ctx.getDoc.mockReturnValue(ctx.oldDoc)
|
||||
jsonpatch.deepClone.mockReturnValue(ctx.newDoc)
|
||||
|
||||
vi.stubGlobal("Math", { random: vi.fn() })
|
||||
|
||||
ctx.forwardPatch = { foo: 1 }
|
||||
ctx.backwardsPatch = { bar: 2 }
|
||||
|
||||
jsonpatch.compare.mockReturnValueOnce(ctx.forwardPatch)
|
||||
jsonpatch.compare.mockReturnValueOnce(ctx.backwardsPatch)
|
||||
Math.random.mockReturnValue(1234)
|
||||
|
||||
const wrappedSaveFn = ctx.returnedStore.wrapSaveDoc(ctx.saveFn)
|
||||
await wrappedSaveFn(ctx.doc, null)
|
||||
})
|
||||
|
||||
it("sets the state to loading", ctx => {
|
||||
expect(ctx.writableReturn.update.calls[0][0]({})).toEqual({
|
||||
loading: true,
|
||||
})
|
||||
})
|
||||
|
||||
it("retrieves the old doc", ctx => {
|
||||
expect(ctx.getDoc).toHaveBeenCalledTimes(1)
|
||||
expect(ctx.getDoc).toHaveBeenCalledWith("id")
|
||||
})
|
||||
|
||||
it("clones the new doc", ctx => {
|
||||
expect(ctx.saveFn).toHaveBeenCalledTimes(1)
|
||||
expect(ctx.saveFn).toHaveBeenCalledWith(ctx.doc)
|
||||
expect(jsonpatch.deepClone).toHaveBeenCalledTimes(1)
|
||||
expect(jsonpatch.deepClone).toHaveBeenCalledWith("fn")
|
||||
})
|
||||
|
||||
it("creates the undo/redo patches", ctx => {
|
||||
expect(jsonpatch.compare).toHaveBeenCalledTimes(2)
|
||||
expect(jsonpatch.compare.calls[0]).toEqual([ctx.oldDoc, ctx.doc])
|
||||
expect(jsonpatch.compare.calls[1]).toEqual([ctx.doc, ctx.oldDoc])
|
||||
})
|
||||
|
||||
it("saves the operation", ctx => {
|
||||
expect(
|
||||
ctx.writableReturn.update.calls[1][0]({
|
||||
history: [],
|
||||
position: 0,
|
||||
})
|
||||
).toEqual({
|
||||
history: [
|
||||
{
|
||||
type: Operations.Change,
|
||||
backwardsPatch: ctx.backwardsPatch,
|
||||
forwardPatch: ctx.forwardPatch,
|
||||
doc: ctx.newDoc,
|
||||
id: 1234,
|
||||
},
|
||||
],
|
||||
position: 1,
|
||||
})
|
||||
})
|
||||
|
||||
it("sets the state to not loading", ctx => {
|
||||
expect(ctx.writableReturn.update).toHaveBeenCalledTimes(3)
|
||||
expect(ctx.writableReturn.update.calls[2][0]({})).toEqual({
|
||||
loading: false,
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe("wrapDeleteDoc", () => {
|
||||
beforeEach(async ctx => {
|
||||
ctx.deleteFn = vi.fn().mockResolvedValue("fn")
|
||||
|
||||
ctx.doc = { _id: "id" }
|
||||
ctx.oldDoc = { _id: "oldDoc" }
|
||||
jsonpatch.deepClone.mockReturnValue(ctx.oldDoc)
|
||||
|
||||
vi.stubGlobal("Math", { random: vi.fn() })
|
||||
Math.random.mockReturnValue(1235)
|
||||
|
||||
const wrappedDeleteDoc = ctx.returnedStore.wrapDeleteDoc(ctx.deleteFn)
|
||||
await wrappedDeleteDoc(ctx.doc, null)
|
||||
})
|
||||
|
||||
it("sets the state to loading", ctx => {
|
||||
expect(ctx.writableReturn.update.calls[0][0]({})).toEqual({
|
||||
loading: true,
|
||||
})
|
||||
})
|
||||
|
||||
it("clones the doc", ctx => {
|
||||
expect(jsonpatch.deepClone).toHaveBeenCalledTimes(1)
|
||||
expect(jsonpatch.deepClone).toHaveBeenCalledWith(ctx.doc)
|
||||
})
|
||||
|
||||
it("calls the delete fn with the doc", ctx => {
|
||||
expect(ctx.deleteFn).toHaveBeenCalledTimes(1)
|
||||
expect(ctx.deleteFn).toHaveBeenCalledWith(ctx.doc)
|
||||
})
|
||||
|
||||
it("saves the operation", ctx => {
|
||||
expect(
|
||||
ctx.writableReturn.update.calls[1][0]({
|
||||
history: [],
|
||||
position: 0,
|
||||
})
|
||||
).toEqual({
|
||||
history: [
|
||||
{
|
||||
type: Operations.Delete,
|
||||
doc: ctx.oldDoc,
|
||||
id: 1235,
|
||||
},
|
||||
],
|
||||
position: 1,
|
||||
})
|
||||
})
|
||||
|
||||
it("sets the state to not loading", ctx => {
|
||||
expect(ctx.writableReturn.update).toHaveBeenCalledTimes(3)
|
||||
expect(ctx.writableReturn.update.calls[2][0]({})).toEqual({
|
||||
loading: false,
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe("reset", () => {
|
||||
beforeEach(ctx => {
|
||||
ctx.returnedStore.reset()
|
||||
})
|
||||
|
||||
it("sets the store to the initial state", ctx => {
|
||||
expect(ctx.writableReturn.set).toHaveBeenCalledTimes(1)
|
||||
expect(ctx.writableReturn.set).toHaveBeenCalledWith(initialState)
|
||||
})
|
||||
})
|
||||
|
||||
describe("undo", () => {
|
||||
beforeEach(async ctx => {
|
||||
ctx.history = [
|
||||
{ type: Operations.Delete, doc: { _id: 1236, _rev: "rev" } },
|
||||
]
|
||||
ctx.derivedReturn = {
|
||||
subscribe: vi.fn(),
|
||||
canUndo: true,
|
||||
history: ctx.history,
|
||||
position: 1,
|
||||
loading: false,
|
||||
}
|
||||
get.mockReturnValue(ctx.derivedReturn)
|
||||
|
||||
jsonpatch.deepClone.mockReturnValueOnce(ctx.history[0].doc)
|
||||
|
||||
ctx.newDoc = { _id: 1337 }
|
||||
ctx.saveFn = vi.fn().mockResolvedValue(ctx.newDoc)
|
||||
jsonpatch.deepClone.mockReturnValueOnce(ctx.newDoc)
|
||||
|
||||
// We need to create a wrapped saveFn before undo can be invoked
|
||||
ctx.returnedStore.wrapSaveDoc(ctx.saveFn)
|
||||
await ctx.returnedStore.undo()
|
||||
})
|
||||
|
||||
it("sets the state to loading", ctx => {
|
||||
expect(ctx.writableReturn.update.calls[0][0]({})).toEqual({
|
||||
loading: true,
|
||||
})
|
||||
})
|
||||
|
||||
it("calls the beforeAction", ctx => {
|
||||
expect(ctx.beforeAction).toHaveBeenCalledTimes(1)
|
||||
expect(ctx.beforeAction).toHaveBeenCalledWith(ctx.history[0])
|
||||
})
|
||||
|
||||
it("sets the state to the previous position", ctx => {
|
||||
expect(
|
||||
ctx.writableReturn.update.calls[1][0]({ history: [], position: 1 })
|
||||
).toEqual({ history: [], position: 0 })
|
||||
})
|
||||
|
||||
it("clones the doc", ctx => {
|
||||
expect(jsonpatch.deepClone).toHaveBeenCalledWith(ctx.history[0].doc)
|
||||
})
|
||||
|
||||
it("deletes the doc's rev", ctx => {
|
||||
expect(ctx.history[0].doc._rev).toBe(undefined)
|
||||
})
|
||||
|
||||
it("calls the wrappedSaveFn", ctx => {
|
||||
expect(jsonpatch.deepClone).toHaveBeenCalledWith(ctx.newDoc)
|
||||
expect(ctx.saveFn).toHaveBeenCalledWith(ctx.history[0].doc)
|
||||
})
|
||||
|
||||
it("calls selectDoc", ctx => {
|
||||
expect(ctx.selectDoc).toHaveBeenCalledWith(ctx.newDoc._id)
|
||||
})
|
||||
|
||||
it("sets the state to not loading", ctx => {
|
||||
expect(ctx.writableReturn.update.calls[5][0]({})).toEqual({
|
||||
loading: false,
|
||||
})
|
||||
})
|
||||
|
||||
it("calls the afterAction", ctx => {
|
||||
expect(ctx.afterAction).toHaveBeenCalledTimes(1)
|
||||
expect(ctx.afterAction).toHaveBeenCalledWith(ctx.history[0])
|
||||
})
|
||||
})
|
||||
|
||||
describe("redo", () => {
|
||||
beforeEach(async ctx => {
|
||||
ctx.history = [
|
||||
{ type: Operations.Delete, doc: { _id: 1236, _rev: "rev" } },
|
||||
]
|
||||
ctx.derivedReturn = {
|
||||
subscribe: vi.fn(),
|
||||
canRedo: true,
|
||||
history: ctx.history,
|
||||
position: 0,
|
||||
loading: false,
|
||||
}
|
||||
|
||||
get.mockReturnValue(ctx.derivedReturn)
|
||||
|
||||
ctx.latestDoc = { _id: 1337 }
|
||||
ctx.getDoc.mockReturnValue(ctx.latestDoc)
|
||||
|
||||
// We need to create a wrapped deleteFn before redo can be invoked
|
||||
ctx.deleteFn = vi.fn().mockResolvedValue(ctx.newDoc)
|
||||
ctx.returnedStore.wrapDeleteDoc(ctx.deleteFn)
|
||||
|
||||
await ctx.returnedStore.redo()
|
||||
})
|
||||
|
||||
it("sets the state to loading", ctx => {
|
||||
expect(ctx.writableReturn.update.calls[0][0]({})).toEqual({
|
||||
loading: true,
|
||||
})
|
||||
})
|
||||
|
||||
it("calls the beforeAction", ctx => {
|
||||
expect(ctx.beforeAction).toHaveBeenCalledTimes(1)
|
||||
expect(ctx.beforeAction).toHaveBeenCalledWith(ctx.history[0])
|
||||
})
|
||||
|
||||
it("sets the state to the next position", ctx => {
|
||||
expect(
|
||||
ctx.writableReturn.update.calls[1][0]({ history: [], position: 0 })
|
||||
).toEqual({ history: [], position: 1 })
|
||||
})
|
||||
|
||||
it("calls the wrappedDeleteFn", ctx => {
|
||||
expect(ctx.deleteFn).toHaveBeenCalledWith(ctx.latestDoc)
|
||||
})
|
||||
|
||||
it("sets the state to not loading", ctx => {
|
||||
expect(ctx.writableReturn.update.calls[5][0]({})).toEqual({
|
||||
loading: false,
|
||||
})
|
||||
})
|
||||
|
||||
it("calls the afterAction", ctx => {
|
||||
expect(ctx.afterAction).toHaveBeenCalledTimes(1)
|
||||
expect(ctx.afterAction).toHaveBeenCalledWith(ctx.history[0])
|
||||
})
|
||||
})
|
||||
})
|
|
@ -26,7 +26,7 @@
|
|||
|
||||
const external = actions.reduce((acc, elm) => {
|
||||
const [k, v] = elm
|
||||
if (!v.internal) {
|
||||
if (!v.internal && !v.custom) {
|
||||
acc[k] = v
|
||||
}
|
||||
return acc
|
||||
|
@ -41,6 +41,15 @@
|
|||
return acc
|
||||
}, {})
|
||||
|
||||
const plugins = actions.reduce((acc, elm) => {
|
||||
const [k, v] = elm
|
||||
if (v.custom) {
|
||||
acc[k] = v
|
||||
}
|
||||
return acc
|
||||
}, {})
|
||||
console.log(plugins)
|
||||
|
||||
const selectAction = action => {
|
||||
actionVal = action
|
||||
selectedAction = action.name
|
||||
|
@ -116,6 +125,26 @@
|
|||
{/each}
|
||||
</div>
|
||||
</Layout>
|
||||
|
||||
{#if Object.keys(plugins).length}
|
||||
<Layout noPadding gap="XS">
|
||||
<Detail size="S">Plugins</Detail>
|
||||
<div class="item-list">
|
||||
{#each Object.entries(plugins) as [idx, action]}
|
||||
<div
|
||||
class="item"
|
||||
class:selected={selectedAction === action.name}
|
||||
on:click={() => selectAction(action)}
|
||||
>
|
||||
<div class="item-body">
|
||||
<Icon name={action.icon} />
|
||||
<Body size="XS">{action.name}</Body>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</Layout>
|
||||
{/if}
|
||||
</ModalContent>
|
||||
|
||||
<style>
|
||||
|
@ -126,7 +155,7 @@
|
|||
}
|
||||
.item-list {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
|
||||
grid-template-columns: repeat(2, minmax(150px, 1fr));
|
||||
grid-gap: var(--spectrum-alias-grid-baseline);
|
||||
}
|
||||
|
||||
|
|
|
@ -67,6 +67,15 @@
|
|||
newInputData = cloneDeep(blockInputs)
|
||||
}
|
||||
inputData = newInputData
|
||||
setDefaultEnumValues()
|
||||
}
|
||||
|
||||
const setDefaultEnumValues = () => {
|
||||
for (const [key, value] of schemaProperties) {
|
||||
if (value.type === "string" && value.enum && inputData[key] == null) {
|
||||
inputData[key] = value.enum[0]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const onChange = Utils.sequential(async (e, key) => {
|
||||
|
@ -243,6 +252,7 @@
|
|||
<Select
|
||||
on:change={e => onChange(e, key)}
|
||||
value={inputData[key]}
|
||||
placeholder={false}
|
||||
options={value.enum}
|
||||
getOptionLabel={(x, idx) => (value.pretty ? value.pretty[idx] : x)}
|
||||
/>
|
||||
|
|
|
@ -1,286 +1,75 @@
|
|||
<script>
|
||||
import { fade } from "svelte/transition"
|
||||
import { tables } from "stores/backend"
|
||||
import CreateRowButton from "./buttons/CreateRowButton.svelte"
|
||||
import CreateColumnButton from "./buttons/CreateColumnButton.svelte"
|
||||
import CreateViewButton from "./buttons/CreateViewButton.svelte"
|
||||
import ExistingRelationshipButton from "./buttons/ExistingRelationshipButton.svelte"
|
||||
import ExportButton from "./buttons/ExportButton.svelte"
|
||||
import ImportButton from "./buttons/ImportButton.svelte"
|
||||
import EditRolesButton from "./buttons/EditRolesButton.svelte"
|
||||
import ManageAccessButton from "./buttons/ManageAccessButton.svelte"
|
||||
import HideAutocolumnButton from "./buttons/HideAutocolumnButton.svelte"
|
||||
import TableFilterButton from "./buttons/TableFilterButton.svelte"
|
||||
import Table from "./Table.svelte"
|
||||
import { TableNames } from "constants"
|
||||
import CreateEditRow from "./modals/CreateEditRow.svelte"
|
||||
import {
|
||||
Pagination,
|
||||
Heading,
|
||||
Body,
|
||||
Layout,
|
||||
notifications,
|
||||
} from "@budibase/bbui"
|
||||
import { fetchData } from "@budibase/frontend-core"
|
||||
import { Grid } from "@budibase/frontend-core"
|
||||
import { API } from "api"
|
||||
import GridAddColumnModal from "components/backend/DataTable/modals/grid/GridCreateColumnModal.svelte"
|
||||
import GridCreateEditRowModal from "components/backend/DataTable/modals/grid/GridCreateEditRowModal.svelte"
|
||||
import GridEditUserModal from "components/backend/DataTable/modals/grid/GridEditUserModal.svelte"
|
||||
import GridCreateViewButton from "components/backend/DataTable/buttons/grid/GridCreateViewButton.svelte"
|
||||
import GridImportButton from "components/backend/DataTable/buttons/grid/GridImportButton.svelte"
|
||||
import GridExportButton from "components/backend/DataTable/buttons/grid/GridExportButton.svelte"
|
||||
import GridFilterButton from "components/backend/DataTable/buttons/grid/GridFilterButton.svelte"
|
||||
import GridManageAccessButton from "components/backend/DataTable/buttons/grid/GridManageAccessButton.svelte"
|
||||
import GridRelationshipButton from "components/backend/DataTable/buttons/grid/GridRelationshipButton.svelte"
|
||||
import GridEditColumnModal from "components/backend/DataTable/modals/grid/GridEditColumnModal.svelte"
|
||||
|
||||
let hideAutocolumns = true
|
||||
let filters
|
||||
const userSchemaOverrides = {
|
||||
firstName: { name: "First name", disabled: true },
|
||||
lastName: { name: "Last name", disabled: true },
|
||||
email: { name: "Email", disabled: true },
|
||||
roleId: { name: "Role", disabled: true },
|
||||
status: { name: "Status", disabled: true },
|
||||
}
|
||||
|
||||
$: isUsersTable = $tables.selected?._id === TableNames.USERS
|
||||
$: type = $tables.selected?.type
|
||||
$: isInternal = type !== "external"
|
||||
$: schema = $tables.selected?.schema
|
||||
$: enrichedSchema = enrichSchema($tables.selected?.schema)
|
||||
$: id = $tables.selected?._id
|
||||
$: fetch = createFetch(id)
|
||||
$: hasCols = checkHasCols(schema)
|
||||
$: hasRows = !!$fetch.rows?.length
|
||||
$: showError($fetch.error)
|
||||
$: id, (filters = null)
|
||||
|
||||
let appliedFilter
|
||||
let rawFilter
|
||||
let appliedSort
|
||||
let selectedRows = []
|
||||
|
||||
$: enrichedSchema,
|
||||
() => {
|
||||
appliedFilter = null
|
||||
rawFilter = null
|
||||
appliedSort = null
|
||||
selectedRows = []
|
||||
}
|
||||
|
||||
$: if (Number.isInteger($fetch.pageNumber)) {
|
||||
selectedRows = []
|
||||
}
|
||||
|
||||
const showError = error => {
|
||||
if (error) {
|
||||
notifications.error(error?.message || "Unable to fetch data.")
|
||||
}
|
||||
}
|
||||
|
||||
const enrichSchema = schema => {
|
||||
let tempSchema = { ...schema }
|
||||
tempSchema._id = {
|
||||
type: "internal",
|
||||
editable: false,
|
||||
displayName: "ID",
|
||||
autocolumn: true,
|
||||
}
|
||||
if (isInternal) {
|
||||
tempSchema._rev = {
|
||||
type: "internal",
|
||||
editable: false,
|
||||
displayName: "Revision",
|
||||
autocolumn: true,
|
||||
}
|
||||
}
|
||||
|
||||
return tempSchema
|
||||
}
|
||||
|
||||
const checkHasCols = schema => {
|
||||
if (!schema || Object.keys(schema).length === 0) {
|
||||
return false
|
||||
}
|
||||
let fields = Object.values(schema)
|
||||
for (let field of fields) {
|
||||
if (!field.autocolumn) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Fetches new data whenever the table changes
|
||||
const createFetch = tableId => {
|
||||
return fetchData({
|
||||
API,
|
||||
datasource: {
|
||||
tableId,
|
||||
type: "table",
|
||||
},
|
||||
options: {
|
||||
schema,
|
||||
limit: 10,
|
||||
paginate: true,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// Fetch data whenever sorting option changes
|
||||
const onSort = async e => {
|
||||
const sort = {
|
||||
sortColumn: e.detail.column,
|
||||
sortOrder: e.detail.order,
|
||||
}
|
||||
await fetch.update(sort)
|
||||
appliedSort = { ...sort }
|
||||
appliedSort.sortOrder = appliedSort.sortOrder.toLowerCase()
|
||||
selectedRows = []
|
||||
}
|
||||
|
||||
// Fetch data whenever filters change
|
||||
const onFilter = e => {
|
||||
filters = e.detail
|
||||
fetch.update({
|
||||
filter: filters,
|
||||
})
|
||||
appliedFilter = e.detail
|
||||
}
|
||||
|
||||
// Fetch data whenever schema changes
|
||||
const onUpdateColumns = () => {
|
||||
selectedRows = []
|
||||
fetch.refresh()
|
||||
tables.fetchTable(id)
|
||||
}
|
||||
|
||||
// Fetch data whenever rows are modified. Unfortunately we have to lose
|
||||
// our pagination place, as our bookmarks will have shifted.
|
||||
const onUpdateRows = () => {
|
||||
selectedRows = []
|
||||
fetch.refresh()
|
||||
}
|
||||
|
||||
// When importing new rows it is better to reinitialise request/paging data.
|
||||
// Not doing so causes inconsistency in paging behaviour and content.
|
||||
const onImportData = () => {
|
||||
fetch.getInitialData()
|
||||
}
|
||||
$: isUsersTable = id === TableNames.USERS
|
||||
$: isInternal = $tables.selected?.type !== "external"
|
||||
</script>
|
||||
|
||||
<div>
|
||||
<Table
|
||||
title={$tables.selected?.name}
|
||||
schema={enrichedSchema}
|
||||
{type}
|
||||
<div class="wrapper">
|
||||
<Grid
|
||||
{API}
|
||||
tableId={id}
|
||||
data={$fetch.rows}
|
||||
bind:hideAutocolumns
|
||||
loading={!$fetch.loaded}
|
||||
on:sort={onSort}
|
||||
allowEditing
|
||||
disableSorting
|
||||
on:updatecolumns={onUpdateColumns}
|
||||
on:updaterows={onUpdateRows}
|
||||
on:selectionUpdated={e => {
|
||||
selectedRows = e.detail
|
||||
}}
|
||||
customPlaceholder
|
||||
allowAddRows={!isUsersTable}
|
||||
allowDeleteRows={!isUsersTable}
|
||||
schemaOverrides={isUsersTable ? userSchemaOverrides : null}
|
||||
on:updatetable={e => tables.updateTable(e.detail)}
|
||||
>
|
||||
<div class="buttons">
|
||||
<div class="left-buttons">
|
||||
<CreateColumnButton
|
||||
highlighted={$fetch.loaded && (!hasCols || !hasRows)}
|
||||
on:updatecolumns={onUpdateColumns}
|
||||
/>
|
||||
{#if !isUsersTable}
|
||||
<CreateRowButton
|
||||
on:updaterows={onUpdateRows}
|
||||
title={"Create row"}
|
||||
modalContentComponent={CreateEditRow}
|
||||
disabled={!hasCols}
|
||||
highlighted={$fetch.loaded && hasCols && !hasRows}
|
||||
/>
|
||||
{/if}
|
||||
{#if isInternal}
|
||||
<CreateViewButton disabled={!hasCols || !hasRows} />
|
||||
{/if}
|
||||
</div>
|
||||
<div class="right-buttons">
|
||||
<ManageAccessButton resourceId={$tables.selected?._id} />
|
||||
{#if isUsersTable}
|
||||
<EditRolesButton />
|
||||
{/if}
|
||||
{#if !isInternal}
|
||||
<ExistingRelationshipButton
|
||||
table={$tables.selected}
|
||||
on:updatecolumns={onUpdateColumns}
|
||||
/>
|
||||
{/if}
|
||||
<HideAutocolumnButton bind:hideAutocolumns />
|
||||
<ImportButton
|
||||
disabled={$tables.selected?._id === "ta_users"}
|
||||
tableId={$tables.selected?._id}
|
||||
on:importrows={onImportData}
|
||||
/>
|
||||
<ExportButton
|
||||
disabled={!hasRows || !hasCols}
|
||||
view={$tables.selected?._id}
|
||||
filters={appliedFilter}
|
||||
sorting={appliedSort}
|
||||
{selectedRows}
|
||||
/>
|
||||
{#key id}
|
||||
<TableFilterButton
|
||||
{schema}
|
||||
{filters}
|
||||
on:change={onFilter}
|
||||
disabled={!hasCols}
|
||||
tableId={id}
|
||||
/>
|
||||
{/key}
|
||||
</div>
|
||||
</div>
|
||||
<div slot="placeholder">
|
||||
<Layout gap="S">
|
||||
{#if !hasCols}
|
||||
<Heading>Let's create some columns</Heading>
|
||||
<Body>
|
||||
Start building out your table structure<br />
|
||||
by adding some columns
|
||||
</Body>
|
||||
{:else}
|
||||
<Heading>Now let's add a row</Heading>
|
||||
<Body>
|
||||
Add some data to your table<br />
|
||||
by adding some rows
|
||||
</Body>
|
||||
{/if}
|
||||
</Layout>
|
||||
</div>
|
||||
</Table>
|
||||
{#key id}
|
||||
<div in:fade={{ delay: 200, duration: 100 }}>
|
||||
<div class="pagination">
|
||||
<Pagination
|
||||
page={$fetch.pageNumber + 1}
|
||||
hasPrevPage={$fetch.hasPrevPage}
|
||||
hasNextPage={$fetch.hasNextPage}
|
||||
goToPrevPage={$fetch.loading ? null : fetch.prevPage}
|
||||
goToNextPage={$fetch.loading ? null : fetch.nextPage}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{/key}
|
||||
<svelte:fragment slot="controls">
|
||||
{#if isInternal}
|
||||
<GridCreateViewButton />
|
||||
{/if}
|
||||
<GridManageAccessButton />
|
||||
{#if !isInternal}
|
||||
<GridRelationshipButton />
|
||||
{/if}
|
||||
{#if isUsersTable}
|
||||
<EditRolesButton />
|
||||
{:else}
|
||||
<GridImportButton />
|
||||
{/if}
|
||||
<GridExportButton />
|
||||
<GridFilterButton />
|
||||
<GridAddColumnModal />
|
||||
<GridEditColumnModal />
|
||||
{#if isUsersTable}
|
||||
<GridEditUserModal />
|
||||
{:else}
|
||||
<GridCreateEditRowModal />
|
||||
{/if}
|
||||
</svelte:fragment>
|
||||
</Grid>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.pagination {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
margin-top: var(--spacing-xl);
|
||||
}
|
||||
.buttons {
|
||||
.wrapper {
|
||||
flex: 1 1 auto;
|
||||
margin: -28px -40px -40px -40px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.left-buttons,
|
||||
.right-buttons {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
gap: var(--spacing-m);
|
||||
flex-direction: column;
|
||||
background: var(--background);
|
||||
overflow: hidden;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -1,15 +1,10 @@
|
|||
<script>
|
||||
import { fade } from "svelte/transition"
|
||||
import { goto, params } from "@roxi/routify"
|
||||
import { Table, Modal, Heading, notifications, Layout } from "@budibase/bbui"
|
||||
import { API } from "api"
|
||||
import { Table, Heading, Layout } from "@budibase/bbui"
|
||||
import Spinner from "components/common/Spinner.svelte"
|
||||
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
|
||||
import DeleteRowsButton from "./buttons/DeleteRowsButton.svelte"
|
||||
import CreateEditRow from "./modals/CreateEditRow.svelte"
|
||||
import CreateEditUser from "./modals/CreateEditUser.svelte"
|
||||
import CreateEditColumn from "./modals/CreateEditColumn.svelte"
|
||||
import { cloneDeep } from "lodash/fp"
|
||||
import {
|
||||
TableNames,
|
||||
UNEDITABLE_USER_FIELDS,
|
||||
|
@ -22,7 +17,6 @@
|
|||
export let data = []
|
||||
export let tableId
|
||||
export let title
|
||||
export let allowEditing = false
|
||||
export let loading = false
|
||||
export let hideAutocolumns
|
||||
export let rowCount
|
||||
|
@ -32,12 +26,7 @@
|
|||
const dispatch = createEventDispatcher()
|
||||
|
||||
let selectedRows = []
|
||||
let editableColumn
|
||||
let editableRow
|
||||
let editRowModal
|
||||
let editColumnModal
|
||||
let customRenderers = []
|
||||
let confirmDelete
|
||||
|
||||
$: selectedRows, dispatch("selectionUpdated", selectedRows)
|
||||
$: isUsersTable = tableId === TableNames.USERS
|
||||
|
@ -92,36 +81,6 @@
|
|||
`/builder/app/${$params.application}/data/table/${tableId}/relationship/${rowId}/${fieldName}`
|
||||
)
|
||||
}
|
||||
|
||||
const deleteRows = async targetRows => {
|
||||
try {
|
||||
await API.deleteRows({
|
||||
tableId,
|
||||
rows: targetRows,
|
||||
})
|
||||
|
||||
const deletedRowIds = targetRows.map(row => row._id)
|
||||
data = data.filter(row => deletedRowIds.indexOf(row._id))
|
||||
|
||||
notifications.success(`Successfully deleted ${targetRows.length} rows`)
|
||||
} catch (error) {
|
||||
notifications.error("Error deleting rows")
|
||||
}
|
||||
}
|
||||
|
||||
const editRow = row => {
|
||||
editableRow = row
|
||||
if (row) {
|
||||
editRowModal.show()
|
||||
}
|
||||
}
|
||||
|
||||
const editColumn = field => {
|
||||
editableColumn = cloneDeep(schema?.[field])
|
||||
if (editableColumn) {
|
||||
editColumnModal.show()
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<Layout noPadding gap="S">
|
||||
|
@ -138,16 +97,6 @@
|
|||
{/if}
|
||||
<div class="popovers">
|
||||
<slot />
|
||||
{#if !isUsersTable && selectedRows.length > 0}
|
||||
<DeleteRowsButton
|
||||
on:updaterows
|
||||
{selectedRows}
|
||||
deleteRows={async rows => {
|
||||
await deleteRows(rows)
|
||||
resetSelectedRows()
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
</Layout>
|
||||
{#key tableId}
|
||||
|
@ -160,13 +109,7 @@
|
|||
{rowCount}
|
||||
{disableSorting}
|
||||
{customPlaceholder}
|
||||
bind:selectedRows
|
||||
allowSelectRows={allowEditing && !isUsersTable}
|
||||
allowEditRows={allowEditing}
|
||||
allowEditColumns={allowEditing}
|
||||
showAutoColumns={!hideAutocolumns}
|
||||
on:editcolumn={e => editColumn(e.detail)}
|
||||
on:editrow={e => editRow(e.detail)}
|
||||
on:clickrelationship={e => selectRelationship(e.detail)}
|
||||
on:sort
|
||||
>
|
||||
|
@ -176,42 +119,6 @@
|
|||
{/key}
|
||||
</Layout>
|
||||
|
||||
<Modal bind:this={editRowModal}>
|
||||
<svelte:component
|
||||
this={editRowComponent}
|
||||
on:updaterows
|
||||
on:deleteRows={() => {
|
||||
confirmDelete.show()
|
||||
}}
|
||||
row={editableRow}
|
||||
/>
|
||||
</Modal>
|
||||
|
||||
<ConfirmDialog
|
||||
bind:this={confirmDelete}
|
||||
okText="Delete"
|
||||
onOk={async () => {
|
||||
if (editableRow) {
|
||||
await deleteRows([editableRow])
|
||||
}
|
||||
editableRow = undefined
|
||||
}}
|
||||
onCancel={async () => {
|
||||
editRow(editableRow)
|
||||
}}
|
||||
title="Confirm Deletion"
|
||||
>
|
||||
Are you sure you want to delete this row?
|
||||
</ConfirmDialog>
|
||||
|
||||
<Modal bind:this={editColumnModal}>
|
||||
<CreateEditColumn
|
||||
field={editableColumn}
|
||||
on:updatecolumns
|
||||
onClosed={editColumnModal.hide}
|
||||
/>
|
||||
</Modal>
|
||||
|
||||
<style>
|
||||
.table-title {
|
||||
height: 24px;
|
||||
|
|
|
@ -57,7 +57,6 @@
|
|||
{data}
|
||||
{loading}
|
||||
{type}
|
||||
allowEditing={false}
|
||||
rowCount={10}
|
||||
bind:hideAutocolumns
|
||||
>
|
||||
|
|
|
@ -9,7 +9,6 @@
|
|||
|
||||
<ActionButton
|
||||
icon="Calculator"
|
||||
size="S"
|
||||
quiet
|
||||
on:click={modal.show}
|
||||
active={view.field && view.calculation}
|
||||
|
|
|
@ -1,24 +0,0 @@
|
|||
<script>
|
||||
import { ActionButton, Modal } from "@budibase/bbui"
|
||||
import CreateEditColumn from "../modals/CreateEditColumn.svelte"
|
||||
|
||||
export let highlighted = false
|
||||
export let disabled = false
|
||||
|
||||
let modal
|
||||
</script>
|
||||
|
||||
<ActionButton
|
||||
{disabled}
|
||||
selected={highlighted}
|
||||
emphasized={highlighted}
|
||||
icon="TableColumnAddRight"
|
||||
quiet
|
||||
size="S"
|
||||
on:click={modal.show}
|
||||
>
|
||||
Create column
|
||||
</ActionButton>
|
||||
<Modal bind:this={modal}>
|
||||
<CreateEditColumn on:updatecolumns />
|
||||
</Modal>
|
|
@ -1,26 +0,0 @@
|
|||
<script>
|
||||
import { ActionButton, Modal } from "@budibase/bbui"
|
||||
import CreateEditRow from "../modals/CreateEditRow.svelte"
|
||||
|
||||
export let modalContentComponent = CreateEditRow
|
||||
export let title = "Create row"
|
||||
export let disabled = false
|
||||
export let highlighted = false
|
||||
|
||||
let modal
|
||||
</script>
|
||||
|
||||
<ActionButton
|
||||
{disabled}
|
||||
emphasized={highlighted}
|
||||
selected={highlighted}
|
||||
icon="TableRowAddBottom"
|
||||
size="S"
|
||||
quiet
|
||||
on:click={modal.show}
|
||||
>
|
||||
{title}
|
||||
</ActionButton>
|
||||
<Modal bind:this={modal}>
|
||||
<svelte:component this={modalContentComponent} on:updaterows />
|
||||
</Modal>
|
|
@ -1,21 +0,0 @@
|
|||
<script>
|
||||
import { Modal, ActionButton } from "@budibase/bbui"
|
||||
import CreateViewModal from "../modals/CreateViewModal.svelte"
|
||||
|
||||
export let disabled = false
|
||||
|
||||
let modal
|
||||
</script>
|
||||
|
||||
<ActionButton
|
||||
{disabled}
|
||||
icon="CollectionAdd"
|
||||
size="S"
|
||||
quiet
|
||||
on:click={modal.show}
|
||||
>
|
||||
Create view
|
||||
</ActionButton>
|
||||
<Modal bind:this={modal}>
|
||||
<CreateViewModal />
|
||||
</Modal>
|
|
@ -19,7 +19,7 @@
|
|||
$: text = `${item}${selectedRows?.length === 1 ? "" : "s"}`
|
||||
</script>
|
||||
|
||||
<Button icon="Delete" size="s" warning quiet on:click={modal.show}>
|
||||
<Button icon="Delete" warning quiet on:click={modal.show}>
|
||||
Delete
|
||||
{selectedRows.length}
|
||||
{text}
|
||||
|
|
|
@ -1,15 +1,13 @@
|
|||
<script>
|
||||
import { Button, Modal } from "@budibase/bbui"
|
||||
import { ActionButton, Modal } from "@budibase/bbui"
|
||||
import EditRolesModal from "../modals/EditRoles.svelte"
|
||||
|
||||
let modal
|
||||
</script>
|
||||
|
||||
<div>
|
||||
<Button icon="UsersLock" primary size="S" quiet on:click={modal.show}>
|
||||
Edit roles
|
||||
</Button>
|
||||
</div>
|
||||
<ActionButton icon="UsersLock" quiet on:click={modal.show}>
|
||||
Edit roles
|
||||
</ActionButton>
|
||||
<Modal bind:this={modal}>
|
||||
<EditRolesModal />
|
||||
</Modal>
|
||||
|
|
|
@ -7,15 +7,23 @@
|
|||
export let table
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
$: datasource = findDatasource(table?._id)
|
||||
$: plusTables = datasource?.plus
|
||||
? Object.values(datasource?.entities || {})
|
||||
: []
|
||||
$: datasource = $datasources.list.find(
|
||||
source => source._id === table?.sourceId
|
||||
)
|
||||
|
||||
let modal
|
||||
|
||||
const findDatasource = tableId => {
|
||||
return $datasources.list.find(datasource => {
|
||||
return (
|
||||
Object.values(datasource.entities || {}).find(entity => {
|
||||
return entity._id === tableId
|
||||
}) != null
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
async function saveRelationship() {
|
||||
try {
|
||||
// Create datasource
|
||||
|
@ -28,15 +36,9 @@
|
|||
}
|
||||
</script>
|
||||
|
||||
{#if table.sourceId}
|
||||
{#if datasource}
|
||||
<div>
|
||||
<ActionButton
|
||||
icon="DataCorrelated"
|
||||
primary
|
||||
size="S"
|
||||
quiet
|
||||
on:click={modal.show}
|
||||
>
|
||||
<ActionButton icon="DataCorrelated" primary quiet on:click={modal.show}>
|
||||
Define existing relationship
|
||||
</ActionButton>
|
||||
</div>
|
||||
|
|
|
@ -11,13 +11,7 @@
|
|||
let modal
|
||||
</script>
|
||||
|
||||
<ActionButton
|
||||
{disabled}
|
||||
icon="DataDownload"
|
||||
size="S"
|
||||
quiet
|
||||
on:click={modal.show}
|
||||
>
|
||||
<ActionButton {disabled} icon="DataDownload" quiet on:click={modal.show}>
|
||||
Export
|
||||
</ActionButton>
|
||||
<Modal bind:this={modal}>
|
||||
|
|
|
@ -10,7 +10,6 @@
|
|||
<Button
|
||||
icon="Group"
|
||||
primary
|
||||
size="S"
|
||||
quiet
|
||||
active={!!view.groupBy}
|
||||
on:click={modal.show}
|
||||
|
|
|
@ -11,7 +11,6 @@
|
|||
<ActionButton
|
||||
icon={hideAutocolumns ? "VisibilityOff" : "Visibility"}
|
||||
primary
|
||||
size="S"
|
||||
quiet
|
||||
on:click={hideOrUnhide}
|
||||
>
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
let modal
|
||||
</script>
|
||||
|
||||
<ActionButton icon="DataUpload" size="S" quiet on:click={modal.show} {disabled}>
|
||||
<ActionButton icon="DataUpload" quiet on:click={modal.show} {disabled}>
|
||||
Import
|
||||
</ActionButton>
|
||||
<Modal bind:this={modal}>
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
import ManageAccessModal from "../modals/ManageAccessModal.svelte"
|
||||
|
||||
export let resourceId
|
||||
export let disabled = false
|
||||
|
||||
let modal
|
||||
let resourcePermissions
|
||||
|
@ -14,8 +15,8 @@
|
|||
}
|
||||
</script>
|
||||
|
||||
<ActionButton icon="LockClosed" size="S" quiet on:click={openDropdown}>
|
||||
Manage access
|
||||
<ActionButton icon="LockClosed" quiet on:click={openDropdown} {disabled}>
|
||||
Access
|
||||
</ActionButton>
|
||||
<Modal bind:this={modal}>
|
||||
<ManageAccessModal
|
||||
|
|
|
@ -18,11 +18,10 @@
|
|||
|
||||
<ActionButton
|
||||
icon="Filter"
|
||||
size="S"
|
||||
quiet
|
||||
{disabled}
|
||||
on:click={modal.show}
|
||||
active={tempValue?.length > 0}
|
||||
selected={tempValue?.length > 0}
|
||||
>
|
||||
Filter
|
||||
</ActionButton>
|
||||
|
|
|
@ -9,7 +9,6 @@
|
|||
|
||||
<ActionButton
|
||||
icon="Filter"
|
||||
size="S"
|
||||
quiet
|
||||
on:click={modal.show}
|
||||
active={view.filters?.length}
|
||||
|
|
|
@ -0,0 +1,18 @@
|
|||
<script>
|
||||
import { getContext } from "svelte"
|
||||
import { Modal, ActionButton } from "@budibase/bbui"
|
||||
import CreateViewModal from "../../modals/CreateViewModal.svelte"
|
||||
|
||||
const { rows, columns } = getContext("grid")
|
||||
|
||||
let modal
|
||||
|
||||
$: disabled = !$columns.length || !$rows.length
|
||||
</script>
|
||||
|
||||
<ActionButton {disabled} icon="CollectionAdd" quiet on:click={modal.show}>
|
||||
Add view
|
||||
</ActionButton>
|
||||
<Modal bind:this={modal}>
|
||||
<CreateViewModal />
|
||||
</Modal>
|
|
@ -0,0 +1,21 @@
|
|||
<script>
|
||||
import ExportButton from "../ExportButton.svelte"
|
||||
import { getContext } from "svelte"
|
||||
|
||||
const { rows, columns, tableId, sort, selectedRows, filter } =
|
||||
getContext("grid")
|
||||
|
||||
$: disabled = !$rows.length || !$columns.length
|
||||
$: selectedRowArray = Object.keys($selectedRows).map(id => ({ _id: id }))
|
||||
</script>
|
||||
|
||||
<ExportButton
|
||||
{disabled}
|
||||
view={$tableId}
|
||||
filters={$filter}
|
||||
sorting={{
|
||||
sortColumn: $sort.column,
|
||||
sortOrder: $sort.order,
|
||||
}}
|
||||
selectedRows={selectedRowArray}
|
||||
/>
|
|
@ -0,0 +1,20 @@
|
|||
<script>
|
||||
import TableFilterButton from "../TableFilterButton.svelte"
|
||||
import { getContext } from "svelte"
|
||||
|
||||
const { columns, config, filter, table } = getContext("grid")
|
||||
|
||||
const onFilter = e => {
|
||||
filter.set(e.detail || [])
|
||||
}
|
||||
</script>
|
||||
|
||||
{#key $config.tableId}
|
||||
<TableFilterButton
|
||||
schema={$table?.schema}
|
||||
filters={$filter}
|
||||
on:change={onFilter}
|
||||
disabled={!$columns.length}
|
||||
tableId={$config.tableId}
|
||||
/>
|
||||
{/key}
|
|
@ -0,0 +1,14 @@
|
|||
<script>
|
||||
import ImportButton from "../ImportButton.svelte"
|
||||
import { getContext } from "svelte"
|
||||
|
||||
export let disabled = false
|
||||
|
||||
const { rows, tableId } = getContext("grid")
|
||||
</script>
|
||||
|
||||
<ImportButton
|
||||
{disabled}
|
||||
tableId={$tableId}
|
||||
on:importrows={rows.actions.refreshData}
|
||||
/>
|
|
@ -0,0 +1,8 @@
|
|||
<script>
|
||||
import ManageAccessButton from "../ManageAccessButton.svelte"
|
||||
import { getContext } from "svelte"
|
||||
|
||||
const { config } = getContext("grid")
|
||||
</script>
|
||||
|
||||
<ManageAccessButton resourceId={$config.tableId} />
|
|
@ -0,0 +1,13 @@
|
|||
<script>
|
||||
import ExistingRelationshipButton from "../ExistingRelationshipButton.svelte"
|
||||
import { getContext } from "svelte"
|
||||
|
||||
const { table, rows } = getContext("grid")
|
||||
</script>
|
||||
|
||||
{#if $table}
|
||||
<ExistingRelationshipButton
|
||||
table={$table}
|
||||
on:updatecolumns={() => rows.actions.refreshData()}
|
||||
/>
|
||||
{/if}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue