@dreamtree-org/korm-js 1.0.55 → 1.0.56

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.
@@ -0,0 +1 @@
1
+ const KormError=require("./KormError"),WHERE_SCOPED_ACTIONS=new Set(["list","show","count","sum","update","delete"]),DATA_SCOPED_ACTIONS=new Set(["create","replace","upsert","sync"]);function mergeWhere(e,r){return Array.isArray(e)?[...e,{...r}]:{...e||{},...r}}function stampData(e,r){return Array.isArray(e)?e.map(e=>({...e&&"object"==typeof e?e:{},...r})):e&&"object"==typeof e?{...e,...r}:e}class AuthorizationService{constructor(){this.authorizers=new Map,this.scopers=new Map}reset(){this.authorizers.clear(),this.scopers.clear()}hasRules(){return this.authorizers.size>0||this.scopers.size>0}registerAuthorize(e,r,t){if("function"!=typeof t)throw new KormError("authorize(model, action, predicate): predicate must be a function.",{code:KormError.CODES.INTERNAL,context:{model:e,action:r}});this.authorizers.set(`${e}.${r}`,t)}registerScope(e,r){if("function"!=typeof r)throw new KormError("scope(model, fn): fn must be a function.",{code:KormError.CODES.INTERNAL,context:{model:e}});const t=this.scopers.get(e)||[];t.push(r),this.scopers.set(e,t)}_predicateFor(e,r){return this.authorizers.get(`${e}.${r}`)||this.authorizers.get(`${e}.*`)||null}isAllowed(e,r,t,o){const s=this._predicateFor(e,r);if(!s)return!0;try{return!!s(t,o)}catch{return!1}}enforce(e,r,t,o){if(!this.isAllowed(e,r,t,o))throw KormError.forbidden({model:e,action:r})}applyScope(e,r,t,o){const s=this.scopers.get(e);if(!s||0===s.length)return t;const i={};for(const e of s){const r=e(t,o);r&&"object"==typeof r&&Object.assign(i,r)}if(0===Object.keys(i).length)return t;const n={...t};return(WHERE_SCOPED_ACTIONS.has(r)||"sync"===r)&&(n.where=mergeWhere(t.where,i)),DATA_SCOPED_ACTIONS.has(r)&&(n.data=stampData(t.data,i)),n}availableActions(e,r,t){return r.filter(r=>this.isAllowed(e,r,{action:r},t))}}module.exports=AuthorizationService,module.exports.WHERE_SCOPED_ACTIONS=WHERE_SCOPED_ACTIONS,module.exports.DATA_SCOPED_ACTIONS=DATA_SCOPED_ACTIONS;
@@ -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};if(e.startsWith("!")){const r=e.substring(1);return{operator:"!=",value:this.parseValue(r)}}if(e.includes("%"))return{operator:"like",value:e};const r=e.match(/^(><|<>)(.+)$/);if(r){const[,e,t]=r,s=t.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 t=e.match(/^(>=|<=|<|>)(.+)$/);if(t){const[,e,r]=t;return{operator:e,value:this.parseValue(r)}}const s=e.match(/^(!?)\[\]?(.+)$/);if(s){const[,e,r]=s;return{operator:e?"notIn":"in",value:r.split(",").map(e=>this.parseValue(e.trim()))}}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}}module.exports=BaseHelperUtility;
@@ -1 +1 @@
1
- const mysqlWrapper=require("./clients/mysql"),sqliteWrapper=require("./clients/sqlite"),pgWrapper=require("./clients/pg"),KormError=require("./KormError"),{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;requestInstance=null;constructor(){this.requestInstance={}}static initializeKORM({db:e,dbClient:t,schema:s,resolverPath:r=null,debug:a=!1}){this.db=e,this.dbClient=t,this.schema=s,this.resolverPath=r,this.debug=a;const n=dbClientMapper[t];if(!n)throw new Error(`Database client ${t} not found`);const i=InstanceMapper[n];if(!i)throw new Error(`Database client ${t} not found`);return this.dbClientClass=i,this.dbInstance=new i(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 async processRequest(e,t=null,s=null){return await this.dbInstance.processRequest(e,t,s)}static async processRequestWithOthers(e,t=null,s=null){return await this.dbInstance.processRequest(e,t,s)}static async syncDatabase(){return await this.dbInstance.syncDatabase()}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){const t=this.schema||{},s=Object.entries(t).find(([t,s])=>t===e||s&&s.table===e);if(!s)throw KormError.unknownModel({model:e,available:Object.keys(t)});const[r,a]=s;return buildModelDescription(r,a,{softDelete:this._modelHasSoftDelete(r)})}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{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(){return await this.dbInstance.syncDatabase()}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;
package/KormError.js CHANGED
@@ -1 +1 @@
1
- const CODES=Object.freeze({NO_MATCHING_ROW:"NO_MATCHING_ROW",UNKNOWN_ACTION:"UNKNOWN_ACTION",VALIDATION_FAILED:"VALIDATION_FAILED",UNKNOWN_MODEL:"UNKNOWN_MODEL",NO_CUSTOM_ACTION_HOOK:"NO_CUSTOM_ACTION_HOOK",INTERNAL:"INTERNAL"}),ACTIONS=Object.freeze(["count","sum","list","show","create","update","replace","upsert","sync","delete"]);function levenshtein(e,o){const t=e.length,r=o.length;if(0===t)return r;if(0===r)return t;let n=Array.from({length:r+1},(e,o)=>o),s=new Array(r+1);for(let i=1;i<=t;i++){s[0]=i;for(let t=1;t<=r;t++){const r=e[i-1]===o[t-1]?0:1;s[t]=Math.min(n[t]+1,s[t-1]+1,n[t-1]+r)}[n,s]=[s,n]}return n[r]}function closestAction(e,o=ACTIONS){if(!e)return null;const t=String(e).toLowerCase();let r=null,n=1/0;for(const e of o){const o=levenshtein(t,e);o<n&&(n=o,r=e)}return n<=Math.max(2,Math.ceil(t.length/2))?r:null}class KormError extends Error{constructor(e,{code:o=CODES.INTERNAL,hint:t=null,context:r={},suggestedFixes:n=null}={}){super(e),this.name="KormError",this.code=o,this.hint=t,this.context=r,this.suggestedFixes=n,Error.captureStackTrace&&Error.captureStackTrace(this,KormError)}toJSON(){return{name:this.name,code:this.code,message:this.message,hint:this.hint,context:this.context,suggestedFixes:this.suggestedFixes}}}KormError.noMatchingRow=({action:e,model:o})=>{const t="update"===e?"No row matched the where clause. Use `upsert` to insert-or-update, or `sync` to reconcile.":"No row matched the where clause for this action.";return new KormError(`No matching row for action "${e}" on model "${o}".`,{code:CODES.NO_MATCHING_ROW,hint:t,context:{action:e,model:o}})},KormError.unknownAction=({action:e,model:o,hasCustomHook:t=!1})=>{const r=closestAction(e),n=t?CODES.NO_CUSTOM_ACTION_HOOK:CODES.UNKNOWN_ACTION,s=t?`No custom action hook found for "${o}.${e}".`:`Unknown action "${e}".`,i=r?`Did you mean "${r}"? Valid actions: ${ACTIONS.join(", ")}.`:`Valid actions: ${ACTIONS.join(", ")}. Custom actions require an on<Action>Action hook on the model.`;return new KormError(s,{code:n,hint:i,context:{action:e,model:o,validActions:ACTIONS,closest:r}})},KormError.unknownModel=({model:e,available:o=[]})=>{const t=o.length?`Available models: ${o.join(", ")}.`:"No models are registered in the schema.";return new KormError(`Model "${e}" not found.`,{code:CODES.UNKNOWN_MODEL,hint:t,context:{model:e,available:o}})},KormError.validationFailed=({errors:e=[],source:o=null})=>{const t=e.map(e=>({field:e.field,message:e.message,value:e.value,rule:e.rule})),r=t.map(e=>e.field).filter(Boolean),n=new KormError(`Validation failed${o?` for ${o}`:""}${r.length?`: ${r.join(", ")}`:""}.`,{code:CODES.VALIDATION_FAILED,hint:"Fix the listed fields and resubmit. See context.fields for per-field detail.",context:{source:o,fields:t}});return n.errors=e,n},KormError.CODES=CODES,KormError.ACTIONS=ACTIONS,KormError.closestAction=closestAction,module.exports=KormError;
1
+ const CODES=Object.freeze({NO_MATCHING_ROW:"NO_MATCHING_ROW",UNKNOWN_ACTION:"UNKNOWN_ACTION",VALIDATION_FAILED:"VALIDATION_FAILED",UNKNOWN_MODEL:"UNKNOWN_MODEL",NO_CUSTOM_ACTION_HOOK:"NO_CUSTOM_ACTION_HOOK",FORBIDDEN:"FORBIDDEN",INTERNAL:"INTERNAL"}),ACTIONS=Object.freeze(["count","sum","list","show","create","update","replace","upsert","sync","delete"]);function levenshtein(e,o){const t=e.length,r=o.length;if(0===t)return r;if(0===r)return t;let n=Array.from({length:r+1},(e,o)=>o),i=new Array(r+1);for(let s=1;s<=t;s++){i[0]=s;for(let t=1;t<=r;t++){const r=e[s-1]===o[t-1]?0:1;i[t]=Math.min(n[t]+1,i[t-1]+1,n[t-1]+r)}[n,i]=[i,n]}return n[r]}function closestAction(e,o=ACTIONS){if(!e)return null;const t=String(e).toLowerCase();let r=null,n=1/0;for(const e of o){const o=levenshtein(t,e);o<n&&(n=o,r=e)}return n<=Math.max(2,Math.ceil(t.length/2))?r:null}class KormError extends Error{constructor(e,{code:o=CODES.INTERNAL,hint:t=null,context:r={},suggestedFixes:n=null}={}){super(e),this.name="KormError",this.code=o,this.hint=t,this.context=r,this.suggestedFixes=n,Error.captureStackTrace&&Error.captureStackTrace(this,KormError)}toJSON(){return{name:this.name,code:this.code,message:this.message,hint:this.hint,context:this.context,suggestedFixes:this.suggestedFixes}}}KormError.noMatchingRow=({action:e,model:o})=>{const t="update"===e?"No row matched the where clause. Use `upsert` to insert-or-update, or `sync` to reconcile.":"No row matched the where clause for this action.";return new KormError(`No matching row for action "${e}" on model "${o}".`,{code:CODES.NO_MATCHING_ROW,hint:t,context:{action:e,model:o}})},KormError.unknownAction=({action:e,model:o,hasCustomHook:t=!1})=>{const r=closestAction(e),n=t?CODES.NO_CUSTOM_ACTION_HOOK:CODES.UNKNOWN_ACTION,i=t?`No custom action hook found for "${o}.${e}".`:`Unknown action "${e}".`,s=r?`Did you mean "${r}"? Valid actions: ${ACTIONS.join(", ")}.`:`Valid actions: ${ACTIONS.join(", ")}. Custom actions require an on<Action>Action hook on the model.`;return new KormError(i,{code:n,hint:s,context:{action:e,model:o,validActions:ACTIONS,closest:r}})},KormError.unknownModel=({model:e,available:o=[]})=>{const t=o.length?`Available models: ${o.join(", ")}.`:"No models are registered in the schema.";return new KormError(`Model "${e}" not found.`,{code:CODES.UNKNOWN_MODEL,hint:t,context:{model:e,available:o}})},KormError.forbidden=({model:e,action:o,hint:t=null,context:r={}}={})=>new KormError(`Action "${o}" on model "${e}" is not permitted in this context.`,{code:CODES.FORBIDDEN,hint:t||"A registered authorize() predicate denied this request for the current context.",context:{action:o,model:e,...r}}),KormError.validationFailed=({errors:e=[],source:o=null})=>{const t=e.map(e=>({field:e.field,message:e.message,value:e.value,rule:e.rule})),r=t.map(e=>e.field).filter(Boolean),n=new KormError(`Validation failed${o?` for ${o}`:""}${r.length?`: ${r.join(", ")}`:""}.`,{code:CODES.VALIDATION_FAILED,hint:"Fix the listed fields and resubmit. See context.fields for per-field detail.",context:{source:o,fields:t}});return n.errors=e,n},KormError.CODES=CODES,KormError.ACTIONS=ACTIONS,KormError.closestAction=closestAction,module.exports=KormError;
package/README.md CHANGED
@@ -5,6 +5,48 @@
5
5
  [![npm version](https://badge.fury.io/js/@dreamtree-org%2Fkorm-js.svg)](https://badge.fury.io/js/@dreamtree-org%2Fkorm-js)
6
6
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
7
7
 
8
+ ## About the Company
9
+
10
+ KORM-JS is built and maintained under **[Dreamtree Global](http://dreamtreeglobal.com/)** — a team crafting thoughtful, developer-first software for the modern web. We believe great products start with a great foundation, and that's exactly what this library aims to be: a polished, open, and dependable data layer for teams everywhere.
11
+
12
+ 🌐 **Website:** [dreamtreeglobal.com](http://dreamtreeglobal.com/)
13
+
14
+ ## Author & Maintainer
15
+
16
+ **Partha Preetham Krishna M L**
17
+ 📧 [preetham.krishna.dev@gmail.com](mailto:preetham.krishna.dev@gmail.com)
18
+
19
+ Have an idea, a bug, or just want to say hi? Reach out anytime — feedback from developers like you is what keeps this library growing.
20
+
21
+ ## 💚 Sponsor & Support
22
+
23
+ KORM-JS is **free and open source**, built in the open and powered entirely by passion and late nights. Every feature you rely on — every multi-engine query, relation-aware join, and line of zero-boilerplate CRUD — represents hours of careful craft so that *your* backend can ship faster and stay safer.
24
+
25
+ If this library has saved you time, sparked an idea, or spared you a pile of hand-written SQL, please consider supporting its continued development. **Your sponsorship keeps the updates coming, the bugs squashed, and the docs sharp — for the whole community.** Even the smallest contribution is a huge encouragement. 🙏
26
+
27
+ ### ☕ Make a one-tap donation via UPI
28
+
29
+ **📷 Scan the QR with any UPI app** (Google Pay, PhonePe, Paytm, BHIM, …) to pay instantly:
30
+
31
+ <p>
32
+ <img src="https://unpkg.com/@dreamtree-org/twreact-ui@latest/doc/assets/upi-qr.png" alt="Scan to pay Dreamtree Global via UPI" width="220" height="220" />
33
+ </p>
34
+
35
+ > **UPI ID:** `dhrugantha.llp@kotak` &nbsp;•&nbsp; **Payee:** Dreamtree Global
36
+ >
37
+ > 📱 On mobile, you can also tap the button below — it opens your UPI app directly. On desktop, scan the QR above or copy the UPI ID into your payment app.
38
+
39
+ [![Pay via UPI](https://img.shields.io/badge/Pay-via%20UPI-22c55e?style=for-the-badge&logo=googlepay&logoColor=white)](upi://pay?pa=dhrugantha.llp@kotak&pn=Dreamtree%20Global&cu=INR&tn=Support%20KORM-JS)
40
+
41
+ ### 🤝 Become a Sponsor
42
+
43
+ Want to back the project long-term or as a company? You can sponsor the package directly:
44
+
45
+ - 📦 **npm:** [`@dreamtree-org/korm-js`](https://www.npmjs.com/package/@dreamtree-org/korm-js) — star, share, and sponsor the package
46
+ - ✉️ **Corporate sponsorships & partnerships:** [preetham.krishna.dev@gmail.com](mailto:preetham.krishna.dev@gmail.com)
47
+
48
+ Every star ⭐, share, and contribution helps more than you know. Thank you for being part of the journey!
49
+
8
50
  ## Features
9
51
 
10
52
  - 🚀 **Multi-Database Support**: MySQL, PostgreSQL, SQLite
@@ -22,6 +64,7 @@
22
64
  - 📝 **Logger Utility**: Configurable logging with multiple log levels
23
65
  - 🔎 **SQL Debugging**: Debug mode with SQL statement output for troubleshooting
24
66
  - 🚫 **NOT EXISTS Queries**: Support for checking absence of related records with `!` prefix
67
+ - 🤖 **AI-native**: a bundled [MCP server](#running-as-an-mcp-server) serves your tables to coding agents as typed JSON tools, plus an [`init --ai`](#ai-assistant-skills-init---ai) skill installer for Claude, Cursor, Copilot, and more
25
68
 
26
69
  ## Installation
27
70
 
@@ -405,8 +448,25 @@ POST /api/Employee/crud
405
448
 
406
449
  ### 8. Replace Operation
407
450
 
451
+ Replaces an entire row, keyed on the model's primary key (include the PK in
452
+ `data`). Supported on **all three engines**, but the semantics differ by what
453
+ each engine can express:
454
+
455
+ | Engine | Statement | Semantics |
456
+ | --- | --- | --- |
457
+ | MySQL | `REPLACE INTO` | True delete + insert. Columns omitted from `data` reset to their column **DEFAULT** (or `NULL`). |
458
+ | SQLite | `INSERT OR REPLACE` | True delete + insert (same as MySQL). |
459
+ | Postgres | `INSERT … ON CONFLICT (pk) DO UPDATE` | **Merge**, not a true replace: columns omitted from `data` keep their existing values (Postgres has no native `REPLACE`). |
460
+
461
+ > ⚠️ **Cross-engine caveat:** on Postgres, `replace` is emulated and behaves
462
+ > like a merge — fields you omit are **retained**, whereas MySQL/SQLite reset
463
+ > them. If you need portable insert-or-update semantics, prefer `upsert`. The
464
+ > conflict target defaults to the model's primary key; pass an explicit
465
+ > `conflict: ['col', …]` to override (required on Postgres if the model has no
466
+ > primary key).
467
+
408
468
  ```javascript
409
- // Replace record (MySQL specific - replaces entire row)
469
+ // Replace record replaces the entire row by primary key
410
470
  POST /api/Users/crud
411
471
  {
412
472
  "action": "replace",
@@ -1576,10 +1636,11 @@ type|modifier1|modifier2|...
1576
1636
  | `default:value` | Default value | `tinyint | default:1` |
1577
1637
  | `onUpdate:value`² | On update value | `timestamp | onUpdate:CURRENT_TIMESTAMP` |
1578
1638
  | `comment:text` | Column comment | `varchar | comment:User email address` |
1579
- | `foreignKey:table:column` | Foreign key | `int | foreignKey:users:id` |
1639
+ | `foreignKey:table:column`³| Foreign key | `int | foreignKey:users:id` |
1580
1640
 
1581
1641
  ¹ **Engine-specific.** `unsigned` is honored on MySQL and SQLite. PostgreSQL has no unsigned integer type and silently drops the modifier.
1582
1642
  ² **Engine-specific.** `onUpdate` is honored on MySQL (emitted via `ON UPDATE <expr>`). PostgreSQL and SQLite log a one-time warning and ignore it — the modifier cannot be expressed inline on those engines. See [`docs/agents/05-multi-db-parity.md`](docs/agents/05-multi-db-parity.md).
1643
+ ³ **Auto type-matching.** When the referenced table is part of the same schema, `syncDatabase()` automatically widens the foreign-key column's type to match the referenced primary key, so the FK constraint is always type-compatible. In particular, an `autoIncrement` primary key is emitted as `BIGINT UNSIGNED` (MySQL) / `BIGSERIAL` (PostgreSQL) / `INTEGER` (SQLite) via Knex's `.increments()`, so a child column written as `int|foreignKey:…` is created as `BIGINT` — you do **not** need to hand-match the width. Foreign keys to tables outside the schema (pre-existing/third-party) are created exactly as declared.
1583
1644
 
1584
1645
  **Special Default Values:**
1585
1646
 
@@ -1694,7 +1755,7 @@ const modelInstance = korm.getModelInstance(modelDef);
1694
1755
  | `delete` | Delete record(s) | `where` |
1695
1756
  | `count` | Count records | None (optional: `where`) |
1696
1757
  | `sum` | Sum column or expression | `data.sumColumn` or `data.sumFormula` (optional: `where`) |
1697
- | `replace` | Replace record (MySQL) | `data` |
1758
+ | `replace` | Replace full row by PK (all engines; pg = merge, see §8) | `data` (optional: `conflict`) |
1698
1759
  | `upsert` | Insert or update | `data`, `conflict` |
1699
1760
  | `sync` | Upsert + delete | `data`, `conflict`, `where` |
1700
1761
 
@@ -2101,7 +2162,14 @@ The schema is derived from the model's column definitions and relations, so it s
2101
2162
 
2102
2163
  ## Running as an MCP server
2103
2164
 
2104
- KORM-JS ships with an optional [Model Context Protocol](https://modelcontextprotocol.io) server, `korm-mcp`. It exposes your KORM-registered tables as typed JSON-in/JSON-out tools that any MCP client (Claude Desktop, Claude Code, Cursor, custom agents) can call — no HTTP layer, no hand-written CRUD.
2165
+ KORM-JS ships with an optional [Model Context Protocol](https://modelcontextprotocol.io) server. It exposes your KORM-registered tables as typed JSON-in/JSON-out tools that any MCP client (Claude Desktop, Claude Code, Cursor, custom agents) can call — no HTTP layer, no hand-written CRUD.
2166
+
2167
+ Two equivalent ways to launch it (both take the same `--config`):
2168
+
2169
+ ```bash
2170
+ korm-mcp --config ./korm-mcp.config.js # the dedicated bin
2171
+ npx -y @dreamtree-org/korm-js mcp --config ./korm-mcp.config.js # the `mcp` subcommand (zero install)
2172
+ ```
2105
2173
 
2106
2174
  ### Install the SDK
2107
2175
 
@@ -2151,6 +2219,19 @@ module.exports = {
2151
2219
  }
2152
2220
  ```
2153
2221
 
2222
+ Or, with no global install, use the `mcp` subcommand via `npx`:
2223
+
2224
+ ```json
2225
+ {
2226
+ "mcpServers": {
2227
+ "my-app-db": {
2228
+ "command": "npx",
2229
+ "args": ["-y", "@dreamtree-org/korm-js", "mcp", "--config", "/abs/path/to/korm-mcp.config.js"]
2230
+ }
2231
+ }
2232
+ }
2233
+ ```
2234
+
2154
2235
  ### What you get
2155
2236
 
2156
2237
  For each allowlisted table, the server emits one tool per action permitted by `mcp.mode`. Example for a `User` model:
@@ -2185,6 +2266,38 @@ Three meta tools (unless disabled via `mcp.metaTools: false`):
2185
2266
 
2186
2267
  See `docs/agents/11-mcp-server.md` for the full design rationale and the locked decisions behind these defaults.
2187
2268
 
2269
+ ## Authorization (multi-tenancy & RBAC)
2270
+
2271
+ KORM-JS enforces authorization **at the contract layer**, in front of every engine, via two opt-in registrations. The `ctx` you already pass as the third argument to `processRequest(body, model, ctx)` is what predicates and scopes receive — wire it from your auth middleware.
2272
+
2273
+ ```js
2274
+ const korm = initializeKORM({ db, dbClient, schema });
2275
+
2276
+ // 1) Per-(model, action) permission predicate. Return falsy → the request
2277
+ // throws KormError({ code: 'FORBIDDEN' }) before any SQL runs (dryRun too).
2278
+ korm.authorize('User', 'delete', (req, ctx) => ctx.user.role === 'admin');
2279
+ korm.authorize('Invoice', '*', (req, ctx) => Boolean(ctx.tenantId)); // '*' = every action
2280
+
2281
+ // 2) Row scope. Merged into `where` for reads / update / delete / count / sum,
2282
+ // and stamped into each inserted `data` row for create / replace / upsert /
2283
+ // sync — so a tenant can neither read nor create rows outside its scope.
2284
+ korm.scope('Invoice', (req, ctx) => ({ tenantId: ctx.tenantId }));
2285
+ ```
2286
+
2287
+ ```js
2288
+ // In your route handler — ctx carries the authenticated principal:
2289
+ app.post('/api/:model', (req, res) =>
2290
+ korm.processRequest(req.body, req.params.model, { user: req.user, tenantId: req.user.tenantId })
2291
+ );
2292
+ ```
2293
+
2294
+ - **Opt-in & non-breaking:** with no `authorize()`/`scope()` registered, requests pass through unchanged.
2295
+ - **Scope wins:** a client cannot widen its scope via `where`, nor create a row in another tenant via `data` — the scope value overrides any conflicting client value.
2296
+ - **Capability discovery:** `describeModel(name, ctx)` returns `actions` filtered to those permitted for `ctx`, so an agent sees what it may call before generating a request.
2297
+ - **Denied requests** throw `KormError({ code: 'FORBIDDEN', context: { model, action } })` (see below). A throwing predicate fails closed (denies).
2298
+
2299
+ > Scoping applies to every read/write action — including **`count`** and **`sum`** (they honor `where`) — so a tenant's totals reflect only its own rows.
2300
+
2188
2301
  ## Error Handling
2189
2302
 
2190
2303
  `processRequest` and `validate` throw a structured **`KormError`** (which
@@ -2321,8 +2434,12 @@ This project is licensed under the MIT License - see the [LICENSE](LICENSE) file
2321
2434
 
2322
2435
  ## Support
2323
2436
 
2324
- If you have any questions or need help, please open an issue on GitHub or contact us at [partha.preetham.krishna@gmail.com](mailto:partha-preetham.krishna@gmail.com).
2437
+ - 🐛 [Issue Tracker](https://github.com/DreamtreeTech/korm-js/issues)
2438
+ - 💬 [Discussions](https://github.com/DreamtreeTech/korm-js/discussions)
2439
+ - 📦 [npm package](https://www.npmjs.com/package/@dreamtree-org/korm-js)
2440
+ - 🌐 [Dreamtree Global](http://dreamtreeglobal.com/)
2441
+ - 📧 [preetham.krishna.dev@gmail.com](mailto:preetham.krishna.dev@gmail.com)
2325
2442
 
2326
2443
  ---
2327
2444
 
2328
- **Made with ❤️ by [Partha Preetham Krishna](https://www.linkedin.com/in/partha-preetham-krishna-68ba00197/)**
2445
+ **Made with ❤️ by [Partha Preetham Krishna](https://www.linkedin.com/in/partha-preetham-krishna-68ba00197/)** under [Dreamtree Global](http://dreamtreeglobal.com/)
@@ -68,7 +68,7 @@ In Express/Next/Fastify the consumer just forwards `req.body` and the model name
68
68
  | `delete` | Delete (soft if the model declares soft-delete; otherwise hard) |
69
69
  | `count` | COUNT(\*) of matching rows |
70
70
  | `sum` | Sum a column or formula; needs `data.sumColumn` or `data.sumFormula` |
71
- | `replace` | MySQL-only full row replace (requires PK in `data`) |
71
+ | `replace` | Full-row replace by PK, all engines (MySQL/SQLite = delete+insert; pg = ON CONFLICT merge — omitted cols retained). Optional `conflict` |
72
72
  | `upsert` | Insert-or-update keyed by `conflict` columns |
73
73
  | `sync` | Upsert matching `data` + delete non-matching within `where` scope |
74
74
 
@@ -146,6 +146,7 @@ still works) with a machine-readable `code` you can branch on:
146
146
  | `NO_CUSTOM_ACTION_HOOK` | Custom action requested, no hook on the model |
147
147
  | `VALIDATION_FAILED` | Input failed validation (`e.context.fields`) |
148
148
  | `UNKNOWN_MODEL` | Model name not in the schema (`e.context.available`) |
149
+ | `FORBIDDEN` | A registered `authorize()` predicate denied the request (`e.context.model`/`action`) |
149
150
  | `INTERNAL` | Internal invariant / misconfiguration |
150
151
 
151
152
  ```js
@@ -167,6 +168,8 @@ Two read-only helpers for agent integration:
167
168
  description of tables, typed columns, relations, soft-delete flag, and
168
169
  available actions. Use it to discover what's queryable before building
169
170
  a request. Throws `KormError` (`code: 'UNKNOWN_MODEL'`) for a bad name.
171
+ Pass a context — `korm.describeModel('User', ctx)` — and `actions` is
172
+ filtered to those the current context may call (see authorization).
170
173
  - `korm.getRequestJsonSchema('User')` — draft-2020-12 JSON Schema for
171
174
  every valid request body for that model (an `action`-discriminated
172
175
  `oneOf`). Attach it to an OpenAI/Anthropic tool definition or use it
@@ -256,7 +259,7 @@ await korm.processRequest(
256
259
 
257
260
  1. **Use the JSON contract.** When the user asks for a query, return a KORM request object plus the `processRequest` call — not raw Knex chains.
258
261
  2. **Never concatenate user input into SQL.** All filtering goes through `where` operators above.
259
- 3. **Multi-DB.** Assume the same request runs on MySQL, Postgres, and SQLite. If a feature is engine-specific (e.g. `replace` is MySQL-only), call it out.
262
+ 3. **Multi-DB.** Assume the same request runs on MySQL, Postgres, and SQLite. If a feature's _semantics_ differ by engine, call it out — e.g. `replace` is a true delete+insert on MySQL/SQLite but a merge on Postgres (omitted columns are retained); prefer `upsert` for portable insert-or-update.
260
263
  4. **Don't invent operators.** If the user needs something not in the operator table, use `where` with relation traversal, `having`, or `groupBy` — or tell the user the contract doesn't support it.
261
264
  5. **Don't invent fields.** The top-level keys above are the entire contract surface. No `filter`, no `query`, no `params`.
262
265
  6. **Soft delete is per-model.** `delete` becomes a soft-delete only if the model declares it; don't assume.
package/bin/korm-mcp.js CHANGED
@@ -1,2 +1,2 @@
1
1
  #!/usr/bin/env node
2
- "use strict";const path=require("path"),{initializeKORM:initializeKORM}=require("../index"),{createServer:createServer}=require("../src/mcp/server"),{McpConfigError:McpConfigError}=require("../src/mcp/errors");function parseArgv(e){const r={config:null,help:!1};for(let o=2;o<e.length;o++){const t=e[o];"--help"===t||"-h"===t?r.help=!0:"--config"===t||"-c"===t?r.config=e[++o]:t.startsWith("--config=")?r.config=t.slice(9):(process.stderr.write(`korm-mcp: unknown argument "${t}"\n`),r.help=!0)}return r}function printHelp(){process.stderr.write("korm-mcp — Model Context Protocol server for @dreamtree-org/korm-js\n\nUsage:\n korm-mcp --config <path-to-config.js>\n\nOptions:\n -c, --config <path> Path to a Node CJS module exporting { db, dbClient, schema, mcp }.\n -h, --help Show this help.\n\nSee docs/agents/11-mcp-server.md for the config shape.\n")}function requireConfigModule(e){try{const r=require(e);return r&&r.default?r.default:r}catch(r){throw new McpConfigError(`Failed to load config at ${e}: ${r.message}`)}}function assertConfigFields(e,r){if(!e||"object"!=typeof e)throw new McpConfigError(`Config at ${r} must export an object (got ${typeof e}).`);const o=["db","dbClient","schema"];for(const r of o)if(!e[r])throw new McpConfigError(`Config: \`${r}\` is required.`);if(!e.mcp||"object"!=typeof e.mcp)throw new McpConfigError("Config: `mcp` object is required (see spec §6).")}function loadConfig(e){if(!e)throw new McpConfigError("--config is required. See `korm-mcp --help`.");const r=path.resolve(process.cwd(),e),o=requireConfigModule(r);return assertConfigFields(o,r),o}const stderrLogger={info:(...e)=>process.stderr.write(`[korm-mcp] ${e.join(" ")}\n`),error:(...e)=>process.stderr.write(`[korm-mcp:error] ${e.join(" ")}\n`)};function installShutdownHandlers(e){const r=async r=>{stderrLogger.info(`received ${r}, shutting down`);try{await e.stop()}catch(e){stderrLogger.error(`stop error: ${e.message}`)}process.exit(0)};process.on("SIGINT",()=>r("SIGINT")),process.on("SIGTERM",()=>r("SIGTERM"))}async function main(){const e=parseArgv(process.argv);let r;e.help&&(printHelp(),process.exit(0));try{r=loadConfig(e.config)}catch(e){process.stderr.write(`korm-mcp: ${e.message}\n`),process.exit(2)}const o=initializeKORM({db:r.db,dbClient:r.dbClient,schema:r.schema,resolverPath:r.resolverPath||null,debug:r.debug||!1}),t=require("../package.json"),n=createServer({controller:o,schema:r.schema,mcpConfig:r.mcp,packageInfo:{name:t.name,version:t.version}});installShutdownHandlers(n);try{await n.start({logger:stderrLogger}),stderrLogger.info(`started; ${n.tools.length} tools exposed (mode=${r.mcp.mode||"ro"})`)}catch(e){stderrLogger.error(`failed to start: ${e.message}`),process.exit(1)}}require.main===module&&main().catch(e=>{process.stderr.write(`korm-mcp: fatal: ${e.message}\n`),process.exit(1)}),module.exports={parseArgv:parseArgv,loadConfig:loadConfig,main:main};
2
+ "use strict";const path=require("path"),{initializeKORM:initializeKORM}=require("../index"),{createServer:createServer}=require("../src/mcp/server"),{McpConfigError:McpConfigError}=require("../src/mcp/errors");function parseArgv(e){const r={config:null,help:!1};for(let o=2;o<e.length;o++){const t=e[o];"--help"===t||"-h"===t?r.help=!0:"--config"===t||"-c"===t?r.config=e[++o]:t.startsWith("--config=")?r.config=t.slice(9):(process.stderr.write(`korm-mcp: unknown argument "${t}"\n`),r.help=!0)}return r}function printHelp(){process.stderr.write("korm-mcp — Model Context Protocol server for @dreamtree-org/korm-js\n\nUsage:\n korm-mcp --config <path-to-config.js>\n\nOptions:\n -c, --config <path> Path to a Node CJS module exporting { db, dbClient, schema, mcp }.\n -h, --help Show this help.\n\nSee docs/agents/11-mcp-server.md for the config shape.\n")}function requireConfigModule(e){try{const r=require(e);return r&&r.default?r.default:r}catch(r){throw new McpConfigError(`Failed to load config at ${e}: ${r.message}`)}}function assertConfigFields(e,r){if(!e||"object"!=typeof e)throw new McpConfigError(`Config at ${r} must export an object (got ${typeof e}).`);const o=["db","dbClient","schema"];for(const r of o)if(!e[r])throw new McpConfigError(`Config: \`${r}\` is required.`);if(!e.mcp||"object"!=typeof e.mcp)throw new McpConfigError("Config: `mcp` object is required (see spec §6).")}function loadConfig(e){if(!e)throw new McpConfigError("--config is required. See `korm-mcp --help`.");const r=path.resolve(process.cwd(),e),o=requireConfigModule(r);return assertConfigFields(o,r),o}const stderrLogger={info:(...e)=>process.stderr.write(`[korm-mcp] ${e.join(" ")}\n`),error:(...e)=>process.stderr.write(`[korm-mcp:error] ${e.join(" ")}\n`)};function installShutdownHandlers(e){const r=async r=>{stderrLogger.info(`received ${r}, shutting down`);try{await e.stop()}catch(e){stderrLogger.error(`stop error: ${e.message}`)}process.exit(0)};process.on("SIGINT",()=>r("SIGINT")),process.on("SIGTERM",()=>r("SIGTERM"))}async function main(e=process.argv){const r=parseArgv(e);let o;r.help&&(printHelp(),process.exit(0));try{o=loadConfig(r.config)}catch(e){process.stderr.write(`korm-mcp: ${e.message}\n`),process.exit(2)}const t=initializeKORM({db:o.db,dbClient:o.dbClient,schema:o.schema,resolverPath:o.resolverPath||null,debug:o.debug||!1}),n=require("../package.json"),s=createServer({controller:t,schema:o.schema,mcpConfig:o.mcp,packageInfo:{name:n.name,version:n.version}});installShutdownHandlers(s);try{await s.start({logger:stderrLogger}),stderrLogger.info(`started; ${s.tools.length} tools exposed (mode=${o.mcp.mode||"ro"})`)}catch(e){stderrLogger.error(`failed to start: ${e.message}`),process.exit(1)}}require.main===module&&main().catch(e=>{process.stderr.write(`korm-mcp: fatal: ${e.message}\n`),process.exit(1)}),module.exports={parseArgv:parseArgv,loadConfig:loadConfig,main:main};
package/cli.js CHANGED
@@ -1,2 +1,2 @@
1
1
  #!/usr/bin/env node
2
- "use strict";const fs=require("fs"),path=require("path"),BEGIN="\x3c!-- BEGIN korm-js skill (auto-generated — re-run `npx @dreamtree-org/korm-js init --ai <provider>` to refresh) --\x3e",END="\x3c!-- END korm-js skill --\x3e",PROVIDERS={claude:{path:"CLAUDE.md",mode:"block"},openai:{path:"AGENTS.md",mode:"block"},gemini:{path:"GEMINI.md",mode:"block"},copilot:{path:".github/copilot-instructions.md",mode:"block"},kiro:{path:".kiro/steering/korm-js.md",mode:"file"},windsurf:{path:".windsurf/rules/korm-js.md",mode:"file"},cursor:{path:".cursor/rules/korm-js.mdc",mode:"file",frontmatter:"---\ndescription: KORM-JS request contract reference for @dreamtree-org/korm-js\nalwaysApply: false\n---\n\n"}},PROVIDER_ALIASES={"claude-code":"claude",codex:"openai","github-copilot":"copilot"};function parseArgs(e){const r={_:[],flags:{}};for(let o=0;o<e.length;o++){const n=e[o];if(n.startsWith("--")){const s=n.slice(2),t=e[o+1];t&&!t.startsWith("--")?(r.flags[s]=t,o++):r.flags[s]=!0}else r._.push(n)}return r}function usage(){return["Usage:"," npx @dreamtree-org/korm-js init --ai <provider> [--cwd <dir>] [--force] [--dry-run]","","Providers:"," claude -> CLAUDE.md (block insert)"," openai -> AGENTS.md (block insert)"," gemini -> GEMINI.md (block insert)"," copilot -> .github/copilot-instructions.md (block insert)"," kiro -> .kiro/steering/korm-js.md"," windsurf -> .windsurf/rules/korm-js.md"," cursor -> .cursor/rules/korm-js.mdc","","Use --ai all to install every provider in one go."].join("\n")}function readSkillBody(){const e=path.join(__dirname,"ai-skills","korm-js.md");if(!fs.existsSync(e))throw new Error("Skill source missing at "+e+" — reinstall @dreamtree-org/korm-js.");return fs.readFileSync(e,"utf8")}function ensureDir(e){const r=path.dirname(e);r&&"."!==r&&!fs.existsSync(r)&&fs.mkdirSync(r,{recursive:!0})}function writeBlock(e,r,o){const n=BEGIN+"\n\n"+r.trim()+"\n\n"+END+"\n";let s,t;if(fs.existsSync(e)){const r=fs.readFileSync(e,"utf8"),o=r.indexOf(BEGIN),i=r.indexOf(END);if(-1!==o&&-1!==i&&i>o){const e=r.slice(0,o).replace(/\s+$/,""),l=r.slice(i+26).replace(/^\s+/,"");s=(e?e+"\n\n":"")+n+(l?"\n"+l:""),t="updated"}else{s=r.replace(/\s+$/,"")+"\n\n"+n,t="appended"}}else s=n,t="created";return o.dryRun?{action:t+" (dry-run)",path:e}:(ensureDir(e),fs.writeFileSync(e,s,"utf8"),{action:t,path:e})}function writeFile(e,r,o,n){const s=(o||"")+r;let t;if(fs.existsSync(e)){if(!n.force)return{action:"skipped (exists; pass --force to overwrite)",path:e};t="overwritten"}else t="created";return n.dryRun?{action:t+" (dry-run)",path:e}:(ensureDir(e),fs.writeFileSync(e,s,"utf8"),{action:t,path:e})}function installFor(e,r,o,n){const s=PROVIDERS[e],t=path.join(o,s.path);return"block"===s.mode?writeBlock(t,r,n):writeFile(t,r,s.frontmatter,n)}function resolveProvider(e){if(!e)return null;const r=String(e).toLowerCase();return"all"===r?"all":PROVIDERS[r]?r:PROVIDER_ALIASES[r]?PROVIDER_ALIASES[r]:null}function runInit(e){const r=e.flags.ai;r&&!0!==r||(console.error("Missing --ai <provider>.\n"),console.error(usage()),process.exit(1));const o=resolveProvider(r);o||(console.error("Unknown provider: "+r),console.error("Known: "+Object.keys(PROVIDERS).join(", ")+", all"),process.exit(1));const n=e.flags.cwd?path.resolve(String(e.flags.cwd)):process.cwd(),s={force:Boolean(e.flags.force),dryRun:Boolean(e.flags["dry-run"])},t=readSkillBody(),i=("all"===o?Object.keys(PROVIDERS):[o]).map(function(e){try{const r=installFor(e,t,n,s);return{provider:e,ok:!0,action:r.action,path:r.path}}catch(r){return{provider:e,ok:!1,error:r.message}}}),l=i.filter(function(e){return!e.ok});for(const e of i)e.ok?console.log(" ["+e.provider.padEnd(8)+"] "+e.action+" -> "+path.relative(n,e.path)):console.error(" ["+e.provider.padEnd(8)+"] FAILED: "+e.error);console.log(""),console.log(s.dryRun?"Dry run complete. No files were written.":"KORM-JS skill installed."),l.length&&process.exit(1)}function main(){const e=parseArgs(process.argv.slice(2)),r=e._[0];r&&"--help"!==r&&"-h"!==r&&"help"!==r?"init"!==r?(console.error("Unknown command: "+r+"\n"),console.error(usage()),process.exit(1)):runInit(e):console.log(usage())}require.main===module&&main(),module.exports={PROVIDERS:PROVIDERS,PROVIDER_ALIASES:PROVIDER_ALIASES,parseArgs:parseArgs,resolveProvider:resolveProvider,BEGIN:BEGIN,END:END};
2
+ "use strict";const fs=require("fs"),path=require("path"),BEGIN="\x3c!-- BEGIN korm-js skill (auto-generated — re-run `npx @dreamtree-org/korm-js init --ai <provider>` to refresh) --\x3e",END="\x3c!-- END korm-js skill --\x3e",PROVIDERS={claude:{path:"CLAUDE.md",mode:"block"},openai:{path:"AGENTS.md",mode:"block"},gemini:{path:"GEMINI.md",mode:"block"},copilot:{path:".github/copilot-instructions.md",mode:"block"},kiro:{path:".kiro/steering/korm-js.md",mode:"file"},windsurf:{path:".windsurf/rules/korm-js.md",mode:"file"},cursor:{path:".cursor/rules/korm-js.mdc",mode:"file",frontmatter:"---\ndescription: KORM-JS request contract reference for @dreamtree-org/korm-js\nalwaysApply: false\n---\n\n"}},PROVIDER_ALIASES={"claude-code":"claude",codex:"openai","github-copilot":"copilot"};function parseArgs(e){const r={_:[],flags:{}};for(let o=0;o<e.length;o++){const n=e[o];if(n.startsWith("--")){const t=n.slice(2),s=e[o+1];s&&!s.startsWith("--")?(r.flags[t]=s,o++):r.flags[t]=!0}else r._.push(n)}return r}function usage(){return["Usage:"," npx @dreamtree-org/korm-js init --ai <provider> [--cwd <dir>] [--force] [--dry-run]"," npx @dreamtree-org/korm-js mcp --config <path-to-config.js>","","Commands:"," init Install the KORM-JS AI-assistant skill into your project."," mcp Start the Model Context Protocol server (wraps `korm-mcp`).","","Providers:"," claude -> CLAUDE.md (block insert)"," openai -> AGENTS.md (block insert)"," gemini -> GEMINI.md (block insert)"," copilot -> .github/copilot-instructions.md (block insert)"," kiro -> .kiro/steering/korm-js.md"," windsurf -> .windsurf/rules/korm-js.md"," cursor -> .cursor/rules/korm-js.mdc","","Use --ai all to install every provider in one go."].join("\n")}function readSkillBody(){const e=path.join(__dirname,"ai-skills","korm-js.md");if(!fs.existsSync(e))throw new Error("Skill source missing at "+e+" — reinstall @dreamtree-org/korm-js.");return fs.readFileSync(e,"utf8")}function ensureDir(e){const r=path.dirname(e);r&&"."!==r&&!fs.existsSync(r)&&fs.mkdirSync(r,{recursive:!0})}function writeBlock(e,r,o){const n=BEGIN+"\n\n"+r.trim()+"\n\n"+END+"\n";let t,s;if(fs.existsSync(e)){const r=fs.readFileSync(e,"utf8"),o=r.indexOf(BEGIN),i=r.indexOf(END);if(-1!==o&&-1!==i&&i>o){const e=r.slice(0,o).replace(/\s+$/,""),c=r.slice(i+26).replace(/^\s+/,"");t=(e?e+"\n\n":"")+n+(c?"\n"+c:""),s="updated"}else{t=r.replace(/\s+$/,"")+"\n\n"+n,s="appended"}}else t=n,s="created";return o.dryRun?{action:s+" (dry-run)",path:e}:(ensureDir(e),fs.writeFileSync(e,t,"utf8"),{action:s,path:e})}function writeFile(e,r,o,n){const t=(o||"")+r;let s;if(fs.existsSync(e)){if(!n.force)return{action:"skipped (exists; pass --force to overwrite)",path:e};s="overwritten"}else s="created";return n.dryRun?{action:s+" (dry-run)",path:e}:(ensureDir(e),fs.writeFileSync(e,t,"utf8"),{action:s,path:e})}function installFor(e,r,o,n){const t=PROVIDERS[e],s=path.join(o,t.path);return"block"===t.mode?writeBlock(s,r,n):writeFile(s,r,t.frontmatter,n)}function resolveProvider(e){if(!e)return null;const r=String(e).toLowerCase();return"all"===r?"all":PROVIDERS[r]?r:PROVIDER_ALIASES[r]?PROVIDER_ALIASES[r]:null}function runInit(e){const r=e.flags.ai;r&&!0!==r||(console.error("Missing --ai <provider>.\n"),console.error(usage()),process.exit(1));const o=resolveProvider(r);o||(console.error("Unknown provider: "+r),console.error("Known: "+Object.keys(PROVIDERS).join(", ")+", all"),process.exit(1));const n=e.flags.cwd?path.resolve(String(e.flags.cwd)):process.cwd(),t={force:Boolean(e.flags.force),dryRun:Boolean(e.flags["dry-run"])},s=readSkillBody(),i=("all"===o?Object.keys(PROVIDERS):[o]).map(function(e){try{const r=installFor(e,s,n,t);return{provider:e,ok:!0,action:r.action,path:r.path}}catch(r){return{provider:e,ok:!1,error:r.message}}}),c=i.filter(function(e){return!e.ok});for(const e of i)e.ok?console.log(" ["+e.provider.padEnd(8)+"] "+e.action+" -> "+path.relative(n,e.path)):console.error(" ["+e.provider.padEnd(8)+"] FAILED: "+e.error);console.log(""),console.log(t.dryRun?"Dry run complete. No files were written.":"KORM-JS skill installed."),c.length&&process.exit(1)}function runMcp(e){const{main:r}=require("./bin/korm-mcp");return Promise.resolve().then(()=>r(["node","korm-mcp",...e])).catch(e=>{console.error("korm-mcp: fatal: "+(e&&e.message?e.message:e)),process.exit(1)})}function main(){const e=process.argv.slice(2),r=parseArgs(e),o=r._[0];if(o&&"--help"!==o&&"-h"!==o&&"help"!==o)if("init"!==o){if("mcp"===o)return runMcp(e.slice(e.indexOf("mcp")+1));console.error("Unknown command: "+o+"\n"),console.error(usage()),process.exit(1)}else runInit(r);else console.log(usage())}require.main===module&&main(),module.exports={PROVIDERS:PROVIDERS,PROVIDER_ALIASES:PROVIDER_ALIASES,parseArgs:parseArgs,resolveProvider:resolveProvider,BEGIN:BEGIN,END:END,usage:usage,readSkillBody:readSkillBody,ensureDir:ensureDir,writeBlock:writeBlock,writeFile:writeFile,installFor:installFor,runInit:runInit,runMcp:runMcp,main:main};
@@ -1 +1 @@
1
- const logger=require("../Logger"),ENGINE_WARNINGS=new Set;function warnOnce(e,t){ENGINE_WARNINGS.has(e)||(ENGINE_WARNINGS.add(e),logger.warn(t))}const BASE_TYPE_DISPATCHER={VARCHAR:(e,t,n)=>e.string(t,n.size||255),CHAR:(e,t,n)=>e.string(t,n.size||255),TEXT:(e,t)=>e.text(t),MEDIUMTEXT:(e,t)=>e.text(t),LONGTEXT:(e,t)=>e.text(t),INT:(e,t)=>e.integer(t),INTEGER:(e,t)=>e.integer(t),MEDIUMINT:(e,t)=>e.integer(t),SMALLINT:(e,t)=>e.integer(t),BIGINT:(e,t)=>e.bigInteger(t),TINYINT:(e,t,n)=>e.tinyint?e.tinyint(t):e.specificType(t,n.size?`TINYINT(${n.size})`:"TINYINT"),BOOLEAN:(e,t)=>e.boolean(t),BOOL:(e,t)=>e.boolean(t),DATE:(e,t)=>e.date(t),DATETIME:(e,t)=>e.dateTime(t),TIMESTAMP:(e,t)=>e.timestamp(t),TIME:(e,t)=>e.time(t),JSON:(e,t)=>e.json(t),FLOAT:(e,t)=>e.float(t),DOUBLE:(e,t)=>e.double?e.double(t):e.float(t),REAL:(e,t)=>e.double?e.double(t):e.float(t),DECIMAL:(e,t)=>e.decimal(t),NUMERIC:(e,t)=>e.decimal(t),BINARY:(e,t)=>e.binary(t),VARBINARY:(e,t)=>e.binary(t),BLOB:(e,t)=>e.binary(t),UUID:(e,t)=>e.uuid?e.uuid(t):e.string(t,36)},COLUMN_STRING_SUFFIXES=[e=>e.size?`|size:${e.size}`:"",e=>e.isUnsigned?"|unsigned":"",e=>e.primary?"|primaryKey":"",e=>e.autoIncrement?"|autoIncrement":"",e=>e.nullable?"":"|notNull",e=>e.unique?"|unique":"",e=>null!=e.default&&""!==e.default?`|default:${e.default}`:"",e=>e.onUpdate?`|onUpdate:${e.onUpdate}`:"",e=>e.comment?`|comment:${e.comment}`:"",e=>e.hasForeignKey&&e.foreignMapTables?.[0]?`|foreignKey:${e.foreignMapTables[0].table}:${e.foreignMapTables[0].column}`:""];class BaseSyncTable{constructor(e,t,n=null){this.db=e,this.utils=t,this.controllerWrapper=n}_getClientName(){throw new Error("_getClientName must be overridden by engine subclass")}async existsTable(e){return this.db.schema.hasTable(e)}async syncTable(e){if(await this.existsTable(e.table)){const t=await this.getAlterations(e);await this.alterTable(e.table,t)}else await this.createTable(e);await this._applyExtras(e)}async syncDatabase(){if(!this.controllerWrapper?.schema)throw new Error("controllerWrapper.schema not set.");const e=this.controllerWrapper.schema;for(const t of Object.keys(e))await this.syncTable(e[t]),await this.syncSeedData(e[t],t);logger.info("Database synced by SyncTable...")}async syncSeedData(e,t){if(!e.seed||!Array.isArray(e.seed)||0===e.seed.length)return;const n=await this.db(e.table).count("* as n").first();Number(n?.n)>0?logger.info("Seed data already synced for",t):(await this.db(e.table).insert(e.seed),logger.info("Seed data synced for",t))}async generateSchema(){const e=await this._listTables(),t={},n=this._getHelperUtility();for(const a of e){const e=n?n.modelName(a):a;t[e]={table:a,alias:e,modelName:e,columns:this.getColumnString(await this.getCurrentColumns(a)),seed:[],hasRelations:await this._getRelations(a),indexes:[]}}return t}async createTable(e){await this.db.schema.createTable(e.table,t=>{for(const[n,a]of Object.entries(e.columns)){const e="string"==typeof a?this.utils.formatColumnSchema(n,a):a;this._applyColumnToBuilder(t,e)}})}async alterTable(e,t){if(!t||"object"!=typeof t)throw new Error("alterations must be an object");(t.add?.length||0)+(t.drop?.length||0)+(t.modify?.length||0)>0?await this.db.schema.alterTable(e,e=>{for(const n of t.add||[])this._applyColumnToBuilder(e,n);for(const n of t.drop||[])e.dropColumn(n.name);for(const n of t.modify||[]){const t=this._applyColumnToBuilder(e,n);t&&"function"==typeof t.alter&&t.alter()}}):logger.info("No alterations to apply for",e)}async dropTable(e){await this.db.schema.dropTableIfExists(e)}async getCurrentColumns(e){const t=await this.db(e).columnInfo(),n={};for(const[e,a]of Object.entries(t))n[e]=this._formatColumnInfo(e,a);return n}_formatColumnInfo(e,t){const n=String(t.type||"").toLowerCase(),a=n.match(/^([a-z_]+)(?:\((\d+)(?:,\s*\d+)?\))?/);return{name:e,type:(a?a[1]:n).toUpperCase(),size:(a&&a[2]?Number(a[2]):t.maxLength||null)||null,nullable:!1!==t.nullable,default:this._parseDefault(t.defaultValue),primary:!1,unique:!1,autoIncrement:!1,isUnsigned:!1,hasForeignKey:!1,foreignMapTables:[],onUpdate:null,comment:""}}_parseDefault(e){if(null==e)return null;const t=String(e).trim();return""===t?null:t.replace(/^'+|'+$/g,"")}hasColumnChanged(e,t){return!1}async getAlterations(e){const t={add:[],drop:[],modify:[]},n=await this.getCurrentColumns(e.table);for(const[a,s]of Object.entries(e.columns)){const e="string"==typeof s?this.utils.formatColumnSchema(a,s):s,r=n[a];r?this.hasColumnChanged(r,e)&&t.modify.push(e):t.add.push(e)}for(const a of Object.keys(n))e.columns[a]||t.drop.push({name:a});return t}getColumnString(e){return Object.keys(e).reduce((t,n)=>{const a=e[n],s=String(a.type||"").toLowerCase();return t[n]=COLUMN_STRING_SUFFIXES.reduce((e,t)=>e+t(a),s),t},{})}_applyColumnToBuilder(e,t){if(t.autoIncrement&&t.primary)return this._buildIncrementsColumn(e,t);const n=this._typeBuilder(e,t.name,t);return this._applyColumnModifiers(n,t),n}_buildIncrementsColumn(e,t){const n=e.increments(t.name);return t.comment&&n.comment(t.comment),n}_applyColumnModifiers(e,t){if(this._applyConstraintModifiers(e,t),this._applyNullabilityAndDefault(e,t),t.comment&&e.comment(t.comment),t.hasForeignKey&&t.foreignMapTables?.[0]){const n=t.foreignMapTables[0];e.references(n.column||"id").inTable(n.table)}}_applyConstraintModifiers(e,t){t.primary&&e.primary(),t.unique&&e.unique(),t.isUnsigned&&this._supportsUnsigned()&&e.unsigned()}_applyNullabilityAndDefault(e,t){t.nullable?e.nullable():e.notNullable(),null!=t.default&&""!==t.default&&e.defaultTo(this._renderDefault(t.default))}_typeBuilder(e,t,n){const a=String(n.type||"").toUpperCase(),s=this._typeDispatcher()[a];return s?s(e,t,n):e.specificType(t,n.columnType||(n.size?`${a}(${n.size})`:a))}_typeDispatcher(){return BASE_TYPE_DISPATCHER}_renderDefault(e){const t=String(e).trim();return"CURRENT_TIMESTAMP"===t.toUpperCase()||"NOW()"===t.toUpperCase()?this.db.fn.now():/^-?\d+(\.\d+)?$/.test(t)?Number(t):"true"===t||"false"===t?"true"===t:t}_supportsUnsigned(){return!0}_getHelperUtility(){try{return new(require(`./${this._getClientName()}/HelperUtility`))}catch{return null}}async _applyExtras(e){}async _getRelations(e){return{}}async _listTables(){throw new Error("_listTables must be overridden by engine subclass")}_warnOnUnsupportedModifier(e,t,n){warnOnce(`${this._getClientName()}.${e}`,`[${this._getClientName()}] '${e}' modifier is not supported on this engine (seen on ${t}.${n}). See docs/agents/05-multi-db-parity.md.`)}}module.exports=BaseSyncTable;
1
+ const logger=require("../Logger"),ENGINE_WARNINGS=new Set;function warnOnce(e,t){ENGINE_WARNINGS.has(e)||(ENGINE_WARNINGS.add(e),logger.warn(t))}const BASE_TYPE_DISPATCHER={VARCHAR:(e,t,n)=>e.string(t,n.size||255),CHAR:(e,t,n)=>e.string(t,n.size||255),TEXT:(e,t)=>e.text(t),MEDIUMTEXT:(e,t)=>e.text(t),LONGTEXT:(e,t)=>e.text(t),INT:(e,t)=>e.integer(t),INTEGER:(e,t)=>e.integer(t),MEDIUMINT:(e,t)=>e.integer(t),SMALLINT:(e,t)=>e.integer(t),BIGINT:(e,t)=>e.bigInteger(t),TINYINT:(e,t,n)=>e.tinyint?e.tinyint(t):e.specificType(t,n.size?`TINYINT(${n.size})`:"TINYINT"),BOOLEAN:(e,t)=>e.boolean(t),BOOL:(e,t)=>e.boolean(t),DATE:(e,t)=>e.date(t),DATETIME:(e,t)=>e.dateTime(t),TIMESTAMP:(e,t)=>e.timestamp(t),TIME:(e,t)=>e.time(t),JSON:(e,t)=>e.json(t),FLOAT:(e,t)=>e.float(t),DOUBLE:(e,t)=>e.double?e.double(t):e.float(t),REAL:(e,t)=>e.double?e.double(t):e.float(t),DECIMAL:(e,t)=>e.decimal(t),NUMERIC:(e,t)=>e.decimal(t),BINARY:(e,t)=>e.binary(t),VARBINARY:(e,t)=>e.binary(t),BLOB:(e,t)=>e.binary(t),UUID:(e,t)=>e.uuid?e.uuid(t):e.string(t,36)},COLUMN_STRING_SUFFIXES=[e=>e.size?`|size:${e.size}`:"",e=>e.isUnsigned?"|unsigned":"",e=>e.primary?"|primaryKey":"",e=>e.autoIncrement?"|autoIncrement":"",e=>e.nullable?"":"|notNull",e=>e.unique?"|unique":"",e=>null!=e.default&&""!==e.default?`|default:${e.default}`:"",e=>e.onUpdate?`|onUpdate:${e.onUpdate}`:"",e=>e.comment?`|comment:${e.comment}`:"",e=>e.hasForeignKey&&e.foreignMapTables?.[0]?`|foreignKey:${e.foreignMapTables[0].table}:${e.foreignMapTables[0].column}`:""];class BaseSyncTable{constructor(e,t,n=null){this.db=e,this.utils=t,this.controllerWrapper=n}_getClientName(){throw new Error("_getClientName must be overridden by engine subclass")}async existsTable(e){return this.db.schema.hasTable(e)}async syncTable(e){if(await this.existsTable(e.table)){const t=await this.getAlterations(e);await this.alterTable(e.table,t)}else await this.createTable(e);await this._applyExtras(e)}async syncDatabase(){if(!this.controllerWrapper?.schema)throw new Error("controllerWrapper.schema not set.");const e=this.controllerWrapper.schema;for(const t of Object.keys(e))await this.syncTable(e[t]),await this.syncSeedData(e[t],t);logger.info("Database synced by SyncTable...")}async syncSeedData(e,t){if(!e.seed||!Array.isArray(e.seed)||0===e.seed.length)return;const n=await this.db(e.table).count("* as n").first();Number(n?.n)>0?logger.info("Seed data already synced for",t):(await this.db(e.table).insert(e.seed),logger.info("Seed data synced for",t))}async generateSchema(){const e=await this._listTables(),t={},n=this._getHelperUtility();for(const r of e){const e=n?n.modelName(r):r;t[e]={table:r,alias:e,modelName:e,columns:this.getColumnString(await this.getCurrentColumns(r)),seed:[],hasRelations:await this._getRelations(r),indexes:[]}}return t}async createTable(e){await this.db.schema.createTable(e.table,t=>{for(const[n,r]of Object.entries(e.columns))this._applyColumnToBuilder(t,this._resolveColumnFrm(n,r))})}async alterTable(e,t){if(!t||"object"!=typeof t)throw new Error("alterations must be an object");(t.add?.length||0)+(t.drop?.length||0)+(t.modify?.length||0)>0?await this.db.schema.alterTable(e,e=>{for(const n of t.add||[])this._applyColumnToBuilder(e,n);for(const n of t.drop||[])e.dropColumn(n.name);for(const n of t.modify||[]){const t=this._applyColumnToBuilder(e,n);t&&"function"==typeof t.alter&&t.alter()}}):logger.info("No alterations to apply for",e)}async dropTable(e){await this.db.schema.dropTableIfExists(e)}async getCurrentColumns(e){const t=await this.db(e).columnInfo(),n={};for(const[e,r]of Object.entries(t))n[e]=this._formatColumnInfo(e,r);return n}_formatColumnInfo(e,t){const n=String(t.type||"").toLowerCase(),r=n.match(/^([a-z_]+)(?:\((\d+)(?:,\s*\d+)?\))?/);return{name:e,type:(r?r[1]:n).toUpperCase(),size:(r&&r[2]?Number(r[2]):t.maxLength||null)||null,nullable:!1!==t.nullable,default:this._parseDefault(t.defaultValue),primary:!1,unique:!1,autoIncrement:!1,isUnsigned:!1,hasForeignKey:!1,foreignMapTables:[],onUpdate:null,comment:""}}_parseDefault(e){if(null==e)return null;const t=String(e).trim();return""===t?null:t.replace(/^'+|'+$/g,"")}hasColumnChanged(e,t){return!1}async getAlterations(e){const t={add:[],drop:[],modify:[]},n=await this.getCurrentColumns(e.table);for(const[r,a]of Object.entries(e.columns)){const e=this._resolveColumnFrm(r,a),s=n[r];s?this.hasColumnChanged(s,e)&&t.modify.push(e):t.add.push(e)}for(const r of Object.keys(n))e.columns[r]||t.drop.push({name:r});return t}getColumnString(e){return Object.keys(e).reduce((t,n)=>{const r=e[n],a=String(r.type||"").toLowerCase();return t[n]=COLUMN_STRING_SUFFIXES.reduce((e,t)=>e+t(r),a),t},{})}_resolveColumnFrm(e,t){const n="string"==typeof t?this.utils.formatColumnSchema(e,t):t;return this._alignForeignKeyType(n)}_alignForeignKeyType(e){if(!e?.hasForeignKey)return e;const t=e.foreignMapTables?.[0];if(!t?.table)return e;const n=this._resolveParentColumnFrm(t);return n?{...e,...this._matchedForeignKeyType(n)}:e}_resolveParentColumnFrm(e){const t=this._findSchemaColumns(e.table);if(!t)return null;const n=t[e.column||"id"];return null==n?null:"string"==typeof n?this.utils.formatColumnSchema(e.column||"id",n):n}_findSchemaColumns(e){const t=this.controllerWrapper?.schema;if(!t)return null;for(const n of Object.keys(t)){const r=t[n];if(r&&(r.table===e||n===e))return r.columns||null}return null}_matchedForeignKeyType(e){return e.autoIncrement&&e.primary?{type:"BIGINT",size:null,columnType:"BIGINT",isUnsigned:!0}:{type:e.type,size:e.size,columnType:e.columnType,isUnsigned:e.isUnsigned}}_applyColumnToBuilder(e,t){if(t.autoIncrement&&t.primary)return this._buildIncrementsColumn(e,t);const n=this._typeBuilder(e,t.name,t);return this._applyColumnModifiers(n,t),n}_buildIncrementsColumn(e,t){const n=e.increments(t.name);return t.comment&&n.comment(t.comment),n}_applyColumnModifiers(e,t){if(this._applyConstraintModifiers(e,t),this._applyNullabilityAndDefault(e,t),t.comment&&e.comment(t.comment),t.hasForeignKey&&t.foreignMapTables?.[0]){const n=t.foreignMapTables[0];e.references(n.column||"id").inTable(n.table)}}_applyConstraintModifiers(e,t){t.primary&&e.primary(),t.unique&&e.unique(),t.isUnsigned&&this._supportsUnsigned()&&e.unsigned()}_applyNullabilityAndDefault(e,t){t.nullable?e.nullable():e.notNullable(),null!=t.default&&""!==t.default&&e.defaultTo(this._renderDefault(t.default))}_typeBuilder(e,t,n){const r=String(n.type||"").toUpperCase(),a=this._typeDispatcher()[r];return a?a(e,t,n):e.specificType(t,n.columnType||(n.size?`${r}(${n.size})`:r))}_typeDispatcher(){return BASE_TYPE_DISPATCHER}_renderDefault(e){const t=String(e).trim();return"CURRENT_TIMESTAMP"===t.toUpperCase()||"NOW()"===t.toUpperCase()?this.db.fn.now():/^-?\d+(\.\d+)?$/.test(t)?Number(t):"true"===t||"false"===t?"true"===t:t}_supportsUnsigned(){return!0}_getHelperUtility(){try{return new(require(`./${this._getClientName()}/HelperUtility`))}catch{return null}}async _applyExtras(e){}async _getRelations(e){return{}}async _listTables(){throw new Error("_listTables must be overridden by engine subclass")}_warnOnUnsupportedModifier(e,t,n){warnOnce(`${this._getClientName()}.${e}`,`[${this._getClientName()}] '${e}' modifier is not supported on this engine (seen on ${t}.${n}). See docs/agents/05-multi-db-parity.md.`)}}module.exports=BaseSyncTable;
@@ -1 +1 @@
1
- const HelperUtility=require("./HelperUtility"),logger=require("../../Logger");class QueryBuilder{constructor(e,t,r=null){this.db=e,this.utils=t,this.controllerWrapper=r,this.helperUtility=new HelperUtility}getHookService(){return this.controllerWrapper.hookService}getQueryBuilder(e,t=null){let r=this.db(e.table);return t&&(r=t),r._getMyModel=()=>e,r}parseValue(e){return this.helperUtility.parseValue(e)}parseWhereValue(e){return this.helperUtility.parseWhereValue(e)}parseWhereColumn(e){return this.helperUtility.parseWhereColumn(e)}_applyOrWhereCondition(e,t,r,o){if(null!==o)if(Array.isArray(o))e.orWhereIn(t,o);else switch(r){case"between":e.orWhereBetween(t,o);break;case"notBetween":e.orWhereNotBetween(t,o);break;case"in":e.orWhereIn(t,o);break;case"notIn":e.orWhereNotIn(t,o);break;case"like":e.orWhereRaw("?? LIKE ?",[t,o]);break;default:e.orWhere(t,r,o)}else e.orWhereNull(t)}_applyAndWhereCondition(e,t,r,o){if(null!==o)if(Array.isArray(o))e.whereIn(t,o);else switch(r){case"between":e.whereBetween(t,o);break;case"notBetween":e.whereNotBetween(t,o);break;case"in":e.whereIn(t,o);break;case"notIn":e.whereNotIn(t,o);break;case"like":e.whereRaw("?? LIKE ?",[t,o]);break;default:e.where(t,r,o)}else e.whereNull(t)}buildWithTree(e,t=null){return this.helperUtility.dotWalkTree(e,{resolver:({current:e,part:r,source:o})=>t?t({current:e,part:r,source:o}):{}})}_getWithWhereForRelation(e,t){if(!e||"object"!=typeof e)return{direct:{},nested:{}};const r=`${t}.`,o={},i={};for(const[t,n]of Object.entries(e))if(t.startsWith(r)){const e=t.slice(r.length);e.includes(".")?i[e]=n:o[e]=n}return{direct:o,nested:i}}_applyWithWhereConditions(e,t){for(const[r,o]of Object.entries(t)){const{joinType:t="AND",column:i}=this.parseWhereColumn(r),{operator:n,value:s}=this.parseWhereValue(o);"AND"===t?this._applyAndWhereCondition(e,i,n,s):this._applyOrWhereCondition(e,i,n,s)}}async fetchRelatedRows(e,t,r={}){if(e.through){const o=await this.db(e.through).whereIn(e.throughLocalKey,t),i=this.db(e.table).whereIn(e.foreignKey,o.map(t=>t[e.throughForeignKey]));return Object.keys(r).length>0&&this._applyWithWhereConditions(i,r),i}{const o=this.db(e.table).whereIn(e.foreignKey,t);return Object.keys(r).length>0&&this._applyWithWhereConditions(o,r),o}}async fetchAndAttachRelated(e){const{parentRows:t,relName:r,model:o,withTree:i,relation:n,withWhere:s={}}=e;if(!n){const e=this.getHookService().getModelInstance(o),n=`get${r.charAt(0).toUpperCase()+r.slice(1)}Relation`;if("function"==typeof e[n]){const{direct:l,nested:a}=this._getWithWhereForRelation(s,r),h={rows:t,relName:r,model:o,withTree:i,controller:this.controllerWrapper,relation:o.hasRelations[r],qb:this,db:this.db,withWhere:l,nestedWithWhere:a};await e[n](h)}else logger.warn(`Method ${n} not found in model ${o.name}`);return t}const l=t.map(e=>e[n.localKey]);let a=[];const h="one"===n?.type,{direct:c,nested:p}=this._getWithWhereForRelation(s,r);let u={...c};try{const e=this.utils.getModel(this.controllerWrapper,r);if(this.controllerWrapper?.hookService){await this.controllerWrapper.hookService.executeHasSoftDeleteHook(e)&&(u={...u,deleted_at:null})}}catch(e){}a=await this.fetchRelatedRows(n,l,u);const y=new Map;for(const e of a){const t=e[n.foreignKey];y.has(t)||y.set(t,[]),y.get(t).push(e)}for(const e of t){const t=e[n.localKey];h&&1==y.get(t)?.length?e[r]=y.get(t)[0]:e[r]=y.get(t)||[]}const d=Object.keys(i);for(const e of d){const o=i[e],n=t.filter(e=>h?e[r]:e[r].length>0).map(e=>e[r]).reduce((e,t)=>e.concat(t),[]),s=this.utils.getModel(this.controllerWrapper,r);await this.fetchAndAttachRelated({parentRows:n,relName:e,model:s,withTree:o,relation:s.hasRelations[e],withWhere:p})}return t}filterWhere(e,t=""){return t?Object.keys(e).filter(e=>e.includes(t)).reduce((r,o)=>(r[o.replace(t,"")]=e[o],r),{}):Object.keys(e).filter(e=>!e.includes(".")).reduce((t,r)=>(t[r]=e[r],t),{})}buildSelectQuery(e,t){const{where:r={},with:o,select:i,orderBy:n={column:"id",direction:"asc"},limit:s=10,offset:l=0,page:a,groupBy:h,having:c,distinct:p,join:u,leftJoin:y,rightJoin:d,innerJoin:f}=t,g=this.getQueryBuilder(e);i&&(Array.isArray(i)||"string"==typeof i)?g.select(i):g.select("*"),p&&(Array.isArray(p)||"string"==typeof p?g.distinct(p):g.distinct()),u&&this._applyJoins(g,u,"join"),y&&this._applyJoins(g,y,"leftJoin"),d&&this._applyJoins(g,d,"rightJoin"),f&&this._applyJoins(g,f,"innerJoin"),this._applyWhereClause(g,r,o),h&&g.groupBy(h),c&&this._applyHavingClause(g,c),n&&this._applyOrderBy(g,n);let W=l;return a&&s>0&&(W=(Math.max(1,parseInt(a))-1)*s),s>0&&(g.limit(s),W>0&&g.offset(W)),g}async getQuery(e,t){try{const{where:r={},with:o,withWhere:i,limit:n=10,offset:s=0,page:l,join:a,leftJoin:h,rightJoin:c,innerJoin:p}=t,u=this.buildSelectQuery(e,t);let y=!1;const d=n;let f=s,g=1,W=0;l&&n>0&&(g=Math.max(1,parseInt(l)),f=(g-1)*n);const w=[],b=u.toSQL();w.push(b.sql);const m=await u;if(o&&o.length>0){const t=this.buildWithTree(o);for(const r of Object.keys(t))await this.fetchAndAttachRelated({parentRows:m,relName:r,model:e,withTree:t[r],relation:e.hasRelations[r],withWhere:i||{}})}let _=null;if(n>0)try{const t=this.getQueryBuilder(e);r&&Object.keys(r).length>0&&this._applyWhereClause(t,r),a&&this._applyJoins(t,a,"join"),h&&this._applyJoins(t,h,"leftJoin"),c&&this._applyJoins(t,c,"rightJoin"),p&&this._applyJoins(t,p,"innerJoin");const o=t.count("* as cnt");w.push(o.toSQL().sql);_=(await o.first()).cnt}catch(e){logger.warn("Failed to get total count:",e.message),_=m.length}n>0&&null!==_&&(W=Math.ceil(_/n),y=g<W);const A=y?g+1:null,j=g>1?g-1:null;return{data:m,totalCount:_,...this.controllerWrapper.debug?{sqlDebug:w}:{},...n>0?{pagination:{page:g,limit:d,offset:f,totalPages:W,hasNext:y,hasPrev:g>1,nextPage:A,prevPage:j}}:{}}}catch(t){throw logger.error("QueryService.getQuery error:",t),new Error(`Failed to execute query: ${t.message} on model ${e.name}`)}}getSumQuery(e,t){const{where:r={},join:o,leftJoin:i,rightJoin:n,innerJoin:s}=t,l=t.data||t,a=l.sumColumn,h=l.sumFormula;if(!a&&!h)throw new Error("Sum action requires either data.sumColumn or data.sumFormula");const c=e=>"`"+String(e).replace(/[`\\]/g,"")+"`";let p;if(a){const e=String(a).trim().replace(/[^a-zA-Z0-9_]/g,"");if(!e)throw new Error("data.sumColumn must be a valid column name");p="SUM("+c(e)+")"}else{const e=String(h).trim();if(!/\{[a-zA-Z0-9_]+\}/.test(e))throw new Error("data.sumFormula must contain at least one {columnName}");let t=0;for(const r of e)if("("===r)t++;else if(")"===r&&(t--,t<0))break;if(0!==t)throw new Error("data.sumFormula has unbalanced or misordered parentheses ( and )");const r=e.replace(/\{[a-zA-Z0-9_]+\}/g,"@");if(!/^[\s0-9.+*\/\-()@]+$/.test(r))throw new Error("data.sumFormula may only use BODMAS: numbers, + - * /, parentheses, and {columnName}");p="SUM("+e.replace(/\{([a-zA-Z0-9_]+)\}/g,(e,t)=>c(t))+")"}const u=this.getQueryBuilder(e);return o&&this._applyJoins(u,o,"join"),i&&this._applyJoins(u,i,"leftJoin"),n&&this._applyJoins(u,n,"rightJoin"),s&&this._applyJoins(u,s,"innerJoin"),this._applyWhereClause(u,r,[]),u.select(this.db.raw(p+" as sum")),u}_applyWhereWithArray(e,t,r){for(const o of t)this._applyWhereClause(e,o,r)}_applyWhereClause(e,t,r=[]){if(Array.isArray(t))this._applyWhereWithArray(e,t,r);else{if(t&&Object.keys(t).length>0){let r=this.helperUtility.getDotWalkQuery(t);logger.debug({filteredWhere:r}),r=this.helperUtility.objectFilter(r,(e,t)=>!e.startsWith("!")&&("__exists__"!==e&&("object"!=typeof t||null===t)));for(const[t,o]of Object.entries(r)){const{joinType:r="AND",column:i}=this.parseWhereColumn(t),{operator:n,value:s}=this.parseWhereValue(o);"AND"===r?this._applyAndWhereCondition(e,i,n,s):this._applyOrWhereCondition(e,i,n,s)}}this._applyNestedWhere(e,t,r)}}_getNestedWhereConditions(e,t){if(!e||"object"!=typeof e)return{};const r=`${t}.`,o={};for(const[i,n]of Object.entries(e))if(i.startsWith(r)){o[i.slice(r.length)]=n}else i===t&&!0===n&&(o.__exists__=!0);return o}_getTopLevelRelationsFromWhere(e){if(!e||"object"!=typeof e)return[];const t=new Set;for(const r of Object.keys(e))if(r.includes(".")){const e=r.split(".")[0];t.add(e)}else r.startsWith("!")&&t.add(r);return[...t]}_applyNestedWhere(e,t,r){const o=this,i=e._getMyModel(),n=this._getTopLevelRelationsFromWhere(t);if(0!==n.length)for(const r of n){const n=r.startsWith("!"),s=n?r.slice(1):r,l=this._getNestedWhereConditions(t,r);if(0===Object.keys(l).length)continue;const a=i.hasRelations?.[s];if(!a){logger.warn(`Relation ${s} not found in model ${i.modelName}`);continue}let h;try{h=this.utils.getModel(this.controllerWrapper,a.table||s)}catch(e){try{h=this.utils.getModel(this.controllerWrapper,s)}catch(e){logger.warn(`Model for relation ${s} (table: ${a.table}) not found`);continue}}e[n?"whereNotExists":"whereExists"](function(){const e=o.getQueryBuilder(h,this.select("*").from(a.table));e.whereRaw("??.?? = ??.??",[a.table,a.foreignKey,i.table,a.localKey]);if(!(!0===l.__exists__&&1===Object.keys(l).length)){const t={...l};delete t.__exists__,o._applyWhereClause(e,t,[])}})}}_applyJoins(e,t,r){const o=Array.isArray(t)?t:[t];for(const t of o)"string"==typeof t?e[r](t):"object"==typeof t&&(t.table&&t.on?e[r](t.table,t.on):t.table&&t.first&&t.operator&&t.second&&e[r](t.table,t.first,t.operator,t.second))}_applyWithWhere(e,t){try{if(Array.isArray(t))for(const r of t)"string"==typeof r?e.withWhere(r):"object"==typeof r&&e.withWhere(r.column,r.operator,r.value);else if("object"==typeof t)for(const[r,o]of Object.entries(t))e.withWhere(r,o)}catch(e){logger.warn("Failed to apply withWhere:",e.message)}}_applyHavingClause(e,t){for(const[r,o]of Object.entries(t))"object"==typeof o&&o.operator?e.having(r,o.operator,o.value):e.having(r,o)}_applyOrderBy(e,t){if(Array.isArray(t))for(const r of t)"string"==typeof r?e.orderBy(r):"object"==typeof r&&e.orderBy(r.column,r.direction||"asc");else"string"==typeof t?e.orderBy(t):"object"==typeof t&&e.orderBy(t.column,t.direction||"asc")}}module.exports=QueryBuilder;
1
+ const HelperUtility=require("./HelperUtility"),logger=require("../../Logger");class QueryBuilder{constructor(e,t,r=null){this.db=e,this.utils=t,this.controllerWrapper=r,this.helperUtility=new HelperUtility}getHookService(){return this.controllerWrapper.hookService}getQueryBuilder(e,t=null){let r=this.db(e.table);return t&&(r=t),r._getMyModel=()=>e,r}parseValue(e){return this.helperUtility.parseValue(e)}parseWhereValue(e){return this.helperUtility.parseWhereValue(e)}parseWhereColumn(e){return this.helperUtility.parseWhereColumn(e)}_applyOrWhereCondition(e,t,r,o){if(null!==o)switch(r){case"between":e.orWhereBetween(t,o);break;case"notBetween":e.orWhereNotBetween(t,o);break;case"in":e.orWhereIn(t,o);break;case"notIn":e.orWhereNotIn(t,o);break;case"like":e.orWhereRaw("?? LIKE ?",[t,o]);break;default:Array.isArray(o)?e.orWhereIn(t,o):e.orWhere(t,r,o)}else e.orWhereNull(t)}_applyAndWhereCondition(e,t,r,o){if(null!==o)switch(r){case"between":e.whereBetween(t,o);break;case"notBetween":e.whereNotBetween(t,o);break;case"in":e.whereIn(t,o);break;case"notIn":e.whereNotIn(t,o);break;case"like":e.whereRaw("?? LIKE ?",[t,o]);break;default:Array.isArray(o)?e.whereIn(t,o):e.where(t,r,o)}else e.whereNull(t)}buildWithTree(e,t=null){return this.helperUtility.dotWalkTree(e,{resolver:({current:e,part:r,source:o})=>t?t({current:e,part:r,source:o}):{}})}_getWithWhereForRelation(e,t){if(!e||"object"!=typeof e)return{direct:{},nested:{}};const r=`${t}.`,o={},i={};for(const[t,n]of Object.entries(e))if(t.startsWith(r)){const e=t.slice(r.length);e.includes(".")?i[e]=n:o[e]=n}return{direct:o,nested:i}}_applyWithWhereConditions(e,t){for(const[r,o]of Object.entries(t)){const{joinType:t="AND",column:i}=this.parseWhereColumn(r),{operator:n,value:s}=this.parseWhereValue(o);"AND"===t?this._applyAndWhereCondition(e,i,n,s):this._applyOrWhereCondition(e,i,n,s)}}async fetchRelatedRows(e,t,r={}){if(e.through){const o=await this.db(e.through).whereIn(e.throughLocalKey,t),i=this.db(e.table).whereIn(e.foreignKey,o.map(t=>t[e.throughForeignKey]));return Object.keys(r).length>0&&this._applyWithWhereConditions(i,r),i}{const o=this.db(e.table).whereIn(e.foreignKey,t);return Object.keys(r).length>0&&this._applyWithWhereConditions(o,r),o}}async fetchAndAttachRelated(e){const{parentRows:t,relName:r,model:o,withTree:i,relation:n,withWhere:s={}}=e;if(!n){const e=this.getHookService().getModelInstance(o),n=`get${r.charAt(0).toUpperCase()+r.slice(1)}Relation`;if("function"==typeof e[n]){const{direct:l,nested:a}=this._getWithWhereForRelation(s,r),h={rows:t,relName:r,model:o,withTree:i,controller:this.controllerWrapper,relation:o.hasRelations[r],qb:this,db:this.db,withWhere:l,nestedWithWhere:a};await e[n](h)}else logger.warn(`Method ${n} not found in model ${o.name}`);return t}const l=t.map(e=>e[n.localKey]);let a=[];const h="one"===n?.type,{direct:c,nested:p}=this._getWithWhereForRelation(s,r);let u={...c};try{const e=this.utils.getModel(this.controllerWrapper,r);if(this.controllerWrapper?.hookService){await this.controllerWrapper.hookService.executeHasSoftDeleteHook(e)&&(u={...u,deleted_at:null})}}catch(e){}a=await this.fetchRelatedRows(n,l,u);const y=new Map;for(const e of a){const t=e[n.foreignKey];y.has(t)||y.set(t,[]),y.get(t).push(e)}for(const e of t){const t=e[n.localKey];h&&1==y.get(t)?.length?e[r]=y.get(t)[0]:e[r]=y.get(t)||[]}const d=Object.keys(i);for(const e of d){const o=i[e],n=t.filter(e=>h?e[r]:e[r].length>0).map(e=>e[r]).reduce((e,t)=>e.concat(t),[]),s=this.utils.getModel(this.controllerWrapper,r);await this.fetchAndAttachRelated({parentRows:n,relName:e,model:s,withTree:o,relation:s.hasRelations[e],withWhere:p})}return t}filterWhere(e,t=""){return t?Object.keys(e).filter(e=>e.includes(t)).reduce((r,o)=>(r[o.replace(t,"")]=e[o],r),{}):Object.keys(e).filter(e=>!e.includes(".")).reduce((t,r)=>(t[r]=e[r],t),{})}buildSelectQuery(e,t){const{where:r={},with:o,select:i,orderBy:n={column:"id",direction:"asc"},limit:s=10,offset:l=0,page:a,groupBy:h,having:c,distinct:p,join:u,leftJoin:y,rightJoin:d,innerJoin:f}=t,g=this.getQueryBuilder(e);i&&(Array.isArray(i)||"string"==typeof i)?g.select(i):g.select("*"),p&&(Array.isArray(p)||"string"==typeof p?g.distinct(p):g.distinct()),u&&this._applyJoins(g,u,"join"),y&&this._applyJoins(g,y,"leftJoin"),d&&this._applyJoins(g,d,"rightJoin"),f&&this._applyJoins(g,f,"innerJoin"),this._applyWhereClause(g,r,o),h&&g.groupBy(h),c&&this._applyHavingClause(g,c),n&&this._applyOrderBy(g,n);let W=l;return a&&s>0&&(W=(Math.max(1,parseInt(a))-1)*s),s>0&&(g.limit(s),W>0&&g.offset(W)),g}async getQuery(e,t){try{const{where:r={},with:o,withWhere:i,limit:n=10,offset:s=0,page:l,join:a,leftJoin:h,rightJoin:c,innerJoin:p}=t,u=this.buildSelectQuery(e,t);let y=!1;const d=n;let f=s,g=1,W=0;l&&n>0&&(g=Math.max(1,parseInt(l)),f=(g-1)*n);const w=[],b=u.toSQL();w.push(b.sql);const m=await u;if(o&&o.length>0){const t=this.buildWithTree(o);for(const r of Object.keys(t))await this.fetchAndAttachRelated({parentRows:m,relName:r,model:e,withTree:t[r],relation:e.hasRelations[r],withWhere:i||{}})}let _=null;if(n>0)try{const t=this.getQueryBuilder(e);r&&Object.keys(r).length>0&&this._applyWhereClause(t,r),a&&this._applyJoins(t,a,"join"),h&&this._applyJoins(t,h,"leftJoin"),c&&this._applyJoins(t,c,"rightJoin"),p&&this._applyJoins(t,p,"innerJoin");const o=t.count("* as cnt");w.push(o.toSQL().sql);_=(await o.first()).cnt}catch(e){logger.warn("Failed to get total count:",e.message),_=m.length}n>0&&null!==_&&(W=Math.ceil(_/n),y=g<W);const A=y?g+1:null,j=g>1?g-1:null;return{data:m,totalCount:_,...this.controllerWrapper.debug?{sqlDebug:w}:{},...n>0?{pagination:{page:g,limit:d,offset:f,totalPages:W,hasNext:y,hasPrev:g>1,nextPage:A,prevPage:j}}:{}}}catch(t){throw logger.error("QueryService.getQuery error:",t),new Error(`Failed to execute query: ${t.message} on model ${e.name}`)}}getSumQuery(e,t){const{where:r={},join:o,leftJoin:i,rightJoin:n,innerJoin:s}=t,l=t.data||t,a=l.sumColumn,h=l.sumFormula;if(!a&&!h)throw new Error("Sum action requires either data.sumColumn or data.sumFormula");const c=e=>"`"+String(e).replace(/[`\\]/g,"")+"`";let p;if(a){const e=String(a).trim().replace(/[^a-zA-Z0-9_]/g,"");if(!e)throw new Error("data.sumColumn must be a valid column name");p="SUM("+c(e)+")"}else{const e=String(h).trim();if(!/\{[a-zA-Z0-9_]+\}/.test(e))throw new Error("data.sumFormula must contain at least one {columnName}");let t=0;for(const r of e)if("("===r)t++;else if(")"===r&&(t--,t<0))break;if(0!==t)throw new Error("data.sumFormula has unbalanced or misordered parentheses ( and )");const r=e.replace(/\{[a-zA-Z0-9_]+\}/g,"@");if(!/^[\s0-9.+*\/\-()@]+$/.test(r))throw new Error("data.sumFormula may only use BODMAS: numbers, + - * /, parentheses, and {columnName}");p="SUM("+e.replace(/\{([a-zA-Z0-9_]+)\}/g,(e,t)=>c(t))+")"}const u=this.getQueryBuilder(e);return o&&this._applyJoins(u,o,"join"),i&&this._applyJoins(u,i,"leftJoin"),n&&this._applyJoins(u,n,"rightJoin"),s&&this._applyJoins(u,s,"innerJoin"),this._applyWhereClause(u,r,[]),u.select(this.db.raw(p+" as sum")),u}_applyWhereWithArray(e,t,r){for(const o of t)this._applyWhereClause(e,o,r)}_applyWhereClause(e,t,r=[]){if(Array.isArray(t))this._applyWhereWithArray(e,t,r);else{if(t&&Object.keys(t).length>0){let r=this.helperUtility.getDotWalkQuery(t);logger.debug({filteredWhere:r}),r=this.helperUtility.objectFilter(r,(e,t)=>!e.startsWith("!")&&("__exists__"!==e&&("object"!=typeof t||null===t)));for(const[t,o]of Object.entries(r)){const{joinType:r="AND",column:i}=this.parseWhereColumn(t),{operator:n,value:s}=this.parseWhereValue(o);"AND"===r?this._applyAndWhereCondition(e,i,n,s):this._applyOrWhereCondition(e,i,n,s)}}this._applyNestedWhere(e,t,r)}}_getNestedWhereConditions(e,t){if(!e||"object"!=typeof e)return{};const r=`${t}.`,o={};for(const[i,n]of Object.entries(e))if(i.startsWith(r)){o[i.slice(r.length)]=n}else i===t&&!0===n&&(o.__exists__=!0);return o}_getTopLevelRelationsFromWhere(e){if(!e||"object"!=typeof e)return[];const t=new Set;for(const r of Object.keys(e))if(r.includes(".")){const e=r.split(".")[0];t.add(e)}else r.startsWith("!")&&t.add(r);return[...t]}_applyNestedWhere(e,t,r){const o=this,i=e._getMyModel(),n=this._getTopLevelRelationsFromWhere(t);if(0!==n.length)for(const r of n){const n=r.startsWith("!"),s=n?r.slice(1):r,l=this._getNestedWhereConditions(t,r);if(0===Object.keys(l).length)continue;const a=i.hasRelations?.[s];if(!a){logger.warn(`Relation ${s} not found in model ${i.modelName}`);continue}let h;try{h=this.utils.getModel(this.controllerWrapper,a.table||s)}catch(e){try{h=this.utils.getModel(this.controllerWrapper,s)}catch(e){logger.warn(`Model for relation ${s} (table: ${a.table}) not found`);continue}}e[n?"whereNotExists":"whereExists"](function(){const e=o.getQueryBuilder(h,this.select("*").from(a.table));e.whereRaw("??.?? = ??.??",[a.table,a.foreignKey,i.table,a.localKey]);if(!(!0===l.__exists__&&1===Object.keys(l).length)){const t={...l};delete t.__exists__,o._applyWhereClause(e,t,[])}})}}_applyJoins(e,t,r){const o=Array.isArray(t)?t:[t];for(const t of o)"string"==typeof t?e[r](t):"object"==typeof t&&(t.table&&t.on?e[r](t.table,t.on):t.table&&t.first&&t.operator&&t.second&&e[r](t.table,t.first,t.operator,t.second))}_applyWithWhere(e,t){try{if(Array.isArray(t))for(const r of t)"string"==typeof r?e.withWhere(r):"object"==typeof r&&e.withWhere(r.column,r.operator,r.value);else if("object"==typeof t)for(const[r,o]of Object.entries(t))e.withWhere(r,o)}catch(e){logger.warn("Failed to apply withWhere:",e.message)}}_applyHavingClause(e,t){for(const[r,o]of Object.entries(t))"object"==typeof o&&o.operator?e.having(r,o.operator,o.value):e.having(r,o)}_applyOrderBy(e,t){if(Array.isArray(t))for(const r of t)"string"==typeof r?e.orderBy(r):"object"==typeof r&&e.orderBy(r.column,r.direction||"asc");else"string"==typeof t?e.orderBy(t):"object"==typeof t&&e.orderBy(t.column,t.direction||"asc")}}module.exports=QueryBuilder;
@@ -1 +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}async executeCountQuery(e,t){const r=await this.db(e.table).count();return Object.values(r[0])[0]}async executeSumQuery(e,t){const r=await this.queryBuilder.getSumQuery(e,t).first(),a=r&&null!=r.sum?r.sum:0;return Number(a)}async executeCreateQuery(e,t){const r=await this.db(e.table).insert(t.data),a=Array.isArray(r)?r[0]:r;if(null==a)return[];const u=e.columns&&e.columns.find(e=>e.primary),n=u&&u.name?u.name:"id",s=await this.db(e.table).where(n,a).select("*");return Array.isArray(s)?s:[s]}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)}async executeReplaceQuery(e,t){return await this.db(e.table).replace(t.data)}buildDryRun(e,t,r){const a=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:a[0]?a[0].sql:null,bindings:a[0]?a[0].bindings:[],statements:a}}_dryRunBuilders(e,t,r){const a=e.table,u=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.db(a).count()];case"sum":return[this.queryBuilder.getSumQuery(e,t)];case"create":return[this.db(a).insert(t.data).returning("*")];case"update":return[this.db(a).where(u).update(t.data).returning("*")];case"softDelete":return[this.db(a).where(u).update({deleted_at:new Date}).returning("*")];case"delete":return[this.db(a).where(u).delete()];case"replace":return[this.db(a).replace(t.data)];case"upsert":return[this.db(a).insert(t.data).onConflict(t.conflict).merge(t.data)];case"sync":return[this.db(a).insert(t.data).onConflict(t.conflict).merge(t.data),this.db(a).where(u).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||{});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 +1 @@
1
- const HelperUtility=require("./HelperUtility"),logger=require("../../Logger");class QueryBuilder{constructor(e,t,r=null){this.db=e,this.utils=t,this.controllerWrapper=r,this.helperUtility=new HelperUtility}getHookService(){return this.controllerWrapper.hookService}getQueryBuilder(e,t=null){let r=this.db(e.table);return t&&(r=t),r._getMyModel=()=>e,r}parseValue(e){return this.helperUtility.parseValue(e)}parseWhereValue(e){return this.helperUtility.parseWhereValue(e)}parseWhereColumn(e){return this.helperUtility.parseWhereColumn(e)}_applyOrWhereCondition(e,t,r,o){if(null!==o)if(Array.isArray(o))e.orWhereIn(t,o);else switch(r){case"between":e.orWhereBetween(t,o);break;case"notBetween":e.orWhereNotBetween(t,o);break;case"in":e.orWhereIn(t,o);break;case"notIn":e.orWhereNotIn(t,o);break;case"like":e.orWhere(t,"like",o);break;default:e.orWhere(t,r,o)}else e.orWhereNull(t)}_applyAndWhereCondition(e,t,r,o){if(null!==o)if(Array.isArray(o))e.whereIn(t,o);else switch(r){case"between":e.whereBetween(t,o);break;case"notBetween":e.whereNotBetween(t,o);break;case"in":e.whereIn(t,o);break;case"notIn":e.whereNotIn(t,o);break;case"like":e.where(t,"like",o);break;default:e.where(t,r,o)}else e.whereNull(t)}buildWithTree(e,t=null){return this.helperUtility.dotWalkTree(e,{resolver:({current:e,part:r,source:o})=>t?t({current:e,part:r,source:o}):{}})}_getWithWhereForRelation(e,t){if(!e||"object"!=typeof e)return{direct:{},nested:{}};const r=`${t}.`,o={},i={};for(const[t,n]of Object.entries(e))if(t.startsWith(r)){const e=t.slice(r.length);e.includes(".")?i[e]=n:o[e]=n}return{direct:o,nested:i}}_applyWithWhereConditions(e,t){for(const[r,o]of Object.entries(t)){const{joinType:t="AND",column:i}=this.parseWhereColumn(r),{operator:n,value:s}=this.parseWhereValue(o);"AND"===t?this._applyAndWhereCondition(e,i,n,s):this._applyOrWhereCondition(e,i,n,s)}}async fetchRelatedRows(e,t,r={}){if(e.through){const o=await this.db(e.through).whereIn(e.throughLocalKey,t),i=this.db(e.table).whereIn(e.foreignKey,o.map(t=>t[e.throughForeignKey]));return Object.keys(r).length>0&&this._applyWithWhereConditions(i,r),i}{const o=this.db(e.table).whereIn(e.foreignKey,t);return Object.keys(r).length>0&&this._applyWithWhereConditions(o,r),o}}async fetchAndAttachRelated(e){const{parentRows:t,relName:r,model:o,withTree:i,relation:n,withWhere:s={}}=e;if(!n){const e=this.getHookService().getModelInstance(o),n=`get${r.charAt(0).toUpperCase()+r.slice(1)}Relation`;if("function"==typeof e[n]){const{direct:l,nested:a}=this._getWithWhereForRelation(s,r),h={rows:t,relName:r,model:o,withTree:i,controller:this.controllerWrapper,relation:o.hasRelations[r],qb:this,db:this.db,withWhere:l,nestedWithWhere:a};await e[n](h)}else logger.warn(`Method ${n} not found in model ${o.name}`);return t}const l=t.map(e=>e[n.localKey]),a="one"===n?.type;let h=[];const{direct:c,nested:p}=this._getWithWhereForRelation(s,r);let u={...c};try{const e=this.utils.getModel(this.controllerWrapper,r);if(this.controllerWrapper?.hookService){await this.controllerWrapper.hookService.executeHasSoftDeleteHook(e)&&(u={...u,deleted_at:null})}}catch(e){}h=await this.fetchRelatedRows(n,l,u);const y=new Map;for(const e of h){const t=e[n.foreignKey];y.has(t)||y.set(t,[]),y.get(t).push(e)}for(const e of t){const t=e[n.localKey];a&&1==y.get(t)?.length?e[r]=y.get(t)[0]:e[r]=y.get(t)||[]}const d=Object.keys(i);for(const e of d){const o=i[e],n=t.filter(e=>a?e[r]:e[r].length>0).map(e=>e[r]).reduce((e,t)=>e.concat(t),[]),s=this.utils.getModel(this.controllerWrapper,r);await this.fetchAndAttachRelated({parentRows:n,relName:e,model:s,withTree:o,relation:s.hasRelations[e],withWhere:p})}return t}filterWhere(e,t=""){return t?Object.keys(e).filter(e=>e.includes(t)).reduce((r,o)=>(r[o.replace(t,"")]=e[o],r),{}):Object.keys(e).filter(e=>!e.includes(".")).reduce((t,r)=>(t[r]=e[r],t),{})}buildSelectQuery(e,t){const{where:r={},with:o,select:i,orderBy:n={column:"id",direction:"asc"},limit:s=10,offset:l=0,page:a,groupBy:h,having:c,distinct:p,join:u,leftJoin:y,rightJoin:d,innerJoin:f}=t,g=this.getQueryBuilder(e);i&&(Array.isArray(i)||"string"==typeof i)?g.select(i):g.select("*"),p&&(Array.isArray(p)||"string"==typeof p?g.distinct(p):g.distinct()),u&&this._applyJoins(g,u,"join"),y&&this._applyJoins(g,y,"leftJoin"),d&&this._applyJoins(g,d,"rightJoin"),f&&this._applyJoins(g,f,"innerJoin"),this._applyWhereClause(g,r,o),h&&g.groupBy(h),c&&this._applyHavingClause(g,c),n&&this._applyOrderBy(g,n);let W=l;return a&&s>0&&(W=(Math.max(1,parseInt(a))-1)*s),s>0&&(g.limit(s),W>0&&g.offset(W)),g}async getQuery(e,t){try{const{where:r={},with:o,withWhere:i,limit:n=10,offset:s=0,page:l,join:a,leftJoin:h,rightJoin:c,innerJoin:p}=t,u=this.buildSelectQuery(e,t);let y=!1;const d=n;let f=s,g=1,W=0;l&&n>0&&(g=Math.max(1,parseInt(l)),f=(g-1)*n);const w=[],b=u.toSQL();w.push(b.sql);const m=await u;if(o&&o.length>0){const t=this.buildWithTree(o);for(const r of Object.keys(t))await this.fetchAndAttachRelated({parentRows:m,relName:r,model:e,withTree:t[r],relation:e.hasRelations[r],withWhere:i||{}})}let _=null;if(n>0)try{const t=this.getQueryBuilder(e);r&&Object.keys(r).length>0&&this._applyWhereClause(t,r),a&&this._applyJoins(t,a,"join"),h&&this._applyJoins(t,h,"leftJoin"),c&&this._applyJoins(t,c,"rightJoin"),p&&this._applyJoins(t,p,"innerJoin");const o=t.count("* as cnt");w.push(o.toSQL().sql);_=(await o.first()).cnt}catch(e){logger.warn("Failed to get total count:",e.message),_=m.length}n>0&&null!==_&&(W=Math.ceil(_/n),y=g<W);const A=y?g+1:null,j=g>1?g-1:null;return{data:m,totalCount:_,...this.controllerWrapper.debug?{sqlDebug:w}:{},...n>0?{pagination:{page:g,limit:d,offset:f,totalPages:W,hasNext:y,hasPrev:g>1,nextPage:A,prevPage:j}}:{}}}catch(t){throw logger.error("QueryService.getQuery error:",t),new Error(`Failed to execute query: ${t.message} on model ${e.name}`)}}getSumQuery(e,t){const{where:r={},join:o,leftJoin:i,rightJoin:n,innerJoin:s}=t,l=t.data||t,a=l.sumColumn,h=l.sumFormula;if(!a&&!h)throw new Error("Sum action requires either data.sumColumn or data.sumFormula");const c=e=>'"'+String(e).replace(/["\\]/g,"")+'"';let p;if(a){const e=String(a).trim().replace(/[^a-zA-Z0-9_]/g,"");if(!e)throw new Error("data.sumColumn must be a valid column name");p="SUM("+c(e)+")"}else{const e=String(h).trim();if(!/\{[a-zA-Z0-9_]+\}/.test(e))throw new Error("data.sumFormula must contain at least one {columnName}");let t=0;for(const r of e)if("("===r)t++;else if(")"===r&&(t--,t<0))break;if(0!==t)throw new Error("data.sumFormula has unbalanced or misordered parentheses ( and )");const r=e.replace(/\{[a-zA-Z0-9_]+\}/g,"@");if(!/^[\s0-9.+*\/\-()@]+$/.test(r))throw new Error("data.sumFormula may only use BODMAS: numbers, + - * /, parentheses, and {columnName}");p="SUM("+e.replace(/\{([a-zA-Z0-9_]+)\}/g,(e,t)=>c(t))+")"}const u=this.getQueryBuilder(e);return o&&this._applyJoins(u,o,"join"),i&&this._applyJoins(u,i,"leftJoin"),n&&this._applyJoins(u,n,"rightJoin"),s&&this._applyJoins(u,s,"innerJoin"),this._applyWhereClause(u,r,[]),u.select(this.db.raw(p+" as sum")),u}_applyWhereWithArray(e,t,r){for(const o of t)this._applyWhereClause(e,o,r)}_applyWhereClause(e,t,r=[]){if(Array.isArray(t))this._applyWhereWithArray(e,t,r);else{if(t&&Object.keys(t).length>0){let r=this.helperUtility.getDotWalkQuery(t);logger.debug({filteredWhere:r}),r=this.helperUtility.objectFilter(r,(e,t)=>!e.startsWith("!")&&("__exists__"!==e&&("object"!=typeof t||null===t)));for(const[t,o]of Object.entries(r)){const{joinType:r="AND",column:i}=this.parseWhereColumn(t),{operator:n,value:s}=this.parseWhereValue(o);"AND"===r?this._applyAndWhereCondition(e,i,n,s):this._applyOrWhereCondition(e,i,n,s)}}this._applyNestedWhere(e,t,r)}}_getNestedWhereConditions(e,t){if(!e||"object"!=typeof e)return{};const r=`${t}.`,o={};for(const[i,n]of Object.entries(e))if(i.startsWith(r)){o[i.slice(r.length)]=n}else i===t&&!0===n&&(o.__exists__=!0);return o}_getTopLevelRelationsFromWhere(e){if(!e||"object"!=typeof e)return[];const t=new Set;for(const r of Object.keys(e))if(r.includes(".")){const e=r.split(".")[0];t.add(e)}else r.startsWith("!")&&t.add(r);return[...t]}_applyNestedWhere(e,t,r){const o=this,i=e._getMyModel(),n=this._getTopLevelRelationsFromWhere(t);if(0!==n.length)for(const r of n){const n=r.startsWith("!"),s=n?r.slice(1):r,l=this._getNestedWhereConditions(t,r);if(0===Object.keys(l).length)continue;const a=i.hasRelations?.[s];if(!a){logger.warn(`Relation ${s} not found in model ${i.modelName}`);continue}let h;try{h=this.utils.getModel(this.controllerWrapper,a.table||s)}catch(e){try{h=this.utils.getModel(this.controllerWrapper,s)}catch(e){logger.warn(`Model for relation ${s} (table: ${a.table}) not found`);continue}}e[n?"whereNotExists":"whereExists"](function(){const e=o.getQueryBuilder(h,this.select("*").from(a.table));e.whereRaw("??.?? = ??.??",[a.table,a.foreignKey,i.table,a.localKey]);if(!(!0===l.__exists__&&1===Object.keys(l).length)){const t={...l};delete t.__exists__,o._applyWhereClause(e,t,[])}})}}_applyJoins(e,t,r){const o=Array.isArray(t)?t:[t];for(const t of o)"string"==typeof t?e[r](t):"object"==typeof t&&(t.table&&t.on?e[r](t.table,t.on):t.table&&t.first&&t.operator&&t.second&&e[r](t.table,t.first,t.operator,t.second))}_applyWithWhere(e,t){try{if(Array.isArray(t))for(const r of t)"string"==typeof r?e.withWhere(r):"object"==typeof r&&e.withWhere(r.column,r.operator,r.value);else if("object"==typeof t)for(const[r,o]of Object.entries(t))e.withWhere(r,o)}catch(e){logger.warn("Failed to apply withWhere:",e.message)}}_applyHavingClause(e,t){for(const[r,o]of Object.entries(t))"object"==typeof o&&o.operator?e.having(r,o.operator,o.value):e.having(r,o)}_applyOrderBy(e,t){if(Array.isArray(t))for(const r of t)"string"==typeof r?e.orderBy(r):"object"==typeof r&&e.orderBy(r.column,r.direction||"asc");else"string"==typeof t?e.orderBy(t):"object"==typeof t&&e.orderBy(t.column,t.direction||"asc")}}module.exports=QueryBuilder;
1
+ const HelperUtility=require("./HelperUtility"),logger=require("../../Logger");class QueryBuilder{constructor(e,t,r=null){this.db=e,this.utils=t,this.controllerWrapper=r,this.helperUtility=new HelperUtility}getHookService(){return this.controllerWrapper.hookService}getQueryBuilder(e,t=null){let r=this.db(e.table);return t&&(r=t),r._getMyModel=()=>e,r}parseValue(e){return this.helperUtility.parseValue(e)}parseWhereValue(e){return this.helperUtility.parseWhereValue(e)}parseWhereColumn(e){return this.helperUtility.parseWhereColumn(e)}_applyOrWhereCondition(e,t,r,o){if(null!==o)switch(r){case"between":e.orWhereBetween(t,o);break;case"notBetween":e.orWhereNotBetween(t,o);break;case"in":e.orWhereIn(t,o);break;case"notIn":e.orWhereNotIn(t,o);break;case"like":e.orWhere(t,"like",o);break;default:Array.isArray(o)?e.orWhereIn(t,o):e.orWhere(t,r,o)}else e.orWhereNull(t)}_applyAndWhereCondition(e,t,r,o){if(null!==o)switch(r){case"between":e.whereBetween(t,o);break;case"notBetween":e.whereNotBetween(t,o);break;case"in":e.whereIn(t,o);break;case"notIn":e.whereNotIn(t,o);break;case"like":e.where(t,"like",o);break;default:Array.isArray(o)?e.whereIn(t,o):e.where(t,r,o)}else e.whereNull(t)}buildWithTree(e,t=null){return this.helperUtility.dotWalkTree(e,{resolver:({current:e,part:r,source:o})=>t?t({current:e,part:r,source:o}):{}})}_getWithWhereForRelation(e,t){if(!e||"object"!=typeof e)return{direct:{},nested:{}};const r=`${t}.`,o={},i={};for(const[t,n]of Object.entries(e))if(t.startsWith(r)){const e=t.slice(r.length);e.includes(".")?i[e]=n:o[e]=n}return{direct:o,nested:i}}_applyWithWhereConditions(e,t){for(const[r,o]of Object.entries(t)){const{joinType:t="AND",column:i}=this.parseWhereColumn(r),{operator:n,value:s}=this.parseWhereValue(o);"AND"===t?this._applyAndWhereCondition(e,i,n,s):this._applyOrWhereCondition(e,i,n,s)}}async fetchRelatedRows(e,t,r={}){if(e.through){const o=await this.db(e.through).whereIn(e.throughLocalKey,t),i=this.db(e.table).whereIn(e.foreignKey,o.map(t=>t[e.throughForeignKey]));return Object.keys(r).length>0&&this._applyWithWhereConditions(i,r),i}{const o=this.db(e.table).whereIn(e.foreignKey,t);return Object.keys(r).length>0&&this._applyWithWhereConditions(o,r),o}}async fetchAndAttachRelated(e){const{parentRows:t,relName:r,model:o,withTree:i,relation:n,withWhere:s={}}=e;if(!n){const e=this.getHookService().getModelInstance(o),n=`get${r.charAt(0).toUpperCase()+r.slice(1)}Relation`;if("function"==typeof e[n]){const{direct:l,nested:a}=this._getWithWhereForRelation(s,r),h={rows:t,relName:r,model:o,withTree:i,controller:this.controllerWrapper,relation:o.hasRelations[r],qb:this,db:this.db,withWhere:l,nestedWithWhere:a};await e[n](h)}else logger.warn(`Method ${n} not found in model ${o.name}`);return t}const l=t.map(e=>e[n.localKey]),a="one"===n?.type;let h=[];const{direct:c,nested:p}=this._getWithWhereForRelation(s,r);let u={...c};try{const e=this.utils.getModel(this.controllerWrapper,r);if(this.controllerWrapper?.hookService){await this.controllerWrapper.hookService.executeHasSoftDeleteHook(e)&&(u={...u,deleted_at:null})}}catch(e){}h=await this.fetchRelatedRows(n,l,u);const y=new Map;for(const e of h){const t=e[n.foreignKey];y.has(t)||y.set(t,[]),y.get(t).push(e)}for(const e of t){const t=e[n.localKey];a&&1==y.get(t)?.length?e[r]=y.get(t)[0]:e[r]=y.get(t)||[]}const d=Object.keys(i);for(const e of d){const o=i[e],n=t.filter(e=>a?e[r]:e[r].length>0).map(e=>e[r]).reduce((e,t)=>e.concat(t),[]),s=this.utils.getModel(this.controllerWrapper,r);await this.fetchAndAttachRelated({parentRows:n,relName:e,model:s,withTree:o,relation:s.hasRelations[e],withWhere:p})}return t}filterWhere(e,t=""){return t?Object.keys(e).filter(e=>e.includes(t)).reduce((r,o)=>(r[o.replace(t,"")]=e[o],r),{}):Object.keys(e).filter(e=>!e.includes(".")).reduce((t,r)=>(t[r]=e[r],t),{})}buildSelectQuery(e,t){const{where:r={},with:o,select:i,orderBy:n={column:"id",direction:"asc"},limit:s=10,offset:l=0,page:a,groupBy:h,having:c,distinct:p,join:u,leftJoin:y,rightJoin:d,innerJoin:f}=t,g=this.getQueryBuilder(e);i&&(Array.isArray(i)||"string"==typeof i)?g.select(i):g.select("*"),p&&(Array.isArray(p)||"string"==typeof p?g.distinct(p):g.distinct()),u&&this._applyJoins(g,u,"join"),y&&this._applyJoins(g,y,"leftJoin"),d&&this._applyJoins(g,d,"rightJoin"),f&&this._applyJoins(g,f,"innerJoin"),this._applyWhereClause(g,r,o),h&&g.groupBy(h),c&&this._applyHavingClause(g,c),n&&this._applyOrderBy(g,n);let W=l;return a&&s>0&&(W=(Math.max(1,parseInt(a))-1)*s),s>0&&(g.limit(s),W>0&&g.offset(W)),g}async getQuery(e,t){try{const{where:r={},with:o,withWhere:i,limit:n=10,offset:s=0,page:l,join:a,leftJoin:h,rightJoin:c,innerJoin:p}=t,u=this.buildSelectQuery(e,t);let y=!1;const d=n;let f=s,g=1,W=0;l&&n>0&&(g=Math.max(1,parseInt(l)),f=(g-1)*n);const w=[],b=u.toSQL();w.push(b.sql);const m=await u;if(o&&o.length>0){const t=this.buildWithTree(o);for(const r of Object.keys(t))await this.fetchAndAttachRelated({parentRows:m,relName:r,model:e,withTree:t[r],relation:e.hasRelations[r],withWhere:i||{}})}let _=null;if(n>0)try{const t=this.getQueryBuilder(e);r&&Object.keys(r).length>0&&this._applyWhereClause(t,r),a&&this._applyJoins(t,a,"join"),h&&this._applyJoins(t,h,"leftJoin"),c&&this._applyJoins(t,c,"rightJoin"),p&&this._applyJoins(t,p,"innerJoin");const o=t.count("* as cnt");w.push(o.toSQL().sql);_=(await o.first()).cnt}catch(e){logger.warn("Failed to get total count:",e.message),_=m.length}n>0&&null!==_&&(W=Math.ceil(_/n),y=g<W);const A=y?g+1:null,j=g>1?g-1:null;return{data:m,totalCount:_,...this.controllerWrapper.debug?{sqlDebug:w}:{},...n>0?{pagination:{page:g,limit:d,offset:f,totalPages:W,hasNext:y,hasPrev:g>1,nextPage:A,prevPage:j}}:{}}}catch(t){throw logger.error("QueryService.getQuery error:",t),new Error(`Failed to execute query: ${t.message} on model ${e.name}`)}}getSumQuery(e,t){const{where:r={},join:o,leftJoin:i,rightJoin:n,innerJoin:s}=t,l=t.data||t,a=l.sumColumn,h=l.sumFormula;if(!a&&!h)throw new Error("Sum action requires either data.sumColumn or data.sumFormula");const c=e=>'"'+String(e).replace(/["\\]/g,"")+'"';let p;if(a){const e=String(a).trim().replace(/[^a-zA-Z0-9_]/g,"");if(!e)throw new Error("data.sumColumn must be a valid column name");p="SUM("+c(e)+")"}else{const e=String(h).trim();if(!/\{[a-zA-Z0-9_]+\}/.test(e))throw new Error("data.sumFormula must contain at least one {columnName}");let t=0;for(const r of e)if("("===r)t++;else if(")"===r&&(t--,t<0))break;if(0!==t)throw new Error("data.sumFormula has unbalanced or misordered parentheses ( and )");const r=e.replace(/\{[a-zA-Z0-9_]+\}/g,"@");if(!/^[\s0-9.+*\/\-()@]+$/.test(r))throw new Error("data.sumFormula may only use BODMAS: numbers, + - * /, parentheses, and {columnName}");p="SUM("+e.replace(/\{([a-zA-Z0-9_]+)\}/g,(e,t)=>c(t))+")"}const u=this.getQueryBuilder(e);return o&&this._applyJoins(u,o,"join"),i&&this._applyJoins(u,i,"leftJoin"),n&&this._applyJoins(u,n,"rightJoin"),s&&this._applyJoins(u,s,"innerJoin"),this._applyWhereClause(u,r,[]),u.select(this.db.raw(p+" as sum")),u}_applyWhereWithArray(e,t,r){for(const o of t)this._applyWhereClause(e,o,r)}_applyWhereClause(e,t,r=[]){if(Array.isArray(t))this._applyWhereWithArray(e,t,r);else{if(t&&Object.keys(t).length>0){let r=this.helperUtility.getDotWalkQuery(t);logger.debug({filteredWhere:r}),r=this.helperUtility.objectFilter(r,(e,t)=>!e.startsWith("!")&&("__exists__"!==e&&("object"!=typeof t||null===t)));for(const[t,o]of Object.entries(r)){const{joinType:r="AND",column:i}=this.parseWhereColumn(t),{operator:n,value:s}=this.parseWhereValue(o);"AND"===r?this._applyAndWhereCondition(e,i,n,s):this._applyOrWhereCondition(e,i,n,s)}}this._applyNestedWhere(e,t,r)}}_getNestedWhereConditions(e,t){if(!e||"object"!=typeof e)return{};const r=`${t}.`,o={};for(const[i,n]of Object.entries(e))if(i.startsWith(r)){o[i.slice(r.length)]=n}else i===t&&!0===n&&(o.__exists__=!0);return o}_getTopLevelRelationsFromWhere(e){if(!e||"object"!=typeof e)return[];const t=new Set;for(const r of Object.keys(e))if(r.includes(".")){const e=r.split(".")[0];t.add(e)}else r.startsWith("!")&&t.add(r);return[...t]}_applyNestedWhere(e,t,r){const o=this,i=e._getMyModel(),n=this._getTopLevelRelationsFromWhere(t);if(0!==n.length)for(const r of n){const n=r.startsWith("!"),s=n?r.slice(1):r,l=this._getNestedWhereConditions(t,r);if(0===Object.keys(l).length)continue;const a=i.hasRelations?.[s];if(!a){logger.warn(`Relation ${s} not found in model ${i.modelName}`);continue}let h;try{h=this.utils.getModel(this.controllerWrapper,a.table||s)}catch(e){try{h=this.utils.getModel(this.controllerWrapper,s)}catch(e){logger.warn(`Model for relation ${s} (table: ${a.table}) not found`);continue}}e[n?"whereNotExists":"whereExists"](function(){const e=o.getQueryBuilder(h,this.select("*").from(a.table));e.whereRaw("??.?? = ??.??",[a.table,a.foreignKey,i.table,a.localKey]);if(!(!0===l.__exists__&&1===Object.keys(l).length)){const t={...l};delete t.__exists__,o._applyWhereClause(e,t,[])}})}}_applyJoins(e,t,r){const o=Array.isArray(t)?t:[t];for(const t of o)"string"==typeof t?e[r](t):"object"==typeof t&&(t.table&&t.on?e[r](t.table,t.on):t.table&&t.first&&t.operator&&t.second&&e[r](t.table,t.first,t.operator,t.second))}_applyWithWhere(e,t){try{if(Array.isArray(t))for(const r of t)"string"==typeof r?e.withWhere(r):"object"==typeof r&&e.withWhere(r.column,r.operator,r.value);else if("object"==typeof t)for(const[r,o]of Object.entries(t))e.withWhere(r,o)}catch(e){logger.warn("Failed to apply withWhere:",e.message)}}_applyHavingClause(e,t){for(const[r,o]of Object.entries(t))"object"==typeof o&&o.operator?e.having(r,o.operator,o.value):e.having(r,o)}_applyOrderBy(e,t){if(Array.isArray(t))for(const r of t)"string"==typeof r?e.orderBy(r):"object"==typeof r&&e.orderBy(r.column,r.direction||"asc");else"string"==typeof t?e.orderBy(t):"object"==typeof t&&e.orderBy(t.column,t.direction||"asc")}}module.exports=QueryBuilder;
@@ -1 +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}async executeCountQuery(e,t){const r=await this.db(e.table).count();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)}async executeReplaceQuery(e,t){return await this.db(e.table).replace(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,a=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.db(u).count()];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(a).update(t.data).returning("*")];case"softDelete":return[this.db(u).where(a).update({deleted_at:new Date}).returning("*")];case"delete":return[this.db(u).where(a).delete()];case"replace":return[this.db(u).replace(t.data)];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(a).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"),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 +1 @@
1
- const HelperUtility=require("./HelperUtility"),logger=require("../../Logger");class QueryBuilder{constructor(e,t,r=null){this.db=e,this.utils=t,this.controllerWrapper=r,this.helperUtility=new HelperUtility}getHookService(){return this.controllerWrapper.hookService}getQueryBuilder(e,t=null){let r=this.db(e.table);return t&&(r=t),r._getMyModel=()=>e,r}parseValue(e){return this.helperUtility.parseValue(e)}parseWhereValue(e){return this.helperUtility.parseWhereValue(e)}parseWhereColumn(e){return this.helperUtility.parseWhereColumn(e)}_applyOrWhereCondition(e,t,r,o){if(null!==o)if(Array.isArray(o))e.orWhereIn(t,o);else switch(r){case"between":e.orWhereBetween(t,o);break;case"notBetween":e.orWhereNotBetween(t,o);break;case"in":e.orWhereIn(t,o);break;case"notIn":e.orWhereNotIn(t,o);break;case"like":e.orWhere(t,"like",o);break;default:e.orWhere(t,r,o)}else e.orWhereNull(t)}_applyAndWhereCondition(e,t,r,o){if(null!==o)if(Array.isArray(o))e.whereIn(t,o);else switch(r){case"between":e.whereBetween(t,o);break;case"notBetween":e.whereNotBetween(t,o);break;case"in":e.whereIn(t,o);break;case"notIn":e.whereNotIn(t,o);break;case"like":e.where(t,"like",o);break;default:e.where(t,r,o)}else e.whereNull(t)}buildWithTree(e,t=null){return this.helperUtility.dotWalkTree(e,{resolver:({current:e,part:r,source:o})=>t?t({current:e,part:r,source:o}):{}})}_getWithWhereForRelation(e,t){if(!e||"object"!=typeof e)return{direct:{},nested:{}};const r=`${t}.`,o={},i={};for(const[t,n]of Object.entries(e))if(t.startsWith(r)){const e=t.slice(r.length);e.includes(".")?i[e]=n:o[e]=n}return{direct:o,nested:i}}_applyWithWhereConditions(e,t){for(const[r,o]of Object.entries(t)){const{joinType:t="AND",column:i}=this.parseWhereColumn(r),{operator:n,value:s}=this.parseWhereValue(o);"AND"===t?this._applyAndWhereCondition(e,i,n,s):this._applyOrWhereCondition(e,i,n,s)}}async fetchRelatedRows(e,t,r={}){if(e.through){const o=await this.db(e.through).whereIn(e.throughLocalKey,t),i=this.db(e.table).whereIn(e.foreignKey,o.map(t=>t[e.throughForeignKey]));return Object.keys(r).length>0&&this._applyWithWhereConditions(i,r),i}{const o=this.db(e.table).whereIn(e.foreignKey,t);return Object.keys(r).length>0&&this._applyWithWhereConditions(o,r),o}}async fetchAndAttachRelated(e){const{parentRows:t,relName:r,model:o,withTree:i,relation:n,withWhere:s={}}=e;if(!n){const e=this.getHookService().getModelInstance(o),n=`get${r.charAt(0).toUpperCase()+r.slice(1)}Relation`;if("function"==typeof e[n]){const{direct:l,nested:a}=this._getWithWhereForRelation(s,r),h={rows:t,relName:r,model:o,withTree:i,controller:this.controllerWrapper,relation:o.hasRelations[r],qb:this,db:this.db,withWhere:l,nestedWithWhere:a};await e[n](h)}else logger.warn(`Method ${n} not found in model ${o.name}`);return t}const l=t.map(e=>e[n.localKey]),a="one"===n?.type;let h=[];const{direct:c,nested:p}=this._getWithWhereForRelation(s,r);let u={...c};try{const e=this.utils.getModel(this.controllerWrapper,r);if(this.controllerWrapper?.hookService){await this.controllerWrapper.hookService.executeHasSoftDeleteHook(e)&&(u={...u,deleted_at:null})}}catch(e){}h=await this.fetchRelatedRows(n,l,u);const y=new Map;for(const e of h){const t=e[n.foreignKey];y.has(t)||y.set(t,[]),y.get(t).push(e)}for(const e of t){const t=e[n.localKey];a&&1==y.get(t)?.length?e[r]=y.get(t)[0]:e[r]=y.get(t)||[]}const d=Object.keys(i);for(const e of d){const o=i[e],n=t.filter(e=>a?e[r]:e[r].length>0).map(e=>e[r]).reduce((e,t)=>e.concat(t),[]),s=this.utils.getModel(this.controllerWrapper,r);await this.fetchAndAttachRelated({parentRows:n,relName:e,model:s,withTree:o,relation:s.hasRelations[e],withWhere:p})}return t}filterWhere(e,t=""){return t?Object.keys(e).filter(e=>e.includes(t)).reduce((r,o)=>(r[o.replace(t,"")]=e[o],r),{}):Object.keys(e).filter(e=>!e.includes(".")).reduce((t,r)=>(t[r]=e[r],t),{})}buildSelectQuery(e,t){const{where:r={},with:o,select:i,orderBy:n={column:"id",direction:"asc"},limit:s=10,offset:l=0,page:a,groupBy:h,having:c,distinct:p,join:u,leftJoin:y,rightJoin:d,innerJoin:f}=t,g=this.getQueryBuilder(e);i&&(Array.isArray(i)||"string"==typeof i)?g.select(i):g.select("*"),p&&(Array.isArray(p)||"string"==typeof p?g.distinct(p):g.distinct()),u&&this._applyJoins(g,u,"join"),y&&this._applyJoins(g,y,"leftJoin"),d&&this._applyJoins(g,d,"rightJoin"),f&&this._applyJoins(g,f,"innerJoin"),this._applyWhereClause(g,r,o),h&&g.groupBy(h),c&&this._applyHavingClause(g,c),n&&this._applyOrderBy(g,n);let W=l;return a&&s>0&&(W=(Math.max(1,parseInt(a))-1)*s),s>0&&(g.limit(s),W>0&&g.offset(W)),g}async getQuery(e,t){try{const{where:r={},with:o,withWhere:i,limit:n=10,offset:s=0,page:l,join:a,leftJoin:h,rightJoin:c,innerJoin:p}=t,u=this.buildSelectQuery(e,t);let y=!1;const d=n;let f=s,g=1,W=0;l&&n>0&&(g=Math.max(1,parseInt(l)),f=(g-1)*n);const w=[],m=u.toSQL();w.push(m.sql);const b=await u;if(o&&o.length>0){const t=this.buildWithTree(o);for(const r of Object.keys(t))await this.fetchAndAttachRelated({parentRows:b,relName:r,model:e,withTree:t[r],relation:e.hasRelations[r],withWhere:i||{}})}let _=null;if(n>0)try{const t=this.getQueryBuilder(e);r&&Object.keys(r).length>0&&this._applyWhereClause(t,r),a&&this._applyJoins(t,a,"join"),h&&this._applyJoins(t,h,"leftJoin"),c&&this._applyJoins(t,c,"rightJoin"),p&&this._applyJoins(t,p,"innerJoin");const o=t.count("* as cnt");w.push(o.toSQL().sql);_=(await o.first()).cnt}catch(e){logger.warn("Failed to get total count:",e.message),_=b.length}n>0&&null!==_&&(W=Math.ceil(_/n),y=g<W);const A=y?g+1:null,j=g>1?g-1:null;return{data:b,totalCount:_,...this.controllerWrapper.debug?{sqlDebug:w}:{},...n>0?{pagination:{page:g,limit:d,offset:f,totalPages:W,hasNext:y,hasPrev:g>1,nextPage:A,prevPage:j}}:{}}}catch(t){throw logger.error("QueryService.getQuery error:",t),new Error(`Failed to execute query: ${t.message} on model ${e.name}`)}}getSumQuery(e,t){const{where:r={},join:o,leftJoin:i,rightJoin:n,innerJoin:s}=t,l=t.data||t,a=l.sumColumn,h=l.sumFormula;if(!a&&!h)throw new Error("Sum action requires either data.sumColumn or data.sumFormula");const c=e=>'"'+String(e).replace(/["\\]/g,"")+'"';let p;if(a){const e=String(a).trim().replace(/[^a-zA-Z0-9_]/g,"");if(!e)throw new Error("data.sumColumn must be a valid column name");p="SUM("+c(e)+")"}else{const e=String(h).trim();if(!/\{[a-zA-Z0-9_]+\}/.test(e))throw new Error("data.sumFormula must contain at least one {columnName}");let t=0;for(const r of e)if("("===r)t++;else if(")"===r&&(t--,t<0))break;if(0!==t)throw new Error("data.sumFormula has unbalanced or misordered parentheses ( and )");const r=e.replace(/\{[a-zA-Z0-9_]+\}/g,"@");if(!/^[\s0-9.+*\/\-()@]+$/.test(r))throw new Error("data.sumFormula may only use BODMAS: numbers, + - * /, parentheses, and {columnName}");p="SUM("+e.replace(/\{([a-zA-Z0-9_]+)\}/g,(e,t)=>c(t))+")"}const u=this.getQueryBuilder(e);return o&&this._applyJoins(u,o,"join"),i&&this._applyJoins(u,i,"leftJoin"),n&&this._applyJoins(u,n,"rightJoin"),s&&this._applyJoins(u,s,"innerJoin"),this._applyWhereClause(u,r,[]),u.select(this.db.raw(p+" as sum")),u}_applyWhereWithArray(e,t,r){for(const o of t)this._applyWhereClause(e,o,r)}_applyWhereClause(e,t,r=[]){if(Array.isArray(t))this._applyWhereWithArray(e,t,r);else{if(t&&Object.keys(t).length>0){let r=this.helperUtility.getDotWalkQuery(t);r=this.helperUtility.objectFilter(r,(e,t)=>!e.startsWith("!")&&("__exists__"!==e&&("object"!=typeof t||null===t)));for(const[t,o]of Object.entries(r)){const{joinType:r="AND",column:i}=this.parseWhereColumn(t),{operator:n,value:s}=this.parseWhereValue(o);"AND"===r?this._applyAndWhereCondition(e,i,n,s):this._applyOrWhereCondition(e,i,n,s)}}this._applyNestedWhere(e,t,r)}}_getNestedWhereConditions(e,t){if(!e||"object"!=typeof e)return{};const r=`${t}.`,o={};for(const[i,n]of Object.entries(e))if(i.startsWith(r)){o[i.slice(r.length)]=n}else i===t&&!0===n&&(o.__exists__=!0);return o}_getTopLevelRelationsFromWhere(e){if(!e||"object"!=typeof e)return[];const t=new Set;for(const r of Object.keys(e))if(r.includes(".")){const e=r.split(".")[0];t.add(e)}else r.startsWith("!")&&t.add(r);return[...t]}_applyNestedWhere(e,t,r){const o=this,i=e._getMyModel(),n=this._getTopLevelRelationsFromWhere(t);if(0!==n.length)for(const r of n){const n=r.startsWith("!"),s=n?r.slice(1):r,l=this._getNestedWhereConditions(t,r);if(0===Object.keys(l).length)continue;const a=i.hasRelations?.[s];if(!a){logger.warn(`Relation ${s} not found in model ${i.modelName}`);continue}let h;try{h=this.utils.getModel(this.controllerWrapper,a.table||s)}catch(e){try{h=this.utils.getModel(this.controllerWrapper,s)}catch(e){logger.warn(`Model for relation ${s} (table: ${a.table}) not found`);continue}}e[n?"whereNotExists":"whereExists"](function(){const e=o.getQueryBuilder(h,this.select("*").from(a.table));e.whereRaw("??.?? = ??.??",[a.table,a.foreignKey,i.table,a.localKey]);if(!(!0===l.__exists__&&1===Object.keys(l).length)){const t={...l};delete t.__exists__,o._applyWhereClause(e,t,[])}})}}_applyJoins(e,t,r){const o=Array.isArray(t)?t:[t];for(const t of o)"string"==typeof t?e[r](t):"object"==typeof t&&(t.table&&t.on?e[r](t.table,t.on):t.table&&t.first&&t.operator&&t.second&&e[r](t.table,t.first,t.operator,t.second))}_applyWithWhere(e,t){try{if(Array.isArray(t))for(const r of t)"string"==typeof r?e.withWhere(r):"object"==typeof r&&e.withWhere(r.column,r.operator,r.value);else if("object"==typeof t)for(const[r,o]of Object.entries(t))e.withWhere(r,o)}catch(e){logger.warn("Failed to apply withWhere:",e.message)}}_applyHavingClause(e,t){for(const[r,o]of Object.entries(t))"object"==typeof o&&o.operator?e.having(r,o.operator,o.value):e.having(r,o)}_applyOrderBy(e,t){if(Array.isArray(t))for(const r of t)"string"==typeof r?e.orderBy(r):"object"==typeof r&&e.orderBy(r.column,r.direction||"asc");else"string"==typeof t?e.orderBy(t):"object"==typeof t&&e.orderBy(t.column,t.direction||"asc")}}module.exports=QueryBuilder;
1
+ const HelperUtility=require("./HelperUtility"),logger=require("../../Logger");class QueryBuilder{constructor(e,t,r=null){this.db=e,this.utils=t,this.controllerWrapper=r,this.helperUtility=new HelperUtility}getHookService(){return this.controllerWrapper.hookService}getQueryBuilder(e,t=null){let r=this.db(e.table);return t&&(r=t),r._getMyModel=()=>e,r}parseValue(e){return this.helperUtility.parseValue(e)}parseWhereValue(e){return this.helperUtility.parseWhereValue(e)}parseWhereColumn(e){return this.helperUtility.parseWhereColumn(e)}_applyOrWhereCondition(e,t,r,o){if(null!==o)switch(r){case"between":e.orWhereBetween(t,o);break;case"notBetween":e.orWhereNotBetween(t,o);break;case"in":e.orWhereIn(t,o);break;case"notIn":e.orWhereNotIn(t,o);break;case"like":e.orWhere(t,"like",o);break;default:Array.isArray(o)?e.orWhereIn(t,o):e.orWhere(t,r,o)}else e.orWhereNull(t)}_applyAndWhereCondition(e,t,r,o){if(null!==o)switch(r){case"between":e.whereBetween(t,o);break;case"notBetween":e.whereNotBetween(t,o);break;case"in":e.whereIn(t,o);break;case"notIn":e.whereNotIn(t,o);break;case"like":e.where(t,"like",o);break;default:Array.isArray(o)?e.whereIn(t,o):e.where(t,r,o)}else e.whereNull(t)}buildWithTree(e,t=null){return this.helperUtility.dotWalkTree(e,{resolver:({current:e,part:r,source:o})=>t?t({current:e,part:r,source:o}):{}})}_getWithWhereForRelation(e,t){if(!e||"object"!=typeof e)return{direct:{},nested:{}};const r=`${t}.`,o={},i={};for(const[t,n]of Object.entries(e))if(t.startsWith(r)){const e=t.slice(r.length);e.includes(".")?i[e]=n:o[e]=n}return{direct:o,nested:i}}_applyWithWhereConditions(e,t){for(const[r,o]of Object.entries(t)){const{joinType:t="AND",column:i}=this.parseWhereColumn(r),{operator:n,value:s}=this.parseWhereValue(o);"AND"===t?this._applyAndWhereCondition(e,i,n,s):this._applyOrWhereCondition(e,i,n,s)}}async fetchRelatedRows(e,t,r={}){if(e.through){const o=await this.db(e.through).whereIn(e.throughLocalKey,t),i=this.db(e.table).whereIn(e.foreignKey,o.map(t=>t[e.throughForeignKey]));return Object.keys(r).length>0&&this._applyWithWhereConditions(i,r),i}{const o=this.db(e.table).whereIn(e.foreignKey,t);return Object.keys(r).length>0&&this._applyWithWhereConditions(o,r),o}}async fetchAndAttachRelated(e){const{parentRows:t,relName:r,model:o,withTree:i,relation:n,withWhere:s={}}=e;if(!n){const e=this.getHookService().getModelInstance(o),n=`get${r.charAt(0).toUpperCase()+r.slice(1)}Relation`;if("function"==typeof e[n]){const{direct:l,nested:a}=this._getWithWhereForRelation(s,r),h={rows:t,relName:r,model:o,withTree:i,controller:this.controllerWrapper,relation:o.hasRelations[r],qb:this,db:this.db,withWhere:l,nestedWithWhere:a};await e[n](h)}else logger.warn(`Method ${n} not found in model ${o.name}`);return t}const l=t.map(e=>e[n.localKey]),a="one"===n?.type;let h=[];const{direct:c,nested:p}=this._getWithWhereForRelation(s,r);let u={...c};try{const e=this.utils.getModel(this.controllerWrapper,r);if(this.controllerWrapper?.hookService){await this.controllerWrapper.hookService.executeHasSoftDeleteHook(e)&&(u={...u,deleted_at:null})}}catch(e){}h=await this.fetchRelatedRows(n,l,u);const y=new Map;for(const e of h){const t=e[n.foreignKey];y.has(t)||y.set(t,[]),y.get(t).push(e)}for(const e of t){const t=e[n.localKey];a&&1==y.get(t)?.length?e[r]=y.get(t)[0]:e[r]=y.get(t)||[]}const d=Object.keys(i);for(const e of d){const o=i[e],n=t.filter(e=>a?e[r]:e[r].length>0).map(e=>e[r]).reduce((e,t)=>e.concat(t),[]),s=this.utils.getModel(this.controllerWrapper,r);await this.fetchAndAttachRelated({parentRows:n,relName:e,model:s,withTree:o,relation:s.hasRelations[e],withWhere:p})}return t}filterWhere(e,t=""){return t?Object.keys(e).filter(e=>e.includes(t)).reduce((r,o)=>(r[o.replace(t,"")]=e[o],r),{}):Object.keys(e).filter(e=>!e.includes(".")).reduce((t,r)=>(t[r]=e[r],t),{})}buildSelectQuery(e,t){const{where:r={},with:o,select:i,orderBy:n={column:"id",direction:"asc"},limit:s=10,offset:l=0,page:a,groupBy:h,having:c,distinct:p,join:u,leftJoin:y,rightJoin:d,innerJoin:f}=t,g=this.getQueryBuilder(e);i&&(Array.isArray(i)||"string"==typeof i)?g.select(i):g.select("*"),p&&(Array.isArray(p)||"string"==typeof p?g.distinct(p):g.distinct()),u&&this._applyJoins(g,u,"join"),y&&this._applyJoins(g,y,"leftJoin"),d&&this._applyJoins(g,d,"rightJoin"),f&&this._applyJoins(g,f,"innerJoin"),this._applyWhereClause(g,r,o),h&&g.groupBy(h),c&&this._applyHavingClause(g,c),n&&this._applyOrderBy(g,n);let W=l;return a&&s>0&&(W=(Math.max(1,parseInt(a))-1)*s),s>0&&(g.limit(s),W>0&&g.offset(W)),g}async getQuery(e,t){try{const{where:r={},with:o,withWhere:i,limit:n=10,offset:s=0,page:l,join:a,leftJoin:h,rightJoin:c,innerJoin:p}=t,u=this.buildSelectQuery(e,t);let y=!1;const d=n;let f=s,g=1,W=0;l&&n>0&&(g=Math.max(1,parseInt(l)),f=(g-1)*n);const w=[],m=u.toSQL();w.push(m.sql);const b=await u;if(o&&o.length>0){const t=this.buildWithTree(o);for(const r of Object.keys(t))await this.fetchAndAttachRelated({parentRows:b,relName:r,model:e,withTree:t[r],relation:e.hasRelations[r],withWhere:i||{}})}let _=null;if(n>0)try{const t=this.getQueryBuilder(e);r&&Object.keys(r).length>0&&this._applyWhereClause(t,r),a&&this._applyJoins(t,a,"join"),h&&this._applyJoins(t,h,"leftJoin"),c&&this._applyJoins(t,c,"rightJoin"),p&&this._applyJoins(t,p,"innerJoin");const o=t.count("* as cnt");w.push(o.toSQL().sql);_=(await o.first()).cnt}catch(e){logger.warn("Failed to get total count:",e.message),_=b.length}n>0&&null!==_&&(W=Math.ceil(_/n),y=g<W);const A=y?g+1:null,j=g>1?g-1:null;return{data:b,totalCount:_,...this.controllerWrapper.debug?{sqlDebug:w}:{},...n>0?{pagination:{page:g,limit:d,offset:f,totalPages:W,hasNext:y,hasPrev:g>1,nextPage:A,prevPage:j}}:{}}}catch(t){throw logger.error("QueryService.getQuery error:",t),new Error(`Failed to execute query: ${t.message} on model ${e.name}`)}}getSumQuery(e,t){const{where:r={},join:o,leftJoin:i,rightJoin:n,innerJoin:s}=t,l=t.data||t,a=l.sumColumn,h=l.sumFormula;if(!a&&!h)throw new Error("Sum action requires either data.sumColumn or data.sumFormula");const c=e=>'"'+String(e).replace(/["\\]/g,"")+'"';let p;if(a){const e=String(a).trim().replace(/[^a-zA-Z0-9_]/g,"");if(!e)throw new Error("data.sumColumn must be a valid column name");p="SUM("+c(e)+")"}else{const e=String(h).trim();if(!/\{[a-zA-Z0-9_]+\}/.test(e))throw new Error("data.sumFormula must contain at least one {columnName}");let t=0;for(const r of e)if("("===r)t++;else if(")"===r&&(t--,t<0))break;if(0!==t)throw new Error("data.sumFormula has unbalanced or misordered parentheses ( and )");const r=e.replace(/\{[a-zA-Z0-9_]+\}/g,"@");if(!/^[\s0-9.+*\/\-()@]+$/.test(r))throw new Error("data.sumFormula may only use BODMAS: numbers, + - * /, parentheses, and {columnName}");p="SUM("+e.replace(/\{([a-zA-Z0-9_]+)\}/g,(e,t)=>c(t))+")"}const u=this.getQueryBuilder(e);return o&&this._applyJoins(u,o,"join"),i&&this._applyJoins(u,i,"leftJoin"),n&&this._applyJoins(u,n,"rightJoin"),s&&this._applyJoins(u,s,"innerJoin"),this._applyWhereClause(u,r,[]),u.select(this.db.raw(p+" as sum")),u}_applyWhereWithArray(e,t,r){for(const o of t)this._applyWhereClause(e,o,r)}_applyWhereClause(e,t,r=[]){if(Array.isArray(t))this._applyWhereWithArray(e,t,r);else{if(t&&Object.keys(t).length>0){let r=this.helperUtility.getDotWalkQuery(t);r=this.helperUtility.objectFilter(r,(e,t)=>!e.startsWith("!")&&("__exists__"!==e&&("object"!=typeof t||null===t)));for(const[t,o]of Object.entries(r)){const{joinType:r="AND",column:i}=this.parseWhereColumn(t),{operator:n,value:s}=this.parseWhereValue(o);"AND"===r?this._applyAndWhereCondition(e,i,n,s):this._applyOrWhereCondition(e,i,n,s)}}this._applyNestedWhere(e,t,r)}}_getNestedWhereConditions(e,t){if(!e||"object"!=typeof e)return{};const r=`${t}.`,o={};for(const[i,n]of Object.entries(e))if(i.startsWith(r)){o[i.slice(r.length)]=n}else i===t&&!0===n&&(o.__exists__=!0);return o}_getTopLevelRelationsFromWhere(e){if(!e||"object"!=typeof e)return[];const t=new Set;for(const r of Object.keys(e))if(r.includes(".")){const e=r.split(".")[0];t.add(e)}else r.startsWith("!")&&t.add(r);return[...t]}_applyNestedWhere(e,t,r){const o=this,i=e._getMyModel(),n=this._getTopLevelRelationsFromWhere(t);if(0!==n.length)for(const r of n){const n=r.startsWith("!"),s=n?r.slice(1):r,l=this._getNestedWhereConditions(t,r);if(0===Object.keys(l).length)continue;const a=i.hasRelations?.[s];if(!a){logger.warn(`Relation ${s} not found in model ${i.modelName}`);continue}let h;try{h=this.utils.getModel(this.controllerWrapper,a.table||s)}catch(e){try{h=this.utils.getModel(this.controllerWrapper,s)}catch(e){logger.warn(`Model for relation ${s} (table: ${a.table}) not found`);continue}}e[n?"whereNotExists":"whereExists"](function(){const e=o.getQueryBuilder(h,this.select("*").from(a.table));e.whereRaw("??.?? = ??.??",[a.table,a.foreignKey,i.table,a.localKey]);if(!(!0===l.__exists__&&1===Object.keys(l).length)){const t={...l};delete t.__exists__,o._applyWhereClause(e,t,[])}})}}_applyJoins(e,t,r){const o=Array.isArray(t)?t:[t];for(const t of o)"string"==typeof t?e[r](t):"object"==typeof t&&(t.table&&t.on?e[r](t.table,t.on):t.table&&t.first&&t.operator&&t.second&&e[r](t.table,t.first,t.operator,t.second))}_applyWithWhere(e,t){try{if(Array.isArray(t))for(const r of t)"string"==typeof r?e.withWhere(r):"object"==typeof r&&e.withWhere(r.column,r.operator,r.value);else if("object"==typeof t)for(const[r,o]of Object.entries(t))e.withWhere(r,o)}catch(e){logger.warn("Failed to apply withWhere:",e.message)}}_applyHavingClause(e,t){for(const[r,o]of Object.entries(t))"object"==typeof o&&o.operator?e.having(r,o.operator,o.value):e.having(r,o)}_applyOrderBy(e,t){if(Array.isArray(t))for(const r of t)"string"==typeof r?e.orderBy(r):"object"==typeof r&&e.orderBy(r.column,r.direction||"asc");else"string"==typeof t?e.orderBy(t):"object"==typeof t&&e.orderBy(t.column,t.direction||"asc")}}module.exports=QueryBuilder;
@@ -1 +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}async executeCountQuery(e,t){const r=await this.db(e.table).count();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)}async executeReplaceQuery(e,t){return await this.db(e.table).replace(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,a=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.db(u).count()];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(a).update(t.data).returning("*")];case"softDelete":return[this.db(u).where(a).update({deleted_at:new Date}).returning("*")];case"delete":return[this.db(u).where(a).delete()];case"replace":return[this.db(u).replace(t.data)];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(a).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||{});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;
package/index.d.ts CHANGED
@@ -62,8 +62,181 @@ export interface SchemaDescription {
62
62
  models: ModelDescription[];
63
63
  }
64
64
 
65
+ // ---- Request contract (issue #18) ---------------------------------------
66
+ // A discriminated union over `action`, derived from the contract in
67
+ // docs/agents/06-request-contract.md §1/§5. `where`/`data` are intentionally
68
+ // permissive (the runtime validates string-encoded operators + arbitrary
69
+ // columns); the types give shape, action-level narrowing, and autocomplete.
70
+
71
+ export type KormAction =
72
+ | 'list'
73
+ | 'show'
74
+ | 'count'
75
+ | 'sum'
76
+ | 'create'
77
+ | 'update'
78
+ | 'delete'
79
+ | 'replace'
80
+ | 'upsert'
81
+ | 'sync';
82
+
83
+ export type WhereValue = string | number | boolean | null | Array<string | number | boolean>;
84
+ export type WhereConditions = Record<string, WhereValue | Record<string, any>>;
85
+ export type WhereClause = WhereConditions | WhereConditions[];
86
+ export type OrderBy = string | { column: string; direction?: 'asc' | 'desc' };
87
+ export type JoinSpec =
88
+ | string
89
+ | { table: string; on?: any }
90
+ | { table: string; first: string; operator: string; second: string };
91
+ export type SumPayload = { sumColumn: string } | { sumFormula: string };
92
+
93
+ /** Optional read/shaping modifiers shared by the query-style actions. */
94
+ export interface KormQueryModifiers {
95
+ where?: WhereClause;
96
+ select?: string | string[];
97
+ orderBy?: OrderBy | OrderBy[];
98
+ limit?: number;
99
+ offset?: number;
100
+ page?: number;
101
+ with?: string[];
102
+ withWhere?: WhereClause;
103
+ groupBy?: string | string[];
104
+ having?: WhereClause;
105
+ distinct?: boolean | string | string[];
106
+ join?: JoinSpec | JoinSpec[];
107
+ leftJoin?: JoinSpec | JoinSpec[];
108
+ rightJoin?: JoinSpec | JoinSpec[];
109
+ innerJoin?: JoinSpec | JoinSpec[];
110
+ }
111
+
112
+ interface KormRequestCommon {
113
+ /** Build + return the SQL without executing it (see DryRunResult). */
114
+ dryRun?: boolean;
115
+ /** Nested calls keyed by model name. */
116
+ other_requests?: Record<string, KormRequest | KormRequest[]>;
117
+ }
118
+
119
+ export interface KormListRequest extends KormRequestCommon, KormQueryModifiers {
120
+ action?: 'list';
121
+ }
122
+ export interface KormShowRequest extends KormRequestCommon, KormQueryModifiers {
123
+ action: 'show';
124
+ }
125
+ export interface KormCountRequest extends KormRequestCommon, KormQueryModifiers {
126
+ action: 'count';
127
+ }
128
+ export interface KormSumRequest extends KormRequestCommon, KormQueryModifiers {
129
+ action: 'sum';
130
+ data: SumPayload;
131
+ }
132
+ export interface KormCreateRequest extends KormRequestCommon {
133
+ action: 'create';
134
+ data: Record<string, any> | Record<string, any>[];
135
+ }
136
+ export interface KormUpdateRequest extends KormRequestCommon {
137
+ action: 'update';
138
+ where?: WhereClause;
139
+ data: Record<string, any>;
140
+ }
141
+ export interface KormDeleteRequest extends KormRequestCommon {
142
+ action: 'delete';
143
+ where?: WhereClause;
144
+ }
145
+ export interface KormReplaceRequest extends KormRequestCommon {
146
+ action: 'replace';
147
+ data: Record<string, any> | Record<string, any>[];
148
+ }
149
+ export interface KormUpsertRequest extends KormRequestCommon {
150
+ action: 'upsert';
151
+ data: Record<string, any> | Record<string, any>[];
152
+ conflict?: string[];
153
+ }
154
+ export interface KormSyncRequest extends KormRequestCommon {
155
+ action: 'sync';
156
+ data: Record<string, any>[];
157
+ where?: WhereClause;
158
+ conflict?: string[];
159
+ }
160
+
161
+ export type KormRequest =
162
+ | KormListRequest
163
+ | KormShowRequest
164
+ | KormCountRequest
165
+ | KormSumRequest
166
+ | KormCreateRequest
167
+ | KormUpdateRequest
168
+ | KormDeleteRequest
169
+ | KormReplaceRequest
170
+ | KormUpsertRequest
171
+ | KormSyncRequest;
172
+
173
+ /** Loosest accepted input: a built-in request, or a custom-action body. */
174
+ export type KormRequestInput =
175
+ | KormRequest
176
+ | (KormRequestCommon & KormQueryModifiers & { action: string; data?: any });
177
+
178
+ // ---- Response contract (per action; see 06-request-contract.md §5) ------
179
+
180
+ export interface Pagination {
181
+ page: number;
182
+ limit: number;
183
+ offset: number;
184
+ totalPages: number;
185
+ hasNext: boolean;
186
+ hasPrev: boolean;
187
+ nextPage: number | null;
188
+ prevPage: number | null;
189
+ }
190
+ export interface ListResult<Row = Record<string, any>> {
191
+ data: Row[];
192
+ totalCount: number | null;
193
+ pagination?: Pagination;
194
+ sqlDebug?: string[];
195
+ }
196
+ export interface MutationResult<Data = any> {
197
+ message: string;
198
+ data: Data;
199
+ success: true;
200
+ }
201
+ export interface SyncResult {
202
+ message: string;
203
+ data: { insertOrUpdateQuery: any; deleteQuery: any };
204
+ success: true;
205
+ }
206
+ export type ShowResult<Row = Record<string, any>> = Row | null;
207
+
208
+ /**
209
+ * Maps a request to its result shape. `dryRun: true` overrides regardless
210
+ * of action; a custom (non-built-in) action → `any`; an `any` request body
211
+ * stays `any` (back-compat for untyped Express `req.body` callers).
212
+ */
213
+ export type KormResult<TReq> = TReq extends { dryRun: true }
214
+ ? DryRunResult
215
+ : TReq extends { action: 'show' }
216
+ ? ShowResult
217
+ : TReq extends { action: 'count' | 'sum' }
218
+ ? number
219
+ : TReq extends { action: 'sync' }
220
+ ? SyncResult
221
+ : TReq extends { action: 'create' | 'update' | 'delete' | 'replace' | 'upsert' }
222
+ ? MutationResult
223
+ : TReq extends { action: 'list' }
224
+ ? ListResult
225
+ : TReq extends { action: string }
226
+ ? any
227
+ : ListResult;
228
+
65
229
  export interface KormInstance {
66
- processRequest(requestBody: any, modelName: string, context?: any): Promise<any | DryRunResult>;
230
+ /**
231
+ * Execute a request against `modelName`. The result type narrows by the
232
+ * request's `action` literal (and `dryRun`) — see KormResult. An `any`
233
+ * body (e.g. an untyped Express `req.body`) resolves to `any`.
234
+ */
235
+ processRequest<TReq extends KormRequestInput = KormListRequest>(
236
+ requestBody: TReq,
237
+ modelName: string,
238
+ context?: any
239
+ ): Promise<KormResult<TReq>>;
67
240
  syncDatabase?(options?: any): Promise<any>;
68
241
  generateSchema?(options?: any): Promise<any>;
69
242
  /**
@@ -77,14 +250,45 @@ export interface KormInstance {
77
250
  describeSchema(): SchemaDescription;
78
251
  /**
79
252
  * Pure-data description of one model. Throws KormError (code
80
- * 'UNKNOWN_MODEL') for an unregistered model.
253
+ * 'UNKNOWN_MODEL') for an unregistered model. When `ctx` is given and
254
+ * authorize() predicates are registered, `actions` is filtered to those
255
+ * permitted for that context (issue #19).
81
256
  */
82
- describeModel(modelName: string): ModelDescription;
257
+ describeModel(modelName: string, ctx?: AuthContext): ModelDescription;
83
258
  setSchema(schema: any): void;
259
+
260
+ // ---- Authorization (issue #19) ----------------------------------------
261
+
262
+ /**
263
+ * Register a permission predicate for `(model, action)`. The predicate
264
+ * receives `(request, ctx)` and returns truthy to allow; a denied request
265
+ * throws KormError (code 'FORBIDDEN'). `action` may be `'*'` to gate every
266
+ * action on the model. Opt-in: unregistered pairs are allowed.
267
+ */
268
+ authorize(
269
+ model: string,
270
+ action: KormAction | '*' | string,
271
+ predicate: (request: KormRequestInput, ctx: AuthContext) => boolean
272
+ ): KormInstance;
273
+ /**
274
+ * Register a row-scope for `model`. `fn(request, ctx)` returns an object
275
+ * merged into `where` for reads/update/delete/count/sum and stamped into
276
+ * each inserted `data` row for create/replace/upsert/sync.
277
+ */
278
+ scope(
279
+ model: string,
280
+ fn: (request: KormRequestInput, ctx: AuthContext) => Record<string, any>
281
+ ): KormInstance;
282
+ /** Clear all registered authorize()/scope() rules (mainly for tests). */
283
+ resetAuthorization(): KormInstance;
284
+
84
285
  loadModelClass?(name: string): any;
85
286
  getModelInstance?(name: string): any;
86
287
  }
87
288
 
289
+ /** Authorization context passed as the 3rd arg to processRequest. */
290
+ export type AuthContext = Record<string, any>;
291
+
88
292
  export function initializeKORM(opts: InitializeOptions): KormInstance;
89
293
  export function validate(body: any, rules: any, opts?: any): Promise<any>;
90
294
  export const helperUtility: any;
@@ -99,6 +303,7 @@ export type KormErrorCode =
99
303
  | 'VALIDATION_FAILED'
100
304
  | 'UNKNOWN_MODEL'
101
305
  | 'NO_CUSTOM_ACTION_HOOK'
306
+ | 'FORBIDDEN'
102
307
  | 'INTERNAL';
103
308
 
104
309
  export interface KormErrorContext {
@@ -143,6 +348,12 @@ export class KormError extends Error {
143
348
  static unknownAction(opts: { action: string; model?: string; hasCustomHook?: boolean }): KormError;
144
349
  static unknownModel(opts: { model: string; available?: string[] }): KormError;
145
350
  static validationFailed(opts: { errors?: any[]; source?: string | null }): KormError;
351
+ static forbidden(opts: {
352
+ model: string;
353
+ action: string;
354
+ hint?: string | null;
355
+ context?: Record<string, unknown>;
356
+ }): KormError;
146
357
  }
147
358
 
148
359
  export const LibClasses: { Emitter: any; KormError: typeof KormError };
@@ -0,0 +1 @@
1
+ const base=require("./jest.config"),engine=process.env.KORM_COVERAGE_ENGINE;if("pg"!==engine&&"mysql"!==engine)throw new Error(`jest.config.engine.js requires KORM_COVERAGE_ENGINE=pg|mysql (got: ${engine||"unset"})`);const ENGINE_RATCHET={statements:47,branches:30,functions:60,lines:48},RATCHETS={pg:ENGINE_RATCHET,mysql:ENGINE_RATCHET};module.exports={...base,collectCoverageFrom:[`clients/${engine}/**/*.js`],coverageThreshold:{global:RATCHETS[engine]}};
package/jest.config.js CHANGED
@@ -1 +1 @@
1
- module.exports={testEnvironment:"node",testMatch:["**/test/**/*.test.js","**/test/**/*.spec.js","**/__tests__/**/*.js"],testPathIgnorePatterns:["/node_modules/"],collectCoverage:!1,coverageDirectory:"coverage",coverageReporters:["text","lcov","html"],collectCoverageFrom:["clients/sqlite/**/*.js","clients/Base*.js","ControllerWrapper.js","RequestValidator.js","BaseHelperUtility.js","Logger.js","Emitter.js","index.js","cli.js","helpers/**/*.js","src/mcp/**/*.js","bin/korm-mcp.js","!**/node_modules/**","!**/test/**","!**/coverage/**","!**/dist/**"],coverageThreshold:{global:{statements:60,branches:50,functions:60,lines:61}},testTimeout:1e4,moduleNameMapper:{"^@modelcontextprotocol/sdk/(.*)$":"<rootDir>/node_modules/@modelcontextprotocol/sdk/dist/cjs/$1"},clearMocks:!0,verbose:!0};
1
+ module.exports={testEnvironment:"node",testMatch:["**/test/**/*.test.js","**/test/**/*.spec.js","**/__tests__/**/*.js"],testPathIgnorePatterns:["/node_modules/"],collectCoverage:!1,coverageDirectory:"coverage",coverageReporters:["text","lcov","html"],collectCoverageFrom:["clients/sqlite/**/*.js","clients/Base*.js","ControllerWrapper.js","AuthorizationService.js","RequestValidator.js","BaseHelperUtility.js","Logger.js","Emitter.js","index.js","cli.js","helpers/**/*.js","src/mcp/**/*.js","bin/korm-mcp.js","!**/node_modules/**","!**/test/**","!**/coverage/**","!**/dist/**"],coverageThreshold:{global:{statements:90,branches:85,functions:90,lines:90},"clients/sqlite/CurdTable.js":{statements:95,branches:90},"RequestValidator.js":{statements:95,branches:88},"clients/sqlite/QueryBuilder.js":{statements:93,branches:83}},testTimeout:1e4,moduleNameMapper:{"^@modelcontextprotocol/sdk/(.*)$":"<rootDir>/node_modules/@modelcontextprotocol/sdk/dist/cjs/$1"},clearMocks:!0,verbose:!0};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dreamtree-org/korm-js",
3
- "version": "1.0.55",
3
+ "version": "1.0.56",
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",
@@ -18,6 +18,7 @@
18
18
  "test:watch": "jest --watch",
19
19
  "test:coverage": "jest --coverage",
20
20
  "test:all": "jest --coverage --verbose",
21
+ "test:types": "tsc -p tsconfig.types.json",
21
22
  "lint": "eslint . --max-warnings=0",
22
23
  "lint:fix": "eslint . --fix",
23
24
  "format": "prettier --write .",