@memberjunction/server 2.112.0 → 2.113.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (250) hide show
  1. package/dist/agents/skip-agent.d.ts +4 -4
  2. package/dist/agents/skip-agent.d.ts.map +1 -1
  3. package/dist/agents/skip-agent.js +951 -808
  4. package/dist/agents/skip-agent.js.map +1 -1
  5. package/dist/agents/skip-sdk.d.ts +1 -1
  6. package/dist/agents/skip-sdk.d.ts.map +1 -1
  7. package/dist/agents/skip-sdk.js +43 -53
  8. package/dist/agents/skip-sdk.js.map +1 -1
  9. package/dist/apolloServer/index.js +1 -1
  10. package/dist/auth/AuthProviderFactory.d.ts +1 -1
  11. package/dist/auth/AuthProviderFactory.d.ts.map +1 -1
  12. package/dist/auth/AuthProviderFactory.js +3 -1
  13. package/dist/auth/AuthProviderFactory.js.map +1 -1
  14. package/dist/auth/BaseAuthProvider.d.ts +1 -1
  15. package/dist/auth/BaseAuthProvider.d.ts.map +1 -1
  16. package/dist/auth/BaseAuthProvider.js +2 -3
  17. package/dist/auth/BaseAuthProvider.js.map +1 -1
  18. package/dist/auth/IAuthProvider.d.ts +1 -1
  19. package/dist/auth/IAuthProvider.d.ts.map +1 -1
  20. package/dist/auth/exampleNewUserSubClass.d.ts.map +1 -1
  21. package/dist/auth/exampleNewUserSubClass.js +1 -1
  22. package/dist/auth/exampleNewUserSubClass.js.map +1 -1
  23. package/dist/auth/index.d.ts +1 -1
  24. package/dist/auth/index.d.ts.map +1 -1
  25. package/dist/auth/index.js +6 -6
  26. package/dist/auth/index.js.map +1 -1
  27. package/dist/auth/initializeProviders.js +1 -1
  28. package/dist/auth/initializeProviders.js.map +1 -1
  29. package/dist/auth/newUsers.d.ts +1 -1
  30. package/dist/auth/newUsers.d.ts.map +1 -1
  31. package/dist/auth/newUsers.js +7 -7
  32. package/dist/auth/newUsers.js.map +1 -1
  33. package/dist/auth/providers/Auth0Provider.d.ts +1 -1
  34. package/dist/auth/providers/Auth0Provider.d.ts.map +1 -1
  35. package/dist/auth/providers/Auth0Provider.js +1 -1
  36. package/dist/auth/providers/Auth0Provider.js.map +1 -1
  37. package/dist/auth/providers/CognitoProvider.d.ts +1 -1
  38. package/dist/auth/providers/CognitoProvider.d.ts.map +1 -1
  39. package/dist/auth/providers/CognitoProvider.js +6 -3
  40. package/dist/auth/providers/CognitoProvider.js.map +1 -1
  41. package/dist/auth/providers/GoogleProvider.d.ts +1 -1
  42. package/dist/auth/providers/GoogleProvider.d.ts.map +1 -1
  43. package/dist/auth/providers/GoogleProvider.js +1 -1
  44. package/dist/auth/providers/GoogleProvider.js.map +1 -1
  45. package/dist/auth/providers/MSALProvider.d.ts +1 -1
  46. package/dist/auth/providers/MSALProvider.d.ts.map +1 -1
  47. package/dist/auth/providers/MSALProvider.js +1 -1
  48. package/dist/auth/providers/MSALProvider.js.map +1 -1
  49. package/dist/auth/providers/OktaProvider.d.ts +1 -1
  50. package/dist/auth/providers/OktaProvider.d.ts.map +1 -1
  51. package/dist/auth/providers/OktaProvider.js +1 -1
  52. package/dist/auth/providers/OktaProvider.js.map +1 -1
  53. package/dist/config.d.ts.map +1 -1
  54. package/dist/config.js +10 -22
  55. package/dist/config.js.map +1 -1
  56. package/dist/context.d.ts +1 -1
  57. package/dist/context.d.ts.map +1 -1
  58. package/dist/context.js +7 -9
  59. package/dist/context.js.map +1 -1
  60. package/dist/entitySubclasses/entityPermissions.server.d.ts +1 -1
  61. package/dist/entitySubclasses/entityPermissions.server.d.ts.map +1 -1
  62. package/dist/entitySubclasses/entityPermissions.server.js +1 -1
  63. package/dist/entitySubclasses/entityPermissions.server.js.map +1 -1
  64. package/dist/generated/generated.d.ts +788 -658
  65. package/dist/generated/generated.d.ts.map +1 -1
  66. package/dist/generated/generated.js +2050 -3054
  67. package/dist/generated/generated.js.map +1 -1
  68. package/dist/generic/KeyInputOutputTypes.d.ts +1 -1
  69. package/dist/generic/KeyInputOutputTypes.d.ts.map +1 -1
  70. package/dist/generic/KeyInputOutputTypes.js +1 -1
  71. package/dist/generic/KeyInputOutputTypes.js.map +1 -1
  72. package/dist/generic/ResolverBase.d.ts +1 -1
  73. package/dist/generic/ResolverBase.d.ts.map +1 -1
  74. package/dist/generic/ResolverBase.js +10 -15
  75. package/dist/generic/ResolverBase.js.map +1 -1
  76. package/dist/generic/RunViewResolver.d.ts +1 -1
  77. package/dist/generic/RunViewResolver.d.ts.map +1 -1
  78. package/dist/generic/RunViewResolver.js +15 -15
  79. package/dist/generic/RunViewResolver.js.map +1 -1
  80. package/dist/index.d.ts.map +1 -1
  81. package/dist/index.js +13 -18
  82. package/dist/index.js.map +1 -1
  83. package/dist/resolvers/ActionResolver.d.ts +2 -2
  84. package/dist/resolvers/ActionResolver.d.ts.map +1 -1
  85. package/dist/resolvers/ActionResolver.js +30 -28
  86. package/dist/resolvers/ActionResolver.js.map +1 -1
  87. package/dist/resolvers/AskSkipResolver.d.ts +2 -2
  88. package/dist/resolvers/AskSkipResolver.d.ts.map +1 -1
  89. package/dist/resolvers/AskSkipResolver.js +50 -60
  90. package/dist/resolvers/AskSkipResolver.js.map +1 -1
  91. package/dist/resolvers/ComponentRegistryResolver.d.ts.map +1 -1
  92. package/dist/resolvers/ComponentRegistryResolver.js +38 -36
  93. package/dist/resolvers/ComponentRegistryResolver.js.map +1 -1
  94. package/dist/resolvers/CreateQueryResolver.d.ts +1 -1
  95. package/dist/resolvers/CreateQueryResolver.d.ts.map +1 -1
  96. package/dist/resolvers/CreateQueryResolver.js +40 -43
  97. package/dist/resolvers/CreateQueryResolver.js.map +1 -1
  98. package/dist/resolvers/DatasetResolver.d.ts.map +1 -1
  99. package/dist/resolvers/DatasetResolver.js +1 -1
  100. package/dist/resolvers/DatasetResolver.js.map +1 -1
  101. package/dist/resolvers/EntityRecordNameResolver.d.ts +1 -1
  102. package/dist/resolvers/EntityRecordNameResolver.d.ts.map +1 -1
  103. package/dist/resolvers/EntityRecordNameResolver.js +1 -1
  104. package/dist/resolvers/EntityRecordNameResolver.js.map +1 -1
  105. package/dist/resolvers/EntityResolver.d.ts.map +1 -1
  106. package/dist/resolvers/EntityResolver.js +1 -1
  107. package/dist/resolvers/EntityResolver.js.map +1 -1
  108. package/dist/resolvers/FileCategoryResolver.js +1 -1
  109. package/dist/resolvers/FileCategoryResolver.js.map +1 -1
  110. package/dist/resolvers/FileResolver.js +1 -1
  111. package/dist/resolvers/FileResolver.js.map +1 -1
  112. package/dist/resolvers/GetDataContextDataResolver.d.ts +1 -1
  113. package/dist/resolvers/GetDataContextDataResolver.d.ts.map +1 -1
  114. package/dist/resolvers/GetDataContextDataResolver.js +5 -5
  115. package/dist/resolvers/GetDataContextDataResolver.js.map +1 -1
  116. package/dist/resolvers/GetDataResolver.d.ts.map +1 -1
  117. package/dist/resolvers/GetDataResolver.js +6 -8
  118. package/dist/resolvers/GetDataResolver.js.map +1 -1
  119. package/dist/resolvers/MergeRecordsResolver.d.ts +3 -3
  120. package/dist/resolvers/MergeRecordsResolver.d.ts.map +1 -1
  121. package/dist/resolvers/MergeRecordsResolver.js +3 -3
  122. package/dist/resolvers/MergeRecordsResolver.js.map +1 -1
  123. package/dist/resolvers/PotentialDuplicateRecordResolver.d.ts +1 -1
  124. package/dist/resolvers/PotentialDuplicateRecordResolver.d.ts.map +1 -1
  125. package/dist/resolvers/PotentialDuplicateRecordResolver.js +1 -1
  126. package/dist/resolvers/PotentialDuplicateRecordResolver.js.map +1 -1
  127. package/dist/resolvers/QueryResolver.d.ts.map +1 -1
  128. package/dist/resolvers/QueryResolver.js +11 -11
  129. package/dist/resolvers/QueryResolver.js.map +1 -1
  130. package/dist/resolvers/ReportResolver.js +1 -1
  131. package/dist/resolvers/ReportResolver.js.map +1 -1
  132. package/dist/resolvers/RunAIAgentResolver.d.ts.map +1 -1
  133. package/dist/resolvers/RunAIAgentResolver.js +28 -27
  134. package/dist/resolvers/RunAIAgentResolver.js.map +1 -1
  135. package/dist/resolvers/RunAIPromptResolver.d.ts.map +1 -1
  136. package/dist/resolvers/RunAIPromptResolver.js +31 -31
  137. package/dist/resolvers/RunAIPromptResolver.js.map +1 -1
  138. package/dist/resolvers/RunTemplateResolver.d.ts.map +1 -1
  139. package/dist/resolvers/RunTemplateResolver.js +9 -9
  140. package/dist/resolvers/RunTemplateResolver.js.map +1 -1
  141. package/dist/resolvers/SqlLoggingConfigResolver.d.ts.map +1 -1
  142. package/dist/resolvers/SqlLoggingConfigResolver.js +10 -10
  143. package/dist/resolvers/SqlLoggingConfigResolver.js.map +1 -1
  144. package/dist/resolvers/SyncDataResolver.d.ts +1 -1
  145. package/dist/resolvers/SyncDataResolver.d.ts.map +1 -1
  146. package/dist/resolvers/SyncDataResolver.js +14 -15
  147. package/dist/resolvers/SyncDataResolver.js.map +1 -1
  148. package/dist/resolvers/SyncRolesUsersResolver.d.ts +1 -1
  149. package/dist/resolvers/SyncRolesUsersResolver.d.ts.map +1 -1
  150. package/dist/resolvers/SyncRolesUsersResolver.js +44 -48
  151. package/dist/resolvers/SyncRolesUsersResolver.js.map +1 -1
  152. package/dist/resolvers/TaskResolver.d.ts.map +1 -1
  153. package/dist/resolvers/TaskResolver.js +7 -7
  154. package/dist/resolvers/TaskResolver.js.map +1 -1
  155. package/dist/resolvers/TransactionGroupResolver.d.ts +1 -1
  156. package/dist/resolvers/TransactionGroupResolver.d.ts.map +1 -1
  157. package/dist/resolvers/TransactionGroupResolver.js +12 -12
  158. package/dist/resolvers/TransactionGroupResolver.js.map +1 -1
  159. package/dist/resolvers/UserFavoriteResolver.d.ts +1 -1
  160. package/dist/resolvers/UserFavoriteResolver.d.ts.map +1 -1
  161. package/dist/resolvers/UserFavoriteResolver.js +1 -1
  162. package/dist/resolvers/UserFavoriteResolver.js.map +1 -1
  163. package/dist/resolvers/UserViewResolver.d.ts.map +1 -1
  164. package/dist/resolvers/UserViewResolver.js.map +1 -1
  165. package/dist/rest/EntityCRUDHandler.d.ts +1 -1
  166. package/dist/rest/EntityCRUDHandler.d.ts.map +1 -1
  167. package/dist/rest/EntityCRUDHandler.js +16 -14
  168. package/dist/rest/EntityCRUDHandler.js.map +1 -1
  169. package/dist/rest/RESTEndpointHandler.d.ts.map +1 -1
  170. package/dist/rest/RESTEndpointHandler.js +25 -23
  171. package/dist/rest/RESTEndpointHandler.js.map +1 -1
  172. package/dist/rest/ViewOperationsHandler.d.ts +1 -1
  173. package/dist/rest/ViewOperationsHandler.d.ts.map +1 -1
  174. package/dist/rest/ViewOperationsHandler.js +21 -17
  175. package/dist/rest/ViewOperationsHandler.js.map +1 -1
  176. package/dist/scheduler/LearningCycleScheduler.d.ts.map +1 -1
  177. package/dist/scheduler/LearningCycleScheduler.js.map +1 -1
  178. package/dist/services/ScheduledJobsService.d.ts.map +1 -1
  179. package/dist/services/ScheduledJobsService.js +6 -4
  180. package/dist/services/ScheduledJobsService.js.map +1 -1
  181. package/dist/services/TaskOrchestrator.d.ts +1 -1
  182. package/dist/services/TaskOrchestrator.d.ts.map +1 -1
  183. package/dist/services/TaskOrchestrator.js +30 -30
  184. package/dist/services/TaskOrchestrator.js.map +1 -1
  185. package/dist/types.d.ts +3 -3
  186. package/dist/types.d.ts.map +1 -1
  187. package/dist/types.js +1 -0
  188. package/dist/types.js.map +1 -1
  189. package/dist/util.d.ts +1 -1
  190. package/dist/util.d.ts.map +1 -1
  191. package/dist/util.js +2 -2
  192. package/dist/util.js.map +1 -1
  193. package/package.json +39 -36
  194. package/src/agents/skip-agent.ts +1200 -1067
  195. package/src/agents/skip-sdk.ts +851 -877
  196. package/src/apolloServer/index.ts +2 -2
  197. package/src/auth/AuthProviderFactory.ts +14 -8
  198. package/src/auth/BaseAuthProvider.ts +4 -5
  199. package/src/auth/IAuthProvider.ts +2 -2
  200. package/src/auth/exampleNewUserSubClass.ts +2 -9
  201. package/src/auth/index.ts +26 -31
  202. package/src/auth/initializeProviders.ts +3 -3
  203. package/src/auth/newUsers.ts +134 -166
  204. package/src/auth/providers/Auth0Provider.ts +5 -5
  205. package/src/auth/providers/CognitoProvider.ts +10 -7
  206. package/src/auth/providers/GoogleProvider.ts +5 -4
  207. package/src/auth/providers/MSALProvider.ts +5 -5
  208. package/src/auth/providers/OktaProvider.ts +7 -6
  209. package/src/config.ts +54 -63
  210. package/src/context.ts +30 -42
  211. package/src/entitySubclasses/entityPermissions.server.ts +3 -3
  212. package/src/generated/generated.ts +40442 -48106
  213. package/src/generic/KeyInputOutputTypes.ts +6 -3
  214. package/src/generic/ResolverBase.ts +78 -119
  215. package/src/generic/RunViewResolver.ts +23 -27
  216. package/src/index.ts +48 -66
  217. package/src/resolvers/ActionResolver.ts +57 -46
  218. package/src/resolvers/AskSkipResolver.ts +533 -607
  219. package/src/resolvers/ComponentRegistryResolver.ts +562 -547
  220. package/src/resolvers/CreateQueryResolver.ts +655 -683
  221. package/src/resolvers/DatasetResolver.ts +6 -5
  222. package/src/resolvers/EntityCommunicationsResolver.ts +1 -1
  223. package/src/resolvers/EntityRecordNameResolver.ts +5 -9
  224. package/src/resolvers/EntityResolver.ts +7 -9
  225. package/src/resolvers/FileCategoryResolver.ts +2 -2
  226. package/src/resolvers/FileResolver.ts +4 -4
  227. package/src/resolvers/GetDataContextDataResolver.ts +118 -106
  228. package/src/resolvers/GetDataResolver.ts +205 -194
  229. package/src/resolvers/MergeRecordsResolver.ts +5 -5
  230. package/src/resolvers/PotentialDuplicateRecordResolver.ts +1 -1
  231. package/src/resolvers/QueryResolver.ts +78 -95
  232. package/src/resolvers/ReportResolver.ts +2 -2
  233. package/src/resolvers/RunAIAgentResolver.ts +828 -818
  234. package/src/resolvers/RunAIPromptResolver.ts +709 -693
  235. package/src/resolvers/RunTemplateResolver.ts +103 -105
  236. package/src/resolvers/SqlLoggingConfigResolver.ts +72 -69
  237. package/src/resolvers/SyncDataResolver.ts +352 -386
  238. package/src/resolvers/SyncRolesUsersResolver.ts +350 -387
  239. package/src/resolvers/TaskResolver.ts +115 -110
  240. package/src/resolvers/TransactionGroupResolver.ts +138 -143
  241. package/src/resolvers/UserFavoriteResolver.ts +8 -17
  242. package/src/resolvers/UserViewResolver.ts +12 -17
  243. package/src/rest/EntityCRUDHandler.ts +268 -291
  244. package/src/rest/RESTEndpointHandler.ts +776 -782
  245. package/src/rest/ViewOperationsHandler.ts +195 -191
  246. package/src/scheduler/LearningCycleScheduler.ts +52 -8
  247. package/src/services/ScheduledJobsService.ts +132 -129
  248. package/src/services/TaskOrchestrator.ts +776 -792
  249. package/src/types.ts +9 -15
  250. package/src/util.ts +109 -112
