Got TS 2739 error while returning value from promise. Type 'Promise<any>' is missing the following properties from type - node.js

Currently I'm writing code for request login to server and receive it's session data and send to global context.
Basic principle is 1)Request and get Promise 2)Validate fetch result itself and response status. 3)Provide value to external component. And I'm working on 1) and 2).
But I got an error about data typing Type 'Promise<any>' is missing the following properties from type 'SessionInfo': userEmail, userName, sessionToken, duets(2739) at code that returns result data to external components. Despite of strict data typing(Maybe I think), I'm not sure why linter says Promise not Promise>. I think TS fails to assert it's type.
When I run very similar code with Javascript(without typing), it works in past. I'm not sure why this happens and I don't know what's wrong. Can you check my code?
Codes are below, there's 4 files -- interface definition file related to User, interface definition for handling response json, Actual request fetch, Response validation and evaluation.
When I checked linting at return res.data at actionHandler.ts, linter succeed to predict it's type. res.data is ResponseSet<SessionInfo>.data?: userTypes.SessionInfo as linter said.
In userTypes.ts
export interface SessionInfo {
userEmail: string,
userName: string,
sessionToken: string,
due: number,
}
In commonTypes.ts
export interface ResponseSet<T> { // Response wrapper when it returns with code 200
statusCode: ResponseStatusCode, // ResponseStatusCode is custom typed status code not for request it self.
data?: T,
description?: string,
};
In userReq.ts
const login = async (email: string, password: string): Promise<commonTypes.ResponseSet<userTypes.SessionInfo>> => {
try {
const request: Request = new Request(
composeUri(`user/login`, { email, password }),
{
method: 'GET',
headers: { 'Content-type': 'application/json' },
mode: 'cors',
}
);
const response: Response = await fetch(request);
if (response.status != 200) throw response.status;
return await response.json();
} catch {
return {
statusCode: 1,
};
}
}
In actionHandler.ts
export const doLogin = (email: string, password: string): userTypes.SessionInfo => {
const result: userTypes.SessionInfo = userReq.login(email, password)
.then(res => {
if (res.statusCode != 0) throw new Error(res.description || 'UnknownError');
return res.data;
})
.catch(err => {
return null;
});
return result;
}
Where I got an error is const result:.... I got Type 'Promise<any>' is missing the following properties from type 'SessionInfo': userEmail, userName, sessionToken, due ts(2739). I'm not sure why it is recognized as 'Promise` despite of strict type definition of my code.

the issue is that result isn't SessionInfo. It is a Promise of it.
const result: Promisse<userTypes.SessionInfo | null>;
doLogin is async due to used promise, you should follow async await inside and it can't return userTypes.SessionInfo, result will be a promise.
export const doLogin = async (email: string, password: string): Promise<userTypes.SessionInfo | null> => {
try {
const result: commonTypes.ResponseSet<userTypes.SessionInfo> = await userReq.login(email, password);
if (res.statusCode != 0) throw new Error(res.description || 'UnknownError');
} catch (e) {
return null;
}
return res.data;
}
// somewhere in the code (async env)
await doLogin(email, password);

Related

Implementing retry logic in node-fetch api call

I hope you will forgive the beginners question, I'm trying to implement a simple retry policy for an api call using node-fetch
This is being added to an existing repo using TypeScript so I'm using this also, hence the data definitions.
async checkStatus(custId: string, expectedStatus: string) {
const response = await fetch(
`${'env.API_URL'}/api/customer/applications/${custId}`,
{
method: 'GET',
headers: headers,
},
)
expect(response.status, "Response status should be 200").to.be.equal(200)
const resp = await response.json()
expect(resp.status).to.contain(expectedStatus)
return resp.status;
}
I am calling it like so
await this.checkApplicationStatus(custId, 'NEW')
await this.checkApplicationStatus(custId, 'EXISTING')//and so forth
Is there a neat way of retrying based on an unexpected expectedStatus ?
Again, I appreciate there may be many examples out there but as a beginner, I am struggling to see a good/best-practice approach so looking for someone to provide an example. I don't need to use Chai assertions, this was just my first attempt.
TIA
you can check a library that I've published #teneff/with-retry
to use it you'll have to define error classes like these:
class UnexpectedStatus extends Error {
constructor(readonly status: number) {
super("Unexpected status returned from API");
}
}
class ResourceNotFound extends Error {
constructor() {
super("Resource not found in API");
}
}
and throw respectively depending on the status code:
export const getCustomerApplications = async ( custId, headers ) => {
const result = await fetch(`https://example.com/api/customer/applications/${custId}`, {
method: "GET",
headers,
});
if (result.status === 404) {
throw new ResourceNotFound();
} else if (result.status > 200) {
throw new UnexpectedStatus(result.status);
}
return result;
};
and then just use the HOF from the library to wrap your function, with options (how many retries should be attempted and for which errors should it be retrying)
const retryWhenReourceNotFoundOrUnexpectedStatus = withRetry({
errors: [UnexpectedStatus, ResourceNotFound],
maxCalls: 3,
});
const getCustomerApplicationsWithRetry =
retryWhenReourceNotFoundOrUnexpectedStatus(getCustomerApplications);
const result = await getCustomerApplicationsWithRetry(1234, {
Authorization: "Bearer mockToken",
});
Check this working setup

