@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.
- package/README.md +15 -1
- package/dist/config.d.ts +69 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +11 -1
- package/dist/config.js.map +1 -1
- package/dist/generated/generated.d.ts +15 -12
- package/dist/generated/generated.d.ts.map +1 -1
- package/dist/generated/generated.js +73 -58
- package/dist/generated/generated.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +41 -0
- package/dist/index.js.map +1 -1
- package/dist/resolvers/AskSkipResolver.d.ts +60 -5
- package/dist/resolvers/AskSkipResolver.d.ts.map +1 -1
- package/dist/resolvers/AskSkipResolver.js +587 -31
- package/dist/resolvers/AskSkipResolver.js.map +1 -1
- package/dist/rest/EntityCRUDHandler.d.ts +29 -0
- package/dist/rest/EntityCRUDHandler.d.ts.map +1 -0
- package/dist/rest/EntityCRUDHandler.js +197 -0
- package/dist/rest/EntityCRUDHandler.js.map +1 -0
- package/dist/rest/RESTEndpointHandler.d.ts +41 -0
- package/dist/rest/RESTEndpointHandler.d.ts.map +1 -0
- package/dist/rest/RESTEndpointHandler.js +537 -0
- package/dist/rest/RESTEndpointHandler.js.map +1 -0
- package/dist/rest/ViewOperationsHandler.d.ts +21 -0
- package/dist/rest/ViewOperationsHandler.d.ts.map +1 -0
- package/dist/rest/ViewOperationsHandler.js +144 -0
- package/dist/rest/ViewOperationsHandler.js.map +1 -0
- package/dist/rest/index.d.ts +5 -0
- package/dist/rest/index.d.ts.map +1 -0
- package/dist/rest/index.js +5 -0
- package/dist/rest/index.js.map +1 -0
- package/dist/rest/setupRESTEndpoints.d.ts +12 -0
- package/dist/rest/setupRESTEndpoints.d.ts.map +1 -0
- package/dist/rest/setupRESTEndpoints.js +27 -0
- package/dist/rest/setupRESTEndpoints.js.map +1 -0
- package/dist/scheduler/LearningCycleScheduler.d.ts +44 -0
- package/dist/scheduler/LearningCycleScheduler.d.ts.map +1 -0
- package/dist/scheduler/LearningCycleScheduler.js +188 -0
- package/dist/scheduler/LearningCycleScheduler.js.map +1 -0
- package/package.json +24 -26
- package/src/config.ts +15 -1
- package/src/generated/generated.ts +53 -44
- package/src/index.ts +56 -1
- package/src/resolvers/AskSkipResolver.ts +787 -51
- package/src/rest/EntityCRUDHandler.ts +279 -0
- package/src/rest/RESTEndpointHandler.ts +834 -0
- package/src/rest/ViewOperationsHandler.ts +207 -0
- package/src/rest/index.ts +4 -0
- package/src/rest/setupRESTEndpoints.ts +89 -0
- package/src/scheduler/LearningCycleScheduler.ts +312 -0
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
import {
|
|
2
|
+
LogError, Metadata, RunView, RunViewParams,
|
|
3
|
+
RunViewResult, UserInfo
|
|
4
|
+
} from '@memberjunction/core';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* View Operations Implementation for REST endpoints
|
|
8
|
+
* These functions handle running views through the REST API
|
|
9
|
+
*/
|
|
10
|
+
export class ViewOperationsHandler {
|
|
11
|
+
/**
|
|
12
|
+
* Run a view and return results
|
|
13
|
+
*/
|
|
14
|
+
static async runView(params: RunViewParams, user: UserInfo): Promise<{ success: boolean, result?: RunViewResult, error?: string }> {
|
|
15
|
+
try {
|
|
16
|
+
// Validate entity exists
|
|
17
|
+
const md = new Metadata();
|
|
18
|
+
const entity = md.Entities.find(e => e.Name === params.EntityName);
|
|
19
|
+
if (!entity) {
|
|
20
|
+
return {
|
|
21
|
+
success: false,
|
|
22
|
+
error: `Entity '${params.EntityName}' not found`
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Check read permission
|
|
27
|
+
const permissions = entity.GetUserPermisions(user);
|
|
28
|
+
if (!permissions.CanRead) {
|
|
29
|
+
return {
|
|
30
|
+
success: false,
|
|
31
|
+
error: `User ${user.Name} does not have permission to read ${params.EntityName} records`
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Sanitize and validate parameters
|
|
36
|
+
this.sanitizeRunViewParams(params);
|
|
37
|
+
|
|
38
|
+
// Execute the view
|
|
39
|
+
const runView = new RunView();
|
|
40
|
+
const result = await runView.RunView(params, user);
|
|
41
|
+
|
|
42
|
+
return { success: true, result };
|
|
43
|
+
} catch (error) {
|
|
44
|
+
LogError(error);
|
|
45
|
+
return { success: false, error: (error as Error)?.message || 'Unknown error' };
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Run multiple views in batch
|
|
51
|
+
*/
|
|
52
|
+
static async runViews(paramsArray: RunViewParams[], user: UserInfo): Promise<{ success: boolean, results?: RunViewResult[], error?: string }> {
|
|
53
|
+
try {
|
|
54
|
+
// Validate and sanitize each set of parameters
|
|
55
|
+
const md = new Metadata();
|
|
56
|
+
for (const params of paramsArray) {
|
|
57
|
+
// Validate entity exists
|
|
58
|
+
const entity = md.Entities.find(e => e.Name === params.EntityName);
|
|
59
|
+
if (!entity) {
|
|
60
|
+
return {
|
|
61
|
+
success: false,
|
|
62
|
+
error: `Entity '${params.EntityName}' not found`
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Check read permission
|
|
67
|
+
const permissions = entity.GetUserPermisions(user);
|
|
68
|
+
if (!permissions.CanRead) {
|
|
69
|
+
return {
|
|
70
|
+
success: false,
|
|
71
|
+
error: `User ${user.Name} does not have permission to read ${params.EntityName} records`
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Sanitize parameters
|
|
76
|
+
this.sanitizeRunViewParams(params);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Execute the views
|
|
80
|
+
const runView = new RunView();
|
|
81
|
+
const results = await runView.RunViews(paramsArray, user);
|
|
82
|
+
|
|
83
|
+
return { success: true, results };
|
|
84
|
+
} catch (error) {
|
|
85
|
+
LogError(error);
|
|
86
|
+
return { success: false, error: (error as Error)?.message || 'Unknown error' };
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* List entities with optional filtering
|
|
92
|
+
*/
|
|
93
|
+
static async listEntities(params: RunViewParams, user: UserInfo): Promise<RunViewResult> {
|
|
94
|
+
try {
|
|
95
|
+
// Check entity exists and user has permission
|
|
96
|
+
const md = new Metadata();
|
|
97
|
+
const entity = md.Entities.find(e => e.Name === params.EntityName);
|
|
98
|
+
if (!entity) {
|
|
99
|
+
throw new Error(`Entity '${params.EntityName}' not found`);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const permissions = entity.GetUserPermisions(user);
|
|
103
|
+
if (!permissions.CanRead) {
|
|
104
|
+
throw new Error(`User ${user.Name} does not have permission to read ${params.EntityName} records`);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Sanitize and validate parameters
|
|
108
|
+
this.sanitizeRunViewParams(params);
|
|
109
|
+
|
|
110
|
+
// Execute the view
|
|
111
|
+
const runView = new RunView();
|
|
112
|
+
return await runView.RunView(params, user);
|
|
113
|
+
} catch (error) {
|
|
114
|
+
LogError(error);
|
|
115
|
+
throw error;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Get available views for an entity
|
|
121
|
+
*/
|
|
122
|
+
static async getEntityViews(entityName: string, user: UserInfo): Promise<{ success: boolean, views?: any[], error?: string }> {
|
|
123
|
+
try {
|
|
124
|
+
// Validate entity exists
|
|
125
|
+
const md = new Metadata();
|
|
126
|
+
const entity = md.Entities.find(e => e.Name === entityName);
|
|
127
|
+
if (!entity) {
|
|
128
|
+
return {
|
|
129
|
+
success: false,
|
|
130
|
+
error: `Entity '${entityName}' not found`
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Check read permission
|
|
135
|
+
const permissions = entity.GetUserPermisions(user);
|
|
136
|
+
if (!permissions.CanRead) {
|
|
137
|
+
return {
|
|
138
|
+
success: false,
|
|
139
|
+
error: `User ${user.Name} does not have permission to read ${entityName} records`
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Run a view to get the available views
|
|
144
|
+
const params: RunViewParams = {
|
|
145
|
+
EntityName: 'User Views',
|
|
146
|
+
ExtraFilter: `Entity = '${entityName}'`
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
const runView = new RunView();
|
|
150
|
+
const result = await runView.RunView(params, user);
|
|
151
|
+
|
|
152
|
+
if (!result.Success) {
|
|
153
|
+
return {
|
|
154
|
+
success: false,
|
|
155
|
+
error: result.ErrorMessage || 'Failed to retrieve views'
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Format the view data
|
|
160
|
+
const views = result.Results.map(view => ({
|
|
161
|
+
ID: view.ID,
|
|
162
|
+
Name: view.Name,
|
|
163
|
+
Description: view.Description,
|
|
164
|
+
IsShared: view.IsShared,
|
|
165
|
+
CreatedAt: view.CreatedAt
|
|
166
|
+
}));
|
|
167
|
+
|
|
168
|
+
return { success: true, views };
|
|
169
|
+
} catch (error) {
|
|
170
|
+
LogError(error);
|
|
171
|
+
return { success: false, error: (error as Error)?.message || 'Unknown error' };
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Sanitize and validate RunViewParams
|
|
177
|
+
*/
|
|
178
|
+
private static sanitizeRunViewParams(params: RunViewParams): void {
|
|
179
|
+
// Ensure EntityName is provided
|
|
180
|
+
if (!params.EntityName) {
|
|
181
|
+
throw new Error('EntityName is required');
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Convert string arrays if they came in as comma-separated strings
|
|
185
|
+
if (params.Fields && typeof params.Fields === 'string') {
|
|
186
|
+
params.Fields = (params.Fields as string).split(',');
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Sanitize numeric values
|
|
190
|
+
if (params.MaxRows !== undefined) {
|
|
191
|
+
params.MaxRows = typeof params.MaxRows === 'string'
|
|
192
|
+
? parseInt(params.MaxRows as string)
|
|
193
|
+
: params.MaxRows;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
if (params.StartRow !== undefined) {
|
|
197
|
+
params.StartRow = typeof params.StartRow === 'string'
|
|
198
|
+
? parseInt(params.StartRow as string)
|
|
199
|
+
: params.StartRow;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// Default ResultType if not provided
|
|
203
|
+
if (!params.ResultType) {
|
|
204
|
+
params.ResultType = 'simple';
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import express from 'express';
|
|
2
|
+
import { RESTEndpointHandler } from './RESTEndpointHandler.js';
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
export const ___REST_API_BASE_PATH = '/api/v1';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Configuration options for REST API endpoints
|
|
9
|
+
*/
|
|
10
|
+
export interface RESTApiOptions {
|
|
11
|
+
/**
|
|
12
|
+
* Whether to enable REST API endpoints (default: true)
|
|
13
|
+
*/
|
|
14
|
+
enabled: boolean;
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Array of entity names to include in the API (case-insensitive)
|
|
18
|
+
* If provided, only these entities will be accessible through the REST API
|
|
19
|
+
* Supports wildcards using '*' (e.g., 'User*' matches 'User', 'UserRole', etc.)
|
|
20
|
+
*/
|
|
21
|
+
includeEntities?: string[];
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Array of entity names to exclude from the API (case-insensitive)
|
|
25
|
+
* These entities will not be accessible through the REST API
|
|
26
|
+
* Supports wildcards using '*' (e.g., 'Secret*' matches 'Secret', 'SecretKey', etc.)
|
|
27
|
+
* Note: Exclude patterns always override include patterns
|
|
28
|
+
*/
|
|
29
|
+
excludeEntities?: string[];
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Array of schema names to include in the API (case-insensitive)
|
|
33
|
+
* If provided, only entities in these schemas will be accessible through the REST API
|
|
34
|
+
*/
|
|
35
|
+
includeSchemas?: string[];
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Array of schema names to exclude from the API (case-insensitive)
|
|
39
|
+
* Entities in these schemas will not be accessible through the REST API
|
|
40
|
+
* Note: Exclude patterns always override include patterns
|
|
41
|
+
*/
|
|
42
|
+
excludeSchemas?: string[];
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Default REST API configuration
|
|
47
|
+
*/
|
|
48
|
+
export const DEFAULT_REST_API_OPTIONS: RESTApiOptions = {
|
|
49
|
+
enabled: true,
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Adds the REST API endpoints to an existing Express application
|
|
54
|
+
* @param app The Express application to add the endpoints to
|
|
55
|
+
* @param options Configuration options for REST API
|
|
56
|
+
* @param authMiddleware Optional authentication middleware to use
|
|
57
|
+
*/
|
|
58
|
+
export function setupRESTEndpoints(
|
|
59
|
+
app: express.Application,
|
|
60
|
+
options?: Partial<RESTApiOptions>,
|
|
61
|
+
authMiddleware?: express.RequestHandler
|
|
62
|
+
): void {
|
|
63
|
+
// Merge with default options
|
|
64
|
+
const config = { ...DEFAULT_REST_API_OPTIONS, ...options };
|
|
65
|
+
|
|
66
|
+
// Skip setup if REST API is disabled
|
|
67
|
+
if (!config.enabled) {
|
|
68
|
+
console.log('REST API endpoints are disabled');
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Create REST endpoint handler with entity and schema filters
|
|
73
|
+
const restHandler = new RESTEndpointHandler({
|
|
74
|
+
includeEntities: config.includeEntities ? config.includeEntities.map(e => e.toLowerCase()) : undefined,
|
|
75
|
+
excludeEntities: config.excludeEntities ? config.excludeEntities.map(e => e.toLowerCase()) : undefined,
|
|
76
|
+
includeSchemas: config.includeSchemas ? config.includeSchemas.map(s => s.toLowerCase()) : undefined,
|
|
77
|
+
excludeSchemas: config.excludeSchemas ? config.excludeSchemas.map(s => s.toLowerCase()) : undefined
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
// Mount REST API at the specified base path with authentication
|
|
81
|
+
const basePath = ___REST_API_BASE_PATH;
|
|
82
|
+
if (authMiddleware) {
|
|
83
|
+
app.use(basePath, authMiddleware, restHandler.getRouter());
|
|
84
|
+
} else {
|
|
85
|
+
app.use(basePath, restHandler.getRouter());
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
console.log(`REST API endpoints have been set up at ${basePath}`);
|
|
89
|
+
}
|
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
import { LogStatus, LogError } from '@memberjunction/core';
|
|
2
|
+
import { UserCache } from '@memberjunction/sqlserver-dataprovider';
|
|
3
|
+
import { GetReadWriteDataSource } from '../util.js';
|
|
4
|
+
import { AskSkipResolver } from '../resolvers/AskSkipResolver.js';
|
|
5
|
+
import { DataSourceInfo } from '../types.js';
|
|
6
|
+
import { getSystemUser } from '../auth/index.js';
|
|
7
|
+
import { BaseSingleton } from '@memberjunction/global';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* A simple scheduler for the Skip AI learning cycle
|
|
11
|
+
* Implements BaseSingleton pattern for cross-instance synchronization
|
|
12
|
+
*/
|
|
13
|
+
export class LearningCycleScheduler extends BaseSingleton<LearningCycleScheduler> {
|
|
14
|
+
private intervalId: NodeJS.Timeout | null = null;
|
|
15
|
+
|
|
16
|
+
// Track executions by organization ID instead of a global flag
|
|
17
|
+
private runningOrganizations: Map<string, { startTime: Date, learningCycleId: string }> = new Map();
|
|
18
|
+
|
|
19
|
+
private lastRunTime: Date | null = null;
|
|
20
|
+
private dataSources: DataSourceInfo[] = [];
|
|
21
|
+
|
|
22
|
+
// Protected constructor to enforce singleton pattern via BaseSingleton
|
|
23
|
+
protected constructor() {
|
|
24
|
+
super();
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
public static get Instance(): LearningCycleScheduler {
|
|
28
|
+
return super.getInstance<LearningCycleScheduler>();
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Set the data sources for the scheduler
|
|
33
|
+
* @param dataSources Array of data sources
|
|
34
|
+
*/
|
|
35
|
+
public setDataSources(dataSources: DataSourceInfo[]): void {
|
|
36
|
+
this.dataSources = dataSources;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Start the scheduler with the specified interval in minutes
|
|
41
|
+
* @param intervalMinutes The interval in minutes between runs
|
|
42
|
+
*/
|
|
43
|
+
public start(intervalMinutes: number = 60): void {
|
|
44
|
+
|
|
45
|
+
// start learning cycle immediately upon the server start
|
|
46
|
+
this.runLearningCycle()
|
|
47
|
+
.catch(error => LogError(`Error in initial learning cycle: ${error}`));
|
|
48
|
+
|
|
49
|
+
const intervalMs = intervalMinutes * 60 * 1000;
|
|
50
|
+
|
|
51
|
+
LogStatus(`Starting learning cycle scheduler with interval of ${intervalMinutes} minutes`);
|
|
52
|
+
|
|
53
|
+
// Schedule the recurring task
|
|
54
|
+
this.intervalId = setInterval(() => {
|
|
55
|
+
this.runLearningCycle()
|
|
56
|
+
.catch(error => LogError(`Error in scheduled learning cycle: ${error}`));
|
|
57
|
+
}, intervalMs);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Stop the scheduler
|
|
62
|
+
*/
|
|
63
|
+
public stop(): void {
|
|
64
|
+
if (this.intervalId) {
|
|
65
|
+
clearInterval(this.intervalId);
|
|
66
|
+
this.intervalId = null;
|
|
67
|
+
LogStatus('Learning cycle scheduler stopped');
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Run the learning cycle if it's not already running
|
|
73
|
+
* @returns A promise that resolves when the learning cycle completes
|
|
74
|
+
*/
|
|
75
|
+
public async runLearningCycle(): Promise<boolean> {
|
|
76
|
+
const startTime = new Date();
|
|
77
|
+
|
|
78
|
+
try {
|
|
79
|
+
LogStatus('Starting scheduled learning cycle execution');
|
|
80
|
+
|
|
81
|
+
// Make sure we have data sources
|
|
82
|
+
if (!this.dataSources || this.dataSources.length === 0) {
|
|
83
|
+
throw new Error('No data sources available for the learning cycle');
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const dataSource = GetReadWriteDataSource(this.dataSources);
|
|
87
|
+
|
|
88
|
+
// Get system user for operation
|
|
89
|
+
const systemUser = await getSystemUser(dataSource);
|
|
90
|
+
if (!systemUser) {
|
|
91
|
+
throw new Error('System user not found');
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Create context for the resolver
|
|
95
|
+
const context = {
|
|
96
|
+
dataSource: dataSource,
|
|
97
|
+
dataSources: this.dataSources,
|
|
98
|
+
userPayload: {
|
|
99
|
+
email: systemUser.Email,
|
|
100
|
+
sessionId: `scheduler_${Date.now()}`,
|
|
101
|
+
userRecord: systemUser,
|
|
102
|
+
isSystemUser: true
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
// Execute the learning cycle
|
|
107
|
+
const skipResolver = new AskSkipResolver();
|
|
108
|
+
const result = await skipResolver.ExecuteAskSkipLearningCycle(
|
|
109
|
+
context,
|
|
110
|
+
false // forceEntityRefresh
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
const endTime = new Date();
|
|
114
|
+
const elapsedMs = endTime.getTime() - startTime.getTime();
|
|
115
|
+
|
|
116
|
+
this.lastRunTime = startTime;
|
|
117
|
+
|
|
118
|
+
if (result.success) {
|
|
119
|
+
LogStatus(`Learning cycle completed successfully in ${elapsedMs}ms`);
|
|
120
|
+
return true;
|
|
121
|
+
} else {
|
|
122
|
+
LogError(`Learning cycle failed after ${elapsedMs}ms: ${result.error}`);
|
|
123
|
+
return false;
|
|
124
|
+
}
|
|
125
|
+
} catch (error) {
|
|
126
|
+
LogError(`Error executing learning cycle: ${error}`);
|
|
127
|
+
return false;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Get the current status of the scheduler
|
|
133
|
+
*/
|
|
134
|
+
public getStatus() {
|
|
135
|
+
return {
|
|
136
|
+
isSchedulerRunning: this.intervalId !== null,
|
|
137
|
+
lastRunTime: this.lastRunTime,
|
|
138
|
+
runningOrganizations: Array.from(this.runningOrganizations.entries()).map(([orgId, info]) => ({
|
|
139
|
+
organizationId: orgId,
|
|
140
|
+
learningCycleId: info.learningCycleId,
|
|
141
|
+
startTime: info.startTime,
|
|
142
|
+
runningForMinutes: (new Date().getTime() - info.startTime.getTime()) / (1000 * 60)
|
|
143
|
+
}))
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Checks if an organization is currently running a learning cycle
|
|
149
|
+
* @param organizationId The organization ID to check
|
|
150
|
+
* @returns Whether the organization is running a cycle and details if running
|
|
151
|
+
*/
|
|
152
|
+
public isOrganizationRunningCycle(
|
|
153
|
+
organizationId: string
|
|
154
|
+
): { isRunning: boolean, startTime?: Date, learningCycleId?: string, runningForMinutes?: number } {
|
|
155
|
+
const runningInfo = this.runningOrganizations.get(organizationId);
|
|
156
|
+
|
|
157
|
+
if (runningInfo) {
|
|
158
|
+
// Check if it's been running too long and should be considered stalled
|
|
159
|
+
const now = new Date();
|
|
160
|
+
const elapsedMinutes = (now.getTime() - runningInfo.startTime.getTime()) / (1000 * 60);
|
|
161
|
+
|
|
162
|
+
return {
|
|
163
|
+
isRunning: true,
|
|
164
|
+
startTime: runningInfo.startTime,
|
|
165
|
+
learningCycleId: runningInfo.learningCycleId,
|
|
166
|
+
runningForMinutes: elapsedMinutes
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
return { isRunning: false };
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Registers an organization as running a learning cycle
|
|
175
|
+
* @param organizationId The organization ID to register
|
|
176
|
+
* @param learningCycleId The ID of the learning cycle
|
|
177
|
+
* @returns true if successfully registered, false if already running
|
|
178
|
+
*/
|
|
179
|
+
public registerRunningCycle(organizationId: string, learningCycleId: string): boolean {
|
|
180
|
+
// First check if already running
|
|
181
|
+
const { isRunning } = this.isOrganizationRunningCycle(organizationId);
|
|
182
|
+
|
|
183
|
+
if (isRunning) {
|
|
184
|
+
return false;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Register the organization as running a cycle
|
|
188
|
+
this.runningOrganizations.set(organizationId, {
|
|
189
|
+
startTime: new Date(),
|
|
190
|
+
learningCycleId
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
return true;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Unregisters an organization after its learning cycle completes
|
|
198
|
+
* @param organizationId The organization ID to unregister
|
|
199
|
+
* @returns true if successfully unregistered, false if wasn't registered
|
|
200
|
+
*/
|
|
201
|
+
public unregisterRunningCycle(organizationId: string): boolean {
|
|
202
|
+
if (this.runningOrganizations.has(organizationId)) {
|
|
203
|
+
this.runningOrganizations.delete(organizationId);
|
|
204
|
+
return true;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
return false;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Manually execute a learning cycle run for testing purposes
|
|
212
|
+
* This is intended for debugging/testing only and will force a run
|
|
213
|
+
* even if the scheduler is not started
|
|
214
|
+
* @param organizationId Optional organization ID to register for the manual run
|
|
215
|
+
* @returns A promise that resolves when the learning cycle completes
|
|
216
|
+
*/
|
|
217
|
+
public async manuallyExecuteLearningCycle(organizationId?: string): Promise<boolean> {
|
|
218
|
+
try {
|
|
219
|
+
LogStatus('🧪 Manually executing learning cycle for testing...');
|
|
220
|
+
|
|
221
|
+
// If an organization ID is provided, register it as running
|
|
222
|
+
const learningCycleId = `manual_${Date.now()}`;
|
|
223
|
+
let orgRegistered = false;
|
|
224
|
+
|
|
225
|
+
if (organizationId) {
|
|
226
|
+
// Check if already running
|
|
227
|
+
const runningStatus = this.isOrganizationRunningCycle(organizationId);
|
|
228
|
+
|
|
229
|
+
if (runningStatus.isRunning) {
|
|
230
|
+
LogError(`Organization ${organizationId} is already running a learning cycle. Cannot start a new one.`);
|
|
231
|
+
return false;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// Register this organization
|
|
235
|
+
orgRegistered = this.registerRunningCycle(organizationId, learningCycleId);
|
|
236
|
+
if (!orgRegistered) {
|
|
237
|
+
LogError(`Failed to register organization ${organizationId} for manual learning cycle execution`);
|
|
238
|
+
return false;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// Run the learning cycle
|
|
243
|
+
const result = await this.runLearningCycle();
|
|
244
|
+
LogStatus(`🧪 Manual learning cycle execution completed with result: ${result ? 'Success' : 'Failed'}`);
|
|
245
|
+
|
|
246
|
+
// Unregister the organization if it was registered
|
|
247
|
+
if (organizationId && orgRegistered) {
|
|
248
|
+
this.unregisterRunningCycle(organizationId);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
return result;
|
|
252
|
+
} catch (error) {
|
|
253
|
+
// Make sure to unregister on error
|
|
254
|
+
if (organizationId && this.runningOrganizations.has(organizationId)) {
|
|
255
|
+
this.unregisterRunningCycle(organizationId);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
LogError(`Error in manual learning cycle execution: ${error}`);
|
|
259
|
+
return false;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Force stop a running learning cycle for an organization
|
|
265
|
+
* @param organizationId The organization ID to stop the cycle for
|
|
266
|
+
* @returns Information about the stopped cycle
|
|
267
|
+
*/
|
|
268
|
+
public stopLearningCycleForOrganization(organizationId: string): {
|
|
269
|
+
success: boolean,
|
|
270
|
+
message: string,
|
|
271
|
+
wasRunning: boolean,
|
|
272
|
+
cycleDetails?: { learningCycleId: string, startTime: Date, runningForMinutes: number }
|
|
273
|
+
} {
|
|
274
|
+
// Check if this organization has a running cycle
|
|
275
|
+
const runningStatus = this.isOrganizationRunningCycle(organizationId);
|
|
276
|
+
|
|
277
|
+
if (!runningStatus.isRunning) {
|
|
278
|
+
return {
|
|
279
|
+
success: false,
|
|
280
|
+
message: `No running learning cycle found for organization ${organizationId}`,
|
|
281
|
+
wasRunning: false
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// Capture details before unregistering
|
|
286
|
+
const startTime = runningStatus.startTime!;
|
|
287
|
+
const learningCycleId = runningStatus.learningCycleId!;
|
|
288
|
+
const runningForMinutes = runningStatus.runningForMinutes!;
|
|
289
|
+
|
|
290
|
+
// Unregister the organization
|
|
291
|
+
const unregistered = this.unregisterRunningCycle(organizationId);
|
|
292
|
+
|
|
293
|
+
if (unregistered) {
|
|
294
|
+
return {
|
|
295
|
+
success: true,
|
|
296
|
+
message: `Successfully stopped learning cycle for organization ${organizationId}`,
|
|
297
|
+
wasRunning: true,
|
|
298
|
+
cycleDetails: {
|
|
299
|
+
learningCycleId,
|
|
300
|
+
startTime,
|
|
301
|
+
runningForMinutes
|
|
302
|
+
}
|
|
303
|
+
};
|
|
304
|
+
} else {
|
|
305
|
+
return {
|
|
306
|
+
success: false,
|
|
307
|
+
message: `Failed to stop learning cycle for organization ${organizationId}`,
|
|
308
|
+
wasRunning: true
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
}
|