Giter Club home page Giter Club logo

kraken's Introduction

kraken

CircleCI codecov npm package

The problem is always the same. You create an application and need to connect it to your API. That means you need to think about caches, cache invalidation, request lifecycles and more. The kraken helps you to focus on what is really important: everything else but that. This library takes your types (e.g. users) and creates everything that is needed to manage the data. All you need to care about is from where and when to fetch.

kraken is built on top of Redux and redux-saga and connects to your React components.

Installation

Install the @signavio/kraken module and its peer depedencies:

# using npm
npm install --save @signavio/kraken react redux react-redux redux-saga normalizr

# using yarn
yarn add @signavio/kraken react redux react-redux redux-saga normalizr

Usage

@signavio/kraken provides you with the building blocks to connect React components to your backend API. Instead of using the kraken directly from your application, you are supposed to create wrapper module that defines the API types for application based upon the functionality this library provides. The main export of the kraken is a creator function that takes your custom type definitions as an argument and returns a ready-to-use module for connecting to your API.

API Types

The core idea of the library is that you can split you API into different types. Each type must export a certain set of attributes. These contain descriptions about the structure of each type and also methods to read, create or modify an instance of that type.

Attribute Required Description
schema true A normalizr schema that is used to store and access your data.
collection true Identifier where all retrieved instances for that type will be stored.
(callApi, apiBase) => fetch(query, body, requestParams) false A method that takes a custom set of properties and maps it to a call of callAPI to retrieve data from your backend.
(callApi, apiBase) => create(query, body, requestParams) false Function that describes how an instance is created.
(callApi, apiBase) => update(query, body, requestParams) false Function that describes how an instance is updaetd.
(callApi, apiBase) => remove(query, body, requestParams) false Function that describes how an instance is removed.
cachePolicy false The cache policy that should be used for that type. See cache policies for the possible options.

callApi(fullUrl, schema[, options])

For every method you implement (i.e. fetch, create, update, remove) the callAPI method will be injected and must be called in the end. It requires the two parameters fullUrl and schema and accepts extra options through the third parameter options.

fullUrl

The URL to which the request is sent.

schema

The normalizr for the concerned API type.

options

A set of extra parameters that are also understood by fetch (the browser fetch method).

Example

import { schema as schemas } from 'normalizr'

export const collection = 'users'
export const schema = new schemas.Entity(collection)

export const fetch = callApi => ({ id }) => callApi(`users/${id}`, schema)
export const create = callApi => (_, body) =>
  callApi('users', schema, { method: 'POST', body })
export const update = callApi => ({ id }, body) =>
  callApi(`users/${id}`, schema, { method: 'PUT', body })
export const remove = callApi => ({ id }) =>
  callApi(`users/${id}`, schema, { method: 'DELETE' })

In the example above we created a user type. It defines its collection as users since it makes sense to store all fetched users together in a large user database. Based on the collection we can create an Entity schema that describes a user. As we only want to be able to fetch users for now, we only need to define the fetch method. fetch will then map the id of a user to an API call at /users/{id}.

Exporting the pre-configured API module

After you have declared your types you can use the creator function, the default export of the kraken, to initialize your pre-configured API module. The rest of your application should never have use the kraken directly, but only ever interact with the pre-configured API module. To create your custom API module using your types, you will need some glue code similar to the following snippet:

import { mapValues } from 'lodash'

import creator, { promise, actionTypes } from '@signavio/kraken'

// These are the types you have declared
import apiTypes from './types'

const { connect, saga, reducer } = creator(apiTypes)

// create a set of keys for all the types you have declared
const types = mapValues(apiTypes, (definition, key) => key)

export { connect, types }

Whenever you use this library to access a resource using you API, you will need to provide the identifying key of the respective type you want to access. As a safeguard against nasty errors caused by typos in type references it is advisable to export the type keys as string constants that can be imported from any component module you want to connect to the API.

Integrating the API module in your application

The kraken uses redux and redux-saga under the hood to manage the cache state. To integrate it in an existing redux application you basically have to follow two steps:

Apply saga middleware

Apply redux-saga's sagaMiddleware to your redux store and use it to run the saga returned by the creator function.

Hook in cache reducer

The kraken expects to find its state under the key kraken in the redux store. Use combineReducers to hook the reducer returned by the creator function under the kraken key in your root reducer.

If your application is not using redux yet, you can hide this integration complexity inside the pre-configured API module by additionally exporting an ApiProvider component which provides the minimal setup:

import React from 'react'
import { createStore, combineReducers, applyMiddleware } from 'redux'
import { Provider } from 'react-redux'
import createSagaMiddleware from 'redux-saga'

