Working on plumbing 'source' all the way through our code.
This commit is contained in:
parent
6cf7c55fd9
commit
51774b3434
|
@ -10,7 +10,7 @@ import {
|
|||
StaticDatabases,
|
||||
DEFAULT_TENANT_ID,
|
||||
} from "../constants"
|
||||
import { Database, IdentityContext, Snippet, App } from "@budibase/types"
|
||||
import { Database, IdentityContext, Snippet, App, Table } from "@budibase/types"
|
||||
import { ContextMap } from "./types"
|
||||
|
||||
let TEST_APP_ID: string | null = null
|
||||
|
@ -394,3 +394,20 @@ export function setFeatureFlags(key: string, value: Record<string, any>) {
|
|||
context.featureFlagCache ??= {}
|
||||
context.featureFlagCache[key] = value
|
||||
}
|
||||
|
||||
export function getTableForView(viewId: string): Table | undefined {
|
||||
const context = getCurrentContext()
|
||||
if (!context) {
|
||||
return
|
||||
}
|
||||
return context.viewToTableCache?.[viewId]
|
||||
}
|
||||
|
||||
export function setTableForView(viewId: string, table: Table) {
|
||||
const context = getCurrentContext()
|
||||
if (!context) {
|
||||
return
|
||||
}
|
||||
context.viewToTableCache ??= {}
|
||||
context.viewToTableCache[viewId] = table
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { IdentityContext, Snippet, VM } from "@budibase/types"
|
||||
import { IdentityContext, Snippet, Table, VM } from "@budibase/types"
|
||||
import { OAuth2Client } from "google-auth-library"
|
||||
import { GoogleSpreadsheet } from "google-spreadsheet"
|
||||
|
||||
|
@ -21,4 +21,5 @@ export type ContextMap = {
|
|||
featureFlagCache?: {
|
||||
[key: string]: Record<string, any>
|
||||
}
|
||||
viewToTableCache?: Record<string, Table>
|
||||
}
|
||||
|
|
|
@ -6,7 +6,7 @@ import {
|
|||
ViewName,
|
||||
} from "../constants"
|
||||
import { getProdAppID } from "./conversions"
|
||||
import { DatabaseQueryOpts } from "@budibase/types"
|
||||
import { DatabaseQueryOpts, VirtualDocumentType } from "@budibase/types"
|
||||
|
||||
/**
|
||||
* If creating DB allDocs/query params with only a single top level ID this can be used, this
|
||||
|
@ -66,9 +66,8 @@ export function getQueryIndex(viewName: ViewName) {
|
|||
|
||||
/**
|
||||
* Check if a given ID is that of a table.
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export const isTableId = (id: string) => {
|
||||
export const isTableId = (id: string): boolean => {
|
||||
// this includes datasource plus tables
|
||||
return (
|
||||
!!id &&
|
||||
|
@ -77,13 +76,16 @@ export const isTableId = (id: string) => {
|
|||
)
|
||||
}
|
||||
|
||||
export function isViewId(id: string): boolean {
|
||||
return !!id && id.startsWith(`${VirtualDocumentType.VIEW}${SEPARATOR}`)
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a given ID is that of a datasource or datasource plus.
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export const isDatasourceId = (id: string) => {
|
||||
export const isDatasourceId = (id: string): boolean => {
|
||||
// this covers both datasources and datasource plus
|
||||
return id && id.startsWith(`${DocumentType.DATASOURCE}${SEPARATOR}`)
|
||||
return !!id && id.startsWith(`${DocumentType.DATASOURCE}${SEPARATOR}`)
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -1274,9 +1274,6 @@ class InternalBuilder {
|
|||
if (counting) {
|
||||
query = this.addDistinctCount(query)
|
||||
} else if (aggregations.length > 0) {
|
||||
query = query.select(
|
||||
this.knex.raw("ROW_NUMBER() OVER (ORDER BY (SELECT 0)) as _id")
|
||||
)
|
||||
query = this.addAggregations(query, aggregations)
|
||||
} else {
|
||||
query = query.select(this.generateSelectStatement())
|
||||
|
|
|
@ -19,6 +19,7 @@ import {
|
|||
SortJson,
|
||||
SortType,
|
||||
Table,
|
||||
ViewV2,
|
||||
} from "@budibase/types"
|
||||
import {
|
||||
breakExternalTableId,
|
||||
|
@ -159,19 +160,43 @@ function isEditableColumn(column: FieldSchema) {
|
|||
|
||||
export class ExternalRequest<T extends Operation> {
|
||||
private readonly operation: T
|
||||
private readonly tableId: string
|
||||
private datasource?: Datasource
|
||||
private tables: { [key: string]: Table } = {}
|
||||
private readonly source: Table | ViewV2
|
||||
private datasource: Datasource
|
||||
|
||||
constructor(operation: T, tableId: string, datasource?: Datasource) {
|
||||
this.operation = operation
|
||||
this.tableId = tableId
|
||||
this.datasource = datasource
|
||||
if (datasource && datasource.entities) {
|
||||
this.tables = datasource.entities
|
||||
public static async for<T extends Operation>(
|
||||
operation: T,
|
||||
source: Table | ViewV2,
|
||||
opts: { datasource?: Datasource } = {}
|
||||
) {
|
||||
if (!opts.datasource) {
|
||||
if (sdk.views.isView(source)) {
|
||||
const table = await sdk.views.getTable(source.id)
|
||||
opts.datasource = await sdk.datasources.get(table.sourceId!)
|
||||
} else {
|
||||
opts.datasource = await sdk.datasources.get(source.sourceId!)
|
||||
}
|
||||
}
|
||||
|
||||
return new ExternalRequest(operation, source, opts.datasource)
|
||||
}
|
||||
|
||||
private get tables(): { [key: string]: Table } {
|
||||
if (!this.datasource.entities) {
|
||||
throw new Error("Datasource does not have entities")
|
||||
}
|
||||
return this.datasource.entities
|
||||
}
|
||||
|
||||
private constructor(
|
||||
operation: T,
|
||||
source: Table | ViewV2,
|
||||
datasource: Datasource
|
||||
) {
|
||||
this.operation = operation
|
||||
this.source = source
|
||||
this.datasource = datasource
|
||||
}
|
||||
|
||||
private prepareFilters(
|
||||
id: string | undefined | string[],
|
||||
filters: SearchFilters,
|
||||
|
@ -290,20 +315,6 @@ export class ExternalRequest<T extends Operation> {
|
|||
return this.tables[tableName]
|
||||
}
|
||||
|
||||
// seeds the object with table and datasource information
|
||||
async retrieveMetadata(
|
||||
datasourceId: string
|
||||
): Promise<{ tables: Record<string, Table>; datasource: Datasource }> {
|
||||
if (!this.datasource) {
|
||||
this.datasource = await sdk.datasources.get(datasourceId)
|
||||
if (!this.datasource || !this.datasource.entities) {
|
||||
throw "No tables found, fetch tables before query."
|
||||
}
|
||||
this.tables = this.datasource.entities
|
||||
}
|
||||
return { tables: this.tables, datasource: this.datasource }
|
||||
}
|
||||
|
||||
async getRow(table: Table, rowId: string): Promise<Row> {
|
||||
const response = await getDatasourceAndQuery({
|
||||
endpoint: getEndpoint(table._id!, Operation.READ),
|
||||
|
@ -619,24 +630,16 @@ export class ExternalRequest<T extends Operation> {
|
|||
}
|
||||
|
||||
async run(config: RunConfig): Promise<ExternalRequestReturnType<T>> {
|
||||
const { operation, tableId } = this
|
||||
if (!tableId) {
|
||||
throw new Error("Unable to run without a table ID")
|
||||
}
|
||||
let { datasourceId, tableName } = breakExternalTableId(tableId)
|
||||
let datasource = this.datasource
|
||||
if (!datasource) {
|
||||
const { datasource: ds } = await this.retrieveMetadata(datasourceId)
|
||||
datasource = ds
|
||||
}
|
||||
const tables = this.tables
|
||||
const table = tables[tableName]
|
||||
let isSql = isSQL(datasource)
|
||||
if (!table) {
|
||||
throw new Error(
|
||||
`Unable to process query, table "${tableName}" not defined.`
|
||||
)
|
||||
const { operation } = this
|
||||
let table: Table
|
||||
if (sdk.views.isView(this.source)) {
|
||||
table = await sdk.views.getTable(this.source.id)
|
||||
} else {
|
||||
table = this.source
|
||||
}
|
||||
|
||||
let isSql = isSQL(this.datasource)
|
||||
|
||||
// look for specific components of config which may not be considered acceptable
|
||||
let { id, row, filters, sort, paginate, rows } = cleanupConfig(
|
||||
config,
|
||||
|
@ -687,8 +690,8 @@ export class ExternalRequest<T extends Operation> {
|
|||
}
|
||||
let json: QueryJson = {
|
||||
endpoint: {
|
||||
datasourceId: datasourceId!,
|
||||
entityId: tableName,
|
||||
datasourceId: this.datasource._id!,
|
||||
entityId: table.name,
|
||||
operation,
|
||||
},
|
||||
resource: {
|
||||
|
@ -714,7 +717,7 @@ export class ExternalRequest<T extends Operation> {
|
|||
},
|
||||
meta: {
|
||||
table,
|
||||
tables: tables,
|
||||
tables: this.tables,
|
||||
},
|
||||
}
|
||||
|
||||
|
|
|
@ -17,6 +17,7 @@ import {
|
|||
Row,
|
||||
Table,
|
||||
UserCtx,
|
||||
ViewV2,
|
||||
} from "@budibase/types"
|
||||
import sdk from "../../../sdk"
|
||||
import * as utils from "./utils"
|
||||
|
@ -29,29 +30,29 @@ import { generateIdForRow } from "./utils"
|
|||
|
||||
export async function handleRequest<T extends Operation>(
|
||||
operation: T,
|
||||
tableId: string,
|
||||
source: Table | ViewV2,
|
||||
opts?: RunConfig
|
||||
): Promise<ExternalRequestReturnType<T>> {
|
||||
return new ExternalRequest<T>(operation, tableId, opts?.datasource).run(
|
||||
opts || {}
|
||||
)
|
||||
return (
|
||||
await ExternalRequest.for<T>(operation, source, {
|
||||
datasource: opts?.datasource,
|
||||
})
|
||||
).run(opts || {})
|
||||
}
|
||||
|
||||
export async function patch(ctx: UserCtx<PatchRowRequest, PatchRowResponse>) {
|
||||
const { tableId, viewId } = utils.getSourceId(ctx)
|
||||
|
||||
const source = await utils.getSource(ctx)
|
||||
const { _id, ...rowData } = ctx.request.body
|
||||
const table = await sdk.tables.getTable(tableId)
|
||||
|
||||
const { row: dataToUpdate } = await inputProcessing(
|
||||
ctx.user?._id,
|
||||
cloneDeep(table),
|
||||
cloneDeep(source),
|
||||
rowData
|
||||
)
|
||||
|
||||
const validateResult = await sdk.rows.utils.validate({
|
||||
row: dataToUpdate,
|
||||
tableId,
|
||||
source,
|
||||
})
|
||||
if (!validateResult.valid) {
|
||||
throw { validation: validateResult.errors }
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import * as utils from "../../../../db/utils"
|
||||
|
||||
import { context } from "@budibase/backend-core"
|
||||
import { context, docIds } from "@budibase/backend-core"
|
||||
import {
|
||||
Aggregation,
|
||||
Ctx,
|
||||
|
@ -9,6 +9,7 @@ import {
|
|||
RelationshipsJson,
|
||||
Row,
|
||||
Table,
|
||||
ViewV2,
|
||||
} from "@budibase/types"
|
||||
import {
|
||||
processDates,
|
||||
|
@ -78,7 +79,7 @@ export function getSourceId(ctx: Ctx): { tableId: string; viewId?: string } {
|
|||
// top priority, use the URL first
|
||||
if (ctx.params?.sourceId) {
|
||||
const { sourceId } = ctx.params
|
||||
if (utils.isViewID(sourceId)) {
|
||||
if (docIds.isViewId(sourceId)) {
|
||||
return {
|
||||
tableId: utils.extractViewInfoFromID(sourceId).tableId,
|
||||
viewId: sourceId,
|
||||
|
@ -97,6 +98,14 @@ export function getSourceId(ctx: Ctx): { tableId: string; viewId?: string } {
|
|||
throw new Error("Unable to find table ID in request")
|
||||
}
|
||||
|
||||
export async function getSource(ctx: Ctx): Promise<Table | ViewV2> {
|
||||
const { tableId, viewId } = getSourceId(ctx)
|
||||
if (viewId) {
|
||||
return sdk.views.get(viewId)
|
||||
}
|
||||
return sdk.tables.getTable(tableId)
|
||||
}
|
||||
|
||||
export async function validate(
|
||||
opts: { row: Row } & ({ tableId: string } | { table: Table })
|
||||
) {
|
||||
|
|
|
@ -84,11 +84,8 @@ export async function searchView(
|
|||
}))
|
||||
|
||||
const searchOptions: RequiredKeys<SearchViewRowRequest> &
|
||||
RequiredKeys<
|
||||
Pick<RowSearchParams, "tableId" | "viewId" | "query" | "fields">
|
||||
> = {
|
||||
tableId: view.tableId,
|
||||
viewId: view.id,
|
||||
RequiredKeys<Pick<RowSearchParams, "sourceId" | "query" | "fields">> = {
|
||||
sourceId: view.id,
|
||||
query: enrichedQuery,
|
||||
fields: viewFields,
|
||||
...getSortOptions(body, view),
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { context, db as dbCore, utils } from "@budibase/backend-core"
|
||||
import { context, db as dbCore, docIds, utils } from "@budibase/backend-core"
|
||||
import {
|
||||
DatabaseQueryOpts,
|
||||
Datasource,
|
||||
|
@ -318,12 +318,8 @@ export function generateViewID(tableId: string) {
|
|||
}${SEPARATOR}${tableId}${SEPARATOR}${newid()}`
|
||||
}
|
||||
|
||||
export function isViewID(viewId: string) {
|
||||
return viewId?.split(SEPARATOR)[0] === VirtualDocumentType.VIEW
|
||||
}
|
||||
|
||||
export function extractViewInfoFromID(viewId: string) {
|
||||
if (!isViewID(viewId)) {
|
||||
if (!docIds.isViewId(viewId)) {
|
||||
throw new Error("Unable to extract table ID, is not a view ID")
|
||||
}
|
||||
const split = viewId.split(SEPARATOR)
|
||||
|
|
|
@ -15,7 +15,7 @@ export function triggerRowActionAuthorised(
|
|||
const rowActionId: string = ctx.params[actionPath]
|
||||
|
||||
const isTableId = docIds.isTableId(sourceId)
|
||||
const isViewId = utils.isViewID(sourceId)
|
||||
const isViewId = docIds.isViewId(sourceId)
|
||||
if (!isTableId && !isViewId) {
|
||||
ctx.throw(400, `'${sourceId}' is not a valid source id`)
|
||||
}
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
import { db, roles } from "@budibase/backend-core"
|
||||
import { db, docIds, roles } from "@budibase/backend-core"
|
||||
import {
|
||||
PermissionLevel,
|
||||
PermissionSource,
|
||||
VirtualDocumentType,
|
||||
} from "@budibase/types"
|
||||
import { extractViewInfoFromID, isViewID } from "../../../db/utils"
|
||||
import { extractViewInfoFromID } from "../../../db/utils"
|
||||
import {
|
||||
CURRENTLY_SUPPORTED_LEVELS,
|
||||
getBasePermissions,
|
||||
|
@ -20,7 +20,7 @@ type ResourcePermissions = Record<
|
|||
export async function getInheritablePermissions(
|
||||
resourceId: string
|
||||
): Promise<ResourcePermissions | undefined> {
|
||||
if (isViewID(resourceId)) {
|
||||
if (docIds.isViewId(resourceId)) {
|
||||
return await getResourcePerms(extractViewInfoFromID(resourceId).tableId)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
import { context, HTTPError, utils } from "@budibase/backend-core"
|
||||
import { context, docIds, HTTPError, utils } from "@budibase/backend-core"
|
||||
import {
|
||||
AutomationTriggerStepId,
|
||||
SEPARATOR,
|
||||
TableRowActions,
|
||||
VirtualDocumentType,
|
||||
} from "@budibase/types"
|
||||
import { generateRowActionsID, isViewID } from "../../db/utils"
|
||||
import { generateRowActionsID } from "../../db/utils"
|
||||
import automations from "./automations"
|
||||
import { definitions as TRIGGER_DEFINITIONS } from "../../automations/triggerInfo"
|
||||
import * as triggers from "../../automations/triggers"
|
||||
|
@ -155,7 +155,7 @@ export async function update(
|
|||
|
||||
async function guardView(tableId: string, viewId: string) {
|
||||
let view
|
||||
if (isViewID(viewId)) {
|
||||
if (docIds.isViewId(viewId)) {
|
||||
view = await sdk.views.get(viewId)
|
||||
}
|
||||
if (!view || view.tableId !== tableId) {
|
||||
|
|
|
@ -53,8 +53,8 @@ export const removeInvalidFilters = (
|
|||
}
|
||||
|
||||
export const getQueryableFields = async (
|
||||
fields: string[],
|
||||
table: Table
|
||||
table: Table,
|
||||
fields?: string[]
|
||||
): Promise<string[]> => {
|
||||
const extractTableFields = async (
|
||||
table: Table,
|
||||
|
@ -110,6 +110,9 @@ export const getQueryableFields = async (
|
|||
"_id", // Querying by _id is always allowed, even if it's never part of the schema
|
||||
]
|
||||
|
||||
if (fields === undefined) {
|
||||
fields = Object.keys(table.schema)
|
||||
}
|
||||
result.push(...(await extractTableFields(table, fields, [table._id!])))
|
||||
|
||||
return result
|
||||
|
|
|
@ -1,9 +1,8 @@
|
|||
import { db as dbCore, context } from "@budibase/backend-core"
|
||||
import { db as dbCore, context, docIds } from "@budibase/backend-core"
|
||||
import { Database, Row } from "@budibase/types"
|
||||
import {
|
||||
extractViewInfoFromID,
|
||||
getRowParams,
|
||||
isViewID,
|
||||
} from "../../../db/utils"
|
||||
import { isExternalTableID } from "../../../integrations/utils"
|
||||
import * as internal from "./internal"
|
||||
|
@ -26,7 +25,7 @@ export async function getAllInternalRows(appId?: string) {
|
|||
|
||||
function pickApi(tableOrViewId: string) {
|
||||
let tableId = tableOrViewId
|
||||
if (isViewID(tableOrViewId)) {
|
||||
if (docIds.isViewId(tableOrViewId)) {
|
||||
tableId = extractViewInfoFromID(tableOrViewId).tableId
|
||||
}
|
||||
|
||||
|
|
|
@ -4,6 +4,8 @@ import {
|
|||
RowSearchParams,
|
||||
SearchResponse,
|
||||
SortOrder,
|
||||
Table,
|
||||
ViewV2,
|
||||
} from "@budibase/types"
|
||||
import { isExternalTableID } from "../../../integrations/utils"
|
||||
import * as internal from "./search/internal"
|
||||
|
@ -12,7 +14,7 @@ import { ExportRowsParams, ExportRowsResult } from "./search/types"
|
|||
import { dataFilters } from "@budibase/shared-core"
|
||||
import sdk from "../../index"
|
||||
import { searchInputMapping } from "./search/utils"
|
||||
import { features } from "@budibase/backend-core"
|
||||
import { features, docIds } from "@budibase/backend-core"
|
||||
import tracer from "dd-trace"
|
||||
import { getQueryableFields, removeInvalidFilters } from "./queryUtils"
|
||||
|
||||
|
@ -36,8 +38,7 @@ export async function search(
|
|||
): Promise<SearchResponse<Row>> {
|
||||
return await tracer.trace("search", async span => {
|
||||
span?.addTags({
|
||||
tableId: options.tableId,
|
||||
viewId: options.viewId,
|
||||
sourceId: options.sourceId,
|
||||
query: options.query,
|
||||
sort: options.sort,
|
||||
sortOrder: options.sortOrder,
|
||||
|
@ -52,20 +53,18 @@ export async function search(
|
|||
.join(", "),
|
||||
})
|
||||
|
||||
const isExternalTable = isExternalTableID(options.tableId)
|
||||
options.query = dataFilters.cleanupQuery(options.query || {})
|
||||
options.query = dataFilters.fixupFilterArrays(options.query)
|
||||
|
||||
span?.addTags({
|
||||
span.addTags({
|
||||
cleanedQuery: options.query,
|
||||
isExternalTable,
|
||||
})
|
||||
|
||||
if (
|
||||
!dataFilters.hasFilters(options.query) &&
|
||||
options.query.onEmptyFilter === EmptyFilterOption.RETURN_NONE
|
||||
) {
|
||||
span?.addTags({ emptyQuery: true })
|
||||
span.addTags({ emptyQuery: true })
|
||||
return {
|
||||
rows: [],
|
||||
}
|
||||
|
@ -75,34 +74,47 @@ export async function search(
|
|||
options.sortOrder = options.sortOrder.toLowerCase() as SortOrder
|
||||
}
|
||||
|
||||
const table = await sdk.tables.getTable(options.tableId)
|
||||
let source: Table | ViewV2
|
||||
let table: Table
|
||||
if (docIds.isTableId(options.sourceId)) {
|
||||
source = await sdk.tables.getTable(options.sourceId)
|
||||
table = source
|
||||
options = searchInputMapping(source, options)
|
||||
} else if (docIds.isViewId(options.sourceId)) {
|
||||
source = await sdk.views.get(options.sourceId)
|
||||
table = await sdk.tables.getTable(source.tableId)
|
||||
options = searchInputMapping(table, options)
|
||||
|
||||
if (options.query) {
|
||||
const tableFields = Object.keys(table.schema).filter(
|
||||
f => table.schema[f].visible !== false
|
||||
)
|
||||
|
||||
const queriableFields = await getQueryableFields(
|
||||
options.fields?.filter(f => tableFields.includes(f)) ?? tableFields,
|
||||
table
|
||||
)
|
||||
options.query = removeInvalidFilters(options.query, queriableFields)
|
||||
span.addTags({
|
||||
tableId: table._id,
|
||||
})
|
||||
} else {
|
||||
throw new Error(`Invalid source ID: ${options.sourceId}`)
|
||||
}
|
||||
|
||||
if (options.query) {
|
||||
const visibleFields = (
|
||||
options.fields || Object.keys(table.schema)
|
||||
).filter(field => table.schema[field].visible)
|
||||
|
||||
const queryableFields = await getQueryableFields(table, visibleFields)
|
||||
options.query = removeInvalidFilters(options.query, queryableFields)
|
||||
}
|
||||
|
||||
const isExternalTable = isExternalTableID(table._id!)
|
||||
let result: SearchResponse<Row>
|
||||
if (isExternalTable) {
|
||||
span?.addTags({ searchType: "external" })
|
||||
result = await external.search(options, table)
|
||||
result = await external.search(options, source)
|
||||
} else if (await features.flags.isEnabled("SQS")) {
|
||||
span?.addTags({ searchType: "sqs" })
|
||||
result = await internal.sqs.search(options, table)
|
||||
result = await internal.sqs.search(options, source)
|
||||
} else {
|
||||
span?.addTags({ searchType: "lucene" })
|
||||
result = await internal.lucene.search(options, table)
|
||||
result = await internal.lucene.search(options, source)
|
||||
}
|
||||
|
||||
span?.addTags({
|
||||
span.addTags({
|
||||
foundRows: result.rows.length,
|
||||
totalRows: result.totalRows,
|
||||
})
|
||||
|
|
|
@ -9,6 +9,7 @@ import {
|
|||
SortJson,
|
||||
SortOrder,
|
||||
Table,
|
||||
ViewV2,
|
||||
} from "@budibase/types"
|
||||
import * as exporters from "../../../../api/controllers/view/exporters"
|
||||
import { handleRequest } from "../../../../api/controllers/row/external"
|
||||
|
@ -60,9 +61,8 @@ function getPaginationAndLimitParameters(
|
|||
|
||||
export async function search(
|
||||
options: RowSearchParams,
|
||||
table: Table
|
||||
source: Table | ViewV2
|
||||
): Promise<SearchResponse<Row>> {
|
||||
const { tableId } = options
|
||||
const { countRows, paginate, query, ...params } = options
|
||||
const { limit } = params
|
||||
let bookmark =
|
||||
|
@ -112,10 +112,9 @@ export async function search(
|
|||
: Promise.resolve(undefined),
|
||||
])
|
||||
|
||||
let processed = await outputProcessing(table, rows, {
|
||||
let processed = await outputProcessing(source, rows, {
|
||||
preserveLinks: true,
|
||||
squash: true,
|
||||
fromViewId: options.viewId,
|
||||
})
|
||||
|
||||
let hasNextPage = false
|
||||
|
|
|
@ -9,6 +9,7 @@ import {
|
|||
SearchResponse,
|
||||
Row,
|
||||
RowSearchParams,
|
||||
ViewV2,
|
||||
} from "@budibase/types"
|
||||
import { db as dbCore, context } from "@budibase/backend-core"
|
||||
import { utils } from "@budibase/shared-core"
|
||||
|
@ -83,10 +84,7 @@ function userColumnMapping(column: string, options: RowSearchParams) {
|
|||
// 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) {
|
||||
if (!table?.schema) {
|
||||
return options
|
||||
}
|
||||
for (let [key, column] of Object.entries(table.schema)) {
|
||||
for (let [key, column] of Object.entries(table.schema || {})) {
|
||||
switch (column.type) {
|
||||
case FieldType.BB_REFERENCE_SINGLE: {
|
||||
const subtype = column.subtype
|
||||
|
|
|
@ -203,7 +203,7 @@ describe("query utils", () => {
|
|||
},
|
||||
})
|
||||
|
||||
const result = await getQueryableFields(Object.keys(table.schema), table)
|
||||
const result = await getQueryableFields(table)
|
||||
expect(result).toEqual(["_id", "name", "age"])
|
||||
})
|
||||
|
||||
|
@ -216,7 +216,7 @@ describe("query utils", () => {
|
|||
},
|
||||
})
|
||||
|
||||
const result = await getQueryableFields(Object.keys(table.schema), table)
|
||||
const result = await getQueryableFields(table)
|
||||
expect(result).toEqual(["_id", "name"])
|
||||
})
|
||||
|
||||
|
@ -245,7 +245,7 @@ describe("query utils", () => {
|
|||
})
|
||||
|
||||
const result = await config.doInContext(config.appId, () => {
|
||||
return getQueryableFields(Object.keys(table.schema), table)
|
||||
return getQueryableFields(table)
|
||||
})
|
||||
expect(result).toEqual([
|
||||
"_id",
|
||||
|
@ -282,7 +282,7 @@ describe("query utils", () => {
|
|||
})
|
||||
|
||||
const result = await config.doInContext(config.appId, () => {
|
||||
return getQueryableFields(Object.keys(table.schema), table)
|
||||
return getQueryableFields(table)
|
||||
})
|
||||
expect(result).toEqual(["_id", "name", "aux.name", "auxTable.name"])
|
||||
})
|
||||
|
@ -313,7 +313,7 @@ describe("query utils", () => {
|
|||
})
|
||||
|
||||
const result = await config.doInContext(config.appId, () => {
|
||||
return getQueryableFields(Object.keys(table.schema), table)
|
||||
return getQueryableFields(table)
|
||||
})
|
||||
expect(result).toEqual(["_id", "name"])
|
||||
})
|
||||
|
@ -381,7 +381,7 @@ describe("query utils", () => {
|
|||
|
||||
it("includes nested relationship fields from main table", async () => {
|
||||
const result = await config.doInContext(config.appId, () => {
|
||||
return getQueryableFields(Object.keys(table.schema), table)
|
||||
return getQueryableFields(table)
|
||||
})
|
||||
expect(result).toEqual([
|
||||
"_id",
|
||||
|
@ -398,7 +398,7 @@ describe("query utils", () => {
|
|||
|
||||
it("includes nested relationship fields from aux 1 table", async () => {
|
||||
const result = await config.doInContext(config.appId, () => {
|
||||
return getQueryableFields(Object.keys(aux1.schema), aux1)
|
||||
return getQueryableFields(aux1)
|
||||
})
|
||||
expect(result).toEqual([
|
||||
"_id",
|
||||
|
@ -420,7 +420,7 @@ describe("query utils", () => {
|
|||
|
||||
it("includes nested relationship fields from aux 2 table", async () => {
|
||||
const result = await config.doInContext(config.appId, () => {
|
||||
return getQueryableFields(Object.keys(aux2.schema), aux2)
|
||||
return getQueryableFields(aux2)
|
||||
})
|
||||
expect(result).toEqual([
|
||||
"_id",
|
||||
|
@ -474,7 +474,7 @@ describe("query utils", () => {
|
|||
|
||||
it("includes nested relationship fields from main table", async () => {
|
||||
const result = await config.doInContext(config.appId, () => {
|
||||
return getQueryableFields(Object.keys(table.schema), table)
|
||||
return getQueryableFields(table)
|
||||
})
|
||||
expect(result).toEqual([
|
||||
"_id",
|
||||
|
@ -488,7 +488,7 @@ describe("query utils", () => {
|
|||
|
||||
it("includes nested relationship fields from aux table", async () => {
|
||||
const result = await config.doInContext(config.appId, () => {
|
||||
return getQueryableFields(Object.keys(aux.schema), aux)
|
||||
return getQueryableFields(aux)
|
||||
})
|
||||
expect(result).toEqual([
|
||||
"_id",
|
||||
|
|
|
@ -13,16 +13,14 @@ import {
|
|||
TableSchema,
|
||||
SqlClient,
|
||||
ArrayOperator,
|
||||
ViewV2,
|
||||
} from "@budibase/types"
|
||||
import { makeExternalQuery } from "../../../integrations/base/query"
|
||||
import { Format } from "../../../api/controllers/view/exporters"
|
||||
import sdk from "../.."
|
||||
import {
|
||||
extractViewInfoFromID,
|
||||
isRelationshipColumn,
|
||||
isViewID,
|
||||
} from "../../../db/utils"
|
||||
import { extractViewInfoFromID, isRelationshipColumn } from "../../../db/utils"
|
||||
import { isSQL } from "../../../integrations/utils"
|
||||
import { docIds } from "@budibase/backend-core"
|
||||
|
||||
const SQL_CLIENT_SOURCE_MAP: Record<SourceName, SqlClient | undefined> = {
|
||||
[SourceName.POSTGRES]: SqlClient.POSTGRES,
|
||||
|
@ -142,37 +140,32 @@ function isForeignKey(key: string, table: Table) {
|
|||
}
|
||||
|
||||
export async function validate({
|
||||
tableId,
|
||||
source,
|
||||
row,
|
||||
table,
|
||||
}: {
|
||||
tableId?: string
|
||||
source: Table | ViewV2
|
||||
row: Row
|
||||
table?: Table
|
||||
}): Promise<{
|
||||
valid: boolean
|
||||
errors: Record<string, any>
|
||||
}> {
|
||||
let fetchedTable: Table | undefined
|
||||
if (!table && tableId) {
|
||||
fetchedTable = await sdk.tables.getTable(tableId)
|
||||
} else if (table) {
|
||||
fetchedTable = table
|
||||
}
|
||||
if (fetchedTable === undefined) {
|
||||
throw new Error("Unable to fetch table for validation")
|
||||
let table: Table
|
||||
if (sdk.views.isView(source)) {
|
||||
table = await sdk.views.getTable(source.id)
|
||||
} else {
|
||||
table = source
|
||||
}
|
||||
const errors: Record<string, any> = {}
|
||||
const disallowArrayTypes = [
|
||||
FieldType.ATTACHMENT_SINGLE,
|
||||
FieldType.BB_REFERENCE_SINGLE,
|
||||
]
|
||||
for (let fieldName of Object.keys(fetchedTable.schema)) {
|
||||
const column = fetchedTable.schema[fieldName]
|
||||
for (let fieldName of Object.keys(table.schema)) {
|
||||
const column = table.schema[fieldName]
|
||||
const constraints = cloneDeep(column.constraints)
|
||||
const type = column.type
|
||||
// foreign keys are likely to be enriched
|
||||
if (isForeignKey(fieldName, fetchedTable)) {
|
||||
if (isForeignKey(fieldName, table)) {
|
||||
continue
|
||||
}
|
||||
// formulas shouldn't validated, data will be deleted anyway
|
||||
|
@ -323,7 +316,7 @@ export function isArrayFilter(operator: any): operator is ArrayOperator {
|
|||
}
|
||||
|
||||
export function tryExtractingTableAndViewId(tableOrViewId: string) {
|
||||
if (isViewID(tableOrViewId)) {
|
||||
if (docIds.isViewId(tableOrViewId)) {
|
||||
return {
|
||||
tableId: extractViewInfoFromID(tableOrViewId).tableId,
|
||||
viewId: tableOrViewId,
|
||||
|
|
|
@ -9,3 +9,7 @@ export function isExternal(opts: { table?: Table; tableId?: string }): boolean {
|
|||
}
|
||||
return false
|
||||
}
|
||||
|
||||
export function isTable(table: any): table is Table {
|
||||
return table.type === "table"
|
||||
}
|
||||
|
|
|
@ -9,7 +9,7 @@ import {
|
|||
ViewV2ColumnEnriched,
|
||||
ViewV2Enriched,
|
||||
} from "@budibase/types"
|
||||
import { HTTPError } from "@budibase/backend-core"
|
||||
import { context, HTTPError } from "@budibase/backend-core"
|
||||
import {
|
||||
helpers,
|
||||
PROTECTED_EXTERNAL_COLUMNS,
|
||||
|
@ -40,6 +40,23 @@ export async function getEnriched(viewId: string): Promise<ViewV2Enriched> {
|
|||
return pickApi(tableId).getEnriched(viewId)
|
||||
}
|
||||
|
||||
export async function getTable(viewId: string): Promise<Table> {
|
||||
const cached = context.getTableForView(viewId)
|
||||
if (cached) {
|
||||
return cached
|
||||
}
|
||||
const { tableId } = utils.extractViewInfoFromID(viewId)
|
||||
const table = await sdk.tables.getTable(tableId)
|
||||
context.setTableForView(viewId, table)
|
||||
return table
|
||||
}
|
||||
|
||||
export function isView(view: any): view is ViewV2 {
|
||||
return (
|
||||
view.version === 2 && "id" in view && "tableId" in view && "name" in view
|
||||
)
|
||||
}
|
||||
|
||||
async function guardCalculationViewSchema(
|
||||
table: Table,
|
||||
view: Omit<ViewV2, "id" | "version">
|
||||
|
|
|
@ -10,8 +10,7 @@ export interface Aggregation {
|
|||
}
|
||||
|
||||
export interface SearchParams {
|
||||
tableId?: string
|
||||
viewId?: string
|
||||
sourceId?: string
|
||||
query?: SearchFilters
|
||||
paginate?: boolean
|
||||
bookmark?: string | number
|
||||
|
@ -30,7 +29,7 @@ export interface SearchParams {
|
|||
|
||||
// when searching for rows we want a more extensive search type that requires certain properties
|
||||
export interface RowSearchParams
|
||||
extends WithRequired<SearchParams, "tableId" | "query"> {}
|
||||
extends WithRequired<SearchParams, "sourceId" | "query"> {}
|
||||
|
||||
export interface SearchResponse<T> {
|
||||
rows: T[]
|
||||
|
|
64
yarn.lock
64
yarn.lock
|
@ -17796,21 +17796,11 @@ periscopic@^3.1.0:
|
|||
estree-walker "^3.0.0"
|
||||
is-reference "^3.0.0"
|
||||
|
||||
pg-cloudflare@^1.1.1:
|
||||
version "1.1.1"
|
||||
resolved "https://registry.yarnpkg.com/pg-cloudflare/-/pg-cloudflare-1.1.1.tgz#e6d5833015b170e23ae819e8c5d7eaedb472ca98"
|
||||
integrity sha512-xWPagP/4B6BgFO+EKz3JONXv3YDgvkbVrGw2mTo3D6tVDQRh1e7cqVGvyR3BE+eQgAvx1XhW/iEASj4/jCWl3Q==
|
||||
|
||||
pg-connection-string@2.5.0, pg-connection-string@^2.5.0:
|
||||
version "2.5.0"
|
||||
resolved "https://registry.yarnpkg.com/pg-connection-string/-/pg-connection-string-2.5.0.tgz#538cadd0f7e603fc09a12590f3b8a452c2c0cf34"
|
||||
integrity sha512-r5o/V/ORTA6TmUnyWZR9nCj1klXCO2CEKNRlVuJptZe85QuhFayC7WeMic7ndayT5IRIR0S0xFxFi2ousartlQ==
|
||||
|
||||
pg-connection-string@^2.6.4:
|
||||
version "2.6.4"
|
||||
resolved "https://registry.yarnpkg.com/pg-connection-string/-/pg-connection-string-2.6.4.tgz#f543862adfa49fa4e14bc8a8892d2a84d754246d"
|
||||
integrity sha512-v+Z7W/0EO707aNMaAEfiGnGL9sxxumwLl2fJvCQtMn9Fxsg+lPpPkdcyBSv/KFgpGdYkMfn+EI1Or2EHjpgLCA==
|
||||
|
||||
pg-int8@1.0.1:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/pg-int8/-/pg-int8-1.0.1.tgz#943bd463bf5b71b4170115f80f8efc9a0c0eb78c"
|
||||
|
@ -17821,21 +17811,11 @@ pg-pool@^3.6.0:
|
|||
resolved "https://registry.yarnpkg.com/pg-pool/-/pg-pool-3.6.0.tgz#3190df3e4747a0d23e5e9e8045bcd99bda0a712e"
|
||||
integrity sha512-clFRf2ksqd+F497kWFyM21tMjeikn60oGDmqMT8UBrynEwVEX/5R5xd2sdvdo1cZCFlguORNpVuqxIj+aK4cfQ==
|
||||
|
||||
pg-pool@^3.6.2:
|
||||
version "3.6.2"
|
||||
resolved "https://registry.yarnpkg.com/pg-pool/-/pg-pool-3.6.2.tgz#3a592370b8ae3f02a7c8130d245bc02fa2c5f3f2"
|
||||
integrity sha512-Htjbg8BlwXqSBQ9V8Vjtc+vzf/6fVUuak/3/XXKA9oxZprwW3IMDQTGHP+KDmVL7rtd+R1QjbnCFPuTHm3G4hg==
|
||||
|
||||
pg-protocol@*, pg-protocol@^1.6.0:
|
||||
version "1.6.0"
|
||||
resolved "https://registry.yarnpkg.com/pg-protocol/-/pg-protocol-1.6.0.tgz#4c91613c0315349363af2084608db843502f8833"
|
||||
integrity sha512-M+PDm637OY5WM307051+bsDia5Xej6d9IR4GwJse1qA1DIhiKlksvrneZOYQq42OM+spubpcNYEo2FcKQrDk+Q==
|
||||
|
||||
pg-protocol@^1.6.1:
|
||||
version "1.6.1"
|
||||
resolved "https://registry.yarnpkg.com/pg-protocol/-/pg-protocol-1.6.1.tgz#21333e6d83b01faaebfe7a33a7ad6bfd9ed38cb3"
|
||||
integrity sha512-jPIlvgoD63hrEuihvIg+tJhoGjUsLPn6poJY9N5CnlPd91c2T18T/9zBtLxZSb1EhYxBRoZJtzScCaWlYLtktg==
|
||||
|
||||
pg-types@^2.1.0, pg-types@^2.2.0:
|
||||
version "2.2.0"
|
||||
resolved "https://registry.yarnpkg.com/pg-types/-/pg-types-2.2.0.tgz#2d0250d636454f7cfa3b6ae0382fdfa8063254a3"
|
||||
|
@ -17860,19 +17840,6 @@ pg@8.10.0:
|
|||
pg-types "^2.1.0"
|
||||
pgpass "1.x"
|
||||
|
||||
pg@^8.12.0:
|
||||
version "8.12.0"
|
||||
resolved "https://registry.yarnpkg.com/pg/-/pg-8.12.0.tgz#9341724db571022490b657908f65aee8db91df79"
|
||||
integrity sha512-A+LHUSnwnxrnL/tZ+OLfqR1SxLN3c/pgDztZ47Rpbsd4jUytsTtwQo/TLPRzPJMp/1pbhYVhH9cuSZLAajNfjQ==
|
||||
dependencies:
|
||||
pg-connection-string "^2.6.4"
|
||||
pg-pool "^3.6.2"
|
||||
pg-protocol "^1.6.1"
|
||||
pg-types "^2.1.0"
|
||||
pgpass "1.x"
|
||||
optionalDependencies:
|
||||
pg-cloudflare "^1.1.1"
|
||||
|
||||
pgpass@1.x:
|
||||
version "1.0.5"
|
||||
resolved "https://registry.yarnpkg.com/pgpass/-/pgpass-1.0.5.tgz#9b873e4a564bb10fa7a7dbd55312728d422a223d"
|
||||
|
@ -20786,16 +20753,7 @@ string-similarity@^4.0.4:
|
|||
resolved "https://registry.yarnpkg.com/string-similarity/-/string-similarity-4.0.4.tgz#42d01ab0b34660ea8a018da8f56a3309bb8b2a5b"
|
||||
integrity sha512-/q/8Q4Bl4ZKAPjj8WerIBJWALKkaPRfrvhfF8k/B23i4nzrlRj2/go1m90In7nG/3XDSbOo0+pu6RvCTM9RGMQ==
|
||||
|
||||
"string-width-cjs@npm:string-width@^4.2.0":
|
||||
version "4.2.3"
|
||||
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
|
||||
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
|
||||
dependencies:
|
||||
emoji-regex "^8.0.0"
|
||||
is-fullwidth-code-point "^3.0.0"
|
||||
strip-ansi "^6.0.1"
|
||||
|
||||
"string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.0.0, string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.2, string-width@^4.2.3:
|
||||
"string-width-cjs@npm:string-width@^4.2.0", "string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.0.0, string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.2, string-width@^4.2.3:
|
||||
version "4.2.3"
|
||||
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
|
||||
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
|
||||
|
@ -20886,7 +20844,7 @@ stringify-object@^3.2.1:
|
|||
is-obj "^1.0.1"
|
||||
is-regexp "^1.0.0"
|
||||
|
||||
"strip-ansi-cjs@npm:strip-ansi@^6.0.1":
|
||||
"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1:
|
||||
version "6.0.1"
|
||||
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
|
||||
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
|
||||
|
@ -20900,13 +20858,6 @@ strip-ansi@^5.0.0, strip-ansi@^5.1.0, strip-ansi@^5.2.0:
|
|||
dependencies:
|
||||
ansi-regex "^4.1.0"
|
||||
|
||||
strip-ansi@^6.0.0, strip-ansi@^6.0.1:
|
||||
version "6.0.1"
|
||||
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
|
||||
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
|
||||
dependencies:
|
||||
ansi-regex "^5.0.1"
|
||||
|
||||
strip-ansi@^7.0.1:
|
||||
version "7.0.1"
|
||||
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-7.0.1.tgz#61740a08ce36b61e50e65653f07060d000975fb2"
|
||||
|
@ -22862,7 +22813,7 @@ worker-farm@1.7.0:
|
|||
dependencies:
|
||||
errno "~0.1.7"
|
||||
|
||||
"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0":
|
||||
"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0:
|
||||
version "7.0.0"
|
||||
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
|
||||
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
|
||||
|
@ -22880,15 +22831,6 @@ wrap-ansi@^5.1.0:
|
|||
string-width "^3.0.0"
|
||||
strip-ansi "^5.0.0"
|
||||
|
||||
wrap-ansi@^7.0.0:
|
||||
version "7.0.0"
|
||||
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
|
||||
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
|
||||
dependencies:
|
||||
ansi-styles "^4.0.0"
|
||||
string-width "^4.1.0"
|
||||
strip-ansi "^6.0.0"
|
||||
|
||||
wrap-ansi@^8.1.0:
|
||||
version "8.1.0"
|
||||
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214"
|
||||
|
|
Loading…
Reference in New Issue