@blackcode_sa/metaestetics-api 1.7.4 → 1.7.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.
- package/dist/index.d.mts +16 -16
- package/dist/index.d.ts +16 -16
- package/dist/index.js +1019 -1012
- package/dist/index.mjs +291 -284
- package/package.json +1 -1
- package/src/services/media/media.service.ts +330 -0
- package/src/types/index.ts +7 -7
- package/src/validations/schemas.ts +17 -6
package/package.json
CHANGED
|
@@ -0,0 +1,330 @@
|
|
|
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
|
+
listAll,
|
|
10
|
+
FirebaseStorage,
|
|
11
|
+
} from "firebase/storage";
|
|
12
|
+
import {
|
|
13
|
+
doc,
|
|
14
|
+
getDoc,
|
|
15
|
+
setDoc,
|
|
16
|
+
updateDoc,
|
|
17
|
+
arrayUnion,
|
|
18
|
+
arrayRemove,
|
|
19
|
+
collection,
|
|
20
|
+
query,
|
|
21
|
+
where,
|
|
22
|
+
limit,
|
|
23
|
+
getDocs,
|
|
24
|
+
deleteDoc,
|
|
25
|
+
orderBy,
|
|
26
|
+
} from "firebase/firestore";
|
|
27
|
+
import { BaseService } from "../base.service";
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Enum for media access levels
|
|
31
|
+
*/
|
|
32
|
+
export enum MediaAccessLevel {
|
|
33
|
+
PUBLIC = "public",
|
|
34
|
+
PRIVATE = "private",
|
|
35
|
+
CONFIDENTIAL = "confidential",
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Media file metadata interface
|
|
40
|
+
*/
|
|
41
|
+
export interface MediaMetadata {
|
|
42
|
+
id: string;
|
|
43
|
+
name: string;
|
|
44
|
+
url: string;
|
|
45
|
+
contentType: string;
|
|
46
|
+
size: number;
|
|
47
|
+
createdAt: Timestamp;
|
|
48
|
+
accessLevel: MediaAccessLevel;
|
|
49
|
+
ownerId: string;
|
|
50
|
+
collectionName: string;
|
|
51
|
+
path: string;
|
|
52
|
+
updatedAt?: Timestamp;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const MEDIA_METADATA_COLLECTION = "media_metadata";
|
|
56
|
+
|
|
57
|
+
export class MediaService extends BaseService {
|
|
58
|
+
constructor(db: Firestore, auth: Auth, app: FirebaseApp) {
|
|
59
|
+
super(db, auth, app);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Upload a media file, store its metadata, and return the metadata including the URL.
|
|
64
|
+
* @param file - The file to upload.
|
|
65
|
+
* @param ownerId - ID of the owner (user, patient, clinic, etc.).
|
|
66
|
+
* @param accessLevel - Access level (public, private, confidential).
|
|
67
|
+
* @param collectionName - The logical collection name this media belongs to (e.g., 'patient_profile_pictures', 'clinic_logos').
|
|
68
|
+
* @param originalFileName - Optional: the original name of the file, if not using file.name.
|
|
69
|
+
* @returns Promise with the media metadata.
|
|
70
|
+
*/
|
|
71
|
+
async uploadMedia(
|
|
72
|
+
file: File,
|
|
73
|
+
ownerId: string,
|
|
74
|
+
accessLevel: MediaAccessLevel,
|
|
75
|
+
collectionName: string,
|
|
76
|
+
originalFileName?: string
|
|
77
|
+
): Promise<MediaMetadata> {
|
|
78
|
+
const mediaId = this.generateId();
|
|
79
|
+
const fileNameToUse = originalFileName || file.name;
|
|
80
|
+
// Sanitize fileNameToUse if necessary, e.g., remove special characters or ensure a safe extension
|
|
81
|
+
const uniqueFileName = `${mediaId}-${fileNameToUse}`;
|
|
82
|
+
const filePath = `media/${accessLevel}/${ownerId}/${uniqueFileName}`;
|
|
83
|
+
|
|
84
|
+
console.log(`[MediaService] Uploading file to: ${filePath}`);
|
|
85
|
+
|
|
86
|
+
const storageRef = ref(this.storage, filePath);
|
|
87
|
+
|
|
88
|
+
try {
|
|
89
|
+
const uploadResult = await uploadBytes(storageRef, file, {
|
|
90
|
+
contentType: file.type,
|
|
91
|
+
});
|
|
92
|
+
console.log("[MediaService] File uploaded successfully", uploadResult);
|
|
93
|
+
|
|
94
|
+
const downloadURL = await getDownloadURL(uploadResult.ref);
|
|
95
|
+
console.log("[MediaService] Got download URL:", downloadURL);
|
|
96
|
+
|
|
97
|
+
const metadata: MediaMetadata = {
|
|
98
|
+
id: mediaId,
|
|
99
|
+
name: fileNameToUse,
|
|
100
|
+
url: downloadURL,
|
|
101
|
+
contentType: file.type,
|
|
102
|
+
size: file.size,
|
|
103
|
+
createdAt: Timestamp.now(),
|
|
104
|
+
accessLevel: accessLevel,
|
|
105
|
+
ownerId: ownerId,
|
|
106
|
+
collectionName: collectionName,
|
|
107
|
+
path: filePath, // Store the full storage path
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
const metadataDocRef = doc(this.db, MEDIA_METADATA_COLLECTION, mediaId);
|
|
111
|
+
await setDoc(metadataDocRef, metadata);
|
|
112
|
+
console.log("[MediaService] Metadata stored in Firestore:", mediaId);
|
|
113
|
+
|
|
114
|
+
return metadata;
|
|
115
|
+
} catch (error) {
|
|
116
|
+
console.error("[MediaService] Error during media upload:", error);
|
|
117
|
+
// Consider more specific error handling or re-throwing a custom error
|
|
118
|
+
throw error;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Get media metadata from Firestore by its ID.
|
|
124
|
+
* @param mediaId - ID of the media.
|
|
125
|
+
* @returns Promise with the media metadata or null if not found.
|
|
126
|
+
*/
|
|
127
|
+
async getMediaMetadata(mediaId: string): Promise<MediaMetadata | null> {
|
|
128
|
+
console.log(`[MediaService] Getting media metadata for ID: ${mediaId}`);
|
|
129
|
+
const docRef = doc(this.db, MEDIA_METADATA_COLLECTION, mediaId);
|
|
130
|
+
const docSnap = await getDoc(docRef);
|
|
131
|
+
|
|
132
|
+
if (docSnap.exists()) {
|
|
133
|
+
console.log("[MediaService] Metadata found:", docSnap.data());
|
|
134
|
+
return docSnap.data() as MediaMetadata;
|
|
135
|
+
}
|
|
136
|
+
console.log("[MediaService] No metadata found for ID:", mediaId);
|
|
137
|
+
return null;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Get media metadata from Firestore by its public URL.
|
|
142
|
+
* @param url - The public URL of the media file.
|
|
143
|
+
* @returns Promise with the media metadata or null if not found.
|
|
144
|
+
*/
|
|
145
|
+
async getMediaMetadataByUrl(url: string): Promise<MediaMetadata | null> {
|
|
146
|
+
console.log(`[MediaService] Getting media metadata by URL: ${url}`);
|
|
147
|
+
const q = query(
|
|
148
|
+
collection(this.db, MEDIA_METADATA_COLLECTION),
|
|
149
|
+
where("url", "==", url),
|
|
150
|
+
limit(1)
|
|
151
|
+
);
|
|
152
|
+
|
|
153
|
+
try {
|
|
154
|
+
const querySnapshot = await getDocs(q);
|
|
155
|
+
if (!querySnapshot.empty) {
|
|
156
|
+
const metadata = querySnapshot.docs[0].data() as MediaMetadata;
|
|
157
|
+
console.log("[MediaService] Metadata found by URL:", metadata);
|
|
158
|
+
return metadata;
|
|
159
|
+
}
|
|
160
|
+
console.log("[MediaService] No metadata found for URL:", url);
|
|
161
|
+
return null;
|
|
162
|
+
} catch (error) {
|
|
163
|
+
console.error("[MediaService] Error fetching metadata by URL:", error);
|
|
164
|
+
throw error;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Delete media from storage and remove metadata from Firestore.
|
|
170
|
+
* @param mediaId - ID of the media to delete.
|
|
171
|
+
*/
|
|
172
|
+
async deleteMedia(mediaId: string): Promise<void> {
|
|
173
|
+
console.log(`[MediaService] Deleting media with ID: ${mediaId}`);
|
|
174
|
+
const metadata = await this.getMediaMetadata(mediaId);
|
|
175
|
+
|
|
176
|
+
if (!metadata) {
|
|
177
|
+
console.warn(
|
|
178
|
+
`[MediaService] Metadata not found for media ID ${mediaId}. Cannot delete.`
|
|
179
|
+
);
|
|
180
|
+
// Optionally throw an error or return a status
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const storageFileRef = ref(this.storage, metadata.path);
|
|
185
|
+
|
|
186
|
+
try {
|
|
187
|
+
// Delete the file from Firebase Storage
|
|
188
|
+
await deleteObject(storageFileRef);
|
|
189
|
+
console.log(`[MediaService] File deleted from Storage: ${metadata.path}`);
|
|
190
|
+
|
|
191
|
+
// Delete the metadata from Firestore
|
|
192
|
+
const metadataDocRef = doc(this.db, MEDIA_METADATA_COLLECTION, mediaId);
|
|
193
|
+
await deleteDoc(metadataDocRef);
|
|
194
|
+
console.log(
|
|
195
|
+
`[MediaService] Metadata deleted from Firestore for ID: ${mediaId}`
|
|
196
|
+
);
|
|
197
|
+
} catch (error) {
|
|
198
|
+
console.error(`[MediaService] Error deleting media ${mediaId}:`, error);
|
|
199
|
+
// Handle specific errors, e.g., file not found in storage, permissions issues
|
|
200
|
+
// If Firestore delete fails after storage delete, there might be an orphaned metadata entry.
|
|
201
|
+
// Consider how to handle such inconsistencies or if it's acceptable.
|
|
202
|
+
throw error;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Update media access level. This is a complex operation if it involves moving the file.
|
|
208
|
+
* For now, this will only update the metadata. True access control might need storage path changes and Firestore rule updates.
|
|
209
|
+
* @param mediaId - ID of the media to update.
|
|
210
|
+
* @param newAccessLevel - New access level.
|
|
211
|
+
* @returns Promise with the updated media metadata.
|
|
212
|
+
*/
|
|
213
|
+
async updateMediaAccessLevel(
|
|
214
|
+
mediaId: string,
|
|
215
|
+
newAccessLevel: MediaAccessLevel
|
|
216
|
+
): Promise<MediaMetadata | null> {
|
|
217
|
+
console.log(
|
|
218
|
+
`[MediaService] Updating access level for media ID: ${mediaId} to ${newAccessLevel}`
|
|
219
|
+
);
|
|
220
|
+
const metadata = await this.getMediaMetadata(mediaId);
|
|
221
|
+
if (!metadata) {
|
|
222
|
+
console.warn(
|
|
223
|
+
`[MediaService] Metadata not found for media ID ${mediaId}. Cannot update access level.`
|
|
224
|
+
);
|
|
225
|
+
return null;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// IMPORTANT: This only updates the metadata field.
|
|
229
|
+
// If your Firebase Storage rules depend on the path, this won't change file accessibility directly.
|
|
230
|
+
// A full implementation might involve moving the file in storage to a new path (e.g., media/newAccessLevel/...),
|
|
231
|
+
// updating the URL, and then updating the metadata. This is significantly more complex.
|
|
232
|
+
|
|
233
|
+
const metadataDocRef = doc(this.db, MEDIA_METADATA_COLLECTION, mediaId);
|
|
234
|
+
const updateData = {
|
|
235
|
+
accessLevel: newAccessLevel,
|
|
236
|
+
updatedAt: Timestamp.now(),
|
|
237
|
+
};
|
|
238
|
+
|
|
239
|
+
try {
|
|
240
|
+
await updateDoc(metadataDocRef, updateData);
|
|
241
|
+
console.log(`[MediaService] Media access level updated for ${mediaId}.`);
|
|
242
|
+
// Return the updated metadata
|
|
243
|
+
return { ...metadata, ...updateData } as MediaMetadata;
|
|
244
|
+
} catch (error) {
|
|
245
|
+
console.error(
|
|
246
|
+
`[MediaService] Error updating media access level for ${mediaId}:`,
|
|
247
|
+
error
|
|
248
|
+
);
|
|
249
|
+
throw error;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* List all media for an owner, optionally filtered by collection and access level.
|
|
255
|
+
* @param ownerId - ID of the owner.
|
|
256
|
+
* @param collectionName - Optional: Filter by collection name.
|
|
257
|
+
* @param accessLevel - Optional: Filter by access level.
|
|
258
|
+
* @param count - Optional: Number of items to fetch.
|
|
259
|
+
* @param startAfterId - Optional: ID of the document to start after (for pagination).
|
|
260
|
+
*/
|
|
261
|
+
async listMedia(
|
|
262
|
+
ownerId: string,
|
|
263
|
+
collectionName?: string,
|
|
264
|
+
accessLevel?: MediaAccessLevel,
|
|
265
|
+
count?: number,
|
|
266
|
+
startAfterId?: string // Using ID for pagination simplicity with Firestore
|
|
267
|
+
): Promise<MediaMetadata[]> {
|
|
268
|
+
console.log(`[MediaService] Listing media for owner: ${ownerId}`);
|
|
269
|
+
let qConstraints: any[] = [where("ownerId", "==", ownerId)];
|
|
270
|
+
|
|
271
|
+
if (collectionName) {
|
|
272
|
+
qConstraints.push(where("collectionName", "==", collectionName));
|
|
273
|
+
}
|
|
274
|
+
if (accessLevel) {
|
|
275
|
+
qConstraints.push(where("accessLevel", "==", accessLevel));
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
qConstraints.push(orderBy("createdAt", "desc")); // Example ordering
|
|
279
|
+
|
|
280
|
+
if (count) {
|
|
281
|
+
qConstraints.push(limit(count));
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
if (startAfterId) {
|
|
285
|
+
const startAfterDoc = await this.getMediaMetadata(startAfterId);
|
|
286
|
+
if (startAfterDoc) {
|
|
287
|
+
// Firestore's startAfter needs a DocumentSnapshot or field values from it.
|
|
288
|
+
// For simplicity, if using cursor-based pagination with getDocs, you'd pass the snapshot.
|
|
289
|
+
// If building query manually, it's more complex with orderBy fields.
|
|
290
|
+
// This part needs refinement based on actual pagination strategy.
|
|
291
|
+
// For now, let's assume we'd fetch the doc snapshot if needed or simplify.
|
|
292
|
+
// console.log("[MediaService] Pagination: starting after document corresponding to ID", startAfterId);
|
|
293
|
+
// This simple approach of passing ID won't directly work with Firestore's startAfter without fetching the document snapshot first.
|
|
294
|
+
// A more robust solution would involve passing the actual DocumentSnapshot or specific field values of the last retrieved document.
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
const finalQuery = query(
|
|
299
|
+
collection(this.db, MEDIA_METADATA_COLLECTION),
|
|
300
|
+
...qConstraints
|
|
301
|
+
);
|
|
302
|
+
|
|
303
|
+
try {
|
|
304
|
+
const querySnapshot = await getDocs(finalQuery);
|
|
305
|
+
const mediaList = querySnapshot.docs.map(
|
|
306
|
+
(doc) => doc.data() as MediaMetadata
|
|
307
|
+
);
|
|
308
|
+
console.log(`[MediaService] Found ${mediaList.length} media items.`);
|
|
309
|
+
return mediaList;
|
|
310
|
+
} catch (error) {
|
|
311
|
+
console.error("[MediaService] Error listing media:", error);
|
|
312
|
+
throw error;
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Get download URL for media. (Convenience, as URL is in metadata)
|
|
318
|
+
* @param mediaId - ID of the media.
|
|
319
|
+
*/
|
|
320
|
+
async getMediaDownloadUrl(mediaId: string): Promise<string | null> {
|
|
321
|
+
console.log(`[MediaService] Getting download URL for media ID: ${mediaId}`);
|
|
322
|
+
const metadata = await this.getMediaMetadata(mediaId);
|
|
323
|
+
if (metadata && metadata.url) {
|
|
324
|
+
console.log(`[MediaService] URL found: ${metadata.url}`);
|
|
325
|
+
return metadata.url;
|
|
326
|
+
}
|
|
327
|
+
console.log(`[MediaService] URL not found for media ID: ${mediaId}`);
|
|
328
|
+
return null;
|
|
329
|
+
}
|
|
330
|
+
}
|
package/src/types/index.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { User as FirebaseAuthUser } from "firebase/auth";
|
|
2
|
-
import { Timestamp, FieldValue } from "firebase/firestore";
|
|
2
|
+
import { Timestamp, FieldValue, serverTimestamp } from "firebase/firestore";
|
|
3
3
|
|
|
4
4
|
// User tipovi
|
|
5
5
|
export enum UserRole {
|
|
@@ -14,9 +14,9 @@ export interface User {
|
|
|
14
14
|
email: string | null;
|
|
15
15
|
roles: UserRole[];
|
|
16
16
|
isAnonymous: boolean;
|
|
17
|
-
createdAt: Timestamp | FieldValue;
|
|
18
|
-
updatedAt: Timestamp | FieldValue;
|
|
19
|
-
lastLoginAt: Timestamp | FieldValue;
|
|
17
|
+
createdAt: Timestamp | FieldValue | (() => FieldValue);
|
|
18
|
+
updatedAt: Timestamp | FieldValue | (() => FieldValue);
|
|
19
|
+
lastLoginAt: Timestamp | FieldValue | (() => FieldValue);
|
|
20
20
|
patientProfile?: string;
|
|
21
21
|
practitionerProfile?: string;
|
|
22
22
|
adminProfile?: string;
|
|
@@ -27,9 +27,9 @@ export interface CreateUserData {
|
|
|
27
27
|
email: string | null;
|
|
28
28
|
roles: UserRole[];
|
|
29
29
|
isAnonymous: boolean;
|
|
30
|
-
createdAt: FieldValue;
|
|
31
|
-
updatedAt: FieldValue;
|
|
32
|
-
lastLoginAt: FieldValue;
|
|
30
|
+
createdAt: FieldValue | Timestamp | (() => FieldValue);
|
|
31
|
+
updatedAt: FieldValue | Timestamp | (() => FieldValue);
|
|
32
|
+
lastLoginAt: FieldValue | Timestamp | (() => FieldValue);
|
|
33
33
|
}
|
|
34
34
|
|
|
35
35
|
export const USERS_COLLECTION = "users";
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { UserRole } from "../types";
|
|
3
|
-
import { Timestamp, FieldValue } from "firebase/firestore";
|
|
3
|
+
import { Timestamp, FieldValue, serverTimestamp } from "firebase/firestore";
|
|
4
4
|
|
|
5
5
|
export const emailSchema = z
|
|
6
6
|
.string()
|
|
@@ -24,21 +24,32 @@ export const userRolesSchema = z
|
|
|
24
24
|
.min(1, "User must have at least one role")
|
|
25
25
|
.max(3, "User cannot have more than 3 roles");
|
|
26
26
|
|
|
27
|
-
export const timestampSchema = z.custom<
|
|
28
|
-
|
|
27
|
+
export const timestampSchema = z.custom<
|
|
28
|
+
Timestamp | FieldValue | (() => FieldValue)
|
|
29
|
+
>((data) => {
|
|
30
|
+
// If it's serverTimestamp (FieldValue), it's valid
|
|
29
31
|
if (data && typeof data === "object" && "isEqual" in data) {
|
|
30
32
|
return true;
|
|
31
33
|
}
|
|
32
34
|
|
|
33
35
|
// If it's a Timestamp object, validate its structure
|
|
34
|
-
|
|
36
|
+
if (
|
|
35
37
|
data &&
|
|
36
38
|
typeof data === "object" &&
|
|
37
39
|
"toDate" in data &&
|
|
38
40
|
"seconds" in data &&
|
|
39
41
|
"nanoseconds" in data
|
|
40
|
-
)
|
|
41
|
-
|
|
42
|
+
) {
|
|
43
|
+
return true;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// If it's the serverTimestamp function reference
|
|
47
|
+
if (data === serverTimestamp) {
|
|
48
|
+
return true;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return false;
|
|
52
|
+
}, "Must be a Timestamp object, FieldValue or serverTimestamp function");
|
|
42
53
|
|
|
43
54
|
/**
|
|
44
55
|
* Validaciona šema za clinic admin opcije pri kreiranju
|