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 Categoryentity:

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.serverExpressionlà 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/