@@ -1,6 +1,6 @@
1
1
  import { Arg, Ctx, Field, InputType, Mutation, ObjectType, registerEnumType, Resolver, PubSub, PubSubEngine } from 'type-graphql';
2
2
  import { AppContext } from '../types.js';
3
- import { LogError, RunView, UserInfo, CompositeKey, DatabaseProviderBase, LogStatus } from '@memberjunction/global';
3
+ import { LogError, RunView, UserInfo, CompositeKey, DatabaseProviderBase, LogStatus } from '@memberjunction/core';
4
4
  import { RequireSystemUser } from '../directives/RequireSystemUser.js';
5
5
  import { QueryCategoryEntity, QueryPermissionEntity } from '@memberjunction/core-entities';
6
6
  import { MJQueryResolver } from '../generated/generated.js';
@@ -12,827 +12,799 @@ import { QueryEntityExtended } from '@memberjunction/core-entities-server';
12
12
  * Query status enumeration for GraphQL
13
13
  */
14
14
  export enum QueryStatus {
15
- Pending = 'Pending',
16
- Approved = 'Approved',
17
- Rejected = 'Rejected',
18
- Expired = 'Expired',
15
+ Pending = "Pending",
16
+ Approved = "Approved",
17
+ Rejected = "Rejected",
18
+ Expired = "Expired"
19
19
  }
20
20
 
21
21
  registerEnumType(QueryStatus, {
22
- name: 'QueryStatus',
23
- description: 'Status of a query: Pending, Approved, Rejected, or Expired',
22
+ name: "QueryStatus",
23
+ description: "Status of a query: Pending, Approved, Rejected, or Expired"
24
24
  });
25
25
 
26
26
  @InputType()
27
27
  export class QueryPermissionInputType {
28
- @Field(() => String)
29
- RoleID!: string;
28
+ @Field(() => String)
29
+ RoleID!: string;
30
30
  }
31
31
 
32
32
  @InputType()
