Introduction to “Accel Record”: A TypeScript ORM Using the Active Record Pattern

RMAG news

In this article, we’ll briefly introduce Accel Record, an ORM for TypeScript that we’re developing.

Overview of Accel Record

accel-record – npm

Accel Record is a type-safe, synchronous ORM for TypeScript.
It adopts the Active Record pattern, with an interface heavily influenced by Ruby on Rails’ Active Record.

It uses Prisma for schema management and migration, allowing you to use your existing Prisma schema directly.

As of June 2024, it supports MySQL and SQLite, with plans to support PostgreSQL in the future.

Features

Active Record pattern
Type-safe classes
Synchronous API
Validation
Native ESM
Support for MySQL and SQLite

We will introduce some of these features in more detail below.

Usage Example

For example, if you define a User model as follows,

// prisma/schema.prisma
model User {
id Int @id @default(autoincrement())
firstName String
lastName String
age Int?
}

you can use it like this:

import { User } from ./models/index.js;

const user: User = User.create({
firstName: John,
lastName: Doe,
});

user.update({
age: 26,
});

for (const user of User.all()) {
console.log(user.firstName);
}

const john: User | undefined = User.findBy({
firstName: John,
lastName: Doe,
});

john?.delete();

You can also extend models to define custom methods.

// src/models/user.ts
import { ApplicationRecord } from ./applicationRecord.js;

export class UserModel extends ApplicationRecord {
// Define a method to get the full name
get fullName(): string {
return `${this.firstName} ${this.lastName}`;
}
}

import { User } from ./models/index.js;

const user = User.create({
firstName: John,
lastName: Doe,
});

console.log(user.fullName); // => “John Doe”

For more detailed usage, see the README.

Active Record Pattern

Accel Record adopts the Active Record pattern.
Its interface is heavily influenced by Ruby on Rails’ Active Record.
Those with experience in Rails should find it easy to understand how to use it.

Example of Creating and Saving Data

import { NewUser, User } from ./models/index.js;

// Create a User
const user: User = User.create({
firstName: John,
lastName: Doe,
});
console.log(user.id); // => 1

// You can also write it like this
const user: NewUser = User.build({});
user.firstName = Alice;
user.lastName = Smith;
user.save();
console.log(user.id); // => 2

Example of Retrieving Data

import { User } from ./models/index.js;

const allUsers = User.all();
console.log(`IDs of all users: ${allUsers.map((u) => u.id).join(, )}`);

const firstUser = User.first();
console.log(`Name of the first user: ${firstUser?.firstName}`);

const john = User.findBy({ firstName: John });
console.log(`ID of the user with the name John: ${john?.id}`);

const does = User.where({ lastName: Doe });
console.log(`Number of users with the last name Doe: ${does.count()}`);

Type-safe Classes

Accel Record provides type-safe classes.
The query API also includes type information, allowing you to leverage TypeScript’s type system.
Effective editor autocompletion and type checking help maintain high development efficiency.

A notable feature is that the type changes based on the model’s state, so we’ll introduce it here.

Accel Record provides types called NewModel and PersistedModel to distinguish between new and saved models.
Depending on the schema definition, some properties will allow undefined in NewModel but not in PersistedModel.
This allows you to handle both new and saved models in a type-safe manner.

import { User, NewUser } from ./models/index.js;

/*
Example of NewModel:
The NewUser type represents a model before saving and has the following type.

interface NewUser {
id: number | undefined;
firstName: string | undefined;
lastName: string | undefined;
age: number | undefined;
}
*/
const newUser: NewUser = User.build({});

/*
Example of PersistedModel:
The User type represents a saved model and has the following type.

interface User {
id: number;
firstName: string;
lastName: string;
age: number | undefined;
}
*/
const persistedUser: User = User.first()!;

By using methods like save(), you can convert a NewModel type to a PersistedModel type.

import { User, NewUser } from ./models/index.js;

// Prepare a user of the NewModel type
const user: NewUser = User.build({
firstName: John,
lastName: Doe,
});

if (user.save()) {
// If save is successful, the NewModel is converted to a PersistedModel.
// In this block, user is treated as a User type.
console.log(user.id); // user.id is of type number
} else {
// If save fails, the NewModel remains the same type.
// In this block, user remains of type NewUser.
console.log(user.id); // user.id is of type number | undefined
}

Synchronous API

Accel Record provides a synchronous API that does not use Promises or callbacks, even for database access.
This allows you to write simpler code without using await, etc.
This was mainly adopted to enhance application development efficiency.

By adopting a synchronous API, you can perform related operations intuitively, as shown below.

import { User, Setting, Post } from ./models/index.js;

const user = User.first()!;
const setting = Setting.build({ theme: dark });
const post = Post.build({ title: Hello, World! });

// Operations on hasOne associations are automatically saved
user.setting = setting;

// Operations on hasMany associations are also automatically saved
user.posts.push(post);

import { User } from ./models/index.js;

// Related entities are lazily loaded and cached
// You don’t need to explicitly instruct to load related entities when fetching a user.
const user = User.first()!;

console.log(user.setting.theme); // setting is loaded and cached
console.log(user.posts.map((post) => post.title)); // posts are loaded and cached

Synchronous APIs have some drawbacks compared to implementations using asynchronous APIs, primarily related to performance.
We will discuss these trade-offs in a separate article.

Validation

Like Ruby on Rails’ Active Record, Accel Record also provides validation features.

You can define validations by overriding the validateAttributes method of the BaseModel.

// src/models/user.ts
import { ApplicationRecord } from ./applicationRecord.js;

export class UserModel extends ApplicationRecord {
override validateAttributes() {
// Validate that firstName is not empty
this.validates(firstName, { presence: true });
}
}

When using methods like save, validations are automatically executed, and save processing only occurs if there are no errors.

import { User } from ./models/index.js;

const newUser = User.build({ firstName: “” });
// If validation errors occur, save returns false.
if (newUser.save()) {
// If validation errors do not occur, saving succeeds
} else {
// If validation errors occur, saving fails
}

Conclusion

This concludes our brief introduction to Accel Record.
If you are interested, please check the links below for more details.

accel-record – npm
https://www.npmjs.com/package/accel-record