1

Suggest v0.4 spec - TMP (Type Meta Programming) for type safety (succeded to imp...

 1 year ago
source link: https://github.com/typeorm/typeorm/issues/9868
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

Outline

Hello, I'm a lover of TypeORM who has been used it for 7 years.

In today, I suggest TMP (Type Meta Programming) features which can make TypeORM to be type safe. If TypeORM (maybe v0.4) adapts my suggestion, it would be more powerful and convenient than ever. Below list is the expected benefts of my suggestion:

  • When writing SQL Query,
    • Errors would be detected in the compilation level
    • Auto Completion would be provided
    • Type Hint would be supported
  • You can implement App-join very conveniently
  • When SELECTing for JSON conversion
    • App-Join with the related entities would be automatically done
    • Exact JSON type would be automatically deduced
    • The performance would be automatically tuned

Note that, all the features I suggest are, already succeded to implement.

I had implemented those features by myself, as a form of helper library named safe-typeorm. From now on, I'm going to introduce the features of safe-typeorm, and then, I'll suggest those features to be implemented in TypeORM.

Let's defeat challenge from Prisma and make TypeORM great again!

Benefits

Safe Query Builder

JoinQueryBuilder

TypeORM can be much convenient then Prisma

The 1st benefit of my suggestion is, you can construct SQL query builder much type safely, just by taking advantage of the Auto Completions with Type Hints. Also, if you take any mistake when writing the query, the error would be detected in the compilation level.

When you want to run a SQL function about a column, define a tuple type which has the column name and a closure function wrapping the column through a SQL function what you want.

Below is the full source code of the above GIF image:

import * as orm from "typeorm";
import safe from "safe-typeorm";

export function demo_join_query_builder(
    group: BbsGroup,
    exclude: string[],
): orm.SelectQueryBuilder<BbsQuestionArticle> {
    const question = safe.createJoinQueryBuilder(BbsQuestionArticle);

    // JOIN
    const article = question.innerJoin("base");
    const content = article.innerJoin("__mv_last").innerJoin("content");
    const category = article.innerJoin("category");

    // SELECT
    article.addSelect("id").addSelect("writer").addSelect("created_at");
    content.addSelect("title").addSelect("created_at", "updated_at");
    article.leftJoin("answer").leftJoin("base", "AA", (answer) => {
        answer.addSelect("writer", "answer_writer");
        answer.addSelect("created_at", "answer_created_at");
        answer
            .leftJoin("__mv_last", "AL")
            .leftJoin("content", "AC")
            .addSelect(
                ["title", (str) => `COALESCE(${str}, 'NONE)`],
                "answer_title",
            );
    });
    content.addOrderBy("created_at", "DESC");

    // WHERE
    article.andWhere("group", group);
    category.andWhere("code", "NOT IN", exclude);
    return question.statement();
}

Json Select Builder

Class Diagram

Automatic transformation with performance tuning

When you want to convert database records to a specific typed JavaScript objects, you had to implement complicate codes that getting ORM class instances from SelectQueryBuilder and then, combinate (transform) them to construct the destionation typed JavaScript objects. Of course, during the implementation process, you have to consider how to join related entities, and also should consider performance tuning.

However, with my suggestion (safe-typeorm), you don't need to perform such complicate implementation by yourself. Just list up neighborhood entities to join, then you can get the specific typed JavaScript objects.

Its name is JsonSelectBuilder, and joining related entities and performance tuning would be automatically done. Of course, JsonSelectBuilder is composed by TMP (Type Meta Programming), too. Therefore, auto-completion and type hints would be provided. If you take a mistake constructing the JsonSelectBuilder, the error would be detected in the compilation level.

import safe from "safe-typeorm";

// AUTOMATIC TYPE CONVERSION
// AUTOMATIC PERFORMANCE TUNING
export async function demo_app_join_builder(
    groups: BbsGroup[],
): Promise<IBbsGroup[]> {
    const builder = new safe.JsonSelectBuilder(BbsGroup, {
        articles: new safe.JsonSelectBuilder(BbsArticle, {
            group: safe.DEFAULT,
            category: new safe.JsonSelectBuilder(BbsCategory, {
                parent: "recursive" as const,
            }),
            tags: new safe.JsonSelectBuilder(
                BbsArticleTag,
                {},
                (tag) => tag.value, // OUTPUT CONVERSION BY MAPPING
            ),
            contents: new safe.JsonSelectBuilder(BbsArticleContent, {
                files: "join" as const,
            }),
        }),
    });
    return builder.getMany(groups);
}

Insert Collection

Safe massive insertion

When inserting multiple records of multiple entities, you have to consider their dependency relationships. If you take a mistake, then it would be runtime error.

However, with my suggestion (safe-typeorm), you don't need to consider such complicate dependency relationships. It will automatically analyze dependency relationships and insert records in the right order.

import safe from "safe-typeorm";

async function insert(
    tags: BbsArticleTag[],
    articles: BbsArticle[],
    contents: BbsArticleContent[],
    groups: BbsGroup[],
    contentFiles: BbsArticleContentFile[],
    categories: BbsCategory[],
    files: AttachmentFile[],
): Promise<void> {
    // although you've pushed entity records 
    // without considering dependency relationships
    const collection: safe.InsertCollection = new safe.InsertCollection();
    collection.push(tags);
    collection.push(articles);
    collection.push(contents);
    collection.push(groups);
    collection.push(contentFiles);
    collection.push(categories);
    collection.push(files);

    // `InsertCollection` would automatically sort insertion order
    // just by analyzing dependency relationships by itself
    await collection.execute();
}

Break Changes

Only difference between TypeORM and safe-typeorm is, how to define the relationships. If TyperORM can accept the only one difference, then it can get adtantages of my suggestion.

Look at the below example code and consider that, such difference can be acceptable or not.

import safe from "safe-typeorm";
import * as orm from "typeorm";

import { BbsArticleContent } from "./BbsArticleContent";
import { BbsArticleTag } from "./BbsArticleTag";
import { BbsCategory } from "./BbsCategory";
import { BbsComment } from "./BbsComment";
import { BbsGroup } from "./BbsGroup";
import { __MvBbsArticleLastContent } from "./__MvBbsArticleLastContent";

@orm.Index(["bbs_group_id", "bbs_category_id", "created_at"])
@orm.Entity()
export class BbsArticle extends safe.Model {
    /* -----------------------------------------------------------
        COLUMNS
    ----------------------------------------------------------- */
    @orm.PrimaryGeneratedColumn("uuid")
    public readonly id!: string;

    @safe.Belongs.ManyToOne(
        () => BbsGroup,
        (group) => group.articles,
        "uuid",
        "bbs_group_id",
        // INDEXED
    )
    public readonly group!: safe.Belongs.ManyToOne<BbsGroup, "uuid">;

    @safe.Belongs.ManyToOne(
        () => BbsCategory,
        (category) => category.articles,
        "uuid",
        "bbs_category_id",
        { index: true, nullable: true },
    )
    public readonly category!: safe.Belongs.ManyToOne<
        BbsCategory,
        "uuid",
        { nullable: true }
    >;

    @orm.Index()
    @orm.Column("varchar")
    public readonly writer!: string;

    @orm.Column()
    public readonly ip!: string;

    @orm.Index()
    @orm.CreateDateColumn()
    public readonly created_at!: Date;

    @orm.DeleteDateColumn()
    public readonly deleted_at!: Date | null;

    /* -----------------------------------------------------------
        HAS
    ----------------------------------------------------------- */
    @safe.Has.OneToMany(
        () => BbsComment,
        (comment) => comment.article,
        (x, y) => x.created_at.getTime() - y.created_at.getTime(),
    )
    public readonly comments!: safe.Has.OneToMany<BbsComment>;

    @safe.Has.OneToMany(
        () => BbsArticleContent,
        (content) => content.article,
        (x, y) => x.created_at.getTime() - y.created_at.getTime(),
    )
    public readonly contents!: safe.Has.OneToMany<BbsArticleContent>;

    @safe.Has.OneToMany(
        () => BbsArticleTag,
        (tag) => tag.article,
        (x, y) => (x.value < y.value ? -1 : 1),
    )
    public readonly tags!: safe.Has.OneToMany<BbsArticleTag>;

    @safe.Has.OneToOne(() => __MvBbsArticleLastContent, (last) => last.article)
    public readonly __mv_last!: safe.Has.OneToOne<
        __MvBbsArticleLastContent,
        true
    >;
}

Conclusion

In nowadays, challenge from Prisma is serious for TypeORM and lots of TypeORM users already had migrated their projects to Prisma. I think that the reason why they had migrated from TypeORM to Prisma is, they want to use the type safe features of Prisma.

If TypeORM adapts my suggestion in v0.4, I think Prisma no more can compete with TypeORM. TypeORM may be the most powerful ORM library in the TypeScript world.

If you have any questions or suggestions, please leave a comment. Thank you.


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK