Merge branch 'master' into type-portal-oidc-store
This commit is contained in:
commit
4d0a1b03ce
|
@ -0,0 +1,3 @@
|
||||||
|
declare module "./helpers" {
|
||||||
|
export const cloneDeep: <T>(obj: T) => T
|
||||||
|
}
|
|
@ -43,7 +43,6 @@
|
||||||
export let showDataProviders = true
|
export let showDataProviders = true
|
||||||
|
|
||||||
const dispatch = createEventDispatcher()
|
const dispatch = createEventDispatcher()
|
||||||
const arrayTypes = ["attachment", "array"]
|
|
||||||
|
|
||||||
let anchorRight, dropdownRight
|
let anchorRight, dropdownRight
|
||||||
let drawer
|
let drawer
|
||||||
|
@ -116,8 +115,11 @@
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
$: fields = bindings
|
$: fields = bindings
|
||||||
.filter(x => arrayTypes.includes(x.fieldSchema?.type))
|
.filter(
|
||||||
.filter(x => x.fieldSchema?.tableId != null)
|
x =>
|
||||||
|
x.fieldSchema?.type === "attachment" ||
|
||||||
|
(x.fieldSchema?.type === "array" && x.tableId)
|
||||||
|
)
|
||||||
.map(binding => {
|
.map(binding => {
|
||||||
const { providerId, readableBinding, runtimeBinding } = binding
|
const { providerId, readableBinding, runtimeBinding } = binding
|
||||||
const { name, type, tableId } = binding.fieldSchema
|
const { name, type, tableId } = binding.fieldSchema
|
||||||
|
|
|
@ -1,12 +1,12 @@
|
||||||
import { API } from "api"
|
import { API } from "api"
|
||||||
import TableFetch from "@budibase/frontend-core/src/fetch/TableFetch.js"
|
import TableFetch from "@budibase/frontend-core/src/fetch/TableFetch"
|
||||||
import ViewFetch from "@budibase/frontend-core/src/fetch/ViewFetch.js"
|
import ViewFetch from "@budibase/frontend-core/src/fetch/ViewFetch"
|
||||||
import QueryFetch from "@budibase/frontend-core/src/fetch/QueryFetch.js"
|
import QueryFetch from "@budibase/frontend-core/src/fetch/QueryFetch"
|
||||||
import RelationshipFetch from "@budibase/frontend-core/src/fetch/RelationshipFetch.js"
|
import RelationshipFetch from "@budibase/frontend-core/src/fetch/RelationshipFetch"
|
||||||
import NestedProviderFetch from "@budibase/frontend-core/src/fetch/NestedProviderFetch.js"
|
import NestedProviderFetch from "@budibase/frontend-core/src/fetch/NestedProviderFetch"
|
||||||
import FieldFetch from "@budibase/frontend-core/src/fetch/FieldFetch.js"
|
import FieldFetch from "@budibase/frontend-core/src/fetch/FieldFetch"
|
||||||
import JSONArrayFetch from "@budibase/frontend-core/src/fetch/JSONArrayFetch.js"
|
import JSONArrayFetch from "@budibase/frontend-core/src/fetch/JSONArrayFetch"
|
||||||
import ViewV2Fetch from "@budibase/frontend-core/src/fetch/ViewV2Fetch.js"
|
import ViewV2Fetch from "@budibase/frontend-core/src/fetch/ViewV2Fetch"
|
||||||
import QueryArrayFetch from "@budibase/frontend-core/src/fetch/QueryArrayFetch"
|
import QueryArrayFetch from "@budibase/frontend-core/src/fetch/QueryArrayFetch"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -3,7 +3,15 @@ import { BaseAPIClient } from "./types"
|
||||||
|
|
||||||
export interface ViewEndpoints {
|
export interface ViewEndpoints {
|
||||||
// Missing request or response types
|
// 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>
|
exportView: (name: string, format: string) => Promise<any>
|
||||||
saveView: (view: any) => Promise<any>
|
saveView: (view: any) => Promise<any>
|
||||||
deleteView: (name: string) => Promise<any>
|
deleteView: (name: string) => Promise<any>
|
||||||
|
@ -20,7 +28,9 @@ export const buildViewEndpoints = (API: BaseAPIClient): ViewEndpoints => ({
|
||||||
fetchViewData: async (name, { field, groupBy, calculation }) => {
|
fetchViewData: async (name, { field, groupBy, calculation }) => {
|
||||||
const params = new URLSearchParams()
|
const params = new URLSearchParams()
|
||||||
if (calculation) {
|
if (calculation) {
|
||||||
params.set("field", field)
|
if (field) {
|
||||||
|
params.set("field", field)
|
||||||
|
}
|
||||||
params.set("calculation", calculation)
|
params.set("calculation", calculation)
|
||||||
}
|
}
|
||||||
if (groupBy) {
|
if (groupBy) {
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import {
|
import {
|
||||||
CreateViewRequest,
|
CreateViewRequest,
|
||||||
CreateViewResponse,
|
CreateViewResponse,
|
||||||
|
PaginatedSearchRowResponse,
|
||||||
SearchRowResponse,
|
SearchRowResponse,
|
||||||
SearchViewRowRequest,
|
SearchViewRowRequest,
|
||||||
UpdateViewRequest,
|
UpdateViewRequest,
|
||||||
|
@ -13,10 +14,14 @@ export interface ViewV2Endpoints {
|
||||||
fetchDefinition: (viewId: string) => Promise<ViewResponseEnriched>
|
fetchDefinition: (viewId: string) => Promise<ViewResponseEnriched>
|
||||||
create: (view: CreateViewRequest) => Promise<CreateViewResponse>
|
create: (view: CreateViewRequest) => Promise<CreateViewResponse>
|
||||||
update: (view: UpdateViewRequest) => Promise<UpdateViewResponse>
|
update: (view: UpdateViewRequest) => Promise<UpdateViewResponse>
|
||||||
fetch: (
|
fetch: <T extends SearchViewRowRequest>(
|
||||||
viewId: string,
|
viewId: string,
|
||||||
opts: SearchViewRowRequest
|
opts: T
|
||||||
) => Promise<SearchRowResponse>
|
) => Promise<
|
||||||
|
T extends { paginate: true }
|
||||||
|
? PaginatedSearchRowResponse
|
||||||
|
: SearchRowResponse
|
||||||
|
>
|
||||||
delete: (viewId: string) => Promise<void>
|
delete: (viewId: string) => Promise<void>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -59,7 +64,7 @@ export const buildViewV2Endpoints = (API: BaseAPIClient): ViewV2Endpoints => ({
|
||||||
* @param viewId the id of the view
|
* @param viewId the id of the view
|
||||||
* @param opts the search options
|
* @param opts the search options
|
||||||
*/
|
*/
|
||||||
fetch: async (viewId, opts) => {
|
fetch: async (viewId, opts: SearchViewRowRequest) => {
|
||||||
return await API.post({
|
return await API.post({
|
||||||
url: `/api/v2/views/${encodeURIComponent(viewId)}/search`,
|
url: `/api/v2/views/${encodeURIComponent(viewId)}/search`,
|
||||||
body: opts,
|
body: opts,
|
||||||
|
|
|
@ -69,7 +69,7 @@ export const deriveStores = (context: StoreContext): ConfigDerivedStore => {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Disable features for non DS+
|
// Disable features for non DS+
|
||||||
if (!["table", "viewV2"].includes(type)) {
|
if (type && !["table", "viewV2"].includes(type)) {
|
||||||
config.canAddRows = false
|
config.canAddRows = false
|
||||||
config.canEditRows = false
|
config.canEditRows = false
|
||||||
config.canDeleteRows = false
|
config.canDeleteRows = false
|
||||||
|
|
|
@ -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 { derived, get, Readable, Writable } from "svelte/store"
|
||||||
import { getDatasourceDefinition, getDatasourceSchema } from "../../../fetch"
|
import { getDatasourceDefinition, getDatasourceSchema } from "../../../fetch"
|
||||||
import { enrichSchemaWithRelColumns, memo } from "../../../utils"
|
import { enrichSchemaWithRelColumns, memo } from "../../../utils"
|
||||||
|
@ -71,10 +73,10 @@ export const deriveStores = (context: StoreContext): DerivedDatasourceStore => {
|
||||||
} = context
|
} = context
|
||||||
|
|
||||||
const schema = derived(definition, $definition => {
|
const schema = derived(definition, $definition => {
|
||||||
let schema: Record<string, UIFieldSchema> = getDatasourceSchema({
|
const schema: Record<string, any> | undefined = getDatasourceSchema({
|
||||||
API,
|
API,
|
||||||
datasource: get(datasource),
|
datasource: get(datasource) as any, // TODO: see line 1
|
||||||
definition: $definition,
|
definition: $definition ?? undefined,
|
||||||
})
|
})
|
||||||
if (!schema) {
|
if (!schema) {
|
||||||
return null
|
return null
|
||||||
|
@ -82,7 +84,7 @@ export const deriveStores = (context: StoreContext): DerivedDatasourceStore => {
|
||||||
|
|
||||||
// Ensure schema is configured as objects.
|
// Ensure schema is configured as objects.
|
||||||
// Certain datasources like queries use primitives.
|
// Certain datasources like queries use primitives.
|
||||||
Object.keys(schema || {}).forEach(key => {
|
Object.keys(schema).forEach(key => {
|
||||||
if (typeof schema[key] !== "object") {
|
if (typeof schema[key] !== "object") {
|
||||||
schema[key] = { name: key, type: schema[key] }
|
schema[key] = { name: key, type: schema[key] }
|
||||||
}
|
}
|
||||||
|
@ -130,13 +132,13 @@ export const deriveStores = (context: StoreContext): DerivedDatasourceStore => {
|
||||||
([$datasource, $definition]) => {
|
([$datasource, $definition]) => {
|
||||||
let type = $datasource?.type
|
let type = $datasource?.type
|
||||||
if (type === "provider") {
|
if (type === "provider") {
|
||||||
type = ($datasource as any).value?.datasource?.type
|
type = ($datasource as any).value?.datasource?.type // TODO: see line 1
|
||||||
}
|
}
|
||||||
// Handle calculation views
|
// Handle calculation views
|
||||||
if (type === "viewV2" && $definition?.type === ViewV2Type.CALCULATION) {
|
if (type === "viewV2" && $definition?.type === ViewV2Type.CALCULATION) {
|
||||||
return false
|
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 refreshDefinition = async () => {
|
||||||
const def = await getDatasourceDefinition({
|
const def = await getDatasourceDefinition({
|
||||||
API,
|
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
|
// Saves the datasource definition
|
||||||
|
@ -231,7 +233,7 @@ export const createActions = (context: StoreContext): ActionDatasourceStore => {
|
||||||
if ("default" in newDefinition.schema[column]) {
|
if ("default" in newDefinition.schema[column]) {
|
||||||
delete newDefinition.schema[column].default
|
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
|
// Adds a schema mutation for a single field
|
||||||
|
@ -307,7 +309,7 @@ export const createActions = (context: StoreContext): ActionDatasourceStore => {
|
||||||
await saveDefinition({
|
await saveDefinition({
|
||||||
...$definition,
|
...$definition,
|
||||||
schema: newSchema,
|
schema: newSchema,
|
||||||
} as any)
|
} as any) // TODO: see line 1
|
||||||
resetSchemaMutations()
|
resetSchemaMutations()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -10,9 +10,10 @@ import {
|
||||||
import { tick } from "svelte"
|
import { tick } from "svelte"
|
||||||
import { Helpers } from "@budibase/bbui"
|
import { Helpers } from "@budibase/bbui"
|
||||||
import { sleep } from "../../../utils/utils"
|
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 { getRelatedTableValues } from "../../../utils"
|
||||||
import { Store as StoreContext } from "."
|
import { Store as StoreContext } from "."
|
||||||
|
import DataFetch from "../../../fetch/DataFetch"
|
||||||
|
|
||||||
interface IndexedUIRow extends UIRow {
|
interface IndexedUIRow extends UIRow {
|
||||||
__idx: number
|
__idx: number
|
||||||
|
@ -20,7 +21,7 @@ interface IndexedUIRow extends UIRow {
|
||||||
|
|
||||||
interface RowStore {
|
interface RowStore {
|
||||||
rows: Writable<UIRow[]>
|
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>
|
loaded: Writable<boolean>
|
||||||
refreshing: Writable<boolean>
|
refreshing: Writable<boolean>
|
||||||
loading: Writable<boolean>
|
loading: Writable<boolean>
|
||||||
|
@ -225,7 +226,7 @@ export const createActions = (context: StoreContext): RowActionStore => {
|
||||||
})
|
})
|
||||||
|
|
||||||
// Subscribe to changes of this fetch model
|
// Subscribe to changes of this fetch model
|
||||||
unsubscribe = newFetch.subscribe(async ($fetch: UIFetchAPI) => {
|
unsubscribe = newFetch.subscribe(async $fetch => {
|
||||||
if ($fetch.error) {
|
if ($fetch.error) {
|
||||||
// Present a helpful error to the user
|
// Present a helpful error to the user
|
||||||
let message = "An unknown error occurred"
|
let message = "An unknown error occurred"
|
||||||
|
@ -253,7 +254,7 @@ export const createActions = (context: StoreContext): RowActionStore => {
|
||||||
|
|
||||||
// Reset state properties when dataset changes
|
// Reset state properties when dataset changes
|
||||||
if (!$instanceLoaded || resetRows) {
|
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
|
// Reset scroll state when data changes
|
||||||
|
|
|
@ -32,8 +32,8 @@ export const Cookies = {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Table names
|
// Table names
|
||||||
export const TableNames = {
|
export const enum TableNames {
|
||||||
USERS: "ta_users",
|
USERS = "ta_users",
|
||||||
}
|
}
|
||||||
|
|
||||||
export const BudibaseRoles = {
|
export const BudibaseRoles = {
|
||||||
|
|
|
@ -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
|
// Gets the correct Budibase type for a JS value
|
||||||
getType(value) {
|
getType(value: any) {
|
||||||
if (value == null) {
|
if (value == null) {
|
||||||
return "string"
|
return "string"
|
||||||
}
|
}
|
||||||
|
@ -22,7 +31,7 @@ export default class CustomFetch extends DataFetch {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parses the custom data into an array format
|
// Parses the custom data into an array format
|
||||||
parseCustomData(data) {
|
parseCustomData(data: any) {
|
||||||
if (!data) {
|
if (!data) {
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
|
@ -55,7 +64,7 @@ export default class CustomFetch extends DataFetch {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Enriches the custom data to ensure the structure and format is usable
|
// Enriches the custom data to ensure the structure and format is usable
|
||||||
enrichCustomData(data) {
|
enrichCustomData(data: (string | any)[]) {
|
||||||
if (!data?.length) {
|
if (!data?.length) {
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
|
@ -72,7 +81,7 @@ export default class CustomFetch extends DataFetch {
|
||||||
// Try parsing strings
|
// Try parsing strings
|
||||||
if (typeof value === "string") {
|
if (typeof value === "string") {
|
||||||
const split = value.split(",").map(x => x.trim())
|
const split = value.split(",").map(x => x.trim())
|
||||||
let obj = {}
|
const obj: Record<string, string> = {}
|
||||||
for (let i = 0; i < split.length; i++) {
|
for (let i = 0; i < split.length; i++) {
|
||||||
const suffix = i === 0 ? "" : ` ${i + 1}`
|
const suffix = i === 0 ? "" : ` ${i + 1}`
|
||||||
const key = `Value${suffix}`
|
const key = `Value${suffix}`
|
||||||
|
@ -87,27 +96,29 @@ export default class CustomFetch extends DataFetch {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Extracts and parses the custom data from the datasource definition
|
// Extracts and parses the custom data from the datasource definition
|
||||||
getCustomData(datasource) {
|
getCustomData(datasource: CustomDatasource) {
|
||||||
return this.enrichCustomData(this.parseCustomData(datasource?.data))
|
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
|
// Try and work out the schema from the array provided
|
||||||
let schema = {}
|
const schema: CustomDefinition = {}
|
||||||
const data = this.getCustomData(datasource)
|
const data = this.getCustomData(datasource)
|
||||||
if (!data?.length) {
|
if (!data?.length) {
|
||||||
return { schema }
|
return { schema }
|
||||||
}
|
}
|
||||||
|
|
||||||
// Go through every object and extract all valid keys
|
// Go through every object and extract all valid keys
|
||||||
for (let datum of data) {
|
for (const datum of data) {
|
||||||
for (let key of Object.keys(datum)) {
|
for (const key of Object.keys(datum)) {
|
||||||
if (key === "_id") {
|
if (key === "_id") {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if (!schema[key]) {
|
if (!schema[key]) {
|
||||||
let type = this.getType(datum[key])
|
let type = this.getType(datum[key])
|
||||||
let constraints = {}
|
const constraints: any = {}
|
||||||
|
|
||||||
// Determine whether we should render text columns as options instead
|
// Determine whether we should render text columns as options instead
|
||||||
if (type === "string") {
|
if (type === "string") {
|
|
@ -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 { cloneDeep } from "lodash/fp"
|
||||||
import { QueryUtils } from "../utils"
|
import { QueryUtils } from "../utils"
|
||||||
import { convertJSONSchemaToTableSchema } from "../utils/json"
|
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
|
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
|
* Parent class which handles the implementation of fetching data from an
|
||||||
* internal table or datasource plus.
|
* internal table or datasource plus.
|
||||||
* For other types of datasource, this class is overridden and extended.
|
* 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.
|
* Constructs a new DataFetch instance.
|
||||||
* @param opts the fetch options
|
* @param opts the fetch options
|
||||||
*/
|
*/
|
||||||
constructor(opts) {
|
constructor(opts: DataFetchParams<TDatasource, TQuery>) {
|
||||||
// API client
|
|
||||||
this.API = null
|
|
||||||
|
|
||||||
// Feature flags
|
// Feature flags
|
||||||
this.features = {
|
this.features = {
|
||||||
supportsSearch: false,
|
supportsSearch: false,
|
||||||
|
@ -29,12 +106,12 @@ export default class DataFetch {
|
||||||
|
|
||||||
// Config
|
// Config
|
||||||
this.options = {
|
this.options = {
|
||||||
datasource: null,
|
datasource: opts.datasource,
|
||||||
limit: 10,
|
limit: 10,
|
||||||
|
|
||||||
// Search config
|
// Search config
|
||||||
filter: null,
|
filter: null,
|
||||||
query: null,
|
query: opts.query,
|
||||||
|
|
||||||
// Sorting config
|
// Sorting config
|
||||||
sortColumn: null,
|
sortColumn: null,
|
||||||
|
@ -57,11 +134,11 @@ export default class DataFetch {
|
||||||
schema: null,
|
schema: null,
|
||||||
loading: false,
|
loading: false,
|
||||||
loaded: false,
|
loaded: false,
|
||||||
query: null,
|
query: opts.query,
|
||||||
pageNumber: 0,
|
pageNumber: 0,
|
||||||
cursor: null,
|
cursor: null,
|
||||||
cursors: [],
|
cursors: [],
|
||||||
resetKey: Math.random(),
|
resetKey: Math.random().toString(),
|
||||||
error: null,
|
error: null,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -118,7 +195,10 @@ export default class DataFetch {
|
||||||
/**
|
/**
|
||||||
* Gets the default sort column for this datasource
|
* 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]) {
|
if (definition?.primaryDisplay && schema[definition.primaryDisplay]) {
|
||||||
return definition.primaryDisplay
|
return definition.primaryDisplay
|
||||||
} else {
|
} else {
|
||||||
|
@ -130,13 +210,13 @@ export default class DataFetch {
|
||||||
* Fetches a fresh set of data from the server, resetting pagination
|
* Fetches a fresh set of data from the server, resetting pagination
|
||||||
*/
|
*/
|
||||||
async getInitialData() {
|
async getInitialData() {
|
||||||
const { datasource, filter, paginate } = this.options
|
const { filter, paginate } = this.options
|
||||||
|
|
||||||
// Fetch datasource definition and extract sort properties if configured
|
// Fetch datasource definition and extract sort properties if configured
|
||||||
const definition = await this.getDefinition(datasource)
|
const definition = await this.getDefinition()
|
||||||
|
|
||||||
// Determine feature flags
|
// Determine feature flags
|
||||||
const features = this.determineFeatureFlags(definition)
|
const features = await this.determineFeatureFlags()
|
||||||
this.features = {
|
this.features = {
|
||||||
supportsSearch: !!features?.supportsSearch,
|
supportsSearch: !!features?.supportsSearch,
|
||||||
supportsSort: !!features?.supportsSort,
|
supportsSort: !!features?.supportsSort,
|
||||||
|
@ -144,11 +224,11 @@ export default class DataFetch {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fetch and enrich schema
|
// Fetch and enrich schema
|
||||||
let schema = this.getSchema(datasource, definition)
|
let schema = this.getSchema(definition)
|
||||||
schema = this.enrichSchema(schema)
|
|
||||||
if (!schema) {
|
if (!schema) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
schema = this.enrichSchema(schema)
|
||||||
|
|
||||||
// If an invalid sort column is specified, delete it
|
// If an invalid sort column is specified, delete it
|
||||||
if (this.options.sortColumn && !schema[this.options.sortColumn]) {
|
if (this.options.sortColumn && !schema[this.options.sortColumn]) {
|
||||||
|
@ -172,20 +252,25 @@ export default class DataFetch {
|
||||||
if (
|
if (
|
||||||
fieldSchema?.type === FieldType.NUMBER ||
|
fieldSchema?.type === FieldType.NUMBER ||
|
||||||
fieldSchema?.type === FieldType.BIGINT ||
|
fieldSchema?.type === FieldType.BIGINT ||
|
||||||
fieldSchema?.calculationType
|
("calculationType" in fieldSchema && fieldSchema?.calculationType)
|
||||||
) {
|
) {
|
||||||
this.options.sortType = SortType.NUMBER
|
this.options.sortType = SortType.NUMBER
|
||||||
}
|
}
|
||||||
|
|
||||||
// If no sort order, default to ascending
|
// If no sort order, default to ascending
|
||||||
if (!this.options.sortOrder) {
|
if (!this.options.sortOrder) {
|
||||||
this.options.sortOrder = SortOrder.ASCENDING
|
this.options.sortOrder = SortOrder.ASCENDING
|
||||||
|
} else {
|
||||||
|
// Ensure sortOrder matches the enum
|
||||||
|
this.options.sortOrder =
|
||||||
|
this.options.sortOrder.toLowerCase() as SortOrder
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build the query
|
// Build the query
|
||||||
let query = this.options.query
|
let query = this.options.query
|
||||||
if (!query) {
|
if (!query) {
|
||||||
query = buildQuery(filter)
|
query = buildQuery(filter ?? undefined) as TQuery
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update store
|
// Update store
|
||||||
|
@ -210,7 +295,7 @@ export default class DataFetch {
|
||||||
info: page.info,
|
info: page.info,
|
||||||
cursors: paginate && page.hasNextPage ? [null, page.cursor] : [null],
|
cursors: paginate && page.hasNextPage ? [null, page.cursor] : [null],
|
||||||
error: page.error,
|
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 we don't support sorting, do a client-side sort
|
||||||
if (!this.features.supportsSort && clientSideSorting) {
|
if (!this.features.supportsSort && clientSideSorting && sortType) {
|
||||||
rows = sort(rows, sortColumn, sortOrder, sortType)
|
rows = sort(rows, sortColumn as any, sortOrder, sortType)
|
||||||
}
|
}
|
||||||
|
|
||||||
// If we don't support pagination, do a client-side limit
|
// If we don't support pagination, do a client-side limit
|
||||||
|
@ -256,49 +341,28 @@ export default class DataFetch {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
abstract getData(): Promise<{
|
||||||
* Fetches a single page of data from the remote resource.
|
rows: Row[]
|
||||||
* Must be overridden by a datasource specific child class.
|
info?: any
|
||||||
*/
|
hasNextPage?: boolean
|
||||||
async getData() {
|
cursor?: any
|
||||||
return {
|
error?: any
|
||||||
rows: [],
|
}>
|
||||||
info: null,
|
|
||||||
hasNextPage: false,
|
|
||||||
cursor: null,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets the definition for this datasource.
|
* Gets the definition for this datasource.
|
||||||
* Defaults to fetching a table definition.
|
|
||||||
* @param datasource
|
|
||||||
* @return {object} the definition
|
* @return {object} the definition
|
||||||
*/
|
*/
|
||||||
async getDefinition(datasource) {
|
abstract getDefinition(): Promise<TDefinition | null>
|
||||||
if (!datasource?.tableId) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
return await this.API.fetchTableDefinition(datasource.tableId)
|
|
||||||
} catch (error) {
|
|
||||||
this.store.update(state => ({
|
|
||||||
...state,
|
|
||||||
error,
|
|
||||||
}))
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets the schema definition for a datasource.
|
* 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
|
* @param definition the datasource definition
|
||||||
* @return {object} the schema
|
* @return {object} the schema
|
||||||
*/
|
*/
|
||||||
getSchema(datasource, definition) {
|
getSchema(definition: TDefinition | null): Record<string, any> | undefined {
|
||||||
return definition?.schema
|
return definition?.schema ?? undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -307,53 +371,56 @@ export default class DataFetch {
|
||||||
* @param schema the datasource schema
|
* @param schema the datasource schema
|
||||||
* @return {object} the enriched datasource schema
|
* @return {object} the enriched datasource schema
|
||||||
*/
|
*/
|
||||||
enrichSchema(schema) {
|
private enrichSchema(schema: TableSchema): TableSchema {
|
||||||
if (schema == null) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check for any JSON fields so we can add any top level properties
|
// Check for any JSON fields so we can add any top level properties
|
||||||
let jsonAdditions = {}
|
let jsonAdditions: Record<string, { type: string; nestedJSON: true }> = {}
|
||||||
Object.keys(schema).forEach(fieldKey => {
|
for (const fieldKey of Object.keys(schema)) {
|
||||||
const fieldSchema = schema[fieldKey]
|
const fieldSchema = schema[fieldKey]
|
||||||
if (fieldSchema?.type === FieldType.JSON) {
|
if (fieldSchema.type === FieldType.JSON) {
|
||||||
const jsonSchema = convertJSONSchemaToTableSchema(fieldSchema, {
|
const jsonSchema = convertJSONSchemaToTableSchema(fieldSchema, {
|
||||||
squashObjects: true,
|
squashObjects: true,
|
||||||
})
|
}) as Record<string, { type: string }> | null // TODO: remove when convertJSONSchemaToTableSchema is typed
|
||||||
Object.keys(jsonSchema).forEach(jsonKey => {
|
if (jsonSchema) {
|
||||||
jsonAdditions[`${fieldKey}.${jsonKey}`] = {
|
for (const jsonKey of Object.keys(jsonSchema)) {
|
||||||
type: jsonSchema[jsonKey].type,
|
jsonAdditions[`${fieldKey}.${jsonKey}`] = {
|
||||||
nestedJSON: true,
|
type: jsonSchema[jsonKey].type,
|
||||||
|
nestedJSON: true,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
}
|
||||||
}
|
}
|
||||||
})
|
}
|
||||||
schema = { ...schema, ...jsonAdditions }
|
|
||||||
|
|
||||||
// Ensure schema is in the correct structure
|
// Ensure schema is in the correct structure
|
||||||
let enrichedSchema = {}
|
let enrichedSchema: TableSchema = {}
|
||||||
Object.entries(schema).forEach(([fieldName, fieldSchema]) => {
|
Object.entries({ ...schema, ...jsonAdditions }).forEach(
|
||||||
if (typeof fieldSchema === "string") {
|
([fieldName, fieldSchema]) => {
|
||||||
enrichedSchema[fieldName] = {
|
if (typeof fieldSchema === "string") {
|
||||||
type: fieldSchema,
|
enrichedSchema[fieldName] = {
|
||||||
name: fieldName,
|
type: fieldSchema,
|
||||||
}
|
name: fieldName,
|
||||||
} else {
|
}
|
||||||
enrichedSchema[fieldName] = {
|
} else {
|
||||||
...fieldSchema,
|
enrichedSchema[fieldName] = {
|
||||||
name: fieldName,
|
...fieldSchema,
|
||||||
|
type: fieldSchema.type as any, // TODO: check type union definition conflicts
|
||||||
|
name: fieldName,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
)
|
||||||
|
|
||||||
return enrichedSchema
|
return enrichedSchema
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Determine the feature flag for this datasource definition
|
* Determine the feature flag for this datasource
|
||||||
* @param definition
|
|
||||||
*/
|
*/
|
||||||
determineFeatureFlags(_definition) {
|
async determineFeatureFlags(): Promise<{
|
||||||
|
supportsPagination: boolean
|
||||||
|
supportsSearch?: boolean
|
||||||
|
supportsSort?: boolean
|
||||||
|
}> {
|
||||||
return {
|
return {
|
||||||
supportsSearch: false,
|
supportsSearch: false,
|
||||||
supportsSort: false,
|
supportsSort: false,
|
||||||
|
@ -365,12 +432,11 @@ export default class DataFetch {
|
||||||
* Resets the data set and updates options
|
* Resets the data set and updates options
|
||||||
* @param newOptions any new options
|
* @param newOptions any new options
|
||||||
*/
|
*/
|
||||||
async update(newOptions) {
|
async update(newOptions: any) {
|
||||||
// Check if any settings have actually changed
|
// Check if any settings have actually changed
|
||||||
let refresh = false
|
let refresh = false
|
||||||
const entries = Object.entries(newOptions || {})
|
for (const [key, value] of Object.entries(newOptions || {})) {
|
||||||
for (let [key, value] of entries) {
|
const oldVal = this.options[key as keyof typeof this.options] ?? null
|
||||||
const oldVal = this.options[key] == null ? null : this.options[key]
|
|
||||||
const newVal = value == null ? null : value
|
const newVal = value == null ? null : value
|
||||||
if (JSON.stringify(newVal) !== JSON.stringify(oldVal)) {
|
if (JSON.stringify(newVal) !== JSON.stringify(oldVal)) {
|
||||||
refresh = true
|
refresh = true
|
||||||
|
@ -437,7 +503,7 @@ export default class DataFetch {
|
||||||
* @param state the current store state
|
* @param state the current store state
|
||||||
* @return {boolean} whether there is a next page of data or not
|
* @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
|
return state.cursors[state.pageNumber + 1] != null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -447,7 +513,7 @@ export default class DataFetch {
|
||||||
* @param state the current store state
|
* @param state the current store state
|
||||||
* @return {boolean} whether there is a previous page of data or not
|
* @return {boolean} whether there is a previous page of data or not
|
||||||
*/
|
*/
|
||||||
hasPrevPage(state) {
|
private hasPrevPage(state: { pageNumber: number }): boolean {
|
||||||
return state.pageNumber > 0
|
return state.pageNumber > 0
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
// Field sources have their schema statically defined
|
||||||
let schema
|
let schema
|
||||||
if (datasource.fieldType === "attachment") {
|
if (datasource.fieldType === "attachment") {
|
||||||
|
@ -28,8 +48,8 @@ export default class FieldFetch extends DataFetch {
|
||||||
|
|
||||||
// These sources will be available directly from context
|
// These sources will be available directly from context
|
||||||
const data = datasource?.value || []
|
const data = datasource?.value || []
|
||||||
let rows
|
let rows: Row[]
|
||||||
if (Array.isArray(data) && data[0] && typeof data[0] !== "object") {
|
if (isArrayOfStrings(data)) {
|
||||||
rows = data.map(value => ({ value }))
|
rows = data.map(value => ({ value }))
|
||||||
} else {
|
} else {
|
||||||
rows = data
|
rows = data
|
|
@ -1,9 +1,22 @@
|
||||||
import { get } from "svelte/store"
|
import { get } from "svelte/store"
|
||||||
import DataFetch from "./DataFetch.js"
|
import DataFetch, { DataFetchParams } from "./DataFetch"
|
||||||
import { TableNames } from "../constants"
|
import { TableNames } from "../constants"
|
||||||
|
|
||||||
export default class GroupUserFetch extends DataFetch {
|
interface GroupUserQuery {
|
||||||
constructor(opts) {
|
groupId: string
|
||||||
|
emailSearch: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface GroupUserDatasource {
|
||||||
|
tableId: TableNames.USERS
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class GroupUserFetch extends DataFetch<
|
||||||
|
GroupUserDatasource,
|
||||||
|
{},
|
||||||
|
GroupUserQuery
|
||||||
|
> {
|
||||||
|
constructor(opts: DataFetchParams<GroupUserDatasource, GroupUserQuery>) {
|
||||||
super({
|
super({
|
||||||
...opts,
|
...opts,
|
||||||
datasource: {
|
datasource: {
|
||||||
|
@ -12,7 +25,7 @@ export default class GroupUserFetch extends DataFetch {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
determineFeatureFlags() {
|
async determineFeatureFlags() {
|
||||||
return {
|
return {
|
||||||
supportsSearch: true,
|
supportsSearch: true,
|
||||||
supportsSort: false,
|
supportsSort: false,
|
||||||
|
@ -28,11 +41,12 @@ export default class GroupUserFetch extends DataFetch {
|
||||||
|
|
||||||
async getData() {
|
async getData() {
|
||||||
const { query, cursor } = get(this.store)
|
const { query, cursor } = get(this.store)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const res = await this.API.getGroupUsers({
|
const res = await this.API.getGroupUsers({
|
||||||
id: query.groupId,
|
id: query.groupId,
|
||||||
emailSearch: query.emailSearch,
|
emailSearch: query.emailSearch,
|
||||||
bookmark: cursor,
|
bookmark: cursor ?? undefined,
|
||||||
})
|
})
|
||||||
|
|
||||||
return {
|
return {
|
|
@ -1,8 +1,10 @@
|
||||||
import FieldFetch from "./FieldFetch.js"
|
import FieldFetch from "./FieldFetch"
|
||||||
import { getJSONArrayDatasourceSchema } from "../utils/json"
|
import { getJSONArrayDatasourceSchema } from "../utils/json"
|
||||||
|
|
||||||
export default class JSONArrayFetch extends FieldFetch {
|
export default class JSONArrayFetch extends FieldFetch {
|
||||||
async getDefinition(datasource) {
|
async getDefinition() {
|
||||||
|
const { datasource } = this.options
|
||||||
|
|
||||||
// JSON arrays need their table definitions fetched.
|
// JSON arrays need their table definitions fetched.
|
||||||
// We can then extract their schema as a subset of the table schema.
|
// We can then extract their schema as a subset of the table schema.
|
||||||
try {
|
try {
|
|
@ -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,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,11 +1,13 @@
|
||||||
import FieldFetch from "./FieldFetch.js"
|
import FieldFetch from "./FieldFetch"
|
||||||
import {
|
import {
|
||||||
getJSONArrayDatasourceSchema,
|
getJSONArrayDatasourceSchema,
|
||||||
generateQueryArraySchemas,
|
generateQueryArraySchemas,
|
||||||
} from "../utils/json"
|
} from "../utils/json"
|
||||||
|
|
||||||
export default class QueryArrayFetch extends FieldFetch {
|
export default class QueryArrayFetch extends FieldFetch {
|
||||||
async getDefinition(datasource) {
|
async getDefinition() {
|
||||||
|
const { datasource } = this.options
|
||||||
|
|
||||||
if (!datasource?.tableId) {
|
if (!datasource?.tableId) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
@ -14,10 +16,14 @@ export default class QueryArrayFetch extends FieldFetch {
|
||||||
try {
|
try {
|
||||||
const table = await this.API.fetchQueryDefinition(datasource.tableId)
|
const table = await this.API.fetchQueryDefinition(datasource.tableId)
|
||||||
const schema = generateQueryArraySchemas(
|
const schema = generateQueryArraySchemas(
|
||||||
table?.schema,
|
table.schema,
|
||||||
table?.nestedSchemaFields
|
table.nestedSchemaFields
|
||||||
)
|
)
|
||||||
return { schema: getJSONArrayDatasourceSchema(schema, datasource) }
|
const result = {
|
||||||
|
schema: getJSONArrayDatasourceSchema(schema, datasource),
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
|
@ -1,9 +1,24 @@
|
||||||
import DataFetch from "./DataFetch.js"
|
import DataFetch from "./DataFetch"
|
||||||
import { Helpers } from "@budibase/bbui"
|
import { Helpers } from "@budibase/bbui"
|
||||||
|
import { ExecuteQueryRequest, Query } from "@budibase/types"
|
||||||
import { get } from "svelte/store"
|
import { get } from "svelte/store"
|
||||||
|
|
||||||
export default class QueryFetch extends DataFetch {
|
interface QueryDatasource {
|
||||||
determineFeatureFlags(definition) {
|
_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 =
|
const supportsPagination =
|
||||||
!!definition?.fields?.pagination?.type &&
|
!!definition?.fields?.pagination?.type &&
|
||||||
!!definition?.fields?.pagination?.location &&
|
!!definition?.fields?.pagination?.location &&
|
||||||
|
@ -11,7 +26,9 @@ export default class QueryFetch extends DataFetch {
|
||||||
return { supportsPagination }
|
return { supportsPagination }
|
||||||
}
|
}
|
||||||
|
|
||||||
async getDefinition(datasource) {
|
async getDefinition() {
|
||||||
|
const { datasource } = this.options
|
||||||
|
|
||||||
if (!datasource?._id) {
|
if (!datasource?._id) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
@ -40,17 +57,17 @@ export default class QueryFetch extends DataFetch {
|
||||||
const type = definition?.fields?.pagination?.type
|
const type = definition?.fields?.pagination?.type
|
||||||
|
|
||||||
// Set the default query params
|
// Set the default query params
|
||||||
let parameters = Helpers.cloneDeep(datasource?.queryParams || {})
|
const parameters = Helpers.cloneDeep(datasource.queryParams || {})
|
||||||
for (let param of datasource?.parameters || {}) {
|
for (const param of datasource?.parameters || []) {
|
||||||
if (!parameters[param.name]) {
|
if (!parameters[param.name]) {
|
||||||
parameters[param.name] = param.default
|
parameters[param.name] = param.default
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add pagination to query if supported
|
// Add pagination to query if supported
|
||||||
let queryPayload = { parameters }
|
const queryPayload: ExecuteQueryRequest = { parameters }
|
||||||
if (paginate && supportsPagination) {
|
if (paginate && supportsPagination) {
|
||||||
const requestCursor = type === "page" ? parseInt(cursor || 1) : cursor
|
const requestCursor = type === "page" ? parseInt(cursor || "1") : cursor
|
||||||
queryPayload.pagination = { page: requestCursor, limit }
|
queryPayload.pagination = { page: requestCursor, limit }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -65,7 +82,7 @@ export default class QueryFetch extends DataFetch {
|
||||||
if (paginate && supportsPagination) {
|
if (paginate && supportsPagination) {
|
||||||
if (type === "page") {
|
if (type === "page") {
|
||||||
// For "page number" pagination, increment the existing page number
|
// 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
|
hasNextPage = data?.length === limit && limit > 0
|
||||||
} else {
|
} else {
|
||||||
// For "cursor" pagination, the cursor should be in the response
|
// For "cursor" pagination, the cursor should be in the response
|
|
@ -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: [] }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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: [] }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,9 +1,9 @@
|
||||||
import { get } from "svelte/store"
|
import { get } from "svelte/store"
|
||||||
import DataFetch from "./DataFetch.js"
|
import DataFetch from "./DataFetch"
|
||||||
import { SortOrder } from "@budibase/types"
|
import { SortOrder, Table, UITable } from "@budibase/types"
|
||||||
|
|
||||||
export default class TableFetch extends DataFetch {
|
export default class TableFetch extends DataFetch<UITable, Table> {
|
||||||
determineFeatureFlags() {
|
async determineFeatureFlags() {
|
||||||
return {
|
return {
|
||||||
supportsSearch: true,
|
supportsSearch: true,
|
||||||
supportsSort: 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() {
|
async getData() {
|
||||||
const { datasource, limit, sortColumn, sortOrder, sortType, paginate } =
|
const { datasource, limit, sortColumn, sortOrder, sortType, paginate } =
|
||||||
this.options
|
this.options
|
||||||
|
@ -23,7 +40,7 @@ export default class TableFetch extends DataFetch {
|
||||||
query,
|
query,
|
||||||
limit,
|
limit,
|
||||||
sort: sortColumn,
|
sort: sortColumn,
|
||||||
sortOrder: sortOrder?.toLowerCase() ?? SortOrder.ASCENDING,
|
sortOrder: sortOrder ?? SortOrder.ASCENDING,
|
||||||
sortType,
|
sortType,
|
||||||
paginate,
|
paginate,
|
||||||
bookmark: cursor,
|
bookmark: cursor,
|
|
@ -1,10 +1,28 @@
|
||||||
import { get } from "svelte/store"
|
import { get } from "svelte/store"
|
||||||
import DataFetch from "./DataFetch.js"
|
import DataFetch, { DataFetchParams } from "./DataFetch"
|
||||||
import { TableNames } from "../constants"
|
import { TableNames } from "../constants"
|
||||||
import { utils } from "@budibase/shared-core"
|
import { utils } from "@budibase/shared-core"
|
||||||
|
import {
|
||||||
|
BasicOperator,
|
||||||
|
SearchFilters,
|
||||||
|
SearchUsersRequest,
|
||||||
|
} from "@budibase/types"
|
||||||
|
|
||||||
export default class UserFetch extends DataFetch {
|
interface UserFetchQuery {
|
||||||
constructor(opts) {
|
appId: string
|
||||||
|
paginated: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
interface UserDatasource {
|
||||||
|
tableId: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class UserFetch extends DataFetch<
|
||||||
|
UserDatasource,
|
||||||
|
{},
|
||||||
|
UserFetchQuery
|
||||||
|
> {
|
||||||
|
constructor(opts: DataFetchParams<UserDatasource, UserFetchQuery>) {
|
||||||
super({
|
super({
|
||||||
...opts,
|
...opts,
|
||||||
datasource: {
|
datasource: {
|
||||||
|
@ -13,7 +31,7 @@ export default class UserFetch extends DataFetch {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
determineFeatureFlags() {
|
async determineFeatureFlags() {
|
||||||
return {
|
return {
|
||||||
supportsSearch: true,
|
supportsSearch: true,
|
||||||
supportsSort: false,
|
supportsSort: false,
|
||||||
|
@ -22,9 +40,7 @@ export default class UserFetch extends DataFetch {
|
||||||
}
|
}
|
||||||
|
|
||||||
async getDefinition() {
|
async getDefinition() {
|
||||||
return {
|
return { schema: {} }
|
||||||
schema: {},
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async getData() {
|
async getData() {
|
||||||
|
@ -32,15 +48,16 @@ export default class UserFetch extends DataFetch {
|
||||||
const { cursor, query } = get(this.store)
|
const { cursor, query } = get(this.store)
|
||||||
|
|
||||||
// Convert old format to new one - we now allow use of the lucene format
|
// Convert old format to new one - we now allow use of the lucene format
|
||||||
const { appId, paginated, ...rest } = query || {}
|
const { appId, paginated, ...rest } = query
|
||||||
const finalQuery = utils.isSupportedUserSearch(rest)
|
|
||||||
? query
|
const finalQuery: SearchFilters = utils.isSupportedUserSearch(rest)
|
||||||
: { string: { email: null } }
|
? rest
|
||||||
|
: { [BasicOperator.EMPTY]: { email: null } }
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const opts = {
|
const opts: SearchUsersRequest = {
|
||||||
bookmark: cursor,
|
bookmark: cursor ?? undefined,
|
||||||
query: finalQuery,
|
query: finalQuery ?? undefined,
|
||||||
appId: appId,
|
appId: appId,
|
||||||
paginate: paginated || paginate,
|
paginate: paginated || paginate,
|
||||||
limit,
|
limit,
|
|
@ -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: [] }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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: [] }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,9 +1,10 @@
|
||||||
import { ViewV2Type } from "@budibase/types"
|
import { SortOrder, UIView, ViewV2, ViewV2Type } from "@budibase/types"
|
||||||
import DataFetch from "./DataFetch.js"
|
import DataFetch from "./DataFetch"
|
||||||
import { get } from "svelte/store"
|
import { get } from "svelte/store"
|
||||||
|
import { helpers } from "@budibase/shared-core"
|
||||||
|
|
||||||
export default class ViewV2Fetch extends DataFetch {
|
export default class ViewV2Fetch extends DataFetch<UIView, ViewV2> {
|
||||||
determineFeatureFlags() {
|
async determineFeatureFlags() {
|
||||||
return {
|
return {
|
||||||
supportsSearch: true,
|
supportsSearch: true,
|
||||||
supportsSort: true,
|
supportsSort: true,
|
||||||
|
@ -11,18 +12,13 @@ export default class ViewV2Fetch extends DataFetch {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
getSchema(datasource, definition) {
|
async getDefinition() {
|
||||||
return definition?.schema
|
const { datasource } = this.options
|
||||||
}
|
|
||||||
|
|
||||||
async getDefinition(datasource) {
|
|
||||||
if (!datasource?.id) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
try {
|
try {
|
||||||
const res = await this.API.viewV2.fetchDefinition(datasource.id)
|
const res = await this.API.viewV2.fetchDefinition(datasource.id)
|
||||||
return res?.data
|
return res?.data
|
||||||
} catch (error) {
|
} catch (error: any) {
|
||||||
this.store.update(state => ({
|
this.store.update(state => ({
|
||||||
...state,
|
...state,
|
||||||
error,
|
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 this is a calculation view and we have no calculations, return nothing
|
||||||
if (
|
if (
|
||||||
definition.type === ViewV2Type.CALCULATION &&
|
definition?.type === ViewV2Type.CALCULATION &&
|
||||||
!Object.values(definition.schema || {}).some(x => x.calculationType)
|
!Object.values(definition.schema || {}).some(
|
||||||
|
helpers.views.isCalculationField
|
||||||
|
)
|
||||||
) {
|
) {
|
||||||
return {
|
return {
|
||||||
rows: [],
|
rows: [],
|
||||||
|
@ -56,25 +54,41 @@ export default class ViewV2Fetch extends DataFetch {
|
||||||
// If sort/filter params are not defined, update options to store the
|
// If sort/filter params are not defined, update options to store the
|
||||||
// params built in to this view. This ensures that we can accurately
|
// params built in to this view. This ensures that we can accurately
|
||||||
// compare old and new params and skip a redundant API call.
|
// 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.sortColumn = definition.sort.field
|
||||||
this.options.sortOrder = definition.sort.order
|
this.options.sortOrder = definition.sort.order || SortOrder.ASCENDING
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const res = await this.API.viewV2.fetch(datasource.id, {
|
const request = {
|
||||||
...(query ? { query } : {}),
|
query,
|
||||||
paginate,
|
paginate,
|
||||||
limit,
|
limit,
|
||||||
bookmark: cursor,
|
bookmark: cursor,
|
||||||
sort: sortColumn,
|
sort: sortColumn,
|
||||||
sortOrder: sortOrder?.toLowerCase(),
|
sortOrder: sortOrder,
|
||||||
sortType,
|
sortType,
|
||||||
})
|
}
|
||||||
return {
|
if (paginate) {
|
||||||
rows: res?.rows || [],
|
const res = await this.API.viewV2.fetch(datasource.id, {
|
||||||
hasNextPage: res?.hasNextPage || false,
|
...request,
|
||||||
cursor: res?.bookmark || null,
|
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) {
|
} catch (error) {
|
||||||
return {
|
return {
|
|
@ -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)
|
|
||||||
}
|
|
|
@ -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)
|
||||||
|
}
|
|
@ -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 }>
|
||||||
|
}
|
|
@ -355,7 +355,7 @@ async function execute(
|
||||||
ExecuteQueryRequest,
|
ExecuteQueryRequest,
|
||||||
ExecuteV2QueryResponse | ExecuteV1QueryResponse
|
ExecuteV2QueryResponse | ExecuteV1QueryResponse
|
||||||
>,
|
>,
|
||||||
opts: any = { rowsOnly: false, isAutomation: false }
|
opts = { rowsOnly: false, isAutomation: false }
|
||||||
) {
|
) {
|
||||||
const db = context.getAppDB()
|
const db = context.getAppDB()
|
||||||
|
|
||||||
|
@ -416,7 +416,7 @@ export async function executeV1(
|
||||||
export async function executeV2(
|
export async function executeV2(
|
||||||
ctx: UserCtx<ExecuteQueryRequest, ExecuteV2QueryResponse>
|
ctx: UserCtx<ExecuteQueryRequest, ExecuteV2QueryResponse>
|
||||||
) {
|
) {
|
||||||
return execute(ctx, { rowsOnly: false })
|
return execute(ctx, { rowsOnly: false, isAutomation: false })
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function executeV2AsAutomation(
|
export async function executeV2AsAutomation(
|
||||||
|
|
|
@ -1,16 +1,16 @@
|
||||||
import {
|
import {
|
||||||
UserCtx,
|
UserCtx,
|
||||||
ViewV2,
|
ViewV2,
|
||||||
SearchRowResponse,
|
|
||||||
SearchViewRowRequest,
|
SearchViewRowRequest,
|
||||||
RequiredKeys,
|
RequiredKeys,
|
||||||
RowSearchParams,
|
RowSearchParams,
|
||||||
|
PaginatedSearchRowResponse,
|
||||||
} from "@budibase/types"
|
} from "@budibase/types"
|
||||||
import sdk from "../../../sdk"
|
import sdk from "../../../sdk"
|
||||||
import { context } from "@budibase/backend-core"
|
import { context } from "@budibase/backend-core"
|
||||||
|
|
||||||
export async function searchView(
|
export async function searchView(
|
||||||
ctx: UserCtx<SearchViewRowRequest, SearchRowResponse>
|
ctx: UserCtx<SearchViewRowRequest, PaginatedSearchRowResponse>
|
||||||
) {
|
) {
|
||||||
const { viewId } = ctx.params
|
const { viewId } = ctx.params
|
||||||
|
|
||||||
|
@ -49,7 +49,13 @@ export async function searchView(
|
||||||
user: sdk.users.getUserContextBindings(ctx.user),
|
user: sdk.users.getUserContextBindings(ctx.user),
|
||||||
})
|
})
|
||||||
result.rows.forEach(r => (r._viewId = view.id))
|
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) {
|
function getSortOptions(request: SearchViewRowRequest, view: ViewV2) {
|
||||||
|
|
|
@ -3,7 +3,10 @@ import { Datasource, Row, Query } from "@budibase/types"
|
||||||
export type WorkerCallback = (error: any, response?: any) => void
|
export type WorkerCallback = (error: any, response?: any) => void
|
||||||
|
|
||||||
export interface QueryEvent
|
export interface QueryEvent
|
||||||
extends Omit<Query, "datasourceId" | "name" | "parameters" | "readable"> {
|
extends Omit<
|
||||||
|
Query,
|
||||||
|
"datasourceId" | "name" | "parameters" | "readable" | "nestedSchemaFields"
|
||||||
|
> {
|
||||||
appId?: string
|
appId?: string
|
||||||
datasource: Datasource
|
datasource: Datasource
|
||||||
pagination?: any
|
pagination?: any
|
||||||
|
|
|
@ -911,8 +911,8 @@ export function sort<T extends Record<string, any>>(
|
||||||
* @param docs the data
|
* @param docs the data
|
||||||
* @param limit the number of docs to limit to
|
* @param limit the number of docs to limit to
|
||||||
*/
|
*/
|
||||||
export function limit<T>(docs: T[], limit: string): T[] {
|
export function limit<T>(docs: T[], limit: string | number): T[] {
|
||||||
const numLimit = parseFloat(limit)
|
const numLimit = typeof limit === "number" ? limit : parseFloat(limit)
|
||||||
if (isNaN(numLimit)) {
|
if (isNaN(numLimit)) {
|
||||||
return docs
|
return docs
|
||||||
}
|
}
|
||||||
|
|
|
@ -109,7 +109,9 @@ export function trimOtherProps(object: any, allowedProps: string[]) {
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isSupportedUserSearch(query: SearchFilters) {
|
export function isSupportedUserSearch(
|
||||||
|
query: SearchFilters
|
||||||
|
): query is SearchFilters {
|
||||||
const allowed = [
|
const allowed = [
|
||||||
{ op: BasicOperator.STRING, key: "email" },
|
{ op: BasicOperator.STRING, key: "email" },
|
||||||
{ op: BasicOperator.EQUAL, key: "_id" },
|
{ op: BasicOperator.EQUAL, key: "_id" },
|
||||||
|
|
|
@ -40,6 +40,10 @@ export interface ExecuteQueryRequest {
|
||||||
export type ExecuteV1QueryResponse = Record<string, any>[]
|
export type ExecuteV1QueryResponse = Record<string, any>[]
|
||||||
export interface ExecuteV2QueryResponse {
|
export interface ExecuteV2QueryResponse {
|
||||||
data: Record<string, any>[]
|
data: Record<string, any>[]
|
||||||
|
pagination?: {
|
||||||
|
page: number
|
||||||
|
cursor: string
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DeleteQueryResponse {
|
export interface DeleteQueryResponse {
|
||||||
|
|
|
@ -24,4 +24,5 @@ export interface PaginationRequest extends BasicPaginationRequest {
|
||||||
export interface PaginationResponse {
|
export interface PaginationResponse {
|
||||||
bookmark: string | number | undefined
|
bookmark: string | number | undefined
|
||||||
hasNextPage?: boolean
|
hasNextPage?: boolean
|
||||||
|
totalRows?: number
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import { Document } from "../document"
|
import { Document } from "../document"
|
||||||
|
import { Row } from "./row"
|
||||||
|
|
||||||
export interface QuerySchema {
|
export interface QuerySchema {
|
||||||
name?: string
|
name?: string
|
||||||
|
@ -13,6 +14,7 @@ export interface Query extends Document {
|
||||||
fields: RestQueryFields | any
|
fields: RestQueryFields | any
|
||||||
transformer: string | null
|
transformer: string | null
|
||||||
schema: Record<string, QuerySchema | string>
|
schema: Record<string, QuerySchema | string>
|
||||||
|
nestedSchemaFields?: Record<string, Record<string, QuerySchema | string>>
|
||||||
readable: boolean
|
readable: boolean
|
||||||
queryVerb: string
|
queryVerb: string
|
||||||
// flag to state whether the default bindings are empty strings (old behaviour) or null
|
// flag to state whether the default bindings are empty strings (old behaviour) or null
|
||||||
|
@ -29,7 +31,7 @@ export interface QueryParameter {
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface QueryResponse {
|
export interface QueryResponse {
|
||||||
rows: any[]
|
rows: Row[]
|
||||||
keys: string[]
|
keys: string[]
|
||||||
info: any
|
info: any
|
||||||
extra: any
|
extra: any
|
||||||
|
|
|
@ -227,6 +227,7 @@ interface OtherFieldMetadata extends BaseFieldSchema {
|
||||||
| FieldType.OPTIONS
|
| FieldType.OPTIONS
|
||||||
| FieldType.BOOLEAN
|
| FieldType.BOOLEAN
|
||||||
| FieldType.BIGINT
|
| FieldType.BIGINT
|
||||||
|
| FieldType.JSON
|
||||||
>
|
>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,8 +1,6 @@
|
||||||
import { UITable, UIView } from "@budibase/types"
|
import { UITable, UIView } from "@budibase/types"
|
||||||
|
|
||||||
export type UIDatasource = (UITable | UIView) & {
|
export type UIDatasource = UITable | UIView
|
||||||
type: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface UIFieldMutation {
|
export interface UIFieldMutation {
|
||||||
visible?: boolean
|
visible?: boolean
|
||||||
|
|
|
@ -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
|
|
||||||
}
|
|
|
@ -6,4 +6,3 @@ export * from "./view"
|
||||||
export * from "./user"
|
export * from "./user"
|
||||||
export * from "./filters"
|
export * from "./filters"
|
||||||
export * from "./rows"
|
export * from "./rows"
|
||||||
export * from "./fetch"
|
|
||||||
|
|
Loading…
Reference in New Issue