Merge branch 'master' into Fix-user-access-roles-from-displaying-business

This commit is contained in:
Conor Webb 2024-09-12 11:10:57 +01:00 committed by GitHub
commit 423f76950f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
33 changed files with 754 additions and 568 deletions

View File

@ -117,9 +117,9 @@ jobs:
- name: Test
run: |
if ${{ env.ONLY_AFFECTED_TASKS }}; then
yarn test --ignore=@budibase/worker --ignore=@budibase/server --since=${{ env.NX_BASE_BRANCH }}
yarn test --ignore=@budibase/worker --ignore=@budibase/server --ignore @budibase/account-portal-server --since=${{ env.NX_BASE_BRANCH }}
else
yarn test --ignore=@budibase/worker --ignore=@budibase/server
yarn test --ignore=@budibase/worker --ignore=@budibase/server --ignore @budibase/account-portal-server
fi
test-worker:

Binary file not shown.

Binary file not shown.

View File

@ -42,7 +42,7 @@ services:
couchdb-service:
container_name: budi-couchdb3-dev
restart: on-failure
image: budibase/couchdb:v3.3.3
image: budibase/couchdb:v3.3.3-sqs-v2.1.1
environment:
- COUCHDB_PASSWORD=${COUCH_DB_PASSWORD}
- COUCHDB_USER=${COUCH_DB_USER}

View File

