Merge branch 'master' into fix-section-crash
This commit is contained in:
commit
c1018915d7
|
@ -188,4 +188,17 @@ describe("utils", () => {
|
|||
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)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
@ -237,3 +237,17 @@ export function timeout(timeMs: number) {
|
|||
export function isAudited(event: 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
|
||||
}
|
||||
|
|
|
@ -48,15 +48,14 @@
|
|||
<UndoRedoControl store={automationHistoryStore} />
|
||||
</div>
|
||||
<div class="controls">
|
||||
<div class="buttons">
|
||||
<div
|
||||
on:click={() => {
|
||||
testDataModal.show()
|
||||
}}
|
||||
class="buttons"
|
||||
>
|
||||
<Icon hoverable size="M" name="Play" />
|
||||
<div
|
||||
on:click={() => {
|
||||
testDataModal.show()
|
||||
}}
|
||||
>
|
||||
Run test
|
||||
</div>
|
||||
<div>Run test</div>
|
||||
</div>
|
||||
<div class="buttons">
|
||||
<Icon
|
||||
|
|
|
@ -27,7 +27,7 @@
|
|||
$: if (value?.queryId == null) value = { queryId: "" }
|
||||
</script>
|
||||
|
||||
<div class="schema-fields">
|
||||
<div class="schema-field">
|
||||
<Label>Query</Label>
|
||||
<div class="field-width">
|
||||
<Select
|
||||
|
@ -41,8 +41,8 @@
|
|||
</div>
|
||||
|
||||
{#if parameters.length}
|
||||
<div class="schema-fields">
|
||||
{#each parameters as field}
|
||||
{#each parameters as field}
|
||||
<div class="schema-field">
|
||||
<Label>{field.name}</Label>
|
||||
<div class="field-width">
|
||||
<DrawerBindableInput
|
||||
|
@ -56,8 +56,8 @@
|
|||
updateOnChange={false}
|
||||
/>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
|
@ -65,7 +65,7 @@
|
|||
width: 320px;
|
||||
}
|
||||
|
||||
.schema-fields {
|
||||
.schema-field {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
@ -76,7 +76,7 @@
|
|||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.schema-fields :global(label) {
|
||||
.schema-field :global(label) {
|
||||
text-transform: capitalize;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -114,10 +114,10 @@
|
|||
</div>
|
||||
{#if schemaFields.length}
|
||||
{#each schemaFields as [field, schema]}
|
||||
<div class="schema-fields">
|
||||
<Label>{field}</Label>
|
||||
<div class="field-width">
|
||||
{#if !schema.autocolumn && schema.type !== "attachment"}
|
||||
{#if !schema.autocolumn && schema.type !== "attachment"}
|
||||
<div class="schema-fields">
|
||||
<Label>{field}</Label>
|
||||
<div class="field-width">
|
||||
{#if isTestModal}
|
||||
<RowSelectorTypes
|
||||
{isTestModal}
|
||||
|
@ -151,20 +151,20 @@
|
|||
/>
|
||||
</DrawerBindableSlot>
|
||||
{/if}
|
||||
{/if}
|
||||
|
||||
{#if isUpdateRow && schema.type === "link"}
|
||||
<div class="checkbox-field">
|
||||
<Checkbox
|
||||
value={meta.fields?.[field]?.clearRelationships}
|
||||
text={"Clear relationships if empty?"}
|
||||
size={"S"}
|
||||
on:change={e => onChangeSetting(e, field)}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
{#if isUpdateRow && schema.type === "link"}
|
||||
<div class="checkbox-field">
|
||||
<Checkbox
|
||||
value={meta.fields?.[field]?.clearRelationships}
|
||||
text={"Clear relationships if empty?"}
|
||||
size={"S"}
|
||||
on:change={e => onChangeSetting(e, field)}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
{/each}
|
||||
{/if}
|
||||
|
||||
|
|
|
@ -67,6 +67,7 @@
|
|||
bind:linkedRows={value[field]}
|
||||
{schema}
|
||||
on:change={e => onChange(e, field)}
|
||||
useLabel={false}
|
||||
/>
|
||||
{:else if schema.type === "string" || schema.type === "number"}
|
||||
<svelte:component
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
|
||||
export let schema
|
||||
export let linkedRows = []
|
||||
|
||||
export let useLabel = true
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
let rows = []
|
||||
|
@ -51,7 +51,7 @@
|
|||
linkedIds = e.detail ? [e.detail] : []
|
||||
dispatch("change", linkedIds)
|
||||
}}
|
||||
{label}
|
||||
label={useLabel ? label : null}
|
||||
sort
|
||||
/>
|
||||
{:else}
|
||||
|
|
|
@ -26,7 +26,7 @@ export async function fetchSelf(ctx: UserCtx) {
|
|||
}
|
||||
|
||||
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
|
||||
delete user.roles
|
||||
// forward the csrf token from the session
|
||||
|
|
|
@ -106,7 +106,6 @@ export async function fetchDeployments(ctx: any) {
|
|||
}
|
||||
ctx.body = Object.values(deployments.history).reverse()
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
ctx.body = []
|
||||
}
|
||||
}
|
||||
|
|
|
@ -23,7 +23,6 @@ import {
|
|||
breakRowIdField,
|
||||
convertRowId,
|
||||
generateRowIdField,
|
||||
getPrimaryDisplay,
|
||||
isRowId,
|
||||
isSQL,
|
||||
} from "../../../integrations/utils"
|
||||
|
@ -237,7 +236,7 @@ function basicProcessing({
|
|||
thisRow._id = generateIdForRow(row, table, isLinked)
|
||||
thisRow.tableId = table._id
|
||||
thisRow._rev = "rev"
|
||||
return processFormulas(table, thisRow)
|
||||
return thisRow
|
||||
}
|
||||
|
||||
function fixArrayTypes(row: Row, table: Table) {
|
||||
|
@ -392,7 +391,7 @@ export class ExternalRequest<T extends Operation> {
|
|||
return { row: newRow, manyRelationships }
|
||||
}
|
||||
|
||||
squashRelationshipColumns(
|
||||
processRelationshipFields(
|
||||
table: Table,
|
||||
row: Row,
|
||||
relationships: RelationshipsJson[]
|
||||
|
@ -402,7 +401,6 @@ export class ExternalRequest<T extends Operation> {
|
|||
if (!linkedTable || !row[relationship.column]) {
|
||||
continue
|
||||
}
|
||||
const display = linkedTable.primaryDisplay
|
||||
for (let key of Object.keys(row[relationship.column])) {
|
||||
let relatedRow: Row = row[relationship.column][key]
|
||||
// add this row as context for the relationship
|
||||
|
@ -411,15 +409,10 @@ export class ExternalRequest<T extends Operation> {
|
|||
relatedRow[col.name] = [row]
|
||||
}
|
||||
}
|
||||
// process additional types
|
||||
relatedRow = processDates(table, relatedRow)
|
||||
relatedRow = processFormulas(linkedTable, relatedRow)
|
||||
let relatedDisplay
|
||||
if (display) {
|
||||
relatedDisplay = getPrimaryDisplay(relatedRow[display])
|
||||
}
|
||||
row[relationship.column][key] = {
|
||||
primaryDisplay: relatedDisplay || "Invalid display column",
|
||||
_id: relatedRow._id,
|
||||
}
|
||||
row[relationship.column][key] = relatedRow
|
||||
}
|
||||
}
|
||||
return row
|
||||
|
@ -521,14 +514,14 @@ export class ExternalRequest<T extends Operation> {
|
|||
)
|
||||
}
|
||||
|
||||
// Process some additional data types
|
||||
let finalRowArray = Object.values(finalRows)
|
||||
finalRowArray = processDates(table, finalRowArray)
|
||||
finalRowArray = processFormulas(table, finalRowArray) as Row[]
|
||||
|
||||
return finalRowArray.map((row: Row) =>
|
||||
this.squashRelationshipColumns(table, row, relationships)
|
||||
// make sure all related rows are correct
|
||||
let finalRowArray = Object.values(finalRows).map(row =>
|
||||
this.processRelationshipFields(table, row, relationships)
|
||||
)
|
||||
|
||||
// process some additional types
|
||||
finalRowArray = processDates(table, finalRowArray)
|
||||
return finalRowArray
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -663,7 +656,7 @@ export class ExternalRequest<T extends Operation> {
|
|||
linkPrimary,
|
||||
linkSecondary,
|
||||
}: {
|
||||
row: { [key: string]: any }
|
||||
row: Row
|
||||
linkPrimary: string
|
||||
linkSecondary?: string
|
||||
}) {
|
||||
|
|
|
@ -76,6 +76,7 @@ export async function patch(ctx: UserCtx<PatchRowRequest, PatchRowResponse>) {
|
|||
relationships: true,
|
||||
})
|
||||
const enrichedRow = await outputProcessing(table, row, {
|
||||
squash: true,
|
||||
preserveLinks: true,
|
||||
})
|
||||
return {
|
||||
|
@ -119,7 +120,10 @@ export async function save(ctx: UserCtx) {
|
|||
})
|
||||
return {
|
||||
...response,
|
||||
row: await outputProcessing(table, row, { preserveLinks: true }),
|
||||
row: await outputProcessing(table, row, {
|
||||
preserveLinks: true,
|
||||
squash: true,
|
||||
}),
|
||||
}
|
||||
} else {
|
||||
return response
|
||||
|
@ -140,7 +144,7 @@ export async function find(ctx: UserCtx): Promise<Row> {
|
|||
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
|
||||
return await outputProcessing(table, row, {
|
||||
squash: false,
|
||||
squash: true,
|
||||
preserveLinks: true,
|
||||
})
|
||||
}
|
||||
|
@ -207,7 +211,7 @@ export async function fetchEnrichedRow(ctx: UserCtx) {
|
|||
// don't support composite keys right now
|
||||
const linkedIds = links.map((link: Row) => breakRowIdField(link._id!)[0])
|
||||
const primaryLink = linkedTable.primary?.[0] as string
|
||||
row[fieldName] = await handleRequest(Operation.READ, linkedTableId!, {
|
||||
const relatedRows = await handleRequest(Operation.READ, linkedTableId!, {
|
||||
tables,
|
||||
filters: {
|
||||
oneOf: {
|
||||
|
@ -216,6 +220,10 @@ export async function fetchEnrichedRow(ctx: UserCtx) {
|
|||
},
|
||||
includeSqlRelationships: IncludeRelationship.INCLUDE,
|
||||
})
|
||||
row[fieldName] = await outputProcessing(linkedTable, relatedRows, {
|
||||
squash: true,
|
||||
preserveLinks: true,
|
||||
})
|
||||
}
|
||||
return row
|
||||
}
|
||||
|
|
|
@ -2,7 +2,6 @@ import * as linkRows from "../../../db/linkedRows"
|
|||
import {
|
||||
generateRowID,
|
||||
getMultiIDParams,
|
||||
getTableIDFromRowID,
|
||||
InternalTables,
|
||||
} from "../../../db/utils"
|
||||
import * as userController from "../user"
|
||||
|
@ -89,7 +88,7 @@ export async function patch(ctx: UserCtx<PatchRowRequest, PatchRowResponse>) {
|
|||
if (isUserTable) {
|
||||
// the row has been updated, need to put it into the ctx
|
||||
ctx.request.body = row as any
|
||||
await userController.updateMetadata(ctx)
|
||||
await userController.updateMetadata(ctx as any)
|
||||
return { row: ctx.body as Row, table }
|
||||
}
|
||||
|
||||
|
|
|
@ -101,12 +101,12 @@ export async function updateAllFormulasInTable(table: Table) {
|
|||
for (let row of rows) {
|
||||
// find the enriched row, if found process the formulas
|
||||
const enrichedRow = enrichedRows.find(
|
||||
(enriched: any) => enriched._id === row._id
|
||||
(enriched: Row) => enriched._id === row._id
|
||||
)
|
||||
if (enrichedRow) {
|
||||
const processed = processFormulas(table, cloneDeep(row), {
|
||||
dynamic: false,
|
||||
contextRows: enrichedRow,
|
||||
contextRows: [enrichedRow],
|
||||
})
|
||||
// values have changed, need to add to bulk docs to update
|
||||
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
|
||||
row = processFormulas(table, row, {
|
||||
dynamic: false,
|
||||
contextRows: enrichedRow,
|
||||
contextRows: [enrichedRow],
|
||||
})
|
||||
// don't worry about rev, tables handle rev/lastID updates
|
||||
// if another row has been written since processing this will
|
||||
|
@ -163,7 +163,9 @@ export async function finaliseRow(
|
|||
const response = await db.put(row)
|
||||
// for response, calculate the formulas for the enriched row
|
||||
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
|
||||
if (updateFormula) {
|
||||
await updateRelatedFormula(table, enrichedRow)
|
||||
|
|
|
@ -1,14 +1,26 @@
|
|||
import { generateUserFlagID, InternalTables } from "../../db/utils"
|
||||
import { getFullUser } from "../../utilities/users"
|
||||
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 { DocumentInsertResponse } from "@budibase/nano"
|
||||
|
||||
export async function fetchMetadata(ctx: Ctx) {
|
||||
export async function fetchMetadata(ctx: Ctx<void, FetchUserMetadataResponse>) {
|
||||
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
|
||||
ctx.request.body._id = ctx.user?._id
|
||||
// make sure no stale rev
|
||||
|
@ -18,19 +30,21 @@ export async function updateSelfMetadata(ctx: UserCtx) {
|
|||
await updateMetadata(ctx)
|
||||
}
|
||||
|
||||
export async function updateMetadata(ctx: UserCtx) {
|
||||
export async function updateMetadata(
|
||||
ctx: UserCtx<UserMetadata, DocumentInsertResponse>
|
||||
) {
|
||||
const db = context.getAppDB()
|
||||
const user = ctx.request.body
|
||||
// this isn't applicable to the user
|
||||
delete user.roles
|
||||
const metadata = {
|
||||
const metadata: ContextUserMetadata = {
|
||||
tableId: InternalTables.USER_METADATA,
|
||||
...user,
|
||||
}
|
||||
// this isn't applicable to the user
|
||||
delete metadata.roles
|
||||
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()
|
||||
try {
|
||||
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) {
|
||||
ctx.body = await getFullUser(ctx, ctx.params.id)
|
||||
export async function findMetadata(
|
||||
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 { flag, value } = ctx.request.body
|
||||
if (!flag) {
|
||||
|
@ -55,9 +73,9 @@ export async function setFlag(ctx: UserCtx) {
|
|||
}
|
||||
const flagDocId = generateUserFlagID(userId!)
|
||||
const db = context.getAppDB()
|
||||
let doc
|
||||
let doc: Flags
|
||||
try {
|
||||
doc = await db.get<any>(flagDocId)
|
||||
doc = await db.get<Flags>(flagDocId)
|
||||
} catch (err) {
|
||||
doc = { _id: flagDocId }
|
||||
}
|
||||
|
@ -66,13 +84,13 @@ export async function setFlag(ctx: UserCtx) {
|
|||
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 docId = generateUserFlagID(userId!)
|
||||
const db = context.getAppDB()
|
||||
let doc
|
||||
let doc: Flags
|
||||
try {
|
||||
doc = await db.get(docId)
|
||||
doc = await db.get<Flags>(docId)
|
||||
} catch (err) {
|
||||
doc = { _id: docId }
|
||||
}
|
||||
|
|
|
@ -27,51 +27,59 @@ interface KoaRateLimitOptions {
|
|||
}
|
||||
|
||||
const PREFIX = "/api/public/v1"
|
||||
// allow a lot more requests when in test
|
||||
const DEFAULT_API_REQ_LIMIT_PER_SEC = env.isTest() ? 100 : 10
|
||||
|
||||
function getApiLimitPerSecond(): number {
|
||||
if (!env.API_REQ_LIMIT_PER_SEC) {
|
||||
return DEFAULT_API_REQ_LIMIT_PER_SEC
|
||||
}
|
||||
return parseInt(env.API_REQ_LIMIT_PER_SEC)
|
||||
}
|
||||
// type can't be known - untyped libraries
|
||||
let limiter: any, rateLimitStore: any
|
||||
if (!env.DISABLE_RATE_LIMITING) {
|
||||
// allow a lot more requests when in test
|
||||
const DEFAULT_API_REQ_LIMIT_PER_SEC = env.isTest() ? 100 : 10
|
||||
|
||||
let rateLimitStore: any = null
|
||||
if (!env.isTest()) {
|
||||
const { password, host, port } = redis.utils.getRedisConnectionDetails()
|
||||
let options: KoaRateLimitOptions = {
|
||||
socket: {
|
||||
host: host,
|
||||
port: port,
|
||||
},
|
||||
function getApiLimitPerSecond(): number {
|
||||
if (!env.API_REQ_LIMIT_PER_SEC) {
|
||||
return DEFAULT_API_REQ_LIMIT_PER_SEC
|
||||
}
|
||||
return parseInt(env.API_REQ_LIMIT_PER_SEC)
|
||||
}
|
||||
|
||||
if (password) {
|
||||
options.password = password
|
||||
}
|
||||
if (!env.isTest()) {
|
||||
const { password, host, port } = redis.utils.getRedisConnectionDetails()
|
||||
let options: KoaRateLimitOptions = {
|
||||
socket: {
|
||||
host: host,
|
||||
port: port,
|
||||
},
|
||||
}
|
||||
|
||||
if (!env.REDIS_CLUSTERED) {
|
||||
// Can't set direct redis db in clustered env
|
||||
options.database = SelectableDatabase.RATE_LIMITING
|
||||
if (password) {
|
||||
options.password = password
|
||||
}
|
||||
|
||||
if (!env.REDIS_CLUSTERED) {
|
||||
// Can't set direct redis db in clustered env
|
||||
options.database = SelectableDatabase.RATE_LIMITING
|
||||
}
|
||||
rateLimitStore = new Stores.Redis(options)
|
||||
RateLimit.defaultOptions({
|
||||
store: rateLimitStore,
|
||||
})
|
||||
}
|
||||
rateLimitStore = new Stores.Redis(options)
|
||||
RateLimit.defaultOptions({
|
||||
store: rateLimitStore,
|
||||
// rate limiting, allows for 2 requests per second
|
||||
limiter = RateLimit.middleware({
|
||||
interval: { sec: 1 },
|
||||
// per ip, per interval
|
||||
max: getApiLimitPerSecond(),
|
||||
})
|
||||
} else {
|
||||
console.log("**** PUBLIC API RATE LIMITING DISABLED ****")
|
||||
}
|
||||
// rate limiting, allows for 2 requests per second
|
||||
const limiter = RateLimit.middleware({
|
||||
interval: { sec: 1 },
|
||||
// per ip, per interval
|
||||
max: getApiLimitPerSecond(),
|
||||
})
|
||||
|
||||
const publicRouter = new Router({
|
||||
prefix: PREFIX,
|
||||
})
|
||||
|
||||
publicRouter.use(limiter)
|
||||
if (limiter) {
|
||||
publicRouter.use(limiter)
|
||||
}
|
||||
|
||||
function addMiddleware(
|
||||
endpoints: any,
|
||||
|
|
|
@ -10,6 +10,7 @@ import {
|
|||
FieldSchema,
|
||||
FieldType,
|
||||
FieldTypeSubtypes,
|
||||
FormulaTypes,
|
||||
INTERNAL_TABLE_SOURCE_ID,
|
||||
MonthlyQuotaName,
|
||||
PermissionLevel,
|
||||
|
@ -2000,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)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
@ -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")
|
||||
})
|
||||
})
|
||||
})
|
|
@ -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")
|
||||
})
|
||||
})
|
||||
})
|
|
@ -2,7 +2,6 @@ import LinkController from "./LinkController"
|
|||
import {
|
||||
IncludeDocs,
|
||||
getLinkDocuments,
|
||||
createLinkView,
|
||||
getUniqueByProp,
|
||||
getRelatedTableForField,
|
||||
getLinkedTableIDs,
|
||||
|
|
|
@ -8,6 +8,7 @@ import {
|
|||
LinkDocumentValue,
|
||||
Table,
|
||||
} from "@budibase/types"
|
||||
import sdk from "../../sdk"
|
||||
|
||||
export { createLinkView } from "../views/staticViews"
|
||||
|
||||
|
@ -110,12 +111,11 @@ export function getLinkedTableIDs(table: Table): string[] {
|
|||
}
|
||||
|
||||
export async function getLinkedTable(id: string, tables: Table[]) {
|
||||
const db = context.getAppDB()
|
||||
let linkedTable = tables.find(table => table._id === id)
|
||||
if (linkedTable) {
|
||||
return linkedTable
|
||||
}
|
||||
linkedTable = await db.get(id)
|
||||
linkedTable = await sdk.tables.getTable(id)
|
||||
if (linkedTable) {
|
||||
tables.push(linkedTable)
|
||||
}
|
||||
|
|
|
@ -61,6 +61,7 @@ const environment = {
|
|||
ALLOW_DEV_AUTOMATIONS: process.env.ALLOW_DEV_AUTOMATIONS,
|
||||
DISABLE_THREADING: process.env.DISABLE_THREADING,
|
||||
DISABLE_AUTOMATION_LOGS: process.env.DISABLE_AUTOMATION_LOGS,
|
||||
DISABLE_RATE_LIMITING: process.env.DISABLE_RATE_LIMITING,
|
||||
MULTI_TENANCY: process.env.MULTI_TENANCY,
|
||||
ENABLE_ANALYTICS: process.env.ENABLE_ANALYTICS,
|
||||
SELF_HOSTED: process.env.SELF_HOSTED,
|
||||
|
|
|
@ -923,7 +923,6 @@ describe("postgres integrations", () => {
|
|||
[m2mFieldName]: [
|
||||
{
|
||||
_id: row._id,
|
||||
primaryDisplay: "Invalid display column",
|
||||
},
|
||||
],
|
||||
})
|
||||
|
@ -932,7 +931,6 @@ describe("postgres integrations", () => {
|
|||
[m2mFieldName]: [
|
||||
{
|
||||
_id: row._id,
|
||||
primaryDisplay: "Invalid display column",
|
||||
},
|
||||
],
|
||||
})
|
||||
|
|
|
@ -80,7 +80,10 @@ export async function search(options: SearchParams) {
|
|||
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
|
||||
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)
|
||||
return await outputProcessing<Row[]>(table, response, {
|
||||
preserveLinks: true,
|
||||
squash: true,
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
@ -264,7 +264,7 @@ class TestConfiguration {
|
|||
admin = false,
|
||||
email = this.defaultUserValues.email,
|
||||
roles,
|
||||
}: any = {}) {
|
||||
}: any = {}): Promise<User> {
|
||||
const db = tenancy.getTenantDB(this.getTenantId())
|
||||
let existing
|
||||
try {
|
||||
|
|
|
@ -9,6 +9,7 @@ import { ScreenAPI } from "./screen"
|
|||
import { ApplicationAPI } from "./application"
|
||||
import { BackupAPI } from "./backup"
|
||||
import { AttachmentAPI } from "./attachment"
|
||||
import { UserAPI } from "./user"
|
||||
|
||||
export default class API {
|
||||
table: TableAPI
|
||||
|
@ -21,6 +22,7 @@ export default class API {
|
|||
application: ApplicationAPI
|
||||
backup: BackupAPI
|
||||
attachment: AttachmentAPI
|
||||
user: UserAPI
|
||||
|
||||
constructor(config: TestConfiguration) {
|
||||
this.table = new TableAPI(config)
|
||||
|
@ -33,5 +35,6 @@ export default class API {
|
|||
this.application = new ApplicationAPI(config)
|
||||
this.backup = new BackupAPI(config)
|
||||
this.attachment = new AttachmentAPI(config)
|
||||
this.user = new UserAPI(config)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,6 +6,7 @@ import {
|
|||
ExportRowsRequest,
|
||||
BulkImportRequest,
|
||||
BulkImportResponse,
|
||||
SearchRowResponse,
|
||||
} from "@budibase/types"
|
||||
import TestConfiguration from "../TestConfiguration"
|
||||
import { TestAPI } from "./base"
|
||||
|
@ -154,7 +155,7 @@ export class RowAPI extends TestAPI {
|
|||
search = async (
|
||||
sourceId: string,
|
||||
{ expectStatus } = { expectStatus: 200 }
|
||||
): Promise<Row[]> => {
|
||||
): Promise<SearchRowResponse> => {
|
||||
const request = this.request
|
||||
.post(`/api/${sourceId}/search`)
|
||||
.set(this.config.defaultHeaders())
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -2,16 +2,15 @@ import * as linkRows from "../../db/linkedRows"
|
|||
import { FieldTypes, AutoFieldSubTypes } from "../../constants"
|
||||
import { processFormulas, fixAutoColumnSubType } from "./utils"
|
||||
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 { TYPE_TRANSFORM_MAP } from "./map"
|
||||
import {
|
||||
AutoColumnFieldMetadata,
|
||||
FieldSubtype,
|
||||
Row,
|
||||
RowAttachment,
|
||||
Table,
|
||||
} from "@budibase/types"
|
||||
import { FieldSubtype, Row, RowAttachment, Table } from "@budibase/types"
|
||||
import { cloneDeep } from "lodash/fp"
|
||||
import {
|
||||
processInputBBReferences,
|
||||
|
@ -233,6 +232,11 @@ export async function outputProcessing<T extends Row[] | Row>(
|
|||
})
|
||||
: safeRows
|
||||
|
||||
// make sure squash is enabled if needed
|
||||
if (!opts.squash && utils.hasCircularStructure(rows)) {
|
||||
opts.squash = true
|
||||
}
|
||||
|
||||
// process complex types: attachements, bb references...
|
||||
for (let [property, column] of Object.entries(table.schema)) {
|
||||
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
|
||||
enriched = processFormulas(table, enriched, { dynamic: true }) as Row[]
|
||||
enriched = processFormulas(table, enriched, { dynamic: true })
|
||||
|
||||
if (opts.squash) {
|
||||
enriched = (await linkRows.squashLinksToPrimaryDisplay(
|
||||
|
|
|
@ -12,6 +12,11 @@ import {
|
|||
Table,
|
||||
} from "@budibase/types"
|
||||
|
||||
interface FormulaOpts {
|
||||
dynamic?: boolean
|
||||
contextRows?: Row[]
|
||||
}
|
||||
|
||||
/**
|
||||
* If the subtype has been lost for any reason this works out what
|
||||
* 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.
|
||||
*/
|
||||
export function processFormulas(
|
||||
export function processFormulas<T extends Row | Row[]>(
|
||||
table: Table,
|
||||
rows: Row[] | Row,
|
||||
{ dynamic, contextRows }: any = { dynamic: true }
|
||||
) {
|
||||
const single = !Array.isArray(rows)
|
||||
let rowArray: Row[]
|
||||
if (single) {
|
||||
rowArray = [rows]
|
||||
contextRows = contextRows ? [contextRows] : contextRows
|
||||
} else {
|
||||
rowArray = rows
|
||||
}
|
||||
for (let [column, schema] of Object.entries(table.schema)) {
|
||||
if (schema.type !== FieldTypes.FORMULA) {
|
||||
continue
|
||||
}
|
||||
inputRows: T,
|
||||
{ dynamic, contextRows }: FormulaOpts = { dynamic: true }
|
||||
): T {
|
||||
const rows = Array.isArray(inputRows) ? inputRows : [inputRows]
|
||||
if (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 (
|
||||
schema.formula == null ||
|
||||
(dynamic && isStatic) ||
|
||||
(!dynamic && !isStatic)
|
||||
) {
|
||||
continue
|
||||
}
|
||||
// iterate through rows and process formula
|
||||
for (let i = 0; i < rowArray.length; i++) {
|
||||
let row = rowArray[i]
|
||||
let context = contextRows ? contextRows[i] : row
|
||||
rowArray[i] = {
|
||||
...row,
|
||||
[column]: processStringSync(schema.formula, context),
|
||||
if (
|
||||
schema.formula == null ||
|
||||
(dynamic && isStatic) ||
|
||||
(!dynamic && !isStatic)
|
||||
) {
|
||||
continue
|
||||
}
|
||||
// iterate through rows and process formula
|
||||
for (let i = 0; i < rows.length; i++) {
|
||||
let row = rows[i]
|
||||
let context = contextRows ? contextRows[i] : row
|
||||
rows[i] = {
|
||||
...row,
|
||||
[column]: processStringSync(schema.formula, context),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return single ? rowArray[0] : rowArray
|
||||
return Array.isArray(inputRows) ? rows : rows[0]
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes any date columns and ensures that those without the ignoreTimezones
|
||||
* flag set are parsed as UTC rather than local time.
|
||||
*/
|
||||
export function processDates(table: Table, rows: Row[]) {
|
||||
let datesWithTZ = []
|
||||
export function processDates<T extends Row | Row[]>(
|
||||
table: Table,
|
||||
inputRows: T
|
||||
): T {
|
||||
let rows = Array.isArray(inputRows) ? inputRows : [inputRows]
|
||||
let datesWithTZ: string[] = []
|
||||
for (let [column, schema] of Object.entries(table.schema)) {
|
||||
if (schema.type !== FieldTypes.DATETIME) {
|
||||
continue
|
||||
|
@ -102,5 +105,6 @@ export function processDates(table: Table, rows: Row[]) {
|
|||
}
|
||||
}
|
||||
}
|
||||
return rows
|
||||
|
||||
return Array.isArray(inputRows) ? rows : rows[0]
|
||||
}
|
||||
|
|
|
@ -1,11 +1,13 @@
|
|||
import { InternalTables } from "../db/utils"
|
||||
import { getGlobalUser } from "./global"
|
||||
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)
|
||||
let metadata: any = {}
|
||||
let metadata: UserMetadata | undefined = undefined
|
||||
|
||||
// always prefer the user metadata _id and _rev
|
||||
delete global._id
|
||||
|
@ -14,11 +16,11 @@ export async function getFullUser(ctx: UserCtx, userId: string) {
|
|||
try {
|
||||
// this will throw an error if the db doesn't exist, or there is no appId
|
||||
const db = context.getAppDB()
|
||||
metadata = await db.get(userId)
|
||||
metadata = await db.get<UserMetadata>(userId)
|
||||
delete metadata.csrfToken
|
||||
} catch (err) {
|
||||
// it is fine if there is no user metadata yet
|
||||
}
|
||||
delete metadata.csrfToken
|
||||
return {
|
||||
...metadata,
|
||||
...global,
|
||||
|
|
|
@ -6,3 +6,4 @@ export * from "./rows"
|
|||
export * from "./table"
|
||||
export * from "./permission"
|
||||
export * from "./attachment"
|
||||
export * from "./user"
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
import { ContextUserMetadata } from "../../../"
|
||||
|
||||
export type FetchUserMetadataResponse = ContextUserMetadata[]
|
||||
export type FindUserMetadataResponse = ContextUserMetadata
|
||||
|
||||
export interface SetFlagRequest {
|
||||
flag: string
|
||||
value: any
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
import { Document } from "../../"
|
||||
|
||||
export interface Flags extends Document {
|
||||
[key: string]: any
|
||||
}
|
|
@ -1,2 +1,3 @@
|
|||
export * from "./account"
|
||||
export * from "./user"
|
||||
export * from "./flag"
|
||||
|
|
|
@ -20,7 +20,7 @@
|
|||
"test:self:ci": "yarn run test --testPathIgnorePatterns=\\.integration\\. \\.cloud\\. \\.licensing\\.",
|
||||
"serve:test:self:ci": "start-server-and-test dev:built http://localhost:4001/health test:self:ci",
|
||||
"serve": "start-server-and-test dev:built http://localhost:4001/health",
|
||||
"dev:built": "cd ../ && yarn dev:built"
|
||||
"dev:built": "cd ../ && DISABLE_RATE_LIMITING=1 yarn dev:built"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@budibase/types": "^2.3.17",
|
||||
|
|
Loading…
Reference in New Issue