Introduction to "Accel Record": A TypeScript ORM Using the Active Record Pattern
Tags: typescript, activerecord, orm, database
Published: 2024-06-04 18:57:50

In this article, we'll briefly introduce [Accel Record](https://www.npmjs.com/package/accel-record), an ORM for TypeScript that we're developing.

## Overview of Accel Record

[accel-record - npm](https://www.npmjs.com/package/accel-record)

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,

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

you can use it like this:

```ts
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.

```ts
// 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}`;
  }
}
```

```ts
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](https://github.com/koyopro/accella/blob/main/packages/accel-record/README-ja.md).

## 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

```ts
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

```ts
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.

```ts
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.

```ts
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.

```ts
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);
```

```ts
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.

```ts
// 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.

```ts
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