{"id":46672663,"url":"https://github.com/ichiriac/normaljs","last_synced_at":"2026-03-14T20:21:13.535Z","repository":{"id":25639232,"uuid":"29074603","full_name":"ichiriac/normaljs","owner":"ichiriac","description":"Normal : Node Object Relational Mapper Abstraction Layer","archived":false,"fork":false,"pushed_at":"2026-01-17T21:34:50.000Z","size":846,"stargazers_count":2,"open_issues_count":3,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2026-01-18T03:35:25.185Z","etag":null,"topics":["activerecord","javascript","javascript-vanilla","knex","mariadb","mysql","nodejs","orm","postgres","postgresql","sql","sqlite","transaction","typescript"],"latest_commit_sha":null,"homepage":"https://ichiriac.github.io/normaljs/","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/ichiriac.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2015-01-10T23:15:15.000Z","updated_at":"2026-01-17T21:33:48.000Z","dependencies_parsed_at":"2025-12-25T21:08:31.807Z","dependency_job_id":null,"html_url":"https://github.com/ichiriac/normaljs","commit_stats":null,"previous_names":["ichiriac/normaljs"],"tags_count":6,"template":false,"template_full_name":null,"purl":"pkg:github/ichiriac/normaljs","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ichiriac%2Fnormaljs","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ichiriac%2Fnormaljs/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ichiriac%2Fnormaljs/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ichiriac%2Fnormaljs/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/ichiriac","download_url":"https://codeload.github.com/ichiriac/normaljs/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ichiriac%2Fnormaljs/sbom","scorecard":{"id":480287,"data":{"date":"2025-08-11","repo":{"name":"github.com/ichiriac/normal","commit":"1beddc8de34fe0ff990c3b2d14564515e9dbb466"},"scorecard":{"version":"v5.2.1-40-gf6ed084d","commit":"f6ed084d17c9236477efd66e5b258b9d4cc7b389"},"score":3,"checks":[{"name":"Packaging","score":-1,"reason":"packaging workflow not detected","details":["Warn: no GitHub/GitLab publishing workflow detected."],"documentation":{"short":"Determines if the project is published as a package that others can easily download, install, easily update, and uninstall.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#packaging"}},{"name":"Code-Review","score":0,"reason":"Found 0/10 approved changesets -- score normalized to 0","details":null,"documentation":{"short":"Determines if the project requires human code review before pull requests (aka merge requests) are merged.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#code-review"}},{"name":"Binary-Artifacts","score":10,"reason":"no binaries found in the repo","details":null,"documentation":{"short":"Determines if the project has generated executable (binary) artifacts in the source repository.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#binary-artifacts"}},{"name":"SAST","score":0,"reason":"no SAST tool detected","details":["Warn: no pull requests merged into dev branch"],"documentation":{"short":"Determines if the project uses static code analysis.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#sast"}},{"name":"Dangerous-Workflow","score":-1,"reason":"no workflows found","details":null,"documentation":{"short":"Determines if the project's GitHub Action workflows avoid dangerous patterns.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#dangerous-workflow"}},{"name":"Maintained","score":0,"reason":"0 commit(s) and 0 issue activity found in the last 90 days -- score normalized to 0","details":null,"documentation":{"short":"Determines if the project is \"actively maintained\".","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#maintained"}},{"name":"Token-Permissions","score":-1,"reason":"No tokens found","details":null,"documentation":{"short":"Determines if the project's workflows follow the principle of least privilege.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#token-permissions"}},{"name":"Pinned-Dependencies","score":-1,"reason":"no dependencies found","details":null,"documentation":{"short":"Determines if the project has declared and pinned the dependencies of its build process.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#pinned-dependencies"}},{"name":"CII-Best-Practices","score":0,"reason":"no effort to earn an OpenSSF best practices badge detected","details":null,"documentation":{"short":"Determines if the project has an OpenSSF (formerly CII) Best Practices Badge.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#cii-best-practices"}},{"name":"Security-Policy","score":0,"reason":"security policy file not detected","details":["Warn: no security policy file detected","Warn: no security file to analyze","Warn: no security file to analyze","Warn: no security file to analyze"],"documentation":{"short":"Determines if the project has published a security policy.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#security-policy"}},{"name":"Vulnerabilities","score":10,"reason":"0 existing vulnerabilities detected","details":null,"documentation":{"short":"Determines if the project has open, known unfixed vulnerabilities.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#vulnerabilities"}},{"name":"Fuzzing","score":0,"reason":"project is not fuzzed","details":["Warn: no fuzzer integrations found"],"documentation":{"short":"Determines if the project uses fuzzing.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#fuzzing"}},{"name":"License","score":10,"reason":"license file detected","details":["Info: project has a license file: LICENSE:0","Info: FSF or OSI recognized license: MIT License: LICENSE:0"],"documentation":{"short":"Determines if the project has defined a license.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#license"}},{"name":"Signed-Releases","score":-1,"reason":"no releases found","details":null,"documentation":{"short":"Determines if the project cryptographically signs release artifacts.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#signed-releases"}},{"name":"Branch-Protection","score":0,"reason":"branch protection not enabled on development/release branches","details":["Warn: branch protection not enabled for branch 'master'"],"documentation":{"short":"Determines if the default and release branches are protected with GitHub's branch protection settings.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#branch-protection"}}]},"last_synced_at":"2025-08-19T16:26:02.784Z","repository_id":25639232,"created_at":"2025-08-19T16:26:02.784Z","updated_at":"2025-08-19T16:26:02.784Z"},"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":30274929,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-03-08T20:45:49.896Z","status":"ssl_error","status_checked_at":"2026-03-08T20:45:49.525Z","response_time":56,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.6:443 state=error: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["activerecord","javascript","javascript-vanilla","knex","mariadb","mysql","nodejs","orm","postgres","postgresql","sql","sqlite","transaction","typescript"],"created_at":"2026-03-08T22:08:52.067Z","updated_at":"2026-03-08T22:08:55.551Z","avatar_url":"https://github.com/ichiriac.png","language":"TypeScript","readme":"\u003cdiv align=\"center\"\u003e\n\u003cimg src=\"https://github.com/ichiriac/normal/raw/main/docs/assets/normal-logo.svg\" alt=\"Normal ORM\" height=\"120\" /\u003e\n\n# NormalJS\n\nThe simple, straightforward, and most advanced Node.js ORM — without the bloat.\n\nBuild data-rich apps with clean models, powerful relations, and first-class DX. NormalJS blends a tiny API with serious capabilities: schema sync, relations (1:n, n:m), transactions, model extension, and active-record-style instances. All in plain JavaScript.\n\n\u003c/div\u003e\n\n## Why NormalJS\n\n- Simple: minimal surface area. Define models with a static `fields` object and go.\n- Transaction-first: fully isolated repos inside transactions without leaking state.\n- Advanced caching: centralized in-memory cache shared across child processes, with UDP-based clustering for peer invalidation.\n- Powerful: relations (1:n, n:m), transactions, model mixins/extension, inheritance with discriminators, relation proxies.\n- Productive: active records you can call methods on; lazy, ID-first reads that auto-hydrate fields; request-level caching with invalidation markers.\n- Portable: works with Postgres and SQLite. Uses Knex under the hood.\n\n### What makes NormalJS different for complex domains\n\n- Extensible field system: add custom field types that control serialization, JSON output, schema, and lifecycle hooks.\n- Model extension and overwrite: register multiple classes with the same `static _name` to merge fields and attach static/instance behavior over time.\n- Inheritance with discriminators: share a base model schema and behavior; allocate correct child records automatically.\n- Schema sync (base synchronization): generate and evolve tables from model fields with migration-safe helpers.\n- Clear split of responsibilities: simple static APIs for model-level operations, and instance methods/getters for active records.\n\n## Install\n\n[![Coverage Status](https://coveralls.io/repos/github/ichiriac/normal/badge.svg?branch=main)](https://coveralls.io/github/ichiriac/normal?branch=main)\n[![license](https://img.shields.io/github/license/ichiriac/normal)](https://github.com/ichiriac/normal/blob/main/LICENSE)\n[![NPM version](https://badge.fury.io/js/normaljs.svg)](http://badge.fury.io/js/normaljs)\n\n```bash\nnpm install normaljs pg -y\n```\n\n### TypeScript Support\n\nNormalJS is written in TypeScript and includes full type definitions. TypeScript users get:\n\n- **Type-safe model definitions** with autocomplete for fields and relations\n- **Type inference** for query results and active records\n- **Generic types** for models and records\n- **Declaration files** (`.d.ts`) for all public APIs\n\n```bash\nnpm install normaljs pg\n# TypeScript types are included - no @types package needed\n```\n\n```typescript\nclass MyModel {\n  static table = 'my_table';\n  static fields = {\n    /* ... */\n  };\n}\n```\n\n## Database engines\n\nNormalJS supports these SQL databases via Knex:\n\n\u003cdiv align=\"center\"\u003e\n\n\u003ca href=\"https://www.postgresql.org/\"\u003e\u003cimg alt=\"PostgreSQL\" src=\"https://img.shields.io/badge/-PostgreSQL-4169E1?style=for-the-badge\u0026logo=postgresql\u0026logoColor=white\u0026logoSize=auto\" /\u003e\u003c/a\u003e\n\u003ca href=\"https://mariadb.org/\"\u003e\u003cimg alt=\"MariaDB\" src=\"https://img.shields.io/badge/-MariaDB-003545?style=for-the-badge\u0026logo=mariadb\u0026logoColor=white\u0026logoSize=auto\" /\u003e\u003c/a\u003e\n\u003ca href=\"https://www.mysql.com/\"\u003e\u003cimg alt=\"MySQL\" src=\"https://img.shields.io/badge/-MySQL-4479A1?style=for-the-badge\u0026logo=mysql\u0026logoColor=white\u0026logoSize=auto\" /\u003e\u003c/a\u003e\n\u003ca href=\"https://www.cockroachlabs.com/product/cockroachdb/\"\u003e\u003cimg alt=\"CockroachDB\" src=\"https://img.shields.io/badge/-CockroachDB-6933FF?style=for-the-badge\u0026logo=cockroachlabs\u0026logoColor=white\u0026logoSize=auto\" /\u003e\u003c/a\u003e\n\u003ca href=\"https://www.sqlite.org/index.html\"\u003e\u003cimg alt=\"SQLite\" src=\"https://img.shields.io/badge/-SQLite-003B57?style=for-the-badge\u0026logo=sqlite\u0026logoColor=white\u0026logoSize=auto\" /\u003e\u003c/a\u003e\n\u003ca href=\"https://www.oracle.com/database/\"\u003e\u003cimg alt=\"Oracle\" src=\"https://img.shields.io/badge/-Oracle-F80000?style=for-the-badge\u0026logo=oracle\u0026logoColor=white\u0026logoSize=auto\" /\u003e\u003c/a\u003e\n\u003ca href=\"https://www.microsoft.com/sql-server/\"\u003e\u003cimg alt=\"Microsoft SQL Server\" src=\"https://img.shields.io/badge/-Microsoft%20SQL%20Server-CC2927?style=for-the-badge\u0026logo=microsoftsqlserver\u0026logoColor=white\u0026logoSize=auto\" /\u003e\u003c/a\u003e\n\u003ca href=\"https://aws.amazon.com/redshift/\"\u003e\u003cimg alt=\"Amazon Redshift\" src=\"https://img.shields.io/badge/-Amazon%20Redshift-8C4FFF?style=for-the-badge\u0026logo=amazonredshift\u0026logoColor=white\u0026logoSize=auto\" /\u003e\u003c/a\u003e\n\n\u003c/div\u003e\n\nNote: You only need the driver for the database(s) you use (e.g., `pg` for PostgreSQL, `sqlite3` or `better-sqlite3` for SQLite).\n\n## 60‑second Quickstart\n\n### TypeScript\n\n```typescript\n// index.ts\nimport { Connection, Repository } from 'normaljs';\n\n// 1) Create a connection (SQLite in-memory shown; Postgres or MySQL also supported)\nconst db = new Connection({ client: 'sqlite3', connection: { filename: ':memory:' } });\n\n// 2) Define models\nclass Users {\n  static table = 'users';\n  static fields = {\n    id: 'primary' as const,\n    firstname: { type: 'string', required: true },\n    lastname: { type: 'string', required: true },\n    email: { type: 'string', unique: true, required: true },\n    created_at: { type: 'datetime', default: () =\u003e new Date() },\n    updated_at: { type: 'datetime', default: () =\u003e new Date() },\n  };\n\n  get name() {\n    return `${this.firstname} ${this.lastname}`;\n  }\n}\n// Override readonly 'name' property for NormalJS\nObject.defineProperty(Users, 'name', { value: 'Users', configurable: true });\n\nclass Posts {\n  static table = 'posts';\n  static fields = {\n    id: 'primary' as const,\n    title: { type: 'string', required: true },\n    content: { type: 'string', required: true },\n    author_id: { type: 'many-to-one', required: true, model: 'Users' },\n  };\n}\nObject.defineProperty(Posts, 'name', { value: 'Posts', configurable: true });\n\n// 3) Register \u0026 sync\nconst repo = new Repository(db);\nrepo.register({ Users, Posts });\nawait repo.sync({ force: true });\n\n// 4) Use it\nconst u = await repo\n  .get('Users')\n  .create({ firstname: 'Ada', lastname: 'Lovelace', email: 'ada@example.com' });\nconst p = await repo.get('Posts').create({ title: 'Hello', content: 'World', author_id: u.id });\nconsole.log(u.name); // \"Ada Lovelace\"\n```\n\n### JavaScript (CommonJS)\n\n```js\n// index.js\nconst { Connection, Repository } = require('normaljs');\n\n// 1) Create a connection (SQLite in-memory shown; Postgres or MySQL also supported)\nconst db = new Connection({ client: 'sqlite3', connection: { filename: ':memory:' } });\n\n// 2) Define models\nclass Users {\n  static table = 'users';\n  static fields = {\n    id: 'primary',\n    firstname: { type: 'string', required: true },\n    lastname: { type: 'string', required: true },\n    email: { type: 'string', unique: true, required: true },\n    created_at: { type: 'datetime', default: () =\u003e new Date() },\n    updated_at: { type: 'datetime', default: () =\u003e new Date() },\n  };\n\n  get name() {\n    return `${this.firstname} ${this.lastname}`;\n  }\n}\n// Override readonly 'name' property for NormalJS\nObject.defineProperty(Users, 'name', { value: 'Users', configurable: true });\n\nclass Posts {\n  static table = 'posts';\n  static fields = {\n    id: 'primary',\n    title: { type: 'string', required: true },\n    content: { type: 'string', required: true },\n    author_id: { type: 'many-to-one', required: true, model: 'Users' },\n  };\n}\nObject.defineProperty(Posts, 'name', { value: 'Posts', configurable: true });\n\n// 3) Register \u0026 sync\nconst repo = new Repository(db);\nrepo.register({ Users, Posts });\nawait repo.sync({ force: true });\n\n// 4) Use it\nconst u = await repo\n  .get('Users')\n  .create({ firstname: 'Ada', lastname: 'Lovelace', email: 'ada@example.com' });\nconst p = await repo.get('Posts').create({ title: 'Hello', content: 'World', author_id: u.id });\nconsole.log(u.name); // \"Ada Lovelace\"\n```\n\n### Modeling big domains, simply\n\nStatic methods live on models; instance methods live on records. You can extend models incrementally or inherit from a base model.\n\n```js\n// Extension: register the same model name again to add fields + behavior\nclass Users {\n  static _name = 'Users';\n  static fields = { id: 'primary' };\n}\n\n// Extend Users with fields and static/instance APIs\nclass UsersExt {\n  static _name = 'Users';\n  static fields = { email: 'string' };\n  static byEmail(email) {\n    return this.where({ email }).first(); // simple, model-scoped static API\n  }\n  get domain() {\n    return this.email?.split('@')[1] || null; // instance API on active record\n  }\n}\n\n// Inheritance: child model shares base structure and behavior\nclass Payment {\n  static _name = 'Payment';\n  static fields = { id: 'primary', amount: 'float' };\n}\nclass CardPayment {\n  static _name = 'CardPayment';\n  static inherits = 'Payment';\n  static fields = { pan: 'string' };\n}\n\nrepo.register(Users);\nrepo.register(UsersExt); // extension merged\nrepo.register({ Payment, CardPayment });\n```\n\n## Features at a glance\n\n- Models\n  - Simple class with `static _name`, `static table`, `static fields`.\n  - Extension system: register multiple times with same `static _name` to add/override fields and behavior.\n  - Inheritance with discriminators for polymorphic models.\n  - **NEW**: Model scopes for reusable query filters and eager loading patterns.\n- Fields\n  - Built-ins: primary, integer/float, string/text, boolean, date/datetime, enum, json, reference.\n  - Constraints: `default`, `required`, `unique`, `index`.\n  - Custom fields: implement serialization, JSON, schema, and lifecycle hooks.\n  - **NEW**: Model-level indexes for composite, unique, and partial indexes (e.g., `static indexes = { idx_name: { fields: ['email', 'company'], unique: true } }`).\n- Relations\n  - 1:n via `one-to-many` (e.g., `comments: { type: 'one-to-many', foreign: 'Comments.post_id' }`).\n  - n:m via paired `many-to-many` (auto-join table).\n  - Relation proxies on instances: `add`, `remove`, `load`.\n  - **NEW**: Automatic join generation for relational filters (e.g., `where({ 'author.organization.name': 'ACME' })`).\n- Scopes\n  - Define reusable query patterns at the model level.\n  - Support for `defaultScope` applied to all queries unless bypassed.\n  - Parameterized scopes via functions.\n  - Composable scopes with deterministic merging.\n  - **NEW**: See `docs/scopes.md` for comprehensive guide.\n- Transactions\n  - `repo.transaction(async (tx) =\u003e { /* ... */ })` gives an isolated tx-bound repository.\n  - Post-commit cache flush of changed records.\n- Active records\n  - Rows are wrapped into instances; instance methods and getters work naturally.\n  - Default reads select only `id` (fast), with lazy hydration from cache/DB.\n- Cache and discovery\n  - Request-level caching via `.cache(ttl)` and entry cache per `Model:ID`.\n  - Per-model invalidation markers (`$Model`) to evict request caches without dropping entry caches.\n  - Centralized in-memory cache across processes with UDP-based clustering.\n  - Discovery protocol auto-syncs peer list for invalidations.\n- Repository Context\n  - **NEW**: Repository-wide context accessible in transactions, models, and records.\n  - `get_context(key, defaultValue)` retrieves context values.\n  - `set_context(key, value)` stores context values.\n  - Transactions inherit parent context but modifications are isolated.\n  - Use cases: multi-tenancy, user context, feature flags, request metadata.\n- Schema sync\n  - Create/update tables from model fields with `repo.sync()`.\n  - Migration-safe helpers for column replacement and index updates.\n\nSee full field reference in `docs/fields.md`.\n\n### More docs\n\n- `docs/models.md` — Model definitions, inheritance, and extension system.\n- `docs/fields.md` — Built-in field types and options.\n- `docs/scopes.md` — **NEW**: Model scopes for reusable query filters.\n- `docs/requests.md` — Request API, criteria DSL, and request-level caching.\n- `docs/relational-filters.md` — **NEW**: Automatic joins for relational field filters.\n- `docs/cache.md` — Cache architecture, connection options, discovery, and model cache options.\n- `docs/custom-fields.md` — In-depth custom fields with hooks and a file-storage example.\n- `docs/adoption-sequelize.md` — Step-by-step migration guide from Sequelize.\n\n## Demo\n\nExplore `demo/` for a working blog schema (Users, Posts, Tags, Comments) and a CRM and Stocks example.\n\n## License\n\nThe MIT License (MIT)\n","funding_links":[],"categories":[],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fichiriac%2Fnormaljs","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fichiriac%2Fnormaljs","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fichiriac%2Fnormaljs/lists"}