@blackcode_sa/metaestetics-api 1.15.17-staging.4 → 1.15.17-staging.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 +3 -1
- package/dist/admin/index.d.ts +3 -1
- package/dist/admin/index.js +1 -1
- package/dist/admin/index.mjs +1 -1
- package/dist/index.d.mts +15 -1
- package/dist/index.d.ts +15 -1
- package/dist/index.js +102 -117
- package/dist/index.mjs +242 -257
- package/package.json +1 -1
- package/src/admin/aggregation/clinic/clinic.aggregation.service.ts +1 -1
- package/src/services/auth/auth.service.ts +16 -49
- package/src/services/clinic/clinic-group.service.ts +13 -6
- package/src/services/clinic/utils/clinic-group.utils.ts +97 -88
- package/src/types/clinic/index.ts +3 -1
- package/src/validations/clinic.schema.ts +4 -3
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@blackcode_sa/metaestetics-api",
|
|
3
3
|
"private": false,
|
|
4
|
-
"version": "1.15.17-staging.
|
|
4
|
+
"version": "1.15.17-staging.6",
|
|
5
5
|
"description": "Firebase authentication service with anonymous upgrade support",
|
|
6
6
|
"main": "dist/index.js",
|
|
7
7
|
"module": "dist/index.mjs",
|
|
@@ -59,7 +59,7 @@ export class ClinicAggregationService {
|
|
|
59
59
|
try {
|
|
60
60
|
await groupRef.update({
|
|
61
61
|
clinicsInfo: admin.firestore.FieldValue.arrayUnion(clinicInfo),
|
|
62
|
-
|
|
62
|
+
clinics: admin.firestore.FieldValue.arrayUnion(clinicId),
|
|
63
63
|
updatedAt: admin.firestore.FieldValue.serverTimestamp(),
|
|
64
64
|
});
|
|
65
65
|
|
|
@@ -319,11 +319,12 @@ export class AuthService extends BaseService {
|
|
|
319
319
|
// Now update the admin with the group ID
|
|
320
320
|
console.log('[AUTH] Updating admin with clinic group ID');
|
|
321
321
|
await clinicAdminService.updateClinicAdmin(adminProfile.id, {
|
|
322
|
-
// Use admin profile ID, not user UID
|
|
323
322
|
clinicGroupId: clinicGroup.id,
|
|
324
323
|
});
|
|
325
324
|
console.log('[AUTH] Admin updated with clinic group ID successfully');
|
|
326
325
|
|
|
326
|
+
// adminsInfo is populated by onCreateClinicAdmin Firestore trigger
|
|
327
|
+
|
|
327
328
|
// Get the updated admin profile
|
|
328
329
|
adminProfile = await clinicAdminService.getClinicAdmin(adminProfile.id);
|
|
329
330
|
} catch (groupCreationError) {
|
|
@@ -341,57 +342,23 @@ export class AuthService extends BaseService {
|
|
|
341
342
|
token: data.inviteToken,
|
|
342
343
|
});
|
|
343
344
|
|
|
344
|
-
// Find the token
|
|
345
|
-
console.log('[AUTH] Searching for token
|
|
346
|
-
const
|
|
347
|
-
const q = query(groupsRef);
|
|
348
|
-
const querySnapshot = await getDocs(q);
|
|
349
|
-
|
|
350
|
-
let foundGroup: ClinicGroup | null = null;
|
|
351
|
-
let foundToken: AdminToken | null = null;
|
|
352
|
-
|
|
353
|
-
console.log('[AUTH] Found', querySnapshot.size, 'clinic groups to check');
|
|
354
|
-
for (const docSnapshot of querySnapshot.docs) {
|
|
355
|
-
const group = docSnapshot.data() as ClinicGroup;
|
|
356
|
-
console.log('[AUTH] Checking group', {
|
|
357
|
-
groupId: group.id,
|
|
358
|
-
groupName: group.name,
|
|
359
|
-
});
|
|
360
|
-
|
|
361
|
-
// Find the token in the group's tokens
|
|
362
|
-
const token = group.adminTokens.find(t => {
|
|
363
|
-
const isMatch =
|
|
364
|
-
t.token === data.inviteToken &&
|
|
365
|
-
t.status === AdminTokenStatus.ACTIVE &&
|
|
366
|
-
new Date(t.expiresAt.toDate()) > new Date();
|
|
367
|
-
|
|
368
|
-
console.log('[AUTH] Checking token', {
|
|
369
|
-
tokenId: t.id,
|
|
370
|
-
tokenMatch: t.token === data.inviteToken,
|
|
371
|
-
tokenStatus: t.status,
|
|
372
|
-
tokenActive: t.status === AdminTokenStatus.ACTIVE,
|
|
373
|
-
tokenExpiry: new Date(t.expiresAt.toDate()),
|
|
374
|
-
tokenExpired: new Date(t.expiresAt.toDate()) <= new Date(),
|
|
375
|
-
isMatch,
|
|
376
|
-
});
|
|
377
|
-
|
|
378
|
-
return isMatch;
|
|
379
|
-
});
|
|
345
|
+
// Find the token using collection group query (O(1) instead of scanning all groups)
|
|
346
|
+
console.log('[AUTH] Searching for token via collection group query');
|
|
347
|
+
const tokenResult = await clinicGroupService.findAdminTokenByValue(data.inviteToken);
|
|
380
348
|
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
console.log('[AUTH] Found matching token in group', {
|
|
385
|
-
groupId: group.id,
|
|
386
|
-
tokenId: token.id,
|
|
387
|
-
});
|
|
388
|
-
break;
|
|
389
|
-
}
|
|
349
|
+
if (!tokenResult) {
|
|
350
|
+
console.error('[AUTH] No valid active token found');
|
|
351
|
+
throw new Error('Invalid or expired invite token');
|
|
390
352
|
}
|
|
391
353
|
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
354
|
+
console.log('[AUTH] Found matching token', {
|
|
355
|
+
tokenId: tokenResult.token.id,
|
|
356
|
+
clinicGroupId: tokenResult.clinicGroupId,
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
const foundGroup = await clinicGroupService.getClinicGroup(tokenResult.clinicGroupId);
|
|
360
|
+
if (!foundGroup) {
|
|
361
|
+
throw new Error('Clinic group not found for token');
|
|
395
362
|
}
|
|
396
363
|
|
|
397
364
|
clinicGroup = foundGroup;
|
|
@@ -198,13 +198,20 @@ export class ClinicGroupService extends BaseService {
|
|
|
198
198
|
return ClinicGroupUtils.getActiveAdminTokens(this.db, groupId, adminId, this.app);
|
|
199
199
|
}
|
|
200
200
|
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
201
|
+
/**
|
|
202
|
+
* Gets ALL admin tokens for a clinic group (all statuses)
|
|
203
|
+
*/
|
|
204
|
+
async getAllAdminTokens(groupId: string, adminId: string): Promise<AdminToken[]> {
|
|
205
|
+
return ClinicGroupUtils.getAllAdminTokens(this.db, groupId, adminId, this.app);
|
|
206
|
+
}
|
|
205
207
|
|
|
206
|
-
|
|
207
|
-
|
|
208
|
+
/**
|
|
209
|
+
* Finds an admin token by its value across all clinic groups.
|
|
210
|
+
* Uses a collection group query for O(1) lookup.
|
|
211
|
+
*/
|
|
212
|
+
async findAdminTokenByValue(tokenValue: string): Promise<{ token: AdminToken; clinicGroupId: string } | null> {
|
|
213
|
+
return ClinicGroupUtils.findAdminTokenByValue(this.db, tokenValue);
|
|
214
|
+
}
|
|
208
215
|
|
|
209
216
|
/**
|
|
210
217
|
* Updates the onboarding status for a clinic group
|
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
import {
|
|
2
2
|
collection,
|
|
3
|
+
collectionGroup,
|
|
3
4
|
doc,
|
|
4
5
|
getDoc,
|
|
5
6
|
getDocs,
|
|
6
7
|
query,
|
|
7
8
|
where,
|
|
9
|
+
orderBy,
|
|
10
|
+
limit,
|
|
8
11
|
updateDoc,
|
|
9
12
|
setDoc,
|
|
10
13
|
deleteDoc,
|
|
@@ -161,7 +164,7 @@ export async function createClinicGroup(
|
|
|
161
164
|
clinicsInfo: [],
|
|
162
165
|
admins: [ownerId],
|
|
163
166
|
adminsInfo: [],
|
|
164
|
-
adminTokens: [],
|
|
167
|
+
adminTokens: [], // @deprecated — tokens now in subcollection, kept for backward compat
|
|
165
168
|
ownerId,
|
|
166
169
|
createdAt: now,
|
|
167
170
|
updatedAt: now,
|
|
@@ -369,9 +372,10 @@ export async function addAdminToGroup(
|
|
|
369
372
|
adminId,
|
|
370
373
|
groupId,
|
|
371
374
|
});
|
|
372
|
-
return;
|
|
375
|
+
return;
|
|
373
376
|
}
|
|
374
377
|
|
|
378
|
+
// Only add the admin ID — adminsInfo is populated by onCreateClinicAdmin trigger
|
|
375
379
|
console.log("[CLINIC_GROUP] Updating group with new admin");
|
|
376
380
|
await updateClinicGroup(
|
|
377
381
|
db,
|
|
@@ -459,6 +463,18 @@ export async function deactivateClinicGroup(
|
|
|
459
463
|
* @param data - Token data
|
|
460
464
|
* @returns The created admin token
|
|
461
465
|
*/
|
|
466
|
+
// --- Admin Token Subcollection ---
|
|
467
|
+
// Tokens are stored in: clinic_groups/{groupId}/adminTokens/{tokenId}
|
|
468
|
+
|
|
469
|
+
const ADMIN_TOKENS_SUBCOLLECTION = "adminTokens";
|
|
470
|
+
|
|
471
|
+
function adminTokensRef(db: Firestore, groupId: string) {
|
|
472
|
+
return collection(db, CLINIC_GROUPS_COLLECTION, groupId, ADMIN_TOKENS_SUBCOLLECTION);
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
/**
|
|
476
|
+
* Creates an admin token in the subcollection.
|
|
477
|
+
*/
|
|
462
478
|
export async function createAdminToken(
|
|
463
479
|
db: Firestore,
|
|
464
480
|
groupId: string,
|
|
@@ -471,121 +487,105 @@ export async function createAdminToken(
|
|
|
471
487
|
throw new Error("Clinic group not found");
|
|
472
488
|
}
|
|
473
489
|
|
|
474
|
-
// Proveravamo da li admin pripada grupi
|
|
475
490
|
if (!group.admins.includes(creatorAdminId)) {
|
|
476
491
|
throw new Error("Admin does not belong to this clinic group");
|
|
477
492
|
}
|
|
478
493
|
|
|
479
494
|
const now = Timestamp.now();
|
|
480
|
-
const expiresInDays = data?.expiresInDays || 7;
|
|
495
|
+
const expiresInDays = data?.expiresInDays || 7;
|
|
481
496
|
const email = data?.email || null;
|
|
482
497
|
const expiresAt = new Timestamp(
|
|
483
498
|
now.seconds + expiresInDays * 24 * 60 * 60,
|
|
484
499
|
now.nanoseconds
|
|
485
500
|
);
|
|
486
501
|
|
|
502
|
+
const tokenRef = doc(adminTokensRef(db, groupId));
|
|
487
503
|
const token: AdminToken = {
|
|
488
|
-
id:
|
|
504
|
+
id: tokenRef.id,
|
|
489
505
|
token: generateId(),
|
|
490
506
|
status: AdminTokenStatus.ACTIVE,
|
|
491
507
|
email,
|
|
508
|
+
clinicGroupId: groupId,
|
|
492
509
|
createdAt: now,
|
|
493
510
|
expiresAt,
|
|
494
511
|
};
|
|
495
512
|
|
|
496
|
-
|
|
497
|
-
// Ovo treba promeniti, staviti admin tokene u sub-kolekciju u klinickoj grupi
|
|
498
|
-
await updateClinicGroup(
|
|
499
|
-
db,
|
|
500
|
-
groupId,
|
|
501
|
-
{
|
|
502
|
-
adminTokens: [...group.adminTokens, token],
|
|
503
|
-
},
|
|
504
|
-
app
|
|
505
|
-
);
|
|
506
|
-
|
|
513
|
+
await setDoc(tokenRef, token);
|
|
507
514
|
return token;
|
|
508
515
|
}
|
|
509
516
|
|
|
510
517
|
/**
|
|
511
|
-
* Verifies and
|
|
512
|
-
*
|
|
513
|
-
* @param groupId - ID of the clinic group
|
|
514
|
-
* @param token - Token to verify
|
|
515
|
-
* @param userRef - User reference
|
|
516
|
-
* @param app - Firebase app instance
|
|
517
|
-
* @returns Whether the token was successfully used
|
|
518
|
+
* Verifies and marks an admin token as used.
|
|
519
|
+
* Uses a collection group query to find the token across all groups.
|
|
518
520
|
*/
|
|
519
521
|
export async function verifyAndUseAdminToken(
|
|
520
522
|
db: Firestore,
|
|
521
523
|
groupId: string,
|
|
522
|
-
|
|
524
|
+
tokenValue: string,
|
|
523
525
|
userRef: string,
|
|
524
526
|
app: FirebaseApp
|
|
525
527
|
): Promise<boolean> {
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
528
|
+
// Query the subcollection for the matching token
|
|
529
|
+
const tokensQuery = query(
|
|
530
|
+
adminTokensRef(db, groupId),
|
|
531
|
+
where("token", "==", tokenValue),
|
|
532
|
+
limit(1)
|
|
533
|
+
);
|
|
534
|
+
const snapshot = await getDocs(tokensQuery);
|
|
530
535
|
|
|
531
|
-
|
|
532
|
-
if (!adminToken) {
|
|
536
|
+
if (snapshot.empty) {
|
|
533
537
|
throw new Error("Admin token not found");
|
|
534
538
|
}
|
|
535
539
|
|
|
540
|
+
const tokenDoc = snapshot.docs[0];
|
|
541
|
+
const adminToken = tokenDoc.data() as AdminToken;
|
|
542
|
+
|
|
536
543
|
if (adminToken.status !== AdminTokenStatus.ACTIVE) {
|
|
537
544
|
throw new Error("Admin token is not active");
|
|
538
545
|
}
|
|
539
546
|
|
|
540
547
|
const now = Timestamp.now();
|
|
541
548
|
if (adminToken.expiresAt.seconds < now.seconds) {
|
|
542
|
-
|
|
543
|
-
const updatedTokens = group.adminTokens.map((t) =>
|
|
544
|
-
t.id === adminToken.id ? { ...t, status: AdminTokenStatus.EXPIRED } : t
|
|
545
|
-
);
|
|
546
|
-
|
|
547
|
-
await updateClinicGroup(
|
|
548
|
-
db,
|
|
549
|
-
groupId,
|
|
550
|
-
{
|
|
551
|
-
adminTokens: updatedTokens,
|
|
552
|
-
},
|
|
553
|
-
app
|
|
554
|
-
);
|
|
555
|
-
|
|
549
|
+
await updateDoc(tokenDoc.ref, { status: AdminTokenStatus.EXPIRED });
|
|
556
550
|
throw new Error("Admin token has expired");
|
|
557
551
|
}
|
|
558
552
|
|
|
559
|
-
//
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
status: AdminTokenStatus.USED,
|
|
565
|
-
usedByUserRef: userRef,
|
|
566
|
-
}
|
|
567
|
-
: t
|
|
568
|
-
);
|
|
553
|
+
// Mark as used
|
|
554
|
+
await updateDoc(tokenDoc.ref, {
|
|
555
|
+
status: AdminTokenStatus.USED,
|
|
556
|
+
usedByUserRef: userRef,
|
|
557
|
+
});
|
|
569
558
|
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
559
|
+
return true;
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
/**
|
|
563
|
+
* Finds an admin token by its token string across ALL clinic groups.
|
|
564
|
+
* Uses a collection group query — much more efficient than scanning all groups.
|
|
565
|
+
*/
|
|
566
|
+
export async function findAdminTokenByValue(
|
|
567
|
+
db: Firestore,
|
|
568
|
+
tokenValue: string
|
|
569
|
+
): Promise<{ token: AdminToken; clinicGroupId: string } | null> {
|
|
570
|
+
const tokensQuery = query(
|
|
571
|
+
collectionGroup(db, ADMIN_TOKENS_SUBCOLLECTION),
|
|
572
|
+
where("token", "==", tokenValue),
|
|
573
|
+
where("status", "==", AdminTokenStatus.ACTIVE),
|
|
574
|
+
limit(1)
|
|
577
575
|
);
|
|
576
|
+
const snapshot = await getDocs(tokensQuery);
|
|
578
577
|
|
|
579
|
-
return
|
|
578
|
+
if (snapshot.empty) return null;
|
|
579
|
+
|
|
580
|
+
const tokenData = snapshot.docs[0].data() as AdminToken;
|
|
581
|
+
return {
|
|
582
|
+
token: tokenData,
|
|
583
|
+
clinicGroupId: tokenData.clinicGroupId,
|
|
584
|
+
};
|
|
580
585
|
}
|
|
581
586
|
|
|
582
587
|
/**
|
|
583
|
-
* Deletes an admin token
|
|
584
|
-
* @param db - Firestore database instance
|
|
585
|
-
* @param groupId - ID of the clinic group
|
|
586
|
-
* @param tokenId - ID of the token to delete
|
|
587
|
-
* @param adminId - ID of the admin making the deletion
|
|
588
|
-
* @param app - Firebase app instance
|
|
588
|
+
* Deletes an admin token from the subcollection.
|
|
589
589
|
*/
|
|
590
590
|
export async function deleteAdminToken(
|
|
591
591
|
db: Firestore,
|
|
@@ -599,33 +599,43 @@ export async function deleteAdminToken(
|
|
|
599
599
|
throw new Error("Clinic group not found");
|
|
600
600
|
}
|
|
601
601
|
|
|
602
|
-
// Proveravamo da li admin pripada grupi
|
|
603
602
|
if (!group.admins.includes(adminId)) {
|
|
604
603
|
throw new Error("Admin does not belong to this clinic group");
|
|
605
604
|
}
|
|
606
605
|
|
|
607
|
-
|
|
608
|
-
|
|
606
|
+
await deleteDoc(doc(adminTokensRef(db, groupId), tokenId));
|
|
607
|
+
}
|
|
609
608
|
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
609
|
+
/**
|
|
610
|
+
* Gets active admin tokens for a clinic group from the subcollection.
|
|
611
|
+
*/
|
|
612
|
+
export async function getActiveAdminTokens(
|
|
613
|
+
db: Firestore,
|
|
614
|
+
groupId: string,
|
|
615
|
+
adminId: string,
|
|
616
|
+
app: FirebaseApp
|
|
617
|
+
): Promise<AdminToken[]> {
|
|
618
|
+
const group = await getClinicGroup(db, groupId);
|
|
619
|
+
if (!group) {
|
|
620
|
+
throw new Error("Clinic group not found");
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
if (!group.admins.includes(adminId)) {
|
|
624
|
+
throw new Error("Admin does not belong to this clinic group");
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
const tokensQuery = query(
|
|
628
|
+
adminTokensRef(db, groupId),
|
|
629
|
+
where("status", "==", AdminTokenStatus.ACTIVE)
|
|
617
630
|
);
|
|
631
|
+
const snapshot = await getDocs(tokensQuery);
|
|
632
|
+
return snapshot.docs.map((d) => d.data() as AdminToken);
|
|
618
633
|
}
|
|
619
634
|
|
|
620
635
|
/**
|
|
621
|
-
* Gets
|
|
622
|
-
* @param db - Firestore database instance
|
|
623
|
-
* @param groupId - ID of the clinic group
|
|
624
|
-
* @param adminId - ID of the admin requesting the tokens
|
|
625
|
-
* @param app - Firebase app instance (not used but included for consistency)
|
|
626
|
-
* @returns Array of active admin tokens
|
|
636
|
+
* Gets ALL admin tokens for a clinic group (all statuses).
|
|
627
637
|
*/
|
|
628
|
-
export async function
|
|
638
|
+
export async function getAllAdminTokens(
|
|
629
639
|
db: Firestore,
|
|
630
640
|
groupId: string,
|
|
631
641
|
adminId: string,
|
|
@@ -636,11 +646,10 @@ export async function getActiveAdminTokens(
|
|
|
636
646
|
throw new Error("Clinic group not found");
|
|
637
647
|
}
|
|
638
648
|
|
|
639
|
-
// Proveravamo da li admin pripada grupi
|
|
640
649
|
if (!group.admins.includes(adminId)) {
|
|
641
650
|
throw new Error("Admin does not belong to this clinic group");
|
|
642
651
|
}
|
|
643
652
|
|
|
644
|
-
|
|
645
|
-
return
|
|
653
|
+
const snapshot = await getDocs(adminTokensRef(db, groupId));
|
|
654
|
+
return snapshot.docs.map((d) => d.data() as AdminToken);
|
|
646
655
|
}
|
|
@@ -160,6 +160,7 @@ export interface AdminToken {
|
|
|
160
160
|
email?: string | null;
|
|
161
161
|
status: AdminTokenStatus;
|
|
162
162
|
usedByUserRef?: string;
|
|
163
|
+
clinicGroupId: string;
|
|
163
164
|
createdAt: Timestamp;
|
|
164
165
|
expiresAt: Timestamp;
|
|
165
166
|
}
|
|
@@ -308,7 +309,8 @@ export interface ClinicGroup {
|
|
|
308
309
|
clinicsInfo: ClinicInfo[];
|
|
309
310
|
admins: string[];
|
|
310
311
|
adminsInfo: AdminInfo[];
|
|
311
|
-
adminTokens
|
|
312
|
+
/** @deprecated Tokens now stored in subcollection clinic_groups/{id}/adminTokens */
|
|
313
|
+
adminTokens?: AdminToken[];
|
|
312
314
|
ownerId: string | null;
|
|
313
315
|
createdAt: Timestamp;
|
|
314
316
|
updatedAt: Timestamp;
|
|
@@ -143,8 +143,9 @@ export const adminTokenSchema = z.object({
|
|
|
143
143
|
email: z.string().email().optional().nullable(),
|
|
144
144
|
status: z.nativeEnum(AdminTokenStatus),
|
|
145
145
|
usedByUserRef: z.string().optional(),
|
|
146
|
-
|
|
147
|
-
|
|
146
|
+
clinicGroupId: z.string(),
|
|
147
|
+
createdAt: z.instanceof(Date).or(z.instanceof(Timestamp)),
|
|
148
|
+
expiresAt: z.instanceof(Date).or(z.instanceof(Timestamp)),
|
|
148
149
|
});
|
|
149
150
|
|
|
150
151
|
/**
|
|
@@ -233,7 +234,7 @@ export const clinicGroupSchema = z.object({
|
|
|
233
234
|
clinicsInfo: z.array(clinicInfoSchema),
|
|
234
235
|
admins: z.array(z.string()),
|
|
235
236
|
adminsInfo: z.array(adminInfoSchema),
|
|
236
|
-
adminTokens: z.array(adminTokenSchema),
|
|
237
|
+
adminTokens: z.array(adminTokenSchema).optional(),
|
|
237
238
|
ownerId: z.string().nullable(),
|
|
238
239
|
createdAt: z.instanceof(Date).or(z.instanceof(Timestamp)), // Timestamp
|
|
239
240
|
updatedAt: z.instanceof(Date).or(z.instanceof(Timestamp)), // Timestamp
|