@umituz/web-firebase 1.0.5 → 2.0.1

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 (69) hide show
  1. package/README.md +555 -0
  2. package/dist/application/index.d.mts +273 -0
  3. package/dist/application/index.d.ts +273 -0
  4. package/dist/application/index.js +490 -0
  5. package/dist/application/index.mjs +19 -0
  6. package/dist/chunk-34DL2QWQ.mjs +87 -0
  7. package/dist/chunk-4FP2ELQ5.mjs +96 -0
  8. package/dist/chunk-7TX3OU3O.mjs +721 -0
  9. package/dist/chunk-I6WGBPFB.mjs +439 -0
  10. package/dist/chunk-RZ4QR6TB.mjs +96 -0
  11. package/dist/chunk-U2XI4MGO.mjs +397 -0
  12. package/dist/domain/index.d.mts +325 -0
  13. package/dist/domain/index.d.ts +325 -0
  14. package/dist/domain/index.js +662 -0
  15. package/dist/domain/index.mjs +36 -0
  16. package/dist/file.repository.interface-v5vHgVsZ.d.mts +241 -0
  17. package/dist/file.repository.interface-v5vHgVsZ.d.ts +241 -0
  18. package/dist/firebase.entity-xvfEPjXZ.d.mts +15 -0
  19. package/dist/firebase.entity-xvfEPjXZ.d.ts +15 -0
  20. package/dist/index.d.mts +14 -96
  21. package/dist/index.d.ts +14 -96
  22. package/dist/index.js +1717 -78
  23. package/dist/index.mjs +88 -175
  24. package/dist/infrastructure/index.d.mts +170 -0
  25. package/dist/infrastructure/index.d.ts +170 -0
  26. package/dist/infrastructure/index.js +856 -0
  27. package/dist/infrastructure/index.mjs +46 -0
  28. package/dist/presentation/index.d.mts +25 -0
  29. package/dist/presentation/index.d.ts +25 -0
  30. package/dist/presentation/index.js +105 -0
  31. package/dist/presentation/index.mjs +6 -0
  32. package/dist/user.repository.interface-DS74TsJ5.d.mts +298 -0
  33. package/dist/user.repository.interface-DS74TsJ5.d.ts +298 -0
  34. package/package.json +37 -11
  35. package/src/application/dto/auth.dto.ts +69 -0
  36. package/src/application/dto/index.ts +7 -0
  37. package/src/application/dto/user.dto.ts +64 -0
  38. package/src/application/index.ts +7 -0
  39. package/src/application/use-cases/auth/reset-password.use-case.ts +66 -0
  40. package/src/application/use-cases/auth/sign-in-with-google.use-case.ts +86 -0
  41. package/src/application/use-cases/auth/sign-in.use-case.ts +77 -0
  42. package/src/application/use-cases/auth/sign-out.use-case.ts +22 -0
  43. package/src/application/use-cases/auth/sign-up.use-case.ts +99 -0
  44. package/src/application/use-cases/index.ts +12 -0
  45. package/src/application/use-cases/user/delete-account.use-case.ts +77 -0
  46. package/src/application/use-cases/user/update-profile.use-case.ts +98 -0
  47. package/src/domain/entities/file.entity.ts +151 -0
  48. package/src/domain/entities/timestamp.entity.ts +116 -0
  49. package/src/domain/entities/user.entity.ts +193 -0
  50. package/src/domain/errors/auth.errors.ts +115 -0
  51. package/src/domain/errors/repository.errors.ts +121 -0
  52. package/src/domain/index.ts +25 -2
  53. package/src/domain/interfaces/auth.repository.interface.ts +83 -0
  54. package/src/domain/interfaces/file.repository.interface.ts +143 -0
  55. package/src/domain/interfaces/user.repository.interface.ts +75 -0
  56. package/src/domain/value-objects/email.vo.ts +105 -0
  57. package/src/domain/value-objects/file-path.vo.ts +184 -0
  58. package/src/domain/value-objects/user-id.vo.ts +87 -0
  59. package/src/index.ts +19 -4
  60. package/src/infrastructure/firebase/auth.adapter.ts +220 -0
  61. package/src/infrastructure/firebase/client.ts +141 -0
  62. package/src/infrastructure/firebase/firestore.adapter.ts +190 -0
  63. package/src/infrastructure/firebase/storage.adapter.ts +323 -0
  64. package/src/infrastructure/index.ts +10 -5
  65. package/src/infrastructure/utils/storage.util.ts +3 -3
  66. package/src/presentation/hooks/useAuth.ts +153 -0
  67. package/src/presentation/hooks/useFirestore.ts +125 -0
  68. package/src/presentation/hooks/useStorage.ts +141 -0
  69. package/src/presentation/providers/FirebaseProvider.tsx +40 -0
