@ibertran/fortytwo-intra-client 5.0.0 → 5.1.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 CHANGED
@@ -140,6 +140,27 @@ const allProjects = await client.getAll("projects", {
140
140
  });
141
141
  ```
142
142
 
143
+ ### Raw Requests
144
+
145
+ The `rawRequest` method takes a plain [axios request config](https://axios-http.com/docs/req_config), injects the access token, and returns the raw `AxiosResponse`. Relative URLs are resolved against the client's `base_url` unless `baseURL` is set in the config.
146
+
147
+ Unlike the other methods, it does not retry, log, validate, or wrap errors: failures are thrown as `AxiosError`.
148
+
149
+ ```typescript
150
+ // Full axios config, raw response
151
+ const res = await client.rawRequest({
152
+ method: "get",
153
+ url: "campus/9/users",
154
+ params: { "page[size]": 100 },
155
+ });
156
+ console.log(res.status, res.headers["x-total"], res.data);
157
+
158
+ // With a user token (V2 client only)
159
+ const me = await client.rawRequest({ url: "me" }, tokens);
160
+ ```
161
+
162
+ `rawRequest` is available on both `FortytwoIntraClient` and `FortytwoIntraV3Client`. On the V2 client, requests still go through the rate limiter.
163
+
143
164
  ## OAuth Authentication
144
165
 
145
166
  ### Authorization Flow
package/dist/index.d.ts CHANGED
@@ -1,5 +1,6 @@
1
+ import { AxiosRequestConfig, AxiosResponse } from "axios";
1
2
  import z from "zod";
2
- import { inputOptions } from "./types";
3
+ import { inputOptions, userToken } from "./types";
3
4
  export { FortytwoIntraV3Client } from "./intraV3";
4
5
  export * from "./errors/httpError";
5
6
  export * from "./errors/validationError";
@@ -32,9 +33,12 @@ export declare class FortytwoIntraClient {
32
33
  private maxRetry;
33
34
  private logLine;
34
35
  private errLogBody;
35
- private access_token;
36
+ private authorization;
37
+ private tokenRequest;
36
38
  constructor(client_id: string, client_secret: string, conf: Partial<FortytwoIntraClientConf>);
37
39
  private generateToken;
40
+ private isTokenValid;
41
+ private getAccessToken;
38
42
  private fetch;
39
43
  private reqHandler;
40
44
  private logSuccess;
@@ -64,6 +68,7 @@ export declare class FortytwoIntraClient {
64
68
  getAll<S extends z.ZodType>(endpoint: URL | string, options: Omit<inputOptions, "body"> & {
65
69
  schema: S;
66
70
  }): Promise<z.infer<S>>;
71
+ rawRequest<T = any, D = any>(config: AxiosRequestConfig<D>, token?: userToken): Promise<AxiosResponse<T, D>>;
67
72
  URL(endpoint: string): URL;
68
73
  getOAuthUrl(options?: {
69
74
  redirect_uri?: string;
package/dist/index.js CHANGED
@@ -80,7 +80,8 @@ class FortytwoIntraClient {
80
80
  maxRetry;
81
81
  logLine;
82
82
  errLogBody;
83
- access_token;
83
+ authorization;
84
+ tokenRequest = null;
84
85
  constructor(client_id, client_secret, conf) {
85
86
  this.client_id = client_id;
86
87
  this.client_secret = client_secret;
@@ -103,7 +104,7 @@ class FortytwoIntraClient {
103
104
  this.maxRetry = config.maxRetry;
104
105
  this.logLine = config.logLine;
105
106
  this.errLogBody = config.errLogBody;
106
- this.access_token = null;
107
+ this.authorization = null;
107
108
  }
108
109
  async generateToken() {
109
110
  const res = await this.axiosInstance.post(this.token_url, {
@@ -112,15 +113,31 @@ class FortytwoIntraClient {
112
113
  client_secret: this.client_secret,
113
114
  scope: this.scopes.join(" "),
114
115
  });
115
- return res.data.access_token;
116
+ const { access_token, expires_in } = zod_1.default
117
+ .object({ access_token: zod_1.default.string(), expires_in: zod_1.default.number() })
118
+ .parse(res.data);
119
+ return { access_token, expires_at: Date.now() + expires_in * 1_000 };
120
+ }
121
+ isTokenValid() {
122
+ return this.authorization !== null && this.authorization.expires_at - 30_000 > Date.now();
123
+ }
124
+ async getAccessToken(token) {
125
+ if (token) {
126
+ return token.access_token;
127
+ }
128
+ if (!this.isTokenValid()) {
129
+ // Share a single in-flight token request between concurrent callers
130
+ this.tokenRequest ??= this.generateToken().finally(() => {
131
+ this.tokenRequest = null;
132
+ });
133
+ this.authorization = await this.tokenRequest;
134
+ }
135
+ return this.authorization.access_token;
116
136
  }
117
137
  async fetch(url, options) {
118
138
  const { method, body, query } = options;
119
- if (!options.token && !this.access_token) {
120
- this.access_token = await this.generateToken();
121
- }
122
139
  // Attach access_token
123
- const accessToken = options.token ? options.token.access_token : this.access_token;
140
+ const accessToken = await this.getAccessToken(options.token);
124
141
  // Extract query parameters from URL and combine with options.query
125
142
  const urlParams = {};
126
143
  url.searchParams.forEach((value, key) => {
@@ -155,7 +172,7 @@ class FortytwoIntraClient {
155
172
  const status = err.response?.status;
156
173
  if (maxRetry > 0 && attempt < maxRetry && status && this.retryOn.includes(status)) {
157
174
  if (status === 401) {
158
- this.access_token = null;
175
+ this.authorization = null;
159
176
  }
160
177
  options.attempt++;
161
178
  return this.reqHandler(url, options);
@@ -322,6 +339,17 @@ class FortytwoIntraClient {
322
339
  const allData = [...firstPage.data, ...otherPages.flatMap((res) => res.data)];
323
340
  return this.validate(allData, options.schema);
324
341
  }
342
+ async rawRequest(config, token) {
343
+ const accessToken = await this.getAccessToken(token);
344
+ return this.axiosInstance.request({
345
+ baseURL: this.base_url,
346
+ ...config,
347
+ headers: {
348
+ ...config.headers,
349
+ Authorization: `Bearer ${accessToken}`,
350
+ },
351
+ });
352
+ }
325
353
  URL(endpoint) {
326
354
  return new URL(endpoint, this.base_url);
327
355
  }
@@ -1,3 +1,4 @@
1
+ import { AxiosRequestConfig, AxiosResponse } from "axios";
1
2
  import z from "zod";
2
3
  import { inputOptions } from "../types";
3
4
  export * from "../errors/httpError";
@@ -25,10 +26,12 @@ export declare class FortytwoIntraV3Client {
25
26
  private logLine;
26
27
  private errLogBody;
27
28
  private authorization;
29
+ private tokenRequest;
28
30
  constructor(credentials: Credentials, conf?: Partial<FortytwoIntraClientConf>);
29
31
  private generateToken;
30
32
  private isTokenValid;
31
33
  private isRefreshTokenValid;
34
+ private getAccessToken;
32
35
  private fetch;
33
36
  private reqHandler;
34
37
  private logSuccess;
@@ -54,5 +57,6 @@ export declare class FortytwoIntraV3Client {
54
57
  delete<S extends z.ZodType>(endpoint: URL | string, options: Omit<inputOptions, "perPage" | "maxPages"> & {
55
58
  schema: S;
56
59
  }): Promise<z.infer<S>>;
60
+ rawRequest<T = any, D = any>(config: AxiosRequestConfig<D>): Promise<AxiosResponse<T, D>>;
57
61
  URL(endpoint: string): URL;
58
62
  }
@@ -65,6 +65,7 @@ class FortytwoIntraV3Client {
65
65
  logLine;
66
66
  errLogBody;
67
67
  authorization;
68
+ tokenRequest = null;
68
69
  constructor(credentials, conf = {}) {
69
70
  this.credentials = credentials;
70
71
  const config = { ...defaultConf, ...conf };
@@ -102,13 +103,20 @@ class FortytwoIntraV3Client {
102
103
  isRefreshTokenValid() {
103
104
  return (this.authorization !== null && this.authorization.refresh_expires_at - 30000 > Date.now());
104
105
  }
105
- async fetch(url, options) {
106
- const { method, body, query } = options;
106
+ async getAccessToken() {
107
107
  if (!this.isTokenValid()) {
108
- this.authorization = await this.generateToken();
108
+ // Share a single in-flight token request between concurrent callers
109
+ this.tokenRequest ??= this.generateToken().finally(() => {
110
+ this.tokenRequest = null;
111
+ });
112
+ this.authorization = await this.tokenRequest;
109
113
  }
114
+ return this.authorization.access_token;
115
+ }
116
+ async fetch(url, options) {
117
+ const { method, body, query } = options;
110
118
  // Attach access_token
111
- const accessToken = this.authorization?.access_token;
119
+ const accessToken = await this.getAccessToken();
112
120
  // Extract query parameters from URL and combine with options.query
113
121
  const urlParams = {};
114
122
  url.searchParams.forEach((value, key) => {
@@ -259,6 +267,17 @@ class FortytwoIntraV3Client {
259
267
  });
260
268
  return this.validate(res.data, options.schema);
261
269
  }
270
+ async rawRequest(config) {
271
+ const accessToken = await this.getAccessToken();
272
+ return this.axiosInstance.request({
273
+ baseURL: this.base_url,
274
+ ...config,
275
+ headers: {
276
+ ...config.headers,
277
+ Authorization: `Bearer ${accessToken}`,
278
+ },
279
+ });
280
+ }
262
281
  URL(endpoint) {
263
282
  return new URL(endpoint, this.base_url);
264
283
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ibertran/fortytwo-intra-client",
3
- "version": "5.0.0",
3
+ "version": "5.1.0",
4
4
  "main": "dist/index.js",
5
5
  "types": "dist/index.d.ts",
6
6
  "author": {