@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.
- package/AuthorizationService.js +1 -0
- package/BaseHelperUtility.js +1 -1
- package/ControllerWrapper.js +1 -1
- package/KormError.js +1 -0
- package/README.md +331 -42
- package/RequestValidator.js +1 -1
- package/ai-skills/korm-js.md +268 -0
- package/bin/korm-mcp.js +2 -0
- package/build.js +1 -1
- package/cli.js +1 -1
- 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/index.d.ts +424 -0
- package/index.js +1 -1
- package/jest.config.engine.js +1 -0
- package/jest.config.js +1 -1
- package/package.json +7 -2
- 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/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/","
|
|
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.
|
|
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
|
},
|
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;
|