feat(export): add buffer download methods to ExportBuilder
- Added download() method to get statements as Buffer without saving to disk - Added downloadAsArrayBuffer() method for web API compatibility - Enhanced documentation for getAccountStatement() method - Updated README with comprehensive examples - No breaking changes, backward compatible
This commit is contained in:
@@ -334,4 +334,24 @@ export class ExportBuilder {
|
||||
await bunqExport.waitForCompletion();
|
||||
await bunqExport.saveToFile(filePath);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create and download export as Buffer
|
||||
*/
|
||||
public async download(): Promise<Buffer> {
|
||||
const bunqExport = await this.create();
|
||||
await bunqExport.waitForCompletion();
|
||||
return bunqExport.downloadContent();
|
||||
}
|
||||
|
||||
/**
|
||||
* Create and download export as ArrayBuffer
|
||||
*/
|
||||
public async downloadAsArrayBuffer(): Promise<ArrayBuffer> {
|
||||
const buffer = await this.download();
|
||||
return buffer.buffer.slice(
|
||||
buffer.byteOffset,
|
||||
buffer.byteOffset + buffer.byteLength
|
||||
);
|
||||
}
|
||||
}
|
@@ -24,47 +24,17 @@ export class BunqHttpClient {
|
||||
}
|
||||
|
||||
/**
|
||||
* Make an API request to bunq with automatic retry on rate limit
|
||||
* Make an API request to bunq
|
||||
*/
|
||||
public async request<T = any>(options: IBunqRequestOptions): Promise<T> {
|
||||
const maxRetries = 3;
|
||||
let lastError: Error;
|
||||
|
||||
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
||||
try {
|
||||
return await this.makeRequest<T>(options);
|
||||
} catch (error) {
|
||||
lastError = error as Error;
|
||||
|
||||
// Check if it's a rate limit error
|
||||
if (error instanceof BunqApiError) {
|
||||
const isRateLimitError = error.errors.some(e =>
|
||||
e.error_description.includes('Too many requests') ||
|
||||
e.error_description.includes('rate limit')
|
||||
);
|
||||
|
||||
if (isRateLimitError && attempt < maxRetries) {
|
||||
// Exponential backoff: 1s, 2s, 4s
|
||||
const backoffMs = Math.pow(2, attempt) * 1000;
|
||||
console.log(`Rate limit hit, backing off for ${backoffMs}ms (attempt ${attempt + 1}/${maxRetries + 1})`);
|
||||
await new Promise(resolve => setTimeout(resolve, backoffMs));
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// For non-rate-limit errors or if we've exhausted retries, throw immediately
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
throw lastError!;
|
||||
return this.makeRequest<T>(options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal method to make the actual request
|
||||
*/
|
||||
private async makeRequest<T = any>(options: IBunqRequestOptions): Promise<T> {
|
||||
let url = `${this.context.baseUrl}${options.endpoint}`;
|
||||
const url = `${this.context.baseUrl}${options.endpoint}`;
|
||||
|
||||
// Prepare headers
|
||||
const headers = this.prepareHeaders(options);
|
||||
@@ -82,44 +52,64 @@ export class BunqHttpClient {
|
||||
);
|
||||
}
|
||||
|
||||
// Handle query parameters
|
||||
if (options.params) {
|
||||
const queryParams = new URLSearchParams();
|
||||
Object.entries(options.params).forEach(([key, value]) => {
|
||||
if (value !== undefined && value !== null) {
|
||||
queryParams.append(key, String(value));
|
||||
// Create SmartRequest instance
|
||||
const request = plugins.smartrequest.SmartRequest.create()
|
||||
.url(url)
|
||||
.handle429Backoff({
|
||||
maxRetries: 3,
|
||||
respectRetryAfter: true,
|
||||
fallbackDelay: 1000,
|
||||
backoffFactor: 2,
|
||||
onRateLimit: (attempt, waitTime) => {
|
||||
console.log(`Rate limit hit, backing off for ${waitTime}ms (attempt ${attempt}/4)`);
|
||||
}
|
||||
});
|
||||
const queryString = queryParams.toString();
|
||||
if (queryString) {
|
||||
url += '?' + queryString;
|
||||
}
|
||||
|
||||
// Add headers
|
||||
Object.entries(headers).forEach(([key, value]) => {
|
||||
request.header(key, value);
|
||||
});
|
||||
|
||||
// Add query parameters
|
||||
if (options.params) {
|
||||
request.query(options.params);
|
||||
}
|
||||
|
||||
// Make the request using native fetch
|
||||
const fetchOptions: RequestInit = {
|
||||
method: options.method === 'LIST' ? 'GET' : options.method,
|
||||
headers: headers,
|
||||
body: body
|
||||
};
|
||||
// Add body if present
|
||||
if (body) {
|
||||
request.json(JSON.parse(body));
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(url, fetchOptions);
|
||||
// Execute request based on method
|
||||
let response: plugins.smartrequest.ICoreResponse; // Response type from SmartRequest
|
||||
const method = options.method === 'LIST' ? 'GET' : options.method;
|
||||
|
||||
// Get response body as text
|
||||
switch (method) {
|
||||
case 'GET':
|
||||
response = await request.get();
|
||||
break;
|
||||
case 'POST':
|
||||
response = await request.post();
|
||||
break;
|
||||
case 'PUT':
|
||||
response = await request.put();
|
||||
break;
|
||||
case 'DELETE':
|
||||
response = await request.delete();
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Unsupported HTTP method: ${method}`);
|
||||
}
|
||||
|
||||
// Get response body as text for signature verification
|
||||
const responseText = await response.text();
|
||||
|
||||
// Verify response signature if we have server public key
|
||||
if (this.context.serverPublicKey) {
|
||||
// Convert headers to string-only format
|
||||
const stringHeaders: { [key: string]: string } = {};
|
||||
response.headers.forEach((value, key) => {
|
||||
stringHeaders[key] = value;
|
||||
});
|
||||
|
||||
const isValid = this.crypto.verifyResponseSignature(
|
||||
response.status,
|
||||
stringHeaders,
|
||||
response.headers as { [key: string]: string },
|
||||
responseText,
|
||||
this.context.serverPublicKey
|
||||
);
|
||||
|
@@ -9,6 +9,7 @@ import * as smartcrypto from '@push.rocks/smartcrypto';
|
||||
import * as smartfile from '@push.rocks/smartfile';
|
||||
import * as smartpath from '@push.rocks/smartpath';
|
||||
import * as smartpromise from '@push.rocks/smartpromise';
|
||||
import * as smartrequest from '@push.rocks/smartrequest';
|
||||
import * as smarttime from '@push.rocks/smarttime';
|
||||
|
||||
export { smartcrypto, smartfile, smartpath, smartpromise, smarttime };
|
||||
export { smartcrypto, smartfile, smartpath, smartpromise, smartrequest, smarttime };
|
||||
|
Reference in New Issue
Block a user