@dreamtree-org/korm-js 1.0.53 → 1.0.55

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.
Files changed (44) hide show
  1. package/BaseHelperUtility.js +1 -1
  2. package/ControllerWrapper.js +1 -1
  3. package/Emitter.js +1 -1
  4. package/KormError.js +1 -0
  5. package/README.md +463 -254
  6. package/RequestValidator.js +1 -1
  7. package/ai-skills/korm-js.md +265 -0
  8. package/bin/korm-mcp.js +2 -0
  9. package/build.js +1 -1
  10. package/cli.js +2 -0
  11. package/clients/BaseSyncTable.js +1 -0
  12. package/clients/mysql/BaseUtility.js +1 -1
  13. package/clients/mysql/CurdTable.js +1 -1
  14. package/clients/mysql/DataTypeMap.js +1 -1
  15. package/clients/mysql/HookService.js +1 -1
  16. package/clients/mysql/QueryBuilder.js +1 -1
  17. package/clients/mysql/QueryService.js +1 -1
  18. package/clients/mysql/SyncTable.js +1 -1
  19. package/clients/pg/BaseUtility.js +1 -1
  20. package/clients/pg/CurdTable.js +1 -1
  21. package/clients/pg/DataTypeMap.js +1 -1
  22. package/clients/pg/HookService.js +1 -1
  23. package/clients/pg/QueryBuilder.js +1 -1
  24. package/clients/pg/QueryService.js +1 -1
  25. package/clients/pg/SyncTable.js +1 -1
  26. package/clients/sqlite/BaseUtility.js +1 -1
  27. package/clients/sqlite/CurdTable.js +1 -1
  28. package/clients/sqlite/HookService.js +1 -1
  29. package/clients/sqlite/QueryBuilder.js +1 -1
  30. package/clients/sqlite/QueryService.js +1 -1
  31. package/clients/sqlite/SyncTable.js +1 -1
  32. package/columnSchema.js +1 -0
  33. package/helpers/files.js +1 -1
  34. package/index.d.ts +213 -0
  35. package/index.js +1 -1
  36. package/jest.config.js +1 -1
  37. package/package.json +13 -4
  38. package/requestSchema.js +1 -0
  39. package/schemaDescribe.js +1 -0
  40. package/src/mcp/errors.js +1 -0
  41. package/src/mcp/schemaIntrospect.js +1 -0
  42. package/src/mcp/server.js +1 -0
  43. package/src/mcp/toolGenerator.js +1 -0
  44. package/TableSchemaSync.js +0 -1
