Add initial work on component SDK
This commit is contained in:
parent
7a00243e45
commit
f8732b3038
|
@ -1,28 +0,0 @@
|
||||||
import appStore from "../state/store"
|
|
||||||
|
|
||||||
export const USER_STATE_PATH = "_bbuser"
|
|
||||||
|
|
||||||
export const authenticate = api => async ({ username, password }) => {
|
|
||||||
if (!username) {
|
|
||||||
api.error("Authenticate: username not set")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!password) {
|
|
||||||
api.error("Authenticate: password not set")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const user = await api.post({
|
|
||||||
url: "/api/authenticate",
|
|
||||||
body: { username, password },
|
|
||||||
})
|
|
||||||
|
|
||||||
// set user even if error - so it is defined at least
|
|
||||||
appStore.update(s => {
|
|
||||||
s[USER_STATE_PATH] = user
|
|
||||||
return s
|
|
||||||
})
|
|
||||||
|
|
||||||
localStorage.setItem("budibase:user", JSON.stringify(user))
|
|
||||||
}
|
|
|
@ -1,119 +0,0 @@
|
||||||
import { authenticate } from "./authenticate"
|
|
||||||
import { getAppId } from "../render/getAppId"
|
|
||||||
|
|
||||||
export async function baseApiCall(method, url, body) {
|
|
||||||
return await fetch(url, {
|
|
||||||
method: method,
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
"x-budibase-app-id": getAppId(window.document.cookie),
|
|
||||||
},
|
|
||||||
body: body && JSON.stringify(body),
|
|
||||||
credentials: "same-origin",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const apiCall = method => async ({ url, body }) => {
|
|
||||||
const response = await baseApiCall(method, url, body)
|
|
||||||
|
|
||||||
switch (response.status) {
|
|
||||||
case 200:
|
|
||||||
return response.json()
|
|
||||||
case 404:
|
|
||||||
return error(`${url} Not found`)
|
|
||||||
case 400:
|
|
||||||
return error(`${url} Bad Request`)
|
|
||||||
case 403:
|
|
||||||
return error(`${url} Forbidden`)
|
|
||||||
default:
|
|
||||||
if (response.status >= 200 && response.status < 400) {
|
|
||||||
return response.json()
|
|
||||||
}
|
|
||||||
|
|
||||||
return error(`${url} - ${response.statusText}`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const post = apiCall("POST")
|
|
||||||
const get = apiCall("GET")
|
|
||||||
const patch = apiCall("PATCH")
|
|
||||||
const del = apiCall("DELETE")
|
|
||||||
|
|
||||||
const ERROR_MEMBER = "##error"
|
|
||||||
const error = message => {
|
|
||||||
// appStore.update(s => s["##error_message"], message)
|
|
||||||
return { [ERROR_MEMBER]: message }
|
|
||||||
}
|
|
||||||
|
|
||||||
const isSuccess = obj => !obj || !obj[ERROR_MEMBER]
|
|
||||||
|
|
||||||
const apiOpts = {
|
|
||||||
isSuccess,
|
|
||||||
error,
|
|
||||||
post,
|
|
||||||
get,
|
|
||||||
patch,
|
|
||||||
delete: del,
|
|
||||||
}
|
|
||||||
|
|
||||||
const saveRow = async (params, state) =>
|
|
||||||
await post({
|
|
||||||
url: `/api/${params.tableId}/rows`,
|
|
||||||
body: makeRowRequestBody(params, state),
|
|
||||||
})
|
|
||||||
|
|
||||||
const updateRow = async (params, state) => {
|
|
||||||
const row = makeRowRequestBody(params, state)
|
|
||||||
row._id = params._id
|
|
||||||
await patch({
|
|
||||||
url: `/api/${params.tableId}/rows/${params._id}`,
|
|
||||||
body: row,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const deleteRow = async params =>
|
|
||||||
await del({
|
|
||||||
url: `/api/${params.tableId}/rows/${params.rowId}/${params.revId}`,
|
|
||||||
})
|
|
||||||
|
|
||||||
const makeRowRequestBody = (parameters, state) => {
|
|
||||||
// start with the row thats currently in context
|
|
||||||
const body = { ...(state.data || {}) }
|
|
||||||
|
|
||||||
// dont send the table
|
|
||||||
if (body._table) delete body._table
|
|
||||||
|
|
||||||
// then override with supplied parameters
|
|
||||||
if (parameters.fields) {
|
|
||||||
for (let fieldName of Object.keys(parameters.fields)) {
|
|
||||||
const field = parameters.fields[fieldName]
|
|
||||||
|
|
||||||
// ensure fields sent are of the correct type
|
|
||||||
if (field.type === "boolean") {
|
|
||||||
if (field.value === "true") body[fieldName] = true
|
|
||||||
if (field.value === "false") body[fieldName] = false
|
|
||||||
} else if (field.type === "number") {
|
|
||||||
const val = parseFloat(field.value)
|
|
||||||
if (!isNaN(val)) {
|
|
||||||
body[fieldName] = val
|
|
||||||
}
|
|
||||||
} else if (field.type === "datetime") {
|
|
||||||
const date = new Date(field.value)
|
|
||||||
if (!isNaN(date.getTime())) {
|
|
||||||
body[fieldName] = date.toISOString()
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
body[fieldName] = field.value
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return body
|
|
||||||
}
|
|
||||||
|
|
||||||
export default {
|
|
||||||
authenticate: authenticate(apiOpts),
|
|
||||||
saveRow,
|
|
||||||
updateRow,
|
|
||||||
deleteRow,
|
|
||||||
}
|
|
|
@ -0,0 +1,9 @@
|
||||||
|
.DS_Store
|
||||||
|
/node_modules/
|
||||||
|
node_modules_win
|
||||||
|
package-lock.json
|
||||||
|
release/
|
||||||
|
dist/
|
||||||
|
routify
|
||||||
|
cypress/videos
|
||||||
|
cypress/screenshots
|
|
@ -0,0 +1,10 @@
|
||||||
|
{
|
||||||
|
"name": "component-sdk",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "SDK for developing Budibase components",
|
||||||
|
"main": "src/index.js",
|
||||||
|
"repository": "https://github.com/Budibase/budibase",
|
||||||
|
"author": "Andrew Kingston",
|
||||||
|
"license": "AGPLv3",
|
||||||
|
"private": false
|
||||||
|
}
|
|
@ -0,0 +1,54 @@
|
||||||
|
import { get } from "svelte/store"
|
||||||
|
import { getAppId } from "../utils"
|
||||||
|
import { configStore } from "../store"
|
||||||
|
|
||||||
|
const makeURL = path => {
|
||||||
|
const { proto, domain, port } = get(configStore).config
|
||||||
|
let url = `/${path}`.replace("//", "/")
|
||||||
|
return domain ? `${proto}://${domain}:${port}${url}` : url
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleError = error => {
|
||||||
|
store.actions.handleError(error)
|
||||||
|
return { error }
|
||||||
|
}
|
||||||
|
|
||||||
|
const apiCall = method => async ({ url, body }) => {
|
||||||
|
try {
|
||||||
|
const fullURL = makeURL(url)
|
||||||
|
const response = await fetch(fullURL, {
|
||||||
|
method: method,
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"x-budibase-app-id": getAppId(window.document.cookie),
|
||||||
|
},
|
||||||
|
body: body && JSON.stringify(body),
|
||||||
|
credentials: "same-origin",
|
||||||
|
})
|
||||||
|
switch (response.status) {
|
||||||
|
case 200:
|
||||||
|
return response.json()
|
||||||
|
case 404:
|
||||||
|
return handleError(`${url}: Not Found`)
|
||||||
|
case 400:
|
||||||
|
return handleError(`${url}: Bad Request`)
|
||||||
|
case 403:
|
||||||
|
return handleError(`${url}: Forbidden`)
|
||||||
|
default:
|
||||||
|
if (response.status >= 200 && response.status < 400) {
|
||||||
|
return response.json()
|
||||||
|
}
|
||||||
|
return handleError(`${url} - ${response.statusText}`)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
return handleError(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
post: apiCall("POST"),
|
||||||
|
get: apiCall("GET"),
|
||||||
|
patch: apiCall("PATCH"),
|
||||||
|
del: apiCall("DELETE"),
|
||||||
|
error: handleError,
|
||||||
|
}
|
|
@ -0,0 +1,21 @@
|
||||||
|
import api from "./api"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Performs a log in request.
|
||||||
|
*
|
||||||
|
* @param username
|
||||||
|
* @param password
|
||||||
|
* @returns {Promise<{error: *}|any|{error: *}>}
|
||||||
|
*/
|
||||||
|
export const logIn = async ({ username, password }) => {
|
||||||
|
if (!username) {
|
||||||
|
return api.error("Please enter your username")
|
||||||
|
}
|
||||||
|
if (!password) {
|
||||||
|
return api.error("Please enter your password")
|
||||||
|
}
|
||||||
|
return await api.post({
|
||||||
|
url: "/api/authenticate",
|
||||||
|
body: { username, password },
|
||||||
|
})
|
||||||
|
}
|
|
@ -0,0 +1,2 @@
|
||||||
|
export * from "./rows"
|
||||||
|
export * from "./auth"
|
|
@ -0,0 +1,80 @@
|
||||||
|
import api from "./api"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a row in a table.
|
||||||
|
*
|
||||||
|
* @param params
|
||||||
|
* @param state
|
||||||
|
* @returns {Promise<any|{error: *}>}
|
||||||
|
*/
|
||||||
|
export const saveRow = async (params, state) => {
|
||||||
|
return await api.post({
|
||||||
|
url: `/api/${params.tableId}/rows`,
|
||||||
|
body: makeRowRequestBody(params, state),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates a row in a table.
|
||||||
|
*
|
||||||
|
* @param params
|
||||||
|
* @param state
|
||||||
|
* @returns {Promise<any|{error: *}>}
|
||||||
|
*/
|
||||||
|
export const updateRow = async (params, state) => {
|
||||||
|
const row = makeRowRequestBody(params, state)
|
||||||
|
row._id = params._id
|
||||||
|
return await api.patch({
|
||||||
|
url: `/api/${params.tableId}/rows/${params._id}`,
|
||||||
|
body: row,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deletes a row from a table.
|
||||||
|
*
|
||||||
|
* @param tableId
|
||||||
|
* @param rowId
|
||||||
|
* @param revId
|
||||||
|
* @returns {Promise<any|{error: *}>}
|
||||||
|
*/
|
||||||
|
export const deleteRow = async ({ tableId, rowId, revId }) => {
|
||||||
|
return await api.del({
|
||||||
|
url: `/api/${tableId}/rows/${rowId}/${revId}`,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const makeRowRequestBody = (parameters, state) => {
|
||||||
|
// start with the row thats currently in context
|
||||||
|
const body = { ...(state.data || {}) }
|
||||||
|
|
||||||
|
// dont send the table
|
||||||
|
if (body._table) delete body._table
|
||||||
|
|
||||||
|
// then override with supplied parameters
|
||||||
|
if (parameters.fields) {
|
||||||
|
for (let fieldName of Object.keys(parameters.fields)) {
|
||||||
|
const field = parameters.fields[fieldName]
|
||||||
|
|
||||||
|
// ensure fields sent are of the correct type
|
||||||
|
if (field.type === "boolean") {
|
||||||
|
if (field.value === "true") body[fieldName] = true
|
||||||
|
if (field.value === "false") body[fieldName] = false
|
||||||
|
} else if (field.type === "number") {
|
||||||
|
const val = parseFloat(field.value)
|
||||||
|
if (!isNaN(val)) {
|
||||||
|
body[fieldName] = val
|
||||||
|
}
|
||||||
|
} else if (field.type === "datetime") {
|
||||||
|
const date = new Date(field.value)
|
||||||
|
if (!isNaN(date.getTime())) {
|
||||||
|
body[fieldName] = date.toISOString()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
body[fieldName] = field.value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return body
|
||||||
|
}
|
|
@ -0,0 +1,2 @@
|
||||||
|
export * from "./api"
|
||||||
|
export * from "./store"
|
|
@ -0,0 +1,41 @@
|
||||||
|
import { localStorageStore } from "../../../builder/src/builderStore/store/localStorage"
|
||||||
|
import * as api from "../api"
|
||||||
|
|
||||||
|
const initialState = {
|
||||||
|
user: null,
|
||||||
|
}
|
||||||
|
|
||||||
|
export const createAuthStore = () => {
|
||||||
|
const store = localStorageStore("bb-app-auth", initialState)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Logs a user in.
|
||||||
|
*
|
||||||
|
* @param username
|
||||||
|
* @param password
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
|
const logIn = async ({ username, password }) => {
|
||||||
|
const user = await api.logIn({ username, password })
|
||||||
|
if (!user.error) {
|
||||||
|
store.update(state => {
|
||||||
|
state.user = user
|
||||||
|
return state
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Logs a user out.
|
||||||
|
*/
|
||||||
|
const logOut = () => {
|
||||||
|
store.update(() => initialState)
|
||||||
|
}
|
||||||
|
|
||||||
|
store.actions = {
|
||||||
|
logIn,
|
||||||
|
logOut,
|
||||||
|
}
|
||||||
|
|
||||||
|
return store
|
||||||
|
}
|
|
@ -0,0 +1,51 @@
|
||||||
|
import { writable, get } from "svelte/store"
|
||||||
|
|
||||||
|
const initialState = {
|
||||||
|
proto: "http",
|
||||||
|
domain: null,
|
||||||
|
port: 80,
|
||||||
|
onError: null,
|
||||||
|
}
|
||||||
|
|
||||||
|
export const createConfigStore = () => {
|
||||||
|
const store = writable(initialState)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the SDK configuration.
|
||||||
|
*
|
||||||
|
* @param config
|
||||||
|
*/
|
||||||
|
const initialise = config => {
|
||||||
|
store.update(state => {
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
...config,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Rests the SDK configuration
|
||||||
|
*/
|
||||||
|
const reset = () => {
|
||||||
|
store.update(() => initialState)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Store handler for errors which triggers the user defined error handler.
|
||||||
|
*
|
||||||
|
* @param error
|
||||||
|
*/
|
||||||
|
const handleError = error => {
|
||||||
|
const handler = get(store).onError
|
||||||
|
handler && handler(error)
|
||||||
|
}
|
||||||
|
|
||||||
|
store.actions = {
|
||||||
|
initialise,
|
||||||
|
reset,
|
||||||
|
handleError,
|
||||||
|
}
|
||||||
|
|
||||||
|
return store
|
||||||
|
}
|
|
@ -0,0 +1,5 @@
|
||||||
|
import { createConfigStore } from "./config"
|
||||||
|
import { createAuthStore } from "./auth"
|
||||||
|
|
||||||
|
export const configStore = createConfigStore()
|
||||||
|
export const authStore = createAuthStore()
|
|
@ -0,0 +1,47 @@
|
||||||
|
const COOKIE_SEPARATOR = ";"
|
||||||
|
const APP_PREFIX = "app_"
|
||||||
|
const KEY_VALUE_SPLIT = "="
|
||||||
|
|
||||||
|
function confirmAppId(possibleAppId) {
|
||||||
|
return possibleAppId && possibleAppId.startsWith(APP_PREFIX)
|
||||||
|
? possibleAppId
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
function tryGetFromCookie({ cookies }) {
|
||||||
|
if (!cookies) {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
const cookie = cookies
|
||||||
|
.split(COOKIE_SEPARATOR)
|
||||||
|
.find(cookie => cookie.trim().startsWith("budibase:currentapp"))
|
||||||
|
let appId
|
||||||
|
if (cookie && cookie.split(KEY_VALUE_SPLIT).length === 2) {
|
||||||
|
appId = cookie.split("=")[1]
|
||||||
|
}
|
||||||
|
return confirmAppId(appId)
|
||||||
|
}
|
||||||
|
|
||||||
|
function tryGetFromPath() {
|
||||||
|
const appId = location.pathname.split("/")[1]
|
||||||
|
return confirmAppId(appId)
|
||||||
|
}
|
||||||
|
|
||||||
|
function tryGetFromSubdomain() {
|
||||||
|
const parts = window.location.host.split(".")
|
||||||
|
const appId = parts[1] ? parts[0] : undefined
|
||||||
|
return confirmAppId(appId)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getAppId = cookies => {
|
||||||
|
const functions = [tryGetFromSubdomain, tryGetFromPath, tryGetFromCookie]
|
||||||
|
// try getting the app Id in order
|
||||||
|
let appId
|
||||||
|
for (let func of functions) {
|
||||||
|
appId = func({ cookies })
|
||||||
|
if (appId) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return appId
|
||||||
|
}
|
|
@ -0,0 +1 @@
|
||||||
|
export { getAppId } from "./getAppId"
|
Loading…
Reference in New Issue