1
0
Fork 0
mirror of synced 2024-06-22 16:10:40 +12:00

Merge branch 'develop' into feature/enterprise

This commit is contained in:
Rory Powell 2022-09-20 16:01:28 +01:00
commit c3248421d5
93 changed files with 4989 additions and 798 deletions

24
.github/ISSUE_TEMPLATE/epic.md vendored Normal file
View file

@ -0,0 +1,24 @@
---
name: Epic
about: Plan a new project
title: ''
labels: epic
assignees: ''
---
## Description
Brief summary of what this Epic is, whether it's a larger project, goal, or user story. Describe the job to be done, which persona this Epic is mainly for, or if more multiple, break it down by user and job story.
## Spec
Link to confluence spec
## Teams and Stakeholders
Describe who needs to be kept up-to-date about this Epic, included in discussions, or updated along the way. Stakeholders can be both in Product/Engineering, as well as other teams like Customer Success who might want to keep customers updated on the Epic project.
## Workflow
- [ ] Spec Created and pasted above
- [ ] Product Review
- [ ] Designs created
- [ ] Individual Tasks created and assigned to Epic

View file

@ -59,3 +59,9 @@ jobs:
with:
install: false
command: yarn test:e2e:ci
- name: QA Core Integration Tests
run: |
cd qa-core
yarn
yarn api:test:ci

1
.gitignore vendored
View file

@ -63,6 +63,7 @@ typings/
# dotenv environment variables file
.env
!qa-core/.env
!hosting/.env
hosting/.generated-nginx.dev.conf
hosting/proxy/.generated-nginx.prod.conf

View file

@ -8,4 +8,4 @@ packages/server/client
packages/server/src/definitions/openapi.ts
packages/builder/.routify
packages/builder/cypress/support/queryLevelTransformerFunction.js
packages/builder/cypress/support/queryLevelTransformerFunctionWithData.js
packages/builder/cypress/support/queryLevelTransformerFunctionWithData.js

View file

