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