Infer type from return value with JSDoc - jsdoc

What is the correct way to assign type from a functions return type?
async function getFood(){
const food = await {fruit: 'banana', qty: 3}
return food
}
/**
* made up syntax
* #type {returnOf getFood}
* */
let doesntWork;
/** #type {{fruit: string, qty: number}} */
let worksButNoInferring
let worksButRequiresCallingFunction = getFood()
As evidenced by VSCode, the return type is available. I just don't know how to get it without calling the function.

It is probably not according to the JSDoc specification, but VS Code supports Typescript syntax in JSDoc.
async function getFood(){
const food = await {fruit: 'banana', qty: 3}
return food
}
/** #type { typeof getFood extends (...args: any[]) => infer U ? U : any } */
let test;
Playground

Related

mongoose can't store double types?

I am trying to store double BSON type inside mongodb. I am using mongoose. I have tried every possible way but still it's stored as int.
I have tried #mongoosejs/double and mongoose-float but none of them work.
await Variant.insertOne(
{
price: 345,
discount: 10,
},
)
product model
import { Schema } from "mongoose"
const Double = require("#mongoosejs/double")
// const Float = require("mongoose-float").loadType(mongoose)
export const ProductVariantEmbeddedSchema = new Schema({
price: Double,
discount: Double,
})
here is a custom type that I have created by the help of #mongoosejs/double.
import mongoose from "mongoose"
export default function mongooseDouble(mongoose) {
class DoubleType extends Number {
constructor(v) {
super(v)
this.value = v
}
toBSON() {
return this.value
}
}
class Double extends mongoose.SchemaType {
constructor(key, options) {
super(key, options, "Double")
Object.assign(this.$conditionalHandlers, {
$lt: (val) => this.castForQuery(val),
$lte: (val) => this.castForQuery(val),
$gt: (val) => this.castForQuery(val),
$gte: (val) => this.castForQuery(val),
})
}
cast(val) {
if (val == null) {
return val
}
const _val = Number(val)
if (isNaN(_val)) {
throw new mongoose.SchemaType.CastError(
"Double",
val + " is not a valid double"
)
}
return new DoubleType(_val)
}
}
mongoose.Schema.Types.Double = Double
mongoose.Types.Double = DoubleType
return mongoose
}
// export default Double
I believe there is no such type as Double. JavaScript has Number which supports int, float, double, etc. Also, if you see mongoose documentation then you will see Double is not a valid type. Instead, you should use Number.
const ProductVariantEmbeddedSchema = new Schema({
price: Number,
discount: Number,
});
EDIT: After discussion in the comment I believe this can be a workaround.
const price = 5;
await Model.create({
price: price * 1.0001,
...
});
In the database, the price would be a double type but the value would be 5.0005. So, whenever you want the use the value of price either make it int or use .toFixed(2) or similar function to limit the decimal point to 2 places.
ok it seems that if mongoose custom types not worked I can use raw mongodb queries.
import mongoose, {mongo} from 'mongoose'
const result = await mongoose.connection.collection('Variant').insertMany([{price: new mongo.Double(34)}, {price: new mongo.Double(45)}])
const storedValues = result.opt
Another solution that works :
const mongoose = require('mongoose');
const setter = (value: any) => {
const result = mongoose.Types.Decimal128.fromString(parseFloat(value).toFixed(2));
result._bsontype = 'Double'; // solution
return result;
};
const Double = new mongoose.Schema(
{
anyDoubleValue: {
type: mongoose.SchemaTypes.Mixed,
set: setter
}
},
{
collection: 'double'
}
);
export = mongoose.model('DoubleModel', Double);
You can manually change the _bsonType properties to Double

MongoDB and TypeScript: Decouple a domain entity's id type from MongoDB's ObjectID

Inside my MongoDB repositories, entities have an _id: ObjectID type to be handled properly. However, I would like my domain entities to have a simple id: string attribute to avoid any dependencies on any database or framework. The solution I came up with so far looks as follows:
export interface Book {
id: string;
title: string;
}
// A MongodbEntity<Book> would now have an _id instead of its string id
export type MongodbEntity<T extends { id: string; }> = Omit<T, 'id'> & { _id: ObjectID; };
In my repository this would work:
async findOneById(id: string): Promise<Book | null> {
const res = await this.collection.findOneById({_id: new ObjectId(id)});
return res ? toBook(res) : null;
}
function toBook(dbBook: MongodbEntity<Book>): Book {
const {_id, ...rest} = dbBook;
return {...rest, id: _id.toHexString() };
}
What doesn't work is to make this behavior generic. A converter function like this:
function toDomainEntity<T extends {id: string}>(dbEntity: MongoDbEntity<T>): T {
const {_id, ...rest} = dbEntity;
return {...rest, id: _id.toHexString() };
}
leads to an error described here.
What I am looking for is either a working solution for the generic toDomainEntity function or a different (generic) approach that would let me decouple my domain entity types from MongoDB's _id: ObjectID type.

