ilana-orm 1.0.17 → 1.0.19
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 +293 -37
- package/cli/ilana.js +60 -158
- package/database/connection.d.ts +3 -0
- package/database/connection.js +40 -1
- package/database/schema-builder.js +4 -0
- package/ilana.config.js +1 -1
- package/index.d.ts +17 -0
- package/index.edge.js +10 -0
- package/index.edge.mjs +37 -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/MigrationRunner.js +16 -48
- package/orm/Model.d.ts +25 -1
- package/orm/Model.js +219 -19
- package/orm/QueryBuilder.d.ts +18 -0
- package/orm/QueryBuilder.js +109 -3
- package/package.json +6 -1
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) {
|
|
@@ -226,7 +227,32 @@ class QueryBuilder {
|
|
|
226
227
|
}
|
|
227
228
|
|
|
228
229
|
addSelect(...columns) {
|
|
229
|
-
|
|
230
|
+
for (const col of columns) {
|
|
231
|
+
if (col && typeof col === 'object' && !Array.isArray(col)) {
|
|
232
|
+
// Subquery form: addSelect({ alias: (qb) => qb.from('table').select('col').where(...) })
|
|
233
|
+
for (const [alias, subFn] of Object.entries(col)) {
|
|
234
|
+
const knex = Database.connection(this.connectionName);
|
|
235
|
+
const subQuery = knex.queryBuilder();
|
|
236
|
+
const subQb = new QueryBuilder('', null, this.connectionName);
|
|
237
|
+
subQb.query = subQuery;
|
|
238
|
+
subFn(subQb);
|
|
239
|
+
this.query.column(knex.raw(`(${subQuery.toSQL().sql}) as "${alias}"`, subQuery.toSQL().bindings));
|
|
240
|
+
}
|
|
241
|
+
} else {
|
|
242
|
+
this.query.column(col);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
return this;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
orderBySubquery(callback, direction = 'asc') {
|
|
249
|
+
const knex = Database.connection(this.connectionName);
|
|
250
|
+
const subQuery = knex.queryBuilder();
|
|
251
|
+
const subQb = new QueryBuilder('', null, this.connectionName);
|
|
252
|
+
subQb.query = subQuery;
|
|
253
|
+
callback(subQb);
|
|
254
|
+
const { sql, bindings } = subQuery.toSQL();
|
|
255
|
+
this.query.orderByRaw(`(${sql}) ${direction === 'desc' ? 'desc' : 'asc'}`, bindings);
|
|
230
256
|
return this;
|
|
231
257
|
}
|
|
232
258
|
|
|
@@ -249,6 +275,23 @@ class QueryBuilder {
|
|
|
249
275
|
return this;
|
|
250
276
|
}
|
|
251
277
|
|
|
278
|
+
withPendingAttributes(attrs) {
|
|
279
|
+
this._pendingAttributes = { ...(this._pendingAttributes || {}), ...attrs };
|
|
280
|
+
return this;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
async new(attrs = {}) {
|
|
284
|
+
const merged = { ...(this._pendingAttributes || {}), ...attrs };
|
|
285
|
+
const inst = new this.modelClass(merged);
|
|
286
|
+
inst._initialize();
|
|
287
|
+
return inst;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
async create(attrs = {}) {
|
|
291
|
+
const merged = { ...(this._pendingAttributes || {}), ...attrs };
|
|
292
|
+
return this.modelClass.create(merged);
|
|
293
|
+
}
|
|
294
|
+
|
|
252
295
|
when(condition, callback, otherwise) {
|
|
253
296
|
if (condition) {
|
|
254
297
|
callback(this, condition);
|
|
@@ -548,6 +591,37 @@ class QueryBuilder {
|
|
|
548
591
|
return this.whereDoesntHave(relation);
|
|
549
592
|
}
|
|
550
593
|
|
|
594
|
+
has(relation, operator = '>=', count = 1) {
|
|
595
|
+
if (!this.modelClass) return this;
|
|
596
|
+
try {
|
|
597
|
+
const dummy = this._makeDummy();
|
|
598
|
+
const relFn = this.modelClass.prototype[relation];
|
|
599
|
+
if (typeof relFn !== 'function') return this;
|
|
600
|
+
const rel = relFn.call(dummy);
|
|
601
|
+
const relatedClass = rel.getRelatedClass();
|
|
602
|
+
const relatedTable = relatedClass.getTableName();
|
|
603
|
+
const parentTable = this.modelClass.getTableName();
|
|
604
|
+
const isBelongsTo = rel.constructor.name === 'BelongsTo';
|
|
605
|
+
const joinCol = isBelongsTo
|
|
606
|
+
? `${relatedTable}.${rel.localKey || relatedClass.getPrimaryKey()} = ${parentTable}.${rel.foreignKey}`
|
|
607
|
+
: `${relatedTable}.${rel.foreignKey} = ${parentTable}.${rel.localKey || this.modelClass.getPrimaryKey()}`;
|
|
608
|
+
const ops = ['=', '!=', '<', '<=', '>', '>='];
|
|
609
|
+
const safeOp = ops.includes(operator) ? operator : '>=';
|
|
610
|
+
const safeCount = parseInt(count, 10) || 1;
|
|
611
|
+
if (safeOp === '>=' && safeCount === 1) {
|
|
612
|
+
this.query.whereExists((builder) => {
|
|
613
|
+
builder.from(relatedTable).whereRaw(joinCol);
|
|
614
|
+
});
|
|
615
|
+
} else {
|
|
616
|
+
this.query.whereRaw(
|
|
617
|
+
`(select count(*) from ?? where ${joinCol}) ${safeOp} ?`,
|
|
618
|
+
[relatedTable, safeCount]
|
|
619
|
+
);
|
|
620
|
+
}
|
|
621
|
+
} catch (_) {}
|
|
622
|
+
return this;
|
|
623
|
+
}
|
|
624
|
+
|
|
551
625
|
_makeDummy() {
|
|
552
626
|
const dummy = Object.create(this.modelClass.prototype);
|
|
553
627
|
dummy.attributes = {};
|
|
@@ -609,13 +683,13 @@ class QueryBuilder {
|
|
|
609
683
|
|
|
610
684
|
async findOrFail(id) {
|
|
611
685
|
const result = await this.find(id);
|
|
612
|
-
if (!result) throw new
|
|
686
|
+
if (!result) throw new ModelNotFoundException(this.modelClass?.name || 'Model', id);
|
|
613
687
|
return result;
|
|
614
688
|
}
|
|
615
689
|
|
|
616
690
|
async firstOrFail() {
|
|
617
691
|
const result = await this.first();
|
|
618
|
-
if (!result) throw new
|
|
692
|
+
if (!result) throw new ModelNotFoundException(this.modelClass?.name || 'Model');
|
|
619
693
|
return result;
|
|
620
694
|
}
|
|
621
695
|
|
|
@@ -623,6 +697,18 @@ class QueryBuilder {
|
|
|
623
697
|
return !(await this.exists());
|
|
624
698
|
}
|
|
625
699
|
|
|
700
|
+
async sole() {
|
|
701
|
+
const results = await this._softQuery().limit(2);
|
|
702
|
+
if (results.length === 0) throw new ModelNotFoundException(this.modelClass?.name || 'Model');
|
|
703
|
+
if (results.length > 1) throw new Error(`${this.modelClass?.name || 'Model'}: sole() found more than one result.`);
|
|
704
|
+
return this.modelClass ? new this.modelClass(results[0]) : results[0];
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
tap(callback) {
|
|
708
|
+
callback(this);
|
|
709
|
+
return this;
|
|
710
|
+
}
|
|
711
|
+
|
|
626
712
|
async pluck(column) {
|
|
627
713
|
return await this._softQuery().pluck(column);
|
|
628
714
|
}
|
|
@@ -758,6 +844,14 @@ class QueryBuilder {
|
|
|
758
844
|
return this.query.update(data);
|
|
759
845
|
}
|
|
760
846
|
|
|
847
|
+
async increment(column, amount = 1) {
|
|
848
|
+
return this.query.increment(column, amount);
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
async decrement(column, amount = 1) {
|
|
852
|
+
return this.query.decrement(column, amount);
|
|
853
|
+
}
|
|
854
|
+
|
|
761
855
|
async delete() {
|
|
762
856
|
return this.query.del();
|
|
763
857
|
}
|
|
@@ -1033,6 +1127,18 @@ class QueryBuilder {
|
|
|
1033
1127
|
return this;
|
|
1034
1128
|
}
|
|
1035
1129
|
|
|
1130
|
+
async restore() {
|
|
1131
|
+
if (!this.modelClass || !this.modelClass.softDeletes) {
|
|
1132
|
+
throw new Error(`restore() requires softDeletes to be enabled on ${this.modelClass?.name || 'the model'}`);
|
|
1133
|
+
}
|
|
1134
|
+
const col = this.modelClass.deletedAt || 'deleted_at';
|
|
1135
|
+
return this._softQuery().update({ [col]: null });
|
|
1136
|
+
}
|
|
1137
|
+
|
|
1138
|
+
async values() {
|
|
1139
|
+
return this._softQuery();
|
|
1140
|
+
}
|
|
1141
|
+
|
|
1036
1142
|
// Debug
|
|
1037
1143
|
toSql() {
|
|
1038
1144
|
return this.query.toSQL().sql;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ilana-orm",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.19",
|
|
4
4
|
"description": "A fully-featured, Eloquent-style ORM for Node.js with TypeScript support",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"types": "index.d.ts",
|
|
@@ -10,6 +10,11 @@
|
|
|
10
10
|
"require": "./index.js",
|
|
11
11
|
"types": "./index.d.ts"
|
|
12
12
|
},
|
|
13
|
+
"./edge": {
|
|
14
|
+
"import": "./index.edge.mjs",
|
|
15
|
+
"require": "./index.edge.js",
|
|
16
|
+
"types": "./index.d.ts"
|
|
17
|
+
},
|
|
13
18
|
"./orm/Model": {
|
|
14
19
|
"import": "./orm/Model.mjs",
|
|
15
20
|
"require": "./orm/Model.js",
|