-
-
- {#if !schema.autocolumn && schema.type !== "attachment"}
+ {#if !schema.autocolumn && schema.type !== "attachment"}
+
+
+
{#if isTestModal}
{/if}
- {/if}
- {#if isUpdateRow && schema.type === "link"}
-
- onChangeSetting(e, field)}
- />
-
- {/if}
+ {#if isUpdateRow && schema.type === "link"}
+
+ onChangeSetting(e, field)}
+ />
+
+ {/if}
+
-
+ {/if}
{/each}
{/if}
diff --git a/packages/builder/src/components/automation/SetupPanel/RowSelectorTypes.svelte b/packages/builder/src/components/automation/SetupPanel/RowSelectorTypes.svelte
index 373d174541..86235297ae 100644
--- a/packages/builder/src/components/automation/SetupPanel/RowSelectorTypes.svelte
+++ b/packages/builder/src/components/automation/SetupPanel/RowSelectorTypes.svelte
@@ -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"}
{:else}
diff --git a/packages/server/src/api/controllers/auth.ts b/packages/server/src/api/controllers/auth.ts
index eabfe10bab..9b1b78ed9e 100644
--- a/packages/server/src/api/controllers/auth.ts
+++ b/packages/server/src/api/controllers/auth.ts
@@ -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
diff --git a/packages/server/src/api/controllers/deploy/index.ts b/packages/server/src/api/controllers/deploy/index.ts
index 66439d3411..2cf3da3dda 100644
--- a/packages/server/src/api/controllers/deploy/index.ts
+++ b/packages/server/src/api/controllers/deploy/index.ts
@@ -106,7 +106,6 @@ export async function fetchDeployments(ctx: any) {
}
ctx.body = Object.values(deployments.history).reverse()
} catch (err) {
- console.error(err)
ctx.body = []
}
}
diff --git a/packages/server/src/api/controllers/row/ExternalRequest.ts b/packages/server/src/api/controllers/row/ExternalRequest.ts
index c3c5468840..7c98fecb9b 100644
--- a/packages/server/src/api/controllers/row/ExternalRequest.ts
+++ b/packages/server/src/api/controllers/row/ExternalRequest.ts
@@ -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
{
return { row: newRow, manyRelationships }
}
- squashRelationshipColumns(
+ processRelationshipFields(
table: Table,
row: Row,
relationships: RelationshipsJson[]
@@ -402,7 +401,6 @@ export class ExternalRequest {
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 {
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 {
)
}
- // 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 {
linkPrimary,
linkSecondary,
}: {
- row: { [key: string]: any }
+ row: Row
linkPrimary: string
linkSecondary?: string
}) {
diff --git a/packages/server/src/api/controllers/row/external.ts b/packages/server/src/api/controllers/row/external.ts
index 0515b6b97e..287b2ae6aa 100644
--- a/packages/server/src/api/controllers/row/external.ts
+++ b/packages/server/src/api/controllers/row/external.ts
@@ -76,6 +76,7 @@ export async function patch(ctx: UserCtx) {
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 {
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
}
diff --git a/packages/server/src/api/controllers/row/internal.ts b/packages/server/src/api/controllers/row/internal.ts
index d53345a239..fe7d94547a 100644
--- a/packages/server/src/api/controllers/row/internal.ts
+++ b/packages/server/src/api/controllers/row/internal.ts
@@ -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) {
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 }
}
diff --git a/packages/server/src/api/controllers/row/staticFormula.ts b/packages/server/src/api/controllers/row/staticFormula.ts
index 87d551bc4b..8d52b6a05c 100644
--- a/packages/server/src/api/controllers/row/staticFormula.ts
+++ b/packages/server/src/api/controllers/row/staticFormula.ts
@@ -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)
diff --git a/packages/server/src/api/controllers/user.ts b/packages/server/src/api/controllers/user.ts
index b6c3e7c6bd..108e29fd3d 100644
--- a/packages/server/src/api/controllers/user.ts
+++ b/packages/server/src/api/controllers/user.ts
@@ -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) {
ctx.body = await sdk.users.fetchMetadata()
}
-export async function updateSelfMetadata(ctx: UserCtx) {
+export async function updateSelfMetadata(
+ ctx: UserCtx
+) {
// 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
+) {
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) {
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
+) {
+ ctx.body = await getFullUser(ctx.params.id)
}
-export async function setFlag(ctx: UserCtx) {
+export async function setFlag(
+ ctx: UserCtx
+) {
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(flagDocId)
+ doc = await db.get(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) {
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(docId)
} catch (err) {
doc = { _id: docId }
}
diff --git a/packages/server/src/api/routes/public/index.ts b/packages/server/src/api/routes/public/index.ts
index b37ed931fc..f27f3f8857 100644
--- a/packages/server/src/api/routes/public/index.ts
+++ b/packages/server/src/api/routes/public/index.ts
@@ -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,
diff --git a/packages/server/src/api/routes/tests/row.spec.ts b/packages/server/src/api/routes/tests/row.spec.ts
index c426d59f4e..060f6e46c1 100644
--- a/packages/server/src/api/routes/tests/row.spec.ts
+++ b/packages/server/src/api/routes/tests/row.spec.ts
@@ -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)
+ })
+ })
})
diff --git a/packages/server/src/api/routes/tests/user.spec.js b/packages/server/src/api/routes/tests/user.spec.js
deleted file mode 100644
index e8ffd8df2b..0000000000
--- a/packages/server/src/api/routes/tests/user.spec.js
+++ /dev/null
@@ -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")
- })
- })
-})
diff --git a/packages/server/src/api/routes/tests/user.spec.ts b/packages/server/src/api/routes/tests/user.spec.ts
new file mode 100644
index 0000000000..e6349099d7
--- /dev/null
+++ b/packages/server/src/api/routes/tests/user.spec.ts
@@ -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")
+ })
+ })
+})
diff --git a/packages/server/src/db/linkedRows/index.ts b/packages/server/src/db/linkedRows/index.ts
index 8cccf1b96a..7324fa1d94 100644
--- a/packages/server/src/db/linkedRows/index.ts
+++ b/packages/server/src/db/linkedRows/index.ts
@@ -2,7 +2,6 @@ import LinkController from "./LinkController"
import {
IncludeDocs,
getLinkDocuments,
- createLinkView,
getUniqueByProp,
getRelatedTableForField,
getLinkedTableIDs,
diff --git a/packages/server/src/db/linkedRows/linkUtils.ts b/packages/server/src/db/linkedRows/linkUtils.ts
index db9a0dc7d5..5942e7e5a1 100644
--- a/packages/server/src/db/linkedRows/linkUtils.ts
+++ b/packages/server/src/db/linkedRows/linkUtils.ts
@@ -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)
}
diff --git a/packages/server/src/environment.ts b/packages/server/src/environment.ts
index 91424113ac..c126a61c22 100644
--- a/packages/server/src/environment.ts
+++ b/packages/server/src/environment.ts
@@ -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,
diff --git a/packages/server/src/integration-test/postgres.spec.ts b/packages/server/src/integration-test/postgres.spec.ts
index 60416853b3..8dc49a9489 100644
--- a/packages/server/src/integration-test/postgres.spec.ts
+++ b/packages/server/src/integration-test/postgres.spec.ts
@@ -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",
},
],
})
diff --git a/packages/server/src/sdk/app/rows/search/external.ts b/packages/server/src/sdk/app/rows/search/external.ts
index 974900ba6d..2fc6caeb39 100644
--- a/packages/server/src/sdk/app/rows/search/external.ts
+++ b/packages/server/src/sdk/app/rows/search/external.ts
@@ -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 {
const table = await sdk.tables.getTable(tableId)
return await outputProcessing(table, response, {
preserveLinks: true,
+ squash: true,
})
}
diff --git a/packages/server/src/tests/utilities/TestConfiguration.ts b/packages/server/src/tests/utilities/TestConfiguration.ts
index 04c0552457..6877561fcb 100644
--- a/packages/server/src/tests/utilities/TestConfiguration.ts
+++ b/packages/server/src/tests/utilities/TestConfiguration.ts
@@ -264,7 +264,7 @@ class TestConfiguration {
admin = false,
email = this.defaultUserValues.email,
roles,
- }: any = {}) {
+ }: any = {}): Promise {
const db = tenancy.getTenantDB(this.getTenantId())
let existing
try {
diff --git a/packages/server/src/tests/utilities/api/index.ts b/packages/server/src/tests/utilities/api/index.ts
index c553e7b8f4..20b96f7a99 100644
--- a/packages/server/src/tests/utilities/api/index.ts
+++ b/packages/server/src/tests/utilities/api/index.ts
@@ -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)
}
}
diff --git a/packages/server/src/tests/utilities/api/row.ts b/packages/server/src/tests/utilities/api/row.ts
index 20b1d6f9ee..3d4cf6c82c 100644
--- a/packages/server/src/tests/utilities/api/row.ts
+++ b/packages/server/src/tests/utilities/api/row.ts
@@ -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 => {
+ ): Promise => {
const request = this.request
.post(`/api/${sourceId}/search`)
.set(this.config.defaultHeaders())
diff --git a/packages/server/src/tests/utilities/api/user.ts b/packages/server/src/tests/utilities/api/user.ts
new file mode 100644
index 0000000000..2ed23c0461
--- /dev/null
+++ b/packages/server/src/tests/utilities/api/user.ts
@@ -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 => {
+ 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 => {
+ 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 => {
+ 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 => {
+ 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 => {
+ 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
+ }
+}
diff --git a/packages/server/src/utilities/rowProcessor/index.ts b/packages/server/src/utilities/rowProcessor/index.ts
index 098962c646..0e53422a4f 100644
--- a/packages/server/src/utilities/rowProcessor/index.ts
+++ b/packages/server/src/utilities/rowProcessor/index.ts
@@ -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(
})
: 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(
}
// 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(
diff --git a/packages/server/src/utilities/rowProcessor/utils.ts b/packages/server/src/utilities/rowProcessor/utils.ts
index 48697af6a9..9eb725dd7c 100644
--- a/packages/server/src/utilities/rowProcessor/utils.ts
+++ b/packages/server/src/utilities/rowProcessor/utils.ts
@@ -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(
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(
+ 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]
}
diff --git a/packages/server/src/utilities/users.ts b/packages/server/src/utilities/users.ts
index bbc1370355..73b2f48b15 100644
--- a/packages/server/src/utilities/users.ts
+++ b/packages/server/src/utilities/users.ts
@@ -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 {
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(userId)
+ delete metadata.csrfToken
} catch (err) {
// it is fine if there is no user metadata yet
}
- delete metadata.csrfToken
return {
...metadata,
...global,
diff --git a/packages/types/src/api/web/app/index.ts b/packages/types/src/api/web/app/index.ts
index f5b876009b..cb1cea2b08 100644
--- a/packages/types/src/api/web/app/index.ts
+++ b/packages/types/src/api/web/app/index.ts
@@ -6,3 +6,4 @@ export * from "./rows"
export * from "./table"
export * from "./permission"
export * from "./attachment"
+export * from "./user"
diff --git a/packages/types/src/api/web/app/user.ts b/packages/types/src/api/web/app/user.ts
new file mode 100644
index 0000000000..7faec83e9c
--- /dev/null
+++ b/packages/types/src/api/web/app/user.ts
@@ -0,0 +1,9 @@
+import { ContextUserMetadata } from "../../../"
+
+export type FetchUserMetadataResponse = ContextUserMetadata[]
+export type FindUserMetadataResponse = ContextUserMetadata
+
+export interface SetFlagRequest {
+ flag: string
+ value: any
+}
diff --git a/packages/types/src/documents/account/flag.ts b/packages/types/src/documents/account/flag.ts
new file mode 100644
index 0000000000..a214348fe7
--- /dev/null
+++ b/packages/types/src/documents/account/flag.ts
@@ -0,0 +1,5 @@
+import { Document } from "../../"
+
+export interface Flags extends Document {
+ [key: string]: any
+}
diff --git a/packages/types/src/documents/account/index.ts b/packages/types/src/documents/account/index.ts
index 663fb91b58..1e0c800f39 100644
--- a/packages/types/src/documents/account/index.ts
+++ b/packages/types/src/documents/account/index.ts
@@ -1,2 +1,3 @@
export * from "./account"
export * from "./user"
+export * from "./flag"
diff --git a/qa-core/package.json b/qa-core/package.json
index cfccd5e650..87172d2ed9 100644
--- a/qa-core/package.json
+++ b/qa-core/package.json
@@ -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",