@@ -0,0 +1,77 @@
1
+ /**
2
+ * Sign In Use Case
3
+ * @description Handles user sign in with email/password
4
+ */
5
+
6
+ import type { UserCredential } from 'firebase/auth'
7
+ import type { IAuthRepository } from '../../../domain/interfaces/auth.repository.interface'
8
+ import type { SignInDTO } from '../../dto/auth.dto'
9
+ import { createAuthError, AuthErrorCode } from '../../../domain/errors/auth.errors'
10
+
11
+ export class SignInUseCase {
12
+ constructor(private readonly authRepository: IAuthRepository) {}
13
+
14
+ /**
15
+ * Execute sign in use case
16
+ */
17
+ async execute(dto: SignInDTO): Promise<UserCredential> {
18
+ try {
19
+ // Validate input
20
+ this.validateDTO(dto)
21
+
22
+ // Perform sign in
23
+ const result = await this.authRepository.signIn(dto.email, dto.password)
24
+
25
+ return result
26
+ } catch (error) {
27
+ throw this.handleError(error)
28
+ }
29
+ }
30
+
31
+ /**
32
+ * Validate DTO
33
+ */
34
+ private validateDTO(dto: SignInDTO): void {
35
+ if (!dto.email) {
36
+ throw createAuthError(AuthErrorCode.INVALID_CREDENTIALS, 'Email is required')
37
+ }
38
+
39
+ if (!dto.password) {
40
+ throw createAuthError(AuthErrorCode.INVALID_CREDENTIALS, 'Password is required')
41
+ }
42
+
43
+ // Basic email validation
44
+ const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
45
+ if (!emailRegex.test(dto.email)) {
46
+ throw createAuthError(AuthErrorCode.INVALID_CREDENTIALS, 'Invalid email format')
47
+ }
48
+ }
49
+
50
+ /**
51
+ * Handle errors
52
+ */
53
+ private handleError(error: unknown): Error {
54
+ if (error instanceof Error) {
55
+ // Firebase auth errors
56
+ const message = error.message.toLowerCase()
57
+
58
+ if (message.includes('user-not-found') || message.includes('invalid-email')) {
59
+ return createAuthError(AuthErrorCode.USER_NOT_FOUND, 'User not found', error)
60
+ }
61
+
62
+ if (message.includes('wrong-password') || message.includes('invalid-credential')) {
63
+ return createAuthError(AuthErrorCode.INVALID_CREDENTIALS, 'Invalid credentials', error)
64
+ }
65
+
66
+ if (message.includes('too-many-requests')) {
67
+ return createAuthError(AuthErrorCode.TOO_MANY_REQUESTS, 'Too many attempts', error)
68
+ }
69
+
70
+ if (message.includes('user-disabled')) {
71
+ return createAuthError(AuthErrorCode.USER_NOT_FOUND, 'Account disabled', error)
72
+ }
73
+ }
74
+
75
+ return createAuthError(AuthErrorCode.SIGN_IN_FAILED, 'Sign in failed', error)
76
+ }
77
+ }
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Sign Out Use Case
3
+ * @description Handles user sign out
4
+ */
5
+
6
+ import type { IAuthRepository } from '../../../domain/interfaces/auth.repository.interface'
7
+ import { createAuthError, AuthErrorCode } from '../../../domain/errors/auth.errors'
8
+
9
+ export class SignOutUseCase {
10
+ constructor(private readonly authRepository: IAuthRepository) {}
11
+
12
+ /**
13
+ * Execute sign out use case
14
+ */
15
+ async execute(): Promise<void> {
16
+ try {
17
+ await this.authRepository.signOut()
18
+ } catch (error) {
19
+ throw createAuthError(AuthErrorCode.SIGN_OUT_FAILED, 'Sign out failed', error)
20
+ }
21
+ }
22
+ }
@@ -0,0 +1,99 @@
1
+ /**
2
+ * Sign Up Use Case
3
+ * @description Handles user registration with email/password
4
+ */
5
+
6
+ import type { IAuthRepository } from '../../../domain/interfaces/auth.repository.interface'
7
+ import type { IUserRepository } from '../../../domain/interfaces/user.repository.interface'
8
+ import type { SignUpDTO, SignUpResult } from '../../dto/auth.dto'
9
+ import { createAuthError, AuthErrorCode } from '../../../domain/errors/auth.errors'
10
+
11
+ export class SignUpUseCase {
12
+ constructor(
13
+ private readonly authRepository: IAuthRepository,
14
+ private readonly userRepository: IUserRepository
15
+ ) {}
16
+
17
+ /**
18
+ * Execute sign up use case
19
+ */
20
+ async execute(dto: SignUpDTO): Promise<SignUpResult> {
21
+ try {
22
+ // Validate input
23
+ this.validateDTO(dto)
24
+
25
+ // Check if user already exists
26
+ const existingUser = await this.userRepository.getUserByEmail(dto.email)
27
+ if (existingUser) {
28
+ throw createAuthError(AuthErrorCode.USER_ALREADY_EXISTS, 'User already exists')
29
+ }
30
+
31
+ // Perform sign up
32
+ const result = await this.authRepository.signUp(dto.email, dto.password, dto.displayName)
33
+
34
+ // Return enhanced result
35
+ return {
36
+ ...result,
37
+ userId: result.user.uid,
38
+ emailVerified: result.user.emailVerified,
39
+ }
40
+ } catch (error) {
41
+ throw this.handleError(error)
42
+ }
43
+ }
44
+
45
+ /**
46
+ * Validate DTO
47
+ */
48
+ private validateDTO(dto: SignUpDTO): void {
49
+ if (!dto.email) {
50
+ throw createAuthError(AuthErrorCode.SIGN_UP_FAILED, 'Email is required')
51
+ }
52
+
53
+ if (!dto.password) {
54
+ throw createAuthError(AuthErrorCode.SIGN_UP_FAILED, 'Password is required')
55
+ }
56
+
57
+ if (!dto.displayName || dto.displayName.trim().length === 0) {
58
+ throw createAuthError(AuthErrorCode.SIGN_UP_FAILED, 'Display name is required')
59
+ }
60
+
61
+ // Email validation
62
+ const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
63
+ if (!emailRegex.test(dto.email)) {
64
+ throw createAuthError(AuthErrorCode.SIGN_UP_FAILED, 'Invalid email format')
65
+ }
66
+
67
+ // Password strength validation
68
+ if (dto.password.length < 6) {
69
+ throw createAuthError(AuthErrorCode.WEAK_PASSWORD, 'Password must be at least 6 characters')
70
+ }
71
+ }
72
+
73
+ /**
74
+ * Handle errors
75
+ */
76
+ private handleError(error: unknown): Error {
77
+ if (error instanceof Error && 'code' in error) {
78
+ const code = (error as { code: string }).code
79
+
80
+ if (code === 'auth/email-already-in-use') {
81
+ return createAuthError(AuthErrorCode.USER_ALREADY_EXISTS, 'Email already in use', error)
82
+ }
83
+
84
+ if (code === 'auth/invalid-email') {
85
+ return createAuthError(AuthErrorCode.SIGN_UP_FAILED, 'Invalid email', error)
86
+ }
87
+
88
+ if (code === 'auth/weak-password') {
89
+ return createAuthError(AuthErrorCode.WEAK_PASSWORD, 'Password is too weak', error)
90
+ }
91
+
92
+ if (code === 'auth/operation-not-allowed') {
93
+ return createAuthError(AuthErrorCode.SIGN_UP_FAILED, 'Email/password accounts not enabled', error)
94
+ }
95
+ }
96
+
97
+ return createAuthError(AuthErrorCode.SIGN_UP_FAILED, 'Sign up failed', error)
98
+ }
99
+ }
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Application Use Cases
3
+ * @description Use cases for auth and user operations
4
+ */
5
+
6
+ export * from './auth/sign-in.use-case'
7
+ export * from './auth/sign-up.use-case'
8
+ export * from './auth/sign-in-with-google.use-case'
9
+ export * from './auth/reset-password.use-case'
10
+ export * from './auth/sign-out.use-case'
11
+ export * from './user/update-profile.use-case'
12
+ export * from './user/delete-account.use-case'
@@ -0,0 +1,77 @@
1
+ /**
2
+ * Delete Account Use Case
3
+ * @description Handles user account deletion
4
+ */
5
+
6
+ import type { IAuthRepository } from '../../../domain/interfaces/auth.repository.interface'
7
+ import type { DeleteAccountDTO } from '../../dto/auth.dto'
8
+ import { createAuthError, AuthErrorCode } from '../../../domain/errors/auth.errors'
9
+
10
+ export class DeleteAccountUseCase {
11
+ constructor(private readonly authRepository: IAuthRepository) {}
12
+
13
+ /**
14
+ * Execute account deletion use case
15
+ */
16
+ async execute(dto: DeleteAccountDTO): Promise<void> {
17
+ try {
18
+ // Validate input
19
+ this.validateDTO(dto)
20
+
21
+ // Check if user is authenticated
22
+ const currentUser = this.authRepository.getCurrentUser()
23
+ if (!currentUser) {
24
+ throw createAuthError(AuthErrorCode.UNAUTHENTICATED, 'No user logged in')
25
+ }
26
+
27
+ if (!currentUser.email) {
28
+ throw createAuthError(AuthErrorCode.ACCOUNT_DELETE_FAILED, 'User email not available')
29
+ }
30
+
31
+ // Delete account (includes password verification)
32
+ await this.authRepository.deleteAccount(dto.password)
33
+ } catch (error) {
34
+ throw this.handleError(error)
35
+ }
36
+ }
37
+
38
+ /**
39
+ * Validate DTO
40
+ */
41
+ private validateDTO(dto: DeleteAccountDTO): void {
42
+ if (!dto.password) {
43
+ throw createAuthError(AuthErrorCode.ACCOUNT_DELETE_FAILED, 'Password is required')
44
+ }
45
+
46
+ if (dto.password.length < 1) {
47
+ throw createAuthError(AuthErrorCode.ACCOUNT_DELETE_FAILED, 'Password cannot be empty')
48
+ }
49
+ }
50
+
51
+ /**
52
+ * Handle errors
53
+ */
54
+ private handleError(error: unknown): Error {
55
+ if (error instanceof Error && 'code' in error) {
56
+ const code = (error as { code: string }).code
57
+
58
+ if (code === 'auth/requires-recent-login') {
59
+ return createAuthError(AuthErrorCode.REAUTHENTICATION_REQUIRED, 'Please reauthenticate first', error)
60
+ }
61
+
62
+ if (code === 'auth/wrong-password') {
63
+ return createAuthError(AuthErrorCode.REAUTHENTICATION_FAILED, 'Invalid password', error)
64
+ }
65
+
66
+ if (code === 'auth/too-many-requests') {
67
+ return createAuthError(AuthErrorCode.TOO_MANY_REQUESTS, 'Too many requests', error)
68
+ }
69
+
70
+ if (code === 'auth/user-not-found') {
71
+ return createAuthError(AuthErrorCode.ACCOUNT_DELETE_FAILED, 'User not found', error)
72
+ }
73
+ }
74
+
75
+ return createAuthError(AuthErrorCode.ACCOUNT_DELETE_FAILED, 'Account deletion failed', error)
76
+ }
77
+ }
@@ -0,0 +1,98 @@
1
+ /**
2
+ * Update Profile Use Case
3
+ * @description Handles user profile updates
4
+ */
5
+
6
+ import type { IAuthRepository } from '../../../domain/interfaces/auth.repository.interface'
7
+ import type { UpdateProfileDTO } from '../../dto/auth.dto'
8
+ import { createAuthError, AuthErrorCode } from '../../../domain/errors/auth.errors'
9
+
10
+ export class UpdateProfileUseCase {
11
+ constructor(private readonly authRepository: IAuthRepository) {}
12
+
13
+ /**
14
+ * Execute profile update use case
15
+ */
16
+ async execute(dto: UpdateProfileDTO): Promise<void> {
17
+ try {
18
+ // Validate input
19
+ this.validateDTO(dto)
20
+
21
+ // Check if user is authenticated
22
+ const currentUser = this.authRepository.getCurrentUser()
23
+ if (!currentUser) {
24
+ throw createAuthError(AuthErrorCode.UNAUTHENTICATED, 'No user logged in')
25
+ }
26
+
27
+ // Update profile
28
+ await this.authRepository.updateProfile(dto)
29
+ } catch (error) {
30
+ throw this.handleError(error)
31
+ }
32
+ }
33
+
34
+ /**
35
+ * Validate DTO
36
+ */
37
+ private validateDTO(dto: UpdateProfileDTO): void {
38
+ if (!dto.displayName && !dto.photoURL) {
39
+ throw createAuthError(AuthErrorCode.PROFILE_UPDATE_FAILED, 'At least one field must be provided')
40
+ }
41
+
42
+ if (dto.displayName !== undefined) {
43
+ if (typeof dto.displayName !== 'string') {
44
+ throw createAuthError(AuthErrorCode.PROFILE_UPDATE_FAILED, 'Display name must be a string')
45
+ }
46
+
47
+ if (dto.displayName.trim().length === 0) {
48
+ throw createAuthError(AuthErrorCode.PROFILE_UPDATE_FAILED, 'Display name cannot be empty')
49
+ }
50
+
51
+ if (dto.displayName.length > 100) {
52
+ throw createAuthError(AuthErrorCode.PROFILE_UPDATE_FAILED, 'Display name too long (max 100 characters)')
53
+ }
54
+ }
55
+
56
+ if (dto.photoURL !== undefined && dto.photoURL !== null) {
57
+ if (typeof dto.photoURL !== 'string') {
58
+ throw createAuthError(AuthErrorCode.PROFILE_UPDATE_FAILED, 'Photo URL must be a string')
59
+ }
60
+
61
+ // Basic URL validation
62
+ if (dto.photoURL && !this.isValidURL(dto.photoURL)) {
63
+ throw createAuthError(AuthErrorCode.PROFILE_UPDATE_FAILED, 'Invalid photo URL')
64
+ }
65
+ }
66
+ }
67
+
68
+ /**
69
+ * Validate URL format
70
+ */
71
+ private isValidURL(url: string): boolean {
72
+ try {
73
+ new URL(url)
74
+ return true
75
+ } catch {
76
+ return false
77
+ }
78
+ }
79
+
80
+ /**
81
+ * Handle errors
82
+ */
83
+ private handleError(error: unknown): Error {
84
+ if (error instanceof Error && 'code' in error) {
85
+ const code = (error as { code: string }).code
86
+
87
+ if (code === 'auth/requires-recent-login') {
88
+ return createAuthError(AuthErrorCode.REAUTHENTICATION_REQUIRED, 'Please reauthenticate first', error)
89
+ }
90
+
91
+ if (code === 'auth/invalid-photo-url') {
92
+ return createAuthError(AuthErrorCode.PROFILE_UPDATE_FAILED, 'Invalid photo URL', error)
93
+ }
94
+ }
95
+
96
+ return createAuthError(AuthErrorCode.PROFILE_UPDATE_FAILED, 'Profile update failed', error)
97
+ }
98
+ }
@@ -0,0 +1,151 @@
1
+ /**
2
+ * File Domain Entities
3
+ * @description File and storage-related entities
4
+ */
5
+
6
+ /**
7
+ * File Metadata Entity
8
+ * Contains metadata about uploaded files
9
+ */
10
+ export interface FileMetadata {
11
+ readonly id: string
12
+ readonly name: string
13
+ readonly fullPath: string
14
+ readonly contentType: string
15
+ readonly size: number
16
+ readonly createdAt: number
17
+ readonly updatedAt: number
18
+ readonly userId: string
19
+ readonly type: FileType
20
+ category?: FileCategory
21
+ description?: string
22
+ tags?: string[]
23
+ }
24
+
25
+ /**
26
+ * File Types
27
+ */
28
+ export type FileType = 'image' | 'video' | 'audio' | 'document' | 'other'
29
+
30
+ /**
31
+ * File Categories
32
+ */
33
+ export type FileCategory = 'profile' | 'content' | 'document' | 'attachment' | 'backup'
34
+
35
+ /**
36
+ * Upload Progress Entity
37
+ * Tracks upload progress for resumable uploads
38
+ */
39
+ export interface UploadProgress {
40
+ bytesTransferred: number
41
+ totalBytes: number
42
+ progress: number // 0-100
43
+ state: UploadState
44
+ speed?: number // bytes per second
45
+ remaining?: number // seconds remaining
46
+ }
47
+
48
+ /**
49
+ * Upload States
50
+ */
51
+ export type UploadState =
52
+ | 'paused'
53
+ | 'running'
54
+ | 'success'
55
+ | 'canceled'
56
+ | 'error'
57
+
58
+ /**
59
+ * File Upload Result Entity
60
+ * Result of a file upload operation
61
+ */
62
+ export interface FileUploadResult {
63
+ readonly id: string
64
+ readonly name: string
65
+ readonly fullPath: string
66
+ readonly downloadURL: string
67
+ readonly contentType: string
68
+ readonly size: number
69
+ readonly createdAt: number
70
+ }
71
+
72
+ // Export as UploadResult for backward compatibility
73
+ export type UploadResult = FileUploadResult
74
+
75
+ /**
76
+ * Upload Options
77
+ * Configuration for file uploads
78
+ */
79
+ export interface UploadOptions {
80
+ onProgress?: (progress: UploadProgress) => void
81
+ metadata?: FileMetadata
82
+ customMetadata?: Record<string, string>
83
+ }
84
+
85
+ /**
86
+ * File Validation Options
87
+ */
88
+ export interface FileValidationOptions {
89
+ maxSizeBytes?: number
90
+ maxSizeMB?: number
91
+ allowedTypes?: string[]
92
+ allowedExtensions?: string[]
93
+ }
94
+
95
+ /**
96
+ * Validation Result
97
+ */
98
+ export interface ValidationResult {
99
+ valid: boolean
100
+ error?: string
101
+ errorCode?: FileErrorCode
102
+ }
103
+
104
+ /**
105
+ * File Error Codes
106
+ */
107
+ export enum FileErrorCode {
108
+ FILE_TOO_LARGE = 'FILE_TOO_LARGE',
109
+ INVALID_TYPE = 'INVALID_TYPE',
110
+ INVALID_EXTENSION = 'INVALID_EXTENSION',
111
+ UPLOAD_FAILED = 'UPLOAD_FAILED',
112
+ DOWNLOAD_FAILED = 'DOWNLOAD_FAILED',
113
+ DELETE_FAILED = 'DELETE_FAILED',
114
+ FILE_NOT_FOUND = 'FILE_NOT_FOUND',
115
+ NETWORK_ERROR = 'NETWORK_ERROR',
116
+ CANCELLED = 'CANCELLED',
117
+ }
118
+
119
+ /**
120
+ * File Filters
121
+ */
122
+ export interface FileFilters {
123
+ type?: FileType
124
+ category?: FileCategory
125
+ startDate?: number
126
+ endDate?: number
127
+ minSize?: number
128
+ maxSize?: number
129
+ tags?: string[]
130
+ }
131
+
132
+ /**
133
+ * File Query Result
134
+ */
135
+ export interface FileQueryResult {
136
+ files: FileMetadata[]
137
+ totalCount: number
138
+ hasMore: boolean
139
+ lastDocId?: string
140
+ }
141
+
142
+ /**
143
+ * Storage Statistics
144
+ */
145
+ export interface StorageStats {
146
+ totalFiles: number
147
+ totalSize: number
148
+ filesByType: Record<FileType, number>
149
+ filesByCategory: Record<FileCategory, number>
150
+ lastUploadAt?: number
151
+ }
@@ -0,0 +1,116 @@
1
+ /**
2
+ * Timestamp Value Objects
3
+ * @description Value objects for handling timestamps in Firestore
4
+ */
5
+
6
+ import type { Timestamp as FirestoreTimestamp } from 'firebase/firestore'
7
+
8
+ /**
9
+ * Firebase Timestamp wrapper
10
+ * Provides type safety and utility methods for timestamp handling
11
+ */
12
+ export class Timestamp {
13
+ constructor(
14
+ public readonly seconds: number,
15
+ public readonly nanoseconds: number
16
+ ) {}
17
+
18
+ /**
19
+ * Create Timestamp from JavaScript Date
20
+ */
21
+ static fromDate(date: Date): Timestamp {
22
+ const milliseconds = date.getTime()
23
+ const seconds = Math.floor(milliseconds / 1000)
24
+ const nanoseconds = (milliseconds % 1000) * 1_000_000
25
+ return new Timestamp(seconds, nanoseconds)
26
+ }
27
+
28
+ /**
29
+ * Create Timestamp from milliseconds
30
+ */
31
+ static fromMillis(milliseconds: number): Timestamp {
32
+ const seconds = Math.floor(milliseconds / 1000)
33
+ const nanoseconds = (milliseconds % 1000) * 1_000_000
34
+ return new Timestamp(seconds, nanoseconds)
35
+ }
36
+
37
+ /**
38
+ * Create Timestamp from Firestore Timestamp
39
+ */
40
+ static fromFirestoreTimestamp(timestamp: FirestoreTimestamp): Timestamp {
41
+ return new Timestamp(timestamp.seconds, timestamp.nanoseconds)
42
+ }
43
+
44
+ /**
45
+ * Get current timestamp
46
+ */
47
+ static now(): Timestamp {
48
+ return Timestamp.fromDate(new Date())
49
+ }
50
+
51
+ /**
52
+ * Convert to JavaScript Date
53
+ */
54
+ toDate(): Date {
55
+ return new Date(this.seconds * 1000 + this.nanoseconds / 1_000_000)
56
+ }
57
+
58
+ /**
59
+ * Convert to milliseconds
60
+ */
61
+ toMillis(): number {
62
+ return this.seconds * 1000 + this.nanoseconds / 1_000_000
63
+ }
64
+
65
+ /**
66
+ * Convert to Firestore Timestamp
67
+ */
68
+ toFirestoreTimestamp(): FirestoreTimestamp {
69
+ return {
70
+ seconds: this.seconds,
71
+ nanoseconds: this.nanoseconds,
72
+ } as FirestoreTimestamp
73
+ }
74
+
75
+ /**
76
+ * Convert to ISO string
77
+ */
78
+ toISOString(): string {
79
+ return this.toDate().toISOString()
80
+ }
81
+
82
+ /**
83
+ * Check if timestamp is in the past
84
+ */
85
+ isPast(): boolean {
86
+ return this.toMillis() < Date.now()
87
+ }
88
+
89
+ /**
90
+ * Check if timestamp is in the future
91
+ */
92
+ isFuture(): boolean {
93
+ return this.toMillis() > Date.now()
94
+ }
95
+
96
+ /**
97
+ * Get difference in milliseconds with another timestamp
98
+ */
99
+ diff(other: Timestamp): number {
100
+ return this.toMillis() - other.toMillis()
101
+ }
102
+ }
103
+
104
+ /**
105
+ * Server timestamp sentinel for Firestore
106
+ */
107
+ export const serverTimestamp = {
108
+ __type__: 'serverTimestamp',
109
+ } as const
110
+
111
+ export type ServerTimestamp = typeof serverTimestamp
112
+
113
+ /**
114
+ * Timestamp field that can be a Timestamp, server timestamp, or ISO string
115
+ */
116
+ export type TimestampField = Timestamp | ServerTimestamp | string