How to test files and json data at the same time with jest?

I have a post request with express that upload a file and some data to the mongodb:
// Routes
Router.post('/api/training', validator(createVideoSchema, 'body'), uploadVideo, createVideoHandler);
// Route Handlers
async function createVideoHandler (req: Request, res: Response, next: NextFunction) {
try {
const dataToCreate = {
...req.body,
url: req.file?.path,
mimetype: req.file?.mimetype
};
const data = await service.create(dataToCreate);
response(req, res, data, 201);
} catch (error) {
next(error);
}
}
the body must be validate by joi using the following schema:
import Joi from 'joi';
const title = Joi.string().email().min(5).max(255);
const description = Joi.string().min(5).max(255);
const thumbnail = Joi.string().min(5).max(255);
const tags = Joi.array().items(Joi.string().min(5).max(100));
const createVideoSchema = Joi.object({
title: title.required(),
description: description.required(),
thumbnail: thumbnail.required(),
tags: tags.required(),
});
export { createVideoSchema };
Then I am creating a test to verify I am receiving a 201 status code:
it('should have a 201 status code', async () => {
const response = await request(app).post(route)
.set('Accept', 'application/json')
.field('title', data.title)
.field('description', data.description)
.field('thumbnail', data.thumbnail)
.field('tags', data.tags)
.attach('video', Buffer.from('video'), { filename: 'video.mp4' });
expect(response.status).toBe(201);
});
For some reason the validation middleware throws me a 400 error saying that the data is missing:
Error: "title" is required. "description" is required. "thumbnail" is required. "tags" is required
I tried to send the data using .set('Accept', 'multipart/form-data') but it throws me the same error.
I guess this error has to do with the way I send the data, but I don't fully understand.
You typically should not call a live API from a test. Instead you should mock the different possibly API response scenarios and be sure your code handles the different possibilities correctly. Ideally you'll also have a client class of some kind to place direct calls to your API inside a class that can easily be mocked.
For example, you could mock the endpoint response for valid data with something like:
export class VideoClient {
async createVideo(data) {
const response = await request(app).post(route) // Whatever url points to your API endpoint
.set('Accept', 'application/json')
.field('title', data.title)
.field('description', data.description)
.field('thumbnail', data.thumbnail)
.field('tags', data.tags)
.attach('video', Buffer.from('video'), { filename: 'video.mp4' });
if (response.status.ok) {
return { response, message: 'someGoodResponseMessage'};
}
return { response, message: 'someErrorOccurred' };
}
}
Then in your test you can mock your client call:
import { VideoClient } from './clients/VideoClient.js'; // or whatever path you saved your client to
const goodData = { someValidData: 'test' };
const badData = {someBadData: 'test' };
const goodResponse = {
response: { status: 201 },
message: 'someGoodResponseMessage'
}
const badResponse = {
response: { status: 400 },
message: 'someErrorOccurred'
}
it('should have a 201 status code', async () => {
VideoClient.createVideo = jest.fn().mockReturnValue(goodResponse);
const results = await VideoClient.createVideo(goodData);
expect(results.response.status).toBe(201);
expect(results.message).toEqual('someGoodResponseMessage');
});
it('should have a 400 status code', async () => {
VideoClient.createVideo = jest.fn().mockReturnValue(badResponse);
const results = await VideoClient.createVideo(badData);
expect(results.response.status).toBe(400);
expect(results.message).toEqual('someErrorOccurred');
});
This is by no means a working test or exhaustive example, but demonstrating the idea that you really should not call your API in your tests, but instead call mock implementations of your API to handle how your client code responds in different situations.

Assign route dynamically Node/Express

I need dynamically assign a new route but it for some reason refuses to work.
When I send a request in the Postman it just keeps waiting for a response
The whole picture of what I am doing is the following:
I've got a controller with a decorator on one of its methods
#Controller()
export class Test {
#RESTful({
endpoint: '/product/test',
method: 'post',
})
async testMe() {
return {
type: 'hi'
}
}
}
export function RESTful({ endpoint, method, version }: { endpoint: string, version?: string, method: HTTPMethodTypes }) {
return function (target: any, propertyKey: string, descriptor: PropertyDescriptor): void {
const originalMethod = descriptor.value
Reflect.defineMetadata(propertyKey, {
endpoint,
method,
propertyKey,
version
}, target)
return originalMethod
}
}
export function Controller() {
return function (constructor: any) {
const methods = Object.getOwnPropertyNames(constructor.prototype)
Container.set(constructor)
for (let action of methods) {
const route: RESTfulRoute = Reflect.getMetadata(action, constructor.prototype)
if (route) {
const version: string = route.version ? `/${route.version}` : '/v1'
Container.get(Express).injectRoute((instance: Application) => {
instance[route.method](`/api${version}${route.endpoint}`, async () => {
return await Reflect.getOwnPropertyDescriptor(constructor, route.propertyKey)
// return await constructor.prototype[route.propertyKey](req, res)
})
})
}
}
}
}
Is it possible to dynamically set the route in the way?
I mainly use GraphQL but sometimes I need RESTful API too. So, I want to solve this by that decorator
In order for the response to finish, there must be a res.end() or res.json(...) or similar. But I cannot see that anywhere in your code.

MongoDB aggregation with typescript

