Refactor all backend stores and their usages to use new core API and handle errors

This commit is contained in:
Andrew Kingston 2022-01-24 12:37:22 +00:00
parent 816ced96df
commit 453386696f
38 changed files with 635 additions and 319 deletions

View File

@ -5,7 +5,6 @@ import {
} from "@budibase/frontend-core" } from "@budibase/frontend-core"
import { store } from "./index" import { store } from "./index"
import { get } from "svelte/store" import { get } from "svelte/store"
import { notifications } from "@budibase/bbui"
export const API = createAPIClient({ export const API = createAPIClient({
attachHeaders: headers => { attachHeaders: headers => {
@ -25,11 +24,6 @@ export const API = createAPIClient({
return return
} }
// Show a notification for any errors
if (message) {
notifications.error(`Error fetching ${url}: ${message}`)
}
// Log all errors to console // Log all errors to console
console.error(`HTTP ${status} on ${method}:${url}:\n\t${message}`) console.error(`HTTP ${status} on ${method}:${url}:\n\t${message}`)

View File

@ -38,9 +38,13 @@
}) })
function saveView() { function saveView() {
views.save(view) try {
notifications.success(`View ${view.name} saved.`) views.save(view)
analytics.captureEvent(Events.VIEW.ADDED_CALCULATE, { field: view.field }) notifications.success(`View ${view.name} saved`)
analytics.captureEvent(Events.VIEW.ADDED_CALCULATE, { field: view.field })
} catch (error) {
notifications.error("Error saving view")
}
} }
</script> </script>

View File

@ -122,7 +122,7 @@
}) })
dispatch("updatecolumns") dispatch("updatecolumns")
} catch (err) { } catch (err) {
notifications.error(err) notifications.error("Error saving column")
} }
} }
@ -131,17 +131,21 @@
} }
function deleteColumn() { function deleteColumn() {
field.name = deleteColName try {
if (field.name === $tables.selected.primaryDisplay) { field.name = deleteColName
notifications.error("You cannot delete the display column") if (field.name === $tables.selected.primaryDisplay) {
} else { notifications.error("You cannot delete the display column")
tables.deleteField(field) } else {
notifications.success(`Column ${field.name} deleted.`) tables.deleteField(field)
confirmDeleteDialog.hide() notifications.success(`Column ${field.name} deleted.`)
hide() confirmDeleteDialog.hide()
deletion = false hide()
deletion = false
dispatch("updatecolumns")
}
} catch (error) {
notifications.error("Error deleting column")
} }
dispatch("updatecolumns")
} }
function handleTypeChange(event) { function handleTypeChange(event) {

View File

@ -12,17 +12,21 @@
function saveView() { function saveView() {
if (views.includes(name)) { if (views.includes(name)) {
notifications.error(`View exists with name ${name}.`) notifications.error(`View exists with name ${name}`)
return return
} }
viewsStore.save({ try {
name, viewsStore.save({
tableId: $tables.selected._id, name,
field, tableId: $tables.selected._id,
}) field,
notifications.success(`View ${name} created`) })
analytics.captureEvent(Events.VIEW.CREATED, { name }) notifications.success(`View ${name} created`)
$goto(`../../view/${name}`) analytics.captureEvent(Events.VIEW.CREATED, { name })
$goto(`../../view/${name}`)
} catch (error) {
notifications.error("Error creating view")
}
} }
</script> </script>

View File

@ -72,11 +72,15 @@
$: schema = viewTable && viewTable.schema ? viewTable.schema : {} $: schema = viewTable && viewTable.schema ? viewTable.schema : {}
function saveView() { function saveView() {
views.save(view) try {
notifications.success(`View ${view.name} saved.`) views.save(view)
analytics.captureEvent(Events.VIEW.ADDED_FILTER, { notifications.success(`View ${view.name} saved`)
filters: JSON.stringify(view.filters), analytics.captureEvent(Events.VIEW.ADDED_FILTER, {
}) filters: JSON.stringify(view.filters),
})
} catch (error) {
notifcations.error("Error saving view")
}
} }
function removeFilter(idx) { function removeFilter(idx) {

View File

@ -19,8 +19,12 @@
.map(([key]) => key) .map(([key]) => key)
function saveView() { function saveView() {
views.save(view) try {
notifications.success(`View ${view.name} saved.`) views.save(view)
notifications.success(`View ${view.name} saved`)
} catch (error) {
notifications.error("Error saving view")
}
} }
</script> </script>

View File

@ -14,15 +14,19 @@
export let permissions export let permissions
async function changePermission(level, role) { async function changePermission(level, role) {
await permissionsStore.save({ try {
level, await permissionsStore.save({
role, level,
resource: resourceId, role,
}) resource: resourceId,
})
// Show updated permissions in UI: REMOVE // Show updated permissions in UI: REMOVE
permissions = await permissionsStore.forResource(resourceId) permissions = await permissionsStore.forResource(resourceId)
notifications.success("Updated permissions.") notifications.success("Updated permissions")
} catch (error) {
notifications.error("Error updating permissions")
}
} }
</script> </script>

View File

@ -10,6 +10,7 @@
import TableNavigator from "components/backend/TableNavigator/TableNavigator.svelte" import TableNavigator from "components/backend/TableNavigator/TableNavigator.svelte"
import { customQueryIconText, customQueryIconColor } from "helpers/data/utils" import { customQueryIconText, customQueryIconColor } from "helpers/data/utils"
import ICONS from "./icons" import ICONS from "./icons"
import { notifications } from "@budibase/bbui"
let openDataSources = [] let openDataSources = []
$: enrichedDataSources = Array.isArray($datasources.list) $: enrichedDataSources = Array.isArray($datasources.list)
@ -64,8 +65,12 @@
} }
onMount(() => { onMount(() => {
datasources.fetch() try {
queries.fetch() datasources.fetch()
queries.fetch()
} catch (error) {
notifications.error("Error fetching datasources and queries")
}
}) })
const containsActiveEntity = datasource => { const containsActiveEntity = datasource => {

View File

@ -1,5 +1,5 @@
<script> <script>
import { ModalContent, Body, Input } from "@budibase/bbui" import { ModalContent, Body, Input, notifications } from "@budibase/bbui"
import { tables, datasources } from "stores/backend" import { tables, datasources } from "stores/backend"
import { goto } from "@roxi/routify" import { goto } from "@roxi/routify"
@ -29,10 +29,14 @@
} }
async function saveTable() { async function saveTable() {
submitted = true try {
const table = await tables.save(buildDefaultTable(name, datasource._id)) submitted = true
await datasources.fetch() const table = await tables.save(buildDefaultTable(name, datasource._id))
$goto(`../../table/${table._id}`) await datasources.fetch()
$goto(`../../table/${table._id}`)
} catch (error) {
notifications.error("Error saving table")
}
} }
</script> </script>

View File

@ -90,8 +90,8 @@
await datasources.updateSchema(datasource) await datasources.updateSchema(datasource)
notifications.success(`Datasource ${name} tables updated successfully.`) notifications.success(`Datasource ${name} tables updated successfully.`)
await tables.fetch() await tables.fetch()
} catch (err) { } catch (error) {
notifications.error(`Error updating datasource schema: ${err}`) notifications.error("Error updating datasource schema")
} }
} }

