docta-package 1.0.1

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.
Files changed (59) hide show
  1. package/package.json +36 -0
  2. package/src/config.ts +25 -0
  3. package/src/dto/input/doctor.ts +120 -0
  4. package/src/dto/input/education.ts +11 -0
  5. package/src/dto/input/faq.ts +11 -0
  6. package/src/dto/input/index.ts +9 -0
  7. package/src/dto/input/language.ts +13 -0
  8. package/src/dto/input/location.ts +31 -0
  9. package/src/dto/input/patient.ts +17 -0
  10. package/src/dto/input/position.ts +21 -0
  11. package/src/dto/input/specialty.ts +40 -0
  12. package/src/dto/input/user.ts +87 -0
  13. package/src/dto/output/doctor.ts +90 -0
  14. package/src/dto/output/education.ts +11 -0
  15. package/src/dto/output/faq.ts +11 -0
  16. package/src/dto/output/index.ts +9 -0
  17. package/src/dto/output/language.ts +11 -0
  18. package/src/dto/output/location.ts +21 -0
  19. package/src/dto/output/patient.ts +49 -0
  20. package/src/dto/output/position.ts +15 -0
  21. package/src/dto/output/specialty.ts +53 -0
  22. package/src/dto/output/user.ts +57 -0
  23. package/src/enums/gender.ts +4 -0
  24. package/src/enums/index.ts +4 -0
  25. package/src/enums/language-levels.ts +9 -0
  26. package/src/enums/status-codes.ts +28 -0
  27. package/src/enums/user-role.ts +5 -0
  28. package/src/errors/BadRequestError.ts +18 -0
  29. package/src/errors/CustomError.ts +18 -0
  30. package/src/errors/NotFoundError.ts +18 -0
  31. package/src/errors/UnAuthorizedError.ts +18 -0
  32. package/src/errors/index.ts +4 -0
  33. package/src/index.ts +19 -0
  34. package/src/interfaces/LoggedInUserToken.ts +9 -0
  35. package/src/interfaces/index.ts +1 -0
  36. package/src/middleware/errorHandler.ts +31 -0
  37. package/src/middleware/index.ts +5 -0
  38. package/src/middleware/multer.ts +74 -0
  39. package/src/middleware/require-auth.ts +46 -0
  40. package/src/middleware/validate-request.ts +40 -0
  41. package/src/middleware/verify-roles.ts +17 -0
  42. package/src/models/base.ts +52 -0
  43. package/src/models/doctor.ts +96 -0
  44. package/src/models/education.ts +14 -0
  45. package/src/models/faq.ts +14 -0
  46. package/src/models/index.ts +10 -0
  47. package/src/models/language.ts +20 -0
  48. package/src/models/location.ts +24 -0
  49. package/src/models/patient.ts +35 -0
  50. package/src/models/position.ts +19 -0
  51. package/src/models/specialty.ts +37 -0
  52. package/src/models/user.ts +67 -0
  53. package/src/utils/index.ts +5 -0
  54. package/src/utils/orchestration-result.ts +74 -0
  55. package/src/utils/s3-helper.ts +72 -0
  56. package/src/utils/token-utils.ts +86 -0
  57. package/src/utils/validate-info.ts +26 -0
  58. package/src/utils/winston.ts +33 -0
  59. package/tsconfig.json +120 -0
