@blackcode_sa/metaestetics-api 1.13.3 → 1.13.5

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 (293) hide show
  1. package/dist/admin/index.d.mts +15 -28
  2. package/dist/admin/index.d.ts +15 -28
  3. package/dist/index.d.mts +18 -30
  4. package/dist/index.d.ts +18 -30
  5. package/dist/index.js +11 -3
  6. package/dist/index.mjs +11 -3
  7. package/package.json +121 -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 +1984 -1984
  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 +689 -689
  33. package/src/admin/analytics/analytics.admin.service.ts +278 -278
  34. package/src/admin/analytics/index.ts +2 -2
  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 +81 -81
  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 +57 -57
  79. package/src/backoffice/services/analytics.service.proposal.md +863 -863
  80. package/src/backoffice/services/analytics.service.summary.md +143 -143
  81. package/src/backoffice/services/brand.service.ts +256 -256
  82. package/src/backoffice/services/category.service.ts +384 -384
  83. package/src/backoffice/services/constants.service.ts +385 -385
  84. package/src/backoffice/services/documentation-template.service.ts +202 -202
  85. package/src/backoffice/services/index.ts +10 -10
  86. package/src/backoffice/services/migrate-products.ts +116 -116
  87. package/src/backoffice/services/product.service.ts +553 -553
  88. package/src/backoffice/services/requirement.service.ts +235 -235
  89. package/src/backoffice/services/subcategory.service.ts +461 -461
  90. package/src/backoffice/services/technology.service.ts +1151 -1151
  91. package/src/backoffice/types/README.md +12 -12
  92. package/src/backoffice/types/admin-constants.types.ts +69 -69
  93. package/src/backoffice/types/brand.types.ts +29 -29
  94. package/src/backoffice/types/category.types.ts +67 -67
  95. package/src/backoffice/types/documentation-templates.types.ts +28 -28
  96. package/src/backoffice/types/index.ts +10 -10
  97. package/src/backoffice/types/procedure-product.types.ts +38 -38
  98. package/src/backoffice/types/product.types.ts +240 -240
  99. package/src/backoffice/types/requirement.types.ts +63 -63
  100. package/src/backoffice/types/static/README.md +18 -18
  101. package/src/backoffice/types/static/blocking-condition.types.ts +21 -21
  102. package/src/backoffice/types/static/certification.types.ts +37 -37
  103. package/src/backoffice/types/static/contraindication.types.ts +19 -19
  104. package/src/backoffice/types/static/index.ts +6 -6
  105. package/src/backoffice/types/static/pricing.types.ts +16 -16
  106. package/src/backoffice/types/static/procedure-family.types.ts +14 -14
  107. package/src/backoffice/types/static/treatment-benefit.types.ts +22 -22
  108. package/src/backoffice/types/subcategory.types.ts +34 -34
  109. package/src/backoffice/types/technology.types.ts +168 -168
  110. package/src/backoffice/validations/index.ts +1 -1
  111. package/src/backoffice/validations/schemas.ts +164 -164
  112. package/src/config/__mocks__/firebase.ts +99 -99
  113. package/src/config/firebase.ts +78 -78
  114. package/src/config/index.ts +9 -9
  115. package/src/errors/auth.error.ts +6 -6
  116. package/src/errors/auth.errors.ts +200 -200
  117. package/src/errors/clinic.errors.ts +32 -32
  118. package/src/errors/firebase.errors.ts +47 -47
  119. package/src/errors/user.errors.ts +99 -99
  120. package/src/index.backup.ts +407 -407
  121. package/src/index.ts +6 -6
  122. package/src/locales/en.ts +31 -31
  123. package/src/recommender/admin/index.ts +1 -1
  124. package/src/recommender/admin/services/recommender.service.admin.ts +5 -5
  125. package/src/recommender/front/index.ts +1 -1
  126. package/src/recommender/front/services/onboarding.service.ts +5 -5
  127. package/src/recommender/front/services/recommender.service.ts +3 -3
  128. package/src/recommender/index.ts +1 -1
  129. package/src/services/PATIENTAUTH.MD +197 -197
  130. package/src/services/README.md +106 -106
  131. package/src/services/__tests__/auth/auth.mock.test.ts +17 -17
  132. package/src/services/__tests__/auth/auth.setup.ts +293 -293
  133. package/src/services/__tests__/auth.service.test.ts +346 -346
  134. package/src/services/__tests__/base.service.test.ts +77 -77
  135. package/src/services/__tests__/user.service.test.ts +528 -528
  136. package/src/services/analytics/ARCHITECTURE.md +199 -199
  137. package/src/services/analytics/CLOUD_FUNCTIONS.md +225 -225
  138. package/src/services/analytics/GROUPED_ANALYTICS.md +501 -501
  139. package/src/services/analytics/QUICK_START.md +393 -393
  140. package/src/services/analytics/README.md +304 -304
  141. package/src/services/analytics/SUMMARY.md +141 -141
  142. package/src/services/analytics/TRENDS.md +380 -380
  143. package/src/services/analytics/USAGE_GUIDE.md +518 -518
  144. package/src/services/analytics/analytics-cloud.service.ts +222 -222
  145. package/src/services/analytics/analytics.service.ts +2142 -2142
  146. package/src/services/analytics/index.ts +4 -4
  147. package/src/services/analytics/review-analytics.service.ts +941 -941
  148. package/src/services/analytics/utils/appointment-filtering.utils.ts +138 -138
  149. package/src/services/analytics/utils/cost-calculation.utils.ts +182 -182
  150. package/src/services/analytics/utils/grouping.utils.ts +434 -434
  151. package/src/services/analytics/utils/stored-analytics.utils.ts +347 -347
  152. package/src/services/analytics/utils/time-calculation.utils.ts +186 -186
  153. package/src/services/analytics/utils/trend-calculation.utils.ts +200 -200
  154. package/src/services/appointment/README.md +17 -17
  155. package/src/services/appointment/appointment.service.ts +2558 -2558
  156. package/src/services/appointment/index.ts +1 -1
  157. package/src/services/appointment/utils/appointment.utils.ts +552 -552
  158. package/src/services/appointment/utils/extended-procedure.utils.ts +314 -314
  159. package/src/services/appointment/utils/form-initialization.utils.ts +225 -225
  160. package/src/services/appointment/utils/recommended-procedure.utils.ts +195 -195
  161. package/src/services/appointment/utils/zone-management.utils.ts +353 -353
  162. package/src/services/appointment/utils/zone-photo.utils.ts +152 -152
  163. package/src/services/auth/auth.service.ts +989 -989
  164. package/src/services/auth/auth.v2.service.ts +961 -961
  165. package/src/services/auth/index.ts +7 -7
  166. package/src/services/auth/utils/error.utils.ts +90 -90
  167. package/src/services/auth/utils/firebase.utils.ts +49 -49
  168. package/src/services/auth/utils/index.ts +21 -21
  169. package/src/services/auth/utils/practitioner.utils.ts +125 -125
  170. package/src/services/base.service.ts +41 -41
  171. package/src/services/calendar/calendar.service.ts +1077 -1077
  172. package/src/services/calendar/calendar.v2.service.ts +1683 -1683
  173. package/src/services/calendar/calendar.v3.service.ts +313 -313
  174. package/src/services/calendar/externalCalendar.service.ts +178 -178
  175. package/src/services/calendar/index.ts +5 -5
  176. package/src/services/calendar/synced-calendars.service.ts +743 -743
  177. package/src/services/calendar/utils/appointment.utils.ts +265 -265
  178. package/src/services/calendar/utils/calendar-event.utils.ts +646 -646
  179. package/src/services/calendar/utils/clinic.utils.ts +237 -237
  180. package/src/services/calendar/utils/docs.utils.ts +157 -157
  181. package/src/services/calendar/utils/google-calendar.utils.ts +697 -697
  182. package/src/services/calendar/utils/index.ts +8 -8
  183. package/src/services/calendar/utils/patient.utils.ts +198 -198
  184. package/src/services/calendar/utils/practitioner.utils.ts +221 -221
  185. package/src/services/calendar/utils/synced-calendar.utils.ts +472 -472
  186. package/src/services/clinic/README.md +204 -204
  187. package/src/services/clinic/__tests__/clinic-admin.service.test.ts +287 -287
  188. package/src/services/clinic/__tests__/clinic-group.service.test.ts +352 -352
  189. package/src/services/clinic/__tests__/clinic.service.test.ts +354 -354
  190. package/src/services/clinic/billing-transactions.service.ts +217 -217
  191. package/src/services/clinic/clinic-admin.service.ts +202 -202
  192. package/src/services/clinic/clinic-group.service.ts +310 -310
  193. package/src/services/clinic/clinic.service.ts +708 -708
  194. package/src/services/clinic/index.ts +5 -5
  195. package/src/services/clinic/practitioner-invite.service.ts +519 -519
  196. package/src/services/clinic/utils/admin.utils.ts +551 -551
  197. package/src/services/clinic/utils/clinic-group.utils.ts +646 -646
  198. package/src/services/clinic/utils/clinic.utils.ts +949 -949
  199. package/src/services/clinic/utils/filter.utils.d.ts +23 -23
  200. package/src/services/clinic/utils/filter.utils.ts +446 -446
  201. package/src/services/clinic/utils/index.ts +11 -11
  202. package/src/services/clinic/utils/photos.utils.ts +188 -188
  203. package/src/services/clinic/utils/search.utils.ts +84 -84
  204. package/src/services/clinic/utils/tag.utils.ts +124 -124
  205. package/src/services/documentation-templates/documentation-template.service.ts +537 -537
  206. package/src/services/documentation-templates/filled-document.service.ts +587 -587
  207. package/src/services/documentation-templates/index.ts +2 -2
  208. package/src/services/index.ts +14 -14
  209. package/src/services/media/index.ts +1 -1
  210. package/src/services/media/media.service.ts +418 -418
  211. package/src/services/notifications/__tests__/notification.service.test.ts +242 -242
  212. package/src/services/notifications/index.ts +1 -1
  213. package/src/services/notifications/notification.service.ts +215 -215
  214. package/src/services/patient/README.md +48 -48
  215. package/src/services/patient/To-Do.md +43 -43
  216. package/src/services/patient/__tests__/patient.service.test.ts +294 -294
  217. package/src/services/patient/index.ts +2 -2
  218. package/src/services/patient/patient.service.ts +883 -883
  219. package/src/services/patient/patientRequirements.service.ts +285 -285
  220. package/src/services/patient/utils/aesthetic-analysis.utils.ts +176 -176
  221. package/src/services/patient/utils/clinic.utils.ts +80 -80
  222. package/src/services/patient/utils/docs.utils.ts +142 -142
  223. package/src/services/patient/utils/index.ts +9 -9
  224. package/src/services/patient/utils/location.utils.ts +126 -126
  225. package/src/services/patient/utils/medical-stuff.utils.ts +143 -143
  226. package/src/services/patient/utils/medical.utils.ts +458 -458
  227. package/src/services/patient/utils/practitioner.utils.ts +260 -260
  228. package/src/services/patient/utils/profile.utils.ts +510 -510
  229. package/src/services/patient/utils/sensitive.utils.ts +260 -260
  230. package/src/services/patient/utils/token.utils.ts +211 -211
  231. package/src/services/practitioner/README.md +145 -145
  232. package/src/services/practitioner/index.ts +1 -1
  233. package/src/services/practitioner/practitioner.service.ts +1742 -1742
  234. package/src/services/procedure/README.md +163 -163
  235. package/src/services/procedure/index.ts +1 -1
  236. package/src/services/procedure/procedure.service.ts +2200 -2191
  237. package/src/services/reviews/index.ts +1 -1
  238. package/src/services/reviews/reviews.service.ts +734 -734
  239. package/src/services/user/index.ts +1 -1
  240. package/src/services/user/user.service.ts +489 -489
  241. package/src/services/user/user.v2.service.ts +466 -466
  242. package/src/types/analytics/analytics.types.ts +597 -597
  243. package/src/types/analytics/grouped-analytics.types.ts +173 -173
  244. package/src/types/analytics/index.ts +4 -4
  245. package/src/types/analytics/stored-analytics.types.ts +137 -137
  246. package/src/types/appointment/index.ts +480 -480
  247. package/src/types/calendar/index.ts +258 -258
  248. package/src/types/calendar/synced-calendar.types.ts +66 -66
  249. package/src/types/clinic/index.ts +498 -489
  250. package/src/types/clinic/practitioner-invite.types.ts +91 -91
  251. package/src/types/clinic/preferences.types.ts +159 -159
  252. package/src/types/clinic/to-do +3 -3
  253. package/src/types/documentation-templates/index.ts +308 -308
  254. package/src/types/index.ts +47 -47
  255. package/src/types/notifications/README.md +77 -77
  256. package/src/types/notifications/index.ts +286 -286
  257. package/src/types/patient/aesthetic-analysis.types.ts +66 -66
  258. package/src/types/patient/allergies.ts +58 -58
  259. package/src/types/patient/index.ts +275 -275
  260. package/src/types/patient/medical-info.types.ts +152 -152
  261. package/src/types/patient/patient-requirements.ts +92 -92
  262. package/src/types/patient/token.types.ts +61 -61
  263. package/src/types/practitioner/index.ts +206 -206
  264. package/src/types/procedure/index.ts +181 -181
  265. package/src/types/profile/index.ts +39 -39
  266. package/src/types/reviews/index.ts +132 -132
  267. package/src/types/tz-lookup.d.ts +4 -4
  268. package/src/types/user/index.ts +38 -38
  269. package/src/utils/TIMESTAMPS.md +176 -176
  270. package/src/utils/TimestampUtils.ts +241 -241
  271. package/src/utils/index.ts +1 -1
  272. package/src/validations/appointment.schema.ts +574 -574
  273. package/src/validations/calendar.schema.ts +225 -225
  274. package/src/validations/clinic.schema.ts +494 -493
  275. package/src/validations/common.schema.ts +25 -25
  276. package/src/validations/documentation-templates/index.ts +1 -1
  277. package/src/validations/documentation-templates/template.schema.ts +220 -220
  278. package/src/validations/documentation-templates.schema.ts +10 -10
  279. package/src/validations/index.ts +20 -20
  280. package/src/validations/media.schema.ts +10 -10
  281. package/src/validations/notification.schema.ts +90 -90
  282. package/src/validations/patient/aesthetic-analysis.schema.ts +55 -55
  283. package/src/validations/patient/medical-info.schema.ts +125 -125
  284. package/src/validations/patient/patient-requirements.schema.ts +84 -84
  285. package/src/validations/patient/token.schema.ts +29 -29
  286. package/src/validations/patient.schema.ts +217 -217
  287. package/src/validations/practitioner.schema.ts +222 -222
  288. package/src/validations/procedure-product.schema.ts +41 -41
  289. package/src/validations/procedure.schema.ts +124 -124
  290. package/src/validations/profile-info.schema.ts +41 -41
  291. package/src/validations/reviews.schema.ts +195 -195
  292. package/src/validations/schemas.ts +104 -104
  293. 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
+ }