@blackcode_sa/metaestetics-api 1.13.5 → 1.13.6
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.
- package/dist/admin/index.d.mts +20 -1
- package/dist/admin/index.d.ts +20 -1
- package/dist/admin/index.js +217 -1
- package/dist/admin/index.mjs +217 -1
- package/package.json +121 -121
- package/src/__mocks__/firstore.ts +10 -10
- package/src/admin/aggregation/README.md +79 -79
- package/src/admin/aggregation/appointment/README.md +128 -128
- package/src/admin/aggregation/appointment/appointment.aggregation.service.ts +1984 -1984
- package/src/admin/aggregation/appointment/index.ts +1 -1
- package/src/admin/aggregation/clinic/README.md +52 -52
- package/src/admin/aggregation/clinic/clinic.aggregation.service.ts +966 -703
- package/src/admin/aggregation/clinic/index.ts +1 -1
- package/src/admin/aggregation/forms/README.md +13 -13
- package/src/admin/aggregation/forms/filled-forms.aggregation.service.ts +322 -322
- package/src/admin/aggregation/forms/index.ts +1 -1
- package/src/admin/aggregation/index.ts +8 -8
- package/src/admin/aggregation/patient/README.md +27 -27
- package/src/admin/aggregation/patient/index.ts +1 -1
- package/src/admin/aggregation/patient/patient.aggregation.service.ts +141 -141
- package/src/admin/aggregation/practitioner/README.md +42 -42
- package/src/admin/aggregation/practitioner/index.ts +1 -1
- package/src/admin/aggregation/practitioner/practitioner.aggregation.service.ts +433 -433
- package/src/admin/aggregation/practitioner-invite/index.ts +1 -1
- package/src/admin/aggregation/practitioner-invite/practitioner-invite.aggregation.service.ts +961 -961
- package/src/admin/aggregation/procedure/README.md +43 -43
- package/src/admin/aggregation/procedure/index.ts +1 -1
- package/src/admin/aggregation/procedure/procedure.aggregation.service.ts +702 -702
- package/src/admin/aggregation/reviews/index.ts +1 -1
- package/src/admin/aggregation/reviews/reviews.aggregation.service.ts +689 -689
- package/src/admin/analytics/analytics.admin.service.ts +278 -278
- package/src/admin/analytics/index.ts +2 -2
- package/src/admin/booking/README.md +125 -125
- package/src/admin/booking/booking.admin.ts +1037 -1037
- package/src/admin/booking/booking.calculator.ts +712 -712
- package/src/admin/booking/booking.types.ts +59 -59
- package/src/admin/booking/index.ts +3 -3
- package/src/admin/booking/timezones-problem.md +185 -185
- package/src/admin/calendar/README.md +7 -7
- package/src/admin/calendar/calendar.admin.service.ts +345 -345
- package/src/admin/calendar/index.ts +1 -1
- package/src/admin/documentation-templates/document-manager.admin.ts +260 -260
- package/src/admin/documentation-templates/index.ts +1 -1
- package/src/admin/free-consultation/free-consultation-utils.admin.ts +148 -148
- package/src/admin/free-consultation/index.ts +1 -1
- package/src/admin/index.ts +81 -81
- package/src/admin/logger/index.ts +78 -78
- package/src/admin/mailing/README.md +95 -95
- package/src/admin/mailing/appointment/appointment.mailing.service.ts +732 -732
- package/src/admin/mailing/appointment/index.ts +1 -1
- package/src/admin/mailing/appointment/templates/patient/appointment-confirmed.html +40 -40
- package/src/admin/mailing/base.mailing.service.ts +208 -208
- package/src/admin/mailing/index.ts +3 -3
- package/src/admin/mailing/practitionerInvite/existing-practitioner-invite.mailing.ts +611 -611
- package/src/admin/mailing/practitionerInvite/index.ts +2 -2
- package/src/admin/mailing/practitionerInvite/practitionerInvite.mailing.ts +395 -395
- package/src/admin/mailing/practitionerInvite/templates/existing-practitioner-invitation.template.ts +155 -155
- package/src/admin/mailing/practitionerInvite/templates/invitation.template.ts +101 -101
- package/src/admin/mailing/practitionerInvite/templates/invite-accepted-notification.template.ts +228 -228
- package/src/admin/mailing/practitionerInvite/templates/invite-rejected-notification.template.ts +242 -242
- package/src/admin/notifications/index.ts +1 -1
- package/src/admin/notifications/notifications.admin.ts +710 -710
- package/src/admin/requirements/README.md +128 -128
- package/src/admin/requirements/index.ts +1 -1
- package/src/admin/requirements/patient-requirements.admin.service.ts +475 -475
- package/src/admin/users/index.ts +1 -1
- package/src/admin/users/user-profile.admin.ts +405 -405
- package/src/backoffice/constants/certification.constants.ts +13 -13
- package/src/backoffice/constants/index.ts +1 -1
- package/src/backoffice/errors/backoffice.errors.ts +181 -181
- package/src/backoffice/errors/index.ts +1 -1
- package/src/backoffice/expo-safe/README.md +26 -26
- package/src/backoffice/expo-safe/index.ts +41 -41
- package/src/backoffice/index.ts +5 -5
- package/src/backoffice/services/FIXES_README.md +102 -102
- package/src/backoffice/services/README.md +57 -57
- package/src/backoffice/services/analytics.service.proposal.md +863 -863
- package/src/backoffice/services/analytics.service.summary.md +143 -143
- package/src/backoffice/services/brand.service.ts +256 -256
- package/src/backoffice/services/category.service.ts +384 -384
- package/src/backoffice/services/constants.service.ts +385 -385
- package/src/backoffice/services/documentation-template.service.ts +202 -202
- package/src/backoffice/services/index.ts +10 -10
- package/src/backoffice/services/migrate-products.ts +116 -116
- package/src/backoffice/services/product.service.ts +553 -553
- package/src/backoffice/services/requirement.service.ts +235 -235
- package/src/backoffice/services/subcategory.service.ts +461 -461
- package/src/backoffice/services/technology.service.ts +1151 -1151
- package/src/backoffice/types/README.md +12 -12
- package/src/backoffice/types/admin-constants.types.ts +69 -69
- package/src/backoffice/types/brand.types.ts +29 -29
- package/src/backoffice/types/category.types.ts +67 -67
- package/src/backoffice/types/documentation-templates.types.ts +28 -28
- package/src/backoffice/types/index.ts +10 -10
- package/src/backoffice/types/procedure-product.types.ts +38 -38
- package/src/backoffice/types/product.types.ts +240 -240
- package/src/backoffice/types/requirement.types.ts +63 -63
- package/src/backoffice/types/static/README.md +18 -18
- package/src/backoffice/types/static/blocking-condition.types.ts +21 -21
- package/src/backoffice/types/static/certification.types.ts +37 -37
- package/src/backoffice/types/static/contraindication.types.ts +19 -19
- package/src/backoffice/types/static/index.ts +6 -6
- package/src/backoffice/types/static/pricing.types.ts +16 -16
- package/src/backoffice/types/static/procedure-family.types.ts +14 -14
- package/src/backoffice/types/static/treatment-benefit.types.ts +22 -22
- package/src/backoffice/types/subcategory.types.ts +34 -34
- package/src/backoffice/types/technology.types.ts +168 -168
- package/src/backoffice/validations/index.ts +1 -1
- package/src/backoffice/validations/schemas.ts +164 -164
- package/src/config/__mocks__/firebase.ts +99 -99
- package/src/config/firebase.ts +78 -78
- package/src/config/index.ts +9 -9
- package/src/errors/auth.error.ts +6 -6
- package/src/errors/auth.errors.ts +200 -200
- package/src/errors/clinic.errors.ts +32 -32
- package/src/errors/firebase.errors.ts +47 -47
- package/src/errors/user.errors.ts +99 -99
- package/src/index.backup.ts +407 -407
- package/src/index.ts +6 -6
- package/src/locales/en.ts +31 -31
- package/src/recommender/admin/index.ts +1 -1
- package/src/recommender/admin/services/recommender.service.admin.ts +5 -5
- package/src/recommender/front/index.ts +1 -1
- package/src/recommender/front/services/onboarding.service.ts +5 -5
- package/src/recommender/front/services/recommender.service.ts +3 -3
- package/src/recommender/index.ts +1 -1
- package/src/services/PATIENTAUTH.MD +197 -197
- package/src/services/README.md +106 -106
- package/src/services/__tests__/auth/auth.mock.test.ts +17 -17
- package/src/services/__tests__/auth/auth.setup.ts +293 -293
- package/src/services/__tests__/auth.service.test.ts +346 -346
- package/src/services/__tests__/base.service.test.ts +77 -77
- package/src/services/__tests__/user.service.test.ts +528 -528
- package/src/services/analytics/ARCHITECTURE.md +199 -199
- package/src/services/analytics/CLOUD_FUNCTIONS.md +225 -225
- package/src/services/analytics/GROUPED_ANALYTICS.md +501 -501
- package/src/services/analytics/QUICK_START.md +393 -393
- package/src/services/analytics/README.md +304 -304
- package/src/services/analytics/SUMMARY.md +141 -141
- package/src/services/analytics/TRENDS.md +380 -380
- package/src/services/analytics/USAGE_GUIDE.md +518 -518
- package/src/services/analytics/analytics-cloud.service.ts +222 -222
- package/src/services/analytics/analytics.service.ts +2142 -2142
- package/src/services/analytics/index.ts +4 -4
- package/src/services/analytics/review-analytics.service.ts +941 -941
- package/src/services/analytics/utils/appointment-filtering.utils.ts +138 -138
- package/src/services/analytics/utils/cost-calculation.utils.ts +182 -182
- package/src/services/analytics/utils/grouping.utils.ts +434 -434
- package/src/services/analytics/utils/stored-analytics.utils.ts +347 -347
- package/src/services/analytics/utils/time-calculation.utils.ts +186 -186
- package/src/services/analytics/utils/trend-calculation.utils.ts +200 -200
- package/src/services/appointment/README.md +17 -17
- package/src/services/appointment/appointment.service.ts +2558 -2558
- package/src/services/appointment/index.ts +1 -1
- package/src/services/appointment/utils/appointment.utils.ts +552 -552
- package/src/services/appointment/utils/extended-procedure.utils.ts +314 -314
- package/src/services/appointment/utils/form-initialization.utils.ts +225 -225
- package/src/services/appointment/utils/recommended-procedure.utils.ts +195 -195
- package/src/services/appointment/utils/zone-management.utils.ts +353 -353
- package/src/services/appointment/utils/zone-photo.utils.ts +152 -152
- package/src/services/auth/auth.service.ts +989 -989
- package/src/services/auth/auth.v2.service.ts +961 -961
- package/src/services/auth/index.ts +7 -7
- package/src/services/auth/utils/error.utils.ts +90 -90
- package/src/services/auth/utils/firebase.utils.ts +49 -49
- package/src/services/auth/utils/index.ts +21 -21
- package/src/services/auth/utils/practitioner.utils.ts +125 -125
- package/src/services/base.service.ts +41 -41
- package/src/services/calendar/calendar.service.ts +1077 -1077
- package/src/services/calendar/calendar.v2.service.ts +1683 -1683
- package/src/services/calendar/calendar.v3.service.ts +313 -313
- package/src/services/calendar/externalCalendar.service.ts +178 -178
- package/src/services/calendar/index.ts +5 -5
- package/src/services/calendar/synced-calendars.service.ts +743 -743
- package/src/services/calendar/utils/appointment.utils.ts +265 -265
- package/src/services/calendar/utils/calendar-event.utils.ts +646 -646
- package/src/services/calendar/utils/clinic.utils.ts +237 -237
- package/src/services/calendar/utils/docs.utils.ts +157 -157
- package/src/services/calendar/utils/google-calendar.utils.ts +697 -697
- package/src/services/calendar/utils/index.ts +8 -8
- package/src/services/calendar/utils/patient.utils.ts +198 -198
- package/src/services/calendar/utils/practitioner.utils.ts +221 -221
- package/src/services/calendar/utils/synced-calendar.utils.ts +472 -472
- package/src/services/clinic/README.md +204 -204
- package/src/services/clinic/__tests__/clinic-admin.service.test.ts +287 -287
- package/src/services/clinic/__tests__/clinic-group.service.test.ts +352 -352
- package/src/services/clinic/__tests__/clinic.service.test.ts +354 -354
- package/src/services/clinic/billing-transactions.service.ts +217 -217
- package/src/services/clinic/clinic-admin.service.ts +202 -202
- package/src/services/clinic/clinic-group.service.ts +310 -310
- package/src/services/clinic/clinic.service.ts +708 -708
- package/src/services/clinic/index.ts +5 -5
- package/src/services/clinic/practitioner-invite.service.ts +519 -519
- package/src/services/clinic/utils/admin.utils.ts +551 -551
- package/src/services/clinic/utils/clinic-group.utils.ts +646 -646
- package/src/services/clinic/utils/clinic.utils.ts +949 -949
- package/src/services/clinic/utils/filter.utils.d.ts +23 -23
- package/src/services/clinic/utils/filter.utils.ts +446 -446
- package/src/services/clinic/utils/index.ts +11 -11
- package/src/services/clinic/utils/photos.utils.ts +188 -188
- package/src/services/clinic/utils/search.utils.ts +84 -84
- package/src/services/clinic/utils/tag.utils.ts +124 -124
- package/src/services/documentation-templates/documentation-template.service.ts +537 -537
- package/src/services/documentation-templates/filled-document.service.ts +587 -587
- package/src/services/documentation-templates/index.ts +2 -2
- package/src/services/index.ts +14 -14
- package/src/services/media/index.ts +1 -1
- package/src/services/media/media.service.ts +418 -418
- package/src/services/notifications/__tests__/notification.service.test.ts +242 -242
- package/src/services/notifications/index.ts +1 -1
- package/src/services/notifications/notification.service.ts +215 -215
- package/src/services/patient/README.md +48 -48
- package/src/services/patient/To-Do.md +43 -43
- package/src/services/patient/__tests__/patient.service.test.ts +294 -294
- package/src/services/patient/index.ts +2 -2
- package/src/services/patient/patient.service.ts +883 -883
- package/src/services/patient/patientRequirements.service.ts +285 -285
- package/src/services/patient/utils/aesthetic-analysis.utils.ts +176 -176
- package/src/services/patient/utils/clinic.utils.ts +80 -80
- package/src/services/patient/utils/docs.utils.ts +142 -142
- package/src/services/patient/utils/index.ts +9 -9
- package/src/services/patient/utils/location.utils.ts +126 -126
- package/src/services/patient/utils/medical-stuff.utils.ts +143 -143
- package/src/services/patient/utils/medical.utils.ts +458 -458
- package/src/services/patient/utils/practitioner.utils.ts +260 -260
- package/src/services/patient/utils/profile.utils.ts +510 -510
- package/src/services/patient/utils/sensitive.utils.ts +260 -260
- package/src/services/patient/utils/token.utils.ts +211 -211
- package/src/services/practitioner/README.md +145 -145
- package/src/services/practitioner/index.ts +1 -1
- package/src/services/practitioner/practitioner.service.ts +1742 -1742
- package/src/services/procedure/README.md +163 -163
- package/src/services/procedure/index.ts +1 -1
- package/src/services/procedure/procedure.service.ts +2200 -2200
- package/src/services/reviews/index.ts +1 -1
- package/src/services/reviews/reviews.service.ts +734 -734
- package/src/services/user/index.ts +1 -1
- package/src/services/user/user.service.ts +489 -489
- package/src/services/user/user.v2.service.ts +466 -466
- package/src/types/analytics/analytics.types.ts +597 -597
- package/src/types/analytics/grouped-analytics.types.ts +173 -173
- package/src/types/analytics/index.ts +4 -4
- package/src/types/analytics/stored-analytics.types.ts +137 -137
- package/src/types/appointment/index.ts +480 -480
- package/src/types/calendar/index.ts +258 -258
- package/src/types/calendar/synced-calendar.types.ts +66 -66
- package/src/types/clinic/index.ts +498 -498
- package/src/types/clinic/practitioner-invite.types.ts +91 -91
- package/src/types/clinic/preferences.types.ts +159 -159
- package/src/types/clinic/to-do +3 -3
- package/src/types/documentation-templates/index.ts +308 -308
- package/src/types/index.ts +47 -47
- package/src/types/notifications/README.md +77 -77
- package/src/types/notifications/index.ts +286 -286
- package/src/types/patient/aesthetic-analysis.types.ts +66 -66
- package/src/types/patient/allergies.ts +58 -58
- package/src/types/patient/index.ts +275 -275
- package/src/types/patient/medical-info.types.ts +152 -152
- package/src/types/patient/patient-requirements.ts +92 -92
- package/src/types/patient/token.types.ts +61 -61
- package/src/types/practitioner/index.ts +206 -206
- package/src/types/procedure/index.ts +181 -181
- package/src/types/profile/index.ts +39 -39
- package/src/types/reviews/index.ts +132 -132
- package/src/types/tz-lookup.d.ts +4 -4
- package/src/types/user/index.ts +38 -38
- package/src/utils/TIMESTAMPS.md +176 -176
- package/src/utils/TimestampUtils.ts +241 -241
- package/src/utils/index.ts +1 -1
- package/src/validations/appointment.schema.ts +574 -574
- package/src/validations/calendar.schema.ts +225 -225
- package/src/validations/clinic.schema.ts +494 -494
- package/src/validations/common.schema.ts +25 -25
- package/src/validations/documentation-templates/index.ts +1 -1
- package/src/validations/documentation-templates/template.schema.ts +220 -220
- package/src/validations/documentation-templates.schema.ts +10 -10
- package/src/validations/index.ts +20 -20
- package/src/validations/media.schema.ts +10 -10
- package/src/validations/notification.schema.ts +90 -90
- package/src/validations/patient/aesthetic-analysis.schema.ts +55 -55
- package/src/validations/patient/medical-info.schema.ts +125 -125
- package/src/validations/patient/patient-requirements.schema.ts +84 -84
- package/src/validations/patient/token.schema.ts +29 -29
- package/src/validations/patient.schema.ts +217 -217
- package/src/validations/practitioner.schema.ts +222 -222
- package/src/validations/procedure-product.schema.ts +41 -41
- package/src/validations/procedure.schema.ts +124 -124
- package/src/validations/profile-info.schema.ts +41 -41
- package/src/validations/reviews.schema.ts +195 -195
- package/src/validations/schemas.ts +104 -104
- 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
|
+
}
|