@@ -0,0 +1,53 @@
1
+ import { ISpecialtyDocument } from "../../models";
2
+ import { UserOutputDto } from ".";
3
+
4
+ // Base DTO for everyone
5
+ export class SpecialtyOutputDto {
6
+ id: string;
7
+ en: { name: string; description: string | null };
8
+ fr: { name: string; description: string | null } | null;
9
+
10
+ isDeleted: boolean;
11
+ createdAt: number;
12
+ updatedAt: number;
13
+
14
+ constructor(specialty: ISpecialtyDocument) {
15
+ this.id = specialty.id.toString();
16
+ this.en = {
17
+ name: specialty.en.name,
18
+ description: specialty.en.description || null,
19
+ };
20
+ this.fr = specialty.fr
21
+ ? {
22
+ name: specialty.fr.name,
23
+ description: specialty.fr.description || null,
24
+ }
25
+ : null;
26
+ this.isDeleted = specialty.isDeleted;
27
+ this.createdAt = specialty.createdAt;
28
+ this.updatedAt = specialty.updatedAt;
29
+ }
30
+ }
31
+
32
+ // Extended DTO for admin responses
33
+ export class SpecialtyAdminOutputDto extends SpecialtyOutputDto {
34
+ createdBy: UserOutputDto | null;
35
+ updatedBy: UserOutputDto | null;
36
+ deletedBy: UserOutputDto | null;
37
+
38
+ constructor(specialty: ISpecialtyDocument) {
39
+ super(specialty); // call base constructor
40
+
41
+ this.createdBy = specialty.createdBy
42
+ ? new UserOutputDto(specialty.createdBy)
43
+ : null;
44
+
45
+ this.updatedBy = specialty.updatedBy
46
+ ? new UserOutputDto(specialty.updatedBy)
47
+ : null;
48
+
49
+ this.deletedBy = specialty.deletedBy
50
+ ? new UserOutputDto(specialty.deletedBy)
51
+ : null;
52
+ }
53
+ }
@@ -0,0 +1,57 @@
1
+ import { IUserDocument } from "../../models";
2
+
3
+ // Base DTO for everyone
4
+ export class UserOutputDto {
5
+ id: string;
6
+ name: string;
7
+ email: string;
8
+ role: string;
9
+ isActive: boolean;
10
+ isDeleted: boolean;
11
+ createdAt: number;
12
+ updatedAt: number;
13
+
14
+ constructor(user: IUserDocument) {
15
+ this.id = user.id.toString();
16
+ this.name = user.name;
17
+ this.email = user.email;
18
+ this.role = user.role;
19
+
20
+ this.isActive = user.isActive;
21
+ this.isDeleted = user.isDeleted;
22
+ this.createdAt = user.createdAt;
23
+ this.updatedAt = user.updatedAt;
24
+ }
25
+ }
26
+
27
+ // Extended DTO for admin responses
28
+ export class UserAdminOutputDto extends UserOutputDto {
29
+ createdBy: UserOutputDto | null;
30
+ updatedBy: UserOutputDto | null;
31
+ deletedBy: UserOutputDto | null;
32
+
33
+ constructor(user: IUserDocument) {
34
+ super(user); // call base constructor
35
+
36
+ this.createdBy = user.createdBy ? new UserOutputDto(user.createdBy) : null;
37
+ this.updatedBy = user.updatedBy ? new UserOutputDto(user.updatedBy) : null;
38
+ this.deletedBy = user.deletedBy ? new UserOutputDto(user.deletedBy) : null;
39
+ }
40
+ }
41
+
42
+ // DTO for logged-in user responses
43
+ export class LoggedInUserOutputDto {
44
+ user: UserOutputDto | UserAdminOutputDto;
45
+ accessToken: string;
46
+ refreshToken: string;
47
+
48
+ constructor(
49
+ user: UserOutputDto | UserAdminOutputDto,
50
+ accessToken: string,
51
+ refreshToken: string
52
+ ) {
53
+ this.user = user;
54
+ this.accessToken = accessToken;
55
+ this.refreshToken = refreshToken;
56
+ }
57
+ }
@@ -0,0 +1,4 @@
1
+ export enum Gender {
2
+ MALE = "MALE",
3
+ FEMALE = "FEMALE",
4
+ }
@@ -0,0 +1,4 @@
1
+ export * from "./language-levels";
2
+ export * from "./status-codes";
3
+ export * from "./user-role";
4
+ export * from "./gender";
@@ -0,0 +1,9 @@
1
+ export enum EnumLanguageLevel {
2
+ BEGINNER = "BEGINNER", // A1
3
+ ELEMENTARY = "ELEMENTARY", // A2
4
+ INTERMEDIATE = "INTERMEDIATE", // B1
5
+ UPPER_INTERMEDIATE = "UPPER_INTERMEDIATE", // B2
6
+ ADVANCED = "ADVANCED", // C1
7
+ PROFICIENT = "PROFICIENT", // C2
8
+ }
9
+
@@ -0,0 +1,28 @@
1
+ export enum EnumStatusCode {
2
+ BAD_REQUEST = "BAD_REQUEST",
3
+ SOMETHING_WENT_WRONG = "SOMETHING_WENT_WRONG",
4
+ UNAUTHORIZED = "UNAUTHORIZED",
5
+ VALIDATION_ERROR = "VALIDATION_ERROR",
6
+ EXISTS_ALREADY = "EXISTS_ALREADY",
7
+ CREATED_SUCCESSFULLY = "CREATED_SUCCESSFULLY",
8
+ UPDATED_SUCCESSFULLY = "UPDATED_SUCCESSFULLY",
9
+ NOT_FOUND = "NOT_FOUND",
10
+ SPECIALTY_NOT_FOUND = "SPECIALTY_NOT_FOUND",
11
+ NOT_ALLOWED = "NOT_ALLOWED",
12
+ DOCTOR_NOT_FOUND = "DOCTOR_NOT_FOUND",
13
+ ACCOUNT_DELETED = "ACCOUNT_DELETED",
14
+ ACCOUNT_DEACTIVATED = "ACCOUNT_DEACTIVATED",
15
+ TOKEN_REFRESHED = "TOKEN_REFRESHED",
16
+
17
+ NO_ACCESS_TOKEN = "NO_ACCESS_TOKEN",
18
+ ACCESS_TOKEN_EXPIRED = "ACCESS_TOKEN_EXPIRED",
19
+ CANNOT_DECODE_ACCESS_TOKEN = "CANNOT_DECODE_ACCESS_TOKEN",
20
+
21
+ RECOVERED_SUCCESSFULLY = "RECOVERED_SUCCESSFULLY",
22
+
23
+ FILE_TYPE_MISMATCH = "FILE_TYPE_MISMATCH",
24
+ FILE_TOO_LARGE = "FILE_TOO_LARGE",
25
+ NO_FILE_UPLOADED = "NO_FILE_UPLOADED",
26
+
27
+ DELETED_SUCCESSFULLY = "DELETED_SUCCESSFULLY",
28
+ }
@@ -0,0 +1,5 @@
1
+ export enum EnumUserRole {
2
+ PATIENT = "patient",
3
+ DOCTOR = "doctor",
4
+ ADMIN = "admin",
5
+ }
@@ -0,0 +1,18 @@
1
+ import { CustomError } from ".";
2
+ import { EnumStatusCode } from "../enums";
3
+ import { ErrorResult } from "../utils";
4
+
5
+ export class BadRequestError extends CustomError {
6
+ statusCode = 400;
7
+
8
+ constructor(
9
+ code: EnumStatusCode = EnumStatusCode.BAD_REQUEST,
10
+ message: string = "Bad Request"
11
+ ) {
12
+ super(message, code, 400);
13
+ }
14
+
15
+ serializeErrors(): ErrorResult {
16
+ return { code: this.code, message: this.message };
17
+ }
18
+ }
@@ -0,0 +1,18 @@
1
+ import { EnumStatusCode } from "../enums";
2
+ import { ErrorResult } from "../utils";
3
+
4
+ export abstract class CustomError extends Error {
5
+ public readonly code: EnumStatusCode;
6
+ public readonly statusCode: number;
7
+
8
+ constructor(message: string, code: EnumStatusCode, statusCode: number) {
9
+ super(message);
10
+ this.code = code;
11
+ this.statusCode = statusCode;
12
+
13
+ // Maintains proper prototype chain for built-in Error
14
+ Object.setPrototypeOf(this, new.target.prototype);
15
+ }
16
+
17
+ abstract serializeErrors(): ErrorResult;
18
+ }
@@ -0,0 +1,18 @@
1
+ import { CustomError } from ".";
2
+ import { EnumStatusCode } from "../enums";
3
+ import { ErrorResult } from "../utils";
4
+
5
+ export class NotFoundError extends CustomError {
6
+ statusCode = 404;
7
+
8
+ constructor(
9
+ code: EnumStatusCode = EnumStatusCode.NOT_FOUND,
10
+ message: string = "Not Found"
11
+ ) {
12
+ super(message, code, 404);
13
+ }
14
+
15
+ serializeErrors(): ErrorResult {
16
+ return { code: this.code, message: this.message };
17
+ }
18
+ }
@@ -0,0 +1,18 @@
1
+ import { CustomError } from ".";
2
+ import { EnumStatusCode } from "../enums";
3
+ import { ErrorResult } from "../utils";
4
+
5
+ export class UnAuthorizedError extends CustomError {
6
+ statusCode = 401;
7
+
8
+ constructor(
9
+ code: EnumStatusCode = EnumStatusCode.UNAUTHORIZED,
10
+ message: string = "Unauthorized"
11
+ ) {
12
+ super(message, code, 401);
13
+ }
14
+
15
+ serializeErrors(): ErrorResult {
16
+ return { code: this.code, message: this.message };
17
+ }
18
+ }
@@ -0,0 +1,4 @@
1
+ export * from "./BadRequestError";
2
+ export * from "./CustomError";
3
+ export * from "./NotFoundError";
4
+ export * from "./UnAuthorizedError";
package/src/index.ts ADDED
@@ -0,0 +1,19 @@
1
+ import "reflect-metadata";
2
+ import { LoggedInUserTokenData } from "./interfaces";
3
+
4
+ export * from "./errors";
5
+ export * from "./middleware";
6
+ export * from "./models";
7
+ export * from "./dto/input";
8
+ export * from "./dto/output";
9
+ export * from "./utils";
10
+ export * from "./config";
11
+ export * from "./interfaces";
12
+
13
+ declare global {
14
+ namespace Express {
15
+ interface Request {
16
+ currentUser?: LoggedInUserTokenData;
17
+ }
18
+ }
19
+ }
@@ -0,0 +1,9 @@
1
+ import { EnumUserRole } from "../enums";
2
+
3
+ export interface LoggedInUserTokenData {
4
+ id: string;
5
+ email: string;
6
+ role: EnumUserRole;
7
+ // doctorId?: string;
8
+ // patientId?: string;
9
+ }
@@ -0,0 +1 @@
1
+ export * from "./LoggedInUserToken";
@@ -0,0 +1,31 @@
1
+ import { NextFunction, Request, Response } from "express";
2
+ import { CustomError } from "../errors";
3
+ import { EnumStatusCode } from "../enums";
4
+ import { logger } from "../utils";
5
+
6
+ export const errorHandler = (
7
+ err: Error,
8
+ req: Request,
9
+ res: Response,
10
+ next: NextFunction
11
+ ): void => {
12
+ if (err instanceof CustomError) {
13
+ res.status(err.statusCode).json(err.serializeErrors());
14
+ return;
15
+ }
16
+
17
+ // Log the error with stack trace and request info
18
+ logger.error("Unhandled error", {
19
+ message: err.message,
20
+ stack: err.stack,
21
+ path: req.originalUrl,
22
+ method: req.method,
23
+ body: req.body,
24
+ });
25
+
26
+ console.error(err);
27
+ res.status(500).json({
28
+ code: EnumStatusCode.SOMETHING_WENT_WRONG,
29
+ message: "Internal Server Error",
30
+ });
31
+ };
@@ -0,0 +1,5 @@
1
+ export * from "./errorHandler";
2
+ export * from "./multer";
3
+ export * from "./require-auth";
4
+ export * from "./validate-request";
5
+ export * from "./verify-roles";
@@ -0,0 +1,74 @@
1
+ import multer from "multer";
2
+ import { ErrorRequestHandler } from "express";
3
+ import { BadRequestError } from "../errors";
4
+ import { EnumStatusCode } from "../enums";
5
+
6
+ // 5 MB file size limit
7
+ const fileSize = 5 * 1024 * 1024;
8
+
9
+ // Use memory storage since files are likely forwarded to S3
10
+ const storage = multer.memoryStorage();
11
+
12
+ const allowedTypes = [
13
+ "image/jpeg",
14
+ "image/png",
15
+ "image/gif",
16
+ "image/bmp",
17
+ "image/webp",
18
+ "image/tiff",
19
+ "image/svg+xml",
20
+ ];
21
+
22
+ // Base multer instance configured for images
23
+ export const imageUpload = multer({
24
+ storage,
25
+ limits: { fileSize },
26
+ fileFilter(req, file, cb) {
27
+ if (!allowedTypes.includes(file.mimetype)) {
28
+ return cb(
29
+ new BadRequestError(
30
+ EnumStatusCode.FILE_TYPE_MISMATCH,
31
+ "Only image files are allowed!"
32
+ )
33
+ );
34
+ }
35
+ cb(null, true);
36
+ },
37
+ });
38
+
39
+ // Error transformer to convert Multer errors into our CustomError format
40
+ export const transformMulterError: ErrorRequestHandler = (
41
+ err,
42
+ _req,
43
+ _res,
44
+ next
45
+ ) => {
46
+ // Multer throws MulterError for limits and other internal validations
47
+ if (err && (err as any).name === "MulterError") {
48
+ const multerErr = err as multer.MulterError;
49
+ if (multerErr.code === "LIMIT_FILE_SIZE") {
50
+ return next(
51
+ new BadRequestError(
52
+ EnumStatusCode.FILE_TOO_LARGE,
53
+ `File too large. Max ${Math.floor(fileSize / (1024 * 1024))}MB`
54
+ )
55
+ );
56
+ }
57
+ return next(
58
+ new BadRequestError(EnumStatusCode.BAD_REQUEST, multerErr.message)
59
+ );
60
+ }
61
+ // If it's already a CustomError or another error, pass it along
62
+ return next(err);
63
+ };
64
+
65
+ // Convenience helpers to use directly in routers
66
+ export const uploadSingleImage = (fieldName: string) => [
67
+ imageUpload.single(fieldName),
68
+ transformMulterError,
69
+ ];
70
+
71
+ export const uploadArrayImages = (fieldName: string, maxCount: number) => [
72
+ imageUpload.array(fieldName, maxCount),
73
+ transformMulterError,
74
+ ];
@@ -0,0 +1,46 @@
1
+ import { Request, Response, NextFunction } from "express";
2
+ import { EnumStatusCode } from "../enums";
3
+ import { UnAuthorizedError } from "../errors";
4
+ import { TokenUtils } from "../utils";
5
+
6
+ export const requireAuth = async (
7
+ req: Request,
8
+ res: Response,
9
+ next: NextFunction
10
+ ) => {
11
+ const accessToken = req.header("Authorization")?.replace("Bearer ", "");
12
+
13
+ if (!accessToken) {
14
+ throw new UnAuthorizedError(
15
+ EnumStatusCode.NO_ACCESS_TOKEN,
16
+ "No access token"
17
+ );
18
+ }
19
+
20
+ try {
21
+ const decoded = TokenUtils.verifyAccessToken(accessToken);
22
+
23
+ if (!decoded) {
24
+ throw new UnAuthorizedError(
25
+ EnumStatusCode.CANNOT_DECODE_ACCESS_TOKEN,
26
+ "Cannot decode access token"
27
+ );
28
+ }
29
+
30
+ req.currentUser = decoded;
31
+
32
+ next();
33
+ } catch (error: any) {
34
+ if (error.name === "TokenExpiredError") {
35
+ throw new UnAuthorizedError(
36
+ EnumStatusCode.ACCESS_TOKEN_EXPIRED,
37
+ "Access token has expired."
38
+ );
39
+ } else {
40
+ throw new UnAuthorizedError(
41
+ EnumStatusCode.CANNOT_DECODE_ACCESS_TOKEN,
42
+ "Cannot decode access token"
43
+ );
44
+ }
45
+ }
46
+ };
@@ -0,0 +1,40 @@
1
+ import { plainToInstance } from "class-transformer";
2
+ import { validate, ValidationError } from "class-validator";
3
+ import { Request, Response, NextFunction, RequestHandler } from "express";
4
+ import { BadRequestError } from "../errors";
5
+ import { EnumStatusCode } from "../enums";
6
+
7
+ export const validationMiddleware = (
8
+ type: any,
9
+ skipMissingProperties = false,
10
+ whitelist = true,
11
+ forbidNonWhitelisted = true
12
+ ): RequestHandler => {
13
+ return async (req: Request, res: Response, next: NextFunction) => {
14
+ try {
15
+ const object = plainToInstance(type, req.body);
16
+ const errors = await validate(object, {
17
+ skipMissingProperties,
18
+ whitelist,
19
+ forbidNonWhitelisted,
20
+ });
21
+
22
+ if (errors.length > 0) {
23
+ const message = errors
24
+ .map((error: ValidationError) =>
25
+ Object.values(error.constraints ?? {})
26
+ )
27
+ .flat()
28
+ .join(", ");
29
+ return next(
30
+ new BadRequestError(EnumStatusCode.VALIDATION_ERROR, message)
31
+ );
32
+ }
33
+
34
+ req.body = object;
35
+ next();
36
+ } catch (err) {
37
+ next(err);
38
+ }
39
+ };
40
+ };
@@ -0,0 +1,17 @@
1
+ import { Request, Response, NextFunction } from "express";
2
+ import { EnumUserRole } from "../enums/user-role";
3
+ import { UnAuthorizedError } from "../errors";
4
+ import { EnumStatusCode } from "../enums";
5
+
6
+ export const verifyRoles = (roles: EnumUserRole[]) => {
7
+ return (req: Request, res: Response, next: NextFunction) => {
8
+ if (roles.includes(req.currentUser?.role as EnumUserRole)) {
9
+ next();
10
+ } else {
11
+ throw new UnAuthorizedError(
12
+ EnumStatusCode.NOT_ALLOWED,
13
+ "You are not allowed to perform this action."
14
+ );
15
+ }
16
+ };
17
+ };
@@ -0,0 +1,52 @@
1
+ import { Schema, SchemaDefinition } from "mongoose";
2
+ import { IUserDocument } from ".";
3
+
4
+ export interface IBaseModel {
5
+ isDeleted: boolean;
6
+ createdAt: number;
7
+ updatedAt: number;
8
+ deletedAt?: number;
9
+ createdBy?: IUserDocument;
10
+ updatedBy?: IUserDocument;
11
+ deletedBy?: IUserDocument;
12
+ }
13
+
14
+ // Base Schema
15
+ export const BaseSchemaFields: SchemaDefinition = {
16
+ isDeleted: { type: Boolean, default: false },
17
+ createdAt: { type: Number, required: true, default: Date.now() },
18
+ updatedAt: { type: Number, required: true },
19
+ deletedAt: { type: Number, required: false },
20
+ createdBy: {
21
+ type: Schema.Types.ObjectId,
22
+ ref: "User",
23
+ required: false,
24
+ },
25
+ updatedBy: {
26
+ type: Schema.Types.ObjectId,
27
+ ref: "User",
28
+ required: false,
29
+ },
30
+ deletedBy: {
31
+ type: Schema.Types.ObjectId,
32
+ ref: "User",
33
+ required: false,
34
+ },
35
+ };
36
+
37
+ export function BaseSchemaPlugin(schema: Schema) {
38
+ schema.add({
39
+ isDeleted: { type: Boolean, default: false },
40
+ createdAt: { type: Number, default: () => Date.now() },
41
+ updatedAt: { type: Number, default: () => Date.now() },
42
+ deletedAt: { type: Number },
43
+ });
44
+
45
+ schema.pre("save", function () {
46
+ this.updatedAt = Date.now();
47
+ });
48
+
49
+ schema.pre("findOneAndUpdate", function () {
50
+ this.set({ updatedAt: Date.now() });
51
+ });
52
+ }
@@ -0,0 +1,96 @@
1
+ import { Schema, model, Document, Model } from "mongoose";
2
+ import { ISpecialtyDocument, SpecialtyModel } from ".";
3
+ import { IUserDocument, UserModel } from ".";
4
+ import { BaseSchemaFields, BaseSchemaPlugin, IBaseModel } from ".";
5
+ import { EducationSchema, IEducation } from ".";
6
+ import { PositionSchema, IPosition } from ".";
7
+ import { LanguageSchema, ILanguage } from ".";
8
+ import { FaqSchema, IFaq } from ".";
9
+ import { LocationSchema, ILocation } from ".";
10
+
11
+ export interface IDoctor extends IBaseModel {
12
+ name: string;
13
+ slug: string;
14
+ isActive: boolean;
15
+ user: IUserDocument;
16
+ specialty: ISpecialtyDocument;
17
+ biography: string;
18
+ consultationFee: number;
19
+ isVerified: boolean;
20
+ isVisible: boolean;
21
+ isDeactivatedByAdmin: boolean;
22
+ photo?: string;
23
+ educations: IEducation[];
24
+ positions: IPosition[];
25
+ languages: ILanguage[];
26
+ faqs: IFaq[];
27
+ expertises: string[];
28
+ location?: ILocation;
29
+ }
30
+
31
+ export interface IDoctorDocument extends IDoctor, Document {}
32
+
33
+ export interface IDoctorModel extends Model<IDoctorDocument> {}
34
+
35
+ const DoctorSchema = new Schema<IDoctorDocument>({
36
+ ...BaseSchemaFields,
37
+ user: {
38
+ type: Schema.Types.ObjectId,
39
+ ref: UserModel,
40
+ required: true,
41
+ onDelete: "cascade",
42
+ },
43
+ specialty: {
44
+ type: Schema.Types.ObjectId,
45
+ ref: SpecialtyModel,
46
+ required: true,
47
+ },
48
+ name: { type: String, required: true, trim: true },
49
+ biography: { type: String, required: false },
50
+ slug: { type: String, required: true, unique: true, trim: true },
51
+ isActive: { type: Boolean, default: false },
52
+ consultationFee: { type: Number, required: false },
53
+ isVerified: { type: Boolean, default: false },
54
+ isVisible: { type: Boolean, default: true },
55
+ isDeactivatedByAdmin: { type: Boolean, default: false },
56
+ photo: { type: String, required: false },
57
+ educations: { type: [EducationSchema], required: true, default: [] },
58
+ positions: { type: [PositionSchema], required: true, default: [] },
59
+ languages: { type: [LanguageSchema], required: true, default: [] },
60
+ faqs: { type: [FaqSchema], required: true, default: [] },
61
+ expertises: { type: [String], required: true, default: [] },
62
+ location: { type: LocationSchema, required: false },
63
+ });
64
+
65
+ const createSlug = (text: string): string =>
66
+ text
67
+ .toLowerCase()
68
+ .trim()
69
+ .replace(/[\s\W-]+/g, "-")
70
+ .replace(/^-+|-+$/g, "");
71
+
72
+ DoctorSchema.pre<IDoctorDocument>("validate", async function (next) {
73
+ // Only set slug if it hasn't been set before
74
+ if (this.slug) return next();
75
+
76
+ const baseSlug = createSlug(this.name);
77
+ let slug = baseSlug;
78
+ let counter = 0;
79
+
80
+ const Doctor = this.constructor as IDoctorModel;
81
+
82
+ while (await Doctor.exists({ slug })) {
83
+ counter += 1;
84
+ slug = `${baseSlug}-${counter}`;
85
+ }
86
+
87
+ this.slug = slug;
88
+ next();
89
+ });
90
+
91
+ DoctorSchema.plugin(BaseSchemaPlugin);
92
+
93
+ export const DoctorModel = model<IDoctorDocument, IDoctorModel>(
94
+ "Doctor",
95
+ DoctorSchema
96
+ );
@@ -0,0 +1,14 @@
1
+ import { Schema } from "mongoose";
2
+
3
+ export interface IEducation {
4
+ year: number;
5
+ title: string;
6
+ }
7
+
8
+ export const EducationSchema = new Schema<IEducation>(
9
+ {
10
+ year: { type: Number, required: true },
11
+ title: { type: String, required: true, trim: true },
12
+ },
13
+ { _id: false }
14
+ );
@@ -0,0 +1,14 @@
1
+ import { Schema } from "mongoose";
2
+
3
+ export interface IFaq {
4
+ title: string;
5
+ description: string;
6
+ }
7
+
8
+ export const FaqSchema = new Schema<IFaq>(
9
+ {
10
+ title: { type: String, required: true, trim: true },
11
+ description: { type: String, required: true, trim: true },
12
+ },
13
+ { _id: false }
14
+ );