how to make nested bitbucket API http request in ngrx effects - javascript

BitBucket API 1.0 returns tags with no timestamp. Example response:
{
"size": 2,
"limit": 2,
"isLastPage": false,
"values": [
{
"id": "refs/tags/tag1",
"displayId": "tag1",
"type": "TAG",
"latestCommit": "ff7be6fad2e660e8139f410d2585f6b2c9867a61",
"latestChangeset": "ff7be6fad2e660e8139f410d2585f6b2c9867a61",
"hash": "f13db8e5c0b75b57b48777299d820525ad8127b9"
},
{
"id": "refs/tags/tag2",
"displayId": "tag2",
"type": "TAG",
"latestCommit": "4f5878c9554444755dbf6699eac33ff8752add5f",
"latestChangeset": "4f5878c9554444755dbf6699eac33ff8752add5f",
"hash": "23274bd5c9b87614f14a2245d5e70812c83104b7"
}
]
}
Therefore I am trying to request the latestCommit to get it's data. The response retrieves the committerTimestamp which I want to add to the tag object.
The ngrx effect and a following function are written as follows:
#Effect()
loadTags: Observable<LoadTagsRes> = this.actions$.pipe(
ofType(BitbucketActionTypes.LOAD_TAGS_REQ),
switchMap(() => {
return this.http.get('https://bitbucket/rest/api/1.0/projects/projects/repos/project/tags?limit=2', httpOptions).pipe(
map((response: any) => response.values.map(tag => (
{
...tag,
timeStamp: this.getTagTimestamp(tag.latestCommit)
}
))
),
map((response: any) => new LoadTagsRes({tags: response}))
)
}),
);
getTagTimestamp(tag) {
return this.http.get<any>(`https://bitbucket/rest/api/1.0/projects/projects/repos/project/commits/${tag}`, httpOptions).pipe(
switchMap(res => {
return res;
})
);
}
the new timeStamp property in the array of objects displays the following in redux devtools:
Would like to get the correct response for the inner http request.

The function should trigger the secondary request and fetch the timestamp but it isn't being triggered yet. You need to use switchMap (or any other RxJS higher order mapping operator) to trigger it. And seeing that you have multiple requests, you could use RxJS forkJoin function to trigger them in parallel.
Try the following
#Effect()
loadTags: Observable<LoadTagsRes>= this.actions$.pipe(
ofType(BitbucketActionTypes.LOAD_TAGS_REQ),
switchMap(() => {
return this.http.get('https://bitbucket/rest/api/1.0/projects/projects/repos/project/tags?limit=2', httpOptions).pipe(
switchMap((response: any) => {
forkJoin(response.values.map(tag =>
this.getTagTimestamp(tag.latestCommit).pipe(
map(timeStamp => ({...tag, timeStamp: timeStamp}))
)
))
}),
map((response: any) => new LoadTagsRes({
tags: response
}))
)
}),
);

