What is Lacinia?
mastergraphql-js implementation. It can be integrated into any Clojure HTTP pipeline.repository·master·Indexed 23 days ago
https://github.com/walmartlabs/laciniaA backend-agnostic GraphQL query execution engine implemented in Clojure. Lacinia provides a full implementation of the Facebook GraphQL specification, allowing developers to define schemas using EDN or SDL, implement field resolvers, and execute queries against data. It supports custom scalars with parse and serialize functions, standard GraphQL directives like @skip and @include, and schema deprecation metadata.
graphql-js implementation. It can be integrated into any Clojure HTTP pipeline.Lacinia has been used in various real-world implementations and tutorials. You can study these projects to understand how to integrate Lacinia with different backends, databases, and frontend frameworks:
boardgamegeek-graphql-proxy: A proxy exposing BoardGameGeek data as GraphQL.Lacinia LDAP backend: A library for querying LDAP/Active Directory via GraphQL.Lacinia Qliksense backend: A library for querying Qliksense servers via GraphQL.leaderboard-api: A game/high-score API built with Compojure and PostgreSQL.Hacker News GraphQL: A Hacker News implementation using Datomic on the backend and re-frame on the frontend.Event sourcing tutorial: A bank simulation using Kafka for queries, mutations, and subscriptions, with a re-graph frontend.Fullstack Learning Project: A Clojure/Lacinia port of 'The Fullstack Tutorial for GraphQL'.Lacinia provides two ways to attach resolvers to a schema map:
util/inject-resolvers: The preferred and standard approach. It is a concise way to match fields to resolvers by providing a map of namespaces and local names.util/attach-resolvers: An older approach that is still supported but is considered more cumbersome than inject-resolvers.Use util/inject-resolvers for all new development.
In this architecture, the database is encapsulated as a component that manages an in-memory immutable map stored inside a Clojure Atom. This component is decoupled from the schema and server, following a dependency chain: :server -> :schema-provider -> :db.
The :db component is defined as a record with a constructor function. Its lifecycle is managed via a start method, which initializes the :data Atom. This abstraction allows the underlying storage to be swapped from an in-memory Atom to an external database in the future without changing the function signatures used by the rest of the application.
While field resolvers are typically simple functions accepting context, args, and value, large-scale systems can use the FieldResolver protocol to structure resolvers as components.
To implement this protocol, define a class or component that provides a single method: resolve-value. This method acts as the analog to a standard field resolver function.
Supported return types for resolve-value include:
ResolverResult object.Lacinia distinguishes between errors explicitly returned by your code and errors caused by the GraphQL engine during parsing or argument application:
resolve-as. The result map will contain both :data (often nil) and :errors.String where an Int is expected). In these cases, the result map will contain only the :errors key, and the :data key will be missing entirely.In a federated architecture, a service can extend an entity that is owned by another service. To do this, use the @extends directive on the entity definition. This indicates that the entity in the current service is a 'stub' for the full entity residing in the source service.
When extending an entity, you must satisfy these requirements:
@key directive(s) and primary key fields used by the original service. For example, if the original service uses id as the primary key, your stub must also include id.@external directive on fields that are owned by the original service to indicate they are provided by another service.favoriteProducts on a User entity). These new fields require their own resolvers within the extending service.When working with the source stream callback, keep the following timing constraints in mind:
nil immediately. It should not block.resolve/with-error) or, in some historical cases, a ResolverResult. If using ResolverResult, you must extract the resolved value before passing it to execute-query.A Streamer is a component responsible for initiating and managing a source stream of values for a subscription. It is defined in a subscription schema using the :stream key.
Streamers operate in parallel with field resolvers. While field resolvers handle individual field data, the streamer manages the continuous flow of data. To use streamers, you must use the util/inject-streamers function to replace schema keywords with actual implementation functions.
Streamer Function Signature A streamer function receives three arguments:
Lifecycle and Cleanup
nil to the callback).nil to the source stream callback signals the end of the stream and triggers the cleanup callback.extensions key of the GraphQL response. This follows the standard GraphQL specification for providing extra metadata in a response.Objects can implement zero or more interfaces using the :implements key, which takes a list of keywords.
Constraints:
:Character, but an implementing object :Human can define that same field as a list of :Humans).Lacinia handles operations through two primary mechanisms:
Query, Mutation, or Subscription (or your custom names via :roots), any fields attached to them are automatically treated as available operations.:queries, :mutations, and :subscriptions maps in the input schema. These are merged into the corresponding root object.Important Constraints:
:queries, :mutations, and :subscriptions maps is supported, it is not the preferred method compared to defining fields directly on the root objects.:queries) conflicts with an existing field on the corresponding root object, a schema compile exception is thrown.