masterrecord 0.2.36 → 0.3.1
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 +20 -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 +13 -4
- package/SQLLiteEngine.js +331 -20
- package/context.js +91 -14
- 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 +273 -17
- package/package.json +3 -3
- package/postgresEngine.js +600 -483
- package/postgresSyncConnect.js +209 -0
- package/readme.md +1046 -416
- 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/transformerTest.js +287 -0
- package/test/verifyFindById.js +169 -0
- package/test/verifyNewMethod.js +191 -0
package/readme.md
CHANGED
|
@@ -1,600 +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
|
|
19
|
+
|
|
20
|
+
## Database Support
|
|
4
21
|
|
|
5
|
-
|
|
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 |
|
|
6
27
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
-
|
|
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
|
|
27
65
|
|
|
28
|
-
```
|
|
29
|
-
|
|
30
|
-
|
|
66
|
+
```javascript
|
|
67
|
+
// app/models/context.js
|
|
68
|
+
const context = require('masterrecord/context');
|
|
69
|
+
const User = require('./User');
|
|
70
|
+
const Post = require('./Post');
|
|
31
71
|
|
|
32
|
-
|
|
72
|
+
class AppContext extends context {
|
|
73
|
+
constructor() {
|
|
74
|
+
super();
|
|
33
75
|
|
|
34
|
-
|
|
35
|
-
|
|
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;
|
|
36
93
|
```
|
|
37
94
|
|
|
38
|
-
|
|
95
|
+
### 2. Define Entities
|
|
39
96
|
|
|
40
|
-
```
|
|
41
|
-
|
|
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;
|
|
42
110
|
```
|
|
43
111
|
|
|
44
|
-
###
|
|
112
|
+
### 3. Run Migrations
|
|
45
113
|
|
|
46
|
-
- Run from the project root where your Context file lives. Use the Context file name (without extension) as the argument.
|
|
47
114
|
```bash
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
This creates `db/migrations/<context>_contextSnapShot.json` and the `db/migrations` directory.
|
|
115
|
+
# Enable migrations (one-time setup)
|
|
116
|
+
masterrecord enable-migrations AppContext
|
|
51
117
|
|
|
52
|
-
|
|
118
|
+
# Create initial migration
|
|
119
|
+
masterrecord add-migration InitialCreate AppContext
|
|
53
120
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
master=development masterrecord add-migration <MigrationName> AppContext
|
|
121
|
+
# Apply migrations
|
|
122
|
+
masterrecord migrate AppContext
|
|
57
123
|
```
|
|
58
|
-
This writes a new file to `db/migrations/<timestamp>_<MigrationName>_migration.js`.
|
|
59
124
|
|
|
60
|
-
###
|
|
125
|
+
### 4. Query Your Data
|
|
61
126
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
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();
|
|
73
150
|
```
|
|
74
151
|
|
|
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.
|
|
152
|
+
## Database Configuration
|
|
78
153
|
|
|
79
|
-
###
|
|
154
|
+
### PostgreSQL (Async)
|
|
80
155
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
- **Type validation and coercion (Entity Framework-style)**:
|
|
102
|
-
- INSERT and UPDATE operations now validate field types against entity definitions.
|
|
103
|
-
- Auto-converts compatible types with warnings (e.g., string "4" → integer 4).
|
|
104
|
-
- Throws clear errors for incompatible types with detailed context.
|
|
105
|
-
- Prevents silent failures where fields were skipped due to type mismatches.
|
|
106
|
-
- See [Type Validation](#type-validation) section below for details.
|
|
107
|
-
- Query language and SQL engines:
|
|
108
|
-
- Correct parsing of multi-char operators (>=, <=, ===, !==) and spaced logical operators.
|
|
109
|
-
- Support for grouped OR conditions rendered as parenthesized OR in WHERE across SQLite/MySQL.
|
|
110
|
-
- Resilient fallback for partially parsed expressions.
|
|
111
|
-
- Relationships:
|
|
112
|
-
- `hasManyThrough` supported in insert and delete cascades.
|
|
113
|
-
- Environment file discovery:
|
|
114
|
-
- Context now walks up directories to find `config/environments/env.<env>.json`; fixed error throwing.
|
|
115
|
-
- Migrations (DDL generation):
|
|
116
|
-
- Default values emitted for SQLite/MySQL (including boolean coercion).
|
|
117
|
-
- `CREATE TABLE IF NOT EXISTS` to avoid failures when rerunning.
|
|
118
|
-
- Table introspection added; existing tables are synced: missing columns are added, MySQL applies `ALTER ... MODIFY` for NULL/DEFAULT changes, SQLite rebuilds table when necessary.
|
|
119
|
-
- Migration API additions in `schema.js`:
|
|
120
|
-
- `renameColumn(table)` implemented for SQLite/MySQL.
|
|
121
|
-
- `seed(tableName, rows)` implemented for bulk/single inserts with safe quoting.
|
|
122
|
-
|
|
123
|
-
### Using renameColumn and seed in migrations
|
|
124
|
-
|
|
125
|
-
Basic migration skeleton (generated by CLI):
|
|
126
|
-
```js
|
|
127
|
-
var masterrecord = require('masterrecord');
|
|
128
|
-
|
|
129
|
-
class AddSettings extends masterrecord.schema {
|
|
130
|
-
constructor(context){ super(context); }
|
|
131
|
-
|
|
132
|
-
up(table){
|
|
133
|
-
this.init(table);
|
|
134
|
-
// Add a new table
|
|
135
|
-
this.createTable(table.MailSettings);
|
|
136
|
-
|
|
137
|
-
// Rename a column on an existing table
|
|
138
|
-
this.renameColumn({ tableName: 'MailSettings', name: 'from_email', newName: 'reply_to' });
|
|
139
|
-
|
|
140
|
-
// Seed initial data (single row)
|
|
141
|
-
this.seed('MailSettings', {
|
|
142
|
-
from_name: 'System',
|
|
143
|
-
reply_to: 'no-reply@example.com',
|
|
144
|
-
return_path_matches_from: 0,
|
|
145
|
-
weekly_summary_enabled: 0,
|
|
146
|
-
created_at: Date.now(),
|
|
147
|
-
updated_at: Date.now()
|
|
148
|
-
});
|
|
149
|
-
|
|
150
|
-
// Seed multiple rows
|
|
151
|
-
this.seed('MailSettings', [
|
|
152
|
-
{ from_name: 'Support', reply_to: 'support@example.com', created_at: Date.now(), updated_at: Date.now() },
|
|
153
|
-
{ from_name: 'Marketing', reply_to: 'marketing@example.com', created_at: Date.now(), updated_at: Date.now() }
|
|
154
|
-
]);
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
down(table){
|
|
158
|
-
this.init(table);
|
|
159
|
-
// Revert the rename
|
|
160
|
-
this.renameColumn({ tableName: 'MailSettings', name: 'reply_to', newName: 'from_email' });
|
|
161
|
-
|
|
162
|
-
// Optionally clean up seeded rows
|
|
163
|
-
// this.context._execute("DELETE FROM MailSettings WHERE reply_to IN ('no-reply@example.com','support@example.com','marketing@example.com')");
|
|
164
|
-
|
|
165
|
-
// Drop table if that was part of up
|
|
166
|
-
// this.dropTable(table.MailSettings);
|
|
167
|
-
}
|
|
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
|
+
}
|
|
168
175
|
}
|
|
169
|
-
|
|
176
|
+
|
|
177
|
+
// Usage requires await
|
|
178
|
+
const db = new AppContext();
|
|
179
|
+
await db.saveChanges(); // PostgreSQL is async
|
|
170
180
|
```
|
|
171
181
|
|
|
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.
|
|
182
|
+
### MySQL (Synchronous)
|
|
182
183
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
184
|
+
```javascript
|
|
185
|
+
class AppContext extends context {
|
|
186
|
+
constructor() {
|
|
187
|
+
super();
|
|
187
188
|
|
|
188
|
-
|
|
189
|
+
this.env({
|
|
190
|
+
type: 'mysql',
|
|
191
|
+
host: 'localhost',
|
|
192
|
+
port: 3306,
|
|
193
|
+
database: 'myapp',
|
|
194
|
+
user: 'root',
|
|
195
|
+
password: 'password'
|
|
196
|
+
});
|
|
189
197
|
|
|
190
|
-
|
|
198
|
+
this.dbset(User);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
191
201
|
|
|
192
|
-
|
|
202
|
+
// Usage is synchronous
|
|
203
|
+
const db = new AppContext();
|
|
204
|
+
db.saveChanges(); // No await needed
|
|
205
|
+
```
|
|
193
206
|
|
|
194
|
-
|
|
195
|
-
1. **Validates** the value against the field's type definition
|
|
196
|
-
2. **Auto-converts** compatible types with console warnings
|
|
197
|
-
3. **Throws clear errors** for incompatible types
|
|
207
|
+
### SQLite (Synchronous)
|
|
198
208
|
|
|
199
|
-
|
|
209
|
+
```javascript
|
|
210
|
+
class AppContext extends context {
|
|
211
|
+
constructor() {
|
|
212
|
+
super();
|
|
200
213
|
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
- Valid string → integer (`"42"` → `42`)
|
|
206
|
-
- Boolean → integer (`true` → `1`, `false` → `0`)
|
|
207
|
-
- ❌ **Throws error**: invalid strings (`"abc"`)
|
|
214
|
+
this.env({
|
|
215
|
+
type: 'sqlite',
|
|
216
|
+
connection: './data/myapp.db' // File path
|
|
217
|
+
});
|
|
208
218
|
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
- Boolean → string (`true` → `"true"`)
|
|
214
|
-
- ❌ **Throws error**: objects, arrays
|
|
219
|
+
this.dbset(User);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
```
|
|
215
223
|
|
|
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
|
|
224
|
+
### Environment Files
|
|
222
225
|
|
|
223
|
-
|
|
224
|
-
- ✅ **Accepts**: strings or numbers
|
|
225
|
-
- ❌ **Throws error**: objects, booleans
|
|
226
|
+
Store configurations in JSON files:
|
|
226
227
|
|
|
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
|
+
}
|
|
238
|
+
```
|
|
228
239
|
|
|
229
|
-
**Auto-conversion warning (non-breaking):**
|
|
230
240
|
```javascript
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
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
|
+
}
|
|
237
249
|
```
|
|
238
|
-
|
|
250
|
+
|
|
251
|
+
```bash
|
|
252
|
+
# Set environment
|
|
253
|
+
export NODE_ENV=development
|
|
254
|
+
node app.js
|
|
239
255
|
```
|
|
240
256
|
|
|
241
|
-
|
|
257
|
+
## Entity Definitions
|
|
258
|
+
|
|
259
|
+
### Basic Entity
|
|
260
|
+
|
|
242
261
|
```javascript
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
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
|
+
}
|
|
247
297
|
```
|
|
248
|
-
|
|
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
|
+
}
|
|
249
350
|
```
|
|
250
|
-
|
|
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
|
+
}
|
|
374
|
+
}
|
|
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!
|
|
251
383
|
```
|
|
252
384
|
|
|
253
|
-
|
|
385
|
+
## Querying
|
|
254
386
|
|
|
255
|
-
|
|
256
|
-
- **No breaking changes**: Compatible types are still auto-converted
|
|
257
|
-
- **New warnings**: You'll see console warnings for auto-conversions
|
|
258
|
-
- **New errors**: Incompatible types that were silently skipped now throw errors
|
|
387
|
+
### Basic Queries
|
|
259
388
|
|
|
260
|
-
|
|
389
|
+
```javascript
|
|
390
|
+
// Find all
|
|
391
|
+
const users = db.User.all();
|
|
261
392
|
|
|
262
|
-
|
|
393
|
+
// Find by primary key
|
|
394
|
+
const user = db.User.findById(123);
|
|
263
395
|
|
|
264
|
-
|
|
396
|
+
// Find single with where clause
|
|
397
|
+
const alice = db.User
|
|
398
|
+
.where(u => u.email == $$, 'alice@example.com')
|
|
399
|
+
.single();
|
|
265
400
|
|
|
266
|
-
|
|
401
|
+
// Find multiple with conditions
|
|
402
|
+
const adults = db.User
|
|
403
|
+
.where(u => u.age >= $$, 18)
|
|
404
|
+
.toList();
|
|
405
|
+
```
|
|
267
406
|
|
|
268
|
-
|
|
407
|
+
### Parameterized Queries
|
|
269
408
|
|
|
270
|
-
|
|
409
|
+
**Always use `$$` placeholders** for SQL injection protection:
|
|
271
410
|
|
|
272
|
-
|
|
411
|
+
```javascript
|
|
412
|
+
// Single parameter
|
|
413
|
+
const user = db.User.where(u => u.id == $$, 123).single();
|
|
273
414
|
|
|
274
|
-
|
|
415
|
+
// Multiple parameters
|
|
416
|
+
const results = db.User
|
|
417
|
+
.where(u => u.age > $$ && u.status == $$, 25, 'active')
|
|
418
|
+
.toList();
|
|
275
419
|
|
|
276
|
-
|
|
420
|
+
// Single $ for OR conditions
|
|
421
|
+
const results = db.User
|
|
422
|
+
.where(u => u.status == $ || u.status == null, 'active')
|
|
423
|
+
.toList();
|
|
424
|
+
```
|
|
277
425
|
|
|
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`.
|
|
426
|
+
### IN Clauses
|
|
280
427
|
|
|
281
|
-
|
|
282
|
-
|
|
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();
|
|
283
434
|
|
|
284
|
-
|
|
285
|
-
|
|
435
|
+
// Generated SQL: WHERE id IN ($1, $2, $3, $4, $5)
|
|
436
|
+
// PostgreSQL parameters: [1, 2, 3, 4, 5]
|
|
286
437
|
|
|
287
|
-
|
|
288
|
-
|
|
438
|
+
// Alternative .any() syntax
|
|
439
|
+
const users = db.User
|
|
440
|
+
.where(u => u.id.any($$), [1, 2, 3])
|
|
441
|
+
.toList();
|
|
289
442
|
|
|
290
|
-
|
|
291
|
-
|
|
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
|
+
```
|
|
292
448
|
|
|
293
|
-
|
|
294
|
-
- For MySQL contexts, ensures the database exists (like EF’s `Database.EnsureCreated`). Auto-detects connection info from your Context env settings.
|
|
449
|
+
### Query Chaining
|
|
295
450
|
|
|
296
|
-
|
|
451
|
+
```javascript
|
|
452
|
+
let query = db.User;
|
|
297
453
|
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
454
|
+
// Build query dynamically
|
|
455
|
+
if (searchTerm) {
|
|
456
|
+
query = query.where(u => u.name.like($$), `%${searchTerm}%`);
|
|
457
|
+
}
|
|
302
458
|
|
|
303
|
-
|
|
459
|
+
if (minAge) {
|
|
460
|
+
query = query.where(u => u.age >= $$, minAge);
|
|
461
|
+
}
|
|
304
462
|
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
463
|
+
// Add sorting and pagination
|
|
464
|
+
const users = query
|
|
465
|
+
.orderBy(u => u.created_at)
|
|
466
|
+
.skip(offset)
|
|
467
|
+
.take(limit)
|
|
468
|
+
.toList();
|
|
469
|
+
```
|
|
470
|
+
|
|
471
|
+
### Ordering
|
|
472
|
+
|
|
473
|
+
```javascript
|
|
474
|
+
// Ascending
|
|
475
|
+
const users = db.User
|
|
476
|
+
.orderBy(u => u.name)
|
|
477
|
+
.toList();
|
|
309
478
|
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
479
|
+
// Descending
|
|
480
|
+
const users = db.User
|
|
481
|
+
.orderByDescending(u => u.created_at)
|
|
482
|
+
.toList();
|
|
313
483
|
```
|
|
314
484
|
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
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();
|
|
319
494
|
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
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();
|
|
323
502
|
```
|
|
324
503
|
|
|
325
|
-
|
|
326
|
-
```bash
|
|
327
|
-
# macOS/Linux
|
|
328
|
-
master=development masterrecord update-database-all
|
|
504
|
+
### Counting
|
|
329
505
|
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
506
|
+
```javascript
|
|
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();
|
|
333
514
|
```
|
|
334
515
|
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
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();
|
|
339
524
|
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
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();
|
|
343
534
|
```
|
|
344
535
|
|
|
345
|
-
|
|
536
|
+
## Migrations
|
|
537
|
+
|
|
538
|
+
### CLI Commands
|
|
539
|
+
|
|
346
540
|
```bash
|
|
347
|
-
#
|
|
348
|
-
|
|
541
|
+
# Enable migrations (one-time per context)
|
|
542
|
+
masterrecord enable-migrations AppContext
|
|
349
543
|
|
|
350
|
-
#
|
|
351
|
-
|
|
352
|
-
masterrecord update-database-down userContext
|
|
353
|
-
```
|
|
544
|
+
# Create a migration
|
|
545
|
+
masterrecord add-migration MigrationName AppContext
|
|
354
546
|
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
-
|
|
363
|
-
|
|
364
|
-
|
|
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
|
|
365
560
|
```
|
|
366
561
|
|
|
367
|
-
###
|
|
368
|
-
- Each Context should define its own env settings and tables; `update-database-all` operates context-by-context so separate databases are handled cleanly.
|
|
369
|
-
- For SQLite contexts, the `connection` path will be created if the directory does not exist.
|
|
370
|
-
- For MySQL contexts, `ensure-database <ContextName>` can create the DB (permissions required) before migrations run.
|
|
371
|
-
- If you rename/move the project root, re-run `enable-migrations-all` or any single-context command once; snapshots use relative paths and will continue working.
|
|
372
|
-
- If `update-database-all` reports "no migration files found" for a context, run `get-migrations <ContextName>`. If empty, create a migration with `add-migration <Name> <ContextName>` or use `add-migration-all <Name>`.
|
|
562
|
+
### Migration File Structure
|
|
373
563
|
|
|
374
|
-
|
|
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
|
+
```
|
|
375
585
|
|
|
376
|
-
|
|
377
|
-
- Multi-tenant applications sharing a single database
|
|
378
|
-
- Plugin systems where each plugin needs isolated tables
|
|
379
|
-
- Avoiding table name conflicts in shared database environments
|
|
586
|
+
### Migration Operations
|
|
380
587
|
|
|
381
|
-
|
|
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
|
+
```
|
|
382
634
|
|
|
383
|
-
|
|
635
|
+
### Seed Data
|
|
384
636
|
|
|
385
637
|
```javascript
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
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
|
+
```
|
|
389
661
|
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
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`
|
|
393
666
|
|
|
394
|
-
|
|
395
|
-
this.tablePrefix = 'myapp_';
|
|
667
|
+
## Advanced Features
|
|
396
668
|
|
|
397
|
-
|
|
398
|
-
this.env('config/environments');
|
|
669
|
+
### Type Validation
|
|
399
670
|
|
|
400
|
-
|
|
401
|
-
this.dbset(User); // Creates table: myapp_User
|
|
402
|
-
this.dbset(Post); // Creates table: myapp_Post
|
|
403
|
-
}
|
|
404
|
-
}
|
|
671
|
+
MasterRecord validates and coerces field types at runtime:
|
|
405
672
|
|
|
406
|
-
|
|
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"
|
|
407
682
|
```
|
|
408
683
|
|
|
409
|
-
###
|
|
684
|
+
### Field Transformers (Advanced)
|
|
410
685
|
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
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
|
+
```
|
|
711
|
+
|
|
712
|
+
### Table Prefixes
|
|
416
713
|
|
|
417
|
-
|
|
714
|
+
Useful for multi-tenant applications or plugin systems:
|
|
418
715
|
|
|
419
716
|
```javascript
|
|
420
|
-
class AppContext extends
|
|
717
|
+
class AppContext extends context {
|
|
421
718
|
constructor() {
|
|
422
719
|
super();
|
|
423
|
-
|
|
720
|
+
|
|
721
|
+
this.tablePrefix = 'myapp_'; // Set before dbset()
|
|
424
722
|
this.env('config/environments');
|
|
425
723
|
|
|
426
|
-
//
|
|
427
|
-
this.dbset(
|
|
428
|
-
this.dbset(Post, 'blog_posts'); // Creates table: myapp_blog_posts
|
|
724
|
+
this.dbset(User); // Creates table: myapp_User
|
|
725
|
+
this.dbset(Post); // Creates table: myapp_Post
|
|
429
726
|
}
|
|
430
727
|
}
|
|
431
728
|
```
|
|
432
729
|
|
|
433
|
-
###
|
|
730
|
+
### Transactions (PostgreSQL)
|
|
731
|
+
|
|
732
|
+
```javascript
|
|
733
|
+
const { PostgresSyncConnect } = require('masterrecord/postgresSyncConnect');
|
|
734
|
+
|
|
735
|
+
const connection = new PostgresSyncConnect();
|
|
736
|
+
await connection.connect(config);
|
|
737
|
+
|
|
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
|
|
434
758
|
|
|
435
|
-
|
|
759
|
+
Manage multiple databases in one application:
|
|
436
760
|
|
|
437
761
|
```javascript
|
|
438
|
-
//
|
|
439
|
-
class
|
|
762
|
+
// contexts/userContext.js
|
|
763
|
+
class UserContext extends context {
|
|
440
764
|
constructor() {
|
|
441
765
|
super();
|
|
766
|
+
this.env({ type: 'postgres', database: 'users_db', ... });
|
|
767
|
+
this.dbset(User);
|
|
768
|
+
this.dbset(Profile);
|
|
769
|
+
}
|
|
770
|
+
}
|
|
442
771
|
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
this.dbset(
|
|
449
|
-
this.dbset(
|
|
450
|
-
this.dbset(Settings); // Creates table: rag_Settings
|
|
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);
|
|
451
779
|
}
|
|
452
780
|
}
|
|
453
|
-
```
|
|
454
781
|
|
|
455
|
-
|
|
782
|
+
// Usage
|
|
783
|
+
const userDb = new UserContext();
|
|
784
|
+
const analyticsDb = new AnalyticsContext();
|
|
456
785
|
|
|
457
|
-
|
|
786
|
+
const user = userDb.User.findById(123);
|
|
787
|
+
analyticsDb.Event.new().log('user_login', user.id);
|
|
788
|
+
await analyticsDb.saveChanges();
|
|
789
|
+
```
|
|
458
790
|
|
|
459
791
|
```bash
|
|
460
|
-
#
|
|
461
|
-
|
|
792
|
+
# Migrate all contexts at once
|
|
793
|
+
masterrecord migrate-all
|
|
794
|
+
```
|
|
795
|
+
|
|
796
|
+
### Raw SQL Queries
|
|
462
797
|
|
|
463
|
-
|
|
464
|
-
master=development masterrecord add-migration Init AppContext
|
|
798
|
+
When you need full control:
|
|
465
799
|
|
|
466
|
-
|
|
467
|
-
|
|
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
|
+
);
|
|
468
812
|
```
|
|
469
813
|
|
|
470
|
-
|
|
814
|
+
## API Reference
|
|
471
815
|
|
|
472
|
-
###
|
|
473
|
-
- The prefix is applied during Context construction, so it must be set before `dbset()` calls
|
|
474
|
-
- The prefix is stored in migration snapshots, ensuring consistency across migration operations
|
|
475
|
-
- Empty strings or non-string values are ignored (no prefix applied)
|
|
476
|
-
- Both MySQL and SQLite fully support table prefixes with no special configuration needed
|
|
816
|
+
### Context Methods
|
|
477
817
|
|
|
478
|
-
|
|
818
|
+
```javascript
|
|
819
|
+
// Entity registration
|
|
820
|
+
context.dbset(EntityClass)
|
|
821
|
+
context.dbset(EntityClass, 'custom_table_name')
|
|
479
822
|
|
|
480
|
-
|
|
823
|
+
// Save changes
|
|
824
|
+
await context.saveChanges() // PostgreSQL (async)
|
|
825
|
+
context.saveChanges() // MySQL/SQLite (sync)
|
|
481
826
|
|
|
482
|
-
|
|
827
|
+
// Add/Remove entities
|
|
828
|
+
context.EntityName.add(entity)
|
|
829
|
+
context.remove(entity)
|
|
830
|
+
```
|
|
483
831
|
|
|
484
|
-
|
|
832
|
+
### Query Methods
|
|
485
833
|
|
|
486
834
|
```javascript
|
|
487
|
-
//
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
//
|
|
491
|
-
|
|
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
|
|
855
|
+
```
|
|
492
856
|
|
|
493
|
-
|
|
494
|
-
query = query.where(t => t.status == $$, 'pending');
|
|
857
|
+
### Migration Methods
|
|
495
858
|
|
|
496
|
-
|
|
497
|
-
|
|
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)
|
|
498
869
|
```
|
|
499
870
|
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
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();
|
|
506
907
|
```
|
|
507
908
|
|
|
508
|
-
###
|
|
909
|
+
### Pagination Example
|
|
509
910
|
|
|
510
|
-
|
|
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
|
|
511
937
|
|
|
512
938
|
```javascript
|
|
513
|
-
|
|
939
|
+
async function searchUsers(filters) {
|
|
940
|
+
const db = new AppContext();
|
|
941
|
+
let query = db.User;
|
|
514
942
|
|
|
515
|
-
//
|
|
516
|
-
|
|
943
|
+
// Apply filters dynamically
|
|
944
|
+
if (filters.name) {
|
|
945
|
+
query = query.where(u => u.name.like($$), `%${filters.name}%`);
|
|
946
|
+
}
|
|
517
947
|
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
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();
|
|
521
974
|
}
|
|
975
|
+
```
|
|
976
|
+
|
|
977
|
+
### Relationship Example
|
|
522
978
|
|
|
523
|
-
|
|
524
|
-
|
|
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
|
+
}
|
|
525
987
|
}
|
|
526
988
|
|
|
527
|
-
//
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
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();
|
|
532
1006
|
|
|
533
|
-
|
|
534
|
-
let users = query.toList();
|
|
1007
|
+
console.log(`${author.name} has ${posts.length} posts`);
|
|
535
1008
|
```
|
|
536
1009
|
|
|
537
|
-
|
|
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
|
+
```
|
|
538
1029
|
|
|
539
|
-
|
|
1030
|
+
### 2. Use Indexes
|
|
540
1031
|
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
1032
|
+
```javascript
|
|
1033
|
+
class User {
|
|
1034
|
+
constructor() {
|
|
1035
|
+
this.email = {
|
|
1036
|
+
type: 'string',
|
|
1037
|
+
unique: true // Automatically creates index
|
|
1038
|
+
};
|
|
1039
|
+
}
|
|
1040
|
+
}
|
|
549
1041
|
|
|
550
|
-
|
|
1042
|
+
// For complex queries, add database indexes manually
|
|
1043
|
+
// CREATE INDEX idx_user_status ON User(status);
|
|
1044
|
+
```
|
|
551
1045
|
|
|
552
|
-
|
|
1046
|
+
### 3. Limit Result Sets
|
|
553
1047
|
|
|
554
1048
|
```javascript
|
|
555
|
-
//
|
|
556
|
-
|
|
557
|
-
.
|
|
1049
|
+
// ✅ GOOD: Limit results
|
|
1050
|
+
const recentUsers = db.User
|
|
1051
|
+
.orderByDescending(u => u.created_at)
|
|
1052
|
+
.take(100)
|
|
558
1053
|
.toList();
|
|
1054
|
+
|
|
1055
|
+
// ❌ BAD: Load everything
|
|
1056
|
+
const allUsers = db.User.all();
|
|
559
1057
|
```
|
|
560
1058
|
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
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
|
+
});
|
|
565
1068
|
```
|
|
566
1069
|
|
|
567
|
-
|
|
1070
|
+
## Security
|
|
1071
|
+
|
|
1072
|
+
### SQL Injection Protection
|
|
1073
|
+
|
|
1074
|
+
MasterRecord uses **parameterized queries throughout** to prevent SQL injection:
|
|
568
1075
|
|
|
569
1076
|
```javascript
|
|
570
|
-
//
|
|
571
|
-
|
|
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:
|
|
572
1095
|
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
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
|
+
}
|
|
576
1102
|
|
|
577
|
-
|
|
578
|
-
if (startDate) {
|
|
579
|
-
query = query.where(o => o.created_at >= $$, startDate);
|
|
1103
|
+
return db.User.findById(userId);
|
|
580
1104
|
}
|
|
581
|
-
|
|
582
|
-
|
|
1105
|
+
```
|
|
1106
|
+
|
|
1107
|
+
## Troubleshooting
|
|
1108
|
+
|
|
1109
|
+
### PostgreSQL Connection Issues
|
|
1110
|
+
|
|
1111
|
+
```bash
|
|
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
|
|
1117
|
+
|
|
1118
|
+
# Error: Database does not exist
|
|
1119
|
+
createdb myapp
|
|
1120
|
+
|
|
1121
|
+
# Error: Authentication failed
|
|
1122
|
+
# Check pg_hba.conf and user permissions
|
|
1123
|
+
```
|
|
1124
|
+
|
|
1125
|
+
### MySQL Connection Issues
|
|
1126
|
+
|
|
1127
|
+
```bash
|
|
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';
|
|
1131
|
+
|
|
1132
|
+
# Error: ER_ACCESS_DENIED_ERROR
|
|
1133
|
+
# Check user permissions
|
|
1134
|
+
GRANT ALL PRIVILEGES ON myapp.* TO 'user'@'localhost';
|
|
1135
|
+
```
|
|
1136
|
+
|
|
1137
|
+
### Migration Issues
|
|
1138
|
+
|
|
1139
|
+
```bash
|
|
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/
|
|
1148
|
+
|
|
1149
|
+
# Type errors in migration
|
|
1150
|
+
# Check entity definitions match database types
|
|
1151
|
+
```
|
|
1152
|
+
|
|
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();
|
|
583
1166
|
}
|
|
584
1167
|
|
|
585
|
-
//
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
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 }
|
|
1171
|
+
```
|
|
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
|
|
591
1213
|
```
|
|
592
1214
|
|
|
593
|
-
|
|
1215
|
+
## License
|
|
1216
|
+
|
|
1217
|
+
MIT License - see [LICENSE](LICENSE) file for details.
|
|
1218
|
+
|
|
1219
|
+
## Credits
|
|
1220
|
+
|
|
1221
|
+
Created by Alexander Rich
|
|
1222
|
+
|
|
1223
|
+
## Support
|
|
594
1224
|
|
|
595
|
-
-
|
|
596
|
-
-
|
|
597
|
-
- The query is only executed when you call a terminal method: `toList()`, `single()`, `count()`
|
|
598
|
-
- Query builders are reusable - calling `toList()` resets the builder for the next query
|
|
1225
|
+
- GitHub Issues: [Report bugs or request features](https://github.com/Tailor/MasterRecord/issues)
|
|
1226
|
+
- npm: [masterrecord](https://www.npmjs.com/package/masterrecord)
|
|
599
1227
|
|
|
1228
|
+
---
|
|
600
1229
|
|
|
1230
|
+
**MasterRecord** - Code-first ORM for Node.js with multi-database support
|