All Projects → rhwilr → Adonis Bumblebee

rhwilr / Adonis Bumblebee

Licence: mit
Api Transformer for AdonisJs Framework

Programming Languages

javascript
184084 projects - #8 most used programming language

Projects that are alternatives of or similar to Adonis Bumblebee

Hprose Java
Hprose is a cross-language RPC. This project is Hprose 2.0 for Java
Stars: ✭ 542 (+333.6%)
Mutual labels:  api, serializer
Jsonapi Utils
Build JSON API-compliant APIs on Rails with no (or less) learning curve.
Stars: ✭ 191 (+52.8%)
Mutual labels:  api, serializer
Acts as api
makes creating API responses in Rails easy and fun
Stars: ✭ 506 (+304.8%)
Mutual labels:  api, serializer
Bj Decoupage Territorial
API pour récupérer les départements, communes, arrondissements et les quartiers du Bénin
Stars: ✭ 29 (-76.8%)
Mutual labels:  api, adonisjs
Hprose Golang
Hprose is a cross-language RPC. This project is Hprose for Golang.
Stars: ✭ 1,143 (+814.4%)
Mutual labels:  api, serializer
Symfony Jsonapi
JSON API Transformer Bundle for Symfony 2 and Symfony 3
Stars: ✭ 114 (-8.8%)
Mutual labels:  api, serializer
Dictfier
Python library to convert/serialize class instances(Objects) both flat and nested into a dictionary data structure. It's very useful in converting Python Objects into JSON format
Stars: ✭ 67 (-46.4%)
Mutual labels:  api, serializer
Jsonapi.rb
Lightweight, simple and maintained JSON:API support for your next Ruby HTTP API.
Stars: ✭ 116 (-7.2%)
Mutual labels:  api, serializer
Covid19 Api
API for exploring COVID-19 cases around the globe
Stars: ✭ 123 (-1.6%)
Mutual labels:  api
Laravel Hackathon Starter
💻 A hackathon/MVP boilerplate for laravel web applications. Start your hackathons without hassle.
Stars: ✭ 1,589 (+1171.2%)
Mutual labels:  api
Revapi
Revapi is an API analysis and change tracking tool written in Java. Its focus is mainly on Java language itself but it has been specifically designed to not be limited to just Java. API is much more than just java classes - also various configuration files, schemas, etc. can contribute to it and users can become reliant on them.
Stars: ✭ 122 (-2.4%)
Mutual labels:  api
Api Response
Simple and ready to use API response wrapper for Laravel.
Stars: ✭ 123 (-1.6%)
Mutual labels:  api
Covid 19 Api
This is the code running in AWS Lambda powering covid-api.mmediagroup.fr/v1. The API provides realtime and historical data on Coronavirus COVID-19 confirmed cases, deaths, and recovered cases. This API has now been called over 3 million times, thank you!
Stars: ✭ 122 (-2.4%)
Mutual labels:  api
Wizard
Wizard是一款开源的文档管理工具,支持Markdown/Swagger/Table类型的文档。
Stars: ✭ 1,733 (+1286.4%)
Mutual labels:  api
Tooty
An alternative multi-accounts Web client for Mastodon.
Stars: ✭ 124 (-0.8%)
Mutual labels:  api
Structomap
Easily and dynamically generate maps from Go static structures
Stars: ✭ 122 (-2.4%)
Mutual labels:  serializer
Youtube Scrape
Scrape YouTube searches (API)
Stars: ✭ 122 (-2.4%)
Mutual labels:  api
Zoomhub
Share and view high-resolution images effortlessly
Stars: ✭ 122 (-2.4%)
Mutual labels:  api
Mis
模块接口服务,如何在一个模块内维护其对外暴露的接口(包括打包发布),而不是把接口和接口实现分离到两个不同的模块?
Stars: ✭ 124 (-0.8%)
Mutual labels:  api
Huxley
JSON proxy for the UK National Rail Live Departure Board SOAP API
Stars: ✭ 123 (-1.6%)
Mutual labels:  api

adonis-bumblebee

API Transformer Provider for the AdonisJs framework. This library provides a presentation and transformation layer for complex data output.




NPM Version Build Status Codecov

Goals

  • Create a “barrier” between source data and output, so changes to your models do not affect API response
  • Include relationships for complex data structures
  • Manipulate and typecast data in one place only

Table of contents

Installation

Run this command to install the package and follow the instructions in instructions.md.

adonis install adonis-bumblebee

Simple Example

For the sake of simplicity, this example has been put together as one simple route function. In reality, you would create dedicated Transformer classes for each model. But we will get there, let's first have a look at this:

Route.get('/', async ({ response, transform }) => {
  const users = await User.all()

  return transform.collection(users, user => ({
    firstname: user.first_name,
    lastname: user.last_name
  }))
})

You may notice a few things here: First, we can import transform from the context, and then call a method collection on it. This method is called a resources and we will cover it in the next section. We pass our data to this method along with a transformer. In return, we get the transformed data back.

Resources

Resources are objects that represent data and have knowledge of a “Transformer”. There are two types of resources:

  • Item - A singular resource, probably one entry in a data store
  • Collection - A collection of resources

The resource accepts an object or an array as the first argument, representing the data that should be transformed. The second argument is the transformer used for this resource.

Transformers

The simplest transformer you can write is a callback transformer. Just return an object that maps your data.

const users = await User.all()

return transform.collection(users, user => ({
  firstname: user.first_name,
  lastname: user.last_name
}))

But let's be honest, this is not what you want. And we would agree with you, so let's have a look at transformer classes.

Transformer Classes

The recommended way to use transformers is to create a transformer class. This allows the transformer to be easily reused in multiple places.

Creating a Transformer

You can let bumblebee generate the transformer for you by running:

adonis make:transformer User

The command will create a new classfile in app/Transformers/. You can also create the class yourself, you just have to make sure that the class extends Bumblebee/Transformer and implements at least a transform method.

const BumblebeeTransformer = use('Bumblebee/Transformer')

class UserTransformer extends BumblebeeTransformer {
  transform (model) {
    return {
      id: model.id,

      firstname: model.first_name,
      lastname: model.last_name
    }
  }
}

module.exports = UserTransformer

Note: You also get the context as the second argument in the transform method. Through this, you can access the current request or the authenticated user.

Note: A transformer can also return a primitive type, like a string or a number, instead of an object. But keep in mind that including additional data, as covered in the next section, only work when an object is returned.

Using the Transformer

Once the transformer class is defined, it can be passed to the resource as the second argument.

const users = await User.all()

return transform.collection(users, 'UserTransformer')

If the transformer was placed in the default location App/Transformers, you can reference it by just passing the name of the transformer. If you placed the transformer class somewhere else or use a different path for your transformers, you may have to pass the full namespace or change the default namespace in the config file. Lastly, you can also pass a reference to the transformer class directly.

Note: Passing the transformer as the second argument will terminate the fluent interface. If you want to chain more methods after the call to collection or item you should only pass the first argument and then use the transformWith method to define the transformer. See Fluent Interface

Including Data

When transforming a model, you may want to include some additional data. For example, you may have a book model and want to include the author for the book in the same resource. Include methods let you do just that.

Default Include

Includes defined in the defaultInclude getter will always be included in the returned data.

You have to specify the name of the include by returning an array of all includes from the defaultInclude getter. Then you create an additional method for each include, named like in the example: include{Name}.

The include method returns a new resource, that can either be an item or a collection. See Resources.

class BookTransformer extends BumblebeeTransformer {
  static get defaultInclude () {
    return [
      'author'
    ]
  }

  transform (book) {
    return {
      id: book.id,
      title: book.title,
      year: book.yr
    }
  }

  includeAuthor (book) {
    return this.item(book.getRelated('author'), AuthorTransformer)
  }
}

module.exports = BookTransformer

Note: Just like in the transform method, you can also access to the context through the second argument.

Note: If you want to use snake_case property names, you would still name the include function in camelCase, but list it under defaultInclude in snake_case.

Available Include

An availableInclude is almost the same as a defaultInclude, except it is not included by default.

class BookTransformer extends BumblebeeTransformer {
  static get availableInclude () {
    return [
      'author'
    ]
  }

  transform (book) {
    return {
      id: book.id,
      title: book.title,
      year: book.yr
    }
  }

  includeAuthor (book) {
    return this.item(book.getRelated('author'), AuthorTransformer)
  }
}

module.exports = BookTransformer

To include this resource you call the include() method before transforming.

return transform.include('author').item(book, BookTransformer)

These includes can be nested with dot notation too, to include resources within other resources.

return transform.include('author,publisher.something').item(book, BookTransformer)

Parse available includes automatically

In addition to the previously mentioned include method, you can also enable parseRequest in the config file. Now bumblebee will automatically parse the ?include= GET parameter and include the requested resources.

Transformer Variants

Sometimes you may want to transform some model in a slitely different way while sill utilizing existing include methods. To use out book example, you may have an api endpoint that returns a list of all books, but you don't want to include the summary of the book in this response to save on data. However, when requesting a single book you want the summary to be included.

You could define a separate transformer for this, but it would be much easier if you could reuse the existing book transformer. This is where transform variants come in play.

class BookTransformer extends BumblebeeTransformer {
  transform (book) {
    return {
      id: book.id,
      title: book.title,
      year: book.yr
    }
  }

  transformWithSummary (book) {
    return {
      ...this.transform(book),
      summary: book.summary
    }
  }
}

module.exports = BookTransformer

We define a transformWithSummary method that calls our existing transform method and adds the book summary to the result.

Now we can use this variant by specifing it as follows:

return transform.collection(books, 'BookTransformer.withSummary')

EagerLoading

When you include additional models in your transformer be sure to eager load these relations as this can quickly turn into n+1 database queries. If you have defaultIncludes you should load them with your initial query. In addition, bumblebee will try to load related data if the include method is named the same as the relation.

To ensure the eager-loaded data is used, you should always use the .getRelated() method on the model.

Metadata

Sometimes you need to add just a little bit of extra information about your model or response. For these situations, we have the meta method.

const User = use('App/Models/User')

const users = await User.all()

return transform
  .meta({ 
    access: 'limited'
  })
  .collection(users, UserTransformer)

How this data is added to the response is dependent on the Serializer.

Pagination

When dealing with large amounts of data, it can be useful to paginate your API responses to keep them lean. Adonis provides the paginate method on the query builder to do this on the database level. You can then pass the paginated models to the paginate method of bumblebee and your response will be transformed accordingly. The pagination information will be included under the pagination namespace.

const User = use('App/Models/User')
const page = 1

const users = await User.query().paginate(page)

return transform.paginate(users, UserTransformer)

Serializers

After your data passed through the transformer, it has to pass through one more layer. The Serializer will form your data into its final structure. The default serializer is the PlainSerializer but you can change this in the settings. For smaller APIs, the PlainSerializer works fine, but for larger projects, you should consider the DataSerializer.

PlainSerializer

This is the simplest serializer. It does not add any namespaces to the data. It is also compatible with the default structure that you get when you return a lucid model from a route.

// Item
{
  foo: 'bar'
}

// Collection
[
  {
    foo: bar
  },
  {...}
]

There is one major drawback to this serializer. It does not play nicely with metadata:

// Item with meta
{
  foo: 'bar',
  meta: {
    ...
  }
}

// Collection
{
  data: [
    {...}
  ],
  meta: {
    ...
  }
}

Since you cannot mix an Array and Objects in JSON, the serializer has to add a data property if you use metadata on a collection. The same is true if you use pagination. This is why we do not recommend using PlainSerializer when using these features. But other than that, this serializer works great for small and simple APIs.

DataSerializer

This serializer adds the data namespace to all of its items:

// Item
{
  data: {
    foo: 'bar',
    included: {
      data: {
        name: 'test'
      }
    }
  }
}

// Collection
{
  data: [
    {
      foo: bar
    },
    {...}
  ]
}

The advantage over the PlainSerializer is that it does not conflict with meta and pagination:

// Item with meta
{
  data: {
    foo: 'bar'
  },
  meta: {
    ...
  }
}

// Collection
{
  data: [
    {...}
  ],
  meta: {...},
  pagination: {...}
}

SLDataSerializer

This serializer works similarly to the DataSerializer, but it only adds the data namespace on the first level.

// Item
{
  data: {
    foo: 'bar',
    included: {
      name: 'test'
    }
  }
}

Fluent Interface

Bumblebee has a fluent interface for all the setter methods. This means you can chain method calls which makes the API more readable. The following methods are available on the transform object in the context and from Bumblebee.create() (see below).

Chainable methods:

  • collection(data)
  • item(data)
  • null(data)
  • paginate(data)
  • meta(metadata)
  • transformWith(transformer)
  • usingVariant(variant)
  • withContext(ctx)
  • include(include)
  • setSerializer(serializer)
  • serializeWith(serializer) (alias for setSerializer)

Terminating methods:

  • collection(data, transformer)
  • item(data, transformer)
  • paginate(data, transformer)
  • toJSON()

You may want to use the transformer somewhere other than in a controller. You can import bumblebee directly by the following method:

const Bumblebee = use('Adonis/Addons/Bumblebee')

let transformed = await Bumblebee.create()
    .collection(data)
    .transformWith(BookTransformer)
    .withContext(ctx)
    .toJSON()

You can use the same methods as in a controller. With one difference: If you need the context inside the transformer, you have to set it with the .withContext(ctx) method since it is not automatically injected.

Credits

Special thanks to the creator(s) of Fractal, a PHP API transformer that was the main inspiration for this package. Also, a huge thank goes to the creator(s) of AdonisJS for creating such an awesome framework.

Note that the project description data, including the texts, logos, images, and/or trademarks, for each open source project belongs to its rightful owner. If you wish to add or remove any projects, please contact us at [email protected].