I have a User model and I want to perform generic aggregation. mean any array of object I pass to this function it executes.
this is the sample of getUser function
public async getUser(aggregate: object): Promise<ResponseDTO> {
let response = {} as ResponseDTO;
const [err, user] = await To(User.aggregate(aggregate));
if (user) {
response = { success: true, data: user, message: "User fround" };
}
else
response = { success: false, message: "User not fround" };
return response;
}
and I pass this as a Parameter
const query = [
{
$match: {
name:"Jon"
}
},
{
$project:{
_id:1
}
}
]
const userRes = await getUser(query);
But I'm not able to run the program it's giving me syntax error on getUser function
*(method) Model<any, any, any>.aggregate(pipeline?: any[] | undefined): Aggregate<any[]> (+1 overload)
Argument of type 'Aggregate<any[]>' is not assignable to parameter of type 'Promise'.
Type 'Aggregate<any[]>' is missing the following properties from type 'Promise': [Symbol.toStringTag], finally*
I tried to change object to any, Array or Array in getUser parameter
here is the SS of the Error
PS: I'm using node with typescript and IDE is VSCode
Mongoose queries have their own types, and you should use those types to avoid such errors.
You can import those types for anything you need directly from Mongoose package.
I strongly recommend using the Typegoose package, which helps to create fully typed MongoDB schemas. and by that allow you to use those types as a response, find and update queries and much more!
reference:
https://typegoose.github.io/typegoose/docs/guides/quick-start-guide
Example for your usage with correct type:
import { FilterQuery } from 'mongoose';
public async getUser(aggregate: FilterQuery<ResponseDTO>): Promise<ResponseDTO> {
let response = {} as ResponseDTO;
const [err, user] = await To(User.aggregate(aggregate));
if (user) {
response = { success: true, data: user, message: "User fround" };
}
else
response = { success: false, message: "User not fround" };
return response;
}
Mongoose provides PipelineStage interface for pipes We can use that right away
import { PipelineStage } from "mongoose";
public async getUser(aggregate: PipelineStage[]):
Promise<ResponseDTO> {
let response = {} as ResponseDTO;
const [err, user] = await User.aggregate(aggregate);
if (user) {
response = { success: true, data: user, message: "User found" };
}
else
response = { success: false, message: "User not found" };
return response;
}

Mock multiple api call inside one function using Moxios

I am writing a test case for my service class. I want to mock multiple calls inside one function as I am making two API calls from one function. I tried following but it is not working
it('should get store info', async done => {
const store: any = DealersAPIFixture.generateStoreInfo();
moxios.wait(() => {
const request = moxios.requests.mostRecent();
request.respondWith({
status: 200,
response: store
});
const nextRequest = moxios.requests.at(1);
nextRequest.respondWith({
status: 200,
response: DealersAPIFixture.generateLocation()
});
});
const params = {
dealerId: store.dealerId,
storeId: store.storeId,
uid: 'h0pw1p20'
};
return DealerServices.retrieveStoreInfo(params).then((data: IStore) => {
const expectedOutput = DealersFixture.generateStoreInfo(data);
expect(data).toMatchObject(expectedOutput);
});
});
const nextRequest is always undefined
it throw error TypeError: Cannot read property 'respondWith' of undefined
here is my service class
static async retrieveStoreInfo(
queryParam: IStoreQueryString
): Promise<IStore> {
const res = await request(getDealerStoreParams(queryParam));
try {
const locationResponse = await graphQlRequest({
query: locationQuery,
variables: { storeId: res.data.storeId }
});
res.data['inventoryLocationCode'] =
locationResponse.data?.location?.inventoryLocationCode;
} catch (e) {
res.data['inventoryLocationCode'] = 'N/A';
}
return res.data;
}
Late for the party, but I had to resolve this same problem just today.
My (not ideal) solution is to use moxios.stubRequest for each request except for the last one. This solution is based on the fact that moxios.stubRequest pushes requests to moxios.requests, so, you'll be able to analyze all requests after responding to the last call.
The code will look something like this (considering you have 3 requests to do):
moxios.stubRequest("get-dealer-store-params", {
status: 200,
response: {
name: "Audi",
location: "Berlin",
}
});
moxios.stubRequest("graph-ql-request", {
status: 204,
});
moxios.wait(() => {
const lastRequest = moxios.requests.mostRecent();
lastRequest.respondWith({
status: 200,
response: {
isEverythingWentFine: true,
},
});
// Here you can analyze any request you want
// Assert getDealerStoreParams's request
const dealerStoreParamsRequest = moxios.requests.first();
expect(dealerStoreParamsRequest.config.headers.Accept).toBe("application/x-www-form-urlencoded");
// Assert graphQlRequest
const graphQlRequest = moxios.requests.get("POST", "graph-ql-request");
...
// Assert last request
expect(lastRequest.config.url).toBe("status");
});

Resources