Announcing - easy-dgraph: Create DGraph GraphQL on the Fly!

I am going to keep this article and how to on dev.to, since discourse will lock this post after 60 days and I don’t want to update it in multiple places anyway. All updates will be on that link.


For those of you just coming here, this package allows you to easily create any GraphQL, specific to Dgraph, from JSON. See the article above, or jump below to see a quick example…

Here are updated examples.

J

3 Likes

so in essence this is just a json → graphql generator? Quite a few of those as of late. I had to write my own here recently and used the _ prefix as well. Curious if you are supporting aliases and if so how. Supporting aliases would be a must if you are also going to support multiple queries/mutations because to use the same query twice in an operation at least one of them must be aliased to another name.

In essence, yes, although I have automated a few things. My inspiration was to get away from graphql all together in the interface (like firebase), but I haven’t found a way for it to make sense. I cannot do this for instance:

dgraph('task').filter({ id: ['45', '65']}).query({ name: 1, description: 2 })

The problem here, is how would I do things like filter sub types. If I add another filter, how do you make it clear which sub type it goes with etc. I am trying to make something two dimensional into one dimensional, so I stuck with all JSON. :frowning: Let me know if anyone has ideas on this…

Please send me links and names of other repositories worth mentioning you know! Would love to see it.

I will have to add it then! I would probably do something like this assuming it is viable:

mutation {
  name1: addPost(input: [$post]) {
  ...
  }
  name2: addPost(input: [$post]) {
  ...
  }
}

I just found this repository, which I like a lot.

It made me realize I can greatly simply the code by putting things like arguments and directives inside the block instead of outside it. I am going to work on those changes. In the meantime, let me know if you have any other suggestions. Going to be brainstorming and making a few of those changes…

J

@fenos started GitHub - fenos/dqlx: A DGraph Query Builder but that in hindsight was for DQL.

I think the object oriented programming makes it easy to do with GraphQL being object syntax similar. I mean AST is an object representation of GraphQL.

The main point of writing a query in an object for me was the ability to modify parts of it after it was created but before generating it into a query. I used this to inject filters and field selection as a user specifies the needs in the application

UPDATE:

No, I actually used a double underscore __ which gave me the syntax:

{
  block1: {
    __query: "queryFoo",
    __fields: {
      id: null,
      bar: {
        __fields: {
          id: null
        }
      }
    },
    __filter: {
      id: ["\"0x30dbc\""]
    },
    __directives: ["cascade(fields: [\"bar\"])"]
  }
}

generates:

query {
  block1: queryFoo(filter: { id: ["0x30dbc"] }) @cascade(fields: ["bar"]){
    id
    bar {
      id
    }
  }
}

I did not need to support ordering or pagination in my use case so I didn’t even worry about the logic for that but could easily be done using __order and __first / __offset or even just __pagination however you wanted to do it. I also cheated and just stringified the directives in an array, but they could also be an object with properties very easily.

I alias every block using the key of the object which matches the object syntax structure of only allowing unique names. In my use case I don’t need to alias field names, but it could be support easily like:

{
  block1: {
    __query: "queryFoo",
    __fields: {
      aliasedID: {
        __aliased: "id"
      },
      aliasedBar: {
        __aliased: "bar",
        __fields: {
          id: null
        }
      }
    }
  }
}

generating:

query {
  block1: queryFoo {
    aliasedID: id
    aliasedBar: bar {
      id
    }
  }
}

Using a double underscore for my meta fields ensured that I never conflicted with a users fields because a double underscore prefix is only specifically allowed in the query structure in certain places, so if for instance a user wanted the __typename field they could still request that because our meta did not interfere with that reserved word.

Nice. Gives me some ideas. Having to re-write a lot for multiple queries. Also getting rid of _select all together. I should have something in the next few days before 21.03 is on the cloud :crazy_face:

J

That other github project you referenced above may be a good start to a refactor and build on it if possible to add what is missing and make some things easier such as:

import { qb as queryBuilder } from <your-package>

const query = qb()
/* yields:
 * query = { __query: <function>, __get: <function>, __aggregate: <function> }
 */

const queryFoo = query.__query('Foo')
/* yields: 
 * query = {
 *   queryFoo: { 
 *     __setFields: <function>,
 *     __setArgs: <function>,
 *     __setDirectives: <function>,
 *     __setAlias: <function>
 *   },
 *   __query: <function>, __get: <function>, __aggregate: <function>
 * }
 * queryFoo = query.queryFoo
 */

const { renamedBar } = queryFoo.__setFields('id', { renambedBar: 'bar' })

