Implement medicine library and cabinet

This commit is contained in:
Aerilyn Weber 2026-03-28 08:19:48 +09:00
parent db79af06f7
commit 1f66fab30f
72 changed files with 7642 additions and 319 deletions

View file

@ -0,0 +1,16 @@
import { describe, it, expect } from 'vitest';
import { CabinetItemStatus } from './cabinet.enums.js';
describe(CabinetItemStatus.name, () => {
it('has exactly 3 values', () => {
expect(Object.values(CabinetItemStatus)).toHaveLength(3);
});
it.each([
['ACTIVE', 'active'],
['DEPLETED', 'depleted'],
['EXPIRED', 'expired'],
])('%s = %s', (key, value) => {
expect(CabinetItemStatus[key as keyof typeof CabinetItemStatus]).toBe(value);
});
});

View file

@ -0,0 +1,5 @@
export enum CabinetItemStatus {
ACTIVE = 'active',
DEPLETED = 'depleted',
EXPIRED = 'expired',
}

View file

@ -1 +1,3 @@
export * from './roles.enums.js';
export * from './medicine.enums.js';
export * from './cabinet.enums.js';

View file

@ -0,0 +1,115 @@
import { describe, it, expect } from 'vitest';
import {
MedicineForm,
StrengthUnit,
MedicineCategory,
MedicineProductSource,
DosageUnit,
ConcentrationUnit,
allowedUnitsForForm,
defaultUnitForForm,
} from './medicine.enums.js';
describe(MedicineForm.name, () => {
it('has expected values', () => {
expect(MedicineForm.TABLET).toBe('tablet');
expect(MedicineForm.CAPSULE).toBe('capsule');
expect(MedicineForm.LIQUID).toBe('liquid');
expect(MedicineForm.INJECTION).toBe('injection');
expect(MedicineForm.OTHER).toBe('other');
});
it('has exactly 5 forms', () => {
expect(Object.values(MedicineForm)).toHaveLength(5);
});
});
describe(StrengthUnit.name, () => {
it('has expected values', () => {
expect(StrengthUnit.MG).toBe('mg');
expect(StrengthUnit.IU).toBe('IU');
expect(StrengthUnit.PERCENT).toBe('%');
});
it('has exactly 7 units', () => {
expect(Object.values(StrengthUnit)).toHaveLength(7);
});
});
describe(MedicineCategory.name, () => {
it('has expected values', () => {
expect(MedicineCategory.PRESCRIPTION).toBe('prescription');
expect(MedicineCategory.OTC).toBe('otc');
expect(MedicineCategory.SUPPLEMENT).toBe('supplement');
});
it('has exactly 4 categories', () => {
expect(Object.values(MedicineCategory)).toHaveLength(4);
});
});
describe(MedicineProductSource.name, () => {
it('has expected values', () => {
expect(MedicineProductSource.MANUAL).toBe('manual');
expect(MedicineProductSource.IMPORT).toBe('import');
});
it('has exactly 2 sources', () => {
expect(Object.values(MedicineProductSource)).toHaveLength(2);
});
});
describe(DosageUnit.name, () => {
it('has expected values', () => {
expect(DosageUnit.TABLET).toBe('tablet');
expect(DosageUnit.CAPSULE).toBe('capsule');
expect(DosageUnit.ML).toBe('ml');
expect(DosageUnit.VIAL).toBe('vial');
expect(DosageUnit.DOSE).toBe('dose');
});
it('has exactly 5 units', () => {
expect(Object.values(DosageUnit)).toHaveLength(5);
});
});
describe(ConcentrationUnit.name, () => {
it('has expected values', () => {
expect(ConcentrationUnit.MG_PER_ML).toBe('mg/mL');
expect(ConcentrationUnit.MCG_PER_ML).toBe('mcg/mL');
expect(ConcentrationUnit.UNITS_PER_ML).toBe('units/mL');
});
it('has exactly 3 units', () => {
expect(Object.values(ConcentrationUnit)).toHaveLength(3);
});
});
describe('allowedUnitsForForm', () => {
it('returns tablet for tablet form', () => {
expect(allowedUnitsForForm(MedicineForm.TABLET)).toEqual([DosageUnit.TABLET]);
});
it('returns capsule for capsule form', () => {
expect(allowedUnitsForForm(MedicineForm.CAPSULE)).toEqual([DosageUnit.CAPSULE]);
});
it('returns ml for liquid form', () => {
expect(allowedUnitsForForm(MedicineForm.LIQUID)).toEqual([DosageUnit.ML]);
});
it('returns vial and ml for injection form', () => {
expect(allowedUnitsForForm(MedicineForm.INJECTION)).toEqual([DosageUnit.VIAL, DosageUnit.ML]);
});
it('returns dose for other form', () => {
expect(allowedUnitsForForm(MedicineForm.OTHER)).toEqual([DosageUnit.DOSE]);
});
});
describe('defaultUnitForForm', () => {
it('returns the first allowed unit', () => {
expect(defaultUnitForForm(MedicineForm.TABLET)).toBe(DosageUnit.TABLET);
expect(defaultUnitForForm(MedicineForm.INJECTION)).toBe(DosageUnit.VIAL);
});
});

