npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@dreamtree-org/korm-js

v1.1.3

Published

Knowledge Object-Relational Mapping - A powerful, modular ORM system for Node.js with dynamic database operations, complex queries, relationships, and nested requests

Readme

KORM-JS

Knowledge Object-Relational Mapping - A powerful, modular ORM system for Node.js with dynamic database operations, complex queries, relationships, nested requests, and soft delete support.

npm version License: MIT

About the Company

KORM-JS is built and maintained under Dreamtree Global — a team crafting thoughtful, developer-first software for the modern web. We believe great products start with a great foundation, and that's exactly what this library aims to be: a polished, open, and dependable data layer for teams everywhere.

🌐 Website: dreamtreeglobal.com

Author & Maintainer

Partha Preetham Krishna M L 📧 [email protected]

Have an idea, a bug, or just want to say hi? Reach out anytime — feedback from developers like you is what keeps this library growing.

💚 Sponsor & Support

KORM-JS is free and open source, built in the open and powered entirely by passion and late nights. Every feature you rely on — every multi-engine query, relation-aware join, and line of zero-boilerplate CRUD — represents hours of careful craft so that your backend can ship faster and stay safer.

If this library has saved you time, sparked an idea, or spared you a pile of hand-written SQL, please consider supporting its continued development. Your sponsorship keeps the updates coming, the bugs squashed, and the docs sharp — for the whole community. Even the smallest contribution is a huge encouragement. 🙏

☕ Make a one-tap donation via UPI

📷 Scan the QR with any UPI app (Google Pay, PhonePe, Paytm, BHIM, …) to pay instantly:

UPI ID: dhrugantha.llp@kotak  •  Payee: Dreamtree Global

📱 On mobile, you can also tap the button below — it opens your UPI app directly. On desktop, scan the QR above or copy the UPI ID into your payment app.

Pay via UPI

🤝 Become a Sponsor

Want to back the project long-term or as a company? You can sponsor the package directly:

Every star ⭐, share, and contribution helps more than you know. Thank you for being part of the journey!

Features

  • 🚀 Multi-Database Support: MySQL, PostgreSQL, SQLite
  • 🔧 Dynamic Operations: Create, Read, Update, Delete, Replace, Upsert, Sync
  • 🔍 Advanced Querying: Complex queries with relationships, joins, and filtering
  • 🛡️ Data Validation: Built-in request validation with custom rules
  • 🔄 Auto-Sync: Automatic table schema synchronization
  • 📦 Modular Design: Use only what you need
  • 🎯 Express.js Ready: Perfect integration with Express applications
  • 🔄 Schema Generation: Auto-generate schemas from existing databases
  • 🗑️ Soft Delete: Built-in soft delete support with automatic filtering
  • 🎣 Model Hooks: Before, After, Validate, and Custom action hooks
  • 🔗 Custom Relations: Define custom relation hooks for complex data fetching
  • 📦 Nested Requests: Process multiple related requests in a single call
  • 📝 Logger Utility: Configurable logging with multiple log levels
  • 🔎 SQL Debugging: Debug mode with SQL statement output for troubleshooting
  • 🚫 NOT EXISTS Queries: Support for checking absence of related records with ! prefix
  • 🤖 AI-native: a bundled MCP server serves your tables to coding agents as typed JSON tools, plus an init --ai skill installer for Claude, Cursor, Copilot, and more

Installation

npm install @dreamtree-org/korm-js

AI assistant skills (init --ai)

KORM-JS ships a canonical reference doc designed for AI coding assistants. After installing the package (or via npx without installing), drop the skill into the location your assistant picks up:

npx @dreamtree-org/korm-js init --ai claude

Supported providers:

| --ai value | Target file | | ----------------------------------- | ----------------------------------------------------------- | | claude | CLAUDE.md (idempotent block insert) | | openai (alias: codex) | AGENTS.md (idempotent block insert) | | gemini | GEMINI.md (idempotent block insert) | | copilot (alias: github-copilot) | .github/copilot-instructions.md (idempotent block insert) | | kiro | .kiro/steering/korm-js.md | | windsurf | .windsurf/rules/korm-js.md | | cursor | .cursor/rules/korm-js.mdc (with MDC frontmatter) | | all | Installs every provider above |

Flags:

  • --cwd <dir> — install into a directory other than the current working directory.
  • --force — overwrite dedicated-file targets (Cursor / Windsurf / Kiro) that already exist.
  • --dry-run — print what would change without writing.

Block-insert targets (CLAUDE.md, AGENTS.md, GEMINI.md, .github/copilot-instructions.md) wrap the skill in <!-- BEGIN korm-js skill --> ... <!-- END korm-js skill --> markers. Re-running the command after a library upgrade updates the block in place without touching the rest of the file.

Quick Start with Express.js and MySQL

1. Basic Setup

require('dotenv').config();
const express = require('express');
const { initializeKORM, validate, helperUtility } = require('@dreamtree-org/korm-js');
const knex = require('knex');

const app = express();
app.use(express.json());

// MySQL database configuration
const db = knex({
  client: 'mysql2',
  connection: {
    host: process.env.DB_HOST || 'localhost',
    user: process.env.DB_USER || 'root',
    password: process.env.DB_PASS || 'password',
    database: process.env.DB_NAME || 'my_database',
    port: process.env.DB_PORT || 3306,
  },
});

// Initialize KORM with MySQL
const korm = initializeKORM({
  db: db,
  dbClient: 'mysql',
  debug: false, // Set to true for SQL debugging
});

app.listen(3000, () => {
  console.log('🚀 Server running on http://localhost:3000');
});

2. Schema Generation and Database Sync

async function initApp() {
  // Load existing schema if available
  let syncSchema = helperUtility.file.readJSON('schema/sync.json');
  if (syncSchema) {
    korm.setSchema(syncSchema);
  }

  // Sync database with schema (creates tables if they don't exist)
  await korm.syncDatabase().then(() => {
    console.log('✅ Database synced');
  });

  // Generate schema from existing database
  await korm.generateSchema().then((schema) => {
    korm.setSchema(schema); // Set the schema to the korm instance
    helperUtility.file.createDirectory('schema');
    helperUtility.file.writeJSON('schema/schema.json', schema);
    console.log('✅ Schema generated and saved');
  });
}

// Initialize the application
initApp();

3. Generic CRUD API Endpoint

// Generic CRUD endpoint for any model
app.post('/api/:model/crud', async (req, res) => {
  try {
    const { model } = req.params;
    const result = await korm.processRequest(req.body, model);
    res.json(result);
  } catch (error) {
    res.status(400).json({ error: error.message });
  }
});

// Health check endpoint
app.get('/health', (req, res) => {
  res.send('OK');
});

Using KORM-JS with Next.js

