@zs-soft/firestore-repository-engine 0.10.0
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/README.md
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# FirestoreRepositoryEngine
|
|
2
|
+
|
|
3
|
+
This project was generated using [Angular CLI](https://github.com/angular/angular-cli) version 21.0.0.
|
|
4
|
+
|
|
5
|
+
## Code scaffolding
|
|
6
|
+
|
|
7
|
+
Angular CLI includes powerful code scaffolding tools. To generate a new component, run:
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
ng generate component component-name
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
For a complete list of available schematics (such as `components`, `directives`, or `pipes`), run:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
ng generate --help
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Building
|
|
20
|
+
|
|
21
|
+
To build the library, run:
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
ng build firestore-repository-engine
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
This command will compile your project, and the build artifacts will be placed in the `dist/` directory.
|
|
28
|
+
|
|
29
|
+
### Publishing the Library
|
|
30
|
+
|
|
31
|
+
Once the project is built, you can publish your library by following these steps:
|
|
32
|
+
|
|
33
|
+
1. Navigate to the `dist` directory:
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
cd dist/firestore-repository-engine
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
2. Run the `npm publish` command to publish your library to the npm registry:
|
|
40
|
+
```bash
|
|
41
|
+
npm publish
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Running unit tests
|
|
45
|
+
|
|
46
|
+
To execute unit tests with the [Karma](https://karma-runner.github.io) test runner, use the following command:
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
ng test
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## Running end-to-end tests
|
|
53
|
+
|
|
54
|
+
For end-to-end (e2e) testing, run:
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
ng e2e
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Angular CLI does not come with an end-to-end testing framework by default. You can choose one that suits your needs.
|
|
61
|
+
|
|
62
|
+
## Additional Resources
|
|
63
|
+
|
|
64
|
+
For more information on using the Angular CLI, including detailed command references, visit the [Angular CLI Overview and Command Reference](https://angular.dev/tools/cli) page.
|
|
@@ -0,0 +1,829 @@
|
|
|
1
|
+
import { Observable } from 'rxjs';
|
|
2
|
+
import { inject, Injector, runInInjectionContext } from '@angular/core';
|
|
3
|
+
import { where, orderBy, startAfter, startAt, endBefore, endAt, limit, collection, doc, query, getCountFromServer, getDocs, setDoc, deleteDoc, docData, collectionData, Firestore } from '@angular/fire/firestore';
|
|
4
|
+
import { isValidSearchTerm, normalizeSearchTerm, DEFAULT_PAGE_SIZE } from '@zs-soft/common-api';
|
|
5
|
+
import { RepositoryEngine, FIRESTORE_ENGINE_CREATOR } from '@zs-soft/core-api';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Utility for Firestore text search operations
|
|
9
|
+
* Note: Firestore doesn't support full-text search natively.
|
|
10
|
+
* This implements prefix-based search using >= and < operators.
|
|
11
|
+
*/
|
|
12
|
+
class SearchUtil {
|
|
13
|
+
/**
|
|
14
|
+
* Validates search options
|
|
15
|
+
* Returns true if search term meets minimum length requirement
|
|
16
|
+
*/
|
|
17
|
+
static isValidSearch(search) {
|
|
18
|
+
if (!search) {
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
21
|
+
return isValidSearchTerm(search.term) && search.fields.length > 0;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Build Firestore-compatible prefix search filters for a single field
|
|
25
|
+
* Uses >= term and < term + high Unicode character for prefix matching
|
|
26
|
+
*
|
|
27
|
+
* Example: searching "John" will match "John", "Johnny", "Johnson"
|
|
28
|
+
*/
|
|
29
|
+
static buildPrefixSearchFilter(field, term) {
|
|
30
|
+
const normalizedTerm = normalizeSearchTerm(term);
|
|
31
|
+
const endTerm = normalizedTerm + '\uf8ff'; // High Unicode character for range end
|
|
32
|
+
return [
|
|
33
|
+
{ field, operator: '>=', value: normalizedTerm },
|
|
34
|
+
{ field, operator: '<', value: endTerm },
|
|
35
|
+
];
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Build search filters for the first searchable field
|
|
39
|
+
* Note: Firestore only supports range queries on a single field,
|
|
40
|
+
* so we can only search one field at a time with prefix matching
|
|
41
|
+
*/
|
|
42
|
+
static buildSearchFilters(search) {
|
|
43
|
+
if (!this.isValidSearch(search)) {
|
|
44
|
+
return [];
|
|
45
|
+
}
|
|
46
|
+
// Use the first field for prefix search (Firestore limitation)
|
|
47
|
+
const primaryField = search.fields[0];
|
|
48
|
+
return this.buildPrefixSearchFilter(primaryField, search.term);
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Check if an entity matches search criteria (client-side filtering)
|
|
52
|
+
* Used for additional fields that couldn't be queried server-side
|
|
53
|
+
*/
|
|
54
|
+
static matchesSearch(entity, search) {
|
|
55
|
+
if (!this.isValidSearch(search)) {
|
|
56
|
+
return true; // No valid search, include all
|
|
57
|
+
}
|
|
58
|
+
const normalizedTerm = normalizeSearchTerm(search.term);
|
|
59
|
+
return search.fields.some((field) => {
|
|
60
|
+
const value = entity[field];
|
|
61
|
+
if (typeof value !== 'string') {
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
64
|
+
return value.toLowerCase().includes(normalizedTerm);
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Filter entities client-side for multi-field search
|
|
69
|
+
* Use this after fetching data when you need to search multiple fields
|
|
70
|
+
*/
|
|
71
|
+
static filterBySearch(entities, search) {
|
|
72
|
+
if (!this.isValidSearch(search)) {
|
|
73
|
+
return entities;
|
|
74
|
+
}
|
|
75
|
+
return entities.filter((entity) => this.matchesSearch(entity, search));
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Helper for mapping Firestore documents to entities
|
|
81
|
+
*
|
|
82
|
+
* Design Pattern: Factory Pattern
|
|
83
|
+
* - Központosított objektum létrehozás Firestore dokumentumokból
|
|
84
|
+
* - Egységes mapping logika az egész alkalmazásban
|
|
85
|
+
* - Könnyen bővíthető új mapping stratégiákkal
|
|
86
|
+
*
|
|
87
|
+
* Előnyök:
|
|
88
|
+
* - DRY: Nem ismétlődik a mapping kód minden metódusban
|
|
89
|
+
* - Tesztelhetőség: Izoláltan tesztelhető a mapping logika
|
|
90
|
+
* - Karbantarthatóság: Egy helyen módosítható a mapping
|
|
91
|
+
*/
|
|
92
|
+
class EntityMapperHelper {
|
|
93
|
+
/**
|
|
94
|
+
* Map a single document snapshot to an entity
|
|
95
|
+
*/
|
|
96
|
+
static mapDocument(docSnapshot) {
|
|
97
|
+
return {
|
|
98
|
+
...docSnapshot.data(),
|
|
99
|
+
uid: docSnapshot.id,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Map a single document snapshot, excluding uid from data
|
|
104
|
+
*/
|
|
105
|
+
static mapDocumentWithoutUid(docSnapshot) {
|
|
106
|
+
const { uid, ...data } = docSnapshot.data();
|
|
107
|
+
return {
|
|
108
|
+
...data,
|
|
109
|
+
uid: docSnapshot.id,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Map multiple document snapshots to entities
|
|
114
|
+
*/
|
|
115
|
+
static mapDocuments(docs) {
|
|
116
|
+
return docs.map((docSnapshot) => this.mapDocument(docSnapshot));
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Map multiple document snapshots, excluding uid from data
|
|
120
|
+
*/
|
|
121
|
+
static mapDocumentsWithoutUid(docs) {
|
|
122
|
+
return docs.map((docSnapshot) => this.mapDocumentWithoutUid(docSnapshot));
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Apply client-side search filtering if needed
|
|
126
|
+
* Only applies when search has multiple fields (first field is server-side)
|
|
127
|
+
*/
|
|
128
|
+
static applySearchFilter(entities, search) {
|
|
129
|
+
if (!search || !SearchUtil.isValidSearch(search) || search.fields.length <= 1) {
|
|
130
|
+
return entities;
|
|
131
|
+
}
|
|
132
|
+
return SearchUtil.filterBySearch(entities, search);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Builds Firestore QueryConstraints from QueryOptions
|
|
138
|
+
*
|
|
139
|
+
* Design Pattern: Builder Pattern
|
|
140
|
+
* - Lépésről lépésre építi fel a komplex Firestore query-t
|
|
141
|
+
* - Elválasztja a konstrukciós logikát a reprezentációtól
|
|
142
|
+
* - Különböző constraint típusok moduláris kezelése
|
|
143
|
+
*
|
|
144
|
+
* Előnyök:
|
|
145
|
+
* - Olvashatóság: A query építés lépései világosak
|
|
146
|
+
* - Bővíthetőség: Új constraint típusok könnyen hozzáadhatók
|
|
147
|
+
* - Tesztelhetőség: Minden builder metódus külön tesztelhető
|
|
148
|
+
* - Firestore szabályok: A constraint sorrend automatikusan helyes
|
|
149
|
+
* (where → orderBy → cursor → limit)
|
|
150
|
+
*/
|
|
151
|
+
class QueryConstraintBuilder {
|
|
152
|
+
/**
|
|
153
|
+
* Build all query constraints from QueryOptions
|
|
154
|
+
*/
|
|
155
|
+
static build(options) {
|
|
156
|
+
const constraints = [];
|
|
157
|
+
// Add where filters
|
|
158
|
+
constraints.push(...this.buildWhereConstraints(options));
|
|
159
|
+
// Add orderBy constraints
|
|
160
|
+
constraints.push(...this.buildOrderByConstraints(options));
|
|
161
|
+
// Add cursor constraints (must come after orderBy)
|
|
162
|
+
constraints.push(...this.buildCursorConstraints(options));
|
|
163
|
+
// Add limit constraint (must come last)
|
|
164
|
+
constraints.push(...this.buildLimitConstraints(options));
|
|
165
|
+
return constraints;
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* Build where filter constraints
|
|
169
|
+
*/
|
|
170
|
+
static buildWhereConstraints(options) {
|
|
171
|
+
if (!options.where || options.where.length === 0) {
|
|
172
|
+
return [];
|
|
173
|
+
}
|
|
174
|
+
return options.where.map((filter) => where(filter.field, filter.operator, filter.value));
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* Build orderBy constraints
|
|
178
|
+
*/
|
|
179
|
+
static buildOrderByConstraints(options) {
|
|
180
|
+
if (!options.orderBy || options.orderBy.length === 0) {
|
|
181
|
+
return [];
|
|
182
|
+
}
|
|
183
|
+
return options.orderBy.map((sort) => orderBy(sort.field, sort.direction));
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* Build cursor-based pagination constraints
|
|
187
|
+
* Note: Requires orderBy to be set for proper cursor pagination
|
|
188
|
+
*/
|
|
189
|
+
static buildCursorConstraints(options) {
|
|
190
|
+
const constraints = [];
|
|
191
|
+
if (options.startAfter !== undefined) {
|
|
192
|
+
constraints.push(startAfter(options.startAfter));
|
|
193
|
+
}
|
|
194
|
+
if (options.startAt !== undefined) {
|
|
195
|
+
constraints.push(startAt(options.startAt));
|
|
196
|
+
}
|
|
197
|
+
if (options.endBefore !== undefined) {
|
|
198
|
+
constraints.push(endBefore(options.endBefore));
|
|
199
|
+
}
|
|
200
|
+
if (options.endAt !== undefined) {
|
|
201
|
+
constraints.push(endAt(options.endAt));
|
|
202
|
+
}
|
|
203
|
+
return constraints;
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* Build limit constraints
|
|
207
|
+
*/
|
|
208
|
+
static buildLimitConstraints(options) {
|
|
209
|
+
const constraints = [];
|
|
210
|
+
if (options.limit !== undefined && options.limit > 0) {
|
|
211
|
+
constraints.push(limit(options.limit));
|
|
212
|
+
}
|
|
213
|
+
return constraints;
|
|
214
|
+
}
|
|
215
|
+
/**
|
|
216
|
+
* Apply default limit if none specified
|
|
217
|
+
*/
|
|
218
|
+
static applyDefaultLimit(options) {
|
|
219
|
+
if (options.limit === undefined) {
|
|
220
|
+
return { ...options, limit: DEFAULT_PAGE_SIZE };
|
|
221
|
+
}
|
|
222
|
+
return options;
|
|
223
|
+
}
|
|
224
|
+
/**
|
|
225
|
+
* Build constraints for fetching one extra item to determine hasNext
|
|
226
|
+
*/
|
|
227
|
+
static buildWithExtraForPagination(options) {
|
|
228
|
+
const optionsWithExtra = {
|
|
229
|
+
...options,
|
|
230
|
+
limit: (options.limit || DEFAULT_PAGE_SIZE) + 1,
|
|
231
|
+
};
|
|
232
|
+
return this.build(optionsWithExtra);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Helper for processing and preparing QueryOptions
|
|
238
|
+
*
|
|
239
|
+
* Design Pattern: Strategy Pattern (előkészítő)
|
|
240
|
+
* - Különböző query előkészítési stratégiák (alap, paginált)
|
|
241
|
+
* - A konkrét végrehajtás a QueryConstraintBuilder-re delegálódik
|
|
242
|
+
* - Könnyen bővíthető új stratégiákkal (pl. aggregált, real-time)
|
|
243
|
+
*
|
|
244
|
+
* Előnyök:
|
|
245
|
+
* - Szétválasztás: Query előkészítés és végrehajtás külön
|
|
246
|
+
* - Újrafelhasználhatóság: Azonos előkészítés több metódusban
|
|
247
|
+
* - Tesztelhetőség: Az előkészítési logika izoláltan tesztelhető
|
|
248
|
+
*/
|
|
249
|
+
class QueryOptionsHelper {
|
|
250
|
+
/**
|
|
251
|
+
* Prepare query options with defaults and search filters
|
|
252
|
+
*/
|
|
253
|
+
static prepare(options) {
|
|
254
|
+
// Apply default limit
|
|
255
|
+
let prepared = QueryConstraintBuilder.applyDefaultLimit(options);
|
|
256
|
+
// Add search filters if provided
|
|
257
|
+
if (options.search && SearchUtil.isValidSearch(options.search)) {
|
|
258
|
+
const searchFilters = SearchUtil.buildSearchFilters(options.search);
|
|
259
|
+
prepared = {
|
|
260
|
+
...prepared,
|
|
261
|
+
where: [...(prepared.where || []), ...searchFilters],
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
return prepared;
|
|
265
|
+
}
|
|
266
|
+
/**
|
|
267
|
+
* Prepare options for pagination with custom page size
|
|
268
|
+
*/
|
|
269
|
+
static prepareForPagination(options, pageSize, startAfterCursor) {
|
|
270
|
+
const prepared = {
|
|
271
|
+
...options,
|
|
272
|
+
limit: pageSize,
|
|
273
|
+
startAfter: startAfterCursor,
|
|
274
|
+
};
|
|
275
|
+
// Add search filters if provided
|
|
276
|
+
if (options.search && SearchUtil.isValidSearch(options.search)) {
|
|
277
|
+
const searchFilters = SearchUtil.buildSearchFilters(options.search);
|
|
278
|
+
prepared.where = [...(prepared.where || []), ...searchFilters];
|
|
279
|
+
}
|
|
280
|
+
return prepared;
|
|
281
|
+
}
|
|
282
|
+
/**
|
|
283
|
+
* Check if search requires client-side filtering
|
|
284
|
+
* (when multiple fields are specified, only first is server-side)
|
|
285
|
+
*/
|
|
286
|
+
static requiresClientSideSearch(options) {
|
|
287
|
+
return !!(options.search &&
|
|
288
|
+
SearchUtil.isValidSearch(options.search) &&
|
|
289
|
+
options.search.fields.length > 1);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Utility functions for converting query parameters to QueryOptions
|
|
295
|
+
* Separated for better testability and reusability
|
|
296
|
+
*/
|
|
297
|
+
class QueryParamsUtil {
|
|
298
|
+
/**
|
|
299
|
+
* Convert path parameters and query parameters into QueryOptions
|
|
300
|
+
* This allows the list method to work with different implementations (Firestore, REST, etc.)
|
|
301
|
+
*/
|
|
302
|
+
static buildQueryOptionsFromParams(pathParams, queryParams) {
|
|
303
|
+
const options = {
|
|
304
|
+
where: [],
|
|
305
|
+
orderBy: [],
|
|
306
|
+
};
|
|
307
|
+
// Process query parameters
|
|
308
|
+
if (queryParams && queryParams.length > 0) {
|
|
309
|
+
for (const param of queryParams) {
|
|
310
|
+
const { key, value } = param;
|
|
311
|
+
switch (key.toLowerCase()) {
|
|
312
|
+
case 'limit':
|
|
313
|
+
const limitValue = parseInt(value, 10);
|
|
314
|
+
// Allow 0 to mean "no limit" (fetch all)
|
|
315
|
+
if (!isNaN(limitValue) && limitValue >= 0) {
|
|
316
|
+
options.limit = limitValue;
|
|
317
|
+
}
|
|
318
|
+
break;
|
|
319
|
+
case 'offset':
|
|
320
|
+
const offsetValue = parseInt(value, 10);
|
|
321
|
+
if (!isNaN(offsetValue) && offsetValue >= 0) {
|
|
322
|
+
options.offset = offsetValue;
|
|
323
|
+
}
|
|
324
|
+
break;
|
|
325
|
+
case 'sort':
|
|
326
|
+
case 'sortby':
|
|
327
|
+
case 'orderby':
|
|
328
|
+
// Format: field:direction (e.g., "createdAt:desc" or "name:asc")
|
|
329
|
+
const [field, direction = 'asc'] = value.split(':');
|
|
330
|
+
if (field) {
|
|
331
|
+
options.orderBy = options.orderBy || [];
|
|
332
|
+
options.orderBy.push({
|
|
333
|
+
field: field.trim(),
|
|
334
|
+
direction: direction.trim().toLowerCase() === 'desc' ? 'desc' : 'asc',
|
|
335
|
+
});
|
|
336
|
+
}
|
|
337
|
+
break;
|
|
338
|
+
// Handle filter parameters (e.g., status=active, type=user)
|
|
339
|
+
default:
|
|
340
|
+
if (key && value) {
|
|
341
|
+
options.where = options.where || [];
|
|
342
|
+
// Check if it's an 'in' operation (comma-separated values prefixed with 'in:')
|
|
343
|
+
// Format: field=in:value1,value2,value3
|
|
344
|
+
if (value.startsWith('in:')) {
|
|
345
|
+
const valuesStr = value.slice(3); // Remove 'in:' prefix
|
|
346
|
+
const values = valuesStr
|
|
347
|
+
.split(',')
|
|
348
|
+
.map((v) => QueryParamsUtil.parseFilterValue(v.trim()));
|
|
349
|
+
options.where.push({
|
|
350
|
+
field: key,
|
|
351
|
+
operator: 'in',
|
|
352
|
+
value: values,
|
|
353
|
+
});
|
|
354
|
+
}
|
|
355
|
+
// Check if it's a comparison operation (e.g., age>25, count<=10)
|
|
356
|
+
else {
|
|
357
|
+
const comparisonMatch = value.match(/^(>=|<=|>|<|!=)(.+)$/);
|
|
358
|
+
if (comparisonMatch) {
|
|
359
|
+
const [, operator, filterValue] = comparisonMatch;
|
|
360
|
+
options.where.push({
|
|
361
|
+
field: key,
|
|
362
|
+
operator: operator,
|
|
363
|
+
value: QueryParamsUtil.parseFilterValue(filterValue),
|
|
364
|
+
});
|
|
365
|
+
}
|
|
366
|
+
else {
|
|
367
|
+
// Default to equality check
|
|
368
|
+
options.where.push({
|
|
369
|
+
field: key,
|
|
370
|
+
operator: '==',
|
|
371
|
+
value: QueryParamsUtil.parseFilterValue(value),
|
|
372
|
+
});
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
break;
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
// Process path parameters if needed (could be used for nested collections)
|
|
381
|
+
if (pathParams && pathParams.length > 0) {
|
|
382
|
+
// Path parameters could be used for filtering or collection selection
|
|
383
|
+
// For example: /users/{userId}/posts could add a filter for userId
|
|
384
|
+
// This is implementation-specific and can be extended as needed
|
|
385
|
+
console.log('Path parameters provided:', pathParams);
|
|
386
|
+
}
|
|
387
|
+
return options;
|
|
388
|
+
}
|
|
389
|
+
/**
|
|
390
|
+
* Parse filter value to appropriate type (string, number, boolean, etc.)
|
|
391
|
+
*/
|
|
392
|
+
static parseFilterValue(value) {
|
|
393
|
+
// Try to parse as number (check if the entire string is a valid number)
|
|
394
|
+
const numValue = parseFloat(value);
|
|
395
|
+
if (!isNaN(numValue) && isFinite(numValue) && value.trim() === numValue.toString()) {
|
|
396
|
+
return numValue;
|
|
397
|
+
}
|
|
398
|
+
// Try to parse as boolean
|
|
399
|
+
if (value.toLowerCase() === 'true')
|
|
400
|
+
return true;
|
|
401
|
+
if (value.toLowerCase() === 'false')
|
|
402
|
+
return false;
|
|
403
|
+
// Try to parse as null/undefined
|
|
404
|
+
if (value.toLowerCase() === 'null')
|
|
405
|
+
return null;
|
|
406
|
+
if (value.toLowerCase() === 'undefined')
|
|
407
|
+
return undefined;
|
|
408
|
+
// Return as string (default)
|
|
409
|
+
return value;
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
class FirestoreRepositoryEngine extends RepositoryEngine {
|
|
414
|
+
firestore;
|
|
415
|
+
featureKey;
|
|
416
|
+
subcollectionContext;
|
|
417
|
+
injector = inject(Injector);
|
|
418
|
+
collectionPathBuilder;
|
|
419
|
+
constructor(firestore, featureKey, subcollectionContext) {
|
|
420
|
+
super();
|
|
421
|
+
this.firestore = firestore;
|
|
422
|
+
this.featureKey = featureKey;
|
|
423
|
+
this.subcollectionContext = subcollectionContext;
|
|
424
|
+
// Create path builder based on context
|
|
425
|
+
this.collectionPathBuilder = this.createPathBuilder();
|
|
426
|
+
}
|
|
427
|
+
/**
|
|
428
|
+
* Resolves the feature key, supporting both static strings and dynamic functions.
|
|
429
|
+
* Dynamic functions enable multi-tenant scenarios where the collection path
|
|
430
|
+
* depends on runtime context (e.g., active tenant ID).
|
|
431
|
+
*/
|
|
432
|
+
get resolvedFeatureKey() {
|
|
433
|
+
return typeof this.featureKey === 'function' ? this.featureKey() : this.featureKey;
|
|
434
|
+
}
|
|
435
|
+
/**
|
|
436
|
+
* Lazily resolved collection reference based on the current feature key.
|
|
437
|
+
* For dynamic (function-based) feature keys, this re-evaluates on every access,
|
|
438
|
+
* enabling tenant-switching without recreating the engine.
|
|
439
|
+
*/
|
|
440
|
+
get collectionRef() {
|
|
441
|
+
return collection(this.firestore, this.resolvedFeatureKey);
|
|
442
|
+
}
|
|
443
|
+
/**
|
|
444
|
+
* Creates a path builder function based on subcollection context
|
|
445
|
+
*/
|
|
446
|
+
createPathBuilder() {
|
|
447
|
+
if (!this.subcollectionContext) {
|
|
448
|
+
// Flat collection: always use resolved featureKey
|
|
449
|
+
return () => this.resolvedFeatureKey;
|
|
450
|
+
}
|
|
451
|
+
// Subcollection: build path dynamically
|
|
452
|
+
return (entityData) => {
|
|
453
|
+
if (!entityData) {
|
|
454
|
+
// Fallback to flat collection if no parent ID available
|
|
455
|
+
return this.resolvedFeatureKey;
|
|
456
|
+
}
|
|
457
|
+
const parentId = entityData[this.subcollectionContext.parentIdField];
|
|
458
|
+
if (!parentId) {
|
|
459
|
+
throw new Error(`Cannot resolve subcollection path: ${this.subcollectionContext.parentIdField} is required`);
|
|
460
|
+
}
|
|
461
|
+
const parentCollConfig = this.subcollectionContext.parentCollection;
|
|
462
|
+
const parentColl = typeof parentCollConfig === 'function' ? parentCollConfig() : parentCollConfig;
|
|
463
|
+
return `${parentColl}/${parentId}/${this.subcollectionContext.subcollection}`;
|
|
464
|
+
};
|
|
465
|
+
}
|
|
466
|
+
/**
|
|
467
|
+
* Gets collection reference for specific entity data
|
|
468
|
+
*/
|
|
469
|
+
getCollectionRef(entityData) {
|
|
470
|
+
const path = this.collectionPathBuilder(entityData);
|
|
471
|
+
return collection(this.firestore, path);
|
|
472
|
+
}
|
|
473
|
+
batch(operations) {
|
|
474
|
+
return new Observable((subscriber) => {
|
|
475
|
+
const batch = this.firestore.batch();
|
|
476
|
+
for (const op of operations) {
|
|
477
|
+
if (!op.uid)
|
|
478
|
+
continue; // Skip operations without uid
|
|
479
|
+
const ref = doc(this.collectionRef, op.uid);
|
|
480
|
+
switch (op.type) {
|
|
481
|
+
case 'create':
|
|
482
|
+
if (op.data) {
|
|
483
|
+
batch.set(ref, op.data);
|
|
484
|
+
}
|
|
485
|
+
break;
|
|
486
|
+
case 'update':
|
|
487
|
+
if (op.data) {
|
|
488
|
+
batch.update(ref, op.data);
|
|
489
|
+
}
|
|
490
|
+
break;
|
|
491
|
+
case 'delete':
|
|
492
|
+
batch.delete(ref);
|
|
493
|
+
break;
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
batch
|
|
497
|
+
.commit()
|
|
498
|
+
.then(() => {
|
|
499
|
+
subscriber.next();
|
|
500
|
+
subscriber.complete();
|
|
501
|
+
})
|
|
502
|
+
.catch((error) => subscriber.error(error));
|
|
503
|
+
});
|
|
504
|
+
}
|
|
505
|
+
count(options) {
|
|
506
|
+
return new Observable((subscriber) => {
|
|
507
|
+
const constraints = [];
|
|
508
|
+
if (options?.where) {
|
|
509
|
+
for (const filter of options.where) {
|
|
510
|
+
constraints.push(where(filter.field, filter.operator, filter.value));
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
const entitiesQuery = query(this.collectionRef, ...constraints);
|
|
514
|
+
// Use getCountFromServer for efficient counting without downloading documents
|
|
515
|
+
getCountFromServer(entitiesQuery)
|
|
516
|
+
.then((snapshot) => {
|
|
517
|
+
subscriber.next(snapshot.data().count);
|
|
518
|
+
subscriber.complete();
|
|
519
|
+
})
|
|
520
|
+
.catch((error) => {
|
|
521
|
+
// Fallback to the old method if getCountFromServer is not available
|
|
522
|
+
console.warn('getCountFromServer failed, falling back to getDocs:', error);
|
|
523
|
+
getDocs(entitiesQuery)
|
|
524
|
+
.then((docSnapshot) => {
|
|
525
|
+
subscriber.next(docSnapshot.size);
|
|
526
|
+
subscriber.complete();
|
|
527
|
+
})
|
|
528
|
+
.catch((fallbackError) => subscriber.error(fallbackError));
|
|
529
|
+
});
|
|
530
|
+
});
|
|
531
|
+
}
|
|
532
|
+
create$(entityAdd) {
|
|
533
|
+
return runInInjectionContext(this.injector, () => {
|
|
534
|
+
const uid = doc(collection(this.firestore, 'id')).id;
|
|
535
|
+
const newEntity = {
|
|
536
|
+
...entityAdd,
|
|
537
|
+
uid,
|
|
538
|
+
};
|
|
539
|
+
// Use entity data to resolve collection path
|
|
540
|
+
const collectionRef = this.getCollectionRef(newEntity);
|
|
541
|
+
return new Observable((subscriber) => {
|
|
542
|
+
setDoc(doc(collectionRef, uid), newEntity)
|
|
543
|
+
.then(() => {
|
|
544
|
+
subscriber.next({ ...newEntity });
|
|
545
|
+
subscriber.complete();
|
|
546
|
+
})
|
|
547
|
+
.catch((error) => {
|
|
548
|
+
subscriber.error(error);
|
|
549
|
+
});
|
|
550
|
+
});
|
|
551
|
+
});
|
|
552
|
+
}
|
|
553
|
+
delete$(entity) {
|
|
554
|
+
return runInInjectionContext(this.injector, () => {
|
|
555
|
+
// Use entity data to resolve collection path
|
|
556
|
+
const collectionRef = this.getCollectionRef(entity);
|
|
557
|
+
return new Observable((subscriber) => {
|
|
558
|
+
deleteDoc(doc(collectionRef, entity.uid))
|
|
559
|
+
.then(() => {
|
|
560
|
+
subscriber.next(entity);
|
|
561
|
+
subscriber.complete();
|
|
562
|
+
})
|
|
563
|
+
.catch((error) => subscriber.error(error));
|
|
564
|
+
});
|
|
565
|
+
});
|
|
566
|
+
}
|
|
567
|
+
exists(uid) {
|
|
568
|
+
return new Observable((subscriber) => {
|
|
569
|
+
getDocs(query(this.collectionRef, where('uid', '==', uid)))
|
|
570
|
+
.then((snapshot) => {
|
|
571
|
+
subscriber.next(!snapshot.empty);
|
|
572
|
+
subscriber.complete();
|
|
573
|
+
})
|
|
574
|
+
.catch((error) => subscriber.error(error));
|
|
575
|
+
});
|
|
576
|
+
}
|
|
577
|
+
list$(pathParams, queryParams) {
|
|
578
|
+
// Convert path and query parameters to QueryOptions for abstraction
|
|
579
|
+
const queryOptions = QueryParamsUtil.buildQueryOptionsFromParams(pathParams, queryParams);
|
|
580
|
+
// Delegate to the abstract query method for implementation flexibility
|
|
581
|
+
return this.query(queryOptions);
|
|
582
|
+
}
|
|
583
|
+
listByIds$(ids) {
|
|
584
|
+
return runInInjectionContext(this.injector, () => {
|
|
585
|
+
if (ids.length === 0) {
|
|
586
|
+
return new Observable((subscriber) => {
|
|
587
|
+
subscriber.next([]);
|
|
588
|
+
subscriber.complete();
|
|
589
|
+
});
|
|
590
|
+
}
|
|
591
|
+
const entitiesQuery = query(this.collectionRef, where('uid', 'in', ids));
|
|
592
|
+
return new Observable((subscriber) => {
|
|
593
|
+
getDocs(entitiesQuery)
|
|
594
|
+
.then((snapshot) => {
|
|
595
|
+
const entities = EntityMapperHelper.mapDocumentsWithoutUid(snapshot.docs);
|
|
596
|
+
subscriber.next(entities);
|
|
597
|
+
subscriber.complete();
|
|
598
|
+
})
|
|
599
|
+
.catch((error) => subscriber.error(error));
|
|
600
|
+
});
|
|
601
|
+
});
|
|
602
|
+
}
|
|
603
|
+
load$(uid) {
|
|
604
|
+
return runInInjectionContext(this.injector, () => {
|
|
605
|
+
const entityDocument = doc(this.firestore, `${this.resolvedFeatureKey}/${uid}`);
|
|
606
|
+
return docData(entityDocument, {
|
|
607
|
+
idField: 'uid',
|
|
608
|
+
});
|
|
609
|
+
});
|
|
610
|
+
}
|
|
611
|
+
query(options) {
|
|
612
|
+
return new Observable((subscriber) => {
|
|
613
|
+
// Prepare options with defaults and search filters
|
|
614
|
+
const queryOptions = QueryOptionsHelper.prepare(options);
|
|
615
|
+
// Build all constraints using the builder
|
|
616
|
+
const constraints = QueryConstraintBuilder.build(queryOptions);
|
|
617
|
+
const entitiesQuery = query(this.collectionRef, ...constraints);
|
|
618
|
+
getDocs(entitiesQuery)
|
|
619
|
+
.then((snapshot) => {
|
|
620
|
+
let entities = EntityMapperHelper.mapDocuments(snapshot.docs);
|
|
621
|
+
// Apply client-side search filtering for additional fields
|
|
622
|
+
entities = EntityMapperHelper.applySearchFilter(entities, options.search);
|
|
623
|
+
subscriber.next(entities);
|
|
624
|
+
subscriber.complete();
|
|
625
|
+
})
|
|
626
|
+
.catch((error) => {
|
|
627
|
+
console.error(error);
|
|
628
|
+
subscriber.error(error);
|
|
629
|
+
});
|
|
630
|
+
});
|
|
631
|
+
}
|
|
632
|
+
queryPaginated(options, pageSize, lastDocument) {
|
|
633
|
+
return new Observable((subscriber) => {
|
|
634
|
+
// Prepare options for pagination
|
|
635
|
+
const paginatedOptions = QueryOptionsHelper.prepareForPagination(options, pageSize, lastDocument?.uid);
|
|
636
|
+
// Build constraints and execute query
|
|
637
|
+
const constraints = QueryConstraintBuilder.build(paginatedOptions);
|
|
638
|
+
const entitiesQuery = query(this.collectionRef, ...constraints);
|
|
639
|
+
getDocs(entitiesQuery)
|
|
640
|
+
.then((snapshot) => {
|
|
641
|
+
let entities = EntityMapperHelper.mapDocuments(snapshot.docs);
|
|
642
|
+
entities = EntityMapperHelper.applySearchFilter(entities, options.search);
|
|
643
|
+
subscriber.next({
|
|
644
|
+
items: entities,
|
|
645
|
+
totalItems: entities.length,
|
|
646
|
+
totalPages: Math.ceil(entities.length / pageSize),
|
|
647
|
+
currentPage: 1,
|
|
648
|
+
hasNext: entities.length === pageSize,
|
|
649
|
+
hasPrevious: !!lastDocument,
|
|
650
|
+
});
|
|
651
|
+
subscriber.complete();
|
|
652
|
+
})
|
|
653
|
+
.catch((error) => subscriber.error(error));
|
|
654
|
+
});
|
|
655
|
+
}
|
|
656
|
+
/**
|
|
657
|
+
* Query with cursor-based pagination (more efficient for Firestore)
|
|
658
|
+
* Returns cursor information for next/previous page navigation
|
|
659
|
+
*/
|
|
660
|
+
queryCursorPaginated(options) {
|
|
661
|
+
return new Observable((subscriber) => {
|
|
662
|
+
const pageSize = options.limit || DEFAULT_PAGE_SIZE;
|
|
663
|
+
// Prepare options with search filters
|
|
664
|
+
const queryOptions = QueryOptionsHelper.prepare(options);
|
|
665
|
+
// Fetch one extra to determine hasNext
|
|
666
|
+
const constraints = QueryConstraintBuilder.buildWithExtraForPagination(queryOptions);
|
|
667
|
+
const entitiesQuery = query(this.collectionRef, ...constraints);
|
|
668
|
+
getDocs(entitiesQuery)
|
|
669
|
+
.then((snapshot) => {
|
|
670
|
+
let entities = EntityMapperHelper.mapDocuments(snapshot.docs);
|
|
671
|
+
entities = EntityMapperHelper.applySearchFilter(entities, options.search);
|
|
672
|
+
// Check if there are more results
|
|
673
|
+
const hasNext = entities.length > pageSize;
|
|
674
|
+
if (hasNext) {
|
|
675
|
+
entities = entities.slice(0, pageSize);
|
|
676
|
+
}
|
|
677
|
+
subscriber.next({
|
|
678
|
+
items: entities,
|
|
679
|
+
hasNext,
|
|
680
|
+
hasPrevious: !!options.startAfter,
|
|
681
|
+
firstCursor: entities.length > 0 ? entities[0].uid : undefined,
|
|
682
|
+
lastCursor: entities.length > 0 ? entities[entities.length - 1].uid : undefined,
|
|
683
|
+
});
|
|
684
|
+
subscriber.complete();
|
|
685
|
+
})
|
|
686
|
+
.catch((error) => subscriber.error(error));
|
|
687
|
+
});
|
|
688
|
+
}
|
|
689
|
+
search$(params) {
|
|
690
|
+
return runInInjectionContext(this.injector, () => {
|
|
691
|
+
if (!params || params.length === 0) {
|
|
692
|
+
return new Observable((subscriber) => {
|
|
693
|
+
subscriber.next([]);
|
|
694
|
+
subscriber.complete();
|
|
695
|
+
});
|
|
696
|
+
}
|
|
697
|
+
const queries = params.map((param) => where(param.query.field, param.query.operation, param.query.value));
|
|
698
|
+
const entityQuery = query(this.collectionRef, ...queries);
|
|
699
|
+
return new Observable((subscriber) => {
|
|
700
|
+
getDocs(entityQuery)
|
|
701
|
+
.then((snapshot) => {
|
|
702
|
+
const entities = EntityMapperHelper.mapDocumentsWithoutUid(snapshot.docs);
|
|
703
|
+
subscriber.next(entities);
|
|
704
|
+
subscriber.complete();
|
|
705
|
+
})
|
|
706
|
+
.catch((error) => subscriber.error(error));
|
|
707
|
+
});
|
|
708
|
+
});
|
|
709
|
+
}
|
|
710
|
+
update$(entityUpdate) {
|
|
711
|
+
return runInInjectionContext(this.injector, () => {
|
|
712
|
+
const newEntity = {
|
|
713
|
+
...entityUpdate,
|
|
714
|
+
};
|
|
715
|
+
// Use entity data to resolve collection path
|
|
716
|
+
const collectionRef = this.getCollectionRef(newEntity);
|
|
717
|
+
return new Observable((subscriber) => {
|
|
718
|
+
// Use merge: true to only update the provided fields, not replace the entire document
|
|
719
|
+
setDoc(doc(collectionRef, entityUpdate.uid), newEntity, { merge: true })
|
|
720
|
+
.then(() => {
|
|
721
|
+
subscriber.next(newEntity);
|
|
722
|
+
subscriber.complete();
|
|
723
|
+
})
|
|
724
|
+
.catch((error) => {
|
|
725
|
+
subscriber.error(error);
|
|
726
|
+
});
|
|
727
|
+
});
|
|
728
|
+
});
|
|
729
|
+
}
|
|
730
|
+
/**
|
|
731
|
+
* Query entities by parent ID (for subcollections)
|
|
732
|
+
* @param parentId The ID of the parent entity
|
|
733
|
+
* @param options Optional query options for filtering/sorting
|
|
734
|
+
*/
|
|
735
|
+
queryByParentId$(parentId, options) {
|
|
736
|
+
if (!this.subcollectionContext) {
|
|
737
|
+
throw new Error('queryByParentId$ requires subcollectionContext');
|
|
738
|
+
}
|
|
739
|
+
const parentContext = { [this.subcollectionContext.parentIdField]: parentId };
|
|
740
|
+
const collectionRef = this.getCollectionRef(parentContext);
|
|
741
|
+
return new Observable((subscriber) => {
|
|
742
|
+
// Prepare options and build constraints
|
|
743
|
+
const queryOptions = options ? QueryOptionsHelper.prepare(options) : {};
|
|
744
|
+
const constraints = QueryConstraintBuilder.build(queryOptions);
|
|
745
|
+
const entitiesQuery = query(collectionRef, ...constraints);
|
|
746
|
+
getDocs(entitiesQuery)
|
|
747
|
+
.then((snapshot) => {
|
|
748
|
+
let entities = EntityMapperHelper.mapDocuments(snapshot.docs);
|
|
749
|
+
entities = EntityMapperHelper.applySearchFilter(entities, options?.search);
|
|
750
|
+
subscriber.next(entities);
|
|
751
|
+
subscriber.complete();
|
|
752
|
+
})
|
|
753
|
+
.catch((error) => subscriber.error(error));
|
|
754
|
+
});
|
|
755
|
+
}
|
|
756
|
+
/**
|
|
757
|
+
* Real-time query with snapshot listener (continuous updates)
|
|
758
|
+
* Use this when you need live updates from Firestore
|
|
759
|
+
* Note: This creates a persistent connection and incurs read costs on every change
|
|
760
|
+
*/
|
|
761
|
+
queryRealtime$(options) {
|
|
762
|
+
return runInInjectionContext(this.injector, () => {
|
|
763
|
+
// Prepare options with defaults and search filters
|
|
764
|
+
const queryOptions = QueryOptionsHelper.prepare(options);
|
|
765
|
+
// Build constraints and create query
|
|
766
|
+
const constraints = QueryConstraintBuilder.build(queryOptions);
|
|
767
|
+
const entitiesQuery = query(this.collectionRef, ...constraints);
|
|
768
|
+
// Use collectionData for real-time updates
|
|
769
|
+
return collectionData(entitiesQuery, { idField: 'uid' });
|
|
770
|
+
});
|
|
771
|
+
}
|
|
772
|
+
/**
|
|
773
|
+
* Real-time load with snapshot listener (continuous updates for single document)
|
|
774
|
+
* Use this when you need live updates for a single entity
|
|
775
|
+
* Note: This creates a persistent connection and incurs read costs on every change
|
|
776
|
+
*/
|
|
777
|
+
loadRealtime$(uid) {
|
|
778
|
+
return runInInjectionContext(this.injector, () => {
|
|
779
|
+
const entityDocument = doc(this.firestore, `${this.resolvedFeatureKey}/${uid}`);
|
|
780
|
+
return docData(entityDocument, {
|
|
781
|
+
idField: 'uid',
|
|
782
|
+
});
|
|
783
|
+
});
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
/**
|
|
788
|
+
* Factory function that creates a Firestore repository engine
|
|
789
|
+
* This is injected via FIRESTORE_ENGINE_CREATOR token
|
|
790
|
+
*/
|
|
791
|
+
function createFirestoreEngineCreator(firestore) {
|
|
792
|
+
return (featureKey, subcollectionContext) => {
|
|
793
|
+
return new FirestoreRepositoryEngine(firestore, featureKey, subcollectionContext);
|
|
794
|
+
};
|
|
795
|
+
}
|
|
796
|
+
/**
|
|
797
|
+
* Provides the FIRESTORE_ENGINE_CREATOR for use with RepositoryEngineFactory
|
|
798
|
+
* Add this to your app.config.ts providers array
|
|
799
|
+
*
|
|
800
|
+
* @example
|
|
801
|
+
* ```typescript
|
|
802
|
+
* export const appConfig: ApplicationConfig = {
|
|
803
|
+
* providers: [
|
|
804
|
+
* provideFirestoreEngine(),
|
|
805
|
+
* // ... other providers
|
|
806
|
+
* ],
|
|
807
|
+
* };
|
|
808
|
+
* ```
|
|
809
|
+
*/
|
|
810
|
+
function provideFirestoreEngine() {
|
|
811
|
+
return {
|
|
812
|
+
provide: FIRESTORE_ENGINE_CREATOR,
|
|
813
|
+
useFactory: () => {
|
|
814
|
+
const firestore = inject(Firestore);
|
|
815
|
+
return createFirestoreEngineCreator(firestore);
|
|
816
|
+
},
|
|
817
|
+
};
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
/*
|
|
821
|
+
* Public API Surface of firestore-repository-engine
|
|
822
|
+
*/
|
|
823
|
+
|
|
824
|
+
/**
|
|
825
|
+
* Generated bundle index. Do not edit.
|
|
826
|
+
*/
|
|
827
|
+
|
|
828
|
+
export { EntityMapperHelper, FirestoreRepositoryEngine, QueryConstraintBuilder, QueryOptionsHelper, QueryParamsUtil, SearchUtil, provideFirestoreEngine };
|
|
829
|
+
//# sourceMappingURL=zs-soft-firestore-repository-engine.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"zs-soft-firestore-repository-engine.mjs","sources":["../../../projects/firestore-repository-engine/src/lib/utils/search.util.ts","../../../projects/firestore-repository-engine/src/lib/helpers/entity-mapper.helper.ts","../../../projects/firestore-repository-engine/src/lib/utils/query-constraint.builder.ts","../../../projects/firestore-repository-engine/src/lib/helpers/query-options.helper.ts","../../../projects/firestore-repository-engine/src/lib/utils/query-params.util.ts","../../../projects/firestore-repository-engine/src/lib/firestore-repository.engine.ts","../../../projects/firestore-repository-engine/src/lib/firestore-engine.provider.ts","../../../projects/firestore-repository-engine/src/public-api.ts","../../../projects/firestore-repository-engine/src/zs-soft-firestore-repository-engine.ts"],"sourcesContent":["import {\n isValidSearchTerm,\n normalizeSearchTerm,\n QueryFilter,\n SearchOptions,\n} from '@zs-soft/common-api';\n\n/**\n * Utility for Firestore text search operations\n * Note: Firestore doesn't support full-text search natively.\n * This implements prefix-based search using >= and < operators.\n */\nexport class SearchUtil {\n /**\n * Validates search options\n * Returns true if search term meets minimum length requirement\n */\n static isValidSearch(search: SearchOptions | undefined): boolean {\n if (!search) {\n return false;\n }\n return isValidSearchTerm(search.term) && search.fields.length > 0;\n }\n\n /**\n * Build Firestore-compatible prefix search filters for a single field\n * Uses >= term and < term + high Unicode character for prefix matching\n *\n * Example: searching \"John\" will match \"John\", \"Johnny\", \"Johnson\"\n */\n static buildPrefixSearchFilter(field: string, term: string): QueryFilter[] {\n const normalizedTerm = normalizeSearchTerm(term);\n const endTerm = normalizedTerm + '\\uf8ff'; // High Unicode character for range end\n\n return [\n { field, operator: '>=', value: normalizedTerm },\n { field, operator: '<', value: endTerm },\n ];\n }\n\n /**\n * Build search filters for the first searchable field\n * Note: Firestore only supports range queries on a single field,\n * so we can only search one field at a time with prefix matching\n */\n static buildSearchFilters(search: SearchOptions): QueryFilter[] {\n if (!this.isValidSearch(search)) {\n return [];\n }\n\n // Use the first field for prefix search (Firestore limitation)\n const primaryField = search.fields[0];\n return this.buildPrefixSearchFilter(primaryField, search.term);\n }\n\n /**\n * Check if an entity matches search criteria (client-side filtering)\n * Used for additional fields that couldn't be queried server-side\n */\n static matchesSearch(entity: Record<string, unknown>, search: SearchOptions): boolean {\n if (!this.isValidSearch(search)) {\n return true; // No valid search, include all\n }\n\n const normalizedTerm = normalizeSearchTerm(search.term);\n\n return search.fields.some((field) => {\n const value = entity[field];\n if (typeof value !== 'string') {\n return false;\n }\n return value.toLowerCase().includes(normalizedTerm);\n });\n }\n\n /**\n * Filter entities client-side for multi-field search\n * Use this after fetching data when you need to search multiple fields\n */\n static filterBySearch<T extends Record<string, unknown>>(\n entities: T[],\n search: SearchOptions,\n ): T[] {\n if (!this.isValidSearch(search)) {\n return entities;\n }\n\n return entities.filter((entity) => this.matchesSearch(entity, search));\n }\n}\n","import { DocumentData, QueryDocumentSnapshot } from '@angular/fire/firestore';\nimport { SearchOptions } from '@zs-soft/common-api';\nimport { Entity, EntityModel } from '@zs-soft/core-api';\n\nimport { SearchUtil } from '../utils/search.util';\n\n/**\n * Helper for mapping Firestore documents to entities\n *\n * Design Pattern: Factory Pattern\n * - Központosított objektum létrehozás Firestore dokumentumokból\n * - Egységes mapping logika az egész alkalmazásban\n * - Könnyen bővíthető új mapping stratégiákkal\n *\n * Előnyök:\n * - DRY: Nem ismétlődik a mapping kód minden metódusban\n * - Tesztelhetőség: Izoláltan tesztelhető a mapping logika\n * - Karbantarthatóság: Egy helyen módosítható a mapping\n */\nexport class EntityMapperHelper {\n /**\n * Map a single document snapshot to an entity\n */\n static mapDocument<T extends Entity>(docSnapshot: QueryDocumentSnapshot<DocumentData>): T {\n return {\n ...docSnapshot.data(),\n uid: docSnapshot.id,\n } as T;\n }\n\n /**\n * Map a single document snapshot, excluding uid from data\n */\n static mapDocumentWithoutUid<T extends EntityModel>(\n docSnapshot: QueryDocumentSnapshot<DocumentData>,\n ): T {\n const { uid, ...data } = docSnapshot.data() as EntityModel;\n return {\n ...data,\n uid: docSnapshot.id,\n } as T;\n }\n\n /**\n * Map multiple document snapshots to entities\n */\n static mapDocuments<T extends Entity>(docs: QueryDocumentSnapshot<DocumentData>[]): T[] {\n return docs.map((docSnapshot) => this.mapDocument<T>(docSnapshot));\n }\n\n /**\n * Map multiple document snapshots, excluding uid from data\n */\n static mapDocumentsWithoutUid<T extends EntityModel>(\n docs: QueryDocumentSnapshot<DocumentData>[],\n ): T[] {\n return docs.map((docSnapshot) => this.mapDocumentWithoutUid<T>(docSnapshot));\n }\n\n /**\n * Apply client-side search filtering if needed\n * Only applies when search has multiple fields (first field is server-side)\n */\n static applySearchFilter<T extends Entity>(entities: T[], search?: SearchOptions): T[] {\n if (!search || !SearchUtil.isValidSearch(search) || search.fields.length <= 1) {\n return entities;\n }\n\n return SearchUtil.filterBySearch(\n entities as unknown as Record<string, unknown>[],\n search,\n ) as unknown as T[];\n }\n}\n","import {\n limit,\n orderBy,\n QueryConstraint,\n startAfter,\n startAt,\n endAt,\n endBefore,\n where,\n} from '@angular/fire/firestore';\nimport { DEFAULT_PAGE_SIZE, QueryOptions } from '@zs-soft/common-api';\n\n/**\n * Builds Firestore QueryConstraints from QueryOptions\n *\n * Design Pattern: Builder Pattern\n * - Lépésről lépésre építi fel a komplex Firestore query-t\n * - Elválasztja a konstrukciós logikát a reprezentációtól\n * - Különböző constraint típusok moduláris kezelése\n *\n * Előnyök:\n * - Olvashatóság: A query építés lépései világosak\n * - Bővíthetőség: Új constraint típusok könnyen hozzáadhatók\n * - Tesztelhetőség: Minden builder metódus külön tesztelhető\n * - Firestore szabályok: A constraint sorrend automatikusan helyes\n * (where → orderBy → cursor → limit)\n */\nexport class QueryConstraintBuilder {\n /**\n * Build all query constraints from QueryOptions\n */\n static build(options: QueryOptions): QueryConstraint[] {\n const constraints: QueryConstraint[] = [];\n\n // Add where filters\n constraints.push(...this.buildWhereConstraints(options));\n\n // Add orderBy constraints\n constraints.push(...this.buildOrderByConstraints(options));\n\n // Add cursor constraints (must come after orderBy)\n constraints.push(...this.buildCursorConstraints(options));\n\n // Add limit constraint (must come last)\n constraints.push(...this.buildLimitConstraints(options));\n\n return constraints;\n }\n\n /**\n * Build where filter constraints\n */\n static buildWhereConstraints(options: QueryOptions): QueryConstraint[] {\n if (!options.where || options.where.length === 0) {\n return [];\n }\n\n return options.where.map((filter) => where(filter.field, filter.operator, filter.value));\n }\n\n /**\n * Build orderBy constraints\n */\n static buildOrderByConstraints(options: QueryOptions): QueryConstraint[] {\n if (!options.orderBy || options.orderBy.length === 0) {\n return [];\n }\n\n return options.orderBy.map((sort) => orderBy(sort.field, sort.direction));\n }\n\n /**\n * Build cursor-based pagination constraints\n * Note: Requires orderBy to be set for proper cursor pagination\n */\n static buildCursorConstraints(options: QueryOptions): QueryConstraint[] {\n const constraints: QueryConstraint[] = [];\n\n if (options.startAfter !== undefined) {\n constraints.push(startAfter(options.startAfter));\n }\n\n if (options.startAt !== undefined) {\n constraints.push(startAt(options.startAt));\n }\n\n if (options.endBefore !== undefined) {\n constraints.push(endBefore(options.endBefore));\n }\n\n if (options.endAt !== undefined) {\n constraints.push(endAt(options.endAt));\n }\n\n return constraints;\n }\n\n /**\n * Build limit constraints\n */\n static buildLimitConstraints(options: QueryOptions): QueryConstraint[] {\n const constraints: QueryConstraint[] = [];\n\n if (options.limit !== undefined && options.limit > 0) {\n constraints.push(limit(options.limit));\n }\n\n return constraints;\n }\n\n /**\n * Apply default limit if none specified\n */\n static applyDefaultLimit(options: QueryOptions): QueryOptions {\n if (options.limit === undefined) {\n return { ...options, limit: DEFAULT_PAGE_SIZE };\n }\n return options;\n }\n\n /**\n * Build constraints for fetching one extra item to determine hasNext\n */\n static buildWithExtraForPagination(options: QueryOptions): QueryConstraint[] {\n const optionsWithExtra = {\n ...options,\n limit: (options.limit || DEFAULT_PAGE_SIZE) + 1,\n };\n return this.build(optionsWithExtra);\n }\n}\n","import { QueryOptions } from '@zs-soft/common-api';\n\nimport { QueryConstraintBuilder } from '../utils/query-constraint.builder';\nimport { SearchUtil } from '../utils/search.util';\n\n/**\n * Helper for processing and preparing QueryOptions\n *\n * Design Pattern: Strategy Pattern (előkészítő)\n * - Különböző query előkészítési stratégiák (alap, paginált)\n * - A konkrét végrehajtás a QueryConstraintBuilder-re delegálódik\n * - Könnyen bővíthető új stratégiákkal (pl. aggregált, real-time)\n *\n * Előnyök:\n * - Szétválasztás: Query előkészítés és végrehajtás külön\n * - Újrafelhasználhatóság: Azonos előkészítés több metódusban\n * - Tesztelhetőség: Az előkészítési logika izoláltan tesztelhető\n */\nexport class QueryOptionsHelper {\n /**\n * Prepare query options with defaults and search filters\n */\n static prepare(options: QueryOptions): QueryOptions {\n // Apply default limit\n let prepared = QueryConstraintBuilder.applyDefaultLimit(options);\n\n // Add search filters if provided\n if (options.search && SearchUtil.isValidSearch(options.search)) {\n const searchFilters = SearchUtil.buildSearchFilters(options.search);\n prepared = {\n ...prepared,\n where: [...(prepared.where || []), ...searchFilters],\n };\n }\n\n return prepared;\n }\n\n /**\n * Prepare options for pagination with custom page size\n */\n static prepareForPagination(\n options: QueryOptions,\n pageSize: number,\n startAfterCursor?: string,\n ): QueryOptions {\n const prepared: QueryOptions = {\n ...options,\n limit: pageSize,\n startAfter: startAfterCursor,\n };\n\n // Add search filters if provided\n if (options.search && SearchUtil.isValidSearch(options.search)) {\n const searchFilters = SearchUtil.buildSearchFilters(options.search);\n prepared.where = [...(prepared.where || []), ...searchFilters];\n }\n\n return prepared;\n }\n\n /**\n * Check if search requires client-side filtering\n * (when multiple fields are specified, only first is server-side)\n */\n static requiresClientSideSearch(options: QueryOptions): boolean {\n return !!(\n options.search &&\n SearchUtil.isValidSearch(options.search) &&\n options.search.fields.length > 1\n );\n }\n}\n","import { KeyValue } from '@angular/common';\nimport { QueryOptions } from '@zs-soft/common-api';\n\n/**\n * Utility functions for converting query parameters to QueryOptions\n * Separated for better testability and reusability\n */\nexport class QueryParamsUtil {\n /**\n * Convert path parameters and query parameters into QueryOptions\n * This allows the list method to work with different implementations (Firestore, REST, etc.)\n */\n static buildQueryOptionsFromParams(\n pathParams: string[],\n queryParams: KeyValue<string, string>[],\n ): QueryOptions {\n const options: QueryOptions = {\n where: [],\n orderBy: [],\n };\n\n // Process query parameters\n if (queryParams && queryParams.length > 0) {\n for (const param of queryParams) {\n const { key, value } = param;\n\n switch (key.toLowerCase()) {\n case 'limit':\n const limitValue = parseInt(value, 10);\n // Allow 0 to mean \"no limit\" (fetch all)\n if (!isNaN(limitValue) && limitValue >= 0) {\n options.limit = limitValue;\n }\n break;\n\n case 'offset':\n const offsetValue = parseInt(value, 10);\n if (!isNaN(offsetValue) && offsetValue >= 0) {\n options.offset = offsetValue;\n }\n break;\n\n case 'sort':\n case 'sortby':\n case 'orderby':\n // Format: field:direction (e.g., \"createdAt:desc\" or \"name:asc\")\n const [field, direction = 'asc'] = value.split(':');\n if (field) {\n options.orderBy = options.orderBy || [];\n options.orderBy.push({\n field: field.trim(),\n direction: direction.trim().toLowerCase() === 'desc' ? 'desc' : 'asc',\n });\n }\n break;\n\n // Handle filter parameters (e.g., status=active, type=user)\n default:\n if (key && value) {\n options.where = options.where || [];\n\n // Check if it's an 'in' operation (comma-separated values prefixed with 'in:')\n // Format: field=in:value1,value2,value3\n if (value.startsWith('in:')) {\n const valuesStr = value.slice(3); // Remove 'in:' prefix\n const values = valuesStr\n .split(',')\n .map((v) => QueryParamsUtil.parseFilterValue(v.trim()));\n options.where.push({\n field: key,\n operator: 'in',\n value: values,\n });\n }\n // Check if it's a comparison operation (e.g., age>25, count<=10)\n else {\n const comparisonMatch = value.match(/^(>=|<=|>|<|!=)(.+)$/);\n if (comparisonMatch) {\n const [, operator, filterValue] = comparisonMatch;\n options.where.push({\n field: key,\n operator: operator as any,\n value: QueryParamsUtil.parseFilterValue(filterValue),\n });\n } else {\n // Default to equality check\n options.where.push({\n field: key,\n operator: '==',\n value: QueryParamsUtil.parseFilterValue(value),\n });\n }\n }\n }\n break;\n }\n }\n }\n\n // Process path parameters if needed (could be used for nested collections)\n if (pathParams && pathParams.length > 0) {\n // Path parameters could be used for filtering or collection selection\n // For example: /users/{userId}/posts could add a filter for userId\n // This is implementation-specific and can be extended as needed\n console.log('Path parameters provided:', pathParams);\n }\n\n return options;\n }\n\n /**\n * Parse filter value to appropriate type (string, number, boolean, etc.)\n */\n static parseFilterValue(value: string): any {\n // Try to parse as number (check if the entire string is a valid number)\n const numValue = parseFloat(value);\n if (!isNaN(numValue) && isFinite(numValue) && value.trim() === numValue.toString()) {\n return numValue;\n }\n\n // Try to parse as boolean\n if (value.toLowerCase() === 'true') return true;\n if (value.toLowerCase() === 'false') return false;\n\n // Try to parse as null/undefined\n if (value.toLowerCase() === 'null') return null;\n if (value.toLowerCase() === 'undefined') return undefined;\n\n // Return as string (default)\n return value;\n }\n}\n","import { Observable } from 'rxjs';\n\nimport { KeyValue } from '@angular/common';\nimport { inject, Injector, runInInjectionContext } from '@angular/core';\nimport {\n collection,\n collectionData,\n CollectionReference,\n deleteDoc,\n doc,\n docData,\n Firestore,\n getCountFromServer,\n getDocs,\n query,\n QueryConstraint,\n setDoc,\n where,\n} from '@angular/fire/firestore';\nimport {\n CursorPaginatedResult,\n DEFAULT_PAGE_SIZE,\n PaginatedResult,\n QueryOptions,\n SearchParams,\n} from '@zs-soft/common-api';\nimport {\n BatchOperation,\n CollectionPathBuilder,\n Entity,\n EntityModel,\n EntityModelAdd,\n EntityModelUpdate,\n RepositoryEngine,\n SubcollectionContext,\n} from '@zs-soft/core-api';\n\nimport { EntityMapperHelper } from './helpers/entity-mapper.helper';\nimport { QueryOptionsHelper } from './helpers/query-options.helper';\nimport { QueryConstraintBuilder } from './utils/query-constraint.builder';\nimport { QueryParamsUtil } from './utils/query-params.util';\n\nexport class FirestoreRepositoryEngine extends RepositoryEngine<\n EntityModel,\n EntityModelAdd,\n EntityModelUpdate\n> {\n private injector = inject(Injector);\n\n protected collectionPathBuilder: CollectionPathBuilder;\n\n public constructor(\n protected firestore: Firestore,\n protected featureKey: string | (() => string),\n protected subcollectionContext?: SubcollectionContext,\n ) {\n super();\n\n // Create path builder based on context\n this.collectionPathBuilder = this.createPathBuilder();\n }\n\n /**\n * Resolves the feature key, supporting both static strings and dynamic functions.\n * Dynamic functions enable multi-tenant scenarios where the collection path\n * depends on runtime context (e.g., active tenant ID).\n */\n protected get resolvedFeatureKey(): string {\n return typeof this.featureKey === 'function' ? this.featureKey() : this.featureKey;\n }\n\n /**\n * Lazily resolved collection reference based on the current feature key.\n * For dynamic (function-based) feature keys, this re-evaluates on every access,\n * enabling tenant-switching without recreating the engine.\n */\n protected get collectionRef(): CollectionReference<EntityModel> {\n return collection(this.firestore, this.resolvedFeatureKey) as CollectionReference<EntityModel>;\n }\n\n /**\n * Creates a path builder function based on subcollection context\n */\n private createPathBuilder(): CollectionPathBuilder {\n if (!this.subcollectionContext) {\n // Flat collection: always use resolved featureKey\n return () => this.resolvedFeatureKey;\n }\n\n // Subcollection: build path dynamically\n return (entityData?: Partial<EntityModel>) => {\n if (!entityData) {\n // Fallback to flat collection if no parent ID available\n return this.resolvedFeatureKey;\n }\n\n const parentId = (entityData as any)[this.subcollectionContext!.parentIdField];\n if (!parentId) {\n throw new Error(\n `Cannot resolve subcollection path: ${this.subcollectionContext!.parentIdField} is required`,\n );\n }\n\n const parentCollConfig = this.subcollectionContext!.parentCollection;\n const parentColl =\n typeof parentCollConfig === 'function' ? parentCollConfig() : parentCollConfig;\n\n return `${parentColl}/${parentId}/${this.subcollectionContext!.subcollection}`;\n };\n }\n\n /**\n * Gets collection reference for specific entity data\n */\n private getCollectionRef(entityData?: Partial<EntityModel>): CollectionReference<EntityModel> {\n const path = this.collectionPathBuilder(entityData);\n return collection(this.firestore, path) as CollectionReference<EntityModel>;\n }\n\n public override batch(operations: BatchOperation<Entity>[]): Observable<void> {\n return new Observable<void>((subscriber) => {\n const batch = (this.firestore as any).batch();\n for (const op of operations) {\n if (!op.uid) continue; // Skip operations without uid\n const ref = doc(this.collectionRef, op.uid);\n switch (op.type) {\n case 'create':\n if (op.data) {\n batch.set(ref, op.data);\n }\n break;\n case 'update':\n if (op.data) {\n batch.update(ref, op.data);\n }\n break;\n case 'delete':\n batch.delete(ref);\n break;\n }\n }\n batch\n .commit()\n .then(() => {\n subscriber.next();\n subscriber.complete();\n })\n .catch((error: unknown) => subscriber.error(error));\n });\n }\n\n public override count(options?: QueryOptions): Observable<number> {\n return new Observable<number>((subscriber) => {\n const constraints: QueryConstraint[] = [];\n if (options?.where) {\n for (const filter of options.where) {\n constraints.push(where(filter.field, filter.operator, filter.value));\n }\n }\n const entitiesQuery = query(this.collectionRef, ...constraints);\n\n // Use getCountFromServer for efficient counting without downloading documents\n getCountFromServer(entitiesQuery)\n .then((snapshot) => {\n subscriber.next(snapshot.data().count);\n subscriber.complete();\n })\n .catch((error) => {\n // Fallback to the old method if getCountFromServer is not available\n console.warn('getCountFromServer failed, falling back to getDocs:', error);\n getDocs(entitiesQuery)\n .then((docSnapshot) => {\n subscriber.next(docSnapshot.size);\n subscriber.complete();\n })\n .catch((fallbackError) => subscriber.error(fallbackError));\n });\n });\n }\n\n public override create$(entityAdd: EntityModelAdd): Observable<EntityModel> {\n return runInInjectionContext(this.injector, () => {\n const uid = doc(collection(this.firestore, 'id')).id;\n const newEntity = {\n ...entityAdd,\n uid,\n };\n\n // Use entity data to resolve collection path\n const collectionRef = this.getCollectionRef(newEntity);\n\n return new Observable<EntityModel>((subscriber) => {\n setDoc(doc(collectionRef, uid), newEntity)\n .then(() => {\n subscriber.next({ ...newEntity } as EntityModel);\n subscriber.complete();\n })\n .catch((error) => {\n subscriber.error(error);\n });\n });\n });\n }\n\n public override delete$(entity: EntityModel): Observable<EntityModel> {\n return runInInjectionContext(this.injector, () => {\n // Use entity data to resolve collection path\n const collectionRef = this.getCollectionRef(entity);\n\n return new Observable<EntityModel>((subscriber) => {\n deleteDoc(doc(collectionRef, entity.uid))\n .then(() => {\n subscriber.next(entity);\n subscriber.complete();\n })\n .catch((error) => subscriber.error(error));\n });\n });\n }\n\n public override exists(uid: string): Observable<boolean> {\n return new Observable<boolean>((subscriber) => {\n getDocs(query(this.collectionRef, where('uid', '==', uid)))\n .then((snapshot) => {\n subscriber.next(!snapshot.empty);\n subscriber.complete();\n })\n .catch((error) => subscriber.error(error));\n });\n }\n\n public override list$(\n pathParams: string[],\n queryParams: KeyValue<string, string>[],\n ): Observable<EntityModel[]> {\n // Convert path and query parameters to QueryOptions for abstraction\n const queryOptions = QueryParamsUtil.buildQueryOptionsFromParams(pathParams, queryParams);\n\n // Delegate to the abstract query method for implementation flexibility\n return this.query(queryOptions);\n }\n\n public listByIds$(ids: string[]): Observable<EntityModel[]> {\n return runInInjectionContext(this.injector, () => {\n if (ids.length === 0) {\n return new Observable<EntityModel[]>((subscriber) => {\n subscriber.next([]);\n subscriber.complete();\n });\n }\n\n const entitiesQuery = query(this.collectionRef, where('uid', 'in', ids));\n\n return new Observable<EntityModel[]>((subscriber) => {\n getDocs(entitiesQuery)\n .then((snapshot) => {\n const entities = EntityMapperHelper.mapDocumentsWithoutUid<EntityModel>(snapshot.docs);\n subscriber.next(entities);\n subscriber.complete();\n })\n .catch((error) => subscriber.error(error));\n });\n });\n }\n\n public override load$(uid: string): Observable<EntityModel | undefined> {\n return runInInjectionContext(this.injector, () => {\n const entityDocument = doc(this.firestore, `${this.resolvedFeatureKey}/${uid}`);\n\n return docData(entityDocument, {\n idField: 'uid',\n }) as Observable<EntityModel>;\n });\n }\n\n public override query(options: QueryOptions): Observable<Entity[]> {\n return new Observable<Entity[]>((subscriber) => {\n // Prepare options with defaults and search filters\n const queryOptions = QueryOptionsHelper.prepare(options);\n\n // Build all constraints using the builder\n const constraints = QueryConstraintBuilder.build(queryOptions);\n const entitiesQuery = query(this.collectionRef, ...constraints);\n\n getDocs(entitiesQuery)\n .then((snapshot) => {\n let entities = EntityMapperHelper.mapDocuments<Entity>(snapshot.docs);\n\n // Apply client-side search filtering for additional fields\n entities = EntityMapperHelper.applySearchFilter(entities, options.search);\n\n subscriber.next(entities);\n subscriber.complete();\n })\n .catch((error) => {\n console.error(error);\n subscriber.error(error);\n });\n });\n }\n\n public override queryPaginated(\n options: QueryOptions,\n pageSize: number,\n lastDocument?: Entity,\n ): Observable<PaginatedResult<Entity>> {\n return new Observable<PaginatedResult<Entity>>((subscriber) => {\n // Prepare options for pagination\n const paginatedOptions = QueryOptionsHelper.prepareForPagination(\n options,\n pageSize,\n lastDocument?.uid,\n );\n\n // Build constraints and execute query\n const constraints = QueryConstraintBuilder.build(paginatedOptions);\n const entitiesQuery = query(this.collectionRef, ...constraints);\n\n getDocs(entitiesQuery)\n .then((snapshot) => {\n let entities = EntityMapperHelper.mapDocuments<Entity>(snapshot.docs);\n entities = EntityMapperHelper.applySearchFilter(entities, options.search);\n\n subscriber.next({\n items: entities,\n totalItems: entities.length,\n totalPages: Math.ceil(entities.length / pageSize),\n currentPage: 1,\n hasNext: entities.length === pageSize,\n hasPrevious: !!lastDocument,\n });\n subscriber.complete();\n })\n .catch((error) => subscriber.error(error));\n });\n }\n\n /**\n * Query with cursor-based pagination (more efficient for Firestore)\n * Returns cursor information for next/previous page navigation\n */\n public queryCursorPaginated(options: QueryOptions): Observable<CursorPaginatedResult<Entity>> {\n return new Observable<CursorPaginatedResult<Entity>>((subscriber) => {\n const pageSize = options.limit || DEFAULT_PAGE_SIZE;\n\n // Prepare options with search filters\n const queryOptions = QueryOptionsHelper.prepare(options);\n\n // Fetch one extra to determine hasNext\n const constraints = QueryConstraintBuilder.buildWithExtraForPagination(queryOptions);\n const entitiesQuery = query(this.collectionRef, ...constraints);\n\n getDocs(entitiesQuery)\n .then((snapshot) => {\n let entities = EntityMapperHelper.mapDocuments<Entity>(snapshot.docs);\n entities = EntityMapperHelper.applySearchFilter(entities, options.search);\n\n // Check if there are more results\n const hasNext = entities.length > pageSize;\n if (hasNext) {\n entities = entities.slice(0, pageSize);\n }\n\n subscriber.next({\n items: entities,\n hasNext,\n hasPrevious: !!options.startAfter,\n firstCursor: entities.length > 0 ? entities[0].uid : undefined,\n lastCursor: entities.length > 0 ? entities[entities.length - 1].uid : undefined,\n });\n subscriber.complete();\n })\n .catch((error) => subscriber.error(error));\n });\n }\n\n public search$(params: SearchParams): Observable<EntityModel[]> {\n return runInInjectionContext(this.injector, () => {\n if (!params || params.length === 0) {\n return new Observable<EntityModel[]>((subscriber) => {\n subscriber.next([]);\n subscriber.complete();\n });\n }\n\n const queries: QueryConstraint[] = params.map((param: any) =>\n where(param.query.field, param.query.operation, param.query.value),\n );\n\n const entityQuery = query(this.collectionRef, ...queries);\n\n return new Observable<EntityModel[]>((subscriber) => {\n getDocs(entityQuery)\n .then((snapshot) => {\n const entities = EntityMapperHelper.mapDocumentsWithoutUid<EntityModel>(snapshot.docs);\n subscriber.next(entities);\n subscriber.complete();\n })\n .catch((error) => subscriber.error(error));\n });\n });\n }\n\n public override update$(entityUpdate: EntityModelUpdate): Observable<EntityModelUpdate> {\n return runInInjectionContext(this.injector, () => {\n const newEntity: EntityModel = {\n ...entityUpdate,\n } as EntityModel;\n\n // Use entity data to resolve collection path\n const collectionRef = this.getCollectionRef(newEntity);\n\n return new Observable<EntityModelUpdate>((subscriber) => {\n // Use merge: true to only update the provided fields, not replace the entire document\n setDoc(doc(collectionRef, entityUpdate.uid), newEntity, { merge: true })\n .then(() => {\n subscriber.next(newEntity as EntityModelUpdate);\n subscriber.complete();\n })\n .catch((error) => {\n subscriber.error(error);\n });\n });\n });\n }\n\n /**\n * Query entities by parent ID (for subcollections)\n * @param parentId The ID of the parent entity\n * @param options Optional query options for filtering/sorting\n */\n public queryByParentId$(parentId: string, options?: QueryOptions): Observable<EntityModel[]> {\n if (!this.subcollectionContext) {\n throw new Error('queryByParentId$ requires subcollectionContext');\n }\n\n const parentContext = { [this.subcollectionContext.parentIdField]: parentId };\n const collectionRef = this.getCollectionRef(parentContext);\n\n return new Observable<EntityModel[]>((subscriber) => {\n // Prepare options and build constraints\n const queryOptions = options ? QueryOptionsHelper.prepare(options) : {};\n const constraints = QueryConstraintBuilder.build(queryOptions);\n const entitiesQuery = query(collectionRef, ...constraints);\n\n getDocs(entitiesQuery)\n .then((snapshot) => {\n let entities = EntityMapperHelper.mapDocuments<EntityModel>(snapshot.docs);\n entities = EntityMapperHelper.applySearchFilter(entities, options?.search);\n subscriber.next(entities);\n subscriber.complete();\n })\n .catch((error) => subscriber.error(error));\n });\n }\n\n /**\n * Real-time query with snapshot listener (continuous updates)\n * Use this when you need live updates from Firestore\n * Note: This creates a persistent connection and incurs read costs on every change\n */\n public queryRealtime$(options: QueryOptions): Observable<Entity[]> {\n return runInInjectionContext(this.injector, () => {\n // Prepare options with defaults and search filters\n const queryOptions = QueryOptionsHelper.prepare(options);\n\n // Build constraints and create query\n const constraints = QueryConstraintBuilder.build(queryOptions);\n const entitiesQuery = query(this.collectionRef, ...constraints);\n\n // Use collectionData for real-time updates\n return collectionData(entitiesQuery, { idField: 'uid' }) as Observable<Entity[]>;\n });\n }\n\n /**\n * Real-time load with snapshot listener (continuous updates for single document)\n * Use this when you need live updates for a single entity\n * Note: This creates a persistent connection and incurs read costs on every change\n */\n public loadRealtime$(uid: string): Observable<EntityModel | undefined> {\n return runInInjectionContext(this.injector, () => {\n const entityDocument = doc(this.firestore, `${this.resolvedFeatureKey}/${uid}`);\n\n return docData(entityDocument, {\n idField: 'uid',\n }) as Observable<EntityModel>;\n });\n }\n}\n","import { inject, Provider } from '@angular/core';\nimport { Firestore } from '@angular/fire/firestore';\nimport {\n EntityModel,\n EntityModelAdd,\n EntityModelUpdate,\n FIRESTORE_ENGINE_CREATOR,\n RepositoryEngine,\n RepositoryEngineCreator,\n SubcollectionContext,\n} from '@zs-soft/core-api';\n\nimport { FirestoreRepositoryEngine } from './firestore-repository.engine';\n\n/**\n * Factory function that creates a Firestore repository engine\n * This is injected via FIRESTORE_ENGINE_CREATOR token\n */\nfunction createFirestoreEngineCreator(firestore: Firestore): RepositoryEngineCreator {\n return <R extends EntityModel, S extends EntityModelAdd, T extends EntityModelUpdate>(\n featureKey: string | (() => string),\n subcollectionContext?: SubcollectionContext,\n ): RepositoryEngine<R, S, T> => {\n return new FirestoreRepositoryEngine(\n firestore,\n featureKey,\n subcollectionContext,\n ) as unknown as RepositoryEngine<R, S, T>;\n };\n}\n\n/**\n * Provides the FIRESTORE_ENGINE_CREATOR for use with RepositoryEngineFactory\n * Add this to your app.config.ts providers array\n *\n * @example\n * ```typescript\n * export const appConfig: ApplicationConfig = {\n * providers: [\n * provideFirestoreEngine(),\n * // ... other providers\n * ],\n * };\n * ```\n */\nexport function provideFirestoreEngine(): Provider {\n return {\n provide: FIRESTORE_ENGINE_CREATOR,\n useFactory: () => {\n const firestore = inject(Firestore);\n return createFirestoreEngineCreator(firestore);\n },\n };\n}\n","/*\n * Public API Surface of firestore-repository-engine\n */\n\nexport * from './lib/firestore-repository.engine';\nexport * from './lib/firestore-engine.provider';\nexport * from './lib/helpers/entity-mapper.helper';\nexport * from './lib/helpers/query-options.helper';\nexport * from './lib/utils/query-constraint.builder';\nexport * from './lib/utils/query-params.util';\nexport * from './lib/utils/search.util';\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './public-api';\n"],"names":[],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@zs-soft/firestore-repository-engine",
|
|
3
|
+
"version": "0.10.0",
|
|
4
|
+
"description": "Firestore repository engine for Angular applications",
|
|
5
|
+
"author": "zssz-soft",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "https://github.com/zssz-soft/libraries.git",
|
|
10
|
+
"directory": "projects/firestore-repository-engine"
|
|
11
|
+
},
|
|
12
|
+
"homepage": "https://github.com/zssz-soft/libraries/tree/main/projects/firestore-repository-engine",
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/zssz-soft/libraries/issues"
|
|
15
|
+
},
|
|
16
|
+
"keywords": [
|
|
17
|
+
"angular",
|
|
18
|
+
"firestore",
|
|
19
|
+
"repository",
|
|
20
|
+
"firebase"
|
|
21
|
+
],
|
|
22
|
+
"publishConfig": {
|
|
23
|
+
"access": "public"
|
|
24
|
+
},
|
|
25
|
+
"peerDependencies": {
|
|
26
|
+
"@angular/common": "^21.2.0",
|
|
27
|
+
"@angular/core": "^21.2.0",
|
|
28
|
+
"@angular/fire": "^21.2.0",
|
|
29
|
+
"@zs-soft/common-api": "^0.10.0",
|
|
30
|
+
"@zs-soft/core-api": "^0.10.0",
|
|
31
|
+
"rxjs": "^7.0.0"
|
|
32
|
+
},
|
|
33
|
+
"dependencies": {
|
|
34
|
+
"tslib": "^2.3.0"
|
|
35
|
+
},
|
|
36
|
+
"sideEffects": false,
|
|
37
|
+
"tags": [
|
|
38
|
+
"type:engine",
|
|
39
|
+
"scope:shared"
|
|
40
|
+
],
|
|
41
|
+
"module": "fesm2022/zs-soft-firestore-repository-engine.mjs",
|
|
42
|
+
"typings": "types/zs-soft-firestore-repository-engine.d.ts",
|
|
43
|
+
"exports": {
|
|
44
|
+
"./package.json": {
|
|
45
|
+
"default": "./package.json"
|
|
46
|
+
},
|
|
47
|
+
".": {
|
|
48
|
+
"types": "./types/zs-soft-firestore-repository-engine.d.ts",
|
|
49
|
+
"default": "./fesm2022/zs-soft-firestore-repository-engine.mjs"
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
import { Observable } from 'rxjs';
|
|
2
|
+
import { KeyValue } from '@angular/common';
|
|
3
|
+
import { Firestore, CollectionReference, QueryDocumentSnapshot, DocumentData, QueryConstraint } from '@angular/fire/firestore';
|
|
4
|
+
import { QueryOptions, PaginatedResult, CursorPaginatedResult, SearchParams, SearchOptions, QueryFilter } from '@zs-soft/common-api';
|
|
5
|
+
import { RepositoryEngine, EntityModel, EntityModelAdd, EntityModelUpdate, SubcollectionContext, CollectionPathBuilder, BatchOperation, Entity } from '@zs-soft/core-api';
|
|
6
|
+
import { Provider } from '@angular/core';
|
|
7
|
+
|
|
8
|
+
declare class FirestoreRepositoryEngine extends RepositoryEngine<EntityModel, EntityModelAdd, EntityModelUpdate> {
|
|
9
|
+
protected firestore: Firestore;
|
|
10
|
+
protected featureKey: string | (() => string);
|
|
11
|
+
protected subcollectionContext?: SubcollectionContext | undefined;
|
|
12
|
+
private injector;
|
|
13
|
+
protected collectionPathBuilder: CollectionPathBuilder;
|
|
14
|
+
constructor(firestore: Firestore, featureKey: string | (() => string), subcollectionContext?: SubcollectionContext | undefined);
|
|
15
|
+
/**
|
|
16
|
+
* Resolves the feature key, supporting both static strings and dynamic functions.
|
|
17
|
+
* Dynamic functions enable multi-tenant scenarios where the collection path
|
|
18
|
+
* depends on runtime context (e.g., active tenant ID).
|
|
19
|
+
*/
|
|
20
|
+
protected get resolvedFeatureKey(): string;
|
|
21
|
+
/**
|
|
22
|
+
* Lazily resolved collection reference based on the current feature key.
|
|
23
|
+
* For dynamic (function-based) feature keys, this re-evaluates on every access,
|
|
24
|
+
* enabling tenant-switching without recreating the engine.
|
|
25
|
+
*/
|
|
26
|
+
protected get collectionRef(): CollectionReference<EntityModel>;
|
|
27
|
+
/**
|
|
28
|
+
* Creates a path builder function based on subcollection context
|
|
29
|
+
*/
|
|
30
|
+
private createPathBuilder;
|
|
31
|
+
/**
|
|
32
|
+
* Gets collection reference for specific entity data
|
|
33
|
+
*/
|
|
34
|
+
private getCollectionRef;
|
|
35
|
+
batch(operations: BatchOperation<Entity>[]): Observable<void>;
|
|
36
|
+
count(options?: QueryOptions): Observable<number>;
|
|
37
|
+
create$(entityAdd: EntityModelAdd): Observable<EntityModel>;
|
|
38
|
+
delete$(entity: EntityModel): Observable<EntityModel>;
|
|
39
|
+
exists(uid: string): Observable<boolean>;
|
|
40
|
+
list$(pathParams: string[], queryParams: KeyValue<string, string>[]): Observable<EntityModel[]>;
|
|
41
|
+
listByIds$(ids: string[]): Observable<EntityModel[]>;
|
|
42
|
+
load$(uid: string): Observable<EntityModel | undefined>;
|
|
43
|
+
query(options: QueryOptions): Observable<Entity[]>;
|
|
44
|
+
queryPaginated(options: QueryOptions, pageSize: number, lastDocument?: Entity): Observable<PaginatedResult<Entity>>;
|
|
45
|
+
/**
|
|
46
|
+
* Query with cursor-based pagination (more efficient for Firestore)
|
|
47
|
+
* Returns cursor information for next/previous page navigation
|
|
48
|
+
*/
|
|
49
|
+
queryCursorPaginated(options: QueryOptions): Observable<CursorPaginatedResult<Entity>>;
|
|
50
|
+
search$(params: SearchParams): Observable<EntityModel[]>;
|
|
51
|
+
update$(entityUpdate: EntityModelUpdate): Observable<EntityModelUpdate>;
|
|
52
|
+
/**
|
|
53
|
+
* Query entities by parent ID (for subcollections)
|
|
54
|
+
* @param parentId The ID of the parent entity
|
|
55
|
+
* @param options Optional query options for filtering/sorting
|
|
56
|
+
*/
|
|
57
|
+
queryByParentId$(parentId: string, options?: QueryOptions): Observable<EntityModel[]>;
|
|
58
|
+
/**
|
|
59
|
+
* Real-time query with snapshot listener (continuous updates)
|
|
60
|
+
* Use this when you need live updates from Firestore
|
|
61
|
+
* Note: This creates a persistent connection and incurs read costs on every change
|
|
62
|
+
*/
|
|
63
|
+
queryRealtime$(options: QueryOptions): Observable<Entity[]>;
|
|
64
|
+
/**
|
|
65
|
+
* Real-time load with snapshot listener (continuous updates for single document)
|
|
66
|
+
* Use this when you need live updates for a single entity
|
|
67
|
+
* Note: This creates a persistent connection and incurs read costs on every change
|
|
68
|
+
*/
|
|
69
|
+
loadRealtime$(uid: string): Observable<EntityModel | undefined>;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Provides the FIRESTORE_ENGINE_CREATOR for use with RepositoryEngineFactory
|
|
74
|
+
* Add this to your app.config.ts providers array
|
|
75
|
+
*
|
|
76
|
+
* @example
|
|
77
|
+
* ```typescript
|
|
78
|
+
* export const appConfig: ApplicationConfig = {
|
|
79
|
+
* providers: [
|
|
80
|
+
* provideFirestoreEngine(),
|
|
81
|
+
* // ... other providers
|
|
82
|
+
* ],
|
|
83
|
+
* };
|
|
84
|
+
* ```
|
|
85
|
+
*/
|
|
86
|
+
declare function provideFirestoreEngine(): Provider;
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Helper for mapping Firestore documents to entities
|
|
90
|
+
*
|
|
91
|
+
* Design Pattern: Factory Pattern
|
|
92
|
+
* - Központosított objektum létrehozás Firestore dokumentumokból
|
|
93
|
+
* - Egységes mapping logika az egész alkalmazásban
|
|
94
|
+
* - Könnyen bővíthető új mapping stratégiákkal
|
|
95
|
+
*
|
|
96
|
+
* Előnyök:
|
|
97
|
+
* - DRY: Nem ismétlődik a mapping kód minden metódusban
|
|
98
|
+
* - Tesztelhetőség: Izoláltan tesztelhető a mapping logika
|
|
99
|
+
* - Karbantarthatóság: Egy helyen módosítható a mapping
|
|
100
|
+
*/
|
|
101
|
+
declare class EntityMapperHelper {
|
|
102
|
+
/**
|
|
103
|
+
* Map a single document snapshot to an entity
|
|
104
|
+
*/
|
|
105
|
+
static mapDocument<T extends Entity>(docSnapshot: QueryDocumentSnapshot<DocumentData>): T;
|
|
106
|
+
/**
|
|
107
|
+
* Map a single document snapshot, excluding uid from data
|
|
108
|
+
*/
|
|
109
|
+
static mapDocumentWithoutUid<T extends EntityModel>(docSnapshot: QueryDocumentSnapshot<DocumentData>): T;
|
|
110
|
+
/**
|
|
111
|
+
* Map multiple document snapshots to entities
|
|
112
|
+
*/
|
|
113
|
+
static mapDocuments<T extends Entity>(docs: QueryDocumentSnapshot<DocumentData>[]): T[];
|
|
114
|
+
/**
|
|
115
|
+
* Map multiple document snapshots, excluding uid from data
|
|
116
|
+
*/
|
|
117
|
+
static mapDocumentsWithoutUid<T extends EntityModel>(docs: QueryDocumentSnapshot<DocumentData>[]): T[];
|
|
118
|
+
/**
|
|
119
|
+
* Apply client-side search filtering if needed
|
|
120
|
+
* Only applies when search has multiple fields (first field is server-side)
|
|
121
|
+
*/
|
|
122
|
+
static applySearchFilter<T extends Entity>(entities: T[], search?: SearchOptions): T[];
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Helper for processing and preparing QueryOptions
|
|
127
|
+
*
|
|
128
|
+
* Design Pattern: Strategy Pattern (előkészítő)
|
|
129
|
+
* - Különböző query előkészítési stratégiák (alap, paginált)
|
|
130
|
+
* - A konkrét végrehajtás a QueryConstraintBuilder-re delegálódik
|
|
131
|
+
* - Könnyen bővíthető új stratégiákkal (pl. aggregált, real-time)
|
|
132
|
+
*
|
|
133
|
+
* Előnyök:
|
|
134
|
+
* - Szétválasztás: Query előkészítés és végrehajtás külön
|
|
135
|
+
* - Újrafelhasználhatóság: Azonos előkészítés több metódusban
|
|
136
|
+
* - Tesztelhetőség: Az előkészítési logika izoláltan tesztelhető
|
|
137
|
+
*/
|
|
138
|
+
declare class QueryOptionsHelper {
|
|
139
|
+
/**
|
|
140
|
+
* Prepare query options with defaults and search filters
|
|
141
|
+
*/
|
|
142
|
+
static prepare(options: QueryOptions): QueryOptions;
|
|
143
|
+
/**
|
|
144
|
+
* Prepare options for pagination with custom page size
|
|
145
|
+
*/
|
|
146
|
+
static prepareForPagination(options: QueryOptions, pageSize: number, startAfterCursor?: string): QueryOptions;
|
|
147
|
+
/**
|
|
148
|
+
* Check if search requires client-side filtering
|
|
149
|
+
* (when multiple fields are specified, only first is server-side)
|
|
150
|
+
*/
|
|
151
|
+
static requiresClientSideSearch(options: QueryOptions): boolean;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Builds Firestore QueryConstraints from QueryOptions
|
|
156
|
+
*
|
|
157
|
+
* Design Pattern: Builder Pattern
|
|
158
|
+
* - Lépésről lépésre építi fel a komplex Firestore query-t
|
|
159
|
+
* - Elválasztja a konstrukciós logikát a reprezentációtól
|
|
160
|
+
* - Különböző constraint típusok moduláris kezelése
|
|
161
|
+
*
|
|
162
|
+
* Előnyök:
|
|
163
|
+
* - Olvashatóság: A query építés lépései világosak
|
|
164
|
+
* - Bővíthetőség: Új constraint típusok könnyen hozzáadhatók
|
|
165
|
+
* - Tesztelhetőség: Minden builder metódus külön tesztelhető
|
|
166
|
+
* - Firestore szabályok: A constraint sorrend automatikusan helyes
|
|
167
|
+
* (where → orderBy → cursor → limit)
|
|
168
|
+
*/
|
|
169
|
+
declare class QueryConstraintBuilder {
|
|
170
|
+
/**
|
|
171
|
+
* Build all query constraints from QueryOptions
|
|
172
|
+
*/
|
|
173
|
+
static build(options: QueryOptions): QueryConstraint[];
|
|
174
|
+
/**
|
|
175
|
+
* Build where filter constraints
|
|
176
|
+
*/
|
|
177
|
+
static buildWhereConstraints(options: QueryOptions): QueryConstraint[];
|
|
178
|
+
/**
|
|
179
|
+
* Build orderBy constraints
|
|
180
|
+
*/
|
|
181
|
+
static buildOrderByConstraints(options: QueryOptions): QueryConstraint[];
|
|
182
|
+
/**
|
|
183
|
+
* Build cursor-based pagination constraints
|
|
184
|
+
* Note: Requires orderBy to be set for proper cursor pagination
|
|
185
|
+
*/
|
|
186
|
+
static buildCursorConstraints(options: QueryOptions): QueryConstraint[];
|
|
187
|
+
/**
|
|
188
|
+
* Build limit constraints
|
|
189
|
+
*/
|
|
190
|
+
static buildLimitConstraints(options: QueryOptions): QueryConstraint[];
|
|
191
|
+
/**
|
|
192
|
+
* Apply default limit if none specified
|
|
193
|
+
*/
|
|
194
|
+
static applyDefaultLimit(options: QueryOptions): QueryOptions;
|
|
195
|
+
/**
|
|
196
|
+
* Build constraints for fetching one extra item to determine hasNext
|
|
197
|
+
*/
|
|
198
|
+
static buildWithExtraForPagination(options: QueryOptions): QueryConstraint[];
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Utility functions for converting query parameters to QueryOptions
|
|
203
|
+
* Separated for better testability and reusability
|
|
204
|
+
*/
|
|
205
|
+
declare class QueryParamsUtil {
|
|
206
|
+
/**
|
|
207
|
+
* Convert path parameters and query parameters into QueryOptions
|
|
208
|
+
* This allows the list method to work with different implementations (Firestore, REST, etc.)
|
|
209
|
+
*/
|
|
210
|
+
static buildQueryOptionsFromParams(pathParams: string[], queryParams: KeyValue<string, string>[]): QueryOptions;
|
|
211
|
+
/**
|
|
212
|
+
* Parse filter value to appropriate type (string, number, boolean, etc.)
|
|
213
|
+
*/
|
|
214
|
+
static parseFilterValue(value: string): any;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Utility for Firestore text search operations
|
|
219
|
+
* Note: Firestore doesn't support full-text search natively.
|
|
220
|
+
* This implements prefix-based search using >= and < operators.
|
|
221
|
+
*/
|
|
222
|
+
declare class SearchUtil {
|
|
223
|
+
/**
|
|
224
|
+
* Validates search options
|
|
225
|
+
* Returns true if search term meets minimum length requirement
|
|
226
|
+
*/
|
|
227
|
+
static isValidSearch(search: SearchOptions | undefined): boolean;
|
|
228
|
+
/**
|
|
229
|
+
* Build Firestore-compatible prefix search filters for a single field
|
|
230
|
+
* Uses >= term and < term + high Unicode character for prefix matching
|
|
231
|
+
*
|
|
232
|
+
* Example: searching "John" will match "John", "Johnny", "Johnson"
|
|
233
|
+
*/
|
|
234
|
+
static buildPrefixSearchFilter(field: string, term: string): QueryFilter[];
|
|
235
|
+
/**
|
|
236
|
+
* Build search filters for the first searchable field
|
|
237
|
+
* Note: Firestore only supports range queries on a single field,
|
|
238
|
+
* so we can only search one field at a time with prefix matching
|
|
239
|
+
*/
|
|
240
|
+
static buildSearchFilters(search: SearchOptions): QueryFilter[];
|
|
241
|
+
/**
|
|
242
|
+
* Check if an entity matches search criteria (client-side filtering)
|
|
243
|
+
* Used for additional fields that couldn't be queried server-side
|
|
244
|
+
*/
|
|
245
|
+
static matchesSearch(entity: Record<string, unknown>, search: SearchOptions): boolean;
|
|
246
|
+
/**
|
|
247
|
+
* Filter entities client-side for multi-field search
|
|
248
|
+
* Use this after fetching data when you need to search multiple fields
|
|
249
|
+
*/
|
|
250
|
+
static filterBySearch<T extends Record<string, unknown>>(entities: T[], search: SearchOptions): T[];
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
export { EntityMapperHelper, FirestoreRepositoryEngine, QueryConstraintBuilder, QueryOptionsHelper, QueryParamsUtil, SearchUtil, provideFirestoreEngine };
|