graphql + mongoose + typescript, how to reduce model definition duplicated - graphql

I use mongoose, typescript and graphql to build my app.
I am a full-stack developer.
The problem is I define the fields and types of model FIVE times.
server side:
models/book.ts:
// first time
interface IBook extends mongoose.Document {
title: string;
author: string;
}
// second time
const bookSchema = new mongoose.Schema({
title: String,
author: String
})
const Book: mongoose.Model<IBook> = mongoose.model<IBook>('Book', bookSchema)
graphql/typeDefs.ts
const typeDefs = `
// third time
type Book {
title: String!
author: String!
}
// fourth time
input BookInput {
title: String!
author: String!
}
`
client side:
interfaces/book.ts
// fifth time
interface IBook {
title: string;
author: string;
}
As you can see. the title and author fields and types are defined FIVE times.
There are three main disadvantages:
duplicated
lack of maintainability
inefficient
Is there a way to solve this? I think this is almost a DRY problem.
Here are my thinkings:
universal app - extract some common modules used in client and server side.
make a tool handle this.
make a project generator or command line tool like ng-cli for generating model and types statically which means before the run-time
make model definition decorator or syntax sugar generate model and types dynamically at run-time

We recently ran into this issue, requiring us to maintaining a duplicated Typescript interface alongside our Mongoose schemas (for us the issue was solely server-side as we are not using graphql or client-side Typescript)
We built mongoose-tsgen to address this. It may not handle all cases concerned here, but could be easily bootstrapped to handle your use case.
It reads your Mongoose schemas and generates an index.d.ts file which includes an interface for each document and subdocument. These interfaces can be imported from the mongoose module directly like so:
import { IUser } from "mongoose"
async function getUser(uid: string): IUser {
// `user` is of type IUser
const user = await User.findById(uid)
return user;
}

In case anyone is still wondering,
You can use TypeGraphQL together with Typegoose to create all the schemas within one single class with decorators like this:
#ObjectType()
export class Book{
#Field() #prop({ required: true })
title!: string;
#Field() #prop({ required: true })
name!: string;
}

Related

NestJS GraphQL using the same class as input and output type

After reading this post, I wanted to know what's the right approach assuming a "code first" paradigm. The following seems to work - using Location as both input and output (implicitly generating 2 separate sections in the schema file, derived from the same code).
#ObjectType()
#InputType('LocationInput')
export class Location {
#Field()
lat: number;
#Field()
lon: number;
}
I know there's a lot of no-nos about input and output types sharing code - the fact that inputs have various validations that outputs dont, and so on. But I think Location is a good exception because in the big picture, let's say Location is like String or Int - treated throughout the entire schema as a primitive:
#InputType
class MyInput {
#Field(type => Location)
location: Location
}
#ObjectType
class MyPlace {
#Field(type => Location, { nullable: true })
location?: Location
}
Then I don't see why I should not re-use something as simple as a Location.
Does anyone else have a take on this?

How to alias a type (e.g. string, int...)

I know from the doc I can alias a field. But how could I alias a type?
E.g. how could I alias a int or string type? Will type MyStringType = String work?
Motivation
I have a signin mutation that returns an auth token, and I would like to write something like:
type Mutation {
signin(email: String!, password: String!): AuthToken
}
type AuthToken = String
GraphQL does not support type aliases.
You can, however, implement a custom scalar with the same properties as an existing scalar but a different name. It's unclear from your question what language or libraries you're working with, but in GraphQL.js, you can just do something like:
const { GraphQLString, GraphQLScalarType } = require('graphql')
const AuthToken = new GraphQLScalarType({
name: 'AuthToken',
description: 'Your description here',
serialize: GraphQLString.serialize,
parseValue: GraphQLString.parseValue,
parseLiteral: GraphQLString.parseLiteral,
})
Here's how to add a custom scalar in Apollo Server. Keep in mind that doing this may actually make things harder for clients, especially ones written in strongly typed languages. If you don't need custom serialization or parsing behavior, I would stick to the built-in scalars.

How to have calculated properties on state?