loadTags: Observable<LoadTagsRes> = this.actions$.pipe(
ofType(BitbucketActionTypes.LOAD_TAGS_REQ),
switchMap(() => {
return this.http.get('https://bitbucket/rest/api/1.0/projects/projects/repos/project/tags?limit=2', httpOptions).pipe(
mergeMap((response: any) => response.values.map(tag => {
// array of observables
const tag$=this.getTagTimestamp(tag.latestCommit);
// use forkJoin to return single observable that returns array of results
return forkJoin(tag$)
})
),
map((response: any) => new LoadTagsRes({tags: response}))
)
}),
);
getTagTimestamp(tag) {
return this.http.get<any>(`https://bitbucket/rest/api/1.0/projects/projects/repos/project/commits/${tag}`, httpOptions).pipe(
switchMap(res => {
return res;
})
);

Related

JavaScript PromiseAll allSettled does not catch the rejected

I have a sns lambda function that returns void (https://docs.aws.amazon.com/lambda/latest/dg/with-sns.html). This event orderId and one message'status: success' are what I'm publishing. I check if the 'orderId' exists in my data database in the sns subscription lambda event. If it already exists, update the database; if it doesn't, console error it.
I created an integration test in which I transmit a random 'uuid' that isn't a valid 'orderId,' but it appears that my promise doesn't capture the'rejected'. It should show in console error failed to find order... I'm not sure where I'm going wrong. Also My promise syntax looks complicated, is there any neat way, I can do it. Thank you in advance 🙏🏽
This is sns event, which listen the publishing
interface PromiseFulfilledResult<T> {
status: "fulfilled" | "rejected";
value: T;
}
const parseOrdersFromSns = (event: SNSEvent) => {
try {
return event.Records.flatMap((r) => JSON.parse(r.Sns.Message))
} catch (error) {
console.error('New order from SNS failed at parsing orders', { event }, error)
return []
}
}
export const handlerFn = async (event: SNSEvent): Promise<void> => {
const orders = parseOrdersFromSns(event)
if (orders.length === 0) return
const existingOrdersPromiseResult = await Promise.allSettled(
orders.map(
async (o) => await findOrderStateNode(tagOrderStateId(o.orderId))
)
); // This returns of data if the order exsiit other it will return undefined
const existingOrders = existingOrdersPromiseResult // should returns arrays of data
.filter(({ status }) => status === "fulfilled")
.map(
(o) =>
(
o as PromiseFulfilledResult<
TaggedDatabaseDocument<
OrderStateNode,
TaggedOrderStateId,
TaggedOrderStateId
>
>
).value
);
const failedOrders = existingOrdersPromiseResult.filter( // should stop the opeartion if the data is exsit
({ status }) => status === "rejected"
);
failedOrders.forEach((failure) => {
console.error("failed to find order", { failure });
});
const updateOrder = await Promise.all(
existingOrders.map((o) => {
const existingOrderId = o?.pk as TaggedOrderStateId;
console.log({ existingOrderId }); // Return Undefined
})
);
return updateOrder;
};
this is my test suite
describe('Creating and updating order', () => {
integrationTest(
'Creating and updating the order',
async (correlationId: string) => {
CorrelationIds.set('x-correlation-id', correlationId)
const createdOrder = await createNewOrder(correlationId) // This create random order
if (!createdOrder.id) {
fail('order id is not defined')
}
const order = await getOrder(createdOrder.id)
// Add new order to table
await initializeOrderState([order])
const exisitingOrder = await findOrderStateNode(tagOrderStateId(order.id))
if (!exisitingOrder) fail(`Could not existing order with this orderId: ${order.id}`)
const event = {
Records: [
{
Sns: {
Message: JSON.stringify([
{
orderId: uuid(), // random order it
roundName,
startTime,
},
{
orderId: order.id,
roundName,
startTime,
},
{
orderId: uuid(),
roundName,
startTime,
},
]),
},
},
],
} as SNSEvent
await SnsLambda(event)
const updateOrderState = await findOrderStateNode(tagOrderStateId(order.id))
expect(updateOrderState?.status).toEqual('success')
},
)
})

Issues when testing Epic with TestScheduler

I'm using an rxjs epic as a middleware for an async action in a react-redux app.
I'm trying to simulate an ajax request (through a dependency injection) and test the behavior of this epic based on the response.
This is my epic :
export const loginEpic = (action$, store$, { ajax }) => { // Ajax method is injected
return action$.ofType(LoginActions.LOGIN_PENDING).pipe(
mergeMap(action => {
if (action.mail.length === 0) {
return [ loginFailure(-1) ]; // This action is properly returned while testing
} else {
return ajax({ ... }).pipe(
mergeMap(response => {
if (response.code !== 0) {
console.log(response.code); // This is logged
return [ loginFailure(response.code) ]; // This action is expected
} else {
return [ loginSuccess() ];
}
}),
catchError(() => {
return [ loginFailure(-2) ];
})
);
}
})
);
};
This part test if the mail adress is empty and works just fine (Or at least just as expected):
it("empty mail address", () => {
testScheduler.run(({ hot, expectObservable }) => {
let action$ = new ActionsObservable(
hot("a", {
a: {
type: LoginActions.LOGIN_PENDING,
mail: ""
}
})
);
let output$ = loginEpic(action$, undefined, { ajax: () => ({}) });
expectObservable(output$).toBe("a", {
a: {
type: LoginActions.LOGIN_FAILURE,
code: -1
}
});
});
});
However, I have this second test that fails because the actual value is an empty array (There is no login failed returned):
it("wrong credentials", () => {
testScheduler.run(({ hot, cold, expectObservable }) => {
let action$ = new ActionsObservable(
hot("a", {
a: {
type: LoginActions.LOGIN_PENDING,
mail: "foo#bar.com"
}
})
);
let dependencies = {
ajax: () =>
from(
new Promise(resolve => {
let response = {
code: -3
};
resolve(response);
})
)
};
let output$ = loginEpic(action$, undefined, dependencies);
expectObservable(output$).toBe("a", {
a: {
type: LoginActions.LOGIN_FAILURE,
code: -3
}
});
});
});
Any idea on what I'm doing wrong / why this part returns an empty array (The console.log does actually log the code):
if (response.code !== 0) {
console.log(response.code);
return [ loginFailure(response.code) ];
}
While this part returns a populated array:
if (action.mail.length === 0) {
return [ loginFailure(-1) ];
}
I'm guessing the use of Promise is causing the test to actually be asynchronous. Try changing the stub of ajax to use of(response) instead of from

Avoid multiple request when using ngrx effect

I would like to make two treatments on a same api call data.
I have a first effect:
loadSchedulings$ = createEffect(() =>
this.actions$.pipe(
ofType(ESchedulesActions.GetSchedulesByDate),
mergeMap(() =>
this.apiCallsService.getSchedulings().pipe(
map(trips => ({ type: ESchedulesActions.GetSchedulesByDateSuccess, payload: trips })),
catchError(() => EMPTY)
)
)
)
);
I call getSchedulings service method which make an api call then a treatment 1 on data is done
ApiCallsService :
getSchedulings() {
return this.http.get<ISchedules>(this.SchedulingByDateLocal2).pipe(
...
return groupByDate;
})
);
}
I would like to make a second treatment on the same data source. (raw data got from api ) but in parallel of the first because they are independent
So by logic I create a second effect
loadDirections$ = createEffect(() =>
this.actions$.pipe(
ofType(ESchedulesActions.GetSchedulesByDate),
mergeMap(() =>
this.apiCallsService.getDirections().pipe(
map(trips => ({ type: ESchedulesActions.GetDirectionsByDateSuccess, payload: directions})),
catchError(() => EMPTY)
)
)
)
);
Then in apiCallService I should have a method
getDirections() {
return this.http.get<ISchedules>(this.SchedulingByDateLocal2).pipe(
...
return groupByDirections;
})
);
}
The problem here is that I will have two requests for the same data.
To summarize the actual workflow :
LoadSchedulings ( effect ) ==> loadSchedulings ( service ) ==> API Call ==> treatment 1
LoadDirections ( effect ) ==> loadDirections ( service ) ==>(Same) API Call ==> treatment 2
So I would like to only use the first api request's data for two treatments
Update: According to the response of Manuel Panizzo I should have something like this ?
getRawData() {
return this.http.get<ISchedules>(this.SchedulingByDateLocal2)
}
Effect.ts
loadSchedulings$ = createEffect(() =>
this.actions$.pipe(
ofType(ESchedulesActions.getRawData),
pipe((data) =>
this.apiCallsService.getSchedulings(data).pipe(
map(trips => ({ type: ESchedulesActions.GetSchedulesByDateSuccess, payload: trips })),
catchError(() => EMPTY)
)
),
pipe((data) =>
this.apiCallsService.getDirections(data).pipe(
map(directions=> ({ type: ESchedulesActions.GetDirectionsByDateSuccess, payload: directions})),
catchError(() => EMPTY)
)
),
)
);
I think you could also dispatch a getRawDataSuccess action (that performs 1 api call)
getRawData$ = createEffect(() =>
this.actions$.pipe(
ofType(ESchedulesActions.getRawData),
mergeMap(() =>
this.apiCallsService.getRawData().pipe(
map(data => ({ type: ESchedulesActions.GetRawDataSuccess, payload: data })),
catchError(err => ({ type: ESchedulesActions.GetRawDataError, payload: err }))
)
)
)
);
Then create one effect per treatment listening for getRawDataSuccess action:
getSchedulesByDate$ = createEffect(() =>
this.actions$.pipe(
ofType(ESchedulesActions.getRawDataSuccess),
map((action) => {
return {
type: ESchedulesActions.GetSchedulesByDateSuccess,
payload: action.payload.schedulesByDate,
}
})
)
);
getDirectionsByDate$ = createEffect(() =>
this.actions$.pipe(
ofType(ESchedulesActions.getRawDataSuccess),
map((action) => {
return {
type: ESchedulesActions.GetDirectionsByDateSuccess,
payload: action.payload.directionsByDate,
}
})
)
);
This would be cleaner IMO and will theoretically run in parallel too.
use only one effect to get the raw data from the API and put in your store then create two diferents selectors that aply your groupByDirections and groupByDate logic.
Or extract the groupByDirections and groupByDate logic to the effect. an make a pipe in your effect that aply both logics and dispatch two actions in the same effect
UPDATE:
if you want execute two actions try this:
loadSchedulings$ = createEffect(() =>
this.actions$.pipe(
ofType(ESchedulesActions.getRawData),
mergeMap(action => this.apiCallsService.getRawData()),
map(rawApiData => {
const groupByDate = {}; // do your logic with rawApiData
const groupByDirections = {}; // do your logic with rawApiData
return { groupByDate, groupByDirections };
}),
mergeMap(groupedData => [
{
type: ESchedulesActions.GetDirectionsByDateSuccess,
payload: groupedData.groupByDirections,
},
{
type: ESchedulesActions.GetSchedulesByDateSuccess,
payload: groupedData.groupByDate,
},
]),
),
);