@@ -1 +1 @@
1
- const{processRequest:processRequest}=require("./ControllerWrapper"),HelperUtility=require("./BaseHelperUtility");class ValidationError extends Error{constructor(e,t,a,r){super(e),this.name="ValidationError",this.field=t,this.value=a,this.rule=r,this.timestamp=(new Date).toISOString()}}class RequestValidator{constructor(){this.rules=new Map,this.customMessages=new Map,this.transformers=new Map,this.customRegex=new Map,this.customCallbacks=new Map,this.helperUtility=new HelperUtility}rule(e,t,a=null){return this.rules.has(e)||this.rules.set(e,[]),this.rules.get(e).push(t),a&&this.customMessages.set(`${e}.${t.type}`,a),this}string(e,t=null){return this.rule(e,{type:"string",validator:e=>"string"==typeof e},t)}number(e,t=null){return this.rule(e,{type:"number",validator:e=>"number"==typeof e&&!isNaN(e)},t)}boolean(e,t=null){return this.rule(e,{type:"boolean",validator:e=>"boolean"==typeof e},t)}required(e,t=null){return this.rule(e,{type:"required",validator:e=>null!=e&&""!==e},t)}email(e,t=null){const a=/^[^\s@]+@[^\s@]+\.[^\s@]+$/;return this.rule(e,{type:"email",validator:e=>a.test(e)},t)}url(e,t=null){return this.rule(e,{type:"url",validator:e=>{try{return new URL(e),!0}catch{return!1}}},t)}minLength(e,t,a=null){return this.rule(e,{type:"minLength",validator:e=>String(e).length>=t,params:{min:t}},a)}maxLength(e,t,a=null){return this.rule(e,{type:"maxLength",validator:e=>String(e).length<=t,params:{max:t}},a)}min(e,t,a=null){return this.rule(e,{type:"min",validator:e=>Number(e)>=t,params:{min:t}},a)}max(e,t,a=null){return this.rule(e,{type:"max",validator:e=>Number(e)<=t,params:{max:t}},a)}enum(e,t,a=null){return this.rule(e,{type:"enum",validator:e=>t.includes(e),params:{allowedValues:t}},a)}regex(e,t,a=null){return this.rule(e,{type:"regex",validator:e=>t.test(e),params:{pattern:t}},a)}uuid(e,t=null){const a=/^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;return this.rule(e,{type:"uuid",validator:e=>a.test(e)},t)}date(e,t=null){return this.rule(e,{type:"date",validator:e=>!isNaN(Date.parse(e))},t)}array(e,t=null){return this.rule(e,{type:"array",validator:e=>Array.isArray(e)},t)}object(e,t=null){return this.rule(e,{type:"object",validator:e=>"object"==typeof e&&null!==e&&!Array.isArray(e)},t)}custom(e,t,a=null){return this.rule(e,{type:"custom",validator:t},a)}transform(e,t){return this.transformers.set(e,t),this}message(e,t,a){return this.customMessages.set(`${e}.${t}`,a),this}getDefaultMessage(e,t,a,r={}){return{required:`${e} is required`,string:`${e} must be a string`,number:`${e} must be a number`,boolean:`${e} must be a boolean`,email:`${e} must be a valid email address`,url:`${e} must be a valid URL`,minLength:`${e} must be at least ${r.min} characters long`,maxLength:`${e} must be at most ${r.max} characters long`,min:`${e} must be at least ${r.min}`,max:`${e} must be at most ${r.max}`,enum:`${e} must be one of: ${r.allowedValues?.join(", ")}`,regex:`${e} format is invalid`,uuid:`${e} must be a valid UUID`,date:`${e} must be a valid date`,array:`${e} must be an array`,object:`${e} must be an object`,custom:`${e} validation failed`}[t]||`${e} validation failed`}validateField(e,t){const a=this.rules.get(e)||[],r=[];let s=t;this.transformers.has(e)&&(s=this.transformers.get(e)(t));for(const t of a)try{if(!t.validator(s)){const a=this.customMessages.get(`${e}.${t.type}`)||this.getDefaultMessage(e,t.type,s,t.params);r.push(new ValidationError(a,e,s,t.type))}}catch(a){const i=this.customMessages.get(`${e}.${t.type}`)||this.getDefaultMessage(e,t.type,s,t.params);r.push(new ValidationError(i,e,s,t.type))}return{value:s,errors:r}}validate(e,t={}){const{source:a="body"}=t,r=[],s={};for(const[t,a]of this.rules){const a=e[t],{value:i,errors:n}=this.validateField(t,a);n.length>0?r.push(...n):s[t]=i}if(r.length>0){const e=new Error(`Validation failed for ${a}`);throw e.name="ValidationError",e.errors=r,e.source=a,e}return s}validateParams(e){return this.validate(e,{source:"params"})}validateBody(e){return this.validate(e,{source:"body"})}validateQuery(e){return this.validate(e,{source:"query"})}validateRequest(e){const t={params:{},body:{},query:{}};try{e.params&&(t.params=this.validateParams(e.params))}catch(e){t.params={error:e}}try{e.body&&(t.body=this.validateBody(e.body))}catch(e){t.body={error:e}}try{e.query&&(t.query=this.validateQuery(e.query))}catch(e){t.query={error:e}}return t}static create(){return new RequestValidator}static schema(e){const t=new RequestValidator;for(const[a,r]of Object.entries(e))Array.isArray(r)?r.forEach(e=>{"string"==typeof e?t[e](a):"object"==typeof e&&t.rule(a,e)}):"string"==typeof r?t[r](a):"object"==typeof r&&t.rule(a,r);return t}parseRuleString(e){const t=[],a=e.split("|");for(const e of a){const a=e.trim();if(a)if("required"===a)t.push({type:"required"});else if(a.startsWith("type:")){const e=a.substring(5).replace(/[()]/g,"").split(",");t.push({type:"type",params:e})}else if(a.startsWith("maxLen:")){const e=parseInt(a.substring(7));isNaN(e)||t.push({type:"maxLength",params:{max:e}})}else if(a.startsWith("minLen:")){const e=parseInt(a.substring(7));isNaN(e)||t.push({type:"minLength",params:{min:e}})}else if(a.startsWith("max:")){const e=parseInt(a.substring(4));isNaN(e)||t.push({type:"max",params:{max:e}})}else if(a.startsWith("min:")){const e=parseInt(a.substring(4));isNaN(e)||t.push({type:"min",params:{min:e}})}else if(a.startsWith("in:")){const e=a.substring(3).split(",");t.push({type:"in",params:{values:e}})}else if(a.startsWith("exists:")){const[e,r]=a.substring(7).split(",");e&&r&&t.push({type:"exists",params:{table:e,field:r}})}else if(a.startsWith("regex:")){const e=a.substring(6).replace(/[{}]/g,"");e&&t.push({type:"regex",params:{regexName:e}})}else if(a.startsWith("default:")){const e=a.substring(8);void 0!==e&&t.push({type:"default",params:{value:e}})}else if(a.startsWith("call:")){const e=a.substring(5).replace(/[{}]/g,"");e&&t.push({type:"call",params:{callbackName:e}})}}return t}addRegex(e,t){return this.customRegex.set(e,new RegExp(t)),this}addCallback(e,t){return this.customCallbacks.set(e,t),this}async validateWithRules(e,t,a={}){const{customRegex:r={},customCallbacks:s={}}=a;for(const[e,t]of Object.entries(r))this.addRegex(e,t);for(const[e,t]of Object.entries(s))this.addCallback(e,t);const i=[],n={};for(const[a,r]of Object.entries(t)){const t=this.parseRuleString(r),s=t.find(e=>"default"===e.type);let l=e[a];(a.includes(".")||a.includes("[]"))&&(l=this.helperUtility.dotParse(a,e));let u=l;!s||null!=l&&""!==l||(u=s.params.value);let o=!0;if(t.some(e=>"required"===e.type)||null!=u&&""!==u){for(const e of t){if("default"===e.type)continue;const t=await this.validateRule(a,u,e);if(!t.isValid){i.push(t.error),o=!1;break}}o&&(n[a]=u)}}if(i.length>0){const e=new Error("Validation failed"),t={name:"ValidationError",errors:i,details:i.map(e=>({field:e.field,message:e.message,value:e.value,rule:e.rule}))};throw e.message=t,e}return n}async validateRule(e,t,a){try{let r=!0,s="";switch(a.type){case"required":r=null!=t&&""!==t,s=r?"":`${e} is required`;break;case"type":const i=a.params;r=i.some(e=>{switch(e){case"string":return"string"==typeof t;case"number":return"number"==typeof t&&!isNaN(t);case"boolean":return"boolean"==typeof t;case"array":return Array.isArray(t);case"object":return"object"==typeof t&&null!==t&&!Array.isArray(t);case"longText":return"string"==typeof t&&t.length>255;default:return!1}}),s=r?"":`${e} must be one of: ${i.join(", ")}`;break;case"maxLength":"string"==typeof t?(r=String(t).length<=a.params.max,s=r?"":`${e} must be at most ${a.params.max} characters long`):Array.isArray(t)&&(r=t.length<=a.params.max,s=r?"":`${e} must be at most ${a.params.max} items`);break;case"minLength":"string"==typeof t?(r=String(t).length>=a.params.min,s=r?"":`${e} must be at least ${a.params.min} characters long`):Array.isArray(t)&&(r=t.length>=a.params.min,s=r?"":`${e} must be at least ${a.params.min} items`);break;case"max":r=Number(t)<=a.params.max,s=r?"":`${e} must be at most ${a.params.max}`;break;case"min":r=Number(t)>=a.params.min,s=r?"":`${e} must be at least ${a.params.min}`;break;case"in":try{if(a.params.values){const i=a.params.values;r=i.includes(t),s=r?"":`${e} must be one of: ${i.join(", ")}`}else{const{table:i,field:n}=a.params,l=await processRequest({model:i,action:"get",request:{where:{[n]:t},limit:1}});r=l&&l.data&&l.data.length>0,s=r?"":`${e} must be a valid value from ${i}`}}catch(t){r=!1,s=`${e} database validation error: ${t.message}`}break;case"exists":try{const{table:i,field:n}=a.params,l=await processRequest({model:i,action:"get",request:{where:{[n]:t},limit:1}});r=l&&l.data&&l.data.length>0,s=r?"":`${e} must exist in ${i}`}catch(t){r=!1,s=`${e} database validation error: ${t.message}`}break;case"regex":const n=a.params.regexName,l=this.customRegex.get(n);l?(r=l.test(t),s=r?"":`${e} format is invalid`):(r=!1,s=`${e} regex pattern '${n}' not found`);break;case"call":const u=a.params.callbackName,o=this.customCallbacks.get(u);if(o&&"function"==typeof o)try{r=o(t),s=r?"":`${e} validation failed`}catch(t){r=!1,s=`${e} validation error: ${t.message}`}else r=!1,s=`${e} callback function '${u}' not found`;break;default:r=!1,s=`${e} unknown validation rule: ${a.type}`}return{isValid:r,error:r?null:new ValidationError(s,e,t,a.type)}}catch(r){return{isValid:!1,error:new ValidationError(`${e} validation failed`,e,t,a.type)}}}}async function validate(e,t,a={}){const r=new RequestValidator;let s={};return!e||"object"!=typeof e||e.body||e.params||e.query?(e.body&&(s={...s,...e.body}),e.params&&(s={...s,...e.params}),e.query&&(s={...s,...e.query})):s=e,await r.validateWithRules(s,t,a)}function validateEmail(e){return/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(e)}function validatePassword(e){return/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)[a-zA-Z\d@$!%*?&]{8,}$/.test(e)}function validatePhone(e){return/^\+?[\d\s-()]{10,15}$/.test(e)}function validatePAN(e){return/^[A-Z]{5}[0-9]{4}[A-Z]{1}$/.test(e)}function validateAadhaar(e){return/^\d{12}$/.test(e)&&!/^0{12}$/.test(e)}function createValidationMiddleware(e,t={}){return async(a,r,s)=>{try{const i=await validate(a,e,t);if(!1===i.success)return r.status(400).json(i);a.validated=i,s()}catch(e){return r.status(500).json({success:!1,reason:"Validation middleware error",error:e.message})}}}module.exports={RequestValidator:RequestValidator,validate:validate,createValidationMiddleware:createValidationMiddleware,validateEmail:validateEmail,validatePassword:validatePassword,validatePhone:validatePhone,validatePAN:validatePAN,validateAadhaar:validateAadhaar};
1
+ const{processRequest:processRequest}=require("./ControllerWrapper"),HelperUtility=require("./BaseHelperUtility"),KormError=require("./KormError");class ValidationError extends Error{constructor(e,t,a,r){super(e),this.name="ValidationError",this.field=t,this.value=a,this.rule=r,this.timestamp=(new Date).toISOString()}}class RequestValidator{constructor(){this.rules=new Map,this.customMessages=new Map,this.transformers=new Map,this.customRegex=new Map,this.customCallbacks=new Map,this.helperUtility=new HelperUtility}rule(e,t,a=null){return this.rules.has(e)||this.rules.set(e,[]),this.rules.get(e).push(t),a&&this.customMessages.set(`${e}.${t.type}`,a),this}string(e,t=null){return this.rule(e,{type:"string",validator:e=>"string"==typeof e},t)}number(e,t=null){return this.rule(e,{type:"number",validator:e=>"number"==typeof e&&!isNaN(e)},t)}boolean(e,t=null){return this.rule(e,{type:"boolean",validator:e=>"boolean"==typeof e},t)}required(e,t=null){return this.rule(e,{type:"required",validator:e=>null!=e&&""!==e},t)}email(e,t=null){const a=/^[^\s@]+@[^\s@]+\.[^\s@]+$/;return this.rule(e,{type:"email",validator:e=>a.test(e)},t)}url(e,t=null){return this.rule(e,{type:"url",validator:e=>{try{return new URL(e),!0}catch{return!1}}},t)}minLength(e,t,a=null){return this.rule(e,{type:"minLength",validator:e=>String(e).length>=t,params:{min:t}},a)}maxLength(e,t,a=null){return this.rule(e,{type:"maxLength",validator:e=>String(e).length<=t,params:{max:t}},a)}min(e,t,a=null){return this.rule(e,{type:"min",validator:e=>Number(e)>=t,params:{min:t}},a)}max(e,t,a=null){return this.rule(e,{type:"max",validator:e=>Number(e)<=t,params:{max:t}},a)}enum(e,t,a=null){return this.rule(e,{type:"enum",validator:e=>t.includes(e),params:{allowedValues:t}},a)}regex(e,t,a=null){return this.rule(e,{type:"regex",validator:e=>t.test(e),params:{pattern:t}},a)}uuid(e,t=null){const a=/^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;return this.rule(e,{type:"uuid",validator:e=>a.test(e)},t)}date(e,t=null){return this.rule(e,{type:"date",validator:e=>!isNaN(Date.parse(e))},t)}array(e,t=null){return this.rule(e,{type:"array",validator:e=>Array.isArray(e)},t)}object(e,t=null){return this.rule(e,{type:"object",validator:e=>"object"==typeof e&&null!==e&&!Array.isArray(e)},t)}custom(e,t,a=null){return this.rule(e,{type:"custom",validator:t},a)}transform(e,t){return this.transformers.set(e,t),this}message(e,t,a){return this.customMessages.set(`${e}.${t}`,a),this}getDefaultMessage(e,t,a,r={}){return{required:`${e} is required`,string:`${e} must be a string`,number:`${e} must be a number`,boolean:`${e} must be a boolean`,email:`${e} must be a valid email address`,url:`${e} must be a valid URL`,minLength:`${e} must be at least ${r.min} characters long`,maxLength:`${e} must be at most ${r.max} characters long`,min:`${e} must be at least ${r.min}`,max:`${e} must be at most ${r.max}`,enum:`${e} must be one of: ${r.allowedValues?.join(", ")}`,regex:`${e} format is invalid`,uuid:`${e} must be a valid UUID`,date:`${e} must be a valid date`,array:`${e} must be an array`,object:`${e} must be an object`,custom:`${e} validation failed`}[t]||`${e} validation failed`}validateField(e,t){const a=this.rules.get(e)||[],r=[];let s=t;this.transformers.has(e)&&(s=this.transformers.get(e)(t));for(const t of a)try{if(!t.validator(s)){const a=this.customMessages.get(`${e}.${t.type}`)||this.getDefaultMessage(e,t.type,s,t.params);r.push(new ValidationError(a,e,s,t.type))}}catch(a){const i=this.customMessages.get(`${e}.${t.type}`)||this.getDefaultMessage(e,t.type,s,t.params);r.push(new ValidationError(i,e,s,t.type))}return{value:s,errors:r}}validate(e,t={}){const{source:a="body"}=t,r=[],s={};for(const[t,a]of this.rules){const a=e[t],{value:i,errors:n}=this.validateField(t,a);n.length>0?r.push(...n):s[t]=i}if(r.length>0)throw KormError.validationFailed({errors:r,source:a});return s}validateParams(e){return this.validate(e,{source:"params"})}validateBody(e){return this.validate(e,{source:"body"})}validateQuery(e){return this.validate(e,{source:"query"})}validateRequest(e){const t={params:{},body:{},query:{}};try{e.params&&(t.params=this.validateParams(e.params))}catch(e){t.params={error:e}}try{e.body&&(t.body=this.validateBody(e.body))}catch(e){t.body={error:e}}try{e.query&&(t.query=this.validateQuery(e.query))}catch(e){t.query={error:e}}return t}static create(){return new RequestValidator}static schema(e){const t=new RequestValidator;for(const[a,r]of Object.entries(e))Array.isArray(r)?r.forEach(e=>{"string"==typeof e?t[e](a):"object"==typeof e&&t.rule(a,e)}):"string"==typeof r?t[r](a):"object"==typeof r&&t.rule(a,r);return t}parseRuleString(e){const t=[],a=e.split("|");for(const e of a){const a=e.trim();if(a)if("required"===a)t.push({type:"required"});else if(a.startsWith("type:")){const e=a.substring(5).replace(/[()]/g,"").split(",");t.push({type:"type",params:e})}else if(a.startsWith("maxLen:")){const e=parseInt(a.substring(7));isNaN(e)||t.push({type:"maxLength",params:{max:e}})}else if(a.startsWith("minLen:")){const e=parseInt(a.substring(7));isNaN(e)||t.push({type:"minLength",params:{min:e}})}else if(a.startsWith("max:")){const e=parseInt(a.substring(4));isNaN(e)||t.push({type:"max",params:{max:e}})}else if(a.startsWith("min:")){const e=parseInt(a.substring(4));isNaN(e)||t.push({type:"min",params:{min:e}})}else if(a.startsWith("in:")){const e=a.substring(3).split(",");t.push({type:"in",params:{values:e}})}else if(a.startsWith("exists:")){const[e,r]=a.substring(7).split(",");e&&r&&t.push({type:"exists",params:{table:e,field:r}})}else if(a.startsWith("regex:")){const e=a.substring(6).replace(/[{}]/g,"");e&&t.push({type:"regex",params:{regexName:e}})}else if(a.startsWith("default:")){const e=a.substring(8);void 0!==e&&t.push({type:"default",params:{value:e}})}else if(a.startsWith("call:")){const e=a.substring(5).replace(/[{}]/g,"");e&&t.push({type:"call",params:{callbackName:e}})}}return t}addRegex(e,t){return this.customRegex.set(e,new RegExp(t)),this}addCallback(e,t){return this.customCallbacks.set(e,t),this}async validateWithRules(e,t,a={}){const{customRegex:r={},customCallbacks:s={}}=a;for(const[e,t]of Object.entries(r))this.addRegex(e,t);for(const[e,t]of Object.entries(s))this.addCallback(e,t);const i=[],n={};for(const[a,r]of Object.entries(t)){const t=this.parseRuleString(r),s=t.find(e=>"default"===e.type);let l=e[a];(a.includes(".")||a.includes("[]"))&&(l=this.helperUtility.dotParse(a,e));let u=l;!s||null!=l&&""!==l||(u=s.params.value);let o=!0;if(t.some(e=>"required"===e.type)||null!=u&&""!==u){for(const e of t){if("default"===e.type)continue;const t=await this.validateRule(a,u,e);if(!t.isValid){i.push(t.error),o=!1;break}}o&&(n[a]=u)}}if(i.length>0)throw KormError.validationFailed({errors:i});return n}async validateRule(e,t,a){try{let r=!0,s="";switch(a.type){case"required":r=null!=t&&""!==t,s=r?"":`${e} is required`;break;case"type":const i=a.params;r=i.some(e=>{switch(e){case"string":return"string"==typeof t;case"number":return"number"==typeof t&&!isNaN(t);case"boolean":return"boolean"==typeof t;case"array":return Array.isArray(t);case"object":return"object"==typeof t&&null!==t&&!Array.isArray(t);case"longText":return"string"==typeof t&&t.length>255;default:return!1}}),s=r?"":`${e} must be one of: ${i.join(", ")}`;break;case"maxLength":"string"==typeof t?(r=String(t).length<=a.params.max,s=r?"":`${e} must be at most ${a.params.max} characters long`):Array.isArray(t)&&(r=t.length<=a.params.max,s=r?"":`${e} must be at most ${a.params.max} items`);break;case"minLength":"string"==typeof t?(r=String(t).length>=a.params.min,s=r?"":`${e} must be at least ${a.params.min} characters long`):Array.isArray(t)&&(r=t.length>=a.params.min,s=r?"":`${e} must be at least ${a.params.min} items`);break;case"max":r=Number(t)<=a.params.max,s=r?"":`${e} must be at most ${a.params.max}`;break;case"min":r=Number(t)>=a.params.min,s=r?"":`${e} must be at least ${a.params.min}`;break;case"in":try{if(a.params.values){const i=a.params.values;r=i.includes(t),s=r?"":`${e} must be one of: ${i.join(", ")}`}else{const{table:i,field:n}=a.params,l=await processRequest({model:i,action:"get",request:{where:{[n]:t},limit:1}});r=l&&l.data&&l.data.length>0,s=r?"":`${e} must be a valid value from ${i}`}}catch(t){r=!1,s=`${e} database validation error: ${t.message}`}break;case"exists":try{const{table:i,field:n}=a.params,l=await processRequest({model:i,action:"get",request:{where:{[n]:t},limit:1}});r=l&&l.data&&l.data.length>0,s=r?"":`${e} must exist in ${i}`}catch(t){r=!1,s=`${e} database validation error: ${t.message}`}break;case"regex":const n=a.params.regexName,l=this.customRegex.get(n);l?(r=l.test(t),s=r?"":`${e} format is invalid`):(r=!1,s=`${e} regex pattern '${n}' not found`);break;case"call":const u=a.params.callbackName,o=this.customCallbacks.get(u);if(o&&"function"==typeof o)try{r=o(t),s=r?"":`${e} validation failed`}catch(t){r=!1,s=`${e} validation error: ${t.message}`}else r=!1,s=`${e} callback function '${u}' not found`;break;default:r=!1,s=`${e} unknown validation rule: ${a.type}`}return{isValid:r,error:r?null:new ValidationError(s,e,t,a.type)}}catch(r){return{isValid:!1,error:new ValidationError(`${e} validation failed`,e,t,a.type)}}}}async function validate(e,t,a={}){const r=new RequestValidator;let s={};return!e||"object"!=typeof e||e.body||e.params||e.query?(e.body&&(s={...s,...e.body}),e.params&&(s={...s,...e.params}),e.query&&(s={...s,...e.query})):s=e,await r.validateWithRules(s,t,a)}function validateEmail(e){return/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(e)}function validatePassword(e){return/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)[a-zA-Z\d@$!%*?&]{8,}$/.test(e)}function validatePhone(e){return/^\+?[\d\s-()]{10,15}$/.test(e)}function validatePAN(e){return/^[A-Z]{5}[0-9]{4}[A-Z]{1}$/.test(e)}function validateAadhaar(e){return/^\d{12}$/.test(e)&&!/^0{12}$/.test(e)}function createValidationMiddleware(e,t={}){return async(a,r,s)=>{try{const i=await validate(a,e,t);if(!1===i.success)return r.status(400).json(i);a.validated=i,s()}catch(e){return r.status(500).json({success:!1,reason:"Validation middleware error",error:e.message})}}}module.exports={RequestValidator:RequestValidator,validate:validate,createValidationMiddleware:createValidationMiddleware,validateEmail:validateEmail,validatePassword:validatePassword,validatePhone:validatePhone,validatePAN:validatePAN,validateAadhaar:validateAadhaar};
@@ -0,0 +1,265 @@
1
+ # KORM-JS — AI assistant reference
2
+
3
+ > Skill installed by `npx @dreamtree-org/korm-js init --ai <provider>`.
4
+ > Source of truth: [`@dreamtree-org/korm-js`](https://www.npmjs.com/package/@dreamtree-org/korm-js).
5
+ > Re-run the installer to refresh this block when the library updates.
6
+
7
+ ## What KORM-JS is
8
+
9
+ `@dreamtree-org/korm-js` is a **JSON-contract ORM** built on top of Knex. The consumer sends a single JSON request describing the operation; KORM translates it into safe, parameterized SQL across **MySQL, PostgreSQL, and SQLite**. Models and their relations are declared once; CRUD is never hand-written.
10
+
11
+ When helping the user, **always express data access as a KORM request object**, not as raw Knex calls or string SQL.
12
+
13
+ ## Wiring (do not invent alternatives)
14
+
15
+ ```js
16
+ const { initializeKORM } = require('@dreamtree-org/korm-js');
17
+ const knex = require('knex');
18
+
19
+ const db = knex({
20
+ client: 'mysql2', // 'mysql2' | 'pg' | 'sqlite3'
21
+ connection: {
22
+ /* ... */
23
+ },
24
+ });
25
+
26
+ const korm = initializeKORM({
27
+ db,
28
+ dbClient: 'mysql', // 'mysql' | 'pg' | 'sqlite'
29
+ debug: false,
30
+ });
31
+
32
+ const result = await korm.processRequest(requestObject, 'ModelName');
33
+ ```
34
+
35
+ In Express/Next/Fastify the consumer just forwards `req.body` and the model name. KORM does **not** own routing — never suggest an HTTP framework as part of KORM itself.
36
+
37
+ ## Request contract
38
+
39
+ `processRequest(request, modelName)` accepts a JSON object with these top-level fields:
40
+
41
+ | Field | Type | Purpose |
42
+ | ----------------------------------------------- | -------------------------- | -------------------------------------------------------------------------------------------------------- |
43
+ | `action` | string (required) | The operation: `list`, `show`, `create`, `update`, `delete`, `count`, `sum`, `replace`, `upsert`, `sync` |
44
+ | `where` | object \| array | Filter conditions for `list`/`show`/`update`/`delete`/`count`/`sum` |
45
+ | `data` | object \| array | Payload for `create`/`update`/`upsert`/`replace`/`sync` |
46
+ | `select` | array \| string | Columns to return (default: all) |
47
+ | `with` | array of strings | Relations to eager-load (dot-nested allowed: `"Post.Comment"`) |
48
+ | `withWhere` | object | Filters scoped to related rows only — does NOT filter parents |
49
+ | `orderBy` | object \| array \| string | `{column, direction}` / `"column"` / array of either |
50
+ | `limit` | number | Max rows |
51
+ | `offset` / `page` | number | Pagination |
52
+ | `groupBy` | array \| string | GROUP BY columns |
53
+ | `having` | object | Post-group filter |
54
+ | `distinct` | boolean \| array \| string | DISTINCT / DISTINCT ON |
55
+ | `join` / `innerJoin` / `leftJoin` / `rightJoin` | object \| array | Explicit joins (rarely needed — prefer `with`) |
56
+ | `conflict` | array | Conflict columns for `upsert` / `sync` |
57
+ | `other_requests` | object | Nested requests on related models; results returned under `other_responses` |
58
+ | `dryRun` | boolean | If `true`, return the SQL that would run without executing it (see "Inspecting queries" below) |
59
+
60
+ ### Actions
61
+
62
+ | Action | Behavior |
63
+ | --------- | -------------------------------------------------------------------- |
64
+ | `list` | Multi-row read with where/order/limit/offset |
65
+ | `show` | Single-row read |
66
+ | `create` | Insert from `data` (object = 1 row, array = bulk) |
67
+ | `update` | Update rows matching `where` with `data` |
68
+ | `delete` | Delete (soft if the model declares soft-delete; otherwise hard) |
69
+ | `count` | COUNT(\*) of matching rows |
70
+ | `sum` | Sum a column or formula; needs `data.sumColumn` or `data.sumFormula` |
71
+ | `replace` | MySQL-only full row replace (requires PK in `data`) |
72
+ | `upsert` | Insert-or-update keyed by `conflict` columns |
73
+ | `sync` | Upsert matching `data` + delete non-matching within `where` scope |
74
+
75
+ ### `where` operator cheat-sheet
76
+
77
+ Operators are **encoded as string prefixes on the value** (not separate keys):
78
+
79
+ | Operator | Value form | Example | SQL |
80
+ | ------------------- | --------------------------- | --------------------------- | --------------------- |
81
+ | Equals (default) | bare value | `{status: "active"}` | `= ?` |
82
+ | `>=` | `">=N"` | `{age: ">=18"}` | `>= ?` |
83
+ | `<=` | `"<=N"` | `{age: "<=65"}` | `<= ?` |
84
+ | `>` | `">N"` | `{price: ">100"}` | `> ?` |
85
+ | `<` | `"<N"` | `{price: "<500"}` | `< ?` |
86
+ | `!=` | `"!V"` | `{status: "!deleted"}` | `!= ?` |
87
+ | LIKE | `"%V%"` (or `"V%"`, `"%V"`) | `{name: "%john%"}` | `LIKE ?` |
88
+ | IN | `"[]a,b,c"` | `{role: "[]admin,user"}` | `IN (?, ?, ?)` |
89
+ | NOT IN | `"![]a,b"` | `{role: "![]banned"}` | `NOT IN (...)` |
90
+ | BETWEEN | `"><min,max"` | `{age: "><18,65"}` | `BETWEEN ? AND ?` |
91
+ | NOT BETWEEN | `"<>min,max"` | `{score: "<>0,50"}` | `NOT BETWEEN ? AND ?` |
92
+ | IS NULL | `null` | `{deleted_at: null}` | `IS NULL` |
93
+ | OR group | key prefix `"Or:"` | `{"Or:first_name": "John"}` | `OR (...)` |
94
+ | NOT EXISTS relation | `"!RelName": true` | `{"User.!UserRole": true}` | `NOT EXISTS (...)` |
95
+
96
+ Rules:
97
+
98
+ - Non-`Or:`-prefixed keys are ANDed together.
99
+ - Array form `where: [ {a: 1}, {b: 2} ]` is equivalent to object form for ANDs but lets you repeat the same column.
100
+ - `sumFormula` uses `{columnName}` placeholders and accepts only `+ - * / ( )` and decimal literals — **never interpolate user input**.
101
+ - All values flow through Knex bindings. **Do not hand-build SQL strings.**
102
+
103
+ ### Relations (`with`)
104
+
105
+ Relation metadata lives on the model definition (`hasRelations`). The consumer just names them:
106
+
107
+ ```js
108
+ {
109
+ action: "list",
110
+ where: { id: 1 },
111
+ with: ["UserDetail", "Post", "Post.Comment"],
112
+ withWhere: { "Post.status": "published" }
113
+ }
114
+ ```
115
+
116
+ `withWhere` filters child rows but does **not** drop parent rows that have no matching children. To drop parents, filter on the relation in the top-level `where` (e.g. `{"Post.status": "published"}`).
117
+
118
+ Supported relation `type` values when defining a model: `"one"` (belongs-to / one-to-one) and `"many"` (one-to-many or many-to-many via `through`).
119
+
120
+ ### Inspecting queries (`dryRun`)
121
+
122
+ Add `dryRun: true` to any request to get back the SQL it **would** run,
123
+ without executing it. Validation still runs; the database is untouched.
124
+
125
+ ```js
126
+ await korm.processRequest(
127
+ { action: 'delete', where: { status: 'archived' }, dryRun: true },
128
+ 'Post'
129
+ );
130
+ // → { success: true, dryRun: true, action: 'delete', model: 'Post',
131
+ // sql: 'delete from `posts` where `status` = ?', bindings: ['archived'],
132
+ // statements: [{ sql, bindings }] } // `sync` returns 2 statements
133
+ ```
134
+
135
+ Bindings come back as a separate array (never interpolated into `sql`).
136
+
137
+ ### Errors (`KormError`)
138
+
139
+ `processRequest` throws a `KormError` (extends `Error`, so `e.message`
140
+ still works) with a machine-readable `code` you can branch on:
141
+
142
+ | `code` | When |
143
+ | ----------------------- | ---------------------------------------------------- |
144
+ | `NO_MATCHING_ROW` | A mutating action matched no row |
145
+ | `UNKNOWN_ACTION` | Action isn't built-in and has no custom hook |
146
+ | `NO_CUSTOM_ACTION_HOOK` | Custom action requested, no hook on the model |
147
+ | `VALIDATION_FAILED` | Input failed validation (`e.context.fields`) |
148
+ | `UNKNOWN_MODEL` | Model name not in the schema (`e.context.available`) |
149
+ | `INTERNAL` | Internal invariant / misconfiguration |
150
+
151
+ ```js
152
+ const { KormError } = require('@dreamtree-org/korm-js');
153
+ try {
154
+ await korm.processRequest({ action: 'updaet' }, 'User');
155
+ } catch (e) {
156
+ if (e instanceof KormError && e.code === 'UNKNOWN_ACTION') {
157
+ // e.context.closest → "update" (typo suggestion); e.toJSON() for HTTP
158
+ }
159
+ }
160
+ ```
161
+
162
+ ### Discovery + tool schema (`describeSchema` / `getRequestJsonSchema`)
163
+
164
+ Two read-only helpers for agent integration:
165
+
166
+ - `korm.describeSchema()` / `korm.describeModel('User')` — pure-data
167
+ description of tables, typed columns, relations, soft-delete flag, and
168
+ available actions. Use it to discover what's queryable before building
169
+ a request. Throws `KormError` (`code: 'UNKNOWN_MODEL'`) for a bad name.
170
+ - `korm.getRequestJsonSchema('User')` — draft-2020-12 JSON Schema for
171
+ every valid request body for that model (an `action`-discriminated
172
+ `oneOf`). Attach it to an OpenAI/Anthropic tool definition or use it
173
+ for client-side prevalidation:
174
+
175
+ ```js
176
+ const ctx = korm.describeModel('User'); // discovery
177
+ const schema = korm.getRequestJsonSchema('User'); // request contract
178
+ // OpenAI: { type: 'function', function: { name, description, parameters: schema } }
179
+ // Anthropic:{ name, description, input_schema: schema }
180
+ ```
181
+
182
+ ## Canonical examples
183
+
184
+ ### Read with filter + pagination
185
+
186
+ ```js
187
+ await korm.processRequest(
188
+ {
189
+ action: 'list',
190
+ where: { is_active: true, age: '>=18' },
191
+ select: ['id', 'username', 'email'],
192
+ orderBy: { column: 'created_at', direction: 'desc' },
193
+ limit: 20,
194
+ offset: 0,
195
+ },
196
+ 'User'
197
+ );
198
+ ```
199
+
200
+ ### Create
201
+
202
+ ```js
203
+ await korm.processRequest(
204
+ {
205
+ action: 'create',
206
+ data: { username: 'john_doe', email: 'john@example.com', age: 30 },
207
+ },
208
+ 'User'
209
+ );
210
+ ```
211
+
212
+ ### Update by relation
213
+
214
+ ```js
215
+ await korm.processRequest(
216
+ {
217
+ action: 'update',
218
+ where: { 'User.id': 1 },
219
+ data: { status: 'active' },
220
+ with: ['User'],
221
+ },
222
+ 'Profile'
223
+ );
224
+ ```
225
+
226
+ ### Nested eager-load
227
+
228
+ ```js
229
+ await korm.processRequest(
230
+ {
231
+ action: 'list',
232
+ where: { 'User.is_active': true },
233
+ select: ['id', 'title', 'User.username'],
234
+ with: ['User', 'User.UserDetail', 'Comment'],
235
+ withWhere: { 'Comment.is_approved': true },
236
+ limit: 5,
237
+ },
238
+ 'Post'
239
+ );
240
+ ```
241
+
242
+ ### Upsert
243
+
244
+ ```js
245
+ await korm.processRequest(
246
+ {
247
+ action: 'upsert',
248
+ data: { email: 'a@b.com', name: 'Alice' },
249
+ conflict: ['email'],
250
+ },
251
+ 'User'
252
+ );
253
+ ```
254
+
255
+ ## Rules for AI assistants helping consumers
256
+
257
+ 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
+ 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.
260
+ 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
+ 5. **Don't invent fields.** The top-level keys above are the entire contract surface. No `filter`, no `query`, no `params`.
262
+ 6. **Soft delete is per-model.** `delete` becomes a soft-delete only if the model declares it; don't assume.
263
+ 7. **Preview before mutating.** For a risky write, add `dryRun: true` first to inspect the SQL, then re-issue without it.
264
+ 8. **Handle errors by `code`.** Catch `KormError` and branch on `e.code` (table above) rather than string-matching `e.message`.
265
+ 9. **Refresh this doc** by re-running `npx @dreamtree-org/korm-js init --ai <provider>` when the library is upgraded.
@@ -0,0 +1,2 @@
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};
package/build.js CHANGED
@@ -1,2 +1,2 @@
1
1
  #!/usr/bin/env node
