Merge branch 'master' into BUDI-7580/account_portal_submodule

This commit is contained in:
Adria Navarro 2023-11-08 17:05:42 +01:00 committed by GitHub
commit 6fd8a58850
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
68 changed files with 923 additions and 604 deletions

View File

@ -2,8 +2,8 @@
echo ${TARGETBUILD} > /buildtarget.txt echo ${TARGETBUILD} > /buildtarget.txt
if [[ "${TARGETBUILD}" = "aas" ]]; then if [[ "${TARGETBUILD}" = "aas" ]]; then
# Azure AppService uses /home for persisent data & SSH on port 2222 # Azure AppService uses /home for persistent data & SSH on port 2222
DATA_DIR=/home DATA_DIR="${DATA_DIR:-/home}"
WEBSITES_ENABLE_APP_SERVICE_STORAGE=true WEBSITES_ENABLE_APP_SERVICE_STORAGE=true
mkdir -p $DATA_DIR/{search,minio,couch} mkdir -p $DATA_DIR/{search,minio,couch}
mkdir -p $DATA_DIR/couch/{dbs,views} mkdir -p $DATA_DIR/couch/{dbs,views}

View File

@ -2,8 +2,8 @@
echo ${TARGETBUILD} > /buildtarget.txt echo ${TARGETBUILD} > /buildtarget.txt
if [[ "${TARGETBUILD}" = "aas" ]]; then if [[ "${TARGETBUILD}" = "aas" ]]; then
# Azure AppService uses /home for persisent data & SSH on port 2222 # Azure AppService uses /home for persistent data & SSH on port 2222
DATA_DIR=/home DATA_DIR="${DATA_DIR:-/home}"
WEBSITES_ENABLE_APP_SERVICE_STORAGE=true WEBSITES_ENABLE_APP_SERVICE_STORAGE=true
mkdir -p $DATA_DIR/{search,minio,couch} mkdir -p $DATA_DIR/{search,minio,couch}
mkdir -p $DATA_DIR/couch/{dbs,views} mkdir -p $DATA_DIR/couch/{dbs,views}

View File

@ -22,7 +22,7 @@ declare -a DOCKER_VARS=("APP_PORT" "APPS_URL" "ARCHITECTURE" "BUDIBASE_ENVIRONME
# Azure App Service customisations # Azure App Service customisations
if [[ "${TARGETBUILD}" = "aas" ]]; then if [[ "${TARGETBUILD}" = "aas" ]]; then
DATA_DIR=/home DATA_DIR="${DATA_DIR:-/home}"
WEBSITES_ENABLE_APP_SERVICE_STORAGE=true WEBSITES_ENABLE_APP_SERVICE_STORAGE=true
/etc/init.d/ssh start /etc/init.d/ssh start
else else

View File

@ -1,5 +1,5 @@
{ {
"version": "2.13.3", "version": "2.13.5",
"npmClient": "yarn", "npmClient": "yarn",
"packages": [ "packages": [
"packages/*", "packages/*",

View File

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

View File

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

View File

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

View File

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

View File

@ -151,7 +151,7 @@ export const searchGlobalUsersByApp = async (
include_docs: true, include_docs: true,
}) })
params.startkey = opts && opts.startkey ? opts.startkey : params.startkey 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) { if (!response) {
response = [] response = []

View File

@ -188,4 +188,17 @@ describe("utils", () => {
expectResult(false) expectResult(false)
}) })
}) })
describe("hasCircularStructure", () => {
it("should detect a circular structure", () => {
const a: any = { b: "b" }
const b = { a }
a.b = b
expect(utils.hasCircularStructure(b)).toBe(true)
})
it("should allow none circular structures", () => {
expect(utils.hasCircularStructure({ a: "b" })).toBe(false)
})
})
}) })

View File

