@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":";;;;;;AAOA;;;;AAIG;MACU,UAAU,CAAA;AACrB;;;AAGG;IACH,OAAO,aAAa,CAAC,MAAiC,EAAA;QACpD,IAAI,CAAC,MAAM,EAAE;AACX,YAAA,OAAO,KAAK;QACd;AACA,QAAA,OAAO,iBAAiB,CAAC,MAAM,CAAC,IAAI,CAAC,IAAI,MAAM,CAAC,MAAM,CAAC,MAAM,GAAG,CAAC;IACnE;AAEA;;;;;AAKG;AACH,IAAA,OAAO,uBAAuB,CAAC,KAAa,EAAE,IAAY,EAAA;AACxD,QAAA,MAAM,cAAc,GAAG,mBAAmB,CAAC,IAAI,CAAC;AAChD,QAAA,MAAM,OAAO,GAAG,cAAc,GAAG,QAAQ,CAAC;QAE1C,OAAO;YACL,EAAE,KAAK,EAAE,QAAQ,EAAE,IAAI,EAAE,KAAK,EAAE,cAAc,EAAE;YAChD,EAAE,KAAK,EAAE,QAAQ,EAAE,GAAG,EAAE,KAAK,EAAE,OAAO,EAAE;SACzC;IACH;AAEA;;;;AAIG;IACH,OAAO,kBAAkB,CAAC,MAAqB,EAAA;QAC7C,IAAI,CAAC,IAAI,CAAC,aAAa,CAAC,MAAM,CAAC,EAAE;AAC/B,YAAA,OAAO,EAAE;QACX;;QAGA,MAAM,YAAY,GAAG,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC;QACrC,OAAO,IAAI,CAAC,uBAAuB,CAAC,YAAY,EAAE,MAAM,CAAC,IAAI,CAAC;IAChE;AAEA;;;AAGG;AACH,IAAA,OAAO,aAAa,CAAC,MAA+B,EAAE,MAAqB,EAAA;QACzE,IAAI,CAAC,IAAI,CAAC,aAAa,CAAC,MAAM,CAAC,EAAE;YAC/B,OAAO,IAAI,CAAC;QACd;QAEA,MAAM,cAAc,GAAG,mBAAmB,CAAC,MAAM,CAAC,IAAI,CAAC;QAEvD,OAAO,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,KAAK,KAAI;AAClC,YAAA,MAAM,KAAK,GAAG,MAAM,CAAC,KAAK,CAAC;AAC3B,YAAA,IAAI,OAAO,KAAK,KAAK,QAAQ,EAAE;AAC7B,gBAAA,OAAO,KAAK;YACd;YACA,OAAO,KAAK,CAAC,WAAW,EAAE,CAAC,QAAQ,CAAC,cAAc,CAAC;AACrD,QAAA,CAAC,CAAC;IACJ;AAEA;;;AAGG;AACH,IAAA,OAAO,cAAc,CACnB,QAAa,EACb,MAAqB,EAAA;QAErB,IAAI,CAAC,IAAI,CAAC,aAAa,CAAC,MAAM,CAAC,EAAE;AAC/B,YAAA,OAAO,QAAQ;QACjB;AAEA,QAAA,OAAO,QAAQ,CAAC,MAAM,CAAC,CAAC,MAAM,KAAK,IAAI,CAAC,aAAa,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACxE;AACD;;ACnFD;;;;;;;;;;;;AAYG;MACU,kBAAkB,CAAA;AAC7B;;AAEG;IACH,OAAO,WAAW,CAAmB,WAAgD,EAAA;QACnF,OAAO;YACL,GAAG,WAAW,CAAC,IAAI,EAAE;YACrB,GAAG,EAAE,WAAW,CAAC,EAAE;SACf;IACR;AAEA;;AAEG;IACH,OAAO,qBAAqB,CAC1B,WAAgD,EAAA;QAEhD,MAAM,EAAE,GAAG,EAAE,GAAG,IAAI,EAAE,GAAG,WAAW,CAAC,IAAI,EAAiB;QAC1D,OAAO;AACL,YAAA,GAAG,IAAI;YACP,GAAG,EAAE,WAAW,CAAC,EAAE;SACf;IACR;AAEA;;AAEG;IACH,OAAO,YAAY,CAAmB,IAA2C,EAAA;AAC/E,QAAA,OAAO,IAAI,CAAC,GAAG,CAAC,CAAC,WAAW,KAAK,IAAI,CAAC,WAAW,CAAI,WAAW,CAAC,CAAC;IACpE;AAEA;;AAEG;IACH,OAAO,sBAAsB,CAC3B,IAA2C,EAAA;AAE3C,QAAA,OAAO,IAAI,CAAC,GAAG,CAAC,CAAC,WAAW,KAAK,IAAI,CAAC,qBAAqB,CAAI,WAAW,CAAC,CAAC;IAC9E;AAEA;;;AAGG;AACH,IAAA,OAAO,iBAAiB,CAAmB,QAAa,EAAE,MAAsB,EAAA;AAC9E,QAAA,IAAI,CAAC,MAAM,IAAI,CAAC,UAAU,CAAC,aAAa,CAAC,MAAM,CAAC,IAAI,MAAM,CAAC,MAAM,CAAC,MAAM,IAAI,CAAC,EAAE;AAC7E,YAAA,OAAO,QAAQ;QACjB;QAEA,OAAO,UAAU,CAAC,cAAc,CAC9B,QAAgD,EAChD,MAAM,CACW;IACrB;AACD;;AC7DD;;;;;;;;;;;;;;AAcG;MACU,sBAAsB,CAAA;AACjC;;AAEG;IACH,OAAO,KAAK,CAAC,OAAqB,EAAA;QAChC,MAAM,WAAW,GAAsB,EAAE;;QAGzC,WAAW,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC,qBAAqB,CAAC,OAAO,CAAC,CAAC;;QAGxD,WAAW,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC,uBAAuB,CAAC,OAAO,CAAC,CAAC;;QAG1D,WAAW,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC,sBAAsB,CAAC,OAAO,CAAC,CAAC;;QAGzD,WAAW,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC,qBAAqB,CAAC,OAAO,CAAC,CAAC;AAExD,QAAA,OAAO,WAAW;IACpB;AAEA;;AAEG;IACH,OAAO,qBAAqB,CAAC,OAAqB,EAAA;AAChD,QAAA,IAAI,CAAC,OAAO,CAAC,KAAK,IAAI,OAAO,CAAC,KAAK,CAAC,MAAM,KAAK,CAAC,EAAE;AAChD,YAAA,OAAO,EAAE;QACX;QAEA,OAAO,OAAO,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,MAAM,KAAK,KAAK,CAAC,MAAM,CAAC,KAAK,EAAE,MAAM,CAAC,QAAQ,EAAE,MAAM,CAAC,KAAK,CAAC,CAAC;IAC1F;AAEA;;AAEG;IACH,OAAO,uBAAuB,CAAC,OAAqB,EAAA;AAClD,QAAA,IAAI,CAAC,OAAO,CAAC,OAAO,IAAI,OAAO,CAAC,OAAO,CAAC,MAAM,KAAK,CAAC,EAAE;AACpD,YAAA,OAAO,EAAE;QACX;QAEA,OAAO,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,IAAI,KAAK,OAAO,CAAC,IAAI,CAAC,KAAK,EAAE,IAAI,CAAC,SAAS,CAAC,CAAC;IAC3E;AAEA;;;AAGG;IACH,OAAO,sBAAsB,CAAC,OAAqB,EAAA;QACjD,MAAM,WAAW,GAAsB,EAAE;AAEzC,QAAA,IAAI,OAAO,CAAC,UAAU,KAAK,SAAS,EAAE;YACpC,WAAW,CAAC,IAAI,CAAC,UAAU,CAAC,OAAO,CAAC,UAAU,CAAC,CAAC;QAClD;AAEA,QAAA,IAAI,OAAO,CAAC,OAAO,KAAK,SAAS,EAAE;YACjC,WAAW,CAAC,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC;QAC5C;AAEA,QAAA,IAAI,OAAO,CAAC,SAAS,KAAK,SAAS,EAAE;YACnC,WAAW,CAAC,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC;QAChD;AAEA,QAAA,IAAI,OAAO,CAAC,KAAK,KAAK,SAAS,EAAE;YAC/B,WAAW,CAAC,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC;QACxC;AAEA,QAAA,OAAO,WAAW;IACpB;AAEA;;AAEG;IACH,OAAO,qBAAqB,CAAC,OAAqB,EAAA;QAChD,MAAM,WAAW,GAAsB,EAAE;AAEzC,QAAA,IAAI,OAAO,CAAC,KAAK,KAAK,SAAS,IAAI,OAAO,CAAC,KAAK,GAAG,CAAC,EAAE;YACpD,WAAW,CAAC,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC;QACxC;AAEA,QAAA,OAAO,WAAW;IACpB;AAEA;;AAEG;IACH,OAAO,iBAAiB,CAAC,OAAqB,EAAA;AAC5C,QAAA,IAAI,OAAO,CAAC,KAAK,KAAK,SAAS,EAAE;YAC/B,OAAO,EAAE,GAAG,OAAO,EAAE,KAAK,EAAE,iBAAiB,EAAE;QACjD;AACA,QAAA,OAAO,OAAO;IAChB;AAEA;;AAEG;IACH,OAAO,2BAA2B,CAAC,OAAqB,EAAA;AACtD,QAAA,MAAM,gBAAgB,GAAG;AACvB,YAAA,GAAG,OAAO;YACV,KAAK,EAAE,CAAC,OAAO,CAAC,KAAK,IAAI,iBAAiB,IAAI,CAAC;SAChD;AACD,QAAA,OAAO,IAAI,CAAC,KAAK,CAAC,gBAAgB,CAAC;IACrC;AACD;;AC7HD;;;;;;;;;;;;AAYG;MACU,kBAAkB,CAAA;AAC7B;;AAEG;IACH,OAAO,OAAO,CAAC,OAAqB,EAAA;;QAElC,IAAI,QAAQ,GAAG,sBAAsB,CAAC,iBAAiB,CAAC,OAAO,CAAC;;AAGhE,QAAA,IAAI,OAAO,CAAC,MAAM,IAAI,UAAU,CAAC,aAAa,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE;YAC9D,MAAM,aAAa,GAAG,UAAU,CAAC,kBAAkB,CAAC,OAAO,CAAC,MAAM,CAAC;AACnE,YAAA,QAAQ,GAAG;AACT,gBAAA,GAAG,QAAQ;AACX,gBAAA,KAAK,EAAE,CAAC,IAAI,QAAQ,CAAC,KAAK,IAAI,EAAE,CAAC,EAAE,GAAG,aAAa,CAAC;aACrD;QACH;AAEA,QAAA,OAAO,QAAQ;IACjB;AAEA;;AAEG;AACH,IAAA,OAAO,oBAAoB,CACzB,OAAqB,EACrB,QAAgB,EAChB,gBAAyB,EAAA;AAEzB,QAAA,MAAM,QAAQ,GAAiB;AAC7B,YAAA,GAAG,OAAO;AACV,YAAA,KAAK,EAAE,QAAQ;AACf,YAAA,UAAU,EAAE,gBAAgB;SAC7B;;AAGD,QAAA,IAAI,OAAO,CAAC,MAAM,IAAI,UAAU,CAAC,aAAa,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE;YAC9D,MAAM,aAAa,GAAG,UAAU,CAAC,kBAAkB,CAAC,OAAO,CAAC,MAAM,CAAC;AACnE,YAAA,QAAQ,CAAC,KAAK,GAAG,CAAC,IAAI,QAAQ,CAAC,KAAK,IAAI,EAAE,CAAC,EAAE,GAAG,aAAa,CAAC;QAChE;AAEA,QAAA,OAAO,QAAQ;IACjB;AAEA;;;AAGG;IACH,OAAO,wBAAwB,CAAC,OAAqB,EAAA;AACnD,QAAA,OAAO,CAAC,EACN,OAAO,CAAC,MAAM;AACd,YAAA,UAAU,CAAC,aAAa,CAAC,OAAO,CAAC,MAAM,CAAC;YACxC,OAAO,CAAC,MAAM,CAAC,MAAM,CAAC,MAAM,GAAG,CAAC,CACjC;IACH;AACD;;ACrED;;;AAGG;MACU,eAAe,CAAA;AAC1B;;;AAGG;AACH,IAAA,OAAO,2BAA2B,CAChC,UAAoB,EACpB,WAAuC,EAAA;AAEvC,QAAA,MAAM,OAAO,GAAiB;AAC5B,YAAA,KAAK,EAAE,EAAE;AACT,YAAA,OAAO,EAAE,EAAE;SACZ;;QAGD,IAAI,WAAW,IAAI,WAAW,CAAC,MAAM,GAAG,CAAC,EAAE;AACzC,YAAA,KAAK,MAAM,KAAK,IAAI,WAAW,EAAE;AAC/B,gBAAA,MAAM,EAAE,GAAG,EAAE,KAAK,EAAE,GAAG,KAAK;AAE5B,gBAAA,QAAQ,GAAG,CAAC,WAAW,EAAE;AACvB,oBAAA,KAAK,OAAO;wBACV,MAAM,UAAU,GAAG,QAAQ,CAAC,KAAK,EAAE,EAAE,CAAC;;wBAEtC,IAAI,CAAC,KAAK,CAAC,UAAU,CAAC,IAAI,UAAU,IAAI,CAAC,EAAE;AACzC,4BAAA,OAAO,CAAC,KAAK,GAAG,UAAU;wBAC5B;wBACA;AAEF,oBAAA,KAAK,QAAQ;wBACX,MAAM,WAAW,GAAG,QAAQ,CAAC,KAAK,EAAE,EAAE,CAAC;wBACvC,IAAI,CAAC,KAAK,CAAC,WAAW,CAAC,IAAI,WAAW,IAAI,CAAC,EAAE;AAC3C,4BAAA,OAAO,CAAC,MAAM,GAAG,WAAW;wBAC9B;wBACA;AAEF,oBAAA,KAAK,MAAM;AACX,oBAAA,KAAK,QAAQ;AACb,oBAAA,KAAK,SAAS;;AAEZ,wBAAA,MAAM,CAAC,KAAK,EAAE,SAAS,GAAG,KAAK,CAAC,GAAG,KAAK,CAAC,KAAK,CAAC,GAAG,CAAC;wBACnD,IAAI,KAAK,EAAE;4BACT,OAAO,CAAC,OAAO,GAAG,OAAO,CAAC,OAAO,IAAI,EAAE;AACvC,4BAAA,OAAO,CAAC,OAAO,CAAC,IAAI,CAAC;AACnB,gCAAA,KAAK,EAAE,KAAK,CAAC,IAAI,EAAE;AACnB,gCAAA,SAAS,EAAE,SAAS,CAAC,IAAI,EAAE,CAAC,WAAW,EAAE,KAAK,MAAM,GAAG,MAAM,GAAG,KAAK;AACtE,6BAAA,CAAC;wBACJ;wBACA;;AAGF,oBAAA;AACE,wBAAA,IAAI,GAAG,IAAI,KAAK,EAAE;4BAChB,OAAO,CAAC,KAAK,GAAG,OAAO,CAAC,KAAK,IAAI,EAAE;;;AAInC,4BAAA,IAAI,KAAK,CAAC,UAAU,CAAC,KAAK,CAAC,EAAE;gCAC3B,MAAM,SAAS,GAAG,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;gCACjC,MAAM,MAAM,GAAG;qCACZ,KAAK,CAAC,GAAG;AACT,qCAAA,GAAG,CAAC,CAAC,CAAC,KAAK,eAAe,CAAC,gBAAgB,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC;AACzD,gCAAA,OAAO,CAAC,KAAK,CAAC,IAAI,CAAC;AACjB,oCAAA,KAAK,EAAE,GAAG;AACV,oCAAA,QAAQ,EAAE,IAAI;AACd,oCAAA,KAAK,EAAE,MAAM;AACd,iCAAA,CAAC;4BACJ;;iCAEK;gCACH,MAAM,eAAe,GAAG,KAAK,CAAC,KAAK,CAAC,sBAAsB,CAAC;gCAC3D,IAAI,eAAe,EAAE;oCACnB,MAAM,GAAG,QAAQ,EAAE,WAAW,CAAC,GAAG,eAAe;AACjD,oCAAA,OAAO,CAAC,KAAK,CAAC,IAAI,CAAC;AACjB,wCAAA,KAAK,EAAE,GAAG;AACV,wCAAA,QAAQ,EAAE,QAAe;AACzB,wCAAA,KAAK,EAAE,eAAe,CAAC,gBAAgB,CAAC,WAAW,CAAC;AACrD,qCAAA,CAAC;gCACJ;qCAAO;;AAEL,oCAAA,OAAO,CAAC,KAAK,CAAC,IAAI,CAAC;AACjB,wCAAA,KAAK,EAAE,GAAG;AACV,wCAAA,QAAQ,EAAE,IAAI;AACd,wCAAA,KAAK,EAAE,eAAe,CAAC,gBAAgB,CAAC,KAAK,CAAC;AAC/C,qCAAA,CAAC;gCACJ;4BACF;wBACF;wBACA;;YAEN;QACF;;QAGA,IAAI,UAAU,IAAI,UAAU,CAAC,MAAM,GAAG,CAAC,EAAE;;;;AAIvC,YAAA,OAAO,CAAC,GAAG,CAAC,2BAA2B,EAAE,UAAU,CAAC;QACtD;AAEA,QAAA,OAAO,OAAO;IAChB;AAEA;;AAEG;IACH,OAAO,gBAAgB,CAAC,KAAa,EAAA;;AAEnC,QAAA,MAAM,QAAQ,GAAG,UAAU,CAAC,KAAK,CAAC;QAClC,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,IAAI,QAAQ,CAAC,QAAQ,CAAC,IAAI,KAAK,CAAC,IAAI,EAAE,KAAK,QAAQ,CAAC,QAAQ,EAAE,EAAE;AAClF,YAAA,OAAO,QAAQ;QACjB;;AAGA,QAAA,IAAI,KAAK,CAAC,WAAW,EAAE,KAAK,MAAM;AAAE,YAAA,OAAO,IAAI;AAC/C,QAAA,IAAI,KAAK,CAAC,WAAW,EAAE,KAAK,OAAO;AAAE,YAAA,OAAO,KAAK;;AAGjD,QAAA,IAAI,KAAK,CAAC,WAAW,EAAE,KAAK,MAAM;AAAE,YAAA,OAAO,IAAI;AAC/C,QAAA,IAAI,KAAK,CAAC,WAAW,EAAE,KAAK,WAAW;AAAE,YAAA,OAAO,SAAS;;AAGzD,QAAA,OAAO,KAAK;IACd;AACD;;ACzFK,MAAO,yBAA0B,SAAQ,gBAI9C,CAAA;AAMa,IAAA,SAAA;AACA,IAAA,UAAA;AACA,IAAA,oBAAA;AAPJ,IAAA,QAAQ,GAAG,MAAM,CAAC,QAAQ,CAAC;AAEzB,IAAA,qBAAqB;AAE/B,IAAA,WAAA,CACY,SAAoB,EACpB,UAAmC,EACnC,oBAA2C,EAAA;AAErD,QAAA,KAAK,EAAE;QAJG,IAAA,CAAA,SAAS,GAAT,SAAS;QACT,IAAA,CAAA,UAAU,GAAV,UAAU;QACV,IAAA,CAAA,oBAAoB,GAApB,oBAAoB;;AAK9B,QAAA,IAAI,CAAC,qBAAqB,GAAG,IAAI,CAAC,iBAAiB,EAAE;IACvD;AAEA;;;;AAIG;AACH,IAAA,IAAc,kBAAkB,GAAA;AAC9B,QAAA,OAAO,OAAO,IAAI,CAAC,UAAU,KAAK,UAAU,GAAG,IAAI,CAAC,UAAU,EAAE,GAAG,IAAI,CAAC,UAAU;IACpF;AAEA;;;;AAIG;AACH,IAAA,IAAc,aAAa,GAAA;QACzB,OAAO,UAAU,CAAC,IAAI,CAAC,SAAS,EAAE,IAAI,CAAC,kBAAkB,CAAqC;IAChG;AAEA;;AAEG;IACK,iBAAiB,GAAA;AACvB,QAAA,IAAI,CAAC,IAAI,CAAC,oBAAoB,EAAE;;AAE9B,YAAA,OAAO,MAAM,IAAI,CAAC,kBAAkB;QACtC;;QAGA,OAAO,CAAC,UAAiC,KAAI;YAC3C,IAAI,CAAC,UAAU,EAAE;;gBAEf,OAAO,IAAI,CAAC,kBAAkB;YAChC;YAEA,MAAM,QAAQ,GAAI,UAAkB,CAAC,IAAI,CAAC,oBAAqB,CAAC,aAAa,CAAC;YAC9E,IAAI,CAAC,QAAQ,EAAE;gBACb,MAAM,IAAI,KAAK,CACb,CAAA,mCAAA,EAAsC,IAAI,CAAC,oBAAqB,CAAC,aAAa,CAAA,YAAA,CAAc,CAC7F;YACH;AAEA,YAAA,MAAM,gBAAgB,GAAG,IAAI,CAAC,oBAAqB,CAAC,gBAAgB;AACpE,YAAA,MAAM,UAAU,GACd,OAAO,gBAAgB,KAAK,UAAU,GAAG,gBAAgB,EAAE,GAAG,gBAAgB;YAEhF,OAAO,CAAA,EAAG,UAAU,CAAA,CAAA,EAAI,QAAQ,CAAA,CAAA,EAAI,IAAI,CAAC,oBAAqB,CAAC,aAAa,CAAA,CAAE;AAChF,QAAA,CAAC;IACH;AAEA;;AAEG;AACK,IAAA,gBAAgB,CAAC,UAAiC,EAAA;QACxD,MAAM,IAAI,GAAG,IAAI,CAAC,qBAAqB,CAAC,UAAU,CAAC;QACnD,OAAO,UAAU,CAAC,IAAI,CAAC,SAAS,EAAE,IAAI,CAAqC;IAC7E;AAEgB,IAAA,KAAK,CAAC,UAAoC,EAAA;AACxD,QAAA,OAAO,IAAI,UAAU,CAAO,CAAC,UAAU,KAAI;YACzC,MAAM,KAAK,GAAI,IAAI,CAAC,SAAiB,CAAC,KAAK,EAAE;AAC7C,YAAA,KAAK,MAAM,EAAE,IAAI,UAAU,EAAE;gBAC3B,IAAI,CAAC,EAAE,CAAC,GAAG;AAAE,oBAAA,SAAS;AACtB,gBAAA,MAAM,GAAG,GAAG,GAAG,CAAC,IAAI,CAAC,aAAa,EAAE,EAAE,CAAC,GAAG,CAAC;AAC3C,gBAAA,QAAQ,EAAE,CAAC,IAAI;AACb,oBAAA,KAAK,QAAQ;AACX,wBAAA,IAAI,EAAE,CAAC,IAAI,EAAE;4BACX,KAAK,CAAC,GAAG,CAAC,GAAG,EAAE,EAAE,CAAC,IAAI,CAAC;wBACzB;wBACA;AACF,oBAAA,KAAK,QAAQ;AACX,wBAAA,IAAI,EAAE,CAAC,IAAI,EAAE;4BACX,KAAK,CAAC,MAAM,CAAC,GAAG,EAAE,EAAE,CAAC,IAAI,CAAC;wBAC5B;wBACA;AACF,oBAAA,KAAK,QAAQ;AACX,wBAAA,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC;wBACjB;;YAEN;YACA;AACG,iBAAA,MAAM;iBACN,IAAI,CAAC,MAAK;gBACT,UAAU,CAAC,IAAI,EAAE;gBACjB,UAAU,CAAC,QAAQ,EAAE;AACvB,YAAA,CAAC;AACA,iBAAA,KAAK,CAAC,CAAC,KAAc,KAAK,UAAU,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;AACvD,QAAA,CAAC,CAAC;IACJ;AAEgB,IAAA,KAAK,CAAC,OAAsB,EAAA;AAC1C,QAAA,OAAO,IAAI,UAAU,CAAS,CAAC,UAAU,KAAI;YAC3C,MAAM,WAAW,GAAsB,EAAE;AACzC,YAAA,IAAI,OAAO,EAAE,KAAK,EAAE;AAClB,gBAAA,KAAK,MAAM,MAAM,IAAI,OAAO,CAAC,KAAK,EAAE;AAClC,oBAAA,WAAW,CAAC,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,KAAK,EAAE,MAAM,CAAC,QAAQ,EAAE,MAAM,CAAC,KAAK,CAAC,CAAC;gBACtE;YACF;YACA,MAAM,aAAa,GAAG,KAAK,CAAC,IAAI,CAAC,aAAa,EAAE,GAAG,WAAW,CAAC;;YAG/D,kBAAkB,CAAC,aAAa;AAC7B,iBAAA,IAAI,CAAC,CAAC,QAAQ,KAAI;gBACjB,UAAU,CAAC,IAAI,CAAC,QAAQ,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC;gBACtC,UAAU,CAAC,QAAQ,EAAE;AACvB,YAAA,CAAC;AACA,iBAAA,KAAK,CAAC,CAAC,KAAK,KAAI;;AAEf,gBAAA,OAAO,CAAC,IAAI,CAAC,qDAAqD,EAAE,KAAK,CAAC;gBAC1E,OAAO,CAAC,aAAa;AAClB,qBAAA,IAAI,CAAC,CAAC,WAAW,KAAI;AACpB,oBAAA,UAAU,CAAC,IAAI,CAAC,WAAW,CAAC,IAAI,CAAC;oBACjC,UAAU,CAAC,QAAQ,EAAE;AACvB,gBAAA,CAAC;AACA,qBAAA,KAAK,CAAC,CAAC,aAAa,KAAK,UAAU,CAAC,KAAK,CAAC,aAAa,CAAC,CAAC;AAC9D,YAAA,CAAC,CAAC;AACN,QAAA,CAAC,CAAC;IACJ;AAEgB,IAAA,OAAO,CAAC,SAAyB,EAAA;AAC/C,QAAA,OAAO,qBAAqB,CAAC,IAAI,CAAC,QAAQ,EAAE,MAAK;AAC/C,YAAA,MAAM,GAAG,GAAG,GAAG,CAAC,UAAU,CAAC,IAAI,CAAC,SAAS,EAAE,IAAI,CAAC,CAAC,CAAC,EAAE;AACpD,YAAA,MAAM,SAAS,GAAG;AAChB,gBAAA,GAAG,SAAS;gBACZ,GAAG;aACJ;;YAGD,MAAM,aAAa,GAAG,IAAI,CAAC,gBAAgB,CAAC,SAAS,CAAC;AAEtD,YAAA,OAAO,IAAI,UAAU,CAAc,CAAC,UAAU,KAAI;gBAChD,MAAM,CAAC,GAAG,CAAC,aAAa,EAAE,GAAG,CAAC,EAAE,SAAS;qBACtC,IAAI,CAAC,MAAK;oBACT,UAAU,CAAC,IAAI,CAAC,EAAE,GAAG,SAAS,EAAiB,CAAC;oBAChD,UAAU,CAAC,QAAQ,EAAE;AACvB,gBAAA,CAAC;AACA,qBAAA,KAAK,CAAC,CAAC,KAAK,KAAI;AACf,oBAAA,UAAU,CAAC,KAAK,CAAC,KAAK,CAAC;AACzB,gBAAA,CAAC,CAAC;AACN,YAAA,CAAC,CAAC;AACJ,QAAA,CAAC,CAAC;IACJ;AAEgB,IAAA,OAAO,CAAC,MAAmB,EAAA;AACzC,QAAA,OAAO,qBAAqB,CAAC,IAAI,CAAC,QAAQ,EAAE,MAAK;;YAE/C,MAAM,aAAa,GAAG,IAAI,CAAC,gBAAgB,CAAC,MAAM,CAAC;AAEnD,YAAA,OAAO,IAAI,UAAU,CAAc,CAAC,UAAU,KAAI;gBAChD,SAAS,CAAC,GAAG,CAAC,aAAa,EAAE,MAAM,CAAC,GAAG,CAAC;qBACrC,IAAI,CAAC,MAAK;AACT,oBAAA,UAAU,CAAC,IAAI,CAAC,MAAM,CAAC;oBACvB,UAAU,CAAC,QAAQ,EAAE;AACvB,gBAAA,CAAC;AACA,qBAAA,KAAK,CAAC,CAAC,KAAK,KAAK,UAAU,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;AAC9C,YAAA,CAAC,CAAC;AACJ,QAAA,CAAC,CAAC;IACJ;AAEgB,IAAA,MAAM,CAAC,GAAW,EAAA;AAChC,QAAA,OAAO,IAAI,UAAU,CAAU,CAAC,UAAU,KAAI;AAC5C,YAAA,OAAO,CAAC,KAAK,CAAC,IAAI,CAAC,aAAa,EAAE,KAAK,CAAC,KAAK,EAAE,IAAI,EAAE,GAAG,CAAC,CAAC;AACvD,iBAAA,IAAI,CAAC,CAAC,QAAQ,KAAI;gBACjB,UAAU,CAAC,IAAI,CAAC,CAAC,QAAQ,CAAC,KAAK,CAAC;gBAChC,UAAU,CAAC,QAAQ,EAAE;AACvB,YAAA,CAAC;AACA,iBAAA,KAAK,CAAC,CAAC,KAAK,KAAK,UAAU,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;AAC9C,QAAA,CAAC,CAAC;IACJ;IAEgB,KAAK,CACnB,UAAoB,EACpB,WAAuC,EAAA;;QAGvC,MAAM,YAAY,GAAG,eAAe,CAAC,2BAA2B,CAAC,UAAU,EAAE,WAAW,CAAC;;AAGzF,QAAA,OAAO,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC;IACjC;AAEO,IAAA,UAAU,CAAC,GAAa,EAAA;AAC7B,QAAA,OAAO,qBAAqB,CAAC,IAAI,CAAC,QAAQ,EAAE,MAAK;AAC/C,YAAA,IAAI,GAAG,CAAC,MAAM,KAAK,CAAC,EAAE;AACpB,gBAAA,OAAO,IAAI,UAAU,CAAgB,CAAC,UAAU,KAAI;AAClD,oBAAA,UAAU,CAAC,IAAI,CAAC,EAAE,CAAC;oBACnB,UAAU,CAAC,QAAQ,EAAE;AACvB,gBAAA,CAAC,CAAC;YACJ;AAEA,YAAA,MAAM,aAAa,GAAG,KAAK,CAAC,IAAI,CAAC,aAAa,EAAE,KAAK,CAAC,KAAK,EAAE,IAAI,EAAE,GAAG,CAAC,CAAC;AAExE,YAAA,OAAO,IAAI,UAAU,CAAgB,CAAC,UAAU,KAAI;gBAClD,OAAO,CAAC,aAAa;AAClB,qBAAA,IAAI,CAAC,CAAC,QAAQ,KAAI;oBACjB,MAAM,QAAQ,GAAG,kBAAkB,CAAC,sBAAsB,CAAc,QAAQ,CAAC,IAAI,CAAC;AACtF,oBAAA,UAAU,CAAC,IAAI,CAAC,QAAQ,CAAC;oBACzB,UAAU,CAAC,QAAQ,EAAE;AACvB,gBAAA,CAAC;AACA,qBAAA,KAAK,CAAC,CAAC,KAAK,KAAK,UAAU,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;AAC9C,YAAA,CAAC,CAAC;AACJ,QAAA,CAAC,CAAC;IACJ;AAEgB,IAAA,KAAK,CAAC,GAAW,EAAA;AAC/B,QAAA,OAAO,qBAAqB,CAAC,IAAI,CAAC,QAAQ,EAAE,MAAK;AAC/C,YAAA,MAAM,cAAc,GAAG,GAAG,CAAC,IAAI,CAAC,SAAS,EAAE,CAAA,EAAG,IAAI,CAAC,kBAAkB,CAAA,CAAA,EAAI,GAAG,CAAA,CAAE,CAAC;YAE/E,OAAO,OAAO,CAAC,cAAc,EAAE;AAC7B,gBAAA,OAAO,EAAE,KAAK;AACf,aAAA,CAA4B;AAC/B,QAAA,CAAC,CAAC;IACJ;AAEgB,IAAA,KAAK,CAAC,OAAqB,EAAA;AACzC,QAAA,OAAO,IAAI,UAAU,CAAW,CAAC,UAAU,KAAI;;YAE7C,MAAM,YAAY,GAAG,kBAAkB,CAAC,OAAO,CAAC,OAAO,CAAC;;YAGxD,MAAM,WAAW,GAAG,sBAAsB,CAAC,KAAK,CAAC,YAAY,CAAC;YAC9D,MAAM,aAAa,GAAG,KAAK,CAAC,IAAI,CAAC,aAAa,EAAE,GAAG,WAAW,CAAC;YAE/D,OAAO,CAAC,aAAa;AAClB,iBAAA,IAAI,CAAC,CAAC,QAAQ,KAAI;gBACjB,IAAI,QAAQ,GAAG,kBAAkB,CAAC,YAAY,CAAS,QAAQ,CAAC,IAAI,CAAC;;gBAGrE,QAAQ,GAAG,kBAAkB,CAAC,iBAAiB,CAAC,QAAQ,EAAE,OAAO,CAAC,MAAM,CAAC;AAEzE,gBAAA,UAAU,CAAC,IAAI,CAAC,QAAQ,CAAC;gBACzB,UAAU,CAAC,QAAQ,EAAE;AACvB,YAAA,CAAC;AACA,iBAAA,KAAK,CAAC,CAAC,KAAK,KAAI;AACf,gBAAA,OAAO,CAAC,KAAK,CAAC,KAAK,CAAC;AACpB,gBAAA,UAAU,CAAC,KAAK,CAAC,KAAK,CAAC;AACzB,YAAA,CAAC,CAAC;AACN,QAAA,CAAC,CAAC;IACJ;AAEgB,IAAA,cAAc,CAC5B,OAAqB,EACrB,QAAgB,EAChB,YAAqB,EAAA;AAErB,QAAA,OAAO,IAAI,UAAU,CAA0B,CAAC,UAAU,KAAI;;AAE5D,YAAA,MAAM,gBAAgB,GAAG,kBAAkB,CAAC,oBAAoB,CAC9D,OAAO,EACP,QAAQ,EACR,YAAY,EAAE,GAAG,CAClB;;YAGD,MAAM,WAAW,GAAG,sBAAsB,CAAC,KAAK,CAAC,gBAAgB,CAAC;YAClE,MAAM,aAAa,GAAG,KAAK,CAAC,IAAI,CAAC,aAAa,EAAE,GAAG,WAAW,CAAC;YAE/D,OAAO,CAAC,aAAa;AAClB,iBAAA,IAAI,CAAC,CAAC,QAAQ,KAAI;gBACjB,IAAI,QAAQ,GAAG,kBAAkB,CAAC,YAAY,CAAS,QAAQ,CAAC,IAAI,CAAC;gBACrE,QAAQ,GAAG,kBAAkB,CAAC,iBAAiB,CAAC,QAAQ,EAAE,OAAO,CAAC,MAAM,CAAC;gBAEzE,UAAU,CAAC,IAAI,CAAC;AACd,oBAAA,KAAK,EAAE,QAAQ;oBACf,UAAU,EAAE,QAAQ,CAAC,MAAM;oBAC3B,UAAU,EAAE,IAAI,CAAC,IAAI,CAAC,QAAQ,CAAC,MAAM,GAAG,QAAQ,CAAC;AACjD,oBAAA,WAAW,EAAE,CAAC;AACd,oBAAA,OAAO,EAAE,QAAQ,CAAC,MAAM,KAAK,QAAQ;oBACrC,WAAW,EAAE,CAAC,CAAC,YAAY;AAC5B,iBAAA,CAAC;gBACF,UAAU,CAAC,QAAQ,EAAE;AACvB,YAAA,CAAC;AACA,iBAAA,KAAK,CAAC,CAAC,KAAK,KAAK,UAAU,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;AAC9C,QAAA,CAAC,CAAC;IACJ;AAEA;;;AAGG;AACI,IAAA,oBAAoB,CAAC,OAAqB,EAAA;AAC/C,QAAA,OAAO,IAAI,UAAU,CAAgC,CAAC,UAAU,KAAI;AAClE,YAAA,MAAM,QAAQ,GAAG,OAAO,CAAC,KAAK,IAAI,iBAAiB;;YAGnD,MAAM,YAAY,GAAG,kBAAkB,CAAC,OAAO,CAAC,OAAO,CAAC;;YAGxD,MAAM,WAAW,GAAG,sBAAsB,CAAC,2BAA2B,CAAC,YAAY,CAAC;YACpF,MAAM,aAAa,GAAG,KAAK,CAAC,IAAI,CAAC,aAAa,EAAE,GAAG,WAAW,CAAC;YAE/D,OAAO,CAAC,aAAa;AAClB,iBAAA,IAAI,CAAC,CAAC,QAAQ,KAAI;gBACjB,IAAI,QAAQ,GAAG,kBAAkB,CAAC,YAAY,CAAS,QAAQ,CAAC,IAAI,CAAC;gBACrE,QAAQ,GAAG,kBAAkB,CAAC,iBAAiB,CAAC,QAAQ,EAAE,OAAO,CAAC,MAAM,CAAC;;AAGzE,gBAAA,MAAM,OAAO,GAAG,QAAQ,CAAC,MAAM,GAAG,QAAQ;gBAC1C,IAAI,OAAO,EAAE;oBACX,QAAQ,GAAG,QAAQ,CAAC,KAAK,CAAC,CAAC,EAAE,QAAQ,CAAC;gBACxC;gBAEA,UAAU,CAAC,IAAI,CAAC;AACd,oBAAA,KAAK,EAAE,QAAQ;oBACf,OAAO;AACP,oBAAA,WAAW,EAAE,CAAC,CAAC,OAAO,CAAC,UAAU;AACjC,oBAAA,WAAW,EAAE,QAAQ,CAAC,MAAM,GAAG,CAAC,GAAG,QAAQ,CAAC,CAAC,CAAC,CAAC,GAAG,GAAG,SAAS;oBAC9D,UAAU,EAAE,QAAQ,CAAC,MAAM,GAAG,CAAC,GAAG,QAAQ,CAAC,QAAQ,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,GAAG,GAAG,SAAS;AAChF,iBAAA,CAAC;gBACF,UAAU,CAAC,QAAQ,EAAE;AACvB,YAAA,CAAC;AACA,iBAAA,KAAK,CAAC,CAAC,KAAK,KAAK,UAAU,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;AAC9C,QAAA,CAAC,CAAC;IACJ;AAEO,IAAA,OAAO,CAAC,MAAoB,EAAA;AACjC,QAAA,OAAO,qBAAqB,CAAC,IAAI,CAAC,QAAQ,EAAE,MAAK;YAC/C,IAAI,CAAC,MAAM,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC,EAAE;AAClC,gBAAA,OAAO,IAAI,UAAU,CAAgB,CAAC,UAAU,KAAI;AAClD,oBAAA,UAAU,CAAC,IAAI,CAAC,EAAE,CAAC;oBACnB,UAAU,CAAC,QAAQ,EAAE;AACvB,gBAAA,CAAC,CAAC;YACJ;AAEA,YAAA,MAAM,OAAO,GAAsB,MAAM,CAAC,GAAG,CAAC,CAAC,KAAU,KACvD,KAAK,CAAC,KAAK,CAAC,KAAK,CAAC,KAAK,EAAE,KAAK,CAAC,KAAK,CAAC,SAAS,EAAE,KAAK,CAAC,KAAK,CAAC,KAAK,CAAC,CACnE;YAED,MAAM,WAAW,GAAG,KAAK,CAAC,IAAI,CAAC,aAAa,EAAE,GAAG,OAAO,CAAC;AAEzD,YAAA,OAAO,IAAI,UAAU,CAAgB,CAAC,UAAU,KAAI;gBAClD,OAAO,CAAC,WAAW;AAChB,qBAAA,IAAI,CAAC,CAAC,QAAQ,KAAI;oBACjB,MAAM,QAAQ,GAAG,kBAAkB,CAAC,sBAAsB,CAAc,QAAQ,CAAC,IAAI,CAAC;AACtF,oBAAA,UAAU,CAAC,IAAI,CAAC,QAAQ,CAAC;oBACzB,UAAU,CAAC,QAAQ,EAAE;AACvB,gBAAA,CAAC;AACA,qBAAA,KAAK,CAAC,CAAC,KAAK,KAAK,UAAU,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;AAC9C,YAAA,CAAC,CAAC;AACJ,QAAA,CAAC,CAAC;IACJ;AAEgB,IAAA,OAAO,CAAC,YAA+B,EAAA;AACrD,QAAA,OAAO,qBAAqB,CAAC,IAAI,CAAC,QAAQ,EAAE,MAAK;AAC/C,YAAA,MAAM,SAAS,GAAgB;AAC7B,gBAAA,GAAG,YAAY;aACD;;YAGhB,MAAM,aAAa,GAAG,IAAI,CAAC,gBAAgB,CAAC,SAAS,CAAC;AAEtD,YAAA,OAAO,IAAI,UAAU,CAAoB,CAAC,UAAU,KAAI;;AAEtD,gBAAA,MAAM,CAAC,GAAG,CAAC,aAAa,EAAE,YAAY,CAAC,GAAG,CAAC,EAAE,SAAS,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE;qBACpE,IAAI,CAAC,MAAK;AACT,oBAAA,UAAU,CAAC,IAAI,CAAC,SAA8B,CAAC;oBAC/C,UAAU,CAAC,QAAQ,EAAE;AACvB,gBAAA,CAAC;AACA,qBAAA,KAAK,CAAC,CAAC,KAAK,KAAI;AACf,oBAAA,UAAU,CAAC,KAAK,CAAC,KAAK,CAAC;AACzB,gBAAA,CAAC,CAAC;AACN,YAAA,CAAC,CAAC;AACJ,QAAA,CAAC,CAAC;IACJ;AAEA;;;;AAIG;IACI,gBAAgB,CAAC,QAAgB,EAAE,OAAsB,EAAA;AAC9D,QAAA,IAAI,CAAC,IAAI,CAAC,oBAAoB,EAAE;AAC9B,YAAA,MAAM,IAAI,KAAK,CAAC,gDAAgD,CAAC;QACnE;AAEA,QAAA,MAAM,aAAa,GAAG,EAAE,CAAC,IAAI,CAAC,oBAAoB,CAAC,aAAa,GAAG,QAAQ,EAAE;QAC7E,MAAM,aAAa,GAAG,IAAI,CAAC,gBAAgB,CAAC,aAAa,CAAC;AAE1D,QAAA,OAAO,IAAI,UAAU,CAAgB,CAAC,UAAU,KAAI;;AAElD,YAAA,MAAM,YAAY,GAAG,OAAO,GAAG,kBAAkB,CAAC,OAAO,CAAC,OAAO,CAAC,GAAG,EAAE;YACvE,MAAM,WAAW,GAAG,sBAAsB,CAAC,KAAK,CAAC,YAAY,CAAC;YAC9D,MAAM,aAAa,GAAG,KAAK,CAAC,aAAa,EAAE,GAAG,WAAW,CAAC;YAE1D,OAAO,CAAC,aAAa;AAClB,iBAAA,IAAI,CAAC,CAAC,QAAQ,KAAI;gBACjB,IAAI,QAAQ,GAAG,kBAAkB,CAAC,YAAY,CAAc,QAAQ,CAAC,IAAI,CAAC;gBAC1E,QAAQ,GAAG,kBAAkB,CAAC,iBAAiB,CAAC,QAAQ,EAAE,OAAO,EAAE,MAAM,CAAC;AAC1E,gBAAA,UAAU,CAAC,IAAI,CAAC,QAAQ,CAAC;gBACzB,UAAU,CAAC,QAAQ,EAAE;AACvB,YAAA,CAAC;AACA,iBAAA,KAAK,CAAC,CAAC,KAAK,KAAK,UAAU,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;AAC9C,QAAA,CAAC,CAAC;IACJ;AAEA;;;;AAIG;AACI,IAAA,cAAc,CAAC,OAAqB,EAAA;AACzC,QAAA,OAAO,qBAAqB,CAAC,IAAI,CAAC,QAAQ,EAAE,MAAK;;YAE/C,MAAM,YAAY,GAAG,kBAAkB,CAAC,OAAO,CAAC,OAAO,CAAC;;YAGxD,MAAM,WAAW,GAAG,sBAAsB,CAAC,KAAK,CAAC,YAAY,CAAC;YAC9D,MAAM,aAAa,GAAG,KAAK,CAAC,IAAI,CAAC,aAAa,EAAE,GAAG,WAAW,CAAC;;YAG/D,OAAO,cAAc,CAAC,aAAa,EAAE,EAAE,OAAO,EAAE,KAAK,EAAE,CAAyB;AAClF,QAAA,CAAC,CAAC;IACJ;AAEA;;;;AAIG;AACI,IAAA,aAAa,CAAC,GAAW,EAAA;AAC9B,QAAA,OAAO,qBAAqB,CAAC,IAAI,CAAC,QAAQ,EAAE,MAAK;AAC/C,YAAA,MAAM,cAAc,GAAG,GAAG,CAAC,IAAI,CAAC,SAAS,EAAE,CAAA,EAAG,IAAI,CAAC,kBAAkB,CAAA,CAAA,EAAI,GAAG,CAAA,CAAE,CAAC;YAE/E,OAAO,OAAO,CAAC,cAAc,EAAE;AAC7B,gBAAA,OAAO,EAAE,KAAK;AACf,aAAA,CAA4B;AAC/B,QAAA,CAAC,CAAC;IACJ;AACD;;AC3dD;;;AAGG;AACH,SAAS,4BAA4B,CAAC,SAAoB,EAAA;AACxD,IAAA,OAAO,CACL,UAAmC,EACnC,oBAA2C,KACd;QAC7B,OAAO,IAAI,yBAAyB,CAClC,SAAS,EACT,UAAU,EACV,oBAAoB,CACmB;AAC3C,IAAA,CAAC;AACH;AAEA;;;;;;;;;;;;;AAaG;SACa,sBAAsB,GAAA;IACpC,OAAO;AACL,QAAA,OAAO,EAAE,wBAAwB;QACjC,UAAU,EAAE,MAAK;AACf,YAAA,MAAM,SAAS,GAAG,MAAM,CAAC,SAAS,CAAC;AACnC,YAAA,OAAO,4BAA4B,CAAC,SAAS,CAAC;QAChD,CAAC;KACF;AACH;;ACrDA;;AAEG;;ACFH;;AAEG;;;;"}
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 };