@blackcode_sa/metaestetics-api 1.12.61 → 1.12.62

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 (269) hide show
  1. package/dist/admin/index.d.mts +2 -0
  2. package/dist/admin/index.d.ts +2 -0
  3. package/dist/admin/index.js +45 -4
  4. package/dist/admin/index.mjs +45 -4
  5. package/dist/index.d.mts +2 -0
  6. package/dist/index.d.ts +2 -0
  7. package/dist/index.js +53 -11
  8. package/dist/index.mjs +53 -11
  9. package/package.json +119 -119
  10. package/src/__mocks__/firstore.ts +10 -10
  11. package/src/admin/aggregation/README.md +79 -79
  12. package/src/admin/aggregation/appointment/README.md +128 -128
  13. package/src/admin/aggregation/appointment/appointment.aggregation.service.ts +1844 -1844
  14. package/src/admin/aggregation/appointment/index.ts +1 -1
  15. package/src/admin/aggregation/clinic/README.md +52 -52
  16. package/src/admin/aggregation/clinic/clinic.aggregation.service.ts +703 -703
  17. package/src/admin/aggregation/clinic/index.ts +1 -1
  18. package/src/admin/aggregation/forms/README.md +13 -13
  19. package/src/admin/aggregation/forms/filled-forms.aggregation.service.ts +322 -322
  20. package/src/admin/aggregation/forms/index.ts +1 -1
  21. package/src/admin/aggregation/index.ts +8 -8
  22. package/src/admin/aggregation/patient/README.md +27 -27
  23. package/src/admin/aggregation/patient/index.ts +1 -1
  24. package/src/admin/aggregation/patient/patient.aggregation.service.ts +141 -141
  25. package/src/admin/aggregation/practitioner/README.md +42 -42
  26. package/src/admin/aggregation/practitioner/index.ts +1 -1
  27. package/src/admin/aggregation/practitioner/practitioner.aggregation.service.ts +433 -433
  28. package/src/admin/aggregation/practitioner-invite/index.ts +1 -1
  29. package/src/admin/aggregation/practitioner-invite/practitioner-invite.aggregation.service.ts +961 -961
  30. package/src/admin/aggregation/procedure/README.md +43 -43
  31. package/src/admin/aggregation/procedure/index.ts +1 -1
  32. package/src/admin/aggregation/procedure/procedure.aggregation.service.ts +702 -702
  33. package/src/admin/aggregation/reviews/index.ts +1 -1
  34. package/src/admin/aggregation/reviews/reviews.aggregation.service.ts +689 -641
  35. package/src/admin/booking/README.md +125 -125
  36. package/src/admin/booking/booking.admin.ts +1037 -1037
  37. package/src/admin/booking/booking.calculator.ts +712 -712
  38. package/src/admin/booking/booking.types.ts +59 -59
  39. package/src/admin/booking/index.ts +3 -3
  40. package/src/admin/booking/timezones-problem.md +185 -185
  41. package/src/admin/calendar/README.md +7 -7
  42. package/src/admin/calendar/calendar.admin.service.ts +345 -345
  43. package/src/admin/calendar/index.ts +1 -1
  44. package/src/admin/documentation-templates/document-manager.admin.ts +260 -260
  45. package/src/admin/documentation-templates/index.ts +1 -1
  46. package/src/admin/free-consultation/free-consultation-utils.admin.ts +148 -148
  47. package/src/admin/free-consultation/index.ts +1 -1
  48. package/src/admin/index.ts +75 -75
  49. package/src/admin/logger/index.ts +78 -78
  50. package/src/admin/mailing/README.md +95 -95
  51. package/src/admin/mailing/appointment/appointment.mailing.service.ts +732 -732
  52. package/src/admin/mailing/appointment/index.ts +1 -1
  53. package/src/admin/mailing/appointment/templates/patient/appointment-confirmed.html +40 -40
  54. package/src/admin/mailing/base.mailing.service.ts +208 -208
  55. package/src/admin/mailing/index.ts +3 -3
  56. package/src/admin/mailing/practitionerInvite/existing-practitioner-invite.mailing.ts +611 -611
  57. package/src/admin/mailing/practitionerInvite/index.ts +2 -2
  58. package/src/admin/mailing/practitionerInvite/practitionerInvite.mailing.ts +395 -395
  59. package/src/admin/mailing/practitionerInvite/templates/existing-practitioner-invitation.template.ts +155 -155
  60. package/src/admin/mailing/practitionerInvite/templates/invitation.template.ts +101 -101
  61. package/src/admin/mailing/practitionerInvite/templates/invite-accepted-notification.template.ts +228 -228
  62. package/src/admin/mailing/practitionerInvite/templates/invite-rejected-notification.template.ts +242 -242
  63. package/src/admin/notifications/index.ts +1 -1
  64. package/src/admin/notifications/notifications.admin.ts +710 -710
  65. package/src/admin/requirements/README.md +128 -128
  66. package/src/admin/requirements/index.ts +1 -1
  67. package/src/admin/requirements/patient-requirements.admin.service.ts +475 -475
  68. package/src/admin/users/index.ts +1 -1
  69. package/src/admin/users/user-profile.admin.ts +405 -405
  70. package/src/backoffice/constants/certification.constants.ts +13 -13
  71. package/src/backoffice/constants/index.ts +1 -1
  72. package/src/backoffice/errors/backoffice.errors.ts +181 -181
  73. package/src/backoffice/errors/index.ts +1 -1
  74. package/src/backoffice/expo-safe/README.md +26 -26
  75. package/src/backoffice/expo-safe/index.ts +41 -41
  76. package/src/backoffice/index.ts +5 -5
  77. package/src/backoffice/services/FIXES_README.md +102 -102
  78. package/src/backoffice/services/README.md +40 -40
  79. package/src/backoffice/services/brand.service.ts +256 -256
  80. package/src/backoffice/services/category.service.ts +318 -318
  81. package/src/backoffice/services/constants.service.ts +385 -385
  82. package/src/backoffice/services/documentation-template.service.ts +202 -202
  83. package/src/backoffice/services/index.ts +8 -8
  84. package/src/backoffice/services/migrate-products.ts +116 -116
  85. package/src/backoffice/services/product.service.ts +553 -553
  86. package/src/backoffice/services/requirement.service.ts +235 -235
  87. package/src/backoffice/services/subcategory.service.ts +395 -395
  88. package/src/backoffice/services/technology.service.ts +1070 -1070
  89. package/src/backoffice/types/README.md +12 -12
  90. package/src/backoffice/types/admin-constants.types.ts +69 -69
  91. package/src/backoffice/types/brand.types.ts +29 -29
  92. package/src/backoffice/types/category.types.ts +62 -62
  93. package/src/backoffice/types/documentation-templates.types.ts +28 -28
  94. package/src/backoffice/types/index.ts +10 -10
  95. package/src/backoffice/types/procedure-product.types.ts +38 -38
  96. package/src/backoffice/types/product.types.ts +240 -240
  97. package/src/backoffice/types/requirement.types.ts +63 -63
  98. package/src/backoffice/types/static/README.md +18 -18
  99. package/src/backoffice/types/static/blocking-condition.types.ts +21 -21
  100. package/src/backoffice/types/static/certification.types.ts +37 -37
  101. package/src/backoffice/types/static/contraindication.types.ts +19 -19
  102. package/src/backoffice/types/static/index.ts +6 -6
  103. package/src/backoffice/types/static/pricing.types.ts +16 -16
  104. package/src/backoffice/types/static/procedure-family.types.ts +14 -14
  105. package/src/backoffice/types/static/treatment-benefit.types.ts +22 -22
  106. package/src/backoffice/types/subcategory.types.ts +34 -34
  107. package/src/backoffice/types/technology.types.ts +161 -161
  108. package/src/backoffice/validations/index.ts +1 -1
  109. package/src/backoffice/validations/schemas.ts +163 -163
  110. package/src/config/__mocks__/firebase.ts +99 -99
  111. package/src/config/firebase.ts +78 -78
  112. package/src/config/index.ts +9 -9
  113. package/src/errors/auth.error.ts +6 -6
  114. package/src/errors/auth.errors.ts +200 -200
  115. package/src/errors/clinic.errors.ts +32 -32
  116. package/src/errors/firebase.errors.ts +47 -47
  117. package/src/errors/user.errors.ts +99 -99
  118. package/src/index.backup.ts +407 -407
  119. package/src/index.ts +6 -6
  120. package/src/locales/en.ts +31 -31
  121. package/src/recommender/admin/index.ts +1 -1
  122. package/src/recommender/admin/services/recommender.service.admin.ts +5 -5
  123. package/src/recommender/front/index.ts +1 -1
  124. package/src/recommender/front/services/onboarding.service.ts +5 -5
  125. package/src/recommender/front/services/recommender.service.ts +3 -3
  126. package/src/recommender/index.ts +1 -1
  127. package/src/services/PATIENTAUTH.MD +197 -197
  128. package/src/services/README.md +106 -106
  129. package/src/services/__tests__/auth/auth.mock.test.ts +17 -17
  130. package/src/services/__tests__/auth/auth.setup.ts +293 -293
  131. package/src/services/__tests__/auth.service.test.ts +346 -346
  132. package/src/services/__tests__/base.service.test.ts +77 -77
  133. package/src/services/__tests__/user.service.test.ts +528 -528
  134. package/src/services/appointment/README.md +17 -17
  135. package/src/services/appointment/appointment.service.ts +2082 -2082
  136. package/src/services/appointment/index.ts +1 -1
  137. package/src/services/appointment/utils/appointment.utils.ts +552 -552
  138. package/src/services/appointment/utils/extended-procedure.utils.ts +314 -314
  139. package/src/services/appointment/utils/form-initialization.utils.ts +225 -225
  140. package/src/services/appointment/utils/recommended-procedure.utils.ts +195 -195
  141. package/src/services/appointment/utils/zone-management.utils.ts +353 -353
  142. package/src/services/appointment/utils/zone-photo.utils.ts +152 -152
  143. package/src/services/auth/auth.service.ts +989 -989
  144. package/src/services/auth/auth.v2.service.ts +961 -961
  145. package/src/services/auth/index.ts +7 -7
  146. package/src/services/auth/utils/error.utils.ts +90 -90
  147. package/src/services/auth/utils/firebase.utils.ts +49 -49
  148. package/src/services/auth/utils/index.ts +21 -21
  149. package/src/services/auth/utils/practitioner.utils.ts +125 -125
  150. package/src/services/base.service.ts +41 -41
  151. package/src/services/calendar/calendar.service.ts +1077 -1077
  152. package/src/services/calendar/calendar.v2.service.ts +1683 -1683
  153. package/src/services/calendar/calendar.v3.service.ts +313 -313
  154. package/src/services/calendar/externalCalendar.service.ts +178 -178
  155. package/src/services/calendar/index.ts +5 -5
  156. package/src/services/calendar/synced-calendars.service.ts +743 -743
  157. package/src/services/calendar/utils/appointment.utils.ts +265 -265
  158. package/src/services/calendar/utils/calendar-event.utils.ts +646 -646
  159. package/src/services/calendar/utils/clinic.utils.ts +237 -237
  160. package/src/services/calendar/utils/docs.utils.ts +157 -157
  161. package/src/services/calendar/utils/google-calendar.utils.ts +697 -697
  162. package/src/services/calendar/utils/index.ts +8 -8
  163. package/src/services/calendar/utils/patient.utils.ts +198 -198
  164. package/src/services/calendar/utils/practitioner.utils.ts +221 -221
  165. package/src/services/calendar/utils/synced-calendar.utils.ts +472 -472
  166. package/src/services/clinic/README.md +204 -204
  167. package/src/services/clinic/__tests__/clinic-admin.service.test.ts +287 -287
  168. package/src/services/clinic/__tests__/clinic-group.service.test.ts +352 -352
  169. package/src/services/clinic/__tests__/clinic.service.test.ts +354 -354
  170. package/src/services/clinic/billing-transactions.service.ts +217 -217
  171. package/src/services/clinic/clinic-admin.service.ts +202 -202
  172. package/src/services/clinic/clinic-group.service.ts +310 -310
  173. package/src/services/clinic/clinic.service.ts +708 -708
  174. package/src/services/clinic/index.ts +5 -5
  175. package/src/services/clinic/practitioner-invite.service.ts +519 -519
  176. package/src/services/clinic/utils/admin.utils.ts +551 -551
  177. package/src/services/clinic/utils/clinic-group.utils.ts +646 -646
  178. package/src/services/clinic/utils/clinic.utils.ts +949 -949
  179. package/src/services/clinic/utils/filter.utils.d.ts +23 -23
  180. package/src/services/clinic/utils/filter.utils.ts +446 -446
  181. package/src/services/clinic/utils/index.ts +11 -11
  182. package/src/services/clinic/utils/photos.utils.ts +188 -188
  183. package/src/services/clinic/utils/search.utils.ts +84 -84
  184. package/src/services/clinic/utils/tag.utils.ts +124 -124
  185. package/src/services/documentation-templates/documentation-template.service.ts +537 -537
  186. package/src/services/documentation-templates/filled-document.service.ts +587 -587
  187. package/src/services/documentation-templates/index.ts +2 -2
  188. package/src/services/index.ts +13 -13
  189. package/src/services/media/index.ts +1 -1
  190. package/src/services/media/media.service.ts +418 -418
  191. package/src/services/notifications/__tests__/notification.service.test.ts +242 -242
  192. package/src/services/notifications/index.ts +1 -1
  193. package/src/services/notifications/notification.service.ts +215 -215
  194. package/src/services/patient/README.md +48 -48
  195. package/src/services/patient/To-Do.md +43 -43
  196. package/src/services/patient/__tests__/patient.service.test.ts +294 -294
  197. package/src/services/patient/index.ts +2 -2
  198. package/src/services/patient/patient.service.ts +883 -883
  199. package/src/services/patient/patientRequirements.service.ts +285 -285
  200. package/src/services/patient/utils/aesthetic-analysis.utils.ts +176 -176
  201. package/src/services/patient/utils/clinic.utils.ts +80 -80
  202. package/src/services/patient/utils/docs.utils.ts +142 -142
  203. package/src/services/patient/utils/index.ts +9 -9
  204. package/src/services/patient/utils/location.utils.ts +126 -126
  205. package/src/services/patient/utils/medical-stuff.utils.ts +143 -143
  206. package/src/services/patient/utils/medical.utils.ts +458 -458
  207. package/src/services/patient/utils/practitioner.utils.ts +260 -260
  208. package/src/services/patient/utils/profile.utils.ts +510 -510
  209. package/src/services/patient/utils/sensitive.utils.ts +260 -260
  210. package/src/services/patient/utils/token.utils.ts +211 -211
  211. package/src/services/practitioner/README.md +145 -145
  212. package/src/services/practitioner/index.ts +1 -1
  213. package/src/services/practitioner/practitioner.service.ts +1742 -1742
  214. package/src/services/procedure/README.md +163 -163
  215. package/src/services/procedure/index.ts +1 -1
  216. package/src/services/procedure/procedure.service.ts +1682 -1682
  217. package/src/services/reviews/index.ts +1 -1
  218. package/src/services/reviews/reviews.service.ts +683 -636
  219. package/src/services/user/index.ts +1 -1
  220. package/src/services/user/user.service.ts +489 -489
  221. package/src/services/user/user.v2.service.ts +466 -466
  222. package/src/types/appointment/index.ts +453 -453
  223. package/src/types/calendar/index.ts +258 -258
  224. package/src/types/calendar/synced-calendar.types.ts +66 -66
  225. package/src/types/clinic/index.ts +489 -489
  226. package/src/types/clinic/practitioner-invite.types.ts +91 -91
  227. package/src/types/clinic/preferences.types.ts +159 -159
  228. package/src/types/clinic/to-do +3 -3
  229. package/src/types/documentation-templates/index.ts +308 -308
  230. package/src/types/index.ts +44 -44
  231. package/src/types/notifications/README.md +77 -77
  232. package/src/types/notifications/index.ts +265 -265
  233. package/src/types/patient/aesthetic-analysis.types.ts +66 -66
  234. package/src/types/patient/allergies.ts +58 -58
  235. package/src/types/patient/index.ts +273 -273
  236. package/src/types/patient/medical-info.types.ts +152 -152
  237. package/src/types/patient/patient-requirements.ts +92 -92
  238. package/src/types/patient/token.types.ts +61 -61
  239. package/src/types/practitioner/index.ts +206 -206
  240. package/src/types/procedure/index.ts +181 -181
  241. package/src/types/profile/index.ts +39 -39
  242. package/src/types/reviews/index.ts +132 -130
  243. package/src/types/tz-lookup.d.ts +4 -4
  244. package/src/types/user/index.ts +38 -38
  245. package/src/utils/TIMESTAMPS.md +176 -176
  246. package/src/utils/TimestampUtils.ts +241 -241
  247. package/src/utils/index.ts +1 -1
  248. package/src/validations/appointment.schema.ts +574 -574
  249. package/src/validations/calendar.schema.ts +225 -225
  250. package/src/validations/clinic.schema.ts +493 -493
  251. package/src/validations/common.schema.ts +25 -25
  252. package/src/validations/documentation-templates/index.ts +1 -1
  253. package/src/validations/documentation-templates/template.schema.ts +220 -220
  254. package/src/validations/documentation-templates.schema.ts +10 -10
  255. package/src/validations/index.ts +20 -20
  256. package/src/validations/media.schema.ts +10 -10
  257. package/src/validations/notification.schema.ts +90 -90
  258. package/src/validations/patient/aesthetic-analysis.schema.ts +55 -55
  259. package/src/validations/patient/medical-info.schema.ts +125 -125
  260. package/src/validations/patient/patient-requirements.schema.ts +84 -84
  261. package/src/validations/patient/token.schema.ts +29 -29
  262. package/src/validations/patient.schema.ts +216 -216
  263. package/src/validations/practitioner.schema.ts +222 -222
  264. package/src/validations/procedure-product.schema.ts +41 -41
  265. package/src/validations/procedure.schema.ts +124 -124
  266. package/src/validations/profile-info.schema.ts +41 -41
  267. package/src/validations/reviews.schema.ts +195 -189
  268. package/src/validations/schemas.ts +104 -104
  269. 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
+ }