In this article, we'll briefly introduce Accel Record, an ORM for TypeScript that we're developing.
Overview of 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.
MySQL, PostgreSQL, and SQLite are supported.
Features
- Active Record pattern
- Type-safe classes
- Synchronous API
- Validation
- Native ESM
- Support for MySQL, PostgreSQL, 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. 1
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
Seeking a Type-Safe Ruby on Rails in TypeScript, I Started Developing an ORM - DEV Community
https://dev.to/koyopro/seeking-a-type-safe-ruby-on-rails-in-typescript-i-started-developing-an-orm-1of5
Why We Adopted a Synchronous API for the New TypeScript ORM - DEV Community
https://dev.to/koyopro/why-we-adopted-a-synchronous-api-for-the-new-typescript-orm-1jm
Even Server-Side TypeScript Needs the Option to Avoid Asynchronous Processing - DEV Community
https://dev.to/koyopro/even-server-side-typescript-needs-the-option-to-avoid-asynchronous-processing-1opm
Top comments (0)