nanodb-orm 0.0.3 → 0.0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (87) hide show
  1. package/README.md +506 -333
  2. package/dist/cli.d.ts +96 -0
  3. package/dist/cli.d.ts.map +1 -0
  4. package/dist/cli.js +348 -0
  5. package/dist/cli.js.map +1 -0
  6. package/dist/constants/index.d.ts +7 -54
  7. package/dist/constants/index.d.ts.map +1 -1
  8. package/dist/constants/index.js +9 -61
  9. package/dist/constants/index.js.map +1 -1
  10. package/dist/core/config.d.ts +3 -13
  11. package/dist/core/config.d.ts.map +1 -1
  12. package/dist/core/config.js +5 -27
  13. package/dist/core/config.js.map +1 -1
  14. package/dist/core/connection.d.ts +16 -31
  15. package/dist/core/connection.d.ts.map +1 -1
  16. package/dist/core/connection.js +42 -78
  17. package/dist/core/connection.js.map +1 -1
  18. package/dist/core/index.d.ts +2 -3
  19. package/dist/core/index.d.ts.map +1 -1
  20. package/dist/core/index.js +3 -18
  21. package/dist/core/index.js.map +1 -1
  22. package/dist/index.d.ts +235 -12
  23. package/dist/index.d.ts.map +1 -1
  24. package/dist/index.js +252 -35
  25. package/dist/index.js.map +1 -1
  26. package/dist/init.d.ts +127 -18
  27. package/dist/init.d.ts.map +1 -1
  28. package/dist/init.js +280 -47
  29. package/dist/init.js.map +1 -1
  30. package/dist/jest.setup.d.ts +4 -0
  31. package/dist/jest.setup.d.ts.map +1 -0
  32. package/dist/jest.setup.js +40 -0
  33. package/dist/jest.setup.js.map +1 -0
  34. package/dist/types/errors.d.ts +30 -12
  35. package/dist/types/errors.d.ts.map +1 -1
  36. package/dist/types/errors.js +98 -23
  37. package/dist/types/errors.js.map +1 -1
  38. package/dist/types/index.d.ts +268 -4
  39. package/dist/types/index.d.ts.map +1 -1
  40. package/dist/types/index.js +5 -4
  41. package/dist/types/index.js.map +1 -1
  42. package/dist/utils/error-handler.d.ts +6 -31
  43. package/dist/utils/error-handler.d.ts.map +1 -1
  44. package/dist/utils/error-handler.js +24 -81
  45. package/dist/utils/error-handler.js.map +1 -1
  46. package/dist/utils/index.d.ts +1 -3
  47. package/dist/utils/index.d.ts.map +1 -1
  48. package/dist/utils/index.js +4 -6
  49. package/dist/utils/index.js.map +1 -1
  50. package/dist/utils/logger.d.ts +6 -25
  51. package/dist/utils/logger.d.ts.map +1 -1
  52. package/dist/utils/logger.js +20 -38
  53. package/dist/utils/logger.js.map +1 -1
  54. package/dist/utils/migrations.d.ts +16 -90
  55. package/dist/utils/migrations.d.ts.map +1 -1
  56. package/dist/utils/migrations.js +220 -422
  57. package/dist/utils/migrations.js.map +1 -1
  58. package/dist/utils/schema-introspection.d.ts +30 -169
  59. package/dist/utils/schema-introspection.d.ts.map +1 -1
  60. package/dist/utils/schema-introspection.js +125 -462
  61. package/dist/utils/schema-introspection.js.map +1 -1
  62. package/dist/utils/seeds.d.ts +15 -48
  63. package/dist/utils/seeds.d.ts.map +1 -1
  64. package/dist/utils/seeds.js +108 -186
  65. package/dist/utils/seeds.js.map +1 -1
  66. package/dist/utils/sync.d.ts +16 -41
  67. package/dist/utils/sync.d.ts.map +1 -1
  68. package/dist/utils/sync.js +78 -172
  69. package/dist/utils/sync.js.map +1 -1
  70. package/dist/utils/transactions.d.ts +61 -44
  71. package/dist/utils/transactions.d.ts.map +1 -1
  72. package/dist/utils/transactions.js +103 -137
  73. package/dist/utils/transactions.js.map +1 -1
  74. package/package.json +29 -10
  75. package/dist/example.d.ts +0 -67
  76. package/dist/example.d.ts.map +0 -1
  77. package/dist/example.js +0 -86
  78. package/dist/example.js.map +0 -1
  79. package/dist/types/database.d.ts +0 -74
  80. package/dist/types/database.d.ts.map +0 -1
  81. package/dist/types/database.js +0 -6
  82. package/dist/types/database.js.map +0 -1
  83. package/dist/types/types.d.ts +0 -30
  84. package/dist/types/types.d.ts.map +0 -1
  85. package/dist/types/types.js +0 -6
  86. package/dist/types/types.js.map +0 -1
  87. package/llm.txt +0 -336
package/README.md CHANGED
@@ -1,503 +1,676 @@
1
1
  # nanodb-orm
2
2
 
3
- A production-ready, generic database package built on top of Drizzle ORM with automatic migrations, schema introspection, atomic transactions, and support for both local SQLite and remote Turso databases.
3
+ A lightweight ORM wrapper for Drizzle ORM with automatic migrations, schema introspection, CLI tools, and support for SQLite/Turso databases.
4
4
 
5
5
  ## Features
6
6
 
