Skip to content
This repository was archived by the owner on Oct 18, 2024. It is now read-only.

Commit a485fdb

Browse files
Merge pull request #740 from SuperViz/feat/api-first-participant-initialization
feat: implement api first participant initialization
2 parents 0b9f64e + a5f1850 commit a485fdb

File tree

11 files changed

+106
-73
lines changed

11 files changed

+106
-73
lines changed

__mocks__/config.mock.ts

-1
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ import { LIMITS_MOCK } from './limits.mock';
66
import { WATERMARK_MOCK } from './watermark.mock';
77

88
export const MOCK_CONFIG: Configuration = {
9-
ablyKey: 'unit-test-ably-key',
109
apiKey: 'unit-test-api-key',
1110
apiUrl: 'http://unit-test-api-url',
1211
conferenceLayerUrl: 'https://unit-test-conference-layer-url',

src/common/types/sdk-options.types.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,9 @@ export interface SuperVizSdkOptions {
1111
roomId: string;
1212
participant: {
1313
id: string;
14-
name: string;
14+
name?: string;
1515
avatar?: Avatar;
16+
email?: string;
1617
};
1718
group: Group;
1819
customColors?: ColorsVariables;

src/core/index.test.ts

+24-19
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ describe('initialization errors', () => {
8888
...SIMPLE_INITIALIZATION_MOCK,
8989
participant: undefined as unknown as SuperVizSdkOptions['participant'],
9090
}),
91-
).rejects.toThrow('Participant name and id is required');
91+
).rejects.toThrow('Participant id is required');
9292
});
9393

9494
test('should throw an error if no participant id is provided', async () => {
@@ -97,16 +97,7 @@ describe('initialization errors', () => {
9797
...SIMPLE_INITIALIZATION_MOCK,
9898
participant: { name: 'unit-test-participant-name' } as SuperVizSdkOptions['participant'],
9999
}),
100-
).rejects.toThrow('Participant name and id is required');
101-
});
102-
103-
test('should throw an error if participant name is not provided', async () => {
104-
await expect(
105-
sdk(UNIT_TEST_API_KEY, {
106-
...SIMPLE_INITIALIZATION_MOCK,
107-
participant: { id: 'unit-test-participant-id' } as SuperVizSdkOptions['participant'],
108-
}),
109-
).rejects.toThrow('Participant name and id is required');
100+
).rejects.toThrow('Participant id is required');
110101
});
111102

112103
test('should throw an error if no group name is provided', async () => {
@@ -118,14 +109,6 @@ describe('initialization errors', () => {
118109
).rejects.toThrow('Group fields is required');
119110
});
120111

121-
test('should throw an error if envoriment is invalid', async () => {
122-
ApiService.fetchConfig = jest.fn().mockResolvedValue(undefined);
123-
124-
expect(sdk(UNIT_TEST_API_KEY, SIMPLE_INITIALIZATION_MOCK)).rejects.toThrow(
125-
'Failed to load configuration from server',
126-
);
127-
});
128-
129112
test('should throw an error if custom colors variables names are invalid', async () => {
130113
const colorKey = 'invalid-color';
131114

@@ -189,4 +172,26 @@ describe('initialization errors', () => {
189172
'[SuperViz] Participant id is invalid, it should be between 2 and 64 characters and only accept letters, numbers and special characters: -_&@+=,(){}[]/«».:|\'"',
190173
);
191174
});
175+
176+
test('should throw an error if participant email is invalid', async () => {
177+
await expect(
178+
sdk(UNIT_TEST_API_KEY, {
179+
...SIMPLE_INITIALIZATION_MOCK,
180+
participant: { ...SIMPLE_INITIALIZATION_MOCK.participant, email: 'invalid-email' },
181+
}),
182+
).rejects.toThrow('[SuperViz] Participant email is invalid');
183+
});
184+
185+
test('should throw an error if participant does not exist and name is not defined', async () => {
186+
ApiService.fetchParticipant = jest.fn().mockRejectedValue({});
187+
188+
await expect(
189+
sdk(UNIT_TEST_API_KEY, {
190+
...SIMPLE_INITIALIZATION_MOCK,
191+
participant: { ...SIMPLE_INITIALIZATION_MOCK.participant, name: undefined },
192+
}),
193+
).rejects.toThrow(
194+
'[SuperViz] - Participant does not exist, create the user in the API or add the name in the initialization to initialize the SuperViz room.',
195+
);
196+
});
192197
});

