Typescript hierarchy of a sharable module - javascript

I want to write a package in Typescript such that once I compile it into .js, the typing is also available if you use it in a Typescript application.
Here is an example of a Http client that is built on top of Axios:
// http.ts
import Axios, {AxiosInstance, AxiosPromise, AxiosRequestConfig} from 'axios';
import {HttpStatic} from './interfaces';
export default class Http implements HttpStatic {
private headers: object;
private _client: AxiosInstance;
constructor(options?: AxiosRequestConfig) {
options = Object.assign({}, options);
this.headers = {};
this._client = Axios.create(options);
}
public get<R>(url: string, params: object = {}, headers: object = {}): AxiosPromise<R> {
return this._client.get(url, {
headers: {
...this.headers,
...headers
}, params
});
}
}
And this is the interface:
// interface.d.ts
import {AxiosPromise, AxiosRequestConfig} from 'axios';
export interface HttpInstance {
new(uri: string, options?: AxiosRequestConfig):HttpStatic
get<R>(url: string, params: object, headers: object): AxiosPromise<R>;
}
export interface HttpStatic {
}
When I compile this, I get http.js and http.d.ts. But http.d.ts is basically the http.ts file and includes the line import {HttpStatic} from './interfaces' which does not exist in the dist folder.
So when I try to use this module, I get errors saying ./interfaces not found.
Now, if I put all the content of the ./interfaces inside the http.ts then everything will compile fine. But I don't want to do that, and I rather to keep the interface/types separate from the source code.
How do I do this?

Related

NestJs: Make sure your class is decorated with an appropriate decorator

I am using graphql-request as a GraphQL client to query a headless CMS to fetch stuff, modify and return to the original request/query. headless cms is hosted separately fyi.
I have the following code :
#Query(returns => BlogPost)
async test() {
const endpoint = 'https://contentxx.com/api/content/project-dev/graphql'
const graphQLClient = new GraphQLClient(endpoint, {
headers: {
authorization: 'Bearer xxxxxxx',
},
})
const query = gql`
{
findContentContent(id: "9f5dde89-7f9b-4b9c-8669-1f0425b2b55d") {
id
flatData {
body
slug
subtitle
title
}
}
}`
return await graphQLClient.request(query);
}
BlogPost is a model having the types :
import { Field, ObjectType } from '#nestjs/graphql';
import { BaseModel } from './base.model';
import FlatDateType from '../resolvers/blogPost/types/flatDatatype.type';
#ObjectType()
export class BlogPost extends BaseModel {
#Field({ nullable: true })
id!: string;
#Field((type) => FlatDateType)
flatData: FlatDateType;
}
and FlatDateType has the following code
export default class FlatDateType {
body: string;
slug: string;
subtitle: string;
title: string;
}
it throws the following exception :
Error: Cannot determine a GraphQL output type for the "flatData". Make
sure your class is decorated with an appropriate decorator.
What is missing in here?
How is your graphql server supposed to understand the type of FlatDataType when there's no information about it being passed to the graphql parser? You need to add the graphql decorators to it as well. #ObjectType(), #Field(), etc.
FlatDataType is not defined as #ObjectType(), therefore type-graphql (or #nestjs/graphql) can't take it as an output in GraphQL.

HttpClient, Make Action String Type Safe

Is there anyway to make the 'post' action string type safe below? Right now it accepts Any word to substitute in for 'post', example 'abcd', will Not create compilation error.
Example:
saveUsers(body?: UpdateIdentityUserDto): Observable<any> {
return this.httpClient.request<IdentityUserDtoBaseRequestResponse>('post',`${this.baseUserUrl}`,
{
body: body
}
);
}
Option:
Here is another option below, however, I prefer to use string option above since those are auto generated from Swagger IO proxy generator.
saveUsers(body?: UpdateIdentityUserDto): Observable<any> {
return this.httpClient.post<IdentityUserDtoBaseRequestResponse>(`${this.baseUserUrl}`,body);
}
Currently using Angular 10
I suggest wrapping it around and making it a generic method. Make a separate file for these generics.
type HttpMethods = 'post' | 'get' | 'patch' | 'delete';
request<T>(method: HttpMethods, body: any): Observable<any> {
this.httpClient.request<T>(method, this.baseUrl, { body: body });
}
Then if you want to make a call an api call in a separate file, call this
import { request } from '...'
saveUsers(body?: BodyInterface): Observable<ResponseInterface> {
return request<IdentityUserDtoBaseRequestResponse>('post', body)
}
Create a namespace and export constants for the different types of request methods.
export namespace RequestMethod {
export const GET:string = "get";
export const HEAD:string = "head";
export const POST:string = "post";
export const PUT:string = "put";
export const DELETE:string = "delete";
export const CONNECT:string = "connect";
export const OPTIONS:string = "options";
export const TRACE:string = "trace";
export const PATCH:string = "patch";
}
Then you can utilize this namespace inside of a service
import { RequestMethod } from '../request-method.ts';
saveUsers(body?: UpdateIdentityUserDto): Observable<any> {
return this.httpClient.request<IdentityUserDtoBaseRequestResponse>(RequestMethod.POST,`${this.baseUserUrl}`,
{
body: body
}
);
}
Understand that this does not prevent a developer from typing any string they want. But it does give you a type-safe way of ensuring your strings are consistent through the app when utilized as a standard within your team.

Types of property 'cacheLocation' are incompatible

I have an old app in react with javascript, but I started a new one, to slowly migrate the .JS code to Typescript.
The first file I want to migrate its a configuration file, when its .js build succeeds.
WHen renamed to .TS I get this error
/Users/xx/yy/lulo/src/adalConfig.ts
(13,54): Argument of type '{ tenant: string; clientId: string; endpoints: { api: string; }; 'apiUrl': string; cacheLocation: string; }' is not assignable to parameter of type 'AdalConfig'.
Types of property 'cacheLocation' are incompatible.
Type 'string' is not assignable to type '"localStorage" | "sessionStorage" | undefined'
The file is this:
import { AuthenticationContext, adalFetch, withAdalLogin } from 'react-adal';
export const adalConfig = {
tenant: 'xxxx-c220-48a2-a73f-1177fa2c098e',
clientId: 'xxxxx-bd54-456d-8aa7-f8cab3147fd2',
endpoints: {
api:'xxxxx-abaa-4519-82cf-e9d022b87536'
},
'apiUrl': 'https://xxxxx-app.azurewebsites.net/api',
cacheLocation: 'localStorage'
};
export const authContext = new AuthenticationContext(adalConfig);
export const adalApiFetch = (fetch, url, options) =>
adalFetch(authContext, adalConfig.endpoints.api, fetch, adalConfig.apiUrl+url, options);
export const withAdalLoginApi = withAdalLogin(authContext, adalConfig.endpoints.api);
The issue is that the type of adalConfig gets asserted instead of being defined. You can read more about it in the docs but it basically means that TypeScript guesses the type. Short example:
type FooBar = 'foo' | 'bar';
const fooBar1 = 'foo'; // fooBar1: 'foo'
const fooBar2: FooBar = 'foo'; // fooBar2: FooBar
TypeScript playground link
Type assertion depends on a whole bunch of stuff and it's really hard to guess by hand which type TypeScript is going to assert. It's really useful to write TS code quickly, though. In any case - the problem in your code is that adalConfig.cacheLocation gets asserted to a string, but instead you want TypeScript to understand that its type is compatible with "localStorage" | "sessionStorage" | undefined.
Two ways to do that:
cacheLocation: 'localStorage' as 'localStorage': will precise to TypeScript that cacheLocation is of type 'localStorage', thus compatible with what you want
export const adalConfig: AdalConfig = ... will precise to TypeScript that the whole adalConfig object is of type AdalConfig, so it has basically the same effect
Kudos to #explosion-pills and #zerkms who contributed in the comments to this question
I know this is an oldie but always helpful to post an update. You can import configuration from the msal library to set the type of the config variable.
import { MsalAuthProvider, LoginType } from 'react-aad-msal';
import { Configuration } from 'msal';
// Msal Configurations
const config: Configuration = {
auth: {
authority: 'https://login.microsoftonline.com/',
clientId: '<YOUR APPLICATION ID>'
},
cache: {
cacheLocation:"localStorage",
storeAuthStateInCookie: true,
}
};
// Authentication Parameters
const authenticationParameters = {
scopes: [
`<your app registartion app id>/.default`
]
}
// Options
const options = {
loginType: LoginType.Popup,
tokenRefreshUri: window.location.origin + '/auth.html'
}
export const authProvider = new MsalAuthProvider(config, authenticationParameters, options)
import { CacheLocation } from "#auth0/auth0-react";
const AUTH0_CASH_LOCATION: CacheLocation | unefined = "localstorage";
this will help you. Auth0 provider makes sure that caching should save in memory or localStorage so you should provide either localStorage or memory for cacheLocation option

