@rooguys/sdk 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 +478 -113
- package/dist/__tests__/utils/mockClient.d.ts +65 -3
- package/dist/__tests__/utils/mockClient.js +144 -5
- package/dist/errors.d.ts +123 -0
- package/dist/errors.js +163 -0
- package/dist/http-client.d.ts +167 -0
- package/dist/http-client.js +250 -0
- package/dist/index.d.ts +160 -10
- package/dist/index.js +585 -146
- package/dist/types.d.ts +372 -50
- package/dist/types.js +21 -0
- package/package.json +1 -1
- package/src/__tests__/property/request-construction.property.test.ts +142 -91
- package/src/__tests__/property/response-parsing.property.test.ts +118 -67
- package/src/__tests__/property/sdk-modules.property.test.ts +450 -0
- package/src/__tests__/unit/aha.test.ts +61 -50
- package/src/__tests__/unit/badges.test.ts +27 -33
- package/src/__tests__/unit/config.test.ts +94 -126
- package/src/__tests__/unit/errors.test.ts +106 -150
- package/src/__tests__/unit/events.test.ts +119 -144
- package/src/__tests__/unit/leaderboards.test.ts +173 -40
- package/src/__tests__/unit/levels.test.ts +25 -33
- package/src/__tests__/unit/questionnaires.test.ts +33 -42
- package/src/__tests__/unit/users.test.ts +214 -99
- package/src/__tests__/utils/mockClient.ts +193 -6
- package/src/errors.ts +255 -0
- package/src/http-client.ts +433 -0
- package/src/index.ts +742 -150
- package/src/types.ts +429 -51
package/dist/index.js
CHANGED
|
@@ -1,222 +1,661 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
|
|
3
|
-
|
|
2
|
+
/**
|
|
3
|
+
* Rooguys Node.js SDK
|
|
4
|
+
* Official TypeScript SDK for the Rooguys Gamification Platform
|
|
5
|
+
*/
|
|
6
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
7
|
+
if (k2 === undefined) k2 = k;
|
|
8
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
9
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
10
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
11
|
+
}
|
|
12
|
+
Object.defineProperty(o, k2, desc);
|
|
13
|
+
}) : (function(o, m, k, k2) {
|
|
14
|
+
if (k2 === undefined) k2 = k;
|
|
15
|
+
o[k2] = m[k];
|
|
16
|
+
}));
|
|
17
|
+
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
18
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
4
19
|
};
|
|
5
20
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.Rooguys = void 0;
|
|
7
|
-
const
|
|
21
|
+
exports.parseResponseBody = exports.extractRequestId = exports.extractRateLimitInfo = exports.HttpClient = exports.mapStatusToError = exports.ServerError = exports.RateLimitError = exports.ConflictError = exports.NotFoundError = exports.ForbiddenError = exports.AuthenticationError = exports.ValidationError = exports.RooguysError = exports.Rooguys = void 0;
|
|
22
|
+
const http_client_1 = require("./http-client");
|
|
23
|
+
const errors_1 = require("./errors");
|
|
24
|
+
/**
|
|
25
|
+
* Main Rooguys SDK class
|
|
26
|
+
*/
|
|
8
27
|
class Rooguys {
|
|
28
|
+
/**
|
|
29
|
+
* Create a new Rooguys SDK instance
|
|
30
|
+
* @param apiKey - API key for authentication
|
|
31
|
+
* @param options - Configuration options
|
|
32
|
+
*/
|
|
9
33
|
constructor(apiKey, options = {}) {
|
|
10
|
-
|
|
34
|
+
/**
|
|
35
|
+
* Events module for tracking user events
|
|
36
|
+
*/
|
|
11
37
|
this.events = {
|
|
38
|
+
/**
|
|
39
|
+
* Track a single event
|
|
40
|
+
*/
|
|
12
41
|
track: async (eventName, userId, properties = {}, options = {}) => {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
42
|
+
const body = {
|
|
43
|
+
event_name: eventName,
|
|
44
|
+
user_id: userId,
|
|
45
|
+
properties,
|
|
46
|
+
};
|
|
47
|
+
// Add custom timestamp if provided
|
|
48
|
+
if (options.timestamp) {
|
|
49
|
+
const timestamp = options.timestamp instanceof Date ? options.timestamp : new Date(options.timestamp);
|
|
50
|
+
const sevenDaysAgo = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000);
|
|
51
|
+
if (timestamp < sevenDaysAgo) {
|
|
52
|
+
throw new errors_1.ValidationError('Custom timestamp cannot be more than 7 days in the past', {
|
|
53
|
+
code: 'TIMESTAMP_TOO_OLD',
|
|
54
|
+
fieldErrors: [{ field: 'timestamp', message: 'Timestamp must be within the last 7 days' }],
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
body.timestamp = timestamp.toISOString();
|
|
58
|
+
}
|
|
59
|
+
const response = await this._httpClient.post('/events', body, {
|
|
60
|
+
params: { include_profile: options.includeProfile },
|
|
61
|
+
idempotencyKey: options.idempotencyKey,
|
|
62
|
+
});
|
|
63
|
+
return response.data;
|
|
64
|
+
},
|
|
65
|
+
/**
|
|
66
|
+
* Track multiple events in a single request (batch operation)
|
|
67
|
+
*/
|
|
68
|
+
trackBatch: async (events, options = {}) => {
|
|
69
|
+
// Validate array
|
|
70
|
+
if (!Array.isArray(events)) {
|
|
71
|
+
throw new errors_1.ValidationError('Events must be an array', {
|
|
72
|
+
code: 'INVALID_EVENTS',
|
|
73
|
+
fieldErrors: [{ field: 'events', message: 'Events must be an array' }],
|
|
22
74
|
});
|
|
23
|
-
return response.data;
|
|
24
75
|
}
|
|
25
|
-
|
|
26
|
-
|
|
76
|
+
// Validate array length
|
|
77
|
+
if (events.length === 0) {
|
|
78
|
+
throw new errors_1.ValidationError('Events array cannot be empty', {
|
|
79
|
+
code: 'EMPTY_EVENTS',
|
|
80
|
+
fieldErrors: [{ field: 'events', message: 'At least one event is required' }],
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
if (events.length > 100) {
|
|
84
|
+
throw new errors_1.ValidationError('Batch size exceeds maximum of 100 events', {
|
|
85
|
+
code: 'BATCH_TOO_LARGE',
|
|
86
|
+
fieldErrors: [{ field: 'events', message: 'Maximum batch size is 100 events' }],
|
|
87
|
+
});
|
|
27
88
|
}
|
|
89
|
+
const sevenDaysAgo = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000);
|
|
90
|
+
// Transform and validate events
|
|
91
|
+
const transformedEvents = events.map((event, index) => {
|
|
92
|
+
const transformed = {
|
|
93
|
+
event_name: event.eventName,
|
|
94
|
+
user_id: event.userId,
|
|
95
|
+
properties: event.properties || {},
|
|
96
|
+
};
|
|
97
|
+
// Validate and add custom timestamp if provided
|
|
98
|
+
if (event.timestamp) {
|
|
99
|
+
const timestamp = event.timestamp instanceof Date ? event.timestamp : new Date(event.timestamp);
|
|
100
|
+
if (timestamp < sevenDaysAgo) {
|
|
101
|
+
throw new errors_1.ValidationError(`Event at index ${index}: Custom timestamp cannot be more than 7 days in the past`, {
|
|
102
|
+
code: 'TIMESTAMP_TOO_OLD',
|
|
103
|
+
fieldErrors: [{ field: `events[${index}].timestamp`, message: 'Timestamp must be within the last 7 days' }],
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
transformed.timestamp = timestamp.toISOString();
|
|
107
|
+
}
|
|
108
|
+
return transformed;
|
|
109
|
+
});
|
|
110
|
+
const response = await this._httpClient.post('/events/batch', {
|
|
111
|
+
events: transformedEvents,
|
|
112
|
+
}, {
|
|
113
|
+
idempotencyKey: options.idempotencyKey,
|
|
114
|
+
});
|
|
115
|
+
return response.data;
|
|
116
|
+
},
|
|
117
|
+
/**
|
|
118
|
+
* @deprecated Use track() instead. The /v1/event endpoint is deprecated.
|
|
119
|
+
*/
|
|
120
|
+
trackLegacy: async (eventName, userId, properties = {}, options = {}) => {
|
|
121
|
+
console.warn('DEPRECATION WARNING: events.trackLegacy() uses the deprecated /v1/event endpoint. Please use events.track() instead which uses /v1/events.');
|
|
122
|
+
const response = await this._httpClient.post('/event', {
|
|
123
|
+
event_name: eventName,
|
|
124
|
+
user_id: userId,
|
|
125
|
+
properties,
|
|
126
|
+
}, {
|
|
127
|
+
params: { include_profile: options.includeProfile },
|
|
128
|
+
});
|
|
129
|
+
return response.data;
|
|
28
130
|
},
|
|
29
131
|
};
|
|
132
|
+
/**
|
|
133
|
+
* Users module for user management and queries
|
|
134
|
+
*/
|
|
30
135
|
this.users = {
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
136
|
+
/**
|
|
137
|
+
* Create a new user
|
|
138
|
+
*/
|
|
139
|
+
create: async (userData) => {
|
|
140
|
+
// Validate required fields
|
|
141
|
+
if (!userData || !userData.userId) {
|
|
142
|
+
throw new errors_1.ValidationError('User ID is required', {
|
|
143
|
+
code: 'MISSING_USER_ID',
|
|
144
|
+
fieldErrors: [{ field: 'userId', message: 'User ID is required' }],
|
|
145
|
+
});
|
|
35
146
|
}
|
|
36
|
-
|
|
37
|
-
|
|
147
|
+
// Validate email format if provided
|
|
148
|
+
if (userData.email && !this.isValidEmail(userData.email)) {
|
|
149
|
+
throw new errors_1.ValidationError('Invalid email format', {
|
|
150
|
+
code: 'INVALID_EMAIL',
|
|
151
|
+
fieldErrors: [{ field: 'email', message: 'Email must be a valid email address' }],
|
|
152
|
+
});
|
|
38
153
|
}
|
|
154
|
+
const body = this.buildUserRequestBody(userData);
|
|
155
|
+
const response = await this._httpClient.post('/users', body);
|
|
156
|
+
return this.parseUserProfile(response.data);
|
|
39
157
|
},
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
158
|
+
/**
|
|
159
|
+
* Update an existing user
|
|
160
|
+
*/
|
|
161
|
+
update: async (userId, userData) => {
|
|
162
|
+
// Validate user ID
|
|
163
|
+
if (!userId) {
|
|
164
|
+
throw new errors_1.ValidationError('User ID is required', {
|
|
165
|
+
code: 'MISSING_USER_ID',
|
|
166
|
+
fieldErrors: [{ field: 'userId', message: 'User ID is required' }],
|
|
167
|
+
});
|
|
44
168
|
}
|
|
45
|
-
|
|
46
|
-
|
|
169
|
+
// Validate email format if provided
|
|
170
|
+
if (userData.email && !this.isValidEmail(userData.email)) {
|
|
171
|
+
throw new errors_1.ValidationError('Invalid email format', {
|
|
172
|
+
code: 'INVALID_EMAIL',
|
|
173
|
+
fieldErrors: [{ field: 'email', message: 'Email must be a valid email address' }],
|
|
174
|
+
});
|
|
47
175
|
}
|
|
176
|
+
const body = this.buildUserRequestBody(userData);
|
|
177
|
+
const response = await this._httpClient.patch(`/users/${encodeURIComponent(userId)}`, body);
|
|
178
|
+
return this.parseUserProfile(response.data);
|
|
48
179
|
},
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
throw
|
|
180
|
+
/**
|
|
181
|
+
* Create multiple users in a single request (batch operation)
|
|
182
|
+
*/
|
|
183
|
+
createBatch: async (users) => {
|
|
184
|
+
// Validate array
|
|
185
|
+
if (!Array.isArray(users)) {
|
|
186
|
+
throw new errors_1.ValidationError('Users must be an array', {
|
|
187
|
+
code: 'INVALID_USERS',
|
|
188
|
+
fieldErrors: [{ field: 'users', message: 'Users must be an array' }],
|
|
189
|
+
});
|
|
56
190
|
}
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
191
|
+
// Validate array length
|
|
192
|
+
if (users.length === 0) {
|
|
193
|
+
throw new errors_1.ValidationError('Users array cannot be empty', {
|
|
194
|
+
code: 'EMPTY_USERS',
|
|
195
|
+
fieldErrors: [{ field: 'users', message: 'At least one user is required' }],
|
|
62
196
|
});
|
|
63
|
-
return response.data;
|
|
64
197
|
}
|
|
65
|
-
|
|
66
|
-
throw
|
|
198
|
+
if (users.length > 100) {
|
|
199
|
+
throw new errors_1.ValidationError('Batch size exceeds maximum of 100 users', {
|
|
200
|
+
code: 'BATCH_TOO_LARGE',
|
|
201
|
+
fieldErrors: [{ field: 'users', message: 'Maximum batch size is 100 users' }],
|
|
202
|
+
});
|
|
67
203
|
}
|
|
204
|
+
// Validate and transform each user
|
|
205
|
+
const transformedUsers = users.map((user, index) => {
|
|
206
|
+
if (!user.userId) {
|
|
207
|
+
throw new errors_1.ValidationError(`User at index ${index}: User ID is required`, {
|
|
208
|
+
code: 'MISSING_USER_ID',
|
|
209
|
+
fieldErrors: [{ field: `users[${index}].userId`, message: 'User ID is required' }],
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
if (user.email && !this.isValidEmail(user.email)) {
|
|
213
|
+
throw new errors_1.ValidationError(`User at index ${index}: Invalid email format`, {
|
|
214
|
+
code: 'INVALID_EMAIL',
|
|
215
|
+
fieldErrors: [{ field: `users[${index}].email`, message: 'Email must be a valid email address' }],
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
return this.buildUserRequestBody(user);
|
|
219
|
+
});
|
|
220
|
+
const response = await this._httpClient.post('/users/batch', { users: transformedUsers });
|
|
221
|
+
return response.data;
|
|
222
|
+
},
|
|
223
|
+
/**
|
|
224
|
+
* Get user profile with optional field selection
|
|
225
|
+
*/
|
|
226
|
+
get: async (userId, options = {}) => {
|
|
227
|
+
const params = {};
|
|
228
|
+
// Add field selection if provided
|
|
229
|
+
if (options.fields && Array.isArray(options.fields) && options.fields.length > 0) {
|
|
230
|
+
params.fields = options.fields.join(',');
|
|
231
|
+
}
|
|
232
|
+
const response = await this._httpClient.get(`/users/${encodeURIComponent(userId)}`, params);
|
|
233
|
+
return this.parseUserProfile(response.data);
|
|
234
|
+
},
|
|
235
|
+
/**
|
|
236
|
+
* Search users with pagination
|
|
237
|
+
*/
|
|
238
|
+
search: async (query, options = {}) => {
|
|
239
|
+
const params = {
|
|
240
|
+
q: query,
|
|
241
|
+
page: options.page || 1,
|
|
242
|
+
limit: options.limit || 50,
|
|
243
|
+
};
|
|
244
|
+
// Add field selection if provided
|
|
245
|
+
if (options.fields && Array.isArray(options.fields) && options.fields.length > 0) {
|
|
246
|
+
params.fields = options.fields.join(',');
|
|
247
|
+
}
|
|
248
|
+
const response = await this._httpClient.get('/users/search', params);
|
|
249
|
+
return {
|
|
250
|
+
...response.data,
|
|
251
|
+
users: (response.data.users || []).map(user => this.parseUserProfile(user)),
|
|
252
|
+
};
|
|
253
|
+
},
|
|
254
|
+
/**
|
|
255
|
+
* Get multiple user profiles
|
|
256
|
+
*/
|
|
257
|
+
getBulk: async (userIds) => {
|
|
258
|
+
const response = await this._httpClient.post('/users/bulk', { user_ids: userIds });
|
|
259
|
+
return {
|
|
260
|
+
...response.data,
|
|
261
|
+
users: (response.data.users || []).map(user => this.parseUserProfile(user)),
|
|
262
|
+
};
|
|
263
|
+
},
|
|
264
|
+
/**
|
|
265
|
+
* Get user badges
|
|
266
|
+
*/
|
|
267
|
+
getBadges: async (userId) => {
|
|
268
|
+
const response = await this._httpClient.get(`/users/${encodeURIComponent(userId)}/badges`);
|
|
269
|
+
return response.data;
|
|
270
|
+
},
|
|
271
|
+
/**
|
|
272
|
+
* Get user rank
|
|
273
|
+
*/
|
|
274
|
+
getRank: async (userId, timeframe = 'all-time') => {
|
|
275
|
+
const response = await this._httpClient.get(`/users/${encodeURIComponent(userId)}/rank`, { timeframe });
|
|
276
|
+
return {
|
|
277
|
+
...response.data,
|
|
278
|
+
percentile: response.data.percentile !== undefined ? response.data.percentile : null,
|
|
279
|
+
};
|
|
68
280
|
},
|
|
281
|
+
/**
|
|
282
|
+
* Submit questionnaire answers
|
|
283
|
+
*/
|
|
69
284
|
submitAnswers: async (userId, questionnaireId, answers) => {
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
return response.data;
|
|
76
|
-
}
|
|
77
|
-
catch (error) {
|
|
78
|
-
throw this.handleError(error);
|
|
79
|
-
}
|
|
285
|
+
const response = await this._httpClient.post(`/users/${encodeURIComponent(userId)}/answers`, {
|
|
286
|
+
questionnaire_id: questionnaireId,
|
|
287
|
+
answers,
|
|
288
|
+
});
|
|
289
|
+
return response.data;
|
|
80
290
|
},
|
|
81
291
|
};
|
|
292
|
+
/**
|
|
293
|
+
* Leaderboards module for ranking queries
|
|
294
|
+
*/
|
|
82
295
|
this.leaderboards = {
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
296
|
+
/**
|
|
297
|
+
* Get global leaderboard with optional filters
|
|
298
|
+
*/
|
|
299
|
+
getGlobal: async (timeframeOrOptions = 'all-time', page = 1, limit = 50, options = {}) => {
|
|
300
|
+
let params;
|
|
301
|
+
// Support both legacy signature and new options object
|
|
302
|
+
if (typeof timeframeOrOptions === 'object') {
|
|
303
|
+
params = this.buildFilterParams({
|
|
304
|
+
timeframe: 'all-time',
|
|
305
|
+
page: 1,
|
|
306
|
+
limit: 50,
|
|
307
|
+
...timeframeOrOptions,
|
|
87
308
|
});
|
|
88
|
-
return response.data;
|
|
89
309
|
}
|
|
90
|
-
|
|
91
|
-
|
|
310
|
+
else {
|
|
311
|
+
params = this.buildFilterParams({
|
|
312
|
+
timeframe: timeframeOrOptions,
|
|
313
|
+
page,
|
|
314
|
+
limit,
|
|
315
|
+
...options,
|
|
316
|
+
});
|
|
92
317
|
}
|
|
318
|
+
const response = await this._httpClient.get('/leaderboards/global', params);
|
|
319
|
+
return this.parseLeaderboardResponse(response.data);
|
|
93
320
|
},
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
321
|
+
/**
|
|
322
|
+
* List all leaderboards
|
|
323
|
+
*/
|
|
324
|
+
list: async (pageOrOptions = 1, limit = 50, search = null) => {
|
|
325
|
+
let params;
|
|
326
|
+
if (typeof pageOrOptions === 'object') {
|
|
327
|
+
params = {
|
|
328
|
+
page: pageOrOptions.page || 1,
|
|
329
|
+
limit: pageOrOptions.limit || 50,
|
|
330
|
+
};
|
|
331
|
+
if (pageOrOptions.search !== undefined && pageOrOptions.search !== null) {
|
|
332
|
+
params.search = pageOrOptions.search;
|
|
99
333
|
}
|
|
100
|
-
const response = await this.client.get('/leaderboards', { params });
|
|
101
|
-
return response.data;
|
|
102
334
|
}
|
|
103
|
-
|
|
104
|
-
|
|
335
|
+
else {
|
|
336
|
+
params = { page: pageOrOptions, limit };
|
|
337
|
+
if (search !== null)
|
|
338
|
+
params.search = search;
|
|
105
339
|
}
|
|
340
|
+
const response = await this._httpClient.get('/leaderboards', params);
|
|
341
|
+
return response.data;
|
|
106
342
|
},
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
343
|
+
/**
|
|
344
|
+
* Get custom leaderboard with optional filters
|
|
345
|
+
*/
|
|
346
|
+
getCustom: async (leaderboardId, pageOrOptions = 1, limit = 50, search = null, options = {}) => {
|
|
347
|
+
let params;
|
|
348
|
+
if (typeof pageOrOptions === 'object') {
|
|
349
|
+
params = this.buildFilterParams({
|
|
350
|
+
page: 1,
|
|
351
|
+
limit: 50,
|
|
352
|
+
...pageOrOptions,
|
|
353
|
+
});
|
|
115
354
|
}
|
|
116
|
-
|
|
117
|
-
|
|
355
|
+
else {
|
|
356
|
+
params = this.buildFilterParams({
|
|
357
|
+
page: pageOrOptions,
|
|
358
|
+
limit,
|
|
359
|
+
search: search || undefined,
|
|
360
|
+
...options,
|
|
361
|
+
});
|
|
118
362
|
}
|
|
363
|
+
const response = await this._httpClient.get(`/leaderboards/${encodeURIComponent(leaderboardId)}`, params);
|
|
364
|
+
return this.parseLeaderboardResponse(response.data);
|
|
119
365
|
},
|
|
366
|
+
/**
|
|
367
|
+
* Get user rank in leaderboard
|
|
368
|
+
*/
|
|
120
369
|
getUserRank: async (leaderboardId, userId) => {
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
370
|
+
const response = await this._httpClient.get(`/leaderboards/${encodeURIComponent(leaderboardId)}/users/${encodeURIComponent(userId)}/rank`);
|
|
371
|
+
return {
|
|
372
|
+
...response.data,
|
|
373
|
+
percentile: response.data.percentile !== undefined ? response.data.percentile : null,
|
|
374
|
+
};
|
|
375
|
+
},
|
|
376
|
+
/**
|
|
377
|
+
* Get leaderboard entries around a specific user ("around me" view)
|
|
378
|
+
*/
|
|
379
|
+
getAroundUser: async (leaderboardId, userId, range = 5) => {
|
|
380
|
+
const response = await this._httpClient.get(`/leaderboards/${encodeURIComponent(leaderboardId)}/users/${encodeURIComponent(userId)}/around`, { range });
|
|
381
|
+
return this.parseLeaderboardResponse(response.data);
|
|
128
382
|
},
|
|
129
383
|
};
|
|
384
|
+
/**
|
|
385
|
+
* Badges module for badge queries
|
|
386
|
+
*/
|
|
130
387
|
this.badges = {
|
|
388
|
+
/**
|
|
389
|
+
* List all badges
|
|
390
|
+
*/
|
|
131
391
|
list: async (page = 1, limit = 50, activeOnly = false) => {
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
catch (error) {
|
|
139
|
-
throw this.handleError(error);
|
|
140
|
-
}
|
|
392
|
+
const response = await this._httpClient.get('/badges', {
|
|
393
|
+
page,
|
|
394
|
+
limit,
|
|
395
|
+
active_only: activeOnly,
|
|
396
|
+
});
|
|
397
|
+
return response.data;
|
|
141
398
|
},
|
|
142
399
|
};
|
|
400
|
+
/**
|
|
401
|
+
* Levels module for level queries
|
|
402
|
+
*/
|
|
143
403
|
this.levels = {
|
|
404
|
+
/**
|
|
405
|
+
* List all levels
|
|
406
|
+
*/
|
|
144
407
|
list: async (page = 1, limit = 50) => {
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
params: { page, limit },
|
|
148
|
-
});
|
|
149
|
-
return response.data;
|
|
150
|
-
}
|
|
151
|
-
catch (error) {
|
|
152
|
-
throw this.handleError(error);
|
|
153
|
-
}
|
|
408
|
+
const response = await this._httpClient.get('/levels', { page, limit });
|
|
409
|
+
return response.data;
|
|
154
410
|
},
|
|
155
411
|
};
|
|
412
|
+
/**
|
|
413
|
+
* Questionnaires module for questionnaire queries
|
|
414
|
+
*/
|
|
156
415
|
this.questionnaires = {
|
|
416
|
+
/**
|
|
417
|
+
* Get questionnaire by slug
|
|
418
|
+
*/
|
|
157
419
|
get: async (slug) => {
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
return response.data;
|
|
161
|
-
}
|
|
162
|
-
catch (error) {
|
|
163
|
-
throw this.handleError(error);
|
|
164
|
-
}
|
|
420
|
+
const response = await this._httpClient.get(`/questionnaires/${slug}`);
|
|
421
|
+
return response.data;
|
|
165
422
|
},
|
|
423
|
+
/**
|
|
424
|
+
* Get active questionnaire
|
|
425
|
+
*/
|
|
166
426
|
getActive: async () => {
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
return response.data;
|
|
170
|
-
}
|
|
171
|
-
catch (error) {
|
|
172
|
-
throw this.handleError(error);
|
|
173
|
-
}
|
|
427
|
+
const response = await this._httpClient.get('/questionnaires/active');
|
|
428
|
+
return response.data;
|
|
174
429
|
},
|
|
175
430
|
};
|
|
431
|
+
/**
|
|
432
|
+
* Aha moment module for engagement tracking
|
|
433
|
+
*/
|
|
176
434
|
this.aha = {
|
|
435
|
+
/**
|
|
436
|
+
* Declare aha moment score
|
|
437
|
+
*/
|
|
177
438
|
declare: async (userId, value) => {
|
|
178
439
|
// Validate value is between 1 and 5
|
|
179
440
|
if (!Number.isInteger(value) || value < 1 || value > 5) {
|
|
180
|
-
throw new
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
const response = await this.client.post('/aha/declare', {
|
|
184
|
-
user_id: userId,
|
|
185
|
-
value,
|
|
441
|
+
throw new errors_1.ValidationError('Aha score value must be an integer between 1 and 5', {
|
|
442
|
+
code: 'INVALID_AHA_VALUE',
|
|
443
|
+
fieldErrors: [{ field: 'value', message: 'value must be an integer between 1 and 5' }],
|
|
186
444
|
});
|
|
187
|
-
return response.data;
|
|
188
|
-
}
|
|
189
|
-
catch (error) {
|
|
190
|
-
throw this.handleError(error);
|
|
191
445
|
}
|
|
446
|
+
const response = await this._httpClient.post('/aha/declare', {
|
|
447
|
+
user_id: userId,
|
|
448
|
+
value,
|
|
449
|
+
});
|
|
450
|
+
return response.data;
|
|
192
451
|
},
|
|
452
|
+
/**
|
|
453
|
+
* Get user aha score
|
|
454
|
+
*/
|
|
193
455
|
getUserScore: async (userId) => {
|
|
456
|
+
const response = await this._httpClient.get(`/users/${encodeURIComponent(userId)}/aha`);
|
|
457
|
+
return response.data;
|
|
458
|
+
},
|
|
459
|
+
};
|
|
460
|
+
/**
|
|
461
|
+
* Health module for API health checks
|
|
462
|
+
*/
|
|
463
|
+
this.health = {
|
|
464
|
+
/**
|
|
465
|
+
* Check API health status
|
|
466
|
+
*/
|
|
467
|
+
check: async () => {
|
|
468
|
+
const response = await this._httpClient.get('/health');
|
|
469
|
+
return response.data;
|
|
470
|
+
},
|
|
471
|
+
/**
|
|
472
|
+
* Quick availability check
|
|
473
|
+
*/
|
|
474
|
+
isReady: async () => {
|
|
194
475
|
try {
|
|
195
|
-
|
|
196
|
-
return
|
|
476
|
+
await this._httpClient.get('/health');
|
|
477
|
+
return true;
|
|
197
478
|
}
|
|
198
|
-
catch (
|
|
199
|
-
|
|
479
|
+
catch (_a) {
|
|
480
|
+
return false;
|
|
200
481
|
}
|
|
201
482
|
},
|
|
202
483
|
};
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
484
|
+
if (!apiKey) {
|
|
485
|
+
throw new errors_1.ValidationError('API key is required', { code: 'MISSING_API_KEY' });
|
|
486
|
+
}
|
|
487
|
+
this.apiKey = apiKey;
|
|
488
|
+
this.baseUrl = options.baseUrl || 'https://api.rooguys.com/v1';
|
|
489
|
+
this.timeout = options.timeout || 10000;
|
|
490
|
+
// Initialize HTTP client
|
|
491
|
+
this._httpClient = new http_client_1.HttpClient(apiKey, {
|
|
492
|
+
baseUrl: this.baseUrl,
|
|
493
|
+
timeout: this.timeout,
|
|
494
|
+
onRateLimitWarning: options.onRateLimitWarning,
|
|
495
|
+
autoRetry: options.autoRetry,
|
|
496
|
+
maxRetries: options.maxRetries,
|
|
210
497
|
});
|
|
211
498
|
}
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
499
|
+
/**
|
|
500
|
+
* Validate email format client-side
|
|
501
|
+
*/
|
|
502
|
+
isValidEmail(email) {
|
|
503
|
+
if (!email || typeof email !== 'string')
|
|
504
|
+
return true; // Optional field
|
|
505
|
+
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
506
|
+
return emailRegex.test(email);
|
|
507
|
+
}
|
|
508
|
+
/**
|
|
509
|
+
* Build request body with only provided fields (partial update support)
|
|
510
|
+
*/
|
|
511
|
+
buildUserRequestBody(userData) {
|
|
512
|
+
const body = {};
|
|
513
|
+
if ('userId' in userData && userData.userId !== undefined) {
|
|
514
|
+
body.user_id = userData.userId;
|
|
515
|
+
}
|
|
516
|
+
if (userData.displayName !== undefined) {
|
|
517
|
+
body.display_name = userData.displayName;
|
|
518
|
+
}
|
|
519
|
+
if (userData.email !== undefined) {
|
|
520
|
+
body.email = userData.email;
|
|
521
|
+
}
|
|
522
|
+
if (userData.firstName !== undefined) {
|
|
523
|
+
body.first_name = userData.firstName;
|
|
524
|
+
}
|
|
525
|
+
if (userData.lastName !== undefined) {
|
|
526
|
+
body.last_name = userData.lastName;
|
|
527
|
+
}
|
|
528
|
+
if (userData.metadata !== undefined) {
|
|
529
|
+
body.metadata = userData.metadata;
|
|
530
|
+
}
|
|
531
|
+
return body;
|
|
532
|
+
}
|
|
533
|
+
/**
|
|
534
|
+
* Parse user profile to include activity summary, streak, and inventory
|
|
535
|
+
*/
|
|
536
|
+
parseUserProfile(profile) {
|
|
537
|
+
if (!profile)
|
|
538
|
+
return profile;
|
|
539
|
+
const parsed = { ...profile };
|
|
540
|
+
// Parse activity summary if present
|
|
541
|
+
const activitySummary = profile.activity_summary;
|
|
542
|
+
if (activitySummary) {
|
|
543
|
+
parsed.activitySummary = {
|
|
544
|
+
lastEventAt: activitySummary.last_event_at
|
|
545
|
+
? new Date(activitySummary.last_event_at)
|
|
546
|
+
: null,
|
|
547
|
+
eventCount: activitySummary.event_count || 0,
|
|
548
|
+
daysActive: activitySummary.days_active || 0,
|
|
549
|
+
};
|
|
550
|
+
}
|
|
551
|
+
// Parse streak info if present
|
|
552
|
+
const streak = profile.streak;
|
|
553
|
+
if (streak) {
|
|
554
|
+
parsed.streak = {
|
|
555
|
+
currentStreak: streak.current_streak || 0,
|
|
556
|
+
longestStreak: streak.longest_streak || 0,
|
|
557
|
+
lastActivityAt: streak.last_activity_at
|
|
558
|
+
? new Date(streak.last_activity_at)
|
|
559
|
+
: null,
|
|
560
|
+
streakStartedAt: streak.streak_started_at
|
|
561
|
+
? new Date(streak.streak_started_at)
|
|
562
|
+
: null,
|
|
563
|
+
};
|
|
564
|
+
}
|
|
565
|
+
// Parse inventory summary if present
|
|
566
|
+
const inventory = profile.inventory;
|
|
567
|
+
if (inventory) {
|
|
568
|
+
parsed.inventory = {
|
|
569
|
+
itemCount: inventory.item_count || 0,
|
|
570
|
+
activeEffects: inventory.active_effects || [],
|
|
571
|
+
};
|
|
572
|
+
}
|
|
573
|
+
return parsed;
|
|
574
|
+
}
|
|
575
|
+
/**
|
|
576
|
+
* Build filter query parameters from options
|
|
577
|
+
*/
|
|
578
|
+
buildFilterParams(options = {}) {
|
|
579
|
+
const params = {};
|
|
580
|
+
// Pagination
|
|
581
|
+
if (options.page !== undefined)
|
|
582
|
+
params.page = options.page;
|
|
583
|
+
if (options.limit !== undefined)
|
|
584
|
+
params.limit = options.limit;
|
|
585
|
+
if (options.search !== undefined && options.search !== null)
|
|
586
|
+
params.search = options.search;
|
|
587
|
+
// Persona filter
|
|
588
|
+
if (options.persona !== undefined && options.persona !== null) {
|
|
589
|
+
params.persona = options.persona;
|
|
590
|
+
}
|
|
591
|
+
// Level range filters
|
|
592
|
+
if (options.minLevel !== undefined && options.minLevel !== null) {
|
|
593
|
+
params.min_level = options.minLevel;
|
|
594
|
+
}
|
|
595
|
+
if (options.maxLevel !== undefined && options.maxLevel !== null) {
|
|
596
|
+
params.max_level = options.maxLevel;
|
|
597
|
+
}
|
|
598
|
+
// Date range filters (convert to ISO 8601)
|
|
599
|
+
if (options.startDate !== undefined && options.startDate !== null) {
|
|
600
|
+
const startDate = options.startDate instanceof Date
|
|
601
|
+
? options.startDate
|
|
602
|
+
: new Date(options.startDate);
|
|
603
|
+
params.start_date = startDate.toISOString();
|
|
604
|
+
}
|
|
605
|
+
if (options.endDate !== undefined && options.endDate !== null) {
|
|
606
|
+
const endDate = options.endDate instanceof Date
|
|
607
|
+
? options.endDate
|
|
608
|
+
: new Date(options.endDate);
|
|
609
|
+
params.end_date = endDate.toISOString();
|
|
610
|
+
}
|
|
611
|
+
// Timeframe
|
|
612
|
+
if (options.timeframe !== undefined)
|
|
613
|
+
params.timeframe = options.timeframe;
|
|
614
|
+
return params;
|
|
615
|
+
}
|
|
616
|
+
/**
|
|
617
|
+
* Parse leaderboard response to include cache metadata and percentile ranks
|
|
618
|
+
*/
|
|
619
|
+
parseLeaderboardResponse(response) {
|
|
620
|
+
const parsed = { ...response };
|
|
621
|
+
// Parse cache metadata if present
|
|
622
|
+
const cacheData = (response.cache_metadata || response.cacheMetadata);
|
|
623
|
+
if (cacheData) {
|
|
624
|
+
parsed.cacheMetadata = {
|
|
625
|
+
cachedAt: cacheData.cached_at ? new Date(cacheData.cached_at) : null,
|
|
626
|
+
ttl: cacheData.ttl || 0,
|
|
627
|
+
};
|
|
628
|
+
delete parsed.cache_metadata;
|
|
629
|
+
}
|
|
630
|
+
// Parse rankings to include percentile if present
|
|
631
|
+
if (parsed.rankings && Array.isArray(parsed.rankings)) {
|
|
632
|
+
parsed.rankings = parsed.rankings.map(entry => ({
|
|
633
|
+
...entry,
|
|
634
|
+
percentile: entry.percentile !== undefined ? entry.percentile : null,
|
|
635
|
+
}));
|
|
217
636
|
}
|
|
218
|
-
return
|
|
637
|
+
return parsed;
|
|
219
638
|
}
|
|
220
639
|
}
|
|
221
640
|
exports.Rooguys = Rooguys;
|
|
641
|
+
// Export error classes
|
|
642
|
+
var errors_2 = require("./errors");
|
|
643
|
+
Object.defineProperty(exports, "RooguysError", { enumerable: true, get: function () { return errors_2.RooguysError; } });
|
|
644
|
+
Object.defineProperty(exports, "ValidationError", { enumerable: true, get: function () { return errors_2.ValidationError; } });
|
|
645
|
+
Object.defineProperty(exports, "AuthenticationError", { enumerable: true, get: function () { return errors_2.AuthenticationError; } });
|
|
646
|
+
Object.defineProperty(exports, "ForbiddenError", { enumerable: true, get: function () { return errors_2.ForbiddenError; } });
|
|
647
|
+
Object.defineProperty(exports, "NotFoundError", { enumerable: true, get: function () { return errors_2.NotFoundError; } });
|
|
648
|
+
Object.defineProperty(exports, "ConflictError", { enumerable: true, get: function () { return errors_2.ConflictError; } });
|
|
649
|
+
Object.defineProperty(exports, "RateLimitError", { enumerable: true, get: function () { return errors_2.RateLimitError; } });
|
|
650
|
+
Object.defineProperty(exports, "ServerError", { enumerable: true, get: function () { return errors_2.ServerError; } });
|
|
651
|
+
Object.defineProperty(exports, "mapStatusToError", { enumerable: true, get: function () { return errors_2.mapStatusToError; } });
|
|
652
|
+
// Export HTTP client and utilities
|
|
653
|
+
var http_client_2 = require("./http-client");
|
|
654
|
+
Object.defineProperty(exports, "HttpClient", { enumerable: true, get: function () { return http_client_2.HttpClient; } });
|
|
655
|
+
Object.defineProperty(exports, "extractRateLimitInfo", { enumerable: true, get: function () { return http_client_2.extractRateLimitInfo; } });
|
|
656
|
+
Object.defineProperty(exports, "extractRequestId", { enumerable: true, get: function () { return http_client_2.extractRequestId; } });
|
|
657
|
+
Object.defineProperty(exports, "parseResponseBody", { enumerable: true, get: function () { return http_client_2.parseResponseBody; } });
|
|
658
|
+
// Export types
|
|
659
|
+
__exportStar(require("./types"), exports);
|
|
660
|
+
// Default export
|
|
222
661
|
exports.default = Rooguys;
|