Merge branch 'master' of github.com:Budibase/budibase into v3-ui

This commit is contained in:
Andrew Kingston 2024-10-14 08:49:53 +01:00
commit 963b3ae35c
No known key found for this signature in database
9 changed files with 172 additions and 37 deletions

View File

@ -14,6 +14,7 @@ import {
InternalTable, InternalTable,
tenancy, tenancy,
features, features,
utils,
} from "@budibase/backend-core" } from "@budibase/backend-core"
import { quotas } from "@budibase/pro" import { quotas } from "@budibase/pro"
import { import {
@ -757,6 +758,37 @@ describe.each([
}) })
}) })
describe("user column", () => {
beforeAll(async () => {
table = await config.api.table.save(
saveTableRequest({
schema: {
user: {
name: "user",
type: FieldType.BB_REFERENCE_SINGLE,
subtype: BBReferenceFieldSubType.USER,
default: "{{ [Current User]._id }}",
},
},
})
)
})
it("creates a new row with a default value successfully", async () => {
const row = await config.api.row.save(table._id!, {})
expect(row.user._id).toEqual(config.getUser()._id)
})
it("does not use default value if value specified", async () => {
const id = `us_${utils.newid()}`
await config.createUser({ _id: id })
const row = await config.api.row.save(table._id!, {
user: id,
})
expect(row.user._id).toEqual(id)
})
})
describe("bindings", () => { describe("bindings", () => {
describe("string column", () => { describe("string column", () => {
beforeAll(async () => { beforeAll(async () => {

View File

@ -21,6 +21,7 @@ import {
ViewCalculation, ViewCalculation,
ViewV2Enriched, ViewV2Enriched,
RowExportFormat, RowExportFormat,
PermissionLevel,
} from "@budibase/types" } from "@budibase/types"
import { checkBuilderEndpoint } from "./utilities/TestFunctions" import { checkBuilderEndpoint } from "./utilities/TestFunctions"
import * as setup from "./utilities" import * as setup from "./utilities"
@ -191,6 +192,55 @@ describe.each([
) )
}) })
describe("permissions", () => {
it("get the base permissions for the table", async () => {
const table = await config.api.table.save(
tableForDatasource(datasource, {
schema: {
name: {
type: FieldType.STRING,
name: "name",
},
},
})
)
// get the explicit permissions
const { permissions } = await config.api.permission.get(table._id!, {
status: 200,
})
const explicitPermissions = {
role: "ADMIN",
permissionType: "EXPLICIT",
}
expect(permissions.write).toEqual(explicitPermissions)
expect(permissions.read).toEqual(explicitPermissions)
// revoke the explicit permissions
for (let level of [PermissionLevel.WRITE, PermissionLevel.READ]) {
await config.api.permission.revoke(
{
roleId: permissions[level].role,
resourceId: table._id!,
level,
},
{ status: 200 }
)
}
// check base permissions
const { permissions: basePermissions } = await config.api.permission.get(
table._id!,
{
status: 200,
}
)
const basePerms = { role: "BASIC", permissionType: "BASE" }
expect(basePermissions.write).toEqual(basePerms)
expect(basePermissions.read).toEqual(basePerms)
})
})
describe("update", () => { describe("update", () => {
it("updates a table", async () => { it("updates a table", async () => {
const table = await config.api.table.save( const table = await config.api.table.save(

View File

@ -3546,6 +3546,50 @@ describe.each([
expect(response.rows).toHaveLength(1) expect(response.rows).toHaveLength(1)
expect(response.rows[0].sum).toEqual(61) expect(response.rows[0].sum).toEqual(61)
}) })
it("should be able to filter on a single user field in both the view query and search query", async () => {
const table = await config.api.table.save(
saveTableRequest({
schema: {
user: {
name: "user",
type: FieldType.BB_REFERENCE_SINGLE,
subtype: BBReferenceFieldSubType.USER,
},
},
})
)
await config.api.row.save(table._id!, {
user: config.getUser()._id,
})
const view = await config.api.viewV2.create({
tableId: table._id!,
name: generator.guid(),
query: {
equal: {
user: "{{ [user].[_id] }}",
},
},
schema: {
user: {
visible: true,
},
},
})
const { rows } = await config.api.viewV2.search(view.id, {
query: {
equal: {
user: "{{ [user].[_id] }}",
},
},
})
expect(rows).toHaveLength(1)
expect(rows[0].user._id).toEqual(config.getUser()._id)
})
}) })
describe("permissions", () => { describe("permissions", () => {

View File

@ -73,8 +73,7 @@ export async function getResourcePerms(
p[level] = { role, type: PermissionSource.BASE } p[level] = { role, type: PermissionSource.BASE }
return p return p
}, {}) }, {})
const result = Object.assign(basePermissions, permissions) return Object.assign(basePermissions, permissions)
return result
} }
export async function getDependantResources( export async function getDependantResources(

View File

@ -16,7 +16,7 @@ import * as external from "./search/external"
import { ExportRowsParams, ExportRowsResult } from "./search/types" import { ExportRowsParams, ExportRowsResult } from "./search/types"
import { dataFilters } from "@budibase/shared-core" import { dataFilters } from "@budibase/shared-core"
import sdk from "../../index" import sdk from "../../index"
import { searchInputMapping } from "./search/utils" import { checkFilters, searchInputMapping } from "./search/utils"
import { db, features } from "@budibase/backend-core" import { db, features } from "@budibase/backend-core"
import tracer from "dd-trace" import tracer from "dd-trace"
import { getQueryableFields, removeInvalidFilters } from "./queryUtils" import { getQueryableFields, removeInvalidFilters } from "./queryUtils"
@ -81,6 +81,10 @@ export async function search(
options.query = {} options.query = {}
} }
if (context) {
options.query = await enrichSearchContext(options.query, context)
}
// need to make sure filters in correct shape before checking for view // need to make sure filters in correct shape before checking for view
options = searchInputMapping(table, options) options = searchInputMapping(table, options)
@ -92,12 +96,15 @@ export async function search(
// Enrich saved query with ephemeral query params. // Enrich saved query with ephemeral query params.
// We prevent searching on any fields that are saved as part of the query, as // We prevent searching on any fields that are saved as part of the query, as
// that could let users find rows they should not be allowed to access. // that could let users find rows they should not be allowed to access.
let viewQuery = dataFilters.buildQueryLegacy(view.query) || {} let viewQuery = await enrichSearchContext(view.query || {}, context)
viewQuery = dataFilters.buildQueryLegacy(viewQuery) || {}
viewQuery = checkFilters(table, viewQuery)
delete viewQuery?.onEmptyFilter delete viewQuery?.onEmptyFilter
const sqsEnabled = await features.flags.isEnabled("SQS") const sqsEnabled = await features.flags.isEnabled("SQS")
const supportsLogicalOperators = const supportsLogicalOperators =
isExternalTableID(view.tableId) || sqsEnabled isExternalTableID(view.tableId) || sqsEnabled
if (!supportsLogicalOperators) { if (!supportsLogicalOperators) {
// In the unlikely event that a Grouped Filter is in a non-SQS environment // In the unlikely event that a Grouped Filter is in a non-SQS environment
// It needs to be ignored entirely // It needs to be ignored entirely
@ -113,13 +120,12 @@ export async function search(
?.filter(filter => filter.field) ?.filter(filter => filter.field)
.map(filter => db.removeKeyNumbering(filter.field)) || [] .map(filter => db.removeKeyNumbering(filter.field)) || []
viewQuery ??= {}
// Carry over filters for unused fields // Carry over filters for unused fields
Object.keys(options.query).forEach(key => { Object.keys(options.query).forEach(key => {
const operator = key as Exclude<SearchFilterKey, LogicalOperator> const operator = key as Exclude<SearchFilterKey, LogicalOperator>
Object.keys(options.query[operator] || {}).forEach(field => { Object.keys(options.query[operator] || {}).forEach(field => {
if (!existingFields.includes(db.removeKeyNumbering(field))) { if (!existingFields.includes(db.removeKeyNumbering(field))) {
viewQuery![operator]![field] = options.query[operator]![field] viewQuery[operator]![field] = options.query[operator]![field]
} }
}) })
}) })
@ -137,10 +143,6 @@ export async function search(
} }
} }
if (context) {
options.query = await enrichSearchContext(options.query, context)
}
options.query = dataFilters.cleanupQuery(options.query) options.query = dataFilters.cleanupQuery(options.query)
options.query = dataFilters.fixupFilterArrays(options.query) options.query = dataFilters.fixupFilterArrays(options.query)

View File

@ -80,11 +80,10 @@ function userColumnMapping(column: string, filters: SearchFilters) {
}) })
} }
// maps through the search parameters to check if any of the inputs are invalid export function checkFilters(
// based on the table schema, converts them to something that is valid. table: Table,
export function searchInputMapping(table: Table, options: RowSearchParams) { filters: SearchFilters
// need an internal function to loop over filters, because this takes the full options ): SearchFilters {
function checkFilters(filters: SearchFilters) {
for (let [key, column] of Object.entries(table.schema || {})) { for (let [key, column] of Object.entries(table.schema || {})) {
switch (column.type) { switch (column.type) {
case FieldType.BB_REFERENCE_SINGLE: { case FieldType.BB_REFERENCE_SINGLE: {
@ -105,10 +104,17 @@ export function searchInputMapping(table: Table, options: RowSearchParams) {
} }
} }
} }
return dataFilters.recurseLogicalOperators(filters, checkFilters) return dataFilters.recurseLogicalOperators(filters, filters =>
checkFilters(table, filters)
)
} }
// maps through the search parameters to check if any of the inputs are invalid
// based on the table schema, converts them to something that is valid.
export function searchInputMapping(table: Table, options: RowSearchParams) {
// need an internal function to loop over filters, because this takes the full options
if (options.query) { if (options.query) {
options.query = checkFilters(options.query) options.query = checkFilters(table, options.query)
} }
return options return options
} }

View File

@ -1,5 +1,5 @@
import { permissions, roles } from "@budibase/backend-core" import { DocumentType, permissions, roles } from "@budibase/backend-core"
import { DocumentType, VirtualDocumentType } from "../db/utils" import { VirtualDocumentType } from "../db/utils"
import { getDocumentType, getVirtualDocumentType } from "@budibase/types" import { getDocumentType, getVirtualDocumentType } from "@budibase/types"
export const CURRENTLY_SUPPORTED_LEVELS: string[] = [ export const CURRENTLY_SUPPORTED_LEVELS: string[] = [
@ -19,6 +19,7 @@ export function getPermissionType(resourceId: string) {
switch (docType) { switch (docType) {
case DocumentType.TABLE: case DocumentType.TABLE:
case DocumentType.ROW: case DocumentType.ROW:
case DocumentType.DATASOURCE_PLUS:
return permissions.PermissionType.TABLE return permissions.PermissionType.TABLE
case DocumentType.AUTOMATION: case DocumentType.AUTOMATION:
return permissions.PermissionType.AUTOMATION return permissions.PermissionType.AUTOMATION

View File

@ -67,7 +67,7 @@ const allowDefaultColumnByType: Record<FieldType, boolean> = {
[FieldType.SIGNATURE_SINGLE]: false, [FieldType.SIGNATURE_SINGLE]: false,
[FieldType.LINK]: false, [FieldType.LINK]: false,
[FieldType.BB_REFERENCE]: false, [FieldType.BB_REFERENCE]: false,
[FieldType.BB_REFERENCE_SINGLE]: false, [FieldType.BB_REFERENCE_SINGLE]: true,
} }
export function canBeDisplayColumn(type: FieldType): boolean { export function canBeDisplayColumn(type: FieldType): boolean {

View File

@ -126,6 +126,7 @@ export interface BBReferenceSingleFieldMetadata
extends Omit<BaseFieldSchema, "subtype"> { extends Omit<BaseFieldSchema, "subtype"> {
type: FieldType.BB_REFERENCE_SINGLE type: FieldType.BB_REFERENCE_SINGLE
subtype: Exclude<BBReferenceFieldSubType, BBReferenceFieldSubType.USERS> subtype: Exclude<BBReferenceFieldSubType, BBReferenceFieldSubType.USERS>
default?: string
} }
export interface AttachmentFieldMetadata extends BaseFieldSchema { export interface AttachmentFieldMetadata extends BaseFieldSchema {