Merge branch 'master' into execute-script-v2

This commit is contained in:
deanhannigan 2025-03-04 15:11:24 +00:00 committed by GitHub
commit 8566813677
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
42 changed files with 131 additions and 194 deletions

View File

@ -1,6 +1,6 @@
{ {
"$schema": "node_modules/lerna/schemas/lerna-schema.json", "$schema": "node_modules/lerna/schemas/lerna-schema.json",
"version": "3.4.22", "version": "3.4.23",
"npmClient": "yarn", "npmClient": "yarn",
"concurrency": 20, "concurrency": 20,
"command": { "command": {

View File

@ -1,22 +1,26 @@
<script> <script lang="ts" context="module">
type Option = any
</script>
<script lang="ts">
import Picker from "./Picker.svelte" import Picker from "./Picker.svelte"
import { createEventDispatcher } from "svelte" import { createEventDispatcher } from "svelte"
export let value = [] export let value: string[] = []
export let id = null export let id: string | undefined = undefined
export let placeholder = null export let placeholder: string | null = null
export let disabled = false export let disabled: boolean = false
export let options = [] export let options: Option[] = []
export let getOptionLabel = option => option export let getOptionLabel = (option: Option, _index?: number) => option
export let getOptionValue = option => option export let getOptionValue = (option: Option, _index?: number) => option
export let readonly = false export let readonly: boolean = false
export let autocomplete = false export let autocomplete: boolean = false
export let sort = false export let sort: boolean = false
export let autoWidth = false export let autoWidth: boolean = false
export let searchTerm = null export let searchTerm: string | null = null
export let customPopoverHeight = undefined export let customPopoverHeight: string | undefined = undefined
export let open = false export let open: boolean = false
export let loading export let loading: boolean
export let onOptionMouseenter = () => {} export let onOptionMouseenter = () => {}
export let onOptionMouseleave = () => {} export let onOptionMouseleave = () => {}
@ -27,10 +31,15 @@
$: optionLookupMap = getOptionLookupMap(options) $: optionLookupMap = getOptionLookupMap(options)
$: fieldText = getFieldText(arrayValue, optionLookupMap, placeholder) $: fieldText = getFieldText(arrayValue, optionLookupMap, placeholder)
$: isOptionSelected = optionValue => selectedLookupMap[optionValue] === true $: isOptionSelected = (optionValue: string) =>
selectedLookupMap[optionValue] === true
$: toggleOption = makeToggleOption(selectedLookupMap, arrayValue) $: toggleOption = makeToggleOption(selectedLookupMap, arrayValue)
const getFieldText = (value, map, placeholder) => { const getFieldText = (
value: string[],
map: Record<string, any> | null,
placeholder: string | null
) => {
if (Array.isArray(value) && value.length > 0) { if (Array.isArray(value) && value.length > 0) {
if (!map) { if (!map) {
return "" return ""
@ -42,8 +51,8 @@
} }
} }
const getSelectedLookupMap = value => { const getSelectedLookupMap = (value: string[]) => {
let map = {} const map: Record<string, boolean> = {}
if (Array.isArray(value) && value.length > 0) { if (Array.isArray(value) && value.length > 0) {
value.forEach(option => { value.forEach(option => {
if (option) { if (option) {
@ -54,22 +63,23 @@
return map return map
} }
const getOptionLookupMap = options => { const getOptionLookupMap = (options: Option[]) => {
let map = null if (!options?.length) {
if (options?.length) { return null
map = {} }
const map: Record<string, any> = {}
options.forEach((option, idx) => { options.forEach((option, idx) => {
const optionValue = getOptionValue(option, idx) const optionValue = getOptionValue(option, idx)
if (optionValue != null) { if (optionValue != null) {
map[optionValue] = getOptionLabel(option, idx) || "" map[optionValue] = getOptionLabel(option, idx) || ""
} }
}) })
}
return map return map
} }
const makeToggleOption = (map, value) => { const makeToggleOption = (map: Record<string, boolean>, value: string[]) => {
return optionValue => { return (optionValue: string) => {
if (map[optionValue]) { if (map[optionValue]) {
const filtered = value.filter(option => option !== optionValue) const filtered = value.filter(option => option !== optionValue)
dispatch("change", filtered) dispatch("change", filtered)

View File

@ -5,11 +5,12 @@
import { memo } from "@budibase/frontend-core" import { memo } from "@budibase/frontend-core"
import Placeholder from "../Placeholder.svelte" import Placeholder from "../Placeholder.svelte"
import InnerForm from "./InnerForm.svelte" import InnerForm from "./InnerForm.svelte"
import type { FieldApi } from "."
export let label: string | undefined = undefined export let label: string | undefined = undefined
export let field: string | undefined = undefined export let field: string | undefined = undefined
export let fieldState: any export let fieldState: any
export let fieldApi: any export let fieldApi: FieldApi
export let fieldSchema: any export let fieldSchema: any
export let defaultValue: string | undefined = undefined export let defaultValue: string | undefined = undefined
export let type: any export let type: any

View File

@ -1,6 +1,6 @@
<script lang="ts"> <script lang="ts">
import { CoreSelect, CoreMultiselect } from "@budibase/bbui" import { CoreSelect, CoreMultiselect } from "@budibase/bbui"
import { FieldType, InternalTable } from "@budibase/types" import { BasicOperator, FieldType, InternalTable } from "@budibase/types"
import { fetchData, Utils } from "@budibase/frontend-core" import { fetchData, Utils } from "@budibase/frontend-core"
import { getContext } from "svelte" import { getContext } from "svelte"
import Field from "./Field.svelte" import Field from "./Field.svelte"
@ -9,10 +9,11 @@
RelationshipFieldMetadata, RelationshipFieldMetadata,
Row, Row,
} from "@budibase/types" } from "@budibase/types"
import type { FieldApi, FieldState } from "."
export let field: string | undefined = undefined export let field: string | undefined = undefined
export let label: string | undefined = undefined export let label: string | undefined = undefined
export let placeholder: any = undefined export let placeholder: string | undefined = undefined
export let disabled: boolean = false export let disabled: boolean = false
export let readonly: boolean = false export let readonly: boolean = false
export let validation: any export let validation: any
@ -35,12 +36,13 @@
const { API } = getContext("sdk") const { API } = getContext("sdk")
// Field state // Field state
let fieldState: any let fieldState: FieldState<string | string[]> | undefined
let fieldApi: any
let fieldApi: FieldApi
let fieldSchema: RelationshipFieldMetadata | undefined let fieldSchema: RelationshipFieldMetadata | undefined
// Local UI state // Local UI state
let searchTerm: any let searchTerm: string
let open: boolean = false let open: boolean = false
// Options state // Options state
@ -106,17 +108,14 @@
filter: SearchFilter[], filter: SearchFilter[],
linkedTableId?: string linkedTableId?: string
) => { ) => {
if (!linkedTableId) {
return undefined
}
const datasource = const datasource =
datasourceType === "table" dsType === "table"
? { ? {
type: datasourceType, type: dsType,
tableId: fieldSchema?.tableId!, tableId: linkedTableId!,
} }
: { : {
type: datasourceType, type: dsType,
tableId: InternalTable.USER_METADATA, tableId: InternalTable.USER_METADATA,
} }
return fetchData({ return fetchData({
@ -306,14 +305,14 @@
} }
// Ensure we match all filters, rather than any // Ensure we match all filters, rather than any
let newFilter: any = filter let newFilter = filter
if (searchTerm) { if (searchTerm) {
// @ts-expect-error this doesn't fit types, but don't want to change it yet // @ts-expect-error this doesn't fit types, but don't want to change it yet
newFilter = (newFilter || []).filter(x => x.operator !== "allOr") newFilter = (newFilter || []).filter(x => x.operator !== "allOr")
newFilter.push({ newFilter.push({
// Use a big numeric prefix to avoid clashing with an existing filter // Use a big numeric prefix to avoid clashing with an existing filter
field: `999:${primaryDisplay}`, field: `999:${primaryDisplay}`,
operator: "string", operator: BasicOperator.STRING,
value: searchTerm, value: searchTerm,
}) })
} }

View File

@ -19,3 +19,15 @@ export { default as codescanner } from "./CodeScannerField.svelte"
export { default as signaturesinglefield } from "./SignatureField.svelte" export { default as signaturesinglefield } from "./SignatureField.svelte"
export { default as bbreferencefield } from "./BBReferenceField.svelte" export { default as bbreferencefield } from "./BBReferenceField.svelte"
export { default as bbreferencesinglefield } from "./BBReferenceSingleField.svelte" export { default as bbreferencesinglefield } from "./BBReferenceSingleField.svelte"
export interface FieldApi {
setValue(value: any): boolean
deregister(): void
}
export interface FieldState<T> {
value: T
fieldId: string
disabled: boolean
readonly: boolean
}

View File

@ -9,6 +9,7 @@ import {
const DISABLED_EXTERNAL_INTEGRATIONS = [ const DISABLED_EXTERNAL_INTEGRATIONS = [
SourceName.AIRTABLE, SourceName.AIRTABLE,
SourceName.BUDIBASE, SourceName.BUDIBASE,
SourceName.ARANGODB,
] ]
export async function fetch(ctx: UserCtx<void, FetchIntegrationsResponse>) { export async function fetch(ctx: UserCtx<void, FetchIntegrationsResponse>) {

View File

@ -26,7 +26,7 @@ export function csv(
headers.map(header => { headers.map(header => {
const val = row[header] const val = row[header]
if (typeof val === "object" && !(val instanceof Date)) { if (typeof val === "object" && !(val instanceof Date)) {
return `"${JSON.stringify(val).replace(/"/g, "'")}"` return `"${escapeCsvString(JSON.stringify(val))}"`
} }
if (val !== undefined) { if (val !== undefined) {
return `"${escapeCsvString(val.toString())}"` return `"${escapeCsvString(val.toString())}"`

View File

@ -1,19 +1,6 @@
import { DEFAULT_TABLES } from "../../../db/defaultData/datasource_bb_default" import { DEFAULT_TABLES } from "../../../db/defaultData/datasource_bb_default"
import { setEnv } from "../../../environment" import { setEnv } from "../../../environment"
jest.mock("../../../utilities/redis", () => ({
init: jest.fn(),
getLocksById: () => {
return {}
},
doesUserHaveLock: () => {
return true
},
updateLock: jest.fn(),
setDebounce: jest.fn(),
checkDebounce: jest.fn(),
shutdown: jest.fn(),
}))
import { checkBuilderEndpoint } from "./utilities/TestFunctions" import { checkBuilderEndpoint } from "./utilities/TestFunctions"
import * as setup from "./utilities" import * as setup from "./utilities"
import { AppStatus } from "../../../db/utils" import { AppStatus } from "../../../db/utils"

View File

@ -2515,15 +2515,14 @@ if (descriptions.length) {
csvString: exportedValue, csvString: exportedValue,
}) })
const stringified = (value: string) => const stringified = (value: string) => JSON.stringify(value)
JSON.stringify(value).replace(/"/g, "'")
const matchingObject = ( const matchingObject = (
key: string, key: string,
value: any, value: any,
isArray: boolean isArray: boolean
) => { ) => {
const objectMatcher = `{'${key}':'${value[key]}'.*?}` const objectMatcher = `{"${key}":"${value[key]}".*?}`
if (isArray) { if (isArray) {
return expect.stringMatching( return expect.stringMatching(
new RegExp(`^\\[${objectMatcher}\\]$`) new RegExp(`^\\[${objectMatcher}\\]$`)

View File

@ -1246,10 +1246,7 @@ if (descriptions.length) {
}) })
describe.each([ describe.each([
[ [RowExportFormat.CSV, (val: any) => JSON.stringify(val)],
RowExportFormat.CSV,
(val: any) => JSON.stringify(val).replace(/"/g, "'"),
],
[RowExportFormat.JSON, (val: any) => val], [RowExportFormat.JSON, (val: any) => val],
])("import validation (%s)", (_, userParser) => { ])("import validation (%s)", (_, userParser) => {
const basicSchema: TableSchema = { const basicSchema: TableSchema = {

View File

@ -16,6 +16,7 @@ describe("Execute Bash Automations", () => {
name: "test row", name: "test row",
description: "test description", description: "test description",
}) })
await config.api.automation.deleteAll()
}) })
afterAll(() => { afterAll(() => {

View File

@ -33,6 +33,7 @@ describe("test the create row action", () => {
name: "test", name: "test",
description: "test", description: "test",
} }
await config.api.automation.deleteAll()
}) })
afterAll(() => { afterAll(() => {

View File

@ -6,6 +6,7 @@ describe("test the delay logic", () => {
beforeAll(async () => { beforeAll(async () => {
await config.init() await config.init()
await config.api.automation.deleteAll()
}) })
afterAll(() => { afterAll(() => {

View File

@ -13,6 +13,7 @@ describe("test the delete row action", () => {
await config.init() await config.init()
table = await config.api.table.save(basicTable()) table = await config.api.table.save(basicTable())
row = await config.api.row.save(table._id!, {}) row = await config.api.row.save(table._id!, {})
await config.api.automation.deleteAll()
}) })
afterAll(() => { afterAll(() => {

View File

@ -7,6 +7,7 @@ describe("test the outgoing webhook action", () => {
beforeAll(async () => { beforeAll(async () => {
await config.init() await config.init()
await config.api.automation.deleteAll()
}) })
afterAll(() => { afterAll(() => {

View File

@ -26,6 +26,7 @@ if (descriptions.length) {
const ds = await dsProvider() const ds = await dsProvider()
datasource = ds.datasource! datasource = ds.datasource!
client = ds.client! client = ds.client!
await config.api.automation.deleteAll()
}) })
beforeEach(async () => { beforeEach(async () => {

View File

@ -13,6 +13,7 @@ describe("Execute Script Automations", () => {
await config.init() await config.init()
table = await config.api.table.save(basicTable()) table = await config.api.table.save(basicTable())
await config.api.row.save(table._id!, {}) await config.api.row.save(table._id!, {})
await config.api.automation.deleteAll()
}) })
afterAll(() => { afterAll(() => {

View File

@ -26,6 +26,7 @@ describe("test the filter logic", () => {
beforeAll(async () => { beforeAll(async () => {
await config.init() await config.init()
await config.api.automation.deleteAll()
}) })
afterAll(() => { afterAll(() => {

View File

@ -22,10 +22,7 @@ describe("Attempt to run a basic loop automation", () => {
}) })
beforeEach(async () => { beforeEach(async () => {
const { automations } = await config.api.automation.fetch() await config.api.automation.deleteAll()
for (const automation of automations) {
await config.api.automation.delete(automation)
}
table = await config.api.table.save(basicTable()) table = await config.api.table.save(basicTable())
await config.api.row.save(table._id!, {}) await config.api.row.save(table._id!, {})

View File

@ -7,6 +7,7 @@ describe("test the outgoing webhook action", () => {
beforeAll(async () => { beforeAll(async () => {
await config.init() await config.init()
await config.api.automation.deleteAll()
}) })
afterAll(() => { afterAll(() => {

View File

@ -8,6 +8,7 @@ describe("test the outgoing webhook action", () => {
beforeAll(async () => { beforeAll(async () => {
await config.init() await config.init()
await config.api.automation.deleteAll()
}) })
afterAll(() => { afterAll(() => {

View File

@ -16,6 +16,7 @@ describe("test the openai action", () => {
beforeAll(async () => { beforeAll(async () => {
await config.init() await config.init()
await config.api.automation.deleteAll()
}) })
beforeEach(() => { beforeEach(() => {

View File

@ -8,6 +8,7 @@ describe("test the outgoing webhook action", () => {
beforeAll(async () => { beforeAll(async () => {
await config.init() await config.init()
await config.api.automation.deleteAll()
}) })
afterAll(() => { afterAll(() => {

View File

@ -21,6 +21,7 @@ describe("Test a query step automation", () => {
} }
await config.api.row.save(table._id!, row) await config.api.row.save(table._id!, row)
await config.api.row.save(table._id!, row) await config.api.row.save(table._id!, row)
await config.api.automation.deleteAll()
}) })
afterAll(() => { afterAll(() => {

View File

@ -28,6 +28,7 @@ describe("test the outgoing webhook action", () => {
beforeAll(async () => { beforeAll(async () => {
await config.init() await config.init()
await config.api.automation.deleteAll()
}) })
afterAll(() => { afterAll(() => {

View File

@ -6,6 +6,7 @@ describe("test the server log action", () => {
beforeAll(async () => { beforeAll(async () => {
await config.init() await config.init()
await config.api.automation.deleteAll()
}) })
afterAll(() => { afterAll(() => {

View File

@ -9,6 +9,7 @@ describe("Test triggering an automation from another automation", () => {
beforeAll(async () => { beforeAll(async () => {
await automation.init() await automation.init()
await config.init() await config.init()
await config.api.automation.deleteAll()
}) })
afterAll(async () => { afterAll(async () => {

View File

@ -23,6 +23,7 @@ describe("test the update row action", () => {
await config.init() await config.init()
table = await config.createTable() table = await config.createTable()
row = await config.createRow() row = await config.createRow()
await config.api.automation.deleteAll()
}) })
afterAll(() => { afterAll(() => {

View File

@ -7,6 +7,7 @@ describe("test the outgoing webhook action", () => {
beforeAll(async () => { beforeAll(async () => {
await config.init() await config.init()
await config.api.automation.deleteAll()
}) })
afterAll(() => { afterAll(() => {

View File

@ -9,6 +9,8 @@ describe("app action trigger", () => {
beforeAll(async () => { beforeAll(async () => {
await config.init() await config.init()
await config.api.automation.deleteAll()
automation = await createAutomationBuilder(config) automation = await createAutomationBuilder(config)
.onAppAction() .onAppAction()
.serverLog({ .serverLog({

View File

@ -16,6 +16,7 @@ describe("cron trigger", () => {
beforeAll(async () => { beforeAll(async () => {
await config.init() await config.init()
await config.api.automation.deleteAll()
}) })
afterAll(() => { afterAll(() => {

View File

@ -11,6 +11,7 @@ describe("row deleted trigger", () => {
beforeAll(async () => { beforeAll(async () => {
await config.init() await config.init()
await config.api.automation.deleteAll()
table = await config.api.table.save(basicTable()) table = await config.api.table.save(basicTable())
automation = await createAutomationBuilder(config) automation = await createAutomationBuilder(config)
.onRowDeleted({ tableId: table._id! }) .onRowDeleted({ tableId: table._id! })

View File

@ -11,6 +11,7 @@ describe("row saved trigger", () => {
beforeAll(async () => { beforeAll(async () => {
await config.init() await config.init()
await config.api.automation.deleteAll()
table = await config.api.table.save(basicTable()) table = await config.api.table.save(basicTable())
automation = await createAutomationBuilder(config) automation = await createAutomationBuilder(config)
.onRowSaved({ tableId: table._id! }) .onRowSaved({ tableId: table._id! })

View File

@ -11,6 +11,7 @@ describe("row updated trigger", () => {
beforeAll(async () => { beforeAll(async () => {
await config.init() await config.init()
await config.api.automation.deleteAll()
table = await config.api.table.save(basicTable()) table = await config.api.table.save(basicTable())
automation = await createAutomationBuilder(config) automation = await createAutomationBuilder(config)
.onRowUpdated({ tableId: table._id! }) .onRowUpdated({ tableId: table._id! })

View File

@ -37,6 +37,7 @@ describe("Webhook trigger test", () => {
beforeEach(async () => { beforeEach(async () => {
await config.init() await config.init()
await config.api.automation.deleteAll()
table = await config.createTable() table = await config.createTable()
}) })

View File

@ -9,6 +9,11 @@ import {
import { Database, aql } from "arangojs" import { Database, aql } from "arangojs"
/**
* @deprecated 3rd March 2025
* datasource disabled - this datasource is marked for deprecation and removal
*/
interface ArangodbConfig { interface ArangodbConfig {
url: string url: string
username: string username: string

View File

@ -33,15 +33,17 @@ const DEFINITIONS: Record<SourceName, Integration | undefined> = {
[SourceName.COUCHDB]: couchdb.schema, [SourceName.COUCHDB]: couchdb.schema,
[SourceName.SQL_SERVER]: sqlServer.schema, [SourceName.SQL_SERVER]: sqlServer.schema,
[SourceName.S3]: s3.schema, [SourceName.S3]: s3.schema,
[SourceName.AIRTABLE]: airtable.schema,
[SourceName.MYSQL]: mysql.schema, [SourceName.MYSQL]: mysql.schema,
[SourceName.ARANGODB]: arangodb.schema,
[SourceName.REST]: rest.schema, [SourceName.REST]: rest.schema,
[SourceName.FIRESTORE]: firebase.schema, [SourceName.FIRESTORE]: firebase.schema,
[SourceName.GOOGLE_SHEETS]: googlesheets.schema, [SourceName.GOOGLE_SHEETS]: googlesheets.schema,
[SourceName.REDIS]: redis.schema, [SourceName.REDIS]: redis.schema,
[SourceName.SNOWFLAKE]: snowflake.schema, [SourceName.SNOWFLAKE]: snowflake.schema,
[SourceName.ORACLE]: oracle.schema, [SourceName.ORACLE]: oracle.schema,
/* deprecated - not available through UI */
[SourceName.ARANGODB]: arangodb.schema,
[SourceName.AIRTABLE]: airtable.schema,
/* un-used */
[SourceName.BUDIBASE]: undefined, [SourceName.BUDIBASE]: undefined,
} }
@ -56,15 +58,17 @@ const INTEGRATIONS: Record<SourceName, IntegrationBaseConstructor | undefined> =
[SourceName.COUCHDB]: couchdb.integration, [SourceName.COUCHDB]: couchdb.integration,
[SourceName.SQL_SERVER]: sqlServer.integration, [SourceName.SQL_SERVER]: sqlServer.integration,
[SourceName.S3]: s3.integration, [SourceName.S3]: s3.integration,
[SourceName.AIRTABLE]: airtable.integration,
[SourceName.MYSQL]: mysql.integration, [SourceName.MYSQL]: mysql.integration,
[SourceName.ARANGODB]: arangodb.integration,
[SourceName.REST]: rest.integration, [SourceName.REST]: rest.integration,
[SourceName.FIRESTORE]: firebase.integration, [SourceName.FIRESTORE]: firebase.integration,
[SourceName.GOOGLE_SHEETS]: googlesheets.integration, [SourceName.GOOGLE_SHEETS]: googlesheets.integration,
[SourceName.REDIS]: redis.integration, [SourceName.REDIS]: redis.integration,
[SourceName.SNOWFLAKE]: snowflake.integration, [SourceName.SNOWFLAKE]: snowflake.integration,
[SourceName.ORACLE]: oracle.integration, [SourceName.ORACLE]: oracle.integration,
/* deprecated - not available through UI */
[SourceName.ARANGODB]: arangodb.integration,
[SourceName.AIRTABLE]: airtable.integration,
/* un-used */
[SourceName.BUDIBASE]: undefined, [SourceName.BUDIBASE]: undefined,
} }

View File

@ -1,76 +0,0 @@
import { default as AirtableIntegration } from "../airtable"
jest.mock("airtable")
class TestConfiguration {
integration: any
client: any
constructor(config: any = {}) {
this.integration = new AirtableIntegration.integration(config)
this.client = {
create: jest.fn(),
select: jest.fn(() => ({
firstPage: jest.fn(() => []),
})),
update: jest.fn(),
destroy: jest.fn(),
}
this.integration.client = () => this.client
}
}
describe("Airtable Integration", () => {
let config: any
beforeEach(() => {
config = new TestConfiguration()
})
it("calls the create method with the correct params", async () => {
await config.integration.create({
table: "test",
json: {},
})
expect(config.client.create).toHaveBeenCalledWith([
{
fields: {},
},
])
})
it("calls the read method with the correct params", async () => {
await config.integration.read({
table: "test",
view: "Grid view",
})
expect(config.client.select).toHaveBeenCalledWith({
maxRecords: 10,
view: "Grid view",
})
})
it("calls the update method with the correct params", async () => {
await config.integration.update({
table: "table",
id: "123",
json: {
name: "test",
},
})
expect(config.client.update).toHaveBeenCalledWith([
{
id: "123",
fields: { name: "test" },
},
])
})
it("calls the delete method with the correct params", async () => {
const ids = [1, 2, 3, 4]
await config.integration.delete({
ids,
})
expect(config.client.destroy).toHaveBeenCalledWith(ids)
})
})

View File

@ -1,38 +0,0 @@
import { default as ArangoDBIntegration } from "../arangodb"
jest.mock("arangojs")
class TestConfiguration {
integration: any
constructor(config: any = {}) {
this.integration = new ArangoDBIntegration.integration(config)
}
}
describe("ArangoDB Integration", () => {
let config: any
beforeEach(() => {
config = new TestConfiguration()
})
it("calls the create method with the correct params", async () => {
const body = {
json: "Hello",
}
await config.integration.create(body)
expect(config.integration.client.query).toHaveBeenCalledWith(
`INSERT Hello INTO collection RETURN NEW`
)
})
it("calls the read method with the correct params", async () => {
const query = {
sql: `test`,
}
await config.integration.read(query)
expect(config.integration.client.query).toHaveBeenCalledWith(query.sql)
})
})

View File

@ -133,4 +133,11 @@ export class AutomationAPI extends TestAPI {
} }
) )
} }
deleteAll = async (expectations?: Expectations): Promise<void> => {
const { automations } = await this.fetch()
await Promise.all(
automations.map(automation => this.delete(automation, expectations))
)
}
} }

View File

@ -270,6 +270,7 @@ function parseJsonExport<T>(value: any) {
if (typeof value !== "string") { if (typeof value !== "string") {
return value return value
} }
try { try {
const parsed = JSON.parse(value) const parsed = JSON.parse(value)
@ -278,12 +279,17 @@ function parseJsonExport<T>(value: any) {
if ( if (
e.message.startsWith("Expected property name or '}' in JSON at position ") e.message.startsWith("Expected property name or '}' in JSON at position ")
) { ) {
// This was probably converted as CSV and it has single quotes instead of double ones // In order to store JSON within CSVs what we used to do is replace double
// quotes with single quotes. This results in invalid JSON, so the line
// below is a workaround to parse it. However, this method of storing JSON
// was never valid, and we don't do it anymore. However, people may have
// exported data and stored it, hoping to be able to restore it later, so
// we leave this in place to support that.
const parsed = JSON.parse(value.replace(/'/g, '"')) const parsed = JSON.parse(value.replace(/'/g, '"'))
return parsed as T return parsed as T
} }
// It is no a valid JSON // It is not valid JSON
throw e throw e
} }
} }

View File

@ -117,7 +117,8 @@ export function isSupportedUserSearch(
{ op: BasicOperator.EQUAL, key: "_id" }, { op: BasicOperator.EQUAL, key: "_id" },
{ op: ArrayOperator.ONE_OF, key: "_id" }, { op: ArrayOperator.ONE_OF, key: "_id" },
] ]
for (const [key, operation] of Object.entries(query)) { const { allOr, onEmptyFilter, ...filters } = query
for (const [key, operation] of Object.entries(filters)) {
if (typeof operation !== "object") { if (typeof operation !== "object") {
return false return false
} }