@umituz/react-native-firebase 2.6.0 → 2.6.2

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 (102) hide show
  1. package/package.json +1 -1
  2. package/src/application/auth/index.ts +42 -0
  3. package/src/application/auth/ports/AuthPort.ts.bak +164 -0
  4. package/src/application/auth/ports/AuthPort_part_aa +150 -0
  5. package/src/application/auth/ports/AuthPort_part_ab +14 -0
  6. package/src/application/auth/use-cases/SignInUseCase.ts.bak +253 -0
  7. package/src/application/auth/use-cases/SignInUseCaseHelpers.ts +0 -0
  8. package/src/application/auth/use-cases/SignInUseCaseMain.ts +0 -0
  9. package/src/application/auth/use-cases/SignInUseCase_part_aa +150 -0
  10. package/src/application/auth/use-cases/SignInUseCase_part_ab +103 -0
  11. package/src/application/auth/use-cases/SignOutUseCase.ts.bak +288 -0
  12. package/src/application/auth/use-cases/SignOutUseCaseCleanup.ts +0 -0
  13. package/src/application/auth/use-cases/SignOutUseCaseMain.ts +0 -0
  14. package/src/application/auth/use-cases/SignOutUseCase_part_aa +150 -0
  15. package/src/application/auth/use-cases/SignOutUseCase_part_ab +138 -0
  16. package/src/application/auth/use-cases/index.ts +26 -0
  17. package/src/domains/account-deletion/domain/index.ts +15 -0
  18. package/src/domains/account-deletion/domain/services/UserValidationHelpers.ts.bak +181 -0
  19. package/src/domains/account-deletion/domain/services/UserValidationHelpers_part_aa +150 -0
  20. package/src/domains/account-deletion/domain/services/UserValidationHelpers_part_ab +31 -0
  21. package/src/domains/account-deletion/domain/services/UserValidationService.ts.bak +286 -0
  22. package/src/domains/account-deletion/domain/services/UserValidationService_part_aa +150 -0
  23. package/src/domains/account-deletion/domain/services/UserValidationService_part_ab +136 -0
  24. package/src/domains/account-deletion/index.ts +43 -6
  25. package/src/domains/account-deletion/infrastructure/services/AccountDeletionExecutor.ts.bak +230 -0
  26. package/src/domains/account-deletion/infrastructure/services/AccountDeletionExecutor_part_aa +150 -0
  27. package/src/domains/account-deletion/infrastructure/services/AccountDeletionExecutor_part_ab +80 -0
  28. package/src/domains/account-deletion/infrastructure/services/AccountDeletionReauthHandler.ts.bak +174 -0
  29. package/src/domains/account-deletion/infrastructure/services/AccountDeletionReauthHandler_part_aa +150 -0
  30. package/src/domains/account-deletion/infrastructure/services/AccountDeletionReauthHandler_part_ab +24 -0
  31. package/src/domains/account-deletion/infrastructure/services/AccountDeletionRepository.ts.bak +266 -0
  32. package/src/domains/account-deletion/infrastructure/services/AccountDeletionRepository_part_aa +150 -0
  33. package/src/domains/account-deletion/infrastructure/services/AccountDeletionRepository_part_ab +116 -0
  34. package/src/domains/account-deletion/infrastructure/services/AccountDeletionTypes.ts +33 -0
  35. package/src/domains/account-deletion/infrastructure/services/account-deletion.service.ts +39 -227
  36. package/src/domains/account-deletion/infrastructure/services/reauthentication.service_part_aa +150 -0
  37. package/src/domains/account-deletion/infrastructure/services/reauthentication.service_part_ab +10 -0
  38. package/src/domains/auth/domain.ts +16 -0
  39. package/src/domains/auth/index.ts +7 -148
  40. package/src/domains/auth/infrastructure.ts.bak +156 -0
  41. package/src/domains/auth/infrastructure_part_aa +150 -0
  42. package/src/domains/auth/infrastructure_part_ab +6 -0
  43. package/src/domains/auth/presentation/hooks/GoogleOAuthHelpers.ts +0 -0
  44. package/src/domains/auth/presentation/hooks/GoogleOAuthHookService.ts.bak +247 -0
  45. package/src/domains/auth/presentation/hooks/GoogleOAuthHookService_part_aa +150 -0
  46. package/src/domains/auth/presentation/hooks/GoogleOAuthHookService_part_ab +97 -0
  47. package/src/domains/auth/presentation/hooks/GoogleOAuthService.ts +0 -0
  48. package/src/domains/auth/presentation/hooks/useGoogleOAuth.ts +49 -103
  49. package/src/domains/auth/presentation.ts +25 -0
  50. package/src/domains/firestore/domain/entities/Collection.ts +128 -0
  51. package/src/domains/firestore/domain/entities/Collection.ts.bak +288 -0
  52. package/src/domains/firestore/domain/entities/CollectionFactory.ts +55 -0
  53. package/src/domains/firestore/domain/entities/CollectionHelpers.ts +143 -0
  54. package/src/domains/firestore/domain/entities/CollectionUtils.ts +72 -0
  55. package/src/domains/firestore/domain/entities/CollectionValidation.ts +138 -0
  56. package/src/domains/firestore/domain/entities/Collection_part_aa +150 -0
  57. package/src/domains/firestore/domain/entities/Collection_part_ab +138 -0
  58. package/src/domains/firestore/domain/entities/Document.ts.bak +233 -0
  59. package/src/domains/firestore/domain/entities/DocumentHelpers.ts +0 -0
  60. package/src/domains/firestore/domain/entities/DocumentMain.ts +0 -0
  61. package/src/domains/firestore/domain/entities/Document_part_aa +150 -0
  62. package/src/domains/firestore/domain/entities/Document_part_ab +83 -0
  63. package/src/domains/firestore/domain/index.ts +65 -0
  64. package/src/domains/firestore/domain/services/QueryService.ts.bak +182 -0
  65. package/src/domains/firestore/domain/services/QueryServiceAnalysis.ts.bak +169 -0
  66. package/src/domains/firestore/domain/services/QueryServiceAnalysis_part_aa +150 -0
  67. package/src/domains/firestore/domain/services/QueryServiceAnalysis_part_ab +19 -0
  68. package/src/domains/firestore/domain/services/QueryServiceHelpers.ts.bak +151 -0
  69. package/src/domains/firestore/domain/services/QueryServiceHelpers_part_aa +150 -0
  70. package/src/domains/firestore/domain/services/QueryServiceHelpers_part_ab +1 -0
  71. package/src/domains/firestore/domain/services/QueryService_part_aa +150 -0
  72. package/src/domains/firestore/domain/services/QueryService_part_ab +32 -0
  73. package/src/domains/firestore/domain/value-objects/QueryOptions.ts.bak +191 -0
  74. package/src/domains/firestore/domain/value-objects/QueryOptionsSerialization.ts.bak +207 -0
  75. package/src/domains/firestore/domain/value-objects/QueryOptionsSerialization_part_aa +150 -0
  76. package/src/domains/firestore/domain/value-objects/QueryOptionsSerialization_part_ab +57 -0
  77. package/src/domains/firestore/domain/value-objects/QueryOptionsValidation.ts.bak +182 -0
  78. package/src/domains/firestore/domain/value-objects/QueryOptionsValidation_part_aa +150 -0
  79. package/src/domains/firestore/domain/value-objects/QueryOptionsValidation_part_ab +32 -0
  80. package/src/domains/firestore/domain/value-objects/QueryOptions_part_aa +150 -0
  81. package/src/domains/firestore/domain/value-objects/QueryOptions_part_ab +41 -0
  82. package/src/domains/firestore/domain/value-objects/WhereClause.ts.bak +299 -0
  83. package/src/domains/firestore/domain/value-objects/WhereClauseFactory.ts.bak +207 -0
  84. package/src/domains/firestore/domain/value-objects/WhereClauseFactory_part_aa +150 -0
  85. package/src/domains/firestore/domain/value-objects/WhereClauseFactory_part_ab +57 -0
  86. package/src/domains/firestore/domain/value-objects/WhereClause_part_aa +150 -0
  87. package/src/domains/firestore/domain/value-objects/WhereClause_part_ab +149 -0
  88. package/src/domains/firestore/index.ts +9 -6
  89. package/src/index.ts +25 -0
  90. package/src/shared/domain/utils/error-handlers/error-messages.ts +11 -0
  91. package/src/shared/infrastructure/base/ErrorHandler.ts.bak +189 -0
  92. package/src/shared/infrastructure/base/ErrorHandler_part_aa +150 -0
  93. package/src/shared/infrastructure/base/ErrorHandler_part_ab +39 -0
  94. package/src/shared/infrastructure/base/ServiceBase.ts.bak +220 -0
  95. package/src/shared/infrastructure/base/ServiceBase_part_aa +150 -0
  96. package/src/shared/infrastructure/base/ServiceBase_part_ab +70 -0
  97. package/src/shared/infrastructure/base/TypedGuard.ts +131 -0
  98. package/src/shared/infrastructure/base/index.ts +34 -0
  99. package/src/shared/infrastructure/config/base/ServiceClientSingleton_part_aa +150 -0
  100. package/src/shared/infrastructure/config/base/ServiceClientSingleton_part_ab +5 -0
  101. /package/src/domains/account-deletion/infrastructure/services/{reauthentication.service.ts → reauthentication.service.ts.bak} +0 -0
  102. /package/src/shared/infrastructure/config/base/{ServiceClientSingleton.ts → ServiceClientSingleton.ts.bak} +0 -0