action creator does not return value to stream in marble test

I've got following Epic which works well in application, but I can't get my marble test working. I am calling action creator in map and it does return correct object into stream, but in the test I am getting empty stream back.
export const updateRemoteFieldEpic = action$ =>
action$.pipe(
ofType(UPDATE_REMOTE_FIELD),
filter(({ payload: { update = true } }) => update),
mergeMap(({ payload }) => {
const { orderId, fields } = payload;
const requiredFieldIds = [4, 12]; // 4 = Name, 12 = Client-lookup
const requestData = {
id: orderId,
customFields: fields
.map(field => {
return (!field.value && !requiredFieldIds.includes(field.id)) ||
field.value
? field
: null;
})
.filter(Boolean)
};
if (requestData.customFields.length > 0) {
return from(axios.post(`/customfields/${orderId}`, requestData)).pipe(
map(() => queueAlert("Draft Saved")),
catchError(err => {
const errorMessage =
err.response &&
err.response.data &&
err.response.data.validationResult
? err.response.data.validationResult[0]
: undefined;
return of(queueAlert(errorMessage));
})
);
}
return of();
})
);
On successfull response from server I am calling queueAlert action creator.
export const queueAlert = (
message,
position = {
vertical: "bottom",
horizontal: "center"
}
) => ({
type: QUEUE_ALERT,
payload: {
key: uniqueId(),
open: true,
message,
position
}
});
and here is my test case
describe("updateRemoteFieldEpic", () => {
const sandbox = sinon.createSandbox();
let scheduler;
beforeEach(() => {
scheduler = new TestScheduler((actual, expected) => {
expect(actual).toEqual(expected);
});
});
afterEach(() => {
sandbox.restore();
});
it("should return success message", () => {
scheduler.run(ts => {
const inputM = "--a--";
const outputM = "--b--";
const values = {
a: updateRemoteField({
orderId: 1,
fields: [{ value: "test string", id: 20 }],
update: true
}),
b: queueAlert("Draft Saved")
};
const source = ActionsObservable.from(ts.cold(inputM, values));
const actual = updateRemoteFieldEpic(source);
const axiosStub = sandbox
.stub(axios, "post")
.returns([]);
ts.expectObservable(actual).toBe(outputM, values);
ts.flush();
expect(axiosStub.called).toBe(true);
});
});
});
output stream in actual returns empty array
I tried to return from map observable of the action creator which crashed application because action expected object.
By stubbing axios.post(...) as [], you get from([]) in the epic - an empty observable that doesn't emit any values. That's why your mergeMap is never called. You can fix this by using a single-element array as stubbed value instead, e.g. [null] or [{}].
The below is an answer to a previous version of the question. I kept it for reference, and because I think the content is useful for those who attempt to mock promise-returning functions in epic tests.
I think your problem is the from(axios.post(...)) in your epic. Axios returns a promise, and the RxJS TestScheduler has no way of making that synchronous, so expectObservable will not work as intended.
The way I usually address this is to create a simple wrapper module that does Promise-to-Observable conversion. In your case, it could look like this:
// api.js
import axios from 'axios';
import { map } from 'rxjs/operators';
export function post(path, data) {
return from(axios.post(path, options));
}
Once you have this wrapper, you can mock the function to return a constant Observable, taking promises completely out of the picture. If you do this with Jest, you can mock the module directly:
import * as api from '../api.js';
jest.mock('../api.js');
// In the test:
api.post.mockReturnValue(of(/* the response */));
Otherwise, you can also use redux-observable's dependency injection mechanism to inject the API module. Your epic would then receive it as third argument:
export const updateRemoteFieldEpic = (action$, state, { api }) =>
action$.pipe(
ofType(UPDATE_REMOTE_FIELD),
filter(({ payload: { update = true } }) => update),
mergeMap(({ payload }) => {
// ...
return api.post(...).pipe(...);
})
);
In your test, you would then just passed a mocked api object.