// These are the configured API building blocks returned by the kraken `creator` call
import { reducer, saga } from '../configuredApi'

// Create the saga middleware
const sagaMiddleware = createSagaMiddleware()

// Mount it on the Store
const store = createStore(
  combineReducers({ cache: reducer }),
  applyMiddleware(sagaMiddleware)
)

// Then run your pre-configured API saga
sagaMiddleware.run(saga, store.getState)

// Export a provider component wrapping the redux Provider
const ApiProvider = ({ children }) => (
  <Provider store={store}>{children}</Provider>
)

export default ApiProvider

Connecting your components

Once you have finished the setup and declared your types, you can start using your API methods in your comopnents. connect is a higher-order component creator, which you can use to connect React components to your API.

import { connect, types } from 'your-api'

function User({ userFetch }) {
  if (userFetch.pending) {
    return <div>Loading user...</div>
  }

  if (userFetch.rejected) {
    return (
      <div>
        Could not fetch user:
        <br />
        {userFetch.reason}
      </div>
    )
  }

  const user = userFetch.value

  return (
    <div>
      {user.firstName} {user.lastName}
    </div>
  )
}

const enhance = connect(({ value }) => ({
  userFetch: {
    type: types.USER,
    id: value,
  },
}))

export default enhance(User)

As you can see using the connect method requires minimal setup. Also in this case we didn't have to state that we want to fetch as this is the default action. When we enhance a component using connect we can hook it up to as many API resources as we want to. In this example we only used the user resource and created a new prop userFetch that represents the API request. You include certain configuration options to get more power over when and how requests go out.

Option Default Required Description
type undefined true The API type which is concered.
query undefined false An object of query parameters.
refresh . undefined false Compares the old value with the new value to determine if a fresh request should be made.
requestParams undefined false An object of extra parameters to tell different requests apart.
id undefined false Shortcut to provide an ID query parameter, id: 1 is equivalent to query: { id: 1 }
method fetch false Either one of fetch, create, update, or remove.
lazy false false Only useful in combination with method: 'fetch'. Per default, the resource is fetched on component mount. Set lazy: true to not fetch on mount.
denormalize false false Inline all referenced data into the injected prop.

Notes on using denormalize: true

In most scenarios you will not need to use this. If you choose to use it, do so with caution.

By default the injected value prop will be normalized according to the schema you have defined for your api types. This means that even if your server response inlines data for referenced entities, you will only have access to the id property of any referenced entity. In a lot of cases this will be absolutely fine, since different data is probably handled by different UI components that can be connected to kraken respectively. However, in some rare cases you might need to access the referenced data right away. In these circumstances you can pass denormalize: true. The injected value will then be denormalized and contain all nested or referenced data. If you choose to do so be advised that the injected props will change on every render - even when your state does not change. This is due to the fact, that kraken can no longer know exactly where different parts of the state are being used. If you want to optimise for the least number of re-renders, you need to implement shouldComponentUpdate on your own in these cases.

Injected props

For every key returned by the function passed to connect, a prop with that name will be passed down to the wrapped component. This prop will be a function that you can call to trigger the request. While fetch requests will be triggered automatically on component mount, the requests for all other methods will not be dispatched until you call the injected function prop. The argument you provide in this call will be passed as second argument to the respective type's method implemention and is usually used as request body.

The function type props passed to your wrapped component carry additional properties. (JavaScript allows to assign properties to functions, too.) You can read these properties to render different stuff at different request lifecycle states.

When connect creates such a promise-like prop it adds certain lifecycle information that can be used to render a resource.

Name Type Description
value any The response value. It will only be set if fullfilled === true.
pending boolean true when the request is sent out but has not returned yet. false otherwise.
fullfilled boolean true when the last request returned without error. false otherwise.
rejected boolean true when the last request returned with errors. false otherwise.
reason string When rejected is true, we try to include the error message we got from the server in this property.
responseHeaders Headers The Fetch API's response Headers object, only returned when fullfilled is true.

Cache Policies

kraken builds up a cache of your data. When you integrate kraken into your app you might want to change how certain data is handled during its lifecycle. To activate a cache policy you need to export it under the key cachePolicy in your api type definition file (where you also define the schema, etc.).

In order to do this we provide some prebuild cache policies and also allow you to create your own.

Query from cache (cachePolicies.queryFromCache)

This policy is mainly used as an optimistic create. It will enhance request results with data that is already present in the cache. By doing so a promise prop will already have a value attribute even when the request didn't return yet from the server. The objects from the cache will be filtered by using the query option that was set inside the connect call.

// type definition
import { cachePolicies } from '@signavio/kraken'