33
33
  export class CreateQuerySystemUserInput {
34
- @Field(() => String)
35
- Name!: string;
34
+ @Field(() => String)
35
+ Name!: string;
36
36
 
37
- @Field(() => String, { nullable: true })
38
- CategoryID?: string;
37
+ @Field(() => String, { nullable: true })
38
+ CategoryID?: string;
39
39
 
40
- @Field(() => String, { nullable: true })
41
- CategoryPath?: string;
40
+ @Field(() => String, { nullable: true })
41
+ CategoryPath?: string;
42
42
 
43
- @Field(() => String, { nullable: true })
44
- UserQuestion?: string;
43
+ @Field(() => String, { nullable: true })
44
+ UserQuestion?: string;
45
45
 
46
- @Field(() => String, { nullable: true })
47
- Description?: string;
46
+ @Field(() => String, { nullable: true })
47
+ Description?: string;
48
48
 
49
- @Field(() => String, { nullable: true })
50
- SQL?: string;
49
+ @Field(() => String, { nullable: true })
50
+ SQL?: string;
51
51
 
52
- @Field(() => String, { nullable: true })
53
- TechnicalDescription?: string;
52
+ @Field(() => String, { nullable: true })
53
+ TechnicalDescription?: string;
54
54
 
55
- @Field(() => String, { nullable: true })
56
- OriginalSQL?: string;
55
+ @Field(() => String, { nullable: true })
56
+ OriginalSQL?: string;
57
57
 
58
- @Field(() => String, { nullable: true })
59
- Feedback?: string;
58
+ @Field(() => String, { nullable: true })
59
+ Feedback?: string;
60
60
 
61
- @Field(() => QueryStatus, { nullable: true, defaultValue: QueryStatus.Pending })
62
- Status?: QueryStatus;
61
+ @Field(() => QueryStatus, { nullable: true, defaultValue: QueryStatus.Pending })
62
+ Status?: QueryStatus;
63
63
 
64
- @Field(() => Number, { nullable: true })
65
- QualityRank?: number;
64
+ @Field(() => Number, { nullable: true })
65
+ QualityRank?: number;
66
66
 
67
- @Field(() => Number, { nullable: true })
68
- ExecutionCostRank?: number;
67
+ @Field(() => Number, { nullable: true })
68
+ ExecutionCostRank?: number;
69
69
 
70
- @Field(() => Boolean, { nullable: true })
71
- UsesTemplate?: boolean;
70
+ @Field(() => Boolean, { nullable: true })
71
+ UsesTemplate?: boolean;
72
72
 
73
- @Field(() => Boolean, { nullable: true })
74
- AuditQueryRuns?: boolean;
73
+ @Field(() => Boolean, { nullable: true })
74
+ AuditQueryRuns?: boolean;
75
75
 
76
- @Field(() => Boolean, { nullable: true })
77
- CacheEnabled?: boolean;
76
+ @Field(() => Boolean, { nullable: true })
77
+ CacheEnabled?: boolean;
78
78
 
79
- @Field(() => Number, { nullable: true })
80
- CacheTTLMinutes?: number;
79
+ @Field(() => Number, { nullable: true })
80
+ CacheTTLMinutes?: number;
81
81
 
82
- @Field(() => Number, { nullable: true })
83
- CacheMaxSize?: number;
82
+ @Field(() => Number, { nullable: true })
83
+ CacheMaxSize?: number;
84
84
 
85
- @Field(() => [QueryPermissionInputType], { nullable: true })
86
- Permissions?: QueryPermissionInputType[];
85
+ @Field(() => [QueryPermissionInputType], { nullable: true })
86
+ Permissions?: QueryPermissionInputType[];
87
87
  }
88
88
 
89
89
  @InputType()
90
90
  export class UpdateQuerySystemUserInput {
91
- @Field(() => String)
92
- ID!: string;
91
+ @Field(() => String)
92
+ ID!: string;
93
93
 
94
- @Field(() => String, { nullable: true })
95
- Name?: string;
94
+ @Field(() => String, { nullable: true })
95
+ Name?: string;
96
96
 
97
- @Field(() => String, { nullable: true })
98
- CategoryID?: string;
97
+ @Field(() => String, { nullable: true })
98
+ CategoryID?: string;
99
99
 
100
- @Field(() => String, { nullable: true })
101
- CategoryPath?: string;
100
+ @Field(() => String, { nullable: true })
101
+ CategoryPath?: string;
102
102
 
103
- @Field(() => String, { nullable: true })
104
- UserQuestion?: string;
103
+ @Field(() => String, { nullable: true })
104
+ UserQuestion?: string;
105
105
 
106
- @Field(() => String, { nullable: true })
107
- Description?: string;
106
+ @Field(() => String, { nullable: true })
107
+ Description?: string;
108
108
 
109
- @Field(() => String, { nullable: true })
110
- SQL?: string;
109
+ @Field(() => String, { nullable: true })
110
+ SQL?: string;
111
111
 
112
- @Field(() => String, { nullable: true })
113
- TechnicalDescription?: string;
112
+ @Field(() => String, { nullable: true })
113
+ TechnicalDescription?: string;
114
114
 
115
- @Field(() => String, { nullable: true })
116
- OriginalSQL?: string;
115
+ @Field(() => String, { nullable: true })
116
+ OriginalSQL?: string;
117
117
 
118
- @Field(() => String, { nullable: true })
119
- Feedback?: string;
118
+ @Field(() => String, { nullable: true })
119
+ Feedback?: string;
120
120
 
121
- @Field(() => QueryStatus, { nullable: true })
122
- Status?: QueryStatus;
121
+ @Field(() => QueryStatus, { nullable: true })
122
+ Status?: QueryStatus;
123
123
 
124
- @Field(() => Number, { nullable: true })
125
- QualityRank?: number;
124
+ @Field(() => Number, { nullable: true })
125
+ QualityRank?: number;
126
126
 
127
- @Field(() => Number, { nullable: true })
128
- ExecutionCostRank?: number;
127
+ @Field(() => Number, { nullable: true })
128
+ ExecutionCostRank?: number;
129
129
 
130
- @Field(() => Boolean, { nullable: true })
131
- UsesTemplate?: boolean;
130
+ @Field(() => Boolean, { nullable: true })
131
+ UsesTemplate?: boolean;
132
132
 
133
- @Field(() => Boolean, { nullable: true })
134
- AuditQueryRuns?: boolean;
133
+ @Field(() => Boolean, { nullable: true })
134
+ AuditQueryRuns?: boolean;
135
135
 
136
- @Field(() => Boolean, { nullable: true })
137
- CacheEnabled?: boolean;
136
+ @Field(() => Boolean, { nullable: true })
137
+ CacheEnabled?: boolean;
138
138
 
139
- @Field(() => Number, { nullable: true })
140
- CacheTTLMinutes?: number;
139
+ @Field(() => Number, { nullable: true })
140
+ CacheTTLMinutes?: number;
141
141
 
142
- @Field(() => Number, { nullable: true })
143
- CacheMaxSize?: number;
142
+ @Field(() => Number, { nullable: true })
143
+ CacheMaxSize?: number;
144
144
 
145
- @Field(() => [QueryPermissionInputType], { nullable: true })
146
- Permissions?: QueryPermissionInputType[];
145
+ @Field(() => [QueryPermissionInputType], { nullable: true })
146
+ Permissions?: QueryPermissionInputType[];
147
147
  }
148
148
 
149
149
  @ObjectType()
150
150
  export class QueryFieldType {
151
- @Field(() => String)
152
- ID!: string;
151
+ @Field(() => String)
152
+ ID!: string;
153
153
 
154
- @Field(() => String)
155
- QueryID!: string;
154
+ @Field(() => String)
155
+ QueryID!: string;
156
156
 
157
- @Field(() => String)
158
- Name!: string;
157
+ @Field(() => String)
158
+ Name!: string;
159
159
 
160
- @Field(() => String, { nullable: true })
161
- Description?: string;
160
+ @Field(() => String, { nullable: true })
161
+ Description?: string;
162
162
 
163
- @Field(() => String, { nullable: true })
164
- Type?: string;
163
+ @Field(() => String, { nullable: true })
164
+ Type?: string;
165
165
 
166
- @Field(() => Number)
167
- Sequence!: number;
166
+ @Field(() => Number)
167
+ Sequence!: number;
168
168
 
169
- @Field(() => String, { nullable: true })
170
- SQLBaseType?: string;
169
+ @Field(() => String, { nullable: true })
170
+ SQLBaseType?: string;
171
171
 
172
- @Field(() => String, { nullable: true })
173
- SQLFullType?: string;
172
+ @Field(() => String, { nullable: true })
173
+ SQLFullType?: string;
174
174
 
175
- @Field(() => Boolean)
176
- IsComputed!: boolean;
175
+ @Field(() => Boolean)
176
+ IsComputed!: boolean;
177
177
 
178
- @Field(() => String, { nullable: true })
179
- ComputationDescription?: string;
178
+ @Field(() => String, { nullable: true })
179
+ ComputationDescription?: string;
180
180
  }
181
181
 
182
182
  @ObjectType()
183
183
  export class QueryParameterType {
184
- @Field(() => String)
185
- ID!: string;
184
+ @Field(() => String)
185
+ ID!: string;
186
186
 
187
- @Field(() => String)
188
- QueryID!: string;
187
+ @Field(() => String)
188
+ QueryID!: string;
189
189
 
190
- @Field(() => String)
191
- Name!: string;
190
+ @Field(() => String)
191
+ Name!: string;
192
192
 
193
- @Field(() => String)
194
- Type!: string;
193
+ @Field(() => String)
194
+ Type!: string;
195
195
 
196
- @Field(() => String, { nullable: true })
197
- DefaultValue?: string;
196
+ @Field(() => String, { nullable: true })
197
+ DefaultValue?: string;
198
198
 
199
- @Field(() => String, { nullable: true })
200
- Comments?: string;
199
+ @Field(() => String, { nullable: true })
200
+ Comments?: string;
201
201
 
202
- @Field(() => Boolean)
203
- IsRequired!: boolean;
202
+ @Field(() => Boolean)
203
+ IsRequired!: boolean;
204
204
  }
205
205
 
206
206
  @ObjectType()
207
207
  export class QueryEntityType {
208
- @Field(() => String)
209
- ID!: string;
208
+ @Field(() => String)
209
+ ID!: string;
210
210
 
211
- @Field(() => String)
212
- QueryID!: string;
211
+ @Field(() => String)
212
+ QueryID!: string;
213
213
 
214
- @Field(() => String)
215
- EntityID!: string;
214
+ @Field(() => String)
215
+ EntityID!: string;
216
216
 
217
- @Field(() => String, { nullable: true })
218
- EntityName?: string;
217
+ @Field(() => String, { nullable: true })
218
+ EntityName?: string;
219
219
  }
220
220
 
221
221
  @ObjectType()
222
222
  export class QueryPermissionType {
223
- @Field(() => String)
224
- ID!: string;
223
+ @Field(() => String)
224
+ ID!: string;
225
225
 
226
- @Field(() => String)
227
- QueryID!: string;
226
+ @Field(() => String)
227
+ QueryID!: string;
228
228
 
229
- @Field(() => String)
230
- RoleID!: string;
229
+ @Field(() => String)
230
+ RoleID!: string;
231
231
 
232
- @Field(() => String, { nullable: true })
233
- RoleName?: string;
232
+ @Field(() => String, { nullable: true })
233
+ RoleName?: string;
234
234
  }
235
235
 
236
236
  @ObjectType()
237
237
  export class CreateQueryResultType {
238
- @Field(() => Boolean)
239
- Success!: boolean;
238
+ @Field(() => Boolean)
239
+ Success!: boolean;
240
240
 
241
- @Field(() => String, { nullable: true })
242
- ErrorMessage?: string;
241
+ @Field(() => String, { nullable: true })
242
+ ErrorMessage?: string;
243
243
 
244
- @Field(() => String, { nullable: true })
245
- QueryData?: string;
244
+ @Field(() => String, { nullable: true })
245
+ QueryData?: string;
246
246
 
247
- @Field(() => [QueryFieldType], { nullable: true })
248
- Fields?: QueryFieldType[];
247
+ @Field(() => [QueryFieldType], { nullable: true })
248
+ Fields?: QueryFieldType[];
249
249
 
250
- @Field(() => [QueryParameterType], { nullable: true })
251
- Parameters?: QueryParameterType[];
250
+ @Field(() => [QueryParameterType], { nullable: true })
251
+ Parameters?: QueryParameterType[];
252
252
 
253
- @Field(() => [QueryEntityType], { nullable: true })
254
- Entities?: QueryEntityType[];
253
+ @Field(() => [QueryEntityType], { nullable: true })
254
+ Entities?: QueryEntityType[];
255
255
 
256
- @Field(() => [QueryPermissionType], { nullable: true })
257
- Permissions?: QueryPermissionType[];
256
+ @Field(() => [QueryPermissionType], { nullable: true })
257
+ Permissions?: QueryPermissionType[];
258
258
  }
259
259
 
260
260
  @ObjectType()
261
261
  export class UpdateQueryResultType {
262
- @Field(() => Boolean)
263
- Success!: boolean;
262
+ @Field(() => Boolean)
263
+ Success!: boolean;
264
264
 
265
- @Field(() => String, { nullable: true })
266
- ErrorMessage?: string;
265
+ @Field(() => String, { nullable: true })
266
+ ErrorMessage?: string;
267
267
 
268
- @Field(() => String, { nullable: true })
269
- QueryData?: string;
268
+ @Field(() => String, { nullable: true })
269
+ QueryData?: string;
270
270
 
271
- @Field(() => [QueryFieldType], { nullable: true })
272
- Fields?: QueryFieldType[];
271
+ @Field(() => [QueryFieldType], { nullable: true })
272
+ Fields?: QueryFieldType[];
273
273
 
274
- @Field(() => [QueryParameterType], { nullable: true })
275
- Parameters?: QueryParameterType[];
274
+ @Field(() => [QueryParameterType], { nullable: true })
275
+ Parameters?: QueryParameterType[];
276
276
 
277
- @Field(() => [QueryEntityType], { nullable: true })
278
- Entities?: QueryEntityType[];
277
+ @Field(() => [QueryEntityType], { nullable: true })
278
+ Entities?: QueryEntityType[];
279
279
 
280
- @Field(() => [QueryPermissionType], { nullable: true })
281
- Permissions?: QueryPermissionType[];
280
+ @Field(() => [QueryPermissionType], { nullable: true })
281
+ Permissions?: QueryPermissionType[];
282
282
  }
283
283
 
284
284
  @ObjectType()
285
285
  export class DeleteQueryResultType {
286
- @Field(() => Boolean)
287
- Success!: boolean;
286
+ @Field(() => Boolean)
287
+ Success!: boolean;
288
288
 
289
- @Field(() => String, { nullable: true })
290
- ErrorMessage?: string;
289
+ @Field(() => String, { nullable: true })
290
+ ErrorMessage?: string;
291
291
 
292
- @Field(() => String, { nullable: true })
293
- QueryData?: string;
292
+ @Field(() => String, { nullable: true })
293
+ QueryData?: string;
294
294
  }
295
295
 
296
296
  @Resolver()
