@dreamtree-org/korm-js 1.0.53 → 1.0.55
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/BaseHelperUtility.js +1 -1
- package/ControllerWrapper.js +1 -1
- package/Emitter.js +1 -1
- package/KormError.js +1 -0
- package/README.md +463 -254
- package/RequestValidator.js +1 -1
- package/ai-skills/korm-js.md +265 -0
- package/bin/korm-mcp.js +2 -0
- package/build.js +1 -1
- package/cli.js +2 -0
- package/clients/BaseSyncTable.js +1 -0
- package/clients/mysql/BaseUtility.js +1 -1
- package/clients/mysql/CurdTable.js +1 -1
- package/clients/mysql/DataTypeMap.js +1 -1
- package/clients/mysql/HookService.js +1 -1
- package/clients/mysql/QueryBuilder.js +1 -1
- package/clients/mysql/QueryService.js +1 -1
- package/clients/mysql/SyncTable.js +1 -1
- package/clients/pg/BaseUtility.js +1 -1
- package/clients/pg/CurdTable.js +1 -1
- package/clients/pg/DataTypeMap.js +1 -1
- package/clients/pg/HookService.js +1 -1
- package/clients/pg/QueryBuilder.js +1 -1
- package/clients/pg/QueryService.js +1 -1
- package/clients/pg/SyncTable.js +1 -1
- package/clients/sqlite/BaseUtility.js +1 -1
- package/clients/sqlite/CurdTable.js +1 -1
- package/clients/sqlite/HookService.js +1 -1
- package/clients/sqlite/QueryBuilder.js +1 -1
- package/clients/sqlite/QueryService.js +1 -1
- package/clients/sqlite/SyncTable.js +1 -1
- package/columnSchema.js +1 -0
- package/helpers/files.js +1 -1
- package/index.d.ts +213 -0
- package/index.js +1 -1
- package/jest.config.js +1 -1
- package/package.json +13 -4
- package/requestSchema.js +1 -0
- package/schemaDescribe.js +1 -0
- package/src/mcp/errors.js +1 -0
- package/src/mcp/schemaIntrospect.js +1 -0
- package/src/mcp/server.js +1 -0
- package/src/mcp/toolGenerator.js +1 -0
- package/TableSchemaSync.js +0 -1
package/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
|
|
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
|
|
603
|
-
|
|
604
|
-
| `=` (default) | Equals
|
|
605
|
-
| `>=`
|
|
606
|
-
| `<=`
|
|
607
|
-
| `>`
|
|
608
|
-
| `<`
|
|
609
|
-
| `!=`
|
|
610
|
-
| `like`
|
|
611
|
-
| `in`
|
|
612
|
-
| `notIn`
|
|
613
|
-
| `between`
|
|
614
|
-
| `notBetween`
|
|
615
|
-
| `null`
|
|
616
|
-
| `Or:column`
|
|
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
|
|
916
|
-
|
|
917
|
-
| `"!RelName": true`
|
|
918
|
-
| `"Parent.!Child": true`
|
|
919
|
-
| `"Parent.!Child.column": value` | NOT EXISTS with condition
|
|
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
|
|
987
|
-
|
|
988
|
-
| Affects parent query | Yes (filters parent rows)
|
|
989
|
-
| Purpose
|
|
990
|
-
| SQL generated
|
|
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
|
-
|
|
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
|
|
1159
|
-
|
|
1160
|
-
| `rows`
|
|
1161
|
-
| `relName`
|
|
1162
|
-
| `model`
|
|
1163
|
-
| `withTree`
|
|
1164
|
-
| `controller` | ControllerWrapper instance
|
|
1165
|
-
| `relation`
|
|
1166
|
-
| `qb`
|
|
1167
|
-
| `db`
|
|
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
|
|
1316
|
-
|
|
1317
|
-
| `model`
|
|
1318
|
-
| `action`
|
|
1319
|
-
| `request`
|
|
1320
|
-
| `context`
|
|
1321
|
-
| `db`
|
|
1322
|
-
| `utils`
|
|
1323
|
-
| `controller` | ControllerWrapper instance
|
|
1324
|
-
| `data`
|
|
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
|
|
1329
|
-
|
|
1330
|
-
| Validate
|
|
1331
|
-
| Before
|
|
1332
|
-
| After
|
|
1333
|
-
| Custom Action
|
|
1334
|
-
| Soft Delete
|
|
1335
|
-
| Custom Relation | `get{RelName}Relation` | During eager loading
|
|
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
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
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
|
|
1534
|
-
|
|
1535
|
-
| `size:n`
|
|
1536
|
-
| `unsigned
|
|
1537
|
-
| `primaryKey`
|
|
1538
|
-
| `autoIncrement`
|
|
1539
|
-
| `notNull`
|
|
1540
|
-
| `unique`
|
|
1541
|
-
| `default:value`
|
|
1542
|
-
| `onUpdate:value
|
|
1543
|
-
| `comment:text`
|
|
1544
|
-
| `foreignKey:table:column` | Foreign key
|
|
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,
|
|
1595
|
-
dbClient: 'mysql',
|
|
1596
|
-
schema: null,
|
|
1597
|
-
resolverPath: null,
|
|
1598
|
-
debug: 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
|
|
1626
|
-
|
|
1627
|
-
| `action`
|
|
1628
|
-
| `where`
|
|
1629
|
-
| `data`
|
|
1630
|
-
| `select`
|
|
1631
|
-
| `orderBy`
|
|
1632
|
-
| `limit`
|
|
1633
|
-
| `offset`
|
|
1634
|
-
| `page`
|
|
1635
|
-
| `with`
|
|
1636
|
-
| `withWhere`
|
|
1637
|
-
| `groupBy`
|
|
1638
|
-
| `having`
|
|
1639
|
-
| `distinct`
|
|
1640
|
-
| `join`
|
|
1641
|
-
| `leftJoin`
|
|
1642
|
-
| `rightJoin`
|
|
1643
|
-
| `innerJoin`
|
|
1644
|
-
| `conflict`
|
|
1645
|
-
| `other_requests` | object
|
|
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
|
|
1650
|
-
|
|
1651
|
-
| `list`
|
|
1652
|
-
| `show`
|
|
1653
|
-
| `create`
|
|
1654
|
-
| `update`
|
|
1655
|
-
| `delete`
|
|
1656
|
-
| `count`
|
|
1657
|
-
| `sum`
|
|
1658
|
-
| `replace` | Replace record (MySQL)
|
|
1659
|
-
| `upsert`
|
|
1660
|
-
| `sync`
|
|
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
|
|
1665
|
-
|
|
1666
|
-
| `required`
|
|
1667
|
-
| `type:string`
|
|
1668
|
-
| `type:number`
|
|
1669
|
-
| `type:boolean`
|
|
1670
|
-
| `type:array`
|
|
1671
|
-
| `type:object`
|
|
1672
|
-
| `type:longText`
|
|
1673
|
-
| `minLen:n`
|
|
1674
|
-
| `maxLen:n`
|
|
1675
|
-
| `min:n`
|
|
1676
|
-
| `max:n`
|
|
1677
|
-
| `in:val1,val2`
|
|
1678
|
-
| `regex:name`
|
|
1679
|
-
| `call:name`
|
|
1680
|
-
| `exists:table,column` | Value must exist in database table
|
|
1681
|
-
| `default:value`
|
|
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,
|
|
1698
|
-
helperUtility,
|
|
1699
|
-
emitter,
|
|
1700
|
-
validate,
|
|
1701
|
-
logger,
|
|
1702
|
-
lib,
|
|
1703
|
-
LibClasses
|
|
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
|
|
1740
|
-
|
|
1741
|
-
| `NONE`
|
|
1742
|
-
| `ERROR` | 1
|
|
1743
|
-
| `WARN`
|
|
1744
|
-
| `INFO`
|
|
1745
|
-
| `LOG`
|
|
1746
|
-
| `DEBUG` | 5
|
|
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');
|
|
1755
|
-
logger.setLevel('warn');
|
|
1756
|
-
logger.setLevel('error');
|
|
1757
|
-
logger.setLevel('none');
|
|
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();
|
|
1761
|
-
logger.enable();
|
|
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');
|
|
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');
|
|
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]',
|
|
1804
|
-
enabled: true,
|
|
1805
|
-
level: 'debug',
|
|
1806
|
-
timestamps: true
|
|
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
|
|
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
|
|
1887
|
-
|
|
1888
|
-
| 0
|
|
1889
|
-
| 1
|
|
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'
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
2001
|
-
|
|
2002
|
-
|
|
2003
|
-
|
|
2004
|
-
|
|
2005
|
-
|
|
2006
|
-
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
|
|
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'
|
|
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');
|