nn-model
v1.3.7
Published
A useful model for NodeJS
Readme
NN-Model
A TypeScript ORM built on top of Knex.js with decorator-based model definitions and powerful eager loading support.
Features
- 🎯 Decorator-based model definitions - Clean, type-safe model definitions using TypeScript decorators
- 🔗 Eager loading - Load related data with
include().thenInclude()chains - ✅ Field validation - Automatic validation based on schema decorators
- 🔄 Transaction support - Full transaction support for CRUD operations
- 🗄️ Multiple connections - Manage multiple database connections
- 📦 Auto migrations - Generate migrations from model definitions
Installation
npm install ../model knex
# Install your database driver
npm install mysql2 # MySQL
npm install pg # PostgreSQL
npm install sqlite3 # SQLite
npm install mssql # SQL ServerQuick Start
1. Setup Connection
import "reflect-metadata";
import { Model, ConnectionManager } from "../model";
import Knex from "knex";
// Configure connections
const connections = new Map<string, Knex.Config>();
connections.set("default", {
client: "mysql2",
connection: {
host: "localhost",
user: "root",
password: "password",
database: "mydb",
},
});
// Initialize
ConnectionManager.setConnections(connections);2. Define Models
import {
Model,
ModelDecorator,
PrimaryKey,
String,
Number,
HasMany,
BelongsTo,
} from "../model";
@ModelDecorator()
export class User extends Model<User> {
protected tableName = "users";
public primaryKey = "id";
@PrimaryKey({ type: "increments" })
public id?: number;
@String({ max: 100 })
public name?: string;
@String({ max: 255 })
public email?: string;
@HasMany({ tableName: "orders", fk: "user_id", lk: "id" })
public orders?: Order[];
}
@ModelDecorator()
export class Order extends Model<Order> {
protected tableName = "orders";
public primaryKey = "id";
@PrimaryKey({ type: "increments" })
public id?: number;
@Number()
public user_id?: number;
@Decimal({ precision: 10, scale: 2 })
public total?: number;
@BelongsTo({ tableName: "users", fk: "user_id", lk: "id" })
public user?: User;
@HasMany({ tableName: "order_details", fk: "order_id", lk: "id" })
public details?: OrderDetail[];
}3. TypeScript Configuration
Add to your tsconfig.json:
{
"compilerOptions": {
"experimentalDecorators": true,
"emitDecoratorMetadata": true
}
}Decorators
Class Decorator
| Decorator | Description |
| ------------------- | --------------------------------------- |
| @ModelDecorator() | Required. Registers the model class |
Property Decorators
| Decorator | Options | Description |
| --------------- | ---------------------------------------- | ---------------------- |
| @PrimaryKey() | { type: 'increments' \| 'uuid' } | Primary key field |
| @String() | { max?: number, nullable?: boolean } | String field |
| @Number() | { nullable?: boolean } | Integer field |
| @Decimal() | { precision?: number, scale?: number } | Decimal field |
| @Json() | { nullable?: boolean } | JSON field |
| @ForeignKey() | { table, column, onDelete? } | Foreign key constraint |
Relation Decorators
| Decorator | Options | Description |
| -------------- | ----------------------- | ------------------------ |
| @HasMany() | { tableName, fk, lk } | One-to-many relationship |
| @HasOne() | { tableName, fk, lk } | One-to-one relationship |
| @BelongsTo() | { tableName, fk, lk } | Inverse relationship |
Options:
tableName: Related table namefk: Foreign key column in related tablelk: Local key column (defaults to primary key)
CRUD Operations
Create
const user = new User();
// Single insert
const newUser = await user.create({
name: "John Doe",
email: "[email protected]",
});
// Bulk insert
const users = await user.createBulk([
{ name: "User 1", email: "[email protected]" },
{ name: "User 2", email: "[email protected]" },
]);Read
const user = new User();
// Get all
const allUsers = await user.all();
// Get with conditions
const users = await user.where("name", "like", "%John%").get();
// Get first match
const firstUser = await user.where("id", 1).first();
// Find by ID
const foundUser = await user.find(1);Update
const user = new User();
// Update by ID
await user.update({ name: "Jane Doe" }, 1);
// Save (insert or update)
await user.save({ id: 1, name: "Jane Doe" });
// Upsert
await user.upsert({ id: 1, name: "Jane" }, "id");Delete
const user = new User();
// Delete by ID
await user.delete(1);Eager Loading (Relations)
Basic Include
const user = new User();
// Load single relation
const usersWithOrders = await user.include("orders").get();Nested Includes
// 2-level nesting
const users = await user.include("orders").thenInclude("details").get();
// 3-level nesting
const users = await user
.include("orders")
.thenInclude("details")
.thenInclude("product")
.get();
// 4-level nesting
const users = await user
.include("orders")
.thenInclude("details")
.thenInclude("product")
.thenInclude("category")
.get();Multiple Relations
// Parallel includes (back to root after thenInclude chain)
const users = await user
.include("orders")
.thenInclude("details")
.include("payments") // Resets to root (User)
.thenInclude("paymentDetails")
.include("coupons") // Another root relation
.get();With Query Conditions
// Filter nested relations
const users = await user
.include("orders", (qb) => qb.where("total", ">", 100))
.thenInclude("details", (qb) => qb.where("quantity", ">", 1))
.get();
// Combine with root conditions
const users = await user
.where("name", "like", "%John%")
.include("orders", (qb) => qb.orderBy("created_at", "desc"))
.get();Access Loaded Data
const users = await user.include("orders").get();
for (const u of users) {
console.log(u.name);
console.log(u.orders); // Already loaded
// Get model instance for further operations
const userInstance = u.instance();
await userInstance.update({ name: "Updated" });
}Transactions
import { ConnectionManager } from "../model";
await ConnectionManager.transaction(async (trx) => {
const user = new User();
const order = new Order();
const newUser = await user.create(
{
name: "John",
email: "[email protected]",
},
trx,
);
await order.create(
{
user_id: newUser.id,
total: 99.99,
},
trx,
);
// Transaction automatically commits
// If error thrown, automatically rolls back
});Multiple Connections
const connections = new Map<string, Knex.Config>();
connections.set("default", {
client: "mysql2",
connection: {
/* ... */
},
});
connections.set("analytics", {
client: "pg",
connection: {
/* ... */
},
});
ConnectionManager.setConnections(connections);
// Switch connection
await ConnectionManager.switchConnection("analytics");
const analyticsData = await new AnalyticsModel().all();
// Switch back
await ConnectionManager.switchConnection("default");Migrations
The model can auto-generate and run migrations based on decorators:
const user = new User();
// Create table from model definition
await user.migrate();
// Drop table
await user.drop();Query Builder Methods
All Knex query builder methods are available:
const user = new User();
// Where clauses
user.where("id", 1);
user.where("name", "like", "%John%");
user.whereIn("id", [1, 2, 3]);
user.whereNull("deleted_at");
user.whereBetween("age", [18, 65]);
// Ordering
user.orderBy("created_at", "desc");
// Limiting
user.limit(10).offset(20);
// Selecting specific columns
user.select("id", "name", "email");
// Chaining
const results = await user
.where("active", true)
.orderBy("name")
.limit(10)
.include("orders")
.get();Full Example
import "reflect-metadata";
import {
Model,
ModelDecorator,
ConnectionManager,
PrimaryKey,
String,
Decimal,
HasMany,
BelongsTo,
ForeignKey,
} from "../model";
// Setup connection
const connections = new Map();
connections.set("default", {
client: "mysql2",
connection: {
host: "localhost",
user: "root",
password: "password",
database: "shop",
},
});
ConnectionManager.setConnections(connections);
// Define models
@ModelDecorator()
class Category extends Model<Category> {
protected tableName = "categories";
@PrimaryKey({ type: "increments" }) id?: number;
@String({ max: 100 }) name?: string;
@HasMany({ tableName: "products", fk: "category_id", lk: "id" })
products?: Product[];
}
@ModelDecorator()
class Product extends Model<Product> {
protected tableName = "products";
@PrimaryKey({ type: "increments" }) id?: number;
@ForeignKey({ table: "categories", column: "id" }) category_id?: number;
@String({ max: 200 }) name?: string;
@Decimal({ precision: 10, scale: 2 }) price?: number;
@BelongsTo({ tableName: "categories", fk: "category_id", lk: "id" })
category?: Category;
}
// Usage
async function main() {
const category = new Category();
const product = new Product();
// Create tables
await category.migrate();
await product.migrate();
// Insert data
const electronics = await category.create({ name: "Electronics" });
await product.create({
name: "iPhone 15",
price: 999.99,
category_id: electronics.id,
});
// Query with relations
const categories = await category.include("products").get();
for (const cat of categories) {
console.log(`${cat.name}: ${cat.products?.length} products`);
}
// Cleanup
await ConnectionManager.destroy();
}
main();License
MIT