KORM-JS is HTTP-framework-agnostic — mount processRequest inside the route handlers / Server Actions Next.js already gives you (App Router). The complete integration guide (setup, route handlers, hooks, deployment, and an SEO + GEO section: generateMetadata, JSON-LD, sitemap, robots, llms.txt) lives in docs/NEXTJS.md.

A runnable App-Router example backed by KORM over SQLite is in examples/nextjs-seo-geo/.

Complete CRUD Operations Guide

1. Create Operation

// Create a new record
POST /api/Users/crud
{
  "action": "create",
  "data": {
    "username": "john_doe",
    "email": "[email protected]",
    "first_name": "John",
    "last_name": "Doe",
    "age": 30,
    "is_active": true
  }
}

// Response
{
  "message": "Record created successfully",
  "data": [
    {
      "id": 1,
      "username": "john_doe",
      "email": "[email protected]",
      "first_name": "John",
      "last_name": "Doe",
      "age": 30,
      "is_active": true,
      "created_at": "2024-01-01T00:00:00.000Z",
      "updated_at": "2024-01-01T00:00:00.000Z"
    }
  ],
  "success": true
}

2. List Operation (Read Multiple Records)

// Get all records with basic filtering
POST /api/Users/crud
{
  "action": "list",
  "where": { "is_active": true },
  "select": ["id", "username", "email", "first_name", "last_name"],
  "orderBy": { "column": "created_at", "direction": "desc" },
  "limit": 10
}

// List with pagination
POST /api/Users/crud
{
  "action": "list",
  "select": ["id", "username", "email"],
  "orderBy": { "column": "created_at", "direction": "desc" },
  "limit": 10,
  "offset": 20
}

// List with complex conditions
POST /api/Users/crud
{
  "action": "list",
  "where": {
    "first_name": "%John%",
    "email": "%@example.com",
    "age": "><18,65",
    "is_active": true
  },
  "select": ["id", "username", "email", "first_name", "last_name"],
  "orderBy": [
    { "column": "created_at", "direction": "desc" },
    { "column": "last_name", "direction": "asc" }
  ],
  "limit": 10
}

// Response
{
  "data": [
    {
      "id": 1,
      "username": "john_doe",
      "email": "[email protected]",
      "first_name": "John",
      "last_name": "Doe"
    }
  ],
  "total": 1,
  "page": 1,
  "limit": 10
}

3. Show Operation (Read Single Record)

// Get single record by ID
POST /api/Users/crud
{
  "action": "show",
  "where": { "id": 1 }
}

// Get single record with multiple conditions
POST /api/Users/crud
{
  "action": "show",
  "where": {
    "username": "john_doe",
    "is_active": true
  }
}

// Response
{
  "id": 1,
  "username": "john_doe",
  "email": "[email protected]",
  "first_name": "John",
  "last_name": "Doe",
  "age": 30,
  "is_active": true,
  "created_at": "2024-01-01T00:00:00.000Z",
  "updated_at": "2024-01-01T00:00:00.000Z"
}

4. Update Operation

// Update record by ID
POST /api/Users/crud
{
  "action": "update",
  "where": { "id": 1 },
  "data": {
    "first_name": "Jane",
    "last_name": "Smith"
  }
}

// Update multiple records
POST /api/Users/crud
{
  "action": "update",
  "where": { "is_active": false },
  "data": {
    "is_active": true
  }
}

// Response
{
  "message": "Record updated successfully",
  "data": [
    {
      "id": 1,
      "first_name": "Jane",
      "last_name": "Smith",
      "updated_at": "2024-01-01T00:00:00.000Z"
    }
  ],
  "success": true
}

5. Delete Operation

// Hard delete (permanent deletion)
POST /api/Users/crud
{
  "action": "delete",
  "where": { "id": 1 }
}

// Delete multiple records
POST /api/Users/crud
{
  "action": "delete",
  "where": { "is_active": false }
}

// Response
{
  "message": "Record deleted successfully",
  "data": [/* deleted records */],
  "success": true
}

6. Count Operation

// Count all records
POST /api/Users/crud
{
  "action": "count"
}

// Count with conditions
POST /api/Users/crud
{
  "action": "count",
  "where": {
    "is_active": true,
    "created_at": ">=2024-01-01"
  }
}

// Response
42  // Number of matching records

7. Sum Operation

Use either data.sumColumn (single column) or data.sumFormula (expression with {columnName} placeholders). Optional where filters the rows before summing.

// Sum a single column
POST /api/Employee/crud
{
  "action": "sum",
  "data": {
    "sumColumn": "salary"
  },
  "where": {
    "is_active": true,
    "created_at": ">=2024-01-01"
  }
}

// Sum an expression (e.g. salary + bonus)
POST /api/Employee/crud
{
  "action": "sum",
  "data": {
    "sumFormula": "{salary}+{bonus}"
  },
  "where": {
    "is_active": true
  }
}

// Response
125000  // Sum value (number)

Notes:

  • Use either sumColumn or sumFormula, not both. If both are provided, sumColumn is used.
  • In sumFormula, column names are written as {columnName} and are safely quoted; only letters, numbers, and underscore are allowed in names.
  • Expression guards: sumFormula allows only BODMAS-safe content: numbers (including decimals), operators + - * /, parentheses ( ), and {columnName} placeholders. Any other character (e.g. commas, quotes, SQL) is rejected. Parentheses must be balanced and correctly ordered.
  • Examples: {salary}+{bonus}, ({salary}+{bonus})*0.9, {amount}*1.1, {a}/{b}*100.
  • where supports the same operators as list/count (e.g. >=, ><, %...%, etc.).

8. Replace Operation

Replaces an entire row, keyed on the model's primary key (include the PK in data). Supported on all three engines, but the semantics differ by what each engine can express:

| Engine | Statement | Semantics | | -------- | ------------------------------------- | ------------------------------------------------------------------------------------------------------------------------- | | MySQL | REPLACE INTO | True delete + insert. Columns omitted from data reset to their column DEFAULT (or NULL). | | SQLite | INSERT OR REPLACE | True delete + insert (same as MySQL). | | Postgres | INSERT … ON CONFLICT (pk) DO UPDATE | Merge, not a true replace: columns omitted from data keep their existing values (Postgres has no native REPLACE). |

⚠️ Cross-engine caveat: on Postgres, replace is emulated and behaves like a merge — fields you omit are retained, whereas MySQL/SQLite reset them. If you need portable insert-or-update semantics, prefer upsert. The conflict target defaults to the model's primary key; pass an explicit conflict: ['col', …] to override (required on Postgres if the model has no primary key).

// Replace record — replaces the entire row by primary key
POST /api/Users/crud
{
  "action": "replace",
  "data": {
    "id": 1,
    "username": "john_doe_updated",
    "email": "[email protected]",
    "first_name": "John",
    "last_name": "Doe Updated"
  }
}

// Response
{
  "message": "Record replaced successfully",
  "data": [/* replaced record */],
  "success": true
}

