Fork me on GitHub
#graphql
<
2017-12-14
>
admay00:12:40

@petr Have you worked through the lacinia tutorial yet?

PB00:12:32

@admay I just did, I'm a little unsure of what to do in the even that I don't want ot return an object? Unless I define that object as something like {:job "done"}

admay01:12:39

I would warn against not returning anything. the reason being, that’s an incredibly unfriendly design. However, having a status type is not unheard of. The status type I use usually looks something like this.

{:objects
  {:Status
    {:fields
      {:status {:type Int}
       :message {:type String}}}}}

admay01:12:27

Just as a thing to give the consumer something to go off of

admay01:12:36

I’m sure theres a more ‘Lacinia-y’ way to do it though

hlship01:12:22

Thanks for the feedback; I have a rough idea of how the tutorial will play out, but it's all in my head.

hlship01:12:49

For the moment, read the lacinia-pedestal documentation carefully for answers about status codes and the like.

hlship01:12:03

It's there, but the tutorial will ultimately lead you to it more effectively.

PB04:12:48

I'm pretty familiar with httpie. But I've been using insomnia more recently

PB04:12:27

@admay whencreating a request that doesn't necessarily get anything from a database, How do I structure that? Currently I have somethign that looks as such:

;; cgg-schema.edn
;; objects....
:Request
  {:description "a request"
   :fields
   {}
   }
;;queries
:do_the_thing
  {:type :Request
   :resolve :query/do-the-thing}}

;; resolver-map
(defn resolver-map
  [component]
  (let [db (:db component)]
    {:query/game-by-id (game-by-id db)
     :query/member-by-id (member-by-id db)
     :BoardGame/designers (board-game-designers db)
     :BoardGame/rating-summary (rating-summary db)
     :GameRating/game (game-rating->game db)
     :Designer/games (designer-games db)
     :Member/ratings (member-ratings db)
     :Request/do-the-thing (do-the-thing db)}))


(defn do-the-thing
  [db]
  (fn [_ args _]
    (prn "AAAAAAAAAA")))

PB04:12:04

Yet I cannt for the life of me get it to prn that in my repl

admay04:12:01

You have a couple of mistypes in there

myguidingstar04:12:26

I'm learning to use variables. I tried this code (execute my-schema "query { getEntry(id:$id) {id}}" {:id "aa"} nil) but it returns: {:errors [{:message "Exception applying arguments to field getEntry': For argument id', argument references undeclared variable `id'.", :query-path [], :locations [{:line 1, :column 6}], :field :getEntry, :argument :id, :unknown-variable :id, :declared-variables ()}]}

admay04:12:37

First one is in the resolver-map

PB04:12:54

do-the-thing?

admay04:12:00

:Request/do-the-thing should be :query/do-the-thing

admay04:12:12

as defined by :resolve :query/do-the-thing

admay04:12:46

Actually, I think that’s the only one

PB04:12:52

Ah! So are only those with the :query ns able to be queried?

hlship04:12:36

A query with variables must declare the variables.

hlship04:12:31

Docs need more examples.

hlship04:12:36

Check the tests.

admay04:12:47

It’s not that you need to have the prefix set to :query. It’s just that you need one. :query is more of a convention

hlship04:12:09

And goodnight! I've been up since 4am (travel day).

PB04:12:20

So I'm running into the issue that "Field do_the_thing' (of type Request') must have at least one selection." I thought I explicitly did not have any args defined

admay04:12:59

Lacinia might force you to have a return of some type, i’m not sure. How about we try something like this,

;; cgg-schema.edn
;; objects....
:Request
  {:description "a request"
   :fields
   {:message {:type String}}
   }
;;queries
:do_the_thing
  {:type :Request
   :resolve :query/do-the-thing}}

;; resolver-map
(defn resolver-map
  [component]
  (let [db (:db component)]
    {:query/game-by-id (game-by-id db)
     :query/member-by-id (member-by-id db)
     :BoardGame/designers (board-game-designers db)
     :BoardGame/rating-summary (rating-summary db)
     :GameRating/game (game-rating->game db)
     :Designer/games (designer-games db)
     :Member/ratings (member-ratings db)
     :query/do-the-thing (do-the-thing db)}))


(defn do-the-thing
  [db]
  (fn [_ args _]
    (do
      (println "AHHH")
      {:message "Woo!"})))
This should print to the console then return a status message of “Woo!”

PB04:12:39

That actually works! Thank you

PB04:12:10

So to the status codes, do you just return the status in the body?

PB04:12:55

Also. I see this example is using pedestal. How does it work with interceptors?

PB04:12:33

As I see that there isn't really a convenient place for any kind of middleware

admay04:12:10