View file

@ -0,0 +1,64 @@
export enum MedicineForm {
TABLET = 'tablet',
CAPSULE = 'capsule',
LIQUID = 'liquid',
INJECTION = 'injection',
OTHER = 'other',
}
export enum StrengthUnit {
MG = 'mg',
MCG = 'mcg',
G = 'g',
ML = 'ml',
IU = 'IU',
PERCENT = '%',
OTHER = 'other',
}
export enum MedicineCategory {
PRESCRIPTION = 'prescription',
OTC = 'otc',
SUPPLEMENT = 'supplement',
OTHER = 'other',
}
export enum MedicineProductSource {
MANUAL = 'manual',
IMPORT = 'import',
}
export enum DosageUnit {
TABLET = 'tablet',
CAPSULE = 'capsule',
ML = 'ml',
VIAL = 'vial',
DOSE = 'dose',
}
export enum ConcentrationUnit {
MG_PER_ML = 'mg/mL',
MCG_PER_ML = 'mcg/mL',
UNITS_PER_ML = 'units/mL',
}
/** Returns the allowed DosageUnit values for a given MedicineForm. */
export function allowedUnitsForForm(form: MedicineForm): DosageUnit[] {
switch (form) {
case MedicineForm.TABLET:
return [DosageUnit.TABLET];
case MedicineForm.CAPSULE:
return [DosageUnit.CAPSULE];
case MedicineForm.LIQUID:
return [DosageUnit.ML];
case MedicineForm.INJECTION:
return [DosageUnit.VIAL, DosageUnit.ML];
case MedicineForm.OTHER:
return [DosageUnit.DOSE];
}
}
/** Returns the default DosageUnit for a given MedicineForm. */
export function defaultUnitForForm(form: MedicineForm): DosageUnit {
return allowedUnitsForForm(form)[0];
}

View file

@ -0,0 +1,41 @@
import type { CabinetItemStatus } from '../enums/cabinet.enums.js';
import type {
ConcentrationUnit,
DosageUnit,
MedicineForm,
StrengthUnit,
} from '../enums/medicine.enums.js';
export interface CabinetItem {
id: string;
householdId: string;
medicineId: string;
medicineName: string;
medicineStrength: number;
medicineStrengthUnit: StrengthUnit;
medicineForm: MedicineForm;
medicineProductId?: string;
medicineProductBrand?: string;
concentration?: number;
concentrationUnit?: ConcentrationUnit;
quantity: number;
unit: DosageUnit;
expirationDate?: Date;
status: CabinetItemStatus;
notes?: string;
createdBy: string;
createdAt: Date;
updatedAt: Date;
}
export interface CabinetSummary {
medicineId: string;
medicineName: string;
medicineStrength: number;
medicineStrengthUnit: string;
medicineForm: string;
totalQuantity: number;
unit: string;
earliestExpiry: string | null;
itemCount: number;
}

