Major update - removing the use of context for PouchDB instances, swapping knowledge of PouchDB to the PouchLike structure that replaces it.

This commit is contained in:
mike12345567 2022-11-09 16:53:42 +00:00
parent a5624142a8
commit c744d23832
25 changed files with 211 additions and 419 deletions

View File

@ -1,6 +1,6 @@
require("../../../tests/utilities/TestConfiguration") require("../../../tests/utilities/TestConfiguration")
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 { PouchLike } from "../couch"
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: PouchLike, 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: PouchLike,
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: PouchLike, 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: PouchLike,
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: PouchLike
writeRateMs: number writeRateMs: number
constructor( constructor(db: PouchLike, 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,17 +1,5 @@
export enum ContextKey { export enum ContextKey {
TENANT_ID = "tenantId", TENANT_ID = "tenantId",
GLOBAL_DB = "globalDb",
APP_ID = "appId", APP_ID = "appId",
IDENTITY = "identity", IDENTITY = "identity",
// whatever the request app DB was
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,20 +1,12 @@
import env from "../environment" import env from "../environment"
import { SEPARATOR, DocumentType } from "../db/constants" import { SEPARATOR, DocumentType } from "../db/constants"
import cls from "./FunctionContext" import cls from "./FunctionContext"
import { dangerousGetDB, closeDB } from "../db"
import { baseGlobalDBName } from "../db/tenancy" import { baseGlobalDBName } from "../db/tenancy"
import { IdentityContext } from "@budibase/types" import { IdentityContext } from "@budibase/types"
import { DEFAULT_TENANT_ID as _DEFAULT_TENANT_ID } from "../constants" import { DEFAULT_TENANT_ID as _DEFAULT_TENANT_ID } from "../constants"
import { ContextKey } from "./constants" import { ContextKey } from "./constants"
import PouchDB from "pouchdb" import { PouchLike } from "../couch"
import { import { getDevelopmentAppID, getProdAppID } from "../db/conversions"
updateUsing,
closeWithUsing,
setAppTenantId,
setIdentity,
closeAppDBs,
getContextDB,
} from "./utils"
export const DEFAULT_TENANT_ID = _DEFAULT_TENANT_ID export const DEFAULT_TENANT_ID = _DEFAULT_TENANT_ID
@ -22,29 +14,19 @@ export const DEFAULT_TENANT_ID = _DEFAULT_TENANT_ID
// 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 () => {
try {
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 = () => {
// return getTenantId() === DEFAULT_TENANT_ID
// }
export const isMultiTenant = () => { export const isMultiTenant = () => {
return env.MULTI_TENANCY return env.MULTI_TENANCY
} }
const setAppTenantId = (appId: string) => {
const appTenantId = getTenantIDFromAppID(appId) || DEFAULT_TENANT_ID
updateTenantId(appTenantId)
}
const setIdentity = (identity: IdentityContext | null) => {
cls.setOnContext(ContextKey.IDENTITY, identity)
}
/** /**
* 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.
@ -78,47 +60,28 @@ export const doInContext = async (appId: string, task: any) => {
}) })
} }
export const doInTenant = (tenantId: string | null, task: any) => { export const doInTenant = (tenantId: string | null, task: any): 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 { return cls.run(async () => {
// invoke the task updateTenantId(tenantId)
return await task() return await task()
} finally {
await closeWithUsing(ContextKey.TENANCY_IN_USE, () => {
return closeTenancy()
}) })
} }
}
const existing = cls.getFromContext(ContextKey.TENANT_ID) === tenantId export const doInAppContext = (appId: string, task: any): any => {
return updateUsing(ContextKey.TENANCY_IN_USE, existing, internal)
}
export const doInAppContext = (appId: string, task: any) => {
if (!appId) { if (!appId) {
throw new Error("appId is required") throw new Error("appId is required")
} }
const identity = getIdentity() const identity = getIdentity()
// the internal function is so that we can re-use an existing return cls.run(async () => {
// context - don't want to close DB on a parent context
async function internal(opts = { existing: false }) {
// set the app tenant id // set the app tenant id
if (!opts.existing) {
setAppTenantId(appId) setAppTenantId(appId)
}
// set the app ID // set the app ID
cls.setOnContext(ContextKey.APP_ID, appId) cls.setOnContext(ContextKey.APP_ID, appId)
@ -126,48 +89,29 @@ export const doInAppContext = (appId: string, task: any) => {
if (identity) { if (identity) {
setIdentity(identity) setIdentity(identity)
} }
try {
// invoke the task // invoke the task
return await 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 updateUsing(ContextKey.APP_IN_USE, existing, internal)
}
export const doInIdentityContext = (identity: IdentityContext, task: any) => { export const doInIdentityContext = (
identity: IdentityContext,
task: any
): any => {
if (!identity) { if (!identity) {
throw new Error("identity is required") throw new Error("identity is required")
} }
async function internal(opts = { existing: false }) { return cls.run(async () => {
if (!opts.existing) {
cls.setOnContext(ContextKey.IDENTITY, identity) cls.setOnContext(ContextKey.IDENTITY, identity)
// set the tenant so that doInTenant will preserve identity // set the tenant so that doInTenant will preserve identity
if (identity.tenantId) { if (identity.tenantId) {
updateTenantId(identity.tenantId) updateTenantId(identity.tenantId)
} }
}
try {
// invoke the task // invoke the task
return await task() return await task()
} finally {
await closeWithUsing(ContextKey.IDENTITY_IN_USE, async () => {
setIdentity(null)
await closeTenancy()
}) })
} }
}
const existing = cls.getFromContext(ContextKey.IDENTITY)
return updateUsing(ContextKey.IDENTITY_IN_USE, existing, internal)
}
export const getIdentity = (): IdentityContext | undefined => { export const getIdentity = (): IdentityContext | undefined => {
try { try {
@ -179,15 +123,10 @@ export const getIdentity = (): IdentityContext | undefined => {
export const updateTenantId = (tenantId: string | null) => { export const updateTenantId = (tenantId: string | null) => {
cls.setOnContext(ContextKey.TENANT_ID, tenantId) cls.setOnContext(ContextKey.TENANT_ID, tenantId)
if (env.USE_COUCH) {
setGlobalDB(tenantId)
}
} }
export const updateAppId = async (appId: string) => { export const updateAppId = async (appId: string) => {
try { try {
// have to close first, before removing the databases from context
await closeAppDBs()
cls.setOnContext(ContextKey.APP_ID, appId) cls.setOnContext(ContextKey.APP_ID, appId)
} catch (err) { } catch (err) {
if (env.isTest()) { if (env.isTest()) {
@ -198,19 +137,9 @@ export const updateAppId = async (appId: string) => {
} }
} }
export const setGlobalDB = (tenantId: string | null) => { export const getGlobalDB = (): PouchLike => {
const dbName = baseGlobalDBName(tenantId) const tenantId = cls.getFromContext(ContextKey.TENANT_ID)
const db = dangerousGetDB(dbName) return new PouchLike(baseGlobalDBName(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")
}
return db
} }
export const isTenantIdSet = () => { export const isTenantIdSet = () => {
@ -246,22 +175,25 @@ export const isTenancyEnabled = () => {
* Opens the app database based on whatever the request * Opens the app database based on whatever the request
* contained, dev or prod. * contained, dev or prod.
*/ */
export const getAppDB = (opts?: any) => { export const getAppDB = (opts?: any): PouchLike => {
return getContextDB(ContextKey.CURRENT_DB, opts) const appId = getAppId()
return new PouchLike(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 open the prod one.
*/ */
export const getProdAppDB = (opts?: any) => { export const getProdAppDB = (opts?: any): PouchLike => {
return getContextDB(ContextKey.PROD_DB, opts) const appId = getAppId()
return new PouchLike(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 open the dev one.
*/ */
export const getDevAppDB = (opts?: any) => { export const getDevAppDB = (opts?: any): PouchLike => {
return getContextDB(ContextKey.DEV_DB, opts) const appId = getAppId()
return new PouchLike(getDevelopmentAppID(appId), opts)
} }

View File

@ -5,8 +5,8 @@ import env from "../../environment"
// must use require to spy index file exports due to known issue in jest // must use require to spy index file exports due to known issue in jest
const dbUtils = require("../../db") const dbUtils = require("../../db")
jest.spyOn(dbUtils, "closeDB") jest.spyOn(dbUtils, "closePouchDB")
jest.spyOn(dbUtils, "dangerousGetDB") jest.spyOn(dbUtils, "getDB")
describe("context", () => { describe("context", () => {
beforeEach(() => { beforeEach(() => {
@ -25,8 +25,8 @@ 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.getDB).toHaveBeenCalledTimes(1)
expect(dbUtils.closeDB).toHaveBeenCalledTimes(1) expect(dbUtils.closePouchDB).toHaveBeenCalledTimes(1)
}) })
}) })
@ -85,8 +85,8 @@ 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.getDB).toHaveBeenCalledTimes(1)
expect(dbUtils.closeDB).toHaveBeenCalledTimes(1) expect(dbUtils.closePouchDB).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 () => {
@ -123,8 +123,8 @@ describe("context", () => {
}) })
// only 1 db is opened and closed // only 1 db is opened and closed
expect(dbUtils.dangerousGetDB).toHaveBeenCalledTimes(1) expect(dbUtils.getDB).toHaveBeenCalledTimes(1)
expect(dbUtils.closeDB).toHaveBeenCalledTimes(1) expect(dbUtils.closePouchDB).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,3 +1,4 @@
export * from "./couch" export * from "./couch"
export * from "./pouchLike" export * from "./pouchLike"
export * from "./utils" export * from "./utils"
export { init } from "./pouchDB"

View File

@ -0,0 +1,37 @@
import PouchDB from "pouchdb"
import env from "../environment"
import { PouchOptions } from "@budibase/types"
import * as pouch from "../db/pouch"
let Pouch: any
let initialised = false
export async function init(opts?: PouchOptions) {
Pouch = 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()
return new Pouch(dbName, opts)
}
// 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

@ -2,12 +2,16 @@ import Nano from "nano"
import { AnyDocument } from "@budibase/types" import { AnyDocument } from "@budibase/types"
import { getCouchInfo } from "./couch" import { getCouchInfo } from "./couch"
import { directCouchCall } from "./utils" import { directCouchCall } from "./utils"
import { getPouchDB } from "../db" import { getPouchDB } from "./pouchDB"
export type PouchLikeOpts = { export type PouchLikeOpts = {
skip_setup?: boolean skip_setup?: boolean
} }
export type PutOpts = {
force?: boolean
}
export type QueryOpts = { export type QueryOpts = {
include_docs?: boolean include_docs?: boolean
startkey?: string startkey?: string
@ -19,6 +23,10 @@ export type QueryOpts = {
keys?: string[] keys?: string[]
} }
type QueryResp<T> = Promise<{
rows: { doc?: T | any; value?: any }[]
}>
export class PouchLike { export class PouchLike {
public readonly name: string public readonly name: string
private static nano: Nano.ServerScope private static nano: Nano.ServerScope
@ -45,11 +53,15 @@ export class PouchLike {
}) })
} }
async exists() {
let response = await directCouchCall(`/${this.name}`, "HEAD")
return response.status === 200
}
async checkSetup() { async checkSetup() {
let shouldCreate = !this.pouchOpts?.skip_setup let shouldCreate = !this.pouchOpts?.skip_setup
// check exists in a lightweight fashion // check exists in a lightweight fashion
let response = await directCouchCall(`/${this.name}`, "HEAD") let exists = await this.exists()
let exists = response.status === 200
if (!shouldCreate && !exists) { if (!shouldCreate && !exists) {
throw new Error("DB does not exist") throw new Error("DB does not exist")
} }
@ -70,26 +82,43 @@ export class PouchLike {
} }
} }
async info() { async get<T>(id?: string): Promise<T | any> {
const db = PouchLike.nano.db.use(this.name)
return db.info()
}
async get(id: string) {
const db = await this.checkSetup() const db = await this.checkSetup()
if (!id) {
throw new Error("Unable to get doc without a valid _id.")
}
return this.updateOutput(() => db.get(id)) return this.updateOutput(() => db.get(id))
} }
async remove(id: string, rev: string) { async remove(id?: string, rev?: string) {
const db = await this.checkSetup() 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)) return this.updateOutput(() => db.destroy(id, rev))
} }
async put(document: AnyDocument) { async put(document: AnyDocument, opts?: PutOpts) {
if (!document._id) { if (!document._id) {
throw new Error("Cannot store document without _id field.") throw new Error("Cannot store document without _id field.")
} }
const db = await this.checkSetup() 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)) return this.updateOutput(() => db.insert(document))
} }
@ -98,12 +127,12 @@ export class PouchLike {
return this.updateOutput(() => db.bulk({ docs: documents })) return this.updateOutput(() => db.bulk({ docs: documents }))
} }
async allDocs(params: QueryOpts) { async allDocs<T>(params: QueryOpts): QueryResp<T> {
const db = await this.checkSetup() const db = await this.checkSetup()
return this.updateOutput(() => db.list(params)) return this.updateOutput(() => db.list(params))
} }
async query(viewName: string, params: QueryOpts) { async query<T>(viewName: string, params: QueryOpts): QueryResp<T> {
const db = await this.checkSetup() const db = await this.checkSetup()
const [database, view] = viewName.split("/") const [database, view] = viewName.split("/")
return this.updateOutput(() => db.view(database, view, params)) return this.updateOutput(() => db.view(database, view, params))

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,87 +1,30 @@
import * as pouch from "./pouch"
import env from "../environment" import env from "../environment"
import { PouchOptions, CouchFindOptions } from "@budibase/types" import { CouchFindOptions } from "@budibase/types"
import PouchDB from "pouchdb"
import { PouchLike } from "../couch" import { PouchLike } from "../couch"
import { directCouchQuery } from "../couch" import { directCouchQuery } from "../couch"
export { directCouchQuery } from "../couch" export { init, PouchLike } from "../couch"
const openDbs: string[] = []
let Pouch: any
let initialised = false let initialised = false
const dbList = new Set() 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 = () => { const checkInitialised = () => {
if (!initialised) { if (!initialised) {
throw new Error("init has not been called") throw new Error("init has not been called")
} }
} }
export async function init(opts?: PouchOptions) { export function getDB(dbName: string, opts?: any): PouchLike {
Pouch = pouch.getPouch(opts)
initialised = true
}
export function getPouchDB(dbName: string, opts?: any): PouchDB.Database {
checkInitialised()
if (env.isTest()) { if (env.isTest()) {
dbList.add(dbName) 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
}
// 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): PouchLike {
return new PouchLike(dbName, opts) return new PouchLike(dbName, opts)
} }
// 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 // we have to use a callback for this so that we can close
// the DB when we're done, without this manual requests would // the DB when we're done, without this manual requests would
// need to close the database when done with it to avoid memory leaks // need to close the database when done with it to avoid memory leaks
export async function doWithDB(dbName: string, cb: any, opts = {}) { export async function doWithDB(dbName: string, cb: any, opts = {}) {
const db = dangerousGetDB(dbName, opts) const db = getDB(dbName, opts)
// need this to be async so that we can correctly close DB after all // need this to be async so that we can correctly close DB after all
// async operations have been completed // async operations have been completed
return await cb(db) return await cb(db)

View File

@ -1,11 +1,11 @@
require("../../../tests/utilities/TestConfiguration") require("../../../tests/utilities/TestConfiguration")
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,6 +1,6 @@
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 { PouchLike, QueryOpts } from "../couch"
import { StaticDatabases } from "./constants" import { StaticDatabases } from "./constants"
import { doWithDB } from "./" import { doWithDB } from "./"
@ -19,7 +19,7 @@ interface DesignDocument {
views: any views: any
} }
async function removeDeprecated(db: PouchDB.Database, viewName: ViewName) { async function removeDeprecated(db: PouchLike, 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: PouchLike) => {
StaticDatabases.PLATFORM_INFO.name,
async (db: PouchDB.Database) => {
await createView(db, viewJs, ViewName.ACCOUNT_BY_EMAIL) 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: PouchLike) => {
StaticDatabases.PLATFORM_INFO.name,
async (db: PouchDB.Database) => {
await createView(db, viewJs, ViewName.PLATFORM_USERS_LOWERCASE) await createView(db, viewJs, ViewName.PLATFORM_USERS_LOWERCASE)
} })
)
} }
export interface QueryViewOptions { export interface QueryViewOptions {
@ -131,13 +125,13 @@ export interface QueryViewOptions {
export const queryView = async <T>( export const queryView = async <T>(
viewName: ViewName, viewName: ViewName,
params: PouchDB.Query.Options<T, T>, params: QueryOpts,
db: PouchDB.Database, db: PouchLike,
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(`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 => (params.include_docs ? row.doc : row.value))
@ -161,7 +155,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: QueryOpts,
opts?: QueryViewOptions opts?: QueryViewOptions
): Promise<T[] | T | undefined> => { ): Promise<T[] | T | undefined> => {
const CreateFuncByName: any = { const CreateFuncByName: any = {
@ -169,19 +163,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: PouchLike) => {
StaticDatabases.PLATFORM_INFO.name,
async (db: PouchDB.Database) => {
const createFn = CreateFuncByName[viewName] const createFn = CreateFuncByName[viewName]
return queryView(viewName, params, db, createFn, opts) 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: QueryOpts,
db?: PouchDB.Database, db?: PouchLike,
opts?: QueryViewOptions opts?: QueryViewOptions
): Promise<T[] | T | undefined> => { ): Promise<T[] | T | undefined> => {
const CreateFuncByName: any = { const CreateFuncByName: any = {
@ -192,7 +183,7 @@ 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

@ -21,6 +21,7 @@ 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 types from "./types"
// mimic the outer package exports // mimic the outer package exports
import * as db from "./pkg/db" import * as db from "./pkg/db"
@ -67,6 +68,7 @@ const core = {
encryption, encryption,
queue, queue,
permissions, permissions,
...types,
} }
export = core export = core

View File

@ -1,6 +1,6 @@
require("../../../tests/utilities/TestConfiguration") require("../../../tests/utilities/TestConfiguration")
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

@ -0,0 +1 @@
export { PouchLike } from "./couch"

View File

@ -8,10 +8,9 @@ import { queryGlobalView } from "./db/views"
import { UNICODE_MAX } from "./db/constants" import { UNICODE_MAX } from "./db/constants"
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
} }

View File

@ -524,12 +524,10 @@ export const sync = async (ctx: any, next: any) => {
// replicate prod to dev // replicate prod to dev
const prodAppId = getProdAppID(appId) const prodAppId = 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 info = await prodDb.info() const exists = await prodDb.exists()
if (info.error) throw info.error if (!exists) {
} 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 = {

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

@ -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,9 +1,8 @@
import { events } from "@budibase/backend-core" import { events, PouchLike } from "@budibase/backend-core"
import sdk from "../../../../sdk" import sdk from "../../../../sdk"
import PouchDB from "pouchdb"
export const backfill = async ( export const backfill = async (
appDb: PouchDB.Database, appDb: PouchLike,
timestamp: string | number timestamp: string | number
) => { ) => {
const tables = await sdk.tables.getAllInternalTables(appDb) const tables = await sdk.tables.getAllInternalTables(appDb)

View File

@ -1,4 +1,4 @@
import { db as dbCore } from "@budibase/backend-core" import { db as dbCore, PouchLike } from "@budibase/backend-core"
import { getAutomationParams, TABLE_ROW_PREFIX } from "../../../db/utils" import { getAutomationParams, TABLE_ROW_PREFIX } from "../../../db/utils"
import { budibaseTempDir } from "../../../utilities/budibaseDir" import { budibaseTempDir } from "../../../utilities/budibaseDir"
import { DB_EXPORT_FILE, GLOBAL_DB_EXPORT_FILE } from "./constants" import { DB_EXPORT_FILE, GLOBAL_DB_EXPORT_FILE } from "./constants"
@ -17,7 +17,6 @@ import {
CouchFindOptions, CouchFindOptions,
RowAttachment, RowAttachment,
} from "@budibase/types" } from "@budibase/types"
import PouchDB from "pouchdb"
const uuid = require("uuid/v4") const uuid = require("uuid/v4")
const tar = require("tar") const tar = require("tar")
@ -29,10 +28,7 @@ type TemplateType = {
key?: string key?: string
} }
async function updateAttachmentColumns( async function updateAttachmentColumns(prodAppId: string, db: PouchLike) {
prodAppId: string,
db: PouchDB.Database
) {
// iterate through attachment documents and update them // iterate through attachment documents and update them
const tables = await sdk.tables.getAllInternalTables(db) const tables = await sdk.tables.getAllInternalTables(db)
for (let table of tables) { for (let table of tables) {
@ -86,7 +82,7 @@ async function updateAttachmentColumns(
} }
} }
async function updateAutomations(prodAppId: string, db: PouchDB.Database) { async function updateAutomations(prodAppId: string, db: PouchLike) {
const automations = ( const automations = (
await db.allDocs( await db.allDocs(
getAutomationParams(null, { getAutomationParams(null, {
@ -154,7 +150,7 @@ export function getListOfAppsInMulti(tmpPath: string) {
export async function importApp( export async function importApp(
appId: string, appId: string,
db: PouchDB.Database, db: PouchLike,
template: TemplateType template: TemplateType
) { ) {
let prodAppId = dbCore.getProdAppID(appId) let prodAppId = dbCore.getProdAppID(appId)

View File

@ -1,13 +1,12 @@
import { context, db as dbCore } from "@budibase/backend-core" import { context, db as dbCore, PouchLike } from "@budibase/backend-core"
import { import {
getDatasourceParams, getDatasourceParams,
getTableParams, getTableParams,
getAutomationParams, getAutomationParams,
getScreenParams, getScreenParams,
} from "../../../db/utils" } from "../../../db/utils"
import PouchDB from "pouchdb"
async function runInContext(appId: string, cb: any, db?: PouchDB.Database) { async function runInContext(appId: string, cb: any, db?: PouchLike) {
if (db) { if (db) {
return cb(db) return cb(db)
} else { } else {
@ -19,13 +18,10 @@ async function runInContext(appId: string, cb: any, db?: PouchDB.Database) {
} }
} }
export async function calculateDatasourceCount( export async function calculateDatasourceCount(appId: string, db?: PouchLike) {
appId: string,
db?: PouchDB.Database
) {
return runInContext( return runInContext(
appId, appId,
async (db: PouchDB.Database) => { async (db: PouchLike) => {
const datasourceList = await db.allDocs(getDatasourceParams()) const datasourceList = await db.allDocs(getDatasourceParams())
const tableList = await db.allDocs(getTableParams()) const tableList = await db.allDocs(getTableParams())
return datasourceList.rows.length + tableList.rows.length return datasourceList.rows.length + tableList.rows.length
@ -34,13 +30,10 @@ export async function calculateDatasourceCount(
) )
} }
export async function calculateAutomationCount( export async function calculateAutomationCount(appId: string, db?: PouchLike) {
appId: string,
db?: PouchDB.Database
) {
return runInContext( return runInContext(
appId, appId,
async (db: PouchDB.Database) => { async (db: PouchLike) => {
const automationList = await db.allDocs(getAutomationParams()) const automationList = await db.allDocs(getAutomationParams())
return automationList.rows.length return automationList.rows.length
}, },
@ -48,13 +41,10 @@ export async function calculateAutomationCount(
) )
} }
export async function calculateScreenCount( export async function calculateScreenCount(appId: string, db?: PouchLike) {
appId: string,
db?: PouchDB.Database
) {
return runInContext( return runInContext(
appId, appId,
async (db: PouchDB.Database) => { async (db: PouchLike) => {
const screenList = await db.allDocs(getScreenParams()) const screenList = await db.allDocs(getScreenParams())
return screenList.rows.length return screenList.rows.length
}, },
@ -63,7 +53,7 @@ export async function calculateScreenCount(
} }
export async function calculateBackupStats(appId: string) { export async function calculateBackupStats(appId: string) {
return runInContext(appId, async (db: PouchDB.Database) => { return runInContext(appId, async (db: PouchLike) => {
const promises = [] const promises = []
promises.push(calculateDatasourceCount(appId, db)) promises.push(calculateDatasourceCount(appId, db))
promises.push(calculateAutomationCount(appId, db)) promises.push(calculateAutomationCount(appId, db))

View File

@ -1,4 +1,4 @@
import { getAppDB } from "@budibase/backend-core/context" import { context, PouchLike } from "@budibase/backend-core"
import { BudibaseInternalDB, getTableParams } from "../../../db/utils" import { BudibaseInternalDB, getTableParams } from "../../../db/utils"
import { import {
breakExternalTableId, breakExternalTableId,
@ -6,11 +6,10 @@ import {
isSQL, isSQL,
} from "../../../integrations/utils" } from "../../../integrations/utils"
import { Table } from "@budibase/types" import { Table } from "@budibase/types"
import PouchDB from "pouchdb"
async function getAllInternalTables(db?: PouchDB.Database): Promise<Table[]> { async function getAllInternalTables(db?: PouchLike): Promise<Table[]> {
if (!db) { if (!db) {
db = getAppDB() as PouchDB.Database db = context.getAppDB()
} }
const internalTables = await db.allDocs( const internalTables = await db.allDocs(
getTableParams(null, { getTableParams(null, {
@ -25,7 +24,7 @@ async function getAllInternalTables(db?: PouchDB.Database): Promise<Table[]> {
} }
async function getAllExternalTables(datasourceId: any): Promise<Table[]> { async function getAllExternalTables(datasourceId: any): Promise<Table[]> {
const db = getAppDB() const db = context.getAppDB()
const datasource = await db.get(datasourceId) const datasource = await db.get(datasourceId)
if (!datasource || !datasource.entities) { if (!datasource || !datasource.entities) {
throw "Datasource is not configured fully." throw "Datasource is not configured fully."
@ -42,7 +41,7 @@ async function getExternalTable(
} }
async function getTable(tableId: any): Promise<Table> { async function getTable(tableId: any): Promise<Table> {
const db = getAppDB() const db = context.getAppDB()
if (isExternalTable(tableId)) { if (isExternalTable(tableId)) {
let { datasourceId, tableName } = breakExternalTableId(tableId) let { datasourceId, tableName } = breakExternalTableId(tableId)
const datasource = await db.get(datasourceId) const datasource = await db.get(datasourceId)