I am trying to create some kind of generic http service in angular + ts, where I provide model type and it should be able connect to corresponding endpoint.
I have following working solution:
type Type<T> = new (...args: any[]) => T;
public getList<T>(c: Type<T>): Observable<T[]> {
const service = this.getService<T>(c);
return service.getList();
}
private getService<T>(c: Type<T>) {
const match = this.serviceMapping.find(i => c === i.type);
if (!match) {
throw new Error('No service registered for type ' + c.name);
}
return match.service;
}
I have created mapping object to map provided objects to services
this.serviceMapping = [
{
type: Customer, // object class
service: customerService, // injected singleton instance of http service
},
]
and getList is called from component e.g.:
genericService.getList<Customer>(Customer).subscribe(...)
This is working fine and service returns results, but I do not like that I have to provide Customer type as method parameter. I would like to remove it from method parameters and leave parameters only for request related data (query parameters, body, ...).
Is it somehow possible?
I would like to use it like this:
genericService.getList<Customer>().subscribe(res => this.customers = res);
genericService.getById<Customer>(customerId).subscribe(...)
genericService.create<Customer>(customerData).subscribe(...)
genericService.getList<Foo>(customerData).subscribe(res => this.foos = res;)
genericService.getList<Bar>(customerData).subscribe(res => this.bars = res)
(Of course that I will make sure that every service will provide those methods. This should handle default CRUD operations).
Thank you
As of typescript 2.8, this is possible. You can get the return type of the function.
Refer to this merged PR - https://github.com/Microsoft/TypeScript/pull/21847
you can create a common method which passes the function as a parameter and check for the type.
Related
Got some problems with observable.
I have a function, with one returns me an Observable.
public getData(userId) {
const data = this.execute({userId: userId});
return {event: "data.get", data: data}
}
private execute(input: SomeDto): Observable<SomeRefType[]> {
return this.databaseGateway.queryMany(DatabaseCommand.WebRecordGetbyparticipantid, {
parameters: {
prm_contextuserid: input.userId,
prm_filterparticipantids: null,
prm_filtertext: null
}
}).pipe(map(res => res));
}
Type what pipe(map) returns
What I'm got when trying to return or log data
Question: Why .pipe(map(res => res)) don't work? What am I doing wrong?
For sure, I can read data from .pipe(take(1)).subscribe(data => console.log(data)), but, how can I return data from construction like this?
Thanks everyone! Have a good day!
As said in the Rxjs documentation observable are lazy computation.
It means the as long as you don't subscribe to them they won't do anything. It exists two ways to trigger a subscription.
Either within a ts file using .susbcribe() or within a view when calling an endpoint.
If you're using nestjs it would be when calling the url defined within a #Controller('') with an http verb like #Get('path')
By convention you suffix observable variables with $: data$ = new Observable<any>().
At some point you'll have to convert the observable to a promise. Best to do it early and convert it using firstValueFrom immediately after the query.
Then convert your caller to an async method to use the returned value.
public async getData(userId): Promise<{ event: string, data: SomeRefType[] }> {
const data = await this.execute({userId: userId});
return { event: 'data.get', data };
}
private execute(input: SomeDto): Promise<SomeRefType[]> {
const res$ = this.databaseGateway.queryMany(DatabaseCommand.WebRecordGetbyparticipantid, {
parameters: {
prm_contextuserid: input.userId,
prm_filterparticipantids: null,
prm_filtertext: null
}
});
return firstValueFrom(res$);
}
Following the Apollo Server docs on dataSources, one would assume that the best way to access data in a GraphQL resolver would be through the dataSources option on the server config.
However, the app I'm working on has requirements that result in many different data source classes (truncated into ... in my handler.ts file below, but there are about 10 now and there will be many more in the future). This means that every data source class will be instantiated for each query/mutation the server receives, which I'm thinking could eventually result in a bit of latency if I have enough data source classes.
I'm thinking of not using the dataSources option and instead just instantiating each data source class as necessary by using the resolver context to call the initialize() method on the Apollo RESTDataSource class (sample resolver method pasted below), which seems to provide all the same functionality to the data source class that the dataSources config option does.
I'm wondering:
Are there any benefits / disadvantages of using the dataSources option that I'm not aware of?
Why would the Apollo docs encourage you to instantiate all data source classes for each request?
Thanks in advance for any insight you may have!
Server config from handler.ts:
const server = new ApolloServer({
typeDefs,
resolvers,
introspection: true,
playground: true,
dataSources: (): IDataSources => {
return {
entity: new Entity(),
notes: new Notes(), // this could be removed if "new" resolver method is used
... // like 8 more data sources go here
};
},
context: ({
event,
context
}: {
event: APIGatewayEvent;
context: Context;
}): IGraphqlServerOptionContext => {
const user = new User(event);
return { apiGatewayEvent: event, apiGatewayContext: context, user };
},
extensions: [() => new LogFunctionExtension()]
});
Sample (alternative) resolver method from resolvers.ts:
export const resolvers: IResolvers<any, IResolverContext> = {
Query: {
getNotes: async (
_source,
args: QueryGetNotesArgs,
resolverContext: IResolverContext
) => {
validateRequestHeaders('common', resolverContext.apiGatewayEvent);
// old method of accessing notes data source class:
// const {
// dataSources: { notes }
// } = resolverContext;
// new method of accessing notes data source class:
const notes = new Notes(resolverContext);
return notes.getNoteDetails(args);
},
}
};
new Notes data source class constructor:
export default class Notes extends RESTDataSource<IDataSourceContext> {
constructor(inputContext: IDataSourceContext) {
this.initialize({
context: inputContext,
cache: new InMemoryLRUCache()
});
}
}
I'm subscribing to observable and unshift a new element into the resulting array which mutates data. How can I prevent data mutation on a service level by throwing exception if someone tries to mutate data instead of doing deep copy?
I have an angular service to get the list of states from API based on country code. It's a singleton service meaning same instance is shared between different modules and their components. I'm caching the response with ShareReplay(). When in my component I subscribe to the observable and mutate the result (by unshifting a new value to the array) the cached data gets mutated. Right now I'm doing deep copy on a result which is fine but I want my observable to throw exception if somebody tries to use the service and mutates the original value instead of doing deep copy. I also tried to make service return deep copy of the observable but that didn't work, guess lodash doesnt know how to deep copy observables, unless I ditch shareReplay(), implement my own ReplaySubject on service level and make next() method return deep copy of emitted value maybe ?
Service
#Injectable()
export class StatesService {
private states$: Observable<State[]> = null;
constructor(private _http: HttpService) {}
getStatesFromCountry(countryCode3: string, freshData: boolean = false): Observable<State[]> {
let code = Object.keys(CountryEnum)
.filter((key: any) => isNaN(key))
.find(key => key === countryCode3.toUpperCase());
if(!code) return Observable.throw("Country Code was not recognized. Make sure it exists in CountryEnum.");
let url: string = `api/states?country=${countryCode3}`;
if (freshData) return this.getStates(url);
return this.states$ ? this.states$ : this.states$ = this.getStates(url).shareReplay(1);
}
private getStates(url: string): Observable<State[]> {
return this._http.get(url)
.map(response => response)
.catch(error => Observable.throw(<ApiError>JSON.parse(error)));
}
}
Component
private loadStates(): Subscription {
const usaStates$ = this._statesService.getStatesFromCountry(CountryEnum[CountryEnum.USA]);
const canStates$ = this._statesService.getStatesFromCountry(CountryEnum[CountryEnum.CAN]);
return Observable.forkJoin(usaStates$, canStates$).subscribe(
([usaStates, canStates]) => {
this.statesList = _.cloneDeep(usaStates.concat(canStates));
//Here if I unshift without doing deep copy first, other
//components that are using this shared service will now
//receive a mutated array of states
this.statesList.unshift(<State>{id: null, code: 'All'});
},
error => { ApiError.onError(error, this._notificationService, this._loaderService); }
);
}
You should be able to use Object.freeze on the value (somewhere).
const test = [1,2,3,4];
Object.freeze(test);
test.unshift(0); // error will be thrown
Cannot add property 5, object is not extensible
I've been sitting with a problem for the past days & I can't seem to get a solution anywhere.
Background:
I have a typescript class defined as follows:
export class Client extends Person {
claimNumber: string;
policyNumber: string;
address: string;
insuranceCompany: Organisation = new Organisation();
toString(): string {
return this.policyNumber
.concat(this.claimNumber);
}
}
The above is used as a model that drives an angular 5 template. In my component, I fetch (using angular 5 HttpClient) a list of clients from a remote api & generate an html table rows. The LOC to generate the table rows is:
<tr *ngFor="let client of clients | filter:searchString"> ... </tr>
searchString above is property bound to a search input tag & filter is a custom filter Pipe defined as follows:
export class FilterPipe implements PipeTransform {
transform(items: Client[], term: string) {
if (term == undefined || term === '') return items;
return items.filter(item =>item.toString().toLocaleLowerCase().includes(term.toLocaleLowerCase()));
}
}
Problem:
When I inspect item.toString() in the filter pipe above, it returns [object Object] as opposed to a string made up of policyNumber, & claimNumber.
Investigation:
I investigated this issue as follows: I instantiated the Client class as follows:
let c = new Client();
c.policyNumber = 'ababababa';
c.claimNumber = 'aaaaaaa';
console.log('client toString() is => ' + c.toString());
Interesting enough, the console.log above outputs : 'ababababaaaaaaaa'.
Question:
What am I doing wrong that results in the item.toString() in the filter pipe return [object Object] whereas toString() on a class I instantiated returns the correct string?
If you get the clients from a WebService (or something similar), you are just getting plain json objects. If you say that the received objects are of type Client, typescript will show them as objects of such type, but only the properties will be the same, the methods will not be from the Client class, but from the Object class.
You might want to instantiate them as real client objects after you retrieve them from the server:
public myServiceMethod() {
return this.http.get(...).map(plainClients => {
const realClients: Array<Client> = (plainClients || []).map(plainClient => {
let realClient = new Client();
realClient.claimNumber = plainClient.claimNumber;
realClient.policyNumber = plainClient.policyNumber;
realClient.address = plainClient.address;
return realClient;
});
return realClients;
})
}
You might prefer to use anemic objects with their types being interfaces, and use an utilitarian function to retrieve the client as a string, to avoid cases like the one you're having:
export interface Person {
...
}
export interface Client extends Person {
claimNumber: string;
policyNumber: string;
address: string;
insuranceCompany: Organisation;
}
// In some utilitarian class
public static getClientInfo(client: Client) {
return client.policyNumber.concat(client.claimNumber);
}
// In your pipe
return items.filter(item => getClientInfo(item).toLocaleLowerCase().includes(term.toLocaleLowerCase()));
I'm not saying to just use anemic classes in your app, but if some type of object is passed around and will probably be serialized, then using anemic objects can avoid problems like the one you are having.
A way to figure out what the problem might be would be to rename your method to something that isn't a built in method name, like maybe toSearchString. It would also be worth adding console logs to your filter function to make sure you're actually getting clients there. You may actually be getting a different object.
This is my first time ever working with angular observables and I'm a bit confused on how this works. I was given a mostly functioning angular CLI app that I just need to wire up to my already existing API.
I have a service with this function
public getApps(): Observable<ApplicationInterface[]> {
return this.http.get(url);
}
Then in my component, I have
public data: ApplicationInterface[];
ngOnInit() {
this.route.params
.subscribe(params => {
this.fetchData();
});
}
fetchData() {
this.service.getApps()
.subscribe(data => {
this.data = data;
});
}
My api endpoint returns a JSON structure of {"applications": []}
I can't seem to figure out how to access the array in that JSON hash.
If I console.log(data) in the subscribe block, it is the API response with the applications key that I expect, but if I change the data assignment to this.data = data.applications, ng build fails with Property 'applications' does not exist on type 'ApplicationInterface[]'
You should design the interface to be aligned with the response. If the response is object, than you need to have it like this also in the interface.
Try something like this (using the new HttpClient):
interface ApplicationInterfaceResponse {
applications: ApplicationInterface[];
}
public getApps(): Observable<ApplicationInterface[]> {
return this.httpClient
.get<ApplicationInterfaceResponse>(url)
.map(response => {
console.log(response.applications);
return data.applications;
});
}
If your return is of type ApplicationInterface[], then it's an array of ApplicationInterfaces, thus does not have a property called applications on it. This has nothing to do with your observable; it's fine. Rather, you've mistyped your variable.
If you don't need any other properties of data, you can map the value:
public getApps(): Observable<ApplicationInterface[]> {
return this.http.get(url).map(data => data.applications);
}
However, I recommend against this in most situations. If your object changes in the future, then you have to change this function and all attached subscriptions. Instead, you should create an interface for your response (your response right now does not match the type you're giving it), and use values of it as necessary.
The simplest fix is to indicate the correct form of the data that is returned by your service method since it doesn't actually return an array:
public getApps(): Observable<{applications:ApplicationInterface[]}> {
return this.http.get(url);
}
Now in your subscribe, you can get at the array as you would expect
.subscribe(e => this.data = e.applications)