9. Upsert Operation (Insert or Update)

// Upsert record (insert if not exists, update if exists)
POST /api/Users/crud
{
  "action": "upsert",
  "data": {
    "username": "john_doe",
    "email": "[email protected]",
    "first_name": "John",
    "last_name": "Doe"
  },
  "conflict": ["username"]  // Conflict columns for MySQL
}

// Response
{
  "message": "Record upserted successfully",
  "data": [/* upserted record */],
  "success": true
}

10. Sync Operation (Upsert + Delete)

// Sync operation: upsert records and delete others matching where clause
POST /api/Users/crud
{
  "action": "sync",
  "data": {
    "username": "john_doe",
    "email": "[email protected]",
    "first_name": "John",
    "last_name": "Doe"
  },
  "conflict": ["username"],
  "where": {
    "created_at": "<2024-01-01"
  }
}

// Response
{
  "message": "Record synced successfully",
  "data": {
    "insertOrUpdateQuery": [/* upserted records */],
    "deleteQuery": [/* deleted records */]
  },
  "success": true
}

Advanced Querying

Complex Where Conditions

// Multiple operators
POST /api/Users/crud
{
  "action": "list",
  "where": {
    "age": "><18,65",
    "email": "%@example.com",
    "first_name": "[]John,Jane,Bob",
    "is_active": true,
    "created_at": "><2024-01-01,2024-12-31"
  }
}

// NULL checks
POST /api/Users/crud
{
  "action": "list",
  "where": {
    "deleted_at": null
  }
}

OR Conditions with Or: Prefix

Use the Or: prefix on column names to create OR conditions in your queries:

// OR condition on a single column
POST /api/Users/crud
{
  "action": "list",
  "where": {
    "status": "active",
    "Or:name": "%john%"
  }
}
// Generated SQL: WHERE status = 'active' OR name LIKE '%john%'

// Multiple OR conditions
POST /api/Users/crud
{
  "action": "list",
  "where": {
    "is_active": true,
    "Or:first_name": "John",
    "Or:last_name": "Doe"
  }
}
// Generated SQL: WHERE is_active = 1 OR first_name = 'John' OR last_name = 'Doe'

// Combining AND and OR conditions
POST /api/Users/crud
{
  "action": "list",
  "where": {
    "status": "active",
    "age": ">=18",
    "Or:role": "admin",
    "Or:role": "super_admin"
  }
}
// Generated SQL: WHERE status = 'active' AND age >= 18 OR role = 'admin' OR role = 'super_admin'

// OR with operators
POST /api/Users/crud
{
  "action": "list",
  "where": {
    "department": "sales",
    "Or:email": "%@company.com",
    "Or:created_at": ">=2024-01-01"
  }
}
// Generated SQL: WHERE department = 'sales' OR email LIKE '%@company.com' OR created_at >= '2024-01-01'

// OR with NULL checks
POST /api/Users/crud
{
  "action": "list",
  "where": {
    "is_verified": true,
    "Or:verified_at": null
  }
}
// Generated SQL: WHERE is_verified = 1 OR verified_at IS NULL

// OR with IN operator
POST /api/Users/crud
{
  "action": "list",
  "where": {
    "status": "pending",
    "Or:role": ["admin", "moderator", "editor"]
  }
}
// Generated SQL: WHERE status = 'pending' OR role IN ('admin', 'moderator', 'editor')

Array-Based Where Conditions

You can also pass where as an array of condition objects for more complex queries:

// Array of conditions - each object is applied sequentially
POST /api/Users/crud
{
  "action": "list",
  "where": [
    { "status": "active" },
    { "Or:role": "admin" },
    { "Or:role": "moderator" }
  ]
}
// Generated SQL: WHERE status = 'active' OR role = 'admin' OR role = 'moderator'

// Combining multiple AND and OR groups
POST /api/Users/crud
{
  "action": "list",
  "where": [
    { "is_active": true, "age": ">=18" },
    { "Or:status": "verified" },
    { "Or:role": "admin" }
  ]
}
// Generated SQL: WHERE is_active = 1 AND age >= 18 OR status = 'verified' OR role = 'admin'

// Complex filtering with array conditions
POST /api/Users/crud
{
  "action": "list",
  "where": [
    { "department": "engineering" },
    { "Or:department": "product" },
    { "created_at": ">=2024-01-01" }
  ]
}
// Generated SQL: WHERE department = 'engineering' OR department = 'product' AND created_at >= '2024-01-01'

Where Operators Reference

| Operator | Description | Syntax | Example | | ------------- | ----------------------- | -------------- | ------------------------------- | | = (default) | Equals | value | "status": "active" | | >= | Greater than or equal | >=value | "age": ">=18" | | <= | Less than or equal | <=value | "age": "<=65" | | > | Greater than | >value | "price": ">100" | | < | Less than | <value | "price": "<500" | | != | Not equal | !value | "status": "!deleted" | | like | Pattern matching (auto) | %value% | "name": "%john%" | | in | Value in list | []val1,val2 | "status": "[]active,pending" | | notIn | Value not in list | ![]val1,val2 | "role": "![]banned,suspended" | | between | Range (inclusive) | ><min,max | "age": "><18,65" | | notBetween | Outside range | <>min,max | "score": "<>0,50" | | null | IS NULL check | null | "deleted_at": null | | Or:column | OR condition prefix | Or:column | "Or:name": "John" |

Examples:

// Complex query with string-based operators
POST /api/Users/crud
{
  "action": "list",
  "where": {
    "age": ">=18",
    "status": "!deleted",
    "name": "%john%",
    "role": "[]admin,moderator,editor",
    "score": "><50,100"
  }
}
// SQL: WHERE age >= 18 AND status != 'deleted' AND name LIKE '%john%'
//      AND role IN ('admin', 'moderator', 'editor') AND score BETWEEN 50 AND 100

Sorting (orderBy)

// Single sort with object
POST /api/Users/crud
{
  "action": "list",
  "orderBy": { "column": "created_at", "direction": "desc" }
}

// Single sort with string (default: ascending)
POST /api/Users/crud
{
  "action": "list",
  "orderBy": "created_at"
}

// Multiple sorts with array of objects
POST /api/Users/crud
{
  "action": "list",
  "orderBy": [
    { "column": "is_active", "direction": "desc" },
    { "column": "created_at", "direction": "desc" },
    { "column": "last_name", "direction": "asc" }
  ]
}

// Multiple sorts with array of strings (all ascending)
POST /api/Users/crud
{
  "action": "list",
  "orderBy": ["is_active", "created_at", "last_name"]
}

An orderBy object must use the { column, direction } shape. A column→direction map that mirrors the where shape (e.g. { "created_at": "desc" }) is rejected with a KormError (code: 'VALIDATION_FAILED', message naming the offending keys) instead of generating broken SQL. Use { "column": "created_at", "direction": "desc" } or the string form "created_at".

Default ordering. When a list/show request omits orderBy, KORM orders by the model's primary key ascending — i.e. the first column flagged primary (which may be uuid, code, etc., not just id). If the model declares no primary key it falls back to an id column when one exists, and otherwise emits no ORDER BY at all. Pass an explicit orderBy to override.

Joins

// Inner join (using innerJoin parameter)
POST /api/Users/crud
{
  "action": "list",
  "select": ["users.id", "users.username", "user_profiles.bio"],
  "innerJoin": {
    "table": "user_profiles",
    "on": "users.id = user_profiles.user_id"
  },
  "where": { "users.is_active": true }
}

// Left join (using leftJoin parameter)
POST /api/Users/crud
{
  "action": "list",
  "select": ["users.*", "user_profiles.bio"],
  "leftJoin": {
    "table": "user_profiles",
    "on": "users.id = user_profiles.user_id"
  }
}

// Multiple joins (array format)
POST /api/Users/crud
{
  "action": "list",
  "select": ["users.*", "profiles.bio", "roles.name"],
  "leftJoin": [
    { "table": "user_profiles", "on": "users.id = user_profiles.user_id" },
    { "table": "roles", "on": "users.role_id = roles.id" }
  ]
}

// Join with explicit columns
POST /api/Users/crud
{
  "action": "list",
  "innerJoin": {
    "table": "orders",
    "first": "users.id",
    "operator": "=",
    "second": "orders.user_id"
  }
}

Available join types:

  • join - Regular join
  • innerJoin - Inner join
  • leftJoin - Left join
  • rightJoin - Right join

Relational Support with hasRelations

Understanding hasRelations Structure

The hasRelations object in your schema defines relationships between models. Relationships in your schema use the following structure:

One-to-One or Belongs-To Relationship (type: "one")

{
  "UserDetail": {
    "hasRelations": {
      "User": {
        "type": "one",
        "table": "users",
        "localKey": "user_id",
        "foreignKey": "id"
      }
    }
  }
}

One-to-Many Relationship (type: "many")

{
  "User": {
    "hasRelations": {
      "Post": {
        "type": "many",
        "table": "posts",
        "localKey": "id",
        "foreignKey": "user_id"
      },
      "Comment": {
        "type": "many",
        "table": "comments",
        "localKey": "id",
        "foreignKey": "user_id"
      }
    }
  }
}

Many-to-Many Relationship (using through)

For many-to-many relationships, use the through key to specify the join/pivot table:

{
  "User": {
    "hasRelations": {
      "Role": {
        "type": "many",
        "table": "roles",
        "localKey": "id",
        "foreignKey": "id",
        "through": "user_roles",
        "throughLocalKey": "user_id",
        "throughForeignKey": "role_id"
      }
    }
  }
}

Many-to-Many Structure:

  • type: "many" (always use "many" for many-to-many)
  • table: The related table name (e.g., "roles")
  • localKey: The primary key in the current model (e.g., "id")
  • foreignKey: The primary key in the related table (e.g., "id")
  • through: Required - The join/pivot table name (e.g., "user_roles")
  • throughLocalKey: The foreign key in the join table pointing to the current model (e.g., "user_id")
  • throughForeignKey: The foreign key in the join table pointing to the related model (e.g., "role_id")

Using Relationships in Queries

Eager Loading with with

// Load User with related Posts and Comments
POST /api/User/crud
{
  "action": "list",
  "where": { "id": 1 },
  "with": ["Post", "Comment"]
}

// Nested relationships
POST /api/User/crud
{
  "action": "list",
  "where": { "id": 1 },
  "with": ["Post.Comment", "Post.Like"]
}

// Multiple relationships
POST /api/User/crud
{
  "action": "list",
  "where": { "id": 1 },
  "with": ["UserDetail", "Post", "Comment", "Like"]
}

Filtering Related Data with Nested Where

// Get users who have posts with specific hashtag
POST /api/User/crud
{
  "action": "list",
  "where": {
    "Post.hashtag": "#OrganicFarming"
  },
  "with": ["Post"]
}

// Get posts with comments from specific user
POST /api/Post/crud
{
  "action": "list",
  "where": {
    "Comment.user_id": 1
  },
  "with": ["Comment"]
}

// Deeply nested where conditions
POST /api/Post/crud
{
  "action": "list",
  "where": {
    "User.UserRole.Role.name": "admin"
  },
  "with": ["User", "User.UserRole", "User.UserRole.Role"]
}
// SQL: WHERE EXISTS (SELECT * FROM users WHERE users.id = posts.user_id
//        AND EXISTS (SELECT * FROM user_roles WHERE user_roles.user_id = users.id
//          AND EXISTS (SELECT * FROM roles WHERE roles.id = user_roles.role_id AND name = 'admin')))

NOT EXISTS Queries with ! Prefix

Use the ! prefix on relation names to check for the absence of related records:

// Get posts where the user has NO UserRole entries
POST /api/Post/crud
{
  "action": "list",
  "where": {
    "User.!UserRole": true
  },
  "with": ["User"]
}
// SQL: WHERE EXISTS (SELECT * FROM users WHERE users.id = posts.user_id
//        AND NOT EXISTS (SELECT * FROM user_roles WHERE user_roles.user_id = users.id))

// Get users with no posts
POST /api/User/crud
{
  "action": "list",
  "where": {
    "!Post": true
  }
}
// SQL: WHERE NOT EXISTS (SELECT * FROM posts WHERE posts.user_id = users.id)

// Get posts where user has no admin role
POST /api/Post/crud
{
  "action": "list",
  "where": {
    "User.!UserRole.Role.name": "admin"
  }
}
// SQL: WHERE EXISTS (SELECT * FROM users WHERE users.id = posts.user_id
//        AND NOT EXISTS (SELECT * FROM user_roles WHERE user_roles.user_id = users.id
//          AND EXISTS (SELECT * FROM roles WHERE roles.id = user_roles.role_id AND name = 'admin')))

NOT EXISTS Syntax Summary:

| Pattern | Meaning | Use Case | | ------------------------------- | ----------------------------- | ---------------------------------- | | "!RelName": true | NOT EXISTS on direct relation | Users with no posts | | "Parent.!Child": true | NOT EXISTS on nested relation | Posts where user has no roles | | "Parent.!Child.column": value | NOT EXISTS with condition | Posts where user has no admin role |


#### Filtering Eager Loaded Relations with `withWhere`

The `withWhere` parameter allows you to apply additional where conditions when fetching related data. This filters the related rows without affecting the parent query.

