Merge branch 'master' into fix/conditional-ui-updates

This commit is contained in:
Andrew Kingston 2024-04-19 10:53:39 +01:00 committed by GitHub
commit a049afcf65
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
84 changed files with 1007 additions and 942 deletions

View File

@ -42,7 +42,15 @@
},
"rules": {
"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"
}
},
@ -59,7 +67,15 @@
},
"rules": {
"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/email-domain-example-com": "error",
"no-console": "warn",
@ -89,7 +105,8 @@
{
"varsIgnorePattern": "^_",
"argsIgnorePattern": "^_",
"destructuredArrayIgnorePattern": "^_"
"destructuredArrayIgnorePattern": "^_",
"ignoreRestSiblings": true
}
],
"import/no-relative-packages": "error",

View File

@ -24,5 +24,8 @@
},
"[svelte]": {
"editor.defaultFormatter": "svelte.svelte-vscode"
},
"[handlebars]": {
"editor.formatOnSave": false
}
}

View File

@ -1,5 +1,5 @@
{
"version": "2.23.5",
"version": "2.23.9",
"npmClient": "yarn",
"packages": [
"packages/*",

View File

@ -9,10 +9,7 @@
},
"targetDefaults": {
"build": {
"inputs": [
"{workspaceRoot}/scripts/build.js",
"{workspaceRoot}/lerna.json"
]
"inputs": ["{workspaceRoot}/scripts/*", "{workspaceRoot}/lerna.json"]
}
}
}

View File

@ -59,7 +59,7 @@
"dev:camunda": "./scripts/deploy-camunda.sh",
"dev:all": "yarn run kill-all && lerna run --stream dev",
"dev:built": "yarn run kill-all && cd packages/server && yarn dev:stack:up && cd ../../ && lerna run --stream dev:built",
"dev:docker": "yarn build --scope @budibase/server --scope @budibase/worker && docker-compose -f hosting/docker-compose.build.yaml -f hosting/docker-compose.dev.yaml --env-file hosting/.env up --build --scale proxy-service=0",
"dev:docker": "./scripts/devDocker.sh",
"test": "REUSE_CONTAINERS=1 lerna run --concurrency 1 --stream test --stream",
"lint:eslint": "eslint packages --max-warnings=0",
"lint:prettier": "prettier --check \"packages/**/*.{js,ts,svelte}\" && prettier --write \"examples/**/*.{js,ts,svelte}\"",

@ -1 +1 @@
Subproject commit bd0e01d639ec3b2547e7c859a1c43b622dce8344
Subproject commit eb7d5da233885c5cffd9c255d3e954d0cd39185e

View File

