@memberjunction/server 6.2.0-edge.2 → 6.2.0-edge.3
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 +30 -0
- package/dist/.tsbuildinfo +1 -1
- package/dist/config.d.ts +2189 -71
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +123 -3
- package/dist/config.js.map +1 -1
- package/dist/context.d.ts.map +1 -1
- package/dist/context.js +11 -23
- package/dist/context.js.map +1 -1
- package/dist/generated/graphql-schemas/__mj.d.ts +928 -0
- package/dist/generated/graphql-schemas/__mj.d.ts.map +1 -1
- package/dist/generated/graphql-schemas/__mj.js +8482 -3649
- package/dist/generated/graphql-schemas/__mj.js.map +1 -1
- package/dist/generic/ResolverBase.d.ts +43 -3
- package/dist/generic/ResolverBase.d.ts.map +1 -1
- package/dist/generic/ResolverBase.js +142 -27
- package/dist/generic/ResolverBase.js.map +1 -1
- package/dist/generic/RunViewResolver.d.ts +4 -0
- package/dist/generic/RunViewResolver.d.ts.map +1 -1
- package/dist/generic/RunViewResolver.js +33 -1
- package/dist/generic/RunViewResolver.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +115 -49
- package/dist/index.js.map +1 -1
- package/dist/postgresqlCompat.d.ts +8 -0
- package/dist/postgresqlCompat.d.ts.map +1 -0
- package/dist/postgresqlCompat.js +19 -0
- package/dist/postgresqlCompat.js.map +1 -0
- package/dist/postgresqlPoolSettings.d.ts +108 -0
- package/dist/postgresqlPoolSettings.d.ts.map +1 -0
- package/dist/postgresqlPoolSettings.js +134 -0
- package/dist/postgresqlPoolSettings.js.map +1 -0
- package/dist/resolvers/AdhocQueryResolver.d.ts +23 -18
- package/dist/resolvers/AdhocQueryResolver.d.ts.map +1 -1
- package/dist/resolvers/AdhocQueryResolver.js +77 -167
- package/dist/resolvers/AdhocQueryResolver.js.map +1 -1
- package/dist/resolvers/EntityRecordNameResolver.d.ts +7 -23
- package/dist/resolvers/EntityRecordNameResolver.d.ts.map +1 -1
- package/dist/resolvers/EntityRecordNameResolver.js +9 -47
- package/dist/resolvers/EntityRecordNameResolver.js.map +1 -1
- package/dist/resolvers/GetDataResolver.d.ts.map +1 -1
- package/dist/resolvers/GetDataResolver.js +9 -0
- package/dist/resolvers/GetDataResolver.js.map +1 -1
- package/dist/resolvers/HumanHandoffResolver.d.ts +107 -0
- package/dist/resolvers/HumanHandoffResolver.d.ts.map +1 -0
- package/dist/resolvers/HumanHandoffResolver.js +324 -0
- package/dist/resolvers/HumanHandoffResolver.js.map +1 -0
- package/dist/resolvers/MeetingResolver.d.ts +69 -0
- package/dist/resolvers/MeetingResolver.d.ts.map +1 -0
- package/dist/resolvers/MeetingResolver.js +381 -0
- package/dist/resolvers/MeetingResolver.js.map +1 -0
- package/dist/resolvers/RealtimeBridgeResolver.d.ts +34 -1
- package/dist/resolvers/RealtimeBridgeResolver.d.ts.map +1 -1
- package/dist/resolvers/RealtimeBridgeResolver.js +202 -7
- package/dist/resolvers/RealtimeBridgeResolver.js.map +1 -1
- package/dist/resolvers/RealtimeClientSessionResolver.d.ts +1 -83
- package/dist/resolvers/RealtimeClientSessionResolver.d.ts.map +1 -1
- package/dist/resolvers/RealtimeClientSessionResolver.js +4 -206
- package/dist/resolvers/RealtimeClientSessionResolver.js.map +1 -1
- package/dist/resolvers/adhocTimeout.d.ts +7 -0
- package/dist/resolvers/adhocTimeout.d.ts.map +1 -0
- package/dist/resolvers/adhocTimeout.js +12 -0
- package/dist/resolvers/adhocTimeout.js.map +1 -0
- package/dist/resolvers/meetings/index.d.ts +5 -0
- package/dist/resolvers/meetings/index.d.ts.map +1 -0
- package/dist/resolvers/meetings/index.js +5 -0
- package/dist/resolvers/meetings/index.js.map +1 -0
- package/dist/resolvers/meetings/meetingHelpers.d.ts +17 -0
- package/dist/resolvers/meetings/meetingHelpers.d.ts.map +1 -0
- package/dist/resolvers/meetings/meetingHelpers.js +212 -0
- package/dist/resolvers/meetings/meetingHelpers.js.map +1 -0
- package/dist/resolvers/meetings/meetingMutations.d.ts +12 -0
- package/dist/resolvers/meetings/meetingMutations.d.ts.map +1 -0
- package/dist/resolvers/meetings/meetingMutations.js +418 -0
- package/dist/resolvers/meetings/meetingMutations.js.map +1 -0
- package/dist/resolvers/meetings/meetingQueries.d.ts +7 -0
- package/dist/resolvers/meetings/meetingQueries.d.ts.map +1 -0
- package/dist/resolvers/meetings/meetingQueries.js +113 -0
- package/dist/resolvers/meetings/meetingQueries.js.map +1 -0
- package/dist/resolvers/meetings/meetingTypes.d.ts +89 -0
- package/dist/resolvers/meetings/meetingTypes.d.ts.map +1 -0
- package/dist/resolvers/meetings/meetingTypes.js +345 -0
- package/dist/resolvers/meetings/meetingTypes.js.map +1 -0
- package/dist/rest/ViewOperationsHandler.d.ts.map +1 -1
- package/dist/rest/ViewOperationsHandler.js +8 -5
- package/dist/rest/ViewOperationsHandler.js.map +1 -1
- package/dist/services/TaskGraphActionRunner.d.ts +8 -0
- package/dist/services/TaskGraphActionRunner.d.ts.map +1 -1
- package/dist/services/TaskGraphActionRunner.js +27 -1
- package/dist/services/TaskGraphActionRunner.js.map +1 -1
- package/dist/services/WorkQueueHostService.d.ts +36 -0
- package/dist/services/WorkQueueHostService.d.ts.map +1 -0
- package/dist/services/WorkQueueHostService.js +97 -0
- package/dist/services/WorkQueueHostService.js.map +1 -0
- package/dist/services/workQueueConfig.d.ts +103 -0
- package/dist/services/workQueueConfig.d.ts.map +1 -0
- package/dist/services/workQueueConfig.js +48 -0
- package/dist/services/workQueueConfig.js.map +1 -0
- package/dist/types.d.ts +5 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/package.json +95 -94
- package/src/__tests__/AdhocQueryResolver.bugs.test.ts +14 -35
- package/src/__tests__/AdhocQueryResolver.pagination.test.ts +6 -2
- package/src/__tests__/AdhocQueryResolver.scopeGuard.test.ts +3 -3
- package/src/__tests__/EntityRecordNameResolver.fieldSecurity.test.ts +27 -72
- package/src/__tests__/HumanHandoffResolver.test.ts +233 -0
- package/src/__tests__/MeetingResolver.test.ts +374 -0
- package/src/__tests__/RealtimeBridgeResolver.test.ts +240 -2
- package/src/__tests__/ResolverBase.adhocReadScope.test.ts +96 -0
- package/src/__tests__/ResolverBase.filterEscaping.test.ts +21 -2
- package/src/__tests__/ResolverBase.includeBinaryFields.test.ts +291 -0
- package/src/__tests__/TaskGraphActionRunner.durablePayload.test.ts +41 -0
- package/src/__tests__/WorkQueueHostService.test.ts +133 -0
- package/src/__tests__/adhocTimeoutClamp.test.ts +26 -0
- package/src/__tests__/postgresqlPoolSettings.test.ts +131 -0
- package/src/__tests__/translateBracketsToPG.test.ts +20 -0
- package/src/config.ts +130 -3
- package/src/context.ts +11 -24
- package/src/generated/graphql-schemas/__mj.ts +22138 -18770
- package/src/generic/ResolverBase.ts +171 -27
- package/src/generic/RunViewResolver.ts +33 -2
- package/src/index.ts +127 -50
- package/src/postgresqlCompat.ts +21 -0
- package/src/postgresqlPoolSettings.ts +213 -0
- package/src/resolvers/AdhocQueryResolver.ts +88 -188
- package/src/resolvers/EntityRecordNameResolver.ts +9 -51
- package/src/resolvers/GetDataResolver.ts +10 -0
- package/src/resolvers/HumanHandoffResolver.ts +304 -0
- package/src/resolvers/MeetingResolver.ts +438 -0
- package/src/resolvers/RealtimeBridgeResolver.ts +227 -13
- package/src/resolvers/RealtimeClientSessionResolver.ts +6 -252
- package/src/resolvers/adhocTimeout.ts +12 -0
- package/src/resolvers/meetings/index.ts +4 -0
- package/src/resolvers/meetings/meetingHelpers.ts +307 -0
- package/src/resolvers/meetings/meetingMutations.ts +535 -0
- package/src/resolvers/meetings/meetingQueries.ts +172 -0
- package/src/resolvers/meetings/meetingTypes.ts +238 -0
- package/src/rest/ViewOperationsHandler.ts +8 -5
- package/src/services/TaskGraphActionRunner.ts +29 -1
- package/src/services/WorkQueueHostService.ts +129 -0
- package/src/services/workQueueConfig.ts +52 -0
- package/src/types.ts +5 -0
- package/dist/resolvers/adhoc-query-helpers.d.ts +0 -33
- package/dist/resolvers/adhoc-query-helpers.d.ts.map +0 -1
- package/dist/resolvers/adhoc-query-helpers.js +0 -50
- package/dist/resolvers/adhoc-query-helpers.js.map +0 -1
- package/src/__tests__/adhoc-query-helpers.test.ts +0 -50
- package/src/resolvers/adhoc-query-helpers.ts +0 -67
|
@@ -0,0 +1,535 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import {
|
|
3
|
+
IMetadataProvider,
|
|
4
|
+
LogError,
|
|
5
|
+
RunView,
|
|
6
|
+
UserInfo,
|
|
7
|
+
} from '@memberjunction/core';
|
|
8
|
+
import { EscapeSQLString, UUIDsEqual } from '@memberjunction/global';
|
|
9
|
+
import type {
|
|
10
|
+
MJMeetingEntity,
|
|
11
|
+
MJMeetingParticipantEntity,
|
|
12
|
+
} from '@memberjunction/core-entities';
|
|
13
|
+
import {
|
|
14
|
+
LiveKitTokenService,
|
|
15
|
+
LiveKitAgentRoomCoordinator,
|
|
16
|
+
LiveKitEgressService,
|
|
17
|
+
LiveKitUserIdentity,
|
|
18
|
+
} from '@memberjunction/livekit-room-server';
|
|
19
|
+
import { SessionManager } from '../../agentSessions/SessionManager.js';
|
|
20
|
+
import { CorrelateRecordingStart } from '../meetingRecordingRegistration.js';
|
|
21
|
+
import {
|
|
22
|
+
MeetingResult,
|
|
23
|
+
StartMeetingResult,
|
|
24
|
+
CreateMeetingInput,
|
|
25
|
+
UpdateMeetingInput,
|
|
26
|
+
RSVPMeetingInput,
|
|
27
|
+
MeetingParticipantType,
|
|
28
|
+
} from './meetingTypes.js';
|
|
29
|
+
import * as defaultHelpers from './meetingHelpers.js';
|
|
30
|
+
|
|
31
|
+
export type MeetingHelpers = typeof defaultHelpers;
|
|
32
|
+
|
|
33
|
+
function resolveHelpers(overrides?: Partial<MeetingHelpers>): MeetingHelpers {
|
|
34
|
+
return { ...defaultHelpers, ...overrides };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export async function ExecuteCreateMeeting(
|
|
38
|
+
provider: IMetadataProvider,
|
|
39
|
+
user: UserInfo,
|
|
40
|
+
input: CreateMeetingInput,
|
|
41
|
+
helpers?: Partial<MeetingHelpers>,
|
|
42
|
+
): Promise<MeetingResult> {
|
|
43
|
+
const h = resolveHelpers(helpers);
|
|
44
|
+
try {
|
|
45
|
+
const trimmedTitle = input.Title?.trim();
|
|
46
|
+
if (!trimmedTitle) {
|
|
47
|
+
return { Success: false, ErrorMessage: 'Meeting title is required.' };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (input.ScheduledStartAt && input.ScheduledEndAt && input.ScheduledEndAt <= input.ScheduledStartAt) {
|
|
51
|
+
return { Success: false, ErrorMessage: 'Scheduled end time must be after scheduled start time.' };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const allowDialIn = input.AllowPhoneDialIn ?? false;
|
|
55
|
+
let dialInPhoneNumberID = input.DialInPhoneNumberID?.trim();
|
|
56
|
+
let dialInCode: string | undefined;
|
|
57
|
+
|
|
58
|
+
if (allowDialIn) {
|
|
59
|
+
if (!dialInPhoneNumberID) {
|
|
60
|
+
// Select default active phone number
|
|
61
|
+
const defaultNumber = await h.ResolveDefaultPhoneNumber(user, provider);
|
|
62
|
+
if (!defaultNumber) {
|
|
63
|
+
return {
|
|
64
|
+
Success: false,
|
|
65
|
+
ErrorMessage: 'Cannot allow phone dial-in: no active phone numbers are configured in the system.',
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
dialInPhoneNumberID = defaultNumber.ID;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Generate a random unique dial-in code (6 digits)
|
|
72
|
+
dialInCode = await h.GenerateUniqueDialInCode(dialInPhoneNumberID, user, provider);
|
|
73
|
+
if (!dialInCode) {
|
|
74
|
+
return { Success: false, ErrorMessage: 'Failed to generate a unique dial-in code for this phone number.' };
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Room name is always unguessable and generated: mj-mtg-<uuid>
|
|
79
|
+
const roomName = `mj-mtg-${randomUUID()}`;
|
|
80
|
+
|
|
81
|
+
const meeting = await provider.GetEntityObject<MJMeetingEntity>('MJ: Meetings', user);
|
|
82
|
+
meeting.Title = trimmedTitle;
|
|
83
|
+
meeting.Description = input.Description?.trim() || null;
|
|
84
|
+
meeting.HostUserID = user.ID;
|
|
85
|
+
meeting.RoomName = roomName;
|
|
86
|
+
meeting.Status = 'Scheduled';
|
|
87
|
+
meeting.ScheduledStartAt = input.ScheduledStartAt ?? null;
|
|
88
|
+
meeting.ScheduledEndAt = input.ScheduledEndAt ?? null;
|
|
89
|
+
meeting.AllowPhoneDialIn = allowDialIn;
|
|
90
|
+
meeting.DialInPhoneNumberID = allowDialIn ? dialInPhoneNumberID! : null;
|
|
91
|
+
meeting.DialInCode = allowDialIn ? dialInCode! : null;
|
|
92
|
+
meeting.RecordingPolicy = h.NormalizeRecordingPolicy(input.RecordingPolicy);
|
|
93
|
+
|
|
94
|
+
if (!(await meeting.Save())) {
|
|
95
|
+
const err = meeting.LatestResult?.CompleteMessage ?? 'Failed to save meeting.';
|
|
96
|
+
LogError(`[MeetingResolver] CreateMeeting save failed: ${err}`);
|
|
97
|
+
return { Success: false, ErrorMessage: err };
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Add Host as accepted participant
|
|
101
|
+
const hostParticipant = await provider.GetEntityObject<MJMeetingParticipantEntity>('MJ: Meeting Participants', user);
|
|
102
|
+
hostParticipant.MeetingID = meeting.ID;
|
|
103
|
+
hostParticipant.UserID = user.ID;
|
|
104
|
+
hostParticipant.Role = 'Host';
|
|
105
|
+
hostParticipant.InviteStatus = 'Accepted';
|
|
106
|
+
await hostParticipant.Save();
|
|
107
|
+
|
|
108
|
+
// Add other participants
|
|
109
|
+
const participants: MeetingParticipantType[] = [h.MapParticipantToType(hostParticipant, user.Name, user.Email)];
|
|
110
|
+
|
|
111
|
+
if (input.Participants && input.Participants.length > 0) {
|
|
112
|
+
for (const pInput of input.Participants) {
|
|
113
|
+
// Skip if duplicate of host
|
|
114
|
+
if (pInput.UserID && UUIDsEqual(pInput.UserID, user.ID)) {
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const participant = await provider.GetEntityObject<MJMeetingParticipantEntity>('MJ: Meeting Participants', user);
|
|
119
|
+
participant.MeetingID = meeting.ID;
|
|
120
|
+
|
|
121
|
+
if (pInput.AgentID && pInput.AgentID.trim()) {
|
|
122
|
+
participant.AgentID = pInput.AgentID.trim();
|
|
123
|
+
participant.Role = 'Agent';
|
|
124
|
+
participant.InviteStatus = 'Accepted';
|
|
125
|
+
} else if (pInput.UserID && pInput.UserID.trim()) {
|
|
126
|
+
participant.UserID = pInput.UserID.trim();
|
|
127
|
+
participant.Role = h.NormalizeParticipantRole(pInput.Role, 'Attendee');
|
|
128
|
+
participant.InviteStatus = 'Invited';
|
|
129
|
+
} else {
|
|
130
|
+
// External guest
|
|
131
|
+
participant.ExternalName = pInput.ExternalName?.trim() || null;
|
|
132
|
+
participant.ExternalEmail = pInput.ExternalEmail?.trim() || null;
|
|
133
|
+
participant.ExternalPhone = pInput.ExternalPhone?.trim() || null;
|
|
134
|
+
participant.Role = h.NormalizeParticipantRole(pInput.Role, 'Attendee');
|
|
135
|
+
participant.InviteStatus = 'Invited';
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (await participant.Save()) {
|
|
139
|
+
participants.push(h.MapParticipantToType(participant));
|
|
140
|
+
|
|
141
|
+
// Send notification for internal users
|
|
142
|
+
if (participant.UserID) {
|
|
143
|
+
void h.SendMeetingInviteNotification(meeting, participant.UserID, user, provider);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return {
|
|
150
|
+
Success: true,
|
|
151
|
+
Meeting: h.MapMeetingToType(meeting, participants),
|
|
152
|
+
};
|
|
153
|
+
} catch (error) {
|
|
154
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
155
|
+
LogError(`[MeetingResolver] CreateMeeting exception: ${msg}`);
|
|
156
|
+
return { Success: false, ErrorMessage: msg };
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export async function ExecuteUpdateMeeting(
|
|
161
|
+
provider: IMetadataProvider,
|
|
162
|
+
user: UserInfo,
|
|
163
|
+
input: UpdateMeetingInput,
|
|
164
|
+
helpers?: Partial<MeetingHelpers>,
|
|
165
|
+
): Promise<MeetingResult> {
|
|
166
|
+
const h = resolveHelpers(helpers);
|
|
167
|
+
try {
|
|
168
|
+
const meeting = await h.LoadMeetingEntity(input.MeetingID, user, provider);
|
|
169
|
+
if (!meeting) {
|
|
170
|
+
return { Success: false, ErrorMessage: 'Meeting not found.' };
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Check authorization (host or co-host)
|
|
174
|
+
const authorized = await h.IsUserHostOrCoHost(meeting, user, provider);
|
|
175
|
+
if (!authorized) {
|
|
176
|
+
return { Success: false, ErrorMessage: 'Unauthorized: only the host or co-host may modify this meeting.' };
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if (meeting.Status === 'Ended' || meeting.Status === 'Cancelled') {
|
|
180
|
+
return { Success: false, ErrorMessage: `Cannot modify a meeting with status '${meeting.Status}'.` };
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if (input.Title !== undefined) {
|
|
184
|
+
const trimmedTitle = input.Title.trim();
|
|
185
|
+
if (!trimmedTitle) {
|
|
186
|
+
return { Success: false, ErrorMessage: 'Meeting title cannot be empty.' };
|
|
187
|
+
}
|
|
188
|
+
meeting.Title = trimmedTitle;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
if (input.Description !== undefined) {
|
|
192
|
+
meeting.Description = input.Description?.trim() || null;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
if (input.ScheduledStartAt !== undefined) {
|
|
196
|
+
meeting.ScheduledStartAt = input.ScheduledStartAt;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
if (input.ScheduledEndAt !== undefined) {
|
|
200
|
+
meeting.ScheduledEndAt = input.ScheduledEndAt;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
if (meeting.ScheduledStartAt && meeting.ScheduledEndAt && meeting.ScheduledEndAt <= meeting.ScheduledStartAt) {
|
|
204
|
+
return { Success: false, ErrorMessage: 'Scheduled end time must be after scheduled start time.' };
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
if (input.RecordingPolicy !== undefined) {
|
|
208
|
+
meeting.RecordingPolicy = h.NormalizeRecordingPolicy(input.RecordingPolicy);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
if (input.AllowPhoneDialIn !== undefined) {
|
|
212
|
+
meeting.AllowPhoneDialIn = input.AllowPhoneDialIn;
|
|
213
|
+
if (meeting.AllowPhoneDialIn && !meeting.DialInCode) {
|
|
214
|
+
let phoneID = input.DialInPhoneNumberID?.trim() || meeting.DialInPhoneNumberID;
|
|
215
|
+
if (!phoneID) {
|
|
216
|
+
const defPhone = await h.ResolveDefaultPhoneNumber(user, provider);
|
|
217
|
+
phoneID = defPhone?.ID;
|
|
218
|
+
}
|
|
219
|
+
if (phoneID) {
|
|
220
|
+
meeting.DialInPhoneNumberID = phoneID;
|
|
221
|
+
meeting.DialInCode = await h.GenerateUniqueDialInCode(phoneID, user, provider);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
if (input.DialInPhoneNumberID !== undefined && input.DialInPhoneNumberID.trim()) {
|
|
227
|
+
meeting.DialInPhoneNumberID = input.DialInPhoneNumberID.trim();
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
if (!(await meeting.Save())) {
|
|
231
|
+
const err = meeting.LatestResult?.CompleteMessage ?? 'Failed to update meeting.';
|
|
232
|
+
return { Success: false, ErrorMessage: err };
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// If participants specified in update, sync participants (upsert by UserID/AgentID/ExternalEmail)
|
|
236
|
+
if (input.Participants !== undefined) {
|
|
237
|
+
const existing = await h.LoadParticipants(meeting.ID, user, provider);
|
|
238
|
+
|
|
239
|
+
for (const pInput of input.Participants) {
|
|
240
|
+
const isSelf = pInput.UserID && UUIDsEqual(pInput.UserID, user.ID);
|
|
241
|
+
const match = existing.find(
|
|
242
|
+
(e) =>
|
|
243
|
+
(pInput.UserID && e.UserID && UUIDsEqual(e.UserID, pInput.UserID)) ||
|
|
244
|
+
(pInput.AgentID && e.AgentID && UUIDsEqual(e.AgentID, pInput.AgentID)) ||
|
|
245
|
+
(pInput.ExternalEmail && e.ExternalEmail && e.ExternalEmail.toLowerCase() === pInput.ExternalEmail.toLowerCase()),
|
|
246
|
+
);
|
|
247
|
+
|
|
248
|
+
if (match) {
|
|
249
|
+
if (!isSelf && pInput.Role) {
|
|
250
|
+
const rv = provider ? RunView.FromMetadataProvider(provider) : new RunView();
|
|
251
|
+
const escapedPID = EscapeSQLString(match.ID);
|
|
252
|
+
const partRes = await rv.RunView<MJMeetingParticipantEntity>(
|
|
253
|
+
{
|
|
254
|
+
EntityName: 'MJ: Meeting Participants',
|
|
255
|
+
ExtraFilter: `ID = '${escapedPID}'`,
|
|
256
|
+
ResultType: 'entity_object',
|
|
257
|
+
},
|
|
258
|
+
user,
|
|
259
|
+
);
|
|
260
|
+
if (partRes.Success && partRes.Results && partRes.Results.length > 0) {
|
|
261
|
+
const part = partRes.Results[0];
|
|
262
|
+
part.Role = h.NormalizeParticipantRole(pInput.Role, part.Role);
|
|
263
|
+
await part.Save();
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
} else {
|
|
267
|
+
// New participant
|
|
268
|
+
const newPart = await provider.GetEntityObject<MJMeetingParticipantEntity>('MJ: Meeting Participants', user);
|
|
269
|
+
newPart.MeetingID = meeting.ID;
|
|
270
|
+
|
|
271
|
+
if (pInput.AgentID && pInput.AgentID.trim()) {
|
|
272
|
+
newPart.AgentID = pInput.AgentID.trim();
|
|
273
|
+
newPart.Role = 'Agent';
|
|
274
|
+
newPart.InviteStatus = 'Accepted';
|
|
275
|
+
} else if (pInput.UserID && pInput.UserID.trim()) {
|
|
276
|
+
newPart.UserID = pInput.UserID.trim();
|
|
277
|
+
newPart.Role = h.NormalizeParticipantRole(pInput.Role, 'Attendee');
|
|
278
|
+
newPart.InviteStatus = 'Invited';
|
|
279
|
+
} else {
|
|
280
|
+
newPart.ExternalName = pInput.ExternalName?.trim() || null;
|
|
281
|
+
newPart.ExternalEmail = pInput.ExternalEmail?.trim() || null;
|
|
282
|
+
newPart.ExternalPhone = pInput.ExternalPhone?.trim() || null;
|
|
283
|
+
newPart.Role = h.NormalizeParticipantRole(pInput.Role, 'Attendee');
|
|
284
|
+
newPart.InviteStatus = 'Invited';
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
if (await newPart.Save()) {
|
|
288
|
+
if (newPart.UserID) {
|
|
289
|
+
void h.SendMeetingInviteNotification(meeting, newPart.UserID, user, provider);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
const updatedParticipants = await h.LoadParticipants(meeting.ID, user, provider);
|
|
297
|
+
return {
|
|
298
|
+
Success: true,
|
|
299
|
+
Meeting: h.MapMeetingToType(meeting, updatedParticipants),
|
|
300
|
+
};
|
|
301
|
+
} catch (error) {
|
|
302
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
303
|
+
LogError(`[MeetingResolver] UpdateMeeting exception: ${msg}`);
|
|
304
|
+
return { Success: false, ErrorMessage: msg };
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
export async function ExecuteStartMeeting(
|
|
309
|
+
provider: IMetadataProvider,
|
|
310
|
+
user: UserInfo,
|
|
311
|
+
meetingID: string,
|
|
312
|
+
sessionManager: SessionManager,
|
|
313
|
+
helpers?: Partial<MeetingHelpers>,
|
|
314
|
+
): Promise<StartMeetingResult> {
|
|
315
|
+
const h = resolveHelpers(helpers);
|
|
316
|
+
try {
|
|
317
|
+
const meeting = await h.LoadMeetingEntity(meetingID, user, provider);
|
|
318
|
+
if (!meeting) {
|
|
319
|
+
return { Success: false, ErrorMessage: 'Meeting not found.' };
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
const authorized = await h.IsUserHostOrCoHost(meeting, user, provider);
|
|
323
|
+
if (!authorized) {
|
|
324
|
+
return { Success: false, ErrorMessage: 'Unauthorized: only the host or co-host may start this meeting.' };
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
if (meeting.Status === 'Ended' || meeting.Status === 'Cancelled') {
|
|
328
|
+
return { Success: false, ErrorMessage: `Meeting cannot be started because its status is '${meeting.Status}'.` };
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
const now = new Date();
|
|
332
|
+
meeting.Status = 'Live';
|
|
333
|
+
meeting.StartedAt = meeting.StartedAt ?? now;
|
|
334
|
+
if (!(await meeting.Save())) {
|
|
335
|
+
return { Success: false, ErrorMessage: meeting.LatestResult?.CompleteMessage ?? 'Failed to update meeting status to Live.' };
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// 1. Create or get Interaction row for this meeting
|
|
339
|
+
const interaction = await h.EnsureMeetingInteraction(meeting, user, provider);
|
|
340
|
+
|
|
341
|
+
// 2. Start all AI agent participants
|
|
342
|
+
const participants = await h.LoadParticipants(meeting.ID, user, provider);
|
|
343
|
+
const agentParticipants = participants.filter((p) => p.Role === 'Agent' && p.AgentID);
|
|
344
|
+
|
|
345
|
+
for (const ap of agentParticipants) {
|
|
346
|
+
try {
|
|
347
|
+
const createdSession = await sessionManager.CreateSession(
|
|
348
|
+
{ agentID: ap.AgentID!, userID: user.ID },
|
|
349
|
+
user,
|
|
350
|
+
provider,
|
|
351
|
+
);
|
|
352
|
+
await LiveKitAgentRoomCoordinator.Instance.StartAgentRoomSession({
|
|
353
|
+
AgentSessionID: createdSession.ID,
|
|
354
|
+
RoomName: meeting.RoomName,
|
|
355
|
+
AgentID: ap.AgentID!,
|
|
356
|
+
ContextUser: user,
|
|
357
|
+
MetadataProvider: provider,
|
|
358
|
+
TurnMode: 'Active',
|
|
359
|
+
TurnAddressing: 'Auto',
|
|
360
|
+
});
|
|
361
|
+
} catch (agentErr) {
|
|
362
|
+
LogError(`[MeetingResolver] Failed to start agent participant ${ap.AgentID} in room ${meeting.RoomName}: ${agentErr instanceof Error ? agentErr.message : String(agentErr)}`);
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// 3. Initiate recording if RecordingPolicy is 'Automatic'
|
|
367
|
+
if (meeting.RecordingPolicy === 'Automatic') {
|
|
368
|
+
try {
|
|
369
|
+
const egress = await new LiveKitEgressService().StartRoomRecording({ RoomName: meeting.RoomName });
|
|
370
|
+
if (egress && egress.EgressID) {
|
|
371
|
+
void CorrelateRecordingStart(meeting.RoomName, egress.EgressID, user, provider);
|
|
372
|
+
if (interaction) {
|
|
373
|
+
await h.RecordInteractionEvent(interaction.ID, 'RecordingStarted', user, provider, { EgressID: egress.EgressID });
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
} catch (recErr) {
|
|
377
|
+
LogError(`[MeetingResolver] Automatic recording initiation failed for room ${meeting.RoomName}: ${recErr instanceof Error ? recErr.message : String(recErr)}`);
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
// 4. Mint LiveKit client token for the caller
|
|
382
|
+
const tokenService = new LiveKitTokenService();
|
|
383
|
+
const identity = LiveKitUserIdentity(user.ID);
|
|
384
|
+
const mintedToken = await tokenService.MintClientToken(meeting.RoomName, identity, user.Name ?? user.Email);
|
|
385
|
+
|
|
386
|
+
return {
|
|
387
|
+
Success: true,
|
|
388
|
+
Meeting: h.MapMeetingToType(meeting, participants),
|
|
389
|
+
RoomName: meeting.RoomName,
|
|
390
|
+
ClientToken: mintedToken.Token,
|
|
391
|
+
};
|
|
392
|
+
} catch (error) {
|
|
393
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
394
|
+
LogError(`[MeetingResolver] StartMeeting exception: ${msg}`);
|
|
395
|
+
return { Success: false, ErrorMessage: msg };
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
export async function ExecuteEndMeeting(
|
|
400
|
+
provider: IMetadataProvider,
|
|
401
|
+
user: UserInfo,
|
|
402
|
+
meetingID: string,
|
|
403
|
+
helpers?: Partial<MeetingHelpers>,
|
|
404
|
+
): Promise<MeetingResult> {
|
|
405
|
+
const h = resolveHelpers(helpers);
|
|
406
|
+
try {
|
|
407
|
+
const meeting = await h.LoadMeetingEntity(meetingID, user, provider);
|
|
408
|
+
if (!meeting) {
|
|
409
|
+
return { Success: false, ErrorMessage: 'Meeting not found.' };
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
const authorized = await h.IsUserHostOrCoHost(meeting, user, provider);
|
|
413
|
+
if (!authorized) {
|
|
414
|
+
return { Success: false, ErrorMessage: 'Unauthorized: only the host or co-host may end this meeting.' };
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
const now = new Date();
|
|
418
|
+
meeting.Status = 'Ended';
|
|
419
|
+
meeting.EndedAt = now;
|
|
420
|
+
if (!(await meeting.Save())) {
|
|
421
|
+
return { Success: false, ErrorMessage: meeting.LatestResult?.CompleteMessage ?? 'Failed to update meeting status to Ended.' };
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
// 1. Stop all AI agents in room
|
|
425
|
+
try {
|
|
426
|
+
await LiveKitAgentRoomCoordinator.Instance.StopAllAgentsInRoom(meeting.RoomName, 'Explicit', user, provider);
|
|
427
|
+
} catch (err) {
|
|
428
|
+
LogError(`[MeetingResolver] Error stopping agents for room ${meeting.RoomName}: ${err instanceof Error ? err.message : String(err)}`);
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
// 2. Close Interaction record if exists
|
|
432
|
+
await h.CloseMeetingInteraction(meeting.RoomName, user, provider);
|
|
433
|
+
|
|
434
|
+
const participants = await h.LoadParticipants(meeting.ID, user, provider);
|
|
435
|
+
return {
|
|
436
|
+
Success: true,
|
|
437
|
+
Meeting: h.MapMeetingToType(meeting, participants),
|
|
438
|
+
};
|
|
439
|
+
} catch (error) {
|
|
440
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
441
|
+
LogError(`[MeetingResolver] EndMeeting exception: ${msg}`);
|
|
442
|
+
return { Success: false, ErrorMessage: msg };
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
export async function ExecuteCancelMeeting(
|
|
447
|
+
provider: IMetadataProvider,
|
|
448
|
+
user: UserInfo,
|
|
449
|
+
meetingID: string,
|
|
450
|
+
helpers?: Partial<MeetingHelpers>,
|
|
451
|
+
): Promise<MeetingResult> {
|
|
452
|
+
const h = resolveHelpers(helpers);
|
|
453
|
+
try {
|
|
454
|
+
const meeting = await h.LoadMeetingEntity(meetingID, user, provider);
|
|
455
|
+
if (!meeting) {
|
|
456
|
+
return { Success: false, ErrorMessage: 'Meeting not found.' };
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
const authorized = await h.IsUserHostOrCoHost(meeting, user, provider);
|
|
460
|
+
if (!authorized) {
|
|
461
|
+
return { Success: false, ErrorMessage: 'Unauthorized: only the host or co-host may cancel this meeting.' };
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
meeting.Status = 'Cancelled';
|
|
465
|
+
if (!(await meeting.Save())) {
|
|
466
|
+
return { Success: false, ErrorMessage: meeting.LatestResult?.CompleteMessage ?? 'Failed to cancel meeting.' };
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
const participants = await h.LoadParticipants(meeting.ID, user, provider);
|
|
470
|
+
return {
|
|
471
|
+
Success: true,
|
|
472
|
+
Meeting: h.MapMeetingToType(meeting, participants),
|
|
473
|
+
};
|
|
474
|
+
} catch (error) {
|
|
475
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
476
|
+
LogError(`[MeetingResolver] CancelMeeting exception: ${msg}`);
|
|
477
|
+
return { Success: false, ErrorMessage: msg };
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
export async function ExecuteRSVPMeeting(
|
|
482
|
+
provider: IMetadataProvider,
|
|
483
|
+
user: UserInfo,
|
|
484
|
+
input: RSVPMeetingInput,
|
|
485
|
+
helpers?: Partial<MeetingHelpers>,
|
|
486
|
+
): Promise<MeetingResult> {
|
|
487
|
+
const h = resolveHelpers(helpers);
|
|
488
|
+
try {
|
|
489
|
+
const rv = provider ? RunView.FromMetadataProvider(provider) : new RunView();
|
|
490
|
+
|
|
491
|
+
const validStatuses: MJMeetingParticipantEntity['InviteStatus'][] = ['Accepted', 'Declined', 'Tentative'];
|
|
492
|
+
const statusInput = input.InviteStatus.trim();
|
|
493
|
+
const matchedStatus = validStatuses.find((s) => s.toLowerCase() === statusInput.toLowerCase());
|
|
494
|
+
if (!matchedStatus) {
|
|
495
|
+
return { Success: false, ErrorMessage: `Invalid RSVP status: '${statusInput}'. Expected Accepted, Declined, or Tentative.` };
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
const escapedMeetingID = EscapeSQLString(input.MeetingID.trim());
|
|
499
|
+
const escapedUserID = EscapeSQLString(user.ID);
|
|
500
|
+
|
|
501
|
+
const pResult = await rv.RunView<MJMeetingParticipantEntity>(
|
|
502
|
+
{
|
|
503
|
+
EntityName: 'MJ: Meeting Participants',
|
|
504
|
+
ExtraFilter: `MeetingID = '${escapedMeetingID}' AND UserID = '${escapedUserID}'`,
|
|
505
|
+
ResultType: 'entity_object',
|
|
506
|
+
},
|
|
507
|
+
user,
|
|
508
|
+
);
|
|
509
|
+
|
|
510
|
+
if (!pResult.Success || !pResult.Results || pResult.Results.length === 0) {
|
|
511
|
+
return { Success: false, ErrorMessage: 'Participant record not found for the caller in this meeting.' };
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
const participant = pResult.Results[0];
|
|
515
|
+
participant.InviteStatus = matchedStatus;
|
|
516
|
+
if (!(await participant.Save())) {
|
|
517
|
+
return { Success: false, ErrorMessage: participant.LatestResult?.CompleteMessage ?? 'Failed to save RSVP response.' };
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
const meeting = await h.LoadMeetingEntity(input.MeetingID, user, provider);
|
|
521
|
+
if (!meeting) {
|
|
522
|
+
return { Success: false, ErrorMessage: 'Meeting not found.' };
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
const participants = await h.LoadParticipants(meeting.ID, user, provider);
|
|
526
|
+
return {
|
|
527
|
+
Success: true,
|
|
528
|
+
Meeting: h.MapMeetingToType(meeting, participants),
|
|
529
|
+
};
|
|
530
|
+
} catch (error) {
|
|
531
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
532
|
+
LogError(`[MeetingResolver] RSVPMeeting exception: ${msg}`);
|
|
533
|
+
return { Success: false, ErrorMessage: msg };
|
|
534
|
+
}
|
|
535
|
+
}
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import {
|
|
2
|
+
IMetadataProvider,
|
|
3
|
+
LogError,
|
|
4
|
+
RunView,
|
|
5
|
+
UserInfo,
|
|
6
|
+
} from '@memberjunction/core';
|
|
7
|
+
import { EscapeSQLString, UUIDsEqual } from '@memberjunction/global';
|
|
8
|
+
import type {
|
|
9
|
+
MJMeetingEntity,
|
|
10
|
+
MJMeetingParticipantEntity,
|
|
11
|
+
MJPhoneNumberEntity,
|
|
12
|
+
} from '@memberjunction/core-entities';
|
|
13
|
+
import {
|
|
14
|
+
MeetingType,
|
|
15
|
+
MeetingParticipantType,
|
|
16
|
+
DialInPhoneNumberType,
|
|
17
|
+
} from './meetingTypes.js';
|
|
18
|
+
import {
|
|
19
|
+
LoadParticipants,
|
|
20
|
+
MapMeetingToType,
|
|
21
|
+
} from './meetingHelpers.js';
|
|
22
|
+
|
|
23
|
+
export async function ExecuteMyMeetings(
|
|
24
|
+
provider: IMetadataProvider,
|
|
25
|
+
user: UserInfo,
|
|
26
|
+
status?: string,
|
|
27
|
+
): Promise<MeetingType[]> {
|
|
28
|
+
try {
|
|
29
|
+
const rv = provider ? RunView.FromMetadataProvider(provider) : new RunView();
|
|
30
|
+
const escapedUserID = EscapeSQLString(user.ID);
|
|
31
|
+
|
|
32
|
+
// 1. Find participant rows for this user
|
|
33
|
+
const participantRows = await rv.RunView<MJMeetingParticipantEntity>(
|
|
34
|
+
{
|
|
35
|
+
EntityName: 'MJ: Meeting Participants',
|
|
36
|
+
ExtraFilter: `UserID = '${escapedUserID}'`,
|
|
37
|
+
ResultType: 'entity_object',
|
|
38
|
+
},
|
|
39
|
+
user,
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
const participantMeetingIDs = new Set<string>();
|
|
43
|
+
if (participantRows.Success && participantRows.Results) {
|
|
44
|
+
for (const p of participantRows.Results) {
|
|
45
|
+
if (p.MeetingID) {
|
|
46
|
+
participantMeetingIDs.add(p.MeetingID);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// 2. Query meetings where user is host OR in participantMeetingIDs
|
|
52
|
+
let filter = `HostUserID = '${escapedUserID}'`;
|
|
53
|
+
if (participantMeetingIDs.size > 0) {
|
|
54
|
+
const idList = Array.from(participantMeetingIDs)
|
|
55
|
+
.map((id) => `'${EscapeSQLString(id)}'`)
|
|
56
|
+
.join(', ');
|
|
57
|
+
filter = `(HostUserID = '${escapedUserID}' OR ID IN (${idList}))`;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (status && status.trim()) {
|
|
61
|
+
const escapedStatus = EscapeSQLString(status.trim());
|
|
62
|
+
filter = `${filter} AND Status = '${escapedStatus}'`;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const meetingsResult = await rv.RunView<MJMeetingEntity>(
|
|
66
|
+
{
|
|
67
|
+
EntityName: 'MJ: Meetings',
|
|
68
|
+
ExtraFilter: filter,
|
|
69
|
+
OrderBy: 'ScheduledStartAt DESC, __mj_CreatedAt DESC',
|
|
70
|
+
ResultType: 'entity_object',
|
|
71
|
+
},
|
|
72
|
+
user,
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
if (!meetingsResult.Success || !meetingsResult.Results) {
|
|
76
|
+
return [];
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const output: MeetingType[] = [];
|
|
80
|
+
for (const m of meetingsResult.Results) {
|
|
81
|
+
const participants = await LoadParticipants(m.ID, user, provider);
|
|
82
|
+
output.push(MapMeetingToType(m, participants));
|
|
83
|
+
}
|
|
84
|
+
return output;
|
|
85
|
+
} catch (error) {
|
|
86
|
+
LogError(`[MeetingResolver] MyMeetings failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
87
|
+
return [];
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export async function ExecuteMeeting(
|
|
92
|
+
provider: IMetadataProvider,
|
|
93
|
+
user: UserInfo,
|
|
94
|
+
id: string,
|
|
95
|
+
): Promise<MeetingType | null> {
|
|
96
|
+
try {
|
|
97
|
+
const rv = provider ? RunView.FromMetadataProvider(provider) : new RunView();
|
|
98
|
+
const escapedID = EscapeSQLString(id.trim());
|
|
99
|
+
|
|
100
|
+
const result = await rv.RunView<MJMeetingEntity>(
|
|
101
|
+
{
|
|
102
|
+
EntityName: 'MJ: Meetings',
|
|
103
|
+
ExtraFilter: `ID = '${escapedID}'`,
|
|
104
|
+
ResultType: 'entity_object',
|
|
105
|
+
},
|
|
106
|
+
user,
|
|
107
|
+
);
|
|
108
|
+
|
|
109
|
+
if (!result.Success || !result.Results || result.Results.length === 0) {
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const meeting = result.Results[0];
|
|
114
|
+
const participants = await LoadParticipants(meeting.ID, user, provider);
|
|
115
|
+
|
|
116
|
+
// Verify authorization: caller must be host or participant
|
|
117
|
+
const isHost = UUIDsEqual(meeting.HostUserID, user.ID);
|
|
118
|
+
const isParticipant = participants.some((p) => p.UserID && UUIDsEqual(p.UserID, user.ID));
|
|
119
|
+
if (!isHost && !isParticipant) {
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return MapMeetingToType(meeting, participants);
|
|
124
|
+
} catch (error) {
|
|
125
|
+
LogError(`[MeetingResolver] Meeting query failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
126
|
+
return null;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export async function ExecuteMeetingParticipants(
|
|
131
|
+
provider: IMetadataProvider,
|
|
132
|
+
user: UserInfo,
|
|
133
|
+
meetingID: string,
|
|
134
|
+
): Promise<MeetingParticipantType[]> {
|
|
135
|
+
try {
|
|
136
|
+
return await LoadParticipants(meetingID.trim(), user, provider);
|
|
137
|
+
} catch (error) {
|
|
138
|
+
LogError(`[MeetingResolver] MeetingParticipants failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
139
|
+
return [];
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export async function ExecuteAvailableDialInPhoneNumbers(
|
|
144
|
+
provider: IMetadataProvider,
|
|
145
|
+
user: UserInfo,
|
|
146
|
+
): Promise<DialInPhoneNumberType[]> {
|
|
147
|
+
try {
|
|
148
|
+
const rv = provider ? RunView.FromMetadataProvider(provider) : new RunView();
|
|
149
|
+
|
|
150
|
+
const result = await rv.RunView<MJPhoneNumberEntity>(
|
|
151
|
+
{
|
|
152
|
+
EntityName: 'MJ: Phone Numbers',
|
|
153
|
+
ExtraFilter: "Status = 'Active'",
|
|
154
|
+
ResultType: 'entity_object',
|
|
155
|
+
},
|
|
156
|
+
user,
|
|
157
|
+
);
|
|
158
|
+
|
|
159
|
+
if (!result.Success || !result.Results) {
|
|
160
|
+
return [];
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return result.Results.map((p) => ({
|
|
164
|
+
ID: p.ID,
|
|
165
|
+
Number: p.Number,
|
|
166
|
+
Label: p.Label ?? undefined,
|
|
167
|
+
}));
|
|
168
|
+
} catch (error) {
|
|
169
|
+
LogError(`[MeetingResolver] AvailableDialInPhoneNumbers failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
170
|
+
return [];
|
|
171
|
+
}
|
|
172
|
+
}
|