@dreamtree-org/korm-js 1.0.45 → 1.0.47

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/README.md CHANGED
@@ -17,6 +17,8 @@
17
17
  - 🔄 **Schema Generation**: Auto-generate schemas from existing databases
18
18
  - 🗑️ **Soft Delete**: Built-in soft delete support with automatic filtering
19
19
  - 🎣 **Model Hooks**: Before, After, Validate, and Custom action hooks
20
+ - 🔗 **Custom Relations**: Define custom relation hooks for complex data fetching
21
+ - 📦 **Nested Requests**: Process multiple related requests in a single call
20
22
 
21
23
  ## Installation
22
24
 
@@ -156,7 +158,7 @@ POST /api/Users/crud
156
158
  "action": "list",
157
159
  "where": { "is_active": true },
158
160
  "select": ["id", "username", "email", "first_name", "last_name"],
159
- "sort": [["created_at", "desc"]],
161
+ "orderBy": { "column": "created_at", "direction": "desc" },
160
162
  "limit": 10
161
163
  }
162
164
 
@@ -165,7 +167,7 @@ POST /api/Users/crud
165
167
  {
166
168
  "action": "list",
167
169
  "select": ["id", "username", "email"],
168
- "sort": [["created_at", "desc"]],
170
+ "orderBy": { "column": "created_at", "direction": "desc" },
169
171
  "limit": 10,
170
172
  "offset": 20
171
173
  }
@@ -181,7 +183,10 @@ POST /api/Users/crud
181
183
  "is_active": true
182
184
  },
183
185
  "select": ["id", "username", "email", "first_name", "last_name"],
184
- "sort": [["created_at", "desc"], ["last_name", "asc"]],
186
+ "orderBy": [
187
+ { "column": "created_at", "direction": "desc" },
188
+ { "column": "last_name", "direction": "asc" }
189
+ ],
185
190
  "limit": 10
186
191
  }
187
192
 
@@ -584,63 +589,98 @@ POST /api/Users/crud
584
589
  // AND role IN ('admin', 'moderator', 'editor') AND score BETWEEN 50 AND 100
585
590
  ```
586
591
 
587
- ### Sorting
592
+ ### Sorting (orderBy)
588
593
 
589
594
  ```javascript
590
- // Single sort
595
+ // Single sort with object
591
596
  POST /api/Users/crud
592
597
  {
593
598
  "action": "list",
594
- "sort": [["created_at", "desc"]]
599
+ "orderBy": { "column": "created_at", "direction": "desc" }
595
600
  }
596
601
 
597
- // Multiple sorts
602
+ // Single sort with string (default: ascending)
598
603
  POST /api/Users/crud