So what we’re doing here is we defined an object type Request that has one field message. Then we defined a query :query/do-the-thing with a return type of Request. We map the query :query/do-the-thing to the function do-the-thing in the resolver-map. Then, to finish it up, we define a function do-the-thing that takes an argument db and returns a function of three arguments that Lacinia can recognize as a resolver. The way that Lacinia works is, if that resolver function, (do-the-thing db) returns a map-like thing that has the same shape as the fields for the expected return type Request, then it can resolve. Try changing the :message key to something else, say :hoopla and see how it breaks.

admay04:12:33

As far as lacinia-pedestal goes, check this project out, https://github.com/sashton/lacinial-pedestal-demo

admay04:12:59

There is an example handler and interceptor in there

PB04:12:28

Ah I do see that. I see he is also setting a response status

PB04:12:44

Is it common for graphql queries to not have status codes?

PB04:12:33

It seems you could get away with just returning an error

PB04:12:15

Do you restart your repl everytime you make a change

admay05:12:41

Typically, GraphQL queries are used to return some type of data. The power in GraphQL is that you can pick and choose which data to get back instead of having to get all of it back in a rest style API. The practice of returning a Status like object isn’t particularly something I want to stick with in the long run as a developer and will most likely be moving away from when I come up with a better approach.

PB05:12:43

Also, there are no examples of mutations 😕

PB05:12:56

Their docs are so bad

admay05:12:00

They are still in the process of being written. Keep in mind that Lacinia is still a very new library. @hlship and the rest of the Walmart Labs team are working at light speed lately getting bugs fixed, features added, and documentation written 🙂

admay05:12:23

Re: Mutations They are written under the :mutation top level object (i.e. same level as :objects and :queries) and have an identical structure to :queries.

PB05:12:58

So :mutation is singular and all the others are plural? Or is it :mutations?

admay05:12:13

Re: Restarting the REPL. I’ve integrated component and mount into my workflow to prevent me from ever having to restart my REPL during development.

admay05:12:25

Oh, typo! it’s plural, :mutations

admay05:12:37

good catch

PB05:12:45

So yeah, this uses components, but I have to constantly (stop) and (start)

PB05:12:14

I guess the least I can do is, if I use this. Is try to contribute to their docs

admay05:12:27

The reason why is because you have to re-attach the resolvers every time you add or remove one from the resolver-map. That’s the only time you should have to restart though.

PB05:12:27

So taking a look at this: https://dev-blog.apollodata.com/designing-graphql-mutations-e09de826ed97 . It appears there are other fields such as types?

admay05:12:04

What do you mean, ‘other fields’?

admay05:12:26

The fields of the objects, query args, and mutation args are all up to you to choose

PB05:12:38

Erm, as an example:

type Todo {
  id: ID!
  text: String
  completed: Boolean
}

schema {
  # The query types are omitted so we can focus on the mutations!
  mutation: RootMutation
}

type RootMutation {
  createTodo(input: CreateTodoInput!): CreateTodoPayload
  toggleTodoCompleted(input: ToggleTodoCompletedInput!): ToggleTodoCompletedPayload
  updateTodoText(input: UpdateTodoTextInput!): UpdateTodoTextPayload
  completeAllTodos(input: CompleteAllTodosInput!): CompleteAllTodosPayload
}

PB05:12:47

Unless that is all handled by their edn file

PB05:12:52

And generated behind the scenes

PB05:12:34

Acutally reading their stuff on mutations, I think that's exactly what happens

admay05:12:44

That’s just how you define a GraphQL schema in other languages. Lacinia is the only GraphQL library I’ve seen that uses a different syntax as the conventional default.

admay05:12:24

In other words, in a language like JavaScript, where EDN doesn’t exist, the above is how the schema is defined.

PB05:12:36

That make sense

PB05:12:18

Is it idiomatic to use the mutation ns inside the resolver-map as such:

(defn resolver-map
  [component]
  (let [db (:db component)]
    {:query/game-by-id (game-by-id db)
     :query/member-by-id (member-by-id db)
     :BoardGame/designers (board-game-designers db)
     :BoardGame/rating-summary (rating-summary db)
     :GameRating/game (game-rating->game db)
     :Designer/games (designer-games db)
     :Member/ratings (member-ratings db)
     :query/do-the-thing (do-the-thing db)
     :mutation/create-users (create-users db)}))

admay05:12:22

If you haven’t read through it yet, the official GraphQL intro is a good read, http://graphql.org/learn/

PB05:12:34

I'm actually trying to use that as I go

admay05:12:56

It’s a good walkthrough to give you a nice big picture view before diving into code

admay05:12:06

And yes, that’s how most people I’ve seen define their mutations

