Fork me on GitHub

Hi! How do you deal with GraphQL data you receive, which has circular structures (-> app :img :app :img ...), when both the "application" and the "image" contain fields of the same name (e.g. :id)? How do you spec that? I tried to put the two into different namespaces to be able to distinguish the keywords by namespace, but that will create circular dependencies...


And how do you spec the incomplete data that you fetch? Especially when you try to share spec definitions across services? I.e. if I fetch an "application" from a service, there it contains "instances running", which are speced, but not all of my clients might require and this data and hence not fetch it, so the original spec will not validate this partial data...


I’m not sure I understand 100% what you’re asking, but one thing to note is that you don’t create circular dependencies if you’re just using namespaced keywords. E.g. using :app/id is quite possible without ever require-ing the app namespace.


@U7PBP4UVA So if I have circular structures in my data, I would be forced to use simple :keywords as opposed to ::namespaced/keywords for the keys? Thought maybe I could (def) something or so, like I would do in C, when I get into header #include trouble.


You can use :fully.qualified.namespace/keyword with a single colon and the full namespace. You cannot use ::namespace-alias/keyword with two colons without requiring the namespace (or, there’s no easy way, I think there are some hacks).


I should add that I tried to create something like (ns application) (s/def ::id string?), i.e. ideally the different fields would have keywords bound to a namespace, so that I can distinguish them.


So ::alias/kw is exactly equivalent to :fqns/kw? I always assumed that :: created an object that cannot be created using :.


Sorry, missed this — yes, ::alias/kw expects to find a (:require [some.ns :as alias]) and then is 100% equivalent to some.ns/kw.


For partial data, use the opt or opt-un of spec?


@U7PBP4UVA So you generally have different specs on the consumer than on the producer, because the consumer might opt to request only parts of the data that the producer deems to be required to form a complete object?


In general I think GraphQL and spec have a little bit of friction, but can easily play well together. I don’t have a huge experience myself, so it’d help if you post some example code so that we can talk specifics.


There will be a talk at the conj in a few days that deals with spec and GraphQL and I’m looking forward to it.


(Well, when the video comes out)


Nice, thanks for the hint towards the talk!


One solution to the problem I think you’re describing is to have two specs


"... two specs": And because they will have to use different keywords, I will rewrite the keys of my maps when I receive them through GraphQL, but before I validate and store them?


One key idea of spec, I believe, is to be able to separate the “keys” of a map, e.g. :user/email has to be a string, of this length, valid email etc etc, to the “aggregation” of those keys (e.g. a user has to have at least a :user/id and :user/email.


Where are you storing your data?


re-frame (it uses an internal db that is available to event handlers registered with re-frame)


Two specs can quite easily use the same keywords. Let me type an example…


(require '[clojure.spec.alpha :as s])

(defn email? [x]

(s/def :user/id string?)
(s/def :user/email (s/and string? email?))

(s/def :user/user-creation (s/keys :req-un [:user/id :user/email]))

(s/valid? :user/user-creation {:id "abc" :email ""})

(s/def :user/user-fetch (s/keys :opt-un [:user/id :user/email]))

(s/valid? :user/user-fetch {:email ""})


Is this what you were referring to?


Note how :user/id and :user/email are shared between :user/user-creation, :user/user-fetch, but in one they are required, whereas in the other optional.


Also note, there’s no user namespace required anywhere.


What about data structures like these: {:users [{:id ... :email ...}]} I thought I need to (s/def :users (s/* :user)) and (s/def :user (s/keys :req-un [:id :email])) and the :user keyword in both have to match, in order for spec to understand that one refers to the other.


And given these it becomes harder to have to definitions of :users (plural) where one contains :users (singular) where :email is required and the other does not require this keyword. (e.g.)


I would think that now I have to recreate the whole data structure / tree. And what's required (and hence fetched with GraphQL) and what not depends a lot on where in the application the data is used...


So you mean that depending on the query, sometimes you will need to send back: {:users [{:email ...} {:email ...}]} and others: {:users [{:name ...} {:name ...}]}


If this is the case, I’d have my :user contain only optional keys, then have another pass using a simple function to validate that every element of that vector contains only the expected keys.


(Note: I think spec best practice, if not enforced, is to use namespaced keywords when doing (s/def ...)


Re: Note: This is exactly how I arrived at the fqns issue mentioned in the other thread.


Re: "that vector contains only the expected keys": So I have a loose spec on the consumer side and then use another function to validate that the subset of data required for this operation is actually present? And if it is present, I can be sure that it follows the spec. I just have to validate it down to the leaves, in order to ensure it is complete?


Yes, more or less. I would expect the graphql library to do some sort of validation as well though.


Ah, but you’re doing this in the browser, right?


Note as well — there is work underway for a programmatic API to spec, so that you are able to create specs on the fly. But its ETA is unknown at the moment.


I am doing this in the browser, yes.


I wrote down what I think I need in #clojure-spec:, because that channel seems more suitable to this question in retrospect.


Thanks for all the help! I think it got me quite a bit towards better understanding how the problem I face actually looks like.


I’ve been thinking about the same problem recently 🙂