Merge branch 'master' of github.com:Budibase/budibase into v3-ui
This commit is contained in:
commit
963b3ae35c
|
@ -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 () => {
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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", () => {
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
@ -80,35 +80,41 @@ function userColumnMapping(column: string, filters: SearchFilters) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function checkFilters(
|
||||||
|
table: Table,
|
||||||
|
filters: SearchFilters
|
||||||
|
): SearchFilters {
|
||||||
|
for (let [key, column] of Object.entries(table.schema || {})) {
|
||||||
|
switch (column.type) {
|
||||||
|
case FieldType.BB_REFERENCE_SINGLE: {
|
||||||
|
const subtype = column.subtype
|
||||||
|
switch (subtype) {
|
||||||
|
case BBReferenceFieldSubType.USER:
|
||||||
|
userColumnMapping(key, filters)
|
||||||
|
break
|
||||||
|
|
||||||
|
default:
|
||||||
|
utils.unreachable(subtype)
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case FieldType.BB_REFERENCE: {
|
||||||
|
userColumnMapping(key, filters)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return dataFilters.recurseLogicalOperators(filters, filters =>
|
||||||
|
checkFilters(table, filters)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
// maps through the search parameters to check if any of the inputs are invalid
|
// 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.
|
// based on the table schema, converts them to something that is valid.
|
||||||
export function searchInputMapping(table: Table, options: RowSearchParams) {
|
export function searchInputMapping(table: Table, options: RowSearchParams) {
|
||||||
// need an internal function to loop over filters, because this takes the full options
|
// need an internal function to loop over filters, because this takes the full options
|
||||||
function checkFilters(filters: SearchFilters) {
|
|
||||||
for (let [key, column] of Object.entries(table.schema || {})) {
|
|
||||||
switch (column.type) {
|
|
||||||
case FieldType.BB_REFERENCE_SINGLE: {
|
|
||||||
const subtype = column.subtype
|
|
||||||
switch (subtype) {
|
|
||||||
case BBReferenceFieldSubType.USER:
|
|
||||||
userColumnMapping(key, filters)
|
|
||||||
break
|
|
||||||
|
|
||||||
default:
|
|
||||||
utils.unreachable(subtype)
|
|
||||||
}
|
|
||||||
break
|
|
||||||
}
|
|
||||||
case FieldType.BB_REFERENCE: {
|
|
||||||
userColumnMapping(key, filters)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return dataFilters.recurseLogicalOperators(filters, checkFilters)
|
|
||||||
}
|
|
||||||
if (options.query) {
|
if (options.query) {
|
||||||
options.query = checkFilters(options.query)
|
options.query = checkFilters(table, options.query)
|
||||||
}
|
}
|
||||||
return options
|
return options
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
Loading…
Reference in New Issue