FaunaDB types using TS - FQL vs GQL

What are good practices when using TS with Fauna + GQL ?

Because I have a User type that represents a user queried using FQL, and another User type that represents the same user queried using GQL, and the schema might be slightly different ( id vs _id , ref vs no ref, etc.)

Eventually, they represent the same entity, but the shapes is different. I’m tempted to use the same TS Type for both but I wonder about pros/cons of this.

Here are the basics when using FQL:

export type FaunadbBaseFields = {
  ref?: any;
  ts?: number;
}
import { GenericObject } from '../GenericObject';
import { FaunadbBaseFields } from './FaunadbBaseFields';

export type FaunadbRecordBaseFields<Data extends GenericObject> = FaunadbBaseFields & {
  data: Data;
}

import { FaunadbRecordBaseFields } from './FaunadbRecordBaseFields';

/**
 * User type in FaunaDB.
 */
export type User = FaunadbRecordBaseFields<{
  email: string;
}>

When using GQL, the User wouldn’t have a ref, but id instead.

I’m not sure about how to design those variations in a simple way, although generic and reusable.

Another thing that is quite difficult to describe are relationships.

export type Canvas = FaunadbRecordBaseFields<{
  owner: Expr;
}>

The owner might be different types. It would be a FaunaDB Expr when resolved through FQL, but it would rather be a User object when resolved through GQL.

Those kinds of differences make me believe I should use different types depending on whether the data is resolved using FQL or GQL.

Are both GraphQL and FQl queries being run on the client, or are the FQL ones going through an API? I am more curious than anything.

Others should speak up, because they may have other stacks and use cases in mind. I’ve not ever actually had to deal with dealing with both GraphQL and FQL at the same time. Or rather, I’ve not had to deal with the same types coming in from both.

But I think my advice, for this problem specifically, is to

  1. Enforce the same types for… the same types.
  2. Normalize everything when it comes in.

However the direct FQL responses are coming in, transform it into the GraphQL type before leaving that FQL “space”, where you can deal with more loosy goosy types, and pass it into the rest of the GraphQL and client space where you have the strict types. I am not confident how straight forward the types will be here, but a thin wrapper around the faunadb-js driver to transform the data before applying concrete types should be possible.

If you look at something like Apollo, the cache takes in all of the queries and normalizes the responses. That is, if you have a deep query, it still stores each item by it’s ID in it’s own local collection, and resolves the relationships when you ask for them again in the client. Don’t actually need Apollo for this, but it is nice out of the box.

Forgive me, please, this is what I know! I know that not everyone uses Apollo – I spent a whole bunch of time talking elsewhere in the forum about Apollo and the OP was using Elm! :upside_down_face: I think there are some lessons here, however.

One way to normalize the data could be to convert all relationship fields to objects with nothing but _id and __typename fields. This transformation could happen to both incoming GraphQL results and straight FQL results.

type RelationStub<T extends string> = {
  __typename: T
  _id: string
}

type Canvas = FaunadbRecordBaseFields<{
  owner: RelationStub<'User'>;
}>

const cavas =  {
  owner: {
    __typename: 'User'
    _id: '123456789'
  }
}

Don’t forget the __typename field in your queries! It doesn’t show up in docs, but it is available to query. Apollo Client, as an example, injects __typename into every level of your query to make the cache work.

example of __typename in a query:

But… lol then I wonder, why are the same types coming in from both FQL and GraphQL? :smile:

Honestly, though, if you are using a GraphQL client that has any catch built in, then I expect getting data from outside of that system is going to wreck it, or it will be tedious and error prone to keep things in sync. If you’re building things from scratch anyway then maybe not so bad.

So, these comments do not answer your question, but try to avoid encountering the problem altogether.

Leverage UDFs and Custom Resolvers

You can do awesome things with UDFs and customer resolvers. If you have a straight FQL query you are making, there IS a way to add that to your GraphQL schema. And often, you can return types in such a way that you can let the normal GraphQL api take over.

For example, some arbitrary function that compares some things, creates some things, links them in a certain way, and then returns a result of “[OrgMemberLink]”. You could then use the GraphQL api like normal to expand the fields for the returned items.

Maybe you can manage to do this only for those queries that return types in common with GraphQL? :man_shrugging:

This is really my recommendation. The two following are kinda options. But this is the real one. But you may certainly have a use case that demands you use both FQL and GraphQL separately.

Make your other APIs work with your GraphQL client.

Apollo, for example, could access the FQL through a REST api you create and often inject that into existing GraphQL client.

Like I noted before, I hesitate any more to highlight Apollo stuff, since there’s plenty of other stacks to work with. But this is there.

Other clients I am pretty sure let you add client only resolvers that can fetch and format data, building it into the existing client and cache.

GraphQL Schema Stitching

You can also create a separate GraphQL server that “delegates” the fauna one, and “stitch” the two on the client.

I’m not actually a fan of this one any more. I used “schema stitching” to add cookies to login requests, and clear cookies on logout. It would also be easy enough to provide serverless functions for those two requests and just manage the login state completely separate from all other queries. Note that there is no type overlap here.

Apollo effectively deprecated schema stitching, in favor of Apollo Federation (:wave: oooOOOooo :wave:) lol. Anyway, Fauna GraphQL api is not compatible with Federation, so it’s not an option yet. Somewhere, I could swear Fauna mentioned considering it…

Both are run on the client, both are run on the server. GQL has limitations, FQL is more complicated to visualise, so it’s a mix of both depending on the nature/complexity of what I’m trying to do.

However the direct FQL responses are coming in, transform it into the GraphQL type before leaving that FQL “space”, where you can deal with more loosy goosy types, and pass it into the rest of the GraphQL and client space where you have the strict types. I am not confident how straight forward the types will be here, but a thin wrapper around the faunadb-js driver to transform the data before applying concrete types should be possible.

Yeah, there are differences between FQL and GQL results that makes using the same TS type impossible (because of the inconsistencies), unless using a wrapper.

Some inconsistencies are (focus on querying, not mutating):

  • FQL returns ref, and ref.id VS GQL returns _id
  • FQL returns a data wrapper, GQL doesn’t
  • FQL relationships use Expr type (which is an object containing an id field), GQL use an object representing the shape
  • GQL internal properties (_at, _typename) are prefixed by _, while FQL doesn’t (and doesn’t have typename)

Etc.

I’m not using Apollo but SWR instead. I might use Apollo but I’m not sure at this time.

What I did since yesterday, is to completely split FQL and GQL types, because the difference weren’t friendly enough to keep the same type for both. I know it’s not the best solution but it was the simplest at this point.

Another issue I have (which is more TS-related) is how to represent objects that share a similar type (a common base) but don’t contain the same data. I’m quite familiar with TS, but having a User type doesn’t really work out for all scenarios. Sometimes you’re querying only part of the User entity (id, name, email), sometimes you’re querying more (projects), sometimes those relationships are resolved (projectId, projectName) and sometimes they’re not. Using the ? for optional field doesn’t really help understand what’s the actual shape of the object I’m using on a particular part of the app.

Right now, I’ve created different types like “User” and “UserWithProjects”, but it kinda bugs me. It’s simple as long as all queries return the same shape, but as soon as there are a few variations then it becomes much more complicated to scale.