@rooguys/js 0.1.0 → 1.0.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 +342 -141
- package/package.json +1 -1
- package/src/__tests__/fixtures/responses.js +249 -0
- package/src/__tests__/property/batch-event-validation.property.test.js +225 -0
- package/src/__tests__/property/email-validation.property.test.js +272 -0
- package/src/__tests__/property/error-mapping.property.test.js +506 -0
- package/src/__tests__/property/field-selection.property.test.js +297 -0
- package/src/__tests__/property/idempotency-key.property.test.js +350 -0
- package/src/__tests__/property/leaderboard-filter.property.test.js +585 -0
- package/src/__tests__/property/partial-update.property.test.js +251 -0
- package/src/__tests__/property/rate-limit-error.property.test.js +276 -0
- package/src/__tests__/property/rate-limit-extraction.property.test.js +193 -0
- package/src/__tests__/property/request-construction.property.test.js +20 -28
- package/src/__tests__/property/response-format.property.test.js +418 -0
- package/src/__tests__/property/response-parsing.property.test.js +16 -21
- package/src/__tests__/property/timestamp-validation.property.test.js +345 -0
- package/src/__tests__/unit/aha.test.js +57 -26
- package/src/__tests__/unit/config.test.js +7 -1
- package/src/__tests__/unit/errors.test.js +6 -8
- package/src/__tests__/unit/events.test.js +253 -14
- package/src/__tests__/unit/leaderboards.test.js +249 -0
- package/src/__tests__/unit/questionnaires.test.js +6 -6
- package/src/__tests__/unit/users.test.js +275 -12
- package/src/__tests__/utils/generators.js +87 -0
- package/src/__tests__/utils/mockClient.js +71 -5
- package/src/errors.js +156 -0
- package/src/http-client.js +276 -0
- package/src/index.js +856 -66
package/src/index.js
CHANGED
|
@@ -1,138 +1,928 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Rooguys Browser SDK
|
|
3
|
+
* Official JavaScript SDK for the Rooguys Gamification Platform
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { HttpClient, extractRateLimitInfo, extractRequestId, parseResponseBody } from './http-client.js';
|
|
7
|
+
import {
|
|
8
|
+
RooguysError,
|
|
9
|
+
ValidationError,
|
|
10
|
+
AuthenticationError,
|
|
11
|
+
ForbiddenError,
|
|
12
|
+
NotFoundError,
|
|
13
|
+
ConflictError,
|
|
14
|
+
RateLimitError,
|
|
15
|
+
ServerError,
|
|
16
|
+
mapStatusToError,
|
|
17
|
+
} from './errors.js';
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Main Rooguys SDK class
|
|
3
21
|
*/
|
|
4
22
|
class Rooguys {
|
|
23
|
+
/**
|
|
24
|
+
* Create a new Rooguys SDK instance
|
|
25
|
+
* @param {string} apiKey - API key for authentication
|
|
26
|
+
* @param {Object} [options] - Configuration options
|
|
27
|
+
* @param {string} [options.baseUrl] - Base URL for API (default: https://api.rooguys.com/v1)
|
|
28
|
+
* @param {number} [options.timeout] - Request timeout in ms (default: 10000)
|
|
29
|
+
* @param {Function} [options.onRateLimitWarning] - Callback when rate limit is 80% consumed
|
|
30
|
+
* @param {boolean} [options.autoRetry] - Enable auto-retry for rate-limited requests (default: false)
|
|
31
|
+
* @param {number} [options.maxRetries] - Maximum retry attempts for rate limits (default: 3)
|
|
32
|
+
*/
|
|
5
33
|
constructor(apiKey, options = {}) {
|
|
34
|
+
if (!apiKey) {
|
|
35
|
+
throw new ValidationError('API key is required', { code: 'MISSING_API_KEY' });
|
|
36
|
+
}
|
|
37
|
+
|
|
6
38
|
this.apiKey = apiKey;
|
|
7
39
|
this.baseUrl = options.baseUrl || 'https://api.rooguys.com/v1';
|
|
8
40
|
this.timeout = options.timeout || 10000;
|
|
41
|
+
this.onRateLimitWarning = options.onRateLimitWarning || null;
|
|
42
|
+
this.autoRetry = options.autoRetry || false;
|
|
43
|
+
this.maxRetries = options.maxRetries || 3;
|
|
44
|
+
|
|
45
|
+
// Initialize HTTP client
|
|
46
|
+
this._httpClient = new HttpClient(apiKey, {
|
|
47
|
+
baseUrl: this.baseUrl,
|
|
48
|
+
timeout: this.timeout,
|
|
49
|
+
onRateLimitWarning: this.onRateLimitWarning,
|
|
50
|
+
autoRetry: this.autoRetry,
|
|
51
|
+
maxRetries: this.maxRetries,
|
|
52
|
+
});
|
|
9
53
|
}
|
|
10
54
|
|
|
55
|
+
/**
|
|
56
|
+
* Legacy request method for backward compatibility
|
|
57
|
+
* @deprecated Use the new module methods instead
|
|
58
|
+
*/
|
|
11
59
|
async _request(endpoint, method = 'GET', data = null, params = {}) {
|
|
12
|
-
const
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
60
|
+
const response = await this._httpClient.request({
|
|
61
|
+
method,
|
|
62
|
+
path: endpoint,
|
|
63
|
+
body: data,
|
|
64
|
+
params,
|
|
17
65
|
});
|
|
66
|
+
return response.data;
|
|
67
|
+
}
|
|
18
68
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
69
|
+
/**
|
|
70
|
+
* Events module for tracking user events
|
|
71
|
+
*/
|
|
72
|
+
get events() {
|
|
73
|
+
const self = this;
|
|
74
|
+
return {
|
|
75
|
+
/**
|
|
76
|
+
* Track a single event
|
|
77
|
+
* @param {string} eventName - Name of the event
|
|
78
|
+
* @param {string} userId - User ID
|
|
79
|
+
* @param {Object} [properties] - Event properties
|
|
80
|
+
* @param {Object} [options] - Additional options
|
|
81
|
+
* @param {boolean} [options.includeProfile] - Include user profile in response
|
|
82
|
+
* @param {Date} [options.timestamp] - Custom timestamp for historical events (max 7 days old)
|
|
83
|
+
* @param {string} [options.idempotencyKey] - Idempotency key to prevent duplicate processing
|
|
84
|
+
* @returns {Promise<Object>} Event tracking response
|
|
85
|
+
*/
|
|
86
|
+
track: async (eventName, userId, properties = {}, options = {}) => {
|
|
87
|
+
const body = {
|
|
88
|
+
event_name: eventName,
|
|
89
|
+
user_id: userId,
|
|
90
|
+
properties,
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
// Add custom timestamp if provided
|
|
94
|
+
if (options.timestamp) {
|
|
95
|
+
const timestamp = options.timestamp instanceof Date ? options.timestamp : new Date(options.timestamp);
|
|
96
|
+
const sevenDaysAgo = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000);
|
|
97
|
+
|
|
98
|
+
if (timestamp < sevenDaysAgo) {
|
|
99
|
+
throw new ValidationError('Custom timestamp cannot be more than 7 days in the past', {
|
|
100
|
+
code: 'TIMESTAMP_TOO_OLD',
|
|
101
|
+
fieldErrors: [{ field: 'timestamp', message: 'Timestamp must be within the last 7 days' }],
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
body.timestamp = timestamp.toISOString();
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const response = await self._httpClient.post('/events', body, {
|
|
109
|
+
params: {
|
|
110
|
+
include_profile: options.includeProfile,
|
|
111
|
+
},
|
|
112
|
+
idempotencyKey: options.idempotencyKey,
|
|
113
|
+
});
|
|
114
|
+
return response.data;
|
|
24
115
|
},
|
|
25
|
-
};
|
|
26
116
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
117
|
+
/**
|
|
118
|
+
* Track multiple events in a single request (batch operation)
|
|
119
|
+
* @param {Array<Object>} events - Array of events to track (max 100)
|
|
120
|
+
* @param {string} events[].eventName - Name of the event
|
|
121
|
+
* @param {string} events[].userId - User ID
|
|
122
|
+
* @param {Object} [events[].properties] - Event properties
|
|
123
|
+
* @param {Date} [events[].timestamp] - Custom timestamp for historical events
|
|
124
|
+
* @param {Object} [options] - Batch options
|
|
125
|
+
* @param {string} [options.idempotencyKey] - Idempotency key for the batch
|
|
126
|
+
* @returns {Promise<Object>} Batch tracking response with individual status for each event
|
|
127
|
+
*/
|
|
128
|
+
trackBatch: async (events, options = {}) => {
|
|
129
|
+
// Validate array
|
|
130
|
+
if (!Array.isArray(events)) {
|
|
131
|
+
throw new ValidationError('Events must be an array', {
|
|
132
|
+
code: 'INVALID_EVENTS',
|
|
133
|
+
fieldErrors: [{ field: 'events', message: 'Events must be an array' }],
|
|
134
|
+
});
|
|
135
|
+
}
|
|
30
136
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
137
|
+
// Validate array length
|
|
138
|
+
if (events.length === 0) {
|
|
139
|
+
throw new ValidationError('Events array cannot be empty', {
|
|
140
|
+
code: 'EMPTY_EVENTS',
|
|
141
|
+
fieldErrors: [{ field: 'events', message: 'At least one event is required' }],
|
|
142
|
+
});
|
|
143
|
+
}
|
|
35
144
|
|
|
36
|
-
|
|
37
|
-
|
|
145
|
+
if (events.length > 100) {
|
|
146
|
+
throw new ValidationError('Batch size exceeds maximum of 100 events', {
|
|
147
|
+
code: 'BATCH_TOO_LARGE',
|
|
148
|
+
fieldErrors: [{ field: 'events', message: 'Maximum batch size is 100 events' }],
|
|
149
|
+
});
|
|
150
|
+
}
|
|
38
151
|
|
|
39
|
-
|
|
40
|
-
const errorData = await response.json().catch(() => ({}));
|
|
41
|
-
throw new Error(errorData.message || `API Error: ${response.statusText}`);
|
|
42
|
-
}
|
|
152
|
+
const sevenDaysAgo = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000);
|
|
43
153
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
154
|
+
// Transform and validate events
|
|
155
|
+
const transformedEvents = events.map((event, index) => {
|
|
156
|
+
const transformed = {
|
|
157
|
+
event_name: event.eventName,
|
|
158
|
+
user_id: event.userId,
|
|
159
|
+
properties: event.properties || {},
|
|
160
|
+
};
|
|
49
161
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
162
|
+
// Validate and add custom timestamp if provided
|
|
163
|
+
if (event.timestamp) {
|
|
164
|
+
const timestamp = event.timestamp instanceof Date ? event.timestamp : new Date(event.timestamp);
|
|
165
|
+
|
|
166
|
+
if (timestamp < sevenDaysAgo) {
|
|
167
|
+
throw new ValidationError(`Event at index ${index}: Custom timestamp cannot be more than 7 days in the past`, {
|
|
168
|
+
code: 'TIMESTAMP_TOO_OLD',
|
|
169
|
+
fieldErrors: [{ field: `events[${index}].timestamp`, message: 'Timestamp must be within the last 7 days' }],
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
transformed.timestamp = timestamp.toISOString();
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return transformed;
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
const response = await self._httpClient.post('/events/batch', {
|
|
180
|
+
events: transformedEvents,
|
|
181
|
+
}, {
|
|
182
|
+
idempotencyKey: options.idempotencyKey,
|
|
183
|
+
});
|
|
184
|
+
return response.data;
|
|
185
|
+
},
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* @deprecated Use track() instead. The /v1/event endpoint is deprecated.
|
|
189
|
+
* Track a single event using the legacy endpoint
|
|
190
|
+
* @param {string} eventName - Name of the event
|
|
191
|
+
* @param {string} userId - User ID
|
|
192
|
+
* @param {Object} [properties] - Event properties
|
|
193
|
+
* @param {Object} [options] - Additional options
|
|
194
|
+
* @returns {Promise<Object>} Event tracking response
|
|
195
|
+
*/
|
|
196
|
+
trackLegacy: async (eventName, userId, properties = {}, options = {}) => {
|
|
197
|
+
console.warn('DEPRECATION WARNING: events.trackLegacy() uses the deprecated /v1/event endpoint. Please use events.track() instead which uses /v1/events.');
|
|
198
|
+
|
|
199
|
+
const response = await self._httpClient.post('/event', {
|
|
54
200
|
event_name: eventName,
|
|
55
201
|
user_id: userId,
|
|
56
202
|
properties,
|
|
57
203
|
}, {
|
|
58
|
-
|
|
204
|
+
params: {
|
|
205
|
+
include_profile: options.includeProfile,
|
|
206
|
+
},
|
|
59
207
|
});
|
|
208
|
+
return response.data;
|
|
60
209
|
},
|
|
61
210
|
};
|
|
62
211
|
}
|
|
63
212
|
|
|
213
|
+
/**
|
|
214
|
+
* Users module for user management and queries
|
|
215
|
+
*/
|
|
64
216
|
get users() {
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
217
|
+
const self = this;
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Validate email format client-side
|
|
221
|
+
* @param {string} email - Email to validate
|
|
222
|
+
* @returns {boolean} True if valid
|
|
223
|
+
*/
|
|
224
|
+
const isValidEmail = (email) => {
|
|
225
|
+
if (!email || typeof email !== 'string') return true; // Optional field
|
|
226
|
+
// RFC 5322 compliant email regex (simplified)
|
|
227
|
+
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
228
|
+
return emailRegex.test(email);
|
|
229
|
+
};
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Build request body with only provided fields (partial update support)
|
|
233
|
+
* @param {Object} userData - User data object
|
|
234
|
+
* @returns {Object} Cleaned request body
|
|
235
|
+
*/
|
|
236
|
+
const buildUserRequestBody = (userData) => {
|
|
237
|
+
const body = {};
|
|
69
238
|
|
|
70
|
-
|
|
239
|
+
if (userData.userId !== undefined) {
|
|
240
|
+
body.user_id = userData.userId;
|
|
241
|
+
}
|
|
242
|
+
if (userData.displayName !== undefined) {
|
|
243
|
+
body.display_name = userData.displayName;
|
|
244
|
+
}
|
|
245
|
+
if (userData.email !== undefined) {
|
|
246
|
+
body.email = userData.email;
|
|
247
|
+
}
|
|
248
|
+
if (userData.metadata !== undefined) {
|
|
249
|
+
body.metadata = userData.metadata;
|
|
250
|
+
}
|
|
251
|
+
if (userData.firstName !== undefined) {
|
|
252
|
+
body.first_name = userData.firstName;
|
|
253
|
+
}
|
|
254
|
+
if (userData.lastName !== undefined) {
|
|
255
|
+
body.last_name = userData.lastName;
|
|
256
|
+
}
|
|
71
257
|
|
|
72
|
-
|
|
73
|
-
|
|
258
|
+
return body;
|
|
259
|
+
};
|
|
260
|
+
|
|
261
|
+
return {
|
|
262
|
+
/**
|
|
263
|
+
* Create a new user
|
|
264
|
+
* @param {Object} userData - User data
|
|
265
|
+
* @param {string} userData.userId - Unique user ID
|
|
266
|
+
* @param {string} [userData.displayName] - Display name
|
|
267
|
+
* @param {string} [userData.email] - Email address
|
|
268
|
+
* @param {string} [userData.firstName] - First name
|
|
269
|
+
* @param {string} [userData.lastName] - Last name
|
|
270
|
+
* @param {Object} [userData.metadata] - Custom metadata
|
|
271
|
+
* @returns {Promise<Object>} Created user profile
|
|
272
|
+
*/
|
|
273
|
+
create: async (userData) => {
|
|
274
|
+
// Validate required fields
|
|
275
|
+
if (!userData || !userData.userId) {
|
|
276
|
+
throw new ValidationError('User ID is required', {
|
|
277
|
+
code: 'MISSING_USER_ID',
|
|
278
|
+
fieldErrors: [{ field: 'userId', message: 'User ID is required' }],
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// Validate email format if provided
|
|
283
|
+
if (userData.email && !isValidEmail(userData.email)) {
|
|
284
|
+
throw new ValidationError('Invalid email format', {
|
|
285
|
+
code: 'INVALID_EMAIL',
|
|
286
|
+
fieldErrors: [{ field: 'email', message: 'Email must be a valid email address' }],
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
const body = buildUserRequestBody(userData);
|
|
291
|
+
const response = await self._httpClient.post('/users', body);
|
|
292
|
+
return response.data;
|
|
293
|
+
},
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Update an existing user
|
|
297
|
+
* @param {string} userId - User ID to update
|
|
298
|
+
* @param {Object} userData - User data to update (partial update supported)
|
|
299
|
+
* @param {string} [userData.displayName] - Display name
|
|
300
|
+
* @param {string} [userData.email] - Email address
|
|
301
|
+
* @param {string} [userData.firstName] - First name
|
|
302
|
+
* @param {string} [userData.lastName] - Last name
|
|
303
|
+
* @param {Object} [userData.metadata] - Custom metadata
|
|
304
|
+
* @returns {Promise<Object>} Updated user profile
|
|
305
|
+
*/
|
|
306
|
+
update: async (userId, userData) => {
|
|
307
|
+
// Validate user ID
|
|
308
|
+
if (!userId) {
|
|
309
|
+
throw new ValidationError('User ID is required', {
|
|
310
|
+
code: 'MISSING_USER_ID',
|
|
311
|
+
fieldErrors: [{ field: 'userId', message: 'User ID is required' }],
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// Validate email format if provided
|
|
316
|
+
if (userData.email && !isValidEmail(userData.email)) {
|
|
317
|
+
throw new ValidationError('Invalid email format', {
|
|
318
|
+
code: 'INVALID_EMAIL',
|
|
319
|
+
fieldErrors: [{ field: 'email', message: 'Email must be a valid email address' }],
|
|
320
|
+
});
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
const body = buildUserRequestBody(userData);
|
|
324
|
+
const response = await self._httpClient.patch(`/users/${encodeURIComponent(userId)}`, body);
|
|
325
|
+
return response.data;
|
|
326
|
+
},
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* Create multiple users in a single request (batch operation)
|
|
330
|
+
* @param {Array<Object>} users - Array of user data objects (max 100)
|
|
331
|
+
* @param {string} users[].userId - Unique user ID
|
|
332
|
+
* @param {string} [users[].displayName] - Display name
|
|
333
|
+
* @param {string} [users[].email] - Email address
|
|
334
|
+
* @param {Object} [users[].metadata] - Custom metadata
|
|
335
|
+
* @returns {Promise<Object>} Batch creation response with individual status for each user
|
|
336
|
+
*/
|
|
337
|
+
createBatch: async (users) => {
|
|
338
|
+
// Validate array
|
|
339
|
+
if (!Array.isArray(users)) {
|
|
340
|
+
throw new ValidationError('Users must be an array', {
|
|
341
|
+
code: 'INVALID_USERS',
|
|
342
|
+
fieldErrors: [{ field: 'users', message: 'Users must be an array' }],
|
|
343
|
+
});
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// Validate array length
|
|
347
|
+
if (users.length === 0) {
|
|
348
|
+
throw new ValidationError('Users array cannot be empty', {
|
|
349
|
+
code: 'EMPTY_USERS',
|
|
350
|
+
fieldErrors: [{ field: 'users', message: 'At least one user is required' }],
|
|
351
|
+
});
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
if (users.length > 100) {
|
|
355
|
+
throw new ValidationError('Batch size exceeds maximum of 100 users', {
|
|
356
|
+
code: 'BATCH_TOO_LARGE',
|
|
357
|
+
fieldErrors: [{ field: 'users', message: 'Maximum batch size is 100 users' }],
|
|
358
|
+
});
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// Validate and transform each user
|
|
362
|
+
const transformedUsers = users.map((user, index) => {
|
|
363
|
+
// Validate required fields
|
|
364
|
+
if (!user.userId) {
|
|
365
|
+
throw new ValidationError(`User at index ${index}: User ID is required`, {
|
|
366
|
+
code: 'MISSING_USER_ID',
|
|
367
|
+
fieldErrors: [{ field: `users[${index}].userId`, message: 'User ID is required' }],
|
|
368
|
+
});
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// Validate email format if provided
|
|
372
|
+
if (user.email && !isValidEmail(user.email)) {
|
|
373
|
+
throw new ValidationError(`User at index ${index}: Invalid email format`, {
|
|
374
|
+
code: 'INVALID_EMAIL',
|
|
375
|
+
fieldErrors: [{ field: `users[${index}].email`, message: 'Email must be a valid email address' }],
|
|
376
|
+
});
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
return buildUserRequestBody(user);
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
const response = await self._httpClient.post('/users/batch', { users: transformedUsers });
|
|
383
|
+
return response.data;
|
|
384
|
+
},
|
|
385
|
+
|
|
386
|
+
/**
|
|
387
|
+
* Get user profile with optional field selection
|
|
388
|
+
* @param {string} userId - User ID
|
|
389
|
+
* @param {Object} [options] - Query options
|
|
390
|
+
* @param {string[]} [options.fields] - Fields to include in response
|
|
391
|
+
* @returns {Promise<Object>} User profile
|
|
392
|
+
*/
|
|
393
|
+
get: async (userId, options = {}) => {
|
|
394
|
+
const params = {};
|
|
74
395
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
396
|
+
// Add field selection if provided
|
|
397
|
+
if (options.fields && Array.isArray(options.fields) && options.fields.length > 0) {
|
|
398
|
+
params.fields = options.fields.join(',');
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
const response = await self._httpClient.get(`/users/${encodeURIComponent(userId)}`, params);
|
|
402
|
+
return self._parseUserProfile(response.data);
|
|
403
|
+
},
|
|
404
|
+
|
|
405
|
+
/**
|
|
406
|
+
* Search users with pagination
|
|
407
|
+
* @param {string} query - Search query
|
|
408
|
+
* @param {Object} [options] - Search options
|
|
409
|
+
* @param {number} [options.page=1] - Page number
|
|
410
|
+
* @param {number} [options.limit=50] - Items per page
|
|
411
|
+
* @param {string[]} [options.fields] - Fields to include in response
|
|
412
|
+
* @returns {Promise<Object>} Paginated search results
|
|
413
|
+
*/
|
|
414
|
+
search: async (query, options = {}) => {
|
|
415
|
+
const params = {
|
|
416
|
+
q: query,
|
|
417
|
+
page: options.page || 1,
|
|
418
|
+
limit: options.limit || 50,
|
|
419
|
+
};
|
|
420
|
+
|
|
421
|
+
// Add field selection if provided
|
|
422
|
+
if (options.fields && Array.isArray(options.fields) && options.fields.length > 0) {
|
|
423
|
+
params.fields = options.fields.join(',');
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
const response = await self._httpClient.get('/users/search', params);
|
|
427
|
+
return {
|
|
428
|
+
...response.data,
|
|
429
|
+
users: (response.data.users || []).map(user => self._parseUserProfile(user)),
|
|
430
|
+
};
|
|
431
|
+
},
|
|
432
|
+
|
|
433
|
+
/**
|
|
434
|
+
* Get multiple user profiles
|
|
435
|
+
* @param {string[]} userIds - Array of user IDs
|
|
436
|
+
* @returns {Promise<Object>} Bulk users response
|
|
437
|
+
*/
|
|
438
|
+
getBulk: async (userIds) => {
|
|
439
|
+
const response = await self._httpClient.post('/users/bulk', { user_ids: userIds });
|
|
440
|
+
return {
|
|
441
|
+
...response.data,
|
|
442
|
+
users: (response.data.users || []).map(user => self._parseUserProfile(user)),
|
|
443
|
+
};
|
|
444
|
+
},
|
|
445
|
+
|
|
446
|
+
/**
|
|
447
|
+
* Get user badges
|
|
448
|
+
* @param {string} userId - User ID
|
|
449
|
+
* @returns {Promise<Object[]>} User badges
|
|
450
|
+
*/
|
|
451
|
+
getBadges: async (userId) => {
|
|
452
|
+
const response = await self._httpClient.get(`/users/${encodeURIComponent(userId)}/badges`);
|
|
453
|
+
return response.data;
|
|
454
|
+
},
|
|
455
|
+
|
|
456
|
+
/**
|
|
457
|
+
* Get user rank
|
|
458
|
+
* @param {string} userId - User ID
|
|
459
|
+
* @param {string} [timeframe='all-time'] - Timeframe for ranking
|
|
460
|
+
* @returns {Promise<Object>} User rank info
|
|
461
|
+
*/
|
|
462
|
+
getRank: async (userId, timeframe = 'all-time') => {
|
|
463
|
+
const response = await self._httpClient.get(
|
|
464
|
+
`/users/${encodeURIComponent(userId)}/rank`,
|
|
465
|
+
{ timeframe }
|
|
466
|
+
);
|
|
467
|
+
return response.data;
|
|
468
|
+
},
|
|
469
|
+
|
|
470
|
+
/**
|
|
471
|
+
* Submit questionnaire answers
|
|
472
|
+
* @param {string} userId - User ID
|
|
473
|
+
* @param {string} questionnaireId - Questionnaire ID
|
|
474
|
+
* @param {Object[]} answers - Array of answers
|
|
475
|
+
* @returns {Promise<Object>} Submission response
|
|
476
|
+
*/
|
|
477
|
+
submitAnswers: async (userId, questionnaireId, answers) => {
|
|
478
|
+
const response = await self._httpClient.post(
|
|
479
|
+
`/users/${encodeURIComponent(userId)}/answers`,
|
|
480
|
+
{
|
|
481
|
+
questionnaire_id: questionnaireId,
|
|
482
|
+
answers,
|
|
483
|
+
}
|
|
484
|
+
);
|
|
485
|
+
return response.data;
|
|
486
|
+
},
|
|
80
487
|
};
|
|
81
488
|
}
|
|
82
489
|
|
|
490
|
+
/**
|
|
491
|
+
* Parse user profile to include activity summary, streak, and inventory
|
|
492
|
+
* @private
|
|
493
|
+
* @param {Object} profile - Raw user profile from API
|
|
494
|
+
* @returns {Object} Parsed user profile
|
|
495
|
+
*/
|
|
496
|
+
_parseUserProfile(profile) {
|
|
497
|
+
if (!profile) return profile;
|
|
498
|
+
|
|
499
|
+
const parsed = { ...profile };
|
|
500
|
+
|
|
501
|
+
// Parse activity summary if present
|
|
502
|
+
if (profile.activity_summary) {
|
|
503
|
+
parsed.activitySummary = {
|
|
504
|
+
lastEventAt: profile.activity_summary.last_event_at
|
|
505
|
+
? new Date(profile.activity_summary.last_event_at)
|
|
506
|
+
: null,
|
|
507
|
+
eventCount: profile.activity_summary.event_count || 0,
|
|
508
|
+
daysActive: profile.activity_summary.days_active || 0,
|
|
509
|
+
};
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
// Parse streak info if present
|
|
513
|
+
if (profile.streak) {
|
|
514
|
+
parsed.streak = {
|
|
515
|
+
currentStreak: profile.streak.current_streak || 0,
|
|
516
|
+
longestStreak: profile.streak.longest_streak || 0,
|
|
517
|
+
lastActivityAt: profile.streak.last_activity_at
|
|
518
|
+
? new Date(profile.streak.last_activity_at)
|
|
519
|
+
: null,
|
|
520
|
+
streakStartedAt: profile.streak.streak_started_at
|
|
521
|
+
? new Date(profile.streak.streak_started_at)
|
|
522
|
+
: null,
|
|
523
|
+
};
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
// Parse inventory summary if present
|
|
527
|
+
if (profile.inventory) {
|
|
528
|
+
parsed.inventory = {
|
|
529
|
+
itemCount: profile.inventory.item_count || 0,
|
|
530
|
+
activeEffects: profile.inventory.active_effects || [],
|
|
531
|
+
};
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
return parsed;
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
/**
|
|
538
|
+
* Leaderboards module for ranking queries
|
|
539
|
+
*/
|
|
83
540
|
get leaderboards() {
|
|
541
|
+
const self = this;
|
|
542
|
+
|
|
543
|
+
/**
|
|
544
|
+
* Build filter query parameters from options
|
|
545
|
+
* @private
|
|
546
|
+
* @param {Object} options - Filter options
|
|
547
|
+
* @returns {Object} Query parameters
|
|
548
|
+
*/
|
|
549
|
+
const buildFilterParams = (options = {}) => {
|
|
550
|
+
const params = {};
|
|
551
|
+
|
|
552
|
+
// Pagination
|
|
553
|
+
if (options.page !== undefined) params.page = options.page;
|
|
554
|
+
if (options.limit !== undefined) params.limit = options.limit;
|
|
555
|
+
if (options.search !== undefined && options.search !== null) params.search = options.search;
|
|
556
|
+
|
|
557
|
+
// Persona filter
|
|
558
|
+
if (options.persona !== undefined && options.persona !== null) {
|
|
559
|
+
params.persona = options.persona;
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
// Level range filters
|
|
563
|
+
if (options.minLevel !== undefined && options.minLevel !== null) {
|
|
564
|
+
params.min_level = options.minLevel;
|
|
565
|
+
}
|
|
566
|
+
if (options.maxLevel !== undefined && options.maxLevel !== null) {
|
|
567
|
+
params.max_level = options.maxLevel;
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
// Date range filters (convert to ISO 8601)
|
|
571
|
+
if (options.startDate !== undefined && options.startDate !== null) {
|
|
572
|
+
const startDate = options.startDate instanceof Date
|
|
573
|
+
? options.startDate
|
|
574
|
+
: new Date(options.startDate);
|
|
575
|
+
params.start_date = startDate.toISOString();
|
|
576
|
+
}
|
|
577
|
+
if (options.endDate !== undefined && options.endDate !== null) {
|
|
578
|
+
const endDate = options.endDate instanceof Date
|
|
579
|
+
? options.endDate
|
|
580
|
+
: new Date(options.endDate);
|
|
581
|
+
params.end_date = endDate.toISOString();
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
// Timeframe
|
|
585
|
+
if (options.timeframe !== undefined) params.timeframe = options.timeframe;
|
|
586
|
+
|
|
587
|
+
return params;
|
|
588
|
+
};
|
|
589
|
+
|
|
590
|
+
/**
|
|
591
|
+
* Parse leaderboard response to include cache metadata and percentile ranks
|
|
592
|
+
* @private
|
|
593
|
+
* @param {Object} response - Raw API response
|
|
594
|
+
* @returns {Object} Parsed leaderboard response
|
|
595
|
+
*/
|
|
596
|
+
const parseLeaderboardResponse = (response) => {
|
|
597
|
+
const parsed = { ...response };
|
|
598
|
+
|
|
599
|
+
// Parse cache metadata if present
|
|
600
|
+
if (response.cache_metadata || response.cacheMetadata) {
|
|
601
|
+
const cacheData = response.cache_metadata || response.cacheMetadata;
|
|
602
|
+
parsed.cacheMetadata = {
|
|
603
|
+
cachedAt: cacheData.cached_at ? new Date(cacheData.cached_at) : null,
|
|
604
|
+
ttl: cacheData.ttl || 0,
|
|
605
|
+
};
|
|
606
|
+
// Remove snake_case version
|
|
607
|
+
delete parsed.cache_metadata;
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
// Parse rankings to include percentile if present
|
|
611
|
+
if (parsed.rankings && Array.isArray(parsed.rankings)) {
|
|
612
|
+
parsed.rankings = parsed.rankings.map(entry => ({
|
|
613
|
+
...entry,
|
|
614
|
+
percentile: entry.percentile !== undefined ? entry.percentile : null,
|
|
615
|
+
}));
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
return parsed;
|
|
619
|
+
};
|
|
620
|
+
|
|
84
621
|
return {
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
622
|
+
/**
|
|
623
|
+
* Get global leaderboard with optional filters
|
|
624
|
+
* @param {string|Object} [timeframeOrOptions='all-time'] - Timeframe string or options object
|
|
625
|
+
* @param {number} [page=1] - Page number (deprecated, use options object)
|
|
626
|
+
* @param {number} [limit=50] - Items per page (deprecated, use options object)
|
|
627
|
+
* @param {Object} [options] - Additional filter options
|
|
628
|
+
* @param {string} [options.timeframe='all-time'] - Timeframe (all-time, weekly, monthly)
|
|
629
|
+
* @param {number} [options.page=1] - Page number
|
|
630
|
+
* @param {number} [options.limit=50] - Items per page
|
|
631
|
+
* @param {string} [options.persona] - Filter by persona
|
|
632
|
+
* @param {number} [options.minLevel] - Minimum level filter
|
|
633
|
+
* @param {number} [options.maxLevel] - Maximum level filter
|
|
634
|
+
* @param {Date|string} [options.startDate] - Start date filter (ISO 8601)
|
|
635
|
+
* @param {Date|string} [options.endDate] - End date filter (ISO 8601)
|
|
636
|
+
* @returns {Promise<Object>} Leaderboard response with rankings, stats, pagination, and cache metadata
|
|
637
|
+
*/
|
|
638
|
+
getGlobal: async (timeframeOrOptions = 'all-time', page = 1, limit = 50, options = {}) => {
|
|
639
|
+
let params;
|
|
640
|
+
|
|
641
|
+
// Support both legacy signature and new options object
|
|
642
|
+
if (typeof timeframeOrOptions === 'object') {
|
|
643
|
+
// New signature: getGlobal(options)
|
|
644
|
+
params = buildFilterParams({
|
|
645
|
+
timeframe: 'all-time',
|
|
646
|
+
page: 1,
|
|
647
|
+
limit: 50,
|
|
648
|
+
...timeframeOrOptions,
|
|
649
|
+
});
|
|
650
|
+
} else {
|
|
651
|
+
// Legacy signature: getGlobal(timeframe, page, limit, options)
|
|
652
|
+
params = buildFilterParams({
|
|
653
|
+
timeframe: timeframeOrOptions,
|
|
654
|
+
page,
|
|
655
|
+
limit,
|
|
656
|
+
...options,
|
|
657
|
+
});
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
const response = await self._httpClient.get('/leaderboards/global', params);
|
|
661
|
+
return parseLeaderboardResponse(response.data);
|
|
662
|
+
},
|
|
663
|
+
|
|
664
|
+
/**
|
|
665
|
+
* List all leaderboards
|
|
666
|
+
* @param {number|Object} [pageOrOptions=1] - Page number or options object
|
|
667
|
+
* @param {number} [limit=50] - Items per page (deprecated, use options object)
|
|
668
|
+
* @param {string} [search=null] - Search query (deprecated, use options object)
|
|
669
|
+
* @returns {Promise<Object>} Leaderboards list
|
|
670
|
+
*/
|
|
671
|
+
list: async (pageOrOptions = 1, limit = 50, search = null) => {
|
|
672
|
+
let params;
|
|
673
|
+
|
|
674
|
+
// Support both legacy signature and new options object
|
|
675
|
+
if (typeof pageOrOptions === 'object') {
|
|
676
|
+
params = {
|
|
677
|
+
page: pageOrOptions.page || 1,
|
|
678
|
+
limit: pageOrOptions.limit || 50,
|
|
679
|
+
};
|
|
680
|
+
if (pageOrOptions.search !== undefined && pageOrOptions.search !== null) {
|
|
681
|
+
params.search = pageOrOptions.search;
|
|
682
|
+
}
|
|
683
|
+
} else {
|
|
684
|
+
params = { page: pageOrOptions, limit };
|
|
685
|
+
if (search !== null) params.search = search;
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
const response = await self._httpClient.get('/leaderboards', params);
|
|
689
|
+
return response.data;
|
|
690
|
+
},
|
|
691
|
+
|
|
692
|
+
/**
|
|
693
|
+
* Get custom leaderboard with optional filters
|
|
694
|
+
* @param {string} leaderboardId - Leaderboard ID
|
|
695
|
+
* @param {number|Object} [pageOrOptions=1] - Page number or options object
|
|
696
|
+
* @param {number} [limit=50] - Items per page (deprecated, use options object)
|
|
697
|
+
* @param {string} [search=null] - Search query (deprecated, use options object)
|
|
698
|
+
* @param {Object} [options] - Additional filter options
|
|
699
|
+
* @param {number} [options.page=1] - Page number
|
|
700
|
+
* @param {number} [options.limit=50] - Items per page
|
|
701
|
+
* @param {string} [options.search] - Search query
|
|
702
|
+
* @param {string} [options.persona] - Filter by persona
|
|
703
|
+
* @param {number} [options.minLevel] - Minimum level filter
|
|
704
|
+
* @param {number} [options.maxLevel] - Maximum level filter
|
|
705
|
+
* @param {Date|string} [options.startDate] - Start date filter (ISO 8601)
|
|
706
|
+
* @param {Date|string} [options.endDate] - End date filter (ISO 8601)
|
|
707
|
+
* @returns {Promise<Object>} Leaderboard response with rankings, stats, pagination, and cache metadata
|
|
708
|
+
*/
|
|
709
|
+
getCustom: async (leaderboardId, pageOrOptions = 1, limit = 50, search = null, options = {}) => {
|
|
710
|
+
let params;
|
|
711
|
+
|
|
712
|
+
// Support both legacy signature and new options object
|
|
713
|
+
if (typeof pageOrOptions === 'object') {
|
|
714
|
+
// New signature: getCustom(leaderboardId, options)
|
|
715
|
+
params = buildFilterParams({
|
|
716
|
+
page: 1,
|
|
717
|
+
limit: 50,
|
|
718
|
+
...pageOrOptions,
|
|
719
|
+
});
|
|
720
|
+
} else {
|
|
721
|
+
// Legacy signature: getCustom(leaderboardId, page, limit, search, options)
|
|
722
|
+
params = buildFilterParams({
|
|
723
|
+
page: pageOrOptions,
|
|
724
|
+
limit,
|
|
725
|
+
search,
|
|
726
|
+
...options,
|
|
727
|
+
});
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
const response = await self._httpClient.get(
|
|
731
|
+
`/leaderboards/${encodeURIComponent(leaderboardId)}`,
|
|
732
|
+
params
|
|
733
|
+
);
|
|
734
|
+
return parseLeaderboardResponse(response.data);
|
|
735
|
+
},
|
|
736
|
+
|
|
737
|
+
/**
|
|
738
|
+
* Get user rank in leaderboard
|
|
739
|
+
* @param {string} leaderboardId - Leaderboard ID
|
|
740
|
+
* @param {string} userId - User ID
|
|
741
|
+
* @returns {Promise<Object>} User rank response with rank, score, and percentile
|
|
742
|
+
*/
|
|
743
|
+
getUserRank: async (leaderboardId, userId) => {
|
|
744
|
+
const response = await self._httpClient.get(
|
|
745
|
+
`/leaderboards/${encodeURIComponent(leaderboardId)}/users/${encodeURIComponent(userId)}/rank`
|
|
746
|
+
);
|
|
93
747
|
|
|
94
|
-
|
|
95
|
-
|
|
748
|
+
// Include percentile in response if available
|
|
749
|
+
const data = response.data;
|
|
750
|
+
return {
|
|
751
|
+
...data,
|
|
752
|
+
percentile: data.percentile !== undefined ? data.percentile : null,
|
|
753
|
+
};
|
|
754
|
+
},
|
|
755
|
+
|
|
756
|
+
/**
|
|
757
|
+
* Get leaderboard entries around a specific user ("around me" view)
|
|
758
|
+
* @param {string} leaderboardId - Leaderboard ID
|
|
759
|
+
* @param {string} userId - User ID to center the view around
|
|
760
|
+
* @param {number} [range=5] - Number of entries above and below the user
|
|
761
|
+
* @returns {Promise<Object>} Leaderboard entries around the user with user's rank and percentile
|
|
762
|
+
*/
|
|
763
|
+
getAroundUser: async (leaderboardId, userId, range = 5) => {
|
|
764
|
+
const response = await self._httpClient.get(
|
|
765
|
+
`/leaderboards/${encodeURIComponent(leaderboardId)}/users/${encodeURIComponent(userId)}/around`,
|
|
766
|
+
{ range }
|
|
767
|
+
);
|
|
768
|
+
return parseLeaderboardResponse(response.data);
|
|
769
|
+
},
|
|
96
770
|
};
|
|
97
771
|
}
|
|
98
772
|
|
|
773
|
+
/**
|
|
774
|
+
* Badges module for badge queries
|
|
775
|
+
*/
|
|
99
776
|
get badges() {
|
|
100
777
|
return {
|
|
101
|
-
|
|
102
|
-
|
|
778
|
+
/**
|
|
779
|
+
* List all badges
|
|
780
|
+
* @param {number} [page=1] - Page number
|
|
781
|
+
* @param {number} [limit=50] - Items per page
|
|
782
|
+
* @param {boolean} [activeOnly=false] - Only active badges
|
|
783
|
+
* @returns {Promise<Object>} Badges list
|
|
784
|
+
*/
|
|
785
|
+
list: async (page = 1, limit = 50, activeOnly = false) => {
|
|
786
|
+
const response = await this._httpClient.get('/badges', {
|
|
787
|
+
page,
|
|
788
|
+
limit,
|
|
789
|
+
active_only: activeOnly,
|
|
790
|
+
});
|
|
791
|
+
return response.data;
|
|
792
|
+
},
|
|
103
793
|
};
|
|
104
794
|
}
|
|
105
795
|
|
|
796
|
+
/**
|
|
797
|
+
* Levels module for level queries
|
|
798
|
+
*/
|
|
106
799
|
get levels() {
|
|
107
800
|
return {
|
|
108
|
-
|
|
109
|
-
|
|
801
|
+
/**
|
|
802
|
+
* List all levels
|
|
803
|
+
* @param {number} [page=1] - Page number
|
|
804
|
+
* @param {number} [limit=50] - Items per page
|
|
805
|
+
* @returns {Promise<Object>} Levels list
|
|
806
|
+
*/
|
|
807
|
+
list: async (page = 1, limit = 50) => {
|
|
808
|
+
const response = await this._httpClient.get('/levels', { page, limit });
|
|
809
|
+
return response.data;
|
|
810
|
+
},
|
|
110
811
|
};
|
|
111
812
|
}
|
|
112
813
|
|
|
814
|
+
/**
|
|
815
|
+
* Questionnaires module for questionnaire queries
|
|
816
|
+
*/
|
|
113
817
|
get questionnaires() {
|
|
114
818
|
return {
|
|
115
|
-
|
|
116
|
-
|
|
819
|
+
/**
|
|
820
|
+
* Get questionnaire by slug
|
|
821
|
+
* @param {string} slug - Questionnaire slug
|
|
822
|
+
* @returns {Promise<Object>} Questionnaire data
|
|
823
|
+
*/
|
|
824
|
+
get: async (slug) => {
|
|
825
|
+
const response = await this._httpClient.get(`/questionnaires/${slug}`);
|
|
826
|
+
return response.data;
|
|
827
|
+
},
|
|
828
|
+
|
|
829
|
+
/**
|
|
830
|
+
* Get active questionnaire
|
|
831
|
+
* @returns {Promise<Object>} Active questionnaire
|
|
832
|
+
*/
|
|
833
|
+
getActive: async () => {
|
|
834
|
+
const response = await this._httpClient.get('/questionnaires/active');
|
|
835
|
+
return response.data;
|
|
836
|
+
},
|
|
117
837
|
};
|
|
118
838
|
}
|
|
119
839
|
|
|
840
|
+
/**
|
|
841
|
+
* Aha moment module for engagement tracking
|
|
842
|
+
*/
|
|
120
843
|
get aha() {
|
|
121
844
|
return {
|
|
122
|
-
|
|
845
|
+
/**
|
|
846
|
+
* Declare aha moment score
|
|
847
|
+
* @param {string} userId - User ID
|
|
848
|
+
* @param {number} value - Score value (1-5)
|
|
849
|
+
* @returns {Promise<Object>} Declaration response
|
|
850
|
+
*/
|
|
851
|
+
declare: async (userId, value) => {
|
|
123
852
|
// Validate value is between 1 and 5
|
|
124
853
|
if (!Number.isInteger(value) || value < 1 || value > 5) {
|
|
125
|
-
throw new
|
|
854
|
+
throw new ValidationError('Aha score value must be an integer between 1 and 5', {
|
|
855
|
+
code: 'INVALID_AHA_VALUE',
|
|
856
|
+
fieldErrors: [{ field: 'value', message: 'value must be an integer between 1 and 5' }],
|
|
857
|
+
});
|
|
126
858
|
}
|
|
127
|
-
|
|
859
|
+
const response = await this._httpClient.post('/aha/declare', {
|
|
128
860
|
user_id: userId,
|
|
129
861
|
value,
|
|
130
862
|
});
|
|
863
|
+
return response.data;
|
|
864
|
+
},
|
|
865
|
+
|
|
866
|
+
/**
|
|
867
|
+
* Get user aha score
|
|
868
|
+
* @param {string} userId - User ID
|
|
869
|
+
* @returns {Promise<Object>} User aha score
|
|
870
|
+
*/
|
|
871
|
+
getUserScore: async (userId) => {
|
|
872
|
+
const response = await this._httpClient.get(`/users/${encodeURIComponent(userId)}/aha`);
|
|
873
|
+
return response.data;
|
|
874
|
+
},
|
|
875
|
+
};
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
/**
|
|
879
|
+
* Health module for API health checks
|
|
880
|
+
*/
|
|
881
|
+
get health() {
|
|
882
|
+
return {
|
|
883
|
+
/**
|
|
884
|
+
* Check API health status
|
|
885
|
+
* @returns {Promise<Object>} Health status
|
|
886
|
+
*/
|
|
887
|
+
check: async () => {
|
|
888
|
+
const response = await this._httpClient.get('/health');
|
|
889
|
+
return response.data;
|
|
131
890
|
},
|
|
132
891
|
|
|
133
|
-
|
|
892
|
+
/**
|
|
893
|
+
* Quick availability check
|
|
894
|
+
* @returns {Promise<boolean>} True if API is ready
|
|
895
|
+
*/
|
|
896
|
+
isReady: async () => {
|
|
897
|
+
try {
|
|
898
|
+
await this._httpClient.get('/health');
|
|
899
|
+
return true;
|
|
900
|
+
} catch {
|
|
901
|
+
return false;
|
|
902
|
+
}
|
|
903
|
+
},
|
|
134
904
|
};
|
|
135
905
|
}
|
|
136
906
|
}
|
|
137
907
|
|
|
908
|
+
// Export error classes for external use
|
|
909
|
+
export {
|
|
910
|
+
RooguysError,
|
|
911
|
+
ValidationError,
|
|
912
|
+
AuthenticationError,
|
|
913
|
+
ForbiddenError,
|
|
914
|
+
NotFoundError,
|
|
915
|
+
ConflictError,
|
|
916
|
+
RateLimitError,
|
|
917
|
+
ServerError,
|
|
918
|
+
mapStatusToError,
|
|
919
|
+
};
|
|
920
|
+
|
|
921
|
+
// Export utility functions
|
|
922
|
+
export { extractRateLimitInfo, extractRequestId, parseResponseBody };
|
|
923
|
+
|
|
924
|
+
// Export HttpClient for advanced usage
|
|
925
|
+
export { HttpClient };
|
|
926
|
+
|
|
927
|
+
// Default export
|
|
138
928
|
export default Rooguys;
|