How to get the types of a prisma model and place them in an array in Nestjs? - nestjs

I have a Nestjs app using Prisma. For one given model, I want to send via controller the model attributes with its types, so that I can generate a form in the front end. What is the best way to do it?
What I have:
A prisma model and its corresponding DTO class in Nestjs:
// create-job-offer.dto.ts
import { IsOptional, IsNumber } from 'class-validator';
export class CreateJobOfferDto {
#IsNumber()
#IsOptional()
mentorId: number;
#IsNumber()
#IsOptional()
companyId: number;
}
I want to be able to send to my FE something like:
[{key: 'mentorId', type: 'number'}, {key :'companyId', type: 'number'}]
So that I can create a form, and for every input have the correct type. For example, if type is boolean, I'd generate a checkbox.

TypeScipt type is not an object. You can not treat typescript types as objects - ie can't iterate through them etc.
The standard way to share types is to use a #types package. TSLang website has a good explanation on how that works.
https://www.typescriptlang.org/docs/handbook/declaration-files/publishing.html
However, at my company, we will only use #types packages for packages that get published to npm because it takes too long to create one.
We will have a types.ts file for each module. Say, a custom-form.types.ts file, which may have the following code in it.
export interface MentorForm {
mentorId: number;
companyId: number | undefined;
mentorName: string;
...
}
And then we will share this file with the frontend development team. Because of how typescript works, you can not iterate through the keys of the interface directly. You can use a transformer to get the keys of an interface in an array.
const KeysOfMentorForm = keys<MentorForm>();
If you want to generate your forms in the front end programmatically, the easiest option would probably be to do something like this.
let mentorA : MentorForm;
let keyAndTypeArray : {key : string, type: string}[] = [];
for(let key of KeysOfMentorForm){
let type : string = (typeOf mentorA[key]).toString();
keyAndTypeArray.push({key : key, type : type})
}
Then you can iterate through the keyAndTypeArray to generate the form.

Related

Prisma client type system creates strong coupling with service methods

I am using prisma ORM with nestjs and it is awesome. Can you please help me understand how can I separate my database layer from my service methods since results produced by prisma client queries are of types generated by prisma client itself ( so i wont be having those types when i shift to lets say typeorm ). how can i prevent such coupling of my service methods returning results of types generated by prisma client and not my custom entities. Hope it makes sense.
The generated #prisma/client library is responsible for generating both the types as well as the custom entity classes. As a result, if you replace Prisma you end up losing both.
Here are two possible workarounds that can decouple the types of your service methods from the Prisma ORM.
Workaround 1: Generate types indepedently of Prisma
With this approach you can get rid of Prisma altogether in the future by manually defining the types for your functions. You can use the types generated by Prisma as reference (or just copy paste them directly). Let me show you an example.
Imagine this is your Prisma Schema.
model Post {
id Int #default(autoincrement()) #id
createdAt DateTime #default(now())
updatedAt DateTime #updatedAt
title String #db.VarChar(255)
author User #relation(fields: [authorId], references: [id])
authorId Int
}
model User {
id Int #default(autoincrement()) #id
name String?
posts Post[]
}
You could define a getUserWithPosts function as follows:
// Copied over from '#prisma/client'. Modify as necessary.
type User = {
id: number
name: string | null
}
// Copied over from '#prisma/client'. Modify as necessary.
type Post = {
id: number
createdAt: Date
updatedAt: Date
title: string
authorId: number
}
type UserWithPosts = User & {posts: Post[]}
const prisma = new PrismaClient()
async function getUserWithPosts(userId: number) : Promise<UserWithPosts> {
let user = await prisma.user.findUnique({
where: {
id: userId,
},
include: {
posts: true
}
})
return user;
}
This way, you should be able to get rid of Prisma altogether and replace it with an ORM of your choice. One notable drawback is this approach increases the maintenance burden upon changes to the Prisma schema as you need to manually maintain the types.
Workaround 2: Generate types using Prisma
You could keep Prisma in your codebase simply to generate the #prisma/client and use it for your types. This is possible with the Prisma.validator type that is exposed by the #prisma/client. Code snippet to demonstrate this for the exact same function:
// 1: Define the validator
const userWithPosts = Prisma.validator<Prisma.UserArgs>()({
include: { posts: true },
})
// 2: This type will include a user and all their posts
type UserWithPosts = Prisma.UserGetPayload<typeof userWithPosts>
// function is same as before
async function getUserWithPosts(userId: number): Promise<UserWithPosts> {
let user = await prisma.user.findUnique({
where: {
id: userId,
},
include: {
posts: true
}
})
return user;
}
Additionally, you can always keep the Prisma types updated to your current database state using the Introspect feature. This will work even for changes you have made with other ORMS/Query Builders/SQL.
If you want more details, a lot of what I've mentioned here is touched opon in the Operating against partial structures of your model types concept guide in the Prisma Docs.
Finally, if this dosen't solve your problem, I would request that you open a new issue with the problem and your use case. This really helps us to track and prioritize problems that people are facing.

