@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 +398 -90
- package/clients/mysql/HookService.js +1 -1
- package/clients/mysql/QueryBuilder.js +1 -1
- package/clients/pg/HookService.js +1 -1
- package/clients/pg/QueryBuilder.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/package.json +1 -1
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
|
-
"
|
|
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
|
-
"
|
|
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
|
-
"
|
|
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
|
-
"
|
|
599
|
+
"orderBy": { "column": "created_at", "direction": "desc" }
|
|
595
600
|
}
|
|
596
601
|
|
|
597
|
-
//
|
|
602
|
+
// Single sort with string (default: ascending)
|
|
598
603
|
POST /api/Users/crud
|
|
599
604
|
{
|
|
600
605
|
"action": "list",
|
|
601
|
-
"
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
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", "
|
|
617
|
-
"
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
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.*", "
|
|
633
|
-
"
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
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
|
|
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
|
-
//
|
|
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
|
-
//
|
|
1057
|
+
// Modify request data before update
|
|
929
1058
|
request.data.updated_at = new Date();
|
|
930
1059
|
return request.data;
|
|
931
1060
|
}
|
|
932
1061
|
|
|
933
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
948
|
-
|
|
949
|
-
|
|
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
|
-
//
|
|
959
|
-
|
|
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|
|
|
1103
|
-
"username": "varchar|size:255",
|
|
1104
|
-
"email": "varchar|size:255",
|
|
1105
|
-
"first_name": "varchar|size:255
|
|
1106
|
-
"last_name": "varchar|size:255
|
|
1107
|
-
"age": "int|size:4
|
|
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
|
|
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'
|
|
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
|
-
|
|
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 =
|
|
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
|
-
| `
|
|
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 | `
|
|
1186
|
-
| `type:string` | Field must be string | `
|
|
1187
|
-
| `type:number` | Field must be number | `
|
|
1188
|
-
| `type:boolean` | Field must be boolean | `
|
|
1189
|
-
| `
|
|
1190
|
-
| `
|
|
1191
|
-
| `
|
|
1192
|
-
| `
|
|
1193
|
-
| `
|
|
1194
|
-
| `
|
|
1195
|
-
| `
|
|
1196
|
-
| `
|
|
1197
|
-
| `
|
|
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){
|
|
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:
|
|
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){
|
|
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:
|
|
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={}){
|
|
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){
|
|
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){
|
|
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.
|
|
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",
|