Merge branch 'master' into borderless-grid
This commit is contained in:
commit
426b69287f
|
@ -42,7 +42,15 @@
|
||||||
},
|
},
|
||||||
"rules": {
|
"rules": {
|
||||||
"no-unused-vars": "off",
|
"no-unused-vars": "off",
|
||||||
"@typescript-eslint/no-unused-vars": "error",
|
"@typescript-eslint/no-unused-vars": [
|
||||||
|
"error",
|
||||||
|
{
|
||||||
|
"varsIgnorePattern": "^_",
|
||||||
|
"argsIgnorePattern": "^_",
|
||||||
|
"destructuredArrayIgnorePattern": "^_",
|
||||||
|
"ignoreRestSiblings": true
|
||||||
|
}
|
||||||
|
],
|
||||||
"local-rules/no-budibase-imports": "error"
|
"local-rules/no-budibase-imports": "error"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -59,7 +67,15 @@
|
||||||
},
|
},
|
||||||
"rules": {
|
"rules": {
|
||||||
"no-unused-vars": "off",
|
"no-unused-vars": "off",
|
||||||
"@typescript-eslint/no-unused-vars": "error",
|
"@typescript-eslint/no-unused-vars": [
|
||||||
|
"error",
|
||||||
|
{
|
||||||
|
"varsIgnorePattern": "^_",
|
||||||
|
"argsIgnorePattern": "^_",
|
||||||
|
"destructuredArrayIgnorePattern": "^_",
|
||||||
|
"ignoreRestSiblings": true
|
||||||
|
}
|
||||||
|
],
|
||||||
"local-rules/no-test-com": "error",
|
"local-rules/no-test-com": "error",
|
||||||
"local-rules/email-domain-example-com": "error",
|
"local-rules/email-domain-example-com": "error",
|
||||||
"no-console": "warn",
|
"no-console": "warn",
|
||||||
|
@ -89,7 +105,8 @@
|
||||||
{
|
{
|
||||||
"varsIgnorePattern": "^_",
|
"varsIgnorePattern": "^_",
|
||||||
"argsIgnorePattern": "^_",
|
"argsIgnorePattern": "^_",
|
||||||
"destructuredArrayIgnorePattern": "^_"
|
"destructuredArrayIgnorePattern": "^_",
|
||||||
|
"ignoreRestSiblings": true
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"import/no-relative-packages": "error",
|
"import/no-relative-packages": "error",
|
||||||
|
|
|
@ -17,6 +17,7 @@ APP_PORT=4002
|
||||||
WORKER_PORT=4003
|
WORKER_PORT=4003
|
||||||
MINIO_PORT=4004
|
MINIO_PORT=4004
|
||||||
COUCH_DB_PORT=4005
|
COUCH_DB_PORT=4005
|
||||||
|
COUCH_DB_SQS_PORT=4006
|
||||||
REDIS_PORT=6379
|
REDIS_PORT=6379
|
||||||
WATCHTOWER_PORT=6161
|
WATCHTOWER_PORT=6161
|
||||||
BUDIBASE_ENVIRONMENT=PRODUCTION
|
BUDIBASE_ENVIRONMENT=PRODUCTION
|
||||||
|
@ -28,4 +29,4 @@ BB_ADMIN_USER_PASSWORD=
|
||||||
|
|
||||||
# A path that is watched for plugin bundles. Any bundles found are imported automatically/
|
# A path that is watched for plugin bundles. Any bundles found are imported automatically/
|
||||||
PLUGINS_DIR=
|
PLUGINS_DIR=
|
||||||
ROLLING_LOG_MAX_SIZE=
|
ROLLING_LOG_MAX_SIZE=
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
{
|
{
|
||||||
"version": "2.23.5",
|
"version": "2.23.6",
|
||||||
"npmClient": "yarn",
|
"npmClient": "yarn",
|
||||||
"packages": [
|
"packages": [
|
||||||
"packages/*",
|
"packages/*",
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
Subproject commit bd0e01d639ec3b2547e7c859a1c43b622dce8344
|
Subproject commit eb7d5da233885c5cffd9c255d3e954d0cd39185e
|
|
@ -8,19 +8,9 @@ import {
|
||||||
SearchParams,
|
SearchParams,
|
||||||
WithRequired,
|
WithRequired,
|
||||||
} from "@budibase/types"
|
} from "@budibase/types"
|
||||||
|
import { dataFilters } from "@budibase/shared-core"
|
||||||
|
|
||||||
const QUERY_START_REGEX = /\d[0-9]*:/g
|
export const removeKeyNumbering = dataFilters.removeKeyNumbering
|
||||||
|
|
||||||
export function removeKeyNumbering(key: any): string {
|
|
||||||
if (typeof key === "string" && key.match(QUERY_START_REGEX) != null) {
|
|
||||||
const parts = key.split(":")
|
|
||||||
// remove the number
|
|
||||||
parts.shift()
|
|
||||||
return parts.join(":")
|
|
||||||
} else {
|
|
||||||
return key
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Class to build lucene query URLs.
|
* Class to build lucene query URLs.
|
||||||
|
|
|
@ -107,7 +107,7 @@ const environment = {
|
||||||
ENCRYPTION_KEY: process.env.ENCRYPTION_KEY,
|
ENCRYPTION_KEY: process.env.ENCRYPTION_KEY,
|
||||||
API_ENCRYPTION_KEY: getAPIEncryptionKey(),
|
API_ENCRYPTION_KEY: getAPIEncryptionKey(),
|
||||||
COUCH_DB_URL: process.env.COUCH_DB_URL || "http://localhost:4005",
|
COUCH_DB_URL: process.env.COUCH_DB_URL || "http://localhost:4005",
|
||||||
COUCH_DB_SQL_URL: process.env.COUCH_DB_SQL_URL || "http://localhost:4984",
|
COUCH_DB_SQL_URL: process.env.COUCH_DB_SQL_URL || "http://localhost:4006",
|
||||||
COUCH_DB_USERNAME: process.env.COUCH_DB_USER,
|
COUCH_DB_USERNAME: process.env.COUCH_DB_USER,
|
||||||
COUCH_DB_PASSWORD: process.env.COUCH_DB_PASSWORD,
|
COUCH_DB_PASSWORD: process.env.COUCH_DB_PASSWORD,
|
||||||
GOOGLE_CLIENT_ID: process.env.GOOGLE_CLIENT_ID,
|
GOOGLE_CLIENT_ID: process.env.GOOGLE_CLIENT_ID,
|
||||||
|
|
|
@ -115,7 +115,6 @@ class InMemoryQueue implements Partial<Queue> {
|
||||||
* a JSON message as this is required by Bull.
|
* a JSON message as this is required by Bull.
|
||||||
* @param repeat serves no purpose for the import queue.
|
* @param repeat serves no purpose for the import queue.
|
||||||
*/
|
*/
|
||||||
// eslint-disable-next-line no-unused-vars
|
|
||||||
async add(data: any, opts?: JobOptions) {
|
async add(data: any, opts?: JobOptions) {
|
||||||
const jobId = opts?.jobId?.toString()
|
const jobId = opts?.jobId?.toString()
|
||||||
if (jobId && this._queuedJobIds.has(jobId)) {
|
if (jobId && this._queuedJobIds.has(jobId)) {
|
||||||
|
@ -166,8 +165,7 @@ class InMemoryQueue implements Partial<Queue> {
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
async removeJobs(_pattern: string) {
|
||||||
async removeJobs(pattern: string) {
|
|
||||||
// no-op
|
// no-op
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -17,8 +17,8 @@ import {
|
||||||
ContextUser,
|
ContextUser,
|
||||||
CouchFindOptions,
|
CouchFindOptions,
|
||||||
DatabaseQueryOpts,
|
DatabaseQueryOpts,
|
||||||
SearchQuery,
|
SearchFilters,
|
||||||
SearchQueryOperators,
|
SearchFilterOperator,
|
||||||
SearchUsersRequest,
|
SearchUsersRequest,
|
||||||
User,
|
User,
|
||||||
} from "@budibase/types"
|
} from "@budibase/types"
|
||||||
|
@ -44,11 +44,11 @@ function removeUserPassword(users: User | User[]) {
|
||||||
return users
|
return users
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isSupportedUserSearch(query: SearchQuery) {
|
export function isSupportedUserSearch(query: SearchFilters) {
|
||||||
const allowed = [
|
const allowed = [
|
||||||
{ op: SearchQueryOperators.STRING, key: "email" },
|
{ op: SearchFilterOperator.STRING, key: "email" },
|
||||||
{ op: SearchQueryOperators.EQUAL, key: "_id" },
|
{ op: SearchFilterOperator.EQUAL, key: "_id" },
|
||||||
{ op: SearchQueryOperators.ONE_OF, key: "_id" },
|
{ op: SearchFilterOperator.ONE_OF, key: "_id" },
|
||||||
]
|
]
|
||||||
for (let [key, operation] of Object.entries(query)) {
|
for (let [key, operation] of Object.entries(query)) {
|
||||||
if (typeof operation !== "object") {
|
if (typeof operation !== "object") {
|
||||||
|
|
|
@ -14,6 +14,7 @@
|
||||||
notifications,
|
notifications,
|
||||||
Checkbox,
|
Checkbox,
|
||||||
DatePicker,
|
DatePicker,
|
||||||
|
DrawerContent,
|
||||||
} from "@budibase/bbui"
|
} from "@budibase/bbui"
|
||||||
import CreateWebhookModal from "components/automation/Shared/CreateWebhookModal.svelte"
|
import CreateWebhookModal from "components/automation/Shared/CreateWebhookModal.svelte"
|
||||||
import { automationStore, selectedAutomation, tables } from "stores/builder"
|
import { automationStore, selectedAutomation, tables } from "stores/builder"
|
||||||
|
@ -37,7 +38,7 @@
|
||||||
hbAutocomplete,
|
hbAutocomplete,
|
||||||
EditorModes,
|
EditorModes,
|
||||||
} from "components/common/CodeEditor"
|
} from "components/common/CodeEditor"
|
||||||
import FilterDrawer from "components/design/settings/controls/FilterEditor/FilterDrawer.svelte"
|
import FilterBuilder from "components/design/settings/controls/FilterEditor/FilterBuilder.svelte"
|
||||||
import { LuceneUtils, Utils } from "@budibase/frontend-core"
|
import { LuceneUtils, Utils } from "@budibase/frontend-core"
|
||||||
import {
|
import {
|
||||||
getSchemaForDatasourcePlus,
|
getSchemaForDatasourcePlus,
|
||||||
|
@ -442,15 +443,16 @@
|
||||||
<Button cta slot="buttons" on:click={() => saveFilters(key)}>
|
<Button cta slot="buttons" on:click={() => saveFilters(key)}>
|
||||||
Save
|
Save
|
||||||
</Button>
|
</Button>
|
||||||
<FilterDrawer
|
<DrawerContent slot="body">
|
||||||
slot="body"
|
<FilterBuilder
|
||||||
{filters}
|
{filters}
|
||||||
{bindings}
|
{bindings}
|
||||||
{schemaFields}
|
{schemaFields}
|
||||||
datasource={{ type: "table", tableId }}
|
datasource={{ type: "table", tableId }}
|
||||||
panel={AutomationBindingPanel}
|
panel={AutomationBindingPanel}
|
||||||
on:change={e => (tempFilters = e.detail)}
|
on:change={e => (tempFilters = e.detail)}
|
||||||
/>
|
/>
|
||||||
|
</DrawerContent>
|
||||||
</Drawer>
|
</Drawer>
|
||||||
{:else if value.customType === "password"}
|
{:else if value.customType === "password"}
|
||||||
<Input
|
<Input
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
<script>
|
<script>
|
||||||
import { createEventDispatcher } from "svelte"
|
import { createEventDispatcher } from "svelte"
|
||||||
import { ActionButton, Modal, ModalContent } from "@budibase/bbui"
|
import { ActionButton, Modal, ModalContent } from "@budibase/bbui"
|
||||||
import FilterDrawer from "components/design/settings/controls/FilterEditor/FilterDrawer.svelte"
|
import FilterBuilder from "components/design/settings/controls/FilterEditor/FilterBuilder.svelte"
|
||||||
|
|
||||||
export let schema
|
export let schema
|
||||||
export let filters
|
export let filters
|
||||||
|
@ -40,7 +40,7 @@
|
||||||
onConfirm={() => dispatch("change", tempValue)}
|
onConfirm={() => dispatch("change", tempValue)}
|
||||||
>
|
>
|
||||||
<div class="wrapper">
|
<div class="wrapper">
|
||||||
<FilterDrawer
|
<FilterBuilder
|
||||||
allowBindings={false}
|
allowBindings={false}
|
||||||
{filters}
|
{filters}
|
||||||
{schemaFields}
|
{schemaFields}
|
||||||
|
|
|
@ -27,14 +27,6 @@
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function deleteAttachments(fileList) {
|
|
||||||
try {
|
|
||||||
return await API.deleteBuilderAttachments(fileList)
|
|
||||||
} catch (error) {
|
|
||||||
return []
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Dropzone
|
<Dropzone
|
||||||
|
@ -42,6 +34,5 @@
|
||||||
{label}
|
{label}
|
||||||
{...$$restProps}
|
{...$$restProps}
|
||||||
{processFiles}
|
{processFiles}
|
||||||
{deleteAttachments}
|
|
||||||
{handleFileTooLarge}
|
{handleFileTooLarge}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -9,7 +9,6 @@
|
||||||
"",
|
"",
|
||||||
requiredValidator
|
requiredValidator
|
||||||
)
|
)
|
||||||
// eslint-disable-next-line no-unused-vars
|
|
||||||
const [repeatPassword, _, repeatTouched] = createValidationStore(
|
const [repeatPassword, _, repeatTouched] = createValidationStore(
|
||||||
"",
|
"",
|
||||||
requiredValidator
|
requiredValidator
|
||||||
|
|
|
@ -0,0 +1,84 @@
|
||||||
|
<script>
|
||||||
|
import DrawerBindableInput from "components/common/bindings/DrawerBindableInput.svelte"
|
||||||
|
import ClientBindingPanel from "components/common/bindings/ClientBindingPanel.svelte"
|
||||||
|
|
||||||
|
import { dataFilters } from "@budibase/shared-core"
|
||||||
|
import { FilterBuilder } from "@budibase/frontend-core"
|
||||||
|
|
||||||
|
import { createEventDispatcher, onMount } from "svelte"
|
||||||
|
|
||||||
|
export let schemaFields
|
||||||
|
export let filters = []
|
||||||
|
export let bindings = []
|
||||||
|
export let panel = ClientBindingPanel
|
||||||
|
export let allowBindings = true
|
||||||
|
export let datasource
|
||||||
|
|
||||||
|
const dispatch = createEventDispatcher()
|
||||||
|
|
||||||
|
let rawFilters
|
||||||
|
|
||||||
|
$: parseFilters(rawFilters)
|
||||||
|
$: dispatch("change", enrichFilters(rawFilters))
|
||||||
|
|
||||||
|
// Remove field key prefixes and determine which behaviours to use
|
||||||
|
const parseFilters = filters => {
|
||||||
|
rawFilters = (filters || []).map(filter => {
|
||||||
|
const { field } = filter
|
||||||
|
let newFilter = { ...filter }
|
||||||
|
delete newFilter.allOr
|
||||||
|
newFilter.field = dataFilters.removeKeyNumbering(field)
|
||||||
|
return newFilter
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
parseFilters(filters)
|
||||||
|
rawFilters.forEach(filter => {
|
||||||
|
filter.type =
|
||||||
|
schemaFields.find(field => field.name === filter.field)?.type ||
|
||||||
|
filter.type
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// Add field key prefixes and a special metadata filter object to indicate
|
||||||
|
// how to handle filter behaviour
|
||||||
|
const enrichFilters = rawFilters => {
|
||||||
|
let count = 1
|
||||||
|
return rawFilters
|
||||||
|
.filter(filter => filter.field)
|
||||||
|
.map(filter => ({
|
||||||
|
...filter,
|
||||||
|
field: `${count++}:${filter.field}`,
|
||||||
|
}))
|
||||||
|
.concat(...rawFilters.filter(filter => !filter.field))
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<FilterBuilder
|
||||||
|
bind:filters={rawFilters}
|
||||||
|
behaviourFilters={true}
|
||||||
|
{schemaFields}
|
||||||
|
{datasource}
|
||||||
|
{allowBindings}
|
||||||
|
>
|
||||||
|
<div slot="filtering-hero-content" />
|
||||||
|
|
||||||
|
<DrawerBindableInput
|
||||||
|
let:filter
|
||||||
|
slot="binding"
|
||||||
|
disabled={filter.noValue}
|
||||||
|
title={filter.field}
|
||||||
|
value={filter.value}
|
||||||
|
placeholder="Value"
|
||||||
|
{panel}
|
||||||
|
{bindings}
|
||||||
|
on:change={event => {
|
||||||
|
const indexToUpdate = rawFilters.findIndex(f => f.id === filter.id)
|
||||||
|
rawFilters[indexToUpdate] = {
|
||||||
|
...rawFilters[indexToUpdate],
|
||||||
|
value: event.detail,
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</FilterBuilder>
|
|
@ -1,8 +1,14 @@
|
||||||
<script>
|
<script>
|
||||||
import { notifications, ActionButton, Button, Drawer } from "@budibase/bbui"
|
import {
|
||||||
|
notifications,
|
||||||
|
ActionButton,
|
||||||
|
Button,
|
||||||
|
Drawer,
|
||||||
|
DrawerContent,
|
||||||
|
} from "@budibase/bbui"
|
||||||
import { createEventDispatcher } from "svelte"
|
import { createEventDispatcher } from "svelte"
|
||||||
import { getDatasourceForProvider, getSchemaForDatasource } from "dataBinding"
|
import { getDatasourceForProvider, getSchemaForDatasource } from "dataBinding"
|
||||||
import FilterDrawer from "./FilterDrawer.svelte"
|
import FilterBuilder from "./FilterBuilder.svelte"
|
||||||
import { selectedScreen } from "stores/builder"
|
import { selectedScreen } from "stores/builder"
|
||||||
|
|
||||||
const dispatch = createEventDispatcher()
|
const dispatch = createEventDispatcher()
|
||||||
|
@ -40,14 +46,15 @@
|
||||||
</div>
|
</div>
|
||||||
<Drawer bind:this={drawer} title="Filtering" on:drawerHide on:drawerShow>
|
<Drawer bind:this={drawer} title="Filtering" on:drawerHide on:drawerShow>
|
||||||
<Button cta slot="buttons" on:click={saveFilter}>Save</Button>
|
<Button cta slot="buttons" on:click={saveFilter}>Save</Button>
|
||||||
<FilterDrawer
|
<DrawerContent slot="body">
|
||||||
slot="body"
|
<FilterBuilder
|
||||||
filters={value}
|
filters={value}
|
||||||
{bindings}
|
{bindings}
|
||||||
{schemaFields}
|
{schemaFields}
|
||||||
{datasource}
|
{datasource}
|
||||||
on:change={e => (tempValue = e.detail)}
|
on:change={e => (tempValue = e.detail)}
|
||||||
/>
|
/>
|
||||||
|
</DrawerContent>
|
||||||
</Drawer>
|
</Drawer>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
|
|
@ -4,8 +4,6 @@ import {
|
||||||
createDatasourceCreationStore,
|
createDatasourceCreationStore,
|
||||||
} from "./datasourceCreation"
|
} from "./datasourceCreation"
|
||||||
import { get } from "svelte/store"
|
import { get } from "svelte/store"
|
||||||
// eslint-disable-next-line no-unused-vars
|
|
||||||
import { shouldIntegrationFetchTableNames } from "stores/selectors"
|
|
||||||
|
|
||||||
vi.mock("stores/selectors", () => ({
|
vi.mock("stores/selectors", () => ({
|
||||||
shouldIntegrationFetchTableNames: vi.fn(),
|
shouldIntegrationFetchTableNames: vi.fn(),
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
import { it, expect, describe, beforeEach, vi } from "vitest"
|
import { it, expect, describe, beforeEach, vi } from "vitest"
|
||||||
import { createOnGoogleAuthStore } from "./onGoogleAuth"
|
import { createOnGoogleAuthStore } from "./onGoogleAuth"
|
||||||
import { writable, get } from "svelte/store"
|
import { writable, get } from "svelte/store"
|
||||||
// eslint-disable-next-line no-unused-vars
|
// eslint-disable-next-line
|
||||||
import { params } from "@roxi/routify"
|
import { params } from "@roxi/routify"
|
||||||
// eslint-disable-next-line no-unused-vars
|
// eslint-disable-next-line
|
||||||
import { integrations } from "stores/builder"
|
import { integrations } from "stores/builder"
|
||||||
import { IntegrationTypes } from "constants/backend"
|
import { IntegrationTypes } from "constants/backend"
|
||||||
|
|
||||||
|
|
|
@ -105,7 +105,6 @@ export function getAppService(path: string) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function updateDockerComposeService(
|
export function updateDockerComposeService(
|
||||||
// eslint-disable-next-line no-unused-vars
|
|
||||||
updateFn: (service: DockerCompose) => void
|
updateFn: (service: DockerCompose) => void
|
||||||
) {
|
) {
|
||||||
const opts = ["docker-compose.yaml", "docker-compose.yml"]
|
const opts = ["docker-compose.yaml", "docker-compose.yml"]
|
||||||
|
|
|
@ -157,6 +157,11 @@
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Use normal theme colors for links when using a top nav */
|
||||||
|
.dropdown:not(.left) .sublinks a {
|
||||||
|
color: var(--spectrum-alias-text-color);
|
||||||
|
}
|
||||||
|
|
||||||
/* Left dropdowns */
|
/* Left dropdowns */
|
||||||
.dropdown.left .sublinks-wrapper {
|
.dropdown.left .sublinks-wrapper {
|
||||||
display: none;
|
display: none;
|
||||||
|
|
|
@ -1,216 +1,14 @@
|
||||||
<script>
|
<script>
|
||||||
import {
|
import { FilterBuilder } from "@budibase/frontend-core"
|
||||||
Body,
|
|
||||||
Button,
|
|
||||||
Combobox,
|
|
||||||
DatePicker,
|
|
||||||
Icon,
|
|
||||||
Input,
|
|
||||||
Layout,
|
|
||||||
Select,
|
|
||||||
} from "@budibase/bbui"
|
|
||||||
import { generate } from "shortid"
|
|
||||||
import { LuceneUtils, Constants } from "@budibase/frontend-core"
|
|
||||||
import { getContext } from "svelte"
|
|
||||||
|
|
||||||
export let schemaFields
|
export let schemaFields
|
||||||
export let filters = []
|
export let filters = []
|
||||||
export let datasource
|
export let datasource
|
||||||
|
|
||||||
const context = getContext("context")
|
|
||||||
const BannedTypes = ["link", "attachment", "json"]
|
|
||||||
|
|
||||||
$: fieldOptions = (schemaFields ?? [])
|
|
||||||
.filter(
|
|
||||||
field =>
|
|
||||||
!BannedTypes.includes(field.type) ||
|
|
||||||
(field.type === "formula" && field.formulaType === "static")
|
|
||||||
)
|
|
||||||
.map(field => ({
|
|
||||||
label: field.displayName || field.name,
|
|
||||||
value: field.name,
|
|
||||||
}))
|
|
||||||
|
|
||||||
const addFilter = () => {
|
|
||||||
filters = [
|
|
||||||
...filters,
|
|
||||||
{
|
|
||||||
id: generate(),
|
|
||||||
field: null,
|
|
||||||
operator: Constants.OperatorOptions.Equals.value,
|
|
||||||
value: null,
|
|
||||||
valueType: "Value",
|
|
||||||
},
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
const removeFilter = id => {
|
|
||||||
filters = filters.filter(field => field.id !== id)
|
|
||||||
}
|
|
||||||
|
|
||||||
const duplicateFilter = id => {
|
|
||||||
const existingFilter = filters.find(filter => filter.id === id)
|
|
||||||
const duplicate = { ...existingFilter, id: generate() }
|
|
||||||
filters = [...filters, duplicate]
|
|
||||||
}
|
|
||||||
|
|
||||||
const onFieldChange = (expression, field) => {
|
|
||||||
// Update the field type
|
|
||||||
expression.type = schemaFields.find(x => x.name === field)?.type
|
|
||||||
expression.externalType = schemaFields.find(
|
|
||||||
x => x.name === field
|
|
||||||
)?.externalType
|
|
||||||
|
|
||||||
// Ensure a valid operator is set
|
|
||||||
const validOperators = LuceneUtils.getValidOperatorsForType(
|
|
||||||
{ type: expression.type },
|
|
||||||
expression.field,
|
|
||||||
datasource
|
|
||||||
).map(x => x.value)
|
|
||||||
if (!validOperators.includes(expression.operator)) {
|
|
||||||
expression.operator =
|
|
||||||
validOperators[0] ?? Constants.OperatorOptions.Equals.value
|
|
||||||
onOperatorChange(expression, expression.operator)
|
|
||||||
}
|
|
||||||
|
|
||||||
// if changed to an array, change default value to empty array
|
|
||||||
const idx = filters.findIndex(x => x.field === field)
|
|
||||||
if (expression.type === "array") {
|
|
||||||
filters[idx].value = []
|
|
||||||
} else {
|
|
||||||
filters[idx].value = null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const onOperatorChange = (expression, operator) => {
|
|
||||||
const noValueOptions = [
|
|
||||||
Constants.OperatorOptions.Empty.value,
|
|
||||||
Constants.OperatorOptions.NotEmpty.value,
|
|
||||||
]
|
|
||||||
expression.noValue = noValueOptions.includes(operator)
|
|
||||||
if (expression.noValue) {
|
|
||||||
expression.value = null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const getFieldOptions = field => {
|
|
||||||
const schema = schemaFields.find(x => x.name === field)
|
|
||||||
return schema?.constraints?.inclusion || []
|
|
||||||
}
|
|
||||||
|
|
||||||
const getSchema = filter => {
|
|
||||||
return schemaFields.find(field => field.name === filter.field)
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="container" class:mobile={$context.device.mobile}>
|
<FilterBuilder bind:filters {schemaFields} {datasource} filtersLabel={null}>
|
||||||
<Layout noPadding>
|
<div slot="filtering-hero-content">
|
||||||
<Body size="S">
|
Results are filtered to only those which match all of the following
|
||||||
{#if !filters?.length}
|
constraints.
|
||||||
Add your first filter expression.
|
</div>
|
||||||
{:else}
|
</FilterBuilder>
|
||||||
Results are filtered to only those which match all of the following
|
|
||||||
constraints.
|
|
||||||
{/if}
|
|
||||||
</Body>
|
|
||||||
{#if filters?.length}
|
|
||||||
<div class="fields">
|
|
||||||
{#each filters as filter}
|
|
||||||
<Select
|
|
||||||
bind:value={filter.field}
|
|
||||||
options={fieldOptions}
|
|
||||||
on:change={e => onFieldChange(filter, e.detail)}
|
|
||||||
placeholder="Column"
|
|
||||||
/>
|
|
||||||
<Select
|
|
||||||
disabled={!filter.field}
|
|
||||||
options={LuceneUtils.getValidOperatorsForType(
|
|
||||||
{ type: filter.type, subtype: filter.subtype },
|
|
||||||
filter.field,
|
|
||||||
datasource
|
|
||||||
)}
|
|
||||||
bind:value={filter.operator}
|
|
||||||
on:change={e => onOperatorChange(filter, e.detail)}
|
|
||||||
placeholder={null}
|
|
||||||
/>
|
|
||||||
{#if ["string", "longform", "number", "bigint", "formula"].includes(filter.type)}
|
|
||||||
<Input disabled={filter.noValue} bind:value={filter.value} />
|
|
||||||
{:else if ["options", "array"].includes(filter.type)}
|
|
||||||
<Combobox
|
|
||||||
disabled={filter.noValue}
|
|
||||||
options={getFieldOptions(filter.field)}
|
|
||||||
bind:value={filter.value}
|
|
||||||
/>
|
|
||||||
{:else if filter.type === "boolean"}
|
|
||||||
<Combobox
|
|
||||||
disabled={filter.noValue}
|
|
||||||
options={[
|
|
||||||
{ label: "True", value: "true" },
|
|
||||||
{ label: "False", value: "false" },
|
|
||||||
]}
|
|
||||||
bind:value={filter.value}
|
|
||||||
/>
|
|
||||||
{:else if filter.type === "datetime"}
|
|
||||||
<DatePicker
|
|
||||||
disabled={filter.noValue}
|
|
||||||
enableTime={!getSchema(filter).dateOnly}
|
|
||||||
timeOnly={getSchema(filter).timeOnly}
|
|
||||||
bind:value={filter.value}
|
|
||||||
/>
|
|
||||||
{:else}
|
|
||||||
<Input disabled />
|
|
||||||
{/if}
|
|
||||||
<div class="controls">
|
|
||||||
<Icon
|
|
||||||
name="Duplicate"
|
|
||||||
hoverable
|
|
||||||
size="S"
|
|
||||||
on:click={() => duplicateFilter(filter.id)}
|
|
||||||
/>
|
|
||||||
<Icon
|
|
||||||
name="Close"
|
|
||||||
hoverable
|
|
||||||
size="S"
|
|
||||||
on:click={() => removeFilter(filter.id)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
{/each}
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
<div>
|
|
||||||
<Button icon="AddCircle" size="M" secondary on:click={addFilter}>
|
|
||||||
Add filter
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</Layout>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.container {
|
|
||||||
width: 100%;
|
|
||||||
max-width: 1000px;
|
|
||||||
margin: 0 auto;
|
|
||||||
}
|
|
||||||
.fields {
|
|
||||||
display: grid;
|
|
||||||
column-gap: var(--spacing-l);
|
|
||||||
row-gap: var(--spacing-s);
|
|
||||||
align-items: center;
|
|
||||||
grid-template-columns: 1fr 120px 1fr auto auto;
|
|
||||||
}
|
|
||||||
.controls {
|
|
||||||
display: contents;
|
|
||||||
}
|
|
||||||
|
|
||||||
.container.mobile .fields {
|
|
||||||
grid-template-columns: 1fr;
|
|
||||||
}
|
|
||||||
.container.mobile .controls {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
justify-content: flex-start;
|
|
||||||
align-items: center;
|
|
||||||
padding: var(--spacing-s) 0;
|
|
||||||
gap: var(--spacing-s);
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
|
@ -58,17 +58,6 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const deleteAttachments = async fileList => {
|
|
||||||
try {
|
|
||||||
return await API.deleteAttachments({
|
|
||||||
keys: fileList,
|
|
||||||
tableId: formContext?.dataSource?.tableId,
|
|
||||||
})
|
|
||||||
} catch (error) {
|
|
||||||
return []
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleChange = e => {
|
const handleChange = e => {
|
||||||
const value = fieldApiMapper.set(e.detail)
|
const value = fieldApiMapper.set(e.detail)
|
||||||
const changed = fieldApi.setValue(value)
|
const changed = fieldApi.setValue(value)
|
||||||
|
@ -98,7 +87,6 @@
|
||||||
error={fieldState.error}
|
error={fieldState.error}
|
||||||
on:change={handleChange}
|
on:change={handleChange}
|
||||||
{processFiles}
|
{processFiles}
|
||||||
{deleteAttachments}
|
|
||||||
{handleFileTooLarge}
|
{handleFileTooLarge}
|
||||||
{handleTooManyFiles}
|
{handleTooManyFiles}
|
||||||
{maximum}
|
{maximum}
|
||||||
|
|
|
@ -11,6 +11,7 @@
|
||||||
"@budibase/types": "0.0.0",
|
"@budibase/types": "0.0.0",
|
||||||
"dayjs": "^1.10.8",
|
"dayjs": "^1.10.8",
|
||||||
"lodash": "4.17.21",
|
"lodash": "4.17.21",
|
||||||
|
"shortid": "2.2.15",
|
||||||
"socket.io-client": "^4.6.1"
|
"socket.io-client": "^4.6.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -61,34 +61,6 @@ export const buildAttachmentEndpoints = API => {
|
||||||
})
|
})
|
||||||
return { publicUrl }
|
return { publicUrl }
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
|
||||||
* Deletes attachments from the bucket.
|
|
||||||
* @param keys the attachments to delete
|
|
||||||
* @param tableId the associated table ID
|
|
||||||
*/
|
|
||||||
deleteAttachments: async ({ keys, tableId }) => {
|
|
||||||
return await API.post({
|
|
||||||
url: `/api/attachments/${tableId}/delete`,
|
|
||||||
body: {
|
|
||||||
keys,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Deletes attachments from the builder bucket.
|
|
||||||
* @param keys the attachments to delete
|
|
||||||
*/
|
|
||||||
deleteBuilderAttachments: async keys => {
|
|
||||||
return await API.post({
|
|
||||||
url: `/api/attachments/delete`,
|
|
||||||
body: {
|
|
||||||
keys,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Download an attachment from a row given its column name.
|
* Download an attachment from a row given its column name.
|
||||||
* @param datasourceId the ID of the datasource to download from
|
* @param datasourceId the ID of the datasource to download from
|
||||||
|
|
|
@ -4,33 +4,36 @@
|
||||||
Button,
|
Button,
|
||||||
Combobox,
|
Combobox,
|
||||||
DatePicker,
|
DatePicker,
|
||||||
DrawerContent,
|
|
||||||
Icon,
|
Icon,
|
||||||
Input,
|
Input,
|
||||||
Label,
|
|
||||||
Layout,
|
Layout,
|
||||||
Multiselect,
|
|
||||||
Select,
|
Select,
|
||||||
|
Label,
|
||||||
|
Multiselect,
|
||||||
} from "@budibase/bbui"
|
} from "@budibase/bbui"
|
||||||
import DrawerBindableInput from "components/common/bindings/DrawerBindableInput.svelte"
|
import { FieldType, SearchFilterOperator } from "@budibase/types"
|
||||||
import ClientBindingPanel from "components/common/bindings/ClientBindingPanel.svelte"
|
|
||||||
import { generate } from "shortid"
|
import { generate } from "shortid"
|
||||||
import { Constants, LuceneUtils } from "@budibase/frontend-core"
|
import { LuceneUtils, Constants } from "@budibase/frontend-core"
|
||||||
import { getFields } from "helpers/searchFields"
|
import { getContext } from "svelte"
|
||||||
import { FieldType } from "@budibase/types"
|
|
||||||
import { createEventDispatcher, onMount } from "svelte"
|
|
||||||
import FilterUsers from "./FilterUsers.svelte"
|
import FilterUsers from "./FilterUsers.svelte"
|
||||||
|
|
||||||
|
const { OperatorOptions } = Constants
|
||||||
|
|
||||||
export let schemaFields
|
export let schemaFields
|
||||||
export let filters = []
|
export let filters = []
|
||||||
export let bindings = []
|
|
||||||
export let panel = ClientBindingPanel
|
|
||||||
export let allowBindings = true
|
|
||||||
export let datasource
|
export let datasource
|
||||||
|
export let behaviourFilters = false
|
||||||
|
export let allowBindings = false
|
||||||
|
export let filtersLabel = "Filters"
|
||||||
|
|
||||||
|
$: matchAny = filters?.find(filter => filter.operator === "allOr") != null
|
||||||
|
$: onEmptyFilter =
|
||||||
|
filters?.find(filter => filter.onEmptyFilter)?.onEmptyFilter ?? "all"
|
||||||
|
|
||||||
|
$: fieldFilters = filters.filter(
|
||||||
|
filter => filter.operator !== "allOr" && !filter.onEmptyFilter
|
||||||
|
)
|
||||||
|
|
||||||
const dispatch = createEventDispatcher()
|
|
||||||
const { OperatorOptions } = Constants
|
|
||||||
const KeyedFieldRegex = /\d[0-9]*:/g
|
|
||||||
const behaviourOptions = [
|
const behaviourOptions = [
|
||||||
{ value: "and", label: "Match all filters" },
|
{ value: "and", label: "Match all filters" },
|
||||||
{ value: "or", label: "Match any filter" },
|
{ value: "or", label: "Match any filter" },
|
||||||
|
@ -40,62 +43,18 @@
|
||||||
{ value: "none", label: "Return no rows" },
|
{ value: "none", label: "Return no rows" },
|
||||||
]
|
]
|
||||||
|
|
||||||
let rawFilters
|
const context = getContext("context")
|
||||||
let matchAny = false
|
|
||||||
let onEmptyFilter = "all"
|
|
||||||
|
|
||||||
$: parseFilters(filters)
|
$: fieldOptions = (schemaFields ?? [])
|
||||||
$: dispatch("change", enrichFilters(rawFilters, matchAny, onEmptyFilter))
|
.filter(field => getValidOperatorsForType(field).length)
|
||||||
$: enrichedSchemaFields = getFields(schemaFields || [], { allowLinks: true })
|
.map(field => ({
|
||||||
$: fieldOptions = enrichedSchemaFields.map(field => field.name) || []
|
label: field.displayName || field.name,
|
||||||
$: valueTypeOptions = allowBindings ? ["Value", "Binding"] : ["Value"]
|
value: field.name,
|
||||||
|
}))
|
||||||
// Remove field key prefixes and determine which behaviours to use
|
|
||||||
const parseFilters = filters => {
|
|
||||||
matchAny = filters?.find(filter => filter.operator === "allOr") != null
|
|
||||||
onEmptyFilter =
|
|
||||||
filters?.find(filter => filter.onEmptyFilter)?.onEmptyFilter ?? "all"
|
|
||||||
rawFilters = (filters || [])
|
|
||||||
.filter(filter => filter.operator !== "allOr" && !filter.onEmptyFilter)
|
|
||||||
.map(filter => {
|
|
||||||
const { field } = filter
|
|
||||||
let newFilter = { ...filter }
|
|
||||||
delete newFilter.allOr
|
|
||||||
if (typeof field === "string" && field.match(KeyedFieldRegex) != null) {
|
|
||||||
const parts = field.split(":")
|
|
||||||
parts.shift()
|
|
||||||
newFilter.field = parts.join(":")
|
|
||||||
}
|
|
||||||
return newFilter
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
onMount(() => {
|
|
||||||
parseFilters(filters)
|
|
||||||
rawFilters.forEach(filter => {
|
|
||||||
filter.type =
|
|
||||||
schemaFields.find(field => field.name === filter.field)?.type ||
|
|
||||||
filter.type
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
// Add field key prefixes and a special metadata filter object to indicate
|
|
||||||
// how to handle filter behaviour
|
|
||||||
const enrichFilters = (rawFilters, matchAny, onEmptyFilter) => {
|
|
||||||
let count = 1
|
|
||||||
return rawFilters
|
|
||||||
.filter(filter => filter.field)
|
|
||||||
.map(filter => ({
|
|
||||||
...filter,
|
|
||||||
field: `${count++}:${filter.field}`,
|
|
||||||
}))
|
|
||||||
.concat(matchAny ? [{ operator: "allOr" }] : [])
|
|
||||||
.concat([{ onEmptyFilter }])
|
|
||||||
}
|
|
||||||
|
|
||||||
const addFilter = () => {
|
const addFilter = () => {
|
||||||
rawFilters = [
|
filters = [
|
||||||
...rawFilters,
|
...(filters || []),
|
||||||
{
|
{
|
||||||
id: generate(),
|
id: generate(),
|
||||||
field: null,
|
field: null,
|
||||||
|
@ -107,22 +66,57 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
const removeFilter = id => {
|
const removeFilter = id => {
|
||||||
rawFilters = rawFilters.filter(field => field.id !== id)
|
filters = filters.filter(field => field.id !== id)
|
||||||
}
|
}
|
||||||
|
|
||||||
const duplicateFilter = id => {
|
const duplicateFilter = id => {
|
||||||
const existingFilter = rawFilters.find(filter => filter.id === id)
|
const existingFilter = filters.find(filter => filter.id === id)
|
||||||
const duplicate = { ...existingFilter, id: generate() }
|
const duplicate = { ...existingFilter, id: generate() }
|
||||||
rawFilters = [...rawFilters, duplicate]
|
filters = [...filters, duplicate]
|
||||||
|
}
|
||||||
|
|
||||||
|
const onFieldChange = filter => {
|
||||||
|
const previousType = filter.type
|
||||||
|
sanitizeTypes(filter)
|
||||||
|
sanitizeOperator(filter)
|
||||||
|
sanitizeValue(filter, previousType)
|
||||||
|
}
|
||||||
|
|
||||||
|
const onOperatorChange = filter => {
|
||||||
|
sanitizeOperator(filter)
|
||||||
|
sanitizeValue(filter, filter.type)
|
||||||
|
}
|
||||||
|
|
||||||
|
const onValueTypeChange = filter => {
|
||||||
|
sanitizeValue(filter)
|
||||||
|
}
|
||||||
|
|
||||||
|
const getFieldOptions = field => {
|
||||||
|
const schema = schemaFields.find(x => x.name === field)
|
||||||
|
return schema?.constraints?.inclusion || []
|
||||||
}
|
}
|
||||||
|
|
||||||
const getSchema = filter => {
|
const getSchema = filter => {
|
||||||
return enrichedSchemaFields.find(field => field.name === filter.field)
|
return schemaFields.find(field => field.name === filter.field)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const getValidOperatorsForType = filter => {
|
||||||
|
if (!filter?.field && !filter?.name) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
return LuceneUtils.getValidOperatorsForType(
|
||||||
|
filter,
|
||||||
|
filter.field || filter.name,
|
||||||
|
datasource
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
$: valueTypeOptions = allowBindings ? ["Value", "Binding"] : ["Value"]
|
||||||
|
|
||||||
const sanitizeTypes = filter => {
|
const sanitizeTypes = filter => {
|
||||||
// Update type based on field
|
// Update type based on field
|
||||||
const fieldSchema = enrichedSchemaFields.find(x => x.name === filter.field)
|
const fieldSchema = schemaFields.find(x => x.name === filter.field)
|
||||||
filter.type = fieldSchema?.type
|
filter.type = fieldSchema?.type
|
||||||
filter.subtype = fieldSchema?.subtype
|
filter.subtype = fieldSchema?.subtype
|
||||||
|
|
||||||
|
@ -154,88 +148,79 @@
|
||||||
|
|
||||||
// Ensure array values are properly set and cleared
|
// Ensure array values are properly set and cleared
|
||||||
if (Array.isArray(filter.value)) {
|
if (Array.isArray(filter.value)) {
|
||||||
if (filter.valueType !== "Value" || filter.type !== "array") {
|
if (filter.valueType !== "Value" || filter.type !== FieldType.ARRAY) {
|
||||||
filter.value = null
|
filter.value = null
|
||||||
}
|
}
|
||||||
} else if (filter.type === "array" && filter.valueType === "Value") {
|
} else if (
|
||||||
|
filter.type === FieldType.ARRAY &&
|
||||||
|
filter.valueType === "Value"
|
||||||
|
) {
|
||||||
filter.value = []
|
filter.value = []
|
||||||
} else if (
|
} else if (
|
||||||
previousType !== filter.type &&
|
previousType !== filter.type &&
|
||||||
(previousType === FieldType.BB_REFERENCE ||
|
(previousType === FieldType.BB_REFERENCE ||
|
||||||
filter.type === FieldType.BB_REFERENCE)
|
filter.type === FieldType.BB_REFERENCE)
|
||||||
) {
|
) {
|
||||||
filter.value = filter.type === "array" ? [] : null
|
filter.value = filter.type === FieldType.ARRAY ? [] : null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const onFieldChange = filter => {
|
function handleAllOr(option) {
|
||||||
const previousType = filter.type
|
filters = filters.filter(f => f.operator !== "allOr")
|
||||||
sanitizeTypes(filter)
|
if (option === "or") {
|
||||||
sanitizeOperator(filter)
|
filters.push({ operator: "allOr" })
|
||||||
sanitizeValue(filter, previousType)
|
|
||||||
}
|
|
||||||
|
|
||||||
const onOperatorChange = filter => {
|
|
||||||
sanitizeOperator(filter)
|
|
||||||
sanitizeValue(filter, filter.type)
|
|
||||||
}
|
|
||||||
|
|
||||||
const onValueTypeChange = filter => {
|
|
||||||
sanitizeValue(filter)
|
|
||||||
}
|
|
||||||
|
|
||||||
const getFieldOptions = field => {
|
|
||||||
const schema = enrichedSchemaFields.find(x => x.name === field)
|
|
||||||
return schema?.constraints?.inclusion || []
|
|
||||||
}
|
|
||||||
|
|
||||||
const getValidOperatorsForType = filter => {
|
|
||||||
if (!filter?.field) {
|
|
||||||
return []
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return LuceneUtils.getValidOperatorsForType(
|
function handleOnEmptyFilter(value) {
|
||||||
{ type: filter.type, subtype: filter.subtype },
|
filters = filters?.filter(filter => !filter.onEmptyFilter)
|
||||||
filter.field,
|
filters.push({ onEmptyFilter: value })
|
||||||
datasource
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<DrawerContent>
|
<div class="container" class:mobile={$context?.device?.mobile}>
|
||||||
<div class="container">
|
<Layout noPadding>
|
||||||
<Layout noPadding>
|
{#if fieldOptions?.length}
|
||||||
{#if !rawFilters?.length}
|
<Body size="S">
|
||||||
<Body size="S">Add your first filter expression.</Body>
|
{#if !fieldFilters?.length}
|
||||||
{:else}
|
Add your first filter expression.
|
||||||
<div class="fields">
|
{:else}
|
||||||
<Select
|
<slot name="filtering-hero-content" />
|
||||||
label="Behaviour"
|
{#if behaviourFilters}
|
||||||
value={matchAny ? "or" : "and"}
|
<div class="behaviour-filters">
|
||||||
options={behaviourOptions}
|
<Select
|
||||||
getOptionLabel={opt => opt.label}
|
label="Behaviour"
|
||||||
getOptionValue={opt => opt.value}
|
value={matchAny ? "or" : "and"}
|
||||||
on:change={e => (matchAny = e.detail === "or")}
|
options={behaviourOptions}
|
||||||
placeholder={null}
|
getOptionLabel={opt => opt.label}
|
||||||
/>
|
getOptionValue={opt => opt.value}
|
||||||
{#if datasource?.type === "table"}
|
on:change={e => handleAllOr(e.detail)}
|
||||||
<Select
|
placeholder={null}
|
||||||
label="When filter empty"
|
/>
|
||||||
value={onEmptyFilter}
|
{#if datasource?.type === "table"}
|
||||||
options={onEmptyOptions}
|
<Select
|
||||||
getOptionLabel={opt => opt.label}
|
label="When filter empty"
|
||||||
getOptionValue={opt => opt.value}
|
value={onEmptyFilter}
|
||||||
on:change={e => (onEmptyFilter = e.detail)}
|
options={onEmptyOptions}
|
||||||
placeholder={null}
|
getOptionLabel={opt => opt.label}
|
||||||
/>
|
getOptionValue={opt => opt.value}
|
||||||
|
on:change={e => handleOnEmptyFilter(e.detail)}
|
||||||
|
placeholder={null}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
{/if}
|
||||||
|
</Body>
|
||||||
|
{#if fieldFilters?.length}
|
||||||
<div>
|
<div>
|
||||||
<div class="filter-label">
|
{#if filtersLabel}
|
||||||
<Label>Filters</Label>
|
<div class="filter-label">
|
||||||
</div>
|
<Label>{filtersLabel}</Label>
|
||||||
<div class="fields">
|
</div>
|
||||||
{#each rawFilters as filter}
|
{/if}
|
||||||
|
<div class="fields" class:with-bindings={allowBindings}>
|
||||||
|
{#each fieldFilters as filter}
|
||||||
<Select
|
<Select
|
||||||
bind:value={filter.field}
|
bind:value={filter.field}
|
||||||
options={fieldOptions}
|
options={fieldOptions}
|
||||||
|
@ -249,38 +234,32 @@
|
||||||
on:change={() => onOperatorChange(filter)}
|
on:change={() => onOperatorChange(filter)}
|
||||||
placeholder={null}
|
placeholder={null}
|
||||||
/>
|
/>
|
||||||
<Select
|
{#if allowBindings}
|
||||||
disabled={filter.noValue || !filter.field}
|
<Select
|
||||||
options={valueTypeOptions}
|
disabled={filter.noValue || !filter.field}
|
||||||
bind:value={filter.valueType}
|
options={valueTypeOptions}
|
||||||
on:change={() => onValueTypeChange(filter)}
|
bind:value={filter.valueType}
|
||||||
placeholder={null}
|
on:change={() => onValueTypeChange(filter)}
|
||||||
/>
|
placeholder={null}
|
||||||
{#if filter.field && filter.valueType === "Binding"}
|
|
||||||
<DrawerBindableInput
|
|
||||||
disabled={filter.noValue}
|
|
||||||
title={filter.field}
|
|
||||||
value={filter.value}
|
|
||||||
placeholder="Value"
|
|
||||||
{panel}
|
|
||||||
{bindings}
|
|
||||||
on:change={event => (filter.value = event.detail)}
|
|
||||||
/>
|
/>
|
||||||
{:else if ["string", "longform", "number", "bigint", "formula"].includes(filter.type)}
|
{/if}
|
||||||
|
{#if allowBindings && filter.field && filter.valueType === "Binding"}
|
||||||
|
<slot name="binding" {filter} />
|
||||||
|
{:else if [FieldType.STRING, FieldType.LONGFORM, FieldType.NUMBER, FieldType.BIGINT, FieldType.FORMULA].includes(filter.type)}
|
||||||
<Input disabled={filter.noValue} bind:value={filter.value} />
|
<Input disabled={filter.noValue} bind:value={filter.value} />
|
||||||
{:else if filter.type === "array" || (filter.type === "options" && filter.operator === "oneOf")}
|
{:else if filter.type === FieldType.ARRAY || (filter.type === FieldType.OPTIONS && filter.operator === SearchFilterOperator.ONE_OF)}
|
||||||
<Multiselect
|
<Multiselect
|
||||||
disabled={filter.noValue}
|
disabled={filter.noValue}
|
||||||
options={getFieldOptions(filter.field)}
|
options={getFieldOptions(filter.field)}
|
||||||
bind:value={filter.value}
|
bind:value={filter.value}
|
||||||
/>
|
/>
|
||||||
{:else if filter.type === "options"}
|
{:else if filter.type === FieldType.OPTIONS}
|
||||||
<Combobox
|
<Combobox
|
||||||
disabled={filter.noValue}
|
disabled={filter.noValue}
|
||||||
options={getFieldOptions(filter.field)}
|
options={getFieldOptions(filter.field)}
|
||||||
bind:value={filter.value}
|
bind:value={filter.value}
|
||||||
/>
|
/>
|
||||||
{:else if filter.type === "boolean"}
|
{:else if filter.type === FieldType.BOOLEAN}
|
||||||
<Combobox
|
<Combobox
|
||||||
disabled={filter.noValue}
|
disabled={filter.noValue}
|
||||||
options={[
|
options={[
|
||||||
|
@ -289,7 +268,7 @@
|
||||||
]}
|
]}
|
||||||
bind:value={filter.value}
|
bind:value={filter.value}
|
||||||
/>
|
/>
|
||||||
{:else if filter.type === "datetime"}
|
{:else if filter.type === FieldType.DATETIME}
|
||||||
<DatePicker
|
<DatePicker
|
||||||
disabled={filter.noValue}
|
disabled={filter.noValue}
|
||||||
enableTime={!getSchema(filter)?.dateOnly}
|
enableTime={!getSchema(filter)?.dateOnly}
|
||||||
|
@ -306,32 +285,36 @@
|
||||||
disabled={filter.noValue}
|
disabled={filter.noValue}
|
||||||
/>
|
/>
|
||||||
{:else}
|
{:else}
|
||||||
<DrawerBindableInput disabled />
|
<Input disabled />
|
||||||
{/if}
|
{/if}
|
||||||
<Icon
|
<div class="controls">
|
||||||
name="Duplicate"
|
<Icon
|
||||||
hoverable
|
name="Duplicate"
|
||||||
size="S"
|
hoverable
|
||||||
on:click={() => duplicateFilter(filter.id)}
|
size="S"
|
||||||
/>
|
on:click={() => duplicateFilter(filter.id)}
|
||||||
<Icon
|
/>
|
||||||
name="Close"
|
<Icon
|
||||||
hoverable
|
name="Close"
|
||||||
size="S"
|
hoverable
|
||||||
on:click={() => removeFilter(filter.id)}
|
size="S"
|
||||||
/>
|
on:click={() => removeFilter(filter.id)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
<div class="bottom">
|
<div>
|
||||||
<Button icon="AddCircle" size="M" secondary on:click={addFilter}>
|
<Button icon="AddCircle" size="M" secondary on:click={addFilter}>
|
||||||
Add filter
|
Add filter
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</Layout>
|
{:else}
|
||||||
</div>
|
<Body size="S">None of the table column can be used for filtering.</Body>
|
||||||
</DrawerContent>
|
{/if}
|
||||||
|
</Layout>
|
||||||
|
</div>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.container {
|
.container {
|
||||||
|
@ -339,22 +322,42 @@
|
||||||
max-width: 1000px;
|
max-width: 1000px;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.fields {
|
.fields {
|
||||||
display: grid;
|
display: grid;
|
||||||
column-gap: var(--spacing-l);
|
column-gap: var(--spacing-l);
|
||||||
row-gap: var(--spacing-s);
|
row-gap: var(--spacing-s);
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
grid-template-columns: 1fr 120px 1fr auto auto;
|
||||||
|
}
|
||||||
|
.fields.with-bindings {
|
||||||
grid-template-columns: minmax(150px, 1fr) 170px 120px minmax(150px, 1fr) 16px 16px;
|
grid-template-columns: minmax(150px, 1fr) 170px 120px minmax(150px, 1fr) 16px 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.controls {
|
||||||
|
display: contents;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container.mobile .fields {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
.container.mobile .controls {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: flex-start;
|
||||||
|
align-items: center;
|
||||||
|
padding: var(--spacing-s) 0;
|
||||||
|
gap: var(--spacing-s);
|
||||||
|
}
|
||||||
|
|
||||||
.filter-label {
|
.filter-label {
|
||||||
margin-bottom: var(--spacing-s);
|
margin-bottom: var(--spacing-s);
|
||||||
}
|
}
|
||||||
|
|
||||||
.bottom {
|
.behaviour-filters {
|
||||||
display: flex;
|
display: grid;
|
||||||
justify-content: space-between;
|
column-gap: var(--spacing-l);
|
||||||
|
row-gap: var(--spacing-s);
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
grid-template-columns: minmax(150px, 1fr) 170px 120px minmax(150px, 1fr) 16px 16px;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
|
@ -1,9 +1,9 @@
|
||||||
<script>
|
<script>
|
||||||
import { Select, Multiselect } from "@budibase/bbui"
|
import { Select, Multiselect } from "@budibase/bbui"
|
||||||
import { fetchData } from "@budibase/frontend-core"
|
import { fetchData } from "@budibase/frontend-core"
|
||||||
|
import { createAPIClient } from "../api"
|
||||||
|
|
||||||
import { API } from "api"
|
export let API = createAPIClient()
|
||||||
|
|
||||||
export let value = null
|
export let value = null
|
||||||
export let disabled
|
export let disabled
|
||||||
export let multiselect = false
|
export let multiselect = false
|
|
@ -61,14 +61,6 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const deleteAttachments = async fileList => {
|
|
||||||
try {
|
|
||||||
return await API.deleteBuilderAttachments(fileList)
|
|
||||||
} catch (error) {
|
|
||||||
return []
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
api = {
|
api = {
|
||||||
focus: () => open(),
|
focus: () => open(),
|
||||||
|
@ -101,7 +93,6 @@
|
||||||
on:change={e => onChange(e.detail)}
|
on:change={e => onChange(e.detail)}
|
||||||
maximum={maximum || schema.constraints?.length?.maximum}
|
maximum={maximum || schema.constraints?.length?.maximum}
|
||||||
{processFiles}
|
{processFiles}
|
||||||
{deleteAttachments}
|
|
||||||
{handleFileTooLarge}
|
{handleFileTooLarge}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -6,3 +6,4 @@ export { default as UserAvatars } from "./UserAvatars.svelte"
|
||||||
export { default as Updating } from "./Updating.svelte"
|
export { default as Updating } from "./Updating.svelte"
|
||||||
export { Grid } from "./grid"
|
export { Grid } from "./grid"
|
||||||
export { default as ClientAppSkeleton } from "./ClientAppSkeleton.svelte"
|
export { default as ClientAppSkeleton } from "./ClientAppSkeleton.svelte"
|
||||||
|
export { default as FilterBuilder } from "./FilterBuilder.svelte"
|
||||||
|
|
|
@ -348,8 +348,7 @@ export default class DataFetch {
|
||||||
* Determine the feature flag for this datasource definition
|
* Determine the feature flag for this datasource definition
|
||||||
* @param definition
|
* @param definition
|
||||||
*/
|
*/
|
||||||
// eslint-disable-next-line no-unused-vars
|
determineFeatureFlags(_definition) {
|
||||||
determineFeatureFlags(definition) {
|
|
||||||
return {
|
return {
|
||||||
supportsSearch: false,
|
supportsSearch: false,
|
||||||
supportsSort: false,
|
supportsSort: false,
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
Subproject commit ef186d00241f96037f9fd34d7a3826041977ab3a
|
Subproject commit 06b1064f7e2f7cac5d4bef2ee999796a2a1f0f2c
|
|
@ -125,7 +125,7 @@
|
||||||
"@babel/preset-env": "7.16.11",
|
"@babel/preset-env": "7.16.11",
|
||||||
"@swc/core": "1.3.71",
|
"@swc/core": "1.3.71",
|
||||||
"@swc/jest": "0.2.27",
|
"@swc/jest": "0.2.27",
|
||||||
"@types/archiver": "^6.0.2",
|
"@types/archiver": "6.0.2",
|
||||||
"@types/global-agent": "2.1.1",
|
"@types/global-agent": "2.1.1",
|
||||||
"@types/google-spreadsheet": "3.1.5",
|
"@types/google-spreadsheet": "3.1.5",
|
||||||
"@types/jest": "29.5.5",
|
"@types/jest": "29.5.5",
|
||||||
|
|
|
@ -36,7 +36,6 @@ import { getDatasourceAndQuery } from "../../../sdk/app/rows/utils"
|
||||||
import { processObjectSync } from "@budibase/string-templates"
|
import { processObjectSync } from "@budibase/string-templates"
|
||||||
import { cloneDeep } from "lodash/fp"
|
import { cloneDeep } from "lodash/fp"
|
||||||
import { db as dbCore } from "@budibase/backend-core"
|
import { db as dbCore } from "@budibase/backend-core"
|
||||||
import AliasTables from "./alias"
|
|
||||||
import sdk from "../../../sdk"
|
import sdk from "../../../sdk"
|
||||||
import env from "../../../environment"
|
import env from "../../../environment"
|
||||||
|
|
||||||
|
@ -120,6 +119,9 @@ async function removeManyToManyRelationships(
|
||||||
endpoint: getEndpoint(tableId, Operation.DELETE),
|
endpoint: getEndpoint(tableId, Operation.DELETE),
|
||||||
body: { [colName]: null },
|
body: { [colName]: null },
|
||||||
filters,
|
filters,
|
||||||
|
meta: {
|
||||||
|
table,
|
||||||
|
},
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
return []
|
return []
|
||||||
|
@ -134,6 +136,9 @@ async function removeOneToManyRelationships(rowId: string, table: Table) {
|
||||||
return getDatasourceAndQuery({
|
return getDatasourceAndQuery({
|
||||||
endpoint: getEndpoint(tableId, Operation.UPDATE),
|
endpoint: getEndpoint(tableId, Operation.UPDATE),
|
||||||
filters,
|
filters,
|
||||||
|
meta: {
|
||||||
|
table,
|
||||||
|
},
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
return []
|
return []
|
||||||
|
@ -249,6 +254,9 @@ export class ExternalRequest<T extends Operation> {
|
||||||
const response = await getDatasourceAndQuery({
|
const response = await getDatasourceAndQuery({
|
||||||
endpoint: getEndpoint(table._id!, Operation.READ),
|
endpoint: getEndpoint(table._id!, Operation.READ),
|
||||||
filters: buildFilters(rowId, {}, table),
|
filters: buildFilters(rowId, {}, table),
|
||||||
|
meta: {
|
||||||
|
table,
|
||||||
|
},
|
||||||
})
|
})
|
||||||
if (Array.isArray(response) && response.length > 0) {
|
if (Array.isArray(response) && response.length > 0) {
|
||||||
return response[0]
|
return response[0]
|
||||||
|
@ -396,6 +404,9 @@ export class ExternalRequest<T extends Operation> {
|
||||||
[fieldName]: row[lookupField],
|
[fieldName]: row[lookupField],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
meta: {
|
||||||
|
table,
|
||||||
|
},
|
||||||
})
|
})
|
||||||
// this is the response from knex if no rows found
|
// this is the response from knex if no rows found
|
||||||
const rows: Row[] =
|
const rows: Row[] =
|
||||||
|
@ -426,6 +437,7 @@ export class ExternalRequest<T extends Operation> {
|
||||||
// if we're creating (in a through table) need to wipe the existing ones first
|
// if we're creating (in a through table) need to wipe the existing ones first
|
||||||
const promises = []
|
const promises = []
|
||||||
const related = await this.lookupRelations(mainTableId, row)
|
const related = await this.lookupRelations(mainTableId, row)
|
||||||
|
const table = this.getTable(mainTableId)!
|
||||||
for (let relationship of relationships) {
|
for (let relationship of relationships) {
|
||||||
const { key, tableId, isUpdate, id, ...rest } = relationship
|
const { key, tableId, isUpdate, id, ...rest } = relationship
|
||||||
const body: { [key: string]: any } = processObjectSync(rest, row, {})
|
const body: { [key: string]: any } = processObjectSync(rest, row, {})
|
||||||
|
@ -471,6 +483,9 @@ export class ExternalRequest<T extends Operation> {
|
||||||
// if we're doing many relationships then we're writing, only one response
|
// if we're doing many relationships then we're writing, only one response
|
||||||
body,
|
body,
|
||||||
filters: buildFilters(id, {}, linkTable),
|
filters: buildFilters(id, {}, linkTable),
|
||||||
|
meta: {
|
||||||
|
table,
|
||||||
|
},
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
|
@ -618,7 +633,7 @@ export class ExternalRequest<T extends Operation> {
|
||||||
if (env.SQL_ALIASING_DISABLE) {
|
if (env.SQL_ALIASING_DISABLE) {
|
||||||
response = await getDatasourceAndQuery(json)
|
response = await getDatasourceAndQuery(json)
|
||||||
} else {
|
} else {
|
||||||
const aliasing = new AliasTables(Object.keys(this.tables))
|
const aliasing = new sdk.rows.AliasTables(Object.keys(this.tables))
|
||||||
response = await aliasing.queryWithAliasing(json)
|
response = await aliasing.queryWithAliasing(json)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -62,12 +62,12 @@ export function basicProcessing({
|
||||||
row,
|
row,
|
||||||
table,
|
table,
|
||||||
isLinked,
|
isLinked,
|
||||||
internal,
|
sqs,
|
||||||
}: {
|
}: {
|
||||||
row: Row
|
row: Row
|
||||||
table: Table
|
table: Table
|
||||||
isLinked: boolean
|
isLinked: boolean
|
||||||
internal?: boolean
|
sqs?: boolean
|
||||||
}): Row {
|
}): Row {
|
||||||
const thisRow: Row = {}
|
const thisRow: Row = {}
|
||||||
// filter the row down to what is actually the row (not joined)
|
// filter the row down to what is actually the row (not joined)
|
||||||
|
@ -84,12 +84,13 @@ export function basicProcessing({
|
||||||
thisRow[fieldName] = value
|
thisRow[fieldName] = value
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!internal) {
|
if (!sqs) {
|
||||||
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"
|
||||||
} else {
|
} else {
|
||||||
for (let internalColumn of CONSTANT_INTERNAL_ROW_COLS) {
|
const columns = Object.keys(table.schema)
|
||||||
|
for (let internalColumn of [...CONSTANT_INTERNAL_ROW_COLS, ...columns]) {
|
||||||
thisRow[internalColumn] = extractFieldValue({
|
thisRow[internalColumn] = extractFieldValue({
|
||||||
row,
|
row,
|
||||||
tableName: table._id!,
|
tableName: table._id!,
|
||||||
|
|
|
@ -51,11 +51,11 @@ export async function updateRelationshipColumns(
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
let linked = await basicProcessing({
|
let linked = basicProcessing({
|
||||||
row,
|
row,
|
||||||
table: linkedTable,
|
table: linkedTable,
|
||||||
isLinked: true,
|
isLinked: true,
|
||||||
internal: opts?.sqs,
|
sqs: opts?.sqs,
|
||||||
})
|
})
|
||||||
if (!linked._id) {
|
if (!linked._id) {
|
||||||
continue
|
continue
|
||||||
|
|
|
@ -132,6 +132,7 @@ export async function sqlOutputProcessing(
|
||||||
let rowId = row._id
|
let rowId = row._id
|
||||||
if (opts?.sqs) {
|
if (opts?.sqs) {
|
||||||
rowId = getInternalRowId(row, table)
|
rowId = getInternalRowId(row, table)
|
||||||
|
row._id = rowId
|
||||||
} else if (!rowId) {
|
} else if (!rowId) {
|
||||||
rowId = generateIdForRow(row, table)
|
rowId = generateIdForRow(row, table)
|
||||||
row._id = rowId
|
row._id = rowId
|
||||||
|
@ -153,7 +154,7 @@ export async function sqlOutputProcessing(
|
||||||
row,
|
row,
|
||||||
table,
|
table,
|
||||||
isLinked: false,
|
isLinked: false,
|
||||||
internal: opts?.sqs,
|
sqs: opts?.sqs,
|
||||||
}),
|
}),
|
||||||
table
|
table
|
||||||
)
|
)
|
||||||
|
@ -167,7 +168,8 @@ export async function sqlOutputProcessing(
|
||||||
tables,
|
tables,
|
||||||
row,
|
row,
|
||||||
finalRows,
|
finalRows,
|
||||||
relationships
|
relationships,
|
||||||
|
opts
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -127,13 +127,6 @@ export const uploadFile = async function (
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export const deleteObjects = async function (ctx: Ctx) {
|
|
||||||
ctx.body = await objectStore.deleteFiles(
|
|
||||||
ObjectStoreBuckets.APPS,
|
|
||||||
ctx.request.body.keys
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const requiresMigration = async (ctx: Ctx) => {
|
const requiresMigration = async (ctx: Ctx) => {
|
||||||
const appId = context.getAppId()
|
const appId = context.getAppId()
|
||||||
if (!appId) {
|
if (!appId) {
|
||||||
|
|
|
@ -22,6 +22,7 @@ export async function makeTableRequest(
|
||||||
operation,
|
operation,
|
||||||
},
|
},
|
||||||
meta: {
|
meta: {
|
||||||
|
table,
|
||||||
tables,
|
tables,
|
||||||
},
|
},
|
||||||
table,
|
table,
|
||||||
|
|
|
@ -32,11 +32,6 @@ router
|
||||||
.get("/builder/:file*", controller.serveBuilder)
|
.get("/builder/:file*", controller.serveBuilder)
|
||||||
.get("/api/assets/client", controller.serveClientLibrary)
|
.get("/api/assets/client", controller.serveClientLibrary)
|
||||||
.post("/api/attachments/process", authorized(BUILDER), controller.uploadFile)
|
.post("/api/attachments/process", authorized(BUILDER), controller.uploadFile)
|
||||||
.post(
|
|
||||||
"/api/attachments/delete",
|
|
||||||
authorized(BUILDER),
|
|
||||||
controller.deleteObjects
|
|
||||||
)
|
|
||||||
.post("/api/beta/:feature", controller.toggleBetaUiFeature)
|
.post("/api/beta/:feature", controller.toggleBetaUiFeature)
|
||||||
.post(
|
.post(
|
||||||
"/api/attachments/:tableId/upload",
|
"/api/attachments/:tableId/upload",
|
||||||
|
@ -44,12 +39,6 @@ router
|
||||||
authorized(PermissionType.TABLE, PermissionLevel.WRITE),
|
authorized(PermissionType.TABLE, PermissionLevel.WRITE),
|
||||||
controller.uploadFile
|
controller.uploadFile
|
||||||
)
|
)
|
||||||
.post(
|
|
||||||
"/api/attachments/:tableId/delete",
|
|
||||||
paramResource("tableId"),
|
|
||||||
authorized(PermissionType.TABLE, PermissionLevel.WRITE),
|
|
||||||
controller.deleteObjects
|
|
||||||
)
|
|
||||||
.get("/app/preview", authorized(BUILDER), controller.serveBuilderPreview)
|
.get("/app/preview", authorized(BUILDER), controller.serveBuilderPreview)
|
||||||
.get("/app/:appUrl/:path*", controller.serveApp)
|
.get("/app/:appUrl/:path*", controller.serveApp)
|
||||||
.get("/:appId/:path*", controller.serveApp)
|
.get("/:appId/:path*", controller.serveApp)
|
||||||
|
|
|
@ -8,7 +8,7 @@ import {
|
||||||
PermissionLevel,
|
PermissionLevel,
|
||||||
QuotaUsageType,
|
QuotaUsageType,
|
||||||
SaveTableRequest,
|
SaveTableRequest,
|
||||||
SearchQueryOperators,
|
SearchFilterOperator,
|
||||||
SortOrder,
|
SortOrder,
|
||||||
SortType,
|
SortType,
|
||||||
StaticQuotaName,
|
StaticQuotaName,
|
||||||
|
@ -132,7 +132,7 @@ describe.each([
|
||||||
primaryDisplay: generator.word(),
|
primaryDisplay: generator.word(),
|
||||||
query: [
|
query: [
|
||||||
{
|
{
|
||||||
operator: SearchQueryOperators.EQUAL,
|
operator: SearchFilterOperator.EQUAL,
|
||||||
field: "field",
|
field: "field",
|
||||||
value: "value",
|
value: "value",
|
||||||
},
|
},
|
||||||
|
@ -236,7 +236,7 @@ describe.each([
|
||||||
...view,
|
...view,
|
||||||
query: [
|
query: [
|
||||||
{
|
{
|
||||||
operator: SearchQueryOperators.EQUAL,
|
operator: SearchFilterOperator.EQUAL,
|
||||||
field: "newField",
|
field: "newField",
|
||||||
value: "thatValue",
|
value: "thatValue",
|
||||||
},
|
},
|
||||||
|
@ -263,7 +263,7 @@ describe.each([
|
||||||
primaryDisplay: generator.word(),
|
primaryDisplay: generator.word(),
|
||||||
query: [
|
query: [
|
||||||
{
|
{
|
||||||
operator: SearchQueryOperators.EQUAL,
|
operator: SearchFilterOperator.EQUAL,
|
||||||
field: generator.word(),
|
field: generator.word(),
|
||||||
value: generator.word(),
|
value: generator.word(),
|
||||||
},
|
},
|
||||||
|
@ -341,7 +341,7 @@ describe.each([
|
||||||
tableId: generator.guid(),
|
tableId: generator.guid(),
|
||||||
query: [
|
query: [
|
||||||
{
|
{
|
||||||
operator: SearchQueryOperators.EQUAL,
|
operator: SearchFilterOperator.EQUAL,
|
||||||
field: "newField",
|
field: "newField",
|
||||||
value: "thatValue",
|
value: "thatValue",
|
||||||
},
|
},
|
||||||
|
@ -671,7 +671,7 @@ describe.each([
|
||||||
name: generator.guid(),
|
name: generator.guid(),
|
||||||
query: [
|
query: [
|
||||||
{
|
{
|
||||||
operator: SearchQueryOperators.EQUAL,
|
operator: SearchFilterOperator.EQUAL,
|
||||||
field: "two",
|
field: "two",
|
||||||
value: "bar2",
|
value: "bar2",
|
||||||
},
|
},
|
||||||
|
|
|
@ -62,7 +62,6 @@ export const definition: AutomationStepSchema = {
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function run({ inputs }: AutomationStepInput) {
|
export async function run({ inputs }: AutomationStepInput) {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
||||||
const { automationId, ...fieldParams } = inputs.automation
|
const { automationId, ...fieldParams } = inputs.automation
|
||||||
|
|
||||||
if (await features.isTriggerAutomationRunEnabled()) {
|
if (await features.isTriggerAutomationRunEnabled()) {
|
||||||
|
|
|
@ -40,6 +40,7 @@ export const USER_METDATA_PREFIX = `${DocumentType.ROW}${SEPARATOR}${dbCore.Inte
|
||||||
export const LINK_USER_METADATA_PREFIX = `${DocumentType.LINK}${SEPARATOR}${dbCore.InternalTable.USER_METADATA}${SEPARATOR}`
|
export const LINK_USER_METADATA_PREFIX = `${DocumentType.LINK}${SEPARATOR}${dbCore.InternalTable.USER_METADATA}${SEPARATOR}`
|
||||||
export const TABLE_ROW_PREFIX = `${DocumentType.ROW}${SEPARATOR}${DocumentType.TABLE}`
|
export const TABLE_ROW_PREFIX = `${DocumentType.ROW}${SEPARATOR}${DocumentType.TABLE}`
|
||||||
export const AUTOMATION_LOG_PREFIX = `${DocumentType.AUTOMATION_LOG}${SEPARATOR}`
|
export const AUTOMATION_LOG_PREFIX = `${DocumentType.AUTOMATION_LOG}${SEPARATOR}`
|
||||||
|
export const SQS_DATASOURCE_INTERNAL = "internal"
|
||||||
export const ViewName = dbCore.ViewName
|
export const ViewName = dbCore.ViewName
|
||||||
export const InternalTables = dbCore.InternalTable
|
export const InternalTables = dbCore.InternalTable
|
||||||
export const UNICODE_MAX = dbCore.UNICODE_MAX
|
export const UNICODE_MAX = dbCore.UNICODE_MAX
|
||||||
|
|
|
@ -28,6 +28,7 @@ const DEFAULTS = {
|
||||||
PLUGINS_DIR: "/plugins",
|
PLUGINS_DIR: "/plugins",
|
||||||
FORKED_PROCESS_NAME: "main",
|
FORKED_PROCESS_NAME: "main",
|
||||||
JS_RUNNER_MEMORY_LIMIT: 64,
|
JS_RUNNER_MEMORY_LIMIT: 64,
|
||||||
|
COUCH_DB_SQL_URL: "http://localhost:4006",
|
||||||
}
|
}
|
||||||
|
|
||||||
const QUERY_THREAD_TIMEOUT =
|
const QUERY_THREAD_TIMEOUT =
|
||||||
|
@ -39,6 +40,7 @@ const environment = {
|
||||||
// important - prefer app port to generic port
|
// important - prefer app port to generic port
|
||||||
PORT: process.env.APP_PORT || process.env.PORT,
|
PORT: process.env.APP_PORT || process.env.PORT,
|
||||||
COUCH_DB_URL: process.env.COUCH_DB_URL,
|
COUCH_DB_URL: process.env.COUCH_DB_URL,
|
||||||
|
COUCH_DB_SQL_URL: process.env.COUCH_DB_SQL_URL || DEFAULTS.COUCH_DB_SQL_URL,
|
||||||
MINIO_URL: process.env.MINIO_URL,
|
MINIO_URL: process.env.MINIO_URL,
|
||||||
WORKER_URL: process.env.WORKER_URL,
|
WORKER_URL: process.env.WORKER_URL,
|
||||||
AWS_REGION: process.env.AWS_REGION,
|
AWS_REGION: process.env.AWS_REGION,
|
||||||
|
|
|
@ -1,11 +1,8 @@
|
||||||
import { features } from "@budibase/backend-core"
|
import { features } from "@budibase/backend-core"
|
||||||
import env from "./environment"
|
import env from "./environment"
|
||||||
|
|
||||||
// eslint-disable-next-line no-unused-vars
|
|
||||||
enum AppFeature {
|
enum AppFeature {
|
||||||
// eslint-disable-next-line no-unused-vars
|
|
||||||
API = "api",
|
API = "api",
|
||||||
// eslint-disable-next-line no-unused-vars
|
|
||||||
AUTOMATIONS = "automations",
|
AUTOMATIONS = "automations",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -22,6 +22,8 @@ import {
|
||||||
SortDirection,
|
SortDirection,
|
||||||
SqlQueryBinding,
|
SqlQueryBinding,
|
||||||
Table,
|
Table,
|
||||||
|
TableSourceType,
|
||||||
|
INTERNAL_TABLE_SOURCE_ID,
|
||||||
} from "@budibase/types"
|
} from "@budibase/types"
|
||||||
import environment from "../../environment"
|
import environment from "../../environment"
|
||||||
|
|
||||||
|
@ -135,6 +137,18 @@ function generateSelectStatement(
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getTableName(table?: Table): string | undefined {
|
||||||
|
// SQS uses the table ID rather than the table name
|
||||||
|
if (
|
||||||
|
table?.sourceType === TableSourceType.INTERNAL ||
|
||||||
|
table?.sourceId === INTERNAL_TABLE_SOURCE_ID
|
||||||
|
) {
|
||||||
|
return table?._id
|
||||||
|
} else {
|
||||||
|
return table?.name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class InternalBuilder {
|
class InternalBuilder {
|
||||||
private readonly client: string
|
private readonly client: string
|
||||||
|
|
||||||
|
@ -149,7 +163,7 @@ class InternalBuilder {
|
||||||
tableName: string,
|
tableName: string,
|
||||||
opts: { aliases?: Record<string, string>; relationship?: boolean }
|
opts: { aliases?: Record<string, string>; relationship?: boolean }
|
||||||
): Knex.QueryBuilder {
|
): Knex.QueryBuilder {
|
||||||
function getTableName(name: string) {
|
function getTableAlias(name: string) {
|
||||||
const alias = opts.aliases?.[name]
|
const alias = opts.aliases?.[name]
|
||||||
return alias || name
|
return alias || name
|
||||||
}
|
}
|
||||||
|
@ -161,11 +175,11 @@ class InternalBuilder {
|
||||||
const updatedKey = dbCore.removeKeyNumbering(key)
|
const updatedKey = dbCore.removeKeyNumbering(key)
|
||||||
const isRelationshipField = updatedKey.includes(".")
|
const isRelationshipField = updatedKey.includes(".")
|
||||||
if (!opts.relationship && !isRelationshipField) {
|
if (!opts.relationship && !isRelationshipField) {
|
||||||
fn(`${getTableName(tableName)}.${updatedKey}`, value)
|
fn(`${getTableAlias(tableName)}.${updatedKey}`, value)
|
||||||
}
|
}
|
||||||
if (opts.relationship && isRelationshipField) {
|
if (opts.relationship && isRelationshipField) {
|
||||||
const [filterTableName, property] = updatedKey.split(".")
|
const [filterTableName, property] = updatedKey.split(".")
|
||||||
fn(`${getTableName(filterTableName)}.${property}`, value)
|
fn(`${getTableAlias(filterTableName)}.${property}`, value)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -346,9 +360,10 @@ class InternalBuilder {
|
||||||
addSorting(query: Knex.QueryBuilder, json: QueryJson): Knex.QueryBuilder {
|
addSorting(query: Knex.QueryBuilder, json: QueryJson): Knex.QueryBuilder {
|
||||||
let { sort, paginate } = json
|
let { sort, paginate } = json
|
||||||
const table = json.meta?.table
|
const table = json.meta?.table
|
||||||
|
const tableName = getTableName(table)
|
||||||
const aliases = json.tableAliases
|
const aliases = json.tableAliases
|
||||||
const aliased =
|
const aliased =
|
||||||
table?.name && aliases?.[table.name] ? aliases[table.name] : table?.name
|
tableName && aliases?.[tableName] ? aliases[tableName] : table?.name
|
||||||
if (sort && Object.keys(sort || {}).length > 0) {
|
if (sort && Object.keys(sort || {}).length > 0) {
|
||||||
for (let [key, value] of Object.entries(sort)) {
|
for (let [key, value] of Object.entries(sort)) {
|
||||||
const direction =
|
const direction =
|
||||||
|
@ -729,12 +744,13 @@ class SqlQueryBuilder extends SqlTableQueryBuilder {
|
||||||
results: Record<string, any>[],
|
results: Record<string, any>[],
|
||||||
aliases?: Record<string, string>
|
aliases?: Record<string, string>
|
||||||
): Record<string, any>[] {
|
): Record<string, any>[] {
|
||||||
|
const tableName = getTableName(table)
|
||||||
for (const [name, field] of Object.entries(table.schema)) {
|
for (const [name, field] of Object.entries(table.schema)) {
|
||||||
if (!this._isJsonColumn(field)) {
|
if (!this._isJsonColumn(field)) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
const tableName = aliases?.[table.name] || table.name
|
const aliasedTableName = (tableName && aliases?.[tableName]) || tableName
|
||||||
const fullName = `${tableName}.${name}`
|
const fullName = `${aliasedTableName}.${name}`
|
||||||
for (let row of results) {
|
for (let row of results) {
|
||||||
if (typeof row[fullName] === "string") {
|
if (typeof row[fullName] === "string") {
|
||||||
row[fullName] = JSON.parse(row[fullName])
|
row[fullName] = JSON.parse(row[fullName])
|
||||||
|
|
|
@ -1,19 +1,20 @@
|
||||||
import { Knex, knex } from "knex"
|
import { Knex, knex } from "knex"
|
||||||
import {
|
import {
|
||||||
RelationshipType,
|
|
||||||
FieldSubtype,
|
FieldSubtype,
|
||||||
|
FieldType,
|
||||||
NumberFieldMetadata,
|
NumberFieldMetadata,
|
||||||
Operation,
|
Operation,
|
||||||
QueryJson,
|
QueryJson,
|
||||||
|
RelationshipType,
|
||||||
RenameColumn,
|
RenameColumn,
|
||||||
Table,
|
|
||||||
FieldType,
|
|
||||||
SqlQuery,
|
SqlQuery,
|
||||||
|
Table,
|
||||||
|
TableSourceType,
|
||||||
} from "@budibase/types"
|
} from "@budibase/types"
|
||||||
import { breakExternalTableId, getNativeSql, SqlClient } from "../utils"
|
import { breakExternalTableId, getNativeSql, SqlClient } from "../utils"
|
||||||
|
import { utils } from "@budibase/shared-core"
|
||||||
import SchemaBuilder = Knex.SchemaBuilder
|
import SchemaBuilder = Knex.SchemaBuilder
|
||||||
import CreateTableBuilder = Knex.CreateTableBuilder
|
import CreateTableBuilder = Knex.CreateTableBuilder
|
||||||
import { utils } from "@budibase/shared-core"
|
|
||||||
|
|
||||||
function isIgnoredType(type: FieldType) {
|
function isIgnoredType(type: FieldType) {
|
||||||
const ignored = [FieldType.LINK, FieldType.FORMULA]
|
const ignored = [FieldType.LINK, FieldType.FORMULA]
|
||||||
|
@ -105,13 +106,13 @@ function generateSchema(
|
||||||
column.relationshipType !== RelationshipType.MANY_TO_MANY
|
column.relationshipType !== RelationshipType.MANY_TO_MANY
|
||||||
) {
|
) {
|
||||||
if (!column.foreignKey || !column.tableId) {
|
if (!column.foreignKey || !column.tableId) {
|
||||||
throw "Invalid relationship schema"
|
throw new Error("Invalid relationship schema")
|
||||||
}
|
}
|
||||||
const { tableName } = breakExternalTableId(column.tableId)
|
const { tableName } = breakExternalTableId(column.tableId)
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
const relatedTable = tables[tableName]
|
const relatedTable = tables[tableName]
|
||||||
if (!relatedTable) {
|
if (!relatedTable) {
|
||||||
throw "Referenced table doesn't exist"
|
throw new Error("Referenced table doesn't exist")
|
||||||
}
|
}
|
||||||
const relatedPrimary = relatedTable.primary[0]
|
const relatedPrimary = relatedTable.primary[0]
|
||||||
const externalType = relatedTable.schema[relatedPrimary].externalType
|
const externalType = relatedTable.schema[relatedPrimary].externalType
|
||||||
|
@ -209,15 +210,19 @@ class SqlTableQueryBuilder {
|
||||||
|
|
||||||
let query: Knex.SchemaBuilder
|
let query: Knex.SchemaBuilder
|
||||||
if (!json.table || !json.meta || !json.meta.tables) {
|
if (!json.table || !json.meta || !json.meta.tables) {
|
||||||
throw "Cannot execute without table being specified"
|
throw new Error("Cannot execute without table being specified")
|
||||||
}
|
}
|
||||||
|
if (json.table.sourceType === TableSourceType.INTERNAL) {
|
||||||
|
throw new Error("Cannot perform table actions for SQS.")
|
||||||
|
}
|
||||||
|
|
||||||
switch (this._operation(json)) {
|
switch (this._operation(json)) {
|
||||||
case Operation.CREATE_TABLE:
|
case Operation.CREATE_TABLE:
|
||||||
query = buildCreateTable(client, json.table, json.meta.tables)
|
query = buildCreateTable(client, json.table, json.meta.tables)
|
||||||
break
|
break
|
||||||
case Operation.UPDATE_TABLE:
|
case Operation.UPDATE_TABLE:
|
||||||
if (!json.meta || !json.meta.table) {
|
if (!json.meta || !json.meta.table) {
|
||||||
throw "Must specify old table for update"
|
throw new Error("Must specify old table for update")
|
||||||
}
|
}
|
||||||
// renameColumn does not work for MySQL, so return a raw query
|
// renameColumn does not work for MySQL, so return a raw query
|
||||||
if (this.sqlClient === SqlClient.MY_SQL && json.meta.renamed) {
|
if (this.sqlClient === SqlClient.MY_SQL && json.meta.renamed) {
|
||||||
|
@ -264,7 +269,7 @@ class SqlTableQueryBuilder {
|
||||||
query = buildDeleteTable(client, json.table)
|
query = buildDeleteTable(client, json.table)
|
||||||
break
|
break
|
||||||
default:
|
default:
|
||||||
throw "Table operation is of unknown type"
|
throw new Error("Table operation is of unknown type")
|
||||||
}
|
}
|
||||||
return getNativeSql(query)
|
return getNativeSql(query)
|
||||||
}
|
}
|
||||||
|
|
|
@ -168,8 +168,7 @@ class GoogleSheetsIntegration implements DatasourcePlus {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
getStringConcat(_parts: string[]) {
|
||||||
getStringConcat(parts: string[]) {
|
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -9,6 +9,14 @@ import {
|
||||||
} from "@budibase/types"
|
} from "@budibase/types"
|
||||||
|
|
||||||
const TABLE_NAME = "test"
|
const TABLE_NAME = "test"
|
||||||
|
const TABLE: Table = {
|
||||||
|
type: "table",
|
||||||
|
sourceType: TableSourceType.EXTERNAL,
|
||||||
|
sourceId: "SOURCE_ID",
|
||||||
|
schema: {},
|
||||||
|
name: TABLE_NAME,
|
||||||
|
primary: ["id"],
|
||||||
|
}
|
||||||
|
|
||||||
function endpoint(table: any, operation: any) {
|
function endpoint(table: any, operation: any) {
|
||||||
return {
|
return {
|
||||||
|
@ -25,6 +33,10 @@ function generateReadJson({
|
||||||
sort,
|
sort,
|
||||||
paginate,
|
paginate,
|
||||||
}: any = {}): QueryJson {
|
}: any = {}): QueryJson {
|
||||||
|
const tableObj = { ...TABLE }
|
||||||
|
if (table) {
|
||||||
|
tableObj.name = table
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
endpoint: endpoint(table || TABLE_NAME, "READ"),
|
endpoint: endpoint(table || TABLE_NAME, "READ"),
|
||||||
resource: {
|
resource: {
|
||||||
|
@ -34,14 +46,7 @@ function generateReadJson({
|
||||||
sort: sort || {},
|
sort: sort || {},
|
||||||
paginate: paginate || {},
|
paginate: paginate || {},
|
||||||
meta: {
|
meta: {
|
||||||
table: {
|
table: tableObj,
|
||||||
type: "table",
|
|
||||||
sourceType: TableSourceType.EXTERNAL,
|
|
||||||
sourceId: "SOURCE_ID",
|
|
||||||
schema: {},
|
|
||||||
name: table || TABLE_NAME,
|
|
||||||
primary: ["id"],
|
|
||||||
} as any,
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -49,6 +54,9 @@ function generateReadJson({
|
||||||
function generateCreateJson(table = TABLE_NAME, body = {}): QueryJson {
|
function generateCreateJson(table = TABLE_NAME, body = {}): QueryJson {
|
||||||
return {
|
return {
|
||||||
endpoint: endpoint(table, "CREATE"),
|
endpoint: endpoint(table, "CREATE"),
|
||||||
|
meta: {
|
||||||
|
table: TABLE,
|
||||||
|
},
|
||||||
body,
|
body,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -58,7 +66,15 @@ function generateUpdateJson({
|
||||||
body = {},
|
body = {},
|
||||||
filters = {},
|
filters = {},
|
||||||
meta = {},
|
meta = {},
|
||||||
|
}: {
|
||||||
|
table: string
|
||||||
|
body?: any
|
||||||
|
filters?: any
|
||||||
|
meta?: any
|
||||||
}): QueryJson {
|
}): QueryJson {
|
||||||
|
if (!meta.table) {
|
||||||
|
meta.table = table
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
endpoint: endpoint(table, "UPDATE"),
|
endpoint: endpoint(table, "UPDATE"),
|
||||||
filters,
|
filters,
|
||||||
|
@ -70,6 +86,9 @@ function generateUpdateJson({
|
||||||
function generateDeleteJson(table = TABLE_NAME, filters = {}): QueryJson {
|
function generateDeleteJson(table = TABLE_NAME, filters = {}): QueryJson {
|
||||||
return {
|
return {
|
||||||
endpoint: endpoint(table, "DELETE"),
|
endpoint: endpoint(table, "DELETE"),
|
||||||
|
meta: {
|
||||||
|
table: TABLE,
|
||||||
|
},
|
||||||
filters,
|
filters,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -102,6 +121,9 @@ function generateRelationshipJson(config: { schema?: string } = {}): QueryJson {
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
extra: { idFilter: {} },
|
extra: { idFilter: {} },
|
||||||
|
meta: {
|
||||||
|
table: TABLE,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -4,12 +4,26 @@ import {
|
||||||
QueryJson,
|
QueryJson,
|
||||||
SourceName,
|
SourceName,
|
||||||
SqlQuery,
|
SqlQuery,
|
||||||
|
Table,
|
||||||
|
TableSourceType,
|
||||||
} from "@budibase/types"
|
} from "@budibase/types"
|
||||||
import { join } from "path"
|
import { join } from "path"
|
||||||
import Sql from "../base/sql"
|
import Sql from "../base/sql"
|
||||||
import { SqlClient } from "../utils"
|
import { SqlClient } from "../utils"
|
||||||
import AliasTables from "../../api/controllers/row/alias"
|
|
||||||
import { generator } from "@budibase/backend-core/tests"
|
import { generator } from "@budibase/backend-core/tests"
|
||||||
|
import sdk from "../../sdk"
|
||||||
|
|
||||||
|
// this doesn't exist strictly
|
||||||
|
const TABLE: Table = {
|
||||||
|
type: "table",
|
||||||
|
sourceType: TableSourceType.EXTERNAL,
|
||||||
|
sourceId: "SOURCE_ID",
|
||||||
|
schema: {},
|
||||||
|
name: "tableName",
|
||||||
|
primary: ["id"],
|
||||||
|
}
|
||||||
|
|
||||||
|
const AliasTables = sdk.rows.AliasTables
|
||||||
|
|
||||||
function multiline(sql: string) {
|
function multiline(sql: string) {
|
||||||
return sql.replace(/\n/g, "").replace(/ +/g, " ")
|
return sql.replace(/\n/g, "").replace(/ +/g, " ")
|
||||||
|
@ -220,6 +234,9 @@ describe("Captures of real examples", () => {
|
||||||
resource: {
|
resource: {
|
||||||
fields,
|
fields,
|
||||||
},
|
},
|
||||||
|
meta: {
|
||||||
|
table: TABLE,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -35,7 +35,6 @@ export function init() {
|
||||||
// Because we can't pass functions into an Isolate, we remove them from
|
// Because we can't pass functions into an Isolate, we remove them from
|
||||||
// the passed context and rely on the withHelpers() method to add them
|
// the passed context and rely on the withHelpers() method to add them
|
||||||
// back in.
|
// back in.
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
||||||
const { helpers, snippets, ...rest } = ctx
|
const { helpers, snippets, ...rest } = ctx
|
||||||
return vm.withContext(rest, () => vm.execute(js))
|
return vm.withContext(rest, () => vm.execute(js))
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
|
|
|
@ -3,6 +3,7 @@ import * as rows from "./rows"
|
||||||
import * as search from "./search"
|
import * as search from "./search"
|
||||||
import * as utils from "./utils"
|
import * as utils from "./utils"
|
||||||
import * as external from "./external"
|
import * as external from "./external"
|
||||||
|
import AliasTables from "./sqlAlias"
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
...attachments,
|
...attachments,
|
||||||
|
@ -10,4 +11,5 @@ export default {
|
||||||
...search,
|
...search,
|
||||||
utils,
|
utils,
|
||||||
external,
|
external,
|
||||||
|
AliasTables,
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,6 +13,8 @@ import * as sqs from "./search/sqs"
|
||||||
import env from "../../../environment"
|
import env from "../../../environment"
|
||||||
import { ExportRowsParams, ExportRowsResult } from "./search/types"
|
import { ExportRowsParams, ExportRowsResult } from "./search/types"
|
||||||
import { dataFilters } from "@budibase/shared-core"
|
import { dataFilters } from "@budibase/shared-core"
|
||||||
|
import sdk from "../../index"
|
||||||
|
import { searchInputMapping } from "./search/utils"
|
||||||
|
|
||||||
export { isValidFilter } from "../../../integrations/utils"
|
export { isValidFilter } from "../../../integrations/utils"
|
||||||
|
|
||||||
|
@ -76,12 +78,15 @@ export async function search(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const table = await sdk.tables.getTable(options.tableId)
|
||||||
|
options = searchInputMapping(table, options)
|
||||||
|
|
||||||
if (isExternalTable) {
|
if (isExternalTable) {
|
||||||
return external.search(options)
|
return external.search(options, table)
|
||||||
} else if (env.SQS_SEARCH_ENABLE) {
|
} else if (env.SQS_SEARCH_ENABLE) {
|
||||||
return sqs.search(options)
|
return sqs.search(options, table)
|
||||||
} else {
|
} else {
|
||||||
return internal.search(options)
|
return internal.search(options, table)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -8,6 +8,7 @@ import {
|
||||||
SearchFilters,
|
SearchFilters,
|
||||||
RowSearchParams,
|
RowSearchParams,
|
||||||
SearchResponse,
|
SearchResponse,
|
||||||
|
Table,
|
||||||
} from "@budibase/types"
|
} from "@budibase/types"
|
||||||
import * as exporters from "../../../../api/controllers/view/exporters"
|
import * as exporters from "../../../../api/controllers/view/exporters"
|
||||||
import { handleRequest } from "../../../../api/controllers/row/external"
|
import { handleRequest } from "../../../../api/controllers/row/external"
|
||||||
|
@ -18,13 +19,13 @@ import {
|
||||||
import { utils } from "@budibase/shared-core"
|
import { utils } from "@budibase/shared-core"
|
||||||
import { ExportRowsParams, ExportRowsResult } from "./types"
|
import { ExportRowsParams, ExportRowsResult } from "./types"
|
||||||
import { HTTPError, db } from "@budibase/backend-core"
|
import { HTTPError, db } from "@budibase/backend-core"
|
||||||
import { searchInputMapping } from "./utils"
|
|
||||||
import pick from "lodash/pick"
|
import pick from "lodash/pick"
|
||||||
import { outputProcessing } from "../../../../utilities/rowProcessor"
|
import { outputProcessing } from "../../../../utilities/rowProcessor"
|
||||||
import sdk from "../../../"
|
import sdk from "../../../"
|
||||||
|
|
||||||
export async function search(
|
export async function search(
|
||||||
options: RowSearchParams
|
options: RowSearchParams,
|
||||||
|
table: Table
|
||||||
): Promise<SearchResponse<Row>> {
|
): Promise<SearchResponse<Row>> {
|
||||||
const { tableId } = options
|
const { tableId } = options
|
||||||
const { paginate, query, ...params } = options
|
const { paginate, query, ...params } = options
|
||||||
|
@ -68,8 +69,6 @@ export async function search(
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const table = await sdk.tables.getTable(tableId)
|
|
||||||
options = searchInputMapping(table, options)
|
|
||||||
let rows = await handleRequest(Operation.READ, tableId, {
|
let rows = await handleRequest(Operation.READ, tableId, {
|
||||||
filters: query,
|
filters: query,
|
||||||
sort,
|
sort,
|
||||||
|
@ -150,11 +149,15 @@ export async function exportRows(
|
||||||
}
|
}
|
||||||
|
|
||||||
const datasource = await sdk.datasources.get(datasourceId!)
|
const datasource = await sdk.datasources.get(datasourceId!)
|
||||||
|
const table = await sdk.tables.getTable(tableId)
|
||||||
if (!datasource || !datasource.entities) {
|
if (!datasource || !datasource.entities) {
|
||||||
throw new HTTPError("Datasource has not been configured for plus API.", 400)
|
throw new HTTPError("Datasource has not been configured for plus API.", 400)
|
||||||
}
|
}
|
||||||
|
|
||||||
let result = await search({ tableId, query: requestQuery, sort, sortOrder })
|
let result = await search(
|
||||||
|
{ tableId, query: requestQuery, sort, sortOrder },
|
||||||
|
table
|
||||||
|
)
|
||||||
let rows: Row[] = []
|
let rows: Row[] = []
|
||||||
let headers
|
let headers
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { context, db, HTTPError } from "@budibase/backend-core"
|
import { context, db, HTTPError } from "@budibase/backend-core"
|
||||||
import env from "../../../../environment"
|
import env from "../../../../environment"
|
||||||
import { fullSearch, paginatedSearch, searchInputMapping } from "./utils"
|
import { fullSearch, paginatedSearch } from "./utils"
|
||||||
import { getRowParams, InternalTables } from "../../../../db/utils"
|
import { getRowParams, InternalTables } from "../../../../db/utils"
|
||||||
import {
|
import {
|
||||||
Database,
|
Database,
|
||||||
|
@ -33,7 +33,8 @@ import pick from "lodash/pick"
|
||||||
import { breakRowIdField } from "../../../../integrations/utils"
|
import { breakRowIdField } from "../../../../integrations/utils"
|
||||||
|
|
||||||
export async function search(
|
export async function search(
|
||||||
options: RowSearchParams
|
options: RowSearchParams,
|
||||||
|
table: Table
|
||||||
): Promise<SearchResponse<Row>> {
|
): Promise<SearchResponse<Row>> {
|
||||||
const { tableId } = options
|
const { tableId } = options
|
||||||
|
|
||||||
|
@ -51,8 +52,6 @@ export async function search(
|
||||||
query: {},
|
query: {},
|
||||||
}
|
}
|
||||||
|
|
||||||
let table = await sdk.tables.getTable(tableId)
|
|
||||||
options = searchInputMapping(table, options)
|
|
||||||
if (params.sort && !params.sortType) {
|
if (params.sort && !params.sortType) {
|
||||||
const schema = table.schema
|
const schema = table.schema
|
||||||
const sortField = schema[params.sort]
|
const sortField = schema[params.sort]
|
||||||
|
@ -122,12 +121,15 @@ export async function exportRows(
|
||||||
|
|
||||||
result = await outputProcessing<Row[]>(table, response)
|
result = await outputProcessing<Row[]>(table, response)
|
||||||
} else if (query) {
|
} else if (query) {
|
||||||
let searchResponse = await search({
|
let searchResponse = await search(
|
||||||
tableId,
|
{
|
||||||
query,
|
tableId,
|
||||||
sort,
|
query,
|
||||||
sortOrder,
|
sort,
|
||||||
})
|
sortOrder,
|
||||||
|
},
|
||||||
|
table
|
||||||
|
)
|
||||||
result = searchResponse.rows
|
result = searchResponse.rows
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -20,7 +20,12 @@ import {
|
||||||
} from "../../../../api/controllers/row/utils"
|
} from "../../../../api/controllers/row/utils"
|
||||||
import sdk from "../../../index"
|
import sdk from "../../../index"
|
||||||
import { context } from "@budibase/backend-core"
|
import { context } from "@budibase/backend-core"
|
||||||
import { CONSTANT_INTERNAL_ROW_COLS } from "../../../../db/utils"
|
import {
|
||||||
|
CONSTANT_INTERNAL_ROW_COLS,
|
||||||
|
SQS_DATASOURCE_INTERNAL,
|
||||||
|
} from "../../../../db/utils"
|
||||||
|
import AliasTables from "../sqlAlias"
|
||||||
|
import { outputProcessing } from "../../../../utilities/rowProcessor"
|
||||||
|
|
||||||
function buildInternalFieldList(
|
function buildInternalFieldList(
|
||||||
table: Table,
|
table: Table,
|
||||||
|
@ -31,19 +36,19 @@ function buildInternalFieldList(
|
||||||
fieldList = fieldList.concat(
|
fieldList = fieldList.concat(
|
||||||
CONSTANT_INTERNAL_ROW_COLS.map(col => `${table._id}.${col}`)
|
CONSTANT_INTERNAL_ROW_COLS.map(col => `${table._id}.${col}`)
|
||||||
)
|
)
|
||||||
if (opts.relationships) {
|
for (let col of Object.values(table.schema)) {
|
||||||
for (let col of Object.values(table.schema)) {
|
const isRelationship = col.type === FieldType.LINK
|
||||||
if (col.type === FieldType.LINK) {
|
if (!opts.relationships && isRelationship) {
|
||||||
const linkCol = col as RelationshipFieldMetadata
|
continue
|
||||||
const relatedTable = tables.find(
|
}
|
||||||
table => table._id === linkCol.tableId
|
if (isRelationship) {
|
||||||
)!
|
const linkCol = col as RelationshipFieldMetadata
|
||||||
fieldList = fieldList.concat(
|
const relatedTable = tables.find(table => table._id === linkCol.tableId)!
|
||||||
buildInternalFieldList(relatedTable, tables, { relationships: false })
|
fieldList = fieldList.concat(
|
||||||
)
|
buildInternalFieldList(relatedTable, tables, { relationships: false })
|
||||||
} else {
|
)
|
||||||
fieldList.push(`${table._id}.${col.name}`)
|
} else {
|
||||||
}
|
fieldList.push(`${table._id}.${col.name}`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return fieldList
|
return fieldList
|
||||||
|
@ -94,14 +99,14 @@ function buildTableMap(tables: Table[]) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function search(
|
export async function search(
|
||||||
options: RowSearchParams
|
options: RowSearchParams,
|
||||||
|
table: Table
|
||||||
): Promise<SearchResponse<Row>> {
|
): Promise<SearchResponse<Row>> {
|
||||||
const { tableId, paginate, query, ...params } = options
|
const { paginate, query, ...params } = options
|
||||||
|
|
||||||
const builder = new SqlQueryBuilder(SqlClient.SQL_LITE)
|
const builder = new SqlQueryBuilder(SqlClient.SQL_LITE)
|
||||||
const allTables = await sdk.tables.getAllInternalTables()
|
const allTables = await sdk.tables.getAllInternalTables()
|
||||||
const allTablesMap = buildTableMap(allTables)
|
const allTablesMap = buildTableMap(allTables)
|
||||||
const table = allTables.find(table => table._id === tableId)
|
|
||||||
if (!table) {
|
if (!table) {
|
||||||
throw new Error("Unable to find table")
|
throw new Error("Unable to find table")
|
||||||
}
|
}
|
||||||
|
@ -111,7 +116,7 @@ export async function search(
|
||||||
const request: QueryJson = {
|
const request: QueryJson = {
|
||||||
endpoint: {
|
endpoint: {
|
||||||
// not important, we query ourselves
|
// not important, we query ourselves
|
||||||
datasourceId: "internal",
|
datasourceId: SQS_DATASOURCE_INTERNAL,
|
||||||
entityId: table._id!,
|
entityId: table._id!,
|
||||||
operation: Operation.READ,
|
operation: Operation.READ,
|
||||||
},
|
},
|
||||||
|
@ -154,34 +159,44 @@ export async function search(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const query = builder._query(request, {
|
const alias = new AliasTables(allTables.map(table => table.name))
|
||||||
disableReturning: true,
|
const rows = await alias.queryWithAliasing(request, async json => {
|
||||||
|
const query = builder._query(json, {
|
||||||
|
disableReturning: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
if (Array.isArray(query)) {
|
||||||
|
throw new Error("SQS cannot currently handle multiple queries")
|
||||||
|
}
|
||||||
|
|
||||||
|
let sql = query.sql,
|
||||||
|
bindings = query.bindings
|
||||||
|
|
||||||
|
// quick hack for docIds
|
||||||
|
sql = sql.replace(/`doc1`.`rowId`/g, "`doc1.rowId`")
|
||||||
|
sql = sql.replace(/`doc2`.`rowId`/g, "`doc2.rowId`")
|
||||||
|
|
||||||
|
const db = context.getAppDB()
|
||||||
|
return await db.sql<Row>(sql, bindings)
|
||||||
})
|
})
|
||||||
|
|
||||||
if (Array.isArray(query)) {
|
// process from the format of tableId.column to expected format
|
||||||
throw new Error("SQS cannot currently handle multiple queries")
|
const processed = await sqlOutputProcessing(
|
||||||
}
|
rows,
|
||||||
|
table!,
|
||||||
let sql = query.sql,
|
allTablesMap,
|
||||||
bindings = query.bindings
|
relationships,
|
||||||
|
{
|
||||||
// quick hack for docIds
|
sqs: true,
|
||||||
sql = sql.replace(/`doc1`.`rowId`/g, "`doc1.rowId`")
|
}
|
||||||
sql = sql.replace(/`doc2`.`rowId`/g, "`doc2.rowId`")
|
)
|
||||||
|
|
||||||
const db = context.getAppDB()
|
|
||||||
const rows = await db.sql<Row>(sql, bindings)
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
rows: await sqlOutputProcessing(
|
// final row processing for response
|
||||||
rows,
|
rows: await outputProcessing<Row[]>(table, processed, {
|
||||||
table!,
|
preserveLinks: true,
|
||||||
allTablesMap,
|
squash: true,
|
||||||
relationships,
|
}),
|
||||||
{
|
|
||||||
sqs: true,
|
|
||||||
}
|
|
||||||
),
|
|
||||||
}
|
}
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
const msg = typeof err === "string" ? err : err.message
|
const msg = typeof err === "string" ? err : err.message
|
||||||
|
|
|
@ -112,7 +112,7 @@ describe("external search", () => {
|
||||||
tableId,
|
tableId,
|
||||||
query: {},
|
query: {},
|
||||||
}
|
}
|
||||||
const result = await search(searchParams)
|
const result = await search(searchParams, config.table!)
|
||||||
|
|
||||||
expect(result.rows).toHaveLength(10)
|
expect(result.rows).toHaveLength(10)
|
||||||
expect(result.rows).toEqual(
|
expect(result.rows).toEqual(
|
||||||
|
@ -130,7 +130,7 @@ describe("external search", () => {
|
||||||
query: {},
|
query: {},
|
||||||
fields: ["name", "age"],
|
fields: ["name", "age"],
|
||||||
}
|
}
|
||||||
const result = await search(searchParams)
|
const result = await search(searchParams, config.table!)
|
||||||
|
|
||||||
expect(result.rows).toHaveLength(10)
|
expect(result.rows).toHaveLength(10)
|
||||||
expect(result.rows).toEqual(
|
expect(result.rows).toEqual(
|
||||||
|
@ -157,7 +157,7 @@ describe("external search", () => {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
const result = await search(searchParams)
|
const result = await search(searchParams, config.table!)
|
||||||
|
|
||||||
expect(result.rows).toHaveLength(3)
|
expect(result.rows).toHaveLength(3)
|
||||||
expect(result.rows.map(row => row.id)).toEqual([1, 4, 8])
|
expect(result.rows.map(row => row.id)).toEqual([1, 4, 8])
|
||||||
|
|
|
@ -81,7 +81,7 @@ describe("internal", () => {
|
||||||
tableId,
|
tableId,
|
||||||
query: {},
|
query: {},
|
||||||
}
|
}
|
||||||
const result = await search(searchParams)
|
const result = await search(searchParams, config.table!)
|
||||||
|
|
||||||
expect(result.rows).toHaveLength(10)
|
expect(result.rows).toHaveLength(10)
|
||||||
expect(result.rows).toEqual(
|
expect(result.rows).toEqual(
|
||||||
|
@ -99,7 +99,7 @@ describe("internal", () => {
|
||||||
query: {},
|
query: {},
|
||||||
fields: ["name", "age"],
|
fields: ["name", "age"],
|
||||||
}
|
}
|
||||||
const result = await search(searchParams)
|
const result = await search(searchParams, config.table!)
|
||||||
|
|
||||||
expect(result.rows).toHaveLength(10)
|
expect(result.rows).toHaveLength(10)
|
||||||
expect(result.rows).toEqual(
|
expect(result.rows).toEqual(
|
||||||
|
|
|
@ -160,7 +160,7 @@ describe("internal search", () => {
|
||||||
const response = await search.paginatedSearch(
|
const response = await search.paginatedSearch(
|
||||||
{
|
{
|
||||||
contains: {
|
contains: {
|
||||||
column: "a",
|
column: ["a"],
|
||||||
colArr: [1, 2, 3],
|
colArr: [1, 2, 3],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -168,7 +168,7 @@ describe("internal search", () => {
|
||||||
)
|
)
|
||||||
checkLucene(
|
checkLucene(
|
||||||
response,
|
response,
|
||||||
`(*:* AND column:a AND colArr:(1 AND 2 AND 3))`,
|
`(*:* AND column:(a) AND colArr:(1 AND 2 AND 3))`,
|
||||||
PARAMS
|
PARAMS
|
||||||
)
|
)
|
||||||
})
|
})
|
|
@ -6,11 +6,12 @@ import {
|
||||||
Row,
|
Row,
|
||||||
SearchFilters,
|
SearchFilters,
|
||||||
} from "@budibase/types"
|
} from "@budibase/types"
|
||||||
import { getSQLClient } from "../../../sdk/app/rows/utils"
|
import { getSQLClient } from "./utils"
|
||||||
import { cloneDeep } from "lodash"
|
import { cloneDeep } from "lodash"
|
||||||
import sdk from "../../../sdk"
|
import datasources from "../datasources"
|
||||||
import { makeExternalQuery } from "../../../integrations/base/query"
|
import { makeExternalQuery } from "../../../integrations/base/query"
|
||||||
import { SqlClient } from "../../../integrations/utils"
|
import { SqlClient } from "../../../integrations/utils"
|
||||||
|
import { SQS_DATASOURCE_INTERNAL } from "../../../db/utils"
|
||||||
|
|
||||||
const WRITE_OPERATIONS: Operation[] = [
|
const WRITE_OPERATIONS: Operation[] = [
|
||||||
Operation.CREATE,
|
Operation.CREATE,
|
||||||
|
@ -156,12 +157,19 @@ export default class AliasTables {
|
||||||
}
|
}
|
||||||
|
|
||||||
async queryWithAliasing(
|
async queryWithAliasing(
|
||||||
json: QueryJson
|
json: QueryJson,
|
||||||
|
queryFn?: (json: QueryJson) => Promise<DatasourcePlusQueryResponse>
|
||||||
): Promise<DatasourcePlusQueryResponse> {
|
): Promise<DatasourcePlusQueryResponse> {
|
||||||
const datasourceId = json.endpoint.datasourceId
|
const datasourceId = json.endpoint.datasourceId
|
||||||
const datasource = await sdk.datasources.get(datasourceId)
|
const isSqs = datasourceId === SQS_DATASOURCE_INTERNAL
|
||||||
|
let aliasingEnabled: boolean, datasource: Datasource | undefined
|
||||||
|
if (isSqs) {
|
||||||
|
aliasingEnabled = true
|
||||||
|
} else {
|
||||||
|
datasource = await datasources.get(datasourceId)
|
||||||
|
aliasingEnabled = this.isAliasingEnabled(json, datasource)
|
||||||
|
}
|
||||||
|
|
||||||
const aliasingEnabled = this.isAliasingEnabled(json, datasource)
|
|
||||||
if (aliasingEnabled) {
|
if (aliasingEnabled) {
|
||||||
json = cloneDeep(json)
|
json = cloneDeep(json)
|
||||||
// run through the query json to update anywhere a table may be used
|
// run through the query json to update anywhere a table may be used
|
||||||
|
@ -207,7 +215,15 @@ export default class AliasTables {
|
||||||
}
|
}
|
||||||
json.tableAliases = invertedTableAliases
|
json.tableAliases = invertedTableAliases
|
||||||
}
|
}
|
||||||
const response = await makeExternalQuery(datasource, json)
|
|
||||||
|
let response: DatasourcePlusQueryResponse
|
||||||
|
if (datasource && !isSqs) {
|
||||||
|
response = await makeExternalQuery(datasource, json)
|
||||||
|
} else if (queryFn) {
|
||||||
|
response = await queryFn(json)
|
||||||
|
} else {
|
||||||
|
throw new Error("No supplied method to perform aliased query")
|
||||||
|
}
|
||||||
if (Array.isArray(response) && aliasingEnabled) {
|
if (Array.isArray(response) && aliasingEnabled) {
|
||||||
return this.reverse(response)
|
return this.reverse(response)
|
||||||
} else {
|
} else {
|
|
@ -1,8 +1,19 @@
|
||||||
import { context, SQLITE_DESIGN_DOC_ID } from "@budibase/backend-core"
|
import { context, SQLITE_DESIGN_DOC_ID } from "@budibase/backend-core"
|
||||||
import { FieldType, SQLiteDefinition, SQLiteType, Table } from "@budibase/types"
|
import {
|
||||||
|
FieldType,
|
||||||
|
RelationshipFieldMetadata,
|
||||||
|
SQLiteDefinition,
|
||||||
|
SQLiteTable,
|
||||||
|
SQLiteTables,
|
||||||
|
SQLiteType,
|
||||||
|
Table,
|
||||||
|
} from "@budibase/types"
|
||||||
import { cloneDeep } from "lodash"
|
import { cloneDeep } from "lodash"
|
||||||
import tablesSdk from "../"
|
import tablesSdk from "../"
|
||||||
import { CONSTANT_INTERNAL_ROW_COLS } from "../../../../db/utils"
|
import {
|
||||||
|
CONSTANT_INTERNAL_ROW_COLS,
|
||||||
|
generateJunctionTableID,
|
||||||
|
} from "../../../../db/utils"
|
||||||
|
|
||||||
const BASIC_SQLITE_DOC: SQLiteDefinition = {
|
const BASIC_SQLITE_DOC: SQLiteDefinition = {
|
||||||
_id: SQLITE_DESIGN_DOC_ID,
|
_id: SQLITE_DESIGN_DOC_ID,
|
||||||
|
@ -36,9 +47,42 @@ const FieldTypeMap: Record<FieldType, SQLiteType> = {
|
||||||
[FieldType.BB_REFERENCE]: SQLiteType.TEXT,
|
[FieldType.BB_REFERENCE]: SQLiteType.TEXT,
|
||||||
}
|
}
|
||||||
|
|
||||||
function mapTable(table: Table): { [key: string]: SQLiteType } {
|
function buildRelationshipDefinitions(
|
||||||
|
table: Table,
|
||||||
|
relationshipColumn: RelationshipFieldMetadata
|
||||||
|
): {
|
||||||
|
tableId: string
|
||||||
|
definition: SQLiteTable
|
||||||
|
} {
|
||||||
|
const tableId = table._id!,
|
||||||
|
relatedTableId = relationshipColumn.tableId
|
||||||
|
return {
|
||||||
|
tableId: generateJunctionTableID(tableId, relatedTableId),
|
||||||
|
definition: {
|
||||||
|
["doc1.rowId"]: SQLiteType.TEXT,
|
||||||
|
["doc1.tableId"]: SQLiteType.TEXT,
|
||||||
|
["doc1.fieldName"]: SQLiteType.TEXT,
|
||||||
|
["doc2.rowId"]: SQLiteType.TEXT,
|
||||||
|
["doc2.tableId"]: SQLiteType.TEXT,
|
||||||
|
["doc2.fieldName"]: SQLiteType.TEXT,
|
||||||
|
tableId: SQLiteType.TEXT,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// this can generate relationship tables as part of the mapping
|
||||||
|
function mapTable(table: Table): SQLiteTables {
|
||||||
|
const tables: SQLiteTables = {}
|
||||||
const fields: Record<string, SQLiteType> = {}
|
const fields: Record<string, SQLiteType> = {}
|
||||||
for (let [key, column] of Object.entries(table.schema)) {
|
for (let [key, column] of Object.entries(table.schema)) {
|
||||||
|
// relationships should be handled differently
|
||||||
|
if (column.type === FieldType.LINK) {
|
||||||
|
const { tableId, definition } = buildRelationshipDefinitions(
|
||||||
|
table,
|
||||||
|
column
|
||||||
|
)
|
||||||
|
tables[tableId] = { fields: definition }
|
||||||
|
}
|
||||||
if (!FieldTypeMap[column.type]) {
|
if (!FieldTypeMap[column.type]) {
|
||||||
throw new Error(`Unable to map type "${column.type}" to SQLite type`)
|
throw new Error(`Unable to map type "${column.type}" to SQLite type`)
|
||||||
}
|
}
|
||||||
|
@ -49,10 +93,12 @@ function mapTable(table: Table): { [key: string]: SQLiteType } {
|
||||||
CONSTANT_INTERNAL_ROW_COLS.forEach(col => {
|
CONSTANT_INTERNAL_ROW_COLS.forEach(col => {
|
||||||
constantMap[col] = SQLiteType.TEXT
|
constantMap[col] = SQLiteType.TEXT
|
||||||
})
|
})
|
||||||
return {
|
const thisTable: SQLiteTable = {
|
||||||
...constantMap,
|
...constantMap,
|
||||||
...fields,
|
...fields,
|
||||||
}
|
}
|
||||||
|
tables[table._id!] = { fields: thisTable }
|
||||||
|
return tables
|
||||||
}
|
}
|
||||||
|
|
||||||
// nothing exists, need to iterate though existing tables
|
// nothing exists, need to iterate though existing tables
|
||||||
|
@ -60,8 +106,9 @@ async function buildBaseDefinition(): Promise<SQLiteDefinition> {
|
||||||
const tables = await tablesSdk.getAllInternalTables()
|
const tables = await tablesSdk.getAllInternalTables()
|
||||||
const definition = cloneDeep(BASIC_SQLITE_DOC)
|
const definition = cloneDeep(BASIC_SQLITE_DOC)
|
||||||
for (let table of tables) {
|
for (let table of tables) {
|
||||||
definition.sql.tables[table._id!] = {
|
definition.sql.tables = {
|
||||||
fields: mapTable(table),
|
...definition.sql.tables,
|
||||||
|
...mapTable(table),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return definition
|
return definition
|
||||||
|
@ -75,8 +122,9 @@ export async function addTableToSqlite(table: Table) {
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
definition = await buildBaseDefinition()
|
definition = await buildBaseDefinition()
|
||||||
}
|
}
|
||||||
definition.sql.tables[table._id!] = {
|
definition.sql.tables = {
|
||||||
fields: mapTable(table),
|
...definition.sql.tables,
|
||||||
|
...mapTable(table),
|
||||||
}
|
}
|
||||||
await db.put(definition)
|
await db.put(definition)
|
||||||
}
|
}
|
||||||
|
|
|
@ -351,7 +351,6 @@ describe("table sdk", () => {
|
||||||
const view: ViewV2 = {
|
const view: ViewV2 = {
|
||||||
...basicView,
|
...basicView,
|
||||||
}
|
}
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
||||||
const { name, description, ...newTableSchema } = basicTable.schema
|
const { name, description, ...newTableSchema } = basicTable.schema
|
||||||
|
|
||||||
const result = syncSchema(_.cloneDeep(view), newTableSchema, undefined)
|
const result = syncSchema(_.cloneDeep(view), newTableSchema, undefined)
|
||||||
|
@ -365,7 +364,6 @@ describe("table sdk", () => {
|
||||||
const view: ViewV2 = {
|
const view: ViewV2 = {
|
||||||
...basicView,
|
...basicView,
|
||||||
}
|
}
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
||||||
const { description, ...newTableSchema } = {
|
const { description, ...newTableSchema } = {
|
||||||
...basicTable.schema,
|
...basicTable.schema,
|
||||||
updatedDescription: {
|
updatedDescription: {
|
||||||
|
@ -450,7 +448,6 @@ describe("table sdk", () => {
|
||||||
hiddenField: { visible: false },
|
hiddenField: { visible: false },
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
||||||
const { name, description, ...newTableSchema } = basicTable.schema
|
const { name, description, ...newTableSchema } = basicTable.schema
|
||||||
|
|
||||||
const result = syncSchema(_.cloneDeep(view), newTableSchema, undefined)
|
const result = syncSchema(_.cloneDeep(view), newTableSchema, undefined)
|
||||||
|
@ -474,7 +471,6 @@ describe("table sdk", () => {
|
||||||
hiddenField: { visible: false },
|
hiddenField: { visible: false },
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
||||||
const { name, description, ...newTableSchema } = {
|
const { name, description, ...newTableSchema } = {
|
||||||
...basicTable.schema,
|
...basicTable.schema,
|
||||||
newField1: {
|
newField1: {
|
||||||
|
@ -506,7 +502,6 @@ describe("table sdk", () => {
|
||||||
hiddenField: { visible: false },
|
hiddenField: { visible: false },
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
||||||
const { description, ...newTableSchema } = {
|
const { description, ...newTableSchema } = {
|
||||||
...basicTable.schema,
|
...basicTable.schema,
|
||||||
updatedDescription: {
|
updatedDescription: {
|
||||||
|
|
|
@ -60,7 +60,10 @@ export class DatasourceAPI extends TestAPI {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
query = async (query: QueryJson, expectations?: Expectations) => {
|
query = async (
|
||||||
|
query: Omit<QueryJson, "meta">,
|
||||||
|
expectations?: Expectations
|
||||||
|
) => {
|
||||||
return await this._post<any>(`/api/datasources/query`, {
|
return await this._post<any>(`/api/datasources/query`, {
|
||||||
body: query,
|
body: query,
|
||||||
expectations,
|
expectations,
|
||||||
|
|
|
@ -374,11 +374,13 @@ class Orchestrator {
|
||||||
for (let [innerObject, innerValue] of Object.entries(
|
for (let [innerObject, innerValue] of Object.entries(
|
||||||
originalStepInput[key][innerKey]
|
originalStepInput[key][innerKey]
|
||||||
)) {
|
)) {
|
||||||
originalStepInput[key][innerKey][innerObject] =
|
if (typeof innerValue === "string") {
|
||||||
automationUtils.substituteLoopStep(
|
originalStepInput[key][innerKey][innerObject] =
|
||||||
innerValue as string,
|
automationUtils.substituteLoopStep(
|
||||||
`steps.${loopStepNumber}`
|
innerValue,
|
||||||
)
|
`steps.${loopStepNumber}`
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -458,7 +460,6 @@ class Orchestrator {
|
||||||
inputs,
|
inputs,
|
||||||
step.schema.inputs
|
step.schema.inputs
|
||||||
)
|
)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// appId is always passed
|
// appId is always passed
|
||||||
const outputs = await stepFn({
|
const outputs = await stepFn({
|
||||||
|
|
|
@ -262,13 +262,11 @@ export class BaseSocket {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
async onConnect(_socket: Socket) {
|
||||||
async onConnect(socket: Socket) {
|
|
||||||
// Override
|
// Override
|
||||||
}
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
async onDisconnect(_socket: Socket) {
|
||||||
async onDisconnect(socket: Socket) {
|
|
||||||
// Override
|
// Override
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -2,10 +2,11 @@ import {
|
||||||
Datasource,
|
Datasource,
|
||||||
FieldSubtype,
|
FieldSubtype,
|
||||||
FieldType,
|
FieldType,
|
||||||
|
FormulaType,
|
||||||
SearchFilter,
|
SearchFilter,
|
||||||
SearchQuery,
|
SearchFilters,
|
||||||
SearchQueryFields,
|
SearchQueryFields,
|
||||||
SearchQueryOperators,
|
SearchFilterOperator,
|
||||||
SortDirection,
|
SortDirection,
|
||||||
SortType,
|
SortType,
|
||||||
} from "@budibase/types"
|
} from "@budibase/types"
|
||||||
|
@ -19,9 +20,13 @@ const HBS_REGEX = /{{([^{].*?)}}/g
|
||||||
* Returns the valid operator options for a certain data type
|
* Returns the valid operator options for a certain data type
|
||||||
*/
|
*/
|
||||||
export const getValidOperatorsForType = (
|
export const getValidOperatorsForType = (
|
||||||
fieldType: { type: FieldType; subtype?: FieldSubtype },
|
fieldType: {
|
||||||
|
type: FieldType
|
||||||
|
subtype?: FieldSubtype
|
||||||
|
formulaType?: FormulaType
|
||||||
|
},
|
||||||
field: string,
|
field: string,
|
||||||
datasource: Datasource & { tableId: any } // TODO: is this table id ever populated?
|
datasource: Datasource & { tableId: any }
|
||||||
) => {
|
) => {
|
||||||
const Op = OperatorOptions
|
const Op = OperatorOptions
|
||||||
const stringOps = [
|
const stringOps = [
|
||||||
|
@ -46,7 +51,7 @@ export const getValidOperatorsForType = (
|
||||||
value: string
|
value: string
|
||||||
label: string
|
label: string
|
||||||
}[] = []
|
}[] = []
|
||||||
const { type, subtype } = fieldType
|
const { type, subtype, formulaType } = fieldType
|
||||||
if (type === FieldType.STRING) {
|
if (type === FieldType.STRING) {
|
||||||
ops = stringOps
|
ops = stringOps
|
||||||
} else if (type === FieldType.NUMBER || type === FieldType.BIGINT) {
|
} else if (type === FieldType.NUMBER || type === FieldType.BIGINT) {
|
||||||
|
@ -61,7 +66,7 @@ export const getValidOperatorsForType = (
|
||||||
ops = stringOps
|
ops = stringOps
|
||||||
} else if (type === FieldType.DATETIME) {
|
} else if (type === FieldType.DATETIME) {
|
||||||
ops = numOps
|
ops = numOps
|
||||||
} else if (type === FieldType.FORMULA) {
|
} else if (type === FieldType.FORMULA && formulaType === FormulaType.STATIC) {
|
||||||
ops = stringOps.concat([Op.MoreThan, Op.LessThan])
|
ops = stringOps.concat([Op.MoreThan, Op.LessThan])
|
||||||
} else if (type === FieldType.BB_REFERENCE && subtype == FieldSubtype.USER) {
|
} else if (type === FieldType.BB_REFERENCE && subtype == FieldSubtype.USER) {
|
||||||
ops = [Op.Equals, Op.NotEquals, Op.Empty, Op.NotEmpty, Op.In]
|
ops = [Op.Equals, Op.NotEquals, Op.Empty, Op.NotEmpty, Op.In]
|
||||||
|
@ -94,18 +99,19 @@ export const NoEmptyFilterStrings = [
|
||||||
* Removes any fields that contain empty strings that would cause inconsistent
|
* Removes any fields that contain empty strings that would cause inconsistent
|
||||||
* behaviour with how backend tables are filtered (no value means no filter).
|
* behaviour with how backend tables are filtered (no value means no filter).
|
||||||
*/
|
*/
|
||||||
const cleanupQuery = (query: SearchQuery) => {
|
const cleanupQuery = (query: SearchFilters) => {
|
||||||
if (!query) {
|
if (!query) {
|
||||||
return query
|
return query
|
||||||
}
|
}
|
||||||
for (let filterField of NoEmptyFilterStrings) {
|
for (let filterField of NoEmptyFilterStrings) {
|
||||||
if (!query[filterField]) {
|
const operator = filterField as SearchFilterOperator
|
||||||
|
if (!query[operator]) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
for (let [key, value] of Object.entries(query[filterField]!)) {
|
for (let [key, value] of Object.entries(query[operator]!)) {
|
||||||
if (value == null || value === "") {
|
if (value == null || value === "") {
|
||||||
delete query[filterField]![key]
|
delete query[operator]![key]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -115,9 +121,10 @@ const cleanupQuery = (query: SearchQuery) => {
|
||||||
/**
|
/**
|
||||||
* Removes a numeric prefix on field names designed to give fields uniqueness
|
* Removes a numeric prefix on field names designed to give fields uniqueness
|
||||||
*/
|
*/
|
||||||
const removeKeyNumbering = (key: string) => {
|
export const removeKeyNumbering = (key: string): string => {
|
||||||
if (typeof key === "string" && key.match(/\d[0-9]*:/g) != null) {
|
if (typeof key === "string" && key.match(/\d[0-9]*:/g) != null) {
|
||||||
const parts = key.split(":")
|
const parts = key.split(":")
|
||||||
|
// remove the number
|
||||||
parts.shift()
|
parts.shift()
|
||||||
return parts.join(":")
|
return parts.join(":")
|
||||||
} else {
|
} else {
|
||||||
|
@ -130,7 +137,7 @@ const removeKeyNumbering = (key: string) => {
|
||||||
* @param filter the builder filter structure
|
* @param filter the builder filter structure
|
||||||
*/
|
*/
|
||||||
export const buildLuceneQuery = (filter: SearchFilter[]) => {
|
export const buildLuceneQuery = (filter: SearchFilter[]) => {
|
||||||
let query: SearchQuery = {
|
let query: SearchFilters = {
|
||||||
string: {},
|
string: {},
|
||||||
fuzzy: {},
|
fuzzy: {},
|
||||||
range: {},
|
range: {},
|
||||||
|
@ -151,6 +158,7 @@ export const buildLuceneQuery = (filter: SearchFilter[]) => {
|
||||||
filter.forEach(expression => {
|
filter.forEach(expression => {
|
||||||
let { operator, field, type, value, externalType, onEmptyFilter } =
|
let { operator, field, type, value, externalType, onEmptyFilter } =
|
||||||
expression
|
expression
|
||||||
|
const queryOperator = operator as SearchFilterOperator
|
||||||
const isHbs =
|
const isHbs =
|
||||||
typeof value === "string" && (value.match(HBS_REGEX) || []).length > 0
|
typeof value === "string" && (value.match(HBS_REGEX) || []).length > 0
|
||||||
// Parse all values into correct types
|
// Parse all values into correct types
|
||||||
|
@ -165,8 +173,8 @@ export const buildLuceneQuery = (filter: SearchFilter[]) => {
|
||||||
if (
|
if (
|
||||||
type === "datetime" &&
|
type === "datetime" &&
|
||||||
!isHbs &&
|
!isHbs &&
|
||||||
operator !== "empty" &&
|
queryOperator !== "empty" &&
|
||||||
operator !== "notEmpty"
|
queryOperator !== "notEmpty"
|
||||||
) {
|
) {
|
||||||
// Ensure date value is a valid date and parse into correct format
|
// Ensure date value is a valid date and parse into correct format
|
||||||
if (!value) {
|
if (!value) {
|
||||||
|
@ -179,7 +187,7 @@ export const buildLuceneQuery = (filter: SearchFilter[]) => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (type === "number" && typeof value === "string" && !isHbs) {
|
if (type === "number" && typeof value === "string" && !isHbs) {
|
||||||
if (operator === "oneOf") {
|
if (queryOperator === "oneOf") {
|
||||||
value = value.split(",").map(item => parseFloat(item))
|
value = value.split(",").map(item => parseFloat(item))
|
||||||
} else {
|
} else {
|
||||||
value = parseFloat(value)
|
value = parseFloat(value)
|
||||||
|
@ -219,24 +227,24 @@ export const buildLuceneQuery = (filter: SearchFilter[]) => {
|
||||||
) {
|
) {
|
||||||
query.range[field].high = value
|
query.range[field].high = value
|
||||||
}
|
}
|
||||||
} else if (query[operator] && operator !== "onEmptyFilter") {
|
} else if (query[queryOperator] && operator !== "onEmptyFilter") {
|
||||||
if (type === "boolean") {
|
if (type === "boolean") {
|
||||||
// Transform boolean filters to cope with null.
|
// Transform boolean filters to cope with null.
|
||||||
// "equals false" needs to be "not equals true"
|
// "equals false" needs to be "not equals true"
|
||||||
// "not equals false" needs to be "equals true"
|
// "not equals false" needs to be "equals true"
|
||||||
if (operator === "equal" && value === false) {
|
if (queryOperator === "equal" && value === false) {
|
||||||
query.notEqual = query.notEqual || {}
|
query.notEqual = query.notEqual || {}
|
||||||
query.notEqual[field] = true
|
query.notEqual[field] = true
|
||||||
} else if (operator === "notEqual" && value === false) {
|
} else if (queryOperator === "notEqual" && value === false) {
|
||||||
query.equal = query.equal || {}
|
query.equal = query.equal || {}
|
||||||
query.equal[field] = true
|
query.equal[field] = true
|
||||||
} else {
|
} else {
|
||||||
query[operator] = query[operator] || {}
|
query[queryOperator] = query[queryOperator] || {}
|
||||||
query[operator]![field] = value
|
query[queryOperator]![field] = value
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
query[operator] = query[operator] || {}
|
query[queryOperator] = query[queryOperator] || {}
|
||||||
query[operator]![field] = value
|
query[queryOperator]![field] = value
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -249,7 +257,7 @@ export const buildLuceneQuery = (filter: SearchFilter[]) => {
|
||||||
* @param docs the data
|
* @param docs the data
|
||||||
* @param query the JSON lucene query
|
* @param query the JSON lucene query
|
||||||
*/
|
*/
|
||||||
export const runLuceneQuery = (docs: any[], query?: SearchQuery) => {
|
export const runLuceneQuery = (docs: any[], query?: SearchFilters) => {
|
||||||
if (!docs || !Array.isArray(docs)) {
|
if (!docs || !Array.isArray(docs)) {
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
|
@ -263,7 +271,7 @@ export const runLuceneQuery = (docs: any[], query?: SearchQuery) => {
|
||||||
// Iterates over a set of filters and evaluates a fail function against a doc
|
// Iterates over a set of filters and evaluates a fail function against a doc
|
||||||
const match =
|
const match =
|
||||||
(
|
(
|
||||||
type: keyof SearchQueryFields,
|
type: SearchFilterOperator,
|
||||||
failFn: (docValue: any, testValue: any) => boolean
|
failFn: (docValue: any, testValue: any) => boolean
|
||||||
) =>
|
) =>
|
||||||
(doc: any) => {
|
(doc: any) => {
|
||||||
|
@ -280,7 +288,7 @@ export const runLuceneQuery = (docs: any[], query?: SearchQuery) => {
|
||||||
|
|
||||||
// Process a string match (fails if the value does not start with the string)
|
// Process a string match (fails if the value does not start with the string)
|
||||||
const stringMatch = match(
|
const stringMatch = match(
|
||||||
SearchQueryOperators.STRING,
|
SearchFilterOperator.STRING,
|
||||||
(docValue: string, testValue: string) => {
|
(docValue: string, testValue: string) => {
|
||||||
return (
|
return (
|
||||||
!docValue ||
|
!docValue ||
|
||||||
|
@ -291,7 +299,7 @@ export const runLuceneQuery = (docs: any[], query?: SearchQuery) => {
|
||||||
|
|
||||||
// Process a fuzzy match (treat the same as starts with when running locally)
|
// Process a fuzzy match (treat the same as starts with when running locally)
|
||||||
const fuzzyMatch = match(
|
const fuzzyMatch = match(
|
||||||
SearchQueryOperators.FUZZY,
|
SearchFilterOperator.FUZZY,
|
||||||
(docValue: string, testValue: string) => {
|
(docValue: string, testValue: string) => {
|
||||||
return (
|
return (
|
||||||
!docValue ||
|
!docValue ||
|
||||||
|
@ -302,7 +310,7 @@ export const runLuceneQuery = (docs: any[], query?: SearchQuery) => {
|
||||||
|
|
||||||
// Process a range match
|
// Process a range match
|
||||||
const rangeMatch = match(
|
const rangeMatch = match(
|
||||||
SearchQueryOperators.RANGE,
|
SearchFilterOperator.RANGE,
|
||||||
(
|
(
|
||||||
docValue: string | number | null,
|
docValue: string | number | null,
|
||||||
testValue: { low: number; high: number }
|
testValue: { low: number; high: number }
|
||||||
|
@ -325,7 +333,7 @@ export const runLuceneQuery = (docs: any[], query?: SearchQuery) => {
|
||||||
|
|
||||||
// Process an equal match (fails if the value is different)
|
// Process an equal match (fails if the value is different)
|
||||||
const equalMatch = match(
|
const equalMatch = match(
|
||||||
SearchQueryOperators.EQUAL,
|
SearchFilterOperator.EQUAL,
|
||||||
(docValue: any, testValue: string | null) => {
|
(docValue: any, testValue: string | null) => {
|
||||||
return testValue != null && testValue !== "" && docValue !== testValue
|
return testValue != null && testValue !== "" && docValue !== testValue
|
||||||
}
|
}
|
||||||
|
@ -333,7 +341,7 @@ export const runLuceneQuery = (docs: any[], query?: SearchQuery) => {
|
||||||
|
|
||||||
// Process a not-equal match (fails if the value is the same)
|
// Process a not-equal match (fails if the value is the same)
|
||||||
const notEqualMatch = match(
|
const notEqualMatch = match(
|
||||||
SearchQueryOperators.NOT_EQUAL,
|
SearchFilterOperator.NOT_EQUAL,
|
||||||
(docValue: any, testValue: string | null) => {
|
(docValue: any, testValue: string | null) => {
|
||||||
return testValue != null && testValue !== "" && docValue === testValue
|
return testValue != null && testValue !== "" && docValue === testValue
|
||||||
}
|
}
|
||||||
|
@ -341,7 +349,7 @@ export const runLuceneQuery = (docs: any[], query?: SearchQuery) => {
|
||||||
|
|
||||||
// Process an empty match (fails if the value is not empty)
|
// Process an empty match (fails if the value is not empty)
|
||||||
const emptyMatch = match(
|
const emptyMatch = match(
|
||||||
SearchQueryOperators.EMPTY,
|
SearchFilterOperator.EMPTY,
|
||||||
(docValue: string | null) => {
|
(docValue: string | null) => {
|
||||||
return docValue != null && docValue !== ""
|
return docValue != null && docValue !== ""
|
||||||
}
|
}
|
||||||
|
@ -349,7 +357,7 @@ export const runLuceneQuery = (docs: any[], query?: SearchQuery) => {
|
||||||
|
|
||||||
// Process a not-empty match (fails is the value is empty)
|
// Process a not-empty match (fails is the value is empty)
|
||||||
const notEmptyMatch = match(
|
const notEmptyMatch = match(
|
||||||
SearchQueryOperators.NOT_EMPTY,
|
SearchFilterOperator.NOT_EMPTY,
|
||||||
(docValue: string | null) => {
|
(docValue: string | null) => {
|
||||||
return docValue == null || docValue === ""
|
return docValue == null || docValue === ""
|
||||||
}
|
}
|
||||||
|
@ -357,7 +365,7 @@ export const runLuceneQuery = (docs: any[], query?: SearchQuery) => {
|
||||||
|
|
||||||
// Process an includes match (fails if the value is not included)
|
// Process an includes match (fails if the value is not included)
|
||||||
const oneOf = match(
|
const oneOf = match(
|
||||||
SearchQueryOperators.ONE_OF,
|
SearchFilterOperator.ONE_OF,
|
||||||
(docValue: any, testValue: any) => {
|
(docValue: any, testValue: any) => {
|
||||||
if (typeof testValue === "string") {
|
if (typeof testValue === "string") {
|
||||||
testValue = testValue.split(",")
|
testValue = testValue.split(",")
|
||||||
|
@ -370,28 +378,28 @@ export const runLuceneQuery = (docs: any[], query?: SearchQuery) => {
|
||||||
)
|
)
|
||||||
|
|
||||||
const containsAny = match(
|
const containsAny = match(
|
||||||
SearchQueryOperators.CONTAINS_ANY,
|
SearchFilterOperator.CONTAINS_ANY,
|
||||||
(docValue: any, testValue: any) => {
|
(docValue: any, testValue: any) => {
|
||||||
return !docValue?.includes(...testValue)
|
return !docValue?.includes(...testValue)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
const contains = match(
|
const contains = match(
|
||||||
SearchQueryOperators.CONTAINS,
|
SearchFilterOperator.CONTAINS,
|
||||||
(docValue: string | any[], testValue: any[]) => {
|
(docValue: string | any[], testValue: any[]) => {
|
||||||
return !testValue?.every((item: any) => docValue?.includes(item))
|
return !testValue?.every((item: any) => docValue?.includes(item))
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
const notContains = match(
|
const notContains = match(
|
||||||
SearchQueryOperators.NOT_CONTAINS,
|
SearchFilterOperator.NOT_CONTAINS,
|
||||||
(docValue: string | any[], testValue: any[]) => {
|
(docValue: string | any[], testValue: any[]) => {
|
||||||
return testValue?.every((item: any) => docValue?.includes(item))
|
return testValue?.every((item: any) => docValue?.includes(item))
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
const docMatch = (doc: any) => {
|
const docMatch = (doc: any) => {
|
||||||
const filterFunctions: Record<SearchQueryOperators, (doc: any) => boolean> =
|
const filterFunctions: Record<SearchFilterOperator, (doc: any) => boolean> =
|
||||||
{
|
{
|
||||||
string: stringMatch,
|
string: stringMatch,
|
||||||
fuzzy: fuzzyMatch,
|
fuzzy: fuzzyMatch,
|
||||||
|
@ -406,7 +414,7 @@ export const runLuceneQuery = (docs: any[], query?: SearchQuery) => {
|
||||||
notContains: notContains,
|
notContains: notContains,
|
||||||
}
|
}
|
||||||
|
|
||||||
const activeFilterKeys: SearchQueryOperators[] = Object.entries(query || {})
|
const activeFilterKeys: SearchFilterOperator[] = Object.entries(query || {})
|
||||||
.filter(
|
.filter(
|
||||||
([key, value]: [string, any]) =>
|
([key, value]: [string, any]) =>
|
||||||
!["allOr", "onEmptyFilter"].includes(key) &&
|
!["allOr", "onEmptyFilter"].includes(key) &&
|
||||||
|
@ -474,7 +482,7 @@ export const luceneLimit = (docs: any[], limit: string) => {
|
||||||
return docs.slice(0, numLimit)
|
return docs.slice(0, numLimit)
|
||||||
}
|
}
|
||||||
|
|
||||||
export const hasFilters = (query?: SearchQuery) => {
|
export const hasFilters = (query?: SearchFilters) => {
|
||||||
if (!query) {
|
if (!query) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import {
|
import {
|
||||||
SearchQuery,
|
SearchFilters,
|
||||||
SearchQueryOperators,
|
SearchFilterOperator,
|
||||||
FieldType,
|
FieldType,
|
||||||
SearchFilter,
|
SearchFilter,
|
||||||
} from "@budibase/types"
|
} from "@budibase/types"
|
||||||
|
@ -46,8 +46,8 @@ describe("runLuceneQuery", () => {
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
function buildQuery(filters: { [filterKey: string]: any }): SearchQuery {
|
function buildQuery(filters: { [filterKey: string]: any }): SearchFilters {
|
||||||
const query: SearchQuery = {
|
const query: SearchFilters = {
|
||||||
string: {},
|
string: {},
|
||||||
fuzzy: {},
|
fuzzy: {},
|
||||||
range: {},
|
range: {},
|
||||||
|
@ -63,7 +63,7 @@ describe("runLuceneQuery", () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const filterKey in filters) {
|
for (const filterKey in filters) {
|
||||||
query[filterKey as SearchQueryOperators] = filters[filterKey]
|
query[filterKey as SearchFilterOperator] = filters[filterKey]
|
||||||
}
|
}
|
||||||
|
|
||||||
return query
|
return query
|
||||||
|
@ -265,13 +265,13 @@ describe("buildLuceneQuery", () => {
|
||||||
it("should parseFloat if the type is a number, but the value is a numeric string", () => {
|
it("should parseFloat if the type is a number, but the value is a numeric string", () => {
|
||||||
const filter: SearchFilter[] = [
|
const filter: SearchFilter[] = [
|
||||||
{
|
{
|
||||||
operator: SearchQueryOperators.EQUAL,
|
operator: SearchFilterOperator.EQUAL,
|
||||||
field: "customer_id",
|
field: "customer_id",
|
||||||
type: FieldType.NUMBER,
|
type: FieldType.NUMBER,
|
||||||
value: "1212",
|
value: "1212",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
operator: SearchQueryOperators.ONE_OF,
|
operator: SearchFilterOperator.ONE_OF,
|
||||||
field: "customer_id",
|
field: "customer_id",
|
||||||
type: FieldType.NUMBER,
|
type: FieldType.NUMBER,
|
||||||
value: "1000,1212,3400",
|
value: "1000,1212,3400",
|
||||||
|
@ -299,13 +299,13 @@ describe("buildLuceneQuery", () => {
|
||||||
it("should not parseFloat if the type is a number, but the value is a handlebars binding string", () => {
|
it("should not parseFloat if the type is a number, but the value is a handlebars binding string", () => {
|
||||||
const filter: SearchFilter[] = [
|
const filter: SearchFilter[] = [
|
||||||
{
|
{
|
||||||
operator: SearchQueryOperators.EQUAL,
|
operator: SearchFilterOperator.EQUAL,
|
||||||
field: "customer_id",
|
field: "customer_id",
|
||||||
type: FieldType.NUMBER,
|
type: FieldType.NUMBER,
|
||||||
value: "{{ customer_id }}",
|
value: "{{ customer_id }}",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
operator: SearchQueryOperators.ONE_OF,
|
operator: SearchFilterOperator.ONE_OF,
|
||||||
field: "customer_id",
|
field: "customer_id",
|
||||||
type: FieldType.NUMBER,
|
type: FieldType.NUMBER,
|
||||||
value: "{{ list_of_customer_ids }}",
|
value: "{{ list_of_customer_ids }}",
|
||||||
|
@ -333,19 +333,19 @@ describe("buildLuceneQuery", () => {
|
||||||
it("should cast string to boolean if the type is boolean", () => {
|
it("should cast string to boolean if the type is boolean", () => {
|
||||||
const filter: SearchFilter[] = [
|
const filter: SearchFilter[] = [
|
||||||
{
|
{
|
||||||
operator: SearchQueryOperators.EQUAL,
|
operator: SearchFilterOperator.EQUAL,
|
||||||
field: "a",
|
field: "a",
|
||||||
type: FieldType.BOOLEAN,
|
type: FieldType.BOOLEAN,
|
||||||
value: "not_true",
|
value: "not_true",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
operator: SearchQueryOperators.NOT_EQUAL,
|
operator: SearchFilterOperator.NOT_EQUAL,
|
||||||
field: "b",
|
field: "b",
|
||||||
type: FieldType.BOOLEAN,
|
type: FieldType.BOOLEAN,
|
||||||
value: "not_true",
|
value: "not_true",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
operator: SearchQueryOperators.EQUAL,
|
operator: SearchFilterOperator.EQUAL,
|
||||||
field: "c",
|
field: "c",
|
||||||
type: FieldType.BOOLEAN,
|
type: FieldType.BOOLEAN,
|
||||||
value: "true",
|
value: "true",
|
||||||
|
@ -374,19 +374,19 @@ describe("buildLuceneQuery", () => {
|
||||||
it("should split the string for contains operators", () => {
|
it("should split the string for contains operators", () => {
|
||||||
const filter: SearchFilter[] = [
|
const filter: SearchFilter[] = [
|
||||||
{
|
{
|
||||||
operator: SearchQueryOperators.CONTAINS,
|
operator: SearchFilterOperator.CONTAINS,
|
||||||
field: "description",
|
field: "description",
|
||||||
type: FieldType.ARRAY,
|
type: FieldType.ARRAY,
|
||||||
value: "Large box,Heavy box,Small box",
|
value: "Large box,Heavy box,Small box",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
operator: SearchQueryOperators.NOT_CONTAINS,
|
operator: SearchFilterOperator.NOT_CONTAINS,
|
||||||
field: "description",
|
field: "description",
|
||||||
type: FieldType.ARRAY,
|
type: FieldType.ARRAY,
|
||||||
value: "Large box,Heavy box,Small box",
|
value: "Large box,Heavy box,Small box",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
operator: SearchQueryOperators.CONTAINS_ANY,
|
operator: SearchFilterOperator.CONTAINS_ANY,
|
||||||
field: "description",
|
field: "description",
|
||||||
type: FieldType.ARRAY,
|
type: FieldType.ARRAY,
|
||||||
value: "Large box,Heavy box,Small box",
|
value: "Large box,Heavy box,Small box",
|
||||||
|
|
|
@ -4,7 +4,6 @@ export const PostProcessorNames = {
|
||||||
CONVERT_LITERALS: "convert-literals",
|
CONVERT_LITERALS: "convert-literals",
|
||||||
}
|
}
|
||||||
|
|
||||||
/* eslint-disable no-unused-vars */
|
|
||||||
class Postprocessor {
|
class Postprocessor {
|
||||||
name: string
|
name: string
|
||||||
private fn: any
|
private fn: any
|
||||||
|
|
|
@ -9,7 +9,6 @@ export const PreprocessorNames = {
|
||||||
FINALISE: "finalise",
|
FINALISE: "finalise",
|
||||||
}
|
}
|
||||||
|
|
||||||
/* eslint-disable no-unused-vars */
|
|
||||||
class Preprocessor {
|
class Preprocessor {
|
||||||
name: string
|
name: string
|
||||||
private fn: any
|
private fn: any
|
||||||
|
|
|
@ -1,68 +1,11 @@
|
||||||
import { FieldType } from "../../documents"
|
import { FieldType } from "../../documents"
|
||||||
import { EmptyFilterOption } from "../../sdk"
|
import { EmptyFilterOption, SearchFilters } from "../../sdk"
|
||||||
|
|
||||||
export type SearchFilter = {
|
export type SearchFilter = {
|
||||||
operator: keyof SearchQuery
|
operator: keyof SearchFilters | "rangeLow" | "rangeHigh"
|
||||||
onEmptyFilter?: EmptyFilterOption
|
onEmptyFilter?: EmptyFilterOption
|
||||||
field: string
|
field: string
|
||||||
type?: FieldType
|
type?: FieldType
|
||||||
value: any
|
value: any
|
||||||
externalType?: string
|
externalType?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum SearchQueryOperators {
|
|
||||||
STRING = "string",
|
|
||||||
FUZZY = "fuzzy",
|
|
||||||
RANGE = "range",
|
|
||||||
EQUAL = "equal",
|
|
||||||
NOT_EQUAL = "notEqual",
|
|
||||||
EMPTY = "empty",
|
|
||||||
NOT_EMPTY = "notEmpty",
|
|
||||||
ONE_OF = "oneOf",
|
|
||||||
CONTAINS = "contains",
|
|
||||||
NOT_CONTAINS = "notContains",
|
|
||||||
CONTAINS_ANY = "containsAny",
|
|
||||||
}
|
|
||||||
|
|
||||||
export type SearchQuery = {
|
|
||||||
allOr?: boolean
|
|
||||||
onEmptyFilter?: EmptyFilterOption
|
|
||||||
[SearchQueryOperators.STRING]?: {
|
|
||||||
[key: string]: string
|
|
||||||
}
|
|
||||||
[SearchQueryOperators.FUZZY]?: {
|
|
||||||
[key: string]: string
|
|
||||||
}
|
|
||||||
[SearchQueryOperators.RANGE]?: {
|
|
||||||
[key: string]: {
|
|
||||||
high: number | string
|
|
||||||
low: number | string
|
|
||||||
}
|
|
||||||
}
|
|
||||||
[SearchQueryOperators.EQUAL]?: {
|
|
||||||
[key: string]: any
|
|
||||||
}
|
|
||||||
[SearchQueryOperators.NOT_EQUAL]?: {
|
|
||||||
[key: string]: any
|
|
||||||
}
|
|
||||||
[SearchQueryOperators.EMPTY]?: {
|
|
||||||
[key: string]: any
|
|
||||||
}
|
|
||||||
[SearchQueryOperators.NOT_EMPTY]?: {
|
|
||||||
[key: string]: any
|
|
||||||
}
|
|
||||||
[SearchQueryOperators.ONE_OF]?: {
|
|
||||||
[key: string]: any[]
|
|
||||||
}
|
|
||||||
[SearchQueryOperators.CONTAINS]?: {
|
|
||||||
[key: string]: any[]
|
|
||||||
}
|
|
||||||
[SearchQueryOperators.NOT_CONTAINS]?: {
|
|
||||||
[key: string]: any[]
|
|
||||||
}
|
|
||||||
[SearchQueryOperators.CONTAINS_ANY]?: {
|
|
||||||
[key: string]: any[]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export type SearchQueryFields = Omit<SearchQuery, "allOr" | "onEmptyFilter">
|
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { User } from "../../documents"
|
import { User } from "../../documents"
|
||||||
import { SearchQuery } from "./searchFilter"
|
import { SearchFilters } from "../../sdk"
|
||||||
|
|
||||||
export interface SaveUserResponse {
|
export interface SaveUserResponse {
|
||||||
_id: string
|
_id: string
|
||||||
|
@ -55,7 +55,7 @@ export interface InviteUsersResponse {
|
||||||
|
|
||||||
export interface SearchUsersRequest {
|
export interface SearchUsersRequest {
|
||||||
bookmark?: string
|
bookmark?: string
|
||||||
query?: SearchQuery
|
query?: SearchFilters
|
||||||
appId?: string
|
appId?: string
|
||||||
limit?: number
|
limit?: number
|
||||||
paginate?: boolean
|
paginate?: boolean
|
||||||
|
|
|
@ -6,17 +6,23 @@ export enum SQLiteType {
|
||||||
NUMERIC = "NUMERIC",
|
NUMERIC = "NUMERIC",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type SQLiteTable = Record<
|
||||||
|
string,
|
||||||
|
SQLiteType | { field: string; type: SQLiteType }
|
||||||
|
>
|
||||||
|
|
||||||
|
export type SQLiteTables = Record<
|
||||||
|
string,
|
||||||
|
{
|
||||||
|
fields: SQLiteTable
|
||||||
|
}
|
||||||
|
>
|
||||||
|
|
||||||
export interface SQLiteDefinition {
|
export interface SQLiteDefinition {
|
||||||
_id: string
|
_id: string
|
||||||
language: string
|
language: string
|
||||||
sql: {
|
sql: {
|
||||||
tables: {
|
tables: SQLiteTables
|
||||||
[tableName: string]: {
|
|
||||||
fields: {
|
|
||||||
[key: string]: SQLiteType | { field: string; type: SQLiteType }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
options: {
|
options: {
|
||||||
table_name: string
|
table_name: string
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,47 +3,63 @@ import { Row, Table } from "../documents"
|
||||||
import { SortType } from "../api"
|
import { SortType } from "../api"
|
||||||
import { Knex } from "knex"
|
import { Knex } from "knex"
|
||||||
|
|
||||||
|
export enum SearchFilterOperator {
|
||||||
|
STRING = "string",
|
||||||
|
FUZZY = "fuzzy",
|
||||||
|
RANGE = "range",
|
||||||
|
EQUAL = "equal",
|
||||||
|
NOT_EQUAL = "notEqual",
|
||||||
|
EMPTY = "empty",
|
||||||
|
NOT_EMPTY = "notEmpty",
|
||||||
|
ONE_OF = "oneOf",
|
||||||
|
CONTAINS = "contains",
|
||||||
|
NOT_CONTAINS = "notContains",
|
||||||
|
CONTAINS_ANY = "containsAny",
|
||||||
|
}
|
||||||
|
|
||||||
export interface SearchFilters {
|
export interface SearchFilters {
|
||||||
allOr?: boolean
|
allOr?: boolean
|
||||||
onEmptyFilter?: EmptyFilterOption
|
onEmptyFilter?: EmptyFilterOption
|
||||||
string?: {
|
[SearchFilterOperator.STRING]?: {
|
||||||
[key: string]: string
|
[key: string]: string
|
||||||
}
|
}
|
||||||
fuzzy?: {
|
[SearchFilterOperator.FUZZY]?: {
|
||||||
[key: string]: string
|
[key: string]: string
|
||||||
}
|
}
|
||||||
range?: {
|
[SearchFilterOperator.RANGE]?: {
|
||||||
[key: string]: {
|
[key: string]: {
|
||||||
high: number | string
|
high: number | string
|
||||||
low: number | string
|
low: number | string
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
equal?: {
|
[SearchFilterOperator.EQUAL]?: {
|
||||||
[key: string]: any
|
[key: string]: any
|
||||||
}
|
}
|
||||||
notEqual?: {
|
[SearchFilterOperator.NOT_EQUAL]?: {
|
||||||
[key: string]: any
|
[key: string]: any
|
||||||
}
|
}
|
||||||
empty?: {
|
[SearchFilterOperator.EMPTY]?: {
|
||||||
[key: string]: any
|
[key: string]: any
|
||||||
}
|
}
|
||||||
notEmpty?: {
|
[SearchFilterOperator.NOT_EMPTY]?: {
|
||||||
[key: string]: any
|
[key: string]: any
|
||||||
}
|
}
|
||||||
oneOf?: {
|
[SearchFilterOperator.ONE_OF]?: {
|
||||||
[key: string]: any[]
|
[key: string]: any[]
|
||||||
}
|
}
|
||||||
contains?: {
|
[SearchFilterOperator.CONTAINS]?: {
|
||||||
[key: string]: any[] | any
|
|
||||||
}
|
|
||||||
notContains?: {
|
|
||||||
[key: string]: any[]
|
[key: string]: any[]
|
||||||
}
|
}
|
||||||
containsAny?: {
|
[SearchFilterOperator.NOT_CONTAINS]?: {
|
||||||
|
[key: string]: any[]
|
||||||
|
}
|
||||||
|
[SearchFilterOperator.CONTAINS_ANY]?: {
|
||||||
[key: string]: any[]
|
[key: string]: any[]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type SearchQueryFields = Omit<SearchFilters, "allOr" | "onEmptyFilter">
|
||||||
|
|
||||||
export interface SortJson {
|
export interface SortJson {
|
||||||
[key: string]: {
|
[key: string]: {
|
||||||
direction: SortDirection
|
direction: SortDirection
|
||||||
|
@ -90,8 +106,8 @@ export interface QueryJson {
|
||||||
paginate?: PaginationJson
|
paginate?: PaginationJson
|
||||||
body?: Row | Row[]
|
body?: Row | Row[]
|
||||||
table?: Table
|
table?: Table
|
||||||
meta?: {
|
meta: {
|
||||||
table?: Table
|
table: Table
|
||||||
tables?: Record<string, Table>
|
tables?: Record<string, Table>
|
||||||
renamed?: RenameColumn
|
renamed?: RenameColumn
|
||||||
}
|
}
|
||||||
|
|
|
@ -704,7 +704,6 @@ describe("scim", () => {
|
||||||
expect(response).toEqual({
|
expect(response).toEqual({
|
||||||
Resources: expect.arrayContaining(
|
Resources: expect.arrayContaining(
|
||||||
groups.map(g => {
|
groups.map(g => {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
||||||
const { members, ...groupData } = g
|
const { members, ...groupData } = g
|
||||||
return groupData
|
return groupData
|
||||||
})
|
})
|
||||||
|
@ -724,7 +723,6 @@ describe("scim", () => {
|
||||||
expect(response).toEqual({
|
expect(response).toEqual({
|
||||||
Resources: expect.arrayContaining(
|
Resources: expect.arrayContaining(
|
||||||
groups.map(g => {
|
groups.map(g => {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
||||||
const { members, displayName, ...groupData } = g
|
const { members, displayName, ...groupData } = g
|
||||||
return groupData
|
return groupData
|
||||||
})
|
})
|
||||||
|
@ -874,7 +872,6 @@ describe("scim", () => {
|
||||||
qs: "excludedAttributes=members",
|
qs: "excludedAttributes=members",
|
||||||
})
|
})
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
||||||
const { members, ...expectedResponse } = group
|
const { members, ...expectedResponse } = group
|
||||||
|
|
||||||
expect(response).toEqual(expectedResponse)
|
expect(response).toEqual(expectedResponse)
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
import { features } from "@budibase/backend-core"
|
import { features } from "@budibase/backend-core"
|
||||||
import env from "./environment"
|
import env from "./environment"
|
||||||
|
|
||||||
// eslint-disable-next-line no-unused-vars
|
|
||||||
enum WorkerFeature {}
|
enum WorkerFeature {}
|
||||||
|
|
||||||
const featureList: WorkerFeature[] = features.processFeatureEnvVar(
|
const featureList: WorkerFeature[] = features.processFeatureEnvVar(
|
||||||
|
|
|
@ -4,7 +4,7 @@ import {
|
||||||
InviteUsersRequest,
|
InviteUsersRequest,
|
||||||
User,
|
User,
|
||||||
CreateAdminUserRequest,
|
CreateAdminUserRequest,
|
||||||
SearchQuery,
|
SearchFilters,
|
||||||
InviteUsersResponse,
|
InviteUsersResponse,
|
||||||
} from "@budibase/types"
|
} from "@budibase/types"
|
||||||
import structures from "../structures"
|
import structures from "../structures"
|
||||||
|
@ -150,7 +150,7 @@ export class UserAPI extends TestAPI {
|
||||||
}
|
}
|
||||||
|
|
||||||
searchUsers = (
|
searchUsers = (
|
||||||
{ query }: { query?: SearchQuery },
|
{ query }: { query?: SearchFilters },
|
||||||
opts?: { status?: number; noHeaders?: boolean }
|
opts?: { status?: number; noHeaders?: boolean }
|
||||||
) => {
|
) => {
|
||||||
const req = this.request
|
const req = this.request
|
||||||
|
|
|
@ -5174,7 +5174,7 @@
|
||||||
dependencies:
|
dependencies:
|
||||||
"@types/node" "*"
|
"@types/node" "*"
|
||||||
|
|
||||||
"@types/archiver@^6.0.2":
|
"@types/archiver@6.0.2":
|
||||||
version "6.0.2"
|
version "6.0.2"
|
||||||
resolved "https://registry.yarnpkg.com/@types/archiver/-/archiver-6.0.2.tgz#0daf8c83359cbde69de1e4b33dcade6a48a929e2"
|
resolved "https://registry.yarnpkg.com/@types/archiver/-/archiver-6.0.2.tgz#0daf8c83359cbde69de1e4b33dcade6a48a929e2"
|
||||||
integrity sha512-KmROQqbQzKGuaAbmK+ZcytkJ51+YqDa7NmbXjmtC5YBLSyQYo21YaUnQ3HbaPFKL1ooo6RQ6OPYPIDyxfpDDXw==
|
integrity sha512-KmROQqbQzKGuaAbmK+ZcytkJ51+YqDa7NmbXjmtC5YBLSyQYo21YaUnQ3HbaPFKL1ooo6RQ6OPYPIDyxfpDDXw==
|
||||||
|
|
Loading…
Reference in New Issue