masterrecord 0.2.34 โ 0.3.0
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.
- package/.claude/settings.local.json +25 -1
- package/Entity/entityModel.js +6 -0
- package/Entity/entityTrackerModel.js +20 -3
- package/Entity/fieldTransformer.js +266 -0
- package/Migrations/migrationMySQLQuery.js +145 -1
- package/Migrations/migrationPostgresQuery.js +402 -0
- package/Migrations/migrationSQLiteQuery.js +145 -1
- package/Migrations/schema.js +131 -28
- package/QueryLanguage/queryMethods.js +193 -15
- package/QueryLanguage/queryParameters.js +136 -0
- package/QueryLanguage/queryScript.js +14 -5
- package/SQLLiteEngine.js +309 -19
- package/context.js +57 -12
- package/docs/INCLUDES_CLARIFICATION.md +202 -0
- package/docs/METHODS_REFERENCE.md +184 -0
- package/docs/MIGRATIONS_GUIDE.md +699 -0
- package/docs/POSTGRESQL_SETUP.md +415 -0
- package/examples/jsonArrayTransformer.js +215 -0
- package/mySQLEngine.js +249 -17
- package/package.json +6 -6
- package/postgresEngine.js +434 -491
- package/postgresSyncConnect.js +209 -0
- package/readme.md +1121 -265
- package/test/anyCommaStringTest.js +237 -0
- package/test/anyMethodTest.js +176 -0
- package/test/findByIdTest.js +227 -0
- package/test/includesFeatureTest.js +183 -0
- package/test/includesTransformTest.js +110 -0
- package/test/newMethodTest.js +330 -0
- package/test/newMethodUnitTest.js +320 -0
- package/test/parameterizedPlaceholderTest.js +159 -0
- package/test/postgresEngineTest.js +463 -0
- package/test/postgresIntegrationTest.js +381 -0
- package/test/securityTest.js +268 -0
- package/test/singleDollarPlaceholderTest.js +238 -0
- package/test/tablePrefixTest.js +100 -0
- package/test/transformerTest.js +287 -0
- package/test/verifyFindById.js +169 -0
- package/test/verifyNewMethod.js +191 -0
- package/test/whereChainingTest.js +88 -0
package/readme.md
CHANGED
|
@@ -1,374 +1,1230 @@
|
|
|
1
|
+
# MasterRecord
|
|
1
2
|
|
|
3
|
+
[](https://www.npmjs.com/package/masterrecord)
|
|
4
|
+
[](https://opensource.org/licenses/MIT)
|
|
2
5
|
|
|
3
|
-
|
|
6
|
+
**MasterRecord** is a lightweight, code-first ORM for Node.js with a fluent query API, comprehensive migrations, and multi-database support. Build type-safe queries with lambda expressions, manage schema changes with CLI-driven migrations, and work seamlessly across MySQL, PostgreSQL, and SQLite.
|
|
7
|
+
|
|
8
|
+
## Key Features
|
|
9
|
+
|
|
10
|
+
๐น **Multi-Database Support** - MySQL, PostgreSQL, SQLite with consistent API
|
|
11
|
+
๐น **Code-First Design** - Define entities in JavaScript, generate schema automatically
|
|
12
|
+
๐น **Fluent Query API** - Lambda-based queries with parameterized placeholders
|
|
13
|
+
๐น **Migration System** - CLI-driven migrations with rollback support
|
|
14
|
+
๐น **SQL Injection Protection** - Automatic parameterized queries throughout
|
|
15
|
+
๐น **Field Transformers** - Custom serialization/deserialization for complex types
|
|
16
|
+
๐น **Type Validation** - Runtime type checking and coercion
|
|
17
|
+
๐น **Relationship Mapping** - One-to-many, many-to-one, many-to-many support
|
|
18
|
+
๐น **Seed Data** - Built-in seeding with idempotent operations
|
|
4
19
|
|
|
5
|
-
|
|
20
|
+
## Database Support
|
|
6
21
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
22
|
+
| Database | Version | Features |
|
|
23
|
+
|------------|--------------|---------------------------------------------------|
|
|
24
|
+
| PostgreSQL | 9.6+ (12+) | JSONB, UUID, async/await, connection pooling |
|
|
25
|
+
| MySQL | 5.7+ (8.0+) | JSON, transactions, AUTO_INCREMENT |
|
|
26
|
+
| SQLite | 3.x | Embedded, zero-config, file-based |
|
|
27
|
+
|
|
28
|
+
## Table of Contents
|
|
29
|
+
|
|
30
|
+
- [Installation](#installation)
|
|
31
|
+
- [Quick Start](#quick-start)
|
|
32
|
+
- [Database Configuration](#database-configuration)
|
|
33
|
+
- [Entity Definitions](#entity-definitions)
|
|
34
|
+
- [Querying](#querying)
|
|
35
|
+
- [Migrations](#migrations)
|
|
36
|
+
- [Advanced Features](#advanced-features)
|
|
37
|
+
- [API Reference](#api-reference)
|
|
38
|
+
- [Examples](#examples)
|
|
10
39
|
|
|
11
40
|
## Installation
|
|
12
41
|
|
|
13
42
|
```bash
|
|
14
|
-
#
|
|
43
|
+
# Global installation (recommended for CLI)
|
|
15
44
|
npm install -g masterrecord
|
|
16
45
|
|
|
17
|
-
#
|
|
18
|
-
|
|
46
|
+
# Local installation
|
|
47
|
+
npm install masterrecord
|
|
19
48
|
|
|
20
|
-
#
|
|
21
|
-
|
|
49
|
+
# With specific database drivers
|
|
50
|
+
npm install masterrecord pg # PostgreSQL
|
|
51
|
+
npm install masterrecord mysql2 # MySQL
|
|
52
|
+
npm install masterrecord better-sqlite3 # SQLite
|
|
22
53
|
```
|
|
23
54
|
|
|
55
|
+
### Dependencies
|
|
56
|
+
|
|
57
|
+
MasterRecord includes the following database drivers by default:
|
|
58
|
+
- `pg@^8.16.3` - PostgreSQL
|
|
59
|
+
- `sync-mysql2@^1.0.8` - MySQL
|
|
60
|
+
- `better-sqlite3@^12.6.0` - SQLite
|
|
61
|
+
|
|
24
62
|
## Quick Start
|
|
25
63
|
|
|
26
|
-
1
|
|
64
|
+
### 1. Create a Context
|
|
65
|
+
|
|
66
|
+
```javascript
|
|
67
|
+
// app/models/context.js
|
|
68
|
+
const context = require('masterrecord/context');
|
|
69
|
+
const User = require('./User');
|
|
70
|
+
const Post = require('./Post');
|
|
71
|
+
|
|
72
|
+
class AppContext extends context {
|
|
73
|
+
constructor() {
|
|
74
|
+
super();
|
|
75
|
+
|
|
76
|
+
// Configure database connection
|
|
77
|
+
this.env({
|
|
78
|
+
type: 'postgres', // or 'mysql', 'sqlite'
|
|
79
|
+
host: 'localhost',
|
|
80
|
+
port: 5432,
|
|
81
|
+
database: 'myapp',
|
|
82
|
+
user: 'postgres',
|
|
83
|
+
password: 'password'
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
// Register entities
|
|
87
|
+
this.dbset(User);
|
|
88
|
+
this.dbset(Post);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
module.exports = AppContext;
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
### 2. Define Entities
|
|
96
|
+
|
|
97
|
+
```javascript
|
|
98
|
+
// app/models/User.js
|
|
99
|
+
class User {
|
|
100
|
+
constructor() {
|
|
101
|
+
this.id = { type: 'integer', primary: true, auto: true };
|
|
102
|
+
this.name = { type: 'string', nullable: false };
|
|
103
|
+
this.email = { type: 'string', nullable: false, unique: true };
|
|
104
|
+
this.age = { type: 'integer', nullable: true };
|
|
105
|
+
this.created_at = { type: 'timestamp', default: 'CURRENT_TIMESTAMP' };
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
module.exports = User;
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
### 3. Run Migrations
|
|
27
113
|
|
|
28
114
|
```bash
|
|
115
|
+
# Enable migrations (one-time setup)
|
|
29
116
|
masterrecord enable-migrations AppContext
|
|
117
|
+
|
|
118
|
+
# Create initial migration
|
|
119
|
+
masterrecord add-migration InitialCreate AppContext
|
|
120
|
+
|
|
121
|
+
# Apply migrations
|
|
122
|
+
masterrecord migrate AppContext
|
|
30
123
|
```
|
|
31
124
|
|
|
32
|
-
|
|
125
|
+
### 4. Query Your Data
|
|
33
126
|
|
|
34
|
-
```
|
|
35
|
-
|
|
127
|
+
```javascript
|
|
128
|
+
const AppContext = require('./app/models/context');
|
|
129
|
+
const db = new AppContext();
|
|
130
|
+
|
|
131
|
+
// Create
|
|
132
|
+
const user = db.User.new();
|
|
133
|
+
user.name = 'Alice';
|
|
134
|
+
user.email = 'alice@example.com';
|
|
135
|
+
user.age = 28;
|
|
136
|
+
await db.saveChanges();
|
|
137
|
+
|
|
138
|
+
// Read with parameterized query
|
|
139
|
+
const alice = db.User
|
|
140
|
+
.where(u => u.email == $$, 'alice@example.com')
|
|
141
|
+
.single();
|
|
142
|
+
|
|
143
|
+
// Update
|
|
144
|
+
alice.age = 29;
|
|
145
|
+
await db.saveChanges();
|
|
146
|
+
|
|
147
|
+
// Delete
|
|
148
|
+
db.remove(alice);
|
|
149
|
+
await db.saveChanges();
|
|
36
150
|
```
|
|
37
151
|
|
|
38
|
-
|
|
152
|
+
## Database Configuration
|
|
39
153
|
|
|
40
|
-
|
|
41
|
-
|
|
154
|
+
### PostgreSQL (Async)
|
|
155
|
+
|
|
156
|
+
```javascript
|
|
157
|
+
class AppContext extends context {
|
|
158
|
+
constructor() {
|
|
159
|
+
super();
|
|
160
|
+
|
|
161
|
+
this.env({
|
|
162
|
+
type: 'postgres',
|
|
163
|
+
host: 'localhost',
|
|
164
|
+
port: 5432,
|
|
165
|
+
database: 'myapp',
|
|
166
|
+
user: 'postgres',
|
|
167
|
+
password: 'password',
|
|
168
|
+
max: 20, // Connection pool size
|
|
169
|
+
idleTimeoutMillis: 30000,
|
|
170
|
+
connectionTimeoutMillis: 2000
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
this.dbset(User);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Usage requires await
|
|
178
|
+
const db = new AppContext();
|
|
179
|
+
await db.saveChanges(); // PostgreSQL is async
|
|
42
180
|
```
|
|
43
181
|
|
|
44
|
-
###
|
|
182
|
+
### MySQL (Synchronous)
|
|
45
183
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
184
|
+
```javascript
|
|
185
|
+
class AppContext extends context {
|
|
186
|
+
constructor() {
|
|
187
|
+
super();
|
|
188
|
+
|
|
189
|
+
this.env({
|
|
190
|
+
type: 'mysql',
|
|
191
|
+
host: 'localhost',
|
|
192
|
+
port: 3306,
|
|
193
|
+
database: 'myapp',
|
|
194
|
+
user: 'root',
|
|
195
|
+
password: 'password'
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
this.dbset(User);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// Usage is synchronous
|
|
203
|
+
const db = new AppContext();
|
|
204
|
+
db.saveChanges(); // No await needed
|
|
49
205
|
```
|
|
50
|
-
This creates `db/migrations/<context>_contextSnapShot.json` and the `db/migrations` directory.
|
|
51
206
|
|
|
52
|
-
###
|
|
207
|
+
### SQLite (Synchronous)
|
|
53
208
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
209
|
+
```javascript
|
|
210
|
+
class AppContext extends context {
|
|
211
|
+
constructor() {
|
|
212
|
+
super();
|
|
213
|
+
|
|
214
|
+
this.env({
|
|
215
|
+
type: 'sqlite',
|
|
216
|
+
connection: './data/myapp.db' // File path
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
this.dbset(User);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
57
222
|
```
|
|
58
|
-
This writes a new file to `db/migrations/<timestamp>_<MigrationName>_migration.js`.
|
|
59
223
|
|
|
60
|
-
###
|
|
224
|
+
### Environment Files
|
|
61
225
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
226
|
+
Store configurations in JSON files:
|
|
227
|
+
|
|
228
|
+
```json
|
|
229
|
+
// config/environments/env.development.json
|
|
230
|
+
{
|
|
231
|
+
"type": "postgres",
|
|
232
|
+
"host": "localhost",
|
|
233
|
+
"port": 5432,
|
|
234
|
+
"database": "myapp_dev",
|
|
235
|
+
"user": "postgres",
|
|
236
|
+
"password": "dev_password"
|
|
237
|
+
}
|
|
65
238
|
```
|
|
66
|
-
|
|
67
|
-
```
|
|
68
|
-
|
|
239
|
+
|
|
240
|
+
```javascript
|
|
241
|
+
// Load environment file
|
|
242
|
+
class AppContext extends context {
|
|
243
|
+
constructor() {
|
|
244
|
+
super();
|
|
245
|
+
this.env('config/environments'); // Loads env.<NODE_ENV>.json
|
|
246
|
+
this.dbset(User);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
69
249
|
```
|
|
70
|
-
|
|
250
|
+
|
|
71
251
|
```bash
|
|
72
|
-
|
|
252
|
+
# Set environment
|
|
253
|
+
export NODE_ENV=development
|
|
254
|
+
node app.js
|
|
73
255
|
```
|
|
74
256
|
|
|
75
|
-
|
|
76
|
-
- The CLI searches for `<context>_contextSnapShot.json` under `db/migrations` relative to your current working directory.
|
|
77
|
-
- For MySQL, ensure your credentials allow DDL. For SQLite, the data directory is created if missing.
|
|
257
|
+
## Entity Definitions
|
|
78
258
|
|
|
79
|
-
###
|
|
259
|
+
### Basic Entity
|
|
80
260
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
261
|
+
```javascript
|
|
262
|
+
class User {
|
|
263
|
+
constructor() {
|
|
264
|
+
// Primary key with auto-increment
|
|
265
|
+
this.id = {
|
|
266
|
+
type: 'integer',
|
|
267
|
+
primary: true,
|
|
268
|
+
auto: true
|
|
269
|
+
};
|
|
270
|
+
|
|
271
|
+
// Required string field
|
|
272
|
+
this.name = {
|
|
273
|
+
type: 'string',
|
|
274
|
+
nullable: false
|
|
275
|
+
};
|
|
276
|
+
|
|
277
|
+
// Optional field with default
|
|
278
|
+
this.status = {
|
|
279
|
+
type: 'string',
|
|
280
|
+
nullable: true,
|
|
281
|
+
default: 'active'
|
|
282
|
+
};
|
|
283
|
+
|
|
284
|
+
// Unique constraint
|
|
285
|
+
this.email = {
|
|
286
|
+
type: 'string',
|
|
287
|
+
unique: true
|
|
288
|
+
};
|
|
289
|
+
|
|
290
|
+
// Timestamp
|
|
291
|
+
this.created_at = {
|
|
292
|
+
type: 'timestamp',
|
|
293
|
+
default: 'CURRENT_TIMESTAMP'
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
### Field Types
|
|
300
|
+
|
|
301
|
+
| MasterRecord Type | PostgreSQL | MySQL | SQLite |
|
|
302
|
+
|-------------------|---------------|---------------|-----------|
|
|
303
|
+
| `integer` | INTEGER | INT | INTEGER |
|
|
304
|
+
| `bigint` | BIGINT | BIGINT | INTEGER |
|
|
305
|
+
| `string` | VARCHAR(255) | VARCHAR(255) | TEXT |
|
|
306
|
+
| `text` | TEXT | TEXT | TEXT |
|
|
307
|
+
| `float` | REAL | FLOAT | REAL |
|
|
308
|
+
| `decimal` | DECIMAL | DECIMAL | REAL |
|
|
309
|
+
| `boolean` | BOOLEAN | TINYINT | INTEGER |
|
|
310
|
+
| `date` | DATE | DATE | TEXT |
|
|
311
|
+
| `time` | TIME | TIME | TEXT |
|
|
312
|
+
| `datetime` | TIMESTAMP | DATETIME | TEXT |
|
|
313
|
+
| `timestamp` | TIMESTAMP | TIMESTAMP | TEXT |
|
|
314
|
+
| `json` | JSON | JSON | TEXT |
|
|
315
|
+
| `jsonb` | JSONB | JSON | TEXT |
|
|
316
|
+
| `uuid` | UUID | VARCHAR(36) | TEXT |
|
|
317
|
+
| `binary` | BYTEA | BLOB | BLOB |
|
|
318
|
+
|
|
319
|
+
### Relationships
|
|
320
|
+
|
|
321
|
+
```javascript
|
|
322
|
+
class User {
|
|
323
|
+
constructor() {
|
|
324
|
+
this.id = { type: 'integer', primary: true, auto: true };
|
|
325
|
+
this.name = { type: 'string' };
|
|
326
|
+
|
|
327
|
+
// One-to-many: User has many Posts
|
|
328
|
+
this.Posts = {
|
|
329
|
+
type: 'hasMany',
|
|
330
|
+
model: 'Post',
|
|
331
|
+
foreignKey: 'user_id'
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
class Post {
|
|
337
|
+
constructor() {
|
|
338
|
+
this.id = { type: 'integer', primary: true, auto: true };
|
|
339
|
+
this.title = { type: 'string' };
|
|
340
|
+
this.user_id = { type: 'integer' };
|
|
341
|
+
|
|
342
|
+
// Many-to-one: Post belongs to User
|
|
343
|
+
this.User = {
|
|
344
|
+
type: 'belongsTo',
|
|
345
|
+
model: 'User',
|
|
346
|
+
foreignKey: 'user_id'
|
|
347
|
+
};
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
```
|
|
351
|
+
|
|
352
|
+
### Field Transformers
|
|
353
|
+
|
|
354
|
+
Store complex JavaScript types in simple database columns:
|
|
355
|
+
|
|
356
|
+
```javascript
|
|
357
|
+
class User {
|
|
358
|
+
constructor() {
|
|
359
|
+
this.id = { type: 'integer', primary: true, auto: true };
|
|
360
|
+
|
|
361
|
+
// Store arrays as JSON strings
|
|
362
|
+
this.tags = {
|
|
363
|
+
type: 'string',
|
|
364
|
+
transform: {
|
|
365
|
+
toDatabase: (value) => {
|
|
366
|
+
return Array.isArray(value) ? JSON.stringify(value) : value;
|
|
367
|
+
},
|
|
368
|
+
fromDatabase: (value) => {
|
|
369
|
+
return value ? JSON.parse(value) : [];
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
};
|
|
373
|
+
}
|
|
168
374
|
}
|
|
169
|
-
|
|
375
|
+
|
|
376
|
+
// Usage is natural
|
|
377
|
+
const user = db.User.new();
|
|
378
|
+
user.tags = ['admin', 'moderator']; // Assign array
|
|
379
|
+
await db.saveChanges(); // Stored as '["admin","moderator"]'
|
|
380
|
+
|
|
381
|
+
const loaded = db.User.findById(user.id);
|
|
382
|
+
console.log(loaded.tags); // ['admin', 'moderator'] - JavaScript array!
|
|
170
383
|
```
|
|
171
384
|
|
|
172
|
-
|
|
173
|
-
- `renameColumn` expects an object: `{ tableName, name, newName }` and works in both SQLite and MySQL.
|
|
174
|
-
- `seed(tableName, rows)` accepts:
|
|
175
|
-
- a single object: `{ col: value, ... }`
|
|
176
|
-
- or an array of objects: `[{...}, {...}]`
|
|
177
|
-
Values are auto-quoted; booleans become 1/0.
|
|
178
|
-
- When a table already exists, `update-database` will sync schema:
|
|
179
|
-
- Add missing columns.
|
|
180
|
-
- MySQL: adjust default/nullability via `ALTER ... MODIFY`.
|
|
181
|
-
- SQLite: rebuilds the table when nullability/default/type changes require it.
|
|
385
|
+
## Querying
|
|
182
386
|
|
|
183
|
-
###
|
|
184
|
-
- Prefer additive changes (add columns) before destructive changes (drops/renames) to minimize downtime.
|
|
185
|
-
- For large SQLite tables, a rebuild copies data; consider maintenance windows.
|
|
186
|
-
- Use `master=development masterrecord get-migrations AppContext` to inspect migration order.
|
|
387
|
+
### Basic Queries
|
|
187
388
|
|
|
188
|
-
|
|
389
|
+
```javascript
|
|
390
|
+
// Find all
|
|
391
|
+
const users = db.User.all();
|
|
189
392
|
|
|
190
|
-
|
|
393
|
+
// Find by primary key
|
|
394
|
+
const user = db.User.findById(123);
|
|
191
395
|
|
|
192
|
-
|
|
396
|
+
// Find single with where clause
|
|
397
|
+
const alice = db.User
|
|
398
|
+
.where(u => u.email == $$, 'alice@example.com')
|
|
399
|
+
.single();
|
|
193
400
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
401
|
+
// Find multiple with conditions
|
|
402
|
+
const adults = db.User
|
|
403
|
+
.where(u => u.age >= $$, 18)
|
|
404
|
+
.toList();
|
|
405
|
+
```
|
|
198
406
|
|
|
199
|
-
###
|
|
407
|
+
### Parameterized Queries
|
|
200
408
|
|
|
201
|
-
|
|
202
|
-
- โ
**Accepts**: integer numbers
|
|
203
|
-
- โ ๏ธ **Auto-converts with warning**:
|
|
204
|
-
- Float โ integer (rounds: `3.7` โ `4`)
|
|
205
|
-
- Valid string โ integer (`"42"` โ `42`)
|
|
206
|
-
- Boolean โ integer (`true` โ `1`, `false` โ `0`)
|
|
207
|
-
- โ **Throws error**: invalid strings (`"abc"`)
|
|
409
|
+
**Always use `$$` placeholders** for SQL injection protection:
|
|
208
410
|
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
411
|
+
```javascript
|
|
412
|
+
// Single parameter
|
|
413
|
+
const user = db.User.where(u => u.id == $$, 123).single();
|
|
414
|
+
|
|
415
|
+
// Multiple parameters
|
|
416
|
+
const results = db.User
|
|
417
|
+
.where(u => u.age > $$ && u.status == $$, 25, 'active')
|
|
418
|
+
.toList();
|
|
419
|
+
|
|
420
|
+
// Single $ for OR conditions
|
|
421
|
+
const results = db.User
|
|
422
|
+
.where(u => u.status == $ || u.status == null, 'active')
|
|
423
|
+
.toList();
|
|
424
|
+
```
|
|
215
425
|
|
|
216
|
-
|
|
217
|
-
- โ
**Accepts**: booleans
|
|
218
|
-
- โ ๏ธ **Auto-converts with warning**:
|
|
219
|
-
- Number โ boolean (`0` โ `false`, others โ `true`)
|
|
220
|
-
- String โ boolean (`"true"/"1"/"yes"` โ `true`, `"false"/"0"/"no"/""`โ `false`)
|
|
221
|
-
- โ **Throws error**: invalid strings, objects
|
|
426
|
+
### IN Clauses
|
|
222
427
|
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
428
|
+
```javascript
|
|
429
|
+
// Array parameter with .includes()
|
|
430
|
+
const ids = [1, 2, 3, 4, 5];
|
|
431
|
+
const users = db.User
|
|
432
|
+
.where(u => $$.includes(u.id), ids)
|
|
433
|
+
.toList();
|
|
434
|
+
|
|
435
|
+
// Generated SQL: WHERE id IN ($1, $2, $3, $4, $5)
|
|
436
|
+
// PostgreSQL parameters: [1, 2, 3, 4, 5]
|
|
437
|
+
|
|
438
|
+
// Alternative .any() syntax
|
|
439
|
+
const users = db.User
|
|
440
|
+
.where(u => u.id.any($$), [1, 2, 3])
|
|
441
|
+
.toList();
|
|
442
|
+
|
|
443
|
+
// Comma-separated strings (auto-splits)
|
|
444
|
+
const users = db.User
|
|
445
|
+
.where(u => u.id.any($$), "1,2,3,4,5")
|
|
446
|
+
.toList();
|
|
447
|
+
```
|
|
226
448
|
|
|
227
|
-
###
|
|
449
|
+
### Query Chaining
|
|
228
450
|
|
|
229
|
-
**Auto-conversion warning (non-breaking):**
|
|
230
451
|
```javascript
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
452
|
+
let query = db.User;
|
|
453
|
+
|
|
454
|
+
// Build query dynamically
|
|
455
|
+
if (searchTerm) {
|
|
456
|
+
query = query.where(u => u.name.like($$), `%${searchTerm}%`);
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
if (minAge) {
|
|
460
|
+
query = query.where(u => u.age >= $$, minAge);
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
// Add sorting and pagination
|
|
464
|
+
const users = query
|
|
465
|
+
.orderBy(u => u.created_at)
|
|
466
|
+
.skip(offset)
|
|
467
|
+
.take(limit)
|
|
468
|
+
.toList();
|
|
235
469
|
```
|
|
236
|
-
|
|
470
|
+
|
|
471
|
+
### Ordering
|
|
472
|
+
|
|
473
|
+
```javascript
|
|
474
|
+
// Ascending
|
|
475
|
+
const users = db.User
|
|
476
|
+
.orderBy(u => u.name)
|
|
477
|
+
.toList();
|
|
478
|
+
|
|
479
|
+
// Descending
|
|
480
|
+
const users = db.User
|
|
481
|
+
.orderByDescending(u => u.created_at)
|
|
482
|
+
.toList();
|
|
237
483
|
```
|
|
238
|
-
|
|
484
|
+
|
|
485
|
+
### Pagination
|
|
486
|
+
|
|
487
|
+
```javascript
|
|
488
|
+
// Skip 20, take 10
|
|
489
|
+
const users = db.User
|
|
490
|
+
.orderBy(u => u.id)
|
|
491
|
+
.skip(20)
|
|
492
|
+
.take(10)
|
|
493
|
+
.toList();
|
|
494
|
+
|
|
495
|
+
// Page-based pagination
|
|
496
|
+
const page = 2;
|
|
497
|
+
const pageSize = 10;
|
|
498
|
+
const users = db.User
|
|
499
|
+
.skip(page * pageSize)
|
|
500
|
+
.take(pageSize)
|
|
501
|
+
.toList();
|
|
239
502
|
```
|
|
240
503
|
|
|
241
|
-
|
|
504
|
+
### Counting
|
|
505
|
+
|
|
242
506
|
```javascript
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
507
|
+
// Count all
|
|
508
|
+
const total = db.User.count();
|
|
509
|
+
|
|
510
|
+
// Count with conditions
|
|
511
|
+
const activeCount = db.User
|
|
512
|
+
.where(u => u.status == $$, 'active')
|
|
513
|
+
.count();
|
|
247
514
|
```
|
|
248
|
-
|
|
515
|
+
|
|
516
|
+
### Complex Queries
|
|
517
|
+
|
|
518
|
+
```javascript
|
|
519
|
+
// Multiple conditions with OR
|
|
520
|
+
const results = db.User
|
|
521
|
+
.where(u => (u.status == 'active' || u.status == 'pending') && u.age >= $$, 18)
|
|
522
|
+
.orderBy(u => u.name)
|
|
523
|
+
.toList();
|
|
524
|
+
|
|
525
|
+
// Nullable checks
|
|
526
|
+
const usersWithoutEmail = db.User
|
|
527
|
+
.where(u => u.email == null)
|
|
528
|
+
.toList();
|
|
529
|
+
|
|
530
|
+
// LIKE queries
|
|
531
|
+
const matching = db.User
|
|
532
|
+
.where(u => u.name.like($$), '%john%')
|
|
533
|
+
.toList();
|
|
249
534
|
```
|
|
250
|
-
|
|
535
|
+
|
|
536
|
+
## Migrations
|
|
537
|
+
|
|
538
|
+
### CLI Commands
|
|
539
|
+
|
|
540
|
+
```bash
|
|
541
|
+
# Enable migrations (one-time per context)
|
|
542
|
+
masterrecord enable-migrations AppContext
|
|
543
|
+
|
|
544
|
+
# Create a migration
|
|
545
|
+
masterrecord add-migration MigrationName AppContext
|
|
546
|
+
|
|
547
|
+
# Apply migrations
|
|
548
|
+
masterrecord migrate AppContext
|
|
549
|
+
|
|
550
|
+
# Apply all migrations from scratch
|
|
551
|
+
masterrecord migrate-restart AppContext
|
|
552
|
+
|
|
553
|
+
# List migrations
|
|
554
|
+
masterrecord get-migrations AppContext
|
|
555
|
+
|
|
556
|
+
# Multi-context commands
|
|
557
|
+
masterrecord enable-migrations-all # Enable for all contexts
|
|
558
|
+
masterrecord add-migration-all Init # Create migration for all
|
|
559
|
+
masterrecord migrate-all # Apply all pending migrations
|
|
251
560
|
```
|
|
252
561
|
|
|
253
|
-
### Migration
|
|
562
|
+
### Migration File Structure
|
|
254
563
|
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
564
|
+
```javascript
|
|
565
|
+
// db/migrations/20250111_143052_CreateUser.js
|
|
566
|
+
module.exports = {
|
|
567
|
+
up: function(table, schema) {
|
|
568
|
+
// Create table
|
|
569
|
+
schema.createTable(table.User);
|
|
570
|
+
|
|
571
|
+
// Seed initial data
|
|
572
|
+
schema.seed('User', {
|
|
573
|
+
name: 'Admin',
|
|
574
|
+
email: 'admin@example.com',
|
|
575
|
+
role: 'admin'
|
|
576
|
+
});
|
|
577
|
+
},
|
|
578
|
+
|
|
579
|
+
down: function(table, schema) {
|
|
580
|
+
// Rollback
|
|
581
|
+
schema.dropTable(table.User);
|
|
582
|
+
}
|
|
583
|
+
};
|
|
584
|
+
```
|
|
259
585
|
|
|
260
|
-
|
|
586
|
+
### Migration Operations
|
|
261
587
|
|
|
262
|
-
|
|
588
|
+
```javascript
|
|
589
|
+
module.exports = {
|
|
590
|
+
up: function(table, schema) {
|
|
591
|
+
// Create table
|
|
592
|
+
schema.createTable(table.User);
|
|
593
|
+
|
|
594
|
+
// Add column
|
|
595
|
+
schema.addColumn({
|
|
596
|
+
tableName: 'User',
|
|
597
|
+
name: 'phone',
|
|
598
|
+
type: 'string'
|
|
599
|
+
});
|
|
600
|
+
|
|
601
|
+
// Alter column
|
|
602
|
+
schema.alterColumn({
|
|
603
|
+
tableName: 'User',
|
|
604
|
+
table: {
|
|
605
|
+
name: 'age',
|
|
606
|
+
type: 'integer',
|
|
607
|
+
nullable: false,
|
|
608
|
+
default: 0
|
|
609
|
+
}
|
|
610
|
+
});
|
|
611
|
+
|
|
612
|
+
// Rename column
|
|
613
|
+
schema.renameColumn({
|
|
614
|
+
tableName: 'User',
|
|
615
|
+
name: 'old_name',
|
|
616
|
+
newName: 'new_name'
|
|
617
|
+
});
|
|
618
|
+
|
|
619
|
+
// Drop column
|
|
620
|
+
schema.dropColumn({
|
|
621
|
+
tableName: 'User',
|
|
622
|
+
name: 'deprecated_field'
|
|
623
|
+
});
|
|
624
|
+
|
|
625
|
+
// Drop table
|
|
626
|
+
schema.dropTable(table.OldTable);
|
|
627
|
+
},
|
|
628
|
+
|
|
629
|
+
down: function(table, schema) {
|
|
630
|
+
// Reverse operations
|
|
631
|
+
}
|
|
632
|
+
};
|
|
633
|
+
```
|
|
263
634
|
|
|
264
|
-
|
|
635
|
+
### Seed Data
|
|
265
636
|
|
|
266
|
-
|
|
637
|
+
```javascript
|
|
638
|
+
module.exports = {
|
|
639
|
+
up: function(table, schema) {
|
|
640
|
+
schema.createTable(table.User);
|
|
641
|
+
|
|
642
|
+
// Single record
|
|
643
|
+
schema.seed('User', {
|
|
644
|
+
name: 'Admin',
|
|
645
|
+
email: 'admin@example.com'
|
|
646
|
+
});
|
|
647
|
+
|
|
648
|
+
// Multiple records (efficient bulk insert)
|
|
649
|
+
schema.bulkSeed('User', [
|
|
650
|
+
{ name: 'Alice', email: 'alice@example.com', age: 25 },
|
|
651
|
+
{ name: 'Bob', email: 'bob@example.com', age: 30 },
|
|
652
|
+
{ name: 'Charlie', email: 'charlie@example.com', age: 35 }
|
|
653
|
+
]);
|
|
654
|
+
},
|
|
655
|
+
|
|
656
|
+
down: function(table, schema) {
|
|
657
|
+
schema.dropTable(table.User);
|
|
658
|
+
}
|
|
659
|
+
};
|
|
660
|
+
```
|
|
267
661
|
|
|
268
|
-
|
|
662
|
+
**Seed data is idempotent** - re-running migrations won't create duplicates:
|
|
663
|
+
- SQLite: `INSERT OR IGNORE`
|
|
664
|
+
- MySQL: `INSERT IGNORE`
|
|
665
|
+
- PostgreSQL: `INSERT ... ON CONFLICT DO NOTHING`
|
|
269
666
|
|
|
270
|
-
|
|
667
|
+
## Advanced Features
|
|
271
668
|
|
|
272
|
-
|
|
669
|
+
### Type Validation
|
|
273
670
|
|
|
274
|
-
|
|
671
|
+
MasterRecord validates and coerces field types at runtime:
|
|
275
672
|
|
|
276
|
-
|
|
673
|
+
```javascript
|
|
674
|
+
const user = db.User.new();
|
|
675
|
+
user.age = "25"; // String assigned to integer field
|
|
676
|
+
await db.saveChanges();
|
|
677
|
+
// โ ๏ธ Console: Auto-converting string "25" to integer 25
|
|
678
|
+
|
|
679
|
+
user.age = "invalid";
|
|
680
|
+
await db.saveChanges();
|
|
681
|
+
// โ Error: Field User.age must be an integer, got string "invalid"
|
|
682
|
+
```
|
|
277
683
|
|
|
278
|
-
|
|
279
|
-
- Scans the project for MasterRecord Context files (heuristic) and enables migrations for each by writing a portable snapshot next to the context at `<ContextDir>/db/migrations/<context>_contextSnapShot.json`.
|
|
684
|
+
### Field Transformers (Advanced)
|
|
280
685
|
|
|
281
|
-
|
|
282
|
-
|
|
686
|
+
```javascript
|
|
687
|
+
class Post {
|
|
688
|
+
constructor() {
|
|
689
|
+
this.id = { type: 'integer', primary: true, auto: true };
|
|
690
|
+
|
|
691
|
+
// Store array as JSON
|
|
692
|
+
this.tags = {
|
|
693
|
+
type: 'string',
|
|
694
|
+
transform: {
|
|
695
|
+
toDatabase: (v) => Array.isArray(v) ? JSON.stringify(v) : v,
|
|
696
|
+
fromDatabase: (v) => v ? JSON.parse(v) : []
|
|
697
|
+
}
|
|
698
|
+
};
|
|
699
|
+
|
|
700
|
+
// PostgreSQL JSONB (native JSON support)
|
|
701
|
+
this.metadata = {
|
|
702
|
+
type: 'jsonb', // PostgreSQL only
|
|
703
|
+
transform: {
|
|
704
|
+
toDatabase: (v) => JSON.stringify(v || {}),
|
|
705
|
+
fromDatabase: (v) => typeof v === 'string' ? JSON.parse(v) : v
|
|
706
|
+
}
|
|
707
|
+
};
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
```
|
|
283
711
|
|
|
284
|
-
|
|
285
|
-
- Applies the latest migration for every detected context with migrations.
|
|
712
|
+
### Table Prefixes
|
|
286
713
|
|
|
287
|
-
|
|
288
|
-
- Runs the latest migrationโs `down()` for the specified context.
|
|
714
|
+
Useful for multi-tenant applications or plugin systems:
|
|
289
715
|
|
|
290
|
-
|
|
291
|
-
|
|
716
|
+
```javascript
|
|
717
|
+
class AppContext extends context {
|
|
718
|
+
constructor() {
|
|
719
|
+
super();
|
|
292
720
|
|
|
293
|
-
|
|
294
|
-
|
|
721
|
+
this.tablePrefix = 'myapp_'; // Set before dbset()
|
|
722
|
+
this.env('config/environments');
|
|
295
723
|
|
|
296
|
-
|
|
724
|
+
this.dbset(User); // Creates table: myapp_User
|
|
725
|
+
this.dbset(Post); // Creates table: myapp_Post
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
```
|
|
297
729
|
|
|
298
|
-
|
|
299
|
-
- `contextLocation`: path from the migrations folder to the Context file
|
|
300
|
-
- `migrationFolder`: `.` (the snapshot resides in the migrations folder)
|
|
301
|
-
- `snapShotLocation`: the snapshot filename
|
|
730
|
+
### Transactions (PostgreSQL)
|
|
302
731
|
|
|
303
|
-
|
|
732
|
+
```javascript
|
|
733
|
+
const { PostgresSyncConnect } = require('masterrecord/postgresSyncConnect');
|
|
304
734
|
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
# macOS/Linux
|
|
308
|
-
master=development masterrecord enable-migrations-all
|
|
735
|
+
const connection = new PostgresSyncConnect();
|
|
736
|
+
await connection.connect(config);
|
|
309
737
|
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
738
|
+
const result = await connection.transaction(async (client) => {
|
|
739
|
+
// Insert user
|
|
740
|
+
const userResult = await client.query(
|
|
741
|
+
'INSERT INTO User (name, email) VALUES ($1, $2) RETURNING id',
|
|
742
|
+
['Alice', 'alice@example.com']
|
|
743
|
+
);
|
|
744
|
+
|
|
745
|
+
// Insert related record
|
|
746
|
+
await client.query(
|
|
747
|
+
'INSERT INTO Profile (user_id, bio) VALUES ($1, $2)',
|
|
748
|
+
[userResult.rows[0].id, 'Software Engineer']
|
|
749
|
+
);
|
|
750
|
+
|
|
751
|
+
return userResult.rows[0].id;
|
|
752
|
+
});
|
|
753
|
+
|
|
754
|
+
// Automatically commits on success, rolls back on error
|
|
755
|
+
```
|
|
756
|
+
|
|
757
|
+
### Multi-Context Applications
|
|
758
|
+
|
|
759
|
+
Manage multiple databases in one application:
|
|
760
|
+
|
|
761
|
+
```javascript
|
|
762
|
+
// contexts/userContext.js
|
|
763
|
+
class UserContext extends context {
|
|
764
|
+
constructor() {
|
|
765
|
+
super();
|
|
766
|
+
this.env({ type: 'postgres', database: 'users_db', ... });
|
|
767
|
+
this.dbset(User);
|
|
768
|
+
this.dbset(Profile);
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
// contexts/analyticsContext.js
|
|
773
|
+
class AnalyticsContext extends context {
|
|
774
|
+
constructor() {
|
|
775
|
+
super();
|
|
776
|
+
this.env({ type: 'postgres', database: 'analytics_db', ... });
|
|
777
|
+
this.dbset(Event);
|
|
778
|
+
this.dbset(Metric);
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
// Usage
|
|
783
|
+
const userDb = new UserContext();
|
|
784
|
+
const analyticsDb = new AnalyticsContext();
|
|
785
|
+
|
|
786
|
+
const user = userDb.User.findById(123);
|
|
787
|
+
analyticsDb.Event.new().log('user_login', user.id);
|
|
788
|
+
await analyticsDb.saveChanges();
|
|
313
789
|
```
|
|
314
790
|
|
|
315
|
-
2) Create an initial migration for all contexts:
|
|
316
791
|
```bash
|
|
317
|
-
#
|
|
318
|
-
|
|
792
|
+
# Migrate all contexts at once
|
|
793
|
+
masterrecord migrate-all
|
|
794
|
+
```
|
|
795
|
+
|
|
796
|
+
### Raw SQL Queries
|
|
797
|
+
|
|
798
|
+
When you need full control:
|
|
799
|
+
|
|
800
|
+
```javascript
|
|
801
|
+
// PostgreSQL parameterized query
|
|
802
|
+
const users = await db._SQLEngine.exec(
|
|
803
|
+
'SELECT * FROM "User" WHERE age > $1 AND status = $2',
|
|
804
|
+
[25, 'active']
|
|
805
|
+
);
|
|
806
|
+
|
|
807
|
+
// MySQL parameterized query
|
|
808
|
+
const users = db._SQLEngine.exec(
|
|
809
|
+
'SELECT * FROM User WHERE age > ? AND status = ?',
|
|
810
|
+
[25, 'active']
|
|
811
|
+
);
|
|
812
|
+
```
|
|
813
|
+
|
|
814
|
+
## API Reference
|
|
815
|
+
|
|
816
|
+
### Context Methods
|
|
817
|
+
|
|
818
|
+
```javascript
|
|
819
|
+
// Entity registration
|
|
820
|
+
context.dbset(EntityClass)
|
|
821
|
+
context.dbset(EntityClass, 'custom_table_name')
|
|
822
|
+
|
|
823
|
+
// Save changes
|
|
824
|
+
await context.saveChanges() // PostgreSQL (async)
|
|
825
|
+
context.saveChanges() // MySQL/SQLite (sync)
|
|
826
|
+
|
|
827
|
+
// Add/Remove entities
|
|
828
|
+
context.EntityName.add(entity)
|
|
829
|
+
context.remove(entity)
|
|
830
|
+
```
|
|
319
831
|
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
832
|
+
### Query Methods
|
|
833
|
+
|
|
834
|
+
```javascript
|
|
835
|
+
// Chainable query builders
|
|
836
|
+
.where(query, ...params) // Add WHERE condition
|
|
837
|
+
.and(query, ...params) // Add AND condition
|
|
838
|
+
.orderBy(field) // Sort ascending
|
|
839
|
+
.orderByDescending(field) // Sort descending
|
|
840
|
+
.skip(number) // Skip N records
|
|
841
|
+
.take(number) // Limit to N records
|
|
842
|
+
.include(relationship) // Eager load
|
|
843
|
+
|
|
844
|
+
// Terminal methods (execute query)
|
|
845
|
+
.toList() // Return array
|
|
846
|
+
.single() // Return one or null
|
|
847
|
+
.first() // Return first or null
|
|
848
|
+
.count() // Return count
|
|
849
|
+
.any() // Return boolean
|
|
850
|
+
.all() // Return all records
|
|
851
|
+
|
|
852
|
+
// Convenience methods
|
|
853
|
+
.findById(id) // Find by primary key
|
|
854
|
+
.new() // Create new entity instance
|
|
323
855
|
```
|
|
324
856
|
|
|
325
|
-
|
|
857
|
+
### Migration Methods
|
|
858
|
+
|
|
859
|
+
```javascript
|
|
860
|
+
// In migration up/down functions
|
|
861
|
+
schema.createTable(table.EntityName)
|
|
862
|
+
schema.dropTable(table.EntityName)
|
|
863
|
+
schema.addColumn({ tableName, name, type })
|
|
864
|
+
schema.dropColumn({ tableName, name })
|
|
865
|
+
schema.alterColumn({ tableName, table: { name, type, nullable, default } })
|
|
866
|
+
schema.renameColumn({ tableName, name, newName })
|
|
867
|
+
schema.seed(tableName, data)
|
|
868
|
+
schema.bulkSeed(tableName, dataArray)
|
|
869
|
+
```
|
|
870
|
+
|
|
871
|
+
## Examples
|
|
872
|
+
|
|
873
|
+
### Complete CRUD Example
|
|
874
|
+
|
|
875
|
+
```javascript
|
|
876
|
+
const AppContext = require('./app/models/context');
|
|
877
|
+
|
|
878
|
+
async function demo() {
|
|
879
|
+
const db = new AppContext();
|
|
880
|
+
|
|
881
|
+
// CREATE
|
|
882
|
+
const user = db.User.new();
|
|
883
|
+
user.name = 'Alice';
|
|
884
|
+
user.email = 'alice@example.com';
|
|
885
|
+
user.age = 28;
|
|
886
|
+
await db.saveChanges();
|
|
887
|
+
console.log('Created user:', user.id);
|
|
888
|
+
|
|
889
|
+
// READ
|
|
890
|
+
const alice = db.User
|
|
891
|
+
.where(u => u.email == $$, 'alice@example.com')
|
|
892
|
+
.single();
|
|
893
|
+
console.log('Found user:', alice.name);
|
|
894
|
+
|
|
895
|
+
// UPDATE
|
|
896
|
+
alice.age = 29;
|
|
897
|
+
await db.saveChanges();
|
|
898
|
+
console.log('Updated age to:', alice.age);
|
|
899
|
+
|
|
900
|
+
// DELETE
|
|
901
|
+
db.remove(alice);
|
|
902
|
+
await db.saveChanges();
|
|
903
|
+
console.log('User deleted');
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
demo();
|
|
907
|
+
```
|
|
908
|
+
|
|
909
|
+
### Pagination Example
|
|
910
|
+
|
|
911
|
+
```javascript
|
|
912
|
+
async function getUsers(page = 0, pageSize = 10) {
|
|
913
|
+
const db = new AppContext();
|
|
914
|
+
|
|
915
|
+
const users = db.User
|
|
916
|
+
.where(u => u.status == $$, 'active')
|
|
917
|
+
.orderBy(u => u.created_at)
|
|
918
|
+
.skip(page * pageSize)
|
|
919
|
+
.take(pageSize)
|
|
920
|
+
.toList();
|
|
921
|
+
|
|
922
|
+
const total = db.User
|
|
923
|
+
.where(u => u.status == $$, 'active')
|
|
924
|
+
.count();
|
|
925
|
+
|
|
926
|
+
return {
|
|
927
|
+
users,
|
|
928
|
+
page,
|
|
929
|
+
pageSize,
|
|
930
|
+
total,
|
|
931
|
+
totalPages: Math.ceil(total / pageSize)
|
|
932
|
+
};
|
|
933
|
+
}
|
|
934
|
+
```
|
|
935
|
+
|
|
936
|
+
### Search with Filters
|
|
937
|
+
|
|
938
|
+
```javascript
|
|
939
|
+
async function searchUsers(filters) {
|
|
940
|
+
const db = new AppContext();
|
|
941
|
+
let query = db.User;
|
|
942
|
+
|
|
943
|
+
// Apply filters dynamically
|
|
944
|
+
if (filters.name) {
|
|
945
|
+
query = query.where(u => u.name.like($$), `%${filters.name}%`);
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
if (filters.minAge) {
|
|
949
|
+
query = query.where(u => u.age >= $$, filters.minAge);
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
if (filters.status) {
|
|
953
|
+
query = query.where(u => u.status == $$, filters.status);
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
// Add sorting
|
|
957
|
+
const sortField = filters.sortBy || 'created_at';
|
|
958
|
+
const sortOrder = filters.sortOrder || 'desc';
|
|
959
|
+
|
|
960
|
+
if (sortOrder === 'asc') {
|
|
961
|
+
query = query.orderBy(sortField);
|
|
962
|
+
} else {
|
|
963
|
+
query = query.orderByDescending(sortField);
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
// Add pagination
|
|
967
|
+
if (filters.page && filters.pageSize) {
|
|
968
|
+
query = query
|
|
969
|
+
.skip(filters.page * filters.pageSize)
|
|
970
|
+
.take(filters.pageSize);
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
return query.toList();
|
|
974
|
+
}
|
|
975
|
+
```
|
|
976
|
+
|
|
977
|
+
### Relationship Example
|
|
978
|
+
|
|
979
|
+
```javascript
|
|
980
|
+
class BlogContext extends context {
|
|
981
|
+
constructor() {
|
|
982
|
+
super();
|
|
983
|
+
this.env('config/environments');
|
|
984
|
+
this.dbset(Author);
|
|
985
|
+
this.dbset(Post);
|
|
986
|
+
}
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
// Create author with posts
|
|
990
|
+
const db = new BlogContext();
|
|
991
|
+
|
|
992
|
+
const author = db.Author.new();
|
|
993
|
+
author.name = 'John Doe';
|
|
994
|
+
await db.saveChanges();
|
|
995
|
+
|
|
996
|
+
const post = db.Post.new();
|
|
997
|
+
post.title = 'My First Post';
|
|
998
|
+
post.content = 'Hello World!';
|
|
999
|
+
post.author_id = author.id;
|
|
1000
|
+
await db.saveChanges();
|
|
1001
|
+
|
|
1002
|
+
// Query with relationships
|
|
1003
|
+
const posts = db.Post
|
|
1004
|
+
.where(p => p.author_id == $$, author.id)
|
|
1005
|
+
.toList();
|
|
1006
|
+
|
|
1007
|
+
console.log(`${author.name} has ${posts.length} posts`);
|
|
1008
|
+
```
|
|
1009
|
+
|
|
1010
|
+
## Performance Tips
|
|
1011
|
+
|
|
1012
|
+
### 1. Use Bulk Operations
|
|
1013
|
+
|
|
1014
|
+
```javascript
|
|
1015
|
+
// โ BAD: Multiple inserts
|
|
1016
|
+
for (const item of items) {
|
|
1017
|
+
const entity = db.Entity.new();
|
|
1018
|
+
entity.data = item;
|
|
1019
|
+
await db.saveChanges();
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
// โ
GOOD: Single bulk insert
|
|
1023
|
+
for (const item of items) {
|
|
1024
|
+
const entity = db.Entity.new();
|
|
1025
|
+
entity.data = item;
|
|
1026
|
+
}
|
|
1027
|
+
await db.saveChanges(); // Batch insert
|
|
1028
|
+
```
|
|
1029
|
+
|
|
1030
|
+
### 2. Use Indexes
|
|
1031
|
+
|
|
1032
|
+
```javascript
|
|
1033
|
+
class User {
|
|
1034
|
+
constructor() {
|
|
1035
|
+
this.email = {
|
|
1036
|
+
type: 'string',
|
|
1037
|
+
unique: true // Automatically creates index
|
|
1038
|
+
};
|
|
1039
|
+
}
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1042
|
+
// For complex queries, add database indexes manually
|
|
1043
|
+
// CREATE INDEX idx_user_status ON User(status);
|
|
1044
|
+
```
|
|
1045
|
+
|
|
1046
|
+
### 3. Limit Result Sets
|
|
1047
|
+
|
|
1048
|
+
```javascript
|
|
1049
|
+
// โ
GOOD: Limit results
|
|
1050
|
+
const recentUsers = db.User
|
|
1051
|
+
.orderByDescending(u => u.created_at)
|
|
1052
|
+
.take(100)
|
|
1053
|
+
.toList();
|
|
1054
|
+
|
|
1055
|
+
// โ BAD: Load everything
|
|
1056
|
+
const allUsers = db.User.all();
|
|
1057
|
+
```
|
|
1058
|
+
|
|
1059
|
+
### 4. Use Connection Pooling (PostgreSQL)
|
|
1060
|
+
|
|
1061
|
+
```javascript
|
|
1062
|
+
this.env({
|
|
1063
|
+
type: 'postgres',
|
|
1064
|
+
max: 20, // Pool size
|
|
1065
|
+
idleTimeoutMillis: 30000,
|
|
1066
|
+
connectionTimeoutMillis: 2000
|
|
1067
|
+
});
|
|
1068
|
+
```
|
|
1069
|
+
|
|
1070
|
+
## Security
|
|
1071
|
+
|
|
1072
|
+
### SQL Injection Protection
|
|
1073
|
+
|
|
1074
|
+
MasterRecord uses **parameterized queries throughout** to prevent SQL injection:
|
|
1075
|
+
|
|
1076
|
+
```javascript
|
|
1077
|
+
// โ
SAFE: Parameterized
|
|
1078
|
+
const user = db.User.where(u => u.name == $$, userInput).single();
|
|
1079
|
+
|
|
1080
|
+
// โ UNSAFE: Never do this
|
|
1081
|
+
// const query = `SELECT * FROM User WHERE name = '${userInput}'`;
|
|
1082
|
+
```
|
|
1083
|
+
|
|
1084
|
+
All operations use parameterized queries:
|
|
1085
|
+
- SELECT queries
|
|
1086
|
+
- INSERT operations
|
|
1087
|
+
- UPDATE operations
|
|
1088
|
+
- DELETE operations
|
|
1089
|
+
- IN clauses
|
|
1090
|
+
- LIKE patterns
|
|
1091
|
+
|
|
1092
|
+
### Input Validation
|
|
1093
|
+
|
|
1094
|
+
While SQL injection is prevented, always validate business logic:
|
|
1095
|
+
|
|
1096
|
+
```javascript
|
|
1097
|
+
// Validate input before querying
|
|
1098
|
+
function getUser(userId) {
|
|
1099
|
+
if (!Number.isInteger(userId) || userId <= 0) {
|
|
1100
|
+
throw new Error('Invalid user ID');
|
|
1101
|
+
}
|
|
1102
|
+
|
|
1103
|
+
return db.User.findById(userId);
|
|
1104
|
+
}
|
|
1105
|
+
```
|
|
1106
|
+
|
|
1107
|
+
## Troubleshooting
|
|
1108
|
+
|
|
1109
|
+
### PostgreSQL Connection Issues
|
|
1110
|
+
|
|
326
1111
|
```bash
|
|
327
|
-
#
|
|
328
|
-
|
|
1112
|
+
# Error: Cannot find module 'pg'
|
|
1113
|
+
npm install pg@^8.16.3
|
|
1114
|
+
|
|
1115
|
+
# Error: Connection refused
|
|
1116
|
+
# Check PostgreSQL is running: sudo service postgresql status
|
|
329
1117
|
|
|
330
|
-
#
|
|
331
|
-
|
|
332
|
-
|
|
1118
|
+
# Error: Database does not exist
|
|
1119
|
+
createdb myapp
|
|
1120
|
+
|
|
1121
|
+
# Error: Authentication failed
|
|
1122
|
+
# Check pg_hba.conf and user permissions
|
|
333
1123
|
```
|
|
334
1124
|
|
|
335
|
-
|
|
1125
|
+
### MySQL Connection Issues
|
|
1126
|
+
|
|
336
1127
|
```bash
|
|
337
|
-
#
|
|
338
|
-
|
|
1128
|
+
# Error: ER_NOT_SUPPORTED_AUTH_MODE
|
|
1129
|
+
# Use mysql_native_password for MySQL 8.0+
|
|
1130
|
+
ALTER USER 'root'@'localhost' IDENTIFIED WITH mysql_native_password BY 'password';
|
|
339
1131
|
|
|
340
|
-
#
|
|
341
|
-
|
|
342
|
-
|
|
1132
|
+
# Error: ER_ACCESS_DENIED_ERROR
|
|
1133
|
+
# Check user permissions
|
|
1134
|
+
GRANT ALL PRIVILEGES ON myapp.* TO 'user'@'localhost';
|
|
343
1135
|
```
|
|
344
1136
|
|
|
345
|
-
|
|
1137
|
+
### Migration Issues
|
|
1138
|
+
|
|
346
1139
|
```bash
|
|
347
|
-
#
|
|
348
|
-
|
|
1140
|
+
# Cannot find context file
|
|
1141
|
+
# Ensure you're running from project root
|
|
1142
|
+
cd /path/to/project
|
|
1143
|
+
masterrecord migrate AppContext
|
|
1144
|
+
|
|
1145
|
+
# No migrations found
|
|
1146
|
+
# Check migrations directory exists
|
|
1147
|
+
ls app/models/db/migrations/
|
|
349
1148
|
|
|
350
|
-
#
|
|
351
|
-
|
|
352
|
-
masterrecord update-database-down userContext
|
|
1149
|
+
# Type errors in migration
|
|
1150
|
+
# Check entity definitions match database types
|
|
353
1151
|
```
|
|
354
1152
|
|
|
355
|
-
###
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
1153
|
+
### Common Errors
|
|
1154
|
+
|
|
1155
|
+
```javascript
|
|
1156
|
+
// Error: "expected N value(s) for '$', but received M"
|
|
1157
|
+
// Solution: Match placeholder count with parameters
|
|
1158
|
+
db.User.where(u => u.age > $$ && u.status == $$, 25, 'active');
|
|
1159
|
+
// Two $$ placeholders โ โ Two parameters
|
|
1160
|
+
|
|
1161
|
+
// Error: "Cannot create IN clause with empty array"
|
|
1162
|
+
// Solution: Check array has values before querying
|
|
1163
|
+
const ids = [1, 2, 3];
|
|
1164
|
+
if (ids.length > 0) {
|
|
1165
|
+
db.User.where(u => $$.includes(u.id), ids).toList();
|
|
1166
|
+
}
|
|
1167
|
+
|
|
1168
|
+
// Error: "Field X cannot be null"
|
|
1169
|
+
// Solution: Entity defines field as non-nullable
|
|
1170
|
+
user.name = null; // Error if name is { nullable: false }
|
|
361
1171
|
```
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
1172
|
+
|
|
1173
|
+
## Version Compatibility
|
|
1174
|
+
|
|
1175
|
+
| Component | Version | Notes |
|
|
1176
|
+
|---------------|---------------|------------------------------------------|
|
|
1177
|
+
| MasterRecord | 0.3.0+ | Current version with PostgreSQL support |
|
|
1178
|
+
| Node.js | 14+ | Async/await support required |
|
|
1179
|
+
| PostgreSQL | 9.6+ (12+) | Tested with 12, 13, 14, 15, 16 |
|
|
1180
|
+
| MySQL | 5.7+ (8.0+) | Tested with 8.0+ |
|
|
1181
|
+
| SQLite | 3.x | Any recent version |
|
|
1182
|
+
| pg | 8.16.3+ | PostgreSQL driver |
|
|
1183
|
+
| sync-mysql2 | 1.0.8+ | MySQL driver |
|
|
1184
|
+
| better-sqlite3| 12.6.0+ | SQLite driver |
|
|
1185
|
+
|
|
1186
|
+
## Documentation
|
|
1187
|
+
|
|
1188
|
+
- [PostgreSQL Setup Guide](./docs/POSTGRESQL_SETUP.md) - Complete PostgreSQL configuration
|
|
1189
|
+
- [Migrations Guide](./docs/MIGRATIONS_GUIDE.md) - Detailed migration tutorial
|
|
1190
|
+
- [Methods Reference](./docs/METHODS_REFERENCE.md) - Complete API reference
|
|
1191
|
+
- [Field Transformers](./docs/FIELD_TRANSFORMERS.md) - Custom type handling
|
|
1192
|
+
|
|
1193
|
+
## Contributing
|
|
1194
|
+
|
|
1195
|
+
Contributions are welcome! Please:
|
|
1196
|
+
|
|
1197
|
+
1. Fork the repository
|
|
1198
|
+
2. Create a feature branch
|
|
1199
|
+
3. Make your changes with tests
|
|
1200
|
+
4. Submit a pull request
|
|
1201
|
+
|
|
1202
|
+
### Running Tests
|
|
1203
|
+
|
|
1204
|
+
```bash
|
|
1205
|
+
# PostgreSQL engine tests
|
|
1206
|
+
node test/postgresEngineTest.js
|
|
1207
|
+
|
|
1208
|
+
# Integration tests (requires database)
|
|
1209
|
+
node test/postgresIntegrationTest.js
|
|
1210
|
+
|
|
1211
|
+
# All tests
|
|
1212
|
+
npm test
|
|
365
1213
|
```
|
|
366
1214
|
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
1215
|
+
## License
|
|
1216
|
+
|
|
1217
|
+
MIT License - see [LICENSE](LICENSE) file for details.
|
|
1218
|
+
|
|
1219
|
+
## Credits
|
|
1220
|
+
|
|
1221
|
+
Created by Alexander Rich
|
|
1222
|
+
|
|
1223
|
+
## Support
|
|
1224
|
+
|
|
1225
|
+
- GitHub Issues: [Report bugs or request features](https://github.com/Tailor/MasterRecord/issues)
|
|
1226
|
+
- npm: [masterrecord](https://www.npmjs.com/package/masterrecord)
|
|
373
1227
|
|
|
1228
|
+
---
|
|
374
1229
|
|
|
1230
|
+
**MasterRecord** - Code-first ORM for Node.js with multi-database support
|