2021-04-19 12:34:07 +02:00
|
|
|
const { newid } = require("../hashing")
|
2021-05-13 12:06:08 +02:00
|
|
|
const Replication = require("./Replication")
|
2021-08-05 10:59:08 +02:00
|
|
|
const { DEFAULT_TENANT_ID } = require("../constants")
|
|
|
|
const env = require("../environment")
|
|
|
|
const { StaticDatabases, SEPARATOR } = require("./constants")
|
|
|
|
const { getTenantId } = require("../tenancy")
|
2021-08-06 17:38:07 +02:00
|
|
|
const fetch = require("node-fetch")
|
|
|
|
const { getCouch } = require("./index")
|
2021-04-19 12:34:07 +02:00
|
|
|
|
2021-05-13 14:29:53 +02:00
|
|
|
const UNICODE_MAX = "\ufff0"
|
|
|
|
|
2021-04-19 18:31:47 +02:00
|
|
|
exports.ViewNames = {
|
|
|
|
USER_BY_EMAIL: "by_email",
|
|
|
|
}
|
|
|
|
|
2021-08-05 10:59:08 +02:00
|
|
|
exports.StaticDatabases = StaticDatabases
|
|
|
|
|
|
|
|
const PRE_APP = "app"
|
|
|
|
const PRE_DEV = "dev"
|
2021-07-23 21:01:53 +02:00
|
|
|
|
2021-04-07 12:33:16 +02:00
|
|
|
const DocumentTypes = {
|
|
|
|
USER: "us",
|
2021-08-05 10:59:08 +02:00
|
|
|
WORKSPACE: "workspace",
|
2021-04-20 19:14:36 +02:00
|
|
|
CONFIG: "config",
|
2021-04-21 17:42:44 +02:00
|
|
|
TEMPLATE: "template",
|
2021-08-05 10:59:08 +02:00
|
|
|
APP: PRE_APP,
|
|
|
|
DEV: PRE_DEV,
|
|
|
|
APP_DEV: `${PRE_APP}${SEPARATOR}${PRE_DEV}`,
|
|
|
|
APP_METADATA: `${PRE_APP}${SEPARATOR}metadata`,
|
2021-05-14 16:43:41 +02:00
|
|
|
ROLE: "role",
|
2021-04-07 12:33:16 +02:00
|
|
|
}
|
|
|
|
|
2021-04-08 12:20:37 +02:00
|
|
|
exports.DocumentTypes = DocumentTypes
|
2021-05-13 14:29:53 +02:00
|
|
|
exports.APP_PREFIX = DocumentTypes.APP + SEPARATOR
|
2021-08-05 10:59:08 +02:00
|
|
|
exports.APP_DEV = exports.APP_DEV_PREFIX = DocumentTypes.APP_DEV + SEPARATOR
|
2021-04-13 14:26:13 +02:00
|
|
|
exports.SEPARATOR = SEPARATOR
|
|
|
|
|
2021-05-19 16:09:57 +02:00
|
|
|
function isDevApp(app) {
|
|
|
|
return app.appId.startsWith(exports.APP_DEV_PREFIX)
|
|
|
|
}
|
|
|
|
|
2021-05-14 16:43:41 +02:00
|
|
|
/**
|
|
|
|
* If creating DB allDocs/query params with only a single top level ID this can be used, this
|
|
|
|
* is usually the case as most of our docs are top level e.g. tables, automations, users and so on.
|
|
|
|
* More complex cases such as link docs and rows which have multiple levels of IDs that their
|
|
|
|
* ID consists of need their own functions to build the allDocs parameters.
|
|
|
|
* @param {string} docType The type of document which input params are being built for, e.g. user,
|
|
|
|
* link, app, table and so on.
|
|
|
|
* @param {string|null} docId The ID of the document minus its type - this is only needed if looking
|
|
|
|
* for a singular document.
|
|
|
|
* @param {object} otherProps Add any other properties onto the request, e.g. include_docs.
|
|
|
|
* @returns {object} Parameters which can then be used with an allDocs request.
|
|
|
|
*/
|
|
|
|
function getDocParams(docType, docId = null, otherProps = {}) {
|
|
|
|
if (docId == null) {
|
|
|
|
docId = ""
|
|
|
|
}
|
|
|
|
return {
|
|
|
|
...otherProps,
|
|
|
|
startkey: `${docType}${SEPARATOR}${docId}`,
|
|
|
|
endkey: `${docType}${SEPARATOR}${docId}${UNICODE_MAX}`,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-04-19 12:34:07 +02:00
|
|
|
/**
|
2021-08-05 10:59:08 +02:00
|
|
|
* Generates a new workspace ID.
|
|
|
|
* @returns {string} The new workspace ID which the workspace doc can be stored under.
|
2021-04-19 12:34:07 +02:00
|
|
|
*/
|
2021-08-05 10:59:08 +02:00
|
|
|
exports.generateWorkspaceID = () => {
|
|
|
|
return `${DocumentTypes.WORKSPACE}${SEPARATOR}${newid()}`
|
2021-04-19 12:34:07 +02:00
|
|
|
}
|
|
|
|
|
2021-04-07 12:33:16 +02:00
|
|
|
/**
|
2021-08-05 10:59:08 +02:00
|
|
|
* Gets parameters for retrieving workspaces.
|
2021-04-19 17:16:46 +02:00
|
|
|
*/
|
2021-08-05 10:59:08 +02:00
|
|
|
exports.getWorkspaceParams = (id = "", otherProps = {}) => {
|
2021-04-19 17:16:46 +02:00
|
|
|
return {
|
|
|
|
...otherProps,
|
2021-08-05 10:59:08 +02:00
|
|
|
startkey: `${DocumentTypes.WORKSPACE}${SEPARATOR}${id}`,
|
|
|
|
endkey: `${DocumentTypes.WORKSPACE}${SEPARATOR}${id}${UNICODE_MAX}`,
|
2021-04-19 17:16:46 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2021-04-20 18:17:44 +02:00
|
|
|
* Generates a new global user ID.
|
|
|
|
* @returns {string} The new user ID which the user doc can be stored under.
|
|
|
|
*/
|
2021-05-04 12:32:22 +02:00
|
|
|
exports.generateGlobalUserID = id => {
|
2021-04-21 22:08:04 +02:00
|
|
|
return `${DocumentTypes.USER}${SEPARATOR}${id || newid()}`
|
2021-04-20 18:17:44 +02:00
|
|
|
}
|
|
|
|
|
2021-04-19 17:16:46 +02:00
|
|
|
/**
|
|
|
|
* Gets parameters for retrieving users.
|
2021-04-07 12:33:16 +02:00
|
|
|
*/
|
2021-04-21 17:42:44 +02:00
|
|
|
exports.getGlobalUserParams = (globalId, otherProps = {}) => {
|
2021-04-19 18:31:47 +02:00
|
|
|
if (!globalId) {
|
|
|
|
globalId = ""
|
2021-04-09 16:11:49 +02:00
|
|
|
}
|
2021-04-07 12:33:16 +02:00
|
|
|
return {
|
|
|
|
...otherProps,
|
2021-04-19 18:31:47 +02:00
|
|
|
startkey: `${DocumentTypes.USER}${SEPARATOR}${globalId}`,
|
|
|
|
endkey: `${DocumentTypes.USER}${SEPARATOR}${globalId}${UNICODE_MAX}`,
|
2021-04-07 12:33:16 +02:00
|
|
|
}
|
|
|
|
}
|
2021-04-21 17:42:44 +02:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Generates a template ID.
|
2021-08-05 10:59:08 +02:00
|
|
|
* @param ownerId The owner/user of the template, this could be global or a workspace level.
|
2021-04-21 17:42:44 +02:00
|
|
|
*/
|
2021-05-04 12:32:22 +02:00
|
|
|
exports.generateTemplateID = ownerId => {
|
2021-05-10 15:41:52 +02:00
|
|
|
return `${DocumentTypes.TEMPLATE}${SEPARATOR}${ownerId}${SEPARATOR}${newid()}`
|
2021-04-21 17:42:44 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2021-08-05 10:59:08 +02:00
|
|
|
* Gets parameters for retrieving templates. Owner ID must be specified, either global or a workspace level.
|
2021-04-21 17:42:44 +02:00
|
|
|
*/
|
|
|
|
exports.getTemplateParams = (ownerId, templateId, otherProps = {}) => {
|
|
|
|
if (!templateId) {
|
|
|
|
templateId = ""
|
|
|
|
}
|
2021-04-21 19:15:57 +02:00
|
|
|
let final
|
|
|
|
if (templateId) {
|
|
|
|
final = templateId
|
|
|
|
} else {
|
|
|
|
final = `${DocumentTypes.TEMPLATE}${SEPARATOR}${ownerId}${SEPARATOR}`
|
2021-04-09 16:11:49 +02:00
|
|
|
}
|
2021-04-07 12:33:16 +02:00
|
|
|
return {
|
|
|
|
...otherProps,
|
2021-04-21 17:42:44 +02:00
|
|
|
startkey: final,
|
|
|
|
endkey: `${final}${UNICODE_MAX}`,
|
2021-04-07 12:33:16 +02:00
|
|
|
}
|
|
|
|
}
|
2021-04-20 19:14:36 +02:00
|
|
|
|
2021-05-14 16:43:41 +02:00
|
|
|
/**
|
|
|
|
* Generates a new role ID.
|
|
|
|
* @returns {string} The new role ID which the role doc can be stored under.
|
|
|
|
*/
|
|
|
|
exports.generateRoleID = id => {
|
|
|
|
return `${DocumentTypes.ROLE}${SEPARATOR}${id || newid()}`
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Gets parameters for retrieving a role, this is a utility function for the getDocParams function.
|
|
|
|
*/
|
|
|
|
exports.getRoleParams = (roleId = null, otherProps = {}) => {
|
|
|
|
return getDocParams(DocumentTypes.ROLE, roleId, otherProps)
|
|
|
|
}
|
|
|
|
|
2021-05-17 15:20:19 +02:00
|
|
|
/**
|
|
|
|
* Convert a development app ID to a deployed app ID.
|
|
|
|
*/
|
|
|
|
exports.getDeployedAppID = appId => {
|
|
|
|
// if dev, convert it
|
|
|
|
if (appId.startsWith(exports.APP_DEV_PREFIX)) {
|
|
|
|
const id = appId.split(exports.APP_DEV_PREFIX)[1]
|
|
|
|
return `${exports.APP_PREFIX}${id}`
|
|
|
|
}
|
|
|
|
return appId
|
|
|
|
}
|
|
|
|
|
2021-08-06 17:38:07 +02:00
|
|
|
/**
|
|
|
|
* if in production this will use the CouchDB _all_dbs call to retrieve a list of databases. If testing
|
|
|
|
* when using Pouch it will use the pouchdb-all-dbs package.
|
|
|
|
*/
|
|
|
|
exports.getAllDbs = async () => {
|
|
|
|
// specifically for testing we use the pouch package for this
|
|
|
|
if (env.isTest()) {
|
|
|
|
return getCouch().allDbs()
|
|
|
|
}
|
|
|
|
const response = await fetch(`${env.COUCH_DB_URL}/_all_dbs`)
|
|
|
|
if (response.status === 200) {
|
|
|
|
return response.json()
|
|
|
|
} else {
|
|
|
|
throw "Cannot connect to CouchDB instance"
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-05-14 17:31:07 +02:00
|
|
|
/**
|
|
|
|
* Lots of different points in the system need to find the full list of apps, this will
|
|
|
|
* enumerate the entire CouchDB cluster and get the list of databases (every app).
|
|
|
|
* NOTE: this operation is fine in self hosting, but cannot be used when hosting many
|
|
|
|
* different users/companies apps as there is no security around it - all apps are returned.
|
|
|
|
* @return {Promise<object[]>} returns the app information document stored in each app database.
|
|
|
|
*/
|
2021-08-06 18:14:31 +02:00
|
|
|
exports.getAllApps = async (CouchDB, { dev, all, idsOnly } = {}) => {
|
2021-08-05 10:59:08 +02:00
|
|
|
let tenantId = getTenantId()
|
|
|
|
if (!env.MULTI_TENANCY && !tenantId) {
|
|
|
|
tenantId = DEFAULT_TENANT_ID
|
|
|
|
}
|
2021-08-06 17:38:07 +02:00
|
|
|
let dbs = await exports.getAllDbs()
|
2021-08-06 18:14:31 +02:00
|
|
|
const appDbNames = dbs.filter(dbName => {
|
2021-08-05 10:59:08 +02:00
|
|
|
const split = dbName.split(SEPARATOR)
|
|
|
|
// it is an app, check the tenantId
|
|
|
|
if (split[0] === DocumentTypes.APP) {
|
|
|
|
const noTenantId = split.length === 2 || split[1] === DocumentTypes.DEV
|
|
|
|
// tenantId is always right before the UUID
|
|
|
|
const possibleTenantId = split[split.length - 2]
|
|
|
|
return (
|
|
|
|
(tenantId === DEFAULT_TENANT_ID && noTenantId) ||
|
|
|
|
possibleTenantId === tenantId
|
|
|
|
)
|
|
|
|
}
|
|
|
|
return false
|
|
|
|
})
|
2021-08-06 18:14:31 +02:00
|
|
|
if (idsOnly) {
|
|
|
|
return appDbNames
|
|
|
|
}
|
2021-05-17 22:43:50 +02:00
|
|
|
const appPromises = appDbNames.map(db =>
|
2021-06-08 19:06:16 +02:00
|
|
|
// skip setup otherwise databases could be re-created
|
|
|
|
new CouchDB(db, { skip_setup: true }).get(DocumentTypes.APP_METADATA)
|
2021-05-17 22:43:50 +02:00
|
|
|
)
|
2021-05-14 17:31:07 +02:00
|
|
|
if (appPromises.length === 0) {
|
|
|
|
return []
|
|
|
|
} else {
|
|
|
|
const response = await Promise.allSettled(appPromises)
|
|
|
|
const apps = response
|
2021-07-16 19:24:32 +02:00
|
|
|
.filter(result => result.status === "fulfilled")
|
2021-05-14 17:31:07 +02:00
|
|
|
.map(({ value }) => value)
|
2021-05-19 16:09:57 +02:00
|
|
|
if (!all) {
|
|
|
|
return apps.filter(app => {
|
|
|
|
if (dev) {
|
|
|
|
return isDevApp(app)
|
|
|
|
}
|
|
|
|
return !isDevApp(app)
|
|
|
|
})
|
|
|
|
} else {
|
|
|
|
return apps.map(app => ({
|
|
|
|
...app,
|
2021-05-19 16:55:00 +02:00
|
|
|
status: isDevApp(app) ? "development" : "published",
|
2021-05-19 16:09:57 +02:00
|
|
|
}))
|
|
|
|
}
|
2021-05-14 17:31:07 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-06-08 19:06:16 +02:00
|
|
|
exports.dbExists = async (CouchDB, dbName) => {
|
|
|
|
let exists = false
|
|
|
|
try {
|
2021-06-08 19:06:56 +02:00
|
|
|
const db = CouchDB(dbName, { skip_setup: true })
|
2021-06-08 19:06:16 +02:00
|
|
|
// check if database exists
|
|
|
|
const info = await db.info()
|
|
|
|
if (info && !info.error) {
|
|
|
|
exists = true
|
|
|
|
}
|
|
|
|
} catch (err) {
|
|
|
|
exists = false
|
|
|
|
}
|
|
|
|
return exists
|
|
|
|
}
|
|
|
|
|
2021-04-20 19:14:36 +02:00
|
|
|
/**
|
|
|
|
* Generates a new configuration ID.
|
|
|
|
* @returns {string} The new configuration ID which the config doc can be stored under.
|
|
|
|
*/
|
2021-08-05 10:59:08 +02:00
|
|
|
const generateConfigID = ({ type, workspace, user }) => {
|
|
|
|
const scope = [type, workspace, user].filter(Boolean).join(SEPARATOR)
|
2021-04-20 19:14:36 +02:00
|
|
|
|
2021-04-22 12:45:22 +02:00
|
|
|
return `${DocumentTypes.CONFIG}${SEPARATOR}${scope}`
|
2021-04-20 19:14:36 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Gets parameters for retrieving configurations.
|
|
|
|
*/
|
2021-08-05 10:59:08 +02:00
|
|
|
const getConfigParams = ({ type, workspace, user }, otherProps = {}) => {
|
|
|
|
const scope = [type, workspace, user].filter(Boolean).join(SEPARATOR)
|
2021-04-22 12:45:22 +02:00
|
|
|
|
2021-04-20 19:14:36 +02:00
|
|
|
return {
|
|
|
|
...otherProps,
|
2021-04-22 12:45:22 +02:00
|
|
|
startkey: `${DocumentTypes.CONFIG}${SEPARATOR}${scope}`,
|
|
|
|
endkey: `${DocumentTypes.CONFIG}${SEPARATOR}${scope}${UNICODE_MAX}`,
|
2021-04-20 19:14:36 +02:00
|
|
|
}
|
|
|
|
}
|
2021-04-22 14:46:54 +02:00
|
|
|
|
|
|
|
/**
|
2021-08-05 10:59:08 +02:00
|
|
|
* Returns the most granular configuration document from the DB based on the type, workspace and userID passed.
|
2021-04-22 15:53:19 +02:00
|
|
|
* @param {Object} db - db instance to query
|
2021-08-05 10:59:08 +02:00
|
|
|
* @param {Object} scopes - the type, workspace and userID scopes of the configuration.
|
2021-04-22 14:46:54 +02:00
|
|
|
* @returns The most granular configuration document based on the scope.
|
|
|
|
*/
|
2021-08-05 10:59:08 +02:00
|
|
|
const getScopedFullConfig = async function (db, { type, user, workspace }) {
|
2021-04-22 14:46:54 +02:00
|
|
|
const response = await db.allDocs(
|
|
|
|
getConfigParams(
|
2021-08-05 10:59:08 +02:00
|
|
|
{ type, user, workspace },
|
2021-04-22 14:46:54 +02:00
|
|
|
{
|
|
|
|
include_docs: true,
|
|
|
|
}
|
|
|
|
)
|
|
|
|
)
|
2021-05-04 18:31:06 +02:00
|
|
|
|
|
|
|
function determineScore(row) {
|
2021-04-22 15:07:00 +02:00
|
|
|
const config = row.doc
|
2021-04-22 14:46:54 +02:00
|
|
|
|
2021-08-05 10:59:08 +02:00
|
|
|
// Config is specific to a user and a workspace
|
|
|
|
if (config._id.includes(generateConfigID({ type, user, workspace }))) {
|
2021-05-04 18:31:06 +02:00
|
|
|
return 4
|
2021-04-22 15:07:00 +02:00
|
|
|
} else if (config._id.includes(generateConfigID({ type, user }))) {
|
|
|
|
// Config is specific to a user only
|
2021-05-04 18:31:06 +02:00
|
|
|
return 3
|
2021-08-05 10:59:08 +02:00
|
|
|
} else if (config._id.includes(generateConfigID({ type, workspace }))) {
|
|
|
|
// Config is specific to a workspace only
|
2021-05-04 18:31:06 +02:00
|
|
|
return 2
|
2021-04-22 15:07:00 +02:00
|
|
|
} else if (config._id.includes(generateConfigID({ type }))) {
|
|
|
|
// Config is specific to a type only
|
2021-05-04 18:31:06 +02:00
|
|
|
return 1
|
2021-04-22 14:46:54 +02:00
|
|
|
}
|
2021-05-04 18:31:06 +02:00
|
|
|
return 0
|
|
|
|
}
|
2021-04-22 14:46:54 +02:00
|
|
|
|
2021-04-22 15:07:00 +02:00
|
|
|
// Find the config with the most granular scope based on context
|
2021-05-04 19:14:13 +02:00
|
|
|
const scopedConfig = response.rows.sort(
|
|
|
|
(a, b) => determineScore(a) - determineScore(b)
|
|
|
|
)[0]
|
2021-04-22 15:07:00 +02:00
|
|
|
|
2021-05-05 21:58:31 +02:00
|
|
|
return scopedConfig && scopedConfig.doc
|
2021-04-22 14:46:54 +02:00
|
|
|
}
|
|
|
|
|
2021-05-06 11:51:21 +02:00
|
|
|
async function getScopedConfig(db, params) {
|
|
|
|
const configDoc = await getScopedFullConfig(db, params)
|
|
|
|
return configDoc && configDoc.config ? configDoc.config : configDoc
|
|
|
|
}
|
|
|
|
|
2021-05-13 12:06:08 +02:00
|
|
|
exports.Replication = Replication
|
2021-05-06 11:51:21 +02:00
|
|
|
exports.getScopedConfig = getScopedConfig
|
2021-04-22 14:46:54 +02:00
|
|
|
exports.generateConfigID = generateConfigID
|
|
|
|
exports.getConfigParams = getConfigParams
|
2021-05-06 11:51:21 +02:00
|
|
|
exports.getScopedFullConfig = getScopedFullConfig
|