@memberjunction/server 2.35.0 → 2.36.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 (52) hide show
  1. package/README.md +15 -1
  2. package/dist/config.d.ts +69 -1
  3. package/dist/config.d.ts.map +1 -1
  4. package/dist/config.js +11 -1
  5. package/dist/config.js.map +1 -1
  6. package/dist/generated/generated.d.ts +15 -12
  7. package/dist/generated/generated.d.ts.map +1 -1
  8. package/dist/generated/generated.js +73 -58
  9. package/dist/generated/generated.js.map +1 -1
  10. package/dist/index.d.ts +2 -0
  11. package/dist/index.d.ts.map +1 -1
  12. package/dist/index.js +41 -0
  13. package/dist/index.js.map +1 -1
  14. package/dist/resolvers/AskSkipResolver.d.ts +60 -5
  15. package/dist/resolvers/AskSkipResolver.d.ts.map +1 -1
  16. package/dist/resolvers/AskSkipResolver.js +587 -31
  17. package/dist/resolvers/AskSkipResolver.js.map +1 -1
  18. package/dist/rest/EntityCRUDHandler.d.ts +29 -0
  19. package/dist/rest/EntityCRUDHandler.d.ts.map +1 -0
  20. package/dist/rest/EntityCRUDHandler.js +197 -0
  21. package/dist/rest/EntityCRUDHandler.js.map +1 -0
  22. package/dist/rest/RESTEndpointHandler.d.ts +41 -0
  23. package/dist/rest/RESTEndpointHandler.d.ts.map +1 -0
  24. package/dist/rest/RESTEndpointHandler.js +537 -0
  25. package/dist/rest/RESTEndpointHandler.js.map +1 -0
  26. package/dist/rest/ViewOperationsHandler.d.ts +21 -0
  27. package/dist/rest/ViewOperationsHandler.d.ts.map +1 -0
  28. package/dist/rest/ViewOperationsHandler.js +144 -0
  29. package/dist/rest/ViewOperationsHandler.js.map +1 -0
  30. package/dist/rest/index.d.ts +5 -0
  31. package/dist/rest/index.d.ts.map +1 -0
  32. package/dist/rest/index.js +5 -0
  33. package/dist/rest/index.js.map +1 -0
  34. package/dist/rest/setupRESTEndpoints.d.ts +12 -0
  35. package/dist/rest/setupRESTEndpoints.d.ts.map +1 -0
  36. package/dist/rest/setupRESTEndpoints.js +27 -0
  37. package/dist/rest/setupRESTEndpoints.js.map +1 -0
  38. package/dist/scheduler/LearningCycleScheduler.d.ts +44 -0
  39. package/dist/scheduler/LearningCycleScheduler.d.ts.map +1 -0
  40. package/dist/scheduler/LearningCycleScheduler.js +188 -0
  41. package/dist/scheduler/LearningCycleScheduler.js.map +1 -0
  42. package/package.json +24 -26
  43. package/src/config.ts +15 -1
  44. package/src/generated/generated.ts +53 -44
  45. package/src/index.ts +56 -1
  46. package/src/resolvers/AskSkipResolver.ts +787 -51
  47. package/src/rest/EntityCRUDHandler.ts +279 -0
  48. package/src/rest/RESTEndpointHandler.ts +834 -0
  49. package/src/rest/ViewOperationsHandler.ts +207 -0
  50. package/src/rest/index.ts +4 -0
  51. package/src/rest/setupRESTEndpoints.ts +89 -0
  52. package/src/scheduler/LearningCycleScheduler.ts +312 -0
