@dreamtree-org/korm-js 1.0.53 → 1.0.55

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/BaseHelperUtility.js +1 -1
  2. package/ControllerWrapper.js +1 -1
  3. package/Emitter.js +1 -1
  4. package/KormError.js +1 -0
  5. package/README.md +463 -254
  6. package/RequestValidator.js +1 -1
  7. package/ai-skills/korm-js.md +265 -0
  8. package/bin/korm-mcp.js +2 -0
  9. package/build.js +1 -1
  10. package/cli.js +2 -0
  11. package/clients/BaseSyncTable.js +1 -0
  12. package/clients/mysql/BaseUtility.js +1 -1
  13. package/clients/mysql/CurdTable.js +1 -1
  14. package/clients/mysql/DataTypeMap.js +1 -1
  15. package/clients/mysql/HookService.js +1 -1
  16. package/clients/mysql/QueryBuilder.js +1 -1
  17. package/clients/mysql/QueryService.js +1 -1
  18. package/clients/mysql/SyncTable.js +1 -1
  19. package/clients/pg/BaseUtility.js +1 -1
  20. package/clients/pg/CurdTable.js +1 -1
  21. package/clients/pg/DataTypeMap.js +1 -1
  22. package/clients/pg/HookService.js +1 -1
  23. package/clients/pg/QueryBuilder.js +1 -1
  24. package/clients/pg/QueryService.js +1 -1
  25. package/clients/pg/SyncTable.js +1 -1
  26. package/clients/sqlite/BaseUtility.js +1 -1
  27. package/clients/sqlite/CurdTable.js +1 -1
  28. package/clients/sqlite/HookService.js +1 -1
  29. package/clients/sqlite/QueryBuilder.js +1 -1
  30. package/clients/sqlite/QueryService.js +1 -1
  31. package/clients/sqlite/SyncTable.js +1 -1
  32. package/columnSchema.js +1 -0
  33. package/helpers/files.js +1 -1
  34. package/index.d.ts +213 -0
  35. package/index.js +1 -1
  36. package/jest.config.js +1 -1
  37. package/package.json +13 -4
  38. package/requestSchema.js +1 -0
  39. package/schemaDescribe.js +1 -0
  40. package/src/mcp/errors.js +1 -0
  41. package/src/mcp/schemaIntrospect.js +1 -0
  42. package/src/mcp/server.js +1 -0
  43. package/src/mcp/toolGenerator.js +1 -0
  44. package/TableSchemaSync.js +0 -1
package/README.md CHANGED
@@ -29,6 +29,35 @@
29
29
  npm install @dreamtree-org/korm-js
30
30
  ```
31
31
 
32
+ ## AI assistant skills (`init --ai`)
33
+
34
+ KORM-JS ships a canonical reference doc designed for AI coding assistants. After installing the package (or via `npx` without installing), drop the skill into the location your assistant picks up:
35
+
36
+ ```bash
37
+ npx @dreamtree-org/korm-js init --ai claude
38
+ ```
39
+
40
+ Supported providers:
41
+
42
+ | `--ai` value | Target file |
43
+ | ----------------------------------- | ----------------------------------------------------------- |
44
+ | `claude` | `CLAUDE.md` (idempotent block insert) |
45
+ | `openai` (alias: `codex`) | `AGENTS.md` (idempotent block insert) |
46
+ | `gemini` | `GEMINI.md` (idempotent block insert) |
47
+ | `copilot` (alias: `github-copilot`) | `.github/copilot-instructions.md` (idempotent block insert) |
48
+ | `kiro` | `.kiro/steering/korm-js.md` |
49
+ | `windsurf` | `.windsurf/rules/korm-js.md` |
50
+ | `cursor` | `.cursor/rules/korm-js.mdc` (with MDC frontmatter) |
51
+ | `all` | Installs every provider above |
52
+
53
+ Flags:
54
+
55
+ - `--cwd <dir>` — install into a directory other than the current working directory.
56
+ - `--force` — overwrite dedicated-file targets (Cursor / Windsurf / Kiro) that already exist.
57
+ - `--dry-run` — print what would change without writing.
58
+
59
+ Block-insert targets (`CLAUDE.md`, `AGENTS.md`, `GEMINI.md`, `.github/copilot-instructions.md`) wrap the skill in `<!-- BEGIN korm-js skill --> ... <!-- END korm-js skill -->` markers. Re-running the command after a library upgrade updates the block in place without touching the rest of the file.
60
+
32
61
  ## Quick Start with Express.js and MySQL
33
62
 
34
63
  ### 1. Basic Setup
@@ -50,15 +79,15 @@ const db = knex({
50
79
  user: process.env.DB_USER || 'root',
51
80
  password: process.env.DB_PASS || 'password',
52
81
  database: process.env.DB_NAME || 'my_database',
53
- port: process.env.DB_PORT || 3306
54
- }
82
+ port: process.env.DB_PORT || 3306,
83
+ },
55
84
  });
56
85
 
57
86
  // Initialize KORM with MySQL
58
87
  const korm = initializeKORM({
59
88
  db: db,
60
89
  dbClient: 'mysql',
61
- debug: false // Set to true for SQL debugging
90
+ debug: false, // Set to true for SQL debugging
62
91
  });
63
92
 
64
93
  app.listen(3000, () => {
@@ -75,14 +104,14 @@ async function initApp() {
75
104
  if (syncSchema) {
76
105
  korm.setSchema(syncSchema);
77
106
  }
78
-
107
+
79
108
  // Sync database with schema (creates tables if they don't exist)
80
109
  await korm.syncDatabase().then(() => {
81
110
  console.log('✅ Database synced');
82
111
  });
83
-
112
+
84
113
  // Generate schema from existing database
85
- await korm.generateSchema().then(schema => {
114
+ await korm.generateSchema().then((schema) => {
86
115
  korm.setSchema(schema); // Set the schema to the korm instance
87
116
  helperUtility.file.createDirectory('schema');
88
117
  helperUtility.file.writeJSON('schema/schema.json', schema);
@@ -367,6 +396,7 @@ POST /api/Employee/crud
367
396
  ```
368
397
 
369
398
  **Notes:**
399
+
370
400
  - Use **either** `sumColumn` or `sumFormula`, not both. If both are provided, `sumColumn` is used.
371
401
  - In `sumFormula`, column names are written as `{columnName}` and are safely quoted; only letters, numbers, and underscore are allowed in names.
372
402
  - **Expression guards:** `sumFormula` allows only BODMAS-safe content: numbers (including decimals), operators `+` `-` `*` `/`, parentheses `(` `)`, and `{columnName}` placeholders. Any other character (e.g. commas, quotes, SQL) is rejected. Parentheses must be balanced and correctly ordered.
@@ -599,21 +629,21 @@ POST /api/Users/crud
599
629
 
600
630
  ### Where Operators Reference
601
631
 
