Code Monkey home page Code Monkey logo

adonis-bumblebee's Introduction

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.

adonis-bumblebee's People

Contributors

dependabot-preview[bot] avatar dependabot[bot] avatar greenkeeper[bot] avatar rhwilr avatar romainlanz avatar spamoom avatar vincentducorps 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

adonis-bumblebee's Issues

Namespaced Transformers Support

Hi, again...

I think is good to implement a way for developers create transformers inside Folders.

E.g. User Run: adonis make:transformer Products/Product

The output should be a file inside the folder app/Transformers/Products/ProductTransformer.js

So, i've just created a Pull Request supporting this feature.

.paginate can't call getRelated method

As the title says, it seams like it's impossible to transform related content if using .paginate() instead, for example, .collection().

This will work (but it will not return adonis pagination info):

const posts = await Posts.query().with('tags').paginate()
transform.include('tags').collection(posts, PostTransformer)

This will not work:

const posts = await Posts.query().with('tags').paginate()
transform.include('tags').paginate(posts, PostTransformer)

Error thrown is: model.getRelated is not a function... PostTransformer has this include:

includeTags(model) {
        return this.collection(model.getRelated('tags'), TagTransformer)
    }

Without .include() and defaultIncludes inside transformer, .paginate() works as expected

Chain Transformers with Routes

It would be nice to do:

Route
  .get('/users', 'UsersController.list')
  .validator('vue/Pagination')
  .middleware(['auth'])
  .transform('UsersTransformer.withPosts')

Is it hard to implement?

Thank you for addon!

Error: Transformer is not a function

is the first time i use this package, can you help me?

I'm getting this error when sending a request to the endpoint that uses the transform method.

captura de tela 2018-10-16 as 12 42 23

Suggestion: make include{Name} method name accept context as second param

I belive this is sometimes needed.
For example, you want to transform users who have posts.
Posts can have status of unpublished, and when fetching a list of users you shouldn't see unpublished posts attached to a user. But, if you are an admin or you are a user that is accessing his own profile (logged in user). You would for example like to see all unpublished posts also.

This would be easy to implement if include{Name} could also have access to ctx param. For example:

includePosts(model, ctx) {
    const currentUser = ctx.auth.getUser()
    let query = model.posts()
    if(currentUser) {
            // modify query to your liking...
    } else {
            // make default query, not including for example unpublished posts
    }

    return this.collection(query.fetch(), TagTransformer)
}

Include underscore not working

First of all, i'm not a native speaker and i used google translator to help me write this issue.
By now congratulations on the package. It is very useful and practical

I am having difficulties adding an underscore property in the available include

On my Transformer:

static get availableInclude () {
     return ['amount_used', 'administrator']
   }

includeAmountUsed (coupon) {
     const {amount_used} = coupon.toJSON ()

     return amount_used
   }

In Controller:

async show ({...}) {
...
const transformed = await transform
         .include ('amount_used, administrator')
         .item (coupon, CouponTransformer)

       return response.json (transformed)
}

JSON response example:

{
  "administrator": {
    "id": 12,
    "first_name": "Kate",
    "last_name": "Julien",
    "email": "[email protected]",
    "cpf": "58278510784",
    "date_birth": "2102-05-05T03: 00: 00.000Z"
  },
  "id": 8,
  "code": "jTh) vM5!",
  "valid_from": null,
  "valid_until": null,
  "type": "currency",
  "quantity": 995,
  "discount": "78.53"
}

Already tried a debug or add a console log in includeAmountUsed, but the function is not called. I have also tried renaming the function to includeAmount_used, includeAmount_Used.
If rename the property to amountUsed works perfectly, but I wanted JSON to be amount_used, can you help me? Thanks

Error in README

I think there is a mistake in the README.md :

Shouldn't this :

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

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

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

module.exports = UserTransformer

be this instead :

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

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

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

module.exports = UserTransformer

? (model instead of user)

PS: Thank you for your job!

Access transform object from services

Hi!

I'm not being able to see any documentation on how I can access 'transform' instance outside controller. Is such a thing bad design choice, or is there any way that I can do that.

I have an ProductController and a ProductService, instead of transforming data in controller I would like to be able to use transform object inside service, something like this
const transform = use('Transform')
and then do the transformation.

Thanks :)

How to access to pivot table data?

Hello :)

I have 3 tables :

  • users
  • skills
  • user_skill (pivot table)

In user_skill (the pivot table) I have a field for the level (rank) of the user on this skill.

How to get this field in my transformer?
Thanks

CODE BELLOW DOESN'T WORK

"Cannot read property 'rank' of undefined"

Model

class User extends Model {
  skills() {
    return this.belongsToMany('App/Models/Skill')
      .pivotTable('user_skill')
      .withPivot(['rank'])
  }
}

Controller

class UserController {
    async show({ params, request, response, transform }) {
        let user = await User.find(params.id)

        if (!user) {
          return response.status(404).json({ data: 'Resource not found' })
        }

        await user.loadMany(['skills'])

        return transform.include(['skills']).item(user, UserTransformer)
  }
}

Transformer

class UserTransformer extends TransformerAbstract {
  availableInclude() {
    return [
      'skills'
    ]
  }

  transform(user) {
    return {
      id: user.id,
      firstname: user.firstname,
      lastname: user.lastname
    }
  }

  includeSkills(user) {
    return this.collection(user.getRelated('skills'), skill => ({
        id: skill.id,
        name: skill.name,
        rank: skill.pivot.rank
    }))
}

availableInclude not working as expected

When I try to use the include method I don't get the expected response, it's like the "transform.include('product')" I used was ignored. But when i try to do the same using the defaultInclude, it's work.

right now im using the following method on my controller:

async index({ request, response, pagination, transform }) {
	const query = Category.query()

	let categories = await query.paginate(pagination.page, pagination.limit)
	categories = await transform.include('product').paginate(categories, Transformer)
	
	return response.send(categories)
}

and thats on my transformer class:

class ProductsCategoryTransformer extends BumblebeeTransformer {
	static get availableInclude() {
		return ['product', 'category']
	}
	
	transform(model) {
		return {
			category_id: model.category_id,
			product_id: model.product_id,
		}
	}

	includeCategory(model) {
		return this.item(model.getRelated('category'), CategoryTransformer)
	}

	includeProduct(model) {
		return this.item(model.getRelated('product'), ProductTransformer)
	}
}

module.exports = ProductsCategoryTransformer

A transformer must be a function or a class extending TransformerAbstract

When I try to use any of the following: "ProductController.index" or "CategoryController.show" everything works as expected. But after making the first one, at the next request I get the following error message: "A transformer must be a function or class extending TransformerAbstract" .

Below is the summary of the classes that I am using.
 

class Product extends Model {
    categories() {
        return this.belongsToMany('App/Models/Category', 'product_id', 'category_id').pivotTable('product_categories')
    }
}
class Category extends Model {
    products() {
        return this.belongsToMany('App/Models/Product', 'category_id', 'product_id').pivotTable('product_categories')
    }
}
class ProductCategorySchema extends Schema {
	up() {
		this.create('product_categories', table => {
			table.increments()
			table
				.integer('category_id')
				.unsigned()
				.references('id')
				.inTable('categories')
				.onDelete('cascade')
			table
				.integer('product_id')
				.unsigned()
				.references('id')
				.inTable('products')
				.onDelete('cascade')
		})
	}
}
class ProductController {
    async index({ request, response, pagination, transform }) {
        const query = Product.query()
        let products = await query.paginate(pagination.page, pagination.limit)
        products = await transform.include('categories').paginate(products, Transformer)
        return response.send(products)
    }
}
class CategoryController {
	async show({ params: { id }, response, transform }) {
		let category = await Category.findOrFail(id)
		category = await transform.include('products').item(category, Transformer)
		return response.send(category)
	}
}
class ProductTransformer extends BumblebeeTransformer {
	static get availableInclude() {
		return ['categories']
	}
	includeCategories(model) {
		return this.collection(model.getRelated('categories'), CategoryTransformer)
	}
}
class CategoryTransformer extends BumblebeeTransformer {
	static get availableInclude() {
		return ['products']
	}
	includeProducts(model) {
		return this.collection(model.getRelated('products'), ProductTransformer)
	}
}

Suggestion: return array of strings after transform

For example I have TagTransformer which should return tags as array of tag.title strings like:
tags: ['tag1', 'tag2', 'tag3' ... ]

Currently it's only possible to return array of tag objects like:
tags: [{title: 'tag1'}, {title: 'tag2'}, {title: 'tag3'} ...]

property transform does not exits in ctx

Hey Geeks,
After installing transformer I found out property transform does not exist in httpContextContract in adonis JS even after installing bumblebee-transformer.When will it be available or will v5 make us use bumblbee transformer like mentioned.

How to handle .paginate metadata?

When transforming, for example, users collection and using .paginate to query them... Is it possible to get classic adonis response containing paginate metadata?

When using:

const users = await User.query().paginate()
return transform.collection(users, UserTransformer)

Expected JSON response would be:

{
	"data": {
		"total": 112,
		"perPage": 10,
		"page": 1,
		"lastPage": 12,
		"data": [{..... transformed users here .... } ....
...
}

Some ideas for a future release

Hey all 👋

First of all, thanks for this package. It's really useful and helps to keep an API consistent and simple.

I've been working with Bumblebee for some projects and found some things that could be changed to improve the developer experience. I can work on them if they are accepted.


1. Accept namespace instead of an instance of transformers

Currently, we need to require the transformer in our controller (or another transformer) and then use this require to use it.

const UserTransformer = use('App/Transformers/UserTransformer')

...

return transform.collection(users, UserTransformer)

It could be better to directly use the namespace.

return transform.collection(users, 'App/Transformers/UserTransformer')
// or via default prefix
return transform.collection(users, 'UserTransformer')

2. Rename method toArray => toJSON
When we use the fluent interface the latest method to call is named toArray(). In fact, this method transforms your object to JSON syntax, so it would be better to name it toJSON().


3. Use JS getter
When we define our own transformer we make use of JS method. In the meanwhile, using static JS getter would be way faster (~2x).

// before
class BookTransformer extends TransformerAbstract {
  defaultInclude () {
    return ['author']
  }
}

// after
class BookTransformer extends TransformerAbstract {
  static get defaultInclude () {
    return ['author']
  }
}

4. Defines the transformer for collection and item
Currently, we have only one transform method inside our custom transformer. It could be better to have a transformCollection and transformItem when you need a different schema for a collection and an item.

For the moment, you need to create two transformers ArticleItemTransformer ArticleCollectionTransformer.


Let me know what you think about them.

Honor setHidden and setVisible

Thanks for creating such a useful and easy to use library!

I have just started playing around with it and have bumped into something I would like to hear your thoughts about. I am used to applying setHidden or setVisible in my queries to filter out redundant data. For example, consider the following query:

const study = Study
    .where('id', params.id)
    .with('jobs.variables')
    .with('participants')
    .first()

This provides me with the result:

{
  "data": {
    "jobs": [
      {
        "variables": [
          {
            "id": 1,
            "study_id": 1,
            "dtype_id": 1,
            "name": "distractor",
            "created_at": "2020-06-30T12:40:02.000Z",
            "updated_at": "2020-06-30T12:40:02.000Z"
          }
        ],
        "id": 1,
        "study_id": 1,
        "order": 1,
        "created_at": "2020-06-30T12:40:02.000Z",
        "updated_at": "2020-06-30T12:40:02.000Z"
      },
      {
        "variables": [
          {
            "id": 1,
            "study_id": 1,
            "dtype_id": 1,
            "name": "distractor",
            "created_at": "2020-06-30T12:40:02.000Z",
            "updated_at": "2020-06-30T12:40:02.000Z"
          }
        ],
        "id": 2,
        "study_id": 1,
        "order": 2,
        "created_at": "2020-06-30T12:40:02.000Z",
        "updated_at": "2020-06-30T12:40:02.000Z"
      }
    ],
    "id": 1,
    "name": "Attentional Capture",
    "description": "Basic attentional capture experiment",
    "active": 1,
    "osexp_path": "/public/osexp/attentional-capture.osexp",
    "created_at": "2020-06-30T12:40:02.000Z",
    "updated_at": "2020-06-30T12:40:02.000Z"
  }
}

Because the data of jobs is provided as related data for study, I for instance don't see any use for including study_id in each job record (and often I leave away even more data). I used to omit superfluous data by writing my first query as:

const study = await auth.user.studies()
  .where('id', params.id)
  .with('jobs', (builder) => {
    builder
      .setHidden(['study_id'])
      .with('variables')
  })
  .with('participants')
  .first()

This effectively hides away all study_id fields in job records if I simply return {data: study} at the end of the controller. However, if I use a transformer, this study_id field is present again! Thus if I end the controller action with:

return transform
  .include('jobs')
  .item(study, 'StudyTransformer')

where my transformers look like:

class StudyTransformer extends BumblebeeTransformer {
  static get availableInclude () {
    return ['jobs', 'variables', 'participants']
  }

  transform (model) {
    return { ...model.$attributes }
  }

  includeJobs (study) {
    return this.collection(study.getRelated('jobs'), 'JobTransformer')
  }
  ...
}

class JobTransformer extends BumblebeeTransformer {
  static get defaultInclude () {
    return ['variables']
  }

  transform (model) {
    return { ...model.$attributes }
  }

  includeVariables (job) {
    return this.collection(job.getRelated('variables'), 'VariableTransformer')
  }
}

I know the problem probably lies in my usage of return {...model.$attributes} in the transform function, but shouldn't its usage be subjected to or honor the 'setVisible/Hidden' too?

eagerloadIncludedResource tries to load when not needed

Let's say that we have users who have avatar image relation in another media table.

User is not required to have avatar uploaded, so sometimes this relation can be NULL.

If you do the query:
const users = await User.query().with('avatar').fetch()
And then afterwards you transform it via transformer
transform.include('avatar').collection(users, UserTransformer)

Bumbleebee will inside TransformerAbstract eagerloadIncludedResource try to loadMany avatar for each user that has this realtion as NULL.

Suggestion:
eagerloadIncludedResource filter should change from:
return (data[resource] instanceof Function) && !data.getRelated(resource)
to:
return (data[resource] instanceof Function) && !data.getRelated(resource) && data.$relations[resource] !== null

add before/after hooks

Currently on a project that I'm working on, we have implemented our custom transformers for data and we have logic to do something before transformation of each resource begin.

We use this to populate likes data for each home (user can like homes on our site). Before hook was made so we query likes table only once even if we are returning list of 20 homes per page...

This is part of our implemntation showing how we reuse one query data to populate a list of homes with user likes... So only two queries will happen. One that queries 20 homes, and one that queries likes of users for ids of homes in that 20 list.

class HomePopulator extends BasePopulator {

    async beforeHook(model, ctx, meta) {
        const currentUser = ctx.getUserId()
        // here we fetch all like metadata, using whereIn query (**thanks to before hook**)
        meta._likes = await HomeLike.query()
            .select(['home_id', 'type']) // we have likes, and type of like (happy smile, angry smile, etc...)
            .count('type as count')
            .whereIn('home_id', model.rows ? model.rows.map(o => o.id) : model.id)
            .groupBy(['home_id', 'type'])
        meta._likes = _.mapValues(_.groupBy(meta._likes, 'home_id'), value => _.map(value, i => _.pick(i, ['type', 'count'])))

        // also we are fetching data if currently logged in user already liked something from the list...
        meta._myLike = currentUser && await HomeLike.query()
            .whereIn('home_id', model.rows ? model.rows.map(o => o.id) : model.id)
            .where('user_id', currentUser)
        meta._myLike = _.keyBy(meta._myLike, 'home_id')

        return model
    }

     // this is almost identical logic as bumblebee transform...
    async populate(model, ctx, meta) {

        let payload = {
            id: model.id,
            slug: model.slug,
            title: model.title,
            // .... other key/values
        }

        // but here we can access before hook data, and connect data without queries...
        payload.likes = meta._likes[model.id]
        payload.myLike = meta._myLike[model.id] && meta._myLike[model.id].type

        return payload
    }
}

Suggestion: try to reuse .getRelated instead of always fetching on include

This is solvable using "smarter" code inside incude methods, but imho this package should try and handle this out of the box.

So what's the issue?

Let's take Transformer from example in README.md:

class BookTransformer extends TransformerAbstract {
  defaultInclude () {
    return [
      'author'
    ]
  }

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

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

module.exports = BookTransformer

If someone is going to show 10 books, including authors, he will probably do it something like this:

    async index({transform}) {
        const books = await Book.all()

        return transform.include('author').collection(books)
    }

This will do 11 queries... :(

To optimize this a bit, one can do something like:

    async index({transform}) {
        const books = await Book.query().with('author').fetch()

        return transform.include('author').collection(books)
    }

Now, Lucid will select all books with authors in 2 queries... and transformer needs to be updated a bit:

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

So by using .getRelated('author') instead of .author().fetch(), we lowered amount of queries by 9!

For now, I'm using include methods like this:

    includeAuthor(book) {
        const author = book.getRelated('author') || book.author().fetch()

        if(author instanceof Promise) Logger.warning('Lazy load detected!')

        return this.item(author, AuthorTransformer)
    }

It would be awesome if include could be a little bit smarter and try to reuse already queried relations if possible...

What do you think?

Includes don't work for many to many relations

Hi guys! Now I'm creating my first project with adonisJS and in fact i enjoy it at all, but there is a problem with bumblebee transformer and i could not find any solution for that. When I use "include" method in transformer for many to many relations - it doesn't work and there is no kind of error, it just ignores relations. I'm doing all straight by docs, "include" works for belongsTo relations and doesn't work for "pivot table" to get all related entities for transforming model. Can you help me and describe why it does't it work properly with many to many relation, how could you miss that moment (i don't believe you could, but it seems to me that it's a pure fact) and which solution for many to many relations in transformer i can get out from box? Thank you!

[Question] Accessing Manager from inside Transformer

Hi, just curious if there is a way to change serializer from inside the transformer.

Sometime when returning X-to-one relation it's better to change from DataSerializer into PlainSerializer (Resource.item()) since it will have another nesting variable inside the relations.

Already read the code, but can't find out myself. Thanks for the help.

A transformer must be a function or a class extending TransformerAbstract

Sometimes i see this error, but I do not understand why the error appears and disappears. Sometimes this is decided by a rebuild

Error: A transformer must be a function or a class extending TransformerAbstract
at Scope._getTransformerInstance (/app/node_modules/adonis-bumblebee/src/Bumblebee/Scope.js:204:11)
at Scope._fireTransformer (/app/node_modules/adonis-bumblebee/src/Bumblebee/Scope.js:121:38)
at Scope._executeResourceTransformers (/app/node_modules/adonis-bumblebee/src/Bumblebee/Scope.js:93:52)
at

An exception is thrown if an include function does not exist

If an include is listed as available or default, but the function does not exist, an exception is thrown.

this[includeName] is not a function

Instead a more readable exception should be throne like:

The include function "name" is not defined in the transformer "name"

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.