Merge branch 'feature/opinionated-sql' of github.com:Budibase/budibase into feature/opinionated-sql

This commit is contained in:
mike12345567 2021-06-17 14:42:41 +01:00
commit 1014260ad5
25 changed files with 166 additions and 136 deletions

View File

@ -2,21 +2,23 @@ import { store } from "./index"
import { get as svelteGet } from "svelte/store" import { get as svelteGet } from "svelte/store"
import { removeCookie, Cookies } from "./cookies" import { removeCookie, Cookies } from "./cookies"
const apiCall = const apiCall = method => async (
method => url,
async (url, body, headers = { "Content-Type": "application/json" }) => { body,
headers["x-budibase-app-id"] = svelteGet(store).appId headers = { "Content-Type": "application/json" }
const json = headers["Content-Type"] === "application/json" ) => {
const resp = await fetch(url, { headers["x-budibase-app-id"] = svelteGet(store).appId
method: method, const json = headers["Content-Type"] === "application/json"
body: json ? JSON.stringify(body) : body, const resp = await fetch(url, {
headers, method: method,
}) body: json ? JSON.stringify(body) : body,
if (resp.status === 403) { headers,
removeCookie(Cookies.Auth) })
} if (resp.status === 403) {
return resp removeCookie(Cookies.Auth)
} }
return resp
}
export const post = apiCall("POST") export const post = apiCall("POST")
export const get = apiCall("GET") export const get = apiCall("GET")

View File

