# Leoric Documentation (Full)
> Concatenated from the guides under docs/, for AI agents to load the full context in one shot.
## Starter
### Getting Started
This guide try to illustrate the config and usage about Leoric with an abstract project about photography which is called **Portra**. Let's assume this project uses MySQL database, is based on the Egg framework, which features photo management, backup, and sharing.
#### Configuring Database
Leoric supports MySQL, SQLite, and PostgreSQL. It is able to run in both Node.js and Electron, which makes it perfectly suitable for Portra. In following configuration, we decide to put the models in `app/models` directory, with the database set to `portra`:
```js
const Realm = require('leoric');
const realm = new Realm({
host: 'localhost',
user: 'portra',
database: 'portra',
models: 'app/models',
migrations: 'database/migrations',
});
await realm.connect();
```
For detailed configuration options about different databases, please take a look about the [Setup]({{ '/zh/setup' | relative_url }}) documentation.
#### Model Basics
If the table schemas aren't managed with Leoric, we can omit the attributes definition in models and let Leoric load them from `information_schema.columns` automatically. Take `app/model/user.js` for example, it can be defined as:
```js
const { Bone } = require('leoric');
module.exports = class User extends Bone {
static initialize() {
this.hasMany('books');
this.hasMany('comments');
}
}
```
After database is connected with `await realm.connect()`, `User` model will be loaded with `User.attributes`. Then we can create, find, update, or delete records in `users` table:
```js
// create user
await User.create({ name: 'Stranger' });
// find the user just created
const user = await User.first;
assert.equal(user.name, 'Stranger');
// change the name of the user
await user.update({ name: 'Tyrael' });
// remove user
await user.remove();
```
For more information about model attributes and `information_schema.columns`, or the methods that deal with record manipulation, it is recommend to start with the [Basics]({{ '/zh/basics' | relative_url }}) introduction.
#### Creating Models with Migrations
### Migrating from Sequelize
Projects that consider migrating from Sequelize to Leoric, may try the Sequelize adapter to mitigate the migration work. With the Sequelize adapter activated, Leoric will inject an extra layer above Bone to provide compatible APIs. Please take a look about [Sequelize Adapter]({{ '/zh/sequelize' | relative_url }}) for detail.
## Basics
This guide is an introduction to Leoric. After reading this guide, you will know:
- What Object Relational Mapping (roughly) and Leoric are, and how they are used.
- How to use Leoric models to manipulate data stored in a relational database.
- Leoric schema naming conventions.
### What is Leoric
Leoric is a thin Object Relational Mapping layer between Node.js and database. It can be used as the M in [MVC](https://en.wikipedia.org/wiki/Model%E2%80%93view%E2%80%93controller) - which is the layer of the system responsible for representing business data and logic.
Object Relational Mapping, or ORM, is a way of connecting rich objects of an application to tables in a relational database management system. The idea of ORM is quite popular in a lot of the programming languages, such as Active Record for Ruby, SQLAlchemy for Python, or Hibernate for Java. Leoric is heavily influenced by [Active Record](http://guides.rubyonrails.org/active_record_basics.html) which you probably can tell already by the similar documentation structure.
As one of the many ORM libraries for JavaScript, the most promising features of Leoric shall be the abilities to:
- Represent models and their data.
- Represent associations between models.
- Map existing tables without repeating column definitions in model.
- CRUD (create, read, update, delete) in asynchronous fashion.
- Create and consume models in morden JavaScript.
### Convention over Configuration
Generally speaking, configuration is preferred over convention because of its explicitness. However, if you follow the conventions introduced by Leoric, you'll need to write few configuration when authoring models.
#### Naming Conventions
By default, Leoric uses some naming conventions to find out how the mapping between models and database tables should be created. Here're the rules:
- model names shall be in `CamelCase`, the table names will be the pluralized model name in `snake_case`,
- model attributes shall be in `camelCase`. The attribute names are fetched and transformed from the schema information, which would be in `snake_case` mostly.
Here's a few transform examples.
| Model/Class | Table/Schema |
|-------------|--------------|
| Shop | shops |
| TagMap | tagMaps |
| Mouse | mice |
| Person | people |
Under the hood, Leoric uses [pluralize](https://www.npmjs.com/package/pluralize) to transform table names from model names. If you find the transform rules counter intuitive (which is very common for non-native speakers), you can explicitly configure the table name or rename the attribute. We'll cover that in the *Overrding the Naming Conventions* section.
#### Schema Conventions
Leoric provides three static methods for relationship authoring, `.hasMany()`, `.hasOne()`, and `.belongsTo()`. The conventional primary keys and foreign keys are as below,
- **Foreign keys** should be named following the pattern `modelNameId` (e.g. `shopId`). The corresponding columns are the keys in snake case `model_name_id` (e.g. `shop_id`).
- **Primary keys** should be an unsigned integer `id`.
There're some optional column names that will add additional features to Leoric:
| column | attribute | description |
|--------------|-------------|-----------------------------------------------|
| `created_at` | `createdAt` | updated when the record is first created. |
| `updated_at` | `updatedAt` | updated whenever the record is updated. |
| `deleted_at` | `deletedAt` | updated whenever the record is softly deleted |
> For TDDL users, the conventional `gmt_create` is mapped to `createdAt`, `gmt_modified` is mapped to `updatedAt`, and `gmt_deleted` (if present) is mapped to `deletedAt`.
When `Bone.remove({...})` is called on a Model with `deletedAt` present, Leoric will perform a soft delete by updating the value of `deletedAt` column instead of delete it from the database permanently. Call `Bone.remove({...}, true)` to force delete.
### Authoring Models
Suppose the `shops` table were created already:
```sql
CREATE TABLE shops (
id int(11) NOT NULL auto_increment,
name varchar(255),
PRIMARY KEY (id)
);
```
Simply extend from the `Bone` class exported by Leoric, have it connected to database, and you're all set:
```js
const { Bone } = require('leoric');
class Shop extends Bone {}
await connect({ host: 'localhost', models: [ Shop ]});
```
It is possible to manage schema with Leoric as well. By defining attributes when authring models, Leoric learns what the model needs and will have the schema migrated when necessary:
```js
const { Bone, Realm } = require('leoric');
const { BIGINT, STRING } = Bone.DataTypes;
// define Shop
class Shop extends Bone {
static attributes = {
id: { type: BIGINT, primaryKey: true },
name: STRING,
}
}
// connecting Shop to shops table in database
const realm = new Realm({ host: 'localhost', models: [ Shop ] });
// synchronize model attributes to table
await realm.sync();
```
Now we have got the models connected to and synchronized with database, we can start querying:
```js
const shop = new Shop({ name: 'Horadric Cube' })
await shop.save()
// or simply
await Shop.create({ name: 'Horadric Cube' })
```
### Overriding the Naming Conventions
Most of the conventional names and keys can be overridden by corresponding methods. You can specify `static table` to override the default table name:
```js
class Shop extends Bone {
static table = 'stores'
}
```
It's also possible to override the the name of the primary key by specifying `static primaryKey`:
```js
class Shop extends Bone {
static primaryKey = 'shopId'
}
```
We can rename the attribute names too. By default, these names are transformed from column names by converting them into camel case. If the names don't match, we can specify the column names manually in `static attributes`, such as:
```js
class Shop extends Bone {
static attributes = {
deletedAt: { type: DATE, columnName: 'removed_at' },
}
}
```
We can also rename the attribute in the `static initialize()` method, which gets called after models are loaded.
```js
class Shop extends Bone {
static initialize() {
this.renameAttribute('removedAt', 'deletedAt')
}
}
```
A lot of schema settings can be done within the `static initialize()` method. We'll get to that later. For TypeScript projects this static method is unnecessary, most of the settings can be tweaked with the equivalent decorators. The example above can be refactored with decorator like below:
```ts
class Shop extends Bone {
@Column({ name: 'removed_at' })
deltedAt: Date;
}
```
### Connecting Models to Database
Models need to be connected to database before use, which can be connected in the following way:
```js
const Realm = require('leoric');
const realm = new Realm({
dialect: 'mysql',
host: 'localhost',
models: '/path/to/models',
});
await realm.sync();
```
`realm.sync()` not only connects models to database, but also tries to synchronize `Model.attributes` in each model back to database structure automatically to make sure consistency between each other. If your application data changes a lot, this practice is not recommended.
In that case, please use the [migrations]({{ '/migrations' | relative_url }}) to change database structure instead.
```js
const Realm = require('leoric');
const realm = new Realm(...);
await realm.connect();
```
For those who started using Leoric since v0.x, we can still `connect()` to database directly.
```js
const { connect } = require('leoric');
await connect({
host: 'example.com',
port: 3306,
user: 'john',
password: 'inputYourCodeHere',
db: 'tmall',
models: [Shop]
});
// or
await connect({ ...opts, path: '/path/to/models' });
```
If developing web applications with Egg framework, it's highly recommended using the [egg-orm](https://github.com/eggjs/egg-orm) plugin.
### Reading and Writing Data
With the models defined and connected, developers can,
- query the model with static methods such as `Model.find()` and `Model.findOne()`,
- writing data with `Model.create()` and `Model.update()`,
- removing data with `Model.remove()`, and
- persisting instance changes with `model.save()` of course.
```js
async function() {
// create shop
await Shop.create({ name: 'Barracks' })
// find one and update it
const shop = await Shop.findOne({ name: 'Barracks' })
shop.name = 'Horadric Cube'
await shop.save()
// remove the shop
await Shop.remove({ name: 'Horadric Cube' })
})
```
#### Create
There are two ways in Leoric to INSERT records into database. We can do this either by calling `Model.create()` with one blow:
```js
const shop = await Shop.create({ name: 'Barracks', credit: 10000 })
```
or by instantiating a model from scratch, settings the attributes, the `model.save()` it at last:
```js
const shop = new Shop({ name: 'Barracks' })
shop.credit = 10000
await shop.save()
```
The SQL equivalent of both is:
```sql
INSERT INTO shops (name, credit, type) VALUES ('Barracks', 10000);
```
#### Read
Although Leoric provides a rich API for starting a query, `Model.find()` and `Model.findOne()` are the most used methods.
```js
// find all of the shops
Shop.find()
// => SELECT * FROM shops;
// find the first one
Shop.findOne()
// => SELECT * FROM shops LIMIT 1;
// find the shop of Deckard Cain
Shop.findOne({ name: 'Deckard Cain' })
// => SELECT * FROM shops WHERE name = 'Deckard Cain' LIMIT 1;
// find a collection of shops with their credit above 1000
Shop.where('credit > 1000')
// => SELECT * FROM shops WHERE credit > 1000;
```
For detailed introductions about reading data from the database, please read [Query Interface]({{ '/querying' | relative_url }})
#### Update
Like the way records are created, records can be updated in two manners too. If the objects are already at hand, we can fiddle their attributes and persist the updates by calling `model.save()`:
```js
const shop = await Shop.findOne({ name: 'Barracks' })
// => Shop { id: 1, name: 'Barracks' }
shop.credit = 10000
await shop.save()
```
The SQL equivalent of the above is:
```sql
UPDATE shops SET credit = 10000 WHERE id = 1;
```
If the back and forth traffic needs to be skipped, we can also update the records with one blow using `Model.update()`:
```js
await Shop.update({ name: 'Barracks' }, { credit: 10000 })
```
The SQL equivalent of the above is:
```sql
UPDATE shops SET credit = 10000 WHERE name = 'Barracks';
```
#### Delete
Likewise, both `model.remove()` and `Model.remove()` are available to delete records from database. For example:
```js
const shop = await Shop.find({ name: 'Barracks' })
// => Shop { id: 1, name: 'Barracks' }
await shop.remove(true)
// DELETE FROM shops WHERE id = 1
await Shop.remove({ name: 'Barracks' }, true)
// DELETE FROM shops WHERE name = 'Barracks'
```
What's with the parameter `true` you might ask. That is because by default Leoric performs a soft delete instead of truly DELETE FROM the database. To make soft delete possible, the model must have a attribute called `deletedAt` to be used as a mark of deletion.
Therefore, if `deletedAt` were present in `Shop` model:
```js
const shop = await Shop.find({ name: 'Barracks' })
// => Shop { id: 1, name: 'Barracks' }
await shop.remove()
// UPDATE shops SET deleted_at = NOW() WHERE id = 1
await Shop.remove({ name: 'Barracks' })
// UPDATE shops SET deleted_at = NOW() WHERE name = 'Barracks'
```
If `deletedAt` were absent in `Shop` model, calling either `model.remove()` or `Model.remove()` without passing `true` throws an Error.
## Migrations
Developers can use migrations to manage operations that changes table schema etc.
### What Is A Migration
Take following migration for example:
```js
module.exports = {
async up(driver, DataTypes) {
const { BIGINT, STRING, TEXT } = DataTypes;
await driver.createTable('products', {
id: { type: BIGINT, primaryKey: true },
name: STRING,
description: TEXT,
});
},
async down(driver, DataTypes) {
await driver.dropTable('products');
},
}
```
By executing this migration, the `up()` part, we create a table called `products` that consists of three columns, primary key `id`, a `name VARCHAR(255)` column to store product name, and a `description TEXT` to store descriptive information of the product. A migration consists of two methods, `up()` and `down()`, which make sure the task is able to migrate or rollback.
In this migration, the rollback operation is to drop the `products` table, which makes sure the change brought by `up()` is correctly reverted in `down()`.
Migrations can not only be used to change schema, but also can be used to migrate existing data. For example if we're trying to add a new column which has default value different than existing data, we can do it like below:
```js
module.exports = {
async up(driver, DataTypes) {
await driver.addColumn('users', {
wants_marketing_email: { type: BOOLEAN, default: false },
});
await driver.query('UPDATE users SET wants_marketing_email = 1');
},
async down(driver, DataTypes) {
await driver.removeColumn('users', 'wants_marketing_email');
},
}
```
The migration above adds a new column called `users.wants_marketing_email` which defaults to `false`, but we want existing users remain subscribed to our marketing emails.
#### Creating Migration File
We can use `Realm#createMigrationFile(name)` to create migration file:
```js
const Realm = require('leoric');
const realm = new Realm({
client: 'mysql',
migrations: 'database/migrations',
});
await realm.createMigrationFile('create-products');
// which creates migration file like 20210621170235-create-products.js in database/migrations
```
The generated migration file looks like below:
```js
'use strict';
module.exports = {
async up(driver, DataTypes) {
// TODO
},
async down(driver, DataTypes) {
// TODO
}
};
```
#### Naming Conventions
As described above, `realm.createMigrationFile(name)` creates migration file like `20210621170235-create-products.js`. The prefix is the timestamp of the file when it's created, which is formatted as `YYYYMMDDHHMMSS`. The rest part of the file name is the `name` passed to the method. Therefore the full format of the migration file is `YYYYMMDDHHMMSS-${name}`.
Migrations that are executed will be stored in `leoric_meta` table to track the progress. If the table `leoric_meta` does not exist when migrating, it will be created automatically.
If the migration is reverted with `realm.rollback()`, the corresponding record will be removed from `leoric_meta`.
#### Changing Migration
We'd recommend not to change migration back and forth, especially if the migration were committed into version control system, which might have its older version executed in other developer's database. In that case, it is highly unlikely to make it right for everyone involed.
Creating new migration to correct previous mistakes is recommended.
#### Data Types Supported
Following data types are supported:
```js
STRING
INTEGER
BIGINT
DATE
BOOLEAN
TEXT
BLOB
JSON
JSONB
```
These data types will be mapped to corresponding type in database. For example, in MySQL `STRING` is mapped to `VARCHAR(255)`.
### Writing a Migration
#### Creating a Table
```js
module.exports = {
async up(driver, DataTypes) {
const { STRING, BIGINT, INTEGER } = DataTypes;
await driver.createTable('products', {
id: { type: BIGINT, primary: true },
category_id: { type: BIGINT },
name: STRING,
price: INTEGER,
});
},
};
```
The code above is equivalent to the SQL below:
```sql
CREATE TABLE `products` (
`id` BIGINT PRIMARY KEY,
`category_id` BIGINT,
`name` VARCHAR(255),
`price` INT,
);
```
#### Adding Columns
```js
module.exports = {
async up(driver, DataTypes) {
await driver.addColumn('products', 'volume', {
type: DataTypes.INTEGER,
defaultValue: 0,
});
},
}
```
The code above is equivalent to the SQL below:
```sql
ALTER TABLE `products` ADD COLUMN `volume` INTEGER;
```
#### Changing Columns
```js
module.exports = {
async up(driver, DataTypes) {
await driver.changeColumn('products', 'volume', {
type: DataTypes.INTEGER.UNSIGNED,
defaultValue: 0,
});
},
}
```
The code above is equivalent to the SQL below:
```sql
ALTER TABLE `products` ADD COLUMN `volume` INTEGER UNSIGNED;
```
#### Renaming Column
```js
module.exports = {
async up(driver, DataTypes) {
await driver.renameColumn('products', 'volume', 'stock');
},
};
```
The code above is equivalent to the SQL below (which is not quite the same in older versions of MySQL):
```sql
ALTER TABLE `products` RENAME COLUMN `volume` TO `stock`;
```
#### Removing Columns
|---|-------------------------|
| ⚠️ | PLEASE BACK UP AT FIRST |
```js
module.exports = {
async up(driver, DataTypes) {
await driver.removeColumn('products', 'stock');
},
};
```
The code above is equivalent to the SQL below:
```sql
ALTER TABLE `products` DROP COLUMN `stock`;
```
#### Creating Indices
```js
module.exports = {
async up(driver, DataTypes) {
await driver.addIndex('products', [ 'category_id', 'price' ]);
},
};
```
The code above is equivalent to the SQL below:
```sql
CREATE INDEX `idx_products_category_id_price` ON `products` (`category_id`, `price`);
```
#### Removing Indices
```js
module.exports = {
async up(driver, DataTypes) {
await driver.removeIndex('products', [ 'category_id', 'price' ]);
},
};
```
The code above is equivalent to the SQL below:
```sql
DROP INDEX `idx_products_category_id_price`;
```
#### Truncating Tables
|---|-------------------------|
| ⚠️ | PLEASE BACK UP AT FIRST |
```js
module.exports = {
async up(driver, DataTypes) {
await driver.truncateTable('products');
}
};
```
The code above is equivalent to the SQL below:
```sql
TRUNCATE TABLE `products`;
```
#### Dropping Tables
|---|-------------------------|
| ⚠️ | PLEASE BACK UP AT FIRST |
```js
module.exports = {
async down(driver, DataTypes) {
await driver.dropTable('products');
},
};
```
The code above is equivalent to the SQL below:
```sql
DROP TABLE `table_name`;
```
#### Using the `up`/`down` Methods
Migrations should provide both `up` and `down` methods. The former one is used to perform the intended change to schema, and the latter one is used to revert the change brought by `up`. The default content of the newly created migration is like below:
```js
'use strict';
module.exports = {
async up(driver, DataTypes) {
},
async down(driver, DataTypes) {
},
};
```
It is strongly recommended that make sure the changes will be properly reverted in `down`, nothing that might interfere other migrations, such as redundant columns or tables, should be left behind.
### Running Migrations
```js
const Realm = require('leoric');
const realm = new Realm();
await realm.migrate();
```
All of the migrations that are not executed yet will be loaded and executed accordingly. For example, if we have following migrations to execute:
```
// database/migrations
20210622130000-create-products.js
20210623150000-add-product-price.js
20210623160000-create-recipients.js
```
Then when we call `realm.migrate()`, the `up()` methods in `create-products`, `add-product-price`, and `create-recipients` get called one after another.
The name of the performed migrations are stored in `leoric_meta`:
```bash
mysql> select * from leoric_meta;
+------------------------------------------------------------------------+
| name |
+------------------------------------------------------------------------+
| 20210622130000-create-products.js |
| 20210623150000-add-product-price.js |
| 20210623160000-create-recipients.js |
+------------------------------------------------------------------------+
```
#### Rolling Back
```js
const Realm = require('leoric');
const realm = new Realm();
// one step backward
await realm.rollback()
// three steps backward
await realm.rollback(3);
```
`realm.rollback()` will query executed migrations from `leoric_meta`, and then execute the `down()` method accordingly.
#### Resetting the Database
To reset the database, rollback all executed migrations and then re-run them from scratch. Currently Leoric does not provide a dedicated `reset()` method, but this can be achieved by combining `rollback()` and `migrate()`:
```js
const Realm = require('leoric');
const realm = new Realm({
client: 'mysql',
migrations: 'database/migrations',
});
// rollback all migrations (use a large number to cover all)
await realm.rollback(Infinity);
// re-run all migrations
await realm.migrate();
```
> **Warning**: This will destroy all existing data. Always back up your database before resetting.
#### Running Specific Migration
You can control how many pending migrations to run by passing a `steps` parameter to `realm.migrate()`:
```js
// run only the next pending migration
await realm.migrate(1);
// run the next 3 pending migrations
await realm.migrate(3);
```
Similarly, `realm.rollback()` accepts a step count:
```js
// rollback the last migration
await realm.rollback();
// rollback the last 3 migrations
await realm.rollback(3);
```
> **Note**: There is currently no built-in way to run a specific migration by name. Migrations are always executed in chronological order based on their filename timestamps.
### Using Models in Migrations
In some cases, you may need to use models within migrations to manipulate data. To do this, require the model and use raw queries or model methods. Keep in mind that the model must already be connected:
```js
module.exports = {
async up(driver, DataTypes) {
// Add the new column first
await driver.addColumn('users', 'display_name', {
type: DataTypes.STRING,
});
// Use raw SQL to populate the new column from existing data
await driver.query(`
UPDATE users SET display_name = CONCAT(first_name, ' ', last_name)
`);
},
async down(driver, DataTypes) {
await driver.removeColumn('users', 'display_name');
},
};
```
If you need to use the model API instead of raw SQL, you can set up a Realm instance within the migration. However, this is generally discouraged because model definitions may change over time and become out of sync with the migration:
```js
const Realm = require('leoric');
const User = require('../../app/models/user');
module.exports = {
async up(driver, DataTypes) {
await driver.addColumn('users', 'display_name', {
type: DataTypes.STRING,
});
// Use model API (ensure connect is called)
const realm = new Realm({
client: 'mysql',
database: 'myapp',
models: [User],
});
await realm.connect();
const users = await User.find();
for (const user of users) {
await user.update({ displayName: `${user.firstName} ${user.lastName}` });
}
await realm.disconnect();
},
async down(driver, DataTypes) {
await driver.removeColumn('users', 'display_name');
},
};
```
> **Best practice**: Prefer raw SQL queries (`driver.query()`) over model APIs in migrations. Raw SQL is deterministic and won't break if the model definition changes later.
### Schema Dumping
|---|---------------------------|
| ⚠️ | NOT FULLY IMPLEMENTED YET |
When migration finishes executing, not matter `realm.migrate()` or `realm.rollback()`, Leoric will create a schema dump in the parent directory of `opts.migrations`. For example, if the path specified with `opts.migrations` is `database/migrations`, the file will be created at `database/schema.js`, which contains statements like below:
```js
module.exports = async function createSchema(driver, DataTypes) {
const { STRING, INTEGER, BIGINT, DATE } = DataTypes;
await driver.dropTable('products');
await driver.createTable('products', {
id: { type: BIGINT, primaryKey: true },
name: STRING,
price: INTEGER,
createdAt: DATE,
updatedAt: DATE,
});
// other tables
}
```
The schema dump contains only statements about the table structures. The data stored in those tables is not included.
## Validations
This article will introduce how to use leoric to constrain model's attributes and validate its' values.
### allowNull
The attribute definition of the model can set whether the attribute can be `null` or not. When the model is synchronized, the corresponding table's field attribute characteristics (`NOT NULL` or `NULL`) will be generated according to the value of `allowNull`. When the model instance attribute value does not match the setting, an error will be thrown.
```javascript
const { Bone, DataTypes } = require('leoric');
class User extends Bone {
static attributes = {
id: { type: DataTypes.BIGINT, primaryKey: true },
email: { type: DataTypes.STRING, allowNull: false },
...
}
};
User.sync();
/*
CREATE TABLE `users` (
`id` bigint(20) AUTO_INCREMENT PRIMARY KEY,
`email` varchar(256) NOT NULL,
....
);
*/
User.create({ name: 'OldHunter' }); // throw LeoricValidateError('notNull'); email should not be null
```
### unique
You can set a unique constraint on a field using 'unique':
```javascript
const { Bone, DataTypes } = require('leoric');
class User extends Bone {
static attributes = {
id: { type: DataTypes.BIGINT, primaryKey: true },
email: { type: DataTypes.STRING, allowNull: false, unique: true },
...
}
};
User.sync();
/*
CREATE TABLE `users` (
`id` bigint(20) AUTO_INCREMENT PRIMARY KEY,
`email` varchar(256) NOT NULL UNIQUE,
....
);
*/
```
### Built-in validator
In addition to validators included in [validator.js](https://github.com/validatorjs/validator.js) as built-in validators, leoric also provides the following built-in validators:
```javascript
class User extends Bone {
static attributes = {
var: {
type: ANYTYPE,
validate: {
notIn: [['MHW', 'Bloodborne']], // Not one of them
notNull: true, // Can't be NULL
isNull: true, // Must be NULL
min: 1988, // MinValue
max: 2077, // MaxValue
contains: 'Handsome', // Must contains 'Handsome'
notContains: 'Handsome', // Mustn't contain 'Handsome'
regex: /^iceborne/g, // Matching RegExp
notRegex: /^iceborne/g, // Not matching RegExp
is: /^iceborne/g, // Matching RegExp
notEmpty: true, // not allow empty string
}
}
}
}
```
#### Custom error message
The built-in validator supports customize error messages instead of the default error messages of leoric.
```javascript
class User extends Bone {
static attributes = {
var: {
type: ANYTYPE,
validate: {
isIn: {
args: [ 'MHW', 'Bloodborne' ], // 'args' are the arguments of validator
msg: 'OH! WHAT HAVE YOU DONE?!' // `msg` is custom error message
},
notNull: {
args: true,
msg: 'OH! WHAT HAVE YOU DONE?!'
}
}
}
}
}
```
### Custom validators
leoric supports set custom validators. You can throw an error or return `false` from the validator while the validation fails, and Leoric will take the next step based on the returns.
```javascript
class User extends Bone {
static attributes = {
desc: {
type: DataTypes.STRING,
validate: {
isValid() {
if (this.desc && this.desc.length < 2) { // you can access attribute's value by this
throw new Error('Invalid desc');
}
},
lengthMax(value) { // the first argument is the value of attribute
if (value && value.length >= 10) {
return false;
}
}
}
}
}
}
```
## Associations
This guide covers the association features of Leoric. After reading this guide, you will know:
- How to declare assocations between models.
- How to understand the various types of associations.
### Why Associations
With associations well defined, developers can pull structured data with a single query such as:
```js
const shop = await Shop.findOne({ id }).with('items', 'owner')
// => Shop { id: 1,
// name: 'Barracks',
// items: [ Item { name: "Wirt's Leg" }, ... ],
// owner: User { name: 'Tyreal' } }
```
### Types of Associations
Leoric supports four types of associations:
- `belongsTo()`
- `hasMany()`
- `hasMany({ through })`
- `hasOne()`
Associations can be declared within the `Model.describe()` method. For example, by declaring a shop `belongsTo()` its owner, you're telling Leoric that when `Shop.find().with('owner')`, Leoric should join the table of owners, load the data, and instantiate `shop.owner` on the found objects.
There are four equivalent decorators for projects written in TypeScript
- `@BelongsTo()`
- `@HasMany()`
- `@HasMany({ through })`
- `@HasOne()`
The major difference between static method and decorators for associations is that the first parameter can be omitted in the decorator equivalent. For example, `Post.belongsTo('user')` declared with decorator is like below:
```ts
class Post {
@BelongsTo()
user: User
}
```
#### `belongsTo()`
A `belongsTo()` association sets up a one-to-one or many-to-one relationship. For example, a shop can have many items as it finds fit. On the other hand, an item can `belongsTo()` to exactly one shop. We can declare the `Item` this way:
```js
class Item extends Bone {
static initialize() {
this.belongsTo('shop')
}
}
```
The TypeScript equivalent with decorator is like below:
```ts
class Item extends Bone {
@BelongsTo()
shop: Shop;
}
```
Leoric locates the model class `Shop` automatically by capitalizing `shop` as the model name. If that's not the case, we can specify the model name explicitly by passing `className`:
```js
class Item extends Bone {
static initialize() {
this.belongsTo('shop', { className: 'Seller' })
}
}
```
The TypeScript equivalent with decorator is like below:
```ts
class Item extends Bone {
@BelongsTo({ className: 'Seller' })
shop: Shop;
}
```
> Please be noted that the value passed to `className` is a string rather than the actual model class. Tossing the actual classes back and forth between the two parties of an association at `Model.describe()` phase can be error prone because it causes cyclic dependencies.
As you can tell from the ER diagram, the foreign key used to associate a `belongsTo()` relationship is located on the model that initiates it. The name of the foreign key is found by uncapitalizing the target model's name and then appending an `Id`. In this case, the foerign key is converted from `Shop` to `shopId`.
> Leoric has two sets of names maintained under the hood. One is the attribute names of the model, which usually are in camel case to be compliant with common JavaScript coding conventions. The other is the columns names of the actual table, which may be in camel case but usually are in snake case.
To override foreign key, we can specify it explicitly:
```js
class Item extends Bone {
static initialize() {
this.belongsTo('shop', { foreignKey: 'sellerId' })
}
}
```
The TypeScript equivalent with decorator is like below:
```ts
class Item extends Bone {
@BelongsTo({ foreignKey: 'sellerId' })
shop: Shop;
}
```
#### `hasMany()`
If you look this ER diagram from the shops point of view, you may notice that there is a `hasMany()` association too. The shop `hasMany()` items:
```js
class Shop extends Bone {
static initialize() {
this.hasMany('items')
}
}
```
The TypeScript equivalent with decorator is like below:
```ts
class Shop extends Bone {
@HasMany()
items: Item[];
}
```
> Please be noted that unlike `belongsTo()`, the name passed to `hasMany()` is usually in plural.
The way Leoric locates the actual model class is quite similar. It starts with singularizing the name, then capitalizing. In this case, `items` get singularized to `item`, and then `Item` is used to look for the actual model class.
To override the model name, we can specify it explicitly:
```js
class Shop extends Bone {
static initialize() {
this.hasMany('items', { className: 'Commodity' })
}
}
```
The TypeScript equivalent with decorator is like below:
```ts
class Shop extends Bone {
// It might be able to deduce the className from `Commodify[]` type
@HasMany({ className: 'Commodity' })
items: Commodity[];
}
```
As you can tell from the ER diagram, the foreign key used to join two tables is located at the target table, `items`. To override the foreign key, just pass it to the option of `hasMany()`:
```js
class Shop extends Bone {
static initialize() {
this.hasMany('items', { foreignKey: 'sellerId' })
}
}
```
The TypeScript equivalent with decorator is like below:
```ts
class Shop extends Bone {
@HasMany({ foreignKey: 'sellerId' })
items: Item[];
}
```
#### `hasMany({ through })`
The world of entity relationships doesn't consist of one-to-one or one-to-many associations only. There are many scenarios that require a many-to-many association to be setup. However, in relational databases many-to-many between two tables isn't possible by nature. To accompish this, we need to introduce an intermediate table to bridge the associations.
Take following tag system for example:
A shop can have as many tags as it see fit. And a tag can be related to as many shops as it like. The actual relationships are recorded in the `tag_maps` table. To find associations either from the shop or the tag, the query needs to go through `tag_maps` first.
> As you may have noticed already, the `tag_maps` doesn't necessarily relate to `shops` as their targets in this ER model. It supports generic targets with the `target_type` column. In this way, the `tags` can have many-to-many associations to any other models.
`hasMany({ through })` is just the method that helps us setup that kind of associations. From `Shop`'s point of view:
```js
class Shop extends Bone {
static initialize() {
// the extra where is needed if you fancy this generic tag system
this.hasMany('tagMaps', { foreignKey: 'targetId', where: { targetType: 0 } })
this.hasMany('tags', { through: 'tagMaps' })
}
}
```
The TypeScript equivalent with decorator is like below:
```ts
class Shop extends Bone {
@HasMany({ foreignKey: 'targetId', where: { targetType: 0 } })
tagMaps: TagMap[];
@HasMany({ through: 'tagMaps' })
tags: Tag[];
}
```
On `Tag`'s side:
```js
class Tag extends Bone {
static initialize() {
this.hasMany('shopTagMaps', { className: 'TagMap', where: { targetType: 0 } })
this.hasMany('shops', { through: 'shopTagMaps' })
}
}
```
The TypeScript equivalent with decorator is like below:
```ts
class Shop extends Bone {
@HasMany({ className: 'TagMap', foreignKey: 'targetId', where: { targetType: 0 } })
shopTagMaps: TagMap[];
@HasMany({ through: 'shopTagMaps' })
shops: Tag[];
}
```
If suddenly our business requires us to apply the tag system to items too, the changes needed on `Tag` model is trivial:
```diff
class Tag extends Bone {
static initialize() {
this.hasMany('shopTagMaps', { className: 'TagMap', where: { targetType: 0 } })
this.hasMany('shops', { through: 'shopTagMaps' })
+ this.hasMany('itemTagMaps', { className: 'TagMap', where: { targetType: 1 } })
+ this.hasMany('items', { through: 'itemTagMaps' })
}
}
```
#### `hasOne()`
A `hasOne()` association also sets up a one-to-one connection with another model, but with a few sematic differences. At first glance it may look quite similar to `belongsTo()` or even `hasMany()`.
The difference between `hasOne()` and `belongsTo()` is mainly at the position of the foreign key. `hasOne()`, like `hasMany()`, needs the foreign key to be added in the target model, while `belongsTo()` needs the it located in the initiating model.
The difference between `hasOne()` and `hasMany()` is subtle. When a model `hasOne()` of another model, the other model will be mounted as a singleton. When it `hasMany()` of another model, the mounted attribute will be a collection that contains all the target models instead.
In this example, a user has one shop:
```js
class User extends Bone {
static initialize() {
this.hasOne('shop', { foreignKey: 'ownerId' })
}
}
```
The TypeScript equivalent with decorator is like below:
```ts
class User extends Bone {
@HasOne({ foreignKey: 'ownerId' })
shop: Shop;
}
```
And the shop belongs to the user:
```js
class Shop extends Bone {
static initialize() {
this.belongsTo('owner', { className: 'User' })
}
}
```
The TypeScript equivalent with decorator is like below:
```ts
class Shop extends Bone {
@BelongsTo({ className: 'User' })
owner: User;
}
```
#### Choosing Between `belongsTo()` and `hasOne()`
As dicussed in the `hasOne()` section, the difference between `belongsTo()` and `hasOne()` is mostly at where to place the foreign key. The corresponding model of the table that contains the foreign key should be the one that declares the `belongsTo()` association.
For example, it makes perfect sense that a user should `hasOne()` shop, and the shop should `belongsTo()` an owner (which is a special type of user). Then the shops table should declare the foreign column called `owner_id` to make `this.hasOne('shop', { foreignKey: 'ownerId' })` work.
There's a bonus of reasoning this kind of differences between associations. If the business needs to allow a user to have multiple shops someday, we can just change the `hasOne()` to `hasMany()`, wrap existing code into a `for (const shop of user.shops)` loop, and then go get a coffee. There's no need to touch code at the `Shop` model.
## Query Interface
This guide covers different ways to retrieve data from the database using Leoric. After reading this guide, you will know:
- How to filter records using a variety of methods and conditions.
- How to specify the order, retrieved attributes, grouping, and other properties of the found records.
- How to use `.include()` to reduce the number of database queries needed for data retrieval.
### Retrieving Objects from the Database
Leoric provides two major ways to start a query, `.find()` and `.findOne()`. `.findOne()` is basically the same as `.find()`, except that it returns only one record or null if no record were found.
#### Retrieving a Single Object
##### `.findOne()`
```js
const post = await Post.findOne(1)
// => Post { id: 1, ... }
```
The SQL equivalent of the above is:
```sql
SELECT * FROM posts WHERE id = 1 LIMIT 1;
```
`.findOne()` is like a stingy twin of `.find()` because it works exactly like `.find()` except that it will always append a `.limit(1)` to it. Hence complex query is possible with `.findOne()` too:
```js
const post = await Post.findOne({
title: ['New Post', 'Untitled'],
createdAt: new Date(2012, 4, 15)
})
// => Post { id: 1, title: 'New Post', ... }
```
The SQL equivalent of the above is:
```sql
SELECT * FROM posts WHERE title IN ('New Post', 'Untitled') AND created_at = '2012-04-15 00:00:00' LIMIT 1;
```
If no record is found, `.findOne()` will return `null` whereas `.find()` will return an empty collection.
##### `.first`
The `.first` getter finds the first record ordered by primary key. For example:
```js
const post = await Post.first
// => Post { id: 1, ... }
```
The SQL equivalent of the above is:
```sql
SELECT * FROM posts ORDER BY id LIMIT 1;
```
##### `.last`
The `.last` getter finds the last record ordered by primary key. For example:
```js
const post = await Post.last
// => Post { id: 42, ... }
```
The SQL equivalent of the above is:
```sql
SELECT * FROM posts ORDER BY id DESC LIMIT 1;
```
#### Retrieving Multiple Objects
To retrieve multiple objects, just change from `.findOne()` to `.find()`. It takes the same parameters as `.findOne` but will always return a collection. If no records were found, the collection will be empty. For example:
```js
const posts = await Post.find({ id: [1, 10] })
// => Collection [ Post { id: 1, ... },
// Post { id: 10, ... } ]
```
The SQL equivalent of the above is:
```sql
SELECT * FROM posts WHERE id in (1, 10);
```
#### Retrieving Multiple Objects in Batches
When we need to iterate over a large collection, the solution might seems straightforward:
```js
const posts = await Post.all
for (const post of posts) {
// handle post
}
```
But if the posts table is at large size, this approach becomes slow and memory consuming, hence impractical. There are many ways to circumvent situations like this, such as refactor the implementation into smaller operations without loading all rows at once and so on. Switch to find in batch is the most convenient one:
```js
for await (const post of Post.all.batch()) {
// handle post
}
```
The SQL equivalent of the above is:
```sql
-- assume posts contains 2000 rows, the default LIMIT is 1000
SELECT * FROM posts LIMIT 1000;
SELECT * FROM posts LIMIT 1000 OFFSET 1000;
SELECT * FROM posts LIMIT 1000 OFFSET 2000;
```
To set batch size, we can pass a number to `.batch()`:
```js
// This queries database with LIMIT 100
for await (const post of Post.all.batch(100)) {
// handle post
}
```
### Conditions
Both `.find()` and `.findOne()` allow you to specify conditions to filter records stored in database. Conditions can either be specified as:
- pure string,
- template string with arguments, or
- object.
For brevity and security concerns, we'd recommend using template string conditions.
#### Pure String Conditions
Pure string conditions is quite handy if you need to query with literal values:
```js
Post.find('title != "New Post"')
// => SELECT * FROM posts WHERE title != 'New Post';
```
But it can be dangerous too, if it is in clumsy hands:
```js
Post.find(`title != ${title}`)
// let title be "'' or 1 = 1"
// => SELECT * FROM posts WHERE title != '' OR 1 = 1;
```
To prevent this SQL injection prone usage, Leoric will throw an error if complex values were found while parsing string conditions. The allowed values are:
- numbers
- strings with single/double quotations (e.g. `'foo'`, `"bar"`)
- `true`/`false`
- `null`/`undefined`
For other type of values, consider object conditions or templated string conditions.
#### Object Conditions
Object conditions may sound familiar because it's a common approach of condition mapping in JavaScript, let alone in NoSQL databases like MongoDB. With object conditions, most of the simple conditions can be carried out by listing fields as keys and values as, well, values. The values can be extended as objects with `$operator`s as key, hence make comparison conditions possible as well. Here are a few examples of object conditions with primitive values:
```js
Post.find({ id: 1 })
// => SELECT * FROM posts WHERE id = 1;
Post.find({ title: 'New Post' })
// => SELECT * FROM posts WHERE title = 'New Post';
Post.find({ title: undefined })
Post.find({ title: null })
// => SELECT * FROM posts WHERE title IS NULL;
```
and with values of array or other non-primitive types:
```js
Post.find({ title: ['New Post', 'Untitled'] })
// => SELECT * FROM posts WHERE title IN ('New Post', 'Untitled');
Post.find({
title: { toSqlString: () => "'New Post'" }
})
// toSqlString() will be called when it comes to objects with toSqlString() method.
// => SELECT * FROM posts WHERE title = 'New Post';
```
#### Object Conditions with Operators
As you may have noticed in the previous example, the values in object conditions can be objects as well. If every property of the object is one of `($eq, $gt, $gte, $lt, $lte, $ne, $in, $nin, $notIn, $like, $notLike, $between, $notBetween)`, the object is considered operator object condition:
```js
Post.find({ title: { $ne: 'New Post' } })
// => SELECT * FROM posts WHERE title != 'New Post';
Post.find({ title: { $like: '%Post%' } })
// => SELECT * FROM posts WHERE title LIKE '%Post%';
Post.find({ createdAt: { $lt: new Date(2017, 10, 11) } })
// => SELECT * FROM posts WHERE gmt_create < '2017-11-11 00:00:00';
Post.find({ createdAt: { $notBetween: [new Date(2017, 10, 11), new Date(2017, 11, 12)] } })
// => SELECT * FROM posts WHERE gmt_create NOT BETWEEN '2017-11-11 00:00:00' AND '2017-12-12 00:00:00';
```
If the object has multiple operators, the condition is combined with `AND`:
```js
Post.find({ id: { $gt: 0, $lt: 999999 }})
// => SELECT * FROM posts WHERE id >= 0 AND id <= 999999
```
Currently no logical operators (such as `AND`, `OR`, and `!`) is supported via operator object condition. Consider using string conditions instead.
#### Templated String Conditions
Templated string conditions usually are the better option against object conditions when it comes to multiple conditions or comparison conditions for its brevity. The example of object conditions above can be written in templated string conditions as this:
```js
Post.find('title != ?', 'New Post')
// => SELECT * FROM posts WHERE title != 'New Post';
Post.find('title like ?', '%Post%')
// => SELECT * FROM posts WHERE title LIKE '%Post%';
Post.find('createdAt < ?', new Date(2017, 10, 11))
// => SELECT * FROM posts WHERE gmt_create < '2017-11-11 00:00:00'
Post.find('createdAt < ? or createdAt > ?', new Date(2017, 10, 11), new Date(2017, 11, 12))
// => SELECT * FROM posts WHERE gmt_create < '2017-11-11 00:00:00'
```
Templated string conditions works with special primitive values or non-primitive values too:
```js
Post.find('title = ?', null)
Post.find('title = ?', undefined)
// => SELECT * FROM posts WHERE title IS NULL;
Post.find('title = ?', ['New Post', 'Untitled'])
// => SELECT * FROM posts WHERE title in ('New Post', 'Untitled');
```
When it comes to combining multiple conditions, templated string conditions is at its best advantage:
```js
Post.find('title != ? and createdAt > ?', 'New Post', new Date(2017, 10, 11))
// => SELECT * FROM posts WHERE title != 'New Post' AND gmt_create > '2017-10-11';
```
### Ordering
To retrieve the records from the database in specific order, you can use the order method.
For example, to retrieve posts updated most recently, we can order the posts by `updatedAt` in descending order:
```js
Post.order('updatedAt', 'desc')
```
`.order()` also accepts parameters in following types:
```js
Post.order('updatedAt desc')
Post.order({ updatedAt: 'desc' })
```
The SQL equivalent of the above is:
```sql
SELECT * FROM posts ORDER BY updated_at DESC;
```
The order is default to `asc`. Hence `Post.order('updatedAt')` is the same as `Post.order('updatedAt asc')`.
To order by multiple columns:
```js
Post.order({ updatedAt: 'desc', title: 'asc' })
// or
Post.order('updatedAt desc').order('title')
```
Both are equivalent to the following SQL:
```sql
SELECT * FROM posts ORDER BY updated_at DESC, title ASC;
```
### Selecting Specific Fields
By default, `.find()` selects all the fields from the result set using `*`. To select a subset of fields from the result set, you can specify the subset with the `.select()` method:
```js
Post.select('id', 'title', 'createdAt')
// or
Post.select('id, title, createdAt')
```
The SQL equivalent of the above is:
```sql
SELECT id, title, created_at FROM posts;
```
### Limit and Offset
It is always recommended to limit the query, unless the query result is unlikely to be bloated. One of the scenarios where limit and offset are used most often, is pagination. For example, to get the top 20 posts updated most recently:
```js
const posts = await Post.order('updatedAt desc').limit(20)
```
To get the second 20 posts updated most recent, e.g. user turned to page 2 and the page size is 20:
```js
const posts = await Post.order('updatedAt desc').limit(20).offset(20)
```
The SQL equivalent of the above is:
```sql
SELECT * FROM posts ORDER BY updated_at DESC LIMIT 20 OFFSET 20;
```
### Group
`GROUP BY` is one of most important features of relational database. Combined with calculation functions such as `COUNT()` and `SUM()`, it is a convenient way of accumulating meaningful data from records.
For example, if you want to find out at which date most posts where published:
```js
Post.group('DATE(createdAt)').count().order('count desc')
```
The SQL equivalent of the above is:
```sql
SELECT COUNT(*) as count, DATE(created_at) FROM posts GROUP BY DATE(created_at) ORDER BY count DESC;
```
When the query is grouped, it returns vanilla query results of the database instead of dispatching the results to the corresponding models because there's none. The example above might return:
```js
[ { count: 1, 'DATE(created_at)': '2017-12-12' },
{ count: 5, 'DATE(created_at)': '2017-11-11' },
... ]
```
It is still possible to join other models to the query though, we'll discuss that in the *Joining Tables* section.
### Having
`HAVING` is only necessary when you need to filter the results by calculated columns. It is recommended to put the conditions into `WHERE` as much as possible and leave only the calculated ones to `HAVING` because in this way the temporary data set would be smaller.
Take the group example above for another example, we can rule out dates that has the count of posts published less than 5.
```js
Post.group('DATE(createdAt)').count().order('count desc').having('count < 5')
```
The SQL equivalent of the above is:
```sql
SELECT COUNT(*) as count, DATE(created_at) FROM posts GROUP BY DATE(created_at) HAVING count < 5 ORDER BY count DESC;
```
And the results might be:
```js
[ { count: 4, 'DATE(created_at)': '2017-11-11' },
... ]
```
### Transactions
> The transaction ability is a bit premature currently due to the lack of `LOCK`. Hopefully we'll see to it soon.
We can use `Model.transaction()` to obtain a connection from the connection pool, and wrap the queries between `BEGIN` and `COMMIT`/`ROLLBACK` through the obtained connection. `Model.transaction()` takes either `AsyncFunction` or `GeneratorFunction` as argument. Take following transaction for example:
```js
Post.transaction(async function({ connection }) {
await Comment.create({ content: 'tl;dr', articleId: 1 }, { connection });
await Post.findOne({ id: 1 }).increment('commentCount', { connection });
});
```
the equivalent generator function version is like below:
```js
Post.transaction(function* () {
yield Comment.create({ content: 'tl;dr', articleId: 1 });
yield Post.findOne({ id: 1 }).increment('commentCount');
});
// => Promise
```
The SQL equivalent of the above is:
```sql
BEGIN
INSERT INTO comments (content, article_id) VALUES ('tl;dr', 1);
UPDATE posts SET comment_count = comment_count + 1 WHERE id = 1;
COMMIT
```
If there were any exceptions thrown during iteration, `Model.transaction()` forwards the exception after executing `ROLLBACK`.
The use of `function* () {}` might be a bit absurd at first glance. Behind the curtain,
1. A connection is obtained from the pool before the generator function is called.
2. `BEGIN`
3. Call `generator.next()` to push the iterator forward.
4. If `generator.next()` returns an instance of `Spell`, the obtained connection is set to `spell.connection`.
5. Spell performs the query through given connection.
6. Continue the iteration until the very end.
7. `COMMIT`
In this way we make sure all the related SQLs are queried through the same connection.
### Joining Tables
Leoric provides two ways of constructing JOIN querys:
- Join predefined associations using `.with(relationName)`,
- Join arbitrary models using `.join(Model, onConditions)`.
#### Predefined Joins
Predefined associations can be found by examining `Model.relations`, which is generated by calling `Model.describe()` implicitly. We can define associations by arranging `.hasMany()`, `.hasOne()`, and `.belongsTo()` in `Model.describe()` such as:
```js
class Post extends Bone {
static initialize() {
this.hasMany('comments')
this.belongsTo('author', { foreignKey: 'authorId', Model: 'User' })
}
}
```
To find with predefined joins, we call `.include(name)`:
```js
Post.include('comments')
// or
Post.find().with('comments')
```
The SQL equivalent of the above is:
```sql
SELECT * FROM posts LEFT JOIN comments ON posts.id = comments.post_id;
```
A LEFT JOIN is performed to preserve posts that have got no comments. The ON conditional expression is generated according to the type and the settings of the association. See [Associations]({{ '/associations' | relative_url }}) for detailed informations.
To find multiple predefined joins, we can either pass multiple association names to `.include()` or chain them one by one using `.with()`:
```js
Post.include('comments', 'author')
// or
Post.find().with('comments').with('author')
```
Please be noted that the chaining order of `.with()` matters, queries like below are not equivalent:
```js
Post.findOne().with('comments')
// NOT EQUALS TO
Post.find().with('comments').first
```
By type definitions, both queries will return a Post instance or null depending on the record can be found or not. But the generated SQLs are quite different:
```sql
SELECT * FROM (SELECT * FROM posts LIMIT 1) AS posts LEFT JOIN comments ON comments.post_id = posts.id
-- NOT EQUALS TO
SELECT * FROM posts AS posts LEFT JOIN comments ON comments.post_id = posts.id LIMIT 1
```
The major difference is the place of `LIMIT`, the former query will fetch the first post and all of its associated comments, the latter query however, will only return the first post and its first comment.
We can keep on chaining the query methods if comments need to be limited as well:
```js
Post.findOne().with('comments').limit(10)
```
which is equivalent of SQL below:
```sql
SELECT * FROM (SELECT * FROM posts LIMIT 1) AS posts LEFT JOIN comments ON comments.post_id = posts.id LIMIT 10
```
#### Arbitrary Joins
If a join is needed but not predefined in `Model.describe()`, it can still be accomplished with `.join()`:
```js
Post
.join(Comment, 'posts.id = comments.postId')
.join(User, 'posts.authorId = users.id')
```
The SQL equivalent of the above is:
```sql
SELECT * FROM posts LEFT JOIN comments ON posts.id = comments.post_id LEFT JOIN users ON posts.author_id = users.id;
```
Like predefined joins, LEFT JOIN is preferred to preserve left table in the final results.
The table aliases were transformed by `pluralize(camelCase(Model.name))`. In the example above, here are the transformed table aliases:
| Model Name | Table Alias |
|------------|-------------|
| Post | posts |
| Comment | comments |
| User | users |
We can reference these table aliases futher after the join, such as `.where()` or `.order()`:
```js
Post.join(Comment, 'posts.id = comments.postId').where('comments.id = 1')
Post.join(Comment, 'posts.id = comments.postId').where({ 'comments.id': 1 })
```
### Scopes
If the model has `deletedAt` attribute, it won't be actually deleted when calling `Model.remove()` but will be updated by setting `deletedAt` to the time when `Model.remove()` is called. This behavior is called soft delete.
To make soft delete transparent to model consumers, a default WHERE condition is added every time before a query generates the final SQL. For example, if `Post` model has `deletedAt` attribute, the SQL equivalent of `Post.find()` would be:
```sql
SELECT * FROM posts WHERE deleted_at IS NULL;
```
But if any `WHERE` conditional expressions have got `deletedAt` referenced already, the default `.where({ deletedAt: null })` won't be appended. For example, the SQL equivalent of `Post.find('deletedAt != null')` is:
```sql
SELECT * FROM posts WHERE deleted_at IS NOT NULL;
```
Leoric implemented this behavior as scopes, which is a concept (among many others) stolen from Active Record. Currently this conditional `.where({ deletedAt: null })` is the only default scope.
#### unscoped
To truly go scope free, we can get the unscoped version of the query by accessing the `unscoped` attribute:
```js
Post.find({ id: [1, 10] }).unscoped
```
Regardless of whether `Post` has got a `deletedAt` attribute or not, the SQL equivalent of the above is:
```sql
SELECT * FROM posts WHERE id IN (1, 10)
```
### Understanding Method Chaining
Leoric supports [Method Chaining](http://en.wikipedia.org/wiki/Method_chaining), which allows methods be appended consecutively to complete the query. It is implemented by returning an instance of `Spell` when a query method of the model, such as `.find()` and `.order()`, is called.
```js
Post.find() // => Spell { Model: Post }
```
The spell provides methods such as `.where()`, `.order()`, `.group()`, `.having()`, `limit()`, and `.join()`. Most of them returns an instance of `Spell`, hence making method chaining possible. When the methods were called, the SQL isn't generated right away. We can get the final SQL manually by calling `.toSqlString()`. To get the query results, we can treat spells as promises. For example:
```js
// ES5 style
const spell = Post.find()
spell
.then(posts => { ... })
.catch(err => console.error(err.stacak))
// ES6 with co
co(function* () {
const posts = yield Post.find()
})
// ES2016 style
async function() {
const posts = await Post.find()
}
```
Since Leoric is written in ES2016, which is supported by Node.js LTS already, we'd encourage you to start using async/await too.
Anyway, you can always append further query details onto the spell until it's done, even if there's asynchronous jobs in between:
```js
const query = Post.where('title LIKE ?', '%Post%')
const posts = await query.order('updatedAt desc').limit(10)
const [{ count }] = await query.count() // unordered and unlimited count
this.body = { posts, count }
```
### Find or Build a New Object
It's common that you need to find a record or create it if it doesn't exist. Hence our source of inspiration, Active Record, provides a specific `find_or_create_by` method. It's trivial to implement but can get confused with `upsert` behaviour a lot.
> In MongoDB there's [`db.collection.update({ upsert: true })`](https://docs.mongodb.com/manual/reference/method/db.collection.update/#mongodb30-upsert-id), in PostgreSQL there's [`INSERT ... ON CONFLICT ... DO UPDATE`](https://www.postgresql.org/docs/9.5/static/sql-insert.html#SQL-ON-CONFLICT), and in MySQL (and forks such as MariaDB) there's [`INSERT ... ON DUPLICATE KEY UPDATE`](https://dev.mysql.com/doc/refman/5.7/en/insert-on-duplicate.html). In general, if duplicated values of primary key were found, the record gets updated. If not, the record gets inserted.
Leoric takes this `upsert` behavior to update on duplicated keys. For example:
```js
const post = new Post({ id: 1, title: 'New Post' })
await post.save()
```
If `Post { id: 1 }` exists, its `title` gets updated to `New Post`.
But this `upsert` thing is **NOT** exactly the same as the meaning of *Find or Build a New Object*. For example, if our users were distinguished by email, we can find the user by email, or create a new one if not found:
```js
let user = await User.findOne({ email: 'john@example.com' })
if (!user) user = await User.create({ email: 'john@example.com' })
```
To make a long story short, if the value of the primary key is known, feel free to use `model.save()` because it's taken care of with `upsert`. If not, we'll need to find or build a new object by hand.
### Calculations
All calculation methods work directly on a model:
```js
const results = await Post.count()
```
Or on a query:
```js
const results = await Post.where('name like ?', '%Post%').count()
```
#### Count
If you want to count the total numbers of records in your model's table you could call `Model.count()`. If you need to be more specific, say to find how many items does the shop have got, you can:
```js
Shop.find(1).with('items').count('items.*')
```
The SQL equivalent of the above is:
```sql
SELECT COUNT(items.*) AS count FROM (SELECT * FROM shops WHERE id = 1) AS shops LEFT JOIN items ON items.shop_id = shops.id;
```
#### Average
If you want to see the average of certain number in your model's table, you could call `Model.average()`. Say to find the average age of your website's subscribed users, you can:
```js
User.where({ subscribed: true }).average('age')
```
The SQL equivalent of the above is:
```sql
SELECT AVG(age) FROM users WHERE subscribed = 1;
```
#### Minimum
If you want to see the minimum of certain number in your model's table, you could call `Model.minimum()`. Say to find the minimum age of your website's subscribed users, you can:
```js
User.minimum('age')
```
The SQL equivalent of the above is:
```sql
SELECT MIN(age) AS minimum FROM users;
```
#### Maximum
If you want to see the maximum of certain number in your model's table, you could call `Model.maximum()`. Say to find the maximum age of your website's subscribed users, you can:
```js
User.maximum('age')
```
The SQL equivalent of the above is:
```sql
SELECT MAX(age) AS maximum FROM users;
```
#### Sum
If you want to find the sum of a field for all records in your model's table you could call `Model.sum()`. Say to find the total price of the items of certain shop, you can:
```js
Shop.find(42).with('items').sum('items.price')
```
The SQL equivalent of the above is:
```sql
SELECT SUM(items.price) FROM (SELECT * FROM shops WHERE id = 42) AS shops LEFT JOIN items ON items.shop_id = shops.id;
```
## JSON Fields
### Field Declaration
```typescript
import { Bone, DataTypes } from 'leoric';
class Post extends Bone {
@Column(DataTypes.JSONB)
extra: Record;
}
```
### Querying
You can use JSON functions to customize filter conditions:
```typescript
const post = await Post.find('JSON_EXTRACT(extra, "$.foo") = ?', 1);
```
The `column->path` shorthand syntax in MySQL (such as `extra->"$.foo"`) is not currently supported.
### Updating
The following update approach is prone to concurrency issues that can cause data to be overwritten:
```typescript
const post = await Post.first;
// If another process updates post.extra during this time interval, the updated data will be overwritten
await post.update('extra', { ...post.extra, foo: 1 });
```
MySQL provides two functions to address this situation:
- [JSON_MERGE_PATCH()](https://dev.mysql.com/doc/refman/8.4/en/json-modification-functions.html#function_json-merge-patch) // Overwrite merge
- [JSON_MERGE_PRESERVE()](https://dev.mysql.com/doc/refman/8.4/en/json-modification-functions.html#function_json-merge-preserve) // Preserves both values when duplicate properties are encountered
#### JSON_MERGE_PATCH()
Leoric provides a corresponding wrapper:
```typescript
const post = await Post.first;
await post.jsonMerge('extra', { foo: 1 });
```
The SQL executed by the second statement looks something like this:
```sql
UPDATE posts SET extra = JSON_MERGE_PATCH('extra', '{"foo":1}')
```
Note that the JSON_MERGE_PATCH() function only merges properties for objects. For arrays, strings, or boolean types, it will directly overwrite them.
> Since JSON_MERGE_PATCH() is closer to the merge behavior in JavaScript (`Object.assign()`, lodash/merge), the default bone.jsonMerge() method does not correspond to MySQL's deprecated JSON_MERGE() function, which is equivalent to JSON_MERGE_PRESERVE().
#### JSON_MERGE_PRESERVE()
JSON_MERGE_PRESERVE() has different logic. For arrays, strings, and other types, it returns a merged result:
```sql
JSON_MERGE_PRESERVE('[1, 2]', '[true, false]') // -> [1, 2, true, false]
JSON_MERGE_PRESERVE('1', 'true'); // -> [1, true]
JSON_MERGE_PRESERVE('{ "a": 1 }', '{ "a": 2 }'); // -> { "a": [1, 2] }
```
Leoric also provides a corresponding wrapper:
```typescript
const post = await Post.first;
await post.jsonMergePreserve('extra', { foo: 1 });
```
Since JSON_MERGE_PRESERVE() can change the value type, you need to be cautious when updating if the original property value is not an array.
### Change Tracking
By default, Leoric makes a copy of the model's attribute values when query results are returned, enabling the following feature:
```typescript
const post = await Post.first;
post.extra.foo = 2;
post.changes();
// -> { extra: [ { foo: 1 }, { foo: 2 } ] }
await post.save();
// -> UPDATE posts SET extra = JSON_MERGE_PATCH('extra', '{"foo":1}');
```
Deep copying objects in JavaScript is expensive. The native `structuredClone(value)` is even slower than `JSON.parse(JSON.stringify(value))`. When using mysql2, the query results are already objects, which further limits the optimization possibilities.
If the database contains large or numerous JSON data, and you don't rely on the automatic update marking feature above, you can consider skipping deep cloning of objects:
```typescript
new Realm({
skipCloneValue: true,
});
```
Then manually handle where you need to save objects:
```typescript
const post = await Post.first;
post.extra.foo = 2;
post.changes();
// -> {}
post.extra = { ...post.extra, foo, 2 };
post.changes();
// -> { extra: [ { foo: 1 }, { foo: 2 } ] }
await post.save();
// -> UPDATE posts SET extra = JSON_MERGE_PATCH('extra', '{"foo":1}');
```
## Hooks
Hooks support the insertion of specific contextual operations at specific times when a query is executed. This article describes the hook functions supported by Leoric and how to use them
### Declaring
You can declare Hook in the following way :
```javascript
// class syntax
class User extends Bone {
static beforeCreate() {}
static afterUpdate() {}
});
// define
Realm.define('User', attrs, {
hooks: {
beforeCreate() {},
afterUpdate() {},
}
});
```
### Available Hooks:
> `Model.method` means hook call by Model class, `instance.method` means hook call by Model's instance, the arguments of hooks are slightly different for different calling methods
#### create
`create` supports:
```javascript
// create hooks, args are create function's arguments
Model.beforeCreate(args) // function's context is the instance to be created
Model.afterCreate(instance, createResult) // createResult is create function's returns
instance.beforeCreate(args)
instance.afterCreate(instance, createResult) // function's context is the instance to be created
```
**Please be noted that the function context of the hooks of `create` are the instance to be created.**
#### bulkCreate
`bulkCreate` supports two types of hooks:
```javascript
// bulkCreate hooks
Model.beforeBulkCreate(records, queryOptions) // function's context is the Model
Model.afterBulkCreate(instances, Model) // the argument 'instances' are instances to be created
```
#### update
`update` supports:
```javascript
// update hooks
Model.beforeUpdate(args) // function's context is the `Model`
Model.afterUpdate(updateResult, Model)
instance.beforeUpdate(args) // function's context is the instance to be created
instance.afterUpdate(instance, updateResult) // function's context is the instance to be created, 'updateResult' is update function's returns.
```
#### save
`save` supports:
```javascript
instance.beforeSave(options)
instance.afterSave(instance, options)
```
Note that calling `save` may trigger the other functions' hooks (such as `create`, `update` or `upsert`).
- The `create` hook function fires when the instance is not persistent or with an unset primary key
- The `upsert` hook function fires when the instance is not persistent and the primary key has been set
- When the instance is persistent, the hook function of `upsert` will be triggered
#### upsert
`upsert` supports:
```javascript
instance.beforeUpsert(opts) // function's context is the instance
instance.afterUpsert(instance, upsertResult)
```
#### remove
`remove` supports:
```javascript
Model.beforeRemove(args)
Model.afterRemove(removeResult, Model)
instance.beforeRemove(args)
instance.afterRemove(instance, removeResult)
```
### Table of hooks
```javascript
// create hooks
Model.beforeCreate(args)
Model.afterCreate(instance, createResult)
instance.beforeCreate(args)
instance.afterCreate(instance, createResult)
// bulkCreate hooks
Model.beforeBulkCreate(records, queryOptions)
Model.afterBulkCreate(instances, Model)
// update hooks
Model.beforeUpdate(args)
Model.afterUpdate(updateResult, Model)
instance.beforeUpdate(args)
instance.afterUpdate(instance, updateResult)
// save hooks
instance.beforeSave(options)
instance.afterSave(instance, options)
// upsert hooks
instance.beforeUpsert(opts)
instance.afterUpsert(instance, upsertResult)
// remove hooks
Model.beforeRemove(args)
Model.afterRemove(removeResult, Model)
instance.beforeRemove(args)
instance.afterRemove(instance, removeResult)
```
## Logging
We can customzie logging methods with the `logger` option like below:
```js
const realm = new Realm({
dialect: 'mysql',
host: 'localhost',
logger: {
logQuery(sql, duration, opts) {}, // queries
logQueryError(err, sql, duration, opts) {}, // failed queries
logMigration(name) {}, // migrations
},
});
```
### `logQuery`
`logQuery(sql, duration, opts)` gets called when query completes, which receives arguments as explained in following table:
| name | type | description |
|-----|------|------|
| `sql` | `string` | SQL of the query |
| `duration` | `number` | response time of the query |
| `opts.command` | `string` | command of the query |
| `opts.connection` | `Connection` | related connection |
| `opts.Model` | `Model` | the model that initiates the query |
#### SQL
By default, we log the SQL as is. If there are sensible data to ignore, please consider the `hideKeys` option to hide certain columns from the logger.
#### Response Time
When performing model queries, such as `await User.findOne()`, the most time consuming steps are as below:
1. obtaining a connection from the connection pool,
2. sending query through obtained connection,
3. getting and formatting the result.
The duration in `logQuery`is about the time elapsed between step 2 and 3, which should be close to the response time from the database perspective.
#### Related Model
The model related to the query is accessible through `opts.Model`. If there are multiple models participated in the query, `opts.Model` is only bound to the initiator.
#### Extra Info
When performing queries through model methods, `opts` might contain extra info including but not limited to below:
| name | type | description |
|-----|------|-----|
| `opts.hints` | `Hint[]` | optimizer hints |
| `opts.columns` | `string[]` | columns to select |
| `opts.whereConditions` | `object[]` | where conditions |
### `logQueryError`
`logQueryError(err, sql, duration, opts)` receives almost the same arguments like `logQuery()` with an extra first arugment `err`. This method only gets called when the query fails to be carried out in database, mostly due to syntax error, validation error, or other constraints.
| name | type | description |
|-----|-----|------|
| `err` | `Error` | related error |
### `logMigration`
When performing migration tasks, besides the default `logQuery()` or `logQueryError()`, there is also `logMigration(name)` to log the related migration task.
### `hideKeys`
We can use the `hideKeys` option to refrain the values of certain columns from being printed.
```js
const realm = new Realm({
logging: {
hideKeys: [ 'users.password', 'docs.content' ],
},
});
```
The SQL with corresponding column values hidden is like below:
```sql
INSERT INTO users (name, password) VALUES ('John', '***');
```
## TypeScript Support
### Decorations
#### Model and Class Fields
Declare mapped fields with `declare` so TypeScript emits no runtime class field that could
shadow Leoric's attribute accessors. This is the preferred path and does not require a class
decorator.
```ts
import { Bone, Column } from 'leoric';
class User extends Bone {
@Column()
declare name: string;
}
```
Models supplied to `connect()` or a realm are registered directly and retain their original
constructor, inheritance, and `instanceof` behavior. Leoric reports an ES class field that
shadows a mapped accessor when it encounters the first ORM-created instance. A lint rule can
report the same mistake before runtime.
Use `@Model()` only when regular ES2022 fields must be supported:
```ts
import { Bone, Column, Model } from 'leoric';
@Model()
class User extends Bone {
@Column()
name!: string;
}
```
`@Model()` compiles the definition into a fresh `Bone` subclass once. It copies supported
methods, accessors, static configuration, and model metadata, but does not execute the
definition's constructor or instance field initializers. Put mapped defaults in `@Column()`
metadata. Custom constructors, private instance fields, and ordinary instance initializers
are not supported on compiled definitions.
#### Column
```ts
import { Bone, Column, DataTypes: { SMALLINT } } from 'leoric';
class User extends Bone {
@Column({ primaryKey: true })
declare id: bigint;
@Column({ allowNull: false })
declare name: string;
@Column()
declare createdAt: Date;
@Column()
declare updatedAt: Date;
@Column({ type: SMALLINT })
declare age: number;
}
```
Here is the list of options supported by `@Column()` that can be used to customize column definitions:
| option | description |
|-----------------------|-------------|
| primaryKey = false | declare class field as the primary key |
| autoIncrement = false | enable auto increment on corresponding class field, must be numeric type |
| allowNull = true | class field can not be null when persisting to database |
| type = typeof field | override the data type deduced from class field type |
| name = string | actual name of the table field in database |
If `type` option is omitted, `@Column()` will try to deduce the corresponding one as below:
| ts type | data type |
|---------|-----------|
| number | INTEGER |
| string | STRING / VARCHAR(255) |
| Date | DATE |
| bigint | BIGINT |
| boolean | BOOLEAN / TINYINT(1) |
Here is an example that is a little bit more comprehensive:
```ts
class User extends Bone {
@Column({ name: 'ssn', primaryKey: true, type: VARCHAR(16) })
declare ssn: string;
@Column({ name: 'gmt_create', allowNull: false })
declare createdAt: Date;
}
```
#### BelongsTo
```ts
import User from './user';
class Post extends Bone {
@BelongsTo()
declare user: User;
}
const post = await Post.include('user').first;
assert.ok(post.user.id);
```
If the foreign key didn't follow the naming convention, please provide it with:
```ts
class Post extends Bone {
@BelongsTo({ foreignKey: 'authorId' })
declare user: User;
}
```
#### HasMany
```ts
import Post from './post';
class User extends Bone {
@HasMany()
declare posts: Post[];
}
```
If the foreign key didn't follow the naming convention, please provide it with:
```ts
class User extends Bone {
@HasMany({ foreignKey: 'authorId' })
declare posts: Post[];
}
```
In a `hasMany` association, e.g. one-to-many, the foreign key should be at the associated table. Please refer to our documentation about [Associations]({% link associations.md %}) for more detail.
#### HasOne
```ts
import Profile from './profile';
class User extends Bone {
@HasOne()
declare profile: Profile;
}
```
If the foreign key didn't follow the naming convention, please provide it with:
```ts
import Profile from './profile';
class User extends Bone {
@HasOne({ foreignKey: 'ownerId' })
declare profile: Profile;
}
```
`hasOne` works almost the same as `hasMany`, which needs the foreign key to be at the associated table as well.
Whilst both `hasOne` and `belongsTo` can be used to create a one-to-one association, the major difference between them is where the foreign key is expected at. If you weren't familiar with the difference yet, please refer to our documentation about [Associations]({% link associations.md %}) for more detail.
#### HasMany with Through
For many-to-many associations, use the `through` option:
```ts
import Tag from './tag';
import TagMap from './tag_map';
class Post extends Bone {
@HasMany({ through: 'tagMaps' })
declare tags: Tag[];
@HasMany()
declare tagMaps: TagMap[];
}
```
### Validate Decorator
You can add a `validate` option to `@Column()` to enable field validation:
```ts
class User extends Bone {
@Column({
allowNull: false,
validate: {
isEmail: true,
},
})
declare email: string;
@Column({
validate: {
isUrl: true,
},
})
declare website: string;
}
```
### Complete TypeScript Model Example
Here is a comprehensive example showing a full model definition in TypeScript:
```ts
import { Bone, Column, BelongsTo, HasMany, DataTypes } from 'leoric';
const { TEXT, JSONB } = DataTypes;
import User from './user';
import Comment from './comment';
export default class Post extends Bone {
@Column({ primaryKey: true, autoIncrement: true })
declare id: bigint;
@Column({ allowNull: false })
declare title: string;
@Column(TEXT)
declare content: string;
@Column(JSONB)
declare extra: Record;
@Column()
declare userId: bigint;
@Column()
declare createdAt: Date;
@Column()
declare updatedAt: Date;
@Column()
declare deletedAt: Date;
@BelongsTo()
declare user: User;
@HasMany()
declare comments: Comment[];
}
```
### TypeScript 4.9 Compatibility
Leoric provides backward-compatible type declarations for TypeScript 4.9 and earlier through `typesVersions` in `package.json`. This is handled automatically - no additional configuration is needed.
If you're using TypeScript <= 4.9, the type declarations from the `types/ts4.9/` directory will be used instead of the default ones.
### Type Inference in Queries
TypeScript integration enables type-safe queries:
```ts
// Return type is inferred as Post | null
const post = await Post.findOne({ title: 'Hello' });
// Return type is inferred as Post[]
const posts = await Post.find({ userId: 1 });
// Attributes are type-checked
await Post.create({
title: 'New Post', // OK
content: 'Hello', // OK
// unknown: 'value', // TypeScript error: unknown property
});
```
### Configuration
To use decorators in TypeScript, ensure the following compiler options are enabled in your `tsconfig.json`:
```json
{
"compilerOptions": {
"experimentalDecorators": true,
"emitDecoratorMetadata": true
}
}
```
The `emitDecoratorMetadata` option is required for the automatic type inference in `@Column()` to work. You also need to install `reflect-metadata` (which is a dependency of Leoric).
## Sequelize Adapter
To ease the transition work from sequelize to leoric, a sequelize adapter is available once the `sequelize` switch is on:
```js
const Realm = require('leoric');
const realm = new Realm({
sequelize: true, // turn on sequelize adapter
host: 'localhost',
});
await realm.connect();
```
When sequelize adapter is active, the model API will behave similarly with the actual [Sequelize Model](https://sequelize.org/master/class/lib/model.js~Model.html). See the content below for detail.
### CRUD: Reading and Writing Data
#### Create
The sequelize adapter supports three most common ways of inserting data:
```js
await Shop.create({ name: 'MILL' });
await Shop.bulkCreate([
{ name: 'wagas' },
{ name: 'family mart' },
]);
await (new Shop({ name: "McDonald's" })).save();
```
All of the statments above yield SQL like below:
```sql
INSERT INTO shops (name) VALUES ('MILL');
INSERT INTO shops (name) VALUES ('wagas'), ('family mart');
INSERT INTO shops (name) VALUES ('McDonalds');
```
Compound queries like trying to find record before creating are supported as well:
```js
// a bit like upsert but this does not deal with udpating existing records
await Shop.findOrCreate({
where: { name: 'Shanghai Brewhouse' },
});
// try to find record first, if not found try to create, if create fails, find again
await Shop.findCreateFind({
where: { name: 'Shanghai Brewhouse' },
});
```
#### Read
In sequelize both `Model.find()` and `Model.findOne()` returns single result. The former one is just an alias of the latter, following content sticks with `Model.findOne()`:
```js
const shop = await Shop.findOne({
where: { name: 'Free Mori' },
});
```
The return type of `Model.findOne()` is `Model|null`, which means an instance of the module will be returned if record exists, and `null` is what you get if there is nothing.
To find multiple records, `Model.findAll()` should be used. This method accepts basically the same arguments of `Model.findOne()`, like below:
```js
const shops = await Shop.findAll({
attributes: [ 'id', 'name', 'created_at', 'updated_at' ],
where: {
name: { $like: '%Mori%' },
},
order: [[ 'id', 'desc' ]],
});
```
Leoric provides a bit more methods about reading data from database. If there is not an sequelize version, then the one defined at the base class of Leoric will be called as default.
```js
const shops = await Shop.all;
const shop = await Shop.first;
const top10 = await Shop.order('credit', 'desc').limit(10);
```
#### Update
#### Delete
#### Dirty Check
### Migrations
### Validations
### Hooks
### Associations
### Querying
#### Overriding Conditions
## Setup
### Web Frameworks
- [Setup with Egg / Chair]({{ '/setup/egg' | relative_url }})
- [Setup with Express]({{ '/setup/express' | relative_url }})
- [Setup wtih Midway]({{ '/setup/midway' | relative_url }})
### RDMS / dialects
Leoric supports many Relational Database Management Systems through `options.dialect`. Switching different clients to access same type of database is supported as well.
- [Setup with MySQL]({{ '/setup/mysql' | relative_url }})
- [Setup with SQLite]({{ '/setup/sqlite' | relative_url }})
- [Setup with PostgreSQL]({{ '/setup/postgres' | relative_url }})
## Setup with Egg
To reduce the effort to setup Leoric in Egg applications, a specific Egg plugin called [egg-orm](https://github.com/eggjs/egg-orm) is provided.
### Install
```bash
$ npm i --save egg-orm
$ npm install --save mysql2 # MySQL or other compatible databases
# other databases
$ npm install --save pg # PostgreSQL
$ npm install --save sqlite3 # SQLite
```
### Usage
With egg-orm, we can define models in `app/model` like below:
```js
// app/model/user.js
module.exports = function(app) {
const { STRING } = app.model.DataTypes;
return app.model.define('User', {
name: STRING,
password: STRING,
avatar: STRING(2048),
}, {
tableName: 'users',
});
}
```
Or even better, define models in `app/model` with `class` syntax like below:
```js
// app/model/user.js
module.exports = function(app) {
const { Bone } = app.model;
const { STRING } = app.model.DataTypes;
return class User extends Bone {
static table = 'users'
static attributes = {
name: STRING,
password: STRING,
avatar: STRING(2048),
}
};
}
```
then consume them in controllers (or services) in following fashion:
```js
// app/controller/home.js
const { Controller } = require('egg');
module.exports = class HomeController extends Controller {
async index() {
const users = await ctx.model.User.find({
corpId: ctx.model.Corp.findOne({ name: 'tyrael' }),
});
ctx.body = users;
}
};
```
### Configuration
Firstly, we need to install and enable egg-orm plugin:
```js
// config/plugin.js
exports.orm = {
enable: true,
package: 'egg-orm',
};
```
Secondly, we need to tell egg-orm how to connect with our database:
```js
// config/config.default.js
exports.orm = {
client: 'mysql',
database: 'temp',
host: 'localhost',
baseDir: 'app/model',
};
```
In the example configuration above, we have told egg-orm the models are at `app/model` directory, and the tables are at `temp` database which is accessible via `localhost`.
#### opts.baseDir
If our models reside in directory other than `app/model`, we can change the default with `opts.baseDir`:
```js
// config/config.default.js
exports.orm = {
baseDir: 'app/bone',
};
```
#### opts.delegate
If the mount point `app.model` or `ctx.model` is taken, we can change the delegated property name with `opts.delegate`:
```js
// config/config.default.js
exports.orm = {
delegate: 'bone',
};
```
Now we can access egg-orm from `app.bone` and `ctx.bone`.
#### opts.sequelize
If there already are models defined in Sequelize way, we can switch on the sequelize adapter in egg-orm to minimize the migration work.
```js
// config/config.default.js
exports.orm = {
client: 'mysql',
sequelize: true,
};
```
Please refer to the [sequelize adapter]({{ '/zh/sequelize' | relative_url }}) documentation about more information.
## Setup with Express
### Install
```bash
$ npm i --save leoric
$ npm i --save mysql2 # MySQL or compatible databases
# Other databases
$ npm i --save pg # PostgreSQL
$ npm i --save better-sqlite3 # SQLite
```
### Quick Start
#### Project Structure
A typical Express + Leoric project structure:
```text
my-app/
├── app.js # Express app entry
├── models/
│ ├── user.js
│ ├── post.js
│ └── comment.js
├── routes/
│ ├── users.js
│ └── posts.js
├── database/
│ └── migrations/ # Migration files
└── package.json
```
#### Defining Models
```js
// models/user.js
const { Bone, DataTypes } = require('leoric');
const { STRING, BIGINT, INTEGER } = DataTypes;
class User extends Bone {
static attributes = {
id: { type: BIGINT, primaryKey: true, autoIncrement: true },
name: STRING,
email: STRING,
age: INTEGER,
}
}
module.exports = User;
```
```js
// models/post.js
const { Bone, DataTypes } = require('leoric');
const { STRING, TEXT, BIGINT } = DataTypes;
class Post extends Bone {
static attributes = {
id: { type: BIGINT, primaryKey: true, autoIncrement: true },
title: STRING,
content: TEXT,
userId: BIGINT,
}
static initialize() {
this.belongsTo('user');
}
}
module.exports = Post;
```
#### Connecting to Database
Create a Realm instance and connect before starting the Express server:
```js
// app.js
const express = require('express');
const { Realm } = require('leoric');
const app = express();
app.use(express.json());
const realm = new Realm({
dialect: 'mysql',
host: 'localhost',
user: 'root',
database: 'my_app',
models: 'models',
});
// Mount realm on app for easy access in routes
app.set('realm', realm);
// Routes
app.use('/users', require('./routes/users'));
app.use('/posts', require('./routes/posts'));
// Connect to database, then start server
realm.connect().then(() => {
app.listen(3000, () => {
console.log('Server running on http://localhost:3000');
});
}).catch(err => {
console.error('Failed to connect to database:', err);
process.exit(1);
});
```
Alternatively, if models are defined with `class` syntax and no explicit `static attributes`, Leoric will load the schema from `information_schema.columns` automatically at `connect()` time:
```js
// models/user.js
const { Bone } = require('leoric');
class User extends Bone {
static initialize() {
this.hasMany('posts');
}
}
module.exports = User;
```
#### Using Models in Routes
```js
// routes/users.js
const express = require('express');
const User = require('../models/user');
const router = express.Router();
// GET /users
router.get('/', async (req, res) => {
const users = await User.find().order('id', 'desc').limit(20);
res.json(users);
});
// GET /users/:id
router.get('/:id', async (req, res) => {
const user = await User.findOne(req.params.id);
if (!user) return res.status(404).json({ error: 'User not found' });
res.json(user);
});
// POST /users
router.post('/', async (req, res) => {
const user = await User.create(req.body);
res.status(201).json(user);
});
// PUT /users/:id
router.put('/:id', async (req, res) => {
const user = await User.findOne(req.params.id);
if (!user) return res.status(404).json({ error: 'User not found' });
await user.update(req.body);
res.json(user);
});
// DELETE /users/:id
router.delete('/:id', async (req, res) => {
const user = await User.findOne(req.params.id);
if (!user) return res.status(404).json({ error: 'User not found' });
await user.remove();
res.status(204).end();
});
module.exports = router;
```
### Configuration
#### Database Options
All database connection options are passed to the Realm constructor:
```js
const realm = new Realm({
dialect: 'mysql', // 'mysql', 'postgres', or 'sqlite'
host: 'localhost',
port: 3306,
user: 'root',
password: 'secret',
database: 'my_app',
models: 'models', // path to models directory
migrations: 'database/migrations',
connectionLimit: 10, // connection pool size
});
```
For SQLite, use the `database` option (or `storage`) to specify the file path:
```js
const realm = new Realm({
dialect: 'sqlite',
database: './database.sqlite3',
models: 'models',
});
```
For PostgreSQL:
```js
const realm = new Realm({
dialect: 'postgres',
host: 'localhost',
port: 5432,
user: 'postgres',
password: 'secret',
database: 'my_app',
models: 'models',
});
```
#### Passing Models as Array
Instead of providing a directory path, you can pass model classes directly:
```js
const User = require('./models/user');
const Post = require('./models/post');
const realm = new Realm({
dialect: 'mysql',
database: 'my_app',
models: [User, Post],
});
```
### Middleware Pattern
For larger applications, you may want to create a middleware that ensures database connectivity:
```js
// middleware/database.js
const { Realm } = require('leoric');
const realm = new Realm({
dialect: 'mysql',
host: process.env.DB_HOST || 'localhost',
user: process.env.DB_USER || 'root',
password: process.env.DB_PASSWORD || '',
database: process.env.DB_NAME || 'my_app',
models: 'models',
});
let connected = false;
async function database(req, res, next) {
if (!connected) {
await realm.connect();
connected = true;
}
req.realm = realm;
next();
}
module.exports = { realm, database };
```
```js
// app.js
const express = require('express');
const { realm, database } = require('./middleware/database');
const app = express();
app.use(express.json());
app.use(database);
app.use('/users', require('./routes/users'));
realm.connect().then(() => {
app.listen(3000);
});
```
### Transactions
Use `Bone.transaction()` to wrap multiple operations in a transaction:
```js
router.post('/transfer', async (req, res) => {
const { fromId, toId, amount } = req.body;
await User.transaction(async ({ connection }) => {
const from = await User.findOne(fromId, { connection });
const to = await User.findOne(toId, { connection });
await from.update({ balance: from.balance - amount }, { connection });
await to.update({ balance: to.balance + amount }, { connection });
});
res.json({ success: true });
});
```
### Migrations
Create and run migrations with the Realm instance:
```js
// scripts/migrate.js
const { realm } = require('../middleware/database');
(async () => {
await realm.connect();
await realm.migrate();
await realm.disconnect();
console.log('Migrations complete');
})();
```
```js
// scripts/create-migration.js
const { realm } = require('../middleware/database');
const name = process.argv[2];
if (!name) {
console.error('Usage: node scripts/create-migration.js ');
process.exit(1);
}
(async () => {
await realm.createMigrationFile(name);
console.log(`Migration file created: ${name}`);
})();
```
Add scripts to `package.json`:
```json
{
"scripts": {
"migrate": "node scripts/migrate.js",
"migrate:create": "node scripts/create-migration.js"
}
}
```
### Error Handling
Add an error handler middleware to catch database errors:
```js
// app.js
app.use((err, req, res, next) => {
if (err.code === 'ER_DUP_ENTRY') {
return res.status(409).json({ error: 'Duplicate entry' });
}
if (err.name === 'LeoricValidateError') {
return res.status(400).json({ error: err.message });
}
console.error(err);
res.status(500).json({ error: 'Internal server error' });
});
```
### Graceful Shutdown
Disconnect from the database when the process exits:
```js
process.on('SIGTERM', async () => {
await realm.disconnect();
process.exit(0);
});
process.on('SIGINT', async () => {
await realm.disconnect();
process.exit(0);
});
```
### TypeScript
Leoric works with TypeScript in Express applications. Define models using decorators or static attributes:
```typescript
// models/user.ts
import { Bone, Column, DataTypes } from 'leoric';
class User extends Bone {
@Column({ primaryKey: true, autoIncrement: true })
id: bigint;
@Column()
name: string;
@Column()
email: string;
}
export default User;
```
```typescript
// app.ts
import express from 'express';
import { Realm } from 'leoric';
import User from './models/user';
import Post from './models/post';
const app = express();
const realm = new Realm({
dialect: 'mysql',
database: 'my_app',
models: [User, Post],
});
app.get('/users', async (req, res) => {
const users = await User.find();
res.json(users);
});
realm.connect().then(() => {
app.listen(3000);
});
```
## Setup with Midway
### Usage
Firstly, activate the leoric component in src/configuration.ts
```ts
// src/configuration.ts
import { Configuration, ILifeCycle } from '@midwayjs/core';
import * as leoric from '@midwayjs/leoric';
@Configuration({
imports: [
leoric,
],
})
export class ContainerLifeCycle implements ILifeCycle {}
```
Secondly, supply database configurations in src/config/config.default.ts
```ts
// src/config/config.default.ts
export default () => {
return {
leoric: {
dataSource: {
default: {
dialect: 'sqlite',
database: path.join(__dirname, '../../', 'database.sqlite'),
sync: true,
models: [
'**/models/*{.ts,.js}'
]
},
},
},
}
}
```
Lastly, models from the configured directory should be available with `@InjectModel()`:
```ts
// src/controller/user.ts
import { Controller } from '@midwayjs/core';
import { InjectModel } from '@midwayjs/leoric';
import User from '../model/user';
@Controller('/api/users')
export class UserController {
@InjectModel(User)
User: typeof User;
@Get('/')
async index() {
return await this.User.order('id', 'desc').limit(10);
}
}
```
### Decorators
#### @InjectModel()
Use `@InjectModel()` to inject model class in to class fields, such as:
```ts
// src/service/user.ts
import { Provide } from '@midwayjs/core';
import { InjectModel } from '@midwayjs/leoric';
import User from '../model/user';
@Provide()
export class UserService {
@InjectModel(User)
User: typeof User;
}
```
#### @InjectDataSource()
Use `@InjectDataSource()` to inject data source instance in to class fields, like below:
```ts
// src/service/user.ts
import { Provide } from '@midwayjs/core';
import { InjectDataSource, Realm } from '@midwayjs/leoric';
@Provide()
export class UserService {
@InjectDataSource()
realm: Realm;
async findAll() {
const { rows, fields, ...etc } = this.realm.query('SELECT * FROM users');
return rows;
}
}
```
If multiple datasources were configured, pass the name of the data source to `@InjectDataSource(name)` for the corresponding one.
### Multiple Data Sources
The way to configure multiple data sources in midway with leoric should not be very different from in midway with other ORM components. Here is one example of utilizing two sqlite databases in midway.
```ts
// src/config/config.default.ts
export default () => {
return {
leoric: {
dataSource: {
main: {
dialect: 'sqlite',
database: path.join(__dirname, '../../', 'database.sqlite'),
models: [
'models/*{.ts,.js}'
]
},
backup: {
dialect: 'sqlite',
database: path.join(__dirname, '../../', 'backup.sqlite'),
models: [
'backup/models/*{.ts,.js}'
]
},
},
defaultDataSourceName: 'main',
},
};
}
```
By specifing the `dataSource` parameter, the related models can be injected accordingly. If `dataSource` isn't specified, the one set with `defaultDataSourceName` will be used.
For example, the backup models or the backup data source itself can be injected with `@InjectModel(BoneLike, 'backup')` or `@InjectDataSource('backup')`:
```ts
// src/controller/user.ts
import { Controller, Get } from '@midwayjs/core';
import Realm, { InjectDataSource, InjectModel } from '@midwayjs/leoric';
import User from '../model/user';
@Controller('/api/users')
export class UserController {
@InjectModel(User, 'backup')
User: typeof User;
@InjectDataSource('backup')
backupRealm: Realm
@Get('')
async index() {
const users = await this.User.find();
return users.toJSON();
}
}
```
## Setup with MySQL
### Quick Start
MySQL, or any of its incarnations, is the default dialect Leoric supports, which can be setup easily as follows:
```js
const Realm = require('leoric');
const realm = new Realm({
host: 'localhost',
user: 'test',
database: 'test',
models: 'app/models',
});
await realm.connect();
```
Leoric uses [mysqljs/mysql](https://github.com/mysqljs/mysql) as the default client to access MySQL database, which should be added as dependency along with `leoric` itself:
```diff
diff --git a/package.json b/package.json
index cf91c34..7ae144d 100644
--- a/package.json
+++ b/package.json
@@ -45,6 +45,8 @@
"dependencies": {
+ "leoric": "^1.10.0",
+ "mysql": "^2.18.1",
```
### Options
#### `host`
The hostname of the database you are connecting to. (Default: `localhost`)
#### `port`
The port number to connect to. (Default: `3306`)
#### `user`
The MySQL user to authenticate as.
#### `password`
The password of the MySQL user to authenticate.
#### `database`
#### `appName`
In PolarDB, which is a MySQL compliant cloud based database formerly known as TDDL, there is a bit of confusion in database names. The name used to route the tables in database is called `appName`, which is also the one we use to config `database` of the MySQL client.
But the `table_schema` stored in `information_schema.columns` is another value. For example, if your database name is called `foo` in your local MySQL instance, when migrated to PolarDB it could be `foo` in `information_schema.columns.table_schema` and `FOO_APP` as the `database`:
```js
const realm = new Realm({
host: 'polardb.host',
user: 'FOO_APP',
appName: 'FOO_APP',
database: 'foo',
});
```
This option won't be necessary unless PolarDB is used.
#### `charset`
The charset for the connection. This is called "collation" in the SQL-level of MySQL (like `utf8_general_ci`). If a SQL-level charset is specified (like `utf8mb4`) then the default collation for that charset is used. (Default: `'UTF8_GENERAL_CI'`)
#### `connectionLimit`
The default pool provided by the client is used, which means all [pool options](https://github.com/mysqljs/mysql#pool-options) should be available. `connectionLimit` is the one whitelisted for now, more would be allowed in the future.
The option name explains itself, the pool size can be customized with this option, which is default to `10`.
#### `idleTimeout`
IMHO, this option should be available through the pool options we mentioned above but it actually doesn't, which is a pity.
Please subscribe [#148](https://github.com/cyjake/leoric/issues/148) for future updates.
#### `stringifyObjects`
When object is accidentally passed as the query value to MySQL client, the value will be formatted into expressions by default. Take following query for example:
```js
await Post.where({ name: { id: 1, name: 'Untitled' } });
```
which generates following SQL (and makes no sense):
```sql
SELECT * FROM `articles` WHERE `name` = `id` = 1 AND `name` = `Untitled`;
```
To mitigate this problem, we can turn on `stringifyObjects` to make sure object will be stringified if accidentally passed to query.
## Setup with SQLite
### Quick Start
Setting up Leoric with SQLite is easy as follows:
```js
const Realm = require('leoric');
const realm = new Realm({
dialect: 'sqlite',
database: 'database/development.sqlite3',
models: 'app/models',
});
await realm.connect();
```
Leoric uses [mapbox/node-sqlite3](https://github.com/mapbox/node-sqlite3) as the default client to access SQLite database, hence both `leoric` and `sqlite3` need to be added as dependencies:
```diff
diff --git a/package.json b/package.json
index cf91c34..7ae144d 100644
--- a/package.json
+++ b/package.json
@@ -45,6 +45,8 @@
"dependencies": {
+ "leoric": "^1.10.0",
+ "sqlite3": "^5.0.2",
```
### Options
#### `client`
The client used to access SQLite database can be customized with `client`. For example, if the database is encrypted with sqlcipher and `@journeyapps/sqlcipher` is the preferred client:
```js
const realm = new Realm({
client: '@journeyapps/sqlcipher',
dialect: 'sqlite',
database: 'database/development.sqlite3',
models: 'app/models',
});
```
Remember to add the customized client as dependencies. Currently both `sqlite3` and `@journeyapps/sqlcipher` are tested with Leoric in our continuous integration tests.
#### `trace`
To better generate the stack trace when error occurs while querying database, `client.verbose()` is called by default. This helper method is provided by `sqlite3` and has a slight performance penalty because each time a query is performed, there is a `new Error()` to capture the stack trace before the asynchronous call.
```
1) => SQLite driver.query()
should support async stack trace:
Error: SQLITE_ERROR: no such table: missing
--> in Database#all('SELECT * FROM missing', undefined, [Function: Leoric_all])
at /Users/nil/Projects/cyjake/leoric/src/drivers/sqlite/connection.js:48:21
at new Promise ()
at Connection.all (src/drivers/sqlite/connection.js:47:12)
at Connection.query (src/drivers/sqlite/connection.js:39:33)
at SqliteDriver.query (src/drivers/sqlite/index.js:46:33)
at async Context. (test/unit/drivers/sqlite/index.test.js:181:7)
```
For more detail about the result and the related code, see [!175](https://github.com/cyjake/leoric/pull/175).
This behavior can be turned off by setting `trace` to `false`.
#### `connectionLimit`
Connection pool for SQLite is supported as well, which is turned on by default with `connectionLimit` set to `10`.
Accessing SQLite database with multiple read/write connections (or should we say, file handles?) might cause random `SQLITE_BUSY` errors because there is no server to resolve database or table lock. To keep from situations like this, we can either turn off multiple connections by setting `connectionLimit` to `1`, or try telling the client to wait a little longer with bigger `busyTimeout`.
```js
const realm = new Realm({
dialect: 'sqlite',
database: 'database/development.sqlite3',
models: 'app/models',
connectionLimit: 1,
});
```
#### `busyTimeout`
The default `busyTimeout` is set to `30000` in milliseconds.
```js
const realm = new Realm({
dialect: 'sqlite',
database: 'database/development.sqlite3',
models: 'app/models',
busyTimeout: 30000,
});
```
For more information about `SQLITE_BUSY`:
-
-
### Using SQLCipher
The major different between SQLCipher and vanilla SQLite is the former one will encrypt the database file with a key. The key needs to be set before any queries are performed, otherwise SQLCipher won't be able to decrypt the database file and an error with message like `SQLITE_ERROR: file is not a database` gets thrown.
To make sure the key is set at the first place, regardless of the connection limit settings, we can listen on the `connection` event emitted from `realm.driver.pool`:
```js
realm.driver.pool.on('connection', function(connection) {
connection.query('PRAGMA key = "Riddikulus!"');
});
```
## Setup with PostgreSQL
### Quick Start
Leoric supprts PostgreSQL as well, which can be easily configured as follow:
```js
const Realm = require('leoric');
const realm = new Realm({
dialect: 'postgres',
host: 'localhost',
user: 'test',
database: 'test',
models: 'app/models',
});
await realm.connect();
```
[pg](https://node-postgres.com/) is the default client to access PostgreSQL database, hence we need to add both `pg` and `leoric` to package dependencies:
```diff
diff --git a/package.json b/package.json
index cf91c34..7ae144d 100644
--- a/package.json
+++ b/package.json
@@ -45,6 +45,8 @@
"dependencies": {
+ "leoric": "^1.10.0",
+ "pg": "^8.5.1",
```
### Options
#### `host`
The host of the database to connect. (Default: `localhost`)
#### `port`
The port of the database to connect. (Default: `5432`)
#### `user`
The user with enough privilege to access the database.
#### `password`
The password of the user to authenticate.
#### `database`
The name of the database to access.
## Data Types
### Overview
Leoric provides a set of data types through the `DataTypes` object. These types are used when defining model attributes either statically or via decorators.
```js
import { Bone, DataTypes } from 'leoric';
const { STRING, BIGINT, TEXT, BOOLEAN } = DataTypes;
class User extends Bone {
static attributes = {
id: { type: BIGINT, primaryKey: true },
name: { type: STRING(100), allowNull: false },
bio: TEXT,
active: BOOLEAN,
}
}
```
### String Types
#### `STRING(length)`
Variable-length character string. Maps to `VARCHAR` in SQL.
| Parameter | Default | Description |
|-----------|---------|---------------------|
| `length` | `255` | Maximum string length |
```js
STRING // VARCHAR(255)
STRING(100) // VARCHAR(100)
```
#### `CHAR(length)`
Fixed-length character string.
```js
CHAR // CHAR(255)
CHAR(10) // CHAR(10)
```
#### `TEXT(length)`
Long text type. The `length` parameter controls the size variant.
| Variant | SQL Type |
|--------------------|---------------|
| `TEXT` | `TEXT` |
| `TEXT('tiny')` | `TINYTEXT` |
| `TEXT('medium')` | `MEDIUMTEXT` |
| `TEXT('long')` | `LONGTEXT` |
```js
import { DataTypes, LENGTH_VARIANTS } from 'leoric';
TEXT // TEXT
TEXT(LENGTH_VARIANTS.tiny) // TINYTEXT
TEXT(LENGTH_VARIANTS.medium) // MEDIUMTEXT
TEXT(LENGTH_VARIANTS.long) // LONGTEXT
```
### Numeric Types
#### `INTEGER(length)`
32-bit integer. Supports `UNSIGNED` and `ZEROFILL` modifiers.
```js
INTEGER // INTEGER
INTEGER(10) // INTEGER(10)
INTEGER.UNSIGNED // INTEGER UNSIGNED
```
#### `TINYINT(length)`
8-bit integer.
```js
TINYINT // TINYINT
TINYINT(1) // TINYINT(1) - commonly used for boolean in MySQL
TINYINT.UNSIGNED // TINYINT UNSIGNED
```
#### `SMALLINT(length)`
16-bit integer.
```js
SMALLINT // SMALLINT
SMALLINT.UNSIGNED // SMALLINT UNSIGNED
```
#### `MEDIUMINT(length)`
24-bit integer.
```js
MEDIUMINT // MEDIUMINT
MEDIUMINT.UNSIGNED // MEDIUMINT UNSIGNED
```
#### `BIGINT(length)`
64-bit integer. Commonly used for primary keys.
```js
BIGINT // BIGINT
BIGINT.UNSIGNED // BIGINT UNSIGNED
```
> **Note**: JavaScript cannot safely represent integers larger than `Number.MAX_SAFE_INTEGER` (2^53 - 1). For very large numbers, values may be returned as strings.
#### `DECIMAL(precision, scale)`
Fixed-point decimal type. Suitable for financial data.
```js
DECIMAL // DECIMAL
DECIMAL(10, 2) // DECIMAL(10,2) - 10 digits total, 2 after decimal point
DECIMAL.UNSIGNED // DECIMAL UNSIGNED
```
#### `BOOLEAN`
Boolean type. Maps to `BOOLEAN` in SQL.
```js
BOOLEAN // BOOLEAN
```
### Date & Time Types
#### `DATE(precision, timezone)`
Datetime type. Maps to `DATETIME` or `TIMESTAMP` in SQL.
| Parameter | Default | Description |
|------------|---------|------------------------------------------|
| `precision`| — | Fractional seconds precision (0-6) |
| `timezone` | `true` | Enable timezone support (PostgreSQL only) |
```js
DATE // DATETIME
DATE(3) // DATETIME(3) - millisecond precision
DATE(6) // DATETIME(6) - microsecond precision
```
#### `DATEONLY`
Date-only type without time component. Maps to `DATE` in SQL.
```js
DATEONLY // DATE
```
### Binary Types
#### `BINARY(length)`
Fixed-length binary data.
```js
BINARY // BINARY(255)
BINARY(16) // BINARY(16)
```
#### `VARBINARY(length)`
Variable-length binary data.
```js
VARBINARY // VARBINARY
VARBINARY(255) // VARBINARY(255)
```
#### `BLOB(length)`
Binary large object.
| Variant | SQL Type |
|----------------------|---------------|
| `BLOB` | `BLOB` |
| `BLOB('tiny')` | `TINYBLOB` |
| `BLOB('medium')` | `MEDIUMBLOB` |
| `BLOB('long')` | `LONGBLOB` |
```js
BLOB // BLOB
BLOB(LENGTH_VARIANTS.long) // LONGBLOB
```
### JSON Types
#### `JSON`
JSON text type. Stored as `TEXT` in the database, but automatically serialized/deserialized.
```js
import { DataTypes } from 'leoric';
class Post extends Bone {
static attributes = {
meta: DataTypes.JSON,
}
}
```
#### `JSONB`
Native JSON binary type. Available in PostgreSQL and MySQL 5.7+. Stored as native `JSON` type.
```js
class Post extends Bone {
static attributes = {
extra: DataTypes.JSONB,
}
}
```
See [JSON Fields]({{ '/json' | relative_url }}) for querying and updating JSON data.
### Virtual Type
#### `VIRTUAL`
Virtual columns that are not persisted to the database. Useful for computed properties.
```js
class User extends Bone {
static attributes = {
firstName: STRING,
lastName: STRING,
fullName: {
type: VIRTUAL,
get() {
return `${this.firstName} ${this.lastName}`;
},
},
}
}
```
### LENGTH_VARIANTS
The `LENGTH_VARIANTS` enum provides named size variants for `TEXT` and `BLOB` types:
```js
import { LENGTH_VARIANTS } from 'leoric';
LENGTH_VARIANTS.tiny // 'tiny'
LENGTH_VARIANTS.empty // '' (default)
LENGTH_VARIANTS.medium // 'medium'
LENGTH_VARIANTS.long // 'long'
```
### Using with TypeScript Decorators
When using TypeScript, data types can be specified through the `@Column` decorator:
```ts
import { Bone, Column, DataTypes } from 'leoric';
const { TEXT, SMALLINT, JSONB } = DataTypes;
class User extends Bone {
@Column({ primaryKey: true })
id: bigint;
@Column()
name: string; // Inferred as STRING
@Column({ type: SMALLINT })
age: number; // Override: use SMALLINT instead of INTEGER
@Column(TEXT)
bio: string; // Override: use TEXT instead of STRING
@Column(JSONB)
meta: Record;
@Column()
createdAt: Date; // Inferred as DATE
}
```
See [TypeScript Support]({{ '/types' | relative_url }}) for more details on type inference.
### Database Dialect Differences
| Leoric Type | MySQL | PostgreSQL | SQLite |
|-------------|--------------------|---------------------|-----------|
| `STRING` | `VARCHAR` | `VARCHAR` | `TEXT` |
| `TEXT` | `TEXT` | `TEXT` | `TEXT` |
| `INTEGER` | `INT` | `INTEGER` | `INTEGER` |
| `BIGINT` | `BIGINT` | `BIGINT` | `INTEGER` |
| `BOOLEAN` | `TINYINT(1)` | `BOOLEAN` | `INTEGER` |
| `DATE` | `DATETIME` | `TIMESTAMP` | `TEXT` |
| `DATEONLY` | `DATE` | `DATE` | `TEXT` |
| `JSONB` | `JSON` | `JSONB` | `TEXT` |
| `BLOB` | `BLOB` | `BYTEA` | `BLOB` |
| `DECIMAL` | `DECIMAL` | `DECIMAL`/`NUMERIC` | `REAL` |
## Transactions
### Overview
Transactions ensure that a set of database operations either all succeed or all fail together. Leoric supports transactions through both `Bone.transaction()` and `realm.transaction()`, with support for both async functions and generator functions.
### Basic Usage
#### Using Async Functions
The most common way to use transactions is with an async function. The transaction will be automatically committed if the function completes successfully, or rolled back if an error is thrown.
```js
import { Bone } from 'leoric';
await Bone.transaction(async ({ connection }) => {
const post = await Post.create({ title: 'New Post' }, { connection });
await Comment.create({ postId: post.id, content: 'First!' }, { connection });
});
```
> **Important**: You must pass `{ connection }` to every query inside the transaction to ensure they all use the same database connection. Otherwise, the queries will run outside the transaction.
#### Using Generator Functions
Generator functions provide a convenient alternative where the connection is automatically passed to yielded Spell queries:
```js
await Bone.transaction(function* () {
const post = yield Post.create({ title: 'New Post' });
yield Comment.create({ postId: post.id, content: 'First!' });
});
```
With generator functions, Leoric automatically intercepts yielded `Spell` instances and assigns the transaction connection to them. This eliminates the need to manually pass `{ connection }` to every query.
### Using `realm.transaction()`
If you have a `Realm` instance, you can also start transactions from it:
```js
const realm = new Realm({ /* options */ });
await realm.connect();
await realm.transaction(async ({ connection }) => {
await Post.create({ title: 'Hello' }, { connection });
await User.update({ id: 1 }, { lastPostAt: new Date() }, { connection });
});
```
### Error Handling and Rollback
If any error is thrown inside the transaction callback, the entire transaction will be automatically rolled back:
```js
try {
await Bone.transaction(async ({ connection }) => {
await Post.create({ title: 'New Post' }, { connection });
// This will cause the entire transaction to rollback
throw new Error('Something went wrong');
});
} catch (err) {
console.error('Transaction failed:', err.message);
// Neither the Post nor anything else was created
}
```
### Manual Commit and Rollback
The transaction callback also receives `commit` and `rollback` functions for advanced control:
```js
await Bone.transaction(async ({ connection, commit, rollback }) => {
await Post.create({ title: 'New Post' }, { connection });
const result = await someExternalService();
if (!result.ok) {
await rollback();
return;
}
// Transaction will still be auto-committed at the end if not manually committed/rolled back
});
```
### Transactions with Hooks
Model hooks (such as `beforeCreate`, `afterUpdate`) are executed within the same connection context when triggered inside a transaction. This ensures that any additional database operations performed in hooks are part of the same transaction.
```js
class Post extends Bone {
static afterCreate(post, result) {
// This runs within the transaction if Post.create was called inside one
return AuditLog.create({
action: 'create',
modelName: 'Post',
modelId: post.id,
});
}
}
```
### Best Practices
1. **Always pass `connection`** when using async functions. Without it, queries run outside the transaction.
2. **Prefer generator functions** for simpler transaction code where all operations are Leoric queries.
3. **Keep transactions short**. Long-running transactions can cause lock contention and performance issues.
4. **Handle errors appropriately**. Wrap transactions in try-catch blocks when you need to handle failures gracefully.
5. **Avoid nested transactions**. Leoric does not currently support savepoints. If you need nested transactional behavior, restructure your code to use a single transaction.
## Raw Queries
### Overview
While Leoric's query interface covers most use cases, sometimes you need to execute raw SQL directly. Leoric provides several ways to work with raw SQL: `realm.query()`, `Model.query()`, the `raw()` function, and the `heresql` template helper.
### `realm.query(sql, values, options)`
Execute a raw SQL query through the `Realm` instance:
```js
const result = await realm.query('SELECT * FROM posts WHERE id = ?', [1]);
console.log(result.rows);
// => [{ id: 1, title: 'Hello', content: '...' }]
```
#### Return Value
The returned object contains:
| Property | Type | Description |
|---------------|----------|------------------------------------------------------|
| `rows` | `Array` | Query result rows |
| `fields` | `Array` | Column metadata (table, name) |
| `affectedRows`| `number` | Number of affected rows (for INSERT/UPDATE/DELETE) |
| `insertId` | `number` | Auto-generated ID (for INSERT) |
#### Parameterized Queries
Always use parameterized queries to prevent SQL injection:
```js
// Good - parameterized
const result = await realm.query(
'SELECT * FROM posts WHERE title = ? AND author_id = ?',
['Hello', 42]
);
// BAD - SQL injection risk!
const result = await realm.query(
`SELECT * FROM posts WHERE title = '${title}'`
);
```
#### Named Replacements
You can use named replacements with the `:name` syntax:
```js
const result = await realm.query(
'SELECT * FROM posts WHERE title = :title AND author_id = :authorId',
{
replacements: {
title: 'Hello',
authorId: 42,
},
}
);
```
#### Returning Model Instances
Pass a `model` option to have the results returned as model instances instead of plain objects:
```js
const result = await realm.query(
'SELECT * FROM posts WHERE id = ?',
{ model: Post, replacements: {} }
);
// result.rows are now Post instances
const post = result.rows[0];
console.log(post instanceof Post); // true
console.log(post.title);
```
#### Using a Transaction Connection
```js
await realm.transaction(async ({ connection }) => {
await realm.query(
'UPDATE posts SET title = ? WHERE id = ?',
['New Title', 1],
{ connection }
);
});
```
### `Model.query(sql, values)` (v2.14+)
Since v2.14, you can execute raw queries directly from a model class:
```js
const result = await Post.query('SELECT * FROM posts WHERE id = ?', [1]);
```
### The `raw()` Function
The `raw()` function creates a `Raw` SQL expression that won't be escaped. This is useful for using SQL functions or expressions in queries:
```js
import { raw } from 'leoric';
// Use SQL functions
await Post.update({ id: 1 }, { updatedAt: raw('NOW()') });
// UPDATE posts SET updated_at = NOW() WHERE id = 1
// Use in where clauses
const posts = await Post.find({
createdAt: raw('NOW() - INTERVAL 7 DAY'),
});
```
You can also access `raw()` from a `Realm` instance:
```js
await Post.update({ id: 1 }, { updatedAt: realm.raw('NOW()') });
```
> **Warning**: `raw()` bypasses escaping. Never pass user input directly to `raw()` as it can lead to SQL injection vulnerabilities.
### The `Raw` Class
The `Raw` class is the underlying implementation. You can use it directly or via `Raw.build()`:
```js
import { Raw } from 'leoric';
const expr = new Raw('COUNT(*)');
const expr2 = Raw.build('COUNT(*)');
```
### `heresql` Helper
The `heresql` function helps format multiline SQL strings into single-line queries, which is useful for logging and readability:
```js
import { heresql } from 'leoric';
const sql = heresql(`
SELECT *
FROM posts
WHERE author_id = ?
ORDER BY created_at DESC
LIMIT 10
`);
// => 'SELECT * FROM posts WHERE author_id = ? ORDER BY created_at DESC LIMIT 10'
```
It simply trims each line and joins them with a single space, making multiline SQL more readable in source code while producing clean single-line SQL for execution.
### Security Considerations
1. **Always use parameterized queries** for user-provided values. Never concatenate user input into SQL strings.
2. **Use `raw()` sparingly**. Only use it for SQL functions and expressions, never for user input.
3. **Use `realm.escape()`** when you absolutely must interpolate a value, though parameterized queries are always preferred.
```js
// Preferred: parameterized
await realm.query('SELECT * FROM posts WHERE title = ?', [userInput]);
// If you must escape manually
const escaped = realm.escape(userInput);
```
## Soft Delete
### Overview
Soft delete (also known as "paranoid" mode) allows you to mark records as deleted without actually removing them from the database. Instead of a `DELETE` statement, the record's `deletedAt` column is set to the current timestamp.
This is useful when you need to:
- Preserve data for auditing or compliance
- Allow users to recover accidentally deleted records
- Maintain referential integrity while hiding records from normal queries
### Enabling Soft Delete
To enable soft delete on a model, simply add a `deletedAt` attribute:
#### JavaScript
```js
import { Bone, DataTypes } from 'leoric';
class Post extends Bone {
static attributes = {
id: { type: DataTypes.BIGINT, primaryKey: true },
title: DataTypes.STRING,
deletedAt: DataTypes.DATE, // This enables soft delete
}
}
```
#### TypeScript
```ts
import { Bone, Column } from 'leoric';
class Post extends Bone {
@Column({ primaryKey: true })
id: bigint;
@Column()
title: string;
@Column()
deletedAt: Date; // This enables soft delete
}
```
#### Schema-based (without explicit attributes)
If you don't define attributes explicitly and let Leoric infer them from the database schema, soft delete is automatically enabled when the table has a `deleted_at` column.
### How It Works
#### Deleting Records
When soft delete is enabled, calling `.remove()` on a model instance or `Model.remove()` will update the `deletedAt` column instead of deleting the row:
```js
const post = await Post.findOne({ id: 1 });
await post.remove();
// SQL: UPDATE posts SET deleted_at = '2026-03-26 00:00:00' WHERE id = 1
```
Static method:
```js
await Post.remove({ id: 1 });
// SQL: UPDATE posts SET deleted_at = '2026-03-26 00:00:00' WHERE id = 1
```
#### Querying
By default, soft-deleted records are automatically excluded from all queries:
```js
const posts = await Post.find();
// SQL: SELECT * FROM posts WHERE deleted_at IS NULL
const post = await Post.findOne({ id: 1 });
// SQL: SELECT * FROM posts WHERE id = 1 AND deleted_at IS NULL LIMIT 1
```
#### Force Delete (Hard Delete)
To permanently delete a record from the database, pass `true` to `.remove()`:
```js
// Instance method
const post = await Post.findOne({ id: 1 });
await post.remove(true);
// SQL: DELETE FROM posts WHERE id = 1
// Static method
await Post.remove({ id: 1 }, true);
// SQL: DELETE FROM posts WHERE id = 1
```
### Querying Soft-Deleted Records
#### `unscoped`
To include soft-deleted records in your query, use `.unscoped`:
```js
const allPosts = await Post.unscoped.find();
// SQL: SELECT * FROM posts (no WHERE deleted_at IS NULL filter)
```
#### `paranoid: false`
You can also pass `paranoid: false` to specific queries:
```js
await Post.update({ title: 'Updated' }, { where: { id: 1 }, paranoid: false });
```
### Restoring Soft-Deleted Records
#### Instance Method
```js
// First, find the soft-deleted record using unscoped
const post = await Post.findOne({ id: 1 }).unparanoid;
// Or find via direct query
await post.restore();
// SQL: UPDATE posts SET deleted_at = NULL WHERE id = 1 AND deleted_at IS NOT NULL
```
#### Static Method
```js
await Post.restore({ id: 1 });
// SQL: UPDATE posts SET deleted_at = NULL WHERE id = 1 AND deleted_at IS NOT NULL
```
> **Note**: `restore()` will throw an error if the model does not have soft delete enabled (i.e., no `deletedAt` attribute).
### Soft Delete with Associations
When soft delete is enabled on a model, its associations will also respect the `deletedAt` scope. When loading associated records through `include()` or `with()`, soft-deleted associated records are automatically filtered out.
```js
class Post extends Bone {
static initialize() {
this.hasMany('comments');
}
}
class Comment extends Bone {
static attributes = {
deletedAt: DataTypes.DATE,
}
}
const post = await Post.findOne({ id: 1 }).with('comments');
// Comments with non-null deletedAt will be excluded
```
### Timestamps
Soft delete works with Leoric's automatic timestamp management. When a record is soft-deleted:
- `deletedAt` is set to the current date/time
- `updatedAt` is **not** automatically updated during soft-delete operations
When a record is restored:
- `deletedAt` is set to `null`
## Index Hints
### Overview
Index hints allow you to suggest or enforce which indexes the database engine should use when executing a query. Leoric supports MySQL index hints (`USE INDEX`, `FORCE INDEX`, `IGNORE INDEX`) and optimizer hints.
> **Note**: Index hints are primarily a MySQL feature. PostgreSQL and SQLite have different mechanisms for query optimization.
### Use Index
Suggest the database to use a specific index. The optimizer may choose to ignore the suggestion.
```js
Post.find().useIndex('idx_title')
// SELECT * FROM posts USE INDEX (idx_title)
// Multiple indexes
Post.find().useIndex('idx_title', 'idx_created_at')
// SELECT * FROM posts USE INDEX (idx_title,idx_created_at)
```
### Force Index
Force the database to use a specific index. The optimizer will not consider a full table scan unless no rows match.
```js
Post.find().forceIndex('idx_title')
// SELECT * FROM posts FORCE INDEX (idx_title)
```
### Ignore Index
Tell the database to not use a specific index.
```js
Post.find().ignoreIndex('idx_title')
// SELECT * FROM posts IGNORE INDEX (idx_title)
```
### Scoped Index Hints
You can limit index hints to specific query phases using scope objects:
#### For JOIN
```js
Post.find().useIndex({ join: 'idx_user_id' })
// SELECT * FROM posts USE INDEX FOR JOIN (idx_user_id)
```
#### For ORDER BY
```js
Post.find().useIndex({ orderBy: 'idx_created_at' })
// SELECT * FROM posts USE INDEX FOR ORDER BY (idx_created_at)
```
#### For GROUP BY
```js
Post.find().useIndex({ groupBy: 'idx_author_id' })
// SELECT * FROM posts USE INDEX FOR GROUP BY (idx_author_id)
```
#### Multiple scoped hints
```js
Post.find().useIndex(
'idx_id',
{ orderBy: ['idx_title', 'idx_org_id'] },
{ groupBy: 'idx_type' }
)
```
### Object Syntax
For more control, pass an object with explicit `index`, `type`, and `scope` properties:
```js
import { INDEX_HINT_TYPE, INDEX_HINT_SCOPE } from 'leoric';
Post.find().useIndex({
index: 'idx_title',
type: INDEX_HINT_TYPE.force,
scope: INDEX_HINT_SCOPE.orderBy,
})
// SELECT * FROM posts FORCE INDEX FOR ORDER BY (idx_title)
```
### Optimizer Hints
MySQL optimizer hints are embedded in `/*+ ... */` comments:
```js
Post.find().optimizerHints('SET_VAR(foreign_key_checks=OFF)')
// SELECT /*+ SET_VAR(foreign_key_checks=OFF) */ * FROM posts
Post.find().optimizerHints(
'SET_VAR(foreign_key_checks=OFF)',
'MAX_EXECUTION_TIME(1000)'
)
// SELECT /*+ SET_VAR(foreign_key_checks=OFF) MAX_EXECUTION_TIME(1000) */ * FROM posts
```
### Chaining with Other Query Methods
Index hints can be chained with all other query methods:
```js
Post
.find({ authorId: 1 })
.forceIndex('idx_author_id')
.order('createdAt', 'desc')
.limit(10)
```
## Realm
### Overview
`Realm` is the central entry point of Leoric. It manages the database connection, model registration, schema synchronization, and provides methods for raw queries and transactions.
```js
import Realm from 'leoric';
const realm = new Realm({
dialect: 'mysql',
host: 'localhost',
user: 'root',
database: 'my_app',
models: 'app/models',
});
await realm.connect();
```
### Constructor Options
The `Realm` constructor accepts a configuration object with the following options:
| Option | Type | Default | Description |
|--------------------|-------------------------------|------------|-----------------------------------------------------------------------------|
| `dialect` | `string` | `'mysql'` | Database dialect: `'mysql'`, `'postgres'`, or `'sqlite'` |
| `client` | `string` | — | Client module name: `'mysql'`, `'mysql2'`, `'pg'`, `'sqlite3'`, `'@journeyapps/sqlcipher'` |
| `dialectModulePath`| `string` | — | Alias for `client` |
| `host` | `string` | — | Database host |
| `port` | `number \| string` | — | Database port |
| `user` | `string` | — | Database user |
| `password` | `string` | — | Database password |
| `database` | `string` | — | Database name (aliases: `db`, `storage`) |
| `models` | `Array \| string` | — | Model classes, or a directory containing model classes |
| `subclass` | `boolean` | `false` | Whether to create a subclass of `Bone` to isolate models |
| `driver` | `AbstractDriver` | — | Custom driver class |
| `define` | `object` | — | Default model define options, e.g. `{ underscored: true }` |
| `logger` | `object` | — | Custom logger, see [Logging]({{ '/logging' | relative_url }}) |
| `charset` | `string` | — | Database charset |
| `idleTimeout` | `number` | — | Connection idle timeout in milliseconds |
| `sequelize` | `boolean` | `false` | Enable Sequelize compatibility adapter |
| `skipCloneValue` | `boolean` | `false` | Skip cloning attribute values for performance (v2.14+) |
#### Models as Directory Path
When `models` is a string, Leoric will scan the directory and load all `.js`, `.mjs`, and `.ts` files that export a `Bone` subclass:
```js
const realm = new Realm({
dialect: 'mysql',
host: 'localhost',
database: 'my_app',
models: 'app/models', // scans this directory
});
```
#### Models as Array
You can also pass model classes directly:
```ts
import { Bone } from 'leoric';
import Post from './models/post';
import User from './models/user';
class AuditLog extends Bone {}
const realm = new Realm({
dialect: 'mysql',
host: 'localhost',
database: 'my_app',
models: [Post, User, AuditLog],
});
```
Classes in `models` are registered directly. TypeScript mapped fields should use `declare` so
they do not emit own properties that shadow Leoric's accessors. Use `@Model()` only when the
definition intentionally contains regular ES2022 fields.
### Connecting
#### `realm.connect()`
Connect to the database and initialize all models. This method loads schema information from the database and maps it to the registered models.
```js
await realm.connect();
// Models are now ready to use
const posts = await Post.find();
```
#### `realm.disconnect()`
Disconnect from the database and release the connection pool.
```js
await realm.disconnect();
```
You can also pass a callback that will be executed before releasing connections:
```js
await realm.disconnect(async () => {
console.log('Cleaning up...');
});
```
### Using `connect()` Shortcut
For simple use cases, you can use the `connect()` function exported from `leoric` directly, without creating a `Realm` instance explicitly:
```ts
import { Bone, connect } from 'leoric';
class Post extends Bone {}
await connect({
host: 'localhost',
database: 'my_app',
models: [Post],
});
// Post is now ready
const posts = await Post.find();
```
> **Note**: `connect()` can only be called once with the default `Bone`. If you need multiple connections, use separate `Realm` instances.
### Defining Models Dynamically
#### `realm.define(name, attributes, options, descriptors)`
#### `realm.define(Model, attributes, options, descriptors)`
Define and register a model at runtime. The string overload generates a `Bone` subclass. The
class overload compiles the supplied definition once; always use the returned class binding.
```js
import Realm, { Bone } from 'leoric';
const realm = new Realm({ database: 'my_app' });
const { BIGINT, STRING, TEXT } = realm.DataTypes;
const Post = realm.define(
class Post extends Bone {
static initialize() {
this.belongsTo('author', { Model: 'User' });
}
},
{
id: { type: BIGINT, primaryKey: true },
title: STRING,
content: TEXT,
},
);
await realm.sync();
// Now you can use the model
await Post.create({ title: 'Hello', content: 'World' });
```
**Parameters:**
| Parameter | Type | Description |
|---------------|----------|--------------------------------------------|
| `name / Model`| `string / Bone subclass` | Model name or class to compile |
| `attributes` | `object` | Column definitions |
| `options` | `object` | Optional model init options |
| `descriptors` | `object` | Optional property descriptors |
Compilation creates a fresh subclass of the nearest ready `Bone`, copies the supported class
footprint, and never executes the definition constructor. Regular mapped fields therefore do
not shadow Leoric's accessors. Instance field initializers, custom constructors, and private
instance fields are not supported on compiled definitions; put defaults in attribute metadata.
### Schema Synchronization
#### `realm.sync(options)`
Synchronize the model definitions to the database. This will create tables that don't exist, and optionally alter existing tables to match the model definitions.
```js
await realm.sync();
```
**Options:**
| Option | Type | Default | Description |
|---------|-----------|---------|-------------------------------------------------------|
| `force` | `boolean` | `false` | Drop existing tables before creating (destructive!) |
| `alter` | `boolean` | `false` | Alter existing tables to match model definitions |
```js
// Create tables that don't exist
await realm.sync();
// Drop and recreate all tables (WARNING: data loss!)
await realm.sync({ force: true });
// Alter existing tables to match models
await realm.sync({ alter: true });
```
> **Warning**: `realm.sync({ force: true })` will drop all existing tables. Use with extreme caution, and never in production!
### Raw Queries
#### `realm.query(sql, values, options)`
Execute a raw SQL query against the database.
```js
const result = await realm.query('SELECT * FROM posts WHERE id = ?', [1]);
console.log(result.rows); // => [{ id: 1, title: '...', ... }]
```
See [Raw Queries]({{ '/raw-query' | relative_url }}) for more details.
#### `realm.raw(sql)`
Create a `Raw` SQL expression that won't be escaped.
```js
await Post.update({ title: 'New Title' }, {
updatedAt: realm.raw('NOW()'),
});
```
#### `realm.escape(value)`
Escape a value for safe use in SQL queries.
```js
const safe = realm.escape("O'Reilly");
// => "'O\\'Reilly'"
```
### Transactions
#### `realm.transaction(callback)`
Start a transaction. The callback receives a `{ connection }` object that can be used to ensure all queries within the transaction use the same connection.
```js
await realm.transaction(async ({ connection }) => {
await Post.create({ title: 'Hello' }, { connection });
await Comment.create({ postId: 1, content: 'World' }, { connection });
});
```
See [Transactions]({{ '/transactions' | relative_url }}) for more details.
### Multiple Database Instances
You can create multiple `Realm` instances to connect to different databases:
```js
const realmA = new Realm({
dialect: 'mysql',
database: 'app_primary',
models: [User, Post],
subclass: true, // isolate models
});
const realmB = new Realm({
dialect: 'postgres',
database: 'app_analytics',
models: [Event, Metric],
subclass: true, // isolate models
});
await realmA.connect();
await realmB.connect();
```
> **Important**: When using multiple `Realm` instances, set `subclass: true` to ensure models from different realms don't share the same `Bone` base class internals.
### Properties
| Property | Type | Description |
|---------------|-----------|--------------------------------------|
| `realm.Bone` | `class` | The base model class for this realm |
| `realm.models`| `object` | Map of registered model names to classes |
| `realm.driver`| `object` | The database driver instance |
| `realm.connected` | `boolean` | Whether the realm is connected |
| `realm.DataTypes` | `object` | Data type constructors |
## Best Practices
### Avoiding the N+1 Query Problem
The N+1 query problem occurs when you load a list of records and then make a separate query for each record's associations.
#### The Problem
```js
// BAD: 1 query for posts + N queries for comments
const posts = await Post.find({ authorId: 1 });
for (const post of posts) {
post.comments = await Comment.find({ postId: post.id });
}
```
#### The Solution: Eager Loading
Use `.with()` or `.include()` to load associations in a single query:
```js
// GOOD: 1 query with JOINs
const posts = await Post.find({ authorId: 1 }).with('comments');
for (const post of posts) {
console.log(post.comments); // Already loaded
}
```
You can load multiple associations at once:
```js
const posts = await Post.find().with('author', 'comments');
```
### Selecting Only Needed Columns
By default, Leoric selects all columns (`SELECT *`). When you only need specific columns, use `.select()`:
```js
// BAD: loads all columns including large text fields
const posts = await Post.find();
// GOOD: only loads what you need
const posts = await Post.find().select('id', 'title', 'createdAt');
```
This is especially important for tables with large `TEXT` or `BLOB` columns.
### Batch Processing for Large Tables
When processing large tables, avoid loading all records into memory at once. Use pagination:
```js
// BAD: loads everything into memory
const allPosts = await Post.find();
// GOOD: process in batches
const pageSize = 100;
let offset = 0;
while (true) {
const posts = await Post.find().limit(pageSize).offset(offset);
if (posts.length === 0) break;
for (const post of posts) {
// process each post
}
offset += pageSize;
}
```
### Connection Pool Management
#### Configure Idle Timeout
For long-running applications, configure the idle timeout to prevent stale connections:
```js
const realm = new Realm({
host: 'localhost',
database: 'my_app',
idleTimeout: 30000, // 30 seconds
});
```
#### Disconnect on Shutdown
Always disconnect when your application shuts down:
```js
process.on('SIGTERM', async () => {
await realm.disconnect();
process.exit(0);
});
```
### Transaction Best Practices
#### Keep Transactions Short
```js
// BAD: external API call inside transaction holds connection
await Bone.transaction(async ({ connection }) => {
const user = await User.create({ name: 'Alice' }, { connection });
const result = await fetch('https://api.example.com/notify'); // Slow!
await AuditLog.create({ action: 'user_created' }, { connection });
});
// GOOD: move external calls outside the transaction
const user = await Bone.transaction(async ({ connection }) => {
const user = await User.create({ name: 'Alice' }, { connection });
await AuditLog.create({ action: 'user_created' }, { connection });
return user;
});
await fetch('https://api.example.com/notify');
```
#### Use Generator Functions for Cleaner Code
```js
// Generator functions auto-pass the connection
await Bone.transaction(function* () {
const user = yield User.create({ name: 'Alice' });
yield AuditLog.create({ action: 'user_created', userId: user.id });
});
```
### Indexing Strategies
#### Use Composite Indexes for Common Query Patterns
If you frequently query with multiple conditions:
```js
// If this is a common query pattern:
Post.find({ authorId: 1, status: 'published' }).order('createdAt', 'desc')
```
Consider adding a composite index: `(author_id, status, created_at)`.
#### Use Index Hints When Needed
When the query optimizer makes a suboptimal choice:
```js
Post.find({ authorId: 1 }).forceIndex('idx_author_created')
```
See [Index Hints]({{ '/index-hints' | relative_url }}) for more details.
### Model Organization
#### Use Directory-Based Model Loading
```js
const realm = new Realm({
models: 'app/models', // Auto-loads all models from directory
});
```
#### Define Associations in `initialize()`
```js
class Post extends Bone {
static initialize() {
this.belongsTo('author', { Model: 'User' });
this.hasMany('comments');
this.hasMany('tags', { through: 'tagMaps' });
}
}
```
#### Use TypeScript Decorators When Possible
```ts
class Post extends Bone {
@Column({ primaryKey: true })
id: bigint;
@BelongsTo()
author: User;
@HasMany()
comments: Comment[];
}
```
### Security
#### Never Use Raw SQL with User Input
```js
// BAD: SQL injection vulnerability
await realm.query(`SELECT * FROM posts WHERE title = '${userInput}'`);
// GOOD: parameterized query
await realm.query('SELECT * FROM posts WHERE title = ?', [userInput]);
// GOOD: use the ORM query interface
await Post.find({ title: userInput });
```
#### Use `raw()` Sparingly
The `raw()` function bypasses escaping. Only use it for SQL functions and expressions, never for user-provided values:
```js
// GOOD: SQL function
await Post.update({ id: 1 }, { viewCount: raw('view_count + 1') });
// BAD: user input in raw()
await Post.find({ title: raw(userInput) }); // SQL injection!
```
## Troubleshooting
### Connection Issues
#### `Error: connect ECONNREFUSED ::1:3306`
This is a common issue on macOS where `localhost` resolves to IPv6 `::1`, but MySQL is only listening on `127.0.0.1`.
**Solution**: Update MySQL config to also bind to IPv6:
```diff
# /usr/local/etc/my.cnf (Homebrew MySQL)
[mysqld]
-bind-address = 127.0.0.1
+bind-address = 127.0.0.1,::1
```
Then restart MySQL:
```bash
brew services mysql restart
```
Alternatively, use `127.0.0.1` instead of `localhost` in your connection config:
```js
const realm = new Realm({
host: '127.0.0.1', // Use IP instead of 'localhost'
database: 'my_app',
});
```
#### `Error: connected already`
This error occurs when calling `connect()` multiple times with the default `Bone` class.
**Solution**: Either:
- Call `connect()` only once in your application lifecycle
- Use separate `Realm` instances with `subclass: true` for multiple connections
```js
// Wrong: calling connect twice
await connect({ models: [Post], database: 'db1' });
await connect({ models: [User], database: 'db2' }); // Error!
// Correct: use separate Realm instances
const realm1 = new Realm({ models: [Post], database: 'db1', subclass: true });
const realm2 = new Realm({ models: [User], database: 'db2', subclass: true });
await realm1.connect();
await realm2.connect();
```
#### `Error: DriverClass must be a subclass of AbstractDriver`
This usually occurs when using `BaseRealm` directly instead of the full `Realm` class, or when the `dialect` option doesn't match an available driver.
**Solution**: Ensure you're importing `Realm` from `leoric` (not `BaseRealm`) and have the correct database client installed:
```bash
# For MySQL
npm install mysql2
# For PostgreSQL
npm install pg
# For SQLite
npm install sqlite3
```
### Model Issues
#### `Error: Model is not paranoid`
This error occurs when calling `restore()` on a model that doesn't have soft delete enabled.
**Solution**: Add a `deletedAt` attribute to your model. See [Soft Delete]({{ '/soft-delete' | relative_url }}).
#### Columns not mapping to attributes
By default, Leoric maps `snake_case` column names to `camelCase` attributes. If your column names don't follow this convention, use the `name` option:
```ts
@Column({ name: 'gmt_create' })
createdAt: Date;
```
#### `createdAt` / `updatedAt` not auto-updating
Leoric automatically manages `createdAt` and `updatedAt` timestamps if the corresponding columns exist. Ensure your table has `created_at` and `updated_at` columns.
To suppress automatic timestamp updates for a specific operation, pass `{ silent: true }`:
```js
await post.update({ title: 'Updated' }, { silent: true });
```
### Query Issues
#### Unexpected results with soft delete
If you're not seeing records you expect, they may be soft-deleted. Use `.unscoped` to include all records:
```js
// This excludes soft-deleted records
const posts = await Post.find();
// This includes all records
const allPosts = await Post.unscoped.find();
```
#### N+1 query problem
If you're loading associations in a loop, you likely have an N+1 problem:
```js
// Bad: N+1 queries
const posts = await Post.find();
for (const post of posts) {
const comments = await Comment.find({ postId: post.id }); // N queries!
}
// Good: eager loading
const posts = await Post.find().with('comments'); // 1 query with JOIN
```
See [Best Practices]({{ '/best-practices' | relative_url }}) for more details.
### Debugging
#### Enable Debug Logging
Leoric uses the `debug` module. Enable SQL logging with:
```bash
DEBUG=leoric node app.js
```
#### Custom Logger
You can provide a custom logger to see all queries:
```js
const realm = new Realm({
logger: {
logQuery(sql, duration) {
console.log(`[${duration}ms] ${sql}`);
},
logQueryError(err, sql, duration) {
console.error(`[${duration}ms] ${sql}\n Error: ${err.message}`);
},
logMigration(name) {
console.log(`Migration: ${name}`);
},
},
});
```
See [Logging]({{ '/logging' | relative_url }}) for more details.
### TypeScript Issues
#### `emitDecoratorMetadata` error
If decorator type inference is not working, ensure your `tsconfig.json` has:
```json
{
"compilerOptions": {
"experimentalDecorators": true,
"emitDecoratorMetadata": true
}
}
```
#### Type errors with `bigint`
JavaScript's `bigint` type requires special handling. If you encounter type errors:
```ts
// Correct: use bigint type
@Column({ primaryKey: true })
id: bigint;
// When creating, bigint literals use 'n' suffix
const post = await Post.create({ title: 'Hello' });
console.log(typeof post.id); // 'bigint' or 'number' depending on value
```
### Migration Issues
#### Table already exists
When running `realm.sync()`, if a table already exists and you want to update it:
```js
// Alter existing tables (add new columns, etc.)
await realm.sync({ alter: true });
// WARNING: Drop and recreate (data loss!)
await realm.sync({ force: true });
```
#### Migration rollback
If a migration fails partway, you may need to manually rollback:
```js
module.exports = {
async up(driver, DataTypes) {
// forward migration
},
async down(driver, DataTypes) {
// rollback migration - make sure this is complete
},
};
```
## How to Contribute
### Get Started
Three steps:
1. Install databases we intend to support, namely MySQL, PostgreSQL, and SQLite
2. Install `node_modules` (which might take long)
3. Happy hacking
#### Preparing Environment
```bash
$ brew install mysql postgres sqlite
$ brew service start mysql
$ brew service start postgres
```
#### Running Tests
```bash
$ npm install
# prepare table schema, run all tests
$ npm run test
# run unit tests
$ npm run test:unit
# run integration tests
$ npm run test:integration
# run typescript definition tests
$ npm run test:dts
```
To be more specific, we can filter test files and cases:
```bash
$ npm run test -- test/unit/test.connect.js --grep "should work"
$ npm run test:unit --grep "=> Sequelize adapter"
$ npm run test:mysql --grep "bone.toJSON()"
```
If `--grep [pattern]` isn't specific enough, we can always insert `.only`:
```js
describe('=> Spell', function() {
it.only('supports error convention with nodeify', async function() {
// asserts
});
});
```
Please remember to remove them before commit.
### Working on Documentations
The Leoric documentation is served with Github Pages, which requries Jekyll to build. See the [Jekyll on macOS](https://jekyllrb.com/docs/installation/macos/) instructions, refer to the guide [Install Ruby on Mac](https://mac.install.guide/ruby/index.html), or use Moncef Belyamani's [Ruby setup scripts](https://www.moncefbelyamani.com/ruby-script/). If you only intend to use Jekyll, you can [install Ruby with Homebrew](https://mac.install.guide/ruby/13.html) without a version manager. After Ruby is installed, follow instructions to [install Jekyll](https://jekyllrb.com/docs/installation/macos/#install-jekyll).
If your network struggles to connect to https://rubygems.org, consider changing the Ruby Gems source in `docs/Gemfile`:
```diff
diff --git a/docs/Gemfile b/docs/Gemfile
index 4382725..b4dba82 100644
--- a/docs/Gemfile
+++ b/docs/Gemfile
@@ -1,4 +1,4 @@
-source "https://rubygems.org"
+source "https://gems.ruby-china.com"
```
When bundle install completes, you can now build docs locally:
```bash
$ cd docs # if you're not at this directory yet
$ jekyll serve
Configuration file: leoric/docs/_config.yml
Source: leoric/docs
Destination: leoric/docs/_site
Incremental build: disabled. Enable with --incremental
Generating...
GitHub Metadata: No GitHub API authentication could be found. Some fields may be missing or have incorrect data.
done in 3.73 seconds.
Auto-regeneration: enabled for 'leoric/docs'
Server address: http://127.0.0.1:4000/
Server running... press ctrl-c to stop.
```
The documentation will be available at .
### How the code is organized
The code basically breaks into following layers (from bottom to top):
- the SQL parser `lib/expr.js`
- the SQL intermediate representation `lib/spell.js`, which breaks SQL into accessible properties
- the SQL driver `lib/drivers/*.js`, which generates vendor specific SQLs then queries said SQLs
- the Bone `lib/bone.js`, which serve as the base model
- and the optional adapter for sequelize `lib/sequelize.js`
#### SQL Drivers
The model driver provides following abilities:
- Attributes in model definition `lib/drivers/*/attribute.js`
- Data types in model definition `lib/drivers/*/data_types.js`
- SQL formatters that deal with table structure `lib/drivers/*/schema.js`
- SQL formatters that translate Spell, the SQL intermediate representation, into vendor specific SQLs `lib/drivers/*/spellbook.js`
- The exported driver that have them assembled together, `lib/drivers/*/index.js`
## AI Cookbook
This page is a pattern library for AI coding assistants (and humans) who write
code with Leoric. Every snippet below is copy-pasteable and verified against a
real database. If you are an AI agent, prefer copying these patterns verbatim
over inventing new API shapes. A Chinese version is available at
[ai-cookbook (中文)](https://leoric.js.org/zh/ai-cookbook.html).
### Minimal Runnable Skeleton
```js
import Realm, { Bone, DataTypes } from 'leoric';
const { BIGINT, STRING, TEXT } = DataTypes;
class Post extends Bone {
static attributes = {
id: { type: BIGINT, primaryKey: true },
title: { type: STRING, allowNull: false },
content: { type: TEXT },
};
}
const realm = new Realm({
dialect: 'sqlite', // 'mysql' | 'postgres' | 'sqlite'
storage: ':memory:', // or host/user/database for MySQL/PostgreSQL
models: [Post], // register models declared as top-level classes
});
async function main() {
await realm.connect(); // must connect before any query
await realm.sync(); // create tables from model definitions
const post = await Post.create({ title: 'New Post', content: 'Hello' });
post.title = 'Untitled';
await post.save();
const found = await Post.findOne({ title: 'Untitled' });
console.log(found.id, found.content);
await Post.update({ title: 'Untitled' }, { content: 'Updated' });
await Post.remove({ title: 'Untitled' });
}
main();
```
### Model Definition Patterns
#### Attributes
Attributes are declared with `static attributes`. Use `DataTypes` constants or
their string names; `allowNull`, `primaryKey`, `unique`, `defaultValue`,
`autoIncrement` are the most common meta options.
```js
import Realm, { Bone, DataTypes } from 'leoric';
const { STRING, INTEGER, BIGINT, DECIMAL, BOOLEAN, DATE, JSON, TEXT } = DataTypes;
class User extends Bone {
static attributes = {
id: { type: BIGINT, primaryKey: true, autoIncrement: true },
nickname: { type: STRING, allowNull: false, unique: true },
age: { type: INTEGER, defaultValue: 0 },
balance: { type: DECIMAL(10, 2), allowNull: false, defaultValue: 0 },
active: { type: BOOLEAN, defaultValue: true },
lastLoginAt: { type: DATE },
profile: { type: JSON },
};
}
```
#### Associations
Associations are declared inside `static initialize()`. The model name is
resolved by convention (camelCase of the target model name); use the
`className` option when the target name cannot be inferred.
```js
class Shop extends Bone {
static initialize() {
this.hasMany('items');
}
}
class Item extends Bone {
static initialize() {
this.belongsTo('shop');
// custom class name: this.belongsTo('seller', { className: 'User' })
}
}
// eager loading
const shops = await Shop.find().with('items');
console.log(shops[0].items); // => [ Item, ... ]
// hasMany with through
class Post extends Bone {
static initialize() {
this.hasMany('comments');
this.hasMany('commenters', { through: 'comments' });
}
}
```
#### TypeScript Decorators
```ts
import { Bone, BelongsTo, HasMany } from 'leoric';
class Shop extends Bone {
@HasMany()
items: Item[];
}
class Item extends Bone {
@BelongsTo()
shop: Shop;
}
```
### Query Patterns
#### Condition Objects
Plain object conditions map to `WHERE` clauses. Nested objects are treated as
operator conditions when every key is one of `$eq, $gt, $gte, $lt, $lte, $ne,
$in, $nin, $notIn, $like, $notLike, $between, $notBetween`.
```js
Post.find({ title: 'New Post' }); // WHERE title = 'New Post'
Post.find({ title: { $like: '%Post%' } }); // WHERE title LIKE '%Post%'
Post.find({ id: { $gt: 0, $lt: 999999 } }); // WHERE id > 0 AND id < 999999
Post.find({ id: { $in: [1, 2, 3] } }); // WHERE id IN (1, 2, 3)
Post.find({ id: { $between: [1, 10] } }); // WHERE id BETWEEN 1 AND 10
```
#### Chainable Queries
`find()`/`findOne()` return a `Spell` — a lazy, chainable query object. It only
hits the database when awaited or iterated.
```js
const posts = await Post.find({ title: { $like: '%Post%' } })
.with('comments')
.order('id', 'desc')
.limit(10)
.offset(20);
const count = await Post.find({ active: true }).count();
const sum = await Post.sum('views');
const first = await Post.find().order('id').first;
```
#### Raw Queries
```js
const { rows } = await realm.query('SELECT * FROM posts WHERE id = ?', [42]);
const posts = await Post.find(raw('title = "x"')); // or new Raw(...)
```
#### String Conditions
```js
Post.find('title = ? OR title = ?', 'a', 'b');
```
### Transactions and Bulk Operations
```js
import { Bone } from 'leoric';
// async callback — every query must pass { connection }
await Bone.transaction(async ({ connection }) => {
const post = await Post.create({ title: 'New Post' }, { connection });
await Comment.create({ postId: post.id, content: 'First!' }, { connection });
});
// generator — connection is injected into yielded spells automatically
await Bone.transaction(function* () {
const post = yield Post.create({ title: 'New Post' });
yield Comment.create({ postId: post.id, content: 'First!' });
});
// from a realm instance
await realm.transaction(async ({ connection }) => {
await Post.create({ title: 'Hello' }, { connection });
});
// bulk create / upsert
await Post.bulkCreate([{ title: 'a' }, { title: 'b' }, { title: 'c' }]);
await Post.upsert({ title: 'a' }); // upsert on unique key conflict
```
### Sequelize Compatibility Mode
Set `sequelize: true` to activate the Sequelize adapter and get a
Sequelize-like API, easing migration from Sequelize:
```js
const realm = new Realm({
dialect: 'sqlite',
storage: '/tmp/leoric.sqlite3',
sequelize: true, // turn on the sequelize adapter
});
await realm.connect();
// in sequelize mode, define models by name + attributes (or extend realm.Bone)
const Shop = realm.define('Shop', {
id: { type: BIGINT, primaryKey: true, autoIncrement: true },
name: { type: STRING, allowNull: false },
credit: { type: INTEGER, defaultValue: 0 },
});
await realm.sync();
```
#### CRUD
```js
// create
await Shop.create({ name: 'MILL' });
await Shop.bulkCreate([{ name: 'wagas' }, { name: 'family mart' }]);
await new Shop({ name: "McDonald's" }).save();
// read
const shop = await Shop.findOne({ where: { name: 'MILL' } });
const shops = await Shop.findAll({
attributes: [ 'id', 'name' ],
where: { name: { $like: '%M%' } },
order: [[ 'id', 'desc' ]],
limit: 10,
});
const byPk = await Shop.findByPk(1);
// find or create — returns [instance, created]
const [brewhouse, created] = await Shop.findOrCreate({
where: { name: 'Shanghai Brewhouse' },
});
// update — sequelize semantics: Model.update(values, { where }), returns affected count
const affected = await Shop.update({ credit: 10 }, { where: { name: 'MILL' } });
// delete / aggregate
await Shop.destroy({ where: { name: 'wagas' } });
await Shop.increment({ credit: 5 }, { where: { name: 'MILL' } }); // or increment('credit', { where }) for +1
const { count, rows } = await Shop.findAndCountAll({ where: { name: { $like: '%M%' } } });
const total = await Shop.count();
```
Notes:
- In sequelize mode, `update` uses Sequelize semantics (`values`, `{ where }`), not the Leoric order.
- Prefer a file-based database when combining `sequelize: true` with SQLite — `:memory:` databases are per-connection, and queries that run concurrently (e.g. `findAndCountAll`) can hit a fresh connection with no tables.
- See [Sequelize Adapter](https://leoric.js.org/sequelize.html) for the full compatibility matrix.
### Common Errors and Fixes
| Symptom | Cause | Fix |
|---|---|---|
| `model X is not connected yet` | Query before `realm.connect()` | `await realm.connect()` first |
| `ER_NO_SUCH_TABLE` | Tables not created yet | `await realm.sync()` (or `{ force: true }` to recreate) |
| Queries in transaction run outside it | Missing `{ connection }` option | Pass `{ connection }` to every query in the callback, or use a generator function |
| N+1 queries | Lazy association access in a loop | Use `.with('assoc')` eager loading |
| `X must extend this realm's Bone` | Model class from another realm/instance | Use the same `Realm` instance's `realm.define()` |
| Attribute shadowing by class field | ES class field shadows Leoric accessor | Use `declare`, `@Model()`, or define attributes via `realm.define(Model, attributes)` |
### Prompting Template for AI Assistants
When asking an AI assistant to write Leoric code, include these three things:
1. The database dialect and connection info (or say "use sqlite in-memory").
2. The model definitions (copy them verbatim from your code).
3. The desired behavior in terms of the data, not the API.
Example prompt:
> Using Leoric with a MySQL database, given these models:
> ```js
> class Post extends Bone {
> static initialize() {
> this.belongsTo('author', { className: 'User' });
> this.hasMany('comments');
> }
> }
> ```
> Write code that fetches the 10 most recent posts with their authors and
> comment counts, without N+1 queries.
Also point the assistant at this page or at
[llms-full.txt](https://leoric.js.org/llms-full.txt) for the full API context.
## Design: ES2022 Class Fields and Model Compilation
## ES2022 Class Fields and Model Compilation
### Status
**Status:** Implemented on the model-compilation branch
This document records the target design for supporting ES2022 class-field
semantics without putting every model instance behind a `Proxy`.
This design supersedes the earlier model-finalization experiment. It does not
repair class fields after every construction.
### Problem
Leoric exposes mapped attributes through accessors on a model prototype. Before
ES2022, TypeScript commonly emitted class fields as assignments:
```js
this.name = undefined;
```
An assignment invokes an inherited setter. With ES2022 `Define` semantics, the
same field is initialized as an own property:
```js
Object.defineProperty(this, 'name', {
value: undefined,
writable: true,
enumerable: true,
configurable: true,
});
```
The own property shadows Leoric's accessor. The JavaScript value and Leoric's
raw attribute storage can then disagree.
An instance `Proxy` can intercept this operation, but benchmarks show a material
cost on the hottest paths: approximately 3.45x for getters and 3.72x for
setters. The preferred design therefore keeps ordinary model instances and
makes compatibility an explicit, one-time model operation.
### Terminology
The one-time replacement process is called **model compilation**.
```ts
compileModel(Definition, Base, attributes)
```
Model compilation turns a declarative class into the actual runtime model. It
creates a fresh subclass of the appropriate `Bone`, copies the supported class
footprint, initializes mapped attributes, and returns the compiled class. It
does not execute the definition class's constructor.
The public model paths are:
- **Generated model:** `realm.define('User', attributes)`
- **Direct model:** `class User extends Bone` with `declare` fields
- **Compiled model:** `@Model()` or `realm.define(Class, attributes)`
### Design
#### 1. Generated models
```ts
const User = realm.define('User', {
name: STRING,
});
```
This path already has all information needed to create a subclass of
`realm.Bone` and initialize its attributes. It does not involve a user-defined
constructor or class fields, so no compatibility work is required.
#### 2. Direct models
```ts
class User extends Bone {
@Column()
declare name: string;
}
```
This is the preferred TypeScript path. `declare` gives TypeScript the field type
but emits no runtime field, leaving Leoric's prototype accessor visible.
A direct model is used as-is:
- no `Proxy`;
- no replacement constructor;
- normal inheritance and `instanceof` behavior;
- no per-instance class-field repair;
- registration or `connect()` initializes and marks the concrete model ready.
Every concrete leaf model must have its own readiness marker. Readiness must not
be inherited accidentally:
```ts
class User extends Bone {
declare name: string;
}
class Admin extends User {
declare role: string;
}
```
Registering `User` does not implicitly register `Admin`. Constructing or using
`Admin` before it is registered must produce a model-definition error.
##### Guarding accidental runtime fields
This declaration is unsafe under ES2022 semantics:
```ts
class User extends Bone {
name!: string;
}
```
There is no reliable reflection API that can distinguish it from `declare
name: string` before construction. Runtime fields are instance operations and
do not appear in `User.prototype` descriptors. Constructing a probe at model
registration time is also unsafe because it would execute user constructors and
field initializers.
The guard therefore has two complementary parts:
1. A lint rule reports mapped fields that emit runtime class fields. This is the
earliest and most complete diagnostic for users and coding agents.
2. Leoric validates the first ORM-managed instance of each direct model. If a
mapped attribute is an own property, it throws a prescriptive error and does
not attempt repair. Successful validation is cached per model.
Suggested diagnostic:
```text
User.name is emitted as an ES class field and shadows Leoric's attribute
accessor. Add `declare`, decorate User with `@Model()`, or define it through
`realm.define(User, attributes)`.
```
`Bone` cannot perform this check synchronously in its constructor: derived class
fields run only after `super()` returns. A direct `new User()` cannot therefore
be rejected at the exact field definition without a wrapper or `Proxy`; the
lint rule and the next ORM-managed boundary provide the guard.
#### 3. Compiled models
Model compilation is the compatibility layer for definitions that emit regular
ES2022 fields. It is available through both TypeScript decorators and
`realm.define(Class, attributes)`.
##### Decorator entry point
```ts
@Model()
class User extends Bone {
@Column()
name!: string;
get displayName() {
return this.name.toUpperCase();
}
}
```
The decorator returns the compiled constructor. TypeScript continues to expose
the binding using the declared `User` type, including inherited `Bone` APIs and
declared attributes.
##### Realm entry point
```ts
const User = realm.define(
class User extends Bone {
name!: string;
get displayName() {
return this.name.toUpperCase();
}
},
{
name: STRING,
},
);
```
The class overload continues to require a subclass of the realm's `Bone`.
Automatic base-class wiring for an arbitrary class is not part of this design.
Both the decorator and realm entry points call the same model compiler.
##### Compilation behavior
Conceptually, compilation performs the following work once:
```ts
function compileModel(Definition, Base, attributes) {
class CompiledModel extends Base {}
copyPrototypeDescriptors(Definition, CompiledModel);
copySupportedStaticDescriptors(Definition, CompiledModel);
transferModelMetadata(Definition, CompiledModel);
initializeAttributes(CompiledModel, attributes);
markModelReady(CompiledModel);
return CompiledModel;
}
```
The resulting chain is ordinarily:
```text
instance -> CompiledModel.prototype -> Bone.prototype
```
The original constructor is never invoked. Consequently its emitted
`defineProperty(this, 'name', ...)` operation never runs, and the mapped
accessor installed on `CompiledModel.prototype` remains effective. There is no
per-instance rewriting from `define` to `set`.
The supported class footprint includes:
- public methods, getters, setters, and symbol-named prototype members;
- supported public static configuration;
- column, association, validation, and hook metadata;
- the model name and relevant initialization options.
The initial contract excludes or constrains:
- custom instance constructors;
- private instance fields and methods;
- ordinary non-mapped instance field initializers;
- defaults expressed as class-field initializers;
- static private state;
- method behavior whose lexical `super` target is incompatible with the
compiled base.
Mapped defaults belong in attribute metadata. Unsupported class constructs
should produce diagnostics where they can be identified reliably.
##### Identity
Compilation returns a different constructor:
```ts
class UserDefinition extends Bone {}
const User = realm.define(UserDefinition, attributes);
User !== UserDefinition;
new User() instanceof UserDefinition; // false
```
The inline form avoids ambiguity because the public binding receives the
compiled class:
```ts
const User = realm.define(class User extends Bone {}, attributes);
new User() instanceof User; // true
```
The same principle applies to `@Model()`: the decorated class binding refers to
the replacement returned by the decorator.
### Inheritance
A compiled child model extends an already ready runtime parent when one exists.
If it inherits through unready definition classes, compilation flattens that
definition segment onto the nearest ready runtime base and copies supported
descriptors and metadata from base to leaf. This skips fields and constructors
throughout the definition segment. Consequently, instances are not
`instanceof` detached intermediate definition classes.
Each concrete child still requires explicit registration or compilation. This
prevents a raw subclass from inheriting a parent's readiness marker and silently
using uninitialized metadata.
Inheritance details involving copied methods and lexical `super` need focused
tests before the compiler contract is considered stable.
### Performance Requirements
The design is intended to preserve direct-model performance:
- direct models perform no per-instance field scan after their one-time guard;
- compiled models perform descriptor and metadata work once;
- neither path uses an instance `Proxy`;
- neither path repairs mapped fields after each construction;
- hot getter and setter performance should remain within benchmark noise of a
direct `Bone` subclass;
- construction and row hydration should be compared separately.
The benchmark suite must cover generated, direct, and compiled models, including
construction with values, row hydration, hot getters, and hot setters.
On Node.js 22.21.1 on Apple Silicon, compiling a fresh definition measured about
2.67 microseconds through `@Model()` and 2.78 microseconds through
`realm.define(Class)`, compared with 0.61 microseconds to create and mark a
direct class. This roughly 4.4-4.6x ratio is a one-time cost of about two
microseconds per model, not an instance cost. A conservative steady-state run
measured compiled construction within 3% of direct construction, hydration
within 1%, and no getter or setter regression. These findings reinforce
`declare` as the default TypeScript recommendation while keeping compilation as
an inexpensive compatibility path.
### Compatibility Test Matrix
The class-field contract is verified at three levels:
- focused unit tests cover model readiness, class-field diagnostics, successful
guard caching, compilation, inheritance, metadata, identity, and both
`@Model()` and `realm.define(Class)` entry points;
- TypeScript fixtures are compiled and executed with explicit `tsconfig` files
for ES2022 `Define` semantics and legacy assignment semantics;
- native JavaScript class fields are evaluated directly by each Node.js runtime
in the CI matrix, without passing through the repository's `ts-node` hook.
The emitted/runtime fixtures must verify the unsafe direct path, the preferred
`declare` path, and both compilation entry points. This prevents simulated
class fields from standing in for the compiler and runtime behavior users
actually receive.
### Implementation Order
The design should be implemented and reviewed in the following slices:
1. Define readiness and direct-model registration semantics.
2. Add the accidental-class-field diagnostic and one-time runtime guard.
3. Preserve `realm.define('Name', attributes)` as the generated-model path.
4. Implement the shared `compileModel()` contract and footprint copying.
5. Route `realm.define(Class, attributes)` through model compilation.
6. Route `@Model()` through model compilation while preserving TypeScript's
public class type.
7. Define and test compiled-model inheritance, identity, metadata, and `super`
behavior.
8. Add compatibility documentation, migration guidance, and lint guidance.
9. Run the full benchmark matrix and publish results with the implementation.
### Non-Goals
- Keeping an instance `Proxy` as the default compatibility mechanism.
- Silently repairing direct models that use unsafe runtime fields.
- Inferring arbitrary runtime fields from prototype descriptors.
- Executing a user constructor merely to discover its class fields.
- Automatically turning any unrelated class into a realm-specific `Bone`
subclass.
## Design: TypeScript Migration
## TypeScript Migration Guide
This document tracks the ongoing effort to make Leoric a fully TypeScript-native library, removing legacy JavaScript design decisions introduced before or during the 2.14.x migration.
### Background
The `master` branch completed the TypeScript migration in 2.14.x: all source files under `src/` are now `.ts`. However several design decisions carried over from the JavaScript era remain and should be addressed incrementally.
### Issues and Status
#### 1. Remove `allowJs` from `tsconfig.json`
**Status:** Done (2026-03-06)
`allowJs: true` was needed during the migration to let TypeScript process any remaining `.js` source files. Since `src/` is now fully `.ts`, this flag is unnecessary.
```diff
- "allowJs": true,
"allowSyntheticDefaultImports": true,
```
---
#### 2. Enable `strict: true`
**Status:** Done (2026-03-06)
`strict: true` replaces the previous standalone `strictNullChecks: true` and additionally enables `noImplicitAny`, `strictFunctionTypes`, `strictBindCallApply`, `strictPropertyInitialization`, `noImplicitThis`, and `alwaysStrict`. The codebase passed with zero new errors.
```diff
- "strictNullChecks": true,
+ "strict": true,
```
---
#### 3. Remove `[key: string]: any` index signature from `AbstractBone`
**Status:** Pending
```ts
// src/abstract_bone.ts
[key: string]: any;
```
This was added to allow `this[attributeName]` lookups inside ORM internals (e.g. `attribute(name)`, `loadAttribute()`). The side-effect is that any subclass also carries this signature, which defeats TypeScript's property checking on model instances entirely.
**Fix:** Replace internal dynamic property accesses with `(this as Record)[name]` and remove the index signature from the public class surface.
---
#### 4. Replace `module.exports = Realm` dual-export
**Status:** Pending
```ts
// src/index.ts
module.exports = Realm; // CJS legacy
export default Realm; // TS export
```
The `module.exports` assignment was needed for bare `require('leoric')` consumers before `"exports"` was added to `package.json`. Now that the `"exports"` field is present and the `main` field points to `lib/index.js`, the CJS assignment is redundant and causes the emitted `module.exports` to shadow the TypeScript-declared default export, creating `.default` confusion for mixed CJS/ESM consumers.
**Fix:** Remove `module.exports = Realm` and verify the `"exports"` field in `package.json` handles all entry-point cases.
---
#### 5. Type the `Object.assign(Realm, ...)` static augmentation
**Status:** Pending
```ts
// src/index.ts
Object.assign(Realm.prototype, migrations);
Object.assign(Realm, { connect, disconnect, Bone, ... });
```
These runtime augmentations are invisible to TypeScript. Consumers who import `Realm` directly see an incomplete type surface.
**Fix:** Use interface/namespace merging or incorporate the members directly into the class, then export a properly typed `Realm`.
---
#### 6. Fix `InitOptions.hooks` union type
**Status:** Pending
```ts
// src/abstract_bone.ts
hooks?: {
[key in BeforeHooksType]: (options: QueryOptions) => Promise
} | {
[key in AfterHooksType]: (instance: AbstractBone, result: object) => Promise
};
```
The union forces hooks to be either all-before or all-after. Mixed hook objects are valid at runtime but rejected by the type.
**Fix:**
```ts
hooks?: Partial<
{ [K in BeforeHooksType]: (options: QueryOptions) => Promise } &
{ [K in AfterHooksType]: (instance: AbstractBone, result: object) => Promise }
>;
```
---
#### 7. Move `src/types/common.d.ts` to a `.ts` file
**Status:** Done (2026-03-06)
`src/types/common.d.ts` was a hand-authored declaration file sitting alongside TypeScript source. The `copy-dts` npm script manually rsynced it (and any other `src/**/*.d.ts` files) to `lib/`. Hand-maintained `.d.ts` files diverge from runtime behaviour silently — and in fact the file contained several bugs that were only caught when the file was actually compiled as `.ts`:
- `Pool` was declared as a `class` with an `async` method signature, which is invalid syntax in a type declaration. Converted to `interface`.
- `Connection.query` had two overloads where the second had an optional `values` followed by a required `opts` — TypeScript correctly rejected this as "a required parameter cannot follow an optional parameter". Collapsed into a single generic signature. Also tightened `opts` to `spell?: Spell` to match the concrete driver connection implementations.
- `declare class Attribute` re-declared the real `Attribute` class from `src/drivers/abstract/attribute.ts`. Removed. Updated `src/setup_hooks.ts` to import `Attribute` directly from the real source.
- `OrderOptions` and `GeneratorReturnType` were declared but never exported or used. Removed.
- `TransactionMethodOptions` was declared but never exported or used. Removed.
With `common.d.ts` converted to `common.ts`, `tsc` now emits `lib/types/common.d.ts` automatically. The `copy-dts`, `copy-dts:browser` scripts and the `pretest` reference to `copy-dts` were all removed from `package.json`.
```diff
- "copy-dts": "mkdir -p lib && cd src && rsync -R ./**/*.d.ts ../lib && cd -",
- "copy-dts:browser": "mkdir -p dist && cd src && rsync -R ./**/*.d.ts ../dist && cd -",
- "prepack": "tsc && npm run copy-dts",
- "prepack:browser": "rm -rf dist && tsc -p tsconfig.browser.json && npm run copy-dts:browser",
- "pretest": "tsc && npm run copy-dts && ./test/prepare.sh",
+ "prepack": "tsc",
+ "prepack:browser": "rm -rf dist && tsc -p tsconfig.browser.json",
+ "pretest": "tsc && ./test/prepare.sh",
```
---
#### 8. Replace `any[]` for AST condition arrays in `Spell`
**Status:** Pending
```ts
// src/spell.ts
whereConditions: any[];
havingConditions: any[];
```
The expression AST types (`Expr`, `Token`, `Operator`, etc.) already exist in `src/expr.ts`. These fields should use them.
---
#### 9. Update `moduleResolution` to `Node16`
**Status:** Done (2026-03-06)
The legacy `"Node"` resolution algorithm predates the `package.json` `"exports"` field and does not honour it. The correct setting for Node ≥ 16 is `"moduleResolution": "node16"` (or `"nodenext"`), which respects the `"exports"` field for subpath and conditional exports.
In practice, `"moduleResolution"` and `"module"` must be a compatible pair. Attempting `"moduleResolution": "NodeNext"` while keeping `"module": "CommonJS"` is a TypeScript error. The base config `@tsconfig/node18` already sets the correct pair:
```json
{ "module": "node16", "moduleResolution": "node16" }
```
Our local overrides of both settings were simply removed, letting the base take over. Since `package.json` has no `"type": "module"`, TypeScript's Node16 mode treats all `.ts` files as CommonJS — so existing relative imports without file extensions continue to work.
The browser config (`tsconfig.browser.json`) uses `"module": "ESNext"` to target bundlers. Inheriting `moduleResolution: node16` from the base is an invalid combination; it was fixed by adding `"moduleResolution": "Bundler"` — the appropriate pairing for an ESNext/bundler build that also respects `"exports"` fields.
```diff
// tsconfig.json
- "module": "CommonJS",
- "moduleResolution": "Node",
// tsconfig.browser.json
+ "moduleResolution": "Bundler",
```
---
#### 10. Add `.ts` extension to model directory scanner
**Status:** Done (2026-03-06)
The model directory scanner in `src/realm/index.ts` only accepted `.js` and `.mjs` extensions, silently ignoring `.ts` model files when running under `ts-node`, Bun, or any other TypeScript-first runtime.
```diff
- if (entry.isFile() && ['.js', '.mjs'].includes(extname)) {
+ if (entry.isFile() && ['.js', '.mjs', '.ts'].includes(extname)) {
---
### 11. Remove `Spell.nodeify()`
**Status:** Done (2026-03-06)
`nodeify` was a Node.js pre-Promise error-convention bridge (callback with `(err, result)` signature). The minimum Node.js version is 18 and all public APIs are `async`/`await`. The method and its two test cases in `test/unit/spell.test.js` were removed.
---
### 12. Remove redundant `structuredClone` global declaration
**Status:** Done (2026-03-06)
The manual `declare global { function structuredClone... }` in `src/spell.ts` was removed. The original doc note that it was available in `lib.esnext.d.ts` was incorrect: TypeScript only ships `structuredClone` in `lib.dom.d.ts` and `lib.webworker.d.ts`, not in the ESNext Node libs.
The actual fix was to update `@types/node` from `^16.10.1` to `^18.19.130`. Node 18 types include `structuredClone` natively, which is consistent with `"engines": { "node": ">= 18.0.0" }` in `package.json`. This also brings other Node 18 global types into scope.
```diff
- "@types/node": "^16.10.1",
+ "@types/node": "^18.19.130",
```
---
### 13. Strip JSDoc `@param {Type}` annotations from `.ts` files
**Status:** Done (2026-03-06)
163 occurrences of `@param {Type}` (142) and `@returns {Type}` (19) across 18 source files, plus 2 `@typedef {Object}` blocks, were cleaned up:
- `@param {Type} name description` → `@param name description` (type info is already in the TypeScript signature)
- `@returns {Type}` with no description → line removed entirely
- `@returns {Type} description` → `@returns description`
- `@typedef {Object} RawSql` block in `src/browser.ts` — removed (the type is expressed as a TypeScript interface elsewhere)
- `@typedef {Object} QueryResult` block in `src/realm/index.ts` — removed (the type is `QueryResult` in `src/types/common.ts`)
The cleanup was applied across: `src/abstract_bone.ts`, `src/adapters/sequelize.ts`, `src/browser.ts`, `src/collection.ts`, `src/data_types.ts`, `src/drivers/abstract/attribute.ts`, `src/drivers/abstract/index.ts`, `src/drivers/sqlite/sqlstring.ts`, `src/expr.ts`, `src/expr_formatter.ts`, `src/hint.ts`, `src/index.ts`, `src/query_object.ts`, `src/raw.ts`, `src/realm/base.ts`, `src/realm/index.ts`, `src/setup_hooks.ts`, `src/spell.ts`, `src/utils/string.ts`.
---
### 14. Migrate to TC39 Stage 3 decorators (long-term)
**Status:** Pending
```json
"experimentalDecorators": true,
"emitDecoratorMetadata": true
```
TypeScript 5.0 shipped native Stage 3 decorator support. The current implementation uses stage-1 experimental decorators and depends on `reflect-metadata` at runtime. Migrating `@Column`, `@HasMany`, `@HasOne`, `@BelongsTo` to TC39 decorators removes the `reflect-metadata` peer dependency and the `emitDecoratorMetadata` compiler option.
This is a breaking change for consumer code using the decorators.
---
### 15. Replace `invokable` Proxy + `DATA_TYPE as any` with proper factory types (long-term)
**Status:** Pending
```ts
// src/data_types.ts
static INTEGER: DATA_TYPE = INTEGER as any;
```
Every `DataTypes` static member requires `as any` to bridge the class constructor and the callable `AbstractDataType` interface, because the `Proxy`-based `invokable` helper cannot be expressed in TypeScript's type system without losing information.
A typed factory approach (e.g. `createDataType(ctor)`) would eliminate all the `as any` casts on data type statics while preserving the `INTEGER(255)` / `new INTEGER(255)` dual syntax.