Getting null in values from Promise.all

I am using promises. This is in continuation to my question here
The issue I am having is that in response, i.e. an array of objects is having null values. I will try to explain this
First I get the userId
Get user whishlist products from the userId
Then using userId I get stores/shop list
Then I iterate over store list and call another API to check if this store is user favourite store.
Then I get the products of each store and append in an object and return.
function getStoresList(context) {
const userID = common.getUserID(context)
let userWishListProd = []
return userID
.then(uid => Wishlist.getUserWishlistProducts(uid).then((products) => {
userWishListProd = products.data.map(product => +product.id)
return uid
}))
.then(uid => api.getOfficialStoresList(uid).then((response) => {
if (!response.data) {
const raw = JSON.stringify(response)
return []
}
const shops = response.data
return Promise.all(
shops.map((shop) => {
const id = shop.shop_id
const shopobj = {
id,
name: shop.shop_name,
}
return favAPI.checkFavourite(uid, id)
.then((favData) => {
shopobj.is_fave_shop = favData
// Fetch the products of shop
return getProductList(id, uid)
.then((responsedata) => {
shopobj.products = responsedata.data.products.map(product => ({
id: product.id,
name: product.name,
is_wishlist: userWishListProd.indexOf(product.id) > -1,
}))
return shopobj
})
.catch(() => {})
})
.catch(err => console.error(err))
}))
.then(responses => responses)
.catch(err => console.log(err))
})
.catch(() => {}))
.catch()
}
The response I get is
[{
"id": 1001,
"name": "Gaurdian Store",
"is_fave_shop": "0",
"products": [{
"id": 14285912,
"name": "Sofra Cream",
"is_wishlist": false
}]
},
null,
null,
{
"id": 1002,
"name": "decolite",
"is_fave_shop": "1",
"products": [{
"id": 14285912,
"name": "Denca SPF",
"is_wishlist": false
}]
}
]
The actual store are coming as 4 but instead of it null gets appended. What is wrong I am doing with Promises here.
This appears to have to do with your .catch(() => {}) and .catch(err => console.error(err)) invocations. If one promise in your loop has an error, it will be transformed to an undefined value (optionally being reported before), so that Promise.all will fulfill with an array that might contain undefined values. If you JSON.stringify that, you'll get null at those indices.
Drop the .catch(() => {}) statements that do nothing (or replace them with logging ones), and check your error logs.
Have you debug your code?
I would debug on Chrome and add some break points on the code to see what the actual response is.

Categories

Resources