How can get return value in pipe? - rxjs

I have create(newItem) method which creates item on database and returns id (Observable) and getDetails(id) which returns Observable
import { mergeMap as _observableMergeMap, catchError as _observableCatch } from 'rxjs/operators';
import { Observable, throwError as _observableThrow, of as _observableOf } from 'rxjs';
create(input: Input | null | undefined): Observable<number> {
let url_ = this.baseUrl + "/api/services/app/create";
url_ = url_.replace(/[?&]$/, "");
const content_ = JSON.stringify(input);
let options_ : any = {
body: content_,
observe: "response",
responseType: "blob",
headers: new HttpHeaders({
"Content-Type": "application/json",
"Accept": "application/json"
})
};
return this.http.request("post", url_, options_).pipe(_observableMergeMap((response_ : any) => {
return this.processCreateAsync(response_);
})).pipe(_observableCatch((response_: any) => {
if (response_ instanceof HttpResponseBase) {
try {
return this.processCreateAsync(<any>response_);
} catch (e) {
return <Observable<number>><any>_observableThrow(e);
}
} else
return <Observable<number>><any>_observableThrow(response_);
}));
}
I am trying to call create method and get id from create method and get details with this id.
this._dataService.create(newItem)
.pipe(
switchMap( id => {
return this._dataService.getDetails(id);
})
)
.subscribe(result => {
// Do stuff with result
},
error => {
console.log(error);
}));
But I get an error which is "id is not defined" in switchMap
How can I get return value from create method and pass to getDetails method?

In your code example, the variable result in the subscribe should hold the value of id.

I downgraded rxjs to 6.2.0 and #types/node to 10.1.4 and it works now.

Related

Providing two combined Reducers for my redux saga store prevents my websocket channel message from triggering, but only one does not?

Configured my store this way with redux toolkit for sure
const rootReducer = combineReducers({
someReducer,
systemsConfigs
});
const store = return configureStore({
devTools: true,
reducer: rootReducer ,
// middleware: [middleware, logger],
middleware: (getDefaultMiddleware) => getDefaultMiddleware({ thunk: false }).concat(middleware),
});
middleware.run(sagaRoot)
And thats my channel i am connecting to it
export function createSocketChannel(
productId: ProductId,
pair: string,
createSocket = () => new WebSocket('wss://somewebsocket')
) {
return eventChannel<SocketEvent>((emitter) => {
const socket_OrderBook = createSocket();
socket_OrderBook.addEventListener('open', () => {
emitter({
type: 'connection-established',
payload: true,
});
socket_OrderBook.send(
`subscribe-asdqwe`
);
});
socket_OrderBook.addEventListener('message', (event) => {
if (event.data?.includes('bids')) {
emitter({
type: 'message',
payload: JSON.parse(event.data),
});
//
}
});
socket_OrderBook.addEventListener('close', (event: any) => {
emitter(new SocketClosedByServer());
});
return () => {
if (socket_OrderBook.readyState === WebSocket.OPEN) {
socket_OrderBook.send(
`unsubscribe-order-book-${pair}`
);
}
if (socket_OrderBook.readyState === WebSocket.OPEN || socket_OrderBook.readyState === WebSocket.CONNECTING) {
socket_OrderBook.close();
}
};
}, buffers.expanding<SocketEvent>());
}
And here's how my saga connecting handlers looks like
export function* handleConnectingSocket(ctx: SagaContext) {
try {
const productId = yield select((state: State) => state.productId);
const requested_pair = yield select((state: State) => state.requested_pair);
if (ctx.socketChannel === null) {
ctx.socketChannel = yield call(createSocketChannel, productId, requested_pair);
}
//
const message: SocketEvent = yield take(ctx.socketChannel!);
if (message.type !== 'connection-established') {
throw new SocketUnexpectedResponseError();
}
yield put(connectedSocket());
} catch (error: any) {
reportError(error);
yield put(
disconnectedSocket({
reason: SocketStateReasons.BAD_CONNECTION,
})
);
}
}
export function* handleConnectedSocket(ctx: SagaContext) {
try {
while (true) {
if (ctx.socketChannel === null) {
break;
}
const events = yield flush(ctx.socketChannel);
const startedExecutingAt = performance.now();
if (Array.isArray(events)) {
const deltas = events.reduce(
(patch, event) => {
if (event.type === 'message') {
patch.bids.push(...event.payload.data?.bids);
patch.asks.push(...event.payload.data?.asks);
//
}
//
return patch;
},
{ bids: [], asks: [] } as SocketMessage
);
if (deltas.bids.length || deltas.asks.length) {
yield putResolve(receivedDeltas(deltas));
}
}
yield call(delayNextDispatch, startedExecutingAt);
}
} catch (error: any) {
reportError(error);
yield put(
disconnectedSocket({
reason: SocketStateReasons.UNKNOWN,
})
);
}
}
After Debugging I got the following:
The Thing is that when I Provide one Reducer to my store the channel works well and data is fetched where as when providing combinedReducers I am getting
an established connection from my handleConnectingSocket generator function
and an empty event array [] from
const events = yield flush(ctx.socketChannel) written in handleConnectedSocket
Tried to clarify as much as possible
ok so I start refactoring my typescript by changing the types, then saw all the places that break, there was a problem in my sagas.tsx.
Ping me if someone faced such an issue in the future

