I have an use case right now where I need to autoload certain relationships and transform all fields to camelcase before sending them to the API consumer. I'm already autoloading the relationships using .addGlobalScope and it works fine, but I don't understand how to serialize my data as camelcase.
For example, when making an API call I get:
{
"id": 1,
"name": "John",
"last_name": "Doe",
"posts": [{
"id": 3,
"title": "Hey!",
"created_at": "..."
}]
}
When I want:
{
"id": 1,
"name": "John",
"lastName": "Doe",
"posts": [{
"id": 3,
"title": "Hey!",
"createdAt": "..."
}]
}
What is the most general way to achieve this result?
Rolling out a serializer is the only way do it. Lemme share some code for that.
'use strict'
const _ = use('lodash')
const VanillaSerializer = require('@adonisjs/lucid/src/Lucid/Serializers/Vanilla')
class JsonSerializer extends VanillaSerializer {
_getRowJSON (modelInstance) {
const json = _.transform(modelInstance.toObject(), (result, value, key) => {
result[_.kebabCase(key)] = value
return result
}, {})
this._attachRelations(modelInstance, json)
this._attachMeta(modelInstance, json)
return json
}
}
module.exports = JsonSerializer
Save it inside app/Models/Serializers/JsonSerializer.js
And use this as the serializer for your models.
class User extends Model {
static get Serializer () {
return use('App/Models/Serializers/JsonSerializer')
}
}
The result[_.kebabCase(key)] = value is the place where you do the actual transformation of keys to camel case
Closing since the answer was given and there's no answer from issue reporter.
@thetutlage your solution works when returning data to the client, but how do you do the same transformation on your model, for example:
const User = use('App/Models/User')
const user = await User.find(1)
// Would like to access fields like this:
user.userName
// Instead of:
user.user_name
Also, would be nice to do the inverse.
const user = new User()
user.userName = 'test' // Transform it to 'user_name'
await user.save()
We write everything with JS: mobile, desktop, web, server. Using snake case on properties is uncomfortable when everything else in our stack is camel case. So I believe what @walbertoibarra pointing out is a valid concern in many use cases.
How about naming fields as camelCase within SQL?
It is going to be much pain in transforming fields on fly before and after every SQL query for Lucid.
@thetutlage I think many things in Lucid kind of expect snake case structure to work out of the box. I don't know how much of a pain in the ass it would be to use everything in camel case inside SQL.
I don't think Lucid should enforce casing for SQL. If there are hardcoded strings like that, I am happy to fix them
@thetutlage the thing is there is a problem using camelcase in column due to some operating system case sensitivity, you can look it up on the net
Yes, then you should rely on snake_case itself. I don't see any point of adding layers of converting keys to different cases before and after saving them to the DB.
Every weird layer has it's own complexity and I want to stay away from it.
@thetutlage I think my question is relevant to this thread:
It's not even about converting different cases, but different names - Is there equivalent to columnName of Sails.Js attributes in Adonis/Lucid?
I couldn't find a proper workaround for this issue:
I can use computed properties as an alias for the field (getter) + hook beforeCreate/beforeUpdate for a setter (I haven't tried it yet, so I'm not sure it will work)
As I wrote, this is not a proper workaround and make it too complicated for maintenance.
Thanks!
@gweizi I think @thetutlage's solution is the most appropiate. Just name your fields with camelCase. I haven't tried it but it should work. The only thing that may require some extra work may be relationships as they expect a particular structure for foreign key fields by default, but you can provide your own.
@sebasgarcep Probably I didn't explain my issue that well.
I give an example to make it more clear. Assume I have an exist database with a table called foo, this table has a column called "bad_name_666". I would like to create a model with a field called good_name that refer to the db column "bad_name_666". Is there any option to do that? or I must change the db scheme (in this case the column name bad_name_666 to good_name)?
Thanks again!
No there is no way to do that, since it adds a complexity layer on top of your database schema and also I have no plans to introduce anything like this.
The attributes should be 100% same as the database schema, since it's easier to reason about them
Objection.js, which is built top on knex.js, has a tranform fields function. Adonis Lucid is also built on knex.js, so you can use this feature from objection.js
npm i objection
config/database.jsconst { knexSnakeCaseMappers } = require("objection");
pg: {
client: 'pg',
connection: {
host: Env.get('DB_HOST', 'localhost'),
port: Env.get('DB_PORT', ''),
user: Env.get('DB_USER', 'root'),
password: Env.get('DB_PASSWORD', ''),
database: Env.get('DB_DATABASE', 'adonis')
},
debug: Env.get('DB_DEBUG', false)
}
To this
pg: ({
...{
client: 'pg',
connection: {
host: Env.get('DB_HOST', 'localhost'),
port: Env.get('DB_PORT', ''),
user: Env.get('DB_USER', 'root'),
password: Env.get('DB_PASSWORD', ''),
database: Env.get('DB_DATABASE', 'adonis')
},
debug: Env.get('DB_DEBUG', false)
},
...knexSnakeCaseMappers()
})
That's all, now you can use .where('createdAt', ...) instead of .where('created_at', ...) and SomeModel.createdAt instead of SomeModel.created_at. This conversion also works for SomeModel.toJSON()
Thanks @xxxwww , great solution ~
I was originally using @xxxwww's solution, but found it causes Lucid's child population to fail silently. I haven't delved into it, but my guess is it is not assigning the ids correctly due to the property name mismatch.
let user = await User.query().where('email', email).with('role').fetch()
and
let user = await User.find(1)
user.load('role')
both fail silently, leaving role null. I've had to revert back to a custom serializer.
@skrenek looks like the conversion works for the foreign key in relationships too. Therefore, you need to explicitly specify the key if it is stored in a database in a format other than camelCase
Can you elaborate on what you mean by specifying the key? I thought the point of the workaround was to allow db columns to be specified in snake case and properties in code in camel case.
Any update on this guys? Since my whole table's name are in camelCase.
Please help with answer how can i change the default behavior of table.timestamps() which create table name in snack_case. I want it to be in camelCase.
@xxxwww per @skrenek could you elaborate on what you meant by specifying the key?
there is a library for knex.js that does exactly this conversion of snake_case to camelCase.
https://github.com/Kequc/knex-stringcase
you can use it to wrap your database config in config/database.js
for example,
const knextStringcase = require('knex-stringcase')
// .....
sqlite: knexStringcase({
client: 'sqlite3',
connection: {
filename: Helpers.databasePath(`${Env.get('DB_DATABASE', 'development')}.sqlite`)
},
useNullAsDefault: true,
debug: Env.get('DB_DEBUG', false)
}),
// ... or wrap it for other driver
@xxxwww's fix works lika a charm except when you return a newly created record. Adonis saves a DB query by just appending created_at and updated_at in snake_case to the record when created which means it will not go through the knexSnakeCaseMapper.
This will then give you this when returning a created record:
// Creating record and returning
const user = new User()
user.firstName = 'Foo'
await user.save()
return response.json({ user })
```js
// Returned response
{
"id": 1,
"firstName": "Foo",
"created_at": "
"updated_at": "
}
and this when retrieving existing record:
```js
// Finding existing record and returning
const user = await User.find(1)
return response.json({ user })
// Returned response
{
"id": 1,
"firstName": "Foo",
"createdAt": "<timestamp>", // <-- camelCase
"updatedAt": "<timestamp>", // <-- camelCase
}
knexSnakeCaseMappers
This solution makes Adonis unable to load relations.
@e200 I am going to have to look into that. I have been unable to load relations (probably since adding this) and couldn't figure out why for the life of me. Also, I have been looking for a solution to accomplish this outside of models / serializers. Even with knexSnakeCaseMappers, it doesn't seem to convert snake_case to camelCase in the instance of running raw sql statements via Database.raw(). Is there any way to intercept responses in AdonisJS and run that logic there?
The missing link (for me at least, and maybe some other tortured soul who stumbles upon this issue) is in your model files. You can successfully use knexSnakCaseMappers, but you have to make sure that in your Lucid model files, you define your relationships in camelCase. By default, Lucid will assume those arguments by naming convention and I believe that assumption is why relationships were failing to load when using knexSnakeCaseMappers
@xxxwww I have a problem with knexSnakCaseMappers with ralations in adonis.
For exaple:
We have 3 models: users, roles and user_role;
in user model we have method:
roles(){
return this.belongsToMany(
'App/Models/Role',
'userId',
'roleId',
).pivotTable('user_role');
}
Two way to get data:
1) Works
const user = await User.find(1)
user.load('roles')
2) Not works. Resuls is empty array of roles
conts user = await User.query().with('roles').fetch()
@amakarenko-sumy I am not positive, but try to camelCase your pivotTable 'userRole' instead of 'user_role'
This thread has been automatically locked since there has not been any recent activity after it was closed. Please open a new issue for related bugs.
Most helpful comment
Objection.js, which is built top on knex.js, has a tranform fields function. Adonis Lucid is also built on knex.js, so you can use this feature from objection.js
config/database.jsTo this
That's all, now you can use
.where('createdAt', ...)instead of.where('created_at', ...)andSomeModel.createdAtinstead ofSomeModel.created_at. This conversion also works forSomeModel.toJSON()