@ -100,10 +100,9 @@ const automationActions = store => ({
}, },
deleteAutomationBlock: block => { deleteAutomationBlock: block => {
store.update(state => { store.update(state => {
const idx = const idx = state.selectedAutomation.automation.definition.steps.findIndex(
state.selectedAutomation.automation.definition.steps.findIndex( x => x.id === block.id
x => x.id === block.id )
)
state.selectedAutomation.deleteBlock(block.id) state.selectedAutomation.deleteBlock(block.id)
// Select next closest step // Select next closest step

View File

@ -53,7 +53,9 @@
bind:hideAutocolumns bind:hideAutocolumns
{loading} {loading}
> >
<CreateColumnButton /> {#if isInternal}
<CreateColumnButton />
{/if}
{#if schema && Object.keys(schema).length > 0} {#if schema && Object.keys(schema).length > 0}
{#if !isUsersTable} {#if !isUsersTable}
<CreateRowButton <CreateRowButton

View File

@ -9,7 +9,11 @@
import CreateEditRow from "./modals/CreateEditRow.svelte" import CreateEditRow from "./modals/CreateEditRow.svelte"
import CreateEditUser from "./modals/CreateEditUser.svelte" import CreateEditUser from "./modals/CreateEditUser.svelte"
import CreateEditColumn from "./modals/CreateEditColumn.svelte" import CreateEditColumn from "./modals/CreateEditColumn.svelte"
import { TableNames, UNEDITABLE_USER_FIELDS, BUDIBASE_INTERNAL_DB } from "constants" import {
TableNames,
UNEDITABLE_USER_FIELDS,
BUDIBASE_INTERNAL_DB,
} from "constants"
import RoleCell from "./cells/RoleCell.svelte" import RoleCell from "./cells/RoleCell.svelte"
export let schema = {} export let schema = {}

View File

@ -15,9 +15,6 @@
} }
function onClickQuery(query) { function onClickQuery(query) {
if ($queries.selected === query._id) {
return
}
queries.select(query) queries.select(query)
$goto(`./datasource/${query.datasourceId}/${query._id}`) $goto(`./datasource/${query.datasourceId}/${query._id}`)
} }

View File

@ -2,7 +2,7 @@
import { Icon } from "@budibase/bbui" import { Icon } from "@budibase/bbui"
</script> </script>
<a target="_blank" href="https://github.com/Budibase/budibase/discussions"> <a target="_blank" href="https://github.com/Budibase/budibase/discussions">
<Icon hoverable name="Help" size="XXL" /> <Icon hoverable name="Help" size="XXL" />
</a> </a>
@ -14,4 +14,4 @@
right: var(--spacing-m); right: var(--spacing-m);
border-radius: 55%; border-radius: 55%;
} }
</style> </style>

View File

@ -59,7 +59,9 @@
<section> <section>
<Heading size="XS">Columns</Heading> <Heading size="XS">Columns</Heading>
<ul> <ul>
{#each context.filter( context => context.readableBinding.match(searchRgx) ) as { readableBinding }} {#each context.filter(context =>
context.readableBinding.match(searchRgx)
) as { readableBinding }}
<li <li
on:click={() => { on:click={() => {
value = addToText(value, getCaretPosition(), readableBinding) value = addToText(value, getCaretPosition(), readableBinding)
@ -75,7 +77,9 @@
<section> <section>
<Heading size="XS">Components</Heading> <Heading size="XS">Components</Heading>
<ul> <ul>
{#each instance.filter( instance => instance.readableBinding.match(searchRgx) ) as { readableBinding }} {#each instance.filter(instance =>
instance.readableBinding.match(searchRgx)
) as { readableBinding }}
<li on:click={() => addToText(readableBinding)}> <li on:click={() => addToText(readableBinding)}>
{readableBinding} {readableBinding}
</li> </li>

View File

@ -49,7 +49,9 @@
<div class="section"> <div class="section">
{#each categories as [categoryName, bindings]} {#each categories as [categoryName, bindings]}
<Heading size="XS">{categoryName}</Heading> <Heading size="XS">{categoryName}</Heading>
{#each bindings.filter( binding => binding.label.match(searchRgx) ) as binding} {#each bindings.filter(binding =>
binding.label.match(searchRgx)
) as binding}
<div <div
class="binding" class="binding"
on:click={() => { on:click={() => {

View File

@ -103,9 +103,8 @@
} }
function fetchQueryDefinition(query) { function fetchQueryDefinition(query) {
const source = $datasources.list.find( const source = $datasources.list.find(ds => ds._id === query.datasourceId)
ds => ds._id === query.datasourceId .source
).source
return $integrations[source].query[query.queryVerb] return $integrations[source].query[query.queryVerb]
} }
</script> </script>

View File

@ -18,9 +18,8 @@
) )
function fetchQueryDefinition(query) { function fetchQueryDefinition(query) {
const source = $datasources.list.find( const source = $datasources.list.find(ds => ds._id === query.datasourceId)
ds => ds._id === query.datasourceId .source
).source
return $integrations[source].query[query.queryVerb] return $integrations[source].query[query.queryVerb]
} }
</script> </script>

View File

@ -1,12 +1,7 @@
<script> <script>
import { goto, beforeUrlChange } from "@roxi/routify" import { goto, beforeUrlChange } from "@roxi/routify"
import { Button, Heading, Body, Divider, Layout } from "@budibase/bbui" import { Button, Heading, Body, Divider, Layout } from "@budibase/bbui"
import { import { datasources, integrations, queries, tables } from "stores/backend"
datasources,
integrations,
queries,
tables,
} from "stores/backend"
import { notifications } from "@budibase/bbui" import { notifications } from "@budibase/bbui"
import IntegrationConfigForm from "components/backend/DatasourceNavigator/TableIntegrationMenu/IntegrationConfigForm.svelte" import IntegrationConfigForm from "components/backend/DatasourceNavigator/TableIntegrationMenu/IntegrationConfigForm.svelte"
import ICONS from "components/backend/DatasourceNavigator/icons" import ICONS from "components/backend/DatasourceNavigator/icons"
@ -100,11 +95,16 @@
> >
</div> </div>
<Body> <Body>
This datasource can determine tables automatically. Budibase can fetch your tables directly from the database and you can use them without having to write any queries at all. This datasource can determine tables automatically. Budibase can fetch
your tables directly from the database and you can use them without
having to write any queries at all.
</Body> </Body>
<div class="query-list"> <div class="query-list">
{#each Object.keys(datasource.entities) as entity} {#each Object.keys(datasource.entities) as entity}
<div class="query-list-item" on:click={() => onClickTable(datasource.entities[entity])}> <div
class="query-list-item"
on:click={() => onClickTable(datasource.entities[entity])}
>
<p class="query-name">{entity}</p> <p class="query-name">{entity}</p>
<p>Primary Key: {datasource.entities[entity].primary}</p> <p>Primary Key: {datasource.entities[entity].primary}</p>
<p></p> <p></p>

View File

@ -2,9 +2,7 @@
import { Button, Heading, Body, Layout, Modal } from "@budibase/bbui" import { Button, Heading, Body, Layout, Modal } from "@budibase/bbui"
import CreateTableModal from "components/backend/TableNavigator/modals/CreateTableModal.svelte" import CreateTableModal from "components/backend/TableNavigator/modals/CreateTableModal.svelte"
let modal let modal
</script> </script>
<Modal bind:this={modal}> <Modal bind:this={modal}>
@ -12,14 +10,9 @@
</Modal> </Modal>
<Layout> <Layout>
<Heading> <Heading>Budibase Internal DB</Heading>
Budibase Internal DB
</Heading>
<Body> <div>
Stuff about the internal table <Button cta on:click={modal.show}>Create new table</Button>
</Body> </div>
</Layout>
<Button cta on:click={modal.show}>Create new table</Button>
</Layout>

View File

@ -1,5 +1,5 @@
import { writable } from "svelte/store" import { writable } from "svelte/store"
import { queries } from "./" import { queries, tables, views } from "./"
import api from "../../builderStore/api" import api from "../../builderStore/api"
export const INITIAL_DATASOURCE_VALUES = { export const INITIAL_DATASOURCE_VALUES = {
@ -26,7 +26,12 @@ export function createDatasourcesStore() {
}, },
select: async datasourceId => { select: async datasourceId => {
update(state => ({ ...state, selected: datasourceId })) update(state => ({ ...state, selected: datasourceId }))
queries.update(state => ({ ...state, selected: null })) queries.unselect()
tables.unselect()
views.unselect()
},
unselect: () => {
update(state => ({ ...state, selected: null }))
}, },
updateSchema: async datasource => { updateSchema: async datasource => {
let url = `/api/datasources/${datasource._id}/schema` let url = `/api/datasources/${datasource._id}/schema`

View File

@ -6,4 +6,4 @@ export { permissions } from "./permissions"
export { roles } from "./roles" export { roles } from "./roles"
export { datasources } from "./datasources" export { datasources } from "./datasources"
export { integrations } from "./integrations" export { integrations } from "./integrations"
export { queries } from "./queries" export { queries } from "./queries"

View File

@ -55,10 +55,6 @@ export function createQueriesStore() {
}, },
select: query => { select: query => {
update(state => ({ ...state, selected: query._id })) update(state => ({ ...state, selected: query._id }))
datasources.update(state => ({
...state,
selected: query.datasourceId,
}))
tables.update(state => ({ tables.update(state => ({
...state, ...state,
selected: null, selected: null,
@ -66,10 +62,6 @@ export function createQueriesStore() {
}, },
unselect: () => { unselect: () => {
update(state => ({ ...state, selected: null })) update(state => ({ ...state, selected: null }))
datasources.update(state => ({
...state,
selected: null,
}))
}, },
delete: async query => { delete: async query => {
const response = await api.delete( const response = await api.delete(

View File

@ -1,5 +1,5 @@
import { writable, get } from "svelte/store" import { writable, get } from "svelte/store"
import { views, queries } from "./" import { views, queries, datasources } from "./"
import { cloneDeep } from "lodash/fp" import { cloneDeep } from "lodash/fp"
import api from "builderStore/api" import api from "builderStore/api"
@ -25,8 +25,9 @@ export function createTablesStore() {
selected: table, selected: table,
draft: cloneDeep(table), draft: cloneDeep(table),
})) }))
views.select({ name: table._id }) views.unselect()
queries.unselect() queries.unselect()
datasources.unselect()
} }
} }
@ -70,6 +71,12 @@ export function createTablesStore() {
update, update,
fetch, fetch,
select, select,
unselect: () => {
update(state => ({
...state,
selected: null,
}))
},
save, save,
init: async () => { init: async () => {
const response = await api.get("/api/tables") const response = await api.get("/api/tables")

View File

@ -9,7 +9,8 @@ export const SOME_QUERY = {
queryVerb: "read", queryVerb: "read",
schema: {}, schema: {},
name: "Speakers", name: "Speakers",
_id: "query_datasource_04b003a7b4a8428eadd3bb2f7eae0255_bcb8ffc6fcbc484e8d63121fc0bf986f", _id:
"query_datasource_04b003a7b4a8428eadd3bb2f7eae0255_bcb8ffc6fcbc484e8d63121fc0bf986f",
_rev: "2-941f8699eb0adf995f8bd59c99203b26", _rev: "2-941f8699eb0adf995f8bd59c99203b26",
readable: true, readable: true,
} }
@ -74,7 +75,8 @@ export const SAVE_QUERY_RESPONSE = {
}, },
}, },
name: "Speakers", name: "Speakers",
_id: "query_datasource_04b003a7b4a8428eadd3bb2f7eae0255_bcb8ffc6fcbc484e8d63121fc0bf986f", _id:
"query_datasource_04b003a7b4a8428eadd3bb2f7eae0255_bcb8ffc6fcbc484e8d63121fc0bf986f",
_rev: "3-5a64adef494b1e9c793dc91b51ce73c6", _rev: "3-5a64adef494b1e9c793dc91b51ce73c6",
readable: true, readable: true,
} }

View File

@ -1,5 +1,5 @@
import { writable, get } from "svelte/store" import { writable, get } from "svelte/store"
import { tables } from "./" import { tables, datasources, queries } from "./"
import api from "builderStore/api" import api from "builderStore/api"
export function createViewsStore() { export function createViewsStore() {
@ -10,11 +10,20 @@ export function createViewsStore() {
return { return {
subscribe, subscribe,
update,
select: async view => { select: async view => {
update(state => ({ update(state => ({
...state, ...state,
selected: view, selected: view,
})) }))
queries.unselect()
datasources.unselect()
},
unselect: () => {
update(state => ({
...state,
selected: null,
}))
}, },
delete: async view => { delete: async view => {
await api.delete(`/api/views/${view}`) await api.delete(`/api/views/${view}`)

View File

@ -2,7 +2,8 @@ const { Client } = require("@elastic/elasticsearch")
const { QUERY_TYPES, FIELD_TYPES } = require("./Integration") const { QUERY_TYPES, FIELD_TYPES } = require("./Integration")
const SCHEMA = { const SCHEMA = {
docs: "https://www.elastic.co/guide/en/elasticsearch/client/javascript-api/current/index.html", docs:
"https://www.elastic.co/guide/en/elasticsearch/client/javascript-api/current/index.html",
description: description:
"Elasticsearch is a search engine based on the Lucene library. It provides a distributed, multitenant-capable full-text search engine with an HTTP web interface and schema-free JSON documents.", "Elasticsearch is a search engine based on the Lucene library. It provides a distributed, multitenant-capable full-text search engine with an HTTP web interface and schema-free JSON documents.",
friendlyName: "ElasticSearch", friendlyName: "ElasticSearch",

View File

@ -14,52 +14,50 @@ const WEBHOOK_ENDPOINTS = new RegExp(
["webhooks/trigger", "webhooks/schema"].join("|") ["webhooks/trigger", "webhooks/schema"].join("|")
) )
module.exports = module.exports = (permType, permLevel = null) => async (ctx, next) => {
(permType, permLevel = null) => // webhooks don't need authentication, each webhook unique
async (ctx, next) => { if (WEBHOOK_ENDPOINTS.test(ctx.request.url)) {
// webhooks don't need authentication, each webhook unique
if (WEBHOOK_ENDPOINTS.test(ctx.request.url)) {
return next()
}
if (!ctx.user) {
return ctx.throw(403, "No user info found")
}
// check general builder stuff, this middleware is a good way
// to find API endpoints which are builder focused
await builderMiddleware(ctx, permType)
const isAuthed = ctx.isAuthenticated
const { basePermissions, permissions } = await getUserPermissions(
ctx.appId,
ctx.roleId
)
// builders for now have permission to do anything
// TODO: in future should consider separating permissions with an require("@budibase/auth").isClient check
let isBuilder = ctx.user && ctx.user.builder && ctx.user.builder.global
const isBuilderApi = permType === PermissionTypes.BUILDER
if (isBuilder) {
return next()
} else if (isBuilderApi && !isBuilder) {
return ctx.throw(403, "Not Authorized")
}
if (
hasResource(ctx) &&
doesHaveResourcePermission(permissions, permLevel, ctx)
) {
return next()
}
if (!isAuthed) {
ctx.throw(403, "Session not authenticated")
}
if (!doesHaveBasePermission(permType, permLevel, basePermissions)) {
ctx.throw(403, "User does not have permission")
}
return next() return next()
} }
if (!ctx.user) {
return ctx.throw(403, "No user info found")
}
// check general builder stuff, this middleware is a good way
// to find API endpoints which are builder focused
await builderMiddleware(ctx, permType)
const isAuthed = ctx.isAuthenticated
const { basePermissions, permissions } = await getUserPermissions(
ctx.appId,
ctx.roleId
)
// builders for now have permission to do anything
// TODO: in future should consider separating permissions with an require("@budibase/auth").isClient check
let isBuilder = ctx.user && ctx.user.builder && ctx.user.builder.global
const isBuilderApi = permType === PermissionTypes.BUILDER
if (isBuilder) {
return next()
} else if (isBuilderApi && !isBuilder) {
return ctx.throw(403, "Not Authorized")
}
if (
hasResource(ctx) &&
doesHaveResourcePermission(permissions, permLevel, ctx)
) {
return next()
}
if (!isAuthed) {
ctx.throw(403, "Session not authenticated")
}
if (!doesHaveBasePermission(permType, permLevel, basePermissions)) {
ctx.throw(403, "User does not have permission")
}
return next()
}

View File

@ -1,5 +1,9 @@
const { getAppId, setCookie, getCookie, clearCookie } = const {
require("@budibase/auth").utils getAppId,
setCookie,
getCookie,
clearCookie,
} = require("@budibase/auth").utils
const { Cookies } = require("@budibase/auth").constants const { Cookies } = require("@budibase/auth").constants
const { getRole } = require("@budibase/auth/roles") const { getRole } = require("@budibase/auth/roles")
const { getGlobalSelf } = require("../utilities/workerRequests") const { getGlobalSelf } = require("../utilities/workerRequests")

View File

@ -90,17 +90,15 @@ const numericalConstraint = (constraint, error) => value => {
return null return null
} }
const inclusionConstraint = const inclusionConstraint = (options = []) => value => {
(options = []) => if (value == null || value === "") {
value => {
if (value == null || value === "") {
return null
}
if (!options.includes(value)) {
return "Invalid value"
}
return null return null
} }
if (!options.includes(value)) {
return "Invalid value"
}
return null
}
const dateConstraint = (dateString, isEarliest) => { const dateConstraint = (dateString, isEarliest) => {
const dateLimit = Date.parse(dateString) const dateLimit = Date.parse(dateString)

View File

@ -5,8 +5,15 @@ const authPkg = require("@budibase/auth")
const GLOBAL_DB = authPkg.StaticDatabases.GLOBAL.name const GLOBAL_DB = authPkg.StaticDatabases.GLOBAL.name
exports.sendEmail = async ctx => { exports.sendEmail = async ctx => {
const { groupId, email, userId, purpose, contents, from, subject } = const {
ctx.request.body groupId,
email,
userId,
purpose,
contents,
from,
subject,
} = ctx.request.body
let user let user
if (userId) { if (userId) {
const db = new CouchDB(GLOBAL_DB) const db = new CouchDB(GLOBAL_DB)

View File

@ -1,6 +1,9 @@
const CouchDB = require("../../../db") const CouchDB = require("../../../db")
const { getGroupParams, generateGroupID, StaticDatabases } = const {
require("@budibase/auth").db getGroupParams,
generateGroupID,
StaticDatabases,
} = require("@budibase/auth").db
const GLOBAL_DB = StaticDatabases.GLOBAL.name const GLOBAL_DB = StaticDatabases.GLOBAL.name

View File

@ -1,6 +1,9 @@
const CouchDB = require("../../../db") const CouchDB = require("../../../db")
const { generateGlobalUserID, getGlobalUserParams, StaticDatabases } = const {
require("@budibase/auth").db generateGlobalUserID,
getGlobalUserParams,
StaticDatabases,
} = require("@budibase/auth").db
const { hash, getGlobalUserByEmail } = require("@budibase/auth").utils const { hash, getGlobalUserByEmail } = require("@budibase/auth").utils
const { UserStatus, EmailTemplatePurpose } = require("../../../constants") const { UserStatus, EmailTemplatePurpose } = require("../../../constants")
const { checkInviteCode } = require("../../../utilities/redis") const { checkInviteCode } = require("../../../utilities/redis")