2
- const fs=require("fs"),path=require("path"),{execSync:execSync}=require("child_process");function progressBar(e,s,i=30){const o=e/s,t=Math.round(i*o),n=i-t,c="█".repeat(t)+"-".repeat(n);process.stdout.write(`\r[${c}] ${(100*o).toFixed(1)}% (${e}/${s})`),e===s&&process.stdout.write("\n")}function getAllJsFiles(e,s=["node_modules","dist","test"]){let i=[];return fs.readdirSync(e).forEach(o=>{const t=path.join(e,o),n=fs.statSync(t);n&&n.isDirectory()?s.includes(o)||(i=i.concat(getAllJsFiles(t,s))):o.endsWith(".js")&&i.push(t)}),i}function ensureDir(e){fs.existsSync(e)||(fs.mkdirSync(e,{recursive:!0}),console.log(`✅ Created directory: ${e}`))}function minifyFile(e,s){try{return execSync(`npx terser "${e}" -o "${s}" --compress --mangle --comments false`,{stdio:"pipe"}),!0}catch(i){return fs.copyFileSync(e,s),!1}}function copyFile(e,s){fs.copyFileSync(e,s)}function copyDirectory(e,s){if(!fs.existsSync(e))return;ensureDir(s);fs.readdirSync(e,{withFileTypes:!0}).forEach(i=>{const o=path.join(e,i.name),t=path.join(s,i.name);i.isDirectory()?copyDirectory(o,t):copyFile(o,t)})}async function build(){console.log("🚀 KORM Build: Minifying all JS files to dist/ with progress bar\n"),fs.existsSync("dist")&&(fs.rmSync("dist",{recursive:!0,force:!0}),console.log("✅ Cleaned dist/")),ensureDir("dist");const e=getAllJsFiles(".",["node_modules","dist","test"]),s=e.length;let i=0,o=0,t=0;e.forEach((e,n)=>{const c=path.relative(".",e),r=path.join("dist",c);ensureDir(path.dirname(r));const l=fs.statSync(e).size,a=(minifyFile(e,r),fs.statSync(r).size);o+=l,t+=a,i++,progressBar(i,s)}),["README.md","LICENSE"].forEach(e=>{fs.existsSync(e)&&(copyFile(e,path.join("dist",e)),console.log(`✅ Copied: ${e}`))}),fs.existsSync("templates")&&(copyDirectory("templates",path.join("dist","templates")),console.log("✅ Copied templates/ to dist/")),fs.existsSync("node_modules")&&(execSync("cp -r node_modules dist/",{stdio:"pipe"}),console.log("✅ Copied node_modules/ to dist/"));const n=path.join(".","version-manager.js");let c=!1,r=null;if(fs.existsSync(n))try{const e=new(require("./version-manager"));"function"==typeof e.smartAutoIncrement&&(r=await e.smartAutoIncrement(),c=!0,console.log(`✅ Version updated using version-manager.js: ${r}`))}catch(e){console.warn("⚠️ Could not update version using version-manager.js:",e.message)}if(fs.existsSync("package.json")){const e=JSON.parse(fs.readFileSync("package.json","utf8"));e.scripts&&(delete e.scripts.build,delete e.scripts.clean,delete e.scripts.minify,delete e.scripts["minify:js"],delete e.scripts.prepublishOnly),delete e.devDependencies,e.main="index.js",delete e.files,c&&r&&("string"==typeof r?(e.version=r,console.log(`✅ Set version in dist/package.json: ${r}`)):(console.warn(`⚠️ newVersion is not a string: ${typeof r} - ${JSON.stringify(r)}`),e.version=e.version||"1.0.0")),fs.writeFileSync(path.join("dist","package.json"),JSON.stringify(e,null,2)),console.log("✅ Created dist/package.json")}const l=((o-t)/o*100).toFixed(1);console.log("\n📊 Build Statistics:"),console.log(` JS files processed: ${s}`),console.log(` Original size: ${(o/1024).toFixed(1)} KB`),console.log(` Minified size: ${(t/1024).toFixed(1)} KB`),console.log(` Size reduction: ${l}%`),c&&r&&console.log(` New version: ${r}`),console.log("\n🎉 Build completed! Output in dist/")}build().catch(console.error);
2
+ const fs=require("fs"),path=require("path"),{execSync:execSync}=require("child_process"),FILE_ASSETS=["README.md","LICENSE","index.d.ts"],DIR_ASSETS=["templates","ai-skills"];function progressBar(e,s,i=30){const o=e/s,n=Math.round(i*o),t=i-n,r="█".repeat(n)+"-".repeat(t);process.stdout.write(`\r[${r}] ${(100*o).toFixed(1)}% (${e}/${s})`),e===s&&process.stdout.write("\n")}function getAllJsFiles(e,s=["node_modules","dist","test"]){let i=[];return fs.readdirSync(e).forEach(o=>{const n=path.join(e,o),t=fs.statSync(n);t&&t.isDirectory()?s.includes(o)||(i=i.concat(getAllJsFiles(n,s))):o.endsWith(".js")&&i.push(n)}),i}function ensureDir(e){fs.existsSync(e)||(fs.mkdirSync(e,{recursive:!0}),console.log(`✅ Created directory: ${e}`))}function minifyFile(e,s){try{return execSync(`npx terser "${e}" -o "${s}" --compress --mangle --comments false`,{stdio:"pipe"}),!0}catch(i){return fs.copyFileSync(e,s),!1}}function copyFile(e,s){fs.copyFileSync(e,s)}function copyDirectory(e,s){if(!fs.existsSync(e))return;ensureDir(s);fs.readdirSync(e,{withFileTypes:!0}).forEach(i=>{const o=path.join(e,i.name),n=path.join(s,i.name);i.isDirectory()?copyDirectory(o,n):copyFile(o,n)})}async function build(){console.log("🚀 KORM Build: Minifying all JS files to dist/ with progress bar\n"),fs.existsSync("dist")&&(fs.rmSync("dist",{recursive:!0,force:!0}),console.log("✅ Cleaned dist/")),ensureDir("dist");const e=getAllJsFiles(".",["node_modules","dist","test"]),s=e.length;let i=0,o=0,n=0;e.forEach((e,t)=>{const r=path.relative(".",e),c=path.join("dist",r);ensureDir(path.dirname(c));const l=fs.statSync(e).size,d=(minifyFile(e,c),fs.statSync(c).size);o+=l,n+=d,i++,progressBar(i,s)}),FILE_ASSETS.forEach(e=>{fs.existsSync(e)&&(copyFile(e,path.join("dist",e)),console.log(`✅ Copied: ${e}`))}),DIR_ASSETS.forEach(e=>{fs.existsSync(e)&&(copyDirectory(e,path.join("dist",e)),console.log(`✅ Copied ${e}/ to dist/`))}),fs.existsSync("node_modules")&&(execSync("cp -r node_modules dist/",{stdio:"pipe"}),console.log("✅ Copied node_modules/ to dist/"));const t=path.join(".","version-manager.js");let r=!1,c=null;if(!("1"===process.env.BUILD_SKIP_VERSION_BUMP)&&fs.existsSync(t))try{const e=new(require("./version-manager"));"function"==typeof e.smartAutoIncrement&&(c=await e.smartAutoIncrement(),r=!0,console.log(`✅ Version updated using version-manager.js: ${c}`))}catch(e){console.warn("⚠️ Could not update version using version-manager.js:",e.message)}if(fs.existsSync("package.json")){const e=JSON.parse(fs.readFileSync("package.json","utf8"));e.scripts&&(delete e.scripts.build,delete e.scripts.clean,delete e.scripts.minify,delete e.scripts["minify:js"],delete e.scripts.prepublishOnly),delete e.devDependencies,e.main="index.js",delete e.files,r&&c&&("string"==typeof c?(e.version=c,console.log(`✅ Set version in dist/package.json: ${c}`)):(console.warn(`⚠️ newVersion is not a string: ${typeof c} - ${JSON.stringify(c)}`),e.version=e.version||"1.0.0")),fs.writeFileSync(path.join("dist","package.json"),JSON.stringify(e,null,2)),console.log("✅ Created dist/package.json")}const l=((o-n)/o*100).toFixed(1);console.log("\n📊 Build Statistics:"),console.log(` JS files processed: ${s}`),console.log(` Original size: ${(o/1024).toFixed(1)} KB`),console.log(` Minified size: ${(n/1024).toFixed(1)} KB`),console.log(` Size reduction: ${l}%`),r&&c&&console.log(` New version: ${c}`),console.log("\n🎉 Build completed! Output in dist/")}require.main===module&&build().catch(console.error),module.exports={build:build,FILE_ASSETS:FILE_ASSETS,DIR_ASSETS:DIR_ASSETS};
package/cli.js ADDED
@@ -0,0 +1,2 @@
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};
@@ -0,0 +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 +1 @@
1
- const{getDefaultTypeSize:getDefaultTypeSize,getDbType:getDbType}=require("./DataTypeMap");class BaseUtility{getModel(e,t){let n=e?.schema,l=null,r=Object.keys(n);if(r.forEach(e=>{e===t&&(l=n[e])}),l||r.forEach(e=>{let r=n[e];r.table===t&&(l=r)}),l)return{...l,name:t,columns:Object.keys(l.columns).map(e=>{let t=l.columns[e];return this.parseColumnString(e,t)})};throw new Error(`Model ${t} not found`)}map2DbDefault(e){switch(e){case"now":case"now()":return"CURRENT_TIMESTAMP";default:return e}}getCollenedValue(e,{splitChar:t=":",give:n="right",trimWrap:l="{}"}={}){if(null==e)return null;const r=String(e),i=r.indexOf(t),a=e=>{if(!l)return e;if("string"==typeof l&&2===l.length){const[t,n]=l;return e.startsWith(t)&&e.endsWith(n)?e.slice(1,-1):e}return"string"==typeof l&&l.length&&e.startsWith(l)&&e.endsWith(l)?e.slice(l.length,-l.length):e};if(i>=0){const e=r.slice(0,i).trim(),l=r.slice(i+t.length).trim();return"both"===n?{left:a(e),right:a(l)}:a("left"===n?e:l)}return a(r)}parseForeignKey(e){if(!e)return{foreignMapTables:[]};const t=this.getCollenedValue(e),[n,l="id"]=String(t||"").split(":"),r=(this.getCollenedValue(n)||"").split(",").filter(Boolean),i=(this.getCollenedValue(l)||"").split(",").filter(Boolean);return{foreignMapTables:r.map((e,t)=>({table:e,column:i[t]||i[0]||"id"}))}}parseColumnString(e,t){const[n,...l]=String(t).split("|"),r=e=>l.find(t=>t.startsWith(e+":")),i=e=>l.join("|").includes(e),a=e=>e&&e.includes(":")?e.slice(e.indexOf(":")+1):null,u=r("default"),s=r("onUpdate"),o=r("comment"),g=r("foreignKey"),p=r("size"),m=this.parseForeignKey(g),c=!!g,f=getDbType(n,p?a(p):null),E=p?a(p):getDefaultTypeSize(f);return{name:e,type:f,size:E,isUnsigned:i("unsigned")||i("primaryKey")||c,columnType:`${n}${E?`(${E})`:""}`,nullable:!i("notNull"),primary:i("primaryKey"),autoIncrement:i("autoIncrement"),unique:i("unique"),default:this.map2DbDefault(a(u)),onUpdate:this.map2DbDefault(a(s)),comment:a(o)||"",hasForeignKey:c,...m}}formatColumnSchema(e,t){return this.parseColumnString(e,t)}formatColumnDef(e,t){return{name:e,type:t.DATA_TYPE,size:t.CHARACTER_MAXIMUM_LENGTH||getDefaultTypeSize(t.DATA_TYPE),isUnsigned:String(t.COLUMN_TYPE||"").toLowerCase().includes("unsigned"),columnType:t.COLUMN_TYPE,nullable:"YES"===t.IS_NULLABLE,primary:"PRI"===t.COLUMN_KEY,unique:"UNI"===t.COLUMN_KEY,autoIncrement:String(t.EXTRA||"").toLowerCase().includes("auto_increment"),hasForeignKey:"MUL"===t.COLUMN_KEY,comment:t.COMMENT,default:t.COLUMN_DEFAULT,onUpdate:((e="")=>{const t=String(e).match(/on update\s+([a-zA-Z_]+)/i);return t?t[1]:null})(t.EXTRA),foreignMapTables:"MUL"===t.COLUMN_KEY&&t.REFERENCED_TABLE_NAME?[{table:t.REFERENCED_TABLE_NAME,column:t.REFERENCED_COLUMN_NAME}]:[]}}escapeComment(e){return null==e?"":String(e).replace(/'/g,"\\'")}getDefaultTypeSize(e){return getDefaultTypeSize(e)}}module.exports=BaseUtility;
1
+ const{getDefaultTypeSize:getDefaultTypeSize,getDbType:getDbType}=require("./DataTypeMap"),KormError=require("../../KormError");class BaseUtility{getModel(e,t){const n=e?.schema;let r=null;const l=Object.keys(n);if(l.forEach(e=>{e===t&&(r=n[e])}),r||l.forEach(e=>{const l=n[e];l.table===t&&(r=l)}),r)return{...r,name:t,columns:Object.keys(r.columns).map(e=>{const t=r.columns[e];return this.parseColumnString(e,t)})};throw KormError.unknownModel({model:t,available:Object.keys(n||{})})}map2DbDefault(e){switch(e){case"now":case"now()":return"CURRENT_TIMESTAMP";default:return e}}getCollenedValue(e,{splitChar:t=":",give:n="right",trimWrap:r="{}"}={}){if(null==e)return null;const l=String(e),i=l.indexOf(t),a=e=>{if(!r)return e;if("string"==typeof r&&2===r.length){const[t,n]=r;return e.startsWith(t)&&e.endsWith(n)?e.slice(1,-1):e}return"string"==typeof r&&r.length&&e.startsWith(r)&&e.endsWith(r)?e.slice(r.length,-r.length):e};if(i>=0){const e=l.slice(0,i).trim(),r=l.slice(i+t.length).trim();return"both"===n?{left:a(e),right:a(r)}:a("left"===n?e:r)}return a(l)}parseForeignKey(e){if(!e)return{foreignMapTables:[]};const t=this.getCollenedValue(e),[n,r="id"]=String(t||"").split(":"),l=(this.getCollenedValue(n)||"").split(",").filter(Boolean),i=(this.getCollenedValue(r)||"").split(",").filter(Boolean);return{foreignMapTables:l.map((e,t)=>({table:e,column:i[t]||i[0]||"id"}))}}parseColumnString(e,t){const[n,...r]=String(t).split("|"),l=e=>r.find(t=>t.startsWith(e+":")),i=e=>r.join("|").includes(e),a=e=>e&&e.includes(":")?e.slice(e.indexOf(":")+1):null,s=l("default"),o=l("onUpdate"),u=l("comment"),m=l("foreignKey"),c=l("size"),g=this.parseForeignKey(m),p=!!m,E=getDbType(n,c?a(c):null),f=c?a(c):getDefaultTypeSize(E);return{name:e,type:E,size:f,isUnsigned:i("unsigned")||i("primaryKey")||p,columnType:`${n}${f?`(${f})`:""}`,nullable:!i("notNull"),primary:i("primaryKey"),autoIncrement:i("autoIncrement"),unique:i("unique"),default:this.map2DbDefault(a(s)),onUpdate:this.map2DbDefault(a(o)),comment:a(u)||"",hasForeignKey:p,...g}}formatColumnSchema(e,t){return this.parseColumnString(e,t)}formatColumnDef(e,t){return{name:e,type:t.DATA_TYPE,size:t.CHARACTER_MAXIMUM_LENGTH||getDefaultTypeSize(t.DATA_TYPE),isUnsigned:String(t.COLUMN_TYPE||"").toLowerCase().includes("unsigned"),columnType:t.COLUMN_TYPE,nullable:"YES"===t.IS_NULLABLE,primary:"PRI"===t.COLUMN_KEY,unique:"UNI"===t.COLUMN_KEY,autoIncrement:String(t.EXTRA||"").toLowerCase().includes("auto_increment"),hasForeignKey:"MUL"===t.COLUMN_KEY,comment:t.COMMENT,default:t.COLUMN_DEFAULT,onUpdate:((e="")=>{const t=String(e).match(/on update\s+([a-zA-Z_]+)/i);return t?t[1]:null})(t.EXTRA),foreignMapTables:"MUL"===t.COLUMN_KEY&&t.REFERENCED_TABLE_NAME?[{table:t.REFERENCED_TABLE_NAME,column:t.REFERENCED_COLUMN_NAME}]:[]}}escapeComment(e){return null==e?"":String(e).replace(/'/g,"\\'")}getDefaultTypeSize(e){return getDefaultTypeSize(e)}}module.exports=BaseUtility;
@@ -1 +1 @@
1
- const QueryService=require("./QueryService"),HookService=require("./HookService");class CurdTable{constructor(e,r,t=null){if(this.db=e,this.utils=r,this.controllerWrapper=t,this.hookService=new HookService(e,r,t),this.queryService=new QueryService(e,r,t),!this.queryService)throw new Error("CurdTable requires queryService (execute*Query / getQuery).")}async processRequest(e,r=null,t={}){const o=this.controllerWrapper;let s=this.utils.getModel(this.controllerWrapper,r);const c=e?.action||"list";let a=null;const i={model:s,action:c,request:e,ctx:t,controller:o};switch(this.hookService?.executeValidatorHook&&await this.hookService.executeValidatorHook({...i}),this.hookService?.executeBeforeHook&&(e.beforeActionData=await this.hookService.executeBeforeHook({...i})),c){case"count":a=await this.queryService.executeCountQuery(s,e);break;case"sum":a=await this.queryService.executeSumQuery(s,e);break;case"list":a=await this.hookService.executeHasSoftDeleteHook(s)?await this.queryService.getSoftDeleteQuery(s,e):await this.queryService.getQuery(s,e);break;case"show":a=await this.queryService.executeShowQuery(s,e);break;case"create":a=await this.queryService.executeCreateQuery(s,e);break;case"update":{const r=await this.queryService.executeUpdateQuery(s,e);if(!r)throw new Error(`Record not found or not updated: ${s.table} returned ${r}`);a={message:"Record updated successfully",data:r,success:!0};break}case"replace":if(a=await this.queryService.executeReplaceQuery(s,e),!a)throw new Error(`Record not found or not replaced: ${r} returned ${a}`);a={message:"Record replaced successfully",data:a,success:!0};break;case"upsert":if(a=await this.queryService.executeUpsertQuery(s,e),!a)throw new Error(`Record not found or not upserted: ${r} returned ${a}`);a={message:"Record upserted successfully",data:a,success:!0};break;case"sync":if(a=await this.queryService.executeSyncQuery(s,e),!a)throw new Error(`Record not found or not synced: ${r} returned ${a}`);a={message:"Record synced successfully",data:a,success:!0};break;case"delete":{let t=null;if(t=await this.hookService.executeHasSoftDeleteHook(s)?await this.queryService.executeSoftDeleteQuery(s,e):await this.queryService.executeDeleteQuery(s,e),!t)throw new Error(`Record not found or not deleted: ${r} returned ${t}`);a={message:"Record deleted successfully",data:t,success:!0};break}default:if(!this.hookService?.executeCustomAction)throw new Error(`Unknown action "${c}" and no custom action hook provided.`);a=await this.hookService.executeCustomAction({...i})}if(this.hookService?.executeAfterHook&&(a=await this.hookService.executeAfterHook({...i,data:a})),e?.other_requests&&"object"==typeof e.other_requests){const r={},o=Object.entries(e.other_requests);for(const[e,s]of o)Array.isArray(s)?r[e]=await Promise.all(s.map(r=>this.processRequest(r,e,t))):r[e]=await this.processRequest(s,e,t);a.other_responses=r}return a}}module.exports=CurdTable;
1
+ const QueryService=require("./QueryService"),HookService=require("./HookService"),KormError=require("../../KormError");class CurdTable{constructor(e,r,t=null){if(this.db=e,this.utils=r,this.controllerWrapper=t,this.hookService=new HookService(e,r,t),this.queryService=new QueryService(e,r,t),!this.queryService)throw new KormError("CurdTable requires queryService (execute*Query / getQuery).",{code:KormError.CODES.INTERNAL})}async processRequest(e,r=null,t={}){const o=this.controllerWrapper,s=this.utils.getModel(this.controllerWrapper,r),c=e?.action||"list";let i=null;const a={model:s,action:c,request:e,ctx:t,controller:o};if(this.hookService?.executeValidatorHook&&await this.hookService.executeValidatorHook({...a}),e?.dryRun)return this.buildDryRunResult(s,e,c);switch(this.hookService?.executeBeforeHook&&(e.beforeActionData=await this.hookService.executeBeforeHook({...a})),c){case"count":i=await this.queryService.executeCountQuery(s,e);break;case"sum":i=await this.queryService.executeSumQuery(s,e);break;case"list":i=await this.hookService.executeHasSoftDeleteHook(s)?await this.queryService.getSoftDeleteQuery(s,e):await this.queryService.getQuery(s,e);break;case"show":i=await this.queryService.executeShowQuery(s,e);break;case"create":i=await this.queryService.executeCreateQuery(s,e);break;case"update":{const r=await this.queryService.executeUpdateQuery(s,e);if(!r)throw KormError.noMatchingRow({action:c,model:s.name});i={message:"Record updated successfully",data:r,success:!0};break}case"replace":if(i=await this.queryService.executeReplaceQuery(s,e),!i)throw KormError.noMatchingRow({action:c,model:s.name});i={message:"Record replaced successfully",data:i,success:!0};break;case"upsert":if(i=await this.queryService.executeUpsertQuery(s,e),!i)throw KormError.noMatchingRow({action:c,model:s.name});i={message:"Record upserted successfully",data:i,success:!0};break;case"sync":if(i=await this.queryService.executeSyncQuery(s,e),!i)throw KormError.noMatchingRow({action:c,model:s.name});i={message:"Record synced successfully",data:i,success:!0};break;case"delete":{let r=null;if(r=await this.hookService.executeHasSoftDeleteHook(s)?await this.queryService.executeSoftDeleteQuery(s,e):await this.queryService.executeDeleteQuery(s,e),!r)throw KormError.noMatchingRow({action:c,model:s.name});i={message:"Record deleted successfully",data:r,success:!0};break}default:if(!this.hookService?.executeCustomAction)throw KormError.unknownAction({action:c,model:s?.name});i=await this.hookService.executeCustomAction({...a})}if(this.hookService?.executeAfterHook&&(i=await this.hookService.executeAfterHook({...a,data:i})),e?.other_requests&&"object"==typeof e.other_requests){const r={},o=Object.entries(e.other_requests);for(const[e,s]of o)Array.isArray(s)?r[e]=await Promise.all(s.map(r=>this.processRequest(r,e,t))):r[e]=await this.processRequest(s,e,t);i.other_responses=r}return i}async buildDryRunResult(e,r,t){if(!["list","show","count","sum","create","update","replace","upsert","sync","delete"].includes(t))throw KormError.unknownAction({action:t,model:e?.name});let o=t,s=r;const c=await this.hookService.executeHasSoftDeleteHook(e);return c&&"delete"===t?o="softDelete":c&&"list"===t&&(s={...r,where:{...r.where||{},deleted_at:null}}),this.queryService.buildDryRun(e,s,o)}}module.exports=CurdTable;
@@ -1 +1 @@
1
- const DataTypeMap=[{type:"number",maxSize:1,dbType:"tinyint"},{type:"number",maxSize:2,dbType:"smallint"},{type:"number",maxSize:3,dbType:"mediumint"},{type:"number",maxSize:4,dbType:"int"},{type:"number",maxSize:8,dbType:"bigint"},{type:"number",maxSize:null,dbType:"decimal"},{type:"number",maxSize:null,dbType:"numeric"},{type:"number",maxSize:null,dbType:"float"},{type:"number",maxSize:null,dbType:"double"},{type:"number",maxSize:null,dbType:"real"},{type:"number",maxSize:null,dbType:"bit"},{type:"string",maxSize:255,dbType:"varchar"},{type:"string",maxSize:1,dbType:"char"},{type:"string",maxSize:65535,dbType:"text"},{type:"string",maxSize:255,dbType:"tinytext"},{type:"string",maxSize:65535,dbType:"text"},{type:"string",maxSize:16777215,dbType:"mediumtext"},{type:"string",maxSize:4294967295,dbType:"longtext"},{type:"string",maxSize:255,dbType:"enum"},{type:"string",maxSize:255,dbType:"set"},{type:"string",maxSize:null,dbType:"json"},{type:"string",maxSize:36,dbType:"uuid"},{type:"buffer",maxSize:255,dbType:"binary"},{type:"buffer",maxSize:255,dbType:"varbinary"},{type:"buffer",maxSize:255,dbType:"tinyblob"},{type:"buffer",maxSize:65535,dbType:"blob"},{type:"buffer",maxSize:16777215,dbType:"mediumblob"},{type:"buffer",maxSize:4294967295,dbType:"longblob"},{type:"boolean",maxSize:null,dbType:"tinyint(1)"},{type:"boolean",maxSize:null,dbType:"boolean"},{type:"date",maxSize:null,dbType:"date"},{type:"date",maxSize:null,dbType:"datetime"},{type:"date",maxSize:null,dbType:"timestamp"},{type:"date",maxSize:null,dbType:"time"},{type:"date",maxSize:null,dbType:"year"},{type:"string",maxSize:null,dbType:"geometry"},{type:"string",maxSize:null,dbType:"point"},{type:"string",maxSize:null,dbType:"linestring"},{type:"string",maxSize:null,dbType:"polygon"},{type:"string",maxSize:null,dbType:"multipoint"},{type:"string",maxSize:null,dbType:"multilinestring"},{type:"string",maxSize:null,dbType:"multipolygon"},{type:"string",maxSize:null,dbType:"geometrycollection"},{type:"string",maxSize:null,dbType:"USER-DEFINED"}];function getDbType(e,t,p){if(p){const y=DataTypeMap.find(y=>y.type===e&&y.maxSize===t&&y.dbType.toLowerCase()===p.toLowerCase());if(y)return y.dbType}let y=DataTypeMap.find(p=>p.type===e&&p.maxSize===t);if(!y){let t=DataTypeMap.filter(t=>t.type===e&&null===t.maxSize);y=t.length>1&&p?t.find(e=>e.dbType.toLowerCase()===p.toLowerCase()):t[0]}return y?y.dbType:e}function getSchemaType(e){return DataTypeMap.find(t=>t.dbType===e)?.type}function getDefaultTypeSize(e){return DataTypeMap.find(t=>t.dbType===e)?.maxSize}module.exports={DataTypeMap:DataTypeMap,getDbType:getDbType,getSchemaType:getSchemaType,getDefaultTypeSize:getDefaultTypeSize};
1
+ const DataTypeMap=[{type:"number",maxSize:1,dbType:"tinyint"},{type:"number",maxSize:2,dbType:"smallint"},{type:"number",maxSize:3,dbType:"mediumint"},{type:"number",maxSize:4,dbType:"int"},{type:"number",maxSize:8,dbType:"bigint"},{type:"number",maxSize:null,dbType:"decimal"},{type:"number",maxSize:null,dbType:"numeric"},{type:"number",maxSize:null,dbType:"float"},{type:"number",maxSize:null,dbType:"double"},{type:"number",maxSize:null,dbType:"real"},{type:"number",maxSize:null,dbType:"bit"},{type:"string",maxSize:255,dbType:"varchar"},{type:"string",maxSize:1,dbType:"char"},{type:"string",maxSize:65535,dbType:"text"},{type:"string",maxSize:255,dbType:"tinytext"},{type:"string",maxSize:65535,dbType:"text"},{type:"string",maxSize:16777215,dbType:"mediumtext"},{type:"string",maxSize:4294967295,dbType:"longtext"},{type:"string",maxSize:255,dbType:"enum"},{type:"string",maxSize:255,dbType:"set"},{type:"string",maxSize:null,dbType:"json"},{type:"string",maxSize:36,dbType:"uuid"},{type:"buffer",maxSize:255,dbType:"binary"},{type:"buffer",maxSize:255,dbType:"varbinary"},{type:"buffer",maxSize:255,dbType:"tinyblob"},{type:"buffer",maxSize:65535,dbType:"blob"},{type:"buffer",maxSize:16777215,dbType:"mediumblob"},{type:"buffer",maxSize:4294967295,dbType:"longblob"},{type:"boolean",maxSize:null,dbType:"tinyint(1)"},{type:"boolean",maxSize:null,dbType:"boolean"},{type:"date",maxSize:null,dbType:"date"},{type:"date",maxSize:null,dbType:"datetime"},{type:"date",maxSize:null,dbType:"timestamp"},{type:"date",maxSize:null,dbType:"time"},{type:"date",maxSize:null,dbType:"year"},{type:"string",maxSize:null,dbType:"geometry"},{type:"string",maxSize:null,dbType:"point"},{type:"string",maxSize:null,dbType:"linestring"},{type:"string",maxSize:null,dbType:"polygon"},{type:"string",maxSize:null,dbType:"multipoint"},{type:"string",maxSize:null,dbType:"multilinestring"},{type:"string",maxSize:null,dbType:"multipolygon"},{type:"string",maxSize:null,dbType:"geometrycollection"},{type:"string",maxSize:null,dbType:"USER-DEFINED"}];function _normaliseMaxSize(e){if(null==e||""===e)return null;const t=Number(e);return Number.isNaN(t)?null:t}function getDbType(e,t,p){const y=_normaliseMaxSize(t);if(p){const t=DataTypeMap.find(t=>t.type===e&&t.maxSize===y&&t.dbType.toLowerCase()===p.toLowerCase());if(t)return t.dbType}let i=DataTypeMap.find(t=>t.type===e&&t.maxSize===y);if(!i){const t=DataTypeMap.filter(t=>t.type===e&&null===t.maxSize);i=t.length>1&&p?t.find(e=>e.dbType.toLowerCase()===p.toLowerCase()):t[0]}return i?i.dbType:e}function getSchemaType(e){return DataTypeMap.find(t=>t.dbType===e)?.type}function getDefaultTypeSize(e){return DataTypeMap.find(t=>t.dbType===e)?.maxSize}module.exports={DataTypeMap:DataTypeMap,getDbType:getDbType,getSchemaType:getSchemaType,getDefaultTypeSize:getDefaultTypeSize};
@@ -1 +1 @@
1
- const path=require("path"),logger=require("../../Logger");class HookService{constructor(e,t,o=null){this.db=e,this.utils=t,this.controllerWrapper=o,this.controllerWrapper.hookService=this,this.appRoot=o&&o.resolverPath?o.resolverPath:process.cwd()}loadModelClass(e){try{const t=path.join(this.appRoot,"models",`${e}.model.js`);delete require.cache[require.resolve(t)];return require(t)}catch(e){return void logger.debug("loadModelClass error",{err:e})}}getModelInstance(e){let t="string"==typeof e?e:e.modelName;const o=this.loadModelClass(t);if(o)return"function"==typeof o?new o:o}resolveModelHook(e,t,o){const r=e.modelName,s=this.loadModelClass(r);if(!s)return;const l=this.getModelInstance(e);let i;if("validate"===t)i="validate";else if("on"===t)i=`on${o.charAt(0).toUpperCase()+o.slice(1)}`;else if("before"===t)i=`before${o.charAt(0).toUpperCase()+o.slice(1)}`;else if("after"===t)i=`after${o.charAt(0).toUpperCase()+o.slice(1)}`;else{if("custom"!==t)return;i=`on${o.charAt(0).toUpperCase()+o.slice(1)}Action`}return"function"==typeof l[i]?l[i].bind(l):"function"==typeof s[i]?s[i].bind(s):void 0}async executeValidatorHook({model:e,action:t,request:o,ctx:r,controller:s}){const l=this.resolveModelHook(e,"validate",t);if(l)return await l({model:e,action:t,request:o,context:r,db:this.db,utils:this.utils,controller:s})}async executeBeforeHook({model:e,action:t,request:o,ctx:r,controller:s}){const l=this.resolveModelHook(e,"before",t);if(l)return await l({model:e,action:t,request:o,context:r,db:this.db,utils:this.utils,controller:s})}async executeAfterHook({model:e,action:t,data:o,request:r,ctx:s,controller:l}){const i=this.resolveModelHook(e,"after",t);return i?await i({model:e,action:t,data:o,request:r,context:s,db:this.db,utils:this.utils,controller:l}):o}async executeCustomAction({model:e,action:t,request:o,ctx:r,controller:s}){const l=this.resolveModelHook(e,"custom",t);if(l)return await l({model:e,action:t,request:o,context:r,db:this.db,utils:this.utils,controller:s});throw new Error(`No custom action hook found for ${e.modelName}.${t}`)}async executeHasSoftDeleteHook(e){const t=this.getModelInstance(e);if(!t)return!1;return t.hasOwnProperty("hasSoftDelete")&&!0===t.hasSoftDelete}}module.exports=HookService;
1
+ const path=require("path"),KormError=require("../../KormError"),logger=require("../../Logger");class HookService{constructor(e,t,o=null){this.db=e,this.utils=t,this.controllerWrapper=o,this.controllerWrapper.hookService=this,this.appRoot=o&&o.resolverPath?o.resolverPath:process.cwd()}loadModelClass(e){try{const t=path.join(this.appRoot,"models",`${e}.model.js`);delete require.cache[require.resolve(t)];return require(t)}catch(e){return void logger.debug("loadModelClass error",{err:e})}}getModelInstance(e){const t="string"==typeof e?e:e.modelName,o=this.loadModelClass(t);if(o)return"function"==typeof o?new o:o}resolveModelHook(e,t,o){const r=e.modelName,s=this.loadModelClass(r);if(!s)return;const l=this.getModelInstance(e);let i;if("validate"===t)i="validate";else if("on"===t)i=`on${o.charAt(0).toUpperCase()+o.slice(1)}`;else if("before"===t)i=`before${o.charAt(0).toUpperCase()+o.slice(1)}`;else if("after"===t)i=`after${o.charAt(0).toUpperCase()+o.slice(1)}`;else{if("custom"!==t)return;i=`on${o.charAt(0).toUpperCase()+o.slice(1)}Action`}return"function"==typeof l[i]?l[i].bind(l):"function"==typeof s[i]?s[i].bind(s):void 0}async executeValidatorHook({model:e,action:t,request:o,ctx:r,controller:s}){const l=this.resolveModelHook(e,"validate",t);if(l)return await l({model:e,action:t,request:o,context:r,db:this.db,utils:this.utils,controller:s})}async executeBeforeHook({model:e,action:t,request:o,ctx:r,controller:s}){const l=this.resolveModelHook(e,"before",t);if(l)return await l({model:e,action:t,request:o,context:r,db:this.db,utils:this.utils,controller:s})}async executeAfterHook({model:e,action:t,data:o,request:r,ctx:s,controller:l}){const i=this.resolveModelHook(e,"after",t);return i?await i({model:e,action:t,data:o,request:r,context:s,db:this.db,utils:this.utils,controller:l}):o}async executeCustomAction({model:e,action:t,request:o,ctx:r,controller:s}){const l=this.resolveModelHook(e,"custom",t);if(l)return await l({model:e,action:t,request:o,context:r,db:this.db,utils:this.utils,controller:s});throw KormError.unknownAction({action:t,model:e.modelName||e.name,hasCustomHook:!0})}async executeHasSoftDeleteHook(e){const t=this.getModelInstance(e);if(!t)return!1;return t.hasOwnProperty("hasSoftDelete")&&!0===t.hasSoftDelete}}module.exports=HookService;
@@ -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(`\`${t}\` LIKE ?`,[o]);break;default:e.orWhere(t,r,o)}else e.orWhereNull(t)}_applyAndWhereCondition(e,t,r,o){if(null!==o)if(Array.isArray(o))e.whereIn(t,o);else switch(r){case"between":e.whereBetween(t,o);break;case"notBetween":e.whereNotBetween(t,o);break;case"in":e.whereIn(t,o);break;case"notIn":e.whereNotIn(t,o);break;case"like":e.whereRaw(`\`${t}\` LIKE ?`,[o]);break;default:e.where(t,r,o)}else e.whereNull(t)}buildWithTree(e,t=null){return this.helperUtility.dotWalkTree(e,{resolver:({current:e,part:r,source:o})=>t?t({current:e,part:r,source:o}):{}})}_getWithWhereForRelation(e,t){if(!e||"object"!=typeof e)return{direct:{},nested:{}};const r=`${t}.`,o={},i={};for(const[t,s]of Object.entries(e))if(t.startsWith(r)){const e=t.slice(r.length);e.includes(".")?i[e]=s:o[e]=s}return{direct:o,nested:i}}_applyWithWhereConditions(e,t){for(const[r,o]of Object.entries(t)){const{joinType:t="AND",column:i}=this.parseWhereColumn(r),{operator:s,value:n}=this.parseWhereValue(o);"AND"===t?this._applyAndWhereCondition(e,i,s,n):this._applyOrWhereCondition(e,i,s,n)}}async fetchRelatedRows(e,t,r={}){if(e.through){let o=await this.db(e.through).whereIn(e.throughLocalKey,t),i=this.db(e.table).whereIn(e.foreignKey,o.map(t=>t[e.throughForeignKey]));return Object.keys(r).length>0&&this._applyWithWhereConditions(i,r),i}{let o=this.db(e.table).whereIn(e.foreignKey,t);return Object.keys(r).length>0&&this._applyWithWhereConditions(o,r),o}}async fetchAndAttachRelated(e){const{parentRows:t,relName:r,model:o,withTree:i,relation:s,withWhere:n={}}=e;if(!s){const e=this.getHookService().getModelInstance(o),s=`get${r.charAt(0).toUpperCase()+r.slice(1)}Relation`;if("function"==typeof e[s]){const{direct:l,nested:a}=this._getWithWhereForRelation(n,r),h={rows:t,relName:r,model:o,withTree:i,controller:this.controllerWrapper,relation:o.hasRelations[r],qb:this,db:this.db,withWhere:l,nestedWithWhere:a};await e[s](h)}else logger.warn(`Method ${s} not found in model ${o.name}`);return t}let l=t.map(e=>e[s.localKey]),a=[];const h="one"===s?.type,{direct:c,nested:p}=this._getWithWhereForRelation(n,r);let u={...c};try{const e=this.utils.getModel(this.controllerWrapper,r);if(this.controllerWrapper?.hookService){await this.controllerWrapper.hookService.executeHasSoftDeleteHook(e)&&(u={...u,deleted_at:null})}}catch(e){}a=await this.fetchRelatedRows(s,l,u);let y=new Map;for(const e of a){let t=e[s.foreignKey];y.has(t)||y.set(t,[]),y.get(t).push(e)}for(const e of t){let t=e[s.localKey];h&&1==y.get(t)?.length?e[r]=y.get(t)[0]:e[r]=y.get(t)||[]}let d=Object.keys(i);for(const e of d){let o=i[e],s=t.filter(e=>h?e[r]:e[r].length>0).map(e=>e[r]).reduce((e,t)=>e.concat(t),[]),n=this.utils.getModel(this.controllerWrapper,r);await this.fetchAndAttachRelated({parentRows:s,relName:e,model:n,withTree:o,relation:n.hasRelations[e],withWhere:p})}return t}filterWhere(e,t=""){return t?Object.keys(e).filter(e=>e.includes(t)).reduce((r,o)=>(r[o.replace(t,"")]=e[o],r),{}):Object.keys(e).filter(e=>!e.includes(".")).reduce((t,r)=>(t[r]=e[r],t),{})}async getQuery(e,t){try{const{where:r={},with:o,withWhere:i,select:s,orderBy:n={column:"id",direction:"asc"},limit:l=10,offset:a=0,page:h,groupBy:c,having:p,distinct:u,join:y,leftJoin:d,rightJoin:f,innerJoin:g,count:W=!1}=t;let w=this.getQueryBuilder(e);s&&(Array.isArray(s)||"string"==typeof s)?w.select(s):w.select("*"),u&&(Array.isArray(u)||"string"==typeof u?w.distinct(u):w.distinct()),y&&this._applyJoins(w,y,"join"),d&&this._applyJoins(w,d,"leftJoin"),f&&this._applyJoins(w,f,"rightJoin"),g&&this._applyJoins(w,g,"innerJoin"),this._applyWhereClause(w,r,o),c&&(Array.isArray(c),w.groupBy(c)),p&&this._applyHavingClause(w,p),n&&this._applyOrderBy(w,n);let b=!1,m=l,_=a,A=1,j=0;h&&l>0&&(A=Math.max(1,parseInt(h)),_=(A-1)*l),l>0&&(w.limit(m),_>0&&w.offset(_));const k=[],C=w.toSQL();k.push(C.sql);const J=await w;if(o&&o.length>0){const t=this.buildWithTree(o);for(const r of Object.keys(t))await this.fetchAndAttachRelated({parentRows:J,relName:r,model:e,withTree:t[r],relation:e.hasRelations[r],withWhere:i||{}})}let R=null;if(l>0)try{let t=this.getQueryBuilder(e);r&&Object.keys(r).length>0&&this._applyWhereClause(t,r),y&&this._applyJoins(t,y,"join"),d&&this._applyJoins(t,d,"leftJoin"),f&&this._applyJoins(t,f,"rightJoin"),g&&this._applyJoins(t,g,"innerJoin");const o=t.count("* as cnt");k.push(o.toSQL().sql);R=(await o.first()).cnt}catch(e){logger.warn("Failed to get total count:",e.message),R=J.length}l>0&&null!==R&&(j=Math.ceil(R/l),b=A<j);const B=b?A+1:null,N=A>1?A-1:null;return{data:J,totalCount:R,...this.controllerWrapper.debug?{sqlDebug:k}:{},...l>0?{pagination:{page:A,limit:m,offset:_,totalPages:j,hasNext:b,hasPrev:A>1,nextPage:B,prevPage:N}}:{}}}catch(t){throw logger.error("QueryService.getQuery error:",t),new Error(`Failed to execute query: ${t.message} on model ${e.name}`)}}getSumQuery(e,t){const{where:r={},join:o,leftJoin:i,rightJoin:s,innerJoin:n}=t,l=t.data||t,a=l.sumColumn,h=l.sumFormula;if(!a&&!h)throw new Error("Sum action requires either data.sumColumn or data.sumFormula");const c=e=>"`"+String(e).replace(/[`\\]/g,"")+"`";let p;if(a){const e=String(a).trim().replace(/[^a-zA-Z0-9_]/g,"");if(!e)throw new Error("data.sumColumn must be a valid column name");p="SUM("+c(e)+")"}else{const e=String(h).trim();if(!/\{[a-zA-Z0-9_]+\}/.test(e))throw new Error("data.sumFormula must contain at least one {columnName}");let t=0;for(const r of e)if("("===r)t++;else if(")"===r&&(t--,t<0))break;if(0!==t)throw new Error("data.sumFormula has unbalanced or misordered parentheses ( and )");const r=e.replace(/\{[a-zA-Z0-9_]+\}/g,"@");if(!/^[\s0-9.+*\/\-()@]+$/.test(r))throw new Error("data.sumFormula may only use BODMAS: numbers, + - * /, parentheses, and {columnName}");p="SUM("+e.replace(/\{([a-zA-Z0-9_]+)\}/g,(e,t)=>c(t))+")"}let u=this.getQueryBuilder(e);return o&&this._applyJoins(u,o,"join"),i&&this._applyJoins(u,i,"leftJoin"),s&&this._applyJoins(u,s,"rightJoin"),n&&this._applyJoins(u,n,"innerJoin"),this._applyWhereClause(u,r,[]),u.select(this.db.raw(p+" as sum")),u}_applyWhereWithArray(e,t,r){for(const o of t)this._applyWhereClause(e,o,r)}_applyWhereClause(e,t,r=[]){if(Array.isArray(t))this._applyWhereWithArray(e,t,r);else{if(t&&Object.keys(t).length>0){let r=this.helperUtility.getDotWalkQuery(t);logger.debug({filteredWhere:r}),r=this.helperUtility.objectFilter(r,(e,t)=>!e.startsWith("!")&&("__exists__"!==e&&("object"!=typeof t||null===t)));for(const[t,o]of Object.entries(r)){const{joinType:r="AND",column:i}=this.parseWhereColumn(t),{operator:s,value:n}=this.parseWhereValue(o);"AND"===r?this._applyAndWhereCondition(e,i,s,n):this._applyOrWhereCondition(e,i,s,n)}}this._applyNestedWhere(e,t,r)}}_getNestedWhereConditions(e,t){if(!e||"object"!=typeof e)return{};const r=`${t}.`,o={};for(const[i,s]of Object.entries(e))if(i.startsWith(r)){o[i.slice(r.length)]=s}else i===t&&!0===s&&(o.__exists__=!0);return o}_getTopLevelRelationsFromWhere(e){if(!e||"object"!=typeof e)return[];const t=new Set;for(const r of Object.keys(e))if(r.includes(".")){const e=r.split(".")[0];t.add(e)}else r.startsWith("!")&&t.add(r);return[...t]}_applyNestedWhere(e,t,r){let o=this,i=e._getMyModel();const s=this._getTopLevelRelationsFromWhere(t);if(0!==s.length)for(let r of s){let s=r.startsWith("!"),n=s?r.slice(1):r,l=this._getNestedWhereConditions(t,r);if(0===Object.keys(l).length)continue;let a,h=i.hasRelations?.[n];if(!h){logger.warn(`Relation ${n} not found in model ${i.modelName}`);continue}try{a=this.utils.getModel(this.controllerWrapper,h.table||n)}catch(e){try{a=this.utils.getModel(this.controllerWrapper,n)}catch(e){logger.warn(`Model for relation ${n} (table: ${h.table}) not found`);continue}}e[s?"whereNotExists":"whereExists"](function(){let e=o.getQueryBuilder(a,this.select("*").from(h.table));e.whereRaw(`\`${h.table}\`.\`${h.foreignKey}\` = \`${i.table}\`.\`${h.localKey}\``);if(!(!0===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)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 +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){let r=await this.getQuery(e,{...t,limit:1,offset:0});return r.data.length>0?r.data[0]:null}async executeCountQuery(e,t){let 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),i=u&&u.name?u.name:"id",n=await this.db(e.table).where(i,a).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).update(t.data)}async executeReplaceQuery(e,t){return await this.db(e.table).replace(t.data)}async executeSyncQuery(e,t){return{insertOrUpdateQuery:await this.db(e.table).insert(t.data).onConflict(t.conflict).update(t.data),deleteQuery:await this.db(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}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 +1 @@
1
- const CurdTable=require("./CurdTable"),HelperUtility=require("./HelperUtility"),logger=require("../../Logger");class SyncTable{constructor(e,t,a=null){this.db=e,this.utils=t,this.controllerWrapper=a,this.curd=new CurdTable(e,t,a),this.helperUtility=new HelperUtility}_getClientName(){return"mysql"}async executeSql(e,t=[]){return this.db.raw(e,t)}async existsTable(e){return this.db.schema.hasTable(e)}async getCurrentColumns(e){const t=this.db.client.database(),[a]=await this.executeSql("\n SELECT \n c.COLUMN_NAME,\n c.DATA_TYPE,\n c.COLUMN_TYPE,\n c.IS_NULLABLE,\n c.COLUMN_KEY,\n c.EXTRA,\n c.CHARACTER_MAXIMUM_LENGTH,\n c.COLUMN_DEFAULT,\n c.COLUMN_COMMENT AS COMMENT,\n kcu.REFERENCED_TABLE_NAME,\n kcu.REFERENCED_COLUMN_NAME\n FROM information_schema.COLUMNS c\n LEFT JOIN information_schema.KEY_COLUMN_USAGE kcu\n ON c.TABLE_SCHEMA = kcu.TABLE_SCHEMA\n AND c.TABLE_NAME = kcu.TABLE_NAME\n AND c.COLUMN_NAME = kcu.COLUMN_NAME\n WHERE c.TABLE_SCHEMA = ?\n AND c.TABLE_NAME = ?\n ",[t,e]),n={};for(const e of a)n[e.COLUMN_NAME]=this.utils.formatColumnDef(e.COLUMN_NAME,e);return n}hasColumnChanged(e,t){const a={isNullableChanged:e.nullable!==t.nullable,isTypeChanged:e.type!==t.type,isSizeChanged:e.size!==t.size,isUnsignedChanged:e.isUnsigned!==t.isUnsigned,isPrimaryChanged:e.primary!==t.primary,isUniqueChanged:e.unique!==t.unique,isAutoIncrementChanged:e.autoIncrement!==t.autoIncrement,isDefaultChanged:e.default!==t.default,isOnUpdateChanged:e.onUpdate!==t.onUpdate,isCommentChanged:e.comment!==t.comment,isForeignKeyChanged:e.hasForeignKey!==t.hasForeignKey},n=Object.values(a).some(Boolean);return n&&logger.debug({changes:a,oldComment:e.comment,newComment:t.comment,name:e.name}),n}async getAlterations(e){const t={add:[],drop:[],modify:[]},a=await this.getCurrentColumns(e.table);for(const[n,s]of Object.entries(e.columns)){const e=this.utils.formatColumnSchema(n,s),o=a[n];o?(e.oldColDef=o,this.hasColumnChanged(o,e)&&t.modify.push(e)):t.add.push(e)}for(const n of Object.keys(a))e.columns[n]||t.drop.push({name:n});return t}getColumnStr(e,t,a={actionType:"CREATE",tableName:""}){const{actionType:n,tableName:s}=a,o="string"==typeof t?this.utils.formatColumnSchema(e,t):t,i=o.type,r=o.size??this.utils.getDefaultTypeSize(i);let l=`\`${e}\` ${i}${r?`(${r})`:""}`;if(o.isUnsigned&&(l+=" UNSIGNED"),o.primary&&["CREATE","ADD_COLUMN"].includes(n)&&(l+=" PRIMARY KEY"),o.autoIncrement&&(l+=" AUTO_INCREMENT"),o.nullable||(l+=" NOT NULL"),o.unique&&(l+=" UNIQUE"),o.default&&(l+=` DEFAULT ${o.default}`),o.onUpdate&&(l+=` ON UPDATE ${o.onUpdate}`),o.comment&&(l+=` COMMENT '${this.utils.escapeComment(o.comment)}'`),o.hasForeignKey&&1===o.foreignMapTables?.length&&"CREATE"===n){const{table:t,column:a}=o.foreignMapTables[0],n=`idx_${s}__${e}__fk_${t}_${a}`;l+=`, KEY \`${n}\` (\`${e}\`), CONSTRAINT \`cn_${n}\`\n FOREIGN KEY (\`${e}\`) REFERENCES \`${t}\` (\`${a}\`)\n ON DELETE RESTRICT ON UPDATE RESTRICT`}return l}async createTable(e){const t=e.table,a=e.columns,n=[];for(const[e,s]of Object.entries(a))n.push(this.getColumnStr(e,s,{actionType:"CREATE",tableName:t}));const s=`CREATE TABLE IF NOT EXISTS \`${t}\` (${n.join(", ")})`;await this.executeSql(s)}async alterTable(e,t){if(!t||"object"!=typeof t)throw new Error("alterations must be an object");const a=[];if(t.add?.length)for(const n of t.add)a.push(`ADD COLUMN ${this.getColumnStr(n.name,n,{actionType:"ADD_COLUMN",tableName:e})}`);if(t.drop?.length)for(const e of t.drop)a.push(`DROP COLUMN \`${e.name}\``);if(t.modify?.length)for(const n of t.modify)a.push(`MODIFY COLUMN ${this.getColumnStr(n.name,n,{actionType:"MODIFY_COLUMN",tableName:e})}`);if(!a.length)return void logger.info("No alterations to apply for",e);const n=`ALTER TABLE \`${e}\` ${a.join(", ")}`;await this.executeSql(n)}async alterColumn(e,t,a){const n=`ALTER TABLE \`${e}\` MODIFY COLUMN ${this.getColumnStr(t,a,{actionType:"MODIFY_COLUMN",tableName:e})}`;await this.executeSql(n)}async alterIndex(e,t,a){const n=`ALTER TABLE \`${e}\` MODIFY INDEX ${`${t} ${a.type} ${a.unique?"UNIQUE":""}`}`;await this.executeSql(n)}async dropIndex(e,t){const a=`DROP INDEX \`${t}\` ON \`${e}\``;await this.executeSql(a)}async dropTable(e){const t=`DROP TABLE IF EXISTS \`${e}\``;await this.executeSql(t)}async updateTable(e){logger.warn(`Update table not implemented for ${e.table}`)}async syncTable(e){if(await this.existsTable(e.table)){const t=await this.getAlterations(e);return logger.debug(...Object.values(t)),void await this.alterTable(e.table,t)}await this.createTable(e)}async syncSeedData(e,t){let a=this.utils.getModel(this.controllerWrapper,t),n=await this.curd.processRequest({action:"count"},a.name,{isCallFromServer:!0});if(logger.debug("count",n),n>0)logger.info("Seed data already synced for",t);else if(e.seed&&Array.isArray(e.seed)){for(const t of e.seed)await this.curd.processRequest({action:"create",data:t},a.name,{isCallFromServer:!0});logger.info("Seed data synced for",t)}}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 getTablesOfDatabase(){const[e]=await this.db.raw("SHOW TABLES");return e.map(e=>e[`Tables_in_${this.db.client.database()}`])}getColumnString(e){return Object.keys(e).reduce((t,a)=>{const n=e[a];let s=n.type,o=n.size,i=n.isUnsigned,r=n.primary,l=n.autoIncrement,c=n.nullable,u=n.unique,m=n.default,d=n.onUpdate,E=n.comment,h=n.hasForeignKey,C=n.foreignMapTables?.[0]?.table,T=n.foreignMapTables?.[0]?.column;return t[a]=`${s}`,o&&(t[a]+=`|size:${o}`),i&&(t[a]+="|unsigned"),r&&(t[a]+="|primaryKey"),l&&(t[a]+="|autoIncrement"),c&&(t[a]+="|nullable"),u&&(t[a]+="|unique"),m&&(t[a]+=`|default:${m}`),d&&(t[a]+=`|onUpdate:${d}`),E&&(t[a]+=`|comment:${E}`),h&&(t[a]+=`|foreignKey:${C}:${T}`),t},{})}async getRelations(e){await this.db.raw(`SHOW CREATE TABLE ${e}`);return{}}async generateSchema(){const e=await this.getTablesOfDatabase(),t={};for(const a of e){let e=this.helperUtility.modelName(a),n=await this.getRelations(a);t[e]={table:a,alias:e,columns:this.getColumnString(await this.getCurrentColumns(a)),modelName:e,seed:[],hasRelations:n,indexes:[]}}return t}}module.exports=SyncTable;
1
+ const BaseSyncTable=require("../BaseSyncTable"),logger=require("../../Logger");class MySQLSyncTable extends BaseSyncTable{_getClientName(){return"mysql"}async _listTables(){const e=this.db.client.database(),[t]=await this.db.raw("SHOW TABLES");return t.map(t=>t[`Tables_in_${e}`]).filter(Boolean)}async _applyExtras(e){for(const[t,a]of Object.entries(e.columns)){const l="string"==typeof a?this.utils.formatColumnSchema(t,a):a;if(!l.onUpdate)continue;const s=l.columnType||(l.size?`${l.type}(${l.size})`:l.type),n=l.nullable?"NULL":"NOT NULL",r=null!=l.default&&""!==l.default?` DEFAULT ${this._renderRawDefault(l.default)}`:"",o=`ALTER TABLE \`${e.table}\` MODIFY COLUMN \`${t}\` ${s} ${n}${r} ON UPDATE ${l.onUpdate}`;await this.db.raw(o)}}_renderRawDefault(e){const t=String(e).trim();return/^current_timestamp$/i.test(t)||/^now\(\)$/i.test(t)?"CURRENT_TIMESTAMP":/^-?\d+(\.\d+)?$/.test(t)?t:/^(true|false)$/i.test(t)?t.toUpperCase():`'${t.replace(/'/g,"''")}'`}async _getRelations(e){const t=this.db.client.database(),a=await this.db("information_schema.KEY_COLUMN_USAGE").where({TABLE_SCHEMA:t,TABLE_NAME:e}).whereNotNull("REFERENCED_TABLE_NAME").select("COLUMN_NAME AS local_column","REFERENCED_TABLE_NAME AS ref_table","REFERENCED_COLUMN_NAME AS ref_column"),l={};for(const e of a)l[e.local_column]={one:{table:e.ref_table,column:e.ref_column}};return l}async getTablesOfDatabase(){return this._listTables()}async executeSql(e,t=[]){return logger.debug("executeSql (legacy)",{sql:e,params:t}),this.db.raw(e,t)}}module.exports=MySQLSyncTable;
@@ -1 +1 @@
1
- const{getDefaultTypeSize:getDefaultTypeSize,getDbType:getDbType}=require("./DataTypeMap");class BaseUtility{isInternalDefault(e){if("string"!=typeof e)return!1;const t=/^CURRENT_TIMESTAMP$/i.test(e),n=/^[a-zA-Z_][a-zA-Z0-9_]*\s*\(.*\)$/.test(e),r=/::[a-zA-Z0-9_]+/.test(e);return t||n||r}getModel(e,t){let n=e?.schema,r=null,l=Object.keys(n);if(l.forEach(e=>{e===t&&(r=n[e])}),r||l.forEach(e=>{let l=n[e];l.table===t&&(r=l)}),r)return{...r,name:t,columns:Object.keys(r.columns).map(e=>{let t=r.columns[e];return this.parseColumnString(e,t)})};throw new Error(`Model ${t} not found`)}map2DbDefault(e){return"now"===e?"CURRENT_TIMESTAMP":e}getCollenedValue(e,{splitChar:t=":",give:n="right",trimWrap:r="{}"}={}){if(null==e)return null;const l=String(e),i=l.indexOf(t),a=e=>{if(!r)return e;if("string"==typeof r&&2===r.length){const[t,n]=r;return e.startsWith(t)&&e.endsWith(n)?e.slice(1,-1):e}return"string"==typeof r&&r.length&&e.startsWith(r)&&e.endsWith(r)?e.slice(r.length,-r.length):e};if(i>=0){const e=l.slice(0,i).trim(),r=l.slice(i+t.length).trim();return"both"===n?{left:a(e),right:a(r)}:a("left"===n?e:r)}return a(l)}parseForeignKey(e){if(!e)return{foreignMapTables:[]};const t=this.getCollenedValue(e),[n,r="id"]=String(t||"").split(":"),l=(this.getCollenedValue(n)||"").split(",").filter(Boolean),i=(this.getCollenedValue(r)||"").split(",").filter(Boolean);return{foreignMapTables:l.map((e,t)=>({table:e,column:i[t]||i[0]||"id"}))}}parseColumnString(e,t){const[n,...r]=String(t).split("|"),l=e=>r.find(t=>t.startsWith(e+":")),i=e=>r.join("|").includes(e),a=e=>e&&e.includes(":")?e.slice(e.indexOf(":")+1):null,s=l("default"),u=l("onUpdate"),o=l("comment"),g=l("foreignKey"),p=l("size"),T=this.parseForeignKey(g),E=!!g,m=getDbType(n,p?a(p):null),c=p?a(p):getDefaultTypeSize(m);return{name:e,type:m,size:+c,isUnsigned:i("unsigned")||i("primaryKey")||E,columnType:`${n}${c?`(${c})`:""}`,nullable:!i("notNull"),primary:i("primaryKey"),autoIncrement:i("autoIncrement"),unique:i("unique"),default:this.map2DbDefault(a(s)),onUpdate:this.map2DbDefault(a(u)),comment:a(o)||"",hasForeignKey:E,...T}}formatColumnSchema(e,t){return this.parseColumnString(e,t)}formatColumnDef(e,t){return{name:e,type:t.DATA_TYPE,size:+(t.CHARACTER_MAXIMUM_LENGTH||getDefaultTypeSize(t.DATA_TYPE)),isUnsigned:String(t.COLUMN_TYPE||"").toLowerCase().includes("unsigned"),columnType:t.COLUMN_TYPE,nullable:"YES"===t.IS_NULLABLE,primary:"PRIMARY KEY"===t.CONSTRAINT_TYPE,unique:"UNIQUE"===t.CONSTRAINT_TYPE,autoIncrement:String(t.EXTRA||"").toLowerCase().includes("auto_increment"),hasForeignKey:"FOREIGN KEY"===t.CONSTRAINT_TYPE,comment:t.COMMENT||"",default:t.COLUMN_DEFAULT,onUpdate:((e="")=>{const t=String(e).match(/on update\s+([a-zA-Z_]+)/i);return t?t[1]:null})(t.EXTRA),foreignMapTables:"FOREIGN KEY"===t.CONSTRAINT_TYPE&&t.REFERENCED_TABLE_NAME?[{table:t.REFERENCED_TABLE_NAME,column:t.REFERENCED_COLUMN_NAME}]:[]}}escapeComment(e){return null==e?"":String(e).replace(/'/g,"\\'")}getDefaultTypeSize(e){return getDefaultTypeSize(e)}}module.exports=BaseUtility;
1
+ const{getDefaultTypeSize:getDefaultTypeSize,getDbType:getDbType}=require("./DataTypeMap"),KormError=require("../../KormError");class BaseUtility{isInternalDefault(e){if("string"!=typeof e)return!1;const t=/^CURRENT_TIMESTAMP$/i.test(e),n=/^[a-zA-Z_][a-zA-Z0-9_]*\s*\(.*\)$/.test(e),r=/::[a-zA-Z0-9_]+/.test(e);return t||n||r}getModel(e,t){const n=e?.schema;let r=null;const i=Object.keys(n);if(i.forEach(e=>{e===t&&(r=n[e])}),r||i.forEach(e=>{const i=n[e];i.table===t&&(r=i)}),r)return{...r,name:t,columns:Object.keys(r.columns).map(e=>{const t=r.columns[e];return this.parseColumnString(e,t)})};throw KormError.unknownModel({model:t,available:Object.keys(n||{})})}map2DbDefault(e){return"now"===e?"CURRENT_TIMESTAMP":e}getCollenedValue(e,{splitChar:t=":",give:n="right",trimWrap:r="{}"}={}){if(null==e)return null;const i=String(e),l=i.indexOf(t),a=e=>{if(!r)return e;if("string"==typeof r&&2===r.length){const[t,n]=r;return e.startsWith(t)&&e.endsWith(n)?e.slice(1,-1):e}return"string"==typeof r&&r.length&&e.startsWith(r)&&e.endsWith(r)?e.slice(r.length,-r.length):e};if(l>=0){const e=i.slice(0,l).trim(),r=i.slice(l+t.length).trim();return"both"===n?{left:a(e),right:a(r)}:a("left"===n?e:r)}return a(i)}parseForeignKey(e){if(!e)return{foreignMapTables:[]};const t=this.getCollenedValue(e),[n,r="id"]=String(t||"").split(":"),i=(this.getCollenedValue(n)||"").split(",").filter(Boolean),l=(this.getCollenedValue(r)||"").split(",").filter(Boolean);return{foreignMapTables:i.map((e,t)=>({table:e,column:l[t]||l[0]||"id"}))}}parseColumnString(e,t){const[n,...r]=String(t).split("|"),i=e=>r.find(t=>t.startsWith(e+":")),l=e=>r.join("|").includes(e),a=e=>e&&e.includes(":")?e.slice(e.indexOf(":")+1):null,s=i("default"),o=i("onUpdate"),u=i("comment"),m=i("foreignKey"),g=i("size"),p=this.parseForeignKey(m),E=!!m,T=getDbType(n,g?a(g):null),c=g?a(g):getDefaultTypeSize(T);return{name:e,type:T,size:+c,isUnsigned:l("unsigned")||l("primaryKey")||E,columnType:`${n}${c?`(${c})`:""}`,nullable:!l("notNull"),primary:l("primaryKey"),autoIncrement:l("autoIncrement"),unique:l("unique"),default:this.map2DbDefault(a(s)),onUpdate:this.map2DbDefault(a(o)),comment:a(u)||"",hasForeignKey:E,...p}}formatColumnSchema(e,t){return this.parseColumnString(e,t)}formatColumnDef(e,t){return{name:e,type:t.DATA_TYPE,size:+(t.CHARACTER_MAXIMUM_LENGTH||getDefaultTypeSize(t.DATA_TYPE)),isUnsigned:String(t.COLUMN_TYPE||"").toLowerCase().includes("unsigned"),columnType:t.COLUMN_TYPE,nullable:"YES"===t.IS_NULLABLE,primary:"PRIMARY KEY"===t.CONSTRAINT_TYPE,unique:"UNIQUE"===t.CONSTRAINT_TYPE,autoIncrement:String(t.EXTRA||"").toLowerCase().includes("auto_increment"),hasForeignKey:"FOREIGN KEY"===t.CONSTRAINT_TYPE,comment:t.COMMENT||"",default:t.COLUMN_DEFAULT,onUpdate:((e="")=>{const t=String(e).match(/on update\s+([a-zA-Z_]+)/i);return t?t[1]:null})(t.EXTRA),foreignMapTables:"FOREIGN KEY"===t.CONSTRAINT_TYPE&&t.REFERENCED_TABLE_NAME?[{table:t.REFERENCED_TABLE_NAME,column:t.REFERENCED_COLUMN_NAME}]:[]}}escapeComment(e){return null==e?"":String(e).replace(/'/g,"\\'")}getDefaultTypeSize(e){return getDefaultTypeSize(e)}}module.exports=BaseUtility;