1
0
Fork 0
mirror of synced 2024-09-25 13:51:40 +12:00

Working on plumbing 'source' all the way through our code.

This commit is contained in:
Sam Rose 2024-09-24 12:30:45 +01:00
parent 6cf7c55fd9
commit 51774b3434
No known key found for this signature in database
23 changed files with 204 additions and 215 deletions

View file

@ -10,7 +10,7 @@ import {
StaticDatabases, StaticDatabases,
DEFAULT_TENANT_ID, DEFAULT_TENANT_ID,
} from "../constants" } from "../constants"
import { Database, IdentityContext, Snippet, App } from "@budibase/types" import { Database, IdentityContext, Snippet, App, Table } from "@budibase/types"
import { ContextMap } from "./types" import { ContextMap } from "./types"
let TEST_APP_ID: string | null = null let TEST_APP_ID: string | null = null
@ -394,3 +394,20 @@ export function setFeatureFlags(key: string, value: Record<string, any>) {
context.featureFlagCache ??= {} context.featureFlagCache ??= {}
context.featureFlagCache[key] = value 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
}

View file

@ -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 { OAuth2Client } from "google-auth-library"
import { GoogleSpreadsheet } from "google-spreadsheet" import { GoogleSpreadsheet } from "google-spreadsheet"
@ -21,4 +21,5 @@ export type ContextMap = {
featureFlagCache?: { featureFlagCache?: {
[key: string]: Record<string, any> [key: string]: Record<string, any>
} }
viewToTableCache?: Record<string, Table>
} }

View file

