SkillAgentSearch skills...

Refineorm

ORM based heavily on Linq2DB and EFCore that built for Typescript

Install / Use

/learn @drizward/Refineorm
About this skill

Quality Score

0/100

Supported Platforms

Universal

README

Refine ORM

WIP, some functionality could be not working as expected!

Refine is a type-safe ORM for TS/JS in both code-first and database-first paradigm that uses lambda/arrow function as the query definition that inspired by LINQ query style in C#. Refine are highly inspired by those popular .NET ORM such as Entity Framework and Linq2db, and also TS ORM such as TypeORM.

In other word, you may query in database as how you work with array. It allow you to write your query in a JS/TS' lambda to enable fast database prototyping on Node.js, React Native and Ionic, and support most popular database such as MySQL, MSSQL, PostgreSQL, Oracle and SQLite.

Refine may not as complicated and complex as many other ORM in JS/TS world but it will give you a very strong resemblance than any other ORM in way to query your data.

Working feature listed as:

  • Query API to let you work in lambda/arrow function
  • Model and data configuration with decorator
  • Associations in bi-directional, while uni-directional are partially supported for One-to-one
  • Inheritance pattern
  • Transaction
  • Multiple migration strategy
  • Lazy and eager loading for associations
  • Promise-based
  • EF-like DataContext

Expected features:

  • Fluent model configurator for Javascript
  • Custom repository
  • Navigation property for associations

Installation

npm install --save refineorm

# one of
npm install --save mysql named-placeholder
npm install --save pg
npm install --save mssql
npm install --save oracledb
npm install --save sqlite

If you use Typescript make sure to include following configuration in your tsconfig.json

"emitDecoratorMetadata": true,
"experimentalDecorators": true,

Also since Refine rely very much on ES6 arrow function, you must set your target to minimum of es6, however esnext are more reccomended. You will know why after you read the detail below.

Get Started

Configuring your model

Refine model are defined through a class, so you may start with creating your class as like the following

export class User {
    name: string;
    job: string;
    age: number;
    maritalStatus: boolean;
    address: string;
}

Now if you done with your model, you may start decorating your class with metadata that needed to sturcture out your physical database

@Table('users')
export class User {

    @Key()
    name: string;

    @Column()
    job: string;

    @Column()
    age: number;

    @Column('isMarried')
    maritalStatus: boolean;

    @Column()
    address: string;
}

From the latter, we decorate our class with a @Table and @Column for the properties. Both decorator may accept string paramater to rename your table or property into more suited name of your choice.

Also we use @Key to decorate name as our primary key. Notice how we only use @Key without a @Column. Since @Key indicate that it is a primary key and since primary key must be a column, you may omit the @Column from it.

However the @Key decorator don't accept any parameter as how @Column accept string as the physical column name. So if you want to name your column, you have to pass it with the @Column as the following example

@Key() @Column('userName')
name: string;

Fluent model configuration

If you use a plain Javascript and can't work with decorator, or you may but you hate it because it's not a standard even in ESNext, you may use fluent model configurator to describe your table structure. You can follow the example below

// TO DO

Creating the DataContext

All interaction with database in Refine are passed through DataContext. In simple word, it is a representation of your connection to the database and even the strcuture of the database itself. First of all, you need to create a class that extends the base DataContext class. Let's define it as the following

export class MyContext extends DataContext {

    static readonly config: ConnectionConfig = {
        database: 'skrpsi',
        host: 'localhost',
        password: '',
        port: 3306,
        type: 'mysql',
        user: 'root'
    };

    constructor() {
        super(MyContext.config, new MysqlProvider());
    }

}

You can suit the config as your own connection configuration. In the example above, we use MySQL as the database provider and we define it from the type property inside the config. The valid type are mysql, pgsql, mssql, oracle, and sqlite. If you want to change your vendor or provider, you can simply change it.

Adding your DataSet