PB05:12:57

I'm just trying to see how the basics are supposed to work. I honestly didn't expect to stumble as much as I am

PB05:12:12

Mutation syntax is weird

PB05:12:07

Ooof!!!

curl '' \
  -H 'authorization: Bearer xxxxxxx' \
  -d '{
    "query": "mutation($schedule:PipelineScheduleCreateInput!) { pipelineScheduleCreate(input:$schedule) { pipelineScheduleEdge { node { label nextBuildAt cronline } } } }",
    "variables": {
      "schedule": {
        "pipelineID": "UGlwZWxpbmUtLS02MzliNWJjOC0wMGZmLT",
        "cronline": "@midnight",
        "label": "Nightly build"
      }
    }
  }'

sashton15:12:36

> When developing an application, it is desirable to be able to change the schema without restarting. Lacinia-Pedestal supports this: in the above example, the schema passed to pedestal-service could be a function that returns the compiled schema.

guy15:12:40

^ yeah we did something similar but with :env variables. So if you are in dev it just has a function that recompiles the schema everytime

PB16:12:49

Does anyone mind explaining that mutation query to me? In my example I have the following:

;;cgg-schema.edn

:User
  {:description "a user"
   :fields
   {:uid {:type String}
    :email {:type String}}}}

 :mutations
 {:create_users
  {:type User
   #_:args
   #_{:email {:type (not-null ID)}
      :password {:type (not-null String)}}
   :resolve :mutation/create-users}}

;;shema.clj
(defn resolver-map
  [component]
  (let [db (:db component)]
    {:query/game-by-id (game-by-id db)
     :query/member-by-id (member-by-id db)
     :BoardGame/designers (board-game-designers db)
     :BoardGame/rating-summary (rating-summary db)
     :GameRating/game (game-rating->game db)
     :Designer/games (designer-games db)
     :Member/ratings (member-ratings db)
     :query/do-the-thing (do-the-thing db)
     :mutation/create-users (create-users db)}))

(defn create-users
  [db]
  (fn [_ args _]
    {:uid "134134-ljdj-lksfj-2094824"
     :email ""}))
I"m unsure how to write the mutation request

guy16:12:26

the graphql request?

guy16:12:56

as in the client would send?

PB16:12:06

Exactly

PB16:12:12

How that is formatted confuses me

guy16:12:28

ill come up with something in a sec

guy16:12:33

but have u read the graphql docs?

PB16:12:12

And I don't think they do much to explain it

guy16:12:59

So the first thing to understand i would say is what the mutation is called

guy16:12:15

mutation CreateReviewForEpisode($ep: Episode!, $review: ReviewInput!) {
  createReview(episode: $ep, review: $review) {
    stars
    commentary
  }
}

guy16:12:21

So thats their mutation request

guy16:12:25

Yours would be

guy16:12:17

mutation create_users(email String! password ID!) {
   create_users(email: "some-email", password: "someid") {
   uid
   email
  }
}

PB16:12:18

So the writer of the request names the mutation request?

PB16:12:33

They basically write the fn?

guy16:12:33

it took me a little while to understand

guy16:12:42

So the server has the mutations defined right?

guy16:12:44

in the schema

guy16:12:53

;;cgg-schema.edn

:User
  {:description "a user"
   :fields
   {:uid {:type String}
    :email {:type String}}}}

 :mutations
 {:create_users
  {:type User
   #_:args
   #_{:email {:type (not-null ID)}
      :password {:type (not-null String)}}
   :resolve :mutation/create-users}}

guy16:12:03

Here u have said u have a user object and create_users mutation

guy16:12:14

So the server can called create_users with a alias if it wishes

PB16:12:16

OK so... They have CreateReviewForEpisode and createReview. Which is which? Which is defined where?

guy16:12:32

which is defined in their schema above somewhere in their example

PB16:12:43

So CreateReviewForEpisode is defined in the schema?

guy16:12:49

thats an alias sorry

PB16:12:59

An alias that the person writing the rquest made?

guy16:12:30

yes thats right

guy16:12:37

so say you wanted to do multiple queries

guy16:12:00

short-people tall-people could both call the same query find-people

guy16:12:08

but the client can give them an alias

guy16:12:16

so the data returned would be something like

guy16:12:35

{:data :short-people […] :tall-people [..]}

guy16:12:38

if that makes sense?

guy16:12:06

To relate it back to your example

guy16:12:30

mutation create_users(email String! password ID!) {
   create_users(email: "some-email", password: "someid") {
   uid
   email
  }
}
You as the client, are also requesting the data you want back

guy16:12:36

inside the last map

guy16:12:40

{
   uid
   email
  }

guy16:12:11

does that make sense?

