@blackcode_sa/metaestetics-api 1.12.58 → 1.12.61

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (267) hide show
  1. package/dist/admin/index.d.mts +36 -4
  2. package/dist/admin/index.d.ts +36 -4
  3. package/dist/admin/index.js +156 -28
  4. package/dist/admin/index.mjs +156 -28
  5. package/dist/index.d.mts +9 -1
  6. package/dist/index.d.ts +9 -1
  7. package/package.json +119 -119
  8. package/src/__mocks__/firstore.ts +10 -10
  9. package/src/admin/aggregation/README.md +79 -79
  10. package/src/admin/aggregation/appointment/README.md +128 -128
  11. package/src/admin/aggregation/appointment/appointment.aggregation.service.ts +1844 -1689
  12. package/src/admin/aggregation/appointment/index.ts +1 -1
  13. package/src/admin/aggregation/clinic/README.md +52 -52
  14. package/src/admin/aggregation/clinic/clinic.aggregation.service.ts +703 -703
  15. package/src/admin/aggregation/clinic/index.ts +1 -1
  16. package/src/admin/aggregation/forms/README.md +13 -13
  17. package/src/admin/aggregation/forms/filled-forms.aggregation.service.ts +322 -322
  18. package/src/admin/aggregation/forms/index.ts +1 -1
  19. package/src/admin/aggregation/index.ts +8 -8
  20. package/src/admin/aggregation/patient/README.md +27 -27
  21. package/src/admin/aggregation/patient/index.ts +1 -1
  22. package/src/admin/aggregation/patient/patient.aggregation.service.ts +141 -141
  23. package/src/admin/aggregation/practitioner/README.md +42 -42
  24. package/src/admin/aggregation/practitioner/index.ts +1 -1
  25. package/src/admin/aggregation/practitioner/practitioner.aggregation.service.ts +433 -433
  26. package/src/admin/aggregation/practitioner-invite/index.ts +1 -1
  27. package/src/admin/aggregation/practitioner-invite/practitioner-invite.aggregation.service.ts +961 -961
  28. package/src/admin/aggregation/procedure/README.md +43 -43
  29. package/src/admin/aggregation/procedure/index.ts +1 -1
  30. package/src/admin/aggregation/procedure/procedure.aggregation.service.ts +702 -702
  31. package/src/admin/aggregation/reviews/index.ts +1 -1
  32. package/src/admin/aggregation/reviews/reviews.aggregation.service.ts +641 -641
  33. package/src/admin/booking/README.md +125 -125
  34. package/src/admin/booking/booking.admin.ts +1037 -1037
  35. package/src/admin/booking/booking.calculator.ts +712 -699
  36. package/src/admin/booking/booking.types.ts +59 -59
  37. package/src/admin/booking/index.ts +3 -3
  38. package/src/admin/booking/timezones-problem.md +185 -185
  39. package/src/admin/calendar/README.md +7 -7
  40. package/src/admin/calendar/calendar.admin.service.ts +345 -345
  41. package/src/admin/calendar/index.ts +1 -1
  42. package/src/admin/documentation-templates/document-manager.admin.ts +260 -260
  43. package/src/admin/documentation-templates/index.ts +1 -1
  44. package/src/admin/free-consultation/free-consultation-utils.admin.ts +148 -148
  45. package/src/admin/free-consultation/index.ts +1 -1
  46. package/src/admin/index.ts +75 -75
  47. package/src/admin/logger/index.ts +78 -78
  48. package/src/admin/mailing/README.md +95 -95
  49. package/src/admin/mailing/appointment/appointment.mailing.service.ts +732 -732
  50. package/src/admin/mailing/appointment/index.ts +1 -1
  51. package/src/admin/mailing/appointment/templates/patient/appointment-confirmed.html +40 -40
  52. package/src/admin/mailing/base.mailing.service.ts +208 -208
  53. package/src/admin/mailing/index.ts +3 -3
  54. package/src/admin/mailing/practitionerInvite/existing-practitioner-invite.mailing.ts +611 -611
  55. package/src/admin/mailing/practitionerInvite/index.ts +2 -2
  56. package/src/admin/mailing/practitionerInvite/practitionerInvite.mailing.ts +395 -395
  57. package/src/admin/mailing/practitionerInvite/templates/existing-practitioner-invitation.template.ts +155 -155
  58. package/src/admin/mailing/practitionerInvite/templates/invitation.template.ts +101 -101
  59. package/src/admin/mailing/practitionerInvite/templates/invite-accepted-notification.template.ts +228 -228
  60. package/src/admin/mailing/practitionerInvite/templates/invite-rejected-notification.template.ts +242 -242
  61. package/src/admin/notifications/index.ts +1 -1
  62. package/src/admin/notifications/notifications.admin.ts +710 -710
  63. package/src/admin/requirements/README.md +128 -128
  64. package/src/admin/requirements/index.ts +1 -1
  65. package/src/admin/requirements/patient-requirements.admin.service.ts +475 -475
  66. package/src/admin/users/index.ts +1 -1
  67. package/src/admin/users/user-profile.admin.ts +405 -405
  68. package/src/backoffice/constants/certification.constants.ts +13 -13
  69. package/src/backoffice/constants/index.ts +1 -1
  70. package/src/backoffice/errors/backoffice.errors.ts +181 -181
  71. package/src/backoffice/errors/index.ts +1 -1
  72. package/src/backoffice/expo-safe/README.md +26 -26
  73. package/src/backoffice/expo-safe/index.ts +41 -41
  74. package/src/backoffice/index.ts +5 -5
  75. package/src/backoffice/services/FIXES_README.md +102 -102
  76. package/src/backoffice/services/README.md +40 -40
  77. package/src/backoffice/services/brand.service.ts +256 -256
  78. package/src/backoffice/services/category.service.ts +318 -318
  79. package/src/backoffice/services/constants.service.ts +385 -385
  80. package/src/backoffice/services/documentation-template.service.ts +202 -202
  81. package/src/backoffice/services/index.ts +8 -8
  82. package/src/backoffice/services/migrate-products.ts +116 -116
  83. package/src/backoffice/services/product.service.ts +553 -553
  84. package/src/backoffice/services/requirement.service.ts +235 -235
  85. package/src/backoffice/services/subcategory.service.ts +395 -395
  86. package/src/backoffice/services/technology.service.ts +1070 -1070
  87. package/src/backoffice/types/README.md +12 -12
  88. package/src/backoffice/types/admin-constants.types.ts +69 -69
  89. package/src/backoffice/types/brand.types.ts +29 -29
  90. package/src/backoffice/types/category.types.ts +62 -62
  91. package/src/backoffice/types/documentation-templates.types.ts +28 -28
  92. package/src/backoffice/types/index.ts +10 -10
  93. package/src/backoffice/types/procedure-product.types.ts +38 -38
  94. package/src/backoffice/types/product.types.ts +240 -240
  95. package/src/backoffice/types/requirement.types.ts +63 -63
  96. package/src/backoffice/types/static/README.md +18 -18
  97. package/src/backoffice/types/static/blocking-condition.types.ts +21 -21
  98. package/src/backoffice/types/static/certification.types.ts +37 -37
  99. package/src/backoffice/types/static/contraindication.types.ts +19 -19
  100. package/src/backoffice/types/static/index.ts +6 -6
  101. package/src/backoffice/types/static/pricing.types.ts +16 -16
  102. package/src/backoffice/types/static/procedure-family.types.ts +14 -14
  103. package/src/backoffice/types/static/treatment-benefit.types.ts +22 -22
  104. package/src/backoffice/types/subcategory.types.ts +34 -34
  105. package/src/backoffice/types/technology.types.ts +161 -161
  106. package/src/backoffice/validations/index.ts +1 -1
  107. package/src/backoffice/validations/schemas.ts +163 -163
  108. package/src/config/__mocks__/firebase.ts +99 -99
  109. package/src/config/firebase.ts +78 -78
  110. package/src/config/index.ts +9 -9
  111. package/src/errors/auth.error.ts +6 -6
  112. package/src/errors/auth.errors.ts +200 -200
  113. package/src/errors/clinic.errors.ts +32 -32
  114. package/src/errors/firebase.errors.ts +47 -47
  115. package/src/errors/user.errors.ts +99 -99
  116. package/src/index.backup.ts +407 -407
  117. package/src/index.ts +6 -6
  118. package/src/locales/en.ts +31 -31
  119. package/src/recommender/admin/index.ts +1 -1
  120. package/src/recommender/admin/services/recommender.service.admin.ts +5 -5
  121. package/src/recommender/front/index.ts +1 -1
  122. package/src/recommender/front/services/onboarding.service.ts +5 -5
  123. package/src/recommender/front/services/recommender.service.ts +3 -3
  124. package/src/recommender/index.ts +1 -1
  125. package/src/services/PATIENTAUTH.MD +197 -197
  126. package/src/services/README.md +106 -106
  127. package/src/services/__tests__/auth/auth.mock.test.ts +17 -17
  128. package/src/services/__tests__/auth/auth.setup.ts +293 -293
  129. package/src/services/__tests__/auth.service.test.ts +346 -346
  130. package/src/services/__tests__/base.service.test.ts +77 -77
  131. package/src/services/__tests__/user.service.test.ts +528 -528
  132. package/src/services/appointment/README.md +17 -17
  133. package/src/services/appointment/appointment.service.ts +2082 -2082
  134. package/src/services/appointment/index.ts +1 -1
  135. package/src/services/appointment/utils/appointment.utils.ts +552 -552
  136. package/src/services/appointment/utils/extended-procedure.utils.ts +314 -314
  137. package/src/services/appointment/utils/form-initialization.utils.ts +225 -225
  138. package/src/services/appointment/utils/recommended-procedure.utils.ts +195 -195
  139. package/src/services/appointment/utils/zone-management.utils.ts +353 -353
  140. package/src/services/appointment/utils/zone-photo.utils.ts +152 -152
  141. package/src/services/auth/auth.service.ts +989 -989
  142. package/src/services/auth/auth.v2.service.ts +961 -961
  143. package/src/services/auth/index.ts +7 -7
  144. package/src/services/auth/utils/error.utils.ts +90 -90
  145. package/src/services/auth/utils/firebase.utils.ts +49 -49
  146. package/src/services/auth/utils/index.ts +21 -21
  147. package/src/services/auth/utils/practitioner.utils.ts +125 -125
  148. package/src/services/base.service.ts +41 -41
  149. package/src/services/calendar/calendar.service.ts +1077 -1077
  150. package/src/services/calendar/calendar.v2.service.ts +1683 -1683
  151. package/src/services/calendar/calendar.v3.service.ts +313 -313
  152. package/src/services/calendar/externalCalendar.service.ts +178 -178
  153. package/src/services/calendar/index.ts +5 -5
  154. package/src/services/calendar/synced-calendars.service.ts +743 -743
  155. package/src/services/calendar/utils/appointment.utils.ts +265 -265
  156. package/src/services/calendar/utils/calendar-event.utils.ts +646 -646
  157. package/src/services/calendar/utils/clinic.utils.ts +237 -237
  158. package/src/services/calendar/utils/docs.utils.ts +157 -157
  159. package/src/services/calendar/utils/google-calendar.utils.ts +697 -697
  160. package/src/services/calendar/utils/index.ts +8 -8
  161. package/src/services/calendar/utils/patient.utils.ts +198 -198
  162. package/src/services/calendar/utils/practitioner.utils.ts +221 -221
  163. package/src/services/calendar/utils/synced-calendar.utils.ts +472 -472
  164. package/src/services/clinic/README.md +204 -204
  165. package/src/services/clinic/__tests__/clinic-admin.service.test.ts +287 -287
  166. package/src/services/clinic/__tests__/clinic-group.service.test.ts +352 -352
  167. package/src/services/clinic/__tests__/clinic.service.test.ts +354 -354
  168. package/src/services/clinic/billing-transactions.service.ts +217 -217
  169. package/src/services/clinic/clinic-admin.service.ts +202 -202
  170. package/src/services/clinic/clinic-group.service.ts +310 -310
  171. package/src/services/clinic/clinic.service.ts +708 -708
  172. package/src/services/clinic/index.ts +5 -5
  173. package/src/services/clinic/practitioner-invite.service.ts +519 -519
  174. package/src/services/clinic/utils/admin.utils.ts +551 -551
  175. package/src/services/clinic/utils/clinic-group.utils.ts +646 -646
  176. package/src/services/clinic/utils/clinic.utils.ts +949 -949
  177. package/src/services/clinic/utils/filter.utils.d.ts +23 -23
  178. package/src/services/clinic/utils/filter.utils.ts +446 -446
  179. package/src/services/clinic/utils/index.ts +11 -11
  180. package/src/services/clinic/utils/photos.utils.ts +188 -188
  181. package/src/services/clinic/utils/search.utils.ts +84 -84
  182. package/src/services/clinic/utils/tag.utils.ts +124 -124
  183. package/src/services/documentation-templates/documentation-template.service.ts +537 -537
  184. package/src/services/documentation-templates/filled-document.service.ts +587 -587
  185. package/src/services/documentation-templates/index.ts +2 -2
  186. package/src/services/index.ts +13 -13
  187. package/src/services/media/index.ts +1 -1
  188. package/src/services/media/media.service.ts +418 -418
  189. package/src/services/notifications/__tests__/notification.service.test.ts +242 -242
  190. package/src/services/notifications/index.ts +1 -1
  191. package/src/services/notifications/notification.service.ts +215 -215
  192. package/src/services/patient/README.md +48 -48
  193. package/src/services/patient/To-Do.md +43 -43
  194. package/src/services/patient/__tests__/patient.service.test.ts +294 -294
  195. package/src/services/patient/index.ts +2 -2
  196. package/src/services/patient/patient.service.ts +883 -883
  197. package/src/services/patient/patientRequirements.service.ts +285 -285
  198. package/src/services/patient/utils/aesthetic-analysis.utils.ts +176 -176
  199. package/src/services/patient/utils/clinic.utils.ts +80 -80
  200. package/src/services/patient/utils/docs.utils.ts +142 -142
  201. package/src/services/patient/utils/index.ts +9 -9
  202. package/src/services/patient/utils/location.utils.ts +126 -126
  203. package/src/services/patient/utils/medical-stuff.utils.ts +143 -143
  204. package/src/services/patient/utils/medical.utils.ts +458 -458
  205. package/src/services/patient/utils/practitioner.utils.ts +260 -260
  206. package/src/services/patient/utils/profile.utils.ts +510 -510
  207. package/src/services/patient/utils/sensitive.utils.ts +260 -260
  208. package/src/services/patient/utils/token.utils.ts +211 -211
  209. package/src/services/practitioner/README.md +145 -145
  210. package/src/services/practitioner/index.ts +1 -1
  211. package/src/services/practitioner/practitioner.service.ts +1742 -1742
  212. package/src/services/procedure/README.md +163 -163
  213. package/src/services/procedure/index.ts +1 -1
  214. package/src/services/procedure/procedure.service.ts +1682 -1682
  215. package/src/services/reviews/index.ts +1 -1
  216. package/src/services/reviews/reviews.service.ts +636 -636
  217. package/src/services/user/index.ts +1 -1
  218. package/src/services/user/user.service.ts +489 -489
  219. package/src/services/user/user.v2.service.ts +466 -466
  220. package/src/types/appointment/index.ts +453 -453
  221. package/src/types/calendar/index.ts +258 -258
  222. package/src/types/calendar/synced-calendar.types.ts +66 -66
  223. package/src/types/clinic/index.ts +489 -489
  224. package/src/types/clinic/practitioner-invite.types.ts +91 -91
  225. package/src/types/clinic/preferences.types.ts +159 -159
  226. package/src/types/clinic/to-do +3 -3
  227. package/src/types/documentation-templates/index.ts +308 -308
  228. package/src/types/index.ts +44 -44
  229. package/src/types/notifications/README.md +77 -77
  230. package/src/types/notifications/index.ts +265 -265
  231. package/src/types/patient/aesthetic-analysis.types.ts +66 -66
  232. package/src/types/patient/allergies.ts +58 -58
  233. package/src/types/patient/index.ts +273 -273
  234. package/src/types/patient/medical-info.types.ts +152 -152
  235. package/src/types/patient/patient-requirements.ts +92 -81
  236. package/src/types/patient/token.types.ts +61 -61
  237. package/src/types/practitioner/index.ts +206 -206
  238. package/src/types/procedure/index.ts +181 -181
  239. package/src/types/profile/index.ts +39 -39
  240. package/src/types/reviews/index.ts +130 -130
  241. package/src/types/tz-lookup.d.ts +4 -4
  242. package/src/types/user/index.ts +38 -38
  243. package/src/utils/TIMESTAMPS.md +176 -176
  244. package/src/utils/TimestampUtils.ts +241 -241
  245. package/src/utils/index.ts +1 -1
  246. package/src/validations/appointment.schema.ts +574 -574
  247. package/src/validations/calendar.schema.ts +225 -225
  248. package/src/validations/clinic.schema.ts +493 -493
  249. package/src/validations/common.schema.ts +25 -25
  250. package/src/validations/documentation-templates/index.ts +1 -1
  251. package/src/validations/documentation-templates/template.schema.ts +220 -220
  252. package/src/validations/documentation-templates.schema.ts +10 -10
  253. package/src/validations/index.ts +20 -20
  254. package/src/validations/media.schema.ts +10 -10
  255. package/src/validations/notification.schema.ts +90 -90
  256. package/src/validations/patient/aesthetic-analysis.schema.ts +55 -55
  257. package/src/validations/patient/medical-info.schema.ts +125 -125
  258. package/src/validations/patient/patient-requirements.schema.ts +84 -75
  259. package/src/validations/patient/token.schema.ts +29 -29
  260. package/src/validations/patient.schema.ts +216 -216
  261. package/src/validations/practitioner.schema.ts +222 -222
  262. package/src/validations/procedure-product.schema.ts +41 -41
  263. package/src/validations/procedure.schema.ts +124 -124
  264. package/src/validations/profile-info.schema.ts +41 -41
  265. package/src/validations/reviews.schema.ts +189 -189
  266. package/src/validations/schemas.ts +104 -104
  267. package/src/validations/shared.schema.ts +78 -78
