Add initial work on component SDK

This commit is contained in:
Andrew Kingston 2020-11-11 12:25:50 +00:00
parent d994177f3b
commit 499ad2e35d
17 changed files with 323 additions and 147 deletions

View File

@ -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))
}

View File

@ -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,
}

9
packages/component-sdk/.gitignore vendored Normal file
View File

@ -0,0 +1,9 @@
.DS_Store
/node_modules/
node_modules_win
package-lock.json
release/
dist/
routify
cypress/videos
cypress/screenshots

View File

@ -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
}

View File

@ -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,
}

View File

@ -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 },
})
}

View File

@ -0,0 +1,2 @@
export * from "./rows"
export * from "./auth"

View File

View File

@ -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
}

View File

View File

@ -0,0 +1,2 @@
export * from "./api"
export * from "./store"

View File

@ -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
}

View File

@ -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
}

View File

@ -0,0 +1,5 @@
import { createConfigStore } from "./config"
import { createAuthStore } from "./auth"
export const configStore = createConfigStore()
export const authStore = createAuthStore()

View File

@ -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
}

View File

@ -0,0 +1 @@
export { getAppId } from "./getAppId"