@ -1,6 +1,6 @@
{
"$schema": "node_modules/lerna/schemas/lerna-schema.json",
"version": "2.31.8",
"version": "2.32.0",
"npmClient": "yarn",
"packages": [
"packages/*",

View File

@ -76,7 +76,7 @@
"build:docker:single": "./scripts/build-single-image.sh",
"build:docker:single:sqs": "./scripts/build-single-image-sqs.sh",
"build:docker:dependencies": "docker build -f hosting/dependencies/Dockerfile -t budibase/dependencies:latest ./hosting",
"publish:docker:couch": "docker buildx build --platform linux/arm64,linux/amd64 -f hosting/couchdb/Dockerfile -t budibase/couchdb:latest -t budibase/couchdb:v3.3.3 --push ./hosting/couchdb",
"publish:docker:couch": "docker buildx build --platform linux/arm64,linux/amd64 -f hosting/couchdb/Dockerfile -t budibase/couchdb:latest -t budibase/couchdb:v3.3.3 -t budibase/couchdb:v3.3.3-sqs-v2.1.1 --push ./hosting/couchdb",
"publish:docker:dependencies": "docker buildx build --platform linux/arm64,linux/amd64 -f hosting/dependencies/Dockerfile -t budibase/dependencies:latest -t budibase/dependencies:v3.2.1 --push ./hosting",
"release:helm": "node scripts/releaseHelmChart",
"env:multi:enable": "lerna run --stream env:multi:enable",

@ -1 +1 @@
Subproject commit 048c37ecd921340614bf61a76a774aaa46176569
Subproject commit 7899d07904d89d48954dd500da7b5dec32b781dd

View File

@ -63,14 +63,25 @@ async function populateUsersFromDB(
* If not present fallback to loading the user directly and re-caching.
* @param userId the id of the user to get
* @param tenantId the tenant of the user to get
* @param email the email of the user to populate from account if needed
* @param populateUser function to provide the user for re-caching. default to couch db
* @returns
*/
export async function getUser(
userId: string,
tenantId?: string,
populateUser?: (userId: string, tenantId: string) => Promise<User>
) {
export async function getUser({
userId,
tenantId,
email,
populateUser,
}: {
userId: string
email?: string
tenantId?: string
populateUser?: (
userId: string,
tenantId: string,
email?: string
) => Promise<User>
}) {
if (!populateUser) {
populateUser = populateFromDB
}
@ -85,7 +96,7 @@ export async function getUser(
// try cache
let user: User = await client.get(userId)
if (!user) {
user = await populateUser(userId, tenantId)
user = await populateUser(userId, tenantId, email)
await client.store(userId, user, EXPIRY_SECONDS)
}
if (user && !user.tenantId && tenantId) {

View File

@ -3,6 +3,7 @@ export * as processors from "./processors"
export * as analytics from "./analytics"
export { default as identification } from "./identification"
export * as backfillCache from "./backfill"
export { publishEvent } from "./events"
import { processors } from "./processors"

View File

@ -43,7 +43,11 @@ function finalise(ctx: any, opts: FinaliseOpts = {}) {
async function checkApiKey(
apiKey: string,
populateUser?: (userId: string, tenantId: string) => Promise<User>
populateUser?: (
userId: string,
tenantId: string,
email?: string
) => Promise<User>
) {
// check both the primary and the fallback internal api keys
// this allows for rotation
@ -70,7 +74,11 @@ async function checkApiKey(
if (userId) {
return {
valid: true,
user: await getUser(userId, tenantId, populateUser),
user: await getUser({
userId,
tenantId,
populateUser,
}),
}
} else {
throw new InvalidAPIKeyError()
@ -123,13 +131,18 @@ export default function (
// getting session handles error checking (if session exists etc)
session = await getSession(userId, sessionId)
if (opts && opts.populateUser) {
user = await getUser(
user = await getUser({
userId,
session.tenantId,
opts.populateUser(ctx)
)
tenantId: session.tenantId,
email: session.email,
populateUser: opts.populateUser(ctx),
})
} else {
user = await getUser(userId, session.tenantId)
user = await getUser({
userId,
tenantId: session.tenantId,
email: session.email,
})
}
// @ts-ignore
user.csrfToken = session.csrfToken
@ -148,7 +161,11 @@ export default function (
}
// this is an internal request, no user made it
if (!authenticated && apiKey) {
const populateUser = opts.populateUser ? opts.populateUser(ctx) : null
const populateUser: (
userId: string,
tenantId: string,
email?: string
) => Promise<User> = opts.populateUser ? opts.populateUser(ctx) : null
const { valid, user: foundUser } = await checkApiKey(
apiKey,
populateUser

View File

@ -862,6 +862,21 @@ class InternalBuilder {
return withSchema
}
private buildJsonField(field: string): string {
const parts = field.split(".")
let tableField: string, unaliased: string
if (parts.length > 1) {
const alias = parts.shift()!
unaliased = parts.join(".")
tableField = `${this.quote(alias)}.${this.quote(unaliased)}`
} else {
unaliased = parts.join(".")
tableField = this.quote(unaliased)
}
const separator = this.client === SqlClient.ORACLE ? " VALUE " : ","
return `'${unaliased}'${separator}${tableField}`
}
addJsonRelationships(
query: Knex.QueryBuilder,
fromTable: string,
@ -871,27 +886,6 @@ class InternalBuilder {
const knex = this.knex
const { resource, tableAliases: aliases, endpoint } = this.query
const fields = resource?.fields || []
const jsonField = (field: string) => {
const parts = field.split(".")
let tableField: string, unaliased: string
if (parts.length > 1) {
const alias = parts.shift()!
unaliased = parts.join(".")
tableField = `${this.quote(alias)}.${this.quote(unaliased)}`
} else {
unaliased = parts.join(".")
tableField = this.quote(unaliased)
}
let separator = ","
switch (sqlClient) {
case SqlClient.ORACLE:
separator = " VALUE "
break
case SqlClient.MS_SQL:
separator = ":"
}
return `'${unaliased}'${separator}${tableField}`
}
for (let relationship of relationships) {
const {
tableName: toTable,
@ -921,7 +915,7 @@ class InternalBuilder {
)
}
const fieldList: string = relationshipFields
.map(field => jsonField(field))
.map(field => this.buildJsonField(field))
.join(",")
// SQL Server uses TOP - which performs a little differently to the normal LIMIT syntax
// it reduces the result set rather than limiting how much data it filters over
@ -1073,43 +1067,6 @@ class InternalBuilder {
return query
}
addRelationships(
query: Knex.QueryBuilder,
fromTable: string,
relationships: RelationshipsJson[]
): Knex.QueryBuilder {
const tableSets: Record<string, [RelationshipsJson]> = {}
// aggregate into table sets (all the same to tables)
for (let relationship of relationships) {
const keyObj: { toTable: string; throughTable: string | undefined } = {
toTable: relationship.tableName,
throughTable: undefined,
}
if (relationship.through) {
keyObj.throughTable = relationship.through
}
const key = JSON.stringify(keyObj)
if (tableSets[key]) {
tableSets[key].push(relationship)
} else {
tableSets[key] = [relationship]
}
}
for (let [key, relationships] of Object.entries(tableSets)) {
const { toTable, throughTable } = JSON.parse(key)
query = this.addJoin(
query,
{
from: fromTable,
to: toTable,
through: throughTable,
},
relationships
)
}
return query
}
qualifiedKnex(opts?: { alias?: string | boolean }): Knex.QueryBuilder {
let alias = this.query.tableAliases?.[this.query.endpoint.entityId]
if (opts?.alias === false) {
@ -1193,8 +1150,7 @@ class InternalBuilder {
if (!primary) {
throw new Error("Primary key is required for upsert")
}
const ret = query.insert(parsedBody).onConflict(primary).merge()
return ret
return query.insert(parsedBody).onConflict(primary).merge()
} else if (
this.client === SqlClient.MS_SQL ||
this.client === SqlClient.ORACLE
@ -1253,12 +1209,29 @@ class InternalBuilder {
if (!counting) {
query = this.addSorting(query)
}
// handle joins
if (relationships) {
query = this.addJsonRelationships(query, tableName, relationships)
}
return this.addFilters(query, filters, { relationship: true })
query = this.addFilters(query, filters, { relationship: true })
// handle relationships with a CTE for all others
if (relationships?.length) {
const mainTable =
this.query.tableAliases?.[this.query.endpoint.entityId] ||
this.query.endpoint.entityId
const cte = this.addSorting(
this.knex
.with("paginated", query)
.select(this.generateSelectStatement())
.from({
[mainTable]: "paginated",
})
)
// add JSON aggregations attached to the CTE
return this.addJsonRelationships(cte, tableName, relationships)
}
// no relationships found - return query
else {
return query
}
}
update(opts: QueryOptions): Knex.QueryBuilder {

View File

@ -2,6 +2,7 @@ import { it, expect, describe, vi } from "vitest"
import AISettings from "./index.svelte"
import { render } from "@testing-library/svelte"
import { admin, licensing } from "stores/portal"
import { notifications } from "@budibase/bbui"
vi.spyOn(notifications, "error").mockImplementation(vi.fn)
vi.spyOn(notifications, "success").mockImplementation(vi.fn)

View File

@ -1,4 +1,4 @@
FROM node:20-slim
FROM node:20-alpine
LABEL com.centurylinklabs.watchtower.lifecycle.pre-check="scripts/watchtower-hooks/pre-check.sh"
LABEL com.centurylinklabs.watchtower.lifecycle.pre-update="scripts/watchtower-hooks/pre-update.sh"
@ -15,37 +15,35 @@ ENV POSTHOG_TOKEN=phc_bIjZL7oh2GEUd2vqvTBH8WvrX0fWTFQMs6H5KQxiUxU
ENV ACCOUNT_PORTAL_URL=https://account.budibase.app
ENV TOP_LEVEL_PATH=/
# handle node-gyp
RUN apt-get update \
&& apt-get install -y --no-install-recommends g++ make python3 jq
RUN yarn global add pm2
# handle node-gyp and install postgres client for pg_dump utils
RUN apk add --no-cache \
g++ \
make \
python3 \
jq \
bash \
postgresql-client \
git
# Install postgres client for pg_dump utils
RUN apt update && apt upgrade -y \
&& apt install software-properties-common apt-transport-https curl gpg -y \
&& curl -fsSl https://www.postgresql.org/media/keys/ACCC4CF8.asc | gpg --dearmor | tee /usr/share/keyrings/postgresql.gpg > /dev/null \
&& echo deb [arch=amd64,arm64,ppc64el signed-by=/usr/share/keyrings/postgresql.gpg] http://apt.postgresql.org/pub/repos/apt/ $(lsb_release -cs)-pgdg main | tee /etc/apt/sources.list.d/postgresql.list \
&& apt update -y \
&& apt install postgresql-client-15 -y \
&& apt remove software-properties-common apt-transport-https curl gpg -y
RUN yarn global add pm2
WORKDIR /
COPY scripts/removeWorkspaceDependencies.sh scripts/removeWorkspaceDependencies.sh
RUN chmod +x ./scripts/removeWorkspaceDependencies.sh
WORKDIR /app
COPY packages/server/package.json .
COPY packages/server/dist/yarn.lock .
COPY scripts/removeWorkspaceDependencies.sh scripts/removeWorkspaceDependencies.sh
RUN chmod +x ./scripts/removeWorkspaceDependencies.sh
RUN ./scripts/removeWorkspaceDependencies.sh package.json
RUN ./scripts/removeWorkspaceDependencies.sh package.json
# Install yarn packages with caching
RUN --mount=type=cache,target=/root/.yarn YARN_CACHE_FOLDER=/root/.yarn yarn install --production=true --network-timeout 1000000 \
# Remove unneeded data from file system to reduce image size
&& yarn cache clean && apt-get remove -y --purge --auto-remove g++ make python jq \
&& yarn cache clean \
&& apk del g++ make python3 jq \
&& rm -rf /tmp/* /root/.node-gyp /usr/local/lib/node_modules/npm/node_modules/node-gyp
COPY packages/server/dist/ dist/
@ -69,7 +67,7 @@ EXPOSE 4001
# due to this causing yarn to stop installing dev dependencies
# which are actually needed to get this environment up and running
ENV NODE_ENV=production
# this is required for isolated-vm to work on Node 20+
# This is required for isolated-vm to work on Node 20+
ENV NODE_OPTIONS="--no-node-snapshot"
ENV CLUSTER_MODE=${CLUSTER_MODE}
ENV TOP_LEVEL_PATH=/app

View File

@ -145,35 +145,32 @@ export function basicProcessing({
: typeof value === "string"
? JSON.parse(value)
: undefined
if (array) {
if (array && Array.isArray(array)) {
thisRow[col] = array
// make sure all of them have an _id
if (Array.isArray(thisRow[col])) {
const sortField =
relatedTable.primaryDisplay || relatedTable.primary![0]!
thisRow[col] = (thisRow[col] as Row[])
.map(relatedRow =>
basicProcessing({
row: relatedRow,
table: relatedTable,
tables,
isLinked: false,
sqs,
})
)
.sort((a, b) => {
const aField = a?.[sortField],
bField = b?.[sortField]
if (!aField) {
return 1
} else if (!bField) {
return -1
}
return aField.localeCompare
? aField.localeCompare(bField)
: aField - bField
const sortField = relatedTable.primaryDisplay || relatedTable.primary![0]!
thisRow[col] = (thisRow[col] as Row[])
.map(relatedRow =>
basicProcessing({
row: relatedRow,
table: relatedTable,
tables,
isLinked: false,
sqs,
})
}
)
.sort((a, b) => {
const aField = a?.[sortField],
bField = b?.[sortField]
if (!aField) {
return 1
} else if (!bField) {
return -1
}
return aField.localeCompare
? aField.localeCompare(bField)
: aField - bField
})
}
}
return fixJsonTypes(thisRow, table)

View File

@ -832,12 +832,13 @@ describe.each(
},
})
expect(res).toHaveLength(1)
expect(res[0]).toEqual(
expect.objectContaining({
id: 2,
name: "two",
})
)
expect(res[0]).toEqual({
id: 2,
name: "two",
// the use of table.* introduces the possibility of nulls being returned
birthday: null,
number: null,
})
})
// this parameter really only impacts SQL queries

View File

@ -2907,6 +2907,28 @@ describe.each([
'Invalid body - "query.$and.conditions[1].$and.conditions" is required'
)
})
it("returns no rows when onEmptyFilter set to none", async () => {
await expectSearch({
query: {
onEmptyFilter: EmptyFilterOption.RETURN_NONE,
$and: {
conditions: [{ equal: { name: "" } }],
},
},
}).toFindNothing()
})
it("returns all rows when onEmptyFilter set to all", async () => {
await expectSearch({
query: {
onEmptyFilter: EmptyFilterOption.RETURN_ALL,
$and: {
conditions: [{ equal: { name: "" } }],
},
},
}).toHaveLength(4)
})
})
!isLucene &&
@ -3035,5 +3057,27 @@ describe.each([
},
}).toContainExactly([{ age: 1, name: "Jane" }])
})
it("returns no rows when onEmptyFilter set to none", async () => {
await expectSearch({
query: {
onEmptyFilter: EmptyFilterOption.RETURN_NONE,
$or: {
conditions: [{ equal: { name: "" } }],
},
},
}).toFindNothing()
})
it("returns all rows when onEmptyFilter set to all", async () => {
await expectSearch({
query: {
onEmptyFilter: EmptyFilterOption.RETURN_ALL,
$or: {
conditions: [{ equal: { name: "" } }],
},
},
}).toHaveLength(4)
})
})
})

View File

@ -0,0 +1,199 @@
import * as automation from "../../index"
import * as setup from "../utilities"
import { Table, AutomationStatus } from "@budibase/types"
import { createAutomationBuilder } from "../utilities/AutomationTestBuilder"
describe("Branching automations", () => {
let config = setup.getConfig(),
table: Table
beforeEach(async () => {
await automation.init()
await config.init()
table = await config.createTable()
await config.createRow()
})
afterAll(setup.afterAll)
it("should run a multiple nested branching automation", async () => {
const builder = createAutomationBuilder({
name: "Test Trigger with Loop and Create Row",
})
const results = await builder
.appAction({ fields: {} })
.serverLog({ text: "Starting automation" })
.branch({
topLevelBranch1: {
steps: stepBuilder =>
stepBuilder.serverLog({ text: "Branch 1" }).branch({
branch1: {
steps: stepBuilder =>
stepBuilder.serverLog({ text: "Branch 1.1" }),
condition: {
equal: { "steps.1.success": true },
},
},
branch2: {
steps: stepBuilder =>
stepBuilder.serverLog({ text: "Branch 1.2" }),
condition: {
equal: { "steps.1.success": false },
},
},
}),
condition: {
equal: { "steps.1.success": true },
},
},
topLevelBranch2: {
steps: stepBuilder => stepBuilder.serverLog({ text: "Branch 2" }),
condition: {
equal: { "steps.1.success": false },
},
},
})
.run()
expect(results.steps[3].outputs.status).toContain("branch1 branch taken")
expect(results.steps[4].outputs.message).toContain("Branch 1.1")
})
it("should execute correct branch based on string equality", async () => {
const builder = createAutomationBuilder({
name: "String Equality Branching",
})
const results = await builder
.appAction({ fields: { status: "active" } })
.branch({
activeBranch: {
steps: stepBuilder => stepBuilder.serverLog({ text: "Active user" }),
condition: {
equal: { "trigger.fields.status": "active" },
},
},
inactiveBranch: {
steps: stepBuilder =>
stepBuilder.serverLog({ text: "Inactive user" }),
condition: {
equal: { "trigger.fields.status": "inactive" },
},
},
})
.run()
expect(results.steps[0].outputs.status).toContain(
"activeBranch branch taken"
)
expect(results.steps[1].outputs.message).toContain("Active user")
})
it("should handle multiple conditions with AND operator", async () => {
const builder = createAutomationBuilder({
name: "Multiple AND Conditions Branching",
})
const results = await builder
.appAction({ fields: { status: "active", role: "admin" } })
.branch({
activeAdminBranch: {
steps: stepBuilder =>
stepBuilder.serverLog({ text: "Active admin user" }),
condition: {
$and: {
conditions: [
{ equal: { "trigger.fields.status": "active" } },
{ equal: { "trigger.fields.role": "admin" } },
],
},
},
},
otherBranch: {
steps: stepBuilder => stepBuilder.serverLog({ text: "Other user" }),
condition: {
notEqual: { "trigger.fields.status": "active" },
},
},
})
.run()
expect(results.steps[1].outputs.message).toContain("Active admin user")
})
it("should handle multiple conditions with OR operator", async () => {
const builder = createAutomationBuilder({
name: "Multiple OR Conditions Branching",
})
const results = await builder
.appAction({ fields: { status: "test", role: "user" } })
.branch({
specialBranch: {
steps: stepBuilder => stepBuilder.serverLog({ text: "Special user" }),
condition: {
$or: {
conditions: [
{ equal: { "trigger.fields.status": "test" } },
{ equal: { "trigger.fields.role": "admin" } },
],
},
},
},
regularBranch: {
steps: stepBuilder => stepBuilder.serverLog({ text: "Regular user" }),
condition: {
$and: {
conditions: [
{ notEqual: { "trigger.fields.status": "active" } },
{ notEqual: { "trigger.fields.role": "admin" } },
],
},
},
},
})
.run()
expect(results.steps[1].outputs.message).toContain("Special user")
})
it("should stop the branch automation when no conditions are met", async () => {
const builder = createAutomationBuilder({
name: "Multiple OR Conditions Branching",
})
const results = await builder
.appAction({ fields: { status: "test", role: "user" } })
.createRow({ row: { name: "Test", tableId: table._id } })
.branch({
specialBranch: {
steps: stepBuilder => stepBuilder.serverLog({ text: "Special user" }),
condition: {
$or: {
conditions: [
{ equal: { "trigger.fields.status": "new" } },
{ equal: { "trigger.fields.role": "admin" } },
],
},
},
},
regularBranch: {
steps: stepBuilder => stepBuilder.serverLog({ text: "Regular user" }),
condition: {
$and: {
conditions: [
{ equal: { "trigger.fields.status": "active" } },
{ equal: { "trigger.fields.role": "admin" } },
],
},
},
},
})
.serverLog({ text: "Test" })
.run()
expect(results.steps[1].outputs.status).toEqual(
AutomationStatus.NO_CONDITION_MET
)
expect(results.steps[2]).toBeUndefined()
})
})

View File

@ -0,0 +1,245 @@
import * as automation from "../../index"
import * as setup from "../utilities"
import {
Table,
LoopStepType,
CreateRowStepOutputs,
ServerLogStepOutputs,
} from "@budibase/types"
import { createAutomationBuilder } from "../utilities/AutomationTestBuilder"
describe("Loop automations", () => {
let config = setup.getConfig(),
table: Table
beforeEach(async () => {
await automation.init()
await config.init()
table = await config.createTable()
await config.createRow()
})
afterAll(setup.afterAll)
it("should run an automation with a trigger, loop, and create row step", async () => {
const builder = createAutomationBuilder({
name: "Test Trigger with Loop and Create Row",
})
const results = await builder
.rowSaved(
{ tableId: table._id! },
{
row: {
name: "Trigger Row",
description: "This row triggers the automation",
},
id: "1234",
revision: "1",
}
)
.loop({
option: LoopStepType.ARRAY,
binding: [1, 2, 3],
})
.createRow({
row: {
name: "Item {{ loop.currentItem }}",
description: "Created from loop",
tableId: table._id,
},
})
.run()
expect(results.trigger).toBeDefined()
expect(results.steps).toHaveLength(1)
expect(results.steps[0].outputs.iterations).toBe(3)
expect(results.steps[0].outputs.items).toHaveLength(3)
results.steps[0].outputs.items.forEach((output: any, index: number) => {
expect(output).toMatchObject({
success: true,
row: {
name: `Item ${index + 1}`,
description: "Created from loop",
},
})
})
})
it("should run an automation where a loop step is between two normal steps to ensure context correctness", async () => {
const builder = createAutomationBuilder({
name: "Test Trigger with Loop and Create Row",
})
const results = await builder
.rowSaved(
{ tableId: table._id! },
{
row: {
name: "Trigger Row",
description: "This row triggers the automation",
},
id: "1234",
revision: "1",
}
)
.queryRows({
tableId: table._id!,
})
.loop({
option: LoopStepType.ARRAY,
binding: [1, 2, 3],
})
.serverLog({ text: "Message {{loop.currentItem}}" })
.serverLog({ text: "{{steps.1.rows.0._id}}" })
.run()
results.steps[1].outputs.items.forEach(
(output: ServerLogStepOutputs, index: number) => {
expect(output).toMatchObject({
success: true,
})
expect(output.message).toContain(`Message ${index + 1}`)
}
)
expect(results.steps[2].outputs.message).toContain("ro_ta")
})
it("if an incorrect type is passed to the loop it should return an error", async () => {
const builder = createAutomationBuilder({
name: "Test Loop error",
})
const results = await builder
.appAction({ fields: {} })
.loop({
option: LoopStepType.ARRAY,
binding: "1, 2, 3",
})
.serverLog({ text: "Message {{loop.currentItem}}" })
.run()
expect(results.steps[0].outputs).toEqual({
success: false,
status: "INCORRECT_TYPE",
})
})
it("ensure the loop stops if the failure condition is reached", async () => {
const builder = createAutomationBuilder({
name: "Test Loop error",
})
const results = await builder
.appAction({ fields: {} })
.loop({
option: LoopStepType.ARRAY,
binding: ["test", "test2", "test3"],
failure: "test2",
})
.serverLog({ text: "Message {{loop.currentItem}}" })
.run()
expect(results.steps[0].outputs).toEqual(
expect.objectContaining({
status: "FAILURE_CONDITION_MET",
success: false,
})
)
})
it("should run an automation where a loop is successfully run twice", async () => {
const builder = createAutomationBuilder({
name: "Test Trigger with Loop and Create Row",
})
const results = await builder
.rowSaved(
{ tableId: table._id! },
{
row: {
name: "Trigger Row",
description: "This row triggers the automation",
},
id: "1234",
revision: "1",
}
)
.loop({
option: LoopStepType.ARRAY,
binding: [1, 2, 3],
})
.createRow({
row: {
name: "Item {{ loop.currentItem }}",
description: "Created from loop",
tableId: table._id,
},
})
.loop({
option: LoopStepType.STRING,
binding: "Message 1,Message 2,Message 3",
})
.serverLog({ text: "{{loop.currentItem}}" })
.run()
expect(results.trigger).toBeDefined()
expect(results.steps).toHaveLength(2)
expect(results.steps[0].outputs.iterations).toBe(3)
expect(results.steps[0].outputs.items).toHaveLength(3)
results.steps[0].outputs.items.forEach(
(output: CreateRowStepOutputs, index: number) => {
expect(output).toMatchObject({
success: true,
row: {
name: `Item ${index + 1}`,
description: "Created from loop",
},
})
}
)
expect(results.steps[1].outputs.iterations).toBe(3)
expect(results.steps[1].outputs.items).toHaveLength(3)
results.steps[1].outputs.items.forEach(
(output: ServerLogStepOutputs, index: number) => {
expect(output).toMatchObject({
success: true,
})
expect(output.message).toContain(`Message ${index + 1}`)
}
)
})
it("should run an automation where a loop is used twice to ensure context correctness further down the tree", async () => {
const builder = createAutomationBuilder({
name: "Test Trigger with Loop and Create Row",
})
const results = await builder
.appAction({ fields: {} })
.loop({
option: LoopStepType.ARRAY,
binding: [1, 2, 3],
})
.serverLog({ text: "Message {{loop.currentItem}}" })
.serverLog({ text: "{{steps.1.iterations}}" })
.loop({
option: LoopStepType.ARRAY,
binding: [1, 2, 3],
})
.serverLog({ text: "{{loop.currentItem}}" })
.serverLog({ text: "{{steps.3.iterations}}" })
.run()
// We want to ensure that bindings are corr
expect(results.steps[1].outputs.message).toContain("- 3")
expect(results.steps[3].outputs.message).toContain("- 3")
})
})

View File

@ -1,397 +1,19 @@
import * as automation from "../../index"
import * as setup from "../utilities"
import {
Table,
LoopStepType,
CreateRowStepOutputs,
ServerLogStepOutputs,
FieldType,
} from "@budibase/types"
import { LoopStepType, FieldType } from "@budibase/types"
import { createAutomationBuilder } from "../utilities/AutomationTestBuilder"
import { DatabaseName } from "../../../integrations/tests/utils"
describe("Automation Scenarios", () => {
let config = setup.getConfig(),
table: Table
let config = setup.getConfig()
beforeEach(async () => {
await automation.init()
await config.init()
table = await config.createTable()
await config.createRow()
})
afterAll(setup.afterAll)
describe("Branching automations", () => {
it("should run a multiple nested branching automation", async () => {
const builder = createAutomationBuilder({
name: "Test Trigger with Loop and Create Row",
})
const results = await builder
.appAction({ fields: {} })
.serverLog({ text: "Starting automation" })
.branch({
topLevelBranch1: {
steps: stepBuilder =>
stepBuilder.serverLog({ text: "Branch 1" }).branch({
branch1: {
steps: stepBuilder =>
stepBuilder.serverLog({ text: "Branch 1.1" }),
condition: {
equal: { "steps.1.success": true },
},
},
branch2: {
steps: stepBuilder =>
stepBuilder.serverLog({ text: "Branch 1.2" }),
condition: {
equal: { "steps.1.success": false },
},
},
}),
condition: {
equal: { "steps.1.success": true },
},
},
topLevelBranch2: {
steps: stepBuilder => stepBuilder.serverLog({ text: "Branch 2" }),
condition: {
equal: { "steps.1.success": false },
},
},
})
.run()
expect(results.steps[3].outputs.status).toContain("branch1 branch taken")
expect(results.steps[4].outputs.message).toContain("Branch 1.1")
})
it("should execute correct branch based on string equality", async () => {
const builder = createAutomationBuilder({
name: "String Equality Branching",
})
const results = await builder
.appAction({ fields: { status: "active" } })
.branch({
activeBranch: {
steps: stepBuilder =>
stepBuilder.serverLog({ text: "Active user" }),
condition: {
equal: { "trigger.fields.status": "active" },
},
},
inactiveBranch: {
steps: stepBuilder =>
stepBuilder.serverLog({ text: "Inactive user" }),
condition: {
equal: { "trigger.fields.status": "inactive" },
},
},
})
.run()
expect(results.steps[0].outputs.status).toContain(
"activeBranch branch taken"
)
expect(results.steps[1].outputs.message).toContain("Active user")
})
it("should handle multiple conditions with AND operator", async () => {
const builder = createAutomationBuilder({
name: "Multiple AND Conditions Branching",
})
const results = await builder
.appAction({ fields: { status: "active", role: "admin" } })
.branch({
activeAdminBranch: {
steps: stepBuilder =>
stepBuilder.serverLog({ text: "Active admin user" }),
condition: {
$and: {
conditions: [
{ equal: { "trigger.fields.status": "active" } },
{ equal: { "trigger.fields.role": "admin" } },
],
},
},
},
otherBranch: {
steps: stepBuilder => stepBuilder.serverLog({ text: "Other user" }),
condition: {
notEqual: { "trigger.fields.status": "active" },
},
},
})
.run()
expect(results.steps[1].outputs.message).toContain("Active admin user")
})
it("should handle multiple conditions with OR operator", async () => {
const builder = createAutomationBuilder({
name: "Multiple OR Conditions Branching",
})
const results = await builder
.appAction({ fields: { status: "test", role: "user" } })
.branch({
specialBranch: {
steps: stepBuilder =>
stepBuilder.serverLog({ text: "Special user" }),
condition: {
$or: {
conditions: [
{ equal: { "trigger.fields.status": "test" } },
{ equal: { "trigger.fields.role": "admin" } },
],
},
},
},
regularBranch: {
steps: stepBuilder =>
stepBuilder.serverLog({ text: "Regular user" }),
condition: {
$and: {
conditions: [
{ notEqual: { "trigger.fields.status": "active" } },
{ notEqual: { "trigger.fields.role": "admin" } },
],
},
},
},
})
.run()
expect(results.steps[1].outputs.message).toContain("Special user")
})
})
describe("Loop automations", () => {
it("should run an automation with a trigger, loop, and create row step", async () => {
const builder = createAutomationBuilder({
name: "Test Trigger with Loop and Create Row",
})
const results = await builder
.rowSaved(
{ tableId: table._id! },
{
row: {
name: "Trigger Row",
description: "This row triggers the automation",
},
id: "1234",
revision: "1",
}
)
.loop({
option: LoopStepType.ARRAY,
binding: [1, 2, 3],
})
.createRow({
row: {
name: "Item {{ loop.currentItem }}",
description: "Created from loop",
tableId: table._id,
},
})
.run()
expect(results.trigger).toBeDefined()
expect(results.steps).toHaveLength(1)
expect(results.steps[0].outputs.iterations).toBe(3)
expect(results.steps[0].outputs.items).toHaveLength(3)
results.steps[0].outputs.items.forEach((output: any, index: number) => {
expect(output).toMatchObject({
success: true,
row: {
name: `Item ${index + 1}`,
description: "Created from loop",
},
})
})
})
it("should run an automation where a loop step is between two normal steps to ensure context correctness", async () => {
const builder = createAutomationBuilder({
name: "Test Trigger with Loop and Create Row",
})
const results = await builder
.rowSaved(
{ tableId: table._id! },
{
row: {
name: "Trigger Row",
description: "This row triggers the automation",
},
id: "1234",
revision: "1",
}
)
.queryRows({
tableId: table._id!,
})
.loop({
option: LoopStepType.ARRAY,
binding: [1, 2, 3],
})
.serverLog({ text: "Message {{loop.currentItem}}" })
.serverLog({ text: "{{steps.1.rows.0._id}}" })
.run()
results.steps[1].outputs.items.forEach(
(output: ServerLogStepOutputs, index: number) => {
expect(output).toMatchObject({
success: true,
})
expect(output.message).toContain(`Message ${index + 1}`)
}
)
expect(results.steps[2].outputs.message).toContain("ro_ta")
})
it("if an incorrect type is passed to the loop it should return an error", async () => {
const builder = createAutomationBuilder({
name: "Test Loop error",
})
const results = await builder
.appAction({ fields: {} })
.loop({
option: LoopStepType.ARRAY,
binding: "1, 2, 3",
})
.serverLog({ text: "Message {{loop.currentItem}}" })
.run()
expect(results.steps[0].outputs).toEqual({
success: false,
status: "INCORRECT_TYPE",
})
})
it("ensure the loop stops if the failure condition is reached", async () => {
const builder = createAutomationBuilder({
name: "Test Loop error",
})
const results = await builder
.appAction({ fields: {} })
.loop({
option: LoopStepType.ARRAY,
binding: ["test", "test2", "test3"],
failure: "test2",
})
.serverLog({ text: "Message {{loop.currentItem}}" })
.run()
expect(results.steps[0].outputs).toEqual(
expect.objectContaining({
status: "FAILURE_CONDITION_MET",
success: false,
})
)
})
it("should run an automation where a loop is successfully run twice", async () => {
const builder = createAutomationBuilder({
name: "Test Trigger with Loop and Create Row",
})
const results = await builder
.rowSaved(
{ tableId: table._id! },
{
row: {
name: "Trigger Row",
description: "This row triggers the automation",
},
id: "1234",
revision: "1",
}
)
.loop({
option: LoopStepType.ARRAY,
binding: [1, 2, 3],
})
.createRow({
row: {
name: "Item {{ loop.currentItem }}",
description: "Created from loop",
tableId: table._id,
},
})
.loop({
option: LoopStepType.STRING,
binding: "Message 1,Message 2,Message 3",
})
.serverLog({ text: "{{loop.currentItem}}" })
.run()
expect(results.trigger).toBeDefined()
expect(results.steps).toHaveLength(2)
expect(results.steps[0].outputs.iterations).toBe(3)
expect(results.steps[0].outputs.items).toHaveLength(3)
results.steps[0].outputs.items.forEach(
(output: CreateRowStepOutputs, index: number) => {
expect(output).toMatchObject({
success: true,
row: {
name: `Item ${index + 1}`,
description: "Created from loop",
},
})
}
)
expect(results.steps[1].outputs.iterations).toBe(3)
expect(results.steps[1].outputs.items).toHaveLength(3)
results.steps[1].outputs.items.forEach(
(output: ServerLogStepOutputs, index: number) => {
expect(output).toMatchObject({
success: true,
})
expect(output.message).toContain(`Message ${index + 1}`)
}
)
})
it("should run an automation where a loop is used twice to ensure context correctness further down the tree", async () => {
const builder = createAutomationBuilder({
name: "Test Trigger with Loop and Create Row",
})
const results = await builder
.appAction({ fields: {} })
.loop({
option: LoopStepType.ARRAY,
binding: [1, 2, 3],
})
.serverLog({ text: "Message {{loop.currentItem}}" })
.serverLog({ text: "{{steps.1.iterations}}" })
.loop({
option: LoopStepType.ARRAY,
binding: [1, 2, 3],
})
.serverLog({ text: "{{loop.currentItem}}" })
.serverLog({ text: "{{steps.3.iterations}}" })
.run()
// We want to ensure that bindings are corr
expect(results.steps[1].outputs.message).toContain("- 3")
expect(results.steps[3].outputs.message).toContain("- 3")
})
})
describe("Row Automations", () => {
it("should trigger an automation which then creates a row", async () => {
const table = await config.createTable()

View File

@ -161,16 +161,16 @@ describe("SQL query builder", () => {
it("should add the schema to the LEFT JOIN", () => {
const query = sql._query(generateRelationshipJson({ schema: "production" }))
expect(query).toEqual({
bindings: [relationshipLimit, limit],
sql: `select "brands".*, (select json_agg(json_build_object('product_id',"products"."product_id",'product_name',"products"."product_name",'brand_id',"products"."brand_id")) from (select "products".* from "production"."products" as "products" where "products"."brand_id" = "brands"."brand_id" order by "products"."brand_id" asc limit $1) as "products") as "products" from "production"."brands" order by "test"."id" asc limit $2`,
bindings: [limit, relationshipLimit],
sql: `with "paginated" as (select "brands".* from "production"."brands" order by "test"."id" asc limit $1) select "brands".*, (select json_agg(json_build_object('product_id',"products"."product_id",'product_name',"products"."product_name",'brand_id',"products"."brand_id")) from (select "products".* from "production"."products" as "products" where "products"."brand_id" = "brands"."brand_id" order by "products"."brand_id" asc limit $2) as "products") as "products" from "paginated" as "brands" order by "test"."id" asc`,
})
})
it("should handle if the schema is not present when doing a LEFT JOIN", () => {
const query = sql._query(generateRelationshipJson())
expect(query).toEqual({
bindings: [relationshipLimit, limit],
sql: `select "brands".*, (select json_agg(json_build_object('product_id',"products"."product_id",'product_name',"products"."product_name",'brand_id',"products"."brand_id")) from (select "products".* from "products" as "products" where "products"."brand_id" = "brands"."brand_id" order by "products"."brand_id" asc limit $1) as "products") as "products" from "brands" order by "test"."id" asc limit $2`,
bindings: [limit, relationshipLimit],
sql: `with "paginated" as (select "brands".* from "brands" order by "test"."id" asc limit $1) select "brands".*, (select json_agg(json_build_object('product_id',"products"."product_id",'product_name',"products"."product_name",'brand_id',"products"."brand_id")) from (select "products".* from "products" as "products" where "products"."brand_id" = "brands"."brand_id" order by "products"."brand_id" asc limit $2) as "products") as "products" from "paginated" as "brands" order by "test"."id" asc`,
})
})
@ -179,8 +179,8 @@ describe("SQL query builder", () => {
generateManyRelationshipJson({ schema: "production" })
)
expect(query).toEqual({
bindings: [relationshipLimit, limit],
sql: `select "stores".*, (select json_agg(json_build_object('product_id',"products"."product_id",'product_name',"products"."product_name")) from (select "products".* from "production"."products" as "products" inner join "production"."stocks" as "stocks" on "products"."product_id" = "stocks"."product_id" where "stocks"."store_id" = "stores"."store_id" order by "products"."product_id" asc limit $1) as "products") as "products" from "production"."stores" order by "test"."id" asc limit $2`,
bindings: [limit, relationshipLimit],
sql: `with "paginated" as (select "stores".* from "production"."stores" order by "test"."id" asc limit $1) select "stores".*, (select json_agg(json_build_object('product_id',"products"."product_id",'product_name',"products"."product_name")) from (select "products".* from "production"."products" as "products" inner join "production"."stocks" as "stocks" on "products"."product_id" = "stocks"."product_id" where "stocks"."store_id" = "stores"."store_id" order by "products"."product_id" asc limit $2) as "products") as "products" from "paginated" as "stores" order by "test"."id" asc`,
})
})

View File

@ -60,7 +60,7 @@ describe("Captures of real examples", () => {
queryJson
)
expect(query).toEqual({
bindings: [relationshipLimit, relationshipLimit, primaryLimit],
bindings: [primaryLimit, relationshipLimit, relationshipLimit],
sql: expect.stringContaining(
multiline(
`select json_agg(json_build_object('executorid',"b"."executorid",'taskname',"b"."taskname",'taskid',"b"."taskid",'completed',"b"."completed",'qaid',"b"."qaid",'executorid',"b"."executorid",'taskname',"b"."taskname",'taskid',"b"."taskid",'completed',"b"."completed",'qaid',"b"."qaid")`
@ -75,11 +75,11 @@ describe("Captures of real examples", () => {
queryJson
)
expect(query).toEqual({
bindings: [relationshipLimit, "assembling", primaryLimit],
bindings: ["assembling", primaryLimit, relationshipLimit],
sql: expect.stringContaining(
multiline(
`where exists (select 1 from "tasks" as "b" inner join "products_tasks" as "c" on "b"."taskid" = "c"."taskid"
where "c"."productid" = "a"."productid" and COALESCE("b"."taskname" = $2, FALSE)`
`where exists (select 1 from "tasks" as "b" inner join "products_tasks" as "c" on "b"."taskid" = "c"."taskid" where "c"."productid" = "a"."productid"
and COALESCE("b"."taskname" = $1, FALSE)`
)
),
})
@ -91,12 +91,13 @@ describe("Captures of real examples", () => {
queryJson
)
expect(query).toEqual({
bindings: [relationshipLimit, primaryLimit],
bindings: [primaryLimit, relationshipLimit],
sql: expect.stringContaining(
multiline(
`select json_agg(json_build_object('executorid',"b"."executorid",'taskname',"b"."taskname",'taskid',"b"."taskid",'completed',"b"."completed",'qaid',"b"."qaid"))
from (select "b".* from "tasks" as "b" inner join "products_tasks" as "c" on "b"."taskid" = "c"."taskid"
where "c"."productid" = "a"."productid" order by "b"."taskid" asc limit $1`
`with "paginated" as (select "a".* from "products" as "a" order by "a"."productname" asc nulls first, "a"."productid" asc limit $1)
select "a".*, (select json_agg(json_build_object('executorid',"b"."executorid",'taskname',"b"."taskname",'taskid',"b"."taskid",'completed',"b"."completed",'qaid',"b"."qaid"))
from (select "b".* from "tasks" as "b" inner join "products_tasks" as "c" on "b"."taskid" = "c"."taskid" where "c"."productid" = "a"."productid" order by "b"."taskid" asc limit $2) as "b") as "tasks"
from "paginated" as "a" order by "a"."productname" asc nulls first, "a"."productid" asc`
)
),
})
@ -109,12 +110,12 @@ describe("Captures of real examples", () => {
queryJson
)
expect(query).toEqual({
bindings: [relationshipLimit, ...filters, relationshipLimit],
bindings: [...filters, relationshipLimit, relationshipLimit],
sql: multiline(
`select "a".*, (select json_agg(json_build_object('productname',"b"."productname",'productid',"b"."productid"))
`with "paginated" as (select "a".* from "tasks" as "a" where "a"."taskid" in ($1, $2) order by "a"."taskid" asc limit $3)
select "a".*, (select json_agg(json_build_object('productname',"b"."productname",'productid',"b"."productid"))
from (select "b".* from "products" as "b" inner join "products_tasks" as "c" on "b"."productid" = "c"."productid"
where "c"."taskid" = "a"."taskid" order by "b"."productid" asc limit $1) as "b") as "products"
from "tasks" as "a" where "a"."taskid" in ($2, $3) order by "a"."taskid" asc limit $4`
where "c"."taskid" = "a"."taskid" order by "b"."productid" asc limit $4) as "b") as "products" from "paginated" as "a" order by "a"."taskid" asc`
),
})
})
@ -132,18 +133,18 @@ describe("Captures of real examples", () => {
expect(query).toEqual({
bindings: [
relationshipLimit,
relationshipLimit,
relationshipLimit,
rangeValue.low,
rangeValue.high,
equalValue,
notEqualsValue,
primaryLimit,
relationshipLimit,
relationshipLimit,
relationshipLimit,
],
sql: expect.stringContaining(
multiline(
`where exists (select 1 from "persons" as "c" where "c"."personid" = "a"."executorid" and "c"."year" between $4 and $5)`
`where exists (select 1 from "persons" as "c" where "c"."personid" = "a"."executorid" and "c"."year" between $1 and $2)`
)
),
})

View File

@ -333,6 +333,7 @@ export default class TestConfiguration {
sessionId: this.sessionIdForUser(_id),
tenantId: this.getTenantId(),
csrfToken: this.csrfToken,
email,
})
const resp = await db.put(user)
await cache.user.invalidateUser(_id)
@ -396,16 +397,17 @@ export default class TestConfiguration {
}
// make sure the user exists in the global DB
if (roleId !== roles.BUILTIN_ROLE_IDS.PUBLIC) {
await this.globalUser({
const user = await this.globalUser({
_id: userId,
builder: { global: builder },
roles: { [appId]: roleId || roles.BUILTIN_ROLE_IDS.BASIC },
})
await sessions.createASession(userId, {
sessionId: this.sessionIdForUser(userId),
tenantId: this.getTenantId(),
email: user.email,
})
}
await sessions.createASession(userId, {
sessionId: this.sessionIdForUser(userId),
tenantId: this.getTenantId(),
})
// have to fake this
const authObj = {
userId,

View File

@ -323,7 +323,9 @@ class Orchestrator {
} else if (step.stepId === AutomationActionStepId.LOOP) {
stepIndex = await this.executeLoopStep(step, steps, stepIndex)
} else {
await this.executeStep(step)
if (!this.stopped) {
await this.executeStep(step)
}
stepIndex++
}
}
@ -465,7 +467,7 @@ class Orchestrator {
for (const branch of branches) {
const condition = await this.evaluateBranchCondition(branch.condition)
if (condition) {
let branchStatus = {
const branchStatus = {
status: `${branch.name} branch taken`,
success: true,
}
@ -480,9 +482,20 @@ class Orchestrator {
const branchSteps = children?.[branch.name] || []
await this.executeSteps(branchSteps)
break
return
}
}
this.stopped = true
this.updateExecutionOutput(
branchStep.id,
branchStep.stepId,
branchStep.inputs,
{
success: false,
status: AutomationStatus.NO_CONDITION_MET,
}
)
}
private async evaluateBranchCondition(

View File

@ -247,7 +247,9 @@ class QueryRunner {
if (!resp.err) {
const globalUserId = getGlobalIDFromUserMetadataID(_id)
await auth.updateUserOAuth(globalUserId, resp)
this.ctx.user = await cache.user.getUser(globalUserId)
this.ctx.user = await cache.user.getUser({
userId: globalUserId,
})
} else {
// In this event the user may have oAuth issues that
// could require re-authenticating with their provider.

View File

@ -77,7 +77,9 @@ export async function getCachedSelf(
): Promise<ContextUser> {
// this has to be tenant aware, can't depend on the context to find it out
// running some middlewares before the tenancy causes context to break
const user = await cache.user.getUser(ctx.user?._id!)
const user = await cache.user.getUser({
userId: ctx.user?._id!,
})
return processUser(user, { appId })
}

View File

@ -35,7 +35,9 @@ export async function processInputBBReference(
}
try {
await cache.user.getUser(id)
await cache.user.getUser({
userId: id,
})
return id
} catch (e: any) {
if (e.statusCode === 404) {
@ -125,7 +127,9 @@ export async function processOutputBBReference(
case BBReferenceFieldSubType.USER: {
let user
try {
user = await cache.user.getUser(value as string)
user = await cache.user.getUser({
userId: value as string,
})
} catch (err: any) {
if (err.statusCode !== 404) {
throw err

View File

@ -110,7 +110,9 @@ async function processDefaultValues(table: Table, row: Row) {
const identity = context.getIdentity()
if (identity?._id && identity.type === IdentityType.USER) {
const user = await cache.user.getUser(identity._id)
const user = await cache.user.getUser({
userId: identity._id,
})
delete user.password
ctx["Current User"] = user

View File

@ -74,7 +74,9 @@ describe("bbReferenceProcessor", () => {
expect(result).toEqual(userId)
expect(cacheGetUserSpy).toHaveBeenCalledTimes(1)
expect(cacheGetUserSpy).toHaveBeenCalledWith(userId)
expect(cacheGetUserSpy).toHaveBeenCalledWith({
userId,
})
})
it("throws an error given an invalid id", async () => {
@ -98,7 +100,9 @@ describe("bbReferenceProcessor", () => {
expect(result).toEqual(userId)
expect(cacheGetUserSpy).toHaveBeenCalledTimes(1)
expect(cacheGetUserSpy).toHaveBeenCalledWith(userId)
expect(cacheGetUserSpy).toHaveBeenCalledWith({
userId,
})
})
it("empty strings will return null", async () => {
@ -243,7 +247,9 @@ describe("bbReferenceProcessor", () => {
lastName: user.lastName,
})
expect(cacheGetUserSpy).toHaveBeenCalledTimes(1)
expect(cacheGetUserSpy).toHaveBeenCalledWith(userId)
expect(cacheGetUserSpy).toHaveBeenCalledWith({
userId,
})
})
it("returns undefined given an unexisting user", async () => {
@ -255,7 +261,9 @@ describe("bbReferenceProcessor", () => {
expect(result).toBeUndefined()
expect(cacheGetUserSpy).toHaveBeenCalledTimes(1)
expect(cacheGetUserSpy).toHaveBeenCalledWith(userId)
expect(cacheGetUserSpy).toHaveBeenCalledWith({
userId,
})
})
})
})

View File

@ -27,6 +27,12 @@ import { isPlainObject, isEmpty } from "lodash"
import { decodeNonAscii } from "./helpers/schema"
const HBS_REGEX = /{{([^{].*?)}}/g
const LOGICAL_OPERATORS = Object.values(LogicalOperator)
const SEARCH_OPERATORS = [
...Object.values(BasicOperator),
...Object.values(ArrayOperator),
...Object.values(RangeOperator),
]
/**
* Returns the valid operator options for a certain data type
@ -117,7 +123,7 @@ export function recurseLogicalOperators(
filters: SearchFilters,
fn: (f: SearchFilters) => SearchFilters
) {
for (const logical of Object.values(LogicalOperator)) {
for (const logical of LOGICAL_OPERATORS) {
if (filters[logical]) {
filters[logical]!.conditions = filters[logical]!.conditions.map(
condition => fn(condition)
@ -135,7 +141,7 @@ export function recurseSearchFilters(
filters = processFn(filters)
// Recurse through logical operators
for (const logical of Object.values(LogicalOperator)) {
for (const logical of LOGICAL_OPERATORS) {
if (filters[logical]) {
filters[logical]!.conditions = filters[logical]!.conditions.map(
condition => recurseSearchFilters(condition, processFn)
@ -773,12 +779,16 @@ export function runQuery<T extends Record<string, any>>(
return filterFunctions[key as SearchFilterOperator]?.(doc) ?? false
})
if (query.allOr) {
// there are no filters - logical operators can cover this up
if (!hasFilters(query)) {
return true
} else if (query.allOr) {
return results.some(result => result === true)
} else {
return results.every(result => result === true)
}
}
return docs.filter(docMatch)
}
@ -841,14 +851,33 @@ export const hasFilters = (query?: SearchFilters) => {
if (!query) {
return false
}
const skipped = ["allOr", "onEmptyFilter"]
for (let [key, value] of Object.entries(query)) {
if (skipped.includes(key) || typeof value !== "object") {
continue
const check = (filters: SearchFilters): boolean => {
for (const logical of LOGICAL_OPERATORS) {
if (filters[logical]) {
for (const condition of filters[logical]?.conditions || []) {
const result = check(condition)
if (result) {
return result
}
}
}
}
if (Object.keys(value || {}).length !== 0) {
return true
for (const search of SEARCH_OPERATORS) {
const searchValue = filters[search]
if (!searchValue || typeof searchValue !== "object") {
continue
}
const filtered = Object.entries(searchValue).filter(entry => {
const valueDefined =
entry[1] !== undefined || entry[1] !== null || entry[1] !== ""
// not empty is an edge case, null is allowed for it - this is covered by test cases
return search === BasicOperator.NOT_EMPTY || valueDefined
})
if (filtered.length !== 0) {
return true
}
}
return false
}
return false
return check(query)
}

View File

@ -179,6 +179,7 @@ export enum AutomationStatus {
ERROR = "error",
STOPPED = "stopped",
STOPPED_ERROR = "stopped_error",
NO_CONDITION_MET = "No branch condition met",
}
export enum AutomationStoppedReason {

View File

@ -10,6 +10,7 @@ export interface AuthToken {
export interface CreateSession {
sessionId: string
tenantId: string
email: string
csrfToken?: string
hosting?: Hosting
}

View File

@ -19,12 +19,17 @@ import { EmailTemplatePurpose } from "../../constants"
export async function loginUser(user: User) {
const sessionId = coreUtils.newid()
const tenantId = tenancy.getTenantId()
await sessions.createASession(user._id!, { sessionId, tenantId })
await sessions.createASession(user._id!, {
sessionId,
tenantId,
email: user.email,
})
return jwt.sign(
{
userId: user._id,
sessionId,
tenantId,
email: user.email,
},
coreEnv.JWT_SECRET!
)

View File

@ -170,19 +170,26 @@ class TestConfiguration {
async _createSession({
userId,
tenantId,
email,
}: {
userId: string
tenantId: string
email: string
}) {
await sessions.createASession(userId!, {
sessionId: "sessionid",
tenantId: tenantId,
tenantId,
csrfToken: CSRF_TOKEN,
email,
})
}
async createSession(user: User) {
return this._createSession({ userId: user._id!, tenantId: user.tenantId })
return this._createSession({
userId: user._id!,
tenantId: user.tenantId,
email: user.email,
})
}
cookieHeader(cookies: any) {