@dreamtree-org/korm-js 1.0.50 → 1.0.52

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 +1 @@
1
- const mysqlWrapper=require("./clients/mysql"),sqliteWrapper=require("./clients/sqlite"),pgWrapper=require("./clients/pg"),BaseHelperUtility=require("./BaseHelperUtility"),InstanceMapper={mysql2:mysqlWrapper,sqlite:sqliteWrapper,pg:pgWrapper},dbClientMapper={mysql2:"mysql2",mysql:"mysql2",pg:"pg",postgresql:"pg",sqlite:"sqlite",sqlite3:"sqlite"};class ControllerWrapper{static db=null;static dbClient=null;static dbClientClass=null;static schema=null;static resolverPath=null;static dbInstance=null;requestInstance=null;constructor(){this.requestInstance={}}static initializeKORM({db:t,dbClient:e,schema:s,resolverPath:n=null}){this.db=t,this.dbClient=e,this.schema=s,this.resolverPath=n;const a=dbClientMapper[e];if(!a)throw new Error(`Database client ${e} not found`);const r=InstanceMapper[a];if(!r)throw new Error(`Database client ${e} not found`);return this.dbClientClass=r,this.dbInstance=new r(this),this}static setSchema(t){this.schema=t;const e=this.dbClientClass;if(!e)throw new Error(`Database client ${dbClient} not found`);return this.dbInstance=new e(this),this}static async processRequest(t,e=null,s=null){return await this.dbInstance.processRequest(t,e,s)}static async processRequestWithOthers(t,e=null,s=null){return await this.dbInstance.processRequest(t,e,s)}static async syncDatabase(){return await this.dbInstance.syncDatabase()}static async generateSchema(){return await this.dbInstance.generateSchema()}static loadModelClass(t){return this.dbInstance.hookService.loadModelClass(t)}static getModelInstance(t){return this.dbInstance.hookService.getModelInstance(t)}}module.exports=ControllerWrapper;
1
+ const mysqlWrapper=require("./clients/mysql"),sqliteWrapper=require("./clients/sqlite"),pgWrapper=require("./clients/pg"),BaseHelperUtility=require("./BaseHelperUtility"),InstanceMapper={mysql2:mysqlWrapper,sqlite:sqliteWrapper,pg:pgWrapper},dbClientMapper={mysql2:"mysql2",mysql:"mysql2",pg:"pg",postgresql:"pg",sqlite:"sqlite",sqlite3:"sqlite"};class ControllerWrapper{static db=null;static dbClient=null;static dbClientClass=null;static schema=null;static resolverPath=null;static dbInstance=null;static debug=!1;requestInstance=null;constructor(){this.requestInstance={}}static initializeKORM({db:t,dbClient:e,schema:s,resolverPath:a=null,debug:n=!1}){this.db=t,this.dbClient=e,this.schema=s,this.resolverPath=a,this.debug=n;const r=dbClientMapper[e];if(!r)throw new Error(`Database client ${e} not found`);const i=InstanceMapper[r];if(!i)throw new Error(`Database client ${e} not found`);return this.dbClientClass=i,this.dbInstance=new i(this),this}static setSchema(t){this.schema=t;const e=this.dbClientClass;if(!e)throw new Error(`Database client ${dbClient} not found`);return this.dbInstance=new e(this),this}static async processRequest(t,e=null,s=null){return await this.dbInstance.processRequest(t,e,s)}static async processRequestWithOthers(t,e=null,s=null){return await this.dbInstance.processRequest(t,e,s)}static async syncDatabase(){return await this.dbInstance.syncDatabase()}static async generateSchema(){return await this.dbInstance.generateSchema()}static loadModelClass(t){return this.dbInstance.hookService.loadModelClass(t)}static getModelInstance(t){return this.dbInstance.hookService.getModelInstance(t)}}module.exports=ControllerWrapper;
package/README.md CHANGED
@@ -19,6 +19,9 @@
19
19
  - 🎣 **Model Hooks**: Before, After, Validate, and Custom action hooks
20
20
  - 🔗 **Custom Relations**: Define custom relation hooks for complex data fetching
21
21
  - 📦 **Nested Requests**: Process multiple related requests in a single call
22
+ - 📝 **Logger Utility**: Configurable logging with multiple log levels
23
+ - 🔎 **SQL Debugging**: Debug mode with SQL statement output for troubleshooting
24
+ - 🚫 **NOT EXISTS Queries**: Support for checking absence of related records with `!` prefix
22
25
 
23
26
  ## Installation
24
27
 
