mongo-graphql-starter icon indicating copy to clipboard operation
mongo-graphql-starter copied to clipboard

Impossible to define a type with 'partially dynamic' fields?

Open MrDoomBringer opened this issue 4 years ago • 6 comments

I had a question regarding a case with typescript integration for a project I'm working on.

Our DB entries ('Reports') all contain an Object field ('Characteristics') that in turn has 3 elements of a known type (Date created, date updated, ID), and some unknown number of other elements of varying type. For example, one 'Characteristic' object might have 4 fields, and another might have 20, but both will always have those 3 elements of a known type.

As far as I can tell, there is no way to define a type for those 3 known fields, while at the same time allowing for more fields of an unknown type to be defined for that same object.

image Would doing this involve creating a custom schema, interface or resolver? Using the ObjectOf() function? I'm a bit stumped as to where to go from here.

It seems like the only thing I can do is assign the entire 'Characteristic' field to a JSONType and naively deal with that data, but I'd really like to have as much of this typed as possible.

I hope I'm not missing some simple way to achieve this effect. Any insights would be appreciated.

MrDoomBringer avatar Aug 04 '20 18:08 MrDoomBringer

This is tricky. Honestly I’m not sure how you could do that with GraphQL at all, let alone with this project.

As you note, you could make the whole thing a json field, but then of course you lose typing on the three or so fields common to all instances of that column.

Best I can suggest would be to split the mongo field in two: have a column dedicated to the common properties, and then have a separate json column for everything else. But that may not be feasible

arackaf avatar Aug 05 '20 04:08 arackaf

Hi, Adam! This is for the app I've been working on :)

I've decided we'll handle this by leaving the DB side with a JSONObject type for this particular field so that we always get the entire set of sub-fields returned for each entry, but then do some specific TS subtype declarations from there, along the lines of:

interface Report {
  date: string;
  characteristics: any;
} // generic type from GraphQL

interface BaseReport extends Report {
  characteristics: {
    id: string;
  }
}

interface SpecificReportA extends BaseReport {
  characteristics: BaseReport["characteristics"] & {
    specificField: number;
  }
}

In our client code, we should always be able to know what actual Report type should be depending on where we are in the code, so we'll do whatever manual casting is necessary.

markerikson avatar Aug 06 '20 13:08 markerikson

@markerikson - nice! Super cool to hear someone I know is actually using this lib :D

arackaf avatar Aug 06 '20 13:08 arackaf

Sure :) Fwiw, this intern-powered research project appears to have worked out great. We've been able to set up our server-side Mongo typedefs, generate all the resolvers and TS types using your toolset, and use those in the client.

Even better: our existing Express app (remember this is a classic MEAN.js / AngularJS 1.x stack) has a large hand-written REST API, with a custom "include these fields / filter those values" query string syntax that frankly is ugly and hard to read:

GET /api/items?some.field=value&include[some][nested][field]&include[other][nested][field], etc

Since those query strings can be arbitrarily complex, the backend logic tries to auto-magically generate Mongo aggregations that are also arbitrarily complex, and frankly does so in a way that seems pretty inefficient when combined with our potential data structures. (We were seeing a lot of queries taking around 25s to complete!)

I wrote a custom agg pipeline that is typically returning a paginated set of items in around 3s or so, mostly using $lookup with a couple nested stages for the relations.

My long-term migration plan is setting up a new Next.js app, building new features there, and migrating functionality over to it long-term. We're building our first feature in there now, and I figured that made a good starting point for investigating GraphQL. So, we set up a GraphQL endpoint in Next with a playground test page to try out queries.

One of my main questions here was how well this toolset would perform if we ask for several layers of related items coming back, and it looks like this is returning all the items in around 3-4s :) So, whatever you've got here appears to be performing better than the REST logic we've got in the main app already.

I'll have to look through some of the generated code at some point to figure out how you're doing the actual query logic.

(And yeah, while I might have eventually stumbled across this on my own just while doing research, I really only knew about it because you'd tweeted about it repeatedly :) )

markerikson avatar Aug 06 '20 14:08 markerikson

@markerikson fyi - there's two different mechanisms for looking up nested relationships: using a $lookup, or having that nested object's resolver fire off its own query using dataloader. There are times when the former is required (ie if you're paging it) but other than that, the latter (resolver query with dataloader) is the default, since my preliminary testing showed it to usually perform better. But this can be overridden. See the implementation section here:

https://github.com/arackaf/mongo-graphql-starter#using-relationships

arackaf avatar Aug 06 '20 14:08 arackaf

@arackaf Sounds good. Thanks a bunch for the quick reply, by the way! Definitely appreciated the insight.

MrDoomBringer avatar Aug 06 '20 19:08 MrDoomBringer