Remult - Một CRUD framework cho Fullstack TypeScript (phần 1)
Một Framework đang trong quá trình tìm hiểu...
Remult là một framework CRUD sử dụng các loại model TypeScript của bạn để cung cấp (copy nguyên văn trên Doc):
- Secure REST API (highly configurable)
- Type-safe frontend API client
- Type-safe backend query builder
Ở bài blog này mình sẽ tìm hiểu về cách connect DB, tạo các Entity cũng như tìm hiểu về relation giữa các Entity với nhau.
Theo như Doc của Remult, để tạo connection tới DB của Remult, ta cần đặt nó trong thuộc tính dataProvider
của remult Express middleware. Dưới đây là các ví dụ về kết nối với một số cơ sở dữ liệu thường được sử dụng:
Install knex và mysql2
npm i knex mysql2
Tiếp theo sửa đổi module chính của server API
// index.ts
import express from 'express';
import { remultExpress } from 'remult/remult-express';
import { createKnexDataProvider } from 'remult/remult-knex';
const app = express();
app.use(remultExpress({
dataProvider: createKnexDataProvider({
// Knex client configuration for MySQL
client: 'mysql2',
connection: {
user: 'your_database_user',
password: 'your_database_password',
host: '127.0.0.1',
database: 'test'
}
}, true /* autoCreateTables - entities will be synced with the database. Default: false */)
}));
Install mongodb:
npm i mongodb
// index.ts
import express from 'express';
import { remultExpress } from 'remult/remult-express';
import { MongoClient } from 'mongodb';
import { MongoDataProvider } from 'remult/remult-mongo';
const app = express();
app.use(remultExpress({
dataProvider: async () => {
const client = new MongoClient("mongodb://localhost:27017/local");
await client.connect();
return new MongoDataProvider(client.db('test'), client);
}
}));
Install node-postgres:
npm i pg
npm i --save-dev @types/pg
// index.ts
import express from 'express';
import { remultExpress } from 'remult/remult-express';
import { createPostgresConnection } from 'remult/postgres';
const app = express();
const connectionString = 'postgres://user:password@host:5432/database';
app.use(remultExpress({
dataProvider: () => createPostgresConnection({
connectionString, // Default: process.env.DATABASE_URL
autoCreateTables: true // Entities will be synced with the database. Default: true
})
}));
Một số Field Types trong Remult:
@Fields.dateOnly , @Fields.integer, @Fields.string()
@Fields.string() // string
title = '';
@Fields.integer() // integer
quantity = 0;
@Fields.dateOnly() // time
fromDate?:Date;
Enum Field
@Entity('tasks', {
allowApiCrud: true
})
export class Task extends IdEntity {
@Fields.string()
title = '';
@Fields.boolean()
completed = false;
@Fields.object()
priority = Priority.Low;
}
export enum Priority {
Low,
High,
Critical
}
Json Field
@Entity('tasks', {
allowApiCrud: true
})
export class Task extends IdEntity {
@Fields.string()
title = '';
@Fields.boolean()
completed = false;
@Fields.object()
tags: string[] = [];
}
Hoặc chúng ta cũng có thể convert data lưu vào DB:
import { Field, FieldOptions, Remult } from "remult";
export function CommaSeparatedStringArrayField<entityType = any>(
...options: (FieldOptions<entityType, string[]> |
((options: FieldOptions<entityType, string[]>, remult: Remult) => void))[]) {
return Fields.object({
valueConverter: {
toDb: x => x ? x.join(',') : undefined,
fromDb: x => x ? x.split(',') : undefined
}
}, ...options);
}
@Entity('tasks', {
allowApiCrud: true
})
export class Task extends IdEntity {
@Fields.string()
title = '';
@Fields.boolean()
completed = false;
@CommaSeparatedStringArrayField()
tags: string[] = [];
}
Class Fields
@Entity('tasks', {
allowApiCrud: true
})
export class Task extends IdEntity {
@Fields.string()
title = '';
@Fields.boolean()
completed = false;
@Field<Task, Phone>(() => Phone, {
valueConverter: {
fromJson: x => x ? new Phone(x) : undefined!,
toJson: x => x ? x.phone : undefined!
}
})
phone?: Phone;
}
export class Phone {
constructor(public phone: string) { }
call() {
window.open("tel:" + this.phone);
}
}
Tiếp theo chúng ta sẽ đi tìm hiểu cách tạo Entity và relation giữa chúng:
- Cách tạo Entity: một entity sẽ bao gồm các thuộc tính của nó
@Entity('tasks', {
allowApiCrud: true
})
export class Task extends IdEntity {
@Fields.string()
title = '';
@Fields.boolean()
completed = false;
@BackendMethod({ allowed: true })
async toggleCompleted() {
this.completed = !this.completed;
console.log({
title: this.title,
titleOriginalValue: this.$.title.originalValue
})
await this.save();
}
}
- Relation giữa các Entities:
1. Many to One Relationship
Chúng ta sẽ tạo 1 Category
entity:
import { Entity, Field, IdEntity } from "remult";
import { Task } from "./Task";
@Entity('categories', {
allowApiCrud: true
})
export class Category extends IdEntity {
@Fields.string()
name = '';
}
Và chúng ta sẽ sử dụng nó như một Field in Task
entity:
import { Entity, Field, IdEntity } from "remult";
import { Category } from "./Category";
@Entity('tasks', {
allowApiCrud: true
})
export class Task extends IdEntity {
@Fields.string()
title = '';
@Fields.boolean()
completed = false;
@Field(() => Category)
category?: Category;
}
2. One to Many Relationship
VD: Chúng ta muốn lưu lịch sử của các Task. Vì vậy chúng ta sẽ tạo ra 1 HistoryRecord
entity để phục vụ điều đó:
import { Entity, IdEntity } from "remult";
@Entity('historyRecords', {
allowApiCrud: true
})
export class HistoryRecord extends IdEntity {
@Fields.string()
taskId: String;
@Fields.string()
event: String;
}
Mỗi đối tượng HistoryRecord
sẽ tham chiếu Task
tương ứng của nó theo taskId.
Bây giờ, chúng ta có thể cập nhật Task
để bao gồm một mảng các đối tượng HistoryRecord
khi nó được load:
import { Entity, Field, IdEntity } from "remult";
import { Category } from "./Category";
import { HistoryRecord } from "./HistoryRecord";
@Entity('tasks', {
allowApiCrud: true
})
export class Task extends IdEntity {
@Fields.string()
title = '';
@Fields.boolean()
completed = false;
@Field(() => Category)
category?: Category;
@Fields.object<Task>((options, remult) => {
options.serverExpression = async (task) =>
remult.repo(HistoryRecord).find({ where: { taskId: task.id } })
})
history: HistoryRecord[]
}
options.serverExpression
là một hàm có thể resolve một Field từ bất kỳ đâu. Trong ví dụ này, chúng tôi chỉ đơn giản gọi bảng HistoryRecord
để giải quyết tất cả các bản ghi trong đó taskId
khớp với id của Task
đang được giải quyết. Mặc dù được trang trí bằng @ Fields.object ..., thuộc tính history
sẽ không được lưu trữ trên bảng tasks
trong cơ sở dữ liệu.
Phần này đến đây là kết thúc. Phần sau mình sẽ nói về các method của back-end
Tài liệu tham khảo tại đây: https://remult.dev/