guy16:12:25

Its really worth reading the graphql docs again, it took me a few times to understand it

guy16:12:35

I can PM u a example request too

PB16:12:07

That would be great, I'm currently trying to understand your tall people short people example

PB16:12:33

I think it makes sense. So you have the mutation request fn you write. You then add another map with the vars you're sending to it?

guy16:12:12

so their example

guy16:12:18

its really hard to see but they have a variables label

guy16:12:20

let me get it

guy16:12:49

So this is what the $ep is about

guy16:12:58

let me PM u

hlship18:12:19

I have big plans for the tutorial BUT there's a limit to how much "generic GraphQL" discussion can go into it, vs. the detailed information needed to implement your app in lacinia and lacinia-pedestal.

admay18:12:27

@hlship at some point, developers and engineers ought to be left alone to discover some things on their own haha

hlship18:12:37

There's a lot of "aha!" moment in GraphQL, such as the idea that you can select the same field in different ways (different arguments) and use aliases to get what you want. Is there room in the tutorial to cover this kind of thing? Only if it comes about organically.

admay18:12:20

@hlship The documentation so far has been awesome and a great reference! Thanks for all of the work you and your team puts in!

PB19:12:12

As someone who is coming to it new. I found it difficult to make the jump between what I had read about graphql and what was in your docs. How things are laid out is very different. I would suggest adding one type of each operation a person would expect to perform. I.e. Modify a game

guy19:12:19

I think the clientside graphql element takes longer than the actual lacinia part

PB19:12:31

Is anyone here using graphql to do file uploads?

guy19:12:34

Lacinia makes tonnes of sense once u learn the graphql part

admay19:12:51

@guy I agree. Without the knowledge of the underlying pattern in GraphQL, it doesn’t matter what library you’re using, it’ll be difficult to execute

guy19:12:33

yeah exactly

guy19:12:02

i made the mistake of starting with lacinia first then going back and reading the graphql docs 😅

guy19:12:34

Helped me so much though

guy19:12:40

and actually

guy19:12:58

was really nice

guy19:12:05

because it gave you something you could just try

admay19:12:03

I started with Alumbra before learning how GraphQL really works and what it is. I’m now re-writing the service that I built because the original turned out so poorly haha I love the GitHub GraphiQL explorer, it’s a great way to see GraphQL without having to build something! Just like anything else in the tech-sphere though, newcomers to the technology tend to dive straight into code before understanding the underlying concepts. Especially younger developers. The idea of instant gratification via tutorial is always more appealing than delaying satisfaction and forcing yourself to read technical docs. I see it a lot with React + Redux (Reagent + Re-frame), Angular, D3, RabbitMQ, Elasticsearch, AWS, etc…

guy20:12:09

one thing i’ve not understood yet though

guy20:12:26

is how to er propagate id’s for objects through different objects/mutations/queries

guy20:12:40

like how do you match up a user id through different edges/nodes

guy20:12:42

if that makes sense?

sashton20:12:15

@guy, do you mean sharing an id between separate invocations of graphql, or between a resolver and it’s descendent resolvers?

guy20:12:09

Its something i saw from apollo graphql library let me find the video

guy20:12:30

Sorry i didn’t articulate my question very well

guy20:12:39

Let me spend a bit of time thinking about what i really mean 😅

admay20:12:15

Anyone know anything about resolving objects with qualified keywords? I’m reading maps from a db like so:

{:client/id 1, :client/label "the baddies", :client/modified_date #inst "2017-12-09T02:43:03.931654000-00:00", :client/created_date #inst "2017-12-09T02:43:03.931654000-00:00"}
But since you can only use simple-keywords in a Lacinia schema (to the best of my knowledge), I can’t resolve that map as a client. Should I just have a ->client function to coerce the object into the proper shape or is there a better way to do it?

guy21:12:05

I think someone asked about that before :thinking_face:

admay21:12:15

I did lol

admay21:12:01

I’m just curious as to whether or not Lacinia can work with qualified keywords like this in some way or if the approach is going to be to write a bunch of row->thing functions

guy21:12:02

sorry haha

PB21:12:23

I've always been of the school of thought that the server should receive unqualified keywords(json-style), work internally with fully qualified keywords and return unqualified (json-style)

PB21:12:43

But I am woefully under-qualified to speak in terms of how things should work with graphql

guy21:12:17

I would probably opt for that route too

guy21:12:29

wrap your resolves with some convert maybe

guy21:12:33

converter*

admay21:12:17

My goal isn’t to send qualified keywords to the consumer. Rather, just to tell Lacinia to ignore the qualification. I’m not a fan of it, but I think something like (map-keys unqualify-keyword object-map) might be the move for me.