How to implement a node query resolver with apollo / graphql

I am working on implementing a node interface for graphql -- a pretty standard design pattern.
Looking for guidance on the best way to implement a node query resolver for graphql
node(id ID!): Node
The main thing that I am struggling with is how to encode/decode the ID the typename so that we can find the right table/collection to query from.
Currently I am using postgreSQL uuid strategy with pgcrytpo to generate ids.
Where is the right seam in the application to do this?:
could be done in the primary key generation at the database
could be done at the graphql seam (using a visitor pattern maybe)
And once the best seam is picked:
how/where do you encode/decode?
Note my stack is:
ApolloClient/Server (from graphql-yoga)
node
TypeORM
PostgreSQL
The id exposed to the client (the global object id) is not persisted on the backend -- the encoding and decoding should be done by the GraphQL server itself. Here's a rough example based on how relay does it:
import Foo from '../../models/Foo'
function encode (id, __typename) {
return Buffer.from(`${id}:${__typename}`, 'utf8').toString('base64');
}
function decode (objectId) {
const decoded = Buffer.from(objectId, 'base64').toString('utf8')
const parts = decoded.split(':')
return {
id: parts[0],
__typename: parts[1],
}
}
const typeDefs = `
type Query {
node(id: ID!): Node
}
type Foo implements Node {
id: ID!
foo: String
}
interface Node {
id: ID!
}
`;
// Just in case model name and typename do not always match
const modelsByTypename = {
Foo,
}
const resolvers = {
Query: {
node: async (root, args, context) => {
const { __typename, id } = decode(args.id)
const Model = modelsByTypename[__typename]
const node = await Model.getById(id)
return {
...node,
__typename,
};
},
},
Foo: {
id: (obj) => encode(obj.id, 'Foo')
}
};
Note: by returning the __typename, we're letting GraphQL's default resolveType behavior figure out which type the interface is returning, so there's no need to provide a resolver for __resolveType.
Edit: to apply the id logic to multiple types:
function addIDResolvers (resolvers, types) {
for (const type of types) {
if (!resolvers[type]) {
resolvers[type] = {}
}
resolvers[type].id = encode(obj.id, type)
}
}
addIDResolvers(resolvers, ['Foo', 'Bar', 'Qux'])
#Jonathan I can share an implementation that I have and you see what you think. This is using graphql-js, MongoDB and relay on the client.
/**
* Given a function to map from an ID to an underlying object, and a function
* to map from an underlying object to the concrete GraphQLObjectType it
* corresponds to, constructs a `Node` interface that objects can implement,
* and a field config for a `node` root field.
*
* If the typeResolver is omitted, object resolution on the interface will be
* handled with the `isTypeOf` method on object types, as with any GraphQL
* interface without a provided `resolveType` method.
*/
export function nodeDefinitions<TContext>(
idFetcher: (id: string, context: TContext, info: GraphQLResolveInfo) => any,
typeResolver?: ?GraphQLTypeResolver<*, TContext>,
): GraphQLNodeDefinitions<TContext> {
const nodeInterface = new GraphQLInterfaceType({
name: 'Node',
description: 'An object with an ID',
fields: () => ({
id: {
type: new GraphQLNonNull(GraphQLID),
description: 'The id of the object.',
},
}),
resolveType: typeResolver,
});
const nodeField = {
name: 'node',
description: 'Fetches an object given its ID',
type: nodeInterface,
args: {
id: {
type: GraphQLID,
description: 'The ID of an object',
},
},
resolve: (obj, { id }, context, info) => (id ? idFetcher(id, context, info) : null),
};
const nodesField = {
name: 'nodes',
description: 'Fetches objects given their IDs',
type: new GraphQLNonNull(new GraphQLList(nodeInterface)),
args: {
ids: {
type: new GraphQLNonNull(new GraphQLList(new GraphQLNonNull(GraphQLID))),
description: 'The IDs of objects',
},
},
resolve: (obj, { ids }, context, info) => Promise.all(ids.map(id => Promise.resolve(idFetcher(id, context, info)))),
};
return { nodeInterface, nodeField, nodesField };
}
Then:
import { nodeDefinitions } from './node';
const { nodeField, nodesField, nodeInterface } = nodeDefinitions(
// A method that maps from a global id to an object
async (globalId, context) => {
const { id, type } = fromGlobalId(globalId);
if (type === 'User') {
return UserLoader.load(context, id);
}
....
...
...
// it should not get here
return null;
},
// A method that maps from an object to a type
obj => {
if (obj instanceof User) {
return UserType;
}
....
....
// it should not get here
return null;
},
);
The load method resolves the actual object. This part you would have work more specifically with your DB and etc...
If it's not clear, you can ask! Hope it helps :)