View File

@ -62,9 +62,13 @@
externalDatasourceModal.hide() externalDatasourceModal.hide()
internalTableModal.show() internalTableModal.show()
} else if (integration.type === IntegrationTypes.REST) { } else if (integration.type === IntegrationTypes.REST) {
// skip modal for rest, create straight away try {
const resp = await createRestDatasource(integration) // Skip modal for rest, create straight away
$goto(`./datasource/${resp._id}`) const resp = await createRestDatasource(integration)
$goto(`./datasource/${resp._id}`)
} catch (error) {
notifications.error("Error creating datasource")
}
} else { } else {
externalDatasourceModal.show() externalDatasourceModal.show()
} }

View File

@ -20,7 +20,7 @@
$goto(`./datasource/${resp._id}`) $goto(`./datasource/${resp._id}`)
notifications.success(`Datasource updated successfully.`) notifications.success(`Datasource updated successfully.`)
} catch (err) { } catch (err) {
notifications.error(`Error saving datasource: ${err}`) notifications.error("Error saving datasource")
} }
} }

View File

@ -79,8 +79,8 @@
}) })
return true return true
} catch (err) { } catch (error) {
notifications.error(`Error importing: ${err}`) notifications.error("Error importing queries")
return false return false
} }
} }

View File

@ -12,24 +12,28 @@
let updateDatasourceDialog let updateDatasourceDialog
async function deleteDatasource() { async function deleteDatasource() {
let wasSelectedSource = $datasources.selected try {
if (!wasSelectedSource && $queries.selected) { let wasSelectedSource = $datasources.selected
const queryId = $queries.selected if (!wasSelectedSource && $queries.selected) {
wasSelectedSource = $datasources.list.find(ds => const queryId = $queries.selected
queryId.includes(ds._id) wasSelectedSource = $datasources.list.find(ds =>
)?._id queryId.includes(ds._id)
} )?._id
const wasSelectedTable = $tables.selected }
await datasources.delete(datasource) const wasSelectedTable = $tables.selected
notifications.success("Datasource deleted") await datasources.delete(datasource)
// navigate to first index page if the source you are deleting is selected notifications.success("Datasource deleted")
const entities = Object.values(datasource?.entities || {}) // Navigate to first index page if the source you are deleting is selected
if ( const entities = Object.values(datasource?.entities || {})
wasSelectedSource === datasource._id || if (
(entities && wasSelectedSource === datasource._id ||
entities.find(entity => entity._id === wasSelectedTable?._id)) (entities &&
) { entities.find(entity => entity._id === wasSelectedTable?._id))
$goto("./datasource") ) {
$goto("./datasource")
}
} catch (error) {
notifications.error("Error deleting datasource")
} }
} }
</script> </script>

View File

@ -10,26 +10,30 @@
let confirmDeleteDialog let confirmDeleteDialog
async function deleteQuery() { async function deleteQuery() {
const wasSelectedQuery = $queries.selected try {
// need to calculate this before the query is deleted const wasSelectedQuery = $queries.selected
const navigateToDatasource = wasSelectedQuery === query._id // need to calculate this before the query is deleted
const navigateToDatasource = wasSelectedQuery === query._id
await queries.delete(query) await queries.delete(query)
await datasources.fetch() await datasources.fetch()
if (navigateToDatasource) { if (navigateToDatasource) {
await datasources.select(query.datasourceId) await datasources.select(query.datasourceId)
$goto(`./datasource/${query.datasourceId}`) $goto(`./datasource/${query.datasourceId}`)
}
notifications.success("Query deleted")
} catch (error) {
notifications.error("Error deleting query")
} }
notifications.success("Query deleted")
} }
async function duplicateQuery() { async function duplicateQuery() {
try { try {
const newQuery = await queries.duplicate(query) const newQuery = await queries.duplicate(query)
onClickQuery(newQuery) onClickQuery(newQuery)
} catch (e) { } catch (error) {
notifications.error(e.message) notifications.error("Error duplicating query")
} }
} }
</script> </script>

View File

@ -49,8 +49,8 @@
if (wasSelectedTable && wasSelectedTable._id === table._id) { if (wasSelectedTable && wasSelectedTable._id === table._id) {
$goto("./table") $goto("./table")
} }
} catch (err) { } catch (error) {
notifications.error(err) notifications.error("Error deleting table")
} }
} }