599
604
  {
600
605
  "action": "list",
601
- "sort": [
602
- ["is_active", "desc"],
603
- ["created_at", "desc"],
604
- ["last_name", "asc"]
606
+ "orderBy": "created_at"
607
+ }
608
+
609
+ // Multiple sorts with array of objects
610
+ POST /api/Users/crud
611
+ {
612
+ "action": "list",
613
+ "orderBy": [
614
+ { "column": "is_active", "direction": "desc" },
615
+ { "column": "created_at", "direction": "desc" },
616
+ { "column": "last_name", "direction": "asc" }
605
617
  ]
606
618
  }
619
+
620
+ // Multiple sorts with array of strings (all ascending)
621
+ POST /api/Users/crud
622
+ {
623
+ "action": "list",
624
+ "orderBy": ["is_active", "created_at", "last_name"]
625
+ }
607
626
  ```
608
627
 
609
628
  ### Joins
610
629
 
611
630
  ```javascript
612
- // Inner join
631
+ // Inner join (using innerJoin parameter)
613
632
  POST /api/Users/crud
614
633
  {
615
634
  "action": "list",
616
- "select": ["users.id", "users.username", "profiles.bio"],
617
- "join": [
618
- {
619
- "table": "user_profiles",
620
- "as": "profiles",
621
- "on": "users.id = profiles.user_id",
622
- "type": "inner"
623
- }
624
- ],
635
+ "select": ["users.id", "users.username", "user_profiles.bio"],
636
+ "innerJoin": {
637
+ "table": "user_profiles",
638
+ "on": "users.id = user_profiles.user_id"
639
+ },
625
640
  "where": { "users.is_active": true }
626
641
  }
627
642
 
628
- // Left join
643
+ // Left join (using leftJoin parameter)
629
644
  POST /api/Users/crud
630
645
  {
631
646
  "action": "list",
632
- "select": ["users.*", "profiles.bio"],
633
- "join": [
634
- {
635
- "table": "user_profiles",
636
- "as": "profiles",
637
- "on": "users.id = profiles.user_id",
638
- "type": "left"
639
- }
647
+ "select": ["users.*", "user_profiles.bio"],
648
+ "leftJoin": {
649
+ "table": "user_profiles",
650
+ "on": "users.id = user_profiles.user_id"
651
+ }
652
+ }
653
+
654
+ // Multiple joins (array format)
655
+ POST /api/Users/crud
656
+ {
657
+ "action": "list",
658
+ "select": ["users.*", "profiles.bio", "roles.name"],
659
+ "leftJoin": [
660
+ { "table": "user_profiles", "on": "users.id = user_profiles.user_id" },
661
+ { "table": "roles", "on": "users.role_id = roles.id" }
640
662
  ]
641
663
  }
664
+
665
+ // Join with explicit columns
666
+ POST /api/Users/crud
667
+ {
668
+ "action": "list",
669
+ "innerJoin": {
670
+ "table": "orders",
671
+ "first": "users.id",
672
+ "operator": "=",
673
+ "second": "orders.user_id"
674
+ }
675
+ }
642
676
  ```
643
677
 
678
+ **Available join types:**
679
+ - `join` - Regular join
680
+ - `innerJoin` - Inner join
681
+ - `leftJoin` - Left join
682
+ - `rightJoin` - Right join
683
+
644
684
  ## Relational Support with hasRelations
645
685
 
646
686
  ### Understanding hasRelations Structure
@@ -833,6 +873,90 @@ Example complete relationship structure:
833
873
  }
834
874
  ```
835
875
 
876
+ ### Custom Relation Hooks
877
+
878
+ 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:
879
+ - Virtual/computed relations
880
+ - Cross-database relations
881
+ - Complex aggregations
882
+ - Custom data transformations
883
+
884
+ Create a method named `get{RelationName}Relation` in your model file:
885
+
886
+ ```javascript
887
+ // models/Users.model.js
888
+ class Users {
889
+ // Custom relation hook for "Statistics" relation
890
+ async getStatisticsRelation({ rows, relName, model, withTree, controller, relation, qb, db }) {
891
+ // rows = parent rows to attach relation data to
892
+ // db = Knex database instance
893
+ // qb = QueryBuilder instance
894
+
895
+ for (const row of rows) {
896
+ // Fetch custom data for each row
897
+ const stats = await db('user_statistics')
898
+ .where('user_id', row.id)
899
+ .first();
900
+
901
+ // Attach to row
902
+ row.Statistics = stats || { posts: 0, comments: 0, likes: 0 };
903
+ }
904
+ }
905
+
906
+ // Custom relation with aggregation
907
+ async getPostCountRelation({ rows, db }) {
908
+ const userIds = rows.map(r => r.id);
909
+
910
+ const counts = await db('posts')
911
+ .select('user_id')
912
+ .count('* as count')
913
+ .whereIn('user_id', userIds)
914
+ .groupBy('user_id');
915
+
916
+ const countMap = new Map(counts.map(c => [c.user_id, c.count]));
917
+
918
+ for (const row of rows) {
919
+ row.PostCount = countMap.get(row.id) || 0;
920
+ }
921
+ }
922
+
923
+ // Custom relation from external API or different database
924
+ async getExternalProfileRelation({ rows }) {
925
+ for (const row of rows) {
926
+ // Fetch from external source
927
+ row.ExternalProfile = await fetchFromExternalAPI(row.external_id);
928
+ }
929
+ }
930
+ }
931
+
932
+ module.exports = Users;
933
+ ```
934
+
935
+ **Using Custom Relations:**
936
+
937
+ ```javascript
938
+ // Use custom relation just like schema-defined relations
939
+ POST /api/Users/crud
940
+ {
941
+ "action": "list",
942
+ "where": { "is_active": true },
943
+ "with": ["Statistics", "PostCount", "ExternalProfile"]
944
+ }
945
+ ```
946
+
947
+ **Custom Relation Hook Arguments:**
948
+
949
+ | Argument | Description |
950
+ |----------|-------------|
951
+ | `rows` | Parent rows to attach relation data to (modify in place) |
952
+ | `relName` | The relation name being fetched |
953
+ | `model` | Model definition object |
954
+ | `withTree` | Nested relations tree for further loading |
955
+ | `controller` | ControllerWrapper instance |
956
+ | `relation` | Relation definition from schema (may be undefined for custom relations) |
957
+ | `qb` | QueryBuilder instance for building queries |
958
+ | `db` | Knex database instance for direct queries |
959
+
836
960
  ### Important Notes
837
961
 
838
962
  - `type: "one"` = One-to-One or Belongs-To relationship
@@ -842,6 +966,7 @@ Example complete relationship structure:
842
966
  - `foreignKey` = The key in the related table
843
967
  - `throughLocalKey` = The key in the join table pointing to current model (for many-to-many)
844
968
  - `throughForeignKey` = The key in the join table pointing to related model (for many-to-many)
969
+ - Custom relation hooks take precedence when relation is not defined in schema
845
970
 
846
971
  ## Soft Delete Support
847
972
 
@@ -900,6 +1025,9 @@ Create a model file at `models/Users.model.js`:
900
1025
 
901
1026
  ```javascript
902
1027
  class Users {
1028
+ // Soft delete support (property, not method)
1029
+ hasSoftDelete = true;
1030
+
903
1031
  // Validation hook - runs before any action
904
1032
  async validate({ model, action, request, context, db, utils, controller }) {
905
1033
  if (action === 'create' || action === 'update') {
@@ -916,61 +1044,121 @@ class Users {
916
1044
  }
917
1045
  }
918
1046
 
919
- // Before hook - runs before the action executes
1047
+ // Before hooks - run before the action executes
1048
+ // Method naming: before{Action} (e.g., beforeCreate, beforeUpdate, beforeList, beforeDelete)
920
1049
  async beforeCreate({ model, action, request, context, db, utils, controller }) {
921
- // Add timestamps
1050
+ // Modify request data before insert
922
1051
  request.data.created_at = new Date();
923
1052
  request.data.updated_at = new Date();
924
1053
  return request.data;
925
1054
  }
926
1055
 
927
1056
  async beforeUpdate({ model, action, request, context, db, utils, controller }) {
928
- // Update timestamp
1057
+ // Modify request data before update
929
1058
  request.data.updated_at = new Date();
930
1059
  return request.data;
931
1060
  }
932
1061
 
933
- // After hook - runs after the action executes
1062
+ async beforeDelete({ model, action, request, context, db, utils, controller }) {
1063
+ // Logic before delete (works with both hard and soft delete)
1064
+ console.log('Deleting user:', request.where);
1065
+ }
1066
+
1067
+ // After hooks - run after the action executes
1068
+ // Method naming: after{Action} (e.g., afterCreate, afterUpdate, afterList, afterDelete)
934
1069
  async afterCreate({ model, action, data, request, context, db, utils, controller }) {
935
- // Log creation
1070
+ // data contains the result of the action
936
1071
  console.log('User created:', data);
937
- // Send welcome email, etc.
1072
+ // Send welcome email, trigger notifications, etc.
938
1073
  return data;
939
1074
  }
940
1075
 
941
1076
  async afterUpdate({ model, action, data, request, context, db, utils, controller }) {
942
- // Log update
943
1077
  console.log('User updated:', data);
944
1078
  return data;
945
1079
  }
946
1080
 
947
- // Custom action hook
948
- async onCustomAction({ model, action, request, context, db, utils, controller }) {
949
- // Handle custom actions like 'activate', 'deactivate', etc.
950
- if (action === 'activate') {
951
- return await db('users')
952
- .where(request.where)
953
- .update({ is_active: true, updated_at: new Date() });
954
- }
955
- throw new Error(`Unknown custom action: ${action}`);
1081
+ async afterList({ model, action, data, request, context, db, utils, controller }) {
1082
+ // Modify list results before returning
1083
+ return data;
956
1084
  }
957
1085
 
958
- // Soft delete support
959
- hasSoftDelete = true;
1086
+ // Custom action hooks
1087
+ // Method naming: on{Action}Action (e.g., onActivateAction, onDeactivateAction)
1088
+ async onActivateAction({ model, action, request, context, db, utils, controller }) {
1089
+ return await db('users')
1090
+ .where(request.where)
1091
+ .update({ is_active: true, updated_at: new Date() });
1092
+ }
1093
+
1094
+ async onDeactivateAction({ model, action, request, context, db, utils, controller }) {
1095
+ return await db('users')
1096
+ .where(request.where)
1097
+ .update({ is_active: false, updated_at: new Date() });
1098
+ }
960
1099
  }
961
1100
 
962
1101
  module.exports = Users;
963
1102
  ```
964
1103
 
1104
+ ### Hook Arguments Reference
1105
+
1106
+ | Argument | Description |
1107
+ |----------|-------------|
1108
+ | `model` | Model definition object with table, columns, relations |
1109
+ | `action` | Current action being performed (create, update, delete, etc.) |
1110
+ | `request` | The request object containing where, data, etc. |
1111
+ | `context` | Custom context passed from the controller |
1112
+ | `db` | Knex database instance for direct queries |
1113
+ | `utils` | Utility functions |
1114
+ | `controller` | ControllerWrapper instance |
1115
+ | `data` | (After hooks only) Result of the action |
1116
+
1117
+ ### Complete Hook Types Reference
1118
+
1119
+ | Hook Type | Method Naming | When Called | Use Case |
1120
+ |-----------|---------------|-------------|----------|
1121
+ | Validate | `validate` | Before any action | Input validation, authorization |
1122
+ | Before | `before{Action}` | Before action executes | Modify request data, add timestamps |
1123
+ | After | `after{Action}` | After action executes | Transform results, trigger side effects |
1124
+ | Custom Action | `on{Action}Action` | For custom actions | Implement business logic |
1125
+ | Soft Delete | `hasSoftDelete = true` | During delete/list | Enable soft delete |
1126
+ | Custom Relation | `get{RelName}Relation` | During eager loading | Custom data fetching |
1127
+
1128
+ **Available Before/After hooks:**
1129
+ - `beforeCreate` / `afterCreate`
1130
+ - `beforeUpdate` / `afterUpdate`
1131
+ - `beforeDelete` / `afterDelete`
1132
+ - `beforeList` / `afterList`
1133
+ - `beforeShow` / `afterShow`
1134
+ - `beforeCount` / `afterCount`
1135
+ - `beforeReplace` / `afterReplace`
1136
+ - `beforeUpsert` / `afterUpsert`
1137
+ - `beforeSync` / `afterSync`
1138
+
965
1139
  ### Using Custom Actions
966
1140
 
967
1141
  ```javascript
968
- // Call custom action
1142
+ // Call custom action - triggers on{Action}Action hook
969
1143
  POST /api/Users/crud
970
1144
  {
971
1145
  "action": "activate",
972
1146
  "where": { "id": 1 }
973
1147
  }
1148
+
1149
+ POST /api/Users/crud
1150
+ {
1151
+ "action": "deactivate",
1152
+ "where": { "id": 1 }
1153
+ }
1154
+
1155
+ // You can create any custom action
1156
+ POST /api/Users/crud
1157
+ {
1158
+ "action": "sendWelcomeEmail",
1159
+ "where": { "id": 1 }
1160
+ }
1161
+ // Triggers: onSendWelcomeEmailAction({ model, action, request, context, db, utils, controller })
974
1162
  ```
975
1163
 
976
1164
  ## Data Validation
@@ -1099,16 +1287,16 @@ POST /api/Users/crud
1099
1287
  "alias": "Users",
1100
1288
  "modelName": "Users",
1101
1289
  "columns": {
1102
- "id": "bigint|size:8|unsigned|auto_increment",
1103
- "username": "varchar|size:255",
1104
- "email": "varchar|size:255",
1105
- "first_name": "varchar|size:255|nullable",
1106
- "last_name": "varchar|size:255|nullable",
1107
- "age": "int|size:4|nullable",
1290
+ "id": "bigint|size:8|unsigned|primaryKey|autoIncrement",
1291
+ "username": "varchar|size:255|notNull|unique",
1292
+ "email": "varchar|size:255|notNull",
1293
+ "first_name": "varchar|size:255",
1294
+ "last_name": "varchar|size:255",
1295
+ "age": "int|size:4",
1108
1296
  "is_active": "tinyint|size:1|default:1",
1109
1297
  "created_at": "timestamp|default:CURRENT_TIMESTAMP",
1110
1298
  "updated_at": "timestamp|default:CURRENT_TIMESTAMP|onUpdate:CURRENT_TIMESTAMP",
1111
- "deleted_at": "timestamp|nullable"
1299
+ "deleted_at": "timestamp"
1112
1300
  },
1113
1301
  "seed": [],
1114
1302
  "hasRelations": {},
@@ -1123,54 +1311,135 @@ POST /api/Users/crud
1123
1311
  }
1124
1312
  ```
1125
1313
 
1314
+ ### Column Definition String Format
1315
+
1316
+ Column definitions use a pipe-separated string format:
1317
+
1318
+ ```
1319
+ type|modifier1|modifier2|...
1320
+ ```
1321
+
1322
+ **Column Modifiers:**
1323
+
1324
+ | Modifier | Description | Example |
1325
+ |----------|-------------|---------|
1326
+ | `size:n` | Column size | `varchar|size:255` |
1327
+ | `unsigned` | Unsigned integer | `int|unsigned` |
1328
+ | `primaryKey` | Primary key column | `bigint|primaryKey` |
1329
+ | `autoIncrement` | Auto increment | `bigint|primaryKey|autoIncrement` |
1330
+ | `notNull` | Not nullable | `varchar|size:255|notNull` |
1331
+ | `unique` | Unique constraint | `varchar|unique` |
1332
+ | `default:value` | Default value | `tinyint|default:1` |
1333
+ | `onUpdate:value` | On update value | `timestamp|onUpdate:CURRENT_TIMESTAMP` |
1334
+ | `comment:text` | Column comment | `varchar|comment:User email address` |
1335
+ | `foreignKey:table:column` | Foreign key | `int|foreignKey:users:id` |
1336
+
1337
+ **Special Default Values:**
1338
+ - `now` or `now()` → `CURRENT_TIMESTAMP`
1339
+
1340
+ **Example Column Definitions:**
1341
+
1342
+ ```javascript
1343
+ {
1344
+ "id": "bigint|size:8|unsigned|primaryKey|autoIncrement",
1345
+ "user_id": "int|unsigned|notNull|foreignKey:users:id",
1346
+ "status": "enum|in:active,inactive,pending|default:pending",
1347
+ "created_at": "timestamp|default:now",
1348
+ "updated_at": "timestamp|default:now|onUpdate:now",
1349
+ "bio": "text|comment:User biography"
1350
+ }
1351
+ ```
1352
+
1353
+ ### Seed Data
1354
+
1355
+ You can define seed data in the schema to auto-populate tables:
1356
+
1357
+ ```javascript
1358
+ {
1359
+ "Roles": {
1360
+ "table": "roles",
1361
+ "columns": {
1362
+ "id": "int|primaryKey|autoIncrement",
1363
+ "name": "varchar|size:50|notNull|unique",
1364
+ "description": "text"
1365
+ },
1366
+ "seed": [
1367
+ { "name": "admin", "description": "Administrator role" },
1368
+ { "name": "user", "description": "Regular user role" },
1369
+ { "name": "moderator", "description": "Moderator role" }
1370
+ ]
1371
+ }
1372
+ }
1373
+ ```
1374
+
1375
+ Seed data is automatically inserted when `syncDatabase()` is called and the table is empty.
1376
+
1126
1377
  ## API Reference
1127
1378
 
1128
1379
  ### KORM Instance Methods
1129
1380
 
1130
1381
  ```javascript
1382
+ const { initializeKORM } = require('@dreamtree-org/korm-js');
1383
+
1131
1384
  const korm = initializeKORM({
1132
- db: db,
1133
- dbClient: 'mysql' // or 'pg', 'sqlite'
1385
+ db: db, // Knex database instance
1386
+ dbClient: 'mysql', // 'mysql', 'mysql2', 'pg', 'postgresql', 'sqlite', 'sqlite3'
1387
+ schema: null, // Optional: initial schema object
1388
+ resolverPath: null // Optional: path to models directory (default: process.cwd())
1134
1389
  });
1135
1390
 
1136
- // Process any CRUD request
1137
- const result = await korm.processRequest(requestBody, modelName);
1391
+ // Process any CRUD request (automatically handles other_requests if present)
1392
+ const result = await korm.processRequest(requestBody, modelName, context);
1138
1393
 
1139
- // Process request with nested requests
1140
- const result = await korm.processRequestWithOthers(requestBody, modelName);
1394
+ // Process request with nested requests (legacy - now same as processRequest)
1395
+ const result = await korm.processRequestWithOthers(requestBody, modelName, context);
1141
1396
 
1142
1397
  // Set schema manually
1143
1398
  korm.setSchema(schemaObject);
1144
1399
 
1145
- // Sync database with schema
1400
+ // Sync database with schema (creates/updates tables)
1146
1401
  await korm.syncDatabase();
1147
1402
 
1148
1403
  // Generate schema from existing database
1149
1404
  const schema = await korm.generateSchema();
1150
- ```
1151
1405
 
1152
- ### ControllerWrapper Static Methods
1153
-
1154
- ```javascript
1155
- const { ControllerWrapper } = require('@dreamtree-org/korm-js');
1156
-
1157
- // Load model class
1158
- const ModelClass = ControllerWrapper.loadModelClass('Users');
1406
+ // Load model class from models/{ModelName}.model.js
1407
+ const ModelClass = korm.loadModelClass('Users');
1159
1408
 
1160
1409
  // Get model instance
1161
- const modelInstance = ControllerWrapper.getModelInstance(model);
1162
-
1163
- // Generate schema
1164
- const schema = await ControllerWrapper.generateSchema();
1410
+ const modelInstance = korm.getModelInstance(modelDef);
1165
1411
  ```
1166
1412
 
1413
+ ### ProcessRequest Options
1414
+
1415
+ | Parameter | Type | Description |
1416
+ |-----------|------|-------------|
1417
+ | `action` | string | Action to perform (list, show, create, update, delete, count, replace, upsert, sync) |
1418
+ | `where` | object/array | Filter conditions |
1419
+ | `data` | object/array | Data for create/update operations |
1420
+ | `select` | array/string | Columns to select |
1421
+ | `orderBy` | object/array/string | Sorting configuration |
1422
+ | `limit` | number | Maximum records to return |
1423
+ | `offset` | number | Records to skip |
1424
+ | `page` | number | Page number (alternative to offset) |
1425
+ | `with` | array | Related models to eager load |
1426
+ | `groupBy` | array/string | Group by columns |
1427
+ | `having` | object | Having conditions |
1428
+ | `distinct` | boolean/array/string | Distinct results |
1429
+ | `join` | object/array | Join configuration |
1430
+ | `leftJoin` | object/array | Left join configuration |
1431
+ | `rightJoin` | object/array | Right join configuration |
1432
+ | `innerJoin` | object/array | Inner join configuration |
1433
+ | `conflict` | array | Conflict columns for upsert |
1434
+ | `other_requests` | object | Nested requests for related models |
1435
+
1167
1436
  ### ProcessRequest Actions Summary
1168
1437
 
1169
1438
  | Action | Description | Required Fields |
1170
1439
  |--------|-------------|----------------|
1171
- | `create` | Create new record | `data` |
1172
- | `list` | Get multiple records | None (optional: `where`, `select`, `sort`, `limit`, `offset`) |
1440
+ | `list` | Get multiple records | None (optional: `where`, `select`, `orderBy`, `limit`, `offset`) |
1173
1441
  | `show` | Get single record | `where` |
1442
+ | `create` | Create new record | `data` |
1174
1443
  | `update` | Update record(s) | `where`, `data` |
1175
1444
  | `delete` | Delete record(s) | `where` |
1176
1445
  | `count` | Count records | None (optional: `where`) |
@@ -1182,19 +1451,58 @@ const schema = await ControllerWrapper.generateSchema();
1182
1451
 
1183
1452
  | Rule | Description | Example |
1184
1453
  |------|-------------|---------|
1185
- | `required` | Field is required | `username: 'required'` |
1186
- | `type:string` | Field must be string | `name: 'type:string'` |
1187
- | `type:number` | Field must be number | `age: 'type:number'` |
1188
- | `type:boolean` | Field must be boolean | `is_active: 'type:boolean'` |
1189
- | `minLen:n` | Minimum length | `username: 'minLen:3'` |
1190
- | `maxLen:n` | Maximum length | `email: 'maxLen:255'` |
1191
- | `min:n` | Minimum value | `age: 'min:0'` |
1192
- | `max:n` | Maximum value | `age: 'max:150'` |
1193
- | `in:val1,val2` | Value must be in list | `status: 'in:active,inactive'` |
1194
- | `regex:name` | Custom regex pattern | `email: 'regex:email'` |
1195
- | `call:name` | Custom callback function | `field: 'call:myValidator'` |
1196
- | `exists:table,column` | Value must exist in table | `user_id: 'exists:users,id'` |
1197
- | `default:value` | Default value if not provided | `status: 'default:active'` |
1454
+ | `required` | Field is required | `'required'` |
1455
+ | `type:string` | Field must be string | `'type:string'` |
1456
+ | `type:number` | Field must be number | `'type:number'` |
1457
+ | `type:boolean` | Field must be boolean | `'type:boolean'` |
1458
+ | `type:array` | Field must be array | `'type:array'` |
1459
+ | `type:object` | Field must be object | `'type:object'` |
1460
+ | `type:longText` | Field must be string > 255 chars | `'type:longText'` |
1461
+ | `minLen:n` | Minimum string/array length | `'minLen:3'` |
1462
+ | `maxLen:n` | Maximum string/array length | `'maxLen:255'` |
1463
+ | `min:n` | Minimum numeric value | `'min:0'` |
1464
+ | `max:n` | Maximum numeric value | `'max:150'` |
1465
+ | `in:val1,val2` | Value must be in list | `'in:active,inactive,pending'` |
1466
+ | `regex:name` | Custom regex pattern (define in options) | `'regex:email'` |
1467
+ | `call:name` | Custom callback function (define in options) | `'call:myValidator'` |
1468
+ | `exists:table,column` | Value must exist in database table | `'exists:users,id'` |
1469
+ | `default:value` | Default value if not provided | `'default:active'` |
1470
+
1471
+ **Rule Chaining:** Combine multiple rules with `|` pipe character:
1472
+ ```javascript
1473
+ {
1474
+ username: 'required|type:string|minLen:3|maxLen:50',
1475
+ email: 'required|type:string|regex:email',
1476
+ age: 'type:number|min:0|max:150',
1477
+ status: 'in:active,inactive|default:active'
1478
+ }
1479
+ ```
1480
+
1481
+ ### Library Exports
1482
+
1483
+ ```javascript
1484
+ const {
1485
+ initializeKORM, // Initialize KORM with database connection
1486
+ helperUtility, // Utility functions (file operations, string manipulation)
1487
+ emitter, // Event emitter instance
1488
+ validate, // Validation function
1489
+ lib, // Additional utilities
1490
+ LibClasses // Library classes (Emitter)
1491
+ } = require('@dreamtree-org/korm-js');
1492
+
1493
+ // lib contains:
1494
+ // - createValidationMiddleware(rules, options) - Express middleware
1495
+ // - validateEmail(email) - Email validation
1496
+ // - validatePassword(password) - Password strength validation
1497
+ // - validatePhone(phone) - Phone number validation
1498
+ // - validatePAN(pan) - PAN validation (India)
1499
+ // - validateAadhaar(aadhaar) - Aadhaar validation (India)
1500
+
1501
+ // helperUtility.file contains:
1502
+ // - readJSON(path) - Read JSON file
1503
+ // - writeJSON(path, data) - Write JSON file
1504
+ // - createDirectory(path) - Create directory
1505
+ ```
1198
1506
 
1199
1507
  ## Database Support
1200
1508
 
@@ -1 +1 @@
1
- const path=require("path");class HookService{constructor(e,t,o=null){this.db=e,this.utils=t,this.controllerWrapper=o,this.appRoot=o&&o.resolverPath?o.resolverPath:process.cwd()}loadModelClass(e){try{const t=path.join(this.appRoot,"models",`${e}.model.js`);delete require.cache[require.resolve(t)];return require(t)}catch(e){return void console.log("loadModelClass error",{err:e})}}getModelInstance(e){const t=e.modelName,o=this.loadModelClass(t);if(o)return"function"==typeof o?new o:o}resolveModelHook(e,t,o){const r=e.modelName,s=this.loadModelClass(r);if(!s)return;const l=this.getModelInstance(e);let i;if("validate"===t)i="validate";else if("on"===t)i=`on${o.charAt(0).toUpperCase()+o.slice(1)}`;else if("before"===t)i=`before${o.charAt(0).toUpperCase()+o.slice(1)}`;else if("after"===t)i=`after${o.charAt(0).toUpperCase()+o.slice(1)}`;else{if("custom"!==t)return;i=`on${o.charAt(0).toUpperCase()+o.slice(1)}Action`}return"function"==typeof l[i]?l[i].bind(l):"function"==typeof s[i]?s[i].bind(s):void 0}async executeValidatorHook({model:e,action:t,request:o,ctx:r,controller:s}){const l=this.resolveModelHook(e,"validate",t);if(l)return await l({model:e,action:t,request:o,context:r,db:this.db,utils:this.utils,controller:s})}async executeBeforeHook({model:e,action:t,request:o,ctx:r,controller:s}){const l=this.resolveModelHook(e,"before",t);if(l)return await l({model:e,action:t,request:o,context:r,db:this.db,utils:this.utils,controller:s})}async executeAfterHook({model:e,action:t,data:o,request:r,ctx:s,controller:l}){const i=this.resolveModelHook(e,"after",t);return i?await i({model:e,action:t,data:o,request:r,context:s,db:this.db,utils:this.utils,controller:l}):o}async executeCustomAction({model:e,action:t,request:o,ctx:r,controller:s}){const l=this.resolveModelHook(e,"custom",t);if(l)return await l({model:e,action:t,request:o,context:r,db:this.db,utils:this.utils,controller:s});throw new Error(`No custom action hook found for ${e.modelName}.${t}`)}async executeHasSoftDeleteHook(e){const t=this.getModelInstance(e);if(!t)return!1;return t.hasOwnProperty("hasSoftDelete")&&!0===t.hasSoftDelete}}module.exports=HookService;
1
+ const path=require("path");class HookService{constructor(e,t,o=null){this.db=e,this.utils=t,this.controllerWrapper=o,this.controllerWrapper.hookService=this,this.appRoot=o&&o.resolverPath?o.resolverPath:process.cwd()}loadModelClass(e){try{const t=path.join(this.appRoot,"models",`${e}.model.js`);delete require.cache[require.resolve(t)];return require(t)}catch(e){return void console.log("loadModelClass error",{err:e})}}getModelInstance(e){let t="string"==typeof e?e:e.modelName;const o=this.loadModelClass(t);if(o)return"function"==typeof o?new o:o}resolveModelHook(e,t,o){const r=e.modelName,s=this.loadModelClass(r);if(!s)return;const l=this.getModelInstance(e);let i;if("validate"===t)i="validate";else if("on"===t)i=`on${o.charAt(0).toUpperCase()+o.slice(1)}`;else if("before"===t)i=`before${o.charAt(0).toUpperCase()+o.slice(1)}`;else if("after"===t)i=`after${o.charAt(0).toUpperCase()+o.slice(1)}`;else{if("custom"!==t)return;i=`on${o.charAt(0).toUpperCase()+o.slice(1)}Action`}return"function"==typeof l[i]?l[i].bind(l):"function"==typeof s[i]?s[i].bind(s):void 0}async executeValidatorHook({model:e,action:t,request:o,ctx:r,controller:s}){const l=this.resolveModelHook(e,"validate",t);if(l)return await l({model:e,action:t,request:o,context:r,db:this.db,utils:this.utils,controller:s})}async executeBeforeHook({model:e,action:t,request:o,ctx:r,controller:s}){const l=this.resolveModelHook(e,"before",t);if(l)return await l({model:e,action:t,request:o,context:r,db:this.db,utils:this.utils,controller:s})}async executeAfterHook({model:e,action:t,data:o,request:r,ctx:s,controller:l}){const i=this.resolveModelHook(e,"after",t);return i?await i({model:e,action:t,data:o,request:r,context:s,db:this.db,utils:this.utils,controller:l}):o}async executeCustomAction({model:e,action:t,request:o,ctx:r,controller:s}){const l=this.resolveModelHook(e,"custom",t);if(l)return await l({model:e,action:t,request:o,context:r,db:this.db,utils:this.utils,controller:s});throw new Error(`No custom action hook found for ${e.modelName}.${t}`)}async executeHasSoftDeleteHook(e){const t=this.getModelInstance(e);if(!t)return!1;return t.hasOwnProperty("hasSoftDelete")&&!0===t.hasSoftDelete}}module.exports=HookService;
@@ -1 +1 @@
1
- const HelperUtility=require("./HelperUtility");class QueryBuilder{constructor(e,t,r=null){this.db=e,this.utils=t,this.controllerWrapper=r,this.helperUtility=new HelperUtility}getQueryBuilder(e,t=null){let r=this.db(e.table);return t&&(r=t),r._getMyModel=()=>e,r}parseValue(e){return this.helperUtility.parseValue(e)}parseWhereValue(e){return this.helperUtility.parseWhereValue(e)}parseWhereColumn(e){return this.helperUtility.parseWhereColumn(e)}_applyOrWhereCondition(e,t,r,i){if(null!==i)if(Array.isArray(i))e.orWhereIn(t,i);else switch(r){case"between":e.orWhereBetween(t,i);break;case"notBetween":e.orWhereNotBetween(t,i);break;case"in":e.orWhereIn(t,i);break;case"notIn":e.orWhereNotIn(t,i);break;case"like":e.orWhereRaw(`\`${t}\` LIKE ?`,[i]);break;default:e.orWhere(t,r,i)}else e.orWhereNull(t)}_applyAndWhereCondition(e,t,r,i){if(null!==i)if(Array.isArray(i))e.whereIn(t,i);else switch(r){case"between":e.whereBetween(t,i);break;case"notBetween":e.whereNotBetween(t,i);break;case"in":e.whereIn(t,i);break;case"notIn":e.whereNotIn(t,i);break;case"like":e.whereRaw(`\`${t}\` LIKE ?`,[i]);break;default:e.where(t,r,i)}else e.whereNull(t)}buildWithTree(e,t=null){return this.helperUtility.dotWalkTree(e,{resolver:({current:e,part:r,source:i})=>t?t({current:e,part:r,source:i}):{}})}async fetchRelatedRows(e,t){if(e.through){let r=await this.db(e.through).whereIn(e.throughLocalKey,t);return await this.db(e.table).whereIn(e.foreignKey,r.map(t=>t[e.throughForeignKey]))}return this.db(e.table).whereIn(e.foreignKey,t)}async fetchAndAttachRelated(e){const{parentRows:t,relName:r,model:i,withTree:l,relation:o}=e;let a=t.map(e=>e[o.localKey]),s=[];s=await this.fetchRelatedRows(o,a);let n=new Map;for(const e of s){let t=e[o.foreignKey];n.has(t)||n.set(t,[]),n.get(t).push(e)}for(const e of t){let t=e[o.localKey];"one"===o?.type&&1==n.get(t)?.length?e[r]=n.get(t)[0]:e[r]=n.get(t)||[]}let h=Object.keys(l);for(const e of h){let i=l[e],o=t.filter(e=>e[r].length>0).map(e=>e[r]).reduce((e,t)=>e.concat(t),[]),a=this.utils.getModel(this.controllerWrapper,r);await this.fetchAndAttachRelated({parentRows:o,relName:e,model:a,withTree:i,relation:a.hasRelations[e]})}return t}filterWhere(e,t=""){return t?Object.keys(e).filter(e=>e.includes(t)).reduce((r,i)=>(r[i.replace(t,"")]=e[i],r),{}):Object.keys(e).filter(e=>!e.includes(".")).reduce((t,r)=>(t[r]=e[r],t),{})}async getQuery(e,t){try{const{where:r={},with:i,withWhere:l,select:o,orderBy:a={column:"id",direction:"asc"},limit:s=10,offset:n=0,page:h,groupBy:p,having:c,distinct:y,join:u,leftJoin:f,rightJoin:d,innerJoin:g,count:w=!1}=t;let W=this.getQueryBuilder(e);o&&(Array.isArray(o)||"string"==typeof o)?W.select(o):W.select("*"),y&&(Array.isArray(y)||"string"==typeof y?W.distinct(y):W.distinct()),u&&this._applyJoins(W,u,"join"),f&&this._applyJoins(W,f,"leftJoin"),d&&this._applyJoins(W,d,"rightJoin"),g&&this._applyJoins(W,g,"innerJoin"),this._applyWhereClause(W,r,i),p&&(Array.isArray(p),W.groupBy(p)),c&&this._applyHavingClause(W,c),a&&this._applyOrderBy(W,a);let b=!1,A=s,_=n,m=1,k=0;h&&s>0&&(m=Math.max(1,parseInt(h)),_=(m-1)*s),s>0&&(W.limit(A),_>0&&W.offset(_));const j=await W;if(i&&i.length>0){const t=this.buildWithTree(i);for(const r of Object.keys(t))await this.fetchAndAttachRelated({parentRows:j,relName:r,model:e,withTree:t[r],relation:e.hasRelations[r]})}let B=null;if(s>0)try{let t=this.getQueryBuilder(e);r&&Object.keys(r).length>0&&this._applyWhereClause(t,r),u&&this._applyJoins(t,u,"join"),f&&this._applyJoins(t,f,"leftJoin"),d&&this._applyJoins(t,d,"rightJoin"),g&&this._applyJoins(t,g,"innerJoin");B=(await t.count("* as cnt").first()).cnt}catch(e){console.warn("Failed to get total count:",e.message),B=j.length}s>0&&null!==B&&(k=Math.ceil(B/s),b=m<k);return{data:j,totalCount:B,...s>0?{pagination:{page:m,limit:A,offset:_,totalPages:k,hasNext:b,hasPrev:m>1,nextPage:b?m+1:null,prevPage:m>1?m-1:null}}:{}}}catch(e){throw console.error("QueryService.getQuery error:",e),new Error(`Failed to execute query: ${e.message}`)}}_applyWhereWithArray(e,t,r){for(const i of t)this._applyWhereClause(e,i,r)}_applyWhereClause(e,t,r=[]){if(Array.isArray(t))this._applyWhereWithArray(e,t,r);else{if(t&&Object.keys(t).length>0){let r=this.helperUtility.getDotWalkQuery(t);console.log({filteredWhere:r}),r=this.helperUtility.objectFilter(r,(e,t)=>"object"!=typeof t||null===t);for(const[t,i]of Object.entries(r)){const{joinType:r="AND",column:l}=this.parseWhereColumn(t),{operator:o,value:a}=this.parseWhereValue(i);"AND"===r?this._applyAndWhereCondition(e,l,o,a):this._applyOrWhereCondition(e,l,o,a)}}r&&r.length>0&&this._applyNestedWhere(e,t,r)}}_applyNestedWhere(e,t,r){let i=this,l=e._getMyModel();if(r&&r.length>0)for(const o of r){let a=this.helperUtility.getDotWalkQuery(t,o);if(a&&Object.keys(a).length>0){let t=this.utils.getModel(this.controllerWrapper,o),s=l.hasRelations[o],n=r.map(e=>this.helperUtility.pluckDotWalkKey(e,1));e.whereExists(function(){let e=i.getQueryBuilder(t,this.select("*").from(t.table));e.whereRaw(`${s.foreignKey} = ${l.table}.${s.localKey}`),i._applyWhereClause(e,a,n)})}}}_applyJoins(e,t,r){const i=Array.isArray(t)?t:[t];for(const t of i)"string"==typeof t?e[r](t):"object"==typeof t&&(t.table&&t.on?e[r](t.table,t.on):t.table&&t.first&&t.operator&&t.second&&e[r](t.table,t.first,t.operator,t.second))}_applyWithWhere(e,t){try{if(Array.isArray(t))for(const r of t)"string"==typeof r?e.withWhere(r):"object"==typeof r&&e.withWhere(r.column,r.operator,r.value);else if("object"==typeof t)for(const[r,i]of Object.entries(t))e.withWhere(r,i)}catch(e){console.warn("Failed to apply withWhere:",e.message)}}_applyHavingClause(e,t){for(const[r,i]of Object.entries(t))"object"==typeof i&&i.operator?e.having(r,i.operator,i.value):e.having(r,i)}_applyOrderBy(e,t){if(Array.isArray(t))for(const r of t)"string"==typeof r?e.orderBy(r):"object"==typeof r&&e.orderBy(r.column,r.direction||"asc");else"string"==typeof t?e.orderBy(t):"object"==typeof t&&e.orderBy(t.column,t.direction||"asc")}}module.exports=QueryBuilder;
1
+ const HelperUtility=require("./HelperUtility");class QueryBuilder{constructor(e,t,r=null){this.db=e,this.utils=t,this.controllerWrapper=r,this.helperUtility=new HelperUtility}getHookService(){return this.controllerWrapper.hookService}getQueryBuilder(e,t=null){let r=this.db(e.table);return t&&(r=t),r._getMyModel=()=>e,r}parseValue(e){return this.helperUtility.parseValue(e)}parseWhereValue(e){return this.helperUtility.parseWhereValue(e)}parseWhereColumn(e){return this.helperUtility.parseWhereColumn(e)}_applyOrWhereCondition(e,t,r,i){if(null!==i)if(Array.isArray(i))e.orWhereIn(t,i);else switch(r){case"between":e.orWhereBetween(t,i);break;case"notBetween":e.orWhereNotBetween(t,i);break;case"in":e.orWhereIn(t,i);break;case"notIn":e.orWhereNotIn(t,i);break;case"like":e.orWhereRaw(`\`${t}\` LIKE ?`,[i]);break;default:e.orWhere(t,r,i)}else e.orWhereNull(t)}_applyAndWhereCondition(e,t,r,i){if(null!==i)if(Array.isArray(i))e.whereIn(t,i);else switch(r){case"between":e.whereBetween(t,i);break;case"notBetween":e.whereNotBetween(t,i);break;case"in":e.whereIn(t,i);break;case"notIn":e.whereNotIn(t,i);break;case"like":e.whereRaw(`\`${t}\` LIKE ?`,[i]);break;default:e.where(t,r,i)}else e.whereNull(t)}buildWithTree(e,t=null){return this.helperUtility.dotWalkTree(e,{resolver:({current:e,part:r,source:i})=>t?t({current:e,part:r,source:i}):{}})}async fetchRelatedRows(e,t){if(e.through){let r=await this.db(e.through).whereIn(e.throughLocalKey,t);return await this.db(e.table).whereIn(e.foreignKey,r.map(t=>t[e.throughForeignKey]))}return this.db(e.table).whereIn(e.foreignKey,t)}async fetchAndAttachRelated(e){const{parentRows:t,relName:r,model:i,withTree:o,relation:l}=e;if(!l){const e=this.getHookService().getModelInstance(i),l=`get${r.charAt(0).toUpperCase()+r.slice(1)}Relation`;if("function"==typeof e[l]){const a={rows:t,relName:r,model:i,withTree:o,controller:this.controllerWrapper,relation:i.hasRelations[r],qb:this,db:this.db};await e[l](a)}return t}let a=t.map(e=>e[l.localKey]),s=[];const n="one"===l?.type;s=await this.fetchRelatedRows(l,a);let h=new Map;for(const e of s){let t=e[l.foreignKey];h.has(t)||h.set(t,[]),h.get(t).push(e)}for(const e of t){let t=e[l.localKey];n&&1==h.get(t)?.length?e[r]=h.get(t)[0]:e[r]=h.get(t)||[]}let c=Object.keys(o);for(const e of c){let i=o[e],l=t.filter(e=>n?e[r]:e[r].length>0).map(e=>e[r]).reduce((e,t)=>e.concat(t),[]),a=this.utils.getModel(this.controllerWrapper,r);await this.fetchAndAttachRelated({parentRows:l,relName:e,model:a,withTree:i,relation:a.hasRelations[e]})}return t}filterWhere(e,t=""){return t?Object.keys(e).filter(e=>e.includes(t)).reduce((r,i)=>(r[i.replace(t,"")]=e[i],r),{}):Object.keys(e).filter(e=>!e.includes(".")).reduce((t,r)=>(t[r]=e[r],t),{})}async getQuery(e,t){try{const{where:r={},with:i,withWhere:o,select:l,orderBy:a={column:"id",direction:"asc"},limit:s=10,offset:n=0,page:h,groupBy:c,having:p,distinct:y,join:u,leftJoin:f,rightJoin:d,innerJoin:g,count:w=!1}=t;let b=this.getQueryBuilder(e);l&&(Array.isArray(l)||"string"==typeof l)?b.select(l):b.select("*"),y&&(Array.isArray(y)||"string"==typeof y?b.distinct(y):b.distinct()),u&&this._applyJoins(b,u,"join"),f&&this._applyJoins(b,f,"leftJoin"),d&&this._applyJoins(b,d,"rightJoin"),g&&this._applyJoins(b,g,"innerJoin"),this._applyWhereClause(b,r,i),c&&(Array.isArray(c),b.groupBy(c)),p&&this._applyHavingClause(b,p),a&&this._applyOrderBy(b,a);let W=!1,A=s,m=n,_=1,k=0;h&&s>0&&(_=Math.max(1,parseInt(h)),m=(_-1)*s),s>0&&(b.limit(A),m>0&&b.offset(m));const j=await b;if(i&&i.length>0){const t=this.buildWithTree(i);for(const r of Object.keys(t))await this.fetchAndAttachRelated({parentRows:j,relName:r,model:e,withTree:t[r],relation:e.hasRelations[r]})}let B=null;if(s>0)try{let t=this.getQueryBuilder(e);r&&Object.keys(r).length>0&&this._applyWhereClause(t,r),u&&this._applyJoins(t,u,"join"),f&&this._applyJoins(t,f,"leftJoin"),d&&this._applyJoins(t,d,"rightJoin"),g&&this._applyJoins(t,g,"innerJoin");B=(await t.count("* as cnt").first()).cnt}catch(e){console.warn("Failed to get total count:",e.message),B=j.length}s>0&&null!==B&&(k=Math.ceil(B/s),W=_<k);return{data:j,totalCount:B,...s>0?{pagination:{page:_,limit:A,offset:m,totalPages:k,hasNext:W,hasPrev:_>1,nextPage:W?_+1:null,prevPage:_>1?_-1:null}}:{}}}catch(e){throw console.error("QueryService.getQuery error:",e),new Error(`Failed to execute query: ${e.message}`)}}_applyWhereWithArray(e,t,r){for(const i of t)this._applyWhereClause(e,i,r)}_applyWhereClause(e,t,r=[]){if(Array.isArray(t))this._applyWhereWithArray(e,t,r);else{if(t&&Object.keys(t).length>0){let r=this.helperUtility.getDotWalkQuery(t);console.log({filteredWhere:r}),r=this.helperUtility.objectFilter(r,(e,t)=>"object"!=typeof t||null===t);for(const[t,i]of Object.entries(r)){const{joinType:r="AND",column:o}=this.parseWhereColumn(t),{operator:l,value:a}=this.parseWhereValue(i);"AND"===r?this._applyAndWhereCondition(e,o,l,a):this._applyOrWhereCondition(e,o,l,a)}}r&&r.length>0&&this._applyNestedWhere(e,t,r)}}_applyNestedWhere(e,t,r){let i=this,o=e._getMyModel();if(r&&r.length>0)for(const l of r){let a=this.helperUtility.getDotWalkQuery(t,l);if(a&&Object.keys(a).length>0){let t=this.utils.getModel(this.controllerWrapper,l),s=o.hasRelations[l],n=r.map(e=>this.helperUtility.pluckDotWalkKey(e,1));e.whereExists(function(){let e=i.getQueryBuilder(t,this.select("*").from(t.table));e.whereRaw(`${s.foreignKey} = ${o.table}.${s.localKey}`),i._applyWhereClause(e,a,n)})}}}_applyJoins(e,t,r){const i=Array.isArray(t)?t:[t];for(const t of i)"string"==typeof t?e[r](t):"object"==typeof t&&(t.table&&t.on?e[r](t.table,t.on):t.table&&t.first&&t.operator&&t.second&&e[r](t.table,t.first,t.operator,t.second))}_applyWithWhere(e,t){try{if(Array.isArray(t))for(const r of t)"string"==typeof r?e.withWhere(r):"object"==typeof r&&e.withWhere(r.column,r.operator,r.value);else if("object"==typeof t)for(const[r,i]of Object.entries(t))e.withWhere(r,i)}catch(e){console.warn("Failed to apply withWhere:",e.message)}}_applyHavingClause(e,t){for(const[r,i]of Object.entries(t))"object"==typeof i&&i.operator?e.having(r,i.operator,i.value):e.having(r,i)}_applyOrderBy(e,t){if(Array.isArray(t))for(const r of t)"string"==typeof r?e.orderBy(r):"object"==typeof r&&e.orderBy(r.column,r.direction||"asc");else"string"==typeof t?e.orderBy(t):"object"==typeof t&&e.orderBy(t.column,t.direction||"asc")}}module.exports=QueryBuilder;
@@ -1 +1 @@
1
- const path=require("path");class HookService{constructor(e,t,o=null){this.db=e,this.utils=t,this.controllerWrapper=o,this.appRoot=o&&o.resolverPath?o.resolverPath:process.cwd()}loadModelClass(e){try{const t=path.join(this.appRoot,"models",`${e}.model.js`);delete require.cache[require.resolve(t)];return require(t)}catch(e){return void console.log("loadModelClass error",{err:e})}}getModelInstance(e){const t=e.modelName,o=this.loadModelClass(t);if(o)return"function"==typeof o?new o:o}resolveModelHook(e,t,o){const r=e.modelName,s=this.loadModelClass(r);if(!s)return;const l=this.getModelInstance(e);let i;if("validate"===t)i="validate";else if("on"===t)i=`on${o.charAt(0).toUpperCase()+o.slice(1)}`;else if("before"===t)i=`before${o.charAt(0).toUpperCase()+o.slice(1)}`;else if("after"===t)i=`after${o.charAt(0).toUpperCase()+o.slice(1)}`;else{if("custom"!==t)return;i=`on${o.charAt(0).toUpperCase()+o.slice(1)}Action`}return"function"==typeof l[i]?l[i].bind(l):"function"==typeof s[i]?s[i].bind(s):void 0}async executeValidatorHook({model:e,action:t,request:o,ctx:r,controller:s}){const l=this.resolveModelHook(e,"validate",t);if(l)return await l({model:e,action:t,request:o,context:r,db:this.db,utils:this.utils,controller:s})}async executeBeforeHook({model:e,action:t,request:o,ctx:r,controller:s}){const l=this.resolveModelHook(e,"before",t);if(l)return await l({model:e,action:t,request:o,context:r,db:this.db,utils:this.utils,controller:s})}async executeAfterHook({model:e,action:t,data:o,request:r,ctx:s,controller:l}){const i=this.resolveModelHook(e,"after",t);return i?await i({model:e,action:t,data:o,request:r,context:s,db:this.db,utils:this.utils,controller:l}):o}async executeCustomAction({model:e,action:t,request:o,ctx:r,controller:s}){const l=this.resolveModelHook(e,"custom",t);if(l)return await l({model:e,action:t,request:o,context:r,db:this.db,utils:this.utils,controller:s});throw new Error(`No custom action hook found for ${e.modelName}.${t}`)}async executeHasSoftDeleteHook(e){const t=this.getModelInstance(e);if(!t)return!1;return t.hasOwnProperty("hasSoftDelete")&&!0===t.hasSoftDelete}}module.exports=HookService;
1
+ const path=require("path");class HookService{constructor(e,t,o=null){this.db=e,this.utils=t,this.controllerWrapper=o,this.controllerWrapper.hookService=this,this.appRoot=o&&o.resolverPath?o.resolverPath:process.cwd()}loadModelClass(e){try{const t=path.join(this.appRoot,"models",`${e}.model.js`);delete require.cache[require.resolve(t)];return require(t)}catch(e){return void console.log("loadModelClass error",{err:e})}}getModelInstance(e){let t="string"==typeof e?e:e.modelName;const o=this.loadModelClass(t);if(o)return"function"==typeof o?new o:o}resolveModelHook(e,t,o){const r=e.modelName,s=this.loadModelClass(r);if(!s)return;const l=this.getModelInstance(e);let i;if("validate"===t)i="validate";else if("on"===t)i=`on${o.charAt(0).toUpperCase()+o.slice(1)}`;else if("before"===t)i=`before${o.charAt(0).toUpperCase()+o.slice(1)}`;else if("after"===t)i=`after${o.charAt(0).toUpperCase()+o.slice(1)}`;else{if("custom"!==t)return;i=`on${o.charAt(0).toUpperCase()+o.slice(1)}Action`}return"function"==typeof l[i]?l[i].bind(l):"function"==typeof s[i]?s[i].bind(s):void 0}async executeValidatorHook({model:e,action:t,request:o,ctx:r,controller:s}){const l=this.resolveModelHook(e,"validate",t);if(l)return await l({model:e,action:t,request:o,context:r,db:this.db,utils:this.utils,controller:s})}async executeBeforeHook({model:e,action:t,request:o,ctx:r,controller:s}){const l=this.resolveModelHook(e,"before",t);if(l)return await l({model:e,action:t,request:o,context:r,db:this.db,utils:this.utils,controller:s})}async executeAfterHook({model:e,action:t,data:o,request:r,ctx:s,controller:l}){const i=this.resolveModelHook(e,"after",t);return i?await i({model:e,action:t,data:o,request:r,context:s,db:this.db,utils:this.utils,controller:l}):o}async executeCustomAction({model:e,action:t,request:o,ctx:r,controller:s}){const l=this.resolveModelHook(e,"custom",t);if(l)return await l({model:e,action:t,request:o,context:r,db:this.db,utils:this.utils,controller:s});throw new Error(`No custom action hook found for ${e.modelName}.${t}`)}async executeHasSoftDeleteHook(e){const t=this.getModelInstance(e);if(!t)return!1;return t.hasOwnProperty("hasSoftDelete")&&!0===t.hasSoftDelete}}module.exports=HookService;
@@ -1 +1 @@
1
- const HelperUtility=require("./HelperUtility");class QueryBuilder{constructor(e,t,r=null){this.db=e,this.utils=t,this.controllerWrapper=r,this.helperUtility=new HelperUtility}getQueryBuilder(e,t=null){let r=this.db(e.table);return t&&(r=t),r._getMyModel=()=>e,r}parseValue(e){return this.helperUtility.parseValue(e)}parseWhereValue(e){return this.helperUtility.parseWhereValue(e)}parseWhereColumn(e){return this.helperUtility.parseWhereColumn(e)}_applyOrWhereCondition(e,t,r,i){if(null!==i)if(Array.isArray(i))e.orWhereIn(t,i);else switch(r){case"between":e.orWhereBetween(t,i);break;case"notBetween":e.orWhereNotBetween(t,i);break;case"in":e.orWhereIn(t,i);break;case"notIn":e.orWhereNotIn(t,i);break;case"like":e.orWhere(t,"like",i);break;default:e.orWhere(t,r,i)}else e.orWhereNull(t)}_applyAndWhereCondition(e,t,r,i){if(null!==i)if(Array.isArray(i))e.whereIn(t,i);else switch(r){case"between":e.whereBetween(t,i);break;case"notBetween":e.whereNotBetween(t,i);break;case"in":e.whereIn(t,i);break;case"notIn":e.whereNotIn(t,i);break;case"like":e.where(t,"like",i);break;default:e.where(t,r,i)}else e.whereNull(t)}buildWithTree(e,t=null){return this.helperUtility.dotWalkTree(e,{resolver:({current:e,part:r,source:i})=>t?t({current:e,part:r,source:i}):{}})}async fetchRelatedRows(e,t){if(e.through){let r=await this.db(e.through).whereIn(e.throughLocalKey,t);return await this.db(e.table).whereIn(e.foreignKey,r.map(t=>t[e.throughForeignKey]))}return this.db(e.table).whereIn(e.foreignKey,t)}async fetchAndAttachRelated(e){const{parentRows:t,relName:r,model:i,withTree:l,relation:o}=e;let a=t.map(e=>e[o.localKey]),s=[];s=await this.fetchRelatedRows(o,a);let n=new Map;for(const e of s){let t=e[o.foreignKey];n.has(t)||n.set(t,[]),n.get(t).push(e)}for(const e of t){let t=e[o.localKey];"one"===o?.type&&1==n.get(t)?.length?e[r]=n.get(t)[0]:e[r]=n.get(t)||[]}let h=Object.keys(l);for(const e of h){let i=l[e],o=t.filter(e=>e[r].length>0).map(e=>e[r]).reduce((e,t)=>e.concat(t),[]),a=this.utils.getModel(this.controllerWrapper,r);await this.fetchAndAttachRelated({parentRows:o,relName:e,model:a,withTree:i,relation:a.hasRelations[e]})}return t}filterWhere(e,t=""){return t?Object.keys(e).filter(e=>e.includes(t)).reduce((r,i)=>(r[i.replace(t,"")]=e[i],r),{}):Object.keys(e).filter(e=>!e.includes(".")).reduce((t,r)=>(t[r]=e[r],t),{})}async getQuery(e,t){try{const{where:r={},with:i,withWhere:l,select:o,orderBy:a={column:"id",direction:"asc"},limit:s=10,offset:n=0,page:h,groupBy:p,having:c,distinct:y,join:u,leftJoin:f,rightJoin:d,innerJoin:g,count:w=!1}=t;let W=this.getQueryBuilder(e);o&&(Array.isArray(o)||"string"==typeof o)?W.select(o):W.select("*"),y&&(Array.isArray(y)||"string"==typeof y?W.distinct(y):W.distinct()),u&&this._applyJoins(W,u,"join"),f&&this._applyJoins(W,f,"leftJoin"),d&&this._applyJoins(W,d,"rightJoin"),g&&this._applyJoins(W,g,"innerJoin"),this._applyWhereClause(W,r,i),p&&(Array.isArray(p),W.groupBy(p)),c&&this._applyHavingClause(W,c),a&&this._applyOrderBy(W,a);let b=!1,A=s,_=n,m=1,k=0;h&&s>0&&(m=Math.max(1,parseInt(h)),_=(m-1)*s),s>0&&(W.limit(A),_>0&&W.offset(_));const j=await W;if(i&&i.length>0){const t=this.buildWithTree(i);for(const r of Object.keys(t))await this.fetchAndAttachRelated({parentRows:j,relName:r,model:e,withTree:t[r],relation:e.hasRelations[r]})}let B=null;if(s>0)try{let t=this.getQueryBuilder(e);r&&Object.keys(r).length>0&&this._applyWhereClause(t,r),u&&this._applyJoins(t,u,"join"),f&&this._applyJoins(t,f,"leftJoin"),d&&this._applyJoins(t,d,"rightJoin"),g&&this._applyJoins(t,g,"innerJoin");B=(await t.count("* as cnt").first()).cnt}catch(e){console.warn("Failed to get total count:",e.message),B=j.length}s>0&&null!==B&&(k=Math.ceil(B/s),b=m<k);return{data:j,totalCount:B,...s>0?{pagination:{page:m,limit:A,offset:_,totalPages:k,hasNext:b,hasPrev:m>1,nextPage:b?m+1:null,prevPage:m>1?m-1:null}}:{}}}catch(e){throw console.error("QueryService.getQuery error:",e),new Error(`Failed to execute query: ${e.message}`)}}_applyWhereWithArray(e,t,r){for(const i of t)this._applyWhereClause(e,i,r)}_applyWhereClause(e,t,r=[]){if(Array.isArray(t))this._applyWhereWithArray(e,t,r);else{if(t&&Object.keys(t).length>0){let r=this.helperUtility.getDotWalkQuery(t);console.log({filteredWhere:r}),r=this.helperUtility.objectFilter(r,(e,t)=>"object"!=typeof t||null===t);for(const[t,i]of Object.entries(r)){const{joinType:r="AND",column:l}=this.parseWhereColumn(t),{operator:o,value:a}=this.parseWhereValue(i);"AND"===r?this._applyAndWhereCondition(e,l,o,a):this._applyOrWhereCondition(e,l,o,a)}}r&&r.length>0&&this._applyNestedWhere(e,t,r)}}_applyNestedWhere(e,t,r){let i=this,l=e._getMyModel();if(r&&r.length>0)for(const o of r){let a=this.helperUtility.getDotWalkQuery(t,o);if(a&&Object.keys(a).length>0){let t=this.utils.getModel(this.controllerWrapper,o),s=l.hasRelations[o],n=r.map(e=>this.helperUtility.pluckDotWalkKey(e,1));e.whereExists(function(){let e=i.getQueryBuilder(t,this.select("*").from(t.table));e.whereRaw(`${s.foreignKey} = ${l.table}.${s.localKey}`),i._applyWhereClause(e,a,n)})}}}_applyJoins(e,t,r){const i=Array.isArray(t)?t:[t];for(const t of i)"string"==typeof t?e[r](t):"object"==typeof t&&(t.table&&t.on?e[r](t.table,t.on):t.table&&t.first&&t.operator&&t.second&&e[r](t.table,t.first,t.operator,t.second))}_applyWithWhere(e,t){try{if(Array.isArray(t))for(const r of t)"string"==typeof r?e.withWhere(r):"object"==typeof r&&e.withWhere(r.column,r.operator,r.value);else if("object"==typeof t)for(const[r,i]of Object.entries(t))e.withWhere(r,i)}catch(e){console.warn("Failed to apply withWhere:",e.message)}}_applyHavingClause(e,t){for(const[r,i]of Object.entries(t))"object"==typeof i&&i.operator?e.having(r,i.operator,i.value):e.having(r,i)}_applyOrderBy(e,t){if(Array.isArray(t))for(const r of t)"string"==typeof r?e.orderBy(r):"object"==typeof r&&e.orderBy(r.column,r.direction||"asc");else"string"==typeof t?e.orderBy(t):"object"==typeof t&&e.orderBy(t.column,t.direction||"asc")}}module.exports=QueryBuilder;
1
+ const HelperUtility=require("./HelperUtility");class QueryBuilder{constructor(e,t,r=null){this.db=e,this.utils=t,this.controllerWrapper=r,this.helperUtility=new HelperUtility}getHookService(){return this.controllerWrapper.hookService}getQueryBuilder(e,t=null){let r=this.db(e.table);return t&&(r=t),r._getMyModel=()=>e,r}parseValue(e){return this.helperUtility.parseValue(e)}parseWhereValue(e){return this.helperUtility.parseWhereValue(e)}parseWhereColumn(e){return this.helperUtility.parseWhereColumn(e)}_applyOrWhereCondition(e,t,r,i){if(null!==i)if(Array.isArray(i))e.orWhereIn(t,i);else switch(r){case"between":e.orWhereBetween(t,i);break;case"notBetween":e.orWhereNotBetween(t,i);break;case"in":e.orWhereIn(t,i);break;case"notIn":e.orWhereNotIn(t,i);break;case"like":e.orWhere(t,"like",i);break;default:e.orWhere(t,r,i)}else e.orWhereNull(t)}_applyAndWhereCondition(e,t,r,i){if(null!==i)if(Array.isArray(i))e.whereIn(t,i);else switch(r){case"between":e.whereBetween(t,i);break;case"notBetween":e.whereNotBetween(t,i);break;case"in":e.whereIn(t,i);break;case"notIn":e.whereNotIn(t,i);break;case"like":e.where(t,"like",i);break;default:e.where(t,r,i)}else e.whereNull(t)}buildWithTree(e,t=null){return this.helperUtility.dotWalkTree(e,{resolver:({current:e,part:r,source:i})=>t?t({current:e,part:r,source:i}):{}})}async fetchRelatedRows(e,t){if(e.through){let r=await this.db(e.through).whereIn(e.throughLocalKey,t);return await this.db(e.table).whereIn(e.foreignKey,r.map(t=>t[e.throughForeignKey]))}return this.db(e.table).whereIn(e.foreignKey,t)}async fetchAndAttachRelated(e){const{parentRows:t,relName:r,model:i,withTree:o,relation:l}=e;if(!l){const e=this.getHookService().getModelInstance(i),l=`get${r.charAt(0).toUpperCase()+r.slice(1)}Relation`;if("function"==typeof e[l]){const a={rows:t,relName:r,model:i,withTree:o,controller:this.controllerWrapper,relation:i.hasRelations[r],qb:this,db:this.db};await e[l](a)}return t}let a=t.map(e=>e[l.localKey]);const s="one"===l?.type;let n=[];n=await this.fetchRelatedRows(l,a);let h=new Map;for(const e of n){let t=e[l.foreignKey];h.has(t)||h.set(t,[]),h.get(t).push(e)}for(const e of t){let t=e[l.localKey];s&&1==h.get(t)?.length?e[r]=h.get(t)[0]:e[r]=h.get(t)||[]}let c=Object.keys(o);for(const e of c){let i=o[e],l=t.filter(e=>s?e[r]:e[r].length>0).map(e=>e[r]).reduce((e,t)=>e.concat(t),[]),a=this.utils.getModel(this.controllerWrapper,r);await this.fetchAndAttachRelated({parentRows:l,relName:e,model:a,withTree:i,relation:a.hasRelations[e]})}return t}filterWhere(e,t=""){return t?Object.keys(e).filter(e=>e.includes(t)).reduce((r,i)=>(r[i.replace(t,"")]=e[i],r),{}):Object.keys(e).filter(e=>!e.includes(".")).reduce((t,r)=>(t[r]=e[r],t),{})}async getQuery(e,t){try{const{where:r={},with:i,withWhere:o,select:l,orderBy:a={column:"id",direction:"asc"},limit:s=10,offset:n=0,page:h,groupBy:c,having:p,distinct:y,join:u,leftJoin:f,rightJoin:d,innerJoin:g,count:w=!1}=t;let b=this.getQueryBuilder(e);l&&(Array.isArray(l)||"string"==typeof l)?b.select(l):b.select("*"),y&&(Array.isArray(y)||"string"==typeof y?b.distinct(y):b.distinct()),u&&this._applyJoins(b,u,"join"),f&&this._applyJoins(b,f,"leftJoin"),d&&this._applyJoins(b,d,"rightJoin"),g&&this._applyJoins(b,g,"innerJoin"),this._applyWhereClause(b,r,i),c&&(Array.isArray(c),b.groupBy(c)),p&&this._applyHavingClause(b,p),a&&this._applyOrderBy(b,a);let W=!1,A=s,k=n,m=1,_=0;h&&s>0&&(m=Math.max(1,parseInt(h)),k=(m-1)*s),s>0&&(b.limit(A),k>0&&b.offset(k));const j=await b;if(i&&i.length>0){const t=this.buildWithTree(i);for(const r of Object.keys(t))await this.fetchAndAttachRelated({parentRows:j,relName:r,model:e,withTree:t[r],relation:e.hasRelations[r]})}let B=null;if(s>0)try{let t=this.getQueryBuilder(e);r&&Object.keys(r).length>0&&this._applyWhereClause(t,r),u&&this._applyJoins(t,u,"join"),f&&this._applyJoins(t,f,"leftJoin"),d&&this._applyJoins(t,d,"rightJoin"),g&&this._applyJoins(t,g,"innerJoin");B=(await t.count("* as cnt").first()).cnt}catch(e){console.warn("Failed to get total count:",e.message),B=j.length}s>0&&null!==B&&(_=Math.ceil(B/s),W=m<_);return{data:j,totalCount:B,...s>0?{pagination:{page:m,limit:A,offset:k,totalPages:_,hasNext:W,hasPrev:m>1,nextPage:W?m+1:null,prevPage:m>1?m-1:null}}:{}}}catch(e){throw console.error("QueryService.getQuery error:",e),new Error(`Failed to execute query: ${e.message}`)}}_applyWhereWithArray(e,t,r){for(const i of t)this._applyWhereClause(e,i,r)}_applyWhereClause(e,t,r=[]){if(Array.isArray(t))this._applyWhereWithArray(e,t,r);else{if(t&&Object.keys(t).length>0){let r=this.helperUtility.getDotWalkQuery(t);console.log({filteredWhere:r}),r=this.helperUtility.objectFilter(r,(e,t)=>"object"!=typeof t||null===t);for(const[t,i]of Object.entries(r)){const{joinType:r="AND",column:o}=this.parseWhereColumn(t),{operator:l,value:a}=this.parseWhereValue(i);"AND"===r?this._applyAndWhereCondition(e,o,l,a):this._applyOrWhereCondition(e,o,l,a)}}r&&r.length>0&&this._applyNestedWhere(e,t,r)}}_applyNestedWhere(e,t,r){let i=this,o=e._getMyModel();if(r&&r.length>0)for(const l of r){let a=this.helperUtility.getDotWalkQuery(t,l);if(a&&Object.keys(a).length>0){let t=this.utils.getModel(this.controllerWrapper,l),s=o.hasRelations[l],n=r.map(e=>this.helperUtility.pluckDotWalkKey(e,1));e.whereExists(function(){let e=i.getQueryBuilder(t,this.select("*").from(t.table));e.whereRaw(`${s.foreignKey} = ${o.table}.${s.localKey}`),i._applyWhereClause(e,a,n)})}}}_applyJoins(e,t,r){const i=Array.isArray(t)?t:[t];for(const t of i)"string"==typeof t?e[r](t):"object"==typeof t&&(t.table&&t.on?e[r](t.table,t.on):t.table&&t.first&&t.operator&&t.second&&e[r](t.table,t.first,t.operator,t.second))}_applyWithWhere(e,t){try{if(Array.isArray(t))for(const r of t)"string"==typeof r?e.withWhere(r):"object"==typeof r&&e.withWhere(r.column,r.operator,r.value);else if("object"==typeof t)for(const[r,i]of Object.entries(t))e.withWhere(r,i)}catch(e){console.warn("Failed to apply withWhere:",e.message)}}_applyHavingClause(e,t){for(const[r,i]of Object.entries(t))"object"==typeof i&&i.operator?e.having(r,i.operator,i.value):e.having(r,i)}_applyOrderBy(e,t){if(Array.isArray(t))for(const r of t)"string"==typeof r?e.orderBy(r):"object"==typeof r&&e.orderBy(r.column,r.direction||"asc");else"string"==typeof t?e.orderBy(t):"object"==typeof t&&e.orderBy(t.column,t.direction||"asc")}}module.exports=QueryBuilder;
@@ -1 +1 @@
1
- const QueryService=require("./QueryService"),HookService=require("./HookService");class CurdTable{constructor(e,r,t=null){if(this.db=e,this.utils=r,this.controllerWrapper=t,this.hookService=new HookService(e,r,t),this.queryService=new QueryService(e,r,t),!this.queryService)throw new Error("CurdTable requires queryService (execute*Query / getQuery).")}async processRequest(e,r=null,t={}){let o=this.utils.getModel(this.controllerWrapper,r);const s=e?.action||"list";let c=null;switch(this.hookService?.executeValidatorHook&&await this.hookService.executeValidatorHook(o,s,e,t),this.hookService?.executeBeforeHook&&(e.beforeActionData=await this.hookService.executeBeforeHook(o,s,e,t)),s){case"count":c=await this.queryService.executeCountQuery(o,e);break;case"list":c=await this.hookService.executeHasSoftDeleteHook(o)?await this.queryService.getSoftDeleteQuery(o,e):await this.queryService.getQuery(o,e);break;case"show":c=await this.queryService.executeShowQuery(o,e);break;case"create":c=await this.queryService.executeCreateQuery(o,e);break;case"update":{const r=await this.queryService.executeUpdateQuery(o,e);if(!r)throw new Error(`Record not found or not updated: ${o.table} returned ${r}`);c={message:"Record updated successfully",data:r,success:!0};break}case"replace":if(c=await this.queryService.executeReplaceQuery(o,e),!c)throw new Error(`Record not found or not replaced: ${r} returned ${c}`);c={message:"Record replaced successfully",data:c,success:!0};break;case"upsert":if(c=await this.queryService.executeUpsertQuery(o,e),!c)throw new Error(`Record not found or not upserted: ${r} returned ${c}`);c={message:"Record upserted successfully",data:c,success:!0};break;case"sync":if(c=await this.queryService.executeSyncQuery(o,e),!c)throw new Error(`Record not found or not synced: ${r} returned ${c}`);c={message:"Record synced successfully",data:c,success:!0};break;case"delete":{let t=null;if(t=await this.hookService.executeHasSoftDeleteHook(o)?await this.queryService.executeSoftDeleteQuery(o,e):await this.queryService.executeDeleteQuery(o,e),!t)throw new Error(`Record not found or not deleted: ${r} returned ${t}`);c={message:"Record deleted successfully",data:t,success:!0};break}default:if(!this.hookService?.executeCustomAction)throw new Error(`Unknown action "${s}" and no custom action hook provided.`);c=await this.hookService.executeCustomAction(r,s,e,t)}if(this.hookService?.executeAfterHook&&(c=await this.hookService.executeAfterHook(r,s,c,e,t)),e?.other_requests&&"object"==typeof e.other_requests){const r={},o=Object.entries(e.other_requests);for(const[e,s]of o)Array.isArray(s)?r[e]=await Promise.all(s.map(r=>this.processRequest(r,e,t))):r[e]=await this.processRequest(s,e,t);c.other_responses=r}return c}}module.exports=CurdTable;
1
+ const QueryService=require("./QueryService"),HookService=require("./HookService");class CurdTable{constructor(e,r,t=null){if(this.db=e,this.utils=r,this.controllerWrapper=t,this.hookService=new HookService(e,r,t),this.queryService=new QueryService(e,r,t),!this.queryService)throw new Error("CurdTable requires queryService (execute*Query / getQuery).")}async processRequest(e,r=null,t={}){const o=this.controllerWrapper;let s=this.utils.getModel(this.controllerWrapper,r);const c=e?.action||"list";let i=null;const a={model:s,action:c,request:e,ctx:t,controller:o};switch(this.hookService?.executeValidatorHook&&await this.hookService.executeValidatorHook({...a}),this.hookService?.executeBeforeHook&&(e.beforeActionData=await this.hookService.executeBeforeHook({...a})),c){case"count":i=await this.queryService.executeCountQuery(s,e);break;case"list":i=await this.hookService.executeHasSoftDeleteHook(s)?await this.queryService.getSoftDeleteQuery(s,e):await this.queryService.getQuery(s,e);break;case"show":i=await this.queryService.executeShowQuery(s,e);break;case"create":i=await this.queryService.executeCreateQuery(s,e);break;case"update":{const r=await this.queryService.executeUpdateQuery(s,e);if(!r)throw new Error(`Record not found or not updated: ${s.table} returned ${r}`);i={message:"Record updated successfully",data:r,success:!0};break}case"replace":if(i=await this.queryService.executeReplaceQuery(s,e),!i)throw new Error(`Record not found or not replaced: ${r} returned ${i}`);i={message:"Record replaced successfully",data:i,success:!0};break;case"upsert":if(i=await this.queryService.executeUpsertQuery(s,e),!i)throw new Error(`Record not found or not upserted: ${r} returned ${i}`);i={message:"Record upserted successfully",data:i,success:!0};break;case"sync":if(i=await this.queryService.executeSyncQuery(s,e),!i)throw new Error(`Record not found or not synced: ${r} returned ${i}`);i={message:"Record synced successfully",data:i,success:!0};break;case"delete":{let t=null;if(t=await this.hookService.executeHasSoftDeleteHook(s)?await this.queryService.executeSoftDeleteQuery(s,e):await this.queryService.executeDeleteQuery(s,e),!t)throw new Error(`Record not found or not deleted: ${r} returned ${t}`);i={message:"Record deleted successfully",data:t,success:!0};break}default:if(!this.hookService?.executeCustomAction)throw new Error(`Unknown action "${c}" and no custom action hook provided.`);i=await this.hookService.executeCustomAction({...a})}if(this.hookService?.executeAfterHook&&(i=await this.hookService.executeAfterHook({...a,data:i})),e?.other_requests&&"object"==typeof e.other_requests){const r={},o=Object.entries(e.other_requests);for(const[e,s]of o)Array.isArray(s)?r[e]=await Promise.all(s.map(r=>this.processRequest(r,e,t))):r[e]=await this.processRequest(s,e,t);i.other_responses=r}return i}}module.exports=CurdTable;
@@ -1 +1 @@
1
- const path=require("path");class HookService{constructor(e,t,o=null){this.db=e,this.utils=t,this.controllerWrapper=o,this.appRoot=o&&o.resolverPath?o.resolverPath:process.cwd()}loadModelClass(e){try{const t=path.join(this.appRoot,"models",`${e}.model.js`);delete require.cache[require.resolve(t)];return require(t)}catch(e){return}}getModelInstance(e){const t=e.name,o=this.loadModelClass(t);if(o)return"function"==typeof o?new o:o}resolveModelHook(e,t,o){const s=e.name,r=this.loadModelClass(s);if(!r)return;const i=this.getModelInstance(e);let a;if("validate"===t)a="validate";else if("on"===t)a=`on${o.charAt(0).toUpperCase()+o.slice(1)}`;else if("before"===t)a=`before${o.charAt(0).toUpperCase()+o.slice(1)}`;else if("after"===t)a=`after${o.charAt(0).toUpperCase()+o.slice(1)}`;else{if("custom"!==t)return;a=`custom${o.charAt(0).toUpperCase()+o.slice(1)}`}return"function"==typeof i[a]?i[a].bind(i):"function"==typeof r[a]?r[a].bind(r):void 0}async executeValidatorHook(e,t,o,s){const r=this.resolveModelHook(e,"validate",t);if(r)return await r({model:e,action:t,request:o,context:s,db:this.db,utils:this.utils})}async executeBeforeHook(e,t,o,s){const r=this.resolveModelHook(e,"before",t);if(r)return await r({model:e,action:t,request:o,context:s,db:this.db,utils:this.utils})}async executeAfterHook(e,t,o,s,r){const i=this.resolveModelHook(e,"after",t);return i?await i({model:e,action:t,data:o,request:s,context:r,db:this.db,utils:this.utils}):o}async executeCustomAction(e,t,o,s){const r=this.resolveModelHook(e,"custom",t);if(r)return await r({model:e,action:t,request:o,context:s,db:this.db,utils:this.utils});throw new Error(`No custom action hook found for ${e}.${t}`)}async executeHasSoftDeleteHook(e){const t=this.getModelInstance(e);if(!t)return!1;return t.hasOwnProperty("hasSoftDelete")&&!0===t.hasSoftDelete}}module.exports=HookService;
1
+ const path=require("path");class HookService{constructor(e,t,o=null){this.db=e,this.utils=t,this.controllerWrapper=o,this.controllerWrapper.hookService=this,this.appRoot=o&&o.resolverPath?o.resolverPath:process.cwd()}loadModelClass(e){try{const t=path.join(this.appRoot,"models",`${e}.model.js`);delete require.cache[require.resolve(t)];return require(t)}catch(e){return}}getModelInstance(e){let t="string"==typeof e?e:e.modelName||e.name;const o=this.loadModelClass(t);if(o)return"function"==typeof o?new o:o}resolveModelHook(e,t,o){const r=e.modelName||e.name,s=this.loadModelClass(r);if(!s)return;const l=this.getModelInstance(e);let i;if("validate"===t)i="validate";else if("on"===t)i=`on${o.charAt(0).toUpperCase()+o.slice(1)}`;else if("before"===t)i=`before${o.charAt(0).toUpperCase()+o.slice(1)}`;else if("after"===t)i=`after${o.charAt(0).toUpperCase()+o.slice(1)}`;else{if("custom"!==t)return;i=`on${o.charAt(0).toUpperCase()+o.slice(1)}Action`}return"function"==typeof l[i]?l[i].bind(l):"function"==typeof s[i]?s[i].bind(s):void 0}async executeValidatorHook({model:e,action:t,request:o,ctx:r,controller:s}){const l=this.resolveModelHook(e,"validate",t);if(l)return await l({model:e,action:t,request:o,context:r,db:this.db,utils:this.utils,controller:s})}async executeBeforeHook({model:e,action:t,request:o,ctx:r,controller:s}){const l=this.resolveModelHook(e,"before",t);if(l)return await l({model:e,action:t,request:o,context:r,db:this.db,utils:this.utils,controller:s})}async executeAfterHook({model:e,action:t,data:o,request:r,ctx:s,controller:l}){const i=this.resolveModelHook(e,"after",t);return i?await i({model:e,action:t,data:o,request:r,context:s,db:this.db,utils:this.utils,controller:l}):o}async executeCustomAction({model:e,action:t,request:o,ctx:r,controller:s}){const l=this.resolveModelHook(e,"custom",t);if(l)return await l({model:e,action:t,request:o,context:r,db:this.db,utils:this.utils,controller:s});throw new Error(`No custom action hook found for ${e.modelName||e.name}.${t}`)}async executeHasSoftDeleteHook(e){const t=this.getModelInstance(e);if(!t)return!1;return t.hasOwnProperty("hasSoftDelete")&&!0===t.hasSoftDelete}}module.exports=HookService;
@@ -1 +1 @@
1
- const HelperUtility=require("./HelperUtility");class QueryBuilder{constructor(e,t,r=null){this.db=e,this.utils=t,this.controllerWrapper=r,this.helperUtility=new HelperUtility}getQueryBuilder(e){return this.db(e.table)}parseValue(e){return this.helperUtility.parseValue(e)}parseWhereValue(e){return this.helperUtility.parseWhereValue(e)}parseWhereColumn(e){return this.helperUtility.parseWhereColumn(e)}_applyOrWhereCondition(e,t,r,i){if(null!==i)if(Array.isArray(i))e.orWhereIn(t,i);else switch(r){case"between":e.orWhereBetween(t,i);break;case"notBetween":e.orWhereNotBetween(t,i);break;case"in":e.orWhereIn(t,i);break;case"notIn":e.orWhereNotIn(t,i);break;case"like":e.orWhere(t,"like",i);break;default:e.orWhere(t,r,i)}else e.orWhereNull(t)}_applyAndWhereCondition(e,t,r,i){if(null!==i)if(Array.isArray(i))e.whereIn(t,i);else switch(r){case"between":e.whereBetween(t,i);break;case"notBetween":e.whereNotBetween(t,i);break;case"in":e.whereIn(t,i);break;case"notIn":e.whereNotIn(t,i);break;case"like":e.where(t,"like",i);break;default:e.where(t,r,i)}else e.whereNull(t)}buildWithTree(e,t=null){return this.helperUtility.dotWalkTree(e,{resolver:({current:e,part:r,source:i})=>t?t({current:e,part:r,source:i}):{}})}async fetchRelatedRows(e,t){if(e.through){let r=await this.db(e.through).whereIn(e.throughLocalKey,t);return await this.db(e.table).whereIn(e.foreignKey,r.map(t=>t[e.throughForeignKey]))}return this.db(e.table).whereIn(e.foreignKey,t)}async fetchAndAttachRelated(e){const{parentRows:t,relName:r,model:i,withTree:l,relation:o}=e;let a=t.map(e=>e[o.localKey]),s=[];s=await this.fetchRelatedRows(o,a);let n=new Map;for(const e of s){let t=e[o.foreignKey];n.has(t)||n.set(t,[]),n.get(t).push(e)}for(const e of t){let t=e[o.localKey];"one"===o?.type&&1==n.get(t)?.length?e[r]=n.get(t)[0]:e[r]=n.get(t)||[]}let h=Object.keys(l);for(const e of h){let i=l[e],o=t.filter(e=>e[r].length>0).map(e=>e[r]).reduce((e,t)=>e.concat(t),[]),a=this.utils.getModel(this.controllerWrapper,r);await this.fetchAndAttachRelated({parentRows:o,relName:e,model:a,withTree:i,relation:a.hasRelations[e]})}return t}filterWhere(e,t=""){return t?Object.keys(e).filter(e=>e.includes(t)).reduce((r,i)=>(r[i.replace(t,"")]=e[i],r),{}):Object.keys(e).filter(e=>!e.includes(".")).reduce((t,r)=>(t[r]=e[r],t),{})}async getQuery(e,t){try{const{where:r={},with:i,withWhere:l,select:o,orderBy:a={column:"id",direction:"asc"},limit:s=10,offset:n=0,page:h,groupBy:p,having:c,distinct:y,join:u,leftJoin:f,rightJoin:d,innerJoin:g,count:w=!1}=t;let W=this.getQueryBuilder(e);o&&(Array.isArray(o)||"string"==typeof o)?W.select(o):W.select("*"),y&&(Array.isArray(y)||"string"==typeof y?W.distinct(y):W.distinct()),u&&this._applyJoins(W,u,"join"),f&&this._applyJoins(W,f,"leftJoin"),d&&this._applyJoins(W,d,"rightJoin"),g&&this._applyJoins(W,g,"innerJoin"),this._applyWhereClause(W,r,i),p&&(Array.isArray(p),W.groupBy(p)),c&&this._applyHavingClause(W,c),a&&this._applyOrderBy(W,a);let b=!1,A=s,_=n,k=1,m=0;h&&s>0&&(k=Math.max(1,parseInt(h)),_=(k-1)*s),s>0&&(W.limit(A),_>0&&W.offset(_));const j=await W;if(i&&i.length>0){const t=this.buildWithTree(i);for(const r of Object.keys(t))await this.fetchAndAttachRelated({parentRows:j,relName:r,model:e,withTree:t[r],relation:e.hasRelations[r]})}let B=null;if(s>0)try{let t=this.getQueryBuilder(e);r&&Object.keys(r).length>0&&this._applyWhereClause(t,r),u&&this._applyJoins(t,u,"join"),f&&this._applyJoins(t,f,"leftJoin"),d&&this._applyJoins(t,d,"rightJoin"),g&&this._applyJoins(t,g,"innerJoin");B=(await t.count("* as cnt").first()).cnt}catch(e){console.warn("Failed to get total count:",e.message),B=j.length}s>0&&null!==B&&(m=Math.ceil(B/s),b=k<m);return{data:j,totalCount:B,...s>0?{pagination:{page:k,limit:A,offset:_,totalPages:m,hasNext:b,hasPrev:k>1,nextPage:b?k+1:null,prevPage:k>1?k-1:null}}:{}}}catch(e){throw console.error("QueryService.getQuery error:",e),new Error(`Failed to execute query: ${e.message}`)}}_applyWhereWithArray(e,t,r){for(const i of t)this._applyWhereClause(e,i,r)}_applyWhereClause(e,t,r=[]){if(Array.isArray(t))this._applyWhereWithArray(e,t,r);else{if(t&&Object.keys(t).length>0){let r=this.helperUtility.getDotWalkQuery(t);r=this.helperUtility.objectFilter(r,(e,t)=>"object"!=typeof t||null===t);for(const[t,i]of Object.entries(r)){const{joinType:r="AND",column:l}=this.parseWhereColumn(t),{operator:o,value:a}=this.parseWhereValue(i);"AND"===r?this._applyAndWhereCondition(e,l,o,a):this._applyOrWhereCondition(e,l,o,a)}}r&&r.length>0&&this._applyNestedWhere(e,t,r)}}_applyNestedWhere(e,t,r){if(r&&r.length>0)for(const i of r){let l=this.helperUtility.getDotWalkQuery(t,i);if(l&&Object.keys(l).length>0){let t=this.getQueryBuilder(this.utils.getModel(this.controllerWrapper,i)),o=this.helperUtility.map(r,e=>this.helperUtility.pluckDotWalkKey(e,1));e.whereExists(this._applyWhereClause(t,l,o))}}}_applyJoins(e,t,r){const i=Array.isArray(t)?t:[t];for(const t of i)"string"==typeof t?e[r](t):"object"==typeof t&&(t.table&&t.on?e[r](t.table,t.on):t.table&&t.first&&t.operator&&t.second&&e[r](t.table,t.first,t.operator,t.second))}_applyWithWhere(e,t){try{if(Array.isArray(t))for(const r of t)"string"==typeof r?e.withWhere(r):"object"==typeof r&&e.withWhere(r.column,r.operator,r.value);else if("object"==typeof t)for(const[r,i]of Object.entries(t))e.withWhere(r,i)}catch(e){console.warn("Failed to apply withWhere:",e.message)}}_applyHavingClause(e,t){for(const[r,i]of Object.entries(t))"object"==typeof i&&i.operator?e.having(r,i.operator,i.value):e.having(r,i)}_applyOrderBy(e,t){if(Array.isArray(t))for(const r of t)"string"==typeof r?e.orderBy(r):"object"==typeof r&&e.orderBy(r.column,r.direction||"asc");else"string"==typeof t?e.orderBy(t):"object"==typeof t&&e.orderBy(t.column,t.direction||"asc")}}module.exports=QueryBuilder;
1
+ const HelperUtility=require("./HelperUtility");class QueryBuilder{constructor(e,t,r=null){this.db=e,this.utils=t,this.controllerWrapper=r,this.helperUtility=new HelperUtility}getHookService(){return this.controllerWrapper.hookService}getQueryBuilder(e,t=null){let r=this.db(e.table);return t&&(r=t),r._getMyModel=()=>e,r}parseValue(e){return this.helperUtility.parseValue(e)}parseWhereValue(e){return this.helperUtility.parseWhereValue(e)}parseWhereColumn(e){return this.helperUtility.parseWhereColumn(e)}_applyOrWhereCondition(e,t,r,i){if(null!==i)if(Array.isArray(i))e.orWhereIn(t,i);else switch(r){case"between":e.orWhereBetween(t,i);break;case"notBetween":e.orWhereNotBetween(t,i);break;case"in":e.orWhereIn(t,i);break;case"notIn":e.orWhereNotIn(t,i);break;case"like":e.orWhere(t,"like",i);break;default:e.orWhere(t,r,i)}else e.orWhereNull(t)}_applyAndWhereCondition(e,t,r,i){if(null!==i)if(Array.isArray(i))e.whereIn(t,i);else switch(r){case"between":e.whereBetween(t,i);break;case"notBetween":e.whereNotBetween(t,i);break;case"in":e.whereIn(t,i);break;case"notIn":e.whereNotIn(t,i);break;case"like":e.where(t,"like",i);break;default:e.where(t,r,i)}else e.whereNull(t)}buildWithTree(e,t=null){return this.helperUtility.dotWalkTree(e,{resolver:({current:e,part:r,source:i})=>t?t({current:e,part:r,source:i}):{}})}async fetchRelatedRows(e,t){if(e.through){let r=await this.db(e.through).whereIn(e.throughLocalKey,t);return await this.db(e.table).whereIn(e.foreignKey,r.map(t=>t[e.throughForeignKey]))}return this.db(e.table).whereIn(e.foreignKey,t)}async fetchAndAttachRelated(e){const{parentRows:t,relName:r,model:i,withTree:o,relation:l}=e;if(!l){const e=this.getHookService().getModelInstance(i),l=`get${r.charAt(0).toUpperCase()+r.slice(1)}Relation`;if("function"==typeof e[l]){const a={rows:t,relName:r,model:i,withTree:o,controller:this.controllerWrapper,relation:i.hasRelations[r],qb:this,db:this.db};await e[l](a)}return t}let a=t.map(e=>e[l.localKey]);const s="one"===l?.type;let n=[];n=await this.fetchRelatedRows(l,a);let h=new Map;for(const e of n){let t=e[l.foreignKey];h.has(t)||h.set(t,[]),h.get(t).push(e)}for(const e of t){let t=e[l.localKey];s&&1==h.get(t)?.length?e[r]=h.get(t)[0]:e[r]=h.get(t)||[]}let c=Object.keys(o);for(const e of c){let i=o[e],l=t.filter(e=>s?e[r]:e[r].length>0).map(e=>e[r]).reduce((e,t)=>e.concat(t),[]),a=this.utils.getModel(this.controllerWrapper,r);await this.fetchAndAttachRelated({parentRows:l,relName:e,model:a,withTree:i,relation:a.hasRelations[e]})}return t}filterWhere(e,t=""){return t?Object.keys(e).filter(e=>e.includes(t)).reduce((r,i)=>(r[i.replace(t,"")]=e[i],r),{}):Object.keys(e).filter(e=>!e.includes(".")).reduce((t,r)=>(t[r]=e[r],t),{})}async getQuery(e,t){try{const{where:r={},with:i,withWhere:o,select:l,orderBy:a={column:"id",direction:"asc"},limit:s=10,offset:n=0,page:h,groupBy:c,having:p,distinct:y,join:u,leftJoin:f,rightJoin:d,innerJoin:g,count:w=!1}=t;let b=this.getQueryBuilder(e);l&&(Array.isArray(l)||"string"==typeof l)?b.select(l):b.select("*"),y&&(Array.isArray(y)||"string"==typeof y?b.distinct(y):b.distinct()),u&&this._applyJoins(b,u,"join"),f&&this._applyJoins(b,f,"leftJoin"),d&&this._applyJoins(b,d,"rightJoin"),g&&this._applyJoins(b,g,"innerJoin"),this._applyWhereClause(b,r,i),c&&(Array.isArray(c),b.groupBy(c)),p&&this._applyHavingClause(b,p),a&&this._applyOrderBy(b,a);let W=!1,A=s,k=n,m=1,_=0;h&&s>0&&(m=Math.max(1,parseInt(h)),k=(m-1)*s),s>0&&(b.limit(A),k>0&&b.offset(k));const j=await b;if(i&&i.length>0){const t=this.buildWithTree(i);for(const r of Object.keys(t))await this.fetchAndAttachRelated({parentRows:j,relName:r,model:e,withTree:t[r],relation:e.hasRelations[r]})}let B=null;if(s>0)try{let t=this.getQueryBuilder(e);r&&Object.keys(r).length>0&&this._applyWhereClause(t,r),u&&this._applyJoins(t,u,"join"),f&&this._applyJoins(t,f,"leftJoin"),d&&this._applyJoins(t,d,"rightJoin"),g&&this._applyJoins(t,g,"innerJoin");B=(await t.count("* as cnt").first()).cnt}catch(e){console.warn("Failed to get total count:",e.message),B=j.length}s>0&&null!==B&&(_=Math.ceil(B/s),W=m<_);return{data:j,totalCount:B,...s>0?{pagination:{page:m,limit:A,offset:k,totalPages:_,hasNext:W,hasPrev:m>1,nextPage:W?m+1:null,prevPage:m>1?m-1:null}}:{}}}catch(e){throw console.error("QueryService.getQuery error:",e),new Error(`Failed to execute query: ${e.message}`)}}_applyWhereWithArray(e,t,r){for(const i of t)this._applyWhereClause(e,i,r)}_applyWhereClause(e,t,r=[]){if(Array.isArray(t))this._applyWhereWithArray(e,t,r);else{if(t&&Object.keys(t).length>0){let r=this.helperUtility.getDotWalkQuery(t);r=this.helperUtility.objectFilter(r,(e,t)=>"object"!=typeof t||null===t);for(const[t,i]of Object.entries(r)){const{joinType:r="AND",column:o}=this.parseWhereColumn(t),{operator:l,value:a}=this.parseWhereValue(i);"AND"===r?this._applyAndWhereCondition(e,o,l,a):this._applyOrWhereCondition(e,o,l,a)}}r&&r.length>0&&this._applyNestedWhere(e,t,r)}}_applyNestedWhere(e,t,r){let i=this,o=e._getMyModel();if(r&&r.length>0)for(const l of r){let a=this.helperUtility.getDotWalkQuery(t,l);if(a&&Object.keys(a).length>0){let t=this.utils.getModel(this.controllerWrapper,l),s=o.hasRelations[l],n=r.map(e=>this.helperUtility.pluckDotWalkKey(e,1));e.whereExists(function(){let e=i.getQueryBuilder(t,this.select("*").from(t.table));e.whereRaw(`${s.foreignKey} = ${o.table}.${s.localKey}`),i._applyWhereClause(e,a,n)})}}}_applyJoins(e,t,r){const i=Array.isArray(t)?t:[t];for(const t of i)"string"==typeof t?e[r](t):"object"==typeof t&&(t.table&&t.on?e[r](t.table,t.on):t.table&&t.first&&t.operator&&t.second&&e[r](t.table,t.first,t.operator,t.second))}_applyWithWhere(e,t){try{if(Array.isArray(t))for(const r of t)"string"==typeof r?e.withWhere(r):"object"==typeof r&&e.withWhere(r.column,r.operator,r.value);else if("object"==typeof t)for(const[r,i]of Object.entries(t))e.withWhere(r,i)}catch(e){console.warn("Failed to apply withWhere:",e.message)}}_applyHavingClause(e,t){for(const[r,i]of Object.entries(t))"object"==typeof i&&i.operator?e.having(r,i.operator,i.value):e.having(r,i)}_applyOrderBy(e,t){if(Array.isArray(t))for(const r of t)"string"==typeof r?e.orderBy(r):"object"==typeof r&&e.orderBy(r.column,r.direction||"asc");else"string"==typeof t?e.orderBy(t):"object"==typeof t&&e.orderBy(t.column,t.direction||"asc")}}module.exports=QueryBuilder;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dreamtree-org/korm-js",
3
- "version": "1.0.45",
3
+ "version": "1.0.47",
4
4
  "description": "Knowledge Object-Relational Mapping - A powerful, modular ORM system for Node.js with dynamic database operations, complex queries, relationships, and nested requests",
5
5
  "author": {
6
6
  "name": "Partha Preetham Krishna",