Merge branch 'master' of github.com:Budibase/budibase into feature/settings-modal

This commit is contained in:
kevmodrome 2020-06-22 17:55:40 +02:00
commit dd5ca67dcd
117 changed files with 1380 additions and 477 deletions

View File

@ -166,6 +166,18 @@ rm -rf ~/.budibase
``` ```
Follow from **Step 3. Install and Build** in the setup guide above. You should have a fresh Budibase installation. Follow from **Step 3. Install and Build** in the setup guide above. You should have a fresh Budibase installation.
### Running tests
#### End-to-end Tests
Budibase uses Cypress to run a number of E2E tests. To run the tests execute the following command in the root folder:
```
yarn test:e2e
```
Or if you are in the builder you can run `yarn cy:test`.
### Other Useful Information ### Other Useful Information

View File

@ -5,6 +5,7 @@
"@rollup/plugin-json": "^4.0.2", "@rollup/plugin-json": "^4.0.2",
"babel-eslint": "^10.0.3", "babel-eslint": "^10.0.3",
"eslint": "^6.8.0", "eslint": "^6.8.0",
"eslint-plugin-cypress": "^2.11.1",
"eslint-plugin-prettier": "^3.1.2", "eslint-plugin-prettier": "^3.1.2",
"eslint-plugin-svelte3": "^2.7.3", "eslint-plugin-svelte3": "^2.7.3",
"lerna": "3.14.1", "lerna": "3.14.1",
@ -24,7 +25,8 @@
"test": "lerna run test", "test": "lerna run test",
"lint": "eslint packages", "lint": "eslint packages",
"lint:fix": "eslint --fix packages", "lint:fix": "eslint --fix packages",
"format": "prettier --write \"{,!(node_modules)/**/}*.{js,jsx,svelte}\"" "format": "prettier --write \"{,!(node_modules)/**/}*.{js,jsx,svelte}\"",
"test:e2e": "lerna run cy:test"
}, },
"dependencies": { "dependencies": {
"@material/icon-button": "4.0.0", "@material/icon-button": "4.0.0",

View File

@ -5,4 +5,5 @@ package-lock.json
yarn.lock yarn.lock
release/ release/
dist/ dist/
cypress/screenshots
routify routify

View File

@ -0,0 +1,4 @@
{
"baseUrl": "http://localhost:4001/_builder/",
"video": false
}

View File

@ -0,0 +1,5 @@
{
"name": "Using fixtures to represent data",
"email": "hello@cypress.io",
"body": "Fixtures are a great way to mock data for responses to routes"
}

View File

@ -0,0 +1,5 @@
{
"id": 8739,
"name": "Jane",
"email": "jane@example.com"
}

View File

@ -0,0 +1,232 @@
[
{
"id": 1,
"name": "Leanne Graham",
"username": "Bret",
"email": "Sincere@april.biz",
"address": {
"street": "Kulas Light",
"suite": "Apt. 556",
"city": "Gwenborough",
"zipcode": "92998-3874",
"geo": {
"lat": "-37.3159",
"lng": "81.1496"
}
},
"phone": "1-770-736-8031 x56442",
"website": "hildegard.org",
"company": {
"name": "Romaguera-Crona",
"catchPhrase": "Multi-layered client-server neural-net",
"bs": "harness real-time e-markets"
}
},
{
"id": 2,
"name": "Ervin Howell",
"username": "Antonette",
"email": "Shanna@melissa.tv",
"address": {
"street": "Victor Plains",
"suite": "Suite 879",
"city": "Wisokyburgh",
"zipcode": "90566-7771",
"geo": {
"lat": "-43.9509",
"lng": "-34.4618"
}
},
"phone": "010-692-6593 x09125",
"website": "anastasia.net",
"company": {
"name": "Deckow-Crist",
"catchPhrase": "Proactive didactic contingency",
"bs": "synergize scalable supply-chains"
}
},
{
"id": 3,
"name": "Clementine Bauch",
"username": "Samantha",
"email": "Nathan@yesenia.net",
"address": {
"street": "Douglas Extension",
"suite": "Suite 847",
"city": "McKenziehaven",
"zipcode": "59590-4157",
"geo": {
"lat": "-68.6102",
"lng": "-47.0653"
}
},
"phone": "1-463-123-4447",
"website": "ramiro.info",
"company": {
"name": "Romaguera-Jacobson",
"catchPhrase": "Face to face bifurcated interface",
"bs": "e-enable strategic applications"
}
},
{
"id": 4,
"name": "Patricia Lebsack",
"username": "Karianne",
"email": "Julianne.OConner@kory.org",
"address": {
"street": "Hoeger Mall",
"suite": "Apt. 692",
"city": "South Elvis",
"zipcode": "53919-4257",
"geo": {
"lat": "29.4572",
"lng": "-164.2990"
}
},
"phone": "493-170-9623 x156",
"website": "kale.biz",
"company": {
"name": "Robel-Corkery",
"catchPhrase": "Multi-tiered zero tolerance productivity",
"bs": "transition cutting-edge web services"
}
},
{
"id": 5,
"name": "Chelsey Dietrich",
"username": "Kamren",
"email": "Lucio_Hettinger@annie.ca",
"address": {
"street": "Skiles Walks",
"suite": "Suite 351",
"city": "Roscoeview",
"zipcode": "33263",
"geo": {
"lat": "-31.8129",
"lng": "62.5342"
}
},
"phone": "(254)954-1289",
"website": "demarco.info",
"company": {
"name": "Keebler LLC",
"catchPhrase": "User-centric fault-tolerant solution",
"bs": "revolutionize end-to-end systems"
}
},
{
"id": 6,
"name": "Mrs. Dennis Schulist",
"username": "Leopoldo_Corkery",
"email": "Karley_Dach@jasper.info",
"address": {
"street": "Norberto Crossing",
"suite": "Apt. 950",
"city": "South Christy",
"zipcode": "23505-1337",
"geo": {
"lat": "-71.4197",
"lng": "71.7478"
}
},
"phone": "1-477-935-8478 x6430",
"website": "ola.org",
"company": {
"name": "Considine-Lockman",
"catchPhrase": "Synchronised bottom-line interface",
"bs": "e-enable innovative applications"
}
},
{
"id": 7,
"name": "Kurtis Weissnat",
"username": "Elwyn.Skiles",
"email": "Telly.Hoeger@billy.biz",
"address": {
"street": "Rex Trail",
"suite": "Suite 280",
"city": "Howemouth",
"zipcode": "58804-1099",
"geo": {
"lat": "24.8918",
"lng": "21.8984"
}
},
"phone": "210.067.6132",
"website": "elvis.io",
"company": {
"name": "Johns Group",
"catchPhrase": "Configurable multimedia task-force",
"bs": "generate enterprise e-tailers"
}
},
{
"id": 8,
"name": "Nicholas Runolfsdottir V",
"username": "Maxime_Nienow",
"email": "Sherwood@rosamond.me",
"address": {
"street": "Ellsworth Summit",
"suite": "Suite 729",
"city": "Aliyaview",
"zipcode": "45169",
"geo": {
"lat": "-14.3990",
"lng": "-120.7677"
}
},
"phone": "586.493.6943 x140",
"website": "jacynthe.com",
"company": {
"name": "Abernathy Group",
"catchPhrase": "Implemented secondary concept",
"bs": "e-enable extensible e-tailers"
}
},
{
"id": 9,
"name": "Glenna Reichert",
"username": "Delphine",
"email": "Chaim_McDermott@dana.io",
"address": {
"street": "Dayna Park",
"suite": "Suite 449",
"city": "Bartholomebury",
"zipcode": "76495-3109",
"geo": {
"lat": "24.6463",
"lng": "-168.8889"
}
},
"phone": "(775)976-6794 x41206",
"website": "conrad.com",
"company": {
"name": "Yost and Sons",
"catchPhrase": "Switchable contextually-based project",
"bs": "aggregate real-time technologies"
}
},
{
"id": 10,
"name": "Clementina DuBuque",
"username": "Moriah.Stanton",
"email": "Rey.Padberg@karina.biz",
"address": {
"street": "Kattie Turnpike",
"suite": "Suite 198",
"city": "Lebsackbury",
"zipcode": "31428-2261",
"geo": {
"lat": "-38.2386",
"lng": "57.2232"
}
},
"phone": "024-648-3804",
"website": "ambrose.net",
"company": {
"name": "Hoeger LLC",
"catchPhrase": "Centralized empowering task-force",
"bs": "target end-to-end models"
}
}
]

View File

@ -0,0 +1,17 @@
context('Create an Application', () => {
beforeEach(() => {
cy.visit('localhost:4001/_builder')
})
// https://on.cypress.io/interacting-with-elements
it('should create a new application', () => {
// https://on.cypress.io/type
cy.createApp('My Cool App', 'This is a description')
cy.visit('localhost:4001/_builder')
cy.contains('My Cool App').should('exist')
})
})

View File

@ -0,0 +1,53 @@
context('Create Components', () => {
before(() => {
cy.visit('localhost:4001/_builder')
// https://on.cypress.io/type
cy.createApp('Model App', 'Model App Description')
cy.createModel('dog', 'name', 'age')
cy.addRecord('bob', '15')
})
// https://on.cypress.io/interacting-with-elements
it('should add a container', () => {
cy.contains('frontend').click()
cy.get('.switcher > :nth-child(2)').click()
cy.contains('Container').click()
})
it('should add a headline', () => {
cy.addHeadlineComponent('An Amazing headline!')
getIframeBody().contains('An Amazing headline!')
})
it('change the font size of the headline', () => {
cy.contains('Typography').click()
cy.get('input[name="font-size"]')
.type('60px')
cy.contains('Design').click()
getIframeBody().contains('An Amazing headline!').should('have.css', 'font-size', '60px')
})
})
const getIframeDocument = () => {
return cy
.get('iframe')
// Cypress yields jQuery element, which has the real
// DOM element under property "0".
// From the real DOM iframe element we can get
// the "document" element, it is stored in "contentDocument" property
// Cypress "its" command can access deep properties using dot notation
// https://on.cypress.io/its
.its('0.contentDocument').should('exist')
}
const getIframeBody = () => {
// get the document
return getIframeDocument()
// automatically retries until body is loaded
.its('body').should('not.be.undefined')
// wraps "body" DOM element to allow
// chaining more Cypress commands, like ".find(...)"
.then(cy.wrap)
}

View File

@ -0,0 +1,22 @@
context('Create a Model', () => {
before(() => {
cy.visit('localhost:4001/_builder')
// https://on.cypress.io/type
cy.createApp('Model App', 'Model App Description')
})
// https://on.cypress.io/interacting-with-elements
it('should create a new model', () => {
cy.createModel('dog', 'name', 'age')
// Check if model exists
cy.get('.title').should('have.text', 'dog')
})
it('should add a record', () => {
cy.addRecord('bob', '15')
cy.contains('bob').should('have.text', 'bob')
})
})

View File

@ -0,0 +1,19 @@
context('Create a User', () => {
before(() => {
cy.visit('localhost:4001/_builder')
// https://on.cypress.io/type
cy.createApp('User App', 'This app is used to test user creation')
})
// https://on.cypress.io/interacting-with-elements
it('should create a user', () => {
// Close Model modal that shows up after creating an app
cy.get('.close').click()
cy.createUser('bbuser', 'test', 'ADMIN')
// Check to make sure user was created!
cy.contains('bbuser').should('have.text', 'bbuser')
})
})

View File

@ -0,0 +1,46 @@
context('Create a workflow', () => {
before(() => {
cy.visit('localhost:4001/_builder')
cy.createApp('Workflow Test App', 'This app is used to test that workflows do in fact work!')
})
// https://on.cypress.io/interacting-with-elements
it('should create a workflow', () => {
cy.createModel('dog', 'name', 'age')
cy.createUser('bbuser', 'test', 'ADMIN')
cy.contains('workflow').click()
cy.get('.new-workflow-button').click()
cy.get('input').type('Add Record')
cy.contains('Save').click()
// Add trigger
cy.get('[data-cy=add-workflow-component]').click()
cy.get('[data-cy=RECORD_SAVED]').click()
cy.get('.budibase__input').select('dog')
// Create action
cy.get('[data-cy=SAVE_RECORD]').click()
cy.get(':nth-child(2) > .budibase__input').type('goodboy')
cy.get(':nth-child(3) > .budibase__input').type('11')
// Save
cy.get('[data-cy=save-workflow-setup]').click()
cy.get('.workflow-button').click()
// Activate Workflow
cy.get('[data-cy=activate-workflow]').click()
})
it('should add record when a new record is added', () => {
cy.contains('backend').click()
cy.addRecord('bob', '15')
cy.contains('goodboy').should('have.text', 'goodboy')
})
})

View File

@ -0,0 +1,22 @@
/// <reference types="cypress" />
// ***********************************************************
// This example plugins/index.js can be used to load plugins
//
// You can change the location of this file or turn off loading
// the plugins file with the 'pluginsFile' configuration option.
//
// You can read more here:
// https://on.cypress.io/plugins-guide
// ***********************************************************
// This function is called when a project is opened or re-opened (e.g. due to
// the project's config changing)
/**
* @type {Cypress.PluginConfig}
*/
// eslint-disable-next-line no-unused-vars
module.exports = (on, config) => {
// `on` is used to hook into various events Cypress emits
// `config` is the resolved Cypress config
}

View File

@ -0,0 +1,17 @@
// What this script does:
// 1. Removes the old test folder if it exists (.budibase-cypress)
// 2. Initialises using `.budibase-cypress`
// 3. Runs the server using said folder
const rimraf = require("rimraf")
const { join } = require("path")
const homedir = join(require("os").homedir(), ".budibase-cypress")
const init = require("../../cli/src/commands/init/initHandler")
const run = require("../../cli/src/commands/run/runHandler")
rimraf.sync(homedir)
init({ dir: homedir, clientId: "cypress-test" }).then(() => {
delete require.cache[require.resolve("../../server/src/environment")]
run({ dir: homedir })
})

View File

@ -0,0 +1,105 @@
// ***********************************************
// This example commands.js shows you how to
// create various custom commands and overwrite
// existing commands.
//
// For more comprehensive examples of custom
// commands please read more here:
// https://on.cypress.io/custom-commands
// ***********************************************
//
//
// -- This is a parent command --
// Cypress.Commands.add("login", (email, password) => { ... })
//
//
// -- This is a child command --
// Cypress.Commands.add("drag", { prevSubject: 'element'}, (subject, options) => { ... })
//
//
// -- This is a dual command --
// Cypress.Commands.add("dismiss", { prevSubject: 'optional'}, (subject, options) => { ... })
//
//
// -- This will overwrite an existing command --
// Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... })
Cypress.Commands.add("createApp", (name, description) => {
cy.get(".banner-button")
.click()
.get('input[name="name"]')
.type(name)
.should("have.value", name)
cy.get('textarea[name="description"]')
.type(description)
.should("have.value", description)
cy.contains("Save").click()
})
Cypress.Commands.add("createModel", (modelName, firstField, secondField) => {
// Enter model name
cy.get("[data-cy=Name]")
.click()
.type(modelName)
// Add 'name' field
cy.get("[data-cy=add-new-model-field]").click()
cy.get("[data-cy=Name]")
.click()
.type(firstField)
cy.contains("Save").click()
// Add 'age' field
cy.get("[data-cy=add-new-model-field]").click()
cy.get("[data-cy=Name]")
.click()
.type(secondField)
cy.get("select").select("number")
cy.contains("Save").click()
cy.contains(secondField).should("exist")
// Save model
cy.contains("Save").click()
})
Cypress.Commands.add("addRecord", (firstField, secondField) => {
cy.contains("Create new record").click()
cy.get("[data-cy=name-input]")
.click()
.type(firstField)
cy.get("[data-cy=age-input]")
.click()
.type(secondField)
// Save
cy.contains("Save").click()
})
Cypress.Commands.add("createUser", (username, password, level) => {
// Create User
cy.get(".nav-group-header > .ri-add-line").click()
cy.get("[data-cy=username]").type(username)
cy.get("[data-cy=password]").type(password)
cy.get("[data-cy=accessLevel]").select(level)
// Save
cy.contains("Save").click()
})
Cypress.Commands.add("addHeadlineComponent", text => {
cy.get(".switcher > :nth-child(2)").click()
cy.get("[data-cy=Text]").click()
cy.get("[data-cy=Headline]").click()
cy.get(".tabs > :nth-child(2)").click()
cy.get('input[type="text"]').type(text)
cy.contains("Design").click()
})
Cypress.Commands.add("addButtonComponent", () => {
cy.get(".switcher > :nth-child(2)").click()
cy.get("[data-cy=Button]").click()
})

View File

@ -0,0 +1,3 @@
Cypress.Cookies.defaults({
whitelist: "builder:token",
})

View File

@ -0,0 +1,21 @@
// ***********************************************************
// This example support/index.js is processed and
// loaded automatically before your test files.
//
// This is a great place to put global configuration and
// behavior that modifies Cypress.
//
// You can change the location of this file or turn off
// automatically serving support files with the
// 'supportFile' configuration option.
//
// You can read more here:
// https://on.cypress.io/configuration
// ***********************************************************
// Import commands.js using ES2015 syntax:
import "./cookies"
import "./commands"
// Alternatively you can use CommonJS syntax:
// require('./commands')

View File

@ -9,7 +9,11 @@
"test": "jest", "test": "jest",
"test:watch": "jest --watchAll", "test:watch": "jest --watchAll",
"dev:builder": "routify --routify-dir routify -c rollup", "dev:builder": "routify --routify-dir routify -c rollup",
"rollup": "rollup -c -w" "rollup": "rollup -c -w",
"cy:setup": "node ./cypress/setup.js",
"cy:run": "cypress run",
"cy:open": "cypress open",
"cy:test": "start-server-and-test cy:setup http://localhost:4001/_builder cy:run"
}, },
"jest": { "jest": {
"globals": { "globals": {
@ -35,6 +39,14 @@
}, },
"transformIgnorePatterns": [ "transformIgnorePatterns": [
"/node_modules/(?!svelte).+\\.js$" "/node_modules/(?!svelte).+\\.js$"
],
"modulePathIgnorePatterns": [
"<rootDir>/cypress/"
]
},
"eslintConfig": {
"extends": [
"plugin:cypress/recommended"
] ]
}, },
"dependencies": { "dependencies": {
@ -67,10 +79,12 @@
"@sveltech/routify": "1.7.11", "@sveltech/routify": "1.7.11",
"babel-jest": "^24.8.0", "babel-jest": "^24.8.0",
"browser-sync": "^2.26.7", "browser-sync": "^2.26.7",
"cypress": "^4.8.0",
"http-proxy-middleware": "^0.19.1", "http-proxy-middleware": "^0.19.1",
"jest": "^24.8.0", "jest": "^24.8.0",
"ncp": "^2.0.0", "ncp": "^2.0.0",
"npm-run-all": "^4.1.5", "npm-run-all": "^4.1.5",
"rimraf": "^3.0.2",
"rollup": "^1.12.0", "rollup": "^1.12.0",
"rollup-plugin-alias": "^1.5.2", "rollup-plugin-alias": "^1.5.2",
"rollup-plugin-browsersync": "^1.0.0", "rollup-plugin-browsersync": "^1.0.0",
@ -83,6 +97,7 @@
"rollup-plugin-svelte": "^5.0.3", "rollup-plugin-svelte": "^5.0.3",
"rollup-plugin-terser": "^4.0.4", "rollup-plugin-terser": "^4.0.4",
"rollup-plugin-url": "^2.2.2", "rollup-plugin-url": "^2.2.2",
"start-server-and-test": "^1.11.0",
"svelte": "3.23.x" "svelte": "3.23.x"
}, },
"gitHead": "115189f72a850bfb52b65ec61d932531bf327072" "gitHead": "115189f72a850bfb52b65ec61d932531bf327072"