View file

@ -1,3 +1,5 @@
export * from './user.js';
export * from './household.js';
export * from './common.js';
export * from './medicine.js';
export * from './cabinet.js';

View file

@ -0,0 +1,42 @@
import type {
MedicineForm,
StrengthUnit,
MedicineCategory,
MedicineProductSource,
DosageUnit,
ConcentrationUnit,
} from '../enums/medicine.enums.js';
export interface Medicine {
id: string;
householdId: string;
name: string;
form: MedicineForm;
strength: number;
strengthUnit: StrengthUnit;
category: MedicineCategory;
notes?: string;
tags: string[];
createdBy: string;
createdAt: Date;
updatedAt: Date;
}
export interface MedicineProduct {
id: string;
householdId: string;
medicineId: string;
medicineName: string;
brand: string;
manufacturer?: string;
packageSize: number;
packageUnit: DosageUnit;
concentration?: number;
concentrationUnit?: ConcentrationUnit;
imageUrl?: string;
notes?: string;
source: MedicineProductSource;
createdBy: string;
createdAt: Date;
updatedAt: Date;
}

View file

@ -0,0 +1,137 @@
import { describe, it, expect } from 'vitest';
import { DosageUnit } from '../enums/medicine.enums.js';
import { CabinetItemStatus } from '../enums/cabinet.enums.js';
import {
CreateCabinetItemSchema,
UpdateCabinetItemSchema,
AdjustQuantitySchema,
CabinetQuerySchema,
} from './cabinet.schemas.js';
describe('CreateCabinetItemSchema', () => {
const validItem = {
medicineId: 'med-1',
quantity: 30,
unit: DosageUnit.TABLET,
};
it('accepts valid minimal input', () => {
const result = CreateCabinetItemSchema.safeParse(validItem);
expect(result.success).toBe(true);
});
it('accepts valid full input', () => {
const result = CreateCabinetItemSchema.safeParse({
...validItem,
medicineProductId: 'prod-1',
expirationDate: '2026-12-31T00:00:00.000Z',
notes: 'Main supply',
});
expect(result.success).toBe(true);
});
it('rejects missing medicineId', () => {
const result = CreateCabinetItemSchema.safeParse({ quantity: 10, unit: DosageUnit.TABLET });
expect(result.success).toBe(false);
});
it('rejects negative quantity', () => {
const result = CreateCabinetItemSchema.safeParse({ ...validItem, quantity: -1 });
expect(result.success).toBe(false);
});
it('accepts quantity of 0', () => {
const result = CreateCabinetItemSchema.safeParse({ ...validItem, quantity: 0 });
expect(result.success).toBe(true);
});
});
describe('UpdateCabinetItemSchema', () => {
it('accepts partial update', () => {
const result = UpdateCabinetItemSchema.safeParse({ quantity: 25 });
expect(result.success).toBe(true);
});
it('accepts status update', () => {
const result = UpdateCabinetItemSchema.safeParse({ status: CabinetItemStatus.EXPIRED });
expect(result.success).toBe(true);
});
it('accepts empty object', () => {
const result = UpdateCabinetItemSchema.safeParse({});
expect(result.success).toBe(true);
});
it('rejects invalid status', () => {
const result = UpdateCabinetItemSchema.safeParse({ status: 'invalid' });
expect(result.success).toBe(false);
});
});
describe('AdjustQuantitySchema', () => {
it('accepts positive delta', () => {
const result = AdjustQuantitySchema.safeParse({ delta: 5 });
expect(result.success).toBe(true);
});
it('accepts negative delta', () => {
const result = AdjustQuantitySchema.safeParse({ delta: -3, reason: 'Took a dose' });
expect(result.success).toBe(true);
});
it('rejects missing delta', () => {
const result = AdjustQuantitySchema.safeParse({});
expect(result.success).toBe(false);
});
it('trims reason', () => {
const result = AdjustQuantitySchema.safeParse({ delta: 1, reason: ' test ' });
expect(result.success).toBe(true);
if (result.success) {
expect(result.data.reason).toBe('test');
}
});
});
describe('CabinetQuerySchema', () => {
it('applies default limit', () => {
const result = CabinetQuerySchema.safeParse({});
expect(result.success).toBe(true);
if (result.success) {
expect(result.data.limit).toBe(20);
}
});
it('accepts medicineId filter', () => {
const result = CabinetQuerySchema.safeParse({ medicineId: 'med-1' });
expect(result.success).toBe(true);
});
it('accepts status filter', () => {
const result = CabinetQuerySchema.safeParse({ status: CabinetItemStatus.ACTIVE });
expect(result.success).toBe(true);
});
it('accepts expiringWithin filter', () => {
const result = CabinetQuerySchema.safeParse({ expiringWithin: 30 });
expect(result.success).toBe(true);
});
it('coerces limit from string', () => {
const result = CabinetQuerySchema.safeParse({ limit: '50' });
expect(result.success).toBe(true);
if (result.success) {
expect(result.data.limit).toBe(50);
}
});
it('rejects limit over 100', () => {
const result = CabinetQuerySchema.safeParse({ limit: 101 });
expect(result.success).toBe(false);
});
it('rejects expiringWithin over 365', () => {
const result = CabinetQuerySchema.safeParse({ expiringWithin: 400 });
expect(result.success).toBe(false);
});
});

