@memberjunction/server 2.94.0 → 2.96.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/dist/config.d.ts +5 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +2 -1
- package/dist/config.js.map +1 -1
- package/dist/context.d.ts.map +1 -1
- package/dist/context.js +4 -0
- package/dist/context.js.map +1 -1
- package/dist/resolvers/ActionResolver.d.ts.map +1 -1
- package/dist/resolvers/ActionResolver.js +5 -4
- package/dist/resolvers/ActionResolver.js.map +1 -1
- package/dist/resolvers/AskSkipResolver.d.ts +8 -3
- package/dist/resolvers/AskSkipResolver.d.ts.map +1 -1
- package/dist/resolvers/AskSkipResolver.js +241 -25
- package/dist/resolvers/AskSkipResolver.js.map +1 -1
- package/dist/resolvers/CreateQueryResolver.d.ts +2 -2
- package/dist/resolvers/CreateQueryResolver.d.ts.map +1 -1
- package/dist/resolvers/CreateQueryResolver.js +25 -17
- package/dist/resolvers/CreateQueryResolver.js.map +1 -1
- package/dist/resolvers/DatasetResolver.d.ts +2 -2
- package/dist/resolvers/DatasetResolver.d.ts.map +1 -1
- package/dist/resolvers/DatasetResolver.js +6 -5
- package/dist/resolvers/DatasetResolver.js.map +1 -1
- package/dist/resolvers/EntityRecordNameResolver.d.ts +4 -4
- package/dist/resolvers/EntityRecordNameResolver.d.ts.map +1 -1
- package/dist/resolvers/EntityRecordNameResolver.js +6 -5
- package/dist/resolvers/EntityRecordNameResolver.js.map +1 -1
- package/dist/resolvers/FileCategoryResolver.d.ts.map +1 -1
- package/dist/resolvers/FileCategoryResolver.js +10 -11
- package/dist/resolvers/FileCategoryResolver.js.map +1 -1
- package/dist/resolvers/FileResolver.d.ts.map +1 -1
- package/dist/resolvers/FileResolver.js +5 -6
- package/dist/resolvers/FileResolver.js.map +1 -1
- package/dist/resolvers/GetDataContextDataResolver.js +2 -2
- package/dist/resolvers/GetDataContextDataResolver.js.map +1 -1
- package/dist/resolvers/GetDataResolver.js +3 -3
- package/dist/resolvers/GetDataResolver.js.map +1 -1
- package/dist/resolvers/MergeRecordsResolver.d.ts +3 -3
- package/dist/resolvers/MergeRecordsResolver.d.ts.map +1 -1
- package/dist/resolvers/MergeRecordsResolver.js +8 -7
- package/dist/resolvers/MergeRecordsResolver.js.map +1 -1
- package/dist/resolvers/PotentialDuplicateRecordResolver.d.ts +1 -1
- package/dist/resolvers/PotentialDuplicateRecordResolver.d.ts.map +1 -1
- package/dist/resolvers/PotentialDuplicateRecordResolver.js +4 -3
- package/dist/resolvers/PotentialDuplicateRecordResolver.js.map +1 -1
- package/dist/resolvers/QueryResolver.d.ts.map +1 -1
- package/dist/resolvers/QueryResolver.js +19 -14
- package/dist/resolvers/QueryResolver.js.map +1 -1
- package/dist/resolvers/ReportResolver.d.ts +2 -2
- package/dist/resolvers/ReportResolver.d.ts.map +1 -1
- package/dist/resolvers/ReportResolver.js +8 -6
- package/dist/resolvers/ReportResolver.js.map +1 -1
- package/dist/resolvers/RunAIAgentResolver.d.ts +3 -7
- package/dist/resolvers/RunAIAgentResolver.d.ts.map +1 -1
- package/dist/resolvers/RunAIAgentResolver.js +8 -5
- package/dist/resolvers/RunAIAgentResolver.js.map +1 -1
- package/dist/resolvers/RunAIPromptResolver.d.ts +3 -7
- package/dist/resolvers/RunAIPromptResolver.d.ts.map +1 -1
- package/dist/resolvers/RunAIPromptResolver.js +10 -8
- package/dist/resolvers/RunAIPromptResolver.js.map +1 -1
- package/dist/resolvers/RunTemplateResolver.d.ts +2 -4
- package/dist/resolvers/RunTemplateResolver.d.ts.map +1 -1
- package/dist/resolvers/RunTemplateResolver.js +5 -4
- package/dist/resolvers/RunTemplateResolver.js.map +1 -1
- package/dist/resolvers/SqlLoggingConfigResolver.d.ts.map +1 -1
- package/dist/resolvers/SqlLoggingConfigResolver.js +7 -7
- package/dist/resolvers/SqlLoggingConfigResolver.js.map +1 -1
- package/dist/resolvers/UserFavoriteResolver.d.ts +3 -4
- package/dist/resolvers/UserFavoriteResolver.d.ts.map +1 -1
- package/dist/resolvers/UserFavoriteResolver.js +10 -68
- package/dist/resolvers/UserFavoriteResolver.js.map +1 -1
- package/dist/resolvers/UserViewResolver.d.ts +1 -1
- package/dist/resolvers/UserViewResolver.d.ts.map +1 -1
- package/dist/resolvers/UserViewResolver.js +3 -4
- package/dist/resolvers/UserViewResolver.js.map +1 -1
- package/package.json +39 -39
- package/src/config.ts +2 -0
- package/src/context.ts +5 -0
- package/src/resolvers/ActionResolver.ts +5 -4
- package/src/resolvers/AskSkipResolver.ts +300 -29
- package/src/resolvers/CreateQueryResolver.ts +28 -17
- package/src/resolvers/DatasetResolver.ts +5 -4
- package/src/resolvers/EntityRecordNameResolver.ts +8 -6
- package/src/resolvers/FileCategoryResolver.ts +9 -10
- package/src/resolvers/FileResolver.ts +6 -7
- package/src/resolvers/GetDataContextDataResolver.ts +2 -2
- package/src/resolvers/GetDataResolver.ts +2 -2
- package/src/resolvers/InfoResolver.ts +1 -1
- package/src/resolvers/MergeRecordsResolver.ts +7 -6
- package/src/resolvers/PotentialDuplicateRecordResolver.ts +3 -2
- package/src/resolvers/QueryResolver.ts +22 -15
- package/src/resolvers/ReportResolver.ts +9 -6
- package/src/resolvers/RunAIAgentResolver.ts +12 -4
- package/src/resolvers/RunAIPromptResolver.ts +12 -8
- package/src/resolvers/RunTemplateResolver.ts +5 -5
- package/src/resolvers/SqlLoggingConfigResolver.ts +7 -6
- package/src/resolvers/UserFavoriteResolver.ts +8 -67
- package/src/resolvers/UserViewResolver.ts +3 -3
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Arg, Ctx, Field, Mutation, ObjectType, PubSub, PubSubEngine, Query, Resolver } from 'type-graphql';
|
|
2
|
-
import { LogError, LogStatus, Metadata, RunView, UserInfo, CompositeKey, EntityFieldInfo, EntityInfo, EntityRelationshipInfo, EntitySaveOptions, EntityDeleteOptions } from '@memberjunction/core';
|
|
2
|
+
import { LogError, LogStatus, Metadata, RunView, UserInfo, CompositeKey, EntityFieldInfo, EntityInfo, EntityRelationshipInfo, EntitySaveOptions, EntityDeleteOptions, IMetadataProvider } from '@memberjunction/core';
|
|
3
3
|
import { AppContext, UserPayload, MJ_SERVER_EVENT_CODE } from '../types.js';
|
|
4
4
|
import { BehaviorSubject } from 'rxjs';
|
|
5
5
|
import { take } from 'rxjs/operators';
|
|
@@ -54,18 +54,145 @@ import {
|
|
|
54
54
|
UserNotificationEntity,
|
|
55
55
|
AIAgentEntityExtended
|
|
56
56
|
} from '@memberjunction/core-entities';
|
|
57
|
-
import { apiKey, baseUrl, configInfo, graphqlPort, mj_core_schema } from '../config.js';
|
|
57
|
+
import { apiKey, baseUrl, publicUrl, configInfo, graphqlPort, graphqlRootPath, mj_core_schema } from '../config.js';
|
|
58
58
|
import mssql from 'mssql';
|
|
59
59
|
|
|
60
60
|
import { registerEnumType } from 'type-graphql';
|
|
61
61
|
import { MJGlobal, CopyScalarsAndArrays } from '@memberjunction/global';
|
|
62
|
-
import { sendPostRequest } from '../util.js';
|
|
62
|
+
import { GetReadWriteProvider, sendPostRequest } from '../util.js';
|
|
63
63
|
import { GetAIAPIKey } from '@memberjunction/ai';
|
|
64
64
|
import { CompositeKeyInputType } from '../generic/KeyInputOutputTypes.js';
|
|
65
65
|
import { AIEngine } from '@memberjunction/aiengine';
|
|
66
66
|
import { deleteAccessToken, GetDataAccessToken, registerAccessToken, tokenExists } from './GetDataResolver.js';
|
|
67
67
|
import e from 'express';
|
|
68
68
|
|
|
69
|
+
/**
|
|
70
|
+
* Store for active conversation streams
|
|
71
|
+
* Maps conversationID to the last status message received
|
|
72
|
+
*/
|
|
73
|
+
class ActiveConversationStreams {
|
|
74
|
+
private static instance: ActiveConversationStreams;
|
|
75
|
+
private streams: Map<string, {
|
|
76
|
+
lastStatus: string,
|
|
77
|
+
lastUpdate: Date,
|
|
78
|
+
startTime: Date, // When processing actually started
|
|
79
|
+
sessionIds: Set<string> // Track which sessions are listening
|
|
80
|
+
}> = new Map();
|
|
81
|
+
|
|
82
|
+
private constructor() {}
|
|
83
|
+
|
|
84
|
+
static getInstance(): ActiveConversationStreams {
|
|
85
|
+
if (!ActiveConversationStreams.instance) {
|
|
86
|
+
ActiveConversationStreams.instance = new ActiveConversationStreams();
|
|
87
|
+
}
|
|
88
|
+
return ActiveConversationStreams.instance;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
updateStatus(conversationId: string, status: string, sessionId?: string) {
|
|
92
|
+
const existing = this.streams.get(conversationId);
|
|
93
|
+
if (existing) {
|
|
94
|
+
existing.lastStatus = status;
|
|
95
|
+
existing.lastUpdate = new Date();
|
|
96
|
+
if (sessionId) {
|
|
97
|
+
existing.sessionIds.add(sessionId);
|
|
98
|
+
}
|
|
99
|
+
} else {
|
|
100
|
+
const now = new Date();
|
|
101
|
+
this.streams.set(conversationId, {
|
|
102
|
+
lastStatus: status,
|
|
103
|
+
lastUpdate: now,
|
|
104
|
+
startTime: now, // Track when processing started
|
|
105
|
+
sessionIds: sessionId ? new Set([sessionId]) : new Set()
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
getStatus(conversationId: string): string | null {
|
|
111
|
+
const stream = this.streams.get(conversationId);
|
|
112
|
+
return stream ? stream.lastStatus : null;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
getStartTime(conversationId: string): Date | null {
|
|
116
|
+
const stream = this.streams.get(conversationId);
|
|
117
|
+
return stream ? stream.startTime : null;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
addSession(conversationId: string, sessionId: string) {
|
|
121
|
+
const stream = this.streams.get(conversationId);
|
|
122
|
+
if (stream) {
|
|
123
|
+
stream.sessionIds.add(sessionId);
|
|
124
|
+
} else {
|
|
125
|
+
// If no stream exists yet, create one with default status
|
|
126
|
+
const now = new Date();
|
|
127
|
+
this.streams.set(conversationId, {
|
|
128
|
+
lastStatus: 'Processing...',
|
|
129
|
+
lastUpdate: now,
|
|
130
|
+
startTime: now, // Track when processing started
|
|
131
|
+
sessionIds: new Set([sessionId])
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
removeConversation(conversationId: string) {
|
|
137
|
+
this.streams.delete(conversationId);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
isActive(conversationId: string): boolean {
|
|
141
|
+
const stream = this.streams.get(conversationId);
|
|
142
|
+
if (!stream) return false;
|
|
143
|
+
|
|
144
|
+
// Consider a stream inactive if no update in last 5 minutes
|
|
145
|
+
const fiveMinutesAgo = new Date(Date.now() - 5 * 60 * 1000);
|
|
146
|
+
return stream.lastUpdate > fiveMinutesAgo;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
getSessionIds(conversationId: string): string[] {
|
|
150
|
+
const stream = this.streams.get(conversationId);
|
|
151
|
+
return stream ? Array.from(stream.sessionIds) : [];
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Clean up stale streams that haven't been updated in a while
|
|
156
|
+
* This prevents memory leaks from abandoned conversations
|
|
157
|
+
*/
|
|
158
|
+
cleanupStaleStreams() {
|
|
159
|
+
const now = new Date();
|
|
160
|
+
const staleThreshold = new Date(now.getTime() - 30 * 60 * 1000); // 30 minutes
|
|
161
|
+
|
|
162
|
+
const staleConversations: string[] = [];
|
|
163
|
+
this.streams.forEach((stream, conversationId) => {
|
|
164
|
+
if (stream.lastUpdate < staleThreshold) {
|
|
165
|
+
staleConversations.push(conversationId);
|
|
166
|
+
}
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
staleConversations.forEach(conversationId => {
|
|
170
|
+
this.streams.delete(conversationId);
|
|
171
|
+
LogStatus(`Cleaned up stale stream for conversation ${conversationId}`);
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
if (staleConversations.length > 0) {
|
|
175
|
+
LogStatus(`Cleaned up ${staleConversations.length} stale conversation streams`);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const activeStreams = ActiveConversationStreams.getInstance();
|
|
181
|
+
|
|
182
|
+
// Set up periodic cleanup of stale streams (every 10 minutes)
|
|
183
|
+
setInterval(() => {
|
|
184
|
+
activeStreams.cleanupStaleStreams();
|
|
185
|
+
}, 10 * 60 * 1000);
|
|
186
|
+
|
|
187
|
+
@ObjectType()
|
|
188
|
+
class ReattachConversationResponse {
|
|
189
|
+
@Field(() => String, { nullable: true })
|
|
190
|
+
lastStatusMessage?: string;
|
|
191
|
+
|
|
192
|
+
@Field(() => Date, { nullable: true })
|
|
193
|
+
startTime?: Date;
|
|
194
|
+
}
|
|
195
|
+
|
|
69
196
|
/**
|
|
70
197
|
* Enumeration representing the different phases of a Skip response
|
|
71
198
|
* Corresponds to the lifecycle of a Skip AI interaction
|
|
@@ -352,7 +479,7 @@ export class AskSkipResolver {
|
|
|
352
479
|
@Arg('ConversationId', () => String) ConversationId: string,
|
|
353
480
|
@Arg('EntityName', () => String) EntityName: string,
|
|
354
481
|
@Arg('CompositeKey', () => CompositeKeyInputType) compositeKey: CompositeKeyInputType,
|
|
355
|
-
@Ctx() { dataSource, userPayload }: AppContext,
|
|
482
|
+
@Ctx() { dataSource, userPayload, providers }: AppContext,
|
|
356
483
|
@PubSub() pubSub: PubSubEngine
|
|
357
484
|
) {
|
|
358
485
|
// In this function we're simply going to call the Skip API and pass along the message from the user
|
|
@@ -371,14 +498,14 @@ export class AskSkipResolver {
|
|
|
371
498
|
);
|
|
372
499
|
}
|
|
373
500
|
|
|
374
|
-
const md =
|
|
501
|
+
const md = GetReadWriteProvider(providers);
|
|
375
502
|
const { convoEntity, dataContextEntity, convoDetailEntity, dataContext } = await this.HandleSkipChatInitialObjectLoading(
|
|
376
503
|
dataSource,
|
|
377
504
|
ConversationId,
|
|
378
505
|
UserQuestion,
|
|
379
506
|
user,
|
|
380
507
|
userPayload,
|
|
381
|
-
md,
|
|
508
|
+
md as unknown as Metadata,
|
|
382
509
|
null
|
|
383
510
|
);
|
|
384
511
|
|
|
@@ -431,7 +558,7 @@ export class AskSkipResolver {
|
|
|
431
558
|
*/
|
|
432
559
|
@Mutation(() => AskSkipResultType)
|
|
433
560
|
async ExecuteAskSkipLearningCycle(
|
|
434
|
-
@Ctx() { dataSource, userPayload }: AppContext,
|
|
561
|
+
@Ctx() { dataSource, userPayload, providers }: AppContext,
|
|
435
562
|
@Arg('ForceEntityRefresh', () => Boolean, { nullable: true }) ForceEntityRefresh?: boolean
|
|
436
563
|
) {
|
|
437
564
|
const skipConfigInfo = configInfo.askSkip;
|
|
@@ -485,7 +612,7 @@ export class AskSkipResolver {
|
|
|
485
612
|
}
|
|
486
613
|
|
|
487
614
|
// Get the Skip agent ID
|
|
488
|
-
const md =
|
|
615
|
+
const md = GetReadWriteProvider(providers);
|
|
489
616
|
const skipAgent = AIEngine.Instance.GetAgentByName('Skip');
|
|
490
617
|
if (!skipAgent) {
|
|
491
618
|
throw new Error("Skip agent not found in AIEngine");
|
|
@@ -932,7 +1059,9 @@ cycle.`);
|
|
|
932
1059
|
organizationID: skipConfigInfo.orgID,
|
|
933
1060
|
organizationInfo: configInfo?.askSkip?.organizationInfo,
|
|
934
1061
|
apiKeys: this.buildSkipAPIKeys(),
|
|
935
|
-
|
|
1062
|
+
// Favors public URL for conciseness or when behind a proxy for local development
|
|
1063
|
+
// otherwise uses base URL and GraphQL port/path from configuration
|
|
1064
|
+
callingServerURL: accessToken ? (publicUrl || `${baseUrl}:${graphqlPort}${graphqlRootPath}`) : undefined,
|
|
936
1065
|
callingServerAPIKey: accessToken ? apiKey : undefined,
|
|
937
1066
|
callingServerAccessToken: accessToken ? accessToken.Token : undefined
|
|
938
1067
|
};
|
|
@@ -1331,6 +1460,110 @@ cycle.`);
|
|
|
1331
1460
|
];
|
|
1332
1461
|
}
|
|
1333
1462
|
|
|
1463
|
+
/**
|
|
1464
|
+
* Re-attaches the current session to receive status updates for a processing conversation
|
|
1465
|
+
* This is needed after page reloads to resume receiving push notifications
|
|
1466
|
+
*/
|
|
1467
|
+
@Query(() => ReattachConversationResponse)
|
|
1468
|
+
async ReattachToProcessingConversation(
|
|
1469
|
+
@Arg('ConversationId', () => String) ConversationId: string,
|
|
1470
|
+
@Ctx() { userPayload, providers }: AppContext,
|
|
1471
|
+
@PubSub() pubSub: PubSubEngine
|
|
1472
|
+
): Promise<ReattachConversationResponse | null> {
|
|
1473
|
+
try {
|
|
1474
|
+
const md = GetReadWriteProvider(providers);
|
|
1475
|
+
const user = UserCache.Instance.Users.find((u) => u.Email.trim().toLowerCase() === userPayload.email.trim().toLowerCase());
|
|
1476
|
+
if (!user) {
|
|
1477
|
+
LogError(`User ${userPayload.email} not found in UserCache`);
|
|
1478
|
+
return null;
|
|
1479
|
+
}
|
|
1480
|
+
|
|
1481
|
+
// Load the conversation
|
|
1482
|
+
const convoEntity = await md.GetEntityObject<ConversationEntity>('Conversations', user);
|
|
1483
|
+
const loadResult = await convoEntity.Load(ConversationId);
|
|
1484
|
+
|
|
1485
|
+
if (!loadResult) {
|
|
1486
|
+
LogError(`Could not load conversation ${ConversationId} for re-attachment`);
|
|
1487
|
+
return null;
|
|
1488
|
+
}
|
|
1489
|
+
|
|
1490
|
+
// Check if the conversation belongs to this user
|
|
1491
|
+
if (convoEntity.UserID !== user.ID) {
|
|
1492
|
+
LogError(`Conversation ${ConversationId} does not belong to user ${user.Email}`);
|
|
1493
|
+
return null;
|
|
1494
|
+
}
|
|
1495
|
+
|
|
1496
|
+
// If the conversation is processing, reattach the session to receive updates
|
|
1497
|
+
if (convoEntity.Status === 'Processing') {
|
|
1498
|
+
// Add this session to the active streams for this conversation
|
|
1499
|
+
activeStreams.addSession(ConversationId, userPayload.sessionId);
|
|
1500
|
+
|
|
1501
|
+
// Get the last known status message and start time from our cache
|
|
1502
|
+
const lastStatusMessage = activeStreams.getStatus(ConversationId) || 'Processing...';
|
|
1503
|
+
const startTime = activeStreams.getStartTime(ConversationId);
|
|
1504
|
+
|
|
1505
|
+
// Check if the stream is still active
|
|
1506
|
+
const isStreamActive = activeStreams.isActive(ConversationId);
|
|
1507
|
+
|
|
1508
|
+
if (isStreamActive) {
|
|
1509
|
+
// Send the last known status to the frontend
|
|
1510
|
+
const statusMessage = {
|
|
1511
|
+
type: 'AskSkip',
|
|
1512
|
+
status: 'OK',
|
|
1513
|
+
ResponsePhase: 'Processing',
|
|
1514
|
+
conversationID: convoEntity.ID,
|
|
1515
|
+
message: lastStatusMessage,
|
|
1516
|
+
};
|
|
1517
|
+
|
|
1518
|
+
pubSub.publish(PUSH_STATUS_UPDATES_TOPIC, {
|
|
1519
|
+
pushStatusUpdates: {
|
|
1520
|
+
message: JSON.stringify(statusMessage),
|
|
1521
|
+
sessionId: userPayload.sessionId
|
|
1522
|
+
}
|
|
1523
|
+
});
|
|
1524
|
+
|
|
1525
|
+
LogStatus(`Re-attached session ${userPayload.sessionId} to active stream for conversation ${ConversationId}, last status: ${lastStatusMessage}`);
|
|
1526
|
+
|
|
1527
|
+
// Return the status and start time
|
|
1528
|
+
return {
|
|
1529
|
+
lastStatusMessage,
|
|
1530
|
+
startTime: startTime || convoEntity.__mj_UpdatedAt
|
|
1531
|
+
};
|
|
1532
|
+
} else {
|
|
1533
|
+
// Stream is inactive or doesn't exist, just send default status
|
|
1534
|
+
const statusMessage = {
|
|
1535
|
+
type: 'AskSkip',
|
|
1536
|
+
status: 'OK',
|
|
1537
|
+
ResponsePhase: 'Processing',
|
|
1538
|
+
conversationID: convoEntity.ID,
|
|
1539
|
+
message: 'Processing...',
|
|
1540
|
+
};
|
|
1541
|
+
|
|
1542
|
+
pubSub.publish(PUSH_STATUS_UPDATES_TOPIC, {
|
|
1543
|
+
pushStatusUpdates: {
|
|
1544
|
+
message: JSON.stringify(statusMessage),
|
|
1545
|
+
sessionId: userPayload.sessionId
|
|
1546
|
+
}
|
|
1547
|
+
});
|
|
1548
|
+
|
|
1549
|
+
LogStatus(`Re-attached session ${userPayload.sessionId} to conversation ${ConversationId}, but stream is inactive`);
|
|
1550
|
+
|
|
1551
|
+
// Return default start time since stream is inactive
|
|
1552
|
+
return {
|
|
1553
|
+
lastStatusMessage: 'Processing...',
|
|
1554
|
+
startTime: convoEntity.__mj_UpdatedAt
|
|
1555
|
+
};
|
|
1556
|
+
}
|
|
1557
|
+
} else {
|
|
1558
|
+
LogStatus(`Conversation ${ConversationId} is not processing (Status: ${convoEntity.Status})`);
|
|
1559
|
+
return null;
|
|
1560
|
+
}
|
|
1561
|
+
} catch (error) {
|
|
1562
|
+
LogError(`Error re-attaching to conversation: ${error}`);
|
|
1563
|
+
return null;
|
|
1564
|
+
}
|
|
1565
|
+
}
|
|
1566
|
+
|
|
1334
1567
|
/**
|
|
1335
1568
|
* Executes an analysis query with Skip
|
|
1336
1569
|
* This is the primary entry point for general Skip conversations
|
|
@@ -1348,13 +1581,13 @@ cycle.`);
|
|
|
1348
1581
|
async ExecuteAskSkipAnalysisQuery(
|
|
1349
1582
|
@Arg('UserQuestion', () => String) UserQuestion: string,
|
|
1350
1583
|
@Arg('ConversationId', () => String) ConversationId: string,
|
|
1351
|
-
@Ctx() { dataSource, userPayload }: AppContext,
|
|
1584
|
+
@Ctx() { dataSource, userPayload, providers }: AppContext,
|
|
1352
1585
|
@PubSub() pubSub: PubSubEngine,
|
|
1353
1586
|
@Arg('DataContextId', () => String, { nullable: true }) DataContextId?: string,
|
|
1354
1587
|
@Arg('ForceEntityRefresh', () => Boolean, { nullable: true }) ForceEntityRefresh?: boolean,
|
|
1355
1588
|
@Arg('StartTime', () => Date, { nullable: true }) StartTime?: Date
|
|
1356
1589
|
) {
|
|
1357
|
-
const md =
|
|
1590
|
+
const md = GetReadWriteProvider(providers);
|
|
1358
1591
|
const user = UserCache.Instance.Users.find((u) => u.Email.trim().toLowerCase() === userPayload.email.trim().toLowerCase());
|
|
1359
1592
|
if (!user) throw new Error(`User ${userPayload.email} not found in UserCache`);
|
|
1360
1593
|
|
|
@@ -1367,12 +1600,12 @@ cycle.`);
|
|
|
1367
1600
|
UserQuestion,
|
|
1368
1601
|
user,
|
|
1369
1602
|
userPayload,
|
|
1370
|
-
md,
|
|
1603
|
+
md as unknown as Metadata,
|
|
1371
1604
|
DataContextId
|
|
1372
1605
|
);
|
|
1373
1606
|
|
|
1374
1607
|
// Set the conversation status to 'Processing' when a request is initiated
|
|
1375
|
-
await this.setConversationStatus(convoEntity, 'Processing', userPayload);
|
|
1608
|
+
await this.setConversationStatus(convoEntity, 'Processing', userPayload, pubSub);
|
|
1376
1609
|
|
|
1377
1610
|
// now load up the messages. We will load up ALL of the messages for this conversation, and then pass them to the Skip API
|
|
1378
1611
|
const messages: SkipMessage[] = await this.LoadConversationDetailsIntoSkipMessages(
|
|
@@ -1392,7 +1625,7 @@ cycle.`);
|
|
|
1392
1625
|
ConversationId,
|
|
1393
1626
|
userPayload,
|
|
1394
1627
|
pubSub,
|
|
1395
|
-
md,
|
|
1628
|
+
md as unknown as Metadata,
|
|
1396
1629
|
convoEntity,
|
|
1397
1630
|
convoDetailEntity,
|
|
1398
1631
|
dataContext,
|
|
@@ -2196,7 +2429,7 @@ cycle.`);
|
|
|
2196
2429
|
|
|
2197
2430
|
if (conversationDetailCount > 10) {
|
|
2198
2431
|
// Set status of conversation to Available since we still want to allow the user to ask questions
|
|
2199
|
-
await this.setConversationStatus(convoEntity, 'Available', userPayload);
|
|
2432
|
+
await this.setConversationStatus(convoEntity, 'Available', userPayload, pubSub);
|
|
2200
2433
|
|
|
2201
2434
|
// At this point it is likely that we are stuck in a loop, so we stop here
|
|
2202
2435
|
pubSub.publish(PUSH_STATUS_UPDATES_TOPIC, {
|
|
@@ -2241,22 +2474,31 @@ cycle.`);
|
|
|
2241
2474
|
}) => {
|
|
2242
2475
|
LogStatus(JSON.stringify(message, null, 4));
|
|
2243
2476
|
if (message.type === 'status_update') {
|
|
2244
|
-
|
|
2245
|
-
|
|
2246
|
-
|
|
2247
|
-
|
|
2248
|
-
|
|
2249
|
-
|
|
2250
|
-
|
|
2251
|
-
|
|
2252
|
-
|
|
2253
|
-
|
|
2477
|
+
const statusContent = message.value.messages[0].content;
|
|
2478
|
+
|
|
2479
|
+
// Store the status in our active streams cache
|
|
2480
|
+
activeStreams.updateStatus(ConversationId, statusContent, userPayload.sessionId);
|
|
2481
|
+
|
|
2482
|
+
// Publish to all sessions listening to this conversation
|
|
2483
|
+
const sessionIds = activeStreams.getSessionIds(ConversationId);
|
|
2484
|
+
for (const sessionId of sessionIds) {
|
|
2485
|
+
pubSub.publish(PUSH_STATUS_UPDATES_TOPIC, {
|
|
2486
|
+
message: JSON.stringify({
|
|
2487
|
+
type: 'AskSkip',
|
|
2488
|
+
status: 'OK',
|
|
2489
|
+
conversationID: ConversationId,
|
|
2490
|
+
ResponsePhase: message.value.responsePhase,
|
|
2491
|
+
message: statusContent,
|
|
2492
|
+
}),
|
|
2493
|
+
sessionId: sessionId,
|
|
2494
|
+
});
|
|
2495
|
+
}
|
|
2254
2496
|
}
|
|
2255
2497
|
}
|
|
2256
2498
|
);
|
|
2257
2499
|
} catch (error) {
|
|
2258
2500
|
// Set conversation status to Available on error so user can try again
|
|
2259
|
-
await this.setConversationStatus(convoEntity, 'Available', userPayload);
|
|
2501
|
+
await this.setConversationStatus(convoEntity, 'Available', userPayload, pubSub);
|
|
2260
2502
|
|
|
2261
2503
|
// Log the error for debugging
|
|
2262
2504
|
LogError(`Error in HandleSkipChatRequest sendPostRequest: ${error}`);
|
|
@@ -2339,7 +2581,7 @@ cycle.`);
|
|
|
2339
2581
|
}
|
|
2340
2582
|
} else {
|
|
2341
2583
|
// Set status of conversation to Available since we still want to allow the user to ask questions
|
|
2342
|
-
await this.setConversationStatus(convoEntity, 'Available', userPayload);
|
|
2584
|
+
await this.setConversationStatus(convoEntity, 'Available', userPayload, pubSub);
|
|
2343
2585
|
|
|
2344
2586
|
pubSub.publish(PUSH_STATUS_UPDATES_TOPIC, {
|
|
2345
2587
|
message: JSON.stringify({
|
|
@@ -2511,7 +2753,7 @@ cycle.`);
|
|
|
2511
2753
|
convoDetailEntityAI.CompletionTime = endTime.getTime() - startTime.getTime();
|
|
2512
2754
|
|
|
2513
2755
|
// Set conversation status back to Available since we need user input for the clarifying question
|
|
2514
|
-
await this.setConversationStatus(convoEntity, 'Available', userPayload);
|
|
2756
|
+
await this.setConversationStatus(convoEntity, 'Available', userPayload, pubSub);
|
|
2515
2757
|
|
|
2516
2758
|
if (await convoDetailEntityAI.Save()) {
|
|
2517
2759
|
return {
|
|
@@ -2908,13 +3150,42 @@ cycle.`);
|
|
|
2908
3150
|
};
|
|
2909
3151
|
}
|
|
2910
3152
|
|
|
2911
|
-
private async setConversationStatus(convoEntity: ConversationEntity, status: 'Processing' | 'Available', userPayload: UserPayload): Promise<boolean> {
|
|
3153
|
+
private async setConversationStatus(convoEntity: ConversationEntity, status: 'Processing' | 'Available', userPayload: UserPayload, pubSub?: PubSubEngine): Promise<boolean> {
|
|
2912
3154
|
if (convoEntity.Status !== status) {
|
|
2913
3155
|
convoEntity.Status = status;
|
|
2914
3156
|
|
|
2915
3157
|
const convoSaveResult = await convoEntity.Save();
|
|
2916
3158
|
if (!convoSaveResult) {
|
|
2917
3159
|
LogError(`Error updating conversation status to '${status}'`, undefined, convoEntity.LatestResult);
|
|
3160
|
+
} else {
|
|
3161
|
+
// If conversation is now Available (completed), remove it from active streams
|
|
3162
|
+
if (status === 'Available') {
|
|
3163
|
+
activeStreams.removeConversation(convoEntity.ID);
|
|
3164
|
+
LogStatus(`Removed conversation ${convoEntity.ID} from active streams (status changed to Available)`);
|
|
3165
|
+
} else if (status === 'Processing') {
|
|
3166
|
+
// If conversation is starting to process, add the session to active streams
|
|
3167
|
+
activeStreams.addSession(convoEntity.ID, userPayload.sessionId);
|
|
3168
|
+
LogStatus(`Added session ${userPayload.sessionId} to active streams for conversation ${convoEntity.ID}`);
|
|
3169
|
+
}
|
|
3170
|
+
|
|
3171
|
+
if (pubSub) {
|
|
3172
|
+
// Publish status update to notify frontend of conversation status change
|
|
3173
|
+
const statusMessage = {
|
|
3174
|
+
type: 'ConversationStatusUpdate',
|
|
3175
|
+
conversationID: convoEntity.ID,
|
|
3176
|
+
status: status,
|
|
3177
|
+
timestamp: new Date().toISOString()
|
|
3178
|
+
};
|
|
3179
|
+
|
|
3180
|
+
pubSub.publish(PUSH_STATUS_UPDATES_TOPIC, {
|
|
3181
|
+
pushStatusUpdates: {
|
|
3182
|
+
message: JSON.stringify(statusMessage),
|
|
3183
|
+
sessionId: userPayload.sessionId
|
|
3184
|
+
}
|
|
3185
|
+
});
|
|
3186
|
+
|
|
3187
|
+
LogStatus(`Published conversation status update for ${convoEntity.ID}: ${status}`);
|
|
3188
|
+
}
|
|
2918
3189
|
}
|
|
2919
3190
|
return convoSaveResult;
|
|
2920
3191
|
}
|
|
@@ -1,10 +1,10 @@
|
|
|
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, Metadata, RunView, UserInfo, CompositeKey } from '@memberjunction/core';
|
|
3
|
+
import { LogError, Metadata, RunView, UserInfo, CompositeKey, DatabaseProviderBase } from '@memberjunction/core';
|
|
4
4
|
import { RequireSystemUser } from '../directives/RequireSystemUser.js';
|
|
5
5
|
import { QueryCategoryEntity, QueryPermissionEntity } from '@memberjunction/core-entities';
|
|
6
6
|
import { QueryResolver } from '../generated/generated.js';
|
|
7
|
-
import { GetReadWriteProvider } from '../util.js';
|
|
7
|
+
import { GetReadOnlyProvider, GetReadWriteProvider } from '../util.js';
|
|
8
8
|
import { DeleteOptionsInput } from '../generic/DeleteOptionsInput.js';
|
|
9
9
|
import { QueryEntityExtended } from '@memberjunction/core-entities-server';
|
|
10
10
|
|
|
@@ -312,8 +312,8 @@ export class QueryResolverExtended extends QueryResolver {
|
|
|
312
312
|
// Handle CategoryPath if provided
|
|
313
313
|
let finalCategoryID = input.CategoryID;
|
|
314
314
|
if (input.CategoryPath) {
|
|
315
|
-
const
|
|
316
|
-
finalCategoryID = await this.findOrCreateCategoryPath(input.CategoryPath,
|
|
315
|
+
const p = GetReadOnlyProvider(context.providers, {allowFallbackToReadWrite: true});
|
|
316
|
+
finalCategoryID = await this.findOrCreateCategoryPath(input.CategoryPath, p, context.userPayload.userRecord);
|
|
317
317
|
}
|
|
318
318
|
|
|
319
319
|
// Use QueryEntityExtended which handles AI processing
|
|
@@ -345,7 +345,7 @@ export class QueryResolverExtended extends QueryResolver {
|
|
|
345
345
|
const queryID = record.ID;
|
|
346
346
|
|
|
347
347
|
if (input.Permissions && input.Permissions.length > 0) {
|
|
348
|
-
await this.createPermissions(input.Permissions, queryID, context.userPayload.userRecord);
|
|
348
|
+
await this.createPermissions(provider, input.Permissions, queryID, context.userPayload.userRecord);
|
|
349
349
|
await record.RefreshRelatedMetadata(true); // force DB update since we just created new permissions
|
|
350
350
|
}
|
|
351
351
|
|
|
@@ -374,13 +374,12 @@ export class QueryResolverExtended extends QueryResolver {
|
|
|
374
374
|
}
|
|
375
375
|
}
|
|
376
376
|
|
|
377
|
-
protected async createPermissions(permissions: QueryPermissionInputType[], queryID: string, contextUser: UserInfo): Promise<QueryPermissionType[]> {
|
|
377
|
+
protected async createPermissions(p: DatabaseProviderBase, permissions: QueryPermissionInputType[], queryID: string, contextUser: UserInfo): Promise<QueryPermissionType[]> {
|
|
378
378
|
// Create permissions if provided
|
|
379
379
|
const createdPermissions: QueryPermissionType[] = [];
|
|
380
380
|
if (permissions && permissions.length > 0) {
|
|
381
|
-
const md = new Metadata();
|
|
382
381
|
for (const perm of permissions) {
|
|
383
|
-
const permissionEntity = await
|
|
382
|
+
const permissionEntity = await p.GetEntityObject<QueryPermissionEntity>('Query Permissions', contextUser);
|
|
384
383
|
if (permissionEntity) {
|
|
385
384
|
permissionEntity.QueryID = queryID;
|
|
386
385
|
permissionEntity.RoleID = perm.RoleID;
|
|
@@ -427,8 +426,20 @@ export class QueryResolverExtended extends QueryResolver {
|
|
|
427
426
|
// Handle CategoryPath if provided
|
|
428
427
|
let finalCategoryID = input.CategoryID;
|
|
429
428
|
if (input.CategoryPath) {
|
|
430
|
-
|
|
431
|
-
|
|
429
|
+
finalCategoryID = await this.findOrCreateCategoryPath(input.CategoryPath, provider, context.userPayload.userRecord);
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
// now make sure there is NO existing query by the same name in the specified category
|
|
433
|
+
const existingQueryResult = await provider.RunView({
|
|
434
|
+
EntityName: 'Queries',
|
|
435
|
+
ExtraFilter: `Name='${input.Name}' AND CategoryID='${finalCategoryID}'`
|
|
436
|
+
}, context.userPayload.userRecord);
|
|
437
|
+
if (existingQueryResult.Success && existingQueryResult.Results?.length > 0) {
|
|
438
|
+
// we have a match! Let's return an error
|
|
439
|
+
return {
|
|
440
|
+
Success: false,
|
|
441
|
+
ErrorMessage: `Query with name '${input.Name}' already exists in the specified ${input.CategoryID ? 'category' : 'categoryPath'}`
|
|
442
|
+
};
|
|
432
443
|
}
|
|
433
444
|
|
|
434
445
|
// Update fields that were provided
|
|
@@ -481,7 +492,7 @@ export class QueryResolverExtended extends QueryResolver {
|
|
|
481
492
|
}
|
|
482
493
|
|
|
483
494
|
// Create new permissions
|
|
484
|
-
await this.createPermissions(input.Permissions, queryID, context.userPayload.userRecord);
|
|
495
|
+
await this.createPermissions(provider, input.Permissions, queryID, context.userPayload.userRecord);
|
|
485
496
|
|
|
486
497
|
// Refresh the metadata to get updated permissions
|
|
487
498
|
await queryEntity.RefreshRelatedMetadata(true);
|
|
@@ -608,7 +619,7 @@ export class QueryResolverExtended extends QueryResolver {
|
|
|
608
619
|
* @param contextUser - User context for operations
|
|
609
620
|
* @returns The ID of the final category in the path
|
|
610
621
|
*/
|
|
611
|
-
private async findOrCreateCategoryPath(categoryPath: string,
|
|
622
|
+
private async findOrCreateCategoryPath(categoryPath: string, p: DatabaseProviderBase, contextUser: UserInfo): Promise<string> {
|
|
612
623
|
if (!categoryPath || categoryPath.trim() === '') {
|
|
613
624
|
throw new Error('CategoryPath cannot be empty');
|
|
614
625
|
}
|
|
@@ -625,7 +636,7 @@ export class QueryResolverExtended extends QueryResolver {
|
|
|
625
636
|
const categoryName = pathParts[i];
|
|
626
637
|
|
|
627
638
|
// Look for existing category at this level
|
|
628
|
-
const existingCategory = await this.findCategoryByNameAndParent(categoryName, currentParentID, contextUser);
|
|
639
|
+
const existingCategory = await this.findCategoryByNameAndParent(p, categoryName, currentParentID, contextUser);
|
|
629
640
|
|
|
630
641
|
if (existingCategory) {
|
|
631
642
|
currentCategoryID = existingCategory.ID;
|
|
@@ -633,7 +644,7 @@ export class QueryResolverExtended extends QueryResolver {
|
|
|
633
644
|
} else {
|
|
634
645
|
try {
|
|
635
646
|
// Create new category
|
|
636
|
-
const newCategory = await
|
|
647
|
+
const newCategory = await p.GetEntityObject<QueryCategoryEntity>("Query Categories", contextUser);
|
|
637
648
|
if (!newCategory) {
|
|
638
649
|
throw new Error(`Failed to create entity object for Query Categories`);
|
|
639
650
|
}
|
|
@@ -652,7 +663,7 @@ export class QueryResolverExtended extends QueryResolver {
|
|
|
652
663
|
currentParentID = newCategory.ID;
|
|
653
664
|
|
|
654
665
|
// Refresh metadata after each category creation to ensure it's available for subsequent lookups
|
|
655
|
-
await
|
|
666
|
+
await p.Refresh();
|
|
656
667
|
} catch (error) {
|
|
657
668
|
throw new Error(`Failed to create category '${categoryName}': ${error instanceof Error ? error.message : String(error)}`);
|
|
658
669
|
}
|
|
@@ -673,9 +684,9 @@ export class QueryResolverExtended extends QueryResolver {
|
|
|
673
684
|
* @param contextUser - User context for database operations
|
|
674
685
|
* @returns The matching category entity or null if not found
|
|
675
686
|
*/
|
|
676
|
-
private async findCategoryByNameAndParent(categoryName: string, parentID: string | null, contextUser: UserInfo): Promise<QueryCategoryEntity | null> {
|
|
687
|
+
private async findCategoryByNameAndParent(provider: DatabaseProviderBase, categoryName: string, parentID: string | null, contextUser: UserInfo): Promise<QueryCategoryEntity | null> {
|
|
677
688
|
try {
|
|
678
|
-
const rv =
|
|
689
|
+
const rv = provider;
|
|
679
690
|
const parentFilter = parentID ? `ParentID='${parentID}'` : 'ParentID IS NULL';
|
|
680
691
|
const nameFilter = `LOWER(Name) = LOWER('${categoryName.replace(/'/g, "''")}')`; // Escape single quotes
|
|
681
692
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Arg, Ctx, Field, InputType, Int, ObjectType, Query, Resolver } from 'type-graphql';
|
|
2
2
|
import { AppContext } from '../types.js';
|
|
3
3
|
import { LogError, Metadata } from '@memberjunction/core';
|
|
4
|
+
import { GetReadOnlyProvider } from '../util.js';
|
|
4
5
|
|
|
5
6
|
@ObjectType()
|
|
6
7
|
export class DatasetResultType {
|
|
@@ -38,11 +39,11 @@ export class DatasetResolverExtended {
|
|
|
38
39
|
@Query(() => DatasetResultType)
|
|
39
40
|
async GetDatasetByName(
|
|
40
41
|
@Arg('DatasetName', () => String) DatasetName: string,
|
|
41
|
-
@Ctx() {}: AppContext,
|
|
42
|
+
@Ctx() {providers}: AppContext,
|
|
42
43
|
@Arg('ItemFilters', () => [DatasetItemFilterTypeGQL], { nullable: 'itemsAndList' }) ItemFilters?: DatasetItemFilterTypeGQL[]
|
|
43
44
|
) {
|
|
44
45
|
try {
|
|
45
|
-
const md =
|
|
46
|
+
const md = GetReadOnlyProvider(providers, {allowFallbackToReadWrite: true});
|
|
46
47
|
const result = await md.GetDatasetByName(DatasetName, ItemFilters);
|
|
47
48
|
if (result) {
|
|
48
49
|
return {
|
|
@@ -89,11 +90,11 @@ export class DatasetStatusResolver {
|
|
|
89
90
|
@Query(() => DatasetStatusResultType)
|
|
90
91
|
async GetDatasetStatusByName(
|
|
91
92
|
@Arg('DatasetName', () => String) DatasetName: string,
|
|
92
|
-
@Ctx() {}: AppContext,
|
|
93
|
+
@Ctx() {providers}: AppContext,
|
|
93
94
|
@Arg('ItemFilters', () => [DatasetItemFilterTypeGQL], { nullable: 'itemsAndList' }) ItemFilters?: DatasetItemFilterTypeGQL[]
|
|
94
95
|
) {
|
|
95
96
|
try {
|
|
96
|
-
const md =
|
|
97
|
+
const md = GetReadOnlyProvider(providers, {allowFallbackToReadWrite: true});
|
|
97
98
|
const result = await md.GetDatasetStatusByName(DatasetName, ItemFilters);
|
|
98
99
|
if (result) {
|
|
99
100
|
return {
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import { Metadata, CompositeKey } from '@memberjunction/core';
|
|
1
|
+
import { Metadata, CompositeKey, DatabaseProviderBase } from '@memberjunction/core';
|
|
2
2
|
import { Arg, Ctx, Field, InputType, ObjectType, Query, Resolver } from 'type-graphql';
|
|
3
3
|
import { AppContext } from '../types.js';
|
|
4
4
|
import { CompositeKeyInputType, CompositeKeyOutputType } from '../generic/KeyInputOutputTypes.js';
|
|
5
|
+
import { GetReadOnlyProvider } from '../util.js';
|
|
5
6
|
|
|
6
7
|
@InputType()
|
|
7
8
|
export class EntityRecordNameInput {
|
|
@@ -36,26 +37,27 @@ export class EntityRecordNameResolver {
|
|
|
36
37
|
async GetEntityRecordName(
|
|
37
38
|
@Arg('EntityName', () => String) EntityName: string,
|
|
38
39
|
@Arg('CompositeKey', () => CompositeKeyInputType) primaryKey: CompositeKey,
|
|
39
|
-
@Ctx() { userPayload }: AppContext
|
|
40
|
+
@Ctx() { providers, userPayload }: AppContext
|
|
40
41
|
): Promise<EntityRecordNameResult> {
|
|
41
|
-
const md =
|
|
42
|
+
const md = GetReadOnlyProvider(providers, {allowFallbackToReadWrite: true});
|
|
43
|
+
|
|
42
44
|
return await this.InnerGetEntityRecordName(md, EntityName, primaryKey);
|
|
43
45
|
}
|
|
44
46
|
|
|
45
47
|
@Query(() => [EntityRecordNameResult])
|
|
46
48
|
async GetEntityRecordNames(
|
|
47
49
|
@Arg('info', () => [EntityRecordNameInput]) info: EntityRecordNameInput[],
|
|
48
|
-
@Ctx() {}: AppContext
|
|
50
|
+
@Ctx() {providers}: AppContext
|
|
49
51
|
): Promise<EntityRecordNameResult[]> {
|
|
50
52
|
const result: EntityRecordNameResult[] = [];
|
|
51
|
-
const md =
|
|
53
|
+
const md = GetReadOnlyProvider(providers, {allowFallbackToReadWrite: true});
|
|
52
54
|
for (const i of info) {
|
|
53
55
|
result.push(await this.InnerGetEntityRecordName(md, i.EntityName, i.CompositeKey));
|
|
54
56
|
}
|
|
55
57
|
return result;
|
|
56
58
|
}
|
|
57
59
|
|
|
58
|
-
async InnerGetEntityRecordName(md:
|
|
60
|
+
async InnerGetEntityRecordName(md: DatabaseProviderBase, EntityName: string, primaryKey: CompositeKeyInputType): Promise<EntityRecordNameResult> {
|
|
59
61
|
const pk = new CompositeKey(primaryKey.KeyValuePairs);
|
|
60
62
|
const e = md.Entities.find((e) => e.Name === EntityName);
|
|
61
63
|
if (e) {
|