@@ -1,418 +1,418 @@
1
- import { Auth } from "firebase/auth";
2
- import { Firestore, Timestamp } from "firebase/firestore";
3
- import { FirebaseApp } from "firebase/app";
4
- import {
5
- ref,
6
- uploadBytes,
7
- getDownloadURL,
8
- deleteObject,
9
- getBytes,
10
- } from "firebase/storage";
11
- import {
12
- doc,
13
- getDoc,
14
- setDoc,
15
- updateDoc,
16
- collection,
17
- query,
18
- where,
19
- limit,
20
- getDocs,
21
- deleteDoc,
22
- orderBy,
23
- } from "firebase/firestore";
24
- import { BaseService } from "../base.service";
25
-
26
- /**
27
- * Enum for media access levels
28
- */
29
- export enum MediaAccessLevel {
30
- PUBLIC = "public",
31
- PRIVATE = "private",
32
- CONFIDENTIAL = "confidential",
33
- }
34
-
35
- /**
36
- * Type that allows a field to be either a URL string or a File object
37
- */
38
- export type MediaResource = string | File | Blob;
39
-
40
- /**
41
- * Media file metadata interface
42
- */
43
- export interface MediaMetadata {
44
- id: string; // Unique ID for the media, also Firestore document ID
45
- name: string; // Original file name or a descriptive name
46
- url: string; // Publicly accessible download URL
47
- contentType: string; // Mime type of the file
48
- size: number; // Size of the file in bytes
49
- createdAt: Timestamp; // Firestore Timestamp of creation
50
- accessLevel: MediaAccessLevel; // Access level
51
- ownerId: string; // ID of the entity that owns this media (e.g., patientId, clinicId)
52
- collectionName: string; // Name of the collection this media belongs to (e.g., 'patient_profile_pictures', 'clinic_gallery')
53
- path: string; // Full path in Firebase Storage
54
- updatedAt?: Timestamp; // Firestore Timestamp of last update
55
- }
56
-
57
- export const MEDIA_METADATA_COLLECTION = "media_metadata";
58
-
59
- export class MediaService extends BaseService {
60
- constructor(...args: ConstructorParameters<typeof BaseService>) {
61
- super(...args);
62
- }
63
-
64
- /**
65
- * Upload a media file, store its metadata, and return the metadata including the URL.
66
- * @param file - The file to upload.
67
- * @param ownerId - ID of the owner (user, patient, clinic, etc.).
68
- * @param accessLevel - Access level (public, private, confidential).
69
- * @param collectionName - The logical collection name this media belongs to (e.g., 'patient_profile_pictures', 'clinic_logos').
70
- * @param originalFileName - Optional: the original name of the file, if not using file.name.
71
- * @returns Promise with the media metadata.
72
- */
73
- async uploadMedia(
74
- file: File | Blob,
75
- ownerId: string,
76
- accessLevel: MediaAccessLevel,
77
- collectionName: string,
78
- originalFileName?: string
79
- ): Promise<MediaMetadata> {
80
- const mediaId = this.generateId();
81
- const fileNameToUse =
82
- originalFileName || (file instanceof File ? file.name : file.toString());
83
- // Using collectionName in the path for better organization
84
- const uniqueFileName = `${mediaId}-${fileNameToUse}`;
85
- const filePath = `media/${accessLevel}/${ownerId}/${collectionName}/${uniqueFileName}`;
86
-
87
- console.log(`[MediaService] Uploading file to: ${filePath}`);
88
- const storageRef = ref(this.storage, filePath);
89
- try {
90
- const uploadResult = await uploadBytes(storageRef, file, {
91
- contentType: file.type,
92
- });
93
- console.log("[MediaService] File uploaded successfully", uploadResult);
94
- const downloadURL = await getDownloadURL(uploadResult.ref);
95
- console.log("[MediaService] Got download URL:", downloadURL);
96
- const metadata: MediaMetadata = {
97
- id: mediaId,
98
- name: fileNameToUse,
99
- url: downloadURL,
100
- contentType: file.type,
101
- size: file.size,
102
- createdAt: Timestamp.now(),
103
- accessLevel: accessLevel,
104
- ownerId: ownerId,
105
- collectionName: collectionName,
106
- path: filePath,
107
- };
108
- const metadataDocRef = doc(this.db, MEDIA_METADATA_COLLECTION, mediaId);
109
- await setDoc(metadataDocRef, metadata);
110
- console.log("[MediaService] Metadata stored in Firestore:", mediaId);
111
- return metadata;
112
- } catch (error) {
113
- console.error("[MediaService] Error during media upload:", error);
114
- throw error;
115
- }
116
- }
117
-
118
- /**
119
- * Get media metadata from Firestore by its ID.
120
- * @param mediaId - ID of the media.
121
- * @returns Promise with the media metadata or null if not found.
122
- */
123
- async getMediaMetadata(mediaId: string): Promise<MediaMetadata | null> {
124
- console.log(`[MediaService] Getting media metadata for ID: ${mediaId}`);
125
- const docRef = doc(this.db, MEDIA_METADATA_COLLECTION, mediaId);
126
- const docSnap = await getDoc(docRef);
127
-
128
- if (docSnap.exists()) {
129
- console.log("[MediaService] Metadata found:", docSnap.data());
130
- return docSnap.data() as MediaMetadata;
131
- }
132
- console.log("[MediaService] No metadata found for ID:", mediaId);
133
- return null;
134
- }
135
-
136
- /**
137
- * Get media metadata from Firestore by its public URL.
138
- * @param url - The public URL of the media file.
139
- * @returns Promise with the media metadata or null if not found.
140
- */
141
- async getMediaMetadataByUrl(url: string): Promise<MediaMetadata | null> {
142
- console.log(`[MediaService] Getting media metadata by URL: ${url}`);
143
- const q = query(
144
- collection(this.db, MEDIA_METADATA_COLLECTION),
145
- where("url", "==", url),
146
- limit(1)
147
- );
148
-
149
- try {
150
- const querySnapshot = await getDocs(q);
151
- if (!querySnapshot.empty) {
152
- const metadata = querySnapshot.docs[0].data() as MediaMetadata;
153
- console.log("[MediaService] Metadata found by URL:", metadata);
154
- return metadata;
155
- }
156
- console.log("[MediaService] No metadata found for URL:", url);
157
- return null;
158
- } catch (error) {
159
- console.error("[MediaService] Error fetching metadata by URL:", error);
160
- throw error;
161
- }
162
- }
163
-
164
- /**
165
- * Delete media from storage and remove metadata from Firestore.
166
- * @param mediaId - ID of the media to delete.
167
- */
168
- async deleteMedia(mediaId: string): Promise<void> {
169
- console.log(`[MediaService] Deleting media with ID: ${mediaId}`);
170
- const metadata = await this.getMediaMetadata(mediaId);
171
-
172
- if (!metadata) {
173
- console.warn(
174
- `[MediaService] Metadata not found for media ID ${mediaId}. Cannot delete.`
175
- );
176
- // Optionally throw an error or return a status
177
- return;
178
- }
179
-
180
- const storageFileRef = ref(this.storage, metadata.path);
181
-
182
- try {
183
- // Delete the file from Firebase Storage
184
- await deleteObject(storageFileRef);
185
- console.log(`[MediaService] File deleted from Storage: ${metadata.path}`);
186
-
187
- // Delete the metadata from Firestore
188
- const metadataDocRef = doc(this.db, MEDIA_METADATA_COLLECTION, mediaId);
189
- await deleteDoc(metadataDocRef);
190
- console.log(
191
- `[MediaService] Metadata deleted from Firestore for ID: ${mediaId}`
192
- );
193
- } catch (error) {
194
- console.error(`[MediaService] Error deleting media ${mediaId}:`, error);
195
- // Handle specific errors, e.g., file not found in storage, permissions issues
196
- // If Firestore delete fails after storage delete, there might be an orphaned metadata entry.
197
- // Consider how to handle such inconsistencies or if it's acceptable.
198
- throw error;
199
- }
200
- }
201
-
202
- /**
203
- * Update media access level. This involves moving the file in Firebase Storage
204
- * to a new path reflecting the new access level, and updating its metadata.
205
- * @param mediaId - ID of the media to update.
206
- * @param newAccessLevel - New access level.
207
- * @returns Promise with the updated media metadata, or null if metadata not found.
208
- */
209
- async updateMediaAccessLevel(
210
- mediaId: string,
211
- newAccessLevel: MediaAccessLevel
212
- ): Promise<MediaMetadata | null> {
213
- console.log(
214
- `[MediaService] Attempting to update access level for media ID: ${mediaId} to ${newAccessLevel}`
215
- );
216
- const metadata = await this.getMediaMetadata(mediaId);
217
-
218
- if (!metadata) {
219
- console.warn(
220
- `[MediaService] Metadata not found for media ID ${mediaId}. Cannot update access level.`
221
- );
222
- return null;
223
- }
224
-
225
- if (metadata.accessLevel === newAccessLevel) {
226
- console.log(
227
- `[MediaService] Media ID ${mediaId} already has access level ${newAccessLevel}. Updating timestamp only.`
228
- );
229
- const metadataDocRef = doc(this.db, MEDIA_METADATA_COLLECTION, mediaId);
230
- try {
231
- await updateDoc(metadataDocRef, { updatedAt: Timestamp.now() });
232
- return { ...metadata, updatedAt: Timestamp.now() };
233
- } catch (error) {
234
- console.error(
235
- `[MediaService] Error updating timestamp for media ID ${mediaId}:`,
236
- error
237
- );
238
- throw error; // Re-throw to indicate the update wasn't fully successful
239
- }
240
- }
241
-
242
- const oldStoragePath = metadata.path;
243
- // Ensure the filename part remains consistent using metadata.id and metadata.name
244
- const fileNamePart = `${metadata.id}-${metadata.name}`;
245
- const newStoragePath = `media/${newAccessLevel}/${metadata.ownerId}/${metadata.collectionName}/${fileNamePart}`;
246
-
247
- console.log(
248
- `[MediaService] Moving file for ${mediaId} from ${oldStoragePath} to ${newStoragePath}`
249
- );
250
-
251
- const oldStorageFileRef = ref(this.storage, oldStoragePath);
252
- const newStorageFileRef = ref(this.storage, newStoragePath);
253
-
254
- try {
255
- // 1. Download (get bytes of) the old file
256
- console.log(`[MediaService] Downloading bytes from ${oldStoragePath}`);
257
- const fileBytes = await getBytes(oldStorageFileRef);
258
- console.log(
259
- `[MediaService] Successfully downloaded ${fileBytes.byteLength} bytes from ${oldStoragePath}`
260
- );
261
-
262
- // 2. Upload the bytes to the new path
263
- console.log(`[MediaService] Uploading bytes to ${newStoragePath}`);
264
- await uploadBytes(newStorageFileRef, fileBytes, {
265
- contentType: metadata.contentType,
266
- });
267
- console.log(
268
- `[MediaService] Successfully uploaded bytes to ${newStoragePath}`
269
- );
270
-
271
- // 3. Get the new download URL
272
- const newDownloadURL = await getDownloadURL(newStorageFileRef);
273
- console.log(
274
- `[MediaService] Got new download URL for ${newStoragePath}: ${newDownloadURL}`
275
- );
276
-
277
- // 4. Prepare metadata for update
278
- const updateData = {
279
- accessLevel: newAccessLevel,
280
- path: newStoragePath,
281
- url: newDownloadURL,
282
- updatedAt: Timestamp.now(),
283
- };
284
-
285
- // 5. Update Firestore metadata
286
- const metadataDocRef = doc(this.db, MEDIA_METADATA_COLLECTION, mediaId);
287
- console.log(
288
- `[MediaService] Updating Firestore metadata for ${mediaId} with new data:`,
289
- updateData
290
- );
291
- await updateDoc(metadataDocRef, updateData);
292
- console.log(
293
- `[MediaService] Successfully updated Firestore metadata for ${mediaId}`
294
- );
295
-
296
- // 6. Delete the old file from Firebase Storage (after metadata is updated)
297
- try {
298
- console.log(`[MediaService] Deleting old file from ${oldStoragePath}`);
299
- await deleteObject(oldStorageFileRef);
300
- console.log(
301
- `[MediaService] Successfully deleted old file from ${oldStoragePath}`
302
- );
303
- } catch (deleteError) {
304
- console.error(
305
- `[MediaService] Failed to delete old file from ${oldStoragePath} for media ID ${mediaId}. This file is now orphaned. Error:`,
306
- deleteError
307
- );
308
- // Do not re-throw here, as the primary operation (move and metadata update) was successful.
309
- // Log this issue for potential manual cleanup.
310
- }
311
-
312
- return { ...metadata, ...updateData } as MediaMetadata;
313
- } catch (error) {
314
- console.error(
315
- `[MediaService] Error updating media access level and moving file for ${mediaId}:`,
316
- error
317
- );
318
- // Attempt to clean up if new file was created but process failed before metadata update
319
- // This is a best-effort cleanup. If newDownloadURL is not defined, it means upload failed.
320
- // If newDownloadURL is defined but metadata update failed, new file might exist.
321
- if (
322
- newStorageFileRef &&
323
- (error as any).code !== "storage/object-not-found" &&
324
- (error as any).message?.includes("uploadBytes")
325
- ) {
326
- // This check is a bit heuristic. Ideally, check if new file actually exists.
327
- console.warn(
328
- `[MediaService] Attempting to delete partially uploaded file at ${newStoragePath} due to error.`
329
- );
330
- try {
331
- await deleteObject(newStorageFileRef);
332
- console.warn(
333
- `[MediaService] Cleaned up partially uploaded file at ${newStoragePath}.`
334
- );
335
- } catch (cleanupError) {
336
- console.error(
337
- `[MediaService] Failed to cleanup partially uploaded file at ${newStoragePath}:`,
338
- cleanupError
339
- );
340
- }
341
- }
342
- throw error; // Re-throw the original error to the caller
343
- }
344
- }
345
-
346
- /**
347
- * List all media for an owner, optionally filtered by collection and access level.
348
- * @param ownerId - ID of the owner.
349
- * @param collectionName - Optional: Filter by collection name.
350
- * @param accessLevel - Optional: Filter by access level.
351
- * @param count - Optional: Number of items to fetch.
352
- * @param startAfterId - Optional: ID of the document to start after (for pagination).
353
- */
354
- async listMedia(
355
- ownerId: string,
356
- collectionName?: string,
357
- accessLevel?: MediaAccessLevel,
358
- count?: number,
359
- startAfterId?: string // Using ID for pagination simplicity with Firestore
360
- ): Promise<MediaMetadata[]> {
361
- console.log(`[MediaService] Listing media for owner: ${ownerId}`);
362
- let qConstraints: any[] = [where("ownerId", "==", ownerId)];
363
-
364
- if (collectionName) {
365
- qConstraints.push(where("collectionName", "==", collectionName));
366
- }
367
- if (accessLevel) {
368
- qConstraints.push(where("accessLevel", "==", accessLevel));
369
- }
370
-
371
- qConstraints.push(orderBy("createdAt", "desc")); // Example ordering
372
-
373
- if (count) {
374
- qConstraints.push(limit(count));
375
- }
376
-
377
- if (startAfterId) {
378
- const startAfterDoc = await this.getMediaMetadata(startAfterId);
379
- if (startAfterDoc) {
380
- // Placeholder: Firestore's startAfter needs a DocumentSnapshot or field values.
381
- // For robust pagination, pass the actual DocumentSnapshot or use field values from it.
382
- // e.g., qConstraints.push(startAfter(snapshotOfStartAfterDoc));
383
- }
384
- }
385
-
386
- const finalQuery = query(
387
- collection(this.db, MEDIA_METADATA_COLLECTION),
388
- ...qConstraints
389
- );
390
-
391
- try {
392
- const querySnapshot = await getDocs(finalQuery);
393
- const mediaList = querySnapshot.docs.map(
394
- (doc) => doc.data() as MediaMetadata
395
- );
396
- console.log(`[MediaService] Found ${mediaList.length} media items.`);
397
- return mediaList;
398
- } catch (error) {
399
- console.error("[MediaService] Error listing media:", error);
400
- throw error;
401
- }
402
- }
403
-
404
- /**
405
- * Get download URL for media. (Convenience, as URL is in metadata)
406
- * @param mediaId - ID of the media.
407
- */
408
- async getMediaDownloadUrl(mediaId: string): Promise<string | null> {
409
- console.log(`[MediaService] Getting download URL for media ID: ${mediaId}`);
410
- const metadata = await this.getMediaMetadata(mediaId);
411
- if (metadata && metadata.url) {
412
- console.log(`[MediaService] URL found: ${metadata.url}`);
413
- return metadata.url;
414
- }
415
- console.log(`[MediaService] URL not found for media ID: ${mediaId}`);
416
- return null;
417
- }
418
- }
1
+ import { Auth } from "firebase/auth";
2
+ import { Firestore, Timestamp } from "firebase/firestore";
3
+ import { FirebaseApp } from "firebase/app";
4
+ import {
5
+ ref,
6
+ uploadBytes,
7
+ getDownloadURL,
8
+ deleteObject,
9
+ getBytes,
10
+ } from "firebase/storage";
11
+ import {
12
+ doc,
13
+ getDoc,
14
+ setDoc,
15
+ updateDoc,
16
+ collection,
17
+ query,
18
+ where,
19
+ limit,
20
+ getDocs,
21
+ deleteDoc,
22
+ orderBy,
23
+ } from "firebase/firestore";
24
+ import { BaseService } from "../base.service";
25
+
26
+ /**
27
+ * Enum for media access levels
28
+ */
29
+ export enum MediaAccessLevel {
30
+ PUBLIC = "public",
31
+ PRIVATE = "private",
32
+ CONFIDENTIAL = "confidential",
33
+ }
34
+
35
+ /**
36
+ * Type that allows a field to be either a URL string or a File object
37
+ */
38
+ export type MediaResource = string | File | Blob;
39
+
40
+ /**
41
+ * Media file metadata interface
42
+ */
43
+ export interface MediaMetadata {
44
+ id: string; // Unique ID for the media, also Firestore document ID
45
+ name: string; // Original file name or a descriptive name
46
+ url: string; // Publicly accessible download URL
47
+ contentType: string; // Mime type of the file
48
+ size: number; // Size of the file in bytes
49
+ createdAt: Timestamp; // Firestore Timestamp of creation
50
+ accessLevel: MediaAccessLevel; // Access level
51
+ ownerId: string; // ID of the entity that owns this media (e.g., patientId, clinicId)
52
+ collectionName: string; // Name of the collection this media belongs to (e.g., 'patient_profile_pictures', 'clinic_gallery')
53
+ path: string; // Full path in Firebase Storage
54
+ updatedAt?: Timestamp; // Firestore Timestamp of last update
55
+ }
56
+
57
+ export const MEDIA_METADATA_COLLECTION = "media_metadata";
58
+
59
+ export class MediaService extends BaseService {
60
+ constructor(...args: ConstructorParameters<typeof BaseService>) {
61
+ super(...args);
62
+ }
63
+
64
+ /**
65
+ * Upload a media file, store its metadata, and return the metadata including the URL.
66
+ * @param file - The file to upload.
67
+ * @param ownerId - ID of the owner (user, patient, clinic, etc.).
68
+ * @param accessLevel - Access level (public, private, confidential).
69
+ * @param collectionName - The logical collection name this media belongs to (e.g., 'patient_profile_pictures', 'clinic_logos').
70
+ * @param originalFileName - Optional: the original name of the file, if not using file.name.
71
+ * @returns Promise with the media metadata.
72
+ */
73
+ async uploadMedia(
74
+ file: File | Blob,
75
+ ownerId: string,
76
+ accessLevel: MediaAccessLevel,
77
+ collectionName: string,
78
+ originalFileName?: string
79
+ ): Promise<MediaMetadata> {
80
+ const mediaId = this.generateId();
81
+ const fileNameToUse =
82
+ originalFileName || (file instanceof File ? file.name : file.toString());
83
+ // Using collectionName in the path for better organization
84
+ const uniqueFileName = `${mediaId}-${fileNameToUse}`;
85
+ const filePath = `media/${accessLevel}/${ownerId}/${collectionName}/${uniqueFileName}`;
86
+
87
+ console.log(`[MediaService] Uploading file to: ${filePath}`);
88
+ const storageRef = ref(this.storage, filePath);
89
+ try {
90
+ const uploadResult = await uploadBytes(storageRef, file, {
91
+ contentType: file.type,
92
+ });
93
+ console.log("[MediaService] File uploaded successfully", uploadResult);
94
+ const downloadURL = await getDownloadURL(uploadResult.ref);
95
+ console.log("[MediaService] Got download URL:", downloadURL);
96
+ const metadata: MediaMetadata = {
97
+ id: mediaId,
98
+ name: fileNameToUse,
99
+ url: downloadURL,
100
+ contentType: file.type,
101
+ size: file.size,
102
+ createdAt: Timestamp.now(),
103
+ accessLevel: accessLevel,
104
+ ownerId: ownerId,
105
+ collectionName: collectionName,
106
+ path: filePath,
107
+ };
108
+ const metadataDocRef = doc(this.db, MEDIA_METADATA_COLLECTION, mediaId);
109
+ await setDoc(metadataDocRef, metadata);
110
+ console.log("[MediaService] Metadata stored in Firestore:", mediaId);
111
+ return metadata;
112
+ } catch (error) {
113
+ console.error("[MediaService] Error during media upload:", error);
114
+ throw error;
115
+ }
116
+ }
117
+
118
+ /**
119
+ * Get media metadata from Firestore by its ID.
120
+ * @param mediaId - ID of the media.
121
+ * @returns Promise with the media metadata or null if not found.
122
+ */
123
+ async getMediaMetadata(mediaId: string): Promise<MediaMetadata | null> {
124
+ console.log(`[MediaService] Getting media metadata for ID: ${mediaId}`);
125
+ const docRef = doc(this.db, MEDIA_METADATA_COLLECTION, mediaId);
126
+ const docSnap = await getDoc(docRef);
127
+
128
+ if (docSnap.exists()) {
129
+ console.log("[MediaService] Metadata found:", docSnap.data());
130
+ return docSnap.data() as MediaMetadata;
131
+ }
132
+ console.log("[MediaService] No metadata found for ID:", mediaId);
133
+ return null;
134
+ }
135
+
136
+ /**
137
+ * Get media metadata from Firestore by its public URL.
138
+ * @param url - The public URL of the media file.
139
+ * @returns Promise with the media metadata or null if not found.
140
+ */
141
+ async getMediaMetadataByUrl(url: string): Promise<MediaMetadata | null> {
142
+ console.log(`[MediaService] Getting media metadata by URL: ${url}`);
143
+ const q = query(
144
+ collection(this.db, MEDIA_METADATA_COLLECTION),
145
+ where("url", "==", url),
146
+ limit(1)
147
+ );
148
+
149
+ try {
150
+ const querySnapshot = await getDocs(q);
151
+ if (!querySnapshot.empty) {
152
+ const metadata = querySnapshot.docs[0].data() as MediaMetadata;
153
+ console.log("[MediaService] Metadata found by URL:", metadata);
154
+ return metadata;
155
+ }
156
+ console.log("[MediaService] No metadata found for URL:", url);
157
+ return null;
158
+ } catch (error) {
159
+ console.error("[MediaService] Error fetching metadata by URL:", error);
160
+ throw error;
161
+ }
162
+ }
163
+
164
+ /**
165
+ * Delete media from storage and remove metadata from Firestore.
166
+ * @param mediaId - ID of the media to delete.
167
+ */
168
+ async deleteMedia(mediaId: string): Promise<void> {
169
+ console.log(`[MediaService] Deleting media with ID: ${mediaId}`);
170
+ const metadata = await this.getMediaMetadata(mediaId);
171
+
172
+ if (!metadata) {
173
+ console.warn(
174
+ `[MediaService] Metadata not found for media ID ${mediaId}. Cannot delete.`
175
+ );
176
+ // Optionally throw an error or return a status
177
+ return;
178
+ }
179
+
180
+ const storageFileRef = ref(this.storage, metadata.path);
181
+
182
+ try {
183
+ // Delete the file from Firebase Storage
184
+ await deleteObject(storageFileRef);
185
+ console.log(`[MediaService] File deleted from Storage: ${metadata.path}`);
186
+
187
+ // Delete the metadata from Firestore
188
+ const metadataDocRef = doc(this.db, MEDIA_METADATA_COLLECTION, mediaId);
189
+ await deleteDoc(metadataDocRef);
190
+ console.log(
191
+ `[MediaService] Metadata deleted from Firestore for ID: ${mediaId}`
192
+ );
193
+ } catch (error) {
194
+ console.error(`[MediaService] Error deleting media ${mediaId}:`, error);
195
+ // Handle specific errors, e.g., file not found in storage, permissions issues
196
+ // If Firestore delete fails after storage delete, there might be an orphaned metadata entry.
197
+ // Consider how to handle such inconsistencies or if it's acceptable.
198
+ throw error;
199
+ }
200
+ }
201
+
202
+ /**
203
+ * Update media access level. This involves moving the file in Firebase Storage
204
+ * to a new path reflecting the new access level, and updating its metadata.
205
+ * @param mediaId - ID of the media to update.
206
+ * @param newAccessLevel - New access level.
207
+ * @returns Promise with the updated media metadata, or null if metadata not found.
208
+ */
209
+ async updateMediaAccessLevel(
210
+ mediaId: string,
211
+ newAccessLevel: MediaAccessLevel
212
+ ): Promise<MediaMetadata | null> {
213
+ console.log(
214
+ `[MediaService] Attempting to update access level for media ID: ${mediaId} to ${newAccessLevel}`
215
+ );
216
+ const metadata = await this.getMediaMetadata(mediaId);
217
+
218
+ if (!metadata) {
219
+ console.warn(
220
+ `[MediaService] Metadata not found for media ID ${mediaId}. Cannot update access level.`
221
+ );
222
+ return null;
223
+ }
224
+
225
+ if (metadata.accessLevel === newAccessLevel) {
226
+ console.log(
227
+ `[MediaService] Media ID ${mediaId} already has access level ${newAccessLevel}. Updating timestamp only.`
228
+ );
229
+ const metadataDocRef = doc(this.db, MEDIA_METADATA_COLLECTION, mediaId);
230
+ try {
231
+ await updateDoc(metadataDocRef, { updatedAt: Timestamp.now() });
232
+ return { ...metadata, updatedAt: Timestamp.now() };
233
+ } catch (error) {
234
+ console.error(
235
+ `[MediaService] Error updating timestamp for media ID ${mediaId}:`,
236
+ error
237
+ );
238
+ throw error; // Re-throw to indicate the update wasn't fully successful
239
+ }
240
+ }
241
+
242
+ const oldStoragePath = metadata.path;
243
+ // Ensure the filename part remains consistent using metadata.id and metadata.name
244
+ const fileNamePart = `${metadata.id}-${metadata.name}`;
245
+ const newStoragePath = `media/${newAccessLevel}/${metadata.ownerId}/${metadata.collectionName}/${fileNamePart}`;
246
+
247
+ console.log(
248
+ `[MediaService] Moving file for ${mediaId} from ${oldStoragePath} to ${newStoragePath}`
249
+ );
250
+
251
+ const oldStorageFileRef = ref(this.storage, oldStoragePath);
252
+ const newStorageFileRef = ref(this.storage, newStoragePath);
253
+
254
+ try {
255
+ // 1. Download (get bytes of) the old file
256
+ console.log(`[MediaService] Downloading bytes from ${oldStoragePath}`);
257
+ const fileBytes = await getBytes(oldStorageFileRef);
258
+ console.log(
259
+ `[MediaService] Successfully downloaded ${fileBytes.byteLength} bytes from ${oldStoragePath}`
260
+ );
261
+
262
+ // 2. Upload the bytes to the new path
263
+ console.log(`[MediaService] Uploading bytes to ${newStoragePath}`);
264
+ await uploadBytes(newStorageFileRef, fileBytes, {
265
+ contentType: metadata.contentType,
266
+ });
267
+ console.log(
268
+ `[MediaService] Successfully uploaded bytes to ${newStoragePath}`
269
+ );
270
+
271
+ // 3. Get the new download URL
272
+ const newDownloadURL = await getDownloadURL(newStorageFileRef);
273
+ console.log(
274
+ `[MediaService] Got new download URL for ${newStoragePath}: ${newDownloadURL}`
275
+ );
276
+
277
+ // 4. Prepare metadata for update
278
+ const updateData = {
279
+ accessLevel: newAccessLevel,
280
+ path: newStoragePath,
281
+ url: newDownloadURL,
282
+ updatedAt: Timestamp.now(),
283
+ };
284
+
285
+ // 5. Update Firestore metadata
286
+ const metadataDocRef = doc(this.db, MEDIA_METADATA_COLLECTION, mediaId);
287
+ console.log(
288
+ `[MediaService] Updating Firestore metadata for ${mediaId} with new data:`,
289
+ updateData
290
+ );
291
+ await updateDoc(metadataDocRef, updateData);
292
+ console.log(
293
+ `[MediaService] Successfully updated Firestore metadata for ${mediaId}`
294
+ );
295
+
296
+ // 6. Delete the old file from Firebase Storage (after metadata is updated)
297
+ try {
298
+ console.log(`[MediaService] Deleting old file from ${oldStoragePath}`);
299
+ await deleteObject(oldStorageFileRef);
300
+ console.log(
301
+ `[MediaService] Successfully deleted old file from ${oldStoragePath}`
302
+ );
303
+ } catch (deleteError) {
304
+ console.error(
305
+ `[MediaService] Failed to delete old file from ${oldStoragePath} for media ID ${mediaId}. This file is now orphaned. Error:`,
306
+ deleteError
307
+ );
308
+ // Do not re-throw here, as the primary operation (move and metadata update) was successful.
309
+ // Log this issue for potential manual cleanup.
310
+ }
311
+
312
+ return { ...metadata, ...updateData } as MediaMetadata;
313
+ } catch (error) {
314
+ console.error(
315
+ `[MediaService] Error updating media access level and moving file for ${mediaId}:`,
316
+ error
317
+ );
318
+ // Attempt to clean up if new file was created but process failed before metadata update
319
+ // This is a best-effort cleanup. If newDownloadURL is not defined, it means upload failed.
320
+ // If newDownloadURL is defined but metadata update failed, new file might exist.
321
+ if (
322
+ newStorageFileRef &&
323
+ (error as any).code !== "storage/object-not-found" &&
324
+ (error as any).message?.includes("uploadBytes")
325
+ ) {
326
+ // This check is a bit heuristic. Ideally, check if new file actually exists.
327
+ console.warn(
328
+ `[MediaService] Attempting to delete partially uploaded file at ${newStoragePath} due to error.`
329
+ );
330
+ try {
331
+ await deleteObject(newStorageFileRef);
332
+ console.warn(
333
+ `[MediaService] Cleaned up partially uploaded file at ${newStoragePath}.`
334
+ );
335
+ } catch (cleanupError) {
336
+ console.error(
337
+ `[MediaService] Failed to cleanup partially uploaded file at ${newStoragePath}:`,
338
+ cleanupError
339
+ );
340
+ }
341
+ }
342
+ throw error; // Re-throw the original error to the caller
343
+ }
344
+ }
345
+
346
+ /**
347
+ * List all media for an owner, optionally filtered by collection and access level.
348
+ * @param ownerId - ID of the owner.
349
+ * @param collectionName - Optional: Filter by collection name.
350
+ * @param accessLevel - Optional: Filter by access level.
351
+ * @param count - Optional: Number of items to fetch.
352
+ * @param startAfterId - Optional: ID of the document to start after (for pagination).
353
+ */
354
+ async listMedia(
355
+ ownerId: string,
356
+ collectionName?: string,
357
+ accessLevel?: MediaAccessLevel,
358
+ count?: number,
359
+ startAfterId?: string // Using ID for pagination simplicity with Firestore
360
+ ): Promise<MediaMetadata[]> {
361
+ console.log(`[MediaService] Listing media for owner: ${ownerId}`);
362
+ let qConstraints: any[] = [where("ownerId", "==", ownerId)];
363
+
364
+ if (collectionName) {
365
+ qConstraints.push(where("collectionName", "==", collectionName));
366
+ }
367
+ if (accessLevel) {
368
+ qConstraints.push(where("accessLevel", "==", accessLevel));
369
+ }
370
+
371
+ qConstraints.push(orderBy("createdAt", "desc")); // Example ordering
372
+
373
+ if (count) {
374
+ qConstraints.push(limit(count));
375
+ }
376
+
377
+ if (startAfterId) {
378
+ const startAfterDoc = await this.getMediaMetadata(startAfterId);
379
+ if (startAfterDoc) {
380
+ // Placeholder: Firestore's startAfter needs a DocumentSnapshot or field values.
381
+ // For robust pagination, pass the actual DocumentSnapshot or use field values from it.
382
+ // e.g., qConstraints.push(startAfter(snapshotOfStartAfterDoc));
383
+ }
384
+ }
385
+
386
+ const finalQuery = query(
387
+ collection(this.db, MEDIA_METADATA_COLLECTION),
388
+ ...qConstraints
389
+ );
390
+
391
+ try {
392
+ const querySnapshot = await getDocs(finalQuery);
393
+ const mediaList = querySnapshot.docs.map(
394
+ (doc) => doc.data() as MediaMetadata
395
+ );
396
+ console.log(`[MediaService] Found ${mediaList.length} media items.`);
397
+ return mediaList;
398
+ } catch (error) {
399
+ console.error("[MediaService] Error listing media:", error);
400
+ throw error;
401
+ }
402
+ }
403
+
404
+ /**
405
+ * Get download URL for media. (Convenience, as URL is in metadata)
406
+ * @param mediaId - ID of the media.
407
+ */
408
+ async getMediaDownloadUrl(mediaId: string): Promise<string | null> {
409
+ console.log(`[MediaService] Getting download URL for media ID: ${mediaId}`);
410
+ const metadata = await this.getMediaMetadata(mediaId);
411
+ if (metadata && metadata.url) {
412
+ console.log(`[MediaService] URL found: ${metadata.url}`);
413
+ return metadata.url;
414
+ }
415
+ console.log(`[MediaService] URL not found for media ID: ${mediaId}`);
416
+ return null;
417
+ }
418
+ }