export const cachePolicy = cachePolicies.queryFromCache

// component

const AddUser = ({ createUser }) => (
  <button onClick={() => createUser({ fullName: 'John Doe' })}>
    {createUser.pending
      ? `Creating: ${createUser.value.fullName}`
      : 'Create new user'}
  </button>
)

const enhance = connect(() => ({
  createUser: {
    type: types.USER,
    method: 'create',
  },
}))

export default enhance(AddUser)

In the example above the type declared that the queryFromCache policy should be used. Now the moment the createUser method is called the value will be available on the promise prop. This way, we can already use the value on the promise prop to show the fullName of the user even though the server didn't return a response yet.

Caveats

In order for this mechanism to work the query params you specify must match attributes on the actual entities.

Optimistic remove (cachePolicies.optimisticRemove)

This policy is active by default for all Entity types

Using this policy you can specify that entities are removed when the remove action is dispatched.

Caveats

In order for this mechanism to work the query params you specify must match attributes on the actual entities.

Remove references to deleted entities (cachePolicies.removeReferencesToDeletedEntities)

This policy is active by default for all Array types

If your application becomes larger then certain entities might have a relation to one another. For instance To-Dos could have assignees. The question then is, what happens when for instance the assignee for a certain To-Do is removed. By default these changes will not be reflected and you need to perform the necessary updates yourself. However, for this particular use case kraken also offers a cache policy you can use. It ensures that once an entity is deleted all references to that entity will also be cleaned up in your local state. In the To-Do example this would mean that once the assignee is deleted also the respective To-Do would no longer point to the stale id.

Adding your own cache policy

There are two possiblities to influence the cache. You can either change the request state or the entity state.

export type CachePolicyT = {
  updateRequestOnCollectionChange?: (
    apiTypes: ApiTypeMap,
    request: Request,
    collection: EntityCollectionT,
    entityType: EntityType
  ) => Request,
  updateEntitiesOnAction?: (
    apiTypes: ApiTypeMap,
    entities: EntitiesState,
    action: Action
  ) => EntitiesState,
}

In order to influence the entity state you can export a method under the key updateEntitiesOnAction. It will be called for every action that is executed by the entities reducer. You will get access to all the api types you have declared, the current state and also get the action that was dispatched.

If you want to influence the state of a certain request when some data changes, you need to export your policy under the key updateRequestOnCollectionChange. This method will be called for every request and will also be handed the entity collection which is influenced by the request.

To apply multiple cache policies at the same time you can use cachePolicies.compose and hand in all policies that you want to apply.

Questions

If you have any questions please talk to one of the authors or create an issue.

Contribute

Feel free to submit PRs!

Every PR that adds new features or fixes a bug should include tests that cover the new code. Also the code should comply to our common code style. You can use the following commands in order to check that while you are developing.

# Install all dependencies
yarn

# Run the test suite
yarn test

# Check test coverage
yarn coverage

# Check whether you have any linting errors
yarn lint

kraken's People

Contributors

alexandr-g avatar dependabot[bot] avatar frontendphil avatar gisaklement avatar hashemkhalifa avatar hermandahlen avatar jfschwarz avatar khamiltonuk avatar mersocarlin avatar renovate[bot] avatar silentgert avatar

Watchers

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

kraken's Issues

IE9 not supported

The current implementation relies on btoa being available in the browser. This is not true for IE9

Delete action must remove references from other types

Assume post and comment entities and their schema definition as:

const commentSchema = new schemas.Entity('comments')
const postSchema = new schemas.Entity('posts', {
  comments: [commentSchema],
}) 
{
  entities: {
    comments: {
      'comment1': { ... }
    },
    posts: {
      'post1': {
        comments: ['comment1'],
        ...
      }
    }
  }
}

Make a DELETE request to /api/comments/comment1.

  • Store at comments is empty
  • Even though we re-fetch /api/posts, our store at posts['post1'].comments still contains comment1 reference

Mismatch between request reducer and sagas

Describe the bug
When I call a promise prop twice two requests will be sent out but only the first call will be recorded in the request state.

Expected behavior
Either the request state should also update with each call or no second request should be sent out because the first one is already pending.

pending flag set to true even though action is lazy

When you connect you component like this:

connect(({ id }) => ({
  fetchEntity: {
    type: 'TYPE',
    lazy: true,
    id,
  },
}))

you would expect that fetchEntity.pending is set to false when you didn't call fetchEntity() before. However pending is set to true even though nothing has happened or is happening yet.

calling a fetch function should always fetch from the server

Consider this setup:

connect(({ id }) => ({
  fetchEntity: {
    type: 'TYPE',
    id,
  },
}))

