Merge branch 'develop' of github.com:Budibase/budibase into feature/formula-filtering
This commit is contained in:
commit
8fbd101448
|
@ -201,9 +201,6 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
|
|||
<td align="center"><a href="https://github.com/seoulaja"><img src="https://avatars.githubusercontent.com/u/15101654?v=4?s=100" width="100px;" alt=""/><br /><sub><b>seoulaja</b></sub></a><br /><a href="#translation-seoulaja" title="Translation">🌍</a></td>
|
||||
<td align="center"><a href="https://github.com/mslourens"><img src="https://avatars.githubusercontent.com/u/1907152?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Maurits Lourens</b></sub></a><br /><a href="https://github.com/Budibase/budibase/commits?author=mslourens" title="Tests">⚠️</a> <a href="https://github.com/Budibase/budibase/commits?author=mslourens" title="Code">💻</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center"><a href="https://github.com/Rory-Powell"><img src="https://avatars.githubusercontent.com/u/8755148?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Rory Powell</b></sub></a><br /><a href="#infra-Rory-Powell" title="Infrastructure (Hosting, Build-Tools, etc)">🚇</a> <a href="https://github.com/Budibase/budibase/commits?author=Rory-Powell" title="Tests">⚠️</a> <a href="https://github.com/Budibase/budibase/commits?author=Rory-Powell" title="Code">💻</a></td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<!-- markdownlint-restore -->
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"version": "1.0.46-alpha.3",
|
||||
"version": "1.0.49-alpha.0",
|
||||
"npmClient": "yarn",
|
||||
"packages": [
|
||||
"packages/*"
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@budibase/backend-core",
|
||||
"version": "1.0.46-alpha.3",
|
||||
"version": "1.0.49-alpha.0",
|
||||
"description": "Budibase backend core libraries used in server and worker",
|
||||
"main": "src/index.js",
|
||||
"author": "Budibase",
|
||||
|
|
|
@ -12,6 +12,7 @@ const {
|
|||
tenancy,
|
||||
appTenancy,
|
||||
authError,
|
||||
csrf,
|
||||
} = require("./middleware")
|
||||
|
||||
// Strategies
|
||||
|
@ -42,4 +43,5 @@ module.exports = {
|
|||
buildAppTenancyMiddleware: appTenancy,
|
||||
auditLog,
|
||||
authError,
|
||||
buildCsrfMiddleware: csrf,
|
||||
}
|
||||
|
|
|
@ -7,8 +7,8 @@ exports.Cookies = {
|
|||
CurrentApp: "budibase:currentapp",
|
||||
Auth: "budibase:auth",
|
||||
Init: "budibase:init",
|
||||
DatasourceAuth: "budibase:datasourceauth",
|
||||
OIDC_CONFIG: "budibase:oidc:config",
|
||||
RETURN_URL: "budibase:returnurl",
|
||||
}
|
||||
|
||||
exports.Headers = {
|
||||
|
@ -18,6 +18,7 @@ exports.Headers = {
|
|||
TYPE: "x-budibase-type",
|
||||
TENANT_ID: "x-budibase-tenant-id",
|
||||
TOKEN: "x-budibase-token",
|
||||
CSRF_TOKEN: "x-csrf-token",
|
||||
}
|
||||
|
||||
exports.GlobalRoles = {
|
||||
|
|
|
@ -60,6 +60,7 @@ module.exports = (
|
|||
} else {
|
||||
user = await getUser(userId, session.tenantId)
|
||||
}
|
||||
user.csrfToken = session.csrfToken
|
||||
delete user.password
|
||||
authenticated = true
|
||||
} catch (err) {
|
||||
|
|
|
@ -0,0 +1,78 @@
|
|||
const { Headers } = require("../constants")
|
||||
const { buildMatcherRegex, matches } = require("./matchers")
|
||||
|
||||
/**
|
||||
* GET, HEAD and OPTIONS methods are considered safe operations
|
||||
*
|
||||
* POST, PUT, PATCH, and DELETE methods, being state changing verbs,
|
||||
* should have a CSRF token attached to the request
|
||||
*/
|
||||
const EXCLUDED_METHODS = ["GET", "HEAD", "OPTIONS"]
|
||||
|
||||
/**
|
||||
* There are only three content type values that can be used in cross domain requests.
|
||||
* If any other value is used, e.g. application/json, the browser will first make a OPTIONS
|
||||
* request which will be protected by CORS.
|
||||
*/
|
||||
const INCLUDED_CONTENT_TYPES = [
|
||||
"application/x-www-form-urlencoded",
|
||||
"multipart/form-data",
|
||||
"text/plain",
|
||||
]
|
||||
|
||||
/**
|
||||
* Validate the CSRF token generated aganst the user session.
|
||||
* Compare the token with the x-csrf-token header.
|
||||
*
|
||||
* If the token is not found within the request or the value provided
|
||||
* does not match the value within the user session, the request is rejected.
|
||||
*
|
||||
* CSRF protection provided using the 'Synchronizer Token Pattern'
|
||||
* https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.html#synchronizer-token-pattern
|
||||
*
|
||||
*/
|
||||
module.exports = (opts = { noCsrfPatterns: [] }) => {
|
||||
const noCsrfOptions = buildMatcherRegex(opts.noCsrfPatterns)
|
||||
return async (ctx, next) => {
|
||||
// don't apply for excluded paths
|
||||
const found = matches(ctx, noCsrfOptions)
|
||||
if (found) {
|
||||
return next()
|
||||
}
|
||||
|
||||
// don't apply for the excluded http methods
|
||||
if (EXCLUDED_METHODS.indexOf(ctx.method) !== -1) {
|
||||
return next()
|
||||
}
|
||||
|
||||
// don't apply when the content type isn't supported
|
||||
let contentType = ctx.get("content-type")
|
||||
? ctx.get("content-type").toLowerCase()
|
||||
: ""
|
||||
if (
|
||||
!INCLUDED_CONTENT_TYPES.filter(type => contentType.includes(type)).length
|
||||
) {
|
||||
return next()
|
||||
}
|
||||
|
||||
// don't apply csrf when the internal api key has been used
|
||||
if (ctx.internal) {
|
||||
return next()
|
||||
}
|
||||
|
||||
// apply csrf when there is a token in the session (new logins)
|
||||
// in future there should be a hard requirement that the token is present
|
||||
const userToken = ctx.user.csrfToken
|
||||
if (!userToken) {
|
||||
return next()
|
||||
}
|
||||
|
||||
// reject if no token in request or mismatch
|
||||
const requestToken = ctx.get(Headers.CSRF_TOKEN)
|
||||
if (!requestToken || requestToken !== userToken) {
|
||||
ctx.throw(403, "Invalid CSRF token")
|
||||
}
|
||||
|
||||
return next()
|
||||
}
|
||||
}
|
|
@ -7,6 +7,8 @@ const authenticated = require("./authenticated")
|
|||
const auditLog = require("./auditLog")
|
||||
const tenancy = require("./tenancy")
|
||||
const appTenancy = require("./appTenancy")
|
||||
const datasourceGoogle = require("./passport/datasource/google")
|
||||
const csrf = require("./csrf")
|
||||
|
||||
module.exports = {
|
||||
google,
|
||||
|
@ -18,4 +20,8 @@ module.exports = {
|
|||
tenancy,
|
||||
appTenancy,
|
||||
authError,
|
||||
datasource: {
|
||||
google: datasourceGoogle,
|
||||
},
|
||||
csrf,
|
||||
}
|
||||
|
|
|
@ -0,0 +1,76 @@
|
|||
const { getScopedConfig } = require("../../../db/utils")
|
||||
const { getGlobalDB } = require("../../../tenancy")
|
||||
const google = require("../google")
|
||||
const { Configs, Cookies } = require("../../../constants")
|
||||
const { clearCookie, getCookie } = require("../../../utils")
|
||||
const { getDB } = require("../../../db")
|
||||
|
||||
async function preAuth(passport, ctx, next) {
|
||||
const db = getGlobalDB()
|
||||
// get the relevant config
|
||||
const config = await getScopedConfig(db, {
|
||||
type: Configs.GOOGLE,
|
||||
workspace: ctx.query.workspace,
|
||||
})
|
||||
const publicConfig = await getScopedConfig(db, {
|
||||
type: Configs.SETTINGS,
|
||||
})
|
||||
let callbackUrl = `${publicConfig.platformUrl}/api/global/auth/datasource/google/callback`
|
||||
const strategy = await google.strategyFactory(config, callbackUrl)
|
||||
|
||||
if (!ctx.query.appId || !ctx.query.datasourceId) {
|
||||
ctx.throw(400, "appId and datasourceId query params not present.")
|
||||
}
|
||||
|
||||
return passport.authenticate(strategy, {
|
||||
scope: ["profile", "email", "https://www.googleapis.com/auth/spreadsheets"],
|
||||
accessType: "offline",
|
||||
prompt: "consent",
|
||||
})(ctx, next)
|
||||
}
|
||||
|
||||
async function postAuth(passport, ctx, next) {
|
||||
const db = getGlobalDB()
|
||||
|
||||
const config = await getScopedConfig(db, {
|
||||
type: Configs.GOOGLE,
|
||||
workspace: ctx.query.workspace,
|
||||
})
|
||||
|
||||
const publicConfig = await getScopedConfig(db, {
|
||||
type: Configs.SETTINGS,
|
||||
})
|
||||
|
||||
let callbackUrl = `${publicConfig.platformUrl}/api/global/auth/datasource/google/callback`
|
||||
const strategy = await google.strategyFactory(
|
||||
config,
|
||||
callbackUrl,
|
||||
(accessToken, refreshToken, profile, done) => {
|
||||
clearCookie(ctx, Cookies.DatasourceAuth)
|
||||
done(null, { accessToken, refreshToken })
|
||||
}
|
||||
)
|
||||
|
||||
const authStateCookie = getCookie(ctx, Cookies.DatasourceAuth)
|
||||
|
||||
return passport.authenticate(
|
||||
strategy,
|
||||
{ successRedirect: "/", failureRedirect: "/error" },
|
||||
async (err, tokens) => {
|
||||
// update the DB for the datasource with all the user info
|
||||
const db = getDB(authStateCookie.appId)
|
||||
const datasource = await db.get(authStateCookie.datasourceId)
|
||||
if (!datasource.config) {
|
||||
datasource.config = {}
|
||||
}
|
||||
datasource.config.auth = { type: "google", ...tokens }
|
||||
await db.put(datasource)
|
||||
ctx.redirect(
|
||||
`/builder/app/${authStateCookie.appId}/data/datasource/${authStateCookie.datasourceId}`
|
||||
)
|
||||
}
|
||||
)(ctx, next)
|
||||
}
|
||||
|
||||
exports.preAuth = preAuth
|
||||
exports.postAuth = postAuth
|
|
@ -1,4 +1,5 @@
|
|||
const redis = require("../redis/authRedis")
|
||||
const { v4: uuidv4 } = require("uuid")
|
||||
|
||||
// a week in seconds
|
||||
const EXPIRY_SECONDS = 86400 * 7
|
||||
|
@ -16,6 +17,9 @@ function makeSessionID(userId, sessionId) {
|
|||
exports.createASession = async (userId, session) => {
|
||||
const client = await redis.getSessionClient()
|
||||
const sessionId = session.sessionId
|
||||
if (!session.csrfToken) {
|
||||
session.csrfToken = uuidv4()
|
||||
}
|
||||
session = {
|
||||
createdAt: new Date().toISOString(),
|
||||
lastAccessedAt: new Date().toISOString(),
|
||||
|
|
|
@ -96,12 +96,7 @@ exports.getCookie = (ctx, name) => {
|
|||
* @param {string|object} value The value of cookie which will be set.
|
||||
* @param {object} opts options like whether to sign.
|
||||
*/
|
||||
exports.setCookie = (
|
||||
ctx,
|
||||
value,
|
||||
name = "builder",
|
||||
opts = { sign: true, requestDomain: false }
|
||||
) => {
|
||||
exports.setCookie = (ctx, value, name = "builder", opts = { sign: true }) => {
|
||||
if (value && opts && opts.sign) {
|
||||
value = jwt.sign(value, options.secretOrKey)
|
||||
}
|
||||
|
@ -113,7 +108,7 @@ exports.setCookie = (
|
|||
overwrite: true,
|
||||
}
|
||||
|
||||
if (environment.COOKIE_DOMAIN && !opts.requestDomain) {
|
||||
if (environment.COOKIE_DOMAIN) {
|
||||
config.domain = environment.COOKIE_DOMAIN
|
||||
}
|
||||
|
||||
|
|
|
@ -3410,9 +3410,9 @@ node-fetch@2.6.0:
|
|||
integrity sha512-8dG4H5ujfvFiqDmVu9fQ5bOHUC15JMjMY/Zumv26oOvvVJjM67KF8koCWIabKQ1GJIa9r2mMZscBq/TbdOcmNA==
|
||||
|
||||
node-fetch@^2.6.1:
|
||||
version "2.6.6"
|
||||
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.6.tgz#1751a7c01834e8e1697758732e9efb6eeadfaf89"
|
||||
integrity sha512-Z8/6vRlTUChSdIgMa51jxQ4lrw/Jy5SOW10ObaA47/RElsAN2c5Pn8bTgFGWn/ibwzXTE8qwr1Yzx28vsecXEA==
|
||||
version "2.6.7"
|
||||
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.7.tgz#24de9fba827e3b4ae44dc8b20256a379160052ad"
|
||||
integrity sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==
|
||||
dependencies:
|
||||
whatwg-url "^5.0.0"
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "@budibase/bbui",
|
||||
"description": "A UI solution used in the different Budibase projects.",
|
||||
"version": "1.0.46-alpha.3",
|
||||
"version": "1.0.49-alpha.0",
|
||||
"license": "MPL-2.0",
|
||||
"svelte": "src/index.js",
|
||||
"module": "dist/bbui.es.js",
|
||||
|
|
|
@ -147,7 +147,9 @@
|
|||
<img alt="preview" src={selectedUrl} />
|
||||
{:else}
|
||||
<div class="placeholder">
|
||||
<div class="extension">{selectedImage.extension}</div>
|
||||
<div class="extension">
|
||||
{selectedImage.name || "Unknown file"}
|
||||
</div>
|
||||
<div>Preview not supported</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
@ -359,18 +361,21 @@
|
|||
white-space: nowrap;
|
||||
width: 0;
|
||||
margin-right: 10px;
|
||||
user-select: all;
|
||||
}
|
||||
.placeholder {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
}
|
||||
.extension {
|
||||
color: var(--spectrum-global-color-gray-600);
|
||||
text-transform: uppercase;
|
||||
font-weight: 600;
|
||||
margin-bottom: 5px;
|
||||
user-select: all;
|
||||
}
|
||||
|
||||
.nav {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@budibase/builder",
|
||||
"version": "1.0.46-alpha.3",
|
||||
"version": "1.0.49-alpha.0",
|
||||
"license": "GPL-3.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
|
@ -65,10 +65,10 @@
|
|||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"@budibase/bbui": "^1.0.46-alpha.3",
|
||||
"@budibase/client": "^1.0.46-alpha.3",
|
||||
"@budibase/bbui": "^1.0.49-alpha.0",
|
||||
"@budibase/client": "^1.0.49-alpha.0",
|
||||
"@budibase/colorpicker": "1.1.2",
|
||||
"@budibase/string-templates": "^1.0.46-alpha.3",
|
||||
"@budibase/string-templates": "^1.0.49-alpha.0",
|
||||
"@sentry/browser": "5.19.1",
|
||||
"@spectrum-css/page": "^3.0.1",
|
||||
"@spectrum-css/vars": "^3.0.1",
|
||||
|
|
|
@ -1,12 +1,20 @@
|
|||
import { store } from "./index"
|
||||
import { get as svelteGet } from "svelte/store"
|
||||
import { removeCookie, Cookies } from "./cookies"
|
||||
import { auth } from "stores/portal"
|
||||
|
||||
const apiCall =
|
||||
method =>
|
||||
async (url, body, headers = { "Content-Type": "application/json" }) => {
|
||||
headers["x-budibase-app-id"] = svelteGet(store).appId
|
||||
headers["x-budibase-api-version"] = "1"
|
||||
|
||||
// add csrf token if authenticated
|
||||
const user = svelteGet(auth).user
|
||||
if (user && user.csrfToken) {
|
||||
headers["x-csrf-token"] = user.csrfToken
|
||||
}
|
||||
|
||||
const json = headers["Content-Type"] === "application/json"
|
||||
const resp = await fetch(url, {
|
||||
method: method,
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
import { getFrontendStore } from "./store/frontend"
|
||||
import { getAutomationStore } from "./store/automation"
|
||||
import { getHostingStore } from "./store/hosting"
|
||||
import { getThemeStore } from "./store/theme"
|
||||
import { derived, writable } from "svelte/store"
|
||||
import { FrontendTypes, LAYOUT_NAMES } from "../constants"
|
||||
|
@ -9,7 +8,6 @@ import { findComponent } from "./componentUtils"
|
|||
export const store = getFrontendStore()
|
||||
export const automationStore = getAutomationStore()
|
||||
export const themeStore = getThemeStore()
|
||||
export const hostingStore = getHostingStore()
|
||||
|
||||
export const currentAsset = derived(store, $store => {
|
||||
const type = $store.currentFrontEndType
|
||||
|
|
|
@ -2,7 +2,6 @@ import { get, writable } from "svelte/store"
|
|||
import { cloneDeep } from "lodash/fp"
|
||||
import {
|
||||
allScreens,
|
||||
hostingStore,
|
||||
currentAsset,
|
||||
mainLayout,
|
||||
selectedComponent,
|
||||
|
@ -100,7 +99,6 @@ export const getFrontendStore = () => {
|
|||
version: application.version,
|
||||
revertableVersion: application.revertableVersion,
|
||||
}))
|
||||
await hostingStore.actions.fetch()
|
||||
|
||||
// Initialise backend stores
|
||||
const [_integrations] = await Promise.all([
|
||||
|
|
|
@ -1,34 +0,0 @@
|
|||
import { writable } from "svelte/store"
|
||||
import api, { get } from "../api"
|
||||
|
||||
const INITIAL_HOSTING_UI_STATE = {
|
||||
appUrl: "",
|
||||
deployedApps: {},
|
||||
deployedAppNames: [],
|
||||
deployedAppUrls: [],
|
||||
}
|
||||
|
||||
export const getHostingStore = () => {
|
||||
const store = writable({ ...INITIAL_HOSTING_UI_STATE })
|
||||
store.actions = {
|
||||
fetch: async () => {
|
||||
const response = await api.get("/api/hosting/urls")
|
||||
const urls = await response.json()
|
||||
store.update(state => {
|
||||
state.appUrl = urls.app
|
||||
return state
|
||||
})
|
||||
},
|
||||
fetchDeployedApps: async () => {
|
||||
let deployments = await (await get("/api/hosting/apps")).json()
|
||||
store.update(state => {
|
||||
state.deployedApps = deployments
|
||||
state.deployedAppNames = Object.values(deployments).map(app => app.name)
|
||||
state.deployedAppUrls = Object.values(deployments).map(app => app.url)
|
||||
return state
|
||||
})
|
||||
return deployments
|
||||
},
|
||||
}
|
||||
return store
|
||||
}
|
|
@ -22,8 +22,10 @@
|
|||
RelationshipTypes,
|
||||
ALLOWABLE_STRING_OPTIONS,
|
||||
ALLOWABLE_NUMBER_OPTIONS,
|
||||
ALLOWABLE_JSON_OPTIONS,
|
||||
ALLOWABLE_STRING_TYPES,
|
||||
ALLOWABLE_NUMBER_TYPES,
|
||||
ALLOWABLE_JSON_TYPES,
|
||||
SWITCHABLE_TYPES,
|
||||
} from "constants/backend"
|
||||
import { getAutoColumnInformation, buildAutoColumn } from "builderStore/utils"
|
||||
|
@ -245,6 +247,11 @@
|
|||
ALLOWABLE_NUMBER_TYPES.indexOf(field.type) !== -1
|
||||
) {
|
||||
return ALLOWABLE_NUMBER_OPTIONS
|
||||
} else if (
|
||||
originalName &&
|
||||
ALLOWABLE_JSON_TYPES.indexOf(field.type) !== -1
|
||||
) {
|
||||
return ALLOWABLE_JSON_OPTIONS
|
||||
} else if (!external) {
|
||||
return [
|
||||
...Object.values(fieldDefinitions),
|
||||
|
|
|
@ -188,7 +188,7 @@
|
|||
{:else}
|
||||
<Body size="S"><i>No tables found.</i></Body>
|
||||
{/if}
|
||||
{#if plusTables?.length !== 0}
|
||||
{#if plusTables?.length !== 0 && integration.relationships}
|
||||
<Divider size="S" />
|
||||
<div class="query-header">
|
||||
<Heading size="S">Relationships</Heading>
|
||||
|
|
|
@ -0,0 +1,47 @@
|
|||
<script>
|
||||
import { ActionButton } from "@budibase/bbui"
|
||||
import GoogleLogo from "assets/google-logo.png"
|
||||
import { store } from "builderStore"
|
||||
import { auth } from "stores/portal"
|
||||
|
||||
export let preAuthStep
|
||||
export let datasource
|
||||
|
||||
$: tenantId = $auth.tenantId
|
||||
</script>
|
||||
|
||||
<ActionButton
|
||||
on:click={async () => {
|
||||
let ds = datasource
|
||||
if (!ds) {
|
||||
ds = await preAuthStep()
|
||||
}
|
||||
window.open(
|
||||
`/api/global/auth/${tenantId}/datasource/google?datasourceId=${datasource._id}&appId=${$store.appId}`,
|
||||
"_blank"
|
||||
)
|
||||
}}
|
||||
>
|
||||
<div class="inner">
|
||||
<img src={GoogleLogo} alt="google icon" />
|
||||
<p>Sign in with Google</p>
|
||||
</div>
|
||||
</ActionButton>
|
||||
|
||||
<style>
|
||||
.inner {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding-top: var(--spacing-xs);
|
||||
padding-bottom: var(--spacing-xs);
|
||||
}
|
||||
.inner img {
|
||||
width: 18px;
|
||||
margin: 3px 10px 3px 3px;
|
||||
}
|
||||
.inner p {
|
||||
margin: 0;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,184 @@
|
|||
<script>
|
||||
export let width = "100"
|
||||
export let height = "100"
|
||||
</script>
|
||||
|
||||
<svg
|
||||
{width}
|
||||
{height}
|
||||
version="1.0"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 50 80"
|
||||
preserveAspectRatio="xMidYMid meet"
|
||||
>
|
||||
<defs>
|
||||
<path
|
||||
d="M29.5833333,0 L4.4375,0 C1.996875,0 0,1.996875 0,4.4375 L0,60.6458333 C0,63.0864583 1.996875,65.0833333 4.4375,65.0833333 L42.8958333,65.0833333 C45.3364583,65.0833333 47.3333333,63.0864583 47.3333333,60.6458333 L47.3333333,17.75 L29.5833333,0 Z"
|
||||
id="path-1"
|
||||
/>
|
||||
<path
|
||||
d="M29.5833333,0 L4.4375,0 C1.996875,0 0,1.996875 0,4.4375 L0,60.6458333 C0,63.0864583 1.996875,65.0833333 4.4375,65.0833333 L42.8958333,65.0833333 C45.3364583,65.0833333 47.3333333,63.0864583 47.3333333,60.6458333 L47.3333333,17.75 L29.5833333,0 Z"
|
||||
id="path-3"
|
||||
/>
|
||||
<path
|
||||
d="M29.5833333,0 L4.4375,0 C1.996875,0 0,1.996875 0,4.4375 L0,60.6458333 C0,63.0864583 1.996875,65.0833333 4.4375,65.0833333 L42.8958333,65.0833333 C45.3364583,65.0833333 47.3333333,63.0864583 47.3333333,60.6458333 L47.3333333,17.75 L29.5833333,0 Z"
|
||||
id="path-5"
|
||||
/>
|
||||
<linearGradient
|
||||
x1="50.0053945%"
|
||||
y1="8.58610612%"
|
||||
x2="50.0053945%"
|
||||
y2="100.013939%"
|
||||
id="linearGradient-7"
|
||||
>
|
||||
<stop stop-color="#263238" stop-opacity="0.2" offset="0%" />
|
||||
<stop stop-color="#263238" stop-opacity="0.02" offset="100%" />
|
||||
</linearGradient>
|
||||
<path
|
||||
d="M29.5833333,0 L4.4375,0 C1.996875,0 0,1.996875 0,4.4375 L0,60.6458333 C0,63.0864583 1.996875,65.0833333 4.4375,65.0833333 L42.8958333,65.0833333 C45.3364583,65.0833333 47.3333333,63.0864583 47.3333333,60.6458333 L47.3333333,17.75 L29.5833333,0 Z"
|
||||
id="path-8"
|
||||
/>
|
||||
<path
|
||||
d="M29.5833333,0 L4.4375,0 C1.996875,0 0,1.996875 0,4.4375 L0,60.6458333 C0,63.0864583 1.996875,65.0833333 4.4375,65.0833333 L42.8958333,65.0833333 C45.3364583,65.0833333 47.3333333,63.0864583 47.3333333,60.6458333 L47.3333333,17.75 L29.5833333,0 Z"
|
||||
id="path-10"
|
||||
/>
|
||||
<path
|
||||
d="M29.5833333,0 L4.4375,0 C1.996875,0 0,1.996875 0,4.4375 L0,60.6458333 C0,63.0864583 1.996875,65.0833333 4.4375,65.0833333 L42.8958333,65.0833333 C45.3364583,65.0833333 47.3333333,63.0864583 47.3333333,60.6458333 L47.3333333,17.75 L29.5833333,0 Z"
|
||||
id="path-12"
|
||||
/>
|
||||
<path
|
||||
d="M29.5833333,0 L4.4375,0 C1.996875,0 0,1.996875 0,4.4375 L0,60.6458333 C0,63.0864583 1.996875,65.0833333 4.4375,65.0833333 L42.8958333,65.0833333 C45.3364583,65.0833333 47.3333333,63.0864583 47.3333333,60.6458333 L47.3333333,17.75 L29.5833333,0 Z"
|
||||
id="path-14"
|
||||
/>
|
||||
<radialGradient
|
||||
cx="3.16804688%"
|
||||
cy="2.71744318%"
|
||||
fx="3.16804688%"
|
||||
fy="2.71744318%"
|
||||
r="161.248516%"
|
||||
gradientTransform="translate(0.031680,0.027174),scale(1.000000,0.727273),translate(-0.031680,-0.027174)"
|
||||
id="radialGradient-16"
|
||||
>
|
||||
<stop stop-color="#FFFFFF" stop-opacity="0.1" offset="0%" />
|
||||
<stop stop-color="#FFFFFF" stop-opacity="0" offset="100%" />
|
||||
</radialGradient>
|
||||
</defs>
|
||||
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<g
|
||||
id="Consumer-Apps-Sheets-Large-VD-R8-"
|
||||
transform="translate(-451.000000, -451.000000)"
|
||||
>
|
||||
<g id="Hero" transform="translate(0.000000, 63.000000)">
|
||||
<g id="Personal" transform="translate(277.000000, 299.000000)">
|
||||
<g id="Sheets-icon" transform="translate(174.833333, 89.958333)">
|
||||
<g id="Group">
|
||||
<g id="Clipped">
|
||||
<mask id="mask-2" fill="white">
|
||||
<use xlink:href="#path-1" />
|
||||
</mask>
|
||||
<g id="SVGID_1_" />
|
||||
<path
|
||||
d="M29.5833333,0 L4.4375,0 C1.996875,0 0,1.996875 0,4.4375 L0,60.6458333 C0,63.0864583 1.996875,65.0833333 4.4375,65.0833333 L42.8958333,65.0833333 C45.3364583,65.0833333 47.3333333,63.0864583 47.3333333,60.6458333 L47.3333333,17.75 L36.9791667,10.3541667 L29.5833333,0 Z"
|
||||
id="Path"
|
||||
fill="#0F9D58"
|
||||
fill-rule="nonzero"
|
||||
mask="url(#mask-2)"
|
||||
/>
|
||||
</g>
|
||||
<g id="Clipped">
|
||||
<mask id="mask-4" fill="white">
|
||||
<use xlink:href="#path-3" />
|
||||
</mask>
|
||||
<g id="SVGID_1_" />
|
||||
<path
|
||||
d="M11.8333333,31.8020833 L11.8333333,53.25 L35.5,53.25 L35.5,31.8020833 L11.8333333,31.8020833 Z M22.1875,50.2916667 L14.7916667,50.2916667 L14.7916667,46.59375 L22.1875,46.59375 L22.1875,50.2916667 Z M22.1875,44.375 L14.7916667,44.375 L14.7916667,40.6770833 L22.1875,40.6770833 L22.1875,44.375 Z M22.1875,38.4583333 L14.7916667,38.4583333 L14.7916667,34.7604167 L22.1875,34.7604167 L22.1875,38.4583333 Z M32.5416667,50.2916667 L25.1458333,50.2916667 L25.1458333,46.59375 L32.5416667,46.59375 L32.5416667,50.2916667 Z M32.5416667,44.375 L25.1458333,44.375 L25.1458333,40.6770833 L32.5416667,40.6770833 L32.5416667,44.375 Z M32.5416667,38.4583333 L25.1458333,38.4583333 L25.1458333,34.7604167 L32.5416667,34.7604167 L32.5416667,38.4583333 Z"
|
||||
id="Shape"
|
||||
fill="#F1F1F1"
|
||||
fill-rule="nonzero"
|
||||
mask="url(#mask-4)"
|
||||
/>
|
||||
</g>
|
||||
<g id="Clipped">
|
||||
<mask id="mask-6" fill="white">
|
||||
<use xlink:href="#path-5" />
|
||||
</mask>
|
||||
<g id="SVGID_1_" />
|
||||
<polygon
|
||||
id="Path"
|
||||
fill="url(#linearGradient-7)"
|
||||
fill-rule="nonzero"
|
||||
mask="url(#mask-6)"
|
||||
points="30.8813021 16.4520313 47.3333333 32.9003646 47.3333333 17.75"
|
||||
/>
|
||||
</g>
|
||||
<g id="Clipped">
|
||||
<mask id="mask-9" fill="white">
|
||||
<use xlink:href="#path-8" />
|
||||
</mask>
|
||||
<g id="SVGID_1_" />
|
||||
<g id="Group" mask="url(#mask-9)">
|
||||
<g transform="translate(26.625000, -2.958333)">
|
||||
<path
|
||||
d="M2.95833333,2.95833333 L2.95833333,16.2708333 C2.95833333,18.7225521 4.94411458,20.7083333 7.39583333,20.7083333 L20.7083333,20.7083333 L2.95833333,2.95833333 Z"
|
||||
id="Path"
|
||||
fill="#87CEAC"
|
||||
fill-rule="nonzero"
|
||||
/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g id="Clipped">
|
||||
<mask id="mask-11" fill="white">
|
||||
<use xlink:href="#path-10" />
|
||||
</mask>
|
||||
<g id="SVGID_1_" />
|
||||
<path
|
||||
d="M4.4375,0 C1.996875,0 0,1.996875 0,4.4375 L0,4.80729167 C0,2.36666667 1.996875,0.369791667 4.4375,0.369791667 L29.5833333,0.369791667 L29.5833333,0 L4.4375,0 Z"
|
||||
id="Path"
|
||||
fill-opacity="0.2"
|
||||
fill="#FFFFFF"
|
||||
fill-rule="nonzero"
|
||||
mask="url(#mask-11)"
|
||||
/>
|
||||
</g>
|
||||
<g id="Clipped">
|
||||
<mask id="mask-13" fill="white">
|
||||
<use xlink:href="#path-12" />
|
||||
</mask>
|
||||
<g id="SVGID_1_" />
|
||||
<path
|
||||
d="M42.8958333,64.7135417 L4.4375,64.7135417 C1.996875,64.7135417 0,62.7166667 0,60.2760417 L0,60.6458333 C0,63.0864583 1.996875,65.0833333 4.4375,65.0833333 L42.8958333,65.0833333 C45.3364583,65.0833333 47.3333333,63.0864583 47.3333333,60.6458333 L47.3333333,60.2760417 C47.3333333,62.7166667 45.3364583,64.7135417 42.8958333,64.7135417 Z"
|
||||
id="Path"
|
||||
fill-opacity="0.2"
|
||||
fill="#263238"
|
||||
fill-rule="nonzero"
|
||||
mask="url(#mask-13)"
|
||||
/>
|
||||
</g>
|
||||
<g id="Clipped">
|
||||
<mask id="mask-15" fill="white">
|
||||
<use xlink:href="#path-14" />
|
||||
</mask>
|
||||
<g id="SVGID_1_" />
|
||||
<path
|
||||
d="M34.0208333,17.75 C31.5691146,17.75 29.5833333,15.7642188 29.5833333,13.3125 L29.5833333,13.6822917 C29.5833333,16.1340104 31.5691146,18.1197917 34.0208333,18.1197917 L47.3333333,18.1197917 L47.3333333,17.75 L34.0208333,17.75 Z"
|
||||
id="Path"
|
||||
fill-opacity="0.1"
|
||||
fill="#263238"
|
||||
fill-rule="nonzero"
|
||||
mask="url(#mask-15)"
|
||||
/>
|
||||
</g>
|
||||
</g>
|
||||
<path
|
||||
d="M29.5833333,0 L4.4375,0 C1.996875,0 0,1.996875 0,4.4375 L0,60.6458333 C0,63.0864583 1.996875,65.0833333 4.4375,65.0833333 L42.8958333,65.0833333 C45.3364583,65.0833333 47.3333333,63.0864583 47.3333333,60.6458333 L47.3333333,17.75 L29.5833333,0 Z"
|
||||
id="Path"
|
||||
fill="url(#radialGradient-16)"
|
||||
fill-rule="nonzero"
|
||||
/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
|
@ -11,6 +11,7 @@ import ArangoDB from "./ArangoDB.svelte"
|
|||
import Rest from "./Rest.svelte"
|
||||
import Budibase from "./Budibase.svelte"
|
||||
import Oracle from "./Oracle.svelte"
|
||||
import GoogleSheets from "./GoogleSheets.svelte"
|
||||
|
||||
export default {
|
||||
BUDIBASE: Budibase,
|
||||
|
@ -26,4 +27,5 @@ export default {
|
|||
ARANGODB: ArangoDB,
|
||||
REST: Rest,
|
||||
ORACLE: Oracle,
|
||||
GOOGLE_SHEETS: GoogleSheets,
|
||||
}
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
import { IntegrationNames, IntegrationTypes } from "constants/backend"
|
||||
import CreateTableModal from "components/backend/TableNavigator/modals/CreateTableModal.svelte"
|
||||
import DatasourceConfigModal from "components/backend/DatasourceNavigator/modals/DatasourceConfigModal.svelte"
|
||||
import GoogleDatasourceConfigModal from "components/backend/DatasourceNavigator/modals/GoogleDatasourceConfigModal.svelte"
|
||||
import { createRestDatasource } from "builderStore/datasource"
|
||||
import { goto } from "@roxi/routify"
|
||||
import ImportRestQueriesModal from "./ImportRestQueriesModal.svelte"
|
||||
|
@ -38,6 +39,7 @@
|
|||
plus: selected.plus,
|
||||
config,
|
||||
schema: selected.datasource,
|
||||
auth: selected.auth,
|
||||
}
|
||||
checkShowImport()
|
||||
}
|
||||
|
@ -79,7 +81,11 @@
|
|||
</Modal>
|
||||
|
||||
<Modal bind:this={externalDatasourceModal}>
|
||||
<DatasourceConfigModal {integration} {modal} />
|
||||
{#if integration?.auth?.type === "google"}
|
||||
<GoogleDatasourceConfigModal {integration} {modal} />
|
||||
{:else}
|
||||
<DatasourceConfigModal {integration} {modal} />
|
||||
{/if}
|
||||
</Modal>
|
||||
|
||||
<Modal bind:this={importModal}>
|
||||
|
|
|
@ -51,13 +51,9 @@
|
|||
>Connect your database to Budibase using the config below.
|
||||
</Body>
|
||||
</Layout>
|
||||
|
||||
<IntegrationConfigForm
|
||||
schema={datasource.schema}
|
||||
bind:datasource
|
||||
creating={true}
|
||||
/>
|
||||
</ModalContent>
|
||||
|
||||
<style>
|
||||
</style>
|
||||
|
|
|
@ -0,0 +1,29 @@
|
|||
<script>
|
||||
import { ModalContent, Body, Layout } from "@budibase/bbui"
|
||||
import { IntegrationNames } from "constants/backend"
|
||||
import cloneDeep from "lodash/cloneDeepWith"
|
||||
import GoogleButton from "../_components/GoogleButton.svelte"
|
||||
import { saveDatasource as save } from "builderStore/datasource"
|
||||
|
||||
export let integration
|
||||
export let modal
|
||||
|
||||
// kill the reference so the input isn't saved
|
||||
let datasource = cloneDeep(integration)
|
||||
</script>
|
||||
|
||||
<ModalContent
|
||||
title={`Connect to ${IntegrationNames[datasource.type]}`}
|
||||
onCancel={() => modal.show()}
|
||||
cancelText="Back"
|
||||
size="L"
|
||||
>
|
||||
<Layout noPadding>
|
||||
<Body size="XS"
|
||||
>Authenticate with your google account to use the {IntegrationNames[
|
||||
datasource.type
|
||||
]} integration.</Body
|
||||
>
|
||||
</Layout>
|
||||
<GoogleButton preAuthStep={() => save(datasource, true)} />
|
||||
</ModalContent>
|
|
@ -6,7 +6,7 @@
|
|||
import api from "builderStore/api"
|
||||
import { notifications } from "@budibase/bbui"
|
||||
import CreateWebhookDeploymentModal from "./CreateWebhookDeploymentModal.svelte"
|
||||
import { store, hostingStore } from "builderStore"
|
||||
import { store } from "builderStore"
|
||||
|
||||
const DeploymentStatus = {
|
||||
SUCCESS: "SUCCESS",
|
||||
|
@ -37,7 +37,7 @@
|
|||
let poll
|
||||
let deployments = []
|
||||
let urlComponent = $store.url || `/${appId}`
|
||||
let deploymentUrl = `${$hostingStore.appUrl}${urlComponent}`
|
||||
let deploymentUrl = `${urlComponent}`
|
||||
|
||||
const formatDate = (date, format) =>
|
||||
Intl.DateTimeFormat("en-GB", DATE_OPTIONS[format]).format(date)
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
<script>
|
||||
import { Select } from "@budibase/bbui"
|
||||
import { store } from "builderStore"
|
||||
import { get } from "svelte/store"
|
||||
|
||||
const themeOptions = [
|
||||
{
|
||||
|
@ -20,6 +21,17 @@
|
|||
value: "spectrum--darkest",
|
||||
},
|
||||
]
|
||||
|
||||
const onChangeTheme = async theme => {
|
||||
await store.actions.theme.save(theme)
|
||||
await store.actions.customTheme.save({
|
||||
...get(store).customTheme,
|
||||
navBackground:
|
||||
theme === "spectrum--light"
|
||||
? "var(--spectrum-global-color-gray-50)"
|
||||
: "var(--spectrum-global-color-gray-100)",
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<div>
|
||||
|
@ -27,7 +39,7 @@
|
|||
value={$store.theme}
|
||||
options={themeOptions}
|
||||
placeholder={null}
|
||||
on:change={e => store.actions.theme.save(e.detail)}
|
||||
on:change={e => onChangeTheme(e.detail)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
|
|
@ -19,7 +19,7 @@
|
|||
primaryColor: "var(--spectrum-global-color-blue-600)",
|
||||
primaryColorHover: "var(--spectrum-global-color-blue-500)",
|
||||
buttonBorderRadius: "16px",
|
||||
navBackground: "var(--spectrum-global-color-gray-100)",
|
||||
navBackground: "var(--spectrum-global-color-gray-50)",
|
||||
navTextColor: "var(--spectrum-global-color-gray-800)",
|
||||
}
|
||||
|
||||
|
@ -52,7 +52,14 @@
|
|||
}
|
||||
|
||||
const resetTheme = () => {
|
||||
store.actions.customTheme.save(null)
|
||||
const theme = get(store).theme
|
||||
store.actions.customTheme.save({
|
||||
...defaultTheme,
|
||||
navBackground:
|
||||
theme === "spectrum--light"
|
||||
? "var(--spectrum-global-color-gray-50)"
|
||||
: "var(--spectrum-global-color-gray-100)",
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
|
|
|
@ -44,7 +44,8 @@
|
|||
"relationshipfield",
|
||||
"daterangepicker",
|
||||
"multifieldselect",
|
||||
"jsonfield"
|
||||
"jsonfield",
|
||||
"s3upload"
|
||||
]
|
||||
},
|
||||
{
|
||||
|
|
|
@ -0,0 +1,33 @@
|
|||
<script>
|
||||
import { Select, Label } from "@budibase/bbui"
|
||||
import { currentAsset } from "builderStore"
|
||||
import { findAllMatchingComponents } from "builderStore/componentUtils"
|
||||
|
||||
export let parameters
|
||||
|
||||
$: components = findAllMatchingComponents($currentAsset.props, component =>
|
||||
component._component.endsWith("s3upload")
|
||||
)
|
||||
</script>
|
||||
|
||||
<div class="root">
|
||||
<Label small>S3 Upload Component</Label>
|
||||
<Select
|
||||
bind:value={parameters.componentId}
|
||||
options={components}
|
||||
getOptionLabel={x => x._instanceName}
|
||||
getOptionValue={x => x._id}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.root {
|
||||
display: grid;
|
||||
column-gap: var(--spacing-l);
|
||||
row-gap: var(--spacing-s);
|
||||
grid-template-columns: 120px 1fr;
|
||||
align-items: center;
|
||||
max-width: 400px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
</style>
|
|
@ -11,3 +11,4 @@ export { default as ChangeFormStep } from "./ChangeFormStep.svelte"
|
|||
export { default as UpdateState } from "./UpdateState.svelte"
|
||||
export { default as RefreshDataProvider } from "./RefreshDataProvider.svelte"
|
||||
export { default as DuplicateRow } from "./DuplicateRow.svelte"
|
||||
export { default as S3Upload } from "./S3Upload.svelte"
|
||||
|
|
|
@ -70,6 +70,16 @@
|
|||
"name": "Update State",
|
||||
"component": "UpdateState",
|
||||
"dependsOnFeature": "state"
|
||||
},
|
||||
{
|
||||
"name": "Upload File to S3",
|
||||
"component": "S3Upload",
|
||||
"context": [
|
||||
{
|
||||
"label": "File URL",
|
||||
"value": "publicUrl"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
<script>
|
||||
import { Select } from "@budibase/bbui"
|
||||
import { datasources } from "stores/backend"
|
||||
|
||||
export let value = null
|
||||
|
||||
$: dataSources = $datasources.list
|
||||
.filter(ds => ds.source === "S3" && !ds.config?.endpoint)
|
||||
.map(ds => ({
|
||||
label: ds.name,
|
||||
value: ds._id,
|
||||
}))
|
||||
</script>
|
||||
|
||||
<Select options={dataSources} {value} on:change />
|
|
@ -1,5 +1,6 @@
|
|||
import { Checkbox, Select, Stepper } from "@budibase/bbui"
|
||||
import DataSourceSelect from "./DataSourceSelect.svelte"
|
||||
import S3DataSourceSelect from "./S3DataSourceSelect.svelte"
|
||||
import DataProviderSelect from "./DataProviderSelect.svelte"
|
||||
import ButtonActionEditor from "./ButtonActionEditor/ButtonActionEditor.svelte"
|
||||
import TableSelect from "./TableSelect.svelte"
|
||||
|
@ -22,6 +23,7 @@ const componentMap = {
|
|||
text: DrawerBindableCombobox,
|
||||
select: Select,
|
||||
dataSource: DataSourceSelect,
|
||||
"dataSource/s3": S3DataSourceSelect,
|
||||
dataProvider: DataProviderSelect,
|
||||
boolean: Checkbox,
|
||||
number: Stepper,
|
||||
|
|
|
@ -1,16 +1,14 @@
|
|||
<script>
|
||||
import { Label, Select } from "@budibase/bbui"
|
||||
import { permissions, roles } from "stores/backend"
|
||||
import { onMount } from "svelte"
|
||||
import { Roles } from "constants/backend"
|
||||
|
||||
export let query
|
||||
export let saveId
|
||||
export let label
|
||||
|
||||
$: updateRole(roleId, saveId)
|
||||
$: getPermissions(query)
|
||||
|
||||
let roleId, loaded
|
||||
let roleId, loaded, fetched
|
||||
|
||||
async function updateRole(role, id) {
|
||||
roleId = role
|
||||
|
@ -26,19 +24,23 @@
|
|||
}
|
||||
}
|
||||
|
||||
onMount(async () => {
|
||||
if (!query || !query._id) {
|
||||
async function getPermissions(queryToFetch) {
|
||||
if (fetched?._id === queryToFetch?._id) {
|
||||
return
|
||||
}
|
||||
fetched = queryToFetch
|
||||
if (!queryToFetch || !queryToFetch._id) {
|
||||
roleId = Roles.BASIC
|
||||
loaded = true
|
||||
return
|
||||
}
|
||||
try {
|
||||
roleId = (await permissions.forResource(query._id))["read"]
|
||||
roleId = (await permissions.forResource(queryToFetch._id))["read"]
|
||||
} catch (err) {
|
||||
roleId = Roles.BASIC
|
||||
}
|
||||
loaded = true
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if loaded}
|
||||
|
|
|
@ -15,8 +15,6 @@
|
|||
queryBindings = [...queryBindings, {}]
|
||||
}
|
||||
|
||||
$: console.log(bindings)
|
||||
|
||||
function deleteQueryBinding(idx) {
|
||||
queryBindings.splice(idx, 1)
|
||||
queryBindings = queryBindings
|
||||
|
|
|
@ -1,100 +1,46 @@
|
|||
<script>
|
||||
import { writable, get as svelteGet } from "svelte/store"
|
||||
|
||||
import { notifications, Input, ModalContent, Dropzone } from "@budibase/bbui"
|
||||
import { store, automationStore, hostingStore } from "builderStore"
|
||||
import { admin, auth } from "stores/portal"
|
||||
import { string, mixed, object } from "yup"
|
||||
import { store, automationStore } from "builderStore"
|
||||
import { apps, admin, auth } from "stores/portal"
|
||||
import api, { get, post } from "builderStore/api"
|
||||
import analytics, { Events } from "analytics"
|
||||
import { onMount } from "svelte"
|
||||
import { capitalise } from "helpers"
|
||||
import { goto } from "@roxi/routify"
|
||||
import { APP_NAME_REGEX } from "constants"
|
||||
import { createValidationStore } from "helpers/validation/yup"
|
||||
import * as appValidation from "helpers/validation/yup/app"
|
||||
|
||||
export let template
|
||||
export let inline
|
||||
|
||||
const values = writable({ name: null })
|
||||
const errors = writable({})
|
||||
const touched = writable({})
|
||||
const validator = {
|
||||
name: string()
|
||||
.trim()
|
||||
.required("Your application must have a name")
|
||||
.matches(
|
||||
APP_NAME_REGEX,
|
||||
"App name must be letters, numbers and spaces only"
|
||||
),
|
||||
file: template?.fromFile
|
||||
? mixed().required("Please choose a file to import")
|
||||
: null,
|
||||
}
|
||||
|
||||
let submitting = false
|
||||
let valid = false
|
||||
let initialTemplateInfo = template?.fromFile || template?.key
|
||||
|
||||
$: checkValidity($values, validator)
|
||||
$: showTemplateSelection = !template && !initialTemplateInfo
|
||||
const values = writable({ name: "", url: null })
|
||||
const validation = createValidationStore()
|
||||
$: validation.check($values)
|
||||
|
||||
onMount(async () => {
|
||||
await hostingStore.actions.fetchDeployedApps()
|
||||
const existingAppNames = svelteGet(hostingStore).deployedAppNames
|
||||
validator.name = string()
|
||||
.trim()
|
||||
.required("Your application must have a name")
|
||||
.matches(APP_NAME_REGEX, "App name must be letters and numbers only")
|
||||
.test(
|
||||
"non-existing-app-name",
|
||||
"Another app with the same name already exists",
|
||||
value => {
|
||||
return !existingAppNames.some(
|
||||
appName => appName.toLowerCase() === value.toLowerCase()
|
||||
)
|
||||
}
|
||||
)
|
||||
await setupValidation()
|
||||
})
|
||||
|
||||
const checkValidity = async (values, validator) => {
|
||||
const obj = object().shape(validator)
|
||||
Object.keys(validator).forEach(key => ($errors[key] = null))
|
||||
if (template?.fromFile && values.file == null) {
|
||||
valid = false
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
await obj.validate(values, { abortEarly: false })
|
||||
} catch (validationErrors) {
|
||||
validationErrors.inner.forEach(error => {
|
||||
$errors[error.path] = capitalise(error.message)
|
||||
})
|
||||
}
|
||||
|
||||
valid = await obj.isValid(values)
|
||||
const setupValidation = async () => {
|
||||
const applications = svelteGet(apps)
|
||||
appValidation.name(validation, { apps: applications })
|
||||
appValidation.url(validation, { apps: applications })
|
||||
appValidation.file(validation, { template })
|
||||
// init validation
|
||||
validation.check($values)
|
||||
}
|
||||
|
||||
async function createNewApp() {
|
||||
const templateToUse = Object.keys(template).length === 0 ? null : template
|
||||
submitting = true
|
||||
|
||||
// Check a template exists if we are important
|
||||
if (templateToUse?.fromFile && !$values.file) {
|
||||
$errors.file = "Please choose a file to import"
|
||||
valid = false
|
||||
submitting = false
|
||||
return false
|
||||
}
|
||||
|
||||
try {
|
||||
// Create form data to create app
|
||||
let data = new FormData()
|
||||
data.append("name", $values.name.trim())
|
||||
data.append("useTemplate", templateToUse != null)
|
||||
if (templateToUse) {
|
||||
data.append("templateName", templateToUse.name)
|
||||
data.append("templateKey", templateToUse.key)
|
||||
if ($values.url) {
|
||||
data.append("url", $values.url.trim())
|
||||
}
|
||||
data.append("useTemplate", template != null)
|
||||
if (template) {
|
||||
data.append("templateName", template.name)
|
||||
data.append("templateKey", template.key)
|
||||
data.append("templateFile", $values.file)
|
||||
}
|
||||
|
||||
|
@ -108,7 +54,7 @@
|
|||
analytics.captureEvent(Events.APP.CREATED, {
|
||||
name: $values.name,
|
||||
appId: appJson.instance._id,
|
||||
templateToUse,
|
||||
templateToUse: template,
|
||||
})
|
||||
|
||||
// Select Correct Application/DB in prep for creating user
|
||||
|
@ -136,44 +82,51 @@
|
|||
} catch (error) {
|
||||
console.error(error)
|
||||
notifications.error(error)
|
||||
submitting = false
|
||||
}
|
||||
}
|
||||
|
||||
async function onCancel() {
|
||||
template = null
|
||||
await auth.setInitInfo({})
|
||||
// auto add slash to url
|
||||
$: {
|
||||
if ($values.url && !$values.url.startsWith("/")) {
|
||||
$values.url = `/${$values.url}`
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<ModalContent
|
||||
title={"Name your app"}
|
||||
title={"Create your app"}
|
||||
confirmText={template?.fromFile ? "Import app" : "Create app"}
|
||||
onConfirm={createNewApp}
|
||||
onCancel={inline ? onCancel : null}
|
||||
cancelText={inline ? "Back" : undefined}
|
||||
showCloseIcon={!inline}
|
||||
disabled={!valid}
|
||||
disabled={!$validation.valid}
|
||||
>
|
||||
{#if template?.fromFile}
|
||||
<Dropzone
|
||||
error={$touched.file && $errors.file}
|
||||
error={$validation.touched.file && $validation.errors.file}
|
||||
gallery={false}
|
||||
label="File to import"
|
||||
value={[$values.file]}
|
||||
on:change={e => {
|
||||
$values.file = e.detail?.[0]
|
||||
$touched.file = true
|
||||
$validation.touched.file = true
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
<Input
|
||||
bind:value={$values.name}
|
||||
error={$touched.name && $errors.name}
|
||||
on:blur={() => ($touched.name = true)}
|
||||
error={$validation.touched.name && $validation.errors.name}
|
||||
on:blur={() => ($validation.touched.name = true)}
|
||||
label="Name"
|
||||
placeholder={$auth.user.firstName
|
||||
? `${$auth.user.firstName}'s app`
|
||||
? `${$auth.user.firstName}s app`
|
||||
: "My app"}
|
||||
/>
|
||||
<Input
|
||||
bind:value={$values.url}
|
||||
error={$validation.touched.url && $validation.errors.url}
|
||||
on:blur={() => ($validation.touched.url = true)}
|
||||
label="URL"
|
||||
placeholder={$values.name
|
||||
? "/" + encodeURIComponent($values.name).toLowerCase()
|
||||
: "/"}
|
||||
/>
|
||||
</ModalContent>
|
||||
|
|
|
@ -1,120 +1,75 @@
|
|||
<script>
|
||||
import { writable, get as svelteGet } from "svelte/store"
|
||||
import {
|
||||
notifications,
|
||||
Input,
|
||||
Modal,
|
||||
ModalContent,
|
||||
Body,
|
||||
} from "@budibase/bbui"
|
||||
import { hostingStore } from "builderStore"
|
||||
import { notifications, Input, ModalContent, Body } from "@budibase/bbui"
|
||||
import { apps } from "stores/portal"
|
||||
import { string, object } from "yup"
|
||||
import { onMount } from "svelte"
|
||||
import { capitalise } from "helpers"
|
||||
import { APP_NAME_REGEX } from "constants"
|
||||
|
||||
const values = writable({ name: null })
|
||||
const errors = writable({})
|
||||
const touched = writable({})
|
||||
const validator = {
|
||||
name: string()
|
||||
.trim()
|
||||
.required("Your application must have a name")
|
||||
.matches(
|
||||
APP_NAME_REGEX,
|
||||
"App name must be letters, numbers and spaces only"
|
||||
),
|
||||
}
|
||||
import { createValidationStore } from "helpers/validation/yup"
|
||||
import * as appValidation from "helpers/validation/yup/app"
|
||||
|
||||
export let app
|
||||
|
||||
let modal
|
||||
let valid = false
|
||||
let dirty = false
|
||||
$: checkValidity($values, validator)
|
||||
$: {
|
||||
// prevent validation by setting name to undefined without an app
|
||||
if (app) {
|
||||
$values.name = app?.name
|
||||
}
|
||||
}
|
||||
const values = writable({ name: "", url: null })
|
||||
const validation = createValidationStore()
|
||||
$: validation.check($values)
|
||||
|
||||
onMount(async () => {
|
||||
await hostingStore.actions.fetchDeployedApps()
|
||||
const existingAppNames = svelteGet(hostingStore).deployedAppNames
|
||||
validator.name = string()
|
||||
.trim()
|
||||
.required("Your application must have a name")
|
||||
.matches(
|
||||
APP_NAME_REGEX,
|
||||
"App name must be letters, numbers and spaces only"
|
||||
)
|
||||
.test(
|
||||
"non-existing-app-name",
|
||||
"Another app with the same name already exists",
|
||||
value => {
|
||||
return !existingAppNames.some(
|
||||
appName => dirty && appName.toLowerCase() === value.toLowerCase()
|
||||
)
|
||||
}
|
||||
)
|
||||
$values.name = app.name
|
||||
$values.url = app.url
|
||||
setupValidation()
|
||||
})
|
||||
|
||||
const checkValidity = async (values, validator) => {
|
||||
const obj = object().shape(validator)
|
||||
Object.keys(validator).forEach(key => ($errors[key] = null))
|
||||
try {
|
||||
await obj.validate(values, { abortEarly: false })
|
||||
} catch (validationErrors) {
|
||||
validationErrors.inner.forEach(error => {
|
||||
$errors[error.path] = capitalise(error.message)
|
||||
})
|
||||
}
|
||||
valid = await obj.isValid(values)
|
||||
const setupValidation = async () => {
|
||||
const applications = svelteGet(apps)
|
||||
appValidation.name(validation, { apps: applications, currentApp: app })
|
||||
appValidation.url(validation, { apps: applications, currentApp: app })
|
||||
// init validation
|
||||
validation.check($values)
|
||||
}
|
||||
|
||||
async function updateApp() {
|
||||
try {
|
||||
// Update App
|
||||
await apps.update(app.instance._id, { name: $values.name.trim() })
|
||||
hide()
|
||||
const body = {
|
||||
name: $values.name.trim(),
|
||||
}
|
||||
if ($values.url) {
|
||||
body.url = $values.url.trim()
|
||||
}
|
||||
await apps.update(app.instance._id, body)
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
notifications.error(error)
|
||||
}
|
||||
}
|
||||
|
||||
export const show = () => {
|
||||
modal.show()
|
||||
}
|
||||
export const hide = () => {
|
||||
modal.hide()
|
||||
}
|
||||
|
||||
const onCancel = () => {
|
||||
hide()
|
||||
}
|
||||
|
||||
const onShow = () => {
|
||||
dirty = false
|
||||
// auto add slash to url
|
||||
$: {
|
||||
if ($values.url && !$values.url.startsWith("/")) {
|
||||
$values.url = `/${$values.url}`
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<Modal bind:this={modal} on:hide={onCancel} on:show={onShow}>
|
||||
<ModalContent
|
||||
title={"Edit app"}
|
||||
confirmText={"Save"}
|
||||
onConfirm={updateApp}
|
||||
disabled={!(valid && dirty)}
|
||||
>
|
||||
<Body size="S">Update the name of your app.</Body>
|
||||
<Input
|
||||
bind:value={$values.name}
|
||||
error={$touched.name && $errors.name}
|
||||
on:blur={() => ($touched.name = true)}
|
||||
on:change={() => (dirty = true)}
|
||||
label="Name"
|
||||
/>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
<ModalContent
|
||||
title={"Edit app"}
|
||||
confirmText={"Save"}
|
||||
onConfirm={updateApp}
|
||||
disabled={!$validation.valid}
|
||||
>
|
||||
<Body size="S">Update the name of your app.</Body>
|
||||
<Input
|
||||
bind:value={$values.name}
|
||||
error={$validation.touched.name && $validation.errors.name}
|
||||
on:blur={() => ($validation.touched.name = true)}
|
||||
label="Name"
|
||||
/>
|
||||
<Input
|
||||
bind:value={$values.url}
|
||||
error={$validation.touched.url && $validation.errors.url}
|
||||
on:blur={() => ($validation.touched.url = true)}
|
||||
label="URL"
|
||||
placeholder={$values.name
|
||||
? "/" + encodeURIComponent($values.name).toLowerCase()
|
||||
: "/"}
|
||||
/>
|
||||
</ModalContent>
|
||||
|
|
|
@ -148,20 +148,23 @@ export const RelationshipTypes = {
|
|||
}
|
||||
|
||||
export const ALLOWABLE_STRING_OPTIONS = [FIELDS.STRING, FIELDS.OPTIONS]
|
||||
|
||||
export const ALLOWABLE_STRING_TYPES = ALLOWABLE_STRING_OPTIONS.map(
|
||||
opt => opt.type
|
||||
)
|
||||
|
||||
export const ALLOWABLE_NUMBER_OPTIONS = [FIELDS.NUMBER, FIELDS.BOOLEAN]
|
||||
|
||||
export const ALLOWABLE_NUMBER_TYPES = ALLOWABLE_NUMBER_OPTIONS.map(
|
||||
opt => opt.type
|
||||
)
|
||||
|
||||
export const SWITCHABLE_TYPES = ALLOWABLE_NUMBER_TYPES.concat(
|
||||
ALLOWABLE_STRING_TYPES
|
||||
)
|
||||
export const ALLOWABLE_JSON_OPTIONS = [FIELDS.JSON, FIELDS.ARRAY]
|
||||
export const ALLOWABLE_JSON_TYPES = ALLOWABLE_JSON_OPTIONS.map(opt => opt.type)
|
||||
|
||||
export const SWITCHABLE_TYPES = [
|
||||
...ALLOWABLE_STRING_TYPES,
|
||||
...ALLOWABLE_NUMBER_TYPES,
|
||||
...ALLOWABLE_JSON_TYPES,
|
||||
]
|
||||
|
||||
export const IntegrationTypes = {
|
||||
POSTGRES: "POSTGRES",
|
||||
|
@ -177,6 +180,7 @@ export const IntegrationTypes = {
|
|||
ARANGODB: "ARANGODB",
|
||||
ORACLE: "ORACLE",
|
||||
INTERNAL: "INTERNAL",
|
||||
GOOGLE_SHEETS: "GOOGLE_SHEETS",
|
||||
}
|
||||
|
||||
export const IntegrationNames = {
|
||||
|
@ -193,6 +197,7 @@ export const IntegrationNames = {
|
|||
[IntegrationTypes.ARANGODB]: "ArangoDB",
|
||||
[IntegrationTypes.ORACLE]: "Oracle",
|
||||
[IntegrationTypes.INTERNAL]: "Internal",
|
||||
[IntegrationTypes.GOOGLE_SHEETS]: "Google Sheets",
|
||||
}
|
||||
|
||||
export const SchemaTypeOptions = [
|
||||
|
|
|
@ -15,6 +15,22 @@ export const AppStatus = {
|
|||
DEPLOYED: "published",
|
||||
}
|
||||
|
||||
export const IntegrationNames = {
|
||||
POSTGRES: "PostgreSQL",
|
||||
MONGODB: "MongoDB",
|
||||
COUCHDB: "CouchDB",
|
||||
S3: "S3",
|
||||
MYSQL: "MySQL",
|
||||
REST: "REST",
|
||||
DYNAMODB: "DynamoDB",
|
||||
ELASTICSEARCH: "ElasticSearch",
|
||||
SQL_SERVER: "SQL Server",
|
||||
AIRTABLE: "Airtable",
|
||||
ARANGODB: "ArangoDB",
|
||||
ORACLE: "Oracle",
|
||||
GOOGLE_SHEETS: "Google Sheets",
|
||||
}
|
||||
|
||||
// fields on the user table that cannot be edited
|
||||
export const UNEDITABLE_USER_FIELDS = [
|
||||
"email",
|
||||
|
@ -36,4 +52,7 @@ export const LAYOUT_NAMES = {
|
|||
|
||||
export const BUDIBASE_INTERNAL_DB = "bb_internal"
|
||||
|
||||
// one or more word characters and whitespace
|
||||
export const APP_NAME_REGEX = /^[\w\s]+$/
|
||||
// zero or more non-whitespace characters
|
||||
export const APP_URL_REGEX = /^\S*$/
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
import { writable, derived } from "svelte/store"
|
||||
|
||||
// DEPRECATED - Use the yup based validators for future validation
|
||||
|
||||
export function createValidationStore(initialValue, ...validators) {
|
||||
let touched = false
|
||||
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
// TODO: Convert to yup based validators
|
||||
|
||||
export function emailValidator(value) {
|
||||
return (
|
||||
(value &&
|
||||
|
|
|
@ -0,0 +1,83 @@
|
|||
import { string, mixed } from "yup"
|
||||
import { APP_NAME_REGEX, APP_URL_REGEX } from "constants"
|
||||
|
||||
export const name = (validation, { apps, currentApp } = { apps: [] }) => {
|
||||
validation.addValidator(
|
||||
"name",
|
||||
string()
|
||||
.trim()
|
||||
.required("Your application must have a name")
|
||||
.matches(
|
||||
APP_NAME_REGEX,
|
||||
"App name must be letters, numbers and spaces only"
|
||||
)
|
||||
.test(
|
||||
"non-existing-app-name",
|
||||
"Another app with the same name already exists",
|
||||
value => {
|
||||
if (!value) {
|
||||
// exit early, above validator will fail
|
||||
return true
|
||||
}
|
||||
if (currentApp) {
|
||||
// filter out the current app if present
|
||||
apps = apps.filter(app => app.appId !== currentApp.appId)
|
||||
}
|
||||
return !apps
|
||||
.map(app => app.name)
|
||||
.some(appName => appName.toLowerCase() === value.toLowerCase())
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
export const url = (validation, { apps, currentApp } = { apps: [] }) => {
|
||||
validation.addValidator(
|
||||
"url",
|
||||
string()
|
||||
.nullable()
|
||||
.matches(APP_URL_REGEX, "App URL must not contain spaces")
|
||||
.test(
|
||||
"non-existing-app-url",
|
||||
"Another app with the same URL already exists",
|
||||
value => {
|
||||
// url is nullable
|
||||
if (!value) {
|
||||
return true
|
||||
}
|
||||
if (currentApp) {
|
||||
// filter out the current app if present
|
||||
apps = apps.filter(app => app.appId !== currentApp.appId)
|
||||
}
|
||||
return !apps
|
||||
.map(app => app.url)
|
||||
.some(appUrl => appUrl?.toLowerCase() === value.toLowerCase())
|
||||
}
|
||||
)
|
||||
.test("valid-url", "Not a valid URL", value => {
|
||||
// url is nullable
|
||||
if (!value) {
|
||||
return true
|
||||
}
|
||||
// make it clear that this is a url path and cannot be a full url
|
||||
return (
|
||||
value.startsWith("/") &&
|
||||
!value.includes("http") &&
|
||||
!value.includes("www") &&
|
||||
!value.includes(".") &&
|
||||
value.length > 1 // just '/' is not valid
|
||||
)
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
export const file = (validation, { template } = {}) => {
|
||||
const templateToUse =
|
||||
template && Object.keys(template).length === 0 ? null : template
|
||||
validation.addValidator(
|
||||
"file",
|
||||
templateToUse?.fromFile
|
||||
? mixed().required("Please choose a file to import")
|
||||
: null
|
||||
)
|
||||
}
|
|
@ -0,0 +1,66 @@
|
|||
import { capitalise } from "helpers"
|
||||
import { object } from "yup"
|
||||
import { writable, get } from "svelte/store"
|
||||
import { notifications } from "@budibase/bbui"
|
||||
|
||||
export const createValidationStore = () => {
|
||||
const DEFAULT = {
|
||||
errors: {},
|
||||
touched: {},
|
||||
valid: false,
|
||||
}
|
||||
|
||||
const validator = {}
|
||||
const validation = writable(DEFAULT)
|
||||
|
||||
const addValidator = (propertyName, propertyValidator) => {
|
||||
if (!propertyValidator || !propertyName) {
|
||||
return
|
||||
}
|
||||
validator[propertyName] = propertyValidator
|
||||
}
|
||||
|
||||
const check = async values => {
|
||||
const obj = object().shape(validator)
|
||||
// clear the previous errors
|
||||
const properties = Object.keys(validator)
|
||||
properties.forEach(property => (get(validation).errors[property] = null))
|
||||
|
||||
let validationError = false
|
||||
try {
|
||||
await obj.validate(values, { abortEarly: false })
|
||||
} catch (error) {
|
||||
if (!error.inner) {
|
||||
notifications.error("Unexpected validation error", error)
|
||||
validationError = true
|
||||
} else {
|
||||
error.inner.forEach(err => {
|
||||
validation.update(store => {
|
||||
store.errors[err.path] = capitalise(err.message)
|
||||
return store
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
let valid
|
||||
if (properties.length && !validationError) {
|
||||
valid = await obj.isValid(values)
|
||||
} else {
|
||||
// don't say valid until validators have been loaded
|
||||
valid = false
|
||||
}
|
||||
|
||||
validation.update(store => {
|
||||
store.valid = valid
|
||||
return store
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
subscribe: validation.subscribe,
|
||||
set: validation.set,
|
||||
check,
|
||||
addValidator,
|
||||
}
|
||||
}
|
|
@ -19,8 +19,8 @@
|
|||
import { IntegrationTypes } from "constants/backend"
|
||||
import { isEqual } from "lodash"
|
||||
import { cloneDeep } from "lodash/fp"
|
||||
|
||||
import ImportRestQueriesModal from "components/backend/DatasourceNavigator/modals/ImportRestQueriesModal.svelte"
|
||||
|
||||
let importQueriesModal
|
||||
|
||||
let changed
|
||||
|
|
|
@ -12,7 +12,7 @@
|
|||
Modal,
|
||||
} from "@budibase/bbui"
|
||||
import { onMount } from "svelte"
|
||||
import { apps, organisation, auth, admin } from "stores/portal"
|
||||
import { apps, organisation, auth } from "stores/portal"
|
||||
import { goto } from "@roxi/routify"
|
||||
import { AppStatus } from "constants"
|
||||
import { gradient } from "actions"
|
||||
|
@ -34,7 +34,6 @@
|
|||
const publishedAppsOnly = app => app.status === AppStatus.DEPLOYED
|
||||
|
||||
$: publishedApps = $apps.filter(publishedAppsOnly)
|
||||
$: isCloud = $admin.cloud
|
||||
$: userApps = $auth.user?.builder?.global
|
||||
? publishedApps
|
||||
: publishedApps.filter(app =>
|
||||
|
@ -42,7 +41,11 @@
|
|||
)
|
||||
|
||||
function getUrl(app) {
|
||||
return !isCloud ? `/app/${encodeURIComponent(app.name)}` : `/${app.prodId}`
|
||||
if (app.url) {
|
||||
return `/app${app.url}`
|
||||
} else {
|
||||
return `/${app.prodId}`
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
|
|
|
@ -31,6 +31,7 @@
|
|||
}
|
||||
|
||||
onMount(async () => {
|
||||
await auth.checkAuth()
|
||||
await organisation.init()
|
||||
})
|
||||
</script>
|
||||
|
|
|
@ -49,7 +49,6 @@
|
|||
$: filteredApps = enrichedApps.filter(app =>
|
||||
app?.name?.toLowerCase().includes(searchTerm.toLowerCase())
|
||||
)
|
||||
$: isCloud = $admin.cloud
|
||||
|
||||
const enrichApps = (apps, user, sortBy) => {
|
||||
const enrichedApps = apps.map(app => ({
|
||||
|
@ -80,7 +79,7 @@
|
|||
}
|
||||
|
||||
const initiateAppCreation = () => {
|
||||
template = {}
|
||||
template = null
|
||||
creationModal.show()
|
||||
creatingApp = true
|
||||
}
|
||||
|
@ -162,12 +161,10 @@
|
|||
}
|
||||
|
||||
const viewApp = app => {
|
||||
if (!isCloud && app.deployed) {
|
||||
// special case to use the short form name if self hosted
|
||||
window.open(`/app/${encodeURIComponent(app.name)}`)
|
||||
if (app.url) {
|
||||
window.open(`/app${app.url}`)
|
||||
} else {
|
||||
const id = app.deployed ? app.prodId : app.devId
|
||||
window.open(`/${id}`, "_blank")
|
||||
window.open(`/${app.prodId}`)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -442,6 +439,11 @@
|
|||
>
|
||||
<CreateAppModal {template} />
|
||||
</Modal>
|
||||
|
||||
<Modal bind:this={updatingModal} padding={false} width="600px">
|
||||
<UpdateAppModal app={selectedApp} />
|
||||
</Modal>
|
||||
|
||||
<ConfirmDialog
|
||||
bind:this={deletionModal}
|
||||
title="Confirm deletion"
|
||||
|
@ -468,7 +470,6 @@
|
|||
Are you sure you want to unpublish the app <b>{selectedApp?.name}</b>?
|
||||
</ConfirmDialog>
|
||||
|
||||
<UpdateAppModal app={selectedApp} bind:this={updatingModal} />
|
||||
<ChooseIconModal app={selectedApp} bind:this={iconModal} />
|
||||
|
||||
<style>
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@budibase/cli",
|
||||
"version": "1.0.46-alpha.3",
|
||||
"version": "1.0.49-alpha.0",
|
||||
"description": "Budibase CLI, for developers, self hosting and migrations.",
|
||||
"main": "src/index.js",
|
||||
"bin": {
|
||||
|
|
|
@ -3340,5 +3340,50 @@
|
|||
"suffix": "repeater"
|
||||
}
|
||||
]
|
||||
},
|
||||
"s3upload": {
|
||||
"name": "S3 File Upload",
|
||||
"info": "This component can't be used with S3 datasources that use custom endpoints.",
|
||||
"icon": "UploadToCloud",
|
||||
"styles": ["size"],
|
||||
"editable": true,
|
||||
"settings": [
|
||||
{
|
||||
"type": "field/attachment",
|
||||
"label": "Field",
|
||||
"key": "field"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"label": "Label",
|
||||
"key": "label"
|
||||
},
|
||||
{
|
||||
"type": "dataSource/s3",
|
||||
"label": "S3 Datasource",
|
||||
"key": "datasourceId"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"label": "Bucket",
|
||||
"key": "bucket"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"label": "File Name",
|
||||
"key": "key"
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"label": "Disabled",
|
||||
"key": "disabled",
|
||||
"defaultValue": false
|
||||
},
|
||||
{
|
||||
"type": "validation/attachment",
|
||||
"label": "Validation",
|
||||
"key": "validation"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@budibase/client",
|
||||
"version": "1.0.46-alpha.3",
|
||||
"version": "1.0.49-alpha.0",
|
||||
"license": "MPL-2.0",
|
||||
"module": "dist/budibase-client.js",
|
||||
"main": "dist/budibase-client.js",
|
||||
|
@ -19,10 +19,11 @@
|
|||
"dev:builder": "rollup -cw"
|
||||
},
|
||||
"dependencies": {
|
||||
"@budibase/bbui": "^1.0.46-alpha.3",
|
||||
"@budibase/bbui": "^1.0.49-alpha.0",
|
||||
"@budibase/standard-components": "^0.9.139",
|
||||
"@budibase/string-templates": "^1.0.46-alpha.3",
|
||||
"@budibase/string-templates": "^1.0.49-alpha.0",
|
||||
"regexparam": "^1.3.0",
|
||||
"rollup-plugin-polyfill-node": "^0.8.0",
|
||||
"shortid": "^2.2.15",
|
||||
"svelte-spa-router": "^3.0.5"
|
||||
},
|
||||
|
@ -45,8 +46,6 @@
|
|||
"postcss": "^8.2.10",
|
||||
"rollup": "^2.44.0",
|
||||
"rollup-plugin-json": "^4.0.0",
|
||||
"rollup-plugin-node-builtins": "^2.1.2",
|
||||
"rollup-plugin-node-globals": "^1.4.0",
|
||||
"rollup-plugin-postcss": "^4.0.0",
|
||||
"rollup-plugin-svelte": "^7.1.0",
|
||||
"rollup-plugin-svg": "^2.0.0",
|
||||
|
|
|
@ -6,8 +6,7 @@ import { terser } from "rollup-plugin-terser"
|
|||
import postcss from "rollup-plugin-postcss"
|
||||
import svg from "rollup-plugin-svg"
|
||||
import json from "rollup-plugin-json"
|
||||
import builtins from "rollup-plugin-node-builtins"
|
||||
import globals from "rollup-plugin-node-globals"
|
||||
import nodePolyfills from "rollup-plugin-polyfill-node"
|
||||
import path from "path"
|
||||
|
||||
const production = !process.env.ROLLUP_WATCH
|
||||
|
@ -75,8 +74,7 @@ export default {
|
|||
}),
|
||||
postcss(),
|
||||
commonjs(),
|
||||
globals(),
|
||||
builtins(),
|
||||
nodePolyfills(),
|
||||
resolve({
|
||||
preferBuiltins: true,
|
||||
browser: true,
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import { notificationStore } from "stores"
|
||||
import { notificationStore, authStore } from "stores"
|
||||
import { get } from "svelte/store"
|
||||
import { ApiVersion } from "constants"
|
||||
|
||||
/**
|
||||
|
@ -28,6 +29,13 @@ const makeApiCall = async ({ method, url, body, json = true }) => {
|
|||
...(json && { "Content-Type": "application/json" }),
|
||||
...(!inBuilder && { "x-budibase-type": "client" }),
|
||||
}
|
||||
|
||||
// add csrf token if authenticated
|
||||
const auth = get(authStore)
|
||||
if (auth && auth.csrfToken) {
|
||||
headers["x-csrf-token"] = auth.csrfToken
|
||||
}
|
||||
|
||||
const response = await fetch(url, {
|
||||
method,
|
||||
headers,
|
||||
|
@ -36,7 +44,11 @@ const makeApiCall = async ({ method, url, body, json = true }) => {
|
|||
})
|
||||
switch (response.status) {
|
||||
case 200:
|
||||
return response.json()
|
||||
try {
|
||||
return await response.json()
|
||||
} catch (error) {
|
||||
return null
|
||||
}
|
||||
case 401:
|
||||
notificationStore.actions.error("Invalid credentials")
|
||||
return handleError(`Invalid credentials`)
|
||||
|
@ -82,14 +94,15 @@ const makeCachedApiCall = async params => {
|
|||
* Constructs an API call function for a particular HTTP method.
|
||||
*/
|
||||
const requestApiCall = method => async params => {
|
||||
const { url, cache = false } = params
|
||||
const fixedUrl = `/${url}`.replace("//", "/")
|
||||
const { external = false, url, cache = false } = params
|
||||
const fixedUrl = external ? url : `/${url}`.replace("//", "/")
|
||||
const enrichedParams = { ...params, method, url: fixedUrl }
|
||||
return await (cache ? makeCachedApiCall : makeApiCall)(enrichedParams)
|
||||
}
|
||||
|
||||
export default {
|
||||
post: requestApiCall("POST"),
|
||||
put: requestApiCall("PUT"),
|
||||
get: requestApiCall("GET"),
|
||||
patch: requestApiCall("PATCH"),
|
||||
del: requestApiCall("DELETE"),
|
||||
|
|
|
@ -10,3 +10,41 @@ export const uploadAttachment = async (data, tableId = "") => {
|
|||
json: false,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a signed URL to upload a file to an external datasource.
|
||||
*/
|
||||
export const getSignedDatasourceURL = async (datasourceId, bucket, key) => {
|
||||
if (!datasourceId) {
|
||||
return null
|
||||
}
|
||||
const res = await API.post({
|
||||
url: `/api/attachments/${datasourceId}/url`,
|
||||
body: { bucket, key },
|
||||
})
|
||||
if (res.error) {
|
||||
throw "Could not generate signed upload URL"
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
/**
|
||||
* Uploads a file to an external datasource.
|
||||
*/
|
||||
export const externalUpload = async (datasourceId, bucket, key, data) => {
|
||||
const { signedUrl, publicUrl } = await getSignedDatasourceURL(
|
||||
datasourceId,
|
||||
bucket,
|
||||
key
|
||||
)
|
||||
const res = await API.put({
|
||||
url: signedUrl,
|
||||
body: data,
|
||||
json: false,
|
||||
external: true,
|
||||
})
|
||||
if (res?.error) {
|
||||
throw "Could not upload file to signed URL"
|
||||
}
|
||||
return { publicUrl }
|
||||
}
|
||||
|
|
|
@ -276,27 +276,29 @@
|
|||
// reactive statements as much as possible.
|
||||
const cacheSettings = (enriched, nested, conditional) => {
|
||||
const allSettings = { ...enriched, ...nested, ...conditional }
|
||||
if (!cachedSettings) {
|
||||
const mounted = ref?.$$set != null
|
||||
if (!cachedSettings || !mounted) {
|
||||
cachedSettings = { ...allSettings }
|
||||
initialSettings = cachedSettings
|
||||
} else {
|
||||
Object.keys(allSettings).forEach(key => {
|
||||
const same = propsAreSame(allSettings[key], cachedSettings[key])
|
||||
if (!same) {
|
||||
// Updated cachedSettings (which is assigned by reference to
|
||||
// initialSettings) so that if we remount the component then the
|
||||
// initial props are up to date. By setting it this way rather than
|
||||
// setting it on initialSettings directly, we avoid a double render.
|
||||
cachedSettings[key] = allSettings[key]
|
||||
assignSetting(key, allSettings[key])
|
||||
|
||||
// Programmatically set the prop to avoid svelte reactive statements
|
||||
// firing inside components. This circumvents the problems caused by
|
||||
// spreading a props object.
|
||||
ref.$$set({ [key]: allSettings[key] })
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Assigns a certain setting to this component.
|
||||
// We manually use the svelte $set function to avoid triggering additional
|
||||
// reactive statements.
|
||||
const assignSetting = (key, value) => {
|
||||
ref?.$$set?.({ [key]: value })
|
||||
}
|
||||
|
||||
// Generates a key used to determine when components need to fully remount.
|
||||
// Currently only toggling editing requires remounting.
|
||||
const getRenderKey = (id, editing) => {
|
||||
|
@ -305,7 +307,7 @@
|
|||
</script>
|
||||
|
||||
{#key renderKey}
|
||||
{#if constructor && cachedSettings && (visible || inSelectedPath)}
|
||||
{#if constructor && initialSettings && (visible || inSelectedPath)}
|
||||
<!-- The ID is used as a class because getElementsByClassName is O(1) -->
|
||||
<!-- and the performance matters for the selection indicators -->
|
||||
<div
|
||||
|
|
|
@ -0,0 +1,143 @@
|
|||
<script>
|
||||
import Field from "./Field.svelte"
|
||||
import { CoreDropzone, ProgressCircle } from "@budibase/bbui"
|
||||
import { getContext, onMount, onDestroy } from "svelte"
|
||||
|
||||
export let datasourceId
|
||||
export let bucket
|
||||
export let key
|
||||
export let field
|
||||
export let label
|
||||
export let disabled = false
|
||||
export let validation
|
||||
|
||||
let fieldState
|
||||
let fieldApi
|
||||
|
||||
const { API, notificationStore, uploadStore } = getContext("sdk")
|
||||
const component = getContext("component")
|
||||
|
||||
// 5GB cap per item sent via S3 REST API
|
||||
const MaxFileSize = 1000000000 * 5
|
||||
|
||||
// Actual file data to upload
|
||||
let data
|
||||
let loading = false
|
||||
|
||||
const handleFileTooLarge = () => {
|
||||
notificationStore.actions.warning(
|
||||
"Files cannot exceed 5GB. Please try again with a smaller file."
|
||||
)
|
||||
}
|
||||
|
||||
// Process the file input and return a serializable structure expected by
|
||||
// the dropzone component to display the file
|
||||
const processFiles = async fileList => {
|
||||
return await new Promise(resolve => {
|
||||
if (!fileList?.length) {
|
||||
return []
|
||||
}
|
||||
|
||||
// Don't read in non-image files
|
||||
data = fileList[0]
|
||||
if (!data.type?.startsWith("image")) {
|
||||
resolve([
|
||||
{
|
||||
name: data.name,
|
||||
type: data.type,
|
||||
},
|
||||
])
|
||||
}
|
||||
|
||||
// Read image files and display as preview
|
||||
const reader = new FileReader()
|
||||
reader.addEventListener(
|
||||
"load",
|
||||
() => {
|
||||
resolve([
|
||||
{
|
||||
url: reader.result,
|
||||
name: data.name,
|
||||
type: data.type,
|
||||
},
|
||||
])
|
||||
},
|
||||
false
|
||||
)
|
||||
reader.readAsDataURL(fileList[0])
|
||||
})
|
||||
}
|
||||
|
||||
const upload = async () => {
|
||||
loading = true
|
||||
try {
|
||||
const res = await API.externalUpload(datasourceId, bucket, key, data)
|
||||
notificationStore.actions.success("File uploaded successfully")
|
||||
loading = false
|
||||
return res
|
||||
} catch (error) {
|
||||
notificationStore.actions.error(`Error uploading file: ${error}`)
|
||||
}
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
uploadStore.actions.registerFileUpload($component.id, upload)
|
||||
})
|
||||
|
||||
onDestroy(() => {
|
||||
uploadStore.actions.unregisterFileUpload($component.id)
|
||||
})
|
||||
</script>
|
||||
|
||||
<Field
|
||||
{label}
|
||||
{field}
|
||||
{disabled}
|
||||
{validation}
|
||||
type="s3upload"
|
||||
bind:fieldState
|
||||
bind:fieldApi
|
||||
defaultValue={[]}
|
||||
>
|
||||
<div class="content">
|
||||
{#if fieldState}
|
||||
<CoreDropzone
|
||||
value={fieldState.value}
|
||||
disabled={loading || fieldState.disabled}
|
||||
error={fieldState.error}
|
||||
on:change={e => {
|
||||
fieldApi.setValue(e.detail)
|
||||
}}
|
||||
{processFiles}
|
||||
{handleFileTooLarge}
|
||||
maximum={1}
|
||||
fileSizeLimit={MaxFileSize}
|
||||
/>
|
||||
{/if}
|
||||
{#if loading}
|
||||
<div class="overlay" />
|
||||
<div class="loading">
|
||||
<ProgressCircle />
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</Field>
|
||||
|
||||
<style>
|
||||
.content {
|
||||
position: relative;
|
||||
}
|
||||
.overlay,
|
||||
.loading {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
}
|
||||
.overlay {
|
||||
background-color: var(--spectrum-global-color-gray-50);
|
||||
opacity: 0.5;
|
||||
}
|
||||
</style>
|
|
@ -12,3 +12,4 @@ export { default as relationshipfield } from "./RelationshipField.svelte"
|
|||
export { default as passwordfield } from "./PasswordField.svelte"
|
||||
export { default as formstep } from "./FormStep.svelte"
|
||||
export { default as jsonfield } from "./JSONField.svelte"
|
||||
export { default as s3upload } from "./S3Upload.svelte"
|
||||
|
|
|
@ -5,6 +5,7 @@ import {
|
|||
routeStore,
|
||||
screenStore,
|
||||
builderStore,
|
||||
uploadStore,
|
||||
} from "stores"
|
||||
import { styleable } from "utils/styleable"
|
||||
import { linkable } from "utils/linkable"
|
||||
|
@ -20,6 +21,7 @@ export default {
|
|||
routeStore,
|
||||
screenStore,
|
||||
builderStore,
|
||||
uploadStore,
|
||||
styleable,
|
||||
linkable,
|
||||
getAction,
|
||||
|
|
|
@ -9,6 +9,7 @@ export { confirmationStore } from "./confirmation"
|
|||
export { peekStore } from "./peek"
|
||||
export { stateStore } from "./state"
|
||||
export { themeStore } from "./theme"
|
||||
export { uploadStore } from "./uploads.js"
|
||||
|
||||
// Context stores are layered and duplicated, so it is not a singleton
|
||||
export { createContextStore } from "./context"
|
||||
|
|
|
@ -0,0 +1,42 @@
|
|||
import { writable, get } from "svelte/store"
|
||||
|
||||
export const createUploadStore = () => {
|
||||
const store = writable([])
|
||||
|
||||
// Registers a new file upload component
|
||||
const registerFileUpload = (componentId, callback) => {
|
||||
if (!componentId || !callback) {
|
||||
return
|
||||
}
|
||||
|
||||
store.update(state => {
|
||||
state.push({
|
||||
componentId,
|
||||
callback,
|
||||
})
|
||||
return state
|
||||
})
|
||||
}
|
||||
|
||||
// Unregisters a file upload component
|
||||
const unregisterFileUpload = componentId => {
|
||||
store.update(state => state.filter(c => c.componentId !== componentId))
|
||||
}
|
||||
|
||||
// Processes a file upload for a given component ID
|
||||
const processFileUpload = async componentId => {
|
||||
if (!componentId) {
|
||||
return
|
||||
}
|
||||
|
||||
const component = get(store).find(c => c.componentId === componentId)
|
||||
return await component?.callback()
|
||||
}
|
||||
|
||||
return {
|
||||
subscribe: store.subscribe,
|
||||
actions: { registerFileUpload, unregisterFileUpload, processFileUpload },
|
||||
}
|
||||
}
|
||||
|
||||
export const uploadStore = createUploadStore()
|
|
@ -5,6 +5,7 @@ import {
|
|||
confirmationStore,
|
||||
authStore,
|
||||
stateStore,
|
||||
uploadStore,
|
||||
} from "stores"
|
||||
import { saveRow, deleteRow, executeQuery, triggerAutomation } from "api"
|
||||
import { ActionTypes } from "constants"
|
||||
|
@ -169,6 +170,17 @@ const updateStateHandler = action => {
|
|||
}
|
||||
}
|
||||
|
||||
const s3UploadHandler = async action => {
|
||||
const { componentId } = action.parameters
|
||||
if (!componentId) {
|
||||
return
|
||||
}
|
||||
const res = await uploadStore.actions.processFileUpload(componentId)
|
||||
return {
|
||||
publicUrl: res?.publicUrl,
|
||||
}
|
||||
}
|
||||
|
||||
const handlerMap = {
|
||||
["Save Row"]: saveRowHandler,
|
||||
["Duplicate Row"]: duplicateRowHandler,
|
||||
|
@ -183,6 +195,7 @@ const handlerMap = {
|
|||
["Close Screen Modal"]: closeScreenModalHandler,
|
||||
["Change Form Step"]: changeFormStepHandler,
|
||||
["Update State"]: updateStateHandler,
|
||||
["Upload File to S3"]: s3UploadHandler,
|
||||
}
|
||||
|
||||
const confirmTextMap = {
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"watch": ["src", "../auth"],
|
||||
"watch": ["src", "../backend-core"],
|
||||
"ext": "js,ts,json",
|
||||
"ignore": ["src/**/*.spec.ts", "src/**/*.spec.js"],
|
||||
"exec": "ts-node src/index.ts"
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "@budibase/server",
|
||||
"email": "hi@budibase.com",
|
||||
"version": "1.0.46-alpha.3",
|
||||
"version": "1.0.49-alpha.0",
|
||||
"description": "Budibase Web Server",
|
||||
"main": "src/index.ts",
|
||||
"repository": {
|
||||
|
@ -70,9 +70,9 @@
|
|||
"license": "GPL-3.0",
|
||||
"dependencies": {
|
||||
"@apidevtools/swagger-parser": "^10.0.3",
|
||||
"@budibase/backend-core": "^1.0.46-alpha.3",
|
||||
"@budibase/client": "^1.0.46-alpha.3",
|
||||
"@budibase/string-templates": "^1.0.46-alpha.3",
|
||||
"@budibase/backend-core": "^1.0.49-alpha.0",
|
||||
"@budibase/client": "^1.0.49-alpha.0",
|
||||
"@budibase/string-templates": "^1.0.49-alpha.0",
|
||||
"@bull-board/api": "^3.7.0",
|
||||
"@bull-board/koa": "^3.7.0",
|
||||
"@elastic/elasticsearch": "7.10.0",
|
||||
|
@ -92,6 +92,8 @@
|
|||
"fix-path": "3.0.0",
|
||||
"form-data": "^4.0.0",
|
||||
"fs-extra": "8.1.0",
|
||||
"google-auth-library": "^7.11.0",
|
||||
"google-spreadsheet": "^3.2.0",
|
||||
"jimp": "0.16.1",
|
||||
"joi": "17.2.1",
|
||||
"js-yaml": "^4.1.0",
|
||||
|
@ -110,7 +112,7 @@
|
|||
"mongodb": "3.6.3",
|
||||
"mssql": "6.2.3",
|
||||
"mysql2": "^2.3.1",
|
||||
"node-fetch": "2.6.0",
|
||||
"node-fetch": "2.6.7",
|
||||
"open": "^8.4.0",
|
||||
"pg": "8.5.1",
|
||||
"pino-pretty": "4.0.0",
|
||||
|
@ -139,6 +141,7 @@
|
|||
"@jest/test-sequencer": "^24.8.0",
|
||||
"@types/apidoc": "^0.50.0",
|
||||
"@types/bull": "^3.15.1",
|
||||
"@types/google-spreadsheet": "^3.1.5",
|
||||
"@types/jest": "^26.0.23",
|
||||
"@types/koa": "^2.13.3",
|
||||
"@types/koa-router": "^7.4.2",
|
||||
|
|
|
@ -33,10 +33,7 @@ const {
|
|||
Replication,
|
||||
} = require("@budibase/backend-core/db")
|
||||
const { USERS_TABLE_SCHEMA } = require("../../constants")
|
||||
const {
|
||||
getDeployedApps,
|
||||
removeAppFromUserRoles,
|
||||
} = require("../../utilities/workerRequests")
|
||||
const { removeAppFromUserRoles } = require("../../utilities/workerRequests")
|
||||
const { clientLibraryPath, stringToReadStream } = require("../../utilities")
|
||||
const { getAllLocks } = require("../../utilities/redis")
|
||||
const {
|
||||
|
@ -78,31 +75,44 @@ function getUserRoleId(ctx) {
|
|||
: ctx.user.role._id
|
||||
}
|
||||
|
||||
async function getAppUrlIfNotInUse(ctx) {
|
||||
async function getAppUrl(ctx) {
|
||||
// construct the url
|
||||
let url
|
||||
if (ctx.request.body.url) {
|
||||
// if the url is provided, use that
|
||||
url = encodeURI(ctx.request.body.url)
|
||||
} else if (ctx.request.body.name) {
|
||||
// otherwise use the name
|
||||
url = encodeURI(`${ctx.request.body.name}`)
|
||||
}
|
||||
if (url) {
|
||||
url = `/${url.replace(URL_REGEX_SLASH, "")}`.toLowerCase()
|
||||
}
|
||||
if (!env.SELF_HOSTED) {
|
||||
return url
|
||||
}
|
||||
const deployedApps = await getDeployedApps()
|
||||
if (
|
||||
url &&
|
||||
deployedApps[url] != null &&
|
||||
ctx.params != null &&
|
||||
deployedApps[url].appId !== ctx.params.appId
|
||||
) {
|
||||
ctx.throw(400, "App name/URL is already in use.")
|
||||
}
|
||||
return url
|
||||
}
|
||||
|
||||
const checkAppUrl = (ctx, apps, url, currentAppId) => {
|
||||
if (currentAppId) {
|
||||
apps = apps.filter(app => app.appId !== currentAppId)
|
||||
}
|
||||
if (apps.some(app => app.url === url)) {
|
||||
ctx.throw(400, "App URL is already in use.")
|
||||
}
|
||||
}
|
||||
|
||||
const checkAppName = (ctx, apps, name, currentAppId) => {
|
||||
// TODO: Replace with Joi
|
||||
if (!name) {
|
||||
ctx.throw(400, "Name is required")
|
||||
}
|
||||
if (currentAppId) {
|
||||
apps = apps.filter(app => app.appId !== currentAppId)
|
||||
}
|
||||
if (apps.some(app => app.name === name)) {
|
||||
ctx.throw(400, "App name is already in use.")
|
||||
}
|
||||
}
|
||||
|
||||
async function createInstance(template) {
|
||||
const tenantId = isMultiTenant() ? getTenantId() : null
|
||||
const baseAppId = generateAppID(tenantId)
|
||||
|
@ -206,6 +216,12 @@ exports.fetchAppPackage = async ctx => {
|
|||
}
|
||||
|
||||
exports.create = async ctx => {
|
||||
const apps = await getAllApps(CouchDB, { dev: true })
|
||||
const name = ctx.request.body.name
|
||||
checkAppName(ctx, apps, name)
|
||||
const url = await getAppUrl(ctx)
|
||||
checkAppUrl(ctx, apps, url)
|
||||
|
||||
const { useTemplate, templateKey, templateString } = ctx.request.body
|
||||
const instanceConfig = {
|
||||
useTemplate,
|
||||
|
@ -218,7 +234,6 @@ exports.create = async ctx => {
|
|||
const instance = await createInstance(instanceConfig)
|
||||
const appId = instance._id
|
||||
|
||||
const url = await getAppUrlIfNotInUse(ctx)
|
||||
const db = new CouchDB(appId)
|
||||
let _rev
|
||||
try {
|
||||
|
@ -235,7 +250,7 @@ exports.create = async ctx => {
|
|||
type: "app",
|
||||
version: packageJson.version,
|
||||
componentLibraries: ["@budibase/standard-components"],
|
||||
name: ctx.request.body.name,
|
||||
name: name,
|
||||
url: url,
|
||||
template: ctx.request.body.template,
|
||||
instance: instance,
|
||||
|
@ -262,8 +277,22 @@ exports.create = async ctx => {
|
|||
ctx.body = newApplication
|
||||
}
|
||||
|
||||
// This endpoint currently operates as a PATCH rather than a PUT
|
||||
// Thus name and url fields are handled only if present
|
||||
exports.update = async ctx => {
|
||||
const data = await updateAppPackage(ctx, ctx.request.body, ctx.params.appId)
|
||||
const apps = await getAllApps(CouchDB, { dev: true })
|
||||
// validation
|
||||
const name = ctx.request.body.name
|
||||
if (name) {
|
||||
checkAppName(ctx, apps, name, ctx.params.appId)
|
||||
}
|
||||
const url = await getAppUrl(ctx)
|
||||
if (url) {
|
||||
checkAppUrl(ctx, apps, url, ctx.params.appId)
|
||||
ctx.request.body.url = url
|
||||
}
|
||||
|
||||
const data = await updateAppPackage(ctx.request.body, ctx.params.appId)
|
||||
ctx.status = 200
|
||||
ctx.body = data
|
||||
}
|
||||
|
@ -285,7 +314,7 @@ exports.updateClient = async ctx => {
|
|||
version: packageJson.version,
|
||||
revertableVersion: currentVersion,
|
||||
}
|
||||
const data = await updateAppPackage(ctx, appPackageUpdates, ctx.params.appId)
|
||||
const data = await updateAppPackage(appPackageUpdates, ctx.params.appId)
|
||||
ctx.status = 200
|
||||
ctx.body = data
|
||||
}
|
||||
|
@ -308,7 +337,7 @@ exports.revertClient = async ctx => {
|
|||
version: application.revertableVersion,
|
||||
revertableVersion: null,
|
||||
}
|
||||
const data = await updateAppPackage(ctx, appPackageUpdates, ctx.params.appId)
|
||||
const data = await updateAppPackage(appPackageUpdates, ctx.params.appId)
|
||||
ctx.status = 200
|
||||
ctx.body = data
|
||||
}
|
||||
|
@ -381,12 +410,11 @@ exports.sync = async (ctx, next) => {
|
|||
}
|
||||
}
|
||||
|
||||
const updateAppPackage = async (ctx, appPackage, appId) => {
|
||||
const url = await getAppUrlIfNotInUse(ctx)
|
||||
const updateAppPackage = async (appPackage, appId) => {
|
||||
const db = new CouchDB(appId)
|
||||
const application = await db.get(DocumentTypes.APP_METADATA)
|
||||
|
||||
const newAppPackage = { ...application, ...appPackage, url }
|
||||
const newAppPackage = { ...application, ...appPackage }
|
||||
if (appPackage._rev !== application._rev) {
|
||||
newAppPackage._rev = application._rev
|
||||
}
|
||||
|
|
|
@ -16,6 +16,8 @@ exports.fetchSelf = async ctx => {
|
|||
const user = await getFullUser(ctx, userId)
|
||||
// this shouldn't be returned by the app self
|
||||
delete user.roles
|
||||
// forward the csrf token from the session
|
||||
user.csrfToken = ctx.user.csrfToken
|
||||
|
||||
if (appId) {
|
||||
const db = new CouchDB(appId)
|
||||
|
@ -24,6 +26,8 @@ exports.fetchSelf = async ctx => {
|
|||
try {
|
||||
const userTable = await db.get(InternalTables.USER_METADATA)
|
||||
const metadata = await db.get(userId)
|
||||
// make sure there is never a stale csrf token
|
||||
delete metadata.csrfToken
|
||||
// specifically needs to make sure is enriched
|
||||
ctx.body = await outputProcessing(ctx, userTable, {
|
||||
...user,
|
||||
|
|
|
@ -38,6 +38,13 @@ exports.fetch = async function (ctx) {
|
|||
)
|
||||
).rows.map(row => row.doc)
|
||||
|
||||
for (let datasource of datasources) {
|
||||
if (datasource.config && datasource.config.auth) {
|
||||
// strip secrets from response so they don't show in the network request
|
||||
delete datasource.config.auth
|
||||
}
|
||||
}
|
||||
|
||||
ctx.body = [bbInternalDb, ...datasources]
|
||||
}
|
||||
|
||||
|
@ -94,8 +101,13 @@ exports.update = async function (ctx) {
|
|||
const db = new CouchDB(ctx.appId)
|
||||
const datasourceId = ctx.params.datasourceId
|
||||
let datasource = await db.get(datasourceId)
|
||||
const auth = datasource.config.auth
|
||||
await invalidateVariables(datasource, ctx.request.body)
|
||||
datasource = { ...datasource, ...ctx.request.body }
|
||||
if (auth && !ctx.request.body.auth) {
|
||||
// don't strip auth config from DB
|
||||
datasource.config.auth = auth
|
||||
}
|
||||
|
||||
const response = await db.put(datasource)
|
||||
datasource._rev = response.rev
|
||||
|
|
|
@ -1,22 +0,0 @@
|
|||
const CouchDB = require("../../db")
|
||||
const { getDeployedApps } = require("../../utilities/workerRequests")
|
||||
const { getScopedConfig } = require("@budibase/backend-core/db")
|
||||
const { Configs } = require("@budibase/backend-core/constants")
|
||||
const { checkSlashesInUrl } = require("../../utilities")
|
||||
|
||||
exports.fetchUrls = async ctx => {
|
||||
const appId = ctx.appId
|
||||
const db = new CouchDB(appId)
|
||||
const settings = await getScopedConfig(db, { type: Configs.SETTINGS })
|
||||
let appUrl = "http://localhost:10000/app"
|
||||
if (settings && settings["platformUrl"]) {
|
||||
appUrl = checkSlashesInUrl(`${settings["platformUrl"]}/app`)
|
||||
}
|
||||
ctx.body = {
|
||||
app: appUrl,
|
||||
}
|
||||
}
|
||||
|
||||
exports.getDeployedApps = async ctx => {
|
||||
ctx.body = await getDeployedApps()
|
||||
}
|
|
@ -141,6 +141,16 @@ async function execute(ctx, opts = { rowsOnly: false }) {
|
|||
const query = await db.get(ctx.params.queryId)
|
||||
const datasource = await db.get(query.datasourceId)
|
||||
|
||||
const enrichedParameters = ctx.request.body.parameters || {}
|
||||
// make sure parameters are fully enriched with defaults
|
||||
if (query && query.parameters) {
|
||||
for (let parameter of query.parameters) {
|
||||
if (!enrichedParameters[parameter.name]) {
|
||||
enrichedParameters[parameter.name] = parameter.default
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// call the relevant CRUD method on the integration class
|
||||
try {
|
||||
const { rows, pagination, extra } = await Runner.run({
|
||||
|
@ -149,7 +159,7 @@ async function execute(ctx, opts = { rowsOnly: false }) {
|
|||
queryVerb: query.queryVerb,
|
||||
fields: query.fields,
|
||||
pagination: ctx.request.body.pagination,
|
||||
parameters: ctx.request.body.parameters,
|
||||
parameters: enrichedParameters,
|
||||
transformer: query.transformer,
|
||||
queryId: ctx.params.queryId,
|
||||
})
|
||||
|
@ -178,8 +188,9 @@ const removeDynamicVariables = async (db, queryId) => {
|
|||
|
||||
if (dynamicVariables) {
|
||||
// delete dynamic variables from the datasource
|
||||
const newVariables = dynamicVariables.filter(dv => dv.queryId !== queryId)
|
||||
datasource.config.dynamicVariables = newVariables
|
||||
datasource.config.dynamicVariables = dynamicVariables.filter(
|
||||
dv => dv.queryId !== queryId
|
||||
)
|
||||
await db.put(datasource)
|
||||
|
||||
// invalidate the deleted variables
|
||||
|
|
|
@ -525,7 +525,7 @@ module External {
|
|||
const linkTable = this.getTable(tableId)
|
||||
// @ts-ignore
|
||||
const linkPrimary = linkTable.primary[0]
|
||||
const rows = related[key].rows || []
|
||||
const rows = related[key]?.rows || []
|
||||
const found = rows.find(
|
||||
(row: { [key: string]: any }) =>
|
||||
row[linkPrimary] === relationship.id ||
|
||||
|
|
|
@ -52,10 +52,7 @@ exports.validate = async ({ appId, tableId, row, table }) => {
|
|||
const constraints = cloneDeep(table.schema[fieldName].constraints)
|
||||
const type = table.schema[fieldName].type
|
||||
// special case for options, need to always allow unselected (null)
|
||||
if (
|
||||
(type === FieldTypes.OPTIONS || type === FieldTypes.ARRAY) &&
|
||||
constraints.inclusion
|
||||
) {
|
||||
if (type === FieldTypes.OPTIONS && constraints.inclusion) {
|
||||
constraints.inclusion.push(null)
|
||||
}
|
||||
let res
|
||||
|
|
|
@ -5,7 +5,7 @@ const { resolve, join } = require("../../../utilities/centralPath")
|
|||
const uuid = require("uuid")
|
||||
const { ObjectStoreBuckets } = require("../../../constants")
|
||||
const { processString } = require("@budibase/string-templates")
|
||||
const { getDeployedApps } = require("../../../utilities/workerRequests")
|
||||
const { getAllApps } = require("@budibase/backend-core/db")
|
||||
const CouchDB = require("../../../db")
|
||||
const {
|
||||
loadHandlebarsFile,
|
||||
|
@ -17,6 +17,8 @@ const { clientLibraryPath } = require("../../../utilities")
|
|||
const { upload } = require("../../../utilities/fileSystem")
|
||||
const { attachmentsRelativeURL } = require("../../../utilities")
|
||||
const { DocumentTypes } = require("../../../db/utils")
|
||||
const AWS = require("aws-sdk")
|
||||
const AWS_REGION = env.AWS_REGION ? env.AWS_REGION : "eu-west-1"
|
||||
|
||||
async function prepareUpload({ s3Key, bucket, metadata, file }) {
|
||||
const response = await upload({
|
||||
|
@ -37,12 +39,18 @@ async function prepareUpload({ s3Key, bucket, metadata, file }) {
|
|||
}
|
||||
}
|
||||
|
||||
async function checkForSelfHostedURL(ctx) {
|
||||
// the "appId" component of the URL may actually be a specific self hosted URL
|
||||
async function getAppIdFromUrl(ctx) {
|
||||
// the "appId" component of the URL can be the id or the custom url
|
||||
let possibleAppUrl = `/${encodeURI(ctx.params.appId).toLowerCase()}`
|
||||
const apps = await getDeployedApps()
|
||||
if (apps[possibleAppUrl] && apps[possibleAppUrl].appId) {
|
||||
return apps[possibleAppUrl].appId
|
||||
|
||||
// search prod apps for a url that matches, exclude dev where id is always used
|
||||
const apps = await getAllApps(CouchDB, { dev: false })
|
||||
const app = apps.filter(
|
||||
a => a.url && a.url.toLowerCase() === possibleAppUrl
|
||||
)[0]
|
||||
|
||||
if (app && app.appId) {
|
||||
return app.appId
|
||||
} else {
|
||||
return ctx.params.appId
|
||||
}
|
||||
|
@ -75,10 +83,7 @@ exports.uploadFile = async function (ctx) {
|
|||
}
|
||||
|
||||
exports.serveApp = async function (ctx) {
|
||||
let appId = ctx.params.appId
|
||||
if (env.SELF_HOSTED) {
|
||||
appId = await checkForSelfHostedURL(ctx)
|
||||
}
|
||||
let appId = await getAppIdFromUrl(ctx)
|
||||
const App = require("./templates/BudibaseApp.svelte").default
|
||||
const db = new CouchDB(appId, { skip_setup: true })
|
||||
const appInfo = await db.get(DocumentTypes.APP_METADATA)
|
||||
|
@ -104,3 +109,51 @@ exports.serveClientLibrary = async function (ctx) {
|
|||
root: join(NODE_MODULES_PATH, "@budibase", "client", "dist"),
|
||||
})
|
||||
}
|
||||
|
||||
exports.getSignedUploadURL = async function (ctx) {
|
||||
const database = new CouchDB(ctx.appId)
|
||||
|
||||
// Ensure datasource is valid
|
||||
let datasource
|
||||
try {
|
||||
const { datasourceId } = ctx.params
|
||||
datasource = await database.get(datasourceId)
|
||||
if (!datasource) {
|
||||
ctx.throw(400, "The specified datasource could not be found")
|
||||
}
|
||||
} catch (error) {
|
||||
ctx.throw(400, "The specified datasource could not be found")
|
||||
}
|
||||
|
||||
// Ensure we aren't using a custom endpoint
|
||||
if (datasource?.config?.endpoint) {
|
||||
ctx.throw(400, "S3 datasources with custom endpoints are not supported")
|
||||
}
|
||||
|
||||
// Determine type of datasource and generate signed URL
|
||||
let signedUrl
|
||||
let publicUrl
|
||||
if (datasource.source === "S3") {
|
||||
const { bucket, key } = ctx.request.body || {}
|
||||
if (!bucket || !key) {
|
||||
ctx.throw(400, "bucket and key values are required")
|
||||
return
|
||||
}
|
||||
try {
|
||||
const s3 = new AWS.S3({
|
||||
region: AWS_REGION,
|
||||
accessKeyId: datasource?.config?.accessKeyId,
|
||||
secretAccessKey: datasource?.config?.secretAccessKey,
|
||||
apiVersion: "2006-03-01",
|
||||
signatureVersion: "v4",
|
||||
})
|
||||
const params = { Bucket: bucket, Key: key }
|
||||
signedUrl = s3.getSignedUrl("putObject", params)
|
||||
publicUrl = `https://${bucket}.s3.${AWS_REGION}.amazonaws.com/${key}`
|
||||
} catch (error) {
|
||||
ctx.throw(400, error)
|
||||
}
|
||||
}
|
||||
|
||||
ctx.body = { signedUrl, publicUrl }
|
||||
}
|
||||
|
|
|
@ -13,7 +13,11 @@ const {
|
|||
inputProcessing,
|
||||
cleanupAttachments,
|
||||
} = require("../../../utilities/rowProcessor")
|
||||
const { USERS_TABLE_SCHEMA, SwitchableTypes } = require("../../../constants")
|
||||
const {
|
||||
USERS_TABLE_SCHEMA,
|
||||
SwitchableTypes,
|
||||
CanSwitchTypes,
|
||||
} = require("../../../constants")
|
||||
const {
|
||||
isExternalTable,
|
||||
breakExternalTableId,
|
||||
|
@ -388,6 +392,23 @@ exports.foreignKeyStructure = (keyName, meta = null) => {
|
|||
return structure
|
||||
}
|
||||
|
||||
exports.areSwitchableTypes = (type1, type2) => {
|
||||
if (
|
||||
SwitchableTypes.indexOf(type1) === -1 &&
|
||||
SwitchableTypes.indexOf(type2) === -1
|
||||
) {
|
||||
return false
|
||||
}
|
||||
for (let option of CanSwitchTypes) {
|
||||
const index1 = option.indexOf(type1),
|
||||
index2 = option.indexOf(type2)
|
||||
if (index1 !== -1 && index2 !== -1 && index1 !== index2) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
exports.hasTypeChanged = (table, oldTable) => {
|
||||
if (!oldTable) {
|
||||
return false
|
||||
|
@ -398,7 +419,7 @@ exports.hasTypeChanged = (table, oldTable) => {
|
|||
continue
|
||||
}
|
||||
const newType = table.schema[key].type
|
||||
if (oldType !== newType && SwitchableTypes.indexOf(oldType) === -1) {
|
||||
if (oldType !== newType && !exports.areSwitchableTypes(oldType, newType)) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
|
|
@ -167,6 +167,8 @@ exports.updateSelfMetadata = async function (ctx) {
|
|||
ctx.request.body._id = ctx.user._id
|
||||
// make sure no stale rev
|
||||
delete ctx.request.body._rev
|
||||
// make sure no csrf token
|
||||
delete ctx.request.body.csrfToken
|
||||
await exports.updateMetadata(ctx)
|
||||
}
|
||||
|
||||
|
|
|
@ -1,13 +0,0 @@
|
|||
const Router = require("@koa/router")
|
||||
const controller = require("../controllers/hosting")
|
||||
const authorized = require("../../middleware/authorized")
|
||||
const { BUILDER } = require("@budibase/backend-core/permissions")
|
||||
|
||||
const router = Router()
|
||||
|
||||
router
|
||||
.get("/api/hosting/urls", authorized(BUILDER), controller.fetchUrls)
|
||||
// this isn't risky, doesn't return anything about apps other than names and URLs
|
||||
.get("/api/hosting/apps", controller.getDeployedApps)
|
||||
|
||||
module.exports = router
|
|
@ -20,7 +20,6 @@ const integrationRoutes = require("./integration")
|
|||
const permissionRoutes = require("./permission")
|
||||
const datasourceRoutes = require("./datasource")
|
||||
const queryRoutes = require("./query")
|
||||
const hostingRoutes = require("./hosting")
|
||||
const backupRoutes = require("./backup")
|
||||
const metadataRoutes = require("./metadata")
|
||||
const devRoutes = require("./dev")
|
||||
|
@ -46,7 +45,6 @@ exports.mainRoutes = [
|
|||
permissionRoutes,
|
||||
datasourceRoutes,
|
||||
queryRoutes,
|
||||
hostingRoutes,
|
||||
backupRoutes,
|
||||
metadataRoutes,
|
||||
devRoutes,
|
||||
|
|
|
@ -46,5 +46,10 @@ router
|
|||
)
|
||||
// TODO: this likely needs to be secured in some way
|
||||
.get("/:appId/:path*", controller.serveApp)
|
||||
.post(
|
||||
"/api/attachments/:datasourceId/url",
|
||||
authorized(PermissionTypes.TABLE, PermissionLevels.READ),
|
||||
controller.getSignedUploadURL
|
||||
)
|
||||
|
||||
module.exports = router
|
||||
|
|
|
@ -53,8 +53,8 @@ describe("/applications", () => {
|
|||
|
||||
describe("fetch", () => {
|
||||
it("lists all applications", async () => {
|
||||
await config.createApp(request, "app1")
|
||||
await config.createApp(request, "app2")
|
||||
await config.createApp("app1")
|
||||
await config.createApp("app2")
|
||||
|
||||
const res = await request
|
||||
.get(`/api/applications?status=${AppStatus.DEV}`)
|
||||
|
|
|
@ -13,10 +13,9 @@ describe("/authenticate", () => {
|
|||
|
||||
describe("fetch self", () => {
|
||||
it("should be able to fetch self", async () => {
|
||||
const headers = await config.login()
|
||||
const res = await request
|
||||
.get(`/api/self`)
|
||||
.set(headers)
|
||||
.set(config.defaultHeaders())
|
||||
.expect("Content-Type", /json/)
|
||||
.expect(200)
|
||||
expect(res.body._id).toEqual(generateUserMetadataID("us_uuid1"))
|
||||
|
|
|
@ -1,36 +0,0 @@
|
|||
// mock out node fetch for this
|
||||
jest.mock("node-fetch")
|
||||
|
||||
const { checkBuilderEndpoint } = require("./utilities/TestFunctions")
|
||||
const setup = require("./utilities")
|
||||
|
||||
describe("/hosting", () => {
|
||||
let request = setup.getRequest()
|
||||
let config = setup.getConfig()
|
||||
let app
|
||||
|
||||
afterAll(setup.afterAll)
|
||||
|
||||
beforeEach(async () => {
|
||||
app = await config.init()
|
||||
})
|
||||
|
||||
describe("fetchUrls", () => {
|
||||
it("should be able to fetch current app URLs", async () => {
|
||||
const res = await request
|
||||
.get(`/api/hosting/urls`)
|
||||
.set(config.defaultHeaders())
|
||||
.expect("Content-Type", /json/)
|
||||
.expect(200)
|
||||
expect(res.body.app).toEqual(`http://localhost:10000/app`)
|
||||
})
|
||||
|
||||
it("should apply authorization to endpoint", async () => {
|
||||
await checkBuilderEndpoint({
|
||||
config,
|
||||
method: "GET",
|
||||
url: `/api/hosting/urls`,
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
|
@ -0,0 +1,98 @@
|
|||
jest.mock("node-fetch")
|
||||
jest.mock("aws-sdk", () => ({
|
||||
config: {
|
||||
update: jest.fn(),
|
||||
},
|
||||
DynamoDB: {
|
||||
DocumentClient: jest.fn(),
|
||||
},
|
||||
S3: jest.fn(() => ({
|
||||
getSignedUrl: jest.fn(() => {
|
||||
return "my-url"
|
||||
}),
|
||||
})),
|
||||
}))
|
||||
|
||||
const setup = require("./utilities")
|
||||
|
||||
describe("/attachments", () => {
|
||||
let request = setup.getRequest()
|
||||
let config = setup.getConfig()
|
||||
let app
|
||||
|
||||
afterAll(setup.afterAll)
|
||||
|
||||
beforeEach(async () => {
|
||||
app = await config.init()
|
||||
})
|
||||
|
||||
describe("generateSignedUrls", () => {
|
||||
let datasource
|
||||
|
||||
beforeEach(async () => {
|
||||
datasource = await config.createDatasource({
|
||||
datasource: {
|
||||
type: "datasource",
|
||||
name: "Test",
|
||||
source: "S3",
|
||||
config: {},
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it("should be able to generate a signed upload URL", async () => {
|
||||
const bucket = "foo"
|
||||
const key = "bar"
|
||||
const res = await request
|
||||
.post(`/api/attachments/${datasource._id}/url`)
|
||||
.send({ bucket, key })
|
||||
.set(config.defaultHeaders())
|
||||
.expect("Content-Type", /json/)
|
||||
.expect(200)
|
||||
expect(res.body.signedUrl).toEqual("my-url")
|
||||
expect(res.body.publicUrl).toEqual(
|
||||
`https://${bucket}.s3.eu-west-1.amazonaws.com/${key}`
|
||||
)
|
||||
})
|
||||
|
||||
it("should handle an invalid datasource ID", async () => {
|
||||
const res = await request
|
||||
.post(`/api/attachments/foo/url`)
|
||||
.send({
|
||||
bucket: "foo",
|
||||
key: "bar",
|
||||
})
|
||||
.set(config.defaultHeaders())
|
||||
.expect("Content-Type", /json/)
|
||||
.expect(400)
|
||||
expect(res.body.message).toEqual(
|
||||
"The specified datasource could not be found"
|
||||
)
|
||||
})
|
||||
|
||||
it("should require a bucket parameter", async () => {
|
||||
const res = await request
|
||||
.post(`/api/attachments/${datasource._id}/url`)
|
||||
.send({
|
||||
bucket: undefined,
|
||||
key: "bar",
|
||||
})
|
||||
.set(config.defaultHeaders())
|
||||
.expect("Content-Type", /json/)
|
||||
.expect(400)
|
||||
expect(res.body.message).toEqual("bucket and key values are required")
|
||||
})
|
||||
|
||||
it("should require a key parameter", async () => {
|
||||
const res = await request
|
||||
.post(`/api/attachments/${datasource._id}/url`)
|
||||
.send({
|
||||
bucket: "foo",
|
||||
})
|
||||
.set(config.defaultHeaders())
|
||||
.expect("Content-Type", /json/)
|
||||
.expect(400)
|
||||
expect(res.body.message).toEqual("bucket and key values are required")
|
||||
})
|
||||
})
|
||||
})
|
|
@ -45,13 +45,16 @@ exports.FieldTypes = {
|
|||
INTERNAL: "internal",
|
||||
}
|
||||
|
||||
exports.SwitchableTypes = [
|
||||
exports.FieldTypes.STRING,
|
||||
exports.FieldTypes.OPTIONS,
|
||||
exports.FieldTypes.NUMBER,
|
||||
exports.FieldTypes.BOOLEAN,
|
||||
exports.CanSwitchTypes = [
|
||||
[exports.FieldTypes.JSON, exports.FieldTypes.ARRAY],
|
||||
[exports.FieldTypes.STRING, exports.FieldTypes.OPTIONS],
|
||||
[exports.FieldTypes.BOOLEAN, exports.FieldTypes.NUMBER],
|
||||
]
|
||||
|
||||
exports.SwitchableTypes = exports.CanSwitchTypes.reduce((prev, current) =>
|
||||
prev ? prev.concat(current) : current
|
||||
)
|
||||
|
||||
exports.RelationshipTypes = {
|
||||
ONE_TO_MANY: "one-to-many",
|
||||
MANY_TO_ONE: "many-to-one",
|
||||
|
@ -80,6 +83,10 @@ exports.DataSourceOperation = {
|
|||
DELETE_TABLE: "DELETE_TABLE",
|
||||
}
|
||||
|
||||
exports.DatasourceAuthTypes = {
|
||||
GOOGLE: "google",
|
||||
}
|
||||
|
||||
exports.SortDirection = {
|
||||
ASCENDING: "ASCENDING",
|
||||
DESCENDING: "DESCENDING",
|
||||
|
|
|
@ -47,6 +47,7 @@ export enum SourceNames {
|
|||
ARANGODB = "ARANGODB",
|
||||
REST = "REST",
|
||||
ORACLE = "ORACLE",
|
||||
GOOGLE_SHEETS = "GOOGLE_SHEETS",
|
||||
}
|
||||
|
||||
export enum IncludeRelationships {
|
||||
|
@ -86,6 +87,8 @@ export interface ExtraQueryConfig {
|
|||
export interface Integration {
|
||||
docs: string
|
||||
plus?: boolean
|
||||
auth?: { type: string }
|
||||
relationships?: boolean
|
||||
description: string
|
||||
friendlyName: string
|
||||
datasource: {}
|
||||
|
|
|
@ -0,0 +1,353 @@
|
|||
import {
|
||||
DatasourceFieldTypes,
|
||||
Integration,
|
||||
QueryJson,
|
||||
QueryTypes,
|
||||
} from "../definitions/datasource"
|
||||
import { OAuth2Client } from "google-auth-library"
|
||||
import { DatasourcePlus } from "./base/datasourcePlus"
|
||||
import { Row, Table, TableSchema } from "../definitions/common"
|
||||
import { buildExternalTableId } from "./utils"
|
||||
import { DataSourceOperation, FieldTypes } from "../constants"
|
||||
import { GoogleSpreadsheet } from "google-spreadsheet"
|
||||
import { table } from "console"
|
||||
|
||||
module GoogleSheetsModule {
|
||||
const { getGlobalDB } = require("@budibase/backend-core/tenancy")
|
||||
const { getScopedConfig } = require("@budibase/backend-core/db")
|
||||
const { Configs } = require("@budibase/backend-core/constants")
|
||||
|
||||
interface GoogleSheetsConfig {
|
||||
spreadsheetId: string
|
||||
auth: OAuthClientConfig
|
||||
}
|
||||
|
||||
interface OAuthClientConfig {
|
||||
appId: string
|
||||
accessToken: string
|
||||
refreshToken: string
|
||||
}
|
||||
|
||||
const SCHEMA: Integration = {
|
||||
plus: true,
|
||||
auth: {
|
||||
type: "google",
|
||||
},
|
||||
relationships: false,
|
||||
docs: "https://developers.google.com/sheets/api/quickstart/nodejs",
|
||||
description:
|
||||
"Create and collaborate on online spreadsheets in real-time and from any device. ",
|
||||
friendlyName: "Google Sheets",
|
||||
datasource: {
|
||||
spreadsheetId: {
|
||||
type: DatasourceFieldTypes.STRING,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
query: {
|
||||
create: {
|
||||
type: QueryTypes.FIELDS,
|
||||
fields: {
|
||||
sheet: {
|
||||
type: DatasourceFieldTypes.STRING,
|
||||
required: true,
|
||||
},
|
||||
row: {
|
||||
type: QueryTypes.JSON,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
read: {
|
||||
type: QueryTypes.FIELDS,
|
||||
fields: {
|
||||
sheet: {
|
||||
type: DatasourceFieldTypes.STRING,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
update: {
|
||||
type: QueryTypes.FIELDS,
|
||||
fields: {
|
||||
sheet: {
|
||||
type: DatasourceFieldTypes.STRING,
|
||||
required: true,
|
||||
},
|
||||
rowIndex: {
|
||||
type: DatasourceFieldTypes.STRING,
|
||||
required: true,
|
||||
},
|
||||
row: {
|
||||
type: QueryTypes.JSON,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
delete: {
|
||||
type: QueryTypes.FIELDS,
|
||||
fields: {
|
||||
sheet: {
|
||||
type: DatasourceFieldTypes.STRING,
|
||||
required: true,
|
||||
},
|
||||
rowIndex: {
|
||||
type: DatasourceFieldTypes.NUMBER,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
class GoogleSheetsIntegration implements DatasourcePlus {
|
||||
private readonly config: GoogleSheetsConfig
|
||||
private client: any
|
||||
public tables: Record<string, Table> = {}
|
||||
public schemaErrors: Record<string, string> = {}
|
||||
|
||||
constructor(config: GoogleSheetsConfig) {
|
||||
this.config = config
|
||||
const spreadsheetId = this.cleanSpreadsheetUrl(this.config.spreadsheetId)
|
||||
this.client = new GoogleSpreadsheet(spreadsheetId)
|
||||
}
|
||||
|
||||
/**
|
||||
* Pull the spreadsheet ID out from a valid google sheets URL
|
||||
* @param spreadsheetId - the URL or standard spreadsheetId of the google sheet
|
||||
* @returns spreadsheet Id of the google sheet
|
||||
*/
|
||||
cleanSpreadsheetUrl(spreadsheetId: string) {
|
||||
if (!spreadsheetId) {
|
||||
throw new Error(
|
||||
"You must set a spreadsheet ID in your configuration to fetch tables."
|
||||
)
|
||||
}
|
||||
const parts = spreadsheetId.split("/")
|
||||
return parts.length > 5 ? parts[5] : spreadsheetId
|
||||
}
|
||||
|
||||
async connect() {
|
||||
try {
|
||||
// Initialise oAuth client
|
||||
const db = getGlobalDB()
|
||||
const googleConfig = await getScopedConfig(db, {
|
||||
type: Configs.GOOGLE,
|
||||
})
|
||||
const oauthClient = new OAuth2Client({
|
||||
clientId: googleConfig.clientID,
|
||||
clientSecret: googleConfig.clientSecret,
|
||||
})
|
||||
oauthClient.credentials.access_token = this.config.auth.accessToken
|
||||
oauthClient.credentials.refresh_token = this.config.auth.refreshToken
|
||||
this.client.useOAuth2Client(oauthClient)
|
||||
await this.client.loadInfo()
|
||||
} catch (err) {
|
||||
console.error("Error connecting to google sheets", err)
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
async buildSchema(datasourceId: string) {
|
||||
await this.connect()
|
||||
const sheets = await this.client.sheetsByIndex
|
||||
const tables: Record<string, Table> = {}
|
||||
for (let sheet of sheets) {
|
||||
// must fetch rows to determine schema
|
||||
await sheet.getRows()
|
||||
// build schema
|
||||
const schema: TableSchema = {}
|
||||
|
||||
// build schema from headers
|
||||
for (let header of sheet.headerValues) {
|
||||
schema[header] = {
|
||||
name: header,
|
||||
type: FieldTypes.STRING,
|
||||
}
|
||||
}
|
||||
|
||||
// create tables
|
||||
tables[sheet.title] = {
|
||||
_id: buildExternalTableId(datasourceId, sheet.title),
|
||||
name: sheet.title,
|
||||
primary: ["rowNumber"],
|
||||
schema,
|
||||
}
|
||||
}
|
||||
|
||||
this.tables = tables
|
||||
}
|
||||
|
||||
async query(json: QueryJson) {
|
||||
const sheet = json.endpoint.entityId
|
||||
|
||||
const handlers = {
|
||||
[DataSourceOperation.CREATE]: () =>
|
||||
this.create({ sheet, row: json.body }),
|
||||
[DataSourceOperation.READ]: () => this.read({ sheet }),
|
||||
[DataSourceOperation.UPDATE]: () =>
|
||||
this.update({
|
||||
// exclude the header row and zero index
|
||||
rowIndex: json.extra?.idFilter?.equal?.rowNumber - 2,
|
||||
sheet,
|
||||
row: json.body,
|
||||
}),
|
||||
[DataSourceOperation.DELETE]: () =>
|
||||
this.delete({
|
||||
// exclude the header row and zero index
|
||||
rowIndex: json.extra?.idFilter?.equal?.rowNumber - 2,
|
||||
sheet,
|
||||
}),
|
||||
[DataSourceOperation.CREATE_TABLE]: () =>
|
||||
this.createTable(json?.table?.name),
|
||||
[DataSourceOperation.UPDATE_TABLE]: () => this.updateTable(json.table),
|
||||
[DataSourceOperation.DELETE_TABLE]: () =>
|
||||
this.deleteTable(json?.table?.name),
|
||||
}
|
||||
|
||||
const internalQueryMethod = handlers[json.endpoint.operation]
|
||||
|
||||
return await internalQueryMethod()
|
||||
}
|
||||
|
||||
buildRowObject(headers: string[], values: string[], rowNumber: number) {
|
||||
const rowObject: { rowNumber: number; [key: string]: any } = { rowNumber }
|
||||
for (let i = 0; i < headers.length; i++) {
|
||||
rowObject._id = rowNumber
|
||||
rowObject[headers[i]] = values[i]
|
||||
}
|
||||
return rowObject
|
||||
}
|
||||
|
||||
async createTable(name?: string) {
|
||||
try {
|
||||
await this.connect()
|
||||
const sheet = await this.client.addSheet({ title: name })
|
||||
return sheet
|
||||
} catch (err) {
|
||||
console.error("Error creating new table in google sheets", err)
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
async updateTable(table?: any) {
|
||||
try {
|
||||
await this.connect()
|
||||
const sheet = await this.client.sheetsByTitle[table.name]
|
||||
await sheet.loadHeaderRow()
|
||||
|
||||
if (table._rename) {
|
||||
const headers = []
|
||||
for (let header of sheet.headerValues) {
|
||||
if (header === table._rename.old) {
|
||||
headers.push(table._rename.updated)
|
||||
} else {
|
||||
headers.push(header)
|
||||
}
|
||||
}
|
||||
await sheet.setHeaderRow(headers)
|
||||
} else {
|
||||
let newField = Object.keys(table.schema).find(
|
||||
key => !sheet.headerValues.includes(key)
|
||||
)
|
||||
await sheet.setHeaderRow([...sheet.headerValues, newField])
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Error updating table in google sheets", err)
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
async deleteTable(sheet: any) {
|
||||
try {
|
||||
await this.connect()
|
||||
const sheetToDelete = await this.client.sheetsByTitle[sheet]
|
||||
return await sheetToDelete.delete()
|
||||
} catch (err) {
|
||||
console.error("Error deleting table in google sheets", err)
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
async create(query: { sheet: string; row: any }) {
|
||||
try {
|
||||
await this.connect()
|
||||
const sheet = await this.client.sheetsByTitle[query.sheet]
|
||||
const rowToInsert =
|
||||
typeof query.row === "string" ? JSON.parse(query.row) : query.row
|
||||
const row = await sheet.addRow(rowToInsert)
|
||||
return [
|
||||
this.buildRowObject(sheet.headerValues, row._rawData, row._rowNumber),
|
||||
]
|
||||
} catch (err) {
|
||||
console.error("Error writing to google sheets", err)
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
async read(query: { sheet: string }) {
|
||||
try {
|
||||
await this.connect()
|
||||
const sheet = await this.client.sheetsByTitle[query.sheet]
|
||||
const rows = await sheet.getRows()
|
||||
const headerValues = sheet.headerValues
|
||||
const response = []
|
||||
for (let row of rows) {
|
||||
response.push(
|
||||
this.buildRowObject(headerValues, row._rawData, row._rowNumber)
|
||||
)
|
||||
}
|
||||
return response
|
||||
} catch (err) {
|
||||
console.error("Error reading from google sheets", err)
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
async update(query: { sheet: string; rowIndex: number; row: any }) {
|
||||
try {
|
||||
await this.connect()
|
||||
const sheet = await this.client.sheetsByTitle[query.sheet]
|
||||
const rows = await sheet.getRows()
|
||||
const row = rows[query.rowIndex]
|
||||
if (row) {
|
||||
const updateValues = query.row
|
||||
for (let key in updateValues) {
|
||||
row[key] = updateValues[key]
|
||||
}
|
||||
await row.save()
|
||||
return [
|
||||
this.buildRowObject(
|
||||
sheet.headerValues,
|
||||
row._rawData,
|
||||
row._rowNumber
|
||||
),
|
||||
]
|
||||
} else {
|
||||
throw new Error("Row does not exist.")
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Error reading from google sheets", err)
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
async delete(query: { sheet: string; rowIndex: number }) {
|
||||
await this.connect()
|
||||
const sheet = await this.client.sheetsByTitle[query.sheet]
|
||||
const rows = await sheet.getRows()
|
||||
const row = rows[query.rowIndex]
|
||||
if (row) {
|
||||
await row.delete()
|
||||
return [{ deleted: query.rowIndex }]
|
||||
} else {
|
||||
throw new Error("Row does not exist.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
schema: SCHEMA,
|
||||
integration: GoogleSheetsIntegration,
|
||||
}
|
||||
}
|
|
@ -9,6 +9,7 @@ const airtable = require("./airtable")
|
|||
const mysql = require("./mysql")
|
||||
const arangodb = require("./arangodb")
|
||||
const rest = require("./rest")
|
||||
const googlesheets = require("./googlesheets")
|
||||
const { SourceNames } = require("../definitions/datasource")
|
||||
|
||||
const DEFINITIONS = {
|
||||
|
@ -23,6 +24,7 @@ const DEFINITIONS = {
|
|||
[SourceNames.MYSQL]: mysql.schema,
|
||||
[SourceNames.ARANGODB]: arangodb.schema,
|
||||
[SourceNames.REST]: rest.schema,
|
||||
[SourceNames.GOOGLE_SHEETS]: googlesheets.schema,
|
||||
}
|
||||
|
||||
const INTEGRATIONS = {
|
||||
|
@ -37,6 +39,7 @@ const INTEGRATIONS = {
|
|||
[SourceNames.MYSQL]: mysql.integration,
|
||||
[SourceNames.ARANGODB]: arangodb.integration,
|
||||
[SourceNames.REST]: rest.integration,
|
||||
[SourceNames.GOOGLE_SHEETS]: googlesheets.integration,
|
||||
}
|
||||
|
||||
// optionally add oracle integration if the oracle binary can be installed
|
||||
|
|
|
@ -153,8 +153,15 @@ export function isIsoDateString(str: string) {
|
|||
* @param column The column to check, to see if it is a valid relationship.
|
||||
* @param tableIds The IDs of the tables which currently exist.
|
||||
*/
|
||||
function shouldCopyRelationship(column: { type: string, tableId?: string }, tableIds: [string]) {
|
||||
return column.type === FieldTypes.LINK && column.tableId && tableIds.includes(column.tableId)
|
||||
function shouldCopyRelationship(
|
||||
column: { type: string; tableId?: string },
|
||||
tableIds: [string]
|
||||
) {
|
||||
return (
|
||||
column.type === FieldTypes.LINK &&
|
||||
column.tableId &&
|
||||
tableIds.includes(column.tableId)
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -165,9 +172,16 @@ function shouldCopyRelationship(column: { type: string, tableId?: string }, tabl
|
|||
* @param column The column to check for options or boolean type.
|
||||
* @param fetchedColumn The fetched column to check for the type in the external database.
|
||||
*/
|
||||
function shouldCopySpecialColumn(column: { type: string }, fetchedColumn: { type: string } | undefined) {
|
||||
return column.type === FieldTypes.OPTIONS ||
|
||||
((!fetchedColumn || fetchedColumn.type === FieldTypes.NUMBER) && column.type === FieldTypes.BOOLEAN)
|
||||
function shouldCopySpecialColumn(
|
||||
column: { type: string },
|
||||
fetchedColumn: { type: string } | undefined
|
||||
) {
|
||||
return (
|
||||
column.type === FieldTypes.OPTIONS ||
|
||||
column.type === FieldTypes.ARRAY ||
|
||||
((!fetchedColumn || fetchedColumn.type === FieldTypes.NUMBER) &&
|
||||
column.type === FieldTypes.BOOLEAN)
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -9,11 +9,59 @@ const {
|
|||
} = require("@budibase/backend-core/permissions")
|
||||
const builderMiddleware = require("./builder")
|
||||
const { isWebhookEndpoint } = require("./utils")
|
||||
const { buildCsrfMiddleware } = require("@budibase/backend-core/auth")
|
||||
|
||||
function hasResource(ctx) {
|
||||
return ctx.resourceId != null
|
||||
}
|
||||
|
||||
const csrf = buildCsrfMiddleware()
|
||||
|
||||
/**
|
||||
* Apply authorization to the requested resource:
|
||||
* - If this is a builder resource the user must be a builder.
|
||||
* - Builders can access all resources.
|
||||
* - Otherwise the user must have the required role.
|
||||
*/
|
||||
const checkAuthorized = async (ctx, resourceRoles, permType, permLevel) => {
|
||||
// check if this is a builder api and the user is not a builder
|
||||
const isBuilder = ctx.user && ctx.user.builder && ctx.user.builder.global
|
||||
const isBuilderApi = permType === PermissionTypes.BUILDER
|
||||
if (isBuilderApi && !isBuilder) {
|
||||
return ctx.throw(403, "Not Authorized")
|
||||
}
|
||||
|
||||
// check for resource authorization
|
||||
if (!isBuilder) {
|
||||
await checkAuthorizedResource(ctx, resourceRoles, permType, permLevel)
|
||||
}
|
||||
}
|
||||
|
||||
const checkAuthorizedResource = async (
|
||||
ctx,
|
||||
resourceRoles,
|
||||
permType,
|
||||
permLevel
|
||||
) => {
|
||||
// get the user's roles
|
||||
const roleId = ctx.roleId || BUILTIN_ROLE_IDS.PUBLIC
|
||||
const userRoles = await getUserRoleHierarchy(ctx.appId, roleId, {
|
||||
idOnly: false,
|
||||
})
|
||||
const permError = "User does not have permission"
|
||||
// check if the user has the required role
|
||||
if (resourceRoles.length > 0) {
|
||||
// deny access if the user doesn't have the required resource role
|
||||
const found = userRoles.find(role => resourceRoles.indexOf(role._id) !== -1)
|
||||
if (!found) {
|
||||
ctx.throw(403, permError)
|
||||
}
|
||||
// fallback to the base permissions when no resource roles are found
|
||||
} else if (!doesHaveBasePermission(permType, permLevel, userRoles)) {
|
||||
ctx.throw(403, permError)
|
||||
}
|
||||
}
|
||||
|
||||
module.exports =
|
||||
(permType, permLevel = null) =>
|
||||
async (ctx, next) => {
|
||||
|
@ -31,40 +79,26 @@ module.exports =
|
|||
// to find API endpoints which are builder focused
|
||||
await builderMiddleware(ctx, permType)
|
||||
|
||||
const isAuthed = ctx.isAuthenticated
|
||||
// builders for now have permission to do anything
|
||||
let isBuilder = ctx.user && ctx.user.builder && ctx.user.builder.global
|
||||
const isBuilderApi = permType === PermissionTypes.BUILDER
|
||||
if (isBuilder) {
|
||||
// get the resource roles
|
||||
let resourceRoles = []
|
||||
if (ctx.appId && hasResource(ctx)) {
|
||||
resourceRoles = await getRequiredResourceRole(ctx.appId, permLevel, ctx)
|
||||
}
|
||||
|
||||
// if the resource is public, proceed
|
||||
const isPublicResource = resourceRoles.includes(BUILTIN_ROLE_IDS.PUBLIC)
|
||||
if (isPublicResource) {
|
||||
return next()
|
||||
} else if (isBuilderApi && !isBuilder) {
|
||||
return ctx.throw(403, "Not Authorized")
|
||||
}
|
||||
|
||||
// need to check this first, in-case public access, don't check authed until last
|
||||
const roleId = ctx.roleId || BUILTIN_ROLE_IDS.PUBLIC
|
||||
const hierarchy = await getUserRoleHierarchy(ctx.appId, roleId, {
|
||||
idOnly: false,
|
||||
})
|
||||
const permError = "User does not have permission"
|
||||
let possibleRoleIds = []
|
||||
if (hasResource(ctx)) {
|
||||
possibleRoleIds = await getRequiredResourceRole(ctx.appId, permLevel, ctx)
|
||||
}
|
||||
// check if we found a role, if not fallback to base permissions
|
||||
if (possibleRoleIds.length > 0) {
|
||||
const found = hierarchy.find(
|
||||
role => possibleRoleIds.indexOf(role._id) !== -1
|
||||
)
|
||||
return found ? next() : ctx.throw(403, permError)
|
||||
} else if (!doesHaveBasePermission(permType, permLevel, hierarchy)) {
|
||||
ctx.throw(403, permError)
|
||||
// check authenticated
|
||||
if (!ctx.isAuthenticated) {
|
||||
return ctx.throw(403, "Session not authenticated")
|
||||
}
|
||||
|
||||
// if they are not authed, then anything using the authorized middleware will fail
|
||||
if (!isAuthed) {
|
||||
ctx.throw(403, "Session not authenticated")
|
||||
}
|
||||
// check authorized
|
||||
await checkAuthorized(ctx, resourceRoles, permType, permLevel)
|
||||
|
||||
return next()
|
||||
// csrf protection
|
||||
return csrf(ctx, next)
|
||||
}
|
||||
|
|
|
@ -47,15 +47,6 @@ module.exports = async (ctx, next) => {
|
|||
(!ctx.user || !ctx.user.builder || !ctx.user.builder.global)
|
||||
) {
|
||||
clearCookie(ctx, Cookies.CurrentApp)
|
||||
// have to set the return url on the server side as client side is not available
|
||||
setCookie(ctx, ctx.url, Cookies.RETURN_URL, {
|
||||
// don't sign so the browser can easily read
|
||||
sign: false,
|
||||
// use the request domain to match how ui handles the return url cookie.
|
||||
// it's important we don't use the shared domain here as the builder
|
||||
// can't delete from it without awareness of the domain.
|
||||
requestDomain: true,
|
||||
})
|
||||
return ctx.redirect("/")
|
||||
}
|
||||
|
||||
|
|
|
@ -17,6 +17,7 @@ class TestConfiguration {
|
|||
this.middleware = authorizedMiddleware(role)
|
||||
this.next = jest.fn()
|
||||
this.throw = jest.fn()
|
||||
this.headers = {}
|
||||
this.ctx = {
|
||||
headers: {},
|
||||
request: {
|
||||
|
@ -25,7 +26,8 @@ class TestConfiguration {
|
|||
appId: "",
|
||||
auth: {},
|
||||
next: this.next,
|
||||
throw: this.throw
|
||||
throw: this.throw,
|
||||
get: (name) => this.headers[name],
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -46,7 +48,7 @@ class TestConfiguration {
|
|||
}
|
||||
|
||||
setAuthenticated(isAuthed) {
|
||||
this.ctx.auth = { authenticated: isAuthed }
|
||||
this.ctx.isAuthenticated = isAuthed
|
||||
}
|
||||
|
||||
setRequestUrl(url) {
|
||||
|
@ -107,7 +109,7 @@ describe("Authorization middleware", () => {
|
|||
expect(config.next).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it("throws if the user has only builder permissions", async () => {
|
||||
it("throws if the user does not have builder permissions", async () => {
|
||||
config.setEnvironment(false)
|
||||
config.setMiddlewareRequiredPermission(PermissionTypes.BUILDER)
|
||||
config.setUser({
|
||||
|
@ -133,7 +135,7 @@ describe("Authorization middleware", () => {
|
|||
expect(config.next).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it("throws if the user session is not authenticated after permission checks", async () => {
|
||||
it("throws if the user session is not authenticated", async () => {
|
||||
config.setUser({
|
||||
role: {
|
||||
_id: ""
|
||||
|
|
|
@ -27,7 +27,7 @@ describe("syncRows", () => {
|
|||
await config.createTable()
|
||||
await config.createRow()
|
||||
// app 2
|
||||
await config.createApp()
|
||||
await config.createApp("second-app")
|
||||
await config.createTable()
|
||||
await config.createRow()
|
||||
await config.createRow()
|
||||
|
|
|
@ -22,10 +22,12 @@ const { getGlobalDB } = require("@budibase/backend-core/tenancy")
|
|||
const { createASession } = require("@budibase/backend-core/sessions")
|
||||
const { user: userCache } = require("@budibase/backend-core/cache")
|
||||
const CouchDB = require("../../db")
|
||||
const newid = require("../../db/newid")
|
||||
core.init(CouchDB)
|
||||
|
||||
const GLOBAL_USER_ID = "us_uuid1"
|
||||
const EMAIL = "babs@babs.com"
|
||||
const CSRF_TOKEN = "e3727778-7af0-4226-b5eb-f43cbe60a306"
|
||||
|
||||
class TestConfiguration {
|
||||
constructor(openServer = true) {
|
||||
|
@ -85,7 +87,11 @@ class TestConfiguration {
|
|||
roles: roles || {},
|
||||
tenantId: TENANT_ID,
|
||||
}
|
||||
await createASession(id, { sessionId: "sessionid", tenantId: TENANT_ID })
|
||||
await createASession(id, {
|
||||
sessionId: "sessionid",
|
||||
tenantId: TENANT_ID,
|
||||
csrfToken: CSRF_TOKEN,
|
||||
})
|
||||
if (builder) {
|
||||
user.builder = { global: true }
|
||||
} else {
|
||||
|
@ -98,7 +104,8 @@ class TestConfiguration {
|
|||
}
|
||||
}
|
||||
|
||||
async init(appName = "test_application") {
|
||||
// use a new id as the name to avoid name collisions
|
||||
async init(appName = newid()) {
|
||||
await this.globalUser()
|
||||
return this.createApp(appName)
|
||||
}
|
||||
|
@ -131,6 +138,7 @@ class TestConfiguration {
|
|||
`${Cookies.Auth}=${authToken}`,
|
||||
`${Cookies.CurrentApp}=${appToken}`,
|
||||
],
|
||||
[Headers.CSRF_TOKEN]: CSRF_TOKEN,
|
||||
}
|
||||
if (this.appId) {
|
||||
headers[Headers.APP_ID] = this.appId
|
||||
|
@ -424,10 +432,6 @@ class TestConfiguration {
|
|||
roles: { [this.prodAppId]: roleId },
|
||||
})
|
||||
}
|
||||
await createASession(userId, {
|
||||
sessionId: "sessionid",
|
||||
tenantId: TENANT_ID,
|
||||
})
|
||||
// have to fake this
|
||||
const auth = {
|
||||
userId,
|
||||
|
|
|
@ -78,6 +78,11 @@ class QueryRunner {
|
|||
return this.execute()
|
||||
}
|
||||
|
||||
// check for undefined response
|
||||
if (!rows) {
|
||||
rows = []
|
||||
}
|
||||
|
||||
// needs to an array for next step
|
||||
if (!Array.isArray(rows)) {
|
||||
rows = [rows]
|
||||
|
|
|
@ -58,29 +58,6 @@ exports.sendSmtpEmail = async (to, from, subject, contents, automation) => {
|
|||
return response.json()
|
||||
}
|
||||
|
||||
exports.getDeployedApps = async () => {
|
||||
try {
|
||||
const response = await fetch(
|
||||
checkSlashesInUrl(env.WORKER_URL + `/api/apps`),
|
||||
request(null, {
|
||||
method: "GET",
|
||||
})
|
||||
)
|
||||
const json = await response.json()
|
||||
const apps = {}
|
||||
for (let [key, value] of Object.entries(json)) {
|
||||
if (value.url) {
|
||||
value.url = value.url.toLowerCase()
|
||||
apps[key.toLowerCase()] = value
|
||||
}
|
||||
}
|
||||
return apps
|
||||
} catch (err) {
|
||||
// error, cannot determine deployed apps, don't stop app creation - sort this later
|
||||
return {}
|
||||
}
|
||||
}
|
||||
|
||||
exports.getGlobalSelf = async (ctx, appId = null) => {
|
||||
const endpoint = `/api/global/users/self`
|
||||
const response = await fetch(
|
||||
|
|
|
@ -983,10 +983,10 @@
|
|||
resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39"
|
||||
integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==
|
||||
|
||||
"@budibase/backend-core@^1.0.27-alpha.13":
|
||||
version "1.0.27-alpha.13"
|
||||
resolved "https://registry.yarnpkg.com/@budibase/backend-core/-/backend-core-1.0.27-alpha.13.tgz#89f46e081eb7b342f483fd0eccd72c42b2b2fa6c"
|
||||
integrity sha512-NiasBvZ5wTpvANG9AjuO34DHMTqWQWSpabLcgwBY0tNG4ekh+wvSCPjCcUvN/bBpOzrVMQ8C4hmS4pvv342BhQ==
|
||||
"@budibase/backend-core@^1.0.46-alpha.5":
|
||||
version "1.0.47"
|
||||
resolved "https://registry.yarnpkg.com/@budibase/backend-core/-/backend-core-1.0.47.tgz#af1e501e20f8a648a40fe7d336b89e65f058c803"
|
||||
integrity sha512-nj+MC2j6WEH+6LEJhs+zMbnm4BRGCaX7kXvlyq7EXA9h6QOxrNkB/PNFqEumkMJGjorkZAQ/qe8MUEjcE26QBw==
|
||||
dependencies:
|
||||
"@techpass/passport-openidconnect" "^0.3.0"
|
||||
aws-sdk "^2.901.0"
|
||||
|
@ -1056,10 +1056,10 @@
|
|||
svelte-flatpickr "^3.2.3"
|
||||
svelte-portal "^1.0.0"
|
||||
|
||||
"@budibase/bbui@^1.0.35":
|
||||
version "1.0.35"
|
||||
resolved "https://registry.yarnpkg.com/@budibase/bbui/-/bbui-1.0.35.tgz#a51886886772257d31e2c6346dbec46fe0c9fd85"
|
||||
integrity sha512-8qeAzTujtO7uvhj+dMiyW4BTkQ7dC4xF1CNIwyuTnDwIeFDlXYgNb09VVRs3+nWcX2e2eC53EUs1RnLUoSlTsw==
|
||||
"@budibase/bbui@^1.0.47":
|
||||
version "1.0.47"
|
||||
resolved "https://registry.yarnpkg.com/@budibase/bbui/-/bbui-1.0.47.tgz#df2848b89f881fe603e7156855d6a6c31d4f58bf"
|
||||
integrity sha512-RRm/BgK5aSx2/vGjMGljw240/48Ksc3/h4yB1nhQj8Xx3fKhlGnWDvWNy+sakvA6+fJvEXuti8RoxHtQ6lXmqA==
|
||||
dependencies:
|
||||
"@adobe/spectrum-css-workflow-icons" "^1.2.1"
|
||||
"@spectrum-css/actionbutton" "^1.0.1"
|
||||
|
@ -1106,14 +1106,14 @@
|
|||
svelte-flatpickr "^3.2.3"
|
||||
svelte-portal "^1.0.0"
|
||||
|
||||
"@budibase/client@^1.0.27-alpha.13":
|
||||
version "1.0.35"
|
||||
resolved "https://registry.yarnpkg.com/@budibase/client/-/client-1.0.35.tgz#b832e7e7e35032fb35fe5492fbb721db1da15394"
|
||||
integrity sha512-maL3V29PQb9VjgnPZq44GSDZCuamAGp01bheUeJxEeskjQqZUdf8QC7Frf1mT+ZjgKJf3gU6qtFOxmWRbVzVbw==
|
||||
"@budibase/client@^1.0.46-alpha.5":
|
||||
version "1.0.47"
|
||||
resolved "https://registry.yarnpkg.com/@budibase/client/-/client-1.0.47.tgz#ce9e2fbd300e5dc389ea29a3a3347897f096c824"
|
||||
integrity sha512-jB/al8v+nY/VLc6sH5Jt9JzWONVo+24/cI95iXlZSV5xwiKIVGj4+2F5QjKZ0c9Gm7SrrfP2T571N+4XaXNCGg==
|
||||
dependencies:
|
||||
"@budibase/bbui" "^1.0.35"
|
||||
"@budibase/bbui" "^1.0.47"
|
||||
"@budibase/standard-components" "^0.9.139"
|
||||
"@budibase/string-templates" "^1.0.35"
|
||||
"@budibase/string-templates" "^1.0.47"
|
||||
regexparam "^1.3.0"
|
||||
shortid "^2.2.15"
|
||||
svelte-spa-router "^3.0.5"
|
||||
|
@ -1163,10 +1163,10 @@
|
|||
svelte-apexcharts "^1.0.2"
|
||||
svelte-flatpickr "^3.1.0"
|
||||
|
||||
"@budibase/string-templates@^1.0.27-alpha.13", "@budibase/string-templates@^1.0.35":
|
||||
version "1.0.35"
|
||||
resolved "https://registry.yarnpkg.com/@budibase/string-templates/-/string-templates-1.0.35.tgz#a888f1e9327bb36416336a91a95a43cb34e6a42d"
|
||||
integrity sha512-8HxSv0ru+cgSmphqtOm1pmBM8rc0TRC/6RQGzQefmFFQFfm/SBLAVLLWRmZxAOYTxt4mittGWeL4y05FqEuocg==
|
||||
"@budibase/string-templates@^1.0.46-alpha.5", "@budibase/string-templates@^1.0.47":
|
||||
version "1.0.47"
|
||||
resolved "https://registry.yarnpkg.com/@budibase/string-templates/-/string-templates-1.0.47.tgz#626b9fc4542c7b36a0ae24e820d25a704c527bec"
|
||||
integrity sha512-87BUfOPr8FGKH8Pt88jhKNGT9PcOmkLRCeen4xi1dI113pAQznBO9vgV+cXOChUBBEQka9Rrt85LMJXidiwVgg==
|
||||
dependencies:
|
||||
"@budibase/handlebars-helpers" "^0.11.7"
|
||||
dayjs "^1.10.4"
|
||||
|
@ -2417,6 +2417,11 @@
|
|||
dependencies:
|
||||
"@types/node" "*"
|
||||
|
||||
"@types/google-spreadsheet@^3.1.5":
|
||||
version "3.1.5"
|
||||
resolved "https://registry.yarnpkg.com/@types/google-spreadsheet/-/google-spreadsheet-3.1.5.tgz#2bdc6f9f5372551e0506cb6ef3f562adcf44fc2e"
|
||||
integrity sha512-7N+mDtZ1pmya2RRFPPl4KYc2TRgiqCNBLUZfyrKfER+u751JgCO+C24/LzF70UmUm/zhHUbzRZ5mtfaxekQ1ZQ==
|
||||
|
||||
"@types/graceful-fs@^4.1.2":
|
||||
version "4.1.5"
|
||||
resolved "https://registry.yarnpkg.com/@types/graceful-fs/-/graceful-fs-4.1.5.tgz#21ffba0d98da4350db64891f92a9e5db3cdb4e15"
|
||||
|
@ -3193,6 +3198,11 @@ array-unique@^0.3.2:
|
|||
resolved "https://registry.yarnpkg.com/array-unique/-/array-unique-0.3.2.tgz#a894b75d4bc4f6cd679ef3244a9fd8f46ae2d428"
|
||||
integrity sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=
|
||||
|
||||
arrify@^2.0.0:
|
||||
version "2.0.1"
|
||||
resolved "https://registry.yarnpkg.com/arrify/-/arrify-2.0.1.tgz#c9655e9331e0abcd588d2a7cad7e9956f66701fa"
|
||||
integrity sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug==
|
||||
|
||||
asap@^2.0.3:
|
||||
version "2.0.6"
|
||||
resolved "https://registry.yarnpkg.com/asap/-/asap-2.0.6.tgz#e50347611d7e690943208bbdafebcbc2fb866d46"
|
||||
|
@ -3495,7 +3505,7 @@ base62@^1.1.0:
|
|||
resolved "https://registry.yarnpkg.com/base62/-/base62-1.2.8.tgz#1264cb0fb848d875792877479dbe8bae6bae3428"
|
||||
integrity sha512-V6YHUbjLxN1ymqNLb1DPHoU1CpfdL7d2YTIp5W3U4hhoG4hhxNmsFDs66M9EXxBiSEke5Bt5dwdfMwwZF70iLA==
|
||||
|
||||
base64-js@^1.0.2, base64-js@^1.3.1:
|
||||
base64-js@^1.0.2, base64-js@^1.3.0, base64-js@^1.3.1:
|
||||
version "1.5.1"
|
||||
resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a"
|
||||
integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==
|
||||
|
@ -3535,6 +3545,11 @@ big.js@^5.2.2:
|
|||
resolved "https://registry.yarnpkg.com/big.js/-/big.js-5.2.2.tgz#65f0af382f578bcdc742bd9c281e9cb2d7768328"
|
||||
integrity sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==
|
||||
|
||||
bignumber.js@^9.0.0:
|
||||
version "9.0.1"
|
||||
resolved "https://registry.yarnpkg.com/bignumber.js/-/bignumber.js-9.0.1.tgz#8d7ba124c882bfd8e43260c67475518d0689e4e5"
|
||||
integrity sha512-IdZR9mh6ahOBv/hYGiXyVuyCetmGJhtYkqLBpTStdhEGjegpPlUawydyaF3pbIOFynJTpllEs+NP+CS9jKFLjA==
|
||||
|
||||
binary-extensions@^2.0.0:
|
||||
version "2.2.0"
|
||||
resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.2.0.tgz#75f502eeaf9ffde42fc98829645be4ea76bd9e2d"
|
||||
|
@ -4826,7 +4841,7 @@ ecc-jsbn@~0.1.1:
|
|||
jsbn "~0.1.0"
|
||||
safer-buffer "^2.1.0"
|
||||
|
||||
ecdsa-sig-formatter@1.0.11:
|
||||
ecdsa-sig-formatter@1.0.11, ecdsa-sig-formatter@^1.0.11:
|
||||
version "1.0.11"
|
||||
resolved "https://registry.yarnpkg.com/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz#ae0f0fa2d85045ef14a817daa3ce9acd0489e5bf"
|
||||
integrity sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==
|
||||
|
@ -5490,7 +5505,7 @@ extend-shallow@^3.0.0, extend-shallow@^3.0.2:
|
|||
assign-symbols "^1.0.0"
|
||||
is-extendable "^1.0.1"
|
||||
|
||||
extend@^3.0.0, extend@~3.0.2:
|
||||
extend@^3.0.0, extend@^3.0.2, extend@~3.0.2:
|
||||
version "3.0.2"
|
||||
resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa"
|
||||
integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==
|
||||
|
@ -5569,6 +5584,11 @@ fast-safe-stringify@^2.0.7, fast-safe-stringify@^2.0.8:
|
|||
resolved "https://registry.yarnpkg.com/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz#c406a83b6e70d9e35ce3b30a81141df30aeba884"
|
||||
integrity sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==
|
||||
|
||||
fast-text-encoding@^1.0.0:
|
||||
version "1.0.3"
|
||||
resolved "https://registry.yarnpkg.com/fast-text-encoding/-/fast-text-encoding-1.0.3.tgz#ec02ac8e01ab8a319af182dae2681213cfe9ce53"
|
||||
integrity sha512-dtm4QZH9nZtcDt8qJiOH9fcQd1NAgi+K1O2DbE6GG1PPCK/BWfOH3idCTRQ4ImXRUOyopDEgDEnVEE7Y/2Wrig==
|
||||
|
||||
fast-url-parser@^1.1.3:
|
||||
version "1.1.3"
|
||||
resolved "https://registry.yarnpkg.com/fast-url-parser/-/fast-url-parser-1.1.3.tgz#f4af3ea9f34d8a271cf58ad2b3759f431f0b318d"
|
||||
|
@ -5919,6 +5939,25 @@ functional-red-black-tree@^1.0.1:
|
|||
resolved "https://registry.yarnpkg.com/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz#1b0ab3bd553b2a0d6399d29c0e3ea0b252078327"
|
||||
integrity sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=
|
||||
|
||||
gaxios@^4.0.0:
|
||||
version "4.3.2"
|
||||
resolved "https://registry.yarnpkg.com/gaxios/-/gaxios-4.3.2.tgz#845827c2dc25a0213c8ab4155c7a28910f5be83f"
|
||||
integrity sha512-T+ap6GM6UZ0c4E6yb1y/hy2UB6hTrqhglp3XfmU9qbLCGRYhLVV5aRPpC4EmoG8N8zOnkYCgoBz+ScvGAARY6Q==
|
||||
dependencies:
|
||||
abort-controller "^3.0.0"
|
||||
extend "^3.0.2"
|
||||
https-proxy-agent "^5.0.0"
|
||||
is-stream "^2.0.0"
|
||||
node-fetch "^2.6.1"
|
||||
|
||||
gcp-metadata@^4.2.0:
|
||||
version "4.3.1"
|
||||
resolved "https://registry.yarnpkg.com/gcp-metadata/-/gcp-metadata-4.3.1.tgz#fb205fe6a90fef2fd9c85e6ba06e5559ee1eefa9"
|
||||
integrity sha512-x850LS5N7V1F3UcV7PoupzGsyD6iVwTVvsh3tbXfkctZnBnjW5yu5z1/3k3SehF7TyoTIe78rJs02GMMy+LF+A==
|
||||
dependencies:
|
||||
gaxios "^4.0.0"
|
||||
json-bigint "^1.0.0"
|
||||
|
||||
generate-function@^2.3.1:
|
||||
version "2.3.1"
|
||||
resolved "https://registry.yarnpkg.com/generate-function/-/generate-function-2.3.1.tgz#f069617690c10c868e73b8465746764f97c3479f"
|
||||
|
@ -6116,6 +6155,36 @@ globby@^11.0.3:
|
|||
merge2 "^1.3.0"
|
||||
slash "^3.0.0"
|
||||
|
||||
google-auth-library@^6.1.3:
|
||||
version "6.1.6"
|
||||
resolved "https://registry.yarnpkg.com/google-auth-library/-/google-auth-library-6.1.6.tgz#deacdcdb883d9ed6bac78bb5d79a078877fdf572"
|
||||
integrity sha512-Q+ZjUEvLQj/lrVHF/IQwRo6p3s8Nc44Zk/DALsN+ac3T4HY/g/3rrufkgtl+nZ1TW7DNAw5cTChdVp4apUXVgQ==
|
||||
dependencies:
|
||||
arrify "^2.0.0"
|
||||
base64-js "^1.3.0"
|
||||
ecdsa-sig-formatter "^1.0.11"
|
||||
fast-text-encoding "^1.0.0"
|
||||
gaxios "^4.0.0"
|
||||
gcp-metadata "^4.2.0"
|
||||
gtoken "^5.0.4"
|
||||
jws "^4.0.0"
|
||||
lru-cache "^6.0.0"
|
||||
|
||||
google-auth-library@^7.11.0:
|
||||
version "7.11.0"
|
||||
resolved "https://registry.yarnpkg.com/google-auth-library/-/google-auth-library-7.11.0.tgz#b63699c65037310a424128a854ba7e736704cbdb"
|
||||
integrity sha512-3S5jn2quRumvh9F/Ubf7GFrIq71HZ5a6vqosgdIu105kkk0WtSqc2jGCRqtWWOLRS8SX3AHACMOEDxhyWAQIcg==
|
||||
dependencies:
|
||||
arrify "^2.0.0"
|
||||
base64-js "^1.3.0"
|
||||
ecdsa-sig-formatter "^1.0.11"
|
||||
fast-text-encoding "^1.0.0"
|
||||
gaxios "^4.0.0"
|
||||
gcp-metadata "^4.2.0"
|
||||
gtoken "^5.0.4"
|
||||
jws "^4.0.0"
|
||||
lru-cache "^6.0.0"
|
||||
|
||||
google-auth-library@~0.10.0:
|
||||
version "0.10.0"
|
||||
resolved "https://registry.yarnpkg.com/google-auth-library/-/google-auth-library-0.10.0.tgz#6e15babee85fd1dd14d8d128a295b6838d52136e"
|
||||
|
@ -6133,6 +6202,22 @@ google-p12-pem@^0.1.0:
|
|||
dependencies:
|
||||
node-forge "^0.7.1"
|
||||
|
||||
google-p12-pem@^3.0.3:
|
||||
version "3.1.2"
|
||||
resolved "https://registry.yarnpkg.com/google-p12-pem/-/google-p12-pem-3.1.2.tgz#c3d61c2da8e10843ff830fdb0d2059046238c1d4"
|
||||
integrity sha512-tjf3IQIt7tWCDsa0ofDQ1qqSCNzahXDxdAGJDbruWqu3eCg5CKLYKN+hi0s6lfvzYZ1GDVr+oDF9OOWlDSdf0A==
|
||||
dependencies:
|
||||
node-forge "^0.10.0"
|
||||
|
||||
google-spreadsheet@^3.2.0:
|
||||
version "3.2.0"
|
||||
resolved "https://registry.yarnpkg.com/google-spreadsheet/-/google-spreadsheet-3.2.0.tgz#ce8aa75c15705aa950ad52b091a6fc4d33dcb329"
|
||||
integrity sha512-z7XMaqb+26rdo8p51r5O03u8aPLAPzn5YhOXYJPcf2hdMVr0dUbIARgdkRdmGiBeoV/QoU/7VNhq1MMCLZv3kQ==
|
||||
dependencies:
|
||||
axios "^0.21.4"
|
||||
google-auth-library "^6.1.3"
|
||||
lodash "^4.17.21"
|
||||
|
||||
googleapis@^16.0.0:
|
||||
version "16.1.0"
|
||||
resolved "https://registry.yarnpkg.com/googleapis/-/googleapis-16.1.0.tgz#0f19f2d70572d918881a0f626e3b1a2fa8629576"
|
||||
|
@ -6197,6 +6282,15 @@ gtoken@^1.2.1:
|
|||
mime "^1.4.1"
|
||||
request "^2.72.0"
|
||||
|
||||
gtoken@^5.0.4:
|
||||
version "5.3.1"
|
||||
resolved "https://registry.yarnpkg.com/gtoken/-/gtoken-5.3.1.tgz#c1c2598a826f2b5df7c6bb53d7be6cf6d50c3c78"
|
||||
integrity sha512-yqOREjzLHcbzz1UrQoxhBtpk8KjrVhuqPE7od1K2uhyxG2BHjKZetlbLw/SPZak/QqTIQW+addS+EcjqQsZbwQ==
|
||||
dependencies:
|
||||
gaxios "^4.0.0"
|
||||
google-p12-pem "^3.0.3"
|
||||
jws "^4.0.0"
|
||||
|
||||
gulp-header@^1.7.1:
|
||||
version "1.8.12"
|
||||
resolved "https://registry.yarnpkg.com/gulp-header/-/gulp-header-1.8.12.tgz#ad306be0066599127281c4f8786660e705080a84"
|
||||
|
@ -8084,6 +8178,13 @@ jsesc@~0.5.0:
|
|||
resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-0.5.0.tgz#e7dee66e35d6fc16f710fe91d5cf69f70f08911d"
|
||||
integrity sha1-597mbjXW/Bb3EP6R1c9p9w8IkR0=
|
||||
|
||||
json-bigint@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/json-bigint/-/json-bigint-1.0.0.tgz#ae547823ac0cad8398667f8cd9ef4730f5b01ff1"
|
||||
integrity sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==
|
||||
dependencies:
|
||||
bignumber.js "^9.0.0"
|
||||
|
||||
json-buffer@3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/json-buffer/-/json-buffer-3.0.0.tgz#5b1f397afc75d677bde8bcfc0e47e1f9a3d9a898"
|
||||
|
@ -8197,6 +8298,15 @@ jwa@^1.4.1:
|
|||
ecdsa-sig-formatter "1.0.11"
|
||||
safe-buffer "^5.0.1"
|
||||
|
||||
jwa@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/jwa/-/jwa-2.0.0.tgz#a7e9c3f29dae94027ebcaf49975c9345593410fc"
|
||||
integrity sha512-jrZ2Qx916EA+fq9cEAeCROWPTfCwi1IVHqT2tapuqLEVVDKFDENFw1oL+MwrTvH6msKxsd1YTDVw6uKEcsrLEA==
|
||||
dependencies:
|
||||
buffer-equal-constant-time "1.0.1"
|
||||
ecdsa-sig-formatter "1.0.11"
|
||||
safe-buffer "^5.0.1"
|
||||
|
||||
jws@3.x.x, jws@^3.0.0, jws@^3.1.4, jws@^3.2.2:
|
||||
version "3.2.2"
|
||||
resolved "https://registry.yarnpkg.com/jws/-/jws-3.2.2.tgz#001099f3639468c9414000e99995fa52fb478304"
|
||||
|
@ -8205,6 +8315,14 @@ jws@3.x.x, jws@^3.0.0, jws@^3.1.4, jws@^3.2.2:
|
|||
jwa "^1.4.1"
|
||||
safe-buffer "^5.0.1"
|
||||
|
||||
jws@^4.0.0:
|
||||
version "4.0.0"
|
||||
resolved "https://registry.yarnpkg.com/jws/-/jws-4.0.0.tgz#2d4e8cf6a318ffaa12615e9dec7e86e6c97310f4"
|
||||
integrity sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==
|
||||
dependencies:
|
||||
jwa "^2.0.0"
|
||||
safe-buffer "^5.0.1"
|
||||
|
||||
keygrip@~1.0.3:
|
||||
version "1.0.3"
|
||||
resolved "https://registry.yarnpkg.com/keygrip/-/keygrip-1.0.3.tgz#399d709f0aed2bab0a059e0cdd3a5023a053e1dc"
|
||||
|
@ -9313,18 +9431,23 @@ node-fetch@2.4.1:
|
|||
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.4.1.tgz#b2e38f1117b8acbedbe0524f041fb3177188255d"
|
||||
integrity sha512-P9UbpFK87NyqBZzUuDBDz4f6Yiys8xm8j7ACDbi6usvFm6KItklQUKjeoqTrYS/S1k6I8oaOC2YLLDr/gg26Mw==
|
||||
|
||||
node-fetch@2.6.0, node-fetch@^2.6.0:
|
||||
node-fetch@2.6.0:
|
||||
version "2.6.0"
|
||||
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.0.tgz#e633456386d4aa55863f676a7ab0daa8fdecb0fd"
|
||||
integrity sha512-8dG4H5ujfvFiqDmVu9fQ5bOHUC15JMjMY/Zumv26oOvvVJjM67KF8koCWIabKQ1GJIa9r2mMZscBq/TbdOcmNA==
|
||||
|
||||
node-fetch@^2.6.1:
|
||||
version "2.6.6"
|
||||
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.6.tgz#1751a7c01834e8e1697758732e9efb6eeadfaf89"
|
||||
integrity sha512-Z8/6vRlTUChSdIgMa51jxQ4lrw/Jy5SOW10ObaA47/RElsAN2c5Pn8bTgFGWn/ibwzXTE8qwr1Yzx28vsecXEA==
|
||||
node-fetch@2.6.7, node-fetch@^2.6.0, node-fetch@^2.6.1:
|
||||
version "2.6.7"
|
||||
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.7.tgz#24de9fba827e3b4ae44dc8b20256a379160052ad"
|
||||
integrity sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==
|
||||
dependencies:
|
||||
whatwg-url "^5.0.0"
|
||||
|
||||
node-forge@^0.10.0:
|
||||
version "0.10.0"
|
||||
resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-0.10.0.tgz#32dea2afb3e9926f02ee5ce8794902691a676bf3"
|
||||
integrity sha512-PPmu8eEeG9saEUvI97fm4OYxXVB6bFvyNTyiUOBichBpFG8A1Ljw3bY62+5oOjDEMHRnd0Y7HQ+x7uzxOzC6JA==
|
||||
|
||||
node-forge@^0.7.1:
|
||||
version "0.7.6"
|
||||
resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-0.7.6.tgz#fdf3b418aee1f94f0ef642cd63486c77ca9724ac"
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@budibase/string-templates",
|
||||
"version": "1.0.46-alpha.3",
|
||||
"version": "1.0.49-alpha.0",
|
||||
"description": "Handlebars wrapper for Budibase templating.",
|
||||
"main": "src/index.cjs",
|
||||
"module": "dist/bundle.mjs",
|
||||
|
|
|
@ -113,9 +113,10 @@ module.exports.processStringSync = (string, context, opts) => {
|
|||
const template = instance.compile(string, {
|
||||
strict: false,
|
||||
})
|
||||
const now = Math.floor(Date.now() / 1000) * 1000
|
||||
return processors.postprocess(
|
||||
template({
|
||||
now: new Date().toISOString(),
|
||||
now: new Date(now).toISOString(),
|
||||
...context,
|
||||
})
|
||||
)
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
{
|
||||
"watch": ["src", "../auth"]
|
||||
"watch": ["src", "../backend-core"]
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue