From 499ad2e35d1807601552a5f44cddc3500f178fc7 Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Wed, 11 Nov 2020 12:25:50 +0000 Subject: [PATCH] Add initial work on component SDK --- packages/client/src/api/authenticate.js | 28 ----- packages/client/src/api/index.js | 119 ------------------- packages/component-sdk/.gitignore | 9 ++ packages/component-sdk/package.json | 10 ++ packages/component-sdk/src/api/api.js | 54 +++++++++ packages/component-sdk/src/api/auth.js | 21 ++++ packages/component-sdk/src/api/index.js | 2 + packages/component-sdk/src/api/routes.js | 0 packages/component-sdk/src/api/rows.js | 80 +++++++++++++ packages/component-sdk/src/api/screens.js | 0 packages/component-sdk/src/api/tables.js | 0 packages/component-sdk/src/index.js | 2 + packages/component-sdk/src/store/auth.js | 41 +++++++ packages/component-sdk/src/store/config.js | 51 ++++++++ packages/component-sdk/src/store/index.js | 5 + packages/component-sdk/src/utils/getAppId.js | 47 ++++++++ packages/component-sdk/src/utils/index.js | 1 + 17 files changed, 323 insertions(+), 147 deletions(-) delete mode 100644 packages/client/src/api/authenticate.js delete mode 100644 packages/client/src/api/index.js create mode 100644 packages/component-sdk/.gitignore create mode 100644 packages/component-sdk/package.json create mode 100644 packages/component-sdk/src/api/api.js create mode 100644 packages/component-sdk/src/api/auth.js create mode 100644 packages/component-sdk/src/api/index.js create mode 100644 packages/component-sdk/src/api/routes.js create mode 100644 packages/component-sdk/src/api/rows.js create mode 100644 packages/component-sdk/src/api/screens.js create mode 100644 packages/component-sdk/src/api/tables.js create mode 100644 packages/component-sdk/src/index.js create mode 100644 packages/component-sdk/src/store/auth.js create mode 100644 packages/component-sdk/src/store/config.js create mode 100644 packages/component-sdk/src/store/index.js create mode 100644 packages/component-sdk/src/utils/getAppId.js create mode 100644 packages/component-sdk/src/utils/index.js diff --git a/packages/client/src/api/authenticate.js b/packages/client/src/api/authenticate.js deleted file mode 100644 index 0b961a6fe6..0000000000 --- a/packages/client/src/api/authenticate.js +++ /dev/null @@ -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)) -} diff --git a/packages/client/src/api/index.js b/packages/client/src/api/index.js deleted file mode 100644 index 0b79081187..0000000000 --- a/packages/client/src/api/index.js +++ /dev/null @@ -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, -} diff --git a/packages/component-sdk/.gitignore b/packages/component-sdk/.gitignore new file mode 100644 index 0000000000..2e8ed59c47 --- /dev/null +++ b/packages/component-sdk/.gitignore @@ -0,0 +1,9 @@ +.DS_Store +/node_modules/ +node_modules_win +package-lock.json +release/ +dist/ +routify +cypress/videos +cypress/screenshots diff --git a/packages/component-sdk/package.json b/packages/component-sdk/package.json new file mode 100644 index 0000000000..e22cae171a --- /dev/null +++ b/packages/component-sdk/package.json @@ -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 +} diff --git a/packages/component-sdk/src/api/api.js b/packages/component-sdk/src/api/api.js new file mode 100644 index 0000000000..10dede1c68 --- /dev/null +++ b/packages/component-sdk/src/api/api.js @@ -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, +} diff --git a/packages/component-sdk/src/api/auth.js b/packages/component-sdk/src/api/auth.js new file mode 100644 index 0000000000..ba1cf51a75 --- /dev/null +++ b/packages/component-sdk/src/api/auth.js @@ -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 }, + }) +} diff --git a/packages/component-sdk/src/api/index.js b/packages/component-sdk/src/api/index.js new file mode 100644 index 0000000000..56ef9b06ad --- /dev/null +++ b/packages/component-sdk/src/api/index.js @@ -0,0 +1,2 @@ +export * from "./rows" +export * from "./auth" diff --git a/packages/component-sdk/src/api/routes.js b/packages/component-sdk/src/api/routes.js new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/component-sdk/src/api/rows.js b/packages/component-sdk/src/api/rows.js new file mode 100644 index 0000000000..239e33e23d --- /dev/null +++ b/packages/component-sdk/src/api/rows.js @@ -0,0 +1,80 @@ +import api from "./api" + +/** + * Creates a row in a table. + * + * @param params + * @param state + * @returns {Promise} + */ +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} + */ +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} + */ +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 +} diff --git a/packages/component-sdk/src/api/screens.js b/packages/component-sdk/src/api/screens.js new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/component-sdk/src/api/tables.js b/packages/component-sdk/src/api/tables.js new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/component-sdk/src/index.js b/packages/component-sdk/src/index.js new file mode 100644 index 0000000000..87b875af48 --- /dev/null +++ b/packages/component-sdk/src/index.js @@ -0,0 +1,2 @@ +export * from "./api" +export * from "./store" diff --git a/packages/component-sdk/src/store/auth.js b/packages/component-sdk/src/store/auth.js new file mode 100644 index 0000000000..d492f65ef8 --- /dev/null +++ b/packages/component-sdk/src/store/auth.js @@ -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} + */ + 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 +} diff --git a/packages/component-sdk/src/store/config.js b/packages/component-sdk/src/store/config.js new file mode 100644 index 0000000000..5de7e73238 --- /dev/null +++ b/packages/component-sdk/src/store/config.js @@ -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 +} diff --git a/packages/component-sdk/src/store/index.js b/packages/component-sdk/src/store/index.js new file mode 100644 index 0000000000..10cab9dfdc --- /dev/null +++ b/packages/component-sdk/src/store/index.js @@ -0,0 +1,5 @@ +import { createConfigStore } from "./config" +import { createAuthStore } from "./auth" + +export const configStore = createConfigStore() +export const authStore = createAuthStore() diff --git a/packages/component-sdk/src/utils/getAppId.js b/packages/component-sdk/src/utils/getAppId.js new file mode 100644 index 0000000000..62d960afe0 --- /dev/null +++ b/packages/component-sdk/src/utils/getAppId.js @@ -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 +} diff --git a/packages/component-sdk/src/utils/index.js b/packages/component-sdk/src/utils/index.js new file mode 100644 index 0000000000..1eb0ca2cfe --- /dev/null +++ b/packages/component-sdk/src/utils/index.js @@ -0,0 +1 @@ +export { getAppId } from "./getAppId"