#[Request] Create a type-safe Prisma query as a function

58 messages ยท Page 1 of 1 (latest)

red fox
#

If you worked with Prisma, keep on reading, please

I know it may sound mean to somebody that I request it, but I am definitely beyond just asking for help..., and managing type inference is not easy at all. Generally, querying with Prisma looks like the following

const users = await db.user.findMany({
  where: { name: 'dnartsbeW' }
})

but I need this particular approach

const users = await query('user', 'findMany', {
  where: { name: 'dnartsbeW' }
})

You may have already guessed why, but if not, I'll tell you anyways. query is a server action/function that cannot get callbacks from the client, because they are not serializable, unlike strings and objects. query would look like the following from the inside

// actions.ts
'use server'

import { db } from '#/prisma'
import { Prisma } from '@prisma/client'

// Mind the types, I have no idea what they should be
export async function query(model: Lowercase<Prisma.ModelName>, operation: ..., filter: ...) {
  const res = await db[model][func](filter)
  return res
}  

This function works alright however types are broken as I could not figure out what type of operation and filter should be. Also, there should be some use of generics to infer the return type.

With the above code, model is inferred, operation, filter and return type are not inferred.

If you would like to help, website of Prisma has AI assistant, it works 50/50 on the time, but is aware of the internal types like no else AI

red fox
#

here is solution generated by Prisma AI assistant

'use server'

import { db } from '#/prisma'
import { Prisma, PrismaClient } from '@prisma/client'
import { Operation } from '@prisma/client/runtime/library'

export async function query<
  TModel extends Lowercase<Prisma.ModelName>,
  TOperation extends Operation,
  TArgs extends Parameters<PrismaClient[TModel][TOperation]>[0],
>(model: TModel, operation: TOperation, args: TArgs): Promise<ReturnType<PrismaClient[TModel][TOperation]>> {
  const res = await db[model][operation](args)
  return res
}

const users = await query('user', 'findFirst', {
  where: {
    name: 'Billy'
  },
  include: {
    createdCourses: true
  }
})

It almost works. The return type is not correct and the type declaration above is incorrect for 3 arguments, even though the inference works fine while actually calling the function (users)

sinful pendant
#

You could probably make an object wrapped in a Proxy, with the same type as the prisma client

#

You'd have to use the proxy handler to capture the various data and pass it to the server, but the types would be correct by just casting it as typeof prismaClient

#

It's how libs like tRPC do remote procedure calls

red fox
#

@sinful pendant thank you I will read it

sinful pendant
#
import type { PrismaClient } from '../server/prisma.js' // import the typeof prisma from server side code