View File

@ -1,17 +1,13 @@
const apiCall = method => async (url, body) => { const apiCall = method => async (url, body) => {
const headers = {
"Content-Type": "application/json",
}
const response = await fetch(url, { const response = await fetch(url, {
method: method, method: method,
headers: {
"Content-Type": "application/json",
"x-user-agent": "Budibase Builder",
},
body: body && JSON.stringify(body), body: body && JSON.stringify(body),
headers,
}) })
// if (response.status === 500) {
// throw new Error("Server Error");
// }
return response return response
} }
@ -22,9 +18,9 @@ export const del = apiCall("DELETE")
export const put = apiCall("PUT") export const put = apiCall("PUT")
export default { export default {
post, post: apiCall("POST"),
get, get: apiCall("GET"),
patch, patch: apiCall("PATCH"),
delete: del, delete: apiCall("DELETE"),
put, put: apiCall("PUT"),
} }

View File

@ -24,8 +24,8 @@ export const getBackendUiStore = () => {
store.actions = { store.actions = {
database: { database: {
select: async db => { select: async db => {
const modelsResponse = await api.get(`/api/${db._id}/models`) const modelsResponse = await api.get(`/api/models`)
const viewsResponse = await api.get(`/api/${db._id}/views`) const viewsResponse = await api.get(`/api/views`)
const models = await modelsResponse.json() const models = await modelsResponse.json()
const views = await viewsResponse.json() const views = await viewsResponse.json()
store.update(state => { store.update(state => {

View File

@ -1,5 +1,4 @@
import { values } from "lodash/fp" import { values } from "lodash/fp"
import { backendUiStore } from "builderStore"
import * as backendStoreActions from "./backend" import * as backendStoreActions from "./backend"
import { writable, get } from "svelte/store" import { writable, get } from "svelte/store"
import api from "../api" import api from "../api"
@ -295,13 +294,10 @@ const addChildComponent = store => (componentToAdd, presetName) => {
const presetProps = presetName ? component.presets[presetName] : {} const presetProps = presetName ? component.presets[presetName] : {}
const instanceId = get(backendUiStore).selectedDatabase._id
const newComponent = createProps( const newComponent = createProps(
component, component,
{ {
...presetProps, ...presetProps,
_instanceId: instanceId,
}, },
state state
) )

View File

@ -3,8 +3,8 @@ import api from "../../api"
import Workflow from "./Workflow" import Workflow from "./Workflow"
const workflowActions = store => ({ const workflowActions = store => ({
fetch: async instanceId => { fetch: async () => {
const WORKFLOWS_URL = `/api/${instanceId}/workflows` const WORKFLOWS_URL = `/api/workflows`
const workflowResponse = await api.get(WORKFLOWS_URL) const workflowResponse = await api.get(WORKFLOWS_URL)
const json = await workflowResponse.json() const json = await workflowResponse.json()
store.update(state => { store.update(state => {
@ -12,14 +12,14 @@ const workflowActions = store => ({
return state return state
}) })
}, },
create: async ({ instanceId, name }) => { create: async ({ name }) => {
const workflow = { const workflow = {
name, name,
definition: { definition: {
steps: [], steps: [],
}, },
} }
const CREATE_WORKFLOW_URL = `/api/${instanceId}/workflows` const CREATE_WORKFLOW_URL = `/api/workflows`
const response = await api.post(CREATE_WORKFLOW_URL, workflow) const response = await api.post(CREATE_WORKFLOW_URL, workflow)
const json = await response.json() const json = await response.json()
store.update(state => { store.update(state => {
@ -28,8 +28,8 @@ const workflowActions = store => ({
return state return state
}) })
}, },
save: async ({ instanceId, workflow }) => { save: async ({ workflow }) => {
const UPDATE_WORKFLOW_URL = `/api/${instanceId}/workflows` const UPDATE_WORKFLOW_URL = `/api/workflows`
const response = await api.put(UPDATE_WORKFLOW_URL, workflow) const response = await api.put(UPDATE_WORKFLOW_URL, workflow)
const json = await response.json() const json = await response.json()
store.update(state => { store.update(state => {
@ -42,8 +42,8 @@ const workflowActions = store => ({
return state return state
}) })
}, },
update: async ({ instanceId, workflow }) => { update: async ({ workflow }) => {
const UPDATE_WORKFLOW_URL = `/api/${instanceId}/workflows` const UPDATE_WORKFLOW_URL = `/api/workflows`
const response = await api.put(UPDATE_WORKFLOW_URL, workflow) const response = await api.put(UPDATE_WORKFLOW_URL, workflow)
const json = await response.json() const json = await response.json()
store.update(state => { store.update(state => {
@ -55,9 +55,9 @@ const workflowActions = store => ({
return state return state
}) })
}, },
delete: async ({ instanceId, workflow }) => { delete: async ({ workflow }) => {
const { _id, _rev } = workflow const { _id, _rev } = workflow
const DELETE_WORKFLOW_URL = `/api/${instanceId}/workflows/${_id}/${_rev}` const DELETE_WORKFLOW_URL = `/api/workflows/${_id}/${_rev}`
await api.delete(DELETE_WORKFLOW_URL) await api.delete(DELETE_WORKFLOW_URL)
store.update(state => { store.update(state => {

View File

@ -2,6 +2,7 @@
import { onMount } from "svelte" import { onMount } from "svelte"
import { buildStyle } from "../../helpers.js" import { buildStyle } from "../../helpers.js"
export let value = "" export let value = ""
export let name = ""
export let textAlign = "left" export let textAlign = "left"
export let width = "160px" export let width = "160px"
export let placeholder = "" export let placeholder = ""
@ -25,6 +26,7 @@
</script> </script>
<input <input
{name}
class:centerPlaceholder class:centerPlaceholder
type="text" type="text"
value={displayValue} value={displayValue}

View File

@ -13,6 +13,7 @@
<label class="uk-form-label">{label}</label> <label class="uk-form-label">{label}</label>
<div class="uk-form-controls"> <div class="uk-form-controls">
<input <input
data-cy={label}
class="budibase__input" class="budibase__input"
class:uk-form-danger={hasError} class:uk-form-danger={hasError}
on:change on:change

View File

@ -53,12 +53,10 @@
let views = [] let views = []
let currentPage = 0 let currentPage = 0
$: instanceId = $backendUiStore.selectedDatabase._id
$: { $: {
if ($backendUiStore.selectedView) { if ($backendUiStore.selectedView) {
api api
.fetchDataForView($backendUiStore.selectedView, instanceId) .fetchDataForView($backendUiStore.selectedView)
.then(records => { .then(records => {
data = records || [] data = records || []
headers = Object.keys($backendUiStore.selectedModel.schema).filter( headers = Object.keys($backendUiStore.selectedModel.schema).filter(

View File

@ -1,7 +1,7 @@
import api from "builderStore/api" import api from "builderStore/api"
export async function createUser(user, instanceId) { export async function createUser(user) {
const CREATE_USER_URL = `/api/${instanceId}/users` const CREATE_USER_URL = `/api/users`
const response = await api.post(CREATE_USER_URL, user) const response = await api.post(CREATE_USER_URL, user)
return await response.json() return await response.json()
} }
@ -14,21 +14,21 @@ export async function createDatabase(appname, instanceName) {
return await response.json() return await response.json()
} }
export async function deleteRecord(record, instanceId) { export async function deleteRecord(record) {
const DELETE_RECORDS_URL = `/api/${instanceId}/${record._modelId}/records/${record._id}/${record._rev}` const DELETE_RECORDS_URL = `/api/${record._modelId}/records/${record._id}/${record._rev}`
const response = await api.delete(DELETE_RECORDS_URL) const response = await api.delete(DELETE_RECORDS_URL)
return response return response
} }
export async function saveRecord(record, instanceId, modelId) { export async function saveRecord(record, modelId) {
const SAVE_RECORDS_URL = `/api/${instanceId}/${modelId}/records` const SAVE_RECORDS_URL = `/api/${modelId}/records`
const response = await api.post(SAVE_RECORDS_URL, record) const response = await api.post(SAVE_RECORDS_URL, record)
return await response.json() return await response.json()
} }
export async function fetchDataForView(viewName, instanceId) { export async function fetchDataForView(viewName) {
const FETCH_RECORDS_URL = `/api/${instanceId}/views/${viewName}` const FETCH_RECORDS_URL = `/api/views/${viewName}`
const response = await api.get(FETCH_RECORDS_URL) const response = await api.get(FETCH_RECORDS_URL)
return await response.json() return await response.json()

View File

@ -18,7 +18,6 @@
let fieldToEdit let fieldToEdit
$: modelFields = model.schema ? Object.entries(model.schema) : [] $: modelFields = model.schema ? Object.entries(model.schema) : []
$: instanceId = $backendUiStore.selectedDatabase._id
function editField() {} function editField() {}
@ -27,7 +26,7 @@
function onFinishedFieldEdit() {} function onFinishedFieldEdit() {}
async function saveModel() { async function saveModel() {
const SAVE_MODEL_URL = `/api/${instanceId}/models` const SAVE_MODEL_URL = `/api/models`
const response = await api.post(SAVE_MODEL_URL, model) const response = await api.post(SAVE_MODEL_URL, model)
const newModel = await response.json() const newModel = await response.json()
backendUiStore.actions.models.create(newModel) backendUiStore.actions.models.create(newModel)
@ -54,7 +53,10 @@
</div> </div>
<div class="table-controls"> <div class="table-controls">
<span class="label">Fields</span> <span class="label">Fields</span>
<div class="hoverable new-field" on:click={() => (showFieldView = true)}> <div
data-cy="add-new-model-field"
class="hoverable new-field"
on:click={() => (showFieldView = true)}>
Add new field Add new field
</div> </div>
</div> </div>

View File

@ -14,8 +14,6 @@
let errors = [] let errors = []
let selectedModel let selectedModel
$: instanceId = $backendUiStore.selectedDatabase._id
$: modelSchema = $backendUiStore.selectedModel $: modelSchema = $backendUiStore.selectedModel
? Object.entries($backendUiStore.selectedModel.schema) ? Object.entries($backendUiStore.selectedModel.schema)
: [] : []
@ -49,7 +47,6 @@
...record, ...record,
modelId: $backendUiStore.selectedModel._id, modelId: $backendUiStore.selectedModel._id,
}, },
instanceId,
$backendUiStore.selectedModel._id $backendUiStore.selectedModel._id
) )
if (recordResponse.errors) { if (recordResponse.errors) {

View File

@ -29,7 +29,7 @@
function deleteView() {} function deleteView() {}
async function saveView() { async function saveView() {
const SAVE_VIEW_URL = `/api/${instanceId}/views` const SAVE_VIEW_URL = `/api/views`
const response = await api.post(SAVE_VIEW_URL, view) const response = await api.post(SAVE_VIEW_URL, view)
backendUiStore.update(state => { backendUiStore.update(state => {
state.views = [...state.views, response.view] state.views = [...state.views, response.view]

View File

@ -10,12 +10,11 @@
let accessLevelId let accessLevelId
$: valid = username && password && accessLevelId $: valid = username && password && accessLevelId
$: instanceId = $backendUiStore.selectedDatabase._id
$: appId = $store.appId $: appId = $store.appId
async function createUser() { async function createUser() {
const user = { name: username, username, password, accessLevelId } const user = { name: username, username, password, accessLevelId }
const response = await api.createUser(user, instanceId) const response = await api.createUser(user)
backendUiStore.actions.users.create(response) backendUiStore.actions.users.create(response)
onClosed() onClosed()
} }
@ -29,15 +28,26 @@
</div> </div>
<div class="uk-margin"> <div class="uk-margin">
<label class="uk-form-label" for="form-stacked-text">Username</label> <label class="uk-form-label" for="form-stacked-text">Username</label>
<input class="uk-input" type="text" bind:value={username} /> <input
data-cy="username"
class="uk-input"
type="text"
bind:value={username} />
</div> </div>
<div class="uk-margin"> <div class="uk-margin">
<label class="uk-form-label" for="form-stacked-text">Password</label> <label class="uk-form-label" for="form-stacked-text">Password</label>
<input class="uk-input" type="password" bind:value={password} /> <input
data-cy="password"
class="uk-input"
type="password"
bind:value={password} />
</div> </div>
<div class="uk-margin"> <div class="uk-margin">
<label class="uk-form-label" for="form-stacked-text">Access Level</label> <label class="uk-form-label" for="form-stacked-text">Access Level</label>
<select class="uk-select" bind:value={accessLevelId}> <select
data-cy="accessLevel"
class="uk-select"
bind:value={accessLevelId}>
<option value="" /> <option value="" />
<option value="POWER_USER">Power User</option> <option value="POWER_USER">Power User</option>
<option value="ADMIN">Admin</option> <option value="ADMIN">Admin</option>

View File

@ -6,7 +6,6 @@
export let record export let record
export let onClosed export let onClosed
$: instanceId = $backendUiStore.selectedDatabase._id
</script> </script>
<section> <section>
@ -25,7 +24,7 @@
<ActionButton <ActionButton
alert alert
on:click={async () => { on:click={async () => {
await api.deleteRecord(record, instanceId) await api.deleteRecord(record)
backendUiStore.actions.records.delete(record) backendUiStore.actions.records.delete(record)
onClosed() onClosed()
}}> }}>

View File

@ -32,6 +32,7 @@
{#if type === 'select'} {#if type === 'select'}
<select <select
data-cy="{label}-select"
class={determineClassName(type)} class={determineClassName(type)}
bind:value bind:value
class:uk-form-danger={errors.length > 0}> class:uk-form-danger={errors.length > 0}>
@ -41,6 +42,7 @@
</select> </select>
{:else} {:else}
<input <input
data-cy="{label}-input"
class={determineClassName(type)} class={determineClassName(type)}
class:uk-form-danger={errors.length > 0} class:uk-form-danger={errors.length > 0}
{checked} {checked}

View File

@ -7,7 +7,6 @@
CreateEditModelModal, CreateEditModelModal,
CreateEditViewModal, CreateEditViewModal,
} from "components/database/ModelDataTable/modals" } from "components/database/ModelDataTable/modals"
import api from "builderStore/api"
const { open, close } = getContext("simple-modal") const { open, close } = getContext("simple-modal")

View File

@ -52,7 +52,7 @@
} }
async function deleteModel(modelToDelete) { async function deleteModel(modelToDelete) {
const DELETE_MODEL_URL = `/api/${instanceId}/models/${node._id}/${node._rev}` const DELETE_MODEL_URL = `/api/models/${node._id}/${node._rev}`
const response = await api.delete(DELETE_MODEL_URL) const response = await api.delete(DELETE_MODEL_URL)
backendUiStore.update(state => { backendUiStore.update(state => {
state.models = state.models.filter( state.models = state.models.filter(

View File

@ -12,11 +12,10 @@
$: currentAppInfo = { $: currentAppInfo = {
appname: $store.appname, appname: $store.appname,
instanceId: $backendUiStore.selectedDatabase._id,
} }
async function fetchUsers() { async function fetchUsers() {
const FETCH_USERS_URL = `/api/${currentAppInfo.instanceId}/users` const FETCH_USERS_URL = `/api/users`
const response = await api.get(FETCH_USERS_URL) const response = await api.get(FETCH_USERS_URL)
const users = await response.json() const users = await response.json()
backendUiStore.update(state => { backendUiStore.update(state => {

View File

@ -91,7 +91,6 @@
? screenPlaceholder ? screenPlaceholder
: $store.currentPreviewItem, : $store.currentPreviewItem,
], ],
appRootPath: "",
} }
$: selectedComponentType = getComponentTypeName($store.currentComponentInfo) $: selectedComponentType = getComponentTypeName($store.currentComponentInfo)
@ -108,6 +107,8 @@
selectedComponentType, selectedComponentType,
selectedComponentId, selectedComponentId,
frontendDefinition, frontendDefinition,
appId: $store.appId,
instanceId: $backendUiStore.selectedDatabase._id,
}) })
) )
} }

View File

@ -20,6 +20,7 @@ export default `<html>
font-weight: bold; font-weight: bold;
} }
</style> </style>
<script src='/assets/budibase-client.js'></script>
<script> <script>
function receiveMessage(event) { function receiveMessage(event) {
@ -45,11 +46,10 @@ export default `<html>
styles.appendChild(document.createTextNode(data.styles)) styles.appendChild(document.createTextNode(data.styles))
window["##BUDIBASE_FRONTEND_DEFINITION##"] = data.frontendDefinition; window["##BUDIBASE_FRONTEND_DEFINITION##"] = data.frontendDefinition;
if (clientModule) { if (window.loadBudibase) {
clientModule.loadBudibase({ window, localStorage }) loadBudibase({ window, localStorage })
} }
} }
let clientModule
let styles let styles
let selectedComponentStyle let selectedComponentStyle
@ -59,12 +59,9 @@ export default `<html>
return false; return false;
}, true) }, true)
import('/_builder/budibase-client.esm.mjs') window.addEventListener('message', receiveMessage)
.then(module => { window.dispatchEvent(new Event('bb-ready'))
clientModule = module
window.addEventListener('message', receiveMessage)
window.dispatchEvent(new Event('bb-ready'))
})
</script> </script>
</head> </head>
<body> <body>

View File

@ -7,6 +7,7 @@
<div class="tabs"> <div class="tabs">
{#each categories as category} {#each categories as category}
<li <li
data-cy={category.name}
on:click={() => onClick(category)} on:click={() => onClick(category)}
class:active={selectedCategory === category}> class:active={selectedCategory === category}>
{category.name} {category.name}

View File

@ -1,12 +1,14 @@
<script> <script>
import {buildStyle} from "./helpers.js" import {buildStyle} from "./helpers.js"
import {fade} from "svelte/transition"
export let backgroundSize = "10px" export let backgroundSize = "10px"
export let borderRadius = "" export let borderRadius = ""
export let height = "" export let height = ""
export let width = "" export let width = ""
export let margin = ""
$: style = buildStyle({backgroundSize, borderRadius, height, width}) $: style = buildStyle({backgroundSize, borderRadius, height, width, margin})
</script> </script>
@ -18,6 +20,6 @@
} }
</style> </style>
<div {style}> <div in:fade {style}>
<slot /> <slot />
</div> </div>

View File

@ -1,5 +1,7 @@
<script> <script>
import { onMount, createEventDispatcher } from "svelte"; import { onMount, createEventDispatcher } from "svelte";
import { fade } from 'svelte/transition';
import Swatch from "./Swatch.svelte";
import CheckedBackground from "./CheckedBackground.svelte" import CheckedBackground from "./CheckedBackground.svelte"
import {buildStyle} from "./helpers.js" import {buildStyle} from "./helpers.js"
import { import {
@ -13,7 +15,15 @@
import Input from "./Input.svelte"; import Input from "./Input.svelte";
export let value = "#3ec1d3ff"; export let value = "#3ec1d3ff";
export let swatches = [] //TODO: Safe swatches - limit to 12. warn in console
export let disableSwatches = false
export let format = "hexa"; export let format = "hexa";
export let open = false;
export let pickerHeight = 0;
export let pickerWidth = 0;
let adder = null;
let h = null; let h = null;
let s = null; let s = null;
@ -23,11 +33,33 @@
const dispatch = createEventDispatcher(); const dispatch = createEventDispatcher();
onMount(() => { onMount(() => {
if(!swatches.length > 0) {
//Don't use locally stored recent colors if swatches have been passed as props
getRecentColors()
}
if (format) { if (format) {
convertAndSetHSVA() convertAndSetHSVA()
} }
}); });
function getRecentColors() {
let colorStore = localStorage.getItem("cp:recent-colors")
if (colorStore) {
swatches = JSON.parse(colorStore)
}
}
function setRecentColor(color) {
if (swatches.length === 12) {
swatches.splice(0, 1)
}
if (!swatches.includes(color)) {
swatches = [...swatches, color]
localStorage.setItem("cp:recent-colors", JSON.stringify(swatches))
}
}
function convertAndSetHSVA() { function convertAndSetHSVA() {
let hsva = convertToHSVA(value, format); let hsva = convertToHSVA(value, format);
setHSVA(hsva); setHSVA(hsva);
@ -45,36 +77,77 @@
s = detail.s; s = detail.s;
v = detail.v; v = detail.v;
value = convertHsvaToFormat([h, s, v, a], format); value = convertHsvaToFormat([h, s, v, a], format);
dispatchValue()
}
function setHue({color, isDrag}) {
h = color;
value = convertHsvaToFormat([h, s, v, a], format);
if(!isDrag) {
dispatchValue()
}
}
function setAlpha({color, isDrag}) {
a = color === "1.00" ? "1" : color;
value = convertHsvaToFormat([h, s, v, a], format);
if(!isDrag) {
dispatchValue()
}
}
function dispatchValue() {
dispatch("change", value) dispatch("change", value)
} }
function setHue(hue) {
h = hue;
value = convertHsvaToFormat([h, s, v, a], format);
}
function setAlpha(alpha) {
a = alpha === "1.00" ? "1" :alpha;
value = convertHsvaToFormat([h, s, v, a], format);
}
function changeFormatAndConvert(f) { function changeFormatAndConvert(f) {
format = f; format = f;
value = convertHsvaToFormat([h, s, v, a], format); value = convertHsvaToFormat([h, s, v, a], format);
} }
function handleColorInput(text) { function handleColorInput(text) {
let f = getColorFormat(text) let format = getColorFormat(text)
if(f) { if(format) {
format = f;
value = text value = text
convertAndSetHSVA() convertAndSetHSVA()
dispatch("change", value)
} }
} }
$: border = s < 10 ? "1px dashed #dedada" : "" function dispatchInputChange() {
if(format) {
dispatchValue()
}
}
function addSwatch() {
if(format) {
dispatch("addswatch", value)
setRecentColor(value)
}
}
function removeSwatch(idx) {
let removedSwatch = swatches.splice(idx, 1);
swatches = swatches
dispatch("removeswatch", removedSwatch)
localStorage.setItem("cp:recent-colors", JSON.stringify(swatches))
}
function applySwatch(color) {
if(value !== color) {
format = getColorFormat(color)
if(format) {
value = color
convertAndSetHSVA()
dispatchValue()
}
}
}
$: border = (v > 90 && s < 5) ? "1px dashed #dedada" : ""
$: style = buildStyle({background: value, border}) $: style = buildStyle({background: value, border})
$: shrink = swatches.length > 0
</script> </script>
<style> <style>
@ -83,7 +156,8 @@
font-size: 11px; font-size: 11px;
font-weight: 400; font-weight: 400;
flex-direction: column; flex-direction: column;
height: 265px; /* height: 265px; */
height: auto;
width: 220px; width: 220px;
background: #ffffff; background: #ffffff;
border-radius: 2px; border-radius: 2px;
@ -118,14 +192,44 @@
border-radius: 50%; border-radius: 50%;
} }
.swatch-panel {
flex: 0 0 15px;
display: flex;
flex-flow: row wrap;
justify-content: flex-start;
padding: 0 5px;
max-height: 56px;
}
.adder {
flex: 1;
height: 20px;
display: flex;
transition: flex 0.5s;
justify-content: center;
align-items: center;
background: #f1f3f4;
cursor: pointer;
border: 1px solid #d4d4d4;
border-radius: 8px;
margin-left: 5px;
margin-top: 3px;
font-weight: 500;
}
.shrink {
flex: 0 0 20px;
}
.format-input-panel { .format-input-panel {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
justify-content: center; justify-content: center;
padding-top: 3px;
} }
</style> </style>
<div class="colorpicker-container"> <div class="colorpicker-container" bind:clientHeight={pickerHeight} bind:clientWidth={pickerWidth}>
<div class="palette-panel"> <div class="palette-panel">
<Palette on:change={setSaturationAndValue} {h} {s} {v} {a} /> <Palette on:change={setSaturationAndValue} {h} {s} {v} {a} />
@ -139,21 +243,38 @@
</CheckedBackground> </CheckedBackground>
</div> </div>
<div> <div>
<Slider type="hue" value={h} on:change={hue => setHue(hue.detail)} /> <Slider type="hue" value={h} on:change={(hue) => setHue(hue.detail)} on:dragend={dispatchValue} />
<CheckedBackground borderRadius="10px" backgroundSize="7px"> <CheckedBackground borderRadius="10px" backgroundSize="7px">
<Slider <Slider
type="alpha" type="alpha"
value={a} value={a}
on:change={alpha => setAlpha(alpha.detail)} /> on:change={(alpha, isDrag) => setAlpha(alpha.detail, isDrag)}
on:dragend={dispatchValue}
/>
</CheckedBackground> </CheckedBackground>
</div> </div>
</div> </div>
{#if !disableSwatches}
<div transition:fade class="swatch-panel">
{#if swatches.length > 0}
{#each swatches as color, idx}
<Swatch {color} on:click={() => applySwatch(color)} on:removeswatch={() => removeSwatch(idx)} />
{/each}
{/if}
{#if swatches.length !== 12}
<div bind:this={adder} transition:fade class="adder" on:click={addSwatch} class:shrink>
<span>&plus;</span>
</div>
{/if}
</div>
{/if}
<div class="format-input-panel"> <div class="format-input-panel">
<ButtonGroup {format} onclick={changeFormatAndConvert} /> <ButtonGroup {format} onclick={changeFormatAndConvert} />
<Input {value} on:input={event => handleColorInput(event.target.value)} /> <Input {value} on:input={event => handleColorInput(event.target.value)} on:change={dispatchInputChange} />
</div> </div>
</div> </div>

View File

@ -2,11 +2,14 @@
import Colorpicker from "./Colorpicker.svelte" import Colorpicker from "./Colorpicker.svelte"
import CheckedBackground from "./CheckedBackground.svelte" import CheckedBackground from "./CheckedBackground.svelte"
import {createEventDispatcher, afterUpdate, beforeUpdate} from "svelte" import {createEventDispatcher, afterUpdate, beforeUpdate} from "svelte"
import {buildStyle} from "./helpers.js" import {buildStyle} from "./helpers.js"
import { fade } from 'svelte/transition'; import { fade } from 'svelte/transition';
import {getColorFormat} from "./utils.js" import {getColorFormat} from "./utils.js"
export let value = "#3ec1d3ff" export let value = "#3ec1d3ff"
export let swatches = []
export let disableSwatches = false
export let open = false; export let open = false;
export let width = "25px" export let width = "25px"
export let height = "25px" export let height = "25px"
@ -17,8 +20,8 @@
let previewHeight = null let previewHeight = null
let previewWidth = null let previewWidth = null
let pickerWidth = 250 let pickerWidth = 0
let pickerHeight = 300 let pickerHeight = 0
let anchorEl = null let anchorEl = null
let parentNodes = []; let parentNodes = [];
@ -61,30 +64,33 @@
function openColorpicker(event) { function openColorpicker(event) {
if(colorPreview) { if(colorPreview) {
const {top: spaceAbove, width, bottom, right, left: spaceLeft} = colorPreview.getBoundingClientRect()
const {innerHeight, innerWidth} = window
const {offsetLeft, offsetTop} = colorPreview
//get the scrollTop value for all scrollable parent elements
let scrollTop = parentNodes.reduce((scrollAcc, el) => scrollAcc += el.scrollTop, 0);
const spaceBelow = (innerHeight - spaceAbove) - previewHeight
const top = spaceAbove > spaceBelow ? (offsetTop - pickerHeight) - scrollTop : (offsetTop + previewHeight) - scrollTop
//TOO: Testing and Scroll Awareness for x Scroll
const spaceRight = (innerWidth - spaceLeft) + previewWidth
const left = spaceRight > spaceLeft ? (offsetLeft + previewWidth) : offsetLeft - pickerWidth
dimensions = {top, left}
open = true; open = true;
} }
} }
$: if(open && colorPreview) {
const {top: spaceAbove, width, bottom, right, left: spaceLeft} = colorPreview.getBoundingClientRect()
const {innerHeight, innerWidth} = window
const {offsetLeft, offsetTop} = colorPreview
//get the scrollTop value for all scrollable parent elements
let scrollTop = parentNodes.reduce((scrollAcc, el) => scrollAcc += el.scrollTop, 0);
const spaceBelow = (innerHeight - spaceAbove) - previewHeight
const top = spaceAbove > spaceBelow ? (offsetTop - pickerHeight) - scrollTop : (offsetTop + previewHeight) - scrollTop
//TOO: Testing and Scroll Awareness for x Scroll
const spaceRight = (innerWidth - spaceLeft) + previewWidth
const left = spaceRight > spaceLeft ? (offsetLeft + previewWidth) : offsetLeft - pickerWidth
dimensions = {top, left}
}
function onColorChange(color) { function onColorChange(color) {
value = color.detail; value = color.detail;
dispatch("change", color.detail) dispatch("change", color.detail)
} }
</script> </script>
<div class="color-preview-container"> <div class="color-preview-container">
@ -94,8 +100,8 @@
</CheckedBackground> </CheckedBackground>
{#if open} {#if open}
<div class="picker-container" bind:clientHeight={pickerHeight} bind:clientWidth={pickerWidth} style={pickerStyle}> <div transition:fade class="picker-container" style={pickerStyle}>
<Colorpicker on:change={onColorChange} {format} {value} /> <Colorpicker on:change={onColorChange} on:addswatch on:removeswatch bind:format bind:value bind:pickerHeight bind:pickerWidth {swatches} {disableSwatches} {open} />
</div> </div>
<div on:click|self={() => open = false} class="overlay"></div> <div on:click|self={() => open = false} class="overlay"></div>
{/if} {/if}

View File

@ -24,5 +24,5 @@
</style> </style>
<div> <div>
<input on:input type="text" {value} maxlength="25" /> <input on:input on:change type="text" {value} maxlength="25" />
</div> </div>

View File

@ -10,7 +10,7 @@
let slider; let slider;
let sliderWidth = 0; let sliderWidth = 0;
function handleClick(mouseX) { function onSliderChange(mouseX, isDrag = false) {
const { left, width } = slider.getBoundingClientRect(); const { left, width } = slider.getBoundingClientRect();
let clickPosition = mouseX - left; let clickPosition = mouseX - left;
@ -21,7 +21,8 @@
type === "hue" type === "hue"
? 360 * percentageClick ? 360 * percentageClick
: percentageClick; : percentageClick;
dispatch("change", value);
dispatch("change", {color: value, isDrag});
} }
} }
@ -75,13 +76,14 @@
<div <div
bind:this={slider} bind:this={slider}
bind:clientWidth={sliderWidth} bind:clientWidth={sliderWidth}
on:click={event => handleClick(event.clientX)} on:click={event => onSliderChange(event.clientX)}
class="color-format-slider" class="color-format-slider"
class:hue={type === 'hue'} class:hue={type === 'hue'}
class:alpha={type === 'alpha'}> class:alpha={type === 'alpha'}>
<div <div
use:dragable use:dragable
on:drag={e => handleClick(e.detail)} on:drag={e => onSliderChange(e.detail, true)}
on:dragend
class="slider-thumb" class="slider-thumb"
{style} /> {style} />
</div> </div>

View File

@ -0,0 +1,53 @@
<script>
import {createEventDispatcher} from "svelte"
import { fade } from 'svelte/transition';
import CheckedBackground from "./CheckedBackground.svelte"
export let hovered = false
export let color = "#fff"
const dispatch = createEventDispatcher()
</script>
<style>
.swatch {
position: relative;
cursor: pointer;
border-radius: 6px;
border: 1px solid #dedada;
height: 20px;
width: 20px;
}
.space {
padding: 3px 5px;
}
.remove-icon {
position: absolute;
right: 0;
top: -5px;
right: -4px;
width:10px;
height: 10px;
border-radius: 50%;
background-color: #800000;
color: #fff;
display: flex;
justify-content: center;
align-items: center;
}
</style>
<div class="space">
<CheckedBackground borderRadius="6px">
<div in:fade class="swatch" style={`background: ${color};`} on:click|self on:mouseover={() => hovered = true} on:mouseleave={() => hovered = false}>
{#if hovered}
<div in:fade class="remove-icon" on:click|self={()=> dispatch("removeswatch")}>
<span on:click|self={()=> dispatch("removeswatch")}>&times;</span>
</div>
{/if}
</div>
</CheckedBackground>
</div>

View File

@ -16,6 +16,7 @@ export default function(node) {
function handleMouseUp() { function handleMouseUp() {
window.removeEventListener("mousedown", handleMouseDown) window.removeEventListener("mousedown", handleMouseDown)
window.removeEventListener("mousemove", handleMouseMove) window.removeEventListener("mousemove", handleMouseMove)
node.dispatchEvent(new CustomEvent("dragend"))
} }
node.addEventListener("mousedown", handleMouseDown) node.addEventListener("mousedown", handleMouseDown)

View File

@ -3,7 +3,11 @@
export let item export let item
</script> </script>
<div class="item-item" in:fly={{ y: 100, duration: 1000 }} on:click> <div
data-cy={item.name}
class="item-item"
in:fly={{ y: 100, duration: 1000 }}
on:click>
<div class="item-icon"> <div class="item-icon">
<i class={item.icon} /> <i class={item.icon} />
</div> </div>

View File

@ -38,7 +38,8 @@
{...handlevalueKey(value)} {...handlevalueKey(value)}
on:change={val => handleChange(key, val)} on:change={val => handleChange(key, val)}
onChange={val => handleChange(key, val)} onChange={val => handleChange(key, val)}
{...props} /> {...props}
name={key} />
</div> </div>
</div> </div>

View File

@ -1,7 +1,6 @@
<script> <script>
import { store, backendUiStore, workflowStore } from "builderStore" import { store, backendUiStore, workflowStore } from "builderStore"
import { notifier } from "@beyonk/svelte-notifications" import { notifier } from "@beyonk/svelte-notifications"
import api from "builderStore/api"
import ActionButton from "components/common/ActionButton.svelte" import ActionButton from "components/common/ActionButton.svelte"
export let onClosed export let onClosed

View File

@ -3,7 +3,6 @@
import { onMount, getContext } from "svelte" import { onMount, getContext } from "svelte"
import { backendUiStore, workflowStore } from "builderStore" import { backendUiStore, workflowStore } from "builderStore"
import { notifier } from "@beyonk/svelte-notifications" import { notifier } from "@beyonk/svelte-notifications"
import api from "builderStore/api"
import WorkflowBlockSetup from "./WorkflowBlockSetup.svelte" import WorkflowBlockSetup from "./WorkflowBlockSetup.svelte"
import DeleteWorkflowModal from "./DeleteWorkflowModal.svelte" import DeleteWorkflowModal from "./DeleteWorkflowModal.svelte"
@ -96,7 +95,10 @@
{#if workflowBlock} {#if workflowBlock}
<WorkflowBlockSetup {workflowBlock} /> <WorkflowBlockSetup {workflowBlock} />
<div class="buttons"> <div class="buttons">
<button class="workflow-button hoverable" on:click={saveWorkflow}> <button
data-cy="save-workflow-setup"
class="workflow-button hoverable"
on:click={saveWorkflow}>
Save Workflow Save Workflow
</button> </button>
<button <button

View File

@ -3,7 +3,6 @@
import { workflowStore, backendUiStore } from "builderStore" import { workflowStore, backendUiStore } from "builderStore"
import { notifier } from "@beyonk/svelte-notifications" import { notifier } from "@beyonk/svelte-notifications"
import Flowchart from "./flowchart/FlowChart.svelte" import Flowchart from "./flowchart/FlowChart.svelte"
import api from "builderStore/api"
let selectedWorkflow let selectedWorkflow
let uiTree let uiTree
@ -50,6 +49,7 @@
class:highlighted={!workflowLive} class:highlighted={!workflowLive}
class:hoverable={!workflowLive} class:hoverable={!workflowLive}
class="play-button hoverable" class="play-button hoverable"
data-cy="activate-workflow"
on:click={() => setWorkflowLive(true)}> on:click={() => setWorkflowLive(true)}>
<i class="ri-play-fill" /> <i class="ri-play-fill" />
</button> </button>

View File

@ -3,7 +3,6 @@
import { backendUiStore, workflowStore } from "builderStore" import { backendUiStore, workflowStore } from "builderStore"
import { WorkflowList } from "../" import { WorkflowList } from "../"
import WorkflowBlock from "./WorkflowBlock.svelte" import WorkflowBlock from "./WorkflowBlock.svelte"
import api from "builderStore/api"
import blockDefinitions from "../blockDefinitions" import blockDefinitions from "../blockDefinitions"
let selectedTab = "TRIGGER" let selectedTab = "TRIGGER"

View File

@ -15,7 +15,10 @@
} }
</script> </script>
<div class="workflow-block hoverable" on:click={addBlockToWorkflow}> <div
class="workflow-block hoverable"
on:click={addBlockToWorkflow}
data-cy={actionId}>
<div> <div>
<i class={blockDefinition.icon} /> <i class={blockDefinition.icon} />
</div> </div>

View File

@ -1,7 +1,6 @@
<script> <script>
import { store, backendUiStore, workflowStore } from "builderStore" import { store, backendUiStore, workflowStore } from "builderStore"
import { notifier } from "@beyonk/svelte-notifications" import { notifier } from "@beyonk/svelte-notifications"
import api from "builderStore/api"
import ActionButton from "components/common/ActionButton.svelte" import ActionButton from "components/common/ActionButton.svelte"
export let onClosed export let onClosed

View File

@ -3,7 +3,6 @@
import { notifier } from "@beyonk/svelte-notifications" import { notifier } from "@beyonk/svelte-notifications"
import { onMount, getContext } from "svelte" import { onMount, getContext } from "svelte"
import { backendUiStore, workflowStore } from "builderStore" import { backendUiStore, workflowStore } from "builderStore"
import api from "builderStore/api"
import CreateWorkflowModal from "./CreateWorkflowModal.svelte" import CreateWorkflowModal from "./CreateWorkflowModal.svelte"
const { open, close } = getContext("simple-modal") const { open, close } = getContext("simple-modal")
@ -23,7 +22,7 @@
} }
onMount(() => { onMount(() => {
workflowStore.actions.fetch($backendUiStore.selectedDatabase._id) workflowStore.actions.fetch()
}) })
</script> </script>

View File

@ -2,7 +2,6 @@
import { onMount } from "svelte" import { onMount } from "svelte"
import { backendUiStore, workflowStore } from "builderStore" import { backendUiStore, workflowStore } from "builderStore"
import { WorkflowList, BlockList } from "./" import { WorkflowList, BlockList } from "./"
import api from "builderStore/api"
import blockDefinitions from "./blockDefinitions" import blockDefinitions from "./blockDefinitions"
let selectedTab = "WORKFLOWS" let selectedTab = "WORKFLOWS"
@ -11,6 +10,7 @@
<header> <header>
<span <span
data-cy="workflow-list"
class="hoverable workflow-header" class="hoverable workflow-header"
class:selected={selectedTab === 'WORKFLOWS'} class:selected={selectedTab === 'WORKFLOWS'}
on:click={() => (selectedTab = 'WORKFLOWS')}> on:click={() => (selectedTab = 'WORKFLOWS')}>
@ -18,6 +18,7 @@
</span> </span>
{#if $workflowStore.currentWorkflow} {#if $workflowStore.currentWorkflow}
<span <span
data-cy="add-workflow-component"
class="hoverable" class="hoverable"
class:selected={selectedTab === 'ADD'} class:selected={selectedTab === 'ADD'}
on:click={() => (selectedTab = 'ADD')}> on:click={() => (selectedTab = 'ADD')}>

View File

@ -1,3 +1,4 @@
<script> <script>
import { params } from "@sveltech/routify"
store.setCurrentPage($params.page) store.setCurrentPage($params.page)
</script> </script>

View File

@ -7,7 +7,7 @@ const Sqrl = require("squirrelly")
const uuid = require("uuid") const uuid = require("uuid")
module.exports = opts => { module.exports = opts => {
run(opts) return run(opts)
} }
const run = async opts => { const run = async opts => {

View File

@ -5,7 +5,6 @@ module.exports = ({ dir }) => {
dir = xPlatHomeDir(dir) dir = xPlatHomeDir(dir)
process.env.BUDIBASE_DIR = resolve(dir) process.env.BUDIBASE_DIR = resolve(dir)
require("dotenv").config({ path: resolve(dir, ".env") }) require("dotenv").config({ path: resolve(dir, ".env") })
console.log("dotenv loaded")
// dont make this a variable or top level require // dont make this a variable or top level require
// ti will cause environment module to be loaded prematurely // ti will cause environment module to be loaded prematurely

View File

@ -15,7 +15,7 @@
"client": "web" "client": "web"
} }
}, },
"testURL": "http://jest-breaks-if-this-does-not-exist", "testURL": "http://test.com",
"moduleNameMapper": { "moduleNameMapper": {
"\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$": "<rootDir>/internals/mocks/fileMock.js", "\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$": "<rootDir>/internals/mocks/fileMock.js",
"\\.(css|less|sass|scss)$": "identity-obj-proxy" "\\.(css|less|sass|scss)$": "identity-obj-proxy"

View File

@ -1,9 +1,9 @@
import { authenticate } from "./authenticate" import { authenticate } from "./authenticate"
import { triggerWorkflow } from "./workflow" import { triggerWorkflow } from "./workflow"
export const createApi = ({ rootPath = "", setState, getState }) => { export const createApi = ({ setState, getState }) => {
const apiCall = method => async ({ url, body }) => { const apiCall = method => async ({ url, body }) => {
const response = await fetch(`${rootPath}${url}`, { const response = await fetch(url, {
method: method, method: method,
headers: { headers: {
"Content-Type": "application/json", "Content-Type": "application/json",
@ -45,7 +45,6 @@ export const createApi = ({ rootPath = "", setState, getState }) => {
const isSuccess = obj => !obj || !obj[ERROR_MEMBER] const isSuccess = obj => !obj || !obj[ERROR_MEMBER]
const apiOpts = { const apiOpts = {
rootPath,
setState, setState,
getState, getState,
isSuccess, isSuccess,

View File

@ -2,6 +2,7 @@ import { attachChildren } from "./render/attachChildren"
import { createTreeNode } from "./render/prepareRenderComponent" import { createTreeNode } from "./render/prepareRenderComponent"
import { screenRouter } from "./render/screenRouter" import { screenRouter } from "./render/screenRouter"
import { createStateManager } from "./state/stateManager" import { createStateManager } from "./state/stateManager"
import { getAppId } from "./render/getAppId"
export const createApp = ({ export const createApp = ({
componentLibraries, componentLibraries,
@ -15,11 +16,9 @@ export const createApp = ({
const onScreenSlotRendered = screenSlotNode => { const onScreenSlotRendered = screenSlotNode => {
const onScreenSelected = (screen, url) => { const onScreenSelected = (screen, url) => {
const stateManager = createStateManager({ const stateManager = createStateManager({
frontendDefinition,
componentLibraries, componentLibraries,
onScreenSlotRendered: () => {}, onScreenSlotRendered: () => {},
routeTo, routeTo,
appRootPath: frontendDefinition.appRootPath,
}) })
const getAttachChildrenParams = attachChildrenParams(stateManager) const getAttachChildrenParams = attachChildrenParams(stateManager)
screenSlotNode.props._children = [screen.props] screenSlotNode.props._children = [screen.props]
@ -36,10 +35,10 @@ export const createApp = ({
routeTo = screenRouter({ routeTo = screenRouter({
screens: frontendDefinition.screens, screens: frontendDefinition.screens,
onScreenSelected, onScreenSelected,
appRootPath: frontendDefinition.appRootPath, window,
}) })
const fallbackPath = window.location.pathname.replace( const fallbackPath = window.location.pathname.replace(
frontendDefinition.appRootPath, getAppId(window.document.cookie),
"" ""
) )
routeTo(currentUrl || fallbackPath) routeTo(currentUrl || fallbackPath)
@ -59,10 +58,8 @@ export const createApp = ({
let rootTreeNode let rootTreeNode
const pageStateManager = createStateManager({ const pageStateManager = createStateManager({
frontendDefinition,
componentLibraries, componentLibraries,
onScreenSlotRendered, onScreenSlotRendered,
appRootPath: frontendDefinition.appRootPath,
// seems weird, but the routeTo variable may not be available at this point // seems weird, but the routeTo variable may not be available at this point
routeTo: url => routeTo(url), routeTo: url => routeTo(url),
}) })

View File

@ -1,5 +1,6 @@
import { createApp } from "./createApp" import { createApp } from "./createApp"
import { builtins, builtinLibName } from "./render/builtinComponents" import { builtins, builtinLibName } from "./render/builtinComponents"
import { getAppId } from "./render/getAppId"
/** /**
* create a web application from static budibase definition files. * create a web application from static budibase definition files.
@ -8,7 +9,7 @@ import { builtins, builtinLibName } from "./render/builtinComponents"
export const loadBudibase = async opts => { export const loadBudibase = async opts => {
const _window = (opts && opts.window) || window const _window = (opts && opts.window) || window
// const _localStorage = (opts && opts.localStorage) || localStorage // const _localStorage = (opts && opts.localStorage) || localStorage
const appId = getAppId(_window.document.cookie)
const frontendDefinition = _window["##BUDIBASE_FRONTEND_DEFINITION##"] const frontendDefinition = _window["##BUDIBASE_FRONTEND_DEFINITION##"]
const user = {} const user = {}
@ -20,9 +21,7 @@ export const loadBudibase = async opts => {
for (let library of libraries) { for (let library of libraries) {
// fetch the JavaScript for the component libraries from the server // fetch the JavaScript for the component libraries from the server
componentLibraryModules[library] = await import( componentLibraryModules[library] = await import(
`/${frontendDefinition.appId}/componentlibrary?library=${encodeURI( `/componentlibrary?library=${encodeURI(library)}`
library
)}`
) )
} }
@ -38,11 +37,11 @@ export const loadBudibase = async opts => {
componentLibraries: componentLibraryModules, componentLibraries: componentLibraryModules,
frontendDefinition, frontendDefinition,
user, user,
window, window: _window,
}) })
const route = _window.location const route = _window.location
? _window.location.pathname.replace(frontendDefinition.appRootPath, "") ? _window.location.pathname.replace(`${appId}/`, "").replace(appId, "")
: "" : ""
initialisePage(frontendDefinition.page, _window.document.body, route) initialisePage(frontendDefinition.page, _window.document.body, route)

View File

@ -0,0 +1,12 @@
export const getAppId = docCookie => {
const cookie =
docCookie.split(";").find(c => c.trim().startsWith("budibase:token")) ||
docCookie.split(";").find(c => c.trim().startsWith("builder:token"))
const base64Token = cookie.substring(lengthOfKey)
const user = JSON.parse(atob(base64Token.split(".")[1]))
return user.appId
}
const lengthOfKey = "budibase:token=".length

View File

@ -1,11 +1,20 @@
import regexparam from "regexparam" import regexparam from "regexparam"
import { routerStore } from "../state/store" import { routerStore } from "../state/store"
import { getAppId } from "./getAppId"
export const screenRouter = ({ screens, onScreenSelected, appRootPath }) => { export const screenRouter = ({ screens, onScreenSelected, window }) => {
const makeRootedPath = url => { const makeRootedPath = url => {
if (appRootPath) { if (
if (url) return `${appRootPath}${url.startsWith("/") ? "" : "/"}${url}` window.location &&
return appRootPath (window.location.hostname === "localhost" ||
window.location.hostname === "127.0.0.1")
) {
const appId = getAppId(window.document.cookie)
if (url) {
if (url.startsWith(appId)) return url
return `/${appId}${url.startsWith("/") ? "" : "/"}${url}`
}
return appId
} }
return url return url
} }
@ -70,7 +79,7 @@ export const screenRouter = ({ screens, onScreenSelected, appRootPath }) => {
) )
return return
const target = x.target || "_self" const target = (x && x.target) || "_self"
if (!y || target !== "_self" || x.host !== location.host) return if (!y || target !== "_self" || x.host !== location.host) return
e.preventDefault() e.preventDefault()

View File

@ -6,31 +6,19 @@ export const trimSlash = str => str.replace(/^\/+|\/+$/g, "")
export const bbFactory = ({ export const bbFactory = ({
store, store,
frontendDefinition,
componentLibraries, componentLibraries,
onScreenSlotRendered, onScreenSlotRendered,
}) => { }) => {
const relativeUrl = url => { const apiCall = method => (url, body) => {
if (!frontendDefinition.appRootPath) return url return fetch(url, {
if (
url.startsWith("http:") ||
url.startsWith("https:") ||
url.startsWith("./")
)
return url
return frontendDefinition.appRootPath + "/" + trimSlash(url)
}
const apiCall = method => (url, body) =>
fetch(url, {
method: method, method: method,
headers: { headers: {
"Content-Type": "application/json", "Content-Type": "application/json",
"x-user-agent": "Budibase Builder",
}, },
body: body && JSON.stringify(body), body: body && JSON.stringify(body),
credentials: "same-origin",
}) })
}
const api = { const api = {
post: apiCall("POST"), post: apiCall("POST"),
@ -63,7 +51,6 @@ export const bbFactory = ({
getContext: getContext(treeNode), getContext: getContext(treeNode),
setContext: setContext(treeNode), setContext: setContext(treeNode),
store: store, store: store,
relativeUrl,
api, api,
parent, parent,
} }

View File

@ -6,14 +6,13 @@ import { createApi } from "../api"
export const EVENT_TYPE_MEMBER_NAME = "##eventHandlerType" export const EVENT_TYPE_MEMBER_NAME = "##eventHandlerType"
export const eventHandlers = (rootPath, routeTo) => { export const eventHandlers = routeTo => {
const handler = (parameters, execute) => ({ const handler = (parameters, execute) => ({
execute, execute,
parameters, parameters,
}) })
const api = createApi({ const api = createApi({
rootPath,
setState, setState,
getState: (path, fallback) => getState(path, fallback), getState: (path, fallback) => getState(path, fallback),
}) })

View File

@ -21,13 +21,11 @@ const isMetaProp = propName =>
propName === "_styles" propName === "_styles"
export const createStateManager = ({ export const createStateManager = ({
appRootPath,
frontendDefinition,
componentLibraries, componentLibraries,
onScreenSlotRendered, onScreenSlotRendered,
routeTo, routeTo,
}) => { }) => {
let handlerTypes = eventHandlers(appRootPath, routeTo) let handlerTypes = eventHandlers(routeTo)
let currentState let currentState
const getCurrentState = () => currentState const getCurrentState = () => currentState
@ -35,7 +33,6 @@ export const createStateManager = ({
const bb = bbFactory({ const bb = bbFactory({
store: appStore, store: appStore,
getCurrentState, getCurrentState,
frontendDefinition,
componentLibraries, componentLibraries,
onScreenSlotRendered, onScreenSlotRendered,
}) })

View File

@ -18,7 +18,7 @@ describe("screenRouting", () => {
it("should load correct screen, for initial URL, when appRootPath is something", async () => { it("should load correct screen, for initial URL, when appRootPath is something", async () => {
const { page, screens } = pageWith3Screens() const { page, screens } = pageWith3Screens()
const { dom } = await load(page, screens, "/testApp/screen2", "/testApp") const { dom } = await load(page, screens, "/TEST_APP_ID/screen2", "127.0.0.1")
const rootDiv = dom.window.document.body.children[0] const rootDiv = dom.window.document.body.children[0]
expect(rootDiv.children.length).toBe(1) expect(rootDiv.children.length).toBe(1)
@ -50,8 +50,8 @@ describe("screenRouting", () => {
const { dom, app } = await load( const { dom, app } = await load(
page, page,
screens, screens,
"/testApp/screen2", "/TEST_APP_ID/screen2",
"/testApp" "127.0.0.1"
) )
app.routeTo()("/screen3") app.routeTo()("/screen3")

View File

@ -1,19 +1,33 @@
import { JSDOM } from "jsdom" import jsdom, { JSDOM } from "jsdom"
import { loadBudibase } from "../src/index" import { loadBudibase } from "../src/index"
export const load = async (page, screens, url, appRootPath) => { export const load = async (page, screens, url, host = "test.com") => {
screens = screens || [] screens = screens || []
url = url || "/" url = url || "/"
appRootPath = appRootPath || ""
const fullUrl = `http://${host}${url}`
const cookieJar = new jsdom.CookieJar()
const cookie = `${btoa("{}")}.${btoa('{"appId":"TEST_APP_ID"}')}.signature`
cookieJar.setCookie(
`budibase:token=${cookie};domain=${host};path=/`,
fullUrl,
{
looseMode: false,
},
() => {}
)
const dom = new JSDOM("<!DOCTYPE html><html><body></body><html>", { const dom = new JSDOM("<!DOCTYPE html><html><body></body><html>", {
url: `http://test${url}`, url: fullUrl,
cookieJar,
}) })
autoAssignIds(page.props) autoAssignIds(page.props)
for (let s of screens) { for (let s of screens) {
autoAssignIds(s.props) autoAssignIds(s.props)
} }
setAppDef(dom.window, page, screens) setAppDef(dom.window, page, screens)
addWindowGlobals(dom.window, page, screens, appRootPath, { addWindowGlobals(dom.window, page, screens, {
hierarchy: {}, hierarchy: {},
actions: [], actions: [],
triggers: [], triggers: [],
@ -27,11 +41,10 @@ export const load = async (page, screens, url, appRootPath) => {
return { dom, app } return { dom, app }
} }
const addWindowGlobals = (window, page, screens, appRootPath) => { const addWindowGlobals = (window, page, screens) => {
window["##BUDIBASE_FRONTEND_DEFINITION##"] = { window["##BUDIBASE_FRONTEND_DEFINITION##"] = {
page, page,
screens, screens,
appRootPath,
} }
} }
@ -88,7 +101,6 @@ const setAppDef = (window, page, screens) => {
componentLibraries: [], componentLibraries: [],
page, page,
screens, screens,
appRootPath: "",
} }
} }

View File

@ -14,12 +14,6 @@ export default async () => {
componentLibraries["@budibase/standard-components"] = standardcomponents componentLibraries["@budibase/standard-components"] = standardcomponents
const appDef = { hierarchy: {}, actions: {} } const appDef = { hierarchy: {}, actions: {} }
const user = { name: "yeo", permissions: [] } const user = { name: "yeo", permissions: [] }
const { initialisePage } = createApp( const { initialisePage } = createApp(componentLibraries, {}, appDef, user, {})
componentLibraries,
{ appRootPath: "" },
appDef,
user,
{}
)
return initialisePage return initialisePage
} }

View File

@ -5,9 +5,6 @@ COUCH_DB_URL={{couchDbUrl}}
# identifies a client database - i.e. group of apps # identifies a client database - i.e. group of apps
CLIENT_ID={{clientId}} CLIENT_ID={{clientId}}
# Full access API key for server
ADMIN_SECRET={{adminSecret}}
# used to create cookie hashes # used to create cookie hashes
JWT_SECRET={{cookieKey1}} JWT_SECRET={{cookieKey1}}

View File

@ -4,5 +4,4 @@ process.env.NODE_ENV = "jest"
process.env.JWT_SECRET = "test-jwtsecret" process.env.JWT_SECRET = "test-jwtsecret"
process.env.CLIENT_ID = "test-client-id" process.env.CLIENT_ID = "test-client-id"
process.env.BUDIBASE_DIR = tmpdir("budibase-unittests") process.env.BUDIBASE_DIR = tmpdir("budibase-unittests")
process.env.ADMIN_SECRET = "test-admin-secret"
process.env.LOG_LEVEL = "silent" process.env.LOG_LEVEL = "silent"

View File

@ -8,7 +8,7 @@ const {
} = require("../../utilities/accessLevels") } = require("../../utilities/accessLevels")
exports.fetch = async function(ctx) { exports.fetch = async function(ctx) {
const db = new CouchDB(ctx.params.instanceId) const db = new CouchDB(ctx.user.instanceId)
const body = await db.query("database/by_type", { const body = await db.query("database/by_type", {
include_docs: true, include_docs: true,
key: ["accesslevel"], key: ["accesslevel"],
@ -19,12 +19,12 @@ exports.fetch = async function(ctx) {
{ {
_id: ADMIN_LEVEL_ID, _id: ADMIN_LEVEL_ID,
name: "Admin", name: "Admin",
permissions: await generateAdminPermissions(ctx.params.instanceId), permissions: await generateAdminPermissions(ctx.user.instanceId),
}, },
{ {
_id: POWERUSER_LEVEL_ID, _id: POWERUSER_LEVEL_ID,
name: "Power User", name: "Power User",
permissions: await generatePowerUserPermissions(ctx.params.instanceId), permissions: await generatePowerUserPermissions(ctx.user.instanceId),
}, },
] ]
@ -32,12 +32,12 @@ exports.fetch = async function(ctx) {
} }
exports.find = async function(ctx) { exports.find = async function(ctx) {
const db = new CouchDB(ctx.params.instanceId) const db = new CouchDB(ctx.user.instanceId)
ctx.body = await db.get(ctx.params.levelId) ctx.body = await db.get(ctx.params.levelId)
} }
exports.update = async function(ctx) { exports.update = async function(ctx) {
const db = new CouchDB(ctx.params.instanceId) const db = new CouchDB(ctx.user.instanceId)
const level = await db.get(ctx.params.levelId) const level = await db.get(ctx.params.levelId)
level.name = ctx.body.name level.name = ctx.body.name
level.permissions = ctx.request.body.permissions level.permissions = ctx.request.body.permissions
@ -48,7 +48,7 @@ exports.update = async function(ctx) {
} }
exports.patch = async function(ctx) { exports.patch = async function(ctx) {
const db = new CouchDB(ctx.params.instanceId) const db = new CouchDB(ctx.user.instanceId)
const level = await db.get(ctx.params.levelId) const level = await db.get(ctx.params.levelId)
const { removedPermissions, addedPermissions, _rev } = ctx.request.body const { removedPermissions, addedPermissions, _rev } = ctx.request.body
@ -84,7 +84,7 @@ exports.patch = async function(ctx) {
} }
exports.create = async function(ctx) { exports.create = async function(ctx) {
const db = new CouchDB(ctx.params.instanceId) const db = new CouchDB(ctx.user.instanceId)
const level = { const level = {
name: ctx.request.body.name, name: ctx.request.body.name,
@ -101,7 +101,7 @@ exports.create = async function(ctx) {
} }
exports.destroy = async function(ctx) { exports.destroy = async function(ctx) {
const db = new CouchDB(ctx.params.instanceId) const db = new CouchDB(ctx.user.instanceId)
await db.remove(ctx.params.levelId, ctx.params.rev) await db.remove(ctx.params.levelId, ctx.params.rev)
ctx.message = `Access Level ${ctx.params.id} deleted successfully` ctx.message = `Access Level ${ctx.params.id} deleted successfully`
ctx.status = 200 ctx.status = 200

View File

@ -9,6 +9,7 @@ const { copy, exists, readFile, writeFile } = require("fs-extra")
const { budibaseAppsDir } = require("../../utilities/budibaseDir") const { budibaseAppsDir } = require("../../utilities/budibaseDir")
const { exec } = require("child_process") const { exec } = require("child_process")
const sqrl = require("squirrelly") const sqrl = require("squirrelly")
const setBuilderToken = require("../../utilities/builder/setBuilderToken")
exports.fetch = async function(ctx) { exports.fetch = async function(ctx) {
const db = new CouchDB(ClientDb.name(getClientId(ctx))) const db = new CouchDB(ClientDb.name(getClientId(ctx)))
@ -25,6 +26,14 @@ exports.fetchAppPackage = async function(ctx) {
const db = new CouchDB(ClientDb.name(clientId)) const db = new CouchDB(ClientDb.name(clientId))
const application = await db.get(ctx.params.applicationId) const application = await db.get(ctx.params.applicationId)
ctx.body = await getPackageForBuilder(ctx.config, application) ctx.body = await getPackageForBuilder(ctx.config, application)
/*
instance is hardcoded now - this can only change when we move
pages and screens into the database
*/
const devInstance = application.instances.find(
i => i.name === `dev-${clientId}`
)
setBuilderToken(ctx, ctx.params.applicationId, devInstance._id)
} }
exports.create = async function(ctx) { exports.create = async function(ctx) {
@ -37,10 +46,12 @@ exports.create = async function(ctx) {
const appId = newid() const appId = newid()
// insert an appId -> clientId lookup // insert an appId -> clientId lookup
const masterDb = new CouchDB("clientAppLookup") const masterDb = new CouchDB("clientAppLookup")
await masterDb.put({ await masterDb.put({
_id: appId, _id: appId,
clientId, clientId,
}) })
const db = new CouchDB(ClientDb.name(clientId)) const db = new CouchDB(ClientDb.name(clientId))
const newApplication = { const newApplication = {
@ -56,18 +67,18 @@ exports.create = async function(ctx) {
description: ctx.request.body.description, description: ctx.request.body.description,
} }
const { rev } = await db.post(newApplication) const { rev } = await db.put(newApplication)
newApplication._rev = rev newApplication._rev = rev
const createInstCtx = { const createInstCtx = {
params: { user: {
applicationId: newApplication._id, appId: newApplication._id,
}, },
request: { request: {
body: { name: `dev-${clientId}` }, body: { name: `dev-${clientId}` },
}, },
} }
await instanceController.create(createInstCtx) await instanceController.create(createInstCtx)
newApplication.instances.push(createInstCtx.body)
if (ctx.isDev) { if (ctx.isDev) {
const newAppFolder = await createEmptyAppPackage(ctx, newApplication) const newAppFolder = await createEmptyAppPackage(ctx, newApplication)

View File

@ -4,25 +4,31 @@ const ClientDb = require("../../db/clientDb")
const bcrypt = require("../../utilities/bcrypt") const bcrypt = require("../../utilities/bcrypt")
exports.authenticate = async ctx => { exports.authenticate = async ctx => {
if (!ctx.user.appId) ctx.throw(400, "No appId")
const { username, password } = ctx.request.body const { username, password } = ctx.request.body
if (!username) ctx.throw(400, "Username Required.") if (!username) ctx.throw(400, "Username Required.")
if (!password) ctx.throw(400, "Password Required") if (!password) ctx.throw(400, "Password Required")
const masterDb = new CouchDB("clientAppLookup") const masterDb = new CouchDB("clientAppLookup")
const { clientId } = await masterDb.get(ctx.params.appId)
const { clientId } = await masterDb.get(ctx.user.appId)
if (!clientId) { if (!clientId) {
ctx.throw(400, "ClientId not suplied") ctx.throw(400, "ClientId not suplied")
} }
// find the instance that the user is associated with // find the instance that the user is associated with
const db = new CouchDB(ClientDb.name(clientId)) const db = new CouchDB(ClientDb.name(clientId))
const appId = ctx.params.appId const app = await db.get(ctx.user.appId)
const app = await db.get(appId)
const instanceId = app.userInstanceMap[username] const instanceId = app.userInstanceMap[username]
if (!instanceId) if (!instanceId)
ctx.throw(500, "User is not associated with an instance of app", appId) ctx.throw(
500,
"User is not associated with an instance of app",
ctx.user.appId
)
// Check the user exists in the instance DB by username // Check the user exists in the instance DB by username
const instanceDb = new CouchDB(instanceId) const instanceDb = new CouchDB(instanceId)
@ -41,16 +47,22 @@ exports.authenticate = async ctx => {
const payload = { const payload = {
userId: dbUser._id, userId: dbUser._id,
accessLevelId: dbUser.accessLevelId, accessLevelId: dbUser.accessLevelId,
instanceId: instanceId, appId: ctx.user.appId,
instanceId,
} }
const token = jwt.sign(payload, ctx.config.jwtSecret, { const token = jwt.sign(payload, ctx.config.jwtSecret, {
expiresIn: "1 day", expiresIn: "1 day",
}) })
const ONE_DAY_FROM_NOW = new Date(Date.now() + 24 * 3600) const expires = new Date()
expires.setDate(expires.getDate() + 1)
ctx.cookies.set("budibase:token", token, { expires: ONE_DAY_FROM_NOW }) ctx.cookies.set("budibase:token", token, {
expires,
path: "/",
httpOnly: false,
})
ctx.body = { ctx.body = {
token, token,

View File

@ -4,19 +4,19 @@ const newid = require("../../db/newid")
exports.create = async function(ctx) { exports.create = async function(ctx) {
const instanceName = ctx.request.body.name const instanceName = ctx.request.body.name
const appShortId = ctx.params.applicationId.substring(0, 7) const { appId } = ctx.user
const appShortId = appId.substring(0, 7)
const instanceId = `inst_${appShortId}_${newid()}` const instanceId = `inst_${appShortId}_${newid()}`
const { applicationId } = ctx.params
const masterDb = new CouchDB("clientAppLookup") const masterDb = new CouchDB("clientAppLookup")
const { clientId } = await masterDb.get(applicationId) const { clientId } = await masterDb.get(appId)
const db = new CouchDB(instanceId) const db = new CouchDB(instanceId)
await db.put({ await db.put({
_id: "_design/database", _id: "_design/database",
metadata: { metadata: {
clientId, clientId,
applicationId, applicationId: appId,
}, },
views: { views: {
by_username: { by_username: {
@ -46,7 +46,7 @@ exports.create = async function(ctx) {
// Add the new instance under the app clientDB // Add the new instance under the app clientDB
const clientDb = new CouchDB(client.name(clientId)) const clientDb = new CouchDB(client.name(clientId))
const budibaseApp = await clientDb.get(applicationId) const budibaseApp = await clientDb.get(appId)
const instance = { _id: instanceId, name: instanceName } const instance = { _id: instanceId, name: instanceName }
budibaseApp.instances.push(instance) budibaseApp.instances.push(instance)
await clientDb.put(budibaseApp) await clientDb.put(budibaseApp)

View File

@ -2,7 +2,7 @@ const CouchDB = require("../../db")
const newid = require("../../db/newid") const newid = require("../../db/newid")
exports.fetch = async function(ctx) { exports.fetch = async function(ctx) {
const db = new CouchDB(ctx.params.instanceId) const db = new CouchDB(ctx.user.instanceId)
const body = await db.query("database/by_type", { const body = await db.query("database/by_type", {
include_docs: true, include_docs: true,
key: ["model"], key: ["model"],
@ -11,13 +11,13 @@ exports.fetch = async function(ctx) {
} }
exports.find = async function(ctx) { exports.find = async function(ctx) {
const db = new CouchDB(ctx.params.instanceId) const db = new CouchDB(ctx.user.instanceId)
const model = await db.get(ctx.params.id) const model = await db.get(ctx.params.id)
ctx.body = model ctx.body = model
} }
exports.create = async function(ctx) { exports.create = async function(ctx) {
const db = new CouchDB(ctx.params.instanceId) const db = new CouchDB(ctx.user.instanceId)
const newModel = { const newModel = {
type: "model", type: "model",
...ctx.request.body, ...ctx.request.body,
@ -65,7 +65,7 @@ exports.create = async function(ctx) {
exports.update = async function() {} exports.update = async function() {}
exports.destroy = async function(ctx) { exports.destroy = async function(ctx) {
const db = new CouchDB(ctx.params.instanceId) const db = new CouchDB(ctx.user.instanceId)
const modelToDelete = await db.get(ctx.params.modelId) const modelToDelete = await db.get(ctx.params.modelId)

View File

@ -3,7 +3,7 @@ const validateJs = require("validate.js")
const newid = require("../../db/newid") const newid = require("../../db/newid")
exports.save = async function(ctx) { exports.save = async function(ctx) {
const db = new CouchDB(ctx.params.instanceId) const db = new CouchDB(ctx.user.instanceId)
const record = ctx.request.body const record = ctx.request.body
record.modelId = ctx.params.modelId record.modelId = ctx.params.modelId
@ -46,7 +46,7 @@ exports.save = async function(ctx) {
ctx.eventEmitter && ctx.eventEmitter &&
ctx.eventEmitter.emit(`record:save`, { ctx.eventEmitter.emit(`record:save`, {
record, record,
instanceId: ctx.params.instanceId, instanceId: ctx.user.instanceId,
}) })
ctx.body = record ctx.body = record
ctx.status = 200 ctx.status = 200
@ -54,7 +54,7 @@ exports.save = async function(ctx) {
} }
exports.fetchView = async function(ctx) { exports.fetchView = async function(ctx) {
const db = new CouchDB(ctx.params.instanceId) const db = new CouchDB(ctx.user.instanceId)
const response = await db.query(`database/${ctx.params.viewName}`, { const response = await db.query(`database/${ctx.params.viewName}`, {
include_docs: true, include_docs: true,
}) })
@ -62,7 +62,7 @@ exports.fetchView = async function(ctx) {
} }
exports.fetchModelRecords = async function(ctx) { exports.fetchModelRecords = async function(ctx) {
const db = new CouchDB(ctx.params.instanceId) const db = new CouchDB(ctx.user.instanceId)
const response = await db.query(`database/all_${ctx.params.modelId}`, { const response = await db.query(`database/all_${ctx.params.modelId}`, {
include_docs: true, include_docs: true,
}) })
@ -70,7 +70,7 @@ exports.fetchModelRecords = async function(ctx) {
} }
exports.search = async function(ctx) { exports.search = async function(ctx) {
const db = new CouchDB(ctx.params.instanceId) const db = new CouchDB(ctx.user.instanceId)
const response = await db.allDocs({ const response = await db.allDocs({
include_docs: true, include_docs: true,
...ctx.request.body, ...ctx.request.body,
@ -79,7 +79,7 @@ exports.search = async function(ctx) {
} }
exports.find = async function(ctx) { exports.find = async function(ctx) {
const db = new CouchDB(ctx.params.instanceId) const db = new CouchDB(ctx.user.instanceId)
const record = await db.get(ctx.params.recordId) const record = await db.get(ctx.params.recordId)
if (record.modelId !== ctx.params.modelId) { if (record.modelId !== ctx.params.modelId) {
ctx.throw(400, "Supplied modelId doe not match the record's modelId") ctx.throw(400, "Supplied modelId doe not match the record's modelId")
@ -89,7 +89,7 @@ exports.find = async function(ctx) {
} }
exports.destroy = async function(ctx) { exports.destroy = async function(ctx) {
const db = new CouchDB(ctx.params.instanceId) const db = new CouchDB(ctx.user.instanceId)
const record = await db.get(ctx.params.recordId) const record = await db.get(ctx.params.recordId)
if (record.modelId !== ctx.params.modelId) { if (record.modelId !== ctx.params.modelId) {
ctx.throw(400, "Supplied modelId doe not match the record's modelId") ctx.throw(400, "Supplied modelId doe not match the record's modelId")
@ -101,7 +101,7 @@ exports.destroy = async function(ctx) {
exports.validate = async function(ctx) { exports.validate = async function(ctx) {
const errors = await validate({ const errors = await validate({
instanceId: ctx.params.instanceId, instanceId: ctx.user.instanceId,
modelId: ctx.params.modelId, modelId: ctx.params.modelId,
record: ctx.request.body, record: ctx.request.body,
}) })

View File

@ -4,11 +4,15 @@ const {
budibaseAppsDir, budibaseAppsDir,
budibaseTempDir, budibaseTempDir,
} = require("../../utilities/budibaseDir") } = require("../../utilities/budibaseDir")
const env = require("../../environment") const setBuilderToken = require("../../utilities/builder/setBuilderToken")
const { ANON_LEVEL_ID } = require("../../utilities/accessLevels")
const jwt = require("jsonwebtoken")
exports.serveBuilder = async function(ctx) { exports.serveBuilder = async function(ctx) {
let builderPath = resolve(__dirname, "../../../builder") let builderPath = resolve(__dirname, "../../../builder")
ctx.cookies.set("builder:token", env.ADMIN_SECRET) if (ctx.file === "index.html") {
setBuilderToken(ctx)
}
await send(ctx, ctx.file, { root: ctx.devPath || builderPath }) await send(ctx, ctx.file, { root: ctx.devPath || builderPath })
} }
@ -20,6 +24,33 @@ exports.serveApp = async function(ctx) {
"public", "public",
ctx.isAuthenticated ? "main" : "unauthenticated" ctx.isAuthenticated ? "main" : "unauthenticated"
) )
// only set the appId cookie for /appId .. we COULD check for valid appIds
// but would like to avoid that DB hit
const looksLikeAppId = /^[0-9a-f]{32}$/.test(ctx.params.appId)
if (looksLikeAppId && !ctx.isAuthenticated) {
const anonUser = {
userId: "ANON",
accessLevelId: ANON_LEVEL_ID,
appId: ctx.params.appId,
}
const anonToken = jwt.sign(anonUser, ctx.config.jwtSecret)
ctx.cookies.set("budibase:token", anonToken, {
path: "/",
httpOnly: false,
})
}
await send(ctx, ctx.file || "index.html", { root: ctx.devPath || appPath })
}
exports.serveAppAsset = async function(ctx) {
// default to homedir
const appPath = resolve(
budibaseAppsDir(),
ctx.user.appId,
"public",
ctx.isAuthenticated ? "main" : "unauthenticated"
)
await send(ctx, ctx.file, { root: ctx.devPath || appPath }) await send(ctx, ctx.file, { root: ctx.devPath || appPath })
} }
@ -28,7 +59,7 @@ exports.serveComponentLibrary = async function(ctx) {
// default to homedir // default to homedir
let componentLibraryPath = resolve( let componentLibraryPath = resolve(
budibaseAppsDir(), budibaseAppsDir(),
ctx.params.appId, ctx.user.appId,
"node_modules", "node_modules",
decodeURI(ctx.query.library), decodeURI(ctx.query.library),
"dist" "dist"

View File

@ -8,7 +8,7 @@ const {
} = require("../../utilities/accessLevels") } = require("../../utilities/accessLevels")
exports.fetch = async function(ctx) { exports.fetch = async function(ctx) {
const database = new CouchDB(ctx.params.instanceId) const database = new CouchDB(ctx.user.instanceId)
const data = await database.query("database/by_type", { const data = await database.query("database/by_type", {
include_docs: true, include_docs: true,
key: ["user"], key: ["user"],
@ -18,7 +18,7 @@ exports.fetch = async function(ctx) {
} }
exports.create = async function(ctx) { exports.create = async function(ctx) {
const database = new CouchDB(ctx.params.instanceId) const database = new CouchDB(ctx.user.instanceId)
const appId = (await database.get("_design/database")).metadata.applicationId const appId = (await database.get("_design/database")).metadata.applicationId
const { username, password, name, accessLevelId } = ctx.request.body const { username, password, name, accessLevelId } = ctx.request.body
@ -50,7 +50,7 @@ exports.create = async function(ctx) {
app.userInstanceMap = { app.userInstanceMap = {
...app.userInstanceMap, ...app.userInstanceMap,
[username]: ctx.params.instanceId, [username]: ctx.user.instanceId,
} }
await db.put(app) await db.put(app)
@ -66,14 +66,14 @@ exports.create = async function(ctx) {
exports.update = async function() {} exports.update = async function() {}
exports.destroy = async function(ctx) { exports.destroy = async function(ctx) {
const database = new CouchDB(ctx.params.instanceId) const database = new CouchDB(ctx.user.instanceId)
await database.destroy(getUserId(ctx.params.username)) await database.destroy(getUserId(ctx.params.username))
ctx.message = `User ${ctx.params.username} deleted.` ctx.message = `User ${ctx.params.username} deleted.`
ctx.status = 200 ctx.status = 200
} }
exports.find = async function(ctx) { exports.find = async function(ctx) {
const database = new CouchDB(ctx.params.instanceId) const database = new CouchDB(ctx.user.instanceId)
const user = await database.get(getUserId(ctx.params.username)) const user = await database.get(getUserId(ctx.params.username))
ctx.body = { ctx.body = {
username: user.username, username: user.username,

View File

@ -3,7 +3,7 @@ const CouchDB = require("../../db")
const controller = { const controller = {
query: async () => {}, query: async () => {},
fetch: async ctx => { fetch: async ctx => {
const db = new CouchDB(ctx.params.instanceId) const db = new CouchDB(ctx.user.instanceId)
const designDoc = await db.get("_design/database") const designDoc = await db.get("_design/database")
const response = [] const response = []
@ -24,7 +24,7 @@ const controller = {
ctx.body = response ctx.body = response
}, },
create: async ctx => { create: async ctx => {
const db = new CouchDB(ctx.params.instanceId) const db = new CouchDB(ctx.user.instanceId)
const newView = ctx.request.body const newView = ctx.request.body
const designDoc = await db.get("_design/database") const designDoc = await db.get("_design/database")
@ -38,7 +38,7 @@ const controller = {
ctx.message = `View ${newView.name} created successfully.` ctx.message = `View ${newView.name} created successfully.`
}, },
destroy: async ctx => { destroy: async ctx => {
const db = new CouchDB(ctx.params.instanceId) const db = new CouchDB(ctx.user.instanceId)
ctx.body = await db.destroy(ctx.params.userId) ctx.body = await db.destroy(ctx.params.userId)
}, },
} }

View File

@ -2,7 +2,7 @@ const CouchDB = require("../../../db")
const newid = require("../../../db/newid") const newid = require("../../../db/newid")
exports.create = async function(ctx) { exports.create = async function(ctx) {
const db = new CouchDB(ctx.params.instanceId) const db = new CouchDB(ctx.user.instanceId)
const workflow = ctx.request.body const workflow = ctx.request.body
workflow._id = newid() workflow._id = newid()
@ -22,7 +22,7 @@ exports.create = async function(ctx) {
} }
exports.update = async function(ctx) { exports.update = async function(ctx) {
const db = new CouchDB(ctx.params.instanceId) const db = new CouchDB(ctx.user.instanceId)
const workflow = ctx.request.body const workflow = ctx.request.body
const response = await db.put(workflow) const response = await db.put(workflow)
@ -40,7 +40,7 @@ exports.update = async function(ctx) {
} }
exports.fetch = async function(ctx) { exports.fetch = async function(ctx) {
const db = new CouchDB(ctx.params.instanceId) const db = new CouchDB(ctx.user.instanceId)
const response = await db.query(`database/by_type`, { const response = await db.query(`database/by_type`, {
key: ["workflow"], key: ["workflow"],
include_docs: true, include_docs: true,
@ -69,6 +69,6 @@ exports.fetchActionScript = async function(ctx) {
} }
exports.destroy = async function(ctx) { exports.destroy = async function(ctx) {
const db = new CouchDB(ctx.params.instanceId) const db = new CouchDB(ctx.user.instanceId)
ctx.body = await db.remove(ctx.params.id, ctx.params.rev) ctx.body = await db.remove(ctx.params.id, ctx.params.rev)
} }

View File

@ -4,11 +4,11 @@ const controller = require("../controllers/accesslevel")
const router = Router() const router = Router()
router router
.post("/api/:instanceId/accesslevels", controller.create) .post("/api/accesslevels", controller.create)
.put("/api/:instanceId/accesslevels", controller.update) .put("/api/accesslevels", controller.update)
.get("/api/:instanceId/accesslevels", controller.fetch) .get("/api/accesslevels", controller.fetch)
.get("/api/:instanceId/accesslevels/:levelId", controller.find) .get("/api/accesslevels/:levelId", controller.find)
.delete("/api/:instanceId/accesslevels/:levelId/:rev", controller.destroy) .delete("/api/accesslevels/:levelId/:rev", controller.destroy)
.patch("/api/:instanceId/accesslevels/:levelId", controller.patch) .patch("/api/accesslevels/:levelId", controller.patch)
module.exports = router module.exports = router

View File

@ -3,6 +3,6 @@ const controller = require("../controllers/auth")
const router = Router() const router = Router()
router.post("/:appId/api/authenticate", controller.authenticate) router.post("/api/authenticate", controller.authenticate)
module.exports = router module.exports = router

View File

@ -6,7 +6,7 @@ const { BUILDER } = require("../../utilities/accessLevels")
const router = Router() const router = Router()
router router
.post("/api/:applicationId/instances", authorized(BUILDER), controller.create) .post("/api/instances", authorized(BUILDER), controller.create)
.delete("/api/instances/:instanceId", authorized(BUILDER), controller.destroy) .delete("/api/instances/:instanceId", authorized(BUILDER), controller.destroy)
module.exports = router module.exports = router

View File

@ -1,17 +1,21 @@
const Router = require("@koa/router") const Router = require("@koa/router")
const modelController = require("../controllers/model") const modelController = require("../controllers/model")
const authorized = require("../../middleware/authorized") const authorized = require("../../middleware/authorized")
const { BUILDER } = require("../../utilities/accessLevels") const { BUILDER, READ_MODEL } = require("../../utilities/accessLevels")
const router = Router() const router = Router()
router router
.get("/api/:instanceId/models", authorized(BUILDER), modelController.fetch) .get("/api/models", authorized(BUILDER), modelController.fetch)
.get("/api/:instanceId/models/:id", authorized(BUILDER), modelController.find) .get(
.post("/api/:instanceId/models", authorized(BUILDER), modelController.create) "/api/models/:id",
authorized(READ_MODEL, ctx => ctx.params.id),
modelController.find
)
.post("/api/models", authorized(BUILDER), modelController.create)
// .patch("/api/:instanceId/models", controller.update) // .patch("/api/:instanceId/models", controller.update)
.delete( .delete(
"/api/:instanceId/models/:modelId/:revId", "/api/models/:modelId/:revId",
authorized(BUILDER), authorized(BUILDER),
modelController.destroy modelController.destroy
) )

View File

@ -7,28 +7,28 @@ const router = Router()
router router
.get( .get(
"/api/:instanceId/:modelId/records", "/api/:modelId/records",
authorized(READ_MODEL, ctx => ctx.params.modelId), authorized(READ_MODEL, ctx => ctx.params.modelId),
recordController.fetchModelRecords recordController.fetchModelRecords
) )
.get( .get(
"/api/:instanceId/:modelId/records/:recordId", "/api/:modelId/records/:recordId",
authorized(READ_MODEL, ctx => ctx.params.modelId), authorized(READ_MODEL, ctx => ctx.params.modelId),
recordController.find recordController.find
) )
.post("/api/:instanceId/records/search", recordController.search) .post("/api/records/search", recordController.search)
.post( .post(
"/api/:instanceId/:modelId/records", "/api/:modelId/records",
authorized(WRITE_MODEL, ctx => ctx.params.modelId), authorized(WRITE_MODEL, ctx => ctx.params.modelId),
recordController.save recordController.save
) )
.post( .post(
"/api/:instanceId/:modelId/records/validate", "/api/:modelId/records/validate",
authorized(WRITE_MODEL, ctx => ctx.params.modelId), authorized(WRITE_MODEL, ctx => ctx.params.modelId),
recordController.validate recordController.validate
) )
.delete( .delete(
"/api/:instanceId/:modelId/records/:recordId/:revId", "/api/:modelId/records/:recordId/:revId",
authorized(WRITE_MODEL, ctx => ctx.params.modelId), authorized(WRITE_MODEL, ctx => ctx.params.modelId),
recordController.destroy recordController.destroy
) )

View File

@ -6,12 +6,8 @@ const { BUILDER } = require("../../utilities/accessLevels")
const router = Router() const router = Router()
router router
.get("/api/:instanceId/screens", authorized(BUILDER), controller.fetch) .get("/api/screens", authorized(BUILDER), controller.fetch)
.post("/api/:instanceId/screens", authorized(BUILDER), controller.save) .post("/api/screens", authorized(BUILDER), controller.save)
.delete( .delete("/api/:screenId/:revId", authorized(BUILDER), controller.destroy)
"/api/:instanceId/:screenId/:revId",
authorized(BUILDER),
controller.destroy
)
module.exports = router module.exports = router

View File

@ -21,7 +21,8 @@ if (env.NODE_ENV !== "production") {
} }
router router
.get("/:appId/componentlibrary", controller.serveComponentLibrary) .get("/componentlibrary", controller.serveComponentLibrary)
.get("/:appId/:file*", controller.serveApp) .get("/assets/:file*", controller.serveAppAsset)
.get("/:appId/:path*", controller.serveApp)
module.exports = router module.exports = router

View File

@ -36,17 +36,17 @@ describe("/accesslevels", () => {
beforeEach(async () => { beforeEach(async () => {
instanceId = (await createInstance(request, appId))._id instanceId = (await createInstance(request, appId))._id
model = await createModel(request, instanceId) model = await createModel(request, appId, instanceId)
view = await createView(request, instanceId) view = await createView(request, appId, instanceId)
}) })
describe("create", () => { describe("create", () => {
it("returns a success message when level is successfully created", async () => { it("returns a success message when level is successfully created", async () => {
const res = await request const res = await request
.post(`/api/${instanceId}/accesslevels`) .post(`/api/accesslevels`)
.send({ name: "user" }) .send({ name: "user" })
.set(defaultHeaders) .set(defaultHeaders(appId, instanceId))
.expect('Content-Type', /json/) .expect('Content-Type', /json/)
.expect(200) .expect(200)
@ -62,17 +62,17 @@ describe("/accesslevels", () => {
it("should list custom levels, plus 2 default levels", async () => { it("should list custom levels, plus 2 default levels", async () => {
const createRes = await request const createRes = await request
.post(`/api/${instanceId}/accesslevels`) .post(`/api/accesslevels`)
.send({ name: "user", permissions: [ { itemId: model._id, name: READ_MODEL }] }) .send({ name: "user", permissions: [ { itemId: model._id, name: READ_MODEL }] })
.set(defaultHeaders) .set(defaultHeaders(appId, instanceId))
.expect('Content-Type', /json/) .expect('Content-Type', /json/)
.expect(200) .expect(200)
const customLevel = createRes.body const customLevel = createRes.body
const res = await request const res = await request
.get(`/api/${instanceId}/accesslevels`) .get(`/api/accesslevels`)
.set(defaultHeaders) .set(defaultHeaders(appId, instanceId))
.expect('Content-Type', /json/) .expect('Content-Type', /json/)
.expect(200) .expect(200)
@ -95,22 +95,22 @@ describe("/accesslevels", () => {
describe("destroy", () => { describe("destroy", () => {
it("should delete custom access level", async () => { it("should delete custom access level", async () => {
const createRes = await request const createRes = await request
.post(`/api/${instanceId}/accesslevels`) .post(`/api/accesslevels`)
.send({ name: "user", permissions: [ { itemId: model._id, name: READ_MODEL } ] }) .send({ name: "user", permissions: [ { itemId: model._id, name: READ_MODEL } ] })
.set(defaultHeaders) .set(defaultHeaders(appId, instanceId))
.expect('Content-Type', /json/) .expect('Content-Type', /json/)
.expect(200) .expect(200)
const customLevel = createRes.body const customLevel = createRes.body
await request await request
.delete(`/api/${instanceId}/accesslevels/${customLevel._id}/${customLevel._rev}`) .delete(`/api/accesslevels/${customLevel._id}/${customLevel._rev}`)
.set(defaultHeaders) .set(defaultHeaders(appId, instanceId))
.expect(200) .expect(200)
await request await request
.get(`/api/${instanceId}/accesslevels/${customLevel._id}`) .get(`/api/accesslevels/${customLevel._id}`)
.set(defaultHeaders) .set(defaultHeaders(appId, instanceId))
.expect(404) .expect(404)
}) })
}) })
@ -118,27 +118,27 @@ describe("/accesslevels", () => {
describe("patch", () => { describe("patch", () => {
it("should add given permissions", async () => { it("should add given permissions", async () => {
const createRes = await request const createRes = await request
.post(`/api/${instanceId}/accesslevels`) .post(`/api/accesslevels`)
.send({ name: "user", permissions: [ { itemId: model._id, name: READ_MODEL }] }) .send({ name: "user", permissions: [ { itemId: model._id, name: READ_MODEL }] })
.set(defaultHeaders) .set(defaultHeaders(appId, instanceId))
.expect('Content-Type', /json/) .expect('Content-Type', /json/)
.expect(200) .expect(200)
const customLevel = createRes.body const customLevel = createRes.body
await request await request
.patch(`/api/${instanceId}/accesslevels/${customLevel._id}`) .patch(`/api/accesslevels/${customLevel._id}`)
.send({ .send({
_rev: customLevel._rev, _rev: customLevel._rev,
addedPermissions: [ { itemId: model._id, name: WRITE_MODEL } ] addedPermissions: [ { itemId: model._id, name: WRITE_MODEL } ]
}) })
.set(defaultHeaders) .set(defaultHeaders(appId, instanceId))
.expect('Content-Type', /json/) .expect('Content-Type', /json/)
.expect(200) .expect(200)
const finalRes = await request const finalRes = await request
.get(`/api/${instanceId}/accesslevels/${customLevel._id}`) .get(`/api/accesslevels/${customLevel._id}`)
.set(defaultHeaders) .set(defaultHeaders(appId, instanceId))
.expect(200) .expect(200)
expect(finalRes.body.permissions.length).toBe(2) expect(finalRes.body.permissions.length).toBe(2)
@ -148,7 +148,7 @@ describe("/accesslevels", () => {
it("should remove given permissions", async () => { it("should remove given permissions", async () => {
const createRes = await request const createRes = await request
.post(`/api/${instanceId}/accesslevels`) .post(`/api/accesslevels`)
.send({ .send({
name: "user", name: "user",
permissions: [ permissions: [
@ -156,25 +156,25 @@ describe("/accesslevels", () => {
{ itemId: model._id, name: WRITE_MODEL }, { itemId: model._id, name: WRITE_MODEL },
] ]
}) })
.set(defaultHeaders) .set(defaultHeaders(appId, instanceId))
.expect('Content-Type', /json/) .expect('Content-Type', /json/)
.expect(200) .expect(200)
const customLevel = createRes.body const customLevel = createRes.body
await request await request
.patch(`/api/${instanceId}/accesslevels/${customLevel._id}`) .patch(`/api/accesslevels/${customLevel._id}`)
.send({ .send({
_rev: customLevel._rev, _rev: customLevel._rev,
removedPermissions: [ { itemId: model._id, name: WRITE_MODEL }] removedPermissions: [ { itemId: model._id, name: WRITE_MODEL }]
}) })
.set(defaultHeaders) .set(defaultHeaders(appId, instanceId))
.expect('Content-Type', /json/) .expect('Content-Type', /json/)
.expect(200) .expect(200)
const finalRes = await request const finalRes = await request
.get(`/api/${instanceId}/accesslevels/${customLevel._id}`) .get(`/api/accesslevels/${customLevel._id}`)
.set(defaultHeaders) .set(defaultHeaders(appId, instanceId))
.expect(200) .expect(200)
expect(finalRes.body.permissions.length).toBe(1) expect(finalRes.body.permissions.length).toBe(1)

View File

@ -34,7 +34,7 @@ describe("/applications", () => {
const res = await request const res = await request
.post("/api/applications") .post("/api/applications")
.send({ name: "My App" }) .send({ name: "My App" })
.set(defaultHeaders) .set(defaultHeaders())
.expect('Content-Type', /json/) .expect('Content-Type', /json/)
.expect(200) .expect(200)
expect(res.res.statusMessage).toEqual("Application My App created successfully") expect(res.res.statusMessage).toEqual("Application My App created successfully")
@ -49,6 +49,7 @@ describe("/applications", () => {
method: "POST", method: "POST",
url: `/api/applications`, url: `/api/applications`,
instanceId: instance._id, instanceId: instance._id,
appId: otherApplication._id,
body: { name: "My App" } body: { name: "My App" }
}) })
}) })
@ -63,7 +64,7 @@ describe("/applications", () => {
const res = await request const res = await request
.get("/api/applications") .get("/api/applications")
.set(defaultHeaders) .set(defaultHeaders())
.expect('Content-Type', /json/) .expect('Content-Type', /json/)
.expect(200) .expect(200)
@ -77,13 +78,13 @@ describe("/applications", () => {
const blah = await request const blah = await request
.post("/api/applications") .post("/api/applications")
.send({ name: "app2", clientId: "new_client"}) .send({ name: "app2", clientId: "new_client"})
.set(defaultHeaders) .set(defaultHeaders())
.expect('Content-Type', /json/) .expect('Content-Type', /json/)
//.expect(200) //.expect(200)
const client1Res = await request const client1Res = await request
.get(`/api/applications?clientId=${TEST_CLIENT_ID}`) .get(`/api/applications?clientId=${TEST_CLIENT_ID}`)
.set(defaultHeaders) .set(defaultHeaders())
.expect('Content-Type', /json/) .expect('Content-Type', /json/)
.expect(200) .expect(200)
@ -92,7 +93,7 @@ describe("/applications", () => {
const client2Res = await request const client2Res = await request
.get(`/api/applications?clientId=new_client`) .get(`/api/applications?clientId=new_client`)
.set(defaultHeaders) .set(defaultHeaders())
.expect('Content-Type', /json/) .expect('Content-Type', /json/)
.expect(200) .expect(200)
@ -109,6 +110,7 @@ describe("/applications", () => {
method: "GET", method: "GET",
url: `/api/applications`, url: `/api/applications`,
instanceId: instance._id, instanceId: instance._id,
appId: otherApplication._id,
}) })
}) })
}) })

View File

@ -4,8 +4,12 @@ const supertest = require("supertest")
const app = require("../../../app") const app = require("../../../app")
const { const {
POWERUSER_LEVEL_ID, POWERUSER_LEVEL_ID,
ANON_LEVEL_ID,
BUILDER_LEVEL_ID,
generateAdminPermissions, generateAdminPermissions,
} = require("../../../utilities/accessLevels") } = require("../../../utilities/accessLevels")
const jwt = require("jsonwebtoken")
const env = require("../../../environment")
const TEST_CLIENT_ID = "test-client-id" const TEST_CLIENT_ID = "test-client-id"
@ -20,13 +24,23 @@ exports.supertest = async () => {
return { request, server } return { request, server }
} }
exports.defaultHeaders = { exports.defaultHeaders = (appId, instanceId) => {
Accept: "application/json", const builderUser = {
Cookie: ["builder:token=test-admin-secret"], userId: "BUILDER",
"x-user-agent": "Budibase Builder", accessLevelId: BUILDER_LEVEL_ID,
appId,
instanceId,
}
const builderToken = jwt.sign(builderUser, env.JWT_SECRET)
return {
Accept: "application/json",
Cookie: [`builder:token=${builderToken}`],
}
} }
exports.createModel = async (request, instanceId, model) => { exports.createModel = async (request, appId, instanceId, model) => {
model = model || { model = model || {
name: "TestModel", name: "TestModel",
type: "model", type: "model",
@ -42,20 +56,20 @@ exports.createModel = async (request, instanceId, model) => {
} }
const res = await request const res = await request
.post(`/api/${instanceId}/models`) .post(`/api/models`)
.set(exports.defaultHeaders) .set(exports.defaultHeaders(appId, instanceId))
.send(model) .send(model)
return res.body return res.body
} }
exports.createView = async (request, instanceId, view) => { exports.createView = async (request, appId, instanceId, view) => {
view = view || { view = view || {
map: "function(doc) { emit(doc[doc.key], doc._id); } ", map: "function(doc) { emit(doc[doc.key], doc._id); } ",
} }
const res = await request const res = await request
.post(`/api/${instanceId}/views`) .post(`/api/views`)
.set(exports.defaultHeaders) .set(exports.defaultHeaders(appId, instanceId))
.send(view) .send(view)
return res.body return res.body
} }
@ -65,10 +79,10 @@ exports.createClientDatabase = async id => await create(id || TEST_CLIENT_ID)
exports.createApplication = async (request, name = "test_application") => { exports.createApplication = async (request, name = "test_application") => {
const res = await request const res = await request
.post("/api/applications") .post("/api/applications")
.set(exports.defaultHeaders)
.send({ .send({
name, name,
}) })
.set(exports.defaultHeaders())
return res.body return res.body
} }
@ -76,23 +90,24 @@ exports.destroyClientDatabase = async () => await destroy(TEST_CLIENT_ID)
exports.createInstance = async (request, appId) => { exports.createInstance = async (request, appId) => {
const res = await request const res = await request
.post(`/api/${appId}/instances`) .post(`/api/instances`)
.set(exports.defaultHeaders)
.send({ .send({
name: "test-instance", name: "test-instance2",
}) })
.set(exports.defaultHeaders(appId))
return res.body return res.body
} }
exports.createUser = async ( exports.createUser = async (
request, request,
appId,
instanceId, instanceId,
username = "babs", username = "babs",
password = "babs_password" password = "babs_password"
) => { ) => {
const res = await request const res = await request
.post(`/api/${instanceId}/users`) .post(`/api/users`)
.set(exports.defaultHeaders) .set(exports.defaultHeaders(appId, instanceId))
.send({ .send({
name: "Bill", name: "Bill",
username, username,
@ -104,6 +119,7 @@ exports.createUser = async (
const createUserWithOnePermission = async ( const createUserWithOnePermission = async (
request, request,
appId,
instanceId, instanceId,
permName, permName,
itemId itemId
@ -115,17 +131,19 @@ const createUserWithOnePermission = async (
return await createUserWithPermissions( return await createUserWithPermissions(
request, request,
appId,
instanceId, instanceId,
permissions, permissions,
"onePermOnlyUser" "onePermOnlyUser"
) )
} }
const createUserWithAdminPermissions = async (request, instanceId) => { const createUserWithAdminPermissions = async (request, appId, instanceId) => {
let permissions = await generateAdminPermissions(instanceId) let permissions = await generateAdminPermissions(instanceId)
return await createUserWithPermissions( return await createUserWithPermissions(
request, request,
appId,
instanceId, instanceId,
permissions, permissions,
"adminUser" "adminUser"
@ -134,6 +152,7 @@ const createUserWithAdminPermissions = async (request, instanceId) => {
const createUserWithAllPermissionExceptOne = async ( const createUserWithAllPermissionExceptOne = async (
request, request,
appId,
instanceId, instanceId,
permName, permName,
itemId itemId
@ -145,6 +164,7 @@ const createUserWithAllPermissionExceptOne = async (
return await createUserWithPermissions( return await createUserWithPermissions(
request, request,
appId,
instanceId, instanceId,
permissions, permissions,
"allPermsExceptOneUser" "allPermsExceptOneUser"
@ -153,19 +173,20 @@ const createUserWithAllPermissionExceptOne = async (
const createUserWithPermissions = async ( const createUserWithPermissions = async (
request, request,
appId,
instanceId, instanceId,
permissions, permissions,
username username
) => { ) => {
const accessRes = await request const accessRes = await request
.post(`/api/${instanceId}/accesslevels`) .post(`/api/accesslevels`)
.send({ name: "TestLevel", permissions }) .send({ name: "TestLevel", permissions })
.set(exports.defaultHeaders) .set(exports.defaultHeaders(appId, instanceId))
const password = `password_${username}` const password = `password_${username}`
await request await request
.post(`/api/${instanceId}/users`) .post(`/api/users`)
.set(exports.defaultHeaders) .set(exports.defaultHeaders(appId, instanceId))
.send({ .send({
name: username, name: username,
username, username,
@ -173,11 +194,20 @@ const createUserWithPermissions = async (
accessLevelId: accessRes.body._id, accessLevelId: accessRes.body._id,
}) })
const db = new CouchDB(instanceId) //const db = new CouchDB(instanceId)
const designDoc = await db.get("_design/database") //const designDoc = await db.get("_design/database")
const anonUser = {
userId: "ANON",
accessLevelId: ANON_LEVEL_ID,
appId: appId,
}
const anonToken = jwt.sign(anonUser, env.JWT_SECRET)
const loginResult = await request const loginResult = await request
.post(`/${designDoc.metadata.applicationId}/api/authenticate`) .post(`/api/authenticate`)
.set({ Cookie: `budibase:token=${anonToken}` })
.send({ username, password }) .send({ username, password })
// returning necessary request headers // returning necessary request headers
@ -192,12 +222,14 @@ exports.testPermissionsForEndpoint = async ({
method, method,
url, url,
body, body,
appId,
instanceId, instanceId,
permissionName, permissionName,
itemId, itemId,
}) => { }) => {
const headers = await createUserWithOnePermission( const headers = await createUserWithOnePermission(
request, request,
appId,
instanceId, instanceId,
permissionName, permissionName,
itemId itemId
@ -209,6 +241,7 @@ exports.testPermissionsForEndpoint = async ({
const noPermsHeaders = await createUserWithAllPermissionExceptOne( const noPermsHeaders = await createUserWithAllPermissionExceptOne(
request, request,
appId,
instanceId, instanceId,
permissionName, permissionName,
itemId itemId
@ -224,9 +257,14 @@ exports.builderEndpointShouldBlockNormalUsers = async ({
method, method,
url, url,
body, body,
appId,
instanceId, instanceId,
}) => { }) => {
const headers = await createUserWithAdminPermissions(request, instanceId) const headers = await createUserWithAdminPermissions(
request,
appId,
instanceId
)
await createRequest(request, method, url, body) await createRequest(request, method, url, body)
.set(headers) .set(headers)

View File

@ -24,9 +24,9 @@ describe("/instances", () => {
it("returns a success message when the instance database is successfully created", async () => { it("returns a success message when the instance database is successfully created", async () => {
const res = await request const res = await request
.post(`/api/${TEST_APP_ID}/instances`) .post(`/api/instances`)
.send({ name: "test-instance" }) .send({ name: "test-instance" })
.set(defaultHeaders) .set(defaultHeaders(TEST_APP_ID))
.expect('Content-Type', /json/) .expect('Content-Type', /json/)
.expect(200) .expect(200)
@ -42,7 +42,7 @@ describe("/instances", () => {
const instance = await createInstance(request, TEST_APP_ID); const instance = await createInstance(request, TEST_APP_ID);
const res = await request const res = await request
.delete(`/api/instances/${instance._id}`) .delete(`/api/instances/${instance._id}`)
.set(defaultHeaders) .set(defaultHeaders(TEST_APP_ID))
.expect(200) .expect(200)
expect(res.res.statusMessage).toEqual(`Instance Database ${instance._id} successfully destroyed.`); expect(res.res.statusMessage).toEqual(`Instance Database ${instance._id} successfully destroyed.`);

View File

@ -33,7 +33,7 @@ describe("/models", () => {
it("returns a success message when the model is successfully created", done => { it("returns a success message when the model is successfully created", done => {
request request
.post(`/api/${instance._id}/models`) .post(`/api/models`)
.send({ .send({
name: "TestModel", name: "TestModel",
key: "name", key: "name",
@ -41,7 +41,7 @@ describe("/models", () => {
name: { type: "string" } name: { type: "string" }
} }
}) })
.set(defaultHeaders) .set(defaultHeaders(app._id, instance._id))
.expect('Content-Type', /json/) .expect('Content-Type', /json/)
.expect(200) .expect(200)
.end(async (err, res) => { .end(async (err, res) => {
@ -55,8 +55,9 @@ describe("/models", () => {
await builderEndpointShouldBlockNormalUsers({ await builderEndpointShouldBlockNormalUsers({
request, request,
method: "POST", method: "POST",
url: `/api/${instance._id}/models`, url: `/api/models`,
instanceId: instance._id, instanceId: instance._id,
appId: app._id,
body: { body: {
name: "TestModel", name: "TestModel",
key: "name", key: "name",
@ -73,13 +74,13 @@ describe("/models", () => {
beforeEach(async () => { beforeEach(async () => {
instance = await createInstance(request, app._id) instance = await createInstance(request, app._id)
testModel = await createModel(request, instance._id, testModel) testModel = await createModel(request, app._id, instance._id, testModel)
}); });
it("returns all the models for that instance in the response body", done => { it("returns all the models for that instance in the response body", done => {
request request
.get(`/api/${instance._id}/models`) .get(`/api/models`)
.set(defaultHeaders) .set(defaultHeaders(app._id, instance._id))
.expect('Content-Type', /json/) .expect('Content-Type', /json/)
.expect(200) .expect(200)
.end(async (_, res) => { .end(async (_, res) => {
@ -94,8 +95,9 @@ describe("/models", () => {
await builderEndpointShouldBlockNormalUsers({ await builderEndpointShouldBlockNormalUsers({
request, request,
method: "GET", method: "GET",
url: `/api/${instance._id}/models`, url: `/api/models`,
instanceId: instance._id, instanceId: instance._id,
appId: app._id,
}) })
}) })
}); });
@ -105,7 +107,7 @@ describe("/models", () => {
beforeEach(async () => { beforeEach(async () => {
instance = await createInstance(request, app._id) instance = await createInstance(request, app._id)
testModel = await createModel(request, instance._id, testModel) testModel = await createModel(request, app._id, instance._id, testModel)
}); });
afterEach(() => { afterEach(() => {
@ -114,8 +116,8 @@ describe("/models", () => {
it("returns a success response when a model is deleted.", async done => { it("returns a success response when a model is deleted.", async done => {
request request
.delete(`/api/${instance._id}/models/${testModel._id}/${testModel._rev}`) .delete(`/api/models/${testModel._id}/${testModel._rev}`)
.set(defaultHeaders) .set(defaultHeaders(app._id, instance._id))
.expect('Content-Type', /json/) .expect('Content-Type', /json/)
.expect(200) .expect(200)
.end(async (_, res) => { .end(async (_, res) => {
@ -125,7 +127,7 @@ describe("/models", () => {
}) })
it("deletes linked references to the model after deletion", async done => { it("deletes linked references to the model after deletion", async done => {
const linkedModel = await createModel(request, instance._id, { const linkedModel = await createModel(request, app._id, instance._id, {
name: "LinkedModel", name: "LinkedModel",
type: "model", type: "model",
key: "name", key: "name",
@ -147,8 +149,8 @@ describe("/models", () => {
}) })
request request
.delete(`/api/${instance._id}/models/${testModel._id}/${testModel._rev}`) .delete(`/api/models/${testModel._id}/${testModel._rev}`)
.set(defaultHeaders) .set(defaultHeaders(app._id, instance._id))
.expect('Content-Type', /json/) .expect('Content-Type', /json/)
.expect(200) .expect(200)
.end(async (_, res) => { .end(async (_, res) => {
@ -163,8 +165,9 @@ describe("/models", () => {
await builderEndpointShouldBlockNormalUsers({ await builderEndpointShouldBlockNormalUsers({
request, request,
method: "DELETE", method: "DELETE",
url: `/api/${instance._id}/models/${testModel._id}/${testModel._rev}`, url: `/api/models/${testModel._id}/${testModel._rev}`,
instanceId: instance._id, instanceId: instance._id,
appId: app._id,
}) })
}) })

View File

@ -27,7 +27,7 @@ describe("/records", () => {
beforeEach(async () => { beforeEach(async () => {
instance = await createInstance(request, app._id) instance = await createInstance(request, app._id)
model = await createModel(request, instance._id) model = await createModel(request, app._id, instance._id)
record = { record = {
name: "Test Contact", name: "Test Contact",
status: "new", status: "new",
@ -39,9 +39,9 @@ describe("/records", () => {
const createRecord = async r => const createRecord = async r =>
await request await request
.post(`/api/${instance._id}/${model._id}/records`) .post(`/api/${model._id}/records`)
.send(r || record) .send(r || record)
.set(defaultHeaders) .set(defaultHeaders(app._id, instance._id))
.expect('Content-Type', /json/) .expect('Content-Type', /json/)
.expect(200) .expect(200)
@ -57,14 +57,14 @@ describe("/records", () => {
const existing = rec.body const existing = rec.body
const res = await request const res = await request
.post(`/api/${instance._id}/${model._id}/records`) .post(`/api/${model._id}/records`)
.send({ .send({
_id: existing._id, _id: existing._id,
_rev: existing._rev, _rev: existing._rev,
modelId: model._id, modelId: model._id,
name: "Updated Name", name: "Updated Name",
}) })
.set(defaultHeaders) .set(defaultHeaders(app._id, instance._id))
.expect('Content-Type', /json/) .expect('Content-Type', /json/)
.expect(200) .expect(200)
@ -77,8 +77,8 @@ describe("/records", () => {
const existing = rec.body const existing = rec.body
const res = await request const res = await request
.get(`/api/${instance._id}/${model._id}/records/${existing._id}`) .get(`/api/${model._id}/records/${existing._id}`)
.set(defaultHeaders) .set(defaultHeaders(app._id, instance._id))
.expect('Content-Type', /json/) .expect('Content-Type', /json/)
.expect(200) .expect(200)
@ -100,8 +100,8 @@ describe("/records", () => {
await createRecord(newRecord) await createRecord(newRecord)
const res = await request const res = await request
.get(`/api/${instance._id}/${model._id}/records`) .get(`/api/${model._id}/records`)
.set(defaultHeaders) .set(defaultHeaders(app._id, instance._id))
.expect('Content-Type', /json/) .expect('Content-Type', /json/)
.expect(200) .expect(200)
@ -122,8 +122,8 @@ describe("/records", () => {
const recordIds = [record.body._id, secondRecord.body._id] const recordIds = [record.body._id, secondRecord.body._id]
const res = await request const res = await request
.post(`/api/${instance._id}/records/search`) .post(`/api/records/search`)
.set(defaultHeaders) .set(defaultHeaders(app._id, instance._id))
.send({ .send({
keys: recordIds keys: recordIds
}) })
@ -137,8 +137,8 @@ describe("/records", () => {
it("load should return 404 when record does not exist", async () => { it("load should return 404 when record does not exist", async () => {
await createRecord() await createRecord()
await request await request
.get(`/api/${instance._id}/${model._id}/records/not-a-valid-id`) .get(`/api/${model._id}/records/not-a-valid-id`)
.set(defaultHeaders) .set(defaultHeaders(app._id, instance._id))
.expect('Content-Type', /json/) .expect('Content-Type', /json/)
.expect(404) .expect(404)
}) })
@ -147,9 +147,9 @@ describe("/records", () => {
describe("validate", () => { describe("validate", () => {
it("should return no errors on valid record", async () => { it("should return no errors on valid record", async () => {
const result = await request const result = await request
.post(`/api/${instance._id}/${model._id}/records/validate`) .post(`/api/${model._id}/records/validate`)
.send({ name: "ivan" }) .send({ name: "ivan" })
.set(defaultHeaders) .set(defaultHeaders(app._id, instance._id))
.expect('Content-Type', /json/) .expect('Content-Type', /json/)
.expect(200) .expect(200)
@ -159,9 +159,9 @@ describe("/records", () => {
it("should errors on invalid record", async () => { it("should errors on invalid record", async () => {
const result = await request const result = await request
.post(`/api/${instance._id}/${model._id}/records/validate`) .post(`/api/${model._id}/records/validate`)
.send({ name: 1 }) .send({ name: 1 })
.set(defaultHeaders) .set(defaultHeaders(app._id, instance._id))
.expect('Content-Type', /json/) .expect('Content-Type', /json/)
.expect(200) .expect(200)

View File

@ -36,11 +36,11 @@ describe("/users", () => {
describe("fetch", () => { describe("fetch", () => {
it("returns a list of users from an instance db", async () => { it("returns a list of users from an instance db", async () => {
await createUser(request, instance._id, "brenda", "brendas_password") await createUser(request, app._id, instance._id, "brenda", "brendas_password")
await createUser(request, instance._id, "pam", "pam_password") await createUser(request, app._id, instance._id, "pam", "pam_password")
const res = await request const res = await request
.get(`/api/${instance._id}/users`) .get(`/api/users`)
.set(defaultHeaders) .set(defaultHeaders(app._id, instance._id))
.expect('Content-Type', /json/) .expect('Content-Type', /json/)
.expect(200) .expect(200)
@ -50,12 +50,13 @@ describe("/users", () => {
}) })
it("should apply authorization to endpoint", async () => { it("should apply authorization to endpoint", async () => {
await createUser(request, instance._id, "brenda", "brendas_password") await createUser(request, app._id, instance._id, "brenda", "brendas_password")
await testPermissionsForEndpoint({ await testPermissionsForEndpoint({
request, request,
method: "GET", method: "GET",
url: `/api/${instance._id}/users`, url: `/api/users`,
instanceId: instance._id, instanceId: instance._id,
appId: app._id,
permissionName: LIST_USERS, permissionName: LIST_USERS,
}) })
}) })
@ -66,8 +67,8 @@ describe("/users", () => {
it("returns a success message when a user is successfully created", async () => { it("returns a success message when a user is successfully created", async () => {
const res = await request const res = await request
.post(`/api/${instance._id}/users`) .post(`/api/users`)
.set(defaultHeaders) .set(defaultHeaders(app._id, instance._id))
.send({ name: "Bill", username: "bill", password: "bills_password", accessLevelId: POWERUSER_LEVEL_ID }) .send({ name: "Bill", username: "bill", password: "bills_password", accessLevelId: POWERUSER_LEVEL_ID })
.expect(200) .expect(200)
.expect('Content-Type', /json/) .expect('Content-Type', /json/)
@ -81,8 +82,9 @@ describe("/users", () => {
request, request,
method: "POST", method: "POST",
body: { name: "brandNewUser", username: "brandNewUser", password: "yeeooo", accessLevelId: POWERUSER_LEVEL_ID }, body: { name: "brandNewUser", username: "brandNewUser", password: "yeeooo", accessLevelId: POWERUSER_LEVEL_ID },
url: `/api/${instance._id}/users`, url: `/api/users`,
instanceId: instance._id, instanceId: instance._id,
appId: app._id,
permissionName: USER_MANAGEMENT, permissionName: USER_MANAGEMENT,
}) })
}) })

View File

@ -30,7 +30,7 @@ describe("/views", () => {
const createView = async () => const createView = async () =>
await request await request
.post(`/api/${instance._id}/views`) .post(`/api/views`)
.send({ .send({
name: "TestView", name: "TestView",
map: `function(doc) { map: `function(doc) {
@ -40,7 +40,7 @@ describe("/views", () => {
}`, }`,
reduce: `function(keys, values) { }` reduce: `function(keys, values) { }`
}) })
.set(defaultHeaders) .set(defaultHeaders(app._id, instance._id))
.expect('Content-Type', /json/) .expect('Content-Type', /json/)
.expect(200) .expect(200)
@ -56,14 +56,14 @@ describe("/views", () => {
describe("fetch", () => { describe("fetch", () => {
beforeEach(async () => { beforeEach(async () => {
model = await createModel(request, instance._id); model = await createModel(request, app._id, instance._id);
}); });
it("should only return custom views", async () => { it("should only return custom views", async () => {
const view = await createView() const view = await createView()
const res = await request const res = await request
.get(`/api/${instance._id}/views`) .get(`/api/views`)
.set(defaultHeaders) .set(defaultHeaders(app._id, instance._id))
.expect('Content-Type', /json/) .expect('Content-Type', /json/)
.expect(200) .expect(200)
expect(res.body.length).toBe(1) expect(res.body.length).toBe(1)

View File

@ -63,8 +63,8 @@ describe("/workflows", () => {
describe("create", () => { describe("create", () => {
it("returns a success message when the workflow is successfully created", async () => { it("returns a success message when the workflow is successfully created", async () => {
const res = await request const res = await request
.post(`/api/${instance._id}/workflows`) .post(`/api/workflows`)
.set(defaultHeaders) .set(defaultHeaders(app._id, instance._id))
.send(TEST_WORKFLOW) .send(TEST_WORKFLOW)
.expect('Content-Type', /json/) .expect('Content-Type', /json/)
.expect(200) .expect(200)
@ -77,8 +77,9 @@ describe("/workflows", () => {
await builderEndpointShouldBlockNormalUsers({ await builderEndpointShouldBlockNormalUsers({
request, request,
method: "POST", method: "POST",
url: `/api/${instance._id}/workflows`, url: `/api/workflows`,
instanceId: instance._id, instanceId: instance._id,
appId: app._id,
body: TEST_WORKFLOW body: TEST_WORKFLOW
}) })
}) })
@ -92,8 +93,8 @@ describe("/workflows", () => {
workflow.name = "Updated Name"; workflow.name = "Updated Name";
const res = await request const res = await request
.put(`/api/${instance._id}/workflows`) .put(`/api/workflows`)
.set(defaultHeaders) .set(defaultHeaders(app._id, instance._id))
.send(workflow) .send(workflow)
.expect('Content-Type', /json/) .expect('Content-Type', /json/)
.expect(200) .expect(200)
@ -107,8 +108,8 @@ describe("/workflows", () => {
it("return all the workflows for an instance", async () => { it("return all the workflows for an instance", async () => {
await createWorkflow(); await createWorkflow();
const res = await request const res = await request
.get(`/api/${instance._id}/workflows`) .get(`/api/workflows`)
.set(defaultHeaders) .set(defaultHeaders(app._id, instance._id))
.expect('Content-Type', /json/) .expect('Content-Type', /json/)
.expect(200) .expect(200)
@ -119,8 +120,9 @@ describe("/workflows", () => {
await builderEndpointShouldBlockNormalUsers({ await builderEndpointShouldBlockNormalUsers({
request, request,
method: "GET", method: "GET",
url: `/api/${instance._id}/workflows`, url: `/api/workflows`,
instanceId: instance._id, instanceId: instance._id,
appId: app._id,
}) })
}) })
}) })
@ -129,8 +131,8 @@ describe("/workflows", () => {
it("deletes a workflow by its ID", async () => { it("deletes a workflow by its ID", async () => {
await createWorkflow(); await createWorkflow();
const res = await request const res = await request
.delete(`/api/${instance._id}/workflows/${workflow.id}/${workflow.rev}`) .delete(`/api/workflows/${workflow.id}/${workflow.rev}`)
.set(defaultHeaders) .set(defaultHeaders(app._id, instance._id))
.expect('Content-Type', /json/) .expect('Content-Type', /json/)
.expect(200) .expect(200)
@ -142,8 +144,9 @@ describe("/workflows", () => {
await builderEndpointShouldBlockNormalUsers({ await builderEndpointShouldBlockNormalUsers({
request, request,
method: "DELETE", method: "DELETE",
url: `/api/${instance._id}/workflows/${workflow.id}/${workflow._rev}`, url: `/api/workflows/${workflow.id}/${workflow._rev}`,
instanceId: instance._id, instanceId: instance._id,
appId: app._id,
}) })
}) })
}) })

View File

@ -6,19 +6,11 @@ const { USER_MANAGEMENT, LIST_USERS } = require("../../utilities/accessLevels")
const router = Router() const router = Router()
router router
.get("/api/:instanceId/users", authorized(LIST_USERS), controller.fetch) .get("/api/users", authorized(LIST_USERS), controller.fetch)
.get( .get("/api/users/:username", authorized(USER_MANAGEMENT), controller.find)
"/api/:instanceId/users/:username", .post("/api/users", authorized(USER_MANAGEMENT), controller.create)
authorized(USER_MANAGEMENT),
controller.find
)
.post(
"/api/:instanceId/users",
authorized(USER_MANAGEMENT),
controller.create
)
.delete( .delete(
"/api/:instanceId/users/:username", "/api/users/:username",
authorized(USER_MANAGEMENT), authorized(USER_MANAGEMENT),
controller.destroy controller.destroy
) )

View File

@ -8,13 +8,13 @@ const router = Router()
router router
.get( .get(
"/api/:instanceId/views/:viewName", "/api/views/:viewName",
authorized(READ_VIEW, ctx => ctx.params.viewName), authorized(READ_VIEW, ctx => ctx.params.viewName),
recordController.fetchView recordController.fetchView
) )
.get("/api/:instanceId/views", authorized(BUILDER), viewController.fetch) .get("/api/views", authorized(BUILDER), viewController.fetch)
// .patch("/api/:databaseId/views", controller.update); // .patch("/api/:databaseId/views", controller.update);
// .delete("/api/:instanceId/views/:viewId/:revId", controller.destroy); // .delete("/api/:instanceId/views/:viewId/:revId", controller.destroy);
.post("/api/:instanceId/views", authorized(BUILDER), viewController.create) .post("/api/views", authorized(BUILDER), viewController.create)
module.exports = router module.exports = router

Some files were not shown because too many files have changed in this diff Show More