backend: refactor code to centralize all TS interfaces, types, enums and schemas under the models directory

This commit is contained in:
juancarmore 2025-11-19 16:39:49 +01:00
parent b711840349
commit 69df748002
45 changed files with 676 additions and 622 deletions

View File

@ -1,37 +1,6 @@
import { MeetRecordingInfo, MeetRecordingStatus } from '@openvidu-meet/typings';
import { SendDataOptions } from 'livekit-server-sdk';
const enum OpenViduComponentsDataTopic {
CHAT = 'chat',
RECORDING_STARTING = 'recordingStarting',
RECORDING_STARTED = 'recordingStarted',
RECORDING_STOPPING = 'recordingStopping',
RECORDING_STOPPED = 'recordingStopped',
RECORDING_DELETED = 'recordingDeleted',
RECORDING_FAILED = 'recordingFailed',
ROOM_STATUS = 'roomStatus'
}
interface RecordingSignalPayload {
id: string;
roomName: string;
roomId: string;
status: string;
filename?: string;
startedAt?: number;
endedAt?: number;
duration?: number;
size?: number;
location?: string;
error?: string;
}
interface RoomStatusSignalPayload {
isRecordingStarted: boolean;
recordingList: RecordingSignalPayload[];
}
export type OpenViduComponentsSignalPayload = RecordingSignalPayload | RoomStatusSignalPayload;
import { OpenViduComponentsDataTopic, RecordingSignalPayload, RoomStatusSignalPayload } from '../models/index.js';
export class OpenViduComponentsAdapterHelper {
private constructor() {

View File

@ -6,6 +6,28 @@ export class MeetRoomHelper {
// Prevent instantiation of this utility class
}
/**
* Sanitizes a room name by normalizing format.
*
* @param val The string to sanitize
* @returns A sanitized string safe for use as a room name
*/
static sanitizeRoomName(val: string): string {
return val
.trim() // Remove leading/trailing spaces
.replace(/\s+/g, ' '); // Replace multiple consecutive spaces with a single space
}
/**
* Sanitizes an identifier by removing invalid characters
*
* @param val The string to sanitize
* @returns A sanitized string safe for use as an identifier
*/
static sanitizeRoomId(val: string): string {
return val.replace(/[^a-zA-Z0-9_-]/g, ''); // Allow only letters, numbers, hyphens and underscores
}
/**
* Creates a sanitized room ID prefix from the given room name.
*

View File

@ -1,11 +1,6 @@
import { NextFunction, Request, Response } from 'express';
import { z } from 'zod';
import { rejectUnprocessableRequest } from '../../models/error.model.js';
const LoginRequestSchema = z.object({
username: z.string().min(4, 'Username must be at least 4 characters long'),
password: z.string().min(4, 'Password must be at least 4 characters long')
});
import { LoginRequestSchema } from '../../models/zod-schemas/index.js';
export const validateLoginRequest = (req: Request, res: Response, next: NextFunction) => {
const { success, error, data } = LoginRequestSchema.safeParse(req.body);

View File

@ -1,66 +1,11 @@
import {
AuthenticationConfig,
AuthMode,
AuthType,
SecurityConfig,
SingleUserAuth,
ValidAuthMethod,
WebhookConfig
} from '@openvidu-meet/typings';
import { NextFunction, Request, Response } from 'express';
import { z } from 'zod';
import { rejectUnprocessableRequest } from '../../models/error.model.js';
import { AppearanceConfigSchema } from './room-validator.middleware.js';
const WebhookConfigSchema: z.ZodType<WebhookConfig> = z
.object({
enabled: z.boolean(),
url: z
.string()
.url('Must be a valid URL')
.regex(/^https?:\/\//, { message: 'URL must start with http:// or https://' })
.optional()
})
.refine(
(data) => {
// If webhooks are enabled, URL must be provided
return !data.enabled || Boolean(data.url);
},
{
message: 'URL is required when webhooks are enabled',
path: ['url']
}
);
const WebhookTestSchema = z.object({
url: z
.string()
.url('Must be a valid URL')
.regex(/^https?:\/\//, { message: 'URL must start with http:// or https://' })
});
const AuthModeSchema: z.ZodType<AuthMode> = z.nativeEnum(AuthMode);
const AuthTypeSchema: z.ZodType<AuthType> = z.nativeEnum(AuthType);
const SingleUserAuthSchema: z.ZodType<SingleUserAuth> = z.object({
type: AuthTypeSchema
});
const ValidAuthMethodSchema: z.ZodType<ValidAuthMethod> = SingleUserAuthSchema;
const AuthenticationConfigSchema: z.ZodType<AuthenticationConfig> = z.object({
authMethod: ValidAuthMethodSchema,
authModeToAccessRoom: AuthModeSchema
});
const SecurityConfigSchema: z.ZodType<SecurityConfig> = z.object({
authentication: AuthenticationConfigSchema
});
const RoomsAppearanceConfigSchema = z.object({
appearance: AppearanceConfigSchema
});
import {
RoomsAppearanceConfigSchema,
SecurityConfigSchema,
WebhookConfigSchema,
WebhookTestSchema
} from '../../models/zod-schemas/index.js';
export const validateWebhookConfig = (req: Request, res: Response, next: NextFunction) => {
const { success, error, data } = WebhookConfigSchema.safeParse(req.body);

View File

@ -1,25 +1,7 @@
import { MeetPermissions, MeetRoomMemberRole, MeetRoomMemberTokenMetadata } from '@openvidu-meet/typings';
import { MeetRoomMemberTokenMetadata } from '@openvidu-meet/typings';
import { NextFunction, Request, Response } from 'express';
import { z } from 'zod';
import { rejectUnprocessableRequest } from '../../models/error.model.js';
const UpdateParticipantRequestSchema = z.object({
role: z.nativeEnum(MeetRoomMemberRole)
});
const MeetPermissionsSchema: z.ZodType<MeetPermissions> = z.object({
canRecord: z.boolean(),
canRetrieveRecordings: z.boolean(),
canDeleteRecordings: z.boolean(),
canChat: z.boolean(),
canChangeVirtualBackground: z.boolean()
});
const RoomMemberTokenMetadataSchema: z.ZodType<MeetRoomMemberTokenMetadata> = z.object({
livekitUrl: z.string().url('LiveKit URL must be a valid URL'),
role: z.nativeEnum(MeetRoomMemberRole),
permissions: MeetPermissionsSchema
});
import { RoomMemberTokenMetadataSchema, UpdateParticipantRequestSchema } from '../../models/zod-schemas/index.js';
export const validateUpdateParticipantRequest = (req: Request, res: Response, next: NextFunction) => {
const { success, error, data } = UpdateParticipantRequestSchema.safeParse(req.body);

View File

@ -1,141 +1,14 @@
import { MeetRecordingFilters } from '@openvidu-meet/typings';
import { NextFunction, Request, Response } from 'express';
import { z } from 'zod';
import { rejectUnprocessableRequest } from '../../models/error.model.js';
import { nonEmptySanitizedRoomId } from './room-validator.middleware.js';
const nonEmptySanitizedRecordingId = (fieldName: string) =>
z
.string()
.min(1, { message: `${fieldName} is required and cannot be empty` })
.transform((val) => {
const sanitizedValue = val.trim();
// Verify the format of the recording ID
// The recording ID should be in the format 'roomId--EG_xxx--uid'
const parts = sanitizedValue.split('--');
// If the recording ID is not in the expected format, return the sanitized value
// The next validation will check if the format is correct
if (parts.length !== 3) return sanitizedValue;
// If the recording ID is in the expected format, sanitize the roomId part
const { success, data } = nonEmptySanitizedRoomId('roomId').safeParse(parts[0]);
if (!success) {
// If the roomId part is not valid, return the sanitized value
return sanitizedValue;
}
return `${data}--${parts[1]}--${parts[2]}`;
})
.refine((data) => data !== '', {
message: `${fieldName} cannot be empty after sanitization`
})
.refine(
(data) => {
const parts = data.split('--');
if (parts.length !== 3) return false;
if (parts[0].length === 0) return false;
if (!parts[1].startsWith('EG_') || parts[1].length <= 3) return false;
if (parts[2].length === 0) return false;
return true;
},
{
message: `${fieldName} does not follow the expected format`
}
);
const StartRecordingRequestSchema = z.object({
roomId: nonEmptySanitizedRoomId('roomId')
});
const GetRecordingSchema = z.object({
params: z.object({
recordingId: nonEmptySanitizedRecordingId('recordingId')
}),
query: z.object({
secret: z.string().optional()
})
});
const MultipleRecordingIdsSchema = z.object({
recordingIds: z.preprocess(
(arg) => {
if (typeof arg === 'string') {
// If the argument is a string, it is expected to be a comma-separated list of recording IDs.
return arg
.split(',')
.map((s) => s.trim())
.filter((s) => s !== '');
}
return [];
},
z
.array(nonEmptySanitizedRecordingId('recordingId'))
.nonempty({ message: 'recordingIds must contain at least one item' })
)
});
const GetRecordingMediaSchema = z.object({
params: z.object({
recordingId: nonEmptySanitizedRecordingId('recordingId')
}),
query: z.object({
secret: z.string().optional()
}),
headers: z
.object({
range: z
.string()
.regex(/^bytes=\d+-\d*$/, {
message: 'Invalid range header format. Expected: bytes=start-end'
})
.optional()
})
.passthrough() // Allow other headers to pass through
});
const GetRecordingsFiltersSchema: z.ZodType<MeetRecordingFilters> = z.object({
maxItems: z.coerce
.number()
.positive('maxItems must be a positive number')
.transform((val) => {
// Convert the value to a number
const intVal = Math.floor(val);
// Ensure it's not greater than 100
return intVal > 100 ? 100 : intVal;
})
.default(10),
// status: z.string().optional(),
roomId: nonEmptySanitizedRoomId('roomId').optional(),
roomName: z.string().optional(),
nextPageToken: z.string().optional(),
fields: z.string().optional()
});
const GetRecordingUrlSchema = z.object({
params: z.object({
recordingId: nonEmptySanitizedRecordingId('recordingId')
}),
query: z.object({
privateAccess: z
.preprocess((val) => {
if (typeof val === 'string') {
return val.toLowerCase() === 'true';
}
return val;
}, z.boolean())
.default(false)
})
});
import {
GetRecordingMediaSchema,
GetRecordingSchema,
GetRecordingsFiltersSchema,
GetRecordingUrlSchema,
MultipleRecordingIdsSchema,
nonEmptySanitizedRecordingId,
StartRecordingRequestSchema
} from '../../models/zod-schemas/index.js';
export const withValidStartRecordingRequest = (req: Request, res: Response, next: NextFunction) => {
const { success, error, data } = StartRecordingRequestSchema.safeParse(req.body);

View File

@ -1,293 +1,15 @@
import {
MeetAppearanceConfig,
MeetChatConfig,
MeetE2EEConfig,
MeetRecordingAccess,
MeetRecordingConfig,
MeetRoomAutoDeletionPolicy,
MeetRoomConfig,
MeetRoomDeletionPolicyWithMeeting,
MeetRoomDeletionPolicyWithRecordings,
MeetRoomFilters,
MeetRoomMemberTokenOptions,
MeetRoomOptions,
MeetRoomStatus,
MeetRoomTheme,
MeetRoomThemeMode,
MeetVirtualBackgroundConfig
} from '@openvidu-meet/typings';
import { NextFunction, Request, Response } from 'express';
import ms from 'ms';
import { z } from 'zod';
import { INTERNAL_CONFIG } from '../../config/internal-config.js';
import { rejectUnprocessableRequest } from '../../models/error.model.js';
/**
* Sanitizes a room name by normalizing format.
*
* @param val The string to sanitize
* @returns A sanitized string safe for use as a room name
*/
const sanitizeRoomName = (val: string): string => {
return val
.trim() // Remove leading/trailing spaces
.replace(/\s+/g, ' '); // Replace multiple consecutive spaces with a single space
};
/**
* Sanitizes an identifier by removing invalid characters
*
* @param val The string to sanitize
* @returns A sanitized string safe for use as an identifier
*/
export const sanitizeRoomId = (val: string): string => {
return val.replace(/[^a-zA-Z0-9_-]/g, ''); // Allow only letters, numbers, hyphens and underscores
};
export const nonEmptySanitizedRoomId = (fieldName: string) =>
z
.string()
.min(1, { message: `${fieldName} is required and cannot be empty` })
.max(100, { message: `${fieldName} cannot exceed 100 characters` })
.transform(sanitizeRoomId)
.refine((data) => data !== '', {
message: `${fieldName} cannot be empty after sanitization`
});
const RecordingAccessSchema: z.ZodType<MeetRecordingAccess> = z.nativeEnum(MeetRecordingAccess);
const RecordingConfigSchema: z.ZodType<MeetRecordingConfig> = z
.object({
enabled: z.boolean(),
allowAccessTo: RecordingAccessSchema.optional()
})
.refine(
(data) => {
// If recording is enabled, allowAccessTo must be provided
return !data.enabled || data.allowAccessTo !== undefined;
},
{
message: 'allowAccessTo is required when recording is enabled',
path: ['allowAccessTo']
}
);
const ChatConfigSchema: z.ZodType<MeetChatConfig> = z.object({
enabled: z.boolean()
});
const VirtualBackgroundConfigSchema: z.ZodType<MeetVirtualBackgroundConfig> = z.object({
enabled: z.boolean()
});
const E2EEConfigSchema: z.ZodType<MeetE2EEConfig> = z.object({
enabled: z.boolean()
});
const ThemeModeSchema: z.ZodType<MeetRoomThemeMode> = z.nativeEnum(MeetRoomThemeMode);
const hexColorSchema = z
.string()
.regex(
/^#([0-9A-Fa-f]{8}|[0-9A-Fa-f]{6}|[0-9A-Fa-f]{4}|[0-9A-Fa-f]{3})$/,
'Must be a valid hex color code (with or without alpha)'
);
const RoomThemeSchema: z.ZodType<MeetRoomTheme> = z.object({
name: z
.string()
.min(1, 'Theme name cannot be empty')
.max(50, 'Theme name cannot exceed 50 characters')
.regex(/^[a-zA-Z0-9_-]+$/, 'Theme name can only contain letters, numbers, hyphens and underscores'),
enabled: z.boolean(),
baseTheme: ThemeModeSchema,
backgroundColor: hexColorSchema.optional(),
primaryColor: hexColorSchema.optional(),
secondaryColor: hexColorSchema.optional(),
accentColor: hexColorSchema.optional(),
surfaceColor: hexColorSchema.optional()
});
export const AppearanceConfigSchema: z.ZodType<MeetAppearanceConfig> = z.object({
themes: z.array(RoomThemeSchema).length(1, 'There must be exactly one theme defined')
});
const RoomConfigSchema: z.ZodType<Partial<MeetRoomConfig>> = z
.object({
recording: RecordingConfigSchema.optional().default({
enabled: true,
allowAccessTo: MeetRecordingAccess.ADMIN_MODERATOR_SPEAKER
}),
chat: ChatConfigSchema.optional().default({ enabled: true }),
virtualBackground: VirtualBackgroundConfigSchema.optional().default({ enabled: true }),
e2ee: E2EEConfigSchema.optional().default({ enabled: false })
// appearance: AppearanceConfigSchema,
})
.transform((data) => {
// Automatically disable recording when E2EE is enabled
if (data.e2ee.enabled && data.recording.enabled) {
return {
...data,
recording: {
...data.recording,
enabled: false
}
};
}
return data;
});
const RoomDeletionPolicyWithMeetingSchema: z.ZodType<MeetRoomDeletionPolicyWithMeeting> = z.nativeEnum(
MeetRoomDeletionPolicyWithMeeting
);
const RoomDeletionPolicyWithRecordingsSchema: z.ZodType<MeetRoomDeletionPolicyWithRecordings> = z.nativeEnum(
MeetRoomDeletionPolicyWithRecordings
);
const RoomAutoDeletionPolicySchema: z.ZodType<MeetRoomAutoDeletionPolicy> = z.object({
withMeeting: RoomDeletionPolicyWithMeetingSchema,
withRecordings: RoomDeletionPolicyWithRecordingsSchema
});
const RoomRequestOptionsSchema: z.ZodType<MeetRoomOptions> = z.object({
roomName: z
.string()
.max(50, 'roomName cannot exceed 50 characters')
.transform(sanitizeRoomName)
.optional()
.default('Room'),
autoDeletionDate: z
.number()
.positive('autoDeletionDate must be a positive integer')
.refine(
(date) => date >= Date.now() + ms(INTERNAL_CONFIG.MIN_FUTURE_TIME_FOR_ROOM_AUTODELETION_DATE),
`autoDeletionDate must be at least ${INTERNAL_CONFIG.MIN_FUTURE_TIME_FOR_ROOM_AUTODELETION_DATE} in the future`
)
.optional(),
autoDeletionPolicy: RoomAutoDeletionPolicySchema.optional()
.default({
withMeeting: MeetRoomDeletionPolicyWithMeeting.WHEN_MEETING_ENDS,
withRecordings: MeetRoomDeletionPolicyWithRecordings.CLOSE
})
.refine(
(policy) => {
return !policy || policy.withMeeting !== MeetRoomDeletionPolicyWithMeeting.FAIL;
},
{
message: 'FAIL policy is not allowed for withMeeting auto-deletion policy',
path: ['withMeeting']
}
)
.refine(
(policy) => {
return !policy || policy.withRecordings !== MeetRoomDeletionPolicyWithRecordings.FAIL;
},
{
message: 'FAIL policy is not allowed for withRecordings auto-deletion policy',
path: ['withRecordings']
}
),
config: RoomConfigSchema.optional().default({
recording: { enabled: true, allowAccessTo: MeetRecordingAccess.ADMIN_MODERATOR_SPEAKER },
chat: { enabled: true },
virtualBackground: { enabled: true },
e2ee: { enabled: false }
})
// maxParticipants: z
// .number()
// .positive('Max participants must be a positive integer')
// .nullable()
// .optional()
// .default(null)
});
const GetRoomFiltersSchema: z.ZodType<MeetRoomFilters> = z.object({
maxItems: z.coerce
.number()
.positive('maxItems must be a positive number')
.transform((val) => {
// Convert the value to a number
const intVal = Math.floor(val);
// Ensure it's not greater than 100
return intVal > 100 ? 100 : intVal;
})
.default(10),
nextPageToken: z.string().optional(),
roomName: z.string().optional(),
fields: z.string().optional()
});
const DeleteRoomQueryParamsSchema = z.object({
withMeeting: RoomDeletionPolicyWithMeetingSchema.optional().default(MeetRoomDeletionPolicyWithMeeting.FAIL),
withRecordings: RoomDeletionPolicyWithRecordingsSchema.optional().default(MeetRoomDeletionPolicyWithRecordings.FAIL)
});
const BulkDeleteRoomsSchema = z.object({
roomIds: z.preprocess(
(arg) => {
// First, convert input to array of strings
let roomIds: string[] = [];
if (typeof arg === 'string') {
roomIds = arg
.split(',')
.map((s) => s.trim())
.filter((s) => s !== '');
} else if (Array.isArray(arg)) {
roomIds = arg.map((item) => String(item)).filter((s) => s !== '');
}
// Apply sanitization BEFORE validation and deduplicate
// This prevents identical IDs from being processed separately
const sanitizedIds = new Set();
// Pre-sanitize to check for duplicates that would become identical
for (const id of roomIds) {
const transformed = sanitizeRoomId(id);
// Only add non-empty IDs
if (transformed !== '') {
sanitizedIds.add(transformed);
}
}
return Array.from(sanitizedIds);
},
z.array(z.string()).min(1, {
message: 'At least one valid roomId is required after sanitization'
})
),
withMeeting: RoomDeletionPolicyWithMeetingSchema.optional().default(MeetRoomDeletionPolicyWithMeeting.FAIL),
withRecordings: RoomDeletionPolicyWithRecordingsSchema.optional().default(MeetRoomDeletionPolicyWithRecordings.FAIL)
});
const UpdateRoomConfigSchema = z.object({
config: RoomConfigSchema
});
const UpdateRoomStatusSchema = z.object({
status: z.enum([MeetRoomStatus.OPEN, MeetRoomStatus.CLOSED])
});
const RoomMemberTokenRequestSchema: z.ZodType<MeetRoomMemberTokenOptions> = z
.object({
secret: z.string().nonempty('Secret is required'),
grantJoinMeetingPermission: z.boolean().optional().default(false),
participantName: z.string().optional(),
participantIdentity: z.string().optional()
})
.refine(
(data) => {
// If grantJoinMeetingPermission is true, participantName must be provided
return !data.grantJoinMeetingPermission || data.participantName;
},
{
message: 'participantName is required when grantJoinMeetingPermission is true',
path: ['participantName']
}
);
import {
BulkDeleteRoomsSchema,
DeleteRoomQueryParamsSchema,
GetRoomFiltersSchema,
nonEmptySanitizedRoomId,
RoomMemberTokenRequestSchema,
RoomRequestOptionsSchema,
UpdateRoomConfigSchema,
UpdateRoomStatusSchema
} from '../../models/zod-schemas/index.js';
export const withValidRoomOptions = (req: Request, res: Response, next: NextFunction) => {
const { success, error, data } = RoomRequestOptionsSchema.safeParse(req.body);

View File

@ -1,11 +1,6 @@
import { NextFunction, Request, Response } from 'express';
import { z } from 'zod';
import { rejectUnprocessableRequest } from '../../models/error.model.js';
const ChangePasswordRequestSchema = z.object({
currentPassword: z.string(),
newPassword: z.string().min(5, 'New password must be at least 5 characters long')
});
import { ChangePasswordRequestSchema } from '../../models/zod-schemas/index.js';
export const validateChangePasswordRequest = (req: Request, res: Response, next: NextFunction) => {
const { success, error, data } = ChangePasswordRequestSchema.safeParse(req.body);

View File

@ -1,5 +1,5 @@
import { ISchemaMigration } from '../models/migration.model.js';
import { MeetApiKeyDocument } from '../repositories/schemas/api-key.schema.js';
import { MeetApiKeyDocument } from '../models/mongoose-schemas/index.js';
/**
* All migrations for the MeetApiKey collection in chronological order.

View File

@ -1,5 +1,5 @@
import { ISchemaMigration } from '../models/migration.model.js';
import { MeetGlobalConfigDocument } from '../repositories/schemas/global-config.schema.js';
import { MeetGlobalConfigDocument } from '../models/mongoose-schemas/index.js';
/**
* All migrations for the MeetGlobalConfig collection in chronological order.

View File

@ -1,10 +1,17 @@
import { INTERNAL_CONFIG } from '../config/internal-config.js';
import { CollectionMigrationRegistry } from '../models/migration.model.js';
import { MeetApiKeyModel, meetApiKeyCollectionName } from '../repositories/schemas/api-key.schema.js';
import { MeetGlobalConfigModel, meetGlobalConfigCollectionName } from '../repositories/schemas/global-config.schema.js';
import { MeetRecordingModel, meetRecordingCollectionName } from '../repositories/schemas/recording.schema.js';
import { MeetRoomModel, meetRoomCollectionName } from '../repositories/schemas/room.schema.js';
import { MeetUserModel, meetUserCollectionName } from '../repositories/schemas/user.schema.js';
import {
MeetApiKeyModel,
MeetGlobalConfigModel,
MeetRecordingModel,
MeetRoomModel,
MeetUserModel,
meetApiKeyCollectionName,
meetGlobalConfigCollectionName,
meetRecordingCollectionName,
meetRoomCollectionName,
meetUserCollectionName
} from '../models/mongoose-schemas/index.js';
import { apiKeyMigrations } from './api-key-migrations.js';
import { globalConfigMigrations } from './global-config-migrations.js';
import { recordingMigrations } from './recording-migrations.js';

View File

@ -1,5 +1,5 @@
import { ISchemaMigration } from '../models/migration.model.js';
import { MeetRecordingDocument } from '../repositories/schemas/recording.schema.js';
import { MeetRecordingDocument } from '../models/mongoose-schemas/index.js';
/**
* All migrations for the MeetRecording collection in chronological order.

View File

@ -1,5 +1,5 @@
import { ISchemaMigration } from '../models/migration.model.js';
import { MeetRoomDocument } from '../repositories/schemas/room.schema.js';
import { MeetRoomDocument } from '../models/mongoose-schemas/index.js';
/**
* All migrations for the MeetRoom collection in chronological order.

View File

@ -1,5 +1,5 @@
import { ISchemaMigration } from '../models/migration.model.js';
import { MeetUserDocument } from '../repositories/schemas/user.schema.js';
import { MeetUserDocument } from '../models/mongoose-schemas/index.js';
/**
* All migrations for the MeetUser collection in chronological order.

View File

@ -0,0 +1,26 @@
/**
* Options for paginated find operations.
*/
export interface PaginatedFindOptions {
maxItems?: number;
nextPageToken?: string;
sortField?: string;
sortOrder?: 'asc' | 'desc';
}
/**
* Result of a paginated find operation.
*/
export interface PaginatedResult<T> {
items: T[];
isTruncated: boolean;
nextPageToken?: string;
}
/**
* Pagination cursor structure.
*/
export interface PaginationCursor {
fieldValue: unknown;
id: string;
}

View File

@ -2,3 +2,7 @@ export * from './error.model.js';
export * from './redis.model.js';
export * from './distributed-event.model.js';
export * from './migration.model.js';
export * from './ov-components-signal.model.js';
export * from './db-pagination.model.js';
export * from './task-scheduler.model.js';
export * from './request-context.model.js';

View File

@ -0,0 +1,6 @@
export * from './migration.schema.js';
export * from './global-config.schema.js';
export * from './api-key.schema.js';
export * from './user.schema.js';
export * from './room.schema.js';
export * from './recording.schema.js';

View File

@ -1,5 +1,5 @@
import { Document, model, Schema } from 'mongoose';
import { MeetMigration, MigrationName, MigrationStatus } from '../../models/index.js';
import { MeetMigration, MigrationName, MigrationStatus } from '../index.js';
/**
* Mongoose Document interface for MeetMigration.

View File

@ -0,0 +1,31 @@
export const enum OpenViduComponentsDataTopic {
CHAT = 'chat',
RECORDING_STARTING = 'recordingStarting',
RECORDING_STARTED = 'recordingStarted',
RECORDING_STOPPING = 'recordingStopping',
RECORDING_STOPPED = 'recordingStopped',
RECORDING_DELETED = 'recordingDeleted',
RECORDING_FAILED = 'recordingFailed',
ROOM_STATUS = 'roomStatus'
}
export interface RecordingSignalPayload {
id: string;
roomName: string;
roomId: string;
status: string;
filename?: string;
startedAt?: number;
endedAt?: number;
duration?: number;
size?: number;
location?: string;
error?: string;
}
export interface RoomStatusSignalPayload {
isRecordingStarted: boolean;
recordingList: RecordingSignalPayload[];
}
export type OpenViduComponentsSignalPayload = RecordingSignalPayload | RoomStatusSignalPayload;

View File

@ -0,0 +1,9 @@
import { MeetRoomMemberRoleAndPermissions, MeetUser } from '@openvidu-meet/typings';
/**
* Context information stored per HTTP request.
*/
export interface RequestContext {
user?: MeetUser;
roomMember?: MeetRoomMemberRoleAndPermissions;
}

View File

@ -0,0 +1,10 @@
import { StringValue } from 'ms';
export type TaskType = 'cron' | 'timeout';
export interface IScheduledTask {
name: string;
type: TaskType;
scheduleOrDelay: StringValue;
callback: () => Promise<void>;
}

View File

@ -0,0 +1,6 @@
import { z } from 'zod';
export const LoginRequestSchema = z.object({
username: z.string().min(4, 'Username must be at least 4 characters long'),
password: z.string().min(4, 'Password must be at least 4 characters long')
});

View File

@ -0,0 +1,61 @@
import {
AuthenticationConfig,
AuthMode,
AuthType,
SecurityConfig,
SingleUserAuth,
ValidAuthMethod,
WebhookConfig
} from '@openvidu-meet/typings';
import { z } from 'zod';
import { AppearanceConfigSchema } from './room.schema.js';
export const WebhookConfigSchema: z.ZodType<WebhookConfig> = z
.object({
enabled: z.boolean(),
url: z
.string()
.url('Must be a valid URL')
.regex(/^https?:\/\//, { message: 'URL must start with http:// or https://' })
.optional()
})
.refine(
(data) => {
// If webhooks are enabled, URL must be provided
return !data.enabled || Boolean(data.url);
},
{
message: 'URL is required when webhooks are enabled',
path: ['url']
}
);
export const WebhookTestSchema = z.object({
url: z
.string()
.url('Must be a valid URL')
.regex(/^https?:\/\//, { message: 'URL must start with http:// or https://' })
});
const AuthModeSchema: z.ZodType<AuthMode> = z.nativeEnum(AuthMode);
const AuthTypeSchema: z.ZodType<AuthType> = z.nativeEnum(AuthType);
const SingleUserAuthSchema: z.ZodType<SingleUserAuth> = z.object({
type: AuthTypeSchema
});
const ValidAuthMethodSchema: z.ZodType<ValidAuthMethod> = SingleUserAuthSchema;
const AuthenticationConfigSchema: z.ZodType<AuthenticationConfig> = z.object({
authMethod: ValidAuthMethodSchema,
authModeToAccessRoom: AuthModeSchema
});
export const SecurityConfigSchema: z.ZodType<SecurityConfig> = z.object({
authentication: AuthenticationConfigSchema
});
export const RoomsAppearanceConfigSchema = z.object({
appearance: AppearanceConfigSchema
});

View File

@ -0,0 +1,6 @@
export * from './global-config.schema.js';
export * from './auth.schema.js';
export * from './user.schema.js';
export * from './room.schema.js';
export * from './recording.schema.js';
export * from './meeting.schema.js';

View File

@ -0,0 +1,6 @@
import { MeetRoomMemberRole } from '@openvidu-meet/typings';
import { z } from 'zod';
export const UpdateParticipantRequestSchema = z.object({
role: z.nativeEnum(MeetRoomMemberRole)
});

View File

@ -0,0 +1,136 @@
import { MeetRecordingFilters } from '@openvidu-meet/typings';
import { z } from 'zod';
import { nonEmptySanitizedRoomId } from './room.schema.js';
export const nonEmptySanitizedRecordingId = (fieldName: string) =>
z
.string()
.min(1, { message: `${fieldName} is required and cannot be empty` })
.transform((val) => {
const sanitizedValue = val.trim();
// Verify the format of the recording ID
// The recording ID should be in the format 'roomId--EG_xxx--uid'
const parts = sanitizedValue.split('--');
// If the recording ID is not in the expected format, return the sanitized value
// The next validation will check if the format is correct
if (parts.length !== 3) return sanitizedValue;
// If the recording ID is in the expected format, sanitize the roomId part
const { success, data } = nonEmptySanitizedRoomId('roomId').safeParse(parts[0]);
if (!success) {
// If the roomId part is not valid, return the sanitized value
return sanitizedValue;
}
return `${data}--${parts[1]}--${parts[2]}`;
})
.refine((data) => data !== '', {
message: `${fieldName} cannot be empty after sanitization`
})
.refine(
(data) => {
const parts = data.split('--');
if (parts.length !== 3) return false;
if (parts[0].length === 0) return false;
if (!parts[1].startsWith('EG_') || parts[1].length <= 3) return false;
if (parts[2].length === 0) return false;
return true;
},
{
message: `${fieldName} does not follow the expected format`
}
);
export const StartRecordingRequestSchema = z.object({
roomId: nonEmptySanitizedRoomId('roomId')
});
export const GetRecordingSchema = z.object({
params: z.object({
recordingId: nonEmptySanitizedRecordingId('recordingId')
}),
query: z.object({
secret: z.string().optional()
})
});
export const MultipleRecordingIdsSchema = z.object({
recordingIds: z.preprocess(
(arg) => {
if (typeof arg === 'string') {
// If the argument is a string, it is expected to be a comma-separated list of recording IDs.
return arg
.split(',')
.map((s) => s.trim())
.filter((s) => s !== '');
}
return [];
},
z
.array(nonEmptySanitizedRecordingId('recordingId'))
.nonempty({ message: 'recordingIds must contain at least one item' })
)
});
export const GetRecordingMediaSchema = z.object({
params: z.object({
recordingId: nonEmptySanitizedRecordingId('recordingId')
}),
query: z.object({
secret: z.string().optional()
}),
headers: z
.object({
range: z
.string()
.regex(/^bytes=\d+-\d*$/, {
message: 'Invalid range header format. Expected: bytes=start-end'
})
.optional()
})
.passthrough() // Allow other headers to pass through
});
export const GetRecordingsFiltersSchema: z.ZodType<MeetRecordingFilters> = z.object({
maxItems: z.coerce
.number()
.positive('maxItems must be a positive number')
.transform((val) => {
// Convert the value to a number
const intVal = Math.floor(val);
// Ensure it's not greater than 100
return intVal > 100 ? 100 : intVal;
})
.default(10),
// status: z.string().optional(),
roomId: nonEmptySanitizedRoomId('roomId').optional(),
roomName: z.string().optional(),
nextPageToken: z.string().optional(),
fields: z.string().optional()
});
export const GetRecordingUrlSchema = z.object({
params: z.object({
recordingId: nonEmptySanitizedRecordingId('recordingId')
}),
query: z.object({
privateAccess: z
.preprocess((val) => {
if (typeof val === 'string') {
return val.toLowerCase() === 'true';
}
return val;
}, z.boolean())
.default(false)
})
});

View File

@ -0,0 +1,284 @@
import {
MeetAppearanceConfig,
MeetChatConfig,
MeetE2EEConfig,
MeetPermissions,
MeetRecordingAccess,
MeetRecordingConfig,
MeetRoomAutoDeletionPolicy,
MeetRoomConfig,
MeetRoomDeletionPolicyWithMeeting,
MeetRoomDeletionPolicyWithRecordings,
MeetRoomFilters,
MeetRoomMemberRole,
MeetRoomMemberTokenMetadata,
MeetRoomMemberTokenOptions,
MeetRoomOptions,
MeetRoomStatus,
MeetRoomTheme,
MeetRoomThemeMode,
MeetVirtualBackgroundConfig
} from '@openvidu-meet/typings';
import ms from 'ms';
import { z } from 'zod';
import { INTERNAL_CONFIG } from '../../config/internal-config.js';
import { MeetRoomHelper } from '../../helpers/index.js';
export const nonEmptySanitizedRoomId = (fieldName: string) =>
z
.string()
.min(1, { message: `${fieldName} is required and cannot be empty` })
.max(100, { message: `${fieldName} cannot exceed 100 characters` })
.transform(MeetRoomHelper.sanitizeRoomId)
.refine((data) => data !== '', {
message: `${fieldName} cannot be empty after sanitization`
});
const RecordingAccessSchema: z.ZodType<MeetRecordingAccess> = z.nativeEnum(MeetRecordingAccess);
const RecordingConfigSchema: z.ZodType<MeetRecordingConfig> = z
.object({
enabled: z.boolean(),
allowAccessTo: RecordingAccessSchema.optional()
})
.refine(
(data) => {
// If recording is enabled, allowAccessTo must be provided
return !data.enabled || data.allowAccessTo !== undefined;
},
{
message: 'allowAccessTo is required when recording is enabled',
path: ['allowAccessTo']
}
);
const ChatConfigSchema: z.ZodType<MeetChatConfig> = z.object({
enabled: z.boolean()
});
const VirtualBackgroundConfigSchema: z.ZodType<MeetVirtualBackgroundConfig> = z.object({
enabled: z.boolean()
});
const E2EEConfigSchema: z.ZodType<MeetE2EEConfig> = z.object({
enabled: z.boolean()
});
const ThemeModeSchema: z.ZodType<MeetRoomThemeMode> = z.nativeEnum(MeetRoomThemeMode);
const hexColorSchema = z
.string()
.regex(
/^#([0-9A-Fa-f]{8}|[0-9A-Fa-f]{6}|[0-9A-Fa-f]{4}|[0-9A-Fa-f]{3})$/,
'Must be a valid hex color code (with or without alpha)'
);
const RoomThemeSchema: z.ZodType<MeetRoomTheme> = z.object({
name: z
.string()
.min(1, 'Theme name cannot be empty')
.max(50, 'Theme name cannot exceed 50 characters')
.regex(/^[a-zA-Z0-9_-]+$/, 'Theme name can only contain letters, numbers, hyphens and underscores'),
enabled: z.boolean(),
baseTheme: ThemeModeSchema,
backgroundColor: hexColorSchema.optional(),
primaryColor: hexColorSchema.optional(),
secondaryColor: hexColorSchema.optional(),
accentColor: hexColorSchema.optional(),
surfaceColor: hexColorSchema.optional()
});
export const AppearanceConfigSchema: z.ZodType<MeetAppearanceConfig> = z.object({
themes: z.array(RoomThemeSchema).length(1, 'There must be exactly one theme defined')
});
const RoomConfigSchema: z.ZodType<Partial<MeetRoomConfig>> = z
.object({
recording: RecordingConfigSchema.optional().default({
enabled: true,
allowAccessTo: MeetRecordingAccess.ADMIN_MODERATOR_SPEAKER
}),
chat: ChatConfigSchema.optional().default({ enabled: true }),
virtualBackground: VirtualBackgroundConfigSchema.optional().default({ enabled: true }),
e2ee: E2EEConfigSchema.optional().default({ enabled: false })
// appearance: AppearanceConfigSchema,
})
.transform((data) => {
// Automatically disable recording when E2EE is enabled
if (data.e2ee.enabled && data.recording.enabled) {
return {
...data,
recording: {
...data.recording,
enabled: false
}
};
}
return data;
});
const RoomDeletionPolicyWithMeetingSchema: z.ZodType<MeetRoomDeletionPolicyWithMeeting> = z.nativeEnum(
MeetRoomDeletionPolicyWithMeeting
);
const RoomDeletionPolicyWithRecordingsSchema: z.ZodType<MeetRoomDeletionPolicyWithRecordings> = z.nativeEnum(
MeetRoomDeletionPolicyWithRecordings
);
const RoomAutoDeletionPolicySchema: z.ZodType<MeetRoomAutoDeletionPolicy> = z.object({
withMeeting: RoomDeletionPolicyWithMeetingSchema,
withRecordings: RoomDeletionPolicyWithRecordingsSchema
});
export const RoomRequestOptionsSchema: z.ZodType<MeetRoomOptions> = z.object({
roomName: z
.string()
.max(50, 'roomName cannot exceed 50 characters')
.transform(MeetRoomHelper.sanitizeRoomName)
.optional()
.default('Room'),
autoDeletionDate: z
.number()
.positive('autoDeletionDate must be a positive integer')
.refine(
(date) => date >= Date.now() + ms(INTERNAL_CONFIG.MIN_FUTURE_TIME_FOR_ROOM_AUTODELETION_DATE),
`autoDeletionDate must be at least ${INTERNAL_CONFIG.MIN_FUTURE_TIME_FOR_ROOM_AUTODELETION_DATE} in the future`
)
.optional(),
autoDeletionPolicy: RoomAutoDeletionPolicySchema.optional()
.default({
withMeeting: MeetRoomDeletionPolicyWithMeeting.WHEN_MEETING_ENDS,
withRecordings: MeetRoomDeletionPolicyWithRecordings.CLOSE
})
.refine(
(policy) => {
return !policy || policy.withMeeting !== MeetRoomDeletionPolicyWithMeeting.FAIL;
},
{
message: 'FAIL policy is not allowed for withMeeting auto-deletion policy',
path: ['withMeeting']
}
)
.refine(
(policy) => {
return !policy || policy.withRecordings !== MeetRoomDeletionPolicyWithRecordings.FAIL;
},
{
message: 'FAIL policy is not allowed for withRecordings auto-deletion policy',
path: ['withRecordings']
}
),
config: RoomConfigSchema.optional().default({
recording: { enabled: true, allowAccessTo: MeetRecordingAccess.ADMIN_MODERATOR_SPEAKER },
chat: { enabled: true },
virtualBackground: { enabled: true },
e2ee: { enabled: false }
})
// maxParticipants: z
// .number()
// .positive('Max participants must be a positive integer')
// .nullable()
// .optional()
// .default(null)
});
export const GetRoomFiltersSchema: z.ZodType<MeetRoomFilters> = z.object({
maxItems: z.coerce
.number()
.positive('maxItems must be a positive number')
.transform((val) => {
// Convert the value to a number
const intVal = Math.floor(val);
// Ensure it's not greater than 100
return intVal > 100 ? 100 : intVal;
})
.default(10),
nextPageToken: z.string().optional(),
roomName: z.string().optional(),
fields: z.string().optional()
});
export const DeleteRoomQueryParamsSchema = z.object({
withMeeting: RoomDeletionPolicyWithMeetingSchema.optional().default(MeetRoomDeletionPolicyWithMeeting.FAIL),
withRecordings: RoomDeletionPolicyWithRecordingsSchema.optional().default(MeetRoomDeletionPolicyWithRecordings.FAIL)
});
export const BulkDeleteRoomsSchema = z.object({
roomIds: z.preprocess(
(arg) => {
// First, convert input to array of strings
let roomIds: string[] = [];
if (typeof arg === 'string') {
roomIds = arg
.split(',')
.map((s) => s.trim())
.filter((s) => s !== '');
} else if (Array.isArray(arg)) {
roomIds = arg.map((item) => String(item)).filter((s) => s !== '');
}
// Apply sanitization BEFORE validation and deduplicate
// This prevents identical IDs from being processed separately
const sanitizedIds = new Set();
// Pre-sanitize to check for duplicates that would become identical
for (const id of roomIds) {
const transformed = MeetRoomHelper.sanitizeRoomId(id);
// Only add non-empty IDs
if (transformed !== '') {
sanitizedIds.add(transformed);
}
}
return Array.from(sanitizedIds);
},
z.array(z.string()).min(1, {
message: 'At least one valid roomId is required after sanitization'
})
),
withMeeting: RoomDeletionPolicyWithMeetingSchema.optional().default(MeetRoomDeletionPolicyWithMeeting.FAIL),
withRecordings: RoomDeletionPolicyWithRecordingsSchema.optional().default(MeetRoomDeletionPolicyWithRecordings.FAIL)
});
export const UpdateRoomConfigSchema = z.object({
config: RoomConfigSchema
});
export const UpdateRoomStatusSchema = z.object({
status: z.enum([MeetRoomStatus.OPEN, MeetRoomStatus.CLOSED])
});
export const RoomMemberTokenRequestSchema: z.ZodType<MeetRoomMemberTokenOptions> = z
.object({
secret: z.string().nonempty('Secret is required'),
grantJoinMeetingPermission: z.boolean().optional().default(false),
participantName: z.string().optional(),
participantIdentity: z.string().optional()
})
.refine(
(data) => {
// If grantJoinMeetingPermission is true, participantName must be provided
return !data.grantJoinMeetingPermission || data.participantName;
},
{
message: 'participantName is required when grantJoinMeetingPermission is true',
path: ['participantName']
}
);
const MeetPermissionsSchema: z.ZodType<MeetPermissions> = z.object({
canRecord: z.boolean(),
canRetrieveRecordings: z.boolean(),
canDeleteRecordings: z.boolean(),
canChat: z.boolean(),
canChangeVirtualBackground: z.boolean()
});
export const RoomMemberTokenMetadataSchema: z.ZodType<MeetRoomMemberTokenMetadata> = z.object({
livekitUrl: z.string().url('LiveKit URL must be a valid URL'),
role: z.nativeEnum(MeetRoomMemberRole),
permissions: MeetPermissionsSchema
});

View File

@ -0,0 +1,6 @@
import { z } from 'zod';
export const ChangePasswordRequestSchema = z.object({
currentPassword: z.string(),
newPassword: z.string().min(5, 'New password must be at least 5 characters long')
});

View File

@ -1,8 +1,8 @@
import { MeetApiKey } from '@openvidu-meet/typings';
import { inject, injectable } from 'inversify';
import { MeetApiKeyDocument, MeetApiKeyModel } from '../models/mongoose-schemas/index.js';
import { LoggerService } from '../services/logger.service.js';
import { BaseRepository } from './base.repository.js';
import { MeetApiKeyDocument, MeetApiKeyModel } from './schemas/api-key.schema.js';
/**
* Repository for managing MeetApiKey entities in MongoDB.

View File

@ -1,34 +1,8 @@
import { inject, injectable, unmanaged } from 'inversify';
import { Document, FilterQuery, Model, UpdateQuery } from 'mongoose';
import { PaginatedFindOptions, PaginatedResult, PaginationCursor } from '../models/index.js';
import { LoggerService } from '../services/logger.service.js';
/**
* Options for paginated find operations.
*/
export interface PaginatedFindOptions {
maxItems?: number;
nextPageToken?: string;
sortField?: string;
sortOrder?: 'asc' | 'desc';
}
/**
* Result of a paginated find operation.
*/
export interface PaginatedResult<T> {
items: T[];
isTruncated: boolean;
nextPageToken?: string;
}
/**
* Pagination cursor structure.
*/
interface PaginationCursor {
fieldValue: unknown;
id: string;
}
/**
* Base repository providing common CRUD operations for MongoDB entities.
* This class is meant to be extended by specific entity repositories.

View File

@ -1,8 +1,8 @@
import { GlobalConfig } from '@openvidu-meet/typings';
import { inject, injectable } from 'inversify';
import { MeetGlobalConfigDocument, MeetGlobalConfigModel } from '../models/mongoose-schemas/index.js';
import { LoggerService } from '../services/logger.service.js';
import { BaseRepository } from './base.repository.js';
import { MeetGlobalConfigDocument, MeetGlobalConfigModel } from './schemas/global-config.schema.js';
/**
* Repository for managing GlobalConfig in MongoDB.

View File

@ -1,8 +1,8 @@
import { inject, injectable } from 'inversify';
import { MeetMigration, MigrationName, MigrationStatus } from '../models/index.js';
import { MeetMigrationDocument, MeetMigrationModel } from '../models/mongoose-schemas/index.js';
import { LoggerService } from '../services/logger.service.js';
import { BaseRepository } from './base.repository.js';
import { MeetMigrationDocument, MeetMigrationModel } from './schemas/migration.schema.js';
@injectable()
export class MigrationRepository extends BaseRepository<MeetMigration, MeetMigrationDocument> {

View File

@ -1,9 +1,9 @@
import { MeetRecordingInfo } from '@openvidu-meet/typings';
import { inject, injectable } from 'inversify';
import { uid as secureUid } from 'uid/secure';
import { MeetRecordingDocument, MeetRecordingModel } from '../models/mongoose-schemas/index.js';
import { LoggerService } from '../services/logger.service.js';
import { BaseRepository } from './base.repository.js';
import { MeetRecordingDocument, MeetRecordingModel } from './schemas/recording.schema.js';
/**
* Repository for managing recording entities in MongoDB.

View File

@ -1,9 +1,9 @@
import { MeetRoom } from '@openvidu-meet/typings';
import { inject, injectable } from 'inversify';
import { MeetRoomDocument, MeetRoomModel } from '../models/mongoose-schemas/index.js';
import { LoggerService } from '../services/logger.service.js';
import { getBaseUrl } from '../utils/url.utils.js';
import { BaseRepository } from './base.repository.js';
import { MeetRoomDocument, MeetRoomModel } from './schemas/room.schema.js';
/**
* Repository for managing MeetRoom entities in MongoDB.

View File

@ -1,8 +1,8 @@
import { MeetUser } from '@openvidu-meet/typings';
import { inject, injectable } from 'inversify';
import { MeetUserDocument, MeetUserModel } from '../models/mongoose-schemas/index.js';
import { LoggerService } from '../services/logger.service.js';
import { BaseRepository } from './base.repository.js';
import { MeetUserDocument, MeetUserModel } from './schemas/user.schema.js';
/**
* Repository for managing MeetUser entities in MongoDB.

View File

@ -9,7 +9,8 @@ import {
} from '@openvidu-meet/typings';
import { inject, injectable } from 'inversify';
import { SendDataOptions } from 'livekit-server-sdk';
import { OpenViduComponentsAdapterHelper, OpenViduComponentsSignalPayload } from '../helpers/index.js';
import { OpenViduComponentsAdapterHelper } from '../helpers/index.js';
import { OpenViduComponentsSignalPayload } from '../models/index.js';
import { LiveKitService, LoggerService } from './index.js';
/**

View File

@ -17,6 +17,7 @@ import {
errorRecordingStartTimeout,
errorRoomHasNoParticipants,
errorRoomNotFound,
IScheduledTask,
isErrorRecordingAlreadyStopped,
isErrorRecordingCannotBeStoppedWhileStarting,
isErrorRecordingNotFound,
@ -27,7 +28,6 @@ import {
BlobStorageService,
DistributedEventService,
FrontendEventService,
IScheduledTask,
LiveKitService,
LoggerService,
MutexService,

View File

@ -1,21 +1,7 @@
import { LiveKitPermissions, MeetPermissions, MeetRoomMemberRole, MeetUser } from '@openvidu-meet/typings';
import { AsyncLocalStorage } from 'async_hooks';
import {
LiveKitPermissions,
MeetPermissions,
MeetRoomMemberRole,
MeetRoomMemberRoleAndPermissions,
MeetUser
} from '@openvidu-meet/typings';
import { injectable } from 'inversify';
/**
* Context stored per HTTP request using AsyncLocalStorage.
* This ensures that each concurrent request has its own isolated data.
*/
interface RequestContext {
user?: MeetUser;
roomMember?: MeetRoomMemberRoleAndPermissions;
}
import { RequestContext } from '../models/index.js';
/**
* Service that manages request-scoped session data using Node.js AsyncLocalStorage.

View File

@ -26,10 +26,10 @@ import {
internalError,
OpenViduMeetError
} from '../models/error.model.js';
import { IScheduledTask } from '../models/index.js';
import { RoomRepository } from '../repositories/index.js';
import {
FrontendEventService,
IScheduledTask,
LiveKitService,
LoggerService,
RecordingService,

View File

@ -3,16 +3,8 @@ import { inject, injectable } from 'inversify';
import ms from 'ms';
import { INTERNAL_CONFIG } from '../config/internal-config.js';
import { MeetLock } from '../helpers/index.js';
import { LoggerService, MutexService, DistributedEventService } from './index.js';
export type TaskType = 'cron' | 'timeout';
export interface IScheduledTask {
name: string;
type: TaskType;
scheduleOrDelay: ms.StringValue;
callback: () => Promise<void>;
}
import { IScheduledTask } from '../models/index.js';
import { DistributedEventService, LoggerService, MutexService } from './index.js';
@injectable()
export class TaskSchedulerService {