FRONTEND-507: New thrift client integration (#259)

This commit is contained in:
Ildar Galeev 2021-04-09 18:29:40 +03:00 committed by GitHub
parent fbde1e47c1
commit 99071f0335
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
30 changed files with 257 additions and 56 deletions

4
package-lock.json generated
View File

@ -9015,8 +9015,8 @@
}
},
"fistful-proto": {
"version": "git+ssh://git@github.com/rbkmoney/fistful-proto.git#7b3f125aa7cbc069f740f598295bb56de713c39f",
"from": "git+ssh://git@github.com/rbkmoney/fistful-proto.git#7b3f125aa7cbc069f740f598295bb56de713c39f"
"version": "git+ssh://git@github.com/rbkmoney/fistful-proto.git#e340259cdd3add024f0139e21f0a2453312ef901",
"from": "git+ssh://git@github.com/rbkmoney/fistful-proto.git#e340259cdd3add024f0139e21f0a2453312ef901"
},
"flake-idgen": {
"version": "1.1.0",

View File

@ -40,7 +40,7 @@
"damsel": "git+ssh://git@github.com/rbkmoney/damsel.git#8851c242d2953cc52397af3d916b52b164ffe4c0",
"deanonimus-proto": "github:rbkmoney/deanonimus-proto#b9fab4fd1c7690186efdc5974d113c82bd5765e9",
"file-storage-proto": "git+ssh://git@github.com:rbkmoney/file-storage-proto.git#281e1ca4cea9bf32229a6c389f0dcf5d49c05a0b",
"fistful-proto": "git+ssh://git@github.com/rbkmoney/fistful-proto.git#7b3f125aa7cbc069f740f598295bb56de713c39f",
"fistful-proto": "git+ssh://git@github.com/rbkmoney/fistful-proto.git#e340259cdd3add024f0139e21f0a2453312ef901",
"humanize-duration": "~3.21.0",
"jsonc-parser": "~2.0.2",
"keycloak-angular": "^8.0.1",

View File

View File

@ -0,0 +1 @@
export * from './wallet';

View File

@ -0,0 +1,2 @@
export * from './wallet.module';
export * from './management.service';

View File

@ -0,0 +1,20 @@
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';
import { KeycloakTokenInfoService } from '@cc/app/shared/services';
import { ThriftConnector } from '../../thrift-connector';
import { WalletState, EventRange as EventRangeModel } from '../gen-model/wallet';
import * as Management from './gen-nodejs/Management';
import { EventRange } from './gen-nodejs/base_types';
@Injectable()
export class ManagementService extends ThriftConnector {
constructor(protected keycloakTokenInfoService: KeycloakTokenInfoService) {
super(keycloakTokenInfoService, Management, '/v1/wallet');
}
get(walletID: string, range: EventRangeModel = new EventRange()): Observable<WalletState> {
return this.callThriftServiceMethod('Get', walletID, range);
}
}

View File

@ -0,0 +1,8 @@
import { NgModule } from '@angular/core';
import { ManagementService } from './management.service';
@NgModule({
providers: [ManagementService],
})
export class WalletModule {}

1
src/app/api/index.ts Normal file
View File

@ -0,0 +1 @@
export * from './fistful';

View File

@ -0,0 +1 @@
export * from './thrift-connector';

View File

@ -0,0 +1,44 @@
import { Observable } from 'rxjs';
import { switchMap, shareReplay, map, first } from 'rxjs/operators';
import { KeycloakTokenInfoService } from '@cc/app/shared/services';
import {
connectToThriftService,
prepareThriftServiceMethod,
toConnectOptions,
ThriftService,
ThriftServiceConnection,
} from './utils';
export class ThriftConnector {
private connection$: Observable<ThriftServiceConnection>;
constructor(
protected keycloakTokenInfoService: KeycloakTokenInfoService,
protected service: ThriftService,
protected endpoint: string
) {
this.connection$ = this.keycloakTokenInfoService.decoded$.pipe(
map((token) => toConnectOptions(token)),
switchMap((connectOptions) =>
connectToThriftService(endpoint, service, connectOptions)
),
shareReplay({
bufferSize: 1,
refCount: true,
})
);
}
protected callThriftServiceMethod<T, P extends any[]>(
serviceMethodName: string,
...args: P
): Observable<T> {
return this.connection$.pipe(
first(),
map((connection) => prepareThriftServiceMethod<T>(connection, serviceMethodName)),
switchMap((fn) => fn(...args))
);
}
}

View File

@ -0,0 +1,28 @@
import { Observable } from 'rxjs';
import connectClient from 'woody_js';
import { ConnectOptions } from 'woody_js/src/connect-options';
import { ThriftService, ThriftServiceConnection } from './types';
export const connectToThriftService = (
endpoint: string,
service: ThriftService,
connectionOptions: ConnectOptions,
hostname: string = location.hostname,
port: string = location.port
): Observable<ThriftServiceConnection> =>
new Observable((observer) => {
const connection = connectClient(
hostname,
port,
endpoint,
service,
connectionOptions,
(err) => {
observer.error(err);
observer.complete();
}
);
observer.next(connection);
observer.complete();
});

View File

@ -0,0 +1,4 @@
export * from './connect-to-thrift-service';
export * from './prepare-thrift-service-method';
export * from './to-connect-options';
export * from './types';

View File

@ -0,0 +1,20 @@
import { Observable } from 'rxjs';
import isNil from 'lodash-es/isNil';
import { ThriftServiceConnection, ThriftServiceMethod } from './types';
export const prepareThriftServiceMethod = <T>(
connection: ThriftServiceConnection,
serviceMethodName: string
): ThriftServiceMethod<T> => (...args): Observable<T> =>
new Observable((observer) => {
const serviceMethod = connection[serviceMethodName];
if (isNil(serviceMethod)) {
observer.error(`Service method: "${serviceMethodName}" is not found in thrift client`);
observer.complete();
}
serviceMethod.bind(connection)(...args, (err, result) => {
err ? observer.error(err) : observer.next(result);
observer.complete();
});
});

View File

@ -0,0 +1,32 @@
import { ConnectOptions } from 'woody_js/src/connect-options';
import { KeycloakToken } from '@cc/app/shared/services';
const toDepricatedHeaders = (email: string, username: string, partyID: string, realm: string) => ({
'x-rbk-meta-user-identity.email': email,
'x-rbk-meta-user-identity.realm': realm,
'x-rbk-meta-user-identity.username': username,
'x-rbk-meta-user-identity.id': partyID,
});
const toHeaders = (email: string, username: string, partyID: string, realm: string) => ({
'woody.meta.user-identity.email': email,
'woody.meta.user-identity.realm': realm,
'woody.meta.user-identity.username': username,
'woody.meta.user-identity.id': partyID,
});
export const toConnectOptions = (
{ email, name, sub }: KeycloakToken,
deprecatedHeaders = false,
realm = 'internal'
): ConnectOptions => ({
headers: {
...toHeaders(email, name, sub, realm),
...(deprecatedHeaders ? toDepricatedHeaders(email, name, sub, realm) : undefined),
},
deadlineConfig: {
amount: 3,
unitOfTime: 'm',
},
});

View File

@ -0,0 +1,5 @@
import { Observable } from 'rxjs';
export type ThriftService = any;
export type ThriftServiceConnection = any;
export type ThriftServiceMethod<T> = (...args) => Observable<T>;

View File

@ -16,6 +16,8 @@ import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import * as moment from 'moment';
import 'moment/locale/ru';
import { KeycloakTokenInfoModule } from '@cc/app/shared/services';
import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { ClaimMgtModule } from './claim-mgt/claim-mgt.module';
@ -78,6 +80,7 @@ moment.locale('en');
SearchClaimsModule,
OperationsModule,
DomainConfigModule,
KeycloakTokenInfoModule,
// It is important that NotFoundModule module should be last
NotFoundModule,
],

View File

@ -1,30 +1,35 @@
import { Injectable } from '@angular/core';
import { merge, NEVER, ReplaySubject } from 'rxjs';
import { catchError, switchMap, shareReplay } from 'rxjs/operators';
import { progress } from '@rbkmoney/partial-fetcher/dist/progress';
import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy';
import { BehaviorSubject, NEVER, ReplaySubject, Subject } from 'rxjs';
import { catchError, switchMap, shareReplay, tap } from 'rxjs/operators';
import { WalletManagementService } from '../../../../../thrift-services/fistful/wallet-management.service';
import { ManagementService as WalletManagementService } from '@cc/app/api/fistful';
@UntilDestroy()
@Injectable()
export class ReceiveWalletService {
private receiveWallet$ = new ReplaySubject<string>();
private error$ = new ReplaySubject<boolean>();
private error$ = new Subject<boolean>();
private loading$ = new BehaviorSubject(false);
wallet$ = this.receiveWallet$.pipe(
tap(() => this.loading$.next(true)),
switchMap((id) =>
this.walletManagementService.getWallet(id).pipe(
this.walletManagementService.get(id).pipe(
catchError((e) => {
console.log(e);
console.error(e);
this.loading$.next(false);
this.error$.next(true);
return NEVER;
})
)
),
tap(() => this.loading$.next(false)),
untilDestroyed(this),
shareReplay(1)
);
isLoading$ = progress(this.receiveWallet$, merge(this.wallet$, this.error$));
isLoading$ = this.loading$.asObservable();
hasError$ = this.error$.asObservable();
constructor(private walletManagementService: WalletManagementService) {}

View File

@ -1,4 +0,0 @@
export interface ReceiveWalletParams {
destinationID: string;
identityID: string;
}

View File

@ -1,7 +1,7 @@
<span *ngIf="wallet$ | async as wallet"> {{ walletID }} - {{ wallet.name }} </span>
<span *ngIf="wallet$ | async as wallet"> {{ wallet.id }} - {{ wallet.name }} </span>
<span *ngIf="isLoading$ | async">
Loading...
</span>
<span *ngIf="hasError$ | async">
An error occurred while destination receiving
An error occurred while wallet receiving
</span>

View File

@ -1,7 +1,8 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FistfulModule } from '../../../thrift-services/fistful/fistful.module';
import { WalletModule } from '@cc/app/api/fistful/wallet';
import { WalletInfoComponent } from './wallet-info.component';
const DECLARATIONS = [WalletInfoComponent];
@ -9,6 +10,6 @@ const DECLARATIONS = [WalletInfoComponent];
@NgModule({
declarations: DECLARATIONS,
exports: DECLARATIONS,
imports: [CommonModule, FistfulModule],
imports: [CommonModule, WalletModule],
})
export class WalletInfoModule {}

View File

@ -1,3 +1,4 @@
export * from './query-params-store';
export * from './app-auth-guard';
export * from './fetch-parties.service';
export * from './keycloak-token-info';

View File

@ -0,0 +1,3 @@
export * from './keycloak-token-info.module';
export * from './keycloak-token-info.service';
export * from './types';

View File

@ -0,0 +1,8 @@
import { NgModule } from '@angular/core';
import { KeycloakTokenInfoService } from './keycloak-token-info.service';
@NgModule({
providers: [KeycloakTokenInfoService],
})
export class KeycloakTokenInfoModule {}

View File

@ -0,0 +1,20 @@
import { Injectable } from '@angular/core';
import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy';
import jwt_decode from 'jwt-decode';
import { KeycloakService } from 'keycloak-angular';
import { from, Observable } from 'rxjs';
import { map, shareReplay } from 'rxjs/operators';
import { KeycloakToken } from './types/keycloak-token';
@UntilDestroy()
@Injectable()
export class KeycloakTokenInfoService {
decoded$: Observable<KeycloakToken> = from(this.keycloakService.getToken()).pipe(
map((token) => jwt_decode<KeycloakToken>(token)),
untilDestroyed(this),
shareReplay(1)
);
constructor(private keycloakService: KeycloakService) {}
}

View File

@ -0,0 +1 @@
export * from './keycloak-token';

View File

@ -0,0 +1,24 @@
export interface KeycloakToken {
acr: string;
'allowed-origins': string[];
aud: string;
auth_time: number;
azp: string;
email: string;
exp: number;
family_name: string;
given_name: string;
iat: number;
iss: string;
jti: string;
name: string;
nbf: number;
nonce: string;
preferred_username: string;
realm_access: object;
resource_access: object;
scope: string;
session_state: string;
sub: string;
typ: string;
}

View File

@ -2,15 +2,9 @@ import { NgModule } from '@angular/core';
import { FistfulAdminService } from './fistful-admin.service';
import { RepairerService } from './repairer.service';
import { WalletManagementService } from './wallet-management.service';
import { RevertManagementService } from './revert-management.service';
@NgModule({
providers: [
RepairerService,
FistfulAdminService,
WalletManagementService,
RevertManagementService,
],
providers: [RepairerService, FistfulAdminService, RevertManagementService],
})
export class FistfulModule {}

View File

@ -1,26 +0,0 @@
import { Injectable, NgZone } from '@angular/core';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
import { KeycloakTokenInfoService } from '../../keycloak-token-info.service';
import { ThriftService } from '../services/thrift/thrift-service';
import * as WalletManagement from './gen-nodejs/Management';
import { EventRange, WalletState } from './gen-model/wallet';
import { EventRange as ApiEventRange } from './gen-nodejs/base_types';
@Injectable()
export class WalletManagementService extends ThriftService {
constructor(keycloakTokenInfoService: KeycloakTokenInfoService, zone: NgZone) {
super(zone, keycloakTokenInfoService, '/v1/wallet', WalletManagement);
}
// @TODO thrift have many Get methods inside different Management services, that's why method returns DepositState with WalletState values
getWallet(id: string, range: EventRange = new ApiEventRange()): Observable<WalletState> {
return this.toObservableAction('Get')(id, range).pipe(
map(
(depositState) =>
({ id: depositState.source_id, name: depositState.id } as WalletState)
)
);
}
}

View File

@ -116,8 +116,12 @@ function toPathConfig(
};
}
async function clear({ model, meta, services }: PathsConfig) {
await del([model.outputFolder, ...services.map((s) => s.outputFolder), meta.outputFile]);
async function clear({ model, meta, services }: PathsConfig, outputServiceDirName = 'gen-nodejs') {
await del([
model.outputFolder,
...services.map((s) => path.join(s.outputFolder, outputServiceDirName)),
meta.outputFile,
]);
}
function prepareOutputDirs({ services, outputNamespacePath }: PathsConfig) {

View File

@ -19,7 +19,8 @@
"paths": {
"@cc/components/*": ["src/components/*"],
"@cc/utils/*": ["src/utils/*"],
"@cc/app/shared/*": ["src/app/shared/*"]
"@cc/app/shared/*": ["src/app/shared/*"],
"@cc/app/api/*": ["src/app/api/*"]
}
},
"angularCompilerOptions": {