@ -1,5 +1,5 @@
{
"version": "1.3.19-alpha.0",
"version": "1.4.2",
"npmClient": "yarn",
"packages": [
"packages/*"

View file

@ -13,6 +13,7 @@
"js-yaml": "^4.1.0",
"kill-port": "^1.6.1",
"lerna": "3.14.1",
"madge": "^5.0.1",
"prettier": "^2.3.1",
"prettier-plugin-svelte": "^2.3.0",
"rimraf": "^3.0.2",
@ -25,6 +26,7 @@
"bootstrap": "lerna bootstrap && lerna link && ./scripts/link-dependencies.sh",
"build": "lerna run build",
"build:dev": "lerna run prebuild && tsc --build --watch --preserveWatchOutput",
"deps:circular": "madge packages/server/dist/index.js packages/worker/src/index.ts packages/backend-core/dist/src/index.js packages/cli/src/index.js --circular",
"release": "lerna publish ${RELEASE_VERSION_TYPE:-patch} --yes --force-publish && yarn release:pro",
"release:develop": "lerna publish prerelease --yes --force-publish --dist-tag develop --exact && yarn release:pro:develop",
"release:pro": "bash scripts/pro/release.sh",
@ -45,8 +47,8 @@
"lint:eslint": "eslint packages",
"lint:prettier": "prettier --check \"packages/**/*.{js,ts,svelte}\"",
"lint": "yarn run lint:eslint && yarn run lint:prettier",
"lint:fix:eslint": "eslint --fix packages",
"lint:fix:prettier": "prettier --write \"packages/**/*.{js,ts,svelte}\" && prettier --write \"examples/**/*.{js,ts,svelte}\"",
"lint:fix:eslint": "eslint --fix packages qa-core",
"lint:fix:prettier": "prettier --write \"packages/**/*.{js,ts,svelte}\" && prettier --write \"examples/**/*.{js,ts,svelte}\" && prettier --write \"qa-core/**/*.{js,ts,svelte}\"",
"lint:fix": "yarn run lint:fix:prettier && yarn run lint:fix:eslint",
"test:e2e": "lerna run cy:test --stream",
"test:e2e:ci": "lerna run cy:ci --stream",

View file

@ -1,6 +1,6 @@
{
"name": "@budibase/backend-core",
"version": "1.3.19-alpha.0",
"version": "1.4.2",
"description": "Budibase backend core libraries used in server and worker",
"main": "dist/src/index.js",
"types": "dist/src/index.d.ts",
@ -20,7 +20,7 @@
"test:watch": "jest --watchAll"
},
"dependencies": {
"@budibase/types": "1.3.19-alpha.0",
"@budibase/types": "^1.4.2",
"@shopify/jest-koa-mocks": "5.0.1",
"@techpass/passport-openidconnect": "0.3.2",
"aws-sdk": "2.1030.0",

View file

@ -2,7 +2,7 @@ import env from "../environment"
import { SEPARATOR, DocumentType } from "../db/constants"
import cls from "./FunctionContext"
import { dangerousGetDB, closeDB } from "../db"
import { baseGlobalDBName } from "../tenancy/utils"
import { baseGlobalDBName } from "../db/tenancy"
import { IdentityContext } from "@budibase/types"
import { DEFAULT_TENANT_ID as _DEFAULT_TENANT_ID } from "../constants"
import { ContextKey } from "./constants"

View file

@ -44,6 +44,7 @@ export enum DocumentType {
DEV_INFO = "devinfo",
AUTOMATION_LOG = "log_au",
ACCOUNT_METADATA = "acc_metadata",
PLUGIN = "plg",
}
export const StaticDatabases = {

View file

@ -1,5 +1,5 @@
import { DEFAULT_TENANT_ID } from "../constants"
import { StaticDatabases, SEPARATOR } from "../db/constants"
import { StaticDatabases, SEPARATOR } from "./constants"
import { getTenantId } from "../context"
export const getGlobalDBName = (tenantId?: string) => {

View file

@ -3,7 +3,7 @@ import { DEFAULT_TENANT_ID, Configs } from "../constants"
import env from "../environment"
import { SEPARATOR, DocumentType, UNICODE_MAX, ViewName } from "./constants"
import { getTenantId, getGlobalDB } from "../context"
import { getGlobalDBName } from "../tenancy/utils"
import { getGlobalDBName } from "./tenancy"
import fetch from "node-fetch"
import { doWithDB, allDbs } from "./index"
import { getCouchInfo } from "./pouch"
@ -16,6 +16,7 @@ import * as events from "../events"
export * from "./constants"
export * from "./conversions"
export { default as Replication } from "./Replication"
export * from "./tenancy"
/**
* Generates a new app ID.
@ -367,6 +368,21 @@ export const generateDevInfoID = (userId: any) => {
return `${DocumentType.DEV_INFO}${SEPARATOR}${userId}`
}
/**
* Generates a new plugin ID - to be used in the global DB.
* @returns {string} The new plugin ID which a plugin metadata document can be stored under.
*/
export const generatePluginID = (name: string) => {
return `${DocumentType.PLUGIN}${SEPARATOR}${name}`
}
/**
* Gets parameters for retrieving automations, this is a utility function for the getDocParams function.
*/
export const getPluginParams = (pluginId?: string | null, otherProps = {}) => {
return getDocParams(DocumentType.PLUGIN, pluginId, otherProps)
}
/**
* Returns the most granular configuration document from the DB based on the type, workspace and userID passed.
* @param {Object} db - db instance to query

View file

@ -3,12 +3,8 @@ import { doWithDB } from "../db"
import { DocumentType, StaticDatabases } from "../db/constants"
import { getAllApps } from "../db/utils"
import environment from "../environment"
import {
doInTenant,
getTenantIds,
getGlobalDBName,
getTenantId,
} from "../tenancy"
import { doInTenant, getTenantIds, getTenantId } from "../tenancy"
import { getGlobalDBName } from "../db/tenancy"
import * as context from "../context"
import { DEFINITIONS } from "."
import {

View file

@ -1,11 +1,9 @@
import * as context from "../context"
import * as tenancy from "./tenancy"
import * as utils from "./utils"
const pkg = {
...context,
...tenancy,
...utils,
}
export = pkg

View file

@ -1,7 +1,7 @@
import { doWithDB } from "../db"
import { queryPlatformView } from "../db/views"
import { StaticDatabases, ViewName } from "../db/constants"
import { getGlobalDBName } from "./utils"
import { getGlobalDBName } from "../db/tenancy"
import {
getTenantId,
DEFAULT_TENANT_ID,
@ -9,7 +9,7 @@ import {
getTenantIDFromAppID,
} from "../context"
import env from "../environment"
import { PlatformUser, PlatformUserByEmail } from "@budibase/types"
import { PlatformUser } from "@budibase/types"
const TENANT_DOC = StaticDatabases.PLATFORM_INFO.docs.tenants
const PLATFORM_INFO_DB = StaticDatabases.PLATFORM_INFO.name

View file

@ -1,7 +1,7 @@
{
"name": "@budibase/bbui",
"description": "A UI solution used in the different Budibase projects.",
"version": "1.3.19-alpha.0",
"version": "1.4.2",
"license": "MPL-2.0",
"svelte": "src/index.js",
"module": "dist/bbui.es.js",
@ -38,7 +38,7 @@
],
"dependencies": {
"@adobe/spectrum-css-workflow-icons": "^1.2.1",
"@budibase/string-templates": "1.3.19-alpha.0",
"@budibase/string-templates": "^1.4.2",
"@spectrum-css/actionbutton": "^1.0.1",
"@spectrum-css/actiongroup": "^1.0.1",
"@spectrum-css/avatar": "^3.0.2",

View file

@ -78,7 +78,7 @@
bottom: 0;
background: var(--background);
border-top: var(--border-light);
z-index: 2;
z-index: 3;
}
.fillWidth {

View file

@ -48,7 +48,7 @@
display: flex;
justify-content: center;
top: 15px;
z-index: 100;
z-index: 200;
width: 160px;
}
.icon {

View file

@ -2,7 +2,7 @@ import filterTests from "../support/filterTests"
const interact = require("../support/interact")
filterTests(["all"], () => {
context("Create Components", () => {
xcontext("Create Components", () => {
let headlineId
before(() => {

View file

@ -1,6 +1,6 @@
{
"name": "@budibase/builder",
"version": "1.3.19-alpha.0",
"version": "1.4.2",
"license": "GPL-3.0",
"private": true,
"scripts": {
@ -9,6 +9,7 @@
"dev:builder": "routify -c dev:vite",
"dev:vite": "vite --host 0.0.0.0",
"rollup": "rollup -c -w",
"test": "jest",
"cy:setup": "ts-node ./cypress/ts/setup.ts",
"cy:setup:ci": "node ./cypress/setup.js",
"cy:open": "cypress open",
@ -36,7 +37,8 @@
"components(.*)$": "<rootDir>/src/components$1",
"builderStore(.*)$": "<rootDir>/src/builderStore$1",
"stores(.*)$": "<rootDir>/src/stores$1",
"analytics(.*)$": "<rootDir>/src/analytics$1"
"analytics(.*)$": "<rootDir>/src/analytics$1",
"constants/backend": "<rootDir>/src/constants/backend/index.js"
},
"moduleFileExtensions": [
"js",
@ -69,10 +71,10 @@
}
},
"dependencies": {
"@budibase/bbui": "1.3.19-alpha.0",
"@budibase/client": "1.3.19-alpha.0",
"@budibase/frontend-core": "1.3.19-alpha.0",
"@budibase/string-templates": "1.3.19-alpha.0",
"@budibase/bbui": "^1.4.2",
"@budibase/client": "^1.4.2",
"@budibase/frontend-core": "^1.4.2",
"@budibase/string-templates": "^1.4.2",
"@sentry/browser": "5.19.1",
"@spectrum-css/page": "^3.0.1",
"@spectrum-css/vars": "^3.0.1",

View file

@ -9,14 +9,14 @@ import {
import { store } from "builderStore"
import {
queries as queriesStores,
tables as tablesStore,
roles as rolesStore,
tables as tablesStore,
} from "stores/backend"
import {
makePropSafe,
isJSBinding,
decodeJSBinding,
encodeJSBinding,
isJSBinding,
makePropSafe,
} from "@budibase/string-templates"
import { TableNames } from "../constants"
import { JSONUtils } from "@budibase/frontend-core"
@ -118,8 +118,7 @@ export const readableToRuntimeMap = (bindings, ctx) => {
return {}
}
return Object.keys(ctx).reduce((acc, key) => {
let parsedQuery = readableToRuntimeBinding(bindings, ctx[key])
acc[key] = parsedQuery
acc[key] = readableToRuntimeBinding(bindings, ctx[key])
return acc
}, {})
}
@ -132,8 +131,7 @@ export const runtimeToReadableMap = (bindings, ctx) => {
return {}
}
return Object.keys(ctx).reduce((acc, key) => {
let parsedQuery = runtimeToReadableBinding(bindings, ctx[key])
acc[key] = parsedQuery
acc[key] = runtimeToReadableBinding(bindings, ctx[key])
return acc
}, {})
}
@ -379,7 +377,7 @@ const getProviderContextBindings = (asset, dataProviders) => {
/**
* Gets all bindable properties from the logged in user.
*/
const getUserBindings = () => {
export const getUserBindings = () => {
let bindings = []
const { schema } = getSchemaForTable(TableNames.USERS)
const keys = Object.keys(schema).sort()

View file

@ -97,7 +97,7 @@
backgroundColour={templateEntry.background}
icon={templateEntry.icon}
>
{#if $licensing?.usageMetrics?.apps < 100}
{#if !($licensing?.usageMetrics?.apps >= 100)}
<Button
cta
on:click={() => {

View file

@ -17,7 +17,7 @@
import ExtraQueryConfig from "./ExtraQueryConfig.svelte"
import IntegrationQueryEditor from "components/integration/index.svelte"
import ExternalDataSourceTable from "components/backend/DataTable/ExternalDataSourceTable.svelte"
import BindingBuilder from "components/integration/QueryBindingBuilder.svelte"
import BindingBuilder from "components/integration/QueryViewerBindingBuilder.svelte"
import { datasources, integrations, queries } from "stores/backend"
import { capitalise } from "../../helpers"
import CodeMirrorEditor from "components/common/CodeMirrorEditor.svelte"

View file

@ -0,0 +1,69 @@
<script>
import { Body, Button, Heading, Layout } from "@budibase/bbui"
import KeyValueBuilder from "components/integration/KeyValueBuilder.svelte"
import { getUserBindings } from "builderStore/dataBinding"
export let bindable = true
export let queryBindings = []
const userBindings = getUserBindings()
let internalBindings = queryBindings.reduce((acc, binding) => {
acc[binding.name] = binding.default
return acc
}, {})
function newQueryBinding() {
queryBindings = [...queryBindings, {}]
}
</script>
<Layout noPadding={bindable} gap="S">
<div class="controls" class:height={!bindable}>
<Heading size="XS">Bindings</Heading>
{#if !bindable}
<Button secondary on:click={newQueryBinding}>Add Binding</Button>
{/if}
</div>
<Body size="S">
{#if !bindable}
Bindings come in two parts: the binding name, and a default/fallback
value. These bindings can be used as Handlebars expressions throughout the
query.
{:else}
Enter a value for each binding. The default values will be used for any
values left blank.
{/if}
</Body>
<div class="bindings" class:bindable>
<KeyValueBuilder
bind:object={internalBindings}
tooltip="Set the name of the binding which can be used in Handlebars statements throughout your query"
name="binding"
headings
keyPlaceholder="Binding name"
valuePlaceholder="Default"
bindings={[...userBindings]}
bindingDrawerLeft="260px"
on:change={e => {
queryBindings = e.detail.map(binding => {
return {
name: binding.name,
default: binding.value,
}
})
}}
/>
</div>
</Layout>
<style>
.controls {
display: flex;
align-items: center;
justify-content: space-between;
}
.height {
height: 40px;
}
</style>

View file

@ -8,6 +8,7 @@
import { ExpiringKeys } from "./constants"
import { getBanners } from "./licensingBanners"
import { banner } from "@budibase/bbui"
import { FEATURE_FLAGS, isEnabled } from "../../../helpers/featureFlags"
const oneDayInSeconds = 86400
@ -81,7 +82,12 @@
}
}
$: if (userLoaded && licensingLoaded && loaded) {
$: if (
userLoaded &&
licensingLoaded &&
loaded &&
isEnabled(FEATURE_FLAGS.LICENSING)
) {
queuedModals = processModals()
queuedBanners = getBanners()
showNextModal()

View file

@ -1,4 +1,5 @@
import { IntegrationTypes } from "constants/backend"
import { findHBSBlocks } from "@budibase/string-templates"
export function schemaToFields(schema) {
const response = {}
@ -31,7 +32,7 @@ export function breakQueryString(qs) {
let paramObj = {}
for (let param of params) {
const split = param.split("=")
paramObj[split[0]] = split.slice(1).join("=")
paramObj[split[0]] = decodeURIComponent(split.slice(1).join("="))
}
return paramObj
}
@ -46,7 +47,19 @@ export function buildQueryString(obj) {
if (str !== "") {
str += "&"
}
str += `${key}=${encodeURIComponent(value || "")}`
const bindings = findHBSBlocks(value)
let count = 0
const bindingMarkers = {}
bindings.forEach(binding => {
const marker = `BINDING...${count++}`
value = value.replace(binding, marker)
bindingMarkers[marker] = binding
})
let encoded = encodeURIComponent(value || "")
Object.entries(bindingMarkers).forEach(([marker, binding]) => {
encoded = encoded.replace(marker, binding)
})
str += `${key}=${encoded}`
}
}
return str

View file

@ -0,0 +1,37 @@
import { breakQueryString, buildQueryString } from "../data/utils"
describe("check query string utils", () => {
const obj1 = {
key1: "123",
key2: " ",
key3: "333",
}
const obj2 = {
key1: "{{ binding.awd }}",
key2: "{{ binding.sed }} ",
}
it("should build a basic query string", () => {
const queryString = buildQueryString(obj1)
expect(queryString).toBe("key1=123&key2=%20%20%20&key3=333")
})
it("should be able to break a basic query string", () => {
const broken = breakQueryString("key1=123&key2=%20%20%20&key3=333")
expect(broken.key1).toBe(obj1.key1)
expect(broken.key2).toBe(obj1.key2)
expect(broken.key3).toBe(obj1.key3)
})
it("should be able to build with a binding", () => {
const queryString = buildQueryString(obj2)
expect(queryString).toBe("key1={{ binding.awd }}&key2={{ binding.sed }}%20%20")
})
it("should be able to break with a binding", () => {
const broken = breakQueryString("key1={{ binding.awd }}&key2={{ binding.sed }}%20%20")
expect(broken.key1).toBe(obj2.key1)
expect(broken.key2).toBe(obj2.key2)
})
})

View file

@ -708,6 +708,7 @@
.url-block {
display: flex;
gap: var(--spacing-s);
z-index: 200;
}
.verb {
flex: 1;

View file

@ -56,7 +56,7 @@
{
title: "Plugins",
href: "/builder/portal/manage/plugins",
badge: "New",
badge: "Beta",
},
{

View file

@ -10,7 +10,7 @@
Search,
} from "@budibase/bbui"
import { onMount } from "svelte"
import { plugins } from "stores/portal"
import { plugins, admin } from "stores/portal"
import PluginRow from "./_components/PluginRow.svelte"
import AddPluginModal from "./_components/AddPluginModal.svelte"
@ -20,9 +20,12 @@
let filterOptions = [
{ label: "All plugins", value: "all" },
{ label: "Components", value: "component" },
{ label: "Datasources", value: "datasource" },
]
if (!$admin.cloud) {
filterOptions.push({ label: "Datasources", value: "datasource" })
}
$: filteredPlugins = $plugins
.filter(plugin => {
return filter === "all" || plugin.schema.type === filter

View file

@ -88,14 +88,14 @@
<Heading size="S">Information</Heading>
<Body size="S">Here you can update your logo and organization name.</Body>
</Layout>
<div className="fields">
<div className="field">
<div class="fields">
<div class="field">
<Label size="L">Org. name</Label>
<Input thin bind:value={$values.company} />
</div>
<div className="field logo">
<div class="field logo">
<Label size="L">Logo</Label>
<div className="file">
<div class="file">
<Dropzone
value={[$values.logo]}
on:change={e => {
@ -115,13 +115,14 @@
<Heading size="S">Platform</Heading>
<Body size="S">Here you can set up general platform settings.</Body>
</Layout>
<div className="fields">
<div className="field">
<div class="fields">
<div class="field">
<Label
size="L"
tooltip={"Update the Platform URL to match your Budibase web URL. This keeps email templates and authentication configs up to date."}
>Platform URL</Label
>
Platform URL
</Label>
<Input thin bind:value={$values.platformUrl} />
</div>
</div>

View file

@ -1,7 +1,7 @@
import { writable, get } from "svelte/store"
import { datasources, integrations, tables, views } from "./"
import { API } from "api"
import { duplicateName } from "../../helpers/duplicate"
import { duplicateName } from "helpers/duplicate"
const sortQueries = queryList => {
queryList.sort((q1, q2) => {

View file

@ -2,7 +2,7 @@ import { get, writable } from "svelte/store"
import { datasources, queries, views } from "./"
import { cloneDeep } from "lodash/fp"
import { API } from "api"
import { SWITCHABLE_TYPES } from "../../constants/backend"
import { SWITCHABLE_TYPES } from "constants/backend"
export function createTablesStore() {
const store = writable({})

View file

@ -1,9 +1,9 @@
import { get } from 'svelte/store'
import api from 'builderStore/api'
import { get } from "svelte/store"
import { API } from "api"
jest.mock('builderStore/api');
jest.mock("api")
import { SOME_DATASOURCE, SAVE_DATASOURCE} from './fixtures/datasources'
import { SOME_DATASOURCE, SAVE_DATASOURCE } from "./fixtures/datasources"
import { createDatasourcesStore } from "../datasources"
import { queries } from '../queries'
@ -12,39 +12,39 @@ describe("Datasources Store", () => {
let store = createDatasourcesStore()
beforeEach(async () => {
api.get.mockReturnValue({ json: () => [SOME_DATASOURCE]})
API.getDatasources.mockReturnValue({ json: () => [SOME_DATASOURCE]})
await store.init()
})
it("Initialises correctly", async () => {
api.get.mockReturnValue({ json: () => [SOME_DATASOURCE]})
API.getDatasources.mockReturnValue({ json: () => [SOME_DATASOURCE]})
await store.init()
expect(get(store)).toEqual({ list: [SOME_DATASOURCE], selected: null})
})
it("fetches all the datasources and updates the store", async () => {
api.get.mockReturnValue({ json: () => [SOME_DATASOURCE] })
API.getDatasources.mockReturnValue({ json: () => [SOME_DATASOURCE] })
await store.fetch()
expect(get(store)).toEqual({ list: [SOME_DATASOURCE], selected: null })
expect(get(store)).toEqual({ list: [SOME_DATASOURCE], selected: null })
})
it("selects a datasource", async () => {
store.select(SOME_DATASOURCE._id)
expect(get(store).select).toEqual(SOME_DATASOURCE._id)
expect(get(store).select).toEqual(SOME_DATASOURCE._id)
})
it("resets the queries store when new datasource is selected", async () => {
await store.select(SOME_DATASOURCE._id)
const queriesValue = get(queries)
expect(queriesValue.selected).toEqual(null)
expect(queriesValue.selected).toEqual(null)
})
it("saves the datasource, updates the store and returns status message", async () => {
api.post.mockReturnValue({ status: 200, json: () => SAVE_DATASOURCE})
API.createDatasource.mockReturnValue({ status: 200, json: () => SAVE_DATASOURCE})
await store.save({
name: 'CoolDB',
@ -56,13 +56,13 @@ describe("Datasources Store", () => {
expect(get(store).list).toEqual(expect.arrayContaining([SAVE_DATASOURCE.datasource]))
})
it("deletes a datasource, updates the store and returns status message", async () => {
api.get.mockReturnValue({ json: () => SOME_DATASOURCE})
API.getDatasources.mockReturnValue({ json: () => SOME_DATASOURCE})
await store.fetch()
api.delete.mockReturnValue({status: 200, message: 'Datasource deleted.'})
API.deleteDatasource.mockReturnValue({status: 200, message: 'Datasource deleted.'})
await store.delete(SOME_DATASOURCE[0])
expect(get(store)).toEqual({ list: [], selected: null})
expect(get(store)).toEqual({ list: [], selected: null})
})
})

View file

@ -1,6 +1,6 @@
import api from 'builderStore/api'
import { API } from "api"
jest.mock('builderStore/api');
jest.mock("api")
const PERMISSIONS_FOR_RESOURCE = {
"write": "BASIC",
@ -13,13 +13,12 @@ describe("Permissions Store", () => {
const store = createPermissionStore()
it("fetches permissions for specific resource", async () => {
api.get.mockReturnValueOnce({ json: () => PERMISSIONS_FOR_RESOURCE})
API.getPermissionForResource.mockReturnValueOnce({ json: () => PERMISSIONS_FOR_RESOURCE})
const resourceId = "ta_013657543b4043b89dbb17e9d3a4723a"
const permissions = await store.forResource(resourceId)
expect(api.get).toBeCalledWith(`/api/permission/${resourceId}`)
expect(permissions).toEqual(PERMISSIONS_FOR_RESOURCE)
})
})

View file

@ -1,9 +1,9 @@
import { get } from 'svelte/store'
import api from 'builderStore/api'
import { get } from "svelte/store"
import { API } from "api"
jest.mock('builderStore/api');
jest.mock("api")
import { SOME_QUERY, SAVE_QUERY_RESPONSE } from './fixtures/queries'
import { SOME_QUERY, SAVE_QUERY_RESPONSE } from "./fixtures/queries"
import { createQueriesStore } from "../queries"
@ -11,36 +11,36 @@ describe("Queries Store", () => {
let store = createQueriesStore()
beforeEach(async () => {
api.get.mockReturnValue({ json: () => [SOME_QUERY]})
API.getQueries.mockReturnValue({ json: () => [SOME_QUERY]})
await store.init()
})
it("Initialises correctly", async () => {
api.get.mockReturnValue({ json: () => [SOME_QUERY]})
API.getQueries.mockReturnValue({ json: () => [SOME_QUERY]})
await store.init()
expect(get(store)).toEqual({ list: [SOME_QUERY], selected: null})
})
it("fetches all the queries", async () => {
api.get.mockReturnValue({ json: () => [SOME_QUERY]})
API.getQueries.mockReturnValue({ json: () => [SOME_QUERY]})
await store.fetch()
expect(get(store)).toEqual({ list: [SOME_QUERY], selected: null})
expect(get(store)).toEqual({ list: [SOME_QUERY], selected: null})
})
it("saves the query, updates the store and returns status message", async () => {
api.post.mockReturnValue({ json: () => SAVE_QUERY_RESPONSE})
API.saveQuery.mockReturnValue({ json: () => SAVE_QUERY_RESPONSE})
await store.select(SOME_QUERY.datasourceId, SOME_QUERY)
expect(get(store).list).toEqual(expect.arrayContaining([SOME_QUERY]))
})
it("deletes a query, updates the store and returns status message", async () => {
api.delete.mockReturnValue({status: 200, message: `Query deleted.`})
API.deleteQuery.mockReturnValue({status: 200, message: `Query deleted.`})
await store.delete(SOME_QUERY)
expect(get(store)).toEqual({ list: [], selected: null})
expect(get(store)).toEqual({ list: [], selected: null})
})
})

View file

@ -1,10 +1,10 @@
import { get } from 'svelte/store'
import api from 'builderStore/api'
import { get } from "svelte/store"
import { API } from "api"
jest.mock('builderStore/api');
jest.mock("api")
import { createRolesStore } from "../roles"
import { ROLES } from './fixtures/roles'
import { ROLES } from "./fixtures/roles"
describe("Roles Store", () => {
let store = createRolesStore()
@ -14,19 +14,18 @@ describe("Roles Store", () => {
})
it("fetches roles from backend", async () => {
api.get.mockReturnValue({ json: () => ROLES})
API.getRoles.mockReturnValue({ json: () => ROLES})
await store.fetch()
expect(api.get).toBeCalledWith("/api/roles")
expect(get(store)).toEqual(ROLES)
})
it("deletes a role", async () => {
api.get.mockReturnValueOnce({ json: () => ROLES})
API.getRoles.mockReturnValueOnce({ json: () => ROLES})
await store.fetch()
api.delete.mockReturnValue({status: 200, message: `Role deleted.`})
API.deleteRole.mockReturnValue({status: 200, message: `Role deleted.`})
const updatedRoles = [...ROLES.slice(1)]
await store.delete(ROLES[0])

View file

@ -1,18 +1,16 @@
import { get } from 'svelte/store'
import api from 'builderStore/api'
import { get } from "svelte/store"
import { API } from "api"
jest.mock('builderStore/api');
import { SOME_TABLES, SAVE_TABLES_RESPONSE, A_TABLE } from './fixtures/tables'
jest.mock("api")
import { SOME_TABLES, SAVE_TABLES_RESPONSE, A_TABLE } from "./fixtures/tables"
import { createTablesStore } from "../tables"
import { views } from '../views'
describe("Tables Store", () => {
let store = createTablesStore()
beforeEach(async () => {
api.get.mockReturnValue({ json: () => SOME_TABLES})
API.getTables.mockReturnValue({ json: () => SOME_TABLES})
await store.init()
})
@ -21,46 +19,46 @@ describe("Tables Store", () => {
})
it("fetches all the tables", async () => {
api.get.mockReturnValue({ json: () => SOME_TABLES})
API.getTables.mockReturnValue({ json: () => SOME_TABLES})
await store.fetch()
expect(get(store)).toEqual({ list: SOME_TABLES, selected: {}, draft: {}})
expect(get(store)).toEqual({ list: SOME_TABLES, selected: {}, draft: {}})
})
it("selects a table", async () => {
const tableToSelect = SOME_TABLES[0]
await store.select(tableToSelect)
expect(get(store).selected).toEqual(tableToSelect)
expect(get(store).draft).toEqual(tableToSelect)
expect(get(store).selected).toEqual(tableToSelect)
expect(get(store).draft).toEqual(tableToSelect)
})
it("selecting without a param resets the selected property", async () => {
await store.select()
expect(get(store).draft).toEqual({})
expect(get(store).draft).toEqual({})
})
it("saving a table also selects it", async () => {
api.post.mockReturnValue({ status: 200, json: () => SAVE_TABLES_RESPONSE})
API.post.mockReturnValue({ status: 200, json: () => SAVE_TABLES_RESPONSE})
await store.save(A_TABLE)
expect(get(store).selected).toEqual(SAVE_TABLES_RESPONSE)
expect(get(store).selected).toEqual(SAVE_TABLES_RESPONSE)
})
it("saving the table returns a response", async () => {
api.post.mockReturnValue({ status: 200, json: () => SAVE_TABLES_RESPONSE})
API.saveTable.mockReturnValue({ status: 200, json: () => SAVE_TABLES_RESPONSE})
const response = await store.save(A_TABLE)
expect(response).toEqual(SAVE_TABLES_RESPONSE)
expect(response).toEqual(SAVE_TABLES_RESPONSE)
})
it("deleting a table removes it from the store", async () => {
api.delete.mockReturnValue({status: 200, message: `Table deleted.`})
API.deleteTable.mockReturnValue({status: 200, message: `Table deleted.`})
await store.delete(A_TABLE)
expect(get(store).list).toEqual(expect.not.arrayContaining([A_TABLE]))
expect(get(store).list).toEqual(expect.not.arrayContaining([A_TABLE]))
})
// TODO: Write tests for saving and deleting fields

View file

@ -3,10 +3,12 @@ import { API } from "api"
import { auth } from "stores/portal"
import { Constants } from "@budibase/frontend-core"
import { StripeStatus } from "components/portal/licensing/constants"
import { FEATURE_FLAGS, isEnabled } from "../../helpers/featureFlags"
export const createLicensingStore = () => {
const DEFAULT = {
plans: {},
usageMetrics: {},
}
const oneDayInMilliseconds = 86400000
@ -27,78 +29,80 @@ export const createLicensingStore = () => {
})
},
getUsageMetrics: async () => {
const quota = get(store).quotaUsage
const license = get(auth).user.license
const now = new Date()
if (isEnabled(FEATURE_FLAGS.LICENSING)) {
const quota = get(store).quotaUsage
const license = get(auth).user.license
const now = new Date()
const getMetrics = (keys, license, quota) => {
if (!license || !quota || !keys) {
return {}
const getMetrics = (keys, license, quota) => {
if (!license || !quota || !keys) {
return {}
}
return keys.reduce((acc, key) => {
const quotaLimit = license[key].value
const quotaUsed = (quota[key] / quotaLimit) * 100
acc[key] = quotaLimit > -1 ? Math.round(quotaUsed) : -1
return acc
}, {})
}
return keys.reduce((acc, key) => {
const quotaLimit = license[key].value
const quotaUsed = (quota[key] / quotaLimit) * 100
acc[key] = quotaLimit > -1 ? Math.round(quotaUsed) : -1
return acc
}, {})
}
const monthlyMetrics = getMetrics(
["dayPasses", "queries", "automations"],
license.quotas.usage.monthly,
quota.monthly.current
)
const staticMetrics = getMetrics(
["apps", "rows"],
license.quotas.usage.static,
quota.usageQuota
)
const getDaysBetween = (dateStart, dateEnd) => {
return dateEnd > dateStart
? Math.round(
(dateEnd.getTime() - dateStart.getTime()) / oneDayInMilliseconds
)
: 0
}
const quotaResetDate = new Date(quota.quotaReset)
const quotaResetDaysRemaining = getDaysBetween(now, quotaResetDate)
const accountDowngraded =
license?.billing?.subscription?.downgradeAt &&
license?.billing?.subscription?.downgradeAt <= now.getTime() &&
license?.billing?.subscription?.status === StripeStatus.PAST_DUE &&
license?.plan.type === Constants.PlanType.FREE
const pastDueAtMilliseconds = license?.billing?.subscription?.pastDueAt
const downgradeAtMilliseconds =
license?.billing?.subscription?.downgradeAt
let pastDueDaysRemaining
let pastDueEndDate
if (pastDueAtMilliseconds && downgradeAtMilliseconds) {
pastDueEndDate = new Date(downgradeAtMilliseconds)
pastDueDaysRemaining = getDaysBetween(
new Date(pastDueAtMilliseconds),
pastDueEndDate
const monthlyMetrics = getMetrics(
["dayPasses", "queries", "automations"],
license.quotas.usage.monthly,
quota.monthly.current
)
const staticMetrics = getMetrics(
["apps", "rows"],
license.quotas.usage.static,
quota.usageQuota
)
}
store.update(state => {
return {
...state,
usageMetrics: { ...monthlyMetrics, ...staticMetrics },
quotaResetDaysRemaining,
quotaResetDate,
accountDowngraded,
accountPastDue: pastDueAtMilliseconds != null,
pastDueEndDate,
pastDueDaysRemaining,
isFreePlan: () => {
return license?.plan.type === Constants.PlanType.FREE
},
const getDaysBetween = (dateStart, dateEnd) => {
return dateEnd > dateStart
? Math.round(
(dateEnd.getTime() - dateStart.getTime()) / oneDayInMilliseconds
)
: 0
}
})
const quotaResetDate = new Date(quota.quotaReset)
const quotaResetDaysRemaining = getDaysBetween(now, quotaResetDate)
const accountDowngraded =
license?.billing?.subscription?.downgradeAt &&
license?.billing?.subscription?.downgradeAt <= now.getTime() &&
license?.billing?.subscription?.status === StripeStatus.PAST_DUE &&
license?.plan.type === Constants.PlanType.FREE
const pastDueAtMilliseconds = license?.billing?.subscription?.pastDueAt
const downgradeAtMilliseconds =
license?.billing?.subscription?.downgradeAt
let pastDueDaysRemaining
let pastDueEndDate
if (pastDueAtMilliseconds && downgradeAtMilliseconds) {
pastDueEndDate = new Date(downgradeAtMilliseconds)
pastDueDaysRemaining = getDaysBetween(
new Date(pastDueAtMilliseconds),
pastDueEndDate
)
}
store.update(state => {
return {
...state,
usageMetrics: { ...monthlyMetrics, ...staticMetrics },
quotaResetDaysRemaining,
quotaResetDate,
accountDowngraded,
accountPastDue: pastDueAtMilliseconds != null,
pastDueEndDate,
pastDueDaysRemaining,
isFreePlan: () => {
return license?.plan.type === Constants.PlanType.FREE
},
}
})
}
},
}

View file

@ -1,6 +1,6 @@
{
"name": "@budibase/cli",
"version": "1.3.19-alpha.0",
"version": "1.4.2",
"description": "Budibase CLI, for developers, self hosting and migrations.",
"main": "src/index.js",
"bin": {
@ -26,9 +26,7 @@
"outputPath": "build"
},
"dependencies": {
"@budibase/backend-core": "1.3.19-alpha.0",
"@budibase/string-templates": "1.3.19-alpha.0",
"@budibase/types": "1.3.19-alpha.0",
"@budibase/backend-core": "^1.4.2",
"axios": "0.21.2",
"chalk": "4.1.0",
"cli-progress": "3.11.2",

View file

@ -1,6 +1,6 @@
{
"name": "@budibase/client",
"version": "1.3.19-alpha.0",
"version": "1.4.2",
"license": "MPL-2.0",
"module": "dist/budibase-client.js",
"main": "dist/budibase-client.js",
@ -19,9 +19,9 @@
"dev:builder": "rollup -cw"
},
"dependencies": {
"@budibase/bbui": "1.3.19-alpha.0",
"@budibase/frontend-core": "1.3.19-alpha.0",
"@budibase/string-templates": "1.3.19-alpha.0",
"@budibase/bbui": "^1.4.2",
"@budibase/frontend-core": "^1.4.2",
"@budibase/string-templates": "^1.4.2",
"@spectrum-css/button": "^3.0.3",
"@spectrum-css/card": "^3.0.3",
"@spectrum-css/divider": "^1.0.3",

View file

@ -1,5 +1,6 @@
export const PlanType = {
FREE: "free",
PRO: "pro",
TEAM: "team",
BUSINESS: "business",
ENTERPRISE: "enterprise",

View file

@ -1,5 +1,6 @@
import { isFreePlan } from "./utils.js"
// import { isFreePlan } from "./utils.js"
export const logoEnabled = () => {
return isFreePlan()
return false
// return isFreePlan()
}

View file

@ -1,12 +1,12 @@
{
"name": "@budibase/frontend-core",
"version": "1.3.19-alpha.0",
"version": "1.4.2",
"description": "Budibase frontend core libraries used in builder and client",
"author": "Budibase",
"license": "MPL-2.0",
"svelte": "src/index.js",
"dependencies": {
"@budibase/bbui": "1.3.19-alpha.0",
"@budibase/bbui": "^1.4.2",
"lodash": "^4.17.21",
"svelte": "^3.46.2"
}

View file

@ -1,7 +1,7 @@
{
"name": "@budibase/server",
"email": "hi@budibase.com",
"version": "1.3.19-alpha.0",
"version": "1.4.2",
"description": "Budibase Web Server",
"main": "src/index.ts",
"repository": {
@ -77,11 +77,11 @@
"license": "GPL-3.0",
"dependencies": {
"@apidevtools/swagger-parser": "10.0.3",
"@budibase/backend-core": "1.3.19-alpha.0",
"@budibase/client": "1.3.19-alpha.0",
"@budibase/pro": "1.3.19-alpha.0",
"@budibase/string-templates": "1.3.19-alpha.0",
"@budibase/types": "1.3.19-alpha.0",
"@budibase/backend-core": "^1.4.2",
"@budibase/client": "^1.4.2",
"@budibase/pro": "1.4.2",
"@budibase/string-templates": "^1.4.2",
"@budibase/types": "^1.4.2",
"@bull-board/api": "3.7.0",
"@bull-board/koa": "3.9.4",
"@elastic/elasticsearch": "7.10.0",

View file

@ -72,6 +72,9 @@ async function up() {
console.log("Spinning up your budibase dev environment... 🔧✨")
await init()
await compose.upAll(CONFIG)
// We always ensure to restart the proxy service in case of nginx conf changes
await compose.restartOne("proxy-service", CONFIG)
}
async function down() {

View file

@ -1659,6 +1659,117 @@
"data"
]
},
"rowSearch": {
"type": "object",
"properties": {
"query": {
"type": "object",
"properties": {
"allOr": {
"type": "boolean",
"description": "Specifies that a row should be returned if it satisfies any of the specified options, rather than requiring it to fulfill all the search parameters. This defaults to false, meaning AND logic will be used."
},
"string": {
"type": "object",
"example": {
"columnName1": "value",
"columnName2": "value"
},
"description": "A map of field name to the string to search for, this will look for rows that have a value starting with the string value.",
"additionalProperties": {
"type": "string",
"description": "The value to search for in the column."
}
},
"fuzzy": {
"type": "object",
"description": "A fuzzy search, only supported by internal tables."
},
"range": {
"type": "object",
"description": "Searches within a range, the format of this must be in the format of an object with a \"low\" and \"high\" property.",
"example": {
"columnName1": {
"low": 10,
"high": 20
}
}
},
"equal": {
"type": "object",
"description": "Searches for rows that have a column value that is exactly the value set."
},
"notEqual": {
"type": "object",
"description": "Searches for any row which does not contain the specified column value."
},
"empty": {
"type": "object",
"description": "Searches for rows which do not contain the specified column. The object should simply contain keys of the column names, these can map to any value.",
"example": {
"columnName1": ""
}
},
"notEmpty": {
"type": "object",
"description": "Searches for rows which have the specified column."
},
"oneOf": {
"type": "object",
"description": "Searches for rows which have a column value that is any of the specified values. The format of this must be columnName -> [value1, value2]."
}
}
},
"paginate": {
"type": "boolean",
"description": "Enables pagination, by default this is disabled."
},
"bookmark": {
"oneOf": [
{
"type": "string"
},
{
"type": "integer"
}
],
"description": "If retrieving another page, the bookmark from the previous request must be supplied."
},
"limit": {
"type": "integer",
"description": "The maximum number of rows to return, useful when paginating, for internal tables this will be limited to 1000, for SQL tables it will be 5000."
},
"sort": {
"type": "object",
"description": "A set of parameters describing the sort behaviour of the search.",
"properties": {
"order": {
"type": "string",
"enum": [
"ascending",
"descending"
],
"description": "The order of the sort, by default this is ascending."
},
"column": {
"type": "string",
"description": "The name of the column by which the rows will be sorted."
},
"type": {
"type": "string",
"enum": [
"string",
"number"
],
"description": "Defines whether the column should be treated as a string or as numbers when sorting."
}
}
}
},
"required": [
"query"
]
},
"nameSearch": {
"type": "object",
"properties": {
@ -2129,115 +2240,7 @@
"content": {
"application/json": {
"schema": {
"type": "object",
"required": [
"query"
],
"properties": {
"query": {
"type": "object",
"properties": {
"allOr": {
"type": "boolean",
"description": "Specifies that a row should be returned if it satisfies any of the specified options, rather than requiring it to fulfill all the search parameters. This defaults to false, meaning AND logic will be used."
},
"string": {
"type": "object",
"example": {
"columnName1": "value",
"columnName2": "value"
},
"description": "A map of field name to the string to search for, this will look for rows that have a value starting with the string value.",
"additionalProperties": {
"type": "string",
"description": "The value to search for in the column."
}
},
"fuzzy": {
"type": "object",
"description": "A fuzzy search, only supported by internal tables."
},
"range": {
"type": "object",
"description": "Searches within a range, the format of this must be in the format of an object with a \"low\" and \"high\" property.",
"example": {
"columnName1": {
"low": 10,
"high": 20
}
}
},
"equal": {
"type": "object",
"description": "Searches for rows that have a column value that is exactly the value set."
},
"notEqual": {
"type": "object",
"description": "Searches for any row which does not contain the specified column value."
},
"empty": {
"type": "object",
"description": "Searches for rows which do not contain the specified column. The object should simply contain keys of the column names, these can map to any value.",
"example": {
"columnName1": ""
}
},
"notEmpty": {
"type": "object",
"description": "Searches for rows which have the specified column."
},
"oneOf": {
"type": "object",
"description": "Searches for rows which have a column value that is any of the specified values. The format of this must be columnName -> [value1, value2]."
}
}
},
"paginate": {
"type": "boolean",
"description": "Enables pagination, by default this is disabled."
},
"bookmark": {
"oneOf": [
{
"type": "string"
},
{
"type": "integer"
}
],
"description": "If retrieving another page, the bookmark from the previous request must be supplied."
},
"limit": {
"type": "integer",
"description": "The maximum number of rows to return, useful when paginating, for internal tables this will be limited to 1000, for SQL tables it will be 5000."
},
"sort": {
"type": "object",
"description": "A set of parameters describing the sort behaviour of the search.",
"properties": {
"order": {
"type": "string",
"enum": [
"ascending",
"descending"
],
"description": "The order of the sort, by default this is ascending."
},
"column": {
"type": "string",
"description": "The name of the column by which the rows will be sorted."
},
"type": {
"type": "string",
"enum": [
"string",
"number"
],
"description": "Defines whether the column should be treated as a string or as numbers when sorting."
}
}
}
}
"$ref": "#/components/schemas/rowSearch"
}
}
}

View file

@ -1264,6 +1264,98 @@ components:
- _id
required:
- data
rowSearch:
type: object
properties:
query:
type: object
properties:
allOr:
type: boolean
description: Specifies that a row should be returned if it satisfies any of the
specified options, rather than requiring it to fulfill all the
search parameters. This defaults to false, meaning AND logic
will be used.
string:
type: object
example:
columnName1: value
columnName2: value
description: A map of field name to the string to search for, this will look for
rows that have a value starting with the string value.
additionalProperties:
type: string
description: The value to search for in the column.
fuzzy:
type: object
description: A fuzzy search, only supported by internal tables.
range:
type: object
description: Searches within a range, the format of this must be in the format
of an object with a "low" and "high" property.
example:
columnName1:
low: 10
high: 20
equal:
type: object
description: Searches for rows that have a column value that is exactly the
value set.
notEqual:
type: object
description: Searches for any row which does not contain the specified column
value.
empty:
type: object
description: Searches for rows which do not contain the specified column. The
object should simply contain keys of the column names, these can
map to any value.
example:
columnName1: ""
notEmpty:
type: object
description: Searches for rows which have the specified column.
oneOf:
type: object
description: Searches for rows which have a column value that is any of the
specified values. The format of this must be columnName ->
[value1, value2].
paginate:
type: boolean
description: Enables pagination, by default this is disabled.
bookmark:
oneOf:
- type: string
- type: integer
description: If retrieving another page, the bookmark from the previous request
must be supplied.
limit:
type: integer
description: The maximum number of rows to return, useful when paginating, for
internal tables this will be limited to 1000, for SQL tables it will
be 5000.
sort:
type: object
description: A set of parameters describing the sort behaviour of the search.
properties:
order:
type: string
enum:
- ascending
- descending
description: The order of the sort, by default this is ascending.
column:
type: string
description: The name of the column by which the rows will be sorted.
type:
type: string
enum:
- string
- number
description: Defines whether the column should be treated as a string or as
numbers when sorting.
required:
- query
nameSearch:
type: object
properties:
@ -1544,97 +1636,7 @@ paths:
content:
application/json:
schema:
type: object
required:
- query
properties:
query:
type: object
properties:
allOr:
type: boolean
description: Specifies that a row should be returned if it satisfies any of the
specified options, rather than requiring it to fulfill
all the search parameters. This defaults to false,
meaning AND logic will be used.
string:
type: object
example:
columnName1: value
columnName2: value
description: A map of field name to the string to search for, this will look for
rows that have a value starting with the string value.
additionalProperties:
type: string
description: The value to search for in the column.
fuzzy:
type: object
description: A fuzzy search, only supported by internal tables.
range:
type: object
description: Searches within a range, the format of this must be in the format
of an object with a "low" and "high" property.
example:
columnName1:
low: 10
high: 20
equal:
type: object
description: Searches for rows that have a column value that is exactly the
value set.
notEqual:
type: object
description: Searches for any row which does not contain the specified column
value.
empty:
type: object
description: Searches for rows which do not contain the specified column. The
object should simply contain keys of the column names,
these can map to any value.
example:
columnName1: ""
notEmpty:
type: object
description: Searches for rows which have the specified column.
oneOf:
type: object
description: Searches for rows which have a column value that is any of the
specified values. The format of this must be columnName
-> [value1, value2].
paginate:
type: boolean
description: Enables pagination, by default this is disabled.
bookmark:
oneOf:
- type: string
- type: integer
description: If retrieving another page, the bookmark from the previous request
must be supplied.
limit:
type: integer
description: The maximum number of rows to return, useful when paginating, for
internal tables this will be limited to 1000, for SQL tables
it will be 5000.
sort:
type: object
description: A set of parameters describing the sort behaviour of the search.
properties:
order:
type: string
enum:
- ascending
- descending
description: The order of the sort, by default this is ascending.
column:
type: string
description: The name of the column by which the rows will be sorted.
type:
type: string
enum:
- string
- number
description: Defines whether the column should be treated as a string or as
numbers when sorting.
$ref: "#/components/schemas/rowSearch"
responses:
"200":
description: The response will contain an array of rows that match the search

View file

@ -2,6 +2,122 @@ const { object } = require("./utils")
const Resource = require("./utils/Resource")
module.exports = new Resource().setSchemas({
rowSearch: object(
{
query: {
type: "object",
properties: {
allOr: {
type: "boolean",
description:
"Specifies that a row should be returned if it satisfies any of the specified options, rather than requiring it to fulfill all the search parameters. This defaults to false, meaning AND logic will be used.",
},
string: {
type: "object",
example: {
columnName1: "value",
columnName2: "value",
},
description:
"A map of field name to the string to search for, this will look for rows that have a value starting with the string value.",
additionalProperties: {
type: "string",
description: "The value to search for in the column.",
},
},
fuzzy: {
type: "object",
description: "A fuzzy search, only supported by internal tables.",
},
range: {
type: "object",
description:
'Searches within a range, the format of this must be in the format of an object with a "low" and "high" property.',
example: {
columnName1: {
low: 10,
high: 20,
},
},
},
equal: {
type: "object",
description:
"Searches for rows that have a column value that is exactly the value set.",
},
notEqual: {
type: "object",
description:
"Searches for any row which does not contain the specified column value.",
},
empty: {
type: "object",
description:
"Searches for rows which do not contain the specified column. The object should simply contain keys of the column names, these can map to any value.",
example: {
columnName1: "",
},
},
notEmpty: {
type: "object",
description: "Searches for rows which have the specified column.",
},
oneOf: {
type: "object",
description:
"Searches for rows which have a column value that is any of the specified values. The format of this must be columnName -> [value1, value2].",
},
},
},
paginate: {
type: "boolean",
description: "Enables pagination, by default this is disabled.",
},
bookmark: {
oneOf: [
{
type: "string",
},
{
type: "integer",
},
],
description:
"If retrieving another page, the bookmark from the previous request must be supplied.",
},
limit: {
type: "integer",
description:
"The maximum number of rows to return, useful when paginating, for internal tables this will be limited to 1000, for SQL tables it will be 5000.",
},
sort: {
type: "object",
description:
"A set of parameters describing the sort behaviour of the search.",
properties: {
order: {
type: "string",
enum: ["ascending", "descending"],
description: "The order of the sort, by default this is ascending.",
},
column: {
type: "string",
description:
"The name of the column by which the rows will be sorted.",
},
type: {
type: "string",
enum: ["string", "number"],
description:
"Defines whether the column should be treated as a string or as numbers when sorting.",
},
},
},
},
{
required: ["query"],
}
),
nameSearch: object({
name: {
type: "string",

View file

@ -1,15 +1,12 @@
const env = require("../../environment")
const { getAllApps } = require("@budibase/backend-core/db")
const { getAllApps, getGlobalDBName } = require("@budibase/backend-core/db")
const {
exportDB,
sendTempFile,
readFileSync,
} = require("../../utilities/fileSystem")
const { stringToReadStream } = require("../../utilities")
const {
getGlobalDBName,
getGlobalDB,
} = require("@budibase/backend-core/tenancy")
const { getGlobalDB } = require("@budibase/backend-core/tenancy")
const { create } = require("./application")
const { getDocParams, DocumentType, isDevAppID } = require("../../db/utils")

View file

@ -1,15 +1,15 @@
const { DocumentType, getPluginParams } = require("../../db/utils")
const { getComponentLibraryManifest } = require("../../utilities/fileSystem")
const { getAppDB } = require("@budibase/backend-core/context")
const { getGlobalDB } = require("@budibase/backend-core/tenancy")
import { DocumentType } from "../../db/utils"
import { Plugin } from "@budibase/types"
import { db as dbCore, context, tenancy } from "@budibase/backend-core"
import { getComponentLibraryManifest } from "../../utilities/fileSystem"
exports.fetchAppComponentDefinitions = async function (ctx) {
exports.fetchAppComponentDefinitions = async function (ctx: any) {
try {
const db = getAppDB()
const db = context.getAppDB()
const app = await db.get(DocumentType.APP_METADATA)
let componentManifests = await Promise.all(
app.componentLibraries.map(async library => {
app.componentLibraries.map(async (library: any) => {
let manifest = await getComponentLibraryManifest(library)
return {
manifest,
@ -17,7 +17,7 @@ exports.fetchAppComponentDefinitions = async function (ctx) {
}
})
)
const definitions = {}
const definitions: { [key: string]: any } = {}
for (let { manifest, library } of componentManifests) {
for (let key of Object.keys(manifest)) {
if (key === "features") {
@ -33,16 +33,16 @@ exports.fetchAppComponentDefinitions = async function (ctx) {
}
// Add custom components
const globalDB = getGlobalDB()
const globalDB = tenancy.getGlobalDB()
const response = await globalDB.allDocs(
getPluginParams(null, {
dbCore.getPluginParams(null, {
include_docs: true,
})
)
response.rows
.map(row => row.doc)
.filter(plugin => plugin.schema.type === "component")
.forEach(plugin => {
.map((row: any) => row.doc)
.filter((plugin: Plugin) => plugin.schema.type === "component")
.forEach((plugin: Plugin) => {
const fullComponentName = `plugin/${plugin.name}`
definitions[fullComponentName] = {
component: fullComponentName,

View file

@ -1,22 +1,16 @@
import { ObjectStoreBuckets } from "../../../constants"
import { loadJSFile } from "../../../utilities/fileSystem"
import { npmUpload, urlUpload, githubUpload, fileUpload } from "./uploaders"
import { getGlobalDB } from "@budibase/backend-core/tenancy"
import { validate } from "@budibase/backend-core/plugins"
import { generatePluginID, getPluginParams } from "../../../db/utils"
import {
uploadDirectory,
deleteFolder,
} from "@budibase/backend-core/objectStore"
import { PluginType, FileType, PluginSource, Plugin } from "@budibase/types"
import { PluginType, FileType, PluginSource } from "@budibase/types"
import env from "../../../environment"
import { ClientAppSocket } from "../../../websocket"
import { events } from "@budibase/backend-core"
import { db as dbCore } from "@budibase/backend-core"
import { plugins } from "@budibase/pro"
export async function getPlugins(type?: PluginType) {
const db = getGlobalDB()
const response = await db.allDocs(
getPluginParams(null, {
dbCore.getPluginParams(null, {
include_docs: true,
})
)
@ -37,7 +31,7 @@ export async function upload(ctx: any) {
let docs = []
// can do single or multiple plugins
for (let plugin of plugins) {
const doc = await processPlugin(plugin, PluginSource.FILE)
const doc = await processUploadedPlugin(plugin, PluginSource.FILE)
docs.push(doc)
}
ctx.body = {
@ -91,18 +85,19 @@ export async function create(ctx: any) {
)
}
const doc = await storePlugin(metadata, directory, source)
const doc = await plugins.storePlugin(metadata, directory, source)
ClientAppSocket.emit("plugins-update", { name, hash: doc.hash })
ctx.body = {
message: "Plugin uploaded successfully",
plugins: [doc],
}
ctx.body = { plugin: doc }
} catch (err: any) {
const errMsg = err?.message ? err?.message : err
ctx.throw(400, `Failed to import plugin: ${errMsg}`)
}
ctx.status = 200
}
export async function fetch(ctx: any) {
@ -110,99 +105,21 @@ export async function fetch(ctx: any) {
}
export async function destroy(ctx: any) {
const db = getGlobalDB()
const { pluginId } = ctx.params
try {
const plugin: Plugin = await db.get(pluginId)
const bucketPath = `${plugin.name}/`
await deleteFolder(ObjectStoreBuckets.PLUGINS, bucketPath)
await plugins.deletePlugin(pluginId)
await db.remove(pluginId, plugin._rev)
await events.plugin.deleted(plugin)
ctx.body = { message: `Plugin ${ctx.params.pluginId} deleted.` }
} catch (err: any) {
const errMsg = err?.message ? err?.message : err
ctx.throw(400, `Failed to delete plugin: ${errMsg}`)
ctx.throw(400, err.message)
}
ctx.message = `Plugin ${ctx.params.pluginId} deleted.`
ctx.status = 200
}
export async function storePlugin(
metadata: any,
directory: any,
export async function processUploadedPlugin(
plugin: FileType,
source?: PluginSource
) {
const db = getGlobalDB()
const version = metadata.package.version,
name = metadata.package.name,
description = metadata.package.description,
hash = metadata.schema.hash
// first open the tarball into tmp directory
const bucketPath = `${name}/`
const files = await uploadDirectory(
ObjectStoreBuckets.PLUGINS,
directory,
bucketPath
)
const jsFile = files.find((file: any) => file.name.endsWith(".js"))
if (!jsFile) {
throw new Error(`Plugin missing .js file.`)
}
// validate the JS for a datasource
if (metadata.schema.type === PluginType.DATASOURCE) {
const js = loadJSFile(directory, jsFile.name)
// TODO: this isn't safe - but we need full node environment
// in future we should do this in a thread for safety
try {
eval(js)
} catch (err: any) {
const message = err?.message ? err.message : JSON.stringify(err)
throw new Error(`JS invalid: ${message}`)
}
}
const jsFileName = jsFile.name
const pluginId = generatePluginID(name)
// overwrite existing docs entirely if they exist
let rev
try {
const existing = await db.get(pluginId)
rev = existing._rev
} catch (err) {
rev = undefined
}
let doc: Plugin = {
_id: pluginId,
_rev: rev,
...metadata,
name,
version,
hash,
description,
jsUrl: `${bucketPath}${jsFileName}`,
}
if (source) {
doc = {
...doc,
source,
}
}
const response = await db.put(doc)
await events.plugin.imported(doc)
ClientAppSocket.emit("plugin-update", { name, hash })
return {
...doc,
_rev: response.rev,
}
}
export async function processPlugin(plugin: FileType, source?: PluginSource) {
const { metadata, directory } = await fileUpload(plugin)
validate(metadata?.schema)
@ -211,5 +128,7 @@ export async function processPlugin(plugin: FileType, source?: PluginSource) {
throw new Error("Only component plugins are supported outside of self-host")
}
return await storePlugin(metadata, directory, source)
const doc = await plugins.storePlugin(metadata, directory, source)
ClientAppSocket.emit("plugins-update", { name: doc.name, hash: doc.hash })
return doc
}

View file

@ -4,10 +4,18 @@ export type Query = components["schemas"]["query"]
export type ExecuteQuery = components["schemas"]["executeQueryOutput"]
export type Application = components["schemas"]["applicationOutput"]["data"]
export type CreateApplicationParams = components["schemas"]["application"]
export type Table = components["schemas"]["tableOutput"]["data"]
export type CreateTableParams = components["schemas"]["table"]
export type Row = components["schemas"]["rowOutput"]["data"]
export type RowSearch = components["schemas"]["searchOutput"]
export type CreateRowParams = components["schemas"]["row"]
export type User = components["schemas"]["userOutput"]["data"]
export type CreateUserParams = components["schemas"]["user"]
export type SearchInputParams =
| components["schemas"]["nameSearch"]
| components["schemas"]["rowSearch"]

View file

@ -1,17 +1,16 @@
const {
getScreenParams,
generateScreenID,
getPluginParams,
DocumentType,
} = require("../../db/utils")
const { AccessController } = require("@budibase/backend-core/roles")
const { getAppDB } = require("@budibase/backend-core/context")
const { events } = require("@budibase/backend-core")
const { getGlobalDB } = require("@budibase/backend-core/tenancy")
const { updateAppPackage } = require("./application")
import { getScreenParams, generateScreenID, DocumentType } from "../../db/utils"
import {
events,
context,
tenancy,
db as dbCore,
roles,
} from "@budibase/backend-core"
import { updateAppPackage } from "./application"
import { Plugin, ScreenProps } from "@budibase/types"
exports.fetch = async ctx => {
const db = getAppDB()
exports.fetch = async (ctx: any) => {
const db = context.getAppDB()
const screens = (
await db.allDocs(
@ -19,16 +18,16 @@ exports.fetch = async ctx => {
include_docs: true,
})
)
).rows.map(element => element.doc)
).rows.map((el: any) => el.doc)
ctx.body = await new AccessController().checkScreensAccess(
ctx.body = await new roles.AccessController().checkScreensAccess(
screens,
ctx.user.role._id
)
}
exports.save = async ctx => {
const db = getAppDB()
exports.save = async (ctx: any) => {
const db = context.getAppDB()
let screen = ctx.request.body
let eventFn
@ -40,19 +39,19 @@ exports.save = async ctx => {
const response = await db.put(screen)
// Find any custom components being used
let pluginNames = []
let pluginNames: string[] = []
let pluginAdded = false
findPlugins(screen.props, pluginNames)
if (pluginNames.length) {
const globalDB = getGlobalDB()
const globalDB = tenancy.getGlobalDB()
const pluginsResponse = await globalDB.allDocs(
getPluginParams(null, {
dbCore.getPluginParams(null, {
include_docs: true,
})
)
const requiredPlugins = pluginsResponse.rows
.map(row => row.doc)
.filter(plugin => {
.map((row: any) => row.doc)
.filter((plugin: Plugin) => {
return (
plugin.schema.type === "component" &&
pluginNames.includes(`plugin/${plugin.name}`)
@ -63,8 +62,8 @@ exports.save = async ctx => {
const application = await db.get(DocumentType.APP_METADATA)
let usedPlugins = application.usedPlugins || []
requiredPlugins.forEach(plugin => {
if (!usedPlugins.find(x => x._id === plugin._id)) {
requiredPlugins.forEach((plugin: Plugin) => {
if (!usedPlugins.find((x: Plugin) => x._id === plugin._id)) {
pluginAdded = true
usedPlugins.push({
_id: plugin._id,
@ -93,8 +92,8 @@ exports.save = async ctx => {
}
}
exports.destroy = async ctx => {
const db = getAppDB()
exports.destroy = async (ctx: any) => {
const db = context.getAppDB()
const id = ctx.params.screenId
const screen = await db.get(id)
@ -107,7 +106,7 @@ exports.destroy = async ctx => {
ctx.status = 200
}
const findPlugins = (component, foundPlugins) => {
const findPlugins = (component: ScreenProps, foundPlugins: string[]) => {
if (!component) {
return
}

View file

@ -143,88 +143,7 @@ read.push(new Endpoint("get", "/tables/:tableId/rows/:rowId", controller.read))
* content:
* application/json:
* schema:
* type: object
* required:
* - query
* properties:
* query:
* type: object
* properties:
* allOr:
* type: boolean
* description: Specifies that a row should be returned if it satisfies
* any of the specified options, rather than requiring it to fulfill all
* the search parameters. This defaults to false, meaning AND logic will be used.
* string:
* type: object
* example:
* columnName1: value
* columnName2: value
* description: A map of field name to the string to search for,
* this will look for rows that have a value starting with the
* string value.
* additionalProperties:
* type: string
* description: The value to search for in the column.
* fuzzy:
* type: object
* description: A fuzzy search, only supported by internal tables.
* range:
* type: object
* description: Searches within a range, the format of this must be
* in the format of an object with a "low" and "high" property.
* example:
* columnName1: { low: 10, high: 20 }
* equal:
* type: object
* description: Searches for rows that have a column value that is
* exactly the value set.
* notEqual:
* type: object
* description: Searches for any row which does not contain the specified
* column value.
* empty:
* type: object
* description: Searches for rows which do not contain the specified column.
* The object should simply contain keys of the column names, these
* can map to any value.
* example:
* columnName1: ""
* notEmpty:
* type: object
* description: Searches for rows which have the specified column.
* oneOf:
* type: object
* description: Searches for rows which have a column value that is any
* of the specified values. The format of this must be columnName -> [value1, value2].
* paginate:
* type: boolean
* description: Enables pagination, by default this is disabled.
* bookmark:
* oneOf:
* - type: string
* - type: integer
* description: If retrieving another page, the bookmark from the previous request must be supplied.
* limit:
* type: integer
* description: The maximum number of rows to return, useful when paginating, for internal tables this
* will be limited to 1000, for SQL tables it will be 5000.
* sort:
* type: object
* description: A set of parameters describing the sort behaviour of the search.
* properties:
* order:
* type: string
* enum: [ascending, descending]
* description: The order of the sort, by default this is ascending.
* column:
* type: string
* description: The name of the column by which the rows will be sorted.
* type:
* type: string
* enum: [string, number]
* description: Defines whether the column should be treated as a string
* or as numbers when sorting.
* $ref: '#/components/schemas/rowSearch'
* responses:
* 200:
* description: The response will contain an array of rows that match the search parameters.

View file

@ -30,7 +30,11 @@ const fs = require("fs")
import redis from "./utilities/redis"
import * as migrations from "./migrations"
import { events, installation, tenancy } from "@budibase/backend-core"
import { createAdminUser, getChecklist } from "./utilities/workerRequests"
import {
createAdminUser,
generateApiKey,
getChecklist,
} from "./utilities/workerRequests"
import { watch } from "./watch"
import { initialise as initialiseWebsockets } from "./websocket"
@ -127,11 +131,16 @@ module.exports = server.listen(env.PORT || 0, async () => {
if (!checklist?.adminUser?.checked) {
try {
const tenantId = tenancy.getTenantId()
await createAdminUser(
const user = await createAdminUser(
env.BB_ADMIN_USER_EMAIL,
env.BB_ADMIN_USER_PASSWORD,
tenantId
)
// Need to set up an API key for automated integration tests
if (env.isTest()) {
await generateApiKey(user._id)
}
console.log(
"Admin account automatically created for",
env.BB_ADMIN_USER_EMAIL

View file

@ -42,7 +42,6 @@ const DocumentType = {
MEM_VIEW: "view",
USER_FLAG: "flag",
AUTOMATION_METADATA: "meta_au",
PLUGIN: "plg",
}
const InternalTables = {
@ -384,10 +383,3 @@ exports.getMultiIDParams = ids => {
include_docs: true,
}
}
/**
* Gets parameters for retrieving automations, this is a utility function for the getDocParams function.
*/
exports.getPluginParams = (pluginId = null, otherProps = {}) => {
return getDocParams(DocumentType.PLUGIN, pluginId, otherProps)
}

View file

@ -278,58 +278,7 @@ export interface paths {
};
requestBody: {
content: {
"application/json": {
query: {
/** @description Specifies that a row should be returned if it satisfies any of the specified options, rather than requiring it to fulfill all the search parameters. This defaults to false, meaning AND logic will be used. */
allOr?: boolean;
/**
* @description A map of field name to the string to search for, this will look for rows that have a value starting with the string value.
* @example [object Object]
*/
string?: { [key: string]: string };
/** @description A fuzzy search, only supported by internal tables. */
fuzzy?: { [key: string]: unknown };
/**
* @description Searches within a range, the format of this must be in the format of an object with a "low" and "high" property.
* @example [object Object]
*/
range?: { [key: string]: unknown };
/** @description Searches for rows that have a column value that is exactly the value set. */
equal?: { [key: string]: unknown };
/** @description Searches for any row which does not contain the specified column value. */
notEqual?: { [key: string]: unknown };
/**
* @description Searches for rows which do not contain the specified column. The object should simply contain keys of the column names, these can map to any value.
* @example [object Object]
*/
empty?: { [key: string]: unknown };
/** @description Searches for rows which have the specified column. */
notEmpty?: { [key: string]: unknown };
/** @description Searches for rows which have a column value that is any of the specified values. The format of this must be columnName -> [value1, value2]. */
oneOf?: { [key: string]: unknown };
};
/** @description Enables pagination, by default this is disabled. */
paginate?: boolean;
/** @description If retrieving another page, the bookmark from the previous request must be supplied. */
bookmark?: string | number;
/** @description The maximum number of rows to return, useful when paginating, for internal tables this will be limited to 1000, for SQL tables it will be 5000. */
limit?: number;
/** @description A set of parameters describing the sort behaviour of the search. */
sort?: {
/**
* @description The order of the sort, by default this is ascending.
* @enum {string}
*/
order?: "ascending" | "descending";
/** @description The name of the column by which the rows will be sorted. */
column?: string;
/**
* @description Defines whether the column should be treated as a string or as numbers when sorting.
* @enum {string}
*/
type?: "string" | "number";
};
};
"application/json": components["schemas"]["rowSearch"];
};
};
};
@ -1105,6 +1054,58 @@ export interface components {
_id: string;
}[];
};
rowSearch: {
query: {
/** @description Specifies that a row should be returned if it satisfies any of the specified options, rather than requiring it to fulfill all the search parameters. This defaults to false, meaning AND logic will be used. */
allOr?: boolean;
/**
* @description A map of field name to the string to search for, this will look for rows that have a value starting with the string value.
* @example [object Object]
*/
string?: { [key: string]: string };
/** @description A fuzzy search, only supported by internal tables. */
fuzzy?: { [key: string]: unknown };
/**
* @description Searches within a range, the format of this must be in the format of an object with a "low" and "high" property.
* @example [object Object]
*/
range?: { [key: string]: unknown };
/** @description Searches for rows that have a column value that is exactly the value set. */
equal?: { [key: string]: unknown };
/** @description Searches for any row which does not contain the specified column value. */
notEqual?: { [key: string]: unknown };
/**
* @description Searches for rows which do not contain the specified column. The object should simply contain keys of the column names, these can map to any value.
* @example [object Object]
*/
empty?: { [key: string]: unknown };
/** @description Searches for rows which have the specified column. */
notEmpty?: { [key: string]: unknown };
/** @description Searches for rows which have a column value that is any of the specified values. The format of this must be columnName -> [value1, value2]. */
oneOf?: { [key: string]: unknown };
};
/** @description Enables pagination, by default this is disabled. */
paginate?: boolean;
/** @description If retrieving another page, the bookmark from the previous request must be supplied. */
bookmark?: string | number;
/** @description The maximum number of rows to return, useful when paginating, for internal tables this will be limited to 1000, for SQL tables it will be 5000. */
limit?: number;
/** @description A set of parameters describing the sort behaviour of the search. */
sort?: {
/**
* @description The order of the sort, by default this is ascending.
* @enum {string}
*/
order?: "ascending" | "descending";
/** @description The name of the column by which the rows will be sorted. */
column?: string;
/**
* @description Defines whether the column should be treated as a string or as numbers when sorting.
* @enum {string}
*/
type?: "string" | "number";
};
};
nameSearch: {
/** @description The name to be used when searching - this will be used in a case insensitive starts with match. */
name: string;

View file

@ -112,13 +112,6 @@ exports.loadHandlebarsFile = path => {
return fs.readFileSync(path, "utf8")
}
/**
* Same as above just with a different name.
*/
exports.loadJSFile = (directory, name) => {
return fs.readFileSync(join(directory, name), "utf8")
}
/**
* When return a file from the API need to write the file to the system temporarily so we
* can create a read stream to send.
@ -412,6 +405,7 @@ exports.getDatasourcePlugin = async (name, url, hash) => {
return require(filename)
} else {
console.log(`Updating plugin: ${name}`)
delete require.cache[require.resolve(filename)]
fs.unlinkSync(filename)
}
}

View file

@ -153,3 +153,11 @@ exports.getChecklist = async () => {
)
return checkResponse(response, "get checklist")
}
exports.generateApiKey = async userId => {
const response = await fetch(
checkSlashesInUrl(env.WORKER_URL + "/api/global/self/api_key"),
request(null, { method: "POST", body: { userId } })
)
return checkResponse(response, "generate API key")
}

View file

@ -4,7 +4,7 @@ import chokidar from "chokidar"
import fs from "fs"
import { tenancy } from "@budibase/backend-core"
import { DEFAULT_TENANT_ID } from "@budibase/backend-core/constants"
import { processPlugin } from "./api/controllers/plugin"
import { processUploadedPlugin } from "./api/controllers/plugin"
export function watch() {
const watchPath = path.join(env.PLUGINS_DIR, "./**/*.tar.gz")
@ -28,7 +28,7 @@ export function watch() {
const split = path.split("/")
const name = split[split.length - 1]
console.log("Importing plugin:", path)
await processPlugin({ name, path })
await processUploadedPlugin({ name, path })
} catch (err: any) {
const message = err?.message ? err?.message : err
console.error("Failed to import plugin:", message)

View file

@ -1094,12 +1094,12 @@
resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39"
integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==
"@budibase/backend-core@1.3.19-alpha.0":
version "1.3.19-alpha.0"
resolved "https://registry.yarnpkg.com/@budibase/backend-core/-/backend-core-1.3.19-alpha.0.tgz#e9785994485b16eb0c2f082fe5803c690199a397"
integrity sha512-avYT4DLDG3GMDNOKHgbNRJIqzHimdhSxjiifbVnMfizLjWL94eAqVgRA4kOiWkEuuH6Wlc5dmI359dJvsWKErw==
"@budibase/backend-core@1.4.2":
version "1.4.2"
resolved "https://registry.yarnpkg.com/@budibase/backend-core/-/backend-core-1.4.2.tgz#9c0a40f5c4a1693fd24048f6f130547b0f08e76e"
integrity sha512-htXj7V5mLdf+IqATBlgjABknbT6n/5qRwArlsawYYZzDRTod3rK+W7dIGHDr0JOPJJ8272SuAcJbF80gd65Z+w==
dependencies:
"@budibase/types" "1.3.19-alpha.0"
"@budibase/types" "^1.4.2"
"@shopify/jest-koa-mocks" "5.0.1"
"@techpass/passport-openidconnect" "0.3.2"
aws-sdk "2.1030.0"
@ -1180,13 +1180,13 @@
svelte-flatpickr "^3.2.3"
svelte-portal "^1.0.0"
"@budibase/pro@1.3.19-alpha.0":
version "1.3.19-alpha.0"
resolved "https://registry.yarnpkg.com/@budibase/pro/-/pro-1.3.19-alpha.0.tgz#26a3a1834add9f2b87d1bb9a04708c72cd2eea94"
integrity sha512-7g8LlKsrCA0iqsghCMppRFNUb+ytdsK4ZageOgzkFhtNxDT5nqYeERDN+fFd4XhqOCsiUebC1bhkRFHapKfLZA==
"@budibase/pro@1.4.2":
version "1.4.2"
resolved "https://registry.yarnpkg.com/@budibase/pro/-/pro-1.4.2.tgz#fdcb50f1f4da59dab2c7b0095bb37b684bf50c78"
integrity sha512-yj3nQ0dn+qlSxl3LEnzaizhBPQxKZqcmgi1GdLMh5bwd0xXquD2f7pXMSY0/ygJ7+lZtTlqqu0EG4LNJh/aeRA==
dependencies:
"@budibase/backend-core" "1.3.19-alpha.0"
"@budibase/types" "1.3.19-alpha.0"
"@budibase/backend-core" "1.4.2"
"@budibase/types" "1.4.2"
"@koa/router" "8.0.8"
joi "17.6.0"
node-fetch "^2.6.1"
@ -1209,10 +1209,10 @@
svelte-apexcharts "^1.0.2"
svelte-flatpickr "^3.1.0"
"@budibase/types@1.3.19-alpha.0":
version "1.3.19-alpha.0"
resolved "https://registry.yarnpkg.com/@budibase/types/-/types-1.3.19-alpha.0.tgz#402801d1ea5b2e2b82a45a37f06bab28eee8ccb6"
integrity sha512-SKp5cKMar+SUryBwBcdP3zcclCe+48jjMseCNNmjG7ZEkrz0SPWsFE6FJTFfG/PbIgq8DPkvrrCXPY7ZlExW/g==
"@budibase/types@1.4.2", "@budibase/types@^1.4.2":
version "1.4.2"
resolved "https://registry.yarnpkg.com/@budibase/types/-/types-1.4.2.tgz#5f5f7c5dabc5c3b1cca8325693f4257233fd2fa4"
integrity sha512-YeNQ7HzYiltn/YNZNdrX0lkxLJ6G9fTxfiQEPiDeFJnhZLZWJf8PliopGTj3t2IALFzIK9j1PuONexlj2yfcOw==
"@bull-board/api@3.7.0":
version "3.7.0"

View file

@ -1,6 +1,6 @@
{
"name": "@budibase/string-templates",
"version": "1.3.19-alpha.0",
"version": "1.4.2",
"description": "Handlebars wrapper for Budibase templating.",
"main": "src/index.cjs",
"module": "dist/bundle.mjs",

View file

@ -1,6 +1,6 @@
{
"name": "@budibase/types",
"version": "1.3.19-alpha.0",
"version": "1.4.2",
"description": "Budibase types",
"main": "dist/index.js",
"types": "dist/index.d.ts",
@ -13,8 +13,8 @@
},
"jest": {},
"devDependencies": {
"@types/node": "14.18.20",
"@types/koa": "2.13.4",
"@types/node": "14.18.20",
"rimraf": "3.0.2",
"typescript": "4.7.3"
}

View file

@ -1,5 +1,17 @@
import { Document } from "../document"
export interface ScreenProps extends Document {
_instanceName: string
_styles: { [key: string]: any }
_component: string
_children: ScreenProps[]
size?: string
gap?: string
direction?: string
vAlign?: string
hAlign?: string
}
export interface Screen extends Document {
layoutId?: string
showNavigation?: boolean
@ -9,4 +21,5 @@ export interface Screen extends Document {
roleId: string
homeScreen?: boolean
}
props: ScreenProps
}

View file

@ -23,6 +23,7 @@ export interface Plugin extends Document {
jsUrl?: string
source: PluginSource
package: { [key: string]: any }
hash: string
schema: {
type: PluginType
[key: string]: any

View file

@ -1,7 +1,7 @@
{
"name": "@budibase/worker",
"email": "hi@budibase.com",
"version": "1.3.19-alpha.0",
"version": "1.4.2",
"description": "Budibase background service",
"main": "src/index.ts",
"repository": {
@ -36,10 +36,10 @@
"author": "Budibase",
"license": "GPL-3.0",
"dependencies": {
"@budibase/backend-core": "1.3.19-alpha.0",
"@budibase/pro": "1.3.19-alpha.0",
"@budibase/string-templates": "1.3.19-alpha.0",
"@budibase/types": "1.3.19-alpha.0",
"@budibase/backend-core": "^1.4.2",
"@budibase/pro": "1.4.2",
"@budibase/string-templates": "^1.4.2",
"@budibase/types": "^1.4.2",
"@koa/router": "8.0.8",
"@sentry/node": "6.17.7",
"@techpass/passport-openidconnect": "0.3.2",

View file

@ -16,6 +16,11 @@ const { newid } = require("@budibase/backend-core/utils")
const { users } = require("../../../sdk")
const { Cookies } = require("@budibase/backend-core/constants")
const { events, featureFlags } = require("@budibase/backend-core")
const env = require("../../../environment")
function newTestApiKey() {
return env.ENCRYPTED_TEST_PUBLIC_API_KEY
}
function newApiKey() {
return encrypt(`${getTenantId()}${SEPARATOR}${newid()}`)
@ -29,15 +34,25 @@ function cleanupDevInfo(info) {
}
exports.generateAPIKey = async ctx => {
let userId
let apiKey
if (env.isTest() && ctx.request.body.userId) {
userId = ctx.request.body.userId
apiKey = newTestApiKey()
} else {
userId = ctx.user._id
apiKey = newApiKey()
}
const db = getGlobalDB()
const id = generateDevInfoID(ctx.user._id)
const id = generateDevInfoID(userId)
let devInfo
try {
devInfo = await db.get(id)
} catch (err) {
devInfo = { _id: id, userId: ctx.user._id }
devInfo = { _id: id, userId }
}
devInfo.apiKey = await newApiKey()
devInfo.apiKey = await apiKey
await db.put(devInfo)
ctx.body = cleanupDevInfo(devInfo)
}
@ -80,16 +95,15 @@ const addSessionAttributesToUser = ctx => {
ctx.body.csrfToken = ctx.user.csrfToken
}
/**
* Remove the attributes that are session based from the current user,
* so that stale values are not written to the db
*/
const removeSessionAttributesFromUser = ctx => {
delete ctx.request.body.csrfToken
delete ctx.request.body.account
delete ctx.request.body.accountPortalAccess
delete ctx.request.body.budibaseAccess
delete ctx.request.body.license
const sanitiseUserUpdate = ctx => {
const allowed = ["firstName", "lastName", "password", "forceResetPassword"]
const resp = {}
for (let [key, value] of Object.entries(ctx.request.body)) {
if (allowed.includes(key)) {
resp[key] = value
}
}
return resp
}
exports.getSelf = async ctx => {
@ -117,10 +131,12 @@ exports.updateSelf = async ctx => {
const db = getGlobalDB()
const user = await db.get(ctx.user._id)
let passwordChange = false
if (ctx.request.body.password) {
const userUpdateObj = sanitiseUserUpdate(ctx)
if (userUpdateObj.password) {
// changing password
passwordChange = true
ctx.request.body.password = await hash(ctx.request.body.password)
userUpdateObj.password = await hash(userUpdateObj.password)
// Log all other sessions out apart from the current one
await platformLogout({
ctx,
@ -128,14 +144,10 @@ exports.updateSelf = async ctx => {
keepActiveSession: true,
})
}
// don't allow sending up an ID/Rev, always use the existing one
delete ctx.request.body._id
delete ctx.request.body._rev
removeSessionAttributesFromUser(ctx)
const response = await db.put({
...user,
...ctx.request.body,
...userUpdateObj,
})
await userCache.invalidateUser(user._id)
ctx.body = {

View file

@ -14,7 +14,6 @@ import {
errors,
events,
tenancy,
users as usersCore,
} from "@budibase/backend-core"
import { checkAnyUserExists } from "../../../utilities/users"
import { groups as groupUtils } from "@budibase/pro"
@ -148,9 +147,7 @@ export const bulkDelete = async (ctx: any) => {
}
try {
let response = await users.bulkDelete(userIds)
ctx.body = response
ctx.body = await users.bulkDelete(userIds)
} catch (err) {
ctx.throw(err)
}

View file

@ -63,6 +63,7 @@ const env = {
// other
CHECKLIST_CACHE_TTL: parseIntSafe(process.env.CHECKLIST_CACHE_TTL) || 3600,
SESSION_UPDATE_PERIOD: process.env.SESSION_UPDATE_PERIOD,
ENCRYPTED_TEST_PUBLIC_API_KEY: process.env.ENCRYPTED_TEST_PUBLIC_API_KEY,
_set(key: any, value: any) {
process.env[key] = value
module.exports[key] = value

View file

@ -291,12 +291,12 @@
resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39"
integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==
"@budibase/backend-core@1.3.19-alpha.0":
version "1.3.19-alpha.0"
resolved "https://registry.yarnpkg.com/@budibase/backend-core/-/backend-core-1.3.19-alpha.0.tgz#e9785994485b16eb0c2f082fe5803c690199a397"
integrity sha512-avYT4DLDG3GMDNOKHgbNRJIqzHimdhSxjiifbVnMfizLjWL94eAqVgRA4kOiWkEuuH6Wlc5dmI359dJvsWKErw==
"@budibase/backend-core@1.4.2":
version "1.4.2"
resolved "https://registry.yarnpkg.com/@budibase/backend-core/-/backend-core-1.4.2.tgz#9c0a40f5c4a1693fd24048f6f130547b0f08e76e"
integrity sha512-htXj7V5mLdf+IqATBlgjABknbT6n/5qRwArlsawYYZzDRTod3rK+W7dIGHDr0JOPJJ8272SuAcJbF80gd65Z+w==
dependencies:
"@budibase/types" "1.3.19-alpha.0"
"@budibase/types" "^1.4.2"
"@shopify/jest-koa-mocks" "5.0.1"
"@techpass/passport-openidconnect" "0.3.2"
aws-sdk "2.1030.0"
@ -327,21 +327,21 @@
uuid "8.3.2"
zlib "1.0.5"
"@budibase/pro@1.3.19-alpha.0":
version "1.3.19-alpha.0"
resolved "https://registry.yarnpkg.com/@budibase/pro/-/pro-1.3.19-alpha.0.tgz#26a3a1834add9f2b87d1bb9a04708c72cd2eea94"
integrity sha512-7g8LlKsrCA0iqsghCMppRFNUb+ytdsK4ZageOgzkFhtNxDT5nqYeERDN+fFd4XhqOCsiUebC1bhkRFHapKfLZA==
"@budibase/pro@1.4.2":
version "1.4.2"
resolved "https://registry.yarnpkg.com/@budibase/pro/-/pro-1.4.2.tgz#fdcb50f1f4da59dab2c7b0095bb37b684bf50c78"
integrity sha512-yj3nQ0dn+qlSxl3LEnzaizhBPQxKZqcmgi1GdLMh5bwd0xXquD2f7pXMSY0/ygJ7+lZtTlqqu0EG4LNJh/aeRA==
dependencies:
"@budibase/backend-core" "1.3.19-alpha.0"
"@budibase/types" "1.3.19-alpha.0"
"@budibase/backend-core" "1.4.2"
"@budibase/types" "1.4.2"
"@koa/router" "8.0.8"
joi "17.6.0"
node-fetch "^2.6.1"
"@budibase/types@1.3.19-alpha.0":
version "1.3.19-alpha.0"
resolved "https://registry.yarnpkg.com/@budibase/types/-/types-1.3.19-alpha.0.tgz#402801d1ea5b2e2b82a45a37f06bab28eee8ccb6"
integrity sha512-SKp5cKMar+SUryBwBcdP3zcclCe+48jjMseCNNmjG7ZEkrz0SPWsFE6FJTFfG/PbIgq8DPkvrrCXPY7ZlExW/g==
"@budibase/types@1.4.2", "@budibase/types@^1.4.2":
version "1.4.2"
resolved "https://registry.yarnpkg.com/@budibase/types/-/types-1.4.2.tgz#5f5f7c5dabc5c3b1cca8325693f4257233fd2fa4"
integrity sha512-YeNQ7HzYiltn/YNZNdrX0lkxLJ6G9fTxfiQEPiDeFJnhZLZWJf8PliopGTj3t2IALFzIK9j1PuONexlj2yfcOw==
"@cspotcode/source-map-consumer@0.8.0":
version "0.8.0"

3
qa-core/.env Normal file
View file

@ -0,0 +1,3 @@
BB_ADMIN_USER_EMAIL=qa@budibase.com
BB_ADMIN_USER_PASSWORD=budibase
ENCRYPTED_TEST_PUBLIC_API_KEY=a65722f06bee5caeadc5d7ca2f543a43-d610e627344210c643bb726f

4
qa-core/.gitignore vendored Normal file
View file

@ -0,0 +1,4 @@
node_modules/
.env
watchtower-hook.json
dist/

52
qa-core/package.json Normal file
View file

@ -0,0 +1,52 @@
{
"name": "@budibase/qa-core",
"email": "hi@budibase.com",
"version": "0.0.1",
"main": "index.js",
"description": "Budibase Integration Test Suite",
"repository": {
"type": "git",
"url": "https://github.com/Budibase/budibase.git"
},
"scripts": {
"test": "jest --runInBand",
"test:watch": "jest --watch",
"test:debug": "DEBUG=1 jest",
"api:server:setup": "env-cmd ts-node ../packages/builder/cypress/ts/setup.ts",
"api:server:setup:ci": "env-cmd node ../packages/builder/cypress/setup.js",
"api:test:ci": "start-server-and-test api:server:setup:ci http://localhost:4100/builder test",
"api:test": "start-server-and-test api:server:setup http://localhost:4100/builder test"
},
"jest": {
"preset": "ts-jest",
"testEnvironment": "node",
"moduleNameMapper": {
"@budibase/types": "<rootDir>/../packages/types/src",
"@budibase/server": "<rootDir>/../packages/server/src"
},
"setupFiles": [
"./scripts/jestSetup.js"
],
"setupFilesAfterEnv": [
"./src/jest.extends.ts"
]
},
"devDependencies": {
"@budibase/types": "1.3.4",
"@types/jest": "29.0.0",
"@types/node-fetch": "2.6.2",
"chance": "1.1.8",
"env-cmd": "^10.1.0",
"jest": "28.0.2",
"prettier": "2.7.1",
"start-server-and-test": "1.14.0",
"timekeeper": "2.2.0",
"ts-jest": "28.0.8",
"ts-node": "10.9.1",
"tsconfig-paths": "4.1.0",
"typescript": "4.7.3"
},
"dependencies": {
"node-fetch": "2"
}
}

View file

@ -0,0 +1,17 @@
const env = require("../src/environment")
env._set("BUDIBASE_SERVER_URL", "http://localhost:4100")
env._set(
"BUDIBASE_PUBLIC_API_KEY",
"a65722f06bee5caeadc5d7ca2f543a43-d610e627344210c643bb726f"
)
// mock all dates to 2020-01-01T00:00:00.000Z
// use tk.reset() to use real dates in individual tests
const MOCK_DATE = new Date("2020-01-01T00:00:00.000Z")
const tk = require("timekeeper")
tk.freeze(MOCK_DATE)
if (!process.env.DEBUG) {
global.console.log = jest.fn() // console.log are ignored in tests
}

View file

@ -0,0 +1,58 @@
import env from "../../../environment"
import fetch, { HeadersInit } from "node-fetch"
type APIMethod = "GET" | "POST" | "PUT" | "PATCH" | "DELETE"
interface ApiOptions {
method?: APIMethod
body?: object
headers?: HeadersInit | undefined
}
class PublicAPIClient {
host: string
apiKey: string
appId?: string
constructor(appId?: string) {
if (!env.BUDIBASE_PUBLIC_API_KEY || !env.BUDIBASE_SERVER_URL) {
throw new Error(
"Must set BUDIBASE_PUBLIC_API_KEY and BUDIBASE_SERVER_URL env vars"
)
}
this.host = `${env.BUDIBASE_SERVER_URL}/api/public/v1`
this.apiKey = env.BUDIBASE_PUBLIC_API_KEY
this.appId = appId
}
apiCall =
(method: APIMethod) =>
async (url = "", options: ApiOptions = {}) => {
const requestOptions = {
method,
body: JSON.stringify(options.body),
headers: {
"x-budibase-api-key": this.apiKey,
"x-budibase-app-id": this.appId,
"Content-Type": "application/json",
Accept: "application/json",
...options.headers,
},
}
// @ts-ignore
const response = await fetch(`${this.host}${url}`, requestOptions)
if (response.status !== 200) {
console.error(response)
}
return response
}
post = this.apiCall("POST")
get = this.apiCall("GET")
patch = this.apiCall("PATCH")
del = this.apiCall("DELETE")
put = this.apiCall("PUT")
}
export default PublicAPIClient

View file

@ -0,0 +1,49 @@
import PublicAPIClient from "./PublicAPIClient"
import {
Application,
SearchInputParams,
CreateApplicationParams,
} from "@budibase/server/api/controllers/public/mapping/types"
import { Response } from "node-fetch"
import generateApp from "../fixtures/applications"
export default class AppApi {
api: PublicAPIClient
constructor(apiClient: PublicAPIClient) {
this.api = apiClient
}
async seed(): Promise<[Response, Application]> {
return this.create(generateApp())
}
async create(
body: CreateApplicationParams
): Promise<[Response, Application]> {
const response = await this.api.post(`/applications`, { body })
const json = await response.json()
return [response, json.data]
}
async read(id: string): Promise<[Response, Application]> {
const response = await this.api.get(`/applications/${id}`)
const json = await response.json()
return [response, json.data]
}
async search(body: SearchInputParams): Promise<[Response, [Application]]> {
const response = await this.api.post(`/applications/search`, { body })
const json = await response.json()
return [response, json.data]
}
async update(
id: string,
body: Application
): Promise<[Response, Application]> {
const response = await this.api.put(`/applications/${id}`, { body })
const json = await response.json()
return [response, json.data]
}
}

View file

@ -0,0 +1,3 @@
const Chance = require("chance")
export default new Chance()

View file

@ -0,0 +1,27 @@
import PublicAPIClient from "./PublicAPIClient"
import ApplicationApi from "./applications"
import TableApi from "./tables"
import UserApi from "./users"
import RowApi from "./rows"
export default class TestConfiguration<T> {
applications: ApplicationApi
users: UserApi
tables: TableApi
rows: RowApi
context: T
constructor(apiClient: PublicAPIClient) {
this.applications = new ApplicationApi(apiClient)
this.users = new UserApi(apiClient)
this.tables = new TableApi(apiClient)
this.rows = new RowApi(apiClient)
this.context = <T>{}
}
async beforeAll() {}
async afterAll() {
this.context = <T>{}
}
}

View file

@ -0,0 +1,53 @@
import PublicAPIClient from "./PublicAPIClient"
import {
CreateRowParams,
Row,
SearchInputParams,
} from "@budibase/server/api/controllers/public/mapping/types"
import { HeadersInit, Response } from "node-fetch"
import { generateRow } from "../fixtures/tables"
export default class RowApi {
api: PublicAPIClient
headers?: HeadersInit
tableId?: string
constructor(apiClient: PublicAPIClient) {
this.api = apiClient
}
async seed(tableId: string) {
return this.create(generateRow({ tableId }))
}
async create(body: CreateRowParams): Promise<[Response, Row]> {
const response = await this.api.post(`/tables/${this.tableId}/rows`, {
body,
})
const json = await response.json()
return [response, json.data]
}
async read(id: string): Promise<[Response, Row]> {
const response = await this.api.get(`/tables/${this.tableId}/rows/${id}`)
const json = await response.json()
return [response, json.data]
}
async search(body: SearchInputParams): Promise<[Response, [Row]]> {
const response = await this.api.post(
`/tables/${this.tableId}/rows/search`,
{ body }
)
const json = await response.json()
return [response, json.data]
}
async update(id: string, body: Row): Promise<[Response, Row]> {
const response = await this.api.put(`/tables/${this.tableId}/rows/${id}`, {
body,
})
const json = await response.json()
return [response, json.data]
}
}

View file

@ -0,0 +1,47 @@
import PublicAPIClient from "./PublicAPIClient"
import {
Table,
SearchInputParams,
CreateTableParams,
} from "@budibase/server/api/controllers/public/mapping/types"
import { HeadersInit, Response } from "node-fetch"
import { generateTable } from "../fixtures/tables"
export default class TableApi {
api: PublicAPIClient
headers?: HeadersInit
constructor(apiClient: PublicAPIClient) {
this.api = apiClient
}
async seed() {
return this.create(generateTable())
}
async create(body: CreateTableParams): Promise<[Response, Table]> {
const response = await this.api.post(`/tables`, {
body,
})
const json = await response.json()
return [response, json.data]
}
async read(id: string): Promise<[Response, Table]> {
const response = await this.api.get(`/tables/${id}`)
const json = await response.json()
return [response, json.data]
}
async search(body: SearchInputParams): Promise<[Response, [Table]]> {
const response = await this.api.post(`/tables/search`, { body })
const json = await response.json()
return [response, json.data]
}
async update(id: string, body: Table): Promise<[Response, Table]> {
const response = await this.api.put(`/tables/${id}`, { body })
const json = await response.json()
return [response, json.data]
}
}

View file

@ -0,0 +1,44 @@
import PublicAPIClient from "./PublicAPIClient"
import {
CreateUserParams,
SearchInputParams,
User,
} from "@budibase/server/api/controllers/public/mapping/types"
import { Response } from "node-fetch"
import generateUser from "../fixtures/users"
export default class UserApi {
api: PublicAPIClient
constructor(apiClient: PublicAPIClient) {
this.api = apiClient
}
async seed() {
return this.create(generateUser())
}
async create(body: CreateUserParams): Promise<[Response, User]> {
const response = await this.api.post(`/users`, { body })
const json = await response.json()
return [response, json.data]
}
async read(id: string): Promise<[Response, User]> {
const response = await this.api.get(`/users/${id}`)
const json = await response.json()
return [response, json.data]
}
async search(body: SearchInputParams): Promise<[Response, [User]]> {
const response = await this.api.post(`/users/search`, { body })
const json = await response.json()
return [response, json.data]
}
async update(id: string, body: User): Promise<[Response, User]> {
const response = await this.api.put(`/users/${id}`, { body })
const json = await response.json()
return [response, json.data]
}
}

View file

@ -0,0 +1,15 @@
import generator from "../TestConfiguration/generator"
import {
Application,
CreateApplicationParams,
} from "@budibase/server/api/controllers/public/mapping/types"
const generate = (
overrides: Partial<Application> = {}
): CreateApplicationParams => ({
name: generator.word(),
url: `/${generator.word()}`,
...overrides,
})
export default generate

View file

@ -0,0 +1,60 @@
import {
CreateRowParams,
CreateTableParams,
Row,
Table,
} from "@budibase/server/api/controllers/public/mapping/types"
import generator from "../TestConfiguration/generator"
export const generateTable = (
overrides: Partial<Table> = {}
): CreateTableParams => ({
name: generator.word(),
primaryDisplay: "testColumn",
schema: {
"Auto ID": {
autocolumn: true,
name: "Auto ID",
type: "number",
},
"Created At": {
autocolumn: true,
name: "Created At",
type: "datetime",
},
"Created By": {
autocolumn: true,
fieldName: generator.word(),
name: "Created By",
relationshipType: "many-to-many",
tableId: "ta_users",
type: "link",
},
testColumn: {
name: "testColumn",
type: "string",
},
"Updated At": {
autocolumn: true,
name: "Updated At",
type: "datetime",
},
"Updated By": {
autocolumn: true,
fieldName: generator.word(),
name: "Updated By",
relationshipType: "many-to-many",
tableId: "ta_users",
type: "link",
},
},
...overrides,
})
export const generateRow = (overrides: Partial<Row> = {}): CreateRowParams => ({
type: "row",
tableId: "seed_table",
testColumn: generator.string({ length: 32, alpha: true, numeric: true }),
relationship: [generator.string({ length: 32, alpha: true, numeric: true })],
...overrides,
})

View file

@ -0,0 +1,25 @@
import {
CreateUserParams,
User,
} from "@budibase/server/api/controllers/public/mapping/types"
import generator from "../TestConfiguration/generator"
const generate = (overrides: Partial<User> = {}): CreateUserParams => ({
email: generator.email(),
roles: {
[generator.string({ length: 32, alpha: true, numeric: true })]:
generator.word(),
},
password: generator.word(),
status: "active",
forceResetPassword: generator.bool(),
builder: {
global: generator.bool(),
},
admin: {
global: generator.bool(),
},
...overrides,
})
export default generate

View file

@ -0,0 +1,10 @@
const env = {
BUDIBASE_SERVER_URL: process.env.BUDIBASE_SERVER_URL,
BUDIBASE_PUBLIC_API_KEY: process.env.BUDIBASE_PUBLIC_API_KEY,
_set(key: any, value: any) {
process.env[key] = value
module.exports[key] = value
},
}
export = env

View file

@ -0,0 +1,23 @@
import { Response } from "node-fetch"
// boilerplate to allow TS updates to the global scope
export {}
declare global {
namespace jest {
interface Matchers<R> {
toHaveStatusCode(code: number): R
}
}
}
// Expect extensions
expect.extend({
toHaveStatusCode(received: Response, code: number) {
const pass = received.status === code
return {
message: () => `expected ${received.status} to match status code ${code}`,
pass,
}
},
})

View file

@ -0,0 +1,50 @@
import TestConfiguration from "../../../config/public-api/TestConfiguration"
import PublicAPIClient from "../../../config/public-api/TestConfiguration/PublicAPIClient"
import generateApp from "../../../config/public-api/fixtures/applications"
import { Application } from "@budibase/server/api/controllers/public/mapping/types"
describe("Public API - /applications endpoints", () => {
const api = new PublicAPIClient()
const config = new TestConfiguration<Application>(api)
beforeAll(async () => {
await config.beforeAll()
const [response, app] = await config.applications.seed()
config.context = app
})
afterAll(async () => {
await config.afterAll()
})
it("POST - Create an application", async () => {
const [response, app] = await config.applications.create(generateApp())
expect(response).toHaveStatusCode(200)
expect(app._id).toBeDefined()
})
it("POST - Search applications", async () => {
const [response, apps] = await config.applications.search({
name: config.context.name,
})
expect(response).toHaveStatusCode(200)
expect(apps[0]).toEqual(config.context)
})
it("GET - Retrieve an application", async () => {
const [response, app] = await config.applications.read(config.context._id)
expect(response).toHaveStatusCode(200)
expect(app).toEqual(config.context)
})
it("PUT - update an application", async () => {
config.context.name = "UpdatedName"
const [response, app] = await config.applications.update(
config.context._id,
config.context
)
expect(response).toHaveStatusCode(200)
expect(app.updatedAt).not.toEqual(config.context.updatedAt)
expect(app.name).toEqual(config.context.name)
})
})

View file

@ -0,0 +1,65 @@
import { Row } from "@budibase/server/api/controllers/public/mapping/types"
import { generateRow } from "../../../config/public-api/fixtures/tables"
import TestConfiguration from "../../../config/public-api/TestConfiguration"
import PublicAPIClient from "../../../config/public-api/TestConfiguration/PublicAPIClient"
describe("Public API - /rows endpoints", () => {
let api = new PublicAPIClient()
const config = new TestConfiguration<Row>(api)
beforeAll(async () => {
await config.beforeAll()
const [aResp, app] = await config.applications.seed()
config.tables.api.appId = app._id
config.rows.api.appId = app._id
const [tResp, table] = await config.tables.seed()
config.rows.tableId = table._id
const [rResp, row] = await config.rows.seed(table._id)
config.context = row
})
afterAll(async () => {
await config.afterAll()
})
it("POST - Create a row", async () => {
const [response, row] = await config.rows.create(generateRow())
expect(response).toHaveStatusCode(200)
expect(row._id).toBeDefined()
})
it("POST - Search rows", async () => {
const [response, rows] = await config.rows.search({
query: {
string: {
testColumn: config.context.testColumn as string,
},
},
})
expect(response).toHaveStatusCode(200)
expect(rows[0]._id).toEqual(config.context._id)
expect(rows[0].tableId).toEqual(config.context.tableId)
expect(rows[0].testColumn).toEqual(config.context.testColumn)
})
it("GET - Retrieve a row", async () => {
const [response, row] = await config.rows.read(config.context._id)
expect(response).toHaveStatusCode(200)
expect(row._id).toEqual(config.context._id)
expect(row.tableId).toEqual(config.context.tableId)
})
it("PUT - update a row", async () => {
config.context.testColumn = "UpdatedName"
const [response, row] = await config.rows.update(
config.context._id,
config.context
)
expect(response).toHaveStatusCode(200)
expect(row.testColumn).toEqual(config.context.testColumn)
})
})

View file

@ -0,0 +1,52 @@
import { Table } from "@budibase/server/api/controllers/public/mapping/types"
import { generateTable } from "../../../config/public-api/fixtures/tables"
import TestConfiguration from "../../../config/public-api/TestConfiguration"
import PublicAPIClient from "../../../config/public-api/TestConfiguration/PublicAPIClient"
describe("Public API - /tables endpoints", () => {
let api = new PublicAPIClient()
const config = new TestConfiguration<Table>(api)
beforeAll(async () => {
await config.beforeAll()
const [appResp, app] = await config.applications.seed()
config.tables.api.appId = app._id
const [tableResp, table] = await config.tables.seed()
config.context = table
})
afterAll(async () => {
await config.afterAll()
})
it("POST - Create a table", async () => {
const [response, table] = await config.tables.create(generateTable())
expect(response).toHaveStatusCode(200)
expect(table._id).toBeDefined()
})
it("POST - Search tables", async () => {
const [response, tables] = await config.tables.search({
name: config.context.name,
})
expect(response).toHaveStatusCode(200)
expect(tables[0]).toEqual(config.context)
})
it("GET - Retrieve a table", async () => {
const [response, table] = await config.tables.read(config.context._id)
expect(response).toHaveStatusCode(200)
expect(table).toEqual(config.context)
})
it("PUT - update a table", async () => {
config.context.name = "updatedName"
const [response, table] = await config.tables.update(
config.context._id,
config.context
)
expect(response).toHaveStatusCode(200)
expect(table).toEqual(config.context)
})
})

View file

@ -0,0 +1,49 @@
import TestConfiguration from "../../../config/public-api/TestConfiguration"
import PublicAPIClient from "../../../config/public-api/TestConfiguration/PublicAPIClient"
import generateUser from "../../../config/public-api/fixtures/users"
import { User } from "@budibase/server/api/controllers/public/mapping/types"
describe("Public API - /users endpoints", () => {
const api = new PublicAPIClient()
const config = new TestConfiguration<User>(api)
beforeAll(async () => {
await config.beforeAll()
const [_, user] = await config.users.seed()
config.context = user
})
afterAll(async () => {
await config.afterAll()
})
it("POST - Create a user", async () => {
const [response, user] = await config.users.create(generateUser())
expect(response).toHaveStatusCode(200)
expect(user._id).toBeDefined()
})
it("POST - Search users", async () => {
const [response, users] = await config.users.search({
name: config.context.email,
})
expect(response).toHaveStatusCode(200)
expect(users[0]).toEqual(config.context)
})
it("GET - Retrieve a user", async () => {
const [response, user] = await config.users.read(config.context._id)
expect(response).toHaveStatusCode(200)
expect(user).toEqual(config.context)
})
it("PUT - update a user", async () => {
config.context.firstName = "Updated First Name"
const [response, user] = await config.users.update(
config.context._id,
config.context
)
expect(response).toHaveStatusCode(200)
expect(user).toEqual(config.context)
})
})

34
qa-core/tsconfig.json Normal file
View file

@ -0,0 +1,34 @@
{
"compilerOptions": {
"target": "es6",
"module": "commonjs",
"lib": ["es2020"],
"allowJs": true,
"strict": true,
"noImplicitAny": true,
"esModuleInterop": true,
"resolveJsonModule": true,
"incremental": true,
"types": ["node", "jest"],
"outDir": "dist",
"skipLibCheck": true,
"paths": {
"@budibase/types": ["../packages/types/src"],
"@budibase/server/*": ["../packages/server/src/*"],
}
},
"ts-node": {
"require": ["tsconfig-paths/register"]
},
"references": [
{ "path": "../packages/types" },
],
"include": [
"src/**/*",
"package.json"
],
"exclude": [
"node_modules",
"dist"
]
}

2670
qa-core/yarn.lock Normal file

File diff suppressed because it is too large Load diff

683
yarn.lock

File diff suppressed because it is too large Load diff