Skip to content
New issue

Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? # to your account

Add nango.paginate helper for sync/action scripts #1103

Merged
merged 53 commits into from
Oct 20, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
53 commits
Select commit Hold shift + click to select a range
feaa5e9
Add hardcoded offset-limit pagination helper
omotnyk Oct 9, 2023
99f1792
Use generics in `paginate` helper
omotnyk Oct 9, 2023
800c78c
Move pagination from proxy to sync
omotnyk Oct 9, 2023
f7598c1
Add pagination based on `paginate` param
omotnyk Oct 9, 2023
2500967
Add GitHub pagination config to `providers.yaml`
omotnyk Oct 9, 2023
4445f2a
Simplify ifs
omotnyk Oct 9, 2023
c0b9eaf
Implement separate method for pagination helper
omotnyk Oct 9, 2023
63dc50d
Support cursor based pagination
omotnyk Oct 10, 2023
e22a6a6
Refactor pagination interface
omotnyk Oct 10, 2023
23cc704
Revert generator return types
omotnyk Oct 10, 2023
3733549
Prohibit passing boolean param for `paginate`
omotnyk Oct 10, 2023
daae1fe
Specify default limit for Slack and HubSpot
omotnyk Oct 10, 2023
a06baf6
Return batch from generator
omotnyk Oct 10, 2023
adb682a
Support passing pagination params in body
omotnyk Oct 10, 2023
8d1ef8c
Extract body/params update into helper method
omotnyk Oct 10, 2023
9cf10aa
Fix limit
omotnyk Oct 10, 2023
198653c
Allow specifying page parameter name
omotnyk Oct 10, 2023
4d93b69
Simplify cursor type names
omotnyk Oct 11, 2023
9a3cbd1
Fix config names & allow custom limit param name
omotnyk Oct 11, 2023
6c8656a
Add support for header link rel pagination
omotnyk Oct 11, 2023
7787ed5
Remove 'page' pagination support
omotnyk Oct 11, 2023
cbf9b9a
Do not support `limit` parameter
omotnyk Oct 11, 2023
a4593ab
Support gettign next URL from reponse body
omotnyk Oct 11, 2023
d534f90
Add missing 'parse-link-header' dependency
omotnyk Oct 11, 2023
6f993f6
Rename params for clarity
omotnyk Oct 11, 2023
969e487
Support offset(by count) pagination
omotnyk Oct 11, 2023
91ee000
Backfill pagination params in `providers.yaml`
omotnyk Oct 11, 2023
5e8ef30
Add error logging in case next URL is invalid
omotnyk Oct 11, 2023
cc01c72
Unify pagination interfaces
omotnyk Oct 12, 2023
0bbf118
Extact next URL extration into separate method
omotnyk Oct 12, 2023
2722926
Ensure override type safety on compile stage
omotnyk Oct 12, 2023
45920e3
Migrate GitHub integration templates
omotnyk Oct 12, 2023
71434e3
Migrate Slack integration templates
omotnyk Oct 12, 2023
394eedb
Migrate Confluence integration templates
omotnyk Oct 12, 2023
6fc0a9e
Support offset pagination
omotnyk Oct 12, 2023
d42d8cb
Convert pagination type to string
omotnyk Oct 12, 2023
299bb04
Migrate Jira integration template
omotnyk Oct 12, 2023
3e9fecc
Revert template updates
omotnyk Oct 12, 2023
bec4745
Merge branch 'master' of github.com:NangoHQ/nango into pagination-helper
omotnyk Oct 12, 2023
ffde95d
Revert accidental changes
omotnyk Oct 12, 2023
bab9e62
Update the interface for link pagination
omotnyk Oct 13, 2023
3336fc9
Cover most of pagination with unit tests
omotnyk Oct 13, 2023
05d4b2d
Update pagination interface
omotnyk Oct 13, 2023
220c233
Slightly optimize offset pagination
omotnyk Oct 13, 2023
aff3f00
Replace mentions of `url` with `link`
omotnyk Oct 13, 2023
5129e1b
Use lodash
omotnyk Oct 13, 2023
216c062
bring in latest
khaliqgant Oct 19, 2023
29f3df4
formatting
khaliqgant Oct 19, 2023
c691bda
Merge branch 'master' of github.com:NangoHQ/nango into pagination-helper
khaliqgant Oct 19, 2023
ec7c58b
initial minor clean up
khaliqgant Oct 19, 2023
8307e09
remove formatting update
khaliqgant Oct 19, 2023
b77b030
organizational refactor
khaliqgant Oct 19, 2023
e02e11d
add notion and add validation
khaliqgant Oct 19, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
649 changes: 236 additions & 413 deletions package-lock.json

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions packages/shared/lib/models/Provider.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import type { CursorPagination, LinkPagination, OffsetPagination } from '../sdk/sync.js';
import type { AuthModes } from './Auth.js';
import type { TimestampsAndDeleted } from './Generic.js';
import type { Sync, Action } from './Sync.js';
Expand Down Expand Up @@ -28,6 +29,7 @@ export interface Template {
after?: string;
};
decompress?: boolean;
paginate?: LinkPagination | CursorPagination | OffsetPagination;
};
authorization_url: string;
authorization_params?: Record<string, string>;
Expand Down
86 changes: 84 additions & 2 deletions packages/shared/lib/sdk/sync.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import errorManager, { ErrorSourceEnum } from '../utils/error.manager.js';
import { LogActionEnum } from '../models/Activity.js';

