Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Convert the Models from Types to Classes #5998

Open
langal opened this issue Mar 6, 2021 · 11 comments
Open

Convert the Models from Types to Classes #5998

langal opened this issue Mar 6, 2021 · 11 comments
Labels
domain/client Issue in the "Client" domain: Prisma Client, Prisma Studio etc. kind/feature A request for a new feature. tech/typescript Issue for tech TypeScript. topic: client api topic: extend-client Extending the Prisma Client

Comments

@langal
Copy link

langal commented Mar 6, 2021

Problem

It would be great if an application can extend or monkey-patch/proxy additional properties onto the objects that are returned from the CRUD operations.

For example, I want to add some simple logical functions to the "User" model that is generated by Prisma. Each "User" that is returned from a Prisma CRUD operation would automatically have these user/application-defined functions or properties.

As far as I know, this is not possible with TypeScript "types".

If the models were classes, an application could add such functions to the class prototype. The objects returned from Prisma could be then extended to become full business/transfer objects.

Suggested solution

Make Prisma auto-generate classes in lieu of "types". Perhaps some conventions can be made so that Prisma automatically looks for any "class/model" overrides upon client instantiation.

Alternatives

I am writing some code that would automatically load up and wrap the Prisma model types with classes. It basically hijacks the "findFirst/findMany/etc." functions and supplants the return type with a Class object that can have application-specific properties.

However, I think this would be a valuable native Prisma feature.

Additional context

ORMs such as Django or Rails allow application developers to override/patch/extend the default ORM data models. Having Prisma be able to do this would make it easier for application developers to take a more OO approach.

@pantharshit00 pantharshit00 added kind/feature A request for a new feature. tech/typescript Issue for tech TypeScript. topic: client api domain/client Issue in the "Client" domain: Prisma Client, Prisma Studio etc. labels Mar 6, 2021
@langal
Copy link
Author

langal commented Mar 19, 2021

Here are some monkey-patch hacks I've been playing around with

This proxy class would be a parent class. It just copies a Model-Type-instance fields. One would implement addition functions inside here.

// Our proxy class
class PrismaProxyClass {
  // database table/prisma model
  constructor({ ...data }) {
    Object.keys(data).forEach((key) => {
      this[key] = data[key];
    });
  }
}

module.exports.PrismaProxyClass = PrismaProxyClass;

getDb basically return a Prisma client.
CLASS_MAP is just a map of Prisma model names to children of PrismaProxyClass

import { getDb } from '../app/db';
import CLASS_MAP from './classMap';

/*
Basically this just hijacks the Prisma CRUD functions
with proxies that return the specified ProxyClass.
This is to be done when the application boots.
TODO - handle nested objects
*/
export const proxyPatch = (tableName, ProxyClass) => {
  // getDb return Prisma client
  const prismaTableRef = getDb()[tableName];

  // monkey patch Prisma CRUD functions
  ['findUnique', 'findOne', 'findFirst'].forEach((func) => {
    const prismaFunction = prismaTableRef[func];
    prismaTableRef[func] = async (args) => {
      const nativePrismaObject = await prismaFunction.call(this, args);
      if (nativePrismaObject) {
        walkPrismaObject(nativePrismaObject);
        return new ProxyClass(nativePrismaObject);
      }
      return null;
    };
  });

  // some CRUD functions return Arrays
  ['findMany'].forEach((func) => {
    const prismaFunction = prismaTableRef[func];
    prismaTableRef[func] = async (args) => {
      const nativePrismaObject = await prismaFunction.call(this, args);
      return nativePrismaObject && nativePrismaObject.map((obj) => {
        if (obj) {
          walkPrismaObject(obj);
          return new ProxyClass(obj);
        }
        return null;
      });
    };
  });
};
const walkPrismaObject = (data) => {
  if (Array.isArray(data) && data.length && typeof data[0] === 'object') {
    data.forEach((dataElement) => {
      walkPrismaObject(dataElement);
    });
  } else if (data && typeof data === 'object') {
    Object.keys(data).forEach((key) => {
      // we handle object types as if they might have to be converted to classes
      if (typeof data[key] === 'object') {
        walkPrismaObject(data[key]);
        if (CLASS_MAP[key]) {
          if (Array.isArray(data[key])) {
            data[key] = data[key].map((_objElement) => {
              return new CLASS_MAP[key](_objElement);
            });
          } else {
            data[key] = new CLASS_MAP[key](data[key]);
          }
        }
      }
    });
  }
};