602
- | Operator | Description | Syntax | Example |
603
- |----------|-------------|--------|---------|
604
- | `=` (default) | Equals | `value` | `"status": "active"` |
605
- | `>=` | Greater than or equal | `>=value` | `"age": ">=18"` |
606
- | `<=` | Less than or equal | `<=value` | `"age": "<=65"` |
607
- | `>` | Greater than | `>value` | `"price": ">100"` |
608
- | `<` | Less than | `<value` | `"price": "<500"` |
609
- | `!=` | Not equal | `!value` | `"status": "!deleted"` |
610
- | `like` | Pattern matching (auto) | `%value%` | `"name": "%john%"` |
611
- | `in` | Value in list | `[]val1,val2` | `"status": "[]active,pending"` |
612
- | `notIn` | Value not in list | `![]val1,val2` | `"role": "![]banned,suspended"` |
613
- | `between` | Range (inclusive) | `><min,max` | `"age": "><18,65"` |
614
- | `notBetween` | Outside range | `<>min,max` | `"score": "<>0,50"` |
615
- | `null` | IS NULL check | `null` | `"deleted_at": null` |
616
- | `Or:column` | OR condition prefix | `Or:column` | `"Or:name": "John"` |
632
+ | Operator | Description | Syntax | Example |
633
+ | ------------- | ----------------------- | -------------- | ------------------------------- |
634
+ | `=` (default) | Equals | `value` | `"status": "active"` |
635
+ | `>=` | Greater than or equal | `>=value` | `"age": ">=18"` |
636
+ | `<=` | Less than or equal | `<=value` | `"age": "<=65"` |
637
+ | `>` | Greater than | `>value` | `"price": ">100"` |
638
+ | `<` | Less than | `<value` | `"price": "<500"` |
639
+ | `!=` | Not equal | `!value` | `"status": "!deleted"` |
640
+ | `like` | Pattern matching (auto) | `%value%` | `"name": "%john%"` |
641
+ | `in` | Value in list | `[]val1,val2` | `"status": "[]active,pending"` |
642
+ | `notIn` | Value not in list | `![]val1,val2` | `"role": "![]banned,suspended"` |
643
+ | `between` | Range (inclusive) | `><min,max` | `"age": "><18,65"` |
644
+ | `notBetween` | Outside range | `<>min,max` | `"score": "<>0,50"` |
645
+ | `null` | IS NULL check | `null` | `"deleted_at": null` |
646
+ | `Or:column` | OR condition prefix | `Or:column` | `"Or:name": "John"` |
617
647
 
618
648
  **Examples:**
619
649
 
@@ -630,7 +660,7 @@ POST /api/Users/crud
630
660
  "score": "><50,100"
631
661
  }
632
662
  }
633
- // SQL: WHERE age >= 18 AND status != 'deleted' AND name LIKE '%john%'
663
+ // SQL: WHERE age >= 18 AND status != 'deleted' AND name LIKE '%john%'
634
664
  // AND role IN ('admin', 'moderator', 'editor') AND score BETWEEN 50 AND 100
635
665
  ```
636
666
 
@@ -721,6 +751,7 @@ POST /api/Users/crud
721
751
  ```
722
752
 
723
753
  **Available join types:**
754
+
724
755
  - `join` - Regular join
725
756
  - `innerJoin` - Inner join
726
757
  - `leftJoin` - Left join
@@ -795,6 +826,7 @@ For many-to-many relationships, use the `through` key to specify the join/pivot
795
826
  ```
796
827
 
797
828
  **Many-to-Many Structure:**
829
+
798
830
  - `type`: `"many"` (always use "many" for many-to-many)
799
831
  - `table`: The related table name (e.g., `"roles"`)
800
832
  - `localKey`: The primary key in the current model (e.g., `"id"`)
@@ -865,7 +897,7 @@ POST /api/Post/crud
865
897
  },
866
898
  "with": ["User", "User.UserRole", "User.UserRole.Role"]
867
899
  }
868
- // SQL: WHERE EXISTS (SELECT * FROM users WHERE users.id = posts.user_id
900
+ // SQL: WHERE EXISTS (SELECT * FROM users WHERE users.id = posts.user_id
869
901
  // AND EXISTS (SELECT * FROM user_roles WHERE user_roles.user_id = users.id
870
902
  // AND EXISTS (SELECT * FROM roles WHERE roles.id = user_roles.role_id AND name = 'admin')))
871
903
  ```
@@ -884,7 +916,7 @@ POST /api/Post/crud
884
916
  },
885
917
  "with": ["User"]
886
918
  }
887
- // SQL: WHERE EXISTS (SELECT * FROM users WHERE users.id = posts.user_id
919
+ // SQL: WHERE EXISTS (SELECT * FROM users WHERE users.id = posts.user_id
888
920
  // AND NOT EXISTS (SELECT * FROM user_roles WHERE user_roles.user_id = users.id))
889
921
 
890
922
  // Get users with no posts
@@ -912,12 +944,13 @@ POST /api/Post/crud
912
944
 
913
945
  **NOT EXISTS Syntax Summary:**
914
946
 
915
- | Pattern | Meaning | Use Case |
916
- |---------|---------|----------|
917
- | `"!RelName": true` | NOT EXISTS on direct relation | Users with no posts |
918
- | `"Parent.!Child": true` | NOT EXISTS on nested relation | Posts where user has no roles |
919
- | `"Parent.!Child.column": value` | NOT EXISTS with condition | Posts where user has no admin role |
920
- ```
947
+ | Pattern | Meaning | Use Case |
948
+ | ------------------------------- | ----------------------------- | ---------------------------------- |
949
+ | `"!RelName": true` | NOT EXISTS on direct relation | Users with no posts |
950
+ | `"Parent.!Child": true` | NOT EXISTS on nested relation | Posts where user has no roles |
951
+ | `"Parent.!Child.column": value` | NOT EXISTS with condition | Posts where user has no admin role |
952
+
953
+ ````
921
954
 
922
955
  #### Filtering Eager Loaded Relations with `withWhere`
923
956
 
@@ -979,15 +1012,15 @@ POST /api/User/crud
979
1012
  "Or:Post.is_featured": true
980
1013
  }
981
1014
  }
982
- ```
1015
+ ````
983
1016
 
984
1017
  **Key differences between `where` and `withWhere`:**
985
1018
 
986
- | Feature | `where` (Nested) | `withWhere` |
987
- |---------|-----------------|-------------|
988
- | Affects parent query | Yes (filters parent rows) | No (only filters related rows) |
989
- | Purpose | Filter parents that have matching relations | Filter which related rows are loaded |
990
- | SQL generated | `WHERE EXISTS (subquery)` | Additional `WHERE` on relation query |
1019
+ | Feature | `where` (Nested) | `withWhere` |
1020
+ | -------------------- | ------------------------------------------- | ------------------------------------ |
1021
+ | Affects parent query | Yes (filters parent rows) | No (only filters related rows) |
1022
+ | Purpose | Filter parents that have matching relations | Filter which related rows are loaded |
1023
+ | SQL generated | `WHERE EXISTS (subquery)` | Additional `WHERE` on relation query |
991
1024
 
992
1025
  **Example showing the difference:**
993
1026
 
@@ -1012,6 +1045,7 @@ POST /api/User/crud
1012
1045
  ```
1013
1046
 
1014
1047
  **withWhere supports all operators:**
1048
+
1015
1049
  - Comparison: `">=18"`, `"<=100"`, `">50"`, `"<10"`, `"!deleted"`
1016
1050
  - LIKE: `"%pattern%"`
1017
1051
  - IN: `"[]val1,val2,val3"`
@@ -1085,6 +1119,7 @@ Example complete relationship structure:
1085
1119
  ### Custom Relation Hooks
1086
1120
 
1087
1121
  For complex relationships that can't be defined in the schema, you can create custom relation hooks in your model class. This is useful for:
1122
+
1088
1123
  - Virtual/computed relations
1089
1124
  - Cross-database relations
1090
1125
  - Complex aggregations
@@ -1100,35 +1135,33 @@ class Users {
1100
1135
  // rows = parent rows to attach relation data to
1101
1136
  // db = Knex database instance
1102
1137
  // qb = QueryBuilder instance
1103
-
1138
+
1104
1139
  for (const row of rows) {
1105
1140
  // Fetch custom data for each row
1106
- const stats = await db('user_statistics')
1107
- .where('user_id', row.id)
1108
- .first();
1109
-
1141
+ const stats = await db('user_statistics').where('user_id', row.id).first();
1142
+
1110
1143
  // Attach to row
1111
1144
  row.Statistics = stats || { posts: 0, comments: 0, likes: 0 };
1112
1145
  }
1113
1146
  }
1114
-
1147
+
1115
1148
  // Custom relation with aggregation
1116
1149
  async getPostCountRelation({ rows, db }) {
1117
- const userIds = rows.map(r => r.id);
1118
-
1150
+ const userIds = rows.map((r) => r.id);
1151
+
1119
1152
  const counts = await db('posts')
1120
1153
  .select('user_id')
1121
1154
  .count('* as count')
1122
1155
  .whereIn('user_id', userIds)
1123
1156
  .groupBy('user_id');
1124
-
1125
- const countMap = new Map(counts.map(c => [c.user_id, c.count]));
1126
-
1157
+
1158
+ const countMap = new Map(counts.map((c) => [c.user_id, c.count]));
1159
+
1127
1160
  for (const row of rows) {
1128
1161
  row.PostCount = countMap.get(row.id) || 0;
1129
1162
  }
1130
1163
  }
1131
-
1164
+
1132
1165
  // Custom relation from external API or different database
1133
1166
  async getExternalProfileRelation({ rows }) {
1134
1167
  for (const row of rows) {
@@ -1155,16 +1188,16 @@ POST /api/Users/crud
1155
1188
 
1156
1189
  **Custom Relation Hook Arguments:**
1157
1190
 
1158
- | Argument | Description |
1159
- |----------|-------------|
1160
- | `rows` | Parent rows to attach relation data to (modify in place) |
1161
- | `relName` | The relation name being fetched |
1162
- | `model` | Model definition object |
1163
- | `withTree` | Nested relations tree for further loading |
1164
- | `controller` | ControllerWrapper instance |
1165
- | `relation` | Relation definition from schema (may be undefined for custom relations) |
1166
- | `qb` | QueryBuilder instance for building queries |
1167
- | `db` | Knex database instance for direct queries |
1191
+ | Argument | Description |
1192
+ | ------------ | ----------------------------------------------------------------------- |
1193
+ | `rows` | Parent rows to attach relation data to (modify in place) |
1194
+ | `relName` | The relation name being fetched |
1195
+ | `model` | Model definition object |
1196
+ | `withTree` | Nested relations tree for further loading |
1197
+ | `controller` | ControllerWrapper instance |
1198
+ | `relation` | Relation definition from schema (may be undefined for custom relations) |
1199
+ | `qb` | QueryBuilder instance for building queries |
1200
+ | `db` | Knex database instance for direct queries |
1168
1201
 
1169
1202
  ### Important Notes
1170
1203
 
@@ -1187,13 +1220,13 @@ Create a model file at `models/Users.model.js`:
1187
1220
  class Users {
1188
1221
  // Enable soft delete for this model
1189
1222
  hasSoftDelete = true;
1190
-
1223
+
1191
1224
  // Optional: Custom soft delete hook
1192
1225
  beforeDelete({ model, action, request, context, db, utils, controller }) {
1193
1226
  // Custom logic before soft delete
1194
1227
  console.log('Soft deleting user:', request.where);
1195
1228
  }
1196
-
1229
+
1197
1230
  // Optional: Custom hook after soft delete
1198
1231
  afterDelete({ model, action, data, request, context, db, utils, controller }) {
1199
1232
  // Custom logic after soft delete
@@ -1244,15 +1277,13 @@ class Users {
1244
1277
  throw new Error('Email is required');
1245
1278
  }
1246
1279
  // Check if email already exists
1247
- const existing = await db('users')
1248
- .where('email', request.data.email)
1249
- .first();
1280
+ const existing = await db('users').where('email', request.data.email).first();
1250
1281
  if (existing && existing.id !== request.where?.id) {
1251
1282
  throw new Error('Email already exists');
1252
1283
  }
1253
1284
  }
1254
1285
  }
1255
-
1286
+
1256
1287
  // Before hooks - run before the action executes
1257
1288
  // Method naming: before{Action} (e.g., beforeCreate, beforeUpdate, beforeList, beforeDelete)
1258
1289
  async beforeCreate({ model, action, request, context, db, utils, controller }) {
@@ -1261,18 +1292,18 @@ class Users {
1261
1292
  request.data.updated_at = new Date();
1262
1293
  return request.data;
1263
1294
  }
1264
-
1295
+
1265
1296
  async beforeUpdate({ model, action, request, context, db, utils, controller }) {
1266
1297
  // Modify request data before update
1267
1298
  request.data.updated_at = new Date();
1268
1299
  return request.data;
1269
1300
  }
1270
-
1301
+
1271
1302
  async beforeDelete({ model, action, request, context, db, utils, controller }) {
1272
1303
  // Logic before delete (works with both hard and soft delete)
1273
1304
  console.log('Deleting user:', request.where);
1274
1305
  }
1275
-
1306
+
1276
1307
  // After hooks - run after the action executes
1277
1308
  // Method naming: after{Action} (e.g., afterCreate, afterUpdate, afterList, afterDelete)
1278
1309
  async afterCreate({ model, action, data, request, context, db, utils, controller }) {
@@ -1281,17 +1312,17 @@ class Users {
1281
1312
  // Send welcome email, trigger notifications, etc.
1282
1313
  return data;
1283
1314
  }
1284
-
1315
+
1285
1316
  async afterUpdate({ model, action, data, request, context, db, utils, controller }) {
1286
1317
  console.log('User updated:', data);
1287
1318
  return data;
1288
1319
  }
1289
-
1320
+
1290
1321
  async afterList({ model, action, data, request, context, db, utils, controller }) {
1291
1322
  // Modify list results before returning
1292
1323
  return data;
1293
1324
  }
1294
-
1325
+
1295
1326
  // Custom action hooks
1296
1327
  // Method naming: on{Action}Action (e.g., onActivateAction, onDeactivateAction)
1297
1328
  async onActivateAction({ model, action, request, context, db, utils, controller }) {
@@ -1299,7 +1330,7 @@ class Users {
1299
1330
  .where(request.where)
1300
1331
  .update({ is_active: true, updated_at: new Date() });
1301
1332
  }
1302
-
1333
+
1303
1334
  async onDeactivateAction({ model, action, request, context, db, utils, controller }) {
1304
1335
  return await db('users')
1305
1336
  .where(request.where)
@@ -1312,29 +1343,30 @@ module.exports = Users;
1312
1343
 
1313
1344
  ### Hook Arguments Reference
1314
1345
 
1315
- | Argument | Description |
1316
- |----------|-------------|
1317
- | `model` | Model definition object with table, columns, relations |
1318
- | `action` | Current action being performed (create, update, delete, etc.) |
1319
- | `request` | The request object containing where, data, etc. |
1320
- | `context` | Custom context passed from the controller |
1321
- | `db` | Knex database instance for direct queries |
1322
- | `utils` | Utility functions |
1323
- | `controller` | ControllerWrapper instance |
1324
- | `data` | (After hooks only) Result of the action |
1346
+ | Argument | Description |
1347
+ | ------------ | ------------------------------------------------------------- |
1348
+ | `model` | Model definition object with table, columns, relations |
1349
+ | `action` | Current action being performed (create, update, delete, etc.) |
1350
+ | `request` | The request object containing where, data, etc. |
1351
+ | `context` | Custom context passed from the controller |
1352
+ | `db` | Knex database instance for direct queries |
1353
+ | `utils` | Utility functions |
1354
+ | `controller` | ControllerWrapper instance |
1355
+ | `data` | (After hooks only) Result of the action |
1325
1356
 
1326
1357
  ### Complete Hook Types Reference
1327
1358
 
1328
- | Hook Type | Method Naming | When Called | Use Case |
1329
- |-----------|---------------|-------------|----------|
1330
- | Validate | `validate` | Before any action | Input validation, authorization |
1331
- | Before | `before{Action}` | Before action executes | Modify request data, add timestamps |
1332
- | After | `after{Action}` | After action executes | Transform results, trigger side effects |
1333
- | Custom Action | `on{Action}Action` | For custom actions | Implement business logic |
1334
- | Soft Delete | `hasSoftDelete = true` | During delete/list | Enable soft delete |
1335
- | Custom Relation | `get{RelName}Relation` | During eager loading | Custom data fetching |
1359
+ | Hook Type | Method Naming | When Called | Use Case |
1360
+ | --------------- | ---------------------- | ---------------------- | --------------------------------------- |
1361
+ | Validate | `validate` | Before any action | Input validation, authorization |
1362
+ | Before | `before{Action}` | Before action executes | Modify request data, add timestamps |
1363
+ | After | `after{Action}` | After action executes | Transform results, trigger side effects |
1364
+ | Custom Action | `on{Action}Action` | For custom actions | Implement business logic |
1365
+ | Soft Delete | `hasSoftDelete = true` | During delete/list | Enable soft delete |
1366
+ | Custom Relation | `get{RelName}Relation` | During eager loading | Custom data fetching |
1336
1367
 
1337
1368
  **Available Before/After hooks:**
1369
+
1338
1370
  - `beforeCreate` / `afterCreate`
1339
1371
  - `beforeUpdate` / `afterUpdate`
1340
1372
  - `beforeDelete` / `afterDelete`
@@ -1357,7 +1389,7 @@ POST /api/Users/crud
1357
1389
 
1358
1390
  POST /api/Users/crud
1359
1391
  {
1360
- "action": "deactivate",
1392
+ "action": "deactivate",
1361
1393
  "where": { "id": 1 }
1362
1394
  }
1363
1395
 
@@ -1384,7 +1416,7 @@ const userValidationRules = {
1384
1416
  first_name: 'required|type:string|maxLen:100',
1385
1417
  last_name: 'required|type:string|maxLen:100',
1386
1418
  age: 'type:number|min:0|max:150',
1387
- is_active: 'type:boolean'
1419
+ is_active: 'type:boolean',
1388
1420
  };
1389
1421
 
1390
1422
  // Validate and create user
@@ -1392,30 +1424,33 @@ app.post('/api/users/validate', async (req, res) => {
1392
1424
  try {
1393
1425
  // Validate request data
1394
1426
  const validatedData = await validate(req.body, userValidationRules);
1395
-
1396
- const result = await korm.processRequest({
1397
- action: 'create',
1398
- data: validatedData
1399
- }, 'Users');
1400
-
1427
+
1428
+ const result = await korm.processRequest(
1429
+ {
1430
+ action: 'create',
1431
+ data: validatedData,
1432
+ },
1433
+ 'Users'
1434
+ );
1435
+
1401
1436
  res.status(201).json({
1402
1437
  success: true,
1403
1438
  message: 'User created successfully',
1404
- data: result
1439
+ data: result,
1405
1440
  });
1406
1441
  } catch (error) {
1407
1442
  if (error.name === 'ValidationError') {
1408
1443
  return res.status(400).json({
1409
1444
  success: false,
1410
1445
  message: 'Validation failed',
1411
- errors: error.message
1446
+ errors: error.message,
1412
1447
  });
1413
1448
  }
1414
-
1449
+
1415
1450
  res.status(500).json({
1416
1451
  success: false,
1417
1452
  message: 'Error creating user',
1418
- error: error.message
1453
+ error: error.message,
1419
1454
  });
1420
1455
  }
1421
1456
  });
@@ -1433,13 +1468,13 @@ const advancedRules = {
1433
1468
  phone: 'regex:phone',
1434
1469
  password: 'required|type:string|minLen:8',
1435
1470
  status: 'in:active,inactive,pending',
1436
- user_id: 'exists:users,id'
1471
+ user_id: 'exists:users,id',
1437
1472
  };
1438
1473
 
1439
1474
  const customRegex = {
1440
1475
  username: /^[a-zA-Z0-9_]+$/,
1441
1476
  email: /^[^\s@]+@[^\s@]+\.[^\s@]+$/,
1442
- phone: /^\+?[\d\s-()]{10,15}$/
1477
+ phone: /^\+?[\d\s-()]{10,15}$/,
1443
1478
  };
1444
1479
 
1445
1480
  const validatedData = await validate(data, advancedRules, { customRegex });
@@ -1530,20 +1565,24 @@ type|modifier1|modifier2|...
1530
1565
 
1531
1566
  **Column Modifiers:**
1532
1567
 
1533
- | Modifier | Description | Example |
1534
- |----------|-------------|---------|
1535
- | `size:n` | Column size | `varchar|size:255` |
1536
- | `unsigned` | Unsigned integer | `int|unsigned` |
1537
- | `primaryKey` | Primary key column | `bigint|primaryKey` |
1538
- | `autoIncrement` | Auto increment | `bigint|primaryKey|autoIncrement` |
1539
- | `notNull` | Not nullable | `varchar|size:255|notNull` |
1540
- | `unique` | Unique constraint | `varchar|unique` |
1541
- | `default:value` | Default value | `tinyint|default:1` |
1542
- | `onUpdate:value` | On update value | `timestamp|onUpdate:CURRENT_TIMESTAMP` |
1543
- | `comment:text` | Column comment | `varchar|comment:User email address` |
1544
- | `foreignKey:table:column` | Foreign key | `int|foreignKey:users:id` |
1568
+ | Modifier | Description | Example |
1569
+ | ------------------------- | ------------------ | ---------- | --------------------------- | -------------- |
1570
+ | `size:n` | Column size | `varchar | size:255` |
1571
+ | `unsigned| Unsigned integer | `int | unsigned` |
1572
+ | `primaryKey` | Primary key column | `bigint | primaryKey` |
1573
+ | `autoIncrement` | Auto increment | `bigint | primaryKey | autoIncrement` |
1574
+ | `notNull` | Not nullable | `varchar | size:255 | notNull` |
1575
+ | `unique` | Unique constraint | `varchar | unique` |
1576
+ | `default:value` | Default value | `tinyint | default:1` |
1577
+ | `onUpdate:value| On update value | `timestamp | onUpdate:CURRENT_TIMESTAMP` |
1578
+ | `comment:text` | Column comment | `varchar | comment:User email address` |
1579
+ | `foreignKey:table:column` | Foreign key | `int | foreignKey:users:id` |
1580
+
1581
+ ¹ **Engine-specific.** `unsigned` is honored on MySQL and SQLite. PostgreSQL has no unsigned integer type and silently drops the modifier.
1582
+ ² **Engine-specific.** `onUpdate` is honored on MySQL (emitted via `ON UPDATE <expr>`). PostgreSQL and SQLite log a one-time warning and ignore it — the modifier cannot be expressed inline on those engines. See [`docs/agents/05-multi-db-parity.md`](docs/agents/05-multi-db-parity.md).
1545
1583
 
1546
1584
  **Special Default Values:**
1585
+
1547
1586
  - `now` or `now()` → `CURRENT_TIMESTAMP`
1548
1587
 
1549
1588
  **Example Column Definitions:**
@@ -1591,11 +1630,11 @@ Seed data is automatically inserted when `syncDatabase()` is called and the tabl
1591
1630
  const { initializeKORM } = require('@dreamtree-org/korm-js');
1592
1631
 
1593
1632
  const korm = initializeKORM({
1594
- db: db, // Knex database instance
1595
- dbClient: 'mysql', // 'mysql', 'mysql2', 'pg', 'postgresql', 'sqlite', 'sqlite3'
1596
- schema: null, // Optional: initial schema object
1597
- resolverPath: null, // Optional: path to models directory (default: process.cwd())
1598
- debug: false // Optional: enable SQL debugging (default: false)
1633
+ db: db, // Knex database instance
1634
+ dbClient: 'mysql', // 'mysql', 'mysql2', 'pg', 'postgresql', 'sqlite', 'sqlite3'
1635
+ schema: null, // Optional: initial schema object
1636
+ resolverPath: null, // Optional: path to models directory (default: process.cwd())
1637
+ debug: false, // Optional: enable SQL debugging (default: false)
1599
1638
  });
1600
1639
 
1601
1640
  // Process any CRUD request (automatically handles other_requests if present)
@@ -1622,65 +1661,66 @@ const modelInstance = korm.getModelInstance(modelDef);
1622
1661
 
1623
1662
  ### ProcessRequest Options
1624
1663
 
1625
- | Parameter | Type | Description |
1626
- |-----------|------|-------------|
1627
- | `action` | string | Action to perform (list, show, create, update, delete, count, replace, upsert, sync) |
1628
- | `where` | object/array | Filter conditions |
1629
- | `data` | object/array | Data for create/update operations |
1630
- | `select` | array/string | Columns to select |
1631
- | `orderBy` | object/array/string | Sorting configuration |
1632
- | `limit` | number | Maximum records to return |
1633
- | `offset` | number | Records to skip |
1634
- | `page` | number | Page number (alternative to offset) |
1635
- | `with` | array | Related models to eager load |
1636
- | `withWhere` | object | Filter conditions for eager loaded relations |
1637
- | `groupBy` | array/string | Group by columns |
1638
- | `having` | object | Having conditions |
1639
- | `distinct` | boolean/array/string | Distinct results |
1640
- | `join` | object/array | Join configuration |
1641
- | `leftJoin` | object/array | Left join configuration |
1642
- | `rightJoin` | object/array | Right join configuration |
1643
- | `innerJoin` | object/array | Inner join configuration |
1644
- | `conflict` | array | Conflict columns for upsert |
1645
- | `other_requests` | object | Nested requests for related models |
1664
+ | Parameter | Type | Description |
1665
+ | ---------------- | -------------------- | ------------------------------------------------------------------------------------ |
1666
+ | `action` | string | Action to perform (list, show, create, update, delete, count, replace, upsert, sync) |
1667
+ | `where` | object/array | Filter conditions |
1668
+ | `data` | object/array | Data for create/update operations |
1669
+ | `select` | array/string | Columns to select |
1670
+ | `orderBy` | object/array/string | Sorting configuration |
1671
+ | `limit` | number | Maximum records to return |
1672
+ | `offset` | number | Records to skip |
1673
+ | `page` | number | Page number (alternative to offset) |
1674
+ | `with` | array | Related models to eager load |
1675
+ | `withWhere` | object | Filter conditions for eager loaded relations |
1676
+ | `groupBy` | array/string | Group by columns |
1677
+ | `having` | object | Having conditions |
1678
+ | `distinct` | boolean/array/string | Distinct results |
1679
+ | `join` | object/array | Join configuration |
1680
+ | `leftJoin` | object/array | Left join configuration |
1681
+ | `rightJoin` | object/array | Right join configuration |
1682
+ | `innerJoin` | object/array | Inner join configuration |
1683
+ | `conflict` | array | Conflict columns for upsert |
1684
+ | `other_requests` | object | Nested requests for related models |
1646
1685
 
1647
1686
  ### ProcessRequest Actions Summary
1648
1687
 
1649
- | Action | Description | Required Fields |
1650
- |--------|-------------|----------------|
1651
- | `list` | Get multiple records | None (optional: `where`, `select`, `orderBy`, `limit`, `offset`) |
1652
- | `show` | Get single record | `where` |
1653
- | `create` | Create new record | `data` |
1654
- | `update` | Update record(s) | `where`, `data` |
1655
- | `delete` | Delete record(s) | `where` |
1656
- | `count` | Count records | None (optional: `where`) |
1657
- | `sum` | Sum column or expression | `data.sumColumn` or `data.sumFormula` (optional: `where`) |
1658
- | `replace` | Replace record (MySQL) | `data` |
1659
- | `upsert` | Insert or update | `data`, `conflict` |
1660
- | `sync` | Upsert + delete | `data`, `conflict`, `where` |
1688
+ | Action | Description | Required Fields |
1689
+ | --------- | ------------------------ | ---------------------------------------------------------------- |
1690
+ | `list` | Get multiple records | None (optional: `where`, `select`, `orderBy`, `limit`, `offset`) |
1691
+ | `show` | Get single record | `where` |
1692
+ | `create` | Create new record | `data` |
1693
+ | `update` | Update record(s) | `where`, `data` |
1694
+ | `delete` | Delete record(s) | `where` |
1695
+ | `count` | Count records | None (optional: `where`) |
1696
+ | `sum` | Sum column or expression | `data.sumColumn` or `data.sumFormula` (optional: `where`) |
1697
+ | `replace` | Replace record (MySQL) | `data` |
1698
+ | `upsert` | Insert or update | `data`, `conflict` |
1699
+ | `sync` | Upsert + delete | `data`, `conflict`, `where` |
1661
1700
 
1662
1701
  ### Validation Rules
1663
1702
 
1664
- | Rule | Description | Example |
1665
- |------|-------------|---------|
1666
- | `required` | Field is required | `'required'` |
1667
- | `type:string` | Field must be string | `'type:string'` |
1668
- | `type:number` | Field must be number | `'type:number'` |
1669
- | `type:boolean` | Field must be boolean | `'type:boolean'` |
1670
- | `type:array` | Field must be array | `'type:array'` |
1671
- | `type:object` | Field must be object | `'type:object'` |
1672
- | `type:longText` | Field must be string > 255 chars | `'type:longText'` |
1673
- | `minLen:n` | Minimum string/array length | `'minLen:3'` |
1674
- | `maxLen:n` | Maximum string/array length | `'maxLen:255'` |
1675
- | `min:n` | Minimum numeric value | `'min:0'` |
1676
- | `max:n` | Maximum numeric value | `'max:150'` |
1677
- | `in:val1,val2` | Value must be in list | `'in:active,inactive,pending'` |
1678
- | `regex:name` | Custom regex pattern (define in options) | `'regex:email'` |
1679
- | `call:name` | Custom callback function (define in options) | `'call:myValidator'` |
1680
- | `exists:table,column` | Value must exist in database table | `'exists:users,id'` |
1681
- | `default:value` | Default value if not provided | `'default:active'` |
1703
+ | Rule | Description | Example |
1704
+ | --------------------- | -------------------------------------------- | ------------------------------ |
1705
+ | `required` | Field is required | `'required'` |
1706
+ | `type:string` | Field must be string | `'type:string'` |
1707
+ | `type:number` | Field must be number | `'type:number'` |
1708
+ | `type:boolean` | Field must be boolean | `'type:boolean'` |
1709
+ | `type:array` | Field must be array | `'type:array'` |
1710
+ | `type:object` | Field must be object | `'type:object'` |
1711
+ | `type:longText` | Field must be string > 255 chars | `'type:longText'` |
1712
+ | `minLen:n` | Minimum string/array length | `'minLen:3'` |
1713
+ | `maxLen:n` | Maximum string/array length | `'maxLen:255'` |
1714
+ | `min:n` | Minimum numeric value | `'min:0'` |
1715
+ | `max:n` | Maximum numeric value | `'max:150'` |
1716
+ | `in:val1,val2` | Value must be in list | `'in:active,inactive,pending'` |
1717
+ | `regex:name` | Custom regex pattern (define in options) | `'regex:email'` |
1718
+ | `call:name` | Custom callback function (define in options) | `'call:myValidator'` |
1719
+ | `exists:table,column` | Value must exist in database table | `'exists:users,id'` |
1720
+ | `default:value` | Default value if not provided | `'default:active'` |
1682
1721
 
1683
1722
  **Rule Chaining:** Combine multiple rules with `|` pipe character:
1723
+
1684
1724
  ```javascript
