Skip to content

samchon/safeorm

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

11 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

SafeORM

Outline

Development of the safeorm has not been completed yet.

Until the completion, only interfaces and test automation programs are being provided. When the development has been completed, its 1st version 0.1.0 would be published. Before the completion, version of the safeorm would keep the 0.0.x.

GitHub license npm version Downloads Build Status

npm install --save safeorm

The ultimate Safe ORM library for the TypeScript.

Demonstrations

With the SafeORM, you can define database and programmable type at the same time through the TMP (Type Meta Programming). Also, such type would be utilized in the whole SafeORM components level. Therefore, you can be helped by auto-completion with type hint when writing SQL query or planning App join.

Furthermore, you don't need to worry about any type of mistake when writing SQL query or planning App join. Your mistake would be caught in the compilation level. Therefore, if you type a wrong word on the SQL query, it would be enhanced by IDE through the red underline.

Unlike other ORM libraries who've to define DB and programmable type duplicatedly and cause critical runtime error by not supporting the TMP, SafeORM supports those features through below components and they make the DB development to be much safer. Read below components and feel how safe it is.

Entity

Template Meta Programming

As you can see from the below example code, SafeORM supports the TMP (Type Meta Programming) ORM who can define database type and programmable type at the same time. When you define a column with database type, SafeORM would convert it to the programmable type automatically.

Such automatic conversion even works for the relationship type. If you define a dependency relationship of the database, SafeORM converts it to the programmable relationship type, too. As you can see from the below, relationship functions like Belongs.ManyToOne and Has.ManyToMany define the database relationships and programmable accessors, at the same time.

Also, such Entity types can be utilized in whole components of the SafeORM. For an example, QueryBuilder realizes the auto completion with type hint by analyzing the target Entity types.

import safe from "safeorm";
import { BbsArticleTag } from "./BbsArticleTag";
import { BbsComment } from "./BbsComment";
import { BbsGroup } from "./BbsGroup";

export class BbsArticle
{
    //----
    // COLUMNS
    //----
    // BE `string`
    public readonly id = safe.PrimaryGeneratedColumn("uuid");

    // BE `Belongs.ManyToOne<BbsGroup, { nullable: true }>`
    //
    // `group.id` -> `string|null`
    // `group.get()` -> `Promise<BbsGroup|null>`
    public readonly group = safe.Belongs.ManyToOne
    (
        () => BbsGroup,
        { nullable: true, index: true }
    );

    // BE `string`
    public readonly title = safe.Column("varchar"); 

    // BE `string | null`
    public readonly sub_title = safe.Column("varchar", { nullable: true });
    public readonly title = safe.Column("varchar"); // `string`
    public readonly body = safe.Column("text"); // `string`
    public readonly created_at = safe.CreateDateColumn("datetime"); // `Date`
    public readonly updated_at = safe.UpdateDateColumn("datetime"); // `Date`
    public readonly deleted_at = safe.DeleteDateColumn("datetime"); // `Date | null`

    // BE `number`
    public hit = safe.Column("int");

    //----
    // HAS
    //----
    // BE `Has.OneToMany<BbsArticleTag>`
    //
    // `tags.get() -> Promise<BbsArticleTag>`
    public readonly tags = safe.Has.OneToMany
    (
        () => BbsArticleTag, 
        tag => tag.article
    );

    // BE `Has.OneToMany<BbsComment>`
    public readonly comments = safe.Has.OneToMany
    (
        () => BbsComment, 
        comment => comment.article
    );

    // BE `Has.ManyToMany`
    //
    // `files.get() -> Promise<AttachmentFile>`
    public readonly files = safe.Has.ManyToMany
    (
        () => AttachmentFile,
        () => BbsArticleFile,
        router => router.file,
        router => router.article,
        (x, y) => x.router.sequence - y.router.sequence // SORT FUNCTION
    );
}
safe.Index(BbsArticle, ["created_at", "deleted_at"]); // COMPOSITE INDEX
safe.Index(BbsArticle, "title"); // SINGULAR INDEX
safe.Table(BbsArticle); // BE TABLE

QueryBuilder

Extremely safe with the TMP.

When you've defined some Entities, you can compose SQL query very easily and safely by this QueryBuilder. The QueryBuilder analyzes those Entities and supports the auto completion with type hint.

Also, some mistakes like mis-written column name would be automatically detected in the compilation level. Therefore, you don't need to worry about any type of mistake when wrting the SQL query. All of the mistakes would be enhanced by IDE by the red underline.