7
- - 🚀 **Auto-Migrations**: Automatically creates and updates database schemas from Drizzle table definitions
8
- - 🔍 **Schema Introspection**: Comprehensive schema analysis and validation
9
- - 🌐 **Multi-Database Support**: Works with local SQLite and remote Turso databases
10
- - **Atomic Transactions**: Full transaction support with rollback protection
11
- - 🛡️ **Type Safe**: Full TypeScript support with proper type inference
12
- - 🔒 **Security**: SQL injection protection and input validation
13
- - 🧵 **Thread Safe**: Race condition fixes and concurrent access protection
14
- - 📦 **NPM Package Ready**: Designed to be used as a standalone npm package
15
- - ⚙️ **Configurable**: Flexible migration and seeding options
16
- - 🧪 **Test Ready**: Built-in testing utilities and isolation
17
- - 🔧 **Production Ready**: Enhanced error handling and reliability features
7
+ - **TypeScript First** Full type inference from schema to queries
8
+ - **Auto-Migrations** Automatically creates and updates database schemas from Drizzle tables
9
+ - **Schema Introspection** Comprehensive schema analysis and validation
10
+ - **Multi-Database** Works with local SQLite and remote Turso databases
11
+ - **Transactions** Full transaction support with automatic rollback
12
+ - **CLI Tools** Built-in commands including Drizzle Studio integration
13
+ - **Plugin System** Extensible with hooks for audit, validation, transformations
14
+ - **Minimal** ~1K lines of code, zero bloat
18
15
 
19
16
  ## Installation
20
17
 
21
18
  ```bash
22
19
  npm install nanodb-orm
20
+
21
+ # For Drizzle Studio support (optional)
22
+ npm install drizzle-kit --save-dev
23
23
  ```
24
24
 
25
- ## Quick Start
25
+ ## CLI
26
26
 
27
- ### 1. Define Your Models with Drizzle
27
+ nanodb-orm includes a CLI for common database operations:
28
28
 
29
- Create your Drizzle table definitions (e.g., `models/users.ts`):
29
+ ```bash
30
+ # Launch Drizzle Studio (visual database browser)
31
+ npx nanodb studio
30
32
 
31
- ```typescript
32
- // models/users.ts
33
- import { integer, sqliteTable, text } from 'drizzle-orm/sqlite-core';
33
+ # With custom port
34
+ npx nanodb studio --port 3000
34
35
 
35
- export const usersTable = sqliteTable('users', {
36
- id: integer('id').primaryKey({ autoIncrement: true }),
37
- name: text('name').notNull(),
38
- age: integer('age').notNull(),
39
- email: text('email').unique().notNull(),
40
- });
36
+ # With specific database file
37
+ npx nanodb studio --db ./data/myapp.db
41
38
 
42
- export type InsertUser = typeof usersTable.$inferInsert;
43
- export type SelectUser = typeof usersTable.$inferSelect;
39
+ # Other commands
40
+ npx nanodb setup # Initialize schema and seed data
41
+ npx nanodb reset # Drop all tables and recreate
42
+ npx nanodb status # Show database health and stats
43
+ npx nanodb validate # Validate schema against database
44
+ npx nanodb help # Show all commands
44
45
  ```
45
46
 
46
- ```typescript
47
- // models/posts.ts
48
- import { sql } from 'drizzle-orm';
49
- import { integer, sqliteTable, text } from 'drizzle-orm/sqlite-core';
50
- import { usersTable } from './users';
47
+ ### Drizzle Studio
51
48
 
52
- export const postsTable = sqliteTable('posts', {
53
- id: integer('id').primaryKey({ autoIncrement: true }),
54
- title: text('title').notNull(),
55
- content: text('content').notNull(),
56
- userId: integer('user_id')
57
- .notNull()
58
- .references(() => usersTable.id, { onDelete: 'cascade' }),
59
- createdAt: text('created_at')
60
- .default(sql`(datetime('now'))`)
61
- .notNull(),
62
- updatedAt: text('updated_at').$onUpdate(() => new Date().toISOString()),
63
- });
49
+ Launch a visual database browser at `https://local.drizzle.studio`:
64
50
 
65
- export type InsertPost = typeof postsTable.$inferInsert;
66
- export type SelectPost = typeof postsTable.$inferSelect;
51
+ ```bash
52
+ npx nanodb studio
67
53
  ```
68
54
 
69
- ```typescript
70
- // models/categories.ts
71
- import { sql } from 'drizzle-orm';
72
- import { integer, sqliteTable, text } from 'drizzle-orm/sqlite-core';
55
+ ![Drizzle Studio](https://orm.drizzle.team/images/drizzle-studio.png)
73
56
 
74
- export const categoriesTable = sqliteTable('categories', {
75
- id: integer('id').primaryKey({ autoIncrement: true }),
76
- name: text('name').notNull(),
77
- description: text('description'),
78
- color: text('color').notNull().default('#000000'),
79
- isActive: integer('is_active').notNull().default(1),
80
- createdAt: text('created_at')
81
- .default(sql`(datetime('now'))`)
82
- .notNull(),
83
- });
57
+ ## Import Styles
58
+
59
+ ```typescript
60
+ // Default import (recommended)
61
+ import nanodb from 'nanodb-orm';
84
62
 
85
- export type InsertCategory = typeof categoriesTable.$inferInsert;
86
- export type SelectCategory = typeof categoriesTable.$inferSelect;
63
+ const users = nanodb.schema.table('users', { ... });
64
+ const db = await nanodb.createDatabase({ tables: { users } });
65
+ await db.select().from(users).where(nanodb.query.eq(users.id, 1));
87
66
  ```
88
67
 
89
68
  ```typescript
90
- // models/index.ts
91
- export * from './users';
92
- export * from './posts';
93
- export * from './categories';
94
-
95
- // Import tables for aggregation
96
- import { usersTable } from './users';
97
- import { postsTable } from './posts';
98
- import { categoriesTable } from './categories';
69
+ // Named imports
70
+ import { createDatabase, schema, query } from 'nanodb-orm';
71
+ ```
99
72
 
100
- // Export aggregated tables for nanodb-orm
101
- export const tables = {
102
- users: usersTable,
103
- posts: postsTable,
104
- categories: categoriesTable,
105
- } as const;
73
+ ```typescript
74
+ // Individual imports (tree-shakeable)
75
+ import { createDatabase, table, integer, text, eq } from 'nanodb-orm';
106
76
  ```
107
77
 
108
- ### 2. Initialize and Setup Database
78
+ ## Quick Start
79
+
80
+ ### 1. Define Your Schema
109
81
 
110
82
  ```typescript