export const browserPrisma = new Proxy({}, {
  get(target: object, prop: string) {
    const model = prop // if prisma.user is used, prop/model  will be 'user'
    return new Proxy({}, {
      get(target: object, prop: string) {
        const action = prop // if prisma.user.findMany is used, prop/action will be 'findMany'
        return function (args: object) {
          // when this function is called we have
          // model: e.g. 'user'
          // action: e.g. 'findMany'
          // args: e.g. { where: { name: 'Bob } }
          // we now need to pass this info to the server, and get the response.
        }
    }
  }
} as PrismaClient  // cast it to the type of prisma, whic
#

Something like this

#

Once you have that info you could call your query action. And just don't worry about the types internally, just use unknown or something. When you use browserPrisma it will have the correct types.

last pumice
#

Pretty sure you don't need a proxy.

#

Hmm, it will depend on how Prisma implements their operations. My initial thought is similar to the answer given by Prisma AI assistant, that answer is almost right except they didn't handle arguments properly with spread.

#

But that answer is problematic if Prisma implements their operations via generics.

sinful pendant
#

I think if you want a prisma like object on the client, you need a proxy. And you don't really have to think about types that way.

#

You can do it the other way of course, with a query action, but then you have to deal with types.

last pumice
#

You don't need a proxy, the code works just not the types.

sinful pendant
#

You're not getting it

#

Prisma looks like this

await prisma.user.findMany({
  where: { name: 'dnartsbeW' }
})

The code in use looks like this

const users = await query('user', 'findMany', {
  where: { name: 'dnartsbeW' }
})
#

If you use a proxy to make the client side code look like the top code, you can just use prisma's built in typing

#

If not, then you need to deal with typing it

last pumice
#

Yes, and await db[model][func](filter) already works, it's just the types not working. I'd rather not introduce runtime impact just for types.

sinful pendant
#

I never said you needed a proxy. I said you could use a proxy, and use prisma's types.

last pumice
#

Either way, it'd be much easier if there's a TS playground reproduction, otherwise it's hard to help.

sinful pendant
#

You can get benefit of being able to use the standard prisma API in the client, which is quite nice. Runtime effect is pretty negligible, just a couple of captures in the browser, that's nothing vs network time

sinful pendant
#

Would have to create a schema, generate types, then find those types and load into TS Playground I think

last pumice
#

Nah that's not needed

#

All they need to do is to just copy the type of db and past into TS playground like declare const db: PasteTheTypeHere.

sinful pendant
#

But the type of db will depend on the generated types

#

the prisma client is created with codegen I think

last pumice
#

Sure, copy the associated types too, it's just go to definition a few times.

sinful pendant
#

Sure

#

It's possible

last pumice
#

I mean it's just good to have a TS playground so people can actually attempt to help and see the solution is working or not, otherwise it's just offering ideas with no way of verifying if it would actually work.

sinful pendant
#

although my prisma client index.d.ts in a small sample project is 6.6k lines. So I mean I wouldn't assume it's trivial.

#

Maybe it's no biggy, I dunno ๐Ÿ˜„

last pumice
#

(Although tbh, I would not expose such a query method, it means anyone can look at your network request and find out you have such a method, and query whatever they want from the database, potentially a query with ridiculous complexity that it exhausts your server resources)

sinful pendant
#

Yeah that is a good point

#

I think technically any unprocted server could be DOS attacked, but the bigger issue for me is how you protect sensitive data

#

It might need controls on what is permitted

last pumice
#

GraphQL also has similar problems, someone can construct a ridiculously expensive GraphQL query to DDoS your server, and the typical solutions are either whitelisting allowed queries, or query complexity analysis.

#

If you are going the whitelist route, then it's equivalent to just having specialized methods instead of query, and you wouldn't need to solve this problem at all.

sinful pendant
#

It's a little more flexible.

last pumice
#

Yeah at the cost of allowing people to DDoS your server by constructing dangerous queries ๐Ÿ˜„

#

One workflow of GraphQL is that during development, the GraphQL server has no limitation and frontend can do whatever it wants for fast prototyping. Once frontend is done, use a tool to extract all the queries used and send them to the backend team. Backend then whitelist these queries in production, and additionally optimize these queries. This way you get the best of both worlds: flexibility during development, safety during production.

sinful pendant
last pumice
#

Ultimately if you need to expose such a query method, you have to solve the same problems GraphQL faces one way or another.

sinful pendant
#

Yeah absolutely

#

Part of me thinks "Maybe they should just use GraphQL?" then another part of me thinks "Is this just as good as/better than GraphQL (with the same problems to overcome)?"

#

You could do the same query whitelisting, store the acceptable queries as objects and do a deep equality check

#

Well, there would need to be wildcard elements

last pumice
#

That's the thing

#

If you are doing query whitelisting, then you don't need such a query method at all.

sinful pendant
#

I think there may be some serialization issues to solve with an approach like this

last pumice
#

If you are doing "okay inside query, it checks the arguments and only allow the combination of user + findMany + where: name = ?" then you might as well just write:

export function fn(name: string) {
    return db.user.findMany({ where: { name } })
}

That's query whitelisting.

sinful pendant
#

If you're sending a Date it may need to be converted to string and back in the abstraction layer

sinful pendant
#

Although you made a point about GraphQL being open during development, so if you followed that idea, this could be useful.