I want to call a paginated API with promises. The information how many pages are available is not known at the beginning but is delivered on every response. In detail I am calling a Jira search, the paging information part looks like:
{
"startAt": 0,
"maxResults": 15,
"total": 100000,
...
}
I solved the handling of the pagination with recursion, this is my solution in Typescript:
search(jql: string, fields: string[] = [], maxResults = 15) : Promise<JiraIssue[]> {
// container for the issues
let issues: Array<JiraIssue> = new Array<JiraIssue>();
// define recursive function to collect the issues per page and
// perform a recursive call to fetch the next page
let recursiveFn = (jql: string, fields: string[], startAt: number, maxResults: number) :
Promise<JiraIssue[]> => {
return this
// retrieves one page
.searchPage(jql, fields, startAt, maxResults)
.then((searchResult: JiraSearchResult) => {
// saves the result of the retrieved page
issues.push.apply(issues, searchResult.issues);
if (searchResult.startAt + searchResult.maxResults < searchResult.total) {
// retrieves the next page with a recursive call
return recursiveFn(jql, fields,
searchResult.startAt + searchResult.maxResults,
maxResults);
}
else {
// returns the collected issues
return issues;
}
})
};
// and execute it
return recursiveFn(jql, fields, 0, maxResults);
}
However, I don't like the recursive approach, because this works only well with small result sets (I am afraid of a stack overflow). How would you solve this problem with a not recursive approach?
This is not actual recursion, and there is no stack overflow danger, because the function is being called inside a then handler.
One options is to wrap this in an iterator pattern.
Something like:
interface Searcher {
(jql: string, fields: string[], startAt: number, maxResults: number) => Promise<JiraSearchResult>;
}
class ResultsIterator {
private jql: string;
private fields: string[];
private searcher: Searcher;
private startAt: number;
private maxResults: number;
private currentPromise: Promise<JiraIssue[]>;
private total: number;
constructor(searcher: Searcher, jql: string, fields?: string[], maxResults?: number) {
this.jql = jql;
this.startAt = 0;
this.searcher = searcher;
this.fields = fields || [];
this.maxResults = maxResults || 15;
this.total = -1;
}
hasNext(): boolean {
return this.total < 0 || this.startAt < this.total;
}
next(): Promise<JiraIssue[]> {
if (!this.hasNext()) {
throw new Error("iterator depleted");
}
return this.searcher(this.jql, this.fields, this.startAt, this.maxResults)
.then((searchResult: JiraSearchResult) => {
this.total = searchResult.total;
this.startAt = searchResult.startAt + searchResult.maxResults;
return searchResult.issues;
});
}
}
This code isn't perfect, as I'm not entirely sure what you're doing there (for example what's this.searchPage?), but you should probably get the idea I'm aiming at.
You'll just do:
if (resultIterator.hasNext()) {
resultIterator.next().then(...);
}
Hope this helps.
Related
I have this observable object in my angular project that has this type:
export interface FavoritesResponse {
wallet: boolean;
deposit: boolean;
withdraw: boolean;
transfer: boolean;
exchange: boolean;
ticket: boolean;
account: boolean;
}
I want to extract an array from this object with only the properties that have the value true.
So for example if my favorites object looks like this:
favorites$ = {
wallet: true;
deposit: true;
withdraw: false;
transfer: false;
exchange: false;
ticket: true;
account: true;
}
I want to have my enabledFavorites$ look like this:
enabledFavorites$ = [
wallet,
deposit,
ticket,
account
]
as in, turn it into an array and only have the keys that had the value of true. How can I do this? I know the solution probably contains an rxjs pipe, map but I don't know what I should be doing exactly.
If you mean to say the observable emits an object of type FavoritesResponse and you wish to transform the emission to an array of it's keys only with value true, you could use
RxJS map operator to transform the incoming object
Native JS methods Object.keys() with Array#filter to perform the actual transformation
enabledFavorites$: Observable<string[]> = favorites$.pipe(
map((favs: FavoritesResponse) =>
Object.keys(favs).filter((key: string) => !!favs[key])
)
);
get favoritesArray$(): Observable<string[]> {
return this.favoritesSettings$.pipe(
map((favorites) => {
const _items = Object.keys(favorites);
const _result: string[] = [];
_items.forEach((item) => {
if (!!(favorites as any)[item]) {
_result.push(item);
}
});
return _result;
})
);
}
So I guess that you want is convert from Observable<FavoritesResponse> to Observable<string[]> with the string[] containing the keys checked.
One way could be:
const enabledFav$ = favOptions$.pipe(
map(resp => {
let result = [];
Object.keys(resp).forEach(key => {
if (resp.key) {
result.push(key);
}
});
return result;
})
);
I dont know the scenario but this is basically the code.
Here enabledFav$is Observable<string[]> and favOptions$ is Observable<FavoritesResponse>
I am getting an error on my forEach loop in javascript using typescript.
This expression is not callable. Type 'TradeData[]' has no call signatures.
using redux I call an api to download data in this format
export interface TradeData {
id: number;
filedate: Date;
poffic: string;
pacct: string;
quantity: number;
sector: string;
psdsC1: string;
name: string;
bbsymbol: string;
last_price: number;
deltasettlement: number;
}
then I use reduce to group that data by a certain property
React.useEffect(() => {
const doGetTrades = async () => {
const unansweredQuestions = await getTrades();
dispatch(gotTradesAction(unansweredQuestions));
const result = unansweredQuestions.reduce<Record<string, TradeData[]>>(
(groupedAccounts, trade) => {
const account = trade.bbsymbol;
if (groupedAccounts[account] == null) {
groupedAccounts[account] = [];
}
groupedAccounts[account].push(trade);
return groupedAccounts;
},
{} as Record<string, TradeData[]>
);
console.log("result");
console.log(result);
console.log("foreach");
result.forEach(function (value) {
console.log(value);
});
};
doGetTrades();
now I am trying to cycle through that returned array "result". First with a simple foreach loop, and logging to console window. I am getting an error which I believe is due to typescript but I do not know the proper way to define.
Can someone please assist to show me how to handle?
I'm looking for a way to wait for the user to stop interaction and then make an HTTP request, for this I'm looking into the debounceTime() operator from RxJs, but the target I'm waiting for is an array I defined.
This is the scenario:
export class KeywordSelectionComponent implements OnInit {
constructor(private proposalService: ProposalService) { }
#ViewChild(MatTable, {static: true}) kwTable: MatTable<any>;
#ViewChild(MatPaginator, {static: false}) paginator: MatPaginator;
#Input() proposalId: string;
keywordInput = new FormControl(null, Validators.required);
dataSource: MatTableDataSource<Keyword>;
displayedColumns = ['action', 'keyword', 'searches', 'competition', 'cpc'];
suggestedKeywords: Keyword[] = [];
selectedKeywords: string[] = [];
fetchSuggestions(seeds?: string[]) {
const ideas = {
seeds: [],
limit: 25
};
this.proposalService.getKeywordIdeas(this.proposalId, ideas).pipe(retry(3)).subscribe(res => {
this.suggestedKeywords = res;
});
}
}
I'm not including the whole component here, but the idea is the following:
I have a list of suggestedKeywords which I render on the page, each of these should call an addKeyword() method to add that keyword to the dataSource, and after that, I call the fetchSuggestions() method to get new keywords to populate the suggestedKeywords list.
The problem comes when I try to select multiple keywords in quick succession, since that would trigger a request for each of those clicks to update the suggestedKeywords list, so I wanted to use the debounceTime() to prevent the request to trigger until the user stops clicking items for a bit; however this requires an Observable to be the element changing as far as I know, but in my case, it is just a simple array.
Is there someway to keep track of the value of the array so it waits for a while after it changes before making the HTTP request, like an Observable?
EDIT: Used the from() operator as suggested in the comments, in order to actually listen to changes do I need to define other methods? I'm thinking something similar to valueChanges() in FormControls.
Going through more documentation I'm leaning towards Subject, BehaviorSubject, etc; but I'm not sure if this would be a correct approach, could anyone provide an example on how to do this?
Wrap your array in Observable.of() RxJS operator and it will behave like observable
What I ended up doing was using a Subject to keep track of the changes, calling it's next() function evrytime a modified the suggestedKeywords array and subscribing to it as an observable.
My component ended up looking like this:
export class KeywordSelectionComponent implements OnInit {
constructor(private proposalService: ProposalService) { }
keywordInput = new FormControl(null, Validators.required);
suggestedKeywords: Keyword[] = [];
selectedKeywords: string[] = [];
isLoadingResults = false;
tag$ = new Subject<string[]>();
ngOnInit() {
this.tag$.asObservable().pipe(
startWith([]),
debounceTime(500),
switchMap(seeds => this.getSuggestionsObservable(seeds))
).subscribe(keywords => {
this.suggestedKeywords = keywords;
});
}
addSuggestedKeyword(keyword: Keyword) {
const suggestedKeyword = keyword;
const existing = this.dataSource.data;
if (!existing.includes(suggestedKeyword)) {
existing.push(suggestedKeyword);
this.dataSource.data = existing;
}
this.tag$.next(this.getCurrentTableKeywords());
}
fetchKeywordSearch(keyword: string) {
this.isLoadingResults = true;
this.keywordInput.disable();
const search = {
type: 'adwords',
keyword
};
const currentData = this.dataSource.data;
this.proposalService.getKeywordSearch(this.proposalId, search).pipe(retry(3)).subscribe(res => {
currentData.push(res);
this.dataSource.data = currentData;
}, error => {},
() => {
this.isLoadingResults = false;
this.keywordInput.enable();
this.tag$.next(this.getCurrentTableKeywords());
});
}
getCurrentTableKeywords(): string[] {}
getSuggestionsObservable(seeds: string[] = []): Observable<Keyword[]> {
const ideas = {
type: 'adwords',
seeds,
limit: 25
};
return this.proposalService.getKeywordIdeas(this.proposalId, ideas).pipe(retry(3));
}
}
I'm new to typescript
export class Reward {
id: number;
point: number;
status: Status;
constructor(id: number, point: number, status: Status) {
this.id = id;
this.point = point;
this.status = Status.NONE;
}
}
export enum Status {
CLAIMED,
AVAILABLE,
NONE
}
public getRewardsOf(numberOfDay: number): Array<Reward> {
return this.availableRewards.map((reward: Reward, index: number) => {
if (index == (numberOfDay - 1)) {
return new Reward(reward.id, reward.point, Status.AVAILABLE);
} else {
return reward;
}
});
}
the if doesn't work for me. It still returns the same old object (the status value is the same). it's supposed to be different even though I create a new object.
It works when I use
if (index == (numberOfDay - 1)) {
return {
'id': reward.id,
'point': reward.point,
'status': Status.AVAILABLE
};
} else {
return reward;
}
if so I lose the power of typescript
Your constructor is hardcoding the value Status.NONE:
...
this.status = Status.NONE
...
This will effectively discard the status argument passed to the constructor.
If you wish to provide Status.NONE as a default parameter, do:
constructor(id: number, point: number, status = Status.NONE) {
this.id = id;
this.point = point;
this.status = status;
}
You can further reduce this to:
constructor(public id: number, public point: number,
public status = Status.NONE) {
}
Edit: Also, because TypeScript is structurally, not nominally typed (see structural typing), you don't lose any type safety by returning a literal versus a class-constructed instance. Both versions of your code can safely be said to be returning a Reward.
TS Playground
class Reward {
constructor(public id: string) {}
}
// This is OK.
const x: Reward = {id: '12435'};
If Reward starts adding methods, then what you lose is the ability to create a Reward easily with your object literal.
I am creating an app in angular, and one task is, I need read data from Firestore and save into an array, my code snippet is as below:
public getListingData(): Observable < RequestListingModel > {
console.log("getting listing...");
this.firestore.collection('Requests').snapshotChanges().subscribe(
requests => {
this._listRequestItems = requests.map(a => {
const data = a.payload.doc.data() as RequestItemModel;
data.requestId = a.payload.doc.id;
console.log("doc found - " + data.requestId);
return data;
})
});
const requestListingModel = {
items: this._listRequestItems
}
as RequestListingModel;
return of(requestListingModel);
}
And my code of RequestListingModel is as below:
import * as dayjs from 'dayjs';
export class RequestItemModel {
name: string;
address: string;
category: string;
requestId: string;
postBy: string;
description: string;
// Default mock value
// expirationDate = '12/01/2018';
expirationDate: string = dayjs().add(5, 'day').format('MM/DD/YYYY HH:mm:ss') as string;
}
export class RequestListingModel {
items: Array<RequestItemModel> = [];
constructor(readonly isShell: boolean) { }
}
And it's not working as I always get empty return when call the function getListingData(), but the console can print out the each requestId successfully, I think something wrong with the way I store into array, please help, thanks!
Your problem is happening because things are happing asynchronously:
So return of(requestListingModel); gets called before this.firestore.collection('Requests').snapshotChanges().subscribe(...) populates the array, thus returning an observable of an empty array.
Since it looks like you want to return an observable, don't subscribe() inside your method. Just do a map() on the snapshotChanges observable to transform the output, and let the consuming code subscribe to your getListingData() method.
public getListingData(): Observable < RequestListingModel > {
console.log("getting listing when someone subscribes...");
return this.firestore.collection('Requests').snapshotChanges().pipe(
map(requests => {
let requestListingModel = {
items: []
} as RequestListingModel;
requests.map(a => {
let data = a.payload.doc.data() as RequestItemModel;
data.requestId = a.payload.doc.id;
console.log("doc found - " + data.requestId);
requestListingModel.items.push(data);
});
return requestListingModel;
})
);
}
Note: still... can't test this at the moment, so it may not be 100% correct :-\
But hopefully points you in the right direction. :-)