111
- import { initializeDatabase, DatabaseSync } from 'nanodb-orm';
112
- import { tables } from './models';
83
+ import nanodb from 'nanodb-orm';
113
84
 
114
- // Initialize the database package with your Drizzle tables
115
- initializeDatabase({
116
- tables,
117
- seedData: {
118
- users: [
119
- { name: 'John Doe', age: 30, email: 'john@example.com' },
120
- { name: 'Jane Smith', age: 25, email: 'jane@example.com' }
121
- ],
122
- posts: [
123
- { title: 'Welcome Post', content: 'This is my first post!', userId: 1 },
124
- { title: 'Getting Started', content: 'Here are some tips...', userId: 2 }
125
- ],
126
- categories: [
127
- { name: 'Technology', description: 'Tech-related posts', color: '#3B82F6' },
128
- { name: 'Lifestyle', description: 'Life and personal posts', color: '#10B981' }
129
- ]
130
- }
85
+ const users = nanodb.schema.table('users', {
86
+ id: nanodb.schema.integer('id').primaryKey({ autoIncrement: true }),
87
+ name: nanodb.schema.text('name').notNull(),
88
+ email: nanodb.schema.text('email').unique().notNull(),
89
+ age: nanodb.schema.integer('age'),
131
90
  });
132
91
 
133
- // Setup database (creates tables, runs migrations, seeds data)
134
- await DatabaseSync.setup();
92
+ const posts = nanodb.schema.table('posts', {
93
+ id: nanodb.schema.integer('id').primaryKey({ autoIncrement: true }),
94
+ title: nanodb.schema.text('title').notNull(),
95
+ userId: nanodb.schema.integer('userId').notNull(),
96
+ });
135
97
  ```
136
98
 
137
- ### 3. Working with Drizzle Tables
99
+ ### 2. Create Database
138
100
 
139
- nanodb-orm works seamlessly with Drizzle ORM table definitions. Here are the key Drizzle column types and methods:
101
+ ```typescript
102
+ const db = await nanodb.createDatabase({
103
+ tables: { users, posts },
104
+ seedData: {
105
+ users: [{ name: 'Alice', email: 'alice@example.com', age: 28 }],
106
+ },
107
+ });
140
108
 
141
- #### Drizzle Column Types
142
- - `integer()` - Integer numbers
143
- - `text()` - Text strings
144
- - `real()` - Floating point numbers
145
- - `blob()` - Binary data
109
+ export { db };
110
+ ```
146
111
 
147
- #### Drizzle Column Methods
148
- - `.primaryKey({ autoIncrement: true })` - Primary key with auto-increment
149
- - `.notNull()` - NOT NULL constraint
150
- - `.unique()` - UNIQUE constraint
151
- - `.default(value)` - Default value
152
- - `.references(table.column)` - Foreign key reference
112
+ ### 3. Query Your Data
153
113
 
154
- #### Example Drizzle Column Definitions
155
114
  ```typescript
156
- // Primary key with auto-increment
157
- id: integer('id').primaryKey({ autoIncrement: true })
115
+ // SELECT
116
+ const allUsers = await db.select().from(users);
117
+ const adults = await db.select().from(users).where(nanodb.query.gte(users.age, 18));
158
118
 
159
- // Required text field
160
- name: text('name').notNull()
119
+ // INSERT
120
+ await db.insert(users).values({ name: 'Bob', email: 'bob@example.com' });
161
121
 
162
- // Optional text field with default
163
- color: text('color').notNull().default('#000000')
122
+ // UPDATE
123
+ await db.update(users).set({ name: 'Robert' }).where(nanodb.query.eq(users.email, 'bob@example.com'));
164
124
 
165
- // Unique email field
166
- email: text('email').unique().notNull()
125
+ // DELETE
126
+ await db.delete(users).where(nanodb.query.eq(users.email, 'bob@example.com'));
127
+ ```
167
128
 
168
- // Boolean field (stored as integer)
169
- isActive: integer('is_active').notNull().default(1)
129
+ ## Type Inference
170
130
 
171
- // Timestamp field with SQL function
172
- createdAt: text('created_at')
173
- .default(sql`(datetime('now'))`)
174
- .notNull()
131
+ nanodb-orm provides full type inference from your schema:
175
132
 
176
- // Foreign key reference
177
- userId: integer('user_id')
178
- .notNull()
179
- .references(() => usersTable.id, { onDelete: 'cascade' })
180
- ```
133
+ ```typescript
134
+ import {
135
+ createDatabase,
136
+ table,
137
+ integer,
138
+ text,
139
+ type SelectModel,
140
+ type InsertModel,
141
+ } from 'nanodb-orm';
142
+
143
+ const users = table('users', {
144
+ id: integer('id').primaryKey({ autoIncrement: true }),
145
+ name: text('name').notNull(),
146
+ email: text('email').notNull(),
147
+ age: integer('age'),
148
+ });
181
149
 
182
- ## API Reference
150
+ // Infer types directly from your table definitions
151
+ type User = SelectModel<typeof users>;
152
+ // { id: number; name: string; email: string; age: number | null }
183
153
 
184
- ### `initializeDatabase(schemaData: SchemaData)`
154
+ type NewUser = InsertModel<typeof users>;
155
+ // { id?: number; name: string; email: string; age?: number | null }
185
156
 
186
- Initializes the database package with your schema data.
157
+ // The database is fully typed
158
+ const db = await createDatabase({ tables: { users } });
187
159
 
188
- ```typescript
189
- interface SchemaData {
190
- tables: Record<string, any>; // Your Drizzle table definitions
191
- seedData?: Record<string, any[]>; // Optional seed data
192
- migrationConfig?: MigrationConfig; // Optional migration configuration
193
- }
160
+ // All operations are type-safe
161
+ const allUsers: User[] = await db.select().from(users);
194
162
 
195
- interface MigrationConfig {
196
- preserveData?: boolean; // Preserve existing data (default: true)
197
- autoMigrate?: boolean; // Enable auto-migrations (default: true)
198
- dropTables?: boolean; // Allow dropping tables (default: false)
199
- }
163
+ // Seed data is type-checked at compile time
164
+ const db2 = await createDatabase({
165
+ tables: { users },
166
+ seedData: {
167
+ users: [
168
+ { name: 'Alice', email: 'alice@example.com' }, // ✓ Valid
169
+ // { name: 123 }, // ✗ TypeScript error!
170
+ ],
171
+ },
172
+ });
200
173
  ```
201
174
 
202
- ### `DatabaseSync`
175
+ ### Available Type Utilities
203
176
 
204
- Main database synchronization class.
177
+ | Type | Description |
178
+ |------|-------------|
179
+ | `SelectModel<T>` | Infer the row type (SELECT result) from a table |
180
+ | `InsertModel<T>` | Infer the insert type from a table (optional auto-generated columns) |
181
+ | `SchemaModels<S>` | Extract all row types from a schema object |
182
+ | `SchemaInsertModels<S>` | Extract all insert types from a schema |
183
+ | `NanoDatabase<S>` | The typed database instance |
184
+ | `Schema` | Type for schema objects |
185
+ | `AnyTable` | Type constraint for Drizzle tables |
205
186
 
206
- ```typescript
207
- // Setup database (create tables, migrate, seed)
208
- await DatabaseSync.setup();
187
+ ## API Reference
209
188
 
210
- // Reset database (drop all tables and recreate)
211
- await DatabaseSync.reset();
189
+ ### `createDatabase(config)`
212
190
 
213
- // Check if database is ready
214
- const isReady = await DatabaseSync.isReady();
215
- ```
191
+ Create and initialize database. Returns `db` with all utilities attached.
216
192
 
217
- ### `SchemaIntrospection`
193
+ ```typescript
194
+ const db = await createDatabase({
195
+ tables: { users, posts },
196
+ seedData: { users: [...] }, // Type-checked against schema
197
+ migrationConfig: {
198
+ preserveData: true, // default: true
199
+ autoMigrate: true, // default: true
200
+ dropTables: false, // default: false
201
+ },
202
+ plugins: [auditPlugin, validationPlugin], // optional
203
+ });
204
+ ```
218
205
 
219
- Comprehensive schema analysis utilities.
206
+ ### Database Operations (from `db`)
220
207
 
221
208
  ```typescript
222
- // Get all table names
223
- const tableNames = SchemaIntrospection.getAllTableNames();
209
+ // Health & Status
210
+ await db.healthCheck(); // { healthy, tables, totalRecords, ... }
211
+ await db.isReady(); // true/false
212
+ await db.sync(); // Sync with Turso (if remote)
213
+
214
+ // Reset & Seed
215
+ await db.reset(); // Drop all, recreate, reseed
216
+ await db.seed(); // Re-seed data
217
+ await db.clearData(); // Delete all data (keep tables)
218
+ ```
224
219
 
225
- // Get table information
226
- const tableInfo = SchemaIntrospection.getTableInfo('users');
220
+ ### Schema Introspection (from `db.schema`)
227
221
 
228
- // Get schema statistics
229
- const stats = SchemaIntrospection.getSchemaStats();
222
+ ```typescript
223
+ db.schema.tables(); // ['users', 'posts'] - typed as (keyof Schema)[]
224
+ db.schema.getTable('users'); // { columns, primaryKey, indexes }
225
+ db.schema.getColumns('users'); // ['id', 'name', 'email']
226
+ await db.schema.validate(); // { isValid, missingTables, ... }
227
+ db.schema.stats(); // Full schema statistics
228
+ db.schema.relationships(); // Foreign key relationships
229
+ ```
230
230
 
231
- // Validate schema
232
- const validation = await SchemaIntrospection.validateSchema();
231
+ ### Migrations (from `db.migrations`)
232
+
233
+ ```typescript
234
+ await db.migrations.run(); // Run pending migrations
235
+ await db.migrations.validate(); // Validate schema vs DB
236
+ await db.migrations.checkTables(); // { users: true, posts: true }
233
237
  ```
234
238
 
235
- ### `DatabaseMigrations`
239
+ ### `transaction(fn)` / `batch(statements)`
236
240
 
237
- Migration management utilities.
241
+ Execute operations atomically. Uses Drizzle's native transaction when available (better for Turso).
238
242
 
239
243
  ```typescript
240
- // Initialize schema
241
- await DatabaseMigrations.initializeSchema();
244
+ import nanodb from 'nanodb-orm';
242
245
 
243
- // Check table existence
244
- const existence = await DatabaseMigrations.checkTableExistence();
246
+ // Transaction with custom logic
247
+ const result = await nanodb.transaction(async (tx) => {
248
+ await tx.run(nanodb.query.sql`INSERT INTO users (name) VALUES ('Alice')`);
249
+ await tx.run(nanodb.query.sql`INSERT INTO posts (title, userId) VALUES ('Hello', 1)`);
250
+ return { created: true };
251
+ });
252
+
253
+ if (result.success) {
254
+ console.log(result.result); // { created: true }
255
+ } else {
256
+ console.log('Rolled back:', result.error?.message);
257
+ }
245
258
 
246
- // Validate schema
247
- const validation = await DatabaseMigrations.validateSchema();
259
+ // Batch multiple statements (simpler for bulk operations)
260
+ const batchResult = await nanodb.batch([
261
+ nanodb.query.sql`INSERT INTO users (name) VALUES ('Bob')`,
262
+ nanodb.query.sql`INSERT INTO users (name) VALUES ('Carol')`,
263
+ ]);
248
264
  ```
249
265
 
250
- ### `DatabaseSeeds`
266
+ ### `parseDbError(error, context)`
251
267
 
252
- Database seeding utilities.
268
+ Parse SQLite errors into user-friendly messages.
253
269
 
254
270
  ```typescript
255
- // Seed database
256
- await DatabaseSeeds.seedDatabase();
271
+ import { parseDbError } from 'nanodb-orm';
257
272
 
258
- // Check if database has data
259
- const hasData = await DatabaseSeeds.hasData();
260
-
261
- // Clear all data
262
- await DatabaseSeeds.clearAllData();
273
+ try {
274
+ await db.insert(users).values({ email: 'duplicate@example.com' });
275
+ } catch (error) {
276
+ const parsed = parseDbError(error, { table: 'users', operation: 'insert' });
277
+ console.log(parsed.message); // "Duplicate value for unique column 'email'"
278
+ }
263
279
  ```
264
280
 
265
- ## Configuration
281
+ ## Plugins
266
282
 
267
- ### Environment Variables
283
+ Extend nanodb-orm with custom hooks that run automatically on database operations.
268
284
 
269
- ```bash
270
- # Turso Database (optional)
271
- TURSO_CONNECTION_URL=libsql://your-database.turso.io
272
- TURSO_AUTH_TOKEN=your-auth-token
285
+ ### Plugin Interface
273
286
 
274
- # Force local database (for testing)
275
- FORCE_LOCAL_DB=true
276
- NODE_ENV=test
287
+ ```typescript
288
+ import { NanoPlugin } from 'nanodb-orm';
289
+
290
+ const myPlugin: NanoPlugin = {
291
+ name: 'my-plugin',
292
+
293
+ // Lifecycle
294
+ install: (db) => db, // Modify db instance
295
+ onReady: (db) => {}, // Called after createDatabase
296
+ onError: (err, op, table) => {}, // Called on hook errors
297
+
298
+ // Auto hooks (run automatically)
299
+ beforeInsert: (table, data) => data, // Transform data before insert
300
+ afterInsert: (table, data, result) => {},
301
+ beforeUpdate: (table, data) => data, // Transform data before update
302
+ afterUpdate: (table, data, result) => {},
303
+ beforeDelete: (table, condition) => condition,
304
+ afterDelete: (table, condition, result) => {},
305
+
306
+ // Query hooks (also auto-triggered)
307
+ beforeQuery: (table, fields) => fields,
308
+ afterQuery: (table, fields, result) => {},
309
+ };
277
310
  ```
278
311
 
279
- ### Migration Configuration
312
+ ### Example Plugins
313
+
314
+ These are **example plugins you can create** - nanodb-orm provides the plugin system, you build the plugins:
280
315
 
281
316
  ```typescript
282
- const migrationConfig = {
283
- preserveData: true, // Always try to preserve existing data
284
- autoMigrate: true, // Automatically handle schema changes
285
- dropTables: false // Don't drop tables by default
317
+ // Example: Audit logging with timing
318
+ const timers = new Map<string, number>();
319
+ const auditPlugin: NanoPlugin = {
320
+ name: 'audit',
321
+ beforeInsert: (table) => { timers.set('op', performance.now()); console.log(`INSERT ${table}`); },
322
+ afterInsert: () => { console.log(` ↳ ${(performance.now() - timers.get('op')!).toFixed(1)}ms`); },
323
+ beforeQuery: (table) => { timers.set('op', performance.now()); console.log(`SELECT ${table}`); },
324
+ afterQuery: (t, _, rows) => { console.log(` ↳ ${rows.length} rows in ${(performance.now() - timers.get('op')!).toFixed(1)}ms`); },
325
+ };
326
+
327
+ // Auto-generate slugs
328
+ const slugPlugin: NanoPlugin = {
329
+ name: 'slug',
330
+ beforeInsert: (table, data) => {
331
+ if (table === 'posts' && data.title && !data.slug) {
332
+ return { ...data, slug: data.title.toLowerCase().replace(/\s+/g, '-') };
333
+ }
334
+ return data;
335
+ },
286
336
  };
287
337
 
288
- initializeDatabase({
338
+ // Validation
339
+ const validationPlugin: NanoPlugin = {
340
+ name: 'validation',
341
+ beforeInsert: (table, data) => {
342
+ if (table === 'users' && !data.email?.includes('@')) {
343
+ throw new Error('Invalid email format');
344
+ }
345
+ return data;
346
+ },
347
+ };
348
+
349
+ // Use plugins
350
+ const db = await createDatabase({
289
351
  tables,
290
- migrationConfig
352
+ plugins: [auditPlugin, slugPlugin, validationPlugin],
291
353
  });
354
+
355
+ // Hooks run automatically
356
+ await db.insert(posts).values({ title: 'My Post' }); // slug auto-generated
357
+ await db.insert(users).values({ email: 'invalid' }); // throws error
358
+
359
+ // Check loaded plugins
360
+ db.plugins.list(); // ['audit', 'slug', 'validation']
292
361
  ```
293
362
 
294
- ## Usage Examples
363
+ ## Best Practices
295
364
 
296
- ### Basic Setup
365
+ ### Recommended Project Structure
366
+
367
+ ```
368
+ db/
369
+ ├── schema.ts # Table definitions
370
+ ├── index.ts # Database instance export
371
+ ├── types.ts # Type aliases (SelectModel, InsertModel)
372
+ ├── plugins.ts # Custom plugins
373
+ └── seeds.ts # Seed data
374
+ ```
375
+
376
+ ### 1. Schema Order Matters
377
+
378
+ Define parent tables before children for correct seeding order:
297
379
 
298
380
  ```typescript
299
- import { initializeDatabase, DatabaseSync } from 'nanodb-orm';
300
- import { usersTable, postsTable } from './models';
381
+ // db/schema.ts
382
+ import nanodb from 'nanodb-orm';
383
+
384
+ // Parent tables first (no foreign keys)
385
+ export const users = nanodb.schema.table('users', {
386
+ id: nanodb.schema.integer('id').primaryKey({ autoIncrement: true }),
387
+ name: nanodb.schema.text('name').notNull(),
388
+ email: nanodb.schema.text('email').notNull().unique(),
389
+ });
301
390
 
302
- const tables = {
303
- users: usersTable,
304
- posts: postsTable
305
- };
391
+ export const categories = nanodb.schema.table('categories', {
392
+ id: nanodb.schema.integer('id').primaryKey({ autoIncrement: true }),
393
+ name: nanodb.schema.text('name').notNull(),
394
+ });
306
395
 
307
- initializeDatabase({ tables });
308
- await DatabaseSync.setup();
396
+ // Child tables after (have foreign keys)
397
+ export const posts = nanodb.schema.table('posts', {
398
+ id: nanodb.schema.integer('id').primaryKey({ autoIncrement: true }),
399
+ title: nanodb.schema.text('title').notNull(),
400
+ userId: nanodb.schema.integer('userId').notNull(), // FK to users
401
+ categoryId: nanodb.schema.integer('categoryId'), // FK to categories
402
+ });
403
+
404
+ // Order: parents → children
405
+ export const schema = { users, categories, posts };
309
406
  ```
310
407
 
311
- ### With Seed Data
408
+ ### 2. Single Database Instance
312
409
 
313
410
  ```typescript
314
- const seedData = {
315
- users: [
316
- { name: 'Alice', age: 25, email: 'alice@example.com' },
317
- { name: 'Bob', age: 30, email: 'bob@example.com' }
318
- ],
319
- posts: [
320
- { title: 'Hello World', content: 'My first post', userId: 1 },
321
- { title: 'Second Post', content: 'Another post', userId: 2 }
322
- ]
323
- };
411
+ // db/index.ts
412
+ import nanodb from 'nanodb-orm';
413
+ import { schema } from './schema';
414
+ import { seedData } from './seeds';
324
415
 
325
- initializeDatabase({ tables, seedData });
326
- await DatabaseSync.setup();
416
+ export const db = await nanodb.createDatabase({ tables: schema, seedData });
327
417
  ```
328
418
 
329
- ### Custom Migration Config
419
+ ```typescript
420
+ // anywhere.ts
421
+ import { db } from './db';
422
+ import { users } from './db/schema';
423
+
424
+ const allUsers = await db.select().from(users);
425
+ ```
426
+
427
+ ### 3. Use Type Inference
330
428
 
331
429
  ```typescript
332
- const migrationConfig = {
333
- preserveData: false, // Allow data loss for development
334
- autoMigrate: true, // Enable auto-migrations
335
- dropTables: true // Allow dropping tables
336
- };
430
+ // db/types.ts
431
+ import { type SelectModel, type InsertModel } from 'nanodb-orm';
432
+ import { users, posts } from './schema';
337
433
 
338
- initializeDatabase({
339
- tables,
340
- migrationConfig
341
- });
434
+ export type User = SelectModel<typeof users>;
435
+ export type NewUser = InsertModel<typeof users>;
436
+ export type Post = SelectModel<typeof posts>;
437
+
438
+ // Usage
439
+ async function createUser(data: NewUser): Promise<void> {
440
+ await db.insert(users).values(data); // TypeScript enforces shape
441
+ }
342
442
  ```
343
443
 
344
- ### Transaction Support (NEW in v0.0.3)
444
+ ### 4. Prefer Grouped Imports
345
445
 
346
446
  ```typescript
347
- import { TransactionManager } from 'nanodb-orm';
447
+ // Clean - default import
448
+ import nanodb from 'nanodb-orm';
348
449
 
349
- // Atomic operations
350
- await TransactionManager.execute(async (db) => {
351
- await db.run('INSERT INTO users (name, email) VALUES (?, ?)', ['John', 'john@example.com']);
352
- await db.run('INSERT INTO posts (title, content, userId) VALUES (?, ?, ?)', ['Hello', 'World', 1]);
353
- // All operations succeed or all fail
354
- });
450
+ const users = nanodb.schema.table('users', { ... });
451
+ await db.select().from(users).where(nanodb.query.eq(users.id, 1));
355
452
 
356
- // Batch operations
357
- await TransactionManager.executeBatch([
358
- async (db) => await db.run('INSERT INTO users ...'),
359
- async (db) => await db.run('INSERT INTO posts ...'),
360
- async (db) => await db.run('UPDATE stats ...')
361
- ]);
453
+ // Also good - grouped imports
454
+ import { schema, query, errors } from 'nanodb-orm';
362
455
 
363
- // Table recreation with transactions
364
- await TransactionManager.recreateTable('users', async (db) => {
365
- await db.run('CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT)');
366
- });
456
+ // Avoid - many individual imports
457
+ import { table, integer, text, eq, gte, and, sql, count, ... } from 'nanodb-orm';
367
458
  ```
368
459
 
369
- ### Enhanced Error Handling (NEW in v0.0.3)
460
+ ### 5. Handle Errors Gracefully
370
461
 
371
462
  ```typescript
372
- import { ErrorHandler, DatabaseError } from 'nanodb-orm';
463
+ import { parseDbError, DatabaseError } from 'nanodb-orm';
373
464
 
374
465
  try {
375
- await DatabaseSync.setup();
466
+ await db.insert(users).values({ email: 'duplicate@test.com' });
376
467
  } catch (error) {
377
468
  if (error instanceof DatabaseError) {
378
- console.log('Operation:', error.operation);
379
- console.log('Context:', error.message);
380
- console.log('Original Error:', error.originalError);
469
+ // Already formatted with context
470
+ console.log(error.message); // "UNIQUE constraint failed: users.email"
471
+ console.log(error.table); // "users"
472
+ } else {
473
+ const parsed = parseDbError(error, { table: 'users' });
474
+ console.log(parsed.format());
381
475
  }
382
476
  }
383
-
384
- // Non-throwing error handling
385
- const result = ErrorHandler.handleNonThrowingError(
386
- someError,
387
- 'optional-operation',
388
- 'This operation is optional'
389
- );
390
477
  ```
391
478
 
392
- ### Thread-Safe Connections (NEW in v0.0.3)
479
+ ### 6. Use Transactions for Atomic Operations
393
480
 
394
481
  ```typescript
395
- import { DatabaseConnection } from 'nanodb-orm';
482
+ import nanodb from 'nanodb-orm';
396
483
 
397
- // NEW: Async connection (recommended)
398
- const db = await DatabaseConnection.getInstance();
399
-
400
- // OLD: Synchronous connection (deprecated but still works)
401
- const db = DatabaseConnection.getInstanceSync();
484
+ const result = await nanodb.transaction(async (tx) => {
485
+ await tx.run(nanodb.query.sql`INSERT INTO users (name) VALUES ('Alice')`);
486
+ await tx.run(nanodb.query.sql`INSERT INTO posts (title, userId) VALUES ('Hello', 1)`);
487
+ return { inserted: 2 };
488
+ });
402
489
 
403
- // Check connection status
404
- if (DatabaseConnection.isConnected()) {
405
- console.log('Database is connected');
490
+ if (!result.success) {
491
+ console.log('Rolled back:', result.error?.message);
406
492
  }
407
493
  ```
408
494
 
409
- ### Schema Introspection
495
+ ### 7. Validate on Startup (Production)
410
496
 
411
497
  ```typescript
412
- import { SchemaIntrospection } from 'nanodb-orm';
498
+ import nanodb from 'nanodb-orm';
413
499
 
414
- // Get comprehensive schema information
415
- const schemaInfo = SchemaIntrospection.getSchemaStats();
416
- console.log('Total tables:', schemaInfo.totalTables);
417
- console.log('Table details:', schemaInfo.tableDetails);
500
+ const db = await nanodb.createDatabase({ tables: schema });
418
501
 
419
- // Validate schema integrity
420
- const validation = await SchemaIntrospection.validateSchema();
421
- if (!validation.isValid) {
422
- console.log('Schema issues:', validation.errors);
502
+ if (process.env.NODE_ENV === 'production') {
503
+ const validation = await db.schema.validate();
504
+ if (!validation.isValid) {
505
+ throw new Error(`Schema mismatch: ${validation.missingTables.join(', ')}`);
506
+ }
507
+
508
+ const health = await db.healthCheck();
509
+ if (!health.healthy) {
510
+ console.warn('Database issues:', health.errors);
511
+ }
423
512
  }
424
513
  ```
425
514
 
426
- ## Testing
427
-
428
- The package includes built-in testing utilities:
515
+ ### 8. Keep Plugins Simple
429
516
 
430
517
  ```typescript
431
- import { DatabaseSync } from 'nanodb-orm';
518
+ // Good: focused, single responsibility
519
+ const timestampPlugin: NanoPlugin = {
520
+ name: 'timestamps',
521
+ beforeInsert: (_table, data) => ({
522
+ ...data,
523
+ createdAt: new Date().toISOString(),
524
+ }),
525
+ beforeUpdate: (_table, data) => ({
526
+ ...data,
527
+ updatedAt: new Date().toISOString(),
528
+ }),
529
+ };
432
530
 
433
- describe('My Tests', () => {
434
- beforeEach(async () => {
435
- // Reset database for each test
436
- await DatabaseSync.reset();
437
- });
531
+ // Avoid: complex business logic in hooks
532
+ ```
438
533
 
439
- test('should work with clean database', async () => {
440
- // Your test code here
441
- });
442
- });
534
+ ### 9. Environment Configuration
535
+
536
+ ```bash
537
+ # .env
538
+ TURSO_CONNECTION_URL=libsql://your-db.turso.io
539
+ TURSO_AUTH_TOKEN=your-token
540
+ FORCE_LOCAL_DB=true # Use local SQLite
541
+ DATABASE_PATH=./data/app.db # Custom DB path
443
542
  ```