View file

@ -0,0 +1,88 @@
import { z } from 'zod/v4';
import { CabinetItemStatus } from '../enums/cabinet.enums.js';
import { DosageUnit } from '../enums/medicine.enums.js';
// --- CabinetItem ---
export const CreateCabinetItemSchema = z.object({
medicineId: z.string().min(1),
medicineProductId: z.string().min(1).optional(),
quantity: z.number().nonnegative(),
unit: z.nativeEnum(DosageUnit),
expirationDate: z.iso.datetime().optional(),
notes: z.string().max(1000).trim().optional(),
});
export const UpdateCabinetItemSchema = z.object({
quantity: z.number().nonnegative().optional(),
unit: z.nativeEnum(DosageUnit).optional(),
expirationDate: z.iso.datetime().optional(),
status: z.nativeEnum(CabinetItemStatus).optional(),
notes: z.string().max(1000).trim().optional(),
});
export const AdjustQuantitySchema = z.object({
delta: z.number(),
reason: z.string().max(500).trim().optional(),
});
export const CabinetQuerySchema = z.object({
medicineId: z.string().optional(),
status: z.nativeEnum(CabinetItemStatus).optional(),
expiringWithin: z.coerce.number().int().min(1).max(365).optional(),
cursor: z.string().optional(),
limit: z.coerce.number().int().min(1).max(100).default(20),
});
export const CabinetItemResponseSchema = z.object({
_id: z.string(),
householdId: z.string(),
medicineId: z.string(),
medicineName: z.string(),
medicineStrength: z.number(),
medicineStrengthUnit: z.string(),
medicineForm: z.string(),
medicineProductId: z.string().optional(),
medicineProductBrand: z.string().optional(),
concentration: z.number().optional(),
concentrationUnit: z.string().optional(),
quantity: z.number(),
unit: z.string(),
expirationDate: z.string().optional(),
status: z.string(),
notes: z.string().optional(),
createdBy: z.string(),
createdAt: z.string(),
updatedAt: z.string(),
});
export const CabinetItemListResponseSchema = z.object({
data: z.array(CabinetItemResponseSchema),
pagination: z.object({
cursor: z.string().nullable(),
hasMore: z.boolean(),
total: z.number().optional(),
}),
});
export const CabinetSummaryItemSchema = z.object({
medicineId: z.string(),
medicineName: z.string(),
medicineStrength: z.number(),
medicineStrengthUnit: z.string(),
medicineForm: z.string(),
totalQuantity: z.number(),
unit: z.string(),
earliestExpiry: z.string().nullable(),
itemCount: z.number(),
});
export const CabinetSummaryResponseSchema = z.object({
data: z.array(CabinetSummaryItemSchema),
});
// Type exports
export type CreateCabinetItemInput = z.infer<typeof CreateCabinetItemSchema>;
export type UpdateCabinetItemInput = z.infer<typeof UpdateCabinetItemSchema>;
export type AdjustQuantityInput = z.infer<typeof AdjustQuantitySchema>;
export type CabinetQueryInput = z.infer<typeof CabinetQuerySchema>;

View file

@ -1,2 +1,4 @@
export * from './user.schemas.js';
export * from './household.schemas.js';
export * from './medicine.schemas.js';
export * from './cabinet.schemas.js';

View file

@ -0,0 +1,196 @@
import { describe, it, expect } from 'vitest';
import {
CreateMedicineSchema,
UpdateMedicineSchema,
MedicineQuerySchema,
CreateMedicineProductSchema,
UpdateMedicineProductSchema,
} from './medicine.schemas.js';
import {
MedicineForm,
StrengthUnit,
MedicineCategory,
DosageUnit,
MedicineProductSource,
} from '../enums/medicine.enums.js';
describe('CreateMedicineSchema', () => {
const validMedicine = {
name: 'Metformin',
form: MedicineForm.TABLET,
strength: 500,
strengthUnit: StrengthUnit.MG,
category: MedicineCategory.PRESCRIPTION,
};
it('accepts valid medicine', () => {
const result = CreateMedicineSchema.safeParse(validMedicine);
expect(result.success).toBe(true);
});
it('accepts medicine with all optional fields', () => {
const result = CreateMedicineSchema.safeParse({
...validMedicine,
notes: 'Take with food',
tags: ['daily', 'morning'],
});
expect(result.success).toBe(true);
});
it('defaults tags to empty array', () => {
const result = CreateMedicineSchema.safeParse(validMedicine);
expect(result.success).toBe(true);
if (result.success) {
expect(result.data.tags).toEqual([]);
}
});
it('rejects empty name', () => {
const result = CreateMedicineSchema.safeParse({ ...validMedicine, name: '' });
expect(result.success).toBe(false);
});
it('rejects zero strength', () => {
const result = CreateMedicineSchema.safeParse({ ...validMedicine, strength: 0 });
expect(result.success).toBe(false);
});
it('rejects negative strength', () => {
const result = CreateMedicineSchema.safeParse({ ...validMedicine, strength: -1 });
expect(result.success).toBe(false);
});
it('rejects invalid form', () => {
const result = CreateMedicineSchema.safeParse({ ...validMedicine, form: 'gummy' });
expect(result.success).toBe(false);
});
it('rejects invalid category', () => {
const result = CreateMedicineSchema.safeParse({ ...validMedicine, category: 'magic' });
expect(result.success).toBe(false);
});
it('trims name whitespace', () => {
const result = CreateMedicineSchema.safeParse({ ...validMedicine, name: ' Metformin ' });
expect(result.success).toBe(true);
if (result.success) {
expect(result.data.name).toBe('Metformin');
}
});
});
describe('UpdateMedicineSchema', () => {
it('accepts empty object (all fields optional)', () => {
const result = UpdateMedicineSchema.safeParse({});
expect(result.success).toBe(true);
});
it('accepts partial update', () => {
const result = UpdateMedicineSchema.safeParse({ name: 'Updated Name' });
expect(result.success).toBe(true);
});
it('rejects invalid strength', () => {
const result = UpdateMedicineSchema.safeParse({ strength: -5 });
expect(result.success).toBe(false);
});
});
describe('MedicineQuerySchema', () => {
it('accepts empty query with defaults', () => {
const result = MedicineQuerySchema.safeParse({});
expect(result.success).toBe(true);
if (result.success) {
expect(result.data.limit).toBe(20);
}
});
it('accepts full query', () => {
const result = MedicineQuerySchema.safeParse({
q: 'metformin',
category: MedicineCategory.PRESCRIPTION,
form: MedicineForm.TABLET,
cursor: 'abc123',
limit: 50,
});
expect(result.success).toBe(true);
});
it('coerces limit from string', () => {
const result = MedicineQuerySchema.safeParse({ limit: '10' });
expect(result.success).toBe(true);
if (result.success) {
expect(result.data.limit).toBe(10);
}
});
it('rejects limit above 100', () => {
const result = MedicineQuerySchema.safeParse({ limit: 101 });
expect(result.success).toBe(false);
});
});
describe('CreateMedicineProductSchema', () => {
const validProduct = {
brand: 'CVS Health',
packageSize: 90,
packageUnit: DosageUnit.TABLET,
};
it('accepts valid product', () => {
const result = CreateMedicineProductSchema.safeParse(validProduct);
expect(result.success).toBe(true);
});
it('defaults source to manual', () => {
const result = CreateMedicineProductSchema.safeParse(validProduct);
expect(result.success).toBe(true);
if (result.success) {
expect(result.data.source).toBe(MedicineProductSource.MANUAL);
}
});
it('accepts product with all optional fields', () => {
const result = CreateMedicineProductSchema.safeParse({
...validProduct,
manufacturer: 'CVS Pharmacy',
imageUrl: 'https://example.com/image.jpg',
notes: 'Generic equivalent',
source: MedicineProductSource.IMPORT,
});
expect(result.success).toBe(true);
});
it('rejects empty brand', () => {
const result = CreateMedicineProductSchema.safeParse({ ...validProduct, brand: '' });
expect(result.success).toBe(false);
});
it('rejects zero package size', () => {
const result = CreateMedicineProductSchema.safeParse({ ...validProduct, packageSize: 0 });
expect(result.success).toBe(false);
});
it('rejects invalid URL', () => {
const result = CreateMedicineProductSchema.safeParse({
...validProduct,
imageUrl: 'not-a-url',
});
expect(result.success).toBe(false);
});
});
describe('UpdateMedicineProductSchema', () => {
it('accepts empty object', () => {
const result = UpdateMedicineProductSchema.safeParse({});
expect(result.success).toBe(true);
});
it('accepts partial update', () => {
const result = UpdateMedicineProductSchema.safeParse({
brand: 'Kirkland',
packageSize: 60,
});
expect(result.success).toBe(true);
});
});