@jsw
Copy link

jsw commented May 9, 2021

Is anyone using plainToClass from class-transformer to map prisma results to objects? Any gotchas or patterns for doing this in an automated way?

@langal
Copy link
Author

langal commented May 17, 2021

Is anyone using plainToClass from class-transformer to map prisma results to objects? Any gotchas or patterns for doing this in an automated way?

Have not heard of that package before =)

Looks like it would work fine.

If you have an explicit "data" layer that abstracts Prisma from the business logic, then all the "plainToClass" calls could be made there. If you this is not the case, then you would have to "plainToClass" them where ever the Prisma calls are made, or monkey-patch/override the Prisma functions.

Prisma could potentially auto-generate default Class definitions for each model-type (ala index.d.ts) and utilize "plainToClass" to return object instances instead of JSON (perhaps via some configuration flag).

There would have to be some convention as to where and how overrides/extensions of the generic Prisma Class definitions would be handled. Perhaps the Prisma generated Class definitions would be deposited in the application code somewhere, or developers would know where to look for them.

@timReynolds
Copy link

I've recently came to the same conclusion that it would be much nicer to map the entities to your own classes than the awkward amount of mapping I'm currently performing especially when using include.

I think the ideal API here would be to register/configure your classes with the client, you'd probably need the classes to conform to a specific interface for this to work but it would then use those returned models.

Without looking too hard I wondered if a custom generator might be able to achieve this nicely.

@Feuerhamster
Copy link

Feuerhamster commented Nov 16, 2021

Same thing here. I want to work object orientated and have classes for my datatypes with some extra utility functions.
And I really have to stick to the generated types from the Prisma client and can't use my own classes?

@zr9
Copy link

zr9 commented Jan 15, 2022

It is already possible albeit with slight limitations on relations.
I created a small package to make it easier to define your wrappers on top of prisma
https://github.com/zr9/prisma-model-wrapper

A triavial example

type yourWrapperType = {
  validate(): boolean;
}

class UserWrapper{
  constructor(ret:yourWrapperType, model: string, client: any){
    Object.assign(this, ret);
  }

  validate() {
    return true;  //just for example always valid
  }
}

const wrappers = {
  [Prisma.ModelName.users]: UserWrapper
}

const prismaNew = prismaWrapper(prisma, wrappers);

//... somewhere else
const user = await prismaNew.users.findUnique({where: {id: 1}});
console.log('validation', user?.validate());

@dcsan
Copy link

dcsan commented Feb 20, 2022

I tried wrapper a Player prisma type with a CPlayer class,
but this just keeps the prisma object as a data field.

which is very simple and would maybe work
but now I have to redefine all the prisma methods... no way to inherit.

Anyone have a better solution?

export class CPlayer {
    data?: Player | null

    constructor(data?: Player | null) {
        this.data = data
    }

    // now we lose all typechecking
    static async findOrCreate(query: Player): Promise<CPlayer> {
        let data = await prisma.player.findFirst({ where: query })
        if (!data) {
            data = await prisma.player.create({ data: query })
        }
        return new CPlayer(data)
    }

    // move a player
    async move(px: number, py: number) {
        await prisma.player.update({
            where: { id: this.data!.id },
            data: { px, py },
        })
    }

}

@millsp
Copy link
Member

millsp commented Aug 31, 2022

Hey everyone, I am excited to share that we have a proposal for this. With the Prisma Client Extensions, you will be able to extend your results so that you can add properties and methods to them. That's not all, we want to enable you to extend any layer of your Prisma Client. I'd love to get your feedback on the proposal and find ways to make it better.

@kn327
Copy link

kn327 commented Jan 24, 2023

Unfortunately, the built-in prisma doesn’t really give the flexibility needed to directly convert the types to classes.

I ended up creating some mappers from the type to class which can convert backwards and forwards the classes to models and vise versa

Taking a note from the Java and C# worlds, this is a common practice to remap your classes to or from a database parameter set

I’m not a fan of the proposed auto-map solutions as they don’t really allow for complex type mappings

Take a model such as below:

class OrganizationModel {
    id:string;
    name:string;
}

class UserModel {
    id: string;
    name:string;
    organization:Organization;
}

An auto-mapper (such as Object.assign or another copy method) would create the user object as follows

