@blackcode_sa/metaestetics-api 1.5.17 → 1.5.18

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.
@@ -13,15 +13,12 @@ import {
13
13
  where,
14
14
  getDocs,
15
15
  QueryConstraint,
16
- } from "firebase/firestore";
17
- import {
18
- ref,
19
- uploadBytes,
20
- getDownloadURL,
21
- deleteObject,
22
- FirebaseStorage,
23
- } from "firebase/storage";
24
- import { z } from "zod";
16
+ limit,
17
+ startAfter,
18
+ doc,
19
+ } from 'firebase/firestore';
20
+ import { ref, uploadBytes, getDownloadURL, deleteObject, FirebaseStorage } from 'firebase/storage';
21
+ import { z } from 'zod';
25
22
  import {
26
23
  PatientProfile,
27
24
  CreatePatientProfileData,
@@ -29,36 +26,36 @@ import {
29
26
  SearchPatientsParams,
30
27
  RequesterInfo,
31
28
  PATIENTS_COLLECTION,
32
- } from "../../../types/patient";
29
+ } from '../../../types/patient';
33
30
  import {
34
31
  patientProfileSchema,
35
32
  createPatientProfileSchema,
36
33
  searchPatientsSchema,
37
34
  requesterInfoSchema,
38
- } from "../../../validations/patient.schema";
35
+ } from '../../../validations/patient.schema';
39
36
  import {
40
37
  getPatientDocRef,
41
38
  getPatientDocRefByUserRef,
42
39
  initSensitiveInfoDocIfNotExists,
43
40
  getSensitiveInfoDocRef,
44
41
  getMedicalInfoDocRef,
45
- } from "./docs.utils";
46
- import { ensureMedicalInfoExists } from "./medical.utils";
47
- import { DEFAULT_MEDICAL_INFO } from "../../../types/patient/medical-info.types";
42
+ } from './docs.utils';
43
+ import { ensureMedicalInfoExists } from './medical.utils';
44
+ import { DEFAULT_MEDICAL_INFO } from '../../../types/patient/medical-info.types';
48
45
 
49
46
  // Funkcije za rad sa profilom
50
47
  export const createPatientProfileUtil = async (
51
48
  db: Firestore,
52
49
  data: CreatePatientProfileData,
53
- generateId: () => string
50
+ generateId: () => string,
54
51
  ): Promise<PatientProfile> => {
55
52
  try {
56
- console.log("[createPatientProfileUtil] Starting patient profile creation");
53
+ console.log('[createPatientProfileUtil] Starting patient profile creation');
57
54
  const validatedData = createPatientProfileSchema.parse(data);
58
55
  const patientId = generateId();
59
56
  console.log(`[createPatientProfileUtil] Generated patientId: ${patientId}`);
60
57
 
61
- const patientData: Omit<PatientProfile, "createdAt" | "updatedAt"> & {
58
+ const patientData: Omit<PatientProfile, 'createdAt' | 'updatedAt'> & {
62
59
  createdAt: ReturnType<typeof serverTimestamp>;
63
60
  updatedAt: ReturnType<typeof serverTimestamp>;
64
61
  } = {
@@ -96,21 +93,16 @@ export const createPatientProfileUtil = async (
96
93
  const sensitiveInfoCreated = await initSensitiveInfoDocIfNotExists(
97
94
  db,
98
95
  patientId,
99
- validatedData.userRef
96
+ validatedData.userRef,
100
97
  );
101
98
  console.log(
102
99
  `[createPatientProfileUtil] Sensitive info document creation result: ${
103
- sensitiveInfoCreated
104
- ? "Document already existed"
105
- : "New document created"
106
- }`
100
+ sensitiveInfoCreated ? 'Document already existed' : 'New document created'
101
+ }`,
107
102
  );
108
103
  sensitiveInfoSuccess = true;
109
104
  } catch (sensitiveError) {
110
- console.error(
111
- `[createPatientProfileUtil] Error creating sensitive info:`,
112
- sensitiveError
113
- );
105
+ console.error(`[createPatientProfileUtil] Error creating sensitive info:`, sensitiveError);
114
106
  // Don't throw the error, we'll try the direct method
115
107
  }
116
108
 
@@ -119,33 +111,21 @@ export const createPatientProfileUtil = async (
119
111
  let medicalInfoSuccess = false;
120
112
  try {
121
113
  await ensureMedicalInfoExists(db, patientId, validatedData.userRef);
122
- console.log(
123
- `[createPatientProfileUtil] Medical info document created successfully`
124
- );
114
+ console.log(`[createPatientProfileUtil] Medical info document created successfully`);
125
115
  medicalInfoSuccess = true;
126
116
  } catch (medicalError) {
127
- console.error(
128
- `[createPatientProfileUtil] Error creating medical info:`,
129
- medicalError
130
- );
117
+ console.error(`[createPatientProfileUtil] Error creating medical info:`, medicalError);
131
118
  // Don't throw the error, we'll try the direct method
132
119
  }
133
120
 
134
121
  // If either utility function failed, try the direct method
135
122
  if (!sensitiveInfoSuccess || !medicalInfoSuccess) {
136
- console.log(
137
- `[createPatientProfileUtil] Using fallback method to create documents`
138
- );
123
+ console.log(`[createPatientProfileUtil] Using fallback method to create documents`);
139
124
  try {
140
125
  await testCreateSubDocuments(db, patientId, validatedData.userRef);
141
- console.log(
142
- `[createPatientProfileUtil] Fallback method completed successfully`
143
- );
126
+ console.log(`[createPatientProfileUtil] Fallback method completed successfully`);
144
127
  } catch (fallbackError) {
145
- console.error(
146
- `[createPatientProfileUtil] Fallback method failed:`,
147
- fallbackError
148
- );
128
+ console.error(`[createPatientProfileUtil] Fallback method failed:`, fallbackError);
149
129
  // Still continue to return the patient profile
150
130
  }
151
131
  }
@@ -153,23 +133,16 @@ export const createPatientProfileUtil = async (
153
133
  console.log(`[createPatientProfileUtil] Verifying patient document exists`);
154
134
  const patientDoc = await getDoc(getPatientDocRef(db, patientId));
155
135
  if (!patientDoc.exists()) {
156
- console.error(
157
- `[createPatientProfileUtil] Patient document not found after creation`
158
- );
159
- throw new Error("Failed to create patient profile");
136
+ console.error(`[createPatientProfileUtil] Patient document not found after creation`);
137
+ throw new Error('Failed to create patient profile');
160
138
  }
161
139
 
162
- console.log(
163
- `[createPatientProfileUtil] Patient profile creation completed successfully`
164
- );
140
+ console.log(`[createPatientProfileUtil] Patient profile creation completed successfully`);
165
141
  return patientDoc.data() as PatientProfile;
166
142
  } catch (error) {
167
- console.error(
168
- `[createPatientProfileUtil] Error in patient profile creation:`,
169
- error
170
- );
143
+ console.error(`[createPatientProfileUtil] Error in patient profile creation:`, error);
171
144
  if (error instanceof z.ZodError) {
172
- throw new Error("Invalid patient data: " + error.message);
145
+ throw new Error('Invalid patient data: ' + error.message);
173
146
  }
174
147
  throw error;
175
148
  }
@@ -177,7 +150,7 @@ export const createPatientProfileUtil = async (
177
150
 
178
151
  export const getPatientProfileUtil = async (
179
152
  db: Firestore,
180
- patientId: string
153
+ patientId: string,
181
154
  ): Promise<PatientProfile | null> => {
182
155
  const patientDoc = await getDoc(getPatientDocRef(db, patientId));
183
156
  return patientDoc.exists() ? (patientDoc.data() as PatientProfile) : null;
@@ -185,7 +158,7 @@ export const getPatientProfileUtil = async (
185
158
 
186
159
  export const getPatientProfileByUserRefUtil = async (
187
160
  db: Firestore,
188
- userRef: string
161
+ userRef: string,
189
162
  ): Promise<PatientProfile | null> => {
190
163
  try {
191
164
  const docRef = await getPatientDocRefByUserRef(db, userRef);
@@ -200,7 +173,7 @@ export const getPatientProfileByUserRefUtil = async (
200
173
  export const addExpoTokenUtil = async (
201
174
  db: Firestore,
202
175
  patientId: string,
203
- token: string
176
+ token: string,
204
177
  ): Promise<void> => {
205
178
  await updateDoc(getPatientDocRef(db, patientId), {
206
179
  expoTokens: arrayUnion(token),
@@ -211,7 +184,7 @@ export const addExpoTokenUtil = async (
211
184
  export const removeExpoTokenUtil = async (
212
185
  db: Firestore,
213
186
  patientId: string,
214
- token: string
187
+ token: string,
215
188
  ): Promise<void> => {
216
189
  await updateDoc(getPatientDocRef(db, patientId), {
217
190
  expoTokens: arrayRemove(token),
@@ -222,10 +195,10 @@ export const removeExpoTokenUtil = async (
222
195
  export const addPointsUtil = async (
223
196
  db: Firestore,
224
197
  patientId: string,
225
- points: number
198
+ points: number,
226
199
  ): Promise<void> => {
227
200
  await updateDoc(getPatientDocRef(db, patientId), {
228
- "gamification.points": increment(points),
201
+ 'gamification.points': increment(points),
229
202
  updatedAt: serverTimestamp(),
230
203
  });
231
204
  };
@@ -233,7 +206,7 @@ export const addPointsUtil = async (
233
206
  export const updatePatientProfileUtil = async (
234
207
  db: Firestore,
235
208
  patientId: string,
236
- data: Partial<Omit<PatientProfile, "id" | "createdAt" | "updatedAt">>
209
+ data: Partial<Omit<PatientProfile, 'id' | 'createdAt' | 'updatedAt'>>,
237
210
  ): Promise<PatientProfile> => {
238
211
  try {
239
212
  const updateData = {
@@ -245,7 +218,7 @@ export const updatePatientProfileUtil = async (
245
218
 
246
219
  const updatedDoc = await getDoc(getPatientDocRef(db, patientId));
247
220
  if (!updatedDoc.exists()) {
248
- throw new Error("Patient profile not found after update");
221
+ throw new Error('Patient profile not found after update');
249
222
  }
250
223
 
251
224
  return updatedDoc.data() as PatientProfile;
@@ -258,23 +231,21 @@ export const updatePatientProfileUtil = async (
258
231
  export const updatePatientProfileByUserRefUtil = async (
259
232
  db: Firestore,
260
233
  userRef: string,
261
- data: Partial<Omit<PatientProfile, "id" | "createdAt" | "updatedAt">>
234
+ data: Partial<Omit<PatientProfile, 'id' | 'createdAt' | 'updatedAt'>>,
262
235
  ): Promise<PatientProfile> => {
263
236
  try {
264
237
  const docRef = await getPatientDocRefByUserRef(db, userRef);
265
238
  const patientDoc = await getDoc(docRef);
266
239
 
267
240
  if (!patientDoc.exists()) {
268
- throw new Error("Patient profile not found");
241
+ throw new Error('Patient profile not found');
269
242
  }
270
243
 
271
244
  const patientData = patientDoc.data() as PatientProfile;
272
245
  return updatePatientProfileUtil(db, patientData.id, data);
273
246
  } catch (error: unknown) {
274
247
  const errorMessage = error instanceof Error ? error.message : String(error);
275
- throw new Error(
276
- `Failed to update patient profile by user ref: ${errorMessage}`
277
- );
248
+ throw new Error(`Failed to update patient profile by user ref: ${errorMessage}`);
278
249
  }
279
250
  };
280
251
 
@@ -282,7 +253,7 @@ export const updatePatientProfileByUserRefUtil = async (
282
253
  export const uploadProfilePhotoUtil = async (
283
254
  storage: FirebaseStorage,
284
255
  patientId: string,
285
- file: File
256
+ file: File,
286
257
  ): Promise<string> => {
287
258
  const photoRef = ref(storage, `patient-photos/${patientId}/profile-photo`);
288
259
  await uploadBytes(photoRef, file);
@@ -293,11 +264,11 @@ export const updateProfilePhotoUtil = async (
293
264
  storage: FirebaseStorage,
294
265
  db: Firestore,
295
266
  patientId: string,
296
- file: File
267
+ file: File,
297
268
  ): Promise<string> => {
298
269
  // Prvo obrišemo staru sliku ako postoji
299
270
  const patientDoc = await getDoc(getPatientDocRef(db, patientId));
300
- if (!patientDoc.exists()) throw new Error("Patient profile not found");
271
+ if (!patientDoc.exists()) throw new Error('Patient profile not found');
301
272
 
302
273
  const patientData = patientDoc.data() as PatientProfile;
303
274
  if (patientData.profilePhoto) {
@@ -305,7 +276,7 @@ export const updateProfilePhotoUtil = async (
305
276
  const oldPhotoRef = ref(storage, patientData.profilePhoto);
306
277
  await deleteObject(oldPhotoRef);
307
278
  } catch (error) {
308
- console.warn("Failed to delete old profile photo:", error);
279
+ console.warn('Failed to delete old profile photo:', error);
309
280
  }
310
281
  }
311
282
 
@@ -324,10 +295,10 @@ export const updateProfilePhotoUtil = async (
324
295
  export const deleteProfilePhotoUtil = async (
325
296
  storage: FirebaseStorage,
326
297
  db: Firestore,
327
- patientId: string
298
+ patientId: string,
328
299
  ): Promise<void> => {
329
300
  const patientDoc = await getDoc(getPatientDocRef(db, patientId));
330
- if (!patientDoc.exists()) throw new Error("Patient profile not found");
301
+ if (!patientDoc.exists()) throw new Error('Patient profile not found');
331
302
 
332
303
  const patientData = patientDoc.data() as PatientProfile;
333
304
  if (patientData.profilePhoto) {
@@ -335,7 +306,7 @@ export const deleteProfilePhotoUtil = async (
335
306
  const photoRef = ref(storage, patientData.profilePhoto);
336
307
  await deleteObject(photoRef);
337
308
  } catch (error) {
338
- console.warn("Failed to delete profile photo:", error);
309
+ console.warn('Failed to delete profile photo:', error);
339
310
  }
340
311
  }
341
312
 
@@ -352,45 +323,39 @@ export const deleteProfilePhotoUtil = async (
352
323
  export const testCreateSubDocuments = async (
353
324
  db: Firestore,
354
325
  patientId: string,
355
- userRef: string
326
+ userRef: string,
356
327
  ): Promise<void> => {
357
328
  console.log(
358
- `[testCreateSubDocuments] Starting test for patientId: ${patientId}, userRef: ${userRef}`
329
+ `[testCreateSubDocuments] Starting test for patientId: ${patientId}, userRef: ${userRef}`,
359
330
  );
360
331
 
361
332
  try {
362
333
  // Test sensitive info creation
363
334
  console.log(`[testCreateSubDocuments] Testing sensitive info creation`);
364
335
  const sensitiveInfoRef = getSensitiveInfoDocRef(db, patientId);
365
- console.log(
366
- `[testCreateSubDocuments] Sensitive info path: ${sensitiveInfoRef.path}`
367
- );
336
+ console.log(`[testCreateSubDocuments] Sensitive info path: ${sensitiveInfoRef.path}`);
368
337
 
369
338
  const defaultSensitiveInfo = {
370
339
  patientId,
371
340
  userRef,
372
- photoUrl: "",
373
- firstName: "Name",
374
- lastName: "Surname",
341
+ photoUrl: '',
342
+ firstName: 'Name',
343
+ lastName: 'Surname',
375
344
  dateOfBirth: Timestamp.now(),
376
345
  gender: Gender.PREFER_NOT_TO_SAY,
377
- email: "test@example.com",
378
- phoneNumber: "",
346
+ email: 'test@example.com',
347
+ phoneNumber: '',
379
348
  createdAt: Timestamp.now(),
380
349
  updatedAt: Timestamp.now(),
381
350
  };
382
351
 
383
352
  await setDoc(sensitiveInfoRef, defaultSensitiveInfo);
384
- console.log(
385
- `[testCreateSubDocuments] Sensitive info document created directly`
386
- );
353
+ console.log(`[testCreateSubDocuments] Sensitive info document created directly`);
387
354
 
388
355
  // Test medical info creation
389
356
  console.log(`[testCreateSubDocuments] Testing medical info creation`);
390
357
  const medicalInfoRef = getMedicalInfoDocRef(db, patientId);
391
- console.log(
392
- `[testCreateSubDocuments] Medical info path: ${medicalInfoRef.path}`
393
- );
358
+ console.log(`[testCreateSubDocuments] Medical info path: ${medicalInfoRef.path}`);
394
359
 
395
360
  const defaultMedicalInfo = {
396
361
  ...DEFAULT_MEDICAL_INFO,
@@ -400,9 +365,7 @@ export const testCreateSubDocuments = async (
400
365
  };
401
366
 
402
367
  await setDoc(medicalInfoRef, defaultMedicalInfo);
403
- console.log(
404
- `[testCreateSubDocuments] Medical info document created directly`
405
- );
368
+ console.log(`[testCreateSubDocuments] Medical info document created directly`);
406
369
 
407
370
  console.log(`[testCreateSubDocuments] Test completed successfully`);
408
371
  } catch (error) {
@@ -423,7 +386,7 @@ export const testCreateSubDocuments = async (
423
386
  export const searchPatientsUtil = async (
424
387
  db: Firestore,
425
388
  params: SearchPatientsParams,
426
- requester: RequesterInfo
389
+ requester: RequesterInfo,
427
390
  ): Promise<PatientProfile[]> => {
428
391
  // Validate input
429
392
  searchPatientsSchema.parse(params);
@@ -434,65 +397,52 @@ export const searchPatientsUtil = async (
434
397
 
435
398
  // --- Security Checks & Initial Filtering ---
436
399
 
437
- if (requester.role === "clinic_admin") {
400
+ if (requester.role === 'clinic_admin') {
438
401
  // Clinic admin can only search within their own clinic
439
402
  if (!requester.associatedClinicId) {
440
- throw new Error(
441
- "Associated clinic ID is required for clinic admin search."
442
- );
403
+ throw new Error('Associated clinic ID is required for clinic admin search.');
443
404
  }
444
405
  // If the search params specify a different clinic, it's an invalid request for this admin.
445
406
  if (params.clinicId && params.clinicId !== requester.associatedClinicId) {
446
407
  console.warn(
447
- `Clinic admin (${requester.id}) attempted to search outside their associated clinic (${requester.associatedClinicId})`
408
+ `Clinic admin (${requester.id}) attempted to search outside their associated clinic (${requester.associatedClinicId})`,
448
409
  );
449
410
  return []; // Or throw an error
450
411
  }
451
412
 
452
413
  // **Mandatory filter**: Ensure patients belong to the admin's clinic.
453
- constraints.push(
454
- where("clinicIds", "array-contains", requester.associatedClinicId)
455
- );
414
+ constraints.push(where('clinicIds', 'array-contains', requester.associatedClinicId));
456
415
 
457
416
  // Optional filter: If practitionerId is also provided, filter by that practitioner *within the admin's clinic*.
458
417
  if (params.practitionerId) {
459
- constraints.push(
460
- where("doctorIds", "array-contains", params.practitionerId)
461
- );
418
+ constraints.push(where('doctorIds', 'array-contains', params.practitionerId));
462
419
  // We might need an additional check here if the practitioner MUST work at the admin's clinic.
463
420
  // This would require fetching practitioner data or having denormalized clinic IDs on the practitioner.
464
421
  }
465
- } else if (requester.role === "practitioner") {
422
+ } else if (requester.role === 'practitioner') {
466
423
  // Practitioner can only search for their own patients.
467
424
  if (!requester.associatedPractitionerId) {
468
- throw new Error(
469
- "Associated practitioner ID is required for practitioner search."
470
- );
425
+ throw new Error('Associated practitioner ID is required for practitioner search.');
471
426
  }
472
427
  // If the search params specify a different practitioner, it's invalid.
473
- if (
474
- params.practitionerId &&
475
- params.practitionerId !== requester.associatedPractitionerId
476
- ) {
428
+ if (params.practitionerId && params.practitionerId !== requester.associatedPractitionerId) {
477
429
  console.warn(
478
- `Practitioner (${requester.id}) attempted to search for patients of another practitioner (${params.practitionerId})`
430
+ `Practitioner (${requester.id}) attempted to search for patients of another practitioner (${params.practitionerId})`,
479
431
  );
480
432
  return []; // Or throw an error
481
433
  }
482
434
 
483
435
  // **Mandatory filter**: Ensure patients are associated with this practitioner.
484
- constraints.push(
485
- where("doctorIds", "array-contains", requester.associatedPractitionerId)
486
- );
436
+ constraints.push(where('doctorIds', 'array-contains', requester.associatedPractitionerId));
487
437
 
488
438
  // Optional filter: If clinicId is provided, filter by patients of this practitioner *at that specific clinic*.
489
439
  if (params.clinicId) {
490
- constraints.push(where("clinicIds", "array-contains", params.clinicId));
440
+ constraints.push(where('clinicIds', 'array-contains', params.clinicId));
491
441
  // Similar to above, we might need to check if the practitioner actually works at this clinic.
492
442
  }
493
443
  } else {
494
444
  // Should not happen due to validation, but good practice to handle.
495
- throw new Error("Invalid requester role.");
445
+ throw new Error('Invalid requester role.');
496
446
  }
497
447
 
498
448
  // --- Execute Query ---
@@ -500,17 +450,63 @@ export const searchPatientsUtil = async (
500
450
  const finalQuery = query(patientsCollectionRef, ...constraints);
501
451
  const querySnapshot = await getDocs(finalQuery);
502
452
 
503
- const patients = querySnapshot.docs.map(
504
- (doc) => doc.data() as PatientProfile
505
- );
453
+ const patients = querySnapshot.docs.map((doc) => doc.data() as PatientProfile);
506
454
 
507
- console.log(
508
- `[searchPatientsUtil] Found ${patients.length} patients matching criteria.`
509
- );
455
+ console.log(`[searchPatientsUtil] Found ${patients.length} patients matching criteria.`);
510
456
  return patients;
511
457
  } catch (error) {
512
- console.error("[searchPatientsUtil] Error searching patients:", error);
458
+ console.error('[searchPatientsUtil] Error searching patients:', error);
513
459
  // Consider logging more details or re-throwing a specific error type
514
460
  return []; // Return empty array on error
515
461
  }
516
462
  };
463
+
464
+ /**
465
+ * Retrieves all patient profiles from the database.
466
+ *
467
+ * @param {Firestore} db - Firestore instance
468
+ * @param {Object} options - Optional parameters for pagination
469
+ * @param {number} options.limit - Maximum number of profiles to return
470
+ * @param {string} options.startAfter - The ID of the document to start after (for pagination)
471
+ * @returns {Promise<PatientProfile[]>} A promise resolving to an array of all patient profiles
472
+ */
473
+ export const getAllPatientsUtil = async (
474
+ db: Firestore,
475
+ options?: { limit?: number; startAfter?: string },
476
+ ): Promise<PatientProfile[]> => {
477
+ try {
478
+ console.log(`[getAllPatientsUtil] Fetching patients with options:`, options);
479
+
480
+ const patientsCollection = collection(db, PATIENTS_COLLECTION);
481
+
482
+ let q = query(patientsCollection);
483
+
484
+ // Apply pagination if needed
485
+ if (options?.limit) {
486
+ q = query(q, limit(options.limit));
487
+ }
488
+
489
+ // If startAfter is provided, get that document and use it for pagination
490
+ if (options?.startAfter) {
491
+ const startAfterDoc = await getDoc(doc(db, PATIENTS_COLLECTION, options.startAfter));
492
+ if (startAfterDoc.exists()) {
493
+ q = query(q, startAfter(startAfterDoc));
494
+ }
495
+ }
496
+
497
+ const patientsSnapshot = await getDocs(q);
498
+
499
+ const patients: PatientProfile[] = [];
500
+ patientsSnapshot.forEach((doc) => {
501
+ patients.push(doc.data() as PatientProfile);
502
+ });
503
+
504
+ console.log(`[getAllPatientsUtil] Found ${patients.length} patients`);
505
+ return patients;
506
+ } catch (error) {
507
+ console.error(`[getAllPatientsUtil] Error fetching patients:`, error);
508
+ throw new Error(
509
+ `Failed to retrieve patients: ${error instanceof Error ? error.message : String(error)}`,
510
+ );
511
+ }
512
+ };