444
543
 
445
- ## Database Support
544
+ nanodb-orm auto-detects the right database:
545
+ - **Turso**: when `TURSO_*` vars are set
546
+ - **Local**: when `FORCE_LOCAL_DB=true` or no Turso config
547
+ - **Test**: isolated `test.db` when `NODE_ENV=test`
446
548
 
447
- ### Local SQLite
448
- - Automatically used when Turso credentials are not available
449
- - Perfect for development and testing
450
- - File-based storage
549
+ ## Configuration
550
+
551
+ ### Environment Variables
552
+
553
+ ```bash
554
+ # Remote Turso database
555
+ TURSO_CONNECTION_URL=libsql://your-db.turso.io
556
+ TURSO_AUTH_TOKEN=your-token
557
+
558
+ # Force local SQLite
559
+ FORCE_LOCAL_DB=true
560
+
561
+ # Custom database path (works with FORCE_LOCAL_DB or as fallback)
562
+ DATABASE_PATH=./data/myapp.db
563
+ ```
451
564
 
452
- ### Remote Turso
453
- - Cloud-hosted SQLite database
454
- - Requires `TURSO_CONNECTION_URL` and `TURSO_AUTH_TOKEN`
455
- - Production-ready with global replication
565
+ ### Database Selection
566
+
567
+ - **Turso** — Used when `TURSO_CONNECTION_URL` and `TURSO_AUTH_TOKEN` are set
568
+ - **Local SQLite** Used when `FORCE_LOCAL_DB=true` or Turso credentials missing
569
+ - **Custom Path** — Set `DATABASE_PATH=./path/to/db.sqlite` for custom location
570
+ - **Test Mode** — Isolated `test.db` when `NODE_ENV=test`
456
571
 
