@umituz/react-native-firebase 2.6.1 → 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.
- package/package.json +1 -1
- package/src/application/auth/ports/AuthPort_part_aa +150 -0
- package/src/application/auth/ports/AuthPort_part_ab +14 -0
- package/src/application/auth/use-cases/SignInUseCaseHelpers.ts +0 -0
- package/src/application/auth/use-cases/SignInUseCaseMain.ts +0 -0
- package/src/application/auth/use-cases/SignInUseCase_part_aa +150 -0
- package/src/application/auth/use-cases/SignInUseCase_part_ab +103 -0
- package/src/application/auth/use-cases/SignOutUseCaseCleanup.ts +0 -0
- package/src/application/auth/use-cases/SignOutUseCaseMain.ts +0 -0
- package/src/application/auth/use-cases/SignOutUseCase_part_aa +150 -0
- package/src/application/auth/use-cases/SignOutUseCase_part_ab +138 -0
- package/src/domains/account-deletion/domain/services/UserValidationHelpers.ts.bak +181 -0
- package/src/domains/account-deletion/domain/services/UserValidationHelpers_part_aa +150 -0
- package/src/domains/account-deletion/domain/services/UserValidationHelpers_part_ab +31 -0
- package/src/domains/account-deletion/domain/services/{UserValidationService.ts → UserValidationService.ts.bak} +1 -10
- package/src/domains/account-deletion/domain/services/UserValidationService_part_aa +150 -0
- package/src/domains/account-deletion/domain/services/UserValidationService_part_ab +136 -0
- package/src/domains/account-deletion/infrastructure/services/AccountDeletionExecutor_part_aa +150 -0
- package/src/domains/account-deletion/infrastructure/services/AccountDeletionExecutor_part_ab +80 -0
- package/src/domains/account-deletion/infrastructure/services/AccountDeletionReauthHandler_part_aa +150 -0
- package/src/domains/account-deletion/infrastructure/services/AccountDeletionReauthHandler_part_ab +24 -0
- package/src/domains/account-deletion/infrastructure/services/AccountDeletionRepository_part_aa +150 -0
- package/src/domains/account-deletion/infrastructure/services/AccountDeletionRepository_part_ab +116 -0
- package/src/domains/account-deletion/infrastructure/services/reauthentication.service_part_aa +150 -0
- package/src/domains/account-deletion/infrastructure/services/reauthentication.service_part_ab +10 -0
- package/src/domains/auth/infrastructure_part_aa +150 -0
- package/src/domains/auth/infrastructure_part_ab +6 -0
- package/src/domains/auth/presentation/hooks/GoogleOAuthHelpers.ts +0 -0
- package/src/domains/auth/presentation/hooks/GoogleOAuthHookService_part_aa +150 -0
- package/src/domains/auth/presentation/hooks/GoogleOAuthHookService_part_ab +97 -0
- package/src/domains/auth/presentation/hooks/GoogleOAuthService.ts +0 -0
- package/src/domains/firestore/domain/entities/Collection.ts +31 -191
- package/src/domains/firestore/domain/entities/Collection.ts.bak +288 -0
- package/src/domains/firestore/domain/entities/CollectionFactory.ts +55 -0
- package/src/domains/firestore/domain/entities/CollectionHelpers.ts +143 -0
- package/src/domains/firestore/domain/entities/CollectionUtils.ts +72 -0
- package/src/domains/firestore/domain/entities/CollectionValidation.ts +138 -0
- package/src/domains/firestore/domain/entities/Collection_part_aa +150 -0
- package/src/domains/firestore/domain/entities/Collection_part_ab +138 -0
- package/src/domains/firestore/domain/entities/DocumentHelpers.ts +0 -0
- package/src/domains/firestore/domain/entities/DocumentMain.ts +0 -0
- package/src/domains/firestore/domain/entities/Document_part_aa +150 -0
- package/src/domains/firestore/domain/entities/Document_part_ab +83 -0
- package/src/domains/firestore/domain/index.ts +44 -9
- package/src/domains/firestore/domain/services/QueryServiceAnalysis_part_aa +150 -0
- package/src/domains/firestore/domain/services/QueryServiceAnalysis_part_ab +19 -0
- package/src/domains/firestore/domain/services/QueryServiceHelpers_part_aa +150 -0
- package/src/domains/firestore/domain/services/QueryServiceHelpers_part_ab +1 -0
- package/src/domains/firestore/domain/services/QueryService_part_aa +150 -0
- package/src/domains/firestore/domain/services/QueryService_part_ab +32 -0
- package/src/domains/firestore/domain/value-objects/QueryOptions.ts.bak +6 -135
- package/src/domains/firestore/domain/value-objects/QueryOptionsSerialization_part_aa +150 -0
- package/src/domains/firestore/domain/value-objects/QueryOptionsSerialization_part_ab +57 -0
- package/src/domains/firestore/domain/value-objects/QueryOptionsValidation_part_aa +150 -0
- package/src/domains/firestore/domain/value-objects/QueryOptionsValidation_part_ab +32 -0
- package/src/domains/firestore/domain/value-objects/{QueryOptions.ts → QueryOptions_part_aa} +0 -41
- package/src/domains/firestore/domain/value-objects/QueryOptions_part_ab +41 -0
- package/src/domains/firestore/domain/value-objects/WhereClauseFactory_part_aa +150 -0
- package/src/domains/firestore/domain/value-objects/WhereClauseFactory_part_ab +57 -0
- package/src/domains/firestore/domain/value-objects/WhereClause_part_aa +150 -0
- package/src/domains/firestore/domain/value-objects/WhereClause_part_ab +149 -0
- package/src/shared/infrastructure/base/ErrorHandler_part_aa +150 -0
- package/src/shared/infrastructure/base/ErrorHandler_part_ab +39 -0
- package/src/shared/infrastructure/base/ServiceBase_part_aa +150 -0
- package/src/shared/infrastructure/base/ServiceBase_part_ab +70 -0
- package/src/shared/infrastructure/config/base/ServiceClientSingleton_part_aa +150 -0
- package/src/shared/infrastructure/config/base/ServiceClientSingleton_part_ab +5 -0
- /package/src/application/auth/ports/{AuthPort.ts → AuthPort.ts.bak} +0 -0
- /package/src/application/auth/use-cases/{SignInUseCase.ts → SignInUseCase.ts.bak} +0 -0
- /package/src/application/auth/use-cases/{SignOutUseCase.ts → SignOutUseCase.ts.bak} +0 -0
- /package/src/domains/account-deletion/infrastructure/services/{AccountDeletionExecutor.ts → AccountDeletionExecutor.ts.bak} +0 -0
- /package/src/domains/account-deletion/infrastructure/services/{AccountDeletionReauthHandler.ts → AccountDeletionReauthHandler.ts.bak} +0 -0
- /package/src/domains/account-deletion/infrastructure/services/{AccountDeletionRepository.ts → AccountDeletionRepository.ts.bak} +0 -0
- /package/src/domains/account-deletion/infrastructure/services/{reauthentication.service.ts → reauthentication.service.ts.bak} +0 -0
- /package/src/domains/auth/{infrastructure.ts → infrastructure.ts.bak} +0 -0
- /package/src/domains/auth/presentation/hooks/{GoogleOAuthHookService.ts → GoogleOAuthHookService.ts.bak} +0 -0
- /package/src/domains/firestore/domain/entities/{Document.ts → Document.ts.bak} +0 -0
- /package/src/domains/firestore/domain/services/{QueryService.ts → QueryService.ts.bak} +0 -0
- /package/src/domains/firestore/domain/services/{QueryServiceAnalysis.ts → QueryServiceAnalysis.ts.bak} +0 -0
- /package/src/domains/firestore/domain/services/{QueryServiceHelpers.ts → QueryServiceHelpers.ts.bak} +0 -0
- /package/src/domains/firestore/domain/value-objects/{QueryOptionsSerialization.ts → QueryOptionsSerialization.ts.bak} +0 -0
- /package/src/domains/firestore/domain/value-objects/{QueryOptionsValidation.ts → QueryOptionsValidation.ts.bak} +0 -0
- /package/src/domains/firestore/domain/value-objects/{WhereClause.ts → WhereClause.ts.bak} +0 -0
- /package/src/domains/firestore/domain/value-objects/{WhereClauseFactory.ts → WhereClauseFactory.ts.bak} +0 -0
- /package/src/shared/infrastructure/base/{ErrorHandler.ts → ErrorHandler.ts.bak} +0 -0
- /package/src/shared/infrastructure/base/{ServiceBase.ts → ServiceBase.ts.bak} +0 -0
- /package/src/shared/infrastructure/config/base/{ServiceClientSingleton.ts → ServiceClientSingleton.ts.bak} +0 -0
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Collection Entity
|
|
2
|
+
* Collection Entity (Main)
|
|
3
3
|
* Single Responsibility: Represent a Firestore collection with metadata
|
|
4
4
|
*
|
|
5
5
|
* Domain entity that encapsulates collection information and metadata.
|
|
@@ -9,7 +9,22 @@
|
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
import type { CollectionReference, Query } from 'firebase/firestore';
|
|
12
|
-
import
|
|
12
|
+
import {
|
|
13
|
+
isValidCollectionName,
|
|
14
|
+
isValidCollectionPath,
|
|
15
|
+
extractCollectionNameFromPath,
|
|
16
|
+
extractParentCollectionPath,
|
|
17
|
+
isUserCollectionPath,
|
|
18
|
+
extractUserIdFromPath,
|
|
19
|
+
createSubCollectionPath as createSubCollectionPathUtil,
|
|
20
|
+
} from './CollectionValidation';
|
|
21
|
+
import {
|
|
22
|
+
getCollectionDepth,
|
|
23
|
+
collectionToObject,
|
|
24
|
+
isQueryReference,
|
|
25
|
+
isCollectionReference as isCollectionReferenceUtil,
|
|
26
|
+
} from './CollectionUtils';
|
|
27
|
+
import { fromReference, fromQuery } from './CollectionFactory';
|
|
13
28
|
|
|
14
29
|
/**
|
|
15
30
|
* Collection metadata
|
|
@@ -46,243 +61,68 @@ export class Collection<TDocument = unknown> {
|
|
|
46
61
|
static fromReference<TDocument = unknown>(
|
|
47
62
|
reference: CollectionReference<TDocument>
|
|
48
63
|
): Collection<TDocument> {
|
|
49
|
-
return
|
|
50
|
-
name: reference.id,
|
|
51
|
-
path: reference.path,
|
|
52
|
-
parentPath: reference.parent?.path || null,
|
|
53
|
-
});
|
|
64
|
+
return fromReference(reference);
|
|
54
65
|
}
|
|
55
66
|
|
|
56
67
|
/**
|
|
57
68
|
* Create collection from query
|
|
58
69
|
*/
|
|
59
70
|
static fromQuery<TDocument = unknown>(query: Query<TDocument>, name: string, path: string): Collection<TDocument> {
|
|
60
|
-
return
|
|
61
|
-
name,
|
|
62
|
-
path,
|
|
63
|
-
parentPath: path.split('/').slice(0, -2).join('/') || null,
|
|
64
|
-
});
|
|
71
|
+
return fromQuery(query, name, path);
|
|
65
72
|
}
|
|
66
73
|
|
|
67
|
-
/**
|
|
68
|
-
* Get collection name
|
|
69
|
-
*/
|
|
70
74
|
getName(): string {
|
|
71
75
|
return this.name;
|
|
72
76
|
}
|
|
73
77
|
|
|
74
|
-
/**
|
|
75
|
-
* Get collection path
|
|
76
|
-
*/
|
|
77
78
|
getPath(): string {
|
|
78
79
|
return this.path;
|
|
79
80
|
}
|
|
80
81
|
|
|
81
|
-
|
|
82
|
-
* Get parent path if exists
|
|
83
|
-
*/
|
|
84
|
-
getParentPath(): string | null {
|
|
82
|
+
getParentPath(): string | undefined {
|
|
85
83
|
return this.parentPath;
|
|
86
84
|
}
|
|
87
85
|
|
|
88
|
-
/**
|
|
89
|
-
* Check if collection is nested (has parent)
|
|
90
|
-
*/
|
|
91
86
|
isNested(): boolean {
|
|
92
|
-
return this.parentPath !==
|
|
87
|
+
return this.parentPath !== undefined;
|
|
93
88
|
}
|
|
94
89
|
|
|
95
|
-
/**
|
|
96
|
-
* Check if collection is root level (no parent)
|
|
97
|
-
*/
|
|
98
90
|
isRootLevel(): boolean {
|
|
99
|
-
return this.parentPath ===
|
|
91
|
+
return this.parentPath === undefined;
|
|
100
92
|
}
|
|
101
93
|
|
|
102
|
-
/**
|
|
103
|
-
* Get collection depth in hierarchy
|
|
104
|
-
* Root collections have depth 0
|
|
105
|
-
*/
|
|
106
94
|
getDepth(): number {
|
|
107
|
-
|
|
108
|
-
return this.path.split('/').length / 2 - 1;
|
|
95
|
+
return getCollectionDepth(this);
|
|
109
96
|
}
|
|
110
97
|
|
|
111
|
-
/**
|
|
112
|
-
* Get the underlying reference
|
|
113
|
-
*/
|
|
114
98
|
getReference(): CollectionReference<TDocument> | Query<TDocument> {
|
|
115
99
|
return this.reference;
|
|
116
100
|
}
|
|
117
101
|
|
|
118
|
-
/**
|
|
119
|
-
* Check if collection is a query (has filters/limits)
|
|
120
|
-
*/
|
|
121
102
|
isQuery(): boolean {
|
|
122
|
-
return
|
|
103
|
+
return isQueryReference(this.reference);
|
|
123
104
|
}
|
|
124
105
|
|
|
125
|
-
/**
|
|
126
|
-
* Check if collection is a collection reference (no filters)
|
|
127
|
-
*/
|
|
128
106
|
isCollectionReference(): boolean {
|
|
129
|
-
return
|
|
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;
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
// Reasonable length limit
|
|
154
|
-
if (name.length > 100) {
|
|
155
|
-
return false;
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
return true;
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
/**
|
|
162
|
-
* Validate collection path format
|
|
163
|
-
* Paths must follow Firestore path structure
|
|
164
|
-
*/
|
|
165
|
-
static isValidPath(path: string): boolean {
|
|
166
|
-
if (!path || typeof path !== 'string' || path.trim() === '') {
|
|
167
|
-
return false;
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
const segments = path.split('/');
|
|
171
|
-
if (segments.length < 2 || segments.length % 2 !== 0) {
|
|
172
|
-
return false;
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
return segments.every(segment => this.isValidName(segment));
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
/**
|
|
179
|
-
* Extract collection name from path
|
|
180
|
-
*/
|
|
181
|
-
static extractNameFromPath(path: string): string | null {
|
|
182
|
-
if (!this.isValidPath(path)) {
|
|
183
|
-
return null;
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
const segments = path.split('/');
|
|
187
|
-
return segments[segments.length - 1] || null;
|
|
107
|
+
return isCollectionReferenceUtil(this.reference);
|
|
188
108
|
}
|
|
189
109
|
|
|
190
|
-
/**
|
|
191
|
-
* Extract parent path from collection path
|
|
192
|
-
*/
|
|
193
|
-
static extractParentPath(path: string): string | null {
|
|
194
|
-
if (!this.isValidPath(path)) {
|
|
195
|
-
return null;
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
const segments = path.split('/');
|
|
199
|
-
if (segments.length <= 2) {
|
|
200
|
-
return null;
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
return segments.slice(0, -1).join('/');
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
/**
|
|
207
|
-
* Convert to plain object (for serialization)
|
|
208
|
-
*/
|
|
209
110
|
toObject(): CollectionMetadata {
|
|
210
|
-
return
|
|
211
|
-
name: this.name,
|
|
212
|
-
path: this.path,
|
|
213
|
-
parentPath: this.parentPath || undefined,
|
|
214
|
-
};
|
|
111
|
+
return collectionToObject(this);
|
|
215
112
|
}
|
|
216
113
|
|
|
217
|
-
/**
|
|
218
|
-
* Create a sub-collection path
|
|
219
|
-
*/
|
|
220
114
|
createSubCollectionPath(subCollectionName: string): string | null {
|
|
221
|
-
|
|
222
|
-
return null;
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
return `${this.path}/${subCollectionName}`;
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
/**
|
|
229
|
-
* Check if collection is a sub-collection of another
|
|
230
|
-
*/
|
|
231
|
-
isSubCollectionOf(other: Collection): boolean {
|
|
232
|
-
return this.parentPath === other.path;
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
/**
|
|
236
|
-
* Check if collection is parent of another
|
|
237
|
-
*/
|
|
238
|
-
isParentOf(other: Collection): boolean {
|
|
239
|
-
return other.isSubCollectionOf(this);
|
|
115
|
+
return createSubCollectionPathUtil(this.path, subCollectionName);
|
|
240
116
|
}
|
|
241
117
|
|
|
242
|
-
/**
|
|
243
|
-
* Get collection ID (similar to name but more explicit)
|
|
244
|
-
*/
|
|
245
|
-
getId(): string {
|
|
246
|
-
return this.name;
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
/**
|
|
250
|
-
* Check if this is a user collection (users/{userId}/{collection})
|
|
251
|
-
*/
|
|
252
118
|
isUserCollection(): boolean {
|
|
253
|
-
return this.
|
|
119
|
+
return isUserCollectionPath(this.path);
|
|
254
120
|
}
|
|
255
121
|
|
|
256
|
-
/**
|
|
257
|
-
* Extract user ID from user collection path
|
|
258
|
-
* Returns null if not a user collection
|
|
259
|
-
*/
|
|
260
122
|
extractUserId(): string | null {
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
const segments = this.path.split('/');
|
|
264
|
-
if (segments.length >= 3 && segments[0] === 'users') {
|
|
265
|
-
return segments[1];
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
return null;
|
|
123
|
+
return extractUserIdFromPath(this.path);
|
|
269
124
|
}
|
|
270
125
|
}
|
|
271
126
|
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
*/
|
|
275
|
-
export function createCollection<TDocument = unknown>(
|
|
276
|
-
reference: CollectionReference<TDocument> | Query<TDocument>,
|
|
277
|
-
name?: string,
|
|
278
|
-
path?: string
|
|
279
|
-
): Collection<TDocument> {
|
|
280
|
-
if ('type' in reference && reference.type === 'query') {
|
|
281
|
-
if (!name || !path) {
|
|
282
|
-
throw new Error('name and path are required for query collections');
|
|
283
|
-
}
|
|
284
|
-
return Collection.fromQuery(reference, name, path);
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
return Collection.fromReference(reference as CollectionReference<TDocument>);
|
|
288
|
-
}
|
|
127
|
+
// Re-export factory function for backward compatibility
|
|
128
|
+
export { createCollection, fromReference, fromQuery } from './CollectionFactory';
|
|
@@ -0,0 +1,288 @@
|
|
|
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;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Reasonable length limit
|
|
154
|
+
if (name.length > 100) {
|
|
155
|
+
return false;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return true;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Validate collection path format
|
|
163
|
+
* Paths must follow Firestore path structure
|
|
164
|
+
*/
|
|
165
|
+
static isValidPath(path: string): boolean {
|
|
166
|
+
if (!path || typeof path !== 'string' || path.trim() === '') {
|
|
167
|
+
return false;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const segments = path.split('/');
|
|
171
|
+
if (segments.length < 2 || segments.length % 2 !== 0) {
|
|
172
|
+
return false;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return segments.every(segment => this.isValidName(segment));
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Extract collection name from path
|
|
180
|
+
*/
|
|
181
|
+
static extractNameFromPath(path: string): string | null {
|
|
182
|
+
if (!this.isValidPath(path)) {
|
|
183
|
+
return null;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const segments = path.split('/');
|
|
187
|
+
return segments[segments.length - 1] || null;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Extract parent path from collection path
|
|
192
|
+
*/
|
|
193
|
+
static extractParentPath(path: string): string | null {
|
|
194
|
+
if (!this.isValidPath(path)) {
|
|
195
|
+
return null;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const segments = path.split('/');
|
|
199
|
+
if (segments.length <= 2) {
|
|
200
|
+
return null;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
return segments.slice(0, -1).join('/');
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Convert to plain object (for serialization)
|
|
208
|
+
*/
|
|
209
|
+
toObject(): CollectionMetadata {
|
|
210
|
+
return {
|
|
211
|
+
name: this.name,
|
|
212
|
+
path: this.path,
|
|
213
|
+
parentPath: this.parentPath || undefined,
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Create a sub-collection path
|
|
219
|
+
*/
|
|
220
|
+
createSubCollectionPath(subCollectionName: string): string | null {
|
|
221
|
+
if (!Collection.isValidName(subCollectionName)) {
|
|
222
|
+
return null;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
return `${this.path}/${subCollectionName}`;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Check if collection is a sub-collection of another
|
|
230
|
+
*/
|
|
231
|
+
isSubCollectionOf(other: Collection): boolean {
|
|
232
|
+
return this.parentPath === other.path;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Check if collection is parent of another
|
|
237
|
+
*/
|
|
238
|
+
isParentOf(other: Collection): boolean {
|
|
239
|
+
return other.isSubCollectionOf(this);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Get collection ID (similar to name but more explicit)
|
|
244
|
+
*/
|
|
245
|
+
getId(): string {
|
|
246
|
+
return this.name;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Check if this is a user collection (users/{userId}/{collection})
|
|
251
|
+
*/
|
|
252
|
+
isUserCollection(): boolean {
|
|
253
|
+
return this.parentPath?.startsWith('users/') || false;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Extract user ID from user collection path
|
|
258
|
+
* Returns null if not a user collection
|
|
259
|
+
*/
|
|
260
|
+
extractUserId(): string | null {
|
|
261
|
+
if (!this.isUserCollection()) return null;
|
|
262
|
+
|
|
263
|
+
const segments = this.path.split('/');
|
|
264
|
+
if (segments.length >= 3 && segments[0] === 'users') {
|
|
265
|
+
return segments[1];
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
return null;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Factory function to create collection entity
|
|
274
|
+
*/
|
|
275
|
+
export function createCollection<TDocument = unknown>(
|
|
276
|
+
reference: CollectionReference<TDocument> | Query<TDocument>,
|
|
277
|
+
name?: string,
|
|
278
|
+
path?: string
|
|
279
|
+
): Collection<TDocument> {
|
|
280
|
+
if ('type' in reference && reference.type === 'query') {
|
|
281
|
+
if (!name || !path) {
|
|
282
|
+
throw new Error('name and path are required for query collections');
|
|
283
|
+
}
|
|
284
|
+
return Collection.fromQuery(reference, name, path);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
return Collection.fromReference(reference as CollectionReference<TDocument>);
|
|
288
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Collection Factory
|
|
3
|
+
* Single Responsibility: Create collection entities
|
|
4
|
+
*
|
|
5
|
+
* Max lines: 150 (enforced for maintainability)
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { CollectionReference, Query } from 'firebase/firestore';
|
|
9
|
+
import { Collection } from './Collection';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Create collection from collection reference
|
|
13
|
+
*/
|
|
14
|
+
export function fromReference<TDocument = unknown>(
|
|
15
|
+
reference: CollectionReference<TDocument>
|
|
16
|
+
): Collection<TDocument> {
|
|
17
|
+
return new Collection(reference, {
|
|
18
|
+
name: reference.id,
|
|
19
|
+
path: reference.path,
|
|
20
|
+
parentPath: reference.parent?.path || undefined,
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Create collection from query
|
|
26
|
+
*/
|
|
27
|
+
export function fromQuery<TDocument = unknown>(
|
|
28
|
+
query: Query<TDocument>,
|
|
29
|
+
name: string,
|
|
30
|
+
path: string
|
|
31
|
+
): Collection<TDocument> {
|
|
32
|
+
return new Collection(query, {
|
|
33
|
+
name,
|
|
34
|
+
path,
|
|
35
|
+
parentPath: path.split('/').slice(0, -2).join('/') || undefined,
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Factory function to create collection entity
|
|
41
|
+
*/
|
|
42
|
+
export function createCollection<TDocument = unknown>(
|
|
43
|
+
reference: CollectionReference<TDocument> | Query<TDocument>,
|
|
44
|
+
name?: string,
|
|
45
|
+
path?: string
|
|
46
|
+
): Collection<TDocument> {
|
|
47
|
+
if ('type' in reference && reference.type === 'query') {
|
|
48
|
+
if (!name || !path) {
|
|
49
|
+
throw new Error('name and path are required for query collections');
|
|
50
|
+
}
|
|
51
|
+
return fromQuery(reference, name, path);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return fromReference(reference as CollectionReference<TDocument>);
|
|
55
|
+
}
|