@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.
- package/BaseHelperUtility.js +1 -1
- package/ControllerWrapper.js +1 -1
- package/Emitter.js +1 -1
- package/KormError.js +1 -0
- package/README.md +463 -254
- package/RequestValidator.js +1 -1
- package/ai-skills/korm-js.md +265 -0
- package/bin/korm-mcp.js +2 -0
- package/build.js +1 -1
- package/cli.js +2 -0
- package/clients/BaseSyncTable.js +1 -0
- package/clients/mysql/BaseUtility.js +1 -1
- package/clients/mysql/CurdTable.js +1 -1
- package/clients/mysql/DataTypeMap.js +1 -1
- package/clients/mysql/HookService.js +1 -1
- package/clients/mysql/QueryBuilder.js +1 -1
- package/clients/mysql/QueryService.js +1 -1
- package/clients/mysql/SyncTable.js +1 -1
- package/clients/pg/BaseUtility.js +1 -1
- package/clients/pg/CurdTable.js +1 -1
- package/clients/pg/DataTypeMap.js +1 -1
- package/clients/pg/HookService.js +1 -1
- package/clients/pg/QueryBuilder.js +1 -1
- package/clients/pg/QueryService.js +1 -1
- package/clients/pg/SyncTable.js +1 -1
- package/clients/sqlite/BaseUtility.js +1 -1
- package/clients/sqlite/CurdTable.js +1 -1
- package/clients/sqlite/HookService.js +1 -1
- package/clients/sqlite/QueryBuilder.js +1 -1
- package/clients/sqlite/QueryService.js +1 -1
- package/clients/sqlite/SyncTable.js +1 -1
- package/columnSchema.js +1 -0
- package/helpers/files.js +1 -1
- package/index.d.ts +213 -0
- package/index.js +1 -1
- package/jest.config.js +1 -1
- package/package.json +13 -4
- package/requestSchema.js +1 -0
- package/schemaDescribe.js +1 -0
- package/src/mcp/errors.js +1 -0
- package/src/mcp/schemaIntrospect.js +1 -0
- package/src/mcp/server.js +1 -0
- package/src/mcp/toolGenerator.js +1 -0
- package/TableSchemaSync.js +0 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dreamtree-org/korm-js",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.55",
|
|
4
4
|
"description": "Knowledge Object-Relational Mapping - A powerful, modular ORM system for Node.js with dynamic database operations, complex queries, relationships, and nested requests",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Partha Preetham Krishna",
|
|
@@ -9,20 +9,26 @@
|
|
|
9
9
|
},
|
|
10
10
|
"main": "index.js",
|
|
11
11
|
"types": "index.d.ts",
|
|
12
|
+
"bin": {
|
|
13
|
+
"korm-js": "./cli.js",
|
|
14
|
+
"korm-mcp": "./bin/korm-mcp.js"
|
|
15
|
+
},
|
|
12
16
|
"scripts": {
|
|
13
17
|
"test": "jest",
|
|
14
18
|
"test:watch": "jest --watch",
|
|
15
19
|
"test:coverage": "jest --coverage",
|
|
16
20
|
"test:all": "jest --coverage --verbose",
|
|
21
|
+
"lint": "eslint . --max-warnings=0",
|
|
22
|
+
"lint:fix": "eslint . --fix",
|
|
23
|
+
"format": "prettier --write .",
|
|
24
|
+
"format:check": "prettier --check .",
|
|
25
|
+
"build:release": "npm run clean && npm run minify",
|
|
17
26
|
"version:show": "node version-manager.js show",
|
|
18
27
|
"version:current": "node version-manager.js current",
|
|
19
28
|
"version:next": "node version-manager.js next",
|
|
20
29
|
"version:auto": "node version-manager.js auto",
|
|
21
30
|
"version:smart": "node version-manager.js smart",
|
|
22
31
|
"version:update": "node version-manager.js update",
|
|
23
|
-
"publish:patch": "npm run version:update patch && npm run build && npm publish",
|
|
24
|
-
"publish:minor": "npm run version:update minor && npm run build && npm publish",
|
|
25
|
-
"publish:major": "npm run version:update major && npm run build && npm publish",
|
|
26
32
|
"docs": "echo \"Documentation available in doc/ directory\""
|
|
27
33
|
},
|
|
28
34
|
"keywords": [
|
|
@@ -62,6 +68,9 @@
|
|
|
62
68
|
"peerDependencies": {
|
|
63
69
|
"knex": "^3.0.0"
|
|
64
70
|
},
|
|
71
|
+
"optionalDependencies": {
|
|
72
|
+
"@modelcontextprotocol/sdk": "^1.0.0"
|
|
73
|
+
},
|
|
65
74
|
"directories": {
|
|
66
75
|
"doc": "Documentation"
|
|
67
76
|
},
|
package/requestSchema.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"use strict";const{parseColumnDef:parseColumnDef,isWritableOnCreate:isWritableOnCreate,isRequiredOnCreate:isRequiredOnCreate,applyNullability:applyNullability}=require("./columnSchema"),READ_ACTIONS=new Set(["list","show","count","sum"]),WRITE_ACTIONS=new Set(["create","update","delete","replace","upsert","sync"]),KNOWN_ACTIONS=new Set([...READ_ACTIONS,...WRITE_ACTIONS]);function buildColumnsMap(e){const t={},r=e&&e.columns||{};for(const[e,i]of Object.entries(r))t[e]=parseColumnDef(i);return t}function buildDataSchemaForCreate(e){const t={},r=[];for(const[i,o]of Object.entries(e))isWritableOnCreate(o)&&(t[i]=applyNullability(o.jsonSchema,o),isRequiredOnCreate(o)&&r.push(i));const i={type:"object",properties:t,additionalProperties:!1};return r.length&&(i.required=r),i}function buildDataSchemaForUpdate(e){const t={};for(const[r,i]of Object.entries(e))isWritableOnCreate(i)&&(t[r]=applyNullability(i.jsonSchema,i));return{type:"object",properties:t,additionalProperties:!1}}function buildWhereSchema(e){const t={};for(const r of Object.keys(e))t[r]={};return{type:"object",properties:t,additionalProperties:!0}}function buildSelectSchema(e){const t=Object.keys(e);return{oneOf:[{type:"string"},{type:"array",items:t.length?{type:"string",enum:t}:{type:"string"}}]}}function buildOrderBySchema(e){const t=Object.keys(e),r={type:"object",properties:{column:t.length?{type:"string",enum:t}:{type:"string"},order:{type:"string",enum:["asc","desc","ASC","DESC"]}},required:["column"],additionalProperties:!1};return{oneOf:[{type:"string"},{type:"array",items:{type:"string"}},r,{type:"array",items:r}]}}function buildWithSchema(e){const t=Object.keys(e||{}),r={type:"string"};return t.length&&(r.description=`Top-level relations available: ${t.join(", ")}. Use dot-paths for deeper traversal, e.g. "${t[0]}.NestedRel".`),{type:"array",items:r}}function buildSumDataSchema(e){const t=Object.keys(e);return{type:"object",properties:{sumColumn:t.length?{type:"string",enum:t}:{type:"string"},sumFormula:{type:"string",description:"Arithmetic expression over column placeholders. Allowed chars: digits, . + - * / ( ), {column} placeholders, whitespace. See docs/agents/06-request-contract.md §5."}},additionalProperties:!1}}function buildConflictSchema(e){const t=Object.keys(e);return{type:"array",items:t.length?{type:"string",enum:t}:{type:"string"},minItems:1}}function commonReadSelectors(e,t){return{where:buildWhereSchema(e),select:buildSelectSchema(e),orderBy:buildOrderBySchema(e),limit:{type:"integer",minimum:1},offset:{type:"integer",minimum:0},page:{type:"integer",minimum:1},with:buildWithSchema(t),withWhere:buildWhereSchema(e),groupBy:{oneOf:[{type:"string"},{type:"array",items:{type:"string"}}]},having:buildWhereSchema(e),distinct:{oneOf:[{type:"boolean"},{type:"string"},{type:"array",items:{type:"string"}}]}}}const ACTION_BUILDERS={list:(e,t)=>({type:"object",properties:commonReadSelectors(e,t),additionalProperties:!1}),show:(e,t)=>({type:"object",properties:{where:buildWhereSchema(e),select:buildSelectSchema(e),with:buildWithSchema(t),withWhere:buildWhereSchema(e)},required:["where"],additionalProperties:!1}),count:e=>({type:"object",properties:{where:buildWhereSchema(e),distinct:{oneOf:[{type:"boolean"},{type:"string"},{type:"array",items:{type:"string"}}]}},additionalProperties:!1}),sum:e=>({type:"object",properties:{data:buildSumDataSchema(e),where:buildWhereSchema(e),groupBy:{oneOf:[{type:"string"},{type:"array",items:{type:"string"}}]}},required:["data"],additionalProperties:!1}),create(e){const t=buildDataSchemaForCreate(e);return{type:"object",properties:{data:{oneOf:[t,{type:"array",items:t,minItems:1}]}},required:["data"],additionalProperties:!1}},update:e=>({type:"object",properties:{where:buildWhereSchema(e),data:buildDataSchemaForUpdate(e)},required:["where","data"],additionalProperties:!1}),delete:e=>({type:"object",properties:{where:buildWhereSchema(e)},required:["where"],additionalProperties:!1}),replace(e){const t=buildDataSchemaForCreate(e);return{type:"object",properties:{data:{oneOf:[t,{type:"array",items:t,minItems:1}]}},required:["data"],additionalProperties:!1}},upsert(e){const t=buildDataSchemaForCreate(e);return{type:"object",properties:{data:{oneOf:[t,{type:"array",items:t,minItems:1}]},conflict:buildConflictSchema(e)},required:["data","conflict"],additionalProperties:!1}},sync(e){const t=buildDataSchemaForCreate(e);return{type:"object",properties:{data:{oneOf:[t,{type:"array",items:t,minItems:1}]},where:buildWhereSchema(e),conflict:buildConflictSchema(e)},required:["data","where","conflict"],additionalProperties:!1}}};function buildRequestSchema({action:e,model:t}){if(!t||"object"!=typeof t)throw new TypeError("buildRequestSchema: `model` is required");if(!KNOWN_ACTIONS.has(e))throw new RangeError(`buildRequestSchema: unknown action "${e}". Known: ${[...KNOWN_ACTIONS].join(", ")}`);const r=buildColumnsMap(t),i=t.hasRelations||{};return ACTION_BUILDERS[e](r,i)}function modelTitle(e){return e.modelName||e.alias||e.table||"Model"}function buildModelRequestSchema(e,t={}){if(!e||"object"!=typeof e)throw new TypeError("buildModelRequestSchema: `model` is required");const r=modelTitle(e),i=[...KNOWN_ACTIONS].map(t=>{const r=buildRequestSchema({action:t,model:e});return{type:"object",title:t,properties:{action:{type:"string",const:t,description:`The "${t}" operation.`},...r.properties,dryRun:{type:"boolean",description:"If true, return the SQL that would run without executing it."}},required:["action",...r.required||[]],additionalProperties:!1}});return{$schema:"https://json-schema.org/draft/2020-12/schema",title:t.title||`KormRequest<${r}>`,description:`Valid processRequest(body, "${r}") shapes. Exactly one action branch applies.`,oneOf:i}}module.exports={buildRequestSchema:buildRequestSchema,buildModelRequestSchema:buildModelRequestSchema,buildColumnsMap:buildColumnsMap,READ_ACTIONS:READ_ACTIONS,WRITE_ACTIONS:WRITE_ACTIONS,KNOWN_ACTIONS:KNOWN_ACTIONS};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"use strict";const{parseColumnDef:parseColumnDef}=require("./columnSchema"),{KNOWN_ACTIONS:KNOWN_ACTIONS}=require("./requestSchema"),SCHEMA_API_VERSION=1;function describeColumn(e,l){const{flags:n}=parseColumnDef(l),t=!0===n.nullable&&!0!==n.primaryKey,u={name:e,type:n.baseType||null,nullable:t,primaryKey:!0===n.primaryKey,autoIncrement:!0===n.autoIncrement,unique:!0===n.unique};return null!=n.size&&(u.size=n.size),n.hasDefault&&(u.default=n.defaultValue),u}function describeRelations(e){return Object.entries(e||{}).map(([e,l])=>{const n={name:e,type:l.type||null,table:l.table||null,localKey:l.localKey||null,foreignKey:l.foreignKey||null};return l.through&&(n.through=l.through,n.throughLocalKey=l.throughLocalKey||null,n.throughForeignKey=l.throughForeignKey||null),n})}function buildModelDescription(e,l,n={}){return{schemaApiVersion:1,model:e,table:l.table||null,alias:l.alias||e,columns:Object.entries(l.columns||{}).map(([e,l])=>describeColumn(e,l)),relations:describeRelations(l.hasRelations),softDelete:!0===n.softDelete,actions:[...KNOWN_ACTIONS]}}module.exports={buildModelDescription:buildModelDescription,describeColumn:describeColumn,describeRelations:describeRelations,SCHEMA_API_VERSION:1};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"use strict";class McpConfigError extends Error{constructor(r){super(r),this.name="McpConfigError"}}function sanitizeDbErrorMessage(r){return`${r&&r.code?`[${r.code}] `:""}${(r&&r.message?String(r.message):"Unknown error").split("\n",1)[0]}`.slice(0,500)}function toolErrorResult(r){return{isError:!0,content:[{type:"text",text:r&&("ValidationError"===r.name||!0===r.isValidation)?`Validation failed: ${r.message}`:`Tool execution failed: ${sanitizeDbErrorMessage(r)}`}]}}function toolSuccessResult(r){let e;try{e=JSON.stringify(r,null,2)}catch(t){e=String(r)}return{content:[{type:"text",text:e}]}}module.exports={McpConfigError:McpConfigError,sanitizeDbErrorMessage:sanitizeDbErrorMessage,toolErrorResult:toolErrorResult,toolSuccessResult:toolSuccessResult};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"use strict";const{toolErrorResult:toolErrorResult,toolSuccessResult:toolSuccessResult}=require("./errors"),{resolveTables:resolveTables}=require("./toolGenerator");function describeColumns(e){const o={};for(const[t,r]of Object.entries(e.columns||{}))o[t]=r;return o}function buildListTablesTool({schema:e,visibleTables:o}){return{name:"korm.list_tables",description:"List the tables (models) currently exposed by this MCP server, with their column counts.",inputSchema:{type:"object",properties:{},additionalProperties:!1},handler:async()=>{try{const t=o().map(o=>{const t=e[o];return{modelName:o,table:t.table,columnCount:Object.keys(t.columns||{}).length,relationCount:Object.keys(t.hasRelations||{}).length}});return toolSuccessResult({tables:t})}catch(e){return toolErrorResult(e)}}}}function buildDescribeSchemaTool({schema:e,visibleTables:o}){return{name:"korm.describe_schema",description:"Describe the columns and relations of a single allowlisted table.",inputSchema:{type:"object",properties:{table:{type:"string",description:'Model name (matches the key in the KORM schema, e.g. "User" — not the SQL table name).'}},required:["table"],additionalProperties:!1},handler:async t=>{try{const r=t&&t.table,s=o();if(!s.includes(r))return toolErrorResult(new Error(`Table "${r}" is not exposed by this MCP server. Available: ${s.join(", ")||"(none)"}.`));const n=e[r];return toolSuccessResult({modelName:r,table:n.table,columns:describeColumns(n),relations:n.hasRelations||{}})}catch(e){return toolErrorResult(e)}}}}async function pingDb(e){if(!e||!e.db||"function"!=typeof e.db.raw)return{ok:!1,error:"controller.db not present or not a Knex instance"};try{return await e.db.raw("SELECT 1"),{ok:!0}}catch(e){return{ok:!1,error:e.message}}}function buildHealthTool({controller:e,mcpConfig:o,packageInfo:t,visibleTables:r}){return{name:"korm.health",description:"Report MCP server health: engine, library version, allowlist size, DB ping.",inputSchema:{type:"object",properties:{},additionalProperties:!1},handler:async()=>{try{const s=e&&(e.dbClient||e.engine)||"unknown",n=r(),l=await pingDb(e);return toolSuccessResult({status:l.ok?"ok":"degraded",engine:s,version:t.version||"unknown",mode:o.mode,allowedTableCount:n.length,dbPing:l.ok,...l.error?{dbError:l.error}:{}})}catch(e){return toolErrorResult(e)}}}}function buildMetaTools({controller:e,schema:o,mcpConfig:t,packageInfo:r={}}){if(!1===t.metaTools)return[];const s=()=>resolveTables(o,t);return[buildListTablesTool({schema:o,visibleTables:s}),buildDescribeSchemaTool({schema:o,visibleTables:s}),buildHealthTool({controller:e,mcpConfig:t,packageInfo:r,visibleTables:s})]}module.exports={buildMetaTools:buildMetaTools};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"use strict";const{generateTools:generateTools}=require("./toolGenerator"),{buildMetaTools:buildMetaTools}=require("./schemaIntrospect"),{toolErrorResult:toolErrorResult}=require("./errors");function loadSdk(){try{const e=require("@modelcontextprotocol/sdk/server/index.js"),o=require("@modelcontextprotocol/sdk/server/stdio.js"),r=require("@modelcontextprotocol/sdk/types.js");return{Server:e.Server,StdioServerTransport:o.StdioServerTransport,CallToolRequestSchema:r.CallToolRequestSchema,ListToolsRequestSchema:r.ListToolsRequestSchema}}catch(e){throw new Error(`The @modelcontextprotocol/sdk package is required to start the KORM MCP server.\nInstall it with: npm install @modelcontextprotocol/sdk\nUnderlying require error: ${e.message}`)}}function buildAllTools({controller:e,schema:o,mcpConfig:r,packageInfo:t}){const l=[];return l.push(...buildMetaTools({controller:e,schema:o,mcpConfig:r,packageInfo:t})),l.push(...generateTools({controller:e,schema:o,mcpConfig:r})),l}function createServer({controller:e,schema:o,mcpConfig:r,packageInfo:t={}}){const l=buildAllTools({controller:e,schema:o,mcpConfig:r,packageInfo:t}),n=new Map(l.map(e=>[e.name,e]));let s=null,a=null;return{start:async function({logger:e=console}={}){const o=loadSdk();return s=new o.Server({name:t.name||"@dreamtree-org/korm-js mcp",version:t.version||"0.0.0"},{capabilities:{tools:{}}}),s.setRequestHandler(o.ListToolsRequestSchema,async()=>({tools:l.map(({name:e,description:o,inputSchema:r})=>({name:e,description:o,inputSchema:r}))})),s.setRequestHandler(o.CallToolRequestSchema,async o=>{const{name:r,arguments:t}=o.params||{},l=n.get(r);if(!l)return toolErrorResult(new Error(`Unknown tool: ${r}`));try{return await l.handler(t||{})}catch(o){return e.error&&e.error(`[korm-mcp] tool "${r}" threw:`,o),toolErrorResult(o)}}),a=new o.StdioServerTransport,await s.connect(a),s},stop:async function(){s&&"function"==typeof s.close&&await s.close(),s=null,a=null},tools:l,toolsByName:n}}module.exports={createServer:createServer,buildAllTools:buildAllTools};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"use strict";const{buildRequestSchema:buildRequestSchema,READ_ACTIONS:READ_ACTIONS,WRITE_ACTIONS:WRITE_ACTIONS}=require("../../requestSchema"),{McpConfigError:McpConfigError,toolErrorResult:toolErrorResult,toolSuccessResult:toolSuccessResult}=require("./errors"),VALID_MODES=new Set(["ro","rw","rw-sync"]);function actionsForMode(e){switch(e){case"ro":return new Set([...READ_ACTIONS]);case"rw":return new Set([...READ_ACTIONS,"create","update","delete","replace","upsert"]);case"rw-sync":return new Set([...READ_ACTIONS,...WRITE_ACTIONS]);default:throw new McpConfigError(`Invalid mcp.mode "${e}". Valid: ${[...VALID_MODES].join(", ")}.`)}}function resolveTables(e,o){const t=Object.keys(e||{}),r=o.allowlist,n=new Set(o.blocklist||[]);if(!r||"*"===r||Array.isArray(r)&&r.includes("*")){if("ro"!==o.mode)throw new McpConfigError(`mcp.allowlist of "*" is only permitted in mode "ro". Current mode: "${o.mode}".`);return t.filter(e=>!n.has(e))}if(!Array.isArray(r))throw new McpConfigError('mcp.allowlist must be an array of model names, or "*" (ro mode only).');const s=r.filter(o=>!e[o]);if(s.length)throw new McpConfigError(`mcp.allowlist references unknown model(s): ${s.join(", ")}`);return r.filter(e=>!n.has(e))}function toolNameFor(e,o){return`${e.table}.${o}`}function descriptionFor({modelName:e,action:o,model:t}){const r={list:`Paginated list of ${e} rows.`,show:`Fetch a single ${e} row matching \`where\`.`,count:`Count ${e} rows matching \`where\`.`,sum:`Sum a numeric column or arithmetic formula over ${e} rows.`,create:`Create one or more ${e} rows.`,update:`Update ${e} rows matching \`where\` with the supplied \`data\`.`,delete:`Delete ${e} rows matching \`where\`.`,replace:`Replace ${e} rows (insert-or-replace semantics).`,upsert:`Upsert ${e} rows; \`conflict\` lists the unique columns.`,sync:`Sync ${e} rows: insert/update from \`data\`, delete others matching \`where\`.`}[o]||`${o} on ${e}.`,n=Object.keys(t.hasRelations||{});return`${r}${n.length?` Available relations: ${n.join(", ")}.`:""}`}function buildHandler({controller:e,modelName:o,action:t,mcpConfig:r}){return async function(n){try{const s={action:t,...n||{}};s.other_requests&&!r.allowNestedRequests&&delete s.other_requests;const c=await e.processRequest(s,o,null);return toolSuccessResult(c)}catch(e){return toolErrorResult(e)}}}function validateCustomActionEntry(e,o){if(!e||"object"!=typeof e)throw new McpConfigError("mcp.customActions entries must be objects: { table, action, schema?, description? }");if(!e.table||!o[e.table])throw new McpConfigError(`mcp.customActions: unknown model "${e.table}"`);if("string"!=typeof e.action||!e.action)throw new McpConfigError('mcp.customActions: each entry needs a string "action"')}function buildCustomActionTools({controller:e,schema:o,mcpConfig:t}){return(t.customActions||[]).map(r=>{validateCustomActionEntry(r,o);const{table:n,action:s,schema:c,description:i}=r;return{name:`${o[n].table}.${s}`,description:i||`Custom action "${s}" on ${n}.`,inputSchema:c||{type:"object",additionalProperties:!0},handler:buildHandler({controller:e,modelName:n,action:s,mcpConfig:t})}})}function generateTools({controller:e,schema:o,mcpConfig:t}){if(!e||"function"!=typeof e.processRequest)throw new McpConfigError("generateTools: `controller` must expose processRequest(request, modelName, ctx)");if(!o||"object"!=typeof o)throw new McpConfigError("generateTools: `schema` is required");const r=t.mode||"ro",n=actionsForMode(r),s=resolveTables(o,{...t,mode:r}),c=[];for(const i of s){const s=o[i];for(const o of n)c.push({name:toolNameFor(s,o),description:descriptionFor({modelName:i,action:o,model:s}),inputSchema:buildRequestSchema({action:o,model:s}),handler:buildHandler({controller:e,modelName:i,action:o,mcpConfig:{...t,mode:r}})})}return c.push(...buildCustomActionTools({controller:e,schema:o,mcpConfig:{...t,mode:r}})),c}module.exports={generateTools:generateTools,actionsForMode:actionsForMode,resolveTables:resolveTables,toolNameFor:toolNameFor,VALID_MODES:VALID_MODES};
|
package/TableSchemaSync.js
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
const MySQLTableSync=require("./clients/mysql/tableSync"),PostgreSQLTableSync=require("./clients/pg/tableSync"),SQLiteTableSync=require("./clients/sqlite/tableSync");class TableSchemaSync{static _db=null;static _schemaTable=null;static setDb(e){TableSchemaSync._db=e}static setSchemaTable(e){TableSchemaSync._schemaTable=e}static getDb(){if(!TableSchemaSync._db)throw new Error("Database connection not set. Call TableSchemaSync.setDb(db) first.");return TableSchemaSync._db}static getSchemaTable(){if(!TableSchemaSync._schemaTable)throw new Error("Schema table not set. Call TableSchemaSync.setSchemaTable(schemaTable) first.");return TableSchemaSync._schemaTable}static getClientName(){try{return TableSchemaSync._db?.client?.config?.client||TableSchemaSync._db?.config?.client||""}catch{return""}}static getDatabaseSync(){const e=TableSchemaSync.getDb(),a=TableSchemaSync.getClientName();if(TableSchemaSync._isPostgres(a))return new PostgreSQLTableSync(e);if(TableSchemaSync._isMySQL(a))return new MySQLTableSync(e);if(TableSchemaSync._isSQLite(a))return new SQLiteTableSync(e);throw new Error(`Unsupported database client: ${a}`)}static mapTypeToDatabase(e){return TableSchemaSync.getDatabaseSync().mapTypeToDatabase(e)}static async getTableStructure(e){const a=TableSchemaSync.getDatabaseSync(),t=await a.getTableStructure(e);return null===t?{columns:[],primaryKeys:[],uniqueConstraints:[],indexes:[],foreignKeys:[]}:t}static _isPostgres(e){return e.includes("pg")||e.includes("postgres")}static _isMySQL(e){return e.includes("mysql")}static _isSQLite(e){return e.includes("sqlite")}static async tableExists(e){try{const a=TableSchemaSync.getDatabaseSync();return await a.tableExists(e)}catch(e){return!1}}static async createTable(e,a={}){const t=TableSchemaSync.getSchemaTable();TableSchemaSync.getDb(),TableSchemaSync.getClientName();if(!t[e])throw new Error(`Model "${e}" not found in schema table`);const c=t[e],n=c.table;if(await TableSchemaSync.tableExists(n)){if(a.ifNotExists)return!0;throw new Error(`Table "${n}" already exists`)}const s=c.columns||{},l={};for(const[e,a]of Object.entries(s)){const t=TableSchemaSync.mapTypeToDatabase(a);l[e]=t}const r=TableSchemaSync.getDatabaseSync();return await r.createTable(n,l,a),await r.addTableConstraints(n,c,a),!0}static async deleteTable(e,a={}){const t=TableSchemaSync.getSchemaTable(),c=TableSchemaSync.getDb();if(!t[e])throw new Error(`Model "${e}" not found in schema table`);const n=t[e].table;if(!await TableSchemaSync.tableExists(n)){if(a.ifExists)return!0;throw new Error(`Table "${n}" does not exist`)}if(!a.force)throw new Error("Table deletion requires force: true option for safety");return await c.schema.dropTable(n),!0}static async alterTable(e,a={}){const t=TableSchemaSync.getSchemaTable();TableSchemaSync.getDb();if(!t[e])throw new Error(`Model "${e}" not found in schema table`);const c=t[e].table;if(!await TableSchemaSync.tableExists(c))throw new Error(`Table "${c}" does not exist`);const n=await TableSchemaSync.getTableStructure(c),s=t[e].columns||{},l=new Set(n.columns.map(e=>e.name)),r=new Set(Object.keys(s)),o=[...r].filter(e=>!l.has(e)),i=[...l].filter(e=>!r.has(e)),S=[...l].filter(e=>r.has(e));for(const e of o)await TableSchemaSync._addColumn(c,e,s[e]);if(a.allowColumnRemoval)for(const e of i)await TableSchemaSync._removeColumn(c,e);for(const e of S)await TableSchemaSync._modifyColumn(c,e,s[e],n);const b=TableSchemaSync.getDatabaseSync();return await b.addTableConstraints(c,t[e],a),!0}static async _addColumn(e,a,t,c={}){const n=TableSchemaSync.getDatabaseSync();await n.addColumn(e,a,t,c)}static async _removeColumn(e,a){const t=TableSchemaSync.getDatabaseSync();await t.removeColumn(e,a)}static async _modifyColumn(e,a,t,c){const n=TableSchemaSync.getDatabaseSync(),s=n.mapTypeToDatabase(t),l=c.columns.find(e=>e.name===a);l&&l.type!==s&&await n.modifyColumn(e,a,t)}static async syncAllTables(e={}){const a=TableSchemaSync.getSchemaTable(),t={created:[],altered:[],deleted:[],errors:[]},c=TableSchemaSync._sortTablesByDependencies(a);for(const[a,n]of c)try{const c=n.table;await TableSchemaSync.tableExists(c)?e.alterExisting&&(await TableSchemaSync.alterTable(a,e),t.altered.push(c)):e.createMissing&&(await TableSchemaSync.createTable(a,{ifNotExists:!0}),t.created.push(c))}catch(e){t.errors.push({model:a,table:n.table,error:e.message})}return t}static _sortTablesByDependencies(e){const a=Object.entries(e),t=[],c=new Set,n=new Set,s=(e,l)=>{if(!n.has(e)&&!c.has(e)){if(n.add(e),l.foreignKeys)for(const e of l.foreignKeys)if(e.references&&e.references.table){const t=e.references.table,c=a.find(([e,a])=>a.table===t);c&&s(c[0],c[1])}n.delete(e),c.add(e),t.push([e,l])}};for(const[e,t]of a)c.has(e)||s(e,t);return t}static async getSyncStatus(){const e=TableSchemaSync.getSchemaTable(),a={inSync:[],outOfSync:[],missing:[]};for(const[t,c]of Object.entries(e)){const e=c.table;if(await TableSchemaSync.tableExists(e))try{const n=await TableSchemaSync.getTableStructure(e),s=c.columns||{},l=new Set(n.columns.map(e=>e.name)),r=new Set(Object.keys(s));l.size===r.size&&[...l].every(e=>r.has(e))?a.inSync.push({model:t,table:e}):a.outOfSync.push({model:t,table:e})}catch(c){a.outOfSync.push({model:t,table:e,error:c.message})}else a.missing.push({model:t,table:e})}return a}}module.exports=TableSchemaSync;
|