View File

@ -27,11 +27,15 @@
} }
async function deleteView() { async function deleteView() {
const name = view.name try {
const id = view.tableId const name = view.name
await views.delete(name) const id = view.tableId
notifications.success("View deleted") await views.delete(name)
$goto(`./table/${id}`) notifications.success("View deleted")
$goto(`./table/${id}`)
} catch (error) {
notifications.error("Error deleting view")
}
} }
</script> </script>

View File

@ -1,5 +1,5 @@
<script> <script>
import { Label, Select } from "@budibase/bbui" import { Label, notifications, Select } from "@budibase/bbui"
import { permissions, roles } from "stores/backend" import { permissions, roles } from "stores/backend"
import { onMount } from "svelte" import { onMount } from "svelte"
import { Roles } from "constants/backend" import { Roles } from "constants/backend"
@ -13,16 +13,20 @@
let roleId, loaded let roleId, loaded
async function updateRole(role, id) { async function updateRole(role, id) {
roleId = role try {
const queryId = query?._id || id roleId = role
if (roleId && queryId) { const queryId = query?._id || id
for (let level of ["read", "write"]) { if (roleId && queryId) {
await permissions.save({ for (let level of ["read", "write"]) {
level, await permissions.save({
role, level,
resource: queryId, role,
}) resource: queryId,
})
}
} }
} catch (error) {
notifications.error("Error updating role")
} }
} }

View File

@ -71,9 +71,9 @@
} }
data = response.rows data = response.rows
fields = response.schema fields = response.schema
notifications.success("Query executed successfully.") notifications.success("Query executed successfully")
} catch (err) { } catch (error) {
notifications.error(err) notifications.error("Error previewing query")
} }
} }
@ -83,9 +83,8 @@
saveId = _id saveId = _id
notifications.success(`Query saved successfully.`) notifications.success(`Query saved successfully.`)
$goto(`../${_id}`) $goto(`../${_id}`)
} catch (err) { } catch (error) {
console.error(err) notifications.error("Error creating query")
notifications.error(`Error creating query. ${err.message}`)
} }
} }
</script> </script>

View File

