forked from tsai/budibase
14 changed files with 938 additions and 390 deletions
@ -1,94 +0,0 @@ |
|||||
import { cloneDeep } from "lodash/fp" |
|
||||
import { fetchTableData, fetchTableDefinition } from "./tables" |
|
||||
import { fetchViewData } from "./views" |
|
||||
import { fetchRelationshipData } from "./relationships" |
|
||||
import { FieldTypes } from "../constants" |
|
||||
import { executeQuery, fetchQueryDefinition } from "./queries" |
|
||||
|
|
||||
/** |
|
||||
* Fetches all rows for a particular Budibase data source. |
|
||||
*/ |
|
||||
export const fetchDatasource = async dataSource => { |
|
||||
if (!dataSource || !dataSource.type) { |
|
||||
return [] |
|
||||
} |
|
||||
|
|
||||
// Fetch all rows in data source
|
|
||||
const { type, tableId, fieldName } = dataSource |
|
||||
let rows = [] |
|
||||
if (type === "table") { |
|
||||
rows = await fetchTableData(tableId) |
|
||||
} else if (type === "view") { |
|
||||
rows = await fetchViewData(dataSource) |
|
||||
} else if (type === "query") { |
|
||||
// Set the default query params
|
|
||||
let parameters = cloneDeep(dataSource.queryParams || {}) |
|
||||
for (let param of dataSource.parameters) { |
|
||||
if (!parameters[param.name]) { |
|
||||
parameters[param.name] = param.default |
|
||||
} |
|
||||
} |
|
||||
rows = await executeQuery({ queryId: dataSource._id, parameters }) |
|
||||
} else if (type === FieldTypes.LINK) { |
|
||||
rows = await fetchRelationshipData({ |
|
||||
rowId: dataSource.rowId, |
|
||||
tableId: dataSource.rowTableId, |
|
||||
fieldName, |
|
||||
}) |
|
||||
} |
|
||||
|
|
||||
// Enrich the result is always an array
|
|
||||
return Array.isArray(rows) ? rows : [] |
|
||||
} |
|
||||
|
|
||||
/** |
|
||||
* Fetches the schema of any kind of datasource. |
|
||||
*/ |
|
||||
export const fetchDatasourceSchema = async dataSource => { |
|
||||
if (!dataSource) { |
|
||||
return null |
|
||||
} |
|
||||
const { type } = dataSource |
|
||||
|
|
||||
// Nested providers should already have exposed their own schema
|
|
||||
if (type === "provider") { |
|
||||
return dataSource.value?.schema |
|
||||
} |
|
||||
|
|
||||
// Field sources have their schema statically defined
|
|
||||
if (type === "field") { |
|
||||
if (dataSource.fieldType === "attachment") { |
|
||||
return { |
|
||||
url: { |
|
||||
type: "string", |
|
||||
}, |
|
||||
name: { |
|
||||
type: "string", |
|
||||
}, |
|
||||
} |
|
||||
} else if (dataSource.fieldType === "array") { |
|
||||
return { |
|
||||
value: { |
|
||||
type: "string", |
|
||||
}, |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
// Tables, views and links can be fetched by table ID
|
|
||||
if ( |
|
||||
(type === "table" || type === "view" || type === "link") && |
|
||||
dataSource.tableId |
|
||||
) { |
|
||||
const table = await fetchTableDefinition(dataSource.tableId) |
|
||||
return table?.schema |
|
||||
} |
|
||||
|
|
||||
// Queries can be fetched by query ID
|
|
||||
if (type === "query" && dataSource._id) { |
|
||||
const definition = await fetchQueryDefinition(dataSource._id) |
|
||||
return definition?.schema |
|
||||
} |
|
||||
|
|
||||
return null |
|
||||
} |
|
||||
@ -0,0 +1,361 @@ |
|||||
|
<script> |
||||
|
import { getContext } from "svelte" |
||||
|
import { ProgressCircle, Pagination } from "@budibase/bbui" |
||||
|
import { |
||||
|
buildLuceneQuery, |
||||
|
luceneQuery, |
||||
|
luceneSort, |
||||
|
luceneLimit, |
||||
|
} from "builder/src/helpers/lucene" |
||||
|
import Placeholder from "./Placeholder.svelte" |
||||
|
import { fetchData } from "utils/fetch/fetchData.js" |
||||
|
|
||||
|
export let dataSource |
||||
|
export let filter |
||||
|
export let sortColumn |
||||
|
export let sortOrder |
||||
|
export let limit |
||||
|
export let paginate |
||||
|
|
||||
|
const { API, styleable, Provider, ActionTypes } = getContext("sdk") |
||||
|
const component = getContext("component") |
||||
|
|
||||
|
// Loading flag every time data is being fetched |
||||
|
let loading = false |
||||
|
|
||||
|
// Loading flag for the initial load |
||||
|
// Mark as loaded if we have no datasource so we don't stall forever |
||||
|
let loaded = !dataSource |
||||
|
let schemaLoaded = false |
||||
|
|
||||
|
// Provider state |
||||
|
let rows = [] |
||||
|
let allRows = [] |
||||
|
let schema = {} |
||||
|
let bookmarks = [null] |
||||
|
let pageNumber = 0 |
||||
|
let query = null |
||||
|
let queryExtensions = {} |
||||
|
|
||||
|
// Sorting can be overridden at run time, so we can't use the prop directly |
||||
|
let currentSortColumn = sortColumn |
||||
|
let currentSortOrder = sortOrder |
||||
|
|
||||
|
// Reset the current sort state to props if props change |
||||
|
$: currentSortColumn = sortColumn |
||||
|
$: currentSortOrder = sortOrder |
||||
|
|
||||
|
$: defaultQuery = buildLuceneQuery(filter) |
||||
|
$: extendQuery(defaultQuery, queryExtensions) |
||||
|
$: internalTable = dataSource?.type === "table" |
||||
|
$: nestedProvider = dataSource?.type === "provider" |
||||
|
$: hasNextPage = bookmarks[pageNumber + 1] != null |
||||
|
$: hasPrevPage = pageNumber > 0 |
||||
|
$: getSchema(dataSource) |
||||
|
$: sortType = getSortType(schema, currentSortColumn) |
||||
|
|
||||
|
// Wait until schema loads before loading data, so that we can determine |
||||
|
// the correct sort type first time |
||||
|
$: { |
||||
|
if (schemaLoaded) { |
||||
|
fetchData( |
||||
|
dataSource, |
||||
|
schema, |
||||
|
query, |
||||
|
limit, |
||||
|
currentSortColumn, |
||||
|
currentSortOrder, |
||||
|
sortType, |
||||
|
paginate |
||||
|
) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// Reactively filter and sort rows if required |
||||
|
$: { |
||||
|
if (internalTable) { |
||||
|
// Internal tables are already processed server-side |
||||
|
rows = allRows |
||||
|
} else { |
||||
|
// For anything else we use client-side implementations to filter, sort |
||||
|
// and limit |
||||
|
const filtered = luceneQuery(allRows, query) |
||||
|
const sorted = luceneSort( |
||||
|
filtered, |
||||
|
currentSortColumn, |
||||
|
currentSortOrder, |
||||
|
sortType |
||||
|
) |
||||
|
rows = luceneLimit(sorted, limit) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// Build our action context |
||||
|
$: actions = [ |
||||
|
{ |
||||
|
type: ActionTypes.RefreshDatasource, |
||||
|
callback: () => refresh(), |
||||
|
metadata: { dataSource }, |
||||
|
}, |
||||
|
{ |
||||
|
type: ActionTypes.AddDataProviderQueryExtension, |
||||
|
callback: addQueryExtension, |
||||
|
}, |
||||
|
{ |
||||
|
type: ActionTypes.RemoveDataProviderQueryExtension, |
||||
|
callback: removeQueryExtension, |
||||
|
}, |
||||
|
{ |
||||
|
type: ActionTypes.SetDataProviderSorting, |
||||
|
callback: ({ column, order }) => { |
||||
|
if (column) { |
||||
|
currentSortColumn = column |
||||
|
} |
||||
|
if (order) { |
||||
|
currentSortOrder = order |
||||
|
} |
||||
|
}, |
||||
|
}, |
||||
|
] |
||||
|
|
||||
|
// Build our data context |
||||
|
$: dataContext = { |
||||
|
rows, |
||||
|
schema, |
||||
|
rowsLength: rows.length, |
||||
|
|
||||
|
// Undocumented properties. These aren't supposed to be used in builder |
||||
|
// bindings, but are used internally by other components |
||||
|
id: $component?.id, |
||||
|
state: { |
||||
|
query, |
||||
|
sortColumn: currentSortColumn, |
||||
|
sortOrder: currentSortOrder, |
||||
|
}, |
||||
|
loaded, |
||||
|
} |
||||
|
|
||||
|
const getSortType = (schema, sortColumn) => { |
||||
|
if (!schema || !sortColumn || !schema[sortColumn]) { |
||||
|
return "string" |
||||
|
} |
||||
|
const type = schema?.[sortColumn]?.type |
||||
|
return type === "number" ? "number" : "string" |
||||
|
} |
||||
|
|
||||
|
const refresh = async () => { |
||||
|
if (schemaLoaded && !nestedProvider) { |
||||
|
fetchData( |
||||
|
dataSource, |
||||
|
schema, |
||||
|
query, |
||||
|
limit, |
||||
|
currentSortColumn, |
||||
|
currentSortOrder, |
||||
|
sortType, |
||||
|
paginate |
||||
|
) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
const fetchData = async ( |
||||
|
dataSource, |
||||
|
schema, |
||||
|
query, |
||||
|
limit, |
||||
|
sortColumn, |
||||
|
sortOrder, |
||||
|
sortType, |
||||
|
paginate |
||||
|
) => { |
||||
|
loading = true |
||||
|
if (dataSource?.type === "table") { |
||||
|
// Sanity check sort column, as using a non-existant column will prevent |
||||
|
// results coming back at all |
||||
|
const sort = schema?.[sortColumn] ? sortColumn : undefined |
||||
|
|
||||
|
// For internal tables we use server-side processing |
||||
|
const res = await API.searchTable({ |
||||
|
tableId: dataSource.tableId, |
||||
|
query, |
||||
|
limit, |
||||
|
sort, |
||||
|
sortOrder: sortOrder?.toLowerCase() ?? "ascending", |
||||
|
sortType, |
||||
|
paginate, |
||||
|
}) |
||||
|
pageNumber = 0 |
||||
|
allRows = res.rows |
||||
|
if (res.hasNextPage) { |
||||
|
bookmarks = [null, res.bookmark] |
||||
|
} else { |
||||
|
bookmarks = [null] |
||||
|
} |
||||
|
} else if (dataSource?.type === "provider") { |
||||
|
// For providers referencing another provider, just use the rows it |
||||
|
// provides |
||||
|
allRows = dataSource?.value?.rows || [] |
||||
|
} else if (dataSource?.type === "field") { |
||||
|
// Field sources will be available from context. |
||||
|
// Enrich non object elements into object to ensure a valid schema. |
||||
|
const data = dataSource?.value || [] |
||||
|
if (Array.isArray(data) && data[0] && typeof data[0] !== "object") { |
||||
|
allRows = data.map(value => ({ value })) |
||||
|
} else { |
||||
|
allRows = data |
||||
|
} |
||||
|
} else { |
||||
|
// For other data sources like queries or views, fetch all rows from the |
||||
|
// server |
||||
|
allRows = await API.fetchDatasource(dataSource) |
||||
|
} |
||||
|
loading = false |
||||
|
loaded = true |
||||
|
} |
||||
|
|
||||
|
const getSchema = async dataSource => { |
||||
|
let newSchema = (await API.fetchDatasourceSchema(dataSource)) || {} |
||||
|
|
||||
|
// Ensure there are "name" properties for all fields and that field schema |
||||
|
// are objects |
||||
|
Object.entries(newSchema).forEach(([fieldName, fieldSchema]) => { |
||||
|
if (typeof fieldSchema === "string") { |
||||
|
newSchema[fieldName] = { |
||||
|
type: fieldSchema, |
||||
|
name: fieldName, |
||||
|
} |
||||
|
} else { |
||||
|
newSchema[fieldName] = { |
||||
|
...fieldSchema, |
||||
|
name: fieldName, |
||||
|
} |
||||
|
} |
||||
|
}) |
||||
|
schema = newSchema |
||||
|
schemaLoaded = true |
||||
|
} |
||||
|
|
||||
|
const nextPage = async () => { |
||||
|
if (!hasNextPage || !internalTable) { |
||||
|
return |
||||
|
} |
||||
|
const sort = schema?.[currentSortColumn] ? currentSortColumn : undefined |
||||
|
const res = await API.searchTable({ |
||||
|
tableId: dataSource?.tableId, |
||||
|
query, |
||||
|
bookmark: bookmarks[pageNumber + 1], |
||||
|
limit, |
||||
|
sort, |
||||
|
sortOrder: currentSortOrder?.toLowerCase() ?? "ascending", |
||||
|
sortType, |
||||
|
paginate: true, |
||||
|
}) |
||||
|
pageNumber++ |
||||
|
allRows = res.rows |
||||
|
if (res.hasNextPage) { |
||||
|
bookmarks[pageNumber + 1] = res.bookmark |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
const prevPage = async () => { |
||||
|
if (!hasPrevPage || !internalTable) { |
||||
|
return |
||||
|
} |
||||
|
const sort = schema?.[currentSortColumn] ? currentSortColumn : undefined |
||||
|
const res = await API.searchTable({ |
||||
|
tableId: dataSource?.tableId, |
||||
|
query, |
||||
|
bookmark: bookmarks[pageNumber - 1], |
||||
|
limit, |
||||
|
sort, |
||||
|
sortOrder: currentSortOrder?.toLowerCase() ?? "ascending", |
||||
|
sortType, |
||||
|
paginate: true, |
||||
|
}) |
||||
|
pageNumber-- |
||||
|
allRows = res.rows |
||||
|
} |
||||
|
|
||||
|
const addQueryExtension = (key, extension) => { |
||||
|
if (!key || !extension) { |
||||
|
return |
||||
|
} |
||||
|
queryExtensions = { ...queryExtensions, [key]: extension } |
||||
|
} |
||||
|
|
||||
|
const removeQueryExtension = key => { |
||||
|
if (!key) { |
||||
|
return |
||||
|
} |
||||
|
const newQueryExtensions = { ...queryExtensions } |
||||
|
delete newQueryExtensions[key] |
||||
|
queryExtensions = newQueryExtensions |
||||
|
} |
||||
|
|
||||
|
const extendQuery = (defaultQuery, extensions) => { |
||||
|
const extensionValues = Object.values(extensions || {}) |
||||
|
let extendedQuery = { ...defaultQuery } |
||||
|
extensionValues.forEach(extension => { |
||||
|
Object.entries(extension || {}).forEach(([operator, fields]) => { |
||||
|
extendedQuery[operator] = { |
||||
|
...extendedQuery[operator], |
||||
|
...fields, |
||||
|
} |
||||
|
}) |
||||
|
}) |
||||
|
|
||||
|
if (JSON.stringify(query) !== JSON.stringify(extendedQuery)) { |
||||
|
query = extendedQuery |
||||
|
} |
||||
|
} |
||||
|
</script> |
||||
|
|
||||
|
<div use:styleable={$component.styles} class="container"> |
||||
|
<Provider {actions} data={dataContext}> |
||||
|
{#if !loaded} |
||||
|
<div class="loading"> |
||||
|
<ProgressCircle /> |
||||
|
</div> |
||||
|
{:else} |
||||
|
{#if $component.emptyState} |
||||
|
<Placeholder /> |
||||
|
{:else} |
||||
|
<slot /> |
||||
|
{/if} |
||||
|
{#if paginate && internalTable} |
||||
|
<div class="pagination"> |
||||
|
<Pagination |
||||
|
page={pageNumber + 1} |
||||
|
{hasPrevPage} |
||||
|
{hasNextPage} |
||||
|
goToPrevPage={prevPage} |
||||
|
goToNextPage={nextPage} |
||||
|
/> |
||||
|
</div> |
||||
|
{/if} |
||||
|
{/if} |
||||
|
</Provider> |
||||
|
</div> |
||||
|
|
||||
|
<style> |
||||
|
.container { |
||||
|
display: flex; |
||||
|
flex-direction: column; |
||||
|
justify-content: flex-start; |
||||
|
align-items: stretch; |
||||
|
} |
||||
|
.loading { |
||||
|
display: flex; |
||||
|
flex-direction: row; |
||||
|
justify-content: center; |
||||
|
align-items: center; |
||||
|
height: 100px; |
||||
|
} |
||||
|
.pagination { |
||||
|
display: flex; |
||||
|
flex-direction: row; |
||||
|
justify-content: flex-end; |
||||
|
align-items: center; |
||||
|
margin-top: var(--spacing-xl); |
||||
|
} |
||||
|
</style> |
||||
@ -0,0 +1,42 @@ |
|||||
|
import TableFetch from "./TableFetch.js" |
||||
|
import { executeQuery, fetchQueryDefinition } from "api" |
||||
|
import { cloneDeep } from "lodash/fp.js" |
||||
|
|
||||
|
export default class ViewFetch extends TableFetch { |
||||
|
SupportsSearch = false |
||||
|
SupportsSort = false |
||||
|
SupportsPagination = false |
||||
|
|
||||
|
/** |
||||
|
* Fetches the schema for a view |
||||
|
* @param datasource the view datasource config |
||||
|
* @return {object} the view schema |
||||
|
*/ |
||||
|
static async getSchema(datasource) { |
||||
|
if (!datasource?._id) { |
||||
|
return null |
||||
|
} |
||||
|
const definition = await fetchQueryDefinition(datasource._id) |
||||
|
return this.enrichSchema(definition?.schema) |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Fetches a single page of data from the remote resource |
||||
|
*/ |
||||
|
async getData() { |
||||
|
const { datasource } = this.options |
||||
|
|
||||
|
// Set the default query params
|
||||
|
let parameters = cloneDeep(datasource?.queryParams || {}) |
||||
|
for (let param of datasource?.parameters || {}) { |
||||
|
if (!parameters[param.name]) { |
||||
|
parameters[param.name] = param.default |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
const res = await executeQuery({ queryId: datasource?._id, parameters }) |
||||
|
return { |
||||
|
rows: res || [], |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,23 @@ |
|||||
|
import TableFetch from "./TableFetch.js" |
||||
|
import { fetchRelationshipData } from "api" |
||||
|
|
||||
|
export default class ViewFetch extends TableFetch { |
||||
|
SupportsSearch = false |
||||
|
SupportsSort = false |
||||
|
SupportsPagination = false |
||||
|
|
||||
|
/** |
||||
|
* Fetches a single page of data from the remote resource |
||||
|
*/ |
||||
|
async getData() { |
||||
|
const { datasource } = this.options |
||||
|
const res = await fetchRelationshipData({ |
||||
|
rowId: datasource?.rowId, |
||||
|
tableId: datasource?.rowTableId, |
||||
|
fieldName: datasource?.fieldName, |
||||
|
}) |
||||
|
return { |
||||
|
rows: res || [], |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,324 @@ |
|||||
|
import { writable, derived, get } from "svelte/store" |
||||
|
import * as API from "api" |
||||
|
import { buildLuceneQuery } from "builder/src/helpers/lucene" |
||||
|
import { fetchTableDefinition } from "api" |
||||
|
|
||||
|
/** |
||||
|
* Parent class which handles the implementation of fetching data from an |
||||
|
* internal table or datasource plus. |
||||
|
* For other types of datasource, this class is overridden and extended. |
||||
|
*/ |
||||
|
export default class TableFetch { |
||||
|
SupportsSearch = true |
||||
|
SupportsSort = true |
||||
|
SupportsPagination = true |
||||
|
|
||||
|
// Config
|
||||
|
options = { |
||||
|
datasource: null, |
||||
|
schema: null, |
||||
|
limit: 10, |
||||
|
|
||||
|
// Search config
|
||||
|
filter: null, |
||||
|
query: null, |
||||
|
|
||||
|
// Sorting config
|
||||
|
sortColumn: null, |
||||
|
sortOrder: "ascending", |
||||
|
|
||||
|
// Pagination config
|
||||
|
paginate: true, |
||||
|
} |
||||
|
|
||||
|
// State of the fetch
|
||||
|
store = writable({ |
||||
|
rows: [], |
||||
|
schema: null, |
||||
|
loading: false, |
||||
|
loaded: false, |
||||
|
query: null, |
||||
|
pageNumber: 0, |
||||
|
cursor: null, |
||||
|
cursors: [], |
||||
|
}) |
||||
|
|
||||
|
/** |
||||
|
* Constructs a new DataFetch instance. |
||||
|
* @param opts the fetch options |
||||
|
*/ |
||||
|
constructor(opts) { |
||||
|
this.options = { |
||||
|
...this.options, |
||||
|
...opts, |
||||
|
} |
||||
|
|
||||
|
// Bind all functions to properly scope "this"
|
||||
|
this.getData = this.getData.bind(this) |
||||
|
this.getInitialData = this.getInitialData.bind(this) |
||||
|
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
|
||||
|
this.derivedStore = derived(this.store, $store => { |
||||
|
return { |
||||
|
...$store, |
||||
|
hasNextPage: this.hasNextPage($store), |
||||
|
hasPrevPage: this.hasPrevPage($store), |
||||
|
} |
||||
|
}) |
||||
|
|
||||
|
// 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 |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Fetches a fresh set of data from the server, resetting pagination |
||||
|
*/ |
||||
|
async getInitialData() { |
||||
|
const { datasource, filter } = this.options |
||||
|
const tableId = datasource?.tableId |
||||
|
|
||||
|
// Ensure table ID exists
|
||||
|
if (!tableId) { |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
// Ensure schema exists and enrich it
|
||||
|
let schema = this.options.schema |
||||
|
if (!schema) { |
||||
|
schema = await this.constructor.getSchema(datasource) |
||||
|
} |
||||
|
if (!schema) { |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
// Build the lucene query
|
||||
|
let query = this.options.query |
||||
|
if (!query) { |
||||
|
query = buildLuceneQuery(filter) |
||||
|
} |
||||
|
|
||||
|
// Update store
|
||||
|
this.store.update($store => ({ ...$store, schema, query, loading: true })) |
||||
|
|
||||
|
// Actually fetch data
|
||||
|
const page = await this.getData() |
||||
|
this.store.update($store => ({ |
||||
|
...$store, |
||||
|
loading: false, |
||||
|
loaded: true, |
||||
|
pageNumber: 0, |
||||
|
rows: page.rows, |
||||
|
cursors: page.hasNextPage ? [null, page.cursor] : [null], |
||||
|
})) |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Fetches a single page of data from the remote resource |
||||
|
*/ |
||||
|
async getData() { |
||||
|
const { datasource, limit, sortColumn, sortOrder, paginate } = this.options |
||||
|
const { tableId } = datasource |
||||
|
const { cursor, schema, query } = get(this.store) |
||||
|
|
||||
|
// Work out what sort type to use
|
||||
|
const type = schema?.[sortColumn]?.type |
||||
|
const sortType = type === "number" ? "number" : "string" |
||||
|
|
||||
|
// Search table
|
||||
|
const res = await API.searchTable({ |
||||
|
tableId, |
||||
|
query, |
||||
|
limit, |
||||
|
sort: sortColumn, |
||||
|
sortOrder: sortOrder?.toLowerCase() ?? "ascending", |
||||
|
sortType, |
||||
|
paginate, |
||||
|
bookmark: cursor, |
||||
|
}) |
||||
|
return { |
||||
|
rows: res?.rows || [], |
||||
|
hasNextPage: res?.hasNextPage || false, |
||||
|
cursor: res?.bookmark || null, |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Gets the schema definition for a table |
||||
|
* @param datasource the datasource definition |
||||
|
* @return {object} the schema |
||||
|
*/ |
||||
|
static async getSchema(datasource) { |
||||
|
if (!datasource?.tableId) { |
||||
|
return null |
||||
|
} |
||||
|
const table = await fetchTableDefinition(datasource.tableId) |
||||
|
return this.enrichSchema(table?.schema) |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Enriches the schema and ensures that entries are objects with names |
||||
|
* @param schema the datasource schema |
||||
|
* @return {object} the enriched datasource schema |
||||
|
*/ |
||||
|
static enrichSchema(schema) { |
||||
|
if (schema == null) { |
||||
|
return null |
||||
|
} |
||||
|
let enrichedSchema = {} |
||||
|
Object.entries(schema).forEach(([fieldName, fieldSchema]) => { |
||||
|
if (typeof fieldSchema === "string") { |
||||
|
enrichedSchema[fieldName] = { |
||||
|
type: fieldSchema, |
||||
|
name: fieldName, |
||||
|
} |
||||
|
} else { |
||||
|
enrichedSchema[fieldName] = { |
||||
|
...fieldSchema, |
||||
|
name: fieldName, |
||||
|
} |
||||
|
} |
||||
|
}) |
||||
|
return enrichedSchema |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Resets the data set and updates options |
||||
|
* @param newOptions any new options |
||||
|
*/ |
||||
|
async update(newOptions) { |
||||
|
// Check if any settings have actually changed
|
||||
|
let refresh = false |
||||
|
const entries = Object.entries(newOptions || {}) |
||||
|
for (let [key, value] of entries) { |
||||
|
if (JSON.stringify(value) !== JSON.stringify(this.options[key])) { |
||||
|
refresh = true |
||||
|
break |
||||
|
} |
||||
|
} |
||||
|
if (!refresh) { |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
// Assign new options and reload data
|
||||
|
this.options = { |
||||
|
...this.options, |
||||
|
...newOptions, |
||||
|
} |
||||
|
await this.getInitialData() |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Loads the same page again |
||||
|
*/ |
||||
|
async refresh() { |
||||
|
if (get(this.store).loading) { |
||||
|
return |
||||
|
} |
||||
|
const { rows } = await this.getData() |
||||
|
this.store.update($store => ({ ...$store, loading: true })) |
||||
|
this.store.update($store => ({ ...$store, rows, loading: false })) |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 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 |
||||
|
*/ |
||||
|
hasNextPage(state) { |
||||
|
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 |
||||
|
*/ |
||||
|
hasPrevPage(state) { |
||||
|
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, |
||||
|
})) |
||||
|
const { rows, hasNextPage, cursor } = await this.getData() |
||||
|
|
||||
|
// Update state
|
||||
|
this.store.update($store => { |
||||
|
let { cursors, pageNumber } = $store |
||||
|
if (hasNextPage) { |
||||
|
cursors[pageNumber + 2] = cursor |
||||
|
} |
||||
|
return { |
||||
|
...$store, |
||||
|
pageNumber: pageNumber + 1, |
||||
|
rows, |
||||
|
cursors, |
||||
|
loading: false, |
||||
|
} |
||||
|
}) |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 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, |
||||
|
})) |
||||
|
const { rows } = await this.getData() |
||||
|
|
||||
|
// Update state
|
||||
|
this.store.update($store => { |
||||
|
return { |
||||
|
...$store, |
||||
|
pageNumber: $store.pageNumber - 1, |
||||
|
rows, |
||||
|
loading: false, |
||||
|
} |
||||
|
}) |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,34 @@ |
|||||
|
import TableFetch from "./TableFetch.js" |
||||
|
import { fetchTableDefinition, fetchViewData } from "api" |
||||
|
|
||||
|
export default class ViewFetch extends TableFetch { |
||||
|
SupportsSearch = false |
||||
|
SupportsSort = false |
||||
|
SupportsPagination = false |
||||
|
|
||||
|
/** |
||||
|
* Fetches the schema for a view |
||||
|
* @param datasource the view datasource config |
||||
|
* @return {object} the view schema |
||||
|
*/ |
||||
|
static async getSchema(datasource) { |
||||
|
if (!datasource?.tableId) { |
||||
|
return null |
||||
|
} |
||||
|
const table = await fetchTableDefinition(datasource.tableId) |
||||
|
return this.enrichSchema(table?.views?.[datasource.name]?.schema) |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Fetches a single page of data from the remote resource |
||||
|
*/ |
||||
|
async getData() { |
||||
|
const { datasource } = this.options |
||||
|
const res = await fetchViewData(datasource) |
||||
|
return { |
||||
|
rows: res || [], |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
ViewFetch.getSchema() |
||||
@ -0,0 +1,16 @@ |
|||||
|
import TableFetch from "./TableFetch.js" |
||||
|
import ViewFetch from "./ViewFetch.js" |
||||
|
import QueryFetch from "./QueryFetch.js" |
||||
|
import RelationshipFetch from "./RelationshipFetch.js" |
||||
|
|
||||
|
const DataFetchMap = { |
||||
|
table: TableFetch, |
||||
|
view: ViewFetch, |
||||
|
query: QueryFetch, |
||||
|
link: RelationshipFetch, |
||||
|
} |
||||
|
|
||||
|
export const fetchData = (datasource, options) => { |
||||
|
const Fetch = DataFetchMap[datasource?.type] || TableFetch |
||||
|
return new Fetch({ datasource, ...options }) |
||||
|
} |
||||
@ -0,0 +1,49 @@ |
|||||
|
import TableFetch from "./fetch/TableFetch.js" |
||||
|
import ViewFetch from "./fetch/ViewFetch.js" |
||||
|
import QueryFetch from "./fetch/QueryFetch.js" |
||||
|
|
||||
|
/** |
||||
|
* Fetches the schema of any kind of datasource. |
||||
|
*/ |
||||
|
export const fetchDatasourceSchema = async datasource => { |
||||
|
const type = datasource?.type |
||||
|
|
||||
|
// Nested providers should already have exposed their own schema
|
||||
|
if (type === "provider") { |
||||
|
return datasource.value?.schema |
||||
|
} |
||||
|
|
||||
|
// Field sources have their schema statically defined
|
||||
|
if (type === "field") { |
||||
|
if (datasource.fieldType === "attachment") { |
||||
|
return { |
||||
|
url: { |
||||
|
type: "string", |
||||
|
}, |
||||
|
name: { |
||||
|
type: "string", |
||||
|
}, |
||||
|
} |
||||
|
} else if (datasource.fieldType === "array") { |
||||
|
return { |
||||
|
value: { |
||||
|
type: "string", |
||||
|
}, |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// All normal datasource schema can use their corresponsing implementations
|
||||
|
// in the data fetch classes
|
||||
|
if (type === "table" || type === "link") { |
||||
|
return TableFetch.getSchema(datasource) |
||||
|
} |
||||
|
if (type === "view") { |
||||
|
return ViewFetch.getSchema(datasource) |
||||
|
} |
||||
|
if (type === "query") { |
||||
|
return QueryFetch.getSchema(datasource) |
||||
|
} |
||||
|
|
||||
|
return null |
||||
|
} |
||||
Loading…
Reference in new issue