Using NGXS, I have state in my project. I use a service to load some data into state. All works well. However, I also need to expose a property which takes data from another property on state and transforms it. I want to use .pipe to ensure that transformed data stays in sync with actual data. I just can't figure out where to put this transformation logic.
You could use a #Selector to project a derived property based on your state model, e.g:
export interface MyStateModel {
firstName: string;
lastName: string;
}
#State<MyStateModel>()
export class MyState {
// Selector to project derived 'fullName' property of the state.
#Selector()
static fullName(state: MyStateModel): string {
return state.firstName + ' ' + state.lastName;
}
// Load the data
#Action(LoadData)
loadData({patchState}: StateContext<MyStateModel>) {
patchState({
firstName: 'Joe',
lastName: 'Bloggs',
}
}
}
Then in your component use to that Selector directly:
#Select(MyState.fullName) fullName$: Observable<string>;

Using GraphQL Fragment on multiple types

If I have a set of field that is common to multiple types in my GraphQL schema, is there a way to do something like this?
type Address {
line1: String
city: String
state: String
zip: String
}
fragment NameAndAddress on Person, Business {
name: String
address: Address
}
type Business {
...NameAndAddress
hours: String
}
type Customer {
...NameAndAddress
customerSince: Date
}
Fragments are only used on the client-side when making requests -- they can't be used inside your schema. GraphQL does not support type inheritance or any other mechanism that would reduce the redundancy of having to write out the same fields for different types.
If you're using apollo-server, the type definitions that make up your schema are just a string, so you can implement the functionality you're looking for through template literals:
const nameAndAddress = `
name: String
address: Address
`
const typeDefs = `
type Business {
${nameAndAddress}
hours: String
}
type Customer {
${nameAndAddress}
customerSince: Date
}
`
Alternatively, there are libraries out there, like graphql-s2s, that allow you to use type inheritance.
Don't know if it was not available at this question time, but I guess interfaces should fit your needs

What and How does Relay and GraphQL interfaces work?

we define a type in GraphQL like this:
const GraphQLTodo = new GraphQLObjectType({
name: 'Todo',
fields: {
id: globalIdField('Todo'),
text: {
type: GraphQLString,
resolve: (obj) => obj.text,
},
complete: {
type: GraphQLBoolean,
resolve: (obj) => obj.complete,
},
},
interfaces: [nodeInterface], // what is this?
});
and I've read there is GraphQLInterfaceType - is more suitable when the types are basically the same but some of the fields are different(is this something like a foreign key?)
and in Relay we get the nodefield and nodeInterface with nodeDefinitions:
const {nodeInterface, nodeField} = nodeDefinitions(
(globalId) => {
const {type, id} = fromGlobalId(globalId);
if (type === 'Todo') {
return getTodo(id);
} else if (type === 'User') {
return getUser(id);
}
return null;
},
(obj) => {
if (obj instanceof Todo) {
return GraphQLTodo;
} else if (obj instanceof User) {
return GraphQLUser;
}
return null;
}
);
The docs and samples only used one on interfaces: [] //it's an array. but when do I need to use many interfaces? I am just confused on what it is, I've read a lot about it(don't know if my understanding is correct), just can't seem to wrap it in my head
A GraphQLInterfaceType is one way GraphQL achieves polymorphism, i.e. types that consist of multiple object types. For example, suppose you have two base object types, Post and Comment. Suppose you want a field that could get a list of both comments and posts. Conveniently, both these types have an id, text, and author field. This is the perfect use case for an interface type. An interface type is a group of shared fields, and it can be implemented by any object type which possesses those fields. So we create an Authored interface and say the Comment and Post implement this interface. By placing this Authored type on a GraphQL field, that field can resolve either posts or comments (or a heterogeneous list of both types).
But wait, Post and Comment accept an array of interfaces. I could pass multiple interfaces here. Why? Since the requirement for implementing an interface is possession of all the fields in that interface, there is no reason why any object type can't implement multiple interfaces. To draw from your example, the Node interface in Relay only needs id. Since our Post and Comment have id, they could implement both Node and Authored. But many other types will likely implement Node, ones that aren't part of Authored.
This makes your object types much more re-usable. If you assign interfaces to your field instead of object types, you can easily add new possible types to the fields in your schema as long as you stick to these agreed-upon interfaces.

Resources