@ibertran/fortytwo-intra-client 2.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/LICENSE +21 -0
- package/README.md +264 -0
- package/dist/HttpError.d.ts +9 -0
- package/dist/HttpError.js +25 -0
- package/dist/index.d.ts +43 -0
- package/dist/index.js +280 -0
- package/dist/types.d.ts +20 -0
- package/dist/types.js +2 -0
- package/dist/utils.d.ts +4 -0
- package/dist/utils.js +79 -0
- package/package.json +23 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 ibertran
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
# FortytwoIntraClient
|
|
2
|
+
|
|
3
|
+
A TypeScript library for interacting with the 42API with built-in rate limiting, automatic token management, and retry logic.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @ibertran/fortytwo-intra-client
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Quick Start
|
|
12
|
+
|
|
13
|
+
```typescript
|
|
14
|
+
import { FortytwoIntraClient } from '@ibertran/fortytwo-intra-client';
|
|
15
|
+
|
|
16
|
+
const client = new FortytwoIntraClient('your_client_id', 'your_client_secret');
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
// Get a specific user by login
|
|
20
|
+
const user = await client.get('users/jdoe');
|
|
21
|
+
console.log(user);
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Configuration
|
|
25
|
+
|
|
26
|
+
The `FortytwoIntraClient` class accepts three parameters:
|
|
27
|
+
|
|
28
|
+
1. `client_id` (required): Your 42 API client ID
|
|
29
|
+
2. `client_secret` (required): Your 42 API client secret
|
|
30
|
+
3. `config` (optional): Configuration object
|
|
31
|
+
|
|
32
|
+
### Configuration Options
|
|
33
|
+
|
|
34
|
+
```typescript
|
|
35
|
+
interface Conf {
|
|
36
|
+
redirect_uri: string | null; // OAuth redirect URI (default: null)
|
|
37
|
+
base_url: string; // API base URL (default: "https://api.intra.42.fr/v2/")
|
|
38
|
+
token_url: string; // Token endpoint (default: "https://api.intra.42.fr/oauth/token")
|
|
39
|
+
oauth_url: string; // OAuth endpoint (default: "https://api.intra.42.fr/oauth/authorize")
|
|
40
|
+
scopes: string[]; // OAuth scopes (default: ["public"])
|
|
41
|
+
rate: number; // Requests per second (default: 2)
|
|
42
|
+
maxRetry: number; // Max retry attempts (default: 5)
|
|
43
|
+
logs: boolean; // Enable logging (default: true)
|
|
44
|
+
errors: boolean; // Enable error logging (default: true)
|
|
45
|
+
}
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
### Example with Custom Configuration
|
|
49
|
+
|
|
50
|
+
```typescript
|
|
51
|
+
const client = new FortytwoIntraClient('your_client_id', 'your_client_secret', {
|
|
52
|
+
rate: 1, // 1 request per second
|
|
53
|
+
maxRetry: 3, // Retry up to 3 times
|
|
54
|
+
logs: false, // Disable logging
|
|
55
|
+
scopes: ['public', 'projects'] // Extended scopes
|
|
56
|
+
});
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## API Methods
|
|
60
|
+
|
|
61
|
+
### GET Requests
|
|
62
|
+
|
|
63
|
+
```typescript
|
|
64
|
+
// Get all campuses
|
|
65
|
+
const campuses = await client.get('campus');
|
|
66
|
+
console.log(`Found ${campuses.length} campuses`);
|
|
67
|
+
|
|
68
|
+
// Get a specific campus
|
|
69
|
+
const lyonCampus = await client.get('campus/9');
|
|
70
|
+
console.log(`${lyonCampus.name}: ${lyonCampus.users_count} users`);
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
### POST Requests
|
|
74
|
+
|
|
75
|
+
```typescript
|
|
76
|
+
// Create a team for a project
|
|
77
|
+
const team = await client.post('teams', {
|
|
78
|
+
body: {
|
|
79
|
+
name: 'awesome-team',
|
|
80
|
+
project_id: 42
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
### PATCH Requests
|
|
86
|
+
|
|
87
|
+
```typescript
|
|
88
|
+
// Update a team name
|
|
89
|
+
const updatedTeam = await client.patch('teams/123', {
|
|
90
|
+
body: {
|
|
91
|
+
team: {
|
|
92
|
+
name: "updated-team-name"
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
### DELETE Requests
|
|
99
|
+
|
|
100
|
+
```typescript
|
|
101
|
+
// Delete a team
|
|
102
|
+
await client.delete('teams/123');
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
### Get All Pages (Pagination Helper)
|
|
106
|
+
|
|
107
|
+
The `getAll` method automatically handles pagination and returns all results:
|
|
108
|
+
|
|
109
|
+
```typescript
|
|
110
|
+
// Get all users from 42 Lyon (handles pagination automatically)
|
|
111
|
+
const lyonUsers = await client.getAll('campus/9/users');
|
|
112
|
+
console.log(`Paris campus has ${lyonUsers.length} users`);
|
|
113
|
+
|
|
114
|
+
// Get all projects with custom page size
|
|
115
|
+
const allProjects = await client.getAll('projects', {
|
|
116
|
+
perPage: 50 // 50 items per page (default: 100)
|
|
117
|
+
});
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
## OAuth Authentication
|
|
121
|
+
|
|
122
|
+
### Authorization Flow
|
|
123
|
+
|
|
124
|
+
```typescript
|
|
125
|
+
// 1. Get OAuth authorization URL
|
|
126
|
+
const authUrl = client.getOAuthUrl('http://localhost:3000/callback');
|
|
127
|
+
// Redirect user to authUrl
|
|
128
|
+
|
|
129
|
+
// 2. Exchange authorization code for tokens
|
|
130
|
+
const tokens = await client.exchangeOAuthCode(code, 'http://localhost:3000/callback');
|
|
131
|
+
// tokens contains: access_token, refresh_token, expires_in, etc.
|
|
132
|
+
|
|
133
|
+
// 3. Use tokens for authenticated requests (USER-SPECIFIC DATA)
|
|
134
|
+
const currentUser = await client.get('users/me', {
|
|
135
|
+
token: tokens
|
|
136
|
+
});
|
|
137
|
+
console.log(`Welcome ${currentUser.displayname}!`);
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
### Using Custom Redirect URI
|
|
141
|
+
|
|
142
|
+
```typescript
|
|
143
|
+
// Set redirect URI in constructor
|
|
144
|
+
const client = new IntraApiProxy('client_id', 'client_secret', {
|
|
145
|
+
redirect_uri: 'http://localhost:3000/callback'
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
// Or pass it to getOAuthUrl
|
|
149
|
+
const authUrl = client.getOAuthUrl('http://localhost:3000/callback');
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
## Utility Methods
|
|
153
|
+
|
|
154
|
+
### URL Builder
|
|
155
|
+
|
|
156
|
+
```typescript
|
|
157
|
+
// Create a URL object for the API
|
|
158
|
+
const userUrl = client.URL('users/jdoe');
|
|
159
|
+
console.log(userUrl.toString()); // "https://api.intra.42.fr/v2/users/jdoe"
|
|
160
|
+
|
|
161
|
+
// Build URLs for specific endpoints
|
|
162
|
+
const campusUrl = client.URL('campus/1/users');
|
|
163
|
+
const projectUrl = client.URL('projects/libft');
|
|
164
|
+
const coalitionUrl = client.URL('coalitions/1/users');
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
## Error Handling
|
|
168
|
+
|
|
169
|
+
The library includes automatic retry logic for rate limiting (429) and authentication (401) errors:
|
|
170
|
+
|
|
171
|
+
```typescript
|
|
172
|
+
try {
|
|
173
|
+
const user = await client.get('users/jdoe');
|
|
174
|
+
console.log(`Found user: ${user.displayname}`);
|
|
175
|
+
} catch (error) {
|
|
176
|
+
if (error.status === 404) {
|
|
177
|
+
console.error('User not found');
|
|
178
|
+
} else if (error.status === 403) {
|
|
179
|
+
console.error('Access forbidden - check your token permissions');
|
|
180
|
+
} else {
|
|
181
|
+
console.error('Request failed after retries:', error.message);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
## Rate Limiting
|
|
187
|
+
|
|
188
|
+
Requests are automatically throttled based on the configured rate limit. The default is 2 requests per second to comply with 42's API limits.
|
|
189
|
+
|
|
190
|
+
## Logging
|
|
191
|
+
|
|
192
|
+
By default, the library logs all requests with status codes:
|
|
193
|
+
|
|
194
|
+
```
|
|
195
|
+
✅ 200 GET https://api.intra.42.fr/v2/users/jdoe
|
|
196
|
+
✅ 200 GET https://api.intra.42.fr/v2/campus/1
|
|
197
|
+
🔄 429 GET https://api.intra.42.fr/v2/projects retry 1/5
|
|
198
|
+
✅ 200 GET https://api.intra.42.fr/v2/projects retry 1/5
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
Disable logging by setting `logs: false` in the configuration.
|
|
202
|
+
|
|
203
|
+
## TypeScript Support
|
|
204
|
+
|
|
205
|
+
This library is written in TypeScript and includes full type definitions:
|
|
206
|
+
|
|
207
|
+
```typescript
|
|
208
|
+
import { FortytwoIntraClient } from '@ibertran/fortytwo-intra-client';
|
|
209
|
+
|
|
210
|
+
// Types are automatically inferred
|
|
211
|
+
const client = new FortytwoIntraClient('client_id', 'client_secret');
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
### OAuth Flow Example
|
|
215
|
+
|
|
216
|
+
```typescript
|
|
217
|
+
import express from 'express';
|
|
218
|
+
import { FortytwoIntraClient } from '@ibertran/fortytwo-intra-client';
|
|
219
|
+
|
|
220
|
+
const app = express();
|
|
221
|
+
const client = new FortytwoIntraClient(
|
|
222
|
+
process.env.INTRA_CLIENT_ID!,
|
|
223
|
+
process.env.INTRA_CLIENT_SECRET!,
|
|
224
|
+
{
|
|
225
|
+
redirect_uri: 'http://localhost:3000/callback',
|
|
226
|
+
oauth_scope: ['public']
|
|
227
|
+
}
|
|
228
|
+
);
|
|
229
|
+
|
|
230
|
+
// Redirect to 42 OAuth
|
|
231
|
+
app.get('/login', (req, res) => {
|
|
232
|
+
const authUrl = client.getOAuthUrl();
|
|
233
|
+
res.redirect(authUrl);
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
// Handle OAuth callback
|
|
237
|
+
app.get('/callback', async (req, res) => {
|
|
238
|
+
try {
|
|
239
|
+
const { code } = req.query;
|
|
240
|
+
const token = await client.exchangeOAuthCode(code as string);
|
|
241
|
+
|
|
242
|
+
// Use tokens to make authenticated requests (accessing personal data)
|
|
243
|
+
const userData = await client.get('users/me', { token: token });
|
|
244
|
+
|
|
245
|
+
res.json({
|
|
246
|
+
user: {
|
|
247
|
+
login: userData.login,
|
|
248
|
+
displayname: userData.displayname,
|
|
249
|
+
email: userData.email,
|
|
250
|
+
level: userData.cursus_users[0]?.level || 0,
|
|
251
|
+
wallet: userData.wallet,
|
|
252
|
+
campus: userData.campus[0]?.name
|
|
253
|
+
},
|
|
254
|
+
});
|
|
255
|
+
} catch (error) {
|
|
256
|
+
res.status(500).json({ error: error.message });
|
|
257
|
+
}
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
app.listen(3000, () => {
|
|
261
|
+
console.log('Server running on http://localhost:3000');
|
|
262
|
+
console.log('Visit http://localhost:3000/login to start OAuth flow');
|
|
263
|
+
});
|
|
264
|
+
```
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
declare class HttpError extends Error {
|
|
2
|
+
status: number | null;
|
|
3
|
+
url: string | null;
|
|
4
|
+
details: any;
|
|
5
|
+
constructor(err: any);
|
|
6
|
+
}
|
|
7
|
+
export declare function simplifySuperagentError(err: any): HttpError;
|
|
8
|
+
export declare function isSuperAgentError(err: any): boolean;
|
|
9
|
+
export {};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.simplifySuperagentError = simplifySuperagentError;
|
|
4
|
+
exports.isSuperAgentError = isSuperAgentError;
|
|
5
|
+
class HttpError extends Error {
|
|
6
|
+
status;
|
|
7
|
+
url;
|
|
8
|
+
details;
|
|
9
|
+
constructor(err) {
|
|
10
|
+
super(err.message || "Unknown API error");
|
|
11
|
+
this.name = "HttpError";
|
|
12
|
+
this.status = err.status || null;
|
|
13
|
+
this.url = err.response?.request?.url || null;
|
|
14
|
+
this.details = err.response?.body || null;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
function simplifySuperagentError(err) {
|
|
18
|
+
if (!err) {
|
|
19
|
+
return new HttpError({ message: "Unknown error" });
|
|
20
|
+
}
|
|
21
|
+
return new HttpError(err);
|
|
22
|
+
}
|
|
23
|
+
function isSuperAgentError(err) {
|
|
24
|
+
return (err && typeof err === "object" && ("status" in err || "response" in err));
|
|
25
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { inputOptions, perPage } from "./types";
|
|
2
|
+
export interface FortytwoIntraClientConf {
|
|
3
|
+
redirect_uri: string | null;
|
|
4
|
+
base_url: string;
|
|
5
|
+
token_url: string;
|
|
6
|
+
oauth_url: string;
|
|
7
|
+
token_info_url: string;
|
|
8
|
+
scopes: string[];
|
|
9
|
+
rate: number;
|
|
10
|
+
maxRetry: number;
|
|
11
|
+
logs: boolean;
|
|
12
|
+
errors: boolean;
|
|
13
|
+
}
|
|
14
|
+
export declare class FortytwoIntraClient {
|
|
15
|
+
private client_id;
|
|
16
|
+
private client_secret;
|
|
17
|
+
private redirect_uri;
|
|
18
|
+
private base_url;
|
|
19
|
+
private token_url;
|
|
20
|
+
private oauth_url;
|
|
21
|
+
private token_info_url;
|
|
22
|
+
private scopes;
|
|
23
|
+
private throttler;
|
|
24
|
+
private retryOn;
|
|
25
|
+
private maxRetry;
|
|
26
|
+
private logs;
|
|
27
|
+
private access_token;
|
|
28
|
+
constructor(client_id: string, client_secret: string, conf: Partial<FortytwoIntraClientConf>);
|
|
29
|
+
private generateToken;
|
|
30
|
+
private fetch;
|
|
31
|
+
private reqHandler;
|
|
32
|
+
private logSuccess;
|
|
33
|
+
private logError;
|
|
34
|
+
get(endpoint: URL | string, options?: Omit<inputOptions, "body">): Promise<any>;
|
|
35
|
+
post(endpoint: URL | string, options?: inputOptions): Promise<any>;
|
|
36
|
+
patch(endpoint: URL | string, options: inputOptions): Promise<any>;
|
|
37
|
+
delete(endpoint: URL | string, options: inputOptions): Promise<any>;
|
|
38
|
+
getAll(endpoint: URL | string, options?: inputOptions & perPage): Promise<any>;
|
|
39
|
+
URL(endpoint: string): URL;
|
|
40
|
+
getOAuthUrl(redirect_uri?: string | null): string;
|
|
41
|
+
exchangeOAuthCode(code: string, redirect_uri?: string): Promise<any>;
|
|
42
|
+
tokenInfos(): Promise<any>;
|
|
43
|
+
}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
36
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
37
|
+
};
|
|
38
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
+
exports.FortytwoIntraClient = void 0;
|
|
40
|
+
const superagent = __importStar(require("superagent"));
|
|
41
|
+
// @ts-ignore
|
|
42
|
+
const superagent_throttle_1 = __importDefault(require("superagent-throttle"));
|
|
43
|
+
const utils_1 = require("./utils");
|
|
44
|
+
const HttpError_1 = require("./HttpError");
|
|
45
|
+
const defaultConf = {
|
|
46
|
+
redirect_uri: null,
|
|
47
|
+
base_url: "https://api.intra.42.fr/v2/",
|
|
48
|
+
token_url: "https://api.intra.42.fr/oauth/token",
|
|
49
|
+
oauth_url: "https://api.intra.42.fr/oauth/authorize",
|
|
50
|
+
token_info_url: "https://api.intra.42.fr/oauth/token/info",
|
|
51
|
+
scopes: ["public"],
|
|
52
|
+
rate: 2,
|
|
53
|
+
maxRetry: 5,
|
|
54
|
+
logs: true,
|
|
55
|
+
errors: true,
|
|
56
|
+
};
|
|
57
|
+
class FortytwoIntraClient {
|
|
58
|
+
client_id;
|
|
59
|
+
client_secret;
|
|
60
|
+
redirect_uri;
|
|
61
|
+
base_url;
|
|
62
|
+
token_url;
|
|
63
|
+
oauth_url;
|
|
64
|
+
token_info_url;
|
|
65
|
+
scopes;
|
|
66
|
+
throttler;
|
|
67
|
+
retryOn;
|
|
68
|
+
maxRetry;
|
|
69
|
+
logs;
|
|
70
|
+
access_token;
|
|
71
|
+
constructor(client_id, client_secret, conf) {
|
|
72
|
+
this.client_id = client_id;
|
|
73
|
+
this.client_secret = client_secret;
|
|
74
|
+
const config = { ...defaultConf, ...conf };
|
|
75
|
+
this.redirect_uri = config.redirect_uri;
|
|
76
|
+
this.base_url = config.base_url;
|
|
77
|
+
this.token_url = config.token_url;
|
|
78
|
+
this.oauth_url = config.oauth_url;
|
|
79
|
+
this.token_info_url = config.token_info_url;
|
|
80
|
+
this.scopes = config.scopes;
|
|
81
|
+
this.throttler = new superagent_throttle_1.default({
|
|
82
|
+
rate: config.rate,
|
|
83
|
+
ratePer: 1100,
|
|
84
|
+
concurrent: config.rate - 1 > 0 ? config.rate - 1 : 1,
|
|
85
|
+
});
|
|
86
|
+
this.retryOn = [401, 429, 500];
|
|
87
|
+
this.maxRetry = config.maxRetry;
|
|
88
|
+
this.logs = config.logs;
|
|
89
|
+
this.access_token = null;
|
|
90
|
+
}
|
|
91
|
+
async generateToken() {
|
|
92
|
+
const res = await superagent.post(this.token_url).send({
|
|
93
|
+
grant_type: "client_credentials",
|
|
94
|
+
client_id: this.client_id,
|
|
95
|
+
client_secret: this.client_secret,
|
|
96
|
+
scope: this.scopes.join(" "),
|
|
97
|
+
});
|
|
98
|
+
return res.body.access_token;
|
|
99
|
+
}
|
|
100
|
+
async fetch(url, options) {
|
|
101
|
+
const { method, body } = options;
|
|
102
|
+
const req = (0, utils_1.initRequest)(method, url);
|
|
103
|
+
if (!options.token && !this.access_token) {
|
|
104
|
+
this.access_token = await this.generateToken();
|
|
105
|
+
}
|
|
106
|
+
// Attach access_token
|
|
107
|
+
const accessToken = options.token
|
|
108
|
+
? options.token.access_token
|
|
109
|
+
: this.access_token;
|
|
110
|
+
req.set("Authorization", `Bearer ${accessToken}`);
|
|
111
|
+
// Add query string
|
|
112
|
+
if (options.query)
|
|
113
|
+
req.query(options.query);
|
|
114
|
+
// Send body if any is provided
|
|
115
|
+
if (body)
|
|
116
|
+
req.send(body);
|
|
117
|
+
// Throttle resquest based on rate limit
|
|
118
|
+
req.use(this.throttler.plugin());
|
|
119
|
+
return req;
|
|
120
|
+
}
|
|
121
|
+
async reqHandler(url, options) {
|
|
122
|
+
try {
|
|
123
|
+
const res = await this.fetch(url, options);
|
|
124
|
+
this.logSuccess(res, options);
|
|
125
|
+
return res;
|
|
126
|
+
}
|
|
127
|
+
catch (err) {
|
|
128
|
+
if ((0, HttpError_1.isSuperAgentError)(err)) {
|
|
129
|
+
const { attempt, maxRetry } = options;
|
|
130
|
+
this.logError(err, url, options);
|
|
131
|
+
if (maxRetry > 0 &&
|
|
132
|
+
attempt < maxRetry &&
|
|
133
|
+
this.retryOn.includes(err.status)) {
|
|
134
|
+
if (err.status === 401) {
|
|
135
|
+
this.access_token = null;
|
|
136
|
+
}
|
|
137
|
+
options.attempt++;
|
|
138
|
+
return this.reqHandler(url, options);
|
|
139
|
+
}
|
|
140
|
+
else {
|
|
141
|
+
throw (0, HttpError_1.simplifySuperagentError)(err);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
else {
|
|
145
|
+
throw err;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
logSuccess(res, options) {
|
|
150
|
+
const method = options.method.padEnd(6, " ");
|
|
151
|
+
const color = "\x1b[42m";
|
|
152
|
+
const reset = "\x1b[0m";
|
|
153
|
+
const msg = `${color + res.status + reset} ${method} ${res.request.url}${options.attempt ? ` retry ${options.attempt}/${options.maxRetry}` : ``}`;
|
|
154
|
+
console.log(msg);
|
|
155
|
+
}
|
|
156
|
+
logError(err, url, options) {
|
|
157
|
+
const method = options.method.padEnd(6, " ");
|
|
158
|
+
const color = "\x1b[41m";
|
|
159
|
+
const reset = "\x1b[0m";
|
|
160
|
+
const msg = `${color + err.status + reset} ${method} ${err.response.request.url}${options.attempt ? ` retry ${options.attempt}/${options.maxRetry}` : ``}`;
|
|
161
|
+
console.log(msg, JSON.stringify(err.response.body, null, 2));
|
|
162
|
+
}
|
|
163
|
+
// Public methods
|
|
164
|
+
async get(endpoint, options = {}) {
|
|
165
|
+
if (endpoint instanceof URL === false) {
|
|
166
|
+
endpoint = new URL(endpoint, this.base_url);
|
|
167
|
+
}
|
|
168
|
+
const res = await this.reqHandler(endpoint, {
|
|
169
|
+
method: "GET",
|
|
170
|
+
attempt: 0,
|
|
171
|
+
maxRetry: this.maxRetry,
|
|
172
|
+
...options,
|
|
173
|
+
});
|
|
174
|
+
return res.body;
|
|
175
|
+
}
|
|
176
|
+
async post(endpoint, options = {}) {
|
|
177
|
+
if (endpoint instanceof URL === false) {
|
|
178
|
+
endpoint = new URL(endpoint, this.base_url);
|
|
179
|
+
}
|
|
180
|
+
const res = await this.reqHandler(endpoint, {
|
|
181
|
+
method: "POST",
|
|
182
|
+
attempt: 0,
|
|
183
|
+
maxRetry: this.maxRetry,
|
|
184
|
+
...options,
|
|
185
|
+
});
|
|
186
|
+
return res.body;
|
|
187
|
+
}
|
|
188
|
+
async patch(endpoint, options) {
|
|
189
|
+
if (endpoint instanceof URL === false) {
|
|
190
|
+
endpoint = new URL(endpoint, this.base_url);
|
|
191
|
+
}
|
|
192
|
+
const res = await this.reqHandler(endpoint, {
|
|
193
|
+
method: "PATCH",
|
|
194
|
+
attempt: 0,
|
|
195
|
+
maxRetry: this.maxRetry,
|
|
196
|
+
...options,
|
|
197
|
+
});
|
|
198
|
+
return res.body;
|
|
199
|
+
}
|
|
200
|
+
async delete(endpoint, options) {
|
|
201
|
+
if (endpoint instanceof URL === false) {
|
|
202
|
+
endpoint = new URL(endpoint, this.base_url);
|
|
203
|
+
}
|
|
204
|
+
const res = await this.reqHandler(endpoint, {
|
|
205
|
+
method: "DELETE",
|
|
206
|
+
attempt: 0,
|
|
207
|
+
maxRetry: this.maxRetry,
|
|
208
|
+
...options,
|
|
209
|
+
});
|
|
210
|
+
return res.body;
|
|
211
|
+
}
|
|
212
|
+
async getAll(endpoint, options = {}) {
|
|
213
|
+
if (endpoint instanceof URL === false) {
|
|
214
|
+
endpoint = new URL(endpoint, this.base_url);
|
|
215
|
+
}
|
|
216
|
+
const perPage = options.perPage || 100;
|
|
217
|
+
let url = new URL(endpoint);
|
|
218
|
+
const initialRes = await this.reqHandler(url, {
|
|
219
|
+
method: "GET",
|
|
220
|
+
attempt: 0,
|
|
221
|
+
maxRetry: this.maxRetry,
|
|
222
|
+
...options,
|
|
223
|
+
query: {
|
|
224
|
+
page: 1,
|
|
225
|
+
per_page: 100,
|
|
226
|
+
},
|
|
227
|
+
});
|
|
228
|
+
let lastPage;
|
|
229
|
+
try {
|
|
230
|
+
lastPage = (0, utils_1.getLastPage)(initialRes.header["link"]);
|
|
231
|
+
}
|
|
232
|
+
catch (err) {
|
|
233
|
+
return initialRes.body;
|
|
234
|
+
}
|
|
235
|
+
const promises = [];
|
|
236
|
+
for (let i = 2; i <= lastPage; i++) {
|
|
237
|
+
url = new URL(endpoint);
|
|
238
|
+
url.searchParams.append("per_page", perPage.toString());
|
|
239
|
+
url.searchParams.append("page", i.toString());
|
|
240
|
+
promises.push(this.reqHandler(url, {
|
|
241
|
+
method: "GET",
|
|
242
|
+
attempt: 0,
|
|
243
|
+
maxRetry: this.maxRetry,
|
|
244
|
+
...options,
|
|
245
|
+
}));
|
|
246
|
+
}
|
|
247
|
+
return Promise.all(promises).then((values) => {
|
|
248
|
+
return initialRes.body.concat(...values.map((value) => value.body));
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
URL(endpoint) {
|
|
252
|
+
return new URL(endpoint, this.base_url);
|
|
253
|
+
}
|
|
254
|
+
getOAuthUrl(redirect_uri = null) {
|
|
255
|
+
redirect_uri ??= this.redirect_uri;
|
|
256
|
+
if (!redirect_uri) {
|
|
257
|
+
throw new Error(`undefined redirect_uri`);
|
|
258
|
+
}
|
|
259
|
+
const url = new URL(this.oauth_url);
|
|
260
|
+
url.searchParams.set("client_id", this.client_id);
|
|
261
|
+
url.searchParams.set("redirect_uri", redirect_uri);
|
|
262
|
+
url.searchParams.set("response_type", "code");
|
|
263
|
+
url.searchParams.set("scope", this.scopes.join(" "));
|
|
264
|
+
return url.toString();
|
|
265
|
+
}
|
|
266
|
+
async exchangeOAuthCode(code, redirect_uri) {
|
|
267
|
+
const res = await superagent.post(this.token_url).send({
|
|
268
|
+
grant_type: "authorization_code",
|
|
269
|
+
client_id: this.client_id,
|
|
270
|
+
client_secret: this.client_secret,
|
|
271
|
+
redirect_uri: redirect_uri ? redirect_uri : this.redirect_uri,
|
|
272
|
+
code: code,
|
|
273
|
+
});
|
|
274
|
+
return res.body;
|
|
275
|
+
}
|
|
276
|
+
async tokenInfos() {
|
|
277
|
+
return this.get(this.token_info_url);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
exports.FortytwoIntraClient = FortytwoIntraClient;
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export type Method = "GET" | "POST" | "PATCH" | "DELETE";
|
|
2
|
+
export type userToken = {
|
|
3
|
+
access_token: string;
|
|
4
|
+
};
|
|
5
|
+
export type reqOptions = {
|
|
6
|
+
method: Method;
|
|
7
|
+
maxRetry: number;
|
|
8
|
+
attempt: number;
|
|
9
|
+
body?: any;
|
|
10
|
+
token?: userToken;
|
|
11
|
+
query?: querystring;
|
|
12
|
+
};
|
|
13
|
+
export type querystring = Record<string, string | number | boolean | Array<string | number | boolean>>;
|
|
14
|
+
export type perPage = {
|
|
15
|
+
perPage?: number;
|
|
16
|
+
};
|
|
17
|
+
type internal = "method" | "attempt";
|
|
18
|
+
type optional = "maxRetry";
|
|
19
|
+
export type inputOptions = Omit<reqOptions, internal | optional> & Partial<Pick<reqOptions, optional>>;
|
|
20
|
+
export {};
|
package/dist/types.js
ADDED
package/dist/utils.d.ts
ADDED
package/dist/utils.js
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.initRequest = initRequest;
|
|
37
|
+
exports.getLastPage = getLastPage;
|
|
38
|
+
const superagent = __importStar(require("superagent"));
|
|
39
|
+
function initRequest(method, url) {
|
|
40
|
+
const endpoint = url.toString();
|
|
41
|
+
switch (method) {
|
|
42
|
+
case "GET":
|
|
43
|
+
return superagent.get(endpoint);
|
|
44
|
+
case "POST":
|
|
45
|
+
return superagent.post(endpoint);
|
|
46
|
+
case "PATCH":
|
|
47
|
+
return superagent.patch(endpoint);
|
|
48
|
+
case "DELETE":
|
|
49
|
+
return superagent.delete(endpoint);
|
|
50
|
+
default:
|
|
51
|
+
throw Error("Invalid HTTP method");
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
function getLastPage(header) {
|
|
55
|
+
if (!header) {
|
|
56
|
+
throw Error("No pagination link header");
|
|
57
|
+
}
|
|
58
|
+
const paginationlinks = header
|
|
59
|
+
.split(",")
|
|
60
|
+
.reduce((links, part) => {
|
|
61
|
+
const match = part.match(/<([^>]+)>\s*;\s*rel="([^"]+)"/);
|
|
62
|
+
if (match) {
|
|
63
|
+
const url = match[1];
|
|
64
|
+
const rel = match[2];
|
|
65
|
+
links[rel] = url;
|
|
66
|
+
}
|
|
67
|
+
return links;
|
|
68
|
+
}, {});
|
|
69
|
+
if (!paginationlinks.last) {
|
|
70
|
+
throw Error("Missing last page link");
|
|
71
|
+
}
|
|
72
|
+
const lastPageUrl = new URL(paginationlinks.last);
|
|
73
|
+
const rawLastPage = lastPageUrl.searchParams.get("page");
|
|
74
|
+
const lastPage = rawLastPage ? parseInt(rawLastPage, 10) : undefined;
|
|
75
|
+
if (!lastPage || isNaN(lastPage)) {
|
|
76
|
+
throw Error("Invalid last page link");
|
|
77
|
+
}
|
|
78
|
+
return lastPage;
|
|
79
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@ibertran/fortytwo-intra-client",
|
|
3
|
+
"version": "2.0.0",
|
|
4
|
+
"main": "dist/index.js",
|
|
5
|
+
"types": "dist/index.d.ts",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"build": "tsc",
|
|
8
|
+
"prepare": "npm run build"
|
|
9
|
+
},
|
|
10
|
+
"dependencies": {
|
|
11
|
+
"superagent": "^10.2.1",
|
|
12
|
+
"superagent-throttle": "^1.0.1"
|
|
13
|
+
},
|
|
14
|
+
"devDependencies": {
|
|
15
|
+
"@types/superagent": "^8.1.9",
|
|
16
|
+
"ts-node": "^10.9.2",
|
|
17
|
+
"tsc": "^2.0.4"
|
|
18
|
+
},
|
|
19
|
+
"files": [
|
|
20
|
+
"dist",
|
|
21
|
+
"README.md"
|
|
22
|
+
]
|
|
23
|
+
}
|