297
297
  export class MJQueryResolverExtended extends MJQueryResolver {
298
- /**
299
- * Creates a new query with the provided attributes. This mutation is restricted to system users only.
300
- * @param input - CreateQuerySystemUserInput containing all the query attributes
301
- * @param context - Application context containing user information
302
- * @returns CreateQueryResultType with success status and query data
303
- */
304
- @RequireSystemUser()
305
- @Mutation(() => CreateQueryResultType)
306
- async CreateQuerySystemUser(
307
- @Arg('input', () => CreateQuerySystemUserInput) input: CreateQuerySystemUserInput,
308
- @Ctx() context: AppContext,
309
- @PubSub() pubSub: PubSubEngine
310
- ): Promise<CreateQueryResultType> {
311
- try {
312
- // Handle CategoryPath if provided
313
- let finalCategoryID = input.CategoryID;
314
- const provider = GetReadWriteProvider(context.providers);
315
- if (input.CategoryPath) {
316
- finalCategoryID = await this.findOrCreateCategoryPath(input.CategoryPath, provider, context.userPayload.userRecord);
317
- }
318
-
319
- // Check for existing query with same name in the same category
320
- const existingQuery = await this.findExistingQuery(provider, input.Name, finalCategoryID, context.userPayload.userRecord);
321
-
322
- if (existingQuery) {
323
- const categoryInfo = input.CategoryPath ? `category path '${input.CategoryPath}'` : `category ID '${finalCategoryID}'`;
324
- return {
325
- Success: false,
326
- ErrorMessage: `Query with name '${input.Name}' already exists in ${categoryInfo}`,
327
- };
328
- }
329
-
330
- // Use QueryEntityExtended which handles AI processing
331
- const record = await provider.GetEntityObject<QueryEntityExtended>('Queries', context.userPayload.userRecord);
332
-
333
- // Set the fields from input, handling CategoryPath resolution
334
- const fieldsToSet = {
335
- ...input,
336
- CategoryID: finalCategoryID || input.CategoryID,
337
- Status: input.Status || 'Approved',
338
- QualityRank: input.QualityRank || 0,
339
- UsesTemplate: input.UsesTemplate || false,
340
- AuditQueryRuns: input.AuditQueryRuns || false,
341
- CacheEnabled: input.CacheEnabled || false,
342
- CacheTTLMinutes: input.CacheTTLMinutes || null,
343
- CacheMaxSize: input.CacheMaxSize || null,
344
- };
345
- // Remove non-database fields that we handle separately or are input-only
346
- delete (fieldsToSet as any).Permissions; // Handled separately via createPermissions
347
- delete (fieldsToSet as any).CategoryPath; // Input-only field, resolved to CategoryID
348
-
349
- record.SetMany(fieldsToSet, true);
350
- this.ListenForEntityMessages(record, pubSub, context.userPayload.userRecord);
351
-
352
- // Attempt to save the query
353
- const saveResult = await record.Save();
354
-
355
- if (saveResult) {
356
- // Save succeeded - fire the AfterCreate event and return all the data
357
- await this.AfterCreate(provider, input); // fire event
358
- const queryID = record.ID;
359
-
360
- if (input.Permissions && input.Permissions.length > 0) {
361
- await this.createPermissions(provider, input.Permissions, queryID, context.userPayload.userRecord);
362
- await record.RefreshRelatedMetadata(true); // force DB update since we just created new permissions
363
- }
298
+ /**
299
+ * Creates a new query with the provided attributes. This mutation is restricted to system users only.
300
+ * @param input - CreateQuerySystemUserInput containing all the query attributes
301
+ * @param context - Application context containing user information
302
+ * @returns CreateQueryResultType with success status and query data
303
+ */
304
+ @RequireSystemUser()
305
+ @Mutation(() => CreateQueryResultType)
306
+ async CreateQuerySystemUser(
307
+ @Arg('input', () => CreateQuerySystemUserInput) input: CreateQuerySystemUserInput,
308
+ @Ctx() context: AppContext,
309
+ @PubSub() pubSub: PubSubEngine
310
+ ): Promise<CreateQueryResultType> {
311
+ try {
312
+ // Handle CategoryPath if provided
313
+ let finalCategoryID = input.CategoryID;
314
+ const provider = GetReadWriteProvider(context.providers);
315
+ if (input.CategoryPath) {
316
+ finalCategoryID = await this.findOrCreateCategoryPath(input.CategoryPath, provider, context.userPayload.userRecord);
317
+ }
364
318
 
365
- // Refresh metadata cache to include the newly created query
366
- // This ensures subsequent operations can find the query without additional DB calls
367
- await provider.Refresh();
319
+ // Check for existing query with same name in the same category
320
+ const existingQuery = await this.findExistingQuery(provider, input.Name, finalCategoryID, context.userPayload.userRecord);
321
+
322
+ if (existingQuery) {
323
+ const categoryInfo = input.CategoryPath ? `category path '${input.CategoryPath}'` : `category ID '${finalCategoryID}'`;
324
+ return {
325
+ Success: false,
326
+ ErrorMessage: `Query with name '${input.Name}' already exists in ${categoryInfo}`
327
+ };
328
+ }
368
329
 
369
- return {
370
- Success: true,
371
- QueryData: JSON.stringify(record.GetAll()),
372
- Fields: record.QueryFields,
373
- Parameters: record.QueryParameters,
374
- Entities: record.QueryEntities.map((e) => {
330
+ // Use QueryEntityExtended which handles AI processing
331
+ const record = await provider.GetEntityObject<QueryEntityExtended>("Queries", context.userPayload.userRecord);
332
+
333
+ // Set the fields from input, handling CategoryPath resolution
334
+ const fieldsToSet = {
335
+ ...input,
336
+ CategoryID: finalCategoryID || input.CategoryID,
337
+ Status: input.Status || 'Approved',
338
+ QualityRank: input.QualityRank || 0,
339
+ UsesTemplate: input.UsesTemplate || false,
340
+ AuditQueryRuns: input.AuditQueryRuns || false,
341
+ CacheEnabled: input.CacheEnabled || false,
342
+ CacheTTLMinutes: input.CacheTTLMinutes || null,
343
+ CacheMaxSize: input.CacheMaxSize || null
344
+ };
345
+ // Remove non-database fields that we handle separately or are input-only
346
+ delete (fieldsToSet as any).Permissions; // Handled separately via createPermissions
347
+ delete (fieldsToSet as any).CategoryPath; // Input-only field, resolved to CategoryID
348
+
349
+ record.SetMany(fieldsToSet, true);
350
+ this.ListenForEntityMessages(record, pubSub, context.userPayload.userRecord);
351
+
352
+ // Attempt to save the query
353
+ const saveResult = await record.Save();
354
+
355
+ if (saveResult) {
356
+ // Save succeeded - fire the AfterCreate event and return all the data
357
+ await this.AfterCreate(provider, input); // fire event
358
+ const queryID = record.ID;
359
+
360
+ if (input.Permissions && input.Permissions.length > 0) {
361
+ await this.createPermissions(provider, input.Permissions, queryID, context.userPayload.userRecord);
362
+ await record.RefreshRelatedMetadata(true); // force DB update since we just created new permissions
363
+ }
364
+
365
+ // Refresh metadata cache to include the newly created query
366
+ // This ensures subsequent operations can find the query without additional DB calls
367
+ await provider.Refresh();
368
+
369
+ return {
370
+ Success: true,
371
+ QueryData: JSON.stringify(record.GetAll()),
372
+ Fields: record.QueryFields,
373
+ Parameters: record.QueryParameters,
374
+ Entities: record.QueryEntities.map(e => {
375
+ return {
376
+ ...e,
377
+ EntityName: e.Entity // alias this to fix variable name mismatch
378
+ }
379
+ }),
380
+ Permissions: record.QueryPermissions
381
+ };
382
+ }
383
+ else {
384
+ // Save failed - check if another request created the same query (race condition)
385
+ // Always recheck regardless of error type to handle all duplicate scenarios
386
+ const existingQuery = await this.findExistingQuery(provider, input.Name, finalCategoryID, context.userPayload.userRecord);
387
+
388
+ if (existingQuery) {
389
+ // Found the query that was created by another request
390
+ // Return it as if we created it (it has the same name/category)
391
+ LogStatus(`[CreateQuery] Unique constraint detected for query '${input.Name}'. Using existing query (ID: ${existingQuery.ID}) created by concurrent request.`);
392
+ return {
393
+ Success: true,
394
+ QueryData: JSON.stringify(existingQuery),
395
+ Fields: existingQuery.Fields || [],
396
+ Parameters: existingQuery.Parameters || [],
397
+ Entities: existingQuery.Entities?.map(e => ({
398
+ ID: e.ID,
399
+ QueryID: e.QueryID,
400
+ EntityID: e.EntityID,
401
+ EntityName: e.Entity
402
+ })) || [],
403
+ Permissions: existingQuery.Permissions || []
404
+ };
405
+ }
406
+
407
+ // Genuine failure - couldn't find an existing query with the same name
408
+ const errorMessage = record.LatestResult?.Message || '';
409
+ return {
410
+ Success: false,
411
+ ErrorMessage: `Failed to create query: ${errorMessage || 'Unknown error'}`
412
+ };
413
+ }
414
+ }
415
+ catch (err) {
416
+ LogError(err);
375
417
  return {
376
- ...e,
377
- EntityName: e.Entity, // alias this to fix variable name mismatch
418
+ Success: false,
419
+ ErrorMessage: `MJQueryResolverExtended::CreateQuerySystemUser --- Error creating query: ${err instanceof Error ? err.message : String(err)}`
378
420
  };
379
- }),
380
- Permissions: record.QueryPermissions,
381
- };
382
- } else {
383
- // Save failed - check if another request created the same query (race condition)
384
- // Always recheck regardless of error type to handle all duplicate scenarios
385
- const existingQuery = await this.findExistingQuery(provider, input.Name, finalCategoryID, context.userPayload.userRecord);
386
-
387
- if (existingQuery) {
388
- // Found the query that was created by another request
389
- // Return it as if we created it (it has the same name/category)
390
- LogStatus(
391
- `[CreateQuery] Unique constraint detected for query '${input.Name}'. Using existing query (ID: ${existingQuery.ID}) created by concurrent request.`
392
- );
393
- return {
394
- Success: true,
395
- QueryData: JSON.stringify(existingQuery),
396
- Fields: existingQuery.Fields || [],
397
- Parameters: existingQuery.Parameters || [],
398
- Entities:
399
- existingQuery.Entities?.map((e) => ({
421
+ }
422
+ }
423
+
424
+ protected async createPermissions(p: DatabaseProviderBase, permissions: QueryPermissionInputType[], queryID: string, contextUser: UserInfo): Promise<QueryPermissionType[]> {
425
+ // Create permissions if provided
426
+ const createdPermissions: QueryPermissionType[] = [];
427
+ if (permissions && permissions.length > 0) {
428
+ for (const perm of permissions) {
429
+ const permissionEntity = await p.GetEntityObject<QueryPermissionEntity>('Query Permissions', contextUser);
430
+ if (permissionEntity) {
431
+ permissionEntity.QueryID = queryID;
432
+ permissionEntity.RoleID = perm.RoleID;
433
+
434
+ const saveResult = await permissionEntity.Save();
435
+ if (saveResult) {
436
+ createdPermissions.push({
437
+ ID: permissionEntity.ID,
438
+ QueryID: permissionEntity.QueryID,
439
+ RoleID: permissionEntity.RoleID,
440
+ RoleName: permissionEntity.Role // The view includes the Role name
441
+ });
442
+ }
443
+ }
444
+ }
445
+ }
446
+ return createdPermissions;
447
+ }
448
+
449
+ /**
450
+ * Updates an existing query with the provided attributes. This mutation is restricted to system users only.
451
+ * @param input - UpdateQuerySystemUserInput containing the query ID and fields to update
452
+ * @param context - Application context containing user information
453
+ * @returns UpdateQueryResultType with success status and updated query data including related entities
454
+ */
455
+ @RequireSystemUser()
456
+ @Mutation(() => UpdateQueryResultType)
457
+ async UpdateQuerySystemUser(
458
+ @Arg('input', () => UpdateQuerySystemUserInput) input: UpdateQuerySystemUserInput,
459
+ @Ctx() context: AppContext,
460
+ @PubSub() pubSub: PubSubEngine
461
+ ): Promise<UpdateQueryResultType> {
462
+ try {
463
+ // Load the existing query using QueryEntityExtended
464
+ const provider = GetReadWriteProvider(context.providers);
465
+ const queryEntity = await provider.GetEntityObject<QueryEntityExtended>('Queries', context.userPayload.userRecord);
466
+ if (!queryEntity || !await queryEntity.Load(input.ID)) {
467
+ return {
468
+ Success: false,
469
+ ErrorMessage: `Query with ID ${input.ID} not found`
470
+ };
471
+ }
472
+
473
+ // Handle CategoryPath if provided
474
+ let finalCategoryID = input.CategoryID;
475
+ if (input.CategoryPath) {
476
+ finalCategoryID = await this.findOrCreateCategoryPath(input.CategoryPath, provider, context.userPayload.userRecord);
477
+ }
478
+
479
+ // now make sure there is NO existing query by the same name in the specified category
480
+ const existingQueryResult = await provider.RunView({
481
+ EntityName: 'Queries',
482
+ ExtraFilter: `Name='${input.Name}' AND CategoryID='${finalCategoryID}'`
483
+ }, context.userPayload.userRecord);
484
+ if (existingQueryResult.Success && existingQueryResult.Results?.length > 0) {
485
+ // we have a match! Let's return an error
486
+ return {
487
+ Success: false,
488
+ ErrorMessage: `Query with name '${input.Name}' already exists in the specified ${input.CategoryID ? 'category' : 'categoryPath'}`
489
+ };
490
+ }
491
+
492
+ // Update fields that were provided
493
+ const updateFields: Record<string, any> = {};
494
+ if (input.Name !== undefined) updateFields.Name = input.Name;
495
+ if (finalCategoryID !== undefined) updateFields.CategoryID = finalCategoryID;
496
+ if (input.UserQuestion !== undefined) updateFields.UserQuestion = input.UserQuestion;
497
+ if (input.Description !== undefined) updateFields.Description = input.Description;
498
+ if (input.SQL !== undefined) updateFields.SQL = input.SQL;
499
+ if (input.TechnicalDescription !== undefined) updateFields.TechnicalDescription = input.TechnicalDescription;
500
+ if (input.OriginalSQL !== undefined) updateFields.OriginalSQL = input.OriginalSQL;
501
+ if (input.Feedback !== undefined) updateFields.Feedback = input.Feedback;
502
+ if (input.Status !== undefined) updateFields.Status = input.Status;
503
+ if (input.QualityRank !== undefined) updateFields.QualityRank = input.QualityRank;
504
+ if (input.ExecutionCostRank !== undefined) updateFields.ExecutionCostRank = input.ExecutionCostRank;
505
+ if (input.UsesTemplate !== undefined) updateFields.UsesTemplate = input.UsesTemplate;
506
+ if (input.AuditQueryRuns !== undefined) updateFields.AuditQueryRuns = input.AuditQueryRuns;
507
+ if (input.CacheEnabled !== undefined) updateFields.CacheEnabled = input.CacheEnabled;
508
+ if (input.CacheTTLMinutes !== undefined) updateFields.CacheTTLMinutes = input.CacheTTLMinutes;
509
+ if (input.CacheMaxSize !== undefined) updateFields.CacheMaxSize = input.CacheMaxSize;
510
+
511
+ // Use SetMany to update all fields at once
512
+ queryEntity.SetMany(updateFields);
513
+
514
+ // Save the updated query
515
+ const saveResult = await queryEntity.Save();
516
+ if (!saveResult) {
517
+ return {
518
+ Success: false,
519
+ ErrorMessage: `Failed to update query: ${queryEntity.LatestResult?.Message || 'Unknown error'}`
520
+ };
521
+ }
522
+
523
+ const queryID = queryEntity.ID;
524
+
525
+ // Handle permissions update if provided
526
+ if (input.Permissions !== undefined) {
527
+ // Delete existing permissions
528
+ const rv = new RunView();
529
+ const existingPermissions = await rv.RunView<QueryPermissionEntity>({
530
+ EntityName: 'Query Permissions',
531
+ ExtraFilter: `QueryID='${queryID}'`,
532
+ ResultType: 'entity_object'
533
+ }, context.userPayload.userRecord);
534
+
535
+ if (existingPermissions.Success && existingPermissions.Results) {
536
+ for (const perm of existingPermissions.Results) {
537
+ await perm.Delete();
538
+ }
539
+ }
540
+
541
+ // Create new permissions
542
+ await this.createPermissions(provider, input.Permissions, queryID, context.userPayload.userRecord);
543
+
544
+ // Refresh the metadata to get updated permissions
545
+ await queryEntity.RefreshRelatedMetadata(true);
546
+ }
547
+
548
+ // Use the properties from QueryEntityExtended instead of manual loading
549
+ const fields: QueryFieldType[] = queryEntity.QueryFields.map(f => ({
550
+ ID: f.ID,
551
+ QueryID: f.QueryID,
552
+ Name: f.Name,
553
+ Description: f.Description || undefined,
554
+ Type: f.SQLBaseType || undefined,
555
+ Sequence: f.Sequence,
556
+ SQLBaseType: f.SQLBaseType || undefined,
557
+ SQLFullType: f.SQLFullType || undefined,
558
+ IsComputed: f.IsComputed,
559
+ ComputationDescription: f.ComputationDescription || undefined
560
+ }));
561
+
562
+ const parameters: QueryParameterType[] = queryEntity.QueryParameters.map(p => ({
563
+ ID: p.ID,
564
+ QueryID: p.QueryID,
565
+ Name: p.Name,
566
+ Type: p.Type,
567
+ DefaultValue: p.DefaultValue || undefined,
568
+ Comments: '', // Not available in QueryParameterInfo
569
+ IsRequired: p.IsRequired
570
+ }));
571
+
572
+ const entities: QueryEntityType[] = queryEntity.QueryEntities.map(e => ({
400
573
  ID: e.ID,
401
574
  QueryID: e.QueryID,
402
575
  EntityID: e.EntityID,
403
- EntityName: e.Entity,
404
- })) || [],
405
- Permissions: existingQuery.Permissions || [],
406
- };
407
- }
576
+ EntityName: e.Entity || undefined // Property is called Entity, not EntityName
577
+ }));
578
+
579
+ const permissions: QueryPermissionType[] = queryEntity.QueryPermissions.map(p => ({
580
+ ID: p.ID,
581
+ QueryID: p.QueryID,
582
+ RoleID: p.RoleID,
583
+ RoleName: p.Role || undefined // Property is called Role, not RoleName
584
+ }));
408
585
 
409
- // Genuine failure - couldn't find an existing query with the same name
410
- const errorMessage = record.LatestResult?.Message || '';
411
- return {
412
- Success: false,
413
- ErrorMessage: `Failed to create query: ${errorMessage || 'Unknown error'}`,
414
- };
415
- }
416
- } catch (err) {
417
- LogError(err);
418
- return {
419
- Success: false,
420
- ErrorMessage: `MJQueryResolverExtended::CreateQuerySystemUser --- Error creating query: ${err instanceof Error ? err.message : String(err)}`,
421
- };
422
- }
423
- }
424
-
425
- protected async createPermissions(
426
- p: DatabaseProviderBase,
427
- permissions: QueryPermissionInputType[],
428
- queryID: string,
429
- contextUser: UserInfo
430
- ): Promise<QueryPermissionType[]> {
431
- // Create permissions if provided
432
- const createdPermissions: QueryPermissionType[] = [];
433
- if (permissions && permissions.length > 0) {
434
- for (const perm of permissions) {
435
- const permissionEntity = await p.GetEntityObject<QueryPermissionEntity>('Query Permissions', contextUser);
436
- if (permissionEntity) {
437
- permissionEntity.QueryID = queryID;
438
- permissionEntity.RoleID = perm.RoleID;
439
-
440
- const saveResult = await permissionEntity.Save();
441
- if (saveResult) {
442
- createdPermissions.push({
443
- ID: permissionEntity.ID,
444
- QueryID: permissionEntity.QueryID,
445
- RoleID: permissionEntity.RoleID,
446
- RoleName: permissionEntity.Role, // The view includes the Role name
447
- });
448
- }
586
+ return {
587
+ Success: true,
588
+ QueryData: JSON.stringify(queryEntity.GetAll()),
589
+ Fields: fields,
590
+ Parameters: parameters,
591
+ Entities: entities,
592
+ Permissions: permissions
593
+ };
594
+
595
+ } catch (err) {
596
+ LogError(err);
597
+ return {
598
+ Success: false,
599
+ ErrorMessage: `MJQueryResolverExtended::UpdateQuerySystemUser --- Error updating query: ${err instanceof Error ? err.message : String(err)}`
600
+ };
449
601
  }
450
- }
451
602
  }
452
- return createdPermissions;
453
- }
454
-
455
- /**
456
- * Updates an existing query with the provided attributes. This mutation is restricted to system users only.
457
- * @param input - UpdateQuerySystemUserInput containing the query ID and fields to update
458
- * @param context - Application context containing user information
459
- * @returns UpdateQueryResultType with success status and updated query data including related entities
460
- */
461
- @RequireSystemUser()
462
- @Mutation(() => UpdateQueryResultType)
463
- async UpdateQuerySystemUser(
464
- @Arg('input', () => UpdateQuerySystemUserInput) input: UpdateQuerySystemUserInput,
465
- @Ctx() context: AppContext,
466
- @PubSub() pubSub: PubSubEngine
467
- ): Promise<UpdateQueryResultType> {
468
- try {
469
- // Load the existing query using QueryEntityExtended
470
- const provider = GetReadWriteProvider(context.providers);
471
- const queryEntity = await provider.GetEntityObject<QueryEntityExtended>('Queries', context.userPayload.userRecord);
472
- if (!queryEntity || !(await queryEntity.Load(input.ID))) {
473
- return {
474
- Success: false,
475
- ErrorMessage: `Query with ID ${input.ID} not found`,
476
- };
477
- }
478
-
479
- // Handle CategoryPath if provided
480
- let finalCategoryID = input.CategoryID;
481
- if (input.CategoryPath) {
482
- finalCategoryID = await this.findOrCreateCategoryPath(input.CategoryPath, provider, context.userPayload.userRecord);
483
- }
484
-
485
- // now make sure there is NO existing query by the same name in the specified category
486
- const existingQueryResult = await provider.RunView(
487
- {
488
- EntityName: 'Queries',
489
- ExtraFilter: `Name='${input.Name}' AND CategoryID='${finalCategoryID}'`,
490
- },
491
- context.userPayload.userRecord
492
- );
493
- if (existingQueryResult.Success && existingQueryResult.Results?.length > 0) {
494
- // we have a match! Let's return an error
495
- return {
496
- Success: false,
497
- ErrorMessage: `Query with name '${input.Name}' already exists in the specified ${input.CategoryID ? 'category' : 'categoryPath'}`,
498
- };
499
- }
500
-
501
- // Update fields that were provided
502
- const updateFields: Record<string, any> = {};
503
- if (input.Name !== undefined) updateFields.Name = input.Name;
504
- if (finalCategoryID !== undefined) updateFields.CategoryID = finalCategoryID;
505
- if (input.UserQuestion !== undefined) updateFields.UserQuestion = input.UserQuestion;
506
- if (input.Description !== undefined) updateFields.Description = input.Description;
507
- if (input.SQL !== undefined) updateFields.SQL = input.SQL;
508
- if (input.TechnicalDescription !== undefined) updateFields.TechnicalDescription = input.TechnicalDescription;
509
- if (input.OriginalSQL !== undefined) updateFields.OriginalSQL = input.OriginalSQL;
510
- if (input.Feedback !== undefined) updateFields.Feedback = input.Feedback;
511
- if (input.Status !== undefined) updateFields.Status = input.Status;
512
- if (input.QualityRank !== undefined) updateFields.QualityRank = input.QualityRank;
513
- if (input.ExecutionCostRank !== undefined) updateFields.ExecutionCostRank = input.ExecutionCostRank;
514
- if (input.UsesTemplate !== undefined) updateFields.UsesTemplate = input.UsesTemplate;
515
- if (input.AuditQueryRuns !== undefined) updateFields.AuditQueryRuns = input.AuditQueryRuns;
516
- if (input.CacheEnabled !== undefined) updateFields.CacheEnabled = input.CacheEnabled;
517
- if (input.CacheTTLMinutes !== undefined) updateFields.CacheTTLMinutes = input.CacheTTLMinutes;
518
- if (input.CacheMaxSize !== undefined) updateFields.CacheMaxSize = input.CacheMaxSize;
519
-
520
- // Use SetMany to update all fields at once
521
- queryEntity.SetMany(updateFields);
522
-
523
- // Save the updated query
524
- const saveResult = await queryEntity.Save();
525
- if (!saveResult) {
526
- return {
527
- Success: false,
528
- ErrorMessage: `Failed to update query: ${queryEntity.LatestResult?.Message || 'Unknown error'}`,
529
- };
530
- }
531
-
532
- const queryID = queryEntity.ID;
533
-
534
- // Handle permissions update if provided
535
- if (input.Permissions !== undefined) {
536
- // Delete existing permissions
537
- const rv = new RunView();
538
- const existingPermissions = await rv.RunView<QueryPermissionEntity>(
539
- {
540
- EntityName: 'Query Permissions',
541
- ExtraFilter: `QueryID='${queryID}'`,
542
- ResultType: 'entity_object',
543
- },
544
- context.userPayload.userRecord
545
- );
546
-
547
- if (existingPermissions.Success && existingPermissions.Results) {
548
- for (const perm of existingPermissions.Results) {
549
- await perm.Delete();
550
- }
551
- }
552
603
 
553
- // Create new permissions
554
- await this.createPermissions(provider, input.Permissions, queryID, context.userPayload.userRecord);
555
-
556
- // Refresh the metadata to get updated permissions
557
- await queryEntity.RefreshRelatedMetadata(true);
558
- }
559
-
560
- // Use the properties from QueryEntityExtended instead of manual loading
561
- const fields: QueryFieldType[] = queryEntity.QueryFields.map((f) => ({
562
- ID: f.ID,
563
- QueryID: f.QueryID,
564
- Name: f.Name,
565
- Description: f.Description || undefined,
566
- Type: f.SQLBaseType || undefined,
567
- Sequence: f.Sequence,
568
- SQLBaseType: f.SQLBaseType || undefined,
569
- SQLFullType: f.SQLFullType || undefined,
570
- IsComputed: f.IsComputed,
571
- ComputationDescription: f.ComputationDescription || undefined,
572
- }));
573
-
574
- const parameters: QueryParameterType[] = queryEntity.QueryParameters.map((p) => ({
575
- ID: p.ID,
576
- QueryID: p.QueryID,
577
- Name: p.Name,
578
- Type: p.Type,
579
- DefaultValue: p.DefaultValue || undefined,
580
- Comments: '', // Not available in QueryParameterInfo
581
- IsRequired: p.IsRequired,
582
- }));
583
-
584
- const entities: QueryEntityType[] = queryEntity.QueryEntities.map((e) => ({
585
- ID: e.ID,
586
- QueryID: e.QueryID,
587
- EntityID: e.EntityID,
588
- EntityName: e.Entity || undefined, // Property is called Entity, not EntityName
589
- }));
590
-
591
- const permissions: QueryPermissionType[] = queryEntity.QueryPermissions.map((p) => ({
592
- ID: p.ID,
593
- QueryID: p.QueryID,
594
- RoleID: p.RoleID,
595
- RoleName: p.Role || undefined, // Property is called Role, not RoleName
596
- }));
597
-
598
- return {
599
- Success: true,
600
- QueryData: JSON.stringify(queryEntity.GetAll()),
601
- Fields: fields,
602
- Parameters: parameters,
603
- Entities: entities,
604
- Permissions: permissions,
605
- };
606
- } catch (err) {
607
- LogError(err);
608
- return {
609
- Success: false,
610
- ErrorMessage: `MJQueryResolverExtended::UpdateQuerySystemUser --- Error updating query: ${err instanceof Error ? err.message : String(err)}`,
611
- };
612
- }
613
- }
614
-
615
- /**
616
- * Deletes a query by ID. This mutation is restricted to system users only.
617
- * @param ID - The ID of the query to delete
618
- * @param options - Delete options controlling action execution
619
- * @param context - Application context containing user information
620
- * @returns DeleteQueryResultType with success status and deleted query data
621
- */
622
- @RequireSystemUser()
623
- @Mutation(() => DeleteQueryResultType)
624
- async DeleteQuerySystemResolver(
625
- @Arg('ID', () => String) ID: string,
626
- @Arg('options', () => DeleteOptionsInput, { nullable: true }) options: DeleteOptionsInput | null,
627
- @Ctx() context: AppContext,
628
- @PubSub() pubSub: PubSubEngine
629
- ): Promise<DeleteQueryResultType> {
630
- try {
631
- // Validate ID is not null/undefined/empty
632
- if (!ID || ID.trim() === '') {
633
- return {
634
- Success: false,
635
- ErrorMessage: 'MJQueryResolverExtended::DeleteQuerySystemResolver --- Invalid query ID: ID cannot be null or empty',
636
- };
637
- }
638
-
639
- const provider = GetReadWriteProvider(context.providers);
640
- const key = new CompositeKey([{ FieldName: 'ID', Value: ID }]);
641
-
642
- // Provide default options if none provided
643
- const deleteOptions = options || {
644
- SkipEntityAIActions: false,
645
- SkipEntityActions: false,
646
- };
647
-
648
- // Use inherited DeleteRecord method from ResolverBase
649
- const deletedQuery = await this.DeleteRecord('Queries', key, deleteOptions, provider, context.userPayload, pubSub);
650
-
651
- if (deletedQuery) {
652
- return {
653
- Success: true,
654
- QueryData: JSON.stringify(deletedQuery),
655
- };
656
- } else {
657
- return {
658
- Success: false,
659
- ErrorMessage: 'Failed to delete query using DeleteRecord method',
660
- };
661
- }
662
- } catch (err) {
663
- LogError(err);
664
- return {
665
- Success: false,
666
- ErrorMessage: `MJQueryResolverExtended::DeleteQuerySystemResolver --- Error deleting query: ${err instanceof Error ? err.message : String(err)}`,
667
- };
668
- }
669
- }
670
-
671
- /**
672
- * Finds or creates a category hierarchy based on the provided path.
673
- * Path format: "Parent/Child/Grandchild" - case insensitive lookup and creation.
674
- * @param categoryPath - Slash-separated category path
675
- * @param md - Metadata instance
676
- * @param contextUser - User context for operations
677
- * @returns The ID of the final category in the path
678
- */
679
- private async findOrCreateCategoryPath(categoryPath: string, p: DatabaseProviderBase, contextUser: UserInfo): Promise<string> {
680
- if (!categoryPath || categoryPath.trim() === '') {
681
- throw new Error('CategoryPath cannot be empty');
682
- }
604
+ /**
605
+ * Deletes a query by ID. This mutation is restricted to system users only.
606
+ * @param ID - The ID of the query to delete
607
+ * @param options - Delete options controlling action execution
608
+ * @param context - Application context containing user information
609
+ * @returns DeleteQueryResultType with success status and deleted query data
610
+ */
611
+ @RequireSystemUser()
612
+ @Mutation(() => DeleteQueryResultType)
613
+ async DeleteQuerySystemResolver(
614
+ @Arg('ID', () => String) ID: string,
615
+ @Arg('options', () => DeleteOptionsInput, { nullable: true }) options: DeleteOptionsInput | null,
616
+ @Ctx() context: AppContext,
617
+ @PubSub() pubSub: PubSubEngine
618
+ ): Promise<DeleteQueryResultType> {
619
+ try {
620
+ // Validate ID is not null/undefined/empty
621
+ if (!ID || ID.trim() === '') {
622
+ return {
623
+ Success: false,
624
+ ErrorMessage: 'MJQueryResolverExtended::DeleteQuerySystemResolver --- Invalid query ID: ID cannot be null or empty'
625
+ };
626
+ }
683
627
 
684
- const pathParts = categoryPath
685
- .split('/')
686
- .map((part) => part.trim())
687
- .filter((part) => part.length > 0);
688
- if (pathParts.length === 0) {
689
- throw new Error('CategoryPath must contain at least one valid category name');
628
+ const provider = GetReadWriteProvider(context.providers);
629
+ const key = new CompositeKey([{FieldName: 'ID', Value: ID}]);
630
+
631
+ // Provide default options if none provided
632
+ const deleteOptions = options || {
633
+ SkipEntityAIActions: false,
634
+ SkipEntityActions: false
635
+ };
636
+
637
+ // Use inherited DeleteRecord method from ResolverBase
638
+ const deletedQuery = await this.DeleteRecord('Queries', key, deleteOptions, provider, context.userPayload, pubSub);
639
+
640
+ if (deletedQuery) {
641
+ return {
642
+ Success: true,
643
+ QueryData: JSON.stringify(deletedQuery)
644
+ };
645
+ } else {
646
+ return {
647
+ Success: false,
648
+ ErrorMessage: 'Failed to delete query using DeleteRecord method'
649
+ };
650
+ }
651
+
652
+ } catch (err) {
653
+ LogError(err);
654
+ return {
655
+ Success: false,
656
+ ErrorMessage: `MJQueryResolverExtended::DeleteQuerySystemResolver --- Error deleting query: ${err instanceof Error ? err.message : String(err)}`
657
+ };
658
+ }
690
659
  }
691
660
 
692
- let currentParentID: string | null = null;
693
- let currentCategoryID: string | null = null;
661
+ /**
662
+ * Finds or creates a category hierarchy based on the provided path.
663
+ * Path format: "Parent/Child/Grandchild" - case insensitive lookup and creation.
664
+ * @param categoryPath - Slash-separated category path
665
+ * @param md - Metadata instance
666
+ * @param contextUser - User context for operations
667
+ * @returns The ID of the final category in the path
668
+ */
669
+ private async findOrCreateCategoryPath(categoryPath: string, p: DatabaseProviderBase, contextUser: UserInfo): Promise<string> {
670
+ if (!categoryPath || categoryPath.trim() === '') {
671
+ throw new Error('CategoryPath cannot be empty');
672
+ }
694
673
 
695
- for (let i = 0; i < pathParts.length; i++) {
696
- const categoryName = pathParts[i];
674
+ const pathParts = categoryPath.split('/').map(part => part.trim()).filter(part => part.length > 0);
675
+ if (pathParts.length === 0) {
676
+ throw new Error('CategoryPath must contain at least one valid category name');
677
+ }
697
678
 
698
- // Look for existing category at this level
699
- const existingCategory = await this.findCategoryByNameAndParent(p, categoryName, currentParentID, contextUser);
679
+ let currentParentID: string | null = null;
680
+ let currentCategoryID: string | null = null;
681
+
682
+ for (let i = 0; i < pathParts.length; i++) {
683
+ const categoryName = pathParts[i];
684
+
685
+ // Look for existing category at this level
686
+ const existingCategory = await this.findCategoryByNameAndParent(p, categoryName, currentParentID, contextUser);
687
+
688
+ if (existingCategory) {
689
+ currentCategoryID = existingCategory.ID;
690
+ currentParentID = existingCategory.ID;
691
+ } else {
692
+ try {
693
+ // Create new category
694
+ const newCategory = await p.GetEntityObject<QueryCategoryEntity>("Query Categories", contextUser);
695
+ if (!newCategory) {
696
+ throw new Error(`Failed to create entity object for Query Categories`);
697
+ }
698
+
699
+ newCategory.Name = categoryName;
700
+ newCategory.ParentID = currentParentID;
701
+ newCategory.UserID = contextUser.ID;
702
+ newCategory.Description = `Auto-created category from path: ${categoryPath}`;
703
+
704
+ const saveResult = await newCategory.Save();
705
+ if (!saveResult) {
706
+ // Save failed - always recheck if another request created the same category
707
+ const recheckExisting = await this.findCategoryByNameAndParent(p, categoryName, currentParentID, contextUser);
708
+ if (recheckExisting) {
709
+ // Another request created it - use that one
710
+ LogStatus(`[CreateQuery] Unique constraint detected for category '${categoryName}'. Using existing category (ID: ${recheckExisting.ID}) created by concurrent request.`);
711
+ currentCategoryID = recheckExisting.ID;
712
+ currentParentID = recheckExisting.ID;
713
+ } else {
714
+ // Genuine failure (not a duplicate)
715
+ const errorMessage = newCategory.LatestResult?.Message || '';
716
+ throw new Error(`Failed to create category '${categoryName}': ${errorMessage || 'Unknown error'}`);
717
+ }
718
+ } else {
719
+ currentCategoryID = newCategory.ID;
720
+ currentParentID = newCategory.ID;
721
+ }
722
+ } catch (error) {
723
+ // On error, double-check if category exists (race condition handling)
724
+ const recheckExisting = await this.findCategoryByNameAndParent(p, categoryName, currentParentID, contextUser);
725
+ if (recheckExisting) {
726
+ // Category exists, another request created it
727
+ LogStatus(`[CreateQuery] Exception during category creation for '${categoryName}'. Using existing category (ID: ${recheckExisting.ID}) created by concurrent request.`);
728
+ currentCategoryID = recheckExisting.ID;
729
+ currentParentID = recheckExisting.ID;
730
+ } else {
731
+ throw new Error(`Failed to create category '${categoryName}': ${error instanceof Error ? error.message : String(error)}`);
732
+ }
733
+ }
734
+ }
735
+ }
736
+
737
+ if (!currentCategoryID) {
738
+ throw new Error('Failed to determine final category ID');
739
+ }
740
+
741
+ return currentCategoryID;
742
+ }
700
743
 
701
- if (existingCategory) {
702
- currentCategoryID = existingCategory.ID;
703
- currentParentID = existingCategory.ID;
704
- } else {
744
+ /**
745
+ * Finds an existing query by name and category ID using RunView.
746
+ * Bypasses metadata cache to ensure we get the latest data from database.
747
+ * @param provider - Database provider
748
+ * @param queryName - Name of the query to find
749
+ * @param categoryID - Category ID (can be null)
750
+ * @param contextUser - User context for database operations
751
+ * @returns The matching query info or null if not found
752
+ */
753
+ private async findExistingQuery(
754
+ provider: DatabaseProviderBase,
755
+ queryName: string,
756
+ categoryID: string | null,
757
+ contextUser: UserInfo
758
+ ): Promise<any | null> {
705
759
  try {
706
- // Create new category
707
- const newCategory = await p.GetEntityObject<QueryCategoryEntity>('Query Categories', contextUser);
708
- if (!newCategory) {
709
- throw new Error(`Failed to create entity object for Query Categories`);
710
- }
711
-
712
- newCategory.Name = categoryName;
713
- newCategory.ParentID = currentParentID;
714
- newCategory.UserID = contextUser.ID;
715
- newCategory.Description = `Auto-created category from path: ${categoryPath}`;
716
-
717
- const saveResult = await newCategory.Save();
718
- if (!saveResult) {
719
- // Save failed - always recheck if another request created the same category
720
- const recheckExisting = await this.findCategoryByNameAndParent(p, categoryName, currentParentID, contextUser);
721
- if (recheckExisting) {
722
- // Another request created it - use that one
723
- LogStatus(
724
- `[CreateQuery] Unique constraint detected for category '${categoryName}'. Using existing category (ID: ${recheckExisting.ID}) created by concurrent request.`
725
- );
726
- currentCategoryID = recheckExisting.ID;
727
- currentParentID = recheckExisting.ID;
728
- } else {
729
- // Genuine failure (not a duplicate)
730
- const errorMessage = newCategory.LatestResult?.Message || '';
731
- throw new Error(`Failed to create category '${categoryName}': ${errorMessage || 'Unknown error'}`);
760
+ // Query database directly to avoid cache staleness issues
761
+ const categoryFilter = categoryID ? `CategoryID='${categoryID}'` : 'CategoryID IS NULL';
762
+ const nameFilter = `LOWER(Name) = LOWER('${queryName.replace(/'/g, "''")}')`;
763
+
764
+ const result = await provider.RunView({
765
+ EntityName: 'Queries',
766
+ ExtraFilter: `${nameFilter} AND ${categoryFilter}`
767
+ }, contextUser);
768
+
769
+ if (result.Success && result.Results && result.Results.length > 0) {
770
+ return result.Results[0];
732
771
  }
733
- } else {
734
- currentCategoryID = newCategory.ID;
735
- currentParentID = newCategory.ID;
736
- }
772
+
773
+ return null;
737
774
  } catch (error) {
738
- // On error, double-check if category exists (race condition handling)
739
- const recheckExisting = await this.findCategoryByNameAndParent(p, categoryName, currentParentID, contextUser);
740
- if (recheckExisting) {
741
- // Category exists, another request created it
742
- LogStatus(
743
- `[CreateQuery] Exception during category creation for '${categoryName}'. Using existing category (ID: ${recheckExisting.ID}) created by concurrent request.`
744
- );
745
- currentCategoryID = recheckExisting.ID;
746
- currentParentID = recheckExisting.ID;
747
- } else {
748
- throw new Error(`Failed to create category '${categoryName}': ${error instanceof Error ? error.message : String(error)}`);
749
- }
775
+ // If query fails, return null (query doesn't exist)
776
+ return null;
750
777
  }
751
- }
752
778
  }
753
779
 
754
- if (!currentCategoryID) {
755
- throw new Error('Failed to determine final category ID');
756
- }
780
+ /**
781
+ * Finds a category by name and parent ID using RunView.
782
+ * Bypasses metadata cache to ensure we get the latest data from database.
783
+ * @param categoryName - Name of the category to find
784
+ * @param parentID - Parent category ID (null for root level)
785
+ * @param contextUser - User context for database operations
786
+ * @returns The matching category entity or null if not found
787
+ */
788
+ private async findCategoryByNameAndParent(provider: DatabaseProviderBase, categoryName: string, parentID: string | null, contextUser: UserInfo): Promise<QueryCategoryEntity | null> {
789
+ try {
790
+ // Query database directly to avoid cache staleness issues
791
+ const parentFilter = parentID ? `ParentID='${parentID}'` : 'ParentID IS NULL';
792
+ const nameFilter = `LOWER(Name) = LOWER('${categoryName.replace(/'/g, "''")}')`; // Escape single quotes
793
+
794
+ const result = await provider.RunView<QueryCategoryEntity>({
795
+ EntityName: 'Query Categories',
796
+ ExtraFilter: `${nameFilter} AND ${parentFilter}`,
797
+ ResultType: 'entity_object'
798
+ }, contextUser);
799
+
800
+ if (result.Success && result.Results && result.Results.length > 0) {
801
+ return result.Results[0];
802
+ }
757
803
 
758
- return currentCategoryID;
759
- }
760
-
761
- /**
762
- * Finds an existing query by name and category ID using RunView.
763
- * Bypasses metadata cache to ensure we get the latest data from database.
764
- * @param provider - Database provider
765
- * @param queryName - Name of the query to find
766
- * @param categoryID - Category ID (can be null)
767
- * @param contextUser - User context for database operations
768
- * @returns The matching query info or null if not found
769
- */
770
- private async findExistingQuery(
771
- provider: DatabaseProviderBase,
772
- queryName: string,
773
- categoryID: string | null,
774
- contextUser: UserInfo
775
- ): Promise<any | null> {
776
- try {
777
- // Query database directly to avoid cache staleness issues
778
- const categoryFilter = categoryID ? `CategoryID='${categoryID}'` : 'CategoryID IS NULL';
779
- const nameFilter = `LOWER(Name) = LOWER('${queryName.replace(/'/g, "''")}')`;
780
-
781
- const result = await provider.RunView(
782
- {
783
- EntityName: 'Queries',
784
- ExtraFilter: `${nameFilter} AND ${categoryFilter}`,
785
- },
786
- contextUser
787
- );
788
-
789
- if (result.Success && result.Results && result.Results.length > 0) {
790
- return result.Results[0];
791
- }
792
-
793
- return null;
794
- } catch (error) {
795
- // If query fails, return null (query doesn't exist)
796
- return null;
797
- }
798
- }
799
-
800
- /**
801
- * Finds a category by name and parent ID using RunView.
802
- * Bypasses metadata cache to ensure we get the latest data from database.
803
- * @param categoryName - Name of the category to find
804
- * @param parentID - Parent category ID (null for root level)
805
- * @param contextUser - User context for database operations
806
- * @returns The matching category entity or null if not found
807
- */
808
- private async findCategoryByNameAndParent(
809
- provider: DatabaseProviderBase,
810
- categoryName: string,
811
- parentID: string | null,
812
- contextUser: UserInfo
813
- ): Promise<QueryCategoryEntity | null> {
814
- try {
815
- // Query database directly to avoid cache staleness issues
816
- const parentFilter = parentID ? `ParentID='${parentID}'` : 'ParentID IS NULL';
817
- const nameFilter = `LOWER(Name) = LOWER('${categoryName.replace(/'/g, "''")}')`; // Escape single quotes
818
-
819
- const result = await provider.RunView<QueryCategoryEntity>(
820
- {
821
- EntityName: 'Query Categories',
822
- ExtraFilter: `${nameFilter} AND ${parentFilter}`,
823
- ResultType: 'entity_object',
824
- },
825
- contextUser
826
- );
827
-
828
- if (result.Success && result.Results && result.Results.length > 0) {
829
- return result.Results[0];
830
- }
831
-
832
- return null;
833
- } catch (error) {
834
- // If query fails, return null
835
- return null;
804
+ return null;
805
+ } catch (error) {
806
+ // If query fails, return null
807
+ return null;
808
+ }
836
809
  }
837
- }
838
- }
810
+ }