import { Nango } from '@nangohq/node';
import configService from '../services/config.service.js';
import paginateService from '../services/paginate.service.js';

type LogLevel = 'info' | 'debug' | 'error' | 'warn' | 'http' | 'verbose' | 'silly';

Expand Down Expand Up @@ -45,7 +47,7 @@ interface ParamsSerializerOptions extends SerializerOptions {
serialize?: CustomParamsSerializer;
}

interface AxiosResponse<T = any, D = any> {
export interface AxiosResponse<T = any, D = any> {
data: T;
status: number;
statusText: string;
Expand All @@ -59,7 +61,34 @@ interface DataResponse {
[index: string]: unknown | undefined | string | number | boolean | Record<string, string | boolean | number | unknown>;
}

interface ProxyConfiguration {
export enum PaginationType {
CURSOR = 'cursor',
LINK = 'link',
OFFSET = 'offset'
}

export interface Pagination {
type: string;
limit?: number;
response_path?: string;
limit_name_in_request: string;
}

export interface CursorPagination extends Pagination {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The tricky thing to understand here IMO is that one is for the response, the other for the request.

Just a suggestion, but we could have:

  • cursor_path_in_response
  • cursor_name_in_request

I would also imply the next_ here.

In any case, I would brainstorm about the best names here because they will likely stay a while (unless we break the interface).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In my current scheme, I use path for something that should be fetched from the response and name for something that is set on the request later on. I agree that in_request/in_response are a bit more explicit about the way these parameters are going to be used so I like this approach.

I would also imply the next_ here.

👍

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@khaliqgant curious to have your opinion on naming!

cursor_path_in_response: string;
cursor_name_in_request: string;
}

export interface LinkPagination extends Pagination {
link_rel_in_response_header?: string;
link_path_in_response_body?: string;
}

export interface OffsetPagination extends Pagination {
offset_name_in_request: string;
}

export interface ProxyConfiguration {
endpoint: string;
providerConfigKey?: string;
connectionId?: string;
Expand All @@ -71,6 +100,7 @@ interface ProxyConfiguration {
data?: unknown;
retries?: number;
baseUrlOverride?: string;
paginate?: Partial<CursorPagination> | Partial<LinkPagination> | Partial<OffsetPagination>;
}

enum AuthModes {
Expand Down Expand Up @@ -307,6 +337,58 @@ export class NangoAction {

return this.attributes as A;
}

public async *paginate<T = any>(config: ProxyConfiguration): AsyncGenerator<T[], undefined, void> {
const providerConfigKey: string = this.providerConfigKey as string;
const template = configService.getTemplate(providerConfigKey);
const templatePaginationConfig: Pagination | undefined = template.proxy?.paginate;

if (!templatePaginationConfig && (!config.paginate || !config.paginate.type)) {
throw Error('There was no pagination configuration for this integration or configuration passed in.');
}

const paginationConfig: Pagination = {
...(templatePaginationConfig || {}),
...(config.paginate || {})
} as Pagination;

paginateService.validateConfiguration(paginationConfig);

config.method = config.method || 'GET';

const configMethod: string = config.method.toLocaleLowerCase();
const passPaginationParamsInBody: boolean = ['post', 'put', 'patch'].includes(configMethod);

const updatedBodyOrParams: Record<string, any> = ((passPaginationParamsInBody ? config.data : config.params) as Record<string, any>) ?? {};
const limitParameterName: string = paginationConfig.limit_name_in_request;

if (paginationConfig['limit']) {
updatedBodyOrParams[limitParameterName] = paginationConfig['limit'];
}

switch (paginationConfig.type.toLowerCase()) {
case PaginationType.CURSOR:
return yield* paginateService.cursor<T>(
config,
paginationConfig as CursorPagination,
updatedBodyOrParams,
passPaginationParamsInBody,
this.proxy.bind(this)
);
case PaginationType.LINK:
return yield* paginateService.link<T>(config, paginationConfig, updatedBodyOrParams, passPaginationParamsInBody, this.proxy.bind(this));
case PaginationType.OFFSET:
return yield* paginateService.offset<T>(
config,
paginationConfig as OffsetPagination,
updatedBodyOrParams,
passPaginationParamsInBody,
this.proxy.bind(this)
);
default:
throw Error(`'${paginationConfig.type} ' pagination is not supported. Please, make sure it's one of ${Object.values(PaginationType)}`);
}
}
}

export class NangoSync extends NangoAction {
Expand Down
Loading