Releases: mdwheele/yorm
v0.4.1
Experimental API to support implicit transactions
YORM now has basic support for transactions!
I've made this release under an "Experimental" flag because I'm not entirely thrilled with the API right now. With that said, we'll likely always support this more verbose / chatty API because why not. Take a look at how a basic transaction works:
const { transaction } = require('yorm')
/**
* Use `transaction` to start a transaction. Pass in each Model
* class you want to be available to participate in the transaction.
*
* The last argument is always a callback that receives Model classes
* (in the order you provided them) that have been bound to the
* transaction. In this way, the Model classes are able to participate
* in the transaction within the scope of the provided callback.
*/
await transaction(User, Post, async (User, Post) => {
// User inside the closure is not the same as User outside
// the closure. The inside User has the Knex transaction bound.
const user = await User.create()
await Post.create({ user_id: user.id, title: 'Created if the User is successfully created.' })
// Any exception / error thrown inside the closure will rollback. Otherwise,
// the transaction is implicitly committed.
})
I don't really like that we have to pass in each participating Model class, but maybe it'll grow on me. There is something to be said for being explicit and honestly, it's not bad until you start seeing 5-6 collaborators in the same transaction. Truth be told, I think that I would consider it a smell if I did have 6+ models mutating within a single transaction so maybe it's for the best that this pain becomes visual.
Anywho, this is what we were shooting for, but didn't arrive at:
import { transaction } from 'yorm.js'
transaction(async () => {
const user = await User.create()
await Post.create({ user_id: user.id, title: 'Created if the User is successfully created.' })
})
import { beginTransaction, commit, rollback } from 'yorm.js'
beginTransaction()
try {
const user = await User.create()
await Post.create({ user_id: user.id, title: 'Created if the User is successfully created.' })
commit()
} catch (error) {
rollback()
}
We'll keep working on it, but in the meantime give it a whirl!
0.3.1
Convenience methods, soft deletes, and hiding sensitive data from JSON
This release adds several convenience methods to Model
for retrieving model instances:
Model.firstOrCreate({ ... })
finds the first record with matching attributes. If no record matches the attributes given, one is inserted and the model instance is returned.Model.findOrFail(id)
retrieves a model instance by primary key. If the record does not exist, it throws anError
.Model.all()
returns all instances.
Additionally, YORM now supports "soft deletes", which are a recoverable form of deletion that uses a deleted_at
field to track when a model instance is deleted. If that field is set, YORM considers the field "deleted".
class User extends Model {
id
deleted_at
get softDeletes() { return true }
}
const user = await User.create()
await user.delete() // UPDATE users SET deleted_at = NOW() WHERE id = 1
We also provide a way of restoring soft-deleted model instances:
// If you already have a model instance, just call...
await user.restore()
// Typically, this won't be the case. For situations where you don't, use the static version:
await User.restore(query => query.where({ id: 1 }))
Lastly, we added a feature to "hide" select properties from the "toJSON
" representation of an object. This can be useful when you're passing your models directly when making API responses (e.g. res.json(...)
in Express).
If you define a hidden
accessor on your model that returns an array of field names, they will automatically be omitted from JSON output.
class User extends Model {
id
username
password
get hidden() {
return ['password']
}
}
const user = User.make({ username: 'user', password: 'super.secret' })
JSON.stringify(user) // { "username": "user" }
You could always do this manually by overriding the model's toJSON
method. For one-offs, this feature doesn't really add much, but it is a more declarative way of expressing sensitivity that's harder to overlook later down the road.
Bring Your Own ID
You can now configure your own strategy for how model identifiers are generated. By default, we delegate to your DBMS of choice to assign a unique identifier. However, there are times when you want to use UUID, ULID, or other identifier strategies that might not be supported by your flavor of SQL.
Let's take a look at an example where we have our own custom libraries for ID generation that have been used for years.
import { generateId } from '../lib/util.js'
class Legacy extends Model {
// First, we have a custom primary key name
identity
// So we have to tell YORM about that
get primaryKey() { return 'identity' }
// "Somebody" wrote a utility to generate totally
// secure IDs like 12 years ago and we're stuck using them.
// No worries, just set up the `newUniqueId` accessor!
get newUniqueId() {
return generateId()
}
}
// Every time an instance is created, it will automatically
// generate an ID following our "legacy" strategy
Legacy.make().identity // '0-384-388385-A'
If you want to use UUID, ULID, or nanoid identifiers, return 'uuid'
, 'ulid'
, or 'nanoid'
, respectively.
class ULID extends Model {
id
get newUniqueId() {
return 'ulid' // '01F7DKCVCVDZN1Z5Q4FWANHHCC'
}
}
As usual, if you provide an ID during the construction of an instance within a .make(...)
or .create(...)
call, that will override any sort of generation that occurs.
Yet Another ORM Beginning
Hello! 🎉
Thanks for checking out YORM (Yet Another ORM)!
I know, I know... you're asking "Did the world really need yet... another... ORM?"
Well, I think so. I've been working with Node.js for quite a while now on several code bases ranging from "legacy" (_ewwww!_🐷) to "modern" and I've tried several of the popular ORM and... I have to say... it feels like a Node.js developer wrote every single one.
Regarding convention over configuration, I've waffled both sides throughout my career. Once a huge fan of Ruby on Rails and Laravel, I was annoyed at times with all the "magic". The code was expressive but almost terse to the point of not being able to understand. So I swung the other way towards configuration and I found myself spending more time writing JSON, XML, and YAML than actually solving problems.
The neat thing is, that everyone has taste. So when it comes to Sequelize, Objection.js, TypeORM, Bookshelf, and the 1,000 other JavaScript ORM libraries... I say use what you want. For me, none of these packages really tasted "good". Or maybe another way of putting it is that the initial taste was great; I felt really productive. But then I started to get the after-taste and simple things became tedious and I didn't fully understand what the tooling was doing.
So that brings us back to YORM. My goal with YORM was to write an Active Record implementation that stayed out of the way and allowed me to write more-or-less Plain ol' JavaScript Objects (POJO). I wanted vanilla ES6 classes with just a touch of magic helpers to facilitate basic CRUD operations and relationships between models. Also, I didn't want to have to write a book every time I set up a model. It should be quick and simple to put together a new model.
Anywho, checkout the README.md and let me know what you think in the Issues. I'm very open to feature ideas and help as long as we maintain a healthy developer experience without a bunch of boilerplate! Writing code should be fun; not a tedious endeavor.