Add more work on API refactor in builder

This commit is contained in:
Andrew Kingston 2022-01-21 09:10:59 +00:00
parent e6cddcca3e
commit f100ae42fd
17 changed files with 343 additions and 211 deletions

View File

@ -14,16 +14,25 @@ export const API = createAPIClient({
},
onError: error => {
const { url, message, status } = error
const { url, message, status, method, handled } = error || {}
// Log all API errors to Sentry
// analytics.captureException(error)
// Log any errors that we haven't manually handled
if (!handled) {
console.error("Unhandled error from API client", error)
return
}
// Show a notification for any errors
if (message) {
notifications.error(`Error fetching ${url}: ${message}`)
}
// Log all errors to console
console.error(`HTTP ${status} on ${method}:${url}:\n\t${message}`)
// Logout on 403's
if (status === 403) {
// Don't do anything if fetching templates.

View File

@ -1,6 +1,6 @@
<script>
import { Dropzone, notifications } from "@budibase/bbui"
import api from "builderStore/api"
import { API } from "api"
export let value = []
export let label
@ -20,8 +20,12 @@
for (let i = 0; i < fileList.length; i++) {
data.append("file", fileList[i])
}
const response = await api.post(`/api/attachments/process`, data, {})
return await response.json()
try {
return await API.uploadBuilderAttachment(data)
} catch (error) {
notifications.error("Failed to upload attachment")
return []
}
}
</script>

View File

@ -9,7 +9,7 @@
async function deployApp() {
try {
await API.deployApp()
await API.deployAppChanges()
analytics.captureEvent(Events.APP.PUBLISHED, {
appId: $store.appId,
})

View File

@ -16,7 +16,7 @@
const revert = async () => {
try {
await API.revertApp(appId)
await API.revertAppChanges(appId)
// Reset frontend state after revert
const applicationPkg = await API.fetchAppPackage(appId)

View File

@ -8,7 +8,7 @@
Button,
} from "@budibase/bbui"
import { store } from "builderStore"
import api from "builderStore/api"
import { API } from "api"
import clientPackage from "@budibase/client/package.json"
let updateModal
@ -18,26 +18,17 @@
$: revertAvailable = $store.revertableVersion != null
const refreshAppPackage = async () => {
const applicationPkg = await api.get(
`/api/applications/${appId}/appPackage`
)
const pkg = await applicationPkg.json()
if (applicationPkg.ok) {
try {
const pkg = await API.fetchAppPackage(appId)
await store.actions.initialise(pkg)
} else {
throw new Error(pkg)
} catch (error) {
notifications.error("Error fetching app package")
}
}
const update = async () => {
try {
const response = await api.post(
`/api/applications/${appId}/client/update`
)
const json = await response.json()
if (response.status !== 200) {
throw json.message
}
await API.updateAppClientVersion(appId)
// Don't wait for the async refresh, since this causes modal flashing
refreshAppPackage()
@ -47,23 +38,17 @@
} catch (err) {
notifications.error(`Error updating app: ${err}`)
}
updateModal.hide()
}
const revert = async () => {
try {
const revertableVersion = $store.revertableVersion
const response = await api.post(
`/api/applications/${appId}/client/revert`
)
const json = await response.json()
if (response.status !== 200) {
throw json.message
}
await API.revertAppClientVersion(appId)
// Don't wait for the async refresh, since this causes modal flashing
refreshAppPackage()
notifications.success(
`App reverted successfully to version ${revertableVersion}`
`App reverted successfully to version ${$store.revertableVersion}`
)
} catch (err) {
notifications.error(`Error reverting app: ${err}`)

View File

@ -5,7 +5,7 @@
import { store, automationStore, hostingStore } from "builderStore"
import { admin, auth } from "stores/portal"
import { string, mixed, object } from "yup"
import api, { get, post } from "builderStore/api"
import { API } from "api"
import analytics, { Events } from "analytics"
import { onMount } from "svelte"
import { capitalise } from "helpers"
@ -99,40 +99,24 @@
}
// Create App
const appResp = await post("/api/applications", data, {})
const appJson = await appResp.json()
if (!appResp.ok) {
throw new Error(appJson.message)
}
const createdApp = await API.createApp(data)
analytics.captureEvent(Events.APP.CREATED, {
name: $values.name,
appId: appJson.instance._id,
appId: createdApp.instance._id,
templateToUse,
})
// Select Correct Application/DB in prep for creating user
const applicationPkg = await get(
`/api/applications/${appJson.instance._id}/appPackage`
)
const pkg = await applicationPkg.json()
if (applicationPkg.ok) {
await store.actions.initialise(pkg)
await automationStore.actions.fetch()
// update checklist - incase first app
await admin.init()
} else {
throw new Error(pkg)
}
const pkg = await API.fetchAppPackage(createdApp.instance._id)
await store.actions.initialise(pkg)
await automationStore.actions.fetch()
// Update checklist - in case first app
await admin.init()
// Create user
const user = {
roleId: $values.roleId,
}
const userResp = await api.post(`/api/users/metadata/self`, user)
await userResp.json()
await API.updateOwnMetadata({ roleId: $values.roleId })
await auth.setInitInfo({})
$goto(`/builder/app/${appJson.instance._id}`)
$goto(`/builder/app/${createdApp.instance._id}`)
} catch (error) {
console.error(error)
notifications.error(error)

View File

@ -1,6 +1,6 @@
<script>
import { notifications, ModalContent, Dropzone, Body } from "@budibase/bbui"
import { post } from "builderStore/api"
import { API } from "api"
import { admin } from "stores/portal"
let submitting = false
@ -9,24 +9,19 @@
async function importApps() {
submitting = true
try {
// Create form data to create app
let data = new FormData()
data.append("importFile", value.file)
// Create App
const importResp = await post("/api/cloud/import", data, {})
const importJson = await importResp.json()
if (!importResp.ok) {
throw new Error(importJson.message)
}
await API.importApps(data)
await admin.checkImportComplete()
notifications.success("Import complete, please finish registration!")
} catch (error) {
notifications.error(error)
submitting = false
notifications.error("Failed to import apps")
}
submitting = false
}
</script>
@ -36,10 +31,10 @@
onConfirm={importApps}
disabled={!value.file}
>
<Body
>Please upload the file that was exported from your Cloud environment to get
started</Body
>
<Body>
Please upload the file that was exported from your Cloud environment to get
started
</Body>
<Dropzone
gallery={false}
label="File to import"

View File

@ -10,7 +10,7 @@
Modal,
} from "@budibase/bbui"
import { goto } from "@roxi/routify"
import api from "builderStore/api"
import { API } from "api"
import { admin, auth } from "stores/portal"
import PasswordRepeatInput from "components/common/users/PasswordRepeatInput.svelte"
import ImportAppsModal from "./_components/ImportAppsModal.svelte"
@ -30,11 +30,7 @@
try {
adminUser.tenantId = tenantId
// Save the admin user
const response = await api.post(`/api/global/users/init`, adminUser)
const json = await response.json()
if (response.status !== 200) {
throw new Error(json.message)
}
await API.createAdminUser(adminUser)
notifications.success(`Admin user created`)
await admin.init()
$goto("../portal")

View File

@ -19,7 +19,7 @@
import ChooseIconModal from "components/start/ChooseIconModal.svelte"
import { store, automationStore } from "builderStore"
import api, { del, post, get } from "builderStore/api"
import { API } from "api"
import { onMount } from "svelte"
import { apps, auth, admin, templates } from "stores/portal"
import download from "downloadjs"
@ -116,43 +116,30 @@
data.append("templateKey", template.key)
// Create App
const appResp = await post("/api/applications", data, {})
const appJson = await appResp.json()
if (!appResp.ok) {
throw new Error(appJson.message)
}
const createdApp = await API.createApp(data)
analytics.captureEvent(Events.APP.CREATED, {
name: appName,
appId: appJson.instance._id,
appId: createdApp.instance._id,
template,
fromTemplateMarketplace: true,
})
// Select Correct Application/DB in prep for creating user
const applicationPkg = await get(
`/api/applications/${appJson.instance._id}/appPackage`
)
const pkg = await applicationPkg.json()
if (applicationPkg.ok) {
await store.actions.initialise(pkg)
await automationStore.actions.fetch()
// update checklist - incase first app
await admin.init()
} else {
throw new Error(pkg)
}
const pkg = await API.fetchAppPackage(createdApp.instance._id)
await store.actions.initialise(pkg)
await automationStore.actions.fetch()
// Update checklist - in case first app
await admin.init()
// Create user
const userResp = await api.post(`/api/users/metadata/self`, {
await API.updateOwnMetadata({
roleId: "BASIC",
})
await userResp.json()
await auth.setInitInfo({})
$goto(`/builder/app/${appJson.instance._id}`)
$goto(`/builder/app/${createdApp.instance._id}`)
} catch (error) {
notifications.error("Error creating app")
console.error(error)
notifications.error(error)
}
}
@ -202,17 +189,11 @@
return
}
try {
const response = await del(
`/api/applications/${selectedApp.prodId}?unpublish=1`
)
if (response.status !== 200) {
const json = await response.json()
throw json.message
}
await API.unpublishApp(selectedApp.prodId)
await apps.load()
notifications.success("App unpublished successfully")
} catch (err) {
notifications.error(`Error unpublishing app: ${err}`)
notifications.error("Error unpublishing app")
}
}
@ -226,17 +207,13 @@
return
}
try {
const response = await del(`/api/applications/${selectedApp?.devId}`)
if (response.status !== 200) {
const json = await response.json()
throw json.message
}
await API.deleteApp(selectedApp?.devId)
await apps.load()
// get checklist, just in case that was the last app
// Get checklist, just in case that was the last app
await admin.init()
notifications.success("App deleted successfully")
} catch (err) {
notifications.error(`Error deleting app: ${err}`)
notifications.error("Error deleting app")
}
selectedApp = null
appName = null
@ -249,15 +226,11 @@
const releaseLock = async app => {
try {
const response = await del(`/api/dev/${app.devId}/lock`)
if (response.status !== 200) {
const json = await response.json()
throw json.message
}
await API.releaseAppLock(app.devId)
await apps.load()
notifications.success("Lock released successfully")
} catch (err) {
notifications.error(`Error releasing lock: ${err}`)
notifications.error("Error releasing lock")
}
}

View File

@ -14,7 +14,7 @@
Checkbox,
} from "@budibase/bbui"
import { email } from "stores/portal"
import api from "builderStore/api"
import { API } from "api"
import { cloneDeep } from "lodash/fp"
import analytics, { Events } from "analytics"
@ -54,55 +54,48 @@
delete smtp.config.auth
}
// Save your SMTP config
const response = await api.post(`/api/global/configs`, smtp)
if (response.status !== 200) {
const error = await response.text()
let message
try {
message = JSON.parse(error).message
} catch (err) {
message = error
}
notifications.error(`Failed to save email settings, reason: ${message}`)
} else {
const json = await response.json()
smtpConfig._rev = json._rev
smtpConfig._id = json._id
notifications.success(`Settings saved.`)
try {
const savedConfig = await API.saveConfig(smtp)
smtpConfig._rev = savedConfig._rev
smtpConfig._id = savedConfig._id
notifications.success(`Settings saved`)
analytics.captureEvent(Events.SMTP.SAVED)
} catch (error) {
notifications.error(
`Failed to save email settings, reason: ${error?.message || "Unknown"}`
)
}
}
async function fetchSmtp() {
loading = true
// fetch the configs for smtp
const smtpResponse = await api.get(
`/api/global/configs/${ConfigTypes.SMTP}`
)
const smtpDoc = await smtpResponse.json()
if (!smtpDoc._id) {
smtpConfig = {
type: ConfigTypes.SMTP,
config: {
secure: true,
},
try {
// Fetch the configs for smtp
const smtpDoc = await API.getConfig(ConfigTypes.SMTP)
if (!smtpDoc._id) {
smtpConfig = {
type: ConfigTypes.SMTP,
config: {
secure: true,
},
}
} else {
smtpConfig = smtpDoc
}
} else {
smtpConfig = smtpDoc
}
loading = false
requireAuth = smtpConfig.config.auth != null
// always attach the auth for the forms purpose -
// this will be removed later if required
if (!smtpDoc.config) {
smtpDoc.config = {}
}
if (!smtpDoc.config.auth) {
smtpConfig.config.auth = {
type: "login",
loading = false
requireAuth = smtpConfig.config.auth != null
// Always attach the auth for the forms purpose -
// this will be removed later if required
if (!smtpDoc.config) {
smtpDoc.config = {}
}
if (!smtpDoc.config.auth) {
smtpConfig.config.auth = {
type: "login",
}
}
} catch (error) {
notifications.error("Error fetching SMTP config")
}
}

View File

@ -1,5 +1,5 @@
import { writable, get } from "svelte/store"
import api from "builderStore/api"
import { API } from "api"
import { auth } from "stores/portal"
export function createAdminStore() {
@ -25,21 +25,19 @@ export function createAdminStore() {
async function init() {
try {
const tenantId = get(auth).tenantId
const response = await api.get(
`/api/global/configs/checklist?tenantId=${tenantId}`
)
const json = await response.json()
const totalSteps = Object.keys(json).length
const completedSteps = Object.values(json).filter(x => x?.checked).length
const checklist = await API.getChecklist(tenantId)
const totalSteps = Object.keys(checklist).length
const completedSteps = Object.values(checklist).filter(
x => x?.checked
).length
await getEnvironment()
admin.update(store => {
store.loaded = true
store.checklist = json
store.checklist = checklist
store.onboardingProgress = (completedSteps / totalSteps) * 100
return store
})
} catch (err) {
} catch (error) {
admin.update(store => {
store.checklist = null
return store
@ -48,11 +46,15 @@ export function createAdminStore() {
}
async function checkImportComplete() {
const response = await api.get(`/api/cloud/import/complete`)
if (response.status === 200) {
const json = await response.json()
try {
const result = await API.checkImportComplete()
admin.update(store => {
store.importComplete = json ? json.imported : false
store.importComplete = result ? result.imported : false
return store
})
} catch (error) {
admin.update(store => {
store.importComplete = false
return store
})
}
@ -65,15 +67,14 @@ export function createAdminStore() {
let accountPortalUrl = ""
let isDev = false
try {
const response = await api.get(`/api/system/environment`)
const json = await response.json()
multiTenancyEnabled = json.multiTenancy
cloud = json.cloud
disableAccountPortal = json.disableAccountPortal
accountPortalUrl = json.accountPortalUrl
isDev = json.isDev
const environment = await API.getEnvironment()
multiTenancyEnabled = environment.multiTenancy
cloud = environment.cloud
disableAccountPortal = environment.disableAccountPortal
accountPortalUrl = environment.accountPortalUrl
isDev = environment.isDev
} catch (err) {
// just let it stay disabled
// Just let it stay disabled
}
admin.update(store => {
store.multiTenancy = multiTenancyEnabled

View File

@ -1,5 +1,5 @@
import { writable, get } from "svelte/store"
import api from "builderStore/api"
import { API } from "api"
import { auth } from "stores/portal"
const OIDC_CONFIG = {
@ -14,16 +14,17 @@ export function createOidcStore() {
async function init() {
const tenantId = get(auth).tenantId
const res = await api.get(
`/api/global/configs/public/oidc?tenantId=${tenantId}`
)
const json = await res.json()
if (json.status === 400 || Object.keys(json).length === 0) {
try {
const config = await API.getOIDCConfig(tenantId)
if (Object.keys(config || {}).length) {
// Just use the first config for now.
// We will be support multiple logins buttons later on.
set(...config)
} else {
set(OIDC_CONFIG)
}
} catch (error) {
set(OIDC_CONFIG)
} else {
// Just use the first config for now. We will be support multiple logins buttons later on.
set(...json)
}
}

View File

@ -18,9 +18,21 @@ export const API = createAPIClient({
// We could also log these to sentry.
// Or we could check error.status and redirect to login on a 403 etc.
onError: error => {
if (error.message) {
notificationStore.actions.error(error.message)
const { status, method, url, message, handled } = error || {}
// Log any errors that we haven't manually handled
if (!handled) {
console.error("Unhandled error from API client", error)
return
}
// Notify all errors
if (message) {
notificationStore.actions.error(message)
}
// Log all errors to console
console.error(`HTTP ${status} on ${method}:${url}:\n\t${message}`)
},
// Patch certain endpoints with functionality specific to client apps

View File

@ -25,7 +25,7 @@ export const buildAppEndpoints = API => ({
/**
* Deploys the current app.
*/
deployApp: async () => {
deployAppChanges: async () => {
return await API.post({
url: "/api/deploy",
})
@ -35,12 +35,32 @@ export const buildAppEndpoints = API => ({
* Reverts an app to a previous version.
* @param appId the app ID to revert
*/
revertApp: async appId => {
revertAppChanges: async appId => {
return await API.post({
url: `/api/dev/${appId}/revert`,
})
},
/**
* Updates an app's version of the client library.
* @param appId the app ID to update
*/
updateAppClientVersion: async appId => {
return await API.post({
url: `/api/applications/${appId}/client/update`,
})
},
/**
* Reverts an app's version of the client library to the previous version.
* @param appId the app ID to revert
*/
revertAppClientVersion: async appId => {
return await API.post({
url: `/api/applications/${appId}/client/revert`,
})
},
/**
* Gets a list of app deployments.
*/
@ -49,4 +69,57 @@ export const buildAppEndpoints = API => ({
url: "/api/deployments",
})
},
/**
* Creates an app.
* @param app the app to create
*/
createApp: async app => {
return await API.post({
url: "/api/applications",
body: app,
})
},
/**
* Imports an export of all apps.
* @param apps the FormData containing the apps to import
*/
importApps: async apps => {
return await API.post({
url: "/api/cloud/import",
body: apps,
json: false,
})
},
/**
* Unpublishes a published app.
* @param appId the production ID of the app to unpublish
*/
unpublishApp: async appId => {
return await API.delete({
url: `/api/applications/${appId}?unpublish=1`,
})
},
/**
* Deletes a dev app.
* @param appId the dev app ID to delete
*/
deleteApp: async appId => {
return await API.delete({
url: `/api/applications/${appId}`,
})
},
/**
* Releases the lock on a dev app.
* @param appId the dev app ID to unlock
*/
releaseAppLock: async appId => {
return await API.delete({
url: `/api/dev/${appId}/lock`,
})
},
})

View File

@ -1,6 +1,8 @@
export const buildAttachmentEndpoints = API => ({
/**
* Uploads an attachment to the server.
* @param data the attachment to upload
* @param tableId the table ID to upload to
*/
uploadAttachment: async ({ data, tableId }) => {
return await API.post({
@ -9,4 +11,16 @@ export const buildAttachmentEndpoints = API => ({
json: false,
})
},
/**
* Uploads an attachment to the server as a builder user from the builder.
* @param data the data to upload
*/
uploadBuilderAttachment: async data => {
return await API.post({
url: "/api/attachments/process",
body: data,
json: false,
})
},
})

View File

@ -31,6 +31,89 @@ export const buildAuthEndpoints = API => ({
* Fetches the currently logged in user object
*/
fetchSelf: async () => {
return await API.get({ url: "/api/self" })
return await API.get({
url: "/api/self",
})
},
/**
* Updates the current user metadata.
* @param metadata the metadata to save
*/
updateOwnMetadata: async metadata => {
return await API.post({
url: "/api/users/metadata/self",
body: metadata,
})
},
/**
* Creates an admin user.
* @param adminUser the admin user to create
*/
createAdminUser: async adminUser => {
return await API.post({
url: "/api/global/users/init",
body: adminUser,
})
},
/**
* Saves a global config.
* @param config the config to save
*/
saveConfig: async config => {
return await API.post({
url: "/api/global/configs",
body: config,
})
},
/**
* Gets a global config of a certain type.
* @param type the type to fetch
*/
getConfig: async type => {
return await API.get({
url: `/api/global/configs/${type}`,
})
},
/**
* Gets the OIDC config for a certain tenant.
* @param tenantId the tenant ID to get the config for
*/
getOIDCConfig: async tenantId => {
return await API.get({
url: `/api/global/configs/public/oidc?tenantId=${tenantId}`,
})
},
/**
* Gets the checklist for a specific tenant.
* @param tenantId the tenant ID to get the checklist for
*/
getChecklist: async tenantId => {
return await API.get({
url: `/api/global/configs/checklist?tenantId=${tenantId}`,
})
},
/**
* TODO: find out what this is
*/
checkImportComplete: async () => {
return await API.get({
url: "/api/cloud/import/complete",
})
},
/**
* Gets the current environment details.
*/
getEnvironment: async () => {
return await API.get({
url: "/api/system/environment",
})
},
})

View File

@ -30,7 +30,9 @@ export const createAPIClient = config => {
}
// Generates an error object from an API response
const makeErrorFromResponse = async response => {
const makeErrorFromResponse = async (response, method) => {
console.log("making error from", response)
// Try to read a message from the error
let message = response.statusText
try {
@ -47,6 +49,8 @@ export const createAPIClient = config => {
message,
status: response.status,
url: response.url,
method,
handled: true,
}
}
@ -56,6 +60,8 @@ export const createAPIClient = config => {
message,
status: 400,
url: "",
method: "",
handled: true,
}
}
@ -110,11 +116,7 @@ export const createAPIClient = config => {
return null
}
} else {
const error = await makeErrorFromResponse(response)
if (config?.onError) {
config.onError(error)
}
throw error
throw await makeErrorFromResponse(response, method)
}
}
@ -134,14 +136,21 @@ export const createAPIClient = config => {
return await cache[identifier]
}
// Constructs an API call function for a particular HTTP method.
// Constructs an API call function for a particular HTTP method
const requestApiCall = method => async params => {
let { url, cache = false, external = false } = params
if (!external) {
url = `/${url}`.replace("//", "/")
try {
let { url, cache = false, external = false } = params
if (!external) {
url = `/${url}`.replace("//", "/")
}
const enrichedParams = { ...params, method, url }
return await (cache ? makeCachedApiCall : makeApiCall)(enrichedParams)
} catch (error) {
if (config?.onError) {
config.onError(error)
}
throw error
}
const enrichedParams = { ...params, method, url }
return await (cache ? makeCachedApiCall : makeApiCall)(enrichedParams)
}
// Build the underlying core API methods