1685
1725
  {
1686
1726
  username: 'required|type:string|minLen:3|maxLen:50',
@@ -1693,14 +1733,14 @@ const modelInstance = korm.getModelInstance(modelDef);
1693
1733
  ### Library Exports
1694
1734
 
1695
1735
  ```javascript
1696
- const {
1697
- initializeKORM, // Initialize KORM with database connection
1698
- helperUtility, // Utility functions (file operations, string manipulation)
1699
- emitter, // Event emitter instance
1700
- validate, // Validation function
1701
- logger, // Logger utility instance
1702
- lib, // Additional utilities
1703
- LibClasses // Library classes (Emitter)
1736
+ const {
1737
+ initializeKORM, // Initialize KORM with database connection
1738
+ helperUtility, // Utility functions (file operations, string manipulation)
1739
+ emitter, // Event emitter instance
1740
+ validate, // Validation function
1741
+ logger, // Logger utility instance
1742
+ lib, // Additional utilities
1743
+ LibClasses, // Library classes (Emitter)
1704
1744
  } = require('@dreamtree-org/korm-js');
1705
1745
 
1706
1746
  // lib contains:
@@ -1736,14 +1776,14 @@ logger.error('Error message');
1736
1776
 
1737
1777
  ### Log Levels
1738
1778
 
1739
- | Level | Value | Description |
1740
- |-------|-------|-------------|
1741
- | `NONE` | 0 | Disable all logging |
1742
- | `ERROR` | 1 | Only errors |
1743
- | `WARN` | 2 | Warnings and errors (default) |
1744
- | `INFO` | 3 | Info, warnings, and errors |
1745
- | `LOG` | 4 | General logs and above |
1746
- | `DEBUG` | 5 | All messages (most verbose) |
1779
+ | Level | Value | Description |
1780
+ | ------- | ----- | ----------------------------- |
1781
+ | `NONE` | 0 | Disable all logging |
1782
+ | `ERROR` | 1 | Only errors |
1783
+ | `WARN` | 2 | Warnings and errors (default) |
1784
+ | `INFO` | 3 | Info, warnings, and errors |
1785
+ | `LOG` | 4 | General logs and above |
1786
+ | `DEBUG` | 5 | All messages (most verbose) |
1747
1787
 
1748
1788
  ### Configuration
1749
1789
 
@@ -1751,14 +1791,14 @@ logger.error('Error message');
1751
1791
  const { logger } = require('@dreamtree-org/korm-js');
1752
1792
 
1753
1793
  // Set log level programmatically
1754
- logger.setLevel('debug'); // Show all logs
1755
- logger.setLevel('warn'); // Only warnings and errors (default)
1756
- logger.setLevel('error'); // Only errors
1757
- logger.setLevel('none'); // Disable all logging
1794
+ logger.setLevel('debug'); // Show all logs
1795
+ logger.setLevel('warn'); // Only warnings and errors (default)
1796
+ logger.setLevel('error'); // Only errors
1797
+ logger.setLevel('none'); // Disable all logging
1758
1798
 
1759
1799
  // Enable/disable logging
1760
- logger.disable(); // Temporarily disable all logging
1761
- logger.enable(); // Re-enable logging
1800
+ logger.disable(); // Temporarily disable all logging
1801
+ logger.enable(); // Re-enable logging
1762
1802
  ```
1763
1803
 
1764
1804
  ### Environment Variable
@@ -1787,10 +1827,10 @@ const { logger } = require('@dreamtree-org/korm-js');
1787
1827
 
1788
1828
  // Create a child logger for a specific module
1789
1829
  const authLogger = logger.child('[Auth]');
1790
- authLogger.info('User logged in'); // Output: [KORM][Auth] [INFO] User logged in
1830
+ authLogger.info('User logged in'); // Output: [KORM][Auth] [INFO] User logged in
1791
1831
 
1792
1832
  const dbLogger = logger.child('[DB]');
1793
- dbLogger.debug('Query executed'); // Output: [KORM][DB] [DEBUG] Query executed
1833
+ dbLogger.debug('Query executed'); // Output: [KORM][DB] [DEBUG] Query executed
1794
1834
  ```
1795
1835
 
1796
1836
  ### Logger Options
@@ -1800,10 +1840,10 @@ const { Logger } = require('@dreamtree-org/korm-js').logger;
1800
1840
 
1801
1841
  // Create a custom logger instance
1802
1842
  const customLogger = new Logger({
1803
- prefix: '[MyApp]', // Custom prefix (default: '[KORM]')
1804
- enabled: true, // Enable/disable logging (default: true)
1805
- level: 'debug', // Log level (default: 'warn' or KORM_LOG_LEVEL)
1806
- timestamps: true // Include timestamps (default: false)
1843
+ prefix: '[MyApp]', // Custom prefix (default: '[KORM]')
1844
+ enabled: true, // Enable/disable logging (default: true)
1845
+ level: 'debug', // Log level (default: 'warn' or KORM_LOG_LEVEL)
1846
+ timestamps: true, // Include timestamps (default: false)
1807
1847
  });
1808
1848
 
1809
1849
  customLogger.info('Application started');
@@ -1829,6 +1869,38 @@ if (process.env.NODE_ENV === 'development') {
1829
1869
  }
1830
1870
  ```
1831
1871
 
1872
+ ## Inspecting queries (`dryRun`)
1873
+
1874
+ Add `dryRun: true` to any request to get back the SQL it **would** run —
1875
+ without executing anything. Useful for audit pipelines, previewing
1876
+ destructive operations, and letting an AI agent review SQL before
1877
+ committing to it.
1878
+
1879
+ ```javascript
1880
+ const result = await korm.processRequest(
1881
+ { action: 'delete', where: { status: 'archived' }, dryRun: true },
1882
+ 'Post'
1883
+ );
1884
+ // → {
1885
+ // success: true,
1886
+ // dryRun: true,
1887
+ // action: 'delete',
1888
+ // model: 'Post',
1889
+ // sql: 'delete from `posts` where `status` = ?',
1890
+ // bindings: ['archived'],
1891
+ // statements: [{ sql: '...', bindings: ['archived'] }],
1892
+ // }
1893
+ ```
1894
+
1895
+ - Validation still runs (you still get a `KormError` for a bad request).
1896
+ - `before`/`after` model hooks do **not** fire, and the database is
1897
+ untouched.
1898
+ - Bindings are returned as a separate array (not interpolated into
1899
+ `sql`), so you can re-parameterize.
1900
+ - `sync` returns both statements (upsert + delete) in `statements`.
1901
+
1902
+ Full reference: [`docs/agents/06-request-contract.md`](docs/agents/06-request-contract.md) §9.
1903
+
1832
1904
  ## SQL Debugging
1833
1905
 
1834
1906
  Enable SQL debugging to see the exact SQL statements generated by your queries. This is useful for troubleshooting complex queries and understanding how KORM translates your requests.
@@ -1841,7 +1913,7 @@ const { initializeKORM } = require('@dreamtree-org/korm-js');
1841
1913
  const korm = initializeKORM({
1842
1914
  db: db,
1843
1915
  dbClient: 'mysql',
1844
- debug: true // Enable SQL debugging
1916
+ debug: true, // Enable SQL debugging
1845
1917
  });
1846
1918
  ```
1847
1919
 
@@ -1883,10 +1955,10 @@ POST /api/Post/crud
1883
1955
 
1884
1956
  ### What's Included in sqlDebug
1885
1957
 
1886
- | Index | Query Type | Description |
1887
- |-------|------------|-------------|
1888
- | 0 | Main Query | The primary SELECT query with all WHERE, ORDER BY, LIMIT clauses |
1889
- | 1 | Count Query | The COUNT query used for pagination (only when limit > 0) |
1958
+ | Index | Query Type | Description |
1959
+ | ----- | ----------- | ---------------------------------------------------------------- |
1960
+ | 0 | Main Query | The primary SELECT query with all WHERE, ORDER BY, LIMIT clauses |
1961
+ | 1 | Count Query | The COUNT query used for pagination (only when limit > 0) |
1890
1962
 
1891
1963
  ### Debug Mode Best Practices
1892
1964
 
@@ -1895,14 +1967,14 @@ POST /api/Post/crud
1895
1967
  const korm = initializeKORM({
1896
1968
  db: db,
1897
1969
  dbClient: 'mysql',
1898
- debug: process.env.NODE_ENV === 'development'
1970
+ debug: process.env.NODE_ENV === 'development',
1899
1971
  });
1900
1972
 
1901
1973
  // Or use environment variable
1902
1974
  const korm = initializeKORM({
1903
1975
  db: db,
1904
1976
  dbClient: 'mysql',
1905
- debug: process.env.KORM_DEBUG === 'true'
1977
+ debug: process.env.KORM_DEBUG === 'true',
1906
1978
  });
1907
1979
  ```
1908
1980
 
@@ -1922,14 +1994,14 @@ const db = knex({
1922
1994
  user: process.env.DB_USER || 'root',
1923
1995
  password: process.env.DB_PASS || 'password',
1924
1996
  database: process.env.DB_NAME || 'database_name',
1925
- port: process.env.DB_PORT || 3306
1926
- }
1997
+ port: process.env.DB_PORT || 3306,
1998
+ },
1927
1999
  });