@ -8,19 +8,9 @@ import {
SearchParams,
WithRequired,
} from "@budibase/types"
import { dataFilters } from "@budibase/shared-core"
const QUERY_START_REGEX = /\d[0-9]*:/g
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
}
}
export const removeKeyNumbering = dataFilters.removeKeyNumbering
/**
* Class to build lucene query URLs.

View File

@ -115,7 +115,6 @@ class InMemoryQueue implements Partial<Queue> {
* a JSON message as this is required by Bull.
* @param repeat serves no purpose for the import queue.
*/
// eslint-disable-next-line no-unused-vars
async add(data: any, opts?: JobOptions) {
const jobId = opts?.jobId?.toString()
if (jobId && this._queuedJobIds.has(jobId)) {
@ -166,8 +165,7 @@ class InMemoryQueue implements Partial<Queue> {
return []
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
async removeJobs(pattern: string) {
async removeJobs(_pattern: string) {
// no-op
}

View File

@ -50,6 +50,8 @@ type CreateAdminUserOpts = {
hashPassword?: boolean
requirePassword?: boolean
skipPasswordValidation?: boolean
firstName?: string
lastName?: string
}
type FeatureFns = { isSSOEnforced: FeatureFn; isAppBuildersEnabled: FeatureFn }
@ -517,6 +519,8 @@ export class UserDB {
global: true,
},
tenantId,
firstName: opts?.firstName,
lastName: opts?.lastName,
}
if (opts?.ssoId) {
user.ssoId = opts.ssoId

View File

@ -17,8 +17,8 @@ import {
ContextUser,
CouchFindOptions,
DatabaseQueryOpts,
SearchQuery,
SearchQueryOperators,
SearchFilters,
SearchFilterOperator,
SearchUsersRequest,
User,
} from "@budibase/types"
@ -44,11 +44,11 @@ function removeUserPassword(users: User | User[]) {
return users
}
export function isSupportedUserSearch(query: SearchQuery) {
export function isSupportedUserSearch(query: SearchFilters) {
const allowed = [
{ op: SearchQueryOperators.STRING, key: "email" },
{ op: SearchQueryOperators.EQUAL, key: "_id" },
{ op: SearchQueryOperators.ONE_OF, key: "_id" },
{ op: SearchFilterOperator.STRING, key: "email" },
{ op: SearchFilterOperator.EQUAL, key: "_id" },
{ op: SearchFilterOperator.ONE_OF, key: "_id" },
]
for (let [key, operation] of Object.entries(query)) {
if (typeof operation !== "object") {

View File

@ -14,6 +14,7 @@
notifications,
Checkbox,
DatePicker,
DrawerContent,
} from "@budibase/bbui"
import CreateWebhookModal from "components/automation/Shared/CreateWebhookModal.svelte"
import { automationStore, selectedAutomation, tables } from "stores/builder"
@ -37,7 +38,7 @@
hbAutocomplete,
EditorModes,
} 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 {
getSchemaForDatasourcePlus,
@ -442,15 +443,16 @@
<Button cta slot="buttons" on:click={() => saveFilters(key)}>
Save
</Button>
<FilterDrawer
slot="body"
{filters}
{bindings}
{schemaFields}
datasource={{ type: "table", tableId }}
panel={AutomationBindingPanel}
on:change={e => (tempFilters = e.detail)}
/>
<DrawerContent slot="body">
<FilterBuilder
{filters}
{bindings}
{schemaFields}
datasource={{ type: "table", tableId }}
panel={AutomationBindingPanel}
on:change={e => (tempFilters = e.detail)}
/>
</DrawerContent>
</Drawer>
{:else if value.customType === "password"}
<Input

View File

@ -1,7 +1,7 @@
<script>
import { createEventDispatcher } from "svelte"
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 filters
@ -40,7 +40,7 @@
onConfirm={() => dispatch("change", tempValue)}
>
<div class="wrapper">
<FilterDrawer
<FilterBuilder
allowBindings={false}
{filters}
{schemaFields}

View File

@ -13,6 +13,7 @@
Layout,
AbsTooltip,
} from "@budibase/bbui"
import { SWITCHABLE_TYPES, ValidColumnNameRegex } from "@budibase/shared-core"
import { createEventDispatcher, getContext, onMount } from "svelte"
import { cloneDeep } from "lodash/fp"
import { tables, datasources } from "stores/builder"
@ -20,11 +21,6 @@
import {
FIELDS,
RelationshipType,
ALLOWABLE_STRING_OPTIONS,
ALLOWABLE_NUMBER_OPTIONS,
ALLOWABLE_STRING_TYPES,
ALLOWABLE_NUMBER_TYPES,
SWITCHABLE_TYPES,
PrettyRelationshipDefinitions,
DB_TYPE_EXTERNAL,
} from "constants/backend"
@ -33,21 +29,20 @@
import ModalBindableInput from "components/common/bindings/ModalBindableInput.svelte"
import { getBindings } from "components/backend/DataTable/formula"
import JSONSchemaModal from "./JSONSchemaModal.svelte"
import { ValidColumnNameRegex } from "@budibase/shared-core"
import { FieldType, FieldSubtype, SourceName } from "@budibase/types"
import RelationshipSelector from "components/common/RelationshipSelector.svelte"
import { RowUtils } from "@budibase/frontend-core"
import ServerBindingPanel from "components/common/bindings/ServerBindingPanel.svelte"
const AUTO_TYPE = FIELDS.AUTO.type
const FORMULA_TYPE = FIELDS.FORMULA.type
const LINK_TYPE = FIELDS.LINK.type
const STRING_TYPE = FIELDS.STRING.type
const NUMBER_TYPE = FIELDS.NUMBER.type
const JSON_TYPE = FIELDS.JSON.type
const DATE_TYPE = FIELDS.DATETIME.type
const USER_TYPE = FIELDS.USER.subtype
const USERS_TYPE = FIELDS.USERS.subtype
const AUTO_TYPE = FieldType.AUTO
const FORMULA_TYPE = FieldType.FORMULA
const LINK_TYPE = FieldType.LINK
const STRING_TYPE = FieldType.STRING
const NUMBER_TYPE = FieldType.NUMBER
const JSON_TYPE = FieldType.JSON
const DATE_TYPE = FieldType.DATETIME
const USER_TYPE = FieldSubtype.USER
const USERS_TYPE = FieldSubtype.USERS
const dispatch = createEventDispatcher()
const PROHIBITED_COLUMN_NAMES = ["type", "_id", "_rev", "tableId"]
@ -61,8 +56,8 @@
let primaryDisplay
let indexes = [...($tables.selected.indexes || [])]
let isCreating = undefined
let relationshipPart1 = PrettyRelationshipDefinitions.Many
let relationshipPart2 = PrettyRelationshipDefinitions.One
let relationshipPart1 = PrettyRelationshipDefinitions.MANY
let relationshipPart2 = PrettyRelationshipDefinitions.ONE
let relationshipTableIdPrimary = null
let relationshipTableIdSecondary = null
let table = $tables.selected
@ -175,7 +170,7 @@
$: typeEnabled =
!originalName ||
(originalName &&
SWITCHABLE_TYPES.indexOf(editableColumn.type) !== -1 &&
SWITCHABLE_TYPES[field.type] &&
!editableColumn?.autocolumn)
const fieldDefinitions = Object.values(FIELDS).reduce(
@ -367,16 +362,15 @@
}
function getAllowedTypes() {
if (
originalName &&
ALLOWABLE_STRING_TYPES.indexOf(editableColumn.type) !== -1
) {
return ALLOWABLE_STRING_OPTIONS
} else if (
originalName &&
ALLOWABLE_NUMBER_TYPES.indexOf(editableColumn.type) !== -1
) {
return ALLOWABLE_NUMBER_OPTIONS
if (originalName) {
const possibleTypes = (
SWITCHABLE_TYPES[field.type] || [editableColumn.type]
).map(t => t.toLowerCase())
return Object.entries(FIELDS)
.filter(([fieldType]) =>
possibleTypes.includes(fieldType.toLowerCase())
)
.map(([_, fieldDefinition]) => fieldDefinition)
}
const isUsers =
@ -632,7 +626,7 @@
/>
</div>
</div>
{:else if editableColumn.type === FieldType.LINK}
{:else if editableColumn.type === FieldType.LINK && !editableColumn.autocolumn}
<RelationshipSelector
bind:relationshipPart1
bind:relationshipPart2

View File

@ -27,14 +27,6 @@
return []
}
}
async function deleteAttachments(fileList) {
try {
return await API.deleteBuilderAttachments(fileList)
} catch (error) {
return []
}
}
</script>
<Dropzone
@ -42,6 +34,5 @@
{label}
{...$$restProps}
{processFiles}
{deleteAttachments}
{handleFileTooLarge}
/>

View File

@ -9,7 +9,6 @@
"",
requiredValidator
)
// eslint-disable-next-line no-unused-vars
const [repeatPassword, _, repeatTouched] = createValidationStore(
"",
requiredValidator

View File

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

View File

@ -1,8 +1,14 @@
<script>
import { notifications, ActionButton, Button, Drawer } from "@budibase/bbui"
import {
notifications,
ActionButton,
Button,
Drawer,
DrawerContent,
} from "@budibase/bbui"
import { createEventDispatcher } from "svelte"
import { getDatasourceForProvider, getSchemaForDatasource } from "dataBinding"
import FilterDrawer from "./FilterDrawer.svelte"
import FilterBuilder from "./FilterBuilder.svelte"
import { selectedScreen } from "stores/builder"
const dispatch = createEventDispatcher()
@ -40,14 +46,15 @@
</div>
<Drawer bind:this={drawer} title="Filtering" on:drawerHide on:drawerShow>
<Button cta slot="buttons" on:click={saveFilter}>Save</Button>
<FilterDrawer
slot="body"
filters={value}
{bindings}
{schemaFields}
{datasource}
on:change={e => (tempValue = e.detail)}
/>
<DrawerContent slot="body">
<FilterBuilder
filters={value}
{bindings}
{schemaFields}
{datasource}
on:change={e => (tempValue = e.detail)}
/>
</DrawerContent>
</Drawer>
<style>

View File

@ -202,26 +202,6 @@ export const PrettyRelationshipDefinitions = {
ONE: "One row",
}
export const ALLOWABLE_STRING_OPTIONS = [
FIELDS.STRING,
FIELDS.OPTIONS,
FIELDS.LONGFORM,
FIELDS.BARCODEQR,
]
export const ALLOWABLE_STRING_TYPES = ALLOWABLE_STRING_OPTIONS.map(
opt => opt.type
)
export const ALLOWABLE_NUMBER_OPTIONS = [FIELDS.NUMBER, FIELDS.BOOLEAN]
export const ALLOWABLE_NUMBER_TYPES = ALLOWABLE_NUMBER_OPTIONS.map(
opt => opt.type
)
export const SWITCHABLE_TYPES = [
...ALLOWABLE_STRING_TYPES,
...ALLOWABLE_NUMBER_TYPES,
]
export const BUDIBASE_INTERNAL_DB_ID = INTERNAL_TABLE_SOURCE_ID
export const DEFAULT_BB_DATASOURCE_ID = "datasource_internal_bb_default"
export const BUDIBASE_DATASOURCE_TYPE = "budibase"

View File

@ -4,8 +4,6 @@ import {
createDatasourceCreationStore,
} from "./datasourceCreation"
import { get } from "svelte/store"
// eslint-disable-next-line no-unused-vars
import { shouldIntegrationFetchTableNames } from "stores/selectors"
vi.mock("stores/selectors", () => ({
shouldIntegrationFetchTableNames: vi.fn(),

View File

@ -1,9 +1,9 @@
import { it, expect, describe, beforeEach, vi } from "vitest"
import { createOnGoogleAuthStore } from "./onGoogleAuth"
import { writable, get } from "svelte/store"
// eslint-disable-next-line no-unused-vars
// eslint-disable-next-line
import { params } from "@roxi/routify"
// eslint-disable-next-line no-unused-vars
// eslint-disable-next-line
import { integrations } from "stores/builder"
import { IntegrationTypes } from "constants/backend"

View File

@ -1,8 +1,8 @@
import { FieldType } from "@budibase/types"
import { SWITCHABLE_TYPES } from "@budibase/shared-core"
import { get, writable, derived } from "svelte/store"
import { cloneDeep } from "lodash/fp"
import { API } from "api"
import { SWITCHABLE_TYPES } from "constants/backend"
export function createTablesStore() {
const store = writable({
@ -64,7 +64,7 @@ export function createTablesStore() {
if (
oldField != null &&
oldField?.type !== field.type &&
SWITCHABLE_TYPES.indexOf(oldField?.type) === -1
!SWITCHABLE_TYPES[oldField?.type]?.includes(field.type)
) {
updatedTable.schema[key] = oldField
}
@ -148,12 +148,6 @@ export function createTablesStore() {
if (indexes) {
draft.indexes = indexes
}
// Add object to indicate if column is being added
if (draft.schema[field.name] === undefined) {
draft._add = {
name: field.name,
}
}
draft.schema = {
...draft.schema,
[field.name]: cloneDeep(field),

View File

@ -105,7 +105,6 @@ export function getAppService(path: string) {
}
export function updateDockerComposeService(
// eslint-disable-next-line no-unused-vars
updateFn: (service: DockerCompose) => void
) {
const opts = ["docker-compose.yaml", "docker-compose.yml"]

View File

@ -1,216 +1,14 @@
<script>
import {
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"
import { FilterBuilder } from "@budibase/frontend-core"
export let schemaFields
export let filters = []
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>
<div class="container" class:mobile={$context.device.mobile}>
<Layout noPadding>
<Body size="S">
{#if !filters?.length}
Add your first filter expression.
{:else}
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>
<FilterBuilder bind:filters {schemaFields} {datasource} filtersLabel={null}>
<div slot="filtering-hero-content">
Results are filtered to only those which match all of the following
constraints.
</div>
</FilterBuilder>

View File

@ -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 value = fieldApiMapper.set(e.detail)
const changed = fieldApi.setValue(value)
@ -98,7 +87,6 @@
error={fieldState.error}
on:change={handleChange}
{processFiles}
{deleteAttachments}
{handleFileTooLarge}
{handleTooManyFiles}
{maximum}

View File

@ -11,6 +11,7 @@
"@budibase/types": "0.0.0",
"dayjs": "^1.10.8",
"lodash": "4.17.21",
"shortid": "2.2.15",
"socket.io-client": "^4.6.1"
}
}

View File

@ -61,34 +61,6 @@ export const buildAttachmentEndpoints = API => {
})
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.
* @param datasourceId the ID of the datasource to download from

View File

@ -4,33 +4,36 @@
Button,
Combobox,
DatePicker,
DrawerContent,
Icon,
Input,
Label,
Layout,
Multiselect,
Select,
Label,
Multiselect,
} from "@budibase/bbui"
import DrawerBindableInput from "components/common/bindings/DrawerBindableInput.svelte"
import ClientBindingPanel from "components/common/bindings/ClientBindingPanel.svelte"
import { FieldType, SearchFilterOperator } from "@budibase/types"
import { generate } from "shortid"
import { Constants, LuceneUtils } from "@budibase/frontend-core"
import { getFields } from "helpers/searchFields"
import { FieldType } from "@budibase/types"
import { createEventDispatcher, onMount } from "svelte"
import { LuceneUtils, Constants } from "@budibase/frontend-core"
import { getContext } from "svelte"
import FilterUsers from "./FilterUsers.svelte"
const { OperatorOptions } = Constants
export let schemaFields
export let filters = []
export let bindings = []
export let panel = ClientBindingPanel
export let allowBindings = true
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 = [
{ value: "and", label: "Match all filters" },
{ value: "or", label: "Match any filter" },
@ -40,62 +43,18 @@
{ value: "none", label: "Return no rows" },
]
let rawFilters
let matchAny = false
let onEmptyFilter = "all"
const context = getContext("context")
$: parseFilters(filters)
$: dispatch("change", enrichFilters(rawFilters, matchAny, onEmptyFilter))
$: enrichedSchemaFields = getFields(schemaFields || [], { allowLinks: true })
$: fieldOptions = enrichedSchemaFields.map(field => field.name) || []
$: valueTypeOptions = allowBindings ? ["Value", "Binding"] : ["Value"]
// 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 }])
}
$: fieldOptions = (schemaFields ?? [])
.filter(field => getValidOperatorsForType(field).length)
.map(field => ({
label: field.displayName || field.name,
value: field.name,
}))
const addFilter = () => {
rawFilters = [
...rawFilters,
filters = [
...(filters || []),
{
id: generate(),
field: null,
@ -107,22 +66,57 @@
}
const removeFilter = id => {
rawFilters = rawFilters.filter(field => field.id !== id)
filters = filters.filter(field => field.id !== 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() }
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 => {
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 => {
// 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.subtype = fieldSchema?.subtype
@ -154,88 +148,79 @@
// Ensure array values are properly set and cleared
if (Array.isArray(filter.value)) {
if (filter.valueType !== "Value" || filter.type !== "array") {
if (filter.valueType !== "Value" || filter.type !== FieldType.ARRAY) {
filter.value = null
}
} else if (filter.type === "array" && filter.valueType === "Value") {
} else if (
filter.type === FieldType.ARRAY &&
filter.valueType === "Value"
) {
filter.value = []
} else if (
previousType !== filter.type &&
(previousType === FieldType.BB_REFERENCE ||
filter.type === FieldType.BB_REFERENCE)
) {
filter.value = filter.type === "array" ? [] : null
filter.value = filter.type === FieldType.ARRAY ? [] : null
}
}
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 = enrichedSchemaFields.find(x => x.name === field)
return schema?.constraints?.inclusion || []
}
const getValidOperatorsForType = filter => {
if (!filter?.field) {
return []
function handleAllOr(option) {
filters = filters.filter(f => f.operator !== "allOr")
if (option === "or") {
filters.push({ operator: "allOr" })
}
}
return LuceneUtils.getValidOperatorsForType(
{ type: filter.type, subtype: filter.subtype },
filter.field,
datasource
)
function handleOnEmptyFilter(value) {
filters = filters?.filter(filter => !filter.onEmptyFilter)
filters.push({ onEmptyFilter: value })
}
</script>
<DrawerContent>
<div class="container">
<Layout noPadding>
{#if !rawFilters?.length}
<Body size="S">Add your first filter expression.</Body>
{:else}
<div class="fields">
<Select
label="Behaviour"
value={matchAny ? "or" : "and"}
options={behaviourOptions}
getOptionLabel={opt => opt.label}
getOptionValue={opt => opt.value}
on:change={e => (matchAny = e.detail === "or")}
placeholder={null}
/>
{#if datasource?.type === "table"}
<Select
label="When filter empty"
value={onEmptyFilter}
options={onEmptyOptions}
getOptionLabel={opt => opt.label}
getOptionValue={opt => opt.value}
on:change={e => (onEmptyFilter = e.detail)}
placeholder={null}
/>
<div class="container" class:mobile={$context?.device?.mobile}>
<Layout noPadding>
{#if fieldOptions?.length}
<Body size="S">
{#if !fieldFilters?.length}
Add your first filter expression.
{:else}
<slot name="filtering-hero-content" />
{#if behaviourFilters}
<div class="behaviour-filters">
<Select
label="Behaviour"
value={matchAny ? "or" : "and"}
options={behaviourOptions}
getOptionLabel={opt => opt.label}
getOptionValue={opt => opt.value}
on:change={e => handleAllOr(e.detail)}
placeholder={null}
/>
{#if datasource?.type === "table"}
<Select
label="When filter empty"
value={onEmptyFilter}
options={onEmptyOptions}
getOptionLabel={opt => opt.label}
getOptionValue={opt => opt.value}
on:change={e => handleOnEmptyFilter(e.detail)}
placeholder={null}
/>
{/if}
</div>
{/if}
</div>
{/if}
</Body>
{#if fieldFilters?.length}
<div>
<div class="filter-label">
<Label>Filters</Label>
</div>
<div class="fields">
{#each rawFilters as filter}
{#if filtersLabel}
<div class="filter-label">
<Label>{filtersLabel}</Label>
</div>
{/if}
<div class="fields" class:with-bindings={allowBindings}>
{#each fieldFilters as filter}
<Select
bind:value={filter.field}
options={fieldOptions}
@ -249,38 +234,32 @@
on:change={() => onOperatorChange(filter)}
placeholder={null}
/>
<Select
disabled={filter.noValue || !filter.field}
options={valueTypeOptions}
bind:value={filter.valueType}
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)}
{#if allowBindings}
<Select
disabled={filter.noValue || !filter.field}
options={valueTypeOptions}
bind:value={filter.valueType}
on:change={() => onValueTypeChange(filter)}
placeholder={null}
/>
{: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} />
{: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
disabled={filter.noValue}
options={getFieldOptions(filter.field)}
bind:value={filter.value}
/>
{:else if filter.type === "options"}
{:else if filter.type === FieldType.OPTIONS}
<Combobox
disabled={filter.noValue}
options={getFieldOptions(filter.field)}
bind:value={filter.value}
/>
{:else if filter.type === "boolean"}
{:else if filter.type === FieldType.BOOLEAN}
<Combobox
disabled={filter.noValue}
options={[
@ -289,7 +268,7 @@
]}
bind:value={filter.value}
/>
{:else if filter.type === "datetime"}
{:else if filter.type === FieldType.DATETIME}
<DatePicker
disabled={filter.noValue}
enableTime={!getSchema(filter)?.dateOnly}
@ -306,32 +285,36 @@
disabled={filter.noValue}
/>
{:else}
<DrawerBindableInput disabled />
<Input disabled />
{/if}
<Icon
name="Duplicate"
hoverable
size="S"
on:click={() => duplicateFilter(filter.id)}
/>
<Icon
name="Close"
hoverable
size="S"
on:click={() => removeFilter(filter.id)}
/>
<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>
</div>
{/if}
<div class="bottom">
<div>
<Button icon="AddCircle" size="M" secondary on:click={addFilter}>
Add filter
</Button>
</div>
</Layout>
</div>
</DrawerContent>
{:else}
<Body size="S">None of the table column can be used for filtering.</Body>
{/if}
</Layout>
</div>
<style>
.container {
@ -339,22 +322,42 @@
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;
}
.fields.with-bindings {
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 {
margin-bottom: var(--spacing-s);
}
.bottom {
display: flex;
justify-content: space-between;
.behaviour-filters {
display: grid;
column-gap: var(--spacing-l);
row-gap: var(--spacing-s);
align-items: center;
grid-template-columns: minmax(150px, 1fr) 170px 120px minmax(150px, 1fr) 16px 16px;
}
</style>

View File

@ -1,9 +1,9 @@
<script>
import { Select, Multiselect } from "@budibase/bbui"
import { fetchData } from "@budibase/frontend-core"
import { createAPIClient } from "../api"
import { API } from "api"
export let API = createAPIClient()
export let value = null
export let disabled
export let multiselect = false

View File

@ -61,14 +61,6 @@
}
}
const deleteAttachments = async fileList => {
try {
return await API.deleteBuilderAttachments(fileList)
} catch (error) {
return []
}
}
onMount(() => {
api = {
focus: () => open(),
@ -101,7 +93,6 @@
on:change={e => onChange(e.detail)}
maximum={maximum || schema.constraints?.length?.maximum}
{processFiles}
{deleteAttachments}
{handleFileTooLarge}
/>
</div>

View File

@ -6,3 +6,4 @@ export { default as UserAvatars } from "./UserAvatars.svelte"
export { default as Updating } from "./Updating.svelte"
export { Grid } from "./grid"
export { default as ClientAppSkeleton } from "./ClientAppSkeleton.svelte"
export { default as FilterBuilder } from "./FilterBuilder.svelte"

View File

@ -348,8 +348,7 @@ export default class DataFetch {
* Determine the feature flag for this datasource definition
* @param definition
*/
// eslint-disable-next-line no-unused-vars
determineFeatureFlags(definition) {
determineFeatureFlags(_definition) {
return {
supportsSearch: false,
supportsSort: false,

@ -1 +1 @@
Subproject commit ef186d00241f96037f9fd34d7a3826041977ab3a
Subproject commit 06b1064f7e2f7cac5d4bef2ee999796a2a1f0f2c

View File

@ -4,6 +4,7 @@ services:
# user: sa
# database: master
mssql:
# platform: linux/amd64
image: bb/mssql
build:
context: .

View File

@ -119,6 +119,9 @@ async function removeManyToManyRelationships(
endpoint: getEndpoint(tableId, Operation.DELETE),
body: { [colName]: null },
filters,
meta: {
table,
},
})
} else {
return []
@ -133,6 +136,9 @@ async function removeOneToManyRelationships(rowId: string, table: Table) {
return getDatasourceAndQuery({
endpoint: getEndpoint(tableId, Operation.UPDATE),
filters,
meta: {
table,
},
})
} else {
return []
@ -248,6 +254,9 @@ export class ExternalRequest<T extends Operation> {
const response = await getDatasourceAndQuery({
endpoint: getEndpoint(table._id!, Operation.READ),
filters: buildFilters(rowId, {}, table),
meta: {
table,
},
})
if (Array.isArray(response) && response.length > 0) {
return response[0]
@ -365,36 +374,45 @@ export class ExternalRequest<T extends Operation> {
) {
continue
}
let tableId: string | undefined,
let relatedTableId: string | undefined,
lookupField: string | undefined,
fieldName: string | undefined
if (isManyToMany(field)) {
tableId = field.through
relatedTableId = field.through
lookupField = primaryKey
fieldName = field.throughTo || primaryKey
} else if (isManyToOne(field)) {
tableId = field.tableId
relatedTableId = field.tableId
lookupField = field.foreignKey
fieldName = field.fieldName
}
if (!tableId || !lookupField || !fieldName) {
if (!relatedTableId || !lookupField || !fieldName) {
throw new Error(
"Unable to lookup relationships - undefined column properties."
)
}
const { tableName: relatedTableName } = breakExternalTableId(tableId)
const { tableName: relatedTableName } =
breakExternalTableId(relatedTableId)
// @ts-ignore
const linkPrimaryKey = this.tables[relatedTableName].primary[0]
if (!lookupField || !row[lookupField]) {
continue
}
const endpoint = getEndpoint(relatedTableId, Operation.READ)
const relatedTable = this.tables[endpoint.entityId]
if (!relatedTable) {
throw new Error("unable to find related table")
}
const response = await getDatasourceAndQuery({
endpoint: getEndpoint(tableId, Operation.READ),
endpoint: endpoint,
filters: {
equal: {
[fieldName]: row[lookupField],
},
},
meta: {
table: relatedTable,
},
})
// this is the response from knex if no rows found
const rows: Row[] =
@ -402,7 +420,11 @@ export class ExternalRequest<T extends Operation> {
const storeTo = isManyToMany(field)
? field.throughFrom || linkPrimaryKey
: fieldName
related[storeTo] = { rows, isMany: isManyToMany(field), tableId }
related[storeTo] = {
rows,
isMany: isManyToMany(field),
tableId: relatedTableId,
}
}
return related
}
@ -470,6 +492,9 @@ export class ExternalRequest<T extends Operation> {
// if we're doing many relationships then we're writing, only one response
body,
filters: buildFilters(id, {}, linkTable),
meta: {
table: linkTable,
},
})
)
} else {

View File

@ -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 appId = context.getAppId()
if (!appId) {

View File

@ -22,6 +22,7 @@ export async function makeTableRequest(
operation,
},
meta: {
table,
tables,
},
table,

View File

@ -31,7 +31,6 @@ export async function save(
renaming?: RenameColumn
) {
const inputs = ctx.request.body
const adding = inputs?._add
// can't do this right now
delete inputs.rows
const tableId = ctx.request.body._id
@ -44,7 +43,7 @@ export async function save(
const { datasource, table } = await sdk.tables.external.save(
datasourceId!,
inputs,
{ tableId, renaming, adding }
{ tableId, renaming }
)
builderSocket?.emitDatasourceUpdate(ctx, datasource)
return table

View File

@ -77,11 +77,6 @@ export async function save(ctx: UserCtx<SaveTableRequest, SaveTableResponse>) {
const renaming = ctx.request.body._rename
const api = pickApi({ table })
// do not pass _rename or _add if saving to CouchDB
if (api === internal) {
delete ctx.request.body._add
delete ctx.request.body._rename
}
let savedTable = await api.save(ctx, renaming)
if (!table._id) {
savedTable = sdk.tables.enrichViewSchemas(savedTable)

View File

@ -16,7 +16,7 @@ export async function save(
ctx: UserCtx<SaveTableRequest, SaveTableResponse>,
renaming?: RenameColumn
) {
const { rows, ...rest } = ctx.request.body
const { _rename, rows, ...rest } = ctx.request.body
let tableToSave: Table = {
_id: generateTableID(),
...rest,

View File

@ -32,11 +32,6 @@ router
.get("/builder/:file*", controller.serveBuilder)
.get("/api/assets/client", controller.serveClientLibrary)
.post("/api/attachments/process", authorized(BUILDER), controller.uploadFile)
.post(
"/api/attachments/delete",
authorized(BUILDER),
controller.deleteObjects
)
.post("/api/beta/:feature", controller.toggleBetaUiFeature)
.post(
"/api/attachments/:tableId/upload",
@ -44,12 +39,6 @@ router
authorized(PermissionType.TABLE, PermissionLevel.WRITE),
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/:appUrl/:path*", controller.serveApp)
.get("/:appId/:path*", controller.serveApp)

View File

@ -4,6 +4,7 @@ import {
Query,
QueryPreview,
SourceName,
TableSourceType,
} from "@budibase/types"
import * as setup from "../utilities"
import {
@ -740,12 +741,25 @@ describe.each(
})
describe("query through datasource", () => {
it("should be able to query a pg datasource", async () => {
it("should be able to query the datasource", async () => {
const entityId = "test_table"
await config.api.datasource.update({
...datasource,
entities: {
[entityId]: {
name: entityId,
schema: {},
type: "table",
sourceId: datasource._id!,
sourceType: TableSourceType.EXTERNAL,
},
},
})
const res = await config.api.datasource.query({
endpoint: {
datasourceId: datasource._id!,
operation: Operation.READ,
entityId: "test_table",
entityId,
},
resource: {
fields: ["id", "name"],

View File

@ -26,6 +26,7 @@ describe.each([
[DatabaseName.MARIADB, getDatasource(DatabaseName.MARIADB)],
])("/api/:sourceId/search (%s)", (name, dsProvider) => {
const isSqs = name === "internal-sqs"
const isInternal = name === "internal"
const config = setup.getConfig()
let envCleanup: (() => void) | undefined
@ -336,6 +337,20 @@ describe.each([
expectQuery({
range: { age: { low: 5, high: 9 } },
}).toFindNothing())
// We never implemented half-open ranges in Lucene.
!isInternal &&
it("can search using just a low value", () =>
expectQuery({
range: { age: { low: 5 } },
}).toContainExactly([{ age: 10 }]))
// We never implemented half-open ranges in Lucene.
!isInternal &&
it("can search using just a high value", () =>
expectQuery({
range: { age: { high: 5 } },
}).toContainExactly([{ age: 1 }]))
})
describe("sort", () => {
@ -440,6 +455,20 @@ describe.each([
expectQuery({
range: { dob: { low: JAN_5TH, high: JAN_9TH } },
}).toFindNothing())
// We never implemented half-open ranges in Lucene.
!isInternal &&
it("can search using just a low value", () =>
expectQuery({
range: { dob: { low: JAN_5TH } },
}).toContainExactly([{ dob: JAN_10TH }]))
// We never implemented half-open ranges in Lucene.
!isInternal &&
it("can search using just a high value", () =>
expectQuery({
range: { dob: { high: JAN_5TH } },
}).toContainExactly([{ dob: JAN_1ST }]))
})
describe("sort", () => {
@ -550,4 +579,100 @@ describe.each([
]))
})
})
describe("bigints", () => {
const SMALL = "1"
const MEDIUM = "10000000"
// Our bigints are int64s in most datasources.
const BIG = "9223372036854775807"
beforeAll(async () => {
await createTable({
num: { name: "num", type: FieldType.BIGINT },
})
await createRows([{ num: SMALL }, { num: MEDIUM }, { num: BIG }])
})
describe("equal", () => {
it("successfully finds a row", () =>
expectQuery({ equal: { num: SMALL } }).toContainExactly([
{ num: SMALL },
]))
it("successfully finds a big value", () =>
expectQuery({ equal: { num: BIG } }).toContainExactly([{ num: BIG }]))
it("fails to find nonexistent row", () =>
expectQuery({ equal: { num: "2" } }).toFindNothing())
})
describe("notEqual", () => {
it("successfully finds a row", () =>
expectQuery({ notEqual: { num: SMALL } }).toContainExactly([
{ num: MEDIUM },
{ num: BIG },
]))
it("fails to find nonexistent row", () =>
expectQuery({ notEqual: { num: 10 } }).toContainExactly([
{ num: SMALL },
{ num: MEDIUM },
{ num: BIG },
]))
})
describe("oneOf", () => {
it("successfully finds a row", () =>
expectQuery({ oneOf: { num: [SMALL] } }).toContainExactly([
{ num: SMALL },
]))
it("successfully finds all rows", () =>
expectQuery({ oneOf: { num: [SMALL, MEDIUM, BIG] } }).toContainExactly([
{ num: SMALL },
{ num: MEDIUM },
{ num: BIG },
]))
it("fails to find nonexistent row", () =>
expectQuery({ oneOf: { num: [2] } }).toFindNothing())
})
// Range searches against bigints don't seem to work at all in Lucene, and I
// couldn't figure out why. Given that we're replacing Lucene with SQS,
// we've decided not to spend time on it.
!isInternal &&
describe("range", () => {
it("successfully finds a row", () =>
expectQuery({
range: { num: { low: SMALL, high: "5" } },
}).toContainExactly([{ num: SMALL }]))
it("successfully finds multiple rows", () =>
expectQuery({
range: { num: { low: SMALL, high: MEDIUM } },
}).toContainExactly([{ num: SMALL }, { num: MEDIUM }]))
it("successfully finds a row with a high bound", () =>
expectQuery({
range: { num: { low: MEDIUM, high: BIG } },
}).toContainExactly([{ num: MEDIUM }, { num: BIG }]))
it("successfully finds no rows", () =>
expectQuery({
range: { num: { low: "5", high: "5" } },
}).toFindNothing())
it("can search using just a low value", () =>
expectQuery({
range: { num: { low: MEDIUM } },
}).toContainExactly([{ num: MEDIUM }, { num: BIG }]))
it("can search using just a high value", () =>
expectQuery({
range: { num: { high: MEDIUM } },
}).toContainExactly([{ num: SMALL }, { num: MEDIUM }]))
})
})
})

View File

@ -219,9 +219,6 @@ describe.each([
it("should add a new column for an internal DB table", async () => {
const saveTableRequest: SaveTableRequest = {
_add: {
name: "NEW_COLUMN",
},
...basicTable(),
}
@ -235,7 +232,6 @@ describe.each([
updatedAt: expect.stringMatching(ISO_REGEX_PATTERN),
views: {},
}
delete expectedResponse._add
expect(response).toEqual(expectedResponse)
})
})

View File

@ -8,7 +8,7 @@ import {
PermissionLevel,
QuotaUsageType,
SaveTableRequest,
SearchQueryOperators,
SearchFilterOperator,
SortOrder,
SortType,
StaticQuotaName,
@ -132,7 +132,7 @@ describe.each([
primaryDisplay: generator.word(),
query: [
{
operator: SearchQueryOperators.EQUAL,
operator: SearchFilterOperator.EQUAL,
field: "field",
value: "value",
},
@ -236,7 +236,7 @@ describe.each([
...view,
query: [
{
operator: SearchQueryOperators.EQUAL,
operator: SearchFilterOperator.EQUAL,
field: "newField",
value: "thatValue",
},
@ -263,7 +263,7 @@ describe.each([
primaryDisplay: generator.word(),
query: [
{
operator: SearchQueryOperators.EQUAL,
operator: SearchFilterOperator.EQUAL,
field: generator.word(),
value: generator.word(),
},
@ -341,7 +341,7 @@ describe.each([
tableId: generator.guid(),
query: [
{
operator: SearchQueryOperators.EQUAL,
operator: SearchFilterOperator.EQUAL,
field: "newField",
value: "thatValue",
},
@ -671,7 +671,7 @@ describe.each([
name: generator.guid(),
query: [
{
operator: SearchQueryOperators.EQUAL,
operator: SearchFilterOperator.EQUAL,
field: "two",
value: "bar2",
},

View File

@ -62,7 +62,6 @@ export const definition: AutomationStepSchema = {
}
export async function run({ inputs }: AutomationStepInput) {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { automationId, ...fieldParams } = inputs.automation
if (await features.isTriggerAutomationRunEnabled()) {

View File

@ -1,11 +1,8 @@
import { features } from "@budibase/backend-core"
import env from "./environment"
// eslint-disable-next-line no-unused-vars
enum AppFeature {
// eslint-disable-next-line no-unused-vars
API = "api",
// eslint-disable-next-line no-unused-vars
AUTOMATIONS = "automations",
}

View File

@ -16,7 +16,6 @@ import {
getDatasource,
rawQuery,
} from "../integrations/tests/utils"
import { builderSocket } from "../websockets"
import { generator } from "@budibase/backend-core/tests"
// @ts-ignore
fetch.mockSearch()
@ -233,72 +232,6 @@ describe("mysql integrations", () => {
})
describe("POST /api/tables/", () => {
const emitDatasourceUpdateMock = jest.fn()
it("will emit the datasource entity schema with externalType to the front-end when adding a new column", async () => {
const addColumnToTable: TableRequest = {
type: "table",
sourceType: TableSourceType.EXTERNAL,
name: uniqueTableName(),
sourceId: datasource._id!,
primary: ["id"],
schema: {
id: {
type: FieldType.AUTO,
name: "id",
autocolumn: true,
},
new_column: {
type: FieldType.NUMBER,
name: "new_column",
},
},
_add: {
name: "new_column",
},
}
jest
.spyOn(builderSocket!, "emitDatasourceUpdate")
.mockImplementation(emitDatasourceUpdateMock)
await makeRequest("post", "/api/tables/", addColumnToTable)
const expectedTable: TableRequest = {
...addColumnToTable,
schema: {
id: {
type: FieldType.NUMBER,
name: "id",
autocolumn: true,
constraints: {
presence: false,
},
externalType: "int unsigned",
},
new_column: {
type: FieldType.NUMBER,
name: "new_column",
autocolumn: false,
constraints: {
presence: false,
},
externalType: "float(8,2)",
},
},
created: true,
_id: `${datasource._id}__${addColumnToTable.name}`,
}
delete expectedTable._add
expect(emitDatasourceUpdateMock).toHaveBeenCalledTimes(1)
const emittedDatasource: Datasource =
emitDatasourceUpdateMock.mock.calls[0][1]
expect(emittedDatasource.entities![expectedTable.name]).toEqual(
expectedTable
)
})
it("will rename a column", async () => {
await makeRequest("post", "/api/tables/", primaryMySqlTable)

View File

@ -2,6 +2,7 @@ import {
QueryJson,
Datasource,
DatasourcePlusQueryResponse,
RowOperations,
} from "@budibase/types"
import { getIntegration } from "../index"
import sdk from "../../sdk"
@ -10,6 +11,17 @@ export async function makeExternalQuery(
datasource: Datasource,
json: QueryJson
): Promise<DatasourcePlusQueryResponse> {
const entityId = json.endpoint.entityId,
tableName = json.meta.table.name,
tableId = json.meta.table._id
// case found during testing - make sure this doesn't happen again
if (
RowOperations.includes(json.endpoint.operation) &&
entityId !== tableId &&
entityId !== tableName
) {
throw new Error("Entity ID and table metadata do not align")
}
datasource = await sdk.datasources.enrich(datasource)
const Integration = await getIntegration(datasource.source)
// query is the opinionated function

View File

@ -6,6 +6,7 @@ import {
SqlClient,
isValidFilter,
getNativeSql,
SqlStatements,
} from "../utils"
import SqlTableQueryBuilder from "./sqlTable"
import {
@ -160,9 +161,19 @@ class InternalBuilder {
addFilters(
query: Knex.QueryBuilder,
filters: SearchFilters | undefined,
tableName: string,
table: Table,
opts: { aliases?: Record<string, string>; relationship?: boolean }
): Knex.QueryBuilder {
if (!filters) {
return query
}
filters = parseFilters(filters)
// if all or specified in filters, then everything is an or
const allOr = filters.allOr
const sqlStatements = new SqlStatements(this.client, table, { allOr })
const tableName =
this.client === SqlClient.SQL_LITE ? table._id! : table.name
function getTableAlias(name: string) {
const alias = opts.aliases?.[name]
return alias || name
@ -258,12 +269,6 @@ class InternalBuilder {
}
}
if (!filters) {
return query
}
filters = parseFilters(filters)
// if all or specified in filters, then everything is an or
const allOr = filters.allOr
if (filters.oneOf) {
iterate(filters.oneOf, (key, array) => {
const fnc = allOr ? "orWhereIn" : "whereIn"
@ -306,17 +311,11 @@ class InternalBuilder {
const lowValid = isValidFilter(value.low),
highValid = isValidFilter(value.high)
if (lowValid && highValid) {
// Use a between operator if we have 2 valid range values
const fnc = allOr ? "orWhereBetween" : "whereBetween"
query = query[fnc](key, [value.low, value.high])
query = sqlStatements.between(query, key, value.low, value.high)
} else if (lowValid) {
// Use just a single greater than operator if we only have a low
const fnc = allOr ? "orWhere" : "where"
query = query[fnc](key, ">", value.low)
query = sqlStatements.lte(query, key, value.low)
} else if (highValid) {
// Use just a single less than operator if we only have a high
const fnc = allOr ? "orWhere" : "where"
query = query[fnc](key, "<", value.high)
query = sqlStatements.gte(query, key, value.high)
}
})
}
@ -359,7 +358,7 @@ class InternalBuilder {
addSorting(query: Knex.QueryBuilder, json: QueryJson): Knex.QueryBuilder {
let { sort, paginate } = json
const table = json.meta?.table
const table = json.meta.table
const tableName = getTableName(table)
const aliases = json.tableAliases
const aliased =
@ -473,14 +472,13 @@ class InternalBuilder {
): Knex.QueryBuilder {
const tableName = endpoint.entityId
const tableAlias = aliases?.[tableName]
let table: string | Record<string, string> = tableName
if (tableAlias) {
table = { [tableAlias]: tableName }
}
let query = knex(table)
if (endpoint.schema) {
query = query.withSchema(endpoint.schema)
}
const query = knex(
this.tableNameWithSchema(tableName, {
alias: tableAlias,
schema: endpoint.schema,
})
)
return query
}
@ -547,7 +545,7 @@ class InternalBuilder {
if (foundOffset) {
query = query.offset(foundOffset)
}
query = this.addFilters(query, filters, tableName, {
query = this.addFilters(query, filters, json.meta.table, {
aliases: tableAliases,
})
// add sorting to pre-query
@ -568,7 +566,7 @@ class InternalBuilder {
endpoint.schema,
tableAliases
)
return this.addFilters(query, filters, tableName, {
return this.addFilters(query, filters, json.meta.table, {
relationship: true,
aliases: tableAliases,
})
@ -578,7 +576,7 @@ class InternalBuilder {
const { endpoint, body, filters, tableAliases } = json
let query = this.knexWithAlias(knex, endpoint, tableAliases)
const parsedBody = parseBody(body)
query = this.addFilters(query, filters, endpoint.entityId, {
query = this.addFilters(query, filters, json.meta.table, {
aliases: tableAliases,
})
// mysql can't use returning
@ -592,7 +590,7 @@ class InternalBuilder {
delete(knex: Knex, json: QueryJson, opts: QueryOptions): Knex.QueryBuilder {
const { endpoint, filters, tableAliases } = json
let query = this.knexWithAlias(knex, endpoint, tableAliases)
query = this.addFilters(query, filters, endpoint.entityId, {
query = this.addFilters(query, filters, json.meta.table, {
aliases: tableAliases,
})
// mysql can't use returning
@ -684,7 +682,7 @@ class SqlQueryBuilder extends SqlTableQueryBuilder {
// when creating if an ID has been inserted need to make sure
// the id filter is enriched with it before trying to retrieve the row
checkLookupKeys(id: any, json: QueryJson) {
if (!id || !json.meta?.table || !json.meta.table.primary) {
if (!id || !json.meta.table || !json.meta.table.primary) {
return json
}
const primaryKey = json.meta.table.primary?.[0]

View File

@ -168,8 +168,7 @@ class GoogleSheetsIntegration implements DatasourcePlus {
return ""
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
getStringConcat(parts: string[]) {
getStringConcat(_parts: string[]) {
return ""
}

View File

@ -1,14 +1,27 @@
import { SqlClient } from "../utils"
import Sql from "../base/sql"
import {
FieldType,
Operation,
QueryJson,
TableSourceType,
Table,
FieldType,
TableSourceType,
} from "@budibase/types"
const TABLE_NAME = "test"
const TABLE: Table = {
type: "table",
sourceType: TableSourceType.EXTERNAL,
sourceId: "SOURCE_ID",
schema: {
id: {
name: "id",
type: FieldType.NUMBER,
},
},
name: TABLE_NAME,
primary: ["id"],
}
function endpoint(table: any, operation: any) {
return {
@ -25,6 +38,10 @@ function generateReadJson({
sort,
paginate,
}: any = {}): QueryJson {
const tableObj = { ...TABLE }
if (table) {
tableObj.name = table
}
return {
endpoint: endpoint(table || TABLE_NAME, "READ"),
resource: {
@ -34,14 +51,7 @@ function generateReadJson({
sort: sort || {},
paginate: paginate || {},
meta: {
table: {
type: "table",
sourceType: TableSourceType.EXTERNAL,
sourceId: "SOURCE_ID",
schema: {},
name: table || TABLE_NAME,
primary: ["id"],
} as any,
table: tableObj,
},
}
}
@ -49,6 +59,9 @@ function generateReadJson({
function generateCreateJson(table = TABLE_NAME, body = {}): QueryJson {
return {
endpoint: endpoint(table, "CREATE"),
meta: {
table: TABLE,
},
body,
}
}
@ -58,7 +71,15 @@ function generateUpdateJson({
body = {},
filters = {},
meta = {},
}: {
table: string
body?: any
filters?: any
meta?: any
}): QueryJson {
if (!meta.table) {
meta.table = TABLE
}
return {
endpoint: endpoint(table, "UPDATE"),
filters,
@ -70,6 +91,9 @@ function generateUpdateJson({
function generateDeleteJson(table = TABLE_NAME, filters = {}): QueryJson {
return {
endpoint: endpoint(table, "DELETE"),
meta: {
table: TABLE,
},
filters,
}
}
@ -102,6 +126,9 @@ function generateRelationshipJson(config: { schema?: string } = {}): QueryJson {
},
],
extra: { idFilter: {} },
meta: {
table: TABLE,
},
}
}
@ -136,6 +163,9 @@ function generateManyRelationshipJson(config: { schema?: string } = {}) {
},
],
extra: { idFilter: {} },
meta: {
table: TABLE,
},
}
}
@ -319,7 +349,7 @@ describe("SQL query builder", () => {
)
expect(query).toEqual({
bindings: [date, limit],
sql: `select * from (select * from "${TABLE_NAME}" where "${TABLE_NAME}"."property" > $1 limit $2) as "${TABLE_NAME}"`,
sql: `select * from (select * from "${TABLE_NAME}" where "${TABLE_NAME}"."property" >= $1 limit $2) as "${TABLE_NAME}"`,
})
})
@ -338,7 +368,7 @@ describe("SQL query builder", () => {
)
expect(query).toEqual({
bindings: [date, limit],
sql: `select * from (select * from "${TABLE_NAME}" where "${TABLE_NAME}"."property" < $1 limit $2) as "${TABLE_NAME}"`,
sql: `select * from (select * from "${TABLE_NAME}" where "${TABLE_NAME}"."property" <= $1 limit $2) as "${TABLE_NAME}"`,
})
})
@ -572,7 +602,7 @@ describe("SQL query builder", () => {
)
expect(query).toEqual({
bindings: ["2000-01-01 00:00:00", 500],
sql: `select * from (select * from "${TABLE_NAME}" where "${TABLE_NAME}"."dob" > $1 limit $2) as "${TABLE_NAME}"`,
sql: `select * from (select * from "${TABLE_NAME}" where "${TABLE_NAME}"."dob" >= $1 limit $2) as "${TABLE_NAME}"`,
})
})
@ -591,7 +621,7 @@ describe("SQL query builder", () => {
)
expect(query).toEqual({
bindings: ["2010-01-01 00:00:00", 500],
sql: `select * from (select * from "${TABLE_NAME}" where "${TABLE_NAME}"."dob" < $1 limit $2) as "${TABLE_NAME}"`,
sql: `select * from (select * from "${TABLE_NAME}" where "${TABLE_NAME}"."dob" <= $1 limit $2) as "${TABLE_NAME}"`,
})
})

View File

@ -4,6 +4,8 @@ import {
QueryJson,
SourceName,
SqlQuery,
Table,
TableSourceType,
} from "@budibase/types"
import { join } from "path"
import Sql from "../base/sql"
@ -11,6 +13,16 @@ import { SqlClient } from "../utils"
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) {
@ -105,7 +117,8 @@ describe("Captures of real examples", () => {
let query = new Sql(SqlClient.POSTGRES, limit)._query(queryJson)
const filters = queryJson.filters
const notEqualsValue = Object.values(filters?.notEqual!)[0]
const rangeValue = Object.values(filters?.range!)[0]
const rangeValue: { high?: string | number; low?: string | number } =
Object.values(filters?.range!)[0]
const equalValue = Object.values(filters?.equal!)[0]
expect(query).toEqual({
@ -222,6 +235,9 @@ describe("Captures of real examples", () => {
resource: {
fields,
},
meta: {
table: TABLE,
},
}
}

View File

@ -0,0 +1,2 @@
export * from "./utils"
export { SqlStatements } from "./sqlStatements"

View File

@ -0,0 +1,80 @@
import { FieldType, Table, FieldSchema } from "@budibase/types"
import { SqlClient } from "./utils"
import { Knex } from "knex"
export class SqlStatements {
client: string
table: Table
allOr: boolean | undefined
constructor(
client: string,
table: Table,
{ allOr }: { allOr?: boolean } = {}
) {
this.client = client
this.table = table
this.allOr = allOr
}
getField(key: string): FieldSchema | undefined {
const fieldName = key.split(".")[1]
return this.table.schema[fieldName]
}
between(
query: Knex.QueryBuilder,
key: string,
low: number | string,
high: number | string
) {
// Use a between operator if we have 2 valid range values
const field = this.getField(key)
if (
field?.type === FieldType.BIGINT &&
this.client === SqlClient.SQL_LITE
) {
query = query.whereRaw(
`CAST(${key} AS INTEGER) BETWEEN CAST(? AS INTEGER) AND CAST(? AS INTEGER)`,
[low, high]
)
} else {
const fnc = this.allOr ? "orWhereBetween" : "whereBetween"
query = query[fnc](key, [low, high])
}
return query
}
lte(query: Knex.QueryBuilder, key: string, low: number | string) {
// Use just a single greater than operator if we only have a low
const field = this.getField(key)
if (
field?.type === FieldType.BIGINT &&
this.client === SqlClient.SQL_LITE
) {
query = query.whereRaw(`CAST(${key} AS INTEGER) >= CAST(? AS INTEGER)`, [
low,
])
} else {
const fnc = this.allOr ? "orWhere" : "where"
query = query[fnc](key, ">=", low)
}
return query
}
gte(query: Knex.QueryBuilder, key: string, high: number | string) {
const field = this.getField(key)
// Use just a single less than operator if we only have a high
if (
field?.type === FieldType.BIGINT &&
this.client === SqlClient.SQL_LITE
) {
query = query.whereRaw(`CAST(${key} AS INTEGER) <= CAST(? AS INTEGER)`, [
high,
])
} else {
const fnc = this.allOr ? "orWhere" : "where"
query = query[fnc](key, "<=", high)
}
return query
}
}

View File

@ -5,10 +5,10 @@ import {
FieldType,
TableSourceType,
} from "@budibase/types"
import { DocumentType, SEPARATOR } from "../db/utils"
import { InvalidColumns, DEFAULT_BB_DATASOURCE_ID } from "../constants"
import { helpers } from "@budibase/shared-core"
import env from "../environment"
import { DocumentType, SEPARATOR } from "../../db/utils"
import { InvalidColumns, DEFAULT_BB_DATASOURCE_ID } from "../../constants"
import { SWITCHABLE_TYPES, helpers } from "@budibase/shared-core"
import env from "../../environment"
import { Knex } from "knex"
const DOUBLE_SEPARATOR = `${SEPARATOR}${SEPARATOR}`
@ -284,8 +284,8 @@ export function isIsoDateString(str: string) {
* @param column The column to check, to see if it is a valid relationship.
* @param tableIds The IDs of the tables which currently exist.
*/
export function shouldCopyRelationship(
column: { type: string; tableId?: string },
function shouldCopyRelationship(
column: { type: FieldType.LINK; tableId?: string },
tableIds: string[]
) {
return (
@ -303,28 +303,18 @@ export function shouldCopyRelationship(
* @param column The column to check for options or boolean type.
* @param fetchedColumn The fetched column to check for the type in the external database.
*/
export function shouldCopySpecialColumn(
column: { type: string },
fetchedColumn: { type: string } | undefined
function shouldCopySpecialColumn(
column: { type: FieldType },
fetchedColumn: { type: FieldType } | undefined
) {
const isFormula = column.type === FieldType.FORMULA
const specialTypes = [
FieldType.OPTIONS,
FieldType.LONGFORM,
FieldType.ARRAY,
FieldType.FORMULA,
FieldType.BB_REFERENCE,
]
// column has been deleted, remove - formulas will never exist, always copy
if (!isFormula && column && !fetchedColumn) {
return false
}
const fetchedIsNumber =
!fetchedColumn || fetchedColumn.type === FieldType.NUMBER
return (
specialTypes.indexOf(column.type as FieldType) !== -1 ||
(fetchedIsNumber && column.type === FieldType.BOOLEAN)
)
return fetchedIsNumber && column.type === FieldType.BOOLEAN
}
/**
@ -357,11 +347,44 @@ function copyExistingPropsOver(
continue
}
const column = existingTableSchema[key]
const existingColumnType = column?.type
const updatedColumnType = table.schema[key]?.type
// If the db column type changed to a non-compatible one, we want to re-fetch it
if (
shouldCopyRelationship(column, tableIds) ||
shouldCopySpecialColumn(column, table.schema[key])
updatedColumnType !== existingColumnType &&
!SWITCHABLE_TYPES[updatedColumnType]?.includes(existingColumnType)
) {
table.schema[key] = existingTableSchema[key]
continue
}
if (
column.type === FieldType.LINK &&
!shouldCopyRelationship(column, tableIds)
) {
continue
}
const specialTypes = [
FieldType.OPTIONS,
FieldType.LONGFORM,
FieldType.ARRAY,
FieldType.FORMULA,
FieldType.BB_REFERENCE,
]
if (
specialTypes.includes(column.type) &&
!shouldCopySpecialColumn(column, table.schema[key])
) {
continue
}
table.schema[key] = {
...existingTableSchema[key],
externalType:
existingTableSchema[key].externalType ||
table.schema[key].externalType,
}
}
}

View File

@ -35,7 +35,6 @@ export function init() {
// 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
// back in.
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { helpers, snippets, ...rest } = ctx
return vm.withContext(rest, () => vm.execute(js))
} catch (error: any) {

View File

@ -348,8 +348,7 @@ const preSaveAction: Partial<Record<SourceName, any>> = {
* Make sure all datasource entities have a display name selected
*/
export function setDefaultDisplayColumns(datasource: Datasource) {
//
for (let entity of Object.values(datasource.entities || {})) {
for (const entity of Object.values(datasource.entities || {})) {
if (entity.primaryDisplay) {
continue
}

View File

@ -200,6 +200,6 @@ export async function search(
}
} catch (err: any) {
const msg = typeof err === "string" ? err : err.message
throw new Error(`Unable to search by SQL - ${msg}`)
throw new Error(`Unable to search by SQL - ${msg}`, { cause: err })
}
}

View File

@ -160,7 +160,7 @@ describe("internal search", () => {
const response = await search.paginatedSearch(
{
contains: {
column: "a",
column: ["a"],
colArr: [1, 2, 3],
},
},
@ -168,7 +168,7 @@ describe("internal search", () => {
)
checkLucene(
response,
`(*:* AND column:a AND colArr:(1 AND 2 AND 3))`,
`(*:* AND column:(a) AND colArr:(1 AND 2 AND 3))`,
PARAMS
)
})

View File

@ -52,6 +52,12 @@ export async function getDatasourceAndQuery(
): Promise<DatasourcePlusQueryResponse> {
const datasourceId = json.endpoint.datasourceId
const datasource = await sdk.datasources.get(datasourceId)
const table = datasource.entities?.[json.endpoint.entityId]
if (!json.meta && table) {
json.meta = {
table,
}
}
return makeExternalQuery(datasource, json)
}

View File

@ -3,7 +3,6 @@ import {
Operation,
RelationshipType,
RenameColumn,
AddColumn,
Table,
TableRequest,
ViewV2,
@ -33,7 +32,7 @@ import * as viewSdk from "../../views"
export async function save(
datasourceId: string,
update: Table,
opts?: { tableId?: string; renaming?: RenameColumn; adding?: AddColumn }
opts?: { tableId?: string; renaming?: RenameColumn }
) {
let tableToSave: TableRequest = {
...update,
@ -52,6 +51,12 @@ export async function save(
!oldTable &&
(tableToSave.primary == null || tableToSave.primary.length === 0)
) {
if (tableToSave.schema.id) {
throw new Error(
"External tables with no `primary` column set will define an `id` column, but we found an `id` column in the supplied schema. Either set a `primary` column or remove the `id` column."
)
}
tableToSave.primary = ["id"]
tableToSave.schema.id = {
type: FieldType.NUMBER,
@ -179,14 +184,7 @@ export async function save(
// remove the rename prop
delete tableToSave._rename
// if adding a new column, we need to rebuild the schema for that table to get the 'externalType' of the column
if (opts?.adding) {
datasource.entities[tableToSave.name] = (
await datasourceSdk.buildFilteredSchema(datasource, [tableToSave.name])
).tables[tableToSave.name]
} else {
datasource.entities[tableToSave.name] = tableToSave
}
datasource.entities[tableToSave.name] = tableToSave
// store it into couch now for budibase reference
await db.put(populateExternalTableSchemas(datasource))

View File

@ -42,7 +42,7 @@ const FieldTypeMap: Record<FieldType, SQLiteType> = {
[FieldType.ATTACHMENT_SINGLE]: SQLiteType.BLOB,
[FieldType.ARRAY]: SQLiteType.BLOB,
[FieldType.LINK]: SQLiteType.BLOB,
[FieldType.BIGINT]: SQLiteType.REAL,
[FieldType.BIGINT]: SQLiteType.TEXT,
// TODO: consider the difference between multi-user and single user types (subtyping)
[FieldType.BB_REFERENCE]: SQLiteType.TEXT,
}

View File

@ -351,7 +351,6 @@ describe("table sdk", () => {
const view: ViewV2 = {
...basicView,
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { name, description, ...newTableSchema } = basicTable.schema
const result = syncSchema(_.cloneDeep(view), newTableSchema, undefined)
@ -365,7 +364,6 @@ describe("table sdk", () => {
const view: ViewV2 = {
...basicView,
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { description, ...newTableSchema } = {
...basicTable.schema,
updatedDescription: {
@ -450,7 +448,6 @@ describe("table sdk", () => {
hiddenField: { visible: false },
},
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { name, description, ...newTableSchema } = basicTable.schema
const result = syncSchema(_.cloneDeep(view), newTableSchema, undefined)
@ -474,7 +471,6 @@ describe("table sdk", () => {
hiddenField: { visible: false },
},
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { name, description, ...newTableSchema } = {
...basicTable.schema,
newField1: {
@ -506,7 +502,6 @@ describe("table sdk", () => {
hiddenField: { visible: false },
},
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { description, ...newTableSchema } = {
...basicTable.schema,
updatedDescription: {

View File

@ -60,7 +60,10 @@ export class DatasourceAPI extends TestAPI {
})
}
query = async (query: QueryJson, expectations?: Expectations) => {
query = async (
query: Omit<QueryJson, "meta"> & Partial<Pick<QueryJson, "meta">>,
expectations?: Expectations
) => {
return await this._post<any>(`/api/datasources/query`, {
body: query,
expectations,

View File

@ -374,11 +374,13 @@ class Orchestrator {
for (let [innerObject, innerValue] of Object.entries(
originalStepInput[key][innerKey]
)) {
originalStepInput[key][innerKey][innerObject] =
automationUtils.substituteLoopStep(
innerValue as string,
`steps.${loopStepNumber}`
)
if (typeof innerValue === "string") {
originalStepInput[key][innerKey][innerObject] =
automationUtils.substituteLoopStep(
innerValue,
`steps.${loopStepNumber}`
)
}
}
}
}
@ -458,7 +460,6 @@ class Orchestrator {
inputs,
step.schema.inputs
)
try {
// appId is always passed
const outputs = await stepFn({

View File

@ -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
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
async onDisconnect(socket: Socket) {
async onDisconnect(_socket: Socket) {
// Override
}

View File

@ -0,0 +1,33 @@
import { FieldType } from "@budibase/types"
type SwitchableTypes = Partial<{
[K in FieldType]: [K, ...FieldType[]]
}>
export const SWITCHABLE_TYPES: SwitchableTypes = {
[FieldType.STRING]: [
FieldType.STRING,
FieldType.OPTIONS,
FieldType.LONGFORM,
FieldType.BARCODEQR,
],
[FieldType.OPTIONS]: [
FieldType.OPTIONS,
FieldType.STRING,
FieldType.LONGFORM,
FieldType.BARCODEQR,
],
[FieldType.LONGFORM]: [
FieldType.LONGFORM,
FieldType.STRING,
FieldType.OPTIONS,
FieldType.BARCODEQR,
],
[FieldType.BARCODEQR]: [
FieldType.BARCODEQR,
FieldType.STRING,
FieldType.OPTIONS,
FieldType.LONGFORM,
],
[FieldType.NUMBER]: [FieldType.NUMBER, FieldType.BOOLEAN],
}

View File

@ -1,4 +1,5 @@
export * from "./api"
export * from "./fields"
export const OperatorOptions = {
Equals: {

View File

@ -2,10 +2,11 @@ import {
Datasource,
FieldSubtype,
FieldType,
FormulaType,
SearchFilter,
SearchQuery,
SearchFilters,
SearchQueryFields,
SearchQueryOperators,
SearchFilterOperator,
SortDirection,
SortType,
} from "@budibase/types"
@ -19,9 +20,13 @@ const HBS_REGEX = /{{([^{].*?)}}/g
* Returns the valid operator options for a certain data type
*/
export const getValidOperatorsForType = (
fieldType: { type: FieldType; subtype?: FieldSubtype },
fieldType: {
type: FieldType
subtype?: FieldSubtype
formulaType?: FormulaType
},
field: string,
datasource: Datasource & { tableId: any } // TODO: is this table id ever populated?
datasource: Datasource & { tableId: any }
) => {
const Op = OperatorOptions
const stringOps = [
@ -46,7 +51,7 @@ export const getValidOperatorsForType = (
value: string
label: string
}[] = []
const { type, subtype } = fieldType
const { type, subtype, formulaType } = fieldType
if (type === FieldType.STRING) {
ops = stringOps
} else if (type === FieldType.NUMBER || type === FieldType.BIGINT) {
@ -61,7 +66,7 @@ export const getValidOperatorsForType = (
ops = stringOps
} else if (type === FieldType.DATETIME) {
ops = numOps
} else if (type === FieldType.FORMULA) {
} else if (type === FieldType.FORMULA && formulaType === FormulaType.STATIC) {
ops = stringOps.concat([Op.MoreThan, Op.LessThan])
} else if (type === FieldType.BB_REFERENCE && subtype == FieldSubtype.USER) {
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
* behaviour with how backend tables are filtered (no value means no filter).
*/
const cleanupQuery = (query: SearchQuery) => {
const cleanupQuery = (query: SearchFilters) => {
if (!query) {
return query
}
for (let filterField of NoEmptyFilterStrings) {
if (!query[filterField]) {
const operator = filterField as SearchFilterOperator
if (!query[operator]) {
continue
}
for (let [key, value] of Object.entries(query[filterField]!)) {
for (let [key, value] of Object.entries(query[operator]!)) {
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
*/
const removeKeyNumbering = (key: string) => {
export const removeKeyNumbering = (key: string): string => {
if (typeof key === "string" && key.match(/\d[0-9]*:/g) != null) {
const parts = key.split(":")
// remove the number
parts.shift()
return parts.join(":")
} else {
@ -130,7 +137,7 @@ const removeKeyNumbering = (key: string) => {
* @param filter the builder filter structure
*/
export const buildLuceneQuery = (filter: SearchFilter[]) => {
let query: SearchQuery = {
let query: SearchFilters = {
string: {},
fuzzy: {},
range: {},
@ -151,6 +158,7 @@ export const buildLuceneQuery = (filter: SearchFilter[]) => {
filter.forEach(expression => {
let { operator, field, type, value, externalType, onEmptyFilter } =
expression
const queryOperator = operator as SearchFilterOperator
const isHbs =
typeof value === "string" && (value.match(HBS_REGEX) || []).length > 0
// Parse all values into correct types
@ -165,8 +173,8 @@ export const buildLuceneQuery = (filter: SearchFilter[]) => {
if (
type === "datetime" &&
!isHbs &&
operator !== "empty" &&
operator !== "notEmpty"
queryOperator !== "empty" &&
queryOperator !== "notEmpty"
) {
// Ensure date value is a valid date and parse into correct format
if (!value) {
@ -179,7 +187,7 @@ export const buildLuceneQuery = (filter: SearchFilter[]) => {
}
}
if (type === "number" && typeof value === "string" && !isHbs) {
if (operator === "oneOf") {
if (queryOperator === "oneOf") {
value = value.split(",").map(item => parseFloat(item))
} else {
value = parseFloat(value)
@ -210,33 +218,35 @@ export const buildLuceneQuery = (filter: SearchFilter[]) => {
high: type === "number" ? maxint : "9999-00-00T00:00:00.000Z",
}
}
if ((operator as any) === "rangeLow" && value != null && value !== "") {
query.range[field].low = value
} else if (
(operator as any) === "rangeHigh" &&
value != null &&
value !== ""
) {
query.range[field].high = value
if (operator === "rangeLow" && value != null && value !== "") {
query.range[field] = {
...query.range[field],
low: value,
}
} else if (operator === "rangeHigh" && value != null && value !== "") {
query.range[field] = {
...query.range[field],
high: value,
}
}
} else if (query[operator] && operator !== "onEmptyFilter") {
} else if (query[queryOperator] && operator !== "onEmptyFilter") {
if (type === "boolean") {
// Transform boolean filters to cope with null.
// "equals false" needs to be "not 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[field] = true
} else if (operator === "notEqual" && value === false) {
} else if (queryOperator === "notEqual" && value === false) {
query.equal = query.equal || {}
query.equal[field] = true
} else {
query[operator] = query[operator] || {}
query[operator]![field] = value
query[queryOperator] = query[queryOperator] || {}
query[queryOperator]![field] = value
}
} else {
query[operator] = query[operator] || {}
query[operator]![field] = value
query[queryOperator] = query[queryOperator] || {}
query[queryOperator]![field] = value
}
}
})
@ -249,7 +259,7 @@ export const buildLuceneQuery = (filter: SearchFilter[]) => {
* @param docs the data
* @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)) {
return []
}
@ -263,7 +273,7 @@ export const runLuceneQuery = (docs: any[], query?: SearchQuery) => {
// Iterates over a set of filters and evaluates a fail function against a doc
const match =
(
type: keyof SearchQueryFields,
type: SearchFilterOperator,
failFn: (docValue: any, testValue: any) => boolean
) =>
(doc: any) => {
@ -280,7 +290,7 @@ export const runLuceneQuery = (docs: any[], query?: SearchQuery) => {
// Process a string match (fails if the value does not start with the string)
const stringMatch = match(
SearchQueryOperators.STRING,
SearchFilterOperator.STRING,
(docValue: string, testValue: string) => {
return (
!docValue ||
@ -291,7 +301,7 @@ export const runLuceneQuery = (docs: any[], query?: SearchQuery) => {
// Process a fuzzy match (treat the same as starts with when running locally)
const fuzzyMatch = match(
SearchQueryOperators.FUZZY,
SearchFilterOperator.FUZZY,
(docValue: string, testValue: string) => {
return (
!docValue ||
@ -302,7 +312,7 @@ export const runLuceneQuery = (docs: any[], query?: SearchQuery) => {
// Process a range match
const rangeMatch = match(
SearchQueryOperators.RANGE,
SearchFilterOperator.RANGE,
(
docValue: string | number | null,
testValue: { low: number; high: number }
@ -325,7 +335,7 @@ export const runLuceneQuery = (docs: any[], query?: SearchQuery) => {
// Process an equal match (fails if the value is different)
const equalMatch = match(
SearchQueryOperators.EQUAL,
SearchFilterOperator.EQUAL,
(docValue: any, testValue: string | null) => {
return testValue != null && testValue !== "" && docValue !== testValue
}
@ -333,7 +343,7 @@ export const runLuceneQuery = (docs: any[], query?: SearchQuery) => {
// Process a not-equal match (fails if the value is the same)
const notEqualMatch = match(
SearchQueryOperators.NOT_EQUAL,
SearchFilterOperator.NOT_EQUAL,
(docValue: any, testValue: string | null) => {
return testValue != null && testValue !== "" && docValue === testValue
}
@ -341,7 +351,7 @@ export const runLuceneQuery = (docs: any[], query?: SearchQuery) => {
// Process an empty match (fails if the value is not empty)
const emptyMatch = match(
SearchQueryOperators.EMPTY,
SearchFilterOperator.EMPTY,
(docValue: string | null) => {
return docValue != null && docValue !== ""
}
@ -349,7 +359,7 @@ export const runLuceneQuery = (docs: any[], query?: SearchQuery) => {
// Process a not-empty match (fails is the value is empty)
const notEmptyMatch = match(
SearchQueryOperators.NOT_EMPTY,
SearchFilterOperator.NOT_EMPTY,
(docValue: string | null) => {
return docValue == null || docValue === ""
}
@ -357,7 +367,7 @@ export const runLuceneQuery = (docs: any[], query?: SearchQuery) => {
// Process an includes match (fails if the value is not included)
const oneOf = match(
SearchQueryOperators.ONE_OF,
SearchFilterOperator.ONE_OF,
(docValue: any, testValue: any) => {
if (typeof testValue === "string") {
testValue = testValue.split(",")
@ -370,28 +380,28 @@ export const runLuceneQuery = (docs: any[], query?: SearchQuery) => {
)
const containsAny = match(
SearchQueryOperators.CONTAINS_ANY,
SearchFilterOperator.CONTAINS_ANY,
(docValue: any, testValue: any) => {
return !docValue?.includes(...testValue)
}
)
const contains = match(
SearchQueryOperators.CONTAINS,
SearchFilterOperator.CONTAINS,
(docValue: string | any[], testValue: any[]) => {
return !testValue?.every((item: any) => docValue?.includes(item))
}
)
const notContains = match(
SearchQueryOperators.NOT_CONTAINS,
SearchFilterOperator.NOT_CONTAINS,
(docValue: string | any[], testValue: any[]) => {
return testValue?.every((item: any) => docValue?.includes(item))
}
)
const docMatch = (doc: any) => {
const filterFunctions: Record<SearchQueryOperators, (doc: any) => boolean> =
const filterFunctions: Record<SearchFilterOperator, (doc: any) => boolean> =
{
string: stringMatch,
fuzzy: fuzzyMatch,
@ -406,7 +416,7 @@ export const runLuceneQuery = (docs: any[], query?: SearchQuery) => {
notContains: notContains,
}
const activeFilterKeys: SearchQueryOperators[] = Object.entries(query || {})
const activeFilterKeys: SearchFilterOperator[] = Object.entries(query || {})
.filter(
([key, value]: [string, any]) =>
!["allOr", "onEmptyFilter"].includes(key) &&
@ -474,7 +484,7 @@ export const luceneLimit = (docs: any[], limit: string) => {
return docs.slice(0, numLimit)
}
export const hasFilters = (query?: SearchQuery) => {
export const hasFilters = (query?: SearchFilters) => {
if (!query) {
return false
}

View File

@ -1,6 +1,6 @@
import {
SearchQuery,
SearchQueryOperators,
SearchFilters,
SearchFilterOperator,
FieldType,
SearchFilter,
} from "@budibase/types"
@ -46,8 +46,8 @@ describe("runLuceneQuery", () => {
},
]
function buildQuery(filters: { [filterKey: string]: any }): SearchQuery {
const query: SearchQuery = {
function buildQuery(filters: { [filterKey: string]: any }): SearchFilters {
const query: SearchFilters = {
string: {},
fuzzy: {},
range: {},
@ -63,7 +63,7 @@ describe("runLuceneQuery", () => {
}
for (const filterKey in filters) {
query[filterKey as SearchQueryOperators] = filters[filterKey]
query[filterKey as SearchFilterOperator] = filters[filterKey]
}
return query
@ -265,13 +265,13 @@ describe("buildLuceneQuery", () => {
it("should parseFloat if the type is a number, but the value is a numeric string", () => {
const filter: SearchFilter[] = [
{
operator: SearchQueryOperators.EQUAL,
operator: SearchFilterOperator.EQUAL,
field: "customer_id",
type: FieldType.NUMBER,
value: "1212",
},
{
operator: SearchQueryOperators.ONE_OF,
operator: SearchFilterOperator.ONE_OF,
field: "customer_id",
type: FieldType.NUMBER,
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", () => {
const filter: SearchFilter[] = [
{
operator: SearchQueryOperators.EQUAL,
operator: SearchFilterOperator.EQUAL,
field: "customer_id",
type: FieldType.NUMBER,
value: "{{ customer_id }}",
},
{
operator: SearchQueryOperators.ONE_OF,
operator: SearchFilterOperator.ONE_OF,
field: "customer_id",
type: FieldType.NUMBER,
value: "{{ list_of_customer_ids }}",
@ -333,19 +333,19 @@ describe("buildLuceneQuery", () => {
it("should cast string to boolean if the type is boolean", () => {
const filter: SearchFilter[] = [
{
operator: SearchQueryOperators.EQUAL,
operator: SearchFilterOperator.EQUAL,
field: "a",
type: FieldType.BOOLEAN,
value: "not_true",
},
{
operator: SearchQueryOperators.NOT_EQUAL,
operator: SearchFilterOperator.NOT_EQUAL,
field: "b",
type: FieldType.BOOLEAN,
value: "not_true",
},
{
operator: SearchQueryOperators.EQUAL,
operator: SearchFilterOperator.EQUAL,
field: "c",
type: FieldType.BOOLEAN,
value: "true",
@ -374,19 +374,19 @@ describe("buildLuceneQuery", () => {
it("should split the string for contains operators", () => {
const filter: SearchFilter[] = [
{
operator: SearchQueryOperators.CONTAINS,
operator: SearchFilterOperator.CONTAINS,
field: "description",
type: FieldType.ARRAY,
value: "Large box,Heavy box,Small box",
},
{
operator: SearchQueryOperators.NOT_CONTAINS,
operator: SearchFilterOperator.NOT_CONTAINS,
field: "description",
type: FieldType.ARRAY,
value: "Large box,Heavy box,Small box",
},
{
operator: SearchQueryOperators.CONTAINS_ANY,
operator: SearchFilterOperator.CONTAINS_ANY,
field: "description",
type: FieldType.ARRAY,
value: "Large box,Heavy box,Small box",

View File

@ -4,7 +4,6 @@ export const PostProcessorNames = {
CONVERT_LITERALS: "convert-literals",
}
/* eslint-disable no-unused-vars */
class Postprocessor {
name: string
private fn: any

View File

@ -9,7 +9,6 @@ export const PreprocessorNames = {
FINALISE: "finalise",
}
/* eslint-disable no-unused-vars */
class Preprocessor {
name: string
private fn: any

View File

@ -1,68 +1,11 @@
import { FieldType } from "../../documents"
import { EmptyFilterOption } from "../../sdk"
import { EmptyFilterOption, SearchFilters } from "../../sdk"
export type SearchFilter = {
operator: keyof SearchQuery
operator: keyof SearchFilters | "rangeLow" | "rangeHigh"
onEmptyFilter?: EmptyFilterOption
field: string
type?: FieldType
value: any
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">

View File

@ -1,5 +1,5 @@
import { User } from "../../documents"
import { SearchQuery } from "./searchFilter"
import { SearchFilters } from "../../sdk"
export interface SaveUserResponse {
_id: string
@ -55,7 +55,7 @@ export interface InviteUsersResponse {
export interface SearchUsersRequest {
bookmark?: string
query?: SearchQuery
query?: SearchFilters
appId?: string
limit?: number
paginate?: boolean
@ -66,6 +66,8 @@ export interface CreateAdminUserRequest {
password?: string
tenantId: string
ssoId?: string
familyName?: string
givenName?: string
}
export interface AddSSoUserRequest {

View File

@ -1,6 +1,6 @@
import { Document } from "../../document"
import { View, ViewV2 } from "../view"
import { AddColumn, RenameColumn } from "../../../sdk"
import { RenameColumn } from "../../../sdk"
import { TableSchema } from "./schema"
export const INTERNAL_TABLE_SOURCE_ID = "bb_internal"
@ -30,6 +30,5 @@ export interface Table extends Document {
export interface TableRequest extends Table {
_rename?: RenameColumn
_add?: AddColumn
created?: boolean
}

View File

@ -22,6 +22,13 @@ export interface UserSSO {
providerType: SSOProviderType
oauth2?: OAuth2
thirdPartyProfile?: SSOProfileJson
profile?: {
displayName?: string
name?: {
givenName?: string
familyName?: string
}
}
}
export type SSOUser = User & UserSSO

View File

@ -14,6 +14,14 @@ export enum Operation {
DELETE_TABLE = "DELETE_TABLE",
}
export const RowOperations = [
Operation.CREATE,
Operation.READ,
Operation.UPDATE,
Operation.DELETE,
Operation.BULK_CREATE,
]
export enum SortDirection {
ASCENDING = "ASCENDING",
DESCENDING = "DESCENDING",

View File

@ -3,47 +3,66 @@ import { Row, Table } from "../documents"
import { SortType } from "../api"
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 {
allOr?: boolean
onEmptyFilter?: EmptyFilterOption
string?: {
[SearchFilterOperator.STRING]?: {
[key: string]: string
}
fuzzy?: {
[SearchFilterOperator.FUZZY]?: {
[key: string]: string
}
range?: {
[key: string]: {
high: number | string
low: number | string
}
[SearchFilterOperator.RANGE]?: {
[key: string]:
| {
high: number | string
low: number | string
}
| { high: number | string }
| { low: number | string }
}
equal?: {
[SearchFilterOperator.EQUAL]?: {
[key: string]: any
}
notEqual?: {
[SearchFilterOperator.NOT_EQUAL]?: {
[key: string]: any
}
empty?: {
[SearchFilterOperator.EMPTY]?: {
[key: string]: any
}
notEmpty?: {
[SearchFilterOperator.NOT_EMPTY]?: {
[key: string]: any
}
oneOf?: {
[SearchFilterOperator.ONE_OF]?: {
[key: string]: any[]
}
contains?: {
[key: string]: any[] | any
}
notContains?: {
[SearchFilterOperator.CONTAINS]?: {
[key: string]: any[]
}
containsAny?: {
[SearchFilterOperator.NOT_CONTAINS]?: {
[key: string]: any[]
}
[SearchFilterOperator.CONTAINS_ANY]?: {
[key: string]: any[]
}
}
export type SearchQueryFields = Omit<SearchFilters, "allOr" | "onEmptyFilter">
export interface SortJson {
[key: string]: {
direction: SortDirection
@ -61,10 +80,6 @@ export interface RenameColumn {
updated: string
}
export interface AddColumn {
name: string
}
export interface RelationshipsJson {
through?: string
from?: string
@ -90,8 +105,8 @@ export interface QueryJson {
paginate?: PaginationJson
body?: Row | Row[]
table?: Table
meta?: {
table?: Table
meta: {
table: Table
tables?: Record<string, Table>
renamed?: RenameColumn
}

View File

@ -116,7 +116,8 @@ const parseBooleanParam = (param: any) => {
export const adminUser = async (
ctx: Ctx<CreateAdminUserRequest, CreateAdminUserResponse>
) => {
const { email, password, tenantId, ssoId } = ctx.request.body
const { email, password, tenantId, ssoId, givenName, familyName } =
ctx.request.body
if (await platform.tenants.exists(tenantId)) {
ctx.throw(403, "Organisation already exists.")
@ -151,6 +152,8 @@ export const adminUser = async (
ssoId,
hashPassword,
requirePassword,
firstName: givenName,
lastName: familyName,
})
// events

View File

@ -704,7 +704,6 @@ describe("scim", () => {
expect(response).toEqual({
Resources: expect.arrayContaining(
groups.map(g => {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { members, ...groupData } = g
return groupData
})
@ -724,7 +723,6 @@ describe("scim", () => {
expect(response).toEqual({
Resources: expect.arrayContaining(
groups.map(g => {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { members, displayName, ...groupData } = g
return groupData
})
@ -874,7 +872,6 @@ describe("scim", () => {
qs: "excludedAttributes=members",
})
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { members, ...expectedResponse } = group
expect(response).toEqual(expectedResponse)

View File

@ -16,6 +16,8 @@ function buildAdminInitValidation() {
password: OPTIONAL_STRING,
tenantId: Joi.string().required(),
ssoId: Joi.string(),
familyName: OPTIONAL_STRING,
givenName: OPTIONAL_STRING,
})
.required()
.unknown(false)

View File

@ -1,7 +1,6 @@
import { features } from "@budibase/backend-core"
import env from "./environment"
// eslint-disable-next-line no-unused-vars
enum WorkerFeature {}
const featureList: WorkerFeature[] = features.processFeatureEnvVar(

View File

@ -4,7 +4,7 @@ import {
InviteUsersRequest,
User,
CreateAdminUserRequest,
SearchQuery,
SearchFilters,
InviteUsersResponse,
} from "@budibase/types"
import structures from "../structures"
@ -150,7 +150,7 @@ export class UserAPI extends TestAPI {
}
searchUsers = (
{ query }: { query?: SearchQuery },
{ query }: { query?: SearchFilters },
opts?: { status?: number; noHeaders?: boolean }
) => {
const req = this.request

12
scripts/devDocker.sh Executable file
View File

@ -0,0 +1,12 @@
#!/bin/bash
# Check if the pro submodule is loaded
if [ ! -d "./packages/pro/src" ]; then
echo "[ERROR] Submodule is not loaded. This is only allowed with loaded submodules."
exit 1
fi
yarn build --scope @budibase/server --scope @budibase/worker
docker-compose -f hosting/docker-compose.build.yaml -f hosting/docker-compose.dev.yaml --env-file hosting/.env up --build --scale proxy-service=0