Code Monkey home page Code Monkey logo

claro's Introduction

claro

Documentation | Guides

claro is a library that allows you to streamline your data access, providing powerful optimisations and abstractions along the way.

Build Status Clojars Artifact codecov

It is inspired by muse and heavily influenced by GraphQL.

claro requires Clojure ≥ 1.7.0.

Features

claro is designed to be flexible and extensible. It'll make your data access more elegant, efficient and testable, providing things like:

  • batched resolution of similar entities,
  • automatic caching of already known resolution results,
  • engine middlewares to hook into the resolution logic, allowing e.g. generic cache or circuit breaker implementations,
  • pluggable resolution strategies in the form of Resolvable selectors,
  • an exchangeable deferred implementation, defaulting to manifold,
  • and pre-built middlewares for data source mocking, introspection and result transformation.

Quickstart

Data access is defined within records implementing the Resolvable interface. Note that they can always produce more resolvables:

(require '[claro.data :as data]
         '[manifold.deferred :as d])

(defrecord Person [id]
  data/Resolvable
  (resolve! [_ env]
    (d/future
      (fetch-person! (:db env) id)))
  data/Transform
  (transform [_ {:keys [friend-ids] :as person}]
    (assoc person :friends (map #(Person. %) friend-ids))))

Blindly resolving an infinite tree like this is usually not a good idea – but claro offers tree projections you can use to describe abstract transformations of infinite structures:

(require '[claro.projection :as projection])

(def person-with-friend-names
  {:id      projection/leaf
   :name    projection/leaf
   :friends [(projection/extract :name)]})

And here we go:

(require '[claro.engine :as engine])

(engine/run!!
  (-> (->Person 1)
      (projection/apply person-with-friend-names)))
;; => {:id 1, :name "Sherlock Holmes", :friends ["Dr. Watson", "Ms. Hudson"]}

Related Projects

Library Description
alumbra GraphQL implementation on top of claro
claro.circuit-breaker Circuit-breaker middleware based on resilience4j

Feel free to open a pull request to add your project to this table.

Documentation

  1. Basic Resolution
  2. Projections
  3. Advanced Projections
  4. Engine
  5. Testing & Debugging
  6. Implementation Notes

All these topics are also available in claro's auto-generated documentation.

Contributing

Contributions are always welcome. Please take a look at the Contribution Guidelines for a quick overview of how your changes can best make it to master.

License

MIT License

Copyright (c) 2015-2017 Yannick Scherer

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

claro's People

Contributors

christoph-frick avatar xsc avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

claro's Issues

Deferred/Streaming Resolution

Claro should allow functionality akin to GraphQL's (proposed?) defer, stream and live directives [1], i.e. return incomplete results that get completed asynchronously. Ideally, this is offered by engine middlewares but there are some things to consider.

Access to the full Engine

To let a middleware run resolution, it needs access to the full engine, i.e. one also including all middlewares on top of the current one. This could be achieved by exposing a dynamic binding or lookup function (e.g. claro.engine/current) to the resolver or by (conditionally) injecting the engine into the environment using a well-known key (e.g. :claro/engine).

Dedicated Value Types + Projection

Resolvable parts have to be "marked" as deferred or to-stream, e.g. by wrapping them in dedicated defer/stream records. This can also be elegantly done using projections:

{:id      projection/leaf
 :name    projection/leaf
 :friends (projection/defer [{:name projection/leaf}])}

For stream resolvables it's probably necessary for them to implement explicit streaming functionality.

Push Mechanism

A callback mechanism has to be used to deliver the results of asynchronous resolution. Possible parameters for such a function could be:

  • a unique ID describing the location of the value to deliver,
  • the result value,
  • the environment (which might contain handles on some kind of push transport).

Race Conditions

Deferred values can be nested, so one has to be careful to only push nested results once the upper level has been finalised.

Batching

Deferred values of the same class should be resolved in batches if possible (i.e. if they implement BatchedResolvable), or individually if not.


Keeping these points in mind, deferred resolution is most likely a multi-stage process:

  1. Run normal resolution, wrap values in defer/stream records. Return the result immediately.
  2. Collect all deferred values, probably tuples of (deferredID, resolvable).
  3. If there are any, resolve the deferred tuples asynchronously, processing results using a callback mechanism. Here, similar resolvables can be batched.
  4. Continue with step 2 for each result.

This requires wrapping the full engine, though, not only the resolver part. The result has to be inspected and further actions have to be initiated.

[1] https://medium.com/apollo-stack/new-features-in-graphql-batch-defer-stream-live-and-subscribe-7585d0c28b07

[ClojureScript] Deferred Implementation

What implementation of deferred values shall be used in ClojureScript?

Possible Candidates:

Common, implementation-independent parts of claro.engine.runtime should be extracted into claro.runtime with claro.engine instantiating Clojure/ClojureScript engines using reader conditionals.

Union Projection on Mutation

The union projection, internally, creates a seq of the partial projections it wants to eventually merge. This means, that the initial value will appear multiple times within the tree, causing the Mutation constraint of "there can only be one per resolution" to fail.

This could be fixed by allowing multiple identical mutations to appear within the tree (which makes the aforementioned constraint kinda useless) or by implementing a special union projection tree node.

(The latter might also address some performance concerns I'm having about the union projection.)

'case' vs. 'case-resolvable'

case does class-based dispatch before resolution. A better name might thus be case-resolvable, freeing case for class-based dispatch after resolution. (Which, incidentally, is the behaviour of conditional fragments in GraphQL.)

Dedicated Error Value

While it's already possible to use e.g. Manifold's d/catch to react to errors it might make sense to introduce an error value that can be produced by Resolvables.

(defrecord ProfilePictures [id]
  data/Resolvable
  (resolve! [_ {{:keys [user-id]} :auth}]
    (d/future
      (if (not= user-id id)
        (data/error "cannot access profile pictures of other users.")
        ...)))) 

This way, we could return a partial tree with error leaves (and let the client handle them) or have a postprocessing step that collects all errors and exposes them at the top-level or within the affected subtrees.

(We might need to make projections error-aware, though, so they don't complain about error/leaf values when expecting a nested one.)

Problem with Projection + Composition in Collection

Applying a projection to a seq whose elements are wrapped by one of claro's composition functions does not have the desired result, e.g.:

(claro.engine/run!!
  (claro.projection/apply
    [(claro.data.ops/then
       (reify claro.data/Resolvable
         (resolve! [_ _]
           {:a 1}))
       identity)]
    [{:a claro.projection/leaf}]))

Instead of [{:a 1}], this produces:

java.lang.IllegalArgumentException: projection template is a map but value is not.
template: {:a <leaf>}
value:    :claro.data.tree.utils/this-should-not-happen

Caching Middleware

Hi Yannick,

I was having a quick look through Claro, and wondered if you have plans to open up the caching strategy (currently you're using an in-memory, transient hash-map I believe).

If I have a couple of JVMs fetching data it would be useful if they could both share the work they're doing via something like Redis or Memcached. Obviously, Clojurescript users in a browser environment won't be hitting up Redis, but in a backend service this could be quite useful.

Maybe a protocol so I can implement my own external caching strategy on top of Redis?

Thanks for open sourcing Claro!

P.S. I guess with Onyx mentioning you in their docs you may get a few questions like mine.

Generalise Batch Count Limit as Cost Limit

Instead of limiting the number of batches that can be resolved during one run we could introduce a per-resolvable-batch cost function and base a limit on it. This addresses the problem that batch count (or tree depth) has only limited usefulness due to every batch being treated equally, no matter the batch size, transformation complexity and expected latency.

For example, resolvables that just wrap other resolvables without side-effects do not contribute in any significant way to overall resolution time, so they could be treated as zero-cost operations.

I thus think we should introduce a Cost protocol, by default returning 1 for BatchedResolvable batches and the respective batch size for non-batched resolvables. This, by default, limits the number of I/O operations. Additionally, I'd add a PureResolvable marker protocol that gets assigned a cost value of 0.

This allows users to protect against too complex queries in a more fine-grained way. It might also be possible to use this information for static analysis purposes, assuming an additional schema layer on top of claro.

Generic Engine Wrappers

The proposals in #3 and #4 both hint at postprocessing steps for engine. While an engine does implement IFn it will no longer implement the Engine protocol if a function wrapper is put around it.

So, we might want to create wrap-pre-processor and wrap-post-processor functions that produce valid engines again.

Partial Resolvable Results

Sometimes, we already know part of the resolution result without doing any work, e.g. IDs or some child resolvables:

(defrecord Person [id]
  data/Resolvable
  ...
  data/Transform
  (transform [_ result]
    (assoc result :friends (->Friends id))))

Since :friends only depends on the already knownid we don't really need to do any Person I/O if we want to apply the following projection:

{:friends [{:name projection/leaf}]}

We could introduce a PartialResult protocol, allowing to expose such data:

(defrecord Person [id]
  data/PartialResult
  (partial-result [_]
    {:id id, :friends (->Friends id)})

  data/Resolvable
  ...

  data/Transform
  (transform [this result]
    (merge result (data/partial-result this)))

While it's possible to do some automatic merging of partial-result into the return value of transform I fear that this might create some surprises if done implicitly. Although, if we limit PartialResult to map values (which might be a reasonable thing to do) we can derive some very simple semantics à la: "If transform return a non-nil value, merge the partial result into it."

Another point of note is the fact that, if a Person does not exist, just using the partial result will not expose that fact (although the friend list will be empty). But I guess that if users are made aware of this caveat it shouldn't be a significant problem.

Specifying unlimited cost of resolution

More of a question than an issue really but I would like to be able to specify that resolution should never fail due to the cost of a batch. It seems that :max-cost does not support any way to specify this (although maybe one of the Java infinities would work?)

Also, it's possible that wanting to do this is wrong in the first place. I would be interested to know more about how the cost concept is supposed to be used. In my case, the number of Resolvables is related to database rows in a xn fashion (say x = 3 resolvables produced per database row, and n database rows), but since the n is variable I could never fix a static :max-cost without counting the rows first.

Circuitbreaker Middleware

An engine middleware could use e.g. resilience4j to add circuit breaking to claro.

This might require exposing which resolvables use which datasource to not only have circuit-breaking on a per-resolvable basis (which could be the default, though).

As for all middlewares that require extra dependencies, I'd prefer a separate repository over integrating it into this one.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.