/* yields:
 * query = {
 *   queryFoo: { 
 *     __setFields: <function>, __setArgs: <function>, __setDirectives: <function>, __setAlias: <function>,
 *     id: {
 *       __setFields: <function>, __setArgs: <function>, __setDirectives: <function>, __setAlias: <function>,
 *     },
 *     renamedBar: {
 *       __setFields: <function>, __setArgs: <function>, __setDirectives: <function>, __setAlias: <function>,
 *       __aliased: 'bar'
 *     }
 *   },
 *   __query: <function>, __get: <function>, __aggregate: <function>
 * }
 * queryFoo = query.queryFoo
 * renamedBar = query.queryFoo.renamedBar
 */

// and this would work also to just set a piece of the query object statically using vanilla javascript:

query.queryFoo.__filters = { name: { eq: "Baz" } }

/* yields:
 * query = {
 *   queryFoo: { 
 *     __setFields: <function>, __setArgs: <function>, __setDirectives: <function>, __setAlias: <function>,
 *     __filters = { name: { eq: "Baz" } },
 *     id: {
 *       __setFields: <function>, __setArgs: <function>, __setDirectives: <function>, __setAlias: <function>,
 *     },
 *     renamedBar: {
 *       __setFields: <function>, __setArgs: <function>, __setDirectives: <function>, __setAlias: <function>,
 *       __aliased: 'bar'
 *     }
 *   },
 *   __query: <function>, __get: <function>, __aggregate: <function>
 * }
 * queryFoo = query.queryFoo
 * renamedBar = query.queryFoo.renamedBar
 */

This would give us the best of the both worlds being able to quickly parse an object into a GraphQL query and being able to create a query in an object oriented way.

@amaster - Well, I did it. I don’t think you can create objects any simpler! Now supports multiple queries. I also added a method for all first level functions.

Please check for bugs. I have some maintenance left to do. I also thought now it might be possible to do something like:

  • cascadeDelete('type1', 'type2') - Which could be used to generate a cascade delete query for whatever nested types you want to delete after the delete() function. May work on that this weekend. (DGraph still needs to allow this option internally)

There could be other possible functions we could think of as well.

Keep me posted on how it works for everyone…

J

easy-dgraph - HUGE UPDATE!

So…

After Spending the last 5 days debugging, I feel like I have finally completley got this thing up and running where it really is something special.

Working Examples - I have ran 37 working unit tests with every example you can think of that is possible with the current Dgraph configuration. The bugs have been knocked out!

If you find any example that does not work as expected, simplify it, and post it here and I will get on it!


I got all possibilities the current working Dgraph can do, now I may start working on more things Dgraph does not do out of the box…

Just a taste…

const d = new Dgraph('task').query({
  tom: 1
}).cascade('name', 'me').first(5).offset(2).build();

produces:

query { queryTask(first: 5, offset: 2) @cascade(fields: ["name", "me"]) { tom } }

Yes, that includes:

(Read those last two again if you know how complicated those chains are…)

Todo list (maybe?):

  • Deep Deletes (Cascade Deletes) - Probably work on this soon, would specify field(s)
  • Deep Mutation Updates (Multiple Levels)
  • Aggregate Counts with Filters
  • Multiple Set Inputs
  • Nested Filters

When I have specific needs for these features, I will probably write them for this package. I am going to write a separate feature request on some things I learned about deep and nested items…

Please let me know your experience with this current version and later (1.50)…

Thanks,

J

1 Like

Update 6/25/21

So I cannot do nested filters. Hopefully Dgraph will add this, and I don’t want to sort anything on the client side. I probably won’t add anything else, as it pretty much does everything (until Dgraph itself adds new features).

Deep Updates

You can specify your ID, or it will infer it to be id automatically…

const d = new Dgraph('lesson').deep({ field: 'cards', type: 'card' }).update({
    me: 1,
    cards: {
        id: 1,
        tommy: 1
    }
}).filter({ id: '12345' })
.set({ me: false, cards: [{ tommy: 'son' }, { id: '2', tommy: 'bill' }] })
.build();

This will automatically create a complex mutation with add and update(s).

See here for more examples…

Deep Deletes

Deep deletes simply cannot be done in one query, as I discussed here. While not perfect or feasible if you have thousands of nodes you want to delete, this might work for most situations.

Say I want to delete all lessons when I delete a class type, which has many lessons…

async delete(id: string): Promise<void> {

  // get ids of lessons
  const ids = await this.dgraph.type('class').filter(id).query({
    lessons: { id: 1 }
  }).build();

  // delete ids && delete lesson
  await this.dgraph.type('lesson').filter(ids).delete()
  .type('class').filter(id).delete().build();

  ...
}

You could basically grab ALL ids of lessons by a certain class, then create a mutation which deletes all those lessons by id, then that class.

Keep in mind, Dgraph returns all the ids in a data type, so you may need to map those ids with something like:

build().then((r: any[]) => {
    if (r[0].lessons) {
      return r[0].lessons.map((r: any) => r.id);
    }
  });

Either way, you avoid custom dql… yay!

Hope this helps someone,

J