@ -6,7 +6,7 @@ import {
ViewName, ViewName,
} from "../constants" } from "../constants"
import { getProdAppID } from "./conversions" 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 * 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. * 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 // this includes datasource plus tables
return ( return (
!!id && !!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. * 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 // this covers both datasources and datasource plus
return id && id.startsWith(`${DocumentType.DATASOURCE}${SEPARATOR}`) return !!id && id.startsWith(`${DocumentType.DATASOURCE}${SEPARATOR}`)
} }
/** /**

View file

@ -1274,9 +1274,6 @@ class InternalBuilder {
if (counting) { if (counting) {
query = this.addDistinctCount(query) query = this.addDistinctCount(query)
} else if (aggregations.length > 0) { } 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) query = this.addAggregations(query, aggregations)
} else { } else {
query = query.select(this.generateSelectStatement()) query = query.select(this.generateSelectStatement())

View file

@ -19,6 +19,7 @@ import {
SortJson, SortJson,
SortType, SortType,
Table, Table,
ViewV2,
} from "@budibase/types" } from "@budibase/types"
import { import {
breakExternalTableId, breakExternalTableId,
@ -159,17 +160,41 @@ function isEditableColumn(column: FieldSchema) {
export class ExternalRequest<T extends Operation> { export class ExternalRequest<T extends Operation> {
private readonly operation: T private readonly operation: T
private readonly tableId: string private readonly source: Table | ViewV2
private datasource?: Datasource private datasource: Datasource
private tables: { [key: string]: Table } = {}
constructor(operation: T, tableId: string, datasource?: Datasource) { public static async for<T extends Operation>(
this.operation = operation operation: T,
this.tableId = tableId source: Table | ViewV2,
this.datasource = datasource opts: { datasource?: Datasource } = {}
if (datasource && datasource.entities) { ) {
this.tables = datasource.entities 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( private prepareFilters(
@ -290,20 +315,6 @@ export class ExternalRequest<T extends Operation> {
return this.tables[tableName] 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> { async getRow(table: Table, rowId: string): Promise<Row> {
const response = await getDatasourceAndQuery({ const response = await getDatasourceAndQuery({
endpoint: getEndpoint(table._id!, Operation.READ), endpoint: getEndpoint(table._id!, Operation.READ),
@ -619,24 +630,16 @@ export class ExternalRequest<T extends Operation> {
} }
async run(config: RunConfig): Promise<ExternalRequestReturnType<T>> { async run(config: RunConfig): Promise<ExternalRequestReturnType<T>> {
const { operation, tableId } = this const { operation } = this
if (!tableId) { let table: Table
throw new Error("Unable to run without a table ID") if (sdk.views.isView(this.source)) {
} table = await sdk.views.getTable(this.source.id)
let { datasourceId, tableName } = breakExternalTableId(tableId) } else {
let datasource = this.datasource table = this.source
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.`
)
} }
let isSql = isSQL(this.datasource)
// look for specific components of config which may not be considered acceptable // look for specific components of config which may not be considered acceptable
let { id, row, filters, sort, paginate, rows } = cleanupConfig( let { id, row, filters, sort, paginate, rows } = cleanupConfig(
config, config,
@ -687,8 +690,8 @@ export class ExternalRequest<T extends Operation> {
} }
let json: QueryJson = { let json: QueryJson = {
endpoint: { endpoint: {
datasourceId: datasourceId!, datasourceId: this.datasource._id!,
entityId: tableName, entityId: table.name,
operation, operation,
}, },
resource: { resource: {
@ -714,7 +717,7 @@ export class ExternalRequest<T extends Operation> {
}, },
meta: { meta: {
table, table,
tables: tables, tables: this.tables,
}, },
} }

View file

@ -17,6 +17,7 @@ import {
Row, Row,
Table, Table,
UserCtx, UserCtx,
ViewV2,
} from "@budibase/types" } from "@budibase/types"
import sdk from "../../../sdk" import sdk from "../../../sdk"
import * as utils from "./utils" import * as utils from "./utils"
@ -29,29 +30,29 @@ import { generateIdForRow } from "./utils"
export async function handleRequest<T extends Operation>( export async function handleRequest<T extends Operation>(
operation: T, operation: T,
tableId: string, source: Table | ViewV2,
opts?: RunConfig opts?: RunConfig
): Promise<ExternalRequestReturnType<T>> { ): Promise<ExternalRequestReturnType<T>> {
return new ExternalRequest<T>(operation, tableId, opts?.datasource).run( return (
opts || {} await ExternalRequest.for<T>(operation, source, {
) datasource: opts?.datasource,
})
).run(opts || {})
} }
export async function patch(ctx: UserCtx<PatchRowRequest, PatchRowResponse>) { 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 { _id, ...rowData } = ctx.request.body
const table = await sdk.tables.getTable(tableId)
const { row: dataToUpdate } = await inputProcessing( const { row: dataToUpdate } = await inputProcessing(
ctx.user?._id, ctx.user?._id,
cloneDeep(table), cloneDeep(source),
rowData rowData
) )
const validateResult = await sdk.rows.utils.validate({ const validateResult = await sdk.rows.utils.validate({
row: dataToUpdate, row: dataToUpdate,
tableId, source,
}) })
if (!validateResult.valid) { if (!validateResult.valid) {
throw { validation: validateResult.errors } throw { validation: validateResult.errors }

View file

@ -1,6 +1,6 @@
import * as utils from "../../../../db/utils" import * as utils from "../../../../db/utils"
import { context } from "@budibase/backend-core" import { context, docIds } from "@budibase/backend-core"
import { import {
Aggregation, Aggregation,
Ctx, Ctx,
@ -9,6 +9,7 @@ import {
RelationshipsJson, RelationshipsJson,
Row, Row,
Table, Table,
ViewV2,
} from "@budibase/types" } from "@budibase/types"
import { import {
processDates, processDates,
@ -78,7 +79,7 @@ export function getSourceId(ctx: Ctx): { tableId: string; viewId?: string } {
// top priority, use the URL first // top priority, use the URL first
if (ctx.params?.sourceId) { if (ctx.params?.sourceId) {
const { sourceId } = ctx.params const { sourceId } = ctx.params
if (utils.isViewID(sourceId)) { if (docIds.isViewId(sourceId)) {
return { return {
tableId: utils.extractViewInfoFromID(sourceId).tableId, tableId: utils.extractViewInfoFromID(sourceId).tableId,
viewId: sourceId, 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") 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( export async function validate(
opts: { row: Row } & ({ tableId: string } | { table: Table }) opts: { row: Row } & ({ tableId: string } | { table: Table })
) { ) {

View file

@ -84,11 +84,8 @@ export async function searchView(
})) }))
const searchOptions: RequiredKeys<SearchViewRowRequest> & const searchOptions: RequiredKeys<SearchViewRowRequest> &
RequiredKeys< RequiredKeys<Pick<RowSearchParams, "sourceId" | "query" | "fields">> = {
Pick<RowSearchParams, "tableId" | "viewId" | "query" | "fields"> sourceId: view.id,
> = {
tableId: view.tableId,
viewId: view.id,
query: enrichedQuery, query: enrichedQuery,
fields: viewFields, fields: viewFields,
...getSortOptions(body, view), ...getSortOptions(body, view),

View file

@ -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 { import {
DatabaseQueryOpts, DatabaseQueryOpts,
Datasource, Datasource,
@ -318,12 +318,8 @@ export function generateViewID(tableId: string) {
}${SEPARATOR}${tableId}${SEPARATOR}${newid()}` }${SEPARATOR}${tableId}${SEPARATOR}${newid()}`
} }
export function isViewID(viewId: string) {
return viewId?.split(SEPARATOR)[0] === VirtualDocumentType.VIEW
}
export function extractViewInfoFromID(viewId: string) { 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") throw new Error("Unable to extract table ID, is not a view ID")
} }
const split = viewId.split(SEPARATOR) const split = viewId.split(SEPARATOR)

View file

@ -15,7 +15,7 @@ export function triggerRowActionAuthorised(
const rowActionId: string = ctx.params[actionPath] const rowActionId: string = ctx.params[actionPath]
const isTableId = docIds.isTableId(sourceId) const isTableId = docIds.isTableId(sourceId)
const isViewId = utils.isViewID(sourceId) const isViewId = docIds.isViewId(sourceId)
if (!isTableId && !isViewId) { if (!isTableId && !isViewId) {
ctx.throw(400, `'${sourceId}' is not a valid source id`) ctx.throw(400, `'${sourceId}' is not a valid source id`)
} }

View file

@ -1,10 +1,10 @@
import { db, roles } from "@budibase/backend-core" import { db, docIds, roles } from "@budibase/backend-core"
import { import {
PermissionLevel, PermissionLevel,
PermissionSource, PermissionSource,
VirtualDocumentType, VirtualDocumentType,
} from "@budibase/types" } from "@budibase/types"
import { extractViewInfoFromID, isViewID } from "../../../db/utils" import { extractViewInfoFromID } from "../../../db/utils"
import { import {
CURRENTLY_SUPPORTED_LEVELS, CURRENTLY_SUPPORTED_LEVELS,
getBasePermissions, getBasePermissions,
@ -20,7 +20,7 @@ type ResourcePermissions = Record<
export async function getInheritablePermissions( export async function getInheritablePermissions(
resourceId: string resourceId: string
): Promise<ResourcePermissions | undefined> { ): Promise<ResourcePermissions | undefined> {
if (isViewID(resourceId)) { if (docIds.isViewId(resourceId)) {
return await getResourcePerms(extractViewInfoFromID(resourceId).tableId) return await getResourcePerms(extractViewInfoFromID(resourceId).tableId)
} }
} }

View file

@ -1,11 +1,11 @@
import { context, HTTPError, utils } from "@budibase/backend-core" import { context, docIds, HTTPError, utils } from "@budibase/backend-core"
import { import {
AutomationTriggerStepId, AutomationTriggerStepId,
SEPARATOR, SEPARATOR,
TableRowActions, TableRowActions,
VirtualDocumentType, VirtualDocumentType,
} from "@budibase/types" } from "@budibase/types"
import { generateRowActionsID, isViewID } from "../../db/utils" import { generateRowActionsID } from "../../db/utils"
import automations from "./automations" import automations from "./automations"
import { definitions as TRIGGER_DEFINITIONS } from "../../automations/triggerInfo" import { definitions as TRIGGER_DEFINITIONS } from "../../automations/triggerInfo"
import * as triggers from "../../automations/triggers" import * as triggers from "../../automations/triggers"
@ -155,7 +155,7 @@ export async function update(
async function guardView(tableId: string, viewId: string) { async function guardView(tableId: string, viewId: string) {
let view let view
if (isViewID(viewId)) { if (docIds.isViewId(viewId)) {
view = await sdk.views.get(viewId) view = await sdk.views.get(viewId)
} }
if (!view || view.tableId !== tableId) { if (!view || view.tableId !== tableId) {

View file

@ -53,8 +53,8 @@ export const removeInvalidFilters = (
} }
export const getQueryableFields = async ( export const getQueryableFields = async (
fields: string[], table: Table,
table: Table fields?: string[]
): Promise<string[]> => { ): Promise<string[]> => {
const extractTableFields = async ( const extractTableFields = async (
table: Table, 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 "_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!]))) result.push(...(await extractTableFields(table, fields, [table._id!])))
return result return result

View file

@ -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 { Database, Row } from "@budibase/types"
import { import {
extractViewInfoFromID, extractViewInfoFromID,
getRowParams, getRowParams,
isViewID,
} from "../../../db/utils" } from "../../../db/utils"
import { isExternalTableID } from "../../../integrations/utils" import { isExternalTableID } from "../../../integrations/utils"
import * as internal from "./internal" import * as internal from "./internal"
@ -26,7 +25,7 @@ export async function getAllInternalRows(appId?: string) {
function pickApi(tableOrViewId: string) { function pickApi(tableOrViewId: string) {
let tableId = tableOrViewId let tableId = tableOrViewId
if (isViewID(tableOrViewId)) { if (docIds.isViewId(tableOrViewId)) {
tableId = extractViewInfoFromID(tableOrViewId).tableId tableId = extractViewInfoFromID(tableOrViewId).tableId
} }

View file

@ -4,6 +4,8 @@ import {
RowSearchParams, RowSearchParams,
SearchResponse, SearchResponse,
SortOrder, SortOrder,
Table,
ViewV2,
} from "@budibase/types" } from "@budibase/types"
import { isExternalTableID } from "../../../integrations/utils" import { isExternalTableID } from "../../../integrations/utils"
import * as internal from "./search/internal" import * as internal from "./search/internal"
@ -12,7 +14,7 @@ import { ExportRowsParams, ExportRowsResult } from "./search/types"
import { dataFilters } from "@budibase/shared-core" import { dataFilters } from "@budibase/shared-core"
import sdk from "../../index" import sdk from "../../index"
import { searchInputMapping } from "./search/utils" import { searchInputMapping } from "./search/utils"
import { features } from "@budibase/backend-core" import { features, docIds } from "@budibase/backend-core"
import tracer from "dd-trace" import tracer from "dd-trace"
import { getQueryableFields, removeInvalidFilters } from "./queryUtils" import { getQueryableFields, removeInvalidFilters } from "./queryUtils"
@ -36,8 +38,7 @@ export async function search(
): Promise<SearchResponse<Row>> { ): Promise<SearchResponse<Row>> {
return await tracer.trace("search", async span => { return await tracer.trace("search", async span => {
span?.addTags({ span?.addTags({
tableId: options.tableId, sourceId: options.sourceId,
viewId: options.viewId,
query: options.query, query: options.query,
sort: options.sort, sort: options.sort,
sortOrder: options.sortOrder, sortOrder: options.sortOrder,
@ -52,20 +53,18 @@ export async function search(
.join(", "), .join(", "),
}) })
const isExternalTable = isExternalTableID(options.tableId)
options.query = dataFilters.cleanupQuery(options.query || {}) options.query = dataFilters.cleanupQuery(options.query || {})
options.query = dataFilters.fixupFilterArrays(options.query) options.query = dataFilters.fixupFilterArrays(options.query)
span?.addTags({ span.addTags({
cleanedQuery: options.query, cleanedQuery: options.query,
isExternalTable,
}) })
if ( if (
!dataFilters.hasFilters(options.query) && !dataFilters.hasFilters(options.query) &&
options.query.onEmptyFilter === EmptyFilterOption.RETURN_NONE options.query.onEmptyFilter === EmptyFilterOption.RETURN_NONE
) { ) {
span?.addTags({ emptyQuery: true }) span.addTags({ emptyQuery: true })
return { return {
rows: [], rows: [],
} }
@ -75,34 +74,47 @@ export async function search(
options.sortOrder = options.sortOrder.toLowerCase() as SortOrder options.sortOrder = options.sortOrder.toLowerCase() as SortOrder
} }
const table = await sdk.tables.getTable(options.tableId) let source: Table | ViewV2
options = searchInputMapping(table, options) 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) { span.addTags({
const tableFields = Object.keys(table.schema).filter( tableId: table._id,
f => table.schema[f].visible !== false })
) } else {
throw new Error(`Invalid source ID: ${options.sourceId}`)
const queriableFields = await getQueryableFields(
options.fields?.filter(f => tableFields.includes(f)) ?? tableFields,
table
)
options.query = removeInvalidFilters(options.query, queriableFields)
} }
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> let result: SearchResponse<Row>
if (isExternalTable) { if (isExternalTable) {
span?.addTags({ searchType: "external" }) span?.addTags({ searchType: "external" })
result = await external.search(options, table) result = await external.search(options, source)
} else if (await features.flags.isEnabled("SQS")) { } else if (await features.flags.isEnabled("SQS")) {
span?.addTags({ searchType: "sqs" }) span?.addTags({ searchType: "sqs" })
result = await internal.sqs.search(options, table) result = await internal.sqs.search(options, source)
} else { } else {
span?.addTags({ searchType: "lucene" }) 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, foundRows: result.rows.length,
totalRows: result.totalRows, totalRows: result.totalRows,
}) })

View file

@ -9,6 +9,7 @@ import {
SortJson, SortJson,
SortOrder, SortOrder,
Table, Table,
ViewV2,
} from "@budibase/types" } from "@budibase/types"
import * as exporters from "../../../../api/controllers/view/exporters" import * as exporters from "../../../../api/controllers/view/exporters"
import { handleRequest } from "../../../../api/controllers/row/external" import { handleRequest } from "../../../../api/controllers/row/external"
@ -60,9 +61,8 @@ function getPaginationAndLimitParameters(
export async function search( export async function search(
options: RowSearchParams, options: RowSearchParams,
table: Table source: Table | ViewV2
): Promise<SearchResponse<Row>> { ): Promise<SearchResponse<Row>> {
const { tableId } = options
const { countRows, paginate, query, ...params } = options const { countRows, paginate, query, ...params } = options
const { limit } = params const { limit } = params
let bookmark = let bookmark =
@ -112,10 +112,9 @@ export async function search(
: Promise.resolve(undefined), : Promise.resolve(undefined),
]) ])
let processed = await outputProcessing(table, rows, { let processed = await outputProcessing(source, rows, {
preserveLinks: true, preserveLinks: true,
squash: true, squash: true,
fromViewId: options.viewId,
}) })
let hasNextPage = false let hasNextPage = false

View file

@ -9,6 +9,7 @@ import {
SearchResponse, SearchResponse,
Row, Row,
RowSearchParams, RowSearchParams,
ViewV2,
} from "@budibase/types" } from "@budibase/types"
import { db as dbCore, context } from "@budibase/backend-core" import { db as dbCore, context } from "@budibase/backend-core"
import { utils } from "@budibase/shared-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 // maps through the search parameters to check if any of the inputs are invalid
// based on the table schema, converts them to something that is valid. // based on the table schema, converts them to something that is valid.
export function searchInputMapping(table: Table, options: RowSearchParams) { export function searchInputMapping(table: Table, options: RowSearchParams) {
if (!table?.schema) { for (let [key, column] of Object.entries(table.schema || {})) {
return options
}
for (let [key, column] of Object.entries(table.schema)) {
switch (column.type) { switch (column.type) {
case FieldType.BB_REFERENCE_SINGLE: { case FieldType.BB_REFERENCE_SINGLE: {
const subtype = column.subtype const subtype = column.subtype

View file

@ -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"]) 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"]) expect(result).toEqual(["_id", "name"])
}) })
@ -245,7 +245,7 @@ describe("query utils", () => {
}) })
const result = await config.doInContext(config.appId, () => { const result = await config.doInContext(config.appId, () => {
return getQueryableFields(Object.keys(table.schema), table) return getQueryableFields(table)
}) })
expect(result).toEqual([ expect(result).toEqual([
"_id", "_id",
@ -282,7 +282,7 @@ describe("query utils", () => {
}) })
const result = await config.doInContext(config.appId, () => { 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"]) expect(result).toEqual(["_id", "name", "aux.name", "auxTable.name"])
}) })
@ -313,7 +313,7 @@ describe("query utils", () => {
}) })
const result = await config.doInContext(config.appId, () => { const result = await config.doInContext(config.appId, () => {
return getQueryableFields(Object.keys(table.schema), table) return getQueryableFields(table)
}) })
expect(result).toEqual(["_id", "name"]) expect(result).toEqual(["_id", "name"])
}) })
@ -381,7 +381,7 @@ describe("query utils", () => {
it("includes nested relationship fields from main table", async () => { it("includes nested relationship fields from main table", async () => {
const result = await config.doInContext(config.appId, () => { const result = await config.doInContext(config.appId, () => {
return getQueryableFields(Object.keys(table.schema), table) return getQueryableFields(table)
}) })
expect(result).toEqual([ expect(result).toEqual([
"_id", "_id",
@ -398,7 +398,7 @@ describe("query utils", () => {
it("includes nested relationship fields from aux 1 table", async () => { it("includes nested relationship fields from aux 1 table", async () => {
const result = await config.doInContext(config.appId, () => { const result = await config.doInContext(config.appId, () => {
return getQueryableFields(Object.keys(aux1.schema), aux1) return getQueryableFields(aux1)
}) })
expect(result).toEqual([ expect(result).toEqual([
"_id", "_id",
@ -420,7 +420,7 @@ describe("query utils", () => {
it("includes nested relationship fields from aux 2 table", async () => { it("includes nested relationship fields from aux 2 table", async () => {
const result = await config.doInContext(config.appId, () => { const result = await config.doInContext(config.appId, () => {
return getQueryableFields(Object.keys(aux2.schema), aux2) return getQueryableFields(aux2)
}) })
expect(result).toEqual([ expect(result).toEqual([
"_id", "_id",
@ -474,7 +474,7 @@ describe("query utils", () => {
it("includes nested relationship fields from main table", async () => { it("includes nested relationship fields from main table", async () => {
const result = await config.doInContext(config.appId, () => { const result = await config.doInContext(config.appId, () => {
return getQueryableFields(Object.keys(table.schema), table) return getQueryableFields(table)
}) })
expect(result).toEqual([ expect(result).toEqual([
"_id", "_id",
@ -488,7 +488,7 @@ describe("query utils", () => {
it("includes nested relationship fields from aux table", async () => { it("includes nested relationship fields from aux table", async () => {
const result = await config.doInContext(config.appId, () => { const result = await config.doInContext(config.appId, () => {
return getQueryableFields(Object.keys(aux.schema), aux) return getQueryableFields(aux)
}) })
expect(result).toEqual([ expect(result).toEqual([
"_id", "_id",

View file

@ -13,16 +13,14 @@ import {
TableSchema, TableSchema,
SqlClient, SqlClient,
ArrayOperator, ArrayOperator,
ViewV2,
} from "@budibase/types" } from "@budibase/types"
import { makeExternalQuery } from "../../../integrations/base/query" import { makeExternalQuery } from "../../../integrations/base/query"
import { Format } from "../../../api/controllers/view/exporters" import { Format } from "../../../api/controllers/view/exporters"
import sdk from "../.." import sdk from "../.."
import { import { extractViewInfoFromID, isRelationshipColumn } from "../../../db/utils"
extractViewInfoFromID,
isRelationshipColumn,
isViewID,
} from "../../../db/utils"
import { isSQL } from "../../../integrations/utils" import { isSQL } from "../../../integrations/utils"
import { docIds } from "@budibase/backend-core"
const SQL_CLIENT_SOURCE_MAP: Record<SourceName, SqlClient | undefined> = { const SQL_CLIENT_SOURCE_MAP: Record<SourceName, SqlClient | undefined> = {
[SourceName.POSTGRES]: SqlClient.POSTGRES, [SourceName.POSTGRES]: SqlClient.POSTGRES,
@ -142,37 +140,32 @@ function isForeignKey(key: string, table: Table) {
} }
export async function validate({ export async function validate({
tableId, source,
row, row,
table,
}: { }: {
tableId?: string source: Table | ViewV2
row: Row row: Row
table?: Table
}): Promise<{ }): Promise<{
valid: boolean valid: boolean
errors: Record<string, any> errors: Record<string, any>
}> { }> {
let fetchedTable: Table | undefined let table: Table
if (!table && tableId) { if (sdk.views.isView(source)) {
fetchedTable = await sdk.tables.getTable(tableId) table = await sdk.views.getTable(source.id)
} else if (table) { } else {
fetchedTable = table table = source
}
if (fetchedTable === undefined) {
throw new Error("Unable to fetch table for validation")
} }
const errors: Record<string, any> = {} const errors: Record<string, any> = {}
const disallowArrayTypes = [ const disallowArrayTypes = [
FieldType.ATTACHMENT_SINGLE, FieldType.ATTACHMENT_SINGLE,
FieldType.BB_REFERENCE_SINGLE, FieldType.BB_REFERENCE_SINGLE,
] ]
for (let fieldName of Object.keys(fetchedTable.schema)) { for (let fieldName of Object.keys(table.schema)) {
const column = fetchedTable.schema[fieldName] const column = table.schema[fieldName]
const constraints = cloneDeep(column.constraints) const constraints = cloneDeep(column.constraints)
const type = column.type const type = column.type
// foreign keys are likely to be enriched // foreign keys are likely to be enriched
if (isForeignKey(fieldName, fetchedTable)) { if (isForeignKey(fieldName, table)) {
continue continue
} }
// formulas shouldn't validated, data will be deleted anyway // 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) { export function tryExtractingTableAndViewId(tableOrViewId: string) {
if (isViewID(tableOrViewId)) { if (docIds.isViewId(tableOrViewId)) {
return { return {
tableId: extractViewInfoFromID(tableOrViewId).tableId, tableId: extractViewInfoFromID(tableOrViewId).tableId,
viewId: tableOrViewId, viewId: tableOrViewId,

View file

@ -9,3 +9,7 @@ export function isExternal(opts: { table?: Table; tableId?: string }): boolean {
} }
return false return false
} }
export function isTable(table: any): table is Table {
return table.type === "table"
}

View file

@ -9,7 +9,7 @@ import {
ViewV2ColumnEnriched, ViewV2ColumnEnriched,
ViewV2Enriched, ViewV2Enriched,
} from "@budibase/types" } from "@budibase/types"
import { HTTPError } from "@budibase/backend-core" import { context, HTTPError } from "@budibase/backend-core"
import { import {
helpers, helpers,
PROTECTED_EXTERNAL_COLUMNS, PROTECTED_EXTERNAL_COLUMNS,
@ -40,6 +40,23 @@ export async function getEnriched(viewId: string): Promise<ViewV2Enriched> {
return pickApi(tableId).getEnriched(viewId) 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( async function guardCalculationViewSchema(
table: Table, table: Table,
view: Omit<ViewV2, "id" | "version"> view: Omit<ViewV2, "id" | "version">

View file

@ -10,8 +10,7 @@ export interface Aggregation {
} }
export interface SearchParams { export interface SearchParams {
tableId?: string sourceId?: string
viewId?: string
query?: SearchFilters query?: SearchFilters
paginate?: boolean paginate?: boolean
bookmark?: string | number 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 // when searching for rows we want a more extensive search type that requires certain properties
export interface RowSearchParams export interface RowSearchParams
extends WithRequired<SearchParams, "tableId" | "query"> {} extends WithRequired<SearchParams, "sourceId" | "query"> {}
export interface SearchResponse<T> { export interface SearchResponse<T> {
rows: T[] rows: T[]

View file

@ -17796,21 +17796,11 @@ periscopic@^3.1.0:
estree-walker "^3.0.0" estree-walker "^3.0.0"
is-reference "^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: pg-connection-string@2.5.0, pg-connection-string@^2.5.0:
version "2.5.0" version "2.5.0"
resolved "https://registry.yarnpkg.com/pg-connection-string/-/pg-connection-string-2.5.0.tgz#538cadd0f7e603fc09a12590f3b8a452c2c0cf34" resolved "https://registry.yarnpkg.com/pg-connection-string/-/pg-connection-string-2.5.0.tgz#538cadd0f7e603fc09a12590f3b8a452c2c0cf34"
integrity sha512-r5o/V/ORTA6TmUnyWZR9nCj1klXCO2CEKNRlVuJptZe85QuhFayC7WeMic7ndayT5IRIR0S0xFxFi2ousartlQ== 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: pg-int8@1.0.1:
version "1.0.1" version "1.0.1"
resolved "https://registry.yarnpkg.com/pg-int8/-/pg-int8-1.0.1.tgz#943bd463bf5b71b4170115f80f8efc9a0c0eb78c" 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" resolved "https://registry.yarnpkg.com/pg-pool/-/pg-pool-3.6.0.tgz#3190df3e4747a0d23e5e9e8045bcd99bda0a712e"
integrity sha512-clFRf2ksqd+F497kWFyM21tMjeikn60oGDmqMT8UBrynEwVEX/5R5xd2sdvdo1cZCFlguORNpVuqxIj+aK4cfQ== 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: pg-protocol@*, pg-protocol@^1.6.0:
version "1.6.0" version "1.6.0"
resolved "https://registry.yarnpkg.com/pg-protocol/-/pg-protocol-1.6.0.tgz#4c91613c0315349363af2084608db843502f8833" resolved "https://registry.yarnpkg.com/pg-protocol/-/pg-protocol-1.6.0.tgz#4c91613c0315349363af2084608db843502f8833"
integrity sha512-M+PDm637OY5WM307051+bsDia5Xej6d9IR4GwJse1qA1DIhiKlksvrneZOYQq42OM+spubpcNYEo2FcKQrDk+Q== 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: pg-types@^2.1.0, pg-types@^2.2.0:
version "2.2.0" version "2.2.0"
resolved "https://registry.yarnpkg.com/pg-types/-/pg-types-2.2.0.tgz#2d0250d636454f7cfa3b6ae0382fdfa8063254a3" 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" pg-types "^2.1.0"
pgpass "1.x" 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: pgpass@1.x:
version "1.0.5" version "1.0.5"
resolved "https://registry.yarnpkg.com/pgpass/-/pgpass-1.0.5.tgz#9b873e4a564bb10fa7a7dbd55312728d422a223d" 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" resolved "https://registry.yarnpkg.com/string-similarity/-/string-similarity-4.0.4.tgz#42d01ab0b34660ea8a018da8f56a3309bb8b2a5b"
integrity sha512-/q/8Q4Bl4ZKAPjj8WerIBJWALKkaPRfrvhfF8k/B23i4nzrlRj2/go1m90In7nG/3XDSbOo0+pu6RvCTM9RGMQ== integrity sha512-/q/8Q4Bl4ZKAPjj8WerIBJWALKkaPRfrvhfF8k/B23i4nzrlRj2/go1m90In7nG/3XDSbOo0+pu6RvCTM9RGMQ==
"string-width-cjs@npm:string-width@^4.2.0": "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==
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:
version "4.2.3" version "4.2.3"
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
@ -20886,7 +20844,7 @@ stringify-object@^3.2.1:
is-obj "^1.0.1" is-obj "^1.0.1"
is-regexp "^1.0.0" 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" version "6.0.1"
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
@ -20900,13 +20858,6 @@ strip-ansi@^5.0.0, strip-ansi@^5.1.0, strip-ansi@^5.2.0:
dependencies: dependencies:
ansi-regex "^4.1.0" 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: strip-ansi@^7.0.1:
version "7.0.1" version "7.0.1"
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-7.0.1.tgz#61740a08ce36b61e50e65653f07060d000975fb2" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-7.0.1.tgz#61740a08ce36b61e50e65653f07060d000975fb2"
@ -22862,7 +22813,7 @@ worker-farm@1.7.0:
dependencies: dependencies:
errno "~0.1.7" 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" version "7.0.0"
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
@ -22880,15 +22831,6 @@ wrap-ansi@^5.1.0:
string-width "^3.0.0" string-width "^3.0.0"
strip-ansi "^5.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: wrap-ansi@^8.1.0:
version "8.1.0" version "8.1.0"
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214"