forked from tsai/budibase
17 changed files with 323 additions and 147 deletions
@ -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