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.
@@ -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
- this.query.column(...columns);
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 Error(`Model not found with id: ${id}`);
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 Error('No records found.');
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.17",
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",