@ -237,3 +237,17 @@ export function timeout(timeMs: number) {
export function isAudited(event: Event) { export function isAudited(event: Event) {
return !!AuditedEventFriendlyName[event] return !!AuditedEventFriendlyName[event]
} }
export function hasCircularStructure(json: any) {
if (typeof json !== "object") {
return false
}
try {
JSON.stringify(json)
} catch (err) {
if (err instanceof Error && err?.message.includes("circular structure")) {
return true
}
}
return false
}

View File

@ -48,15 +48,14 @@
<UndoRedoControl store={automationHistoryStore} /> <UndoRedoControl store={automationHistoryStore} />
</div> </div>
<div class="controls"> <div class="controls">
<div class="buttons"> <div
on:click={() => {
testDataModal.show()
}}
class="buttons"
>
<Icon hoverable size="M" name="Play" /> <Icon hoverable size="M" name="Play" />
<div <div>Run test</div>
on:click={() => {
testDataModal.show()
}}
>
Run test
</div>
</div> </div>
<div class="buttons"> <div class="buttons">
<Icon <Icon

View File

@ -13,13 +13,13 @@
export let idx export let idx
export let addLooping export let addLooping
export let deleteStep export let deleteStep
export let enableNaming = true
let validRegex = /^[A-Za-z0-9_\s]+$/ let validRegex = /^[A-Za-z0-9_\s]+$/
let typing = false let typing = false
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
$: stepNames = $selectedAutomation.definition.stepNames $: stepNames = $selectedAutomation?.definition.stepNames
$: automationName = stepNames?.[block.id] || block?.name || "" $: automationName = stepNames?.[block.id] || block?.name || ""
$: automationNameError = getAutomationNameError(automationName) $: automationNameError = getAutomationNameError(automationName)
$: status = updateStatus(testResult, isTrigger) $: status = updateStatus(testResult, isTrigger)
@ -32,7 +32,7 @@
)?.[0] )?.[0]
} }
} }
$: loopBlock = $selectedAutomation.definition.steps.find( $: loopBlock = $selectedAutomation?.definition.steps.find(
x => x.blockToLoop === block?.id x => x.blockToLoop === block?.id
) )
@ -126,24 +126,33 @@
<Body size="XS"><b>Step {idx}</b></Body> <Body size="XS"><b>Step {idx}</b></Body>
</div> </div>
{/if} {/if}
<input
placeholder="Enter some text" {#if enableNaming}
name="name" <input
autocomplete="off" class="input-text"
value={automationName} disabled={!enableNaming}
on:input={e => { placeholder="Enter some text"
automationName = e.target.value.trim() name="name"
}} autocomplete="off"
on:click={startTyping} value={automationName}
on:blur={async () => { on:input={e => {
typing = false automationName = e.target.value.trim()
if (automationNameError) { }}
automationName = stepNames[block.id] || block?.name on:click={startTyping}
} else { on:blur={async () => {
await saveName() typing = false
} if (automationNameError) {
}} automationName = stepNames[block.id] || block?.name
/> } else {
await saveName()
}
}}
/>
{:else}
<div class="input-text">
{automationName}
</div>
{/if}
</div> </div>
</div> </div>
<div class="blockTitle"> <div class="blockTitle">
@ -178,9 +187,11 @@
<Icon on:click={addLooping} hoverable name="RotateCW" /> <Icon on:click={addLooping} hoverable name="RotateCW" />
</AbsTooltip> </AbsTooltip>
{/if} {/if}
<AbsTooltip type="negative" text="Delete step"> {#if !isHeaderTrigger}
<Icon on:click={deleteStep} hoverable name="DeleteOutline" /> <AbsTooltip type="negative" text="Delete step">
</AbsTooltip> <Icon on:click={deleteStep} hoverable name="DeleteOutline" />
</AbsTooltip>
{/if}
{/if} {/if}
{#if !showTestStatus} {#if !showTestStatus}
<Icon <Icon
@ -244,18 +255,21 @@
display: none; display: none;
} }
input { input {
font-family: var(--font-sans);
color: var(--ink); color: var(--ink);
background-color: transparent; background-color: transparent;
border: 1px solid transparent; border: 1px solid transparent;
font-size: var(--spectrum-alias-font-size-default);
width: 230px; width: 230px;
box-sizing: border-box; box-sizing: border-box;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap; white-space: nowrap;
} }
.input-text {
font-size: var(--spectrum-alias-font-size-default);
font-family: var(--font-sans);
text-overflow: ellipsis;
}
input:focus { input:focus {
outline: none; outline: none;
} }

View File

@ -48,6 +48,7 @@
<div class="block" style={width ? `width: ${width}` : ""}> <div class="block" style={width ? `width: ${width}` : ""}>
{#if block.stepId !== ActionStepID.LOOP} {#if block.stepId !== ActionStepID.LOOP}
<FlowItemHeader <FlowItemHeader
enableNaming={false}
open={!!openBlocks[block.id]} open={!!openBlocks[block.id]}
on:toggle={() => (openBlocks[block.id] = !openBlocks[block.id])} on:toggle={() => (openBlocks[block.id] = !openBlocks[block.id])}
isTrigger={idx === 0} isTrigger={idx === 0}

View File

@ -27,7 +27,7 @@
$: if (value?.queryId == null) value = { queryId: "" } $: if (value?.queryId == null) value = { queryId: "" }
</script> </script>
<div class="schema-fields"> <div class="schema-field">
<Label>Query</Label> <Label>Query</Label>
<div class="field-width"> <div class="field-width">
<Select <Select
@ -41,8 +41,8 @@
</div> </div>
{#if parameters.length} {#if parameters.length}
<div class="schema-fields"> {#each parameters as field}
{#each parameters as field} <div class="schema-field">
<Label>{field.name}</Label> <Label>{field.name}</Label>
<div class="field-width"> <div class="field-width">
<DrawerBindableInput <DrawerBindableInput
@ -56,8 +56,8 @@
updateOnChange={false} updateOnChange={false}
/> />
</div> </div>
{/each} </div>
</div> {/each}
{/if} {/if}
<style> <style>
@ -65,7 +65,7 @@
width: 320px; width: 320px;
} }
.schema-fields { .schema-field {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
@ -76,7 +76,7 @@
margin-bottom: 10px; margin-bottom: 10px;
} }
.schema-fields :global(label) { .schema-field :global(label) {
text-transform: capitalize; text-transform: capitalize;
} }
</style> </style>

View File

@ -114,10 +114,10 @@
</div> </div>
{#if schemaFields.length} {#if schemaFields.length}
{#each schemaFields as [field, schema]} {#each schemaFields as [field, schema]}
<div class="schema-fields"> {#if !schema.autocolumn && schema.type !== "attachment"}
<Label>{field}</Label> <div class="schema-fields">
<div class="field-width"> <Label>{field}</Label>
{#if !schema.autocolumn && schema.type !== "attachment"} <div class="field-width">
{#if isTestModal} {#if isTestModal}
<RowSelectorTypes <RowSelectorTypes
{isTestModal} {isTestModal}
@ -151,20 +151,20 @@
/> />
</DrawerBindableSlot> </DrawerBindableSlot>
{/if} {/if}
{/if}
{#if isUpdateRow && schema.type === "link"} {#if isUpdateRow && schema.type === "link"}
<div class="checkbox-field"> <div class="checkbox-field">
<Checkbox <Checkbox
value={meta.fields?.[field]?.clearRelationships} value={meta.fields?.[field]?.clearRelationships}
text={"Clear relationships if empty?"} text={"Clear relationships if empty?"}
size={"S"} size={"S"}
on:change={e => onChangeSetting(e, field)} on:change={e => onChangeSetting(e, field)}
/> />
</div> </div>
{/if} {/if}
</div>
</div> </div>
</div> {/if}
{/each} {/each}
{/if} {/if}

View File

@ -67,6 +67,7 @@
bind:linkedRows={value[field]} bind:linkedRows={value[field]}
{schema} {schema}
on:change={e => onChange(e, field)} on:change={e => onChange(e, field)}
useLabel={false}
/> />
{:else if schema.type === "string" || schema.type === "number"} {:else if schema.type === "string" || schema.type === "number"}
<svelte:component <svelte:component

View File

@ -7,7 +7,7 @@
export let schema export let schema
export let linkedRows = [] export let linkedRows = []
export let useLabel = true
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
let rows = [] let rows = []
@ -51,7 +51,7 @@
linkedIds = e.detail ? [e.detail] : [] linkedIds = e.detail ? [e.detail] : []
dispatch("change", linkedIds) dispatch("change", linkedIds)
}} }}
{label} label={useLabel ? label : null}
sort sort
/> />
{:else} {:else}

View File

@ -56,7 +56,7 @@
{/if} {/if}
{#key history} {#key history}
<div class="history"> <div class="history">
<TestDisplay testResults={history} width="100%" /> <TestDisplay testResults={history} width="320px" />
</div> </div>
{/key} {/key}
</Layout> </Layout>

View File

@ -26,7 +26,7 @@ export async function fetchSelf(ctx: UserCtx) {
} }
const appId = context.getAppId() const appId = context.getAppId()
let user: ContextUser = await getFullUser(ctx, userId) let user: ContextUser = await getFullUser(userId)
// this shouldn't be returned by the app self // this shouldn't be returned by the app self
delete user.roles delete user.roles
// forward the csrf token from the session // forward the csrf token from the session

View File

@ -106,7 +106,6 @@ export async function fetchDeployments(ctx: any) {
} }
ctx.body = Object.values(deployments.history).reverse() ctx.body = Object.values(deployments.history).reverse()
} catch (err) { } catch (err) {
console.error(err)
ctx.body = [] ctx.body = []
} }
} }

View File

