├── VERSION.txt
├── docs
├── .gitignore
├── _static
│ ├── tutorial
│ │ ├── deps.png
│ │ ├── graphiql-initial.png
│ │ ├── graphiql-basic-query.png
│ │ └── graphiql-doc-browser.png
│ └── css
│ │ └── custom.css
├── _examples
│ ├── extension-result.edn
│ ├── fed
│ │ ├── query.gql
│ │ ├── internal.gql
│ │ ├── schema.gql
│ │ ├── external.gql
│ │ └── products.edn
│ ├── enum-definition.edn
│ ├── selections-tree.edn
│ ├── hero-query-response.edn
│ ├── object-definition.edn
│ ├── tutorial
│ │ ├── docker-compose-1.yml
│ │ ├── user-1.clj
│ │ ├── logback-test-1.xml
│ │ ├── logback-test-2.xml
│ │ ├── system-1.clj
│ │ ├── build-1.clj
│ │ ├── test_utils-1.clj
│ │ ├── system-2.clj
│ │ ├── schema-0.clj
│ │ ├── server-1.clj
│ │ ├── server-2.clj
│ │ ├── cgg-data-1.edn
│ │ ├── system-3.clj
│ │ ├── user-2.clj
│ │ ├── deps-1.edn
│ │ ├── cgg-schema-2.edn
│ │ ├── deps-2.edn
│ │ ├── schema-1.clj
│ │ ├── deps-3.edn
│ │ ├── cgg-schema-1.edn
│ │ ├── deps-4.edn
│ │ ├── deps-5.edn
│ │ ├── cgg-data-2.edn
│ │ ├── user-4.clj
│ │ ├── system_test-1.clj
│ │ ├── deps-6.edn
│ │ ├── user-3.clj
│ │ ├── cgg-schema-3.edn
│ │ ├── db-1.clj
│ │ ├── schema-2.clj
│ │ ├── cgg-data-3.edn
│ │ ├── schema-3.clj
│ │ ├── db-2.clj
│ │ └── schema-5.clj
│ ├── tagged-resolver.edn
│ ├── unions-query-response.edn
│ ├── errors-result.edn
│ ├── query-def.edn
│ ├── selects-field.edn
│ ├── subs-schema.edn
│ ├── query-def-var.edn
│ ├── overview-exec-query.edn
│ ├── directive-defs.edn
│ ├── enum-definition-description.edn
│ ├── interface-definition.edn
│ ├── extension.edn
│ ├── subs-streamer.edn
│ ├── sample_schema.txt
│ ├── mutable-context.edn
│ ├── union-definition.edn
│ ├── object-tag.edn
│ ├── async-example.edn
│ ├── resolve-method.edn
│ ├── compile-schema.clj
│ ├── invoke-streamer.edn
│ ├── parsed_sample_schema.edn
│ ├── star-wars-schema.edn
│ └── custom-scalars.edn
├── tutorial
│ ├── wrap-up.rst
│ ├── index.rst
│ ├── domain.rst
│ └── prereqs.rst
├── clojure.rst
├── subscriptions
│ ├── resolver.rst
│ ├── index.rst
│ └── overview.rst
├── federation
│ ├── internal.rst
│ ├── external.rst
│ ├── reps.rst
│ └── implementation.rst
├── resolve
│ ├── exceptions.rst
│ ├── field-resolver-protocol.rst
│ ├── index.rst
│ ├── attach.rst
│ ├── examples.rst
│ └── context.rst
├── input-objects.rst
├── _exts
│ └── api_link.py
├── introspection.rst
├── mutations.rst
├── deprecation.rst
├── resources.rst
├── spec.rst
├── queries.rst
├── interfaces.rst
├── roots.rst
├── unions.rst
├── tracing.rst
├── samples.rst
└── json-scalar-example.md
├── perf
├── .gitignore
└── create-charts.R
├── dev-resources
├── parser
│ ├── simple.gql
│ ├── explicit-query.gql
│ ├── named-operation.gql
│ ├── reserved-args.gql
│ ├── args.gql
│ ├── nested-fields.gql
│ ├── operation-directives.gql
│ ├── arrays.gql
│ ├── enum.gql
│ ├── reserved-words.gql
│ ├── enum-reserved.gql
│ ├── variable-defaults.gql
│ ├── simple.edn
│ ├── aliases.gql
│ ├── explicit-query.edn
│ ├── vars.gql
│ ├── named-operation.edn
│ ├── field-directive.gql
│ ├── operation-directives.edn
│ ├── frag-spread.gql
│ ├── fragment-directives.gql
│ ├── reserved-args.edn
│ ├── named-fragment.gql
│ ├── nested-fields.edn
│ ├── literals.gql
│ ├── args.edn
│ ├── reserved-words.edn
│ ├── object.gql
│ ├── variable-defaults.edn
│ ├── enum.edn
│ ├── aliases.edn
│ ├── frag-spread.edn
│ ├── enum-reserved.edn
│ ├── named-fragment.edn
│ ├── vars.edn
│ ├── fragment-directives.edn
│ ├── field-directive.edn
│ ├── arrays.edn
│ └── literals.edn
├── enums.sdl
├── field-resolver-protocol-schema.edn
├── interfaces.sdl
├── preamble-docs.md
├── non-chatty-library-failures.edn
├── unions.sdl
├── duplicate-type.sdl
├── selection
│ ├── argument-def-directive.sdl
│ ├── simple.sdl
│ ├── directive-args.sdl
│ ├── scalar-types.sdl
│ ├── enum-types.sdl
│ ├── object-type.sdl
│ ├── wrap-field-directive.sdl
│ ├── union-types.sdl
│ └── interface-types.sdl
├── blockquote.sdl
├── bad-resolver-enum.edn
├── no-entities-federation.sdl
├── extensions-schema.edn
├── opt-req-enum.edn
├── list-default-value-schema.edn
├── query-reserved.edn
├── unknown-argument-type-schema.edn
├── empty-input-objects.edn
├── root-object-schema.edn
├── large-lists-schema.edn
├── deprecated-enums-schema.edn
├── dynamic-input-objects.edn
├── root-object-with-conflicts-schema.edn
├── mult-inheritance.sdl
├── input-object-schema.edn
├── basic-docs.md
├── object-scalars-schema.edn
├── generic-scalar-errors-schema.edn
├── nested-non-nullable-fields-schema.edn
├── selections-schema.edn
├── mutable-context-schema.edn
├── basic-vars.edn
├── deprecated-fields-schema.edn
├── custom-scalar-serialize-schema.edn
├── enum-default-value-with-transformer-schema.edn
├── basic.edn
├── simple-federation.sdl
├── enum-parse-serialize.edn
├── nullability.edn
├── subscription-selection.edn
├── typename-schema.edn
├── logback-test.xml
├── lists-schema.edn
├── union-query-root-schema.edn
├── dyn-args-schema.edn
├── timing-schema.edn
├── non-string-default-value-schema.edn
├── org
│ └── example
│ │ ├── schema.clj
│ │ └── db.clj
├── field-resolver-errors.edn
├── subscriptions-schema.edn
├── fragments-schema.edn
├── select-type-schema.edn
├── fixes-types-schema.edn
├── input-object-default-value-schema.edn
├── user.clj
├── bad_schema.sdl
├── summarize-schema.edn
├── doc-inheritance-schema.edn
├── documented-schema.sdl
├── tracing_demo.clj
├── sample_schema.sdl
├── star-wars-schema.edn
└── large_lists.clj
├── .gitignore
├── codegen.sh
├── java
└── com
│ └── walmartlabs
│ └── lacinia
│ ├── ParseError.java
│ ├── Graphql.tokens
│ ├── GraphqlLexer.tokens
│ ├── GraphqlSchema.tokens
│ └── GraphqlSchemaLexer.tokens
├── LICENSE
├── src
└── com
│ └── walmartlabs
│ └── lacinia
│ ├── describe.clj
│ ├── constants.clj
│ ├── expound.clj
│ ├── validator.clj
│ ├── parser
│ └── docs.clj
│ └── validation
│ ├── no_unused_fragments.clj
│ └── scalar_leafs.clj
├── .github
└── workflows
│ └── config.yml
└── test
└── com
└── walmartlabs
└── lacinia
├── query_ops_test.clj
├── parser
└── docs_test.clj
├── field_tests.clj
├── input_types_test.clj
├── mutable_context_tests.clj
├── invariant_test.clj
├── typename_test.clj
├── timeout_test.clj
├── roots_test.clj
├── documentation_test.clj
├── expound_tests.clj
├── wrapped_values_in_lists_tests.clj
└── extensions_test.clj
/VERSION.txt:
--------------------------------------------------------------------------------
1 | 1.3.0-beta-1
2 |
--------------------------------------------------------------------------------
/docs/.gitignore:
--------------------------------------------------------------------------------
1 | _build*
2 | *.pyc
3 |
--------------------------------------------------------------------------------
/perf/.gitignore:
--------------------------------------------------------------------------------
1 | *.png
2 | *.pdf
3 |
--------------------------------------------------------------------------------
/dev-resources/parser/simple.gql:
--------------------------------------------------------------------------------
1 | {
2 | hello
3 | }
4 |
--------------------------------------------------------------------------------
/dev-resources/enums.sdl:
--------------------------------------------------------------------------------
1 | enum Location { MATRIX ZION MACHINE_CITY}
2 |
--------------------------------------------------------------------------------
/dev-resources/parser/explicit-query.gql:
--------------------------------------------------------------------------------
1 | query {
2 | hello
3 | }
4 |
--------------------------------------------------------------------------------
/dev-resources/parser/named-operation.gql:
--------------------------------------------------------------------------------
1 | query Friendly { hello }
2 |
--------------------------------------------------------------------------------
/dev-resources/parser/reserved-args.gql:
--------------------------------------------------------------------------------
1 | { hello(query: "graphql") }
2 |
--------------------------------------------------------------------------------
/dev-resources/parser/args.gql:
--------------------------------------------------------------------------------
1 | {
2 | hello(world: true) { greeting }
3 | }
4 |
--------------------------------------------------------------------------------
/dev-resources/parser/nested-fields.gql:
--------------------------------------------------------------------------------
1 | {
2 | hello { world timestamp }
3 | }
4 |
--------------------------------------------------------------------------------
/dev-resources/parser/operation-directives.gql:
--------------------------------------------------------------------------------
1 | query @Whatever {
2 | hello
3 | }
4 |
--------------------------------------------------------------------------------
/dev-resources/parser/arrays.gql:
--------------------------------------------------------------------------------
1 | {
2 | hello(i: [3, 4, 5]
3 | s: ["hello", "goodbye"])
4 | }
5 |
--------------------------------------------------------------------------------
/dev-resources/parser/enum.gql:
--------------------------------------------------------------------------------
1 | {
2 | orders(sort: descending) { sales_date(time_zone: utc) }
3 | }
4 |
--------------------------------------------------------------------------------
/dev-resources/parser/reserved-words.gql:
--------------------------------------------------------------------------------
1 | {
2 | q: query
3 | m: mutation
4 | s: subscription
5 | }
6 |
--------------------------------------------------------------------------------
/dev-resources/parser/enum-reserved.gql:
--------------------------------------------------------------------------------
1 | {
2 | introspect(types: [query, mutation, subscription, other])
3 | }
4 |
--------------------------------------------------------------------------------
/docs/_static/tutorial/deps.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/walmartlabs/lacinia/HEAD/docs/_static/tutorial/deps.png
--------------------------------------------------------------------------------
/dev-resources/parser/variable-defaults.gql:
--------------------------------------------------------------------------------
1 | query($format : GreetingFormat = LONG) {
2 | hello(format : $format)
3 | }
4 |
--------------------------------------------------------------------------------
/dev-resources/field-resolver-protocol-schema.edn:
--------------------------------------------------------------------------------
1 | {:queries
2 | {:hello {:type String
3 | :resolve :query/hello}}}
4 |
--------------------------------------------------------------------------------
/dev-resources/parser/simple.edn:
--------------------------------------------------------------------------------
1 | [{:type :query
2 | :selections [{:type :field
3 | :field-name :hello}]}]
4 |
--------------------------------------------------------------------------------
/dev-resources/parser/aliases.gql:
--------------------------------------------------------------------------------
1 | {
2 | greet: hello {
3 | w: world
4 | long: longitude
5 | lat: latitude
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/dev-resources/parser/explicit-query.edn:
--------------------------------------------------------------------------------
1 | [{:type :query
2 | :selections [{:type :field
3 | :field-name :hello}]}]
4 |
--------------------------------------------------------------------------------
/docs/_static/tutorial/graphiql-initial.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/walmartlabs/lacinia/HEAD/docs/_static/tutorial/graphiql-initial.png
--------------------------------------------------------------------------------
/dev-resources/interfaces.sdl:
--------------------------------------------------------------------------------
1 | interface Matrix
2 | {
3 | eject(the_one: Boolean!) : Human
4 | }
5 |
6 | type Human {
7 | name : String!
8 | }
9 |
--------------------------------------------------------------------------------
/dev-resources/parser/vars.gql:
--------------------------------------------------------------------------------
1 | query($id: Int!
2 | $terms: [String!]
3 | $casesensitive: Boolean) {
4 | order(order_id: $id)
5 | }
6 |
--------------------------------------------------------------------------------
/dev-resources/preamble-docs.md:
--------------------------------------------------------------------------------
1 | This text is ignored.
2 |
3 | This text contains a # mark.
4 |
5 | # Customer
6 |
7 | Someone we sell to.
8 |
9 |
--------------------------------------------------------------------------------
/docs/_examples/extension-result.edn:
--------------------------------------------------------------------------------
1 | {:data
2 | {:user
3 | {:id "thx1138"
4 | :name "Thex"}}
5 | :extensions
6 | {:total-db-access-ms 3}}
7 |
--------------------------------------------------------------------------------
/docs/_static/tutorial/graphiql-basic-query.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/walmartlabs/lacinia/HEAD/docs/_static/tutorial/graphiql-basic-query.png
--------------------------------------------------------------------------------
/docs/_static/tutorial/graphiql-doc-browser.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/walmartlabs/lacinia/HEAD/docs/_static/tutorial/graphiql-doc-browser.png
--------------------------------------------------------------------------------
/docs/_examples/fed/query.gql:
--------------------------------------------------------------------------------
1 | {
2 | userById(id: "124c41") {
3 | name
4 | favoriteProducts { upc name price }
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/dev-resources/parser/named-operation.edn:
--------------------------------------------------------------------------------
1 | [{:type :query
2 | :name :Friendly
3 | :selections [{:type :field
4 | :field-name :hello}]}]
5 |
--------------------------------------------------------------------------------
/dev-resources/non-chatty-library-failures.edn:
--------------------------------------------------------------------------------
1 | {:objects
2 | {:Query
3 | {:fields
4 | {:hello {:type (non-null String)
5 | :resolve :hello}}}}}
--------------------------------------------------------------------------------
/dev-resources/unions.sdl:
--------------------------------------------------------------------------------
1 | type Human { name : String }
2 |
3 | type Agent { id : Int
4 | alias : String }
5 |
6 | union Combatant = Human | Agent
7 |
--------------------------------------------------------------------------------
/dev-resources/duplicate-type.sdl:
--------------------------------------------------------------------------------
1 | type Tree { height: Int }
2 |
3 | type Leaf { id: ID }
4 |
5 | type Tree { left: Node, right: Node }
6 |
7 | union Node = Leaf | Tree
8 |
--------------------------------------------------------------------------------
/dev-resources/parser/field-directive.gql:
--------------------------------------------------------------------------------
1 | query($internal: Boolean!) {
2 | order
3 | {
4 | id
5 | customer_id @If(test: $internal)
6 | date
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/docs/_examples/fed/internal.gql:
--------------------------------------------------------------------------------
1 | type User @key(fields: "id") {
2 | id: String!
3 | name: String!
4 | }
5 |
6 | type Query {
7 | userById(id:String!) : User
8 | }
9 |
--------------------------------------------------------------------------------
/dev-resources/selection/argument-def-directive.sdl:
--------------------------------------------------------------------------------
1 | directive @non_negative on ARGUMENT_DEFINITION
2 |
3 | type Query {
4 | sqrt(num: Float! @non_negative) : Float!
5 | }
6 |
--------------------------------------------------------------------------------
/docs/_examples/enum-definition.edn:
--------------------------------------------------------------------------------
1 | {:enums
2 | {:Episode
3 | {:description "The episodes of the original Star Wars trilogy."
4 | :values [:NEWHOPE :EMPIRE :JEDI]}}}
5 |
--------------------------------------------------------------------------------
/docs/_examples/selections-tree.edn:
--------------------------------------------------------------------------------
1 | {:Character/friends [{:selections
2 | {:Character/name [nil]}}]
3 | :Human/homePlanet [nil]
4 | :Character/name [nil]}
5 |
--------------------------------------------------------------------------------
/dev-resources/parser/operation-directives.edn:
--------------------------------------------------------------------------------
1 | [{:type :query
2 | :directives [{:directive-name :Whatever}]
3 | :selections [{:type :field
4 | :field-name :hello}]}]
5 |
--------------------------------------------------------------------------------
/docs/tutorial/wrap-up.rst:
--------------------------------------------------------------------------------
1 | Tutorial Wrapup
2 | ===============
3 |
4 | That's as far as we've made it with the tutorial so far.
5 |
6 | Looking forward to more updates, someday :-).
7 |
--------------------------------------------------------------------------------
/dev-resources/blockquote.sdl:
--------------------------------------------------------------------------------
1 | type Query {
2 | with_default(arg: String = """
3 | line 1
4 |
5 | line 3
6 | indented line 4
7 | line 5
8 | """) : String
9 | }
10 |
--------------------------------------------------------------------------------
/docs/_static/css/custom.css:
--------------------------------------------------------------------------------
1 | /* Fixes code blocks that get squeezed by sidebars */
2 | div.highlight-clojure {
3 | clear: right;
4 | }
5 |
6 | a.headerlink {
7 | float: right;
8 | }
9 |
--------------------------------------------------------------------------------
/dev-resources/bad-resolver-enum.edn:
--------------------------------------------------------------------------------
1 | {:enums
2 | {:status
3 | {:values [:good :bad]}}
4 |
5 | :queries
6 | {:current_status
7 | {:type :status
8 | :resolve :query/current-status}}}
9 |
--------------------------------------------------------------------------------
/dev-resources/no-entities-federation.sdl:
--------------------------------------------------------------------------------
1 | type User {
2 | id: Int!
3 | name: String
4 | }
5 |
6 | # Just want a union to show that _entities is not present
7 |
8 | union Stuff = User
9 |
--------------------------------------------------------------------------------
/dev-resources/extensions-schema.edn:
--------------------------------------------------------------------------------
1 | {:queries
2 | {:extension {:type String
3 | :resolve :queries/extension}
4 | :warning {:type String
5 | :resolve :queries/warning}}}
6 |
--------------------------------------------------------------------------------
/dev-resources/parser/frag-spread.gql:
--------------------------------------------------------------------------------
1 | {
2 | order {
3 | id
4 | ... on InStore {
5 | store_number
6 | }
7 | ... on OnlineOrder {
8 | refer_code
9 | }
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/docs/_examples/hero-query-response.edn:
--------------------------------------------------------------------------------
1 | {:data
2 | {:hero
3 | {:id "2001"
4 | :name "R2-D2"
5 | :friends [{:name "Luke Sykwalker"},
6 | {:name "Han Solo"},
7 | {:name "Leia Organa"}]}}}
8 |
--------------------------------------------------------------------------------
/docs/_examples/object-definition.edn:
--------------------------------------------------------------------------------
1 | {:objects
2 | {:Product
3 | {:fields {:id {:type ID}
4 | :name {:type String}
5 | :sku {:type String}
6 | :keywords {:type (list String)}}}}}
7 |
--------------------------------------------------------------------------------
/docs/_examples/tutorial/docker-compose-1.yml:
--------------------------------------------------------------------------------
1 | version: '3'
2 | services:
3 | db:
4 | ports:
5 | - 25432:5432
6 | image: postgres:15.1-alpine
7 | environment:
8 | POSTGRES_PASSWORD: supersecret
9 |
--------------------------------------------------------------------------------
/dev-resources/opt-req-enum.edn:
--------------------------------------------------------------------------------
1 | {:enums
2 | {:Result
3 | {:values [OK FAIL]}}
4 |
5 | :queries
6 | {:ok {:type :Result
7 | :resolve :null}
8 | :bad {:type (non-null :Result)
9 | :resolve :null}}}
10 |
--------------------------------------------------------------------------------
/docs/_examples/fed/schema.gql:
--------------------------------------------------------------------------------
1 | scalar _Any
2 | scalar _FieldSet
3 |
4 | # a union of all types that use the @key directive
5 | union _Entity
6 |
7 | extend type Query {
8 | _entities(representations: [_Any!]!): [_Entity]!
9 | }
--------------------------------------------------------------------------------
/dev-resources/list-default-value-schema.edn:
--------------------------------------------------------------------------------
1 | {:queries
2 | {:search
3 | {:type String
4 | :resolve :placeholder
5 | :args
6 | {:terms {:type (list String)
7 | :default-value ["columbia" "river" "gorge"]}}}}}
8 |
--------------------------------------------------------------------------------
/docs/_examples/tagged-resolver.edn:
--------------------------------------------------------------------------------
1 | (require '[com.walmartlabs.lacinia.resolve :refer [resolve-as ResolverResult]])
2 |
3 | (def tagged-resolver
4 | ^ResolverResult (fn [context args value]
5 | (resolve-as ...)))
6 |
--------------------------------------------------------------------------------
/docs/_examples/unions-query-response.edn:
--------------------------------------------------------------------------------
1 | {:data
2 | {:search
3 | [{:name "Nik-Nik"}
4 | {:imageURL "http://www.telegraph.co.uk/content/dam/film/ewok-xlarge.jpg"
5 | :title "an Ewok in The Return of the Jedi"}
6 | ]}}
7 |
8 |
--------------------------------------------------------------------------------
/dev-resources/query-reserved.edn:
--------------------------------------------------------------------------------
1 | {:objects
2 | {:subscription
3 | {:fields {:mutation {:type Boolean}}}}
4 | :queries
5 | {:query
6 | {:type :subscription
7 | :args {:mutation {:type Boolean}}
8 | :resolve :resolve-query}}}
9 |
--------------------------------------------------------------------------------
/dev-resources/unknown-argument-type-schema.edn:
--------------------------------------------------------------------------------
1 | {:queries
2 | {:example
3 | {:type String
4 | :resolve :example
5 | :args
6 | ;; Note: UUID is not declared, but it inside the `list` qualifier:
7 | {:id {:type (list UUID)}}}}}
8 |
--------------------------------------------------------------------------------
/dev-resources/selection/simple.sdl:
--------------------------------------------------------------------------------
1 | directive @auth (role: String!) on FIELD_DEFINITION
2 |
3 | directive @concise on FIELD
4 |
5 | schema { query: Query }
6 |
7 | type Query {
8 | basic: String! @auth(role: "basic")
9 | }
10 |
11 |
--------------------------------------------------------------------------------
/dev-resources/selection/directive-args.sdl:
--------------------------------------------------------------------------------
1 | directive @auth (role: String!) on FIELD_DEFINITION
2 |
3 | directive @limit(value: Int = 10) on FIELD
4 |
5 | schema { query: Query }
6 |
7 | type Query {
8 | basic: [String]!
9 | }
10 |
11 |
--------------------------------------------------------------------------------
/docs/_examples/errors-result.edn:
--------------------------------------------------------------------------------
1 | {:data {:hero {:friends [nil nil nil]}}
2 | :errors [{:message "Non-nullable field was null."
3 | :locations [{:line 1
4 | :column 20}]
5 | :path [:hero :friends :arch_enemy]}]}
6 |
--------------------------------------------------------------------------------
/docs/_examples/query-def.edn:
--------------------------------------------------------------------------------
1 | {:queries
2 | {:hero
3 | {:type (non-null :Character)
4 | :args {:episode {:type :episode}}}
5 |
6 | :human
7 | {:type (non-null Hhuman)
8 | :args {:id {:type String
9 | :default-value "1001"}}}}}
10 |
--------------------------------------------------------------------------------
/dev-resources/empty-input-objects.edn:
--------------------------------------------------------------------------------
1 | {:input-objects
2 | {:root_input
3 | {:fields {:name {:type String}}}}
4 |
5 | :queries
6 | {:print_input
7 | {:type String
8 | :args
9 | {:input {:type :root_input}}
10 | :resolve :query/print-input}}}
11 |
--------------------------------------------------------------------------------
/dev-resources/parser/fragment-directives.gql:
--------------------------------------------------------------------------------
1 | query {
2 | order {
3 | ... on InStore @Secure { store_number }
4 | item_count
5 | ... OnlineFrag @InternalOnly
6 | }
7 | }
8 |
9 | fragment OnlineFrag on OnlineOrder {
10 | refer_code
11 | }
12 |
--------------------------------------------------------------------------------
/dev-resources/root-object-schema.edn:
--------------------------------------------------------------------------------
1 | {:queries
2 | {:fred {:type String
3 | :resolve :queries/fred}}
4 | :objects
5 | {:Barney
6 | {:fields
7 | {:last_name {:type String
8 | :resolve :Barney/last-name}}}}
9 |
10 | :roots {:query :Barney}}
11 |
--------------------------------------------------------------------------------
/dev-resources/large-lists-schema.edn:
--------------------------------------------------------------------------------
1 | {:objects
2 | {:Item
3 | {:fields
4 | {:name {:type String}
5 | :age {:type Int}
6 | :id {:type Int}
7 | :city {:type String}}}}
8 |
9 | :queries
10 | {:list {:type (list :Item)
11 | :resolve :resolve-list}}}
12 |
--------------------------------------------------------------------------------
/dev-resources/parser/reserved-args.edn:
--------------------------------------------------------------------------------
1 | [{:type :query
2 | :selections [{:type :field
3 | :field-name :hello
4 | :args [{:arg-name :query
5 | :arg-value {:type :string
6 | :value "graphql"}}]}]}]
7 |
--------------------------------------------------------------------------------
/dev-resources/parser/named-fragment.gql:
--------------------------------------------------------------------------------
1 | query {
2 | order {
3 | ... InStoreFrag
4 | item_count
5 | ... OnlineFrag
6 | }
7 | }
8 |
9 | fragment InStoreFrag on InStore {
10 | store_number
11 | }
12 |
13 | fragment OnlineFrag on OnlineOrder {
14 | refer_code
15 | }
16 |
--------------------------------------------------------------------------------
/dev-resources/selection/scalar-types.sdl:
--------------------------------------------------------------------------------
1 | schema { query: Query }
2 |
3 | directive @auth (role: String!) on SCALAR
4 |
5 | scalar UUID @auth(role: "basic")
6 |
7 | type User {
8 | name: String!
9 | id: UUID
10 | }
11 |
12 | type Query {
13 | me: User
14 | }
15 |
16 |
--------------------------------------------------------------------------------
/docs/_examples/selects-field.edn:
--------------------------------------------------------------------------------
1 |
2 | (require
3 | '[com.walmartlabs.lacinia.executor :as executor])
4 |
5 | (defn resolve-hero
6 | [context args _]
7 | (if (executor/selects-field? context :Character/friends)
8 | (fetch-hero-with-friends args)
9 | (fetch-hero args)))
10 |
--------------------------------------------------------------------------------
/docs/_examples/tutorial/user-1.clj:
--------------------------------------------------------------------------------
1 | (ns user
2 | (:require
3 | [my.clojure-game-geek.schema :as s]
4 | [com.walmartlabs.lacinia :as lacinia]))
5 |
6 | (def schema (s/load-schema))
7 |
8 | (defn q
9 | [query-string]
10 | (lacinia/execute schema query-string nil nil))
11 |
--------------------------------------------------------------------------------
/dev-resources/deprecated-enums-schema.edn:
--------------------------------------------------------------------------------
1 | {:enums
2 | {:mood
3 | {:values [{:enum-value :GOOD
4 | :deprecated "Should use happy."}
5 | {:enum-value :HAPPY
6 | :description "Desired state."}
7 | {:enum-value :SAD
8 | :deprecated true}]}}}
9 |
--------------------------------------------------------------------------------
/dev-resources/dynamic-input-objects.edn:
--------------------------------------------------------------------------------
1 | {:input-objects
2 | {:Filter
3 | {:fields
4 | {:id {:type (non-null :ID)}
5 | :limit {:type (non-null :Int)}}}}
6 | :queries
7 | {:filter
8 | {:type String
9 | :resolve :queries/filter
10 | :args
11 | {:input {:type (non-null :Filter)}}}}}
--------------------------------------------------------------------------------
/dev-resources/root-object-with-conflicts-schema.edn:
--------------------------------------------------------------------------------
1 | {:queries
2 | {:last_name {:type String
3 | :resolve :queries/fred}}
4 | :objects
5 | {:Barney
6 | {:fields
7 | {:last_name {:type String
8 | :resolve :Barney/last-name}}}}
9 |
10 | :roots {:query :Barney}}
11 |
--------------------------------------------------------------------------------
/docs/_examples/subs-schema.edn:
--------------------------------------------------------------------------------
1 | {:objects
2 | {:LogEvent
3 | {:fields
4 | {:severity {:type String}
5 | :message {:type String}}}
6 |
7 | :Subscription
8 | {:fields
9 | {:logs
10 | {:type :LogEvent
11 | :args {:severity {:type String}}
12 | :stream :stream-logs}}}}}
13 |
--------------------------------------------------------------------------------
/dev-resources/mult-inheritance.sdl:
--------------------------------------------------------------------------------
1 | interface Node {
2 | id: ID!
3 | }
4 |
5 | interface Client {
6 | ip: String
7 | }
8 |
9 | type User implements Node & Client {
10 | id: ID!
11 | ip: String
12 | }
13 |
14 | type Query {
15 | me: User
16 | }
17 |
18 | schema {
19 | query: Query
20 | }
21 |
--------------------------------------------------------------------------------
/dev-resources/selection/enum-types.sdl:
--------------------------------------------------------------------------------
1 | directive @auth (role: String!) on ENUM
2 |
3 | schema { query: Query }
4 |
5 | enum Rank @auth(role: "enum") { INTRO, JUNIOR, SENIOR }
6 |
7 | type User {
8 | name: String!
9 | rank: Rank
10 | }
11 |
12 | type Query {
13 | me: User
14 | }
15 |
16 |
--------------------------------------------------------------------------------
/docs/_examples/query-def-var.edn:
--------------------------------------------------------------------------------
1 | {:objects
2 | {:Query
3 | {:fields
4 | {:hero
5 | {:type (non-null :Character)
6 | :args {:episode {:type :episode}}}
7 |
8 | :human
9 | {:type (non-null :Human)
10 | :args {:id {:type String
11 | :default-value "1001"}}}}}}}
12 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /target
2 | /classes
3 | /checkouts
4 | pom.xml
5 | pom.xml.asc
6 | *.jar
7 | *.class
8 | /.lein-*
9 | /.nrepl-port
10 | .hgignore
11 | .hg/
12 | *.iml
13 | .idea
14 | _build
15 | .DS_Store
16 | build.xml
17 | .Rapp.history
18 | .settings
19 | .project
20 | .classpath
21 | *.pom.asc
22 | .cpcache
23 |
--------------------------------------------------------------------------------
/dev-resources/input-object-schema.edn:
--------------------------------------------------------------------------------
1 | {:input-objects
2 | {:Filter
3 | {:fields
4 | {:terms {:type (list (non-null String))}
5 | :max_count {:type Int}}}}
6 |
7 | :queries
8 | {:search
9 | {:type (list String)
10 | :args
11 | {:filter {:type :Filter}}
12 | :resolve :queries/search}}}
13 |
--------------------------------------------------------------------------------
/dev-resources/basic-docs.md:
--------------------------------------------------------------------------------
1 | # Character
2 |
3 | A person who appears in one of the movies.
4 |
5 | Persons are more than humans, and may include droids and computers.
6 |
7 | ## Character/name
8 |
9 | The primary name of the character.
10 |
11 | For droids, this is the announced name, such as "AreToo".
12 |
13 |
--------------------------------------------------------------------------------
/dev-resources/object-scalars-schema.edn:
--------------------------------------------------------------------------------
1 | {:scalars
2 | {:Data
3 | {:description "Arbitrary JSON/EDN data."
4 | :parse :scalars/data.parse
5 | :serialize :scalars/data.serialize}}
6 |
7 | :queries
8 | {:echo {:type :Data
9 | :args {:input {:type :Data}}
10 | :resolve :queries/echo}}}
11 |
--------------------------------------------------------------------------------
/dev-resources/generic-scalar-errors-schema.edn:
--------------------------------------------------------------------------------
1 | {:objects
2 | {:Data
3 | {:fields
4 | {:value {:type Int}}}}
5 |
6 | :queries
7 | {:make_data
8 | {:type :Data
9 | :args {:value {:type Int}}
10 | :resolve :queries/make-data}
11 |
12 | :bad_data
13 | {:type :Data
14 | :resolve :queries/bad-data}}}
15 |
--------------------------------------------------------------------------------
/docs/_examples/fed/external.gql:
--------------------------------------------------------------------------------
1 | type User @extends @key(fields: "id") {
2 | id: String! @external
3 | favoriteProducts: [Product]
4 | }
5 |
6 | type Product @key(fields: "upc") {
7 | upc: String!
8 | name: String!
9 | price: Int!
10 | }
11 |
12 | type Query {
13 | productByUpc(upc: String!) : Product
14 | }
15 |
--------------------------------------------------------------------------------
/dev-resources/parser/nested-fields.edn:
--------------------------------------------------------------------------------
1 | [{:type :query
2 | :selections [{:type :field
3 | :field-name :hello
4 | :selections [{:type :field
5 | :field-name :world}
6 | {:type :field
7 | :field-name :timestamp}]}]}]
8 |
--------------------------------------------------------------------------------
/dev-resources/nested-non-nullable-fields-schema.edn:
--------------------------------------------------------------------------------
1 | {:input-objects
2 | {:game_template
3 | {:fields {:id {:type (non-null Int)}
4 | :name {:type String}}}}
5 |
6 | :mutations
7 | {:create_game
8 | {:type String
9 | :args
10 | {:game_data {:type :game_template}}
11 | :resolve :mutation/create-game}}}
12 |
--------------------------------------------------------------------------------
/dev-resources/selections-schema.edn:
--------------------------------------------------------------------------------
1 | {:objects
2 | {:Root
3 | {:fields
4 | {:tree {:type String}
5 | :detail {:type String
6 | :args
7 | {:int_arg {:type Int}
8 | :string_arg {:type String}}}}}}
9 | :queries
10 | {:get_root {:type :Root
11 | :resolve :root-resolve}}}
12 |
--------------------------------------------------------------------------------
/dev-resources/selection/object-type.sdl:
--------------------------------------------------------------------------------
1 | directive @auth (role: String!) on OBJECT | FIELD_DEFINITION
2 |
3 | schema { query: Query }
4 |
5 | type User @auth(role: "basic") {
6 | name: String! @auth(role: "advanced")
7 | department: String!
8 | }
9 |
10 | type Query {
11 | me: User
12 |
13 | friends: [User!]!
14 | }
15 |
16 |
--------------------------------------------------------------------------------
/dev-resources/mutable-context-schema.edn:
--------------------------------------------------------------------------------
1 | {:objects
2 | {:Root
3 | {:fields
4 | {:container {:type :Container}}}
5 | :Container
6 | {:fields
7 | {:id {:type String}
8 | :leaf {:type String
9 | :resolve :leaf}}}}
10 | :queries
11 | {:root
12 | {:type :Root
13 | :args {:trigger {:type Boolean}}
14 | :resolve :root}}}
15 |
--------------------------------------------------------------------------------
/dev-resources/parser/literals.gql:
--------------------------------------------------------------------------------
1 | {
2 | hello (i: 5
3 | f: 3.14
4 | s: "clojure"
5 | bs: """
6 | This is a block string indented
7 | four characters.
8 |
9 | This line is indented an extra two.
10 |
11 | Characters such as \t are not expanded.
12 | """
13 | b: false
14 | n: null)
15 | }
16 |
--------------------------------------------------------------------------------
/docs/_examples/overview-exec-query.edn:
--------------------------------------------------------------------------------
1 | (require
2 | '[com.walmartlabs.lacinia :refer [execute]]
3 | '[org.example.schema :refer [star-wars-schema]])
4 |
5 | (def compiled-schema (star-wars-schema))
6 |
7 | (execute compiled-schema
8 | "query { human(id: \"1001\") { name }}"
9 | nil nil)
10 | => {:data {:human #ordered/map([:name "Darth Vader"])}}
11 |
--------------------------------------------------------------------------------
/docs/_examples/directive-defs.edn:
--------------------------------------------------------------------------------
1 | {:directive-defs
2 | {:access
3 | {:locations #{:field-definition}
4 | :args {:role {:type (non-null String)}}}}
5 |
6 | :objects
7 | {:Query
8 | {:fields
9 | {:ultimateAnswer
10 | {:type String
11 | :directives [{:directive-type :access
12 | :directive-args {:role "deep-thought"}}]}}}}}
13 |
--------------------------------------------------------------------------------
/docs/_examples/enum-definition-description.edn:
--------------------------------------------------------------------------------
1 | {:enums
2 | {:Episode
3 | {:description "The episodes of the original Star Wars trilogy."
4 | :values [{:enum-value :NEWHOPE :description "The first one you saw."}
5 | {:enum-value :EMPIRE :description "The good one."}
6 | {:enum-value :JEDI :description "The one with the killer teddy bears."}]}}}
7 |
--------------------------------------------------------------------------------
/dev-resources/basic-vars.edn:
--------------------------------------------------------------------------------
1 | {:data
2 | {:default
3 | {:name "Darth Vader",
4 | :appears_in [:NEWHOPE :EMPIRE :JEDI],
5 | :friends [{:name "Wilhuff Tarkin"}],
6 | :home_planet "Tatooine"},
7 | :hope_hero
8 | {:id "2001",
9 | :name "R2-D2",
10 | :friends
11 | [{:name "Luke Skywalker"}
12 | {:name "Han Solo"}
13 | {:name "Leia Organa"}]}}}
14 |
--------------------------------------------------------------------------------
/dev-resources/deprecated-fields-schema.edn:
--------------------------------------------------------------------------------
1 | {:objects
2 | {:user
3 | {:fields
4 | {:title {:type String
5 | :deprecated true
6 | :description "Replaced by honorific."}
7 | :honorific {:type String}
8 | :nomdeplume {:type String
9 | :deprecated "Out of fashion."
10 | :description "Used by poets."}}}}}
11 |
--------------------------------------------------------------------------------
/dev-resources/parser/args.edn:
--------------------------------------------------------------------------------
1 | [{:type :query
2 | :selections [{:type :field
3 | :field-name :hello
4 | :args [{:arg-name :world
5 | :arg-value {:type :boolean
6 | :value "true"}}]
7 | :selections [{:type :field
8 | :field-name :greeting}]}]}]
9 |
--------------------------------------------------------------------------------
/dev-resources/custom-scalar-serialize-schema.edn:
--------------------------------------------------------------------------------
1 | {:scalars
2 | {:LimitedInt {:parse :parse
3 | :serialize :serialize}}
4 |
5 | :queries
6 | {:test {:type :LimitedInt
7 | :args {:in {:type (non-null Int)}}
8 | :resolve :test-query}
9 |
10 | :dupe {:type Int
11 | :args {:in {:type LimitedInt}}
12 | :resolve :test-query}}}
13 |
--------------------------------------------------------------------------------
/dev-resources/parser/reserved-words.edn:
--------------------------------------------------------------------------------
1 | [{:type :query
2 | :selections [{:type :field
3 | :alias :q
4 | :field-name :query}
5 | {:type :field
6 | :alias :m
7 | :field-name :mutation}
8 | {:type :field
9 | :alias :s
10 | :field-name :subscription}]}]
11 |
--------------------------------------------------------------------------------
/docs/_examples/interface-definition.edn:
--------------------------------------------------------------------------------
1 | {:interfaces
2 | {:Named
3 | {:fields {:name {:type String}}}}
4 |
5 | :objects
6 | {:Person
7 | {:implements [:Named]
8 | :fields {:name {:type String}
9 | :age {:type Int}}}
10 |
11 | :Business
12 | {:implements [:Named]
13 | :fields {:name {:type String}
14 | :employee_count {:type Int}}}}}
15 |
--------------------------------------------------------------------------------
/dev-resources/enum-default-value-with-transformer-schema.edn:
--------------------------------------------------------------------------------
1 | {:objects
2 | {:Country
3 | {:fields
4 | {:code {:type :CountryCode}}}}
5 | :enums
6 | {:CountryCode
7 | {:values [:US]}}
8 | :queries
9 | {:countryByCode
10 | {:type :Country
11 | :resolve :placeholder
12 | :args
13 | {:code {:type :CountryCode
14 | :default-value :country-codes/us}}}}}
15 |
--------------------------------------------------------------------------------
/dev-resources/selection/wrap-field-directive.sdl:
--------------------------------------------------------------------------------
1 | directive @changecase (mode: ChangeCaseMode = UPPER_CASE) on FIELD_DEFINITION
2 |
3 | enum ChangeCaseMode { UPPER_CASE LOWER_CASE AS_IS }
4 |
5 | type Query {
6 | echoUpper(input: String!) : String @changecase
7 | echoLower(input: String!) : String @changecase(mode: LOWER_CASE)
8 | echoAsIs(input: String!) : String @deprecated
9 | }
--------------------------------------------------------------------------------
/dev-resources/basic.edn:
--------------------------------------------------------------------------------
1 | {:data {:default {:name "Darth Vader",
2 | :appears_in [:NEWHOPE :EMPIRE :JEDI],
3 | :friends [{:name "Wilhuff Tarkin"}],
4 | :home_planet "Tatooine"},
5 | :hope_hero {:id "2001",
6 | :name "R2-D2",
7 | :friends [{:name "Luke Skywalker"} {:name "Han Solo"} {:name "Leia Organa"}]}}}
8 |
--------------------------------------------------------------------------------
/dev-resources/simple-federation.sdl:
--------------------------------------------------------------------------------
1 | type User @key(fields: "id") {
2 | id: Int!
3 | name: String!
4 | }
5 |
6 | type Query {
7 | user_by_id(id: Int!) : User
8 | }
9 |
10 | type Account @key(fields: "acct_number") {
11 | acct_number: String!
12 | name: String!
13 | }
14 |
15 | type Product @key(fields: "upc") @extends {
16 | upc: String! @external
17 | reviewed_by: User
18 | }
--------------------------------------------------------------------------------
/docs/_examples/tutorial/logback-test-1.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | %-5level %logger - %msg%n
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/codegen.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | set -x
4 |
5 | pip install antlr4-tools
6 |
7 | export ANTLR4_TOOLS_ANTLR_VERSION=4.13.2
8 |
9 | PACKAGE_NAME=com.walmartlabs.lacinia
10 | DIR=$(echo $PACKAGE_NAME | tr . /)
11 |
12 | antlr4 resources/$DIR/Graphql.g4 -o java/$DIR -Xexact-output-dir -package $PACKAGE_NAME
13 | antlr4 resources/$DIR/GraphqlSchema.g4 -o java/$DIR -Xexact-output-dir -package $PACKAGE_NAME
14 |
--------------------------------------------------------------------------------
/dev-resources/enum-parse-serialize.edn:
--------------------------------------------------------------------------------
1 | {:enums
2 | {:status
3 | {:values [:good :bad]}}
4 |
5 | :objects
6 | {:Result
7 | {:fields
8 | {:input {:type String}
9 | :output {:type :status}}}}
10 |
11 | :queries
12 | {:echo
13 | {:type :Result
14 | :args {:in {:type :status}}
15 | :resolve :queries/echo}
16 |
17 | :fail
18 | {:type :Result
19 | :resolve :queries/fail}}}
20 |
--------------------------------------------------------------------------------
/dev-resources/nullability.edn:
--------------------------------------------------------------------------------
1 | {:objects
2 | {:Employer
3 | {:fields
4 | {:id {:type (non-null ID)}
5 | :name {:type (non-null String)}}}
6 | :User
7 | {:fields
8 | {:id {:type (non-null ID)}
9 | :employer {:type :Employer}}}
10 | :Query
11 | {:fields
12 | {:user {:type (non-null :User)
13 | :args {:id {:type (non-null ID)}}
14 | :resolve :query/user}}}}}
--------------------------------------------------------------------------------
/docs/_examples/extension.edn:
--------------------------------------------------------------------------------
1 | (require '[com.walmartlabs.lacinia.resolve :refer [with-extensions]])
2 |
3 | (defn resolve-user
4 | [context args value]
5 | (let [start-ms (System/currentTimeMillis)
6 | user-data (get-user-data (:id args))
7 | elapsed-ms (- (System/currentTimeMillis) start-ms)]
8 | (with-extensions user-data
9 | update :total-db-access-ms (fnil + 0) elapsed-ms)))
10 |
--------------------------------------------------------------------------------
/docs/_examples/subs-streamer.edn:
--------------------------------------------------------------------------------
1 | (defn log-message-streamer
2 | [context args source-stream]
3 | ;; Create an object for the subscription.
4 | (let [subscription (create-log-subscription)]
5 | (on-publish subscription
6 | (fn [log-event]
7 | (-> log-event :payload source-stream)))
8 | ;; Return a function to cleanup the subscription
9 | #(stop-log-subscription subscription)))
10 |
--------------------------------------------------------------------------------
/docs/_examples/sample_schema.txt:
--------------------------------------------------------------------------------
1 | enum episode {
2 | NEWHOPE
3 | EMPIRE
4 | JEDI
5 | }
6 |
7 | type Character {
8 | name: String!
9 | episodes: [episode]
10 | }
11 |
12 | input CharacterArg {
13 | name: String!
14 | episodes: [episode]!
15 | }
16 |
17 | type Query {
18 | findAllInEpisode(episode: episode!) : [Character]
19 | }
20 |
21 | type Mutation {
22 | addCharacter(character: CharacterArg!) : Boolean
23 | }
24 |
--------------------------------------------------------------------------------
/docs/clojure.rst:
--------------------------------------------------------------------------------
1 | Clojure 1.9
2 | ===========
3 |
4 | Lacinia targets Clojure 1.9, and makes specific use of ``clojure.spec``.
5 |
6 | To use Lacinia with Clojure 1.8, modify your :file:`project.clj` to include ``clojure-future-spec``::
7 |
8 | :dependencies [[org.clojure/clojure "1.8.0"]
9 | [com.walmartlabs/lacinia "x.y.z"]
10 | [clojure-future-spec ""]
11 | ...]
12 |
13 |
--------------------------------------------------------------------------------
/dev-resources/subscription-selection.edn:
--------------------------------------------------------------------------------
1 | {:objects
2 | {:Instant
3 | {:fields {:hour {:type (non-null Int)}
4 | :minute {:type (non-null Int)}
5 | :second {:type (non-null Int)}}}
6 |
7 | :Subscription
8 | {:fields {:time_from
9 | {:type :Instant
10 | :args {:when {:type (non-null String)}
11 | :interval {:type Int
12 | :default-value 60}}}}}}}
--------------------------------------------------------------------------------
/dev-resources/typename-schema.edn:
--------------------------------------------------------------------------------
1 | {:objects
2 | {:man
3 | {:fields {:name {:type String}}}
4 |
5 | :machine
6 | {:fields {:serial {:type String}}}}
7 |
8 | :unions
9 | {:all {:members [:man :machine]}}
10 |
11 | :queries
12 | {:get_man
13 | {:type :man
14 | :resolve :get-man}
15 |
16 | :get_machine
17 | {:type :machine
18 | :resolve :get-machine}
19 |
20 | :get_all
21 | {:type (list :all)
22 | :resolve :get-all}}}
23 |
--------------------------------------------------------------------------------
/docs/_examples/mutable-context.edn:
--------------------------------------------------------------------------------
1 | (require '[com.walmartlabs.lacinia.resolve :as resolve])
2 |
3 | (defn resolve-products
4 | [_ args _]
5 | (let [search-term (:search args)]
6 | (-> (perform-product-search args)
7 | (resolve/with-context {::search-term search-term}))))
8 |
9 | (defn resolve-highlighted-name
10 | [context _ product]
11 | (let [{:keys [::search-term]} context]
12 | (-> product :name (add-highlight search-term))))
13 |
--------------------------------------------------------------------------------
/dev-resources/logback-test.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | %d{HHmmss.SSS} %-5level %logger{0} - %msg%n
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/docs/_examples/tutorial/logback-test-2.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | %-5level %logger - %msg%n
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/dev-resources/lists-schema.edn:
--------------------------------------------------------------------------------
1 | {:objects
2 | {:Container
3 | {:fields
4 | {:empty_list {:type (list String)}
5 | :non_empty_list {:type (list String)}
6 | :null_list {:type (list String)}
7 | :non_null_list {:type (non-null (list String))}}}}
8 |
9 | :queries
10 | {:container {:type :Container
11 | :args {:null {:type Boolean
12 | :default-value false}}
13 | :resolve :query/container}}}
14 |
--------------------------------------------------------------------------------
/dev-resources/selection/union-types.sdl:
--------------------------------------------------------------------------------
1 | directive @auth (role: String!) on OBJECT | UNION
2 |
3 | schema { query: Query }
4 |
5 | union User @auth(role: "basic") = ActiveUser | LegacyUser
6 |
7 | type LegacyUser @auth(role: "hidden") {
8 | userName: String!
9 | userId: Int!
10 | }
11 |
12 | type ActiveUser {
13 | name: String!
14 | idCode: String!
15 | }
16 |
17 | type Query {
18 | me: User
19 |
20 | friends: [User!]!
21 | }
22 |
23 |
--------------------------------------------------------------------------------
/docs/_examples/tutorial/system-1.clj:
--------------------------------------------------------------------------------
1 | (ns my.clojure-game-geek.system
2 | (:require [com.stuartsierra.component :as component]
3 | [my.clojure-game-geek.schema :as schema]
4 | [my.clojure-game-geek.server :as server]))
5 |
6 | (defn new-system
7 | []
8 | (assoc (component/system-map)
9 | :server (component/using (server/map->Server {})
10 | [:schema-provider])
11 | :schema-provider (schema/map->SchemaProvider {})))
12 |
--------------------------------------------------------------------------------
/dev-resources/union-query-root-schema.edn:
--------------------------------------------------------------------------------
1 | {:queries
2 | {:version {:type String
3 | :resolve :queries/version}}
4 |
5 | :objects
6 | {:Barney
7 | {:fields
8 | {:last_name {:type String
9 | :resolve :Barney/last-name}}}
10 |
11 | :Fred
12 | {:fields
13 | {:family_name {:type String
14 | :resolve :Fred/family-name}}}}
15 |
16 | :unions
17 | {:Queries {:members [:Fred :Barney]}}
18 |
19 | :roots {:query :Queries}}
20 |
--------------------------------------------------------------------------------
/dev-resources/parser/object.gql:
--------------------------------------------------------------------------------
1 | mutation {
2 | new_person(data: {last_name: "Phelps"
3 | first_name: "Fred"
4 | age: 39
5 | sex: male
6 | address: {
7 | street: "Hamburger Lane"
8 | city: "Wilsonville"
9 | state: "GE"
10 | zip: "87267"
11 | }
12 | notes: ["one" "two" "three"]})}
13 |
14 |
--------------------------------------------------------------------------------
/dev-resources/parser/variable-defaults.edn:
--------------------------------------------------------------------------------
1 | [{:type :query
2 | :vars [{:var-name :format
3 | :var-type {:type :root-type
4 | :type-name :GreetingFormat}
5 | :default {:type :enum
6 | :value :LONG}}]
7 | :selections [{:type :field
8 | :field-name :hello
9 | :args [{:arg-name :format
10 | :arg-value {:type :variable
11 | :value :format}}]}]}]
12 |
--------------------------------------------------------------------------------
/dev-resources/dyn-args-schema.edn:
--------------------------------------------------------------------------------
1 | {:queries
2 | {:cars {:type :CarCollection
3 | :args {:filter {:type :CarCollectionFilter}}}}
4 |
5 | :input-objects
6 | {:CarCollectionFilter
7 | {:fields {:or {:type (list :CarCollectionFilter)}
8 | :color {:type :StringFilter}}}
9 |
10 | :StringFilter
11 | {:fields {:equals {:type String}}}}
12 |
13 | :objects
14 | {:Car
15 | {:fields {:color {:type String}}}
16 |
17 | :CarCollection
18 | {:fields {:nodes {:type (list :Car)}}}}}
--------------------------------------------------------------------------------
/docs/_examples/union-definition.edn:
--------------------------------------------------------------------------------
1 | {:unions
2 | {:SearchResult
3 | {:members [:Person :Photo]}}
4 |
5 | :objects
6 | {:Person
7 | {:fields {:name {:type String}
8 | :age {:type Int}}}
9 |
10 | :Photo
11 | {:fields {:imageURL {:type String}
12 | :title {:type String}
13 | :height {:type Int}
14 | :width {:type Int}}}
15 |
16 | :Query
17 | {:fields
18 | {:search
19 | {:type (list :SearchResult)
20 | :args {:term String}}}}}}
21 |
--------------------------------------------------------------------------------
/dev-resources/selection/interface-types.sdl:
--------------------------------------------------------------------------------
1 | directive @auth (role: String!) on OBJECT | INTERFACE
2 |
3 | schema { query: Query }
4 |
5 | interface User @auth(role: "basic") {
6 | name: String!
7 | }
8 |
9 | type LegacyUser implements User @auth(role: "hidden") {
10 | name: String!
11 | userId: Int!
12 | }
13 |
14 | type ActiveUser implements User {
15 | name: String!
16 | userId: String!
17 | }
18 |
19 | type Query {
20 | me: User
21 |
22 | friends: [User!]!
23 | }
24 |
25 |
--------------------------------------------------------------------------------
/java/com/walmartlabs/lacinia/ParseError.java:
--------------------------------------------------------------------------------
1 | package com.walmartlabs.lacinia;
2 |
3 | import clojure.lang.IDeref;
4 |
5 | public class ParseError extends RuntimeException implements IDeref {
6 | public final Object errors;
7 | public final Object tree;
8 |
9 | public ParseError(final Object errors, final Object tree, final String msg) {
10 | super(msg);
11 | this.errors = errors;
12 | this.tree = tree;
13 | }
14 |
15 | public Object deref() {
16 | return errors;
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/docs/_examples/tutorial/build-1.clj:
--------------------------------------------------------------------------------
1 | (ns build
2 | (:refer-clojure :exclude [test])
3 | (:require [org.corfield.build :as bb]))
4 |
5 | (def lib 'net.clojars.my/clojure-game-geek)
6 | (def version "0.1.0-SNAPSHOT")
7 | (def main 'my.clojure-game-geek)
8 |
9 | (defn test "Run the tests." [opts]
10 | (bb/run-tests (assoc opts :aliases [:dev])))
11 |
12 | (defn ci "Run the CI pipeline of tests (and build the uberjar)." [opts]
13 | (-> opts
14 | (assoc :lib lib :version version :main main)
15 | (bb/run-tests)
16 | (bb/clean)
17 | (bb/uber)))
18 |
--------------------------------------------------------------------------------
/dev-resources/timing-schema.edn:
--------------------------------------------------------------------------------
1 | {:objects
2 | {:Fast
3 | {:fields {:simple {:type String}
4 | :slow {:type :Slow
5 | :resolve :resolve-slow}}}
6 | :Slow
7 | {:fields {:simple {:type String}}}}
8 |
9 | :queries
10 | {:root {:type (non-null Fast)
11 | :args {:delay {:type Int}
12 | :value {:type String
13 | :default-value "fast!"}
14 | :nested_value {:type String
15 | :default-value "slow!!"}}
16 | :resolve :resolve-fast}}}
17 |
--------------------------------------------------------------------------------
/dev-resources/non-string-default-value-schema.edn:
--------------------------------------------------------------------------------
1 | {:enums
2 | {:mood {:values [:happy :sad]}}
3 |
4 | :queries
5 | {:search
6 | {:type String
7 | :resolve :placeholder
8 | :args
9 | {:title {:type String
10 | :default-value "columbia"}
11 | :checked {:type (non-null Boolean)
12 | :default-value true}
13 | :count {:type Int
14 | :default-value 10}
15 | :mood {:type :mood
16 | :default-value :happy}
17 | :has_no_default {:type String}
18 | :target {:type Float
19 | :default-value 3.2}}}}}
20 |
--------------------------------------------------------------------------------
/dev-resources/parser/enum.edn:
--------------------------------------------------------------------------------
1 | [{:type :query
2 | :selections [{:type :field
3 | :field-name :orders
4 | :args [{:arg-name :sort
5 | :arg-value {:type :enum
6 | :value :descending}}]
7 | :selections [{:type :field
8 | :field-name :sales_date
9 | :args [{:arg-name :time_zone
10 | :arg-value {:type :enum
11 | :value :utc}}]}]}]}]
12 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright 2017 WalmartLabs
2 |
3 | Licensed under the Apache License, Version 2.0 (the "License");
4 | you may not use this file except in compliance with the License.
5 | You may obtain a copy of the License at
6 |
7 | http://www.apache.org/licenses/LICENSE-2.0
8 |
9 | Unless required by applicable law or agreed to in writing, software
10 | distributed under the License is distributed on an "AS IS" BASIS,
11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | See the License for the specific language governing permissions and
13 | limitations under the License.
--------------------------------------------------------------------------------
/dev-resources/parser/aliases.edn:
--------------------------------------------------------------------------------
1 | [{:type :query
2 | :selections [{:type :field
3 | :field-name :hello
4 | :alias :greet
5 | :selections [{:type :field
6 | :field-name :world
7 | :alias :w}
8 | {:type :field
9 | :field-name :longitude
10 | :alias :long}
11 | {:type :field
12 | :field-name :latitude
13 | :alias :lat}]}]}]
14 |
--------------------------------------------------------------------------------
/docs/_examples/object-tag.edn:
--------------------------------------------------------------------------------
1 | {:unions
2 | {:Searchable
3 | {:members [:Business :Employee]}}
4 |
5 | :objects
6 | {:Business
7 | {:fields
8 | {:id {:type ID}
9 | :name {:type String}}
10 | :tag com.example.data.Business}
11 |
12 | :Employee
13 | {:fields
14 | {:id {:type ID}
15 | :employer {:type :Business}
16 | :givenName {:type String}
17 | :familyName {:type String}}
18 | :tag com.example.data.Employee}
19 |
20 | :Query
21 | {:fields
22 | {:businesses
23 | {:type (list :Business)}
24 |
25 | :search
26 | {:type (list :Searchable)}}}}}
27 |
28 |
--------------------------------------------------------------------------------
/docs/_examples/tutorial/test_utils-1.clj:
--------------------------------------------------------------------------------
1 | (ns my.clojure-game-geek.test-utils
2 | (:require [clojure.walk :as walk])
3 | (:import (clojure.lang IPersistentMap)))
4 |
5 | (defn simplify
6 | "Converts all ordered maps nested within the map into standard hash maps, and
7 | sequences into vectors, which makes for easier constants in the tests, and eliminates ordering problems."
8 | [m]
9 | (walk/postwalk
10 | (fn [node]
11 | (cond
12 | (instance? IPersistentMap node)
13 | (into {} node)
14 |
15 | (seq? node)
16 | (vec node)
17 |
18 | :else
19 | node))
20 | m))
21 |
--------------------------------------------------------------------------------
/docs/_examples/async-example.edn:
--------------------------------------------------------------------------------
1 | (require
2 | '[com.walmartlabs.lacinia.resolve :as resolve])
3 |
4 | (defn ^:private get-user
5 | [connection user-id]
6 | ...)
7 |
8 | (defn resolve-user
9 | [context args _]
10 | (let [{:keys [id]} args
11 | {:keys [connection]} context
12 | result (resolve/resolve-promise)]
13 | (.start (Thread.
14 | #(try
15 | (resolve/deliver! result (get-user connection id))
16 | (catch Throwable t
17 | (resolve/deliver! result nil
18 | {:message (str "Exception: " (.getMessage t))})))))
19 |
20 | result))
21 |
--------------------------------------------------------------------------------
/docs/_examples/tutorial/system-2.clj:
--------------------------------------------------------------------------------
1 | (ns my.clojure-game-geek.system
2 | (:require [com.stuartsierra.component :as component]
3 | [my.clojure-game-geek.schema :as schema]
4 | [my.clojure-game-geek.server :as server]
5 | [my.clojure-game-geek.db :as db]))
6 |
7 | (defn new-system
8 | []
9 | (assoc (component/system-map)
10 | :db (db/map->ClojureGameGeekDb {})
11 | :server (component/using (server/map->Server {})
12 | [:schema-provider])
13 | :schema-provider (component/using
14 | (schema/map->SchemaProvider {})
15 | [:db])))
16 |
--------------------------------------------------------------------------------
/docs/_examples/tutorial/schema-0.clj:
--------------------------------------------------------------------------------
1 | (ns my.clojure-game-geek.schema
2 | "Contains custom resolvers and a function to provide the full schema."
3 | (:require [clojure.java.io :as io]
4 | [com.walmartlabs.lacinia.util :as util]
5 | [com.walmartlabs.lacinia.schema :as schema]
6 | [clojure.edn :as edn]))
7 |
8 | (defn resolver-map
9 | []
10 | {:Query/gameById (fn [context args value]
11 | nil)})
12 |
13 | (defn load-schema
14 | []
15 | (-> (io/resource "cgg-schema.edn")
16 | slurp
17 | edn/read-string
18 | (util/inject-resolvers (resolver-map))
19 | schema/compile))
20 |
--------------------------------------------------------------------------------
/dev-resources/org/example/schema.clj:
--------------------------------------------------------------------------------
1 | (ns org.example.schema
2 | (:require
3 | [clojure.edn :as edn]
4 | [clojure.java.io :as io]
5 | [com.walmartlabs.lacinia.schema :as schema]
6 | [com.walmartlabs.lacinia.util :as util]
7 | [org.example.db :as db]))
8 |
9 | (defn star-wars-schema
10 | []
11 | (-> (io/resource "star-wars-schema.edn")
12 | slurp
13 | edn/read-string
14 | (util/attach-resolvers {:hero db/resolve-hero
15 | :human db/resolve-human
16 | :droid db/resolve-droid
17 | :friends db/resolve-friends})
18 | schema/compile))
19 |
--------------------------------------------------------------------------------
/docs/_examples/tutorial/server-1.clj:
--------------------------------------------------------------------------------
1 | (ns my.clojure-game-geek.server
2 | (:require [com.stuartsierra.component :as component]
3 | [com.walmartlabs.lacinia.pedestal2 :as lp]
4 | [io.pedestal.http :as http]))
5 |
6 | (defrecord Server [schema-provider server]
7 |
8 | component/Lifecycle
9 |
10 | (start [this]
11 | (assoc this :server (-> schema-provider
12 | :schema
13 | (lp/default-service nil)
14 | http/create-server
15 | http/start)))
16 |
17 | (stop [this]
18 | (http/stop server)
19 | (assoc this :server nil)))
20 |
--------------------------------------------------------------------------------
/dev-resources/field-resolver-errors.edn:
--------------------------------------------------------------------------------
1 | {:enums
2 | {:Color {:values [RED GREEN BLUE]}}
3 | :objects
4 | {:MyObject
5 | {:fields
6 | {:error_field
7 | {:type String
8 | :resolve :single-error}
9 |
10 | :color {:type :Color
11 | :resolve :color}
12 | :exception
13 | {:type String
14 | :args
15 | {:range {:type Int}}
16 | :resolve :exception}
17 |
18 | :with_extensions
19 | {:type String
20 | :resolve :with-extensions}
21 |
22 | :multiple_errors_field
23 | {:type String
24 | :resolve :multiple-errors}}}}
25 |
26 | :queries
27 | {:root {:type :MyObject
28 | :resolve :resolve-root}}}
29 |
--------------------------------------------------------------------------------
/docs/_examples/resolve-method.edn:
--------------------------------------------------------------------------------
1 | (defn- make-accessor
2 | [method-sym]
3 | (let [method-name (name method-sym)
4 | arg-types (make-array Class 0)
5 | args (make-array Object 0)]
6 | (fn [value]
7 | (let [c (class value)
8 | method (.getMethod c method-name arg-types)]
9 | (.invoke method value args)))))
10 |
11 | (defn resolve-method
12 | [method-sym]
13 | (let [f (make-accessor method-sym)]
14 | (fn [_ _ value]
15 | (f value))))
16 |
17 | ;; Later, when injecting resolvers ...
18 |
19 | (-> ...
20 | (utils/inject-resolvers {:MyObject/myField (resolve-method 'myField)})
21 | (schema/compile))
22 |
--------------------------------------------------------------------------------
/docs/_examples/tutorial/server-2.clj:
--------------------------------------------------------------------------------
1 | (ns my.clojure-game-geek.server
2 | (:require [com.stuartsierra.component :as component]
3 | [com.walmartlabs.lacinia.pedestal2 :as lp]
4 | [io.pedestal.http :as http]))
5 |
6 | (defrecord Server [schema-provider server port]
7 |
8 | component/Lifecycle
9 |
10 | (start [this]
11 | (assoc this :server (-> schema-provider
12 | :schema
13 | (lp/default-service {:port port})
14 | http/create-server
15 | http/start)))
16 |
17 | (stop [this]
18 | (http/stop server)
19 | (assoc this :server nil)))
20 |
--------------------------------------------------------------------------------
/dev-resources/subscriptions-schema.edn:
--------------------------------------------------------------------------------
1 | {:directive-defs
2 | {:instrument {:locations #{:field-definition}}}
3 |
4 | :objects
5 | {:LogEvent
6 | {:fields
7 | {:severity {:type String}
8 | :message {:type String}}}}
9 |
10 | :subscriptions
11 | {:logs
12 | {:type :LogEvent
13 | :args {:severity {:type String}
14 | :fakeError {:type Boolean
15 | :default false}}
16 | :stream :stream-logs}
17 |
18 | :directive_logs
19 | {:type :LogEvent
20 | :directives [{:directive-type :instrument}]
21 | :args {:severity {:type String}
22 | :fakeError {:type Boolean
23 | :default false}}
24 | :stream :stream-logs}}}
25 |
--------------------------------------------------------------------------------
/dev-resources/parser/frag-spread.edn:
--------------------------------------------------------------------------------
1 | [{:type :query
2 | :selections [{:type :field
3 | :field-name :order
4 | :selections [{:type :field
5 | :field-name :id}
6 | {:type :inline-fragment
7 | :on-type :InStore
8 | :selections [{:field-name :store_number
9 | :type :field}]}
10 | {:type :inline-fragment
11 | :on-type :OnlineOrder
12 | :selections [{:field-name :refer_code
13 | :type :field}]}]}]}]
14 |
--------------------------------------------------------------------------------
/dev-resources/parser/enum-reserved.edn:
--------------------------------------------------------------------------------
1 | [{:type :query
2 | :selections [{:type :field
3 | :field-name :introspect
4 | :args [{:arg-name :types
5 | :arg-value {:type :array
6 | :value [{:type :enum
7 | :value :query}
8 | {:type :enum
9 | :value :mutation}
10 | {:type :enum
11 | :value :subscription}
12 | {:type :enum
13 | :value :other}]}}]}]}]
14 |
--------------------------------------------------------------------------------
/docs/_examples/tutorial/cgg-data-1.edn:
--------------------------------------------------------------------------------
1 | {:games
2 | [{:id "1234"
3 | :name "Zertz"
4 | :summary "Two player abstract with forced moves and shrinking board"
5 | :minPlayers 2
6 | :maxPlayers 2}
7 | {:id "1235"
8 | :name "Dominion"
9 | :summary "Created the deck-building genre; zillions of expansions"
10 | :minPlayers 2}
11 | {:id "1236"
12 | :name "Tiny Epic Galaxies"
13 | :summary "Fast dice-based sci-fi space game with a bit of chaos"
14 | :minPlayers 1
15 | :maxPlayers 4}
16 | {:id "1237"
17 | :name "7 Wonders: Duel"
18 | :summary "Tense, quick card game of developing civilizations"
19 | :minPlayers 2
20 | :maxPlayers 2}]}
21 |
--------------------------------------------------------------------------------
/dev-resources/fragments-schema.edn:
--------------------------------------------------------------------------------
1 | {:interfaces
2 | {:character
3 | {:fields
4 | {:name {:type String}
5 | :friends {:type (list :character)}}}}
6 |
7 | :objects
8 | {:droid
9 | {:implements [:character]
10 | :fields {:name {:type String}
11 | :power {:type String}
12 | :friends {:type (list :character)
13 | :resolve :resolve-friends }}}
14 |
15 | :human
16 | {:implements [:character]
17 | :fields {:name {:type String}
18 | :friends {:type (list :character)
19 | :resolve :resolve-friends}
20 | :home_world {:type String}}}}
21 |
22 | :queries
23 | {:characters
24 | {:type (list :character)
25 | :resolve :resolve-characters}}}
26 |
--------------------------------------------------------------------------------
/java/com/walmartlabs/lacinia/Graphql.tokens:
--------------------------------------------------------------------------------
1 | T__0=1
2 | T__1=2
3 | T__2=3
4 | T__3=4
5 | T__4=5
6 | T__5=6
7 | T__6=7
8 | T__7=8
9 | T__8=9
10 | T__9=10
11 | T__10=11
12 | T__11=12
13 | T__12=13
14 | T__13=14
15 | T__14=15
16 | T__15=16
17 | T__16=17
18 | BooleanValue=18
19 | NullValue=19
20 | Null=20
21 | NameId=21
22 | IntValue=22
23 | FloatValue=23
24 | Sign=24
25 | IntegerPart=25
26 | NonZeroDigit=26
27 | ExponentPart=27
28 | Digit=28
29 | StringValue=29
30 | BlockStringValue=30
31 | Ignored=31
32 | 'query'=1
33 | 'mutation'=2
34 | 'subscription'=3
35 | '('=4
36 | ')'=5
37 | ':'=6
38 | '$'=7
39 | '='=8
40 | '{'=9
41 | '}'=10
42 | '...'=11
43 | 'on'=12
44 | 'fragment'=13
45 | '['=14
46 | ']'=15
47 | '@'=16
48 | '!'=17
49 | 'null'=20
50 | '-'=24
51 |
--------------------------------------------------------------------------------
/java/com/walmartlabs/lacinia/GraphqlLexer.tokens:
--------------------------------------------------------------------------------
1 | T__0=1
2 | T__1=2
3 | T__2=3
4 | T__3=4
5 | T__4=5
6 | T__5=6
7 | T__6=7
8 | T__7=8
9 | T__8=9
10 | T__9=10
11 | T__10=11
12 | T__11=12
13 | T__12=13
14 | T__13=14
15 | T__14=15
16 | T__15=16
17 | T__16=17
18 | BooleanValue=18
19 | NullValue=19
20 | Null=20
21 | NameId=21
22 | IntValue=22
23 | FloatValue=23
24 | Sign=24
25 | IntegerPart=25
26 | NonZeroDigit=26
27 | ExponentPart=27
28 | Digit=28
29 | StringValue=29
30 | BlockStringValue=30
31 | Ignored=31
32 | 'query'=1
33 | 'mutation'=2
34 | 'subscription'=3
35 | '('=4
36 | ')'=5
37 | ':'=6
38 | '$'=7
39 | '='=8
40 | '{'=9
41 | '}'=10
42 | '...'=11
43 | 'on'=12
44 | 'fragment'=13
45 | '['=14
46 | ']'=15
47 | '@'=16
48 | '!'=17
49 | 'null'=20
50 | '-'=24
51 |
--------------------------------------------------------------------------------
/docs/_examples/compile-schema.clj:
--------------------------------------------------------------------------------
1 | (ns org.example.schema
2 | (:require
3 | [clojure.edn :as edn]
4 | [clojure.java.io :as io]
5 | [com.walmartlabs.lacinia.schema :as schema]
6 | [com.walmartlabs.lacinia.util :as util]
7 | [org.example.db :as db]))
8 |
9 | (defn star-wars-schema
10 | []
11 | (-> (io/resource "star-wars-schema.edn")
12 | slurp
13 | edn/read-string
14 | (util/inject-resolvers {:Query/hero db/resolve-hero
15 | :Query/human db/resolve-human
16 | :Query/droid db/resolve-droid
17 | :Human/friends db/resolve-friends
18 | :Droid/friends db/resolve-friends})
19 | schema/compile))
20 |
21 |
--------------------------------------------------------------------------------
/dev-resources/select-type-schema.edn:
--------------------------------------------------------------------------------
1 | {:interfaces
2 | {:ItemsContainer
3 | {:fields
4 | {:items {:type (non-null (list (non-null :Item)))}}}}
5 |
6 | :objects
7 | {:Order
8 | {:implements [:ItemsContainer]
9 | :fields
10 | {:id {:type (non-null String)}
11 | :items {:type (non-null (list (non-null :Item)))}}}
12 |
13 | :Item
14 | {:fields
15 | {:quantity {:type (non-null Int)}
16 | :description {:type String}
17 | :product {:type (non-null Product)}}}
18 |
19 | :Product
20 | {:fields
21 | {:name {:type (non-null String)}}
22 | :upc {:type (non-null String)}}
23 |
24 | :Query
25 | {:fields
26 | {:order {:type :Order
27 | :args {:id {:type (non-null String)}
28 | :search {:type String}}}}}}}
--------------------------------------------------------------------------------
/docs/subscriptions/resolver.rst:
--------------------------------------------------------------------------------
1 | Resolver
2 | ========
3 |
4 | Unlike a query or mutation, the :doc:`field resolver <../resolve/index>`
5 | for a subscription always starts with a specific value, provided by the streamer, via
6 | the source stream callback.
7 |
8 | Because of this, the resolver is optional: if not provided, a default resolver is used, one that simply returns
9 | the source stream value.
10 |
11 | However, it still makes sense to implement a resolver in some cases.
12 |
13 | Both the resolver and the streamer receive the same map of arguments: it is reasonable that some
14 | may be used by the streamer (for example, to filter which values go into the source stream),
15 | and some by the resolver (to control the selections on the source value).
16 |
17 |
--------------------------------------------------------------------------------
/docs/_examples/tutorial/system-3.clj:
--------------------------------------------------------------------------------
1 | (ns my.clojure-game-geek.system
2 | (:require [com.stuartsierra.component :as component]
3 | [my.clojure-game-geek.schema :as schema]
4 | [my.clojure-game-geek.server :as server]
5 | [my.clojure-game-geek.db :as db]))
6 |
7 | (defn new-system
8 | ([]
9 | (new-system nil))
10 | ([opts]
11 | (let [{:keys [port]
12 | :or {port 8888}} opts]
13 | (assoc (component/system-map)
14 | :db (db/map->ClojureGameGeekDb {})
15 | :server (component/using (server/map->Server {:port port})
16 | [:schema-provider])
17 | :schema-provider (component/using
18 | (schema/map->SchemaProvider {})
19 | [:db])))))
20 |
--------------------------------------------------------------------------------
/dev-resources/parser/named-fragment.edn:
--------------------------------------------------------------------------------
1 | [{:type :query
2 | :selections [{:type :field
3 | :field-name :order
4 | :selections [{:type :named-fragment
5 | :fragment-name :InStoreFrag}
6 | {:type :field
7 | :field-name :item_count}
8 | {:type :named-fragment
9 | :fragment-name :OnlineFrag}]}]}
10 | {:type :fragment-definition
11 | :fragment-name :InStoreFrag
12 | :on-type :InStore
13 | :selections [{:field-name :store_number
14 | :type :field}]}
15 | {:type :fragment-definition
16 | :fragment-name :OnlineFrag
17 | :on-type :OnlineOrder
18 | :selections [{:field-name :refer_code
19 | :type :field}]}]
20 |
--------------------------------------------------------------------------------
/dev-resources/parser/vars.edn:
--------------------------------------------------------------------------------
1 | [{:type :query
2 | :vars [{:var-name :id
3 | :var-type {:type :non-null
4 | :of-type {:type :root-type
5 | :type-name :Int}}}
6 | {:var-name :terms
7 | :var-type {:type :list
8 | :of-type {:type :non-null
9 | :of-type {:type :root-type
10 | :type-name :String}}}}
11 | {:var-name :casesensitive
12 | :var-type {:type :root-type
13 | :type-name :Boolean}}]
14 | :selections [{:type :field
15 | :field-name :order
16 | :args [{:arg-name :order_id
17 | :arg-value {:type :variable
18 | :value :id}}]}]}]
19 |
--------------------------------------------------------------------------------
/docs/federation/internal.rst:
--------------------------------------------------------------------------------
1 | Internal Entities
2 | =================
3 |
4 | Defining an entity that is internal is quite straight-forward, it is almost the same as in
5 | traditional GraphQL.
6 |
7 | .. sidebar:: Examples
8 |
9 | Here, and in the remaining examples, we've simplified the example from
10 | Apollo GraphQL's documentation to just two services:
11 | users and products.
12 |
13 | .. literalinclude:: /_examples/fed/internal.gql
14 |
15 | When federation is enabled, a
16 | `number of new directives `_ are automatically available,
17 | including ``@key``, which defines the primary key, or primary keys, for the entity.
18 |
19 | The above example would be the schema for a users service that can be extended by other services.
20 |
--------------------------------------------------------------------------------
/docs/_examples/invoke-streamer.edn:
--------------------------------------------------------------------------------
1 | (require
2 | '[com.walmartlabs.lacinia.executor :as executor]
3 | [com.walmartlabs.lacinia.parser :as parser]
4 | [com.walmartlabs.lacinia.constants :as constants])
5 |
6 | (let [prepared-query (-> schema
7 | (parser/parse-query query)
8 | (parser/prepare-with-query-variables variables))
9 | source-stream-callback (fn [data]
10 | ;; Do something with the data
11 | ;; e.g. send it to a websocket client
12 | )
13 | cleanup-fn (executor/invoke-streamer
14 | {constants/parsed-query-key prepared-query} source-stream-callback)]
15 | ;; Do something with the cleanup-fn e.g. call it when a websocket connection is closed
16 | )
17 |
--------------------------------------------------------------------------------
/dev-resources/fixes-types-schema.edn:
--------------------------------------------------------------------------------
1 | {:unions
2 | {:Characters
3 | {:members [:Human :Droid]}}
4 | :objects
5 | {:Human
6 | {:fields
7 | {:first_name {:type String
8 | :resolve [:prop :firstName]}
9 | :last_name {:type String
10 | :resolve [:prop :lastName]}}}
11 |
12 | :Droid
13 | {:fields
14 | {:designation {:type String
15 | :resolve [:prop :designation]}
16 | :function {:type String
17 | :resolve [:prop :function]}}}}
18 |
19 | :queries
20 | {:human
21 | {:type :Human
22 | :resolve :resolve-human}
23 |
24 | :droid
25 | {:type :Droid
26 | :resolve :resolve-droid}
27 |
28 | :astromech
29 | {:type :Droid
30 | :resolve :resolve-astromech}
31 |
32 | :characters
33 | {:type (list :Characters)
34 | :resolve :resolve-characters}}}
35 |
--------------------------------------------------------------------------------
/dev-resources/input-object-default-value-schema.edn:
--------------------------------------------------------------------------------
1 | {:enums
2 | {:mood {:values [:happy :sad]}}
3 |
4 | :input-objects
5 | {:Filter
6 | {:fields
7 | {:title {:type String
8 | :default-value "columbia"}
9 | :checked {:type Boolean
10 | :default-value true}
11 | :count {:type Int
12 | :default-value 10}
13 | :mood {:type :mood
14 | :default-value :happy}
15 | :has_no_default {:type String}
16 | :target {:type Float
17 | :default-value 3.2}}}}
18 |
19 | :queries
20 | {:search
21 | {:type String
22 | :resolve :placeholder
23 | :args
24 | {:filter {:type :Filter
25 | :default-value {:title "gorge"
26 | :checked false
27 | :count 20
28 | :target 3.14}}}}}}
29 |
--------------------------------------------------------------------------------
/docs/subscriptions/index.rst:
--------------------------------------------------------------------------------
1 | Subscriptions
2 | =============
3 |
4 | Subscriptions are GraphQL's approach to server-side push.
5 | The description is a bit abstract, as the specification keeps all options open on how
6 | subscriptions are to be implemented.
7 |
8 | .. sidebar:: GraphQL Spec
9 |
10 | Read about :spec:`subscriptions `.
11 |
12 | With subscriptions, a client can establish a long-lived connection to a server, and
13 | will receive new data on the connection as it becomes available to the server.
14 |
15 | Common use cases for subscriptions are updating a conversation page as new messages are added,
16 | updating a dashboard as interesting events about a system occur, or monitoring the progress
17 | of some long-lived process.
18 |
19 | .. toctree::
20 | :hidden:
21 |
22 | overview
23 | streamer
24 | resolver
25 |
--------------------------------------------------------------------------------
/docs/_examples/tutorial/user-2.clj:
--------------------------------------------------------------------------------
1 | (ns user
2 | (:require [my.clojure-game-geek.schema :as s]
3 | [com.walmartlabs.lacinia :as lacinia]
4 | [clojure.walk :as walk])
5 | (:import (clojure.lang IPersistentMap)))
6 |
7 | (def schema (s/load-schema))
8 |
9 | (defn simplify
10 | "Converts all ordered maps nested within the map into standard hash maps, and
11 | sequences into vectors, which makes for easier constants in the tests, and eliminates ordering problems."
12 | [m]
13 | (walk/postwalk
14 | (fn [node]
15 | (cond
16 | (instance? IPersistentMap node)
17 | (into {} node)
18 |
19 | (seq? node)
20 | (vec node)
21 |
22 | :else
23 | node))
24 | m))
25 |
26 | (defn q
27 | [query-string]
28 | (-> (lacinia/execute schema query-string nil nil)
29 | simplify))
30 |
--------------------------------------------------------------------------------
/docs/resolve/exceptions.rst:
--------------------------------------------------------------------------------
1 | Exceptions
2 | ==========
3 |
4 | Field resolvers are responsible for catching any exceptions that occur.
5 |
6 | Uncaught exceptions are *not* converted to errors; they are caught, wrapped in a new
7 | exception to identify the field name, field arguments, query path, and query location, but
8 | then allowed to bubble up out of Lacinia entirely.
9 |
10 | This is not desirable: better to return a partial result along with errors.
11 |
12 | Field resolvers should catch exceptions and use :doc:`ResolverResults `
13 | to communicate errors back to Lacinia for inclusion in the ``:errors`` key of the result.
14 |
15 | Failure to catch exceptions is even more damaging when using :doc:`async field resolvers `,
16 | as this can cause query execution to entirely halt, due to resolver result promises never being
17 | delivered.
18 |
--------------------------------------------------------------------------------
/docs/_examples/tutorial/deps-1.edn:
--------------------------------------------------------------------------------
1 | {:paths ["src" "resources"]
2 | :deps {org.clojure/clojure {:mvn/version "1.11.1"}}
3 | :aliases
4 | {:run-m {:main-opts ["-m" "my.clojure-game-geek"]}
5 | :run-x {:ns-default my.clojure-game-geek
6 | :exec-fn greet
7 | :exec-args {:name "Clojure"}}
8 | :build {:deps {io.github.seancorfield/build-clj
9 | {:git/tag "v0.8.2" :git/sha "0ffdb4c"
10 | ;; since we're building an app uberjar, we do not
11 | ;; need deps-deploy for clojars.org deployment:
12 | :deps/root "slim"}}
13 | :ns-default build}
14 | :test {:extra-paths ["test"]
15 | :extra-deps {org.clojure/test.check {:mvn/version "1.1.1"}
16 | io.github.cognitect-labs/test-runner
17 | {:git/tag "v0.5.0" :git/sha "48c3c67"}}}}}
18 |
--------------------------------------------------------------------------------
/src/com/walmartlabs/lacinia/describe.clj:
--------------------------------------------------------------------------------
1 | ; Copyright (c) 2021-present Walmart, Inc.
2 | ;
3 | ; Licensed under the Apache License, Version 2.0 (the "License")
4 | ; you may not use this file except in compliance with the License.
5 | ; You may obtain a copy of the License at
6 | ;
7 | ; http://www.apache.org/licenses/LICENSE-2.0
8 | ;
9 | ; Unless required by applicable law or agreed to in writing, software
10 | ; distributed under the License is distributed on an "AS IS" BASIS,
11 | ; WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | ; See the License for the specific language governing permissions and
13 | ; limitations under the License.
14 | ;
15 | (ns ^:no-doc com.walmartlabs.lacinia.describe)
16 |
17 | (defprotocol Describe
18 |
19 | "Describe an object, usually for inclusion as part of an error message."
20 |
21 | (description-for [_]))
--------------------------------------------------------------------------------
/docs/resolve/field-resolver-protocol.rst:
--------------------------------------------------------------------------------
1 | FieldResolver Protocol
2 | ======================
3 |
4 | In the majority of cases, a field resolver is simply a function that accepts the three parameters:
5 | context, args, and value.
6 |
7 | However, when structuring large systems using
8 | `Component `_ (or any similar approach), this can be inconvienient, as it
9 | does not make it possible to structure field resolvers as components.
10 |
11 | The :api:`resolve/FieldResolver` protocol addresses this: it defines a single method, ``resolve-value``.
12 | This method is the analog of an ordinary field resolver function.
13 |
14 | The support for this protocol is baked directly into
15 | :api:`schema/compile`.
16 |
17 | Just like field resolver functions, FieldResolver instances can resolve a value directly by
18 | returning it, or can return a ResolverResult.
19 |
--------------------------------------------------------------------------------
/dev-resources/user.clj:
--------------------------------------------------------------------------------
1 | (ns user
2 | (:require
3 | [criterium.core :as c]
4 | [clojure.java.io :as io]
5 | com.walmartlabs.lacinia.expound
6 | [com.walmartlabs.lacinia.schema :as schema]
7 | [com.walmartlabs.lacinia :as l]
8 | [clojure.spec.alpha :as s]
9 | [net.lewisship.trace :as trace]
10 | [expound.alpha :as expound]))
11 |
12 | (require 'com.walmartlabs.lacinia.expound)
13 |
14 | (alter-var-root #'s/*explain-out* (constantly expound/printer))
15 |
16 |
17 | (defn trace-demo
18 | []
19 | (->> (range 10)
20 | (trace/trace>> :values %)
21 | (filter even?)
22 | (take 2)))
23 |
24 | (comment
25 | (do
26 | (set! *warn-on-reflection* true)
27 | (trace/setup-default)
28 | (trace/trace :msg "Tracing is enabled"))
29 |
30 | (trace-demo)
31 |
32 | (trace/set-enable-trace! false)
33 | (trace/set-compile-trace! false)
34 |
35 | )
36 |
37 |
--------------------------------------------------------------------------------
/dev-resources/parser/fragment-directives.edn:
--------------------------------------------------------------------------------
1 | [{:type :query
2 | :selections [{:type :field
3 | :field-name :order
4 | :selections [{:type :inline-fragment
5 | :on-type :InStore
6 | :directives [{:directive-name :Secure}]
7 | :selections [{:type :field
8 | :field-name :store_number}]}
9 | {:type :field
10 | :field-name :item_count}
11 | {:type :named-fragment
12 | :fragment-name :OnlineFrag
13 | :directives [{:directive-name :InternalOnly}]}]}]}
14 | {:type :fragment-definition
15 | :fragment-name :OnlineFrag
16 | :on-type :OnlineOrder
17 | :selections [{:field-name :refer_code
18 | :type :field}]}]
19 |
--------------------------------------------------------------------------------
/dev-resources/parser/field-directive.edn:
--------------------------------------------------------------------------------
1 | [{:type :query
2 | :vars [{:var-name :internal
3 | :var-type {:of-type {:type :root-type
4 | :type-name :Boolean}
5 | :type :non-null}}]
6 | :selections [{:field-name :order
7 | :type :field
8 | :selections [{:type :field
9 | :field-name :id}
10 | {:type :field
11 | :field-name :customer_id
12 | :directives [{:directive-name :If
13 | :args [{:arg-name :test
14 | :arg-value {:type :variable
15 | :value :internal}}]}]}
16 | {:type :field
17 | :field-name :date}]}]}]
18 |
--------------------------------------------------------------------------------
/dev-resources/parser/arrays.edn:
--------------------------------------------------------------------------------
1 | [{:type :query
2 | :selections [{:type :field
3 | :field-name :hello
4 | :args [{:arg-name :i
5 | :arg-value {:type :array
6 | :value [{:type :integer
7 | :value "3"}
8 | {:type :integer
9 | :value "4"}
10 | {:type :integer
11 | :value "5"}]}}
12 | {:arg-name :s
13 | :arg-value {:type :array
14 | :value [{:type :string
15 | :value "hello"}
16 | {:type :string
17 | :value "goodbye"}]}}]}]}]
18 |
--------------------------------------------------------------------------------
/docs/tutorial/index.rst:
--------------------------------------------------------------------------------
1 | Tutorial
2 | ========
3 |
4 | .. toctree::
5 | :hidden:
6 |
7 | prereqs
8 | domain
9 | create-project
10 | init-schema
11 | game-data
12 | designer-data
13 | pedestal
14 | component
15 | member-ratings
16 | mutable-database
17 | rating-mutation
18 | database-1
19 | testing-1
20 | database-2
21 |
22 | wrap-up
23 |
24 | Our goal with this tutorial is to build up the essentials of a full application
25 | implemented in Lacinia, starting from nothing.
26 |
27 | Unlike many of the snippets used elsewhere in the Lacinia documentation, this will be something
28 | you can fork and experiment with yourself.
29 |
30 | Along the way, we hope you'll learn quite a bit about not just Lacinia and GraphQL,
31 | but about building Clojure applications in general.
32 |
33 | You can pull down the full source for the tutorial from GitHub: https://github.com/walmartlabs/clojure-game-geek
34 |
--------------------------------------------------------------------------------
/docs/input-objects.rst:
--------------------------------------------------------------------------------
1 | Input Objects
2 | =============
3 |
4 | .. sidebar:: GraphQL Spec
5 |
6 | Read about :spec:`input objects `.
7 |
8 | In some cases, it is desirable for a query to include arguments with more complex data than a single value.
9 | A typical example would be passing a bundle of values as part of a
10 | :doc:`mutation ` operation (rather than an unmanageable number of individual field arguments).
11 |
12 | Input objects are defined like ordinary objects, with a number of restrictions:
13 |
14 | - Field types are limited to scalars, enums, and input objects (or list and non-null wrappers around
15 | those types)
16 | - There are no field resolvers for input objects; these are values passed in their entirety from
17 | the client to the server
18 | - Input objects do not implement interfaces
19 |
20 | Input objects are defined as keys of the top-level ``:input-objects`` key in the schema.
21 |
22 |
--------------------------------------------------------------------------------
/docs/_exts/api_link.py:
--------------------------------------------------------------------------------
1 | from docutils import nodes, utils
2 |
3 |
4 | def api_link_inner(baseurl, rawtext, text, options):
5 |
6 | package, varname = text.strip().split( "/")
7 |
8 | if package == '':
9 | package = 'com.walmartlabs.lacinia'
10 | else:
11 | package = 'com.walmartlabs.lacinia.' + package
12 |
13 | ref = '%s/%s.html#var-%s' % (baseurl, package, varname)
14 | title = '%s/%s' % (package, varname)
15 | node = nodes.reference(rawtext, utils.unescape(title), refuri=ref, **options)
16 |
17 | return [node], []
18 |
19 |
20 | def api_link_role(role, rawtext, text, lineno, inliner, options={}, content=[]):
21 |
22 | return api_link_inner('http://walmartlabs.github.io/apidocs/lacinia', rawtext, text, options)
23 |
24 | def setup(app):
25 | app.add_role('api', api_link_role)
26 |
27 | return {
28 | 'version': '0.1',
29 | 'parallel_read_safe': True,
30 | 'parallel_write_safe': True,
31 | }
32 |
--------------------------------------------------------------------------------
/docs/_examples/tutorial/cgg-schema-2.edn:
--------------------------------------------------------------------------------
1 | {:objects
2 | {:BoardGame
3 | {:description "A physical or virtual board game."
4 | :fields
5 | {:id {:type ID}
6 | :name {:type (non-null String)}
7 | :summary {:type String
8 | :description "A one-line summary of the game."}
9 | :description {:type String
10 | :description "A long-form description of the game."}
11 | :min_players {:type Int
12 | :description "The minimum number of players the game supports."}
13 | :max_players {:type Int
14 | :description "The maximum number of players the game supports."}
15 | :play_time {:type Int
16 | :description "Play time, in minutes, for a typical game."}}}}
17 |
18 | :queries
19 | {:game_by_id
20 | {:type :BoardGame
21 | :description "Access a BoardGame by its unique id, if it exists."
22 | :args
23 | {:id {:type ID}}
24 | :resolve :query/game-by-id}}}
25 |
--------------------------------------------------------------------------------
/docs/_examples/tutorial/deps-2.edn:
--------------------------------------------------------------------------------
1 | {:paths ["src" "resources"]
2 | :deps {org.clojure/clojure {:mvn/version "1.11.1"}
3 | com.walmartlabs/lacinia {:mvn/version "1.2-alpha-4"}}
4 | :aliases
5 | {:run-m {:main-opts ["-m" "my.clojure-game-geek"]}
6 | :run-x {:ns-default my.clojure-game-geek
7 | :exec-fn greet
8 | :exec-args {:name "Clojure"}}
9 | :build {:deps {io.github.seancorfield/build-clj
10 | {:git/tag "v0.8.2" :git/sha "0ffdb4c"
11 | ;; since we're building an app uberjar, we do not
12 | ;; need deps-deploy for clojars.org deployment:
13 | :deps/root "slim"}}
14 | :ns-default build}
15 | :test {:extra-paths ["test"]
16 | :extra-deps {org.clojure/test.check {:mvn/version "1.1.1"}
17 | io.github.cognitect-labs/test-runner
18 | {:git/tag "v0.5.0" :git/sha "48c3c67"}}}}}
19 |
--------------------------------------------------------------------------------
/docs/introspection.rst:
--------------------------------------------------------------------------------
1 | Introspection
2 | =============
3 |
4 | Introspection is a key part of GraphQL: the schema is self-describing.
5 |
6 | .. sidebar:: GraphQL Spec
7 |
8 | Read about :spec:`introspection `.
9 |
10 | Introspection data is derived directly from the schema.
11 | Often, a ``:description`` key is added to the schema to provide additional help.
12 |
13 | Introspection is necessary to support the in-browser `graphiql`_ IDE.
14 |
15 | Introspection can also be leveraged by smart clients, presumably custom in-browser or mobile applications,
16 | to help deal with schema evolution.
17 | A smart client can use introspection to determine if a particular field exists before
18 | including that field in a query request; this can help defuse the process of introducing
19 | a new field (in the server) at the same time as a new client that needs to make use of that field.
20 |
21 |
22 | .. _graphiql: https://github.com/graphql/graphiql
23 |
24 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/docs/_examples/tutorial/schema-1.clj:
--------------------------------------------------------------------------------
1 | (ns my.clojure-game-geek.schema
2 | "Contains custom resolvers and a function to provide the full schema."
3 | (:require [clojure.java.io :as io]
4 | [com.walmartlabs.lacinia.util :as util]
5 | [com.walmartlabs.lacinia.schema :as schema]
6 | [clojure.edn :as edn]))
7 |
8 | (defn resolve-game-by-id
9 | [games-map context args value]
10 | (let [{:keys [id]} args]
11 | (get games-map id)))
12 |
13 | (defn resolver-map
14 | []
15 | (let [cgg-data (-> (io/resource "cgg-data.edn")
16 | slurp
17 | edn/read-string)
18 | games-map (->> cgg-data
19 | :games
20 | (reduce #(assoc %1 (:id %2) %2) {}))]
21 | {:Query/gameById (partial resolve-game-by-id games-map)}))
22 |
23 | (defn load-schema
24 | []
25 | (-> (io/resource "cgg-schema.edn")
26 | slurp
27 | edn/read-string
28 | (util/inject-resolvers (resolver-map))
29 | schema/compile))
30 |
--------------------------------------------------------------------------------
/docs/_examples/tutorial/deps-3.edn:
--------------------------------------------------------------------------------
1 | {:paths ["src" "resources"]
2 | :deps {org.clojure/clojure {:mvn/version "1.11.1"}
3 | com.walmartlabs/lacinia {:mvn/version "1.2-alpha-4"}}
4 | :aliases
5 | {:run-m {:main-opts ["-m" "my.clojure-game-geek"]}
6 | :run-x {:ns-default my.clojure-game-geek
7 | :exec-fn greet
8 | :exec-args {:name "Clojure"}}
9 | :build {:deps {io.github.seancorfield/build-clj
10 | {:git/tag "v0.8.2" :git/sha "0ffdb4c"
11 | ;; since we're building an app uberjar, we do not
12 | ;; need deps-deploy for clojars.org deployment:
13 | :deps/root "slim"}}
14 | :ns-default build}
15 | :dev {:extra-paths ["dev-resources"]}
16 | :test {:extra-paths ["test"]
17 | :extra-deps {org.clojure/test.check {:mvn/version "1.1.1"}
18 | io.github.cognitect-labs/test-runner
19 | {:git/tag "v0.5.0" :git/sha "48c3c67"}}}}}
20 |
--------------------------------------------------------------------------------
/docs/_examples/tutorial/cgg-schema-1.edn:
--------------------------------------------------------------------------------
1 | {:objects
2 | {:BoardGame
3 | {:description "A physical or virtual board game."
4 | :fields
5 | {:id {:type (non-null ID)}
6 | :name {:type (non-null String)}
7 | :summary {:type String
8 | :description "A one-line summary of the game."}
9 | :description {:type String
10 | :description "A long-form description of the game."}
11 | :minPlayers {:type Int
12 | :description "The minimum number of players the game supports."}
13 | :maxPlayers {:type Int
14 | :description "The maximum number of players the game supports."}
15 | :playTime {:type Int
16 | :description "Play time, in minutes, for a typical game."}}}
17 |
18 | :Query
19 | {:fields
20 | {:gameById
21 | {:type :BoardGame
22 | :description "Access a BoardGame by its unique id, if it exists."
23 | :args
24 | {:id {:type ID}}}}}}}
25 |
--------------------------------------------------------------------------------
/dev-resources/bad_schema.sdl:
--------------------------------------------------------------------------------
1 | # Changing this will probably break tests.
2 | enum episode {
3 | NEWHOPE
4 | EMPIRE
5 | JEDI
6 | }
7 |
8 | scalar Date
9 |
10 | interface Character {
11 | name: String
12 | birthDate: Date
13 | }
14 |
15 | type CharacterOutput implements Character {
16 | name: String
17 | birthDate: Date
18 | episodes: [episode]
19 | }
20 |
21 | input Character {
22 | name: String!
23 | birthDate: Date
24 | episodes: [episode]
25 | }
26 |
27 | type Query {
28 | in_episode(episode: episode = NEWHOPE, episode: String) : [CharacterOutput]
29 | }
30 |
31 | type Query {
32 | find_by_names(names: [String!]!) : [CharacterOutput]
33 | find_by_names(character_names: [String!]!) : [CharacterOutput]
34 | }
35 |
36 | union Queries = Query
37 |
38 | type Queries {
39 | find_by_names: Character
40 | }
41 |
42 | type Mutation {
43 | add(character: Character = {name: "Unspecified", episode: [NEWHOPE, EMPIRE, JEDI]}) : Boolean
44 | }
45 |
46 | schema {
47 | query: Queries
48 | mutation: Mutation
49 | }
50 |
--------------------------------------------------------------------------------
/dev-resources/parser/literals.edn:
--------------------------------------------------------------------------------
1 | [{:type :query
2 | :selections [{:type :field
3 | :field-name :hello
4 | :args [{:arg-name :i
5 | :arg-value {:type :integer
6 | :value "5"}}
7 | {:arg-name :f
8 | :arg-value {:type :float
9 | :value "3.14"}}
10 | {:arg-name :s
11 | :arg-value {:type :string
12 | :value "clojure"}}
13 |
14 | {:arg-name :bs
15 | :arg-value {:type :string
16 | :value "This is a block string indented\nfour characters.\n\n This line is indented an extra two.\n\nCharacters such as \\t are not expanded."}}
17 | {:arg-name :b
18 | :arg-value {:type :boolean
19 | :value "false"}}
20 | {:arg-name :n
21 | :arg-value {:type :null}}]}]}]
22 |
--------------------------------------------------------------------------------
/dev-resources/summarize-schema.edn:
--------------------------------------------------------------------------------
1 | {:objects
2 | {:Toy
3 | {:fields {:name {:type String}
4 | :packaging {:type :Packaging}
5 | :aisle {:type Int}}}
6 | :Truck
7 | {:fields {:model {:type String}
8 | :dealership {:type Dealership}
9 | :cost {:type Int}}}
10 |
11 | :Dealership
12 | {:fields {:name {:type String}
13 | :address {:type String}
14 | :city {:type String}
15 | :state {:type String}
16 | :postal_code {:type String}}}}
17 |
18 | :enums
19 | {:Packaging
20 | {:values [plastic cardboard mixed]}}
21 |
22 | :unions
23 | {:Product {:members [:Toy :Truck]}}
24 |
25 | :queries
26 | {:toy_by_name {:type Toy
27 | :resolve :placeholder
28 | :args {:name {:type String}}}
29 |
30 | :truck_by_model {:type Truck
31 | :resolve :placeholder
32 | :args {:model {:type String}}}
33 |
34 | :product_by_string {:type Product
35 | :resolve :placeholder
36 | :args {:match {:type String}}}}}
37 |
--------------------------------------------------------------------------------
/docs/mutations.rst:
--------------------------------------------------------------------------------
1 | Mutations
2 | =========
3 |
4 | Mutations parallel :doc:`queries `, except that the root field resolvers may make
5 | changes to underlying data in addition to exposing data.
6 |
7 | The :doc:`field resolver ` for a mutation will, as with a query, be passed nil
8 | as its value argument (the third argument).
9 | A mutation is expected to perform some state changing operation, then return a value that
10 | indicates the new state; this value will be recursively resolved and selected, just as with
11 | a query.
12 |
13 | Mutations are defined as fields of the :doc:`Mutation object `.
14 |
15 | When a single query includes more than one mutation, the mutations *must* execute in the client-specified
16 | order.
17 | This is different from queries, which allow for each root query to run in
18 | :doc:`parallel `.
19 |
20 | Typically, mutations are only allowed when the incoming request is explicitly an HTTP POST.
21 | However, that is beyond the scope of Lacinia (it doesn't know about the HTTP request, just
22 | the query string extracted from the HTTP request).
23 |
--------------------------------------------------------------------------------
/docs/deprecation.rst:
--------------------------------------------------------------------------------
1 | Deprecation
2 | ===========
3 |
4 | Schemas grow and change over time.
5 | GraphQL values backwards compatibility quite highly, so changes to a schema are typically additive:
6 | introducing novel fields, types, and enum values.
7 |
8 | However, when the implementation of a field is just `wrong`, it can be kept for compatibility, but
9 | deprecated.
10 |
11 | Both :doc:`fields ` and :doc:`enums ` can include a ``:deprecated`` key.
12 | Remember that queries, mutations, and subscriptions are (under the covers) just fields, so they can be
13 | deprecated as well.
14 |
15 | The value for the key can either be ``true``, or a string description of the reason the field is deprecated.
16 | Typically, the description indicates an alternative field to use instead.
17 |
18 | Deprecation does *not* affect execution of the field in any way; the deprecation flag and reason simply shows up
19 | in :doc:`introspection `.
20 |
21 | When using the :doc:`Schema Definition Language `, elements may be marked with the
22 | ``@deprecated`` :doc:`directive `.
23 |
24 |
25 |
--------------------------------------------------------------------------------
/src/com/walmartlabs/lacinia/constants.clj:
--------------------------------------------------------------------------------
1 | ; Copyright (c) 2017-present Walmart, Inc.
2 | ;
3 | ; Licensed under the Apache License, Version 2.0 (the "License")
4 | ; you may not use this file except in compliance with the License.
5 | ; You may obtain a copy of the License at
6 | ;
7 | ; http://www.apache.org/licenses/LICENSE-2.0
8 | ;
9 | ; Unless required by applicable law or agreed to in writing, software
10 | ; distributed under the License is distributed on an "AS IS" BASIS,
11 | ; WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | ; See the License for the specific language governing permissions and
13 | ; limitations under the License.
14 |
15 | (ns ^:no-doc com.walmartlabs.lacinia.constants
16 | "A handy place to define namespaced constants")
17 |
18 | (def schema-key
19 | "Context key storing the compiled schema."
20 | ::schema)
21 |
22 | (def parsed-query-key
23 | "Context key storing the parsed and prepared query."
24 | ::parsed-query)
25 |
26 | (def ^{:added "0.17.0"} selection-key
27 | "Context key storing the current selection."
28 | :com.walmartlabs.lacinia/selection)
29 |
--------------------------------------------------------------------------------
/dev-resources/doc-inheritance-schema.edn:
--------------------------------------------------------------------------------
1 | {:interfaces
2 | {:sierra
3 | {:fields
4 | {:alpha {:type String}
5 | :bravo {:type String
6 | :description "sierra/bravo"}}}
7 | :tango
8 | {:description "tango interface"
9 | :fields
10 | {:charlie
11 | {:type String
12 | :description "tango/charlie"
13 | :args
14 | {:delta
15 | {:type String
16 | :description "tango/charlie/delta"}
17 | :echo
18 | {:type String
19 | :description "tango/charlie/echo"}}}}}}
20 |
21 | :objects
22 | {:ex1
23 | {:implements [:sierra]
24 | :fields
25 | {:alpha {:type String
26 | :description "ex1/alpha"}
27 | :bravo {:type String}}}
28 | :ex2
29 | {:implements [:sierra]
30 | :fields
31 | {:alpha {:type String
32 | :description "ex2/alpha"}
33 | :bravo {:type String
34 | :description "ex2/bravo"}}}
35 | :ex3
36 | {:implements [:tango]
37 | :fields
38 | {:charlie
39 | {:type String
40 | :args
41 | {:delta
42 | {:type String
43 | :description "ex3/delta"}
44 | :echo
45 | {:type String}}}}}}}
46 |
--------------------------------------------------------------------------------
/dev-resources/documented-schema.sdl:
--------------------------------------------------------------------------------
1 | """
2 | Things that have a name.
3 | """
4 | interface Named {
5 | "The unique name for the Named thing."
6 | name: String
7 | }
8 |
9 | """
10 | File node type.
11 | """
12 | enum FileNodeType {
13 | "A standard file-system file."
14 | FILE
15 |
16 | "A directory that may contain other files and directories."
17 | DIR
18 |
19 | "A special file, such as a device."
20 | SPECIAL
21 | }
22 |
23 | type DirectoryListing implements Named {
24 | name: String
25 | node_type: FileNodeType
26 | }
27 |
28 | """
29 | String that identifies permissions on a file or directory.
30 | """
31 | scalar Permissions
32 |
33 | """
34 | Directory type.
35 | """
36 | type Directory implements Named {
37 | name : String
38 | permissions: Permissions
39 | contents(
40 | """
41 | Wildcard used for matching.
42 | """
43 | match : String) : [DirectoryListing]
44 | }
45 |
46 | type File implements Named {
47 | name : String
48 | }
49 |
50 | "Stuff that can appear on the file system"
51 | union FileSystemEntry = File | Directory
52 |
53 | type Query {
54 | file(path : String) : FileSystemEntry
55 | }
56 |
--------------------------------------------------------------------------------
/docs/_examples/tutorial/deps-4.edn:
--------------------------------------------------------------------------------
1 | {:paths ["src" "resources"]
2 | :deps {org.clojure/clojure {:mvn/version "1.11.1"}
3 | com.walmartlabs/lacinia {:mvn/version "1.2-alpha-4"}
4 | com.walmartlabs/lacinia-pedestal {:mvn/version "1.1"}
5 | io.aviso/logging {:mvn/version "1.0"}}
6 | :aliases
7 | {:run-m {:main-opts ["-m" "my.clojure-game-geek"]}
8 | :run-x {:ns-default my.clojure-game-geek
9 | :exec-fn greet
10 | :exec-args {:name "Clojure"}}
11 | :build {:deps {io.github.seancorfield/build-clj
12 | {:git/tag "v0.8.2" :git/sha "0ffdb4c"
13 | ;; since we're building an app uberjar, we do not
14 | ;; need deps-deploy for clojars.org deployment:
15 | :deps/root "slim"}}
16 | :ns-default build}
17 | :dev {:extra-paths ["dev-resources"]}
18 | :test {:extra-paths ["test"]
19 | :extra-deps {org.clojure/test.check {:mvn/version "1.1.1"}
20 | io.github.cognitect-labs/test-runner
21 | {:git/tag "v0.5.0" :git/sha "48c3c67"}}}}}
22 |
--------------------------------------------------------------------------------
/docs/resolve/index.rst:
--------------------------------------------------------------------------------
1 | Field Resolvers
2 | ===============
3 |
4 | Field resolvers are how Lacinia goes beyond data modelling to actually providing access to data.
5 |
6 | .. sidebar:: GraphQL Spec
7 |
8 | Read about :spec:`value resolution `.
9 |
10 | Field resolvers are attached to fields, including the :doc:`root objects <../roots>` ``Query``, ``Mutation``, and ``Subscription``.
11 | It is only inside field resolvers that a Lacinia application can connect to a database or
12 | an external system: field resolvers are where the data actually *comes* from.
13 |
14 | In essence, the top-level operations perform the initial work in a request, getting the root
15 | object (or collection of objects).
16 |
17 | Field resolvers in nested fields are responsible for extracting and transforming that root data.
18 | In some cases, a field resolver may need to perform additional queries against a back-end data store.
19 |
20 |
21 | .. toctree::
22 | :hidden:
23 |
24 | overview
25 | attach
26 | type-tags
27 | exceptions
28 | resolve-as
29 | field-resolver-protocol
30 | async
31 | selections
32 | context
33 | extensions
34 | examples
35 |
--------------------------------------------------------------------------------
/docs/resources.rst:
--------------------------------------------------------------------------------
1 | Other Resources
2 | ===============
3 |
4 | * `First Release Announcement `_
5 |
6 | A quick introduction to Lacinia, and goes into detail about the problems it solves for us at Walmart.
7 |
8 | * A talk at `Clojure/West 2017 `_: `Power to the (Mobile) People: Clojure and GraphQL`
9 | is available on `YouTube `_.
10 |
11 | * A good introductory `Blog Post by James Borden `_.
12 |
13 | * A talk at `Clojure/Conj 2017 `_:
14 | `The Power of Lacinia & Hystrix in Production` is available on `You Tube `_.
15 |
16 | * Blog Post: `The Case for Clojure and GraphQL: Replacing Django `_.
17 |
18 | * Blog Post: `Through the Looking Graph: Experiences adopting GraphQL on a Clojure/script project `_.
19 |
--------------------------------------------------------------------------------
/docs/spec.rst:
--------------------------------------------------------------------------------
1 | clojure.spec
2 | ============
3 |
4 | Lacinia makes use of clojure.spec; specifically, the arguments to
5 | :api:`schema/compile` and
6 | :api:`parser.schema/parse-schema` are *always* validated with spec.
7 |
8 | This is useful, especially for ``compile``, and the data structure passed in for compilation is complex and
9 | deeply nested.
10 |
11 | However, the exceptions thrown by clojure.spec can be challenging to read.
12 |
13 | The use of `Expound `_ is recommended; it does a much better job of formatting
14 | that wealth of data for a person to read.
15 |
16 | For example, it omits all the extraneous detail, making it much easier to find where the problem exists::
17 |
18 | -- Spec failed --------------------
19 | {:objects
20 | {:Henry
21 | {:fields
22 | {:higgins
23 | {:type ...,
24 | :resolve ...,
25 | :deprecated 7.0}}}}}
26 | ^^^
27 | should satisfy
28 | true?
29 | or
30 | string?
31 |
32 |
33 | Further, Lacinia includes an extra namespace, not loaded by default: ``com.walmartlabs.lacinia.expound``.
34 | This namespace simply defines spec messages for some of the trickier specs defined by Lacinia.
35 |
--------------------------------------------------------------------------------
/java/com/walmartlabs/lacinia/GraphqlSchema.tokens:
--------------------------------------------------------------------------------
1 | T__0=1
2 | T__1=2
3 | T__2=3
4 | T__3=4
5 | T__4=5
6 | T__5=6
7 | T__6=7
8 | T__7=8
9 | T__8=9
10 | T__9=10
11 | T__10=11
12 | T__11=12
13 | EXECUTABLE_DIRECTIVE_LOCATION=13
14 | TYPE_SYSTEM_DIRECTIVE_LOCATION=14
15 | K_TYPE=15
16 | K_IMPLEMENTS=16
17 | K_INTERFACE=17
18 | K_SCHEMA=18
19 | K_ENUM=19
20 | K_UNION=20
21 | K_INPUT=21
22 | K_DIRECTIVE=22
23 | K_EXTEND=23
24 | K_SCALAR=24
25 | K_ON=25
26 | K_FRAGMENT=26
27 | K_QUERY=27
28 | K_MUTATION=28
29 | K_SUBSCRIPTION=29
30 | K_VALUE=30
31 | K_TRUE=31
32 | K_FALSE=32
33 | K_NULL=33
34 | Name=34
35 | IntValue=35
36 | FloatValue=36
37 | Sign=37
38 | IntegerPart=38
39 | NonZeroDigit=39
40 | ExponentPart=40
41 | Digit=41
42 | StringValue=42
43 | BlockStringValue=43
44 | Ignored=44
45 | '{'=1
46 | '}'=2
47 | ':'=3
48 | '|'=4
49 | '@'=5
50 | '('=6
51 | ')'=7
52 | '&'=8
53 | '='=9
54 | '['=10
55 | ']'=11
56 | '!'=12
57 | 'type'=15
58 | 'implements'=16
59 | 'interface'=17
60 | 'schema'=18
61 | 'enum'=19
62 | 'union'=20
63 | 'input'=21
64 | 'directive'=22
65 | 'extend'=23
66 | 'scalar'=24
67 | 'on'=25
68 | 'fragment'=26
69 | 'query'=27
70 | 'mutation'=28
71 | 'subscription'=29
72 | 'value'=30
73 | 'true'=31
74 | 'false'=32
75 | 'null'=33
76 | '-'=37
77 |
--------------------------------------------------------------------------------
/dev-resources/tracing_demo.clj:
--------------------------------------------------------------------------------
1 | (ns tracing-demo
2 | (:require
3 | [com.walmartlabs.lacinia :as lacinia]
4 | [com.walmartlabs.lacinia.tracing :as tracing]
5 | [com.walmartlabs.lacinia-test :as lacinia-test]
6 | [com.walmartlabs.lacinia.resolve :as resolve]
7 | [com.walmartlabs.test-utils :refer [simplify]])
8 | (:import (java.util.concurrent ThreadPoolExecutor TimeUnit SynchronousQueue)))
9 |
10 | (def star-wars-schema lacinia-test/default-schema)
11 |
12 | (comment
13 | (let [queue (SynchronousQueue.)
14 | thread-pool (ThreadPoolExecutor. 5 10 1 TimeUnit/SECONDS queue)]
15 | ;; Ensure all core thread are started and ready, however, for the tiny
16 | ;; resolvers in the example, it's hard to show parallel behavior. Need some I/O.
17 | (while (.prestartCoreThread thread-pool))
18 | (try
19 | (binding [resolve/*callback-executor* thread-pool]
20 |
21 | (simplify
22 | (lacinia/execute
23 | star-wars-schema "
24 | {
25 | luke: human(id: \"1000\") { friends { name }}
26 | leia: human(id: \"1003\") { name }
27 | }"
28 | nil
29 | (tracing/enable-tracing nil))))
30 | (finally
31 | (.shutdown thread-pool))))
32 |
33 |
34 | )
--------------------------------------------------------------------------------
/java/com/walmartlabs/lacinia/GraphqlSchemaLexer.tokens:
--------------------------------------------------------------------------------
1 | T__0=1
2 | T__1=2
3 | T__2=3
4 | T__3=4
5 | T__4=5
6 | T__5=6
7 | T__6=7
8 | T__7=8
9 | T__8=9
10 | T__9=10
11 | T__10=11
12 | T__11=12
13 | EXECUTABLE_DIRECTIVE_LOCATION=13
14 | TYPE_SYSTEM_DIRECTIVE_LOCATION=14
15 | K_TYPE=15
16 | K_IMPLEMENTS=16
17 | K_INTERFACE=17
18 | K_SCHEMA=18
19 | K_ENUM=19
20 | K_UNION=20
21 | K_INPUT=21
22 | K_DIRECTIVE=22
23 | K_EXTEND=23
24 | K_SCALAR=24
25 | K_ON=25
26 | K_FRAGMENT=26
27 | K_QUERY=27
28 | K_MUTATION=28
29 | K_SUBSCRIPTION=29
30 | K_VALUE=30
31 | K_TRUE=31
32 | K_FALSE=32
33 | K_NULL=33
34 | Name=34
35 | IntValue=35
36 | FloatValue=36
37 | Sign=37
38 | IntegerPart=38
39 | NonZeroDigit=39
40 | ExponentPart=40
41 | Digit=41
42 | StringValue=42
43 | BlockStringValue=43
44 | Ignored=44
45 | '{'=1
46 | '}'=2
47 | ':'=3
48 | '|'=4
49 | '@'=5
50 | '('=6
51 | ')'=7
52 | '&'=8
53 | '='=9
54 | '['=10
55 | ']'=11
56 | '!'=12
57 | 'type'=15
58 | 'implements'=16
59 | 'interface'=17
60 | 'schema'=18
61 | 'enum'=19
62 | 'union'=20
63 | 'input'=21
64 | 'directive'=22
65 | 'extend'=23
66 | 'scalar'=24
67 | 'on'=25
68 | 'fragment'=26
69 | 'query'=27
70 | 'mutation'=28
71 | 'subscription'=29
72 | 'value'=30
73 | 'true'=31
74 | 'false'=32
75 | 'null'=33
76 | '-'=37
77 |
--------------------------------------------------------------------------------
/docs/_examples/tutorial/deps-5.edn:
--------------------------------------------------------------------------------
1 | {:paths ["src" "resources"]
2 | :deps {org.clojure/clojure {:mvn/version "1.11.1"}
3 | com.walmartlabs/lacinia {:mvn/version "1.2-alpha-4"}
4 | com.walmartlabs/lacinia-pedestal {:mvn/version "1.1"}
5 | com.stuartsierra/component {:mvn/version "1.1.0"}
6 | io.aviso/logging {:mvn/version "1.0"}}
7 | :aliases
8 | {:run-m {:main-opts ["-m" "my.clojure-game-geek"]}
9 | :run-x {:ns-default my.clojure-game-geek
10 | :exec-fn greet
11 | :exec-args {:name "Clojure"}}
12 | :build {:deps {io.github.seancorfield/build-clj
13 | {:git/tag "v0.8.2" :git/sha "0ffdb4c"
14 | ;; since we're building an app uberjar, we do not
15 | ;; need deps-deploy for clojars.org deployment:
16 | :deps/root "slim"}}
17 | :ns-default build}
18 | :dev {:extra-paths ["dev-resources"]}
19 | :test {:extra-paths ["test"]
20 | :extra-deps {org.clojure/test.check {:mvn/version "1.1.1"}
21 | io.github.cognitect-labs/test-runner
22 | {:git/tag "v0.5.0" :git/sha "48c3c67"}}}}}
23 |
--------------------------------------------------------------------------------
/.github/workflows/config.yml:
--------------------------------------------------------------------------------
1 | name: CI
2 | on:
3 | push:
4 | branches:
5 | - master
6 | pull_request:
7 | branches:
8 | - master
9 | jobs:
10 | build:
11 | strategy:
12 | matrix:
13 | java-version: ["11", "17", "21", "24"]
14 | clojure-version: ["11", "12"]
15 | runs-on: ubuntu-latest
16 | steps:
17 | - name: Checkout
18 | uses: actions/checkout@v5
19 | - name: Setup Java
20 | uses: actions/setup-java@v5
21 | with:
22 | java-version: ${{ matrix.java-version }}
23 | distribution: 'corretto'
24 | - name: Setup Clojure
25 | uses: DeLaGuardo/setup-clojure@13.4
26 | with:
27 | cli: 1.12.3.1577
28 |
29 | - name: Cache clojure dependencies
30 | uses: actions/cache@v4
31 | with:
32 | path: |
33 | ~/.m2/repository
34 | ~/.gitlibs
35 | ~/.deps.clj
36 | # List all files containing dependencies:
37 | key: cljdeps-${{ hashFiles('deps.edn') }}
38 |
39 | - name: Execute tests (JDK ${{ matrix.java-version}}, Clojure ${{matrix.clojure-version}})
40 | run: clojure -T:build compile-java && clojure -X:dev:test:clojure-${{ matrix.clojure-version }}
41 |
--------------------------------------------------------------------------------
/docs/_examples/parsed_sample_schema.edn:
--------------------------------------------------------------------------------
1 | {:objects
2 | {:Character {:fields
3 | {:name {:type (non-null String)
4 | :description "Character name"}
5 | :episodes {:type (list :episode)}}
6 | :description "A Star Wars character"}
7 | :Query {:fields
8 | {:findAllInEpisode
9 | {:type (list :Character)
10 | :args
11 | {:episode
12 | {:type (non-null :episode)
13 | :description "Episode for which to find characters."}}
14 | :resolve :find-all-in-episode
15 | :description "Find all characters in the given episode"}}}
16 | :Mutation {:fields
17 | {:addCharacter
18 | {:type Boolean
19 | :args {:character {:type (non-null :CharacterArg)}}
20 | :resolve :add-character}}}}
21 |
22 | :input-objects
23 | {:CharacterArg {:fields
24 | {:name {:type (non-null String)}
25 | :episodes {:type (non-null (list :episode))}}}}
26 |
27 | :enums
28 | {:episode {:values [{:enum-value :NEWHOPE}
29 | {:enum-value :EMPIRE}
30 | {:enum-value :JEDI}]}}
31 |
32 | :roots
33 | {:query :Query
34 | :mutation :Mutation}}
35 |
--------------------------------------------------------------------------------
/docs/_examples/tutorial/cgg-data-2.edn:
--------------------------------------------------------------------------------
1 | {:games
2 | [{:id "1234"
3 | :name "Zertz"
4 | :summary "Two player abstract with forced moves and shrinking board"
5 | :designers #{"200"}
6 | :minPlayers 2
7 | :maxPlayers 2}
8 | {:id "1235"
9 | :name "Dominion"
10 | :summary "Created the deck-building genre; zillions of expansions"
11 | :designers #{"204"}
12 | :minPlayers 2}
13 | {:id "1236"
14 | :name "Tiny Epic Galaxies"
15 | :summary "Fast dice-based sci-fi space game with a bit of chaos"
16 | :designers #{"203"}
17 | :minPlayers 1
18 | :maxPlayers 4}
19 | {:id "1237"
20 | :name "7 Wonders: Duel"
21 | :summary "Tense, quick card game of developing civilizations"
22 | :designers #{"201" "202"}
23 | :minPlayers 2
24 | :maxPlayers 2}]
25 |
26 | :designers
27 | [{:id "200"
28 | :name "Kris Burm"
29 | :url "http://www.gipf.com/project_gipf/burm/burm.html"}
30 | {:id "201"
31 | :name "Antoine Bauza"
32 | :url "http://www.antoinebauza.fr/"}
33 | {:id "202"
34 | :name "Bruno Cathala"
35 | :url "http://www.brunocathala.com/"}
36 | {:id "203"
37 | :name "Scott Almes"}
38 | {:id "204"
39 | :name "Donald X. Vaccarino"}]}
40 |
--------------------------------------------------------------------------------
/docs/_examples/tutorial/user-4.clj:
--------------------------------------------------------------------------------
1 | (ns user
2 | (:require [com.stuartsierra.component :as component]
3 | [my.clojure-game-geek.system :as system]
4 | [com.walmartlabs.lacinia :as lacinia]
5 | [clojure.java.browse :refer [browse-url]]
6 | [clojure.walk :as walk])
7 | (:import (clojure.lang IPersistentMap)))
8 |
9 | (defn simplify
10 | "Converts all ordered maps nested within the map into standard hash maps, and
11 | sequences into vectors, which makes for easier constants in the tests, and eliminates ordering problems."
12 | [m]
13 | (walk/postwalk
14 | (fn [node]
15 | (cond
16 | (instance? IPersistentMap node)
17 | (into {} node)
18 |
19 | (seq? node)
20 | (vec node)
21 |
22 | :else
23 | node))
24 | m))
25 |
26 | (defonce system (system/new-system))
27 |
28 | (defn q
29 | [query-string]
30 | (-> system
31 | :schema-provider
32 | :schema
33 | (lacinia/execute query-string nil nil)
34 | simplify))
35 |
36 | (defn start
37 | []
38 | (alter-var-root #'system component/start-system)
39 | (browse-url "http://localhost:8888/ide")
40 | :started)
41 |
42 | (defn stop
43 | []
44 | (alter-var-root #'system component/stop-system)
45 | :stopped)
46 |
--------------------------------------------------------------------------------
/docs/queries.rst:
--------------------------------------------------------------------------------
1 | Queries
2 | =======
3 |
4 | .. sidebar:: GraphQL Spec
5 |
6 | Read about :spec:`operations `.
7 |
8 | Queries are responsible for generating the initial resolved values that will be
9 | picked apart to form the result map.
10 |
11 | Other than that, queries are just the same as any other field.
12 | Queries have a type, and accept arguments.
13 |
14 | Queries are defined as the fields of a special object, :doc:`Query object `.
15 |
16 | .. literalinclude:: _examples/query-def-var.edn
17 | :language: clojure
18 |
19 | The :doc:`field resolver ` for a query is passed nil
20 | as the the value (the third parameter).
21 | Outside of this, the query field resolver is the same as any field resolver
22 | anywhere else in the schema.
23 |
24 | In the GraphQL specification, it is noted that queries are idempotent; if
25 | the query document includes multiple queries, they are allowed to execute
26 | in :doc:`parallel `.
27 |
28 |
29 | :queries key
30 | ------------
31 |
32 | The above is the "modern" way to define queries; an older approach is still supported.
33 | Queries may instead be defined using the ``:queries`` key of the schema.
34 |
35 | .. literalinclude:: _examples/query-def.edn
36 | :language: clojure
37 |
--------------------------------------------------------------------------------
/docs/resolve/attach.rst:
--------------------------------------------------------------------------------
1 | Injecting Resolvers
2 | ===================
3 |
4 | Schemas start as EDN files, which has the advantage that symbols do not have to be quoted
5 | (useful when using the ``list`` and ``non-null`` qualifiers on types).
6 | However, EDN is data, not code, which makes it nonsensical to define field resolvers directly in the schema file.
7 |
8 | One option is to use ``assoc-in`` to attach resolvers after reading the EDN, but before invoking
9 | :api:`schema/compile`.
10 | This can become quite cumbersome in practice.
11 |
12 | Instead, the standard approach is use
13 | :api:`util/inject-resolvers`, [#attach-resolvers]_ which is a concise way of matching fields to resolvers, adding
14 | the ``:resolve`` key to the nested field definitions.
15 |
16 | .. literalinclude:: /_examples/compile-schema.clj
17 | :language: clojure
18 |
19 | The keys in the map passed to ``inject-resolvers`` use the namespace to define the object (such as ``Human`` or ``Query``) and the local name to define the field within the object (``hero`` and so forth).
20 |
21 | The ``inject-resolvers`` step occurs **before** the schema is compiled.
22 |
23 | .. [#attach-resolvers] An older approach is still supported, via the function
24 | :api:`util/attach-resolvers`, but ``inject-resolvers`` is preferred, as it is simpler.
25 |
--------------------------------------------------------------------------------
/test/com/walmartlabs/lacinia/query_ops_test.clj:
--------------------------------------------------------------------------------
1 | ; Copyright (c) 2017-present Walmart, Inc.
2 | ;
3 | ; Licensed under the Apache License, Version 2.0 (the "License")
4 | ; you may not use this file except in compliance with the License.
5 | ; You may obtain a copy of the License at
6 | ;
7 | ; http://www.apache.org/licenses/LICENSE-2.0
8 | ;
9 | ; Unless required by applicable law or agreed to in writing, software
10 | ; distributed under the License is distributed on an "AS IS" BASIS,
11 | ; WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | ; See the License for the specific language governing permissions and
13 | ; limitations under the License.
14 |
15 | (ns com.walmartlabs.lacinia.query-ops-test
16 | (:require
17 | [clojure.test :refer [deftest is]]
18 | [com.walmartlabs.test-utils :refer [compile-schema execute]]
19 | [com.walmartlabs.test-schema :refer [test-schema]]
20 | [com.walmartlabs.lacinia.schema :as schema]))
21 |
22 | (def default-schema (schema/compile test-schema))
23 |
24 | (deftest may-identify-op-when-single-op
25 | (is (= {:data {:human {:name "Han Solo"}}}
26 | (execute default-schema
27 | "query solo($id: String!) { human(id: $id) { name } }"
28 | {:id "1002"}
29 | nil
30 | {:operation-name "solo"}))))
31 |
--------------------------------------------------------------------------------
/docs/subscriptions/overview.rst:
--------------------------------------------------------------------------------
1 | Overview
2 | ========
3 |
4 | The specification discusses a `source stream` and a `response stream`.
5 |
6 | Lacinia implements the source stream as a callback function.
7 | The response stream is largely the responsibility of
8 | the `web tier `_.
9 |
10 | - Lacinia invokes a streamer function once, to initialize the subscription stream.
11 |
12 | - The streamer is provided with a source stream callback function; as new values are available
13 | they are passed to this callback.
14 |
15 | Typically, the streamer will create a thread, core.async process, or other long-lived
16 | construct to feed values to the source stream.
17 |
18 | - Whenever the source stream callback is passed a value,
19 | Lacinia will execute the subscription as a query, which will generate a
20 | new response (with the standard ``:data`` and/or ``:errors`` keys).
21 |
22 | - The response will be converted as necessary and streamed to the client, forming
23 | the response stream.
24 |
25 | - The streamer must return a function that will be invoked to perform cleanup.
26 | This cleanup function typically stops whatever process was started earlier.
27 |
28 | Subscriptions are operations, like queries or mutations.
29 | They are defined as fields of the :doc:`Subscription object <../roots>`.
30 |
31 |
32 |
--------------------------------------------------------------------------------
/docs/_examples/star-wars-schema.edn:
--------------------------------------------------------------------------------
1 | {:enums
2 | {:Episode
3 | {:description "The episodes of the original Star Wars trilogy."
4 | :values [:NEWHOPE :EMPIRE :JEDI]}}
5 |
6 | :interfaces
7 | {:Character
8 | {:fields {:id {:type String}
9 | :name {:type String}
10 | :appearsIn {:type (list :Episode)}
11 | :friends {:type (list :Character)}}}}
12 |
13 | :objects
14 | {:Droid
15 | {:implements [:Character]
16 | :fields {:id {:type String}
17 | :name {:type String}
18 | :appearsIn {:type (list :Episode)}
19 | :friends {:type (list :Character)
20 | :resolve :friends}
21 | :primaryFunction {:type (list String)}}}
22 |
23 | :Human
24 | {:implements [:Character]
25 | :fields {:id {:type String}
26 | :name {:type String}
27 | :appearsIn {:type (list :Episode)}
28 | :friends {:type (list :Character)}
29 | :home_planet {:type String}}}
30 |
31 | :Query
32 | {:fields
33 | {:hero {:type (non-null :Character)
34 | :args {:episode {:type :Episode}}}
35 |
36 | :human {:type (non-null :Human)
37 | :args {:id {:type String
38 | :default-value "1001"}}}
39 |
40 | :droid {:type :Droid
41 | :args {:id {:type String
42 | :default-value "2001"}}}}}}}
43 |
--------------------------------------------------------------------------------
/perf/create-charts.R:
--------------------------------------------------------------------------------
1 | #!Rscript
2 |
3 | # Execute with `Rscript create-charts.R`
4 | # Reads perf/benchmarks.csv and generates two PNG charts into perf
5 |
6 | library("ggplot2")
7 |
8 | uniquefactor <- function(v) {
9 | factor(v, ordered=TRUE, levels=unique(v))
10 | }
11 |
12 | # Read the benchmarks in as 'b'
13 |
14 | # Need to do some work here to convert the commit and kind columns into ordered factors.
15 | # There's probably some mojo to do it all in the call to read.csv, but this works.
16 |
17 | b <- read.csv("benchmarks.csv", stringsAsFactors=FALSE)
18 | b$date = factor(b$date, ordered=TRUE)
19 | b$commit = uniquefactor(b$commit)
20 | b$kind = factor(b$kind, ordered=TRUE)
21 |
22 | create_chart <- function(file_name, type_name, data) {
23 | parse.plot <-
24 | qplot(b$commit, data, color=b$kind, shape=b$kind, group=b$kind) +
25 | geom_point(size=4) +
26 | labs(x="Commit",
27 | y=paste(type_name ,"Time (ms)"),
28 | title=paste(type_name, "Time Benchmarks"),
29 | color="Benchmark",
30 | shape="Benchmark") +
31 | geom_path() +
32 | theme(plot.title = element_text(size=18),
33 | axis.text.x=element_text(angle=45, hjust=1))
34 |
35 | cat("Saving chart as ", file_name, "\n", sep="")
36 |
37 | ggsave(file_name, parse.plot, width=10, height=7)
38 | }
39 |
40 | create_chart("parse-time.pdf", "Parse", b$parse)
41 | create_chart("exec-time.pdf", "Execution", b$exec)
42 |
--------------------------------------------------------------------------------
/docs/_examples/tutorial/system_test-1.clj:
--------------------------------------------------------------------------------
1 | (ns my.clojure-game-geek.system-test
2 | (:require [clojure.test :refer [deftest is]]
3 | [com.stuartsierra.component :as component]
4 | [com.walmartlabs.lacinia :as lacinia]
5 | [my.clojure-game-geek.test-utils :refer [simplify]]
6 | [my.clojure-game-geek.system :as system]))
7 |
8 | (defn- test-system
9 | "Creates a new system suitable for testing, and ensures that
10 | the HTTP port won't conflict with a default running system."
11 | []
12 | (system/new-system {:port 8989}))
13 |
14 | (defn- q
15 | "Extracts the compiled schema and executes a query."
16 | [system query variables]
17 | (-> system
18 | (get-in [:schema-provider :schema])
19 | (lacinia/execute query variables nil)
20 | simplify))
21 |
22 | (deftest can-read-board-game
23 | (let [system (component/start-system (test-system))]
24 | (try
25 | (is (= {:data {:gameById {:name "Zertz"
26 | :summary "Two player abstract with forced moves and shrinking board"
27 | :maxPlayers 2
28 | :minPlayers 2
29 | :playTime nil}}}
30 | (q system
31 | "{ gameById(id: 1234) { name summary minPlayers maxPlayers playTime }}"
32 | nil)))
33 | (finally
34 | (component/stop-system system)))))
35 |
--------------------------------------------------------------------------------
/docs/interfaces.rst:
--------------------------------------------------------------------------------
1 | Interfaces
2 | ==========
3 |
4 | GraphQL supports the notion of *interfaces*, collections of fields and their arguments.
5 |
6 | .. sidebar:: GraphQL Spec
7 |
8 | Read about :spec:`interfaces `.
9 |
10 |
11 | To keep things simple, interfaces can not extend other interfaces.
12 | Likewise, objects can implement multiple interfaces, but can not extend other objects.
13 |
14 | Interfaces are valid types, they can be specified as the return type of a query, mutation,
15 | or as the type of a field.
16 |
17 | .. literalinclude:: _examples/interface-definition.edn
18 | :language: clojure
19 |
20 |
21 | An interface definition may include a ``:description`` key; the value is a string exposed through :doc:`introspection`.
22 |
23 | The description on an interface field, or on an argument of an interface field, will be inherited by
24 | the object field (or argument) unless overriden.
25 | This helps to eliminate duplication of documentation between an interface and the object implementing
26 | the interface.
27 |
28 | The :doc:`object definition ` must include all the fields of all extended interfaces.
29 |
30 |
31 | .. tip::
32 |
33 | When a field or operation type is an interface,
34 | the field resolver may return any of a number of different
35 | concrete object types, and Lacinia has no way to determine which;
36 | this information must be :doc:`explicitly provided `.
37 |
--------------------------------------------------------------------------------
/dev-resources/sample_schema.sdl:
--------------------------------------------------------------------------------
1 | # Changing this will probably break tests.
2 | enum episode {
3 | NEWHOPE
4 | EMPIRE
5 | JEDI
6 | }
7 |
8 | "Extra tracing of field operations"
9 | directive @Trace (label : String!) on FIELD_DEFINITION
10 |
11 | "Date in standard ISO format"
12 | scalar Date
13 |
14 | interface Human {
15 | name: String
16 | birthDate: Date
17 | }
18 |
19 | type CharacterOutput implements Human {
20 | name: String
21 | birthDate: Date
22 | episodes: [episode]
23 | }
24 |
25 | input Character {
26 | name: String!
27 | birthDate: Date
28 | episodes: [episode]
29 | }
30 |
31 | type MyQuery {
32 | in_episode(episode: episode = NEWHOPE) : [CharacterOutput] @Trace
33 | }
34 |
35 | type OtherQuery {
36 | find_by_names(names: [String!]!) : [CharacterOutput]
37 | }
38 |
39 | type Mutation {
40 | add(character: Character = {name: "Unspecified", episodes: [NEWHOPE, EMPIRE, JEDI]}) : Boolean
41 | @deprecated(reason: "just for testing")
42 | @Trace(label: "add-character")
43 | }
44 |
45 | type Subscription {
46 | new_character(episodes: [episode!]!) : CharacterOutput
47 | }
48 |
49 | # This doesn't make much sense but is a remnant from when unions were allowed for schema operation types
50 | # (Jun 2018 spec makes it clear they must be objects, not unions). */
51 | union Queries = MyQuery | OtherQuery
52 |
53 | schema {
54 | query: MyQuery
55 | mutation: Mutation
56 | subscription: Subscription
57 | }
58 |
--------------------------------------------------------------------------------
/docs/_examples/tutorial/deps-6.edn:
--------------------------------------------------------------------------------
1 | {:paths ["src" "resources"]
2 | :deps {org.clojure/clojure {:mvn/version "1.11.1"}
3 | com.walmartlabs/lacinia {:mvn/version "1.2-alpha-4"}
4 | com.walmartlabs/lacinia-pedestal {:mvn/version "1.1"}
5 | org.clojure/java.jdbc {:mvn/version "0.7.12"}
6 | org.postgresql/postgresql {:mvn/version "42.5.1"}
7 | com.mchange/c3p0 {:mvn/version "0.9.5.5"}
8 | com.stuartsierra/component {:mvn/version "1.1.0"}
9 | io.aviso/logging {:mvn/version "1.0"}}
10 | :aliases
11 | {:run-m {:main-opts ["-m" "my.clojure-game-geek"]}
12 | :run-x {:ns-default my.clojure-game-geek
13 | :exec-fn greet
14 | :exec-args {:name "Clojure"}}
15 | :build {:deps {io.github.seancorfield/build-clj
16 | {:git/tag "v0.8.2" :git/sha "0ffdb4c"
17 | ;; since we're building an app uberjar, we do not
18 | ;; need deps-deploy for clojars.org deployment:
19 | :deps/root "slim"}}
20 | :ns-default build}
21 | :dev {:extra-paths ["dev-resources"]}
22 | :test {:extra-paths ["test"]
23 | :extra-deps {org.clojure/test.check {:mvn/version "1.1.1"}
24 | io.github.cognitect-labs/test-runner
25 | {:git/tag "v0.5.0" :git/sha "48c3c67"}}}}}
26 |
--------------------------------------------------------------------------------
/docs/tutorial/domain.rst:
--------------------------------------------------------------------------------
1 | Domain
2 | ======
3 |
4 | Our goal will be to provide a GraphQL interface to data about board games
5 | (one of the author's hobbies), as a limited version of
6 | `Board Game Geek `_.
7 |
8 | Board Game Geek itself is a huge resource, with decades of information about games, game designers and publishers,
9 | tracking which users own which games, game ratings, forums for discussing games, and far, far more.
10 | We'll focus on just a couple of simple elements of the full design.
11 |
12 | The basic types in the final system are as follows:
13 |
14 | .. graphviz::
15 |
16 | digraph {
17 |
18 | BoardGame
19 | Publisher
20 | Designer
21 | Member
22 | GameRating
23 |
24 | BoardGame -> GameRating [taillabel="1", headlabel="n"]
25 | BoardGame -> {Publisher, Designer} [taillabel="n", headlabel="m"]
26 | GameRating -> Member [taillabel="n", headlabel="1" ]
27 |
28 | }
29 |
30 | A BoardGame may be published by multiple Publisher companies (the Publisher may
31 | be different for different countries, or may simply vary over time).
32 |
33 | A BoardGame may have any number of Designers.
34 |
35 | Users of Clojure Game Geek, represented as type Member, may provide their personal ratings for board games.
36 |
37 | Even this tiny silver of functionality it sufficiently meaty to give us a taste for building
38 | full applications. We'll start by creating an empty project, in the next chapter.
39 |
--------------------------------------------------------------------------------
/dev-resources/star-wars-schema.edn:
--------------------------------------------------------------------------------
1 | {:enums
2 | {:episode
3 | {:description "The episodes of the original Star Wars trilogy."
4 | :values [:NEWHOPE :EMPIRE :JEDI]}}
5 |
6 | :interfaces
7 | {:character
8 | {:fields {:id {:type String}
9 | :name {:type String}
10 | :appears_in {:type (list :episode)}
11 | :friends {:type (list :character)}}}}
12 |
13 | :objects
14 | {:droid
15 | {:implements [:character]
16 | :fields {:id {:type String}
17 | :name {:type String}
18 | :appears_in {:type (list :episode)}
19 | :friends {:type (list :character)
20 | :resolve :friends}
21 | :primary_function {:type (list String)}}}
22 |
23 | :human
24 | {:implements [:character]
25 | :fields {:id {:type String}
26 | :name {:type String}
27 | :appears_in {:type (list :episode)}
28 | :friends {:type (list :character)
29 | :resolve :friends}
30 | :home_planet {:type String}}}}
31 |
32 | :queries
33 | {:hero {:type (non-null :character)
34 | :args {:episode {:type :episode}}
35 | :resolve :hero}
36 |
37 | :human {:type (non-null :human)
38 | :args {:id {:type String
39 | :default-value "1001"}}
40 | :resolve :human}
41 |
42 | :droid {:type :droid
43 | :args {:id {:type String
44 | :default-value "2001"}}
45 | :resolve :droid}}}
46 |
--------------------------------------------------------------------------------
/docs/_examples/tutorial/user-3.clj:
--------------------------------------------------------------------------------
1 | (ns user
2 | (:require [my.clojure-game-geek.schema :as s]
3 | [com.walmartlabs.lacinia :as lacinia]
4 | [com.walmartlabs.lacinia.pedestal2 :as lp]
5 | [io.pedestal.http :as http]
6 | [clojure.java.browse :refer [browse-url]]
7 | [clojure.walk :as walk])
8 | (:import (clojure.lang IPersistentMap)))
9 |
10 | (def schema (s/load-schema))
11 |
12 | (defn simplify
13 | "Converts all ordered maps nested within the map into standard hash maps, and
14 | sequences into vectors, which makes for easier constants in the tests, and eliminates ordering problems."
15 | [m]
16 | (walk/postwalk
17 | (fn [node]
18 | (cond
19 | (instance? IPersistentMap node)
20 | (into {} node)
21 |
22 | (seq? node)
23 | (vec node)
24 |
25 | :else
26 | node))
27 | m))
28 |
29 | (defn q
30 | [query-string]
31 | (-> (lacinia/execute schema query-string nil nil)
32 | simplify))
33 |
34 | (defonce server nil)
35 |
36 | (defn start-server
37 | [_]
38 | (let [server (-> (lp/default-service schema nil)
39 | http/create-server
40 | http/start)]
41 | (browse-url "http://localhost:8888/ide")
42 | server))
43 |
44 | (defn stop-server
45 | [server]
46 | (http/stop server)
47 | nil)
48 |
49 | (defn start
50 | []
51 | (alter-var-root #'server start-server)
52 | :started)
53 |
54 | (defn stop
55 | []
56 | (alter-var-root #'server stop-server)
57 | :stopped)
58 |
--------------------------------------------------------------------------------
/docs/_examples/tutorial/cgg-schema-3.edn:
--------------------------------------------------------------------------------
1 | {:objects
2 | {:BoardGame
3 | {:description "A physical or virtual board game."
4 | :fields
5 | {:id {:type (non-null ID)}
6 | :name {:type (non-null String)}
7 | :summary {:type String
8 | :description "A one-line summary of the game."}
9 | :description {:type String
10 | :description "A long-form description of the game."}
11 | :designers {:type (non-null (list :Designer))
12 | :description "Designers who contributed to the game."}
13 | :minPlayers {:type Int
14 | :description "The minimum number of players the game supports."}
15 | :maxPlayers {:type Int
16 | :description "The maximum number of players the game supports."}
17 | :playTime {:type Int
18 | :description "Play time, in minutes, for a typical game."}}}
19 |
20 | :Designer
21 | {:description "A person who may have contributed to a board game design."
22 | :fields
23 | {:id {:type (non-null ID)}
24 | :name {:type (non-null String)}
25 | :url {:type String
26 | :description "Home page URL, if known."}
27 | :games {:type (non-null (list :BoardGame))
28 | :description "Games designed by this designer."}}}
29 |
30 | :Query
31 | {:fields
32 | {:gameById
33 | {:type :BoardGame
34 | :description "Access a BoardGame by its unique id, if it exists."
35 | :args
36 | {:id {:type ID}}}}}}}
37 |
--------------------------------------------------------------------------------
/docs/_examples/custom-scalars.edn:
--------------------------------------------------------------------------------
1 | (require
2 | '[clojure.spec.alpha :as s]
3 | '[com.walmartlabs.lacinia :as g]
4 | '[com.walmartlabs.lacinia.schema :as schema])
5 |
6 | (import
7 | java.text.SimpleDateFormat
8 | java.util.Date)
9 |
10 | (def date-formatter
11 | "Used by custom scalar :Date."
12 | (SimpleDateFormat. "yyyy-MM-dd"))
13 |
14 | (def schema
15 | (schema/compile
16 | {:scalars
17 | {:Date
18 | {:parse #(when (string? %)
19 | (try
20 | (.parse date-formatter %)
21 | (catch Throwable _
22 | nil)))
23 | :serialize #(try
24 | (.format date-formatter %)
25 | (catch Throwable _
26 | nil))}}
27 |
28 | :queries
29 | {:today
30 | {:type :Date
31 | :resolve (fn [ctx args v] (Date.))}
32 | :echo
33 | {:type :Date
34 | :args {:input {:type :Date}}
35 | :resolve (fn [ctx {:keys [input]} v] input)}}}))
36 |
37 | (g/execute schema "{today}" nil nil)
38 | => {:data {:today "2018-11-21"}}
39 | (g/execute schema "{ echo(input: \"2018-11-22\") }" nil nil)
40 | => {:data {:echo "2018-11-22"}}
41 | (g/execute schema "{ echo(input: \"thanksgiving\") }" nil nil)
42 | =>
43 | {:errors [{:message "Exception applying arguments to field `echo': For argument `input', unable to convert \"thanksgiving\" to scalar type `Date'.",
44 | :locations [{:line 1, :column 3}],
45 | :extensions {:field :echo, :argument :input, :value "thanksgiving", :type-name :Date}}]}
46 |
47 |
--------------------------------------------------------------------------------
/test/com/walmartlabs/lacinia/parser/docs_test.clj:
--------------------------------------------------------------------------------
1 | ; Copyright (c) 2017-present Walmart, Inc.
2 | ;
3 | ; Licensed under the Apache License, Version 2.0 (the "License")
4 | ; you may not use this file except in compliance with the License.
5 | ; You may obtain a copy of the License at
6 | ;
7 | ; http://www.apache.org/licenses/LICENSE-2.0
8 | ;
9 | ; Unless required by applicable law or agreed to in writing, software
10 | ; distributed under the License is distributed on an "AS IS" BASIS,
11 | ; WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | ; See the License for the specific language governing permissions and
13 | ; limitations under the License.
14 |
15 | (ns com.walmartlabs.lacinia.parser.docs-test
16 | (:require
17 | [clojure.test :refer [deftest is]]
18 | [com.walmartlabs.lacinia.parser.docs :refer [parse-docs]]
19 | [clojure.java.io :as io]))
20 |
21 | (defn ^:private parse
22 | [path]
23 | (-> path
24 | io/resource
25 | slurp
26 | parse-docs))
27 |
28 | (deftest basic-parse
29 | (is (= {:Character "A person who appears in one of the movies.
30 |
31 | Persons are more than humans, and may include droids and computers."
32 | :Character/name "The primary name of the character.
33 |
34 | For droids, this is the announced name, such as \"AreToo\"."}
35 | (parse "basic-docs.md"))))
36 |
37 | (deftest empty-parse
38 | (is (= {}
39 | (parse-docs ""))))
40 |
41 | (deftest ignores-before-first-header
42 | (is (= {:Customer "Someone we sell to."}
43 | (parse "preamble-docs.md"))))
44 |
--------------------------------------------------------------------------------
/test/com/walmartlabs/lacinia/field_tests.clj:
--------------------------------------------------------------------------------
1 | ; Copyright (c) 2019-present Walmart, Inc.
2 | ;
3 | ; Licensed under the Apache License, Version 2.0 (the "License")
4 | ; you may not use this file except in compliance with the License.
5 | ; You may obtain a copy of the License at
6 | ;
7 | ; http://www.apache.org/licenses/LICENSE-2.0
8 | ;
9 | ; Unless required by applicable law or agreed to in writing, software
10 | ; distributed under the License is distributed on an "AS IS" BASIS,
11 | ; WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | ; See the License for the specific language governing permissions and
13 | ; limitations under the License.
14 |
15 | (ns com.walmartlabs.lacinia.field-tests
16 | (:require
17 | [clojure.test :refer [deftest is]]
18 | [com.walmartlabs.test-utils :refer [expect-exception]]
19 | [com.walmartlabs.lacinia.schema :as schema]))
20 |
21 | (deftest field-references-unknown-type
22 | (expect-exception
23 | "Field `Insect/legs' references unknown type `Six'."
24 | {:field-name :Insect/legs
25 | :schema-types {:object [:Insect
26 | :Mutation
27 | :Query
28 | :Subscription]
29 | :scalar [:Boolean
30 | :Float
31 | :ID
32 | :Int
33 | :String]}}
34 | (schema/compile {:objects
35 | {:Insect
36 | {:fields
37 | {:legs {:type :Six}}}}})))
38 |
--------------------------------------------------------------------------------
/docs/_examples/tutorial/db-1.clj:
--------------------------------------------------------------------------------
1 | (ns my.clojure-game-geek.db
2 | (:require [clojure.edn :as edn]
3 | [clojure.java.io :as io]
4 | [com.stuartsierra.component :as component]))
5 |
6 | (defrecord ClojureGameGeekDb [data]
7 |
8 | component/Lifecycle
9 |
10 | (start [this]
11 | (assoc this :data (-> (io/resource "cgg-data.edn")
12 | slurp
13 | edn/read-string
14 | atom)))
15 |
16 | (stop [this]
17 | (assoc this :data nil)))
18 |
19 | (defn find-game-by-id
20 | [db game-id]
21 | (->> db
22 | :data
23 | deref
24 | :games
25 | (filter #(= game-id (:id %)))
26 | first))
27 |
28 | (defn find-member-by-id
29 | [db member-id]
30 | (->> db
31 | :data
32 | deref
33 | :members
34 | (filter #(= member-id (:id %)))
35 | first))
36 |
37 | (defn list-designers-for-game
38 | [db game-id]
39 | (let [designers (:designers (find-game-by-id db game-id))]
40 | (->> db
41 | :data
42 | deref
43 | :designers
44 | (filter #(contains? designers (:id %))))))
45 |
46 | (defn list-games-for-designer
47 | [db designer-id]
48 | (->> db
49 | :data
50 | deref
51 | :games
52 | (filter #(-> % :designers (contains? designer-id)))))
53 |
54 | (defn list-ratings-for-game
55 | [db game-id]
56 | (->> db
57 | :data
58 | deref
59 | :ratings
60 | (filter #(= game-id (:game-id %)))))
61 |
62 | (defn list-ratings-for-member
63 | [db member-id]
64 | (->> db
65 | :data
66 | deref
67 | :ratings
68 | (filter #(= member-id (:member-id %)))))
69 |
--------------------------------------------------------------------------------
/src/com/walmartlabs/lacinia/expound.clj:
--------------------------------------------------------------------------------
1 | ; Copyright (c) 2017-present Walmart, Inc.
2 | ;
3 | ; Licensed under the Apache License, Version 2.0 (the "License")
4 | ; you may not use this file except in compliance with the License.
5 | ; You may obtain a copy of the License at
6 | ;
7 | ; http://www.apache.org/licenses/LICENSE-2.0
8 | ;
9 | ; Unless required by applicable law or agreed to in writing, software
10 | ; distributed under the License is distributed on an "AS IS" BASIS,
11 | ; WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | ; See the License for the specific language governing permissions and
13 | ; limitations under the License.
14 |
15 | (ns com.walmartlabs.lacinia.expound
16 | "Adds improved spec messages to Lacinia specs, when using [Expound](https://github.com/bhb/expound).
17 |
18 | This namespace should simply be required; it exports no functions or constants.
19 |
20 | Expound is an optional library for Lacinia and must be added to your project explicitly."
21 | {:added "0.26.0"}
22 | (:require
23 | [expound.alpha :refer [defmsg]]
24 | [com.walmartlabs.lacinia.schema :as schema]))
25 |
26 | (defmsg ::schema/resolver-type "implement the com.walmartlabs.lacinia.resolve/FieldResolver protocol")
27 |
28 | (defmsg ::schema/wrapped-type-modifier "type wrappers should be either (list type) or (non-null type)")
29 |
30 | (defmsg ::schema/graphql-identifier "must be a valid GraphQL identifier: contain only letters, numbers, and underscores")
31 |
32 | (defmsg ::schema/not-a-conformer "scalar parse and serialize functions must now be simple functions, not clojure.spec conformers (see release notes)")
33 |
--------------------------------------------------------------------------------
/docs/_examples/tutorial/schema-2.clj:
--------------------------------------------------------------------------------
1 | (ns my.clojure-game-geek.schema
2 | "Contains custom resolvers and a function to provide the full schema."
3 | (:require [clojure.java.io :as io]
4 | [com.walmartlabs.lacinia.util :as util]
5 | [com.walmartlabs.lacinia.schema :as schema]
6 | [clojure.edn :as edn]))
7 |
8 | (defn resolve-game-by-id
9 | [games-map context args value]
10 | (let [{:keys [id]} args]
11 | (get games-map id)))
12 |
13 | (defn resolve-board-game-designers
14 | [designers-map context args board-game]
15 | (->> board-game
16 | :designers
17 | (map designers-map)))
18 |
19 | (defn resolve-designer-games
20 | [games-map context args designer]
21 | (let [{:keys [id]} designer]
22 | (->> games-map
23 | vals
24 | (filter #(-> % :designers (contains? id))))))
25 |
26 | (defn entity-map
27 | [data k]
28 | (reduce #(assoc %1 (:id %2) %2)
29 | {}
30 | (get data k)))
31 |
32 | (defn resolver-map
33 | []
34 | (let [cgg-data (-> (io/resource "cgg-data.edn")
35 | slurp
36 | edn/read-string)
37 | games-map (entity-map cgg-data :games)
38 | designers-map (entity-map cgg-data :designers)]
39 | {:Query/gameById (partial resolve-game-by-id games-map)
40 | :BoardGame/designers (partial resolve-board-game-designers designers-map)
41 | :Designer/games (partial resolve-designer-games games-map)}))
42 |
43 | (defn load-schema
44 | []
45 | (-> (io/resource "cgg-schema.edn")
46 | slurp
47 | edn/read-string
48 | (util/inject-resolvers (resolver-map))
49 | schema/compile))
50 |
--------------------------------------------------------------------------------
/docs/roots.rst:
--------------------------------------------------------------------------------
1 | Root Object Names
2 | =================
3 |
4 | Top-level query, mutation, and subscription operations are represented in Lacinia as fields on
5 | special objects.
6 | These objects are the "operation root objects".
7 | The default names of these objects are ``Query``, ``Mutation``, and ``Subscription``.
8 |
9 | When compiling an input schema, these objects will be created if they do not already exist.
10 |
11 | The ``:roots`` key of the input schema is used to override the default names. Inside ``:roots``, you
12 | may specify keys ``:query``, ``:mutation``, or ``:subscription``, with the value being the name of the
13 | corresponding object. There is rarely a need to rename the objects from default values, however.
14 |
15 | If the objects already exist, then
16 | any fields on the objects are automatically available operations.
17 | In the larger GraphQL world (beyond Lacinia), this is the typical way of defining operations.
18 |
19 | Beyond that, the operations from the ``:queries``, ``:mutations``, and ``:subscriptions`` maps of the input schema will be
20 | merged into the fields of the corresponding root object; this is supported, but not prefered.
21 |
22 | Name collisions are not allowed; a schema compile exception is thrown if an operation (via ``:queries``, etc.)
23 | conflicts with a field of the corresponding root object.
24 |
25 | Unions
26 | ------
27 |
28 | It is allowed for the root type to be a union.
29 | This can be a handy way to organize many different operations.
30 |
31 | In this case, Lacinia creates a new object
32 | and merges the fields of the members of the union, along with any operations from the input schema.
33 |
34 | Again, the field names must not conflict.
35 |
36 | The new object becomes the root operation object.
37 |
--------------------------------------------------------------------------------
/docs/_examples/tutorial/cgg-data-3.edn:
--------------------------------------------------------------------------------
1 | {:games
2 | [{:id "1234"
3 | :name "Zertz"
4 | :summary "Two player abstract with forced moves and shrinking board"
5 | :designers #{"200"}
6 | :minPlayers 2
7 | :maxPlayers 2}
8 | {:id "1235"
9 | :name "Dominion"
10 | :summary "Created the deck-building genre; zillions of expansions"
11 | :designers #{"204"}
12 | :minPlayers 2}
13 | {:id "1236"
14 | :name "Tiny Epic Galaxies"
15 | :summary "Fast dice-based sci-fi space game with a bit of chaos"
16 | :designers #{"203"}
17 | :minPlayers 1
18 | :maxPlayers 4}
19 | {:id "1237"
20 | :name "7 Wonders: Duel"
21 | :summary "Tense, quick card game of developing civilizations"
22 | :designers #{"201" "202"}
23 | :minPlayers 2
24 | :maxPlayers 2}]
25 |
26 | :members
27 | [{:id "37"
28 | :name "curiousattemptbunny"}
29 | {:id "1410"
30 | :name "bleedingedge"}
31 | {:id "2812"
32 | :name "missyo"}]
33 |
34 | :ratings
35 | [{:member-id "37" :game-id "1234" :rating 3}
36 | {:member-id "1410" :game-id "1234" :rating 5}
37 | {:member-id "1410" :game-id "1236" :rating 4}
38 | {:member-id "1410" :game-id "1237" :rating 4}
39 | {:member-id "2812" :game-id "1237" :rating 4}
40 | {:member-id "37" :game-id "1237" :rating 5}]
41 |
42 | :designers
43 | [{:id "200"
44 | :name "Kris Burm"
45 | :url "http://www.gipf.com/project_gipf/burm/burm.html"}
46 | {:id "201"
47 | :name "Antoine Bauza"
48 | :url "http://www.antoinebauza.fr/"}
49 | {:id "202"
50 | :name "Bruno Cathala"
51 | :url "http://www.brunocathala.com/"}
52 | {:id "203"
53 | :name "Scott Almes"}
54 | {:id "204"
55 | :name "Donald X. Vaccarino"}]}
56 |
--------------------------------------------------------------------------------
/test/com/walmartlabs/lacinia/input_types_test.clj:
--------------------------------------------------------------------------------
1 | ; Copyright (c) 2021-present Walmart, Inc.
2 | ;
3 | ; Licensed under the Apache License, Version 2.0 (the "License")
4 | ; you may not use this file except in compliance with the License.
5 | ; You may obtain a copy of the License at
6 | ;
7 | ; http://www.apache.org/licenses/LICENSE-2.0
8 | ;
9 | ; Unless required by applicable law or agreed to in writing, software
10 | ; distributed under the License is distributed on an "AS IS" BASIS,
11 | ; WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | ; See the License for the specific language governing permissions and
13 | ; limitations under the License.
14 |
15 | (ns com.walmartlabs.lacinia.input-types-test
16 | (:require [clojure.test :refer [deftest is]]
17 | [com.walmartlabs.test-utils :refer [execute compile-schema-injected]]))
18 |
19 | (deftest mix-of-literals-and-dynamics
20 | (let [resolver (fn [_ args _]
21 | (is (= {:filter {:or [{:color {:equals "blue"}}
22 | {:color {:equals "red"}}]}}
23 | args))
24 | {:nodes [{:color "blue"}]})
25 | schema (compile-schema-injected "dyn-args-schema.edn"
26 | {:queries/cars resolver})
27 | query "query($color: String) {
28 | cars(filter: {or: [{color: {equals: \"blue\"}},
29 | {color: {equals: $color}}]}) {
30 | nodes {
31 | color
32 | }
33 | }
34 | }"
35 | args {:color "red"}]
36 | (is (= {:data
37 | {:cars
38 | {:nodes [{:color "blue"}]}}}
39 | (execute schema query args nil)))))
40 |
41 |
42 |
--------------------------------------------------------------------------------
/docs/unions.rst:
--------------------------------------------------------------------------------
1 | Unions
2 | ======
3 |
4 | A union type is a type that may be any of a list of possible objects.
5 |
6 | .. sidebar:: GraphQL Spec
7 |
8 | Read about :spec:`unions `.
9 |
10 | A union is a type defined in terms of different objects:
11 |
12 | .. literalinclude:: _examples/union-definition.edn
13 | :language: clojure
14 |
15 | A union definition must include a ``:members`` key, a sequence of object types.
16 |
17 | The above example identifies the ``SearchResult` union type to be either a ``Person`` (with fields
18 | ``name`` and ``age``), or a ``Photo`` (with fields ``imageURL``, ``title``, ``height``, and ``width``).
19 |
20 | Unions must define at least one type; each member type must be an object type (members may not be
21 | reference scalar types, interfaces, or other unions).
22 |
23 | When a client makes a union request, they must use the fragment spread syntax to identify what
24 | is to be returned based on the runtime type of object:
25 |
26 | .. code-block:: js
27 |
28 | { search (term:"ewok") {
29 | ... on Person { name }
30 | ... on Photo { imageURL title }
31 | }}
32 |
33 | This breaks down what will be returned in the result map based on the type of the value produced
34 | by the ``search`` query. Sometimes there will be a ``name`` key in the result, and other times
35 | an ``image-url`` and ``title`` key.
36 | This may vary result by result even within a single request:
37 |
38 | .. literalinclude:: _examples/unions-query-response.edn
39 | :language: clojure
40 |
41 | .. tip::
42 |
43 | When a field or operation type is a union,
44 | the field resolver may return any of a number of different
45 | concrete object types, and Lacinia has no way to determine which;
46 | this information must be :doc:`explicitly provided `.
47 |
--------------------------------------------------------------------------------
/docs/_examples/fed/products.edn:
--------------------------------------------------------------------------------
1 | (ns products.server
2 | (:require
3 | [io.pedestal.http :as http]
4 | [clojure.java.io :as io]
5 | [com.walmartlabs.lacinia.pedestal2 :as lp]
6 | [com.walmartlabs.lacinia.parser.schema :refer [parse-schema]]
7 | [com.walmartlabs.lacinia.schema :as schema]
8 | [com.walmartlabs.lacinia.util :as util]))
9 |
10 | (defn resolve-users-external
11 | [_ _ reps]
12 | (for [{:keys [id]} reps]
13 | (schema/tag-with-type {:id id}
14 | :User)))
15 |
16 | (defn get-product-by-upc
17 | [context upc]
18 | ;; Peform DB query here, return map with :upc, :name, :price
19 | )
20 |
21 | (defn get-favorite-products-for-user
22 | [context user-id]
23 | ;; Perform DB query here, return seq of maps with :upc, :name, :price
24 | )
25 |
26 | (defn resolve-products-internal
27 | [context _ reps]
28 | (for [{:keys [upc]} reps
29 | :let [product (get-product-by-upc context upc)]]
30 | (schema/tag-with-type product :Product)))
31 |
32 | (defn resolve-product-by-upc
33 | [context {:keys [upc]} _]
34 | (get-product-by-upc context upc))
35 |
36 | (defn resolve-favorite-products
37 | [context _ user]
38 | (let [{:keys [id]} user]
39 | (get-favorite-products-for-user context id)))
40 |
41 | (defn products-schema
42 | []
43 | (-> "products.gql"
44 | io/resource
45 | slurp
46 | (parse-schema {:federation {:entity-resolvers {:Product resolve-products-internal
47 | :User resolve-users-external}}})
48 | (util/inject-resolvers {:Query/productByUpc #'resolve-product-by-upc
49 | :User/favoriteProducts #'resolve-favorite-products})
50 | schema/compile))
51 |
52 | (defn start
53 | []
54 | (-> (products-schema)
55 | lp/default-service
56 | http/create-server
57 | http/start))
58 |
--------------------------------------------------------------------------------
/docs/_examples/tutorial/schema-3.clj:
--------------------------------------------------------------------------------
1 | (ns my.clojure-game-geek.schema
2 | "Contains custom resolvers and a function to provide the full schema."
3 | (:require [clojure.java.io :as io]
4 | [com.stuartsierra.component :as component]
5 | [com.walmartlabs.lacinia.util :as util]
6 | [com.walmartlabs.lacinia.schema :as schema]
7 | [clojure.edn :as edn]))
8 |
9 | (defn resolve-game-by-id
10 | [games-map context args value]
11 | (let [{:keys [id]} args]
12 | (get games-map id)))
13 |
14 | (defn resolve-board-game-designers
15 | [designers-map context args board-game]
16 | (->> board-game
17 | :designers
18 | (map designers-map)))
19 |
20 | (defn resolve-designer-games
21 | [games-map context args designer]
22 | (let [{:keys [id]} designer]
23 | (->> games-map
24 | vals
25 | (filter #(-> % :designers (contains? id))))))
26 |
27 | (defn entity-map
28 | [data k]
29 | (reduce #(assoc %1 (:id %2) %2)
30 | {}
31 | (get data k)))
32 |
33 | (defn resolver-map
34 | []
35 | (let [cgg-data (-> (io/resource "cgg-data.edn")
36 | slurp
37 | edn/read-string)
38 | games-map (entity-map cgg-data :games)
39 | designers-map (entity-map cgg-data :designers)]
40 | {:Query/gameById (partial resolve-game-by-id games-map)
41 | :BoardGame/designers (partial resolve-board-game-designers designers-map)
42 | :Designer/games (partial resolve-designer-games games-map)}))
43 |
44 | (defn load-schema
45 | []
46 | (-> (io/resource "cgg-schema.edn")
47 | slurp
48 | edn/read-string
49 | (util/inject-resolvers (resolver-map))
50 | schema/compile))
51 |
52 | (defrecord SchemaProvider [schema]
53 |
54 | component/Lifecycle
55 |
56 | (start [this]
57 | (assoc this :schema (load-schema)))
58 |
59 | (stop [this]
60 | (assoc this :schema nil)))
61 |
--------------------------------------------------------------------------------
/docs/tutorial/prereqs.rst:
--------------------------------------------------------------------------------
1 | Pre-Requisites
2 | ==============
3 |
4 | You should have a basic understanding of GraphQL, which you can pick up from this documentation,
5 | and from `the GraphQL home page `_.
6 |
7 | You should be familiar with, but by no means an expert in, Clojure.
8 |
9 | You should have a `recent build of Clojure `_, including the ``clj`` command.
10 |
11 | You should have an editor or IDE ready to go, set up for editing Clojure code.
12 |
13 | A skim of the Lacinia reference documentation (the rest of this manual, outside of
14 | this tutorial) is also helpful, or you can follow links provided as we go.
15 |
16 | The later chapters use a database stored in a Docker container [#dockerfn]_ ;
17 | you should download and install `Docker `_ and
18 | ensure that you can run the ``docker`` command.
19 |
20 | With those basics installed and ready, we can build an empty project and work from there, but first
21 | we'll talk about the application we will be building.
22 |
23 | .. [#dockerfn] A Docker container is
24 | the `Inception `_ of computers; a
25 | container is essentially a
26 | light-weight virtual machine that runs inside your computer.
27 |
28 | To the `PostgreSQL `_ server we'll be running inside the container, it will appear as if
29 | the entire computer is running Linux, just as if Linux and PostgreSQL were installed
30 | on a bare-metal computer.
31 |
32 | Docker images
33 | are smaller and less demanding than full operating system virtual machines. In fact
34 | frequently you will run several interconnected containers together.
35 |
36 | Docker includes infrastructure for downloading the images from a central repository.
37 | Ultimately, it's faster and easier to get PostgreSQL running
38 | inside a container that to install the database onto your computer.
39 |
--------------------------------------------------------------------------------
/docs/resolve/examples.rst:
--------------------------------------------------------------------------------
1 | Examples
2 | ========
3 |
4 | Formatting a Date
5 | -----------------
6 |
7 | This is a common scenario: you have a Date or Timestamp value and it needs to be
8 | in a specific format in the result map.
9 |
10 | In this example, the field resolver will extract the key from the
11 | container's resolved value, and format it:
12 |
13 | .. code-block:: clojure
14 |
15 | (defn resolve-updated-at
16 | [context args resolved-value]
17 | (->> resolved-value
18 | :updated-at
19 | (format "%tm-%`_.
7 |
8 | Timing collection is enabled by passing the context through the
9 | :api:`tracing/enable-tracing`
10 | function:
11 |
12 | .. literalinclude:: /_examples/tracing.edn
13 | :language: clojure
14 |
15 | .. sidebar:: Extensions key?
16 |
17 | GraphQL supports a third result key, ``extensions``, as
18 | described in :spec:`the spec `.
19 | It exists just for this kind of extra information in the response.
20 |
21 | Note that tracing is an `execution` option, not a `schema compilation` option; it's just a matter of setting
22 | up the application context (via ``enable-tracing``) before parsing and executing the query.
23 |
24 | When the field resolvers are :doc:`asynchronous `, you'll often see that the ``startOffset`` and ``duration``
25 | of multiple fields represent overlapping time periods, which is exactly what you want.
26 |
27 | Generally, resolver tracing maps are added to the list in order of completion, but the exact order is
28 | not guaranteed.
29 |
30 | Enabling tracing adds a lot of overhead to execution and parsing, both for the essential overhead of collecting
31 | the tracing information, but also because certain optimizations are disabled when tracing is enabled;
32 | for example default field resolvers (where no explicit resolver is provided) are heavily optimized normally, to
33 | avoid unecessary function calls and object creation.
34 |
35 | .. warning::
36 |
37 | Tracing should never be enabled in production. This can be accomplished by removing the ``com.walmartlabs.lacinia.pedestal2/enable-tracing-interceptor``
38 | interceptor from the pipeline (when using lacinia-pedestal).
39 |
40 |
41 |
42 |
43 |
44 |
--------------------------------------------------------------------------------
/test/com/walmartlabs/lacinia/mutable_context_tests.clj:
--------------------------------------------------------------------------------
1 | ; Copyright (c) 2017-present Walmart, Inc.
2 | ;
3 | ; Licensed under the Apache License, Version 2.0 (the "License")
4 | ; you may not use this file except in compliance with the License.
5 | ; You may obtain a copy of the License at
6 | ;
7 | ; http://www.apache.org/licenses/LICENSE-2.0
8 | ;
9 | ; Unless required by applicable law or agreed to in writing, software
10 | ; distributed under the License is distributed on an "AS IS" BASIS,
11 | ; WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | ; See the License for the specific language governing permissions and
13 | ; limitations under the License.
14 |
15 | (ns com.walmartlabs.lacinia.mutable-context-tests
16 | (:require
17 | [clojure.test :refer [deftest is]]
18 | [com.walmartlabs.lacinia.resolve :as resolve]
19 | [com.walmartlabs.test-utils :refer [compile-schema execute]]))
20 |
21 | (def ^:private schema (compile-schema "mutable-context-schema.edn"
22 | {:root (fn [_ args _]
23 | (cond-> {:container {:id "0001"
24 | :leaf "DEFAULT"}}
25 | (:trigger args) (resolve/with-context {::leaf-value "OVERRIDE"})))
26 | :leaf (fn [context _ container]
27 | (or (::leaf-value context)
28 | (:leaf container)))}))
29 |
30 | (deftest resolver-may-modify-nested-context
31 | (is (= {:data {:disabled {:container {:id "0001"
32 | :leaf "DEFAULT"}}
33 | :enabled {:container {:id "0001"
34 | :leaf "OVERRIDE"}}}}
35 | (execute schema
36 | "{ enabled: root(trigger: true) { container { id leaf }}
37 | disabled: root { container { id leaf }}}"))))
38 |
--------------------------------------------------------------------------------
/docs/resolve/context.rst:
--------------------------------------------------------------------------------
1 | Application Context
2 | ===================
3 |
4 | The application context passed to your field resolvers is normally set by the initial call to
5 | :api:`/execute`.
6 | Lacinia uses the context for its own book-keeping (the keys it places into the map are namespaced to
7 | avoid collisions) but otherwise the same map is passed to all field resolvers.
8 |
9 | In specific cases, it is useful to allow a field resolver to modify the application context, with the change
10 | exposed just to the fields nested below, but to any depth.
11 |
12 | For example, in this query::
13 |
14 | {
15 | products(search: "fuzzy") {
16 | category {
17 | name
18 | product {
19 | upc
20 | name
21 | highlightedName
22 | }
23 | }
24 | }
25 |
26 | Here, the search term is provided to the ``products`` field, but is again needed by the ``highlightedName``
27 | field, to highlight the parts of the name that match the search term.
28 |
29 | The resolver for the ``products`` field can communicate this information
30 | "down tree" to the resolver for the ``highlighted_name`` field, by
31 | using the :api:`resolve/with-context` function.
32 |
33 | .. literalinclude:: /_examples/mutable-context.edn
34 | :language: clojure
35 | :emphasize-lines: 7
36 |
37 | The map provided to ``with-context`` will be merged into the application context before any nested resolvers are invoked.
38 | In this way, the new key, ``::search-term``, is only present in the context for field resolvers below ``products`` field.
39 |
40 | Some field resolvers returns lists of values; the entire list can be wrapped in this way **OR** individual values within the
41 | list may be wrapped.
42 |
43 | .. tip::
44 |
45 | Remember that query execution is top to bottom, then the final result map is assembled from the leaves back up to
46 | the roots.
47 |
48 |
49 | When using `lacinia-pedestal `_, the default behavior is to capture the Ring request map and supply it
50 | in the application context under the ``:request`` key.
51 |
--------------------------------------------------------------------------------
/docs/samples.rst:
--------------------------------------------------------------------------------
1 | Sample Projects
2 | ===============
3 |
4 | `boardgamegeek-graphql-proxy `_
5 | Howard Lewis Ship created this simple proxy to expose part of the
6 | `BoardGameGeek `_ database as GraphQL, using Lacinia.
7 |
8 | It was used for examples in his
9 | Clojure/West 2017 talk: `Power to the (Mobile) People: Clojure and GraphQL `_.
10 |
11 | `leaderboard-api `_
12 | A simple API to track details about games and high scores.
13 | Built on top of Compojure and PostgreSQL.
14 | See `this blog post `_ by the author.
15 |
16 | `Event sourcing tutorial `_
17 | This project consists of multiple components creating a bank simulation.
18 | The `graphql-endpoint `_
19 | leverages Kafka to do queries, mutations and subscriptions.
20 | Also part of the project is a `frontend `_
21 | using `re-graph `_.
22 |
23 | `Fullstack Learning Project `_
24 | A port of `The Fullstack Tutorial for GraphQL `_, ported to Clojure and Lacinia.
25 |
26 | `Hacker News GraphQL `_
27 | A version of Hacker News implemented using GraphQL and `Datomic `_ on the backend,
28 | and `re-frame `_ on the front end.
29 |
30 | `Lacinia LDAP backend `_
31 | A sample library for querying a LDAP/Active directory using GraphQL
32 |
33 | `Lacinia Qliksense backend `_
34 | A sample library for querying an Qliksense server (Repository API) using GraphQL
35 |
--------------------------------------------------------------------------------
/docs/json-scalar-example.md:
--------------------------------------------------------------------------------
1 | # Example: Raw JSON Scalar in Lacinia
2 |
3 | ## Motivation
4 |
5 | It is common in GraphQL APIs to need a field that can accept or return arbitrary JSON data. Lacinia does not provide a built-in JSON scalar, but it is straightforward to define one. This example demonstrates how to implement and use a raw JSON scalar in your Lacinia schema.
6 | ## Recommendations and Warnings
7 |
8 | - Always validate incoming JSON data to avoid security issues and unexpected errors.
9 | - Consider restricting the structure of the JSON if possible, to make your API more predictable.
10 | - Document clearly which fields use the JSON scalar and what kind of data is expected.
11 | - Be aware that large or deeply nested JSON objects may impact performance.
12 | ## Example GraphQL Query and Response
13 |
14 | Suppose you have a field in your schema that returns a JSON value:
15 |
16 | ```edn
17 | {:objects
18 | {:Root
19 | {:fields
20 | {:config {:type :JSON}}}}}
21 | ```
22 |
23 | You can query this field as follows:
24 |
25 | ```graphql
26 | query {
27 | config
28 | }
29 | ```
30 |
31 | And the response might look like:
32 |
33 | ```json
34 | {
35 | "data": {
36 | "config": {
37 | "enabled": true,
38 | "threshold": 42,
39 | "options": ["a", "b", "c"]
40 | }
41 | }
42 | }
43 | ```
44 | ## Implementing the Scalar Functions in Clojure
45 |
46 | You can use the [cheshire](https://github.com/dakrone/cheshire) library to parse and generate JSON in Clojure. Here is an example implementation:
47 |
48 | ```clojure
49 | (ns my-app.json-scalar
50 | (:require [cheshire.core :as json]))
51 |
52 | (defn parse-json [value]
53 | (try
54 | (json/parse-string value true)
55 | (catch Exception _
56 | nil)))
57 |
58 | (defn serialize-json [value]
59 | (json/generate-string value))
60 | ```
61 |
62 | Make sure to add `cheshire` to your dependencies in `deps.edn`:
63 |
64 | ```clojure
65 | ;; deps.edn
66 | {:deps {cheshire {:mvn/version "5.11.0"}}}
67 | ```
68 | type Product {
69 | id: ID!
70 | metadata: JSON
71 | }
72 |
73 | type Query {
74 | getProduct(id: ID!): Product
75 | ...existing code...
76 |
--------------------------------------------------------------------------------
/docs/federation/external.rst:
--------------------------------------------------------------------------------
1 | External Entities
2 | =================
3 |
4 | The previous example showed an internal entity that can be extended; this example shows a different service providing
5 | its own internal entity, but also extending the ``User`` entity.
6 |
7 | .. literalinclude:: /_examples/fed/external.gql
8 |
9 | Note the use of the ``@extends`` directive, this indicates that ``User`` (in the products service) is a stub for the full
10 | ``User`` entity in the users service.
11 |
12 | You must ensure that the external ``User`` includes the same ``@key`` directive (or directives), and the same primary key
13 | fields; here ``id`` must be present, since it is part of the primary key.
14 | The ``@external`` directive indicates that the field is provided by another service (the users service).
15 |
16 | The ``favoriteProducts`` field on ``User`` is an addition provided by this service, the products service.
17 | Like any other field, a resolver must be provided for it.
18 | We'll see how that works shortly.
19 |
20 | Notice that this service adds the ``productByUpc`` query to the ``Query`` object; the Apollo GraphQL gateway
21 | merges together all the queries defined by all the implementing services.
22 |
23 | Again, the point of the gateway is that it mixes and matches from all the implementing services; clients should
24 | *only* go through the gateway since that's the only place where this merged view of all the individual schemas
25 | exists.
26 |
27 | The gateway is capable of building a plan that involves multiple steps to satisfy a client query.
28 |
29 | For example, consider this gateway query:
30 |
31 | .. literalinclude:: /_examples/fed/query.gql
32 |
33 | The gateway will start with a query to the users service; it will invoke the ``userById`` query there and will
34 | select both the ``name`` field (as specified in the client query) and the ``id`` field (since that's specified
35 | in the ``@key`` directive on the ``User`` entity).
36 |
37 | A second query will be sent to the products service.
38 | This query is used to get those favorite products; but to understand exactly
39 | how that works, we must first discuss `representations`.
--------------------------------------------------------------------------------
/docs/_examples/tutorial/db-2.clj:
--------------------------------------------------------------------------------
1 | (ns my.clojure-game-geek.db
2 | (:require [clojure.edn :as edn]
3 | [clojure.java.io :as io]
4 | [com.stuartsierra.component :as component]))
5 |
6 | (defrecord ClojureGameGeekDb [data]
7 |
8 | component/Lifecycle
9 |
10 | (start [this]
11 | (assoc this :data (-> (io/resource "cgg-data.edn")
12 | slurp
13 | edn/read-string
14 | atom)))
15 |
16 | (stop [this]
17 | (assoc this :data nil)))
18 |
19 | (defn find-game-by-id
20 | [db game-id]
21 | (->> db
22 | :data
23 | deref
24 | :games
25 | (filter #(= game-id (:id %)))
26 | first))
27 |
28 | (defn find-member-by-id
29 | [db member-id]
30 | (->> db
31 | :data
32 | deref
33 | :members
34 | (filter #(= member-id (:id %)))
35 | first))
36 |
37 | (defn list-designers-for-game
38 | [db game-id]
39 | (let [designers (:designers (find-game-by-id db game-id))]
40 | (->> db
41 | :data
42 | deref
43 | :designers
44 | (filter #(contains? designers (:id %))))))
45 |
46 | (defn list-games-for-designer
47 | [db designer-id]
48 | (->> db
49 | :data
50 | deref
51 | :games
52 | (filter #(-> % :designers (contains? designer-id)))))
53 |
54 | (defn list-ratings-for-game
55 | [db game-id]
56 | (->> db
57 | :data
58 | deref
59 | :ratings
60 | (filter #(= game-id (:game-id %)))))
61 |
62 | (defn list-ratings-for-member
63 | [db member-id]
64 | (->> db
65 | :data
66 | deref
67 | :ratings
68 | (filter #(= member-id (:member-id %)))))
69 |
70 | (defn ^:private apply-game-rating
71 | [game-ratings game-id member-id rating]
72 | (->> game-ratings
73 | (remove #(and (= game-id (:game-id %))
74 | (= member-id (:member-id %))))
75 | (cons {:game-id game-id
76 | :member-id member-id
77 | :rating rating})))
78 |
79 | (defn upsert-game-rating
80 | "Adds a new game rating, or changes the value of an existing game rating."
81 | [db game-id member-id rating]
82 | (-> db
83 | :data
84 | (swap! update :ratings apply-game-rating game-id member-id rating)))
85 |
--------------------------------------------------------------------------------
/docs/federation/reps.rst:
--------------------------------------------------------------------------------
1 | Representations
2 | ===============
3 |
4 | A `representation` is a map that can be transferred from one implementing service to another, within the same federation.
5 | This is necessary to allow work started in one service to continue in another; consider the query:
6 |
7 | .. literalinclude:: /_examples/fed/query.gql
8 |
9 | The gateway will query the ``User/favoriteProducts`` field on the products service as the second step on this query ... but
10 | where does the ``User`` come from?
11 |
12 | After the gateway performs the initial query on the users service, it builds a representation of the specific ``User``
13 | to pass to the products service, using information from the ``@key`` directive:
14 |
15 | .. code-block:: json
16 |
17 | {"__typename": "User",
18 | "id": "124c41"}
19 |
20 | This representation is JSON, and is passed to an implementing service's ``_entities`` query, which is automaticaly added
21 | to the implementing service's schema by Lacinia:
22 |
23 | .. literalinclude:: /_examples/fed/schema.gql
24 |
25 | The ``_Entity`` union will contain all entities, internal or external, in the local schema; for the products service, this
26 | will be ``User`` (external) and ``Product`` (internal).
27 |
28 | The ``_entities`` query exists to convert some number of representations (here, as scalar type ``_Any``) into entities
29 | (either stub entities or full entities).
30 | The gateway sends a request that passes the representations in, and uses fragments to extract the data needed
31 | by the original client query::
32 |
33 | query($representations:[_Any!]!) {
34 | _entities(representations:$representations) {
35 | ... on User {
36 | favoriteProducts {upc name price}
37 | }
38 | }
39 | }
40 |
41 | So, in the products service, the ``_entities`` resolver converts the representation into a stub ``User`` object,
42 | containing just enough information so that the ``favoriteProducts`` resolver can perform whatever database query
43 | it uses.
44 | The response from the products service is merged together with the response from the users service and a final
45 | response can be returned to the gateway service's client.
46 |
47 |
--------------------------------------------------------------------------------
/test/com/walmartlabs/lacinia/invariant_test.clj:
--------------------------------------------------------------------------------
1 | (ns com.walmartlabs.lacinia.invariant-test
2 | (:require [clojure.edn :as edn]
3 | [clojure.java.io :as io]
4 | [clojure.string :as str]
5 | [clojure.test :refer [deftest testing is]]
6 | [com.walmartlabs.lacinia :as lacinia]
7 | [com.walmartlabs.lacinia.schema :as schema]
8 | [com.walmartlabs.lacinia.util :as util]
9 | [com.walmartlabs.lacinia.parser :refer [parse-query invariant? prepare-with-query-variables]]
10 | [com.walmartlabs.test-schema :refer [test-schema]]
11 | [com.walmartlabs.test-utils :as utils :refer [simplify]]))
12 |
13 | (def ^:private compiled-schema (schema/compile test-schema))
14 |
15 | (deftest invariant-query-is-unchanged-by-prepare
16 | (let [parsed (parse-query compiled-schema "{ hero { name } }")
17 | prepared (prepare-with-query-variables parsed nil)]
18 | (is (invariant? parsed))
19 | (is (identical? parsed prepared))))
20 |
21 | (defmacro expect-invariant
22 | [q]
23 | `(is (= true (invariant? (parse-query compiled-schema ~q)))))
24 |
25 | (defmacro expect-variant
26 | [q]
27 | `(is (= false (invariant? (parse-query compiled-schema ~q)))))
28 |
29 | (deftest field-args-do-not-trigger-variant
30 | (expect-invariant "{ hero (episode: JEDI) { name }}"))
31 |
32 | (deftest fragments-do-not-trigger-variant
33 | (expect-invariant "
34 | {
35 | hero { ...character }
36 | }
37 |
38 | fragment character on character { id name }
39 | "))
40 |
41 | (deftest skip-or-include-are-variant
42 | (expect-variant "{ hero { id name @skip(if: true) } }")
43 |
44 | (expect-variant "
45 | {
46 | hero { ...character }
47 | }
48 |
49 | fragment character on character { id @skip(if: false) name }"))
50 |
51 | (deftest unused-variables-are-invariant
52 | (expect-invariant
53 | "query ($id: String) {
54 | hero { name }
55 | }"))
56 |
57 | (deftest used-variables-are-variant
58 | (expect-variant
59 | "query ($ep: episode) {
60 | hero(episode: $ep) { name }
61 | }")
62 |
63 |
64 | (expect-variant "
65 | query($ep: episode) { ...heroQuery }
66 |
67 | fragment heroQuery on Query {
68 | hero(episode: $ep) { name }
69 | }
70 |
71 | "))
72 |
73 |
74 |
75 |
76 |
77 |
78 |
--------------------------------------------------------------------------------
/src/com/walmartlabs/lacinia/parser/docs.clj:
--------------------------------------------------------------------------------
1 | ; Copyright (c) 2017-present Walmart, Inc.
2 | ;
3 | ; Licensed under the Apache License, Version 2.0 (the "License")
4 | ; you may not use this file except in compliance with the License.
5 | ; You may obtain a copy of the License at
6 | ;
7 | ; http://www.apache.org/licenses/LICENSE-2.0
8 | ;
9 | ; Unless required by applicable law or agreed to in writing, software
10 | ; distributed under the License is distributed on an "AS IS" BASIS,
11 | ; WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | ; See the License for the specific language governing permissions and
13 | ; limitations under the License.
14 |
15 | (ns com.walmartlabs.lacinia.parser.docs
16 | "Parsing of a Markdown file for purposes of attaching documentation to a schema."
17 | {:added "0.27.0"}
18 | (:require
19 | [clojure.string :as str]
20 | [com.walmartlabs.lacinia.internal-utils :refer [cond-let]]))
21 |
22 | (def ^:private header-re
23 | ;; Don't really care how deep the heading is
24 | #"\#+\s+(.*?)\s*$")
25 |
26 | (defn ^:private combine-block
27 | [lines]
28 | (->> lines
29 | (str/join "\n")
30 | str/trim))
31 |
32 | (defn parse-docs
33 | "Parses an input document. Returns a map who keys are the keyword versions of headers, and whose values
34 | are the (trimmed) content immediately beneath that header.
35 |
36 | The result is a documentation map that can be provided to [[inject-descriptions]]."
37 | [input]
38 | (loop [lines (str/split-lines input)
39 | result {}
40 | header nil
41 | block []]
42 | (cond-let
43 | (nil? lines)
44 | (if header
45 | (assoc result header (combine-block block))
46 | result)
47 |
48 | :let [line (first lines)
49 | more-lines (next lines)
50 | [_ new-header] (re-matches header-re line)]
51 |
52 | (some? new-header)
53 | (recur more-lines
54 | (if header
55 | (assoc result header (combine-block block))
56 | result)
57 | (keyword new-header)
58 | [])
59 |
60 | (some? header)
61 | (recur more-lines result header (conj block line))
62 |
63 | :else
64 | ;; In the preamble, if any, before first header.
65 | (recur more-lines result header block))))
66 |
--------------------------------------------------------------------------------
/test/com/walmartlabs/lacinia/typename_test.clj:
--------------------------------------------------------------------------------
1 | ; Copyright (c) 2017-present Walmart, Inc.
2 | ;
3 | ; Licensed under the Apache License, Version 2.0 (the "License")
4 | ; you may not use this file except in compliance with the License.
5 | ; You may obtain a copy of the License at
6 | ;
7 | ; http://www.apache.org/licenses/LICENSE-2.0
8 | ;
9 | ; Unless required by applicable law or agreed to in writing, software
10 | ; distributed under the License is distributed on an "AS IS" BASIS,
11 | ; WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | ; See the License for the specific language governing permissions and
13 | ; limitations under the License.
14 |
15 | (ns com.walmartlabs.lacinia.typename-test
16 | "Tests for the __typename metadata field on objects."
17 | (:require
18 | [clojure.test :refer [deftest is]]
19 | [com.walmartlabs.lacinia.schema :as schema]
20 | [com.walmartlabs.lacinia :refer [execute]]
21 | [com.walmartlabs.test-utils :refer [simplify compile-schema]]))
22 |
23 | (defn ^:private get-man
24 | [_ _ _]
25 | {:name "Adam"})
26 |
27 | (defn ^:private get-machine
28 | [_ _ _]
29 | {:serial "X01"})
30 |
31 | (defn ^:private get-all
32 | [_ _ _]
33 | [(schema/tag-with-type (get-man nil nil nil) :man)
34 | (schema/tag-with-type (get-machine nil nil nil) :machine)])
35 |
36 | (def ^:private compiled-schema
37 | (compile-schema "typename-schema.edn"
38 | {:get-man get-man
39 | :get-machine get-machine
40 | :get-all get-all}))
41 |
42 | (defn ^:private q
43 | [query]
44 | (simplify
45 | (execute compiled-schema query nil nil)))
46 |
47 | (deftest type-name-on-static-type
48 | ;; The :get_man query has a known concrete type, so
49 | ;; the type tag is supplied by Lacinia.
50 | (is (= {:data
51 | {:get_man
52 | {:__typename :man
53 | :name "Adam"}}}
54 | (q "{ get_man { __typename name }}"))))
55 |
56 | (deftest type-name-on-dynamic-types
57 | (is (= {:data {:get_all [{:__typename :man
58 | :name "Adam"}
59 | {:__typename :machine
60 | :serial "X01"}]}}
61 | (q "{ get_all { __typename
62 | ... on man { name }
63 | ... on machine { serial }
64 | }
65 | }"))))
66 |
--------------------------------------------------------------------------------
/test/com/walmartlabs/lacinia/timeout_test.clj:
--------------------------------------------------------------------------------
1 | ; Copyright (c) 2018-present Walmart, Inc.
2 | ;
3 | ; Licensed under the Apache License, Version 2.0 (the "License")
4 | ; you may not use this file except in compliance with the License.
5 | ; You may obtain a copy of the License at
6 | ;
7 | ; http://www.apache.org/licenses/LICENSE-2.0
8 | ;
9 | ; Unless required by applicable law or agreed to in writing, software
10 | ; distributed under the License is distributed on an "AS IS" BASIS,
11 | ; WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | ; See the License for the specific language governing permissions and
13 | ; limitations under the License.
14 |
15 | (ns com.walmartlabs.lacinia.timeout-test
16 | (:require
17 | [clojure.test :refer [deftest is]]
18 | [com.walmartlabs.lacinia :as lacinia]
19 | [com.walmartlabs.lacinia.schema :as schema]))
20 |
21 | (defn create-schema
22 | [timeout-ms]
23 | (schema/compile
24 | {:queries {:wait {:type :String
25 | :resolve (fn [_ _ _]
26 | (Thread/sleep timeout-ms)
27 | "ok")}}}))
28 |
29 | (def ^:private expected-result {:data {:wait "ok"}})
30 |
31 |
32 | (defmacro timed
33 | [& body]
34 | `(let [start# (System/currentTimeMillis)
35 | result# (do ~@body)
36 | elapsed# (- (System/currentTimeMillis) start#)]
37 | [elapsed# result#]))
38 |
39 | (defn execute
40 | [schema options]
41 | (lacinia/execute schema "{ wait }" nil nil options))
42 |
43 | (deftest no-timeout
44 | (let [schema (create-schema 100)
45 | [elapsed result] (timed (execute schema nil))]
46 | (is (= expected-result result))
47 | (is (<= 100 elapsed))))
48 |
49 | (deftest explicit-timeout
50 | (let [schema (create-schema 100)
51 | [elapsed result] (timed (execute schema {:timeout-ms 20}))]
52 | (is (= {:errors [{:message "Query execution timed out."}]} result))
53 | ;; Allow for some overhead ...
54 | (is (<= 20 elapsed 30))))
55 |
56 | (deftest explicit-error
57 | (let [schema (create-schema 100)
58 | [elapsed result] (timed (execute schema {:timeout-ms 50
59 | :timeout-error {:message "Too slow!"}}))]
60 | (is (= {:errors [{:message "Too slow!"}]} result))
61 | ;; Allow for some overhead ...
62 | (is (<= 50 elapsed 60))))
63 |
--------------------------------------------------------------------------------
/docs/_examples/tutorial/schema-5.clj:
--------------------------------------------------------------------------------
1 | (ns my.clojure-game-geek.schema
2 | "Contains custom resolvers and a function to provide the full schema."
3 | (:require [clojure.java.io :as io]
4 | [com.stuartsierra.component :as component]
5 | [com.walmartlabs.lacinia.util :as util]
6 | [com.walmartlabs.lacinia.schema :as schema]
7 | [my.clojure-game-geek.db :as db]
8 | [clojure.edn :as edn]))
9 |
10 | (defn game-by-id
11 | [db]
12 | (fn [_ args _]
13 | (db/find-game-by-id db (:id args))))
14 |
15 | (defn member-by-id
16 | [db]
17 | (fn [_ args _]
18 | (db/find-member-by-id db (:id args))))
19 |
20 | (defn board-game-designers
21 | [db]
22 | (fn [_ _ board-game]
23 | (db/list-designers-for-game db (:id board-game))))
24 |
25 | (defn designer-games
26 | [db]
27 | (fn [_ _ designer]
28 | (db/list-games-for-designer db (:id designer))))
29 |
30 | (defn rating-summary
31 | [db]
32 | (fn [_ _ board-game]
33 | (let [ratings (map :rating (db/list-ratings-for-game db (:id board-game)))
34 | n (count ratings)]
35 | {:count n
36 | :average (if (zero? n)
37 | 0
38 | (/ (apply + ratings)
39 | (float n)))})))
40 |
41 | (defn member-ratings
42 | [db]
43 | (fn [_ _ member]
44 | (db/list-ratings-for-member db (:id member))))
45 |
46 | (defn game-rating->game
47 | [db]
48 | (fn [_ _ game-rating]
49 | (db/find-game-by-id db (:game-id game-rating))))
50 |
51 | (defn resolver-map
52 | [component]
53 | (let [{:keys [db]} component]
54 | {:Query/gameById (game-by-id db)
55 | :Query/memberById (member-by-id db)
56 | :BoardGame/designers (board-game-designers db)
57 | :BoardGame/ratingSummary (rating-summary db)
58 | :Designer/games (designer-games db)
59 | :Member/ratings (member-ratings db)
60 | :GameRating/game (game-rating->game db)}))
61 |
62 | (defn load-schema
63 | [component]
64 | (-> (io/resource "cgg-schema.edn")
65 | slurp
66 | edn/read-string
67 | (util/inject-resolvers (resolver-map component))
68 | schema/compile))
69 |
70 | (defrecord SchemaProvider [db schema]
71 |
72 | component/Lifecycle
73 |
74 | (start [this]
75 | (assoc this :schema (load-schema this)))
76 |
77 | (stop [this]
78 | (assoc this :schema nil)))
79 |
--------------------------------------------------------------------------------
/test/com/walmartlabs/lacinia/roots_test.clj:
--------------------------------------------------------------------------------
1 | ; Copyright (c) 2017-present Walmart, Inc.
2 | ;
3 | ; Licensed under the Apache License, Version 2.0 (the "License")
4 | ; you may not use this file except in compliance with the License.
5 | ; You may obtain a copy of the License at
6 | ;
7 | ; http://www.apache.org/licenses/LICENSE-2.0
8 | ;
9 | ; Unless required by applicable law or agreed to in writing, software
10 | ; distributed under the License is distributed on an "AS IS" BASIS,
11 | ; WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | ; See the License for the specific language governing permissions and
13 | ; limitations under the License.
14 |
15 | (ns com.walmartlabs.lacinia.roots-test
16 | "Tests related to specifying operation root object names."
17 | (:require
18 | [clojure.test :refer [deftest is]]
19 | [com.walmartlabs.lacinia.schema :as schema]
20 | [com.walmartlabs.test-utils :refer [execute compile-schema]]))
21 |
22 | (deftest default-root-name
23 | (let [schema (schema/compile {})]
24 | (is (= {:data {:__schema {:queryType {:name "Query"}}}}
25 | (execute schema "{__schema { queryType { name } } }")))))
26 |
27 | (deftest can-specify-root-name
28 | (let [schema (schema/compile {:roots {:query :MyQueries}})]
29 | (is (= {:data {:__schema {:queryType {:name "MyQueries"}}}}
30 | (execute schema "{__schema { queryType { name } } }")))))
31 |
32 | (deftest root-object-may-contain-fields
33 | (let [schema (compile-schema "root-object-schema.edn"
34 | {:queries/fred (constantly "Flintstone")
35 | :Barney/last-name (constantly "Rubble")})]
36 | (is (= {:data {:barney "Rubble"
37 | :fred "Flintstone"}}
38 | (execute schema "{
39 | fred
40 | barney: last_name
41 | }")
42 | ))))
43 |
44 | (deftest name-collisions-are-detected
45 | (let [e (is (thrown? Exception
46 | (compile-schema "root-object-with-conflicts-schema.edn"
47 | {:queries/fred (constantly "Flintstone")
48 | :Barney/last-name (constantly "Rubble")})))]
49 | (is (= "Name collision compiling schema: `Barney/last_name' already exists with value from :queries."
50 | (.getMessage e)))
51 | (is (= {:field-name :last_name}
52 | (ex-data e)))))
53 |
54 |
--------------------------------------------------------------------------------
/dev-resources/large_lists.clj:
--------------------------------------------------------------------------------
1 | (ns large-lists
2 | (:require
3 | [clojure.spec.alpha :as s]
4 | [clojure.spec.gen.alpha :as gen]
5 | [criterium.core :as c]
6 | [com.walmartlabs.test-utils :refer [compile-schema]]
7 | [com.walmartlabs.lacinia.parser :as parser]
8 | [com.walmartlabs.lacinia :as lacinia]))
9 |
10 | (s/def ::name string?)
11 | (s/def ::age (s/int-in 1 100))
12 | (s/def ::id integer?)
13 | (s/def ::city string?)
14 | (s/def ::item (s/keys :req-un [::name ::age ::id ::city]))
15 |
16 | (def ^:private large-list
17 | (vec
18 | (repeatedly 5000
19 | #(gen/generate (s/gen ::item)))))
20 |
21 |
22 | (def schema (compile-schema "large-lists-schema.edn"
23 | {:resolve-list (constantly large-list)}))
24 |
25 | (defn bench-mapv
26 | []
27 | (binding [c/*report-progress* true]
28 | (c/bench (mapv #(select-keys % [:name :age :id]) large-list))))
29 |
30 | (defn bench-exec
31 | []
32 | (binding [c/*report-progress* true]
33 | (let [q "{ list { name age id }}"
34 | parsed (parser/parse-query schema q)]
35 | (c/bench
36 | (lacinia/execute-parsed-query parsed nil nil)))))
37 |
38 | (defn bench-parse-and-execute
39 | []
40 | (binding [c/*report-progress* true]
41 | (c/bench
42 | (lacinia/execute schema "{ list { name age id }}" nil nil))))
43 |
44 | (comment
45 | (bench-mapv)
46 | ;; 2.308310 ms
47 | ;; -- switch to bench --
48 | ;; 2.290889 ms
49 |
50 | (bench-exec)
51 | ;; 61.779221 ms -- base line
52 | ;; 59.036665 ms -- use hash-map instead of ordered-map
53 | ;; 66.580824 ms -- optimize (?!) combine-results (take 1)
54 | ;; 65.814538 ms -- optimize (?!) combine-results (take 2)
55 | ;; 67.019828 ms -- new base line (should match first base line, but eh, quick-bench)
56 | ;; 63.949198 ms -- optimize selector (removing some check steps)
57 | ;; 66.481703 ms -- remove executing timing penalty when not timing execution
58 | ;; 55.431582 ms -- optimize for single key/value pair (normal case outside of fragments)
59 | ;; -- switch to bench --
60 | ;; 58.143757 ms
61 | ;; 56.056103 ms -- use defrecord SelectionContext
62 | ;; 56.646881 ms -- less use of promises in ResolverResultPromise
63 | ;; 51.936539 ms -- tiny optimization on empty lists
64 | ;; 44.024393 ms -- leaf optimization
65 |
66 |
67 | (bench-parse-and-execute)
68 | ;; 67.814614 ms -- base line
69 | ;; -- switch to bench --
70 | )
71 |
72 |
--------------------------------------------------------------------------------
/test/com/walmartlabs/lacinia/documentation_test.clj:
--------------------------------------------------------------------------------
1 | ; Copyright (c) 2017-present Walmart, Inc.
2 | ;
3 | ; Licensed under the Apache License, Version 2.0 (the "License")
4 | ; you may not use this file except in compliance with the License.
5 | ; You may obtain a copy of the License at
6 | ;
7 | ; http://www.apache.org/licenses/LICENSE-2.0
8 | ;
9 | ; Unless required by applicable law or agreed to in writing, software
10 | ; distributed under the License is distributed on an "AS IS" BASIS,
11 | ; WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | ; See the License for the specific language governing permissions and
13 | ; limitations under the License.
14 |
15 | (ns com.walmartlabs.lacinia.documentation-test
16 | "Tests that documentation for fields and field args can be inhertited from interfaces."
17 | (:require
18 | [clojure.test :refer [deftest is]]
19 | [com.walmartlabs.test-utils :as utils]))
20 |
21 | (def ^:private schema
22 | (utils/compile-schema "doc-inheritance-schema.edn" {}))
23 |
24 | (defn ^:private q [query]
25 | (utils/execute schema query))
26 |
27 | (deftest field-description-may-inherit-from-interface
28 | (is (= {:data
29 | {:ex1
30 | {:fields
31 | [{:description "ex1/alpha"
32 | :name "alpha"}
33 | {:description "sierra/bravo"
34 | :name "bravo"}]}
35 | :ex2
36 | {:fields
37 | [{:description "ex2/alpha"
38 | :name "alpha"}
39 | {:description "ex2/bravo"
40 | :name "bravo"}]}}}
41 | (q "
42 | { ex1: __type (name: \"ex1\") {
43 |
44 | fields {
45 | name
46 | description
47 | }
48 | }
49 |
50 | ex2: __type(name: \"ex2\") {
51 | fields {
52 | name
53 | description
54 | }
55 | }
56 | }"))))
57 |
58 | (deftest arg-description-may-inherit-from-interface
59 | (is (= {:data
60 | {:ex3
61 | {:fields
62 | [{:args
63 | [{:description "ex3/delta"
64 | :name "delta"}
65 | {:description "tango/charlie/echo"
66 | :name "echo"}]}]}}}
67 | (q "
68 | { ex3: __type(name: \"ex3\") {
69 | fields {
70 | args {
71 | name
72 | description
73 | }
74 | }
75 | }
76 | }"))))
77 |
--------------------------------------------------------------------------------
/test/com/walmartlabs/lacinia/expound_tests.clj:
--------------------------------------------------------------------------------
1 | ; Copyright (c) 2017-present Walmart, Inc.
2 | ;
3 | ; Licensed under the Apache License, Version 2.0 (the "License")
4 | ; you may not use this file except in compliance with the License.
5 | ; You may obtain a copy of the License at
6 | ;
7 | ; http://www.apache.org/licenses/LICENSE-2.0
8 | ;
9 | ; Unless required by applicable law or agreed to in writing, software
10 | ; distributed under the License is distributed on an "AS IS" BASIS,
11 | ; WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | ; See the License for the specific language governing permissions and
13 | ; limitations under the License.
14 |
15 | (ns com.walmartlabs.lacinia.expound-tests
16 | "Tests that useful Expound messages are emitted for spec failures."
17 | (:require
18 | [clojure.test :refer [deftest is use-fixtures]]
19 | [com.walmartlabs.test-reporting :refer [reporting]]
20 | com.walmartlabs.lacinia.expound
21 | [com.walmartlabs.lacinia.schema :as schema]
22 | [com.walmartlabs.lacinia.parser.schema :as ps]
23 | [clojure.spec.alpha :as s]
24 | [clojure.string :as str]))
25 |
26 | (defmacro expect
27 | [spec value & substrings]
28 | `(let [explain# (with-out-str
29 | (s/explain ~spec ~value))]
30 | (reporting {:explain explain#}
31 | (doseq [s# ~(vec substrings)]
32 | (is (str/includes? explain# s#))))))
33 |
34 | (deftest uses-messages
35 | (expect ::schema/resolve nil
36 | "fn?"
37 | "implement the com.walmartlabs.lacinia.resolve/FieldResolver protocol"))
38 |
39 | (deftest correctly-reports-incorrect-type-modifier
40 | (expect ::schema/field {:type '(something :String)}
41 | "type wrappers should be either (list type) or (non-null type)"))
42 |
43 | (deftest can-report-enum-value
44 | (expect ::schema/enum-value 123 "string?" "simple-symbol?" "simple-keyword?")
45 | (expect ::schema/enum-value "this-and-that" "must be a valid GraphQL identifier"))
46 |
47 | (deftest sdl-function-map
48 | (expect ::ps/fn-map {:foo :bar :gnip {:q 'gnop}}
49 | "fn?" "simple-keyword?"))
50 |
51 | ;; Really want a good message for this incompatible change in 0.31.0
52 |
53 | (deftest parse-and-serialize-must-be-bare-functions
54 | (expect ::schema/parse (schema/as-conformer identity)
55 | "scalar parse and serialize functions must now be simple functions"
56 | "not clojure.spec conformers"
57 | "see release notes"))
58 |
--------------------------------------------------------------------------------
/test/com/walmartlabs/lacinia/wrapped_values_in_lists_tests.clj:
--------------------------------------------------------------------------------
1 | ; Copyright (c) 2017-present Walmart, Inc.
2 | ;
3 | ; Licensed under the Apache License, Version 2.0 (the "License")
4 | ; you may not use this file except in compliance with the License.
5 | ; You may obtain a copy of the License at
6 | ;
7 | ; http://www.apache.org/licenses/LICENSE-2.0
8 | ;
9 | ; Unless required by applicable law or agreed to in writing, software
10 | ; distributed under the License is distributed on an "AS IS" BASIS,
11 | ; WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | ; See the License for the specific language governing permissions and
13 | ; limitations under the License.
14 |
15 | (ns com.walmartlabs.lacinia.wrapped-values-in-lists-tests
16 | "Tests that wrapped values inside lists (returned by field resolvers) are properly handled."
17 | (:require
18 | [clojure.test :refer [deftest is]]
19 | [com.walmartlabs.lacinia.resolve :refer [with-context]]
20 | [com.walmartlabs.lacinia.schema :as schema]
21 | [com.walmartlabs.test-utils :as utils]))
22 |
23 |
24 |
25 | (def ^:private compiled-schema
26 | (schema/compile
27 | {:objects
28 | {:IdObj
29 | {:fields
30 | {:id {:type 'String}
31 | :xid {:type 'String
32 | :resolve (fn [context _ obj]
33 | (str (::root-prefix context) "-"
34 | (::item-prefix context) "-"
35 | (:id obj)))}}}}
36 |
37 | :queries
38 | {:objects
39 | {:type '(list :IdObj)
40 | :args {:root_prefix {:type 'String}}
41 | :resolve (fn [_ args _]
42 | (with-context
43 | (->> [100 200 555]
44 | (map #(hash-map :id (format "%05d" %)))
45 | (map-indexed (fn [i v]
46 | (with-context v
47 | {::item-prefix (str "i" i)}))))
48 | {::root-prefix (:root_prefix args)}))}}}))
49 |
50 |
51 | (deftest exposes-context-to-sub-resolvers
52 | (is (= {:data {:objects [{:id "00100"
53 | :xid "r-i0-00100"}
54 | {:id "00200"
55 | :xid "r-i1-00200"}
56 | {:id "00555"
57 | :xid "r-i2-00555"}]}}
58 | (utils/execute compiled-schema
59 | "{ objects(root_prefix: \"r\") { id xid }}"))))
60 |
--------------------------------------------------------------------------------
/src/com/walmartlabs/lacinia/validation/no_unused_fragments.clj:
--------------------------------------------------------------------------------
1 | ; Copyright (c) 2017-present Walmart, Inc.
2 | ;
3 | ; Licensed under the Apache License, Version 2.0 (the "License")
4 | ; you may not use this file except in compliance with the License.
5 | ; You may obtain a copy of the License at
6 | ;
7 | ; http://www.apache.org/licenses/LICENSE-2.0
8 | ;
9 | ; Unless required by applicable law or agreed to in writing, software
10 | ; distributed under the License is distributed on an "AS IS" BASIS,
11 | ; WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | ; See the License for the specific language governing permissions and
13 | ; limitations under the License.
14 |
15 | (ns com.walmartlabs.lacinia.validation.no-unused-fragments
16 | {:no-doc true}
17 | (:require
18 | [clojure.set :as set]
19 | [com.walmartlabs.lacinia.internal-utils :refer [q cond-let]])
20 | (:import (clojure.lang PersistentQueue)))
21 |
22 | (defn ^:private all-fragments-used
23 | [fragments root-selections]
24 | (loop [result (transient #{})
25 | queue (-> (PersistentQueue/EMPTY)
26 | (into root-selections)
27 | (into (vals fragments)))]
28 | (cond-let
29 |
30 | :let [selection (peek queue)]
31 |
32 | (nil? selection)
33 | (persistent! result)
34 |
35 | :let [{:keys [fragment-name]} selection
36 | queue' (pop queue)]
37 |
38 | ;; Named fragments do not, themselves, have sub-selections
39 | fragment-name
40 | (recur (conj! result fragment-name) queue')
41 |
42 | :let [sub-selections (:selections selection)]
43 |
44 | (seq sub-selections)
45 | (recur result (into queue' sub-selections))
46 |
47 | :else
48 | (recur result queue'))))
49 |
50 | (defn no-unused-fragments
51 | "Validates if all fragment definitions are spread
52 | within operations, or spread within other fragments
53 | spread within operations."
54 | [prepared-query]
55 | (let [{:keys [fragments selections]} prepared-query
56 | f-locations (into {} (map (fn [[f-name {location :location}]]
57 | {f-name location})
58 | fragments))
59 | f-definitions (set (keys fragments))
60 | f-names-used (all-fragments-used fragments selections)]
61 | (for [unused-f-definition (set/difference f-definitions f-names-used)]
62 | {:message (format "Fragment %s is never used."
63 | (q unused-f-definition))
64 | :locations [(unused-f-definition f-locations)]})))
65 |
--------------------------------------------------------------------------------
/test/com/walmartlabs/lacinia/extensions_test.clj:
--------------------------------------------------------------------------------
1 | ; Copyright (c) 2018-present Walmart, Inc.
2 | ;
3 | ; Licensed under the Apache License, Version 2.0 (the "License")
4 | ; you may not use this file except in compliance with the License.
5 | ; You may obtain a copy of the License at
6 | ;
7 | ; http://www.apache.org/licenses/LICENSE-2.0
8 | ;
9 | ; Unless required by applicable law or agreed to in writing, software
10 | ; distributed under the License is distributed on an "AS IS" BASIS,
11 | ; WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | ; See the License for the specific language governing permissions and
13 | ; limitations under the License.
14 |
15 | (ns com.walmartlabs.lacinia.extensions-test
16 | (:require
17 | [clojure.test :refer [deftest is]]
18 | [com.walmartlabs.lacinia.resolve :as r]
19 | [com.walmartlabs.test-utils :refer [compile-schema execute]]))
20 |
21 | (def ^:private compiled-schema
22 | (compile-schema "extensions-schema.edn"
23 | {:queries/extension (fn [_ _ _]
24 | (r/with-extensions "OK"
25 | assoc-in [:foo :bar] :baz))
26 | :queries/warning (fn [_ _ _]
27 | (r/with-warning "WARN"
28 | {:message "Warning!"
29 | :foo :bar}))}))
30 |
31 | (deftest with-extensions
32 | (is (= {:data {:extension "OK"}
33 | :extensions {:foo {:bar :baz}}}
34 | (execute compiled-schema "{ extension }"))))
35 |
36 | (deftest with-warning
37 | (is (= {:data {:warning "WARN"}
38 | :extensions {:warnings [{:extensions {:foo :bar}
39 | :locations [{:column 3
40 | :line 1}]
41 | :message "Warning!"
42 | :path [:warning]}]}}
43 | (execute compiled-schema "{ warning }"))))
44 |
45 | (deftest combined
46 | (is (= {:data {:extension "OK"
47 | :warning "WARN"}
48 | :extensions {:foo {:bar :baz}
49 | :warnings [{:extensions {:foo :bar}
50 | :locations [{:column 3
51 | :line 1}]
52 | :message "Warning!"
53 | :path [:warning]}]}}
54 | (execute compiled-schema "{ warning extension }"))))
55 |
--------------------------------------------------------------------------------
/dev-resources/org/example/db.clj:
--------------------------------------------------------------------------------
1 | (ns org.example.db
2 | (:require
3 | [com.walmartlabs.lacinia.schema :as schema]))
4 |
5 | (defn ^:private tagged
6 | [x]
7 | (schema/tag-with-type x (::type x)))
8 |
9 | (def ^:private humans-data
10 | (map #(assoc % ::type :human)
11 | [{:id "1000"
12 | :name "Luke Skywalker"
13 | :friends ["1002", "1003", "2000", "2001"]
14 | :appears_in ["NEWHOPE" "EMPIRE" "JEDI"]
15 | :home_planet "Tatooine"
16 | :force_side "3001"}
17 | {:id "1001"
18 | :name "Darth Vader"
19 | :friends ["1004"]
20 | :appears_in ["NEWHOPE" "EMPIRE" "JEDI"]
21 | :home_planet "Tatooine"
22 | :force_side "3000"}
23 | {:id "1003"
24 | :name "Leia Organa"
25 | :friends ["1000", "1002", "2000", "2001"]
26 | :appears_in ["NEWHOPE" "EMPIRE" "JEDI"]
27 | :home_planet "Alderaan"
28 | :force_side "3001"}
29 | {:id "1002"
30 | :name "Han Solo"
31 | :friends ["1000", "1003", "2001"]
32 | :appears_in ["NEWHOPE" "EMPIRE" "JEDI"]
33 | :force_side "3001"}
34 | {:id "1004"
35 | :name "Wilhuff Tarkin"
36 | :friends ["1001"]
37 | :appears_in ["NEWHOPE"]
38 | :force_side "3000"}]))
39 |
40 | (def ^:private droids-data
41 | (map #(assoc % ::type :droid)
42 | [{:id "2001"
43 | :name "R2-D2"
44 | :friends ["1000", "1002", "1003"]
45 | :appears_in ["NEWHOPE" "EMPIRE" "JEDI"]
46 | :primary-function "ASTROMECH"}
47 | {:id "2000"
48 | :name "C-3PO"
49 | :friends ["1000", "1002", "1003", "2001"]
50 | :appears_in ["NEWHOPE" "EMPIRE" "JEDI"]
51 | :primary-function "PROTOCOL"}]))
52 |
53 | (def ^:private character-data (concat humans-data droids-data))
54 |
55 |
56 | (def ^:private hero-data
57 | {:NEWHOPE "2001"
58 | :EMPIRE "1002"
59 | :JEDI "1000"})
60 |
61 | (defn ^:private first-match [data key value]
62 | (-> (filter #(= (get % key) value) data)
63 | first
64 | tagged))
65 |
66 | (defn resolve-hero
67 | [ctx args value]
68 | (let [episode (:episode args :NEWHOPE)
69 | hero-id (get hero-data episode)]
70 | (first-match character-data :id hero-id)))
71 |
72 | (defn resolve-droid
73 | [ctx args value]
74 | (first-match droids-data :id (:id args)))
75 |
76 | (defn resolve-friends
77 | [ctx args value]
78 | (map #(first-match character-data :id %) (:friends value)))
79 |
80 | (defn resolve-human
81 | [ctx args value]
82 | (first-match humans-data :id (:id args)))
83 |
--------------------------------------------------------------------------------
/src/com/walmartlabs/lacinia/validation/scalar_leafs.clj:
--------------------------------------------------------------------------------
1 | ; Copyright (c) 2017-present Walmart, Inc.
2 | ;
3 | ; Licensed under the Apache License, Version 2.0 (the "License")
4 | ; you may not use this file except in compliance with the License.
5 | ; You may obtain a copy of the License at
6 | ;
7 | ; http://www.apache.org/licenses/LICENSE-2.0
8 | ;
9 | ; Unless required by applicable law or agreed to in writing, software
10 | ; distributed under the License is distributed on an "AS IS" BASIS,
11 | ; WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | ; See the License for the specific language governing permissions and
13 | ; limitations under the License.
14 |
15 | (ns com.walmartlabs.lacinia.validation.scalar-leafs
16 | {:no-doc true}
17 | (:require
18 | [com.walmartlabs.lacinia.internal-utils :refer [q cond-let]]
19 | [com.walmartlabs.lacinia.selection :as selection]))
20 |
21 | (defn ^:private validate-selection
22 | "Recursively checks if all specified fields are scalar or enum types.
23 | Non-scalar fields should contain either nested :fields
24 | or :fragments.
25 | Fragments are not validated again, only their presence is checked.
26 | Returns empty sequence if all fields are valid, otherwise returns
27 | a sequence of error maps, e.g.
28 | `[{:message \"Field `friends' (of type `character')must have at least one selection.\"
29 | :locations [{:line 1 :column 7}]}]`"
30 | [selection]
31 | (cond-let
32 | ;; Fragment spreads do not ever have sub-selections, and are validated
33 | ;; elsewhere.
34 | (= :named-fragment (selection/selection-kind selection))
35 | nil
36 |
37 | (:leaf? selection)
38 | nil
39 |
40 | :let [sub-selections (:selections selection)]
41 |
42 | (seq sub-selections)
43 | (mapcat validate-selection sub-selections)
44 |
45 | :else
46 | [{:message (format "Field %s must have at least one selection."
47 | (-> selection :field-definition :qualified-name q))
48 | :locations [(:location selection)]}]))
49 |
50 | (defn ^:private validate-fragment
51 | "Validates fragment once to avoid validating it separately for
52 | each selection."
53 | [[_ fragment]]
54 | (validate-selection fragment))
55 |
56 | (defn scalar-leafs
57 | "A GraphQL query is valid only if all leaf nodes (fields without
58 | sub selections) are of scalar or enum types."
59 | [prepared-query]
60 | (let [selections (:selections prepared-query)
61 | fragments (:fragments prepared-query)]
62 | (concat
63 | (mapcat validate-fragment fragments)
64 | (mapcat validate-selection selections))))
65 |
--------------------------------------------------------------------------------
/docs/federation/implementation.rst:
--------------------------------------------------------------------------------
1 | Service Implementation
2 | ======================
3 |
4 | At this point, we've discussed what goes into each implementing service's schema, and a bit about how
5 | each service is responsible for resolving representations; let's finally see how this all fits together with
6 | Lacinia.
7 |
8 | Below is a sketch of how this comes together in the products service:
9 |
10 | .. literalinclude:: /_examples/fed/products.edn
11 |
12 | The ``resolve-users-external`` function is used to convert a seq of ``User`` representations
13 | into a seq of ``User`` entity stubs; this is called from the resolver for the ``_entities`` query whose type
14 | is a list of the ``_Entities`` union, therefore each value :doc:`must be tagged ` with the ``:User`` type.
15 |
16 | .. sidebar:: Return type
17 |
18 | Return type here is just like a normal field resolver that returns a GraphQL list; it may be seq of values, or a
19 | :doc:`ResolverResult ` that delivers such a seq.
20 |
21 | ``resolve-products-internal`` does the same for ``Product`` representations, but since this is the
22 | products service, the expected behavior is to perform a query against an external data store and
23 | ensure the results match the structure of the ``Product`` entity.
24 |
25 | ``resolve-product-by-upc`` is the resolver function for the ``productByUpc`` query.
26 | Since the field type is ``Product`` there's no need to tag the value.
27 |
28 | ``resolve-favorite-products`` is the resolver function for the ``User/favoriteProducts`` field.
29 | This is passed the ``User`` (provided by ``resolve-users-external``); it extracts the ``id`` and passes
30 | it to ``get-favorite-products-for-user``.
31 |
32 | The remainder is bare-bones scaffolding to read, parse, and compile the schema and build a Pedestal
33 | service endpoint around it.
34 |
35 | Pay careful attention to the call to :api:`parser-schema/parse-schema`; the presence of the
36 | ``:federation`` option is critical; this adds the necessary base types and directives
37 | before parsing the schema definition, and then adds the ``_entities`` query and ``_Entities`` union
38 | afterwards, among other things.
39 |
40 | The ``:entity-resolvers`` map is critical; this maps from a type name to a entity resolver;
41 | this information is used to build the field resolver function for the ``_entities`` query.
42 |
43 | .. warning::
44 |
45 | A lot of details are left out of this, such as initializing the database and storing
46 | the database connection into the application context, where functions like
47 | ``get-product-by-upc`` can access it.
48 |
49 | This is only a sketch to help you connect the dots.
--------------------------------------------------------------------------------