Custom Validator Angular 2

I've written a web api function that takes a username from the textfield and checks if the username is already taken. To know if the username is available or not, my server returns Y if it is available and N if its not.
To validate the username, I'm using a ValidatorFn in Angular2 so validate the input. However, my validator function is not working.
Here is the validator function:
interface Validator<T extends FormControl> {
(c: T): { [error: string]: any };
}
function validateUsername(c: string) : ValidatorFn {
return (this.isAvailable(c)=='Y') ? null : {
validateUsername: {
valid: false
}
};
}
Here is the isAvailable function:
private isAvailable(username: string) {
let usernameAvailable;
let url = 'URL/api/auth/checkuser/' + username;
let headers = new Headers();
headers.append('User', sessionStorage.getItem('username'));
headers.append('Token', sessionStorage.getItem('token'));
headers.append('AccessTime', sessionStorage.getItem('AccessTime'));
let options = new RequestOptions({ headers: headers });
this.http.get(url, options)
.subscribe((res: Response) => usernameAvailable);
return usernameAvailable; //returns Y or N
}
Form Builder:
complexForm: FormGroup;
constructor(private http: Http, fb: FormBuilder) {
this.complexForm = fb.group({
'username': [null, Validators.compose([Validators.required, Validators.minLength(5), Validators.maxLength(10), validateUsername(this.complexForm.controls['username'].value)])],
})
}
validateUsername(this.complexForm.controls['username'].value) is failing and I'm getting this error:
[ts] Type '{ validateUsername: { valid: boolean; }; }' is not assignable to type 'ValidatorFn'. Object literal may only specify known properties, and 'validateUsername' does not exist in type 'ValidatorFn'. (property) validateUsername: {
valid: boolean;
}
You not adding your validator function correctly. You don't need to call your function when you register it:
this.complexForm = fb.group({
'username': [null, Validators.compose(
[
Validators.required,
Validators.minLength(5),
Validators.maxLength(10),
validateUsername <----- don't call it here
]
)],
})
You can see that some functions are called:
Validators.minLength(5),
But that is factory function call and not a validator function call. During initialization they return ValidatorFn:
/**
* Validator that requires controls to have a value of a minimum length.
*/
static minLength(minLength: number): ValidatorFn {
return (control: AbstractControl): ValidationErrors | null => {
...
}
See more in the official docs.
Also, it seems that your validator is async, so you have to pass it in the async array. And I don't think you need Validators.compose. The correct configuration should therefore be like this:
this.complexForm = fb.group({
'username': [null, [
Validators.required,
Validators.minLength(5),
Validators.maxLength(10),
], [validateUsername]]
})
Regarding the error:
Type '{ valid: boolean; }' is not assignable to type ValidatorFn.
You need to use the correct return type ValidationErrors instead of ValidatorFn:
function validateUsername(c: string) : ValidationErrors {
return (this.isAvailable(c)=='Y') ? null : {
validateUsername: {
valid: false
}
};
}

apollostack/graphql-server - how to get the fields requested in a query from resolver

I am trying to figure out a clean way to work with queries and mongdb projections so I don't have to retrieve excessive information from the database.
So assuming I have:
// the query
type Query {
getUserByEmail(email: String!): User
}
And I have a User with an email and a username, to keep things simple. If I send a query and I only want to retrieve the email, I can do the following:
query { getUserByEmail(email: "test#test.com") { email } }
But in the resolver, my DB query still retrieves both username and email, but only one of those is passed back by apollo server as the query result.
I only want the DB to retrieve what the query asks for:
// the resolver
getUserByEmail(root, args, context, info) {
// check what fields the query requested
// create a projection to only request those fields
return db.collection('users').findOne({ email: args.email }, { /* projection */ });
}
Of course the problem is, getting information on what the client is requesting isn't so straightforward.
Assuming I pass in request as context - I considered using context.payload (hapi.js), which has the query string, and searching it through various .split()s, but that feels kind of dirty. As far as I can tell, info.fieldASTs[0].selectionSet.selections has the list of fields, and I could check for it's existence in there. I'm not sure how reliable this is. Especially when I start using more complex queries.
Is there a simpler way?
In case you don't use mongDB, a projection is an additional argument you pass in telling it explicitly what to retrieve:
// telling mongoDB to not retrieve _id
db.collection('users').findOne({ email: 'test#test.com' }, { _id: 0 })
As always, thanks to the amazing community.
2020-Jan answer
The current answer to getting the fields requested in a GraphQL query, is to use the graphql-parse-resolve-info library for parsing the info parameter.
The library is "a pretty complete solution and is actually used under the hood by postgraphile", and is recommended going forward by the author of the other top library for parsing the info field, graphql-fields.
Use graphql-fields
Apollo server example
const rootSchema = [`
type Person {
id: String!
name: String!
email: String!
picture: String!
type: Int!
status: Int!
createdAt: Float
updatedAt: Float
}
schema {
query: Query
mutation: Mutation
}
`];
const rootResolvers = {
Query: {
users(root, args, context, info) {
const topLevelFields = Object.keys(graphqlFields(info));
return fetch(`/api/user?fields=${topLevelFields.join(',')}`);
}
}
};
const schema = [...rootSchema];
const resolvers = Object.assign({}, rootResolvers);
// Create schema
const executableSchema = makeExecutableSchema({
typeDefs: schema,
resolvers,
});
Sure you can. This is actually the same functionality that is implemented on join-monster package for SQL based db's. There's a talk by their creator: https://www.youtube.com/watch?v=Y7AdMIuXOgs
Take a look on their info analysing code to get you started - https://github.com/stems/join-monster/blob/master/src/queryASTToSqlAST.js#L6-L30
Would love to see a projection-monster package for us mongo users :)
UPDATE:
There is a package that creates a projection object from info on npm: https://www.npmjs.com/package/graphql-mongodb-projection
You can generate MongoDB projection from info argument. Here is the sample code that you can follow
/**
* #description - Gets MongoDB projection from graphql query
*
* #return { object }
* #param { object } info
* #param { model } model - MongoDB model for referencing
*/
function getDBProjection(info, model) {
const {
schema: { obj }
} = model;
const keys = Object.keys(obj);
const projection = {};
const { selections } = info.fieldNodes[0].selectionSet;
for (let i = 0; i < keys.length; i++) {
const key = keys[i];
const isSelected = selections.some(
selection => selection.name.value === key
);
projection[key] = isSelected;
}
console.log(projection);
}
module.exports = getDBProjection;
With a few helper functions you can use it like this (typescript version):
import { parceGqlInfo, query } from "#backend";
import { GraphQLResolveInfo } from "graphql";
export const user = async (parent: unknown, args: unknown, ctx: unknown, info: GraphQLResolveInfo): Promise<User | null> => {
const { dbQueryStr } = parceGqlInfo(info, userFields, "id");
const [user] = await query(`SELECT ${dbQueryStr} FROM users WHERE id=$1;`, [1]);
return user;
};
Helper functions.
Few points:
gql_uid used as ID! string type from primary key to not change db types
required option is used for dataloaders (if field was not requested by user)
allowedFields used to filter additional fields from info like '__typename'
queryPrefix is used if you need to prefix selected fields like select u.id from users u
const userFields = [
"gql_uid",
"id",
"email"
]
// merge arrays and delete duplicates
export const mergeDedupe = <T>(arr: any[][]): T => {
// #ts-ignore
return ([...new Set([].concat(...arr))] as unknown) as T;
};
import { parse, simplify, ResolveTree } from "graphql-parse-resolve-info";
import { GraphQLResolveInfo } from "graphql";
export const getQueryFieldsFromInfo = <Required = string>(info: GraphQLResolveInfo, options: { required?: Required[] } = {}): string[] => {
const { fields } = simplify(parse(info) as ResolveTree, info.returnType) as { fields: { [key: string]: { name: string } } };
let astFields = Object.entries(fields).map(([, v]) => v.name);
if (options.required) {
astFields = mergeDedupe([astFields, options.required]);
}
return astFields;
};
export const onlyAllowedFields = <T extends string | number>(raw: T[] | readonly T[], allowed: T[] | readonly T[]): T[] => {
return allowed.filter((f) => raw.includes(f));
};
export const parceGqlInfo = (
info: GraphQLResolveInfo,
allowedFields: string[] | readonly string[],
gqlUidDbAlliasField: string,
options: { required?: string[]; queryPrefix?: string } = {}
): { pureDbFields: string[]; gqlUidRequested: boolean; dbQueryStr: string } => {
const fieldsWithGqlUid = onlyAllowedFields(getQueryFieldsFromInfo(info, options), allowedFields);
return {
pureDbFields: fieldsWithGqlUid.filter((i) => i !== "gql_uid"),
gqlUidRequested: fieldsWithGqlUid.includes("gql_uid"),
dbQueryStr: fieldsWithGqlUid
.map((f) => {
const dbQueryStrField = f === "gql_uid" ? `${gqlUidDbAlliasField}::Text AS gql_uid` : f;
return options.queryPrefix ? `${options.queryPrefix}.${dbQueryStrField}` : dbQueryStrField;
})
.join(),
};
};