DEV Community

koyopro
koyopro

Posted on

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

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?
}
Enter fullscreen mode Exit fullscreen mode

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();
Enter fullscreen mode Exit fullscreen mode

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}`;
  }
}
Enter fullscreen mode Exit fullscreen mode
import { User } from "./models/index.js";

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

console.log(user.fullName); // => "John Doe"
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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()}`);
Enter fullscreen mode Exit fullscreen mode

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()!;
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode
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
Enter fullscreen mode Exit fullscreen mode

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 });
  }
}
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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

Top comments (0)