1

[Safe-TypeORM] AnyORM becomes the real TypeORM

 1 year ago
source link: https://dev.to/samchon/safe-typeorm-make-anyorm-to-be-the-real-typeorm-478n
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.
neoserver,ios ssh client

Summary

logo

Make anyorm to be real typeorm.

safe-typeorm is a helper library of typeorm, enhancing type safety like below:

  • 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
JoinQueryBuilder

AnyORM

The most famous ORM library in TypeScript is typeorm. However, Korean TypeScript backend developers jokingly call the TypeORM as AnyORM. It's because the typeorm does not guarantee type safety, so that cause lots of critical runtime errors.

As you can see from below code, JOIN or SELECT queries are written as a string typed value, and it never be validated in the compilation level. TypeScript developers can identify error only when running the backend server code.

Such type unsafety is the reason why I've developed safe-typeorm, a wrapper library typeorm to make it from AnyORM to be real typeorm.

class User {
    @PrimaryGeneratedColumn()
    id: number;

    @Column()
    name: string;

    @OneToMany(() => Photo, (photo) => photo.user)
    photos: Photo[];
}

const users = await dataSource
    .getRepository(User)
    .createQueryBuilder("user")
    .leftJoinAndSelect("user.photos", "photo") // unsafe
    .andWhere("user.id = :id", { id: 1 }) // fuXXing unsafe
    .getMany();

Prisma

Prisma

When I've completed the safe-typeorm library, another ORM library prisma has been newly inveted and it was much elegant than typeorm. As the prisma ensures perfect type safety and much convenient to use than typeorm, I also moved from typeorm to prisma.

Therefore, no reason to maintain the safe-typeorm, due to I'm not using typeorm more.

Revival

However, unlike my case abandoned typeorm and adapted prisma, many companies have been suffering by AnyORM (typeorm and its type unsafety). One of such company traveled my Github Account (https://github.com/samchon) and unearthed the safe-typeorm. The company requested me like below:

We have been suffering from type unsafety of TypeORM. We had wrapped the TypeORM to our self-developed module for enhancing type safety, but it had been too hard works for us. In nowadays, we've found your safe-typeorm library, and it seems like the solution what we've looked for. Can you write a Guide Documents?

I was a little bit surprised and could hear lots of voices from another companies (using NestJS in South Korea). What they had in common was that they are suffering from AnyORM, but it was too risky to migrate their currently running commercial backend server to prisma. Most of them are dreaming to prisma, but it is not possible by legacy code.

Thus, I urgently wrote a Guide Documents. It took 2 years after the completion of safe-typeorm library that the Guide Documents was created. It is a pleasure story for me that there're some people who want my old libraries, but I also feel regretful that I left it for two years because I couldn't think of such specific requirements.

Enhance Type Safety

JoinQueryBuilder

With safe-typeorm, you can easily construct SQL queries, just by utilzing auto-completion.

If you've taken a mistake during the SQL query construction, compilation error will help you.

  • When writing SQL query,

    • Errors would be detected in the compilation level
    • Auto Completion would be provided
    • Type Hint would be supported
JoinQueryBuilder

AppJoinBuilder

typeorm does not support complicate application level join.

However, safe-typeorm does.

  • You can implement App-join very conveniently
AppJoinBuilder

JsonSelectBuilder

JSON conversion with automatic query construction and performance tuning.

When you want to convert DB records to JSON data with a specific type (maybe DTO), you don't need to write any SQL query like SELECT or JOIN. Just by listing up neighborhoold entities to join and columns to use, safe-typeorm will construct optimal SQL queries and (application level) joining plan by itself.

  • 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

Class Diagram
import safe from "safe-typeorm";

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);
}

Safe Insertions

initialize

When creating a new entity instance, typeorm cannot detect the omission of required properties. However, safe-typeorm supports safe entity instance creation by initialize() function.

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

/**
 * No error when compliation.
 * 
 * However, becomes runtime error due to `writer` property omission.
 */
async function new_bbs_article(group: BbsGroup): Promise<BbsArticle> {
    const article = new BbsArticle();
    await article.group.set(group);
    await article.category.set(null);
    // article.writer = "Samchon";
    article.ip = "127.0.0.1";
    article.created_at = new Date();
    return article;
}

/**
 * Type safe factory function.
 * 
 * Compilation error occurs due to `writer` property omission.
 */
function initialize_bbs_article(group: BbsGroup): BbsArticle {
    return safe.initialize(BbsArticle, {
        group: group,
        category: null,
        // writer: "Samchon",
        ip: "127.0.0.1",
        created_at: new Date(),
        deleted_at: null,
    });
}

InsertCollection

When inserting multiple records of multiple entities, you have to consider their dependency relationships when using typeorm. However, with safe-typeorm, you don't need to consider such complicate dependency relationships. safe-typeorm 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();
}

EntityUtil

With EntityUtil class of safe-typeorm, you can merge duplicated records safely, even if target entity has complicate dependency relationships. The safety would be always keeped even when unique key constraint exists in the dependency relationships.

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

async function unify(
    original: Manufacturer,
    duplicates: Manufacturer[],
): Promise<void> {
    await safe.EntityUtil.unify(original, duplicates);
      // manufacturers would be unified
      // products would be unified
      // product images would be unified
}

@orm.Entity()
class Manufacturer {
    id: string;
    name: string;
}

@orm.Entity()
@orm.Unique(["manufacturer_id", "name"])
class Product {
    id: string;
    manufacturer: safe.Belongs.ManyToOne<Manufacturer, "uuid">;
    name: string;
}

@orm.Entity()
@orm.Unique(["product_id", "name"])
class ProductImage {
    id: string;
    product: safe.Belongs.ManyToOne<Product, "uuid">;
    name: string;
    url: string;
}

Conclusion

When adapting ORM library of TypeScript, I recommend to use prisma.

However, if your legacy project is dependent on typeorm, what about adapt safe-typeorm?


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK