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:
2025-08-02 10:56:17 +00:00
parent 4c0ad95eb1
commit 40f9142d70
8 changed files with 218 additions and 124 deletions

View File

@@ -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
);
}
}

View File

@@ -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
);

View File

@@ -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 };