type-graphql entities how to omit fields from JSON.stringify?

We are using https://github.com/MichalLytek/type-graphql to define our graphql schema When we serialize the raw typescript entity object this doesn't respect the various field annotations in our GQL entities and ends up leaking unwanted data. Example below a Profile entity class
import { Field, Int, ObjectType } from 'type-graphql'
import { Column, Entity, ManyToOne, OneToMany } from 'typeorm'
import { Account } from '../account/account.entity'
export class Profile {
#Field()
#Column({ unique: true })
public username: string
#Field()
#Column()
public name: string
// Relations
#Column()
public accountId: string
#ManyToOne(type => Account, account => account.profiles, { eager: true })
public account: Account
}
account has sensitive data. When we JSON.stringify a Profile reference we don't want account output. Account is not annotated with #Field and we expect it would not be output.
The decorators used by type-graphql only exist to instruct type-graphql how to translate your class to a GraphQL type -- they are not going to somehow impact how an instance of the class is serialized by a native function like JSON.stringify.
In the context of your schema, the account won't ever be returned in the response unless you explicitly create a field for it, even if the Profile instance used by your resolvers has that property. This is a symptom of how field resolution works in GraphQL.js. However, a Profile instance will always have an account property on it because that's what you've defined as part of your class.
It's unclear from your question why you're calling stringify in the first place, but assuming it's to use in some other context, like logging, then you'll want to expose your own method for serializing the instance that limits which properties are returned. This can be done easily using something like lodash's pick or omit.
serialize () {
return _.omit(this, ['account'])
}

Schema Generator cannot determine GraphQL output type for custom object within DTO

Coming from a laravel background I try to get into node.js/graphQL/MongoDB and stumbled over nest.js framework which looks pretty nice. So I tried to set up a simple GraphQL API for testing and understanding how it all works. Therefore I created a mongoose Schema for a user as well as a model (type-graphql) and a DTO for creating such a user via mutation.
This works pretty fine but then I wanted to add a nested object called settings within the user to try how this would work and I simply don't get it and also don't find any nest.js or type-graphql specific examples for such an implementation. Is this simply not feasible using the "code first" approach of nest.js? Because the schema generator always gives me an error while compilation saying :
"UnhandledPromiseRejectionWarning: Error: Cannot determine GraphQL output type for settings".
user.model.ts
import { Field, ID, ObjectType } from 'type-graphql'
import UserSettingsType from './user.settings.type'
#ObjectType()
export class User {
#Field((type) => ID)
id: string
#Field()
email: string
#Field((type) => UserSettingsType)
settings: {}
}
user.settings.type.ts
import { Field, ObjectType } from 'type-graphql'
#ObjectType()
export class UserSettings {
#Field()
theme: string
#Field()
description?: string
#Field((type) => [String])
visited: string[]
}
new-user.input.ts
import { IsOptional, IsEmail, IsBoolean, Length, MaxLength, IsArray } from 'class-validator'
import { Field, InputType } from 'type-graphql'
import UserSettingsType from '../graphql/user.settings.type'
#InputType()
export class NewUserInput {
#Field()
#IsEmail()
email: string
#Field((type) => UserSettingsType)
#IsOptional()
#IsArray()
settings: {}
}
As you can see I defined a new type UserSettings in a separate file (following the best practices of nest.js) and allocated it as a type within the User model as well as the new-user DTO class. The error is thrown for the DTO class (NOT for the model) and if I change it there to something like [String] instead of UserSettingsType it is compiling.
I'm not sure if you need the resolver or the service/mongoose logic for this actual problem if so I can also post that of course!
Had similar issue migrating from express to nestjs.
After reading the docs realised that problem isn't the nested objects, but wrong imports.
You should use #nestjs/graphql imports instead of type-graphql.
E.G.
// changes this
import { Field, ID, ObjectType } from 'type-graphql'
// to
import { Field, ID, ObjectType } from '#nestjs/graphql'
Same applies to any other import from type-graphql