1928
2000
 
1929
2001
  const korm = initializeKORM({
1930
2002
  db: db,
1931
2003
  dbClient: 'mysql',
1932
- debug: process.env.NODE_ENV === 'development' // Enable SQL debugging in development
2004
+ debug: process.env.NODE_ENV === 'development', // Enable SQL debugging in development
1933
2005
  });
1934
2006
  ```
1935
2007
 
@@ -1945,14 +2017,14 @@ const db = knex({
1945
2017
  user: process.env.DB_USER || 'username',
1946
2018
  password: process.env.DB_PASS || 'password',
1947
2019
  database: process.env.DB_NAME || 'database_name',
1948
- port: process.env.DB_PORT || 5432
1949
- }
2020
+ port: process.env.DB_PORT || 5432,
2021
+ },
1950
2022
  });
1951
2023
 
1952
2024
  const korm = initializeKORM({
1953
2025
  db: db,
1954
2026
  dbClient: 'pg',
1955
- debug: process.env.NODE_ENV === 'development'
2027
+ debug: process.env.NODE_ENV === 'development',
1956
2028
  });
1957
2029
  ```
1958
2030
 
@@ -1964,65 +2036,202 @@ const knex = require('knex');
1964
2036
  const db = knex({
1965
2037
  client: 'sqlite3',
1966
2038
  connection: {
1967
- filename: process.env.DB_FILE || './database.sqlite'
1968
- }
2039
+ filename: process.env.DB_FILE || './database.sqlite',
2040
+ },
1969
2041
  });
