UNPKG

emr-types

Version:

Comprehensive TypeScript Types Library for Electronic Medical Record (EMR) Applications - Domain-Driven Design with Zod Validation

686 lines 27.7 kB
import { z } from 'zod'; import { PatientStatus, Gender, BloodType } from '../domains/patient'; // ============================================================================ // BASE SCHEMAS // ============================================================================ export const PatientIdSchema = z.string().uuid().brand(); export const PatientCodeSchema = z.string().min(1).max(20).brand(); export const PhoneNumberSchema = z.string().regex(/^\+?[1-9]\d{1,14}$/, { message: 'Invalid phone number format' }).brand(); // ============================================================================ // ENUM SCHEMAS // ============================================================================ export const PatientStatusSchema = z.nativeEnum(PatientStatus); export const GenderSchema = z.nativeEnum(Gender); export const BloodTypeSchema = z.nativeEnum(BloodType); // ============================================================================ // VALUE OBJECT SCHEMAS // ============================================================================ export const PatientIdValueObjectSchema = z.object({ value: PatientIdSchema, format: z.enum(['uuid', 'auto-increment', 'custom']), createdAt: z.date(), metadata: z.record(z.unknown()).optional() }); export const PatientCodeValueObjectSchema = z.object({ value: PatientCodeSchema, format: z.enum(['numeric', 'alphanumeric', 'custom']), prefix: z.string().max(10).optional(), sequence: z.number().positive(), createdAt: z.date() }); export const PhoneNumberValueObjectSchema = z.object({ value: PhoneNumberSchema, type: z.enum(['mobile', 'home', 'work', 'emergency']), isVerified: z.boolean(), verifiedAt: z.date().optional(), isPrimary: z.boolean(), createdAt: z.date() }); export const AddressValueObjectSchema = z.object({ street: z.string().min(1).max(200), city: z.string().min(1).max(100), state: z.string().min(1).max(100), postalCode: z.string().min(1).max(20), country: z.string().min(1).max(100), type: z.enum(['home', 'work', 'mailing', 'billing']), isVerified: z.boolean(), verifiedAt: z.date().optional(), isPrimary: z.boolean(), createdAt: z.date() }); // ============================================================================ // ENTITY SCHEMAS // ============================================================================ export const PatientProfileSchema = z.object({ id: PatientIdSchema, firstName: z.string().min(1).max(50), lastName: z.string().min(1).max(50), middleName: z.string().max(50).optional(), dateOfBirth: z.date(), gender: GenderSchema, bloodType: BloodTypeSchema.optional(), height: z.number().positive().optional(), // in cm weight: z.number().positive().optional(), // in kg bmi: z.number().positive().optional(), ethnicity: z.string().max(100).optional(), nationality: z.string().max(100).optional(), language: z.string().max(50).optional(), occupation: z.string().max(100).optional(), education: z.string().max(100).optional(), maritalStatus: z.enum(['single', 'married', 'divorced', 'widowed', 'separated']).optional(), emergencyContact: z.object({ name: z.string().min(1).max(100), relationship: z.string().max(50), phoneNumber: PhoneNumberSchema, email: z.string().email().optional(), address: AddressValueObjectSchema.optional() }).optional(), insurance: z.object({ provider: z.string().max(100), policyNumber: z.string().max(50), groupNumber: z.string().max(50).optional(), subscriberName: z.string().max(100), relationship: z.string().max(50), effectiveDate: z.date(), expiryDate: z.date().optional(), copay: z.number().nonnegative().optional(), deductible: z.number().nonnegative().optional() }).optional(), preferences: z.object({ communicationMethod: z.enum(['email', 'sms', 'phone', 'mail']).optional(), appointmentReminders: z.boolean().optional(), marketingConsent: z.boolean().optional(), privacyConsent: z.boolean().optional(), language: z.string().max(50).optional(), timezone: z.string().optional() }).optional(), createdAt: z.date(), updatedAt: z.date(), createdBy: z.string().uuid().optional(), updatedBy: z.string().uuid().optional() }); export const MedicalHistorySchema = z.object({ id: z.string().uuid(), patientId: PatientIdSchema, allergies: z.array(z.object({ allergen: z.string().min(1).max(100), severity: z.enum(['mild', 'moderate', 'severe']), reaction: z.string().max(500), onsetDate: z.date().optional(), isActive: z.boolean() })).optional(), medications: z.array(z.object({ name: z.string().min(1).max(100), dosage: z.string().max(50), frequency: z.string().max(50), route: z.enum(['oral', 'intravenous', 'intramuscular', 'subcutaneous', 'topical', 'inhalation']), startDate: z.date(), endDate: z.date().optional(), isActive: z.boolean(), prescribedBy: z.string().uuid().optional(), notes: z.string().max(500).optional() })).optional(), conditions: z.array(z.object({ name: z.string().min(1).max(100), icd10Code: z.string().max(20).optional(), diagnosisDate: z.date(), severity: z.enum(['mild', 'moderate', 'severe']), isActive: z.boolean(), notes: z.string().max(500).optional(), diagnosedBy: z.string().uuid().optional() })).optional(), surgeries: z.array(z.object({ procedure: z.string().min(1).max(100), date: z.date(), surgeon: z.string().max(100).optional(), hospital: z.string().max(100).optional(), notes: z.string().max(500).optional(), complications: z.string().max(500).optional() })).optional(), immunizations: z.array(z.object({ vaccine: z.string().min(1).max(100), date: z.date(), nextDueDate: z.date().optional(), administeredBy: z.string().max(100).optional(), lotNumber: z.string().max(50).optional(), notes: z.string().max(500).optional() })).optional(), familyHistory: z.array(z.object({ relationship: z.string().max(50), condition: z.string().min(1).max(100), ageOfOnset: z.number().positive().optional(), isDeceased: z.boolean().optional(), notes: z.string().max(500).optional() })).optional(), socialHistory: z.object({ smoking: z.enum(['never', 'former', 'current']).optional(), alcohol: z.enum(['never', 'occasional', 'moderate', 'heavy']).optional(), drugs: z.enum(['never', 'former', 'current']).optional(), exercise: z.enum(['none', 'light', 'moderate', 'heavy']).optional(), diet: z.string().max(100).optional(), occupation: z.string().max(100).optional(), livingSituation: z.string().max(100).optional(), notes: z.string().max(500).optional() }).optional(), createdAt: z.date(), updatedAt: z.date(), createdBy: z.string().uuid().optional(), updatedBy: z.string().uuid().optional() }); export const PatientSchema = z.object({ id: PatientIdSchema, code: PatientCodeValueObjectSchema, status: PatientStatusSchema, profile: PatientProfileSchema, medicalHistory: MedicalHistorySchema.optional(), phoneNumbers: z.array(PhoneNumberValueObjectSchema).optional(), addresses: z.array(AddressValueObjectSchema).optional(), emergencyContacts: z.array(z.object({ name: z.string().min(1).max(100), relationship: z.string().max(50), phoneNumber: PhoneNumberValueObjectSchema, email: z.string().email().optional(), address: AddressValueObjectSchema.optional(), isPrimary: z.boolean(), createdAt: z.date() })).optional(), insurance: z.array(z.object({ provider: z.string().max(100), policyNumber: z.string().max(50), groupNumber: z.string().max(50).optional(), subscriberName: z.string().max(100), relationship: z.string().max(50), effectiveDate: z.date(), expiryDate: z.date().optional(), copay: z.number().nonnegative().optional(), deductible: z.number().nonnegative().optional(), isPrimary: z.boolean(), createdAt: z.date() })).optional(), documents: z.array(z.object({ id: z.string().uuid(), type: z.enum(['id', 'insurance', 'medical', 'consent', 'other']), name: z.string().min(1).max(100), url: z.string().url(), size: z.number().positive(), mimeType: z.string(), uploadedAt: z.date(), uploadedBy: z.string().uuid().optional() })).optional(), notes: z.array(z.object({ id: z.string().uuid(), content: z.string().min(1).max(1000), type: z.enum(['general', 'clinical', 'administrative']), isPrivate: z.boolean(), createdBy: z.string().uuid(), createdAt: z.date(), updatedAt: z.date() })).optional(), tags: z.array(z.string().max(50)).optional(), createdAt: z.date(), updatedAt: z.date(), createdBy: z.string().uuid().optional(), updatedBy: z.string().uuid().optional(), deletedAt: z.date().optional() }); // ============================================================================ // REQUEST/INPUT SCHEMAS // ============================================================================ export const CreatePatientRequestSchema = z.object({ firstName: z.string().min(1).max(50), lastName: z.string().min(1).max(50), middleName: z.string().max(50).optional(), dateOfBirth: z.date(), gender: GenderSchema, bloodType: BloodTypeSchema.optional(), height: z.number().positive().optional(), weight: z.number().positive().optional(), ethnicity: z.string().max(100).optional(), nationality: z.string().max(100).optional(), language: z.string().max(50).optional(), occupation: z.string().max(100).optional(), education: z.string().max(100).optional(), maritalStatus: z.enum(['single', 'married', 'divorced', 'widowed', 'separated']).optional(), phoneNumbers: z.array(z.object({ value: z.string().regex(/^\+?[1-9]\d{1,14}$/), type: z.enum(['mobile', 'home', 'work', 'emergency']), isPrimary: z.boolean().optional() })).optional(), addresses: z.array(z.object({ street: z.string().min(1).max(200), city: z.string().min(1).max(100), state: z.string().min(1).max(100), postalCode: z.string().min(1).max(20), country: z.string().min(1).max(100), type: z.enum(['home', 'work', 'mailing', 'billing']), isPrimary: z.boolean().optional() })).optional(), emergencyContact: z.object({ name: z.string().min(1).max(100), relationship: z.string().max(50), phoneNumber: z.string().regex(/^\+?[1-9]\d{1,14}$/), email: z.string().email().optional(), address: z.object({ street: z.string().min(1).max(200), city: z.string().min(1).max(100), state: z.string().min(1).max(100), postalCode: z.string().min(1).max(20), country: z.string().min(1).max(100) }).optional() }).optional(), insurance: z.object({ provider: z.string().max(100), policyNumber: z.string().max(50), groupNumber: z.string().max(50).optional(), subscriberName: z.string().max(100), relationship: z.string().max(50), effectiveDate: z.date(), expiryDate: z.date().optional(), copay: z.number().nonnegative().optional(), deductible: z.number().nonnegative().optional() }).optional(), preferences: z.object({ communicationMethod: z.enum(['email', 'sms', 'phone', 'mail']).optional(), appointmentReminders: z.boolean().optional(), marketingConsent: z.boolean().optional(), privacyConsent: z.boolean().optional(), language: z.string().max(50).optional(), timezone: z.string().optional() }).optional(), tags: z.array(z.string().max(50)).optional() }); export const UpdatePatientRequestSchema = z.object({ firstName: z.string().min(1).max(50).optional(), lastName: z.string().min(1).max(50).optional(), middleName: z.string().max(50).optional(), dateOfBirth: z.date().optional(), gender: GenderSchema.optional(), bloodType: BloodTypeSchema.optional(), height: z.number().positive().optional(), weight: z.number().positive().optional(), ethnicity: z.string().max(100).optional(), nationality: z.string().max(100).optional(), language: z.string().max(50).optional(), occupation: z.string().max(100).optional(), education: z.string().max(100).optional(), maritalStatus: z.enum(['single', 'married', 'divorced', 'widowed', 'separated']).optional(), status: PatientStatusSchema.optional(), phoneNumbers: z.array(z.object({ value: z.string().regex(/^\+?[1-9]\d{1,14}$/), type: z.enum(['mobile', 'home', 'work', 'emergency']), isPrimary: z.boolean().optional() })).optional(), addresses: z.array(z.object({ street: z.string().min(1).max(200), city: z.string().min(1).max(100), state: z.string().min(1).max(100), postalCode: z.string().min(1).max(20), country: z.string().min(1).max(100), type: z.enum(['home', 'work', 'mailing', 'billing']), isPrimary: z.boolean().optional() })).optional(), emergencyContact: z.object({ name: z.string().min(1).max(100), relationship: z.string().max(50), phoneNumber: z.string().regex(/^\+?[1-9]\d{1,14}$/), email: z.string().email().optional(), address: z.object({ street: z.string().min(1).max(200), city: z.string().min(1).max(100), state: z.string().min(1).max(100), postalCode: z.string().min(1).max(20), country: z.string().min(1).max(100) }).optional() }).optional(), insurance: z.object({ provider: z.string().max(100), policyNumber: z.string().max(50), groupNumber: z.string().max(50).optional(), subscriberName: z.string().max(100), relationship: z.string().max(50), effectiveDate: z.date(), expiryDate: z.date().optional(), copay: z.number().nonnegative().optional(), deductible: z.number().nonnegative().optional() }).optional(), preferences: z.object({ communicationMethod: z.enum(['email', 'sms', 'phone', 'mail']).optional(), appointmentReminders: z.boolean().optional(), marketingConsent: z.boolean().optional(), privacyConsent: z.boolean().optional(), language: z.string().max(50).optional(), timezone: z.string().optional() }).optional(), tags: z.array(z.string().max(50)).optional() }); export const AddMedicalHistoryRequestSchema = z.object({ allergies: z.array(z.object({ allergen: z.string().min(1).max(100), severity: z.enum(['mild', 'moderate', 'severe']), reaction: z.string().max(500), onsetDate: z.date().optional() })).optional(), medications: z.array(z.object({ name: z.string().min(1).max(100), dosage: z.string().max(50), frequency: z.string().max(50), route: z.enum(['oral', 'intravenous', 'intramuscular', 'subcutaneous', 'topical', 'inhalation']), startDate: z.date(), endDate: z.date().optional(), prescribedBy: z.string().uuid().optional(), notes: z.string().max(500).optional() })).optional(), conditions: z.array(z.object({ name: z.string().min(1).max(100), icd10Code: z.string().max(20).optional(), diagnosisDate: z.date(), severity: z.enum(['mild', 'moderate', 'severe']), notes: z.string().max(500).optional(), diagnosedBy: z.string().uuid().optional() })).optional(), surgeries: z.array(z.object({ procedure: z.string().min(1).max(100), date: z.date(), surgeon: z.string().max(100).optional(), hospital: z.string().max(100).optional(), notes: z.string().max(500).optional(), complications: z.string().max(500).optional() })).optional(), immunizations: z.array(z.object({ vaccine: z.string().min(1).max(100), date: z.date(), nextDueDate: z.date().optional(), administeredBy: z.string().max(100).optional(), lotNumber: z.string().max(50).optional(), notes: z.string().max(500).optional() })).optional(), familyHistory: z.array(z.object({ relationship: z.string().max(50), condition: z.string().min(1).max(100), ageOfOnset: z.number().positive().optional(), isDeceased: z.boolean().optional(), notes: z.string().max(500).optional() })).optional(), socialHistory: z.object({ smoking: z.enum(['never', 'former', 'current']).optional(), alcohol: z.enum(['never', 'occasional', 'moderate', 'heavy']).optional(), drugs: z.enum(['never', 'former', 'current']).optional(), exercise: z.enum(['none', 'light', 'moderate', 'heavy']).optional(), diet: z.string().max(100).optional(), occupation: z.string().max(100).optional(), livingSituation: z.string().max(100).optional(), notes: z.string().max(500).optional() }).optional() }); export const AddDocumentRequestSchema = z.object({ type: z.enum(['id', 'insurance', 'medical', 'consent', 'other']), name: z.string().min(1).max(100), file: z.instanceof(File).optional(), url: z.string().url().optional(), notes: z.string().max(500).optional() }); export const AddNoteRequestSchema = z.object({ content: z.string().min(1).max(1000), type: z.enum(['general', 'clinical', 'administrative']), isPrivate: z.boolean().optional() }); // ============================================================================ // RESPONSE SCHEMAS // ============================================================================ export const PatientResponseSchema = z.object({ id: PatientIdSchema, code: z.string(), status: PatientStatusSchema, profile: z.object({ firstName: z.string(), lastName: z.string(), middleName: z.string().optional(), dateOfBirth: z.date(), gender: GenderSchema, bloodType: BloodTypeSchema.optional(), height: z.number().positive().optional(), weight: z.number().positive().optional(), bmi: z.number().positive().optional(), ethnicity: z.string().optional(), nationality: z.string().optional(), language: z.string().optional(), occupation: z.string().optional(), education: z.string().optional(), maritalStatus: z.enum(['single', 'married', 'divorced', 'widowed', 'separated']).optional() }), phoneNumbers: z.array(z.object({ value: z.string(), type: z.enum(['mobile', 'home', 'work', 'emergency']), isVerified: z.boolean(), isPrimary: z.boolean() })).optional(), addresses: z.array(z.object({ street: z.string(), city: z.string(), state: z.string(), postalCode: z.string(), country: z.string(), type: z.enum(['home', 'work', 'mailing', 'billing']), isVerified: z.boolean(), isPrimary: z.boolean() })).optional(), emergencyContact: z.object({ name: z.string(), relationship: z.string(), phoneNumber: z.string(), email: z.string().email().optional() }).optional(), insurance: z.object({ provider: z.string(), policyNumber: z.string(), groupNumber: z.string().optional(), subscriberName: z.string(), relationship: z.string(), effectiveDate: z.date(), expiryDate: z.date().optional() }).optional(), createdAt: z.date(), updatedAt: z.date() }); export const PatientListResponseSchema = z.object({ patients: z.array(PatientResponseSchema), pagination: z.object({ page: z.number().positive(), limit: z.number().positive(), total: z.number().nonnegative(), totalPages: z.number().nonnegative() }) }); export const PatientDetailResponseSchema = z.object({ patient: PatientResponseSchema, medicalHistory: z.object({ allergies: z.array(z.object({ allergen: z.string(), severity: z.enum(['mild', 'moderate', 'severe']), reaction: z.string(), isActive: z.boolean() })).optional(), medications: z.array(z.object({ name: z.string(), dosage: z.string(), frequency: z.string(), isActive: z.boolean() })).optional(), conditions: z.array(z.object({ name: z.string(), icd10Code: z.string().optional(), diagnosisDate: z.date(), severity: z.enum(['mild', 'moderate', 'severe']), isActive: z.boolean() })).optional(), surgeries: z.array(z.object({ procedure: z.string(), date: z.date(), surgeon: z.string().optional(), hospital: z.string().optional() })).optional(), immunizations: z.array(z.object({ vaccine: z.string(), date: z.date(), nextDueDate: z.date().optional() })).optional(), familyHistory: z.array(z.object({ relationship: z.string(), condition: z.string(), ageOfOnset: z.number().positive().optional(), isDeceased: z.boolean().optional() })).optional(), socialHistory: z.object({ smoking: z.enum(['never', 'former', 'current']).optional(), alcohol: z.enum(['never', 'occasional', 'moderate', 'heavy']).optional(), drugs: z.enum(['never', 'former', 'current']).optional(), exercise: z.enum(['none', 'light', 'moderate', 'heavy']).optional(), diet: z.string().optional(), occupation: z.string().optional(), livingSituation: z.string().optional() }).optional() }).optional(), documents: z.array(z.object({ id: z.string().uuid(), type: z.enum(['id', 'insurance', 'medical', 'consent', 'other']), name: z.string(), url: z.string().url(), size: z.number().positive(), uploadedAt: z.date() })).optional(), notes: z.array(z.object({ id: z.string().uuid(), content: z.string(), type: z.enum(['general', 'clinical', 'administrative']), isPrivate: z.boolean(), createdBy: z.string().uuid(), createdAt: z.date() })).optional(), tags: z.array(z.string()).optional() }); // ============================================================================ // FILTER/SEARCH SCHEMAS // ============================================================================ export const PatientSearchFiltersSchema = z.object({ query: z.string().optional(), status: PatientStatusSchema.optional(), gender: GenderSchema.optional(), bloodType: BloodTypeSchema.optional(), ageRange: z.object({ min: z.number().positive().optional(), max: z.number().positive().optional() }).optional(), dateOfBirthAfter: z.date().optional(), dateOfBirthBefore: z.date().optional(), hasInsurance: z.boolean().optional(), hasEmergencyContact: z.boolean().optional(), hasAllergies: z.boolean().optional(), hasActiveMedications: z.boolean().optional(), hasActiveConditions: z.boolean().optional(), tags: z.array(z.string()).optional(), createdAfter: z.date().optional(), createdBefore: z.date().optional() }); export const PatientSortOptionsSchema = z.enum([ 'createdAt', 'updatedAt', 'firstName', 'lastName', 'dateOfBirth', 'status', 'code' ]); // ============================================================================ // VALIDATION UTILITIES // ============================================================================ export const PatientValidationUtils = { isValidPhoneNumber: (phone) => { return /^\+?[1-9]\d{1,14}$/.test(phone); }, calculateAge: (dateOfBirth) => { const today = new Date(); let age = today.getFullYear() - dateOfBirth.getFullYear(); const monthDiff = today.getMonth() - dateOfBirth.getMonth(); if (monthDiff < 0 || (monthDiff === 0 && today.getDate() < dateOfBirth.getDate())) { age--; } return age; }, calculateBMI: (height, weight) => { // height in cm, weight in kg const heightInMeters = height / 100; return weight / (heightInMeters * heightInMeters); }, getBMICategory: (bmi) => { if (bmi < 18.5) return 'underweight'; if (bmi < 25) return 'normal'; if (bmi < 30) return 'overweight'; return 'obese'; }, validatePatientData: (data) => { const errors = []; if (data.dateOfBirth && new Date(data.dateOfBirth) > new Date()) { errors.push('Date of birth cannot be in the future'); } if (data.height && (data.height < 50 || data.height > 300)) { errors.push('Height must be between 50 and 300 cm'); } if (data.weight && (data.weight < 1 || data.weight > 500)) { errors.push('Weight must be between 1 and 500 kg'); } if (data.phoneNumbers) { data.phoneNumbers.forEach((phone, index) => { if (!PatientValidationUtils.isValidPhoneNumber(phone.value)) { errors.push(`Invalid phone number at index ${index}`); } }); } return { isValid: errors.length === 0, errors }; } }; // ============================================================================ // SCHEMA EXPORTS // ============================================================================ export const PatientSchemas = { // Base schemas PatientId: PatientIdSchema, PatientCode: PatientCodeSchema, PhoneNumber: PhoneNumberSchema, // Enum schemas PatientStatus: PatientStatusSchema, Gender: GenderSchema, BloodType: BloodTypeSchema, // Value object schemas PatientIdValueObject: PatientIdValueObjectSchema, PatientCodeValueObject: PatientCodeValueObjectSchema, PhoneNumberValueObject: PhoneNumberValueObjectSchema, AddressValueObject: AddressValueObjectSchema, // Entity schemas Patient: PatientSchema, PatientProfile: PatientProfileSchema, MedicalHistory: MedicalHistorySchema, // Request schemas CreatePatientRequest: CreatePatientRequestSchema, UpdatePatientRequest: UpdatePatientRequestSchema, AddMedicalHistoryRequest: AddMedicalHistoryRequestSchema, AddDocumentRequest: AddDocumentRequestSchema, AddNoteRequest: AddNoteRequestSchema, // Response schemas PatientResponse: PatientResponseSchema, PatientListResponse: PatientListResponseSchema, PatientDetailResponse: PatientDetailResponseSchema, // Filter schemas PatientSearchFilters: PatientSearchFiltersSchema, PatientSortOptions: PatientSortOptionsSchema, // Utilities ValidationUtils: PatientValidationUtils }; //# sourceMappingURL=patient-schemas.js.map