@dreamtree-org/korm-js 1.0.57 → 1.0.59
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
CHANGED
|
@@ -771,6 +771,13 @@ POST /api/Users/crud
|
|
|
771
771
|
}
|
|
772
772
|
```
|
|
773
773
|
|
|
774
|
+
> **An `orderBy` object must use the `{ column, direction }` shape.** A
|
|
775
|
+
> column→direction map that mirrors the `where` shape (e.g.
|
|
776
|
+
> `{ "created_at": "desc" }`) is **rejected** with a `KormError`
|
|
777
|
+
> (`code: 'VALIDATION_FAILED'`, message naming the offending keys) instead of
|
|
778
|
+
> generating broken SQL. Use `{ "column": "created_at", "direction": "desc" }`
|
|
779
|
+
> or the string form `"created_at"`.
|
|
780
|
+
|
|
774
781
|
### Joins
|
|
775
782
|
|
|
776
783
|
```javascript
|
|
@@ -1653,6 +1660,8 @@ type|modifier1|modifier2|...
|
|
|
1653
1660
|
² **Engine-specific.** `onUpdate` is honored on MySQL (emitted via `ON UPDATE <expr>`). PostgreSQL and SQLite log a one-time warning and ignore it — the modifier cannot be expressed inline on those engines. See [`docs/agents/05-multi-db-parity.md`](docs/agents/05-multi-db-parity.md).
|
|
1654
1661
|
³ **Auto type-matching.** When the referenced table is part of the same schema, `syncDatabase()` automatically widens the foreign-key column's type to match the referenced primary key, so the FK constraint is always type-compatible. In particular, an `autoIncrement` primary key is emitted as `BIGINT UNSIGNED` (MySQL) / `BIGSERIAL` (PostgreSQL) / `INTEGER` (SQLite) via Knex's `.increments()`, so a child column written as `int|foreignKey:…` is created as `BIGINT` — you do **not** need to hand-match the width. Foreign keys to tables outside the schema (pre-existing/third-party) are created exactly as declared.
|
|
1655
1662
|
|
|
1663
|
+
> **Dependency ordering.** `syncDatabase()` creates tables in foreign-key dependency order (parents before the children that reference them), so you can declare your models in **any order** — a child listed before its parent still syncs cleanly (MySQL/PostgreSQL otherwise reject the forward FK reference). FKs to tables outside the schema impose no ordering; a genuine circular FK dependency falls back to declaration order with a warning.
|
|
1664
|
+
|
|
1656
1665
|
**Special Default Values:**
|
|
1657
1666
|
|
|
1658
1667
|
- `now` or `now()` → `CURRENT_TIMESTAMP`
|
package/ai-skills/korm-js.md
CHANGED
|
@@ -46,7 +46,7 @@ In Express/Next/Fastify the consumer just forwards `req.body` and the model name
|
|
|
46
46
|
| `select` | array \| string | Columns to return (default: all) |
|
|
47
47
|
| `with` | array of strings | Relations to eager-load (dot-nested allowed: `"Post.Comment"`) |
|
|
48
48
|
| `withWhere` | object | Filters scoped to related rows only — does NOT filter parents |
|
|
49
|
-
| `orderBy` | object \| array \| string | `{column, direction}` / `"column"` / array of either
|
|
49
|
+
| `orderBy` | object \| array \| string | `{column, direction}` / `"column"` / array of either. Object MUST use the `column` key — `{created_at:"desc"}` is rejected (`VALIDATION_FAILED`) |
|
|
50
50
|
| `limit` | number | Max rows |
|
|
51
51
|
| `offset` / `page` | number | Pagination |
|
|
52
52
|
| `groupBy` | array \| string | GROUP BY columns |
|
package/clients/BaseSyncTable.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
const logger=require("../Logger"),ENGINE_WARNINGS=new Set;function warnOnce(e,t){ENGINE_WARNINGS.has(e)||(ENGINE_WARNINGS.add(e),logger.warn(t))}const BASE_TYPE_DISPATCHER={VARCHAR:(e,t,n)=>e.string(t,n.size||255),CHAR:(e,t,n)=>e.string(t,n.size||255),TEXT:(e,t)=>e.text(t),MEDIUMTEXT:(e,t)=>e.text(t),LONGTEXT:(e,t)=>e.text(t),INT:(e,t)=>e.integer(t),INTEGER:(e,t)=>e.integer(t),MEDIUMINT:(e,t)=>e.integer(t),SMALLINT:(e,t)=>e.integer(t),BIGINT:(e,t)=>e.bigInteger(t),TINYINT:(e,t,n)=>e.tinyint?e.tinyint(t):e.specificType(t,n.size?`TINYINT(${n.size})`:"TINYINT"),BOOLEAN:(e,t)=>e.boolean(t),BOOL:(e,t)=>e.boolean(t),DATE:(e,t)=>e.date(t),DATETIME:(e,t)=>e.dateTime(t),TIMESTAMP:(e,t)=>e.timestamp(t),TIME:(e,t)=>e.time(t),JSON:(e,t)=>e.json(t),FLOAT:(e,t)=>e.float(t),DOUBLE:(e,t)=>e.double?e.double(t):e.float(t),REAL:(e,t)=>e.double?e.double(t):e.float(t),DECIMAL:(e,t)=>e.decimal(t),NUMERIC:(e,t)=>e.decimal(t),BINARY:(e,t)=>e.binary(t),VARBINARY:(e,t)=>e.binary(t),BLOB:(e,t)=>e.binary(t),UUID:(e,t)=>e.uuid?e.uuid(t):e.string(t,36)},COLUMN_STRING_SUFFIXES=[e=>e.size?`|size:${e.size}`:"",e=>e.isUnsigned?"|unsigned":"",e=>e.primary?"|primaryKey":"",e=>e.autoIncrement?"|autoIncrement":"",e=>e.nullable?"":"|notNull",e=>e.unique?"|unique":"",e=>null!=e.default&&""!==e.default?`|default:${e.default}`:"",e=>e.onUpdate?`|onUpdate:${e.onUpdate}`:"",e=>e.comment?`|comment:${e.comment}`:"",e=>e.hasForeignKey&&e.foreignMapTables?.[0]?`|foreignKey:${e.foreignMapTables[0].table}:${e.foreignMapTables[0].column}`:""];class BaseSyncTable{constructor(e,t,n=null){this.db=e,this.utils=t,this.controllerWrapper=n}_getClientName(){throw new Error("_getClientName must be overridden by engine subclass")}async existsTable(e){return this.db.schema.hasTable(e)}async syncTable(e){if(await this.existsTable(e.table)){const t=await this.getAlterations(e);await this.alterTable(e.table,t)}else await this.createTable(e);await this._applyExtras(e)}async syncDatabase(){if(!this.controllerWrapper?.schema)throw new Error("controllerWrapper.schema not set.");const e=this.controllerWrapper.schema;for(const t of
|
|
1
|
+
const logger=require("../Logger"),ENGINE_WARNINGS=new Set;function warnOnce(e,t){ENGINE_WARNINGS.has(e)||(ENGINE_WARNINGS.add(e),logger.warn(t))}const BASE_TYPE_DISPATCHER={VARCHAR:(e,t,n)=>e.string(t,n.size||255),CHAR:(e,t,n)=>e.string(t,n.size||255),TEXT:(e,t)=>e.text(t),MEDIUMTEXT:(e,t)=>e.text(t),LONGTEXT:(e,t)=>e.text(t),INT:(e,t)=>e.integer(t),INTEGER:(e,t)=>e.integer(t),MEDIUMINT:(e,t)=>e.integer(t),SMALLINT:(e,t)=>e.integer(t),BIGINT:(e,t)=>e.bigInteger(t),TINYINT:(e,t,n)=>e.tinyint?e.tinyint(t):e.specificType(t,n.size?`TINYINT(${n.size})`:"TINYINT"),BOOLEAN:(e,t)=>e.boolean(t),BOOL:(e,t)=>e.boolean(t),DATE:(e,t)=>e.date(t),DATETIME:(e,t)=>e.dateTime(t),TIMESTAMP:(e,t)=>e.timestamp(t),TIME:(e,t)=>e.time(t),JSON:(e,t)=>e.json(t),FLOAT:(e,t)=>e.float(t),DOUBLE:(e,t)=>e.double?e.double(t):e.float(t),REAL:(e,t)=>e.double?e.double(t):e.float(t),DECIMAL:(e,t)=>e.decimal(t),NUMERIC:(e,t)=>e.decimal(t),BINARY:(e,t)=>e.binary(t),VARBINARY:(e,t)=>e.binary(t),BLOB:(e,t)=>e.binary(t),UUID:(e,t)=>e.uuid?e.uuid(t):e.string(t,36)},COLUMN_STRING_SUFFIXES=[e=>e.size?`|size:${e.size}`:"",e=>e.isUnsigned?"|unsigned":"",e=>e.primary?"|primaryKey":"",e=>e.autoIncrement?"|autoIncrement":"",e=>e.nullable?"":"|notNull",e=>e.unique?"|unique":"",e=>null!=e.default&&""!==e.default?`|default:${e.default}`:"",e=>e.onUpdate?`|onUpdate:${e.onUpdate}`:"",e=>e.comment?`|comment:${e.comment}`:"",e=>e.hasForeignKey&&e.foreignMapTables?.[0]?`|foreignKey:${e.foreignMapTables[0].table}:${e.foreignMapTables[0].column}`:""];class BaseSyncTable{constructor(e,t,n=null){this.db=e,this.utils=t,this.controllerWrapper=n}_getClientName(){throw new Error("_getClientName must be overridden by engine subclass")}async existsTable(e){return this.db.schema.hasTable(e)}async syncTable(e){if(await this.existsTable(e.table)){const t=await this.getAlterations(e);await this.alterTable(e.table,t)}else await this.createTable(e);await this._applyExtras(e)}async syncDatabase(){if(!this.controllerWrapper?.schema)throw new Error("controllerWrapper.schema not set.");const e=this.controllerWrapper.schema;for(const t of this._orderTablesByDependency(e))await this.syncTable(e[t]),await this.syncSeedData(e[t],t);logger.info("Database synced by SyncTable...")}_orderTablesByDependency(e){const t=Object.keys(e),n=this._buildFkDependencyMap(e,t),r=this._topoSort(t,n);if(r.length<t.length){const e=new Set(r),n=t.filter(t=>!e.has(t));logger.warn("syncDatabase: circular foreign-key dependency among",n,"— creating in declaration order; FK constraints may need a second pass."),r.push(...n)}return r}_buildFkDependencyMap(e,t){const n=new Map;for(const r of t){n.set(r,r);const t=e[r]?.table;t&&n.set(t,r)}const r=new Map;for(const s of t){const t=new Set;for(const r of this._foreignTargetsOf(e[s])){const e=n.get(r);e&&e!==s&&t.add(e)}r.set(s,t)}return r}_topoSort(e,t){const n=[],r=new Set;let s=!0;for(;n.length<e.length&&s;){s=!1;for(const a of e)!r.has(a)&&this._depsSatisfied(t.get(a),r)&&(n.push(a),r.add(a),s=!0)}return n}_depsSatisfied(e,t){for(const n of e)if(!t.has(n))return!1;return!0}_foreignTargetsOf(e){const t=[];for(const[n,r]of Object.entries(e?.columns||{})){const e="string"==typeof r?this.utils.formatColumnSchema(n,r):r;if(e?.hasForeignKey&&Array.isArray(e.foreignMapTables))for(const n of e.foreignMapTables)n?.table&&t.push(n.table)}return t}async syncSeedData(e,t){if(!e.seed||!Array.isArray(e.seed)||0===e.seed.length)return;const n=await this.db(e.table).count("* as n").first();Number(n?.n)>0?logger.info("Seed data already synced for",t):(await this.db(e.table).insert(e.seed),logger.info("Seed data synced for",t))}async generateSchema(){const e=await this._listTables(),t={},n=this._getHelperUtility();for(const r of e){const e=n?n.modelName(r):r;t[e]={table:r,alias:e,modelName:e,columns:this.getColumnString(await this.getCurrentColumns(r)),seed:[],hasRelations:await this._getRelations(r),indexes:[]}}return t}async createTable(e){await this.db.schema.createTable(e.table,t=>{for(const[n,r]of Object.entries(e.columns))this._applyColumnToBuilder(t,this._resolveColumnFrm(n,r))})}async alterTable(e,t){if(!t||"object"!=typeof t)throw new Error("alterations must be an object");(t.add?.length||0)+(t.drop?.length||0)+(t.modify?.length||0)>0?await this.db.schema.alterTable(e,e=>{for(const n of t.add||[])this._applyColumnToBuilder(e,n);for(const n of t.drop||[])e.dropColumn(n.name);for(const n of t.modify||[]){const t=this._applyColumnToBuilder(e,n);t&&"function"==typeof t.alter&&t.alter()}}):logger.info("No alterations to apply for",e)}async dropTable(e){await this.db.schema.dropTableIfExists(e)}async getCurrentColumns(e){const t=await this.db(e).columnInfo(),n={};for(const[e,r]of Object.entries(t))n[e]=this._formatColumnInfo(e,r);return n}_formatColumnInfo(e,t){const n=String(t.type||"").toLowerCase(),r=n.match(/^([a-z_]+)(?:\((\d+)(?:,\s*\d+)?\))?/);return{name:e,type:(r?r[1]:n).toUpperCase(),size:(r&&r[2]?Number(r[2]):t.maxLength||null)||null,nullable:!1!==t.nullable,default:this._parseDefault(t.defaultValue),primary:!1,unique:!1,autoIncrement:!1,isUnsigned:!1,hasForeignKey:!1,foreignMapTables:[],onUpdate:null,comment:""}}_parseDefault(e){if(null==e)return null;const t=String(e).trim();return""===t?null:t.replace(/^'+|'+$/g,"")}hasColumnChanged(e,t){return!1}async getAlterations(e){const t={add:[],drop:[],modify:[]},n=await this.getCurrentColumns(e.table);for(const[r,s]of Object.entries(e.columns)){const e=this._resolveColumnFrm(r,s),a=n[r];a?this.hasColumnChanged(a,e)&&t.modify.push(e):t.add.push(e)}for(const r of Object.keys(n))e.columns[r]||t.drop.push({name:r});return t}getColumnString(e){return Object.keys(e).reduce((t,n)=>{const r=e[n],s=String(r.type||"").toLowerCase();return t[n]=COLUMN_STRING_SUFFIXES.reduce((e,t)=>e+t(r),s),t},{})}_resolveColumnFrm(e,t){const n="string"==typeof t?this.utils.formatColumnSchema(e,t):t;return this._alignForeignKeyType(n)}_alignForeignKeyType(e){if(!e?.hasForeignKey)return e;const t=e.foreignMapTables?.[0];if(!t?.table)return e;const n=this._resolveParentColumnFrm(t);return n?{...e,...this._matchedForeignKeyType(n)}:e}_resolveParentColumnFrm(e){const t=this._findSchemaColumns(e.table);if(!t)return null;const n=t[e.column||"id"];return null==n?null:"string"==typeof n?this.utils.formatColumnSchema(e.column||"id",n):n}_findSchemaColumns(e){const t=this.controllerWrapper?.schema;if(!t)return null;for(const n of Object.keys(t)){const r=t[n];if(r&&(r.table===e||n===e))return r.columns||null}return null}_matchedForeignKeyType(e){return e.autoIncrement&&e.primary?{type:"BIGINT",size:null,columnType:"BIGINT",isUnsigned:!0}:{type:e.type,size:e.size,columnType:e.columnType,isUnsigned:e.isUnsigned}}_applyColumnToBuilder(e,t){if(t.autoIncrement&&t.primary)return this._buildIncrementsColumn(e,t);const n=this._typeBuilder(e,t.name,t);return this._applyColumnModifiers(n,t),n}_buildIncrementsColumn(e,t){const n=e.increments(t.name);return t.comment&&n.comment(t.comment),n}_applyColumnModifiers(e,t){if(this._applyConstraintModifiers(e,t),this._applyNullabilityAndDefault(e,t),t.comment&&e.comment(t.comment),t.hasForeignKey&&t.foreignMapTables?.[0]){const n=t.foreignMapTables[0];e.references(n.column||"id").inTable(n.table)}}_applyConstraintModifiers(e,t){t.primary&&e.primary(),t.unique&&e.unique(),t.isUnsigned&&this._supportsUnsigned()&&e.unsigned()}_applyNullabilityAndDefault(e,t){t.nullable?e.nullable():e.notNullable(),null!=t.default&&""!==t.default&&e.defaultTo(this._renderDefault(t.default))}_typeBuilder(e,t,n){const r=String(n.type||"").toUpperCase(),s=this._typeDispatcher()[r];return s?s(e,t,n):e.specificType(t,n.columnType||(n.size?`${r}(${n.size})`:r))}_typeDispatcher(){return BASE_TYPE_DISPATCHER}_renderDefault(e){const t=String(e).trim();return"CURRENT_TIMESTAMP"===t.toUpperCase()||"NOW()"===t.toUpperCase()?this.db.fn.now():/^-?\d+(\.\d+)?$/.test(t)?Number(t):"true"===t||"false"===t?"true"===t:t}_supportsUnsigned(){return!0}_getHelperUtility(){try{return new(require(`./${this._getClientName()}/HelperUtility`))}catch{return null}}async _applyExtras(e){}async _getRelations(e){return{}}async _listTables(){throw new Error("_listTables must be overridden by engine subclass")}_warnOnUnsupportedModifier(e,t,n){warnOnce(`${this._getClientName()}.${e}`,`[${this._getClientName()}] '${e}' modifier is not supported on this engine (seen on ${t}.${n}). See docs/agents/05-multi-db-parity.md.`)}}module.exports=BaseSyncTable;
|
|
@@ -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,o){if(null!==o)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("?? LIKE ?",[t,o]);break;default:Array.isArray(o)?e.orWhereIn(t,o):e.orWhere(t,r,o)}else e.orWhereNull(t)}_applyAndWhereCondition(e,t,r,o){if(null!==o)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("?? LIKE ?",[t,o]);break;default:Array.isArray(o)?e.whereIn(t,o):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,n]of Object.entries(e))if(t.startsWith(r)){const e=t.slice(r.length);e.includes(".")?i[e]=n:o[e]=n}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:n,value:s}=this.parseWhereValue(o);"AND"===t?this._applyAndWhereCondition(e,i,n,s):this._applyOrWhereCondition(e,i,n,s)}}async fetchRelatedRows(e,t,r={}){if(e.through){const 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}{const 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:n,withWhere:s={}}=e;if(!n){const e=this.getHookService().getModelInstance(o),n=`get${r.charAt(0).toUpperCase()+r.slice(1)}Relation`;if("function"==typeof e[n]){const{direct:l,nested:a}=this._getWithWhereForRelation(s,r),h={rows:t,relName:r,model:o,withTree:i,controller:this.controllerWrapper,relation:o.hasRelations[r],qb:this,db:this.db,withWhere:l,nestedWithWhere:a};await e[n](h)}else logger.warn(`Method ${n} not found in model ${o.name}`);return t}const l=t.map(e=>e[n.localKey]);let a=[];const h="one"===n?.type,{direct:c,nested:p}=this._getWithWhereForRelation(s,r);let u={...c};try{const e=this.utils.getModel(this.controllerWrapper,r);if(this.controllerWrapper?.hookService){await this.controllerWrapper.hookService.executeHasSoftDeleteHook(e)&&(u={...u,deleted_at:null})}}catch(e){}a=await this.fetchRelatedRows(n,l,u);const y=new Map;for(const e of a){const t=e[n.foreignKey];y.has(t)||y.set(t,[]),y.get(t).push(e)}for(const e of t){const t=e[n.localKey];h&&1==y.get(t)?.length?e[r]=y.get(t)[0]:e[r]=y.get(t)||[]}const d=Object.keys(i);for(const e of d){const o=i[e],n=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:n,relName:e,model:s,withTree:o,relation:s.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),{})}buildSelectQuery(e,t){const{where:r={},with:o,select:i,orderBy:n={column:"id",direction:"asc"},limit:s=10,offset:l=0,page:a,groupBy:h,having:c,distinct:p,join:u,leftJoin:y,rightJoin:d,innerJoin:f}=t,g=this.getQueryBuilder(e);i&&(Array.isArray(i)||"string"==typeof i)?g.select(i):g.select("*"),p&&(Array.isArray(p)||"string"==typeof p?g.distinct(p):g.distinct()),u&&this._applyJoins(g,u,"join"),y&&this._applyJoins(g,y,"leftJoin"),d&&this._applyJoins(g,d,"rightJoin"),f&&this._applyJoins(g,f,"innerJoin"),this._applyWhereClause(g,r,o),h&&g.groupBy(h),c&&this._applyHavingClause(g,c),n&&this._applyOrderBy(g,n);let W=l;return a&&s>0&&(W=(Math.max(1,parseInt(a))-1)*s),s>0&&(g.limit(s),W>0&&g.offset(W)),g}async getQuery(e,t){try{const{where:r={},with:o,withWhere:i,limit:n=10,offset:s=0,page:l,join:a,leftJoin:h,rightJoin:c,innerJoin:p}=t,u=this.buildSelectQuery(e,t);let y=!1;const d=n;let f=s,g=1,W=0;l&&n>0&&(g=Math.max(1,parseInt(l)),f=(g-1)*n);const w=[],b=u.toSQL();w.push(b.sql);const m=await u;if(o&&o.length>0){const t=this.buildWithTree(o);for(const r of Object.keys(t))await this.fetchAndAttachRelated({parentRows:m,relName:r,model:e,withTree:t[r],relation:e.hasRelations[r],withWhere:i||{}})}let _=null;if(n>0)try{const t=this.getQueryBuilder(e);r&&Object.keys(r).length>0&&this._applyWhereClause(t,r),a&&this._applyJoins(t,a,"join"),h&&this._applyJoins(t,h,"leftJoin"),c&&this._applyJoins(t,c,"rightJoin"),p&&this._applyJoins(t,p,"innerJoin");const o=t.count("* as cnt");w.push(o.toSQL().sql);_=(await o.first()).cnt}catch(e){logger.warn("Failed to get total count:",e.message),_=m.length}n>0&&null!==_&&(W=Math.ceil(_/n),y=g<W);const A=y?g+1:null,j=g>1?g-1:null;return{data:m,totalCount:_,...this.controllerWrapper.debug?{sqlDebug:w}:{},...n>0?{pagination:{page:g,limit:d,offset:f,totalPages:W,hasNext:y,hasPrev:g>1,nextPage:A,prevPage:j}}:{}}}catch(t){throw logger.error("QueryService.getQuery error:",t),new Error(`Failed to execute query: ${t.message} on model ${e.name}`)}}getSumQuery(e,t){const{where:r={},join:o,leftJoin:i,rightJoin:n,innerJoin:s}=t,l=t.data||t,a=l.sumColumn,h=l.sumFormula;if(!a&&!h)throw new Error("Sum action requires either data.sumColumn or data.sumFormula");const c=e=>"`"+String(e).replace(/[`\\]/g,"")+"`";let p;if(a){const e=String(a).trim().replace(/[^a-zA-Z0-9_]/g,"");if(!e)throw new Error("data.sumColumn must be a valid column name");p="SUM("+c(e)+")"}else{const e=String(h).trim();if(!/\{[a-zA-Z0-9_]+\}/.test(e))throw new Error("data.sumFormula must contain at least one {columnName}");let t=0;for(const r of e)if("("===r)t++;else if(")"===r&&(t--,t<0))break;if(0!==t)throw new Error("data.sumFormula has unbalanced or misordered parentheses ( and )");const r=e.replace(/\{[a-zA-Z0-9_]+\}/g,"@");if(!/^[\s0-9.+*\/\-()@]+$/.test(r))throw new Error("data.sumFormula may only use BODMAS: numbers, + - * /, parentheses, and {columnName}");p="SUM("+e.replace(/\{([a-zA-Z0-9_]+)\}/g,(e,t)=>c(t))+")"}const u=this.getQueryBuilder(e);return o&&this._applyJoins(u,o,"join"),i&&this._applyJoins(u,i,"leftJoin"),n&&this._applyJoins(u,n,"rightJoin"),s&&this._applyJoins(u,s,"innerJoin"),this._applyWhereClause(u,r,[]),u.select(this.db.raw(p+" as sum")),u}_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:n,value:s}=this.parseWhereValue(o);"AND"===r?this._applyAndWhereCondition(e,i,n,s):this._applyOrWhereCondition(e,i,n,s)}}this._applyNestedWhere(e,t,r)}}_getNestedWhereConditions(e,t){if(!e||"object"!=typeof e)return{};const r=`${t}.`,o={};for(const[i,n]of Object.entries(e))if(i.startsWith(r)){o[i.slice(r.length)]=n}else i===t&&!0===n&&(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){const o=this,i=e._getMyModel(),n=this._getTopLevelRelationsFromWhere(t);if(0!==n.length)for(const r of n){const n=r.startsWith("!"),s=n?r.slice(1):r,l=this._getNestedWhereConditions(t,r);if(0===Object.keys(l).length)continue;const a=i.hasRelations?.[s];if(!a){logger.warn(`Relation ${s} not found in model ${i.modelName}`);continue}let h;try{h=this.utils.getModel(this.controllerWrapper,a.table||s)}catch(e){try{h=this.utils.getModel(this.controllerWrapper,s)}catch(e){logger.warn(`Model for relation ${s} (table: ${a.table}) not found`);continue}}e[n?"whereNotExists":"whereExists"](function(){const e=o.getQueryBuilder(h,this.select("*").from(a.table));e.whereRaw("??.?? = ??.??",[a.table,a.foreignKey,i.table,a.localKey]);if(!(!0===l.__exists__&&1===Object.keys(l).length)){const t={...l};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
|
+
const HelperUtility=require("./HelperUtility"),logger=require("../../Logger"),KormError=require("../../KormError");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)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("?? LIKE ?",[t,o]);break;default:Array.isArray(o)?e.orWhereIn(t,o):e.orWhere(t,r,o)}else e.orWhereNull(t)}_applyAndWhereCondition(e,t,r,o){if(null!==o)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("?? LIKE ?",[t,o]);break;default:Array.isArray(o)?e.whereIn(t,o):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,n]of Object.entries(e))if(t.startsWith(r)){const e=t.slice(r.length);e.includes(".")?i[e]=n:o[e]=n}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:n,value:s}=this.parseWhereValue(o);"AND"===t?this._applyAndWhereCondition(e,i,n,s):this._applyOrWhereCondition(e,i,n,s)}}async fetchRelatedRows(e,t,r={}){if(e.through){const 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}{const 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:n,withWhere:s={}}=e;if(!n){const e=this.getHookService().getModelInstance(o),n=`get${r.charAt(0).toUpperCase()+r.slice(1)}Relation`;if("function"==typeof e[n]){const{direct:l,nested:a}=this._getWithWhereForRelation(s,r),h={rows:t,relName:r,model:o,withTree:i,controller:this.controllerWrapper,relation:o.hasRelations[r],qb:this,db:this.db,withWhere:l,nestedWithWhere:a};await e[n](h)}else logger.warn(`Method ${n} not found in model ${o.name}`);return t}const l=t.map(e=>e[n.localKey]);let a=[];const h="one"===n?.type,{direct:c,nested:p}=this._getWithWhereForRelation(s,r);let u={...c};try{const e=this.utils.getModel(this.controllerWrapper,r);if(this.controllerWrapper?.hookService){await this.controllerWrapper.hookService.executeHasSoftDeleteHook(e)&&(u={...u,deleted_at:null})}}catch(e){}a=await this.fetchRelatedRows(n,l,u);const y=new Map;for(const e of a){const t=e[n.foreignKey];y.has(t)||y.set(t,[]),y.get(t).push(e)}for(const e of t){const t=e[n.localKey];h&&1==y.get(t)?.length?e[r]=y.get(t)[0]:e[r]=y.get(t)||[]}const d=Object.keys(i);for(const e of d){const o=i[e],n=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:n,relName:e,model:s,withTree:o,relation:s.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),{})}buildSelectQuery(e,t){const{where:r={},with:o,select:i,orderBy:n={column:"id",direction:"asc"},limit:s=10,offset:l=0,page:a,groupBy:h,having:c,distinct:p,join:u,leftJoin:y,rightJoin:d,innerJoin:f}=t,g=this.getQueryBuilder(e);i&&(Array.isArray(i)||"string"==typeof i)?g.select(i):g.select("*"),p&&(Array.isArray(p)||"string"==typeof p?g.distinct(p):g.distinct()),u&&this._applyJoins(g,u,"join"),y&&this._applyJoins(g,y,"leftJoin"),d&&this._applyJoins(g,d,"rightJoin"),f&&this._applyJoins(g,f,"innerJoin"),this._applyWhereClause(g,r,o),h&&g.groupBy(h),c&&this._applyHavingClause(g,c),n&&this._applyOrderBy(g,n);let W=l;return a&&s>0&&(W=(Math.max(1,parseInt(a))-1)*s),s>0&&(g.limit(s),W>0&&g.offset(W)),g}async getQuery(e,t){try{const{where:r={},with:o,withWhere:i,limit:n=10,offset:s=0,page:l,join:a,leftJoin:h,rightJoin:c,innerJoin:p}=t,u=this.buildSelectQuery(e,t);let y=!1;const d=n;let f=s,g=1,W=0;l&&n>0&&(g=Math.max(1,parseInt(l)),f=(g-1)*n);const w=[],m=u.toSQL();w.push(m.sql);const b=await u;if(o&&o.length>0){const t=this.buildWithTree(o);for(const r of Object.keys(t))await this.fetchAndAttachRelated({parentRows:b,relName:r,model:e,withTree:t[r],relation:e.hasRelations[r],withWhere:i||{}})}let _=null;if(n>0)try{const t=this.getQueryBuilder(e);r&&Object.keys(r).length>0&&this._applyWhereClause(t,r),a&&this._applyJoins(t,a,"join"),h&&this._applyJoins(t,h,"leftJoin"),c&&this._applyJoins(t,c,"rightJoin"),p&&this._applyJoins(t,p,"innerJoin");const o=t.count("* as cnt");w.push(o.toSQL().sql);_=(await o.first()).cnt}catch(e){logger.warn("Failed to get total count:",e.message),_=b.length}n>0&&null!==_&&(W=Math.ceil(_/n),y=g<W);const j=y?g+1:null,A=g>1?g-1:null;return{data:b,totalCount:_,...this.controllerWrapper.debug?{sqlDebug:w}:{},...n>0?{pagination:{page:g,limit:d,offset:f,totalPages:W,hasNext:y,hasPrev:g>1,nextPage:j,prevPage:A}}:{}}}catch(t){throw logger.error("QueryService.getQuery error:",t),new Error(`Failed to execute query: ${t.message} on model ${e.name}`)}}getSumQuery(e,t){const{where:r={},join:o,leftJoin:i,rightJoin:n,innerJoin:s}=t,l=t.data||t,a=l.sumColumn,h=l.sumFormula;if(!a&&!h)throw new Error("Sum action requires either data.sumColumn or data.sumFormula");const c=e=>"`"+String(e).replace(/[`\\]/g,"")+"`";let p;if(a){const e=String(a).trim().replace(/[^a-zA-Z0-9_]/g,"");if(!e)throw new Error("data.sumColumn must be a valid column name");p="SUM("+c(e)+")"}else{const e=String(h).trim();if(!/\{[a-zA-Z0-9_]+\}/.test(e))throw new Error("data.sumFormula must contain at least one {columnName}");let t=0;for(const r of e)if("("===r)t++;else if(")"===r&&(t--,t<0))break;if(0!==t)throw new Error("data.sumFormula has unbalanced or misordered parentheses ( and )");const r=e.replace(/\{[a-zA-Z0-9_]+\}/g,"@");if(!/^[\s0-9.+*\/\-()@]+$/.test(r))throw new Error("data.sumFormula may only use BODMAS: numbers, + - * /, parentheses, and {columnName}");p="SUM("+e.replace(/\{([a-zA-Z0-9_]+)\}/g,(e,t)=>c(t))+")"}const u=this.getQueryBuilder(e);return o&&this._applyJoins(u,o,"join"),i&&this._applyJoins(u,i,"leftJoin"),n&&this._applyJoins(u,n,"rightJoin"),s&&this._applyJoins(u,s,"innerJoin"),this._applyWhereClause(u,r,[]),u.select(this.db.raw(p+" as sum")),u}_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:n,value:s}=this.parseWhereValue(o);"AND"===r?this._applyAndWhereCondition(e,i,n,s):this._applyOrWhereCondition(e,i,n,s)}}this._applyNestedWhere(e,t,r)}}_getNestedWhereConditions(e,t){if(!e||"object"!=typeof e)return{};const r=`${t}.`,o={};for(const[i,n]of Object.entries(e))if(i.startsWith(r)){o[i.slice(r.length)]=n}else i===t&&!0===n&&(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){const o=this,i=e._getMyModel(),n=this._getTopLevelRelationsFromWhere(t);if(0!==n.length)for(const r of n){const n=r.startsWith("!"),s=n?r.slice(1):r,l=this._getNestedWhereConditions(t,r);if(0===Object.keys(l).length)continue;const a=i.hasRelations?.[s];if(!a){logger.warn(`Relation ${s} not found in model ${i.modelName}`);continue}let h;try{h=this.utils.getModel(this.controllerWrapper,a.table||s)}catch(e){try{h=this.utils.getModel(this.controllerWrapper,s)}catch(e){logger.warn(`Model for relation ${s} (table: ${a.table}) not found`);continue}}e[n?"whereNotExists":"whereExists"](function(){const e=o.getQueryBuilder(h,this.select("*").from(a.table));e.whereRaw("??.?? = ??.??",[a.table,a.foreignKey,i.table,a.localKey]);if(!(!0===l.__exists__&&1===Object.keys(l).length)){const t={...l};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)this._applyOrderByEntry(e,r);else this._applyOrderByEntry(e,t)}_applyOrderByEntry(e,t){if("string"!=typeof t){if(t&&"object"==typeof t){if(!t.column)throw KormError.validationFailed({errors:[{field:"orderBy",message:`orderBy object must include a "column" key (got keys: ${Object.keys(t).join(", ")||"none"})`,value:t,rule:"required"}],source:"orderBy"});e.orderBy(t.column,t.direction||"asc")}}else e.orderBy(t)}}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,o){if(null!==o)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:Array.isArray(o)?e.orWhereIn(t,o):e.orWhere(t,r,o)}else e.orWhereNull(t)}_applyAndWhereCondition(e,t,r,o){if(null!==o)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:Array.isArray(o)?e.whereIn(t,o):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,n]of Object.entries(e))if(t.startsWith(r)){const e=t.slice(r.length);e.includes(".")?i[e]=n:o[e]=n}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:n,value:s}=this.parseWhereValue(o);"AND"===t?this._applyAndWhereCondition(e,i,n,s):this._applyOrWhereCondition(e,i,n,s)}}async fetchRelatedRows(e,t,r={}){if(e.through){const 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}{const 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:n,withWhere:s={}}=e;if(!n){const e=this.getHookService().getModelInstance(o),n=`get${r.charAt(0).toUpperCase()+r.slice(1)}Relation`;if("function"==typeof e[n]){const{direct:l,nested:a}=this._getWithWhereForRelation(s,r),h={rows:t,relName:r,model:o,withTree:i,controller:this.controllerWrapper,relation:o.hasRelations[r],qb:this,db:this.db,withWhere:l,nestedWithWhere:a};await e[n](h)}else logger.warn(`Method ${n} not found in model ${o.name}`);return t}const l=t.map(e=>e[n.localKey]),a="one"===n?.type;let h=[];const{direct:c,nested:p}=this._getWithWhereForRelation(s,r);let u={...c};try{const e=this.utils.getModel(this.controllerWrapper,r);if(this.controllerWrapper?.hookService){await this.controllerWrapper.hookService.executeHasSoftDeleteHook(e)&&(u={...u,deleted_at:null})}}catch(e){}h=await this.fetchRelatedRows(n,l,u);const y=new Map;for(const e of h){const t=e[n.foreignKey];y.has(t)||y.set(t,[]),y.get(t).push(e)}for(const e of t){const t=e[n.localKey];a&&1==y.get(t)?.length?e[r]=y.get(t)[0]:e[r]=y.get(t)||[]}const d=Object.keys(i);for(const e of d){const o=i[e],n=t.filter(e=>a?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:n,relName:e,model:s,withTree:o,relation:s.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),{})}buildSelectQuery(e,t){const{where:r={},with:o,select:i,orderBy:n={column:"id",direction:"asc"},limit:s=10,offset:l=0,page:a,groupBy:h,having:c,distinct:p,join:u,leftJoin:y,rightJoin:d,innerJoin:f}=t,g=this.getQueryBuilder(e);i&&(Array.isArray(i)||"string"==typeof i)?g.select(i):g.select("*"),p&&(Array.isArray(p)||"string"==typeof p?g.distinct(p):g.distinct()),u&&this._applyJoins(g,u,"join"),y&&this._applyJoins(g,y,"leftJoin"),d&&this._applyJoins(g,d,"rightJoin"),f&&this._applyJoins(g,f,"innerJoin"),this._applyWhereClause(g,r,o),h&&g.groupBy(h),c&&this._applyHavingClause(g,c),n&&this._applyOrderBy(g,n);let W=l;return a&&s>0&&(W=(Math.max(1,parseInt(a))-1)*s),s>0&&(g.limit(s),W>0&&g.offset(W)),g}async getQuery(e,t){try{const{where:r={},with:o,withWhere:i,limit:n=10,offset:s=0,page:l,join:a,leftJoin:h,rightJoin:c,innerJoin:p}=t,u=this.buildSelectQuery(e,t);let y=!1;const d=n;let f=s,g=1,W=0;l&&n>0&&(g=Math.max(1,parseInt(l)),f=(g-1)*n);const w=[],b=u.toSQL();w.push(b.sql);const m=await u;if(o&&o.length>0){const t=this.buildWithTree(o);for(const r of Object.keys(t))await this.fetchAndAttachRelated({parentRows:m,relName:r,model:e,withTree:t[r],relation:e.hasRelations[r],withWhere:i||{}})}let _=null;if(n>0)try{const t=this.getQueryBuilder(e);r&&Object.keys(r).length>0&&this._applyWhereClause(t,r),a&&this._applyJoins(t,a,"join"),h&&this._applyJoins(t,h,"leftJoin"),c&&this._applyJoins(t,c,"rightJoin"),p&&this._applyJoins(t,p,"innerJoin");const o=t.count("* as cnt");w.push(o.toSQL().sql);_=(await o.first()).cnt}catch(e){logger.warn("Failed to get total count:",e.message),_=m.length}n>0&&null!==_&&(W=Math.ceil(_/n),y=g<W);const A=y?g+1:null,j=g>1?g-1:null;return{data:m,totalCount:_,...this.controllerWrapper.debug?{sqlDebug:w}:{},...n>0?{pagination:{page:g,limit:d,offset:f,totalPages:W,hasNext:y,hasPrev:g>1,nextPage:A,prevPage:j}}:{}}}catch(t){throw logger.error("QueryService.getQuery error:",t),new Error(`Failed to execute query: ${t.message} on model ${e.name}`)}}getSumQuery(e,t){const{where:r={},join:o,leftJoin:i,rightJoin:n,innerJoin:s}=t,l=t.data||t,a=l.sumColumn,h=l.sumFormula;if(!a&&!h)throw new Error("Sum action requires either data.sumColumn or data.sumFormula");const c=e=>'"'+String(e).replace(/["\\]/g,"")+'"';let p;if(a){const e=String(a).trim().replace(/[^a-zA-Z0-9_]/g,"");if(!e)throw new Error("data.sumColumn must be a valid column name");p="SUM("+c(e)+")"}else{const e=String(h).trim();if(!/\{[a-zA-Z0-9_]+\}/.test(e))throw new Error("data.sumFormula must contain at least one {columnName}");let t=0;for(const r of e)if("("===r)t++;else if(")"===r&&(t--,t<0))break;if(0!==t)throw new Error("data.sumFormula has unbalanced or misordered parentheses ( and )");const r=e.replace(/\{[a-zA-Z0-9_]+\}/g,"@");if(!/^[\s0-9.+*\/\-()@]+$/.test(r))throw new Error("data.sumFormula may only use BODMAS: numbers, + - * /, parentheses, and {columnName}");p="SUM("+e.replace(/\{([a-zA-Z0-9_]+)\}/g,(e,t)=>c(t))+")"}const u=this.getQueryBuilder(e);return o&&this._applyJoins(u,o,"join"),i&&this._applyJoins(u,i,"leftJoin"),n&&this._applyJoins(u,n,"rightJoin"),s&&this._applyJoins(u,s,"innerJoin"),this._applyWhereClause(u,r,[]),u.select(this.db.raw(p+" as sum")),u}_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:n,value:s}=this.parseWhereValue(o);"AND"===r?this._applyAndWhereCondition(e,i,n,s):this._applyOrWhereCondition(e,i,n,s)}}this._applyNestedWhere(e,t,r)}}_getNestedWhereConditions(e,t){if(!e||"object"!=typeof e)return{};const r=`${t}.`,o={};for(const[i,n]of Object.entries(e))if(i.startsWith(r)){o[i.slice(r.length)]=n}else i===t&&!0===n&&(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){const o=this,i=e._getMyModel(),n=this._getTopLevelRelationsFromWhere(t);if(0!==n.length)for(const r of n){const n=r.startsWith("!"),s=n?r.slice(1):r,l=this._getNestedWhereConditions(t,r);if(0===Object.keys(l).length)continue;const a=i.hasRelations?.[s];if(!a){logger.warn(`Relation ${s} not found in model ${i.modelName}`);continue}let h;try{h=this.utils.getModel(this.controllerWrapper,a.table||s)}catch(e){try{h=this.utils.getModel(this.controllerWrapper,s)}catch(e){logger.warn(`Model for relation ${s} (table: ${a.table}) not found`);continue}}e[n?"whereNotExists":"whereExists"](function(){const e=o.getQueryBuilder(h,this.select("*").from(a.table));e.whereRaw("??.?? = ??.??",[a.table,a.foreignKey,i.table,a.localKey]);if(!(!0===l.__exists__&&1===Object.keys(l).length)){const t={...l};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
|
+
const HelperUtility=require("./HelperUtility"),logger=require("../../Logger"),KormError=require("../../KormError");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)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:Array.isArray(o)?e.orWhereIn(t,o):e.orWhere(t,r,o)}else e.orWhereNull(t)}_applyAndWhereCondition(e,t,r,o){if(null!==o)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:Array.isArray(o)?e.whereIn(t,o):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,n]of Object.entries(e))if(t.startsWith(r)){const e=t.slice(r.length);e.includes(".")?i[e]=n:o[e]=n}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:n,value:s}=this.parseWhereValue(o);"AND"===t?this._applyAndWhereCondition(e,i,n,s):this._applyOrWhereCondition(e,i,n,s)}}async fetchRelatedRows(e,t,r={}){if(e.through){const 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}{const 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:n,withWhere:s={}}=e;if(!n){const e=this.getHookService().getModelInstance(o),n=`get${r.charAt(0).toUpperCase()+r.slice(1)}Relation`;if("function"==typeof e[n]){const{direct:l,nested:a}=this._getWithWhereForRelation(s,r),h={rows:t,relName:r,model:o,withTree:i,controller:this.controllerWrapper,relation:o.hasRelations[r],qb:this,db:this.db,withWhere:l,nestedWithWhere:a};await e[n](h)}else logger.warn(`Method ${n} not found in model ${o.name}`);return t}const l=t.map(e=>e[n.localKey]),a="one"===n?.type;let h=[];const{direct:c,nested:p}=this._getWithWhereForRelation(s,r);let u={...c};try{const e=this.utils.getModel(this.controllerWrapper,r);if(this.controllerWrapper?.hookService){await this.controllerWrapper.hookService.executeHasSoftDeleteHook(e)&&(u={...u,deleted_at:null})}}catch(e){}h=await this.fetchRelatedRows(n,l,u);const y=new Map;for(const e of h){const t=e[n.foreignKey];y.has(t)||y.set(t,[]),y.get(t).push(e)}for(const e of t){const t=e[n.localKey];a&&1==y.get(t)?.length?e[r]=y.get(t)[0]:e[r]=y.get(t)||[]}const d=Object.keys(i);for(const e of d){const o=i[e],n=t.filter(e=>a?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:n,relName:e,model:s,withTree:o,relation:s.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),{})}buildSelectQuery(e,t){const{where:r={},with:o,select:i,orderBy:n={column:"id",direction:"asc"},limit:s=10,offset:l=0,page:a,groupBy:h,having:c,distinct:p,join:u,leftJoin:y,rightJoin:d,innerJoin:f}=t,g=this.getQueryBuilder(e);i&&(Array.isArray(i)||"string"==typeof i)?g.select(i):g.select("*"),p&&(Array.isArray(p)||"string"==typeof p?g.distinct(p):g.distinct()),u&&this._applyJoins(g,u,"join"),y&&this._applyJoins(g,y,"leftJoin"),d&&this._applyJoins(g,d,"rightJoin"),f&&this._applyJoins(g,f,"innerJoin"),this._applyWhereClause(g,r,o),h&&g.groupBy(h),c&&this._applyHavingClause(g,c),n&&this._applyOrderBy(g,n);let W=l;return a&&s>0&&(W=(Math.max(1,parseInt(a))-1)*s),s>0&&(g.limit(s),W>0&&g.offset(W)),g}async getQuery(e,t){try{const{where:r={},with:o,withWhere:i,limit:n=10,offset:s=0,page:l,join:a,leftJoin:h,rightJoin:c,innerJoin:p}=t,u=this.buildSelectQuery(e,t);let y=!1;const d=n;let f=s,g=1,W=0;l&&n>0&&(g=Math.max(1,parseInt(l)),f=(g-1)*n);const m=[],w=u.toSQL();m.push(w.sql);const b=await u;if(o&&o.length>0){const t=this.buildWithTree(o);for(const r of Object.keys(t))await this.fetchAndAttachRelated({parentRows:b,relName:r,model:e,withTree:t[r],relation:e.hasRelations[r],withWhere:i||{}})}let _=null;if(n>0)try{const t=this.getQueryBuilder(e);r&&Object.keys(r).length>0&&this._applyWhereClause(t,r),a&&this._applyJoins(t,a,"join"),h&&this._applyJoins(t,h,"leftJoin"),c&&this._applyJoins(t,c,"rightJoin"),p&&this._applyJoins(t,p,"innerJoin");const o=t.count("* as cnt");m.push(o.toSQL().sql);_=(await o.first()).cnt}catch(e){logger.warn("Failed to get total count:",e.message),_=b.length}n>0&&null!==_&&(W=Math.ceil(_/n),y=g<W);const j=y?g+1:null,k=g>1?g-1:null;return{data:b,totalCount:_,...this.controllerWrapper.debug?{sqlDebug:m}:{},...n>0?{pagination:{page:g,limit:d,offset:f,totalPages:W,hasNext:y,hasPrev:g>1,nextPage:j,prevPage:k}}:{}}}catch(t){throw logger.error("QueryService.getQuery error:",t),new Error(`Failed to execute query: ${t.message} on model ${e.name}`)}}getSumQuery(e,t){const{where:r={},join:o,leftJoin:i,rightJoin:n,innerJoin:s}=t,l=t.data||t,a=l.sumColumn,h=l.sumFormula;if(!a&&!h)throw new Error("Sum action requires either data.sumColumn or data.sumFormula");const c=e=>'"'+String(e).replace(/["\\]/g,"")+'"';let p;if(a){const e=String(a).trim().replace(/[^a-zA-Z0-9_]/g,"");if(!e)throw new Error("data.sumColumn must be a valid column name");p="SUM("+c(e)+")"}else{const e=String(h).trim();if(!/\{[a-zA-Z0-9_]+\}/.test(e))throw new Error("data.sumFormula must contain at least one {columnName}");let t=0;for(const r of e)if("("===r)t++;else if(")"===r&&(t--,t<0))break;if(0!==t)throw new Error("data.sumFormula has unbalanced or misordered parentheses ( and )");const r=e.replace(/\{[a-zA-Z0-9_]+\}/g,"@");if(!/^[\s0-9.+*\/\-()@]+$/.test(r))throw new Error("data.sumFormula may only use BODMAS: numbers, + - * /, parentheses, and {columnName}");p="SUM("+e.replace(/\{([a-zA-Z0-9_]+)\}/g,(e,t)=>c(t))+")"}const u=this.getQueryBuilder(e);return o&&this._applyJoins(u,o,"join"),i&&this._applyJoins(u,i,"leftJoin"),n&&this._applyJoins(u,n,"rightJoin"),s&&this._applyJoins(u,s,"innerJoin"),this._applyWhereClause(u,r,[]),u.select(this.db.raw(p+" as sum")),u}_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:n,value:s}=this.parseWhereValue(o);"AND"===r?this._applyAndWhereCondition(e,i,n,s):this._applyOrWhereCondition(e,i,n,s)}}this._applyNestedWhere(e,t,r)}}_getNestedWhereConditions(e,t){if(!e||"object"!=typeof e)return{};const r=`${t}.`,o={};for(const[i,n]of Object.entries(e))if(i.startsWith(r)){o[i.slice(r.length)]=n}else i===t&&!0===n&&(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){const o=this,i=e._getMyModel(),n=this._getTopLevelRelationsFromWhere(t);if(0!==n.length)for(const r of n){const n=r.startsWith("!"),s=n?r.slice(1):r,l=this._getNestedWhereConditions(t,r);if(0===Object.keys(l).length)continue;const a=i.hasRelations?.[s];if(!a){logger.warn(`Relation ${s} not found in model ${i.modelName}`);continue}let h;try{h=this.utils.getModel(this.controllerWrapper,a.table||s)}catch(e){try{h=this.utils.getModel(this.controllerWrapper,s)}catch(e){logger.warn(`Model for relation ${s} (table: ${a.table}) not found`);continue}}e[n?"whereNotExists":"whereExists"](function(){const e=o.getQueryBuilder(h,this.select("*").from(a.table));e.whereRaw("??.?? = ??.??",[a.table,a.foreignKey,i.table,a.localKey]);if(!(!0===l.__exists__&&1===Object.keys(l).length)){const t={...l};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)this._applyOrderByEntry(e,r);else this._applyOrderByEntry(e,t)}_applyOrderByEntry(e,t){if("string"!=typeof t){if(t&&"object"==typeof t){if(!t.column)throw KormError.validationFailed({errors:[{field:"orderBy",message:`orderBy object must include a "column" key (got keys: ${Object.keys(t).join(", ")||"none"})`,value:t,rule:"required"}],source:"orderBy"});e.orderBy(t.column,t.direction||"asc")}}else e.orderBy(t)}}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,o){if(null!==o)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:Array.isArray(o)?e.orWhereIn(t,o):e.orWhere(t,r,o)}else e.orWhereNull(t)}_applyAndWhereCondition(e,t,r,o){if(null!==o)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:Array.isArray(o)?e.whereIn(t,o):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,n]of Object.entries(e))if(t.startsWith(r)){const e=t.slice(r.length);e.includes(".")?i[e]=n:o[e]=n}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:n,value:s}=this.parseWhereValue(o);"AND"===t?this._applyAndWhereCondition(e,i,n,s):this._applyOrWhereCondition(e,i,n,s)}}async fetchRelatedRows(e,t,r={}){if(e.through){const 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}{const 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:n,withWhere:s={}}=e;if(!n){const e=this.getHookService().getModelInstance(o),n=`get${r.charAt(0).toUpperCase()+r.slice(1)}Relation`;if("function"==typeof e[n]){const{direct:l,nested:a}=this._getWithWhereForRelation(s,r),h={rows:t,relName:r,model:o,withTree:i,controller:this.controllerWrapper,relation:o.hasRelations[r],qb:this,db:this.db,withWhere:l,nestedWithWhere:a};await e[n](h)}else logger.warn(`Method ${n} not found in model ${o.name}`);return t}const l=t.map(e=>e[n.localKey]),a="one"===n?.type;let h=[];const{direct:c,nested:p}=this._getWithWhereForRelation(s,r);let u={...c};try{const e=this.utils.getModel(this.controllerWrapper,r);if(this.controllerWrapper?.hookService){await this.controllerWrapper.hookService.executeHasSoftDeleteHook(e)&&(u={...u,deleted_at:null})}}catch(e){}h=await this.fetchRelatedRows(n,l,u);const y=new Map;for(const e of h){const t=e[n.foreignKey];y.has(t)||y.set(t,[]),y.get(t).push(e)}for(const e of t){const t=e[n.localKey];a&&1==y.get(t)?.length?e[r]=y.get(t)[0]:e[r]=y.get(t)||[]}const d=Object.keys(i);for(const e of d){const o=i[e],n=t.filter(e=>a?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:n,relName:e,model:s,withTree:o,relation:s.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),{})}buildSelectQuery(e,t){const{where:r={},with:o,select:i,orderBy:n={column:"id",direction:"asc"},limit:s=10,offset:l=0,page:a,groupBy:h,having:c,distinct:p,join:u,leftJoin:y,rightJoin:d,innerJoin:f}=t,g=this.getQueryBuilder(e);i&&(Array.isArray(i)||"string"==typeof i)?g.select(i):g.select("*"),p&&(Array.isArray(p)||"string"==typeof p?g.distinct(p):g.distinct()),u&&this._applyJoins(g,u,"join"),y&&this._applyJoins(g,y,"leftJoin"),d&&this._applyJoins(g,d,"rightJoin"),f&&this._applyJoins(g,f,"innerJoin"),this._applyWhereClause(g,r,o),h&&g.groupBy(h),c&&this._applyHavingClause(g,c),n&&this._applyOrderBy(g,n);let W=l;return a&&s>0&&(W=(Math.max(1,parseInt(a))-1)*s),s>0&&(g.limit(s),W>0&&g.offset(W)),g}async getQuery(e,t){try{const{where:r={},with:o,withWhere:i,limit:n=10,offset:s=0,page:l,join:a,leftJoin:h,rightJoin:c,innerJoin:p}=t,u=this.buildSelectQuery(e,t);let y=!1;const d=n;let f=s,g=1,W=0;l&&n>0&&(g=Math.max(1,parseInt(l)),f=(g-1)*n);const w=[],m=u.toSQL();w.push(m.sql);const b=await u;if(o&&o.length>0){const t=this.buildWithTree(o);for(const r of Object.keys(t))await this.fetchAndAttachRelated({parentRows:b,relName:r,model:e,withTree:t[r],relation:e.hasRelations[r],withWhere:i||{}})}let _=null;if(n>0)try{const t=this.getQueryBuilder(e);r&&Object.keys(r).length>0&&this._applyWhereClause(t,r),a&&this._applyJoins(t,a,"join"),h&&this._applyJoins(t,h,"leftJoin"),c&&this._applyJoins(t,c,"rightJoin"),p&&this._applyJoins(t,p,"innerJoin");const o=t.count("* as cnt");w.push(o.toSQL().sql);_=(await o.first()).cnt}catch(e){logger.warn("Failed to get total count:",e.message),_=b.length}n>0&&null!==_&&(W=Math.ceil(_/n),y=g<W);const A=y?g+1:null,j=g>1?g-1:null;return{data:b,totalCount:_,...this.controllerWrapper.debug?{sqlDebug:w}:{},...n>0?{pagination:{page:g,limit:d,offset:f,totalPages:W,hasNext:y,hasPrev:g>1,nextPage:A,prevPage:j}}:{}}}catch(t){throw logger.error("QueryService.getQuery error:",t),new Error(`Failed to execute query: ${t.message} on model ${e.name}`)}}getSumQuery(e,t){const{where:r={},join:o,leftJoin:i,rightJoin:n,innerJoin:s}=t,l=t.data||t,a=l.sumColumn,h=l.sumFormula;if(!a&&!h)throw new Error("Sum action requires either data.sumColumn or data.sumFormula");const c=e=>'"'+String(e).replace(/["\\]/g,"")+'"';let p;if(a){const e=String(a).trim().replace(/[^a-zA-Z0-9_]/g,"");if(!e)throw new Error("data.sumColumn must be a valid column name");p="SUM("+c(e)+")"}else{const e=String(h).trim();if(!/\{[a-zA-Z0-9_]+\}/.test(e))throw new Error("data.sumFormula must contain at least one {columnName}");let t=0;for(const r of e)if("("===r)t++;else if(")"===r&&(t--,t<0))break;if(0!==t)throw new Error("data.sumFormula has unbalanced or misordered parentheses ( and )");const r=e.replace(/\{[a-zA-Z0-9_]+\}/g,"@");if(!/^[\s0-9.+*\/\-()@]+$/.test(r))throw new Error("data.sumFormula may only use BODMAS: numbers, + - * /, parentheses, and {columnName}");p="SUM("+e.replace(/\{([a-zA-Z0-9_]+)\}/g,(e,t)=>c(t))+")"}const u=this.getQueryBuilder(e);return o&&this._applyJoins(u,o,"join"),i&&this._applyJoins(u,i,"leftJoin"),n&&this._applyJoins(u,n,"rightJoin"),s&&this._applyJoins(u,s,"innerJoin"),this._applyWhereClause(u,r,[]),u.select(this.db.raw(p+" as sum")),u}_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:n,value:s}=this.parseWhereValue(o);"AND"===r?this._applyAndWhereCondition(e,i,n,s):this._applyOrWhereCondition(e,i,n,s)}}this._applyNestedWhere(e,t,r)}}_getNestedWhereConditions(e,t){if(!e||"object"!=typeof e)return{};const r=`${t}.`,o={};for(const[i,n]of Object.entries(e))if(i.startsWith(r)){o[i.slice(r.length)]=n}else i===t&&!0===n&&(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){const o=this,i=e._getMyModel(),n=this._getTopLevelRelationsFromWhere(t);if(0!==n.length)for(const r of n){const n=r.startsWith("!"),s=n?r.slice(1):r,l=this._getNestedWhereConditions(t,r);if(0===Object.keys(l).length)continue;const a=i.hasRelations?.[s];if(!a){logger.warn(`Relation ${s} not found in model ${i.modelName}`);continue}let h;try{h=this.utils.getModel(this.controllerWrapper,a.table||s)}catch(e){try{h=this.utils.getModel(this.controllerWrapper,s)}catch(e){logger.warn(`Model for relation ${s} (table: ${a.table}) not found`);continue}}e[n?"whereNotExists":"whereExists"](function(){const e=o.getQueryBuilder(h,this.select("*").from(a.table));e.whereRaw("??.?? = ??.??",[a.table,a.foreignKey,i.table,a.localKey]);if(!(!0===l.__exists__&&1===Object.keys(l).length)){const t={...l};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
|
+
const HelperUtility=require("./HelperUtility"),logger=require("../../Logger"),KormError=require("../../KormError");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)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:Array.isArray(o)?e.orWhereIn(t,o):e.orWhere(t,r,o)}else e.orWhereNull(t)}_applyAndWhereCondition(e,t,r,o){if(null!==o)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:Array.isArray(o)?e.whereIn(t,o):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,n]of Object.entries(e))if(t.startsWith(r)){const e=t.slice(r.length);e.includes(".")?i[e]=n:o[e]=n}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:n,value:s}=this.parseWhereValue(o);"AND"===t?this._applyAndWhereCondition(e,i,n,s):this._applyOrWhereCondition(e,i,n,s)}}async fetchRelatedRows(e,t,r={}){if(e.through){const 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}{const 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:n,withWhere:s={}}=e;if(!n){const e=this.getHookService().getModelInstance(o),n=`get${r.charAt(0).toUpperCase()+r.slice(1)}Relation`;if("function"==typeof e[n]){const{direct:l,nested:a}=this._getWithWhereForRelation(s,r),h={rows:t,relName:r,model:o,withTree:i,controller:this.controllerWrapper,relation:o.hasRelations[r],qb:this,db:this.db,withWhere:l,nestedWithWhere:a};await e[n](h)}else logger.warn(`Method ${n} not found in model ${o.name}`);return t}const l=t.map(e=>e[n.localKey]),a="one"===n?.type;let h=[];const{direct:c,nested:p}=this._getWithWhereForRelation(s,r);let u={...c};try{const e=this.utils.getModel(this.controllerWrapper,r);if(this.controllerWrapper?.hookService){await this.controllerWrapper.hookService.executeHasSoftDeleteHook(e)&&(u={...u,deleted_at:null})}}catch(e){}h=await this.fetchRelatedRows(n,l,u);const y=new Map;for(const e of h){const t=e[n.foreignKey];y.has(t)||y.set(t,[]),y.get(t).push(e)}for(const e of t){const t=e[n.localKey];a&&1==y.get(t)?.length?e[r]=y.get(t)[0]:e[r]=y.get(t)||[]}const d=Object.keys(i);for(const e of d){const o=i[e],n=t.filter(e=>a?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:n,relName:e,model:s,withTree:o,relation:s.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),{})}buildSelectQuery(e,t){const{where:r={},with:o,select:i,orderBy:n={column:"id",direction:"asc"},limit:s=10,offset:l=0,page:a,groupBy:h,having:c,distinct:p,join:u,leftJoin:y,rightJoin:d,innerJoin:f}=t,g=this.getQueryBuilder(e);i&&(Array.isArray(i)||"string"==typeof i)?g.select(i):g.select("*"),p&&(Array.isArray(p)||"string"==typeof p?g.distinct(p):g.distinct()),u&&this._applyJoins(g,u,"join"),y&&this._applyJoins(g,y,"leftJoin"),d&&this._applyJoins(g,d,"rightJoin"),f&&this._applyJoins(g,f,"innerJoin"),this._applyWhereClause(g,r,o),h&&g.groupBy(h),c&&this._applyHavingClause(g,c),n&&this._applyOrderBy(g,n);let W=l;return a&&s>0&&(W=(Math.max(1,parseInt(a))-1)*s),s>0&&(g.limit(s),W>0&&g.offset(W)),g}async getQuery(e,t){try{const{where:r={},with:o,withWhere:i,limit:n=10,offset:s=0,page:l,join:a,leftJoin:h,rightJoin:c,innerJoin:p}=t,u=this.buildSelectQuery(e,t);let y=!1;const d=n;let f=s,g=1,W=0;l&&n>0&&(g=Math.max(1,parseInt(l)),f=(g-1)*n);const m=[],w=u.toSQL();m.push(w.sql);const b=await u;if(o&&o.length>0){const t=this.buildWithTree(o);for(const r of Object.keys(t))await this.fetchAndAttachRelated({parentRows:b,relName:r,model:e,withTree:t[r],relation:e.hasRelations[r],withWhere:i||{}})}let _=null;if(n>0)try{const t=this.getQueryBuilder(e);r&&Object.keys(r).length>0&&this._applyWhereClause(t,r),a&&this._applyJoins(t,a,"join"),h&&this._applyJoins(t,h,"leftJoin"),c&&this._applyJoins(t,c,"rightJoin"),p&&this._applyJoins(t,p,"innerJoin");const o=t.count("* as cnt");m.push(o.toSQL().sql);_=(await o.first()).cnt}catch(e){logger.warn("Failed to get total count:",e.message),_=b.length}n>0&&null!==_&&(W=Math.ceil(_/n),y=g<W);const j=y?g+1:null,k=g>1?g-1:null;return{data:b,totalCount:_,...this.controllerWrapper.debug?{sqlDebug:m}:{},...n>0?{pagination:{page:g,limit:d,offset:f,totalPages:W,hasNext:y,hasPrev:g>1,nextPage:j,prevPage:k}}:{}}}catch(t){throw logger.error("QueryService.getQuery error:",t),new Error(`Failed to execute query: ${t.message} on model ${e.name}`)}}getSumQuery(e,t){const{where:r={},join:o,leftJoin:i,rightJoin:n,innerJoin:s}=t,l=t.data||t,a=l.sumColumn,h=l.sumFormula;if(!a&&!h)throw new Error("Sum action requires either data.sumColumn or data.sumFormula");const c=e=>'"'+String(e).replace(/["\\]/g,"")+'"';let p;if(a){const e=String(a).trim().replace(/[^a-zA-Z0-9_]/g,"");if(!e)throw new Error("data.sumColumn must be a valid column name");p="SUM("+c(e)+")"}else{const e=String(h).trim();if(!/\{[a-zA-Z0-9_]+\}/.test(e))throw new Error("data.sumFormula must contain at least one {columnName}");let t=0;for(const r of e)if("("===r)t++;else if(")"===r&&(t--,t<0))break;if(0!==t)throw new Error("data.sumFormula has unbalanced or misordered parentheses ( and )");const r=e.replace(/\{[a-zA-Z0-9_]+\}/g,"@");if(!/^[\s0-9.+*\/\-()@]+$/.test(r))throw new Error("data.sumFormula may only use BODMAS: numbers, + - * /, parentheses, and {columnName}");p="SUM("+e.replace(/\{([a-zA-Z0-9_]+)\}/g,(e,t)=>c(t))+")"}const u=this.getQueryBuilder(e);return o&&this._applyJoins(u,o,"join"),i&&this._applyJoins(u,i,"leftJoin"),n&&this._applyJoins(u,n,"rightJoin"),s&&this._applyJoins(u,s,"innerJoin"),this._applyWhereClause(u,r,[]),u.select(this.db.raw(p+" as sum")),u}_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:n,value:s}=this.parseWhereValue(o);"AND"===r?this._applyAndWhereCondition(e,i,n,s):this._applyOrWhereCondition(e,i,n,s)}}this._applyNestedWhere(e,t,r)}}_getNestedWhereConditions(e,t){if(!e||"object"!=typeof e)return{};const r=`${t}.`,o={};for(const[i,n]of Object.entries(e))if(i.startsWith(r)){o[i.slice(r.length)]=n}else i===t&&!0===n&&(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){const o=this,i=e._getMyModel(),n=this._getTopLevelRelationsFromWhere(t);if(0!==n.length)for(const r of n){const n=r.startsWith("!"),s=n?r.slice(1):r,l=this._getNestedWhereConditions(t,r);if(0===Object.keys(l).length)continue;const a=i.hasRelations?.[s];if(!a){logger.warn(`Relation ${s} not found in model ${i.modelName}`);continue}let h;try{h=this.utils.getModel(this.controllerWrapper,a.table||s)}catch(e){try{h=this.utils.getModel(this.controllerWrapper,s)}catch(e){logger.warn(`Model for relation ${s} (table: ${a.table}) not found`);continue}}e[n?"whereNotExists":"whereExists"](function(){const e=o.getQueryBuilder(h,this.select("*").from(a.table));e.whereRaw("??.?? = ??.??",[a.table,a.foreignKey,i.table,a.localKey]);if(!(!0===l.__exists__&&1===Object.keys(l).length)){const t={...l};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)this._applyOrderByEntry(e,r);else this._applyOrderByEntry(e,t)}_applyOrderByEntry(e,t){if("string"!=typeof t){if(t&&"object"==typeof t){if(!t.column)throw KormError.validationFailed({errors:[{field:"orderBy",message:`orderBy object must include a "column" key (got keys: ${Object.keys(t).join(", ")||"none"})`,value:t,rule:"required"}],source:"orderBy"});e.orderBy(t.column,t.direction||"asc")}}else e.orderBy(t)}}module.exports=QueryBuilder;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dreamtree-org/korm-js",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.59",
|
|
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",
|