@ethlete/query 0.1.0-next.16 → 0.1.0-next.18
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/esm2020/lib/auth/auth-provider.types.mjs +1 -1
- package/esm2020/lib/auth/auth-provider.utils.mjs +1 -1
- package/esm2020/lib/auth/basic-auth-provider.mjs +1 -2
- package/esm2020/lib/auth/bearer-auth-provider.mjs +84 -91
- package/esm2020/lib/auth/custom-header-auth-provider.mjs +1 -2
- package/esm2020/lib/query/query.utils.mjs +8 -3
- package/esm2020/lib/query-client/query-client.mjs +1 -2
- package/fesm2015/ethlete-query.mjs +172 -179
- package/fesm2015/ethlete-query.mjs.map +1 -1
- package/fesm2020/ethlete-query.mjs +167 -172
- package/fesm2020/ethlete-query.mjs.map +1 -1
- package/lib/auth/auth-provider.types.d.ts +13 -33
- package/lib/auth/auth-provider.utils.d.ts +2 -1
- package/lib/auth/basic-auth-provider.d.ts +0 -2
- package/lib/auth/bearer-auth-provider.d.ts +12 -18
- package/lib/auth/custom-header-auth-provider.d.ts +0 -2
- package/lib/query/query.utils.d.ts +3 -1
- package/package.json +1 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { getCookie,
|
|
2
|
-
import {
|
|
1
|
+
import { getCookie, setCookie, deleteCookie, DestroyService } from '@ethlete/core';
|
|
2
|
+
import { filter, takeWhile, switchMap, of, BehaviorSubject, Subject, interval, takeUntil, tap, timer, fromEvent, startWith, concat, take, debounceTime, combineLatest, map, distinctUntilChanged } from 'rxjs';
|
|
3
3
|
import * as i0 from '@angular/core';
|
|
4
4
|
import { Directive, Input, InjectionToken, inject, ChangeDetectorRef, ViewContainerRef, TemplateRef, ErrorHandler, ElementRef } from '@angular/core';
|
|
5
5
|
import { FormGroup } from '@angular/forms';
|
|
@@ -11,7 +11,6 @@ class CustomHeaderAuthProvider {
|
|
|
11
11
|
}
|
|
12
12
|
constructor(_config) {
|
|
13
13
|
this._config = _config;
|
|
14
|
-
this.queryClient = null;
|
|
15
14
|
}
|
|
16
15
|
cleanUp() {
|
|
17
16
|
// noop
|
|
@@ -27,13 +26,35 @@ class BasicAuthProvider {
|
|
|
27
26
|
}
|
|
28
27
|
constructor(_config) {
|
|
29
28
|
this._config = _config;
|
|
30
|
-
this.queryClient = null;
|
|
31
29
|
}
|
|
32
30
|
cleanUp() {
|
|
33
31
|
// noop
|
|
34
32
|
}
|
|
35
33
|
}
|
|
36
34
|
|
|
35
|
+
const getOpName = /(query|mutation) ?([\w\d-_]+)? ?\(.*?\)? \{/;
|
|
36
|
+
const transformGql = (str) => {
|
|
37
|
+
str = Array.isArray(str) ? str.join('') : str;
|
|
38
|
+
const name = getOpName.exec(str);
|
|
39
|
+
return function (variables) {
|
|
40
|
+
const data = { query: str };
|
|
41
|
+
if (variables)
|
|
42
|
+
data['variables'] = JSON.stringify(variables);
|
|
43
|
+
if (name && name.length) {
|
|
44
|
+
const operationName = name[2];
|
|
45
|
+
if (operationName)
|
|
46
|
+
data['operationName'] = name[2];
|
|
47
|
+
}
|
|
48
|
+
return JSON.stringify(data);
|
|
49
|
+
};
|
|
50
|
+
};
|
|
51
|
+
const gql = (strings, ...values) => {
|
|
52
|
+
const str = strings.reduce((acc, cur, i) => {
|
|
53
|
+
return acc + cur + (values[i] || '');
|
|
54
|
+
}, '');
|
|
55
|
+
return str;
|
|
56
|
+
};
|
|
57
|
+
|
|
37
58
|
const buildErrorMessage = (code, message) => `[@ethlete\\query:${code}] ${message}`;
|
|
38
59
|
class QueryError extends Error {
|
|
39
60
|
constructor(code, message, data) {
|
|
@@ -293,171 +314,6 @@ const request = async (options) => {
|
|
|
293
314
|
}
|
|
294
315
|
};
|
|
295
316
|
|
|
296
|
-
const isBasicAuthProvider = (authProvider) => authProvider instanceof BasicAuthProvider;
|
|
297
|
-
const isBearerAuthProvider = (authProvider) => authProvider instanceof BearerAuthProvider;
|
|
298
|
-
const isCustomHeaderAuthProvider = (authProvider) => authProvider instanceof CustomHeaderAuthProvider;
|
|
299
|
-
const decryptBearer = (token) => {
|
|
300
|
-
try {
|
|
301
|
-
const base64Url = token.split('.')[1];
|
|
302
|
-
const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/');
|
|
303
|
-
const jsonPayload = decodeURIComponent(atob(base64)
|
|
304
|
-
.split('')
|
|
305
|
-
.map((c) => '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2))
|
|
306
|
-
.join(''));
|
|
307
|
-
return JSON.parse(jsonPayload);
|
|
308
|
-
}
|
|
309
|
-
catch (error) {
|
|
310
|
-
console.error(`Invalid bearer token: ${token}`, error);
|
|
311
|
-
return null;
|
|
312
|
-
}
|
|
313
|
-
};
|
|
314
|
-
|
|
315
|
-
class BearerAuthProvider {
|
|
316
|
-
get header() {
|
|
317
|
-
return { Authorization: `Bearer ${this._token}` };
|
|
318
|
-
}
|
|
319
|
-
constructor(_config) {
|
|
320
|
-
this._config = _config;
|
|
321
|
-
this._token = null;
|
|
322
|
-
this._refreshToken = null;
|
|
323
|
-
this._refreshTimerSubscription = null;
|
|
324
|
-
this._failureCount = 0;
|
|
325
|
-
this.queryClient = null;
|
|
326
|
-
this.onRefreshInitiation$ = new Subject();
|
|
327
|
-
this.onRefreshSuccess$ = new Subject();
|
|
328
|
-
this.onRefreshFailure$ = new Subject();
|
|
329
|
-
this._token = _config.token || null;
|
|
330
|
-
this._refreshToken = _config.refreshConfig?.token ?? null;
|
|
331
|
-
if (this._config.refreshConfig && this._token) {
|
|
332
|
-
this._setupRefresh(this._config.refreshConfig);
|
|
333
|
-
}
|
|
334
|
-
else if (this._config.refreshConfig?.cookieName) {
|
|
335
|
-
const cookie = getCookie(this._config.refreshConfig.cookieName);
|
|
336
|
-
if (!cookie) {
|
|
337
|
-
this._autoDestruct();
|
|
338
|
-
return;
|
|
339
|
-
}
|
|
340
|
-
this._refreshToken = cookie;
|
|
341
|
-
this._refresh(this._config.refreshConfig);
|
|
342
|
-
}
|
|
343
|
-
else {
|
|
344
|
-
this._autoDestruct();
|
|
345
|
-
console.error('A BearerAuthProvider was created without token or refresh token. You should provide at least a cookieName where the refresh token might be stored.');
|
|
346
|
-
}
|
|
347
|
-
}
|
|
348
|
-
cleanUp(config) {
|
|
349
|
-
this._refreshTimerSubscription?.unsubscribe();
|
|
350
|
-
this._refreshTimerSubscription = null;
|
|
351
|
-
if (this._config.refreshConfig?.cookieName && !config?.keepCookie) {
|
|
352
|
-
deleteCookie(this._config.refreshConfig.cookieName, '/');
|
|
353
|
-
}
|
|
354
|
-
}
|
|
355
|
-
_autoDestruct() {
|
|
356
|
-
this.cleanUp();
|
|
357
|
-
this.queryClient?.clearAuthProvider();
|
|
358
|
-
}
|
|
359
|
-
_setupRefresh(config) {
|
|
360
|
-
this.cleanUp({ keepCookie: true });
|
|
361
|
-
if (!this._token) {
|
|
362
|
-
return;
|
|
363
|
-
}
|
|
364
|
-
const bearer = decryptBearer(this._token);
|
|
365
|
-
if (!bearer) {
|
|
366
|
-
return;
|
|
367
|
-
}
|
|
368
|
-
const expiresInPropertyName = config.expiresInPropertyName || 'exp';
|
|
369
|
-
const expiresIn = bearer[expiresInPropertyName];
|
|
370
|
-
const fiveMinutes = 5 * 60 * 1000;
|
|
371
|
-
const refreshBuffer = config.refreshBuffer ?? fiveMinutes;
|
|
372
|
-
if (expiresIn === undefined) {
|
|
373
|
-
throw new Error(`Bearer token does not contain an '${expiresInPropertyName}' property`);
|
|
374
|
-
}
|
|
375
|
-
const remainingTime = new Date(expiresIn * 1000).getTime() - refreshBuffer - new Date().getTime();
|
|
376
|
-
const strategy = config.strategy ?? "beforeExpiration" /* AuthBearerRefreshStrategy.BeforeExpiration */;
|
|
377
|
-
switch (strategy) {
|
|
378
|
-
case "beforeExpiration" /* AuthBearerRefreshStrategy.BeforeExpiration */:
|
|
379
|
-
this._refreshTimerSubscription = timer(remainingTime).subscribe(() => this._refresh(config));
|
|
380
|
-
break;
|
|
381
|
-
}
|
|
382
|
-
}
|
|
383
|
-
async _refresh(config) {
|
|
384
|
-
if (!this._refreshToken) {
|
|
385
|
-
throw new Error('No refresh token found');
|
|
386
|
-
}
|
|
387
|
-
if (!this.queryClient) {
|
|
388
|
-
throw new Error('Query client instance is null');
|
|
389
|
-
}
|
|
390
|
-
const { method, route, requestAdapter, responseAdapter, paramLocation } = config;
|
|
391
|
-
const data = requestAdapter?.(this._refreshToken) ?? { refreshToken: this._refreshToken };
|
|
392
|
-
const fullRoute = buildRoute({
|
|
393
|
-
base: this.queryClient.config.baseRoute,
|
|
394
|
-
route,
|
|
395
|
-
queryParams: paramLocation === 'query' ? data : undefined,
|
|
396
|
-
});
|
|
397
|
-
const requestInit = { body: paramLocation === 'body' ? buildBody(data) : undefined, method };
|
|
398
|
-
this.onRefreshInitiation$.next();
|
|
399
|
-
try {
|
|
400
|
-
const result = await request({
|
|
401
|
-
route: fullRoute,
|
|
402
|
-
init: requestInit,
|
|
403
|
-
});
|
|
404
|
-
const tokens = responseAdapter ? responseAdapter(result.data) : result.data;
|
|
405
|
-
if (!tokens.token) {
|
|
406
|
-
throw new Error('Token not found in response');
|
|
407
|
-
}
|
|
408
|
-
if (!tokens.refreshToken) {
|
|
409
|
-
throw new Error('Refresh token not found in response');
|
|
410
|
-
}
|
|
411
|
-
this._token = tokens.token;
|
|
412
|
-
this._refreshToken = tokens.refreshToken;
|
|
413
|
-
this.onRefreshSuccess$.next(tokens);
|
|
414
|
-
if (config.cookieName) {
|
|
415
|
-
setCookie(config.cookieName, tokens.refreshToken);
|
|
416
|
-
}
|
|
417
|
-
this._setupRefresh(config);
|
|
418
|
-
this._failureCount = 0;
|
|
419
|
-
}
|
|
420
|
-
catch (error) {
|
|
421
|
-
if (error instanceof Error) {
|
|
422
|
-
this.onRefreshFailure$.next(await buildRequestError(error, fullRoute, requestInit));
|
|
423
|
-
}
|
|
424
|
-
else {
|
|
425
|
-
this.onRefreshFailure$.next(error);
|
|
426
|
-
}
|
|
427
|
-
this._failureCount++;
|
|
428
|
-
if (this._failureCount <= (config.maxRefreshAttempts ?? 3)) {
|
|
429
|
-
this._setupRefresh(config);
|
|
430
|
-
}
|
|
431
|
-
else {
|
|
432
|
-
this._autoDestruct();
|
|
433
|
-
}
|
|
434
|
-
}
|
|
435
|
-
}
|
|
436
|
-
}
|
|
437
|
-
|
|
438
|
-
const getOpName = /(query|mutation) ?([\w\d-_]+)? ?\(.*?\)? \{/;
|
|
439
|
-
const transformGql = (str) => {
|
|
440
|
-
str = Array.isArray(str) ? str.join('') : str;
|
|
441
|
-
const name = getOpName.exec(str);
|
|
442
|
-
return function (variables) {
|
|
443
|
-
const data = { query: str };
|
|
444
|
-
if (variables)
|
|
445
|
-
data['variables'] = JSON.stringify(variables);
|
|
446
|
-
if (name && name.length) {
|
|
447
|
-
const operationName = name[2];
|
|
448
|
-
if (operationName)
|
|
449
|
-
data['operationName'] = name[2];
|
|
450
|
-
}
|
|
451
|
-
return JSON.stringify(data);
|
|
452
|
-
};
|
|
453
|
-
};
|
|
454
|
-
const gql = (strings, ...values) => {
|
|
455
|
-
const str = strings.reduce((acc, cur, i) => {
|
|
456
|
-
return acc + cur + (values[i] || '');
|
|
457
|
-
}, '');
|
|
458
|
-
return str;
|
|
459
|
-
};
|
|
460
|
-
|
|
461
317
|
function filterSuccess() {
|
|
462
318
|
return function (source) {
|
|
463
319
|
return source.pipe(filter((value) => isQueryStateSuccess(value)));
|
|
@@ -473,9 +329,14 @@ function filterQueryStates(allowedStates) {
|
|
|
473
329
|
return source.pipe(filter((value) => value && allowedStates.includes(value.type)));
|
|
474
330
|
};
|
|
475
331
|
}
|
|
476
|
-
function takeUntilResponse() {
|
|
332
|
+
function takeUntilResponse(config) {
|
|
477
333
|
return function (source) {
|
|
478
|
-
return source.pipe(takeWhile((value) =>
|
|
334
|
+
return source.pipe(takeWhile((value) => {
|
|
335
|
+
if (config?.excludeNull && value === null) {
|
|
336
|
+
return false;
|
|
337
|
+
}
|
|
338
|
+
return isQueryStateLoading(value) || value === null;
|
|
339
|
+
}, true));
|
|
479
340
|
};
|
|
480
341
|
}
|
|
481
342
|
function filterNull() {
|
|
@@ -717,6 +578,141 @@ class Query {
|
|
|
717
578
|
}
|
|
718
579
|
}
|
|
719
580
|
|
|
581
|
+
const isBasicAuthProvider = (authProvider) => authProvider instanceof BasicAuthProvider;
|
|
582
|
+
const isBearerAuthProvider = (authProvider) => authProvider instanceof BearerAuthProvider;
|
|
583
|
+
const isCustomHeaderAuthProvider = (authProvider) => authProvider instanceof CustomHeaderAuthProvider;
|
|
584
|
+
const decryptBearer = (token) => {
|
|
585
|
+
try {
|
|
586
|
+
const base64Url = token.split('.')[1];
|
|
587
|
+
const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/');
|
|
588
|
+
const jsonPayload = decodeURIComponent(atob(base64)
|
|
589
|
+
.split('')
|
|
590
|
+
.map((c) => '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2))
|
|
591
|
+
.join(''));
|
|
592
|
+
return JSON.parse(jsonPayload);
|
|
593
|
+
}
|
|
594
|
+
catch (error) {
|
|
595
|
+
console.error(`Invalid bearer token: ${token}`, error);
|
|
596
|
+
return null;
|
|
597
|
+
}
|
|
598
|
+
};
|
|
599
|
+
|
|
600
|
+
class BearerAuthProvider {
|
|
601
|
+
get header() {
|
|
602
|
+
return { Authorization: `Bearer ${this._tokens$.getValue().token}` };
|
|
603
|
+
}
|
|
604
|
+
get tokens$() {
|
|
605
|
+
return this._tokens$.asObservable();
|
|
606
|
+
}
|
|
607
|
+
get tokens() {
|
|
608
|
+
return this._tokens$.getValue();
|
|
609
|
+
}
|
|
610
|
+
get currentRefreshQuery$() {
|
|
611
|
+
return this._currentRefreshQuery$.asObservable();
|
|
612
|
+
}
|
|
613
|
+
get currentRefreshQuery() {
|
|
614
|
+
return this._currentRefreshQuery$.getValue();
|
|
615
|
+
}
|
|
616
|
+
constructor(_config) {
|
|
617
|
+
this._config = _config;
|
|
618
|
+
this._destroy$ = new Subject();
|
|
619
|
+
this._currentRefreshQuery$ = new BehaviorSubject(null);
|
|
620
|
+
this._tokens$ = new BehaviorSubject({
|
|
621
|
+
token: null,
|
|
622
|
+
refreshToken: null,
|
|
623
|
+
});
|
|
624
|
+
const cookieToken = _config.refreshConfig?.cookieName ? getCookie(_config.refreshConfig.cookieName) ?? null : null;
|
|
625
|
+
this._tokens$.next({
|
|
626
|
+
token: _config.token || null,
|
|
627
|
+
refreshToken: _config.refreshConfig?.token || cookieToken || null,
|
|
628
|
+
});
|
|
629
|
+
if (!this.tokens.token && !this.tokens.refreshToken) {
|
|
630
|
+
if (!_config.refreshConfig?.cookieName) {
|
|
631
|
+
console.error('A BearerAuthProvider was created without token or refresh token. You should provide at least a cookieName where the refresh token might be stored.');
|
|
632
|
+
}
|
|
633
|
+
return;
|
|
634
|
+
}
|
|
635
|
+
if (this.tokens.token) {
|
|
636
|
+
this._prepareForRefresh();
|
|
637
|
+
if (this.tokens.refreshToken && this._config.refreshConfig?.cookieName) {
|
|
638
|
+
setCookie(this._config.refreshConfig.cookieName, this.tokens.refreshToken);
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
else if (this.tokens.refreshToken) {
|
|
642
|
+
this._refreshQuery();
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
cleanUp() {
|
|
646
|
+
this._destroy$.next(true);
|
|
647
|
+
this._destroy$.unsubscribe();
|
|
648
|
+
this._currentRefreshQuery$.next(null);
|
|
649
|
+
if (this._config.refreshConfig?.cookieName) {
|
|
650
|
+
deleteCookie(this._config.refreshConfig.cookieName, '/');
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
_prepareForRefresh() {
|
|
654
|
+
if (!this.tokens.token || !this._config.refreshConfig) {
|
|
655
|
+
return;
|
|
656
|
+
}
|
|
657
|
+
const bearer = decryptBearer(this.tokens.token);
|
|
658
|
+
if (!bearer) {
|
|
659
|
+
return;
|
|
660
|
+
}
|
|
661
|
+
const expiresInPropertyName = this._config.refreshConfig.expiresInPropertyName || 'exp';
|
|
662
|
+
const expiresIn = bearer[expiresInPropertyName];
|
|
663
|
+
const fiveMinutes = 5 * 60 * 1000;
|
|
664
|
+
const refreshBuffer = this._config.refreshConfig.refreshBuffer ?? fiveMinutes;
|
|
665
|
+
if (expiresIn === undefined) {
|
|
666
|
+
throw new Error(`Bearer token does not contain an '${expiresInPropertyName}' property`);
|
|
667
|
+
}
|
|
668
|
+
const remainingTime = new Date(expiresIn * 1000).getTime() - refreshBuffer - new Date().getTime();
|
|
669
|
+
const strategy = this._config.refreshConfig.strategy ?? "beforeExpiration" /* AuthBearerRefreshStrategy.BeforeExpiration */;
|
|
670
|
+
switch (strategy) {
|
|
671
|
+
case "beforeExpiration" /* AuthBearerRefreshStrategy.BeforeExpiration */:
|
|
672
|
+
timer(remainingTime)
|
|
673
|
+
.pipe(takeUntil(this._destroy$))
|
|
674
|
+
.subscribe(() => this._refreshQuery());
|
|
675
|
+
break;
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
_refreshQuery() {
|
|
679
|
+
if (!this.tokens.refreshToken || !this._config.refreshConfig) {
|
|
680
|
+
return;
|
|
681
|
+
}
|
|
682
|
+
const args = this._config.refreshConfig.requestArgsAdapter?.(this.tokens) ?? {
|
|
683
|
+
body: { refreshToken: this.tokens.refreshToken },
|
|
684
|
+
};
|
|
685
|
+
const query = this._config.refreshConfig.queryCreator.prepare(args).execute({ skipCache: true });
|
|
686
|
+
this._currentRefreshQuery$.next(query);
|
|
687
|
+
this._currentRefreshQuery$
|
|
688
|
+
.pipe(switchQueryState(), tap((state) => {
|
|
689
|
+
if (isQueryStateSuccess(state)) {
|
|
690
|
+
if (this._config.refreshConfig?.responseAdapter) {
|
|
691
|
+
const tokens = this._config.refreshConfig.responseAdapter(state.response);
|
|
692
|
+
this._tokens$.next(tokens);
|
|
693
|
+
}
|
|
694
|
+
else {
|
|
695
|
+
this._tokens$.next({
|
|
696
|
+
token: state.response['token'],
|
|
697
|
+
refreshToken: state.response['refreshToken'],
|
|
698
|
+
});
|
|
699
|
+
}
|
|
700
|
+
if (this._config.refreshConfig?.cookieName && this.tokens.refreshToken) {
|
|
701
|
+
setCookie(this._config.refreshConfig.cookieName, this.tokens.refreshToken);
|
|
702
|
+
}
|
|
703
|
+
this._prepareForRefresh();
|
|
704
|
+
}
|
|
705
|
+
else if (isQueryStateFailure(state)) {
|
|
706
|
+
this._tokens$.next({ token: null, refreshToken: null });
|
|
707
|
+
if (this._config.refreshConfig?.cookieName) {
|
|
708
|
+
deleteCookie(this._config.refreshConfig.cookieName, '/');
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
}), takeUntilResponse(), takeUntil(this._destroy$))
|
|
712
|
+
.subscribe();
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
|
|
720
716
|
class QueryDirective {
|
|
721
717
|
get query() {
|
|
722
718
|
return this._query;
|
|
@@ -1328,7 +1324,6 @@ class QueryClient {
|
|
|
1328
1324
|
throw new Error('The auth provider is already set. Please call clearAuthProvider() first.');
|
|
1329
1325
|
}
|
|
1330
1326
|
this._authProvider$.next(authProvider);
|
|
1331
|
-
authProvider.queryClient = this;
|
|
1332
1327
|
};
|
|
1333
1328
|
this.clearAuthProvider = () => {
|
|
1334
1329
|
this.authProvider?.cleanUp();
|