How can I get mapped type names for a graphQL type using type-graphql

Okay, so I'm starting to dig into graphql a little bit, and I've built an api using koa, type-graphql, and sequelize-typescript. Everything works pretty well.... I managed to get a query working, and even managed to optimize a little bit by using graphql-fields to filter the columns I query in the database... However when I've aliased a field name, I can't seem to get the mapped name.....
For example, given the following ObjectType/Sequelize Model....
export interface IDepartment {
departmentId: number;
name: string;
description: string;
}
#ObjectType()
#Table({ underscored: true })
export class Department extends Model<Department> implements IDepartment {
#Field({ name: 'id' })
#PrimaryKey
#Column({ field: 'department_id'})
public departmentId: number;
#Field()
#Length({ max: 100 })
#Column
name: string;
#Field()
#Length({ max: 100 })
#AllowNull
#Column
description: string;
}
and sample query....
query {
department(name: "Test Dept") {
id
name,
description
}
}
sample resolver...
async department(#Arg('name') name: string, #Info() info: GraphQLResolveInfo) {
return Department.findOne({
where: { name }
});
}
This works just fine.... but when I do
async department(#Arg('name') name: string, #Info() info: GraphQLResolveInfo) {
let fields = Object.keys(getFields(info))
return Department.findOne({
attributes: fields,
where: { name }
});
}
(getFields is graphql-fields), it fails because the query specified field name id, which is what graphql-fields returns, but the column name is department_id (sequelize model name departmentId).
I've gone through the schema with a fine tooth comb, using the introspectionFromSchema function to see a detailed copy of my schema, but nowhere is there a mention of departmentId or department_id.... However I know it's out there somewhere because when I exclude the attributes field from my sequelize query, even though sequelize returns departmentId as the property name, when I return it from my resolver and it reaches the client, the property name is id.
Any help would be appreciated.... I'm trying to optimize everything by only fetching requested properties and not the entire object. I could always store the maps as separate constants and use those in my #Field definition, but I want to do that as a last resort, however if I can I'm trying to keep the code as lean as possible....
Thank you all in advance.
Unfortunately, the name option was introduced mostly to support resolvers inheritance. Using this for mapping the schema field names is a kinda undocumented feature so it's doesn't provide any mapping or exposing mapping metadata.
Using the name option for input or args types will be even worse - it will result in no access to the fields and the properties being undefined.
For now my recommendation is to just keep it simple and don't map the field names until a proper fix arrives.

Apply a typescript class to a member of a mongoose document

I have a mongoose schema in typescript and I am already using an interface for properties of the document itself. I want to create a class so when I find a document I can immediately call methods. I also want the document to contain members that are instances of other classes and call the methods those members. Is this possible? Here is what I have so far:
models/user.ts:
import mongoose from '../lib/mongoose';
const Schema = mongoose.Schema;
interface IUser extends mongoose.Document {
username: string;
email: string;
hash: string;
};
var userSchema = new Schema({
username: String,
email: String,
hash: String
});
export default mongoose.model<IUser>('User', userSchema);
I don't actually need to do this for my user model but it is the shortest so I thought it would be a good example. Let's say I wanted a method to check if the user's email contains their username.
Here is a simple user class:
class User {
constructor(public username: string, public email: string, public hash: string) {
}
emailUsernameCheck(): boolean {
return this.email.includes(this.username);
}
}
I want to be able to make a call like this:
import User from '../models/user';
User.findOne({username: 'foo'}).emailUsernameCheck();
Let's also say I wanted to give each user a square with a height and width, and the square class has an area method.
Here is a sample square class:
class square {
constructor(public height: number, public width: number) {
}
area(): number {
return this.height * this.width;
}
}
I also want to be able to make a call like this:
import User from '../models/user';
User.findOne({username: 'foo'}).square.area();
I know I can make the classes separately, give them constructors that take objects, and construct the classes that way. I will do this if it is the only way but it means importing the class separately and adds an extra step to every database lookup. It also opens up room for error since the classes are maintained separately from the schema. I would love to handle this all in my model. Is there something I can add to models/user.ts so I can do things like the examples above?
I think is not possible to use a separate Class in mongoose.
You can extend the Document Model schema using instance and statics methods
Or define a Custom Schema Type for your fields using separate classes (you can find a full example in Mongoose advanced custom schema object type).
Btw you can copy methods from an external object and prototype to Schema.* and Schema.methods.*, but the final object won't share the same prototype as the source class (instanceof).

Resources