2024-12-31 15:43:08 +01:00
|
|
|
import { writable, derived, get, Writable, Readable } from "svelte/store"
|
2022-01-26 18:44:14 +01:00
|
|
|
import { cloneDeep } from "lodash/fp"
|
2024-06-12 16:04:56 +02:00
|
|
|
import { QueryUtils } from "../utils"
|
2022-05-24 11:23:23 +02:00
|
|
|
import { convertJSONSchemaToTableSchema } from "../utils/json"
|
2024-12-31 15:43:08 +01:00
|
|
|
import {
|
|
|
|
FieldType,
|
|
|
|
LegacyFilter,
|
2025-01-02 15:36:27 +01:00
|
|
|
Row,
|
2024-12-31 15:43:08 +01:00
|
|
|
SearchFilters,
|
|
|
|
SortOrder,
|
|
|
|
SortType,
|
|
|
|
TableSchema,
|
|
|
|
UISearchFilter,
|
2025-01-08 14:27:13 +01:00
|
|
|
ViewSchema,
|
2024-12-31 15:43:08 +01:00
|
|
|
} from "@budibase/types"
|
2025-01-02 15:36:27 +01:00
|
|
|
import { APIClient } from "../api/types"
|
2021-12-17 10:52:12 +01:00
|
|
|
|
2024-06-12 16:13:50 +02:00
|
|
|
const { buildQuery, limit: queryLimit, runQuery, sort } = QueryUtils
|
2023-03-09 09:50:26 +01:00
|
|
|
|
2025-01-07 11:02:09 +01:00
|
|
|
interface DataFetchStore<TDefinition, TQuery> {
|
2025-01-02 15:36:27 +01:00
|
|
|
rows: Row[]
|
2025-01-08 16:04:39 +01:00
|
|
|
info: any
|
2024-12-31 15:43:08 +01:00
|
|
|
schema: TableSchema | null
|
|
|
|
loading: boolean
|
|
|
|
loaded: boolean
|
2025-01-07 11:02:09 +01:00
|
|
|
query: TQuery
|
2024-12-31 15:43:08 +01:00
|
|
|
pageNumber: number
|
2025-01-08 16:04:39 +01:00
|
|
|
cursor: string | null
|
|
|
|
cursors: string[]
|
2025-01-07 13:42:51 +01:00
|
|
|
resetKey: string
|
|
|
|
error: {
|
|
|
|
message: string
|
|
|
|
status: number
|
|
|
|
} | null
|
2025-01-07 11:02:09 +01:00
|
|
|
definition?: TDefinition | null
|
2024-12-31 15:43:08 +01:00
|
|
|
}
|
|
|
|
|
2025-01-07 11:02:09 +01:00
|
|
|
interface DataFetchDerivedStore<TDefinition, TQuery>
|
|
|
|
extends DataFetchStore<TDefinition, TQuery> {
|
2024-12-31 15:43:08 +01:00
|
|
|
hasNextPage: boolean
|
|
|
|
hasPrevPage: boolean
|
|
|
|
supportsSearch: boolean
|
|
|
|
supportsSort: boolean
|
|
|
|
supportsPagination: boolean
|
|
|
|
}
|
|
|
|
|
2025-01-07 11:47:10 +01:00
|
|
|
export interface DataFetchParams<
|
|
|
|
TDatasource,
|
|
|
|
TQuery = SearchFilters | undefined
|
|
|
|
> {
|
2025-01-07 11:02:09 +01:00
|
|
|
API: APIClient
|
|
|
|
datasource: TDatasource
|
|
|
|
query: TQuery
|
|
|
|
options?: {}
|
|
|
|
}
|
|
|
|
|
2021-12-17 10:52:12 +01:00
|
|
|
/**
|
|
|
|
* 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.
|
|
|
|
*/
|
2025-01-02 13:33:24 +01:00
|
|
|
export default abstract class DataFetch<
|
2025-01-02 13:36:38 +01:00
|
|
|
TDatasource extends {},
|
2025-01-07 16:02:34 +01:00
|
|
|
TDefinition extends {
|
|
|
|
schema?: Record<string, any> | null
|
|
|
|
primaryDisplay?: string
|
|
|
|
},
|
2025-01-07 11:02:09 +01:00
|
|
|
TQuery extends {} = SearchFilters
|
2025-01-02 13:33:24 +01:00
|
|
|
> {
|
2025-01-02 15:36:27 +01:00
|
|
|
API: APIClient
|
2024-12-31 15:43:08 +01:00
|
|
|
features: {
|
|
|
|
supportsSearch: boolean
|
|
|
|
supportsSort: boolean
|
|
|
|
supportsPagination: boolean
|
|
|
|
}
|
|
|
|
options: {
|
2025-01-02 13:33:24 +01:00
|
|
|
datasource: TDatasource
|
2024-12-31 15:43:08 +01:00
|
|
|
limit: number
|
|
|
|
// Search config
|
|
|
|
filter: UISearchFilter | LegacyFilter[] | null
|
2025-01-07 11:02:09 +01:00
|
|
|
query: TQuery
|
2024-12-31 15:43:08 +01:00
|
|
|
// Sorting config
|
|
|
|
sortColumn: string | null
|
|
|
|
sortOrder: SortOrder
|
|
|
|
sortType: SortType | null
|
|
|
|
// Pagination config
|
|
|
|
paginate: boolean
|
|
|
|
// Client side feature customisation
|
|
|
|
clientSideSearching: boolean
|
|
|
|
clientSideSorting: boolean
|
|
|
|
clientSideLimiting: boolean
|
|
|
|
}
|
2025-01-07 11:02:09 +01:00
|
|
|
store: Writable<DataFetchStore<TDefinition, TQuery>>
|
|
|
|
derivedStore: Readable<DataFetchDerivedStore<TDefinition, TQuery>>
|
2024-12-31 15:43:08 +01:00
|
|
|
|
2022-10-05 16:53:06 +02:00
|
|
|
/**
|
|
|
|
* Constructs a new DataFetch instance.
|
|
|
|
* @param opts the fetch options
|
|
|
|
*/
|
2025-01-07 11:02:09 +01:00
|
|
|
constructor(opts: DataFetchParams<TDatasource, TQuery>) {
|
2022-10-05 16:53:06 +02:00
|
|
|
// Feature flags
|
2023-03-10 17:23:56 +01:00
|
|
|
this.features = {
|
2022-10-05 16:53:06 +02:00
|
|
|
supportsSearch: false,
|
|
|
|
supportsSort: false,
|
|
|
|
supportsPagination: false,
|
2023-03-10 17:23:56 +01:00
|
|
|
}
|
2021-12-17 10:52:12 +01:00
|
|
|
|
2022-10-05 16:53:06 +02:00
|
|
|
// Config
|
|
|
|
this.options = {
|
2025-01-02 12:16:53 +01:00
|
|
|
datasource: opts.datasource,
|
2022-10-05 16:53:06 +02:00
|
|
|
limit: 10,
|
2021-12-17 10:52:12 +01:00
|
|
|
|
2022-10-05 16:53:06 +02:00
|
|
|
// Search config
|
|
|
|
filter: null,
|
2025-01-07 11:02:09 +01:00
|
|
|
query: opts.query,
|
2021-12-17 10:52:12 +01:00
|
|
|
|
2022-10-05 16:53:06 +02:00
|
|
|
// Sorting config
|
|
|
|
sortColumn: null,
|
2024-10-18 17:52:40 +02:00
|
|
|
sortOrder: SortOrder.ASCENDING,
|
2022-10-05 16:53:06 +02:00
|
|
|
sortType: null,
|
2021-12-17 10:52:12 +01:00
|
|
|
|
2022-10-05 16:53:06 +02:00
|
|
|
// Pagination config
|
|
|
|
paginate: true,
|
2023-10-26 18:37:59 +02:00
|
|
|
|
|
|
|
// Client side feature customisation
|
|
|
|
clientSideSearching: true,
|
|
|
|
clientSideSorting: true,
|
|
|
|
clientSideLimiting: true,
|
2022-10-05 16:53:06 +02:00
|
|
|
}
|
2021-12-17 10:52:12 +01:00
|
|
|
|
2022-10-05 16:53:06 +02:00
|
|
|
// State of the fetch
|
|
|
|
this.store = writable({
|
|
|
|
rows: [],
|
|
|
|
info: null,
|
|
|
|
schema: null,
|
|
|
|
loading: false,
|
|
|
|
loaded: false,
|
2025-01-07 11:02:09 +01:00
|
|
|
query: opts.query,
|
2022-10-05 16:53:06 +02:00
|
|
|
pageNumber: 0,
|
|
|
|
cursor: null,
|
|
|
|
cursors: [],
|
2025-01-07 13:42:51 +01:00
|
|
|
resetKey: Math.random().toString(),
|
2023-06-27 12:58:10 +02:00
|
|
|
error: null,
|
2022-10-05 16:53:06 +02:00
|
|
|
})
|
2021-12-17 10:52:12 +01:00
|
|
|
|
2021-12-17 11:49:12 +01:00
|
|
|
// Merge options with their default values
|
2022-01-20 10:40:53 +01:00
|
|
|
this.API = opts?.API
|
2021-12-17 10:52:12 +01:00
|
|
|
this.options = {
|
|
|
|
...this.options,
|
|
|
|
...opts,
|
|
|
|
}
|
2022-01-20 10:40:53 +01:00
|
|
|
if (!this.API) {
|
|
|
|
throw "An API client is required for fetching data"
|
|
|
|
}
|
2021-12-17 10:52:12 +01:00
|
|
|
|
|
|
|
// Bind all functions to properly scope "this"
|
|
|
|
this.getData = this.getData.bind(this)
|
2021-12-17 19:39:48 +01:00
|
|
|
this.getPage = this.getPage.bind(this)
|
2021-12-17 10:52:12 +01:00
|
|
|
this.getInitialData = this.getInitialData.bind(this)
|
2021-12-17 19:39:48 +01:00
|
|
|
this.determineFeatureFlags = this.determineFeatureFlags.bind(this)
|
2021-12-17 10:52:12 +01:00
|
|
|
this.refresh = this.refresh.bind(this)
|
|
|
|
this.update = this.update.bind(this)
|
|
|
|
this.hasNextPage = this.hasNextPage.bind(this)
|
|
|
|
this.hasPrevPage = this.hasPrevPage.bind(this)
|
|
|
|
this.nextPage = this.nextPage.bind(this)
|
|
|
|
this.prevPage = this.prevPage.bind(this)
|
|
|
|
|
|
|
|
// Derive certain properties to return
|
2023-03-10 17:23:56 +01:00
|
|
|
this.derivedStore = derived(this.store, $store => {
|
|
|
|
return {
|
|
|
|
...$store,
|
|
|
|
...this.features,
|
|
|
|
hasNextPage: this.hasNextPage($store),
|
|
|
|
hasPrevPage: this.hasPrevPage($store),
|
2021-12-17 10:52:12 +01:00
|
|
|
}
|
2023-03-10 17:23:56 +01:00
|
|
|
})
|
2021-12-17 10:52:12 +01:00
|
|
|
|
|
|
|
// Mark as loaded if we have no datasource
|
|
|
|
if (!this.options.datasource) {
|
|
|
|
this.store.update($store => ({ ...$store, loaded: true }))
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// Initially fetch data but don't bother waiting for the result
|
|
|
|
this.getInitialData()
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Extend the svelte store subscribe method to that instances of this class
|
|
|
|
* can be treated like stores
|
|
|
|
*/
|
|
|
|
get subscribe() {
|
|
|
|
return this.derivedStore.subscribe
|
|
|
|
}
|
|
|
|
|
2023-08-21 12:56:58 +02:00
|
|
|
/**
|
|
|
|
* Gets the default sort column for this datasource
|
|
|
|
*/
|
2024-12-31 15:43:08 +01:00
|
|
|
getDefaultSortColumn(
|
|
|
|
definition: { primaryDisplay?: string } | null,
|
|
|
|
schema: Record<string, any>
|
2025-01-02 12:16:53 +01:00
|
|
|
): string | null {
|
2023-08-21 12:56:58 +02:00
|
|
|
if (definition?.primaryDisplay && schema[definition.primaryDisplay]) {
|
|
|
|
return definition.primaryDisplay
|
|
|
|
} else {
|
|
|
|
return Object.keys(schema)[0]
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-12-17 10:52:12 +01:00
|
|
|
/**
|
|
|
|
* Fetches a fresh set of data from the server, resetting pagination
|
|
|
|
*/
|
|
|
|
async getInitialData() {
|
2025-01-08 14:27:13 +01:00
|
|
|
const { filter, paginate } = this.options
|
2021-12-17 10:52:12 +01:00
|
|
|
|
2023-08-08 14:13:27 +02:00
|
|
|
// Fetch datasource definition and extract sort properties if configured
|
2025-01-08 14:27:13 +01:00
|
|
|
const definition = await this.getDefinition()
|
2023-08-03 12:18:19 +02:00
|
|
|
|
|
|
|
// Determine feature flags
|
2025-01-08 12:49:17 +01:00
|
|
|
const features = await this.determineFeatureFlags()
|
2023-03-10 17:23:56 +01:00
|
|
|
this.features = {
|
2022-01-05 10:16:10 +01:00
|
|
|
supportsSearch: !!features?.supportsSearch,
|
|
|
|
supportsSort: !!features?.supportsSort,
|
2022-01-07 12:30:47 +01:00
|
|
|
supportsPagination: paginate && !!features?.supportsPagination,
|
2023-03-10 17:23:56 +01:00
|
|
|
}
|
2021-12-17 19:39:48 +01:00
|
|
|
|
2025-01-08 12:55:26 +01:00
|
|
|
if (!definition?.schema) {
|
2021-12-17 10:52:12 +01:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2025-01-08 12:55:26 +01:00
|
|
|
// Fetch and enrich schema
|
|
|
|
const schema = this.enrichSchema(definition.schema)
|
|
|
|
|
2023-08-21 12:56:58 +02:00
|
|
|
// If an invalid sort column is specified, delete it
|
|
|
|
if (this.options.sortColumn && !schema[this.options.sortColumn]) {
|
|
|
|
this.options.sortColumn = null
|
2022-11-17 15:15:24 +01:00
|
|
|
}
|
|
|
|
|
2023-08-21 12:56:58 +02:00
|
|
|
// If no sort column, get the default column for this datasource
|
|
|
|
if (!this.options.sortColumn) {
|
|
|
|
this.options.sortColumn = this.getDefaultSortColumn(definition, schema)
|
2022-11-17 15:15:24 +01:00
|
|
|
}
|
|
|
|
|
2023-08-21 12:56:58 +02:00
|
|
|
// If we don't have a sort column specified then just ensure we don't set
|
|
|
|
// any sorting params
|
|
|
|
if (!this.options.sortColumn) {
|
2024-10-18 17:52:40 +02:00
|
|
|
this.options.sortOrder = SortOrder.ASCENDING
|
2023-08-21 12:56:58 +02:00
|
|
|
this.options.sortType = null
|
|
|
|
} else {
|
|
|
|
// Otherwise determine what sort type to use base on sort column
|
2024-10-18 17:52:40 +02:00
|
|
|
this.options.sortType = SortType.STRING
|
|
|
|
const fieldSchema = schema?.[this.options.sortColumn]
|
|
|
|
if (
|
|
|
|
fieldSchema?.type === FieldType.NUMBER ||
|
|
|
|
fieldSchema?.type === FieldType.BIGINT ||
|
2024-12-31 15:43:08 +01:00
|
|
|
("calculationType" in fieldSchema && fieldSchema?.calculationType)
|
2024-10-18 17:52:40 +02:00
|
|
|
) {
|
|
|
|
this.options.sortType = SortType.NUMBER
|
|
|
|
}
|
2025-01-07 14:01:55 +01:00
|
|
|
|
2023-08-21 12:56:58 +02:00
|
|
|
// If no sort order, default to ascending
|
|
|
|
if (!this.options.sortOrder) {
|
2024-10-18 17:52:40 +02:00
|
|
|
this.options.sortOrder = SortOrder.ASCENDING
|
2025-01-07 14:01:55 +01:00
|
|
|
} else {
|
|
|
|
// Ensure sortOrder matches the enum
|
|
|
|
this.options.sortOrder =
|
|
|
|
this.options.sortOrder.toLowerCase() as SortOrder
|
2023-08-21 12:56:58 +02:00
|
|
|
}
|
2021-12-17 10:52:12 +01:00
|
|
|
}
|
|
|
|
|
2024-06-12 16:13:50 +02:00
|
|
|
// Build the query
|
2024-09-16 13:46:21 +02:00
|
|
|
let query = this.options.query
|
2024-10-29 12:13:54 +01:00
|
|
|
if (!query) {
|
2025-01-07 11:02:09 +01:00
|
|
|
query = buildQuery(filter ?? undefined) as TQuery
|
2021-12-17 10:52:12 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
// Update store
|
2021-12-17 19:39:48 +01:00
|
|
|
this.store.update($store => ({
|
|
|
|
...$store,
|
|
|
|
definition,
|
|
|
|
schema,
|
|
|
|
query,
|
|
|
|
loading: true,
|
2022-08-05 16:57:21 +02:00
|
|
|
cursors: [],
|
|
|
|
cursor: null,
|
2021-12-17 19:39:48 +01:00
|
|
|
}))
|
2021-12-17 10:52:12 +01:00
|
|
|
|
|
|
|
// Actually fetch data
|
|
|
|
const page = await this.getPage()
|
|
|
|
this.store.update($store => ({
|
|
|
|
...$store,
|
|
|
|
loading: false,
|
|
|
|
loaded: true,
|
|
|
|
pageNumber: 0,
|
|
|
|
rows: page.rows,
|
2021-12-17 14:12:28 +01:00
|
|
|
info: page.info,
|
2022-01-07 12:30:47 +01:00
|
|
|
cursors: paginate && page.hasNextPage ? [null, page.cursor] : [null],
|
2022-08-01 19:56:59 +02:00
|
|
|
error: page.error,
|
2025-01-07 13:42:51 +01:00
|
|
|
resetKey: Math.random().toString(),
|
2021-12-17 10:52:12 +01:00
|
|
|
}))
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Fetches some filtered, sorted and paginated data
|
|
|
|
*/
|
|
|
|
async getPage() {
|
2023-10-26 18:37:59 +02:00
|
|
|
const {
|
|
|
|
sortColumn,
|
|
|
|
sortOrder,
|
|
|
|
sortType,
|
|
|
|
limit,
|
|
|
|
clientSideSearching,
|
|
|
|
clientSideSorting,
|
|
|
|
clientSideLimiting,
|
|
|
|
} = this.options
|
2021-12-17 19:48:44 +01:00
|
|
|
const { query } = get(this.store)
|
2021-12-17 10:52:12 +01:00
|
|
|
|
|
|
|
// Get the actual data
|
2022-08-01 19:56:59 +02:00
|
|
|
let { rows, info, hasNextPage, cursor, error } = await this.getData()
|
2021-12-17 10:52:12 +01:00
|
|
|
|
|
|
|
// If we don't support searching, do a client search
|
2023-10-26 18:37:59 +02:00
|
|
|
if (!this.features.supportsSearch && clientSideSearching) {
|
2024-06-12 15:44:20 +02:00
|
|
|
rows = runQuery(rows, query)
|
2021-12-17 10:52:12 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
// If we don't support sorting, do a client-side sort
|
2025-01-08 15:57:15 +01:00
|
|
|
if (!this.features.supportsSort && clientSideSorting && sortType) {
|
2024-12-31 15:43:08 +01:00
|
|
|
rows = sort(rows, sortColumn as any, sortOrder, sortType)
|
2021-12-17 10:52:12 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
// If we don't support pagination, do a client-side limit
|
2023-10-26 18:37:59 +02:00
|
|
|
if (!this.features.supportsPagination && clientSideLimiting) {
|
2024-06-12 16:13:50 +02:00
|
|
|
rows = queryLimit(rows, limit)
|
2021-12-17 10:52:12 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
return {
|
|
|
|
rows,
|
2021-12-17 14:12:28 +01:00
|
|
|
info,
|
2021-12-17 10:52:12 +01:00
|
|
|
hasNextPage,
|
|
|
|
cursor,
|
2022-08-01 19:56:59 +02:00
|
|
|
error,
|
2021-12-17 10:52:12 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-12-31 15:43:08 +01:00
|
|
|
abstract getData(): Promise<{
|
2025-01-02 15:36:27 +01:00
|
|
|
rows: Row[]
|
2025-01-02 10:23:39 +01:00
|
|
|
info?: any
|
2025-01-02 16:25:25 +01:00
|
|
|
hasNextPage?: boolean
|
2025-01-02 10:23:39 +01:00
|
|
|
cursor?: any
|
|
|
|
error?: any
|
2024-12-31 15:43:08 +01:00
|
|
|
}>
|
2021-12-17 10:52:12 +01:00
|
|
|
|
|
|
|
/**
|
2021-12-17 19:39:48 +01:00
|
|
|
* Gets the definition for this datasource.
|
2025-01-08 14:27:13 +01:00
|
|
|
|
2021-12-17 19:39:48 +01:00
|
|
|
* @return {object} the definition
|
2021-12-17 10:52:12 +01:00
|
|
|
*/
|
2025-01-08 14:27:13 +01:00
|
|
|
abstract getDefinition(): Promise<TDefinition | null>
|
2021-12-17 19:39:48 +01:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Gets the schema definition for a datasource.
|
|
|
|
* @param definition the datasource definition
|
|
|
|
* @return {object} the schema
|
|
|
|
*/
|
2025-01-08 14:27:13 +01:00
|
|
|
getSchema(
|
|
|
|
definition: TDefinition | null
|
|
|
|
): ViewSchema | Record<string, any> | undefined {
|
2025-01-08 12:57:25 +01:00
|
|
|
return definition?.schema ?? undefined
|
2025-01-07 16:02:34 +01:00
|
|
|
}
|
2021-12-17 10:52:12 +01:00
|
|
|
|
|
|
|
/**
|
2022-05-24 11:23:23 +02:00
|
|
|
* Enriches a datasource schema with nested fields and ensures the structure
|
|
|
|
* is correct.
|
2021-12-17 10:52:12 +01:00
|
|
|
* @param schema the datasource schema
|
|
|
|
* @return {object} the enriched datasource schema
|
|
|
|
*/
|
2025-01-08 12:55:26 +01:00
|
|
|
private enrichSchema(schema: TableSchema): TableSchema {
|
2022-05-24 11:29:58 +02:00
|
|
|
// Check for any JSON fields so we can add any top level properties
|
2024-12-31 15:43:08 +01:00
|
|
|
let jsonAdditions: Record<string, { type: string; nestedJSON: true }> = {}
|
|
|
|
for (const fieldKey of Object.keys(schema)) {
|
2022-05-24 11:29:58 +02:00
|
|
|
const fieldSchema = schema[fieldKey]
|
2024-10-18 17:52:40 +02:00
|
|
|
if (fieldSchema?.type === FieldType.JSON) {
|
2022-05-24 11:29:58 +02:00
|
|
|
const jsonSchema = convertJSONSchemaToTableSchema(fieldSchema, {
|
|
|
|
squashObjects: true,
|
2024-12-31 15:43:08 +01:00
|
|
|
}) 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,
|
|
|
|
}
|
2022-05-24 11:29:58 +02:00
|
|
|
}
|
2024-12-31 15:43:08 +01:00
|
|
|
}
|
2022-05-24 11:29:58 +02:00
|
|
|
}
|
2024-12-31 15:43:08 +01:00
|
|
|
}
|
2022-05-24 11:29:58 +02:00
|
|
|
|
2022-05-24 11:23:23 +02:00
|
|
|
// Ensure schema is in the correct structure
|
2024-12-31 15:43:08 +01:00
|
|
|
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,
|
|
|
|
}
|
2021-12-17 10:52:12 +01:00
|
|
|
}
|
|
|
|
}
|
2024-12-31 15:43:08 +01:00
|
|
|
)
|
2022-05-24 11:23:23 +02:00
|
|
|
|
2021-12-17 10:52:12 +01:00
|
|
|
return enrichedSchema
|
|
|
|
}
|
|
|
|
|
2021-12-17 19:39:48 +01:00
|
|
|
/**
|
2025-01-08 12:49:17 +01:00
|
|
|
* Determine the feature flag for this datasource
|
2021-12-17 19:39:48 +01:00
|
|
|
*/
|
2025-01-08 12:49:17 +01:00
|
|
|
async determineFeatureFlags(): Promise<{
|
2025-01-07 11:47:10 +01:00
|
|
|
supportsPagination: boolean
|
|
|
|
supportsSearch?: boolean
|
|
|
|
supportsSort?: boolean
|
2025-01-08 12:49:17 +01:00
|
|
|
}> {
|
2022-01-05 10:16:10 +01:00
|
|
|
return {
|
|
|
|
supportsSearch: false,
|
|
|
|
supportsSort: false,
|
|
|
|
supportsPagination: false,
|
|
|
|
}
|
2021-12-17 19:39:48 +01:00
|
|
|
}
|
|
|
|
|
2021-12-17 10:52:12 +01:00
|
|
|
/**
|
|
|
|
* Resets the data set and updates options
|
|
|
|
* @param newOptions any new options
|
|
|
|
*/
|
2024-12-31 15:43:08 +01:00
|
|
|
async update(newOptions: any) {
|
2021-12-17 10:52:12 +01:00
|
|
|
// Check if any settings have actually changed
|
|
|
|
let refresh = false
|
2024-12-31 15:43:08 +01:00
|
|
|
for (const [key, value] of Object.entries(newOptions || {})) {
|
|
|
|
const oldVal = this.options[key as keyof typeof this.options] ?? null
|
2024-09-20 14:59:50 +02:00
|
|
|
const newVal = value == null ? null : value
|
|
|
|
if (JSON.stringify(newVal) !== JSON.stringify(oldVal)) {
|
2021-12-17 10:52:12 +01:00
|
|
|
refresh = true
|
|
|
|
break
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (!refresh) {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2022-01-26 18:44:14 +01:00
|
|
|
// Assign new options and reload data.
|
|
|
|
// Clone the new options to ensure that some external source doesn't end up
|
|
|
|
// mutating the real values in the config.
|
2021-12-17 10:52:12 +01:00
|
|
|
this.options = {
|
|
|
|
...this.options,
|
2022-01-26 18:44:14 +01:00
|
|
|
...cloneDeep(newOptions),
|
2021-12-17 10:52:12 +01:00
|
|
|
}
|
|
|
|
await this.getInitialData()
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Loads the same page again
|
|
|
|
*/
|
|
|
|
async refresh() {
|
|
|
|
if (get(this.store).loading) {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
this.store.update($store => ({ ...$store, loading: true }))
|
2023-05-05 13:10:49 +02:00
|
|
|
const { rows, info, error, cursor } = await this.getPage()
|
|
|
|
|
|
|
|
let { cursors } = get(this.store)
|
|
|
|
const { pageNumber } = get(this.store)
|
|
|
|
|
2023-05-05 14:03:53 +02:00
|
|
|
if (!rows.length && pageNumber > 0) {
|
2023-05-05 14:05:08 +02:00
|
|
|
// If the full page is gone but we have previous pages, navigate to the previous page
|
2023-05-05 14:03:53 +02:00
|
|
|
this.store.update($store => ({
|
|
|
|
...$store,
|
|
|
|
loading: false,
|
|
|
|
cursors: cursors.slice(0, pageNumber),
|
|
|
|
}))
|
|
|
|
return await this.prevPage()
|
|
|
|
}
|
|
|
|
|
2023-05-05 14:05:08 +02:00
|
|
|
const currentNextCursor = cursors[pageNumber + 1]
|
|
|
|
if (currentNextCursor != cursor) {
|
|
|
|
// If the current cursor changed, all the next pages need to be updated, so we mark them as stale
|
|
|
|
cursors = cursors.slice(0, pageNumber + 1)
|
|
|
|
cursors[pageNumber + 1] = cursor
|
|
|
|
}
|
|
|
|
|
2022-08-01 19:56:59 +02:00
|
|
|
this.store.update($store => ({
|
|
|
|
...$store,
|
|
|
|
rows,
|
|
|
|
info,
|
|
|
|
loading: false,
|
|
|
|
error,
|
2023-05-05 13:10:49 +02:00
|
|
|
cursors,
|
2022-08-01 19:56:59 +02:00
|
|
|
}))
|
2021-12-17 10:52:12 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Determines whether there is a next page of data based on the state of the
|
|
|
|
* store
|
|
|
|
* @param state the current store state
|
|
|
|
* @return {boolean} whether there is a next page of data or not
|
|
|
|
*/
|
2025-01-08 12:55:26 +01:00
|
|
|
private hasNextPage(state: DataFetchStore<TDefinition, TQuery>): boolean {
|
2021-12-17 10:52:12 +01:00
|
|
|
return state.cursors[state.pageNumber + 1] != null
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Determines whether there is a previous page of data based on the state of
|
|
|
|
* the store
|
|
|
|
* @param state the current store state
|
|
|
|
* @return {boolean} whether there is a previous page of data or not
|
|
|
|
*/
|
2025-01-08 12:55:26 +01:00
|
|
|
private hasPrevPage(state: { pageNumber: number }): boolean {
|
2021-12-17 10:52:12 +01:00
|
|
|
return state.pageNumber > 0
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Fetches the next page of data
|
|
|
|
*/
|
|
|
|
async nextPage() {
|
|
|
|
const state = get(this.derivedStore)
|
|
|
|
if (state.loading || !this.options.paginate || !state.hasNextPage) {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// Fetch next page
|
|
|
|
const nextCursor = state.cursors[state.pageNumber + 1]
|
|
|
|
this.store.update($store => ({
|
|
|
|
...$store,
|
|
|
|
loading: true,
|
|
|
|
cursor: nextCursor,
|
2021-12-17 19:39:48 +01:00
|
|
|
pageNumber: $store.pageNumber + 1,
|
2021-12-17 10:52:12 +01:00
|
|
|
}))
|
2022-08-01 19:56:59 +02:00
|
|
|
const { rows, info, hasNextPage, cursor, error } = await this.getPage()
|
2021-12-17 10:52:12 +01:00
|
|
|
|
|
|
|
// Update state
|
|
|
|
this.store.update($store => {
|
|
|
|
let { cursors, pageNumber } = $store
|
|
|
|
if (hasNextPage) {
|
2021-12-17 19:39:48 +01:00
|
|
|
cursors[pageNumber + 1] = cursor
|
2021-12-17 10:52:12 +01:00
|
|
|
}
|
|
|
|
return {
|
|
|
|
...$store,
|
|
|
|
rows,
|
2021-12-17 14:12:28 +01:00
|
|
|
info,
|
2021-12-17 10:52:12 +01:00
|
|
|
cursors,
|
|
|
|
loading: false,
|
2022-08-01 19:56:59 +02:00
|
|
|
error,
|
2021-12-17 10:52:12 +01:00
|
|
|
}
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Fetches the previous page of data
|
|
|
|
*/
|
|
|
|
async prevPage() {
|
|
|
|
const state = get(this.derivedStore)
|
|
|
|
if (state.loading || !this.options.paginate || !state.hasPrevPage) {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// Fetch previous page
|
|
|
|
const prevCursor = state.cursors[state.pageNumber - 1]
|
|
|
|
this.store.update($store => ({
|
|
|
|
...$store,
|
|
|
|
loading: true,
|
|
|
|
cursor: prevCursor,
|
2021-12-17 19:39:48 +01:00
|
|
|
pageNumber: $store.pageNumber - 1,
|
2021-12-17 10:52:12 +01:00
|
|
|
}))
|
2022-08-01 19:56:59 +02:00
|
|
|
const { rows, info, error } = await this.getPage()
|
2021-12-17 10:52:12 +01:00
|
|
|
|
|
|
|
// Update state
|
|
|
|
this.store.update($store => {
|
|
|
|
return {
|
|
|
|
...$store,
|
|
|
|
rows,
|
2021-12-17 14:12:28 +01:00
|
|
|
info,
|
2021-12-17 10:52:12 +01:00
|
|
|
loading: false,
|
2022-08-01 19:56:59 +02:00
|
|
|
error,
|
2021-12-17 10:52:12 +01:00
|
|
|
}
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|