If DataContext is a represetation of your connection and database, DataSet is a representation of the table inside your database. Since the tables are bound to database, so does the DataSet are bound to DataContext. To add your dataset to your context, you may see the following

export class MyContext extends DataContext {

    static readonly config: ConnectionConfig = {
        database: 'skrpsi',
        host: 'localhost',
        password: '',
        port: 3306,
        type: 'mysql',
        user: 'root'
    };

    readonly users: DataSet<User> = this.fromSet(User);

    constructor() {
        super(MyContext.config);
    }

}

We add the dataset of type User as a property and initialize it with this.fromSet(..). We have to pass the class, in this case User, as the parameter because TS don't let us to read the generic type. Also notice that readonly modifier are added to ensure no side-effect are happening to the dataset.

Setting up your Migration Strategy

In Refine, migration strategy are the way to handle migration when changes detected in your model. Migration strategy are defined with MigrationStrategy enum, and the most basic is Never, OnEmpty and OnChange. Never are the only valid strategy if you want to work in database-first, while the other can be used to handle the code-first paradigm.

By default, DataContext will implement OnEmpty as it strategy, meaning that the database generation only happen when the database are completely empty. If you want to change it, you may override the migrationStrategy in your data context as below

get migrationStrategy(): MigrationStrategy {
    return MigrationStrategy.RecreateOnChange;
}

Setting your strategy to OnChange will ensure that the database are generated everytime a change happen in the model.

Working with your first query

Inserting your object

After you have done with your model, data context and the data set, now you only need to write somewhere in your code your first query. Let's start with inserting your first User.

async function someWhereInCode() {

    const user = new User();
    user.name = 'John Doe';
    user.job = 'JS Developer';
    user.age = 33;
    user.maritalStatus = false;
    user.address = 'Somewhere in NPM';

    const context = new MyContext();
    await context.users.insert(user);

    await context.release();

}

Notice that we use async modifier in the function and await for the insert and release. If your environment yet to support it you can use the common then as how you work in promise. And don't forget to call release in the end if you want to close and release the resource immediately.

Reading your data

Now since the table have a record, we can begin to write our query to read the data. To read the user by it's name we can write it as

async function someWhereInCode() {
    // -- the previous code

    const john = await context.users.first(x => x.name == 'John Doe');
    console.log(john.job) // output: JS Developer

    await context.release();
}

The first method are equivalent with find in array, it return the first element. Query from above will return the first element with name = 'John Doe'.

If you want to read only one data and such data must be entirely unique, you could use single(...) instead

async function someWhereInCode() {
    // -- the previous code

    try {
        const john = await context.users.single(x => x.name == 'John Doe');
        console.log(john.job) // output: JS Developer
    }
    catch(err) {
        console.log("User with name 'John Doe' are more than 1");
    }

    await context.release();
}

The single method will throw an error when data that fulfill the predicate are more than one record. However, if you sure that it may only exists one record and you didn't need it to be checked, you should use first as it will only send the query once, while single will send 2 query in which first checking whether such data only have 1 record and the second is to read the actual data.

Selecting and filtering your data

If you already have 10 users in the database, and you want to filter it by age that is greater or equals 30, you can use the below query

const filtered = context.users.where(x => x.age > 30);
for await (const user of filtered) {
    // do something with the filtered data here
}

That's how you're querying with the where clause. But why didn't where awaited as how we did it with first and how did it get executed? The query get executed when an access or iteration happens to the filtered or an element that it held. In the above, it's happen in the for statement.

Simply, unlike first which return an object, where method will return an AsyncCollection which is also a base interface of DataSet that will let you interact with the the table. In other word, this mechanism allow you to chain or build your query dynamically as how common QueryBuilder pattern in other ORM. But instead of a string, it typed in a valid expression. Another example can be seen below

``

View on GitHub
GitHub Stars4
CategoryDevelopment
Updated5y ago
Forks0

Languages

TypeScript

Security Score

70/100

Audited on Jan 22, 2021

No findings