View file

@ -0,0 +1,123 @@
import { z } from 'zod/v4';
import {
MedicineForm,
StrengthUnit,
MedicineCategory,
MedicineProductSource,
DosageUnit,
ConcentrationUnit,
} from '../enums/medicine.enums.js';
// --- Medicine (generic level) ---
export const CreateMedicineSchema = z.object({
name: z.string().min(1).max(200).trim(),
form: z.nativeEnum(MedicineForm),
strength: z.number().positive(),
strengthUnit: z.nativeEnum(StrengthUnit),
category: z.nativeEnum(MedicineCategory),
notes: z.string().max(1000).trim().optional(),
tags: z.array(z.string().min(1).max(50).trim()).max(20).default([]),
});
export const UpdateMedicineSchema = z.object({
name: z.string().min(1).max(200).trim().optional(),
form: z.nativeEnum(MedicineForm).optional(),
strength: z.number().positive().optional(),
strengthUnit: z.nativeEnum(StrengthUnit).optional(),
category: z.nativeEnum(MedicineCategory).optional(),
notes: z.string().max(1000).trim().optional(),
tags: z.array(z.string().min(1).max(50).trim()).max(20).optional(),
});
export const MedicineQuerySchema = z.object({
q: z.string().optional(),
category: z.nativeEnum(MedicineCategory).optional(),
form: z.nativeEnum(MedicineForm).optional(),
cursor: z.string().optional(),
limit: z.coerce.number().int().min(1).max(100).default(20),
});
export const MedicineResponseSchema = z.object({
_id: z.string(),
householdId: z.string(),
name: z.string(),
form: z.string(),
strength: z.number(),
strengthUnit: z.string(),
category: z.string(),
notes: z.string().optional(),
tags: z.array(z.string()),
createdBy: z.string(),
createdAt: z.string(),
updatedAt: z.string(),
});
export const MedicineListResponseSchema = z.object({
data: z.array(MedicineResponseSchema),
pagination: z.object({
cursor: z.string().nullable(),
hasMore: z.boolean(),
total: z.number().optional(),
}),
});
// --- MedicineProduct (purchasable level) ---
export const CreateMedicineProductSchema = z.object({
brand: z.string().min(1).max(200).trim(),
manufacturer: z.string().max(200).trim().optional(),
packageSize: z.number().positive(),
packageUnit: z.nativeEnum(DosageUnit),
concentration: z.number().positive().optional(),
concentrationUnit: z.nativeEnum(ConcentrationUnit).optional(),
imageUrl: z.url().optional(),
notes: z.string().max(1000).trim().optional(),
source: z.nativeEnum(MedicineProductSource).default(MedicineProductSource.MANUAL),
});
export const UpdateMedicineProductSchema = z.object({
brand: z.string().min(1).max(200).trim().optional(),
manufacturer: z.string().max(200).trim().optional(),
packageSize: z.number().positive().optional(),
packageUnit: z.nativeEnum(DosageUnit).optional(),
concentration: z.number().positive().optional(),
concentrationUnit: z.nativeEnum(ConcentrationUnit).optional(),
imageUrl: z.url().optional(),
notes: z.string().max(1000).trim().optional(),
});
export const MedicineProductResponseSchema = z.object({
_id: z.string(),
householdId: z.string(),
medicineId: z.string(),
medicineName: z.string(),
brand: z.string(),
manufacturer: z.string().optional(),
packageSize: z.number(),
packageUnit: z.string(),
concentration: z.number().optional(),
concentrationUnit: z.string().optional(),
imageUrl: z.string().optional(),
notes: z.string().optional(),
source: z.string(),
createdBy: z.string(),
createdAt: z.string(),
updatedAt: z.string(),
});
export const MedicineProductListResponseSchema = z.object({
data: z.array(MedicineProductResponseSchema),
pagination: z.object({
cursor: z.string().nullable(),
hasMore: z.boolean(),
total: z.number().optional(),
}),
});
// Type exports
export type CreateMedicineInput = z.infer<typeof CreateMedicineSchema>;
export type UpdateMedicineInput = z.infer<typeof UpdateMedicineSchema>;
export type MedicineQueryInput = z.infer<typeof MedicineQuerySchema>;
export type CreateMedicineProductInput = z.infer<typeof CreateMedicineProductSchema>;
export type UpdateMedicineProductInput = z.infer<typeof UpdateMedicineProductSchema>;

View file

@ -10,5 +10,17 @@ export const CreateUserSchema = z.object({
export const UpdateUserSchema = CreateUserSchema.partial().omit({ keycloakId: true });
export const UserResponseSchema = z.object({
_id: z.string(),
keycloakId: z.string(),
displayName: z.string(),
email: z.string(),
householdIds: z.array(z.string()),
defaultHouseholdId: z.string().nullable(),
createdAt: z.string(),
updatedAt: z.string(),
});
export type CreateUserInput = z.infer<typeof CreateUserSchema>;
export type UpdateUserInput = z.infer<typeof UpdateUserSchema>;
export type UserResponse = z.infer<typeof UserResponseSchema>;