@ -112,14 +112,13 @@
const { _id } = await queries.save(toSave.datasourceId, toSave) const { _id } = await queries.save(toSave.datasourceId, toSave)
saveId = _id saveId = _id
query = getSelectedQuery() query = getSelectedQuery()
notifications.success(`Request saved successfully.`) notifications.success(`Request saved successfully`)
if (dynamicVariables) { if (dynamicVariables) {
datasource.config.dynamicVariables = rebuildVariables(saveId) datasource.config.dynamicVariables = rebuildVariables(saveId)
datasource = await datasources.save(datasource) datasource = await datasources.save(datasource)
} }
} catch (err) { } catch (err) {
notifications.error(`Error saving query. ${err.message}`) notifications.error(`Error saving query`)
} }
} }
@ -127,14 +126,14 @@
try { try {
response = await queries.preview(buildQuery(query)) response = await queries.preview(buildQuery(query))
if (response.rows.length === 0) { if (response.rows.length === 0) {
notifications.info("Request did not return any data.") notifications.info("Request did not return any data")
} else { } else {
response.info = response.info || { code: 200 } response.info = response.info || { code: 200 }
schema = response.schema schema = response.schema
notifications.success("Request sent successfully.") notifications.success("Request sent successfully")
} }
} catch (err) { } catch (error) {
notifications.error(err) notifications.error("Error running query")
} }
} }
@ -226,10 +225,24 @@
) )
} }
const updateFlag = async (flag, value) => {
try {
await flags.updateFlag(flag, value)
} catch (error) {
notifications.error("Error updating flag")
}
}
onMount(async () => { onMount(async () => {
query = getSelectedQuery() query = getSelectedQuery()
// clear any unsaved changes to the datasource
await datasources.init() try {
// Clear any unsaved changes to the datasource
await datasources.init()
} catch (error) {
notifications.error("Error getting datasources")
}
datasource = $datasources.list.find(ds => ds._id === query?.datasourceId) datasource = $datasources.list.find(ds => ds._id === query?.datasourceId)
const datasourceUrl = datasource?.config.url const datasourceUrl = datasource?.config.url
const qs = query?.fields.queryString const qs = query?.fields.queryString
@ -393,8 +406,7 @@
window.open( window.open(
"https://docs.budibase.com/building-apps/data/transformers" "https://docs.budibase.com/building-apps/data/transformers"
)} )}
on:change={() => on:change={() => updateFlag("queryTransformerBanner", true)}
flags.updateFlag("queryTransformerBanner", true)}
> >
Add a JavaScript function to transform the query result. Add a JavaScript function to transform the query result.
</Banner> </Banner>

View File

@ -1,6 +1,6 @@
import { writable, get } from "svelte/store" import { writable, get } from "svelte/store"
import { queries, tables, views } from "./" import { queries, tables, views } from "./"
import api from "../../builderStore/api" import { API } from "api"
export const INITIAL_DATASOURCE_VALUES = { export const INITIAL_DATASOURCE_VALUES = {
list: [], list: [],
@ -13,23 +13,20 @@ export function createDatasourcesStore() {
const { subscribe, update, set } = store const { subscribe, update, set } = store
async function updateDatasource(response) { async function updateDatasource(response) {
if (response.status !== 200) { const { datasource, error } = response
throw new Error(await response.text())
}
const { datasource, error } = await response.json()
update(state => { update(state => {
const currentIdx = state.list.findIndex(ds => ds._id === datasource._id) const currentIdx = state.list.findIndex(ds => ds._id === datasource._id)
const sources = state.list const sources = state.list
if (currentIdx >= 0) { if (currentIdx >= 0) {
sources.splice(currentIdx, 1, datasource) sources.splice(currentIdx, 1, datasource)
} else { } else {
sources.push(datasource) sources.push(datasource)
} }
return {
return { list: sources, selected: datasource._id, schemaError: error } list: sources,
selected: datasource._id,
schemaError: error,
}
}) })
return datasource return datasource
} }
@ -38,25 +35,25 @@ export function createDatasourcesStore() {
subscribe, subscribe,
update, update,
init: async () => { init: async () => {
const response = await api.get(`/api/datasources`) const datasources = await API.getDatasources()
const json = await response.json() set({
set({ list: json, selected: null }) list: datasources,
selected: null,
})
}, },
fetch: async () => { fetch: async () => {
const response = await api.get(`/api/datasources`) const datasources = await API.getDatasources()
const json = await response.json()
// Clear selected if it no longer exists, otherwise keep it // Clear selected if it no longer exists, otherwise keep it
const selected = get(store).selected const selected = get(store).selected
let nextSelected = null let nextSelected = null
if (selected && json.find(source => source._id === selected)) { if (selected && datasources.find(source => source._id === selected)) {
nextSelected = selected nextSelected = selected
} }
update(state => ({ ...state, list: json, selected: nextSelected })) update(state => ({ ...state, list: datasources, selected: nextSelected }))
return json
}, },
select: async datasourceId => { select: datasourceId => {
update(state => ({ ...state, selected: datasourceId })) update(state => ({ ...state, selected: datasourceId }))
queries.unselect() queries.unselect()
tables.unselect() tables.unselect()
@ -66,37 +63,33 @@ export function createDatasourcesStore() {
update(state => ({ ...state, selected: null })) update(state => ({ ...state, selected: null }))
}, },
updateSchema: async datasource => { updateSchema: async datasource => {
let url = `/api/datasources/${datasource._id}/schema` const response = await API.buildDatasourceSchema(datasource?._id)
return await updateDatasource(response)
const response = await api.post(url)
return updateDatasource(response)
}, },
save: async (body, fetchSchema = false) => { save: async (body, fetchSchema = false) => {
let response let response
if (body._id) { if (body._id) {
response = await api.put(`/api/datasources/${body._id}`, body) response = await API.updateDatasource(body)
} else { } else {
response = await api.post("/api/datasources", { response = await API.createDatasource({
datasource: body, datasource: body,
fetchSchema, fetchSchema,
}) })
} }
return updateDatasource(response) return updateDatasource(response)
}, },
delete: async datasource => { delete: async datasource => {
const response = await api.delete( await API.deleteDatasource({
`/api/datasources/${datasource._id}/${datasource._rev}` datasourceId: datasource?._id,
) datasourceRev: datasource?._rev,
})
update(state => { update(state => {
const sources = state.list.filter( const sources = state.list.filter(
existing => existing._id !== datasource._id existing => existing._id !== datasource._id
) )
return { list: sources, selected: null } return { list: sources, selected: null }
}) })
await queries.fetch() await queries.fetch()
return response
}, },
removeSchemaError: () => { removeSchemaError: () => {
update(state => { update(state => {

View File

@ -1,37 +1,27 @@
import { writable } from "svelte/store" import { writable } from "svelte/store"
import api from "builderStore/api" import { API } from "api"
export function createFlagsStore() { export function createFlagsStore() {
const { subscribe, set } = writable({}) const { subscribe, set } = writable({})
return { const actions = {
subscribe,
fetch: async () => { fetch: async () => {
const { doc, response } = await getFlags() const flags = await API.getFlags()
set(doc) set(flags)
return response
}, },
updateFlag: async (flag, value) => { updateFlag: async (flag, value) => {
const response = await api.post("/api/users/flags", { await API.updateFlag({
flag, flag,
value, value,
}) })
if (response.status === 200) { await actions.fetch()
const { doc } = await getFlags()
set(doc)
}
return response
}, },
} }
}
async function getFlags() { return {
const response = await api.get("/api/users/flags") subscribe,
let doc = {} ...actions,
if (response.status === 200) {
doc = await response.json()
} }
return { doc, response }
} }
export const flags = createFlagsStore() export const flags = createFlagsStore()

View File

@ -7,12 +7,8 @@ const createIntegrationsStore = () => {
return { return {
...store, ...store,
init: async () => { init: async () => {
try { const integrations = await API.getIntegrations()
const integrations = await API.getIntegrations() store.set(integrations)
store.set(integrations)
} catch (error) {
store.set(null)
}
}, },
} }
} }

View File

@ -1,5 +1,5 @@
import { writable } from "svelte/store" import { writable } from "svelte/store"
import api from "builderStore/api" import { API } from "api"
export function createPermissionStore() { export function createPermissionStore() {
const { subscribe } = writable([]) const { subscribe } = writable([])
@ -7,14 +7,14 @@ export function createPermissionStore() {
return { return {
subscribe, subscribe,
save: async ({ level, role, resource }) => { save: async ({ level, role, resource }) => {
const response = await api.post( return await API.updatePermissionForResource({
`/api/permission/${role}/${resource}/${level}` resourceId: resource,
) roleId: role,
return await response.json() level,
})
}, },
forResource: async resourceId => { forResource: async resourceId => {
const response = await api.get(`/api/permission/${resourceId}`) return await API.getPermissionForResource(resourceId)
return await response.json()
}, },
} }
} }

View File

@ -1,6 +1,6 @@
import { writable, get } from "svelte/store" import { writable, get } from "svelte/store"
import { datasources, integrations, tables, views } from "./" import { datasources, integrations, tables, views } from "./"
import api from "builderStore/api" import { API } from "api"
import { duplicateName } from "../../helpers/duplicate" import { duplicateName } from "../../helpers/duplicate"
const sortQueries = queryList => { const sortQueries = queryList => {
@ -15,23 +15,26 @@ export function createQueriesStore() {
const actions = { const actions = {
init: async () => { init: async () => {
const response = await api.get(`/api/queries`) const queries = await API.getQueries()
const json = await response.json() set({
set({ list: json, selected: null }) list: queries,
selected: null,
})
}, },
fetch: async () => { fetch: async () => {
const response = await api.get(`/api/queries`) const queries = await API.getQueries()
const json = await response.json() sortQueries(queries)
sortQueries(json) update(state => ({
update(state => ({ ...state, list: json })) ...state,
return json list: queries,
}))
}, },
save: async (datasourceId, query) => { save: async (datasourceId, query) => {
const _integrations = get(integrations) const _integrations = get(integrations)
const dataSource = get(datasources).list.filter( const dataSource = get(datasources).list.filter(
ds => ds._id === datasourceId ds => ds._id === datasourceId
) )
// check if readable attribute is found // Check if readable attribute is found
if (dataSource.length !== 0) { if (dataSource.length !== 0) {
const integration = _integrations[dataSource[0].source] const integration = _integrations[dataSource[0].source]
const readable = integration.query[query.queryVerb].readable const readable = integration.query[query.queryVerb].readable
@ -40,34 +43,28 @@ export function createQueriesStore() {
} }
} }
query.datasourceId = datasourceId query.datasourceId = datasourceId
const response = await api.post(`/api/queries`, query) const savedQuery = await API.saveQuery(query)
if (response.status !== 200) {
throw new Error("Failed saving query.")
}
const json = await response.json()
update(state => { update(state => {
const currentIdx = state.list.findIndex(query => query._id === json._id) const idx = state.list.findIndex(query => query._id === savedQuery._id)
const queries = state.list const queries = state.list
if (idx >= 0) {
if (currentIdx >= 0) { queries.splice(idx, 1, savedQuery)
queries.splice(currentIdx, 1, json)
} else { } else {
queries.push(json) queries.push(savedQuery)
} }
sortQueries(queries) sortQueries(queries)
return { list: queries, selected: json._id } return {
list: queries,
selected: savedQuery._id,
}
}) })
return json return savedQuery
}, },
import: async body => { import: async (data, datasourceId) => {
const response = await api.post(`/api/queries/import`, body) return await API.importQueries({
datasourceId,
if (response.status !== 200) { data,
throw new Error(response.message) })
}
return response.json()
}, },
select: query => { select: query => {
update(state => ({ ...state, selected: query._id })) update(state => ({ ...state, selected: query._id }))
@ -79,48 +76,37 @@ export function createQueriesStore() {
update(state => ({ ...state, selected: null })) update(state => ({ ...state, selected: null }))
}, },
preview: async query => { preview: async query => {
const response = await api.post("/api/queries/preview", { const parameters = query.parameters.reduce(
fields: query.fields, (acc, next) => ({
queryVerb: query.queryVerb, ...acc,
transformer: query.transformer, [next.name]: next.default,
parameters: query.parameters.reduce( }),
(acc, next) => ({ {}
...acc, )
[next.name]: next.default, const result = await API.previewQuery({
}), ...query,
{} parameters,
),
datasourceId: query.datasourceId,
queryId: query._id || undefined,
}) })
if (response.status !== 200) {
const error = await response.text()
throw `Query error: ${error}`
}
const json = await response.json()
// Assume all the fields are strings and create a basic schema from the // Assume all the fields are strings and create a basic schema from the
// unique fields returned by the server // unique fields returned by the server
const schema = {} const schema = {}
for (let field of json.schemaFields) { for (let field of result.schemaFields) {
schema[field] = "string" schema[field] = "string"
} }
return { ...json, schema, rows: json.rows || [] } return { ...result, schema, rows: result.rows || [] }
}, },
delete: async query => { delete: async query => {
const response = await api.delete( await API.deleteQuery({
`/api/queries/${query._id}/${query._rev}` queryId: query?._id,
) queryRev: query?._rev,
})
update(state => { update(state => {
state.list = state.list.filter(existing => existing._id !== query._id) state.list = state.list.filter(existing => existing._id !== query._id)
if (state.selected === query._id) { if (state.selected === query._id) {
state.selected = null state.selected = null
} }
return state return state
}) })
return response
}, },
duplicate: async query => { duplicate: async query => {
let list = get(store).list let list = get(store).list

View File

@ -1,30 +1,32 @@
import { writable } from "svelte/store" import { writable } from "svelte/store"
import api from "builderStore/api" import { API } from "api"
export function createRolesStore() { export function createRolesStore() {
const { subscribe, update, set } = writable([]) const { subscribe, update, set } = writable([])
return { const actions = {
subscribe,
fetch: async () => { fetch: async () => {
set(await getRoles()) const roles = await API.getRoles()
set(roles)
}, },
delete: async role => { delete: async role => {
const response = await api.delete(`/api/roles/${role._id}/${role._rev}`) await API.deleteRole({
roleId: role?._id,
roleRev: role?._rev,
})
update(state => state.filter(existing => existing._id !== role._id)) update(state => state.filter(existing => existing._id !== role._id))
return response
}, },
save: async role => { save: async role => {
const response = await api.post("/api/roles", role) const savedRole = await API.saveRole(role)
set(await getRoles()) await actions.fetch()
return response return savedRole
}, },
} }
return {
subscribe,
...actions,
}
} }
async function getRoles() {
const response = await api.get("/api/roles")
return await response.json()
}
export const roles = createRolesStore() export const roles = createRolesStore()

View File

@ -1,7 +1,7 @@
import { get, writable } from "svelte/store" import { get, writable } from "svelte/store"
import { datasources, queries, views } from "./" import { datasources, queries, views } from "./"
import { cloneDeep } from "lodash/fp" import { cloneDeep } from "lodash/fp"
import api from "builderStore/api" import { API } from "api"
import { SWITCHABLE_TYPES } from "../../constants/backend" import { SWITCHABLE_TYPES } from "../../constants/backend"
export function createTablesStore() { export function createTablesStore() {
@ -9,10 +9,11 @@ export function createTablesStore() {
const { subscribe, update, set } = store const { subscribe, update, set } = store
async function fetch() { async function fetch() {
const tablesResponse = await api.get(`/api/tables`) const tables = await API.getTables()
const tables = await tablesResponse.json() update(state => ({
update(state => ({ ...state, list: tables })) ...state,
return tables list: tables,
}))
} }
async function select(table) { async function select(table) {
@ -38,16 +39,16 @@ export function createTablesStore() {
const oldTable = get(store).list.filter(t => t._id === table._id)[0] const oldTable = get(store).list.filter(t => t._id === table._id)[0]
const fieldNames = [] const fieldNames = []
// update any renamed schema keys to reflect their names // Update any renamed schema keys to reflect their names
for (let key of Object.keys(updatedTable.schema)) { for (let key of Object.keys(updatedTable.schema)) {
// if field name has been seen before remove it // If field name has been seen before remove it
if (fieldNames.indexOf(key.toLowerCase()) !== -1) { if (fieldNames.indexOf(key.toLowerCase()) !== -1) {
delete updatedTable.schema[key] delete updatedTable.schema[key]
continue continue
} }
const field = updatedTable.schema[key] const field = updatedTable.schema[key]
const oldField = oldTable?.schema[key] const oldField = oldTable?.schema[key]
// if the type has changed then revert back to the old field // If the type has changed then revert back to the old field
if ( if (
oldField != null && oldField != null &&
oldField?.type !== field.type && oldField?.type !== field.type &&
@ -55,21 +56,17 @@ export function createTablesStore() {
) { ) {
updatedTable.schema[key] = oldField updatedTable.schema[key] = oldField
} }
// field has been renamed // Field has been renamed
if (field.name && field.name !== key) { if (field.name && field.name !== key) {
updatedTable.schema[field.name] = field updatedTable.schema[field.name] = field
updatedTable._rename = { old: key, updated: field.name } updatedTable._rename = { old: key, updated: field.name }
delete updatedTable.schema[key] delete updatedTable.schema[key]
} }
// finally record this field has been used // Finally record this field has been used
fieldNames.push(key.toLowerCase()) fieldNames.push(key.toLowerCase())
} }
const response = await api.post(`/api/tables`, updatedTable) const savedTable = await API.saveTable(updatedTable)
if (response.status !== 200) {
throw (await response.json()).message
}
const savedTable = await response.json()
await fetch() await fetch()
if (table.type === "external") { if (table.type === "external") {
await datasources.fetch() await datasources.fetch()
@ -91,21 +88,18 @@ export function createTablesStore() {
}, },
save, save,
init: async () => { init: async () => {
const response = await api.get("/api/tables") const tables = await API.getTables()
const json = await response.json()
set({ set({
list: json, list: tables,
selected: {}, selected: {},
draft: {}, draft: {},
}) })
}, },
delete: async table => { delete: async table => {
const response = await api.delete( await API.deleteTable({
`/api/tables/${table._id}/${table._rev}` tableId: table?._id,
) tableRev: table?._rev,
if (response.status !== 200) { })
throw (await response.json()).message
}
update(state => ({ update(state => ({
...state, ...state,
list: state.list.filter(existing => existing._id !== table._id), list: state.list.filter(existing => existing._id !== table._id),
@ -156,12 +150,16 @@ export function createTablesStore() {
await promise await promise
} }
}, },
deleteField: field => { deleteField: async field => {
let promise
update(state => { update(state => {
delete state.draft.schema[field.name] delete state.draft.schema[field.name]
save(state.draft) promise = save(state.draft)
return state return state
}) })
if (promise) {
await promise
}
}, },
} }
} }

View File

@ -1,6 +1,6 @@
import { writable, get } from "svelte/store" import { writable, get } from "svelte/store"
import { tables, datasources, queries } from "./" import { tables, datasources, queries } from "./"
import api from "builderStore/api" import { API } from "api"
export function createViewsStore() { export function createViewsStore() {
const { subscribe, update } = writable({ const { subscribe, update } = writable({
@ -11,7 +11,7 @@ export function createViewsStore() {
return { return {
subscribe, subscribe,
update, update,
select: async view => { select: view => {
update(state => ({ update(state => ({
...state, ...state,
selected: view, selected: view,
@ -27,16 +27,14 @@ export function createViewsStore() {
})) }))
}, },
delete: async view => { delete: async view => {
await api.delete(`/api/views/${view}`) await API.deleteView(view)
await tables.fetch() await tables.fetch()
}, },
save: async view => { save: async view => {
const response = await api.post(`/api/views`, view) const savedView = await API.saveView(view)
const json = await response.json()
const viewMeta = { const viewMeta = {
name: view.name, name: view.name,
...json, ...savedView,
} }
const viewTable = get(tables).list.find( const viewTable = get(tables).list.find(

View File

@ -1,5 +1,6 @@
import { writable } from "svelte/store" import { writable } from "svelte/store"
import api from "builderStore/api" import { API } from "api"
import { notifications } from "@budibase/bbui"
export function createEmailStore() { export function createEmailStore() {
const store = writable({}) const store = writable({})
@ -8,34 +9,35 @@ export function createEmailStore() {
subscribe: store.subscribe, subscribe: store.subscribe,
templates: { templates: {
fetch: async () => { fetch: async () => {
// fetch the email template definitions try {
const response = await api.get(`/api/global/template/definitions`) // fetch the email template definitions and templates
const definitions = await response.json() const definitions = await API.getEmailTemplateDefinitions()
const templates = await API.getEmailTemplates()
// fetch the email templates themselves store.set({
const templatesResponse = await api.get(`/api/global/template/email`) definitions,
const templates = await templatesResponse.json() templates,
})
store.set({ } catch (error) {
definitions, notifications.error("Error fetching email templates")
templates, store.set({})
}) }
}, },
save: async template => { save: async template => {
// Save your template config try {
const response = await api.post(`/api/global/template`, template) // Save your template config
const json = await response.json() const savedTemplate = await API.saveEmailTemplate(template)
if (response.status !== 200) throw new Error(json.message) template._rev = savedTemplate._rev
template._rev = json._rev template._id = savedTemplate._id
template._id = json._id store.update(state => {
const currentIdx = state.templates.findIndex(
store.update(state => { template => template.purpose === savedTemplate.purpose
const currentIdx = state.templates.findIndex( )
template => template.purpose === json.purpose state.templates.splice(currentIdx, 1, template)
) return state
state.templates.splice(currentIdx, 1, template) })
return state } catch (error) {
}) notifications.error("Error saving email template")
}
}, },
}, },
} }

View File

@ -0,0 +1,57 @@
export const buildDatasourceEndpoints = API => ({
/**
* Gets a list of datasources.
*/
getDatasources: async () => {
return await API.get({
url: "/api/datasources",
})
},
/**
* Prompts the server to build the schema for a datasource.
* @param datasourceId the datasource ID to build the schema for
*/
buildDatasourceSchema: async datasourceId => {
return await API.post({
url: `/api/datasources/${datasourceId}/schema`,
})
},
/**
* Creates a datasource
* @param datasource the datasource to create
* @param fetchSchema whether to fetch the schema or not
*/
createDatasource: async ({ datasource, fetchSchema }) => {
return await API.post({
url: "/api/datasources",
body: {
datasource,
fetchSchema,
},
})
},
/**
* Updates a datasource
* @param datasource the datasource to update
*/
updateDatasource: async datasource => {
return await API.put({
url: `/api/datasources/${datasource._id}`,
body: datasource,
})
},
/**
* Deletes a datasource.
* @param datasourceId the ID of the ddtasource to delete
* @param datasourceRev the rev of the datasource to delete
*/
deleteDatasource: async ({ datasourceId, datasourceRev }) => {
return await API.delete({
url: `/api/datasources/${datasourceId}/${datasourceRev}`,
})
},
})

View File

@ -0,0 +1,25 @@
export const buildFlagEndpoints = API => ({
/**
* Gets the current user flags object.
*/
getFlags: async () => {
return await API.get({
url: "/api/users/flags",
})
},
/**
* Updates a flag for the current user.
* @param flag the flag to update
* @param value the value to set the flag to
*/
updateFlag: async ({ flag, value }) => {
return await API.post({
url: "/api/users/flags",
body: {
flag,
value,
},
})
},
})

View File

@ -4,13 +4,18 @@ import { buildAppEndpoints } from "./app"
import { buildAttachmentEndpoints } from "./attachments" import { buildAttachmentEndpoints } from "./attachments"
import { buildAuthEndpoints } from "./auth" import { buildAuthEndpoints } from "./auth"
import { buildAutomationEndpoints } from "./automations" import { buildAutomationEndpoints } from "./automations"
import { buildDatasourceEndpoints } from "./datasources"
import { buildFlagEndpoints } from "./flags"
import { buildHostingEndpoints } from "./hosting" import { buildHostingEndpoints } from "./hosting"
import { buildPermissionsEndpoints } from "./permissions"
import { buildQueryEndpoints } from "./queries" import { buildQueryEndpoints } from "./queries"
import { buildRelationshipEndpoints } from "./relationships" import { buildRelationshipEndpoints } from "./relationships"
import { buildRoleEndpoints } from "./roles"
import { buildRouteEndpoints } from "./routes" import { buildRouteEndpoints } from "./routes"
import { buildRowEndpoints } from "./rows" import { buildRowEndpoints } from "./rows"
import { buildScreenEndpoints } from "./screens" import { buildScreenEndpoints } from "./screens"
import { buildTableEndpoints } from "./tables" import { buildTableEndpoints } from "./tables"
import { buildTemplateEndpoints } from "./templates"
import { buildViewEndpoints } from "./views" import { buildViewEndpoints } from "./views"
const defaultAPIClientConfig = { const defaultAPIClientConfig = {
@ -184,13 +189,18 @@ export const createAPIClient = config => {
...buildAttachmentEndpoints(API), ...buildAttachmentEndpoints(API),
...buildAuthEndpoints(API), ...buildAuthEndpoints(API),
...buildAutomationEndpoints(API), ...buildAutomationEndpoints(API),
...buildDatasourceEndpoints(API),
...buildFlagEndpoints(API),
...buildHostingEndpoints(API), ...buildHostingEndpoints(API),
...buildPermissionsEndpoints(API),
...buildQueryEndpoints(API), ...buildQueryEndpoints(API),
...buildRelationshipEndpoints(API), ...buildRelationshipEndpoints(API),
...buildRoleEndpoints(API),
...buildRouteEndpoints(API), ...buildRouteEndpoints(API),
...buildRowEndpoints(API), ...buildRowEndpoints(API),
...buildScreenEndpoints(API), ...buildScreenEndpoints(API),
...buildTableEndpoints(API), ...buildTableEndpoints(API),
...buildTemplateEndpoints(API),
...buildViewEndpoints(API), ...buildViewEndpoints(API),
} }

View File

@ -0,0 +1,24 @@
export const buildPermissionsEndpoints = API => ({
/**
* Gets the permission required to access a specific resource
* @param resourceId the resource ID to check
*/
getPermissionForResource: async resourceId => {
return await API.get({
url: `/api/permission/${resourceId}`,
})
},
/**
* Updates the permissions for a certain resource
* @param resourceId the ID of the resource to update
* @param roleId the ID of the role to update the permissions of
* @param level the level to assign the role for this resource
* @return {Promise<*>}
*/
updatePermissionForResource: async ({ resourceId, roleId, level }) => {
return await API.post({
url: `/api/permission/${roleId}/${resourceId}/${level}`,
})
},
})

View File

@ -1,6 +1,9 @@
export const buildQueryEndpoints = API => ({ export const buildQueryEndpoints = API => ({
/** /**
* Executes a query against an external data connector. * Executes a query against an external data connector.
* @param queryId the ID of the query to execute
* @param pagination pagination info for the query
* @param parameters parameters for the query
*/ */
executeQuery: async ({ queryId, pagination, parameters }) => { executeQuery: async ({ queryId, pagination, parameters }) => {
return await API.post({ return await API.post({
@ -14,6 +17,7 @@ export const buildQueryEndpoints = API => ({
/** /**
* Fetches the definition of an external query. * Fetches the definition of an external query.
* @param queryId the ID of thr query to fetch the definition of
*/ */
fetchQueryDefinition: async queryId => { fetchQueryDefinition: async queryId => {
return await API.get({ return await API.get({
@ -21,4 +25,61 @@ export const buildQueryEndpoints = API => ({
cache: true, cache: true,
}) })
}, },
/**
* Gets a list of queries
*/
getQueries: async () => {
return await API.get({
url: "/api/queries",
})
},
/**
* Saves a query.
* @param query the query to save
*/
saveQuery: async query => {
return await API.post({
url: "/api/queries",
body: query,
})
},
/**
* Deletes a query
* @param queryId the ID of the query to delete
* @param queryRev the rev of the query to delete
*/
deleteQuery: async ({ queryId, queryRev }) => {
return await API.delete({
url: `/api/queries/${queryId}/${queryRev}`,
})
},
/**
* Imports a set of queries into a certain datasource
* @param datasourceId the datasource ID to import queries into
* @param data the data string of the content to import
*/
importQueries: async ({ datasourceId, data }) => {
return await API.post({
url: "/api/queries/import",
body: {
datasourceId,
data,
},
})
},
/**
* Runs a query with test parameters to see the result.
* @param query the query to run
*/
previewQuery: async query => {
return await API.post({
url: "/api/queries/preview",
body: query,
})
},
}) })

View File

@ -0,0 +1,32 @@
export const buildRoleEndpoints = API => ({
/**
* Deletes a role.
* @param roleId the ID of the role to delete
* @param roleRev the rev of the role to delete
*/
deleteRole: async ({ roleId, roleRev }) => {
return await API.delete({
url: `/api/roles/${roleId}/${roleRev}`,
})
},
/**
* Saves a role.
* @param role the role to save
*/
saveRole: async role => {
return await API.post({
url: "/api/roles",
body: role,
})
},
/**
* Gets a list of roles.
*/
getRoles: async () => {
return await API.get({
url: "/api/roles",
})
},
})

View File

@ -79,4 +79,35 @@ export const buildTableEndpoints = API => ({
}, },
}) })
}, },
/**
* Gets a list o tables.
*/
getTables: async () => {
return await API.get({
url: "/api/tables",
})
},
/**
* Saves a table.
* @param table the table to save
*/
saveTable: async table => {
return await API.post({
url: "/api/tables",
body: table,
})
},
/**
* Deletes a table.
* @param tableId the ID of the table to delete
* @param tableRev the rev of the table to delete
*/
deleteTable: async ({ tableId, tableRev }) => {
return await API.delete({
url: `/api/tables/${tableId}/${tableRev}`,
})
},
}) })

View File

@ -0,0 +1,28 @@
export const buildTemplateEndpoints = API => ({
/**
* Gets the list of email template definitions.
*/
getEmailTemplateDefinitions: async () => {
return await API.get({ url: "/api/global/template/definitions" })
},
/**
* Gets the list of email templates.
*/
getEmailTemplates: async () => {
return await API.get({ url: "/api/global/template/email" })
},
/**
* Saves an email template.
* @param template the template to save
*/
saveEmailTemplate: async template => {
return await API.post({
url: "/api/global/template",
body: {
template,
},
})
},
})

View File

@ -1,6 +1,10 @@
export const buildViewEndpoints = API => ({ export const buildViewEndpoints = API => ({
/** /**
* Fetches all rows in a view. * Fetches all rows in a view
* @param name the name of the view
* @param field the field to perform the calculation on
* @param groupBy the field to group by
* @param calculation the calculation to perform
*/ */
fetchViewData: async ({ name, field, groupBy, calculation }) => { fetchViewData: async ({ name, field, groupBy, calculation }) => {
const params = new URLSearchParams() const params = new URLSearchParams()
@ -9,7 +13,7 @@ export const buildViewEndpoints = API => ({
params.set("calculation", calculation) params.set("calculation", calculation)
} }
if (groupBy) { if (groupBy) {
params.set("group", groupBy ? "true" : "false") params.set("group", groupBy)
} }
const QUERY_VIEW_URL = field const QUERY_VIEW_URL = field
? `/api/views/${name}?${params}` ? `/api/views/${name}?${params}`
@ -31,4 +35,25 @@ export const buildViewEndpoints = API => ({
}, },
}) })
}, },
/**
* Saves a view.
* @param view the view to save
*/
saveView: async view => {
return await API.post({
url: "/api/views",
body: view,
})
},
/**
* Deletes a view.
* @param viewName the name of the view to delete
*/
deleteView: async viewName => {
return await API.delete({
url: `/api/views/${viewName}`,
})
},
}) })