Skip to content

This issue was moved to a discussion.

You can continue the conversation there. Go to discussion →

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

Generate type map for each model #18273

Closed
dever23b opened this issue Mar 10, 2023 · 0 comments
Closed

Generate type map for each model #18273

dever23b opened this issue Mar 10, 2023 · 0 comments

Comments

@dever23b
Copy link

Problem

Attempting to migrate to Prisma and having a tough time wrapping any features generically due to the (apparent?) lack of umbrella exposing the applicable types for a given model. Not sure if I'm just doing this wrong, or if this is a legitimate request you may hopefully consider.

My ultimate goal is to be able to use generics to abstract some of the redundant code away from lifecycle operations in my project. For some of my models, I want to have an audit log of changes. When records are created/updated/deleted, I log these events and record the changes in a separate table.

In my first attempt to accomplish this with Prisma, I read into the Client Extensions preview feature and wrote several extensions. Unfortunately, this route didn't seem to be a good fit for my overall objective. After reading through #15074 and the comments thereof, I found myself in agreement with the opposition comments to the feature. I think what you do with Prisma is simple, focused, and effective and perhaps the CEs would be better implemented in other was in our own projects. Accordingly, I set out to write a new layer of code to sit between the rest of my project and Prisma. In addition to my auditing objective, I also have a myriad of computed fields and such on various models, so I opted to write a class for each model, to serve my goals.

Unfortunately, I'm having a really tough time being able to abstract anything from my classes due to the typings available in Prisma. If I write everything in one class for a given model, it works great; however, this means I have to copy/paste quite a bit of redundant code between several other models, which is a recipe for failure as the code evolves. What I would really like to be able to do is have an abstract, generic base class that can take a few type parameters and retain a lot of the reusable code and then extend this class with each model. I can't find any way to do anything effective with the abstract class, though, because there doesn't seem to be any generic way to refer to the various model types generated by prisma generate: they're all independent of each other, with no base extension or type map. The Prisma.TypeMap has been hugely helpful in many regards, but it doesn't help with exposing models' associated types. For example, I can't find a way to dynamically access a given model's GetPayload or Input types. I've been able to type the appropriate Prisma model from a generic using the code below, but the signature required is very redundant and I still haven't been able to dynamically access the other necessary types in order to type the generic methods I want to write.

import { Prisma, PrismaClient } from "@prisma/client";
export abstract class Model<Client extends Prisma.TypeMap['meta']['modelProps'], Payload extends { id: number }> {
  protected abstract _modelName: Prisma.ModelName;
  protected abstract _client: PrismaClient[Client];
  protected _values: Payload;
  
  protected constructor(data: Payload) {
    this._values = data;
  }


  public static async create(data: HowToTypeThis) {
    // this seems to be a dead end
  }
}

I've been brainstorming for a few days, so perhaps I am going about this all wrong and would certainly appreciate an alternative suggestion if I'm just lost in the woods and not finding trees.

Suggested solution

What would be helpful is a map similar to Prisma.TypeMap that, rather than aggregating types by operations, aggregated the various Input, Args, Output, etc types under a key for each model. Also, an association between the camelCase and PascalCase namings for each type, so that we can dynamically retrieve the appropriate Prisma Client and its types.

Additional context

Here's some rough pseudocode I put together as an example of what I think should be a lot more easily possible.

// lib/Model.class.ts
import { Prisma, PrismaClient } from '@prisma/client'
export abstract interface Model<Name extends Prisma.ModelName, Payload extends Prisma.Models[Name]['payloads']['allIncludes']> {
  onCreate?: () => void | Promise<void>;
  onRead?: () => void | Promise<void>;
  onUpdate?: () => void | Promise<void>;
  onDelete?: () => void | Promise<void>;
}

export abstract class Model<Name extends Prisma.ModelName, Payload extends Prisma.Models[Name]['payloads']['allIncludes']> {
  protected abstract _client: PrismaClient[Prisma.Models[Name]['client']];
  protected _data: {
    stored: Payload,
    current: Payload,
  }
  protected _dirty: string[] = [];

  protected constructor(data: Payload) {
    this._data.current = data;
    this._data.stored = data;

    for( let key in data ) {
      Object.defineProperty(this, key, {
        get: () => this._data.current[key],
        set: (value: unknown) => {
          if( this._data.stored[key] !== value ) {
            this._dirty.push(key);
          }
          if( this._data.current[key] !== value ) {
            this._data.current[key] = value;
          }
        }
      })
    }
  }

  protected _recalculateDirty() {
    const dirty: string[] = [];
    for( const key in Object.keys(this._data.stored) ) {
      const oldValue = this._data.stored[key as keyof Payload];
      const newValue = this._data.current[key as keyof Payload];
      if( oldValue !== newValue) {
        dirty.push(key);
      }
    }
    this._dirty = dirty;
  }

  public reload(data?: Payload) {
    if( data === undefined ) {
      data = this._client.findFirst({ where: { id: this._data.current.id }});
    }
    this._data.stored = data;
    this._recalculateDirty();
    return data;
  }

  public reset() {
    this._data.current = this._data.stored;
    this._dirty = [];
  }

  public save() {
    if( this.onUpdate ) this.onUpdate();

    return this._client
      .update({
        data: this._data.current,
        where: { id: this._data.current.id },
      })
      .then((record) => this.reload(record))
  }

  public delete() {
    if( this.onDelete ) this.onDelete();
    return this._client.delete({ where: { id: this._data.current.id }})
  }
}
// lib/ModelWithAuditing.class.ts
import { Prisma, PrismaClient } from '@prisma/client'
import { Model } from './Model.class';
export abstract class ModelWithAudit<Name extends Prisma.ModelName, Payload extends Prisma.Models[Name]['payloads']['allIncludes']> extends Model<Name, Payload> {
  async onCreate() {
    // make entry into audit log
  }
  async onUpdate() {
    // make entry into audit log
  }
  async onDelete() {
    // make entry into audit log
  }
}
// models/User.class.ts
import { ModelWithAudit } from '../lib/ModelWithAudit.class';
class User extends ModelWithAudit<'user'> {
  onDelete() {
    if( logicThatFindsAnyFlags ) {
      throw new UserCannotBeDeletedException();
    }
    super.onDelete()
  }

  get fullName() { return `${this.firstName} ${this.lastName}`}
}
@prisma prisma locked and limited conversation to collaborators Mar 15, 2023
@janpio janpio converted this issue into discussion #18322 Mar 15, 2023

This issue was moved to a discussion.

You can continue the conversation there. Go to discussion →

Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant