rhwilr / adonis-bumblebee Goto Github PK
View Code? Open in Web Editor NEWApi Transformer for AdonisJs Framework
License: MIT License
Api Transformer for AdonisJs Framework
License: MIT License
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.
I think is a good idea, to introduce a class generator for the Adonis CLI make
command.
Hello :)
I have 3 tables :
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"
class User extends Model {
skills() {
return this.belongsToMany('App/Models/Skill')
.pivotTable('user_skill')
.withPivot(['rank'])
}
}
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)
}
}
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
}))
}
Hi. First, thank you for such a nice library for transforming our data.
Do you think you can create another default serializer for JSONAPI compatible format? It would be nice if we can use: https://github.com/SeyZ/jsonapi-serializer as one of the adonijs compatible serializer. :)
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 .... } ....
...
}
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'} ...]
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
}
}
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
Is v5 planned for this package or should I look for other solutions?
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)
}
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
Auto parse includes in get request params is not working like docs intendeed to.
src/Bumblebee/Manager.js -> _setIncludesFromRequest is using ctx.params instead of ctx.request.get()
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"
Column is not required value and can exist with null value.
when use transform.include("relation") error : E_UNSAVED_MODEL_INSTANCE: Cannot process relation
but add with("relation") to query resolved it.
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
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
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!
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
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 :)
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?
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?
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)
}
}
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.
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.
I want to use the transform context in the websocket controller but it returns null. Kindly help
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!
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!
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.
A declarative, efficient, and flexible JavaScript library for building user interfaces.
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google ❤️ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.