1
0
Fork 0
mirror of synced 2024-09-08 13:41:09 +12:00

Merge branch 'master' into fix-section-crash

This commit is contained in:
Michael Drury 2023-11-08 18:51:08 +00:00 committed by GitHub
commit c1018915d7
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
35 changed files with 607 additions and 379 deletions

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

@ -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

@ -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

@ -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 }
} }

View file

@ -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

@ -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

@ -27,51 +27,59 @@ interface KoaRateLimitOptions {
} }
const PREFIX = "/api/public/v1" 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 { // type can't be known - untyped libraries
if (!env.API_REQ_LIMIT_PER_SEC) { let limiter: any, rateLimitStore: any
return DEFAULT_API_REQ_LIMIT_PER_SEC if (!env.DISABLE_RATE_LIMITING) {
} // allow a lot more requests when in test
return parseInt(env.API_REQ_LIMIT_PER_SEC) const DEFAULT_API_REQ_LIMIT_PER_SEC = env.isTest() ? 100 : 10
}
let rateLimitStore: any = null function getApiLimitPerSecond(): number {
if (!env.isTest()) { if (!env.API_REQ_LIMIT_PER_SEC) {
const { password, host, port } = redis.utils.getRedisConnectionDetails() return DEFAULT_API_REQ_LIMIT_PER_SEC
let options: KoaRateLimitOptions = { }
socket: { return parseInt(env.API_REQ_LIMIT_PER_SEC)
host: host,
port: port,
},
} }
if (password) { if (!env.isTest()) {
options.password = password const { password, host, port } = redis.utils.getRedisConnectionDetails()
} let options: KoaRateLimitOptions = {
socket: {
host: host,
port: port,
},
}
if (!env.REDIS_CLUSTERED) { if (password) {
// Can't set direct redis db in clustered env options.password = password
options.database = SelectableDatabase.RATE_LIMITING }
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) // rate limiting, allows for 2 requests per second
RateLimit.defaultOptions({ limiter = RateLimit.middleware({
store: rateLimitStore, 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({ const publicRouter = new Router({
prefix: PREFIX, prefix: PREFIX,
}) })
publicRouter.use(limiter) if (limiter) {
publicRouter.use(limiter)
}
function addMiddleware( function addMiddleware(
endpoints: any, endpoints: any,

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,
@ -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)
})
})
}) })

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

@ -2,7 +2,6 @@ import LinkController from "./LinkController"
import { import {
IncludeDocs, IncludeDocs,
getLinkDocuments, getLinkDocuments,
createLinkView,
getUniqueByProp, getUniqueByProp,
getRelatedTableForField, getRelatedTableForField,
getLinkedTableIDs, getLinkedTableIDs,

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

@ -61,6 +61,7 @@ const environment = {
ALLOW_DEV_AUTOMATIONS: process.env.ALLOW_DEV_AUTOMATIONS, ALLOW_DEV_AUTOMATIONS: process.env.ALLOW_DEV_AUTOMATIONS,
DISABLE_THREADING: process.env.DISABLE_THREADING, DISABLE_THREADING: process.env.DISABLE_THREADING,
DISABLE_AUTOMATION_LOGS: process.env.DISABLE_AUTOMATION_LOGS, DISABLE_AUTOMATION_LOGS: process.env.DISABLE_AUTOMATION_LOGS,
DISABLE_RATE_LIMITING: process.env.DISABLE_RATE_LIMITING,
MULTI_TENANCY: process.env.MULTI_TENANCY, MULTI_TENANCY: process.env.MULTI_TENANCY,
ENABLE_ANALYTICS: process.env.ENABLE_ANALYTICS, ENABLE_ANALYTICS: process.env.ENABLE_ANALYTICS,
SELF_HOSTED: process.env.SELF_HOSTED, SELF_HOSTED: process.env.SELF_HOSTED,

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

@ -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

@ -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

@ -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

@ -20,7 +20,7 @@
"test:self:ci": "yarn run test --testPathIgnorePatterns=\\.integration\\. \\.cloud\\. \\.licensing\\.", "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: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", "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": { "devDependencies": {
"@budibase/types": "^2.3.17", "@budibase/types": "^2.3.17",