457
572
  ## Error Handling
458
573
 
459
- The package provides comprehensive error handling:
574
+ Errors are automatically parsed into user-friendly messages:
460
575
 
461
576
  ```typescript
462
- import { DatabaseError } from 'nanodb-orm';
577
+ import { DatabaseError, SchemaError, SeedError, parseDbError } from 'nanodb-orm';
463
578
 
464
579
  try {
465
580
  await DatabaseSync.setup();
466
581
  } catch (error) {
467
582
  if (error instanceof DatabaseError) {
468
- console.log('Database error:', error.message);
469
- console.log('Operation:', error.operation);
583
+ console.log(error.message); // User-friendly message
584
+ console.log(error.operation); // 'seed', 'migration', etc.
585
+ console.log(error.table); // Table name if applicable
586
+ console.log(error.detail); // Additional context
470
587
  }
471
588
  }
472
589
  ```
473
590
 
474
- ## Contributing
591
+ Error output is clean and actionable:
475
592
 
476
- 1. Fork the repository
477
- 2. Create your feature branch (`git checkout -b feature/amazing-feature`)
478
- 3. Commit your changes (`git commit -m 'Add some amazing feature'`)
479
- 4. Push to the branch (`git push origin feature/amazing-feature`)
480
- 5. Open a Pull Request
593
+ ```
594
+ ┌─ nanodb-orm error ─────────────────────────────
595
+ Column "email" does not exist
596
+ Table: users
597
+ Operation: seed
598
+ │ Detail: Row data: {"name":"Alice","email":"alice@example.com"}
599
+ └────────────────────────────────────────────────
600
+ ```
601
+
602
+ ## Exports
603
+
604
+ ```typescript
605
+ // Default export (recommended)
606
+ import nanodb from 'nanodb-orm';
607
+
608
+ nanodb.createDatabase // Main entry point
609
+ nanodb.transaction // Atomic operations
610
+ nanodb.schema // .table, .integer, .text, .real, .blob
611
+ nanodb.query // .eq, .gte, .and, .or, .sql, .count, ...
612
+ nanodb.errors // .DatabaseError, .parse
613
+ nanodb.cli // .studio, .setup, .reset, .status, .validate
614
+
615
+ // Types (named imports)
616
+ import { type SelectModel, type InsertModel, type NanoPlugin } from 'nanodb-orm';
617
+ ```
618
+
619
+ ## nanodb-orm vs Drizzle + Turso (Direct)
620
+
621
+ | Feature | nanodb-orm | Drizzle + Turso |
622
+ |---------|------------|-----------------|
623
+ | **Setup** | One-liner: `createDatabase({ tables })` | Manual: create client, drizzle, manage connection |
624
+ | **Migrations** | Automatic on startup | Manual: `drizzle-kit push/migrate` |
625
+ | **Seeding** | Built-in with `seedData` | Write seed scripts |
626
+ | **Type Safety** | ✅ Full (same as Drizzle) | ✅ Full |
627
+ | **Query API** | ✅ Same as Drizzle | ✅ Native Drizzle |
628
+ | **Plugins/Hooks** | ✅ beforeInsert, afterQuery, etc. | ❌ None |
629
+ | **Schema Introspection** | ✅ `db.schema.tables()` | ❌ Manual |
630
+ | **Health Checks** | ✅ `db.healthCheck()` | ❌ Manual |
631
+ | **CLI** | `npx nanodb studio/status/validate` | `npx drizzle-kit studio` only |
632
+ | **Error Parsing** | User-friendly messages | Raw SQLite errors |
633
+ | **Connection** | Auto-detects Turso vs local | Manual configuration |
634
+
635
+ ### When to Use What
636
+
637
+ | Use Case | Recommendation |
638
+ |----------|----------------|
639
+ | Quick prototyping | **nanodb-orm** |
640
+ | Need plugins/hooks | **nanodb-orm** |
641
+ | Want auto-migrations | **nanodb-orm** |
642
+ | New SQLite/Turso project | **nanodb-orm** |
643
+ | Maximum control | Drizzle directly |
644
+ | Complex migration strategies | Drizzle + drizzle-kit |
645
+ | Existing Drizzle project | Keep Drizzle |
646
+
647
+ ### Comparison
648
+
649
+ ```typescript
650
+ // nanodb-orm: 5 lines
651
+ import nanodb from 'nanodb-orm';
652
+
653
+ const users = nanodb.schema.table('users', { id: nanodb.schema.integer('id').primaryKey() });
654
+ const db = await nanodb.createDatabase({ tables: { users }, seedData: { users: [{ id: 1 }] } });
655
+ // Ready - tables created, seeded
656
+ ```
657
+
658
+ ```typescript
659
+ // Drizzle + Turso: More setup
660
+ import { drizzle } from 'drizzle-orm/libsql';
661
+ import { createClient } from '@libsql/client';
662
+ import { sqliteTable, integer } from 'drizzle-orm/sqlite-core';
663
+ import { migrate } from 'drizzle-orm/libsql/migrator';
664
+
665
+ const users = sqliteTable('users', { id: integer('id').primaryKey() });
666
+ const client = createClient({ url: process.env.TURSO_CONNECTION_URL!, authToken: process.env.TURSO_AUTH_TOKEN! });
667
+ const db = drizzle(client);
668
+ await migrate(db, { migrationsFolder: './drizzle' });
669
+ await db.insert(users).values([{ id: 1 }]);
670
+ ```
671
+
672
+ **nanodb-orm is a convenience layer** — it uses Drizzle under the hood and passes through all queries unchanged. You get Drizzle's full type safety plus automatic setup, plugins, and utilities.
481
673
 
482
674
  ## License
483
675
 
484
676
  MIT © Damilola Alao
485
-
486
- ## Changelog
487
-
488
- ### 0.0.2
489
- - **Enhanced Documentation**: Added comprehensive Drizzle ORM integration examples
490
- - **Real Model Examples**: Updated documentation with actual Drizzle table definitions
491
- - **Schema Validation Fix**: Fixed table existence validation logic for proper health checks
492
- - **LLM Documentation**: Added detailed `llm.txt` file for AI/LLM integration
493
- - **Column Types Guide**: Added complete Drizzle column types and methods documentation
494
- - **Foreign Key Support**: Documented foreign key relationships and cascade deletes
495
- - **Type Safety**: Enhanced TypeScript integration examples with Drizzle's type inference
496
-
497
- ### 0.0.1
498
- - Initial release
499
- - Auto-migration system
500
- - Schema introspection
501
- - Multi-database support
502
- - TypeScript support
503
- - Testing utilities