src/core/index.ts

+43-22
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,11 @@ function validateId(id: string): boolean {
3131
return true;
3232
}
3333

34+
function validateEmail(email: string): boolean {
35+
const emailPattern = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/;
36+
return emailPattern.test(email);
37+
}
38+
3439
/**
3540
* @function validateOptions
3641
* @description Validate the options passed to the SDK
@@ -51,8 +56,8 @@ const validateOptions = ({
5156
throw new Error('[SuperViz] Group fields is required');
5257
}
5358

54-
if (!participant || !participant.id || !participant.name) {
55-
throw new Error('[SuperViz] Participant name and id is required');
59+
if (!participant || !participant.id) {
60+
throw new Error('[SuperViz] Participant id is required');
5661
}
5762

5863
if (!roomId) {
@@ -70,6 +75,10 @@ const validateOptions = ({
7075
'[SuperViz] Participant id is invalid, it should be between 2 and 64 characters and only accept letters, numbers and special characters: -_&@+=,(){}[]/«».:|\'"',
7176
);
7277
}
78+
79+
if (participant.email && !validateEmail(participant.email)) {
80+
throw new Error('[SuperViz] Participant email is invalid');
81+
}
7382
};
7483

7584
/**
@@ -81,13 +90,13 @@ const validateColorsVariablesNames = (colors: ColorsVariables) => {
8190
Object.entries(colors).forEach(([key, value]) => {
8291
if (!Object.values(ColorsVariablesNames).includes(key as ColorsVariablesNames)) {
8392
throw new Error(
84-
`Color ${key} is not a valid color variable name. Please check the documentation for more information.`,
93+
`[SuperViz] Color ${key} is not a valid color variable name. Please check the documentation for more information.`,
8594
);
8695
}
8796

8897
if (!/^(\d{1,3}\s){2}\d{1,3}$/.test(value)) {
8998
throw new Error(
90-
`Color ${key} is not a valid color variable value. Please check the documentation for more information.`,
99+
`[SuperViz] Color ${key} is not a valid color variable value. Please check the documentation for more information.`,
91100
);
92101
}
93102
});
@@ -140,44 +149,56 @@ const init = async (apiKey: string, options: SuperVizSdkOptions): Promise<Launch
140149
throw new Error('Failed to validate API key');
141150
}
142151

143-
const [environment, waterMark, limits] = await Promise.all([
144-
ApiService.fetchConfig(apiUrl, apiKey),
152+
const [waterMark, limits] = await Promise.all([
145153
ApiService.fetchWaterMark(apiUrl, apiKey),
146154
ApiService.fetchLimits(apiUrl, apiKey),
147155
]).catch(() => {
148-
throw new Error('Failed to load configuration from server');
156+
throw new Error('[SuperViz] Failed to load configuration from server');
149157
});
150158

151-
if (!environment || !environment.ablyKey) {
152-
throw new Error('Failed to load configuration from server');
153-
}
154-
155-
const { ablyKey } = environment;
156-
const { participant, roomId, customColors: colors } = options;
159+
const { participant, roomId, customColors } = options;
157160

158161
config.setConfig({
159162
apiUrl,
160-
ablyKey,
161163
apiKey,
162164
conferenceLayerUrl,
163165
environment: (options.environment as EnvironmentTypes) ?? EnvironmentTypes.PROD,
164166
roomId,
165167
debug: options.debug,
166168
limits,
167169
waterMark,
168-
colors: options.customColors,
170+
colors: customColors,
169171
features,
170172
});
171173

172-
setColorVariables(options.customColors);
174+
setColorVariables(customColors);
173175

174-
ApiService.createOrUpdateParticipant({
175-
name: participant.name,
176-
participantId: participant.id,
177-
avatar: participant.avatar?.imageUrl,
178-
});
176+
const apiParticipant = await ApiService.fetchParticipant(participant.id).catch(() => null);
179177

180-
return LauncherFacade(options);
178+
if (!apiParticipant && !participant.name) {
179+
throw new Error(
180+
'[SuperViz] - Participant does not exist, create the user in the API or add the name in the initialization to initialize the SuperViz room.',
181+
);
182+
}
183+
184+
if (!apiParticipant) {
185+
await ApiService.createParticipant({
186+
participantId: participant.id,
187+
name: participant?.name,
188+
avatar: participant.avatar?.imageUrl,
189+
email: participant?.email,
190+
});
191+
}
192+
193+
return LauncherFacade({
194+
...options,
195+
participant: {
196+
id: participant.id,
197+
name: participant.name ?? apiParticipant?.name,
198+
avatar: participant.avatar ?? apiParticipant?.avatar,
199+
email: participant.email ?? apiParticipant?.email,
200+
},
201+
});
181202
};
182203

183204
export default init;

src/services/api/index.test.ts

+20-17
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,14 @@ const CHECK_LIMITS_MOCK = {
1111
limits: LIMITS_MOCK,
1212
};
1313

14+
const FETCH_PARTICIPANT_MOCK = {
15+
id: 'any_user_id',
16+
name: 'any_user_name',
17+
email: null,
18+
avatar: null,
19+
createdAt: '2024-08-13T09:13:09.438Z',
20+
};
21+
1422
const FETCH_PARTICIPANTS_BY_GROUP_MOCK = [
1523
{
1624
id: 'any_user_id',
@@ -33,14 +41,6 @@ jest.mock('../../common/utils', () => {
3341
return Promise.resolve({ status: 404 });
3442
}
3543

36-
if (url.includes('/immersive-config')) {
37-
return Promise.resolve(
38-
JSON.stringify({
39-
ablyKey: MOCK_ABLY_KEY,
40-
}),
41-
);
42-
}
43-
4444
if (url.includes('/user/watermark')) {
4545
return Promise.resolve({
4646
message: true,
@@ -81,6 +81,10 @@ jest.mock('../../common/utils', () => {
8181
return Promise.resolve(FETCH_PARTICIPANTS_BY_GROUP_MOCK);
8282
}
8383

84+
if (url.includes('/participants/any_user_id') && method === 'GET') {
85+
return Promise.resolve(FETCH_PARTICIPANT_MOCK);
86+
}
87+
8488
if (url.includes('/mentions') && method === 'POST') {
8589
return Promise.resolve({});
8690
}
@@ -109,15 +113,6 @@ describe('ApiService', () => {
109113
});
110114
});
111115

112-
describe('fetchConfig', () => {
113-
test('should return the config', async () => {
114-
const baseUrl = 'https://dev.nodeapi.superviz.com';
115-
const response = await ApiService.fetchConfig(baseUrl, VALID_API_KEY);
116-
117-
expect(response).toBe(JSON.stringify({ ablyKey: MOCK_ABLY_KEY }));
118-
});
119-
});
120-
121116
describe('fetchWatermark', () => {
122117
test('should return the watermark', async () => {
123118
const baseUrl = 'https://dev.nodeapi.superviz.com';
@@ -233,4 +228,12 @@ describe('ApiService', () => {
233228
expect(response).toEqual({});
234229
});
235230
});
231+
232+
describe('fetchParticipant', () => {
233+
test('should return the participant', async () => {
234+
const response = await ApiService.fetchParticipant('any_user_id');
235+
236+
expect(response).toEqual(FETCH_PARTICIPANT_MOCK);
237+
});
238+
});
236239
});

src/services/api/index.ts

+10-3
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { ComponentLimits } from '../limits/types';
77
import {
88
AnnotationParams,
99
CommentParams,
10-
CreateOrUpdateParticipantParams,
10+
CreateParticipantParams,
1111
FetchAnnotationsParams,
1212
MentionParams,
1313
} from './types';
@@ -105,15 +105,22 @@ export default class ApiService {
105105
return doRequest(url, 'DELETE', {}, { apikey: apiKey });
106106
}
107107

108-
static async createOrUpdateParticipant(
109-
participant: CreateOrUpdateParticipantParams,
108+
static async createParticipant(
109+
participant: CreateParticipantParams,
110110
): Promise<void> {
111111
const baseUrl = config.get<string>('apiUrl');
112112
const path = '/participants';
113113
const url = this.createUrl(baseUrl, path);
114114
return doRequest(url, 'POST', { ...participant }, { apikey: config.get<string>('apiKey') });
115115
}
116116

117+
static async fetchParticipant(id: string) {
118+
const baseUrl = config.get<string>('apiUrl');
119+
const path = `/participants/${id}`;
120+
const url = this.createUrl(baseUrl, path);
121+
return doRequest(url, 'GET', undefined, { apikey: config.get<string>('apiKey') });
122+
}
123+
117124
static async sendActivity(userId: string, groupId: string, groupName: string, product: string) {
118125
const path = '/activity';
119126
const baseUrl = config.get<string>('apiUrl');

src/services/api/types.ts

+4-3
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,9 @@ export type MentionParticipantParams = {
2626
readed: number
2727
}
2828

29-
export type CreateOrUpdateParticipantParams = {
30-
name: string;
29+
export type CreateParticipantParams = {
30+
name?: string;
3131
participantId: string;
32-
avatar: string | null;
32+
avatar?: string | null;
33+
email?: string
3334
};

src/services/config/index.test.ts

+3-4
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ describe('ConfigurationService', () => {
1313

1414
describe('setConfig', () => {
1515
test('configuration should be set correctly', () => {
16-
expect(configService.get('ablyKey')).toEqual(MOCK_CONFIG.ablyKey);
1716
expect(configService.get('apiKey')).toEqual(MOCK_CONFIG.apiKey);
1817
expect(configService.get('apiUrl')).toEqual(MOCK_CONFIG.apiUrl);
1918
expect(configService.get('conferenceLayerUrl')).toEqual(MOCK_CONFIG.conferenceLayerUrl);
@@ -24,8 +23,8 @@ describe('ConfigurationService', () => {
2423

2524
describe('get', () => {
2625
test('should get a value from configuration', () => {
27-
const result = configService.get('ablyKey', 'defaultValue');
28-
expect(result).toBe(MOCK_CONFIG.ablyKey);
26+
const result = configService.get('apiKey', 'defaultValue');
27+
expect(result).toBe(MOCK_CONFIG.apiKey);
2928
});
3029

3130
test('should provide a default value if key is not found', () => {
@@ -35,7 +34,7 @@ describe('ConfigurationService', () => {
3534

3635
test('should return the default value if the config is not available', () => {
3736
configService.setConfig(null as unknown as Configuration);
38-
expect(configService.get('ablyKey')).toBeUndefined();
37+
expect(configService.get('apiKey')).toBeUndefined();
3938
});
4039
});
4140
});

src/services/config/types.ts

-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ export interface Configuration {
77
roomId: string;
88
environment: EnvironmentTypes;
99
apiKey: string;
10-
ablyKey: string;
1110
apiUrl: string;
1211
conferenceLayerUrl: string;
1312
debug: boolean;

src/services/video-conference-manager/index.ts

-1
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,6 @@ export default class VideoConfereceManager {
104104
this.frameConfig = {
105105
apiKey: config.get<string>('apiKey'),
106106
apiUrl: config.get<string>('apiUrl'),
107-
ablyKey: config.get<string>('ablyKey'),
108107
debug: config.get<boolean>('debug'),
109108
roomId: config.get<string>('roomId'),
110109
limits: config.get<ComponentLimits>('limits'),

src/services/video-conference-manager/types.ts

-1
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,6 @@ export interface FrameLocale {
6464
export interface FrameConfig {
6565
apiKey: string;
6666
apiUrl: string;
67-
ablyKey: string;
6867
roomId: string;
6968
debug: boolean;
7069
limits: ComponentLimits;

0 commit comments

Comments
 (0)