How to use validation in NestJs with HTML rendering?

NestJS uses validation with validation pipes and
#UsePipes(ValidationPipe)
If this fails it throws an exception. This is fine for REST APIs that return JSON.
How would one validate parameters when using HTML rendering and return
{ errors: ['First error'] }
to an hbs template?
You can create an Interceptor that transforms the validation error into an error response:
#Injectable()
export class ErrorsInterceptor implements NestInterceptor {
intercept(
context: ExecutionContext,
call$: Observable<any>,
): Observable<any> {
return call$.pipe(
// Here you can map (or rethrow) errors
catchError(err => ({errors: [err.message]}),
),
);
}
}
You can use it by adding #UseInterceptors(ErrorsInterceptor) to your controller or its methods.
I've been driving myself half mad trying to find a "Nest like" way to do this while still retaining a degree of customisability, and I think I finally have it. Firstly, we want an error that has a reference to the exisiting class-validator errors, so we create a custom error class like so:
import { ValidationError } from 'class-validator';
export class ValidationFailedError extends Error {
validationErrors: ValidationError[];
target: any;
constructor(validationErrors) {
super();
this.validationErrors = validationErrors;
this.target = validationErrors[0].target
}
}
(We also have a reference to the class we tried to validate, so we can return our object as appropriate)
Then, in main.ts, we can set a custom exception factory like so:
app.useGlobalPipes(
new ValidationPipe({
exceptionFactory: (validationErrors: ValidationError[] = []) => {
return new ValidationFailedError(validationErrors);
},
}),
);
Next, we create an ExceptionFilter to catch our custom error like so:
#Catch(ValidationFailedError)
export class ValidationExceptionFilter implements ExceptionFilter {
view: string
objectName: string
constructor(view: string, objectName: string) {
this.view = view;
this.objectName = objectName;
}
async catch(exception: ValidationFailedError, host: ArgumentsHost) {
const ctx = host.switchToHttp();
const response = ctx.getResponse<Response>();
const request = ctx.getRequest<Request>();
response.render(this.view, {
errors: exception.validationErrors,
[this.objectName]: exception.target,
url: request.url,
});
}
}
We also add an initializer, so we can specify what view to render and what the object's name is, so we can set up our filter on a controller method like so:
#Post(':postID')
#UseFilters(new ValidationExceptionFilter('blog-posts/edit', 'blogPost'))
#Redirect('/blog-posts', 301)
async update(
#Param('id') postID: string,
#Body() editBlogPostDto: EditBlogPostDto,
) {
await this.blogPostsService.update(postID, editBlogPostDto);
}
Hope this helps some folks, because I like NestJS, but it does seem like the docuemntation and tutorials are much more set up for JSON APIs than for more traditional full stack CRUD apps.

how to access object properties from TypeScript?