@@ -54,7 +57,8 @@ const db = knex({
54
57
  // Initialize KORM with MySQL
55
58
  const korm = initializeKORM({
56
59
  db: db,
57
- dbClient: 'mysql'
60
+ dbClient: 'mysql',
61
+ debug: false // Set to true for SQL debugging
58
62
  });
59
63
 
60
64
  app.listen(3000, () => {
@@ -810,8 +814,172 @@ POST /api/Post/crud
810
814
  },
811
815
  "with": ["Comment"]
812
816
  }
817
+
818
+ // Deeply nested where conditions
819
+ POST /api/Post/crud
820
+ {
821
+ "action": "list",
822
+ "where": {
823
+ "User.UserRole.Role.name": "admin"
824
+ },
825
+ "with": ["User", "User.UserRole", "User.UserRole.Role"]
826
+ }
827
+ // SQL: WHERE EXISTS (SELECT * FROM users WHERE users.id = posts.user_id
828
+ // AND EXISTS (SELECT * FROM user_roles WHERE user_roles.user_id = users.id
829
+ // AND EXISTS (SELECT * FROM roles WHERE roles.id = user_roles.role_id AND name = 'admin')))
830
+ ```
831
+
832
+ #### NOT EXISTS Queries with `!` Prefix
833
+
834
+ Use the `!` prefix on relation names to check for the absence of related records:
835
+
836
+ ```javascript
837
+ // Get posts where the user has NO UserRole entries
838
+ POST /api/Post/crud
839
+ {
840
+ "action": "list",
841
+ "where": {
842
+ "User.!UserRole": true
843
+ },
844
+ "with": ["User"]
845
+ }
846
+ // SQL: WHERE EXISTS (SELECT * FROM users WHERE users.id = posts.user_id
847
+ // AND NOT EXISTS (SELECT * FROM user_roles WHERE user_roles.user_id = users.id))
848
+
849
+ // Get users with no posts
850
+ POST /api/User/crud
851
+ {
852
+ "action": "list",
853
+ "where": {
854
+ "!Post": true
855
+ }
856
+ }
857
+ // SQL: WHERE NOT EXISTS (SELECT * FROM posts WHERE posts.user_id = users.id)
858
+
859
+ // Get posts where user has no admin role
860
+ POST /api/Post/crud
861
+ {
862
+ "action": "list",
863
+ "where": {
864
+ "User.!UserRole.Role.name": "admin"
865
+ }
866
+ }
867
+ // SQL: WHERE EXISTS (SELECT * FROM users WHERE users.id = posts.user_id
868
+ // AND NOT EXISTS (SELECT * FROM user_roles WHERE user_roles.user_id = users.id
869
+ // AND EXISTS (SELECT * FROM roles WHERE roles.id = user_roles.role_id AND name = 'admin')))
870
+ ```
871
+
872
+ **NOT EXISTS Syntax Summary:**
873
+
874
+ | Pattern | Meaning | Use Case |
875
+ |---------|---------|----------|
876
+ | `"!RelName": true` | NOT EXISTS on direct relation | Users with no posts |
877
+ | `"Parent.!Child": true` | NOT EXISTS on nested relation | Posts where user has no roles |
878
+ | `"Parent.!Child.column": value` | NOT EXISTS with condition | Posts where user has no admin role |
879
+ ```
880
+
881
+ #### Filtering Eager Loaded Relations with `withWhere`
882
+
883
+ The `withWhere` parameter allows you to apply additional where conditions when fetching related data. This filters the related rows without affecting the parent query.
884
+
885
+ ```javascript
886
+ // Get all users with their posts, but only posts from user_id = 15
887
+ POST /api/User/crud
888
+ {
889
+ "action": "list",
890
+ "with": ["Post"],
891
+ "withWhere": {
892
+ "Post.user_id": 15
893
+ }
894
+ }
895
+
896
+ // Get users with active posts only
897
+ POST /api/User/crud
898
+ {
899
+ "action": "list",
900
+ "with": ["Post"],
901
+ "withWhere": {
902
+ "Post.status": "active",
903
+ "Post.is_published": true
904
+ }
905
+ }
906
+
907
+ // Multiple relations with different filters
908
+ POST /api/User/crud
909
+ {
910
+ "action": "list",
911
+ "with": ["Post", "Comment"],
912
+ "withWhere": {
913
+ "Post.status": "published",
914
+ "Post.created_at": ">=2024-01-01",
915
+ "Comment.is_approved": true
916
+ }
917
+ }
918
+
919
+ // Using operators in withWhere (same as regular where)
920
+ POST /api/User/crud
921
+ {
922
+ "action": "list",
923
+ "with": ["Post"],
924
+ "withWhere": {
925
+ "Post.title": "%tutorial%",
926
+ "Post.views": ">=100",
927
+ "Post.category": "[]tech,programming"
928
+ }
929
+ }
930
+
931
+ // Using OR conditions in withWhere
932
+ POST /api/User/crud
933
+ {
934
+ "action": "list",
935
+ "with": ["Post"],
936
+ "withWhere": {
937
+ "Post.status": "published",
938
+ "Or:Post.is_featured": true
939
+ }
940
+ }
813
941
  ```
814
942
 
943
+ **Key differences between `where` and `withWhere`:**
944
+
945
+ | Feature | `where` (Nested) | `withWhere` |
946
+ |---------|-----------------|-------------|
947
+ | Affects parent query | Yes (filters parent rows) | No (only filters related rows) |
948
+ | Purpose | Filter parents that have matching relations | Filter which related rows are loaded |
949
+ | SQL generated | `WHERE EXISTS (subquery)` | Additional `WHERE` on relation query |
950
+
951
+ **Example showing the difference:**
952
+
953
+ ```javascript
954
+ // This filters users who have published posts (affects which users are returned)
955
+ {
956
+ "action": "list",
957
+ "where": { "Post.status": "published" },
958
+ "with": ["Post"]
959
+ }
960
+ // Returns: Only users who have at least one published post
961
+ // Each user's Post array contains ALL their posts (not filtered)
962
+
963
+ // This loads all users but only their published posts
964
+ {
965
+ "action": "list",
966
+ "with": ["Post"],
967
+ "withWhere": { "Post.status": "published" }
968
+ }
969
+ // Returns: All users
970
+ // Each user's Post array contains ONLY their published posts
971
+ ```
972
+
973
+ **withWhere supports all operators:**
974
+ - Comparison: `">=18"`, `"<=100"`, `">50"`, `"<10"`, `"!deleted"`
975
+ - LIKE: `"%pattern%"`
976
+ - IN: `"[]val1,val2,val3"`
977
+ - NOT IN: `"![]val1,val2"`
978
+ - BETWEEN: `"><min,max"`
979
+ - NOT BETWEEN: `"<>min,max"`
980
+ - NULL: `null`
981
+ - OR prefix: `"Or:column"`
982
+
815
983
  ### Example: Complete Relationship Structure
816
984
 
817
985
  Example complete relationship structure:
@@ -1385,7 +1553,8 @@ const korm = initializeKORM({
1385
1553
  db: db, // Knex database instance
1386
1554
  dbClient: 'mysql', // 'mysql', 'mysql2', 'pg', 'postgresql', 'sqlite', 'sqlite3'
1387
1555
  schema: null, // Optional: initial schema object
1388
- resolverPath: null // Optional: path to models directory (default: process.cwd())
1556
+ resolverPath: null, // Optional: path to models directory (default: process.cwd())
1557
+ debug: false // Optional: enable SQL debugging (default: false)
1389
1558
  });
1390
1559
 
1391
1560
  // Process any CRUD request (automatically handles other_requests if present)
@@ -1423,6 +1592,7 @@ const modelInstance = korm.getModelInstance(modelDef);
1423
1592
  | `offset` | number | Records to skip |
1424
1593
  | `page` | number | Page number (alternative to offset) |
1425
1594
  | `with` | array | Related models to eager load |
1595
+ | `withWhere` | object | Filter conditions for eager loaded relations |
1426
1596
  | `groupBy` | array/string | Group by columns |
1427
1597
  | `having` | object | Having conditions |
1428
1598
  | `distinct` | boolean/array/string | Distinct results |
@@ -1486,6 +1656,7 @@ const {
1486
1656
  helperUtility, // Utility functions (file operations, string manipulation)
1487
1657
  emitter, // Event emitter instance
1488
1658
  validate, // Validation function
1659
+ logger, // Logger utility instance
1489
1660
  lib, // Additional utilities
1490
1661
  LibClasses // Library classes (Emitter)
1491
1662
  } = require('@dreamtree-org/korm-js');
@@ -1504,6 +1675,197 @@ const {
1504
1675
  // - createDirectory(path) - Create directory
1505
1676
  ```
1506
1677
 
1678
+ ## Logger Utility
1679
+
1680
+ KORM includes a built-in logger utility with configurable log levels for debugging and monitoring.
1681
+
1682
+ ### Basic Usage
1683
+
1684
+ ```javascript
1685
+ const { logger } = require('@dreamtree-org/korm-js');
1686
+
1687
+ // Log methods (from most to least verbose)
1688
+ logger.debug('Detailed debugging information');
1689
+ logger.log('General log message');
1690
+ logger.info('Informational message');
1691
+ logger.warn('Warning message');
1692
+ logger.error('Error message');
1693
+ ```
1694
+
1695
+ ### Log Levels
1696
+
1697
+ | Level | Value | Description |
1698
+ |-------|-------|-------------|
1699
+ | `NONE` | 0 | Disable all logging |
1700
+ | `ERROR` | 1 | Only errors |
1701
+ | `WARN` | 2 | Warnings and errors (default) |
1702
+ | `INFO` | 3 | Info, warnings, and errors |
1703
+ | `LOG` | 4 | General logs and above |
1704
+ | `DEBUG` | 5 | All messages (most verbose) |
1705
+
1706
+ ### Configuration
1707
+
1708
+ ```javascript
1709
+ const { logger } = require('@dreamtree-org/korm-js');
1710
+
1711
+ // Set log level programmatically
1712
+ logger.setLevel('debug'); // Show all logs
1713
+ logger.setLevel('warn'); // Only warnings and errors (default)
1714
+ logger.setLevel('error'); // Only errors
1715
+ logger.setLevel('none'); // Disable all logging
1716
+
1717
+ // Enable/disable logging
1718
+ logger.disable(); // Temporarily disable all logging
1719
+ logger.enable(); // Re-enable logging
1720
+ ```
1721
+
1722
+ ### Environment Variable
1723
+
1724
+ Set the log level via environment variable:
1725
+
1726
+ ```bash
1727
+ # In your .env file or environment
1728
+ KORM_LOG_LEVEL=debug # Show all logs
1729
+ KORM_LOG_LEVEL=warn # Only warnings and errors (default)
1730
+ KORM_LOG_LEVEL=error # Only errors
1731
+ KORM_LOG_LEVEL=none # Disable logging
1732
+ ```
1733
+
1734
+ ```javascript
1735
+ // The logger automatically reads KORM_LOG_LEVEL on initialization
1736
+ // KORM_LOG_LEVEL=debug node app.js
1737
+ ```
1738
+
1739
+ ### Creating Child Loggers
1740
+
1741
+ Create child loggers with custom prefixes for different modules:
1742
+
1743
+ ```javascript
1744
+ const { logger } = require('@dreamtree-org/korm-js');
1745
+
1746
+ // Create a child logger for a specific module
1747
+ const authLogger = logger.child('[Auth]');
1748
+ authLogger.info('User logged in'); // Output: [KORM][Auth] [INFO] User logged in
1749
+
1750
+ const dbLogger = logger.child('[DB]');
1751
+ dbLogger.debug('Query executed'); // Output: [KORM][DB] [DEBUG] Query executed
1752
+ ```
1753
+
1754
+ ### Logger Options
1755
+
1756
+ ```javascript
1757
+ const { Logger } = require('@dreamtree-org/korm-js').logger;
1758
+
1759
+ // Create a custom logger instance
1760
+ const customLogger = new Logger({
1761
+ prefix: '[MyApp]', // Custom prefix (default: '[KORM]')
1762
+ enabled: true, // Enable/disable logging (default: true)
1763
+ level: 'debug', // Log level (default: 'warn' or KORM_LOG_LEVEL)
1764
+ timestamps: true // Include timestamps (default: false)
1765
+ });
1766
+
1767
+ customLogger.info('Application started');
1768
+ // Output: [MyApp] [2024-01-15T10:30:00.000Z] [INFO] Application started
1769
+ ```
1770
+
1771
+ ### Usage in Development vs Production
1772
+
1773
+ ```javascript
1774
+ // Development: Enable debug logging
1775
+ // KORM_LOG_LEVEL=debug node app.js
1776
+
1777
+ // Production: Only show warnings and errors
1778
+ // KORM_LOG_LEVEL=warn node app.js
1779
+
1780
+ // Or configure programmatically
1781
+ const { logger } = require('@dreamtree-org/korm-js');
1782
+
1783
+ if (process.env.NODE_ENV === 'development') {
1784
+ logger.setLevel('debug');
1785
+ } else {
1786
+ logger.setLevel('warn');
1787
+ }
1788
+ ```
1789
+
1790
+ ## SQL Debugging
1791
+
1792
+ Enable SQL debugging to see the exact SQL statements generated by your queries. This is useful for troubleshooting complex queries and understanding how KORM translates your requests.
1793
+
1794
+ ### Enabling Debug Mode
1795
+
1796
+ ```javascript
1797
+ const { initializeKORM } = require('@dreamtree-org/korm-js');
1798
+
1799
+ const korm = initializeKORM({
1800
+ db: db,
1801
+ dbClient: 'mysql',
1802
+ debug: true // Enable SQL debugging
1803
+ });
1804
+ ```
1805
+
1806
+ ### Debug Output
1807
+
1808
+ When `debug: true`, list queries will include a `sqlDebug` array in the response:
1809
+
1810
+ ```javascript
1811
+ // Request
1812
+ POST /api/Post/crud
1813
+ {
1814
+ "action": "list",
1815
+ "where": {
1816
+ "User.!UserRole": true
1817
+ },
1818
+ "limit": 5
1819
+ }
1820
+
1821
+ // Response with debug: true
1822
+ {
1823
+ "data": [...],
1824
+ "totalCount": 10,
1825
+ "sqlDebug": [
1826
+ "select * from `posts` where exists (select * from `users` where `users`.`id` = `posts`.`user_id` and not exists (select * from `user_roles` where `user_roles`.`user_id` = `users`.`id`)) order by `id` asc limit ?",
1827
+ "select count(*) as `cnt` from `posts` where exists (select * from `users` where `users`.`id` = `posts`.`user_id` and not exists (select * from `user_roles` where `user_roles`.`user_id` = `users`.`id`))"
1828
+ ],
1829
+ "pagination": {
1830
+ "page": 1,
1831
+ "limit": 5,
1832
+ "offset": 0,
1833
+ "totalPages": 2,
1834
+ "hasNext": true,
1835
+ "hasPrev": false,
1836
+ "nextPage": 2,
1837
+ "prevPage": null
1838
+ }
1839
+ }
1840
+ ```
1841
+
1842
+ ### What's Included in sqlDebug
1843
+
1844
+ | Index | Query Type | Description |
1845
+ |-------|------------|-------------|
1846
+ | 0 | Main Query | The primary SELECT query with all WHERE, ORDER BY, LIMIT clauses |
1847
+ | 1 | Count Query | The COUNT query used for pagination (only when limit > 0) |
1848
+
1849
+ ### Debug Mode Best Practices
1850
+
1851
+ ```javascript
1852
+ // Development: Enable debug mode
1853
+ const korm = initializeKORM({
1854
+ db: db,
1855
+ dbClient: 'mysql',
1856
+ debug: process.env.NODE_ENV === 'development'
1857
+ });
1858
+
1859
+ // Or use environment variable
1860
+ const korm = initializeKORM({
1861
+ db: db,
1862
+ dbClient: 'mysql',
1863
+ debug: process.env.KORM_DEBUG === 'true'
1864
+ });
1865
+ ```
1866
+
1867
+ **Note:** The `sqlDebug` field is only included in responses when `debug: true`. In production, set `debug: false` to exclude SQL statements from responses.
1868
+
1507
1869
  ## Database Support
1508
1870
 
1509
1871
  ### MySQL (Current Guide)
@@ -1524,7 +1886,8 @@ const db = knex({
1524
1886
 
1525
1887
  const korm = initializeKORM({
1526
1888
  db: db,
1527
- dbClient: 'mysql'
1889
+ dbClient: 'mysql',
1890
+ debug: process.env.NODE_ENV === 'development' // Enable SQL debugging in development
1528
1891
  });
1529
1892
  ```
1530
1893
 
@@ -1546,7 +1909,8 @@ const db = knex({
1546
1909
 
1547
1910
  const korm = initializeKORM({
1548
1911
  db: db,
1549
- dbClient: 'pg'
1912
+ dbClient: 'pg',
1913
+ debug: process.env.NODE_ENV === 'development'
1550
1914
  });
1551
1915
  ```
1552
1916
 
@@ -1564,7 +1928,8 @@ const db = knex({
1564
1928
 
1565
1929
  const korm = initializeKORM({
1566
1930
  db: db,
1567
- dbClient: 'sqlite'
1931
+ dbClient: 'sqlite',
1932
+ debug: process.env.NODE_ENV === 'development'
1568
1933
  });
1569
1934
  ```
1570
1935
 
@@ -1642,7 +2007,8 @@ const db = knex({
1642
2007
  // Initialize KORM
1643
2008
  const korm = initializeKORM({
1644
2009
  db: db,
1645
- dbClient: 'mysql'
2010
+ dbClient: 'mysql',
2011
+ debug: process.env.NODE_ENV === 'development' // SQL debugging in dev mode
1646
2012
  });
1647
2013
 
1648
2014
  // Initialize app
@@ -1 +1 @@
1
- const HelperUtility=require("./HelperUtility"),logger=require("../../Logger");class QueryBuilder{constructor(e,t,r=null){this.db=e,this.utils=t,this.controllerWrapper=r,this.helperUtility=new HelperUtility}getHookService(){return this.controllerWrapper.hookService}getQueryBuilder(e,t=null){let r=this.db(e.table);return t&&(r=t),r._getMyModel=()=>e,r}parseValue(e){return this.helperUtility.parseValue(e)}parseWhereValue(e){return this.helperUtility.parseWhereValue(e)}parseWhereColumn(e){return this.helperUtility.parseWhereColumn(e)}_applyOrWhereCondition(e,t,r,i){if(null!==i)if(Array.isArray(i))e.orWhereIn(t,i);else switch(r){case"between":e.orWhereBetween(t,i);break;case"notBetween":e.orWhereNotBetween(t,i);break;case"in":e.orWhereIn(t,i);break;case"notIn":e.orWhereNotIn(t,i);break;case"like":e.orWhereRaw(`\`${t}\` LIKE ?`,[i]);break;default:e.orWhere(t,r,i)}else e.orWhereNull(t)}_applyAndWhereCondition(e,t,r,i){if(null!==i)if(Array.isArray(i))e.whereIn(t,i);else switch(r){case"between":e.whereBetween(t,i);break;case"notBetween":e.whereNotBetween(t,i);break;case"in":e.whereIn(t,i);break;case"notIn":e.whereNotIn(t,i);break;case"like":e.whereRaw(`\`${t}\` LIKE ?`,[i]);break;default:e.where(t,r,i)}else e.whereNull(t)}buildWithTree(e,t=null){return this.helperUtility.dotWalkTree(e,{resolver:({current:e,part:r,source:i})=>t?t({current:e,part:r,source:i}):{}})}_getWithWhereForRelation(e,t){if(!e||"object"!=typeof e)return{};const r=`${t}.`,i={};for(const[t,o]of Object.entries(e))if(t.startsWith(r)){i[t.slice(r.length)]=o}return i}_applyWithWhereConditions(e,t){for(const[r,i]of Object.entries(t)){const{joinType:t="AND",column:o}=this.parseWhereColumn(r),{operator:l,value:a}=this.parseWhereValue(i);"AND"===t?this._applyAndWhereCondition(e,o,l,a):this._applyOrWhereCondition(e,o,l,a)}}async fetchRelatedRows(e,t,r={}){if(e.through){let i=await this.db(e.through).whereIn(e.throughLocalKey,t),o=this.db(e.table).whereIn(e.foreignKey,i.map(t=>t[e.throughForeignKey]));return Object.keys(r).length>0&&this._applyWithWhereConditions(o,r),o}{let i=this.db(e.table).whereIn(e.foreignKey,t);return Object.keys(r).length>0&&this._applyWithWhereConditions(i,r),i}}async fetchAndAttachRelated(e){const{parentRows:t,relName:r,model:i,withTree:o,relation:l,withWhere:a={}}=e;if(!l){const e=this.getHookService().getModelInstance(i),l=`get${r.charAt(0).toUpperCase()+r.slice(1)}Relation`;if("function"==typeof e[l]){const s={rows:t,relName:r,model:i,withTree:o,controller:this.controllerWrapper,relation:i.hasRelations[r],qb:this,db:this.db,withWhere:a};await e[l](s)}return t}let s=t.map(e=>e[l.localKey]),n=[];const h="one"===l?.type,p=this._getWithWhereForRelation(a,r);n=await this.fetchRelatedRows(l,s,p);let c=new Map;for(const e of n){let t=e[l.foreignKey];c.has(t)||c.set(t,[]),c.get(t).push(e)}for(const e of t){let t=e[l.localKey];h&&1==c.get(t)?.length?e[r]=c.get(t)[0]:e[r]=c.get(t)||[]}let y=Object.keys(o);for(const e of y){let i=o[e],l=t.filter(e=>h?e[r]:e[r].length>0).map(e=>e[r]).reduce((e,t)=>e.concat(t),[]),s=this.utils.getModel(this.controllerWrapper,r);await this.fetchAndAttachRelated({parentRows:l,relName:e,model:s,withTree:i,relation:s.hasRelations[e],withWhere:a})}return t}filterWhere(e,t=""){return t?Object.keys(e).filter(e=>e.includes(t)).reduce((r,i)=>(r[i.replace(t,"")]=e[i],r),{}):Object.keys(e).filter(e=>!e.includes(".")).reduce((t,r)=>(t[r]=e[r],t),{})}async getQuery(e,t){try{const{where:r={},with:i,withWhere:o,select:l,orderBy:a={column:"id",direction:"asc"},limit:s=10,offset:n=0,page:h,groupBy:p,having:c,distinct:y,join:u,leftJoin:f,rightJoin:g,innerJoin:d,count:W=!1}=t;let w=this.getQueryBuilder(e);l&&(Array.isArray(l)||"string"==typeof l)?w.select(l):w.select("*"),y&&(Array.isArray(y)||"string"==typeof y?w.distinct(y):w.distinct()),u&&this._applyJoins(w,u,"join"),f&&this._applyJoins(w,f,"leftJoin"),g&&this._applyJoins(w,g,"rightJoin"),d&&this._applyJoins(w,d,"innerJoin"),this._applyWhereClause(w,r,i),p&&(Array.isArray(p),w.groupBy(p)),c&&this._applyHavingClause(w,c),a&&this._applyOrderBy(w,a);let b=!1,_=s,A=n,m=1,k=0;h&&s>0&&(m=Math.max(1,parseInt(h)),A=(m-1)*s),s>0&&(w.limit(_),A>0&&w.offset(A));const j=await w;if(i&&i.length>0){const t=this.buildWithTree(i);for(const r of Object.keys(t))await this.fetchAndAttachRelated({parentRows:j,relName:r,model:e,withTree:t[r],relation:e.hasRelations[r],withWhere:o||{}})}let C=null;if(s>0)try{let t=this.getQueryBuilder(e);r&&Object.keys(r).length>0&&this._applyWhereClause(t,r),u&&this._applyJoins(t,u,"join"),f&&this._applyJoins(t,f,"leftJoin"),g&&this._applyJoins(t,g,"rightJoin"),d&&this._applyJoins(t,d,"innerJoin");C=(await t.count("* as cnt").first()).cnt}catch(e){logger.warn("Failed to get total count:",e.message),C=j.length}s>0&&null!==C&&(k=Math.ceil(C/s),b=m<k);return{data:j,totalCount:C,...s>0?{pagination:{page:m,limit:_,offset:A,totalPages:k,hasNext:b,hasPrev:m>1,nextPage:b?m+1:null,prevPage:m>1?m-1:null}}:{}}}catch(e){throw logger.error("QueryService.getQuery error:",e),new Error(`Failed to execute query: ${e.message}`)}}_applyWhereWithArray(e,t,r){for(const i of t)this._applyWhereClause(e,i,r)}_applyWhereClause(e,t,r=[]){if(Array.isArray(t))this._applyWhereWithArray(e,t,r);else{if(t&&Object.keys(t).length>0){let r=this.helperUtility.getDotWalkQuery(t);logger.debug({filteredWhere:r}),r=this.helperUtility.objectFilter(r,(e,t)=>"object"!=typeof t||null===t);for(const[t,i]of Object.entries(r)){const{joinType:r="AND",column:o}=this.parseWhereColumn(t),{operator:l,value:a}=this.parseWhereValue(i);"AND"===r?this._applyAndWhereCondition(e,o,l,a):this._applyOrWhereCondition(e,o,l,a)}}r&&r.length>0&&this._applyNestedWhere(e,t,r)}}_applyNestedWhere(e,t,r){let i=this,o=e._getMyModel();if(r&&r.length>0)for(const l of r){let a=this.helperUtility.getDotWalkQuery(t,l);if(a&&Object.keys(a).length>0){let t=this.utils.getModel(this.controllerWrapper,l),s=o.hasRelations[l],n=r.map(e=>this.helperUtility.pluckDotWalkKey(e,1));e.whereExists(function(){let e=i.getQueryBuilder(t,this.select("*").from(t.table));e.whereRaw(`${s.foreignKey} = ${o.table}.${s.localKey}`),i._applyWhereClause(e,a,n)})}}}_applyJoins(e,t,r){const i=Array.isArray(t)?t:[t];for(const t of i)"string"==typeof t?e[r](t):"object"==typeof t&&(t.table&&t.on?e[r](t.table,t.on):t.table&&t.first&&t.operator&&t.second&&e[r](t.table,t.first,t.operator,t.second))}_applyWithWhere(e,t){try{if(Array.isArray(t))for(const r of t)"string"==typeof r?e.withWhere(r):"object"==typeof r&&e.withWhere(r.column,r.operator,r.value);else if("object"==typeof t)for(const[r,i]of Object.entries(t))e.withWhere(r,i)}catch(e){logger.warn("Failed to apply withWhere:",e.message)}}_applyHavingClause(e,t){for(const[r,i]of Object.entries(t))"object"==typeof i&&i.operator?e.having(r,i.operator,i.value):e.having(r,i)}_applyOrderBy(e,t){if(Array.isArray(t))for(const r of t)"string"==typeof r?e.orderBy(r):"object"==typeof r&&e.orderBy(r.column,r.direction||"asc");else"string"==typeof t?e.orderBy(t):"object"==typeof t&&e.orderBy(t.column,t.direction||"asc")}}module.exports=QueryBuilder;
1
+ const HelperUtility=require("./HelperUtility"),logger=require("../../Logger");class QueryBuilder{constructor(e,t,r=null){this.db=e,this.utils=t,this.controllerWrapper=r,this.helperUtility=new HelperUtility}getHookService(){return this.controllerWrapper.hookService}getQueryBuilder(e,t=null){let r=this.db(e.table);return t&&(r=t),r._getMyModel=()=>e,r}parseValue(e){return this.helperUtility.parseValue(e)}parseWhereValue(e){return this.helperUtility.parseWhereValue(e)}parseWhereColumn(e){return this.helperUtility.parseWhereColumn(e)}_applyOrWhereCondition(e,t,r,o){if(null!==o)if(Array.isArray(o))e.orWhereIn(t,o);else switch(r){case"between":e.orWhereBetween(t,o);break;case"notBetween":e.orWhereNotBetween(t,o);break;case"in":e.orWhereIn(t,o);break;case"notIn":e.orWhereNotIn(t,o);break;case"like":e.orWhereRaw(`\`${t}\` LIKE ?`,[o]);break;default:e.orWhere(t,r,o)}else e.orWhereNull(t)}_applyAndWhereCondition(e,t,r,o){if(null!==o)if(Array.isArray(o))e.whereIn(t,o);else switch(r){case"between":e.whereBetween(t,o);break;case"notBetween":e.whereNotBetween(t,o);break;case"in":e.whereIn(t,o);break;case"notIn":e.whereNotIn(t,o);break;case"like":e.whereRaw(`\`${t}\` LIKE ?`,[o]);break;default:e.where(t,r,o)}else e.whereNull(t)}buildWithTree(e,t=null){return this.helperUtility.dotWalkTree(e,{resolver:({current:e,part:r,source:o})=>t?t({current:e,part:r,source:o}):{}})}_getWithWhereForRelation(e,t){if(!e||"object"!=typeof e)return{direct:{},nested:{}};const r=`${t}.`,o={},i={};for(const[t,s]of Object.entries(e))if(t.startsWith(r)){const e=t.slice(r.length);e.includes(".")?i[e]=s:o[e]=s}return{direct:o,nested:i}}_applyWithWhereConditions(e,t){for(const[r,o]of Object.entries(t)){const{joinType:t="AND",column:i}=this.parseWhereColumn(r),{operator:s,value:l}=this.parseWhereValue(o);"AND"===t?this._applyAndWhereCondition(e,i,s,l):this._applyOrWhereCondition(e,i,s,l)}}async fetchRelatedRows(e,t,r={}){if(e.through){let o=await this.db(e.through).whereIn(e.throughLocalKey,t),i=this.db(e.table).whereIn(e.foreignKey,o.map(t=>t[e.throughForeignKey]));return Object.keys(r).length>0&&this._applyWithWhereConditions(i,r),i}{let o=this.db(e.table).whereIn(e.foreignKey,t);return Object.keys(r).length>0&&this._applyWithWhereConditions(o,r),o}}async fetchAndAttachRelated(e){const{parentRows:t,relName:r,model:o,withTree:i,relation:s,withWhere:l={}}=e;if(!s){const e=this.getHookService().getModelInstance(o),s=`get${r.charAt(0).toUpperCase()+r.slice(1)}Relation`;if("function"==typeof e[s]){const{direct:n,nested:a}=this._getWithWhereForRelation(l,r),h={rows:t,relName:r,model:o,withTree:i,controller:this.controllerWrapper,relation:o.hasRelations[r],qb:this,db:this.db,withWhere:n,nestedWithWhere:a};await e[s](h)}else logger.warn(`Method ${s} not found in model ${o.name}`);return t}let n=t.map(e=>e[s.localKey]),a=[];const h="one"===s?.type,{direct:c,nested:p}=this._getWithWhereForRelation(l,r);a=await this.fetchRelatedRows(s,n,c);let y=new Map;for(const e of a){let t=e[s.foreignKey];y.has(t)||y.set(t,[]),y.get(t).push(e)}for(const e of t){let t=e[s.localKey];h&&1==y.get(t)?.length?e[r]=y.get(t)[0]:e[r]=y.get(t)||[]}let u=Object.keys(i);for(const e of u){let o=i[e],s=t.filter(e=>h?e[r]:e[r].length>0).map(e=>e[r]).reduce((e,t)=>e.concat(t),[]),l=this.utils.getModel(this.controllerWrapper,r);await this.fetchAndAttachRelated({parentRows:s,relName:e,model:l,withTree:o,relation:l.hasRelations[e],withWhere:p})}return t}filterWhere(e,t=""){return t?Object.keys(e).filter(e=>e.includes(t)).reduce((r,o)=>(r[o.replace(t,"")]=e[o],r),{}):Object.keys(e).filter(e=>!e.includes(".")).reduce((t,r)=>(t[r]=e[r],t),{})}async getQuery(e,t){try{const{where:r={},with:o,withWhere:i,select:s,orderBy:l={column:"id",direction:"asc"},limit:n=10,offset:a=0,page:h,groupBy:c,having:p,distinct:y,join:u,leftJoin:d,rightJoin:f,innerJoin:g,count:W=!1}=t;let b=this.getQueryBuilder(e);s&&(Array.isArray(s)||"string"==typeof s)?b.select(s):b.select("*"),y&&(Array.isArray(y)||"string"==typeof y?b.distinct(y):b.distinct()),u&&this._applyJoins(b,u,"join"),d&&this._applyJoins(b,d,"leftJoin"),f&&this._applyJoins(b,f,"rightJoin"),g&&this._applyJoins(b,g,"innerJoin"),this._applyWhereClause(b,r,o),c&&(Array.isArray(c),b.groupBy(c)),p&&this._applyHavingClause(b,p),l&&this._applyOrderBy(b,l);let w=!1,_=n,m=a,A=1,j=0;h&&n>0&&(A=Math.max(1,parseInt(h)),m=(A-1)*n),n>0&&(b.limit(_),m>0&&b.offset(m));const k=[],C=b.toSQL();k.push(C.sql);const R=await b;if(o&&o.length>0){const t=this.buildWithTree(o);for(const r of Object.keys(t))await this.fetchAndAttachRelated({parentRows:R,relName:r,model:e,withTree:t[r],relation:e.hasRelations[r],withWhere:i||{}})}let O=null;if(n>0)try{let t=this.getQueryBuilder(e);r&&Object.keys(r).length>0&&this._applyWhereClause(t,r),u&&this._applyJoins(t,u,"join"),d&&this._applyJoins(t,d,"leftJoin"),f&&this._applyJoins(t,f,"rightJoin"),g&&this._applyJoins(t,g,"innerJoin");const o=t.count("* as cnt");k.push(o.toSQL().sql);O=(await o.first()).cnt}catch(e){logger.warn("Failed to get total count:",e.message),O=R.length}n>0&&null!==O&&(j=Math.ceil(O/n),w=A<j);const B=w?A+1:null,N=A>1?A-1:null;return{data:R,totalCount:O,...this.controllerWrapper.debug?{sqlDebug:k}:{},...n>0?{pagination:{page:A,limit:_,offset:m,totalPages:j,hasNext:w,hasPrev:A>1,nextPage:B,prevPage:N}}:{}}}catch(t){throw logger.error("QueryService.getQuery error:",t),new Error(`Failed to execute query: ${t.message} on model ${e.name}`)}}_applyWhereWithArray(e,t,r){for(const o of t)this._applyWhereClause(e,o,r)}_applyWhereClause(e,t,r=[]){if(Array.isArray(t))this._applyWhereWithArray(e,t,r);else{if(t&&Object.keys(t).length>0){let r=this.helperUtility.getDotWalkQuery(t);logger.debug({filteredWhere:r}),r=this.helperUtility.objectFilter(r,(e,t)=>!e.startsWith("!")&&("__exists__"!==e&&("object"!=typeof t||null===t)));for(const[t,o]of Object.entries(r)){const{joinType:r="AND",column:i}=this.parseWhereColumn(t),{operator:s,value:l}=this.parseWhereValue(o);"AND"===r?this._applyAndWhereCondition(e,i,s,l):this._applyOrWhereCondition(e,i,s,l)}}this._applyNestedWhere(e,t,r)}}_getNestedWhereConditions(e,t){if(!e||"object"!=typeof e)return{};const r=`${t}.`,o={};for(const[i,s]of Object.entries(e))if(i.startsWith(r)){o[i.slice(r.length)]=s}else i===t&&!0===s&&(o.__exists__=!0);return o}_getTopLevelRelationsFromWhere(e){if(!e||"object"!=typeof e)return[];const t=new Set;for(const r of Object.keys(e))if(r.includes(".")){const e=r.split(".")[0];t.add(e)}else r.startsWith("!")&&t.add(r);return[...t]}_applyNestedWhere(e,t,r){let o=this,i=e._getMyModel();const s=this._getTopLevelRelationsFromWhere(t);if(0!==s.length)for(let r of s){let s=r.startsWith("!"),l=s?r.slice(1):r,n=this._getNestedWhereConditions(t,r);if(0===Object.keys(n).length)continue;let a,h=i.hasRelations?.[l];if(!h){logger.warn(`Relation ${l} not found in model ${i.modelName}`);continue}try{a=this.utils.getModel(this.controllerWrapper,h.table||l)}catch(e){try{a=this.utils.getModel(this.controllerWrapper,l)}catch(e){logger.warn(`Model for relation ${l} (table: ${h.table}) not found`);continue}}e[s?"whereNotExists":"whereExists"](function(){let e=o.getQueryBuilder(a,this.select("*").from(h.table));e.whereRaw(`\`${h.table}\`.\`${h.foreignKey}\` = \`${i.table}\`.\`${h.localKey}\``);if(!(!0===n.__exists__&&1===Object.keys(n).length)){const t={...n};delete t.__exists__,o._applyWhereClause(e,t,[])}})}}_applyJoins(e,t,r){const o=Array.isArray(t)?t:[t];for(const t of o)"string"==typeof t?e[r](t):"object"==typeof t&&(t.table&&t.on?e[r](t.table,t.on):t.table&&t.first&&t.operator&&t.second&&e[r](t.table,t.first,t.operator,t.second))}_applyWithWhere(e,t){try{if(Array.isArray(t))for(const r of t)"string"==typeof r?e.withWhere(r):"object"==typeof r&&e.withWhere(r.column,r.operator,r.value);else if("object"==typeof t)for(const[r,o]of Object.entries(t))e.withWhere(r,o)}catch(e){logger.warn("Failed to apply withWhere:",e.message)}}_applyHavingClause(e,t){for(const[r,o]of Object.entries(t))"object"==typeof o&&o.operator?e.having(r,o.operator,o.value):e.having(r,o)}_applyOrderBy(e,t){if(Array.isArray(t))for(const r of t)"string"==typeof r?e.orderBy(r):"object"==typeof r&&e.orderBy(r.column,r.direction||"asc");else"string"==typeof t?e.orderBy(t):"object"==typeof t&&e.orderBy(t.column,t.direction||"asc")}}module.exports=QueryBuilder;
@@ -1 +1 @@
1
- const HelperUtility=require("./HelperUtility"),logger=require("../../Logger");class QueryBuilder{constructor(e,t,r=null){this.db=e,this.utils=t,this.controllerWrapper=r,this.helperUtility=new HelperUtility}getHookService(){return this.controllerWrapper.hookService}getQueryBuilder(e,t=null){let r=this.db(e.table);return t&&(r=t),r._getMyModel=()=>e,r}parseValue(e){return this.helperUtility.parseValue(e)}parseWhereValue(e){return this.helperUtility.parseWhereValue(e)}parseWhereColumn(e){return this.helperUtility.parseWhereColumn(e)}_applyOrWhereCondition(e,t,r,i){if(null!==i)if(Array.isArray(i))e.orWhereIn(t,i);else switch(r){case"between":e.orWhereBetween(t,i);break;case"notBetween":e.orWhereNotBetween(t,i);break;case"in":e.orWhereIn(t,i);break;case"notIn":e.orWhereNotIn(t,i);break;case"like":e.orWhere(t,"like",i);break;default:e.orWhere(t,r,i)}else e.orWhereNull(t)}_applyAndWhereCondition(e,t,r,i){if(null!==i)if(Array.isArray(i))e.whereIn(t,i);else switch(r){case"between":e.whereBetween(t,i);break;case"notBetween":e.whereNotBetween(t,i);break;case"in":e.whereIn(t,i);break;case"notIn":e.whereNotIn(t,i);break;case"like":e.where(t,"like",i);break;default:e.where(t,r,i)}else e.whereNull(t)}buildWithTree(e,t=null){return this.helperUtility.dotWalkTree(e,{resolver:({current:e,part:r,source:i})=>t?t({current:e,part:r,source:i}):{}})}_getWithWhereForRelation(e,t){if(!e||"object"!=typeof e)return{};const r=`${t}.`,i={};for(const[t,o]of Object.entries(e))if(t.startsWith(r)){i[t.slice(r.length)]=o}return i}_applyWithWhereConditions(e,t){for(const[r,i]of Object.entries(t)){const{joinType:t="AND",column:o}=this.parseWhereColumn(r),{operator:l,value:s}=this.parseWhereValue(i);"AND"===t?this._applyAndWhereCondition(e,o,l,s):this._applyOrWhereCondition(e,o,l,s)}}async fetchRelatedRows(e,t,r={}){if(e.through){let i=await this.db(e.through).whereIn(e.throughLocalKey,t),o=this.db(e.table).whereIn(e.foreignKey,i.map(t=>t[e.throughForeignKey]));return Object.keys(r).length>0&&this._applyWithWhereConditions(o,r),o}{let i=this.db(e.table).whereIn(e.foreignKey,t);return Object.keys(r).length>0&&this._applyWithWhereConditions(i,r),i}}async fetchAndAttachRelated(e){const{parentRows:t,relName:r,model:i,withTree:o,relation:l,withWhere:s={}}=e;if(!l){const e=this.getHookService().getModelInstance(i),l=`get${r.charAt(0).toUpperCase()+r.slice(1)}Relation`;if("function"==typeof e[l]){const n={rows:t,relName:r,model:i,withTree:o,controller:this.controllerWrapper,relation:i.hasRelations[r],qb:this,db:this.db,withWhere:s};await e[l](n)}return t}let n=t.map(e=>e[l.localKey]);const a="one"===l?.type;let h=[];const p=this._getWithWhereForRelation(s,r);h=await this.fetchRelatedRows(l,n,p);let c=new Map;for(const e of h){let t=e[l.foreignKey];c.has(t)||c.set(t,[]),c.get(t).push(e)}for(const e of t){let t=e[l.localKey];a&&1==c.get(t)?.length?e[r]=c.get(t)[0]:e[r]=c.get(t)||[]}let y=Object.keys(o);for(const e of y){let i=o[e],l=t.filter(e=>a?e[r]:e[r].length>0).map(e=>e[r]).reduce((e,t)=>e.concat(t),[]),n=this.utils.getModel(this.controllerWrapper,r);await this.fetchAndAttachRelated({parentRows:l,relName:e,model:n,withTree:i,relation:n.hasRelations[e],withWhere:s})}return t}filterWhere(e,t=""){return t?Object.keys(e).filter(e=>e.includes(t)).reduce((r,i)=>(r[i.replace(t,"")]=e[i],r),{}):Object.keys(e).filter(e=>!e.includes(".")).reduce((t,r)=>(t[r]=e[r],t),{})}async getQuery(e,t){try{const{where:r={},with:i,withWhere:o,select:l,orderBy:s={column:"id",direction:"asc"},limit:n=10,offset:a=0,page:h,groupBy:p,having:c,distinct:y,join:u,leftJoin:f,rightJoin:g,innerJoin:d,count:W=!1}=t;let w=this.getQueryBuilder(e);l&&(Array.isArray(l)||"string"==typeof l)?w.select(l):w.select("*"),y&&(Array.isArray(y)||"string"==typeof y?w.distinct(y):w.distinct()),u&&this._applyJoins(w,u,"join"),f&&this._applyJoins(w,f,"leftJoin"),g&&this._applyJoins(w,g,"rightJoin"),d&&this._applyJoins(w,d,"innerJoin"),this._applyWhereClause(w,r,i),p&&(Array.isArray(p),w.groupBy(p)),c&&this._applyHavingClause(w,c),s&&this._applyOrderBy(w,s);let b=!1,_=n,A=a,k=1,m=0;h&&n>0&&(k=Math.max(1,parseInt(h)),A=(k-1)*n),n>0&&(w.limit(_),A>0&&w.offset(A));const j=await w;if(i&&i.length>0){const t=this.buildWithTree(i);for(const r of Object.keys(t))await this.fetchAndAttachRelated({parentRows:j,relName:r,model:e,withTree:t[r],relation:e.hasRelations[r],withWhere:o||{}})}let C=null;if(n>0)try{let t=this.getQueryBuilder(e);r&&Object.keys(r).length>0&&this._applyWhereClause(t,r),u&&this._applyJoins(t,u,"join"),f&&this._applyJoins(t,f,"leftJoin"),g&&this._applyJoins(t,g,"rightJoin"),d&&this._applyJoins(t,d,"innerJoin");C=(await t.count("* as cnt").first()).cnt}catch(e){logger.warn("Failed to get total count:",e.message),C=j.length}n>0&&null!==C&&(m=Math.ceil(C/n),b=k<m);return{data:j,totalCount:C,...n>0?{pagination:{page:k,limit:_,offset:A,totalPages:m,hasNext:b,hasPrev:k>1,nextPage:b?k+1:null,prevPage:k>1?k-1:null}}:{}}}catch(e){throw logger.error("QueryService.getQuery error:",e),new Error(`Failed to execute query: ${e.message}`)}}_applyWhereWithArray(e,t,r){for(const i of t)this._applyWhereClause(e,i,r)}_applyWhereClause(e,t,r=[]){if(Array.isArray(t))this._applyWhereWithArray(e,t,r);else{if(t&&Object.keys(t).length>0){let r=this.helperUtility.getDotWalkQuery(t);logger.debug({filteredWhere:r}),r=this.helperUtility.objectFilter(r,(e,t)=>"object"!=typeof t||null===t);for(const[t,i]of Object.entries(r)){const{joinType:r="AND",column:o}=this.parseWhereColumn(t),{operator:l,value:s}=this.parseWhereValue(i);"AND"===r?this._applyAndWhereCondition(e,o,l,s):this._applyOrWhereCondition(e,o,l,s)}}r&&r.length>0&&this._applyNestedWhere(e,t,r)}}_applyNestedWhere(e,t,r){let i=this,o=e._getMyModel();if(r&&r.length>0)for(const l of r){let s=this.helperUtility.getDotWalkQuery(t,l);if(s&&Object.keys(s).length>0){let t=this.utils.getModel(this.controllerWrapper,l),n=o.hasRelations[l],a=r.map(e=>this.helperUtility.pluckDotWalkKey(e,1));e.whereExists(function(){let e=i.getQueryBuilder(t,this.select("*").from(t.table));e.whereRaw(`${n.foreignKey} = ${o.table}.${n.localKey}`),i._applyWhereClause(e,s,a)})}}}_applyJoins(e,t,r){const i=Array.isArray(t)?t:[t];for(const t of i)"string"==typeof t?e[r](t):"object"==typeof t&&(t.table&&t.on?e[r](t.table,t.on):t.table&&t.first&&t.operator&&t.second&&e[r](t.table,t.first,t.operator,t.second))}_applyWithWhere(e,t){try{if(Array.isArray(t))for(const r of t)"string"==typeof r?e.withWhere(r):"object"==typeof r&&e.withWhere(r.column,r.operator,r.value);else if("object"==typeof t)for(const[r,i]of Object.entries(t))e.withWhere(r,i)}catch(e){logger.warn("Failed to apply withWhere:",e.message)}}_applyHavingClause(e,t){for(const[r,i]of Object.entries(t))"object"==typeof i&&i.operator?e.having(r,i.operator,i.value):e.having(r,i)}_applyOrderBy(e,t){if(Array.isArray(t))for(const r of t)"string"==typeof r?e.orderBy(r):"object"==typeof r&&e.orderBy(r.column,r.direction||"asc");else"string"==typeof t?e.orderBy(t):"object"==typeof t&&e.orderBy(t.column,t.direction||"asc")}}module.exports=QueryBuilder;
1
+ const HelperUtility=require("./HelperUtility"),logger=require("../../Logger");class QueryBuilder{constructor(e,t,r=null){this.db=e,this.utils=t,this.controllerWrapper=r,this.helperUtility=new HelperUtility}getHookService(){return this.controllerWrapper.hookService}getQueryBuilder(e,t=null){let r=this.db(e.table);return t&&(r=t),r._getMyModel=()=>e,r}parseValue(e){return this.helperUtility.parseValue(e)}parseWhereValue(e){return this.helperUtility.parseWhereValue(e)}parseWhereColumn(e){return this.helperUtility.parseWhereColumn(e)}_applyOrWhereCondition(e,t,r,o){if(null!==o)if(Array.isArray(o))e.orWhereIn(t,o);else switch(r){case"between":e.orWhereBetween(t,o);break;case"notBetween":e.orWhereNotBetween(t,o);break;case"in":e.orWhereIn(t,o);break;case"notIn":e.orWhereNotIn(t,o);break;case"like":e.orWhere(t,"like",o);break;default:e.orWhere(t,r,o)}else e.orWhereNull(t)}_applyAndWhereCondition(e,t,r,o){if(null!==o)if(Array.isArray(o))e.whereIn(t,o);else switch(r){case"between":e.whereBetween(t,o);break;case"notBetween":e.whereNotBetween(t,o);break;case"in":e.whereIn(t,o);break;case"notIn":e.whereNotIn(t,o);break;case"like":e.where(t,"like",o);break;default:e.where(t,r,o)}else e.whereNull(t)}buildWithTree(e,t=null){return this.helperUtility.dotWalkTree(e,{resolver:({current:e,part:r,source:o})=>t?t({current:e,part:r,source:o}):{}})}_getWithWhereForRelation(e,t){if(!e||"object"!=typeof e)return{direct:{},nested:{}};const r=`${t}.`,o={},i={};for(const[t,s]of Object.entries(e))if(t.startsWith(r)){const e=t.slice(r.length);e.includes(".")?i[e]=s:o[e]=s}return{direct:o,nested:i}}_applyWithWhereConditions(e,t){for(const[r,o]of Object.entries(t)){const{joinType:t="AND",column:i}=this.parseWhereColumn(r),{operator:s,value:l}=this.parseWhereValue(o);"AND"===t?this._applyAndWhereCondition(e,i,s,l):this._applyOrWhereCondition(e,i,s,l)}}async fetchRelatedRows(e,t,r={}){if(e.through){let o=await this.db(e.through).whereIn(e.throughLocalKey,t),i=this.db(e.table).whereIn(e.foreignKey,o.map(t=>t[e.throughForeignKey]));return Object.keys(r).length>0&&this._applyWithWhereConditions(i,r),i}{let o=this.db(e.table).whereIn(e.foreignKey,t);return Object.keys(r).length>0&&this._applyWithWhereConditions(o,r),o}}async fetchAndAttachRelated(e){const{parentRows:t,relName:r,model:o,withTree:i,relation:s,withWhere:l={}}=e;if(!s){const e=this.getHookService().getModelInstance(o),s=`get${r.charAt(0).toUpperCase()+r.slice(1)}Relation`;if("function"==typeof e[s]){const{direct:n,nested:a}=this._getWithWhereForRelation(l,r),h={rows:t,relName:r,model:o,withTree:i,controller:this.controllerWrapper,relation:o.hasRelations[r],qb:this,db:this.db,withWhere:n,nestedWithWhere:a};await e[s](h)}else logger.warn(`Method ${s} not found in model ${o.name}`);return t}let n=t.map(e=>e[s.localKey]);const a="one"===s?.type;let h=[];const{direct:c,nested:p}=this._getWithWhereForRelation(l,r);h=await this.fetchRelatedRows(s,n,c);let y=new Map;for(const e of h){let t=e[s.foreignKey];y.has(t)||y.set(t,[]),y.get(t).push(e)}for(const e of t){let t=e[s.localKey];a&&1==y.get(t)?.length?e[r]=y.get(t)[0]:e[r]=y.get(t)||[]}let u=Object.keys(i);for(const e of u){let o=i[e],s=t.filter(e=>a?e[r]:e[r].length>0).map(e=>e[r]).reduce((e,t)=>e.concat(t),[]),l=this.utils.getModel(this.controllerWrapper,r);await this.fetchAndAttachRelated({parentRows:s,relName:e,model:l,withTree:o,relation:l.hasRelations[e],withWhere:p})}return t}filterWhere(e,t=""){return t?Object.keys(e).filter(e=>e.includes(t)).reduce((r,o)=>(r[o.replace(t,"")]=e[o],r),{}):Object.keys(e).filter(e=>!e.includes(".")).reduce((t,r)=>(t[r]=e[r],t),{})}async getQuery(e,t){try{const{where:r={},with:o,withWhere:i,select:s,orderBy:l={column:"id",direction:"asc"},limit:n=10,offset:a=0,page:h,groupBy:c,having:p,distinct:y,join:u,leftJoin:d,rightJoin:f,innerJoin:g,count:W=!1}=t;let b=this.getQueryBuilder(e);s&&(Array.isArray(s)||"string"==typeof s)?b.select(s):b.select("*"),y&&(Array.isArray(y)||"string"==typeof y?b.distinct(y):b.distinct()),u&&this._applyJoins(b,u,"join"),d&&this._applyJoins(b,d,"leftJoin"),f&&this._applyJoins(b,f,"rightJoin"),g&&this._applyJoins(b,g,"innerJoin"),this._applyWhereClause(b,r,o),c&&(Array.isArray(c),b.groupBy(c)),p&&this._applyHavingClause(b,p),l&&this._applyOrderBy(b,l);let w=!1,_=n,m=a,A=1,j=0;h&&n>0&&(A=Math.max(1,parseInt(h)),m=(A-1)*n),n>0&&(b.limit(_),m>0&&b.offset(m));const k=[],C=b.toSQL();k.push(C.sql);const O=await b;if(o&&o.length>0){const t=this.buildWithTree(o);for(const r of Object.keys(t))await this.fetchAndAttachRelated({parentRows:O,relName:r,model:e,withTree:t[r],relation:e.hasRelations[r],withWhere:i||{}})}let R=null;if(n>0)try{let t=this.getQueryBuilder(e);r&&Object.keys(r).length>0&&this._applyWhereClause(t,r),u&&this._applyJoins(t,u,"join"),d&&this._applyJoins(t,d,"leftJoin"),f&&this._applyJoins(t,f,"rightJoin"),g&&this._applyJoins(t,g,"innerJoin");const o=t.count("* as cnt");k.push(o.toSQL().sql);R=(await o.first()).cnt}catch(e){logger.warn("Failed to get total count:",e.message),R=O.length}n>0&&null!==R&&(j=Math.ceil(R/n),w=A<j);const B=w?A+1:null,N=A>1?A-1:null;return{data:O,totalCount:R,...this.controllerWrapper.debug?{sqlDebug:k}:{},...n>0?{pagination:{page:A,limit:_,offset:m,totalPages:j,hasNext:w,hasPrev:A>1,nextPage:B,prevPage:N}}:{}}}catch(t){throw logger.error("QueryService.getQuery error:",t),new Error(`Failed to execute query: ${t.message} on model ${e.name}`)}}_applyWhereWithArray(e,t,r){for(const o of t)this._applyWhereClause(e,o,r)}_applyWhereClause(e,t,r=[]){if(Array.isArray(t))this._applyWhereWithArray(e,t,r);else{if(t&&Object.keys(t).length>0){let r=this.helperUtility.getDotWalkQuery(t);logger.debug({filteredWhere:r}),r=this.helperUtility.objectFilter(r,(e,t)=>!e.startsWith("!")&&("__exists__"!==e&&("object"!=typeof t||null===t)));for(const[t,o]of Object.entries(r)){const{joinType:r="AND",column:i}=this.parseWhereColumn(t),{operator:s,value:l}=this.parseWhereValue(o);"AND"===r?this._applyAndWhereCondition(e,i,s,l):this._applyOrWhereCondition(e,i,s,l)}}this._applyNestedWhere(e,t,r)}}_getNestedWhereConditions(e,t){if(!e||"object"!=typeof e)return{};const r=`${t}.`,o={};for(const[i,s]of Object.entries(e))if(i.startsWith(r)){o[i.slice(r.length)]=s}else i===t&&!0===s&&(o.__exists__=!0);return o}_getTopLevelRelationsFromWhere(e){if(!e||"object"!=typeof e)return[];const t=new Set;for(const r of Object.keys(e))if(r.includes(".")){const e=r.split(".")[0];t.add(e)}else r.startsWith("!")&&t.add(r);return[...t]}_applyNestedWhere(e,t,r){let o=this,i=e._getMyModel();const s=this._getTopLevelRelationsFromWhere(t);if(0!==s.length)for(let r of s){let s=r.startsWith("!"),l=s?r.slice(1):r,n=this._getNestedWhereConditions(t,r);if(0===Object.keys(n).length)continue;let a,h=i.hasRelations?.[l];if(!h){logger.warn(`Relation ${l} not found in model ${i.modelName}`);continue}try{a=this.utils.getModel(this.controllerWrapper,h.table||l)}catch(e){try{a=this.utils.getModel(this.controllerWrapper,l)}catch(e){logger.warn(`Model for relation ${l} (table: ${h.table}) not found`);continue}}e[s?"whereNotExists":"whereExists"](function(){let e=o.getQueryBuilder(a,this.select("*").from(h.table));e.whereRaw(`"${h.table}"."${h.foreignKey}" = "${i.table}"."${h.localKey}"`);if(!(!0===n.__exists__&&1===Object.keys(n).length)){const t={...n};delete t.__exists__,o._applyWhereClause(e,t,[])}})}}_applyJoins(e,t,r){const o=Array.isArray(t)?t:[t];for(const t of o)"string"==typeof t?e[r](t):"object"==typeof t&&(t.table&&t.on?e[r](t.table,t.on):t.table&&t.first&&t.operator&&t.second&&e[r](t.table,t.first,t.operator,t.second))}_applyWithWhere(e,t){try{if(Array.isArray(t))for(const r of t)"string"==typeof r?e.withWhere(r):"object"==typeof r&&e.withWhere(r.column,r.operator,r.value);else if("object"==typeof t)for(const[r,o]of Object.entries(t))e.withWhere(r,o)}catch(e){logger.warn("Failed to apply withWhere:",e.message)}}_applyHavingClause(e,t){for(const[r,o]of Object.entries(t))"object"==typeof o&&o.operator?e.having(r,o.operator,o.value):e.having(r,o)}_applyOrderBy(e,t){if(Array.isArray(t))for(const r of t)"string"==typeof r?e.orderBy(r):"object"==typeof r&&e.orderBy(r.column,r.direction||"asc");else"string"==typeof t?e.orderBy(t):"object"==typeof t&&e.orderBy(t.column,t.direction||"asc")}}module.exports=QueryBuilder;
@@ -1 +1 @@
1
- const HelperUtility=require("./HelperUtility"),logger=require("../../Logger");class QueryBuilder{constructor(e,t,r=null){this.db=e,this.utils=t,this.controllerWrapper=r,this.helperUtility=new HelperUtility}getHookService(){return this.controllerWrapper.hookService}getQueryBuilder(e,t=null){let r=this.db(e.table);return t&&(r=t),r._getMyModel=()=>e,r}parseValue(e){return this.helperUtility.parseValue(e)}parseWhereValue(e){return this.helperUtility.parseWhereValue(e)}parseWhereColumn(e){return this.helperUtility.parseWhereColumn(e)}_applyOrWhereCondition(e,t,r,i){if(null!==i)if(Array.isArray(i))e.orWhereIn(t,i);else switch(r){case"between":e.orWhereBetween(t,i);break;case"notBetween":e.orWhereNotBetween(t,i);break;case"in":e.orWhereIn(t,i);break;case"notIn":e.orWhereNotIn(t,i);break;case"like":e.orWhere(t,"like",i);break;default:e.orWhere(t,r,i)}else e.orWhereNull(t)}_applyAndWhereCondition(e,t,r,i){if(null!==i)if(Array.isArray(i))e.whereIn(t,i);else switch(r){case"between":e.whereBetween(t,i);break;case"notBetween":e.whereNotBetween(t,i);break;case"in":e.whereIn(t,i);break;case"notIn":e.whereNotIn(t,i);break;case"like":e.where(t,"like",i);break;default:e.where(t,r,i)}else e.whereNull(t)}buildWithTree(e,t=null){return this.helperUtility.dotWalkTree(e,{resolver:({current:e,part:r,source:i})=>t?t({current:e,part:r,source:i}):{}})}_getWithWhereForRelation(e,t){if(!e||"object"!=typeof e)return{};const r=`${t}.`,i={};for(const[t,o]of Object.entries(e))if(t.startsWith(r)){i[t.slice(r.length)]=o}return i}_applyWithWhereConditions(e,t){for(const[r,i]of Object.entries(t)){const{joinType:t="AND",column:o}=this.parseWhereColumn(r),{operator:l,value:s}=this.parseWhereValue(i);"AND"===t?this._applyAndWhereCondition(e,o,l,s):this._applyOrWhereCondition(e,o,l,s)}}async fetchRelatedRows(e,t,r={}){if(e.through){let i=await this.db(e.through).whereIn(e.throughLocalKey,t),o=this.db(e.table).whereIn(e.foreignKey,i.map(t=>t[e.throughForeignKey]));return Object.keys(r).length>0&&this._applyWithWhereConditions(o,r),o}{let i=this.db(e.table).whereIn(e.foreignKey,t);return Object.keys(r).length>0&&this._applyWithWhereConditions(i,r),i}}async fetchAndAttachRelated(e){const{parentRows:t,relName:r,model:i,withTree:o,relation:l,withWhere:s={}}=e;if(!l){const e=this.getHookService().getModelInstance(i),l=`get${r.charAt(0).toUpperCase()+r.slice(1)}Relation`;if("function"==typeof e[l]){const n={rows:t,relName:r,model:i,withTree:o,controller:this.controllerWrapper,relation:i.hasRelations[r],qb:this,db:this.db,withWhere:s};await e[l](n)}return t}let n=t.map(e=>e[l.localKey]);const a="one"===l?.type;let h=[];const p=this._getWithWhereForRelation(s,r);h=await this.fetchRelatedRows(l,n,p);let c=new Map;for(const e of h){let t=e[l.foreignKey];c.has(t)||c.set(t,[]),c.get(t).push(e)}for(const e of t){let t=e[l.localKey];a&&1==c.get(t)?.length?e[r]=c.get(t)[0]:e[r]=c.get(t)||[]}let y=Object.keys(o);for(const e of y){let i=o[e],l=t.filter(e=>a?e[r]:e[r].length>0).map(e=>e[r]).reduce((e,t)=>e.concat(t),[]),n=this.utils.getModel(this.controllerWrapper,r);await this.fetchAndAttachRelated({parentRows:l,relName:e,model:n,withTree:i,relation:n.hasRelations[e],withWhere:s})}return t}filterWhere(e,t=""){return t?Object.keys(e).filter(e=>e.includes(t)).reduce((r,i)=>(r[i.replace(t,"")]=e[i],r),{}):Object.keys(e).filter(e=>!e.includes(".")).reduce((t,r)=>(t[r]=e[r],t),{})}async getQuery(e,t){try{const{where:r={},with:i,withWhere:o,select:l,orderBy:s={column:"id",direction:"asc"},limit:n=10,offset:a=0,page:h,groupBy:p,having:c,distinct:y,join:u,leftJoin:f,rightJoin:g,innerJoin:d,count:W=!1}=t;let w=this.getQueryBuilder(e);l&&(Array.isArray(l)||"string"==typeof l)?w.select(l):w.select("*"),y&&(Array.isArray(y)||"string"==typeof y?w.distinct(y):w.distinct()),u&&this._applyJoins(w,u,"join"),f&&this._applyJoins(w,f,"leftJoin"),g&&this._applyJoins(w,g,"rightJoin"),d&&this._applyJoins(w,d,"innerJoin"),this._applyWhereClause(w,r,i),p&&(Array.isArray(p),w.groupBy(p)),c&&this._applyHavingClause(w,c),s&&this._applyOrderBy(w,s);let b=!1,_=n,A=a,k=1,m=0;h&&n>0&&(k=Math.max(1,parseInt(h)),A=(k-1)*n),n>0&&(w.limit(_),A>0&&w.offset(A));const j=await w;if(i&&i.length>0){const t=this.buildWithTree(i);for(const r of Object.keys(t))await this.fetchAndAttachRelated({parentRows:j,relName:r,model:e,withTree:t[r],relation:e.hasRelations[r],withWhere:o||{}})}let C=null;if(n>0)try{let t=this.getQueryBuilder(e);r&&Object.keys(r).length>0&&this._applyWhereClause(t,r),u&&this._applyJoins(t,u,"join"),f&&this._applyJoins(t,f,"leftJoin"),g&&this._applyJoins(t,g,"rightJoin"),d&&this._applyJoins(t,d,"innerJoin");C=(await t.count("* as cnt").first()).cnt}catch(e){logger.warn("Failed to get total count:",e.message),C=j.length}n>0&&null!==C&&(m=Math.ceil(C/n),b=k<m);return{data:j,totalCount:C,...n>0?{pagination:{page:k,limit:_,offset:A,totalPages:m,hasNext:b,hasPrev:k>1,nextPage:b?k+1:null,prevPage:k>1?k-1:null}}:{}}}catch(e){throw logger.error("QueryService.getQuery error:",e),new Error(`Failed to execute query: ${e.message}`)}}_applyWhereWithArray(e,t,r){for(const i of t)this._applyWhereClause(e,i,r)}_applyWhereClause(e,t,r=[]){if(Array.isArray(t))this._applyWhereWithArray(e,t,r);else{if(t&&Object.keys(t).length>0){let r=this.helperUtility.getDotWalkQuery(t);r=this.helperUtility.objectFilter(r,(e,t)=>"object"!=typeof t||null===t);for(const[t,i]of Object.entries(r)){const{joinType:r="AND",column:o}=this.parseWhereColumn(t),{operator:l,value:s}=this.parseWhereValue(i);"AND"===r?this._applyAndWhereCondition(e,o,l,s):this._applyOrWhereCondition(e,o,l,s)}}r&&r.length>0&&this._applyNestedWhere(e,t,r)}}_applyNestedWhere(e,t,r){let i=this,o=e._getMyModel();if(r&&r.length>0)for(const l of r){let s=this.helperUtility.getDotWalkQuery(t,l);if(s&&Object.keys(s).length>0){let t=this.utils.getModel(this.controllerWrapper,l),n=o.hasRelations[l],a=r.map(e=>this.helperUtility.pluckDotWalkKey(e,1));e.whereExists(function(){let e=i.getQueryBuilder(t,this.select("*").from(t.table));e.whereRaw(`${n.foreignKey} = ${o.table}.${n.localKey}`),i._applyWhereClause(e,s,a)})}}}_applyJoins(e,t,r){const i=Array.isArray(t)?t:[t];for(const t of i)"string"==typeof t?e[r](t):"object"==typeof t&&(t.table&&t.on?e[r](t.table,t.on):t.table&&t.first&&t.operator&&t.second&&e[r](t.table,t.first,t.operator,t.second))}_applyWithWhere(e,t){try{if(Array.isArray(t))for(const r of t)"string"==typeof r?e.withWhere(r):"object"==typeof r&&e.withWhere(r.column,r.operator,r.value);else if("object"==typeof t)for(const[r,i]of Object.entries(t))e.withWhere(r,i)}catch(e){logger.warn("Failed to apply withWhere:",e.message)}}_applyHavingClause(e,t){for(const[r,i]of Object.entries(t))"object"==typeof i&&i.operator?e.having(r,i.operator,i.value):e.having(r,i)}_applyOrderBy(e,t){if(Array.isArray(t))for(const r of t)"string"==typeof r?e.orderBy(r):"object"==typeof r&&e.orderBy(r.column,r.direction||"asc");else"string"==typeof t?e.orderBy(t):"object"==typeof t&&e.orderBy(t.column,t.direction||"asc")}}module.exports=QueryBuilder;
1
+ const HelperUtility=require("./HelperUtility"),logger=require("../../Logger");class QueryBuilder{constructor(e,t,r=null){this.db=e,this.utils=t,this.controllerWrapper=r,this.helperUtility=new HelperUtility}getHookService(){return this.controllerWrapper.hookService}getQueryBuilder(e,t=null){let r=this.db(e.table);return t&&(r=t),r._getMyModel=()=>e,r}parseValue(e){return this.helperUtility.parseValue(e)}parseWhereValue(e){return this.helperUtility.parseWhereValue(e)}parseWhereColumn(e){return this.helperUtility.parseWhereColumn(e)}_applyOrWhereCondition(e,t,r,o){if(null!==o)if(Array.isArray(o))e.orWhereIn(t,o);else switch(r){case"between":e.orWhereBetween(t,o);break;case"notBetween":e.orWhereNotBetween(t,o);break;case"in":e.orWhereIn(t,o);break;case"notIn":e.orWhereNotIn(t,o);break;case"like":e.orWhere(t,"like",o);break;default:e.orWhere(t,r,o)}else e.orWhereNull(t)}_applyAndWhereCondition(e,t,r,o){if(null!==o)if(Array.isArray(o))e.whereIn(t,o);else switch(r){case"between":e.whereBetween(t,o);break;case"notBetween":e.whereNotBetween(t,o);break;case"in":e.whereIn(t,o);break;case"notIn":e.whereNotIn(t,o);break;case"like":e.where(t,"like",o);break;default:e.where(t,r,o)}else e.whereNull(t)}buildWithTree(e,t=null){return this.helperUtility.dotWalkTree(e,{resolver:({current:e,part:r,source:o})=>t?t({current:e,part:r,source:o}):{}})}_getWithWhereForRelation(e,t){if(!e||"object"!=typeof e)return{direct:{},nested:{}};const r=`${t}.`,o={},i={};for(const[t,s]of Object.entries(e))if(t.startsWith(r)){const e=t.slice(r.length);e.includes(".")?i[e]=s:o[e]=s}return{direct:o,nested:i}}_applyWithWhereConditions(e,t){for(const[r,o]of Object.entries(t)){const{joinType:t="AND",column:i}=this.parseWhereColumn(r),{operator:s,value:l}=this.parseWhereValue(o);"AND"===t?this._applyAndWhereCondition(e,i,s,l):this._applyOrWhereCondition(e,i,s,l)}}async fetchRelatedRows(e,t,r={}){if(e.through){let o=await this.db(e.through).whereIn(e.throughLocalKey,t),i=this.db(e.table).whereIn(e.foreignKey,o.map(t=>t[e.throughForeignKey]));return Object.keys(r).length>0&&this._applyWithWhereConditions(i,r),i}{let o=this.db(e.table).whereIn(e.foreignKey,t);return Object.keys(r).length>0&&this._applyWithWhereConditions(o,r),o}}async fetchAndAttachRelated(e){const{parentRows:t,relName:r,model:o,withTree:i,relation:s,withWhere:l={}}=e;if(!s){const e=this.getHookService().getModelInstance(o),s=`get${r.charAt(0).toUpperCase()+r.slice(1)}Relation`;if("function"==typeof e[s]){const{direct:n,nested:a}=this._getWithWhereForRelation(l,r),h={rows:t,relName:r,model:o,withTree:i,controller:this.controllerWrapper,relation:o.hasRelations[r],qb:this,db:this.db,withWhere:n,nestedWithWhere:a};await e[s](h)}else logger.warn(`Method ${s} not found in model ${o.name}`);return t}let n=t.map(e=>e[s.localKey]);const a="one"===s?.type;let h=[];const{direct:c,nested:p}=this._getWithWhereForRelation(l,r);h=await this.fetchRelatedRows(s,n,c);let y=new Map;for(const e of h){let t=e[s.foreignKey];y.has(t)||y.set(t,[]),y.get(t).push(e)}for(const e of t){let t=e[s.localKey];a&&1==y.get(t)?.length?e[r]=y.get(t)[0]:e[r]=y.get(t)||[]}let u=Object.keys(i);for(const e of u){let o=i[e],s=t.filter(e=>a?e[r]:e[r].length>0).map(e=>e[r]).reduce((e,t)=>e.concat(t),[]),l=this.utils.getModel(this.controllerWrapper,r);await this.fetchAndAttachRelated({parentRows:s,relName:e,model:l,withTree:o,relation:l.hasRelations[e],withWhere:p})}return t}filterWhere(e,t=""){return t?Object.keys(e).filter(e=>e.includes(t)).reduce((r,o)=>(r[o.replace(t,"")]=e[o],r),{}):Object.keys(e).filter(e=>!e.includes(".")).reduce((t,r)=>(t[r]=e[r],t),{})}async getQuery(e,t){try{const{where:r={},with:o,withWhere:i,select:s,orderBy:l={column:"id",direction:"asc"},limit:n=10,offset:a=0,page:h,groupBy:c,having:p,distinct:y,join:u,leftJoin:f,rightJoin:d,innerJoin:g,count:W=!1}=t;let b=this.getQueryBuilder(e);s&&(Array.isArray(s)||"string"==typeof s)?b.select(s):b.select("*"),y&&(Array.isArray(y)||"string"==typeof y?b.distinct(y):b.distinct()),u&&this._applyJoins(b,u,"join"),f&&this._applyJoins(b,f,"leftJoin"),d&&this._applyJoins(b,d,"rightJoin"),g&&this._applyJoins(b,g,"innerJoin"),this._applyWhereClause(b,r,o),c&&(Array.isArray(c),b.groupBy(c)),p&&this._applyHavingClause(b,p),l&&this._applyOrderBy(b,l);let w=!1,_=n,m=a,A=1,j=0;h&&n>0&&(A=Math.max(1,parseInt(h)),m=(A-1)*n),n>0&&(b.limit(_),m>0&&b.offset(m));const k=[],C=b.toSQL();k.push(C.sql);const O=await b;if(o&&o.length>0){const t=this.buildWithTree(o);for(const r of Object.keys(t))await this.fetchAndAttachRelated({parentRows:O,relName:r,model:e,withTree:t[r],relation:e.hasRelations[r],withWhere:i||{}})}let R=null;if(n>0)try{let t=this.getQueryBuilder(e);r&&Object.keys(r).length>0&&this._applyWhereClause(t,r),u&&this._applyJoins(t,u,"join"),f&&this._applyJoins(t,f,"leftJoin"),d&&this._applyJoins(t,d,"rightJoin"),g&&this._applyJoins(t,g,"innerJoin");const o=t.count("* as cnt");k.push(o.toSQL().sql);R=(await o.first()).cnt}catch(e){logger.warn("Failed to get total count:",e.message),R=O.length}n>0&&null!==R&&(j=Math.ceil(R/n),w=A<j);const B=w?A+1:null,N=A>1?A-1:null;return{data:O,totalCount:R,...this.controllerWrapper.debug?{sqlDebug:k}:{},...n>0?{pagination:{page:A,limit:_,offset:m,totalPages:j,hasNext:w,hasPrev:A>1,nextPage:B,prevPage:N}}:{}}}catch(t){throw logger.error("QueryService.getQuery error:",t),new Error(`Failed to execute query: ${t.message} on model ${e.name}`)}}_applyWhereWithArray(e,t,r){for(const o of t)this._applyWhereClause(e,o,r)}_applyWhereClause(e,t,r=[]){if(Array.isArray(t))this._applyWhereWithArray(e,t,r);else{if(t&&Object.keys(t).length>0){let r=this.helperUtility.getDotWalkQuery(t);r=this.helperUtility.objectFilter(r,(e,t)=>!e.startsWith("!")&&("__exists__"!==e&&("object"!=typeof t||null===t)));for(const[t,o]of Object.entries(r)){const{joinType:r="AND",column:i}=this.parseWhereColumn(t),{operator:s,value:l}=this.parseWhereValue(o);"AND"===r?this._applyAndWhereCondition(e,i,s,l):this._applyOrWhereCondition(e,i,s,l)}}this._applyNestedWhere(e,t,r)}}_getNestedWhereConditions(e,t){if(!e||"object"!=typeof e)return{};const r=`${t}.`,o={};for(const[i,s]of Object.entries(e))if(i.startsWith(r)){o[i.slice(r.length)]=s}else i===t&&!0===s&&(o.__exists__=!0);return o}_getTopLevelRelationsFromWhere(e){if(!e||"object"!=typeof e)return[];const t=new Set;for(const r of Object.keys(e))if(r.includes(".")){const e=r.split(".")[0];t.add(e)}else r.startsWith("!")&&t.add(r);return[...t]}_applyNestedWhere(e,t,r){let o=this,i=e._getMyModel();const s=this._getTopLevelRelationsFromWhere(t);if(0!==s.length)for(let r of s){let s=r.startsWith("!"),l=s?r.slice(1):r,n=this._getNestedWhereConditions(t,r);if(0===Object.keys(n).length)continue;let a,h=i.hasRelations?.[l];if(!h){logger.warn(`Relation ${l} not found in model ${i.modelName}`);continue}try{a=this.utils.getModel(this.controllerWrapper,h.table||l)}catch(e){try{a=this.utils.getModel(this.controllerWrapper,l)}catch(e){logger.warn(`Model for relation ${l} (table: ${h.table}) not found`);continue}}e[s?"whereNotExists":"whereExists"](function(){let e=o.getQueryBuilder(a,this.select("*").from(h.table));e.whereRaw(`"${h.table}"."${h.foreignKey}" = "${i.table}"."${h.localKey}"`);if(!(!0===n.__exists__&&1===Object.keys(n).length)){const t={...n};delete t.__exists__,o._applyWhereClause(e,t,[])}})}}_applyJoins(e,t,r){const o=Array.isArray(t)?t:[t];for(const t of o)"string"==typeof t?e[r](t):"object"==typeof t&&(t.table&&t.on?e[r](t.table,t.on):t.table&&t.first&&t.operator&&t.second&&e[r](t.table,t.first,t.operator,t.second))}_applyWithWhere(e,t){try{if(Array.isArray(t))for(const r of t)"string"==typeof r?e.withWhere(r):"object"==typeof r&&e.withWhere(r.column,r.operator,r.value);else if("object"==typeof t)for(const[r,o]of Object.entries(t))e.withWhere(r,o)}catch(e){logger.warn("Failed to apply withWhere:",e.message)}}_applyHavingClause(e,t){for(const[r,o]of Object.entries(t))"object"==typeof o&&o.operator?e.having(r,o.operator,o.value):e.having(r,o)}_applyOrderBy(e,t){if(Array.isArray(t))for(const r of t)"string"==typeof r?e.orderBy(r):"object"==typeof r&&e.orderBy(r.column,r.direction||"asc");else"string"==typeof t?e.orderBy(t):"object"==typeof t&&e.orderBy(t.column,t.direction||"asc")}}module.exports=QueryBuilder;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dreamtree-org/korm-js",
3
- "version": "1.0.50",
3
+ "version": "1.0.52",
4
4
  "description": "Knowledge Object-Relational Mapping - A powerful, modular ORM system for Node.js with dynamic database operations, complex queries, relationships, and nested requests",
5
5
  "author": {
6
6
  "name": "Partha Preetham Krishna",