@memberjunction/server 2.78.0 → 2.80.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.
@@ -1,11 +1,12 @@
1
1
  import { Arg, Ctx, Field, InputType, Mutation, ObjectType, registerEnumType, Resolver, PubSub, PubSubEngine } from 'type-graphql';
2
- import { AppContext, UserPayload } from '../types.js';
3
- import { LogError, Metadata, RunView, UserInfo, CompositeKey, EntitySaveOptions } from '@memberjunction/core';
2
+ import { AppContext } from '../types.js';
3
+ import { LogError, Metadata, RunView, UserInfo, CompositeKey } from '@memberjunction/core';
4
4
  import { RequireSystemUser } from '../directives/RequireSystemUser.js';
5
- import { QueryCategoryEntity } from '@memberjunction/core-entities';
5
+ import { QueryCategoryEntity, QueryPermissionEntity } from '@memberjunction/core-entities';
6
6
  import { QueryResolver } from '../generated/generated.js';
7
7
  import { GetReadWriteProvider } from '../util.js';
8
8
  import { DeleteOptionsInput } from '../generic/DeleteOptionsInput.js';
9
+ import { QueryEntityExtended } from '@memberjunction/core-entities-server';
9
10
 
10
11
  /**
11
12
  * Query status enumeration for GraphQL
@@ -22,6 +23,12 @@ registerEnumType(QueryStatus, {
22
23
  description: "Status of a query: Pending, Approved, Rejected, or Expired"
23
24
  });
24
25
 
26
+ @InputType()
27
+ export class QueryPermissionInputType {
28
+ @Field(() => String)
29
+ RoleID!: string;
30
+ }
31
+
25
32
  @InputType()
26
33
  export class CreateQuerySystemUserInput {
27
34
  @Field(() => String)
@@ -62,6 +69,147 @@ export class CreateQuerySystemUserInput {
62
69
 
63
70
  @Field(() => Boolean, { nullable: true })
64
71
  UsesTemplate?: boolean;
72
+
73
+ @Field(() => [QueryPermissionInputType], { nullable: true })
74
+ Permissions?: QueryPermissionInputType[];
75
+ }
76
+
77
+ @InputType()
78
+ export class UpdateQuerySystemUserInput {
79
+ @Field(() => String)
80
+ ID!: string;
81
+
82
+ @Field(() => String, { nullable: true })
83
+ Name?: string;
84
+
85
+ @Field(() => String, { nullable: true })
86
+ CategoryID?: string;
87
+
88
+ @Field(() => String, { nullable: true })
89
+ CategoryPath?: string;
90
+
91
+ @Field(() => String, { nullable: true })
92
+ UserQuestion?: string;
93
+
94
+ @Field(() => String, { nullable: true })
95
+ Description?: string;
96
+
97
+ @Field(() => String, { nullable: true })
98
+ SQL?: string;
99
+
100
+ @Field(() => String, { nullable: true })
101
+ TechnicalDescription?: string;
102
+
103
+ @Field(() => String, { nullable: true })
104
+ OriginalSQL?: string;
105
+
106
+ @Field(() => String, { nullable: true })
107
+ Feedback?: string;
108
+
109
+ @Field(() => QueryStatus, { nullable: true })
110
+ Status?: QueryStatus;
111
+
112
+ @Field(() => Number, { nullable: true })
113
+ QualityRank?: number;
114
+
115
+ @Field(() => Number, { nullable: true })
116
+ ExecutionCostRank?: number;
117
+
118
+ @Field(() => Boolean, { nullable: true })
119
+ UsesTemplate?: boolean;
120
+
121
+ @Field(() => [QueryPermissionInputType], { nullable: true })
122
+ Permissions?: QueryPermissionInputType[];
123
+ }
124
+
125
+ @ObjectType()
126
+ export class QueryFieldType {
127
+ @Field(() => String)
128
+ ID!: string;
129
+
130
+ @Field(() => String)
131
+ QueryID!: string;
132
+
133
+ @Field(() => String)
134
+ Name!: string;
135
+
136
+ @Field(() => String, { nullable: true })
137
+ Description?: string;
138
+
139
+ @Field(() => String, { nullable: true })
140
+ Type?: string;
141
+
142
+ @Field(() => Number)
143
+ Sequence!: number;
144
+
145
+ @Field(() => String, { nullable: true })
146
+ SQLBaseType?: string;
147
+
148
+ @Field(() => String, { nullable: true })
149
+ SQLFullType?: string;
150
+
151
+ @Field(() => Boolean)
152
+ IsComputed!: boolean;
153
+
154
+ @Field(() => String, { nullable: true })
155
+ ComputationDescription?: string;
156
+ }
157
+
158
+ @ObjectType()
159
+ export class QueryParameterType {
160
+ @Field(() => String)
161
+ ID!: string;
162
+
163
+ @Field(() => String)
164
+ QueryID!: string;
165
+
166
+ @Field(() => String)
167
+ Name!: string;
168
+
169
+ @Field(() => String)
170
+ Type!: string;
171
+
172
+ @Field(() => String, { nullable: true })
173
+ DefaultValue?: string;
174
+
175
+ @Field(() => String, { nullable: true })
176
+ Comments?: string;
177
+
178
+ @Field(() => Boolean)
179
+ IsRequired!: boolean;
180
+ }
181
+
182
+ @ObjectType()
183
+ export class QueryEntityType {
184
+ @Field(() => String)
185
+ ID!: string;
186
+
187
+ @Field(() => String)
188
+ QueryID!: string;
189
+
190
+ @Field(() => String)
191
+ EntityID!: string;
192
+
193
+ @Field(() => String, { nullable: true })
194
+ EntityName?: string;
195
+
196
+ @Field(() => Number)
197
+ Sequence!: number;
198
+ }
199
+
200
+ @ObjectType()
201
+ export class QueryPermissionType {
202
+ @Field(() => String)
203
+ ID!: string;
204
+
205
+ @Field(() => String)
206
+ QueryID!: string;
207
+
208
+ @Field(() => String)
209
+ RoleID!: string;
210
+
211
+ @Field(() => String, { nullable: true })
212
+ RoleName?: string;
65
213
  }
66
214
 
67
215
  @ObjectType()
@@ -74,6 +222,42 @@ export class CreateQueryResultType {
74
222
 
75
223
  @Field(() => String, { nullable: true })
76
224
  QueryData?: string;
225
+
226
+ @Field(() => [QueryFieldType], { nullable: true })
227
+ Fields?: QueryFieldType[];
228
+
229
+ @Field(() => [QueryParameterType], { nullable: true })
230
+ Parameters?: QueryParameterType[];
231
+
232
+ @Field(() => [QueryEntityType], { nullable: true })
233
+ Entities?: QueryEntityType[];
234
+
235
+ @Field(() => [QueryPermissionType], { nullable: true })
236
+ Permissions?: QueryPermissionType[];
237
+ }
238
+
239
+ @ObjectType()
240
+ export class UpdateQueryResultType {
241
+ @Field(() => Boolean)
242
+ Success!: boolean;
243
+
244
+ @Field(() => String, { nullable: true })
245
+ ErrorMessage?: string;
246
+
247
+ @Field(() => String, { nullable: true })
248
+ QueryData?: string;
249
+
250
+ @Field(() => [QueryFieldType], { nullable: true })
251
+ Fields?: QueryFieldType[];
252
+
253
+ @Field(() => [QueryParameterType], { nullable: true })
254
+ Parameters?: QueryParameterType[];
255
+
256
+ @Field(() => [QueryEntityType], { nullable: true })
257
+ Entities?: QueryEntityType[];
258
+
259
+ @Field(() => [QueryPermissionType], { nullable: true })
260
+ Permissions?: QueryPermissionType[];
77
261
  }
78
262
 
79
263
  @ObjectType()
@@ -108,46 +292,222 @@ export class QueryResolverExtended extends QueryResolver {
108
292
  let finalCategoryID = input.CategoryID;
109
293
  if (input.CategoryPath) {
110
294
  const md = new Metadata();
111
- finalCategoryID = await this.findOrCreateCategoryPath(input.CategoryPath, md, context.userPayload.userRecord, context.userPayload);
295
+ finalCategoryID = await this.findOrCreateCategoryPath(input.CategoryPath, md, context.userPayload.userRecord);
112
296
  }
113
-
114
- // Create input for the inherited CreateRecord method
115
- const createInput = {
116
- Name: input.Name,
117
- CategoryID: finalCategoryID,
118
- UserQuestion: input.UserQuestion,
119
- Description: input.Description,
120
- SQL: input.SQL,
121
- TechnicalDescription: input.TechnicalDescription,
122
- OriginalSQL: input.OriginalSQL,
123
- Feedback: input.Feedback,
297
+
298
+ // Use QueryEntityExtended which handles AI processing
299
+ const provider = GetReadWriteProvider(context.providers);
300
+ const record = await provider.GetEntityObject<QueryEntityExtended>("Queries", context.userPayload.userRecord);
301
+
302
+ // Set the fields from input, handling CategoryPath resolution
303
+ const fieldsToSet = {
304
+ ...input,
305
+ CategoryID: finalCategoryID || input.CategoryID,
124
306
  Status: input.Status || 'Approved',
125
307
  QualityRank: input.QualityRank || 0,
126
- ExecutionCostRank: input.ExecutionCostRank,
127
308
  UsesTemplate: input.UsesTemplate || false
128
309
  };
310
+ // Remove Permissions from the fields to set since we handle them separately
311
+ delete (fieldsToSet as any).Permissions;
129
312
 
130
- // Use inherited CreateRecord method which bypasses AI processing
131
- const provider = GetReadWriteProvider(context.providers);
132
- const createdQuery = await this.CreateRecord('Queries', createInput, provider, context.userPayload, pubSub);
133
-
134
- if (createdQuery) {
313
+ record.SetMany(fieldsToSet, true);
314
+ this.ListenForEntityMessages(record, pubSub, context.userPayload.userRecord);
315
+
316
+ // Pass the transactionScopeId from the user payload to the save operation
317
+ if (await record.Save()) {
318
+ // save worked, fire the AfterCreate event and then return all the data
319
+ await this.AfterCreate(provider, input); // fire event
320
+ const queryID = record.ID;
321
+
322
+ if (input.Permissions && input.Permissions.length > 0) {
323
+ await this.createPermissions(input.Permissions, queryID, context.userPayload.userRecord);
324
+ await record.RefreshRelatedMetadata(true); // force DB update since we just created new permissions
325
+ }
326
+
135
327
  return {
136
328
  Success: true,
137
- QueryData: JSON.stringify(createdQuery)
329
+ QueryData: JSON.stringify(record.GetAll()),
330
+ Fields: record.QueryFields,
331
+ Parameters: record.QueryParameters,
332
+ Entities: record.QueryEntities,
333
+ Permissions: record.QueryPermissions
138
334
  };
139
- } else {
335
+ }
336
+ else {
140
337
  return {
141
338
  Success: false,
142
339
  ErrorMessage: 'Failed to create query using CreateRecord method'
143
340
  };
144
341
  }
342
+ }
343
+ catch (err) {
344
+ LogError(err);
345
+ return {
346
+ Success: false,
347
+ ErrorMessage: `QueryResolverExtended::CreateQuerySystemUser --- Error creating query: ${err instanceof Error ? err.message : String(err)}`
348
+ };
349
+ }
350
+ }
351
+
352
+ protected async createPermissions(permissions: QueryPermissionInputType[], queryID: string, contextUser: UserInfo): Promise<QueryPermissionType[]> {
353
+ // Create permissions if provided
354
+ const createdPermissions: QueryPermissionType[] = [];
355
+ if (permissions && permissions.length > 0) {
356
+ const md = new Metadata();
357
+ for (const perm of permissions) {
358
+ const permissionEntity = await md.GetEntityObject<QueryPermissionEntity>('Query Permissions', contextUser);
359
+ if (permissionEntity) {
360
+ permissionEntity.QueryID = queryID;
361
+ permissionEntity.RoleID = perm.RoleID;
362
+
363
+ const saveResult = await permissionEntity.Save();
364
+ if (saveResult) {
365
+ createdPermissions.push({
366
+ ID: permissionEntity.ID,
367
+ QueryID: permissionEntity.QueryID,
368
+ RoleID: permissionEntity.RoleID,
369
+ RoleName: permissionEntity.Role // The view includes the Role name
370
+ });
371
+ }
372
+ }
373
+ }
374
+ }
375
+ return createdPermissions;
376
+ }
377
+
378
+ /**
379
+ * Updates an existing query with the provided attributes. This mutation is restricted to system users only.
380
+ * @param input - UpdateQuerySystemUserInput containing the query ID and fields to update
381
+ * @param context - Application context containing user information
382
+ * @returns UpdateQueryResultType with success status and updated query data including related entities
383
+ */
384
+ @RequireSystemUser()
385
+ @Mutation(() => UpdateQueryResultType)
386
+ async UpdateQuerySystemUser(
387
+ @Arg('input', () => UpdateQuerySystemUserInput) input: UpdateQuerySystemUserInput,
388
+ @Ctx() context: AppContext,
389
+ @PubSub() pubSub: PubSubEngine
390
+ ): Promise<UpdateQueryResultType> {
391
+ try {
392
+ // Load the existing query using QueryEntityExtended
393
+ const provider = GetReadWriteProvider(context.providers);
394
+ const queryEntity = await provider.GetEntityObject<QueryEntityExtended>('Queries', context.userPayload.userRecord);
395
+ if (!queryEntity || !await queryEntity.Load(input.ID)) {
396
+ return {
397
+ Success: false,
398
+ ErrorMessage: `Query with ID ${input.ID} not found`
399
+ };
400
+ }
401
+
402
+ // Handle CategoryPath if provided
403
+ let finalCategoryID = input.CategoryID;
404
+ if (input.CategoryPath) {
405
+ const md = new Metadata();
406
+ finalCategoryID = await this.findOrCreateCategoryPath(input.CategoryPath, md, context.userPayload.userRecord);
407
+ }
408
+
409
+ // Update fields that were provided
410
+ if (input.Name !== undefined) queryEntity.Name = input.Name;
411
+ if (finalCategoryID !== undefined) queryEntity.CategoryID = finalCategoryID;
412
+ if (input.UserQuestion !== undefined) queryEntity.UserQuestion = input.UserQuestion;
413
+ if (input.Description !== undefined) queryEntity.Description = input.Description;
414
+ if (input.SQL !== undefined) queryEntity.SQL = input.SQL;
415
+ if (input.TechnicalDescription !== undefined) queryEntity.TechnicalDescription = input.TechnicalDescription;
416
+ if (input.OriginalSQL !== undefined) queryEntity.OriginalSQL = input.OriginalSQL;
417
+ if (input.Feedback !== undefined) queryEntity.Feedback = input.Feedback;
418
+ if (input.Status !== undefined) queryEntity.Status = input.Status;
419
+ if (input.QualityRank !== undefined) queryEntity.QualityRank = input.QualityRank;
420
+ if (input.ExecutionCostRank !== undefined) queryEntity.ExecutionCostRank = input.ExecutionCostRank;
421
+ if (input.UsesTemplate !== undefined) queryEntity.UsesTemplate = input.UsesTemplate;
422
+
423
+ // Save the updated query
424
+ const saveResult = await queryEntity.Save();
425
+ if (!saveResult) {
426
+ return {
427
+ Success: false,
428
+ ErrorMessage: `Failed to update query: ${queryEntity.LatestResult?.Message || 'Unknown error'}`
429
+ };
430
+ }
431
+
432
+ const queryID = queryEntity.ID;
433
+
434
+ // Handle permissions update if provided
435
+ if (input.Permissions !== undefined) {
436
+ // Delete existing permissions
437
+ const rv = new RunView();
438
+ const existingPermissions = await rv.RunView<QueryPermissionEntity>({
439
+ EntityName: 'Query Permissions',
440
+ ExtraFilter: `QueryID='${queryID}'`,
441
+ ResultType: 'entity_object'
442
+ }, context.userPayload.userRecord);
443
+
444
+ if (existingPermissions.Success && existingPermissions.Results) {
445
+ for (const perm of existingPermissions.Results) {
446
+ await perm.Delete();
447
+ }
448
+ }
449
+
450
+ // Create new permissions
451
+ await this.createPermissions(input.Permissions, queryID, context.userPayload.userRecord);
452
+
453
+ // Refresh the metadata to get updated permissions
454
+ await queryEntity.RefreshRelatedMetadata(true);
455
+ }
456
+
457
+ // Use the properties from QueryEntityExtended instead of manual loading
458
+ const fields: QueryFieldType[] = queryEntity.QueryFields.map(f => ({
459
+ ID: f.ID,
460
+ QueryID: f.QueryID,
461
+ Name: f.Name,
462
+ Description: f.Description || undefined,
463
+ Type: f.SQLBaseType || undefined,
464
+ Sequence: f.Sequence,
465
+ SQLBaseType: f.SQLBaseType || undefined,
466
+ SQLFullType: f.SQLFullType || undefined,
467
+ IsComputed: f.IsComputed,
468
+ ComputationEnabled: true, // Default to true as it's not in QueryFieldInfo
469
+ ComputationDescription: f.ComputationDescription || undefined
470
+ }));
471
+
472
+ const parameters: QueryParameterType[] = queryEntity.QueryParameters.map(p => ({
473
+ ID: p.ID,
474
+ QueryID: p.QueryID,
475
+ Name: p.Name,
476
+ Type: p.Type,
477
+ DefaultValue: p.DefaultValue || undefined,
478
+ Comments: '', // Not available in QueryParameterInfo
479
+ IsRequired: p.IsRequired
480
+ }));
481
+
482
+ const entities: QueryEntityType[] = queryEntity.QueryEntities.map(e => ({
483
+ ID: e.ID,
484
+ QueryID: e.QueryID,
485
+ EntityID: e.EntityID,
486
+ EntityName: e.Entity || undefined, // Property is called Entity, not EntityName
487
+ Sequence: e.Sequence || 0
488
+ }));
489
+
490
+ const permissions: QueryPermissionType[] = queryEntity.QueryPermissions.map(p => ({
491
+ ID: p.ID,
492
+ QueryID: p.QueryID,
493
+ RoleID: p.RoleID,
494
+ RoleName: p.Role || undefined // Property is called Role, not RoleName
495
+ }));
496
+
497
+ return {
498
+ Success: true,
499
+ QueryData: JSON.stringify(queryEntity.GetAll()),
500
+ Fields: fields,
501
+ Parameters: parameters,
502
+ Entities: entities,
503
+ Permissions: permissions
504
+ };
145
505
 
146
506
  } catch (err) {
147
507
  LogError(err);
148
508
  return {
149
509
  Success: false,
150
- ErrorMessage: `QueryResolverExtended::CreateQuerySystemUser --- Error creating query: ${err instanceof Error ? err.message : String(err)}`
510
+ ErrorMessage: `QueryResolverExtended::UpdateQuerySystemUser --- Error updating query: ${err instanceof Error ? err.message : String(err)}`
151
511
  };
152
512
  }
153
513
  }