I'm new to Angular and TypeScript and just started working on a project using MEAN stack (MongoDB, Express, Angular, Node.js).
I created this mongoose module :
import * as mongoose from 'mongoose';
var Schema = mongoose.Schema;
const entrepriseSchema = new mongoose.Schema({
name: {type: String, unique: true, required : true},
telephone: Number,
logo: String,
web_site: String,
sites: [
{site_id: {type: Schema.Types.ObjectId, ref: 'Site'}}
]
});
const Entreprise = mongoose.model('Entreprise', entrepriseSchema);
export default Entreprise;
and this is my entreprise.component.ts :
import { Component, OnInit } from '#angular/core';
import { Http } from '#angular/http';
import { FormGroup, FormControl, Validators, FormBuilder } from '#angular/forms';
import { ActivatedRoute } from '#angular/router';
import { EntrepriseService } from '../services/entreprise.service';
import { SiteService } from '../services/site.service';
#Component({
selector: 'app-entreprise',
templateUrl: './entreprise.component.html',
styleUrls: ['./entreprise.component.scss'],
providers: [EntrepriseService, SiteService]
})
export class EntrepriseComponent implements OnInit {
entreprise = {};
sites = [];
id: String;
constructor(private entrepriseService: EntrepriseService,
private siteService: SiteService,
private http: Http,
private route: ActivatedRoute) {
this.id = route.snapshot.params['id'];
}
ngOnInit() {
this.getEntrepriseById(this.id);
//not working
//console.log(this.entreprise.name);
//console.log(this.entreprise.sites);
//this.getSitesIn(this.entreprise.sites);
}
getEntrepriseById(id) {
this.entrepriseService.getEntreprise(id).subscribe(
data => this.entreprise = data,
error => console.log(error)
);
}
getSitesIn(ids) {
this.siteService.getSitesIn(ids).subscribe(
data => this.sites = data,
error => console.log(error)
);
}
}
when I try to display the properties of the returned from entreprise.component.html it works fine and displays all the properties :
<h3>{{entreprise.name}}</h3>
<div *ngFor="let site of entreprise.sites">
{{site.site_id}}
</div>
{{entreprise.logo}}
{{entreprise.web_site}}
but how can I access the same properties on the TypeScript side ?
The commented code in the EntrepriseComponent is what I'm trying to accomplish but it's not working since this.entreprise is type {} .
The Enterprise model/schema that you created in Mongoose in Node.js resides on the server side. If you want the TypeScript code on the UI to recognize the properties in Enterprise, you will have to create a class in your angular codebase.
Create a folder named, say, models at the same level as your services folder. (Optional)
Create two files named site.ts and enterprise.ts in the models folder created in the previous step (You can put these file at a different location if you want) with the following contents:
site.ts
export interface Site {
site_id?: string;
}
enterprise.ts
import { Site } from './site';
export interface Enterprise {
name?: string;
telephone?: string;
logo?: string;
web_site?: string;
sites?: Site[];
}
Now, inside the EntrepriseComponent file, add the following imports
import { Enterprise} from '../models/entreprise';
import { Site } from '../models/site';
And change the first lines inside the EntrepriseComponent file to
export class EntrepriseComponent implements OnInit {
entreprise: Enterprise = {};
sites: Site[] = [];
Now, the enterprise attribute will be of type Enterprise and you will be able to access the properties that we declared in the enterprise.ts file.
Update:
Also, you cannot console.log(this.enterprise.name) immediately after this.getEntrepriseById(this.id); in your ngOnInit() function. This is because the web service you are making to get the enterprise object would not have resolved when you are trying to log it to the console.
If you want to see the enterprise object in the console or you want to run some code that needs to run after the service call has resolved and the this.enterprise object has a value, the best place to do this would be your getEntrepriseById function. Change the getEntrepriseById function to
getEntrepriseById(id) {
this.entrepriseService.getEntreprise(id).subscribe(
data => {
this.enterprise = data;
console.log(this.enterprise.name);
// Any code to run after this.enterprise resolves can go here.
},
error => console.log(error)
);
}

Categories