ilana-orm 1.0.17 → 1.0.18
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/README.md +179 -6
- package/cli/ilana.js +29 -13
- package/database/connection.d.ts +3 -0
- package/database/connection.js +40 -1
- package/ilana.config.js +1 -1
- package/index.d.ts +17 -0
- package/index.js +6 -2
- package/index.mjs +2 -0
- package/jest.config.js +4 -0
- package/orm/Errors.js +18 -0
- package/orm/F.js +22 -0
- package/orm/Factory.js +49 -12
- package/orm/Model.d.ts +9 -0
- package/orm/Model.js +90 -0
- package/orm/QueryBuilder.d.ts +8 -0
- package/orm/QueryBuilder.js +35 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -447,6 +447,9 @@ module.exports = {
|
|
|
447
447
|
extension: "ts",
|
|
448
448
|
},
|
|
449
449
|
|
|
450
|
+
// SQL query logging — logs every query with bound values and execution time
|
|
451
|
+
logging: process.env.NODE_ENV === "development",
|
|
452
|
+
|
|
450
453
|
// Debugging
|
|
451
454
|
debug: process.env.NODE_ENV === "development",
|
|
452
455
|
|
|
@@ -1518,11 +1521,11 @@ const users = await User.all();
|
|
|
1518
1521
|
|
|
1519
1522
|
// Find by primary key
|
|
1520
1523
|
const user = await User.find(1);
|
|
1521
|
-
const user = await User.findOrFail(1); // Throws if not found
|
|
1524
|
+
const user = await User.findOrFail(1); // Throws ModelNotFoundException if not found
|
|
1522
1525
|
|
|
1523
1526
|
// First record
|
|
1524
1527
|
const user = await User.first();
|
|
1525
|
-
const user = await User.firstOrFail(); // Throws if not found
|
|
1528
|
+
const user = await User.firstOrFail(); // Throws ModelNotFoundException if not found
|
|
1526
1529
|
|
|
1527
1530
|
// Create or find
|
|
1528
1531
|
const user = await User.firstOrCreate(
|
|
@@ -1545,11 +1548,11 @@ const users = await User.all();
|
|
|
1545
1548
|
|
|
1546
1549
|
// Find by primary key
|
|
1547
1550
|
const user = await User.find(1);
|
|
1548
|
-
const user = await User.findOrFail(1); // Throws if not found
|
|
1551
|
+
const user = await User.findOrFail(1); // Throws ModelNotFoundException if not found
|
|
1549
1552
|
|
|
1550
1553
|
// First record
|
|
1551
1554
|
const user = await User.first();
|
|
1552
|
-
const user = await User.firstOrFail(); // Throws if not found
|
|
1555
|
+
const user = await User.firstOrFail(); // Throws ModelNotFoundException if not found
|
|
1553
1556
|
|
|
1554
1557
|
// Create or find
|
|
1555
1558
|
const user = await User.firstOrCreate(
|
|
@@ -4022,6 +4025,160 @@ const users = await User.query().on("reporting_db").get();
|
|
|
4022
4025
|
|
|
4023
4026
|
````
|
|
4024
4027
|
|
|
4028
|
+
## Debugging
|
|
4029
|
+
|
|
4030
|
+
### Query Logging
|
|
4031
|
+
|
|
4032
|
+
Enable SQL query logging in `ilana.config.js`:
|
|
4033
|
+
|
|
4034
|
+
```javascript
|
|
4035
|
+
// ilana.config.js
|
|
4036
|
+
module.exports = {
|
|
4037
|
+
default: 'mysql',
|
|
4038
|
+
logging: process.env.NODE_ENV === 'development', // logs all queries in dev
|
|
4039
|
+
connections: { ... }
|
|
4040
|
+
};
|
|
4041
|
+
```
|
|
4042
|
+
|
|
4043
|
+
Or toggle programmatically:
|
|
4044
|
+
|
|
4045
|
+
```javascript
|
|
4046
|
+
import { Database } from 'ilana-orm';
|
|
4047
|
+
|
|
4048
|
+
Database.enableLogging(); // turn on
|
|
4049
|
+
Database.disableLogging(); // turn off
|
|
4050
|
+
```
|
|
4051
|
+
|
|
4052
|
+
Output:
|
|
4053
|
+
```
|
|
4054
|
+
[IlanaORM] select * from "users" where "role" = 'admin' order by "created_at" desc limit 10 — 3ms
|
|
4055
|
+
[IlanaORM] select * from "posts" where "user_id" in (1, 2, 3) — 1ms
|
|
4056
|
+
```
|
|
4057
|
+
|
|
4058
|
+
Inspect a query without executing it:
|
|
4059
|
+
|
|
4060
|
+
```javascript
|
|
4061
|
+
const sql = User.query().where('role', 'admin').toSql();
|
|
4062
|
+
console.log(sql); // select * from "users" where "role" = 'admin'
|
|
4063
|
+
```
|
|
4064
|
+
|
|
4065
|
+
### ModelNotFoundException
|
|
4066
|
+
|
|
4067
|
+
`findOrFail()`, `firstOrFail()`, and `sole()` throw a `ModelNotFoundException` — a named error class you can catch specifically:
|
|
4068
|
+
|
|
4069
|
+
```javascript
|
|
4070
|
+
import { ModelNotFoundException } from 'ilana-orm';
|
|
4071
|
+
|
|
4072
|
+
// In a route
|
|
4073
|
+
app.get('/users/:id', async (req, res) => {
|
|
4074
|
+
const user = await User.findOrFail(req.params.id);
|
|
4075
|
+
res.json(user);
|
|
4076
|
+
});
|
|
4077
|
+
|
|
4078
|
+
// Global Express error handler
|
|
4079
|
+
app.use((err, req, res, next) => {
|
|
4080
|
+
if (err instanceof ModelNotFoundException) {
|
|
4081
|
+
return res.status(404).json({ message: err.message });
|
|
4082
|
+
// "User with id 99 not found"
|
|
4083
|
+
}
|
|
4084
|
+
res.status(500).json({ message: 'Server error' });
|
|
4085
|
+
});
|
|
4086
|
+
```
|
|
4087
|
+
|
|
4088
|
+
Properties: `err.message`, `err.model` (class name), `err.id` (the id passed to `findOrFail`).
|
|
4089
|
+
|
|
4090
|
+
Call `err.toResponse()` to get a plain `{ status: 404, message }` object suitable for any HTTP framework:
|
|
4091
|
+
|
|
4092
|
+
```javascript
|
|
4093
|
+
app.get('/users/:id', async (req, res) => {
|
|
4094
|
+
try {
|
|
4095
|
+
return res.json(await User.findOrFail(req.params.id));
|
|
4096
|
+
} catch (err) {
|
|
4097
|
+
if (err instanceof ModelNotFoundException) {
|
|
4098
|
+
const { status, message } = err.toResponse();
|
|
4099
|
+
return res.status(status).json({ message });
|
|
4100
|
+
}
|
|
4101
|
+
throw err;
|
|
4102
|
+
}
|
|
4103
|
+
});
|
|
4104
|
+
```
|
|
4105
|
+
|
|
4106
|
+
### Column Expressions with `F()`
|
|
4107
|
+
|
|
4108
|
+
Reference a column's current value in an update — no raw SQL, no race conditions, no need to fetch first:
|
|
4109
|
+
|
|
4110
|
+
```javascript
|
|
4111
|
+
import { F } from 'ilana-orm';
|
|
4112
|
+
|
|
4113
|
+
await Post.query().where('id', postId).update({ views: F('views').plus(1) });
|
|
4114
|
+
await Product.query().where('id', id).update({ stock: F('stock').minus(quantity) });
|
|
4115
|
+
```
|
|
4116
|
+
|
|
4117
|
+
Available: `.plus(n)`, `.minus(n)`, `.times(n)`, `.divide(n)`.
|
|
4118
|
+
|
|
4119
|
+
### Bulk Restore
|
|
4120
|
+
|
|
4121
|
+
Restore many soft-deleted records at once via the query builder:
|
|
4122
|
+
|
|
4123
|
+
```javascript
|
|
4124
|
+
await User.query().onlyTrashed().where('role', 'admin').restore();
|
|
4125
|
+
// UPDATE users SET deleted_at = NULL WHERE role = 'admin' AND deleted_at IS NOT NULL
|
|
4126
|
+
```
|
|
4127
|
+
|
|
4128
|
+
### Enum Helpers
|
|
4129
|
+
|
|
4130
|
+
Define possible values for enum columns and get auto-generated `isX()` / `makeX()` helpers on every instance:
|
|
4131
|
+
|
|
4132
|
+
```javascript
|
|
4133
|
+
class User extends Model {
|
|
4134
|
+
static enums = {
|
|
4135
|
+
role: ['user', 'moderator', 'admin'],
|
|
4136
|
+
status: ['active', 'suspended'],
|
|
4137
|
+
};
|
|
4138
|
+
}
|
|
4139
|
+
|
|
4140
|
+
const user = await User.find(1);
|
|
4141
|
+
user.isAdmin(); // true / false
|
|
4142
|
+
await user.makeAdmin(); // sets role = 'admin' and saves
|
|
4143
|
+
user.isSuspended(); // true / false
|
|
4144
|
+
```
|
|
4145
|
+
|
|
4146
|
+
### Strict Loading
|
|
4147
|
+
|
|
4148
|
+
Throw an error when an unloaded relation is accessed — catches N+1 problems at development time:
|
|
4149
|
+
|
|
4150
|
+
```javascript
|
|
4151
|
+
class Post extends Model {
|
|
4152
|
+
static strictLoading = true;
|
|
4153
|
+
}
|
|
4154
|
+
|
|
4155
|
+
const posts = await Post.all(); // no .with('comments')
|
|
4156
|
+
posts[0].relations.comments; // throws: 'comments' was not eager loaded on Post
|
|
4157
|
+
```
|
|
4158
|
+
|
|
4159
|
+
### Touch
|
|
4160
|
+
|
|
4161
|
+
Automatically update a parent's `updated_at` whenever the child saves:
|
|
4162
|
+
|
|
4163
|
+
```javascript
|
|
4164
|
+
class Comment extends Model {
|
|
4165
|
+
static touches = ['post'];
|
|
4166
|
+
|
|
4167
|
+
post() { return this.belongsTo('Post', 'post_id'); }
|
|
4168
|
+
}
|
|
4169
|
+
|
|
4170
|
+
await comment.save(); // also bumps posts SET updated_at = NOW() WHERE id = comment.post_id
|
|
4171
|
+
```
|
|
4172
|
+
|
|
4173
|
+
### Plain Object Results with `values()`
|
|
4174
|
+
|
|
4175
|
+
Return raw plain objects instead of model instances — faster for read-heavy endpoints where you don't need model methods:
|
|
4176
|
+
|
|
4177
|
+
```javascript
|
|
4178
|
+
const users = await User.query().select('id', 'name', 'email').values();
|
|
4179
|
+
// [{ id: 1, name: 'John', email: 'john@example.com' }, ...]
|
|
4180
|
+
```
|
|
4181
|
+
|
|
4025
4182
|
## TypeScript Support
|
|
4026
4183
|
|
|
4027
4184
|
### Type-Safe Models
|
|
@@ -4249,8 +4406,10 @@ User.updateOrCreate(search, update); // Update or create
|
|
|
4249
4406
|
User.firstOrNew(search, create); // Find or new instance
|
|
4250
4407
|
User.upsert(data, unique, update); // Upsert records
|
|
4251
4408
|
|
|
4252
|
-
// Deletion
|
|
4253
|
-
User.destroy(ids); // Delete by IDs
|
|
4409
|
+
// Deletion & seeding
|
|
4410
|
+
User.destroy(ids); // Delete by IDs (soft-delete aware)
|
|
4411
|
+
User.truncate(); // Delete all rows in the table
|
|
4412
|
+
User.seed(n); // Create n records using the registered factory
|
|
4254
4413
|
User.withTrashed(); // Include soft deleted
|
|
4255
4414
|
User.onlyTrashed(); // Only soft deleted
|
|
4256
4415
|
User.withoutTrashed(); // Exclude soft deleted
|
|
@@ -4297,6 +4456,8 @@ user.update(attributes); // Update model
|
|
|
4297
4456
|
user.delete(); // Delete model
|
|
4298
4457
|
user.forceDelete(); // Force delete (ignores softDeletes)
|
|
4299
4458
|
user.restore(); // Restore soft deleted
|
|
4459
|
+
user.fresh(); // Re-fetch from DB and return new instance
|
|
4460
|
+
user.is(other); // Check if two instances are the same record
|
|
4300
4461
|
|
|
4301
4462
|
// Attributes
|
|
4302
4463
|
user.fill(attributes); // Mass assign (respects fillable/guarded)
|
|
@@ -4435,6 +4596,18 @@ query.findOrFail(id); // Find or throw
|
|
|
4435
4596
|
query.pluck(column); // Get column values
|
|
4436
4597
|
query.exists(); // Check existence
|
|
4437
4598
|
query.doesntExist(); // Check non-existence
|
|
4599
|
+
query.sole(); // Get exactly one result — throws if zero or more than one
|
|
4600
|
+
query.tap(callback); // Run a callback for debugging without breaking the chain
|
|
4601
|
+
query.values(); // Return plain objects instead of model instances
|
|
4602
|
+
```
|
|
4603
|
+
|
|
4604
|
+
#### Soft Deletes (QueryBuilder)
|
|
4605
|
+
|
|
4606
|
+
```javascript
|
|
4607
|
+
query.withTrashed(); // include soft-deleted records
|
|
4608
|
+
query.onlyTrashed(); // only soft-deleted records
|
|
4609
|
+
query.withoutTrashed(); // exclude soft-deleted (default)
|
|
4610
|
+
query.restore(); // bulk-restore matched soft-deleted records
|
|
4438
4611
|
```
|
|
4439
4612
|
|
|
4440
4613
|
#### Pagination
|
package/cli/ilana.js
CHANGED
|
@@ -127,7 +127,10 @@ import Database from 'ilana-orm/database/connection.js';
|
|
|
127
127
|
const config = {
|
|
128
128
|
default: process.env.DB_CONNECTION || 'mysql',
|
|
129
129
|
timezone: process.env.DB_TIMEZONE || 'UTC',
|
|
130
|
-
|
|
130
|
+
|
|
131
|
+
// Set to true (or use process.env.NODE_ENV === 'development') to log all SQL queries
|
|
132
|
+
logging: false,
|
|
133
|
+
|
|
131
134
|
connections: {
|
|
132
135
|
sqlite: {
|
|
133
136
|
client: 'sqlite3',
|
|
@@ -136,7 +139,7 @@ const config = {
|
|
|
136
139
|
},
|
|
137
140
|
useNullAsDefault: true
|
|
138
141
|
},
|
|
139
|
-
|
|
142
|
+
|
|
140
143
|
mysql: {
|
|
141
144
|
client: 'mysql2',
|
|
142
145
|
connection: {
|
|
@@ -148,7 +151,7 @@ const config = {
|
|
|
148
151
|
timezone: process.env.DB_TIMEZONE || 'UTC'
|
|
149
152
|
}
|
|
150
153
|
},
|
|
151
|
-
|
|
154
|
+
|
|
152
155
|
postgres: {
|
|
153
156
|
client: 'pg',
|
|
154
157
|
connection: {
|
|
@@ -160,12 +163,12 @@ const config = {
|
|
|
160
163
|
}
|
|
161
164
|
}
|
|
162
165
|
},
|
|
163
|
-
|
|
166
|
+
|
|
164
167
|
migrations: {
|
|
165
168
|
directory: './${structure.databaseDir}/migrations',
|
|
166
169
|
tableName: 'migrations'
|
|
167
170
|
},
|
|
168
|
-
|
|
171
|
+
|
|
169
172
|
seeds: {
|
|
170
173
|
directory: './${structure.databaseDir}/seeds'
|
|
171
174
|
}
|
|
@@ -186,7 +189,10 @@ const Database = require('ilana-orm/database/connection');
|
|
|
186
189
|
const config = {
|
|
187
190
|
default: process.env.DB_CONNECTION || 'mysql',
|
|
188
191
|
timezone: process.env.DB_TIMEZONE || 'UTC',
|
|
189
|
-
|
|
192
|
+
|
|
193
|
+
// Set to true (or use process.env.NODE_ENV === 'development') to log all SQL queries
|
|
194
|
+
logging: false,
|
|
195
|
+
|
|
190
196
|
connections: {
|
|
191
197
|
sqlite: {
|
|
192
198
|
client: 'sqlite3',
|
|
@@ -195,7 +201,7 @@ const config = {
|
|
|
195
201
|
},
|
|
196
202
|
useNullAsDefault: true
|
|
197
203
|
},
|
|
198
|
-
|
|
204
|
+
|
|
199
205
|
mysql: {
|
|
200
206
|
client: 'mysql2',
|
|
201
207
|
connection: {
|
|
@@ -207,7 +213,7 @@ const config = {
|
|
|
207
213
|
timezone: process.env.DB_TIMEZONE || 'UTC'
|
|
208
214
|
}
|
|
209
215
|
},
|
|
210
|
-
|
|
216
|
+
|
|
211
217
|
postgres: {
|
|
212
218
|
client: 'pg',
|
|
213
219
|
connection: {
|
|
@@ -219,12 +225,12 @@ const config = {
|
|
|
219
225
|
}
|
|
220
226
|
}
|
|
221
227
|
},
|
|
222
|
-
|
|
228
|
+
|
|
223
229
|
migrations: {
|
|
224
230
|
directory: './${structure.databaseDir}/migrations',
|
|
225
231
|
tableName: 'migrations'
|
|
226
232
|
},
|
|
227
|
-
|
|
233
|
+
|
|
228
234
|
seeds: {
|
|
229
235
|
directory: './${structure.databaseDir}/seeds'
|
|
230
236
|
}
|
|
@@ -823,13 +829,13 @@ function parseMigrationsForTable(tableName, migrationsDir) {
|
|
|
823
829
|
|
|
824
830
|
// Match column definitions: table.string('col'), table.integer('col').nullable(), etc.
|
|
825
831
|
const colRegex = new RegExp(
|
|
826
|
-
`${alias}\\.(\\w+)\\s*\\(\\s*['"]([^'"]+)['"](
|
|
832
|
+
`${alias}\\.(\\w+)\\s*\\(\\s*['"]([^'"]+)['"]([^)]*)\\)([^;\\n]*)`,
|
|
827
833
|
'g'
|
|
828
834
|
);
|
|
829
835
|
|
|
830
836
|
let colMatch;
|
|
831
837
|
while ((colMatch = colRegex.exec(block)) !== null) {
|
|
832
|
-
const [, method, colName, rest] = colMatch;
|
|
838
|
+
const [, method, colName, args, rest] = colMatch;
|
|
833
839
|
const knexDef = KNEX_COLUMN_MAP[method];
|
|
834
840
|
if (!knexDef) continue;
|
|
835
841
|
|
|
@@ -837,7 +843,17 @@ function parseMigrationsForTable(tableName, migrationsDir) {
|
|
|
837
843
|
const isNotNullable = /\.notNullable\(\)/.test(rest);
|
|
838
844
|
const nullable = isNullable ? true : isNotNullable ? false : knexDef.nullable;
|
|
839
845
|
|
|
840
|
-
columns
|
|
846
|
+
// For enum columns, extract the values array and build a union type
|
|
847
|
+
let tsType = knexDef.type;
|
|
848
|
+
if (method === 'enum') {
|
|
849
|
+
const valuesMatch = args.match(/\[([^\]]+)\]/);
|
|
850
|
+
if (valuesMatch) {
|
|
851
|
+
const values = [...valuesMatch[1].matchAll(/['"]([^'"]+)['"]/g)].map(m => `'${m[1]}'`);
|
|
852
|
+
if (values.length) tsType = values.join(' | ');
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
columns[colName] = { type: tsType, nullable };
|
|
841
857
|
}
|
|
842
858
|
}
|
|
843
859
|
}
|
package/database/connection.d.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { Knex } from 'knex';
|
|
|
2
2
|
|
|
3
3
|
export interface DatabaseConfig {
|
|
4
4
|
default: string;
|
|
5
|
+
logging?: boolean;
|
|
5
6
|
connections: {
|
|
6
7
|
[name: string]: Knex.Config;
|
|
7
8
|
};
|
|
@@ -27,6 +28,8 @@ export default class Database {
|
|
|
27
28
|
private static _currentTransaction: Knex.Transaction | null;
|
|
28
29
|
|
|
29
30
|
static configure(config: DatabaseConfig): void;
|
|
31
|
+
static enableLogging(): void;
|
|
32
|
+
static disableLogging(): void;
|
|
30
33
|
static connection(name?: string): Knex;
|
|
31
34
|
static getDefaultConnection(): string;
|
|
32
35
|
static hasConnection(name: string): boolean;
|
package/database/connection.js
CHANGED
|
@@ -3,11 +3,33 @@ const knex = require('knex');
|
|
|
3
3
|
class Database {
|
|
4
4
|
static connections = new Map();
|
|
5
5
|
static _currentTransaction = null;
|
|
6
|
+
static _logging = false;
|
|
7
|
+
|
|
8
|
+
static enableLogging() {
|
|
9
|
+
this._logging = true;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
static disableLogging() {
|
|
13
|
+
this._logging = false;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
static _log(sql, bindings, ms) {
|
|
17
|
+
if (!this._logging) return;
|
|
18
|
+
const bound = sql.replace(/\?/g, () => {
|
|
19
|
+
const val = bindings?.shift();
|
|
20
|
+
if (val === null || val === undefined) return 'NULL';
|
|
21
|
+
if (typeof val === 'string') return `'${val}'`;
|
|
22
|
+
return val;
|
|
23
|
+
});
|
|
24
|
+
const time = ms !== undefined ? ` \x1b[90m— ${ms}ms\x1b[0m` : '';
|
|
25
|
+
console.log(`\x1b[36m[IlanaORM]\x1b[0m ${bound}${time}`);
|
|
26
|
+
}
|
|
6
27
|
|
|
7
28
|
static configure(config) {
|
|
8
29
|
this.config = config;
|
|
9
30
|
this.defaultConnection = config.default;
|
|
10
|
-
|
|
31
|
+
if (config.logging) this._logging = true;
|
|
32
|
+
|
|
11
33
|
// Initialize all configured connections
|
|
12
34
|
for (const [name, connConfig] of Object.entries(config.connections)) {
|
|
13
35
|
try {
|
|
@@ -21,6 +43,23 @@ class Database {
|
|
|
21
43
|
directory: './seeds'
|
|
22
44
|
}
|
|
23
45
|
});
|
|
46
|
+
|
|
47
|
+
// Attach query logger
|
|
48
|
+
connection.on('query', (query) => {
|
|
49
|
+
this._queryStart = Date.now();
|
|
50
|
+
this._pendingQuery = query.sql;
|
|
51
|
+
this._pendingBindings = [...(query.bindings || [])];
|
|
52
|
+
});
|
|
53
|
+
connection.on('query-response', (response, query) => {
|
|
54
|
+
const ms = Date.now() - (this._queryStart || Date.now());
|
|
55
|
+
this._log(query.sql, [...(query.bindings || [])], ms);
|
|
56
|
+
});
|
|
57
|
+
connection.on('query-error', (error, query) => {
|
|
58
|
+
if (this._logging) {
|
|
59
|
+
console.error(`\x1b[36m[IlanaORM]\x1b[0m \x1b[31mERROR\x1b[0m ${query.sql}`);
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
|
|
24
63
|
this.connections.set(name, connection);
|
|
25
64
|
} catch (error) {
|
|
26
65
|
if (error.code === 'MODULE_NOT_FOUND' && error.message.includes(connConfig.client)) {
|
package/ilana.config.js
CHANGED
package/index.d.ts
CHANGED
|
@@ -5,5 +5,22 @@ export { default as Database } from './database/connection';
|
|
|
5
5
|
export * from './orm/Relation';
|
|
6
6
|
export * from './orm/CustomCasts';
|
|
7
7
|
|
|
8
|
+
export declare class FExpression {
|
|
9
|
+
constructor(column: string);
|
|
10
|
+
plus(n: number): any;
|
|
11
|
+
minus(n: number): any;
|
|
12
|
+
times(n: number): any;
|
|
13
|
+
divide(n: number): any;
|
|
14
|
+
}
|
|
15
|
+
export declare function F(column: string): FExpression;
|
|
16
|
+
|
|
17
|
+
export declare class ModelNotFoundException extends Error {
|
|
18
|
+
name: 'ModelNotFoundException';
|
|
19
|
+
model: string;
|
|
20
|
+
id?: any;
|
|
21
|
+
constructor(model: string, id?: any);
|
|
22
|
+
toResponse(): { status: 404; message: string };
|
|
23
|
+
}
|
|
24
|
+
|
|
8
25
|
// Default export
|
|
9
26
|
export { default } from './orm/Model';
|
package/index.js
CHANGED
|
@@ -10,6 +10,8 @@ const Seeder = require('./orm/Seeder');
|
|
|
10
10
|
const Factory = require('./orm/Factory');
|
|
11
11
|
const Relation = require('./orm/Relation');
|
|
12
12
|
const CustomCasts = require('./orm/CustomCasts');
|
|
13
|
+
const { ModelNotFoundException } = require('./orm/Errors');
|
|
14
|
+
const { F } = require('./orm/F');
|
|
13
15
|
|
|
14
16
|
module.exports = {
|
|
15
17
|
Model,
|
|
@@ -22,11 +24,13 @@ module.exports = {
|
|
|
22
24
|
Seeder,
|
|
23
25
|
Factory: Factory.Factory,
|
|
24
26
|
defineFactory: Factory.defineFactory,
|
|
25
|
-
|
|
27
|
+
ModelNotFoundException,
|
|
28
|
+
F,
|
|
29
|
+
|
|
26
30
|
// Relationships
|
|
27
31
|
MorphOne: Relation.MorphOne,
|
|
28
32
|
...Relation,
|
|
29
|
-
|
|
33
|
+
|
|
30
34
|
// Custom Casts
|
|
31
35
|
...CustomCasts
|
|
32
36
|
};
|
package/index.mjs
CHANGED
package/jest.config.js
ADDED
package/orm/Errors.js
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
class ModelNotFoundException extends Error {
|
|
2
|
+
constructor(model, id) {
|
|
3
|
+
const message = id !== undefined
|
|
4
|
+
? `${model} with id ${id} not found`
|
|
5
|
+
: `${model} not found`;
|
|
6
|
+
super(message);
|
|
7
|
+
this.name = 'ModelNotFoundException';
|
|
8
|
+
this.model = model;
|
|
9
|
+
this.id = id;
|
|
10
|
+
if (Error.captureStackTrace) Error.captureStackTrace(this, ModelNotFoundException);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
toResponse() {
|
|
14
|
+
return { status: 404, message: this.message };
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
module.exports = { ModelNotFoundException };
|
package/orm/F.js
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
const Database = require('../database/connection');
|
|
2
|
+
|
|
3
|
+
class FExpression {
|
|
4
|
+
constructor(column) {
|
|
5
|
+
this.column = column;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
_raw(op, n) {
|
|
9
|
+
return Database.raw(`?? ${op} ?`, [this.column, n]);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
plus(n) { return this._raw('+', n); }
|
|
13
|
+
minus(n) { return this._raw('-', n); }
|
|
14
|
+
times(n) { return this._raw('*', n); }
|
|
15
|
+
divide(n) { return this._raw('/', n); }
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function F(column) {
|
|
19
|
+
return new FExpression(column);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
module.exports = { F, FExpression };
|
package/orm/Factory.js
CHANGED
|
@@ -14,6 +14,9 @@ class Factory {
|
|
|
14
14
|
this.count = 1;
|
|
15
15
|
this.currentStates = [];
|
|
16
16
|
this.relationships = new Map();
|
|
17
|
+
this._has = new Map();
|
|
18
|
+
this._for = new Map();
|
|
19
|
+
this._hasAttached = new Map();
|
|
17
20
|
this._sequenceCount = 0;
|
|
18
21
|
this.sequences = new Map();
|
|
19
22
|
}
|
|
@@ -57,23 +60,30 @@ class Factory {
|
|
|
57
60
|
return this;
|
|
58
61
|
}
|
|
59
62
|
|
|
60
|
-
for(relation,
|
|
61
|
-
this.
|
|
63
|
+
for(relation, relFactory) {
|
|
64
|
+
this._for.set(relation, relFactory);
|
|
62
65
|
return this;
|
|
63
66
|
}
|
|
64
67
|
|
|
65
|
-
has(
|
|
66
|
-
|
|
67
|
-
this.relationships.set(relation, factory);
|
|
68
|
-
}
|
|
68
|
+
has(relFactory, relation) {
|
|
69
|
+
this._has.set(relation, relFactory);
|
|
69
70
|
return this;
|
|
70
71
|
}
|
|
71
72
|
|
|
72
|
-
hasAttached(
|
|
73
|
-
this.
|
|
73
|
+
hasAttached(relFactory, relation) {
|
|
74
|
+
this._hasAttached.set(relation, relFactory);
|
|
74
75
|
return this;
|
|
75
76
|
}
|
|
76
77
|
|
|
78
|
+
_getRelation(relationName) {
|
|
79
|
+
try {
|
|
80
|
+
const dummy = new this.model({});
|
|
81
|
+
return typeof dummy[relationName] === 'function' ? dummy[relationName]() : null;
|
|
82
|
+
} catch (_) {
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
77
87
|
sequence() {
|
|
78
88
|
return ++this._sequenceCount;
|
|
79
89
|
}
|
|
@@ -173,16 +183,40 @@ class Factory {
|
|
|
173
183
|
}
|
|
174
184
|
|
|
175
185
|
async createOne(attributes = {}) {
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
186
|
+
// Handle 'for' (belongsTo) — create parent first, inject FK into this model
|
|
187
|
+
const parentAttrs = {};
|
|
188
|
+
for (const [relationName, relFactory] of this._for) {
|
|
189
|
+
const parent = await relFactory.createOne({});
|
|
190
|
+
const rel = this._getRelation(relationName);
|
|
191
|
+
if (rel && rel.foreignKey) parentAttrs[rel.foreignKey] = parent.getKey();
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const model = await this.makeOne({ ...parentAttrs, ...attributes });
|
|
195
|
+
|
|
179
196
|
for (const callback of this._beforeCreatingCallbacks) {
|
|
180
197
|
await callback(model);
|
|
181
198
|
}
|
|
182
199
|
|
|
183
200
|
await model.save();
|
|
184
201
|
|
|
185
|
-
//
|
|
202
|
+
// Handle 'has' (hasMany) — create children with FK pointing to this model
|
|
203
|
+
for (const [relationName, relFactory] of this._has) {
|
|
204
|
+
const rel = this._getRelation(relationName);
|
|
205
|
+
if (rel && rel.foreignKey) {
|
|
206
|
+
await relFactory.create({ [rel.foreignKey]: model.getKey() });
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// Handle 'hasAttached' (belongsToMany) — create and attach via pivot
|
|
211
|
+
for (const [relationName, relFactory] of this._hasAttached) {
|
|
212
|
+
const related = await relFactory.create({});
|
|
213
|
+
const relatedArr = Array.isArray(related) ? related : [related];
|
|
214
|
+
const rel = this._getRelation(relationName);
|
|
215
|
+
if (rel && typeof rel.attach === 'function') {
|
|
216
|
+
for (const r of relatedArr) await rel.attach(r.getKey());
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
186
220
|
for (const callback of this._afterCreatingCallbacks) {
|
|
187
221
|
await callback(model);
|
|
188
222
|
}
|
|
@@ -354,6 +388,9 @@ if (typeof Model !== 'undefined') {
|
|
|
354
388
|
newFactory.states = new Map(existingFactory.states);
|
|
355
389
|
newFactory._afterCreatingCallbacks = [...existingFactory._afterCreatingCallbacks];
|
|
356
390
|
newFactory._beforeCreatingCallbacks = [...existingFactory._beforeCreatingCallbacks];
|
|
391
|
+
newFactory._has = new Map(existingFactory._has);
|
|
392
|
+
newFactory._for = new Map(existingFactory._for);
|
|
393
|
+
newFactory._hasAttached = new Map(existingFactory._hasAttached);
|
|
357
394
|
return newFactory;
|
|
358
395
|
}
|
|
359
396
|
throw new Error(`No factory defined for model: ${this.name}`);
|
package/orm/Model.d.ts
CHANGED
|
@@ -50,6 +50,9 @@ export default class Model<TAttributes extends ModelAttributes = ModelAttributes
|
|
|
50
50
|
protected static globalScopes: Map<string, (query: QueryBuilder) => void>;
|
|
51
51
|
protected static appends: string[];
|
|
52
52
|
protected static timezone: string;
|
|
53
|
+
static strictLoading: boolean;
|
|
54
|
+
static touches: string[];
|
|
55
|
+
static enums: { [column: string]: string[] };
|
|
53
56
|
|
|
54
57
|
// Instance properties
|
|
55
58
|
attributes: ModelAttributes;
|
|
@@ -90,6 +93,8 @@ export default class Model<TAttributes extends ModelAttributes = ModelAttributes
|
|
|
90
93
|
static generateUuid(): string;
|
|
91
94
|
static insert(data: ModelAttributes | ModelAttributes[]): Promise<any>;
|
|
92
95
|
static destroy(ids: any | any[]): Promise<number>;
|
|
96
|
+
static truncate(): Promise<void>;
|
|
97
|
+
static seed(count?: number): Promise<any[]>;
|
|
93
98
|
static firstOrCreate(attributes: ModelAttributes, values?: ModelAttributes): Promise<Model>;
|
|
94
99
|
static firstOrNew(attributes: ModelAttributes, values?: ModelAttributes): Promise<Model>;
|
|
95
100
|
static updateOrCreate(attributes: ModelAttributes, values?: ModelAttributes): Promise<Model>;
|
|
@@ -143,10 +148,14 @@ export default class Model<TAttributes extends ModelAttributes = ModelAttributes
|
|
|
143
148
|
getDirty(): ModelAttributes;
|
|
144
149
|
delete(): Promise<boolean>;
|
|
145
150
|
restore(): Promise<boolean>;
|
|
151
|
+
increment(column: string, amount?: number): Promise<this>;
|
|
152
|
+
decrement(column: string, amount?: number): Promise<this>;
|
|
146
153
|
trashed(): boolean;
|
|
147
154
|
only(keys: string[]): ModelAttributes;
|
|
148
155
|
except(keys: string[]): ModelAttributes;
|
|
149
156
|
forceDelete(): Promise<boolean>;
|
|
157
|
+
fresh(): Promise<this | null>;
|
|
158
|
+
is(other: Model): boolean;
|
|
150
159
|
toJSON(): any;
|
|
151
160
|
|
|
152
161
|
// Relationships
|
package/orm/Model.js
CHANGED
|
@@ -35,6 +35,9 @@ class Model {
|
|
|
35
35
|
static globalScopes = new Map();
|
|
36
36
|
static appends = [];
|
|
37
37
|
static timezone = 'UTC';
|
|
38
|
+
static strictLoading = false;
|
|
39
|
+
static touches = [];
|
|
40
|
+
static enums = {};
|
|
38
41
|
|
|
39
42
|
// --- Instance props ---
|
|
40
43
|
attributes = {};
|
|
@@ -62,6 +65,25 @@ class Model {
|
|
|
62
65
|
? this.appends
|
|
63
66
|
: this.constructor.appends;
|
|
64
67
|
|
|
68
|
+
// Wrap relations in a Proxy for strict loading enforcement
|
|
69
|
+
const modelClass = this.constructor;
|
|
70
|
+
this.relations = new Proxy({}, {
|
|
71
|
+
get(target, key) {
|
|
72
|
+
if (typeof key !== 'string') return target[key];
|
|
73
|
+
if (modelClass.strictLoading && !(key in target)) {
|
|
74
|
+
throw new Error(
|
|
75
|
+
`Strict loading violation: '${key}' was not eager loaded on ${modelClass.name}. ` +
|
|
76
|
+
`Use .with('${key}') in your query.`
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
return target[key];
|
|
80
|
+
},
|
|
81
|
+
set(target, key, value) { target[key] = value; return true; },
|
|
82
|
+
has(target, key) { return key in target; },
|
|
83
|
+
ownKeys(target) { return Object.keys(target); },
|
|
84
|
+
getOwnPropertyDescriptor(target, key) { return Object.getOwnPropertyDescriptor(target, key); },
|
|
85
|
+
});
|
|
86
|
+
|
|
65
87
|
// defer attribute setting
|
|
66
88
|
if (attrs && Object.keys(attrs).length) this._deferred = attrs;
|
|
67
89
|
|
|
@@ -79,6 +101,7 @@ class Model {
|
|
|
79
101
|
this._deferred = null;
|
|
80
102
|
// Recreate getters after initialization
|
|
81
103
|
this._createAttributeGetters();
|
|
104
|
+
this._generateEnumHelpers();
|
|
82
105
|
}
|
|
83
106
|
|
|
84
107
|
_createAttributeGetters() {
|
|
@@ -106,6 +129,26 @@ class Model {
|
|
|
106
129
|
}
|
|
107
130
|
}
|
|
108
131
|
|
|
132
|
+
_generateEnumHelpers() {
|
|
133
|
+
const enums = this.constructor.enums || {};
|
|
134
|
+
for (const [column, values] of Object.entries(enums)) {
|
|
135
|
+
for (const value of values) {
|
|
136
|
+
const pascal = value.charAt(0).toUpperCase() + value.slice(1);
|
|
137
|
+
const isMethod = `is${pascal}`;
|
|
138
|
+
const makeMethod = `make${pascal}`;
|
|
139
|
+
if (!this[isMethod]) {
|
|
140
|
+
this[isMethod] = () => this.getAttribute(column) === value;
|
|
141
|
+
}
|
|
142
|
+
if (!this[makeMethod]) {
|
|
143
|
+
this[makeMethod] = async () => {
|
|
144
|
+
this.setAttribute(column, value);
|
|
145
|
+
return this.save();
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
109
152
|
_toPascalCase(key) {
|
|
110
153
|
return key.replace(/(^|_)([a-z])/g, (_, __, c) => c.toUpperCase());
|
|
111
154
|
}
|
|
@@ -210,6 +253,11 @@ class Model {
|
|
|
210
253
|
}
|
|
211
254
|
|
|
212
255
|
static async insert(data) { return this.query().insert(data); }
|
|
256
|
+
static async truncate() { return this.query().toKnex().truncate(); }
|
|
257
|
+
static async seed(count = 1) {
|
|
258
|
+
const { factory } = require('./Factory');
|
|
259
|
+
return factory(this).times(count).create();
|
|
260
|
+
}
|
|
213
261
|
static async destroy(ids) {
|
|
214
262
|
const idList = Array.isArray(ids) ? ids : [ids];
|
|
215
263
|
if (this.softDeletes) {
|
|
@@ -525,9 +573,27 @@ class Model {
|
|
|
525
573
|
this.syncOriginal();
|
|
526
574
|
}
|
|
527
575
|
|
|
576
|
+
await this._touchRelations();
|
|
577
|
+
|
|
528
578
|
return true;
|
|
529
579
|
}
|
|
530
580
|
|
|
581
|
+
async _touchRelations() {
|
|
582
|
+
const touches = this.constructor.touches || [];
|
|
583
|
+
for (const relName of touches) {
|
|
584
|
+
if (typeof this[relName] !== 'function') continue;
|
|
585
|
+
const rel = this[relName]();
|
|
586
|
+
if (!rel || rel.constructor.name !== 'BelongsTo') continue;
|
|
587
|
+
const parentClass = rel.getRelatedClass();
|
|
588
|
+
const parentId = this.getAttribute(rel.foreignKey);
|
|
589
|
+
if (!parentId) continue;
|
|
590
|
+
const col = parentClass.updatedAt || 'updated_at';
|
|
591
|
+
await parentClass.query()
|
|
592
|
+
.where(parentClass.primaryKey || 'id', parentId)
|
|
593
|
+
.update({ [col]: new Date() });
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
|
|
531
597
|
async update(attributes = {}) {
|
|
532
598
|
this.fill(attributes);
|
|
533
599
|
return await this.save();
|
|
@@ -604,6 +670,20 @@ class Model {
|
|
|
604
670
|
return result;
|
|
605
671
|
}
|
|
606
672
|
|
|
673
|
+
async increment(column, amount = 1) {
|
|
674
|
+
await this.constructor.query().where(this.constructor.primaryKey, this.getKey()).increment(column, amount);
|
|
675
|
+
this.setAttribute(column, (this.getAttribute(column) || 0) + amount);
|
|
676
|
+
this.syncOriginal();
|
|
677
|
+
return this;
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
async decrement(column, amount = 1) {
|
|
681
|
+
await this.constructor.query().where(this.constructor.primaryKey, this.getKey()).decrement(column, amount);
|
|
682
|
+
this.setAttribute(column, (this.getAttribute(column) || 0) - amount);
|
|
683
|
+
this.syncOriginal();
|
|
684
|
+
return this;
|
|
685
|
+
}
|
|
686
|
+
|
|
607
687
|
async forceDelete() {
|
|
608
688
|
if (!this.exists) return false;
|
|
609
689
|
|
|
@@ -614,6 +694,16 @@ class Model {
|
|
|
614
694
|
return true;
|
|
615
695
|
}
|
|
616
696
|
|
|
697
|
+
async fresh() {
|
|
698
|
+
if (!this.exists) return null;
|
|
699
|
+
return this.constructor.find(this.getKey());
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
is(other) {
|
|
703
|
+
if (!other || !(other instanceof this.constructor)) return false;
|
|
704
|
+
return this.getKey() === other.getKey();
|
|
705
|
+
}
|
|
706
|
+
|
|
617
707
|
// JSON serialization
|
|
618
708
|
toJSON() {
|
|
619
709
|
this._initialize();
|
package/orm/QueryBuilder.d.ts
CHANGED
|
@@ -135,6 +135,8 @@ export default class QueryBuilder {
|
|
|
135
135
|
pluck(column: string): Promise<any[]>;
|
|
136
136
|
exists(): Promise<boolean>;
|
|
137
137
|
doesntExist(): Promise<boolean>;
|
|
138
|
+
sole(): Promise<Model>;
|
|
139
|
+
tap(callback: (query: this) => void): this;
|
|
138
140
|
|
|
139
141
|
// Pagination
|
|
140
142
|
paginate(page?: number, perPage?: number): Promise<PaginationResult<Model>>;
|
|
@@ -150,6 +152,8 @@ export default class QueryBuilder {
|
|
|
150
152
|
insert(data: any | any[]): Promise<any>;
|
|
151
153
|
insertGetId(data: any): Promise<any>;
|
|
152
154
|
update(data: any): Promise<number>;
|
|
155
|
+
increment(column: string, amount?: number): Promise<number>;
|
|
156
|
+
decrement(column: string, amount?: number): Promise<number>;
|
|
153
157
|
delete(): Promise<number>;
|
|
154
158
|
upsert(data: any[], uniqueBy: string[], update?: string[]): Promise<any>;
|
|
155
159
|
|
|
@@ -167,6 +171,10 @@ export default class QueryBuilder {
|
|
|
167
171
|
withTrashed(): this;
|
|
168
172
|
onlyTrashed(): this;
|
|
169
173
|
withoutTrashed(): this;
|
|
174
|
+
restore(): Promise<number>;
|
|
175
|
+
|
|
176
|
+
// Plain object result (no model hydration)
|
|
177
|
+
values(): Promise<any[]>;
|
|
170
178
|
|
|
171
179
|
// Debug
|
|
172
180
|
toSql(): string;
|
package/orm/QueryBuilder.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
const Database = require('../database/connection');
|
|
2
2
|
const Collection = require('./Collection');
|
|
3
3
|
const ModelRegistry = require('./ModelRegistry');
|
|
4
|
+
const { ModelNotFoundException } = require('./Errors');
|
|
4
5
|
|
|
5
6
|
class QueryBuilder {
|
|
6
7
|
constructor(tableName, modelClass, connectionName) {
|
|
@@ -609,13 +610,13 @@ class QueryBuilder {
|
|
|
609
610
|
|
|
610
611
|
async findOrFail(id) {
|
|
611
612
|
const result = await this.find(id);
|
|
612
|
-
if (!result) throw new
|
|
613
|
+
if (!result) throw new ModelNotFoundException(this.modelClass?.name || 'Model', id);
|
|
613
614
|
return result;
|
|
614
615
|
}
|
|
615
616
|
|
|
616
617
|
async firstOrFail() {
|
|
617
618
|
const result = await this.first();
|
|
618
|
-
if (!result) throw new
|
|
619
|
+
if (!result) throw new ModelNotFoundException(this.modelClass?.name || 'Model');
|
|
619
620
|
return result;
|
|
620
621
|
}
|
|
621
622
|
|
|
@@ -623,6 +624,18 @@ class QueryBuilder {
|
|
|
623
624
|
return !(await this.exists());
|
|
624
625
|
}
|
|
625
626
|
|
|
627
|
+
async sole() {
|
|
628
|
+
const results = await this._softQuery().limit(2);
|
|
629
|
+
if (results.length === 0) throw new ModelNotFoundException(this.modelClass?.name || 'Model');
|
|
630
|
+
if (results.length > 1) throw new Error(`${this.modelClass?.name || 'Model'}: sole() found more than one result.`);
|
|
631
|
+
return this.modelClass ? new this.modelClass(results[0]) : results[0];
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
tap(callback) {
|
|
635
|
+
callback(this);
|
|
636
|
+
return this;
|
|
637
|
+
}
|
|
638
|
+
|
|
626
639
|
async pluck(column) {
|
|
627
640
|
return await this._softQuery().pluck(column);
|
|
628
641
|
}
|
|
@@ -758,6 +771,14 @@ class QueryBuilder {
|
|
|
758
771
|
return this.query.update(data);
|
|
759
772
|
}
|
|
760
773
|
|
|
774
|
+
async increment(column, amount = 1) {
|
|
775
|
+
return this.query.increment(column, amount);
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
async decrement(column, amount = 1) {
|
|
779
|
+
return this.query.decrement(column, amount);
|
|
780
|
+
}
|
|
781
|
+
|
|
761
782
|
async delete() {
|
|
762
783
|
return this.query.del();
|
|
763
784
|
}
|
|
@@ -1033,6 +1054,18 @@ class QueryBuilder {
|
|
|
1033
1054
|
return this;
|
|
1034
1055
|
}
|
|
1035
1056
|
|
|
1057
|
+
async restore() {
|
|
1058
|
+
if (!this.modelClass || !this.modelClass.softDeletes) {
|
|
1059
|
+
throw new Error(`restore() requires softDeletes to be enabled on ${this.modelClass?.name || 'the model'}`);
|
|
1060
|
+
}
|
|
1061
|
+
const col = this.modelClass.deletedAt || 'deleted_at';
|
|
1062
|
+
return this._softQuery().update({ [col]: null });
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
async values() {
|
|
1066
|
+
return this._softQuery();
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1036
1069
|
// Debug
|
|
1037
1070
|
toSql() {
|
|
1038
1071
|
return this.query.toSQL().sql;
|