1970
2042
 
1971
2043
  const korm = initializeKORM({
1972
2044
  db: db,
1973
2045
  dbClient: 'sqlite',
1974
- debug: process.env.NODE_ENV === 'development'
2046
+ debug: process.env.NODE_ENV === 'development',
1975
2047
  });
1976
2048
  ```
1977
2049
 
2050
+ ## Using KORM-JS as an AI tool
2051
+
2052
+ The discovery → request flow for an LLM agent is two calls: **describe** what's available, then build a request constrained by its **JSON Schema**.
2053
+
2054
+ ### 1. Discover the schema — `describeSchema()` / `describeModel(name)`
2055
+
2056
+ Pure-data, JSON-safe introspection (no hooks, credentials, or internals leak). Use it to load context before generating a request.
2057
+
2058
+ ```javascript
2059
+ korm.describeSchema();
2060
+ // → { schemaApiVersion: 1, models: [ { model, table, columns, relations, softDelete, actions }, … ] }
2061
+
2062
+ korm.describeModel('User');
2063
+ // → {
2064
+ // schemaApiVersion: 1,
2065
+ // model: 'User', table: 'users', alias: 'User',
2066
+ // columns: [ { name: 'id', type: 'integer', primaryKey: true, autoIncrement: true, nullable: false }, … ],
2067
+ // relations: [ { name: 'Post', type: 'many', table: 'posts', localKey: 'id', foreignKey: 'user_id' } ],
2068
+ // softDelete: false,
2069
+ // actions: ['list','show','count','sum','create','update','delete','replace','upsert','sync'],
2070
+ // }
2071
+ ```
2072
+
2073
+ Unknown models throw a `KormError` with `code: 'UNKNOWN_MODEL'` (the `context.available` list helps the caller recover).
2074
+
2075
+ ### 2. Constrain the request — `getRequestJsonSchema(modelName)`
2076
+
2077
+ `korm.getRequestJsonSchema(modelName)` returns a draft-2020-12 JSON Schema describing every valid `processRequest` body for that model — an `action`-discriminated `oneOf` with typed `data`, a `select`/`orderBy`/`conflict` constrained to the model's columns, and inline descriptions. Attach it to an OpenAI / Anthropic tool definition, or use it for client-side prevalidation, so the model's output is constrained to a request your app can actually run.
2078
+
2079
+ ```javascript
2080
+ const schema = korm.getRequestJsonSchema('User');
2081
+
2082
+ // OpenAI tool definition
2083
+ const tool = {
2084
+ type: 'function',
2085
+ function: {
2086
+ name: 'query_users',
2087
+ description: 'Query or mutate the User model via KORM-JS.',
2088
+ parameters: schema, // the oneOf-over-actions request schema
2089
+ },
2090
+ };
2091
+
2092
+ // Anthropic tool definition
2093
+ const anthropicTool = {
2094
+ name: 'query_users',
2095
+ description: 'Query or mutate the User model via KORM-JS.',
2096
+ input_schema: schema,
2097
+ };
2098
+ ```
2099
+
2100
+ The schema is derived from the model's column definitions and relations, so it stays in sync with your schema. Unknown models throw a `KormError` with `code: 'UNKNOWN_MODEL'`.
2101
+
2102
+ ## Running as an MCP server
2103
+
2104
+ KORM-JS ships with an optional [Model Context Protocol](https://modelcontextprotocol.io) server, `korm-mcp`. It exposes your KORM-registered tables as typed JSON-in/JSON-out tools that any MCP client (Claude Desktop, Claude Code, Cursor, custom agents) can call — no HTTP layer, no hand-written CRUD.
2105
+
2106
+ ### Install the SDK
2107
+
2108
+ The MCP SDK is an _optional_ dependency. If `npm install @dreamtree-org/korm-js` did not auto-install it (locked-down registry, offline mirror, etc.), pull it in explicitly:
2109
+
2110
+ ```bash
2111
+ npm install @modelcontextprotocol/sdk
2112
+ ```
2113
+
2114
+ ### Write a config
2115
+
2116
+ `korm-mcp.config.js`:
2117
+
2118
+ ```javascript
2119
+ const knex = require('knex');
2120
+ const schema = require('./schema'); // your KORM schema map
2121
+
2122
+ module.exports = {
2123
+ // Same shape as initializeKORM
2124
+ db: knex({ client: 'pg', connection: process.env.DATABASE_URL }),
2125
+ dbClient: 'pg',
2126
+ schema,
2127
+ resolverPath: './models', // optional, for model hooks
2128
+ debug: false,
2129
+
2130
+ mcp: {
2131
+ mode: 'ro', // 'ro' | 'rw' | 'rw-sync'
2132
+ allowlist: ['User', 'Post', 'Comment'], // flat list of model names; '*' allowed only in 'ro'
2133
+ blocklist: [], // applied after allowlist
2134
+ metaTools: true, // korm.list_tables, korm.describe_schema, korm.health
2135
+ allowNestedRequests: false, // gate `other_requests` (off by default)
2136
+ customActions: [], // [{ table, action, schema?, description? }]
2137
+ },
2138
+ };
2139
+ ```
2140
+
2141
+ ### Wire it into your MCP client
2142
+
2143
+ ```json
2144
+ {
2145
+ "mcpServers": {
2146
+ "my-app-db": {
2147
+ "command": "korm-mcp",
2148
+ "args": ["--config", "/abs/path/to/korm-mcp.config.js"]
2149
+ }
2150
+ }
2151
+ }
2152
+ ```
2153
+
2154
+ ### What you get
2155
+
2156
+ For each allowlisted table, the server emits one tool per action permitted by `mcp.mode`. Example for a `User` model:
2157
+
2158
+ | Tool | Available in mode | Maps to |
2159
+ | --------------- | --------------------- | -------------------------------------------- |
2160
+ | `users.list` | `ro`, `rw`, `rw-sync` | `processRequest({ action: 'list', ... })` |
2161
+ | `users.show` | `ro`, `rw`, `rw-sync` | `processRequest({ action: 'show', ... })` |
2162
+ | `users.count` | `ro`, `rw`, `rw-sync` | `processRequest({ action: 'count', ... })` |
2163
+ | `users.sum` | `ro`, `rw`, `rw-sync` | `processRequest({ action: 'sum', ... })` |
2164
+ | `users.create` | `rw`, `rw-sync` | `processRequest({ action: 'create', ... })` |
2165
+ | `users.update` | `rw`, `rw-sync` | `processRequest({ action: 'update', ... })` |
2166
+ | `users.delete` | `rw`, `rw-sync` | `processRequest({ action: 'delete', ... })` |
2167
+ | `users.upsert` | `rw`, `rw-sync` | `processRequest({ action: 'upsert', ... })` |
2168
+ | `users.replace` | `rw`, `rw-sync` | `processRequest({ action: 'replace', ... })` |
2169
+ | `users.sync` | `rw-sync` only | `processRequest({ action: 'sync', ... })` |
2170
+
2171
+ Three meta tools (unless disabled via `mcp.metaTools: false`):
2172
+
2173
+ | Tool | Purpose |
2174
+ | ---------------------- | -------------------------------------------------------------- |
2175
+ | `korm.list_tables` | List the allowlisted tables with column / relation counts. |
2176
+ | `korm.describe_schema` | Return columns + relations for a single allowlisted table. |
2177
+ | `korm.health` | Engine name, library version, allowlist size, `SELECT 1` ping. |
2178
+
2179
+ ### Safety properties
2180
+
2181
+ - **No raw SQL surface.** Tools always go through `processRequest`, which routes user-supplied values through Knex bindings.
2182
+ - **Writes are off by default.** `mcp.mode` defaults to `ro`; opting into `rw` or `rw-sync` is a deliberate config choice that also requires a non-`*` allowlist.
2183
+ - **Nested requests are off by default.** `other_requests` from the LLM is stripped unless you set `mcp.allowNestedRequests: true`.
2184
+ - **Custom action hooks are not auto-exposed.** Add an explicit entry to `mcp.customActions` to make an `on{Action}` hook callable.
2185
+
2186
+ See `docs/agents/11-mcp-server.md` for the full design rationale and the locked decisions behind these defaults.
2187
+
1978
2188
  ## Error Handling
1979
2189
 
2190
+ `processRequest` and `validate` throw a structured **`KormError`** (which
2191
+ extends the native `Error`). Branch on `error.code` rather than
2192
+ string-matching `error.message`. Full reference: [`doc/ERRORS.md`](doc/ERRORS.md).
2193
+
2194
+ | `code` | Meaning |
2195
+ | ----------------------- | -------------------------------------------------------- |
2196
+ | `NO_MATCHING_ROW` | A mutating action matched no row |
2197
+ | `UNKNOWN_ACTION` | Action isn't built-in and has no custom hook |
2198
+ | `NO_CUSTOM_ACTION_HOOK` | Custom action requested, no hook on the model |
2199
+ | `VALIDATION_FAILED` | Input failed validation (`error.context.fields`) |
2200
+ | `UNKNOWN_MODEL` | Model name not in the schema (`error.context.available`) |
2201
+ | `INTERNAL` | Internal invariant / misconfiguration |
2202
+
1980
2203
  ```javascript
