How to dynamically assign a name and type to unknown object property in Typescript - node.js

I'm using Shopify's rest client for node and its request and response look something like this:
request
client.get({
path: 'orders/count.json',
query: { fulfillment_status: 'unfulfilled' }
})
If there's an error:
{
"errors": "[API] Invalid API key or access...",
"code": 2342,
"statusText": "Authentication Error",
"Headers": "..."
}
If there's no error:
{
"body": { "count": 8 },
"code": 2342,
"statusText": "Authentication Error",
"Headers": "..."
}
I'd like to add some boilerplate over this client library so that I can get the typings of the response. This is what I'm trying to do but it's not working too well:
const customClient = {
get: async <T, K extends string>(params: GetRequestParams) => {
const response = (await client.get(params));
if (response.body.errors) return { errors: response.body.errors };
// somehow index it. obviously not with the type declaration???
return { [K]: response.body[K] as T };
},
}
With the hopes that I can use it as.
const { count, error } = customClient.get<number, "count">({ ... });
Any help would be appreciated. I have an entire file of the Shopify API types that I would like to leverage. A solution to this would be perfect!

A possible implementation can look like this:
const customClient = {
get: async <T, K extends string>(params: GetRequestParams):
Promise<Partial<Record<K, T> & { errors: string }>> =>
{
const response = (await client.get(params));
if (response.body.errors) return { errors: response.body.errors } as any;
return {
[Object.keys(response)[0]]: response[Object.keys(response)[0]]
} as any
},
}
As you correctly noted, we can't use the TypeScript generic types when constructing the returning object. We need to use JavaScript features instead. In this case I just took the first key of the response and used it for the key of the returning object as well as to index the response object.
The return type of the function is a Promise consisting of both a Record with K and T as keys and values and the error type. I used Partial here since they are not both required.
Destructing the response leads to the correct types now:
async function main(){
const { count, errors } = await customClient.get<number, "count">({} as any);
// count: number | undefined
// errors: string | undefined
}
Playground

Related

Error: Expected one matching request for criteria no parameters

I've found several answers to this problem, but all of them involve having parameters and not including them on the call. But my case is different because I do not have parameters.
According to all the material, I have consulted, I cannot find what I'm doing wrong.
Basically, it is this:
in the service I'm testing, I have a call like this:
public getData(): Observable<string[][]> {
const url = `${this.config.baseUrl}/data/`;
return this.http.get<string[][]>(url);
}
in providers of the spec file
{ provide: ConfigService, useValue: { baseUrl: '/base-url', tokenAuth: true } },
and
httpMock = TestBed.inject(HttpTestingController);
service = TestBed.inject(DataService);
and the test
it('should return the values of data when calling the API,' done => {
const mockValue: [['data1','data2'],['data3','data4'],['data5','data6']];
const sub = service.getData().subscribe(
value => {
expect(value).toHaveLength(3);
httpMock.verify();
done();
},
error => {
throw error;
},
);
subs.add(sub);
httpMock.expectOne('/base-url/data').flush(mockValue);
});
But when I ran the test, I received the following:
Expected one matching request for criteria "Match URL: /base-url/data/", found none.

Typescript check propery for unknow type

