emr-types
Version:
Comprehensive TypeScript Types Library for Electronic Medical Record (EMR) Applications - Domain-Driven Design with Zod Validation
686 lines • 27.7 kB
JavaScript
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