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

Approach for circular references in prisma objects #641

Closed
giraugh opened this issue Oct 26, 2022 · 6 comments
Closed

Approach for circular references in prisma objects #641

giraugh opened this issue Oct 26, 2022 · 6 comments
Labels
enhancement New feature or request needs-api-design

Comments

@giraugh
Copy link
Contributor

giraugh commented Oct 26, 2022

What is the intended approach for addressing circular references for nested relations in prisma objects?

For example, suppose there is a many-to-many relationship between posts and users. The declaration of the Post prisma object and User prisma object reference each other.

export const Post = builder.prismaObject('Post', {
  fields: (t) => ({
    id: t.exposeID('id'),
    // ...

    authors: t.field({
      type: [User], // <--- problem
      select: (args, context, nestedSelection) => ({
        authors: {
          select: {
            user: nestedSelection()
          }
        }
      }),
      resolve: post => post.authors.map(({ user }) => user)
    })
  })
})

export const User = builder.prismaObject('User', {
  fields: (t) => ({
    id: t.exposeID('id'),
    // ...

    posts: t.field({
      type: [Post], // <--- problem
      select: (args, context, nestedSelection) => ({
        posts: {
          select: {
            post: nestedSelection()
          }
        }
      }),
      resolve: user => user.posts.map(({ user }) => user)
    }),
  })
})

This causes typescript to complain about circular declarations

'User' implicitly has type 'any' because it does not have a type annotation and is referenced directly or indirectly in its own initializer.

The circular references section of the guide resolves this by first declaring the object and then implementing the fields that would cause the circular reference in a call to .implement(). However, it seems that there is not an equivalent to .implement() for builder.prismaObject.

Any help or advice would be greatly appreciated :)

@hayes
Copy link
Owner

hayes commented Oct 26, 2022

This is something that there isn't a good for right now.

I can think of 2 workarounds:

export const User = builder.prismaObject('User', {
  fields: (t) => ({
    id: t.exposeID('id'),
    // ...
  })
})

// Move the field out into it's own field, this isn't optimized the same way, but can often still be very similar in efficiency
builder.objectField(User, 'posts', t => t.prismaField({
    resolve: (query, user) => prisma.post.findMany({ ...query, where: { users: { userId: user.id } } })
}))
// create a manually typed ref to break the circular dependency
const PostRef: ObjectRef<Prisma.Post> = Post

export const User = builder.prismaObject('User', {
  fields: (t) => ({
    id: t.exposeID('id'),
    // ...
    posts: t.field({
      type: [PostRef],
      select: (args, context, nestedSelection) => ({
        posts: {
          select: {
            post: nestedSelection()
          }
        }
      }),
      resolve: user => user.posts.map(({ user }) => user)
    }),
  })
})

@giraugh
Copy link
Contributor Author

giraugh commented Oct 27, 2022

That works well, cheers! Any plans in the works for the prisma plugin to avoid this?

@hayes
Copy link
Owner

hayes commented Oct 28, 2022

There isn't a way to completely avoid this issue, but I will probably be adding a simpler workaround.

I'll have to do some investigation, but it would likely be something like adding a builder.prismaObjectField(s) helper, or or a new method on the prisma ref itself like: PrismaObjectRef.field(s)(t => ....). The other option would be a builder.prismaObjectRef method that would let you split defining the ref and implementing it into separate steps. This may be the easiest option.

The underlying issue of typescript reverting to any as soon as you have the circular reference is unfortunately not something that can be worked around right now, so the best option is to provide ways to define those circular references in a separate statement.

@hayes hayes added enhancement New feature or request needs-api-design labels Oct 28, 2022
@outerlook
Copy link

outerlook commented Oct 31, 2022

Edit:
wait, sorry, I provided an answer to a different problem =(

Details

I did solve this with a complex workaround using RxJS until something simpler shows up...

Just sharing if this helps anyone 👀

// getProducer.ts
import { Observable, shareReplay, take } from "rxjs";

/**
 * an observable that must happen in sync, that will be shared between everyone that uses a common key.
 * once emmited, it will be shared between everyone that uses that key.
 * if didn't emit, it will wait for the same event to happen, and then share it.
 */
export const getProducer = <Fn extends (args: any) => any | Observable<any>, Args extends Parameters<Fn>[0]>(
  fn: Fn,
  getKeyFn: (args: Args) => any
) => {
  const producersMap = new Map<string, Observable<any>>();

  return (args: Args) => {
    const key = getKeyFn(args);
    const prevObservable = producersMap.get(key);
    if (prevObservable) {
      return prevObservable;
    }

    const observable = new Observable<any>((subscriber) => {
      const result = fn(args);
      if (result instanceof Observable) {
        result.subscribe(subscriber);
      } else {
        subscriber.next(result);
      }
    }).pipe(shareReplay(1), take(1));

    producersMap.set(key, observable);

    return observable;
  };
};

then everything I want to be shared by a common key (the operation name, for example), thus not provoking any circular dependency or other problem, I use this getProducer wrap on it.

for example:

import type { MainBuilder } from "../setupBuilder/getBuilder";
import { prismaWhereForObjectFromArgs } from "./whereForPrismaObject";
import { map } from "rxjs";
import {getProducer} from "../getProducer";

type BuildListRelationFilterArg = {
  builder: MainBuilder;
  modelName: string;
};

export const listRelationFilterFromArgs = getProducer(({ builder, modelName }: BuildListRelationFilterArg) => {
  const name = modelName + "ListRelationFilter";

  const where$ = prismaWhereForObjectFromArgs({
    builder: builder,
    modelName: modelName
  });
  return where$.pipe(map(WhereForObject => builder.prismaListFilter(WhereForObject, {
    ops: ["every", "some", "none"] as const,
    name: name
  })))
}, ({ modelName }) => modelName + "ListRelationFilter");

and to make this observable sync (nothing async running inside all these)

import type { Observable } from "rxjs";


/**
 * if a observable can be produced in sync mode, it will. Otherwise it will fail.
 * @param observable
 */
export const syncObservable = <T>(observable: Observable<T>): T => {
    let emitted = false;
    let value: T = undefined;
    const subscription = observable.subscribe((v) => {
        emitted = true;
        value = v;
    });
    subscription.unsubscribe();

    if (!emitted) {
        throw new Error("observable did not emit");
    }
    return value;
};

not proud of it, but it is helping me with a relatively large schema

@hayes
Copy link
Owner

hayes commented Nov 14, 2022

I am working on some improvements for the prisma plugin in #671, I'll try to add a way of creating prismaRefs in that PR before it gets merged

@hayes
Copy link
Owner

hayes commented Nov 21, 2022

Add builder.prismaObjectField(s) methods that can be used to definition je circular references without type issues.

@hayes hayes closed this as completed Nov 21, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request needs-api-design
Projects
None yet
Development

No branches or pull requests

3 participants