```javascript
// Get all users with their posts, but only posts from user_id = 15
POST /api/User/crud
{
  "action": "list",
  "with": ["Post"],
  "withWhere": {
    "Post.user_id": 15
  }
}

// Get users with active posts only
POST /api/User/crud
{
  "action": "list",
  "with": ["Post"],
  "withWhere": {
    "Post.status": "active",
    "Post.is_published": true
  }
}

// Multiple relations with different filters
POST /api/User/crud
{
  "action": "list",
  "with": ["Post", "Comment"],
  "withWhere": {
    "Post.status": "published",
    "Post.created_at": ">=2024-01-01",
    "Comment.is_approved": true
  }
}

// Using operators in withWhere (same as regular where)
POST /api/User/crud
{
  "action": "list",
  "with": ["Post"],
  "withWhere": {
    "Post.title": "%tutorial%",
    "Post.views": ">=100",
    "Post.category": "[]tech,programming"
  }
}

// Using OR conditions in withWhere
POST /api/User/crud
{
  "action": "list",
  "with": ["Post"],
  "withWhere": {
    "Post.status": "published",
    "Or:Post.is_featured": true
  }
}

Key differences between where and withWhere:

| Feature | where (Nested) | withWhere | | -------------------- | ------------------------------------------- | ------------------------------------ | | Affects parent query | Yes (filters parent rows) | No (only filters related rows) | | Purpose | Filter parents that have matching relations | Filter which related rows are loaded | | SQL generated | WHERE EXISTS (subquery) | Additional WHERE on relation query |

Example showing the difference:

// This filters users who have published posts (affects which users are returned)
{
  "action": "list",
  "where": { "Post.status": "published" },
  "with": ["Post"]
}
// Returns: Only users who have at least one published post
// Each user's Post array contains ALL their posts (not filtered)

// This loads all users but only their published posts
{
  "action": "list",
  "with": ["Post"],
  "withWhere": { "Post.status": "published" }
}
// Returns: All users
// Each user's Post array contains ONLY their published posts

withWhere supports all operators:

  • Comparison: ">=18", "<=100", ">50", "<10", "!deleted"
  • LIKE: "%pattern%"
  • IN: "[]val1,val2,val3"
  • NOT IN: "![]val1,val2"
  • BETWEEN: "><min,max"
  • NOT BETWEEN: "<>min,max"
  • NULL: null
  • OR prefix: "Or:column"

Example: Complete Relationship Structure

Example complete relationship structure:

{
  "User": {
    "table": "users",
    "hasRelations": {
      "UserDetail": {
        "type": "one",
        "table": "user_details",
        "localKey": "id",
        "foreignKey": "user_id"
      },
      "Post": {
        "type": "many",
        "table": "posts",
        "localKey": "id",
        "foreignKey": "user_id"
      },
      "Comment": {
        "type": "many",
        "table": "comments",
        "localKey": "id",
        "foreignKey": "user_id"
      },
      "Like": {
        "type": "many",
        "table": "likes",
        "localKey": "id",
        "foreignKey": "user_id"
      }
    }
  },
  "Post": {
    "table": "posts",
    "hasRelations": {
      "User": {
        "type": "one",
        "table": "users",
        "localKey": "user_id",
        "foreignKey": "id"
      },
      "Comment": {
        "type": "many",
        "table": "comments",
        "localKey": "id",
        "foreignKey": "post_id"
      },
      "Like": {
        "type": "many",
        "table": "likes",
        "localKey": "id",
        "foreignKey": "post_id"
      }
    }
  }
}

Custom Relation Hooks

For complex relationships that can't be defined in the schema, you can create custom relation hooks in your model class. This is useful for:

  • Virtual/computed relations
  • Cross-database relations
  • Complex aggregations
  • Custom data transformations

Create a method named get{RelationName}Relation in your model file:

// models/Users.model.js
class Users {
  // Custom relation hook for "Statistics" relation
  async getStatisticsRelation({ rows, relName, model, withTree, controller, relation, qb, db }) {
    // rows = parent rows to attach relation data to
    // db = Knex database instance
    // qb = QueryBuilder instance

    for (const row of rows) {
      // Fetch custom data for each row
      const stats = await db('user_statistics').where('user_id', row.id).first();

      // Attach to row
      row.Statistics = stats || { posts: 0, comments: 0, likes: 0 };
    }
  }

  // Custom relation with aggregation
  async getPostCountRelation({ rows, db }) {
    const userIds = rows.map((r) => r.id);

    const counts = await db('posts')
      .select('user_id')
      .count('* as count')
      .whereIn('user_id', userIds)
      .groupBy('user_id');

    const countMap = new Map(counts.map((c) => [c.user_id, c.count]));

    for (const row of rows) {
      row.PostCount = countMap.get(row.id) || 0;
    }
  }

  // Custom relation from external API or different database
  async getExternalProfileRelation({ rows }) {
    for (const row of rows) {
      // Fetch from external source
      row.ExternalProfile = await fetchFromExternalAPI(row.external_id);
    }
  }
}

module.exports = Users;

Using Custom Relations:

// Use custom relation just like schema-defined relations
POST /api/Users/crud
{
  "action": "list",
  "where": { "is_active": true },
  "with": ["Statistics", "PostCount", "ExternalProfile"]
}

Custom Relation Hook Arguments:

| Argument | Description | | ------------ | ----------------------------------------------------------------------- | | rows | Parent rows to attach relation data to (modify in place) | | relName | The relation name being fetched | | model | Model definition object | | withTree | Nested relations tree for further loading | | controller | ControllerWrapper instance | | relation | Relation definition from schema (may be undefined for custom relations) | | qb | QueryBuilder instance for building queries | | db | Knex database instance for direct queries |

Important Notes

  • type: "one" = One-to-One or Belongs-To relationship
  • type: "many" = One-to-Many relationship
  • through = Required for Many-to-Many relationships (specifies the join table)
  • localKey = The key in the current model
  • foreignKey = The key in the related table
  • throughLocalKey = The key in the join table pointing to current model (for many-to-many)
  • throughForeignKey = The key in the join table pointing to related model (for many-to-many)
  • Custom relation hooks take precedence when relation is not defined in schema

Soft Delete Support

Enabling Soft Delete

Create a model file at models/Users.model.js:

class Users {
  // Enable soft delete for this model
  hasSoftDelete = true;

  // Optional: Custom soft delete hook
  beforeDelete({ model, action, request, context, db, utils, controller }) {
    // Custom logic before soft delete
    console.log('Soft deleting user:', request.where);
  }

  // Optional: Custom hook after soft delete
  afterDelete({ model, action, data, request, context, db, utils, controller }) {
    // Custom logic after soft delete
    console.log('User soft deleted:', data);
  }
}

module.exports = Users;

Using Soft Delete

// When soft delete is enabled, delete action automatically sets deleted_at
POST /api/Users/crud
{
  "action": "delete",
  "where": { "id": 1 }
}

// List operation automatically filters out soft-deleted records
POST /api/Users/crud
{
  "action": "list",
  "where": { "is_active": true }
  // Automatically adds: deleted_at IS NULL
}

// To see soft-deleted records, you need to query directly
// (soft delete only affects list and delete operations)

Model Hooks

Creating Model Hooks

Create a model file at models/Users.model.js:

class Users {
  // Soft delete support (property, not method)
  hasSoftDelete = true;

  // Validation hook - runs before any action
  async validate({ model, action, request, context, db, utils, controller }) {
    if (action === 'create' || action === 'update') {
      if (!request.data.email) {
        throw new Error('Email is required');
      }
      // Check if email already exists
      const existing = await db('users').where('email', request.data.email).first();
      if (existing && existing.id !== request.where?.id) {
        throw new Error('Email already exists');
      }
    }
  }

  // Before hooks - run before the action executes
  // Method naming: before{Action} (e.g., beforeCreate, beforeUpdate, beforeList, beforeDelete)
  async beforeCreate({ model, action, request, context, db, utils, controller }) {
    // Modify request data before insert
    request.data.created_at = new Date();
    request.data.updated_at = new Date();
    return request.data;
  }

  async beforeUpdate({ model, action, request, context, db, utils, controller }) {
    // Modify request data before update
    request.data.updated_at = new Date();
    return request.data;
  }

  async beforeDelete({ model, action, request, context, db, utils, controller }) {
    // Logic before delete (works with both hard and soft delete)
    console.log('Deleting user:', request.where);
  }

  // After hooks - run after the action executes
  // Method naming: after{Action} (e.g., afterCreate, afterUpdate, afterList, afterDelete)
  async afterCreate({ model, action, data, request, context, db, utils, controller }) {
    // data contains the result of the action
    console.log('User created:', data);
    // Send welcome email, trigger notifications, etc.
    return data;
  }

  async afterUpdate({ model, action, data, request, context, db, utils, controller }) {
    console.log('User updated:', data);
    return data;
  }

  async afterList({ model, action, data, request, context, db, utils, controller }) {
    // Modify list results before returning
    return data;
  }

  // Custom action hooks
  // Method naming: on{Action}Action (e.g., onActivateAction, onDeactivateAction)
  async onActivateAction({ model, action, request, context, db, utils, controller }) {
    return await db('users')
      .where(request.where)
      .update({ is_active: true, updated_at: new Date() });
  }

  async onDeactivateAction({ model, action, request, context, db, utils, controller }) {
    return await db('users')
      .where(request.where)
      .update({ is_active: false, updated_at: new Date() });
  }
}

module.exports = Users;

Hook Arguments Reference

| Argument | Description | | ------------ | ------------------------------------------------------------- | | model | Model definition object with table, columns, relations | | action | Current action being performed (create, update, delete, etc.) | | request | The request object containing where, data, etc. | | context | Custom context passed from the controller | | db | Knex database instance for direct queries | | utils | Utility functions | | controller | ControllerWrapper instance | | data | (After hooks only) Result of the action |

Complete Hook Types Reference

| Hook Type | Method Naming | When Called | Use Case | | --------------- | ---------------------- | ---------------------- | --------------------------------------- | | Validate | validate | Before any action | Input validation, authorization | | Before | before{Action} | Before action executes | Modify request data, add timestamps | | After | after{Action} | After action executes | Transform results, trigger side effects | | Custom Action | on{Action}Action | For custom actions | Implement business logic | | Soft Delete | hasSoftDelete = true | During delete/list | Enable soft delete | | Custom Relation | get{RelName}Relation | During eager loading | Custom data fetching |

Available Before/After hooks:

  • beforeCreate / afterCreate
  • beforeUpdate / afterUpdate
  • beforeDelete / afterDelete
  • beforeList / afterList
  • beforeShow / afterShow
  • beforeCount / afterCount
  • beforeReplace / afterReplace
  • beforeUpsert / afterUpsert
  • beforeSync / afterSync

Using Custom Actions

// Call custom action - triggers on{Action}Action hook
POST /api/Users/crud
{
  "action": "activate",
  "where": { "id": 1 }
}

POST /api/Users/crud
{
  "action": "deactivate",
  "where": { "id": 1 }
}

// You can create any custom action
POST /api/Users/crud
{
  "action": "sendWelcomeEmail",
  "where": { "id": 1 }
}
// Triggers: onSendWelcomeEmailAction({ model, action, request, context, db, utils, controller })

Data Validation

Basic Validation

const { validate } = require('@dreamtree-org/korm-js');

// Define validation rules
const userValidationRules = {
  username: 'required|type:string|minLen:3|maxLen:50',
  email: 'required|type:string|maxLen:255',
  first_name: 'required|type:string|maxLen:100',
  last_name: 'required|type:string|maxLen:100',
  age: 'type:number|min:0|max:150',
  is_active: 'type:boolean',
};

// Validate and create user
app.post('/api/users/validate', async (req, res) => {
  try {
    // Validate request data
    const validatedData = await validate(req.body, userValidationRules);

    const result = await korm.processRequest(
      {
        action: 'create',
        data: validatedData,
      },
      'Users'
    );

    res.status(201).json({
      success: true,
      message: 'User created successfully',
      data: result,
    });
  } catch (error) {
    if (error.name === 'ValidationError') {
      return res.status(400).json({
        success: false,
        message: 'Validation failed',
        errors: error.message,
      });
    }

    res.status(500).json({
      success: false,
      message: 'Error creating user',
      error: error.message,
    });
  }
});

Advanced Validation

const { validate } = require('@dreamtree-org/korm-js');

// Advanced validation rules
const advancedRules = {
  username: 'required|type:string|regex:username',
  email: 'required|type:string|regex:email',
  phone: 'regex:phone',
  password: 'required|type:string|minLen:8',
  status: 'in:active,inactive,pending',
  user_id: 'exists:users,id',
};

const customRegex = {
  username: /^[a-zA-Z0-9_]+$/,
  email: /^[^\s@]+@[^\s@]+\.[^\s@]+$/,
  phone: /^\+?[\d\s-()]{10,15}$/,
};

const validatedData = await validate(data, advancedRules, { customRegex });

Nested Requests

// Process multiple related requests in one call
POST /api/Users/crud
{
  "action": "show",
  "where": { "id": 1 },
  "other_requests": {
    "Posts": {
      "action": "list",
      "where": { "user_id": 1 },
      "limit": 10
    },
    "Comments": {
      "action": "list",
      "where": { "user_id": 1 },
      "limit": 5
    }
  }
}

// Response includes nested data
{
  "id": 1,
  "username": "john_doe",
  "email": "[email protected]",
  "other_responses": {
    "Posts": {
      "data": [/* posts */],
      "total": 10
    },
    "Comments": {
      "data": [/* comments */],
      "total": 5
    }
  }
}

Schema Structure

Example Schema (Auto-generated from MySQL)

{
  "Users": {
    "table": "users",
    "alias": "Users",
    "modelName": "Users",
    "columns": {
      "id": "bigint|size:8|unsigned|primaryKey|autoIncrement",
      "username": "varchar|size:255|notNull|unique",
      "email": "varchar|size:255|notNull",
      "first_name": "varchar|size:255",
      "last_name": "varchar|size:255",
      "age": "int|size:4",
      "is_active": "tinyint|size:1|default:1",
      "created_at": "timestamp|default:CURRENT_TIMESTAMP",
      "updated_at": "timestamp|default:CURRENT_TIMESTAMP|onUpdate:CURRENT_TIMESTAMP",
      "deleted_at": "timestamp"
    },
    "seed": [],
    "hasRelations": {},
    "indexes": [
      {
        "name": "idx_users_email",
        "columns": ["email"],
        "unique": true
      }
    ]
  }
}

Column Definition String Format

Column definitions use a pipe-separated string format:

type|modifier1|modifier2|...

Column Modifiers:

| Modifier | Description | Example | | -------------------------- | ------------------ | ---------- | --------------------------- | -------------- | | size:n | Column size | varchar | size:255 | | unsigned¹ | Unsigned integer | int | unsigned | | primaryKey | Primary key column | bigint | primaryKey | | autoIncrement | Auto increment | bigint | primaryKey | autoIncrement | | notNull | Not nullable | varchar | size:255 | notNull | | unique | Unique constraint | varchar | unique | | default:value | Default value | tinyint | default:1 | | onUpdate:value² | On update value | timestamp | onUpdate:CURRENT_TIMESTAMP | | comment:text | Column comment | varchar | comment:User email address | | foreignKey:table:column³ | Foreign key | int | foreignKey:users:id |

¹ Engine-specific. unsigned is honored on MySQL and SQLite. PostgreSQL has no unsigned integer type and silently drops the modifier. ² Engine-specific. onUpdate is honored on MySQL (emitted via ON UPDATE <expr>). PostgreSQL and SQLite log a one-time warning and ignore it — the modifier cannot be expressed inline on those engines. See docs/agents/05-multi-db-parity.md. ³ Auto type-matching. When the referenced table is part of the same schema, syncDatabase() automatically widens the foreign-key column's type to match the referenced primary key, so the FK constraint is always type-compatible. In particular, an autoIncrement primary key is emitted as BIGINT UNSIGNED (MySQL) / BIGSERIAL (PostgreSQL) / INTEGER (SQLite) via Knex's .increments(), so a child column written as int|foreignKey:… is created as BIGINT — you do not need to hand-match the width. Foreign keys to tables outside the schema (pre-existing/third-party) are created exactly as declared.

Dependency ordering. syncDatabase() creates tables in foreign-key dependency order (parents before the children that reference them), so you can declare your models in any order — a child listed before its parent still syncs cleanly (MySQL/PostgreSQL otherwise reject the forward FK reference). FKs to tables outside the schema impose no ordering; a genuine circular FK dependency falls back to declaration order with a warning.

Orphan-row guard. Adding a notNull foreign-key column to a table that already has rows makes the database back-fill those rows (with 0 for a numeric column, or the column default). If that value has no matching parent row, MySQL/PostgreSQL reject the new constraint with a cryptic driver error (MySQL errno 1452, ER_NO_REFERENCED_ROW_2). syncDatabase() now detects this before altering the table and throws a clear KormError (code: 'SYNC_FK_ORPHAN') naming the child column, the parent it points at, the affected row count, and the offending back-fill value — no data is mutated. Resolve it by making the column nullable (existing rows back-fill NULL, which is FK-safe), giving it a default that exists in the parent, populating the column for existing rows before syncing, or inserting the missing parent rows.

syncDatabase(options) — additive-only, resilient, dry-run

syncDatabase() accepts an optional options object that controls how it reconciles drift between your schema and the live database. The defaults are safe: it never drops data and never half-migrates silently.

| Option | Default | Effect | | ----------------- | ------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | dropColumns | false | When false (default), a column that exists in the database but is absent from the schema is left in place (reported as drift, never dropped). Set true to drop such columns. | | prune | — | Alias for dropColumns. | | continueOnError | false | When false (default), the first failing statement throws and aborts the run (fail-fast). When true, a failing statement is skipped and collected, and the rest of the schema still applies. | | dryRun | false | When true, walks the same diff without executing anything and returns the planned operations (destructive drops flagged). The database is not touched. |

Why the additive-only default matters. Previously syncDatabase() dropped any column not declared in the schema — silently destroying data for drifted/legacy columns, and aborting the entire multi-table sync if the drop was blocked (e.g. the column was referenced by a foreign key). The default is now additive (create tables, add columns); destructive drops are opt-in.

Return value. A default sync resolves to undefined (unchanged). When you pass dryRun: true or continueOnError: true, it resolves to a report:

// Preview what a sync would do — change nothing
const plan = await korm.syncDatabase({ dryRun: true });
// {
//   dryRun: true,
//   plan: [{ table: 'users', operation: 'add', column: 'bio', destructive: false }, …],
//   skippedDrops: [{ table: 'products', column: 'warehouse' }],  // drift left in place
//   applied: [], errors: [],
// }

// Drop drifted columns too
await korm.syncDatabase({ dropColumns: true });

// Keep going past failures, then inspect what was skipped
const report = await korm.syncDatabase({ continueOnError: true });
// report.errors → [{ table, operation, column, message }]
// report.applied → [{ table, operation, column }]

Special Default Values:

  • now or now()CURRENT_TIMESTAMP

Example Column Definitions:

{
  "id": "bigint|size:8|unsigned|primaryKey|autoIncrement",
  "user_id": "int|unsigned|notNull|foreignKey:users:id",
  "status": "enum|in:active,inactive,pending|default:pending",
  "created_at": "timestamp|default:now",
  "updated_at": "timestamp|default:now|onUpdate:now",
  "bio": "text|comment:User biography"
}

Seed Data

You can define seed data in the schema to auto-populate tables:

{
  "Roles": {
    "table": "roles",
    "columns": {
      "id": "int|primaryKey|autoIncrement",
      "name": "varchar|size:50|notNull|unique",
      "description": "text"
    },
    "seed": [
      { "name": "admin", "description": "Administrator role" },
      { "name": "user", "description": "Regular user role" },
      { "name": "moderator", "description": "Moderator role" }
    ]
  }
}

Seed data is automatically inserted when syncDatabase() is called and the table is empty.

API Reference

KORM Instance Methods

const { initializeKORM } = require('@dreamtree-org/korm-js');

const korm = initializeKORM({
  db: db, // Knex database instance
  dbClient: 'mysql', // 'mysql', 'mysql2', 'pg', 'postgresql', 'sqlite', 'sqlite3'
  schema: null, // Optional: initial schema object
  resolverPath: null, // Optional: path to models directory (default: process.cwd())
  debug: false, // Optional: enable SQL debugging (default: false)
});

// Process any CRUD request (automatically handles other_requests if present)
const result = await korm.processRequest(requestBody, modelName, context);

// Process request with nested requests (legacy - now same as processRequest)
const result = await korm.processRequestWithOthers(requestBody, modelName, context);

// Set schema manually
korm.setSchema(schemaObject);

// Sync database with schema (additive-only by default: creates tables, adds
// columns; never drops drifted columns unless you opt in). Options:
//   { dropColumns | prune, continueOnError, dryRun } — see "syncDatabase(options)".
await korm.syncDatabase();

// Generate schema from existing database
const schema = await korm.generateSchema();

// Load model class from models/{ModelName}.model.js
const ModelClass = korm.loadModelClass('Users');

// Get model instance
const modelInstance = korm.getModelInstance(modelDef);

ProcessRequest Options

| Parameter | Type | Description | | ---------------- | -------------------- | ------------------------------------------------------------------------------------ | | action | string | Action to perform (list, show, create, update, delete, count, replace, upsert, sync) | | where | object/array | Filter conditions | | data | object/array | Data for create/update operations | | select | array/string | Columns to select | | orderBy | object/array/string | Sorting configuration | | limit | number | Maximum records to return | | offset | number | Records to skip | | page | number | Page number (alternative to offset) | | with | array | Related models to eager load | | withWhere | object | Filter conditions for eager loaded relations | | groupBy | array/string | Group by columns | | having | object | Having conditions | | distinct | boolean/array/string | Distinct results | | join | object/array | Join configuration | | leftJoin | object/array | Left join configuration | | rightJoin | object/array | Right join configuration | | innerJoin | object/array | Inner join configuration | | conflict | array | Conflict columns for upsert | | other_requests | object | Nested requests for related models |

ProcessRequest Actions Summary

| Action | Description | Required Fields | | --------- | -------------------------------------------------------- | ---------------------------------------------------------------- | | list | Get multiple records | None (optional: where, select, orderBy, limit, offset) | | show | Get single record | where | | create | Create new record | data | | update | Update record(s) | where, data | | delete | Delete record(s) | where | | count | Count records | None (optional: where) | | sum | Sum column or expression | data.sumColumn or data.sumFormula (optional: where) | | replace | Replace full row by PK (all engines; pg = merge, see §8) | data (optional: conflict) | | upsert | Insert or update | data, conflict | | sync | Upsert + delete | data, conflict, where |

Validation Rules

| Rule | Description | Example | | --------------------- | -------------------------------------------- | ------------------------------ | | required | Field is required | 'required' | | type:string | Field must be string | 'type:string' | | type:number | Field must be number | 'type:number' | | type:boolean | Field must be boolean | 'type:boolean' | | type:array | Field must be array | 'type:array' | | type:object | Field must be object | 'type:object' | | type:longText | Field must be string > 255 chars | 'type:longText' | | minLen:n | Minimum string/array length | 'minLen:3' | | maxLen:n | Maximum string/array length | 'maxLen:255' | | min:n | Minimum numeric value | 'min:0' | | max:n | Maximum numeric value | 'max:150' | | in:val1,val2 | Value must be in list | 'in:active,inactive,pending' | | regex:name | Custom regex pattern (define in options) | 'regex:email' | | call:name | Custom callback function (define in options) | 'call:myValidator' | | exists:table,column | Value must exist in database table | 'exists:users,id' | | default:value | Default value if not provided | 'default:active' |

Rule Chaining: Combine multiple rules with | pipe character:

{
  username: 'required|type:string|minLen:3|maxLen:50',
  email: 'required|type:string|regex:email',
  age: 'type:number|min:0|max:150',
  status: 'in:active,inactive|default:active'
}

Library Exports

const {
  initializeKORM, // Initialize KORM with database connection
  helperUtility, // Utility functions (file operations, string manipulation)
  emitter, // Event emitter instance
  validate, // Validation function
  logger, // Logger utility instance
  lib, // Additional utilities
  LibClasses, // Library classes (Emitter)
} = require('@dreamtree-org/korm-js');

// lib contains:
// - createValidationMiddleware(rules, options) - Express middleware
// - validateEmail(email) - Email validation
// - validatePassword(password) - Password strength validation
// - validatePhone(phone) - Phone number validation
// - validatePAN(pan) - PAN validation (India)
// - validateAadhaar(aadhaar) - Aadhaar validation (India)

// helperUtility.file contains:
// - readJSON(path) - Read JSON file
// - writeJSON(path, data) - Write JSON file
// - createDirectory(path) - Create directory

Logger Utility

KORM includes a built-in logger utility with configurable log levels for debugging and monitoring.

Basic Usage

const { logger } = require('@dreamtree-org/korm-js');

// Log methods (from most to least verbose)
logger.debug('Detailed debugging information');
logger.log('General log message');
logger.info('Informational message');
logger.warn('Warning message');
logger.error('Error message');

Log Levels

| Level | Value | Description | | ------- | ----- | ----------------------------- | | NONE | 0 | Disable all logging | | ERROR | 1 | Only errors | | WARN | 2 | Warnings and errors (default) | | INFO | 3 | Info, warnings, and errors | | LOG | 4 | General logs and above | | DEBUG | 5 | All messages (most verbose) |

Configuration

const { logger } = require('@dreamtree-org/korm-js');

// Set log level programmatically
logger.setLevel('debug'); // Show all logs
logger.setLevel('warn'); // Only warnings and errors (default)
logger.setLevel('error'); // Only errors
logger.setLevel('none'); // Disable all logging

// Enable/disable logging
logger.disable(); // Temporarily disable all logging
logger.enable(); // Re-enable logging

Environment Variable

Set the log level via environment variable:

# In your .env file or environment
KORM_LOG_LEVEL=debug   # Show all logs
KORM_LOG_LEVEL=warn    # Only warnings and errors (default)
KORM_LOG_LEVEL=error   # Only errors
KORM_LOG_LEVEL=none    # Disable logging
// The logger automatically reads KORM_LOG_LEVEL on initialization
// KORM_LOG_LEVEL=debug node app.js

Creating Child Loggers

Create child loggers with custom prefixes for different modules:

const { logger } = require('@dreamtree-org/korm-js');

// Create a child logger for a specific module
const authLogger = logger.child('[Auth]');
authLogger.info('User logged in'); // Output: [KORM][Auth] [INFO] User logged in

const dbLogger = logger.child('[DB]');
dbLogger.debug('Query executed'); // Output: [KORM][DB] [DEBUG] Query executed

Logger Options

const { Logger } = require('@dreamtree-org/korm-js').logger;

// Create a custom logger instance
const customLogger = new Logger({
  prefix: '[MyApp]', // Custom prefix (default: '[KORM]')
  enabled: true, // Enable/disable logging (default: true)
  level: 'debug', // Log level (default: 'warn' or KORM_LOG_LEVEL)
  timestamps: true, // Include