I have an api request and for error statement. I want to return message from payload.
But message object can change depending on error. For example, payload object can be
{ message: 'Not Authorized', type: 'service.not_authorized' }
or
{
"errors": [
{
"category": "AUTHENTICATION_ERROR",
"code": "UNAUTHORIZED",
"detail": "Authorization code is expired.
}
]
}
I can't create interfaces for all possible payloads.
In JS, basically I can write this :
if(payload && payload.message){
return payload.message
}
if(payload && payload.errors){
const message = payload.errors.length>0 ? payload.errors[0].detail : 'Error'
return message;
}
Now I can try to do this with Typescript
import axios from "axios";
interface IApiError{
category:string,
code:string,
detail:string
}
const CustomError=<T>(e:T):string=>{
let message="";
if(axios.isAxiosError(e)){
const payload = e.response?.data; // payload:unknown
if(payload){
if(typeof payload === 'object'){
if(payload.hasOwnProperty('message')){
message=payload.message;
// Error : Property 'message' does not exist on type 'object'.
}
else if(payload.hasOwnProperty('errors')){
const errorsArray:IApiError[]=payload.errors;
message=errorsArray[0].detail;
// Error : Property 'errors' does not exist on type 'object'.
}
}
}
}
return message;
}
How can I solve this?
Edit
I found this topic. But according this we should create interfaces and typeguard for every possible payload option.
I guess, I found a solution. Based on this answer I created a function.
// Basically it takes 3 arguments
// o -> Object that we don't know what is look like
// prop -> The name of prop that we are looking for
// type -> A variable with the same type with our prop
// Because I didn't find to send type to method in typescript
// (for example string,number,boolean)
const getProp = (o: unknown, prop: string, type:any): any => {
// Checks whether the object contains the requested prop
const p = (o as any)[prop]
// If this field instance of requested type
if (typeof p === typeof type) {
return p;
}
return undefined;
}
Let's test this for different scenarios.
let obj:unknown={ message: 'Not Authorized', type: 'service.not_authorized' }
let obj2:unknown={
"errors": [
{
"category": "AUTHENTICATION_ERROR",
"code": "UNAUTHORIZED",
"detail": "Authorization code is expired."
}
]
}
// In first object I looking for message field and it should be string
let res=getProp(obj,"message","");
res ? console.log(res) : console.log("Not Found");
// console result -> Not Authorized
// In second object I'm intentionally sending the wrong type
// There is a field with name 'errors' but it's type is array
res=getProp(obj2,"errors","");
res && res.length>0 ? console.log(res) : console.log("Not Found");
// console result -> Not Found
// I send true types
res=getProp(obj2,"errors",[]);
res && res.length>0 ? console.log(res[0].detail) : console.log("Not Found");
// console result -> Authorization code is expired.

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

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;
}

Firebase. Error "Request contains an invalid argument."

Use on backend side following libraries (from package.json).
"firebase": "^8.3.3",
"firebase-admin": "^9.6.0",
Try to send multicast message to multiple users.
import * as admin from 'firebase-admin';
const createNotificationMessage = (tokens: string[], data?: { [key: string]: string }): admin.messaging.MulticastMessage => {
return {
data,
tokens,
apns: {
payload: {
aps: {
contentAvailable: true,
},
},
},
};
};
const sendMulticast = (payload: admin.messaging.MulticastMessage) =>
admin.messaging().sendMulticast(payload);
const sendNotifications = async () => {
try {
const data = getData(); // here we get main data
const userTokens = getTokens(); // here we get tokens
await sendMulticast(createNotificationMessage(userTokens, data));
} catch (error) {
console.log(error);
}
}
I put 4 tokens to message before sending. But I got this error message in response
[{"success":false,"error":{"code":"messaging/invalid-argument","message":"Request contains an invalid argument."}},{"success":false,"error":{"code":"messaging/invalid-argument","message":"Request contains an invalid argument."}},{"success":false,"error":{"code":"messaging/invalid-argument","message":"Request contains an invalid argument."}},{"success":false,"error":{"code":"messaging/invalid-argument","message":"Request contains an invalid argument."}}]
What I tried to do:
Send messages through method send one by one. Result: the same error on every message
Tried to set header apns-priority to 5. The same error
Tried to set custom properties in aps object - content-available, content_available. The same error
Delete apns property from payload. Works well and there is no errors but I need silent notifications in iOS applications that's why option contentAvailable is required.
One note: this code worked well till 9 April 2021.
After full day search the reason of this errors, I found a solution for my problem.
const createNotificationMessage = (tokens: string[], data?: { [key: string]: string }): admin.messaging.MulticastMessage => {
return {
data,
tokens,
apns: {
payload: {
aps: {
contentAvailable: true,
badge : 0
},
},
},
};
};
Don't know why firebase shows an error because according to official website, parameter badge is optional string.

Resources