The bounty expires in 2 days. Answers to this question are eligible for a +100 reputation bounty.
Ayoub k wants to draw more attention to this question.
As for now, I'm using mongoose middleware to handle Mongoose specific errors (validation, cast, ....).
I'm using the following code in all of my schemas:
schema.post('save', handleValidationError);
schema.post('findOneAndUpdate', handleValidationError);
schema.post(['findOne', 'deleteOne'], handleCastError);
Is there anyway to make this global in all schemas without repeating the code?
I tried to use plugins as the following but they don't get triggered if an error happens.
const errorsPlugin = (schema: any, _options: any): void => {
schema.post('save', handleValidationError);
schema.post('findOneAndUpdate', handleValidationError);
schema.post(['findOne', 'deleteOne'], handleCastError);
};
const connection = await mongoConnect(url);
plugin(errorsPlugin);
logger.info(`MongoDB connected: ${connection.connection.name}`);
Edit 1: error handler function
const handleValidationError = (error: NodeJS.ErrnoException, _res: Response, next: (err?: Error) => void): void => {
if (error.code?.toString() === '11000') {
const { keyPattern, keyValue } = error as Error & {
keyPattern: { [key: string]: number };
keyValue: { [key: string]: string };
};
const key = Object.keys(keyPattern)[0];
const value = Object.values(keyValue)[0];
throw new DuplicatedKeyError(key as string, value as string);
} else if (error instanceof mongooseError.ValidationError) {
throw new ValidationError(error.errors);
} else if (error instanceof mongooseError.CastError) {
throw new CastError(error.kind, error.value);
}
next();
};
Related
I'm new to Typescript and have been doing a refactor a colleague code, I'm currently doing a typecheck and removing all any types. The goal is to make an MSGraph API call and return the a JSON file that translated into BirthdayPerson with a name, birthday date and a ID
I've been trying to a assign a type instead of any in the following code, but whether I assign number, string or any other type a different error will show up.
Perhaps I'm not tackling the solution correctly:
graph.ts
* #param accessToken
* #param endpoint url to call from MS Graph
*/
async function callMsGraph(accessToken: string, endpoint: string) {
const headers = new Headers();
const bearer = `Bearer ${accessToken}`;
headers.append('Authorization', bearer);
const options = {
method: 'GET',
headers,
};
try {
return fetch(endpoint, options);
} catch (error) {
console.error(error);
throw error;
}
}
export const callMsGraphWithoutPagination = async (
accessToken: string,
url: string,
dataToReturn: any[] = []
): Promise<any[]> => {
try {
const data = await callMsGraph(accessToken, url);
const dataJson = await data.json();
const newData = dataToReturn.concat(dataJson.value);
if (dataJson['#odata.nextLink']) {
const NEXT_URL = dataJson['#odata.nextLink'].split('/v1.0')[1];
return await callMsGraphWithoutPagination(
accessToken,
process.env.REACT_APP_GRAPH_URL + NEXT_URL,
newData
);
}
return dataToReturn.concat(dataJson.value);
} catch (error) {
/* eslint-disable no-console */
console.error(error);
/* eslint-enable no-console */
throw error;
}
};
export default callMsGraph;
useUsers.tsx
export const useUsers = () => {
const token = useToken();
const [users, setUsers] = React.useState<BirthdayPerson[]>([]);
React.useEffect(() => {
if (token) {
callMsGraphWithoutPagination(token, graphConfig.usersEndpoint).then(async (data: any) => {
const processedData: any[] = await Promise.all(
data.map(async (element: any) => {
const user = await callMsGraph(token, graphConfig.userBirthdayEndpoint(element.id));
const userJson = await user.json();
const image = await callMsGraph(token, graphConfig.userPhotoEndpoint(element.id));
const blob = await image.blob();
const returnElement: BirthdayPerson = {
displayName: element.displayName,
birthday: userJson.value,
id: element.id,
};
if (blob !== null) {
window.URL = window.URL || window.webkitURL;
returnElement.picture = window.URL.createObjectURL(blob);
}
return returnElement;
})
);
setUsers([].concat(...processedData));
});
}
}, [token]);
return users;
};
helpers.ts
interface IUpdateData {
slideCount: number;
}
const sortAndFilterBirthdays = (people: BirthdayPerson[], daysToGet: number) =>
people
.sort((firstEl, secondEl) => sortDate(firstEl.birthday, secondEl.birthday))
.filter(({ birthday }) => filterByAmountOfDays({ date: birthday, daysAfter: daysToGet }));
const getBirthdays: any = (people: BirthdayPerson[], daysToGet: number) => {
const validBirthdays = people.filter((element: any) => {
const year = moment(element.birthday).year();
return year !== 0;
});
const result = sortAndFilterBirthdays(validBirthdays, daysToGet);
// if it's okay
if (result.length > 1 && daysToGet <= 30) {
return result;
}
// if not okay, filters by future dates, concats with 'next year' dates, returns 2 dates
const fallbackResult = validBirthdays
.sort((firstEl, secondEl) => sortDate(firstEl.birthday, secondEl.birthday))
.filter((person: BirthdayPerson) => {
const currentYear = moment().year();
const date = moment(person.birthday, DATE_FORMAT).set('years', currentYear);
return moment().diff(date, 'days') <= 0;
});
return fallbackResult.concat(validBirthdays).splice(0, 2);
};
Any help or indication would be great!
From all the changes I've done another object will complain that Type 'x' is not assignable to type 'string'
Firstly you need to somehow type responses from API, because right now, as you have seen, call to .json() on Response object returns unknown, which make sense because no one knows what response the server returns. You may know what response it is expected to return, but not what it actually does.
Ideally therefore you need a parser that will verify that the response has correct structure and throws an error otherwise. There are libraries such as superstruct, yup, joi and others which you can use for this. Of course this is a lot of work and will need refactoring. Or if you don't care enough you can just cast the response object to appropriate type, but then if server returns something unexpected and the application cannot handle it, it's your fault.
Example with response parsing using superstruct
import {string, number, create, Infer} from 'superstruct'
// I assume `BirthdayUser` is just a string, but you can type more complex objects as well
const BirthdayUser = string()
// This is so that you don't have to list fields twice: once
// for the parser and once for typescript
type BirthdayUser = Infer<typeof BirthdayUser>
// Then use the parser
const response = await callMsGraph(acessToken, url)
const userJson = await response.json()
// user variable has inferred appropriate type, and if the response doesn't
// comply with the definition of `BirthdayUser` an error will be thrown
// Also I assume MS graph doesn't just return plain value but wraps it in an object with `value` field, so writing it here
const user = create(userJson, object({ value: BirthdayUser }))
Example of "I don't care enough" solution
type BirthdayUser = string
const response = await callMsGraph(accessToken, url)
// Same thing with wrapping into object with `value` field
const userJson = (await response.json()) as {value: BirthdayUser}
This is a bit awkward, because API call returns Response object and not the actual data. It might be easier to work with if you move parsing and casting logic inside of callMsGraph. It's not obligatory of course, but I still provide an example because after that you need to type callMsGraphWithoutPagination and it will use the same idea
import {object, create, Struct} from 'superstruct'
async function callMsGraphParsed<T>(
accessToken: string,
url: string,
// Since we need information about object structure at runtime, just making function
// generic is not enough, you need to pass the parser structure as an argument
struct: Struct<T>
) {
// ...
const response = await fetch(...)
const json = await response.json()
// Same, verifies that response complies to provided structure, if it
// does returns type object (of type `T`), otherwise throws an error
return create(json, object({ value: struct }))
}
async function callMsGraphLazy<T>(accessToken: string, url: string) {
// ...
const response = await fetch(...)
const json = await response.json()
return json as {value: T}
}
However I only call .json() here, if you want to use this solution, you will then need either a different function or another argument if you also want it to call .blob() for some API calls.
Now you type callMsGraphWithoutPagination using in the same way:
export const callMsGraphWithoutPaginationParsed = async <T>(
accessToken: string,
url: string,
dataToReturn: T[] = [],
struct: Struct<T>,
): Promise<T[]> => {
// If you rewrote `callMsGraph` to use parsing
const dataJson = await callMsGraph(accessToken, url, struct);
const newData = dataToReturn.concat(dataJson.value);
// ...
}
export const callMsGraphWithoutPaginationLazy= async <T>(
accessToken: string,
url: string,
dataToReturn: T[] = [],
): Promise<T[]> => {
// If you left `callMsGraph` as is
const data = await callMsGraph(accessToken, url);
const dataJson = (await data.json()) as {value: T}
const newData = dataToReturn.concat(dataJson.value);
// ...
}
Then use it
// Not sure if you are requesting `BirthdayUser` array here or some other entity, so change it to whatever you expect to receive
callMsGraphWithoutPagination<BirthdayUser>(token, graphConfig.usersEndpoint).then(async (data) => {
// "data" is inferred to have type BirthdayUser[]
data.map(element => {
// "element" is inferred to have type BirthdayUser
})
})
Also everywhere I wrote "I assume" and "Not sure" is missing info that you should probably have provided in the question. It didn't turn out to be relevant for me, but it could have. Good luck!
I don't know how to reponse correctly my component.
I'm getting this error:
Type 'Promise<AnyTransaction>[]' is not assignable to type 'AnyTransaction[]'.
Type 'Promise<AnyTransaction>' is missing the following properties from type 'Transaction<any, any, any, any>': id, feeLookup, sender, receiver, and 10 more.
The component looks like this:
import { AnyTransaction, TransactionStatus } from '../types';
// more code...
export default async function getMyTransactions (
_parent: unknown,
_args: unknown,
// context: Context,
): Promise<Array<AnyTransaction>> {
// ): Promise<String> {
// some code
const { Items } = await DocumentClient.getInstance()
.query(some query
.promise();
const transactions = (Items || []) as Array<AnyTransaction>;
// transactions is an array of objects.
return transactions.map(parseDeprecatedStatuses).map(processTransactionStatus);
// parseDeprecatedStatuses: just parse some data
// processTransactionStatus code is below
}
// processTransactionStatus.ts:
import Factory from '../factory';
import { AnyTransaction } from '../types';
export default async function processTransactionStatus (
transaction: AnyTransaction
): Promise<AnyTransaction>{
const agent = Factory.buildAgentFromCode(transaction.destination.agent);
transaction.status = await agent.fetchTransactionStatus(transaction)
return transaction;
}
I'm really confused about how I'm returning from the component and what I got.
You are mapping to a list of promises, so you have to await all of those:
return await Promise.all(transactions
.map(parseDeprecatedStatuses)
.map(processTransactionStatus));
I'm developing a file importer, I decide to move with the stream approach, I have a series of step that I must follow to reach my goal, that can see below:
Download the file.
Parse to CSV
Validate the entire file.
"Translate" and save on db
Here is the snippet that pipe then all:
protected async pipe(readable: Readable, transform: Transform, validator: Transform, importer: T) {
const asyncPipeline = promisify(pipeline);
try {
await asyncPipeline(readable, transform, validator, importer)
logger.info("Import Finished")
} catch (error) {
const { message, stack } = error
logger.error(message, { stack })
}
}
Here is the snippet that calls the pipe method above
super.pipe(
response // response from http.get,
csv() // csv-parser library,
new Validator(),
new Importer()
)
The Validator class:
export class Validator extends Transform {
constructor() {
super({ objectMode: true })
}
_transform(chunk: any, encoding: string, done: any) {
this.push(chunk)
logger.info("Validating", { chunk })
done()
}
_flush(done: any) {
done()
}
}
and finally the importer class:
export class Importer extends Writable {
private buffer: Car[]
constructor() {
super({ objectMode: true })
this.buffer = new Array()
}
_write(row: object, enc: string, next: any) {
this.import(row)
.then(() => next())
.catch((error: Error) => next(error))
}
private async import(data: any): Promise<Car[]> {
this.buffer.push(data)
logger.info(this.buffer.length.toString());
return await db.save(data) // fake method;
}
}
When super.pipe is called the output is alternating from "Validation" and the total size of the buffer array.
{"level":30,"time":1588773096585,"pid":40537,"msg":"1"}
{"level":30,"time":1588773096586,"pid":40537,"msg":"Validating"}
{"level":30,"time":1588783063275,"pid":61571,"msg":"Importation finished"}
{"level":30,"time":1588773096633,"pid":40537,"msg":"2"}
There's a way, that first? I execute the validator stream, and after go to the importer stream?
I am using feathers js 4 with objection ORM and trying to create a service that returns objection js model data that includes an eager loaded BleongsToOneRelation but I keep getting the response:
Cannot read property 'get' of undefined
I have used the Feathers CLI to generate my service (and model) and then modified to add a relationship as follows:
devices.model.js
const { Model } = require('objection');
class devices extends Model {
static get tableName() {
return 'devices';
}
static get jsonSchema() {
return {
type: 'object',
required: ['macAddress'],
properties: {
macAddress: { type: 'string' },
circuitId: { type: 'integer' },
}
};
}
static get relationMappings() {
const Circuit = require('./circuits.model')();
const DeviceType = require('./device-types.model')();
return {
circuit: {
relation: Model.BelongsToOneRelation,
modelClass: Circuit,
join: {
from: 'devices.circuitId',
to: 'circuits.id'
}
}
};
}
$beforeInsert() {
this.createdAt = this.updatedAt = new Date().toISOString();
}
$beforeUpdate() {
this.updatedAt = new Date().toISOString();
}
}
module.exports = function (app) {
const db = app.get('knex');
db.schema.hasTable('devices').then(exists => {
if (!exists) {
db.schema.createTable('devices', table => {
table.increments('id');
table.string('macAddress');
table.integer('circuitId');
table.timestamp('createdAt');
table.timestamp('updatedAt');
})
.then(() => console.log('Created devices table')) // eslint-disable-line no-console
.catch(e => console.error('Error creating devices table', e)); // eslint-disable-line no-console
}
})
.catch(e => console.error('Error creating devices table', e)); // eslint-disable-line no-console
return devices;
};
device.class.js
const { Devices } = require('./devices.class');
const createModel = require('../../models/devices.model');
const hooks = require('./devices.hooks');
module.exports = function (app) {
const options = {
Model: createModel(app),
paginate: app.get('paginate'),
whitelist: ['$eager', '$joinRelation', '$modifyEager'],
allowedEager: ['circuit']
};
// Initialize our service with any options it requires
app.use('/devices', new Devices(options, app));
// Get our initialized service so that we can register hooks
const service = app.service('devices');
service.hooks(hooks);
};
Then I am calling the /devices GET request using http://localhost:3030/devices?$eager=circuit
and the result is:
{
"name": "GeneralError",
"message": "Cannot read property 'get' of undefined",
"code": 500,
"className": "general-error",
"data": {},
"errors": {}
}
Have tried adding a custom find() method to devices.class.js but this doesn't seem to help.
I've searched through similar questions here and read through the feathers-objection docs but just can't see what I'm missing. Any assistance would be greatly appreciated.
Sounds like your app variable is undefined. Debugger could help you a lot if you add a breakpoint and check call stack to see why... Maybe you are not passing it everywhere correctly to your modules.
This is the code that throws your error:
https://runkit.com/embed/kipjj807i90l
It happens because in your relationMappings you are initializing models without the app argument which is expected. In the feathers-objection examples it is shown how to modify your models in order to use them in the relation mappings: you have to explicitly handle the missing app argument.
I know it's much later, but I just found the same problem.
The solution is to modify the feathers-objection models with the following, based on the Models section here
module.exports = function(app) {
if (app) {
const db = app.get('knex');
.... etc
}
return User;
};
module.exports = User;
I am pretty new to TypeScript and Apollo. I'm trying to setup my Apollo server for the first time but I keep getting an error about 'DataSource'. I'm unsure what this means and how to fix it.
import { ApolloServer, gql } from 'apollo-server';
//...
const { RESTDataSource } = require('apollo-datasource-rest');
class MoviesAPI extends RESTDataSource {
constructor() {
super();
this.baseURL = 'https://movies-api.example.com/';
}
async getMovie(id) {
return this.get(`movies/${id}`);
}
async getMostViewedMovies(limit = 10) {
const data = await this.get('movies', {
per_page: limit,
order_by: 'most_viewed',
});
return data.results;
}
}
const SERVERCONFIG = {
'typeDefs': gql(typeDefs) ,
resolvers,
dataSources: ()=> ({
MoviesAPI: new MoviesAPI(),
} ) ,
};
const server = new ApolloServer(SERVERCONFIG);
I get the following error:
Type '{ 'typeDefs': DocumentNode; resolvers: { Query: {}; }; dataSources: () => { MoviesAPI: MoviesAPI; }; }' is not assignable to type 'ApolloServerExpressConfig'.
The types returned by 'dataSources()' are incompatible between these types.
Type '{ MoviesAPI: MoviesAPI; }' is not assignable to type 'DataSources<object>'.
Property 'MoviesAPI' is incompatible with index signature.
Type 'MoviesAPI' has no properties in common with type 'DataSource<object>'.
I can get the error to go away if I did something like: dataSources: ()=> ({...MoviesAPI}) but I do not think that fixes the problem...
Does anyone know what is causing this?
Change
const { RESTDataSource } = require('apollo-datasource-rest');
to
import { RESTDataSource } from 'apollo-datasource-rest';
This should fix it for you.