-
Notifications
You must be signed in to change notification settings - Fork 167
Modify federation to be possible with edn schema as well #420
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from 4 commits
901178b
c20b1e4
d1dadc6
331483b
e940cc6
e43ae52
edf5442
0810db5
1431062
a9c70fe
f5d11f8
2f6c9b6
7e08e79
04ecbcf
15c844e
37885e5
6766a28
eff986d
cfbe115
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,77 @@ | ||
| {:objects | ||
| {:_Service | ||
| {:fields | ||
| {:sdl | ||
| {:type (non-null String)}}} | ||
| :User | ||
| {:fields | ||
| {:id | ||
| {:type (non-null Int)} | ||
| :name | ||
| {:type (non-null String)}} | ||
| :directives [{:directive-type :key :directive-args {:fields "id"}}]} | ||
| :Query | ||
| {:fields | ||
| {:user_by_id | ||
| {:type :User :args | ||
| {:id | ||
| {:type (non-null Int)}}}}} | ||
| :Account | ||
| {:fields | ||
| {:acct_number | ||
| {:type (non-null String)} :name | ||
| {:type (non-null String)}} | ||
| :directives [{:directive-type :key :directive-args {:fields "acct_number"}}]} | ||
| :Product | ||
| {:fields | ||
| {:upc | ||
| {:type (non-null String) :directives [{:directive-type :external}]} :reviewed_by | ||
| {:type :User}} | ||
| :directives [{:directive-type :key :directive-args {:fields "upc"}} | ||
| {:directive-type :extends}]}} | ||
| :scalars | ||
| {:_Any | ||
| {:parse :_Any/parser, | ||
| :serialize :_Any/serializer}, | ||
| :_FieldSet | ||
| {:parse :_FieldSet/parser, | ||
| :serialize :_FieldSet/serializer} | ||
| :link__Import | ||
| {:parse :link__Import/parser, | ||
| :serialize :link__Import/serializer}} | ||
|
|
||
| :enums | ||
| {:link__Purpose | ||
| {:values [{:enum-value :SECURITY} {:enum-value :EXECUTION}]}} | ||
|
|
||
| :directive-defs | ||
| {:external | ||
| {:locations #{:field-definition}} | ||
| :requires | ||
| {:locations #{:field-definition} | ||
| :args {:fields {:type (non-null _FieldSet)}}} | ||
| :provides | ||
| {:locations #{:field-definition} | ||
| :args {:fields {:type (non-null _FieldSet)}}} | ||
| :key | ||
| {:locations #{:object :interface} | ||
| :args {:fields {:type (non-null _FieldSet)} | ||
| :resolvable {:type Boolean :default-value true}}} | ||
| :link | ||
| {:locations #{:schema}, | ||
| :args {:url {:type String}, :as {:type String}, :for {:type :link__Purpose}, :import {:type (list :link__Import)}}} | ||
| :shareable {:locations #{:field-definition :object}}, | ||
| :inaccessible | ||
| {:locations | ||
| #{:enum | ||
| :input-field-definition | ||
| :interface | ||
| :input-object | ||
| :enum-value | ||
| :scalar | ||
| :argument-definition | ||
| :union | ||
| :field-definition | ||
| :object}}, | ||
| :override {:locations #{:field-definition}, :args {:from {:type (non-null String)}}}, | ||
| :extends {:locations #{:interface :object}}}} |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,34 @@ | ||
| type _Service{ | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I wonder if we could get pretty-printed output. It would make things more complicated, for sure.
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It's a bit complicated, but I'll try. |
||
| sdl: String! | ||
| } | ||
| type User @key(fields: "id") { | ||
| id: Int! | ||
| name: String! | ||
| } | ||
| type Query{ | ||
| user_by_id(id: Int!): User | ||
| } | ||
| type Account @key(fields: "acct_number") { | ||
| acct_number: String! | ||
| name: String! | ||
| } | ||
| type Product @key(fields: "upc") @extends { | ||
| upc: String! | ||
| reviewed_by: User | ||
| } | ||
| scalar _Any | ||
| scalar _FieldSet | ||
| scalar link__Import | ||
| enum link__Purpose{ | ||
| SECURITY | ||
| EXECUTION | ||
| } | ||
| directive @extends on INTERFACE | OBJECT | ||
| directive @key(fields: _FieldSet!, resolvable: Boolean = true) on INTERFACE | OBJECT | ||
| directive @external on FIELD_DEFINITION | ||
| directive @shareable on FIELD_DEFINITION | OBJECT | ||
| directive @requires(fields: _FieldSet!) on FIELD_DEFINITION | ||
| directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) on SCHEMA | ||
| directive @provides(fields: _FieldSet!) on FIELD_DEFINITION | ||
| directive @override(from: String!) on FIELD_DEFINITION | ||
| directive @inaccessible on ENUM | INPUT_FIELD_DEFINITION | INTERFACE | INPUT_OBJECT | ENUM_VALUE | SCALAR | ARGUMENT_DEFINITION | UNION | FIELD_DEFINITION | OBJECT | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -14,11 +14,13 @@ | |
|
|
||
| (ns com.walmartlabs.lacinia.federation | ||
| (:require | ||
| [com.walmartlabs.lacinia.resolve :as resolve :refer [with-error]] | ||
| [com.walmartlabs.lacinia.internal-utils :as utils :refer [get-nested]] | ||
| [com.walmartlabs.lacinia.resolve-utils :as ru] | ||
| [com.walmartlabs.lacinia.schema :as schema] | ||
| [clojure.spec.alpha :as s])) | ||
| [com.walmartlabs.lacinia.resolve :as resolve :refer [with-error]] | ||
| [com.walmartlabs.lacinia.internal-utils :as utils :refer [get-nested]] | ||
| [com.walmartlabs.lacinia.resolve-utils :as ru] | ||
| [com.walmartlabs.lacinia.schema :as schema] | ||
| [clojure.spec.alpha :as s] | ||
| [clojure.string :refer [join]] | ||
| [clojure.core.match :refer [match]])) | ||
|
|
||
| (def foundation-types | ||
| "Map of annotations and types to automatically include into an SDL | ||
|
|
@@ -119,25 +121,215 @@ | |
|
|
||
| (ru/aggregate-results results #(maybe-wrap (reduce into [] %)))))))) | ||
|
|
||
| (defn ^:private apply-list | ||
| [f x] | ||
| (if (-> x first seq?) | ||
| (apply f x) | ||
| (f x))) | ||
|
|
||
| (defn ^:private edn-description->sdl-description | ||
| [description] | ||
| (if (nil? description) | ||
| "" | ||
| (str "\"\"\"\n" description "\n\"\"\"\n"))) | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What if description itself includes characters, such as
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The input is edn, so there seems to be no problem. Can you give me an example? |
||
|
|
||
| (defn ^:private edn-type->sdl-type | ||
| [type] | ||
| (if (seq? type) | ||
| (let [[hd & tl] type] | ||
| (match hd | ||
| nil "" | ||
| 'non-null (str (apply-list edn-type->sdl-type tl) "!") | ||
| 'list (str "[" (apply-list edn-type->sdl-type tl) "]") | ||
| 'String "String" | ||
| 'Int "Int" | ||
| 'Float "Float" | ||
| 'Boolean "Boolean" | ||
| 'ID "ID" | ||
| (object :guard keyword?) (name object) | ||
| (scalar :guard symbol?) (name scalar))) | ||
| (recur (list type)))) | ||
|
|
||
| (defn ^:private value->string | ||
| [value] | ||
| (match value | ||
| (string :guard string?) (str "\"" string "\"") | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same here, may need to escape some characters. |
||
| (keyword :guard keyword?) (name keyword) | ||
| else (str else))) | ||
|
|
||
| (defn ^:private edn-default-value->sdl-default-value | ||
| [default-value] | ||
| (if (nil? default-value) | ||
| "" | ||
| (str " = " (value->string default-value)))) | ||
|
|
||
| (defn ^:private edn-arg-descrption->sdl-arg-description | ||
| [description] | ||
| (if (nil? description) | ||
| "" | ||
| (str "\"" description "\" "))) | ||
|
|
||
| (defn ^:private edn-args->sdl-args | ||
| [args] | ||
| (if (nil? args) | ||
| "" | ||
| (str "(" (join ", " (map (fn [[arg-name {:keys [type default-value description]}]] (str (edn-arg-descrption->sdl-arg-description description) (name arg-name) ": " (edn-type->sdl-type type) (edn-default-value->sdl-default-value default-value))) args)) ")"))) | ||
|
|
||
| (defn ^:private edn-directive-args->sdl-directive-args | ||
| [directive-args] | ||
| (if (nil? directive-args) | ||
| "" | ||
| (str "(" (->> directive-args | ||
| (map (fn [[arg-name arg-value]] (str (name arg-name) ": " (value->string arg-value)))) | ||
| (join ", ")) ")"))) | ||
|
|
||
| (defn ^:private edn-directives->sdl-directives | ||
| [directives] | ||
| (if (nil? directives) | ||
| "" | ||
| (str " " | ||
| (->> directives | ||
| (map (fn [{:keys [directive-type directive-args]}] | ||
| (str "@" (name directive-type) (edn-directive-args->sdl-directive-args directive-args)))) | ||
| (join " ")) " "))) | ||
|
|
||
| (defn ^:private edn-fields->sdl-fields | ||
| [fields] | ||
| (str | ||
| "{\n" | ||
| (->> fields | ||
| (map (fn [[field-name {:keys [type args description]}]] | ||
| (str (edn-description->sdl-description description) (name field-name) (edn-args->sdl-args args) ": " (edn-type->sdl-type type)))) | ||
| (join "\n")) | ||
| "\n}")) | ||
|
|
||
| (defn ^:private edn-objects->sdl-objects | ||
| [objects] | ||
| (->> objects | ||
| (map (fn [[key {:keys [fields directives description]}]] | ||
| (str (edn-description->sdl-description description) | ||
| "type " | ||
| (name key) | ||
| (edn-directives->sdl-directives directives) | ||
| (edn-fields->sdl-fields fields)))) | ||
| (join "\n"))) | ||
| (defn ^:private edn-queries->sdl-queries | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I prefer a blank line between
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. :queries is such a hold over from early days Lacinia; we really should deprecate it, it causes problems. That being said, should probably fold :queries in the Query object (likewise mutations and subscriptions) and then pretty print that. It may be ok to skimp on some error checking, such as name collisions between Query fields an names in the :queries map ... incorrect SDL will be generated BUT that will be caught an instant later at schema compilation and/or calls to prevent-collision.
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Edited to reflect the review. |
||
| [queries] | ||
| (str (-> queries :description edn-description->sdl-description) "type Query " (edn-fields->sdl-fields queries))) | ||
|
|
||
| (defn ^:private edn-interfaces->sdl-interfaces | ||
| [interfaces] | ||
| (->> interfaces | ||
| (map (fn [[key val]] | ||
| (str "interface " | ||
| (name key) | ||
| (-> val :fields edn-fields->sdl-fields)))) | ||
| (join "\n"))) | ||
| (defn ^:private edn-input-objects->sdl-input-objects | ||
| [input-objects] | ||
| (->> input-objects | ||
| (map (fn [[key val]] | ||
| (str "input " | ||
| (name key) | ||
| (-> val :fields edn-fields->sdl-fields)))) | ||
| (join "\n"))) | ||
| (defn ^:private edn-unions->sdl-unions | ||
| [unions] | ||
| (->> unions | ||
| (map (fn [[union-name {members :members}]] | ||
| (str "union " (name union-name) " = " (->> members | ||
| (map name) | ||
| (join " | "))))) | ||
| (join "\n"))) | ||
| (defn ^:private edn-mutations->sdl-mutations | ||
| [mutations] | ||
| (str "type Mutation " (edn-fields->sdl-fields mutations))) | ||
|
|
||
| (defn ^:private edn-enum-value->sdl-enum-value | ||
| [enum-value] | ||
| (match enum-value | ||
| {:enum-value value} value | ||
| (value :guard keyword?) value)) | ||
|
|
||
| (defn ^:private edn-enums->sdl-enums | ||
| [enums] | ||
| (->> enums | ||
| (map (fn [[enum-name {values :values}]] | ||
| (str "enum " (name enum-name) "{\n" (->> values (map edn-enum-value->sdl-enum-value) (map name) (join "\n")) "\n}"))) | ||
| (join "\n"))) | ||
| (defn ^:private edn-scalars->sdl-scalars | ||
| [scalars] | ||
| (->> (keys scalars) | ||
| (map name) | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think there's value to sorting by name in each of these blocks, for repeatability.
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Sorting has been added to reflect reviews. |
||
| (map #(str "scalar " %)) | ||
| (join "\n"))) | ||
|
|
||
| (def directive-targets | ||
| {:enum "ENUM" | ||
| :input-field-definition "INPUT_FIELD_DEFINITION" | ||
| :interface "INTERFACE" | ||
| :input-object "INPUT_OBJECT" | ||
| :enum-value "ENUM_VALUE" | ||
| :scalar "SCALAR" | ||
| :argument-definition "ARGUMENT_DEFINITION" | ||
| :union "UNION" | ||
| :field-definition "FIELD_DEFINITION" | ||
| :object "OBJECT" | ||
| :schema "SCHEMA"}) | ||
|
|
||
| (defn ^:private edn-directive-defs->sdl-directives | ||
| [directive-defs] | ||
| (->> directive-defs | ||
| (map (fn [[directive-name {:keys [locations args]}]] | ||
| (str "directive @" | ||
| (name directive-name) | ||
| (edn-args->sdl-args args) | ||
| " on " | ||
| (->> locations | ||
| (map directive-targets) | ||
| (join " | "))))) | ||
| (join "\n"))) | ||
|
|
||
| (defn generate-sdl | ||
| "Translate the edn lacinia schema to the SDL schema." | ||
| [schema] | ||
| (->> schema | ||
| (map (fn [[key val]] | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Again, repeatability; depending on the type of map (array-map vs. hash-map, etc.) this order of all this could shift dramatically; for small maps it's in order of keys added, in larger maps (hash-map) it's related to the hash of the key. I'd say a good fixed order would be:
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Sorting has been added to reflect reviews.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Another problem I'm just noticing is that the extras directives, types, etc. provided by com.walmartlabs.lacinia.federation/foundation-types need to be filtered back out. |
||
| (case key | ||
| :objects (edn-objects->sdl-objects val) | ||
| :queries (edn-queries->sdl-queries val) | ||
| :interfaces (edn-interfaces->sdl-interfaces val) | ||
| :scalars (edn-scalars->sdl-scalars val) | ||
| :unions (edn-unions->sdl-unions val) | ||
| :input-objects (edn-input-objects->sdl-input-objects val) | ||
| :mutations (edn-mutations->sdl-mutations val) | ||
| :enums (edn-enums->sdl-enums val) | ||
| :directive-defs (edn-directive-defs->sdl-directives val) | ||
| :roots ""))) | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Roots could be important, though my experience with Apollo is that it's fragile if the service schemas don't agree on the names of the root objects. That may have changed since I looked at it > 1 year ago.
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Well, I don't understand what you mean, can you elaborate on that?
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In the SDL you can override the default names of the Query, Mutation, and Subscription objects using the So this code must honor that, but must also (as necessary) emit the
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Oh, I didn't know there was such a grammar. reflected. |
||
| (join "\n"))) | ||
|
|
||
| (defn inject-federation | ||
| "Called after SDL parsing to extend the input schema | ||
| (not the compiled schema) with federation support." | ||
| [schema sdl entity-resolvers] | ||
| (let [entity-names (find-entity-names schema) | ||
| entities-resolver (entities-resolver-factory entity-names entity-resolvers) | ||
| query-root (get-nested schema [:roots :query] :Query)] | ||
| (prevent-collision schema [:unions :_Entity]) | ||
| (prevent-collision schema [:objects query-root :fields :_service]) | ||
| (prevent-collision schema [:objects query-root :fields :_entities]) | ||
| (cond-> (assoc-in schema [:objects query-root :fields :_service] | ||
| {:type '(non-null :_Service) | ||
| :resolve (fn [_ _ _] {:sdl sdl})}) | ||
| entity-names (-> (assoc-in [:unions :_Entity :members] entity-names) | ||
| (assoc-in [:objects query-root :fields :_entities] | ||
| {:type '(non-null (list :_Entity)) | ||
| :args | ||
| {:representations | ||
| {:type '(non-null (list (non-null :_Any)))}} | ||
| :resolve entities-resolver}))))) | ||
| (not the compiled schema) with federation support. | ||
| If the SDL string is not given, it is automatically created through the schema." | ||
| ([schema entity-resolvers] | ||
| (inject-federation schema (generate-sdl schema) entity-resolvers)) | ||
| ([schema sdl entity-resolvers] | ||
| (let [entity-names (find-entity-names schema) | ||
| entities-resolver (entities-resolver-factory entity-names entity-resolvers) | ||
| query-root (get-nested schema [:roots :query] :Query)] | ||
| (prevent-collision schema [:unions :_Entity]) | ||
| (prevent-collision schema [:objects query-root :fields :_service]) | ||
| (prevent-collision schema [:objects query-root :fields :_entities]) | ||
| (cond-> (assoc-in schema [:objects query-root :fields :_service] | ||
| {:type '(non-null :_Service) | ||
| :resolve (fn [_ _ _] {:sdl sdl})}) | ||
| entity-names (-> (assoc-in [:unions :_Entity :members] entity-names) | ||
| (assoc-in [:objects query-root :fields :_entities] | ||
| {:type '(non-null (list :_Entity)) | ||
| :args | ||
| {:representations | ||
| {:type '(non-null (list (non-null :_Any)))}} | ||
| :resolve entities-resolver})))))) | ||
|
|
||
| (s/def ::entity-resolvers (s/map-of simple-keyword? ::schema/resolve)) | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looking at how
matchis used in the code, it feels a bit like over kill - most of the cases could be covered withcondorcondp, and adding dependencies is always an issue.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Removed match based on review.