Look at the below gif image and feel how strong it is. Other ORM libraries like TypeORM never can provide such beautiful TMP (Type Meta Programming). They may cause the critical runtime error for the mis-writiten SQL query.

Safe Query Builder

export async function test_safe_query_builder(): Promise<void>
{
    const group: BbsGroup = await BbsGroup.findOneOrFail();
    const category: BbsCategory = await BbsCategory.findOneOrFail();

    const stmt: safe.SelectQueryBuilder<BbsQuestionArticle> = safe
        .createJoinQueryBuilder(BbsQuestionArticle, question =>
        {
            question.innerJoin("base", article =>
            {
                article.innerJoin("group");
                article.innerJoin("category");
                article.innerJoin("__mv_last").innerJoin("content");
            });
            question.leftJoin("answer")
                .leftJoin("base", "AA")
                .leftJoin("__mv_last", "AL")
                .leftJoin("content", "AC");
        })
        .andWhere(...BbsArticle.getWhereArguments("group", group))
        .andWhere(...BbsCategory.getWhereArguments("code", "!=", category.code))
        .select([
            BbsArticle.getColumn("id"),
            BbsGroup.getColumn("name", "group"),
            BbsCategory.getColumn("name", "category"),
            BbsArticle.getColumn("writer"),
            BbsArticleContent.getColumn("title"),
            BbsArticle.getColumn("created_at"),
            BbsArticleContent.getColumn("created_at", "updated_at"),

            BbsArticle.getColumn("AA.writer", "answer_writer"),
            BbsArticleContent.getColumn("AA.title", "answer_title"),
            BbsArticle.getColumn("AA.created_at", "answer_created_at"),
        ]);
}

bindAppJoin

Extremely easy but powerful performance.

The bindAppJoin is a function who binds an ORM object (or objects) to perform the lazy application level join. With the lazy app join binding, relationship accessors would never call the repeated SELECT query for the same type. The SELECT query would be occured only when same type of the relationship accessor has been called at fisrt.

If that description is hard to understand, read the below code, then you may understand. As you can see from the below, the example code called relationshiop accessor for only one instance topArticle: BbsArticle. However, another BbsArticle instances also performs the application level join at the same time and never calls the SELECT query again. It's the lazy app join.

Also, as you can see from the below description and benchmark result of the App join, the App join is much fater and consumes much fewer resources than the DB join query. Even implementation code of the App join is much easier than the DB join in this SafeORM library. Therefore, I say with condidence, "There's no reason to denying this bindAppJoin function".

The application level join means that joining related records are done not by the DB join query, but through the manual application code using the HashMap with their primary and foreign key values mapping.

Comparing those DB join query and application level joining, the application level joining consumes much fewer resources and its elapsed time is also much shorter than the DB join query. Those differences grow up whenever the join relationship be more compliate.

Type DB Join App Join
Records 2,258,000 165
Elapsed Time 8.07508 0.00262
export async function test_lazy_app_join(): Promise<void>
{
    const group: BbsGroup = await BbsGroup.findOneOrFail();
    safe.bindAppJoin(group);

    const articleList: BbsArticle[] = await group.articles.get();
    const topArticle: BbsArticle = articleList[0];

    // APP-JOIN WOULD BE DONE
    await topArticle.comments.get();
    await topArticle.files.get();
    await topArticle.tags.get();

    // ANY SELECT QUERY WOULD BE OCCURED
    await must_not_query_anything("bindAppJoin", async () =>
    {
        for (const article of articleList)
        {
            await article.comments.get();
            await article.files.get();
            await article.tags.get();
        }
    });
}

AppJoinBuilder

With the AppJoinBuilder class, you can implement eager application level join.

Also, grammer of the AppJoinBuilder is exactly same with the QueryBuilder. Therefore, you can swap QueryBuilder and AppJoinBuilder very simply without any cost. Thus, you can just select one of them suitable for your case.

AppJoinBuilder

export async function test_app_join_builder(): Promise<void>
{
    const builder: safe.AppJoinBuilder<BbsReviewArticle> = safe
        .createAppJoinBuilder(BbsReviewArticle, review =>
        {
            review.join("base", article =>
            {
                article.join("group");
                article.join("category");
                article.join("contents", content =>
                {
                    content.join("reviewContent");
                    content.join("files");
                });
                article.join("comments").join("files");
            });
        });
}

Furthermore, you've determined to using only the AppJoinBuilder, you can configure it much safely. With the AppJoinBuilder.initialize() method, you've configure all of the relationship accessors, and it prevents any type of ommission by your mistake.

export async function test_app_join_builder_initialize(): Promise<void>
{
    const builder = safe.AppJoinBuilder.initialize(BbsGroup, {
        articles: safe.AppJoinBuilder.initialize(BbsArticle, {
            group: undefined,
            review: safe.AppJoinBuilder.initialize(BbsReviewArticle, {
                base: undefined,
            }),
            category: safe.AppJoinBuilder.initialize(BbsCategory, {
                articles: undefined,
                children: undefined,
                parent: "recursive"
            }),
            contents: safe.AppJoinBuilder.initialize(BbsArticleContent, {
                article: undefined,
                files: "join"
            }),
            comments: safe.AppJoinBuilder.initialize(BbsComment, {
                article: undefined,
                files: "join"
            }),
            tags: "join",
            __mv_last: undefined,
            question: undefined,
            answer: undefined,
        })
    });
}

JsonSelectBuilder

Class Diagram

JSON converter without any SQL query

In the SafeORM, when you want to load DB records and convert them to a JSON data, you don't need to write any SELECT or JOIN query. You also do not need to consider any performance tuning. Just write down the ORM -> JSON conversion plan, then SafeORM will do everything.

The JsonSelectBuilder is the class doing everything. It will analyze your JSON conversion plan, and compose the JSON conversion method automatically with the exact JSON type what you want. Furthermore, the JsonSelectBuilder finds the best (applicataion level) joining plan by itself.

Below code is an example converting ORM model class instances to JSON data with the JsonSelectBuilder. As you can see, there's no special script in the below code, but only the conversion plan exists. As I've mentioned, JsonSelectBuilder will construct the exact JSON type by analyzing your conversion plan. Also, the performance tuning would be done automatically.

Therefore, just enjoy the JsonSelectBuilder without any worry.

export async function test_json_select_builder(models: BbsGroup[]): Promise<void>
{
    const builder = BbsGroup.createJsonSelectBuilder
    ({
        articles: BbsArticle.createJsonSelectBuilder
        ({
            group: safe.DEFAULT, // ID ONLY
            category: BbsCategory.createJsonSelectBuilder
            ({ 
                parent: "recursive" as const, // RECURSIVE JOIN
            }),
            tags: BbsArticleTag.createJsonSelectBuilder
            (
                {}, 
                tag => tag.value // OUTPUT CONVERSION BY MAPPING
            ),
            contents: BbsArticleContent.createJsonSelectBuilder
            ({
                files: "join" as const
            }),
        })
    });

    // GET JSON DATA FROM THE BUILDER
    const raw = await builder.getMany(models);

    // THE RETURN TYPE IS ALWAYS EXACT
    // THEREFORE, TYPEOF "RAW" AND "I-BBS-GROUP" ARE EXACTLY SAME
    const regular: IBbsGroup[] = raw;
    const inverse: typeof raw = regular;
}

InsertCollection

Massive insertion without considering any dependency relationship

When you want to execute INSERT query for lots of records of plural tables, you've to consider dependency relationships. Also, you may construct extended SQL query manually by yourself, if you're interested in the performance tuning.

However, with the InsertCollection class provided by this SafeORM, you don't need to consider any dependcy relationship. You also do not need to consider any performance tuning. The InsertCollection will analyze the dependency relationships and orders the insertion sequence automatically. Also, the InsertCollection utilizes the extended insertion query for the optimizing performance.

import safe from "safeorm";
import std from "tstl";

export async function archive
    (
        comments: BbsComment[],
        questions: BbsQuestionArticle[],
        reviews: BbsArticleReview[],
        groups: BbsGroup[],
        files: AttachmentFile[],
        answers: BbsAnswerArticle[],
        categories: BbsCategory[],
        comments: BbsComment[],
        articles: BbsArticle[],
        contents: BbsArticleContent[],
        tags: BbsArticleTag[],
    ): Promise<void>
{
    // PREPARE A NEW COLLECTION
    const collection: safe.InsertCollection = new safe.InsertCollection();
    
    // PUSH TABLE RECORDS TO THE COLLECTION WITH RANDOM SHULFFLING
    const massive = [
        comments,
        questions,
        reviews,
        groups,
        files,
        answers,
        comments,
        articles,
        contents,
        tags
    ];
    std.ranges.shuffle(massive);
    for (const records of massive)
        collection.push(records);

    // PUSH INDIVIDUAL RECORDS
    for (const category of categories)
        collection.push(category);
    
    // EXECUTE THE INSERT QUERY
    await collection.execute();
}

About

Ultimate Safe ORM for the TypeScript

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published