@dreamtree-org/korm-js 1.1.1 → 1.1.3

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 Files=require("./helpers/files");class BaseHelperUtility{file=new Files;capitalize(e){return e.charAt(0).toUpperCase()+e.slice(1)}snakeCase(e){return e.replace(/([A-Z])/g,"_$1").toLowerCase()}camelCase(e){return e.replace(/([-_][a-z])/gi,e=>e.toUpperCase().replace("-","").replace("_",""))}kebabCase(e){return e.replace(/([A-Z])/g,"-$1").toLowerCase()}pascalCase(e){return e.replace(/([-_][a-z])/gi,e=>e.toUpperCase().replace("-","").replace("_",""))}pluralize(e){if(e.match(/[^aeiou]y$/i))return e.slice(0,-1)+"ies";if(e.match(/(s|x|z|ch|sh)$/i))return e+"es";if(e.match(/(f|fe)$/i)){if(e.endsWith("fe"))return e.slice(0,-2)+"ves";if(e.endsWith("f"))return e.slice(0,-1)+"ves"}return e.match(/(o)$/i)?e.length>1&&"aeiou".includes(e[e.length-2].toLowerCase())?e+"s":e+"es":e.match(/us$/i)?e.slice(0,-2)+"i":e.match(/is$/i)?e.slice(0,-2)+"es":e.match(/on$/i)||e.match(/um$/i)?e.slice(0,-2)+"a":e.match(/a$/i)?e+"s":e.match(/eau$/i)?e+"x":e.match(/ix$/i)||e.match(/ex$/i)?e.slice(0,-2)+"ices":e.match(/s$/i)?e:e+"s"}singularize(e){return String(e||"").trim().replace(/([b-df-hj-np-tv-z])ies$/i,"$1y").replace(/eaux$/i,"eau").replace(/oes$/i,"o").replace(/(xes|zes|ches|shes|sses)$/i,e=>e.slice(0,-2)).replace(/(us)es$/i,"$1").replace(/ves$/i,"f").replace(/([^s])s$/i,"$1")}modelName(e){let r=this.singularize(e);return r=this.camelCase(r),r=this.capitalize(r),r}dotParse(e,r,t=null){return this.dotWalk(e,{source:r,defaultValue:t})}dotWalk(e,r={}){const{source:t={},defaultValue:s=null,resolver:a=({current:e,part:r,source:t})=>e[r]}=r,i=e.split(".");let l=t;for(const e of i){const r=e.match(/^(\w+)\[(.*?)\]$/);if(r){const e=r[1],i=r[2]||0;if(l=l[e]||[],l=a({current:l,part:i,source:t}),void 0===l)return s}else{if(void 0===l[e])return s;if(l=a({current:l,part:e,source:t}),void 0===l)return s}}return l}dotWalkTree(e,r={}){const{resolver:t=({current:e,part:r,source:t})=>{}}=r,s={};for(const r of e){const e=r.split("."),a=e.length;let i=s,l=0;for(const n of e){const e=l===a-1;i[n]=i[n]||t({current:i,part:n,source:s,path:r,isLastPart:e}),i=i[n],l++}}return s}parseValue(e){return isNaN(e)||""===e?"true"===e.toLowerCase()||"false"!==e.toLowerCase()&&("null"===e.toLowerCase()?null:e):Number(e)}parseWhereValue(e){if("string"!=typeof e)return{operator:"=",value:e};const r=e.match(/^(!?)\[\]?(.+)$/);if(r){const[,e,t]=r;return{operator:e?"notIn":"in",value:t.split(",").map(e=>this.parseValue(e.trim()))}}if(e.startsWith("!")){const r=e.substring(1);return{operator:"!=",value:this.parseValue(r)}}if(e.includes("%"))return{operator:"like",value:e};const t=e.match(/^(><|<>)(.+)$/);if(t){const[,e,r]=t,s=r.split(",").map(e=>this.parseValue(e.trim()));if("><"===e){if(2===s.length)return{operator:"between",value:s}}else if("<>"===e&&2===s.length)return{operator:"notBetween",value:s}}const s=e.match(/^(>=|<=|<|>)(.+)$/);if(s){const[,e,r]=s;return{operator:e,value:this.parseValue(r)}}return{operator:"=",value:this.parseValue(e)}}parseWhereColumn(e){const r={joinType:"AND",column:e};return e.startsWith("Or:")&&(r.joinType="OR",r.column=e.substring(3)),r}objectFilter(e,r){return Object.keys(e).filter(t=>r(t,e[t])).reduce((r,t)=>(r[t]=e[t],r),{})}pluckDotWalkKey(e,r=1,t="."){const s=e.split(t);return[...s].splice(-1*s.length+r).join(t)}getDotWalkQuery(e,r=""){let t=this.dotWalkTree(Object.keys(e).filter(e=>e.includes(r)),{resolver:({current:r,part:t,source:s,path:a,isLastPart:i})=>i?e[a]:{}});return r&&(t=t[r]),t}setNested(e,r,t){const s={name:r,value:t},a=s.name.split(".");let i=e,l=i,n="";return a.length>1?(a.forEach(e=>{i=l,l[e]=l[e]||{},l=l[e],n=e}),i[n]=s.value):(n=s.name,i=e[n],e[n]=s.value),e}}module.exports=BaseHelperUtility;
1
+ const Files=require("./helpers/files");class BaseHelperUtility{file=new Files;capitalize(e){return e.charAt(0).toUpperCase()+e.slice(1)}snakeCase(e){return e.replace(/([A-Z])/g,"_$1").toLowerCase()}camelCase(e){return e.replace(/([-_][a-z])/gi,e=>e.toUpperCase().replace("-","").replace("_",""))}kebabCase(e){return e.replace(/([A-Z])/g,"-$1").toLowerCase()}pascalCase(e){return e.replace(/([-_][a-z])/gi,e=>e.toUpperCase().replace("-","").replace("_",""))}pluralize(e){if(e.match(/[^aeiou]y$/i))return e.slice(0,-1)+"ies";if(e.match(/(s|x|z|ch|sh)$/i))return e+"es";if(e.match(/(f|fe)$/i)){if(e.endsWith("fe"))return e.slice(0,-2)+"ves";if(e.endsWith("f"))return e.slice(0,-1)+"ves"}return e.match(/(o)$/i)?e.length>1&&"aeiou".includes(e[e.length-2].toLowerCase())?e+"s":e+"es":e.match(/us$/i)?e.slice(0,-2)+"i":e.match(/is$/i)?e.slice(0,-2)+"es":e.match(/on$/i)||e.match(/um$/i)?e.slice(0,-2)+"a":e.match(/a$/i)?e+"s":e.match(/eau$/i)?e+"x":e.match(/ix$/i)||e.match(/ex$/i)?e.slice(0,-2)+"ices":e.match(/s$/i)?e:e+"s"}singularize(e){return String(e||"").trim().replace(/([b-df-hj-np-tv-z])ies$/i,"$1y").replace(/eaux$/i,"eau").replace(/oes$/i,"o").replace(/(xes|zes|ches|shes|sses)$/i,e=>e.slice(0,-2)).replace(/(us)es$/i,"$1").replace(/ves$/i,"f").replace(/([^s])s$/i,"$1")}modelName(e){let r=this.singularize(e);return r=this.camelCase(r),r=this.capitalize(r),r}dotParse(e,r,t=null){return this.dotWalk(e,{source:r,defaultValue:t})}dotWalk(e,r={}){const{source:t={},defaultValue:s=null,resolver:a=({current:e,part:r,source:t})=>e[r]}=r,i=e.split(".");let l=t;for(const e of i){const r=e.match(/^(\w+)\[(.*?)\]$/);if(r){const e=r[1],i=r[2]||0;if(l=l[e]||[],l=a({current:l,part:i,source:t}),void 0===l)return s}else{if(void 0===l[e])return s;if(l=a({current:l,part:e,source:t}),void 0===l)return s}}return l}dotWalkTree(e,r={}){const{resolver:t=({current:e,part:r,source:t})=>{}}=r,s={};for(const r of e){const e=r.split("."),a=e.length;let i=s,l=0;for(const n of e){const e=l===a-1;i[n]=i[n]||t({current:i,part:n,source:s,path:r,isLastPart:e}),i=i[n],l++}}return s}parseValue(e){return isNaN(e)||""===e?"true"===e.toLowerCase()||"false"!==e.toLowerCase()&&("null"===e.toLowerCase()?null:e):Number(e)}parseWhereValue(e){if("string"!=typeof e)return{operator:"=",value:e};const r=e.match(/^(!?)\[\]?(.+)$/);if(r){const[,e,t]=r;return{operator:e?"notIn":"in",value:t.split(",").map(e=>this.parseValue(e.trim()))}}if(e.startsWith("!")){const r=e.substring(1);return{operator:"!=",value:this.parseValue(r)}}if(e.includes("%"))return{operator:"like",value:e};const t=e.match(/^(><|<>)(.+)$/);if(t){const[,e,r]=t,s=r.split(",").map(e=>this.parseValue(e.trim()));if("><"===e){if(2===s.length)return{operator:"between",value:s}}else if("<>"===e&&2===s.length)return{operator:"notBetween",value:s}}const s=e.match(/^(>=|<=|<|>)(.+)$/);if(s){const[,e,r]=s;return{operator:e,value:this.parseValue(r)}}return{operator:"=",value:this.parseValue(e)}}parseWhereColumn(e){const r={joinType:"AND",column:e};return e.startsWith("Or:")&&(r.joinType="OR",r.column=e.substring(3)),r}objectFilter(e,r){return Object.keys(e).filter(t=>r(t,e[t])).reduce((r,t)=>(r[t]=e[t],r),{})}pluckDotWalkKey(e,r=1,t="."){const s=e.split(t);return[...s].splice(-1*s.length+r).join(t)}getDotWalkQuery(e,r=""){let t=this.dotWalkTree(Object.keys(e).filter(e=>e.includes(r)),{resolver:({current:r,part:t,source:s,path:a,isLastPart:i})=>i?e[a]:{}});return r&&(t=t[r]),t}setNested(e,r,t){const s={name:r,value:t},a=s.name.split(".");let i=e,l=i,n="";return a.length>1?(a.forEach(e=>{i=l,l[e]=l[e]||{},l=l[e],n=e}),i[n]=s.value):(n=s.name,i=e[n],e[n]=s.value),e}resolveDefaultOrderBy(e){const r=e&&Array.isArray(e.columns)?e.columns:[],t=r.find(e=>e&&e.primary&&e.name);if(t)return{column:t.name,direction:"asc"};return r.some(e=>e&&"id"===e.name)||0===r.length?{column:"id",direction:"asc"}:null}}module.exports=BaseHelperUtility;
@@ -1 +1 @@
1
- const mysqlWrapper=require("./clients/mysql"),sqliteWrapper=require("./clients/sqlite"),pgWrapper=require("./clients/pg"),KormError=require("./KormError"),AuthorizationService=require("./AuthorizationService"),{buildModelRequestSchema:buildModelRequestSchema}=require("./requestSchema"),{buildModelDescription:buildModelDescription,SCHEMA_API_VERSION:SCHEMA_API_VERSION}=require("./schemaDescribe"),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;static _authz=new AuthorizationService;requestInstance=null;constructor(){this.requestInstance={}}static initializeKORM({db:e,dbClient:t,schema:s,resolverPath:i=null,debug:r=!1}){this.db=e,this.dbClient=t,this.schema=s,this.resolverPath=i,this.debug=r;const a=dbClientMapper[t];if(!a)throw new Error(`Database client ${t} not found`);const n=InstanceMapper[a];if(!n)throw new Error(`Database client ${t} not found`);return this.dbClientClass=n,this.dbInstance=new n(this),this}static setSchema(e){this.schema=e;const t=this.dbClientClass;if(!t)throw new Error(`Database client ${this.dbClient} not found`);return this.dbInstance=new t(this),this}static _resolveModelName(e){const t=this.schema||{};if(t[e])return e;return Object.keys(t).find(s=>t[s]&&t[s].table===e)||e}static authorize(e,t,s){return this._authz.registerAuthorize(this._resolveModelName(e),t,s),this}static scope(e,t){return this._authz.registerScope(this._resolveModelName(e),t),this}static resetAuthorization(){return this._authz.reset(),this}static async processRequest(e,t=null,s=null){let i=e;if(this._authz.hasRules()&&t){const r=this._resolveModelName(t),a=e&&e.action||"list";this._authz.enforce(r,a,e,s),i=this._authz.applyScope(r,a,e||{},s)}return await this.dbInstance.processRequest(i,t,s)}static async processRequestWithOthers(e,t=null,s=null){return await this.dbInstance.processRequest(e,t,s)}static async syncDatabase(e={}){return await this.dbInstance.syncDatabase(e)}static async generateSchema(){return await this.dbInstance.generateSchema()}static loadModelClass(e){return this.dbInstance.hookService.loadModelClass(e)}static getModelInstance(e){return this.dbInstance.hookService.getModelInstance(e)}static getRequestJsonSchema(e){const t=this.schema||{},s=t[e]||Object.values(t).find(t=>t&&t.table===e);if(!s)throw KormError.unknownModel({model:e,available:Object.keys(t)});return buildModelRequestSchema(s,{title:`KormRequest<${e}>`})}static _modelHasSoftDelete(e){try{const t=this.dbInstance?.hookService?.getModelInstance?.(e);return!(!t||!0!==t.hasSoftDelete)}catch{return!1}}static describeModel(e,t=null){const s=this.schema||{},i=Object.entries(s).find(([t,s])=>t===e||s&&s.table===e);if(!i)throw KormError.unknownModel({model:e,available:Object.keys(s)});const[r,a]=i,n=buildModelDescription(r,a,{softDelete:this._modelHasSoftDelete(r)});return null!=t&&this._authz.hasRules()&&(n.actions=this._authz.availableActions(r,n.actions,t)),n}static describeSchema(){const e=this.schema||{},t=Object.entries(e).map(([e,t])=>buildModelDescription(e,t,{softDelete:this._modelHasSoftDelete(e)}));return{schemaApiVersion:SCHEMA_API_VERSION,models:t}}}module.exports=ControllerWrapper;
1
+ const mysqlWrapper=require("./clients/mysql"),sqliteWrapper=require("./clients/sqlite"),pgWrapper=require("./clients/pg"),KormError=require("./KormError"),AuthorizationService=require("./AuthorizationService"),{buildModelRequestSchema:buildModelRequestSchema}=require("./requestSchema"),{buildModelDescription:buildModelDescription,SCHEMA_API_VERSION:SCHEMA_API_VERSION}=require("./schemaDescribe"),InstanceMapper={mysql2:mysqlWrapper,sqlite:sqliteWrapper,pg:pgWrapper},dbClientMapper={mysql2:"mysql2",mysql:"mysql2",pg:"pg",postgresql:"pg",sqlite:"sqlite",sqlite3:"sqlite"};class ControllerWrapper{db=null;dbClient=null;dbClientClass=null;schema=null;resolverPath=null;dbInstance=null;debug=!1;_authz=new AuthorizationService;requestInstance=null;constructor({db:e,dbClient:t,schema:s,resolverPath:r=null,debug:n=!1}={}){this.requestInstance={},this.db=e,this.dbClient=t,this.schema=s,this.resolverPath=r,this.debug=n;const i=dbClientMapper[t];if(!i)throw new Error(`Database client ${t} not found`);const l=InstanceMapper[i];if(!l)throw new Error(`Database client ${t} not found`);this.dbClientClass=l,this.dbInstance=new l(this)}static initializeKORM(e){return new ControllerWrapper(e)}setSchema(e){this.schema=e;const t=this.dbClientClass;if(!t)throw new Error(`Database client ${this.dbClient} not found`);return this.dbInstance=new t(this),this}_resolveModelName(e){const t=this.schema||{};if(t[e])return e;return Object.keys(t).find(s=>t[s]&&t[s].table===e)||e}authorize(e,t,s){return this._authz.registerAuthorize(this._resolveModelName(e),t,s),this}scope(e,t){return this._authz.registerScope(this._resolveModelName(e),t),this}resetAuthorization(){return this._authz.reset(),this}async processRequest(e,t=null,s=null){let r=e;if(this._authz.hasRules()&&t){const n=this._resolveModelName(t),i=e&&e.action||"list";this._authz.enforce(n,i,e,s),r=this._authz.applyScope(n,i,e||{},s)}return await this.dbInstance.processRequest(r,t,s)}async processRequestWithOthers(e,t=null,s=null){return await this.dbInstance.processRequest(e,t,s)}async syncDatabase(e={}){return await this.dbInstance.syncDatabase(e)}async generateSchema(){return await this.dbInstance.generateSchema()}loadModelClass(e){return this.dbInstance.hookService.loadModelClass(e)}getModelInstance(e){return this.dbInstance.hookService.getModelInstance(e)}getRequestJsonSchema(e){const t=this.schema||{},s=t[e]||Object.values(t).find(t=>t&&t.table===e);if(!s)throw KormError.unknownModel({model:e,available:Object.keys(t)});return buildModelRequestSchema(s,{title:`KormRequest<${e}>`})}_modelHasSoftDelete(e){try{const t=this.dbInstance?.hookService?.getModelInstance?.(e);return!(!t||!0!==t.hasSoftDelete)}catch{return!1}}describeModel(e,t=null){const s=this.schema||{},r=Object.entries(s).find(([t,s])=>t===e||s&&s.table===e);if(!r)throw KormError.unknownModel({model:e,available:Object.keys(s)});const[n,i]=r,l=buildModelDescription(n,i,{softDelete:this._modelHasSoftDelete(n)});return null!=t&&this._authz.hasRules()&&(l.actions=this._authz.availableActions(n,l.actions,t)),l}describeSchema(){const e=this.schema||{},t=Object.entries(e).map(([e,t])=>buildModelDescription(e,t,{softDelete:this._modelHasSoftDelete(e)}));return{schemaApiVersion:SCHEMA_API_VERSION,models:t}}}module.exports=ControllerWrapper;
package/README.md CHANGED
@@ -778,6 +778,13 @@ POST /api/Users/crud
778
778
  > generating broken SQL. Use `{ "column": "created_at", "direction": "desc" }`
