@@ -50,11 +107,14 @@
label="Client ID*"
placeholder="Type here..."
bind:value={config.clientId}
+ error={errors.clientId}
/>
To learn how to configure OAuth2, our documentation
-
-
+
+
diff --git a/packages/client/src/components/app/forms/RelationshipField.svelte b/packages/client/src/components/app/forms/RelationshipField.svelte
index b5906b72fa..6fa7b32621 100644
--- a/packages/client/src/components/app/forms/RelationshipField.svelte
+++ b/packages/client/src/components/app/forms/RelationshipField.svelte
@@ -1,17 +1,26 @@
-
+
+
-
+
{
- group.filters = group.filters?.filter((filter: any) => {
- return filter.field && filter.operator
+ if (update.groups) {
+ update.groups = update.groups
+ .map(group => {
+ if (group.filters) {
+ group.filters = group.filters.filter((filter: any) => {
+ return filter.field && filter.operator
+ })
+ return group.filters?.length ? group : null
+ }
+ return group
})
- return group.filters?.length ? group : null
- })
- .filter((group): group is SearchFilterGroup => !!group)
+ .filter((group): group is SearchFilterGroup => !!group)
+ }
return update
}
diff --git a/packages/server/src/api/controllers/application.ts b/packages/server/src/api/controllers/application.ts
index 9a4b241f30..6b5ef149a4 100644
--- a/packages/server/src/api/controllers/application.ts
+++ b/packages/server/src/api/controllers/application.ts
@@ -358,8 +358,8 @@ async function performAppCreate(
},
theme: DefaultAppTheme,
customTheme: {
- primaryColor: "var(--spectrum-global-color-static-blue-1200)",
- primaryColorHover: "var(--spectrum-global-color-static-blue-800)",
+ primaryColor: "var(--spectrum-global-color-blue-700)",
+ primaryColorHover: "var(--spectrum-global-color-blue-600)",
buttonBorderRadius: "16px",
},
features: {
diff --git a/packages/server/src/api/controllers/oauth2.ts b/packages/server/src/api/controllers/oauth2.ts
index 8e770ac459..e2ca2dad1d 100644
--- a/packages/server/src/api/controllers/oauth2.ts
+++ b/packages/server/src/api/controllers/oauth2.ts
@@ -1,6 +1,6 @@
import {
- CreateOAuth2ConfigRequest,
- CreateOAuth2ConfigResponse,
+ UpsertOAuth2ConfigRequest,
+ UpsertOAuth2ConfigResponse,
Ctx,
FetchOAuth2ConfigsResponse,
OAuth2Config,
@@ -22,7 +22,7 @@ export async function fetch(ctx: Ctx) {
}
export async function create(
- ctx: Ctx
+ ctx: Ctx
) {
const { body } = ctx.request
const newConfig: RequiredKeys> = {
@@ -36,3 +36,28 @@ export async function create(
ctx.status = 201
ctx.body = { config }
}
+
+export async function edit(
+ ctx: Ctx
+) {
+ const { body } = ctx.request
+ const toUpdate: RequiredKeys = {
+ id: ctx.params.id,
+ name: body.name,
+ url: body.url,
+ clientId: ctx.clientId,
+ clientSecret: ctx.clientSecret,
+ }
+
+ const config = await sdk.oauth2.update(toUpdate)
+ ctx.body = { config }
+}
+
+export async function remove(
+ ctx: Ctx
+) {
+ const configToRemove = ctx.params.id
+
+ await sdk.oauth2.remove(configToRemove)
+ ctx.status = 204
+}
diff --git a/packages/server/src/api/routes/oauth2.ts b/packages/server/src/api/routes/oauth2.ts
index 4fb82d6dfd..2ae3cbdf82 100644
--- a/packages/server/src/api/routes/oauth2.ts
+++ b/packages/server/src/api/routes/oauth2.ts
@@ -1,8 +1,22 @@
import Router from "@koa/router"
import { PermissionType } from "@budibase/types"
+import { middleware } from "@budibase/backend-core"
import authorized from "../../middleware/authorized"
import * as controller from "../controllers/oauth2"
+import Joi from "joi"
+
+function oAuth2ConfigValidator() {
+ return middleware.joiValidator.body(
+ Joi.object({
+ name: Joi.string().required(),
+ url: Joi.string().required(),
+ clientId: Joi.string().required(),
+ clientSecret: Joi.string().required(),
+ }),
+ { allowUnknown: false }
+ )
+}
const router: Router = new Router()
@@ -10,7 +24,19 @@ router.get("/api/oauth2", authorized(PermissionType.BUILDER), controller.fetch)
router.post(
"/api/oauth2",
authorized(PermissionType.BUILDER),
+ oAuth2ConfigValidator(),
controller.create
)
+router.put(
+ "/api/oauth2/:id",
+ authorized(PermissionType.BUILDER),
+ oAuth2ConfigValidator(),
+ controller.edit
+)
+router.delete(
+ "/api/oauth2/:id",
+ authorized(PermissionType.BUILDER),
+ controller.remove
+)
export default router
diff --git a/packages/server/src/api/routes/tests/oauth2.spec.ts b/packages/server/src/api/routes/tests/oauth2.spec.ts
index c9f1ada2a2..9d616203f9 100644
--- a/packages/server/src/api/routes/tests/oauth2.spec.ts
+++ b/packages/server/src/api/routes/tests/oauth2.spec.ts
@@ -1,11 +1,16 @@
-import { CreateOAuth2ConfigRequest, VirtualDocumentType } from "@budibase/types"
+import {
+ OAuth2Config,
+ UpsertOAuth2ConfigRequest,
+ VirtualDocumentType,
+} from "@budibase/types"
import * as setup from "./utilities"
import { generator } from "@budibase/backend-core/tests"
+import _ from "lodash/fp"
describe("/oauth2", () => {
let config = setup.getConfig()
- function makeOAuth2Config(): CreateOAuth2ConfigRequest {
+ function makeOAuth2Config(): UpsertOAuth2ConfigRequest {
return {
name: generator.guid(),
url: generator.url(),
@@ -19,7 +24,7 @@ describe("/oauth2", () => {
beforeEach(async () => await config.newTenant())
const expectOAuth2ConfigId = expect.stringMatching(
- `^${VirtualDocumentType.ROW_ACTION}_.+$`
+ `^${VirtualDocumentType.OAUTH2_CONFIG}_.+$`
)
describe("fetch", () => {
@@ -92,4 +97,96 @@ describe("/oauth2", () => {
])
})
})
+
+ describe("update", () => {
+ let existingConfigs: OAuth2Config[] = []
+
+ beforeEach(async () => {
+ existingConfigs = []
+ for (let i = 0; i < 10; i++) {
+ const oauth2Config = makeOAuth2Config()
+ const result = await config.api.oauth2.create(oauth2Config)
+
+ existingConfigs.push({ ...oauth2Config, id: result.config.id })
+ }
+ })
+
+ it("can update an existing configuration", async () => {
+ const { id: configId, ...configData } = _.sample(existingConfigs)!
+
+ await config.api.oauth2.update(configId, {
+ ...configData,
+ name: "updated name",
+ })
+
+ const response = await config.api.oauth2.fetch()
+ expect(response.configs).toHaveLength(existingConfigs.length)
+ expect(response.configs).toEqual(
+ expect.arrayContaining([
+ {
+ id: configId,
+ name: "updated name",
+ url: configData.url,
+ },
+ ])
+ )
+ })
+
+ it("throw if config not found", async () => {
+ await config.api.oauth2.update("unexisting", makeOAuth2Config(), {
+ status: 404,
+ body: { message: "OAuth2 config with id 'unexisting' not found." },
+ })
+ })
+
+ it("throws if trying to use an existing name", async () => {
+ const [config1, config2] = _.sampleSize(2, existingConfigs)
+ const { id: configId, ...configData } = config1
+
+ await config.api.oauth2.update(
+ configId,
+ {
+ ...configData,
+ name: config2.name,
+ },
+ {
+ status: 400,
+ body: {
+ message: `OAuth2 config with name '${config2.name}' is already taken.`,
+ },
+ }
+ )
+ })
+ })
+
+ describe("delete", () => {
+ let existingConfigs: OAuth2Config[] = []
+
+ beforeEach(async () => {
+ existingConfigs = []
+ for (let i = 0; i < 5; i++) {
+ const oauth2Config = makeOAuth2Config()
+ const result = await config.api.oauth2.create(oauth2Config)
+
+ existingConfigs.push({ ...oauth2Config, id: result.config.id })
+ }
+ })
+
+ it("can delete an existing configuration", async () => {
+ const { id: configId } = _.sample(existingConfigs)!
+
+ await config.api.oauth2.delete(configId, { status: 204 })
+
+ const response = await config.api.oauth2.fetch()
+ expect(response.configs).toHaveLength(existingConfigs.length - 1)
+ expect(response.configs.find(c => c.id === configId)).toBeUndefined()
+ })
+
+ it("throw if config not found", async () => {
+ await config.api.oauth2.delete("unexisting", {
+ status: 404,
+ body: { message: "OAuth2 config with id 'unexisting' not found." },
+ })
+ })
+ })
})
diff --git a/packages/server/src/api/routes/tests/row.spec.ts b/packages/server/src/api/routes/tests/row.spec.ts
index 1ee0e168a1..e910e5ab55 100644
--- a/packages/server/src/api/routes/tests/row.spec.ts
+++ b/packages/server/src/api/routes/tests/row.spec.ts
@@ -2195,95 +2195,94 @@ if (descriptions.length) {
})
})
- isInternal &&
- describe("attachments and signatures", () => {
- const coreAttachmentEnrichment = async (
- schema: TableSchema,
- field: string,
- attachmentCfg: string | string[]
- ) => {
- const testTable = await config.api.table.save(
- defaultTable({
- schema,
- })
- )
- const attachmentToStoreKey = (attachmentId: string) => {
- return {
- key: `${config.getAppId()}/attachments/${attachmentId}`,
- }
- }
- const draftRow = {
- name: "test",
- description: "test",
- [field]:
- typeof attachmentCfg === "string"
- ? attachmentToStoreKey(attachmentCfg)
- : attachmentCfg.map(attachmentToStoreKey),
- tableId: testTable._id,
- }
- const row = await config.api.row.save(testTable._id!, draftRow)
-
- await withEnv({ SELF_HOSTED: "true" }, async () => {
- return context.doInAppContext(config.getAppId(), async () => {
- const enriched: Row[] = await outputProcessing(testTable, [row])
- const [targetRow] = enriched
- const attachmentEntries = Array.isArray(targetRow[field])
- ? targetRow[field]
- : [targetRow[field]]
-
- for (const entry of attachmentEntries) {
- const attachmentId = entry.key.split("/").pop()
- expect(entry.url.split("?")[0]).toBe(
- `/files/signed/prod-budi-app-assets/${config.getProdAppId()}/attachments/${attachmentId}`
- )
- }
- })
+ describe("attachments and signatures", () => {
+ const coreAttachmentEnrichment = async (
+ schema: TableSchema,
+ field: string,
+ attachmentCfg: string | string[]
+ ) => {
+ const testTable = await config.api.table.save(
+ defaultTable({
+ schema,
})
+ )
+ const attachmentToStoreKey = (attachmentId: string) => {
+ return {
+ key: `${config.getAppId()}/attachments/${attachmentId}`,
+ }
}
+ const draftRow = {
+ name: "test",
+ description: "test",
+ [field]:
+ typeof attachmentCfg === "string"
+ ? attachmentToStoreKey(attachmentCfg)
+ : attachmentCfg.map(attachmentToStoreKey),
+ tableId: testTable._id,
+ }
+ const row = await config.api.row.save(testTable._id!, draftRow)
- it("should allow enriching single attachment rows", async () => {
- await coreAttachmentEnrichment(
- {
- attachment: {
- type: FieldType.ATTACHMENT_SINGLE,
- name: "attachment",
- constraints: { presence: false },
- },
- },
- "attachment",
- `${uuid.v4()}.csv`
- )
- })
+ await withEnv({ SELF_HOSTED: "true" }, async () => {
+ return context.doInAppContext(config.getAppId(), async () => {
+ const enriched: Row[] = await outputProcessing(testTable, [row])
+ const [targetRow] = enriched
+ const attachmentEntries = Array.isArray(targetRow[field])
+ ? targetRow[field]
+ : [targetRow[field]]
- it("should allow enriching attachment list rows", async () => {
- await coreAttachmentEnrichment(
- {
- attachments: {
- type: FieldType.ATTACHMENTS,
- name: "attachments",
- constraints: { type: "array", presence: false },
- },
- },
- "attachments",
- [`${uuid.v4()}.csv`]
- )
+ for (const entry of attachmentEntries) {
+ const attachmentId = entry.key.split("/").pop()
+ expect(entry.url.split("?")[0]).toBe(
+ `/files/signed/prod-budi-app-assets/${config.getProdAppId()}/attachments/${attachmentId}`
+ )
+ }
+ })
})
+ }
- it("should allow enriching signature rows", async () => {
- await coreAttachmentEnrichment(
- {
- signature: {
- type: FieldType.SIGNATURE_SINGLE,
- name: "signature",
- constraints: { presence: false },
- },
+ it("should allow enriching single attachment rows", async () => {
+ await coreAttachmentEnrichment(
+ {
+ attachment: {
+ type: FieldType.ATTACHMENT_SINGLE,
+ name: "attachment",
+ constraints: { presence: false },
},
- "signature",
- `${uuid.v4()}.png`
- )
- })
+ },
+ "attachment",
+ `${uuid.v4()}.csv`
+ )
})
+ it("should allow enriching attachment list rows", async () => {
+ await coreAttachmentEnrichment(
+ {
+ attachments: {
+ type: FieldType.ATTACHMENTS,
+ name: "attachments",
+ constraints: { type: "array", presence: false },
+ },
+ },
+ "attachments",
+ [`${uuid.v4()}.csv`]
+ )
+ })
+
+ it("should allow enriching signature rows", async () => {
+ await coreAttachmentEnrichment(
+ {
+ signature: {
+ type: FieldType.SIGNATURE_SINGLE,
+ name: "signature",
+ constraints: { presence: false },
+ },
+ },
+ "signature",
+ `${uuid.v4()}.png`
+ )
+ })
+ })
+
describe("exportRows", () => {
beforeEach(async () => {
table = await config.api.table.save(defaultTable())
diff --git a/packages/server/src/integrations/rest.ts b/packages/server/src/integrations/rest.ts
index 9247f5ef86..7889457903 100644
--- a/packages/server/src/integrations/rest.ts
+++ b/packages/server/src/integrations/rest.ts
@@ -381,32 +381,37 @@ export class RestIntegration implements IntegrationBase {
authConfigId?: string,
authConfigType?: RestAuthType
): Promise<{ [key: string]: any }> {
- let headers: any = {}
+ if (!authConfigId) {
+ return {}
+ }
- if (authConfigId) {
- if (authConfigType === RestAuthType.OAUTH2) {
- headers.Authorization = await sdk.oauth2.generateToken(authConfigId)
- } else if (this.config.authConfigs) {
- const authConfig = this.config.authConfigs.filter(
- c => c._id === authConfigId
- )[0]
- // check the config still exists before proceeding
- // if not - do nothing
- if (authConfig) {
- const { type, config } = authConfig
- switch (type) {
- case RestAuthType.BASIC:
- headers.Authorization = `Basic ${Buffer.from(
- `${config.username}:${config.password}`
- ).toString("base64")}`
- break
- case RestAuthType.BEARER:
- headers.Authorization = `Bearer ${config.token}`
- break
- default:
- throw utils.unreachable(type)
- }
- }
+ if (authConfigType === RestAuthType.OAUTH2) {
+ return { Authorization: await sdk.oauth2.generateToken(authConfigId) }
+ }
+
+ if (!this.config.authConfigs) {
+ return {}
+ }
+
+ let headers: any = {}
+ const authConfig = this.config.authConfigs.filter(
+ c => c._id === authConfigId
+ )[0]
+ // check the config still exists before proceeding
+ // if not - do nothing
+ if (authConfig) {
+ const { type, config } = authConfig
+ switch (type) {
+ case RestAuthType.BASIC:
+ headers.Authorization = `Basic ${Buffer.from(
+ `${config.username}:${config.password}`
+ ).toString("base64")}`
+ break
+ case RestAuthType.BEARER:
+ headers.Authorization = `Bearer ${config.token}`
+ break
+ default:
+ throw utils.unreachable(type)
}
}
diff --git a/packages/server/src/sdk/app/oauth2/crud.ts b/packages/server/src/sdk/app/oauth2/crud.ts
index 66ba718ee6..374c953710 100644
--- a/packages/server/src/sdk/app/oauth2/crud.ts
+++ b/packages/server/src/sdk/app/oauth2/crud.ts
@@ -34,7 +34,7 @@ export async function create(
throw new HTTPError("Name already used", 400)
}
- const id = `${VirtualDocumentType.ROW_ACTION}${SEPARATOR}${utils.newid()}`
+ const id = `${VirtualDocumentType.OAUTH2_CONFIG}${SEPARATOR}${utils.newid()}`
doc.configs[id] = {
id,
...config,
@@ -48,3 +48,49 @@ export async function get(id: string): Promise {
const doc = await getDocument()
return doc?.configs?.[id]
}
+
+export async function update(config: OAuth2Config): Promise {
+ const db = context.getAppDB()
+ const doc: OAuth2Configs = (await getDocument(db)) ?? {
+ _id: DocumentType.OAUTH2_CONFIG,
+ configs: {},
+ }
+
+ if (!doc.configs[config.id]) {
+ throw new HTTPError(`OAuth2 config with id '${config.id}' not found.`, 404)
+ }
+
+ if (
+ Object.values(doc.configs).find(
+ c => c.name === config.name && c.id !== config.id
+ )
+ ) {
+ throw new HTTPError(
+ `OAuth2 config with name '${config.name}' is already taken.`,
+ 400
+ )
+ }
+
+ doc.configs[config.id] = {
+ ...config,
+ }
+
+ await db.put(doc)
+ return doc.configs[config.id]
+}
+
+export async function remove(configId: string): Promise {
+ const db = context.getAppDB()
+ const doc: OAuth2Configs = (await getDocument(db)) ?? {
+ _id: DocumentType.OAUTH2_CONFIG,
+ configs: {},
+ }
+
+ if (!doc.configs[configId]) {
+ throw new HTTPError(`OAuth2 config with id '${configId}' not found.`, 404)
+ }
+
+ delete doc.configs[configId]
+
+ await db.put(doc)
+}
diff --git a/packages/server/src/tests/utilities/api/oauth2.ts b/packages/server/src/tests/utilities/api/oauth2.ts
index 9f52f27bfe..d4c99c9598 100644
--- a/packages/server/src/tests/utilities/api/oauth2.ts
+++ b/packages/server/src/tests/utilities/api/oauth2.ts
@@ -1,6 +1,6 @@
import {
- CreateOAuth2ConfigRequest,
- CreateOAuth2ConfigResponse,
+ UpsertOAuth2ConfigRequest,
+ UpsertOAuth2ConfigResponse,
FetchOAuth2ConfigsResponse,
} from "@budibase/types"
import { Expectations, TestAPI } from "./base"
@@ -13,10 +13,10 @@ export class OAuth2API extends TestAPI {
}
create = async (
- body: CreateOAuth2ConfigRequest,
+ body: UpsertOAuth2ConfigRequest,
expectations?: Expectations
) => {
- return await this._post("/api/oauth2", {
+ return await this._post("/api/oauth2", {
body,
expectations: {
status: expectations?.status ?? 201,
@@ -24,4 +24,21 @@ export class OAuth2API extends TestAPI {
},
})
}
+
+ update = async (
+ id: string,
+ body: UpsertOAuth2ConfigRequest,
+ expectations?: Expectations
+ ) => {
+ return await this._put(`/api/oauth2/${id}`, {
+ body,
+ expectations,
+ })
+ }
+
+ delete = async (id: string, expectations?: Expectations) => {
+ return await this._delete(`/api/oauth2/${id}`, {
+ expectations,
+ })
+ }
}
diff --git a/packages/shared-core/src/filters.ts b/packages/shared-core/src/filters.ts
index afe99d9565..a0bd54fee6 100644
--- a/packages/shared-core/src/filters.ts
+++ b/packages/shared-core/src/filters.ts
@@ -1,30 +1,30 @@
import {
- Datasource,
+ ArrayOperator,
+ BasicOperator,
BBReferenceFieldSubType,
+ Datasource,
+ EmptyFilterOption,
+ FieldConstraints,
FieldType,
FormulaType,
+ isArraySearchOperator,
+ isBasicSearchOperator,
+ isLogicalSearchOperator,
+ isRangeSearchOperator,
LegacyFilter,
+ LogicalOperator,
+ RangeOperator,
+ RowSearchParams,
+ SearchFilter,
+ SearchFilterOperator,
SearchFilters,
SearchQueryFields,
- ArrayOperator,
- SearchFilterOperator,
- SortType,
- FieldConstraints,
- SortOrder,
- RowSearchParams,
- EmptyFilterOption,
SearchResponse,
+ SortOrder,
+ SortType,
Table,
- BasicOperator,
- RangeOperator,
- LogicalOperator,
- isLogicalSearchOperator,
- UISearchFilter,
UILogicalOperator,
- isBasicSearchOperator,
- isArraySearchOperator,
- isRangeSearchOperator,
- SearchFilter,
+ UISearchFilter,
} from "@budibase/types"
import dayjs from "dayjs"
import { OperatorOptions, SqlNumberTypeRangeMap } from "./constants"
@@ -444,6 +444,7 @@ export function buildQuery(
return {}
}
+ // Migrate legacy filters if required
if (Array.isArray(filter)) {
filter = processSearchFilters(filter)
if (!filter) {
@@ -451,10 +452,7 @@ export function buildQuery(
}
}
- const operator = logicalOperatorFromUI(
- filter.logicalOperator || UILogicalOperator.ALL
- )
-
+ // Determine top level empty filter behaviour
const query: SearchFilters = {}
if (filter.onEmptyFilter) {
query.onEmptyFilter = filter.onEmptyFilter
@@ -462,8 +460,24 @@ export function buildQuery(
query.onEmptyFilter = EmptyFilterOption.RETURN_ALL
}
+ // Default to matching all groups/filters
+ const operator = logicalOperatorFromUI(
+ filter.logicalOperator || UILogicalOperator.ALL
+ )
+
query[operator] = {
conditions: (filter.groups || []).map(group => {
+ // Check if we contain more groups
+ if (group.groups) {
+ const searchFilter = buildQuery(group)
+
+ // We don't define this properly in the types, but certain fields should
+ // not be present in these nested search filters
+ delete searchFilter.onEmptyFilter
+ return searchFilter
+ }
+
+ // Otherwise handle filters
const { allOr, onEmptyFilter, filters } = splitFiltersArray(
group.filters || []
)
@@ -471,7 +485,7 @@ export function buildQuery(
query.onEmptyFilter = onEmptyFilter
}
- // logicalOperator takes precendence over allOr
+ // logicalOperator takes precedence over allOr
let operator = allOr ? LogicalOperator.OR : LogicalOperator.AND
if (group.logicalOperator) {
operator = logicalOperatorFromUI(group.logicalOperator)
diff --git a/packages/shared-core/src/tests/filters.spec.ts b/packages/shared-core/src/tests/filters.spec.ts
new file mode 100644
index 0000000000..87c43d83f2
--- /dev/null
+++ b/packages/shared-core/src/tests/filters.spec.ts
@@ -0,0 +1,156 @@
+import { buildQuery } from "../filters"
+import {
+ BasicOperator,
+ EmptyFilterOption,
+ FieldType,
+ UILogicalOperator,
+ UISearchFilter,
+} from "@budibase/types"
+
+describe("filter to query conversion", () => {
+ it("handles a filter with 1 group", () => {
+ const filter: UISearchFilter = {
+ logicalOperator: UILogicalOperator.ALL,
+ onEmptyFilter: EmptyFilterOption.RETURN_NONE,
+ groups: [
+ {
+ logicalOperator: UILogicalOperator.ALL,
+ filters: [
+ {
+ field: "city",
+ operator: BasicOperator.STRING,
+ value: "lon",
+ },
+ ],
+ },
+ ],
+ }
+ const query = buildQuery(filter)
+ expect(query).toEqual({
+ onEmptyFilter: "none",
+ $and: {
+ conditions: [
+ {
+ $and: {
+ conditions: [
+ {
+ string: {
+ city: "lon",
+ },
+ },
+ ],
+ },
+ },
+ ],
+ },
+ })
+ })
+
+ it("handles an empty filter", () => {
+ const filter = undefined
+ const query = buildQuery(filter)
+ expect(query).toEqual({})
+ })
+
+ it("handles legacy filters", () => {
+ const filter = [
+ {
+ field: "city",
+ operator: BasicOperator.STRING,
+ value: "lon",
+ },
+ ]
+ const query = buildQuery(filter)
+ expect(query).toEqual({
+ onEmptyFilter: "all",
+ $and: {
+ conditions: [
+ {
+ $and: {
+ conditions: [
+ {
+ string: {
+ city: "lon",
+ },
+ },
+ ],
+ },
+ },
+ ],
+ },
+ })
+ })
+
+ it("handles nested groups", () => {
+ const filter: UISearchFilter = {
+ logicalOperator: UILogicalOperator.ALL,
+ onEmptyFilter: EmptyFilterOption.RETURN_NONE,
+ groups: [
+ {
+ logicalOperator: UILogicalOperator.ALL,
+ filters: [
+ {
+ field: "city",
+ operator: BasicOperator.STRING,
+ value: "lon",
+ },
+ ],
+ },
+ {
+ logicalOperator: UILogicalOperator.ALL,
+ groups: [
+ {
+ logicalOperator: UILogicalOperator.ANY,
+ filters: [
+ {
+ valueType: "Binding",
+ field: "country.country_name",
+ type: FieldType.STRING,
+ operator: BasicOperator.EQUAL,
+ noValue: false,
+ value: "England",
+ },
+ ],
+ },
+ ],
+ },
+ ],
+ }
+ const query = buildQuery(filter)
+ expect(query).toEqual({
+ onEmptyFilter: "none",
+ $and: {
+ conditions: [
+ {
+ $and: {
+ conditions: [
+ {
+ string: {
+ city: "lon",
+ },
+ },
+ ],
+ },
+ },
+ {
+ $and: {
+ conditions: [
+ {
+ $or: {
+ conditions: [
+ {
+ equal: {
+ "country.country_name": "England",
+ },
+ },
+ ],
+ },
+ },
+ ],
+ },
+ },
+ ],
+ },
+ })
+ })
+})
diff --git a/packages/types/src/api/web/app/oauth2.ts b/packages/types/src/api/web/app/oauth2.ts
index 4a6e836ef4..050cdf67fd 100644
--- a/packages/types/src/api/web/app/oauth2.ts
+++ b/packages/types/src/api/web/app/oauth2.ts
@@ -7,13 +7,13 @@ export interface FetchOAuth2ConfigsResponse {
configs: OAuth2ConfigResponse[]
}
-export interface CreateOAuth2ConfigRequest {
+export interface UpsertOAuth2ConfigRequest {
name: string
url: string
clientId: string
clientSecret: string
}
-export interface CreateOAuth2ConfigResponse {
+export interface UpsertOAuth2ConfigResponse {
config: OAuth2ConfigResponse
}
diff --git a/packages/types/src/api/web/searchFilter.ts b/packages/types/src/api/web/searchFilter.ts
index 7a6920fb60..1872fc0112 100644
--- a/packages/types/src/api/web/searchFilter.ts
+++ b/packages/types/src/api/web/searchFilter.ts
@@ -38,11 +38,19 @@ export type SearchFilter = {
// involved. We convert this to a SearchFilters before use with the search SDK.
export type LegacyFilter = AllOr | OnEmptyFilter | SearchFilter
+// A search filter group should either contain groups or filters, but not both
export type SearchFilterGroup = {
logicalOperator?: UILogicalOperator
- groups?: SearchFilterGroup[]
- filters?: LegacyFilter[]
-}
+} & (
+ | {
+ groups?: (SearchFilterGroup | UISearchFilter)[]
+ filters?: never
+ }
+ | {
+ filters?: LegacyFilter[]
+ groups?: never
+ }
+)
// As of v3, this is the format that the frontend always sends when search
// filters are involved. We convert this to SearchFilters before use with the
diff --git a/packages/types/src/documents/app/table/schema.ts b/packages/types/src/documents/app/table/schema.ts
index 551b1f16a8..86e15e4974 100644
--- a/packages/types/src/documents/app/table/schema.ts
+++ b/packages/types/src/documents/app/table/schema.ts
@@ -156,8 +156,8 @@ export interface FieldConstraints {
message?: string
}
numericality?: {
- greaterThanOrEqualTo: string | null
- lessThanOrEqualTo: string | null
+ greaterThanOrEqualTo?: string | null
+ lessThanOrEqualTo?: string | null
}
presence?:
| boolean
@@ -165,8 +165,8 @@ export interface FieldConstraints {
allowEmpty?: boolean
}
datetime?: {
- latest: string
- earliest: string
+ latest?: string
+ earliest?: string
}
}
@@ -197,7 +197,7 @@ export interface BigIntFieldMetadata extends BaseFieldSchema {
default?: string
}
-interface BaseFieldSchema extends UIFieldMetadata {
+export interface BaseFieldSchema extends UIFieldMetadata {
type: FieldType
name: string
sortable?: boolean
diff --git a/packages/types/src/ui/fields.ts b/packages/types/src/ui/fields.ts
new file mode 100644
index 0000000000..54faf16252
--- /dev/null
+++ b/packages/types/src/ui/fields.ts
@@ -0,0 +1,43 @@
+import {
+ FieldType,
+ FieldConstraints,
+ type FieldSchema,
+ type FormulaResponseType,
+} from "../"
+
+export interface UIField {
+ name: string
+ type: FieldType
+ subtype?: string
+ icon: string
+ constraints?: {
+ type?: string
+ presence?: boolean
+ length?: any
+ inclusion?: string[]
+ numericality?: {
+ greaterThanOrEqualTo?: string
+ lessThanOrEqualTo?: string
+ }
+ datetime?: {
+ latest?: string
+ earliest?: string
+ }
+ }
+}
+
+// an empty/partial field schema which is used when building new columns in the UI
+// the current construction process of a column means that it is never certain what
+// this object contains, or what type it is currently set to, meaning that our
+// strict FieldSchema isn't really usable here, the strict fieldSchema only occurs
+// when the table is saved, but in the UI in can be in a real mix of states
+export type FieldSchemaConfig = FieldSchema & {
+ constraints: FieldConstraints
+ fieldName?: string
+ responseType?: FormulaResponseType
+ default?: any
+ fieldId?: string
+ optionColors?: string[]
+ schema?: any
+ json?: string
+}
diff --git a/packages/types/src/ui/index.ts b/packages/types/src/ui/index.ts
index 0b219f54fb..b40baa61f5 100644
--- a/packages/types/src/ui/index.ts
+++ b/packages/types/src/ui/index.ts
@@ -5,3 +5,4 @@ export * from "./dataFetch"
export * from "./datasource"
export * from "./common"
export * from "./BudibaseApp"
+export * from "./fields"