Add initial work on component SDK
This commit is contained in:
parent
d994177f3b
commit
499ad2e35d
|
@ -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