@foru-ms/sdk 1.1.1 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +286 -11
- package/dist/Client.d.ts +63 -4
- package/dist/Client.js +124 -22
- package/dist/errors.d.ts +52 -0
- package/dist/errors.js +95 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +3 -0
- package/dist/resources/Integrations.d.ts +7 -0
- package/dist/resources/Integrations.js +6 -0
- package/dist/resources/Posts.d.ts +12 -0
- package/dist/resources/Posts.js +44 -0
- package/dist/resources/PrivateMessages.d.ts +4 -0
- package/dist/resources/PrivateMessages.js +6 -0
- package/dist/resources/SSO.d.ts +10 -0
- package/dist/resources/SSO.js +11 -0
- package/dist/resources/Tags.d.ts +8 -0
- package/dist/resources/Tags.js +24 -0
- package/dist/resources/Threads.d.ts +20 -0
- package/dist/resources/Threads.js +85 -0
- package/dist/resources/Users.d.ts +10 -0
- package/dist/resources/Users.js +26 -0
- package/dist/resources/Webhooks.d.ts +69 -0
- package/dist/resources/Webhooks.js +115 -0
- package/dist/response-types.d.ts +105 -0
- package/dist/response-types.js +2 -0
- package/dist/utils.d.ts +80 -0
- package/dist/utils.js +138 -0
- package/examples/README.md +38 -0
- package/examples/authentication.ts +79 -0
- package/examples/error-handling.ts +133 -0
- package/examples/managing-threads.ts +130 -0
- package/examples/pagination.ts +81 -0
- package/examples/webhooks.ts +176 -0
- package/package.json +1 -1
- package/src/Client.ts +165 -25
- package/src/errors.ts +95 -0
- package/src/index.ts +3 -0
- package/src/resources/Integrations.ts +11 -0
- package/src/resources/Posts.ts +56 -0
- package/src/resources/PrivateMessages.ts +10 -0
- package/src/resources/SSO.ts +17 -0
- package/src/resources/Tags.ts +32 -0
- package/src/resources/Threads.ts +109 -0
- package/src/resources/Users.ts +36 -0
- package/src/resources/Webhooks.ts +131 -0
- package/src/response-types.ts +113 -0
- package/src/utils.ts +182 -0
|
@@ -1,9 +1,21 @@
|
|
|
1
1
|
import { ForumClient } from '../Client';
|
|
2
2
|
import { Webhook, WebhookListResponse } from '../types';
|
|
3
|
+
/**
|
|
4
|
+
* Resource for managing webhooks
|
|
5
|
+
*/
|
|
3
6
|
export declare class WebhooksResource {
|
|
4
7
|
private client;
|
|
5
8
|
constructor(client: ForumClient);
|
|
9
|
+
/**
|
|
10
|
+
* List all webhooks
|
|
11
|
+
* @returns Promise resolving to list of webhooks
|
|
12
|
+
*/
|
|
6
13
|
list(): Promise<WebhookListResponse>;
|
|
14
|
+
/**
|
|
15
|
+
* Create a new webhook
|
|
16
|
+
* @param payload - Webhook configuration
|
|
17
|
+
* @returns Promise resolving to created webhook
|
|
18
|
+
*/
|
|
7
19
|
create(payload: {
|
|
8
20
|
name: string;
|
|
9
21
|
url: string;
|
|
@@ -11,9 +23,20 @@ export declare class WebhooksResource {
|
|
|
11
23
|
}): Promise<{
|
|
12
24
|
webhook: Webhook;
|
|
13
25
|
}>;
|
|
26
|
+
/**
|
|
27
|
+
* Get a webhook by ID
|
|
28
|
+
* @param id - Webhook ID
|
|
29
|
+
* @returns Promise resolving to webhook details
|
|
30
|
+
*/
|
|
14
31
|
retrieve(id: string): Promise<{
|
|
15
32
|
webhook: Webhook;
|
|
16
33
|
}>;
|
|
34
|
+
/**
|
|
35
|
+
* Update a webhook
|
|
36
|
+
* @param id - Webhook ID
|
|
37
|
+
* @param payload - Updated webhook data
|
|
38
|
+
* @returns Promise resolving to updated webhook
|
|
39
|
+
*/
|
|
17
40
|
update(id: string, payload: {
|
|
18
41
|
name?: string;
|
|
19
42
|
url?: string;
|
|
@@ -22,9 +45,20 @@ export declare class WebhooksResource {
|
|
|
22
45
|
}): Promise<{
|
|
23
46
|
webhook: Webhook;
|
|
24
47
|
}>;
|
|
48
|
+
/**
|
|
49
|
+
* Delete a webhook
|
|
50
|
+
* @param id - Webhook ID
|
|
51
|
+
* @returns Promise resolving to deletion confirmation
|
|
52
|
+
*/
|
|
25
53
|
delete(id: string): Promise<{
|
|
26
54
|
success: boolean;
|
|
27
55
|
}>;
|
|
56
|
+
/**
|
|
57
|
+
* Get webhook delivery history
|
|
58
|
+
* @param id - Webhook ID
|
|
59
|
+
* @param params - Query parameters
|
|
60
|
+
* @returns Promise resolving to delivery history
|
|
61
|
+
*/
|
|
28
62
|
getDeliveries(id: string, params?: {
|
|
29
63
|
cursor?: string;
|
|
30
64
|
}): Promise<{
|
|
@@ -32,4 +66,39 @@ export declare class WebhooksResource {
|
|
|
32
66
|
total: number;
|
|
33
67
|
nextCursor?: string;
|
|
34
68
|
}>;
|
|
69
|
+
/**
|
|
70
|
+
* Verify webhook signature for security
|
|
71
|
+
* @param payload - Request body (should be the original object, not stringified)
|
|
72
|
+
* @param signature - Signature from X-Webhook-Signature header
|
|
73
|
+
* @param timestamp - Timestamp from X-Webhook-Timestamp header (in milliseconds)
|
|
74
|
+
* @param secret - Webhook secret from creation
|
|
75
|
+
* @param maxAge - Maximum age of webhook in milliseconds (default: 5 minutes)
|
|
76
|
+
* @returns True if signature is valid and timestamp is recent
|
|
77
|
+
* @example
|
|
78
|
+
* ```typescript
|
|
79
|
+
* // In your webhook handler
|
|
80
|
+
* app.post('/webhook', (req, res) => {
|
|
81
|
+
* const signature = req.headers['x-webhook-signature'];
|
|
82
|
+
* const timestamp = req.headers['x-webhook-timestamp'];
|
|
83
|
+
* const event = req.headers['x-webhook-event'];
|
|
84
|
+
*
|
|
85
|
+
* // Verify signature and timestamp
|
|
86
|
+
* const isValid = client.webhooks.verifySignature(
|
|
87
|
+
* req.body,
|
|
88
|
+
* signature,
|
|
89
|
+
* timestamp,
|
|
90
|
+
* 'your_webhook_secret'
|
|
91
|
+
* );
|
|
92
|
+
*
|
|
93
|
+
* if (!isValid) {
|
|
94
|
+
* return res.status(401).send('Invalid signature');
|
|
95
|
+
* }
|
|
96
|
+
*
|
|
97
|
+
* // Process webhook...
|
|
98
|
+
* console.log('Event:', event);
|
|
99
|
+
* res.sendStatus(200);
|
|
100
|
+
* });
|
|
101
|
+
* ```
|
|
102
|
+
*/
|
|
103
|
+
verifySignature(payload: any, signature: string, timestamp: string, secret: string, maxAge?: number): boolean;
|
|
35
104
|
}
|
|
@@ -1,37 +1,71 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.WebhooksResource = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* Resource for managing webhooks
|
|
6
|
+
*/
|
|
4
7
|
class WebhooksResource {
|
|
5
8
|
constructor(client) {
|
|
6
9
|
this.client = client;
|
|
7
10
|
}
|
|
11
|
+
/**
|
|
12
|
+
* List all webhooks
|
|
13
|
+
* @returns Promise resolving to list of webhooks
|
|
14
|
+
*/
|
|
8
15
|
async list() {
|
|
9
16
|
return this.client.request('/webhooks', {
|
|
10
17
|
method: 'GET',
|
|
11
18
|
});
|
|
12
19
|
}
|
|
20
|
+
/**
|
|
21
|
+
* Create a new webhook
|
|
22
|
+
* @param payload - Webhook configuration
|
|
23
|
+
* @returns Promise resolving to created webhook
|
|
24
|
+
*/
|
|
13
25
|
async create(payload) {
|
|
14
26
|
return this.client.request('/webhooks', {
|
|
15
27
|
method: 'POST',
|
|
16
28
|
body: JSON.stringify(payload),
|
|
17
29
|
});
|
|
18
30
|
}
|
|
31
|
+
/**
|
|
32
|
+
* Get a webhook by ID
|
|
33
|
+
* @param id - Webhook ID
|
|
34
|
+
* @returns Promise resolving to webhook details
|
|
35
|
+
*/
|
|
19
36
|
async retrieve(id) {
|
|
20
37
|
return this.client.request(`/webhooks/${id}`, {
|
|
21
38
|
method: 'GET',
|
|
22
39
|
});
|
|
23
40
|
}
|
|
41
|
+
/**
|
|
42
|
+
* Update a webhook
|
|
43
|
+
* @param id - Webhook ID
|
|
44
|
+
* @param payload - Updated webhook data
|
|
45
|
+
* @returns Promise resolving to updated webhook
|
|
46
|
+
*/
|
|
24
47
|
async update(id, payload) {
|
|
25
48
|
return this.client.request(`/webhooks/${id}`, {
|
|
26
49
|
method: 'PATCH',
|
|
27
50
|
body: JSON.stringify(payload),
|
|
28
51
|
});
|
|
29
52
|
}
|
|
53
|
+
/**
|
|
54
|
+
* Delete a webhook
|
|
55
|
+
* @param id - Webhook ID
|
|
56
|
+
* @returns Promise resolving to deletion confirmation
|
|
57
|
+
*/
|
|
30
58
|
async delete(id) {
|
|
31
59
|
return this.client.request(`/webhooks/${id}`, {
|
|
32
60
|
method: 'DELETE',
|
|
33
61
|
});
|
|
34
62
|
}
|
|
63
|
+
/**
|
|
64
|
+
* Get webhook delivery history
|
|
65
|
+
* @param id - Webhook ID
|
|
66
|
+
* @param params - Query parameters
|
|
67
|
+
* @returns Promise resolving to delivery history
|
|
68
|
+
*/
|
|
35
69
|
async getDeliveries(id, params) {
|
|
36
70
|
const searchParams = new URLSearchParams();
|
|
37
71
|
if (params === null || params === void 0 ? void 0 : params.cursor)
|
|
@@ -40,5 +74,86 @@ class WebhooksResource {
|
|
|
40
74
|
method: 'GET',
|
|
41
75
|
});
|
|
42
76
|
}
|
|
77
|
+
/**
|
|
78
|
+
* Verify webhook signature for security
|
|
79
|
+
* @param payload - Request body (should be the original object, not stringified)
|
|
80
|
+
* @param signature - Signature from X-Webhook-Signature header
|
|
81
|
+
* @param timestamp - Timestamp from X-Webhook-Timestamp header (in milliseconds)
|
|
82
|
+
* @param secret - Webhook secret from creation
|
|
83
|
+
* @param maxAge - Maximum age of webhook in milliseconds (default: 5 minutes)
|
|
84
|
+
* @returns True if signature is valid and timestamp is recent
|
|
85
|
+
* @example
|
|
86
|
+
* ```typescript
|
|
87
|
+
* // In your webhook handler
|
|
88
|
+
* app.post('/webhook', (req, res) => {
|
|
89
|
+
* const signature = req.headers['x-webhook-signature'];
|
|
90
|
+
* const timestamp = req.headers['x-webhook-timestamp'];
|
|
91
|
+
* const event = req.headers['x-webhook-event'];
|
|
92
|
+
*
|
|
93
|
+
* // Verify signature and timestamp
|
|
94
|
+
* const isValid = client.webhooks.verifySignature(
|
|
95
|
+
* req.body,
|
|
96
|
+
* signature,
|
|
97
|
+
* timestamp,
|
|
98
|
+
* 'your_webhook_secret'
|
|
99
|
+
* );
|
|
100
|
+
*
|
|
101
|
+
* if (!isValid) {
|
|
102
|
+
* return res.status(401).send('Invalid signature');
|
|
103
|
+
* }
|
|
104
|
+
*
|
|
105
|
+
* // Process webhook...
|
|
106
|
+
* console.log('Event:', event);
|
|
107
|
+
* res.sendStatus(200);
|
|
108
|
+
* });
|
|
109
|
+
* ```
|
|
110
|
+
*/
|
|
111
|
+
verifySignature(payload, signature, timestamp, secret, maxAge = 5 * 60 * 1000 // 5 minutes default
|
|
112
|
+
) {
|
|
113
|
+
try {
|
|
114
|
+
// Verify timestamp (prevent replay attacks)
|
|
115
|
+
const webhookTimestamp = parseInt(timestamp, 10);
|
|
116
|
+
if (isNaN(webhookTimestamp)) {
|
|
117
|
+
console.error('Invalid webhook timestamp format');
|
|
118
|
+
return false;
|
|
119
|
+
}
|
|
120
|
+
const age = Date.now() - webhookTimestamp;
|
|
121
|
+
if (age > maxAge) {
|
|
122
|
+
console.error('Webhook timestamp too old:', age, 'ms');
|
|
123
|
+
return false;
|
|
124
|
+
}
|
|
125
|
+
// Use Node.js crypto if available
|
|
126
|
+
if (typeof require !== 'undefined') {
|
|
127
|
+
try {
|
|
128
|
+
const crypto = require('crypto');
|
|
129
|
+
// Create the signed data: timestamp.payload
|
|
130
|
+
const data = `${timestamp}.${JSON.stringify(payload)}`;
|
|
131
|
+
// Calculate expected signature
|
|
132
|
+
const expectedSignature = crypto
|
|
133
|
+
.createHmac('sha256', secret)
|
|
134
|
+
.update(data)
|
|
135
|
+
.digest('hex');
|
|
136
|
+
// Use timing-safe comparison
|
|
137
|
+
if (crypto.timingSafeEqual) {
|
|
138
|
+
return crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expectedSignature));
|
|
139
|
+
}
|
|
140
|
+
// Fallback to simple comparison (less secure)
|
|
141
|
+
return signature === expectedSignature;
|
|
142
|
+
}
|
|
143
|
+
catch (error) {
|
|
144
|
+
console.error('Error using Node.js crypto:', error);
|
|
145
|
+
// Fall through to browser implementation
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
// For browser environments or when Node crypto is not available
|
|
149
|
+
console.warn('Webhook signature verification not fully supported in this environment');
|
|
150
|
+
console.warn('Please use Node.js for secure webhook verification');
|
|
151
|
+
return false;
|
|
152
|
+
}
|
|
153
|
+
catch (error) {
|
|
154
|
+
console.error('Error verifying webhook signature:', error);
|
|
155
|
+
return false;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
43
158
|
}
|
|
44
159
|
exports.WebhooksResource = WebhooksResource;
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { User } from './types';
|
|
2
|
+
/**
|
|
3
|
+
* Rate limit information from API responses
|
|
4
|
+
*/
|
|
5
|
+
export interface RateLimitInfo {
|
|
6
|
+
/** Maximum number of requests allowed in the time window */
|
|
7
|
+
limit: number;
|
|
8
|
+
/** Number of requests remaining in the current window */
|
|
9
|
+
remaining: number;
|
|
10
|
+
/** Unix timestamp when the rate limit resets */
|
|
11
|
+
reset: number;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Metadata included with API responses
|
|
15
|
+
*/
|
|
16
|
+
export interface ResponseMetadata {
|
|
17
|
+
/** Rate limit information, if available */
|
|
18
|
+
rateLimit?: RateLimitInfo;
|
|
19
|
+
/** Request ID for debugging */
|
|
20
|
+
requestId?: string;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Generic API response wrapper
|
|
24
|
+
*/
|
|
25
|
+
export interface APIResponse<T> {
|
|
26
|
+
/** Response data */
|
|
27
|
+
data: T;
|
|
28
|
+
/** Response metadata */
|
|
29
|
+
meta?: ResponseMetadata;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Response for interaction lists (likes, upvotes, etc.)
|
|
33
|
+
*/
|
|
34
|
+
export interface InteractionListResponse {
|
|
35
|
+
/** List of users who performed the interaction */
|
|
36
|
+
users: User[];
|
|
37
|
+
/** Cursor for next page */
|
|
38
|
+
nextCursor?: string;
|
|
39
|
+
/** Total count of interactions */
|
|
40
|
+
count: number;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Response for poll data
|
|
44
|
+
*/
|
|
45
|
+
export interface PollResponse {
|
|
46
|
+
/** Poll ID */
|
|
47
|
+
id: string;
|
|
48
|
+
/** Poll title */
|
|
49
|
+
title: string;
|
|
50
|
+
/** Poll options */
|
|
51
|
+
options: PollOption[];
|
|
52
|
+
/** Total number of votes */
|
|
53
|
+
totalVotes: number;
|
|
54
|
+
/** Whether the poll allows multiple votes */
|
|
55
|
+
allowMultiple?: boolean;
|
|
56
|
+
/** Poll expiration date */
|
|
57
|
+
expiresAt?: string;
|
|
58
|
+
/** User's vote, if userId was provided */
|
|
59
|
+
userVote?: {
|
|
60
|
+
optionId: string;
|
|
61
|
+
votedAt: string;
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Poll option data
|
|
66
|
+
*/
|
|
67
|
+
export interface PollOption {
|
|
68
|
+
/** Option ID */
|
|
69
|
+
id: string;
|
|
70
|
+
/** Option title */
|
|
71
|
+
title: string;
|
|
72
|
+
/** Option color */
|
|
73
|
+
color?: string;
|
|
74
|
+
/** Number of votes for this option */
|
|
75
|
+
votes: number;
|
|
76
|
+
/** Percentage of total votes */
|
|
77
|
+
percentage: number;
|
|
78
|
+
/** Extended data */
|
|
79
|
+
extendedData?: Record<string, any>;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Response for batch operations
|
|
83
|
+
*/
|
|
84
|
+
export interface BatchOperationResponse {
|
|
85
|
+
/** Number of items successfully processed */
|
|
86
|
+
success: number;
|
|
87
|
+
/** Number of items that failed */
|
|
88
|
+
failed: number;
|
|
89
|
+
/** Error details for failed items */
|
|
90
|
+
errors?: Array<{
|
|
91
|
+
id: string;
|
|
92
|
+
error: string;
|
|
93
|
+
}>;
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Pagination cursor response
|
|
97
|
+
*/
|
|
98
|
+
export interface CursorPagination<T> {
|
|
99
|
+
/** Array of items */
|
|
100
|
+
items: T[];
|
|
101
|
+
/** Cursor for next page */
|
|
102
|
+
nextCursor?: string;
|
|
103
|
+
/** Whether there are more items */
|
|
104
|
+
hasMore: boolean;
|
|
105
|
+
}
|
package/dist/utils.d.ts
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pagination utilities for working with cursor-based pagination
|
|
3
|
+
*/
|
|
4
|
+
export declare class PaginationHelper {
|
|
5
|
+
/**
|
|
6
|
+
* Async generator to auto-paginate through all results
|
|
7
|
+
* @param fetchPage - Function to fetch a page of results
|
|
8
|
+
* @yields Individual items from each page
|
|
9
|
+
* @example
|
|
10
|
+
* ```typescript
|
|
11
|
+
* const helper = new PaginationHelper();
|
|
12
|
+
* for await (const thread of helper.paginateAll((cursor) =>
|
|
13
|
+
* client.threads.list({ cursor })
|
|
14
|
+
* )) {
|
|
15
|
+
* console.log(thread);
|
|
16
|
+
* }
|
|
17
|
+
* ```
|
|
18
|
+
*/
|
|
19
|
+
paginateAll<T>(fetchPage: (cursor?: string) => Promise<{
|
|
20
|
+
threads?: T[];
|
|
21
|
+
posts?: T[];
|
|
22
|
+
users?: T[];
|
|
23
|
+
tags?: T[];
|
|
24
|
+
nextThreadCursor?: string;
|
|
25
|
+
nextPostCursor?: string;
|
|
26
|
+
nextUserCursor?: string;
|
|
27
|
+
nextTagCursor?: string;
|
|
28
|
+
}>): AsyncIterableIterator<T>;
|
|
29
|
+
/**
|
|
30
|
+
* Fetch all items from a paginated endpoint
|
|
31
|
+
* @param fetchPage - Function to fetch a page of results
|
|
32
|
+
* @param maxPages - Maximum number of pages to fetch (default: Infinity)
|
|
33
|
+
* @returns Array of all items
|
|
34
|
+
*/
|
|
35
|
+
fetchAllPages<T>(fetchPage: (cursor?: string) => Promise<{
|
|
36
|
+
threads?: T[];
|
|
37
|
+
posts?: T[];
|
|
38
|
+
users?: T[];
|
|
39
|
+
tags?: T[];
|
|
40
|
+
nextThreadCursor?: string;
|
|
41
|
+
nextPostCursor?: string;
|
|
42
|
+
nextUserCursor?: string;
|
|
43
|
+
nextTagCursor?: string;
|
|
44
|
+
}>, maxPages?: number): Promise<T[]>;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Input validation utilities
|
|
48
|
+
*/
|
|
49
|
+
export declare class Validator {
|
|
50
|
+
/**
|
|
51
|
+
* Validate that a string is not empty
|
|
52
|
+
*/
|
|
53
|
+
static isNonEmptyString(value: any, fieldName: string): void;
|
|
54
|
+
/**
|
|
55
|
+
* Validate email format
|
|
56
|
+
*/
|
|
57
|
+
static isValidEmail(email: string): boolean;
|
|
58
|
+
/**
|
|
59
|
+
* Validate that a value is one of the allowed options
|
|
60
|
+
*/
|
|
61
|
+
static isOneOf<T>(value: T, options: T[], fieldName: string): void;
|
|
62
|
+
/**
|
|
63
|
+
* Validate cursor format (if needed)
|
|
64
|
+
*/
|
|
65
|
+
static isValidCursor(cursor: string): boolean;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Retry utilities for handling transient failures
|
|
69
|
+
*/
|
|
70
|
+
export declare class RetryHelper {
|
|
71
|
+
/**
|
|
72
|
+
* Execute a function with exponential backoff retry
|
|
73
|
+
* @param fn - Function to execute
|
|
74
|
+
* @param maxRetries - Maximum number of retry attempts
|
|
75
|
+
* @param initialDelay - Initial delay in milliseconds
|
|
76
|
+
* @returns Result of the function
|
|
77
|
+
*/
|
|
78
|
+
static withRetry<T>(fn: () => Promise<T>, maxRetries?: number, initialDelay?: number): Promise<T>;
|
|
79
|
+
private static sleep;
|
|
80
|
+
}
|
package/dist/utils.js
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.RetryHelper = exports.Validator = exports.PaginationHelper = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* Pagination utilities for working with cursor-based pagination
|
|
6
|
+
*/
|
|
7
|
+
class PaginationHelper {
|
|
8
|
+
/**
|
|
9
|
+
* Async generator to auto-paginate through all results
|
|
10
|
+
* @param fetchPage - Function to fetch a page of results
|
|
11
|
+
* @yields Individual items from each page
|
|
12
|
+
* @example
|
|
13
|
+
* ```typescript
|
|
14
|
+
* const helper = new PaginationHelper();
|
|
15
|
+
* for await (const thread of helper.paginateAll((cursor) =>
|
|
16
|
+
* client.threads.list({ cursor })
|
|
17
|
+
* )) {
|
|
18
|
+
* console.log(thread);
|
|
19
|
+
* }
|
|
20
|
+
* ```
|
|
21
|
+
*/
|
|
22
|
+
async *paginateAll(fetchPage) {
|
|
23
|
+
let cursor;
|
|
24
|
+
let hasMore = true;
|
|
25
|
+
while (hasMore) {
|
|
26
|
+
const response = await fetchPage(cursor);
|
|
27
|
+
// Determine which array and cursor to use
|
|
28
|
+
const items = response.threads || response.posts || response.users || response.tags || [];
|
|
29
|
+
const nextCursor = response.nextThreadCursor || response.nextPostCursor ||
|
|
30
|
+
response.nextUserCursor || response.nextTagCursor;
|
|
31
|
+
for (const item of items) {
|
|
32
|
+
yield item;
|
|
33
|
+
}
|
|
34
|
+
cursor = nextCursor;
|
|
35
|
+
hasMore = !!nextCursor;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Fetch all items from a paginated endpoint
|
|
40
|
+
* @param fetchPage - Function to fetch a page of results
|
|
41
|
+
* @param maxPages - Maximum number of pages to fetch (default: Infinity)
|
|
42
|
+
* @returns Array of all items
|
|
43
|
+
*/
|
|
44
|
+
async fetchAllPages(fetchPage, maxPages = Infinity) {
|
|
45
|
+
const allItems = [];
|
|
46
|
+
let cursor;
|
|
47
|
+
let pageCount = 0;
|
|
48
|
+
while (pageCount < maxPages) {
|
|
49
|
+
const response = await fetchPage(cursor);
|
|
50
|
+
const items = response.threads || response.posts || response.users || response.tags || [];
|
|
51
|
+
const nextCursor = response.nextThreadCursor || response.nextPostCursor ||
|
|
52
|
+
response.nextUserCursor || response.nextTagCursor;
|
|
53
|
+
allItems.push(...items);
|
|
54
|
+
cursor = nextCursor;
|
|
55
|
+
pageCount++;
|
|
56
|
+
if (!nextCursor)
|
|
57
|
+
break;
|
|
58
|
+
}
|
|
59
|
+
return allItems;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
exports.PaginationHelper = PaginationHelper;
|
|
63
|
+
/**
|
|
64
|
+
* Input validation utilities
|
|
65
|
+
*/
|
|
66
|
+
class Validator {
|
|
67
|
+
/**
|
|
68
|
+
* Validate that a string is not empty
|
|
69
|
+
*/
|
|
70
|
+
static isNonEmptyString(value, fieldName) {
|
|
71
|
+
if (typeof value !== 'string' || value.trim().length === 0) {
|
|
72
|
+
throw new Error(`${fieldName} must be a non-empty string`);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Validate email format
|
|
77
|
+
*/
|
|
78
|
+
static isValidEmail(email) {
|
|
79
|
+
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
80
|
+
return emailRegex.test(email);
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Validate that a value is one of the allowed options
|
|
84
|
+
*/
|
|
85
|
+
static isOneOf(value, options, fieldName) {
|
|
86
|
+
if (!options.includes(value)) {
|
|
87
|
+
throw new Error(`${fieldName} must be one of: ${options.join(', ')}`);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Validate cursor format (if needed)
|
|
92
|
+
*/
|
|
93
|
+
static isValidCursor(cursor) {
|
|
94
|
+
return typeof cursor === 'string' && cursor.length > 0;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
exports.Validator = Validator;
|
|
98
|
+
/**
|
|
99
|
+
* Retry utilities for handling transient failures
|
|
100
|
+
*/
|
|
101
|
+
class RetryHelper {
|
|
102
|
+
/**
|
|
103
|
+
* Execute a function with exponential backoff retry
|
|
104
|
+
* @param fn - Function to execute
|
|
105
|
+
* @param maxRetries - Maximum number of retry attempts
|
|
106
|
+
* @param initialDelay - Initial delay in milliseconds
|
|
107
|
+
* @returns Result of the function
|
|
108
|
+
*/
|
|
109
|
+
static async withRetry(fn, maxRetries = 3, initialDelay = 1000) {
|
|
110
|
+
let lastError;
|
|
111
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
112
|
+
try {
|
|
113
|
+
return await fn();
|
|
114
|
+
}
|
|
115
|
+
catch (error) {
|
|
116
|
+
lastError = error;
|
|
117
|
+
// Don't retry on client errors (4xx) except 429
|
|
118
|
+
if (error.statusCode >= 400 && error.statusCode < 500 && error.statusCode !== 429) {
|
|
119
|
+
throw error;
|
|
120
|
+
}
|
|
121
|
+
// Don't retry on last attempt
|
|
122
|
+
if (attempt === maxRetries) {
|
|
123
|
+
break;
|
|
124
|
+
}
|
|
125
|
+
// Calculate delay with exponential backoff
|
|
126
|
+
const delay = error.retryAfter
|
|
127
|
+
? error.retryAfter * 1000
|
|
128
|
+
: initialDelay * Math.pow(2, attempt);
|
|
129
|
+
await this.sleep(delay);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
throw lastError;
|
|
133
|
+
}
|
|
134
|
+
static sleep(ms) {
|
|
135
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
exports.RetryHelper = RetryHelper;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# SDK Examples
|
|
2
|
+
|
|
3
|
+
This directory contains example code demonstrating common use cases for the Foru.ms SDK.
|
|
4
|
+
|
|
5
|
+
## Examples
|
|
6
|
+
|
|
7
|
+
- [Authentication](./authentication.ts) - Login, registration, and token management
|
|
8
|
+
- [Managing Threads](./managing-threads.ts) - Create, update, and interact with threads
|
|
9
|
+
- [Pagination](./pagination.ts) - Working with cursor-based pagination
|
|
10
|
+
- [Webhooks](./webhooks.ts) - Setting up and verifying webhooks
|
|
11
|
+
- [Error Handling](./error-handling.ts) - Properly handling SDK errors
|
|
12
|
+
|
|
13
|
+
## Running Examples
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
# Install dependencies
|
|
17
|
+
npm install @foru-ms/sdk
|
|
18
|
+
|
|
19
|
+
# Run an example with ts-node
|
|
20
|
+
npx ts-node examples/authentication.ts
|
|
21
|
+
|
|
22
|
+
# Or compile and run
|
|
23
|
+
tsc examples/authentication.ts && node examples/authentication.js
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Configuration
|
|
27
|
+
|
|
28
|
+
Most examples require an API key. Set it as an environment variable:
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
export FORU_API_KEY="your_api_key_here"
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
For user-authenticated examples, you'll also need a user token:
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
export FORU_USER_TOKEN="user_jwt_token"
|
|
38
|
+
```
|