@dreamtree-org/korm-js 1.0.54 → 1.0.56

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/AuthorizationService.js +1 -0
  2. package/BaseHelperUtility.js +1 -1
  3. package/ControllerWrapper.js +1 -1
  4. package/KormError.js +1 -0
  5. package/README.md +331 -42
  6. package/RequestValidator.js +1 -1
  7. package/ai-skills/korm-js.md +268 -0
  8. package/bin/korm-mcp.js +2 -0
  9. package/build.js +1 -1
  10. package/cli.js +1 -1
  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/index.d.ts +424 -0
  34. package/index.js +1 -1
  35. package/jest.config.engine.js +1 -0
  36. package/jest.config.js +1 -1
  37. package/package.json +7 -2
  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
package/index.d.ts ADDED
@@ -0,0 +1,424 @@
1
+ export interface InitializeOptions {
2
+ db: any;
3
+ dbClient: string;
4
+ schema?: any;
5
+ resolverPath?: string;
6
+ debug?: boolean;
7
+ }
8
+
9
+ /**
10
+ * Response when a request is sent with `dryRun: true`: the SQL that
11
+ * would run, without executing it. See docs/agents/06-request-contract.md §9.
12
+ */
13
+ export interface DryRunResult {
14
+ success: true;
15
+ dryRun: true;
16
+ action: string;
17
+ model: string;
18
+ sql: string;
19
+ bindings: any[];
20
+ statements: Array<{ sql: string; bindings: any[] }>;
21
+ }
22
+
23
+ /** One column in a ModelDescription (issue #15). */
24
+ export interface ColumnDescription {
25
+ name: string;
26
+ type: string | null;
27
+ nullable: boolean;
28
+ primaryKey: boolean;
29
+ autoIncrement: boolean;
30
+ unique: boolean;
31
+ size?: number;
32
+ default?: string;
33
+ }
34
+
35
+ /** One relation in a ModelDescription. */
36
+ export interface RelationDescription {
37
+ name: string;
38
+ type: string | null;
39
+ table: string | null;
40
+ localKey: string | null;
41
+ foreignKey: string | null;
42
+ through?: string;
43
+ throughLocalKey?: string | null;
44
+ throughForeignKey?: string | null;
45
+ }
46
+
47
+ /** Pure-data description of one model (korm.describeModel). */
48
+ export interface ModelDescription {
49
+ schemaApiVersion: number;
50
+ model: string;
51
+ table: string | null;
52
+ alias: string;
53
+ columns: ColumnDescription[];
54
+ relations: RelationDescription[];
55
+ softDelete: boolean;
56
+ actions: string[];
57
+ }
58
+
59
+ /** Pure-data description of all models (korm.describeSchema). */
60
+ export interface SchemaDescription {
61
+ schemaApiVersion: number;
62
+ models: ModelDescription[];
63
+ }
64
+
65
+ // ---- Request contract (issue #18) ---------------------------------------
66
+ // A discriminated union over `action`, derived from the contract in
67
+ // docs/agents/06-request-contract.md §1/§5. `where`/`data` are intentionally
68
+ // permissive (the runtime validates string-encoded operators + arbitrary
69
+ // columns); the types give shape, action-level narrowing, and autocomplete.
70
+
71
+ export type KormAction =
72
+ | 'list'
73
+ | 'show'
74
+ | 'count'
75
+ | 'sum'
76
+ | 'create'
77
+ | 'update'
78
+ | 'delete'
79
+ | 'replace'
80
+ | 'upsert'
81
+ | 'sync';
82
+
83
+ export type WhereValue = string | number | boolean | null | Array<string | number | boolean>;
84
+ export type WhereConditions = Record<string, WhereValue | Record<string, any>>;
85
+ export type WhereClause = WhereConditions | WhereConditions[];
86
+ export type OrderBy = string | { column: string; direction?: 'asc' | 'desc' };
87
+ export type JoinSpec =
88
+ | string
89
+ | { table: string; on?: any }
90
+ | { table: string; first: string; operator: string; second: string };
91
+ export type SumPayload = { sumColumn: string } | { sumFormula: string };
92
+
93
+ /** Optional read/shaping modifiers shared by the query-style actions. */
94
+ export interface KormQueryModifiers {
95
+ where?: WhereClause;
96
+ select?: string | string[];
97
+ orderBy?: OrderBy | OrderBy[];
98
+ limit?: number;
99
+ offset?: number;
100
+ page?: number;
101
+ with?: string[];
102
+ withWhere?: WhereClause;
103
+ groupBy?: string | string[];
104
+ having?: WhereClause;
105
+ distinct?: boolean | string | string[];
106
+ join?: JoinSpec | JoinSpec[];
107
+ leftJoin?: JoinSpec | JoinSpec[];
108
+ rightJoin?: JoinSpec | JoinSpec[];
109
+ innerJoin?: JoinSpec | JoinSpec[];
110
+ }
111
+
112
+ interface KormRequestCommon {
113
+ /** Build + return the SQL without executing it (see DryRunResult). */
114
+ dryRun?: boolean;
115
+ /** Nested calls keyed by model name. */
116
+ other_requests?: Record<string, KormRequest | KormRequest[]>;
117
+ }
118
+
119
+ export interface KormListRequest extends KormRequestCommon, KormQueryModifiers {
120
+ action?: 'list';
121
+ }
122
+ export interface KormShowRequest extends KormRequestCommon, KormQueryModifiers {
123
+ action: 'show';
124
+ }
125
+ export interface KormCountRequest extends KormRequestCommon, KormQueryModifiers {
126
+ action: 'count';
127
+ }
128
+ export interface KormSumRequest extends KormRequestCommon, KormQueryModifiers {
129
+ action: 'sum';
130
+ data: SumPayload;
131
+ }
132
+ export interface KormCreateRequest extends KormRequestCommon {
133
+ action: 'create';
134
+ data: Record<string, any> | Record<string, any>[];
135
+ }
136
+ export interface KormUpdateRequest extends KormRequestCommon {
137
+ action: 'update';
138
+ where?: WhereClause;
139
+ data: Record<string, any>;
140
+ }
141
+ export interface KormDeleteRequest extends KormRequestCommon {
142
+ action: 'delete';
143
+ where?: WhereClause;
144
+ }
145
+ export interface KormReplaceRequest extends KormRequestCommon {
146
+ action: 'replace';
147
+ data: Record<string, any> | Record<string, any>[];
148
+ }
149
+ export interface KormUpsertRequest extends KormRequestCommon {
150
+ action: 'upsert';
151
+ data: Record<string, any> | Record<string, any>[];
152
+ conflict?: string[];
153
+ }
154
+ export interface KormSyncRequest extends KormRequestCommon {
155
+ action: 'sync';
156
+ data: Record<string, any>[];
157
+ where?: WhereClause;
158
+ conflict?: string[];
159
+ }
160
+
161
+ export type KormRequest =
162
+ | KormListRequest
163
+ | KormShowRequest
164
+ | KormCountRequest
165
+ | KormSumRequest
166
+ | KormCreateRequest
167
+ | KormUpdateRequest
168
+ | KormDeleteRequest
169
+ | KormReplaceRequest
170
+ | KormUpsertRequest
171
+ | KormSyncRequest;
172
+
173
+ /** Loosest accepted input: a built-in request, or a custom-action body. */
174
+ export type KormRequestInput =
175
+ | KormRequest
176
+ | (KormRequestCommon & KormQueryModifiers & { action: string; data?: any });
177
+
178
+ // ---- Response contract (per action; see 06-request-contract.md §5) ------
179
+
180
+ export interface Pagination {
181
+ page: number;
182
+ limit: number;
183
+ offset: number;
184
+ totalPages: number;
185
+ hasNext: boolean;
186
+ hasPrev: boolean;
187
+ nextPage: number | null;
188
+ prevPage: number | null;
189
+ }
190
+ export interface ListResult<Row = Record<string, any>> {
191
+ data: Row[];
192
+ totalCount: number | null;
193
+ pagination?: Pagination;
194
+ sqlDebug?: string[];
195
+ }
196
+ export interface MutationResult<Data = any> {
197
+ message: string;
198
+ data: Data;
199
+ success: true;
200
+ }
201
+ export interface SyncResult {
202
+ message: string;
203
+ data: { insertOrUpdateQuery: any; deleteQuery: any };
204
+ success: true;
205
+ }
206
+ export type ShowResult<Row = Record<string, any>> = Row | null;
207
+
208
+ /**
209
+ * Maps a request to its result shape. `dryRun: true` overrides regardless
210
+ * of action; a custom (non-built-in) action → `any`; an `any` request body
211
+ * stays `any` (back-compat for untyped Express `req.body` callers).
212
+ */
213
+ export type KormResult<TReq> = TReq extends { dryRun: true }
214
+ ? DryRunResult
215
+ : TReq extends { action: 'show' }
216
+ ? ShowResult
217
+ : TReq extends { action: 'count' | 'sum' }
218
+ ? number
219
+ : TReq extends { action: 'sync' }
220
+ ? SyncResult
221
+ : TReq extends { action: 'create' | 'update' | 'delete' | 'replace' | 'upsert' }
222
+ ? MutationResult
223
+ : TReq extends { action: 'list' }
224
+ ? ListResult
225
+ : TReq extends { action: string }
226
+ ? any
227
+ : ListResult;
228
+
229
+ export interface KormInstance {
230
+ /**
231
+ * Execute a request against `modelName`. The result type narrows by the
232
+ * request's `action` literal (and `dryRun`) — see KormResult. An `any`
233
+ * body (e.g. an untyped Express `req.body`) resolves to `any`.
234
+ */
235
+ processRequest<TReq extends KormRequestInput = KormListRequest>(
236
+ requestBody: TReq,
237
+ modelName: string,
238
+ context?: any
239
+ ): Promise<KormResult<TReq>>;
240
+ syncDatabase?(options?: any): Promise<any>;
241
+ generateSchema?(options?: any): Promise<any>;
242
+ /**
243
+ * Draft-2020-12 JSON Schema for every valid processRequest body for
244
+ * `modelName` (an action-discriminated `oneOf`). For OpenAI/Anthropic
245
+ * tool definitions + client-side prevalidation. Throws KormError
246
+ * (code 'UNKNOWN_MODEL') for an unregistered model.
247
+ */
248
+ getRequestJsonSchema(modelName: string): Record<string, any>;
249
+ /** Pure-data description of all registered models (issue #15). */
250
+ describeSchema(): SchemaDescription;
251
+ /**
252
+ * Pure-data description of one model. Throws KormError (code
253
+ * 'UNKNOWN_MODEL') for an unregistered model. When `ctx` is given and
254
+ * authorize() predicates are registered, `actions` is filtered to those
255
+ * permitted for that context (issue #19).
256
+ */
257
+ describeModel(modelName: string, ctx?: AuthContext): ModelDescription;
258
+ setSchema(schema: any): void;
259
+
260
+ // ---- Authorization (issue #19) ----------------------------------------
261
+
262
+ /**
263
+ * Register a permission predicate for `(model, action)`. The predicate
264
+ * receives `(request, ctx)` and returns truthy to allow; a denied request
265
+ * throws KormError (code 'FORBIDDEN'). `action` may be `'*'` to gate every
266
+ * action on the model. Opt-in: unregistered pairs are allowed.
267
+ */
268
+ authorize(
269
+ model: string,
270
+ action: KormAction | '*' | string,
271
+ predicate: (request: KormRequestInput, ctx: AuthContext) => boolean
272
+ ): KormInstance;
273
+ /**
274
+ * Register a row-scope for `model`. `fn(request, ctx)` returns an object
275
+ * merged into `where` for reads/update/delete/count/sum and stamped into
276
+ * each inserted `data` row for create/replace/upsert/sync.
277
+ */
278
+ scope(
279
+ model: string,
280
+ fn: (request: KormRequestInput, ctx: AuthContext) => Record<string, any>
281
+ ): KormInstance;
282
+ /** Clear all registered authorize()/scope() rules (mainly for tests). */
283
+ resetAuthorization(): KormInstance;
284
+
285
+ loadModelClass?(name: string): any;
286
+ getModelInstance?(name: string): any;
287
+ }
288
+
289
+ /** Authorization context passed as the 3rd arg to processRequest. */
290
+ export type AuthContext = Record<string, any>;
291
+
292
+ export function initializeKORM(opts: InitializeOptions): KormInstance;
293
+ export function validate(body: any, rules: any, opts?: any): Promise<any>;
294
+ export const helperUtility: any;
295
+ export const emitter: any;
296
+ export const logger: any;
297
+
298
+ // ---- Structured errors --------------------------------------------------
299
+
300
+ export type KormErrorCode =
301
+ | 'NO_MATCHING_ROW'
302
+ | 'UNKNOWN_ACTION'
303
+ | 'VALIDATION_FAILED'
304
+ | 'UNKNOWN_MODEL'
305
+ | 'NO_CUSTOM_ACTION_HOOK'
306
+ | 'FORBIDDEN'
307
+ | 'INTERNAL';
308
+
309
+ export interface KormErrorContext {
310
+ action?: string;
311
+ model?: string;
312
+ validActions?: string[];
313
+ closest?: string | null;
314
+ available?: string[];
315
+ source?: string | null;
316
+ fields?: Array<{ field?: string; message?: string; value?: unknown; rule?: unknown }>;
317
+ [key: string]: unknown;
318
+ }
319
+
320
+ export interface KormErrorJSON {
321
+ name: 'KormError';
322
+ code: KormErrorCode;
323
+ message: string;
324
+ hint: string | null;
325
+ context: KormErrorContext;
326
+ suggestedFixes: Array<{ description: string; request?: object }> | null;
327
+ }
328
+
329
+ /**
330
+ * Structured error thrown by processRequest / validate. Extends the
331
+ * native Error, so `catch (e) { e.message }` keeps working; `e.code`
332
+ * and `e.context` let callers (and agents) branch programmatically.
333
+ */
334
+ export class KormError extends Error {
335
+ name: 'KormError';
336
+ code: KormErrorCode;
337
+ hint: string | null;
338
+ context: KormErrorContext;
339
+ suggestedFixes: Array<{ description: string; request?: object }> | null;
340
+ /** Present when code === 'VALIDATION_FAILED' (back-compat alias). */
341
+ errors?: any[];
342
+ toJSON(): KormErrorJSON;
343
+
344
+ static CODES: Record<KormErrorCode, KormErrorCode>;
345
+ static ACTIONS: readonly string[];
346
+ static closestAction(input: string, candidates?: string[]): string | null;
347
+ static noMatchingRow(opts: { action: string; model: string }): KormError;
348
+ static unknownAction(opts: { action: string; model?: string; hasCustomHook?: boolean }): KormError;
349
+ static unknownModel(opts: { model: string; available?: string[] }): KormError;
350
+ static validationFailed(opts: { errors?: any[]; source?: string | null }): KormError;
351
+ static forbidden(opts: {
352
+ model: string;
353
+ action: string;
354
+ hint?: string | null;
355
+ context?: Record<string, unknown>;
356
+ }): KormError;
357
+ }
358
+
359
+ export const LibClasses: { Emitter: any; KormError: typeof KormError };
360
+ export const lib: {
361
+ createValidationMiddleware(...args: any[]): any;
362
+ validateEmail(...args: any[]): any;
363
+ validatePassword(...args: any[]): any;
364
+ validatePhone(...args: any[]): any;
365
+ validatePAN(...args: any[]): any;
366
+ validateAadhaar(...args: any[]): any;
367
+ };
368
+
369
+ // ---- MCP (Model Context Protocol) optional surface ----------------------
370
+
371
+ export type McpMode = 'ro' | 'rw' | 'rw-sync';
372
+
373
+ export interface McpCustomAction {
374
+ table: string;
375
+ action: string;
376
+ schema?: any;
377
+ description?: string;
378
+ }
379
+
380
+ export interface McpConfig {
381
+ mode?: McpMode;
382
+ allowlist?: string[] | '*';
383
+ blocklist?: string[];
384
+ metaTools?: boolean;
385
+ allowNestedRequests?: boolean;
386
+ customActions?: McpCustomAction[];
387
+ rateLimit?: { perMinute?: number };
388
+ logLevel?: string;
389
+ }
390
+
391
+ export interface McpToolResult {
392
+ content: Array<{ type: string; text: string }>;
393
+ isError?: boolean;
394
+ }
395
+
396
+ export interface McpTool {
397
+ name: string;
398
+ description: string;
399
+ inputSchema: any;
400
+ handler: (input: any) => Promise<McpToolResult>;
401
+ }
402
+
403
+ export interface McpServer {
404
+ tools: McpTool[];
405
+ toolsByName: Map<string, McpTool>;
406
+ start(opts?: { logger?: any }): Promise<any>;
407
+ stop(): Promise<void>;
408
+ }
409
+
410
+ export interface CreateMcpServerOptions {
411
+ controller: any;
412
+ schema: any;
413
+ mcpConfig: McpConfig;
414
+ packageInfo?: { name?: string; version?: string };
415
+ }
416
+
417
+ export const mcp: {
418
+ createServer(opts: CreateMcpServerOptions): McpServer;
419
+ generateTools(opts: {
420
+ controller: any;
421
+ schema: any;
422
+ mcpConfig: McpConfig;
423
+ }): McpTool[];
424
+ };
package/index.js CHANGED
@@ -1 +1 @@
1
- const ControllerWrapper=require("./ControllerWrapper"),BaseHelperUtility=require("./BaseHelperUtility"),RequestValidator=require("./RequestValidator"),Emitter=require("./Emitter"),logger=require("./Logger");module.exports={LibClasses:{Emitter:Emitter},initializeKORM(){return ControllerWrapper.initializeKORM(...arguments)},helperUtility:new BaseHelperUtility,emitter:new Emitter,logger:logger,validate:RequestValidator.validate,lib:{createValidationMiddleware:RequestValidator.createValidationMiddleware,validateEmail:RequestValidator.validateEmail,validatePassword:RequestValidator.validatePassword,validatePhone:RequestValidator.validatePhone,validatePAN:RequestValidator.validatePAN,validateAadhaar:RequestValidator.validateAadhaar}};
1
+ const ControllerWrapper=require("./ControllerWrapper"),BaseHelperUtility=require("./BaseHelperUtility"),RequestValidator=require("./RequestValidator"),Emitter=require("./Emitter"),logger=require("./Logger"),KormError=require("./KormError"),{createServer:createMcpServer}=require("./src/mcp/server"),{generateTools:generateMcpTools}=require("./src/mcp/toolGenerator");module.exports={LibClasses:{Emitter:Emitter,KormError:KormError},KormError:KormError,initializeKORM(){return ControllerWrapper.initializeKORM(...arguments)},helperUtility:new BaseHelperUtility,emitter:new Emitter,logger:logger,validate:RequestValidator.validate,lib:{createValidationMiddleware:RequestValidator.createValidationMiddleware,validateEmail:RequestValidator.validateEmail,validatePassword:RequestValidator.validatePassword,validatePhone:RequestValidator.validatePhone,validatePAN:RequestValidator.validatePAN,validateAadhaar:RequestValidator.validateAadhaar},mcp:{createServer:createMcpServer,generateTools:generateMcpTools}};
@@ -0,0 +1 @@
1
+ const base=require("./jest.config"),engine=process.env.KORM_COVERAGE_ENGINE;if("pg"!==engine&&"mysql"!==engine)throw new Error(`jest.config.engine.js requires KORM_COVERAGE_ENGINE=pg|mysql (got: ${engine||"unset"})`);const ENGINE_RATCHET={statements:47,branches:30,functions:60,lines:48},RATCHETS={pg:ENGINE_RATCHET,mysql:ENGINE_RATCHET};module.exports={...base,collectCoverageFrom:[`clients/${engine}/**/*.js`],coverageThreshold:{global:RATCHETS[engine]}};
package/jest.config.js CHANGED
@@ -1 +1 @@
1
- module.exports={testEnvironment:"node",testMatch:["**/test/**/*.test.js","**/test/**/*.spec.js","**/__tests__/**/*.js"],testPathIgnorePatterns:["/node_modules/","<rootDir>/test/mysql/","<rootDir>/test/postgres/","<rootDir>/test/sqlite/sqlite-crud.test.js","<rootDir>/test/sqlite/sqlite-sync-all-tables.test.js","<rootDir>/test/sqlite/sqlite-table-sync.test.js","<rootDir>/test/sqlite/sqlite.test.js","<rootDir>/test/sqlite/crud-operations-coverage.test.js"],collectCoverage:!0,coverageDirectory:"coverage",coverageReporters:["text","lcov","html"],collectCoverageFrom:["**/*.js","!**/node_modules/**","!**/test/**","!**/coverage/**","!jest.config.js","!**/dist/**","!build.js","!version-manager.js"],testTimeout:1e4,clearMocks:!0,verbose:!0};
1
+ module.exports={testEnvironment:"node",testMatch:["**/test/**/*.test.js","**/test/**/*.spec.js","**/__tests__/**/*.js"],testPathIgnorePatterns:["/node_modules/"],collectCoverage:!1,coverageDirectory:"coverage",coverageReporters:["text","lcov","html"],collectCoverageFrom:["clients/sqlite/**/*.js","clients/Base*.js","ControllerWrapper.js","AuthorizationService.js","RequestValidator.js","BaseHelperUtility.js","Logger.js","Emitter.js","index.js","cli.js","helpers/**/*.js","src/mcp/**/*.js","bin/korm-mcp.js","!**/node_modules/**","!**/test/**","!**/coverage/**","!**/dist/**"],coverageThreshold:{global:{statements:90,branches:85,functions:90,lines:90},"clients/sqlite/CurdTable.js":{statements:95,branches:90},"RequestValidator.js":{statements:95,branches:88},"clients/sqlite/QueryBuilder.js":{statements:93,branches:83}},testTimeout:1e4,moduleNameMapper:{"^@modelcontextprotocol/sdk/(.*)$":"<rootDir>/node_modules/@modelcontextprotocol/sdk/dist/cjs/$1"},clearMocks:!0,verbose:!0};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dreamtree-org/korm-js",
3
- "version": "1.0.54",
3
+ "version": "1.0.56",
4
4
  "description": "Knowledge Object-Relational Mapping - A powerful, modular ORM system for Node.js with dynamic database operations, complex queries, relationships, and nested requests",
5
5
  "author": {
6
6
  "name": "Partha Preetham Krishna",
@@ -10,13 +10,15 @@
10
10
  "main": "index.js",
11
11
  "types": "index.d.ts",
12
12
  "bin": {
13
- "korm-js": "./cli.js"
13
+ "korm-js": "./cli.js",
14
+ "korm-mcp": "./bin/korm-mcp.js"
14
15
  },
15
16
  "scripts": {
16
17
  "test": "jest",
17
18
  "test:watch": "jest --watch",
18
19
  "test:coverage": "jest --coverage",
19
20
  "test:all": "jest --coverage --verbose",
21
+ "test:types": "tsc -p tsconfig.types.json",
20
22
  "lint": "eslint . --max-warnings=0",
21
23
  "lint:fix": "eslint . --fix",
22
24
  "format": "prettier --write .",
@@ -67,6 +69,9 @@
67
69
  "peerDependencies": {
68
70
  "knex": "^3.0.0"
69
71
  },
72
+ "optionalDependencies": {
73
+ "@modelcontextprotocol/sdk": "^1.0.0"
74
+ },
70
75
  "directories": {
71
76
  "doc": "Documentation"
72
77
  },
@@ -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};
@@ -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;