Merge branch 'master' of github.com:budibase/budibase into budi-8833-allow-ai-column-to-be-used-in-view-calculations

This commit is contained in:
Sam Rose 2025-01-08 17:33:55 +00:00
commit addd832b6e
No known key found for this signature in database
40 changed files with 688 additions and 379 deletions

3
packages/bbui/src/helpers.d.ts vendored Normal file
View File

@ -0,0 +1,3 @@
declare module "./helpers" {
export const cloneDeep: <T>(obj: T) => T
}

View File

@ -43,7 +43,6 @@
export let showDataProviders = true
const dispatch = createEventDispatcher()
const arrayTypes = ["attachment", "array"]
let anchorRight, dropdownRight
let drawer
@ -116,8 +115,11 @@
}
})
$: fields = bindings
.filter(x => arrayTypes.includes(x.fieldSchema?.type))
.filter(x => x.fieldSchema?.tableId != null)
.filter(
x =>
x.fieldSchema?.type === "attachment" ||
(x.fieldSchema?.type === "array" && x.tableId)
)
.map(binding => {
const { providerId, readableBinding, runtimeBinding } = binding
const { name, type, tableId } = binding.fieldSchema

View File

@ -1,12 +1,12 @@
import { API } from "api"
import TableFetch from "@budibase/frontend-core/src/fetch/TableFetch.js"
import ViewFetch from "@budibase/frontend-core/src/fetch/ViewFetch.js"
import QueryFetch from "@budibase/frontend-core/src/fetch/QueryFetch.js"
import RelationshipFetch from "@budibase/frontend-core/src/fetch/RelationshipFetch.js"
import NestedProviderFetch from "@budibase/frontend-core/src/fetch/NestedProviderFetch.js"
import FieldFetch from "@budibase/frontend-core/src/fetch/FieldFetch.js"
import JSONArrayFetch from "@budibase/frontend-core/src/fetch/JSONArrayFetch.js"
import ViewV2Fetch from "@budibase/frontend-core/src/fetch/ViewV2Fetch.js"
import TableFetch from "@budibase/frontend-core/src/fetch/TableFetch"
import ViewFetch from "@budibase/frontend-core/src/fetch/ViewFetch"
import QueryFetch from "@budibase/frontend-core/src/fetch/QueryFetch"
import RelationshipFetch from "@budibase/frontend-core/src/fetch/RelationshipFetch"
import NestedProviderFetch from "@budibase/frontend-core/src/fetch/NestedProviderFetch"
import FieldFetch from "@budibase/frontend-core/src/fetch/FieldFetch"
import JSONArrayFetch from "@budibase/frontend-core/src/fetch/JSONArrayFetch"
import ViewV2Fetch from "@budibase/frontend-core/src/fetch/ViewV2Fetch"
import QueryArrayFetch from "@budibase/frontend-core/src/fetch/QueryArrayFetch"
/**

View File

@ -3,7 +3,15 @@ import { BaseAPIClient } from "./types"
export interface ViewEndpoints {
// Missing request or response types
fetchViewData: (name: string, opts: any) => Promise<Row[]>
fetchViewData: (
name: string,
opts: {
calculation?: string
field?: string
groupBy?: string
tableId: string
}
) => Promise<Row[]>
exportView: (name: string, format: string) => Promise<any>
saveView: (view: any) => Promise<any>
deleteView: (name: string) => Promise<any>
@ -20,7 +28,9 @@ export const buildViewEndpoints = (API: BaseAPIClient): ViewEndpoints => ({
fetchViewData: async (name, { field, groupBy, calculation }) => {
const params = new URLSearchParams()
if (calculation) {
params.set("field", field)
if (field) {
params.set("field", field)
}
params.set("calculation", calculation)
}
if (groupBy) {

View File

@ -1,6 +1,7 @@
import {
CreateViewRequest,
CreateViewResponse,
PaginatedSearchRowResponse,
SearchRowResponse,
SearchViewRowRequest,
UpdateViewRequest,
@ -13,10 +14,14 @@ export interface ViewV2Endpoints {
fetchDefinition: (viewId: string) => Promise<ViewResponseEnriched>
create: (view: CreateViewRequest) => Promise<CreateViewResponse>
update: (view: UpdateViewRequest) => Promise<UpdateViewResponse>
fetch: (
fetch: <T extends SearchViewRowRequest>(
viewId: string,
opts: SearchViewRowRequest
) => Promise<SearchRowResponse>
opts: T
) => Promise<
T extends { paginate: true }
? PaginatedSearchRowResponse
: SearchRowResponse
>
delete: (viewId: string) => Promise<void>
}
@ -59,7 +64,7 @@ export const buildViewV2Endpoints = (API: BaseAPIClient): ViewV2Endpoints => ({
* @param viewId the id of the view
* @param opts the search options
*/
fetch: async (viewId, opts) => {
fetch: async (viewId, opts: SearchViewRowRequest) => {
return await API.post({
url: `/api/v2/views/${encodeURIComponent(viewId)}/search`,
body: opts,

View File

@ -69,7 +69,7 @@ export const deriveStores = (context: StoreContext): ConfigDerivedStore => {
}
// Disable features for non DS+
if (!["table", "viewV2"].includes(type)) {
if (type && !["table", "viewV2"].includes(type)) {
config.canAddRows = false
config.canEditRows = false
config.canDeleteRows = false

View File

@ -1,3 +1,5 @@
// TODO: datasource and defitions are unions of the different implementations. At this point, the datasource does not know what type is being used, and the assignations will cause TS exceptions. Casting it "as any" for now. This should be fixed improving the type usages.
import { derived, get, Readable, Writable } from "svelte/store"
import { getDatasourceDefinition, getDatasourceSchema } from "../../../fetch"
import { enrichSchemaWithRelColumns, memo } from "../../../utils"
@ -71,10 +73,10 @@ export const deriveStores = (context: StoreContext): DerivedDatasourceStore => {
} = context
const schema = derived(definition, $definition => {
let schema: Record<string, UIFieldSchema> = getDatasourceSchema({
const schema: Record<string, any> | undefined = getDatasourceSchema({
API,
datasource: get(datasource),
definition: $definition,
datasource: get(datasource) as any, // TODO: see line 1
definition: $definition ?? undefined,
})
if (!schema) {
return null
@ -82,7 +84,7 @@ export const deriveStores = (context: StoreContext): DerivedDatasourceStore => {
// Ensure schema is configured as objects.
// Certain datasources like queries use primitives.
Object.keys(schema || {}).forEach(key => {
Object.keys(schema).forEach(key => {
if (typeof schema[key] !== "object") {
schema[key] = { name: key, type: schema[key] }
}
@ -130,13 +132,13 @@ export const deriveStores = (context: StoreContext): DerivedDatasourceStore => {
([$datasource, $definition]) => {
let type = $datasource?.type
if (type === "provider") {
type = ($datasource as any).value?.datasource?.type
type = ($datasource as any).value?.datasource?.type // TODO: see line 1
}
// Handle calculation views
if (type === "viewV2" && $definition?.type === ViewV2Type.CALCULATION) {
return false
}
return ["table", "viewV2", "link"].includes(type)
return !!type && ["table", "viewV2", "link"].includes(type)
}
)
@ -184,9 +186,9 @@ export const createActions = (context: StoreContext): ActionDatasourceStore => {
const refreshDefinition = async () => {
const def = await getDatasourceDefinition({
API,
datasource: get(datasource),
datasource: get(datasource) as any, // TODO: see line 1
})
definition.set(def)
definition.set(def as any) // TODO: see line 1
}
// Saves the datasource definition
@ -231,7 +233,7 @@ export const createActions = (context: StoreContext): ActionDatasourceStore => {
if ("default" in newDefinition.schema[column]) {
delete newDefinition.schema[column].default
}
return await saveDefinition(newDefinition as any)
return await saveDefinition(newDefinition as any) // TODO: see line 1
}
// Adds a schema mutation for a single field
@ -307,7 +309,7 @@ export const createActions = (context: StoreContext): ActionDatasourceStore => {
await saveDefinition({
...$definition,
schema: newSchema,
} as any)
} as any) // TODO: see line 1
resetSchemaMutations()
}

View File

@ -10,9 +10,10 @@ import {
import { tick } from "svelte"
import { Helpers } from "@budibase/bbui"
import { sleep } from "../../../utils/utils"
import { FieldType, Row, UIFetchAPI, UIRow } from "@budibase/types"
import { FieldType, Row, UIRow } from "@budibase/types"
import { getRelatedTableValues } from "../../../utils"
import { Store as StoreContext } from "."
import DataFetch from "../../../fetch/DataFetch"
interface IndexedUIRow extends UIRow {
__idx: number
@ -20,7 +21,7 @@ interface IndexedUIRow extends UIRow {
interface RowStore {
rows: Writable<UIRow[]>
fetch: Writable<UIFetchAPI | null>
fetch: Writable<DataFetch<any, any, any> | null> // TODO: type this properly, having a union of all the possible options
loaded: Writable<boolean>
refreshing: Writable<boolean>
loading: Writable<boolean>
@ -225,7 +226,7 @@ export const createActions = (context: StoreContext): RowActionStore => {
})
// Subscribe to changes of this fetch model
unsubscribe = newFetch.subscribe(async ($fetch: UIFetchAPI) => {
unsubscribe = newFetch.subscribe(async $fetch => {
if ($fetch.error) {
// Present a helpful error to the user
let message = "An unknown error occurred"
@ -253,7 +254,7 @@ export const createActions = (context: StoreContext): RowActionStore => {
// Reset state properties when dataset changes
if (!$instanceLoaded || resetRows) {
definition.set($fetch.definition)
definition.set($fetch.definition as any) // TODO: datasource and defitions are unions of the different implementations. At this point, the datasource does not know what type is being used, and the assignations will cause TS exceptions. Casting it "as any" for now. This should be fixed improving the type usages.
}
// Reset scroll state when data changes

View File

@ -32,8 +32,8 @@ export const Cookies = {
}
// Table names
export const TableNames = {
USERS: "ta_users",
export const enum TableNames {
USERS = "ta_users",
}
export const BudibaseRoles = {

View File

@ -1,8 +1,17 @@
import DataFetch from "./DataFetch.js"
import DataFetch from "./DataFetch"
export default class CustomFetch extends DataFetch {
interface CustomDatasource {
data: any
}
type CustomDefinition = Record<string, any>
export default class CustomFetch extends DataFetch<
CustomDatasource,
CustomDefinition
> {
// Gets the correct Budibase type for a JS value
getType(value) {
getType(value: any) {
if (value == null) {
return "string"
}
@ -22,7 +31,7 @@ export default class CustomFetch extends DataFetch {
}
// Parses the custom data into an array format
parseCustomData(data) {
parseCustomData(data: any) {
if (!data) {
return []
}
@ -55,7 +64,7 @@ export default class CustomFetch extends DataFetch {
}
// Enriches the custom data to ensure the structure and format is usable
enrichCustomData(data) {
enrichCustomData(data: (string | any)[]) {
if (!data?.length) {
return []
}
@ -72,7 +81,7 @@ export default class CustomFetch extends DataFetch {
// Try parsing strings
if (typeof value === "string") {
const split = value.split(",").map(x => x.trim())
let obj = {}
const obj: Record<string, string> = {}
for (let i = 0; i < split.length; i++) {
const suffix = i === 0 ? "" : ` ${i + 1}`
const key = `Value${suffix}`
@ -87,27 +96,29 @@ export default class CustomFetch extends DataFetch {
}
// Extracts and parses the custom data from the datasource definition
getCustomData(datasource) {
getCustomData(datasource: CustomDatasource) {
return this.enrichCustomData(this.parseCustomData(datasource?.data))
}
async getDefinition(datasource) {
async getDefinition() {
const { datasource } = this.options
// Try and work out the schema from the array provided
let schema = {}
const schema: CustomDefinition = {}
const data = this.getCustomData(datasource)
if (!data?.length) {
return { schema }
}
// Go through every object and extract all valid keys
for (let datum of data) {
for (let key of Object.keys(datum)) {
for (const datum of data) {
for (const key of Object.keys(datum)) {
if (key === "_id") {
continue
}
if (!schema[key]) {
let type = this.getType(datum[key])
let constraints = {}
const constraints: any = {}
// Determine whether we should render text columns as options instead
if (type === "string") {

View File

@ -1,25 +1,102 @@
import { writable, derived, get } from "svelte/store"
import { writable, derived, get, Writable, Readable } from "svelte/store"
import { cloneDeep } from "lodash/fp"
import { QueryUtils } from "../utils"
import { convertJSONSchemaToTableSchema } from "../utils/json"
import { FieldType, SortOrder, SortType } from "@budibase/types"
import {
FieldType,
LegacyFilter,
Row,
SearchFilters,
SortOrder,
SortType,
TableSchema,
UISearchFilter,
} from "@budibase/types"
import { APIClient } from "../api/types"
const { buildQuery, limit: queryLimit, runQuery, sort } = QueryUtils
interface DataFetchStore<TDefinition, TQuery> {
rows: Row[]
info: any
schema: TableSchema | null
loading: boolean
loaded: boolean
query: TQuery
pageNumber: number
cursor: string | null
cursors: string[]
resetKey: string
error: {
message: string
status: number
} | null
definition?: TDefinition | null
}
interface DataFetchDerivedStore<TDefinition, TQuery>
extends DataFetchStore<TDefinition, TQuery> {
hasNextPage: boolean
hasPrevPage: boolean
supportsSearch: boolean
supportsSort: boolean
supportsPagination: boolean
}
export interface DataFetchParams<
TDatasource,
TQuery = SearchFilters | undefined
> {
API: APIClient
datasource: TDatasource
query: TQuery
options?: {}
}
/**
* Parent class which handles the implementation of fetching data from an
* internal table or datasource plus.
* For other types of datasource, this class is overridden and extended.
*/
export default class DataFetch {
export default abstract class DataFetch<
TDatasource extends {},
TDefinition extends {
schema?: Record<string, any> | null
primaryDisplay?: string
},
TQuery extends {} = SearchFilters
> {
API: APIClient
features: {
supportsSearch: boolean
supportsSort: boolean
supportsPagination: boolean
}
options: {
datasource: TDatasource
limit: number
// Search config
filter: UISearchFilter | LegacyFilter[] | null
query: TQuery
// Sorting config
sortColumn: string | null
sortOrder: SortOrder
sortType: SortType | null
// Pagination config
paginate: boolean
// Client side feature customisation
clientSideSearching: boolean
clientSideSorting: boolean
clientSideLimiting: boolean
}
store: Writable<DataFetchStore<TDefinition, TQuery>>
derivedStore: Readable<DataFetchDerivedStore<TDefinition, TQuery>>
/**
* Constructs a new DataFetch instance.
* @param opts the fetch options
*/
constructor(opts) {
// API client
this.API = null
constructor(opts: DataFetchParams<TDatasource, TQuery>) {
// Feature flags
this.features = {
supportsSearch: false,
@ -29,12 +106,12 @@ export default class DataFetch {
// Config
this.options = {
datasource: null,
datasource: opts.datasource,
limit: 10,
// Search config
filter: null,
query: null,
query: opts.query,
// Sorting config
sortColumn: null,
@ -57,11 +134,11 @@ export default class DataFetch {
schema: null,
loading: false,
loaded: false,
query: null,
query: opts.query,
pageNumber: 0,
cursor: null,
cursors: [],
resetKey: Math.random(),
resetKey: Math.random().toString(),
error: null,
})
@ -118,7 +195,10 @@ export default class DataFetch {
/**
* Gets the default sort column for this datasource
*/
getDefaultSortColumn(definition, schema) {
getDefaultSortColumn(
definition: { primaryDisplay?: string } | null,
schema: Record<string, any>
): string | null {
if (definition?.primaryDisplay && schema[definition.primaryDisplay]) {
return definition.primaryDisplay
} else {
@ -130,13 +210,13 @@ export default class DataFetch {
* Fetches a fresh set of data from the server, resetting pagination
*/
async getInitialData() {
const { datasource, filter, paginate } = this.options
const { filter, paginate } = this.options
// Fetch datasource definition and extract sort properties if configured
const definition = await this.getDefinition(datasource)
const definition = await this.getDefinition()
// Determine feature flags
const features = this.determineFeatureFlags(definition)
const features = await this.determineFeatureFlags()
this.features = {
supportsSearch: !!features?.supportsSearch,
supportsSort: !!features?.supportsSort,
@ -144,11 +224,11 @@ export default class DataFetch {
}
// Fetch and enrich schema
let schema = this.getSchema(datasource, definition)
schema = this.enrichSchema(schema)
let schema = this.getSchema(definition)
if (!schema) {
return
}
schema = this.enrichSchema(schema)
// If an invalid sort column is specified, delete it
if (this.options.sortColumn && !schema[this.options.sortColumn]) {
@ -172,20 +252,25 @@ export default class DataFetch {
if (
fieldSchema?.type === FieldType.NUMBER ||
fieldSchema?.type === FieldType.BIGINT ||
fieldSchema?.calculationType
("calculationType" in fieldSchema && fieldSchema?.calculationType)
) {
this.options.sortType = SortType.NUMBER
}
// If no sort order, default to ascending
if (!this.options.sortOrder) {
this.options.sortOrder = SortOrder.ASCENDING
} else {
// Ensure sortOrder matches the enum
this.options.sortOrder =
this.options.sortOrder.toLowerCase() as SortOrder
}
}
// Build the query
let query = this.options.query
if (!query) {
query = buildQuery(filter)
query = buildQuery(filter ?? undefined) as TQuery
}
// Update store
@ -210,7 +295,7 @@ export default class DataFetch {
info: page.info,
cursors: paginate && page.hasNextPage ? [null, page.cursor] : [null],
error: page.error,
resetKey: Math.random(),
resetKey: Math.random().toString(),
}))
}
@ -238,8 +323,8 @@ export default class DataFetch {
}
// If we don't support sorting, do a client-side sort
if (!this.features.supportsSort && clientSideSorting) {
rows = sort(rows, sortColumn, sortOrder, sortType)
if (!this.features.supportsSort && clientSideSorting && sortType) {
rows = sort(rows, sortColumn as any, sortOrder, sortType)
}
// If we don't support pagination, do a client-side limit
@ -256,49 +341,28 @@ export default class DataFetch {
}
}
/**
* Fetches a single page of data from the remote resource.
* Must be overridden by a datasource specific child class.
*/
async getData() {
return {
rows: [],
info: null,
hasNextPage: false,
cursor: null,
}
}
abstract getData(): Promise<{
rows: Row[]
info?: any
hasNextPage?: boolean
cursor?: any
error?: any
}>
/**
* Gets the definition for this datasource.
* Defaults to fetching a table definition.
* @param datasource
* @return {object} the definition
*/
async getDefinition(datasource) {
if (!datasource?.tableId) {
return null
}
try {
return await this.API.fetchTableDefinition(datasource.tableId)
} catch (error) {
this.store.update(state => ({
...state,
error,
}))
return null
}
}
abstract getDefinition(): Promise<TDefinition | null>
/**
* Gets the schema definition for a datasource.
* Defaults to getting the "schema" property of the definition.
* @param datasource the datasource
* @param definition the datasource definition
* @return {object} the schema
*/
getSchema(datasource, definition) {
return definition?.schema
getSchema(definition: TDefinition | null): Record<string, any> | undefined {
return definition?.schema ?? undefined
}
/**
@ -307,53 +371,56 @@ export default class DataFetch {
* @param schema the datasource schema
* @return {object} the enriched datasource schema
*/
enrichSchema(schema) {
if (schema == null) {
return null
}
private enrichSchema(schema: TableSchema): TableSchema {
// Check for any JSON fields so we can add any top level properties
let jsonAdditions = {}
Object.keys(schema).forEach(fieldKey => {
let jsonAdditions: Record<string, { type: string; nestedJSON: true }> = {}
for (const fieldKey of Object.keys(schema)) {
const fieldSchema = schema[fieldKey]
if (fieldSchema?.type === FieldType.JSON) {
if (fieldSchema.type === FieldType.JSON) {
const jsonSchema = convertJSONSchemaToTableSchema(fieldSchema, {
squashObjects: true,
})
Object.keys(jsonSchema).forEach(jsonKey => {
jsonAdditions[`${fieldKey}.${jsonKey}`] = {
type: jsonSchema[jsonKey].type,
nestedJSON: true,
}) as Record<string, { type: string }> | null // TODO: remove when convertJSONSchemaToTableSchema is typed
if (jsonSchema) {
for (const jsonKey of Object.keys(jsonSchema)) {
jsonAdditions[`${fieldKey}.${jsonKey}`] = {
type: jsonSchema[jsonKey].type,
nestedJSON: true,
}
}
})
}
}
})
schema = { ...schema, ...jsonAdditions }
}
// Ensure schema is in the correct structure
let enrichedSchema = {}
Object.entries(schema).forEach(([fieldName, fieldSchema]) => {
if (typeof fieldSchema === "string") {
enrichedSchema[fieldName] = {
type: fieldSchema,
name: fieldName,
}
} else {
enrichedSchema[fieldName] = {
...fieldSchema,
name: fieldName,
let enrichedSchema: TableSchema = {}
Object.entries({ ...schema, ...jsonAdditions }).forEach(
([fieldName, fieldSchema]) => {
if (typeof fieldSchema === "string") {
enrichedSchema[fieldName] = {
type: fieldSchema,
name: fieldName,
}
} else {
enrichedSchema[fieldName] = {
...fieldSchema,
type: fieldSchema.type as any, // TODO: check type union definition conflicts
name: fieldName,
}
}
}
})
)
return enrichedSchema
}
/**
* Determine the feature flag for this datasource definition
* @param definition
* Determine the feature flag for this datasource
*/
determineFeatureFlags(_definition) {
async determineFeatureFlags(): Promise<{
supportsPagination: boolean
supportsSearch?: boolean
supportsSort?: boolean
}> {
return {
supportsSearch: false,
supportsSort: false,
@ -365,12 +432,11 @@ export default class DataFetch {
* Resets the data set and updates options
* @param newOptions any new options
*/
async update(newOptions) {
async update(newOptions: any) {
// Check if any settings have actually changed
let refresh = false
const entries = Object.entries(newOptions || {})
for (let [key, value] of entries) {
const oldVal = this.options[key] == null ? null : this.options[key]
for (const [key, value] of Object.entries(newOptions || {})) {
const oldVal = this.options[key as keyof typeof this.options] ?? null
const newVal = value == null ? null : value
if (JSON.stringify(newVal) !== JSON.stringify(oldVal)) {
refresh = true
@ -437,7 +503,7 @@ export default class DataFetch {
* @param state the current store state
* @return {boolean} whether there is a next page of data or not
*/
hasNextPage(state) {
private hasNextPage(state: DataFetchStore<TDefinition, TQuery>): boolean {
return state.cursors[state.pageNumber + 1] != null
}
@ -447,7 +513,7 @@ export default class DataFetch {
* @param state the current store state
* @return {boolean} whether there is a previous page of data or not
*/
hasPrevPage(state) {
private hasPrevPage(state: { pageNumber: number }): boolean {
return state.pageNumber > 0
}

View File

@ -1,7 +1,27 @@
import DataFetch from "./DataFetch.js"
import { Row } from "@budibase/types"
import DataFetch from "./DataFetch"
export interface FieldDatasource {
tableId: string
fieldType: "attachment" | "array"
value: string[] | Row[]
}
export interface FieldDefinition {
schema?: Record<string, { type: string }> | null
}
function isArrayOfStrings(value: string[] | Row[]): value is string[] {
return Array.isArray(value) && !!value[0] && typeof value[0] !== "object"
}
export default class FieldFetch extends DataFetch<
FieldDatasource,
FieldDefinition
> {
async getDefinition(): Promise<FieldDefinition | null> {
const { datasource } = this.options
export default class FieldFetch extends DataFetch {
async getDefinition(datasource) {
// Field sources have their schema statically defined
let schema
if (datasource.fieldType === "attachment") {
@ -28,8 +48,8 @@ export default class FieldFetch extends DataFetch {
// These sources will be available directly from context
const data = datasource?.value || []
let rows
if (Array.isArray(data) && data[0] && typeof data[0] !== "object") {
let rows: Row[]
if (isArrayOfStrings(data)) {
rows = data.map(value => ({ value }))
} else {
rows = data

View File

@ -1,9 +1,22 @@
import { get } from "svelte/store"
import DataFetch from "./DataFetch.js"
import DataFetch, { DataFetchParams } from "./DataFetch"
import { TableNames } from "../constants"
export default class GroupUserFetch extends DataFetch {
constructor(opts) {
interface GroupUserQuery {
groupId: string
emailSearch: string
}
interface GroupUserDatasource {
tableId: TableNames.USERS
}
export default class GroupUserFetch extends DataFetch<
GroupUserDatasource,
{},
GroupUserQuery
> {
constructor(opts: DataFetchParams<GroupUserDatasource, GroupUserQuery>) {
super({
...opts,
datasource: {
@ -12,7 +25,7 @@ export default class GroupUserFetch extends DataFetch {
})
}
determineFeatureFlags() {
async determineFeatureFlags() {
return {
supportsSearch: true,
supportsSort: false,
@ -28,11 +41,12 @@ export default class GroupUserFetch extends DataFetch {
async getData() {
const { query, cursor } = get(this.store)
try {
const res = await this.API.getGroupUsers({
id: query.groupId,
emailSearch: query.emailSearch,
bookmark: cursor,
bookmark: cursor ?? undefined,
})
return {

View File

@ -1,8 +1,10 @@
import FieldFetch from "./FieldFetch.js"
import FieldFetch from "./FieldFetch"
import { getJSONArrayDatasourceSchema } from "../utils/json"
export default class JSONArrayFetch extends FieldFetch {
async getDefinition(datasource) {
async getDefinition() {
const { datasource } = this.options
// JSON arrays need their table definitions fetched.
// We can then extract their schema as a subset of the table schema.
try {

View File

@ -1,21 +0,0 @@
import DataFetch from "./DataFetch.js"
export default class NestedProviderFetch extends DataFetch {
async getDefinition(datasource) {
// Nested providers should already have exposed their own schema
return {
schema: datasource?.value?.schema,
primaryDisplay: datasource?.value?.primaryDisplay,
}
}
async getData() {
const { datasource } = this.options
// Pull the rows from the existing data provider
return {
rows: datasource?.value?.rows || [],
hasNextPage: false,
cursor: null,
}
}
}

View File

@ -0,0 +1,39 @@
import { Row, TableSchema } from "@budibase/types"
import DataFetch from "./DataFetch"
interface NestedProviderDatasource {
value?: {
schema: TableSchema
primaryDisplay: string
rows: Row[]
}
}
interface NestedProviderDefinition {
schema?: TableSchema
primaryDisplay?: string
}
export default class NestedProviderFetch extends DataFetch<
NestedProviderDatasource,
NestedProviderDefinition
> {
async getDefinition() {
const { datasource } = this.options
// Nested providers should already have exposed their own schema
return {
schema: datasource?.value?.schema,
primaryDisplay: datasource?.value?.primaryDisplay,
}
}
async getData() {
const { datasource } = this.options
// Pull the rows from the existing data provider
return {
rows: datasource?.value?.rows || [],
hasNextPage: false,
cursor: null,
}
}
}

View File

@ -1,11 +1,13 @@
import FieldFetch from "./FieldFetch.js"
import FieldFetch from "./FieldFetch"
import {
getJSONArrayDatasourceSchema,
generateQueryArraySchemas,
} from "../utils/json"
export default class QueryArrayFetch extends FieldFetch {
async getDefinition(datasource) {
async getDefinition() {
const { datasource } = this.options
if (!datasource?.tableId) {
return null
}
@ -14,10 +16,14 @@ export default class QueryArrayFetch extends FieldFetch {
try {
const table = await this.API.fetchQueryDefinition(datasource.tableId)
const schema = generateQueryArraySchemas(
table?.schema,
table?.nestedSchemaFields
table.schema,
table.nestedSchemaFields
)
return { schema: getJSONArrayDatasourceSchema(schema, datasource) }
const result = {
schema: getJSONArrayDatasourceSchema(schema, datasource),
}
return result
} catch (error) {
return null
}

View File

@ -1,9 +1,24 @@
import DataFetch from "./DataFetch.js"
import DataFetch from "./DataFetch"
import { Helpers } from "@budibase/bbui"
import { ExecuteQueryRequest, Query } from "@budibase/types"
import { get } from "svelte/store"
export default class QueryFetch extends DataFetch {
determineFeatureFlags(definition) {
interface QueryDatasource {
_id: string
fields: Record<string, any> & {
pagination?: {
type: string
location: string
pageParam: string
}
}
queryParams?: Record<string, string>
parameters: { name: string; default: string }[]
}
export default class QueryFetch extends DataFetch<QueryDatasource, Query> {
async determineFeatureFlags() {
const definition = await this.getDefinition()
const supportsPagination =
!!definition?.fields?.pagination?.type &&
!!definition?.fields?.pagination?.location &&
@ -11,7 +26,9 @@ export default class QueryFetch extends DataFetch {
return { supportsPagination }
}
async getDefinition(datasource) {
async getDefinition() {
const { datasource } = this.options
if (!datasource?._id) {
return null
}
@ -40,17 +57,17 @@ export default class QueryFetch extends DataFetch {
const type = definition?.fields?.pagination?.type
// Set the default query params
let parameters = Helpers.cloneDeep(datasource?.queryParams || {})
for (let param of datasource?.parameters || {}) {
const parameters = Helpers.cloneDeep(datasource.queryParams || {})
for (const param of datasource?.parameters || []) {
if (!parameters[param.name]) {
parameters[param.name] = param.default
}
}
// Add pagination to query if supported
let queryPayload = { parameters }
const queryPayload: ExecuteQueryRequest = { parameters }
if (paginate && supportsPagination) {
const requestCursor = type === "page" ? parseInt(cursor || 1) : cursor
const requestCursor = type === "page" ? parseInt(cursor || "1") : cursor
queryPayload.pagination = { page: requestCursor, limit }
}
@ -65,7 +82,7 @@ export default class QueryFetch extends DataFetch {
if (paginate && supportsPagination) {
if (type === "page") {
// For "page number" pagination, increment the existing page number
nextCursor = queryPayload.pagination.page + 1
nextCursor = queryPayload.pagination!.page! + 1
hasNextPage = data?.length === limit && limit > 0
} else {
// For "cursor" pagination, the cursor should be in the response

View File

@ -1,20 +0,0 @@
import DataFetch from "./DataFetch.js"
export default class RelationshipFetch extends DataFetch {
async getData() {
const { datasource } = this.options
if (!datasource?.rowId || !datasource?.rowTableId) {
return { rows: [] }
}
try {
const res = await this.API.fetchRelationshipData(
datasource.rowTableId,
datasource.rowId,
datasource.fieldName
)
return { rows: res }
} catch (error) {
return { rows: [] }
}
}
}

View File

@ -0,0 +1,48 @@
import { Table } from "@budibase/types"
import DataFetch from "./DataFetch"
interface RelationshipDatasource {
tableId: string
rowId: string
rowTableId: string
fieldName: string
}
export default class RelationshipFetch extends DataFetch<
RelationshipDatasource,
Table
> {
async getDefinition() {
const { datasource } = this.options
if (!datasource?.tableId) {
return null
}
try {
return await this.API.fetchTableDefinition(datasource.tableId)
} catch (error: any) {
this.store.update(state => ({
...state,
error,
}))
return null
}
}
async getData() {
const { datasource } = this.options
if (!datasource?.rowId || !datasource?.rowTableId) {
return { rows: [] }
}
try {
const res = await this.API.fetchRelationshipData(
datasource.rowTableId,
datasource.rowId,
datasource.fieldName
)
return { rows: res }
} catch (error) {
return { rows: [] }
}
}
}

View File

@ -1,9 +1,9 @@
import { get } from "svelte/store"
import DataFetch from "./DataFetch.js"
import { SortOrder } from "@budibase/types"
import DataFetch from "./DataFetch"
import { SortOrder, Table, UITable } from "@budibase/types"
export default class TableFetch extends DataFetch {
determineFeatureFlags() {
export default class TableFetch extends DataFetch<UITable, Table> {
async determineFeatureFlags() {
return {
supportsSearch: true,
supportsSort: true,
@ -11,6 +11,23 @@ export default class TableFetch extends DataFetch {
}
}
async getDefinition() {
const { datasource } = this.options
if (!datasource?.tableId) {
return null
}
try {
return await this.API.fetchTableDefinition(datasource.tableId)
} catch (error: any) {
this.store.update(state => ({
...state,
error,
}))
return null
}
}
async getData() {
const { datasource, limit, sortColumn, sortOrder, sortType, paginate } =
this.options
@ -23,7 +40,7 @@ export default class TableFetch extends DataFetch {
query,
limit,
sort: sortColumn,
sortOrder: sortOrder?.toLowerCase() ?? SortOrder.ASCENDING,
sortOrder: sortOrder ?? SortOrder.ASCENDING,
sortType,
paginate,
bookmark: cursor,

View File

@ -1,10 +1,28 @@
import { get } from "svelte/store"
import DataFetch from "./DataFetch.js"
import DataFetch, { DataFetchParams } from "./DataFetch"
import { TableNames } from "../constants"
import { utils } from "@budibase/shared-core"
import {
BasicOperator,
SearchFilters,
SearchUsersRequest,
} from "@budibase/types"
export default class UserFetch extends DataFetch {
constructor(opts) {
interface UserFetchQuery {
appId: string
paginated: boolean
}
interface UserDatasource {
tableId: string
}
export default class UserFetch extends DataFetch<
UserDatasource,
{},
UserFetchQuery
> {
constructor(opts: DataFetchParams<UserDatasource, UserFetchQuery>) {
super({
...opts,
datasource: {
@ -13,7 +31,7 @@ export default class UserFetch extends DataFetch {
})
}
determineFeatureFlags() {
async determineFeatureFlags() {
return {
supportsSearch: true,
supportsSort: false,
@ -22,9 +40,7 @@ export default class UserFetch extends DataFetch {
}
async getDefinition() {
return {
schema: {},
}
return { schema: {} }
}
async getData() {
@ -32,15 +48,16 @@ export default class UserFetch extends DataFetch {
const { cursor, query } = get(this.store)
// Convert old format to new one - we now allow use of the lucene format
const { appId, paginated, ...rest } = query || {}
const finalQuery = utils.isSupportedUserSearch(rest)
? query
: { string: { email: null } }
const { appId, paginated, ...rest } = query
const finalQuery: SearchFilters = utils.isSupportedUserSearch(rest)
? rest
: { [BasicOperator.EMPTY]: { email: null } }
try {
const opts = {
bookmark: cursor,
query: finalQuery,
const opts: SearchUsersRequest = {
bookmark: cursor ?? undefined,
query: finalQuery ?? undefined,
appId: appId,
paginate: paginated || paginate,
limit,

View File

@ -1,23 +0,0 @@
import DataFetch from "./DataFetch.js"
export default class ViewFetch extends DataFetch {
getSchema(datasource, definition) {
return definition?.views?.[datasource.name]?.schema
}
async getData() {
const { datasource } = this.options
try {
const res = await this.API.fetchViewData(datasource.name, {
calculation: datasource.calculation,
field: datasource.field,
groupBy: datasource.groupBy,
tableId: datasource.tableId,
})
return { rows: res || [] }
} catch (error) {
console.error(error)
return { rows: [] }
}
}
}

View File

@ -0,0 +1,44 @@
import { Table, View } from "@budibase/types"
import DataFetch from "./DataFetch"
type ViewV1 = View & { name: string }
export default class ViewFetch extends DataFetch<ViewV1, Table> {
async getDefinition() {
const { datasource } = this.options
if (!datasource?.tableId) {
return null
}
try {
return await this.API.fetchTableDefinition(datasource.tableId)
} catch (error: any) {
this.store.update(state => ({
...state,
error,
}))
return null
}
}
getSchema(definition: Table) {
const { datasource } = this.options
return definition?.views?.[datasource.name]?.schema
}
async getData() {
const { datasource } = this.options
try {
const res = await this.API.fetchViewData(datasource.name, {
calculation: datasource.calculation,
field: datasource.field,
groupBy: datasource.groupBy,
tableId: datasource.tableId,
})
return { rows: res || [] }
} catch (error) {
console.error(error, { datasource })
return { rows: [] }
}
}
}

View File

@ -1,9 +1,10 @@
import { ViewV2Type } from "@budibase/types"
import DataFetch from "./DataFetch.js"
import { SortOrder, UIView, ViewV2, ViewV2Type } from "@budibase/types"
import DataFetch from "./DataFetch"
import { get } from "svelte/store"
import { helpers } from "@budibase/shared-core"
export default class ViewV2Fetch extends DataFetch {
determineFeatureFlags() {
export default class ViewV2Fetch extends DataFetch<UIView, ViewV2> {
async determineFeatureFlags() {
return {
supportsSearch: true,
supportsSort: true,
@ -11,18 +12,13 @@ export default class ViewV2Fetch extends DataFetch {
}
}
getSchema(datasource, definition) {
return definition?.schema
}
async getDefinition() {
const { datasource } = this.options
async getDefinition(datasource) {
if (!datasource?.id) {
return null
}
try {
const res = await this.API.viewV2.fetchDefinition(datasource.id)
return res?.data
} catch (error) {
} catch (error: any) {
this.store.update(state => ({
...state,
error,
@ -42,8 +38,10 @@ export default class ViewV2Fetch extends DataFetch {
// If this is a calculation view and we have no calculations, return nothing
if (
definition.type === ViewV2Type.CALCULATION &&
!Object.values(definition.schema || {}).some(x => x.calculationType)
definition?.type === ViewV2Type.CALCULATION &&
!Object.values(definition.schema || {}).some(
helpers.views.isCalculationField
)
) {
return {
rows: [],
@ -56,25 +54,41 @@ export default class ViewV2Fetch extends DataFetch {
// If sort/filter params are not defined, update options to store the
// params built in to this view. This ensures that we can accurately
// compare old and new params and skip a redundant API call.
if (!sortColumn && definition.sort?.field) {
if (!sortColumn && definition?.sort?.field) {
this.options.sortColumn = definition.sort.field
this.options.sortOrder = definition.sort.order
this.options.sortOrder = definition.sort.order || SortOrder.ASCENDING
}
try {
const res = await this.API.viewV2.fetch(datasource.id, {
...(query ? { query } : {}),
const request = {
query,
paginate,
limit,
bookmark: cursor,
sort: sortColumn,
sortOrder: sortOrder?.toLowerCase(),
sortOrder: sortOrder,
sortType,
})
return {
rows: res?.rows || [],
hasNextPage: res?.hasNextPage || false,
cursor: res?.bookmark || null,
}
if (paginate) {
const res = await this.API.viewV2.fetch(datasource.id, {
...request,
paginate,
})
return {
rows: res?.rows || [],
hasNextPage: res?.hasNextPage || false,
cursor: res?.bookmark || null,
}
} else {
const res = await this.API.viewV2.fetch(datasource.id, {
...request,
paginate,
})
return {
rows: res?.rows || [],
hasNextPage: false,
cursor: null,
}
}
} catch (error) {
return {

View File

@ -1,57 +0,0 @@
import TableFetch from "./TableFetch.js"
import ViewFetch from "./ViewFetch.js"
import ViewV2Fetch from "./ViewV2Fetch.js"
import QueryFetch from "./QueryFetch.js"
import RelationshipFetch from "./RelationshipFetch.js"
import NestedProviderFetch from "./NestedProviderFetch.js"
import FieldFetch from "./FieldFetch.js"
import JSONArrayFetch from "./JSONArrayFetch.js"
import UserFetch from "./UserFetch.js"
import GroupUserFetch from "./GroupUserFetch.js"
import CustomFetch from "./CustomFetch.js"
import QueryArrayFetch from "./QueryArrayFetch.js"
const DataFetchMap = {
table: TableFetch,
view: ViewFetch,
viewV2: ViewV2Fetch,
query: QueryFetch,
link: RelationshipFetch,
user: UserFetch,
groupUser: GroupUserFetch,
custom: CustomFetch,
// Client specific datasource types
provider: NestedProviderFetch,
field: FieldFetch,
jsonarray: JSONArrayFetch,
queryarray: QueryArrayFetch,
}
// Constructs a new fetch model for a certain datasource
export const fetchData = ({ API, datasource, options }) => {
const Fetch = DataFetchMap[datasource?.type] || TableFetch
return new Fetch({ API, datasource, ...options })
}
// Creates an empty fetch instance with no datasource configured, so no data
// will initially be loaded
const createEmptyFetchInstance = ({ API, datasource }) => {
const handler = DataFetchMap[datasource?.type]
if (!handler) {
return null
}
return new handler({ API })
}
// Fetches the definition of any type of datasource
export const getDatasourceDefinition = async ({ API, datasource }) => {
const instance = createEmptyFetchInstance({ API, datasource })
return await instance?.getDefinition(datasource)
}
// Fetches the schema of any type of datasource
export const getDatasourceSchema = ({ API, datasource, definition }) => {
const instance = createEmptyFetchInstance({ API, datasource })
return instance?.getSchema(datasource, definition)
}

View File

@ -0,0 +1,91 @@
import TableFetch from "./TableFetch.js"
import ViewFetch from "./ViewFetch.js"
import ViewV2Fetch from "./ViewV2Fetch.js"
import QueryFetch from "./QueryFetch"
import RelationshipFetch from "./RelationshipFetch"
import NestedProviderFetch from "./NestedProviderFetch"
import FieldFetch from "./FieldFetch"
import JSONArrayFetch from "./JSONArrayFetch"
import UserFetch from "./UserFetch.js"
import GroupUserFetch from "./GroupUserFetch"
import CustomFetch from "./CustomFetch"
import QueryArrayFetch from "./QueryArrayFetch.js"
import { APIClient } from "../api/types.js"
const DataFetchMap = {
table: TableFetch,
view: ViewFetch,
viewV2: ViewV2Fetch,
query: QueryFetch,
link: RelationshipFetch,
user: UserFetch,
groupUser: GroupUserFetch,
custom: CustomFetch,
// Client specific datasource types
provider: NestedProviderFetch,
field: FieldFetch,
jsonarray: JSONArrayFetch,
queryarray: QueryArrayFetch,
}
// Constructs a new fetch model for a certain datasource
export const fetchData = ({ API, datasource, options }: any) => {
const Fetch =
DataFetchMap[datasource?.type as keyof typeof DataFetchMap] || TableFetch
return new Fetch({ API, datasource, ...options })
}
// Creates an empty fetch instance with no datasource configured, so no data
// will initially be loaded
const createEmptyFetchInstance = <
TDatasource extends {
type: keyof typeof DataFetchMap
}
>({
API,
datasource,
}: {
API: APIClient
datasource: TDatasource
}) => {
const handler = DataFetchMap[datasource?.type as keyof typeof DataFetchMap]
if (!handler) {
return null
}
return new handler({ API, datasource: null as any, query: null as any })
}
// Fetches the definition of any type of datasource
export const getDatasourceDefinition = async <
TDatasource extends {
type: keyof typeof DataFetchMap
}
>({
API,
datasource,
}: {
API: APIClient
datasource: TDatasource
}) => {
const instance = createEmptyFetchInstance({ API, datasource })
return await instance?.getDefinition()
}
// Fetches the schema of any type of datasource
export const getDatasourceSchema = <
TDatasource extends {
type: keyof typeof DataFetchMap
}
>({
API,
datasource,
definition,
}: {
API: APIClient
datasource: TDatasource
definition?: any
}) => {
const instance = createEmptyFetchInstance({ API, datasource })
return instance?.getSchema(definition)
}

View File

@ -0,0 +1,23 @@
import { JsonFieldMetadata, QuerySchema } from "@budibase/types"
type Schema = Record<string, QuerySchema | string>
declare module "./json" {
export const getJSONArrayDatasourceSchema: (
tableSchema: Schema,
datasource: any
) => Record<string, { type: string; name: string; prefixKeys: string }>
export const generateQueryArraySchemas: (
schema: Schema,
nestedSchemaFields?: Record<string, Schema>
) => Schema
export const convertJSONSchemaToTableSchema: (
jsonSchema: JsonFieldMetadata,
options: {
squashObjects?: boolean
prefixKeys?: string
}
) => Record<string, { type: string; name: string; prefixKeys: string }>
}

View File

@ -355,7 +355,7 @@ async function execute(
ExecuteQueryRequest,
ExecuteV2QueryResponse | ExecuteV1QueryResponse
>,
opts: any = { rowsOnly: false, isAutomation: false }
opts = { rowsOnly: false, isAutomation: false }
) {
const db = context.getAppDB()
@ -416,7 +416,7 @@ export async function executeV1(
export async function executeV2(
ctx: UserCtx<ExecuteQueryRequest, ExecuteV2QueryResponse>
) {
return execute(ctx, { rowsOnly: false })
return execute(ctx, { rowsOnly: false, isAutomation: false })
}
export async function executeV2AsAutomation(

View File

@ -1,16 +1,16 @@
import {
UserCtx,
ViewV2,
SearchRowResponse,
SearchViewRowRequest,
RequiredKeys,
RowSearchParams,
PaginatedSearchRowResponse,
} from "@budibase/types"
import sdk from "../../../sdk"
import { context } from "@budibase/backend-core"
export async function searchView(
ctx: UserCtx<SearchViewRowRequest, SearchRowResponse>
ctx: UserCtx<SearchViewRowRequest, PaginatedSearchRowResponse>
) {
const { viewId } = ctx.params
@ -49,7 +49,13 @@ export async function searchView(
user: sdk.users.getUserContextBindings(ctx.user),
})
result.rows.forEach(r => (r._viewId = view.id))
ctx.body = result
ctx.body = {
rows: result.rows,
bookmark: result.bookmark,
hasNextPage: result.hasNextPage,
totalRows: result.totalRows,
}
}
function getSortOptions(request: SearchViewRowRequest, view: ViewV2) {

View File

@ -3,7 +3,10 @@ import { Datasource, Row, Query } from "@budibase/types"
export type WorkerCallback = (error: any, response?: any) => void
export interface QueryEvent
extends Omit<Query, "datasourceId" | "name" | "parameters" | "readable"> {
extends Omit<
Query,
"datasourceId" | "name" | "parameters" | "readable" | "nestedSchemaFields"
> {
appId?: string
datasource: Datasource
pagination?: any

View File

@ -911,8 +911,8 @@ export function sort<T extends Record<string, any>>(
* @param docs the data
* @param limit the number of docs to limit to
*/
export function limit<T>(docs: T[], limit: string): T[] {
const numLimit = parseFloat(limit)
export function limit<T>(docs: T[], limit: string | number): T[] {
const numLimit = typeof limit === "number" ? limit : parseFloat(limit)
if (isNaN(numLimit)) {
return docs
}

View File

@ -109,7 +109,9 @@ export function trimOtherProps(object: any, allowedProps: string[]) {
return result
}
export function isSupportedUserSearch(query: SearchFilters) {
export function isSupportedUserSearch(
query: SearchFilters
): query is SearchFilters {
const allowed = [
{ op: BasicOperator.STRING, key: "email" },
{ op: BasicOperator.EQUAL, key: "_id" },

View File

@ -40,6 +40,10 @@ export interface ExecuteQueryRequest {
export type ExecuteV1QueryResponse = Record<string, any>[]
export interface ExecuteV2QueryResponse {
data: Record<string, any>[]
pagination?: {
page: number
cursor: string
}
}
export interface DeleteQueryResponse {

View File

@ -24,4 +24,5 @@ export interface PaginationRequest extends BasicPaginationRequest {
export interface PaginationResponse {
bookmark: string | number | undefined
hasNextPage?: boolean
totalRows?: number
}

View File

@ -1,4 +1,5 @@
import { Document } from "../document"
import { Row } from "./row"
export interface QuerySchema {
name?: string
@ -13,6 +14,7 @@ export interface Query extends Document {
fields: RestQueryFields | any
transformer: string | null
schema: Record<string, QuerySchema | string>
nestedSchemaFields?: Record<string, Record<string, QuerySchema | string>>
readable: boolean
queryVerb: string
// flag to state whether the default bindings are empty strings (old behaviour) or null
@ -29,7 +31,7 @@ export interface QueryParameter {
}
export interface QueryResponse {
rows: any[]
rows: Row[]
keys: string[]
info: any
extra: any

View File

@ -227,6 +227,7 @@ interface OtherFieldMetadata extends BaseFieldSchema {
| FieldType.OPTIONS
| FieldType.BOOLEAN
| FieldType.BIGINT
| FieldType.JSON
>
}

View File

@ -1,8 +1,6 @@
import { UITable, UIView } from "@budibase/types"
export type UIDatasource = (UITable | UIView) & {
type: string
}
export type UIDatasource = UITable | UIView
export interface UIFieldMutation {
visible?: boolean

View File

@ -1,38 +0,0 @@
import {
Row,
SortOrder,
UIDatasource,
UILegacyFilter,
UISearchFilter,
} from "@budibase/types"
export interface UIFetchAPI {
definition: UIDatasource
getInitialData: () => Promise<void>
loading: any
loaded: boolean
resetKey: string | null
error: any
hasNextPage: boolean
nextPage: () => Promise<void>
rows: Row[]
options?: {
datasource?: {
tableId: string
id: string
}
}
update: ({
sortOrder,
sortColumn,
}: {
sortOrder?: SortOrder
sortColumn?: string
filter?: UILegacyFilter[] | UISearchFilter
}) => any
}

View File

@ -6,4 +6,3 @@ export * from "./view"
export * from "./user"
export * from "./filters"
export * from "./rows"
export * from "./fetch"