@@ -217,7 +577,7 @@ export class QueryResolverExtended extends QueryResolver {
217
577
  * @param contextUser - User context for operations
218
578
  * @returns The ID of the final category in the path
219
579
  */
220
- private async findOrCreateCategoryPath(categoryPath: string, md: Metadata, contextUser: UserInfo, userPayload: UserPayload): Promise<string> {
580
+ private async findOrCreateCategoryPath(categoryPath: string, md: Metadata, contextUser: UserInfo): Promise<string> {
221
581
  if (!categoryPath || categoryPath.trim() === '') {
222
582
  throw new Error('CategoryPath cannot be empty');
223
583
  }
@@ -240,23 +600,31 @@ export class QueryResolverExtended extends QueryResolver {
240
600
  currentCategoryID = existingCategory.ID;
241
601
  currentParentID = existingCategory.ID;
242
602
  } else {
243
- // Create new category
244
- const newCategory = await md.GetEntityObject<QueryCategoryEntity>("Query Categories", contextUser);
245
- newCategory.Name = categoryName;
246
- newCategory.ParentID = currentParentID;
247
- newCategory.UserID = contextUser.ID;
248
- newCategory.Description = `Auto-created category from path: ${categoryPath}`;
249
-
250
- const saveResult = await newCategory.Save();
251
- if (!saveResult) {
252
- throw new Error(`Failed to create category '${categoryName}': ${newCategory.LatestResult?.Message || 'Unknown error'}`);
603
+ try {
604
+ // Create new category
605
+ const newCategory = await md.GetEntityObject<QueryCategoryEntity>("Query Categories", contextUser);
606
+ if (!newCategory) {
607
+ throw new Error(`Failed to create entity object for Query Categories`);
608
+ }
609
+
610
+ newCategory.Name = categoryName;
611
+ newCategory.ParentID = currentParentID;
612
+ newCategory.UserID = contextUser.ID;
613
+ newCategory.Description = `Auto-created category from path: ${categoryPath}`;
614
+
615
+ const saveResult = await newCategory.Save();
616
+ if (!saveResult) {
617
+ throw new Error(`Failed to create category '${categoryName}': ${newCategory.LatestResult?.Message || 'Unknown error'}`);
618
+ }
619
+
620
+ currentCategoryID = newCategory.ID;
621
+ currentParentID = newCategory.ID;
622
+
623
+ // Refresh metadata after each category creation to ensure it's available for subsequent lookups
624
+ await md.Refresh();
625
+ } catch (error) {
626
+ throw new Error(`Failed to create category '${categoryName}': ${error instanceof Error ? error.message : String(error)}`);
253
627
  }
254
-
255
- currentCategoryID = newCategory.ID;
256
- currentParentID = newCategory.ID;
257
-
258
- // Refresh metadata after each category creation to ensure it's available for subsequent lookups
259
- await md.Refresh();
260
628
  }
261
629
  }
262
630