Merge branch 'master' into fix/automation-layout-styling

This commit is contained in:
Peter Clement 2023-11-08 14:14:26 +00:00 committed by GitHub
commit 0cd5b1f431
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
32 changed files with 301 additions and 218 deletions

View File

@ -28,7 +28,7 @@ export enum ViewName {
APP_BACKUP_BY_TRIGGER = "by_trigger",
}
export const DeprecatedViews = {
export const DeprecatedViews: Record<string, string[]> = {
[ViewName.USER_BY_EMAIL]: [
// removed due to inaccuracy in view doc filter logic
"by_email",

View File

@ -175,12 +175,14 @@ export class DatabaseImpl implements Database {
return this.updateOutput(() => db.bulk({ docs: documents }))
}
async allDocs<T>(params: DatabaseQueryOpts): Promise<AllDocsResponse<T>> {
async allDocs<T extends Document>(
params: DatabaseQueryOpts
): Promise<AllDocsResponse<T>> {
const db = await this.checkSetup()
return this.updateOutput(() => db.list(params))
}
async query<T>(
async query<T extends Document>(
viewName: string,
params: DatabaseQueryOpts
): Promise<AllDocsResponse<T>> {

View File

@ -7,12 +7,19 @@ import {
} from "../constants"
import { getGlobalDB } from "../context"
import { doWithDB } from "./"
import { AllDocsResponse, Database, DatabaseQueryOpts } from "@budibase/types"
import {
AllDocsResponse,
Database,
DatabaseQueryOpts,
Document,
DesignDocument,
DBView,
} from "@budibase/types"
import env from "../environment"
const DESIGN_DB = "_design/database"
function DesignDoc() {
function DesignDoc(): DesignDocument {
return {
_id: DESIGN_DB,
// view collation information, read before writing any complex views:
@ -21,20 +28,14 @@ function DesignDoc() {
}
}
interface DesignDocument {
views: any
}
async function removeDeprecated(db: Database, viewName: ViewName) {
// @ts-ignore
if (!DeprecatedViews[viewName]) {
return
}
try {
const designDoc = await db.get<DesignDocument>(DESIGN_DB)
// @ts-ignore
for (let deprecatedNames of DeprecatedViews[viewName]) {
delete designDoc.views[deprecatedNames]
delete designDoc.views?.[deprecatedNames]
}
await db.put(designDoc)
} catch (err) {
@ -43,18 +44,18 @@ async function removeDeprecated(db: Database, viewName: ViewName) {
}
export async function createView(
db: any,
db: Database,
viewJs: string,
viewName: string
): Promise<void> {
let designDoc
try {
designDoc = (await db.get(DESIGN_DB)) as DesignDocument
designDoc = await db.get<DesignDocument>(DESIGN_DB)
} catch (err) {
// no design doc, make one
designDoc = DesignDoc()
}
const view = {
const view: DBView = {
map: viewJs,
}
designDoc.views = {
@ -109,7 +110,7 @@ export interface QueryViewOptions {
arrayResponse?: boolean
}
export async function queryViewRaw<T>(
export async function queryViewRaw<T extends Document>(
viewName: ViewName,
params: DatabaseQueryOpts,
db: Database,
@ -137,18 +138,16 @@ export async function queryViewRaw<T>(
}
}
export const queryView = async <T>(
export const queryView = async <T extends Document>(
viewName: ViewName,
params: DatabaseQueryOpts,
db: Database,
createFunc: any,
opts?: QueryViewOptions
): Promise<T[] | T | undefined> => {
): Promise<T[] | T> => {
const response = await queryViewRaw<T>(viewName, params, db, createFunc, opts)
const rows = response.rows
const docs = rows.map((row: any) =>
params.include_docs ? row.doc : row.value
)
const docs = rows.map(row => (params.include_docs ? row.doc! : row.value))
// if arrayResponse has been requested, always return array regardless of length
if (opts?.arrayResponse) {
@ -198,11 +197,11 @@ export const createPlatformUserView = async () => {
await createPlatformView(viewJs, ViewName.PLATFORM_USERS_LOWERCASE)
}
export const queryPlatformView = async <T>(
export const queryPlatformView = async <T extends Document>(
viewName: ViewName,
params: DatabaseQueryOpts,
opts?: QueryViewOptions
): Promise<T[] | T | undefined> => {
): Promise<T[] | T> => {
const CreateFuncByName: any = {
[ViewName.ACCOUNT_BY_EMAIL]: createPlatformAccountEmailView,
[ViewName.PLATFORM_USERS_LOWERCASE]: createPlatformUserView,
@ -220,7 +219,7 @@ const CreateFuncByName: any = {
[ViewName.USER_BY_APP]: createUserAppView,
}
export const queryGlobalView = async <T>(
export const queryGlobalView = async <T extends Document>(
viewName: ViewName,
params: DatabaseQueryOpts,
db?: Database,
@ -231,10 +230,10 @@ export const queryGlobalView = async <T>(
db = getGlobalDB()
}
const createFn = CreateFuncByName[viewName]
return queryView(viewName, params, db!, createFn, opts)
return queryView<T>(viewName, params, db!, createFn, opts)
}
export async function queryGlobalViewRaw<T>(
export async function queryGlobalViewRaw<T extends Document>(
viewName: ViewName,
params: DatabaseQueryOpts,
opts?: QueryViewOptions

View File

@ -413,15 +413,13 @@ export class UserDB {
}
// Get users and delete
const allDocsResponse: AllDocsResponse<User> = await db.allDocs({
const allDocsResponse = await db.allDocs<User>({
include_docs: true,
keys: userIds,
})
const usersToDelete: User[] = allDocsResponse.rows.map(
(user: RowResponse<User>) => {
return user.doc
}
)
const usersToDelete = allDocsResponse.rows.map(user => {
return user.doc!
})
// Delete from DB
const toDelete = usersToDelete.map(user => ({

View File

@ -151,7 +151,7 @@ export const searchGlobalUsersByApp = async (
include_docs: true,
})
params.startkey = opts && opts.startkey ? opts.startkey : params.startkey
let response = await queryGlobalView(ViewName.USER_BY_APP, params)
let response = await queryGlobalView<User>(ViewName.USER_BY_APP, params)
if (!response) {
response = []

View File

@ -1,7 +1,7 @@
import { EMPTY_LAYOUT } from "../../constants/layouts"
import { generateLayoutID, getScreenParams } from "../../db/utils"
import { events, context } from "@budibase/backend-core"
import { BBContext } from "@budibase/types"
import { BBContext, Layout } from "@budibase/types"
export async function save(ctx: BBContext) {
const db = context.getAppDB()
@ -30,12 +30,12 @@ export async function destroy(ctx: BBContext) {
layoutRev = ctx.params.layoutRev
const layoutsUsedByScreens = (
await db.allDocs(
await db.allDocs<Layout>(
getScreenParams(null, {
include_docs: true,
})
)
).rows.map(element => element.doc.layoutId)
).rows.map(element => element.doc!.layoutId)
if (layoutsUsedByScreens.includes(layoutId)) {
ctx.throw(400, "Cannot delete a layout that's being used by a screen")
}

View File

@ -25,12 +25,12 @@ const SUPPORTED_LEVELS = CURRENTLY_SUPPORTED_LEVELS
// utility function to stop this repetition - permissions always stored under roles
async function getAllDBRoles(db: Database) {
const body = await db.allDocs(
const body = await db.allDocs<Role>(
getRoleParams(null, {
include_docs: true,
})
)
return body.rows.map(row => row.doc)
return body.rows.map(row => row.doc!)
}
async function updatePermissionOnRole(
@ -79,7 +79,7 @@ async function updatePermissionOnRole(
) {
rolePermissions[resourceId] =
typeof rolePermissions[resourceId] === "string"
? [rolePermissions[resourceId]]
? [rolePermissions[resourceId] as unknown as string]
: []
}
// handle the removal/updating the role which has this permission first

View File

@ -6,7 +6,13 @@ import {
Header,
} from "@budibase/backend-core"
import { getUserMetadataParams, InternalTables } from "../../db/utils"
import { Database, Role, UserCtx, UserRoles } from "@budibase/types"
import {
Database,
Role,
UserCtx,
UserMetadata,
UserRoles,
} from "@budibase/types"
import { sdk as sharedSdk } from "@budibase/shared-core"
import sdk from "../../sdk"
@ -115,12 +121,12 @@ export async function destroy(ctx: UserCtx) {
const role = await db.get<Role>(roleId)
// first check no users actively attached to role
const users = (
await db.allDocs(
await db.allDocs<UserMetadata>(
getUserMetadataParams(undefined, {
include_docs: true,
})
)
).rows.map(row => row.doc)
).rows.map(row => row.doc!)
const usersWithRole = users.filter(user => user.roleId === roleId)
if (usersWithRole.length !== 0) {
ctx.throw(400, "Cannot delete role when it is in use.")

View File

@ -233,17 +233,16 @@ export async function fetchEnrichedRow(ctx: UserCtx) {
const tableId = utils.getTableId(ctx)
const rowId = ctx.params.rowId as string
// need table to work out where links go in row, as well as the link docs
let response = await Promise.all([
const [table, row, links] = await Promise.all([
sdk.tables.getTable(tableId),
utils.findRow(ctx, tableId, rowId),
linkRows.getLinkDocuments({ tableId, rowId, fieldName }),
])
const table = response[0] as Table
const row = response[1] as Row
const linkVals = response[2] as LinkDocumentValue[]
const linkVals = links as LinkDocumentValue[]
// look up the actual rows based on the ids
const params = getMultiIDParams(linkVals.map(linkVal => linkVal.id))
let linkedRows = (await db.allDocs(params)).rows.map(row => row.doc)
let linkedRows = (await db.allDocs<Row>(params)).rows.map(row => row.doc!)
// get the linked tables
const linkTableIds = getLinkedTableIDs(table as Table)

View File

@ -86,12 +86,12 @@ export async function updateAllFormulasInTable(table: Table) {
const db = context.getAppDB()
// start by getting the raw rows (which will be written back to DB after update)
let rows = (
await db.allDocs(
await db.allDocs<Row>(
getRowParams(table._id, null, {
include_docs: true,
})
)
).rows.map(row => row.doc)
).rows.map(row => row.doc!)
// now enrich the rows, note the clone so that we have the base state of the
// rows so that we don't write any of the enriched information back
let enrichedRows = await outputProcessing(table, cloneDeep(rows), {

View File

@ -333,29 +333,33 @@ export async function checkForViewUpdates(
columnRename?: RenameColumn
) {
const views = await getViews()
const tableViews = views.filter(view => view.meta.tableId === table._id)
const tableViews = views.filter(view => view.meta?.tableId === table._id)
// Check each table view to see if impacted by this table action
for (let view of tableViews) {
let needsUpdated = false
const viewMetadata = view.meta as any
if (!viewMetadata) {
continue
}
// First check for renames, otherwise check for deletions
if (columnRename) {
// Update calculation field if required
if (view.meta.field === columnRename.old) {
view.meta.field = columnRename.updated
if (viewMetadata.field === columnRename.old) {
viewMetadata.field = columnRename.updated
needsUpdated = true
}
// Update group by field if required
if (view.meta.groupBy === columnRename.old) {
view.meta.groupBy = columnRename.updated
if (viewMetadata.groupBy === columnRename.old) {
viewMetadata.groupBy = columnRename.updated
needsUpdated = true
}
// Update filters if required
if (view.meta.filters) {
view.meta.filters.forEach((filter: any) => {
if (viewMetadata.filters) {
viewMetadata.filters.forEach((filter: any) => {
if (filter.key === columnRename.old) {
filter.key = columnRename.updated
needsUpdated = true
@ -365,26 +369,26 @@ export async function checkForViewUpdates(
} else if (deletedColumns) {
deletedColumns.forEach((column: string) => {
// Remove calculation statement if required
if (view.meta.field === column) {
delete view.meta.field
delete view.meta.calculation
delete view.meta.groupBy
if (viewMetadata.field === column) {
delete viewMetadata.field
delete viewMetadata.calculation
delete viewMetadata.groupBy
needsUpdated = true
}
// Remove group by field if required
if (view.meta.groupBy === column) {
delete view.meta.groupBy
if (viewMetadata.groupBy === column) {
delete viewMetadata.groupBy
needsUpdated = true
}
// Remove filters referencing deleted field if required
if (view.meta.filters && view.meta.filters.length) {
const initialLength = view.meta.filters.length
view.meta.filters = view.meta.filters.filter((filter: any) => {
if (viewMetadata.filters && viewMetadata.filters.length) {
const initialLength = viewMetadata.filters.length
viewMetadata.filters = viewMetadata.filters.filter((filter: any) => {
return filter.key !== column
})
if (initialLength !== view.meta.filters.length) {
if (initialLength !== viewMetadata.filters.length) {
needsUpdated = true
}
}
@ -397,15 +401,16 @@ export async function checkForViewUpdates(
(field: any) => field.name == view.groupBy
)
const newViewTemplate = viewTemplate(
view.meta,
viewMetadata,
groupByField?.type === FieldTypes.ARRAY
)
await saveView(null, view.name, newViewTemplate)
if (!newViewTemplate.meta.schema) {
newViewTemplate.meta.schema = table.schema
const viewName = view.name!
await saveView(null, viewName, newViewTemplate)
if (!newViewTemplate.meta?.schema) {
newViewTemplate.meta!.schema = table.schema
}
if (table.views?.[view.name]) {
table.views[view.name] = newViewTemplate.meta as View
if (table.views?.[viewName]) {
table.views[viewName] = newViewTemplate.meta as View
}
}
}

View File

@ -8,13 +8,13 @@ import {
import env from "../../../environment"
import { context } from "@budibase/backend-core"
import viewBuilder from "./viewBuilder"
import { Database } from "@budibase/types"
import { Database, DBView, DesignDocument, InMemoryView } from "@budibase/types"
export async function getView(viewName: string) {
const db = context.getAppDB()
if (env.SELF_HOSTED) {
const designDoc = await db.get<any>("_design/database")
return designDoc.views[viewName]
const designDoc = await db.get<DesignDocument>("_design/database")
return designDoc.views?.[viewName]
} else {
// This is a table view, don't read the view from the DB
if (viewName.startsWith(DocumentType.TABLE + SEPARATOR)) {
@ -22,7 +22,7 @@ export async function getView(viewName: string) {
}
try {
const viewDoc = await db.get<any>(generateMemoryViewID(viewName))
const viewDoc = await db.get<InMemoryView>(generateMemoryViewID(viewName))
return viewDoc.view
} catch (err: any) {
// Return null when PouchDB doesn't found the view
@ -35,30 +35,33 @@ export async function getView(viewName: string) {
}
}
export async function getViews() {
export async function getViews(): Promise<DBView[]> {
const db = context.getAppDB()
const response = []
const response: DBView[] = []
if (env.SELF_HOSTED) {
const designDoc = await db.get<any>("_design/database")
for (let name of Object.keys(designDoc.views)) {
const designDoc = await db.get<DesignDocument>("_design/database")
for (let name of Object.keys(designDoc.views || {})) {
// Only return custom views, not built ins
const viewNames = Object.values(ViewName) as string[]
if (viewNames.indexOf(name) !== -1) {
continue
}
response.push({
name,
...designDoc.views[name],
})
const view = designDoc.views?.[name]
if (view) {
response.push({
name,
...view,
})
}
}
} else {
const views = (
await db.allDocs(
await db.allDocs<InMemoryView>(
getMemoryViewParams({
include_docs: true,
})
)
).rows.map(row => row.doc)
).rows.map(row => row.doc!)
for (let viewDoc of views) {
response.push({
name: viewDoc.name,
@ -72,11 +75,11 @@ export async function getViews() {
export async function saveView(
originalName: string | null,
viewName: string,
viewTemplate: any
viewTemplate: DBView
) {
const db = context.getAppDB()
if (env.SELF_HOSTED) {
const designDoc = await db.get<any>("_design/database")
const designDoc = await db.get<DesignDocument>("_design/database")
designDoc.views = {
...designDoc.views,
[viewName]: viewTemplate,
@ -89,17 +92,17 @@ export async function saveView(
} else {
const id = generateMemoryViewID(viewName)
const originalId = originalName ? generateMemoryViewID(originalName) : null
const viewDoc: any = {
const viewDoc: InMemoryView = {
_id: id,
view: viewTemplate,
name: viewName,
tableId: viewTemplate.meta.tableId,
tableId: viewTemplate.meta!.tableId,
}
try {
const old = await db.get<any>(id)
const old = await db.get<InMemoryView>(id)
if (originalId) {
const originalDoc = await db.get<any>(originalId)
await db.remove(originalDoc._id, originalDoc._rev)
const originalDoc = await db.get<InMemoryView>(originalId)
await db.remove(originalDoc._id!, originalDoc._rev)
}
if (old && old._rev) {
viewDoc._rev = old._rev
@ -114,52 +117,65 @@ export async function saveView(
export async function deleteView(viewName: string) {
const db = context.getAppDB()
if (env.SELF_HOSTED) {
const designDoc = await db.get<any>("_design/database")
const view = designDoc.views[viewName]
delete designDoc.views[viewName]
const designDoc = await db.get<DesignDocument>("_design/database")
const view = designDoc.views?.[viewName]
delete designDoc.views?.[viewName]
await db.put(designDoc)
return view
} else {
const id = generateMemoryViewID(viewName)
const viewDoc = await db.get<any>(id)
await db.remove(viewDoc._id, viewDoc._rev)
const viewDoc = await db.get<InMemoryView>(id)
await db.remove(viewDoc._id!, viewDoc._rev)
return viewDoc.view
}
}
export async function migrateToInMemoryView(db: Database, viewName: string) {
// delete the view initially
const designDoc = await db.get<any>("_design/database")
const designDoc = await db.get<DesignDocument>("_design/database")
const meta = designDoc.views?.[viewName].meta
if (!meta) {
throw new Error("Unable to migrate view - no metadata")
}
// run the view back through the view builder to update it
const view = viewBuilder(designDoc.views[viewName].meta)
delete designDoc.views[viewName]
const view = viewBuilder(meta)
delete designDoc.views?.[viewName]
await db.put(designDoc)
await exports.saveView(db, null, viewName, view)
await saveView(null, viewName, view)
}
export async function migrateToDesignView(db: Database, viewName: string) {
let view = await db.get<any>(generateMemoryViewID(viewName))
const designDoc = await db.get<any>("_design/database")
designDoc.views[viewName] = viewBuilder(view.view.meta)
let view = await db.get<InMemoryView>(generateMemoryViewID(viewName))
const designDoc = await db.get<DesignDocument>("_design/database")
const meta = view.view.meta
if (!meta) {
throw new Error("Unable to migrate view - no metadata")
}
if (!designDoc.views) {
designDoc.views = {}
}
designDoc.views[viewName] = viewBuilder(meta)
await db.put(designDoc)
await db.remove(view._id, view._rev)
await db.remove(view._id!, view._rev)
}
export async function getFromDesignDoc(db: Database, viewName: string) {
const designDoc = await db.get<any>("_design/database")
let view = designDoc.views[viewName]
const designDoc = await db.get<DesignDocument>("_design/database")
let view = designDoc.views?.[viewName]
if (view == null) {
throw { status: 404, message: "Unable to get view" }
}
return view
}
export async function getFromMemoryDoc(db: Database, viewName: string) {
let view = await db.get<any>(generateMemoryViewID(viewName))
export async function getFromMemoryDoc(
db: Database,
viewName: string
): Promise<DBView> {
let view = await db.get<InMemoryView>(generateMemoryViewID(viewName))
if (view) {
view = view.view
return view.view
} else {
throw { status: 404, message: "Unable to get view" }
}
return view
}

View File

@ -1,13 +1,4 @@
import { ViewFilter } from "@budibase/types"
type ViewTemplateOpts = {
field: string
tableId: string
groupBy: string
filters: ViewFilter[]
calculation: string
groupByMulti: boolean
}
import { ViewFilter, ViewTemplateOpts, DBView } from "@budibase/types"
const TOKEN_MAP: Record<string, string> = {
EQUALS: "===",
@ -146,7 +137,7 @@ function parseEmitExpression(field: string, groupBy: string) {
export default function (
{ field, tableId, groupBy, filters = [], calculation }: ViewTemplateOpts,
groupByMulti?: boolean
) {
): DBView {
// first filter can't have a conjunction
if (filters && filters.length > 0 && filters[0].conjunction) {
delete filters[0].conjunction

View File

@ -47,8 +47,11 @@ export async function save(ctx: Ctx) {
// add views to table document
if (!table.views) table.views = {}
if (!view.meta.schema) {
view.meta.schema = table.schema
if (!view.meta?.schema) {
view.meta = {
...view.meta!,
schema: table.schema,
}
}
table.views[viewName] = { ...view.meta, name: viewName }
if (originalName) {
@ -125,10 +128,13 @@ export async function destroy(ctx: Ctx) {
const db = context.getAppDB()
const viewName = decodeURIComponent(ctx.params.viewName)
const view = await deleteView(viewName)
if (!view || !view.meta) {
ctx.throw(400, "Unable to delete view - no metadata/view not found.")
}
const table = await sdk.tables.getTable(view.meta.tableId)
delete table.views![viewName]
await db.put(table)
await events.view.deleted(view)
await events.view.deleted(view as View)
ctx.body = view
builderSocket?.emitTableUpdate(ctx, table)
@ -147,7 +153,7 @@ export async function exportView(ctx: Ctx) {
)
}
if (view) {
if (view && view.meta) {
ctx.params.viewName = viewName
// Fetch view rows
ctx.query = {

View File

@ -20,10 +20,10 @@ const JOB_OPTS = {
async function getAllAutomations() {
const db = context.getAppDB()
let automations = await db.allDocs(
let automations = await db.allDocs<Automation>(
getAutomationParams(null, { include_docs: true })
)
return automations.rows.map(row => row.doc)
return automations.rows.map(row => row.doc!)
}
async function queueRelevantRowAutomations(
@ -45,19 +45,19 @@ async function queueRelevantRowAutomations(
for (let automation of automations) {
let automationDef = automation.definition
let automationTrigger = automationDef ? automationDef.trigger : {}
let automationTrigger = automationDef?.trigger
// don't queue events which are for dev apps, only way to test automations is
// running tests on them, in production the test flag will never
// be checked due to lazy evaluation (first always false)
if (
!env.ALLOW_DEV_AUTOMATIONS &&
isDevAppID(event.appId) &&
!(await checkTestFlag(automation._id))
!(await checkTestFlag(automation._id!))
) {
continue
}
if (
automationTrigger.inputs &&
automationTrigger?.inputs &&
automationTrigger.inputs.tableId === event.row.tableId
) {
await automationQueue.add({ automation, event }, JOB_OPTS)

View File

@ -1,5 +1,5 @@
import newid from "./newid"
import { Row, View, Document } from "@budibase/types"
import { Row, Document, DBView } from "@budibase/types"
// bypass the main application db config
// use in memory pouchdb directly
@ -7,7 +7,7 @@ import { db as dbCore } from "@budibase/backend-core"
const Pouch = dbCore.getPouch({ inMemory: true })
export async function runView(
view: View,
view: DBView,
calculation: string,
group: boolean,
data: Row[]

View File

@ -14,7 +14,14 @@ import partition from "lodash/partition"
import { getGlobalUsersFromMetadata } from "../../utilities/global"
import { processFormulas } from "../../utilities/rowProcessor"
import { context } from "@budibase/backend-core"
import { Table, Row, LinkDocumentValue, FieldType } from "@budibase/types"
import {
Table,
Row,
LinkDocumentValue,
FieldType,
LinkDocument,
ContextUser,
} from "@budibase/types"
import sdk from "../../sdk"
export { IncludeDocs, getLinkDocuments, createLinkView } from "./linkUtils"
@ -73,18 +80,18 @@ async function getFullLinkedDocs(links: LinkDocumentValue[]) {
const db = context.getAppDB()
const linkedRowIds = links.map(link => link.id)
const uniqueRowIds = [...new Set(linkedRowIds)]
let dbRows = (await db.allDocs(getMultiIDParams(uniqueRowIds))).rows.map(
row => row.doc
let dbRows = (await db.allDocs<Row>(getMultiIDParams(uniqueRowIds))).rows.map(
row => row.doc!
)
// convert the unique db rows back to a full list of linked rows
const linked = linkedRowIds
.map(id => dbRows.find(row => row && row._id === id))
.filter(row => row != null)
.filter(row => row != null) as Row[]
// need to handle users as specific cases
let [users, other] = partition(linked, linkRow =>
linkRow._id.startsWith(USER_METDATA_PREFIX)
linkRow._id!.startsWith(USER_METDATA_PREFIX)
)
users = await getGlobalUsersFromMetadata(users)
users = await getGlobalUsersFromMetadata(users as ContextUser[])
return [...other, ...users]
}
@ -176,7 +183,7 @@ export async function attachFullLinkedDocs(
// clear any existing links that could be dupe'd
rows = clearRelationshipFields(table, rows)
// now get the docs and combine into the rows
let linked = []
let linked: Row[] = []
if (linksWithoutFromRow.length > 0) {
linked = await getFullLinkedDocs(linksWithoutFromRow)
}
@ -189,7 +196,7 @@ export async function attachFullLinkedDocs(
if (opts?.fromRow && opts?.fromRow?._id === link.id) {
linkedRow = opts.fromRow!
} else {
linkedRow = linked.find(row => row._id === link.id)
linkedRow = linked.find(row => row._id === link.id)!
}
if (linkedRow) {
const linkedTableId =

View File

@ -91,7 +91,7 @@ async function getImportableDocuments(db: Database) {
// map the responses to the document itself
let documents: Document[] = []
for (let response of await Promise.all(docPromises)) {
documents = documents.concat(response.rows.map(row => row.doc))
documents = documents.concat(response.rows.map(row => row.doc!))
}
// remove the _rev, stops it being written
documents.forEach(doc => {

View File

@ -3,7 +3,11 @@ import { db as dbCore, context, logging, roles } from "@budibase/backend-core"
import { User, ContextUser, UserGroup } from "@budibase/types"
import { sdk as proSdk } from "@budibase/pro"
import sdk from "../../"
import { getGlobalUsers, processUser } from "../../../utilities/global"
import {
getGlobalUsers,
getRawGlobalUsers,
processUser,
} from "../../../utilities/global"
import { generateUserMetadataID, InternalTables } from "../../../db/utils"
type DeletedUser = { _id: string; deleted: boolean }
@ -77,9 +81,7 @@ async function syncUsersToApp(
export async function syncUsersToAllApps(userIds: string[]) {
// list of users, if one has been deleted it will be undefined in array
const users = (await getGlobalUsers(userIds, {
noProcessing: true,
})) as User[]
const users = await getRawGlobalUsers(userIds)
const groups = await proSdk.groups.fetch()
const finalUsers: (User | DeletedUser)[] = []
for (let userId of userIds) {

View File

@ -51,12 +51,12 @@ export async function fetch(opts?: {
// Get external datasources
const datasources = (
await db.allDocs(
await db.allDocs<Datasource>(
getDatasourceParams(null, {
include_docs: true,
})
)
).rows.map(row => row.doc)
).rows.map(row => row.doc!)
const allDatasources: Datasource[] = await sdk.datasources.removeSecrets([
bbInternalDb,
@ -271,5 +271,5 @@ export async function getExternalDatasources(): Promise<Datasource[]> {
})
)
return externalDatasources.rows.map(r => r.doc)
return externalDatasources.rows.map(r => r.doc!)
}

View File

@ -185,8 +185,8 @@ export async function fetchView(
group: !!group,
})
} else {
const tableId = viewInfo.meta.tableId
const data = await fetchRaw(tableId)
const tableId = viewInfo.meta!.tableId
const data = await fetchRaw(tableId!)
response = await inMemoryViews.runView(
viewInfo,
calculation as string,
@ -200,7 +200,7 @@ export async function fetchView(
response.rows = response.rows.map(row => row.doc)
let table: Table
try {
table = await sdk.tables.getTable(viewInfo.meta.tableId)
table = await sdk.tables.getTable(viewInfo.meta!.tableId)
} catch (err) {
throw new Error("Unable to retrieve view table.")
}

View File

@ -48,7 +48,7 @@ export async function getAllInternalTables(db?: Database): Promise<Table[]> {
if (!db) {
db = context.getAppDB()
}
const internalTables = await db.allDocs<Table[]>(
const internalTables = await db.allDocs<Table>(
getTableParams(null, {
include_docs: true,
})
@ -124,7 +124,7 @@ export async function getTables(tableIds: string[]): Promise<Table[]> {
}
if (internalTableIds.length) {
const db = context.getAppDB()
const internalTableDocs = await db.allDocs<Table[]>(
const internalTableDocs = await db.allDocs<Table>(
getMultiIDParams(internalTableIds)
)
tables = tables.concat(internalTableDocs.rows.map(row => row.doc!))

View File

@ -7,12 +7,17 @@ import {
InternalTables,
} from "../../db/utils"
import isEqual from "lodash/isEqual"
import { ContextUser, UserMetadata, User, Database } from "@budibase/types"
import {
ContextUser,
UserMetadata,
Database,
ContextUserMetadata,
} from "@budibase/types"
export function combineMetadataAndUser(
user: ContextUser,
metadata: UserMetadata | UserMetadata[]
) {
): ContextUserMetadata | null {
const metadataId = generateUserMetadataID(user._id!)
const found = Array.isArray(metadata)
? metadata.find(doc => doc._id === metadataId)
@ -51,33 +56,33 @@ export function combineMetadataAndUser(
return null
}
export async function rawUserMetadata(db?: Database) {
export async function rawUserMetadata(db?: Database): Promise<UserMetadata[]> {
if (!db) {
db = context.getAppDB()
}
return (
await db.allDocs(
await db.allDocs<UserMetadata>(
getUserMetadataParams(null, {
include_docs: true,
})
)
).rows.map(row => row.doc)
).rows.map(row => row.doc!)
}
export async function fetchMetadata() {
export async function fetchMetadata(): Promise<ContextUserMetadata[]> {
const global = await getGlobalUsers()
const metadata = await rawUserMetadata()
const users = []
const users: ContextUserMetadata[] = []
for (let user of global) {
// find the metadata that matches up to the global ID
const info = metadata.find(meta => meta._id.includes(user._id))
const info = metadata.find(meta => meta._id!.includes(user._id!))
// remove these props, not for the correct DB
users.push({
...user,
...info,
tableId: InternalTables.USER_METADATA,
// make sure the ID is always a local ID, not a global one
_id: generateUserMetadataID(user._id),
_id: generateUserMetadataID(user._id!),
})
}
return users
@ -90,9 +95,10 @@ export async function syncGlobalUsers() {
if (!(await db.exists())) {
continue
}
const resp = await Promise.all([getGlobalUsers(), rawUserMetadata(db)])
const users = resp[0] as User[]
const metadata = resp[1] as UserMetadata[]
const [users, metadata] = await Promise.all([
getGlobalUsers(),
rawUserMetadata(db),
])
const toWrite = []
for (let user of users) {
const combined = combineMetadataAndUser(user, metadata)

View File

@ -71,69 +71,67 @@ export async function processUser(
return user
}
export async function getCachedSelf(ctx: UserCtx, appId: string) {
export async function getCachedSelf(
ctx: UserCtx,
appId: string
): Promise<ContextUser> {
// this has to be tenant aware, can't depend on the context to find it out
// running some middlewares before the tenancy causes context to break
const user = await cache.user.getUser(ctx.user?._id!)
return processUser(user, { appId })
}
export async function getRawGlobalUser(userId: string) {
export async function getRawGlobalUser(userId: string): Promise<User> {
const db = tenancy.getGlobalDB()
return db.get<User>(getGlobalIDFromUserMetadataID(userId))
}
export async function getGlobalUser(userId: string) {
export async function getGlobalUser(userId: string): Promise<ContextUser> {
const appId = context.getAppId()
let user = await getRawGlobalUser(userId)
return processUser(user, { appId })
}
export async function getGlobalUsers(
userIds?: string[],
opts?: { noProcessing?: boolean }
) {
const appId = context.getAppId()
export async function getRawGlobalUsers(userIds?: string[]): Promise<User[]> {
const db = tenancy.getGlobalDB()
let globalUsers
let globalUsers: User[]
if (userIds) {
globalUsers = (await db.allDocs(getMultiIDParams(userIds))).rows.map(
row => row.doc
globalUsers = (await db.allDocs<User>(getMultiIDParams(userIds))).rows.map(
row => row.doc!
)
} else {
globalUsers = (
await db.allDocs(
await db.allDocs<User>(
dbCore.getGlobalUserParams(null, {
include_docs: true,
})
)
).rows.map(row => row.doc)
).rows.map(row => row.doc!)
}
globalUsers = globalUsers
return globalUsers
.filter(user => user != null)
.map(user => {
delete user.password
delete user.forceResetPassword
return user
})
}
if (opts?.noProcessing || !appId) {
return globalUsers
} else {
// pass in the groups, meaning we don't actually need to retrieve them for
// each user individually
const allGroups = await groups.fetch()
return Promise.all(
globalUsers.map(user => processUser(user, { groups: allGroups }))
)
}
export async function getGlobalUsers(
userIds?: string[]
): Promise<ContextUser[]> {
const users = await getRawGlobalUsers(userIds)
const allGroups = await groups.fetch()
return Promise.all(
users.map(user => processUser(user, { groups: allGroups }))
)
}
export async function getGlobalUsersFromMetadata(users: ContextUser[]) {
const globalUsers = await getGlobalUsers(users.map(user => user._id!))
return users.map(user => {
const globalUser = globalUsers.find(
globalUser => globalUser && user._id?.includes(globalUser._id)
globalUser => globalUser && user._id?.includes(globalUser._id!)
)
return {
...globalUser,

View File

@ -1,9 +1,9 @@
import { createRoutingView } from "../../db/views/staticViews"
import { ViewName, getQueryIndex, UNICODE_MAX } from "../../db/utils"
import { context } from "@budibase/backend-core"
import { ScreenRouting } from "@budibase/types"
import { ScreenRouting, Document } from "@budibase/types"
type ScreenRoutesView = {
interface ScreenRoutesView extends Document {
id: string
routing: ScreenRouting
}

View File

@ -2,4 +2,5 @@ import { Document } from "../document"
export interface Layout extends Document {
props: any
layoutId?: string
}

View File

@ -1,6 +1,6 @@
import { Document } from "../document"
import { User } from "../global"
import { Row } from "./row"
import { ContextUser } from "../../sdk"
export interface UserMetadata extends Document {
roleId: string
email?: string
}
export type UserMetadata = User & Row
export type ContextUserMetadata = ContextUser & Row

View File

@ -1,5 +1,24 @@
import { SearchFilter, SortOrder, SortType } from "../../api"
import { UIFieldMetadata } from "./table"
import { Document } from "../document"
import { DBView } from "../../sdk"
export type ViewTemplateOpts = {
field: string
tableId: string
groupBy: string
filters: ViewFilter[]
schema: any
calculation: string
groupByMulti?: boolean
}
export interface InMemoryView extends Document {
view: DBView
name: string
tableId: string
groupBy?: string
}
export interface View {
name?: string
@ -10,7 +29,7 @@ export interface View {
calculation?: ViewCalculation
map?: string
reduce?: any
meta?: Record<string, any>
meta?: ViewTemplateOpts
}
export interface ViewV2 {

View File

@ -1,17 +1,19 @@
import { Document } from "../"
export interface RowValue {
rev: string
deleted: boolean
}
export interface RowResponse<T> {
export interface RowResponse<T extends Document> {
id: string
key: string
error: string
value: T | RowValue
doc?: T | any
doc?: T
}
export interface AllDocsResponse<T> {
export interface AllDocsResponse<T extends Document> {
offset: number
total_rows: number
rows: RowResponse<T>[]

View File

@ -1,5 +1,5 @@
import Nano from "@budibase/nano"
import { AllDocsResponse, AnyDocument, Document } from "../"
import { AllDocsResponse, AnyDocument, Document, ViewTemplateOpts } from "../"
import { Writable } from "stream"
export enum SearchIndex {
@ -20,6 +20,37 @@ export enum SortOption {
DESCENDING = "desc",
}
export type IndexAnalyzer = {
name: string
default?: string
fields?: Record<string, string>
}
export type DBView = {
name?: string
map: string
reduce?: string
meta?: ViewTemplateOpts
groupBy?: string
}
export interface DesignDocument extends Document {
// we use this static reference for all design documents
_id: "_design/database"
language?: string
// CouchDB views
views?: {
[viewName: string]: DBView
}
// Lucene indexes
indexes?: {
[indexName: string]: {
index: string
analyzer?: string | IndexAnalyzer
}
}
}
export type CouchFindOptions = {
selector: PouchDB.Find.Selector
fields?: string[]
@ -101,8 +132,10 @@ export interface Database {
opts?: DatabasePutOpts
): Promise<Nano.DocumentInsertResponse>
bulkDocs(documents: AnyDocument[]): Promise<Nano.DocumentBulkResponse[]>
allDocs<T>(params: DatabaseQueryOpts): Promise<AllDocsResponse<T>>
query<T>(
allDocs<T extends Document>(
params: DatabaseQueryOpts
): Promise<AllDocsResponse<T>>
query<T extends Document>(
viewName: string,
params: DatabaseQueryOpts
): Promise<AllDocsResponse<T>>

View File

@ -56,12 +56,12 @@ export async function getTemplates({
id,
}: { ownerId?: string; type?: string; id?: string } = {}) {
const db = tenancy.getGlobalDB()
const response = await db.allDocs(
const response = await db.allDocs<Template>(
dbCore.getTemplateParams(ownerId || GLOBAL_OWNER, id, {
include_docs: true,
})
)
let templates = response.rows.map(row => row.doc)
let templates = response.rows.map(row => row.doc!)
// should only be one template with ID
if (id) {
return templates[0]
@ -73,6 +73,6 @@ export async function getTemplates({
}
export async function getTemplateByPurpose(type: string, purpose: string) {
const templates = await getTemplates({ type })
const templates = (await getTemplates({ type })) as Template[]
return templates.find((template: Template) => template.purpose === purpose)
}

View File

@ -99,8 +99,6 @@ async function buildEmail(
if (!base || !body || !core) {
throw "Unable to build email, missing base components"
}
base = base.contents
body = body.contents
let name = user ? user.name : undefined
if (user && !name && user.firstName) {
@ -114,15 +112,10 @@ async function buildEmail(
user: user || {},
}
// Prepend the core template
const fullBody = core + body
body = await processString(fullBody, context)
// this should now be the core email HTML
return processString(base, {
return processString(base.contents, {
...context,
body,
body: await processString(core + body?.contents, context),
})
}