@ -1,7 +1,7 @@
import { EMPTY_LAYOUT } from "../../constants/layouts" import { EMPTY_LAYOUT } from "../../constants/layouts"
import { generateLayoutID, getScreenParams } from "../../db/utils" import { generateLayoutID, getScreenParams } from "../../db/utils"
import { events, context } from "@budibase/backend-core" import { events, context } from "@budibase/backend-core"
import { BBContext } from "@budibase/types" import { BBContext, Layout } from "@budibase/types"
export async function save(ctx: BBContext) { export async function save(ctx: BBContext) {
const db = context.getAppDB() const db = context.getAppDB()
@ -30,12 +30,12 @@ export async function destroy(ctx: BBContext) {
layoutRev = ctx.params.layoutRev layoutRev = ctx.params.layoutRev
const layoutsUsedByScreens = ( const layoutsUsedByScreens = (
await db.allDocs( await db.allDocs<Layout>(
getScreenParams(null, { getScreenParams(null, {
include_docs: true, include_docs: true,
}) })
) )
).rows.map(element => element.doc.layoutId) ).rows.map(element => element.doc!.layoutId)
if (layoutsUsedByScreens.includes(layoutId)) { if (layoutsUsedByScreens.includes(layoutId)) {
ctx.throw(400, "Cannot delete a layout that's being used by a screen") 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 // utility function to stop this repetition - permissions always stored under roles
async function getAllDBRoles(db: Database) { async function getAllDBRoles(db: Database) {
const body = await db.allDocs( const body = await db.allDocs<Role>(
getRoleParams(null, { getRoleParams(null, {
include_docs: true, include_docs: true,
}) })
) )
return body.rows.map(row => row.doc) return body.rows.map(row => row.doc!)
} }
async function updatePermissionOnRole( async function updatePermissionOnRole(
@ -79,7 +79,7 @@ async function updatePermissionOnRole(
) { ) {
rolePermissions[resourceId] = rolePermissions[resourceId] =
typeof rolePermissions[resourceId] === "string" typeof rolePermissions[resourceId] === "string"
? [rolePermissions[resourceId]] ? [rolePermissions[resourceId] as unknown as string]
: [] : []
} }
// handle the removal/updating the role which has this permission first // handle the removal/updating the role which has this permission first

View File

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

View File

@ -23,7 +23,6 @@ import {
breakRowIdField, breakRowIdField,
convertRowId, convertRowId,
generateRowIdField, generateRowIdField,
getPrimaryDisplay,
isRowId, isRowId,
isSQL, isSQL,
} from "../../../integrations/utils" } from "../../../integrations/utils"
@ -237,7 +236,7 @@ function basicProcessing({
thisRow._id = generateIdForRow(row, table, isLinked) thisRow._id = generateIdForRow(row, table, isLinked)
thisRow.tableId = table._id thisRow.tableId = table._id
thisRow._rev = "rev" thisRow._rev = "rev"
return processFormulas(table, thisRow) return thisRow
} }
function fixArrayTypes(row: Row, table: Table) { function fixArrayTypes(row: Row, table: Table) {
@ -392,7 +391,7 @@ export class ExternalRequest<T extends Operation> {
return { row: newRow, manyRelationships } return { row: newRow, manyRelationships }
} }
squashRelationshipColumns( processRelationshipFields(
table: Table, table: Table,
row: Row, row: Row,
relationships: RelationshipsJson[] relationships: RelationshipsJson[]
@ -402,7 +401,6 @@ export class ExternalRequest<T extends Operation> {
if (!linkedTable || !row[relationship.column]) { if (!linkedTable || !row[relationship.column]) {
continue continue
} }
const display = linkedTable.primaryDisplay
for (let key of Object.keys(row[relationship.column])) { for (let key of Object.keys(row[relationship.column])) {
let relatedRow: Row = row[relationship.column][key] let relatedRow: Row = row[relationship.column][key]
// add this row as context for the relationship // add this row as context for the relationship
@ -411,15 +409,10 @@ export class ExternalRequest<T extends Operation> {
relatedRow[col.name] = [row] relatedRow[col.name] = [row]
} }
} }
// process additional types
relatedRow = processDates(table, relatedRow)
relatedRow = processFormulas(linkedTable, relatedRow) relatedRow = processFormulas(linkedTable, relatedRow)
let relatedDisplay row[relationship.column][key] = relatedRow
if (display) {
relatedDisplay = getPrimaryDisplay(relatedRow[display])
}
row[relationship.column][key] = {
primaryDisplay: relatedDisplay || "Invalid display column",
_id: relatedRow._id,
}
} }
} }
return row return row
@ -521,14 +514,14 @@ export class ExternalRequest<T extends Operation> {
) )
} }
// Process some additional data types // make sure all related rows are correct
let finalRowArray = Object.values(finalRows) let finalRowArray = Object.values(finalRows).map(row =>
finalRowArray = processDates(table, finalRowArray) this.processRelationshipFields(table, row, relationships)
finalRowArray = processFormulas(table, finalRowArray) as Row[]
return finalRowArray.map((row: Row) =>
this.squashRelationshipColumns(table, row, relationships)
) )
// process some additional types
finalRowArray = processDates(table, finalRowArray)
return finalRowArray
} }
/** /**
@ -663,7 +656,7 @@ export class ExternalRequest<T extends Operation> {
linkPrimary, linkPrimary,
linkSecondary, linkSecondary,
}: { }: {
row: { [key: string]: any } row: Row
linkPrimary: string linkPrimary: string
linkSecondary?: string linkSecondary?: string
}) { }) {

View File

@ -76,6 +76,7 @@ export async function patch(ctx: UserCtx<PatchRowRequest, PatchRowResponse>) {
relationships: true, relationships: true,
}) })
const enrichedRow = await outputProcessing(table, row, { const enrichedRow = await outputProcessing(table, row, {
squash: true,
preserveLinks: true, preserveLinks: true,
}) })
return { return {
@ -119,7 +120,10 @@ export async function save(ctx: UserCtx) {
}) })
return { return {
...response, ...response,
row: await outputProcessing(table, row, { preserveLinks: true }), row: await outputProcessing(table, row, {
preserveLinks: true,
squash: true,
}),
} }
} else { } else {
return response return response
@ -140,7 +144,7 @@ export async function find(ctx: UserCtx): Promise<Row> {
const table = await sdk.tables.getTable(tableId) const table = await sdk.tables.getTable(tableId)
// Preserving links, as the outputProcessing does not support external rows yet and we don't need it in this use case // Preserving links, as the outputProcessing does not support external rows yet and we don't need it in this use case
return await outputProcessing(table, row, { return await outputProcessing(table, row, {
squash: false, squash: true,
preserveLinks: true, preserveLinks: true,
}) })
} }
@ -207,7 +211,7 @@ export async function fetchEnrichedRow(ctx: UserCtx) {
// don't support composite keys right now // don't support composite keys right now
const linkedIds = links.map((link: Row) => breakRowIdField(link._id!)[0]) const linkedIds = links.map((link: Row) => breakRowIdField(link._id!)[0])
const primaryLink = linkedTable.primary?.[0] as string const primaryLink = linkedTable.primary?.[0] as string
row[fieldName] = await handleRequest(Operation.READ, linkedTableId!, { const relatedRows = await handleRequest(Operation.READ, linkedTableId!, {
tables, tables,
filters: { filters: {
oneOf: { oneOf: {
@ -216,6 +220,10 @@ export async function fetchEnrichedRow(ctx: UserCtx) {
}, },
includeSqlRelationships: IncludeRelationship.INCLUDE, includeSqlRelationships: IncludeRelationship.INCLUDE,
}) })
row[fieldName] = await outputProcessing(linkedTable, relatedRows, {
squash: true,
preserveLinks: true,
})
} }
return row return row
} }

View File

@ -2,7 +2,6 @@ import * as linkRows from "../../../db/linkedRows"
import { import {
generateRowID, generateRowID,
getMultiIDParams, getMultiIDParams,
getTableIDFromRowID,
InternalTables, InternalTables,
} from "../../../db/utils" } from "../../../db/utils"
import * as userController from "../user" import * as userController from "../user"
@ -89,7 +88,7 @@ export async function patch(ctx: UserCtx<PatchRowRequest, PatchRowResponse>) {
if (isUserTable) { if (isUserTable) {
// the row has been updated, need to put it into the ctx // the row has been updated, need to put it into the ctx
ctx.request.body = row as any ctx.request.body = row as any
await userController.updateMetadata(ctx) await userController.updateMetadata(ctx as any)
return { row: ctx.body as Row, table } return { row: ctx.body as Row, table }
} }
@ -233,17 +232,16 @@ export async function fetchEnrichedRow(ctx: UserCtx) {
const tableId = utils.getTableId(ctx) const tableId = utils.getTableId(ctx)
const rowId = ctx.params.rowId as string const rowId = ctx.params.rowId as string
// need table to work out where links go in row, as well as the link docs // 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), sdk.tables.getTable(tableId),
utils.findRow(ctx, tableId, rowId), utils.findRow(ctx, tableId, rowId),
linkRows.getLinkDocuments({ tableId, rowId, fieldName }), linkRows.getLinkDocuments({ tableId, rowId, fieldName }),
]) ])
const table = response[0] as Table const linkVals = links as LinkDocumentValue[]
const row = response[1] as Row
const linkVals = response[2] as LinkDocumentValue[]
// look up the actual rows based on the ids // look up the actual rows based on the ids
const params = getMultiIDParams(linkVals.map(linkVal => linkVal.id)) 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 // get the linked tables
const linkTableIds = getLinkedTableIDs(table as Table) const linkTableIds = getLinkedTableIDs(table as Table)

View File

@ -86,12 +86,12 @@ export async function updateAllFormulasInTable(table: Table) {
const db = context.getAppDB() const db = context.getAppDB()
// start by getting the raw rows (which will be written back to DB after update) // start by getting the raw rows (which will be written back to DB after update)
let rows = ( let rows = (
await db.allDocs( await db.allDocs<Row>(
getRowParams(table._id, null, { getRowParams(table._id, null, {
include_docs: true, 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 // 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 // rows so that we don't write any of the enriched information back
let enrichedRows = await outputProcessing(table, cloneDeep(rows), { let enrichedRows = await outputProcessing(table, cloneDeep(rows), {
@ -101,12 +101,12 @@ export async function updateAllFormulasInTable(table: Table) {
for (let row of rows) { for (let row of rows) {
// find the enriched row, if found process the formulas // find the enriched row, if found process the formulas
const enrichedRow = enrichedRows.find( const enrichedRow = enrichedRows.find(
(enriched: any) => enriched._id === row._id (enriched: Row) => enriched._id === row._id
) )
if (enrichedRow) { if (enrichedRow) {
const processed = processFormulas(table, cloneDeep(row), { const processed = processFormulas(table, cloneDeep(row), {
dynamic: false, dynamic: false,
contextRows: enrichedRow, contextRows: [enrichedRow],
}) })
// values have changed, need to add to bulk docs to update // values have changed, need to add to bulk docs to update
if (!isEqual(processed, row)) { if (!isEqual(processed, row)) {
@ -139,7 +139,7 @@ export async function finaliseRow(
// use enriched row to generate formulas for saving, specifically only use as context // use enriched row to generate formulas for saving, specifically only use as context
row = processFormulas(table, row, { row = processFormulas(table, row, {
dynamic: false, dynamic: false,
contextRows: enrichedRow, contextRows: [enrichedRow],
}) })
// don't worry about rev, tables handle rev/lastID updates // don't worry about rev, tables handle rev/lastID updates
// if another row has been written since processing this will // if another row has been written since processing this will
@ -163,7 +163,9 @@ export async function finaliseRow(
const response = await db.put(row) const response = await db.put(row)
// for response, calculate the formulas for the enriched row // for response, calculate the formulas for the enriched row
enrichedRow._rev = response.rev enrichedRow._rev = response.rev
enrichedRow = await processFormulas(table, enrichedRow, { dynamic: false }) enrichedRow = processFormulas(table, enrichedRow, {
dynamic: false,
})
// this updates the related formulas in other rows based on the relations to this row // this updates the related formulas in other rows based on the relations to this row
if (updateFormula) { if (updateFormula) {
await updateRelatedFormula(table, enrichedRow) await updateRelatedFormula(table, enrichedRow)

View File

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

View File

@ -1,14 +1,26 @@
import { generateUserFlagID, InternalTables } from "../../db/utils" import { generateUserFlagID, InternalTables } from "../../db/utils"
import { getFullUser } from "../../utilities/users" import { getFullUser } from "../../utilities/users"
import { context } from "@budibase/backend-core" import { context } from "@budibase/backend-core"
import { Ctx, UserCtx } from "@budibase/types" import {
ContextUserMetadata,
Ctx,
FetchUserMetadataResponse,
FindUserMetadataResponse,
Flags,
SetFlagRequest,
UserCtx,
UserMetadata,
} from "@budibase/types"
import sdk from "../../sdk" import sdk from "../../sdk"
import { DocumentInsertResponse } from "@budibase/nano"
export async function fetchMetadata(ctx: Ctx) { export async function fetchMetadata(ctx: Ctx<void, FetchUserMetadataResponse>) {
ctx.body = await sdk.users.fetchMetadata() ctx.body = await sdk.users.fetchMetadata()
} }
export async function updateSelfMetadata(ctx: UserCtx) { export async function updateSelfMetadata(
ctx: UserCtx<UserMetadata, DocumentInsertResponse>
) {
// overwrite the ID with current users // overwrite the ID with current users
ctx.request.body._id = ctx.user?._id ctx.request.body._id = ctx.user?._id
// make sure no stale rev // make sure no stale rev
@ -18,19 +30,21 @@ export async function updateSelfMetadata(ctx: UserCtx) {
await updateMetadata(ctx) await updateMetadata(ctx)
} }
export async function updateMetadata(ctx: UserCtx) { export async function updateMetadata(
ctx: UserCtx<UserMetadata, DocumentInsertResponse>
) {
const db = context.getAppDB() const db = context.getAppDB()
const user = ctx.request.body const user = ctx.request.body
// this isn't applicable to the user const metadata: ContextUserMetadata = {
delete user.roles
const metadata = {
tableId: InternalTables.USER_METADATA, tableId: InternalTables.USER_METADATA,
...user, ...user,
} }
// this isn't applicable to the user
delete metadata.roles
ctx.body = await db.put(metadata) ctx.body = await db.put(metadata)
} }
export async function destroyMetadata(ctx: UserCtx) { export async function destroyMetadata(ctx: UserCtx<void, { message: string }>) {
const db = context.getAppDB() const db = context.getAppDB()
try { try {
const dbUser = await sdk.users.get(ctx.params.id) const dbUser = await sdk.users.get(ctx.params.id)
@ -43,11 +57,15 @@ export async function destroyMetadata(ctx: UserCtx) {
} }
} }
export async function findMetadata(ctx: UserCtx) { export async function findMetadata(
ctx.body = await getFullUser(ctx, ctx.params.id) ctx: UserCtx<void, FindUserMetadataResponse>
) {
ctx.body = await getFullUser(ctx.params.id)
} }
export async function setFlag(ctx: UserCtx) { export async function setFlag(
ctx: UserCtx<SetFlagRequest, { message: string }>
) {
const userId = ctx.user?._id const userId = ctx.user?._id
const { flag, value } = ctx.request.body const { flag, value } = ctx.request.body
if (!flag) { if (!flag) {
@ -55,9 +73,9 @@ export async function setFlag(ctx: UserCtx) {
} }
const flagDocId = generateUserFlagID(userId!) const flagDocId = generateUserFlagID(userId!)
const db = context.getAppDB() const db = context.getAppDB()
let doc let doc: Flags
try { try {
doc = await db.get<any>(flagDocId) doc = await db.get<Flags>(flagDocId)
} catch (err) { } catch (err) {
doc = { _id: flagDocId } doc = { _id: flagDocId }
} }
@ -66,13 +84,13 @@ export async function setFlag(ctx: UserCtx) {
ctx.body = { message: "Flag set successfully" } ctx.body = { message: "Flag set successfully" }
} }
export async function getFlags(ctx: UserCtx) { export async function getFlags(ctx: UserCtx<void, Flags>) {
const userId = ctx.user?._id const userId = ctx.user?._id
const docId = generateUserFlagID(userId!) const docId = generateUserFlagID(userId!)
const db = context.getAppDB() const db = context.getAppDB()
let doc let doc: Flags
try { try {
doc = await db.get(docId) doc = await db.get<Flags>(docId)
} catch (err) { } catch (err) {
doc = { _id: docId } doc = { _id: docId }
} }

View File

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

View File

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

View File

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

View File

@ -10,6 +10,7 @@ import {
FieldSchema, FieldSchema,
FieldType, FieldType,
FieldTypeSubtypes, FieldTypeSubtypes,
FormulaTypes,
INTERNAL_TABLE_SOURCE_ID, INTERNAL_TABLE_SOURCE_ID,
MonthlyQuotaName, MonthlyQuotaName,
PermissionLevel, PermissionLevel,
@ -32,6 +33,7 @@ import {
structures, structures,
} from "@budibase/backend-core/tests" } from "@budibase/backend-core/tests"
import _ from "lodash" import _ from "lodash"
import * as uuid from "uuid"
const timestamp = new Date("2023-01-26T11:48:57.597Z").toISOString() const timestamp = new Date("2023-01-26T11:48:57.597Z").toISOString()
tk.freeze(timestamp) tk.freeze(timestamp)
@ -68,7 +70,7 @@ describe.each([
const generateTableConfig: () => SaveTableRequest = () => { const generateTableConfig: () => SaveTableRequest = () => {
return { return {
name: generator.word(), name: uuid.v4(),
type: "table", type: "table",
primary: ["id"], primary: ["id"],
primaryDisplay: "name", primaryDisplay: "name",
@ -481,7 +483,7 @@ describe.each([
}) })
const createViewResponse = await config.createView({ const createViewResponse = await config.createView({
name: generator.word(), name: uuid.v4(),
schema: { schema: {
Country: { Country: {
visible: true, visible: true,
@ -816,7 +818,8 @@ describe.each([
RelationshipType.ONE_TO_MANY, RelationshipType.ONE_TO_MANY,
["link"], ["link"],
{ {
name: generator.word(), // Making sure that the combined table name + column name is within postgres limits
name: uuid.v4().replace(/-/g, "").substring(0, 16),
type: "table", type: "table",
primary: ["id"], primary: ["id"],
primaryDisplay: "id", primaryDisplay: "id",
@ -949,7 +952,7 @@ describe.each([
describe("view 2.0", () => { describe("view 2.0", () => {
async function userTable(): Promise<Table> { async function userTable(): Promise<Table> {
return { return {
name: `users_${generator.word()}`, name: `users_${uuid.v4()}`,
sourceId: INTERNAL_TABLE_SOURCE_ID, sourceId: INTERNAL_TABLE_SOURCE_ID,
sourceType: TableSourceType.INTERNAL, sourceType: TableSourceType.INTERNAL,
type: "table", type: "table",
@ -1133,7 +1136,7 @@ describe.each([
const viewSchema = { age: { visible: true }, name: { visible: true } } const viewSchema = { age: { visible: true }, name: { visible: true } }
async function userTable(): Promise<Table> { async function userTable(): Promise<Table> {
return { return {
name: `users_${generator.word()}`, name: `users_${uuid.v4()}`,
sourceId: INTERNAL_TABLE_SOURCE_ID, sourceId: INTERNAL_TABLE_SOURCE_ID,
sourceType: TableSourceType.INTERNAL, sourceType: TableSourceType.INTERNAL,
type: "table", type: "table",
@ -1630,7 +1633,7 @@ describe.each([
}), }),
(tableId: string) => (tableId: string) =>
config.api.row.save(tableId, { config.api.row.save(tableId, {
name: generator.word(), name: uuid.v4(),
description: generator.paragraph(), description: generator.paragraph(),
tableId, tableId,
}), }),
@ -1998,4 +2001,52 @@ describe.each([
}) })
}) })
}) })
describe("Formula fields", () => {
let relationshipTable: Table, tableId: string, relatedRow: Row
beforeAll(async () => {
const otherTableId = config.table!._id!
const cfg = generateTableConfig()
relationshipTable = await config.createLinkedTable(
RelationshipType.ONE_TO_MANY,
["links"],
{
...cfg,
// needs to be a short name
name: "b",
schema: {
...cfg.schema,
formula: {
name: "formula",
type: FieldType.FORMULA,
formula: "{{ links.0.name }}",
formulaType: FormulaTypes.DYNAMIC,
},
},
}
)
tableId = relationshipTable._id!
relatedRow = await config.api.row.save(otherTableId, {
name: generator.word(),
description: generator.paragraph(),
})
await config.api.row.save(tableId, {
name: generator.word(),
description: generator.paragraph(),
tableId,
links: [relatedRow._id],
})
})
it("should be able to search for rows containing formulas", async () => {
const { rows } = await config.api.row.search(tableId)
expect(rows.length).toBe(1)
expect(rows[0].links.length).toBe(1)
const row = rows[0]
expect(row.formula).toBe(relatedRow.name)
})
})
}) })

View File

@ -1,208 +0,0 @@
const { roles, utils } = require("@budibase/backend-core")
const { checkPermissionsEndpoint } = require("./utilities/TestFunctions")
const setup = require("./utilities")
const { BUILTIN_ROLE_IDS } = roles
jest.setTimeout(30000)
jest.mock("../../../utilities/workerRequests", () => ({
getGlobalUsers: jest.fn(() => {
return {}
}),
getGlobalSelf: jest.fn(() => {
return {}
}),
deleteGlobalUser: jest.fn(),
}))
describe("/users", () => {
let request = setup.getRequest()
let config = setup.getConfig()
afterAll(setup.afterAll)
beforeAll(async () => {
await config.init()
})
describe("fetch", () => {
it("returns a list of users from an instance db", async () => {
await config.createUser({ id: "uuidx" })
await config.createUser({ id: "uuidy" })
const res = await request
.get(`/api/users/metadata`)
.set(config.defaultHeaders())
.expect("Content-Type", /json/)
.expect(200)
expect(res.body.length).toBe(3)
expect(res.body.find(u => u._id === `ro_ta_users_us_uuidx`)).toBeDefined()
expect(res.body.find(u => u._id === `ro_ta_users_us_uuidy`)).toBeDefined()
})
it("should apply authorization to endpoint", async () => {
await config.createUser()
await checkPermissionsEndpoint({
config,
request,
method: "GET",
url: `/api/users/metadata`,
passRole: BUILTIN_ROLE_IDS.ADMIN,
failRole: BUILTIN_ROLE_IDS.PUBLIC,
})
})
})
describe("update", () => {
it("should be able to update the user", async () => {
const user = await config.createUser({ id: `us_update${utils.newid()}` })
user.roleId = BUILTIN_ROLE_IDS.BASIC
delete user._rev
const res = await request
.put(`/api/users/metadata`)
.set(config.defaultHeaders())
.send(user)
.expect(200)
.expect("Content-Type", /json/)
expect(res.body.ok).toEqual(true)
})
it("should be able to update the user multiple times", async () => {
const user = await config.createUser()
delete user._rev
const res1 = await request
.put(`/api/users/metadata`)
.set(config.defaultHeaders())
.send({ ...user, roleId: BUILTIN_ROLE_IDS.BASIC })
.expect(200)
.expect("Content-Type", /json/)
const res = await request
.put(`/api/users/metadata`)
.set(config.defaultHeaders())
.send({ ...user, _rev: res1.body.rev, roleId: BUILTIN_ROLE_IDS.POWER })
.expect(200)
.expect("Content-Type", /json/)
expect(res.body.ok).toEqual(true)
})
it("should require the _rev field for multiple updates", async () => {
const user = await config.createUser()
delete user._rev
await request
.put(`/api/users/metadata`)
.set(config.defaultHeaders())
.send({ ...user, roleId: BUILTIN_ROLE_IDS.BASIC })
.expect(200)
.expect("Content-Type", /json/)
await request
.put(`/api/users/metadata`)
.set(config.defaultHeaders())
.send({ ...user, roleId: BUILTIN_ROLE_IDS.POWER })
.expect(409)
.expect("Content-Type", /json/)
})
})
describe("destroy", () => {
it("should be able to delete the user", async () => {
const user = await config.createUser()
const res = await request
.delete(`/api/users/metadata/${user._id}`)
.set(config.defaultHeaders())
.expect(200)
.expect("Content-Type", /json/)
expect(res.body.message).toBeDefined()
})
})
describe("find", () => {
it("should be able to find the user", async () => {
const user = await config.createUser()
const res = await request
.get(`/api/users/metadata/${user._id}`)
.set(config.defaultHeaders())
.expect(200)
.expect("Content-Type", /json/)
expect(res.body._id).toEqual(user._id)
expect(res.body.roleId).toEqual(BUILTIN_ROLE_IDS.ADMIN)
expect(res.body.tableId).toBeDefined()
})
})
describe("setFlag", () => {
it("should throw an error if a flag is not provided", async () => {
await config.createUser()
const res = await request
.post(`/api/users/flags`)
.set(config.defaultHeaders())
.send({ value: "test" })
.expect(400)
.expect("Content-Type", /json/)
expect(res.body.message).toEqual(
"Must supply a 'flag' field in request body."
)
})
it("should be able to set a flag on the user", async () => {
await config.createUser()
const res = await request
.post(`/api/users/flags`)
.set(config.defaultHeaders())
.send({ value: "test", flag: "test" })
.expect(200)
.expect("Content-Type", /json/)
expect(res.body.message).toEqual("Flag set successfully")
})
})
describe("getFlags", () => {
it("should get flags for a specific user", async () => {
let flagData = { value: "test", flag: "test" }
await config.createUser()
await request
.post(`/api/users/flags`)
.set(config.defaultHeaders())
.send(flagData)
.expect(200)
.expect("Content-Type", /json/)
const res = await request
.get(`/api/users/flags`)
.set(config.defaultHeaders())
.expect(200)
.expect("Content-Type", /json/)
expect(res.body[flagData.value]).toEqual(flagData.flag)
})
})
describe("setFlag", () => {
it("should throw an error if a flag is not provided", async () => {
await config.createUser()
const res = await request
.post(`/api/users/flags`)
.set(config.defaultHeaders())
.send({ value: "test" })
.expect(400)
.expect("Content-Type", /json/)
expect(res.body.message).toEqual(
"Must supply a 'flag' field in request body."
)
})
it("should be able to set a flag on the user", async () => {
await config.createUser()
const res = await request
.post(`/api/users/flags`)
.set(config.defaultHeaders())
.send({ value: "test", flag: "test" })
.expect(200)
.expect("Content-Type", /json/)
expect(res.body.message).toEqual("Flag set successfully")
})
})
})

View File

@ -0,0 +1,144 @@
import { roles, utils } from "@budibase/backend-core"
import { checkPermissionsEndpoint } from "./utilities/TestFunctions"
import * as setup from "./utilities"
import { UserMetadata } from "@budibase/types"
jest.setTimeout(30000)
jest.mock("../../../utilities/workerRequests", () => ({
getGlobalUsers: jest.fn(() => {
return {}
}),
getGlobalSelf: jest.fn(() => {
return {}
}),
deleteGlobalUser: jest.fn(),
}))
describe("/users", () => {
let request = setup.getRequest()
let config = setup.getConfig()
afterAll(setup.afterAll)
beforeAll(async () => {
await config.init()
})
describe("fetch", () => {
it("returns a list of users from an instance db", async () => {
await config.createUser({ id: "uuidx" })
await config.createUser({ id: "uuidy" })
const res = await config.api.user.fetch()
expect(res.length).toBe(3)
const ids = res.map(u => u._id)
expect(ids).toContain(`ro_ta_users_us_uuidx`)
expect(ids).toContain(`ro_ta_users_us_uuidy`)
})
it("should apply authorization to endpoint", async () => {
await config.createUser()
await checkPermissionsEndpoint({
config,
request,
method: "GET",
url: `/api/users/metadata`,
passRole: roles.BUILTIN_ROLE_IDS.ADMIN,
failRole: roles.BUILTIN_ROLE_IDS.PUBLIC,
})
})
})
describe("update", () => {
it("should be able to update the user", async () => {
const user: UserMetadata = await config.createUser({
id: `us_update${utils.newid()}`,
})
user.roleId = roles.BUILTIN_ROLE_IDS.BASIC
delete user._rev
const res = await config.api.user.update(user)
expect(res.ok).toEqual(true)
})
it("should be able to update the user multiple times", async () => {
const user = await config.createUser()
delete user._rev
const res1 = await config.api.user.update({
...user,
roleId: roles.BUILTIN_ROLE_IDS.BASIC,
})
const res2 = await config.api.user.update({
...user,
_rev: res1.rev,
roleId: roles.BUILTIN_ROLE_IDS.POWER,
})
expect(res2.ok).toEqual(true)
})
it("should require the _rev field for multiple updates", async () => {
const user = await config.createUser()
delete user._rev
await config.api.user.update({
...user,
roleId: roles.BUILTIN_ROLE_IDS.BASIC,
})
await config.api.user.update(
{ ...user, roleId: roles.BUILTIN_ROLE_IDS.POWER },
{ expectStatus: 409 }
)
})
})
describe("destroy", () => {
it("should be able to delete the user", async () => {
const user = await config.createUser()
const res = await config.api.user.destroy(user._id!)
expect(res.message).toBeDefined()
})
})
describe("find", () => {
it("should be able to find the user", async () => {
const user = await config.createUser()
const res = await config.api.user.find(user._id!)
expect(res._id).toEqual(user._id)
expect(res.roleId).toEqual(roles.BUILTIN_ROLE_IDS.ADMIN)
expect(res.tableId).toBeDefined()
})
})
describe("setFlag", () => {
it("should throw an error if a flag is not provided", async () => {
await config.createUser()
const res = await request
.post(`/api/users/flags`)
.set(config.defaultHeaders())
.send({ value: "test" })
.expect(400)
.expect("Content-Type", /json/)
expect(res.body.message).toEqual(
"Must supply a 'flag' field in request body."
)
})
it("should be able to set a flag on the user", async () => {
await config.createUser()
const res = await config.api.user.setFlag("test", true)
expect(res.message).toEqual("Flag set successfully")
})
})
describe("getFlags", () => {
it("should get flags for a specific user", async () => {
await config.createUser()
await config.api.user.setFlag("test", "test")
const res = await config.api.user.getFlags()
expect(res.test).toEqual("test")
})
})
})

View File

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

View File

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

View File

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

View File

@ -8,6 +8,7 @@ import {
LinkDocumentValue, LinkDocumentValue,
Table, Table,
} from "@budibase/types" } from "@budibase/types"
import sdk from "../../sdk"
export { createLinkView } from "../views/staticViews" export { createLinkView } from "../views/staticViews"
@ -110,12 +111,11 @@ export function getLinkedTableIDs(table: Table): string[] {
} }
export async function getLinkedTable(id: string, tables: Table[]) { export async function getLinkedTable(id: string, tables: Table[]) {
const db = context.getAppDB()
let linkedTable = tables.find(table => table._id === id) let linkedTable = tables.find(table => table._id === id)
if (linkedTable) { if (linkedTable) {
return linkedTable return linkedTable
} }
linkedTable = await db.get(id) linkedTable = await sdk.tables.getTable(id)
if (linkedTable) { if (linkedTable) {
tables.push(linkedTable) tables.push(linkedTable)
} }

View File

@ -923,7 +923,6 @@ describe("postgres integrations", () => {
[m2mFieldName]: [ [m2mFieldName]: [
{ {
_id: row._id, _id: row._id,
primaryDisplay: "Invalid display column",
}, },
], ],
}) })
@ -932,7 +931,6 @@ describe("postgres integrations", () => {
[m2mFieldName]: [ [m2mFieldName]: [
{ {
_id: row._id, _id: row._id,
primaryDisplay: "Invalid display column",
}, },
], ],
}) })

View File

@ -91,7 +91,7 @@ async function getImportableDocuments(db: Database) {
// map the responses to the document itself // map the responses to the document itself
let documents: Document[] = [] let documents: Document[] = []
for (let response of await Promise.all(docPromises)) { 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 // remove the _rev, stops it being written
documents.forEach(doc => { 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 { User, ContextUser, UserGroup } from "@budibase/types"
import { sdk as proSdk } from "@budibase/pro" import { sdk as proSdk } from "@budibase/pro"
import sdk from "../../" import sdk from "../../"
import { getGlobalUsers, processUser } from "../../../utilities/global" import {
getGlobalUsers,
getRawGlobalUsers,
processUser,
} from "../../../utilities/global"
import { generateUserMetadataID, InternalTables } from "../../../db/utils" import { generateUserMetadataID, InternalTables } from "../../../db/utils"
type DeletedUser = { _id: string; deleted: boolean } type DeletedUser = { _id: string; deleted: boolean }
@ -77,9 +81,7 @@ async function syncUsersToApp(
export async function syncUsersToAllApps(userIds: string[]) { export async function syncUsersToAllApps(userIds: string[]) {
// list of users, if one has been deleted it will be undefined in array // list of users, if one has been deleted it will be undefined in array
const users = (await getGlobalUsers(userIds, { const users = await getRawGlobalUsers(userIds)
noProcessing: true,
})) as User[]
const groups = await proSdk.groups.fetch() const groups = await proSdk.groups.fetch()
const finalUsers: (User | DeletedUser)[] = [] const finalUsers: (User | DeletedUser)[] = []
for (let userId of userIds) { for (let userId of userIds) {

View File

@ -51,12 +51,12 @@ export async function fetch(opts?: {
// Get external datasources // Get external datasources
const datasources = ( const datasources = (
await db.allDocs( await db.allDocs<Datasource>(
getDatasourceParams(null, { getDatasourceParams(null, {
include_docs: true, include_docs: true,
}) })
) )
).rows.map(row => row.doc) ).rows.map(row => row.doc!)
const allDatasources: Datasource[] = await sdk.datasources.removeSecrets([ const allDatasources: Datasource[] = await sdk.datasources.removeSecrets([
bbInternalDb, 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

@ -80,7 +80,10 @@ export async function search(options: SearchParams) {
rows = rows.map((r: any) => pick(r, fields)) rows = rows.map((r: any) => pick(r, fields))
} }
rows = await outputProcessing(table, rows, { preserveLinks: true }) rows = await outputProcessing(table, rows, {
preserveLinks: true,
squash: true,
})
// need wrapper object for bookmarks etc when paginating // need wrapper object for bookmarks etc when paginating
return { rows, hasNextPage, bookmark: bookmark && bookmark + 1 } return { rows, hasNextPage, bookmark: bookmark && bookmark + 1 }
@ -185,6 +188,7 @@ export async function fetch(tableId: string): Promise<Row[]> {
const table = await sdk.tables.getTable(tableId) const table = await sdk.tables.getTable(tableId)
return await outputProcessing<Row[]>(table, response, { return await outputProcessing<Row[]>(table, response, {
preserveLinks: true, preserveLinks: true,
squash: true,
}) })
} }

View File

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

View File

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

View File

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

View File

@ -264,7 +264,7 @@ class TestConfiguration {
admin = false, admin = false,
email = this.defaultUserValues.email, email = this.defaultUserValues.email,
roles, roles,
}: any = {}) { }: any = {}): Promise<User> {
const db = tenancy.getTenantDB(this.getTenantId()) const db = tenancy.getTenantDB(this.getTenantId())
let existing let existing
try { try {

View File

@ -9,6 +9,7 @@ import { ScreenAPI } from "./screen"
import { ApplicationAPI } from "./application" import { ApplicationAPI } from "./application"
import { BackupAPI } from "./backup" import { BackupAPI } from "./backup"
import { AttachmentAPI } from "./attachment" import { AttachmentAPI } from "./attachment"
import { UserAPI } from "./user"
export default class API { export default class API {
table: TableAPI table: TableAPI
@ -21,6 +22,7 @@ export default class API {
application: ApplicationAPI application: ApplicationAPI
backup: BackupAPI backup: BackupAPI
attachment: AttachmentAPI attachment: AttachmentAPI
user: UserAPI
constructor(config: TestConfiguration) { constructor(config: TestConfiguration) {
this.table = new TableAPI(config) this.table = new TableAPI(config)
@ -33,5 +35,6 @@ export default class API {
this.application = new ApplicationAPI(config) this.application = new ApplicationAPI(config)
this.backup = new BackupAPI(config) this.backup = new BackupAPI(config)
this.attachment = new AttachmentAPI(config) this.attachment = new AttachmentAPI(config)
this.user = new UserAPI(config)
} }
} }

View File

@ -6,6 +6,7 @@ import {
ExportRowsRequest, ExportRowsRequest,
BulkImportRequest, BulkImportRequest,
BulkImportResponse, BulkImportResponse,
SearchRowResponse,
} from "@budibase/types" } from "@budibase/types"
import TestConfiguration from "../TestConfiguration" import TestConfiguration from "../TestConfiguration"
import { TestAPI } from "./base" import { TestAPI } from "./base"
@ -154,7 +155,7 @@ export class RowAPI extends TestAPI {
search = async ( search = async (
sourceId: string, sourceId: string,
{ expectStatus } = { expectStatus: 200 } { expectStatus } = { expectStatus: 200 }
): Promise<Row[]> => { ): Promise<SearchRowResponse> => {
const request = this.request const request = this.request
.post(`/api/${sourceId}/search`) .post(`/api/${sourceId}/search`)
.set(this.config.defaultHeaders()) .set(this.config.defaultHeaders())

View File

@ -0,0 +1,157 @@
import {
FetchUserMetadataResponse,
FindUserMetadataResponse,
Flags,
UserMetadata,
} from "@budibase/types"
import TestConfiguration from "../TestConfiguration"
import { TestAPI } from "./base"
import { DocumentInsertResponse } from "@budibase/nano"
export class UserAPI extends TestAPI {
constructor(config: TestConfiguration) {
super(config)
}
fetch = async (
{ expectStatus } = { expectStatus: 200 }
): Promise<FetchUserMetadataResponse> => {
const res = await this.request
.get(`/api/users/metadata`)
.set(this.config.defaultHeaders())
.expect("Content-Type", /json/)
if (res.status !== expectStatus) {
throw new Error(
`Expected status ${expectStatus} but got ${
res.status
} with body ${JSON.stringify(res.body)}`
)
}
return res.body
}
find = async (
id: string,
{ expectStatus } = { expectStatus: 200 }
): Promise<FindUserMetadataResponse> => {
const res = await this.request
.get(`/api/users/metadata/${id}`)
.set(this.config.defaultHeaders())
.expect("Content-Type", /json/)
if (res.status !== expectStatus) {
throw new Error(
`Expected status ${expectStatus} but got ${
res.status
} with body ${JSON.stringify(res.body)}`
)
}
return res.body
}
update = async (
user: UserMetadata,
{ expectStatus } = { expectStatus: 200 }
): Promise<DocumentInsertResponse> => {
const res = await this.request
.put(`/api/users/metadata`)
.set(this.config.defaultHeaders())
.send(user)
.expect("Content-Type", /json/)
if (res.status !== expectStatus) {
throw new Error(
`Expected status ${expectStatus} but got ${
res.status
} with body ${JSON.stringify(res.body)}`
)
}
return res.body as DocumentInsertResponse
}
updateSelf = async (
user: UserMetadata,
{ expectStatus } = { expectStatus: 200 }
): Promise<DocumentInsertResponse> => {
const res = await this.request
.post(`/api/users/metadata/self`)
.set(this.config.defaultHeaders())
.send(user)
.expect("Content-Type", /json/)
if (res.status !== expectStatus) {
throw new Error(
`Expected status ${expectStatus} but got ${
res.status
} with body ${JSON.stringify(res.body)}`
)
}
return res.body as DocumentInsertResponse
}
destroy = async (
id: string,
{ expectStatus } = { expectStatus: 200 }
): Promise<{ message: string }> => {
const res = await this.request
.delete(`/api/users/metadata/${id}`)
.set(this.config.defaultHeaders())
.expect("Content-Type", /json/)
if (res.status !== expectStatus) {
throw new Error(
`Expected status ${expectStatus} but got ${
res.status
} with body ${JSON.stringify(res.body)}`
)
}
return res.body as { message: string }
}
setFlag = async (
flag: string,
value: any,
{ expectStatus } = { expectStatus: 200 }
): Promise<{ message: string }> => {
const res = await this.request
.post(`/api/users/flags`)
.set(this.config.defaultHeaders())
.send({ flag, value })
.expect("Content-Type", /json/)
if (res.status !== expectStatus) {
throw new Error(
`Expected status ${expectStatus} but got ${
res.status
} with body ${JSON.stringify(res.body)}`
)
}
return res.body as { message: string }
}
getFlags = async (
{ expectStatus } = { expectStatus: 200 }
): Promise<Flags> => {
const res = await this.request
.get(`/api/users/flags`)
.set(this.config.defaultHeaders())
.expect("Content-Type", /json/)
if (res.status !== expectStatus) {
throw new Error(
`Expected status ${expectStatus} but got ${
res.status
} with body ${JSON.stringify(res.body)}`
)
}
return res.body as Flags
}
}

View File

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

View File

@ -1,9 +1,9 @@
import { createRoutingView } from "../../db/views/staticViews" import { createRoutingView } from "../../db/views/staticViews"
import { ViewName, getQueryIndex, UNICODE_MAX } from "../../db/utils" import { ViewName, getQueryIndex, UNICODE_MAX } from "../../db/utils"
import { context } from "@budibase/backend-core" 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 id: string
routing: ScreenRouting routing: ScreenRouting
} }

View File

@ -2,16 +2,15 @@ import * as linkRows from "../../db/linkedRows"
import { FieldTypes, AutoFieldSubTypes } from "../../constants" import { FieldTypes, AutoFieldSubTypes } from "../../constants"
import { processFormulas, fixAutoColumnSubType } from "./utils" import { processFormulas, fixAutoColumnSubType } from "./utils"
import { ObjectStoreBuckets } from "../../constants" import { ObjectStoreBuckets } from "../../constants"
import { context, db as dbCore, objectStore } from "@budibase/backend-core" import {
context,
db as dbCore,
objectStore,
utils,
} from "@budibase/backend-core"
import { InternalTables } from "../../db/utils" import { InternalTables } from "../../db/utils"
import { TYPE_TRANSFORM_MAP } from "./map" import { TYPE_TRANSFORM_MAP } from "./map"
import { import { FieldSubtype, Row, RowAttachment, Table } from "@budibase/types"
AutoColumnFieldMetadata,
FieldSubtype,
Row,
RowAttachment,
Table,
} from "@budibase/types"
import { cloneDeep } from "lodash/fp" import { cloneDeep } from "lodash/fp"
import { import {
processInputBBReferences, processInputBBReferences,
@ -233,6 +232,11 @@ export async function outputProcessing<T extends Row[] | Row>(
}) })
: safeRows : safeRows
// make sure squash is enabled if needed
if (!opts.squash && utils.hasCircularStructure(rows)) {
opts.squash = true
}
// process complex types: attachements, bb references... // process complex types: attachements, bb references...
for (let [property, column] of Object.entries(table.schema)) { for (let [property, column] of Object.entries(table.schema)) {
if (column.type === FieldTypes.ATTACHMENT) { if (column.type === FieldTypes.ATTACHMENT) {
@ -258,7 +262,7 @@ export async function outputProcessing<T extends Row[] | Row>(
} }
// process formulas after the complex types had been processed // process formulas after the complex types had been processed
enriched = processFormulas(table, enriched, { dynamic: true }) as Row[] enriched = processFormulas(table, enriched, { dynamic: true })
if (opts.squash) { if (opts.squash) {
enriched = (await linkRows.squashLinksToPrimaryDisplay( enriched = (await linkRows.squashLinksToPrimaryDisplay(

View File

@ -12,6 +12,11 @@ import {
Table, Table,
} from "@budibase/types" } from "@budibase/types"
interface FormulaOpts {
dynamic?: boolean
contextRows?: Row[]
}
/** /**
* If the subtype has been lost for any reason this works out what * If the subtype has been lost for any reason this works out what
* subtype the auto column should be. * subtype the auto column should be.
@ -40,52 +45,50 @@ export function fixAutoColumnSubType(
/** /**
* Looks through the rows provided and finds formulas - which it then processes. * Looks through the rows provided and finds formulas - which it then processes.
*/ */
export function processFormulas( export function processFormulas<T extends Row | Row[]>(
table: Table, table: Table,
rows: Row[] | Row, inputRows: T,
{ dynamic, contextRows }: any = { dynamic: true } { dynamic, contextRows }: FormulaOpts = { dynamic: true }
) { ): T {
const single = !Array.isArray(rows) const rows = Array.isArray(inputRows) ? inputRows : [inputRows]
let rowArray: Row[] if (rows)
if (single) { for (let [column, schema] of Object.entries(table.schema)) {
rowArray = [rows] if (schema.type !== FieldTypes.FORMULA) {
contextRows = contextRows ? [contextRows] : contextRows continue
} else { }
rowArray = rows
}
for (let [column, schema] of Object.entries(table.schema)) {
if (schema.type !== FieldTypes.FORMULA) {
continue
}
const isStatic = schema.formulaType === FormulaTypes.STATIC const isStatic = schema.formulaType === FormulaTypes.STATIC
if ( if (
schema.formula == null || schema.formula == null ||
(dynamic && isStatic) || (dynamic && isStatic) ||
(!dynamic && !isStatic) (!dynamic && !isStatic)
) { ) {
continue continue
} }
// iterate through rows and process formula // iterate through rows and process formula
for (let i = 0; i < rowArray.length; i++) { for (let i = 0; i < rows.length; i++) {
let row = rowArray[i] let row = rows[i]
let context = contextRows ? contextRows[i] : row let context = contextRows ? contextRows[i] : row
rowArray[i] = { rows[i] = {
...row, ...row,
[column]: processStringSync(schema.formula, context), [column]: processStringSync(schema.formula, context),
}
} }
} }
} return Array.isArray(inputRows) ? rows : rows[0]
return single ? rowArray[0] : rowArray
} }
/** /**
* Processes any date columns and ensures that those without the ignoreTimezones * Processes any date columns and ensures that those without the ignoreTimezones
* flag set are parsed as UTC rather than local time. * flag set are parsed as UTC rather than local time.
*/ */
export function processDates(table: Table, rows: Row[]) { export function processDates<T extends Row | Row[]>(
let datesWithTZ = [] table: Table,
inputRows: T
): T {
let rows = Array.isArray(inputRows) ? inputRows : [inputRows]
let datesWithTZ: string[] = []
for (let [column, schema] of Object.entries(table.schema)) { for (let [column, schema] of Object.entries(table.schema)) {
if (schema.type !== FieldTypes.DATETIME) { if (schema.type !== FieldTypes.DATETIME) {
continue continue
@ -102,5 +105,6 @@ export function processDates(table: Table, rows: Row[]) {
} }
} }
} }
return rows
return Array.isArray(inputRows) ? rows : rows[0]
} }

