Handle your application models in Node.js. Built on top of @secjs/database
To use the high potential from this package you need to install first this other packages from SecJS, it keeps as dev dependency because one day
@secjs/core
will install everything once.
npm install @secjs/env @secjs/utils @secjs/exceptions
Then you can install the package using:
npm install @secjs/orm
To use @secjs/orm you need to set up @secjs/database first and connect it to Database.
Go to @secjs/database documentation
With config/database file configured and Database connection opened you can start using Model class
First you need to create your model class extending Model class and use decorators to map your columns
import { Model, Column } from '@secjs/orm'
export class Product extends Model {
/**
* Defines the table name in database. Default is the
* name of your model in snake_case and plural format
*/
static table = 'products'
/**
* Defines the connection that this Model will use
* to handle the database operations. This string will be used
* in Database.connection method from @secjs/database.
* Default is default
*/
static connection = 'default'
/**
* Defines the primary key of the Model. You will always
* set the value in Model and not the database columName.
* Example: primaryKey will be idName and not id!
* Default value is id
*/
static primaryKey = 'idName'
/**
* Defines the values that are authorized to be persisted
* in database. Example: if you try to create/update the
* createdAt column, it will not be created/updated if it's
* not present in persistOnly array. Default is ['*']
*/
static persistOnly = ['name', 'description']
/**
* The columnName option defines that idName === id in database.
* Default is the same name of the property, in this case, idName
*/
@Column({ columnName: 'id' })
public idName: number
@Column()
public name: string
@Column()
public description: string
/**
* The defaultValue option defines that when creating an Product,
* Model will auto set quantity as '1' if nothing is set on create
*/
@Column({ defaultValue: 1 })
public quantity: number
/**
* The isCreatedAt option defines that when creating an Product,
* Model will auto set createdAt as 'new Date()'
*/
@Column({ isCreatedAt: true })
public createdAt: Date
/**
* The isUpdatedAt option defines that when updating an Product,
* Model will auto set updatedAt as 'new Date()'
*/
@Column({ isUpdatedAt: true })
public updatedAt: Date
/**
* The isDeletedAt option defines that when deleting an Product,
* Model will soft delete instead of delete and auto set deletedAt
* as 'new Date()'
*/
@Column({ isDeletedAt: true })
public deletedAt: Date
}
With our database connection established and our model defined, we can start using Product model
const product = await Product.create({ name: 'iPhone 10', description: 'Nice iPhone' })
console.log(product instanceof Product) // true
console.log(product.idName) // 1
console.log(product.name) // 'iPhone10'
console.log(product.quantity) // 5
console.log(product.createdAt) // 2022-03-19T16:23:35.897Z
console.log(product.updatedAt) // 2022-03-19T16:23:35.897Z
console.log(product.deletedAt) // null
// Product instance has the toJSON method that will
// transform the model to object
const productJson = product.toJSON()
console.log(productJson instanceof Product) // false
const product = await Product.find({ idName: 1 })
console.log(product instanceof Product) // true
const products = await Product.findMany({ idName: 1 })
console.log(products[0] instanceof Product) // true
const where = { idName: 1 }
const product = await Product.update(where, { quantity: 50 })
console.log(product instanceof Product) // true
console.log(product.quantity) // 50
If you set a Column with isDeletedAt property, delete method will always soft delete your data, setting your column as 'new Date()'
// If Product Model didn't have isDeletedAt, it would be deleted
const product = await Product.delete({ idName: 1 })
console.log(product instanceof Product) // true
console.log(product.deletedAt) // 2022-03-19T16:30:17.130Z
// You can force the delete setting the force parameter as true
const force = true
const voidProduct = await Product.delete({ idName: 1 }, force)
console.log(voidProduct instanceof Product) // false
console.log(voidProduct) // undefined
We can use Decorators to define OneToOne, OneToMany and ManyToMany relations in our Model
import { Role } from './Role'
import { Product } from './Product'
import { Model, Column, HasMany, ManyToMany } from '@secjs/orm'
export class User extends Model {
static persistOnly = ['name']
@Column()
public id: number
@Column()
public name: string
/**
* The primaryKey option defines the primaryKey of your Model.
* In this case the default value would be id, because the
* primaryKey of User model is id.
*
* The foreignKey options defines the foreignKey of your RelationModel.
* In this case the default value would be userId. The name of
* your Model in lower case (user) with Id in the end.
*/
@HasMany(() => Product, { primaryKey: 'id', foreignKey: 'userId' })
public products: Product[]
/**
* WARN - In ManyToMany relations you don't need to define the Pivot model
*
* pivotTableName: The name of your pivot table - Default is user_role
* localPrimaryKey: The PK of your Model - Default is id
* pivotLocalForeignKey: The FK of your Model in the Pivot Table - Default is userId
* relationPrimaryKey: The PK of your RelationModel - Default is id
* pivotRelationForeignKey: The FK of your RelationModel in the Pivot Table - Default is roleId
*/
@ManyToMany(() => Role, {
pivotTableName: 'user_role'
})
public roles: Role[]
}
import { User } from './User'
import { Model, Column, BelongsTo } from '@secjs/orm'
export class Product extends Model {
static persistOnly = ['name', 'quantity']
@Column()
public id: number
@Column({ defaultValue: 'Product' })
public name: string
@Column({ defaultValue: 1 })
public quantity: number
// The FK
@Column()
public userId: number
/**
* The primaryKey option defines the primaryKey of your RelationModel.
* In this case the default value would be id, because the
* primaryKey of User model is id.
*
* The foreignKey options defines the foreignKey of your Model.
* In this case the default value would be userId. The name of
* your RelationModel in lower case (user) with Id in the end.
*/
@BelongsTo(() => User, { foreignKey: 'userId', primaryKey: 'id' })
public user: User
}
import { User } from './User'
import { Model, Column, ManyToMany } from '@secjs/orm'
export class Role extends Model {
static persistOnly = ['name']
@Column()
public id: number
@Column({ defaultValue: 'Customer' })
public name: string
/**
* WARN - To map the other side of a ManyToMany relation you need to set the
* pivotTableName property. If you do not set the pivotTableName here and
* try to make an include query it would make the query in role_user pivotTable
*/
@ManyToMany(() => User, {
pivotTableName: 'user_role'
})
public users: User[]
}
import { Product } from './Product'
import { Model, Column, BelongsTo } from '@secjs/orm'
export class ProductDetail extends Model {
static persistOnly = ['detail']
@Column()
public id: number
@Column()
public detail: string
// The FK
@Column()
public productId: number
@BelongsTo(() => Product)
public product: Product
}
We can use load instance method to get the relations from User
const user = await User.find()
// Load roles and products with productDetails from User
await user.load('roles', 'products.productDetails')
console.log(user.roles[0] instanceof Role) // true
console.log(user.products[0] instanceof Product) // true
console.log(user.products[0].productDetails[0] instanceof ProductDetail) // true
But we can use query builder from User too
const user = await User
.query()
.includes('roles')
// Sub query
.includes('products', async (query) => {
query.orderBy('detail', 'asc').includes('productDetails')
})
.get()
console.log(user.roles[0] instanceof Role) // true
console.log(user.products[0] instanceof Product) // true
console.log(user.products[0].productDetails[0] instanceof ProductDetail) // true
If you have mapped right your models with HasMany and BelongsTo you can also take the user from Product model. Example using paginate method
const page = 0
const limit = 1
const { meta, links, data } = await Product
.query()
.includes('user')
.paginate(page, limit, '/products')
console.log(meta)
/**
* itemCount: 1
* totalItems: 1
* totalPages: 1
* currentPage: 0
* itemsPerPage: 1
*/
console.log(links)
/**
* first: '/products?limit=1'
* previous: '/products?page=0&limit=1'
* next: '/products?page=1&limit=1'
* last: '/products?page=1&limit=1'
*/
console.log(data instanceof Product) // true
console.log(data.user instanceof User) // true
You can define your own methods in your Model and use the query builder to get what you want
import { User } from './User'
import { Model, Column, BelongsTo } from '@secjs/orm'
export class Product extends Model {
static persistOnly = ['name', 'quantity']
@Column()
public id: number
@Column({ defaultValue: 'Product' })
public name: string
@Column({ defaultValue: 1 })
public quantity: number
@Column()
public userId: number
@BelongsTo(() => User, { foreignKey: 'userId', primaryKey: 'id' })
public user: User
/**
* You can define it as static and use it like this -> Product.getAllWithUser(1, 2, 3)
*/
static async getAllWithUser(...quantityIn: number[]) {
return this
.query()
.select('id', 'name', 'quantity')
.whereIn('quantity', ...quantityIn)
.orderBy('name', 'ASC')
.includes('user')
.getMany()
}
/**
* Or you can define it as an instance method and use it like this
* -> const product = await Product.find() -> await product.getAllWithUser(1, 2, 3)
*/
async getAllWithUser(quantityIn: number[]) {
// WARN - Do not use Model here, it wont work.
// You need to use your defined model, in this case, Product.
return Product
.query()
.select('id', 'name', 'quantity')
.whereIn('quantity', quantityIn)
.orderBy('name', 'ASC')
.includes('user')
.getMany()
}
}
The factory method from models can be used to generate a massive number of models. You can use this to work in the tests of your application
const { id } = await User.create({ name: 'João' })
// Will create ten products in database with name Test and the owner will the user João
const products = await Product.factory().count(10).create<Product[]>({ name: 'Test', userId: id })
// Will make ten products fake objects with name Test and the owner will the user João
const fakeProducts = await Product.factory().count(10).make<Product[]>({ name: 'Test', userId: id })
To solve the problem of repetitive data you can define in your model the static method
definition
. And use thethis.faker
property to mock values
import { User } from './User'
import { Model, Column, BelongsTo } from '@secjs/orm'
export class Product extends Model {
// Factories ignore the persistOnly rule
static persistOnly = ['name', 'quantity']
static async definition() {
return {
name: this.faker.name.firstName(),
quantity: this.faker.datatype.number(),
/**
* Define that User factory should return only the value from idPrimary.
* If you don't set the returning key, factory will return all the data from User
*/
userId: User.factory('idPrimary'),
}
}
@Column()
public id: number
@Column({ defaultValue: 'Product' })
public name: string
@Column({ defaultValue: 1 })
public quantity: number
@Column()
public userId: number
@BelongsTo(() => User, { foreignKey: 'userId', primaryKey: 'id' })
public user: User
}
Now you can use create method normally
import { Product } from './Product'
const { id } = await User.create({ name: 'João' })
// Will create ten products in database with mocked values and
// the owner will be different for each one of then
const productsDifOwner = await Product.factory().count(10).create<Product[]>()
// Will create ten products in database with mocked values and
// the owner will be the same for each one of then
const productsSameOwner = await Product.factory().count(10).create<Product[]>({ userId: id })
Factory has some assertions that you can make to use in tests
const factory = User.factory()
await factory.count(10).create<User[]>()
// Assert that user table has twenty users
await factory.assertCount(10) // true
// Assert that exists at least one user with name 'João'
await factory.assertHas({ name: 'João' }) // true
// Assert that does not exist any user with name 'Victor'
await factory.assertMissing({ name: 'Victor' }) // true
// Assert that exists one user with id 1
await factory.assertExists({ id: 1 }) // true
// Assert that does not exists one user with id 9999
await factory.assertNotExists({ id: 9999 }) // true
Made with 🖤 by jlenon7 👋