Provide explicit type for the mutation GraphQL

I'm trying to create a mutation which will accept list of products. But doing so GraphQL is throwing error for createMultipleProducts method. Not sure what is the mistake here.
import { Inject } from "#nestjs/common";
import { Args, Mutation, Query, Resolver } from "#nestjs/graphql";
import { ClientProxy } from "#nestjs/microservices";
import { ProductRequest } from "src/types/ms-product/product.request.type";
import { ProductResponse } from "src/types/ms-product/product.response.type";
#Resolver(of => ProductResponse)
export class ProductResolver {
constructor(
#Inject('SERVICE__PRODUCT') private readonly clientServiceProduct: ClientProxy
) {}
#Mutation(returns => ProductResponse)
async createProduct(#Args('data') product: ProductRequest): Promise<ProductResponse> {
const PATTERN = {cmd: 'ms-product-create'};
const PAYLOAD = product;
return this.clientServiceProduct.send(PATTERN, PAYLOAD)
.toPromise()
.then((response: ProductResponse) => {
return response;
})
.catch((error) => {
return error;
})
}
#Mutation(returns => [ProductResponse])
async createMultipleProducts(#Args('data') products: [ProductRequest]): Promise<Array<ProductResponse>> {
try {
const PROMISES = products.map(async (product: ProductRequest) => {
const PATTERN = {cmd: 'ms-product-create'};
const PAYLOAD = product;
return await this.clientServiceProduct.send(PATTERN, PAYLOAD).toPromise();
});
return await Promise.all(PROMISES);
} catch (error) {
throw new Error(error);
}
}
#Query(returns => ProductResponse)
async readProduct(#Args('data') id: string) {
return {}
}
}
I'm getting this error:
UnhandledPromiseRejectionWarning: Error: Undefined type error. Make sure you are providing an explicit type for the "createMultipleProducts" (parameter at index [0]) of the "ProductResolver" class.
There is a need to inform GraphQL the type of the arguments explicitly if it is a complex object array.
#Mutation(returns => [ProductResponse])
async createMultipleProducts(#Args({ name: 'data', type: () => [ProductRequest] }) products: ProductRequest[]): Promise<Array<ProductResponse>> {
...
}
New format 2022
#Query(returns => ProductResponse)
async readProduct(#Args('data', () => String ) id: string) {
return {}
}

Keeping error information and the outer observable alive

To ensure an error doesn't complete the outer observable, a common rxjs effects pattern I've adopted is:
public saySomething$: Observable<Action> = createEffect(() => {
return this.actions.pipe(
ofType<AppActions.SaySomething>(AppActions.SAY_SOMETHING),
// Switch to the result of the inner observable.
switchMap((action) => {
// This service could fail.
return this.service.saySomething(action.payload).pipe(
// Return `null` to keep the outer observable alive!
catchError((error) => {
// What can I do with error here?
return of(null);
})
)
}),
// The result could be null because something could go wrong.
tap((result: Result | null) => {
if (result) {
// Do something with the result!
}
}),
// Update the store state.
map((result: Result | null) => {
if (result) {
return new AppActions.SaySomethingSuccess(result);
}
// It would be nice if I had access the **error** here.
return new AppActions.SaySomethingFail();
}));
});
Notice that I'm using catchError on the inner observable to keep the outer observable alive if the underlying network call fails (service.saySomething(action.payload)):
catchError((error) => {
// What can I do with error here?
return of(null);
})
The subsequent tap and map operators accommodate this in their signatures by allowing null, i.e. (result: Result | null). However, I lose the error information. Ultimately when the final map method returns new AppActions.SaySomethingFail(); I have lost any information about the error.
How can I keep the error information throughout the pipe rather than losing it at the point it's caught?
As suggested in comments you should use Type guard function
Unfortunately I can't run typescript in snippet so I commented types
const { of, throwError, operators: {
switchMap,
tap,
map,
catchError
}
} = rxjs;
const actions = of({payload: 'data'});
const service = {
saySomething: () => throwError(new Error('test'))
}
const AppActions = {
}
AppActions.SaySomethingSuccess = function () {
}
AppActions.SaySomethingFail = function() {
}
/* Type guard */
function isError(value/*: Result | Error*/)/* value is Error*/ {
return value instanceof Error;
}
const observable = actions.pipe(
switchMap((action) => {
return service.saySomething(action.payload).pipe(
catchError((error) => {
return of(error);
})
)
}),
tap((result/*: Result | Error*/) => {
if (isError(result)) {
console.log('tap error')
return;
}
console.log('tap result');
}),
map((result/*: Result | Error*/) => {
if (isError(result)) {
console.log('map error')
return new AppActions.SaySomethingFail();
}
console.log('map result');
return new AppActions.SaySomethingSuccess(result);
}));
observable.subscribe(_ => {
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/rxjs/6.5.5/rxjs.umd.js"></script>
I wouldn't try to keep the error information throughout the pipe. Instead you should separate your success pipeline (tap, map) from your error pipeline (catchError) by adding all operators to the observable whose result they should actually work with, i.e. your inner observable.
public saySomething$: Observable<Action> = createEffect(() => {
return this.actions.pipe(
ofType<AppActions.SaySomething>(AppActions.SAY_SOMETHING),
switchMap((action) => this.service.saySomething(action.payload).pipe(
tap((result: Result) => {
// Do something with the result!
}),
// Update the store state.
map((result: Result) => {
return new AppActions.SaySomethingSuccess(result);
}),
catchError((error) => {
// I can access the **error** here.
return of(new AppActions.SaySomethingFail());
})
)),
);
});
This way tap and map will only be executed on success results from this.service.saySomething. Move all your error side effects and error mapping into catchError.

Nest.js handling errors for HttpService

I'm trying to test NestJS's built in HttpService (which is based on Axios). I'm having trouble testing error/exception states though. In my test suite I have:
let client: SomeClearingFirmClient;
const mockConfigService = {
get: jest.fn((type) => {
switch(type) {
case 'someApiBaseUrl': {
return 'http://example.com'
}
case 'someAddAccountEndpoint': {
return '/ClientAccounts/Add';
}
case 'someApiKey': {
return 'some-api-key';
}
default:
return 'test';
}
}),
};
const successfulAdd: AxiosResponse = {
data: {
batchNo: '39cba402-bfa9-424c-b265-1c98204df7ea',
warning: '',
},
status: 200,
statusText: 'OK',
headers: {},
config: {},
};
const failAddAuth: AxiosError = {
code: '401',
config: {},
name: '',
message: 'Not Authorized',
}
const mockHttpService = {
post: jest.fn(),
get: jest.fn(),
}
it('Handles a failure', async () => {
expect.assertions(1);
mockHttpService.post = jest.fn(() => of(failAddAuth));
const module: TestingModule = await Test.createTestingModule({
providers: [
{
provide: ConfigService,
useValue: mockConfigService,
},
{
provide: HttpService,
useValue: mockHttpService,
},
SomeClearingFirmClient,
],
}).compile();
client = module.get<SomeClearingFirmClient>(SomeClearingFirmClient);
const payload = new SomeClearingPayload();
try {
await client.addAccount(payload);
} catch(e) {
console.log('e', e);
}
});
And my implementation is:
async addAccount(payload: any): Promise<SomeAddResponse> {
const addAccountEndpoint = this.configService.get('api.someAddAccountEndpoint');
const url = `${this.baseUrl}${addAccountEndpoint}?apiKey=${this.apiKey}`;
const config = {
headers: {
'Content-Type': 'application/json',
}
};
const response = this.httpService.post(url, payload, config)
.pipe(
map(res => {
return res.data;
}),
catchError(e => {
throw new HttpException(e.response.data, e.response.status);
}),
).toPromise().catch(e => {
throw new HttpException(e.message, e.code);
});
return response;
}
Regardless of whether I use Observables or Promises, I can't get anything to catch. 4xx level errors sail on through as a success. I feel like I remember Axios adding some sort of config option to reject/send an Observable error to subscribers on failures... but I could be imagining that. Am I doing something wrong in my test harness? The other StackOverflow posts I've seen seem to say that piping through catchError should do the trick, but my errors are going through the map operator.
Your mockHttpService seems to return no error, but a value:
mockHttpService.post = jest.fn(() => of(failAddAuth));
What of(failAddAuth) does is to emit a value(failAddAuth) and then complete.
That's why the catchError from this.httpService.post(url, payload, config) will never be reached, because no errors occur.
In order to make sure that catchError is hit, the observable returned by post() must emit an error notification.
You could try this:
// Something to comply with `HttpException`'s arguments
const err = { response: 'resp', status: '4xx' };
mockHttpService.post = jest.fn(() => throwError(err));
throwError(err) is the same as new Observable(s => s.error(err))(Source code).

Passing value from one RxJS operator to another

Here is my code:
#Injectable()
export class TraitementDetailEffects {
ingoing_loadDetail: { traitementID: number, obs: Promise<any> };
#Effect()
loadTraitementDetail$: Observable<Action> = this.actions$.pipe(
ofType(ETraitementDetailActions.loadTraitementDetail),
map((action: LoadTraitementDetail) => action.payload),
switchMap((traitementID) => {
if (this.ingoing_loadDetail && this.ingoing_loadDetail.traitementID === traitementID) {
return this.ingoing_loadDetail.obs;
}
const obs = this.traitementsService.loadDetail(traitementID);
this.ingoing_loadDetail = {traitementID: traitementID, obs: obs};
return obs;
}),
map(result => {
this.ingoing_loadDetail = null;
//here I don't have access to traitementID :'(
return new LoadTraitementDetailSuccess(traitementID, result);
})
);
constructor(
private actions$: Actions,
private traitementsService: TraitementsService
) {
}
}
I'm trying to pass the variable or value traitementID to the last map.
I tried to avoid the last map with an async await but then I get a weird errors "Effect dispatched an invalid action" and "Actions must have a type property" (FYI all my actions have a type property).
Try to bake this id into observable's resolve, like:
switchMap((traitementID) => {
return this.traitementsService.loadDetail(traitementID).pipe(
map(detail => ({detail,traitementID}))
);
}),
map(({detail,traitementID}) => {
...
})

Resources