Merge branch 'v3-ui' of github.com:Budibase/budibase into new-rbac-ui
This commit is contained in:
commit
609bc3fb79
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"$schema": "node_modules/lerna/schemas/lerna-schema.json",
|
"$schema": "node_modules/lerna/schemas/lerna-schema.json",
|
||||||
"version": "2.32.17",
|
"version": "2.33.2",
|
||||||
"npmClient": "yarn",
|
"npmClient": "yarn",
|
||||||
"packages": [
|
"packages": [
|
||||||
"packages/*",
|
"packages/*",
|
||||||
|
|
|
@ -54,30 +54,46 @@ function getPackageJsonFields(): {
|
||||||
VERSION: string
|
VERSION: string
|
||||||
SERVICE_NAME: string
|
SERVICE_NAME: string
|
||||||
} {
|
} {
|
||||||
function findFileInAncestors(
|
function getParentFile(file: string) {
|
||||||
fileName: string,
|
function findFileInAncestors(
|
||||||
currentDir: string
|
fileName: string,
|
||||||
): string | null {
|
currentDir: string
|
||||||
const filePath = `${currentDir}/${fileName}`
|
): string | null {
|
||||||
if (existsSync(filePath)) {
|
const filePath = `${currentDir}/${fileName}`
|
||||||
return filePath
|
if (existsSync(filePath)) {
|
||||||
|
return filePath
|
||||||
|
}
|
||||||
|
|
||||||
|
const parentDir = `${currentDir}/..`
|
||||||
|
if (parentDir === currentDir) {
|
||||||
|
// reached root directory
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return findFileInAncestors(fileName, parentDir)
|
||||||
}
|
}
|
||||||
|
|
||||||
const parentDir = `${currentDir}/..`
|
const packageJsonFile = findFileInAncestors(file, process.cwd())
|
||||||
if (parentDir === currentDir) {
|
const content = readFileSync(packageJsonFile!, "utf-8")
|
||||||
// reached root directory
|
const parsedContent = JSON.parse(content)
|
||||||
return null
|
return parsedContent
|
||||||
}
|
}
|
||||||
|
|
||||||
return findFileInAncestors(fileName, parentDir)
|
let localVersion: string | undefined
|
||||||
|
if (isDev() && !isTest()) {
|
||||||
|
try {
|
||||||
|
const lerna = getParentFile("lerna.json")
|
||||||
|
localVersion = lerna.version
|
||||||
|
} catch {
|
||||||
|
//
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const packageJsonFile = findFileInAncestors("package.json", process.cwd())
|
const parsedContent = getParentFile("package.json")
|
||||||
const content = readFileSync(packageJsonFile!, "utf-8")
|
|
||||||
const parsedContent = JSON.parse(content)
|
|
||||||
return {
|
return {
|
||||||
VERSION: process.env.BUDIBASE_VERSION || parsedContent.version,
|
VERSION:
|
||||||
|
localVersion || process.env.BUDIBASE_VERSION || parsedContent.version,
|
||||||
SERVICE_NAME: parsedContent.name,
|
SERVICE_NAME: parsedContent.name,
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import semver from "semver"
|
||||||
import { BuiltinPermissionID, PermissionLevel } from "./permissions"
|
import { BuiltinPermissionID, PermissionLevel } from "./permissions"
|
||||||
import {
|
import {
|
||||||
prefixRoleID,
|
prefixRoleID,
|
||||||
|
@ -7,7 +8,13 @@ import {
|
||||||
doWithDB,
|
doWithDB,
|
||||||
} from "../db"
|
} from "../db"
|
||||||
import { getAppDB } from "../context"
|
import { getAppDB } from "../context"
|
||||||
import { Screen, Role as RoleDoc, RoleUIMetadata } from "@budibase/types"
|
import {
|
||||||
|
Screen,
|
||||||
|
Role as RoleDoc,
|
||||||
|
RoleUIMetadata,
|
||||||
|
Database,
|
||||||
|
App,
|
||||||
|
} from "@budibase/types"
|
||||||
import cloneDeep from "lodash/fp/cloneDeep"
|
import cloneDeep from "lodash/fp/cloneDeep"
|
||||||
import { RoleColor, helpers } from "@budibase/shared-core"
|
import { RoleColor, helpers } from "@budibase/shared-core"
|
||||||
|
|
||||||
|
@ -23,14 +30,6 @@ const BUILTIN_IDS = {
|
||||||
BUILDER: "BUILDER",
|
BUILDER: "BUILDER",
|
||||||
}
|
}
|
||||||
|
|
||||||
// exclude internal roles like builder
|
|
||||||
const EXTERNAL_BUILTIN_ROLE_IDS = [
|
|
||||||
BUILTIN_IDS.ADMIN,
|
|
||||||
BUILTIN_IDS.POWER,
|
|
||||||
BUILTIN_IDS.BASIC,
|
|
||||||
BUILTIN_IDS.PUBLIC,
|
|
||||||
]
|
|
||||||
|
|
||||||
export const RoleIDVersion = {
|
export const RoleIDVersion = {
|
||||||
// original version, with a UUID based ID
|
// original version, with a UUID based ID
|
||||||
UUID: undefined,
|
UUID: undefined,
|
||||||
|
@ -425,7 +424,7 @@ export async function getAllRoles(appId?: string): Promise<RoleDoc[]> {
|
||||||
}
|
}
|
||||||
return internal(appDB)
|
return internal(appDB)
|
||||||
}
|
}
|
||||||
async function internal(db: any) {
|
async function internal(db: Database | undefined) {
|
||||||
let roles: RoleDoc[] = []
|
let roles: RoleDoc[] = []
|
||||||
if (db) {
|
if (db) {
|
||||||
const body = await db.allDocs(
|
const body = await db.allDocs(
|
||||||
|
@ -440,8 +439,26 @@ export async function getAllRoles(appId?: string): Promise<RoleDoc[]> {
|
||||||
}
|
}
|
||||||
const builtinRoles = getBuiltinRoles()
|
const builtinRoles = getBuiltinRoles()
|
||||||
|
|
||||||
|
// exclude internal roles like builder
|
||||||
|
let externalBuiltinRoles = []
|
||||||
|
|
||||||
|
if (!db || (await shouldIncludePowerRole(db))) {
|
||||||
|
externalBuiltinRoles = [
|
||||||
|
BUILTIN_IDS.ADMIN,
|
||||||
|
BUILTIN_IDS.POWER,
|
||||||
|
BUILTIN_IDS.BASIC,
|
||||||
|
BUILTIN_IDS.PUBLIC,
|
||||||
|
]
|
||||||
|
} else {
|
||||||
|
externalBuiltinRoles = [
|
||||||
|
BUILTIN_IDS.ADMIN,
|
||||||
|
BUILTIN_IDS.BASIC,
|
||||||
|
BUILTIN_IDS.PUBLIC,
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
// need to combine builtin with any DB record of them (for sake of permissions)
|
// need to combine builtin with any DB record of them (for sake of permissions)
|
||||||
for (let builtinRoleId of EXTERNAL_BUILTIN_ROLE_IDS) {
|
for (let builtinRoleId of externalBuiltinRoles) {
|
||||||
const builtinRole = builtinRoles[builtinRoleId]
|
const builtinRole = builtinRoles[builtinRoleId]
|
||||||
const dbBuiltin = roles.filter(
|
const dbBuiltin = roles.filter(
|
||||||
dbRole =>
|
dbRole =>
|
||||||
|
@ -472,6 +489,18 @@ export async function getAllRoles(appId?: string): Promise<RoleDoc[]> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function shouldIncludePowerRole(db: Database) {
|
||||||
|
const app = await db.tryGet<App>(DocumentType.APP_METADATA)
|
||||||
|
const creationVersion = app?.creationVersion
|
||||||
|
if (!creationVersion || !semver.valid(creationVersion)) {
|
||||||
|
// Old apps don't have creationVersion, so we should include it for backward compatibility
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
const isGreaterThan3x = semver.gte(creationVersion, "3.0.0")
|
||||||
|
return !isGreaterThan3x
|
||||||
|
}
|
||||||
|
|
||||||
export class AccessController {
|
export class AccessController {
|
||||||
userHierarchies: { [key: string]: string[] }
|
userHierarchies: { [key: string]: string[] }
|
||||||
constructor() {
|
constructor() {
|
||||||
|
|
|
@ -23,12 +23,14 @@ import {
|
||||||
InternalSearchFilterOperator,
|
InternalSearchFilterOperator,
|
||||||
JsonFieldMetadata,
|
JsonFieldMetadata,
|
||||||
JsonTypes,
|
JsonTypes,
|
||||||
|
LogicalOperator,
|
||||||
Operation,
|
Operation,
|
||||||
prefixed,
|
prefixed,
|
||||||
QueryJson,
|
QueryJson,
|
||||||
QueryOptions,
|
QueryOptions,
|
||||||
RangeOperator,
|
RangeOperator,
|
||||||
RelationshipsJson,
|
RelationshipsJson,
|
||||||
|
SearchFilterKey,
|
||||||
SearchFilters,
|
SearchFilters,
|
||||||
SortOrder,
|
SortOrder,
|
||||||
SqlClient,
|
SqlClient,
|
||||||
|
@ -96,6 +98,22 @@ function isSqs(table: Table): boolean {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const allowEmptyRelationships: Record<SearchFilterKey, boolean> = {
|
||||||
|
[BasicOperator.EQUAL]: false,
|
||||||
|
[BasicOperator.NOT_EQUAL]: true,
|
||||||
|
[BasicOperator.EMPTY]: false,
|
||||||
|
[BasicOperator.NOT_EMPTY]: true,
|
||||||
|
[BasicOperator.FUZZY]: false,
|
||||||
|
[BasicOperator.STRING]: false,
|
||||||
|
[RangeOperator.RANGE]: false,
|
||||||
|
[ArrayOperator.CONTAINS]: false,
|
||||||
|
[ArrayOperator.NOT_CONTAINS]: true,
|
||||||
|
[ArrayOperator.CONTAINS_ANY]: false,
|
||||||
|
[ArrayOperator.ONE_OF]: false,
|
||||||
|
[LogicalOperator.AND]: false,
|
||||||
|
[LogicalOperator.OR]: false,
|
||||||
|
}
|
||||||
|
|
||||||
class InternalBuilder {
|
class InternalBuilder {
|
||||||
private readonly client: SqlClient
|
private readonly client: SqlClient
|
||||||
private readonly query: QueryJson
|
private readonly query: QueryJson
|
||||||
|
@ -405,31 +423,48 @@ class InternalBuilder {
|
||||||
|
|
||||||
addRelationshipForFilter(
|
addRelationshipForFilter(
|
||||||
query: Knex.QueryBuilder,
|
query: Knex.QueryBuilder,
|
||||||
|
allowEmptyRelationships: boolean,
|
||||||
filterKey: string,
|
filterKey: string,
|
||||||
whereCb: (query: Knex.QueryBuilder) => Knex.QueryBuilder
|
whereCb: (filterKey: string, query: Knex.QueryBuilder) => Knex.QueryBuilder
|
||||||
): Knex.QueryBuilder {
|
): Knex.QueryBuilder {
|
||||||
const mainKnex = this.knex
|
const mainKnex = this.knex
|
||||||
const { relationships, endpoint, tableAliases: aliases } = this.query
|
const { relationships, endpoint, tableAliases: aliases } = this.query
|
||||||
const tableName = endpoint.entityId
|
const tableName = endpoint.entityId
|
||||||
const fromAlias = aliases?.[tableName] || tableName
|
const fromAlias = aliases?.[tableName] || tableName
|
||||||
const matches = (possibleTable: string) =>
|
const matches = (value: string) =>
|
||||||
filterKey.startsWith(`${possibleTable}`)
|
filterKey.match(new RegExp(`^${value}\\.`))
|
||||||
if (!relationships) {
|
if (!relationships) {
|
||||||
return query
|
return query
|
||||||
}
|
}
|
||||||
for (const relationship of relationships) {
|
for (const relationship of relationships) {
|
||||||
const relatedTableName = relationship.tableName
|
const relatedTableName = relationship.tableName
|
||||||
const toAlias = aliases?.[relatedTableName] || relatedTableName
|
const toAlias = aliases?.[relatedTableName] || relatedTableName
|
||||||
|
|
||||||
|
const matchesTableName = matches(relatedTableName) || matches(toAlias)
|
||||||
|
const matchesRelationName = matches(relationship.column)
|
||||||
|
|
||||||
// this is the relationship which is being filtered
|
// this is the relationship which is being filtered
|
||||||
if (
|
if (
|
||||||
(matches(relatedTableName) || matches(toAlias)) &&
|
(matchesTableName || matchesRelationName) &&
|
||||||
relationship.to &&
|
relationship.to &&
|
||||||
relationship.tableName
|
relationship.tableName
|
||||||
) {
|
) {
|
||||||
let subQuery = mainKnex
|
const joinTable = mainKnex
|
||||||
.select(mainKnex.raw(1))
|
.select(mainKnex.raw(1))
|
||||||
.from({ [toAlias]: relatedTableName })
|
.from({ [toAlias]: relatedTableName })
|
||||||
|
let subQuery = joinTable.clone()
|
||||||
const manyToMany = validateManyToMany(relationship)
|
const manyToMany = validateManyToMany(relationship)
|
||||||
|
let updatedKey
|
||||||
|
|
||||||
|
if (!matchesTableName) {
|
||||||
|
updatedKey = filterKey.replace(
|
||||||
|
new RegExp(`^${relationship.column}.`),
|
||||||
|
`${aliases![relationship.tableName]}.`
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
updatedKey = filterKey
|
||||||
|
}
|
||||||
|
|
||||||
if (manyToMany) {
|
if (manyToMany) {
|
||||||
const throughAlias =
|
const throughAlias =
|
||||||
aliases?.[manyToMany.through] || relationship.through
|
aliases?.[manyToMany.through] || relationship.through
|
||||||
|
@ -440,7 +475,6 @@ class InternalBuilder {
|
||||||
subQuery = subQuery
|
subQuery = subQuery
|
||||||
// add a join through the junction table
|
// add a join through the junction table
|
||||||
.innerJoin(throughTable, function () {
|
.innerJoin(throughTable, function () {
|
||||||
// @ts-ignore
|
|
||||||
this.on(
|
this.on(
|
||||||
`${toAlias}.${manyToMany.toPrimary}`,
|
`${toAlias}.${manyToMany.toPrimary}`,
|
||||||
"=",
|
"=",
|
||||||
|
@ -460,18 +494,38 @@ class InternalBuilder {
|
||||||
if (this.client === SqlClient.SQL_LITE) {
|
if (this.client === SqlClient.SQL_LITE) {
|
||||||
subQuery = this.addJoinFieldCheck(subQuery, manyToMany)
|
subQuery = this.addJoinFieldCheck(subQuery, manyToMany)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
query = query.where(q => {
|
||||||
|
q.whereExists(whereCb(updatedKey, subQuery))
|
||||||
|
if (allowEmptyRelationships) {
|
||||||
|
q.orWhereNotExists(
|
||||||
|
joinTable.clone().innerJoin(throughTable, function () {
|
||||||
|
this.on(
|
||||||
|
`${fromAlias}.${manyToMany.fromPrimary}`,
|
||||||
|
"=",
|
||||||
|
`${throughAlias}.${manyToMany.from}`
|
||||||
|
)
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})
|
||||||
} else {
|
} else {
|
||||||
|
const toKey = `${toAlias}.${relationship.to}`
|
||||||
|
const foreignKey = `${fromAlias}.${relationship.from}`
|
||||||
// "join" to the main table, making sure the ID matches that of the main
|
// "join" to the main table, making sure the ID matches that of the main
|
||||||
subQuery = subQuery.where(
|
subQuery = subQuery.where(
|
||||||
`${toAlias}.${relationship.to}`,
|
toKey,
|
||||||
"=",
|
"=",
|
||||||
mainKnex.raw(
|
mainKnex.raw(this.quotedIdentifier(foreignKey))
|
||||||
this.quotedIdentifier(`${fromAlias}.${relationship.from}`)
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
query = query.where(q => {
|
||||||
|
q.whereExists(whereCb(updatedKey, subQuery.clone()))
|
||||||
|
if (allowEmptyRelationships) {
|
||||||
|
q.orWhereNotExists(subQuery)
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
query = query.whereExists(whereCb(subQuery))
|
|
||||||
break
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return query
|
return query
|
||||||
|
@ -502,6 +556,7 @@ class InternalBuilder {
|
||||||
}
|
}
|
||||||
function iterate(
|
function iterate(
|
||||||
structure: AnySearchFilter,
|
structure: AnySearchFilter,
|
||||||
|
operation: SearchFilterKey,
|
||||||
fn: (
|
fn: (
|
||||||
query: Knex.QueryBuilder,
|
query: Knex.QueryBuilder,
|
||||||
key: string,
|
key: string,
|
||||||
|
@ -558,9 +613,14 @@ class InternalBuilder {
|
||||||
if (allOr) {
|
if (allOr) {
|
||||||
query = query.or
|
query = query.or
|
||||||
}
|
}
|
||||||
query = builder.addRelationshipForFilter(query, updatedKey, q => {
|
query = builder.addRelationshipForFilter(
|
||||||
return handleRelationship(q, updatedKey, value)
|
query,
|
||||||
})
|
allowEmptyRelationships[operation],
|
||||||
|
updatedKey,
|
||||||
|
(updatedKey, q) => {
|
||||||
|
return handleRelationship(q, updatedKey, value)
|
||||||
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -592,7 +652,7 @@ class InternalBuilder {
|
||||||
return `[${value.join(",")}]`
|
return `[${value.join(",")}]`
|
||||||
}
|
}
|
||||||
if (this.client === SqlClient.POSTGRES) {
|
if (this.client === SqlClient.POSTGRES) {
|
||||||
iterate(mode, (q, key, value) => {
|
iterate(mode, ArrayOperator.CONTAINS, (q, key, value) => {
|
||||||
const wrap = any ? "" : "'"
|
const wrap = any ? "" : "'"
|
||||||
const op = any ? "\\?| array" : "@>"
|
const op = any ? "\\?| array" : "@>"
|
||||||
const fieldNames = key.split(/\./g)
|
const fieldNames = key.split(/\./g)
|
||||||
|
@ -610,7 +670,7 @@ class InternalBuilder {
|
||||||
this.client === SqlClient.MARIADB
|
this.client === SqlClient.MARIADB
|
||||||
) {
|
) {
|
||||||
const jsonFnc = any ? "JSON_OVERLAPS" : "JSON_CONTAINS"
|
const jsonFnc = any ? "JSON_OVERLAPS" : "JSON_CONTAINS"
|
||||||
iterate(mode, (q, key, value) => {
|
iterate(mode, ArrayOperator.CONTAINS, (q, key, value) => {
|
||||||
return q[rawFnc](
|
return q[rawFnc](
|
||||||
`${not}COALESCE(${jsonFnc}(${key}, '${stringifyArray(
|
`${not}COALESCE(${jsonFnc}(${key}, '${stringifyArray(
|
||||||
value
|
value
|
||||||
|
@ -619,7 +679,7 @@ class InternalBuilder {
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
const andOr = mode === filters?.containsAny ? " OR " : " AND "
|
const andOr = mode === filters?.containsAny ? " OR " : " AND "
|
||||||
iterate(mode, (q, key, value) => {
|
iterate(mode, ArrayOperator.CONTAINS, (q, key, value) => {
|
||||||
let statement = ""
|
let statement = ""
|
||||||
const identifier = this.quotedIdentifier(key)
|
const identifier = this.quotedIdentifier(key)
|
||||||
for (let i in value) {
|
for (let i in value) {
|
||||||
|
@ -673,6 +733,7 @@ class InternalBuilder {
|
||||||
const fnc = allOr ? "orWhereIn" : "whereIn"
|
const fnc = allOr ? "orWhereIn" : "whereIn"
|
||||||
iterate(
|
iterate(
|
||||||
filters.oneOf,
|
filters.oneOf,
|
||||||
|
ArrayOperator.ONE_OF,
|
||||||
(q, key: string, array) => {
|
(q, key: string, array) => {
|
||||||
if (this.client === SqlClient.ORACLE) {
|
if (this.client === SqlClient.ORACLE) {
|
||||||
key = this.convertClobs(key)
|
key = this.convertClobs(key)
|
||||||
|
@ -697,7 +758,7 @@ class InternalBuilder {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
if (filters.string) {
|
if (filters.string) {
|
||||||
iterate(filters.string, (q, key, value) => {
|
iterate(filters.string, BasicOperator.STRING, (q, key, value) => {
|
||||||
const fnc = allOr ? "orWhere" : "where"
|
const fnc = allOr ? "orWhere" : "where"
|
||||||
// postgres supports ilike, nothing else does
|
// postgres supports ilike, nothing else does
|
||||||
if (this.client === SqlClient.POSTGRES) {
|
if (this.client === SqlClient.POSTGRES) {
|
||||||
|
@ -712,10 +773,10 @@ class InternalBuilder {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
if (filters.fuzzy) {
|
if (filters.fuzzy) {
|
||||||
iterate(filters.fuzzy, like)
|
iterate(filters.fuzzy, BasicOperator.FUZZY, like)
|
||||||
}
|
}
|
||||||
if (filters.range) {
|
if (filters.range) {
|
||||||
iterate(filters.range, (q, key, value) => {
|
iterate(filters.range, RangeOperator.RANGE, (q, key, value) => {
|
||||||
const isEmptyObject = (val: any) => {
|
const isEmptyObject = (val: any) => {
|
||||||
return (
|
return (
|
||||||
val &&
|
val &&
|
||||||
|
@ -781,7 +842,7 @@ class InternalBuilder {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
if (filters.equal) {
|
if (filters.equal) {
|
||||||
iterate(filters.equal, (q, key, value) => {
|
iterate(filters.equal, BasicOperator.EQUAL, (q, key, value) => {
|
||||||
const fnc = allOr ? "orWhereRaw" : "whereRaw"
|
const fnc = allOr ? "orWhereRaw" : "whereRaw"
|
||||||
if (this.client === SqlClient.MS_SQL) {
|
if (this.client === SqlClient.MS_SQL) {
|
||||||
return q[fnc](
|
return q[fnc](
|
||||||
|
@ -801,7 +862,7 @@ class InternalBuilder {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
if (filters.notEqual) {
|
if (filters.notEqual) {
|
||||||
iterate(filters.notEqual, (q, key, value) => {
|
iterate(filters.notEqual, BasicOperator.NOT_EQUAL, (q, key, value) => {
|
||||||
const fnc = allOr ? "orWhereRaw" : "whereRaw"
|
const fnc = allOr ? "orWhereRaw" : "whereRaw"
|
||||||
if (this.client === SqlClient.MS_SQL) {
|
if (this.client === SqlClient.MS_SQL) {
|
||||||
return q[fnc](
|
return q[fnc](
|
||||||
|
@ -822,13 +883,13 @@ class InternalBuilder {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
if (filters.empty) {
|
if (filters.empty) {
|
||||||
iterate(filters.empty, (q, key) => {
|
iterate(filters.empty, BasicOperator.EMPTY, (q, key) => {
|
||||||
const fnc = allOr ? "orWhereNull" : "whereNull"
|
const fnc = allOr ? "orWhereNull" : "whereNull"
|
||||||
return q[fnc](key)
|
return q[fnc](key)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
if (filters.notEmpty) {
|
if (filters.notEmpty) {
|
||||||
iterate(filters.notEmpty, (q, key) => {
|
iterate(filters.notEmpty, BasicOperator.NOT_EMPTY, (q, key) => {
|
||||||
const fnc = allOr ? "orWhereNotNull" : "whereNotNull"
|
const fnc = allOr ? "orWhereNotNull" : "whereNotNull"
|
||||||
return q[fnc](key)
|
return q[fnc](key)
|
||||||
})
|
})
|
||||||
|
@ -1224,12 +1285,10 @@ class InternalBuilder {
|
||||||
})
|
})
|
||||||
: undefined
|
: undefined
|
||||||
if (!throughTable) {
|
if (!throughTable) {
|
||||||
// @ts-ignore
|
|
||||||
query = query.leftJoin(toTableWithSchema, function () {
|
query = query.leftJoin(toTableWithSchema, function () {
|
||||||
for (let relationship of columns) {
|
for (let relationship of columns) {
|
||||||
const from = relationship.from,
|
const from = relationship.from,
|
||||||
to = relationship.to
|
to = relationship.to
|
||||||
// @ts-ignore
|
|
||||||
this.orOn(`${fromAlias}.${from}`, "=", `${toAlias}.${to}`)
|
this.orOn(`${fromAlias}.${from}`, "=", `${toAlias}.${to}`)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -1240,7 +1299,6 @@ class InternalBuilder {
|
||||||
for (let relationship of columns) {
|
for (let relationship of columns) {
|
||||||
const fromPrimary = relationship.fromPrimary
|
const fromPrimary = relationship.fromPrimary
|
||||||
const from = relationship.from
|
const from = relationship.from
|
||||||
// @ts-ignore
|
|
||||||
this.orOn(
|
this.orOn(
|
||||||
`${fromAlias}.${fromPrimary}`,
|
`${fromAlias}.${fromPrimary}`,
|
||||||
"=",
|
"=",
|
||||||
|
@ -1252,7 +1310,6 @@ class InternalBuilder {
|
||||||
for (let relationship of columns) {
|
for (let relationship of columns) {
|
||||||
const toPrimary = relationship.toPrimary
|
const toPrimary = relationship.toPrimary
|
||||||
const to = relationship.to
|
const to = relationship.to
|
||||||
// @ts-ignore
|
|
||||||
this.orOn(`${toAlias}.${toPrimary}`, `${throughAlias}.${to}`)
|
this.orOn(`${toAlias}.${toPrimary}`, `${throughAlias}.${to}`)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
|
@ -59,6 +59,7 @@
|
||||||
bind:this={drawer}
|
bind:this={drawer}
|
||||||
title="Filtering"
|
title="Filtering"
|
||||||
on:drawerHide
|
on:drawerHide
|
||||||
|
on:drawerShow
|
||||||
on:drawerShow={() => {
|
on:drawerShow={() => {
|
||||||
// Reset to the currently available value.
|
// Reset to the currently available value.
|
||||||
localFilters = Helpers.cloneDeep(value)
|
localFilters = Helpers.cloneDeep(value)
|
||||||
|
|
|
@ -126,6 +126,9 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
const extendQuery = (defaultQuery, extensions) => {
|
const extendQuery = (defaultQuery, extensions) => {
|
||||||
|
if (!Object.keys(extensions).length) {
|
||||||
|
return defaultQuery
|
||||||
|
}
|
||||||
const extended = {
|
const extended = {
|
||||||
[LogicalOperator.AND]: {
|
[LogicalOperator.AND]: {
|
||||||
conditions: [
|
conditions: [
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
Subproject commit 61391549614b5ac153f267633d0aaea9b07f05c5
|
Subproject commit 1a749caba9c85aab2645e5d00db479eb53d3f80f
|
|
@ -208,9 +208,8 @@ export async function fetchAppDefinition(
|
||||||
export async function fetchAppPackage(
|
export async function fetchAppPackage(
|
||||||
ctx: UserCtx<void, FetchAppPackageResponse>
|
ctx: UserCtx<void, FetchAppPackageResponse>
|
||||||
) {
|
) {
|
||||||
const db = context.getAppDB()
|
|
||||||
const appId = context.getAppId()
|
const appId = context.getAppId()
|
||||||
let application = await db.get<App>(DocumentType.APP_METADATA)
|
const application = await sdk.applications.metadata.get()
|
||||||
const layouts = await getLayouts()
|
const layouts = await getLayouts()
|
||||||
let screens = await getScreens()
|
let screens = await getScreens()
|
||||||
const license = await licensing.cache.getCachedLicense()
|
const license = await licensing.cache.getCachedLicense()
|
||||||
|
@ -272,6 +271,7 @@ async function performAppCreate(ctx: UserCtx<CreateAppRequest, App>) {
|
||||||
path: ctx.request.body.file?.path,
|
path: ctx.request.body.file?.path,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const tenantId = tenancy.isMultiTenant() ? tenancy.getTenantId() : null
|
const tenantId = tenancy.isMultiTenant() ? tenancy.getTenantId() : null
|
||||||
const appId = generateDevAppID(generateAppID(tenantId))
|
const appId = generateDevAppID(generateAppID(tenantId))
|
||||||
|
|
||||||
|
@ -279,7 +279,7 @@ async function performAppCreate(ctx: UserCtx<CreateAppRequest, App>) {
|
||||||
const instance = await createInstance(appId, instanceConfig)
|
const instance = await createInstance(appId, instanceConfig)
|
||||||
const db = context.getAppDB()
|
const db = context.getAppDB()
|
||||||
|
|
||||||
let newApplication: App = {
|
const newApplication: App = {
|
||||||
_id: DocumentType.APP_METADATA,
|
_id: DocumentType.APP_METADATA,
|
||||||
_rev: undefined,
|
_rev: undefined,
|
||||||
appId,
|
appId,
|
||||||
|
@ -310,12 +310,18 @@ async function performAppCreate(ctx: UserCtx<CreateAppRequest, App>) {
|
||||||
disableUserMetadata: true,
|
disableUserMetadata: true,
|
||||||
skeletonLoader: true,
|
skeletonLoader: true,
|
||||||
},
|
},
|
||||||
|
creationVersion: undefined,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const isImport = !!instanceConfig.file
|
||||||
|
if (!isImport) {
|
||||||
|
newApplication.creationVersion = envCore.VERSION
|
||||||
|
}
|
||||||
|
|
||||||
|
const existing = await sdk.applications.metadata.tryGet()
|
||||||
// If we used a template or imported an app there will be an existing doc.
|
// If we used a template or imported an app there will be an existing doc.
|
||||||
// Fetch and migrate some metadata from the existing app.
|
// Fetch and migrate some metadata from the existing app.
|
||||||
try {
|
if (existing) {
|
||||||
const existing: App = await db.get(DocumentType.APP_METADATA)
|
|
||||||
const keys: (keyof App)[] = [
|
const keys: (keyof App)[] = [
|
||||||
"_rev",
|
"_rev",
|
||||||
"navigation",
|
"navigation",
|
||||||
|
@ -323,6 +329,7 @@ async function performAppCreate(ctx: UserCtx<CreateAppRequest, App>) {
|
||||||
"customTheme",
|
"customTheme",
|
||||||
"icon",
|
"icon",
|
||||||
"snippets",
|
"snippets",
|
||||||
|
"creationVersion",
|
||||||
]
|
]
|
||||||
keys.forEach(key => {
|
keys.forEach(key => {
|
||||||
if (existing[key]) {
|
if (existing[key]) {
|
||||||
|
@ -340,14 +347,10 @@ async function performAppCreate(ctx: UserCtx<CreateAppRequest, App>) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Migrate navigation settings and screens if required
|
// Migrate navigation settings and screens if required
|
||||||
if (existing) {
|
const navigation = await migrateAppNavigation()
|
||||||
const navigation = await migrateAppNavigation()
|
if (navigation) {
|
||||||
if (navigation) {
|
newApplication.navigation = navigation
|
||||||
newApplication.navigation = navigation
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} catch (err) {
|
|
||||||
// Nothing to do
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const response = await db.put(newApplication, { force: true })
|
const response = await db.put(newApplication, { force: true })
|
||||||
|
@ -489,8 +492,7 @@ export async function update(
|
||||||
|
|
||||||
export async function updateClient(ctx: UserCtx) {
|
export async function updateClient(ctx: UserCtx) {
|
||||||
// Get current app version
|
// Get current app version
|
||||||
const db = context.getAppDB()
|
const application = await sdk.applications.metadata.get()
|
||||||
const application = await db.get<App>(DocumentType.APP_METADATA)
|
|
||||||
const currentVersion = application.version
|
const currentVersion = application.version
|
||||||
|
|
||||||
let manifest
|
let manifest
|
||||||
|
@ -518,8 +520,7 @@ export async function updateClient(ctx: UserCtx) {
|
||||||
|
|
||||||
export async function revertClient(ctx: UserCtx) {
|
export async function revertClient(ctx: UserCtx) {
|
||||||
// Check app can be reverted
|
// Check app can be reverted
|
||||||
const db = context.getAppDB()
|
const application = await sdk.applications.metadata.get()
|
||||||
const application = await db.get<App>(DocumentType.APP_METADATA)
|
|
||||||
if (!application.revertableVersion) {
|
if (!application.revertableVersion) {
|
||||||
ctx.throw(400, "There is no version to revert to")
|
ctx.throw(400, "There is no version to revert to")
|
||||||
}
|
}
|
||||||
|
@ -577,7 +578,7 @@ async function destroyApp(ctx: UserCtx) {
|
||||||
|
|
||||||
const db = dbCore.getDB(devAppId)
|
const db = dbCore.getDB(devAppId)
|
||||||
// standard app deletion flow
|
// standard app deletion flow
|
||||||
const app = await db.get<App>(DocumentType.APP_METADATA)
|
const app = await sdk.applications.metadata.get()
|
||||||
const result = await db.destroy()
|
const result = await db.destroy()
|
||||||
await quotas.removeApp()
|
await quotas.removeApp()
|
||||||
await events.app.deleted(app)
|
await events.app.deleted(app)
|
||||||
|
@ -728,7 +729,7 @@ export async function updateAppPackage(
|
||||||
) {
|
) {
|
||||||
return context.doInAppContext(appId, async () => {
|
return context.doInAppContext(appId, async () => {
|
||||||
const db = context.getAppDB()
|
const db = context.getAppDB()
|
||||||
const application = await db.get<App>(DocumentType.APP_METADATA)
|
const application = await sdk.applications.metadata.get()
|
||||||
|
|
||||||
const newAppPackage: App = { ...application, ...appPackage }
|
const newAppPackage: App = { ...application, ...appPackage }
|
||||||
if (appPackage._rev !== application._rev) {
|
if (appPackage._rev !== application._rev) {
|
||||||
|
@ -754,7 +755,7 @@ export async function setRevertableVersion(
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const db = context.getAppDB()
|
const db = context.getAppDB()
|
||||||
const app = await db.get<App>(DocumentType.APP_METADATA)
|
const app = await sdk.applications.metadata.get()
|
||||||
app.revertableVersion = ctx.request.body.revertableVersion
|
app.revertableVersion = ctx.request.body.revertableVersion
|
||||||
await db.put(app)
|
await db.put(app)
|
||||||
|
|
||||||
|
@ -763,7 +764,7 @@ export async function setRevertableVersion(
|
||||||
|
|
||||||
async function migrateAppNavigation() {
|
async function migrateAppNavigation() {
|
||||||
const db = context.getAppDB()
|
const db = context.getAppDB()
|
||||||
const existing: App = await db.get(DocumentType.APP_METADATA)
|
const existing = await sdk.applications.metadata.get()
|
||||||
const layouts: Layout[] = await getLayouts()
|
const layouts: Layout[] = await getLayouts()
|
||||||
const screens: Screen[] = await getScreens()
|
const screens: Screen[] = await getScreens()
|
||||||
|
|
||||||
|
|
|
@ -174,9 +174,9 @@ export class ExternalRequest<T extends Operation> {
|
||||||
if (!opts.datasource) {
|
if (!opts.datasource) {
|
||||||
if (sdk.views.isView(source)) {
|
if (sdk.views.isView(source)) {
|
||||||
const table = await sdk.views.getTable(source.id)
|
const table = await sdk.views.getTable(source.id)
|
||||||
opts.datasource = await sdk.datasources.get(table.sourceId!)
|
opts.datasource = await sdk.datasources.get(table.sourceId)
|
||||||
} else {
|
} else {
|
||||||
opts.datasource = await sdk.datasources.get(source.sourceId!)
|
opts.datasource = await sdk.datasources.get(source.sourceId)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -205,18 +205,6 @@ export class ExternalRequest<T extends Operation> {
|
||||||
filters: SearchFilters,
|
filters: SearchFilters,
|
||||||
table: Table
|
table: Table
|
||||||
): SearchFilters {
|
): SearchFilters {
|
||||||
// replace any relationship columns initially, table names and relationship column names are acceptable
|
|
||||||
const relationshipColumns = sdk.rows.filters.getRelationshipColumns(table)
|
|
||||||
filters = sdk.rows.filters.updateFilterKeys(
|
|
||||||
filters,
|
|
||||||
relationshipColumns.map(({ name, definition }) => {
|
|
||||||
const { tableName } = breakExternalTableId(definition.tableId)
|
|
||||||
return {
|
|
||||||
original: name,
|
|
||||||
updated: tableName,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
)
|
|
||||||
const primary = table.primary
|
const primary = table.primary
|
||||||
// if passed in array need to copy for shifting etc
|
// if passed in array need to copy for shifting etc
|
||||||
let idCopy: undefined | string | any[] = cloneDeep(id)
|
let idCopy: undefined | string | any[] = cloneDeep(id)
|
||||||
|
|
|
@ -15,13 +15,16 @@ import {
|
||||||
ExportRowsResponse,
|
ExportRowsResponse,
|
||||||
FieldType,
|
FieldType,
|
||||||
GetRowResponse,
|
GetRowResponse,
|
||||||
|
isRelationshipField,
|
||||||
PatchRowRequest,
|
PatchRowRequest,
|
||||||
PatchRowResponse,
|
PatchRowResponse,
|
||||||
Row,
|
Row,
|
||||||
RowAttachment,
|
RowAttachment,
|
||||||
RowSearchParams,
|
RowSearchParams,
|
||||||
|
SearchFilters,
|
||||||
SearchRowRequest,
|
SearchRowRequest,
|
||||||
SearchRowResponse,
|
SearchRowResponse,
|
||||||
|
Table,
|
||||||
UserCtx,
|
UserCtx,
|
||||||
ValidateResponse,
|
ValidateResponse,
|
||||||
} from "@budibase/types"
|
} from "@budibase/types"
|
||||||
|
@ -33,6 +36,7 @@ import sdk from "../../../sdk"
|
||||||
import * as exporters from "../view/exporters"
|
import * as exporters from "../view/exporters"
|
||||||
import { Format } from "../view/exporters"
|
import { Format } from "../view/exporters"
|
||||||
import { apiFileReturn } from "../../../utilities/fileSystem"
|
import { apiFileReturn } from "../../../utilities/fileSystem"
|
||||||
|
import { dataFilters } from "@budibase/shared-core"
|
||||||
|
|
||||||
export * as views from "./views"
|
export * as views from "./views"
|
||||||
|
|
||||||
|
@ -211,12 +215,15 @@ export async function search(ctx: Ctx<SearchRowRequest, SearchRowResponse>) {
|
||||||
|
|
||||||
await context.ensureSnippetContext(true)
|
await context.ensureSnippetContext(true)
|
||||||
|
|
||||||
const enrichedQuery = await utils.enrichSearchContext(
|
let { query } = ctx.request.body
|
||||||
{ ...ctx.request.body.query },
|
if (query) {
|
||||||
{
|
const allTables = await sdk.tables.getAllTables()
|
||||||
user: sdk.users.getUserContextBindings(ctx.user),
|
query = replaceTableNamesInFilters(tableId, query, allTables)
|
||||||
}
|
}
|
||||||
)
|
|
||||||
|
let enrichedQuery: SearchFilters = await utils.enrichSearchContext(query, {
|
||||||
|
user: sdk.users.getUserContextBindings(ctx.user),
|
||||||
|
})
|
||||||
|
|
||||||
const searchParams: RowSearchParams = {
|
const searchParams: RowSearchParams = {
|
||||||
...ctx.request.body,
|
...ctx.request.body,
|
||||||
|
@ -229,6 +236,47 @@ export async function search(ctx: Ctx<SearchRowRequest, SearchRowResponse>) {
|
||||||
ctx.body = await sdk.rows.search(searchParams)
|
ctx.body = await sdk.rows.search(searchParams)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function replaceTableNamesInFilters(
|
||||||
|
tableId: string,
|
||||||
|
filters: SearchFilters,
|
||||||
|
allTables: Table[]
|
||||||
|
): SearchFilters {
|
||||||
|
for (const filter of Object.values(filters)) {
|
||||||
|
for (const key of Object.keys(filter)) {
|
||||||
|
const matches = key.match(`^(?<relation>.+)\\.(?<field>.+)`)
|
||||||
|
|
||||||
|
const relation = matches?.groups?.["relation"]
|
||||||
|
const field = matches?.groups?.["field"]
|
||||||
|
|
||||||
|
if (!relation || !field) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
const table = allTables.find(r => r._id === tableId)!
|
||||||
|
if (Object.values(table.schema).some(f => f.name === relation)) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
const matchedTable = allTables.find(t => t.name === relation)
|
||||||
|
const relationship = Object.values(table.schema).find(
|
||||||
|
f => isRelationshipField(f) && f.tableId === matchedTable?._id
|
||||||
|
)
|
||||||
|
if (!relationship) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
const updatedField = `${relationship.name}.${field}`
|
||||||
|
if (updatedField && updatedField !== key) {
|
||||||
|
filter[updatedField] = filter[key]
|
||||||
|
delete filter[key]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return dataFilters.recurseLogicalOperators(filters, (f: SearchFilters) => {
|
||||||
|
return replaceTableNamesInFilters(tableId, f, allTables)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
export async function validate(ctx: Ctx<Row, ValidateResponse>) {
|
export async function validate(ctx: Ctx<Row, ValidateResponse>) {
|
||||||
const source = await utils.getSource(ctx)
|
const source = await utils.getSource(ctx)
|
||||||
const table = await utils.getTableFromSource(source)
|
const table = await utils.getTableFromSource(source)
|
||||||
|
|
|
@ -2364,12 +2364,16 @@ describe.each([
|
||||||
// It also can't work for in-memory searching because the related table name
|
// It also can't work for in-memory searching because the related table name
|
||||||
// isn't available.
|
// isn't available.
|
||||||
!isInMemory &&
|
!isInMemory &&
|
||||||
describe("relations", () => {
|
describe.each([
|
||||||
|
RelationshipType.ONE_TO_MANY,
|
||||||
|
RelationshipType.MANY_TO_ONE,
|
||||||
|
RelationshipType.MANY_TO_MANY,
|
||||||
|
])("relations (%s)", relationshipType => {
|
||||||
let productCategoryTable: Table, productCatRows: Row[]
|
let productCategoryTable: Table, productCatRows: Row[]
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
const { relatedTable, tableId } = await basicRelationshipTables(
|
const { relatedTable, tableId } = await basicRelationshipTables(
|
||||||
RelationshipType.ONE_TO_MANY
|
relationshipType
|
||||||
)
|
)
|
||||||
tableOrViewId = tableId
|
tableOrViewId = tableId
|
||||||
productCategoryTable = relatedTable
|
productCategoryTable = relatedTable
|
||||||
|
@ -2466,7 +2470,10 @@ describe.each([
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
}).toContainExactly([
|
}).toContainExactly([
|
||||||
{ name: "foo", productCat: [{ _id: productCatRows[0]._id }] },
|
{
|
||||||
|
name: "foo",
|
||||||
|
productCat: [{ _id: productCatRows[0]._id }],
|
||||||
|
},
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
@ -2544,7 +2551,7 @@ describe.each([
|
||||||
}).toContainExactly([
|
}).toContainExactly([
|
||||||
{ name: "foo", productCat: [{ _id: productCatRows[0]._id }] },
|
{ name: "foo", productCat: [{ _id: productCatRows[0]._id }] },
|
||||||
{ name: "bar", productCat: [{ _id: productCatRows[1]._id }] },
|
{ name: "bar", productCat: [{ _id: productCatRows[1]._id }] },
|
||||||
// { name: "baz", productCat: undefined }, // TODO
|
{ name: "baz", productCat: undefined },
|
||||||
])
|
])
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -2566,7 +2573,10 @@ describe.each([
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
}).toContainExactly([
|
}).toContainExactly([
|
||||||
{ name: "foo", productCat: [{ _id: productCatRows[0]._id }] },
|
{
|
||||||
|
name: "foo",
|
||||||
|
productCat: [{ _id: productCatRows[0]._id }],
|
||||||
|
},
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
@ -2590,9 +2600,15 @@ describe.each([
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
}).toContainExactly([
|
}).toContainExactly([
|
||||||
{ name: "foo", productCat: [{ _id: productCatRows[0]._id }] },
|
{
|
||||||
{ name: "bar", productCat: [{ _id: productCatRows[1]._id }] },
|
name: "foo",
|
||||||
// { name: "baz", productCat: undefined }, // TODO
|
productCat: [{ _id: productCatRows[0]._id }],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "bar",
|
||||||
|
productCat: [{ _id: productCatRows[1]._id }],
|
||||||
|
},
|
||||||
|
{ name: "baz", productCat: undefined },
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
@ -2616,7 +2632,7 @@ describe.each([
|
||||||
}).toContainExactly([
|
}).toContainExactly([
|
||||||
{ name: "foo", productCat: [{ _id: productCatRows[0]._id }] },
|
{ name: "foo", productCat: [{ _id: productCatRows[0]._id }] },
|
||||||
{ name: "bar", productCat: [{ _id: productCatRows[1]._id }] },
|
{ name: "bar", productCat: [{ _id: productCatRows[1]._id }] },
|
||||||
// { name: "baz", productCat: undefined }, // TODO
|
{ name: "baz", productCat: undefined },
|
||||||
])
|
])
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
@ -2624,10 +2640,13 @@ describe.each([
|
||||||
})
|
})
|
||||||
|
|
||||||
isSql &&
|
isSql &&
|
||||||
describe("big relations", () => {
|
describe.each([
|
||||||
|
RelationshipType.MANY_TO_ONE,
|
||||||
|
RelationshipType.MANY_TO_MANY,
|
||||||
|
])("big relations (%s)", relationshipType => {
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
const { relatedTable, tableId } = await basicRelationshipTables(
|
const { relatedTable, tableId } = await basicRelationshipTables(
|
||||||
RelationshipType.MANY_TO_ONE
|
relationshipType
|
||||||
)
|
)
|
||||||
tableOrViewId = tableId
|
tableOrViewId = tableId
|
||||||
const mainRow = await config.api.row.save(tableOrViewId, {
|
const mainRow = await config.api.row.save(tableOrViewId, {
|
||||||
|
@ -2653,7 +2672,8 @@ describe.each([
|
||||||
expect(response.rows[0].productCat).toBeArrayOfSize(11)
|
expect(response.rows[0].productCat).toBeArrayOfSize(11)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
;(isSqs || isLucene) &&
|
|
||||||
|
isSql &&
|
||||||
describe("relations to same table", () => {
|
describe("relations to same table", () => {
|
||||||
let relatedTable: string, relatedRows: Row[]
|
let relatedTable: string, relatedRows: Row[]
|
||||||
|
|
||||||
|
@ -2695,6 +2715,11 @@ describe.each([
|
||||||
related1: [relatedRows[2]._id!],
|
related1: [relatedRows[2]._id!],
|
||||||
related2: [relatedRows[3]._id!],
|
related2: [relatedRows[3]._id!],
|
||||||
}),
|
}),
|
||||||
|
config.api.row.save(tableOrViewId, {
|
||||||
|
name: "test3",
|
||||||
|
related1: [relatedRows[1]._id],
|
||||||
|
related2: [relatedRows[2]._id!],
|
||||||
|
}),
|
||||||
])
|
])
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -2712,42 +2737,59 @@ describe.each([
|
||||||
related1: [{ _id: relatedRows[2]._id }],
|
related1: [{ _id: relatedRows[2]._id }],
|
||||||
related2: [{ _id: relatedRows[3]._id }],
|
related2: [{ _id: relatedRows[3]._id }],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "test3",
|
||||||
|
related1: [{ _id: relatedRows[1]._id }],
|
||||||
|
related2: [{ _id: relatedRows[2]._id }],
|
||||||
|
},
|
||||||
])
|
])
|
||||||
})
|
})
|
||||||
|
|
||||||
isSqs &&
|
it("should be able to filter via the first relation field with equal", async () => {
|
||||||
it("should be able to filter down to second row with equal", async () => {
|
await expectSearch({
|
||||||
await expectSearch({
|
query: {
|
||||||
query: {
|
equal: {
|
||||||
equal: {
|
["related1.name"]: "baz",
|
||||||
["related1.name"]: "baz",
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
}).toContainExactly([
|
},
|
||||||
{
|
}).toContainExactly([
|
||||||
name: "test2",
|
{
|
||||||
related1: [{ _id: relatedRows[2]._id }],
|
name: "test2",
|
||||||
},
|
related1: [{ _id: relatedRows[2]._id }],
|
||||||
])
|
},
|
||||||
})
|
])
|
||||||
|
})
|
||||||
|
|
||||||
isSqs &&
|
it("should be able to filter via the second relation field with not equal", async () => {
|
||||||
it("should be able to filter down to first row with not equal", async () => {
|
await expectSearch({
|
||||||
await expectSearch({
|
query: {
|
||||||
query: {
|
notEqual: {
|
||||||
notEqual: {
|
["1:related2.name"]: "foo",
|
||||||
["1:related2.name"]: "bar",
|
["2:related2.name"]: "baz",
|
||||||
["2:related2.name"]: "baz",
|
["3:related2.name"]: "boo",
|
||||||
["3:related2.name"]: "boo",
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
}).toContainExactly([
|
},
|
||||||
{
|
}).toContainExactly([
|
||||||
name: "test",
|
{
|
||||||
related1: [{ _id: relatedRows[0]._id }],
|
name: "test",
|
||||||
|
},
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should be able to filter on both fields", async () => {
|
||||||
|
await expectSearch({
|
||||||
|
query: {
|
||||||
|
notEqual: {
|
||||||
|
["related1.name"]: "foo",
|
||||||
|
["related2.name"]: "baz",
|
||||||
},
|
},
|
||||||
])
|
},
|
||||||
})
|
}).toContainExactly([
|
||||||
|
{
|
||||||
|
name: "test2",
|
||||||
|
},
|
||||||
|
])
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
isInternal &&
|
isInternal &&
|
||||||
|
|
|
@ -78,8 +78,7 @@ describe("Captures of real examples", () => {
|
||||||
bindings: ["assembling", primaryLimit, relationshipLimit],
|
bindings: ["assembling", primaryLimit, relationshipLimit],
|
||||||
sql: expect.stringContaining(
|
sql: expect.stringContaining(
|
||||||
multiline(
|
multiline(
|
||||||
`where exists (select 1 from "tasks" as "b" inner join "products_tasks" as "c" on "b"."taskid" = "c"."taskid" where "c"."productid" = "a"."productid"
|
`where (exists (select 1 from "tasks" as "b" inner join "products_tasks" as "c" on "b"."taskid" = "c"."taskid" where "c"."productid" = "a"."productid" and (COALESCE("b"."taskname" = $1, FALSE)))`
|
||||||
and (COALESCE("b"."taskname" = $1, FALSE))`
|
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
})
|
})
|
||||||
|
@ -133,6 +132,8 @@ describe("Captures of real examples", () => {
|
||||||
|
|
||||||
expect(query).toEqual({
|
expect(query).toEqual({
|
||||||
bindings: [
|
bindings: [
|
||||||
|
rangeValue.low,
|
||||||
|
rangeValue.high,
|
||||||
rangeValue.low,
|
rangeValue.low,
|
||||||
rangeValue.high,
|
rangeValue.high,
|
||||||
equalValue,
|
equalValue,
|
||||||
|
@ -144,7 +145,7 @@ describe("Captures of real examples", () => {
|
||||||
],
|
],
|
||||||
sql: expect.stringContaining(
|
sql: expect.stringContaining(
|
||||||
multiline(
|
multiline(
|
||||||
`where exists (select 1 from "persons" as "c" where "c"."personid" = "a"."executorid" and ("c"."year" between $1 and $2))`
|
`where (exists (select 1 from "persons" as "c" where "c"."personid" = "a"."executorid" and ("c"."year" between $1 and $2))) and (exists (select 1 from "persons" as "c" where "c"."personid" = "a"."qaid" and ("c"."year" between $3 and $4))) and (exists (select 1 from "products" as "b" inner join "products_tasks" as "d" on "b"."productid" = "d"."productid" where "d"."taskid" = "a"."taskid" and (COALESCE("b"."productname" = $5, FALSE))))`
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
})
|
})
|
||||||
|
|
|
@ -2,10 +2,12 @@ import * as sync from "./sync"
|
||||||
import * as utils from "./utils"
|
import * as utils from "./utils"
|
||||||
import * as applications from "./applications"
|
import * as applications from "./applications"
|
||||||
import * as imports from "./import"
|
import * as imports from "./import"
|
||||||
|
import * as metadata from "./metadata"
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
...sync,
|
...sync,
|
||||||
...utils,
|
...utils,
|
||||||
...applications,
|
...applications,
|
||||||
...imports,
|
...imports,
|
||||||
|
metadata,
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,18 @@
|
||||||
|
import { context, DocumentType } from "@budibase/backend-core"
|
||||||
|
import { App } from "@budibase/types"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @deprecated the plan is to get everything using `tryGet` instead, then rename
|
||||||
|
* `tryGet` to `get`.
|
||||||
|
*/
|
||||||
|
export async function get() {
|
||||||
|
const db = context.getAppDB()
|
||||||
|
const application = await db.get<App>(DocumentType.APP_METADATA)
|
||||||
|
return application
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function tryGet() {
|
||||||
|
const db = context.getAppDB()
|
||||||
|
const application = await db.tryGet<App>(DocumentType.APP_METADATA)
|
||||||
|
return application
|
||||||
|
}
|
|
@ -3,14 +3,12 @@ import * as rows from "./rows"
|
||||||
import * as search from "./search"
|
import * as search from "./search"
|
||||||
import * as utils from "./utils"
|
import * as utils from "./utils"
|
||||||
import * as external from "./external"
|
import * as external from "./external"
|
||||||
import * as filters from "./search/filters"
|
|
||||||
import AliasTables from "./sqlAlias"
|
import AliasTables from "./sqlAlias"
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
...attachments,
|
...attachments,
|
||||||
...rows,
|
...rows,
|
||||||
...search,
|
...search,
|
||||||
filters,
|
|
||||||
utils,
|
utils,
|
||||||
external,
|
external,
|
||||||
AliasTables,
|
AliasTables,
|
||||||
|
|
|
@ -1,65 +0,0 @@
|
||||||
import {
|
|
||||||
FieldType,
|
|
||||||
RelationshipFieldMetadata,
|
|
||||||
SearchFilters,
|
|
||||||
Table,
|
|
||||||
} from "@budibase/types"
|
|
||||||
import { isPlainObject } from "lodash"
|
|
||||||
import { dataFilters } from "@budibase/shared-core"
|
|
||||||
|
|
||||||
export function getRelationshipColumns(table: Table): {
|
|
||||||
name: string
|
|
||||||
definition: RelationshipFieldMetadata
|
|
||||||
}[] {
|
|
||||||
// performing this with a for loop rather than an array filter improves
|
|
||||||
// type guarding, as no casts are required
|
|
||||||
const linkEntries: [string, RelationshipFieldMetadata][] = []
|
|
||||||
for (let entry of Object.entries(table.schema)) {
|
|
||||||
if (entry[1].type === FieldType.LINK) {
|
|
||||||
const linkColumn: RelationshipFieldMetadata = entry[1]
|
|
||||||
linkEntries.push([entry[0], linkColumn])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return linkEntries.map(entry => ({
|
|
||||||
name: entry[0],
|
|
||||||
definition: entry[1],
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getTableIDList(
|
|
||||||
tables: Table[]
|
|
||||||
): { name: string; id: string }[] {
|
|
||||||
return tables
|
|
||||||
.filter(table => table.originalName && table._id)
|
|
||||||
.map(table => ({ id: table._id!, name: table.originalName! }))
|
|
||||||
}
|
|
||||||
|
|
||||||
export function updateFilterKeys(
|
|
||||||
filters: SearchFilters,
|
|
||||||
updates: { original: string; updated: string }[]
|
|
||||||
): SearchFilters {
|
|
||||||
const makeFilterKeyRegex = (str: string) =>
|
|
||||||
new RegExp(`^${str}\\.|:${str}\\.`)
|
|
||||||
for (let filter of Object.values(filters)) {
|
|
||||||
if (!isPlainObject(filter)) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
for (let [key, keyFilter] of Object.entries(filter)) {
|
|
||||||
if (keyFilter === "") {
|
|
||||||
delete filter[key]
|
|
||||||
}
|
|
||||||
const possibleKey = updates.find(({ original }) =>
|
|
||||||
key.match(makeFilterKeyRegex(original))
|
|
||||||
)
|
|
||||||
if (possibleKey && possibleKey.original !== possibleKey.updated) {
|
|
||||||
// only replace the first, not replaceAll
|
|
||||||
filter[key.replace(possibleKey.original, possibleKey.updated)] =
|
|
||||||
filter[key]
|
|
||||||
delete filter[key]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return dataFilters.recurseLogicalOperators(filters, (f: SearchFilters) => {
|
|
||||||
return updateFilterKeys(f, updates)
|
|
||||||
})
|
|
||||||
}
|
|
|
@ -39,11 +39,6 @@ import AliasTables from "../../sqlAlias"
|
||||||
import { outputProcessing } from "../../../../../utilities/rowProcessor"
|
import { outputProcessing } from "../../../../../utilities/rowProcessor"
|
||||||
import pick from "lodash/pick"
|
import pick from "lodash/pick"
|
||||||
import { processRowCountResponse } from "../../utils"
|
import { processRowCountResponse } from "../../utils"
|
||||||
import {
|
|
||||||
getRelationshipColumns,
|
|
||||||
getTableIDList,
|
|
||||||
updateFilterKeys,
|
|
||||||
} from "../filters"
|
|
||||||
import {
|
import {
|
||||||
dataFilters,
|
dataFilters,
|
||||||
helpers,
|
helpers,
|
||||||
|
@ -133,31 +128,7 @@ async function buildInternalFieldList(
|
||||||
return [...new Set(fieldList)]
|
return [...new Set(fieldList)]
|
||||||
}
|
}
|
||||||
|
|
||||||
function cleanupFilters(
|
function cleanupFilters(filters: SearchFilters, allTables: Table[]) {
|
||||||
filters: SearchFilters,
|
|
||||||
table: Table,
|
|
||||||
allTables: Table[]
|
|
||||||
) {
|
|
||||||
// get a list of all relationship columns in the table for updating
|
|
||||||
const relationshipColumns = getRelationshipColumns(table)
|
|
||||||
// get table names to ID map for relationships
|
|
||||||
const tableNameToID = getTableIDList(allTables)
|
|
||||||
// all should be applied at once
|
|
||||||
filters = updateFilterKeys(
|
|
||||||
filters,
|
|
||||||
relationshipColumns
|
|
||||||
.map(({ name, definition }) => ({
|
|
||||||
original: name,
|
|
||||||
updated: definition.tableId,
|
|
||||||
}))
|
|
||||||
.concat(
|
|
||||||
tableNameToID.map(({ name, id }) => ({
|
|
||||||
original: name,
|
|
||||||
updated: id,
|
|
||||||
}))
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
// generate a map of all possible column names (these can be duplicated across tables
|
// generate a map of all possible column names (these can be duplicated across tables
|
||||||
// the map of them will always be the same
|
// the map of them will always be the same
|
||||||
const userColumnMap: Record<string, string> = {}
|
const userColumnMap: Record<string, string> = {}
|
||||||
|
@ -356,7 +327,7 @@ export async function search(
|
||||||
const relationships = buildInternalRelationships(table, allTables)
|
const relationships = buildInternalRelationships(table, allTables)
|
||||||
|
|
||||||
const searchFilters: SearchFilters = {
|
const searchFilters: SearchFilters = {
|
||||||
...cleanupFilters(query, table, allTables),
|
...cleanupFilters(query, allTables),
|
||||||
documentType: DocumentType.ROW,
|
documentType: DocumentType.ROW,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -90,7 +90,11 @@ export async function getExternalTable(
|
||||||
if (!entities[tableName]) {
|
if (!entities[tableName]) {
|
||||||
throw new Error(`Unable to find table named "${tableName}"`)
|
throw new Error(`Unable to find table named "${tableName}"`)
|
||||||
}
|
}
|
||||||
return processTable(entities[tableName])
|
const table = await processTable(entities[tableName])
|
||||||
|
if (!table.sourceId) {
|
||||||
|
table.sourceId = datasourceId
|
||||||
|
}
|
||||||
|
return table
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getTable(tableId: string): Promise<Table> {
|
export async function getTable(tableId: string): Promise<Table> {
|
||||||
|
|
|
@ -27,6 +27,7 @@ export interface App extends Document {
|
||||||
usedPlugins?: Plugin[]
|
usedPlugins?: Plugin[]
|
||||||
upgradableVersion?: string
|
upgradableVersion?: string
|
||||||
snippets?: Snippet[]
|
snippets?: Snippet[]
|
||||||
|
creationVersion?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AppInstance {
|
export interface AppInstance {
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { structures, TestConfiguration } from "../../../../tests"
|
import { structures, TestConfiguration } from "../../../../tests"
|
||||||
import { context, db, permissions, roles } from "@budibase/backend-core"
|
import { context, db, permissions, roles } from "@budibase/backend-core"
|
||||||
import { Database } from "@budibase/types"
|
import { App, Database } from "@budibase/types"
|
||||||
|
|
||||||
jest.mock("@budibase/backend-core", () => {
|
jest.mock("@budibase/backend-core", () => {
|
||||||
const core = jest.requireActual("@budibase/backend-core")
|
const core = jest.requireActual("@budibase/backend-core")
|
||||||
|
@ -30,6 +30,14 @@ async function addAppMetadata() {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function updateAppMetadata(update: Partial<Omit<App, "_id" | "_rev">>) {
|
||||||
|
const app = await appDb.get("app_metadata")
|
||||||
|
await appDb.put({
|
||||||
|
...app,
|
||||||
|
...update,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
describe("/api/global/roles", () => {
|
describe("/api/global/roles", () => {
|
||||||
const config = new TestConfiguration()
|
const config = new TestConfiguration()
|
||||||
|
|
||||||
|
@ -69,6 +77,53 @@ describe("/api/global/roles", () => {
|
||||||
expect(res.body[appId].roles.length).toEqual(5)
|
expect(res.body[appId].roles.length).toEqual(5)
|
||||||
expect(res.body[appId].roles.map((r: any) => r._id)).toContain(ROLE_NAME)
|
expect(res.body[appId].roles.map((r: any) => r._id)).toContain(ROLE_NAME)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it.each(["3.0.0", "3.0.1", "3.1.0", "3.0.0+2146.b125a7c"])(
|
||||||
|
"exclude POWER roles after v3 (%s)",
|
||||||
|
async creationVersion => {
|
||||||
|
await updateAppMetadata({ creationVersion })
|
||||||
|
const res = await config.api.roles.get()
|
||||||
|
expect(res.body).toBeDefined()
|
||||||
|
expect(res.body[appId].roles.map((r: any) => r._id)).toEqual([
|
||||||
|
ROLE_NAME,
|
||||||
|
roles.BUILTIN_ROLE_IDS.ADMIN,
|
||||||
|
roles.BUILTIN_ROLE_IDS.BASIC,
|
||||||
|
roles.BUILTIN_ROLE_IDS.PUBLIC,
|
||||||
|
])
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
it.each(["2.9.0", "1.0.0", "0.0.0", "2.32.17+2146.b125a7c"])(
|
||||||
|
"include POWER roles before v3 (%s)",
|
||||||
|
async creationVersion => {
|
||||||
|
await updateAppMetadata({ creationVersion })
|
||||||
|
const res = await config.api.roles.get()
|
||||||
|
expect(res.body).toBeDefined()
|
||||||
|
expect(res.body[appId].roles.map((r: any) => r._id)).toEqual([
|
||||||
|
ROLE_NAME,
|
||||||
|
roles.BUILTIN_ROLE_IDS.ADMIN,
|
||||||
|
roles.BUILTIN_ROLE_IDS.POWER,
|
||||||
|
roles.BUILTIN_ROLE_IDS.BASIC,
|
||||||
|
roles.BUILTIN_ROLE_IDS.PUBLIC,
|
||||||
|
])
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
it.each(["invalid", ""])(
|
||||||
|
"include POWER roles when the version is corrupted (%s)",
|
||||||
|
async creationVersion => {
|
||||||
|
await updateAppMetadata({ creationVersion })
|
||||||
|
const res = await config.api.roles.get()
|
||||||
|
|
||||||
|
expect(res.body[appId].roles.map((r: any) => r._id)).toEqual([
|
||||||
|
ROLE_NAME,
|
||||||
|
roles.BUILTIN_ROLE_IDS.ADMIN,
|
||||||
|
roles.BUILTIN_ROLE_IDS.POWER,
|
||||||
|
roles.BUILTIN_ROLE_IDS.BASIC,
|
||||||
|
roles.BUILTIN_ROLE_IDS.PUBLIC,
|
||||||
|
])
|
||||||
|
}
|
||||||
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
describe("GET api/global/roles/:appId", () => {
|
describe("GET api/global/roles/:appId", () => {
|
||||||
|
|
Loading…
Reference in New Issue