@@ -0,0 +1,150 @@
1
+ /**
2
+ * Collection Entity
3
+ * Single Responsibility: Represent a Firestore collection with metadata
4
+ *
5
+ * Domain entity that encapsulates collection information and metadata.
6
+ * Provides business logic for collection operations.
7
+ *
8
+ * Max lines: 150 (enforced for maintainability)
9
+ */
10
+
11
+ import type { CollectionReference, Query } from 'firebase/firestore';
12
+ import type { Document } from './Document';
13
+
14
+ /**
15
+ * Collection metadata
16
+ */
17
+ export interface CollectionMetadata {
18
+ readonly name: string;
19
+ readonly path: string;
20
+ readonly parentPath?: string;
21
+ }
22
+
23
+ /**
24
+ * Collection entity
25
+ * Represents a Firestore collection with metadata
26
+ */
27
+ export class Collection<TDocument = unknown> {
28
+ readonly name: string;
29
+ readonly path: string;
30
+ readonly parentPath: string | undefined;
31
+ private readonly reference: CollectionReference<TDocument> | Query<TDocument>;
32
+
33
+ constructor(
34
+ reference: CollectionReference<TDocument> | Query<TDocument>,
35
+ metadata: CollectionMetadata
36
+ ) {
37
+ this.reference = reference;
38
+ this.name = metadata.name;
39
+ this.path = metadata.path;
40
+ this.parentPath = metadata.parentPath || undefined;
41
+ }
42
+
43
+ /**
44
+ * Create collection from collection reference
45
+ */
46
+ static fromReference<TDocument = unknown>(
47
+ reference: CollectionReference<TDocument>
48
+ ): Collection<TDocument> {
49
+ return new Collection(reference, {
50
+ name: reference.id,
51
+ path: reference.path,
52
+ parentPath: reference.parent?.path || null,
53
+ });
54
+ }
55
+
56
+ /**
57
+ * Create collection from query
58
+ */
59
+ static fromQuery<TDocument = unknown>(query: Query<TDocument>, name: string, path: string): Collection<TDocument> {
60
+ return new Collection(query, {
61
+ name,
62
+ path,
63
+ parentPath: path.split('/').slice(0, -2).join('/') || null,
64
+ });
65
+ }
66
+
67
+ /**
68
+ * Get collection name
69
+ */
70
+ getName(): string {
71
+ return this.name;
72
+ }
73
+
74
+ /**
75
+ * Get collection path
76
+ */
77
+ getPath(): string {
78
+ return this.path;
79
+ }
80
+
81
+ /**
82
+ * Get parent path if exists
83
+ */
84
+ getParentPath(): string | null {
85
+ return this.parentPath;
86
+ }
87
+
88
+ /**
89
+ * Check if collection is nested (has parent)
90
+ */
91
+ isNested(): boolean {
92
+ return this.parentPath !== null;
93
+ }
94
+
95
+ /**
96
+ * Check if collection is root level (no parent)
97
+ */
98
+ isRootLevel(): boolean {
99
+ return this.parentPath === null;
100
+ }
101
+
102
+ /**
103
+ * Get collection depth in hierarchy
104
+ * Root collections have depth 0
105
+ */
106
+ getDepth(): number {
107
+ if (!this.parentPath) return 0;
108
+ return this.path.split('/').length / 2 - 1;
109
+ }
110
+
111
+ /**
112
+ * Get the underlying reference
113
+ */
114
+ getReference(): CollectionReference<TDocument> | Query<TDocument> {
115
+ return this.reference;
116
+ }
117
+
118
+ /**
119
+ * Check if collection is a query (has filters/limits)
120
+ */
121
+ isQuery(): boolean {
122
+ return 'type' in this.reference && this.reference.type === 'query';
123
+ }
124
+
125
+ /**
126
+ * Check if collection is a collection reference (no filters)
127
+ */
128
+ isCollectionReference(): boolean {
129
+ return !this.isQuery();
130
+ }
131
+
132
+ /**
133
+ * Validate collection name format
134
+ * Collection names must match Firestore requirements
135
+ */
136
+ static isValidName(name: string): boolean {
137
+ // Collection names must be non-empty strings
138
+ if (!name || typeof name !== 'string' || name.trim() === '') {
139
+ return false;
140
+ }
141
+
142
+ // Cannot contain special characters
143
+ const invalidChars = /[\/\\.\s]/;
144
+ if (invalidChars.test(name)) {
145
+ return false;
146
+ }
147
+
148
+ // Cannot start or end with double underscore
149
+ if (name.startsWith('__') || name.endsWith('__')) {
150
+ return false;
@@ -0,0 +1,138 @@
1
+ }
2
+
3
+ // Reasonable length limit
4
+ if (name.length > 100) {
5
+ return false;
6
+ }
7
+
8
+ return true;
9
+ }
10
+
11
+ /**
12
+ * Validate collection path format
13
+ * Paths must follow Firestore path structure
14
+ */
15
+ static isValidPath(path: string): boolean {
16
+ if (!path || typeof path !== 'string' || path.trim() === '') {
17
+ return false;
18
+ }
19
+
20
+ const segments = path.split('/');
21
+ if (segments.length < 2 || segments.length % 2 !== 0) {
22
+ return false;
23
+ }
24
+
25
+ return segments.every(segment => this.isValidName(segment));
26
+ }
27
+
28
+ /**
29
+ * Extract collection name from path
30
+ */
31
+ static extractNameFromPath(path: string): string | null {
32
+ if (!this.isValidPath(path)) {
33
+ return null;
34
+ }
35
+
36
+ const segments = path.split('/');
37
+ return segments[segments.length - 1] || null;
38
+ }
39
+
40
+ /**
41
+ * Extract parent path from collection path
42
+ */
43
+ static extractParentPath(path: string): string | null {
44
+ if (!this.isValidPath(path)) {
45
+ return null;
46
+ }
47
+
48
+ const segments = path.split('/');
49
+ if (segments.length <= 2) {
50
+ return null;
51
+ }
52
+
53
+ return segments.slice(0, -1).join('/');
54
+ }
55
+
56
+ /**
57
+ * Convert to plain object (for serialization)
58
+ */
59
+ toObject(): CollectionMetadata {
60
+ return {
61
+ name: this.name,
62
+ path: this.path,
63
+ parentPath: this.parentPath || undefined,
64
+ };
65
+ }
66
+
67
+ /**
68
+ * Create a sub-collection path
69
+ */
70
+ createSubCollectionPath(subCollectionName: string): string | null {
71
+ if (!Collection.isValidName(subCollectionName)) {
72
+ return null;
73
+ }
74
+
75
+ return `${this.path}/${subCollectionName}`;
76
+ }
77
+
78
+ /**
79
+ * Check if collection is a sub-collection of another
80
+ */
81
+ isSubCollectionOf(other: Collection): boolean {
82
+ return this.parentPath === other.path;
83
+ }
84
+
85
+ /**
86
+ * Check if collection is parent of another
87
+ */
88
+ isParentOf(other: Collection): boolean {
89
+ return other.isSubCollectionOf(this);
90
+ }
91
+
92
+ /**
93
+ * Get collection ID (similar to name but more explicit)
94
+ */
95
+ getId(): string {
96
+ return this.name;
97
+ }
98
+
99
+ /**
100
+ * Check if this is a user collection (users/{userId}/{collection})
101
+ */
102
+ isUserCollection(): boolean {
103
+ return this.parentPath?.startsWith('users/') || false;
104
+ }
105
+
106
+ /**
107
+ * Extract user ID from user collection path
108
+ * Returns null if not a user collection
109
+ */
110
+ extractUserId(): string | null {
111
+ if (!this.isUserCollection()) return null;
112
+
113
+ const segments = this.path.split('/');
114
+ if (segments.length >= 3 && segments[0] === 'users') {
115
+ return segments[1];
116
+ }
117
+
118
+ return null;
119
+ }
120
+ }
121
+
122
+ /**
123
+ * Factory function to create collection entity
124
+ */
125
+ export function createCollection<TDocument = unknown>(
126
+ reference: CollectionReference<TDocument> | Query<TDocument>,
127
+ name?: string,
128
+ path?: string
129
+ ): Collection<TDocument> {
130
+ if ('type' in reference && reference.type === 'query') {
131
+ if (!name || !path) {
132
+ throw new Error('name and path are required for query collections');
133
+ }
134
+ return Collection.fromQuery(reference, name, path);
135
+ }
136
+
137
+ return Collection.fromReference(reference as CollectionReference<TDocument>);
138
+ }
@@ -0,0 +1,233 @@
1
+ /**
2
+ * Document Entity
3
+ * Single Responsibility: Represent a Firestore document with metadata
4
+ *
5
+ * Domain entity that encapsulates document data and metadata.
6
+ * Provides business logic for document operations.
7
+ *
8
+ * Max lines: 150 (enforced for maintainability)
9
+ */
10
+
11
+ import type { DocumentData, DocumentSnapshot, Timestamp } from 'firebase/firestore';
12
+
13
+ /**
14
+ * Document metadata
15
+ */
16
+ export interface DocumentMetadata {
17
+ readonly id: string;
18
+ readonly createdAt: Timestamp | null;
19
+ readonly updatedAt: Timestamp | null;
20
+ readonly exists: boolean;
21
+ }
22
+
23
+ /**
24
+ * Document entity
25
+ * Represents a Firestore document with data and metadata
26
+ */
27
+ export class Document<T extends DocumentData = DocumentData> {
28
+ readonly id: string;
29
+ readonly data: T | null;
30
+ readonly metadata: DocumentMetadata;
31
+ readonly path: string;
32
+
33
+ constructor(snapshot: DocumentSnapshot<T>, path: string) {
34
+ this.id = snapshot.id;
35
+ this.data = snapshot.data() || null;
36
+ this.path = path;
37
+ this.metadata = {
38
+ id: snapshot.id,
39
+ createdAt: this.extractTimestamp(snapshot, 'createdAt'),
40
+ updatedAt: this.extractTimestamp(snapshot, 'updatedAt'),
41
+ exists: snapshot.exists(),
42
+ };
43
+ }
44
+
45
+ /**
46
+ * Create document from snapshot with custom path
47
+ */
48
+ static fromSnapshot<T extends DocumentData>(
49
+ snapshot: DocumentSnapshot<T>,
50
+ path: string
51
+ ): Document<T> {
52
+ return new Document(snapshot, path);
53
+ }
54
+
55
+ /**
56
+ * Check if document exists
57
+ */
58
+ exists(): boolean {
59
+ return this.metadata.exists && this.data !== null;
60
+ }
61
+
62
+ /**
63
+ * Get field value from document data
64
+ */
65
+ getField<K extends keyof T>(field: K): T[K] | null {
66
+ return this.data?.[field] ?? null;
67
+ }
68
+
69
+ /**
70
+ * Check if document has a specific field
71
+ */
72
+ hasField<K extends keyof T>(field: K): boolean {
73
+ return this.data !== null && field in this.data;
74
+ }
75
+
76
+ /**
77
+ * Get document age in milliseconds
78
+ * Returns null if createdAt is not set
79
+ */
80
+ getAge(): number | null {
81
+ if (!this.metadata.createdAt) return null;
82
+ return Date.now() - this.metadata.createdAt.toMillis();
83
+ }
84
+
85
+ /**
86
+ * Check if document is recent (created within specified milliseconds)
87
+ */
88
+ isRecent(maxAgeMs: number): boolean {
89
+ const age = this.getAge();
90
+ return age !== null && age <= maxAgeMs;
91
+ }
92
+
93
+ /**
94
+ * Check if document was updated after creation (modified)
95
+ */
96
+ isModified(): boolean {
97
+ if (!this.metadata.createdAt || !this.metadata.updatedAt) return false;
98
+ return this.metadata.updatedAt.toMillis() > this.metadata.createdAt.toMillis();
99
+ }
100
+
101
+ /**
102
+ * Get time since last update in milliseconds
103
+ * Returns null if updatedAt is not set
104
+ */
105
+ getTimeSinceUpdate(): number | null {
106
+ if (!this.metadata.updatedAt) return null;
107
+ return Date.now() - this.metadata.updatedAt.toMillis();
108
+ }
109
+
110
+ /**
111
+ * Check if document is stale (not updated within specified milliseconds)
112
+ */
113
+ isStale(maxStaleMs: number): boolean {
114
+ const timeSinceUpdate = this.getTimeSinceUpdate();
115
+ return timeSinceUpdate !== null && timeSinceUpdate > maxStaleMs;
116
+ }
117
+
118
+ /**
119
+ * Convert to plain object (for serialization)
120
+ */
121
+ toObject(): { id: string; data: T | null; metadata: DocumentMetadata } {
122
+ return {
123
+ id: this.id,
124
+ data: this.data,
125
+ metadata: this.metadata,
126
+ };
127
+ }
128
+
129
+ /**
130
+ * Extract timestamp field from document snapshot
131
+ */
132
+ private extractTimestamp(
133
+ snapshot: DocumentSnapshot<T>,
134
+ field: string
135
+ ): Timestamp | null {
136
+ const data = snapshot.data();
137
+ if (!data) return null;
138
+
139
+ const value = data[field as keyof T];
140
+ if (value && typeof value === 'object' && 'toDate' in value && typeof value.toDate === 'function') {
141
+ return value as unknown as Timestamp;
142
+ }
143
+
144
+ return null;
145
+ }
146
+
147
+ /**
148
+ * Create a new document with updated data
149
+ * Useful for immutability patterns
150
+ */
151
+ withData<K extends keyof T>(field: K, value: T[K]): Document<T> {
152
+ if (!this.data) {
153
+ throw new Error('Cannot update null document data');
154
+ }
155
+
156
+ const newData = { ...this.data, [field]: value };
157
+ return new Document(
158
+ {
159
+ id: this.id,
160
+ exists: true,
161
+ data() {
162
+ return newData;
163
+ },
164
+ } as unknown as DocumentSnapshot<T>,
165
+ this.path
166
+ );
167
+ }
168
+
169
+ /**
170
+ * Validate document data against required fields
171
+ */
172
+ validateRequiredFields(requiredFields: (keyof T)[]): { valid: boolean; missing: (keyof T)[] } {
173
+ if (!this.data) {
174
+ return { valid: false, missing: requiredFields };
175
+ }
176
+
177
+ const missing = requiredFields.filter(field => !this.hasField(field));
178
+ return {
179
+ valid: missing.length === 0,
180
+ missing,
181
+ };
182
+ }
183
+
184
+ /**
185
+ * Check if document matches filter criteria
186
+ */
187
+ matches(filter: Partial<T>): boolean {
188
+ if (!this.data) return false;
189
+
190
+ return Object.entries(filter).every(([key, value]) => {
191
+ const fieldValue = this.data![key as keyof T];
192
+ return fieldValue === value;
193
+ });
194
+ }
195
+
196
+ /**
197
+ * Check if document matches filter criteria
198
+ */
199
+ matches(filter: Partial<T>): boolean {
200
+ if (!this.data) return false;
201
+
202
+ return Object.entries(filter).every(([key, value]) => {
203
+ return this.data[key as keyof T] === value;
204
+ });
205
+ }
206
+
207
+ /**
208
+ * Get document size in bytes (approximate)
209
+ * Useful for quota management
210
+ */
211
+ getSize(): number {
212
+ if (!this.data) return 0;
213
+
214
+ return JSON.stringify(this.data).length;
215
+ }
216
+
217
+ /**
218
+ * Check if document size exceeds limit
219
+ */
220
+ exceedsSizeLimit(maxBytes: number): boolean {
221
+ return this.getSize() > maxBytes;
222
+ }
223
+ }
224
+
225
+ /**
226
+ * Factory function to create document entity
227
+ */
228
+ export function createDocument<T extends DocumentData>(
229
+ snapshot: DocumentSnapshot<T>,
230
+ path: string
231
+ ): Document<T> {
232
+ return Document.fromSnapshot(snapshot, path);
233
+ }
@@ -0,0 +1,150 @@
1
+ /**
2
+ * Document Entity
3
+ * Single Responsibility: Represent a Firestore document with metadata
4
+ *
5
+ * Domain entity that encapsulates document data and metadata.
6
+ * Provides business logic for document operations.
7
+ *
8
+ * Max lines: 150 (enforced for maintainability)
9
+ */
10
+
11
+ import type { DocumentData, DocumentSnapshot, Timestamp } from 'firebase/firestore';
12
+
13
+ /**
14
+ * Document metadata
15
+ */
16
+ export interface DocumentMetadata {
17
+ readonly id: string;
18
+ readonly createdAt: Timestamp | null;
19
+ readonly updatedAt: Timestamp | null;
20
+ readonly exists: boolean;
21
+ }
22
+
23
+ /**
24
+ * Document entity
25
+ * Represents a Firestore document with data and metadata
26
+ */
27
+ export class Document<T extends DocumentData = DocumentData> {
28
+ readonly id: string;
29
+ readonly data: T | null;
30
+ readonly metadata: DocumentMetadata;
31
+ readonly path: string;
32
+
33
+ constructor(snapshot: DocumentSnapshot<T>, path: string) {
34
+ this.id = snapshot.id;
35
+ this.data = snapshot.data() || null;
36
+ this.path = path;
37
+ this.metadata = {
38
+ id: snapshot.id,
39
+ createdAt: this.extractTimestamp(snapshot, 'createdAt'),
40
+ updatedAt: this.extractTimestamp(snapshot, 'updatedAt'),
41
+ exists: snapshot.exists(),
42
+ };
43
+ }
44
+
45
+ /**
46
+ * Create document from snapshot with custom path
47
+ */
48
+ static fromSnapshot<T extends DocumentData>(
49
+ snapshot: DocumentSnapshot<T>,
50
+ path: string
51
+ ): Document<T> {
52
+ return new Document(snapshot, path);
53
+ }
54
+
55
+ /**
56
+ * Check if document exists
57
+ */
58
+ exists(): boolean {
59
+ return this.metadata.exists && this.data !== null;
60
+ }
61
+
62
+ /**
63
+ * Get field value from document data
64
+ */
65
+ getField<K extends keyof T>(field: K): T[K] | null {
66
+ return this.data?.[field] ?? null;
67
+ }
68
+
69
+ /**
70
+ * Check if document has a specific field
71
+ */
72
+ hasField<K extends keyof T>(field: K): boolean {
73
+ return this.data !== null && field in this.data;
74
+ }
75
+
76
+ /**
77
+ * Get document age in milliseconds
78
+ * Returns null if createdAt is not set
79
+ */
80
+ getAge(): number | null {
81
+ if (!this.metadata.createdAt) return null;
82
+ return Date.now() - this.metadata.createdAt.toMillis();
83
+ }
84
+
85
+ /**
86
+ * Check if document is recent (created within specified milliseconds)
87
+ */
88
+ isRecent(maxAgeMs: number): boolean {
89
+ const age = this.getAge();
90
+ return age !== null && age <= maxAgeMs;
91
+ }
92
+
93
+ /**
94
+ * Check if document was updated after creation (modified)
95
+ */
96
+ isModified(): boolean {
97
+ if (!this.metadata.createdAt || !this.metadata.updatedAt) return false;
98
+ return this.metadata.updatedAt.toMillis() > this.metadata.createdAt.toMillis();
99
+ }
100
+
101
+ /**
102
+ * Get time since last update in milliseconds
103
+ * Returns null if updatedAt is not set
104
+ */
105
+ getTimeSinceUpdate(): number | null {
106
+ if (!this.metadata.updatedAt) return null;
107
+ return Date.now() - this.metadata.updatedAt.toMillis();
108
+ }
109
+
110
+ /**
111
+ * Check if document is stale (not updated within specified milliseconds)
112
+ */
113
+ isStale(maxStaleMs: number): boolean {
114
+ const timeSinceUpdate = this.getTimeSinceUpdate();
115
+ return timeSinceUpdate !== null && timeSinceUpdate > maxStaleMs;
116
+ }
117
+
118
+ /**
119
+ * Convert to plain object (for serialization)
120
+ */
121
+ toObject(): { id: string; data: T | null; metadata: DocumentMetadata } {
122
+ return {
123
+ id: this.id,
124
+ data: this.data,
125
+ metadata: this.metadata,
126
+ };
127
+ }
128
+
129
+ /**
130
+ * Extract timestamp field from document snapshot
131
+ */
132
+ private extractTimestamp(
133
+ snapshot: DocumentSnapshot<T>,
134
+ field: string
135
+ ): Timestamp | null {
136
+ const data = snapshot.data();
137
+ if (!data) return null;
138
+
139
+ const value = data[field as keyof T];
140
+ if (value && typeof value === 'object' && 'toDate' in value && typeof value.toDate === 'function') {
141
+ return value as unknown as Timestamp;
142
+ }
143
+
144
+ return null;
145
+ }
146
+
147
+ /**
148
+ * Create a new document with updated data
149
+ * Useful for immutability patterns
150
+ */