@@ -0,0 +1,834 @@
1
+ import express from 'express';
2
+ import {
3
+ BaseEntity, CompositeKey, EntityDeleteOptions, EntityInfo,
4
+ EntityPermissionType, EntitySaveOptions, LogError, Metadata,
5
+ RunView, RunViewParams
6
+ } from '@memberjunction/core';
7
+ import { EntityCRUDHandler } from './EntityCRUDHandler.js';
8
+ import { ViewOperationsHandler } from './ViewOperationsHandler.js';
9
+
10
+ /**
11
+ * Configuration options for RESTEndpointHandler
12
+ */
13
+ export interface RESTEndpointHandlerOptions {
14
+ /**
15
+ * Array of entity names to include in the API (case-insensitive)
16
+ * If provided, only these entities will be accessible through the REST API
17
+ * Supports wildcards using '*' (e.g., 'User*' matches 'User', 'UserRole', etc.)
18
+ */
19
+ includeEntities?: string[];
20
+
21
+ /**
22
+ * Array of entity names to exclude from the API (case-insensitive)
23
+ * These entities will not be accessible through the REST API
24
+ * Supports wildcards using '*' (e.g., 'Secret*' matches 'Secret', 'SecretKey', etc.)
25
+ * Note: Exclude patterns always override include patterns
26
+ */
27
+ excludeEntities?: string[];
28
+
29
+ /**
30
+ * Array of schema names to include in the API (case-insensitive)
31
+ * If provided, only entities in these schemas will be accessible through the REST API
32
+ */
33
+ includeSchemas?: string[];
34
+
35
+ /**
36
+ * Array of schema names to exclude from the API (case-insensitive)
37
+ * Entities in these schemas will not be accessible through the REST API
38
+ * Note: Exclude patterns always override include patterns
39
+ */
40
+ excludeSchemas?: string[];
41
+ }
42
+
43
+ /**
44
+ * RESTEndpointHandler provides REST API functionality for MemberJunction entities
45
+ * This class handles request routing and processing for a /rest endpoint that exposes
46
+ * entity operations via REST instead of GraphQL
47
+ */
48
+ export class RESTEndpointHandler {
49
+ private router: express.Router;
50
+ private options: RESTEndpointHandlerOptions;
51
+
52
+ constructor(options: RESTEndpointHandlerOptions = {}) {
53
+ this.router = express.Router();
54
+ this.options = options;
55
+ this.setupRoutes();
56
+ }
57
+
58
+ /**
59
+ * Determines if an entity is allowed based on include/exclude lists
60
+ * with support for wildcards and schema-level filtering
61
+ * @param entityName The name of the entity to check
62
+ * @returns True if the entity is allowed, false otherwise
63
+ */
64
+ private isEntityAllowed(entityName: string): boolean {
65
+ const name = entityName.toLowerCase();
66
+ const md = new Metadata();
67
+ const entity = md.Entities.find(e => e.Name.toLowerCase() === name);
68
+
69
+ // If entity not found in metadata, don't allow it
70
+ if (!entity) {
71
+ return false;
72
+ }
73
+
74
+ const schemaName = entity.SchemaName.toLowerCase();
75
+
76
+ // 1. Check schema exclusions first (these take highest precedence)
77
+ if (this.options.excludeSchemas && this.options.excludeSchemas.length > 0) {
78
+ if (this.options.excludeSchemas.some(schema => schema.toLowerCase() === schemaName)) {
79
+ return false;
80
+ }
81
+ }
82
+
83
+ // 2. Check entity exclusions next (these override entity inclusions)
84
+ if (this.options.excludeEntities && this.options.excludeEntities.length > 0) {
85
+ // Check for direct match
86
+ if (this.options.excludeEntities.includes(name)) {
87
+ return false;
88
+ }
89
+
90
+ // Check for wildcard matches
91
+ for (const pattern of this.options.excludeEntities) {
92
+ if (pattern.includes('*')) {
93
+ const regex = new RegExp('^' + pattern.toLowerCase().replace(/\*/g, '.*') + '$');
94
+ if (regex.test(name)) {
95
+ return false;
96
+ }
97
+ }
98
+ }
99
+ }
100
+
101
+ // 3. Check schema inclusions (if specified, only entities from these schemas are allowed)
102
+ if (this.options.includeSchemas && this.options.includeSchemas.length > 0) {
103
+ if (!this.options.includeSchemas.some(schema => schema.toLowerCase() === schemaName)) {
104
+ return false;
105
+ }
106
+ }
107
+
108
+ // 4. Check entity inclusions
109
+ if (this.options.includeEntities && this.options.includeEntities.length > 0) {
110
+ // Check for direct match
111
+ if (this.options.includeEntities.includes(name)) {
112
+ return true;
113
+ }
114
+
115
+ // Check for wildcard matches
116
+ for (const pattern of this.options.includeEntities) {
117
+ if (pattern.includes('*')) {
118
+ const regex = new RegExp('^' + pattern.toLowerCase().replace(/\*/g, '.*') + '$');
119
+ if (regex.test(name)) {
120
+ return true;
121
+ }
122
+ }
123
+ }
124
+
125
+ // If include list is specified but no matches found, entity is not allowed
126
+ return false;
127
+ }
128
+
129
+ // By default, allow all entities
130
+ return true;
131
+ }
132
+
133
+ /**
134
+ * Set up all the API routes for the REST endpoints
135
+ */
136
+ private setupRoutes() {
137
+ // Middleware to extract MJ user
138
+ this.router.use(this.extractMJUser);
139
+
140
+ // Middleware to check entity allowlist/blocklist
141
+ this.router.use('/entities/:entityName', this.checkEntityAccess.bind(this));
142
+ this.router.use('/views/:entityName', this.checkEntityAccess.bind(this));
143
+ this.router.use('/metadata/entities/:entityName', this.checkEntityAccess.bind(this));
144
+ this.router.use('/users/:userId/favorites/:entityName', this.checkEntityAccess.bind(this));
145
+
146
+ // Entity collection operations
147
+ this.router.get('/entities/:entityName', this.getEntityList.bind(this));
148
+ this.router.post('/entities/:entityName', this.createEntity.bind(this));
149
+
150
+ // Individual entity operations
151
+ this.router.get('/entities/:entityName/:id', this.getEntity.bind(this));
152
+ this.router.put('/entities/:entityName/:id', this.updateEntity.bind(this));
153
+ this.router.delete('/entities/:entityName/:id', this.deleteEntity.bind(this));
154
+
155
+ // Record changes and dependencies
156
+ this.router.get('/entities/:entityName/:id/changes', this.getRecordChanges.bind(this));
157
+ this.router.get('/entities/:entityName/:id/dependencies', this.getRecordDependencies.bind(this));
158
+ this.router.get('/entities/:entityName/:id/name', this.getEntityRecordName.bind(this));
159
+
160
+ // View operations
161
+ this.router.post('/views/:entityName', this.runView.bind(this));
162
+ this.router.post('/views/batch', this.runViews.bind(this));
163
+ this.router.get('/views/entity', this.getViewEntity.bind(this));
164
+
165
+ // Metadata endpoints
166
+ this.router.get('/metadata/entities', this.getEntityMetadata.bind(this));
167
+ this.router.get('/metadata/entities/:entityName', this.getEntityFieldMetadata.bind(this));
168
+
169
+ // Views metadata
170
+ this.router.get('/views/:entityName/metadata', this.getViewsMetadata.bind(this));
171
+
172
+ // User operations
173
+ this.router.get('/users/current', this.getCurrentUser.bind(this));
174
+ this.router.get('/users/:userId/favorites/:entityName/:id', this.getRecordFavoriteStatus.bind(this));
175
+ this.router.put('/users/:userId/favorites/:entityName/:id', this.setRecordFavoriteStatus.bind(this));
176
+ this.router.delete('/users/:userId/favorites/:entityName/:id', this.removeRecordFavoriteStatus.bind(this));
177
+
178
+ // Transaction operations
179
+ this.router.post('/transactions', this.executeTransaction.bind(this));
180
+
181
+ // Reports and queries
182
+ this.router.get('/reports/:reportId', this.runReport.bind(this));
183
+ this.router.post('/queries/run', this.runQuery.bind(this));
184
+
185
+ // Error handling
186
+ this.router.use(this.errorHandler);
187
+ }
188
+
189
+ /**
190
+ * Middleware to check entity access based on include/exclude lists
191
+ */
192
+ private checkEntityAccess(req: express.Request, res: express.Response, next: express.NextFunction): void {
193
+ const entityName = req.params.entityName;
194
+
195
+ if (!entityName) {
196
+ next();
197
+ return;
198
+ }
199
+
200
+ if (!this.isEntityAllowed(entityName)) {
201
+ res.status(403).json({
202
+ error: `Access to entity '${entityName}' is not allowed through the REST API`,
203
+ details: 'This entity is either not included in the allowlist or is explicitly excluded in the REST API configuration'
204
+ });
205
+ return;
206
+ }
207
+
208
+ next();
209
+ }
210
+
211
+ /**
212
+ * Middleware to extract MJ user from request
213
+ */
214
+ private async extractMJUser(req: express.Request, res: express.Response, next: express.NextFunction): Promise<void> {
215
+ try {
216
+ // If authentication middleware has already set req.user with basic info
217
+ if (req['user']) {
218
+ // Get the full MemberJunction user
219
+ const md = new Metadata();
220
+ const userInfo = req['user'];
221
+ // Get user info based on email or ID
222
+ // Note: The actual implementation here would depend on how the MemberJunction core handles user lookup
223
+ // This is a simplification that would need to be implemented properly
224
+ req['mjUser'] = userInfo;
225
+
226
+ if (!req['mjUser']) {
227
+ res.status(401).json({ error: 'User not found in MemberJunction' });
228
+ return;
229
+ }
230
+ } else {
231
+ res.status(401).json({ error: 'Authentication required' });
232
+ return;
233
+ }
234
+
235
+ next();
236
+ } catch (error) {
237
+ next(error);
238
+ }
239
+ }
240
+
241
+ /**
242
+ * Error handling middleware
243
+ */
244
+ private errorHandler(err: any, req: express.Request, res: express.Response, next: express.NextFunction): void {
245
+ LogError(err);
246
+
247
+ if (err.name === 'UnauthorizedError') {
248
+ res.status(401).json({ error: 'Invalid token' });
249
+ return;
250
+ }
251
+
252
+ res.status(500).json({ error: (err as Error)?.message || 'Internal server error' });
253
+ }
254
+
255
+ /**
256
+ * Get the current user
257
+ */
258
+ private async getCurrentUser(req: express.Request, res: express.Response): Promise<void> {
259
+ try {
260
+ const user = req['mjUser'];
261
+
262
+ // Return user info without sensitive data
263
+ res.json({
264
+ ID: user.ID,
265
+ Name: user.Name,
266
+ Email: user.Email,
267
+ FirstName: user.FirstName,
268
+ LastName: user.LastName,
269
+ IsAdmin: user.IsAdmin,
270
+ UserRoles: user.UserRoles.map(role => ({
271
+ ID: role.ID,
272
+ Name: role.Name,
273
+ Description: role.Description
274
+ }))
275
+ });
276
+ } catch (error) {
277
+ LogError(error);
278
+ res.status(500).json({ error: (error as Error)?.message || 'Unknown error' });
279
+ }
280
+ }
281
+
282
+ /**
283
+ * Lists entities with optional filtering
284
+ */
285
+ private async getEntityList(req: express.Request, res: express.Response): Promise<void> {
286
+ try {
287
+ const { entityName } = req.params;
288
+ const { filter, orderBy, fields, maxRows, startRow } = req.query;
289
+
290
+ const user = req['mjUser'];
291
+
292
+ // Convert the request to a RunViewParams object
293
+ const params: RunViewParams = {
294
+ EntityName: entityName,
295
+ ExtraFilter: filter as string,
296
+ OrderBy: orderBy as string,
297
+ Fields: fields ? (fields as string).split(',') : undefined,
298
+ MaxRows: maxRows ? parseInt(maxRows as string) : undefined,
299
+ StartRow: startRow ? parseInt(startRow as string) : undefined
300
+ };
301
+
302
+ const result = await ViewOperationsHandler.listEntities(params, user);
303
+ res.json(result);
304
+ } catch (error) {
305
+ LogError(error);
306
+ res.status(500).json({ error: (error as Error)?.message || 'Unknown error' });
307
+ }
308
+ }
309
+
310
+ /**
311
+ * Get a single entity by ID
312
+ */
313
+ private async getEntity(req: express.Request, res: express.Response): Promise<void> {
314
+ try {
315
+ const { entityName, id } = req.params;
316
+ const { include } = req.query; // Optional related entities to include
317
+
318
+ const user = req['mjUser'];
319
+ const relatedEntities = include ? (include as string).split(',') : null;
320
+
321
+ const result = await EntityCRUDHandler.getEntity(entityName, id, relatedEntities, user);
322
+
323
+ if (result.success) {
324
+ res.json(result.entity);
325
+ } else {
326
+ res.status(result.error.includes('not found') ? 404 : 400).json({ error: result.error });
327
+ }
328
+ } catch (error) {
329
+ LogError(error);
330
+ res.status(500).json({ error: (error as Error)?.message || 'Unknown error' });
331
+ }
332
+ }
333
+
334
+ /**
335
+ * Create a new entity
336
+ */
337
+ private async createEntity(req: express.Request, res: express.Response): Promise<void> {
338
+ try {
339
+ const { entityName } = req.params;
340
+ const entityData = req.body;
341
+
342
+ const user = req['mjUser'];
343
+
344
+ const result = await EntityCRUDHandler.createEntity(entityName, entityData, user);
345
+
346
+ if (result.success) {
347
+ res.status(201).json(result.entity);
348
+ } else {
349
+ res.status(400).json({
350
+ error: result.error,
351
+ details: result.details
352
+ });
353
+ }
354
+ } catch (error) {
355
+ LogError(error);
356
+ res.status(500).json({ error: (error as Error)?.message || 'Unknown error' });
357
+ }
358
+ }
359
+
360
+ /**
361
+ * Update an existing entity
362
+ */
363
+ private async updateEntity(req: express.Request, res: express.Response): Promise<void> {
364
+ try {
365
+ const { entityName, id } = req.params;
366
+ const updateData = req.body;
367
+
368
+ const user = req['mjUser'];
369
+
370
+ const result = await EntityCRUDHandler.updateEntity(entityName, id, updateData, user);
371
+
372
+ if (result.success) {
373
+ res.json(result.entity);
374
+ } else {
375
+ res.status(result.error.includes('not found') ? 404 : 400).json({
376
+ error: result.error,
377
+ details: result.details
378
+ });
379
+ }
380
+ } catch (error) {
381
+ LogError(error);
382
+ res.status(500).json({ error: (error as Error)?.message || 'Unknown error' });
383
+ }
384
+ }
385
+
386
+ /**
387
+ * Delete an entity
388
+ */
389
+ private async deleteEntity(req: express.Request, res: express.Response): Promise<void> {
390
+ try {
391
+ const { entityName, id } = req.params;
392
+ const options = req.query.options ? JSON.parse(req.query.options as string) : {};
393
+
394
+ const user = req['mjUser'];
395
+
396
+ // Convert options to EntityDeleteOptions
397
+ const deleteOptions = new EntityDeleteOptions();
398
+ if (options.SkipEntityAIActions !== undefined) deleteOptions.SkipEntityAIActions = !!options.SkipEntityAIActions;
399
+ if (options.SkipEntityActions !== undefined) deleteOptions.SkipEntityActions = !!options.SkipEntityActions;
400
+ if (options.ReplayOnly !== undefined) deleteOptions.ReplayOnly = !!options.ReplayOnly;
401
+
402
+ const result = await EntityCRUDHandler.deleteEntity(entityName, id, deleteOptions, user);
403
+
404
+ if (result.success) {
405
+ res.status(204).send();
406
+ } else {
407
+ res.status(result.error.includes('not found') ? 404 : 400).json({ error: result.error });
408
+ }
409
+ } catch (error) {
410
+ LogError(error);
411
+ res.status(500).json({ error: (error as Error)?.message || 'Unknown error' });
412
+ }
413
+ }
414
+
415
+ /**
416
+ * Get record changes for an entity
417
+ */
418
+ private async getRecordChanges(req: express.Request, res: express.Response): Promise<void> {
419
+ try {
420
+ const { entityName, id } = req.params;
421
+ const user = req['mjUser'];
422
+
423
+ // Get the entity object
424
+ const md = new Metadata();
425
+ const entity = await md.GetEntityObject(entityName, user);
426
+
427
+ // Create a composite key
428
+ const compositeKey = this.createCompositeKey(entity.EntityInfo, id);
429
+
430
+ // Use a direct approach for getting record changes
431
+ // Note: This is a simplification. The actual implementation may need to be adjusted
432
+ // based on how the MemberJunction core handles record changes
433
+ const changes = []; // This would be populated with actual record changes
434
+
435
+ res.json(changes);
436
+ } catch (error) {
437
+ LogError(error);
438
+ res.status(500).json({ error: (error as Error)?.message || 'Unknown error' });
439
+ }
440
+ }
441
+
442
+ /**
443
+ * Get record dependencies for an entity
444
+ */
445
+ private async getRecordDependencies(req: express.Request, res: express.Response): Promise<void> {
446
+ try {
447
+ const { entityName, id } = req.params;
448
+ const user = req['mjUser'];
449
+
450
+ // Get the entity object
451
+ const md = new Metadata();
452
+ const entity = await md.GetEntityObject(entityName, user);
453
+
454
+ // Create a composite key
455
+ const compositeKey = this.createCompositeKey(entity.EntityInfo, id);
456
+
457
+ // Use a direct approach for getting record dependencies
458
+ // Note: This is a simplification. The actual implementation may need to be adjusted
459
+ // based on how the MemberJunction core handles record dependencies
460
+ const dependencies = []; // This would be populated with actual record dependencies
461
+
462
+ res.json(dependencies);
463
+ } catch (error) {
464
+ LogError(error);
465
+ res.status(500).json({ error: (error as Error)?.message || 'Unknown error' });
466
+ }
467
+ }
468
+
469
+ /**
470
+ * Get entity record name
471
+ */
472
+ private async getEntityRecordName(req: express.Request, res: express.Response): Promise<void> {
473
+ try {
474
+ const { entityName, id } = req.params;
475
+ const user = req['mjUser'];
476
+
477
+ // Get the entity object
478
+ const md = new Metadata();
479
+ const entity = await md.GetEntityObject(entityName, user);
480
+
481
+ // Create a composite key
482
+ const compositeKey = this.createCompositeKey(entity.EntityInfo, id);
483
+
484
+ // Use a direct approach for getting entity record name
485
+ // Note: This is a simplification. The actual implementation may need to be adjusted
486
+ const recordName = "Record Name"; // This would be populated with the actual record name
487
+
488
+ res.json({ recordName });
489
+ } catch (error) {
490
+ LogError(error);
491
+ res.status(500).json({ error: (error as Error)?.message || 'Unknown error' });
492
+ }
493
+ }
494
+
495
+ /**
496
+ * Run a view
497
+ */
498
+ private async runView(req: express.Request, res: express.Response): Promise<void> {
499
+ try {
500
+ const { entityName } = req.params;
501
+ const viewParams = req.body;
502
+
503
+ const user = req['mjUser'];
504
+
505
+ // Create RunViewParams from the request body
506
+ const params: RunViewParams = {
507
+ EntityName: entityName,
508
+ ...viewParams
509
+ };
510
+
511
+ const result = await ViewOperationsHandler.runView(params, user);
512
+
513
+ if (result.success) {
514
+ res.json(result.result);
515
+ } else {
516
+ res.status(400).json({ error: result.error });
517
+ }
518
+ } catch (error) {
519
+ LogError(error);
520
+ res.status(500).json({ error: (error as Error)?.message || 'Unknown error' });
521
+ }
522
+ }
523
+
524
+ /**
525
+ * Run multiple views in batch
526
+ */
527
+ private async runViews(req: express.Request, res: express.Response): Promise<void> {
528
+ try {
529
+ const { params } = req.body;
530
+ const user = req['mjUser'];
531
+
532
+ if (!Array.isArray(params)) {
533
+ res.status(400).json({ error: 'params must be an array of RunViewParams' });
534
+ return;
535
+ }
536
+
537
+ // Filter out any views for entities that aren't allowed
538
+ // using our enhanced entity filtering with wildcards and schema support
539
+ const filteredParams = params.filter(p => this.isEntityAllowed(p.EntityName));
540
+
541
+ // If all requested entities were filtered out, return an error
542
+ if (filteredParams.length === 0 && params.length > 0) {
543
+ res.status(403).json({
544
+ error: 'None of the requested entities are allowed through the REST API',
545
+ details: 'The entities requested are either not included in the allowlist or are explicitly excluded in the REST API configuration'
546
+ });
547
+ return;
548
+ }
549
+
550
+ const result = await ViewOperationsHandler.runViews(filteredParams, user);
551
+
552
+ if (result.success) {
553
+ res.json(result.results);
554
+ } else {
555
+ res.status(400).json({ error: result.error });
556
+ }
557
+ } catch (error) {
558
+ LogError(error);
559
+ res.status(500).json({ error: (error as Error)?.message || 'Unknown error' });
560
+ }
561
+ }
562
+
563
+ /**
564
+ * Get entity for a view
565
+ */
566
+ private async getViewEntity(req: express.Request, res: express.Response): Promise<void> {
567
+ try {
568
+ const { ViewID, ViewName } = req.query;
569
+ const user = req['mjUser'];
570
+
571
+ if (!ViewID && !ViewName) {
572
+ res.status(400).json({ error: 'Either ViewID or ViewName must be provided' });
573
+ return;
574
+ }
575
+
576
+ // Placeholder implementation - this would need to be implemented to lookup view metadata
577
+ const entityName = "SampleEntity"; // This would be determined by looking up the view
578
+
579
+ res.json({ entityName });
580
+ } catch (error) {
581
+ LogError(error);
582
+ res.status(500).json({ error: (error as Error)?.message || 'Unknown error' });
583
+ }
584
+ }
585
+
586
+ /**
587
+ * Get metadata for all entities
588
+ */
589
+ private async getEntityMetadata(req: express.Request, res: express.Response): Promise<void> {
590
+ try {
591
+ const user = req['mjUser'];
592
+
593
+ // Filter entities based on user permissions and REST API configuration
594
+ const md = new Metadata();
595
+ const entities = md.Entities.filter(e => {
596
+ // First check if entity is allowed based on configuration
597
+ if (!this.isEntityAllowed(e.Name)) {
598
+ return false;
599
+ }
600
+
601
+ // Then check user permissions
602
+ const permissions = e.GetUserPermisions(user);
603
+ return permissions.CanRead;
604
+ });
605
+
606
+ const result = entities.map(e => ({
607
+ Name: e.Name,
608
+ ClassName: e.ClassName,
609
+ SchemaName: e.SchemaName,
610
+ DisplayName: e.DisplayName,
611
+ Description: e.Description,
612
+ IncludeInAPI: e.IncludeInAPI,
613
+ AllowCreateAPI: e.AllowCreateAPI,
614
+ AllowUpdateAPI: e.AllowUpdateAPI,
615
+ AllowDeleteAPI: e.AllowDeleteAPI
616
+ }));
617
+
618
+ res.json(result);
619
+ } catch (error) {
620
+ LogError(error);
621
+ res.status(500).json({ error: (error as Error)?.message || 'Unknown error' });
622
+ }
623
+ }
624
+
625
+ /**
626
+ * Get field metadata for a specific entity
627
+ */
628
+ private async getEntityFieldMetadata(req: express.Request, res: express.Response): Promise<void> {
629
+ try {
630
+ const { entityName } = req.params;
631
+
632
+ const user = req['mjUser'];
633
+
634
+ const md = new Metadata();
635
+ const entity = md.Entities.find(e => e.Name === entityName);
636
+ if (!entity) {
637
+ res.status(404).json({ error: `Entity '${entityName}' not found` });
638
+ return;
639
+ }
640
+
641
+ // Check if user can read this entity
642
+ const permissions = entity.GetUserPermisions(user);
643
+ if (!permissions.CanRead) {
644
+ res.status(403).json({ error: 'Permission denied' });
645
+ return;
646
+ }
647
+
648
+ const result = entity.Fields.map(f => ({
649
+ Name: f.Name,
650
+ DisplayName: f.DisplayName,
651
+ Description: f.Description,
652
+ Type: f.Type,
653
+ IsRequired: f.AllowsNull === false,
654
+ IsPrimaryKey: f.IsPrimaryKey,
655
+ IsUnique: f.IsUnique,
656
+ MaxLength: f.MaxLength,
657
+ DefaultValue: f.DefaultValue,
658
+ CodeName: f.CodeName,
659
+ TSType: f.TSType
660
+ }));
661
+
662
+ res.json(result);
663
+ } catch (error) {
664
+ LogError(error);
665
+ res.status(500).json({ error: (error as Error)?.message || 'Unknown error' });
666
+ }
667
+ }
668
+
669
+ /**
670
+ * Get metadata about available views for an entity
671
+ */
672
+ private async getViewsMetadata(req: express.Request, res: express.Response): Promise<void> {
673
+ try {
674
+ const { entityName } = req.params;
675
+
676
+ const user = req['mjUser'];
677
+
678
+ // This would need to be implemented to retrieve available views
679
+ // Placeholder implementation
680
+ const views = []; // Would need to query available views for this entity
681
+
682
+ res.json(views);
683
+ } catch (error) {
684
+ LogError(error);
685
+ res.status(500).json({ error: (error as Error)?.message || 'Unknown error' });
686
+ }
687
+ }
688
+
689
+ /**
690
+ * Get favorite status for a record
691
+ */
692
+ private async getRecordFavoriteStatus(req: express.Request, res: express.Response): Promise<void> {
693
+ try {
694
+ const { userId, entityName, id } = req.params;
695
+ const user = req['mjUser'];
696
+
697
+ // Get the entity object
698
+ const md = new Metadata();
699
+ const entity = await md.GetEntityObject(entityName, user);
700
+
701
+ // Create a composite key
702
+ const compositeKey = this.createCompositeKey(entity.EntityInfo, id);
703
+
704
+ // Use a direct approach for getting favorite status
705
+ // Note: This is a simplification. The actual implementation may need to be adjusted
706
+ const isFavorite = false; // This would be populated with the actual favorite status
707
+
708
+ res.json({ isFavorite });
709
+ } catch (error) {
710
+ LogError(error);
711
+ res.status(500).json({ error: (error as Error)?.message || 'Unknown error' });
712
+ }
713
+ }
714
+
715
+ /**
716
+ * Set favorite status for a record
717
+ */
718
+ private async setRecordFavoriteStatus(req: express.Request, res: express.Response): Promise<void> {
719
+ try {
720
+ const { userId, entityName, id } = req.params;
721
+ const user = req['mjUser'];
722
+
723
+ // Get the entity object
724
+ const md = new Metadata();
725
+ const entity = await md.GetEntityObject(entityName, user);
726
+
727
+ // Create a composite key
728
+ const compositeKey = this.createCompositeKey(entity.EntityInfo, id);
729
+
730
+ // Use a direct approach for setting favorite status
731
+ // Note: This is a simplification. The actual implementation may need to be adjusted
732
+ // This would set the favorite status to true
733
+
734
+ res.status(204).send();
735
+ } catch (error) {
736
+ LogError(error);
737
+ res.status(500).json({ error: (error as Error)?.message || 'Unknown error' });
738
+ }
739
+ }
740
+
741
+ /**
742
+ * Remove favorite status for a record
743
+ */
744
+ private async removeRecordFavoriteStatus(req: express.Request, res: express.Response): Promise<void> {
745
+ try {
746
+ const { userId, entityName, id } = req.params;
747
+ const user = req['mjUser'];
748
+
749
+ // Get the entity object
750
+ const md = new Metadata();
751
+ const entity = await md.GetEntityObject(entityName, user);
752
+
753
+ // Create a composite key
754
+ const compositeKey = this.createCompositeKey(entity.EntityInfo, id);
755
+
756
+ // Use a direct approach for setting favorite status
757
+ // Note: This is a simplification. The actual implementation may need to be adjusted
758
+ // This would set the favorite status to false
759
+
760
+ res.status(204).send();
761
+ } catch (error) {
762
+ LogError(error);
763
+ res.status(500).json({ error: (error as Error)?.message || 'Unknown error' });
764
+ }
765
+ }
766
+
767
+ /**
768
+ * Execute a transaction
769
+ */
770
+ private async executeTransaction(req: express.Request, res: express.Response): Promise<void> {
771
+ try {
772
+ // Placeholder implementation - this would need to be implemented to handle transactions
773
+ res.status(501).json({ error: 'Not implemented' });
774
+ } catch (error) {
775
+ LogError(error);
776
+ res.status(500).json({ error: (error as Error)?.message || 'Unknown error' });
777
+ }
778
+ }
779
+
780
+ /**
781
+ * Run a report
782
+ */
783
+ private async runReport(req: express.Request, res: express.Response): Promise<void> {
784
+ try {
785
+ // Placeholder implementation - this would need to be implemented to run reports
786
+ res.status(501).json({ error: 'Not implemented' });
787
+ } catch (error) {
788
+ LogError(error);
789
+ res.status(500).json({ error: (error as Error)?.message || 'Unknown error' });
790
+ }
791
+ }
792
+
793
+ /**
794
+ * Run a query
795
+ */
796
+ private async runQuery(req: express.Request, res: express.Response): Promise<void> {
797
+ try {
798
+ // Placeholder implementation - this would need to be implemented to run queries
799
+ res.status(501).json({ error: 'Not implemented' });
800
+ } catch (error) {
801
+ LogError(error);
802
+ res.status(500).json({ error: (error as Error)?.message || 'Unknown error' });
803
+ }
804
+ }
805
+
806
+ /**
807
+ * Helper method to create a composite key from an ID
808
+ */
809
+ private createCompositeKey(entityInfo: EntityInfo, id: string): CompositeKey {
810
+ if (entityInfo.PrimaryKeys.length === 1) {
811
+ // Single primary key
812
+ const primaryKeyField = entityInfo.PrimaryKeys[0].Name;
813
+ const compositeKey = new CompositeKey();
814
+
815
+ // Use key-value pairs instead of SetValue
816
+ compositeKey.KeyValuePairs = [
817
+ { FieldName: primaryKeyField, Value: id }
818
+ ];
819
+
820
+ return compositeKey;
821
+ } else {
822
+ // Composite primary key
823
+ // This is a simplification - in a real implementation, we would need to handle composite keys properly
824
+ throw new Error('Composite primary keys are not supported in this implementation');
825
+ }
826
+ }
827
+
828
+ /**
829
+ * Get the Express router with all configured routes
830
+ */
831
+ public getRouter(): express.Router {
832
+ return this.router;
833
+ }
834
+ }