View File

@ -1,11 +1,13 @@
import { InternalTables } from "../db/utils" import { InternalTables } from "../db/utils"
import { getGlobalUser } from "./global" import { getGlobalUser } from "./global"
import { context, roles } from "@budibase/backend-core" import { context, roles } from "@budibase/backend-core"
import { UserCtx } from "@budibase/types" import { ContextUserMetadata, UserCtx, UserMetadata } from "@budibase/types"
export async function getFullUser(ctx: UserCtx, userId: string) { export async function getFullUser(
userId: string
): Promise<ContextUserMetadata> {
const global = await getGlobalUser(userId) const global = await getGlobalUser(userId)
let metadata: any = {} let metadata: UserMetadata | undefined = undefined
// always prefer the user metadata _id and _rev // always prefer the user metadata _id and _rev
delete global._id delete global._id
@ -14,11 +16,11 @@ export async function getFullUser(ctx: UserCtx, userId: string) {
try { try {
// this will throw an error if the db doesn't exist, or there is no appId // this will throw an error if the db doesn't exist, or there is no appId
const db = context.getAppDB() const db = context.getAppDB()
metadata = await db.get(userId) metadata = await db.get<UserMetadata>(userId)
delete metadata.csrfToken
} catch (err) { } catch (err) {
// it is fine if there is no user metadata yet // it is fine if there is no user metadata yet
} }
delete metadata.csrfToken
return { return {
...metadata, ...metadata,
...global, ...global,

View File

@ -6,3 +6,4 @@ export * from "./rows"
export * from "./table" export * from "./table"
export * from "./permission" export * from "./permission"
export * from "./attachment" export * from "./attachment"
export * from "./user"

View File

@ -0,0 +1,9 @@
import { ContextUserMetadata } from "../../../"
export type FetchUserMetadataResponse = ContextUserMetadata[]
export type FindUserMetadataResponse = ContextUserMetadata
export interface SetFlagRequest {
flag: string
value: any
}

View File

@ -0,0 +1,5 @@
import { Document } from "../../"
export interface Flags extends Document {
[key: string]: any
}

View File

@ -1,2 +1,3 @@
export * from "./account" export * from "./account"
export * from "./user" export * from "./user"
export * from "./flag"

View File

@ -2,4 +2,5 @@ import { Document } from "../document"
export interface Layout extends Document { export interface Layout extends Document {
props: any 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 { export type UserMetadata = User & Row
roleId: string export type ContextUserMetadata = ContextUser & Row
email?: string
}

View File

@ -1,5 +1,24 @@
import { SearchFilter, SortOrder, SortType } from "../../api" import { SearchFilter, SortOrder, SortType } from "../../api"
import { UIFieldMetadata } from "./table" 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 { export interface View {
name?: string name?: string
@ -10,7 +29,7 @@ export interface View {
calculation?: ViewCalculation calculation?: ViewCalculation
map?: string map?: string
reduce?: any reduce?: any
meta?: Record<string, any> meta?: ViewTemplateOpts
} }
export interface ViewV2 { export interface ViewV2 {

View File

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

View File

@ -1,5 +1,5 @@
import type Nano from "@budibase/nano" import type Nano from "@budibase/nano"
import { AllDocsResponse, AnyDocument, Document } from "../" import { AllDocsResponse, AnyDocument, Document, ViewTemplateOpts } from "../"
import { Writable } from "stream" import { Writable } from "stream"
export enum SearchIndex { export enum SearchIndex {
@ -20,6 +20,37 @@ export enum SortOption {
DESCENDING = "desc", 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 = { export type CouchFindOptions = {
selector: PouchDB.Find.Selector selector: PouchDB.Find.Selector
fields?: string[] fields?: string[]
@ -101,8 +132,10 @@ export interface Database {
opts?: DatabasePutOpts opts?: DatabasePutOpts
): Promise<Nano.DocumentInsertResponse> ): Promise<Nano.DocumentInsertResponse>
bulkDocs(documents: AnyDocument[]): Promise<Nano.DocumentBulkResponse[]> bulkDocs(documents: AnyDocument[]): Promise<Nano.DocumentBulkResponse[]>
allDocs<T>(params: DatabaseQueryOpts): Promise<AllDocsResponse<T>> allDocs<T extends Document>(
query<T>( params: DatabaseQueryOpts
): Promise<AllDocsResponse<T>>
query<T extends Document>(
viewName: string, viewName: string,
params: DatabaseQueryOpts params: DatabaseQueryOpts
): Promise<AllDocsResponse<T>> ): Promise<AllDocsResponse<T>>

View File

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

View File

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