{
    id: 1,
    name: “John Doe”,
    organizationId: 1
}

At this point, you may as well use the built in types from Prisma. The downside here is that if you eventually want to pull the OrganizationModel details in the future, all consuming services are forced to check two fields, organizationId and organization.id For consuming further fields

My expected output would be

{
    id: 1,
    name: “John Doe”,
    organization: { id: 1 }
}

This format allows for downstream services to access and check the same field everywhere

As this is a backend task, I think it would be beneficial to follow backend principles when accessing data; namely separating concerns in the following layers:

  • API
  • Service
  • DB

Wherein there is a one-way relationship. Users communicate with the API layer, which in turn communicates with the Service layer, which handles negotiations in the DB layer

The service layer, converts the DTO objects to a prisma-compatible query, and returns the class objects as a response via common mappers

Those mappers look something similar to this (as static methods of your Service class, or as exported constants of another file, my preferred approach)

//organization.mapper.ts
export const OrganizationMapper = {
    toPrisma(entity: OrganizationModel): organization {
        return {
            id: entity.id,
            name: entity.name
        };
    },
    fromPrisma(dbo: Partial<organization>): OrganizationModel {
        const entity = new OrganizationModel();
        entity.id = dto.id;
        entity.name = dto.name;
        return entity;
    }
}

// user.mapper.ts
import { OrganizationMapper } from “./organization.mapper.ts”

export const UserMapper = {
    toPrisma(entity: UserModel):user {
        return {
            id: entity.id,
            name: entity.name,
            organizationId: entity.organization?.id
        };
    }
    fromPrisma(dbo: Partial<user> & {
        organization?: organization ; // facilitates the include clause
    }): UserModel {
        const entity = new UserModel();
        entity.id = dbo.id;
        entity.name = dbo.name;
        if (dbo.organization) entity.organization = OrganizationMapper.fromPrisma(dbo.organization);
        else if (dbo.organizationId) entity.organization = OrganizationMapper.fromPrisma({ id: dbo.organizationId });

        return entity;
    }
};

This pattern would allow you to interact with prisma, and still work through your other classes with full support of prisma. The only caution here is that if your relation names change, you will need to ensure the mapper is properly in sync with your schema (ie. If my relation from user to organization is “organizationRelation” instead of “organization”, the mapper would need to have that type specified explicitly to make the map succeed

This also allows you to ensure protected fields such as username or password are explicitly pruned from the return object instead of relying on a downstream service remembering to prune these values

A more automated approach could use the experimental typescript decorators (similar to the TypeORM approach) to perform the map on a more generic scale for you

@janpio
Copy link
Contributor

janpio commented Jun 20, 2023

To go back to the original request to be able to add fields to query results, this is now possible via Client Extensions and the result component type, and adding functions to the models overall via the model component type.

Do I understand correctly though that there is a higher level use case than this specific one to be able to use Classes instead of Types that Prisma provides?

@nolawnchairs
Copy link

nolawnchairs commented Sep 30, 2023

I often use data classes, and have successfully used class-transformer to convert Prisma data objects to class instances, but there are some gotchas:

Prisma's power is in its inference. You just can't get the granular property inclusion when using something concrete like a class, especially when you introduce select and include directives to your find queries.

Your data classes will need to have every possible relation defined that you end up querying. So if you include multiple relations, those must be decorated with @Type(), but as optional since it may not exist. This then requires null checks whenever you need them, even if that relation was explicitly included. In other words, during the process of transforming from object to class, you lose this inferred specificity.

There are also considerations on whether your query is within a function. Returning the query as-is will allow the function caller to get inference. However, if you convert to a class instance within that function, you lose the granularity of Prisma's native query return type in exchange for a class instance with all the uncertainty regarding property inclusion.

For me, I only convert to a class instance for client serialization where I can @Exclude() certain fields, such as PII and internal data not germane to the request being processed. This way, the data can be operated on with type safety, but also allows my serialization middleware to sanitize the output.

I recommend looking at the advanced type safety section of the Prisma docs;
https://www.prisma.io/docs/concepts/components/prisma-client/advanced-type-safety/operating-against-partial-structures-of-model-types

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
domain/client Issue in the "Client" domain: Prisma Client, Prisma Studio etc. kind/feature A request for a new feature. tech/typescript Issue for tech TypeScript. topic: client api topic: extend-client Extending the Prisma Client
Projects
None yet
Development

No branches or pull requests