Merge pull request #8704 from Budibase/feature/replace-pouch

PouchDB replacement and context simplification
This commit is contained in:
Michael Drury 2022-11-17 16:45:59 +00:00 committed by GitHub
commit bb86afe743
141 changed files with 2639 additions and 2823 deletions

View File

@ -1,7 +1 @@
module.exports = { module.exports = require("./src/db")
...require("./src/db/utils"),
...require("./src/db/constants"),
...require("./src/db"),
...require("./src/db/views"),
...require("./src/db/pouch"),
}

View File

@ -12,6 +12,7 @@ if (!process.env.CI) {
// use sources when not in CI // use sources when not in CI
config.moduleNameMapper = { config.moduleNameMapper = {
"@budibase/types": "<rootDir>/../types/src", "@budibase/types": "<rootDir>/../types/src",
"^axios.*$": "<rootDir>/node_modules/axios/lib/axios.js",
} }
} else { } else {
console.log("Running tests with compiled dependency sources") console.log("Running tests with compiled dependency sources")

View File

@ -35,6 +35,7 @@
"koa-passport": "4.1.4", "koa-passport": "4.1.4",
"lodash": "4.17.21", "lodash": "4.17.21",
"lodash.isarguments": "3.1.0", "lodash.isarguments": "3.1.0",
"nano": "^10.1.0",
"node-fetch": "2.6.7", "node-fetch": "2.6.7",
"passport-google-auth": "1.0.2", "passport-google-auth": "1.0.2",
"passport-google-oauth": "2.0.0", "passport-google-oauth": "2.0.0",

View File

@ -3,7 +3,7 @@ const LocalStrategy = require("passport-local").Strategy
const JwtStrategy = require("passport-jwt").Strategy const JwtStrategy = require("passport-jwt").Strategy
import { getGlobalDB } from "./tenancy" import { getGlobalDB } from "./tenancy"
const refresh = require("passport-oauth2-refresh") const refresh = require("passport-oauth2-refresh")
import { Configs } from "./constants" import { Config } from "./constants"
import { getScopedConfig } from "./db/utils" import { getScopedConfig } from "./db/utils"
import { import {
jwt, jwt,
@ -76,7 +76,7 @@ async function refreshOIDCAccessToken(
return new Promise(resolve => { return new Promise(resolve => {
refresh.requestNewAccessToken( refresh.requestNewAccessToken(
Configs.OIDC, Config.OIDC,
refreshToken, refreshToken,
(err: any, accessToken: string, refreshToken: any, params: any) => { (err: any, accessToken: string, refreshToken: any, params: any) => {
resolve({ err, accessToken, refreshToken, params }) resolve({ err, accessToken, refreshToken, params })
@ -106,7 +106,7 @@ async function refreshGoogleAccessToken(
return new Promise(resolve => { return new Promise(resolve => {
refresh.requestNewAccessToken( refresh.requestNewAccessToken(
Configs.GOOGLE, Config.GOOGLE,
refreshToken, refreshToken,
(err: any, accessToken: string, refreshToken: string, params: any) => { (err: any, accessToken: string, refreshToken: string, params: any) => {
resolve({ err, accessToken, refreshToken, params }) resolve({ err, accessToken, refreshToken, params })
@ -129,7 +129,7 @@ async function refreshOAuthToken(
let chosenConfig = {} let chosenConfig = {}
let refreshResponse let refreshResponse
if (configType === Configs.OIDC) { if (configType === Config.OIDC) {
// configId - retrieved from cookie. // configId - retrieved from cookie.
chosenConfig = config.configs.filter((c: any) => c.uuid === configId)[0] chosenConfig = config.configs.filter((c: any) => c.uuid === configId)[0]
if (!chosenConfig) { if (!chosenConfig) {

View File

@ -1,6 +1,6 @@
require("../../../tests") require("../../../tests")
const { Writethrough } = require("../writethrough") const { Writethrough } = require("../writethrough")
const { dangerousGetDB } = require("../../db") const { getDB } = require("../../db")
const tk = require("timekeeper") const tk = require("timekeeper")
const START_DATE = Date.now() const START_DATE = Date.now()
@ -8,8 +8,8 @@ tk.freeze(START_DATE)
const DELAY = 5000 const DELAY = 5000
const db = dangerousGetDB("test") const db = getDB("test")
const db2 = dangerousGetDB("test2") const db2 = getDB("test2")
const writethrough = new Writethrough(db, DELAY), writethrough2 = new Writethrough(db2, DELAY) const writethrough = new Writethrough(db, DELAY), writethrough2 = new Writethrough(db2, DELAY)
describe("writethrough", () => { describe("writethrough", () => {

View File

@ -1,7 +1,7 @@
import BaseCache from "./base" import BaseCache from "./base"
import { getWritethroughClient } from "../redis/init" import { getWritethroughClient } from "../redis/init"
import { logWarn } from "../logging" import { logWarn } from "../logging"
import PouchDB from "pouchdb" import { Database } from "@budibase/types"
const DEFAULT_WRITE_RATE_MS = 10000 const DEFAULT_WRITE_RATE_MS = 10000
let CACHE: BaseCache | null = null let CACHE: BaseCache | null = null
@ -19,7 +19,7 @@ async function getCache() {
return CACHE return CACHE
} }
function makeCacheKey(db: PouchDB.Database, key: string) { function makeCacheKey(db: Database, key: string) {
return db.name + key return db.name + key
} }
@ -28,7 +28,7 @@ function makeCacheItem(doc: any, lastWrite: number | null = null): CacheItem {
} }
export async function put( export async function put(
db: PouchDB.Database, db: Database,
doc: any, doc: any,
writeRateMs: number = DEFAULT_WRITE_RATE_MS writeRateMs: number = DEFAULT_WRITE_RATE_MS
) { ) {
@ -64,7 +64,7 @@ export async function put(
return { ok: true, id: output._id, rev: output._rev } return { ok: true, id: output._id, rev: output._rev }
} }
export async function get(db: PouchDB.Database, id: string): Promise<any> { export async function get(db: Database, id: string): Promise<any> {
const cache = await getCache() const cache = await getCache()
const cacheKey = makeCacheKey(db, id) const cacheKey = makeCacheKey(db, id)
let cacheItem: CacheItem = await cache.get(cacheKey) let cacheItem: CacheItem = await cache.get(cacheKey)
@ -77,7 +77,7 @@ export async function get(db: PouchDB.Database, id: string): Promise<any> {
} }
export async function remove( export async function remove(
db: PouchDB.Database, db: Database,
docOrId: any, docOrId: any,
rev?: any rev?: any
): Promise<void> { ): Promise<void> {
@ -95,13 +95,10 @@ export async function remove(
} }
export class Writethrough { export class Writethrough {
db: PouchDB.Database db: Database
writeRateMs: number writeRateMs: number
constructor( constructor(db: Database, writeRateMs: number = DEFAULT_WRITE_RATE_MS) {
db: PouchDB.Database,
writeRateMs: number = DEFAULT_WRITE_RATE_MS
) {
this.db = db this.db = db
this.writeRateMs = writeRateMs this.writeRateMs = writeRateMs
} }

View File

@ -1,6 +1,6 @@
import API from "./api" import API from "./api"
import env from "../environment" import env from "../environment"
import { Headers } from "../constants" import { Header } from "../constants"
import { CloudAccount } from "@budibase/types" import { CloudAccount } from "@budibase/types"
const api = new API(env.ACCOUNT_PORTAL_URL) const api = new API(env.ACCOUNT_PORTAL_URL)
@ -14,7 +14,7 @@ export const getAccount = async (
const response = await api.post(`/api/accounts/search`, { const response = await api.post(`/api/accounts/search`, {
body: payload, body: payload,
headers: { headers: {
[Headers.API_KEY]: env.ACCOUNT_PORTAL_API_KEY, [Header.API_KEY]: env.ACCOUNT_PORTAL_API_KEY,
}, },
}) })
@ -35,7 +35,7 @@ export const getAccountByTenantId = async (
const response = await api.post(`/api/accounts/search`, { const response = await api.post(`/api/accounts/search`, {
body: payload, body: payload,
headers: { headers: {
[Headers.API_KEY]: env.ACCOUNT_PORTAL_API_KEY, [Header.API_KEY]: env.ACCOUNT_PORTAL_API_KEY,
}, },
}) })
@ -50,7 +50,7 @@ export const getAccountByTenantId = async (
export const getStatus = async () => { export const getStatus = async () => {
const response = await api.get(`/api/status`, { const response = await api.get(`/api/status`, {
headers: { headers: {
[Headers.API_KEY]: env.ACCOUNT_PORTAL_API_KEY, [Header.API_KEY]: env.ACCOUNT_PORTAL_API_KEY,
}, },
}) })
const json = await response.json() const json = await response.json()

View File

@ -1,650 +0,0 @@
const util = require("util")
const assert = require("assert")
const wrapEmitter = require("emitter-listener")
const async_hooks = require("async_hooks")
const CONTEXTS_SYMBOL = "cls@contexts"
const ERROR_SYMBOL = "error@context"
const DEBUG_CLS_HOOKED = process.env.DEBUG_CLS_HOOKED
let currentUid = -1
module.exports = {
getNamespace: getNamespace,
createNamespace: createNamespace,
destroyNamespace: destroyNamespace,
reset: reset,
ERROR_SYMBOL: ERROR_SYMBOL,
}
function Namespace(name) {
this.name = name
// changed in 2.7: no default context
this.active = null
this._set = []
this.id = null
this._contexts = new Map()
this._indent = 0
this._hook = null
}
Namespace.prototype.set = function set(key, value) {
if (!this.active) {
throw new Error(
"No context available. ns.run() or ns.bind() must be called first."
)
}
this.active[key] = value
if (DEBUG_CLS_HOOKED) {
const indentStr = " ".repeat(this._indent < 0 ? 0 : this._indent)
debug2(
indentStr +
"CONTEXT-SET KEY:" +
key +
"=" +
value +
" in ns:" +
this.name +
" currentUid:" +
currentUid +
" active:" +
util.inspect(this.active, { showHidden: true, depth: 2, colors: true })
)
}
return value
}
Namespace.prototype.get = function get(key) {
if (!this.active) {
if (DEBUG_CLS_HOOKED) {
const asyncHooksCurrentId = async_hooks.currentId()
const triggerId = async_hooks.triggerAsyncId()
const indentStr = " ".repeat(this._indent < 0 ? 0 : this._indent)
debug2(
`${indentStr}CONTEXT-GETTING KEY NO ACTIVE NS: (${this.name}) ${key}=undefined currentUid:${currentUid} asyncHooksCurrentId:${asyncHooksCurrentId} triggerId:${triggerId} len:${this._set.length}`
)
}
return undefined
}
if (DEBUG_CLS_HOOKED) {
const asyncHooksCurrentId = async_hooks.executionAsyncId()
const triggerId = async_hooks.triggerAsyncId()
const indentStr = " ".repeat(this._indent < 0 ? 0 : this._indent)
debug2(
indentStr +
"CONTEXT-GETTING KEY:" +
key +
"=" +
this.active[key] +
" (" +
this.name +
") currentUid:" +
currentUid +
" active:" +
util.inspect(this.active, { showHidden: true, depth: 2, colors: true })
)
debug2(
`${indentStr}CONTEXT-GETTING KEY: (${this.name}) ${key}=${
this.active[key]
} currentUid:${currentUid} asyncHooksCurrentId:${asyncHooksCurrentId} triggerId:${triggerId} len:${
this._set.length
} active:${util.inspect(this.active)}`
)
}
return this.active[key]
}
Namespace.prototype.createContext = function createContext() {
// Prototype inherit existing context if created a new child context within existing context.
let context = Object.create(this.active ? this.active : Object.prototype)
context._ns_name = this.name
context.id = currentUid
if (DEBUG_CLS_HOOKED) {
const asyncHooksCurrentId = async_hooks.executionAsyncId()
const triggerId = async_hooks.triggerAsyncId()
const indentStr = " ".repeat(this._indent < 0 ? 0 : this._indent)
debug2(
`${indentStr}CONTEXT-CREATED Context: (${
this.name
}) currentUid:${currentUid} asyncHooksCurrentId:${asyncHooksCurrentId} triggerId:${triggerId} len:${
this._set.length
} context:${util.inspect(context, {
showHidden: true,
depth: 2,
colors: true,
})}`
)
}
return context
}
Namespace.prototype.run = function run(fn) {
let context = this.createContext()
this.enter(context)
try {
if (DEBUG_CLS_HOOKED) {
const triggerId = async_hooks.triggerAsyncId()
const asyncHooksCurrentId = async_hooks.executionAsyncId()
const indentStr = " ".repeat(this._indent < 0 ? 0 : this._indent)
debug2(
`${indentStr}CONTEXT-RUN BEGIN: (${
this.name
}) currentUid:${currentUid} triggerId:${triggerId} asyncHooksCurrentId:${asyncHooksCurrentId} len:${
this._set.length
} context:${util.inspect(context)}`
)
}
fn(context)
return context
} catch (exception) {
if (exception) {
exception[ERROR_SYMBOL] = context
}
throw exception
} finally {
if (DEBUG_CLS_HOOKED) {
const triggerId = async_hooks.triggerAsyncId()
const asyncHooksCurrentId = async_hooks.executionAsyncId()
const indentStr = " ".repeat(this._indent < 0 ? 0 : this._indent)
debug2(
`${indentStr}CONTEXT-RUN END: (${
this.name
}) currentUid:${currentUid} triggerId:${triggerId} asyncHooksCurrentId:${asyncHooksCurrentId} len:${
this._set.length
} ${util.inspect(context)}`
)
}
this.exit(context)
}
}
Namespace.prototype.runAndReturn = function runAndReturn(fn) {
let value
this.run(function (context) {
value = fn(context)
})
return value
}
/**
* Uses global Promise and assumes Promise is cls friendly or wrapped already.
* @param {function} fn
* @returns {*}
*/
Namespace.prototype.runPromise = function runPromise(fn) {
let context = this.createContext()
this.enter(context)
let promise = fn(context)
if (!promise || !promise.then || !promise.catch) {
throw new Error("fn must return a promise.")
}
if (DEBUG_CLS_HOOKED) {
debug2(
"CONTEXT-runPromise BEFORE: (" +
this.name +
") currentUid:" +
currentUid +
" len:" +
this._set.length +
" " +
util.inspect(context)
)
}
return promise
.then(result => {
if (DEBUG_CLS_HOOKED) {
debug2(
"CONTEXT-runPromise AFTER then: (" +
this.name +
") currentUid:" +
currentUid +
" len:" +
this._set.length +
" " +
util.inspect(context)
)
}
this.exit(context)
return result
})
.catch(err => {
err[ERROR_SYMBOL] = context
if (DEBUG_CLS_HOOKED) {
debug2(
"CONTEXT-runPromise AFTER catch: (" +
this.name +
") currentUid:" +
currentUid +
" len:" +
this._set.length +
" " +
util.inspect(context)
)
}
this.exit(context)
throw err
})
}
Namespace.prototype.bind = function bindFactory(fn, context) {
if (!context) {
if (!this.active) {
context = this.createContext()
} else {
context = this.active
}
}
let self = this
return function clsBind() {
self.enter(context)
try {
return fn.apply(this, arguments)
} catch (exception) {
if (exception) {
exception[ERROR_SYMBOL] = context
}
throw exception
} finally {
self.exit(context)
}
}
}
Namespace.prototype.enter = function enter(context) {
assert.ok(context, "context must be provided for entering")
if (DEBUG_CLS_HOOKED) {
const asyncHooksCurrentId = async_hooks.executionAsyncId()
const triggerId = async_hooks.triggerAsyncId()
const indentStr = " ".repeat(this._indent < 0 ? 0 : this._indent)
debug2(
`${indentStr}CONTEXT-ENTER: (${
this.name
}) currentUid:${currentUid} triggerId:${triggerId} asyncHooksCurrentId:${asyncHooksCurrentId} len:${
this._set.length
} ${util.inspect(context)}`
)
}
this._set.push(this.active)
this.active = context
}
Namespace.prototype.exit = function exit(context) {
assert.ok(context, "context must be provided for exiting")
if (DEBUG_CLS_HOOKED) {
const asyncHooksCurrentId = async_hooks.executionAsyncId()
const triggerId = async_hooks.triggerAsyncId()
const indentStr = " ".repeat(this._indent < 0 ? 0 : this._indent)
debug2(
`${indentStr}CONTEXT-EXIT: (${
this.name
}) currentUid:${currentUid} triggerId:${triggerId} asyncHooksCurrentId:${asyncHooksCurrentId} len:${
this._set.length
} ${util.inspect(context)}`
)
}
// Fast path for most exits that are at the top of the stack
if (this.active === context) {
assert.ok(this._set.length, "can't remove top context")
this.active = this._set.pop()
return
}
// Fast search in the stack using lastIndexOf
let index = this._set.lastIndexOf(context)
if (index < 0) {
if (DEBUG_CLS_HOOKED) {
debug2(
"??ERROR?? context exiting but not entered - ignoring: " +
util.inspect(context)
)
}
assert.ok(
index >= 0,
"context not currently entered; can't exit. \n" +
util.inspect(this) +
"\n" +
util.inspect(context)
)
} else {
assert.ok(index, "can't remove top context")
this._set.splice(index, 1)
}
}
Namespace.prototype.bindEmitter = function bindEmitter(emitter) {
assert.ok(
emitter.on && emitter.addListener && emitter.emit,
"can only bind real EEs"
)
let namespace = this
let thisSymbol = "context@" + this.name
// Capture the context active at the time the emitter is bound.
function attach(listener) {
if (!listener) {
return
}
if (!listener[CONTEXTS_SYMBOL]) {
listener[CONTEXTS_SYMBOL] = Object.create(null)
}
listener[CONTEXTS_SYMBOL][thisSymbol] = {
namespace: namespace,
context: namespace.active,
}
}
// At emit time, bind the listener within the correct context.
function bind(unwrapped) {
if (!(unwrapped && unwrapped[CONTEXTS_SYMBOL])) {
return unwrapped
}
let wrapped = unwrapped
let unwrappedContexts = unwrapped[CONTEXTS_SYMBOL]
Object.keys(unwrappedContexts).forEach(function (name) {
let thunk = unwrappedContexts[name]
wrapped = thunk.namespace.bind(wrapped, thunk.context)
})
return wrapped
}
wrapEmitter(emitter, attach, bind)
}
/**
* If an error comes out of a namespace, it will have a context attached to it.
* This function knows how to find it.
*
* @param {Error} exception Possibly annotated error.
*/
Namespace.prototype.fromException = function fromException(exception) {
return exception[ERROR_SYMBOL]
}
function getNamespace(name) {
return process.namespaces[name]
}
function createNamespace(name) {
assert.ok(name, "namespace must be given a name.")
if (DEBUG_CLS_HOOKED) {
debug2(`NS-CREATING NAMESPACE (${name})`)
}
let namespace = new Namespace(name)
namespace.id = currentUid
const hook = async_hooks.createHook({
init(asyncId, type, triggerId, resource) {
currentUid = async_hooks.executionAsyncId()
//CHAIN Parent's Context onto child if none exists. This is needed to pass net-events.spec
// let initContext = namespace.active;
// if(!initContext && triggerId) {
// let parentContext = namespace._contexts.get(triggerId);
// if (parentContext) {
// namespace.active = parentContext;
// namespace._contexts.set(currentUid, parentContext);
// if (DEBUG_CLS_HOOKED) {
// const indentStr = ' '.repeat(namespace._indent < 0 ? 0 : namespace._indent);
// debug2(`${indentStr}INIT [${type}] (${name}) WITH PARENT CONTEXT asyncId:${asyncId} currentUid:${currentUid} triggerId:${triggerId} active:${util.inspect(namespace.active, true)} resource:${resource}`);
// }
// } else if (DEBUG_CLS_HOOKED) {
// const indentStr = ' '.repeat(namespace._indent < 0 ? 0 : namespace._indent);
// debug2(`${indentStr}INIT [${type}] (${name}) MISSING CONTEXT asyncId:${asyncId} currentUid:${currentUid} triggerId:${triggerId} active:${util.inspect(namespace.active, true)} resource:${resource}`);
// }
// }else {
// namespace._contexts.set(currentUid, namespace.active);
// if (DEBUG_CLS_HOOKED) {
// const indentStr = ' '.repeat(namespace._indent < 0 ? 0 : namespace._indent);
// debug2(`${indentStr}INIT [${type}] (${name}) asyncId:${asyncId} currentUid:${currentUid} triggerId:${triggerId} active:${util.inspect(namespace.active, true)} resource:${resource}`);
// }
// }
if (namespace.active) {
namespace._contexts.set(asyncId, namespace.active)
if (DEBUG_CLS_HOOKED) {
const indentStr = " ".repeat(
namespace._indent < 0 ? 0 : namespace._indent
)
debug2(
`${indentStr}INIT [${type}] (${name}) asyncId:${asyncId} currentUid:${currentUid} triggerId:${triggerId} active:${util.inspect(
namespace.active,
{ showHidden: true, depth: 2, colors: true }
)} resource:${resource}`
)
}
} else if (currentUid === 0) {
// CurrentId will be 0 when triggered from C++. Promise events
// https://github.com/nodejs/node/blob/master/doc/api/async_hooks.md#triggerid
const triggerId = async_hooks.triggerAsyncId()
const triggerIdContext = namespace._contexts.get(triggerId)
if (triggerIdContext) {
namespace._contexts.set(asyncId, triggerIdContext)
if (DEBUG_CLS_HOOKED) {
const indentStr = " ".repeat(
namespace._indent < 0 ? 0 : namespace._indent
)
debug2(
`${indentStr}INIT USING CONTEXT FROM TRIGGERID [${type}] (${name}) asyncId:${asyncId} currentUid:${currentUid} triggerId:${triggerId} active:${util.inspect(
namespace.active,
{ showHidden: true, depth: 2, colors: true }
)} resource:${resource}`
)
}
} else if (DEBUG_CLS_HOOKED) {
const indentStr = " ".repeat(
namespace._indent < 0 ? 0 : namespace._indent
)
debug2(
`${indentStr}INIT MISSING CONTEXT [${type}] (${name}) asyncId:${asyncId} currentUid:${currentUid} triggerId:${triggerId} active:${util.inspect(
namespace.active,
{ showHidden: true, depth: 2, colors: true }
)} resource:${resource}`
)
}
}
if (DEBUG_CLS_HOOKED && type === "PROMISE") {
debug2(util.inspect(resource, { showHidden: true }))
const parentId = resource.parentId
const indentStr = " ".repeat(
namespace._indent < 0 ? 0 : namespace._indent
)
debug2(
`${indentStr}INIT RESOURCE-PROMISE [${type}] (${name}) parentId:${parentId} asyncId:${asyncId} currentUid:${currentUid} triggerId:${triggerId} active:${util.inspect(
namespace.active,
{ showHidden: true, depth: 2, colors: true }
)} resource:${resource}`
)
}
},
before(asyncId) {
currentUid = async_hooks.executionAsyncId()
let context
/*
if(currentUid === 0){
// CurrentId will be 0 when triggered from C++. Promise events
// https://github.com/nodejs/node/blob/master/doc/api/async_hooks.md#triggerid
//const triggerId = async_hooks.triggerAsyncId();
context = namespace._contexts.get(asyncId); // || namespace._contexts.get(triggerId);
}else{
context = namespace._contexts.get(currentUid);
}
*/
//HACK to work with promises until they are fixed in node > 8.1.1
context =
namespace._contexts.get(asyncId) || namespace._contexts.get(currentUid)
if (context) {
if (DEBUG_CLS_HOOKED) {
const triggerId = async_hooks.triggerAsyncId()
const indentStr = " ".repeat(
namespace._indent < 0 ? 0 : namespace._indent
)
debug2(
`${indentStr}BEFORE (${name}) asyncId:${asyncId} currentUid:${currentUid} triggerId:${triggerId} active:${util.inspect(
namespace.active,
{ showHidden: true, depth: 2, colors: true }
)} context:${util.inspect(context)}`
)
namespace._indent += 2
}
namespace.enter(context)
} else if (DEBUG_CLS_HOOKED) {
const triggerId = async_hooks.triggerAsyncId()
const indentStr = " ".repeat(
namespace._indent < 0 ? 0 : namespace._indent
)
debug2(
`${indentStr}BEFORE MISSING CONTEXT (${name}) asyncId:${asyncId} currentUid:${currentUid} triggerId:${triggerId} active:${util.inspect(
namespace.active,
{ showHidden: true, depth: 2, colors: true }
)} namespace._contexts:${util.inspect(namespace._contexts, {
showHidden: true,
depth: 2,
colors: true,
})}`
)
namespace._indent += 2
}
},
after(asyncId) {
currentUid = async_hooks.executionAsyncId()
let context // = namespace._contexts.get(currentUid);
/*
if(currentUid === 0){
// CurrentId will be 0 when triggered from C++. Promise events
// https://github.com/nodejs/node/blob/master/doc/api/async_hooks.md#triggerid
//const triggerId = async_hooks.triggerAsyncId();
context = namespace._contexts.get(asyncId); // || namespace._contexts.get(triggerId);
}else{
context = namespace._contexts.get(currentUid);
}
*/
//HACK to work with promises until they are fixed in node > 8.1.1
context =
namespace._contexts.get(asyncId) || namespace._contexts.get(currentUid)
if (context) {
if (DEBUG_CLS_HOOKED) {
const triggerId = async_hooks.triggerAsyncId()
namespace._indent -= 2
const indentStr = " ".repeat(
namespace._indent < 0 ? 0 : namespace._indent
)
debug2(
`${indentStr}AFTER (${name}) asyncId:${asyncId} currentUid:${currentUid} triggerId:${triggerId} active:${util.inspect(
namespace.active,
{ showHidden: true, depth: 2, colors: true }
)} context:${util.inspect(context)}`
)
}
namespace.exit(context)
} else if (DEBUG_CLS_HOOKED) {
const triggerId = async_hooks.triggerAsyncId()
namespace._indent -= 2
const indentStr = " ".repeat(
namespace._indent < 0 ? 0 : namespace._indent
)
debug2(
`${indentStr}AFTER MISSING CONTEXT (${name}) asyncId:${asyncId} currentUid:${currentUid} triggerId:${triggerId} active:${util.inspect(
namespace.active,
{ showHidden: true, depth: 2, colors: true }
)} context:${util.inspect(context)}`
)
}
},
destroy(asyncId) {
currentUid = async_hooks.executionAsyncId()
if (DEBUG_CLS_HOOKED) {
const triggerId = async_hooks.triggerAsyncId()
const indentStr = " ".repeat(
namespace._indent < 0 ? 0 : namespace._indent
)
debug2(
`${indentStr}DESTROY (${name}) currentUid:${currentUid} asyncId:${asyncId} triggerId:${triggerId} active:${util.inspect(
namespace.active,
{ showHidden: true, depth: 2, colors: true }
)} context:${util.inspect(namespace._contexts.get(currentUid))}`
)
}
namespace._contexts.delete(asyncId)
},
})
hook.enable()
namespace._hook = hook
process.namespaces[name] = namespace
return namespace
}
function destroyNamespace(name) {
let namespace = getNamespace(name)
assert.ok(namespace, "can't delete nonexistent namespace! \"" + name + '"')
assert.ok(
namespace.id,
"don't assign to process.namespaces directly! " + util.inspect(namespace)
)
namespace._hook.disable()
namespace._contexts = null
process.namespaces[name] = null
}
function reset() {
// must unregister async listeners
if (process.namespaces) {
Object.keys(process.namespaces).forEach(function (name) {
destroyNamespace(name)
})
}
process.namespaces = Object.create(null)
}
process.namespaces = process.namespaces || {}
//const fs = require('fs');
function debug2(...args) {
if (DEBUG_CLS_HOOKED) {
//fs.writeSync(1, `${util.format(...args)}\n`);
process._rawDebug(`${util.format(...args)}`)
}
}
/*function getFunctionName(fn) {
if (!fn) {
return fn;
}
if (typeof fn === 'function') {
if (fn.name) {
return fn.name;
}
return (fn.toString().trim().match(/^function\s*([^\s(]+)/) || [])[1];
} else if (fn.constructor && fn.constructor.name) {
return fn.constructor.name;
}
}*/

View File

@ -1,44 +0,0 @@
exports.UserStatus = {
ACTIVE: "active",
INACTIVE: "inactive",
}
exports.Cookies = {
CurrentApp: "budibase:currentapp",
Auth: "budibase:auth",
Init: "budibase:init",
ACCOUNT_RETURN_URL: "budibase:account:returnurl",
DatasourceAuth: "budibase:datasourceauth",
OIDC_CONFIG: "budibase:oidc:config",
}
exports.Headers = {
API_KEY: "x-budibase-api-key",
LICENSE_KEY: "x-budibase-license-key",
API_VER: "x-budibase-api-version",
APP_ID: "x-budibase-app-id",
TYPE: "x-budibase-type",
PREVIEW_ROLE: "x-budibase-role",
TENANT_ID: "x-budibase-tenant-id",
TOKEN: "x-budibase-token",
CSRF_TOKEN: "x-csrf-token",
}
exports.GlobalRoles = {
OWNER: "owner",
ADMIN: "admin",
BUILDER: "builder",
WORKSPACE_MANAGER: "workspace_manager",
}
exports.Configs = {
SETTINGS: "settings",
ACCOUNT: "account",
SMTP: "smtp",
GOOGLE: "google",
OIDC: "oidc",
OIDC_LOGOS: "logos_oidc",
}
exports.MAX_VALID_DATE = new Date(2147483647000)
exports.DEFAULT_TENANT_ID = "default"

View File

@ -0,0 +1,44 @@
export enum UserStatus {
ACTIVE = "active",
INACTIVE = "inactive",
}
export enum Cookie {
CurrentApp = "budibase:currentapp",
Auth = "budibase:auth",
Init = "budibase:init",
ACCOUNT_RETURN_URL = "budibase:account:returnurl",
DatasourceAuth = "budibase:datasourceauth",
OIDC_CONFIG = "budibase:oidc:config",
}
export enum Header {
API_KEY = "x-budibase-api-key",
LICENSE_KEY = "x-budibase-license-key",
API_VER = "x-budibase-api-version",
APP_ID = "x-budibase-app-id",
TYPE = "x-budibase-type",
PREVIEW_ROLE = "x-budibase-role",
TENANT_ID = "x-budibase-tenant-id",
TOKEN = "x-budibase-token",
CSRF_TOKEN = "x-csrf-token",
}
export enum GlobalRole {
OWNER = "owner",
ADMIN = "admin",
BUILDER = "builder",
WORKSPACE_MANAGER = "workspace_manager",
}
export enum Config {
SETTINGS = "settings",
ACCOUNT = "account",
SMTP = "smtp",
GOOGLE = "google",
OIDC = "oidc",
OIDC_LOGOS = "logos_oidc",
}
export const MAX_VALID_DATE = new Date(2147483647000)
export const DEFAULT_TENANT_ID = "default"

View File

@ -0,0 +1,18 @@
import { AsyncLocalStorage } from "async_hooks"
import { ContextMap } from "./constants"
export default class Context {
static storage = new AsyncLocalStorage<ContextMap>()
static run(context: ContextMap, func: any) {
return Context.storage.run(context, () => func())
}
static get(): ContextMap {
return Context.storage.getStore() as ContextMap
}
static set(context: ContextMap) {
Context.storage.enterWith(context)
}
}

View File

@ -1,47 +0,0 @@
const cls = require("../clshooked")
const { newid } = require("../hashing")
const REQUEST_ID_KEY = "requestId"
const MAIN_CTX = cls.createNamespace("main")
function getContextStorage(namespace) {
if (namespace && namespace.active) {
let contextData = namespace.active
delete contextData.id
delete contextData._ns_name
return contextData
}
return {}
}
class FunctionContext {
static run(callback) {
return MAIN_CTX.runAndReturn(async () => {
const namespaceId = newid()
MAIN_CTX.set(REQUEST_ID_KEY, namespaceId)
const namespace = cls.createNamespace(namespaceId)
let response = await namespace.runAndReturn(callback)
cls.destroyNamespace(namespaceId)
return response
})
}
static setOnContext(key, value) {
const namespaceId = MAIN_CTX.get(REQUEST_ID_KEY)
const namespace = cls.getNamespace(namespaceId)
namespace.set(key, value)
}
static getFromContext(key) {
const namespaceId = MAIN_CTX.get(REQUEST_ID_KEY)
const namespace = cls.getNamespace(namespaceId)
const context = getContextStorage(namespace)
if (context) {
return context[key]
} else {
return null
}
}
}
module.exports = FunctionContext

View File

@ -1,17 +1,7 @@
export enum ContextKey { import { IdentityContext } from "@budibase/types"
TENANT_ID = "tenantId",
GLOBAL_DB = "globalDb", export type ContextMap = {
APP_ID = "appId", tenantId?: string
IDENTITY = "identity", appId?: string
// whatever the request app DB was identity?: IdentityContext
CURRENT_DB = "currentDb",
// get the prod app DB from the request
PROD_DB = "prodDb",
// get the dev app DB from the request
DEV_DB = "devDb",
DB_OPTS = "dbOpts",
// check if something else is using the context, don't close DB
TENANCY_IN_USE = "tenancyInUse",
APP_IN_USE = "appInUse",
IDENTITY_IN_USE = "identityInUse",
} }

View File

@ -1,15 +1,18 @@
import { getGlobalUserParams, getAllApps } from "../db/utils" import {
import { doWithDB } from "../db" getGlobalUserParams,
getAllApps,
doWithDB,
StaticDatabases,
} from "../db"
import { doWithGlobalDB } from "../tenancy" import { doWithGlobalDB } from "../tenancy"
import { StaticDatabases } from "../db/constants" import { App, Tenants, User, Database } from "@budibase/types"
import { App, Tenants, User } from "@budibase/types"
const TENANT_DOC = StaticDatabases.PLATFORM_INFO.docs.tenants const TENANT_DOC = StaticDatabases.PLATFORM_INFO.docs.tenants
const PLATFORM_INFO_DB = StaticDatabases.PLATFORM_INFO.name const PLATFORM_INFO_DB = StaticDatabases.PLATFORM_INFO.name
const removeTenantFromInfoDB = async (tenantId: string) => { async function removeTenantFromInfoDB(tenantId: string) {
try { try {
await doWithDB(PLATFORM_INFO_DB, async (infoDb: any) => { await doWithDB(PLATFORM_INFO_DB, async (infoDb: Database) => {
const tenants = (await infoDb.get(TENANT_DOC)) as Tenants const tenants = (await infoDb.get(TENANT_DOC)) as Tenants
tenants.tenantIds = tenants.tenantIds.filter(id => id !== tenantId) tenants.tenantIds = tenants.tenantIds.filter(id => id !== tenantId)
@ -21,9 +24,9 @@ const removeTenantFromInfoDB = async (tenantId: string) => {
} }
} }
export const removeUserFromInfoDB = async (dbUser: User) => { export async function removeUserFromInfoDB(dbUser: User) {
await doWithDB(PLATFORM_INFO_DB, async (infoDb: any) => { await doWithDB(PLATFORM_INFO_DB, async (infoDb: Database) => {
const keys = [dbUser._id, dbUser.email] const keys = [dbUser._id!, dbUser.email]
const userDocs = await infoDb.allDocs({ const userDocs = await infoDb.allDocs({
keys, keys,
include_docs: true, include_docs: true,
@ -38,7 +41,7 @@ export const removeUserFromInfoDB = async (dbUser: User) => {
}) })
} }
const removeUsersFromInfoDB = async (tenantId: string) => { async function removeUsersFromInfoDB(tenantId: string) {
return doWithGlobalDB(tenantId, async (db: any) => { return doWithGlobalDB(tenantId, async (db: any) => {
try { try {
const allUsers = await db.allDocs( const allUsers = await db.allDocs(
@ -72,8 +75,8 @@ const removeUsersFromInfoDB = async (tenantId: string) => {
}) })
} }
const removeGlobalDB = async (tenantId: string) => { async function removeGlobalDB(tenantId: string) {
return doWithGlobalDB(tenantId, async (db: any) => { return doWithGlobalDB(tenantId, async (db: Database) => {
try { try {
await db.destroy() await db.destroy()
} catch (err) { } catch (err) {
@ -83,11 +86,11 @@ const removeGlobalDB = async (tenantId: string) => {
}) })
} }
const removeTenantApps = async (tenantId: string) => { async function removeTenantApps(tenantId: string) {
try { try {
const apps = (await getAllApps({ all: true })) as App[] const apps = (await getAllApps({ all: true })) as App[]
const destroyPromises = apps.map(app => const destroyPromises = apps.map(app =>
doWithDB(app.appId, (db: any) => db.destroy()) doWithDB(app.appId, (db: Database) => db.destroy())
) )
await Promise.allSettled(destroyPromises) await Promise.allSettled(destroyPromises)
} catch (err) { } catch (err) {
@ -97,7 +100,7 @@ const removeTenantApps = async (tenantId: string) => {
} }
// can't live in tenancy package due to circular dependency on db/utils // can't live in tenancy package due to circular dependency on db/utils
export const deleteTenant = async (tenantId: string) => { export async function deleteTenant(tenantId: string) {
await removeTenantFromInfoDB(tenantId) await removeTenantFromInfoDB(tenantId)
await removeUsersFromInfoDB(tenantId) await removeUsersFromInfoDB(tenantId)
await removeGlobalDB(tenantId) await removeGlobalDB(tenantId)

View File

@ -1,47 +1,32 @@
import env from "../environment" import env from "../environment"
import { SEPARATOR, DocumentType } from "../db/constants"
import cls from "./FunctionContext"
import { dangerousGetDB, closeDB } from "../db"
import { baseGlobalDBName } from "../db/tenancy"
import { IdentityContext } from "@budibase/types"
import { DEFAULT_TENANT_ID as _DEFAULT_TENANT_ID } from "../constants"
import { ContextKey } from "./constants"
import PouchDB from "pouchdb"
import { import {
updateUsing, SEPARATOR,
closeWithUsing, DocumentType,
setAppTenantId, getDevelopmentAppID,
setIdentity, getProdAppID,
closeAppDBs, baseGlobalDBName,
getContextDB, getDB,
} from "./utils" } from "../db"
import Context from "./Context"
import { IdentityContext, Database } from "@budibase/types"
import { DEFAULT_TENANT_ID as _DEFAULT_TENANT_ID } from "../constants"
import { ContextMap } from "./constants"
export const DEFAULT_TENANT_ID = _DEFAULT_TENANT_ID export const DEFAULT_TENANT_ID = _DEFAULT_TENANT_ID
// some test cases call functions directly, need to // some test cases call functions directly, need to
// store an app ID to pretend there is a context // store an app ID to pretend there is a context
let TEST_APP_ID: string | null = null let TEST_APP_ID: string | null = null
export const closeTenancy = async () => { export function isMultiTenant() {
try { return env.MULTI_TENANCY
if (env.USE_COUCH) {
const db = getGlobalDB()
await closeDB(db)
}
} catch (err) {
// no DB found - skip closing
return
}
// clear from context now that database is closed/task is finished
cls.setOnContext(ContextKey.TENANT_ID, null)
cls.setOnContext(ContextKey.GLOBAL_DB, null)
} }
// export const isDefaultTenant = () => { export function isTenantIdSet() {
// return getTenantId() === DEFAULT_TENANT_ID const context = Context.get()
// } return !!context?.tenantId
}
export const isMultiTenant = () => { export function isTenancyEnabled() {
return env.MULTI_TENANCY return env.MULTI_TENANCY
} }
@ -49,9 +34,9 @@ export const isMultiTenant = () => {
* Given an app ID this will attempt to retrieve the tenant ID from it. * Given an app ID this will attempt to retrieve the tenant ID from it.
* @return {null|string} The tenant ID found within the app ID. * @return {null|string} The tenant ID found within the app ID.
*/ */
export const getTenantIDFromAppID = (appId: string) => { export function getTenantIDFromAppID(appId: string) {
if (!appId) { if (!appId) {
return null return undefined
} }
if (!isMultiTenant()) { if (!isMultiTenant()) {
return DEFAULT_TENANT_ID return DEFAULT_TENANT_ID
@ -59,7 +44,7 @@ export const getTenantIDFromAppID = (appId: string) => {
const split = appId.split(SEPARATOR) const split = appId.split(SEPARATOR)
const hasDev = split[1] === DocumentType.DEV const hasDev = split[1] === DocumentType.DEV
if ((hasDev && split.length === 3) || (!hasDev && split.length === 2)) { if ((hasDev && split.length === 3) || (!hasDev && split.length === 2)) {
return null return undefined
} }
if (hasDev) { if (hasDev) {
return split[2] return split[2]
@ -68,127 +53,125 @@ export const getTenantIDFromAppID = (appId: string) => {
} }
} }
export const doInContext = async (appId: string, task: any) => { function updateContext(updates: ContextMap) {
// gets the tenant ID from the app ID let context: ContextMap
const tenantId = getTenantIDFromAppID(appId) try {
return doInTenant(tenantId, async () => { context = Context.get()
return doInAppContext(appId, async () => { } catch (err) {
return task() // no context, start empty
}) context = {}
}) }
context = {
...context,
...updates,
}
return context
} }
export const doInTenant = (tenantId: string | null, task: any) => { async function newContext(updates: ContextMap, task: any) {
// see if there already is a context setup
let context: ContextMap = updateContext(updates)
return Context.run(context, task)
}
export async function doInContext(appId: string, task: any): Promise<any> {
const tenantId = getTenantIDFromAppID(appId)
return newContext(
{
tenantId,
appId,
},
task
)
}
export async function doInTenant(
tenantId: string | null,
task: any
): Promise<any> {
// make sure default always selected in single tenancy // make sure default always selected in single tenancy
if (!env.MULTI_TENANCY) { if (!env.MULTI_TENANCY) {
tenantId = tenantId || DEFAULT_TENANT_ID tenantId = tenantId || DEFAULT_TENANT_ID
} }
// the internal function is so that we can re-use an existing
// context - don't want to close DB on a parent context
async function internal(opts = { existing: false }) {
// set the tenant id + global db if this is a new context
if (!opts.existing) {
updateTenantId(tenantId)
}
try { const updates = tenantId ? { tenantId } : {}
// invoke the task return newContext(updates, task)
return await task()
} finally {
await closeWithUsing(ContextKey.TENANCY_IN_USE, () => {
return closeTenancy()
})
}
}
const existing = cls.getFromContext(ContextKey.TENANT_ID) === tenantId
return updateUsing(ContextKey.TENANCY_IN_USE, existing, internal)
} }
export const doInAppContext = (appId: string, task: any) => { export async function doInAppContext(appId: string, task: any): Promise<any> {
if (!appId) { if (!appId) {
throw new Error("appId is required") throw new Error("appId is required")
} }
const identity = getIdentity() const tenantId = getTenantIDFromAppID(appId)
const updates: ContextMap = { appId }
// the internal function is so that we can re-use an existing if (tenantId) {
// context - don't want to close DB on a parent context updates.tenantId = tenantId
async function internal(opts = { existing: false }) {
// set the app tenant id
if (!opts.existing) {
setAppTenantId(appId)
}
// set the app ID
cls.setOnContext(ContextKey.APP_ID, appId)
// preserve the identity
if (identity) {
setIdentity(identity)
}
try {
// invoke the task
return await task()
} finally {
await closeWithUsing(ContextKey.APP_IN_USE, async () => {
await closeAppDBs()
await closeTenancy()
})
}
} }
const existing = cls.getFromContext(ContextKey.APP_ID) === appId return newContext(updates, task)
return updateUsing(ContextKey.APP_IN_USE, existing, internal)
} }
export const doInIdentityContext = (identity: IdentityContext, task: any) => { export async function doInIdentityContext(
identity: IdentityContext,
task: any
): Promise<any> {
if (!identity) { if (!identity) {
throw new Error("identity is required") throw new Error("identity is required")
} }
async function internal(opts = { existing: false }) { const context: ContextMap = {
if (!opts.existing) { identity,
cls.setOnContext(ContextKey.IDENTITY, identity)
// set the tenant so that doInTenant will preserve identity
if (identity.tenantId) {
updateTenantId(identity.tenantId)
}
}
try {
// invoke the task
return await task()
} finally {
await closeWithUsing(ContextKey.IDENTITY_IN_USE, async () => {
setIdentity(null)
await closeTenancy()
})
}
} }
if (identity.tenantId) {
const existing = cls.getFromContext(ContextKey.IDENTITY) context.tenantId = identity.tenantId
return updateUsing(ContextKey.IDENTITY_IN_USE, existing, internal) }
return newContext(context, task)
} }
export const getIdentity = (): IdentityContext | undefined => { export function getIdentity(): IdentityContext | undefined {
try { try {
return cls.getFromContext(ContextKey.IDENTITY) const context = Context.get()
return context?.identity
} catch (e) { } catch (e) {
// do nothing - identity is not in context // do nothing - identity is not in context
} }
} }
export const updateTenantId = (tenantId: string | null) => { export function getTenantId(): string {
cls.setOnContext(ContextKey.TENANT_ID, tenantId) if (!isMultiTenant()) {
if (env.USE_COUCH) { return DEFAULT_TENANT_ID
setGlobalDB(tenantId) }
const context = Context.get()
const tenantId = context?.tenantId
if (!tenantId) {
throw new Error("Tenant id not found")
}
return tenantId
}
export function getAppId(): string | undefined {
const context = Context.get()
const foundId = context?.appId
if (!foundId && env.isTest() && TEST_APP_ID) {
return TEST_APP_ID
} else {
return foundId
} }
} }
export const updateAppId = async (appId: string) => { export function updateTenantId(tenantId?: string) {
let context: ContextMap = updateContext({
tenantId,
})
Context.set(context)
}
export function updateAppId(appId: string) {
let context: ContextMap = updateContext({
appId,
})
try { try {
// have to close first, before removing the databases from context Context.set(context)
await closeAppDBs()
cls.setOnContext(ContextKey.APP_ID, appId)
} catch (err) { } catch (err) {
if (env.isTest()) { if (env.isTest()) {
TEST_APP_ID = appId TEST_APP_ID = appId
@ -198,70 +181,43 @@ export const updateAppId = async (appId: string) => {
} }
} }
export const setGlobalDB = (tenantId: string | null) => { export function getGlobalDB(): Database {
const dbName = baseGlobalDBName(tenantId) const context = Context.get()
const db = dangerousGetDB(dbName) if (!context || (env.MULTI_TENANCY && !context.tenantId)) {
cls.setOnContext(ContextKey.GLOBAL_DB, db)
return db
}
export const getGlobalDB = () => {
const db = cls.getFromContext(ContextKey.GLOBAL_DB)
if (!db) {
throw new Error("Global DB not found") throw new Error("Global DB not found")
} }
return db return getDB(baseGlobalDBName(context?.tenantId))
}
export const isTenantIdSet = () => {
const tenantId = cls.getFromContext(ContextKey.TENANT_ID)
return !!tenantId
}
export const getTenantId = () => {
if (!isMultiTenant()) {
return DEFAULT_TENANT_ID
}
const tenantId = cls.getFromContext(ContextKey.TENANT_ID)
if (!tenantId) {
throw new Error("Tenant id not found")
}
return tenantId
}
export const getAppId = () => {
const foundId = cls.getFromContext(ContextKey.APP_ID)
if (!foundId && env.isTest() && TEST_APP_ID) {
return TEST_APP_ID
} else {
return foundId
}
}
export const isTenancyEnabled = () => {
return env.MULTI_TENANCY
} }
/** /**
* Opens the app database based on whatever the request * Gets the app database based on whatever the request
* contained, dev or prod. * contained, dev or prod.
*/ */
export const getAppDB = (opts?: any) => { export function getAppDB(opts?: any): Database {
return getContextDB(ContextKey.CURRENT_DB, opts) const appId = getAppId()
return getDB(appId, opts)
} }
/** /**
* This specifically gets the prod app ID, if the request * This specifically gets the prod app ID, if the request
* contained a development app ID, this will open the prod one. * contained a development app ID, this will get the prod one.
*/ */
export const getProdAppDB = (opts?: any) => { export function getProdAppDB(opts?: any): Database {
return getContextDB(ContextKey.PROD_DB, opts) const appId = getAppId()
if (!appId) {
throw new Error("Unable to retrieve prod DB - no app ID.")
}
return getDB(getProdAppID(appId), opts)
} }
/** /**
* This specifically gets the dev app ID, if the request * This specifically gets the dev app ID, if the request
* contained a prod app ID, this will open the dev one. * contained a prod app ID, this will get the dev one.
*/ */
export const getDevAppDB = (opts?: any) => { export function getDevAppDB(opts?: any): Database {
return getContextDB(ContextKey.DEV_DB, opts) const appId = getAppId()
if (!appId) {
throw new Error("Unable to retrieve dev DB - no app ID.")
}
return getDB(getDevelopmentAppID(appId), opts)
} }

View File

@ -1,18 +1,9 @@
import "../../../tests" require("../../../tests")
import * as context from ".." const context = require("../")
import { DEFAULT_TENANT_ID } from "../../constants" const { DEFAULT_TENANT_ID } = require("../../constants")
import env from "../../environment" const env = require("../../environment")
// must use require to spy index file exports due to known issue in jest
const dbUtils = require("../../db")
jest.spyOn(dbUtils, "closeDB")
jest.spyOn(dbUtils, "dangerousGetDB")
describe("context", () => { describe("context", () => {
beforeEach(() => {
jest.clearAllMocks()
})
describe("doInTenant", () => { describe("doInTenant", () => {
describe("single-tenancy", () => { describe("single-tenancy", () => {
it("defaults to the default tenant", () => { it("defaults to the default tenant", () => {
@ -25,8 +16,6 @@ describe("context", () => {
const db = context.getGlobalDB() const db = context.getGlobalDB()
expect(db.name).toBe("global-db") expect(db.name).toBe("global-db")
}) })
expect(dbUtils.dangerousGetDB).toHaveBeenCalledTimes(1)
expect(dbUtils.closeDB).toHaveBeenCalledTimes(1)
}) })
}) })
@ -40,7 +29,7 @@ describe("context", () => {
let error let error
try { try {
context.getTenantId() context.getTenantId()
} catch (e: any) { } catch (e) {
error = e error = e
} }
expect(error.message).toBe("Tenant id not found") expect(error.message).toBe("Tenant id not found")
@ -59,7 +48,7 @@ describe("context", () => {
let error let error
try { try {
context.getGlobalDB() context.getGlobalDB()
} catch (e: any) { } catch (e) {
error = e error = e
} }
expect(error.message).toBe("Global DB not found") expect(error.message).toBe("Global DB not found")
@ -85,8 +74,6 @@ describe("context", () => {
const db = context.getGlobalDB() const db = context.getGlobalDB()
expect(db.name).toBe("test_global-db") expect(db.name).toBe("test_global-db")
}) })
expect(dbUtils.dangerousGetDB).toHaveBeenCalledTimes(1)
expect(dbUtils.closeDB).toHaveBeenCalledTimes(1)
}) })
it("sets the tenant id when nested with same tenant id", async () => { it("sets the tenant id when nested with same tenant id", async () => {
@ -121,10 +108,6 @@ describe("context", () => {
}) })
}) })
}) })
// only 1 db is opened and closed
expect(dbUtils.dangerousGetDB).toHaveBeenCalledTimes(1)
expect(dbUtils.closeDB).toHaveBeenCalledTimes(1)
}) })
it("sets different tenant id inside another context", () => { it("sets different tenant id inside another context", () => {

View File

@ -1,109 +0,0 @@
import {
DEFAULT_TENANT_ID,
getAppId,
getTenantIDFromAppID,
updateTenantId,
} from "./index"
import cls from "./FunctionContext"
import { IdentityContext } from "@budibase/types"
import { ContextKey } from "./constants"
import { dangerousGetDB, closeDB } from "../db"
import { isEqual } from "lodash"
import { getDevelopmentAppID, getProdAppID } from "../db/conversions"
import env from "../environment"
export async function updateUsing(
usingKey: string,
existing: boolean,
internal: (opts: { existing: boolean }) => Promise<any>
) {
const using = cls.getFromContext(usingKey)
if (using && existing) {
cls.setOnContext(usingKey, using + 1)
return internal({ existing: true })
} else {
return cls.run(async () => {
cls.setOnContext(usingKey, 1)
return internal({ existing: false })
})
}
}
export async function closeWithUsing(
usingKey: string,
closeFn: () => Promise<any>
) {
const using = cls.getFromContext(usingKey)
if (!using || using <= 1) {
await closeFn()
} else {
cls.setOnContext(usingKey, using - 1)
}
}
export const setAppTenantId = (appId: string) => {
const appTenantId = getTenantIDFromAppID(appId) || DEFAULT_TENANT_ID
updateTenantId(appTenantId)
}
export const setIdentity = (identity: IdentityContext | null) => {
cls.setOnContext(ContextKey.IDENTITY, identity)
}
// this function makes sure the PouchDB objects are closed and
// fully deleted when finished - this protects against memory leaks
export async function closeAppDBs() {
const dbKeys = [ContextKey.CURRENT_DB, ContextKey.PROD_DB, ContextKey.DEV_DB]
for (let dbKey of dbKeys) {
const db = cls.getFromContext(dbKey)
if (!db) {
continue
}
await closeDB(db)
// clear the DB from context, incase someone tries to use it again
cls.setOnContext(dbKey, null)
}
// clear the app ID now that the databases are closed
if (cls.getFromContext(ContextKey.APP_ID)) {
cls.setOnContext(ContextKey.APP_ID, null)
}
if (cls.getFromContext(ContextKey.DB_OPTS)) {
cls.setOnContext(ContextKey.DB_OPTS, null)
}
}
export function getContextDB(key: string, opts: any) {
const dbOptsKey = `${key}${ContextKey.DB_OPTS}`
let storedOpts = cls.getFromContext(dbOptsKey)
let db = cls.getFromContext(key)
if (db && isEqual(opts, storedOpts)) {
return db
}
const appId = getAppId()
let toUseAppId
switch (key) {
case ContextKey.CURRENT_DB:
toUseAppId = appId
break
case ContextKey.PROD_DB:
toUseAppId = getProdAppID(appId)
break
case ContextKey.DEV_DB:
toUseAppId = getDevelopmentAppID(appId)
break
}
db = dangerousGetDB(toUseAppId, opts)
try {
cls.setOnContext(key, db)
if (opts) {
cls.setOnContext(dbOptsKey, opts)
}
} catch (err) {
if (!env.isTest()) {
throw err
}
}
return db
}

View File

@ -1,4 +1,4 @@
import { dangerousGetDB, closeDB } from "." import { getPouchDB, closePouchDB } from "./couch/pouchDB"
import { DocumentType } from "./constants" import { DocumentType } from "./constants"
class Replication { class Replication {
@ -12,12 +12,12 @@ class Replication {
* @param {String} target - the DB you want to replicate to, or rollback from * @param {String} target - the DB you want to replicate to, or rollback from
*/ */
constructor({ source, target }: any) { constructor({ source, target }: any) {
this.source = dangerousGetDB(source) this.source = getPouchDB(source)
this.target = dangerousGetDB(target) this.target = getPouchDB(target)
} }
close() { close() {
return Promise.all([closeDB(this.source), closeDB(this.target)]) return Promise.all([closePouchDB(this.source), closePouchDB(this.target)])
} }
promisify(operation: any, opts = {}) { promisify(operation: any, opts = {}) {
@ -68,7 +68,7 @@ class Replication {
async rollback() { async rollback() {
await this.target.destroy() await this.target.destroy()
// Recreate the DB again // Recreate the DB again
this.target = dangerousGetDB(this.target.name) this.target = getPouchDB(this.target.name)
// take the opportunity to remove deleted tombstones // take the opportunity to remove deleted tombstones
await this.replicate() await this.replicate()
} }

View File

@ -1,32 +1,33 @@
import { APP_DEV_PREFIX, APP_PREFIX } from "./constants"
import { App } from "@budibase/types"
const NO_APP_ERROR = "No app provided" const NO_APP_ERROR = "No app provided"
const { APP_DEV_PREFIX, APP_PREFIX } = require("./constants")
exports.isDevAppID = appId => { export function isDevAppID(appId?: string) {
if (!appId) { if (!appId) {
throw NO_APP_ERROR throw NO_APP_ERROR
} }
return appId.startsWith(APP_DEV_PREFIX) return appId.startsWith(APP_DEV_PREFIX)
} }
exports.isProdAppID = appId => { export function isProdAppID(appId?: string) {
if (!appId) { if (!appId) {
throw NO_APP_ERROR throw NO_APP_ERROR
} }
return appId.startsWith(APP_PREFIX) && !exports.isDevAppID(appId) return appId.startsWith(APP_PREFIX) && !isDevAppID(appId)
} }
exports.isDevApp = app => { export function isDevApp(app: App) {
if (!app) { if (!app) {
throw NO_APP_ERROR throw NO_APP_ERROR
} }
return exports.isDevAppID(app.appId) return isDevAppID(app.appId)
} }
/** /**
* Generates a development app ID from a real app ID. * Generates a development app ID from a real app ID.
* @returns {string} the dev app ID which can be used for dev database. * @returns {string} the dev app ID which can be used for dev database.
*/ */
exports.getDevelopmentAppID = appId => { export function getDevelopmentAppID(appId: string) {
if (!appId || appId.startsWith(APP_DEV_PREFIX)) { if (!appId || appId.startsWith(APP_DEV_PREFIX)) {
return appId return appId
} }
@ -36,12 +37,12 @@ exports.getDevelopmentAppID = appId => {
const rest = split.join(APP_PREFIX) const rest = split.join(APP_PREFIX)
return `${APP_DEV_PREFIX}${rest}` return `${APP_DEV_PREFIX}${rest}`
} }
exports.getDevAppID = exports.getDevelopmentAppID export const getDevAppID = getDevelopmentAppID
/** /**
* Convert a development app ID to a deployed app ID. * Convert a development app ID to a deployed app ID.
*/ */
exports.getProdAppID = appId => { export function getProdAppID(appId: string) {
if (!appId || !appId.startsWith(APP_DEV_PREFIX)) { if (!appId || !appId.startsWith(APP_DEV_PREFIX)) {
return appId return appId
} }
@ -52,7 +53,7 @@ exports.getProdAppID = appId => {
return `${APP_PREFIX}${rest}` return `${APP_PREFIX}${rest}`
} }
exports.extractAppUUID = id => { export function extractAppUUID(id: string) {
const split = id?.split("_") || [] const split = id?.split("_") || []
return split.length ? split[split.length - 1] : null return split.length ? split[split.length - 1] : null
} }

View File

@ -0,0 +1,179 @@
import Nano from "nano"
import {
AllDocsResponse,
AnyDocument,
Database,
DatabaseOpts,
DatabaseQueryOpts,
DatabasePutOpts,
} from "@budibase/types"
import { getCouchInfo } from "./connections"
import { directCouchCall } from "./utils"
import { getPouchDB } from "./pouchDB"
export class DatabaseImpl implements Database {
public readonly name: string
private static nano: Nano.ServerScope
private readonly pouchOpts: DatabaseOpts
constructor(dbName?: string, opts?: DatabaseOpts) {
if (dbName == null) {
throw new Error("Database name cannot be undefined.")
}
this.name = dbName
this.pouchOpts = opts || {}
if (!DatabaseImpl.nano) {
DatabaseImpl.init()
}
}
static init() {
const couchInfo = getCouchInfo()
DatabaseImpl.nano = Nano({
url: couchInfo.url,
requestDefaults: {
headers: {
Authorization: couchInfo.cookie,
},
},
parseUrl: false,
})
}
async exists() {
let response = await directCouchCall(`/${this.name}`, "HEAD")
return response.status === 200
}
async checkSetup() {
let shouldCreate = !this.pouchOpts?.skip_setup
// check exists in a lightweight fashion
let exists = await this.exists()
if (!shouldCreate && !exists) {
throw new Error("DB does not exist")
}
if (!exists) {
await DatabaseImpl.nano.db.create(this.name)
}
return DatabaseImpl.nano.db.use(this.name)
}
private async updateOutput(fnc: any) {
try {
return await fnc()
} catch (err: any) {
if (err.statusCode) {
err.status = err.statusCode
}
throw err
}
}
async get<T>(id?: string): Promise<T | any> {
const db = await this.checkSetup()
if (!id) {
throw new Error("Unable to get doc without a valid _id.")
}
return this.updateOutput(() => db.get(id))
}
async remove(id?: string, rev?: string) {
const db = await this.checkSetup()
if (!id || !rev) {
throw new Error("Unable to remove doc without a valid _id and _rev.")
}
return this.updateOutput(() => db.destroy(id, rev))
}
async put(document: AnyDocument, opts?: DatabasePutOpts) {
if (!document._id) {
throw new Error("Cannot store document without _id field.")
}
const db = await this.checkSetup()
if (!document.createdAt) {
document.createdAt = new Date().toISOString()
}
document.updatedAt = new Date().toISOString()
if (opts?.force && document._id) {
try {
const existing = await this.get(document._id)
if (existing) {
document._rev = existing._rev
}
} catch (err: any) {
if (err.status !== 404) {
throw err
}
}
}
return this.updateOutput(() => db.insert(document))
}
async bulkDocs(documents: AnyDocument[]) {
const db = await this.checkSetup()
return this.updateOutput(() => db.bulk({ docs: documents }))
}
async allDocs<T>(params: DatabaseQueryOpts): Promise<AllDocsResponse<T>> {
const db = await this.checkSetup()
return this.updateOutput(() => db.list(params))
}
async query<T>(
viewName: string,
params: DatabaseQueryOpts
): Promise<AllDocsResponse<T>> {
const db = await this.checkSetup()
const [database, view] = viewName.split("/")
return this.updateOutput(() => db.view(database, view, params))
}
async destroy() {
try {
await DatabaseImpl.nano.db.destroy(this.name)
} catch (err: any) {
// didn't exist, don't worry
if (err.statusCode === 404) {
return
} else {
throw { ...err, status: err.statusCode }
}
}
}
async compact() {
const db = await this.checkSetup()
return this.updateOutput(() => db.compact())
}
private doWithPouchDB(func: string) {
const dbName = this.name
return async (args: any[]) => {
const pouch = getPouchDB(dbName)
// @ts-ignore
return pouch[func](...args)
}
}
// All below functions are in-frequently called, just utilise PouchDB
// for them as it implements them better than we can
async dump(...args: any[]) {
return this.doWithPouchDB("dump")(args)
}
async load(...args: any[]) {
return this.doWithPouchDB("load")(args)
}
async createIndex(...args: any[]) {
return this.doWithPouchDB("createIndex")(args)
}
async deleteIndex(...args: any[]) {
return this.doWithPouchDB("createIndex")(args)
}
async getIndexes(...args: any[]) {
return this.doWithPouchDB("createIndex")(args)
}
}

View File

@ -1,6 +1,37 @@
import PouchDB from "pouchdb" import env from "../../environment"
import env from "../environment"
import { PouchOptions } from "@budibase/types" export const getCouchInfo = () => {
const urlInfo = getUrlInfo()
let username
let password
if (env.COUCH_DB_USERNAME) {
// set from env
username = env.COUCH_DB_USERNAME
} else if (urlInfo.auth.username) {
// set from url
username = urlInfo.auth.username
} else if (!env.isTest()) {
throw new Error("CouchDB username not set")
}
if (env.COUCH_DB_PASSWORD) {
// set from env
password = env.COUCH_DB_PASSWORD
} else if (urlInfo.auth.password) {
// set from url
password = urlInfo.auth.password
} else if (!env.isTest()) {
throw new Error("CouchDB password not set")
}
const authCookie = Buffer.from(`${username}:${password}`).toString("base64")
return {
url: urlInfo.url!,
auth: {
username: username,
password: password,
},
cookie: `Basic ${authCookie}`,
}
}
export const getUrlInfo = (url = env.COUCH_DB_URL) => { export const getUrlInfo = (url = env.COUCH_DB_URL) => {
let cleanUrl, username, password, host let cleanUrl, username, password, host
@ -44,85 +75,3 @@ export const getUrlInfo = (url = env.COUCH_DB_URL) => {
}, },
} }
} }
export const getCouchInfo = () => {
const urlInfo = getUrlInfo()
let username
let password
if (env.COUCH_DB_USERNAME) {
// set from env
username = env.COUCH_DB_USERNAME
} else if (urlInfo.auth.username) {
// set from url
username = urlInfo.auth.username
} else if (!env.isTest()) {
throw new Error("CouchDB username not set")
}
if (env.COUCH_DB_PASSWORD) {
// set from env
password = env.COUCH_DB_PASSWORD
} else if (urlInfo.auth.password) {
// set from url
password = urlInfo.auth.password
} else if (!env.isTest()) {
throw new Error("CouchDB password not set")
}
const authCookie = Buffer.from(`${username}:${password}`).toString("base64")
return {
url: urlInfo.url,
auth: {
username: username,
password: password,
},
cookie: `Basic ${authCookie}`,
}
}
/**
* Return a constructor for PouchDB.
* This should be rarely used outside of the main application config.
* Exposed for exceptional cases such as in-memory views.
*/
export const getPouch = (opts: PouchOptions = {}) => {
let { url, cookie } = getCouchInfo()
let POUCH_DB_DEFAULTS = {
prefix: url,
fetch: (url: string, opts: any) => {
// use a specific authorization cookie - be very explicit about how we authenticate
opts.headers.set("Authorization", cookie)
return PouchDB.fetch(url, opts)
},
}
if (opts.inMemory) {
const inMemory = require("pouchdb-adapter-memory")
PouchDB.plugin(inMemory)
POUCH_DB_DEFAULTS = {
prefix: undefined,
// @ts-ignore
adapter: "memory",
}
}
if (opts.onDisk) {
POUCH_DB_DEFAULTS = {
prefix: undefined,
// @ts-ignore
adapter: "leveldb",
}
}
if (opts.replication) {
const replicationStream = require("pouchdb-replication-stream")
PouchDB.plugin(replicationStream.plugin)
// @ts-ignore
PouchDB.adapter("writableStream", replicationStream.adapters.writableStream)
}
if (opts.find) {
const find = require("pouchdb-find")
PouchDB.plugin(find)
}
return PouchDB.defaults(POUCH_DB_DEFAULTS)
}

View File

@ -0,0 +1,4 @@
export * from "./connections"
export * from "./DatabaseImpl"
export * from "./utils"
export { init, getPouch, getPouchDB, closePouchDB } from "./pouchDB"

View File

@ -0,0 +1,97 @@
import PouchDB from "pouchdb"
import env from "../../environment"
import { PouchOptions } from "@budibase/types"
import { getCouchInfo } from "./connections"
let Pouch: any
let initialised = false
/**
* Return a constructor for PouchDB.
* This should be rarely used outside of the main application config.
* Exposed for exceptional cases such as in-memory views.
*/
export const getPouch = (opts: PouchOptions = {}) => {
let { url, cookie } = getCouchInfo()
let POUCH_DB_DEFAULTS = {
prefix: url,
fetch: (url: string, opts: any) => {
// use a specific authorization cookie - be very explicit about how we authenticate
opts.headers.set("Authorization", cookie)
return PouchDB.fetch(url, opts)
},
}
if (opts.inMemory) {
const inMemory = require("pouchdb-adapter-memory")
PouchDB.plugin(inMemory)
POUCH_DB_DEFAULTS = {
// @ts-ignore
adapter: "memory",
}
}
if (opts.onDisk) {
POUCH_DB_DEFAULTS = {
// @ts-ignore
adapter: "leveldb",
}
}
if (opts.replication) {
const replicationStream = require("pouchdb-replication-stream")
PouchDB.plugin(replicationStream.plugin)
// @ts-ignore
PouchDB.adapter("writableStream", replicationStream.adapters.writableStream)
}
if (opts.find) {
const find = require("pouchdb-find")
PouchDB.plugin(find)
}
return PouchDB.defaults(POUCH_DB_DEFAULTS)
}
export function init(opts?: PouchOptions) {
Pouch = getPouch(opts)
initialised = true
}
const checkInitialised = () => {
if (!initialised) {
throw new Error("init has not been called")
}
}
export function getPouchDB(dbName: string, opts?: any): PouchDB.Database {
checkInitialised()
const db = new Pouch(dbName, opts)
const dbPut = db.put
db.put = async (doc: any, options = {}) => {
if (!doc.createdAt) {
doc.createdAt = new Date().toISOString()
}
doc.updatedAt = new Date().toISOString()
return dbPut(doc, options)
}
db.exists = async () => {
const info = await db.info()
return !info.error
}
return db
}
// use this function if you have called getPouchDB - close
// the databases you've opened once finished
export async function closePouchDB(db: PouchDB.Database) {
if (!db || env.isTest()) {
return
}
try {
// specifically await so that if there is an error, it can be ignored
return await db.close()
} catch (err) {
// ignore error, already closed
}
}

View File

@ -0,0 +1,36 @@
import { getCouchInfo } from "./connections"
import fetch from "node-fetch"
import { checkSlashesInUrl } from "../../helpers"
export async function directCouchCall(
path: string,
method: string = "GET",
body?: any
) {
let { url, cookie } = getCouchInfo()
const couchUrl = `${url}/${path}`
const params: any = {
method: method,
headers: {
Authorization: cookie,
},
}
if (body && method !== "GET") {
params.body = JSON.stringify(body)
params.headers["Content-Type"] = "application/json"
}
return await fetch(checkSlashesInUrl(encodeURI(couchUrl)), params)
}
export async function directCouchQuery(
path: string,
method: string = "GET",
body?: any
) {
const response = await directCouchCall(path, method, body)
if (response.status < 300) {
return await response.json()
} else {
throw "Cannot connect to CouchDB instance"
}
}

View File

@ -0,0 +1,46 @@
import env from "../environment"
import { directCouchQuery, getPouchDB } from "./couch"
import { CouchFindOptions, Database } from "@budibase/types"
import { DatabaseImpl } from "../db"
const dbList = new Set()
export function getDB(dbName?: string, opts?: any): Database {
// TODO: once using the test image, need to remove this
if (env.isTest()) {
dbList.add(dbName)
// @ts-ignore
return getPouchDB(dbName, opts)
}
return new DatabaseImpl(dbName, opts)
}
// we have to use a callback for this so that we can close
// the DB when we're done, without this manual requests would
// need to close the database when done with it to avoid memory leaks
export async function doWithDB(dbName: string, cb: any, opts = {}) {
const db = getDB(dbName, opts)
// need this to be async so that we can correctly close DB after all
// async operations have been completed
return await cb(db)
}
export function allDbs() {
if (!env.isTest()) {
throw new Error("Cannot be used outside test environment.")
}
return [...dbList]
}
export async function directCouchAllDbs(queryString?: string) {
let couchPath = "/_all_dbs"
if (queryString) {
couchPath += `?${queryString}`
}
return await directCouchQuery(couchPath)
}
export async function directCouchFind(dbName: string, opts: CouchFindOptions) {
const json = await directCouchQuery(`${dbName}/_find`, "POST", opts)
return { rows: json.docs, bookmark: json.bookmark }
}

View File

@ -1,133 +1,7 @@
import * as pouch from "./pouch" export * from "./couch"
import env from "../environment" export * from "./db"
import { checkSlashesInUrl } from "../helpers" export * from "./utils"
import fetch from "node-fetch" export * from "./views"
import { PouchOptions, CouchFindOptions } from "@budibase/types" export * from "./constants"
import PouchDB from "pouchdb" export * from "./conversions"
export * from "./tenancy"
const openDbs: string[] = []
let Pouch: any
let initialised = false
const dbList = new Set()
if (env.MEMORY_LEAK_CHECK) {
setInterval(() => {
console.log("--- OPEN DBS ---")
console.log(openDbs)
}, 5000)
}
const put =
(dbPut: any) =>
async (doc: any, options = {}) => {
if (!doc.createdAt) {
doc.createdAt = new Date().toISOString()
}
doc.updatedAt = new Date().toISOString()
return dbPut(doc, options)
}
const checkInitialised = () => {
if (!initialised) {
throw new Error("init has not been called")
}
}
export function init(opts?: PouchOptions) {
Pouch = pouch.getPouch(opts)
initialised = true
}
// NOTE: THIS IS A DANGEROUS FUNCTION - USE WITH CAUTION
// this function is prone to leaks, should only be used
// in situations that using the function doWithDB does not work
export function dangerousGetDB(dbName: string, opts?: any): PouchDB.Database {
checkInitialised()
if (env.isTest()) {
dbList.add(dbName)
}
const db = new Pouch(dbName, opts)
if (env.MEMORY_LEAK_CHECK) {
openDbs.push(db.name)
}
const dbPut = db.put
db.put = put(dbPut)
return db
}
// use this function if you have called dangerousGetDB - close
// the databases you've opened once finished
export async function closeDB(db: PouchDB.Database) {
if (!db || env.isTest()) {
return
}
if (env.MEMORY_LEAK_CHECK) {
openDbs.splice(openDbs.indexOf(db.name), 1)
}
try {
// specifically await so that if there is an error, it can be ignored
return await db.close()
} catch (err) {
// ignore error, already closed
}
}
// we have to use a callback for this so that we can close
// the DB when we're done, without this manual requests would
// need to close the database when done with it to avoid memory leaks
export async function doWithDB(dbName: string, cb: any, opts = {}) {
const db = dangerousGetDB(dbName, opts)
// need this to be async so that we can correctly close DB after all
// async operations have been completed
try {
return await cb(db)
} finally {
await closeDB(db)
}
}
export function allDbs() {
if (!env.isTest()) {
throw new Error("Cannot be used outside test environment.")
}
checkInitialised()
return [...dbList]
}
export async function directCouchQuery(
path: string,
method: string = "GET",
body?: any
) {
let { url, cookie } = pouch.getCouchInfo()
const couchUrl = `${url}/${path}`
const params: any = {
method: method,
headers: {
Authorization: cookie,
},
}
if (body && method !== "GET") {
params.body = JSON.stringify(body)
params.headers["Content-Type"] = "application/json"
}
const response = await fetch(checkSlashesInUrl(encodeURI(couchUrl)), params)
if (response.status < 300) {
return await response.json()
} else {
throw "Cannot connect to CouchDB instance"
}
}
export async function directCouchAllDbs(queryString?: string) {
let couchPath = "/_all_dbs"
if (queryString) {
couchPath += `?${queryString}`
}
return await directCouchQuery(couchPath)
}
export async function directCouchFind(dbName: string, opts: CouchFindOptions) {
const json = await directCouchQuery(`${dbName}/_find`, "POST", opts)
return { rows: json.docs, bookmark: json.bookmark }
}

View File

@ -1,11 +1,11 @@
require("../../../tests") require("../../../tests")
const { dangerousGetDB } = require("../") const { getDB } = require("../")
describe("db", () => { describe("db", () => {
describe("getDB", () => { describe("getDB", () => {
it("returns a db", async () => { it("returns a db", async () => {
const db = dangerousGetDB("test") const db = getDB("test")
expect(db).toBeDefined() expect(db).toBeDefined()
expect(db._adapter).toBe("memory") expect(db._adapter).toBe("memory")
expect(db.prefix).toBe("_pouch_") expect(db.prefix).toBe("_pouch_")
@ -13,7 +13,7 @@ describe("db", () => {
}) })
it("uses the custom put function", async () => { it("uses the custom put function", async () => {
const db = dangerousGetDB("test") const db = getDB("test")
let doc = { _id: "test" } let doc = { _id: "test" }
await db.put(doc) await db.put(doc)
doc = await db.get(doc._id) doc = await db.get(doc._id)

View File

@ -1,5 +1,5 @@
require("../../../tests") require("../../../tests")
const getUrlInfo = require("../pouch").getUrlInfo const getUrlInfo = require("../couch").getUrlInfo
describe("pouch", () => { describe("pouch", () => {
describe("Couch DB URL parsing", () => { describe("Couch DB URL parsing", () => {

View File

@ -1,4 +1,4 @@
require("../../../tests"); require("../../../tests")
const { const {
generateAppID, generateAppID,
getDevelopmentAppID, getDevelopmentAppID,
@ -8,8 +8,8 @@ const {
getPlatformUrl, getPlatformUrl,
getScopedConfig getScopedConfig
} = require("../utils") } = require("../utils")
const tenancy = require("../../tenancy"); const tenancy = require("../../tenancy")
const { Configs, DEFAULT_TENANT_ID } = require("../../constants"); const { Config, DEFAULT_TENANT_ID } = require("../../constants")
const env = require("../../environment") const env = require("../../environment")
describe("utils", () => { describe("utils", () => {
@ -77,7 +77,7 @@ const setDbPlatformUrl = async () => {
const db = tenancy.getGlobalDB() const db = tenancy.getGlobalDB()
db.put({ db.put({
_id: "config_settings", _id: "config_settings",
type: Configs.SETTINGS, type: Config.SETTINGS,
config: { config: {
platformUrl: DB_URL platformUrl: DB_URL
} }
@ -178,7 +178,7 @@ describe("getScopedConfig", () => {
await tenancy.doInTenant(DEFAULT_TENANT_ID, async () => { await tenancy.doInTenant(DEFAULT_TENANT_ID, async () => {
await setDbPlatformUrl() await setDbPlatformUrl()
const db = tenancy.getGlobalDB() const db = tenancy.getGlobalDB()
const config = await getScopedConfig(db, { type: Configs.SETTINGS }) const config = await getScopedConfig(db, { type: Config.SETTINGS })
expect(config.platformUrl).toBe(DB_URL) expect(config.platformUrl).toBe(DB_URL)
}) })
}) })
@ -186,7 +186,7 @@ describe("getScopedConfig", () => {
it("returns the platform url without an existing config", async () => { it("returns the platform url without an existing config", async () => {
await tenancy.doInTenant(DEFAULT_TENANT_ID, async () => { await tenancy.doInTenant(DEFAULT_TENANT_ID, async () => {
const db = tenancy.getGlobalDB() const db = tenancy.getGlobalDB()
const config = await getScopedConfig(db, { type: Configs.SETTINGS }) const config = await getScopedConfig(db, { type: Config.SETTINGS })
expect(config.platformUrl).toBe(DEFAULT_URL) expect(config.platformUrl).toBe(DEFAULT_URL)
}) })
}) })

View File

@ -1,5 +1,5 @@
import { newid } from "../hashing" import { newid } from "../hashing"
import { DEFAULT_TENANT_ID, Configs } from "../constants" import { DEFAULT_TENANT_ID, Config } from "../constants"
import env from "../environment" import env from "../environment"
import { import {
SEPARATOR, SEPARATOR,
@ -10,12 +10,12 @@ import {
} from "./constants" } from "./constants"
import { getTenantId, getGlobalDB } from "../context" import { getTenantId, getGlobalDB } from "../context"
import { getGlobalDBName } from "./tenancy" import { getGlobalDBName } from "./tenancy"
import { doWithDB, allDbs, directCouchAllDbs } from "./index" import { doWithDB, allDbs, directCouchAllDbs } from "./db"
import { getAppMetadata } from "../cache/appMetadata" import { getAppMetadata } from "../cache/appMetadata"
import { isDevApp, isDevAppID, getProdAppID } from "./conversions" import { isDevApp, isDevAppID, getProdAppID } from "./conversions"
import { APP_PREFIX } from "./constants" import { APP_PREFIX } from "./constants"
import * as events from "../events" import * as events from "../events"
import { App } from "@budibase/types" import { App, Database } from "@budibase/types"
export * from "./constants" export * from "./constants"
export * from "./conversions" export * from "./conversions"
@ -26,7 +26,7 @@ export * from "./tenancy"
* Generates a new app ID. * Generates a new app ID.
* @returns {string} The new app ID which the app doc can be stored under. * @returns {string} The new app ID which the app doc can be stored under.
*/ */
export const generateAppID = (tenantId = null) => { export const generateAppID = (tenantId?: string | null) => {
let id = APP_PREFIX let id = APP_PREFIX
if (tenantId) { if (tenantId) {
id += `${tenantId}${SEPARATOR}` id += `${tenantId}${SEPARATOR}`
@ -251,11 +251,11 @@ export function generateRoleID(id: any) {
/** /**
* Gets parameters for retrieving a role, this is a utility function for the getDocParams function. * Gets parameters for retrieving a role, this is a utility function for the getDocParams function.
*/ */
export function getRoleParams(roleId = null, otherProps = {}) { export function getRoleParams(roleId?: string | null, otherProps = {}) {
return getDocParams(DocumentType.ROLE, roleId, otherProps) return getDocParams(DocumentType.ROLE, roleId, otherProps)
} }
export function getStartEndKeyURL(baseKey: any, tenantId = null) { export function getStartEndKeyURL(baseKey: any, tenantId?: string) {
const tenancy = tenantId ? `${SEPARATOR}${tenantId}` : "" const tenancy = tenantId ? `${SEPARATOR}${tenantId}` : ""
return `startkey="${baseKey}${tenancy}"&endkey="${baseKey}${tenancy}${UNICODE_MAX}"` return `startkey="${baseKey}${tenancy}"&endkey="${baseKey}${tenancy}${UNICODE_MAX}"`
} }
@ -392,20 +392,10 @@ export async function getDevAppIDs() {
} }
export async function dbExists(dbName: any) { export async function dbExists(dbName: any) {
let exists = false
return doWithDB( return doWithDB(
dbName, dbName,
async (db: any) => { async (db: Database) => {
try { return await db.exists()
// check if database exists
const info = await db.info()
if (info && !info.error) {
exists = true
}
} catch (err) {
exists = false
}
return exists
}, },
{ skip_setup: true } { skip_setup: true }
) )
@ -504,7 +494,7 @@ export const getScopedFullConfig = async function (
)[0] )[0]
// custom logic for settings doc // custom logic for settings doc
if (type === Configs.SETTINGS) { if (type === Config.SETTINGS) {
if (scopedConfig && scopedConfig.doc) { if (scopedConfig && scopedConfig.doc) {
// overrides affected by environment variables // overrides affected by environment variables
scopedConfig.doc.config.platformUrl = await getPlatformUrl({ scopedConfig.doc.config.platformUrl = await getPlatformUrl({
@ -543,7 +533,7 @@ export const getPlatformUrl = async (opts = { tenantAware: true }) => {
// get the doc directly instead of with getScopedConfig to prevent loop // get the doc directly instead of with getScopedConfig to prevent loop
let settings let settings
try { try {
settings = await db.get(generateConfigID({ type: Configs.SETTINGS })) settings = await db.get(generateConfigID({ type: Config.SETTINGS }))
} catch (e: any) { } catch (e: any) {
if (e.status !== 404) { if (e.status !== 404) {
throw e throw e

View File

@ -1,8 +1,8 @@
import { DocumentType, ViewName, DeprecatedViews, SEPARATOR } from "./utils" import { DocumentType, ViewName, DeprecatedViews, SEPARATOR } from "./utils"
import { getGlobalDB } from "../context" import { getGlobalDB } from "../context"
import PouchDB from "pouchdb"
import { StaticDatabases } from "./constants" import { StaticDatabases } from "./constants"
import { doWithDB } from "./" import { doWithDB } from "./"
import { Database, DatabaseQueryOpts } from "@budibase/types"
const DESIGN_DB = "_design/database" const DESIGN_DB = "_design/database"
@ -19,7 +19,7 @@ interface DesignDocument {
views: any views: any
} }
async function removeDeprecated(db: PouchDB.Database, viewName: ViewName) { async function removeDeprecated(db: Database, viewName: ViewName) {
// @ts-ignore // @ts-ignore
if (!DeprecatedViews[viewName]) { if (!DeprecatedViews[viewName]) {
return return
@ -70,16 +70,13 @@ export const createAccountEmailView = async () => {
emit(doc.email.toLowerCase(), doc._id) emit(doc.email.toLowerCase(), doc._id)
} }
}` }`
await doWithDB( await doWithDB(StaticDatabases.PLATFORM_INFO.name, async (db: Database) => {
StaticDatabases.PLATFORM_INFO.name, await createView(db, viewJs, ViewName.ACCOUNT_BY_EMAIL)
async (db: PouchDB.Database) => { })
await createView(db, viewJs, ViewName.ACCOUNT_BY_EMAIL)
}
)
} }
export const createUserAppView = async () => { export const createUserAppView = async () => {
const db = getGlobalDB() as PouchDB.Database const db = getGlobalDB()
const viewJs = `function(doc) { const viewJs = `function(doc) {
if (doc._id.startsWith("${DocumentType.USER}${SEPARATOR}") && doc.roles) { if (doc._id.startsWith("${DocumentType.USER}${SEPARATOR}") && doc.roles) {
for (let prodAppId of Object.keys(doc.roles)) { for (let prodAppId of Object.keys(doc.roles)) {
@ -117,12 +114,9 @@ export const createPlatformUserView = async () => {
emit(doc._id.toLowerCase(), doc._id) emit(doc._id.toLowerCase(), doc._id)
} }
}` }`
await doWithDB( await doWithDB(StaticDatabases.PLATFORM_INFO.name, async (db: Database) => {
StaticDatabases.PLATFORM_INFO.name, await createView(db, viewJs, ViewName.PLATFORM_USERS_LOWERCASE)
async (db: PouchDB.Database) => { })
await createView(db, viewJs, ViewName.PLATFORM_USERS_LOWERCASE)
}
)
} }
export interface QueryViewOptions { export interface QueryViewOptions {
@ -131,22 +125,24 @@ export interface QueryViewOptions {
export const queryView = async <T>( export const queryView = async <T>(
viewName: ViewName, viewName: ViewName,
params: PouchDB.Query.Options<T, T>, params: DatabaseQueryOpts,
db: PouchDB.Database, db: Database,
createFunc: any, createFunc: any,
opts?: QueryViewOptions opts?: QueryViewOptions
): Promise<T[] | T | undefined> => { ): Promise<T[] | T | undefined> => {
try { try {
let response = await db.query<T, T>(`database/${viewName}`, params) let response = await db.query<T>(`database/${viewName}`, params)
const rows = response.rows const rows = response.rows
const docs = rows.map(row => (params.include_docs ? row.doc : row.value)) const docs = rows.map((row: any) =>
params.include_docs ? row.doc : row.value
)
// if arrayResponse has been requested, always return array regardless of length // if arrayResponse has been requested, always return array regardless of length
if (opts?.arrayResponse) { if (opts?.arrayResponse) {
return docs return docs as T[]
} else { } else {
// return the single document if there is only one // return the single document if there is only one
return docs.length <= 1 ? docs[0] : docs return docs.length <= 1 ? (docs[0] as T) : (docs as T[])
} }
} catch (err: any) { } catch (err: any) {
if (err != null && err.name === "not_found") { if (err != null && err.name === "not_found") {
@ -161,7 +157,7 @@ export const queryView = async <T>(
export const queryPlatformView = async <T>( export const queryPlatformView = async <T>(
viewName: ViewName, viewName: ViewName,
params: PouchDB.Query.Options<T, T>, params: DatabaseQueryOpts,
opts?: QueryViewOptions opts?: QueryViewOptions
): Promise<T[] | T | undefined> => { ): Promise<T[] | T | undefined> => {
const CreateFuncByName: any = { const CreateFuncByName: any = {
@ -169,19 +165,16 @@ export const queryPlatformView = async <T>(
[ViewName.PLATFORM_USERS_LOWERCASE]: createPlatformUserView, [ViewName.PLATFORM_USERS_LOWERCASE]: createPlatformUserView,
} }
return doWithDB( return doWithDB(StaticDatabases.PLATFORM_INFO.name, async (db: Database) => {
StaticDatabases.PLATFORM_INFO.name, const createFn = CreateFuncByName[viewName]
async (db: PouchDB.Database) => { return queryView(viewName, params, db, createFn, opts)
const createFn = CreateFuncByName[viewName] })
return queryView(viewName, params, db, createFn, opts)
}
)
} }
export const queryGlobalView = async <T>( export const queryGlobalView = async <T>(
viewName: ViewName, viewName: ViewName,
params: PouchDB.Query.Options<T, T>, params: DatabaseQueryOpts,
db?: PouchDB.Database, db?: Database,
opts?: QueryViewOptions opts?: QueryViewOptions
): Promise<T[] | T | undefined> => { ): Promise<T[] | T | undefined> => {
const CreateFuncByName: any = { const CreateFuncByName: any = {
@ -192,8 +185,8 @@ export const queryGlobalView = async <T>(
} }
// can pass DB in if working with something specific // can pass DB in if working with something specific
if (!db) { if (!db) {
db = getGlobalDB() as PouchDB.Database db = getGlobalDB()
} }
const createFn = CreateFuncByName[viewName] const createFn = CreateFuncByName[viewName]
return queryView(viewName, params, db, createFn, opts) return queryView(viewName, params, db!, createFn, opts)
} }

View File

@ -69,7 +69,6 @@ const env = {
DISABLE_DEVELOPER_LICENSE: process.env.DISABLE_DEVELOPER_LICENSE, DISABLE_DEVELOPER_LICENSE: process.env.DISABLE_DEVELOPER_LICENSE,
DEFAULT_LICENSE: process.env.DEFAULT_LICENSE, DEFAULT_LICENSE: process.env.DEFAULT_LICENSE,
SERVICE: process.env.SERVICE || "budibase", SERVICE: process.env.SERVICE || "budibase",
MEMORY_LEAK_CHECK: process.env.MEMORY_LEAK_CHECK || false,
LOG_LEVEL: process.env.LOG_LEVEL, LOG_LEVEL: process.env.LOG_LEVEL,
SESSION_UPDATE_PERIOD: process.env.SESSION_UPDATE_PERIOD, SESSION_UPDATE_PERIOD: process.env.SESSION_UPDATE_PERIOD,
DEPLOYMENT_ENVIRONMENT: DEPLOYMENT_ENVIRONMENT:

View File

@ -1,7 +1,7 @@
import env from "../environment" import env from "../environment"
import tenancy from "../tenancy" import tenancy from "../tenancy"
import * as dbUtils from "../db/utils" import * as dbUtils from "../db/utils"
import { Configs } from "../constants" import { Config } from "../constants"
import { withCache, TTL, CacheKeys } from "../cache/generic" import { withCache, TTL, CacheKeys } from "../cache/generic"
export const enabled = async () => { export const enabled = async () => {
@ -45,9 +45,7 @@ const getSettingsDoc = async () => {
const db = tenancy.getGlobalDB() const db = tenancy.getGlobalDB()
let settings let settings
try { try {
settings = await db.get( settings = await db.get(dbUtils.generateConfigID({ type: Config.SETTINGS }))
dbUtils.generateConfigID({ type: Configs.SETTINGS })
)
} catch (e: any) { } catch (e: any) {
if (e.status !== 404) { if (e.status !== 404) {
throw e throw e

View File

@ -19,7 +19,7 @@ import {
} from "@budibase/types" } from "@budibase/types"
import { processors } from "./processors" import { processors } from "./processors"
import * as dbUtils from "../db/utils" import * as dbUtils from "../db/utils"
import { Configs } from "../constants" import { Config } from "../constants"
import * as hashing from "../hashing" import * as hashing from "../hashing"
import * as installation from "../installation" import * as installation from "../installation"
import { withCache, TTL, CacheKeys } from "../cache/generic" import { withCache, TTL, CacheKeys } from "../cache/generic"
@ -273,7 +273,7 @@ const getUniqueTenantId = async (tenantId: string): Promise<string> => {
return withCache(CacheKeys.UNIQUE_TENANT_ID, TTL.ONE_DAY, async () => { return withCache(CacheKeys.UNIQUE_TENANT_ID, TTL.ONE_DAY, async () => {
const db = context.getGlobalDB() const db = context.getGlobalDB()
const config: SettingsConfig = await dbUtils.getScopedFullConfig(db, { const config: SettingsConfig = await dbUtils.getScopedFullConfig(db, {
type: Configs.SETTINGS, type: Config.SETTINGS,
}) })
let uniqueTenantId: string let uniqueTenantId: string

View File

@ -13,7 +13,7 @@ import featureFlags from "./featureFlags"
import * as sessions from "./security/sessions" import * as sessions from "./security/sessions"
import * as deprovisioning from "./context/deprovision" import * as deprovisioning from "./context/deprovision"
import auth from "./auth" import auth from "./auth"
import constants from "./constants" import * as constants from "./constants"
import * as dbConstants from "./db/constants" import * as dbConstants from "./db/constants"
import * as logging from "./logging" import * as logging from "./logging"
import pino from "./pino" import pino from "./pino"
@ -21,9 +21,9 @@ import * as middleware from "./middleware"
import plugins from "./plugin" import plugins from "./plugin"
import encryption from "./security/encryption" import encryption from "./security/encryption"
import * as queue from "./queue" import * as queue from "./queue"
import * as db from "./db"
// mimic the outer package exports // mimic the outer package exports
import * as db from "./pkg/db"
import * as objectStore from "./pkg/objectStore" import * as objectStore from "./pkg/objectStore"
import * as utils from "./pkg/utils" import * as utils from "./pkg/utils"
import redis from "./pkg/redis" import redis from "./pkg/redis"

View File

@ -1,11 +1,9 @@
import { Cookies, Headers } from "../constants" import { Cookie, Header } from "../constants"
import { getCookie, clearCookie, openJwt } from "../utils" import { getCookie, clearCookie, openJwt } from "../utils"
import { getUser } from "../cache/user" import { getUser } from "../cache/user"
import { getSession, updateSessionTTL } from "../security/sessions" import { getSession, updateSessionTTL } from "../security/sessions"
import { buildMatcherRegex, matches } from "./matchers" import { buildMatcherRegex, matches } from "./matchers"
import { SEPARATOR } from "../db/constants" import { SEPARATOR, queryGlobalView, ViewName } from "../db"
import { ViewName } from "../db/utils"
import { queryGlobalView } from "../db/views"
import { getGlobalDB, doInTenant } from "../tenancy" import { getGlobalDB, doInTenant } from "../tenancy"
import { decrypt } from "../security/encryption" import { decrypt } from "../security/encryption"
const identity = require("../context/identity") const identity = require("../context/identity")
@ -74,7 +72,7 @@ export = (
const noAuthOptions = noAuthPatterns ? buildMatcherRegex(noAuthPatterns) : [] const noAuthOptions = noAuthPatterns ? buildMatcherRegex(noAuthPatterns) : []
return async (ctx: any, next: any) => { return async (ctx: any, next: any) => {
let publicEndpoint = false let publicEndpoint = false
const version = ctx.request.headers[Headers.API_VER] const version = ctx.request.headers[Header.API_VER]
// the path is not authenticated // the path is not authenticated
const found = matches(ctx, noAuthOptions) const found = matches(ctx, noAuthOptions)
if (found) { if (found) {
@ -82,10 +80,10 @@ export = (
} }
try { try {
// check the actual user is authenticated first, try header or cookie // check the actual user is authenticated first, try header or cookie
const headerToken = ctx.request.headers[Headers.TOKEN] const headerToken = ctx.request.headers[Header.TOKEN]
const authCookie = getCookie(ctx, Cookies.Auth) || openJwt(headerToken) const authCookie = getCookie(ctx, Cookie.Auth) || openJwt(headerToken)
const apiKey = ctx.request.headers[Headers.API_KEY] const apiKey = ctx.request.headers[Header.API_KEY]
const tenantId = ctx.request.headers[Headers.TENANT_ID] const tenantId = ctx.request.headers[Header.TENANT_ID]
let authenticated = false, let authenticated = false,
user = null, user = null,
internal = false internal = false
@ -116,7 +114,7 @@ export = (
authenticated = false authenticated = false
console.error("Auth Error", err?.message || err) console.error("Auth Error", err?.message || err)
// remove the cookie as the user does not exist anymore // remove the cookie as the user does not exist anymore
clearCookie(ctx, Cookies.Auth) clearCookie(ctx, Cookie.Auth)
} }
} }
// this is an internal request, no user made it // this is an internal request, no user made it
@ -140,7 +138,7 @@ export = (
delete user.password delete user.password
} }
// be explicit // be explicit
if (authenticated !== true) { if (!authenticated) {
authenticated = false authenticated = false
} }
// isAuthenticated is a function, so use a variable to be able to check authed state // isAuthenticated is a function, so use a variable to be able to check authed state
@ -155,7 +153,7 @@ export = (
console.error("Auth Error", err?.message || err) console.error("Auth Error", err?.message || err)
// invalid token, clear the cookie // invalid token, clear the cookie
if (err && err.name === "JsonWebTokenError") { if (err && err.name === "JsonWebTokenError") {
clearCookie(ctx, Cookies.Auth) clearCookie(ctx, Cookie.Auth)
} }
// allow configuring for public access // allow configuring for public access
if ((opts && opts.publicAllowed) || publicEndpoint) { if ((opts && opts.publicAllowed) || publicEndpoint) {

View File

@ -1,4 +1,4 @@
const { Headers } = require("../constants") const { Header } = require("../constants")
const { buildMatcherRegex, matches } = require("./matchers") const { buildMatcherRegex, matches } = require("./matchers")
/** /**
@ -68,7 +68,7 @@ module.exports = (opts = { noCsrfPatterns: [] }) => {
} }
// reject if no token in request or mismatch // reject if no token in request or mismatch
const requestToken = ctx.get(Headers.CSRF_TOKEN) const requestToken = ctx.get(Header.CSRF_TOKEN)
if (!requestToken || requestToken !== userToken) { if (!requestToken || requestToken !== userToken) {
ctx.throw(403, "Invalid CSRF token") ctx.throw(403, "Invalid CSRF token")
} }

View File

@ -1,11 +1,11 @@
const env = require("../environment") const env = require("../environment")
const { Headers } = require("../constants") const { Header } = require("../constants")
/** /**
* API Key only endpoint. * API Key only endpoint.
*/ */
module.exports = async (ctx, next) => { module.exports = async (ctx, next) => {
const apiKey = ctx.request.headers[Headers.API_KEY] const apiKey = ctx.request.headers[Header.API_KEY]
if (apiKey !== env.INTERNAL_API_KEY) { if (apiKey !== env.INTERNAL_API_KEY) {
ctx.throw(403, "Unauthorized") ctx.throw(403, "Unauthorized")
} }

View File

@ -1,6 +1,6 @@
const google = require("../google") const google = require("../google")
const GoogleStrategy = require("passport-google-oauth").OAuth2Strategy const GoogleStrategy = require("passport-google-oauth").OAuth2Strategy
const { Cookies, Configs } = require("../../../constants") const { Cookie, Config } = require("../../../constants")
const { clearCookie, getCookie } = require("../../../utils") const { clearCookie, getCookie } = require("../../../utils")
const { getScopedConfig, getPlatformUrl } = require("../../../db/utils") const { getScopedConfig, getPlatformUrl } = require("../../../db/utils")
const { doWithDB } = require("../../../db") const { doWithDB } = require("../../../db")
@ -11,7 +11,7 @@ async function fetchGoogleCreds() {
// try and get the config from the tenant // try and get the config from the tenant
const db = getGlobalDB() const db = getGlobalDB()
const googleConfig = await getScopedConfig(db, { const googleConfig = await getScopedConfig(db, {
type: Configs.GOOGLE, type: Config.GOOGLE,
}) })
// or fall back to env variables // or fall back to env variables
return ( return (
@ -47,7 +47,7 @@ async function postAuth(passport, ctx, next) {
const platformUrl = await getPlatformUrl({ tenantAware: false }) const platformUrl = await getPlatformUrl({ tenantAware: false })
let callbackUrl = `${platformUrl}/api/global/auth/datasource/google/callback` let callbackUrl = `${platformUrl}/api/global/auth/datasource/google/callback`
const authStateCookie = getCookie(ctx, Cookies.DatasourceAuth) const authStateCookie = getCookie(ctx, Cookie.DatasourceAuth)
return passport.authenticate( return passport.authenticate(
new GoogleStrategy( new GoogleStrategy(
@ -57,7 +57,7 @@ async function postAuth(passport, ctx, next) {
callbackURL: callbackUrl, callbackURL: callbackUrl,
}, },
(accessToken, refreshToken, profile, done) => { (accessToken, refreshToken, profile, done) => {
clearCookie(ctx, Cookies.DatasourceAuth) clearCookie(ctx, Cookie.DatasourceAuth)
done(null, { accessToken, refreshToken }) done(null, { accessToken, refreshToken })
} }
), ),

View File

@ -1,7 +1,7 @@
const GoogleStrategy = require("passport-google-oauth").OAuth2Strategy const GoogleStrategy = require("passport-google-oauth").OAuth2Strategy
const { ssoCallbackUrl } = require("./utils") const { ssoCallbackUrl } = require("./utils")
const { authenticateThirdParty } = require("./third-party-common") const { authenticateThirdParty } = require("./third-party-common")
const { Configs } = require("../../../constants") const { Config } = require("../../../constants")
const buildVerifyFn = saveUserFn => { const buildVerifyFn = saveUserFn => {
return (accessToken, refreshToken, profile, done) => { return (accessToken, refreshToken, profile, done) => {
@ -60,7 +60,7 @@ exports.strategyFactory = async function (config, callbackUrl, saveUserFn) {
} }
exports.getCallbackUrl = async function (db, config) { exports.getCallbackUrl = async function (db, config) {
return ssoCallbackUrl(db, config, Configs.GOOGLE) return ssoCallbackUrl(db, config, Config.GOOGLE)
} }
// expose for testing // expose for testing

View File

@ -1,11 +1,11 @@
const { Cookies } = require("../../constants") const { Cookie } = require("../../constants")
const env = require("../../environment") const env = require("../../environment")
const { authError } = require("./utils") const { authError } = require("./utils")
exports.options = { exports.options = {
secretOrKey: env.JWT_SECRET, secretOrKey: env.JWT_SECRET,
jwtFromRequest: function (ctx) { jwtFromRequest: function (ctx) {
return ctx.cookies.get(Cookies.Auth) return ctx.cookies.get(Cookie.Auth)
}, },
} }

View File

@ -2,7 +2,7 @@ const fetch = require("node-fetch")
const OIDCStrategy = require("@techpass/passport-openidconnect").Strategy const OIDCStrategy = require("@techpass/passport-openidconnect").Strategy
const { authenticateThirdParty } = require("./third-party-common") const { authenticateThirdParty } = require("./third-party-common")
const { ssoCallbackUrl } = require("./utils") const { ssoCallbackUrl } = require("./utils")
const { Configs } = require("../../../constants") const { Config } = require("../../../constants")
const buildVerifyFn = saveUserFn => { const buildVerifyFn = saveUserFn => {
/** /**
@ -140,7 +140,7 @@ exports.fetchStrategyConfig = async function (enrichedConfig, callbackUrl) {
} }
exports.getCallbackUrl = async function (db, config) { exports.getCallbackUrl = async function (db, config) {
return ssoCallbackUrl(db, config, Configs.OIDC) return ssoCallbackUrl(db, config, Config.OIDC)
} }
// expose for testing // expose for testing

View File

@ -1,6 +1,6 @@
const { isMultiTenant, getTenantId } = require("../../tenancy") const { isMultiTenant, getTenantId } = require("../../tenancy")
const { getScopedConfig } = require("../../db/utils") const { getScopedConfig } = require("../../db/utils")
const { Configs } = require("../../constants") const { Config } = require("../../constants")
/** /**
* Utility to handle authentication errors. * Utility to handle authentication errors.
@ -24,7 +24,7 @@ exports.ssoCallbackUrl = async (db, config, type) => {
return config.callbackURL return config.callbackURL
} }
const publicConfig = await getScopedConfig(db, { const publicConfig = await getScopedConfig(db, {
type: Configs.SETTINGS, type: Config.SETTINGS,
}) })
let callbackUrl = `/api/global/auth` let callbackUrl = `/api/global/auth`

View File

@ -1,6 +1,6 @@
import { doInTenant, getTenantIDFromCtx } from "../tenancy" import { doInTenant, getTenantIDFromCtx } from "../tenancy"
import { buildMatcherRegex, matches } from "./matchers" import { buildMatcherRegex, matches } from "./matchers"
import { Headers } from "../constants" import { Header } from "../constants"
import { import {
BBContext, BBContext,
EndpointMatcher, EndpointMatcher,
@ -29,7 +29,7 @@ const tenancy = (
} }
const tenantId = getTenantIDFromCtx(ctx, tenantOpts) const tenantId = getTenantIDFromCtx(ctx, tenantOpts)
ctx.set(Headers.TENANT_ID, tenantId as string) ctx.set(Header.TENANT_ID, tenantId as string)
return doInTenant(tenantId, next) return doInTenant(tenantId, next)
} }
} }

View File

@ -42,7 +42,7 @@ export const runMigration = async (
options: MigrationOptions = {} options: MigrationOptions = {}
) => { ) => {
const migrationType = migration.type const migrationType = migration.type
let tenantId: string let tenantId: string | undefined
if (migrationType !== MigrationType.INSTALLATION) { if (migrationType !== MigrationType.INSTALLATION) {
tenantId = getTenantId() tenantId = getTenantId()
} }

View File

@ -1,6 +1,6 @@
require("../../../tests") require("../../../tests")
const { runMigrations, getMigrationsDoc } = require("../index") const { runMigrations, getMigrationsDoc } = require("../index")
const { dangerousGetDB } = require("../../db") const { getDB } = require("../../db")
const { const {
StaticDatabases, StaticDatabases,
} = require("../../db/utils") } = require("../../db/utils")
@ -18,7 +18,7 @@ describe("migrations", () => {
}] }]
beforeEach(() => { beforeEach(() => {
db = dangerousGetDB(StaticDatabases.GLOBAL.name) db = getDB(StaticDatabases.GLOBAL.name)
}) })
afterEach(async () => { afterEach(async () => {

View File

@ -3,9 +3,11 @@
import * as generic from "../cache/generic" import * as generic from "../cache/generic"
import * as user from "../cache/user" import * as user from "../cache/user"
import * as app from "../cache/appMetadata" import * as app from "../cache/appMetadata"
import * as writethrough from "../cache/writethrough"
export = { export = {
app, app,
user, user,
writethrough,
...generic, ...generic,
} }

View File

@ -1,7 +0,0 @@
// Mimic the outer package export for usage in index.ts
// The outer exports can't be used as they now reference dist directly
export * from "../db"
export * from "../db/utils"
export * from "../db/views"
export * from "../db/pouch"
export * from "../db/constants"

View File

@ -1,164 +0,0 @@
const { flatten } = require("lodash")
const { cloneDeep } = require("lodash/fp")
const PermissionLevels = {
READ: "read",
WRITE: "write",
EXECUTE: "execute",
ADMIN: "admin",
}
// these are the global types, that govern the underlying default behaviour
const PermissionTypes = {
APP: "app",
TABLE: "table",
USER: "user",
AUTOMATION: "automation",
WEBHOOK: "webhook",
BUILDER: "builder",
VIEW: "view",
QUERY: "query",
}
function Permission(type, level) {
this.level = level
this.type = type
}
function levelToNumber(perm) {
switch (perm) {
// not everything has execute privileges
case PermissionLevels.EXECUTE:
return 0
case PermissionLevels.READ:
return 1
case PermissionLevels.WRITE:
return 2
case PermissionLevels.ADMIN:
return 3
default:
return -1
}
}
/**
* Given the specified permission level for the user return the levels they are allowed to carry out.
* @param {string} userPermLevel The permission level of the user.
* @return {string[]} All the permission levels this user is allowed to carry out.
*/
function getAllowedLevels(userPermLevel) {
switch (userPermLevel) {
case PermissionLevels.EXECUTE:
return [PermissionLevels.EXECUTE]
case PermissionLevels.READ:
return [PermissionLevels.EXECUTE, PermissionLevels.READ]
case PermissionLevels.WRITE:
case PermissionLevels.ADMIN:
return [
PermissionLevels.READ,
PermissionLevels.WRITE,
PermissionLevels.EXECUTE,
]
default:
return []
}
}
exports.BUILTIN_PERMISSION_IDS = {
PUBLIC: "public",
READ_ONLY: "read_only",
WRITE: "write",
ADMIN: "admin",
POWER: "power",
}
const BUILTIN_PERMISSIONS = {
PUBLIC: {
_id: exports.BUILTIN_PERMISSION_IDS.PUBLIC,
name: "Public",
permissions: [
new Permission(PermissionTypes.WEBHOOK, PermissionLevels.EXECUTE),
],
},
READ_ONLY: {
_id: exports.BUILTIN_PERMISSION_IDS.READ_ONLY,
name: "Read only",
permissions: [
new Permission(PermissionTypes.QUERY, PermissionLevels.READ),
new Permission(PermissionTypes.TABLE, PermissionLevels.READ),
new Permission(PermissionTypes.VIEW, PermissionLevels.READ),
],
},
WRITE: {
_id: exports.BUILTIN_PERMISSION_IDS.WRITE,
name: "Read/Write",
permissions: [
new Permission(PermissionTypes.QUERY, PermissionLevels.WRITE),
new Permission(PermissionTypes.TABLE, PermissionLevels.WRITE),
new Permission(PermissionTypes.VIEW, PermissionLevels.READ),
new Permission(PermissionTypes.AUTOMATION, PermissionLevels.EXECUTE),
],
},
POWER: {
_id: exports.BUILTIN_PERMISSION_IDS.POWER,
name: "Power",
permissions: [
new Permission(PermissionTypes.TABLE, PermissionLevels.WRITE),
new Permission(PermissionTypes.USER, PermissionLevels.READ),
new Permission(PermissionTypes.AUTOMATION, PermissionLevels.EXECUTE),
new Permission(PermissionTypes.VIEW, PermissionLevels.READ),
new Permission(PermissionTypes.WEBHOOK, PermissionLevels.READ),
],
},
ADMIN: {
_id: exports.BUILTIN_PERMISSION_IDS.ADMIN,
name: "Admin",
permissions: [
new Permission(PermissionTypes.TABLE, PermissionLevels.ADMIN),
new Permission(PermissionTypes.USER, PermissionLevels.ADMIN),
new Permission(PermissionTypes.AUTOMATION, PermissionLevels.ADMIN),
new Permission(PermissionTypes.VIEW, PermissionLevels.ADMIN),
new Permission(PermissionTypes.WEBHOOK, PermissionLevels.READ),
new Permission(PermissionTypes.QUERY, PermissionLevels.ADMIN),
],
},
}
exports.getBuiltinPermissions = () => {
return cloneDeep(BUILTIN_PERMISSIONS)
}
exports.getBuiltinPermissionByID = id => {
const perms = Object.values(BUILTIN_PERMISSIONS)
return perms.find(perm => perm._id === id)
}
exports.doesHaveBasePermission = (permType, permLevel, rolesHierarchy) => {
const basePermissions = [
...new Set(rolesHierarchy.map(role => role.permissionId)),
]
const builtins = Object.values(BUILTIN_PERMISSIONS)
let permissions = flatten(
builtins
.filter(builtin => basePermissions.indexOf(builtin._id) !== -1)
.map(builtin => builtin.permissions)
)
for (let permission of permissions) {
if (
permission.type === permType &&
getAllowedLevels(permission.level).indexOf(permLevel) !== -1
) {
return true
}
}
return false
}
exports.isPermissionLevelHigherThanRead = level => {
return levelToNumber(level) > 1
}
// utility as a lot of things need simply the builder permission
exports.BUILDER = PermissionTypes.BUILDER
exports.PermissionTypes = PermissionTypes
exports.PermissionLevels = PermissionLevels

View File

@ -0,0 +1,175 @@
const { flatten } = require("lodash")
const { cloneDeep } = require("lodash/fp")
export type RoleHierarchy = {
permissionId: string
}[]
export enum PermissionLevel {
READ = "read",
WRITE = "write",
EXECUTE = "execute",
ADMIN = "admin",
}
// these are the global types, that govern the underlying default behaviour
export enum PermissionType {
APP = "app",
TABLE = "table",
USER = "user",
AUTOMATION = "automation",
WEBHOOK = "webhook",
BUILDER = "builder",
VIEW = "view",
QUERY = "query",
}
class Permission {
type: PermissionType
level: PermissionLevel
constructor(type: PermissionType, level: PermissionLevel) {
this.type = type
this.level = level
}
}
function levelToNumber(perm: PermissionLevel) {
switch (perm) {
// not everything has execute privileges
case PermissionLevel.EXECUTE:
return 0
case PermissionLevel.READ:
return 1
case PermissionLevel.WRITE:
return 2
case PermissionLevel.ADMIN:
return 3
default:
return -1
}
}
/**
* Given the specified permission level for the user return the levels they are allowed to carry out.
* @param {string} userPermLevel The permission level of the user.
* @return {string[]} All the permission levels this user is allowed to carry out.
*/
function getAllowedLevels(userPermLevel: PermissionLevel) {
switch (userPermLevel) {
case PermissionLevel.EXECUTE:
return [PermissionLevel.EXECUTE]
case PermissionLevel.READ:
return [PermissionLevel.EXECUTE, PermissionLevel.READ]
case PermissionLevel.WRITE:
case PermissionLevel.ADMIN:
return [
PermissionLevel.READ,
PermissionLevel.WRITE,
PermissionLevel.EXECUTE,
]
default:
return []
}
}
export enum BuiltinPermissionID {
PUBLIC = "public",
READ_ONLY = "read_only",
WRITE = "write",
ADMIN = "admin",
POWER = "power",
}
const BUILTIN_PERMISSIONS = {
PUBLIC: {
_id: BuiltinPermissionID.PUBLIC,
name: "Public",
permissions: [
new Permission(PermissionType.WEBHOOK, PermissionLevel.EXECUTE),
],
},
READ_ONLY: {
_id: BuiltinPermissionID.READ_ONLY,
name: "Read only",
permissions: [
new Permission(PermissionType.QUERY, PermissionLevel.READ),
new Permission(PermissionType.TABLE, PermissionLevel.READ),
new Permission(PermissionType.VIEW, PermissionLevel.READ),
],
},
WRITE: {
_id: BuiltinPermissionID.WRITE,
name: "Read/Write",
permissions: [
new Permission(PermissionType.QUERY, PermissionLevel.WRITE),
new Permission(PermissionType.TABLE, PermissionLevel.WRITE),
new Permission(PermissionType.VIEW, PermissionLevel.READ),
new Permission(PermissionType.AUTOMATION, PermissionLevel.EXECUTE),
],
},
POWER: {
_id: BuiltinPermissionID.POWER,
name: "Power",
permissions: [
new Permission(PermissionType.TABLE, PermissionLevel.WRITE),
new Permission(PermissionType.USER, PermissionLevel.READ),
new Permission(PermissionType.AUTOMATION, PermissionLevel.EXECUTE),
new Permission(PermissionType.VIEW, PermissionLevel.READ),
new Permission(PermissionType.WEBHOOK, PermissionLevel.READ),
],
},
ADMIN: {
_id: BuiltinPermissionID.ADMIN,
name: "Admin",
permissions: [
new Permission(PermissionType.TABLE, PermissionLevel.ADMIN),
new Permission(PermissionType.USER, PermissionLevel.ADMIN),
new Permission(PermissionType.AUTOMATION, PermissionLevel.ADMIN),
new Permission(PermissionType.VIEW, PermissionLevel.ADMIN),
new Permission(PermissionType.WEBHOOK, PermissionLevel.READ),
new Permission(PermissionType.QUERY, PermissionLevel.ADMIN),
],
},
}
export function getBuiltinPermissions() {
return cloneDeep(BUILTIN_PERMISSIONS)
}
export function getBuiltinPermissionByID(id: string) {
const perms = Object.values(BUILTIN_PERMISSIONS)
return perms.find(perm => perm._id === id)
}
export function doesHaveBasePermission(
permType: PermissionType,
permLevel: PermissionLevel,
rolesHierarchy: RoleHierarchy
) {
const basePermissions = [
...new Set(rolesHierarchy.map(role => role.permissionId)),
]
const builtins = Object.values(BUILTIN_PERMISSIONS)
let permissions = flatten(
builtins
.filter(builtin => basePermissions.indexOf(builtin._id) !== -1)
.map(builtin => builtin.permissions)
)
for (let permission of permissions) {
if (
permission.type === permType &&
getAllowedLevels(permission.level).indexOf(permLevel) !== -1
) {
return true
}
}
return false
}
export function isPermissionLevelHigherThanRead(level: PermissionLevel) {
return levelToNumber(level) > 1
}
// utility as a lot of things need simply the builder permission
export const BUILDER = PermissionType.BUILDER

View File

@ -1,4 +1,4 @@
import { BUILTIN_PERMISSION_IDS, PermissionLevels } from "./permissions" import { BuiltinPermissionID, PermissionLevel } from "./permissions"
import { import {
generateRoleID, generateRoleID,
getRoleParams, getRoleParams,
@ -54,19 +54,19 @@ export class Role {
const BUILTIN_ROLES = { const BUILTIN_ROLES = {
ADMIN: new Role(BUILTIN_IDS.ADMIN, "Admin") ADMIN: new Role(BUILTIN_IDS.ADMIN, "Admin")
.addPermission(BUILTIN_PERMISSION_IDS.ADMIN) .addPermission(BuiltinPermissionID.ADMIN)
.addInheritance(BUILTIN_IDS.POWER), .addInheritance(BUILTIN_IDS.POWER),
POWER: new Role(BUILTIN_IDS.POWER, "Power") POWER: new Role(BUILTIN_IDS.POWER, "Power")
.addPermission(BUILTIN_PERMISSION_IDS.POWER) .addPermission(BuiltinPermissionID.POWER)
.addInheritance(BUILTIN_IDS.BASIC), .addInheritance(BUILTIN_IDS.BASIC),
BASIC: new Role(BUILTIN_IDS.BASIC, "Basic") BASIC: new Role(BUILTIN_IDS.BASIC, "Basic")
.addPermission(BUILTIN_PERMISSION_IDS.WRITE) .addPermission(BuiltinPermissionID.WRITE)
.addInheritance(BUILTIN_IDS.PUBLIC), .addInheritance(BUILTIN_IDS.PUBLIC),
PUBLIC: new Role(BUILTIN_IDS.PUBLIC, "Public").addPermission( PUBLIC: new Role(BUILTIN_IDS.PUBLIC, "Public").addPermission(
BUILTIN_PERMISSION_IDS.PUBLIC BuiltinPermissionID.PUBLIC
), ),
BUILDER: new Role(BUILTIN_IDS.BUILDER, "Builder").addPermission( BUILDER: new Role(BUILTIN_IDS.BUILDER, "Builder").addPermission(
BUILTIN_PERMISSION_IDS.ADMIN BuiltinPermissionID.ADMIN
), ),
} }
@ -147,9 +147,9 @@ export function lowerBuiltinRoleID(roleId1?: string, roleId2?: string) {
* @param {string|null} roleId The level ID to lookup. * @param {string|null} roleId The level ID to lookup.
* @returns {Promise<Role|object|null>} The role object, which may contain an "inherits" property. * @returns {Promise<Role|object|null>} The role object, which may contain an "inherits" property.
*/ */
export async function getRole(roleId?: string) { export async function getRole(roleId?: string): Promise<RoleDoc | undefined> {
if (!roleId) { if (!roleId) {
return null return undefined
} }
let role: any = {} let role: any = {}
// built in roles mostly come from the in-code implementation, // built in roles mostly come from the in-code implementation,
@ -193,7 +193,9 @@ async function getAllUserRoles(userRoleId?: string): Promise<RoleDoc[]> {
) { ) {
roleIds.push(currentRole.inherits) roleIds.push(currentRole.inherits)
currentRole = await getRole(currentRole.inherits) currentRole = await getRole(currentRole.inherits)
roles.push(currentRole) if (currentRole) {
roles.push(currentRole)
}
} }
return roles return roles
} }
@ -225,8 +227,8 @@ export function checkForRoleResourceArray(
if (rolePerms && !Array.isArray(rolePerms[resourceId])) { if (rolePerms && !Array.isArray(rolePerms[resourceId])) {
const permLevel = rolePerms[resourceId] as any const permLevel = rolePerms[resourceId] as any
rolePerms[resourceId] = [permLevel] rolePerms[resourceId] = [permLevel]
if (permLevel === PermissionLevels.WRITE) { if (permLevel === PermissionLevel.WRITE) {
rolePerms[resourceId].push(PermissionLevels.READ) rolePerms[resourceId].push(PermissionLevel.READ)
} }
} }
return rolePerms return rolePerms

View File

@ -1,12 +1,15 @@
import { doWithDB } from "../db"
import { queryPlatformView } from "../db/views"
import { StaticDatabases, ViewName } from "../db/constants"
import { getGlobalDBName } from "../db/tenancy"
import { import {
getTenantId, doWithDB,
queryPlatformView,
StaticDatabases,
getGlobalDBName,
ViewName,
} from "../db"
import {
DEFAULT_TENANT_ID, DEFAULT_TENANT_ID,
isMultiTenant, getTenantId,
getTenantIDFromAppID, getTenantIDFromAppID,
isMultiTenant,
} from "../context" } from "../context"
import env from "../environment" import env from "../environment"
import { import {
@ -15,12 +18,12 @@ import {
TenantResolutionStrategy, TenantResolutionStrategy,
GetTenantIdOptions, GetTenantIdOptions,
} from "@budibase/types" } from "@budibase/types"
import { Headers } from "../constants" import { Header } from "../constants"
const TENANT_DOC = StaticDatabases.PLATFORM_INFO.docs.tenants const TENANT_DOC = StaticDatabases.PLATFORM_INFO.docs.tenants
const PLATFORM_INFO_DB = StaticDatabases.PLATFORM_INFO.name const PLATFORM_INFO_DB = StaticDatabases.PLATFORM_INFO.name
export const addTenantToUrl = (url: string) => { export function addTenantToUrl(url: string) {
const tenantId = getTenantId() const tenantId = getTenantId()
if (isMultiTenant()) { if (isMultiTenant()) {
@ -31,7 +34,7 @@ export const addTenantToUrl = (url: string) => {
return url return url
} }
export const doesTenantExist = async (tenantId: string) => { export async function doesTenantExist(tenantId: string) {
return doWithDB(PLATFORM_INFO_DB, async (db: any) => { return doWithDB(PLATFORM_INFO_DB, async (db: any) => {
let tenants let tenants
try { try {
@ -48,12 +51,12 @@ export const doesTenantExist = async (tenantId: string) => {
}) })
} }
export const tryAddTenant = async ( export async function tryAddTenant(
tenantId: string, tenantId: string,
userId: string, userId: string,
email: string, email: string,
afterCreateTenant: () => Promise<void> afterCreateTenant: () => Promise<void>
) => { ) {
return doWithDB(PLATFORM_INFO_DB, async (db: any) => { return doWithDB(PLATFORM_INFO_DB, async (db: any) => {
const getDoc = async (id: string) => { const getDoc = async (id: string) => {
if (!id) { if (!id) {
@ -95,11 +98,11 @@ export const tryAddTenant = async (
}) })
} }
export const doWithGlobalDB = (tenantId: string, cb: any) => { export function doWithGlobalDB(tenantId: string, cb: any) {
return doWithDB(getGlobalDBName(tenantId), cb) return doWithDB(getGlobalDBName(tenantId), cb)
} }
export const lookupTenantId = async (userId: string) => { export async function lookupTenantId(userId: string) {
return doWithDB(StaticDatabases.PLATFORM_INFO.name, async (db: any) => { return doWithDB(StaticDatabases.PLATFORM_INFO.name, async (db: any) => {
let tenantId = env.MULTI_TENANCY ? DEFAULT_TENANT_ID : null let tenantId = env.MULTI_TENANCY ? DEFAULT_TENANT_ID : null
try { try {
@ -115,19 +118,26 @@ export const lookupTenantId = async (userId: string) => {
} }
// lookup, could be email or userId, either will return a doc // lookup, could be email or userId, either will return a doc
export const getTenantUser = async ( export async function getTenantUser(
identifier: string identifier: string
): Promise<PlatformUser | null> => { ): Promise<PlatformUser | undefined> {
// use the view here and allow to find anyone regardless of casing // use the view here and allow to find anyone regardless of casing
// Use lowercase to ensure email login is case insensitive // Use lowercase to ensure email login is case-insensitive
const response = queryPlatformView(ViewName.PLATFORM_USERS_LOWERCASE, { const users = await queryPlatformView<PlatformUser>(
keys: [identifier.toLowerCase()], ViewName.PLATFORM_USERS_LOWERCASE,
include_docs: true, {
}) as Promise<PlatformUser> keys: [identifier.toLowerCase()],
return response include_docs: true,
}
)
if (Array.isArray(users)) {
return users[0]
} else {
return users
}
} }
export const isUserInAppTenant = (appId: string, user?: any) => { export function isUserInAppTenant(appId: string, user?: any) {
let userTenantId let userTenantId
if (user) { if (user) {
userTenantId = user.tenantId || DEFAULT_TENANT_ID userTenantId = user.tenantId || DEFAULT_TENANT_ID
@ -138,7 +148,7 @@ export const isUserInAppTenant = (appId: string, user?: any) => {
return tenantId === userTenantId return tenantId === userTenantId
} }
export const getTenantIds = async () => { export async function getTenantIds() {
return doWithDB(PLATFORM_INFO_DB, async (db: any) => { return doWithDB(PLATFORM_INFO_DB, async (db: any) => {
let tenants let tenants
try { try {
@ -193,7 +203,7 @@ export const getTenantIDFromCtx = (
// header // header
if (isAllowed(TenantResolutionStrategy.HEADER)) { if (isAllowed(TenantResolutionStrategy.HEADER)) {
const headerTenantId = ctx.request.headers[Headers.TENANT_ID] const headerTenantId = ctx.request.headers[Header.TENANT_ID]
if (headerTenantId) { if (headerTenantId) {
return headerTenantId as string return headerTenantId as string
} }

View File

@ -3,15 +3,14 @@ import {
getUsersByAppParams, getUsersByAppParams,
getProdAppID, getProdAppID,
generateAppUserID, generateAppUserID,
} from "./db/utils" queryGlobalView,
import { queryGlobalView } from "./db/views" UNICODE_MAX,
import { UNICODE_MAX } from "./db/constants" } from "./db"
import { BulkDocsResponse, User } from "@budibase/types" import { BulkDocsResponse, User } from "@budibase/types"
import { getGlobalDB } from "./context" import { getGlobalDB } from "./context"
import PouchDB from "pouchdb"
export const bulkGetGlobalUsersById = async (userIds: string[]) => { export const bulkGetGlobalUsersById = async (userIds: string[]) => {
const db = getGlobalDB() as PouchDB.Database const db = getGlobalDB()
return ( return (
await db.allDocs({ await db.allDocs({
keys: userIds, keys: userIds,
@ -21,7 +20,7 @@ export const bulkGetGlobalUsersById = async (userIds: string[]) => {
} }
export const bulkUpdateGlobalUsers = async (users: User[]) => { export const bulkUpdateGlobalUsers = async (users: User[]) => {
const db = getGlobalDB() as PouchDB.Database const db = getGlobalDB()
return (await db.bulkDocs(users)) as BulkDocsResponse return (await db.bulkDocs(users)) as BulkDocsResponse
} }
@ -69,7 +68,7 @@ export const getGlobalUserByAppPage = (appId: string, user: User) => {
if (!user) { if (!user) {
return return
} }
return generateAppUserID(getProdAppID(appId), user._id!) return generateAppUserID(getProdAppID(appId)!, user._id!)
} }
/** /**

View File

@ -1,8 +1,12 @@
import { DocumentType, SEPARATOR, ViewName, getAllApps } from "./db/utils" import {
const jwt = require("jsonwebtoken") DocumentType,
SEPARATOR,
ViewName,
getAllApps,
queryGlobalView,
} from "./db"
import { options } from "./middleware/passport/jwt" import { options } from "./middleware/passport/jwt"
import { queryGlobalView } from "./db/views" import { Header, Cookie, MAX_VALID_DATE } from "./constants"
import { Headers, Cookies, MAX_VALID_DATE } from "./constants"
import env from "./environment" import env from "./environment"
import userCache from "./cache/user" import userCache from "./cache/user"
import { getSessionsForUser, invalidateSessions } from "./security/sessions" import { getSessionsForUser, invalidateSessions } from "./security/sessions"
@ -15,6 +19,7 @@ import {
TenantResolutionStrategy, TenantResolutionStrategy,
} from "@budibase/types" } from "@budibase/types"
import { SetOption } from "cookies" import { SetOption } from "cookies"
const jwt = require("jsonwebtoken")
const APP_PREFIX = DocumentType.APP + SEPARATOR const APP_PREFIX = DocumentType.APP + SEPARATOR
const PROD_APP_PREFIX = "/app/" const PROD_APP_PREFIX = "/app/"
@ -29,7 +34,7 @@ async function resolveAppUrl(ctx: BBContext) {
const appUrl = ctx.path.split("/")[2] const appUrl = ctx.path.split("/")[2]
let possibleAppUrl = `/${appUrl.toLowerCase()}` let possibleAppUrl = `/${appUrl.toLowerCase()}`
let tenantId = tenancy.getTenantId() let tenantId: string | null = tenancy.getTenantId()
if (env.MULTI_TENANCY) { if (env.MULTI_TENANCY) {
// always use the tenant id from the subdomain in multi tenancy // always use the tenant id from the subdomain in multi tenancy
// this ensures the logged-in user tenant id doesn't overwrite // this ensures the logged-in user tenant id doesn't overwrite
@ -50,7 +55,7 @@ async function resolveAppUrl(ctx: BBContext) {
return app && app.appId ? app.appId : undefined return app && app.appId ? app.appId : undefined
} }
export const isServingApp = (ctx: BBContext) => { export function isServingApp(ctx: BBContext) {
// dev app // dev app
if (ctx.path.startsWith(`/${APP_PREFIX}`)) { if (ctx.path.startsWith(`/${APP_PREFIX}`)) {
return true return true
@ -67,9 +72,9 @@ export const isServingApp = (ctx: BBContext) => {
* @param {object} ctx The main request body to look through. * @param {object} ctx The main request body to look through.
* @returns {string|undefined} If an appId was found it will be returned. * @returns {string|undefined} If an appId was found it will be returned.
*/ */
export const getAppIdFromCtx = async (ctx: BBContext) => { export async function getAppIdFromCtx(ctx: BBContext) {
// look in headers // look in headers
const options = [ctx.headers[Headers.APP_ID]] const options = [ctx.headers[Header.APP_ID]]
let appId let appId
for (let option of options) { for (let option of options) {
appId = confirmAppId(option as string) appId = confirmAppId(option as string)
@ -103,7 +108,7 @@ export const getAppIdFromCtx = async (ctx: BBContext) => {
* opens the contents of the specified encrypted JWT. * opens the contents of the specified encrypted JWT.
* @return {object} the contents of the token. * @return {object} the contents of the token.
*/ */
export const openJwt = (token: string) => { export function openJwt(token: string) {
if (!token) { if (!token) {
return token return token
} }
@ -115,7 +120,7 @@ export const openJwt = (token: string) => {
* @param {object} ctx The request which is to be manipulated. * @param {object} ctx The request which is to be manipulated.
* @param {string} name The name of the cookie to get. * @param {string} name The name of the cookie to get.
*/ */
export const getCookie = (ctx: BBContext, name: string) => { export function getCookie(ctx: BBContext, name: string) {
const cookie = ctx.cookies.get(name) const cookie = ctx.cookies.get(name)
if (!cookie) { if (!cookie) {
@ -132,12 +137,12 @@ export const getCookie = (ctx: BBContext, name: string) => {
* @param {string|object} value The value of cookie which will be set. * @param {string|object} value The value of cookie which will be set.
* @param {object} opts options like whether to sign. * @param {object} opts options like whether to sign.
*/ */
export const setCookie = ( export function setCookie(
ctx: BBContext, ctx: BBContext,
value: any, value: any,
name = "builder", name = "builder",
opts = { sign: true } opts = { sign: true }
) => { ) {
if (value && opts && opts.sign) { if (value && opts && opts.sign) {
value = jwt.sign(value, options.secretOrKey) value = jwt.sign(value, options.secretOrKey)
} }
@ -159,7 +164,7 @@ export const setCookie = (
/** /**
* Utility function, simply calls setCookie with an empty string for value * Utility function, simply calls setCookie with an empty string for value
*/ */
export const clearCookie = (ctx: BBContext, name: string) => { export function clearCookie(ctx: BBContext, name: string) {
setCookie(ctx, null, name) setCookie(ctx, null, name)
} }
@ -169,11 +174,11 @@ export const clearCookie = (ctx: BBContext, name: string) => {
* @param {object} ctx The koa context object to be tested. * @param {object} ctx The koa context object to be tested.
* @return {boolean} returns true if the call is from the client lib (a built app rather than the builder). * @return {boolean} returns true if the call is from the client lib (a built app rather than the builder).
*/ */
export const isClient = (ctx: BBContext) => { export function isClient(ctx: BBContext) {
return ctx.headers[Headers.TYPE] === "client" return ctx.headers[Header.TYPE] === "client"
} }
const getBuilders = async () => { async function getBuilders() {
const builders = await queryGlobalView(ViewName.USER_BY_BUILDERS, { const builders = await queryGlobalView(ViewName.USER_BY_BUILDERS, {
include_docs: false, include_docs: false,
}) })
@ -189,7 +194,7 @@ const getBuilders = async () => {
} }
} }
export const getBuildersCount = async () => { export async function getBuildersCount() {
const builders = await getBuilders() const builders = await getBuilders()
return builders.length return builders.length
} }
@ -197,14 +202,14 @@ export const getBuildersCount = async () => {
/** /**
* Logs a user out from budibase. Re-used across account portal and builder. * Logs a user out from budibase. Re-used across account portal and builder.
*/ */
export const platformLogout = async (opts: PlatformLogoutOpts) => { export async function platformLogout(opts: PlatformLogoutOpts) {
const ctx = opts.ctx const ctx = opts.ctx
const userId = opts.userId const userId = opts.userId
const keepActiveSession = opts.keepActiveSession const keepActiveSession = opts.keepActiveSession
if (!ctx) throw new Error("Koa context must be supplied to logout.") if (!ctx) throw new Error("Koa context must be supplied to logout.")
const currentSession = getCookie(ctx, Cookies.Auth) const currentSession = getCookie(ctx, Cookie.Auth)
let sessions = await getSessionsForUser(userId) let sessions = await getSessionsForUser(userId)
if (keepActiveSession) { if (keepActiveSession) {
@ -213,8 +218,8 @@ export const platformLogout = async (opts: PlatformLogoutOpts) => {
) )
} else { } else {
// clear cookies // clear cookies
clearCookie(ctx, Cookies.Auth) clearCookie(ctx, Cookie.Auth)
clearCookie(ctx, Cookies.CurrentApp) clearCookie(ctx, Cookie.CurrentApp)
} }
const sessionIds = sessions.map(({ sessionId }) => sessionId) const sessionIds = sessions.map(({ sessionId }) => sessionId)
@ -223,6 +228,6 @@ export const platformLogout = async (opts: PlatformLogoutOpts) => {
await userCache.invalidateUser(userId) await userCache.invalidateUser(userId)
} }
export const timeout = (timeMs: number) => { export function timeout(timeMs: number) {
return new Promise(resolve => setTimeout(resolve, timeMs)) return new Promise(resolve => setTimeout(resolve, timeMs))
} }

View File

@ -12,7 +12,7 @@
"sourceMap": true, "sourceMap": true,
"declaration": true, "declaration": true,
"types": [ "node", "jest" ], "types": [ "node", "jest" ],
"outDir": "dist" "outDir": "dist",
}, },
"include": [ "include": [
"**/*.js", "**/*.js",

View File

@ -1335,6 +1335,11 @@
dependencies: dependencies:
"@types/node" "*" "@types/node" "*"
"@types/tough-cookie@^4.0.2":
version "4.0.2"
resolved "https://registry.yarnpkg.com/@types/tough-cookie/-/tough-cookie-4.0.2.tgz#6286b4c7228d58ab7866d19716f3696e03a09397"
integrity sha512-Q5vtl1W5ue16D+nIaW8JWebSSraJVlK+EthKn7e7UcD4KWsaSJ8BqGPXNaPghgtcn/fhvrN17Tv8ksUsQpiplw==
"@types/uuid@8.3.4": "@types/uuid@8.3.4":
version "8.3.4" version "8.3.4"
resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-8.3.4.tgz#bd86a43617df0594787d38b735f55c805becf1bc" resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-8.3.4.tgz#bd86a43617df0594787d38b735f55c805becf1bc"
@ -1411,7 +1416,7 @@ acorn@^8.4.1:
resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.8.1.tgz#0a3f9cbecc4ec3bea6f0a80b66ae8dd2da250b73" resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.8.1.tgz#0a3f9cbecc4ec3bea6f0a80b66ae8dd2da250b73"
integrity sha512-7zFpHzhnqYKrkYdUjF1HI1bzd0VygEGX8lFk4k5zVMqHEoES+P+7TKI+EvLO9WVMJ8eekdO0aDEK044xTXwPPA== integrity sha512-7zFpHzhnqYKrkYdUjF1HI1bzd0VygEGX8lFk4k5zVMqHEoES+P+7TKI+EvLO9WVMJ8eekdO0aDEK044xTXwPPA==
agent-base@6: agent-base@6, agent-base@^6.0.2:
version "6.0.2" version "6.0.2"
resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-6.0.2.tgz#49fff58577cfee3f37176feab4c22e00f86d7f77" resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-6.0.2.tgz#49fff58577cfee3f37176feab4c22e00f86d7f77"
integrity sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ== integrity sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==
@ -1568,6 +1573,15 @@ axios@0.24.0:
dependencies: dependencies:
follow-redirects "^1.14.4" follow-redirects "^1.14.4"
axios@^1.1.3:
version "1.1.3"
resolved "https://registry.yarnpkg.com/axios/-/axios-1.1.3.tgz#8274250dada2edf53814ed7db644b9c2866c1e35"
integrity sha512-00tXVRwKx/FZr/IDVFt4C+f9FYairX517WoGCL6dpOntqLkZofjhu43F/Xl44UOpqa+9sLFDrG/XAnFsUYgkDA==
dependencies:
follow-redirects "^1.15.0"
form-data "^4.0.0"
proxy-from-env "^1.1.0"
babel-jest@^28.1.3: babel-jest@^28.1.3:
version "28.1.3" version "28.1.3"
resolved "https://registry.yarnpkg.com/babel-jest/-/babel-jest-28.1.3.tgz#c1187258197c099072156a0a121c11ee1e3917d5" resolved "https://registry.yarnpkg.com/babel-jest/-/babel-jest-28.1.3.tgz#c1187258197c099072156a0a121c11ee1e3917d5"
@ -1814,6 +1828,14 @@ cacheable-request@^6.0.0:
normalize-url "^4.1.0" normalize-url "^4.1.0"
responselike "^1.0.2" responselike "^1.0.2"
call-bind@^1.0.0:
version "1.0.2"
resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.2.tgz#b1d4e89e688119c3c9a903ad30abb2f6a919be3c"
integrity sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==
dependencies:
function-bind "^1.1.1"
get-intrinsic "^1.0.2"
callsites@^3.0.0: callsites@^3.0.0:
version "3.1.0" version "3.1.0"
resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73" resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73"
@ -2472,6 +2494,11 @@ follow-redirects@^1.14.4:
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.1.tgz#0ca6a452306c9b276e4d3127483e29575e207ad5" resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.1.tgz#0ca6a452306c9b276e4d3127483e29575e207ad5"
integrity sha512-yLAMQs+k0b2m7cVxpS1VKJVvoz7SS9Td1zss3XRwXj+ZDH00RJgnuLx7E44wx02kQLrdM3aOOy+FpzS7+8OizA== integrity sha512-yLAMQs+k0b2m7cVxpS1VKJVvoz7SS9Td1zss3XRwXj+ZDH00RJgnuLx7E44wx02kQLrdM3aOOy+FpzS7+8OizA==
follow-redirects@^1.15.0:
version "1.15.2"
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.2.tgz#b460864144ba63f2681096f274c4e57026da2c13"
integrity sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==
forever-agent@~0.6.1: forever-agent@~0.6.1:
version "0.6.1" version "0.6.1"
resolved "https://registry.yarnpkg.com/forever-agent/-/forever-agent-0.6.1.tgz#fbc71f0c41adeb37f96c577ad1ed42d8fdacca91" resolved "https://registry.yarnpkg.com/forever-agent/-/forever-agent-0.6.1.tgz#fbc71f0c41adeb37f96c577ad1ed42d8fdacca91"
@ -2486,6 +2513,15 @@ form-data@^3.0.0:
combined-stream "^1.0.8" combined-stream "^1.0.8"
mime-types "^2.1.12" mime-types "^2.1.12"
form-data@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.0.tgz#93919daeaf361ee529584b9b31664dc12c9fa452"
integrity sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==
dependencies:
asynckit "^0.4.0"
combined-stream "^1.0.8"
mime-types "^2.1.12"
form-data@~2.3.2: form-data@~2.3.2:
version "2.3.3" version "2.3.3"
resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.3.3.tgz#dcce52c05f644f298c6a7ab936bd724ceffbf3a6" resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.3.3.tgz#dcce52c05f644f298c6a7ab936bd724ceffbf3a6"
@ -2557,6 +2593,15 @@ get-caller-file@^2.0.5:
resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e" resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e"
integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg== integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==
get-intrinsic@^1.0.2:
version "1.1.3"
resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.1.3.tgz#063c84329ad93e83893c7f4f243ef63ffa351385"
integrity sha512-QJVz1Tj7MS099PevUG5jvnt9tSkXN8K14dxQlikJuPt4uD9hHAHjLyLBiLR5zELelBdD9QNRAXZzsJx0WaDL9A==
dependencies:
function-bind "^1.1.1"
has "^1.0.3"
has-symbols "^1.0.3"
get-package-type@^0.1.0: get-package-type@^0.1.0:
version "0.1.0" version "0.1.0"
resolved "https://registry.yarnpkg.com/get-package-type/-/get-package-type-0.1.0.tgz#8de2d803cff44df3bc6c456e6668b36c3926e11a" resolved "https://registry.yarnpkg.com/get-package-type/-/get-package-type-0.1.0.tgz#8de2d803cff44df3bc6c456e6668b36c3926e11a"
@ -2705,7 +2750,7 @@ has-flag@^4.0.0:
resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b" resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b"
integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==
has-symbols@^1.0.2: has-symbols@^1.0.2, has-symbols@^1.0.3:
version "1.0.3" version "1.0.3"
resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.3.tgz#bb7b2c4349251dce87b125f7bdf874aa7c8b39f8" resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.3.tgz#bb7b2c4349251dce87b125f7bdf874aa7c8b39f8"
integrity sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A== integrity sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==
@ -2752,6 +2797,13 @@ http-cache-semantics@^4.0.0:
resolved "https://registry.yarnpkg.com/http-cache-semantics/-/http-cache-semantics-4.1.0.tgz#49e91c5cbf36c9b94bcfcd71c23d5249ec74e390" resolved "https://registry.yarnpkg.com/http-cache-semantics/-/http-cache-semantics-4.1.0.tgz#49e91c5cbf36c9b94bcfcd71c23d5249ec74e390"
integrity sha512-carPklcUh7ROWRK7Cv27RPtdhYhUsela/ue5/jKzjegVvXDqM2ILE9Q2BGn9JZJh1g87cp56su/FgQSzcWS8cQ== integrity sha512-carPklcUh7ROWRK7Cv27RPtdhYhUsela/ue5/jKzjegVvXDqM2ILE9Q2BGn9JZJh1g87cp56su/FgQSzcWS8cQ==
http-cookie-agent@^4.0.2:
version "4.0.2"
resolved "https://registry.yarnpkg.com/http-cookie-agent/-/http-cookie-agent-4.0.2.tgz#dcdaae18ed1f7452d81ae4d5cd80b227d6831b69"
integrity sha512-noTmxdH5CuytTnLj/Qv3Z84e/YFq8yLXAw3pqIYZ25Edhb9pQErIAC+ednw40Cic6Le/h9ryph5/TqsvkOaUCw==
dependencies:
agent-base "^6.0.2"
http-errors@^1.6.3, http-errors@~1.8.0: http-errors@^1.6.3, http-errors@~1.8.0:
version "1.8.1" version "1.8.1"
resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.8.1.tgz#7c3f28577cbc8a207388455dbd62295ed07bd68c" resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.8.1.tgz#7c3f28577cbc8a207388455dbd62295ed07bd68c"
@ -4018,6 +4070,18 @@ msgpackr@^1.5.2:
optionalDependencies: optionalDependencies:
msgpackr-extract "^2.1.2" msgpackr-extract "^2.1.2"
nano@^10.1.0:
version "10.1.0"
resolved "https://registry.yarnpkg.com/nano/-/nano-10.1.0.tgz#afdd5a7440e62f09a8e23f41fcea328d27383922"
integrity sha512-COeN2TpLcHuSN44QLnPmfZCoCsKAg8/aelPOVqqm/2/MvRHDEA11/Kld5C4sLzDlWlhFZ3SO2WGJGevCsvcEzQ==
dependencies:
"@types/tough-cookie" "^4.0.2"
axios "^1.1.3"
http-cookie-agent "^4.0.2"
node-abort-controller "^3.0.1"
qs "^6.11.0"
tough-cookie "^4.1.2"
napi-macros@~2.0.0: napi-macros@~2.0.0:
version "2.0.0" version "2.0.0"
resolved "https://registry.yarnpkg.com/napi-macros/-/napi-macros-2.0.0.tgz#2b6bae421e7b96eb687aa6c77a7858640670001b" resolved "https://registry.yarnpkg.com/napi-macros/-/napi-macros-2.0.0.tgz#2b6bae421e7b96eb687aa6c77a7858640670001b"
@ -4043,6 +4107,11 @@ negotiator@0.6.3:
resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.3.tgz#58e323a72fedc0d6f9cd4d31fe49f51479590ccd" resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.3.tgz#58e323a72fedc0d6f9cd4d31fe49f51479590ccd"
integrity sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg== integrity sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==
node-abort-controller@^3.0.1:
version "3.0.1"
resolved "https://registry.yarnpkg.com/node-abort-controller/-/node-abort-controller-3.0.1.tgz#f91fa50b1dee3f909afabb7e261b1e1d6b0cb74e"
integrity sha512-/ujIVxthRs+7q6hsdjHMaj8hRG9NuWmwrz+JdRwZ14jdFoKSkm+vDsCbF9PLpnSqjaWQJuTmVtcWHNLr+vrOFw==
node-addon-api@^3.1.0: node-addon-api@^3.1.0:
version "3.2.1" version "3.2.1"
resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-3.2.1.tgz#81325e0a2117789c0128dab65e7e38f07ceba161" resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-3.2.1.tgz#81325e0a2117789c0128dab65e7e38f07ceba161"
@ -4178,6 +4247,11 @@ object-assign@^4.1.1:
resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863"
integrity sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg== integrity sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==
object-inspect@^1.9.0:
version "1.12.2"
resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.12.2.tgz#c0641f26394532f28ab8d796ab954e43c009a8ea"
integrity sha512-z+cPxW0QGUp0mcqcsgQyLVRDoXFQbXOwBaqyF7VIgI4TWNQsDHrBpUQslRmIfAoYWdYzs6UlKJtB2XJpTaNSpQ==
on-finished@^2.3.0: on-finished@^2.3.0:
version "2.4.1" version "2.4.1"
resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.4.1.tgz#58c8c44116e54845ad57f14ab10b03533184ac3f" resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.4.1.tgz#58c8c44116e54845ad57f14ab10b03533184ac3f"
@ -4675,6 +4749,11 @@ prompts@^2.0.1:
kleur "^3.0.3" kleur "^3.0.3"
sisteransi "^1.0.5" sisteransi "^1.0.5"
proxy-from-env@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz#e102f16ca355424865755d2c9e8ea4f24d58c3e2"
integrity sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==
prr@~1.0.1: prr@~1.0.1:
version "1.0.1" version "1.0.1"
resolved "https://registry.yarnpkg.com/prr/-/prr-1.0.1.tgz#d3fc114ba06995a45ec6893f484ceb1d78f5f476" resolved "https://registry.yarnpkg.com/prr/-/prr-1.0.1.tgz#d3fc114ba06995a45ec6893f484ceb1d78f5f476"
@ -4715,6 +4794,13 @@ pupa@^2.1.1:
dependencies: dependencies:
escape-goat "^2.0.0" escape-goat "^2.0.0"
qs@^6.11.0:
version "6.11.0"
resolved "https://registry.yarnpkg.com/qs/-/qs-6.11.0.tgz#fd0d963446f7a65e1367e01abd85429453f0c37a"
integrity sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==
dependencies:
side-channel "^1.0.4"
qs@~6.5.2: qs@~6.5.2:
version "6.5.3" version "6.5.3"
resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.3.tgz#3aeeffc91967ef6e35c0e488ef46fb296ab76aad" resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.3.tgz#3aeeffc91967ef6e35c0e488ef46fb296ab76aad"
@ -4725,6 +4811,11 @@ querystring@0.2.0:
resolved "https://registry.yarnpkg.com/querystring/-/querystring-0.2.0.tgz#b209849203bb25df820da756e747005878521620" resolved "https://registry.yarnpkg.com/querystring/-/querystring-0.2.0.tgz#b209849203bb25df820da756e747005878521620"
integrity sha512-X/xY82scca2tau62i9mDyU9K+I+djTMUsvwf7xnUX5GLvVzgJybOJf4Y6o9Zx3oJK/LSXg5tTZBjwzqVPaPO2g== integrity sha512-X/xY82scca2tau62i9mDyU9K+I+djTMUsvwf7xnUX5GLvVzgJybOJf4Y6o9Zx3oJK/LSXg5tTZBjwzqVPaPO2g==
querystringify@^2.1.1:
version "2.2.0"
resolved "https://registry.yarnpkg.com/querystringify/-/querystringify-2.2.0.tgz#3345941b4153cb9d082d8eee4cda2016a9aef7f6"
integrity sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==
range-parser@^1.2.0: range-parser@^1.2.0:
version "1.2.1" version "1.2.1"
resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031" resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031"
@ -4878,6 +4969,11 @@ require-directory@^2.1.1:
resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42" resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42"
integrity sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q== integrity sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==
requires-port@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff"
integrity sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==
resolve-cwd@^3.0.0: resolve-cwd@^3.0.0:
version "3.0.0" version "3.0.0"
resolved "https://registry.yarnpkg.com/resolve-cwd/-/resolve-cwd-3.0.0.tgz#0f0075f1bb2544766cf73ba6a6e2adfebcb13f2d" resolved "https://registry.yarnpkg.com/resolve-cwd/-/resolve-cwd-3.0.0.tgz#0f0075f1bb2544766cf73ba6a6e2adfebcb13f2d"
@ -4999,6 +5095,15 @@ shimmer@^1.2.0:
resolved "https://registry.yarnpkg.com/shimmer/-/shimmer-1.2.1.tgz#610859f7de327b587efebf501fb43117f9aff337" resolved "https://registry.yarnpkg.com/shimmer/-/shimmer-1.2.1.tgz#610859f7de327b587efebf501fb43117f9aff337"
integrity sha512-sQTKC1Re/rM6XyFM6fIAGHRPVGvyXfgzIDvzoq608vM+jeyVD0Tu1E6Np0Kc2zAIFWIj963V2800iF/9LPieQw== integrity sha512-sQTKC1Re/rM6XyFM6fIAGHRPVGvyXfgzIDvzoq608vM+jeyVD0Tu1E6Np0Kc2zAIFWIj963V2800iF/9LPieQw==
side-channel@^1.0.4:
version "1.0.4"
resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.4.tgz#efce5c8fdc104ee751b25c58d4290011fa5ea2cf"
integrity sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==
dependencies:
call-bind "^1.0.0"
get-intrinsic "^1.0.2"
object-inspect "^1.9.0"
signal-exit@^3.0.0, signal-exit@^3.0.2, signal-exit@^3.0.3, signal-exit@^3.0.7: signal-exit@^3.0.0, signal-exit@^3.0.2, signal-exit@^3.0.3, signal-exit@^3.0.7:
version "3.0.7" version "3.0.7"
resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.7.tgz#a9a1767f8af84155114eaabd73f99273c8f59ad9" resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.7.tgz#a9a1767f8af84155114eaabd73f99273c8f59ad9"
@ -5329,6 +5434,16 @@ touch@^3.1.0:
punycode "^2.1.1" punycode "^2.1.1"
universalify "^0.1.2" universalify "^0.1.2"
tough-cookie@^4.1.2:
version "4.1.2"
resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-4.1.2.tgz#e53e84b85f24e0b65dd526f46628db6c85f6b874"
integrity sha512-G9fqXWoYFZgTc2z8Q5zaHy/vJMjm+WV0AkAeHxVCQiEB1b+dGvWzFW6QV07cY5jQ5gRkeid2qIkzkxUnmoQZUQ==
dependencies:
psl "^1.1.33"
punycode "^2.1.1"
universalify "^0.2.0"
url-parse "^1.5.3"
tough-cookie@~2.5.0: tough-cookie@~2.5.0:
version "2.5.0" version "2.5.0"
resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.5.0.tgz#cd9fb2a0aa1d5a12b473bd9fb96fa3dcff65ade2" resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.5.0.tgz#cd9fb2a0aa1d5a12b473bd9fb96fa3dcff65ade2"
@ -5458,6 +5573,11 @@ universalify@^0.1.2:
resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.1.2.tgz#b646f69be3942dabcecc9d6639c80dc105efaa66" resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.1.2.tgz#b646f69be3942dabcecc9d6639c80dc105efaa66"
integrity sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg== integrity sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==
universalify@^0.2.0:
version "0.2.0"
resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.2.0.tgz#6451760566fa857534745ab1dde952d1b1761be0"
integrity sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==
update-browserslist-db@^1.0.9: update-browserslist-db@^1.0.9:
version "1.0.10" version "1.0.10"
resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.0.10.tgz#0f54b876545726f17d00cd9a2561e6dade943ff3" resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.0.10.tgz#0f54b876545726f17d00cd9a2561e6dade943ff3"
@ -5500,6 +5620,14 @@ url-parse-lax@^3.0.0:
dependencies: dependencies:
prepend-http "^2.0.0" prepend-http "^2.0.0"
url-parse@^1.5.3:
version "1.5.10"
resolved "https://registry.yarnpkg.com/url-parse/-/url-parse-1.5.10.tgz#9d3c2f736c1d75dd3bd2be507dcc111f1e2ea9c1"
integrity sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==
dependencies:
querystringify "^2.1.1"
requires-port "^1.0.0"
url@0.10.3: url@0.10.3:
version "0.10.3" version "0.10.3"
resolved "https://registry.yarnpkg.com/url/-/url-0.10.3.tgz#021e4d9c7705f21bbf37d03ceb58767402774c64" resolved "https://registry.yarnpkg.com/url/-/url-0.10.3.tgz#021e4d9c7705f21bbf37d03ceb58767402774c64"

View File

@ -2,7 +2,7 @@ module FetchMock {
const fetch = jest.requireActual("node-fetch") const fetch = jest.requireActual("node-fetch")
let failCount = 0 let failCount = 0
module.exports = async (url: any, opts: any) => { const func = async (url: any, opts: any) => {
function json(body: any, status = 200) { function json(body: any, status = 200) {
return { return {
status, status,
@ -106,4 +106,8 @@ module FetchMock {
} }
return fetch(url, opts) return fetch(url, opts)
} }
func.Headers = fetch.Headers
module.exports = func
} }

View File

@ -19,6 +19,7 @@ if (!process.env.CI) {
"@budibase/backend-core/(.*)": "<rootDir>/../backend-core/$1", "@budibase/backend-core/(.*)": "<rootDir>/../backend-core/$1",
"@budibase/backend-core": "<rootDir>/../backend-core/src", "@budibase/backend-core": "<rootDir>/../backend-core/src",
"@budibase/types": "<rootDir>/../types/src", "@budibase/types": "<rootDir>/../types/src",
"^axios.*$": "<rootDir>/node_modules/axios/lib/axios.js",
} }
// add pro sources if they exist // add pro sources if they exist
if (fs.existsSync("../../../budibase-pro")) { if (fs.existsSync("../../../budibase-pro")) {

View File

@ -14,17 +14,16 @@ import {
DocumentType, DocumentType,
AppStatus, AppStatus,
} from "../../db/utils" } from "../../db/utils"
const { import {
BUILTIN_ROLE_IDS, db as dbCore,
AccessController, roles,
} = require("@budibase/backend-core/roles") cache,
const { CacheKeys, bustCache } = require("@budibase/backend-core/cache") tenancy,
const { context,
getAllApps, errors,
isDevAppID, events,
getProdAppID, migrations,
Replication, } from "@budibase/backend-core"
} = require("@budibase/backend-core/db")
import { USERS_TABLE_SCHEMA } from "../../constants" import { USERS_TABLE_SCHEMA } from "../../constants"
import { removeAppFromUserRoles } from "../../utilities/workerRequests" import { removeAppFromUserRoles } from "../../utilities/workerRequests"
import { clientLibraryPath, stringToReadStream } from "../../utilities" import { clientLibraryPath, stringToReadStream } from "../../utilities"
@ -34,15 +33,11 @@ import {
backupClientLibrary, backupClientLibrary,
revertClientLibrary, revertClientLibrary,
} from "../../utilities/fileSystem/clientLibrary" } from "../../utilities/fileSystem/clientLibrary"
const { getTenantId, isMultiTenant } = require("@budibase/backend-core/tenancy")
import { syncGlobalUsers } from "./user" import { syncGlobalUsers } from "./user"
const { app: appCache } = require("@budibase/backend-core/cache")
import { cleanupAutomations } from "../../automations/utils" import { cleanupAutomations } from "../../automations/utils"
import { context } from "@budibase/backend-core"
import { checkAppMetadata } from "../../automations/logging" import { checkAppMetadata } from "../../automations/logging"
import { getUniqueRows } from "../../utilities/usageQuota/rows" import { getUniqueRows } from "../../utilities/usageQuota/rows"
import { quotas, groups } from "@budibase/pro" import { quotas, groups } from "@budibase/pro"
import { errors, events, migrations } from "@budibase/backend-core"
import { App, Layout, Screen, MigrationType } from "@budibase/types" import { App, Layout, Screen, MigrationType } from "@budibase/types"
import { BASE_LAYOUT_PROP_IDS } from "../../constants/layouts" import { BASE_LAYOUT_PROP_IDS } from "../../constants/layouts"
import { enrichPluginURLs } from "../../utilities/plugins" import { enrichPluginURLs } from "../../utilities/plugins"
@ -75,7 +70,7 @@ async function getScreens() {
function getUserRoleId(ctx: any) { function getUserRoleId(ctx: any) {
return !ctx.user.role || !ctx.user.role._id return !ctx.user.role || !ctx.user.role._id
? BUILTIN_ROLE_IDS.PUBLIC ? roles.BUILTIN_ROLE_IDS.PUBLIC
: ctx.user.role._id : ctx.user.role._id
} }
@ -123,7 +118,7 @@ const checkAppName = (
} }
async function createInstance(template: any) { async function createInstance(template: any) {
const tenantId = isMultiTenant() ? getTenantId() : null const tenantId = tenancy.isMultiTenant() ? tenancy.getTenantId() : null
const baseAppId = generateAppID(tenantId) const baseAppId = generateAppID(tenantId)
const appId = generateDevAppID(baseAppId) const appId = generateDevAppID(baseAppId)
await context.updateAppId(appId) await context.updateAppId(appId)
@ -162,7 +157,7 @@ async function createInstance(template: any) {
export const fetch = async (ctx: any) => { export const fetch = async (ctx: any) => {
const dev = ctx.query && ctx.query.status === AppStatus.DEV const dev = ctx.query && ctx.query.status === AppStatus.DEV
const all = ctx.query && ctx.query.status === AppStatus.ALL const all = ctx.query && ctx.query.status === AppStatus.ALL
const apps = await getAllApps({ dev, all }) const apps = (await dbCore.getAllApps({ dev, all })) as App[]
const appIds = apps const appIds = apps
.filter((app: any) => app.status === "development") .filter((app: any) => app.status === "development")
@ -187,7 +182,7 @@ export const fetch = async (ctx: any) => {
export const fetchAppDefinition = async (ctx: any) => { export const fetchAppDefinition = async (ctx: any) => {
const layouts = await getLayouts() const layouts = await getLayouts()
const userRoleId = getUserRoleId(ctx) const userRoleId = getUserRoleId(ctx)
const accessController = new AccessController() const accessController = new roles.AccessController()
const screens = await accessController.checkScreensAccess( const screens = await accessController.checkScreensAccess(
await getScreens(), await getScreens(),
userRoleId userRoleId
@ -211,7 +206,7 @@ export const fetchAppPackage = async (ctx: any) => {
// Only filter screens if the user is not a builder // Only filter screens if the user is not a builder
if (!(ctx.user.builder && ctx.user.builder.global)) { if (!(ctx.user.builder && ctx.user.builder.global)) {
const userRoleId = getUserRoleId(ctx) const userRoleId = getUserRoleId(ctx)
const accessController = new AccessController() const accessController = new roles.AccessController()
screens = await accessController.checkScreensAccess(screens, userRoleId) screens = await accessController.checkScreensAccess(screens, userRoleId)
} }
@ -224,7 +219,7 @@ export const fetchAppPackage = async (ctx: any) => {
} }
const performAppCreate = async (ctx: any) => { const performAppCreate = async (ctx: any) => {
const apps = await getAllApps({ dev: true }) const apps = await dbCore.getAllApps({ dev: true })
const name = ctx.request.body.name const name = ctx.request.body.name
checkAppName(ctx, apps, name) checkAppName(ctx, apps, name)
const url = getAppUrl(ctx) const url = getAppUrl(ctx)
@ -254,7 +249,7 @@ const performAppCreate = async (ctx: any) => {
url: url, url: url,
template: templateKey, template: templateKey,
instance, instance,
tenantId: getTenantId(), tenantId: tenancy.getTenantId(),
updatedAt: new Date().toISOString(), updatedAt: new Date().toISOString(),
createdAt: new Date().toISOString(), createdAt: new Date().toISOString(),
status: AppStatus.DEV, status: AppStatus.DEV,
@ -313,7 +308,7 @@ const performAppCreate = async (ctx: any) => {
await createApp(appId) await createApp(appId)
} }
await appCache.invalidateAppMetadata(appId, newApplication) await cache.app.invalidateAppMetadata(appId, newApplication)
return newApplication return newApplication
} }
@ -343,7 +338,7 @@ const creationEvents = async (request: any, app: App) => {
} }
const appPostCreate = async (ctx: any, app: App) => { const appPostCreate = async (ctx: any, app: App) => {
const tenantId = getTenantId() const tenantId = tenancy.getTenantId()
await migrations.backPopulateMigrations({ await migrations.backPopulateMigrations({
type: MigrationType.APP, type: MigrationType.APP,
tenantId, tenantId,
@ -356,7 +351,9 @@ const appPostCreate = async (ctx: any, app: App) => {
const rowCount = rows ? rows.length : 0 const rowCount = rows ? rows.length : 0
if (rowCount) { if (rowCount) {
try { try {
await quotas.addRows(rowCount) await context.doInAppContext(app.appId, () => {
return quotas.addRows(rowCount)
})
} catch (err: any) { } catch (err: any) {
if (err.code && err.code === errors.codes.USAGE_LIMIT_EXCEEDED) { if (err.code && err.code === errors.codes.USAGE_LIMIT_EXCEEDED) {
// this import resulted in row usage exceeding the quota // this import resulted in row usage exceeding the quota
@ -374,7 +371,7 @@ const appPostCreate = async (ctx: any, app: App) => {
export const create = async (ctx: any) => { export const create = async (ctx: any) => {
const newApplication = await quotas.addApp(() => performAppCreate(ctx)) const newApplication = await quotas.addApp(() => performAppCreate(ctx))
await appPostCreate(ctx, newApplication) await appPostCreate(ctx, newApplication)
await bustCache(CacheKeys.CHECKLIST) await cache.bustCache(cache.CacheKeys.CHECKLIST)
ctx.body = newApplication ctx.body = newApplication
ctx.status = 200 ctx.status = 200
} }
@ -382,7 +379,7 @@ export const create = async (ctx: any) => {
// This endpoint currently operates as a PATCH rather than a PUT // This endpoint currently operates as a PATCH rather than a PUT
// Thus name and url fields are handled only if present // Thus name and url fields are handled only if present
export const update = async (ctx: any) => { export const update = async (ctx: any) => {
const apps = await getAllApps({ dev: true }) const apps = await dbCore.getAllApps({ dev: true })
// validation // validation
const name = ctx.request.body.name const name = ctx.request.body.name
if (name) { if (name) {
@ -455,7 +452,7 @@ const destroyApp = async (ctx: any) => {
let isUnpublish = ctx.query && ctx.query.unpublish let isUnpublish = ctx.query && ctx.query.unpublish
if (isUnpublish) { if (isUnpublish) {
appId = getProdAppID(appId) appId = dbCore.getProdAppID(appId)
} }
const db = isUnpublish ? context.getProdAppDB() : context.getAppDB() const db = isUnpublish ? context.getProdAppDB() : context.getAppDB()
@ -481,7 +478,7 @@ const destroyApp = async (ctx: any) => {
else { else {
await removeAppFromUserRoles(ctx, appId) await removeAppFromUserRoles(ctx, appId)
} }
await appCache.invalidateAppMetadata(appId) await cache.app.invalidateAppMetadata(appId)
return result return result
} }
@ -517,19 +514,17 @@ export const sync = async (ctx: any, next: any) => {
} }
const appId = ctx.params.appId const appId = ctx.params.appId
if (!isDevAppID(appId)) { if (!dbCore.isDevAppID(appId)) {
ctx.throw(400, "This action cannot be performed for production apps") ctx.throw(400, "This action cannot be performed for production apps")
} }
// replicate prod to dev // replicate prod to dev
const prodAppId = getProdAppID(appId) const prodAppId = dbCore.getProdAppID(appId)
try { // specific case, want to make sure setup is skipped
// specific case, want to make sure setup is skipped const prodDb = context.getProdAppDB({ skip_setup: true })
const prodDb = context.getProdAppDB({ skip_setup: true }) const exists = await prodDb.exists()
const info = await prodDb.info() if (!exists) {
if (info.error) throw info.error
} catch (err) {
// the database doesn't exist. Don't replicate // the database doesn't exist. Don't replicate
ctx.status = 200 ctx.status = 200
ctx.body = { ctx.body = {
@ -538,7 +533,7 @@ export const sync = async (ctx: any, next: any) => {
return next() return next()
} }
const replication = new Replication({ const replication = new dbCore.Replication({
source: prodAppId, source: prodAppId,
target: appId, target: appId,
}) })
@ -579,7 +574,7 @@ export const updateAppPackage = async (appPackage: any, appId: any) => {
await db.put(newAppPackage) await db.put(newAppPackage)
// remove any cached metadata, so that it will be updated // remove any cached metadata, so that it will be updated
await appCache.invalidateAppMetadata(appId) await cache.app.invalidateAppMetadata(appId)
return newAppPackage return newAppPackage
}) })
} }

View File

@ -3,6 +3,7 @@ import { InternalTables } from "../../db/utils"
import { getFullUser } from "../../utilities/users" import { getFullUser } from "../../utilities/users"
import { roles, context } from "@budibase/backend-core" import { roles, context } from "@budibase/backend-core"
import { groups } from "@budibase/pro" import { groups } from "@budibase/pro"
import { ContextUser, User } from "@budibase/types"
const PUBLIC_ROLE = roles.BUILTIN_ROLE_IDS.PUBLIC const PUBLIC_ROLE = roles.BUILTIN_ROLE_IDS.PUBLIC
@ -24,7 +25,7 @@ export async function fetchSelf(ctx: any) {
} }
const appId = context.getAppId() const appId = context.getAppId()
const user = await getFullUser(ctx, userId) const user: ContextUser = await getFullUser(ctx, userId)
// this shouldn't be returned by the app self // this shouldn't be returned by the app self
delete user.roles delete user.roles
// forward the csrf token from the session // forward the csrf token from the session
@ -34,7 +35,7 @@ export async function fetchSelf(ctx: any) {
const db = context.getAppDB() const db = context.getAppDB()
// check for group permissions // check for group permissions
if (!user.roleId || user.roleId === PUBLIC_ROLE) { if (!user.roleId || user.roleId === PUBLIC_ROLE) {
const groupRoleId = await groups.getGroupRoleId(user, appId) const groupRoleId = await groups.getGroupRoleId(user as User, appId)
user.roleId = groupRoleId || user.roleId user.roleId = groupRoleId || user.roleId
} }
// remove the full roles structure // remove the full roles structure

View File

@ -1,23 +1,11 @@
import Deployment from "./Deployment" import Deployment from "./Deployment"
import { import { context, db as dbCore, events, cache } from "@budibase/backend-core"
getDevelopmentAppID,
getProdAppID,
Replication,
} from "@budibase/backend-core/db"
import { DocumentType, getAutomationParams } from "../../../db/utils" import { DocumentType, getAutomationParams } from "../../../db/utils"
import { import {
clearMetadata, clearMetadata,
disableAllCrons, disableAllCrons,
enableCronTrigger, enableCronTrigger,
} from "../../../automations/utils" } from "../../../automations/utils"
import { app as appCache } from "@budibase/backend-core/cache"
import {
getAppDB,
getAppId,
getDevAppDB,
getProdAppDB,
} from "@budibase/backend-core/context"
import { events } from "@budibase/backend-core"
import { backups } from "@budibase/pro" import { backups } from "@budibase/pro"
import { AppBackupTrigger } from "@budibase/types" import { AppBackupTrigger } from "@budibase/types"
@ -49,7 +37,7 @@ async function checkAllDeployments(deployments: any) {
async function storeDeploymentHistory(deployment: any) { async function storeDeploymentHistory(deployment: any) {
const deploymentJSON = deployment.getJSON() const deploymentJSON = deployment.getJSON()
const db = getAppDB() const db = context.getAppDB()
let deploymentDoc let deploymentDoc
try { try {
@ -77,7 +65,7 @@ async function storeDeploymentHistory(deployment: any) {
} }
async function initDeployedApp(prodAppId: any) { async function initDeployedApp(prodAppId: any) {
const db = getProdAppDB() const db = context.getProdAppDB()
console.log("Reading automation docs") console.log("Reading automation docs")
const automations = ( const automations = (
await db.allDocs( await db.allDocs(
@ -103,9 +91,9 @@ async function initDeployedApp(prodAppId: any) {
async function deployApp(deployment: any, userId: string) { async function deployApp(deployment: any, userId: string) {
let replication let replication
try { try {
const appId = getAppId() const appId = context.getAppId()!
const devAppId = getDevelopmentAppID(appId) const devAppId = dbCore.getDevelopmentAppID(appId)
const productionAppId = getProdAppID(appId) const productionAppId = dbCore.getProdAppID(appId)
// don't try this if feature isn't allowed, will error // don't try this if feature isn't allowed, will error
if (await backups.isEnabled()) { if (await backups.isEnabled()) {
@ -122,8 +110,8 @@ async function deployApp(deployment: any, userId: string) {
source: devAppId, source: devAppId,
target: productionAppId, target: productionAppId,
} }
replication = new Replication(config) replication = new dbCore.Replication(config)
const devDb = getDevAppDB() const devDb = context.getDevAppDB()
console.log("Compacting development DB") console.log("Compacting development DB")
await devDb.compact() await devDb.compact()
console.log("Replication object created") console.log("Replication object created")
@ -131,7 +119,7 @@ async function deployApp(deployment: any, userId: string) {
console.log("replication complete.. replacing app meta doc") console.log("replication complete.. replacing app meta doc")
// app metadata is excluded as it is likely to be in conflict // app metadata is excluded as it is likely to be in conflict
// replicate the app metadata document manually // replicate the app metadata document manually
const db = getProdAppDB() const db = context.getProdAppDB()
const appDoc = await devDb.get(DocumentType.APP_METADATA) const appDoc = await devDb.get(DocumentType.APP_METADATA)
try { try {
const prodAppDoc = await db.get(DocumentType.APP_METADATA) const prodAppDoc = await db.get(DocumentType.APP_METADATA)
@ -147,7 +135,7 @@ async function deployApp(deployment: any, userId: string) {
// remove automation errors if they exist // remove automation errors if they exist
delete appDoc.automationErrors delete appDoc.automationErrors
await db.put(appDoc) await db.put(appDoc)
await appCache.invalidateAppMetadata(productionAppId) await cache.app.invalidateAppMetadata(productionAppId)
console.log("New app doc written successfully.") console.log("New app doc written successfully.")
await initDeployedApp(productionAppId) await initDeployedApp(productionAppId)
console.log("Deployed app initialised, setting deployment to successful") console.log("Deployed app initialised, setting deployment to successful")
@ -170,7 +158,7 @@ async function deployApp(deployment: any, userId: string) {
export async function fetchDeployments(ctx: any) { export async function fetchDeployments(ctx: any) {
try { try {
const db = getAppDB() const db = context.getAppDB()
const deploymentDoc = await db.get(DocumentType.DEPLOYMENTS) const deploymentDoc = await db.get(DocumentType.DEPLOYMENTS)
const { updated, deployments } = await checkAllDeployments(deploymentDoc) const { updated, deployments } = await checkAllDeployments(deploymentDoc)
if (updated) { if (updated) {
@ -184,7 +172,7 @@ export async function fetchDeployments(ctx: any) {
export async function deploymentProgress(ctx: any) { export async function deploymentProgress(ctx: any) {
try { try {
const db = getAppDB() const db = context.getAppDB()
const deploymentDoc = await db.get(DocumentType.DEPLOYMENTS) const deploymentDoc = await db.get(DocumentType.DEPLOYMENTS)
ctx.body = deploymentDoc[ctx.params.deploymentId] ctx.body = deploymentDoc[ctx.params.deploymentId]
} catch (err) { } catch (err) {
@ -197,7 +185,7 @@ export async function deploymentProgress(ctx: any) {
const isFirstDeploy = async () => { const isFirstDeploy = async () => {
try { try {
const db = getProdAppDB() const db = context.getProdAppDB()
await db.get(DocumentType.APP_METADATA) await db.get(DocumentType.APP_METADATA)
} catch (e: any) { } catch (e: any) {
if (e.status === 404) { if (e.status === 404) {

View File

@ -1,29 +1,23 @@
const fetch = require("node-fetch") import fetch from "node-fetch"
const env = require("../../environment") import env from "../../environment"
const { checkSlashesInUrl } = require("../../utilities") import { checkSlashesInUrl } from "../../utilities"
const { request } = require("../../utilities/workerRequests") import { request } from "../../utilities/workerRequests"
const { clearLock } = require("../../utilities/redis") import { clearLock as redisClearLock } from "../../utilities/redis"
const { Replication, getProdAppID } = require("@budibase/backend-core/db") import { DocumentType } from "../../db/utils"
const { DocumentType } = require("../../db/utils") import { context } from "@budibase/backend-core"
const { app: appCache } = require("@budibase/backend-core/cache") import { events, db as dbCore, cache } from "@budibase/backend-core"
const { getProdAppDB, getAppDB } = require("@budibase/backend-core/context")
const { events } = require("@budibase/backend-core")
async function redirect(ctx, method, path = "global") { async function redirect(ctx: any, method: string, path: string = "global") {
const { devPath } = ctx.params const { devPath } = ctx.params
const queryString = ctx.originalUrl.split("?")[1] || "" const queryString = ctx.originalUrl.split("?")[1] || ""
const response = await fetch( const response = await fetch(
checkSlashesInUrl( checkSlashesInUrl(
`${env.WORKER_URL}/api/${path}/${devPath}?${queryString}` `${env.WORKER_URL}/api/${path}/${devPath}?${queryString}`
), ),
request( request(ctx, {
ctx, method,
{ body: ctx.request.body,
method, })
body: ctx.request.body,
},
true
)
) )
if (response.status !== 200) { if (response.status !== 200) {
const err = await response.text() const err = await response.text()
@ -46,28 +40,28 @@ async function redirect(ctx, method, path = "global") {
ctx.cookies ctx.cookies
} }
exports.buildRedirectGet = path => { export function buildRedirectGet(path: string) {
return async ctx => { return async (ctx: any) => {
await redirect(ctx, "GET", path) await redirect(ctx, "GET", path)
} }
} }
exports.buildRedirectPost = path => { export function buildRedirectPost(path: string) {
return async ctx => { return async (ctx: any) => {
await redirect(ctx, "POST", path) await redirect(ctx, "POST", path)
} }
} }
exports.buildRedirectDelete = path => { export function buildRedirectDelete(path: string) {
return async ctx => { return async (ctx: any) => {
await redirect(ctx, "DELETE", path) await redirect(ctx, "DELETE", path)
} }
} }
exports.clearLock = async ctx => { export async function clearLock(ctx: any) {
const { appId } = ctx.params const { appId } = ctx.params
try { try {
await clearLock(appId, ctx.user) await redisClearLock(appId, ctx.user)
} catch (err) { } catch (err) {
ctx.throw(400, `Unable to remove lock. ${err}`) ctx.throw(400, `Unable to remove lock. ${err}`)
} }
@ -76,16 +70,16 @@ exports.clearLock = async ctx => {
} }
} }
exports.revert = async ctx => { export async function revert(ctx: any) {
const { appId } = ctx.params const { appId } = ctx.params
const productionAppId = getProdAppID(appId) const productionAppId = dbCore.getProdAppID(appId)
// App must have been deployed first // App must have been deployed first
try { try {
const db = getProdAppDB({ skip_setup: true }) const db = context.getProdAppDB({ skip_setup: true })
const info = await db.info() const exists = await db.exists()
if (info.error) { if (!exists) {
throw info.error throw new Error("App must be deployed to be reverted.")
} }
const deploymentDoc = await db.get(DocumentType.DEPLOYMENTS) const deploymentDoc = await db.get(DocumentType.DEPLOYMENTS)
if ( if (
@ -98,7 +92,7 @@ exports.revert = async ctx => {
return ctx.throw(400, "App has not yet been deployed") return ctx.throw(400, "App has not yet been deployed")
} }
const replication = new Replication({ const replication = new dbCore.Replication({
source: productionAppId, source: productionAppId,
target: appId, target: appId,
}) })
@ -109,12 +103,12 @@ exports.revert = async ctx => {
} }
// update appID in reverted app to be dev version again // update appID in reverted app to be dev version again
const db = getAppDB() const db = context.getAppDB()
const appDoc = await db.get(DocumentType.APP_METADATA) const appDoc = await db.get(DocumentType.APP_METADATA)
appDoc.appId = appId appDoc.appId = appId
appDoc.instance._id = appId appDoc.instance._id = appId
await db.put(appDoc) await db.put(appDoc)
await appCache.invalidateAppMetadata(appId) await cache.app.invalidateAppMetadata(appId)
ctx.body = { ctx.body = {
message: "Reverted changes successfully.", message: "Reverted changes successfully.",
} }
@ -126,7 +120,7 @@ exports.revert = async ctx => {
} }
} }
exports.getBudibaseVersion = async ctx => { export async function getBudibaseVersion(ctx: any) {
const version = require("../../../package.json").version const version = require("../../../package.json").version
ctx.body = { ctx.body = {
version, version,

View File

@ -1,5 +1,4 @@
const { getAllApps } = require("@budibase/backend-core/db") import { db as dbCore, context } from "@budibase/backend-core"
const { doInAppContext } = require("@budibase/backend-core/context")
import { search as stringSearch, addRev } from "./utils" import { search as stringSearch, addRev } from "./utils"
import * as controller from "../application" import * as controller from "../application"
import { Application } from "../../../definitions/common" import { Application } from "../../../definitions/common"
@ -15,15 +14,22 @@ function fixAppID(app: Application, params: any) {
} }
async function setResponseApp(ctx: any) { async function setResponseApp(ctx: any) {
if (ctx.body && ctx.body.appId && (!ctx.params || !ctx.params.appId)) { const appId = ctx.body?.appId
ctx.params = { appId: ctx.body.appId } if (appId && (!ctx.params || !ctx.params.appId)) {
ctx.params = { appId }
}
if (appId) {
await context.doInContext(appId, () => {
return controller.fetchAppPackage(ctx)
})
} else {
return controller.fetchAppPackage(ctx)
} }
await controller.fetchAppPackage(ctx)
} }
export async function search(ctx: any, next: any) { export async function search(ctx: any, next: any) {
const { name } = ctx.request.body const { name } = ctx.request.body
const apps = await getAllApps({ all: true }) const apps = await dbCore.getAllApps({ all: true })
ctx.body = stringSearch(apps, name) ctx.body = stringSearch(apps, name)
await next() await next()
} }
@ -41,7 +47,7 @@ export async function create(ctx: any, next: any) {
} }
export async function read(ctx: any, next: any) { export async function read(ctx: any, next: any) {
await doInAppContext(ctx.params.appId, async () => { await context.doInAppContext(ctx.params.appId, async () => {
await setResponseApp(ctx) await setResponseApp(ctx)
await next() await next()
}) })
@ -49,7 +55,7 @@ export async function read(ctx: any, next: any) {
export async function update(ctx: any, next: any) { export async function update(ctx: any, next: any) {
ctx.request.body = await addRev(fixAppID(ctx.request.body, ctx.params)) ctx.request.body = await addRev(fixAppID(ctx.request.body, ctx.params))
await doInAppContext(ctx.params.appId, async () => { await context.doInAppContext(ctx.params.appId, async () => {
await controller.update(ctx) await controller.update(ctx)
await setResponseApp(ctx) await setResponseApp(ctx)
await next() await next()
@ -57,7 +63,7 @@ export async function update(ctx: any, next: any) {
} }
export async function destroy(ctx: any, next: any) { export async function destroy(ctx: any, next: any) {
await doInAppContext(ctx.params.appId, async () => { await context.doInAppContext(ctx.params.appId, async () => {
// get the app before deleting it // get the app before deleting it
await setResponseApp(ctx) await setResponseApp(ctx)
const body = ctx.body const body = ctx.body

View File

@ -5,11 +5,8 @@ import { save as saveDatasource } from "../datasource"
import { RestImporter } from "./import" import { RestImporter } from "./import"
import { invalidateDynamicVariables } from "../../../threads/utils" import { invalidateDynamicVariables } from "../../../threads/utils"
import { QUERY_THREAD_TIMEOUT } from "../../../environment" import { QUERY_THREAD_TIMEOUT } from "../../../environment"
import { getAppDB } from "@budibase/backend-core/context"
import { quotas } from "@budibase/pro" import { quotas } from "@budibase/pro"
import { events } from "@budibase/backend-core" import { events, context, utils, constants } from "@budibase/backend-core"
import { getCookie } from "@budibase/backend-core/utils"
import { Cookies, Configs } from "@budibase/backend-core/constants"
const Runner = new Thread(ThreadType.QUERY, { const Runner = new Thread(ThreadType.QUERY, {
timeoutMs: QUERY_THREAD_TIMEOUT || 10000, timeoutMs: QUERY_THREAD_TIMEOUT || 10000,
@ -28,7 +25,7 @@ function enrichQueries(input: any) {
} }
export async function fetch(ctx: any) { export async function fetch(ctx: any) {
const db = getAppDB() const db = context.getAppDB()
const body = await db.allDocs( const body = await db.allDocs(
getQueryParams(null, { getQueryParams(null, {
@ -81,7 +78,7 @@ const _import = async (ctx: any) => {
export { _import as import } export { _import as import }
export async function save(ctx: any) { export async function save(ctx: any) {
const db = getAppDB() const db = context.getAppDB()
const query = ctx.request.body const query = ctx.request.body
const datasource = await db.get(query.datasourceId) const datasource = await db.get(query.datasourceId)
@ -103,7 +100,7 @@ export async function save(ctx: any) {
} }
export async function find(ctx: any) { export async function find(ctx: any) {
const db = getAppDB() const db = context.getAppDB()
const query = enrichQueries(await db.get(ctx.params.queryId)) const query = enrichQueries(await db.get(ctx.params.queryId))
// remove properties that could be dangerous in real app // remove properties that could be dangerous in real app
if (isProdAppID(ctx.appId)) { if (isProdAppID(ctx.appId)) {
@ -115,13 +112,13 @@ export async function find(ctx: any) {
//Required to discern between OIDC OAuth config entries //Required to discern between OIDC OAuth config entries
function getOAuthConfigCookieId(ctx: any) { function getOAuthConfigCookieId(ctx: any) {
if (ctx.user.providerType === Configs.OIDC) { if (ctx.user.providerType === constants.Config.OIDC) {
return getCookie(ctx, Cookies.OIDC_CONFIG) return utils.getCookie(ctx, constants.Cookie.OIDC_CONFIG)
} }
} }
function getAuthConfig(ctx: any) { function getAuthConfig(ctx: any) {
const authCookie = getCookie(ctx, Cookies.Auth) const authCookie = utils.getCookie(ctx, constants.Cookie.Auth)
let authConfigCtx: any = {} let authConfigCtx: any = {}
authConfigCtx["configId"] = getOAuthConfigCookieId(ctx) authConfigCtx["configId"] = getOAuthConfigCookieId(ctx)
authConfigCtx["sessionId"] = authCookie ? authCookie.sessionId : null authConfigCtx["sessionId"] = authCookie ? authCookie.sessionId : null
@ -129,7 +126,7 @@ function getAuthConfig(ctx: any) {
} }
export async function preview(ctx: any) { export async function preview(ctx: any) {
const db = getAppDB() const db = context.getAppDB()
const datasource = await db.get(ctx.request.body.datasourceId) const datasource = await db.get(ctx.request.body.datasourceId)
const query = ctx.request.body const query = ctx.request.body
@ -201,7 +198,7 @@ async function execute(
ctx: any, ctx: any,
opts: any = { rowsOnly: false, isAutomation: false } opts: any = { rowsOnly: false, isAutomation: false }
) { ) {
const db = getAppDB() const db = context.getAppDB()
const query = await db.get(ctx.params.queryId) const query = await db.get(ctx.params.queryId)
const datasource = await db.get(query.datasourceId) const datasource = await db.get(query.datasourceId)
@ -267,7 +264,7 @@ export async function executeV2(
} }
const removeDynamicVariables = async (queryId: any) => { const removeDynamicVariables = async (queryId: any) => {
const db = getAppDB() const db = context.getAppDB()
const query = await db.get(queryId) const query = await db.get(queryId)
const datasource = await db.get(query.datasourceId) const datasource = await db.get(query.datasourceId)
const dynamicVariables = datasource.config.dynamicVariables const dynamicVariables = datasource.config.dynamicVariables
@ -288,7 +285,7 @@ const removeDynamicVariables = async (queryId: any) => {
} }
export async function destroy(ctx: any) { export async function destroy(ctx: any) {
const db = getAppDB() const db = context.getAppDB()
const queryId = ctx.params.queryId const queryId = ctx.params.queryId
await removeDynamicVariables(queryId) await removeDynamicVariables(queryId)
const query = await db.get(queryId) const query = await db.get(queryId)

View File

@ -6,7 +6,7 @@ const {
DocumentType, DocumentType,
InternalTables, InternalTables,
} = require("../../../db/utils") } = require("../../../db/utils")
const { dangerousGetDB } = require("@budibase/backend-core/db") const { getDB } = require("@budibase/backend-core/db")
const userController = require("../user") const userController = require("../user")
const { const {
inputProcessing, inputProcessing,
@ -251,7 +251,7 @@ exports.fetch = async ctx => {
} }
exports.find = async ctx => { exports.find = async ctx => {
const db = dangerousGetDB(ctx.appId) const db = getDB(ctx.appId)
const table = await db.get(ctx.params.tableId) const table = await db.get(ctx.params.tableId)
let row = await findRow(ctx, ctx.params.tableId, ctx.params.rowId) let row = await findRow(ctx, ctx.params.tableId, ctx.params.rowId)
row = await outputProcessing(table, row) row = await outputProcessing(table, row)

View File

@ -3,8 +3,8 @@ const controller = require("../controllers/automation")
const authorized = require("../../middleware/authorized") const authorized = require("../../middleware/authorized")
const { const {
BUILDER, BUILDER,
PermissionLevels, PermissionLevel,
PermissionTypes, PermissionType,
} = require("@budibase/backend-core/permissions") } = require("@budibase/backend-core/permissions")
const { bodyResource, paramResource } = require("../../middleware/resourceId") const { bodyResource, paramResource } = require("../../middleware/resourceId")
const { const {
@ -71,14 +71,14 @@ router
"/api/automations/:id/trigger", "/api/automations/:id/trigger",
appInfoMiddleware({ appType: AppType.PROD }), appInfoMiddleware({ appType: AppType.PROD }),
paramResource("id"), paramResource("id"),
authorized(PermissionTypes.AUTOMATION, PermissionLevels.EXECUTE), authorized(PermissionType.AUTOMATION, PermissionLevel.EXECUTE),
controller.trigger controller.trigger
) )
.post( .post(
"/api/automations/:id/test", "/api/automations/:id/test",
appInfoMiddleware({ appType: AppType.DEV }), appInfoMiddleware({ appType: AppType.DEV }),
paramResource("id"), paramResource("id"),
authorized(PermissionTypes.AUTOMATION, PermissionLevels.EXECUTE), authorized(PermissionType.AUTOMATION, PermissionLevel.EXECUTE),
controller.test controller.test
) )

View File

@ -3,8 +3,8 @@ const datasourceController = require("../controllers/datasource")
const authorized = require("../../middleware/authorized") const authorized = require("../../middleware/authorized")
const { const {
BUILDER, BUILDER,
PermissionLevels, PermissionLevel,
PermissionTypes, PermissionType,
} = require("@budibase/backend-core/permissions") } = require("@budibase/backend-core/permissions")
const { const {
datasourceValidator, datasourceValidator,
@ -17,17 +17,17 @@ router
.get("/api/datasources", authorized(BUILDER), datasourceController.fetch) .get("/api/datasources", authorized(BUILDER), datasourceController.fetch)
.get( .get(
"/api/datasources/:datasourceId", "/api/datasources/:datasourceId",
authorized(PermissionTypes.TABLE, PermissionLevels.READ), authorized(PermissionType.TABLE, PermissionLevel.READ),
datasourceController.find datasourceController.find
) )
.put( .put(
"/api/datasources/:datasourceId", "/api/datasources/:datasourceId",
authorized(PermissionTypes.TABLE, PermissionLevels.READ), authorized(PermissionType.TABLE, PermissionLevel.READ),
datasourceController.update datasourceController.update
) )
.post( .post(
"/api/datasources/query", "/api/datasources/query",
authorized(PermissionTypes.TABLE, PermissionLevels.READ), authorized(PermissionType.TABLE, PermissionLevel.READ),
datasourceQueryValidator(), datasourceQueryValidator(),
datasourceController.query datasourceController.query
) )

View File

@ -13,8 +13,8 @@ import env from "../../../environment"
const Router = require("@koa/router") const Router = require("@koa/router")
const { RateLimit, Stores } = require("koa2-ratelimit") const { RateLimit, Stores } = require("koa2-ratelimit")
const { const {
PermissionLevels, PermissionLevel,
PermissionTypes, PermissionType,
} = require("@budibase/backend-core/permissions") } = require("@budibase/backend-core/permissions")
const { getRedisOptions } = require("@budibase/backend-core/redis").utils const { getRedisOptions } = require("@budibase/backend-core/redis").utils
@ -105,7 +105,7 @@ function applyRoutes(
: paramResource(resource) : paramResource(resource)
const publicApiMiddleware = publicApi({ const publicApiMiddleware = publicApi({
requiresAppId: requiresAppId:
permType !== PermissionTypes.APP && permType !== PermissionTypes.USER, permType !== PermissionType.APP && permType !== PermissionType.USER,
}) })
addMiddleware(endpoints.read, publicApiMiddleware) addMiddleware(endpoints.read, publicApiMiddleware)
addMiddleware(endpoints.write, publicApiMiddleware) addMiddleware(endpoints.write, publicApiMiddleware)
@ -113,8 +113,8 @@ function applyRoutes(
addMiddleware(endpoints.read, paramMiddleware) addMiddleware(endpoints.read, paramMiddleware)
addMiddleware(endpoints.write, paramMiddleware) addMiddleware(endpoints.write, paramMiddleware)
// add the authorization middleware, using the correct perm type // add the authorization middleware, using the correct perm type
addMiddleware(endpoints.read, authorized(permType, PermissionLevels.READ)) addMiddleware(endpoints.read, authorized(permType, PermissionLevel.READ))
addMiddleware(endpoints.write, authorized(permType, PermissionLevels.WRITE)) addMiddleware(endpoints.write, authorized(permType, PermissionLevel.WRITE))
// add the output mapper middleware // add the output mapper middleware
addMiddleware(endpoints.read, mapperMiddleware, { output: true }) addMiddleware(endpoints.read, mapperMiddleware, { output: true })
addMiddleware(endpoints.write, mapperMiddleware, { output: true }) addMiddleware(endpoints.write, mapperMiddleware, { output: true })
@ -122,12 +122,12 @@ function applyRoutes(
addToRouter(endpoints.write) addToRouter(endpoints.write)
} }
applyRoutes(appEndpoints, PermissionTypes.APP, "appId") applyRoutes(appEndpoints, PermissionType.APP, "appId")
applyRoutes(tableEndpoints, PermissionTypes.TABLE, "tableId") applyRoutes(tableEndpoints, PermissionType.TABLE, "tableId")
applyRoutes(userEndpoints, PermissionTypes.USER, "userId") applyRoutes(userEndpoints, PermissionType.USER, "userId")
applyRoutes(queryEndpoints, PermissionTypes.QUERY, "queryId") applyRoutes(queryEndpoints, PermissionType.QUERY, "queryId")
// needs to be applied last for routing purposes, don't override other endpoints // needs to be applied last for routing purposes, don't override other endpoints
applyRoutes(rowEndpoints, PermissionTypes.TABLE, "tableId", "rowId") applyRoutes(rowEndpoints, PermissionType.TABLE, "tableId", "rowId")
export default publicRouter export default publicRouter

View File

@ -31,7 +31,7 @@ async function makeRequest(method, endpoint, body, appId = config.getAppId()) {
if (body) { if (body) {
req.send(body) req.send(body)
} }
const res = await req.expect("Content-Type", /json/).expect(200) const res = await req
expect(res.body).toBeDefined() expect(res.body).toBeDefined()
return res return res
} }

View File

@ -2,8 +2,8 @@ const Router = require("@koa/router")
const queryController = require("../controllers/query") const queryController = require("../controllers/query")
const authorized = require("../../middleware/authorized") const authorized = require("../../middleware/authorized")
const { const {
PermissionLevels, PermissionLevel,
PermissionTypes, PermissionType,
BUILDER, BUILDER,
} = require("@budibase/backend-core/permissions") } = require("@budibase/backend-core/permissions")
const { const {
@ -38,20 +38,20 @@ router
.get( .get(
"/api/queries/:queryId", "/api/queries/:queryId",
paramResource("queryId"), paramResource("queryId"),
authorized(PermissionTypes.QUERY, PermissionLevels.READ), authorized(PermissionType.QUERY, PermissionLevel.READ),
queryController.find queryController.find
) )
// DEPRECATED - use new query endpoint for future work // DEPRECATED - use new query endpoint for future work
.post( .post(
"/api/queries/:queryId", "/api/queries/:queryId",
paramResource("queryId"), paramResource("queryId"),
authorized(PermissionTypes.QUERY, PermissionLevels.WRITE), authorized(PermissionType.QUERY, PermissionLevel.WRITE),
queryController.executeV1 queryController.executeV1
) )
.post( .post(
"/api/v2/queries/:queryId", "/api/v2/queries/:queryId",
paramResource("queryId"), paramResource("queryId"),
authorized(PermissionTypes.QUERY, PermissionLevels.WRITE), authorized(PermissionType.QUERY, PermissionLevel.WRITE),
queryController.executeV2 queryController.executeV2
) )
.delete( .delete(

View File

@ -3,8 +3,8 @@ import * as rowController from "../controllers/row"
import authorized from "../../middleware/authorized" import authorized from "../../middleware/authorized"
import { paramResource, paramSubResource } from "../../middleware/resourceId" import { paramResource, paramSubResource } from "../../middleware/resourceId"
const { const {
PermissionLevels, PermissionLevel,
PermissionTypes, PermissionType,
} = require("@budibase/backend-core/permissions") } = require("@budibase/backend-core/permissions")
const { internalSearchValidator } = require("./utils/validators") const { internalSearchValidator } = require("./utils/validators")
@ -28,7 +28,7 @@ router
.get( .get(
"/api/:tableId/:rowId/enrich", "/api/:tableId/:rowId/enrich",
paramSubResource("tableId", "rowId"), paramSubResource("tableId", "rowId"),
authorized(PermissionTypes.TABLE, PermissionLevels.READ), authorized(PermissionType.TABLE, PermissionLevel.READ),
rowController.fetchEnrichedRow rowController.fetchEnrichedRow
) )
/** /**
@ -48,7 +48,7 @@ router
.get( .get(
"/api/:tableId/rows", "/api/:tableId/rows",
paramResource("tableId"), paramResource("tableId"),
authorized(PermissionTypes.TABLE, PermissionLevels.READ), authorized(PermissionType.TABLE, PermissionLevel.READ),
rowController.fetch rowController.fetch
) )
/** /**
@ -67,7 +67,7 @@ router
.get( .get(
"/api/:tableId/rows/:rowId", "/api/:tableId/rows/:rowId",
paramSubResource("tableId", "rowId"), paramSubResource("tableId", "rowId"),
authorized(PermissionTypes.TABLE, PermissionLevels.READ), authorized(PermissionType.TABLE, PermissionLevel.READ),
rowController.find rowController.find
) )
/** /**
@ -137,7 +137,7 @@ router
"/api/:tableId/search", "/api/:tableId/search",
internalSearchValidator(), internalSearchValidator(),
paramResource("tableId"), paramResource("tableId"),
authorized(PermissionTypes.TABLE, PermissionLevels.READ), authorized(PermissionType.TABLE, PermissionLevel.READ),
rowController.search rowController.search
) )
// DEPRECATED - this is an old API, but for backwards compat it needs to be // DEPRECATED - this is an old API, but for backwards compat it needs to be
@ -145,7 +145,7 @@ router
.post( .post(
"/api/search/:tableId/rows", "/api/search/:tableId/rows",
paramResource("tableId"), paramResource("tableId"),
authorized(PermissionTypes.TABLE, PermissionLevels.READ), authorized(PermissionType.TABLE, PermissionLevel.READ),
rowController.search rowController.search
) )
/** /**
@ -175,7 +175,7 @@ router
.post( .post(
"/api/:tableId/rows", "/api/:tableId/rows",
paramResource("tableId"), paramResource("tableId"),
authorized(PermissionTypes.TABLE, PermissionLevels.WRITE), authorized(PermissionType.TABLE, PermissionLevel.WRITE),
rowController.save rowController.save
) )
/** /**
@ -189,7 +189,7 @@ router
.patch( .patch(
"/api/:tableId/rows", "/api/:tableId/rows",
paramResource("tableId"), paramResource("tableId"),
authorized(PermissionTypes.TABLE, PermissionLevels.WRITE), authorized(PermissionType.TABLE, PermissionLevel.WRITE),
rowController.patch rowController.patch
) )
/** /**
@ -215,7 +215,7 @@ router
.post( .post(
"/api/:tableId/rows/validate", "/api/:tableId/rows/validate",
paramResource("tableId"), paramResource("tableId"),
authorized(PermissionTypes.TABLE, PermissionLevels.WRITE), authorized(PermissionType.TABLE, PermissionLevel.WRITE),
rowController.validate rowController.validate
) )
/** /**
@ -241,7 +241,7 @@ router
.delete( .delete(
"/api/:tableId/rows", "/api/:tableId/rows",
paramResource("tableId"), paramResource("tableId"),
authorized(PermissionTypes.TABLE, PermissionLevels.WRITE), authorized(PermissionType.TABLE, PermissionLevel.WRITE),
rowController.destroy rowController.destroy
) )
@ -261,7 +261,7 @@ router
.post( .post(
"/api/:tableId/rows/exportRows", "/api/:tableId/rows/exportRows",
paramResource("tableId"), paramResource("tableId"),
authorized(PermissionTypes.TABLE, PermissionLevels.WRITE), authorized(PermissionType.TABLE, PermissionLevel.WRITE),
rowController.exportRows rowController.exportRows
) )

View File

@ -4,8 +4,8 @@ import { budibaseTempDir } from "../../utilities/budibaseDir"
import authorized from "../../middleware/authorized" import authorized from "../../middleware/authorized"
import { import {
BUILDER, BUILDER,
PermissionTypes, PermissionType,
PermissionLevels, PermissionLevel,
} from "@budibase/backend-core/permissions" } from "@budibase/backend-core/permissions"
import * as env from "../../environment" import * as env from "../../environment"
import { paramResource } from "../../middleware/resourceId" import { paramResource } from "../../middleware/resourceId"
@ -47,13 +47,13 @@ router
.post( .post(
"/api/attachments/:tableId/upload", "/api/attachments/:tableId/upload",
paramResource("tableId"), paramResource("tableId"),
authorized(PermissionTypes.TABLE, PermissionLevels.WRITE), authorized(PermissionType.TABLE, PermissionLevel.WRITE),
controller.uploadFile controller.uploadFile
) )
.post( .post(
"/api/attachments/:tableId/delete", "/api/attachments/:tableId/delete",
paramResource("tableId"), paramResource("tableId"),
authorized(PermissionTypes.TABLE, PermissionLevels.WRITE), authorized(PermissionType.TABLE, PermissionLevel.WRITE),
controller.deleteObjects controller.deleteObjects
) )
.get("/app/preview", authorized(BUILDER), controller.serveBuilderPreview) .get("/app/preview", authorized(BUILDER), controller.serveBuilderPreview)
@ -61,7 +61,7 @@ router
.get("/app/:appUrl/:path*", controller.serveApp) .get("/app/:appUrl/:path*", controller.serveApp)
.post( .post(
"/api/attachments/:datasourceId/url", "/api/attachments/:datasourceId/url",
authorized(PermissionTypes.TABLE, PermissionLevels.READ), authorized(PermissionType.TABLE, PermissionLevel.READ),
controller.getSignedUploadURL controller.getSignedUploadURL
) )

View File

@ -4,8 +4,8 @@ const authorized = require("../../middleware/authorized")
const { paramResource, bodyResource } = require("../../middleware/resourceId") const { paramResource, bodyResource } = require("../../middleware/resourceId")
const { const {
BUILDER, BUILDER,
PermissionLevels, PermissionLevel,
PermissionTypes, PermissionType,
} = require("@budibase/backend-core/permissions") } = require("@budibase/backend-core/permissions")
const { tableValidator } = require("./utils/validators") const { tableValidator } = require("./utils/validators")
@ -40,7 +40,7 @@ router
.get( .get(
"/api/tables/:tableId", "/api/tables/:tableId",
paramResource("tableId"), paramResource("tableId"),
authorized(PermissionTypes.TABLE, PermissionLevels.READ, { schema: true }), authorized(PermissionType.TABLE, PermissionLevel.READ, { schema: true }),
tableController.find tableController.find
) )
/** /**

View File

@ -43,7 +43,7 @@ describe("/static", () => {
it("should ping from app", async () => { it("should ping from app", async () => {
const headers = config.defaultHeaders() const headers = config.defaultHeaders()
headers[constants.Headers.APP_ID] = config.prodAppId headers[constants.Header.APP_ID] = config.prodAppId
await request await request
.post("/api/bbtel/ping") .post("/api/bbtel/ping")

View File

@ -4,16 +4,21 @@ jest.mock("node-fetch")
// Mock isProdAppID to we can later mock the implementation and pretend we are // Mock isProdAppID to we can later mock the implementation and pretend we are
// using prod app IDs // using prod app IDs
const authDb = require("@budibase/backend-core/db") jest.mock("@budibase/backend-core", () => {
const { isProdAppID } = authDb const core = jest.requireActual("@budibase/backend-core")
const mockIsProdAppID = jest.fn(isProdAppID) return {
authDb.isProdAppID = mockIsProdAppID ...core,
db: {
...core.db,
isProdAppID: jest.fn(),
}
}
})
const setup = require("./utilities") const setup = require("./utilities")
const { checkBuilderEndpoint } = require("./utilities/TestFunctions") const { checkBuilderEndpoint } = require("./utilities/TestFunctions")
const { checkCacheForDynamicVariable } = require("../../../threads/utils") const { checkCacheForDynamicVariable } = require("../../../threads/utils")
const { basicQuery, basicDatasource } = setup.structures const { basicQuery, basicDatasource } = setup.structures
const { events } = require("@budibase/backend-core") const { events, db: dbCore } = require("@budibase/backend-core")
describe("/queries", () => { describe("/queries", () => {
let request = setup.getRequest() let request = setup.getRequest()
@ -152,8 +157,8 @@ describe("/queries", () => {
it("should remove sensitive info for prod apps", async () => { it("should remove sensitive info for prod apps", async () => {
// Mock isProdAppID to pretend we are using a prod app // Mock isProdAppID to pretend we are using a prod app
mockIsProdAppID.mockClear() dbCore.isProdAppID.mockClear()
mockIsProdAppID.mockImplementation(() => true) dbCore.isProdAppID.mockImplementation(() => true)
const query = await config.createQuery() const query = await config.createQuery()
const res = await request const res = await request
@ -167,8 +172,8 @@ describe("/queries", () => {
expect(res.body.schema).toBeDefined() expect(res.body.schema).toBeDefined()
// Reset isProdAppID mock // Reset isProdAppID mock
expect(mockIsProdAppID).toHaveBeenCalledTimes(1) expect(dbCore.isProdAppID).toHaveBeenCalledTimes(1)
mockIsProdAppID.mockImplementation(isProdAppID) dbCore.isProdAppID.mockImplementation(() => false)
}) })
}) })

View File

@ -1,6 +1,6 @@
const { BUILTIN_ROLE_IDS } = require("@budibase/backend-core/roles") const { BUILTIN_ROLE_IDS } = require("@budibase/backend-core/roles")
const { const {
BUILTIN_PERMISSION_IDS, BuiltinPermissionID,
} = require("@budibase/backend-core/permissions") } = require("@budibase/backend-core/permissions")
const setup = require("./utilities") const setup = require("./utilities")
const { basicRole } = setup.structures const { basicRole } = setup.structures
@ -76,18 +76,18 @@ describe("/roles", () => {
const adminRole = res.body.find(r => r._id === BUILTIN_ROLE_IDS.ADMIN) const adminRole = res.body.find(r => r._id === BUILTIN_ROLE_IDS.ADMIN)
expect(adminRole).toBeDefined() expect(adminRole).toBeDefined()
expect(adminRole.inherits).toEqual(BUILTIN_ROLE_IDS.POWER) expect(adminRole.inherits).toEqual(BUILTIN_ROLE_IDS.POWER)
expect(adminRole.permissionId).toEqual(BUILTIN_PERMISSION_IDS.ADMIN) expect(adminRole.permissionId).toEqual(BuiltinPermissionID.ADMIN)
const powerUserRole = res.body.find(r => r._id === BUILTIN_ROLE_IDS.POWER) const powerUserRole = res.body.find(r => r._id === BUILTIN_ROLE_IDS.POWER)
expect(powerUserRole).toBeDefined() expect(powerUserRole).toBeDefined()
expect(powerUserRole.inherits).toEqual(BUILTIN_ROLE_IDS.BASIC) expect(powerUserRole.inherits).toEqual(BUILTIN_ROLE_IDS.BASIC)
expect(powerUserRole.permissionId).toEqual(BUILTIN_PERMISSION_IDS.POWER) expect(powerUserRole.permissionId).toEqual(BuiltinPermissionID.POWER)
const customRoleFetched = res.body.find(r => r._id === customRole._id) const customRoleFetched = res.body.find(r => r._id === customRole._id)
expect(customRoleFetched).toBeDefined() expect(customRoleFetched).toBeDefined()
expect(customRoleFetched.inherits).toEqual(BUILTIN_ROLE_IDS.BASIC) expect(customRoleFetched.inherits).toEqual(BUILTIN_ROLE_IDS.BASIC)
expect(customRoleFetched.permissionId).toEqual( expect(customRoleFetched.permissionId).toEqual(
BUILTIN_PERMISSION_IDS.READ_ONLY BuiltinPermissionID.READ_ONLY
) )
}) })
@ -109,7 +109,7 @@ describe("/roles", () => {
it("should delete custom roles", async () => { it("should delete custom roles", async () => {
const customRole = await config.createRole({ const customRole = await config.createRole({
name: "user", name: "user",
permissionId: BUILTIN_PERMISSION_IDS.READ_ONLY, permissionId: BuiltinPermissionID.READ_ONLY,
inherits: BUILTIN_ROLE_IDS.BASIC, inherits: BUILTIN_ROLE_IDS.BASIC,
}) })
delete customRole._rev_tree delete customRole._rev_tree

View File

@ -46,7 +46,7 @@ describe("/static", () => {
it("should serve the app by id", async () => { it("should serve the app by id", async () => {
const headers = config.defaultHeaders() const headers = config.defaultHeaders()
delete headers[constants.Headers.APP_ID] delete headers[constants.Header.APP_ID]
const res = await request const res = await request
.get(`/${config.prodAppId}`) .get(`/${config.prodAppId}`)
@ -58,7 +58,7 @@ describe("/static", () => {
it("should serve the app by url", async () => { it("should serve the app by url", async () => {
const headers = config.defaultHeaders() const headers = config.defaultHeaders()
delete headers[constants.Headers.APP_ID] delete headers[constants.Header.APP_ID]
const res = await request const res = await request
.get(`/app${config.prodApp.url}`) .get(`/app${config.prodApp.url}`)

View File

@ -1,17 +1,18 @@
const { checkBuilderEndpoint } = require("./utilities/TestFunctions") const { checkBuilderEndpoint } = require("./utilities/TestFunctions")
const { getAppDB } = require("@budibase/backend-core/context")
const setup = require("./utilities") const setup = require("./utilities")
const { basicTable } = setup.structures const { basicTable } = setup.structures
const { events } = require("@budibase/backend-core") const { events, context } = require("@budibase/backend-core")
describe("/tables", () => { describe("/tables", () => {
let request = setup.getRequest() let request = setup.getRequest()
let config = setup.getConfig() let config = setup.getConfig()
let appId
afterAll(setup.afterAll) afterAll(setup.afterAll)
beforeEach(async () => { beforeEach(async () => {
await config.init() const app = await config.init()
appId = app.appId
}) })
describe("create", () => { describe("create", () => {
@ -199,38 +200,6 @@ describe("/tables", () => {
}) })
}) })
describe("indexing", () => {
it("should be able to create a table with indexes", async () => {
const db = getAppDB(config)
const indexCount = (await db.getIndexes()).total_rows
const table = basicTable()
table.indexes = ["name"]
const res = await request
.post(`/api/tables`)
.send(table)
.set(config.defaultHeaders())
.expect('Content-Type', /json/)
.expect(200)
expect(res.body._id).toBeDefined()
expect(res.body._rev).toBeDefined()
expect((await db.getIndexes()).total_rows).toEqual(indexCount + 1)
// update index to see what happens
table.indexes = ["name", "description"]
await request
.post(`/api/tables`)
.send({
...table,
_id: res.body._id,
_rev: res.body._rev,
})
.set(config.defaultHeaders())
.expect('Content-Type', /json/)
.expect(200)
// shouldn't have created a new index
expect((await db.getIndexes()).total_rows).toEqual(indexCount + 1)
})
})
describe("validate csv", () => { describe("validate csv", () => {
it("should be able to validate a CSV layout", async () => { it("should be able to validate a CSV layout", async () => {
const res = await request const res = await request
@ -249,6 +218,40 @@ describe("/tables", () => {
}) })
}) })
describe("indexing", () => {
it("should be able to create a table with indexes", async () => {
await context.doInAppContext(appId, async () => {
const db = context.getAppDB()
const indexCount = (await db.getIndexes()).total_rows
const table = basicTable()
table.indexes = ["name"]
const res = await request
.post(`/api/tables`)
.send(table)
.set(config.defaultHeaders())
.expect('Content-Type', /json/)
.expect(200)
expect(res.body._id).toBeDefined()
expect(res.body._rev).toBeDefined()
expect((await db.getIndexes()).total_rows).toEqual(indexCount + 1)
// update index to see what happens
table.indexes = ["name", "description"]
await request
.post(`/api/tables`)
.send({
...table,
_id: res.body._id,
_rev: res.body._rev,
})
.set(config.defaultHeaders())
.expect('Content-Type', /json/)
.expect(200)
// shouldn't have created a new index
expect((await db.getIndexes()).total_rows).toEqual(indexCount + 1)
})
})
})
describe("destroy", () => { describe("destroy", () => {
let testTable let testTable

View File

@ -2,8 +2,8 @@ const Router = require("@koa/router")
const controller = require("../controllers/user") const controller = require("../controllers/user")
const authorized = require("../../middleware/authorized") const authorized = require("../../middleware/authorized")
const { const {
PermissionLevels, PermissionLevel,
PermissionTypes, PermissionType,
} = require("@budibase/backend-core/permissions") } = require("@budibase/backend-core/permissions")
const router = new Router() const router = new Router()
@ -11,42 +11,42 @@ const router = new Router()
router router
.get( .get(
"/api/users/metadata", "/api/users/metadata",
authorized(PermissionTypes.USER, PermissionLevels.READ), authorized(PermissionType.USER, PermissionLevel.READ),
controller.fetchMetadata controller.fetchMetadata
) )
.get( .get(
"/api/users/metadata/:id", "/api/users/metadata/:id",
authorized(PermissionTypes.USER, PermissionLevels.READ), authorized(PermissionType.USER, PermissionLevel.READ),
controller.findMetadata controller.findMetadata
) )
.put( .put(
"/api/users/metadata", "/api/users/metadata",
authorized(PermissionTypes.USER, PermissionLevels.WRITE), authorized(PermissionType.USER, PermissionLevel.WRITE),
controller.updateMetadata controller.updateMetadata
) )
.post( .post(
"/api/users/metadata/self", "/api/users/metadata/self",
authorized(PermissionTypes.USER, PermissionLevels.WRITE), authorized(PermissionType.USER, PermissionLevel.WRITE),
controller.updateSelfMetadata controller.updateSelfMetadata
) )
.delete( .delete(
"/api/users/metadata/:id", "/api/users/metadata/:id",
authorized(PermissionTypes.USER, PermissionLevels.WRITE), authorized(PermissionType.USER, PermissionLevel.WRITE),
controller.destroyMetadata controller.destroyMetadata
) )
.post( .post(
"/api/users/metadata/sync/:id", "/api/users/metadata/sync/:id",
authorized(PermissionTypes.USER, PermissionLevels.WRITE), authorized(PermissionType.USER, PermissionLevel.WRITE),
controller.syncUser controller.syncUser
) )
.post( .post(
"/api/users/flags", "/api/users/flags",
authorized(PermissionTypes.USER, PermissionLevels.WRITE), authorized(PermissionType.USER, PermissionLevel.WRITE),
controller.setFlag controller.setFlag
) )
.get( .get(
"/api/users/flags", "/api/users/flags",
authorized(PermissionTypes.USER, PermissionLevels.READ), authorized(PermissionType.USER, PermissionLevel.READ),
controller.getFlags controller.getFlags
) )

View File

@ -1,8 +1,8 @@
const { joiValidator } = require("@budibase/backend-core/auth") const { joiValidator } = require("@budibase/backend-core/auth")
const { DataSourceOperation } = require("../../../constants") const { DataSourceOperation } = require("../../../constants")
const { const {
BUILTIN_PERMISSION_IDS, BuiltinPermissionID,
PermissionLevels, PermissionLevel,
} = require("@budibase/backend-core/permissions") } = require("@budibase/backend-core/permissions")
const { WebhookActionType } = require("@budibase/types") const { WebhookActionType } = require("@budibase/types")
const Joi = require("joi") const Joi = require("joi")
@ -133,14 +133,14 @@ exports.webhookValidator = () => {
} }
exports.roleValidator = () => { exports.roleValidator = () => {
const permLevelArray = Object.values(PermissionLevels) const permLevelArray = Object.values(PermissionLevel)
// prettier-ignore // prettier-ignore
return joiValidator.body(Joi.object({ return joiValidator.body(Joi.object({
_id: OPTIONAL_STRING, _id: OPTIONAL_STRING,
_rev: OPTIONAL_STRING, _rev: OPTIONAL_STRING,
name: Joi.string().required(), name: Joi.string().required(),
// this is the base permission ID (for now a built in) // this is the base permission ID (for now a built in)
permissionId: Joi.string().valid(...Object.values(BUILTIN_PERMISSION_IDS)).required(), permissionId: Joi.string().valid(...Object.values(BuiltinPermissionID)).required(),
permissions: Joi.object() permissions: Joi.object()
.pattern(/.*/, [Joi.string().valid(...permLevelArray)]) .pattern(/.*/, [Joi.string().valid(...permLevelArray)])
.optional(), .optional(),
@ -149,7 +149,7 @@ exports.roleValidator = () => {
} }
exports.permissionValidator = () => { exports.permissionValidator = () => {
const permLevelArray = Object.values(PermissionLevels) const permLevelArray = Object.values(PermissionLevel)
// prettier-ignore // prettier-ignore
return joiValidator.params(Joi.object({ return joiValidator.params(Joi.object({
level: Joi.string().valid(...permLevelArray).required(), level: Joi.string().valid(...permLevelArray).required(),

View File

@ -5,8 +5,8 @@ const authorized = require("../../middleware/authorized")
const { paramResource } = require("../../middleware/resourceId") const { paramResource } = require("../../middleware/resourceId")
const { const {
BUILDER, BUILDER,
PermissionTypes, PermissionType,
PermissionLevels, PermissionLevel,
} = require("@budibase/backend-core/permissions") } = require("@budibase/backend-core/permissions")
const router = new Router() const router = new Router()
@ -16,7 +16,7 @@ router
.get( .get(
"/api/views/:viewName", "/api/views/:viewName",
paramResource("viewName"), paramResource("viewName"),
authorized(PermissionTypes.VIEW, PermissionLevels.READ), authorized(PermissionType.VIEW, PermissionLevel.READ),
rowController.fetchView rowController.fetchView
) )
.get("/api/views", authorized(BUILDER), viewController.fetch) .get("/api/views", authorized(BUILDER), viewController.fetch)

View File

@ -1,4 +1,3 @@
const env = require("../../environment")
const setup = require("./utilities") const setup = require("./utilities")
describe("test the update row action", () => { describe("test the update row action", () => {

View File

@ -1,6 +1,5 @@
const TestConfig = require("../../../tests/utilities/TestConfiguration") const TestConfig = require("../../../tests/utilities/TestConfiguration")
const { TENANT_ID } = require("../../../tests/utilities/structures") const { context } = require("@budibase/backend-core")
const { doInTenant } = require("@budibase/backend-core/tenancy")
const actions = require("../../actions") const actions = require("../../actions")
const emitter = require("../../../events/index") const emitter = require("../../../events/index")
const env = require("../../../environment") const env = require("../../../environment")
@ -33,7 +32,7 @@ exports.runInProd = async fn => {
} }
exports.runStep = async function runStep(stepId, inputs) { exports.runStep = async function runStep(stepId, inputs) {
return doInTenant(TENANT_ID, async () => { async function run() {
let step = await actions.getAction(stepId) let step = await actions.getAction(stepId)
expect(step).toBeDefined() expect(step).toBeDefined()
return step({ return step({
@ -43,7 +42,14 @@ exports.runStep = async function runStep(stepId, inputs) {
apiKey: exports.apiKey, apiKey: exports.apiKey,
emitter, emitter,
}) })
}) }
if (config?.appId) {
return context.doInContext(config?.appId, async () => {
return run()
})
} else {
return run()
}
} }
exports.apiKey = "test" exports.apiKey = "test"

View File

@ -4,15 +4,9 @@ import { automationQueue } from "./bullboard"
import newid from "../db/newid" import newid from "../db/newid"
import { updateEntityMetadata } from "../utilities" import { updateEntityMetadata } from "../utilities"
import { MetadataTypes } from "../constants" import { MetadataTypes } from "../constants"
import { getProdAppID, doWithDB } from "@budibase/backend-core/db" import { db as dbCore, context } from "@budibase/backend-core"
import { getAutomationMetadataParams } from "../db/utils" import { getAutomationMetadataParams } from "../db/utils"
import { cloneDeep } from "lodash/fp" import { cloneDeep } from "lodash/fp"
import {
getAppDB,
getAppId,
getProdAppDB,
} from "@budibase/backend-core/context"
import { context } from "@budibase/backend-core"
import { quotas } from "@budibase/pro" import { quotas } from "@budibase/pro"
import { Automation, WebhookActionType } from "@budibase/types" import { Automation, WebhookActionType } from "@budibase/types"
import sdk from "../sdk" import sdk from "../sdk"
@ -102,7 +96,7 @@ export async function disableCronById(jobId: number | string) {
} }
export async function clearMetadata() { export async function clearMetadata() {
const db = getProdAppDB() const db = context.getProdAppDB()
const automationMetadata = ( const automationMetadata = (
await db.allDocs( await db.allDocs(
getAutomationMetadataParams({ getAutomationMetadataParams({
@ -157,7 +151,7 @@ export async function enableCronTrigger(appId: any, automation: Automation) {
// can't use getAppDB here as this is likely to be called from dev app, // can't use getAppDB here as this is likely to be called from dev app,
// but this call could be for dev app or prod app, need to just use what // but this call could be for dev app or prod app, need to just use what
// was passed in // was passed in
await doWithDB(appId, async (db: any) => { await dbCore.doWithDB(appId, async (db: any) => {
const response = await db.put(automation) const response = await db.put(automation)
automation._id = response.id automation._id = response.id
automation._rev = response.rev automation._rev = response.rev
@ -175,7 +169,10 @@ export async function enableCronTrigger(appId: any, automation: Automation) {
* written to DB (this does not write to DB as it would be wasteful to repeat). * written to DB (this does not write to DB as it would be wasteful to repeat).
*/ */
export async function checkForWebhooks({ oldAuto, newAuto }: any) { export async function checkForWebhooks({ oldAuto, newAuto }: any) {
const appId = getAppId() const appId = context.getAppId()
if (!appId) {
throw new Error("Unable to check webhooks - no app ID in context.")
}
const oldTrigger = oldAuto ? oldAuto.definition.trigger : null const oldTrigger = oldAuto ? oldAuto.definition.trigger : null
const newTrigger = newAuto ? newAuto.definition.trigger : null const newTrigger = newAuto ? newAuto.definition.trigger : null
const triggerChanged = const triggerChanged =
@ -194,7 +191,7 @@ export async function checkForWebhooks({ oldAuto, newAuto }: any) {
oldTrigger.webhookId oldTrigger.webhookId
) { ) {
try { try {
let db = getAppDB() let db = context.getAppDB()
// need to get the webhook to get the rev // need to get the webhook to get the rev
const webhook = await db.get(oldTrigger.webhookId) const webhook = await db.get(oldTrigger.webhookId)
// might be updating - reset the inputs to remove the URLs // might be updating - reset the inputs to remove the URLs
@ -224,7 +221,7 @@ export async function checkForWebhooks({ oldAuto, newAuto }: any) {
// the app ID has to be development for this endpoint // the app ID has to be development for this endpoint
// it can only be used when building the app // it can only be used when building the app
// but the trigger endpoint will always be used in production // but the trigger endpoint will always be used in production
const prodAppId = getProdAppID(appId) const prodAppId = dbCore.getProdAppID(appId)
newTrigger.inputs = { newTrigger.inputs = {
schemaUrl: `api/webhooks/schema/${appId}/${id}`, schemaUrl: `api/webhooks/schema/${appId}/${id}`,
triggerUrl: `api/webhooks/trigger/${prodAppId}/${id}`, triggerUrl: `api/webhooks/trigger/${prodAppId}/${id}`,

View File

@ -2,7 +2,7 @@ const newid = require("./newid")
// bypass the main application db config // bypass the main application db config
// use in memory pouchdb directly // use in memory pouchdb directly
const { getPouch, closeDB } = require("@budibase/backend-core/db") const { getPouch, closePouchDB } = require("@budibase/backend-core/db")
const Pouch = getPouch({ inMemory: true }) const Pouch = getPouch({ inMemory: true })
exports.runView = async (view, calculation, group, data) => { exports.runView = async (view, calculation, group, data) => {
@ -44,6 +44,6 @@ exports.runView = async (view, calculation, group, data) => {
return response return response
} finally { } finally {
await db.destroy() await db.destroy()
await closeDB(db) await closePouchDB(db)
} }
} }

View File

@ -1,15 +1,17 @@
const TestConfig = require("../../tests/utilities/TestConfiguration") const TestConfig = require("../../tests/utilities/TestConfiguration")
const { basicRow, basicLinkedRow, basicTable } = require("../../tests/utilities/structures") const { basicRow, basicLinkedRow, basicTable } = require("../../tests/utilities/structures")
const LinkController = require("../linkedRows/LinkController") const LinkController = require("../linkedRows/LinkController")
const { context } = require("@budibase/backend-core")
const { RelationshipTypes } = require("../../constants") const { RelationshipTypes } = require("../../constants")
const { cloneDeep } = require("lodash/fp") const { cloneDeep } = require("lodash/fp")
describe("test the link controller", () => { describe("test the link controller", () => {
let config = new TestConfig(false) let config = new TestConfig(false)
let table1, table2 let table1, table2, appId
beforeEach(async () => { beforeEach(async () => {
await config.init() const app = await config.init()
appId = app.appId
const { _id } = await config.createTable() const { _id } = await config.createTable()
table2 = await config.createLinkedTable(RelationshipTypes.MANY_TO_MANY, ["link", "link2"]) table2 = await config.createLinkedTable(RelationshipTypes.MANY_TO_MANY, ["link", "link2"])
// update table after creating link // update table after creating link
@ -18,18 +20,20 @@ describe("test the link controller", () => {
afterAll(config.end) afterAll(config.end)
function createLinkController(table, row = null, oldTable = null) { async function createLinkController(table, row = null, oldTable = null) {
const linkConfig = { return context.doInAppContext(appId, () => {
tableId: table._id, const linkConfig = {
table, tableId: table._id,
} table,
if (row) { }
linkConfig.row = row if (row) {
} linkConfig.row = row
if (oldTable) { }
linkConfig.oldTable = oldTable if (oldTable) {
} linkConfig.oldTable = oldTable
return new LinkController(linkConfig) }
return new LinkController(linkConfig)
})
} }
async function createLinkedRow(linkField = "link", t1 = table1, t2 = table2) { async function createLinkedRow(linkField = "link", t1 = table1, t2 = table2) {
@ -38,16 +42,16 @@ describe("test the link controller", () => {
return config.getRow(t1._id, _id) return config.getRow(t1._id, _id)
} }
it("should be able to confirm if two table schemas are equal", () => { it("should be able to confirm if two table schemas are equal", async () => {
const controller = createLinkController(table1) const controller = await createLinkController(table1)
let equal = controller.areLinkSchemasEqual(table2.schema.link, table2.schema.link) let equal = controller.areLinkSchemasEqual(table2.schema.link, table2.schema.link)
expect(equal).toEqual(true) expect(equal).toEqual(true)
equal = controller.areLinkSchemasEqual(table1.schema.link, table2.schema.link) equal = controller.areLinkSchemasEqual(table1.schema.link, table2.schema.link)
expect(equal).toEqual(false) expect(equal).toEqual(false)
}) })
it("should be able to check the relationship types across two fields", () => { it("should be able to check the relationship types across two fields", async () => {
const controller = createLinkController(table1) const controller = await createLinkController(table1)
// empty case // empty case
let output = controller.handleRelationshipType({}, {}) let output = controller.handleRelationshipType({}, {})
expect(output.linkedField.relationshipType).toEqual(RelationshipTypes.MANY_TO_MANY) expect(output.linkedField.relationshipType).toEqual(RelationshipTypes.MANY_TO_MANY)
@ -65,29 +69,33 @@ describe("test the link controller", () => {
it("should be able to delete a row", async () => { it("should be able to delete a row", async () => {
const row = await createLinkedRow() const row = await createLinkedRow()
const controller = createLinkController(table1, row) const controller = await createLinkController(table1, row)
// get initial count await context.doInAppContext(appId, async () => {
const beforeLinks = await controller.getRowLinkDocs(row._id) // get initial count
await controller.rowDeleted() const beforeLinks = await controller.getRowLinkDocs(row._id)
let afterLinks = await controller.getRowLinkDocs(row._id) await controller.rowDeleted()
expect(beforeLinks.length).toEqual(1) let afterLinks = await controller.getRowLinkDocs(row._id)
expect(afterLinks.length).toEqual(0) expect(beforeLinks.length).toEqual(1)
expect(afterLinks.length).toEqual(0)
})
}) })
it("shouldn't throw an error when deleting a row with no links", async () => { it("shouldn't throw an error when deleting a row with no links", async () => {
const row = await config.createRow(basicRow(table1._id)) const row = await config.createRow(basicRow(table1._id))
const controller = createLinkController(table1, row) const controller = await createLinkController(table1, row)
let error await context.doInAppContext(appId, async () => {
try { let error
await controller.rowDeleted() try {
} catch (err) { await controller.rowDeleted()
error = err } catch (err) {
} error = err
expect(error).toBeUndefined() }
expect(error).toBeUndefined()
})
}) })
it("should throw an error when validating a table which is invalid", () => { it("should throw an error when validating a table which is invalid", async () => {
const controller = createLinkController(table1) const controller = await createLinkController(table1)
const copyTable = { const copyTable = {
...table1 ...table1
} }
@ -110,32 +118,38 @@ describe("test the link controller", () => {
const row = await createLinkedRow() const row = await createLinkedRow()
// remove the link from the row // remove the link from the row
row.link = [] row.link = []
const controller = createLinkController(table1, row) const controller = await createLinkController(table1, row)
await controller.rowSaved() await context.doInAppContext(appId, async () => {
let links = await controller.getRowLinkDocs(row._id) await controller.rowSaved()
expect(links.length).toEqual(0) let links = await controller.getRowLinkDocs(row._id)
expect(links.length).toEqual(0)
})
}) })
it("should be able to delete a table and have links deleted", async () => { it("should be able to delete a table and have links deleted", async () => {
await createLinkedRow() await createLinkedRow()
const controller = createLinkController(table1) const controller = await createLinkController(table1)
let before = await controller.getTableLinkDocs() await context.doInAppContext(appId, async () => {
await controller.tableDeleted() let before = await controller.getTableLinkDocs()
let after = await controller.getTableLinkDocs() await controller.tableDeleted()
expect(before.length).toEqual(1) let after = await controller.getTableLinkDocs()
expect(after.length).toEqual(0) expect(before.length).toEqual(1)
expect(after.length).toEqual(0)
})
}) })
it("should be able to remove a linked field from a table", async () => { it("should be able to remove a linked field from a table", async () => {
await createLinkedRow() await createLinkedRow()
await createLinkedRow("link2") await createLinkedRow("link2")
const controller = createLinkController(table1, null, table1) const controller = await createLinkController(table1, null, table1)
let before = await controller.getTableLinkDocs() await context.doInAppContext(appId, async () => {
await controller.removeFieldFromTable("link") let before = await controller.getTableLinkDocs()
let after = await controller.getTableLinkDocs() await controller.removeFieldFromTable("link")
expect(before.length).toEqual(2) let after = await controller.getTableLinkDocs()
// shouldn't delete the other field expect(before.length).toEqual(2)
expect(after.length).toEqual(1) // shouldn't delete the other field
expect(after.length).toEqual(1)
})
}) })
it("should throw an error when overwriting a link column", async () => { it("should throw an error when overwriting a link column", async () => {
@ -143,7 +157,7 @@ describe("test the link controller", () => {
update.schema.link.relationshipType = RelationshipTypes.MANY_TO_ONE update.schema.link.relationshipType = RelationshipTypes.MANY_TO_ONE
let error let error
try { try {
const controller = createLinkController(update) const controller = await createLinkController(update)
await controller.tableSaved() await controller.tableSaved()
} catch (err) { } catch (err) {
error = err error = err
@ -156,10 +170,12 @@ describe("test the link controller", () => {
await createLinkedRow() await createLinkedRow()
const newTable = cloneDeep(table1) const newTable = cloneDeep(table1)
delete newTable.schema.link delete newTable.schema.link
const controller = createLinkController(newTable, null, table1) const controller = await createLinkController(newTable, null, table1)
await controller.tableUpdated() await context.doInAppContext(appId, async () => {
const links = await controller.getTableLinkDocs() await controller.tableUpdated()
expect(links.length).toEqual(0) const links = await controller.getTableLinkDocs()
expect(links.length).toEqual(0)
})
}) })
it("shouldn't allow one to many having many relationships against it", async () => { it("shouldn't allow one to many having many relationships against it", async () => {

View File

@ -1,30 +1,34 @@
const TestConfig = require("../../tests/utilities/TestConfiguration") const TestConfig = require("../../tests/utilities/TestConfiguration")
const { basicTable } = require("../../tests/utilities/structures") const { basicTable } = require("../../tests/utilities/structures")
const linkUtils = require("../linkedRows/linkUtils") const linkUtils = require("../linkedRows/linkUtils")
const { getAppDB } = require("@budibase/backend-core/context") const { context } = require("@budibase/backend-core")
const { doWithDB } = require("@budibase/backend-core/db")
describe("test link functionality", () => { describe("test link functionality", () => {
const config = new TestConfig(false) const config = new TestConfig(false)
let appId
describe("getLinkedTable", () => { describe("getLinkedTable", () => {
let db, table let table
beforeEach(async () => { beforeEach(async () => {
await config.init() const app = await config.init()
db = getAppDB() appId = app.appId
table = await config.createTable() table = await config.createTable()
}) })
it("should be able to retrieve a linked table from a list", async () => { it("should be able to retrieve a linked table from a list", async () => {
const retrieved = await linkUtils.getLinkedTable(table._id, [table]) await context.doInAppContext(appId, async () => {
expect(retrieved._id).toBe(table._id) const retrieved = await linkUtils.getLinkedTable(table._id, [table])
expect(retrieved._id).toBe(table._id)
})
}) })
it("should be able to retrieve a table from DB and update list", async () => { it("should be able to retrieve a table from DB and update list", async () => {
const tables = [] const tables = []
const retrieved = await linkUtils.getLinkedTable(table._id, tables) await context.doInAppContext(appId, async () => {
expect(retrieved._id).toBe(table._id) const retrieved = await linkUtils.getLinkedTable(table._id, tables)
expect(tables[0]).toBeDefined() expect(retrieved._id).toBe(table._id)
expect(tables[0]).toBeDefined()
})
}) })
}) })
@ -48,15 +52,14 @@ describe("test link functionality", () => {
describe("getLinkDocuments", () => { describe("getLinkDocuments", () => {
it("should create the link view when it doesn't exist", async () => { it("should create the link view when it doesn't exist", async () => {
// create the DB and a very basic app design DB // create the DB and a very basic app design DB
const output = await doWithDB("test", async db => { await context.doInAppContext(appId, async () => {
await db.put({ _id: "_design/database", views: {} }) const output = await linkUtils.getLinkDocuments({
return await linkUtils.getLinkDocuments({
tableId: "test", tableId: "test",
rowId: "test", rowId: "test",
includeDocs: false, includeDocs: false,
}) })
expect(Array.isArray(output)).toBe(true)
}) })
expect(Array.isArray(output)).toBe(true)
}) })
}) })
}) })

View File

@ -1,44 +1,19 @@
const newid = require("./newid") import newid from "./newid"
const { import { db as dbCore } from "@budibase/backend-core"
DocumentType: CoreDocType,
InternalTable,
getRoleParams,
generateRoleID,
APP_DEV_PREFIX,
APP_PREFIX,
SEPARATOR,
StaticDatabases,
isDevAppID,
isProdAppID,
getDevelopmentAppID,
generateAppID,
getQueryIndex,
ViewName,
getDocParams,
getRowParams,
generateRowID,
getUserMetadataParams,
generateUserMetadataID,
getGlobalIDFromUserMetadataID,
} = require("@budibase/backend-core/db")
const UNICODE_MAX = "\ufff0" type Optional = string | null
const AppStatus = { export const AppStatus = {
DEV: "development", DEV: "development",
ALL: "all", ALL: "all",
DEPLOYED: "published", DEPLOYED: "published",
} }
const DocumentType = CoreDocType export const SearchIndexes = {
const SearchIndexes = {
ROWS: "rows", ROWS: "rows",
} }
exports.StaticDatabases = StaticDatabases export const BudibaseInternalDB = {
const BudibaseInternalDB = {
_id: "bb_internal", _id: "bb_internal",
type: "budibase", type: "budibase",
name: "Budibase DB", name: "Budibase DB",
@ -46,37 +21,36 @@ const BudibaseInternalDB = {
config: {}, config: {},
} }
exports.APP_PREFIX = APP_PREFIX export const SEPARATOR = dbCore.SEPARATOR
exports.APP_DEV_PREFIX = APP_DEV_PREFIX export const StaticDatabases = dbCore.StaticDatabases
exports.isDevAppID = isDevAppID export const DocumentType = dbCore.DocumentType
exports.isProdAppID = isProdAppID export const APP_PREFIX = dbCore.APP_PREFIX
exports.USER_METDATA_PREFIX = `${DocumentType.ROW}${SEPARATOR}${InternalTable.USER_METADATA}${SEPARATOR}` export const APP_DEV_PREFIX = dbCore.APP_DEV_PREFIX
exports.LINK_USER_METADATA_PREFIX = `${DocumentType.LINK}${SEPARATOR}${InternalTable.USER_METADATA}${SEPARATOR}` export const isDevAppID = dbCore.isDevAppID
exports.TABLE_ROW_PREFIX = `${DocumentType.ROW}${SEPARATOR}${DocumentType.TABLE}` export const isProdAppID = dbCore.isProdAppID
exports.ViewName = ViewName export const USER_METDATA_PREFIX = `${DocumentType.ROW}${SEPARATOR}${dbCore.InternalTable.USER_METADATA}${SEPARATOR}`
exports.InternalTables = InternalTable export const LINK_USER_METADATA_PREFIX = `${DocumentType.LINK}${SEPARATOR}${dbCore.InternalTable.USER_METADATA}${SEPARATOR}`
exports.DocumentType = DocumentType export const TABLE_ROW_PREFIX = `${DocumentType.ROW}${SEPARATOR}${DocumentType.TABLE}`
exports.SEPARATOR = SEPARATOR export const ViewName = dbCore.ViewName
exports.UNICODE_MAX = UNICODE_MAX export const InternalTables = dbCore.InternalTable
exports.SearchIndexes = SearchIndexes export const UNICODE_MAX = dbCore.UNICODE_MAX
exports.AppStatus = AppStatus export const generateAppID = dbCore.generateAppID
exports.BudibaseInternalDB = BudibaseInternalDB export const generateDevAppID = dbCore.getDevelopmentAppID
exports.generateAppID = generateAppID export const generateRoleID = dbCore.generateRoleID
exports.generateDevAppID = getDevelopmentAppID export const getRoleParams = dbCore.getRoleParams
exports.generateRoleID = generateRoleID export const getQueryIndex = dbCore.getQueryIndex
exports.getRoleParams = getRoleParams export const getDocParams = dbCore.getDocParams
exports.getQueryIndex = getQueryIndex export const getRowParams = dbCore.getRowParams
exports.getDocParams = getDocParams export const generateRowID = dbCore.generateRowID
exports.getRowParams = getRowParams export const getUserMetadataParams = dbCore.getUserMetadataParams
exports.generateRowID = generateRowID export const generateUserMetadataID = dbCore.generateUserMetadataID
exports.getUserMetadataParams = getUserMetadataParams export const getGlobalIDFromUserMetadataID =
exports.generateUserMetadataID = generateUserMetadataID dbCore.getGlobalIDFromUserMetadataID
exports.getGlobalIDFromUserMetadataID = getGlobalIDFromUserMetadataID
/** /**
* Gets parameters for retrieving tables, this is a utility function for the getDocParams function. * Gets parameters for retrieving tables, this is a utility function for the getDocParams function.
*/ */
exports.getTableParams = (tableId = null, otherProps = {}) => { export function getTableParams(tableId?: Optional, otherProps = {}) {
return getDocParams(DocumentType.TABLE, tableId, otherProps) return getDocParams(DocumentType.TABLE, tableId, otherProps)
} }
@ -84,7 +58,7 @@ exports.getTableParams = (tableId = null, otherProps = {}) => {
* Generates a new table ID. * Generates a new table ID.
* @returns {string} The new table ID which the table doc can be stored under. * @returns {string} The new table ID which the table doc can be stored under.
*/ */
exports.generateTableID = () => { export function generateTableID() {
return `${DocumentType.TABLE}${SEPARATOR}${newid()}` return `${DocumentType.TABLE}${SEPARATOR}${newid()}`
} }
@ -93,7 +67,7 @@ exports.generateTableID = () => {
* @param {string} rowId The ID of the row. * @param {string} rowId The ID of the row.
* @returns {string} The table ID. * @returns {string} The table ID.
*/ */
exports.getTableIDFromRowID = rowId => { export function getTableIDFromRowID(rowId: string) {
const components = rowId const components = rowId
.split(DocumentType.TABLE + SEPARATOR)[1] .split(DocumentType.TABLE + SEPARATOR)[1]
.split(SEPARATOR) .split(SEPARATOR)
@ -103,7 +77,10 @@ exports.getTableIDFromRowID = rowId => {
/** /**
* Gets parameters for retrieving automations, this is a utility function for the getDocParams function. * Gets parameters for retrieving automations, this is a utility function for the getDocParams function.
*/ */
exports.getAutomationParams = (automationId = null, otherProps = {}) => { export function getAutomationParams(
automationId?: Optional,
otherProps: any = {}
) {
return getDocParams(DocumentType.AUTOMATION, automationId, otherProps) return getDocParams(DocumentType.AUTOMATION, automationId, otherProps)
} }
@ -111,7 +88,7 @@ exports.getAutomationParams = (automationId = null, otherProps = {}) => {
* Generates a new automation ID. * Generates a new automation ID.
* @returns {string} The new automation ID which the automation doc can be stored under. * @returns {string} The new automation ID which the automation doc can be stored under.
*/ */
exports.generateAutomationID = () => { export function generateAutomationID() {
return `${DocumentType.AUTOMATION}${SEPARATOR}${newid()}` return `${DocumentType.AUTOMATION}${SEPARATOR}${newid()}`
} }
@ -126,14 +103,14 @@ exports.generateAutomationID = () => {
* @param {string} fieldName2 the name of the field in the linked row. * @param {string} fieldName2 the name of the field in the linked row.
* @returns {string} The new link doc ID which the automation doc can be stored under. * @returns {string} The new link doc ID which the automation doc can be stored under.
*/ */
exports.generateLinkID = ( export function generateLinkID(
tableId1, tableId1: string,
tableId2, tableId2: string,
rowId1, rowId1: string,
rowId2, rowId2: string,
fieldName1, fieldName1: string,
fieldName2 fieldName2: string
) => { ) {
const tables = `${SEPARATOR}${tableId1}${SEPARATOR}${tableId2}` const tables = `${SEPARATOR}${tableId1}${SEPARATOR}${tableId2}`
const rows = `${SEPARATOR}${rowId1}${SEPARATOR}${rowId2}` const rows = `${SEPARATOR}${rowId1}${SEPARATOR}${rowId2}`
const fields = `${SEPARATOR}${fieldName1}${SEPARATOR}${fieldName2}` const fields = `${SEPARATOR}${fieldName1}${SEPARATOR}${fieldName2}`
@ -143,7 +120,7 @@ exports.generateLinkID = (
/** /**
* Gets parameters for retrieving link docs, this is a utility function for the getDocParams function. * Gets parameters for retrieving link docs, this is a utility function for the getDocParams function.
*/ */
exports.getLinkParams = (otherProps = {}) => { export function getLinkParams(otherProps: any = {}) {
return getDocParams(DocumentType.LINK, null, otherProps) return getDocParams(DocumentType.LINK, null, otherProps)
} }
@ -151,14 +128,14 @@ exports.getLinkParams = (otherProps = {}) => {
* Generates a new layout ID. * Generates a new layout ID.
* @returns {string} The new layout ID which the layout doc can be stored under. * @returns {string} The new layout ID which the layout doc can be stored under.
*/ */
exports.generateLayoutID = id => { export function generateLayoutID(id: string) {
return `${DocumentType.LAYOUT}${SEPARATOR}${id || newid()}` return `${DocumentType.LAYOUT}${SEPARATOR}${id || newid()}`
} }
/** /**
* Gets parameters for retrieving layout, this is a utility function for the getDocParams function. * Gets parameters for retrieving layout, this is a utility function for the getDocParams function.
*/ */
exports.getLayoutParams = (layoutId = null, otherProps = {}) => { export function getLayoutParams(layoutId?: Optional, otherProps: any = {}) {
return getDocParams(DocumentType.LAYOUT, layoutId, otherProps) return getDocParams(DocumentType.LAYOUT, layoutId, otherProps)
} }
@ -166,14 +143,14 @@ exports.getLayoutParams = (layoutId = null, otherProps = {}) => {
* Generates a new screen ID. * Generates a new screen ID.
* @returns {string} The new screen ID which the screen doc can be stored under. * @returns {string} The new screen ID which the screen doc can be stored under.
*/ */
exports.generateScreenID = () => { export function generateScreenID() {
return `${DocumentType.SCREEN}${SEPARATOR}${newid()}` return `${DocumentType.SCREEN}${SEPARATOR}${newid()}`
} }
/** /**
* Gets parameters for retrieving screens, this is a utility function for the getDocParams function. * Gets parameters for retrieving screens, this is a utility function for the getDocParams function.
*/ */
exports.getScreenParams = (screenId = null, otherProps = {}) => { export function getScreenParams(screenId?: Optional, otherProps: any = {}) {
return getDocParams(DocumentType.SCREEN, screenId, otherProps) return getDocParams(DocumentType.SCREEN, screenId, otherProps)
} }
@ -181,14 +158,14 @@ exports.getScreenParams = (screenId = null, otherProps = {}) => {
* Generates a new webhook ID. * Generates a new webhook ID.
* @returns {string} The new webhook ID which the webhook doc can be stored under. * @returns {string} The new webhook ID which the webhook doc can be stored under.
*/ */
exports.generateWebhookID = () => { export function generateWebhookID() {
return `${DocumentType.WEBHOOK}${SEPARATOR}${newid()}` return `${DocumentType.WEBHOOK}${SEPARATOR}${newid()}`
} }
/** /**
* Gets parameters for retrieving a webhook, this is a utility function for the getDocParams function. * Gets parameters for retrieving a webhook, this is a utility function for the getDocParams function.
*/ */
exports.getWebhookParams = (webhookId = null, otherProps = {}) => { export function getWebhookParams(webhookId?: Optional, otherProps: any = {}) {
return getDocParams(DocumentType.WEBHOOK, webhookId, otherProps) return getDocParams(DocumentType.WEBHOOK, webhookId, otherProps)
} }
@ -196,7 +173,7 @@ exports.getWebhookParams = (webhookId = null, otherProps = {}) => {
* Generates a new datasource ID. * Generates a new datasource ID.
* @returns {string} The new datasource ID which the webhook doc can be stored under. * @returns {string} The new datasource ID which the webhook doc can be stored under.
*/ */
exports.generateDatasourceID = ({ plus = false } = {}) => { export function generateDatasourceID({ plus = false } = {}) {
return `${ return `${
plus ? DocumentType.DATASOURCE_PLUS : DocumentType.DATASOURCE plus ? DocumentType.DATASOURCE_PLUS : DocumentType.DATASOURCE
}${SEPARATOR}${newid()}` }${SEPARATOR}${newid()}`
@ -205,7 +182,10 @@ exports.generateDatasourceID = ({ plus = false } = {}) => {
/** /**
* Gets parameters for retrieving a datasource, this is a utility function for the getDocParams function. * Gets parameters for retrieving a datasource, this is a utility function for the getDocParams function.
*/ */
exports.getDatasourceParams = (datasourceId = null, otherProps = {}) => { export function getDatasourceParams(
datasourceId?: Optional,
otherProps: any = {}
) {
return getDocParams(DocumentType.DATASOURCE, datasourceId, otherProps) return getDocParams(DocumentType.DATASOURCE, datasourceId, otherProps)
} }
@ -213,7 +193,7 @@ exports.getDatasourceParams = (datasourceId = null, otherProps = {}) => {
* Generates a new query ID. * Generates a new query ID.
* @returns {string} The new query ID which the query doc can be stored under. * @returns {string} The new query ID which the query doc can be stored under.
*/ */
exports.generateQueryID = datasourceId => { export function generateQueryID(datasourceId: string) {
return `${ return `${
DocumentType.QUERY DocumentType.QUERY
}${SEPARATOR}${datasourceId}${SEPARATOR}${newid()}` }${SEPARATOR}${datasourceId}${SEPARATOR}${newid()}`
@ -223,21 +203,21 @@ exports.generateQueryID = datasourceId => {
* Generates a metadata ID for automations, used to track errors in recurring * Generates a metadata ID for automations, used to track errors in recurring
* automations etc. * automations etc.
*/ */
exports.generateAutomationMetadataID = automationId => { export function generateAutomationMetadataID(automationId: string) {
return `${DocumentType.AUTOMATION_METADATA}${SEPARATOR}${automationId}` return `${DocumentType.AUTOMATION_METADATA}${SEPARATOR}${automationId}`
} }
/** /**
* Retrieve all automation metadata in an app database. * Retrieve all automation metadata in an app database.
*/ */
exports.getAutomationMetadataParams = (otherProps = {}) => { export function getAutomationMetadataParams(otherProps: any = {}) {
return getDocParams(DocumentType.AUTOMATION_METADATA, null, otherProps) return getDocParams(DocumentType.AUTOMATION_METADATA, null, otherProps)
} }
/** /**
* Gets parameters for retrieving a query, this is a utility function for the getDocParams function. * Gets parameters for retrieving a query, this is a utility function for the getDocParams function.
*/ */
exports.getQueryParams = (datasourceId = null, otherProps = {}) => { export function getQueryParams(datasourceId?: Optional, otherProps: any = {}) {
if (datasourceId == null) { if (datasourceId == null) {
return getDocParams(DocumentType.QUERY, null, otherProps) return getDocParams(DocumentType.QUERY, null, otherProps)
} }
@ -253,15 +233,19 @@ exports.getQueryParams = (datasourceId = null, otherProps = {}) => {
* Generates a new flag document ID. * Generates a new flag document ID.
* @returns {string} The ID of the flag document that was generated. * @returns {string} The ID of the flag document that was generated.
*/ */
exports.generateUserFlagID = userId => { export function generateUserFlagID(userId: string) {
return `${DocumentType.USER_FLAG}${SEPARATOR}${userId}` return `${DocumentType.USER_FLAG}${SEPARATOR}${userId}`
} }
exports.generateMetadataID = (type, entityId) => { export function generateMetadataID(type: string, entityId: string) {
return `${DocumentType.METADATA}${SEPARATOR}${type}${SEPARATOR}${entityId}` return `${DocumentType.METADATA}${SEPARATOR}${type}${SEPARATOR}${entityId}`
} }
exports.getMetadataParams = (type, entityId = null, otherProps = {}) => { export function getMetadataParams(
type: string,
entityId?: Optional,
otherProps: any = {}
) {
let docId = `${type}${SEPARATOR}` let docId = `${type}${SEPARATOR}`
if (entityId != null) { if (entityId != null) {
docId += entityId docId += entityId
@ -269,22 +253,22 @@ exports.getMetadataParams = (type, entityId = null, otherProps = {}) => {
return getDocParams(DocumentType.METADATA, docId, otherProps) return getDocParams(DocumentType.METADATA, docId, otherProps)
} }
exports.generateMemoryViewID = viewName => { export function generateMemoryViewID(viewName: string) {
return `${DocumentType.MEM_VIEW}${SEPARATOR}${viewName}` return `${DocumentType.MEM_VIEW}${SEPARATOR}${viewName}`
} }
exports.getMemoryViewParams = (otherProps = {}) => { export function getMemoryViewParams(otherProps: any = {}) {
return getDocParams(DocumentType.MEM_VIEW, null, otherProps) return getDocParams(DocumentType.MEM_VIEW, null, otherProps)
} }
exports.generatePluginID = name => { export function generatePluginID(name: string) {
return `${DocumentType.PLUGIN}${SEPARATOR}${name}` return `${DocumentType.PLUGIN}${SEPARATOR}${name}`
} }
/** /**
* This can be used with the db.allDocs to get a list of IDs * This can be used with the db.allDocs to get a list of IDs
*/ */
exports.getMultiIDParams = ids => { export function getMultiIDParams(ids: string[]) {
return { return {
keys: ids, keys: ids,
include_docs: true, include_docs: true,

View File

@ -12,10 +12,7 @@ import { buildExternalTableId } from "./utils"
import { DataSourceOperation, FieldTypes } from "../constants" import { DataSourceOperation, FieldTypes } from "../constants"
import { GoogleSpreadsheet } from "google-spreadsheet" import { GoogleSpreadsheet } from "google-spreadsheet"
import env from "../environment" import env from "../environment"
import { tenancy, db as dbCore, constants } from "@budibase/backend-core"
const { getGlobalDB } = require("@budibase/backend-core/tenancy")
const { getScopedConfig } = require("@budibase/backend-core/db")
const { Configs } = require("@budibase/backend-core/constants")
const fetch = require("node-fetch") const fetch = require("node-fetch")
interface GoogleSheetsConfig { interface GoogleSheetsConfig {
@ -176,9 +173,9 @@ class GoogleSheetsIntegration implements DatasourcePlus {
async connect() { async connect() {
try { try {
// Initialise oAuth client // Initialise oAuth client
const db = getGlobalDB() const db = tenancy.getGlobalDB()
let googleConfig = await getScopedConfig(db, { let googleConfig = await dbCore.getScopedConfig(db, {
type: Configs.GOOGLE, type: constants.Config.GOOGLE,
}) })
if (!googleConfig) { if (!googleConfig) {

View File

@ -1,19 +0,0 @@
const { isDevAppID, isProdAppID } = require("../db/utils")
exports.AppType = {
DEV: "dev",
PROD: "prod",
}
exports.middleware =
({ appType } = {}) =>
(ctx, next) => {
const appId = ctx.appId
if (appType === exports.AppType.DEV && appId && !isDevAppID(appId)) {
ctx.throw(400, "Only apps in development support this endpoint")
}
if (appType === exports.AppType.PROD && appId && !isProdAppID(appId)) {
ctx.throw(400, "Only apps in production support this endpoint")
}
return next()
}

View File

@ -0,0 +1,20 @@
import { isDevAppID, isProdAppID } from "../db/utils"
import { BBContext } from "@budibase/types"
export enum AppType {
DEV = "dev",
PROD = "prod",
}
export function middleware({ appType }: { appType?: AppType } = {}) {
return (ctx: BBContext, next: any) => {
const appId = ctx.appId
if (appType === AppType.DEV && appId && !isDevAppID(appId)) {
ctx.throw(400, "Only apps in development support this endpoint")
}
if (appType === AppType.PROD && appId && !isProdAppID(appId)) {
ctx.throw(400, "Only apps in production support this endpoint")
}
return next()
}
}

View File

@ -4,8 +4,8 @@ import {
BUILTIN_ROLE_IDS, BUILTIN_ROLE_IDS,
} from "@budibase/backend-core/roles" } from "@budibase/backend-core/roles"
const { const {
PermissionTypes, PermissionType,
PermissionLevels, PermissionLevel,
doesHaveBasePermission, doesHaveBasePermission,
} = require("@budibase/backend-core/permissions") } = require("@budibase/backend-core/permissions")
const builderMiddleware = require("./builder") const builderMiddleware = require("./builder")
@ -33,7 +33,7 @@ const checkAuthorized = async (
) => { ) => {
// check if this is a builder api and the user is not a builder // 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 isBuilder = ctx.user && ctx.user.builder && ctx.user.builder.global
const isBuilderApi = permType === PermissionTypes.BUILDER const isBuilderApi = permType === PermissionType.BUILDER
if (isBuilderApi && !isBuilder) { if (isBuilderApi && !isBuilder) {
return ctx.throw(403, "Not Authorized") return ctx.throw(403, "Not Authorized")
} }
@ -91,9 +91,9 @@ export = (permType: any, permLevel: any = null, opts = { schema: false }) =>
let resourceRoles: any = [] let resourceRoles: any = []
let otherLevelRoles: any = [] let otherLevelRoles: any = []
const otherLevel = const otherLevel =
permLevel === PermissionLevels.READ permLevel === PermissionLevel.READ
? PermissionLevels.WRITE ? PermissionLevel.WRITE
: PermissionLevels.READ : PermissionLevel.READ
const appId = getAppId() const appId = getAppId()
if (appId && hasResource(ctx)) { if (appId && hasResource(ctx)) {
resourceRoles = await getRequiredResourceRole(permLevel, ctx) resourceRoles = await getRequiredResourceRole(permLevel, ctx)

View File

@ -1,14 +1,16 @@
const { APP_DEV_PREFIX } = require("../db/utils") import {
const { APP_DEV_PREFIX,
DocumentType,
getGlobalIDFromUserMetadataID,
} from "../db/utils"
import {
doesUserHaveLock, doesUserHaveLock,
updateLock, updateLock,
checkDebounce, checkDebounce,
setDebounce, setDebounce,
} = require("../utilities/redis") } from "../utilities/redis"
const { doWithDB } = require("@budibase/backend-core/db") import { db as dbCore, cache, permissions } from "@budibase/backend-core"
const { DocumentType, getGlobalIDFromUserMetadataID } = require("../db/utils") import { BBContext, Database } from "@budibase/types"
const { PermissionTypes } = require("@budibase/backend-core/permissions")
const { app: appCache } = require("@budibase/backend-core/cache")
const DEBOUNCE_TIME_SEC = 30 const DEBOUNCE_TIME_SEC = 30
@ -21,11 +23,11 @@ const DEBOUNCE_TIME_SEC = 30
* through the authorized middleware * * through the authorized middleware *
****************************************************/ ****************************************************/
async function checkDevAppLocks(ctx) { async function checkDevAppLocks(ctx: BBContext) {
const appId = ctx.appId const appId = ctx.appId
// if any public usage, don't proceed // if any public usage, don't proceed
if (!ctx.user._id && !ctx.user.userId) { if (!ctx.user?._id && !ctx.user?.userId) {
return return
} }
@ -41,34 +43,34 @@ async function checkDevAppLocks(ctx) {
await updateLock(appId, ctx.user) await updateLock(appId, ctx.user)
} }
async function updateAppUpdatedAt(ctx) { async function updateAppUpdatedAt(ctx: BBContext) {
const appId = ctx.appId const appId = ctx.appId
// if debouncing skip this update // if debouncing skip this update
// get methods also aren't updating // get methods also aren't updating
if (ctx.method === "GET" || (await checkDebounce(appId))) { if (ctx.method === "GET" || (await checkDebounce(appId))) {
return return
} }
await doWithDB(appId, async db => { await dbCore.doWithDB(appId, async (db: Database) => {
const metadata = await db.get(DocumentType.APP_METADATA) const metadata = await db.get(DocumentType.APP_METADATA)
metadata.updatedAt = new Date().toISOString() metadata.updatedAt = new Date().toISOString()
metadata.updatedBy = getGlobalIDFromUserMetadataID(ctx.user.userId) metadata.updatedBy = getGlobalIDFromUserMetadataID(ctx.user?.userId!)
const response = await db.put(metadata) const response = await db.put(metadata)
metadata._rev = response.rev metadata._rev = response.rev
await appCache.invalidateAppMetadata(appId, metadata) await cache.app.invalidateAppMetadata(appId, metadata)
// set a new debounce record with a short TTL // set a new debounce record with a short TTL
await setDebounce(appId, DEBOUNCE_TIME_SEC) await setDebounce(appId, DEBOUNCE_TIME_SEC)
}) })
} }
module.exports = async (ctx, permType) => { export = async function builder(ctx: BBContext, permType: string) {
const appId = ctx.appId const appId = ctx.appId
// this only functions within an app context // this only functions within an app context
if (!appId) { if (!appId) {
return return
} }
const isBuilderApi = permType === PermissionTypes.BUILDER const isBuilderApi = permType === permissions.PermissionType.BUILDER
const referer = ctx.headers["referer"] const referer = ctx.headers["referer"]
const overviewPath = "/builder/portal/overview/" const overviewPath = "/builder/portal/overview/"

View File

@ -1,29 +1,26 @@
const { import {
getAppIdFromCtx, utils,
setCookie, constants,
getCookie, roles,
clearCookie, db as dbCore,
} = require("@budibase/backend-core/utils") tenancy,
const { Cookies, Headers } = require("@budibase/backend-core/constants") context,
const { getRole } = require("@budibase/backend-core/roles") } from "@budibase/backend-core"
const { BUILTIN_ROLE_IDS } = require("@budibase/backend-core/roles") import { generateUserMetadataID, isDevAppID } from "../db/utils"
const { generateUserMetadataID, isDevAppID } = require("../db/utils") import { getCachedSelf } from "../utilities/global"
const { dbExists } = require("@budibase/backend-core/db") import env from "../environment"
const { isUserInAppTenant } = require("@budibase/backend-core/tenancy") import { isWebhookEndpoint } from "./utils"
const { getCachedSelf } = require("../utilities/global") import { BBContext } from "@budibase/types"
const env = require("../environment")
const { isWebhookEndpoint } = require("./utils")
const { doInAppContext } = require("@budibase/backend-core/context")
module.exports = async (ctx, next) => { export = async (ctx: BBContext, next: any) => {
// try to get the appID from the request // try to get the appID from the request
let requestAppId = await getAppIdFromCtx(ctx) let requestAppId = await utils.getAppIdFromCtx(ctx)
// get app cookie if it exists // get app cookie if it exists
let appCookie = null let appCookie: { appId?: string } | undefined
try { try {
appCookie = getCookie(ctx, Cookies.CurrentApp) appCookie = utils.getCookie(ctx, constants.Cookie.CurrentApp)
} catch (err) { } catch (err) {
clearCookie(ctx, Cookies.CurrentApp) utils.clearCookie(ctx, constants.Cookie.CurrentApp)
} }
if (!appCookie && !requestAppId) { if (!appCookie && !requestAppId) {
return next() return next()
@ -31,9 +28,9 @@ module.exports = async (ctx, next) => {
// check the app exists referenced in cookie // check the app exists referenced in cookie
if (appCookie) { if (appCookie) {
const appId = appCookie.appId const appId = appCookie.appId
const exists = await dbExists(appId) const exists = await dbCore.dbExists(appId)
if (!exists) { if (!exists) {
clearCookie(ctx, Cookies.CurrentApp) utils.clearCookie(ctx, constants.Cookie.CurrentApp)
return next() return next()
} }
// if the request app ID wasn't set, update it with the cookie // if the request app ID wasn't set, update it with the cookie
@ -47,13 +44,13 @@ module.exports = async (ctx, next) => {
!isWebhookEndpoint(ctx) && !isWebhookEndpoint(ctx) &&
(!ctx.user || !ctx.user.builder || !ctx.user.builder.global) (!ctx.user || !ctx.user.builder || !ctx.user.builder.global)
) { ) {
clearCookie(ctx, Cookies.CurrentApp) utils.clearCookie(ctx, constants.Cookie.CurrentApp)
return ctx.redirect("/") return ctx.redirect("/")
} }
} }
let appId, let appId: string | undefined,
roleId = BUILTIN_ROLE_IDS.PUBLIC roleId = roles.BUILTIN_ROLE_IDS.PUBLIC
if (!ctx.user) { if (!ctx.user) {
// not logged in, try to set a cookie for public apps // not logged in, try to set a cookie for public apps
appId = requestAppId appId = requestAppId
@ -68,16 +65,20 @@ module.exports = async (ctx, next) => {
const isBuilder = const isBuilder =
globalUser && globalUser.builder && globalUser.builder.global globalUser && globalUser.builder && globalUser.builder.global
const isDevApp = appId && isDevAppID(appId) const isDevApp = appId && isDevAppID(appId)
const roleHeader = ctx.request && ctx.request.headers[Headers.PREVIEW_ROLE] const roleHeader =
ctx.request &&
(ctx.request.headers[constants.Header.PREVIEW_ROLE] as string)
if (isBuilder && isDevApp && roleHeader) { if (isBuilder && isDevApp && roleHeader) {
// Ensure the role is valid by ensuring a definition exists // Ensure the role is valid by ensuring a definition exists
try { try {
await getRole(roleHeader) if (roleHeader) {
roleId = roleHeader await roles.getRole(roleHeader)
roleId = roleHeader
// Delete admin and builder flags so that the specified role is honoured // Delete admin and builder flags so that the specified role is honoured
delete ctx.user.builder delete ctx.user.builder
delete ctx.user.admin delete ctx.user.admin
}
} catch (error) { } catch (error) {
// Swallow error and do nothing // Swallow error and do nothing
} }
@ -89,7 +90,7 @@ module.exports = async (ctx, next) => {
return next() return next()
} }
return doInAppContext(appId, async () => { return context.doInAppContext(appId, async () => {
let skipCookie = false let skipCookie = false
// if the user not in the right tenant then make sure they have no permissions // if the user not in the right tenant then make sure they have no permissions
// need to judge this only based on the request app ID, // need to judge this only based on the request app ID,
@ -97,14 +98,14 @@ module.exports = async (ctx, next) => {
env.MULTI_TENANCY && env.MULTI_TENANCY &&
ctx.user && ctx.user &&
requestAppId && requestAppId &&
!isUserInAppTenant(requestAppId, ctx.user) !tenancy.isUserInAppTenant(requestAppId, ctx.user)
) { ) {
// don't error, simply remove the users rights (they are a public user) // don't error, simply remove the users rights (they are a public user)
delete ctx.user.builder delete ctx.user.builder
delete ctx.user.admin delete ctx.user.admin
delete ctx.user.roles delete ctx.user.roles
ctx.isAuthenticated = false ctx.isAuthenticated = false
roleId = BUILTIN_ROLE_IDS.PUBLIC roleId = roles.BUILTIN_ROLE_IDS.PUBLIC
skipCookie = true skipCookie = true
} }
@ -112,15 +113,17 @@ module.exports = async (ctx, next) => {
if (roleId) { if (roleId) {
ctx.roleId = roleId ctx.roleId = roleId
const globalId = ctx.user ? ctx.user._id : undefined const globalId = ctx.user ? ctx.user._id : undefined
const userId = ctx.user ? generateUserMetadataID(ctx.user._id) : null const userId = ctx.user
? generateUserMetadataID(ctx.user._id!)
: undefined
ctx.user = { ctx.user = {
...ctx.user, ...ctx.user!,
// override userID with metadata one // override userID with metadata one
_id: userId, _id: userId,
userId, userId,
globalId, globalId,
roleId, roleId,
role: await getRole(roleId), role: await roles.getRole(roleId),
} }
} }
if ( if (
@ -129,7 +132,7 @@ module.exports = async (ctx, next) => {
appCookie.appId !== requestAppId) && appCookie.appId !== requestAppId) &&
!skipCookie !skipCookie
) { ) {
setCookie(ctx, { appId }, Cookies.CurrentApp) utils.setCookie(ctx, { appId }, constants.Cookie.CurrentApp)
} }
return next() return next()

View File

@ -1,20 +1,21 @@
const Joi = require("joi") import Joi from "joi"
import { BBContext } from "@budibase/types"
function validate(schema, property) { function validate(schema: Joi.Schema, property: string) {
// Return a Koa middleware function // Return a Koa middleware function
return (ctx, next) => { return (ctx: BBContext, next: any) => {
if (!schema) { if (!schema) {
return next() return next()
} }
let params = null let params = null
if (ctx[property] != null) { if (ctx[property] != null) {
params = ctx[property] params = ctx[property]
} else if (ctx.request[property] != null) { } else if (ctx.request.get(property) != null) {
params = ctx.request[property] params = ctx.request.get(property)
} }
// not all schemas have the append property e.g. array schemas // not all schemas have the append property e.g. array schemas
if (schema.append) { if ("append" in schema && schema.append) {
schema = schema.append({ schema = schema.append({
createdAt: Joi.any().optional(), createdAt: Joi.any().optional(),
updatedAt: Joi.any().optional(), updatedAt: Joi.any().optional(),
@ -30,10 +31,10 @@ function validate(schema, property) {
} }
} }
module.exports.body = schema => { export function body(schema: Joi.Schema) {
return validate(schema, "body") return validate(schema, "body")
} }
module.exports.params = schema => { export function params(schema: Joi.Schema) {
return validate(schema, "params") return validate(schema, "params")
} }

View File

@ -1,21 +0,0 @@
const { Headers } = require("@budibase/backend-core/constants")
const { getAppIdFromCtx } = require("@budibase/backend-core/utils")
module.exports = function ({ requiresAppId } = {}) {
return async (ctx, next) => {
const appId = await getAppIdFromCtx(ctx)
if (requiresAppId && !appId) {
ctx.throw(
400,
`Invalid app ID provided, please check the ${Headers.APP_ID} header.`
)
}
if (!ctx.headers[Headers.API_KEY]) {
ctx.throw(
400,
`Invalid API key provided, please check the ${Headers.API_KEY} header.`
)
}
return next()
}
}

View File

@ -0,0 +1,21 @@
import { constants, utils } from "@budibase/backend-core"
import { BBContext } from "@budibase/types"
export = function ({ requiresAppId }: { requiresAppId?: boolean } = {}) {
return async (ctx: BBContext, next: any) => {
const appId = await utils.getAppIdFromCtx(ctx)
if (requiresAppId && !appId) {
ctx.throw(
400,
`Invalid app ID provided, please check the ${constants.Header.APP_ID} header.`
)
}
if (!ctx.headers[constants.Header.API_KEY]) {
ctx.throw(
400,
`Invalid API key provided, please check the ${constants.Header.API_KEY} header.`
)
}
return next()
}
}

View File

@ -1,17 +1,23 @@
class ResourceIdGetter { import { BBContext } from "@budibase/types"
constructor(ctxProperty) {
export class ResourceIdGetter {
parameter: string
main: string | null
sub: string | null
constructor(ctxProperty: string) {
this.parameter = ctxProperty this.parameter = ctxProperty
this.main = null this.main = null
this.sub = null this.sub = null
return this return this
} }
mainResource(field) { mainResource(field: string) {
this.main = field this.main = field
return this return this
} }
subResource(field) { subResource(field: string) {
this.sub = field this.sub = field
return this return this
} }
@ -20,7 +26,8 @@ class ResourceIdGetter {
const parameter = this.parameter, const parameter = this.parameter,
main = this.main, main = this.main,
sub = this.sub sub = this.sub
return (ctx, next) => { return (ctx: BBContext, next: any) => {
// @ts-ignore
const request = ctx.request[parameter] || ctx[parameter] const request = ctx.request[parameter] || ctx[parameter]
if (request == null) { if (request == null) {
return next() return next()
@ -36,24 +43,22 @@ class ResourceIdGetter {
} }
} }
module.exports.ResourceIdGetter = ResourceIdGetter export function paramResource(main: string) {
module.exports.paramResource = main => {
return new ResourceIdGetter("params").mainResource(main).build() return new ResourceIdGetter("params").mainResource(main).build()
} }
module.exports.paramSubResource = (main, sub) => { export function paramSubResource(main: string, sub: string) {
return new ResourceIdGetter("params") return new ResourceIdGetter("params")
.mainResource(main) .mainResource(main)
.subResource(sub) .subResource(sub)
.build() .build()
} }
module.exports.bodyResource = main => { export function bodyResource(main: string) {
return new ResourceIdGetter("body").mainResource(main).build() return new ResourceIdGetter("body").mainResource(main).build()
} }
module.exports.bodySubResource = (main, sub) => { export function bodySubResource(main: string, sub: string) {
return new ResourceIdGetter("body") return new ResourceIdGetter("body")
.mainResource(main) .mainResource(main)
.subResource(sub) .subResource(sub)

View File

@ -1,7 +1,9 @@
const env = require("../environment") import env from "../environment"
import { BBContext } from "@budibase/types"
// if added as a middleware will stop requests unless builder is in self host mode // if added as a middleware will stop requests unless builder is in self host mode
// or cloud is in self host // or cloud is in self host
module.exports = async (ctx, next) => { export = async (ctx: BBContext, next: any) => {
if (env.SELF_HOSTED) { if (env.SELF_HOSTED) {
await next() await next()
return return

View File

@ -9,7 +9,7 @@ jest.mock("../../environment", () => ({
) )
const authorizedMiddleware = require("../authorized") const authorizedMiddleware = require("../authorized")
const env = require("../../environment") const env = require("../../environment")
const { PermissionTypes, PermissionLevels } = require("@budibase/backend-core/permissions") const { PermissionType, PermissionLevel } = require("@budibase/backend-core/permissions")
const { doInAppContext } = require("@budibase/backend-core/context") const { doInAppContext } = require("@budibase/backend-core/context")
const APP_ID = "" const APP_ID = ""
@ -113,7 +113,7 @@ describe("Authorization middleware", () => {
it("throws if the user does not have builder permissions", async () => { it("throws if the user does not have builder permissions", async () => {
config.setEnvironment(false) config.setEnvironment(false)
config.setMiddlewareRequiredPermission(PermissionTypes.BUILDER) config.setMiddlewareRequiredPermission(PermissionType.BUILDER)
config.setUser({ config.setUser({
role: { role: {
_id: "" _id: ""
@ -125,13 +125,13 @@ describe("Authorization middleware", () => {
}) })
it("passes on to next() middleware if the user has resource permission", async () => { it("passes on to next() middleware if the user has resource permission", async () => {
config.setResourceId(PermissionTypes.QUERY) config.setResourceId(PermissionType.QUERY)
config.setUser({ config.setUser({
role: { role: {
_id: "" _id: ""
} }
}) })
config.setMiddlewareRequiredPermission(PermissionTypes.QUERY) config.setMiddlewareRequiredPermission(PermissionType.QUERY)
await config.executeMiddleware() await config.executeMiddleware()
expect(config.next).toHaveBeenCalled() expect(config.next).toHaveBeenCalled()
@ -155,7 +155,7 @@ describe("Authorization middleware", () => {
_id: "" _id: ""
}, },
}) })
config.setMiddlewareRequiredPermission(PermissionTypes.ADMIN, PermissionLevels.BASIC) config.setMiddlewareRequiredPermission(PermissionType.ADMIN, PermissionLevel.BASIC)
await config.executeMiddleware() await config.executeMiddleware()
expect(config.throw).toHaveBeenCalledWith(403, "User does not have permission") expect(config.throw).toHaveBeenCalledWith(403, "User does not have permission")

Some files were not shown because too many files have changed in this diff Show More