This fetches initially from the server. However if you want to re-fetch later on a call to fetchEntity() does not result in any action being taken. However if the user intends to fetch again then this should go out to the server.

Support params in `remove` actions

Currently, it is not possible to hand in parameters to the remove action. This can only be done, through the query part of the promise props. However, this hinders a proper application design, as this means that individual resources are hard to remove. You cannot, for instance, create a container component that listens to onRemove handlers on its children and then triggers the respective remove action.

Include body in request state

This is an idea on which I would like to get some feedback.

I was just trying to write some tests to actually check that a create request would go out with the correct content. However, after the action was fired in the application code I have no means to access the body of the request. I can, however, access the query. Yes, including the body in the request state would definitively blow up the state, but it would also make integration testing easier.

@jfschwarz you got an opinion?

Update repo name and docs

In the context of the whole of Signavio, generic-api is probably too broad.

  • Rename repo to workflow-api, or something else that's more specific
  • Edit the README to refer to Signavio Workflow

normalizr as peer dependency?

I just ran into the problem of non-functioning instanceof schemas.Entity checks as there were two different copies of normalizr, one used for defining the apps API types and another installed as a sub-dependency of generic-api.

Maybe it would be a good idea to define normalizr only as a peer dependency to prevent such problems.

Retrieve results only from entity cache

Currently connected views do not get updated if, for instance data is added to the store by another action. The reason seems to be that data can be both in the promises and entities cache. We'll need to change this, so that the single source of data is the entities cache and promises is only used to get information about the state of something.

Sort entities and requests by alphabetical order

Is your feature request related to a problem? Please describe.
Kraken, entities/requests can get big when debugging, It can be difficult to find items in the entities
store

Describe the solution you'd like
The items should be sorted alphabetically

Describe alternatives you've considered
n/a

Additional context
Screenshot 2019-04-30 at 16 54 51

Allow DELETE request to have response body on success

In the current implementation a delete request does not allow a response body when the request was executed successfully.

I stumbled upon that in this user story effektif/documentation#197:
When requesting the BE to delete a list of caseIds the request itself might be successful but not all caseIds could be deleting e.g. due to no access rights or something similar. In this case I get a list back of caseIds which couldn't be deleted which I have to display to the user.

value undefined when refresh prop is used

Describe the bug
We're running into an issue when we use the refresh property. Before the request is done the pending prop is already set to false even though no new value has been computed. This breaks the application and forces us to do more checks than actually necessary.

Make integration testing instead of testing sagas

Because its so hard to write unit tests for sagas, as you have to deal with generator internals and effects, let's follow a different approach and use integration testing on whole connected example components.

Prepare Open-Source release

I feel that this package is already mature enough to be open sourced. Also open sourcing would mean that their needs to be more documentation, which I would like as well :) @jfschwarz WDYT? I would see the following tasks:

  • Improve documentation
  • Strip eventual references and assumptions based on internal setup
  • Setup PR builders for tests
  • Check and remove linter violations
  • Define issue templates
  • Define PR templates
  • Define how to contribute
  • Make an announcement also to the rest of Signavio to raise awareness
  • (Maybe) move this project to the signavio organization

Show body of requests in kraken

Is your feature request related to a problem? Please describe.
I often use the redux dev tools to debug network requests, when making a request I would like to see the body of the request, currently I can see various properties such as fulfilled, outstanding, pending and more

Describe the solution you'd like
I would like to see the body in the fetch request

Describe alternatives you've considered
Use the network panel in dev tools to see the body of the request

Provide response header

Is your feature request related to a problem? Please describe.
To calculate the number of pages needed for paginating workflow versions I need to get Meta-Result-Size from the response header provided by our backend.

Describe the solution you'd like
When fetching versions via kraken I want to be able to get the property responseHeader
fetchVersions.responseHeader

Add support for pagination

This issue is a a requirement we have at signavio workflow (effektif/client#86). There we have infinite scroll pagination over different data endpoints. Therefore we need to pass offset and pagesize query parameters to the request. I understand this is currently possible. What probably is missing, is the behaviour that all past results should also be included in the final result set.

Expected behaviour:

>> GET /users?offset=0&pagesize=25

The first 25 users are in the result.

>> GET /users?offset=25&pagesize=25

All 50 users are in the result.

If I am wrong about this, please point me in the right direction.

Action Required: Fix Renovate Configuration

There is an error with this repository's Renovate configuration that needs to be fixed. As a precaution, Renovate will stop PRs until it is resolved.

Error type: Preset name not found within published preset config (monorepo:angularmaterial). Note: this is a nested preset so please contact the preset author if you are unable to fix it yourself.

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.