Add support for query param based pagination in REST queries

This commit is contained in:
Andrew Kingston 2022-01-05 09:16:10 +00:00
parent 81cbb6d403
commit e2478e478e
9 changed files with 149 additions and 73 deletions

View File

@ -134,7 +134,7 @@
{:else}
<slot />
{/if}
{#if paginate && dataSource?.type === "table"}
{#if paginate && $fetch.supportsPagination}
<div class="pagination">
<Pagination
page={$fetch.pageNumber + 1}

View File

@ -14,9 +14,11 @@ import { fetchTableDefinition } from "api"
*/
export default class DataFetch {
// Feature flags
supportsSearch = false
supportsSort = false
supportsPagination = false
featureStore = writable({
supportsSearch: false,
supportsSort: false,
supportsPagination: false,
})
// Config
options = {
@ -74,13 +76,17 @@ export default class DataFetch {
this.prevPage = this.prevPage.bind(this)
// Derive certain properties to return
this.derivedStore = derived(this.store, $store => {
this.derivedStore = derived(
[this.store, this.featureStore],
([$store, $featureStore]) => {
return {
...$store,
...$featureStore,
hasNextPage: this.hasNextPage($store),
hasPrevPage: this.hasPrevPage($store),
}
})
}
)
// Mark as loaded if we have no datasource
if (!this.options.datasource) {
@ -114,7 +120,12 @@ export default class DataFetch {
// Fetch datasource definition and determine feature flags
const definition = await this.constructor.getDefinition(datasource)
this.determineFeatureFlags(definition)
const features = this.determineFeatureFlags(definition)
this.featureStore.set({
supportsSearch: !!features?.supportsSearch,
supportsSort: !!features?.supportsSort,
supportsPagination: !!features?.supportsPagination,
})
// Fetch and enrich schema
let schema = this.constructor.getSchema(datasource, definition)
@ -167,22 +178,23 @@ export default class DataFetch {
async getPage() {
const { sortColumn, sortOrder, sortType, limit } = this.options
const { query } = get(this.store)
const features = get(this.featureStore)
// Get the actual data
let { rows, info, hasNextPage, cursor } = await this.getData()
// If we don't support searching, do a client search
if (!this.supportsSearch) {
if (!features.supportsSearch) {
rows = luceneQuery(rows, query)
}
// If we don't support sorting, do a client-side sort
if (!this.supportsSort) {
if (!features.supportsSort) {
rows = luceneSort(rows, sortColumn, sortOrder, sortType)
}
// If we don't support pagination, do a client-side limit
if (!this.supportsPagination) {
if (!features.supportsPagination) {
rows = luceneLimit(rows, limit)
}
@ -263,9 +275,11 @@ export default class DataFetch {
*/
// eslint-disable-next-line no-unused-vars
determineFeatureFlags(definition) {
this.supportsSearch = false
this.supportsSort = false
this.supportsPagination = false
return {
supportsSearch: false,
supportsSort: false,
supportsPagination: false,
}
}
/**

View File

@ -5,10 +5,10 @@ import { get } from "svelte/store"
export default class QueryFetch extends DataFetch {
determineFeatureFlags(definition) {
console.log("pagination config", definition?.fields?.pagination)
this.supportsPagination =
const supportsPagination =
definition?.fields?.pagination?.type != null &&
definition?.fields?.pagination?.pageParam != null
return { supportsPagination }
}
static async getDefinition(datasource) {
@ -20,6 +20,9 @@ export default class QueryFetch extends DataFetch {
async getData() {
const { datasource, limit } = this.options
const { supportsPagination } = get(this.featureStore)
const { cursor, definition } = get(this.store)
const { type } = definition.fields.pagination
// Set the default query params
let parameters = cloneDeep(datasource?.queryParams || {})
@ -31,19 +34,33 @@ export default class QueryFetch extends DataFetch {
// Add pagination to query if supported
let queryPayload = { queryId: datasource?._id, parameters }
if (this.supportsPagination) {
const { cursor, definition, pageNumber } = get(this.store)
const { type } = definition.fields.pagination
const page = type === "page" ? pageNumber : cursor
queryPayload.pagination = { page, limit }
if (supportsPagination) {
const requestCursor = type === "page" ? parseInt(cursor || 0) : cursor
queryPayload.pagination = { page: requestCursor, limit }
}
// Execute query
const { data, pagination, ...rest } = await executeQuery(queryPayload)
// Derive pagination info from response
let nextCursor = null
let hasNextPage = false
if (supportsPagination) {
if (type === "page") {
// For "page number" pagination, increment the existing page number
nextCursor = queryPayload.pagination.page + 1
} else {
// For "cursor" pagination, the cursor should be in the response
nextCursor = pagination.cursor
}
hasNextPage = data?.length === limit && limit > 0
}
return {
rows: data || [],
info: rest,
cursor: pagination?.page,
hasNextPage: data?.length === limit && limit > 0,
cursor: nextCursor,
hasNextPage,
}
}
}

View File

@ -4,9 +4,11 @@ import { searchTable } from "api"
export default class TableFetch extends DataFetch {
determineFeatureFlags() {
this.supportsSearch = true
this.supportsSort = true
this.supportsPagination = true
return {
supportsSearch: true,
supportsSort: true,
supportsPagination: true,
}
}
async getData() {

View File

@ -145,6 +145,7 @@ async function execute(ctx, opts = { rowsOnly: false }) {
queryVerb: query.queryVerb,
fields: query.fields,
parameters: ctx.request.body.parameter,
pagination: ctx.request.body.pagination,
transformer: query.transformer,
queryId: ctx.params.queryId,
})

View File

@ -233,6 +233,7 @@ export interface RestQueryFields {
method: string
authConfigId: string
pagination: PaginationConfig | null
paginationValues: PaginationValues | null
}
export interface RestConfig {
@ -245,11 +246,17 @@ export interface RestConfig {
export interface PaginationConfig {
type: string
location: string
pageParam: string
sizeParam: string | null
responseParam: string | null
}
export interface PaginationValues {
page: string | number | null
limit: number | null
}
export interface Query {
_id?: string
datasourceId: string

View File

@ -4,9 +4,11 @@ import {
QueryTypes,
RestConfig,
RestQueryFields as RestQuery,
PaginationConfig,
AuthType,
BasicAuthConfig,
BearerAuthConfig,
PaginationValues,
} from "../definitions/datasource"
import { IntegrationBase } from "./base/IntegrationBase"
@ -40,6 +42,9 @@ const coreFields = {
type: DatasourceFieldTypes.STRING,
enum: Object.values(BodyTypes),
},
pagination: {
type: DatasourceFieldTypes.OBJECT
}
}
module RestModule {
@ -165,7 +170,29 @@ module RestModule {
}
}
getUrl(path: string, queryString: string): string {
getUrl(path: string, queryString: string, pagination: PaginationConfig | null, paginationValues: PaginationValues | null): string {
// Add pagination params to query string if required
if (pagination?.location === "query" && paginationValues) {
const { pageParam, sizeParam } = pagination
const params = new URLSearchParams()
// Append page number or cursor param if configured
if (pageParam && paginationValues.page != null) {
params.append(pageParam, paginationValues.page)
}
// Append page size param if configured
if (sizeParam && paginationValues.limit != null) {
params.append(sizeParam, paginationValues.limit)
}
// Prepend query string with pagination params
let paginationString = params.toString()
if (paginationString) {
queryString = `${paginationString}&${queryString}`
}
}
const main = `${path}?${queryString}`
let complete = main
if (this.config.url && !main.startsWith(this.config.url)) {
@ -268,6 +295,8 @@ module RestModule {
bodyType,
requestBody,
authConfigId,
pagination,
paginationValues
} = query
const authHeaders = this.getAuthHeaders(authConfigId)
@ -291,7 +320,7 @@ module RestModule {
}
this.startTimeMs = performance.now()
const url = this.getUrl(path, queryString)
const url = this.getUrl(path, queryString, pagination, paginationValues)
const response = await fetch(url, input)
return await this.parseResponse(response)
}

View File

@ -12,6 +12,7 @@ class QueryRunner {
this.queryVerb = input.queryVerb
this.fields = input.fields
this.parameters = input.parameters
this.pagination = input.pagination
this.transformer = input.transformer
this.queryId = input.queryId
this.noRecursiveQuery = flags.noRecursiveQuery
@ -27,7 +28,7 @@ class QueryRunner {
let { datasource, fields, queryVerb, transformer } = this
// pre-query, make sure datasource variables are added to parameters
const parameters = await this.addDatasourceVariables()
const query = threadUtils.enrichQueryFields(fields, parameters)
let query = this.enrichQueryFields(fields, parameters)
const Integration = integrations[datasource.source]
if (!Integration) {
throw "Integration type does not exist."
@ -156,6 +157,52 @@ class QueryRunner {
}
return parameters
}
enrichQueryFields(fields, parameters = {}) {
const enrichedQuery = {}
// enrich the fields with dynamic parameters
for (let key of Object.keys(fields)) {
if (fields[key] == null) {
continue
}
if (typeof fields[key] === "object") {
// enrich nested fields object
enrichedQuery[key] = this.enrichQueryFields(fields[key], parameters)
} else if (typeof fields[key] === "string") {
// enrich string value as normal
enrichedQuery[key] = processStringSync(fields[key], parameters, {
noHelpers: true,
})
} else {
enrichedQuery[key] = fields[key]
}
}
if (
enrichedQuery.json ||
enrichedQuery.customData ||
enrichedQuery.requestBody
) {
try {
enrichedQuery.json = JSON.parse(
enrichedQuery.json ||
enrichedQuery.customData ||
enrichedQuery.requestBody
)
} catch (err) {
// no json found, ignore
}
delete enrichedQuery.customData
}
// Just for REST queries
if (this.pagination) {
enrichedQuery.paginationValues = this.pagination
}
return enrichedQuery
}
}
module.exports = (input, callback) => {

View File

@ -76,44 +76,3 @@ exports.hasExtraData = response => {
response.info != null
)
}
exports.enrichQueryFields = (fields, parameters = {}) => {
const enrichedQuery = {}
// enrich the fields with dynamic parameters
for (let key of Object.keys(fields)) {
if (fields[key] == null) {
continue
}
if (typeof fields[key] === "object") {
// enrich nested fields object
enrichedQuery[key] = this.enrichQueryFields(fields[key], parameters)
} else if (typeof fields[key] === "string") {
// enrich string value as normal
enrichedQuery[key] = processStringSync(fields[key], parameters, {
noHelpers: true,
})
} else {
enrichedQuery[key] = fields[key]
}
}
if (
enrichedQuery.json ||
enrichedQuery.customData ||
enrichedQuery.requestBody
) {
try {
enrichedQuery.json = JSON.parse(
enrichedQuery.json ||
enrichedQuery.customData ||
enrichedQuery.requestBody
)
} catch (err) {
// no json found, ignore
}
delete enrichedQuery.customData
}
return enrichedQuery
}