1981
- // Global error handler
1982
- app.use((error, req, res, next) => {
1983
- console.error('KORM Error:', error);
1984
-
1985
- res.status(error.status || 500).json({
1986
- success: false,
1987
- message: 'Internal server error',
1988
- error: process.env.NODE_ENV === 'development' ? error.message : 'Something went wrong',
1989
- stack: process.env.NODE_ENV === 'development' ? error.stack : undefined
1990
- });
1991
- });
2204
+ const { KormError } = require('@dreamtree-org/korm-js');
1992
2205
 
1993
- // Route-specific error handling
1994
2206
  app.post('/api/:model/crud', async (req, res) => {
1995
2207
  try {
1996
- const { model } = req.params;
1997
- const result = await korm.processRequest(req.body, model);
2208
+ const result = await korm.processRequest(req.body, req.params.model);
1998
2209
  res.json(result);
1999
2210
  } catch (error) {
2000
- // Handle validation errors
2001
- if (error.name === 'ValidationError') {
2002
- return res.status(400).json({
2003
- success: false,
2004
- message: 'Validation failed',
2005
- errors: error.message
2006
- });
2007
- }
2008
-
2009
- // Handle not found errors
2010
- if (error.message.includes('not found')) {
2011
- return res.status(404).json({
2012
- success: false,
2013
- message: error.message
2014
- });
2211
+ if (error instanceof KormError) {
2212
+ const status =
2213
+ error.code === 'UNKNOWN_MODEL' || error.code === 'NO_MATCHING_ROW'
2214
+ ? 404
2215
+ : error.code === 'VALIDATION_FAILED' ||
2216
+ error.code === 'UNKNOWN_ACTION' ||
2217
+ error.code === 'NO_CUSTOM_ACTION_HOOK'
2218
+ ? 400
2219
+ : 500;
2220
+ // error.toJSON() { name, code, message, hint, context, suggestedFixes }
2221
+ return res.status(status).json({ success: false, error: error.toJSON() });
2015
2222
  }
2016
-
2017
- // Handle other errors
2018
- res.status(400).json({
2019
- success: false,
2020
- message: error.message
2021
- });
2223
+ res.status(500).json({ success: false, error: 'Internal server error' });
2022
2224
  }
2023
2225
  });
2024
2226
  ```
2025
2227
 
2228
+ > **Migration note.** Validation errors previously surfaced with
2229
+ > `name: 'ValidationError'`. They are now `KormError` with
2230
+ > `code === 'VALIDATION_FAILED'` (the raw field errors remain on
2231
+ > `error.errors` for back-compat; per-field detail is also under
2232
+ > `error.context.fields`). Switch `error.name === 'ValidationError'`
2233
+ > checks to `error.code === 'VALIDATION_FAILED'`.
2234
+
2026
2235
  ## Complete Example Application
2027
2236
 
2028
2237
  ```javascript
@@ -2042,15 +2251,15 @@ const db = knex({
2042
2251
  user: process.env.DB_USER || 'root',
2043
2252
  password: process.env.DB_PASS || 'password',
2044
2253
  database: process.env.DB_NAME || 'my_database',
2045
- port: process.env.DB_PORT || 3306
2046
- }
2254
+ port: process.env.DB_PORT || 3306,
2255
+ },
2047
2256
  });
2048
2257
 
2049
2258
  // Initialize KORM
2050
2259
  const korm = initializeKORM({
2051
2260
  db: db,
2052
2261
  dbClient: 'mysql',
2053
- debug: process.env.NODE_ENV === 'development' // SQL debugging in dev mode
2262
+ debug: process.env.NODE_ENV === 'development', // SQL debugging in dev mode
2054
2263
  });
2055
2264
 
2056
2265
  // Initialize app
@@ -2065,7 +2274,7 @@ async function initApp() {
2065
2274
  helperUtility.file.createDirectory('schema');
2066
2275
  helperUtility.file.writeJSON('schema/schema.json', schema);
2067
2276
  }
2068
-
2277
+
2069
2278
  // Sync database
2070
2279
  await korm.syncDatabase();
2071
2280
  console.log('✅ Database synced');