# SlintORM — AI Reference > Full plain-text reference of the SlintORM API for AI models and LLM context windows. > Available at: https://slintorm-docs.vercel.app/llms.txt - GitHub: https://github.com/emeraldlinks/slintorm - npm: https://www.npmjs.com/package/slintorm - Author: Joseph Christopher - Organization: emeraldlinks - License: MIT - Version: 1.5.1 --- ## What is SlintORM SlintORM is a lightweight, fully-typed TypeScript ORM for SQLite, PostgreSQL, MySQL, and MongoDB. It runs in Node.js, serverless functions, and V8 isolate edge runtimes (Cloudflare Workers, Vercel Edge, Deno Deploy). It has zero runtime dependencies — all database drivers are loaded dynamically at connect time and installed only when needed. Key design decisions: - Models are plain TypeScript interfaces with annotation comments — no separate schema files - Migrations are derived automatically from interface definitions — no hand-written migration files - Schema is pre-generated at build time and passed as a plain object at runtime — no filesystem reads in edge runtimes - All driver imports are dynamic so bundlers tree-shake unused drivers - `migrate()` is a Node.js-only build-step function — never called in edge runtimes --- ## Installation ```bash npm install slintorm # Install only the driver you need: npm install better-sqlite3 # SQLite (preferred, synchronous, WAL mode) npm install sqlite3 sqlite # SQLite (async fallback) npm install pg # PostgreSQL npm install mysql2 # MySQL / PlanetScale / TiDB npm install mongodb # MongoDB ``` All database drivers are optional — SlintORM installs none of them. Install only the driver(s) you actually use. For Next.js / Turbopack, add to next.config.ts: ```typescript const nextConfig = { serverExternalPackages: ['better-sqlite3', 'sqlite3'], }; ``` --- ## Initialisation ### Recommended pattern (new ORMManager) ```typescript import ORMManager from 'slintorm'; import { schema, type ModelMap } from './schema/generated'; const orm = new ORMManager({ driver: 'sqlite', // 'sqlite' | 'postgres' | 'mysql' | 'mongodb' databaseUrl: './dev.db', // connection string or file path dir: './src', // directory containing TypeScript model interfaces logs: false, // log SQL to stdout modelMap: {} as ModelMap, // type-only — enables typed orm.db store schema, // pre-built schema from generated.ts (optional in Node.js, required on edge) }); await orm.migrate(); // Node.js only — never call in edge runtimes export const db = orm.db; // typed DBStore ``` ### ORMManagerConfig — all options | Option | Type | Required | Description | |-------------|---------------------------------------------|----------|-------------| | driver | 'sqlite' \| 'postgres' \| 'mysql' \| 'mongodb' | Yes | Database driver | | databaseUrl | string | Yes | Connection string or file path | | dir | string | No | Source directory for schema scanning (default: process.cwd()) | | logs | boolean | No | Log all SQL queries to stdout (default: false) | | schema | object | No* | Pre-built schema from generated.ts. Optional in Node.js, required in edge runtimes | | modelMap | object | No | Type-only value for typed db store. Use {} as ModelMap | *schema is required in edge runtimes (Cloudflare Workers, Next.js Edge, Deno Deploy) ### Driver-specific connection strings ``` SQLite: ./path/to/file.db or :memory: Postgres: postgresql://user:pass@host:5432/database MySQL: mysql://user:pass@host:3306/database MongoDB: mongodb://user:pass@host:27017/database mongodb+srv://user:pass@cluster.mongodb.net/database ``` --- ## Model Definitions Models are plain TypeScript interfaces. Annotations in `// comment` lines above each field control column types, constraints, and relations. ```typescript interface User { id: number; // @auto;primaryKey // @unique email: string; name: string; // @nullable bio: string; // @default:user role: string; // @length:500 summary: string; // @json settings: Record; // @enum:(pending,active,banned) status: string; // @index teamId: number; // @softDelete deletedAt: string; createdAt: string; // injected automatically updatedAt: string; // injected automatically } ``` ### Field annotation reference — all supported tags | Annotation | SQL effect | |-------------------|------------| | @auto | AUTOINCREMENT / SERIAL | | @primaryKey | PRIMARY KEY | | @unique | UNIQUE constraint | | @index | CREATE INDEX | | @nullable | allows NULL | | @not null | NOT NULL constraint (explicit) | | @length:N | VARCHAR(N) | | @json | TEXT column, auto JSON.stringify/parse | | @softDelete | marks deletedAt — queries auto-filter WHERE deletedAt IS NULL | | @enum:(a,b,c) | CHECK constraint (SQLite/Postgres) or ENUM type (MySQL) | | @default:value | DEFAULT value | | @comment:text | column comment (MySQL/Postgres) | | @random:string:N | generates random string of length N on insert | | @random:number:N | generates random N-digit number on insert | | @random:string(N) | alternate syntax for @random:string:N | | @random:number(N) | alternate syntax for @random:number:N | | @relation ... | declares a relation (see Relations section) | | @relationship ... | alias for @relation | | @mask:preset | masks output on read (ssn, creditcard, email, phone, showFirst, showLast, char, pattern) | | @omitdb | excludes field from DB — no column created, excluded from all read/write | | @omitjson | stores in DB but strips from read results unless explicitly .select()ed | | @omitmigrate | migrator ignores column — manual DDL; field still usable in queries | Annotations placed on the line directly above the field, or inline after the semicolon on the same line: ```typescript // @unique email: string; // stacked annotation (preceding comment line) field: string; // @unique // inline annotation (same line, after field declaration) ``` Both `@`-prefixed and bare variants are accepted. Inline annotations (after the field) are extracted from trailing `//` comments and merged with any preceding stacked annotations. ### Automatic columns Every table gets `createdAt` and `updatedAt` automatically. `deletedAt` is added only when a field is annotated with `@softDelete`. --- ## defineModel ```typescript // Signature orm.defineModel(table: string, modelName: string, hooks?: ModelHooks): ModelAPI // Examples const User = orm.defineModel('users', 'User'); const Post = orm.defineModel('posts', 'Post', { async onCreateBefore(data) { return { ...data, slug: data.title.toLowerCase().replace(/\s+/g, '-') }; }, }); ``` --- ## ModelAPI — all 24 methods Every model returned by `defineModel` has these methods: ### Mutations ```typescript // insert(data) — returns EntityWithUpdate const user = await User.insert({ email: 'joe@example.com', name: 'Joe' }); // insertMany(data[]) — batch insert, single transaction on SQLite await User.insertMany([ { email: 'a@a.com', name: 'Alice' }, { email: 'b@b.com', name: 'Bob' }, ]); // update(filter, data) — returns number of affected rows await User.update({ id: 1 }, { name: 'Joseph' }); // updateMany(filter, data) — returns number of affected rows await User.updateMany({ role: 'guest' }, { role: 'user' }); // delete(filter) — hard delete or soft delete if @softDelete present await User.delete({ id: 1 }); // deleteMany(filter) — returns number of rows deleted await User.deleteMany({ active: false }); // upsert(data, conflictKey) — insert or update on conflict await User.upsert({ email: 'joe@example.com', name: 'Joe' }, 'email'); // Postgres: ON CONFLICT (email) DO UPDATE // MySQL: ON DUPLICATE KEY UPDATE // SQLite/MongoDB: manual get-then-insert-or-update // truncate() — delete ALL rows await User.truncate(); // restore(filter) — un-soft-delete: sets deletedAt = NULL await User.restore({ id: 1 }); ``` ### Reads ```typescript // get(filter) — returns EntityWithUpdate | null const user = await User.get({ email: 'joe@example.com' }); // getAll(filter?) — returns EntityWithUpdate[] const users = await User.getAll(); const admins = await User.getAll({ role: 'admin' }); // findOrCreate(filter, defaults?) — returns { record, created } const { record, created } = await User.findOrCreate( { email: 'joe@example.com' }, { name: 'Joe', role: 'user' } ); // exists(filter) — returns boolean const taken = await User.exists({ email: 'joe@example.com' }); // count(filter?) — returns number const total = await User.count(); const admins = await User.count({ role: 'admin' }); ``` ### Scalar aggregates ```typescript // All accept optional filter as second argument const total = await Order.sum('amount'); const avg = await Review.avg('rating', { productId: 7 }); const min = await Event.min('startDate'); const max = await Event.max('endDate'); ``` ### Validation ```typescript // validate(data, rules) — throws ValidationError on failure await User.validate(data, rules); // check(data, rules) — returns Record | null const errors = await User.check(data, rules); ``` ### Query builder entry points ```typescript model.query() // QueryBuilder — base builder model.advanced() // AdvancedQueryBuilder — aggregates, window functions, UNION model.softDelete() // SoftDeleteQueryBuilder — withTrashed, onlyTrashed model.extended() // ExtendedQueryBuilder — scope() ``` --- ## EntityWithUpdate Every row returned by `get()`, `insert()`, `findOrCreate()`, `getAll()`, and all query builder methods (`query().get()`, `query().first()`) is an `EntityWithUpdate`: ```typescript const user = await User.get({ id: 1 }); await user.update({ name: 'Bob' }); // update in DB, returns EntityWithUpdate await user.delete(); // delete from DB const fresh = await user.refresh(); // re-fetch from DB by primary key const plain = user.toJSON(); // plain object — no ORM methods ``` --- ## Query Builder All methods are chainable. No query fires until a terminal method is called. ### Terminal methods ```typescript // .get() — execute, returns EntityWithUpdate[] await User.query().where('active', '=', true).get(); // .first(condition?) — execute, returns EntityWithUpdate | null await User.query().orderBy('createdAt', 'DESC').first(); await User.query().first({ role: 'admin' }); // partial filter object await User.query().first('role = "admin"'); // raw SQL string // .getPaginated(page, perPage) — returns { data, total, page, lastPage } const result = await Post.query().getPaginated(1, 20); result.data // Post[] result.total // total matching rows result.page // current page result.lastPage // Math.ceil(total / perPage) // .delete() — bulk delete, returns number of rows deleted const deleted = await User.query() .where('status', '=', 'inactive') .delete(); // .update(data) — bulk update, returns number of rows updated const updated = await User.query() .where('role', '=', 'guest') .update({ role: 'user' }); ``` ### SELECT ```typescript .select('id', 'name', 'email') // pick columns .exclude('password') // strip column from result .exclude('user.password') // strip from preloaded relation ``` ### WHERE ```typescript // Operators: = | != | < | <= | > | >= | LIKE | ILIKE .where('role', '=', 'admin') .where('age', '>', 18) .where('name', 'LIKE', 'Jo%') .orWhere('role', '=', 'moderator') // OR connector .whereRaw('LOWER(email) = ?', ['joe@example.com']) // parameterized raw SQL .whereIn('role', ['admin', 'moderator']) .whereNotIn('status', ['banned', 'deleted']) .whereNull('deletedAt') .whereNotNull('verifiedAt') .whereBetween('age', 18, 65) .ILike('name', '%alice%') // case-insensitive LIKE // SQLite: LIKE | Postgres: ILIKE | MySQL: LIKE ``` ### ORDER, LIMIT, OFFSET ```typescript .orderBy('createdAt', 'DESC') .orderBy('name', 'ASC') // multiple orderBy — applied in order .limit(20) .offset(40) .paginate(3, 20) // page 3, 20 per page (shorthand) ``` ### JOINS ```typescript .join('users', 'posts.userId', '=', 'users.id') // INNER JOIN .leftJoin('users', 'posts.userId', '=', 'users.id') // LEFT JOIN // Via advanced(): .rightJoin('users', 'posts.userId', '=', 'users.id') // RIGHT JOIN .fullOuterJoin('users', 'posts.userId', '=', 'users.id') // FULL OUTER JOIN ``` When joins are active and no `.select()` is called, SlintORM auto-qualifies to `table.*` to avoid ambiguous column errors. ### PRELOADS (eager loading, no N+1) ```typescript .preload('user') // load manytoone relation .preload('posts') // load onetomany relation .preload('profile') // load onetoone relation .preload('teams') // load manytomany relation .preload('posts.comments') // nested preload .preload('posts.comments.user') // any depth .exclude('user.password') // strip field from preloaded relation ``` All preloads are batch-fetched with a single `WHERE id IN (...)` query — not per-row. Cycle detection is built in. ### AGGREGATES (via model.advanced()) ```typescript .countAggregate() // adds COUNT(*) AS count to SELECT .sum('amount') // adds SUM(amount) to SELECT .avg('rating') // adds AVG(rating) to SELECT .min('price') // adds MIN(price) to SELECT .max('price') // adds MAX(price) to SELECT .groupBy('userId', 'status') .having('COUNT(*) > ?', [5]) // filter on aggregated result .distinct('role') // SELECT DISTINCT .window('ROW_NUMBER()', 'PARTITION BY userId ORDER BY createdAt DESC') ``` ### SUBQUERIES (via model.advanced()) ```typescript .selectSubquery('SELECT COUNT(*) FROM comments WHERE postId = posts.id', 'commentCount') .exists('SELECT 1 FROM posts WHERE posts.userId = users.id') .notExists('SELECT 1 FROM posts WHERE posts.userId = users.id') // Static UNION AdvancedQueryBuilder.union([query1, query2]) // UNION AdvancedQueryBuilder.union([query1, query2], true) // UNION ALL ``` ### RELATION TRAVERSAL ```typescript // throughRelation — auto-generates JOINs from schema annotations .throughRelation('enrollment.student') // whereRelated — explicit path + IN subquery .whereRelated('enrollment.student', 'id', 42) // relatedTo — BFS finds the shortest path automatically .relatedTo('Student', 'id', 42) ``` ### SOFT DELETE (via model.softDelete()) ```typescript .withTrashed() // include soft-deleted rows (removes WHERE deletedAt IS NULL) .onlyTrashed() // only soft-deleted rows (WHERE deletedAt IS NOT NULL) ``` ### SCOPES (via model.extended()) ```typescript // ExtendedQueryBuilder inherits ALL base QueryBuilder methods plus scope() .scope(q => q.where('active', '=', true)) // lazy — runs before buildSql() // Named scope pattern const activeScope = (q: ExtendedQueryBuilder) => q.where('active', '=', true); User.extended().scope(activeScope).scope(adminScope).get(); ``` --- ## Dialect differences (auto-handled) | Feature | SQLite | Postgres | MySQL | MongoDB | |------------------|---------------------------|-----------------------|----------------------------|-----------------| | Placeholders | ? | $1, $2, $3 | ? | JSON protocol | | Identifiers | unquoted / "double" | "double-quoted" | \`backtick\` | field names | | Upsert | INSERT OR REPLACE | ON CONFLICT DO UPDATE | ON DUPLICATE KEY UPDATE | manual | | ILIKE | LIKE (case-insensitive) | native ILIKE | LIKE (collation-dependent) | regex | | RETURNING | yes | yes | no | no | | Transactions | yes | yes | yes | no | | FULL OUTER JOIN | no | yes | no | no | | Window functions | limited | full | full | no | --- ## Relations Declared in `// comment` annotations. Both `@relation` and `@relationship` are accepted. ### Annotation syntax ``` @relation :;foreignKey:[;relatedKey:][;through:][;onDelete:CASCADE|SET NULL] ``` ### One-to-Many ```typescript interface User { // @relation onetomany:Post;foreignKey:userId posts?: Post[]; } interface Post { userId: number; // @relation manytoone:User;foreignKey:userId user?: User; } ``` ### One-to-One ```typescript interface User { // @relationship onetoone:Profile;foreignKey:userId profile?: Profile; } interface Profile { // @unique userId: number; // @relationship onetoone:User;foreignKey:userId;onDelete:CASCADE user?: User; } ``` ### Many-to-Many ```typescript interface User { // @relation manytomany:Team;through:team_members;foreignKey:userId;relatedKey:teamId teams?: Team[]; } interface Team { // @relation manytomany:User;through:team_members;foreignKey:teamId;relatedKey:userId members?: User[]; } // Pivot table team_members(userId, teamId) auto-created by migrate() ``` --- ## Soft Delete ```typescript interface User { // @softDelete deletedAt: string; } // Standard queries auto-filter: WHERE deletedAt IS NULL await User.getAll(); // excludes deleted rows // Soft delete — sets deletedAt timestamp, does not remove row await User.delete({ id: 1 }); // Restore — sets deletedAt = NULL await User.restore({ id: 1 }); // Include deleted rows await User.softDelete().withTrashed().get(); // Only deleted rows await User.softDelete().onlyTrashed().get(); ``` --- ## Lifecycle Hooks All 6 hooks, all async-capable. onCreateBefore and onUpdateBefore mutate data by returning a new value. ```typescript const User = orm.defineModel('users', 'User', { // Runs before INSERT — return value replaces data async onCreateBefore(data: User): Promise { return { ...data, email: data.email.toLowerCase() }; }, // Runs after INSERT with the created row async onCreateAfter(item: User): Promise { await sendWelcomeEmail(item.email); }, // Runs before UPDATE — return value replaces newData async onUpdateBefore(oldData: User, newData: Partial): Promise | void> { if (newData.password) { return { ...newData, password: await bcrypt.hash(newData.password, 12) }; } }, // Runs after UPDATE async onUpdateAfter(oldData: User, newData: Partial): Promise { await auditLog(oldData, newData); }, // Runs before DELETE onDeleteBefore(deleted: User): void { console.log('Deleting:', deleted.id); }, // Runs after DELETE onDeleteAfter(deleted: User): void { clearCache(deleted.id); }, }); ``` --- ## Validation ```typescript // FieldRules — all supported rules const rules = { email: { required: true, email: true }, name: { required: true, minLength: 2, maxLength: 100 }, age: { min: 0, max: 120 }, username: { match: /^[a-z0-9_]+$/ }, code: { custom: async (value, row) => { const valid = await Promo.exists({ code: value }); return valid ? null : 'Invalid promo code'; } }, }; // validate() — throws ValidationError try { await User.validate(data, rules); } catch (err) { if (err instanceof ValidationError) { err.message // 'Validation failed' err.errors // { email: 'Invalid email', name: 'Min length is 2' } } } // check() — returns error map or null const errors = await User.check(data, rules); if (errors) return res.status(422).json({ errors }); ``` ### Rule reference | Rule | Type | Description | |-----------|-----------------------------------------|-------------| | required | boolean | Must be present and non-empty | | email | boolean | Must match email pattern | | minLength | number | String min length | | maxLength | number | String max length | | min | number | Numeric minimum | | max | number | Numeric maximum | | match | RegExp | Must match regex | | custom | (value, row) => string \| null | Custom validator, async supported | --- ## Error Handling All constraint violations normalized into ORMError with typed codes. ```typescript import { ORMError, ORMErrorCode } from 'slintorm'; try { await User.insert({ email: 'duplicate@example.com' }); } catch (err) { if (err instanceof ORMError) { err.ormCode // ORMErrorCode value err.table // affected table err.column // affected column err.value // value that caused violation err.sql // SQL that was executed err.params // bound parameters err.original // original driver error } } ``` ### ORMErrorCode values | Code | SQLite | Postgres | MySQL | |-----------------------|-------------------------------|----------|-------------------------| | UNIQUE_VIOLATION | UNIQUE constraint failed | 23505 | ER_DUP_ENTRY | | NOT_NULL_VIOLATION | NOT NULL constraint failed | 23502 | ER_BAD_NULL_ERROR | | FOREIGN_KEY_VIOLATION | FOREIGN KEY constraint failed | 23503 | ER_NO_REFERENCED_ROW_2 | | CHECK_VIOLATION | CHECK constraint failed | 23514 | ER_CHECK_CONSTRAINT_VIOLATED | | UNKNOWN_CONSTRAINT | other constraint errors | other | other | --- ## Transactions ```typescript // orm.transaction() — BEGIN / COMMIT / ROLLBACK auto-managed await orm.transaction(async (trx) => { await trx.User.update({ id: fromId }, { balance: fromBal - amount }); await trx.User.update({ id: toId }, { balance: toBal + amount }); // Any throw -> automatic ROLLBACK }); // orm.batch() — flat statement list, single transaction await orm.batch([ { sql: 'INSERT INTO tags (name) VALUES (?)', params: ['typescript'] }, { sql: 'UPDATE posts SET tagCount = tagCount + 1 WHERE id = ?', params: [postId] }, ]); ``` MongoDB note: `orm.transaction()` is a no-op on MongoDB. Use the MongoDB driver session API for atomic multi-document operations. --- ## Migrations Node.js only — never run in edge runtimes. ```typescript import ORMManager from 'slintorm'; const orm = new ORMManager({ driver: 'postgres', databaseUrl: process.env.DATABASE_URL!, dir: './src', }); await orm.migrate(); // Scans ./src for TypeScript interface definitions // Generates src/schema/generated.ts // Creates/alters tables non-destructively // Tracks changes in _slint_migrations table ``` ### Migration hooks ```typescript await orm.migrate({ onPending(units) { console.log(`${units.length} pending`) }, onProgress(unit) { console.log(`Running: ${unit.name}`) }, onDone(result) { console.log(`Done. ${result.ran} applied.`) }, onError(unit, err) { console.error(`Failed: ${unit.name}`, err) }, }); ``` ### _slint_migrations tracking table schema ```sql -- SQLite / MySQL CREATE TABLE _slint_migrations ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, batch INTEGER NOT NULL, hash TEXT NOT NULL, snapshot TEXT, migratedAt TEXT NOT NULL ); -- Postgres CREATE TABLE _slint_migrations ( id SERIAL PRIMARY KEY, name TEXT NOT NULL, batch INTEGER NOT NULL, hash TEXT NOT NULL, snapshot TEXT, migratedAt TEXT NOT NULL ); ``` --- ## CLI ```bash npx slintorm generate # scan TypeScript files, write schema/generated.ts npx slintorm migrate # apply pending migrations npx slintorm rollback # roll back most recent batch npx slintorm rollback # roll back specific migration by name npx slintorm rollback --to # roll back to batch number N npx slintorm status # show pending / applied migrations npx slintorm fresh # drop all tables and re-run all migrations (dev only) npx slintorm drop-tracking # remove _slint_migrations table only npx slintorm --help # print all commands ``` Config resolution order (highest to lowest): CLI flags > slintorm.config.js > package.json#slintorm --- ## Schema Generation ```bash npx slintorm generate # Output: src/schema/generated.ts ``` `generated.ts` exports: - `ModelMap` — typed model map for use with `new ORMManager()` - `schema` — pre-built schema object to pass as the `schema` option ```typescript // Import both from the same file import { schema, type ModelMap } from './schema/generated'; const orm = new ORMManager({ driver: 'sqlite', databaseUrl: './dev.db', modelMap: {} as ModelMap, schema, // skips filesystem scan; required in edge runtimes }); ``` Generator works by tokenizing TypeScript source files with a lightweight regex-based scanner — no ts-morph, no TypeScript compiler API at runtime. --- ## Edge / Serverless Pattern ```typescript // 1. Generate at build time (CI/CD pipeline): // npx slintorm generate // 2. Commit src/schema/generated.ts // 3. Import schema and use in edge function: import ORMManager from 'slintorm/browser'; // edge-safe entrypoint import { schema } from './schema/generated'; const orm = new ORMManager({ driver: 'postgres', databaseUrl: process.env.DATABASE_URL!, schema, // no filesystem reads // No dir, no migrate() }); ``` ### What is NOT available in edge runtimes - `orm.migrate()` — requires filesystem - SQLite driver — requires native bindings - Schema auto-generation — requires scanning .ts files - `require()` / `fs` — not available in V8 isolates ### Edge runtime support matrix | Runtime | SQLite | Postgres | MySQL | MongoDB | |----------------------|--------|--------------------|--------------------|--------------| | Node.js | yes | yes | yes | yes | | Next.js (Node) | yes* | yes | yes | yes | | Next.js Edge | no | yes (Neon) | yes | HTTP API | | Cloudflare Workers | no | yes (Hyperdrive) | yes (Hyperdrive) | HTTP API | | Deno Deploy | no | yes | no | HTTP API | *requires serverExternalPackages config --- ## TypeScript Types — Full Reference ```typescript import ORMManager, { type ORMManagerConfig, type ModelHooks, type DBStore, type ReadonlyDBStore, type ModelAPI, type EntityWithUpdate, type FieldRules, type SQLExecResult, type ExecFn, type OpComparison, type RelationKind, type RelationDef, type DBDriver, ORMError, ORMErrorCode, ValidationError, Validator, } from 'slintorm'; // DBDriver type DBDriver = 'sqlite' | 'postgres' | 'mysql' | 'mongodb'; // OpComparison — valid WHERE operators type OpComparison = '=' | '!=' | '<' | '<=' | '>' | '>=' | 'LIKE' | 'ILIKE'; // RelationKind type RelationKind = 'onetomany' | 'manytoone' | 'onetoone' | 'manytomany'; // SQLExecResult interface SQLExecResult { rows: unknown[]; rowsAffected: number; lastInsertId: number | null; } // EntityWithUpdate type EntityWithUpdate = T & { update(data: Partial): Promise>; delete(): Promise; refresh(): Promise>; toJSON(): T; }; // FieldRules type FieldRules = { [K in keyof T]?: { required?: boolean; email?: boolean; minLength?: number; maxLength?: number; min?: number; max?: number; match?: RegExp; custom?: (value: unknown, row: Partial) => string | null | Promise; }; }; // ModelHooks interface ModelHooks { onCreateBefore?: (data: T) => T | void | Promise; onCreateAfter?: (item: T) => void | Promise; onUpdateBefore?: (old: T, newData: Partial) => Partial | void | Promise | void>; onUpdateAfter?: (old: T, newData: Partial) => void | Promise; onDeleteBefore?: (deleted: T) => void; onDeleteAfter?: (deleted: T) => void; } // DBStore — typed db object // orm.db is DBStore // Each key is a ModelAPI for the corresponding model // ReadonlyDBStore // Same as DBStore but insert/update/delete are not available ``` --- ## Common Runtime Errors | Error | Cause | Fix | |-------|-------|-----| | ORMError: UNIQUE_VIOLATION | Duplicate in unique column | Check before insert or use upsert() | | ORMError: NOT_NULL_VIOLATION | NULL in NOT NULL column | Provide required field | | ORMError: FOREIGN_KEY_VIOLATION | Reference to missing row | Insert referenced record first | | ORMError: CHECK_VIOLATION | Failed CHECK/ENUM constraint | Use valid enum value | | ValidationError | validate() rule failed | Check err.errors for field messages | | Filter must contain at least one field | get({}) or delete({}) | Always include at least one filter key | | No SQLite driver found | Driver not installed | npm install better-sqlite3 | | Cannot load schema from disk | Edge runtime, no schema passed | Pass schema from generated.ts | --- ## package.json exports map (v1.5.1) ``` slintorm -> dist/src/index.js (Node.js — full API) slintorm (browser condition) -> dist/src/browser.js (edge-safe subset) slintorm (CJS) -> dist/src/index.cjs (CommonJS) ``` The browser export is automatically selected by edge bundlers (Cloudflare, Vercel Edge). Explicitly import `slintorm/browser` if needed. --- ## Recommended db.ts pattern ```typescript import ORMManager from 'slintorm'; import { schema, type ModelMap } from './schema/generated'; const orm = new ORMManager({ driver: process.env.DB_DRIVER as 'sqlite' | 'postgres' | 'mysql' | 'mongodb', databaseUrl: process.env.DATABASE_URL!, dir: './src', logs: process.env.NODE_ENV === 'development', modelMap: {} as ModelMap, schema, }); if (process.env.NODE_ENV !== 'edge') { await orm.migrate(); } export const db = orm.db; export default orm; ``` --- --- ## Upcoming Annotations (in development) These annotations are being implemented in batches. All use zero external dependencies. ### Validation (zero-dep regex-based) - `@email` — Validate email format - `@url` — Validate URL format - `@uuid` — Validate UUID v4 - `@phone` — Phone number format validation - `@min:N` / `@max:N` — Numeric range validation - `@minLength:N` / `@maxLength:N` — String length validation - `@pattern:regex` — Custom regex validation ### Security (Node.js crypto, zero deps) - `@hash` — One-way hashing via pbkdf2/scrypt. `.verify()` for constant-time comparison - `@encrypt` — AES-256-GCM transparent encryption on write, decryption on read - `@token` — crypto.randomBytes token generation with prefix/encoding options - `@secret` — Composite: @hash + @omitjson + log-masking (for API keys, client secrets) ### Sanitization - `@sanitize` — Input sanitization (trim, lower, upper, stripTags, escape) ### Expiry & Audit - `@expires` — Auto-expire values (Nd, Nh, Nm). Expired rows return null / auto-filtered - `@cuid` — Create/Update/Delete user audit from context - `@tenant` / `@owner` — Multi-tenant filter + row ownership from context ### Relationship Shortcuts - `@belongsTo:Model` — Shortcut for manytoone - `@hasMany:Model` — Shortcut for onetomany - `@hasOne:Model` — Shortcut for onetoone - `@belongsToMany:Model` — Shortcut for manytomany - Cross-model relation validation — errors if inverse relation is missing ### Data Lifecycle & DDL - `@slug:sourceField` — Auto-generate URL slug on insert - `@counterCache:relation.field` — Auto-managed counter cache - `@fulltext` / `@spatial` / `@partialIndex` — Advanced index types --- *SlintORM v1.5.1 — https://github.com/emeraldlinks/slintorm*