779
779
  > or the string form `"created_at"`.
780
780
 
781
+ > **Default ordering.** When a `list`/`show` request omits `orderBy`, KORM
782
+ > orders by the model's **primary key** ascending — i.e. the first column
783
+ > flagged `primary` (which may be `uuid`, `code`, etc., not just `id`). If the
784
+ > model declares no primary key it falls back to an `id` column when one
785
+ > exists, and otherwise emits no `ORDER BY` at all. Pass an explicit `orderBy`
786
+ > to override.
787
+
781
788
  ### Joins
782
789
 
783
790
  ```javascript
@@ -38,24 +38,24 @@ In Express/Next/Fastify the consumer just forwards `req.body` and the model name
38
38
 
39
39
  `processRequest(request, modelName)` accepts a JSON object with these top-level fields:
40
40
 
41
- | Field | Type | Purpose |
42
- | ----------------------------------------------- | -------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ |
43
- | `action` | string (required) | The operation: `list`, `show`, `create`, `update`, `delete`, `count`, `sum`, `replace`, `upsert`, `sync` |
44
- | `where` | object \| array | Filter conditions for `list`/`show`/`update`/`delete`/`count`/`sum` |
45
- | `data` | object \| array | Payload for `create`/`update`/`upsert`/`replace`/`sync` |
46
- | `select` | array \| string | Columns to return (default: all) |
47
- | `with` | array of strings | Relations to eager-load (dot-nested allowed: `"Post.Comment"`) |
48
- | `withWhere` | object | Filters scoped to related rows only — does NOT filter parents |
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
- | `limit` | number | Max rows |
51
- | `offset` / `page` | number | Pagination |
52
- | `groupBy` | array \| string | GROUP BY columns |
53
- | `having` | object | Post-group filter |
54
- | `distinct` | boolean \| array \| string | DISTINCT / DISTINCT ON |
55
- | `join` / `innerJoin` / `leftJoin` / `rightJoin` | object \| array | Explicit joins (rarely needed — prefer `with`) |
56
- | `conflict` | array | Conflict columns for `upsert` / `sync` |
57
- | `other_requests` | object | Nested requests on related models; results returned under `other_responses` |
58
- | `dryRun` | boolean | If `true`, return the SQL that would run without executing it (see "Inspecting queries" below) |
41
+ | Field | Type | Purpose |
42
+ | ----------------------------------------------- | -------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
43
+ | `action` | string (required) | The operation: `list`, `show`, `create`, `update`, `delete`, `count`, `sum`, `replace`, `upsert`, `sync` |
44
+ | `where` | object \| array | Filter conditions for `list`/`show`/`update`/`delete`/`count`/`sum` |
45
+ | `data` | object \| array | Payload for `create`/`update`/`upsert`/`replace`/`sync` |
46
+ | `select` | array \| string | Columns to return (default: all) |
47
+ | `with` | array of strings | Relations to eager-load (dot-nested allowed: `"Post.Comment"`) |
48
+ | `withWhere` | object | Filters scoped to related rows only — does NOT filter parents |
49
+ | `orderBy` | object \| array \| string | `{column, direction}` / `"column"` / array of either. Object MUST use the `column` key — `{created_at:"desc"}` is rejected (`VALIDATION_FAILED`). Omitted → defaults to the model's primary key ascending (not always `id`) |
50
+ | `limit` | number | Max rows |
51
+ | `offset` / `page` | number | Pagination |
52
+ | `groupBy` | array \| string | GROUP BY columns |
53
+ | `having` | object | Post-group filter |
54
+ | `distinct` | boolean \| array \| string | DISTINCT / DISTINCT ON |
55
+ | `join` / `innerJoin` / `leftJoin` / `rightJoin` | object \| array | Explicit joins (rarely needed — prefer `with`) |
56
+ | `conflict` | array | Conflict columns for `upsert` / `sync` |
57
+ | `other_requests` | object | Nested requests on related models; results returned under `other_responses` |
58
+ | `dryRun` | boolean | If `true`, return the SQL that would run without executing it (see "Inspecting queries" below) |
59
59
 
60
60
  ### Actions
61
61
 
@@ -1 +1 @@
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
+ 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,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:n}=this.parseWhereValue(o);"AND"===t?this._applyAndWhereCondition(e,i,s,n):this._applyOrWhereCondition(e,i,s,n)}}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:s,withWhere:n={}}=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:l,nested:a}=this._getWithWhereForRelation(n,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[s](h)}else logger.warn(`Method ${s} not found in model ${o.name}`);return t}const l=t.map(e=>e[s.localKey]);let a=[];const h="one"===s?.type,{direct:c,nested:p}=this._getWithWhereForRelation(n,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(s,l,u);const y=new Map;for(const e of a){const t=e[s.foreignKey];y.has(t)||y.set(t,[]),y.get(t).push(e)}for(const e of t){const t=e[s.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],s=t.filter(e=>h?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:s,relName:e,model:n,withTree:o,relation:n.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:s,limit:n=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);const W=void 0===s?this.helperUtility.resolveDefaultOrderBy(e):s;W&&this._applyOrderBy(g,W);let w=l;return a&&n>0&&(w=(Math.max(1,parseInt(a))-1)*n),n>0&&(g.limit(n),w>0&&g.offset(w)),g}async getQuery(e,t){try{const{where:r={},with:o,withWhere:i,limit:s=10,offset:n=0,page:l,join:a,leftJoin:h,rightJoin:c,innerJoin:p}=t,u=this.buildSelectQuery(e,t);let y=!1;const d=s;let f=n,g=1,W=0;l&&s>0&&(g=Math.max(1,parseInt(l)),f=(g-1)*s);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(s>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}s>0&&null!==_&&(W=Math.ceil(_/s),y=g<W);const j=y?g+1:null,A=g>1?g-1:null;return{data:b,totalCount:_,...this.controllerWrapper.debug?{sqlDebug:w}:{},...s>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:s,innerJoin:n}=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"),s&&this._applyJoins(u,s,"rightJoin"),n&&this._applyJoins(u,n,"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:s,value:n}=this.parseWhereValue(o);"AND"===r?this._applyAndWhereCondition(e,i,s,n):this._applyOrWhereCondition(e,i,s,n)}}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){const o=this,i=e._getMyModel(),s=this._getTopLevelRelationsFromWhere(t);if(0!==s.length)for(const r of s){const s=r.startsWith("!"),n=s?r.slice(1):r,l=this._getNestedWhereConditions(t,r);if(0===Object.keys(l).length)continue;const a=i.hasRelations?.[n];if(!a){logger.warn(`Relation ${n} not found in model ${i.modelName}`);continue}let h;try{h=this.utils.getModel(this.controllerWrapper,a.table||n)}catch(e){try{h=this.utils.getModel(this.controllerWrapper,n)}catch(e){logger.warn(`Model for relation ${n} (table: ${a.table}) not found`);continue}}e[s?"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 QueryBuilder=require("./QueryBuilder");class QueryService{constructor(e,t,r=null){this.db=e,this.utils=t,this.controllerWrapper=r,this.queryBuilder=new QueryBuilder(e,t,r)}async getQuery(e,t){return await this.queryBuilder.getQuery(e,t)}async getSoftDeleteQuery(e,t){return await this.queryBuilder.getQuery(e,{...t,where:{...t.where||{},deleted_at:null}})}async executeShowQuery(e,t){const r=await this.getQuery(e,{...t,limit:1,offset:0});return r.data.length>0?r.data[0]:null}_buildCountQuery(e,t={}){const r=this.queryBuilder.getQueryBuilder(e),{join:u,leftJoin:i,rightJoin:a,innerJoin:n,where:s={}}=t;return u&&this.queryBuilder._applyJoins(r,u,"join"),i&&this.queryBuilder._applyJoins(r,i,"leftJoin"),a&&this.queryBuilder._applyJoins(r,a,"rightJoin"),n&&this.queryBuilder._applyJoins(r,n,"innerJoin"),this.queryBuilder._applyWhereClause(r,s,[]),r.count()}async executeCountQuery(e,t){const r=await this._buildCountQuery(e,t||{});return Object.values(r[0])[0]}async executeSumQuery(e,t){const r=await this.queryBuilder.getSumQuery(e,t).first(),u=r&&null!=r.sum?r.sum:0;return Number(u)}async executeCreateQuery(e,t){const r=await this.db(e.table).insert(t.data),u=Array.isArray(r)?r[0]:r;if(null==u)return[];const i=e.columns&&e.columns.find(e=>e.primary),a=i&&i.name?i.name:"id",n=await this.db(e.table).where(a,u).select("*");return Array.isArray(n)?n:[n]}async executeUpdateQuery(e,t){return await this.db.transaction(async r=>(await r(e.table).where(t.where).update(t.data),await r(e.table).where(t.where).first()))}async executeDeleteQuery(e,t){return await this.db(e.table).where(t.where).delete()}async executeSoftDeleteQuery(e,t){await this.db(e.table).where(t.where).update({deleted_at:new Date});const r=await this.db(e.table).where(t.where).select("*");return Array.isArray(r)?r:[r]}async executeUpsertQuery(e,t){return await this.db(e.table).insert(t.data).onConflict(t.conflict).merge(t.data)}_buildReplaceQuery(e,t){const{sql:r,bindings:u}=this.db(e.table).insert(t.data).toSQL();return this.db.raw(r.replace(/^\s*insert/i,"REPLACE"),u)}async executeReplaceQuery(e,t){return await this._buildReplaceQuery(e,t),t.data}buildDryRun(e,t,r){const u=this._dryRunBuilders(e,t,r).map(e=>{const t=e.toSQL();return{sql:t.sql,bindings:t.bindings}});return{success:!0,dryRun:!0,action:r,model:e.name,sql:u[0]?u[0].sql:null,bindings:u[0]?u[0].bindings:[],statements:u}}_dryRunBuilders(e,t,r){const u=e.table,i=t.where||{};switch(r){case"list":return[this.queryBuilder.buildSelectQuery(e,t)];case"show":return[this.queryBuilder.buildSelectQuery(e,{...t,limit:1,offset:0})];case"count":return[this._buildCountQuery(e,t)];case"sum":return[this.queryBuilder.getSumQuery(e,t)];case"create":return[this.db(u).insert(t.data).returning("*")];case"update":return[this.db(u).where(i).update(t.data).returning("*")];case"softDelete":return[this.db(u).where(i).update({deleted_at:new Date}).returning("*")];case"delete":return[this.db(u).where(i).delete()];case"replace":return[this._buildReplaceQuery(e,t)];case"upsert":return[this.db(u).insert(t.data).onConflict(t.conflict).merge(t.data)];case"sync":return[this.db(u).insert(t.data).onConflict(t.conflict).merge(t.data),this.db(u).where(i).delete()];default:return[]}}async executeSyncQuery(e,t){return this.db.transaction(async r=>({insertOrUpdateQuery:await r(e.table).insert(t.data).onConflict(t.conflict).merge(t.data),deleteQuery:await r(e.table).where(t.where).delete()}))}}module.exports=QueryService;
1
+ const QueryBuilder=require("./QueryBuilder");class QueryService{constructor(e,t,r=null){this.db=e,this.utils=t,this.controllerWrapper=r,this.queryBuilder=new QueryBuilder(e,t,r)}async getQuery(e,t){return await this.queryBuilder.getQuery(e,t)}async getSoftDeleteQuery(e,t){return await this.queryBuilder.getQuery(e,{...t,where:{...t.where||{},deleted_at:null}})}async executeShowQuery(e,t){const r=await this.getQuery(e,{...t,limit:1,offset:0});return r.data.length>0?r.data[0]:null}_buildCountQuery(e,t={}){const r=this.queryBuilder.getQueryBuilder(e),{join:u,leftJoin:i,rightJoin:a,innerJoin:n,where:s={}}=t;return u&&this.queryBuilder._applyJoins(r,u,"join"),i&&this.queryBuilder._applyJoins(r,i,"leftJoin"),a&&this.queryBuilder._applyJoins(r,a,"rightJoin"),n&&this.queryBuilder._applyJoins(r,n,"innerJoin"),this.queryBuilder._applyWhereClause(r,s,[]),r.count()}async executeCountQuery(e,t){const r=await this._buildCountQuery(e,t||{}),u=r&&r[0]?Object.values(r[0])[0]:0,i=Number(u);return Number.isNaN(i)?0:i}async executeSumQuery(e,t){const r=await this.queryBuilder.getSumQuery(e,t).first(),u=r&&null!=r.sum?r.sum:0;return Number(u)}async executeCreateQuery(e,t){const r=await this.db(e.table).insert(t.data),u=Array.isArray(r)?r[0]:r;if(null==u)return[];const i=e.columns&&e.columns.find(e=>e.primary),a=i&&i.name?i.name:"id",n=await this.db(e.table).where(a,u).select("*");return Array.isArray(n)?n:[n]}async executeUpdateQuery(e,t){return await this.db.transaction(async r=>(await r(e.table).where(t.where).update(t.data),await r(e.table).where(t.where).first()))}async executeDeleteQuery(e,t){return await this.db(e.table).where(t.where).delete()}async executeSoftDeleteQuery(e,t){await this.db(e.table).where(t.where).update({deleted_at:new Date});const r=await this.db(e.table).where(t.where).select("*");return Array.isArray(r)?r:[r]}async executeUpsertQuery(e,t){return await this.db(e.table).insert(t.data).onConflict(t.conflict).merge(t.data)}_buildReplaceQuery(e,t){const{sql:r,bindings:u}=this.db(e.table).insert(t.data).toSQL();return this.db.raw(r.replace(/^\s*insert/i,"REPLACE"),u)}async executeReplaceQuery(e,t){return await this._buildReplaceQuery(e,t),t.data}buildDryRun(e,t,r){const u=this._dryRunBuilders(e,t,r).map(e=>{const t=e.toSQL();return{sql:t.sql,bindings:t.bindings}});return{success:!0,dryRun:!0,action:r,model:e.name,sql:u[0]?u[0].sql:null,bindings:u[0]?u[0].bindings:[],statements:u}}_dryRunBuilders(e,t,r){const u=e.table,i=t.where||{};switch(r){case"list":return[this.queryBuilder.buildSelectQuery(e,t)];case"show":return[this.queryBuilder.buildSelectQuery(e,{...t,limit:1,offset:0})];case"count":return[this._buildCountQuery(e,t)];case"sum":return[this.queryBuilder.getSumQuery(e,t)];case"create":return[this.db(u).insert(t.data).returning("*")];case"update":return[this.db(u).where(i).update(t.data).returning("*")];case"softDelete":return[this.db(u).where(i).update({deleted_at:new Date}).returning("*")];case"delete":return[this.db(u).where(i).delete()];case"replace":return[this._buildReplaceQuery(e,t)];case"upsert":return[this.db(u).insert(t.data).onConflict(t.conflict).merge(t.data)];case"sync":return[this.db(u).insert(t.data).onConflict(t.conflict).merge(t.data),this.db(u).where(i).delete()];default:return[]}}async executeSyncQuery(e,t){return this.db.transaction(async r=>({insertOrUpdateQuery:await r(e.table).insert(t.data).onConflict(t.conflict).merge(t.data),deleteQuery:await r(e.table).where(t.where).delete()}))}}module.exports=QueryService;
@@ -1 +1 @@
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
+ 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,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:n}=this.parseWhereValue(o);"AND"===t?this._applyAndWhereCondition(e,i,s,n):this._applyOrWhereCondition(e,i,s,n)}}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:s,withWhere:n={}}=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:l,nested:a}=this._getWithWhereForRelation(n,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[s](h)}else logger.warn(`Method ${s} not found in model ${o.name}`);return t}const l=t.map(e=>e[s.localKey]),a="one"===s?.type;let h=[];const{direct:c,nested:p}=this._getWithWhereForRelation(n,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(s,l,u);const y=new Map;for(const e of h){const t=e[s.foreignKey];y.has(t)||y.set(t,[]),y.get(t).push(e)}for(const e of t){const t=e[s.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],s=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:s,relName:e,model:n,withTree:o,relation:n.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:s,limit:n=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);const W=void 0===s?this.helperUtility.resolveDefaultOrderBy(e):s;W&&this._applyOrderBy(g,W);let w=l;return a&&n>0&&(w=(Math.max(1,parseInt(a))-1)*n),n>0&&(g.limit(n),w>0&&g.offset(w)),g}async getQuery(e,t){try{const{where:r={},with:o,withWhere:i,limit:s=10,offset:n=0,page:l,join:a,leftJoin:h,rightJoin:c,innerJoin:p}=t,u=this.buildSelectQuery(e,t);let y=!1;const d=s;let f=n,g=1,W=0;l&&s>0&&(g=Math.max(1,parseInt(l)),f=(g-1)*s);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(s>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}s>0&&null!==_&&(W=Math.ceil(_/s),y=g<W);const j=y?g+1:null,k=g>1?g-1:null;return{data:b,totalCount:_,...this.controllerWrapper.debug?{sqlDebug:w}:{},...s>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:s,innerJoin:n}=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"),s&&this._applyJoins(u,s,"rightJoin"),n&&this._applyJoins(u,n,"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:s,value:n}=this.parseWhereValue(o);"AND"===r?this._applyAndWhereCondition(e,i,s,n):this._applyOrWhereCondition(e,i,s,n)}}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){const o=this,i=e._getMyModel(),s=this._getTopLevelRelationsFromWhere(t);if(0!==s.length)for(const r of s){const s=r.startsWith("!"),n=s?r.slice(1):r,l=this._getNestedWhereConditions(t,r);if(0===Object.keys(l).length)continue;const a=i.hasRelations?.[n];if(!a){logger.warn(`Relation ${n} not found in model ${i.modelName}`);continue}let h;try{h=this.utils.getModel(this.controllerWrapper,a.table||n)}catch(e){try{h=this.utils.getModel(this.controllerWrapper,n)}catch(e){logger.warn(`Model for relation ${n} (table: ${a.table}) not found`);continue}}e[s?"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 QueryBuilder=require("./QueryBuilder"),KormError=require("../../KormError");class QueryService{constructor(e,r,t=null){this.db=e,this.utils=r,this.controllerWrapper=t,this.queryBuilder=new QueryBuilder(e,r,t)}async getQuery(e,r){return await this.queryBuilder.getQuery(e,r)}async getSoftDeleteQuery(e,r){return await this.queryBuilder.getQuery(e,{...r,where:{...r.where||{},deleted_at:null}})}async executeShowQuery(e,r){const t=await this.getQuery(e,{...r,limit:1,offset:0});return t.data.length>0?t.data[0]:null}_buildCountQuery(e,r={}){const t=this.queryBuilder.getQueryBuilder(e),{join:u,leftJoin:i,rightJoin:n,innerJoin:a,where:s={}}=r;return u&&this.queryBuilder._applyJoins(t,u,"join"),i&&this.queryBuilder._applyJoins(t,i,"leftJoin"),n&&this.queryBuilder._applyJoins(t,n,"rightJoin"),a&&this.queryBuilder._applyJoins(t,a,"innerJoin"),this.queryBuilder._applyWhereClause(t,s,[]),t.count()}async executeCountQuery(e,r){const t=await this._buildCountQuery(e,r||{});return Object.values(t[0])[0]}async executeSumQuery(e,r){const t=await this.queryBuilder.getSumQuery(e,r).first(),u=t&&null!=t.sum?t.sum:0;return Number(u)}async executeCreateQuery(e,r){return await this.db(e.table).insert(r.data).returning("*")}async executeUpdateQuery(e,r){return await this.db(e.table).where(r.where).update(r.data).returning("*")}async executeDeleteQuery(e,r){return await this.db(e.table).where(r.where).delete()}async executeSoftDeleteQuery(e,r){return await this.db(e.table).where(r.where).update({deleted_at:new Date}).returning("*")}async executeUpsertQuery(e,r){return await this.db(e.table).insert(r.data).onConflict(r.conflict).merge(r.data)}_primaryKeyColumns(e){return(Array.isArray(e.columns)?e.columns:[]).filter(e=>e&&e.primary).map(e=>e.name)}_buildReplaceQuery(e,r){const t=r.conflict||this._primaryKeyColumns(e);if(!t||0===t.length)throw KormError.validationFailed({errors:[{field:"conflict",message:"replace on Postgres needs a primary key on the model or an explicit `conflict` field",rule:"required"}],source:"replace"});return this.db(e.table).insert(r.data).onConflict(t).merge(r.data)}async executeReplaceQuery(e,r){return await this._buildReplaceQuery(e,r)}buildDryRun(e,r,t){const u=this._dryRunBuilders(e,r,t).map(e=>{const r=e.toSQL();return{sql:r.sql,bindings:r.bindings}});return{success:!0,dryRun:!0,action:t,model:e.name,sql:u[0]?u[0].sql:null,bindings:u[0]?u[0].bindings:[],statements:u}}_dryRunBuilders(e,r,t){const u=e.table,i=r.where||{};switch(t){case"list":return[this.queryBuilder.buildSelectQuery(e,r)];case"show":return[this.queryBuilder.buildSelectQuery(e,{...r,limit:1,offset:0})];case"count":return[this._buildCountQuery(e,r)];case"sum":return[this.queryBuilder.getSumQuery(e,r)];case"create":return[this.db(u).insert(r.data).returning("*")];case"update":return[this.db(u).where(i).update(r.data).returning("*")];case"softDelete":return[this.db(u).where(i).update({deleted_at:new Date}).returning("*")];case"delete":return[this.db(u).where(i).delete()];case"replace":return[this._buildReplaceQuery(e,r)];case"upsert":return[this.db(u).insert(r.data).onConflict(r.conflict).merge(r.data)];case"sync":return[this.db(u).insert(r.data).onConflict(r.conflict).merge(r.data),this.db(u).where(i).delete()];default:return[]}}async executeSyncQuery(e,r){return this.db.transaction(async t=>({insertOrUpdateQuery:await t(e.table).insert(r.data).onConflict(r.conflict).merge(r.data),deleteQuery:await t(e.table).where(r.where).delete()}))}}module.exports=QueryService;
1
+ const QueryBuilder=require("./QueryBuilder"),KormError=require("../../KormError");class QueryService{constructor(e,r,t=null){this.db=e,this.utils=r,this.controllerWrapper=t,this.queryBuilder=new QueryBuilder(e,r,t)}async getQuery(e,r){return await this.queryBuilder.getQuery(e,r)}async getSoftDeleteQuery(e,r){return await this.queryBuilder.getQuery(e,{...r,where:{...r.where||{},deleted_at:null}})}async executeShowQuery(e,r){const t=await this.getQuery(e,{...r,limit:1,offset:0});return t.data.length>0?t.data[0]:null}_buildCountQuery(e,r={}){const t=this.queryBuilder.getQueryBuilder(e),{join:u,leftJoin:i,rightJoin:n,innerJoin:a,where:s={}}=r;return u&&this.queryBuilder._applyJoins(t,u,"join"),i&&this.queryBuilder._applyJoins(t,i,"leftJoin"),n&&this.queryBuilder._applyJoins(t,n,"rightJoin"),a&&this.queryBuilder._applyJoins(t,a,"innerJoin"),this.queryBuilder._applyWhereClause(t,s,[]),t.count()}async executeCountQuery(e,r){const t=await this._buildCountQuery(e,r||{}),u=t&&t[0]?Object.values(t[0])[0]:0,i=Number(u);return Number.isNaN(i)?0:i}async executeSumQuery(e,r){const t=await this.queryBuilder.getSumQuery(e,r).first(),u=t&&null!=t.sum?t.sum:0;return Number(u)}async executeCreateQuery(e,r){return await this.db(e.table).insert(r.data).returning("*")}async executeUpdateQuery(e,r){return await this.db(e.table).where(r.where).update(r.data).returning("*")}async executeDeleteQuery(e,r){return await this.db(e.table).where(r.where).delete()}async executeSoftDeleteQuery(e,r){return await this.db(e.table).where(r.where).update({deleted_at:new Date}).returning("*")}async executeUpsertQuery(e,r){return await this.db(e.table).insert(r.data).onConflict(r.conflict).merge(r.data)}_primaryKeyColumns(e){return(Array.isArray(e.columns)?e.columns:[]).filter(e=>e&&e.primary).map(e=>e.name)}_buildReplaceQuery(e,r){const t=r.conflict||this._primaryKeyColumns(e);if(!t||0===t.length)throw KormError.validationFailed({errors:[{field:"conflict",message:"replace on Postgres needs a primary key on the model or an explicit `conflict` field",rule:"required"}],source:"replace"});return this.db(e.table).insert(r.data).onConflict(t).merge(r.data)}async executeReplaceQuery(e,r){return await this._buildReplaceQuery(e,r)}buildDryRun(e,r,t){const u=this._dryRunBuilders(e,r,t).map(e=>{const r=e.toSQL();return{sql:r.sql,bindings:r.bindings}});return{success:!0,dryRun:!0,action:t,model:e.name,sql:u[0]?u[0].sql:null,bindings:u[0]?u[0].bindings:[],statements:u}}_dryRunBuilders(e,r,t){const u=e.table,i=r.where||{};switch(t){case"list":return[this.queryBuilder.buildSelectQuery(e,r)];case"show":return[this.queryBuilder.buildSelectQuery(e,{...r,limit:1,offset:0})];case"count":return[this._buildCountQuery(e,r)];case"sum":return[this.queryBuilder.getSumQuery(e,r)];case"create":return[this.db(u).insert(r.data).returning("*")];case"update":return[this.db(u).where(i).update(r.data).returning("*")];case"softDelete":return[this.db(u).where(i).update({deleted_at:new Date}).returning("*")];case"delete":return[this.db(u).where(i).delete()];case"replace":return[this._buildReplaceQuery(e,r)];case"upsert":return[this.db(u).insert(r.data).onConflict(r.conflict).merge(r.data)];case"sync":return[this.db(u).insert(r.data).onConflict(r.conflict).merge(r.data),this.db(u).where(i).delete()];default:return[]}}async executeSyncQuery(e,r){return this.db.transaction(async t=>({insertOrUpdateQuery:await t(e.table).insert(r.data).onConflict(r.conflict).merge(r.data),deleteQuery:await t(e.table).where(r.where).delete()}))}}module.exports=QueryService;
@@ -1 +1 @@
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;
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,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:n}=this.parseWhereValue(o);"AND"===t?this._applyAndWhereCondition(e,i,s,n):this._applyOrWhereCondition(e,i,s,n)}}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:s,withWhere:n={}}=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:l,nested:a}=this._getWithWhereForRelation(n,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[s](h)}else logger.warn(`Method ${s} not found in model ${o.name}`);return t}const l=t.map(e=>e[s.localKey]),a="one"===s?.type;let h=[];const{direct:c,nested:p}=this._getWithWhereForRelation(n,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(s,l,u);const y=new Map;for(const e of h){const t=e[s.foreignKey];y.has(t)||y.set(t,[]),y.get(t).push(e)}for(const e of t){const t=e[s.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],s=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:s,relName:e,model:n,withTree:o,relation:n.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:s,limit:n=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);const W=void 0===s?this.helperUtility.resolveDefaultOrderBy(e):s;W&&this._applyOrderBy(g,W);let w=l;return a&&n>0&&(w=(Math.max(1,parseInt(a))-1)*n),n>0&&(g.limit(n),w>0&&g.offset(w)),g}async getQuery(e,t){try{const{where:r={},with:o,withWhere:i,limit:s=10,offset:n=0,page:l,join:a,leftJoin:h,rightJoin:c,innerJoin:p}=t,u=this.buildSelectQuery(e,t);let y=!1;const d=s;let f=n,g=1,W=0;l&&s>0&&(g=Math.max(1,parseInt(l)),f=(g-1)*s);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(s>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}s>0&&null!==_&&(W=Math.ceil(_/s),y=g<W);const j=y?g+1:null,k=g>1?g-1:null;return{data:b,totalCount:_,...this.controllerWrapper.debug?{sqlDebug:w}:{},...s>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:s,innerJoin:n}=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"),s&&this._applyJoins(u,s,"rightJoin"),n&&this._applyJoins(u,n,"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:s,value:n}=this.parseWhereValue(o);"AND"===r?this._applyAndWhereCondition(e,i,s,n):this._applyOrWhereCondition(e,i,s,n)}}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){const o=this,i=e._getMyModel(),s=this._getTopLevelRelationsFromWhere(t);if(0!==s.length)for(const r of s){const s=r.startsWith("!"),n=s?r.slice(1):r,l=this._getNestedWhereConditions(t,r);if(0===Object.keys(l).length)continue;const a=i.hasRelations?.[n];if(!a){logger.warn(`Relation ${n} not found in model ${i.modelName}`);continue}let h;try{h=this.utils.getModel(this.controllerWrapper,a.table||n)}catch(e){try{h=this.utils.getModel(this.controllerWrapper,n)}catch(e){logger.warn(`Model for relation ${n} (table: ${a.table}) not found`);continue}}e[s?"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 QueryBuilder=require("./QueryBuilder");class QueryService{constructor(e,t,r=null){this.db=e,this.utils=t,this.controllerWrapper=r,this.queryBuilder=new QueryBuilder(e,t,r)}async getQuery(e,t){return await this.queryBuilder.getQuery(e,t)}async getSoftDeleteQuery(e,t){return await this.queryBuilder.getQuery(e,{...t,where:{...t.where||{},deleted_at:null}})}async executeShowQuery(e,t){const r=await this.getQuery(e,{...t,limit:1,offset:0});return r.data.length>0?r.data[0]:null}_buildCountQuery(e,t={}){const r=this.queryBuilder.getQueryBuilder(e),{join:u,leftJoin:i,rightJoin:n,innerJoin:a,where:s={}}=t;return u&&this.queryBuilder._applyJoins(r,u,"join"),i&&this.queryBuilder._applyJoins(r,i,"leftJoin"),n&&this.queryBuilder._applyJoins(r,n,"rightJoin"),a&&this.queryBuilder._applyJoins(r,a,"innerJoin"),this.queryBuilder._applyWhereClause(r,s,[]),r.count()}async executeCountQuery(e,t){const r=await this._buildCountQuery(e,t||{});return Object.values(r[0])[0]}async executeSumQuery(e,t){const r=await this.queryBuilder.getSumQuery(e,t).first(),u=r&&null!=r.sum?r.sum:0;return Number(u)}async executeCreateQuery(e,t){return await this.db(e.table).insert(t.data).returning("*")}async executeUpdateQuery(e,t){return await this.db(e.table).where(t.where).update(t.data).returning("*")}async executeDeleteQuery(e,t){return await this.db(e.table).where(t.where).delete()}async executeSoftDeleteQuery(e,t){return await this.db(e.table).where(t.where).update({deleted_at:new Date}).returning("*")}async executeUpsertQuery(e,t){return await this.db(e.table).insert(t.data).onConflict(t.conflict).merge(t.data)}_buildReplaceQuery(e,t){const{sql:r,bindings:u}=this.db(e.table).insert(t.data).toSQL();return this.db.raw(r.replace(/^\s*insert/i,"INSERT OR REPLACE"),u)}async executeReplaceQuery(e,t){return await this._buildReplaceQuery(e,t),t.data}buildDryRun(e,t,r){const u=this._dryRunBuilders(e,t,r).map(e=>{const t=e.toSQL();return{sql:t.sql,bindings:t.bindings}});return{success:!0,dryRun:!0,action:r,model:e.name,sql:u[0]?u[0].sql:null,bindings:u[0]?u[0].bindings:[],statements:u}}_dryRunBuilders(e,t,r){const u=e.table,i=t.where||{};switch(r){case"list":return[this.queryBuilder.buildSelectQuery(e,t)];case"show":return[this.queryBuilder.buildSelectQuery(e,{...t,limit:1,offset:0})];case"count":return[this._buildCountQuery(e,t)];case"sum":return[this.queryBuilder.getSumQuery(e,t)];case"create":return[this.db(u).insert(t.data).returning("*")];case"update":return[this.db(u).where(i).update(t.data).returning("*")];case"softDelete":return[this.db(u).where(i).update({deleted_at:new Date}).returning("*")];case"delete":return[this.db(u).where(i).delete()];case"replace":return[this._buildReplaceQuery(e,t)];case"upsert":return[this.db(u).insert(t.data).onConflict(t.conflict).merge(t.data)];case"sync":return[this.db(u).insert(t.data).onConflict(t.conflict).merge(t.data),this.db(u).where(i).delete()];default:return[]}}async executeSyncQuery(e,t){return this.db.transaction(async r=>({insertOrUpdateQuery:await r(e.table).insert(t.data).onConflict(t.conflict).merge(t.data),deleteQuery:await r(e.table).where(t.where).delete()}))}}module.exports=QueryService;
1
+ const QueryBuilder=require("./QueryBuilder");class QueryService{constructor(e,t,r=null){this.db=e,this.utils=t,this.controllerWrapper=r,this.queryBuilder=new QueryBuilder(e,t,r)}async getQuery(e,t){return await this.queryBuilder.getQuery(e,t)}async getSoftDeleteQuery(e,t){return await this.queryBuilder.getQuery(e,{...t,where:{...t.where||{},deleted_at:null}})}async executeShowQuery(e,t){const r=await this.getQuery(e,{...t,limit:1,offset:0});return r.data.length>0?r.data[0]:null}_buildCountQuery(e,t={}){const r=this.queryBuilder.getQueryBuilder(e),{join:u,leftJoin:i,rightJoin:n,innerJoin:a,where:s={}}=t;return u&&this.queryBuilder._applyJoins(r,u,"join"),i&&this.queryBuilder._applyJoins(r,i,"leftJoin"),n&&this.queryBuilder._applyJoins(r,n,"rightJoin"),a&&this.queryBuilder._applyJoins(r,a,"innerJoin"),this.queryBuilder._applyWhereClause(r,s,[]),r.count()}async executeCountQuery(e,t){const r=await this._buildCountQuery(e,t||{}),u=r&&r[0]?Object.values(r[0])[0]:0,i=Number(u);return Number.isNaN(i)?0:i}async executeSumQuery(e,t){const r=await this.queryBuilder.getSumQuery(e,t).first(),u=r&&null!=r.sum?r.sum:0;return Number(u)}async executeCreateQuery(e,t){return await this.db(e.table).insert(t.data).returning("*")}async executeUpdateQuery(e,t){return await this.db(e.table).where(t.where).update(t.data).returning("*")}async executeDeleteQuery(e,t){return await this.db(e.table).where(t.where).delete()}async executeSoftDeleteQuery(e,t){return await this.db(e.table).where(t.where).update({deleted_at:new Date}).returning("*")}async executeUpsertQuery(e,t){return await this.db(e.table).insert(t.data).onConflict(t.conflict).merge(t.data)}_buildReplaceQuery(e,t){const{sql:r,bindings:u}=this.db(e.table).insert(t.data).toSQL();return this.db.raw(r.replace(/^\s*insert/i,"INSERT OR REPLACE"),u)}async executeReplaceQuery(e,t){return await this._buildReplaceQuery(e,t),t.data}buildDryRun(e,t,r){const u=this._dryRunBuilders(e,t,r).map(e=>{const t=e.toSQL();return{sql:t.sql,bindings:t.bindings}});return{success:!0,dryRun:!0,action:r,model:e.name,sql:u[0]?u[0].sql:null,bindings:u[0]?u[0].bindings:[],statements:u}}_dryRunBuilders(e,t,r){const u=e.table,i=t.where||{};switch(r){case"list":return[this.queryBuilder.buildSelectQuery(e,t)];case"show":return[this.queryBuilder.buildSelectQuery(e,{...t,limit:1,offset:0})];case"count":return[this._buildCountQuery(e,t)];case"sum":return[this.queryBuilder.getSumQuery(e,t)];case"create":return[this.db(u).insert(t.data).returning("*")];case"update":return[this.db(u).where(i).update(t.data).returning("*")];case"softDelete":return[this.db(u).where(i).update({deleted_at:new Date}).returning("*")];case"delete":return[this.db(u).where(i).delete()];case"replace":return[this._buildReplaceQuery(e,t)];case"upsert":return[this.db(u).insert(t.data).onConflict(t.conflict).merge(t.data)];case"sync":return[this.db(u).insert(t.data).onConflict(t.conflict).merge(t.data),this.db(u).where(i).delete()];default:return[]}}async executeSyncQuery(e,t){return this.db.transaction(async r=>({insertOrUpdateQuery:await r(e.table).insert(t.data).onConflict(t.conflict).merge(t.data),deleteQuery:await r(e.table).where(t.where).delete()}))}}module.exports=QueryService;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dreamtree-org/korm-js",
3
- "version": "1.1.1",
3
+ "version": "1.1.3",
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",
@@ -48,12 +48,12 @@
48
48
  "license": "MIT",
49
49
  "repository": {
50
50
  "type": "git",
51
- "url": "git+https://github.com/preethamkrishnadev/korm-js.git"
51
+ "url": "git+https://github.com/DreamtreeTech/korm-js.git"
52
52
  },
53
53
  "bugs": {
54
- "url": "https://github.com/preethamkrishnadev/korm-js/issues"
54
+ "url": "https://github.com/DreamtreeTech/korm-js/issues"
55
55
  },
56
- "homepage": "https://www.npmjs.com/package/korm-js?activeTab=readme",
56
+ "homepage": "https://github.com/DreamtreeTech/korm-js#readme",
57
57
  "engines": {
58
58
  "node": ">=14.0.0"
59
59
  },
@@ -0,0 +1 @@
1
+ const TOKEN=process.env.PIPELINE_PROJECT_TOKEN,OWNER=process.env.PROJECT_OWNER||"DreamtreeTech",NUMBER=Number(process.env.PROJECT_NUMBER||"14"),READY=(process.env.READY_COMMAND||"/ready").toLowerCase(),DRY_RUN="true"===String(process.env.DRY_RUN||"").toLowerCase(),FROM_STATUS="Planning",TO_STATUS="Ready For Dev",INPUT_LABEL="needs-human-input",READY_LABEL="ready-for-dev",AUTHORIZED=new Set(["OWNER","MEMBER","COLLABORATOR"]),API="https://api.github.com";async function gh(e,n={}){const t=await fetch(`${API}${e}`,{...n,headers:{Authorization:`Bearer ${TOKEN}`,Accept:"application/vnd.github+json","X-GitHub-Api-Version":"2022-11-28",...n.body?{"Content-Type":"application/json"}:{},...n.headers}});if(!t.ok){const o=await t.text();throw new Error(`${n.method||"GET"} ${e} → ${t.status}: ${o.slice(0,300)}`)}return 204===t.status?null:t.json()}async function graphql(e,n){const t=await gh("/graphql",{method:"POST",body:JSON.stringify({query:e,variables:n})});if(t.errors)throw new Error(`GraphQL: ${JSON.stringify(t.errors).slice(0,400)}`);return t.data}const PROJECT_QUERY='\nquery($owner:String!, $number:Int!) {\n organization(login:$owner) {\n projectV2(number:$number) {\n id\n field(name:"Status") { ... on ProjectV2SingleSelectField { id options { id name } } }\n items(first:100) {\n totalCount\n pageInfo { hasNextPage }\n nodes {\n id\n status: fieldValueByName(name:"Status") {\n ... on ProjectV2ItemFieldSingleSelectValue { name }\n }\n content {\n __typename\n ... on Issue {\n number\n state\n url\n repository { nameWithOwner }\n labels(first:30) { nodes { name } }\n comments(last:100) {\n totalCount\n nodes { body authorAssociation author { login } }\n }\n }\n }\n }\n }\n }\n }\n}',SET_STATUS="\nmutation($project:ID!, $item:ID!, $field:ID!, $opt:String!) {\n updateProjectV2ItemFieldValue(input:{\n projectId:$project, itemId:$item, fieldId:$field, value:{singleSelectOptionId:$opt}\n }) { projectV2Item { id } }\n}";function findReadyAck(e){const n=e.comments?.nodes||[];return e.comments?.totalCount>n.length&&console.log(` note: #${e.number} has ${e.comments.totalCount} comments; only the latest ${n.length} were scanned.`),n.find(e=>{const n=String(e.body||"").trim().split("\n")[0].trim().toLowerCase();return(n===READY||n.startsWith(`${READY} `))&&AUTHORIZED.has(e.authorAssociation)})||null}function selectCandidates(e){const n=[];for(const t of e.items.nodes){const e=t.content;if(!e||"Issue"!==e.__typename||"OPEN"!==e.state)continue;if("Planning"!==t.status?.name)continue;if(!(e.labels?.nodes||[]).some(e=>e.name===INPUT_LABEL))continue;const o=findReadyAck(e);o&&n.push({itemId:t.id,issue:e,ack:o})}return n}async function advance(e,n,t,{itemId:o,issue:s,ack:a}){const i=s.repository.nameWithOwner,r=s.number;await graphql(SET_STATUS,{project:e.id,item:o,field:n.id,opt:t.id}),await gh(`/repos/${i}/issues/${r}/labels`,{method:"POST",body:JSON.stringify({labels:[READY_LABEL]})}),await gh(`/repos/${i}/issues/${r}/labels/${encodeURIComponent(INPUT_LABEL)}`,{method:"DELETE"}).catch(e=>console.log(` (label remove non-fatal: ${e.message})`));const c=`🟢 Advanced to **${TO_STATUS}** — \`${READY}\` from @${a.author?.login||"maintainer"}.\n\nThis issue is now eligible for the dev loop (docs/agents/12-autonomous-pipeline.md §3.3). An agent may pick it up; it will open a PR for human review — it will not merge.`;await gh(`/repos/${i}/issues/${r}/comments`,{method:"POST",body:JSON.stringify({body:c})})}async function main(){if(!TOKEN)return void console.log("::warning::PIPELINE_PROJECT_TOKEN not set — advance loop is inert. Provision a PAT with project+issues scope to activate (docs/agents/12 §7).");const e=await graphql(PROJECT_QUERY,{owner:OWNER,number:NUMBER}),n=e.organization?.projectV2;if(!n)throw new Error(`Project ${OWNER}/#${NUMBER} not found (token scope?).`);const t=n.field,o=t?.options?.find(e=>e.name===TO_STATUS);if(!o)throw new Error(`Status option "${TO_STATUS}" not found on the board.`);n.items.pageInfo.hasNextPage&&console.log("::warning::Board has >100 items; only the first 100 were scanned this run.");const s=selectCandidates(n);console.log(`Scanned ${n.items.totalCount} item(s); ${s.length} ready to advance`+(DRY_RUN?" (DRY RUN — no changes)":"")+".");let a=0;for(const e of s){const s=`#${e.issue.number} (${e.issue.url})`;if(DRY_RUN)console.log(` would advance ${s} — /ready by @${e.ack.author?.login}`);else try{await advance(n,t,o,e),a+=1,console.log(` advanced ${s}`)}catch(e){console.log(`::error::failed to advance ${s}: ${e.message}`)}}console.log(DRY_RUN?"Dry run complete.":`Advanced ${a}/${s.length} issue(s).`)}main().catch(e=>{console.error(`::error::advance loop failed: ${e.message}`),process.exit(1)});
@@ -0,0 +1 @@
1
+ const fs=require("fs"),TOKEN=process.env.PIPELINE_PROJECT_TOKEN,MAX_CONCURRENT=Number(process.env.MAX_CONCURRENT||"1"),OWNER=process.env.PROJECT_OWNER||"DreamtreeTech",NUMBER=Number(process.env.PROJECT_NUMBER||"14"),REPO=process.env.REPO||"DreamtreeTech/korm-js",DRY_RUN="true"===String(process.env.DRY_RUN||"").toLowerCase(),TASK_FILE=process.env.DEV_TASK_FILE||"pipeline/dev-task.md",READY_STATUS="Ready For Dev",IN_PROGRESS_STATUS="In Progress",IN_PROGRESS_LABEL="in-progress",DEP_RE=/(?:depends on|blocked by)\s+#(\d+)/gi,API="https://api.github.com";async function gh(e,n={}){const t=await fetch(`${API}${e}`,{...n,headers:{Authorization:`Bearer ${TOKEN}`,Accept:"application/vnd.github+json","X-GitHub-Api-Version":"2022-11-28",...n.body?{"Content-Type":"application/json"}:{},...n.headers}});if(!t.ok)throw new Error(`${n.method||"GET"} ${e} → ${t.status}: ${(await t.text()).slice(0,200)}`);return 204===t.status?null:t.json()}async function graphql(e,n){const t=await gh("/graphql",{method:"POST",body:JSON.stringify({query:e,variables:n})});if(t.errors)throw new Error(`GraphQL: ${JSON.stringify(t.errors).slice(0,400)}`);return t.data}const BOARD_QUERY='\nquery($owner:String!, $number:Int!) {\n organization(login:$owner) {\n projectV2(number:$number) {\n id\n field(name:"Status") { ... on ProjectV2SingleSelectField { id options { id name } } }\n items(first:100) {\n pageInfo { hasNextPage }\n nodes {\n id\n status: fieldValueByName(name:"Status") { ... on ProjectV2ItemFieldSingleSelectValue { name } }\n content {\n __typename\n ... on Issue {\n number url state title body\n labels(first:30) { nodes { name } }\n }\n }\n }\n }\n }\n }\n}',SET_STATUS="\nmutation($project:ID!, $item:ID!, $field:ID!, $opt:String!) {\n updateProjectV2ItemFieldValue(input:{\n projectId:$project, itemId:$item, fieldId:$field, value:{singleSelectOptionId:$opt}\n }) { projectV2Item { id } }\n}";async function dependenciesClear(e){const n=[...String(e.body||"").matchAll(DEP_RE)].map(e=>Number(e[1]));for(const t of n)try{if("closed"!==(await gh(`/repos/${REPO}/issues/${t}`)).state)return console.log(` #${e.number} waits on open dependency #${t}`),!1}catch{}return!0}async function getBoard(){const e=await graphql(BOARD_QUERY,{owner:OWNER,number:NUMBER}),n=e.organization?.projectV2;if(!n)throw new Error(`Project ${OWNER}/#${NUMBER} not found (token scope?).`);return n.items.pageInfo.hasNextPage&&console.log("::warning::Board has >100 items; only the first 100 were scanned."),{projectId:n.id,fieldId:n.field.id,options:n.field.options,items:n.items.nodes}}async function selectEligible(e){const n=[];for(const t of e){const e=t.content;e&&"Issue"===e.__typename&&"OPEN"===e.state&&(t.status?.name===READY_STATUS&&await dependenciesClear(e)&&n.push(t))}return n}async function claim(e,n){const t=e.options.find(e=>"In Progress"===e.name);if(!t)throw new Error('Status option "In Progress" not on the board');const o=n.content.number,s=`korm-dev/issue-${o}`;return await graphql(SET_STATUS,{project:e.projectId,item:n.id,field:e.fieldId,opt:t.id}),await gh(`/repos/${REPO}/issues/${o}/labels`,{method:"POST",body:JSON.stringify({labels:["in-progress"]})}),await gh(`/repos/${REPO}/issues/${o}/comments`,{method:"POST",body:JSON.stringify({body:`🛠️ Dev agent starting on branch \`${s}\`. It will open a PR for human review — it will not merge, push to main, or publish (docs/agents/12-autonomous-pipeline.md §2).`})}),s}function writeTaskBrief(e,n){const t=`# Implement issue #${e.number}\n\nBranch (already created for you): \`${n}\`\n\n## ${e.title}\n\n${e.body||"(no body)"}\n\n## Instructions\n- Follow docs/agents/* (SDLC, multi-DB parity, testing + coverage ratchet, SOLID, doc-sync).\n- Implement the acceptance criteria above; run \`npm test\`, \`npm run lint\`, \`npm run format\` until clean.\n- Do NOT run git or gh, commit, push, merge, tag, or publish — the workflow handles the branch, commit, push, and PR. Just edit files and make tests pass.\n- If you can't finish (tests won't pass, ambiguity, scope creep), STOP and explain what's blocking you — do not leave a half-done tree.\n`;fs.mkdirSync(require("path").dirname(TASK_FILE),{recursive:!0}),fs.writeFileSync(TASK_FILE,t)}function emit(e){const n=process.env.GITHUB_OUTPUT;n&&fs.appendFileSync(n,Object.entries(e).map(([e,n])=>`${e}=${n}`).join("\n")+"\n")}async function main(){if(!TOKEN)return void console.log("::warning::PIPELINE_PROJECT_TOKEN not set — dev dispatcher is inert (docs/agents/12 §7).");const e=await getBoard(),n=e.items.filter(e=>"In Progress"===e.status?.name).length,t=Math.max(0,MAX_CONCURRENT-n),o=await selectEligible(e.items);console.log(`Ready For Dev: ${o.length} eligible; ${n}/${MAX_CONCURRENT} in progress; ${t} slot(s) free.`);const s=t>=1?o[0]:null;if(!s)return void console.log("Nothing to dispatch this run.");if(DRY_RUN)return void console.log(` would claim + implement #${s.content.number}: "${s.content.title}"`);const r=await claim(e,s);writeTaskBrief(s.content,r),emit({issue:s.content.number,branch:r}),console.log(`Claimed #${s.content.number} → In Progress; task brief written; handing to Claude Code on ${r}.`)}main().catch(e=>{console.error(`::error::dev dispatcher failed: ${e.message}`),process.exit(1)});
@@ -0,0 +1 @@
1
+ const fs=require("fs"),TOKEN=process.env.PIPELINE_PROJECT_TOKEN,PROPOSALS_FILE=process.env.PROPOSALS_FILE||"pipeline/proposals.json",REPO=process.env.REPO||"DreamtreeTech/korm-js",OWNER=process.env.PROJECT_OWNER||"DreamtreeTech",NUMBER=Number(process.env.PROJECT_NUMBER||"14"),MAX_NEW=Number(process.env.MAX_NEW_ISSUES||"3"),MIN_CONFIDENCE=Number(process.env.MIN_CONFIDENCE||"0.6"),DRY_RUN="true"===String(process.env.DRY_RUN||"").toLowerCase(),PLANNING="Planning",BASE_LABELS=["agent-proposed","needs-human-input"],KIND_LABEL={bug:"bug",enhancement:"enhancement",epic:"enhancement"},MARKER=e=>`\x3c!-- korm-steward:key=${e} --\x3e`,MARKER_RE=/<!--\s*korm-steward:key=([^\s]+)\s*-->/,API="https://api.github.com";async function gh(e,n={}){const t=await fetch(`${API}${e}`,{...n,headers:{Authorization:`Bearer ${TOKEN}`,Accept:"application/vnd.github+json","X-GitHub-Api-Version":"2022-11-28",...n.body?{"Content-Type":"application/json"}:{},...n.headers}});if(!t.ok){const o=await t.text();throw new Error(`${n.method||"GET"} ${e} → ${t.status}: ${o.slice(0,300)}`)}return 204===t.status?null:t.json()}async function graphql(e,n){const t=await gh("/graphql",{method:"POST",body:JSON.stringify({query:e,variables:n})});if(t.errors)throw new Error(`GraphQL: ${JSON.stringify(t.errors).slice(0,400)}`);return t.data}const BOARD_QUERY='\nquery($owner:String!, $number:Int!) {\n organization(login:$owner) {\n projectV2(number:$number) {\n id\n field(name:"Status") { ... on ProjectV2SingleSelectField { id options { id name } } }\n }\n }\n}',ADD_ITEM="\nmutation($project:ID!, $content:ID!) {\n addProjectV2ItemById(input:{projectId:$project, contentId:$content}) { item { id } }\n}",SET_STATUS="\nmutation($project:ID!, $item:ID!, $field:ID!, $opt:String!) {\n updateProjectV2ItemFieldValue(input:{\n projectId:$project, itemId:$item, fieldId:$field, value:{singleSelectOptionId:$opt}\n }) { projectV2Item { id } }\n}";function normalizeTitle(e){return String(e||"").toLowerCase().replace(/[^a-z0-9]+/g," ").trim()}function loadProposals(){if(!fs.existsSync(PROPOSALS_FILE))return console.log(`No proposals file at ${PROPOSALS_FILE} — nothing to file.`),[];const e=JSON.parse(fs.readFileSync(PROPOSALS_FILE,"utf8")),n=Array.isArray(e)?e:e.proposals||[],t=[];for(const e of n)e&&e.title&&e.body&&e.dedupe_key&&"number"==typeof e.confidence?t.push({kind:"enhancement",questions:[],sub_issues:[],...e}):console.log(` skip (malformed): ${JSON.stringify(e).slice(0,120)}`);return t}async function fetchExisting(){const e=new Set,n=new Set,t=await gh(`/repos/${REPO}/issues?state=open&per_page=100`);for(const o of t){if(o.pull_request)continue;n.add(normalizeTitle(o.title));const t=MARKER_RE.exec(o.body||"");t&&e.add(t[1])}return{keys:e,titles:n}}function triage(e,n){const t=[],o=[],s=[];for(const i of e)n.keys.has(i.dedupe_key)||n.titles.has(normalizeTitle(i.title))?t.push(i):i.confidence<MIN_CONFIDENCE?o.push(i):s.push(i);return s.sort((e,n)=>n.confidence-e.confidence),{keep:s.slice(0,MAX_NEW),capped:s.slice(MAX_NEW),dup:t,lowConf:o}}function issueBody(e){return`${e.body.trim()}\n\n${MARKER(e.dedupe_key)}`}function questionComment(e){return`**Open questions before this is ready for dev** — answer inline, then comment \`/ready\` to advance (docs/agents/12-autonomous-pipeline.md).\n\n${(e.questions||[]).map(e=>`- [ ] ${e}`).join("\n")}`}async function createIssue(e,n){const t=[...BASE_LABELS,KIND_LABEL[e.kind]||"enhancement"],o=await gh(`/repos/${REPO}/issues`,{method:"POST",body:JSON.stringify({title:e.title,body:issueBody(e),labels:t})}),s=await graphql(ADD_ITEM,{project:n.projectId,content:o.node_id});return await graphql(SET_STATUS,{project:n.projectId,item:s.addProjectV2ItemById.item.id,field:n.statusFieldId,opt:n.planningOptionId}),(e.questions||[]).length&&await gh(`/repos/${REPO}/issues/${o.number}/comments`,{method:"POST",body:JSON.stringify({body:questionComment(e)})}),o}async function linkSubIssue(e,n){try{return await gh(`/repos/${REPO}/issues/${e.number}/sub_issues`,{method:"POST",body:JSON.stringify({sub_issue_id:n.id})}),"native"}catch(e){return console.log(` (sub-issue link non-fatal for #${n.number}: ${e.message})`),"fallback"}}async function fileEpic(e,n){const t=await createIssue(e,n);for(const o of e.sub_issues||[]){const s=await createIssue({...o,kind:"enhancement",confidence:e.confidence,dedupe_key:`${e.dedupe_key}--${normalizeTitle(o.title).replace(/\s+/g,"-").slice(0,40)}`,body:`${o.body}\n\n_Part of epic #${t.number}._`,questions:[]},n);await linkSubIssue(t,s)}return t}async function getBoard(){const e=await graphql(BOARD_QUERY,{owner:OWNER,number:NUMBER}),n=e.organization?.projectV2;if(!n)throw new Error(`Project ${OWNER}/#${NUMBER} not found (token scope?).`);const t=n.field,o=t?.options?.find(e=>e.name===PLANNING);if(!o)throw new Error(`Status option "${PLANNING}" not found on the board.`);return{projectId:n.id,statusFieldId:t.id,planningOptionId:o.id}}async function main(){if(!TOKEN)return void console.log("::warning::PIPELINE_PROJECT_TOKEN not set — steward FILE stage is inert. Provision a PAT with project+issues scope to activate (docs/agents/12 §7).");const e=loadProposals();if(0===e.length)return;const n=await fetchExisting(),{keep:t,capped:o,dup:s,lowConf:i}=triage(e,n);console.log(`${e.length} proposal(s): ${t.length} to file, ${s.length} dup, ${i.length} below confidence ${MIN_CONFIDENCE}, ${o.length} deferred (cap ${MAX_NEW})`+(DRY_RUN?" — DRY RUN":"")+".");for(const e of s)console.log(` dedupe: "${e.title}" (key=${e.dedupe_key})`);for(const e of i)console.log(` low-confidence ${e.confidence}: "${e.title}"`);for(const e of o)console.log(` deferred (cap): "${e.title}" (${e.confidence})`);if(DRY_RUN){for(const e of t)console.log(` would file [${e.kind} ${e.confidence}]: "${e.title}"`);return void console.log("Dry run complete.")}const r=await getBoard();let c=0;for(const e of t)try{const n="epic"===e.kind?await fileEpic(e,r):await createIssue(e,r);c+=1,console.log(` filed #${n.number}: "${e.title}"`)}catch(n){console.log(`::error::failed to file "${e.title}": ${n.message}`)}console.log(`Filed ${c}/${t.length} proposal(s) to Planning.`)}main().catch(e=>{console.error(`::error::steward file stage failed: ${e.message}`),process.exit(1)});
@@ -0,0 +1 @@
1
+ const TOKEN=process.env.PIPELINE_PROJECT_TOKEN,ISSUE=Number(process.env.ISSUE||"0"),PR_URL=(process.env.PR_URL||"").trim(),FAIL_REASON=(process.env.FAIL_REASON||"").trim(),OWNER=process.env.PROJECT_OWNER||"DreamtreeTech",NUMBER=Number(process.env.PROJECT_NUMBER||"14"),REPO=process.env.REPO||"DreamtreeTech/korm-js",TESTING_STATUS="Testing",BLOCKED_STATUS="Blocked",API="https://api.github.com";async function gh(e,n={}){const o=await fetch(`${API}${e}`,{...n,headers:{Authorization:`Bearer ${TOKEN}`,Accept:"application/vnd.github+json","X-GitHub-Api-Version":"2022-11-28",...n.body?{"Content-Type":"application/json"}:{},...n.headers}});if(!o.ok)throw new Error(`${n.method||"GET"} ${e} → ${o.status}: ${(await o.text()).slice(0,200)}`);return 204===o.status?null:o.json()}async function graphql(e,n){const o=await gh("/graphql",{method:"POST",body:JSON.stringify({query:e,variables:n})});if(o.errors)throw new Error(`GraphQL: ${JSON.stringify(o.errors).slice(0,400)}`);return o.data}const BOARD_QUERY='\nquery($owner:String!, $number:Int!) {\n organization(login:$owner) {\n projectV2(number:$number) {\n id\n field(name:"Status") { ... on ProjectV2SingleSelectField { id options { id name } } }\n items(first:100) {\n nodes { id content { __typename ... on Issue { number } } }\n }\n }\n }\n}',SET_STATUS="\nmutation($project:ID!, $item:ID!, $field:ID!, $opt:String!) {\n updateProjectV2ItemFieldValue(input:{\n projectId:$project, itemId:$item, fieldId:$field, value:{singleSelectOptionId:$opt}\n }) { projectV2Item { id } }\n}";async function comment(e){await gh(`/repos/${REPO}/issues/${ISSUE}/comments`,{method:"POST",body:JSON.stringify({body:e})})}async function main(){if(!TOKEN)return void console.log("::warning::PIPELINE_PROJECT_TOKEN not set — finalize is inert.");if(!ISSUE)return void console.log("No ISSUE to finalize (dispatcher claimed nothing); nothing to do.");const e=await graphql(BOARD_QUERY,{owner:OWNER,number:NUMBER}),n=e.organization?.projectV2,o=n.items.nodes.find(e=>e.content?.number===ISSUE);if(!o)throw new Error(`Issue #${ISSUE} not found on the board.`);const t=async e=>{const t=n.field.options.find(n=>n.name===e);if(!t)throw new Error(`Status option "${e}" not on the board`);await graphql(SET_STATUS,{project:n.id,item:o.id,field:n.field.id,opt:t.id})};PR_URL?(await t("Testing"),await comment(`✅ Dev agent opened a PR for review: ${PR_URL}\n\nMerging is a human decision (CI + review gate it).`),console.log(`#${ISSUE} → Testing (${PR_URL})`)):(await t("Blocked"),await gh(`/repos/${REPO}/issues/${ISSUE}/labels`,{method:"POST",body:JSON.stringify({labels:["blocked"]})}).catch(()=>{}),await comment(`⛔ Dev agent did not open a PR — moved to **Blocked**. ${FAIL_REASON||"The agent failed, was blocked, or produced no changes."} See the workflow run for details; a human can take it from here.`),console.log(`#${ISSUE} → Blocked`))}main().catch(e=>{console.error(`::error::finalize-dev failed: ${e.message}`),process.exit(1)});