feat(docs): Enhance documentation and tests with modern API usage examples and migration guide
This commit is contained in:
@ -3,6 +3,6 @@
|
||||
*/
|
||||
export const commitinfo = {
|
||||
name: '@push.rocks/smartrequest',
|
||||
version: '2.0.23',
|
||||
version: '2.1.0',
|
||||
description: 'A module for modern HTTP/HTTPS requests with support for form data, file uploads, JSON, binary data, streams, and more.'
|
||||
}
|
||||
|
@ -11,13 +11,13 @@ export function createPaginatedResponse<T>(
|
||||
fetchNextPage: (params: Record<string, string>) => Promise<TPaginatedResponse<T>>
|
||||
): TPaginatedResponse<T> {
|
||||
// Default to response.body for items if response is JSON
|
||||
let items: T[] = Array.isArray(response.body)
|
||||
? response.body
|
||||
let items: T[] = Array.isArray(response.body)
|
||||
? response.body
|
||||
: (response.body?.items || response.body?.data || response.body?.results || []);
|
||||
|
||||
|
||||
let hasNextPage = false;
|
||||
let nextPageParams: Record<string, string> = {};
|
||||
|
||||
|
||||
// Determine if there's a next page based on pagination strategy
|
||||
switch (paginationConfig.strategy) {
|
||||
case PaginationStrategy.OFFSET: {
|
||||
@ -25,9 +25,9 @@ export function createPaginatedResponse<T>(
|
||||
const currentPage = parseInt(queryParams[config.pageParam || 'page'] || String(config.startPage || 1));
|
||||
const limit = parseInt(queryParams[config.limitParam || 'limit'] || String(config.pageSize || 20));
|
||||
const total = getValueByPath(response.body, config.totalPath || 'total') || 0;
|
||||
|
||||
|
||||
hasNextPage = currentPage * limit < total;
|
||||
|
||||
|
||||
if (hasNextPage) {
|
||||
nextPageParams = {
|
||||
...queryParams,
|
||||
@ -36,14 +36,14 @@ export function createPaginatedResponse<T>(
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
case PaginationStrategy.CURSOR: {
|
||||
const config = paginationConfig;
|
||||
const nextCursor = getValueByPath(response.body, config.cursorPath || 'nextCursor');
|
||||
const hasMore = getValueByPath(response.body, config.hasMorePath || 'hasMore');
|
||||
|
||||
|
||||
hasNextPage = !!nextCursor || !!hasMore;
|
||||
|
||||
|
||||
if (hasNextPage && nextCursor) {
|
||||
nextPageParams = {
|
||||
...queryParams,
|
||||
@ -52,50 +52,52 @@ export function createPaginatedResponse<T>(
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
case PaginationStrategy.LINK_HEADER: {
|
||||
const linkHeader = response.headers['link'] || '';
|
||||
const links = parseLinkHeader(linkHeader);
|
||||
|
||||
// Handle both string and string[] types for the link header
|
||||
const headerValue = Array.isArray(linkHeader) ? linkHeader[0] : linkHeader;
|
||||
const links = parseLinkHeader(headerValue);
|
||||
|
||||
hasNextPage = !!links.next;
|
||||
|
||||
|
||||
if (hasNextPage && links.next) {
|
||||
// Extract query parameters from next link URL
|
||||
const url = new URL(links.next);
|
||||
nextPageParams = {};
|
||||
|
||||
|
||||
url.searchParams.forEach((value, key) => {
|
||||
nextPageParams[key] = value;
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
case PaginationStrategy.CUSTOM: {
|
||||
const config = paginationConfig;
|
||||
hasNextPage = config.hasNextPage(response);
|
||||
|
||||
|
||||
if (hasNextPage) {
|
||||
nextPageParams = config.getNextPageParams(response, queryParams);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Create a function to fetch the next page
|
||||
const getNextPage = async (): Promise<TPaginatedResponse<T>> => {
|
||||
if (!hasNextPage) {
|
||||
throw new Error('No more pages available');
|
||||
}
|
||||
|
||||
|
||||
return fetchNextPage(nextPageParams);
|
||||
};
|
||||
|
||||
|
||||
// Create a function to fetch all remaining pages
|
||||
const getAllPages = async (): Promise<T[]> => {
|
||||
const allItems = [...items];
|
||||
let currentPage: TPaginatedResponse<T> = { items, hasNextPage, getNextPage, getAllPages, response };
|
||||
|
||||
|
||||
while (currentPage.hasNextPage) {
|
||||
try {
|
||||
currentPage = await currentPage.getNextPage();
|
||||
@ -104,10 +106,10 @@ export function createPaginatedResponse<T>(
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return allItems;
|
||||
};
|
||||
|
||||
|
||||
return {
|
||||
items,
|
||||
hasNextPage,
|
||||
@ -123,27 +125,27 @@ export function createPaginatedResponse<T>(
|
||||
*/
|
||||
export function parseLinkHeader(header: string): Record<string, string> {
|
||||
const links: Record<string, string> = {};
|
||||
|
||||
|
||||
if (!header) {
|
||||
return links;
|
||||
}
|
||||
|
||||
|
||||
// Split parts by comma
|
||||
const parts = header.split(',');
|
||||
|
||||
|
||||
// Parse each part into a name:value pair
|
||||
for (const part of parts) {
|
||||
const section = part.split(';');
|
||||
if (section.length < 2) {
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
const url = section[0].replace(/<(.*)>/, '$1').trim();
|
||||
const name = section[1].replace(/rel="(.*)"/, '$1').trim();
|
||||
|
||||
|
||||
links[name] = url;
|
||||
}
|
||||
|
||||
|
||||
return links;
|
||||
}
|
||||
|
||||
@ -155,16 +157,16 @@ export function getValueByPath(obj: any, path?: string): any {
|
||||
if (!path || !obj) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
|
||||
const keys = path.split('.');
|
||||
let current = obj;
|
||||
|
||||
|
||||
for (const key of keys) {
|
||||
if (current === null || current === undefined || typeof current !== 'object') {
|
||||
return undefined;
|
||||
}
|
||||
current = current[key];
|
||||
}
|
||||
|
||||
|
||||
return current;
|
||||
}
|
@ -2,14 +2,14 @@ import { type ISmartRequestOptions } from '../legacy/smartrequest.interfaces.js'
|
||||
import { request, type IExtendedIncomingMessage } from '../legacy/smartrequest.request.js';
|
||||
import * as plugins from '../legacy/smartrequest.plugins.js';
|
||||
|
||||
import type { HttpMethod, ResponseType, RetryConfig, TimeoutConfig, FormField } from './types/common.js';
|
||||
import type {
|
||||
TPaginationConfig,
|
||||
PaginationStrategy,
|
||||
OffsetPaginationConfig,
|
||||
CursorPaginationConfig,
|
||||
CustomPaginationConfig,
|
||||
TPaginatedResponse
|
||||
import type { HttpMethod, ResponseType, FormField } from './types/common.js';
|
||||
import {
|
||||
type TPaginationConfig,
|
||||
PaginationStrategy,
|
||||
type OffsetPaginationConfig,
|
||||
type CursorPaginationConfig,
|
||||
type CustomPaginationConfig,
|
||||
type TPaginatedResponse
|
||||
} from './types/pagination.js';
|
||||
import { createPaginatedResponse } from './features/pagination.js';
|
||||
|
||||
@ -65,7 +65,7 @@ export class SmartRequestClient<T = any> {
|
||||
*/
|
||||
formData(data: FormField[]): this {
|
||||
const form = new plugins.formData();
|
||||
|
||||
|
||||
for (const item of data) {
|
||||
if (Buffer.isBuffer(item.value)) {
|
||||
form.append(item.name, item.value, {
|
||||
@ -76,16 +76,16 @@ export class SmartRequestClient<T = any> {
|
||||
form.append(item.name, item.value);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (!this._options.headers) {
|
||||
this._options.headers = {};
|
||||
}
|
||||
|
||||
|
||||
this._options.headers = {
|
||||
...this._options.headers,
|
||||
...form.getHeaders()
|
||||
};
|
||||
|
||||
|
||||
this._options.requestBody = form;
|
||||
return this;
|
||||
}
|
||||
@ -149,11 +149,11 @@ export class SmartRequestClient<T = any> {
|
||||
*/
|
||||
responseType(type: ResponseType): this {
|
||||
this._responseType = type;
|
||||
|
||||
|
||||
if (type === 'binary' || type === 'stream') {
|
||||
this._options.autoJsonParse = false;
|
||||
}
|
||||
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
@ -177,13 +177,13 @@ export class SmartRequestClient<T = any> {
|
||||
pageSize: config.pageSize || 20,
|
||||
totalPath: config.totalPath || 'total'
|
||||
};
|
||||
|
||||
|
||||
// Add initial pagination parameters
|
||||
this.query({
|
||||
[this._paginationConfig.pageParam]: String(this._paginationConfig.startPage),
|
||||
[this._paginationConfig.limitParam]: String(this._paginationConfig.pageSize)
|
||||
});
|
||||
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
@ -264,24 +264,24 @@ export class SmartRequestClient<T = any> {
|
||||
if (!this._paginationConfig) {
|
||||
throw new Error('Pagination not configured. Call one of the pagination methods first.');
|
||||
}
|
||||
|
||||
|
||||
// Default to GET if no method specified
|
||||
if (!this._options.method) {
|
||||
this._options.method = 'GET';
|
||||
}
|
||||
|
||||
|
||||
const response = await this.execute();
|
||||
|
||||
|
||||
return createPaginatedResponse<ItemType>(
|
||||
response,
|
||||
this._paginationConfig,
|
||||
response,
|
||||
this._paginationConfig,
|
||||
this._queryParams,
|
||||
(nextPageParams) => {
|
||||
// Create a new client with the same configuration but updated query params
|
||||
const nextClient = new SmartRequestClient<ItemType>();
|
||||
Object.assign(nextClient, this);
|
||||
nextClient._queryParams = nextPageParams;
|
||||
|
||||
|
||||
return nextClient.getPaginated<ItemType>();
|
||||
}
|
||||
);
|
||||
@ -304,28 +304,28 @@ export class SmartRequestClient<T = any> {
|
||||
}
|
||||
|
||||
this._options.queryParams = this._queryParams;
|
||||
|
||||
|
||||
// Handle retry logic
|
||||
let lastError: Error;
|
||||
|
||||
|
||||
for (let attempt = 0; attempt <= this._retries; attempt++) {
|
||||
try {
|
||||
if (this._responseType === 'stream') {
|
||||
return await request(this._url, this._options, true) as IExtendedIncomingMessage<R>;
|
||||
} else if (this._responseType === 'binary') {
|
||||
const response = await request(this._url, this._options, true);
|
||||
|
||||
|
||||
// Handle binary response
|
||||
const dataPromise = plugins.smartpromise.defer<Buffer>();
|
||||
const chunks: Buffer[] = [];
|
||||
|
||||
|
||||
response.on('data', (chunk: Buffer) => chunks.push(chunk));
|
||||
response.on('end', () => {
|
||||
const buffer = Buffer.concat(chunks);
|
||||
(response as IExtendedIncomingMessage<R>).body = buffer as any;
|
||||
dataPromise.resolve();
|
||||
});
|
||||
|
||||
|
||||
await dataPromise.promise;
|
||||
return response as IExtendedIncomingMessage<R>;
|
||||
} else {
|
||||
@ -334,17 +334,17 @@ export class SmartRequestClient<T = any> {
|
||||
}
|
||||
} catch (error) {
|
||||
lastError = error as Error;
|
||||
|
||||
|
||||
// If this is the last attempt, throw the error
|
||||
if (attempt === this._retries) {
|
||||
throw lastError;
|
||||
}
|
||||
|
||||
|
||||
// Otherwise, wait before retrying
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// This should never be reached due to the throw in the loop above
|
||||
throw lastError;
|
||||
}
|
||||
|
Reference in New Issue
Block a user