@lpdjs/firestore-repo-service 1.0.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.
@@ -0,0 +1,420 @@
1
+ import { DocumentReference, Firestore, DocumentSnapshot, WhereFilterOp, Query, QuerySnapshot, CollectionReference, WriteBatch, Transaction } from 'firebase-admin/firestore';
2
+
3
+ /**
4
+ * Extract the documentRef signature from refCb (without the db parameter)
5
+ * @internal
6
+ */
7
+ type ExtractDocumentRefSignature<T> = T extends (db: Firestore, ...args: infer P) => DocumentReference ? (...args: P) => DocumentReference : never;
8
+ /**
9
+ * Extract the update signature from refCb
10
+ * @internal
11
+ */
12
+ type ExtractUpdateSignature<T, TType> = T extends (db: Firestore, ...args: infer P) => DocumentReference ? (...args: [...P, Partial<TType>]) => Promise<TType> : never;
13
+ /**
14
+ * Type for a where condition with strict value typing based on the field
15
+ * @template T - Data model type
16
+ */
17
+ type WhereClause<T = any> = {
18
+ [K in keyof T]: {
19
+ field: K;
20
+ operator: WhereFilterOp;
21
+ value: T[K] | T[K][];
22
+ };
23
+ }[keyof T];
24
+ /**
25
+ * Query options for filtering, sorting and paginating results
26
+ * @template T - Data model type
27
+ */
28
+ interface QueryOptions<T = any> {
29
+ where?: WhereClause<T>[];
30
+ orWhere?: WhereClause<T>[][];
31
+ orderBy?: {
32
+ field: keyof T;
33
+ direction?: "asc" | "desc";
34
+ }[];
35
+ limit?: number;
36
+ offset?: number;
37
+ startAt?: DocumentSnapshot | any[];
38
+ startAfter?: DocumentSnapshot | any[];
39
+ endAt?: DocumentSnapshot | any[];
40
+ endBefore?: DocumentSnapshot | any[];
41
+ }
42
+ /**
43
+ * Result type for get operations with optional document snapshot
44
+ * @internal
45
+ */
46
+ type GetResult<T, ReturnDoc extends boolean> = ReturnDoc extends true ? {
47
+ data: T;
48
+ doc: DocumentSnapshot;
49
+ } | null : T | null;
50
+ /**
51
+ * Relation configuration for a field with strict typing
52
+ * @template TRepoKey - Target repository name (key from mapping)
53
+ * @template TForeignKey - Target foreign key name
54
+ * @template TType - Relation type: "one" for one-to-one, "many" for one-to-many
55
+ * @template TTargetModel - Type of the target model (inferred from mapping)
56
+ */
57
+ interface RelationConfig<TRepoKey extends string = string, TForeignKey extends string = string, TType extends "one" | "many" = "one" | "many", TTargetModel = any> {
58
+ repo: TRepoKey;
59
+ key: TForeignKey;
60
+ type: TType;
61
+ targetType?: TTargetModel;
62
+ }
63
+ /**
64
+ * Relational key mapping between repositories with strict typing
65
+ * Maps a field from the current model to a target repository and foreign key
66
+ * @template T - Current model type
67
+ * @template TMapping - All repositories mapping for validation
68
+ * @example { userId: { repo: "users", key: "docId", type: "one" } }
69
+ *
70
+ * IMPORTANT: Keys must exist in T (the current model)
71
+ * This prevents creating relations on non-existent fields
72
+ */
73
+ type RelationalKeys<T = any, TMapping = any> = {
74
+ [K in keyof T]?: TMapping extends Record<string, any> ? {
75
+ [R in keyof TMapping]: TMapping[R] extends RepositoryConfig<any, infer FKeys, any, any, any, any> ? {
76
+ repo: R;
77
+ key: FKeys[number];
78
+ type: "one" | "many";
79
+ } : never;
80
+ }[keyof TMapping] : RelationConfig;
81
+ };
82
+ /**
83
+ * Configuration interface for repositories with strict literal type inference
84
+ * @template T - The data model type
85
+ * @template TForeignKeys - Foreign keys used for unique document retrieval
86
+ * @template TQueryKeys - Query keys used for multiple document searches
87
+ * @template TIsGroup - Whether this is a collection group query
88
+ * @template TRefCb - Callback function signature for creating document references
89
+ * @template TRelationalKeys - Relational keys mapping to other repositories
90
+ */
91
+ interface RepositoryConfig<T, TForeignKeys extends readonly (keyof T)[], TQueryKeys extends readonly (keyof T)[], TIsGroup extends boolean = boolean, TRefCb = any, TRelationalKeys = {}> {
92
+ path: string;
93
+ isGroup: TIsGroup;
94
+ foreignKeys: TForeignKeys;
95
+ queryKeys: TQueryKeys;
96
+ type: T;
97
+ refCb?: TRefCb;
98
+ relationalKeys?: TRelationalKeys;
99
+ documentRef: TRefCb extends undefined ? TIsGroup extends true ? (...pathSegments: string[]) => DocumentReference : (docId: string) => DocumentReference : ExtractDocumentRefSignature<TRefCb>;
100
+ update: TRefCb extends undefined ? TIsGroup extends true ? (...args: [...string[], Partial<T>]) => Promise<T> : (docId: string, data: Partial<T>) => Promise<T> : ExtractUpdateSignature<TRefCb, T>;
101
+ }
102
+
103
+ /**
104
+ * Pagination result with data and cursor information
105
+ * @template T - Data model type
106
+ */
107
+ interface PaginationResult<T> {
108
+ /** Array of documents for the current page */
109
+ data: T[];
110
+ /** Cursor to the next page (undefined if no more pages) */
111
+ nextCursor?: DocumentSnapshot;
112
+ /** Cursor to the previous page (undefined if on first page) */
113
+ prevCursor?: DocumentSnapshot;
114
+ /** Whether there are more pages after this one */
115
+ hasNextPage: boolean;
116
+ /** Whether there are pages before this one */
117
+ hasPrevPage: boolean;
118
+ /** Total number of items in current page */
119
+ pageSize: number;
120
+ }
121
+ /**
122
+ * Pagination options for cursor-based pagination
123
+ * @template T - Data model type
124
+ */
125
+ interface PaginationOptions<T> extends Omit<QueryOptions<T>, "limit"> {
126
+ /** Number of items per page */
127
+ pageSize: number;
128
+ /** Cursor to start after (for next page) */
129
+ cursor?: DocumentSnapshot;
130
+ /** Direction of pagination */
131
+ direction?: "next" | "prev";
132
+ }
133
+ /**
134
+ * Helper to apply query options to a Firestore query
135
+ */
136
+ declare function applyQueryOptions<T>(q: Query, options: QueryOptions<T>): Query;
137
+ /**
138
+ * Executes a paginated query and returns results with pagination info
139
+ * Uses the advanced query builder that handles OR conditions and automatic splitting
140
+ * @template T - Data model type
141
+ * @param baseQuery - Base Firestore query
142
+ * @param options - Pagination options
143
+ * @returns Pagination result with data and cursor information
144
+ */
145
+ declare function executePaginatedQuery<T>(baseQuery: Query, options: PaginationOptions<T>): Promise<PaginationResult<T>>;
146
+ /**
147
+ * Creates an async generator for iterating through all pages
148
+ * @template T - Data model type
149
+ * @param baseQuery - Base Firestore query
150
+ * @param options - Pagination options (without cursor)
151
+ * @yields Pagination results for each page
152
+ * @example
153
+ * ```typescript
154
+ * const pageIterator = createPaginationIterator(query, { pageSize: 10 });
155
+ * for await (const page of pageIterator) {
156
+ * console.log(`Page with ${page.pageSize} items`);
157
+ * page.data.forEach(item => console.log(item));
158
+ * if (!page.hasNextPage) break;
159
+ * }
160
+ * ```
161
+ */
162
+ declare function createPaginationIterator<T>(baseQuery: Query, options: Omit<PaginationOptions<T>, "cursor" | "direction">): AsyncGenerator<PaginationResult<T>, void, unknown>;
163
+
164
+ /**
165
+ * Build and execute query with automatic splitting for in/array-contains-any
166
+ * Handles both simple AND conditions and complex OR conditions
167
+ */
168
+ declare function buildAndExecuteQuery<T>(baseQuery: Query, options: QueryOptions<T>): Promise<QuerySnapshot>;
169
+
170
+ /**
171
+ * Helper type to extract populated data structure from a single relation
172
+ * @internal
173
+ */
174
+ type ExtractPopulatedFromRelation<TRelation> = TRelation extends RelationConfig<infer TRepo, any, infer TType, infer TTargetModel> ? {
175
+ [P in TRepo]: TType extends "one" ? TTargetModel | null : TTargetModel[];
176
+ } : Record<string, never>;
177
+ /**
178
+ * Helper type to merge multiple populated objects into one
179
+ * @internal
180
+ */
181
+ type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends (k: infer I) => void ? I : never;
182
+ /**
183
+ * Generates get.by* methods from foreign keys
184
+ * @internal
185
+ */
186
+ type GenerateGetMethods<TConfig extends RepositoryConfig<any, any, any, any, any, any>> = {
187
+ [K in TConfig["foreignKeys"][number] as K extends string ? `by${Capitalize<K>}` : never]: <ReturnDoc extends boolean = false>(value: TConfig["type"][K], returnDoc?: ReturnDoc) => Promise<GetResult<TConfig["type"], ReturnDoc>>;
188
+ };
189
+ /**
190
+ * Generates query.by* methods from query keys
191
+ * @internal
192
+ */
193
+ type GenerateQueryMethods<TConfig extends RepositoryConfig<any, any, any, any, any, any>> = {
194
+ [K in TConfig["queryKeys"][number] as K extends string ? `by${Capitalize<K>}` : never]: (value: TConfig["type"][K], options?: QueryOptions<TConfig["type"]>) => Promise<TConfig["type"][]>;
195
+ };
196
+ /**
197
+ * Configured repository with organized methods
198
+ */
199
+ type ConfiguredRepository<T extends RepositoryConfig<any, any, any, any, any, any>> = {
200
+ ref: CollectionReference | Query;
201
+ get: GenerateGetMethods<T> & {
202
+ byList: <K extends keyof T["type"], ReturnDoc extends boolean = false>(key: K, values: T["type"][K][], operator?: "in" | "array-contains-any", returnDoc?: ReturnDoc) => Promise<ReturnDoc extends true ? Array<{
203
+ data: T["type"];
204
+ doc: DocumentSnapshot;
205
+ }> : T["type"][]>;
206
+ };
207
+ query: GenerateQueryMethods<T> & {
208
+ by: (options: QueryOptions<T["type"]>) => Promise<T["type"][]>;
209
+ getAll: (options?: QueryOptions<T["type"]>) => Promise<T["type"][]>;
210
+ onSnapshot: (options: QueryOptions<T["type"]>, onNext: (data: T["type"][]) => void, onError?: (error: Error) => void) => () => void;
211
+ paginate: (options: PaginationOptions<T["type"]>) => ReturnType<typeof executePaginatedQuery<T["type"]>>;
212
+ paginateAll: (options: Omit<PaginationOptions<T["type"]>, "cursor" | "direction">) => ReturnType<typeof createPaginationIterator<T["type"]>>;
213
+ };
214
+ aggregate: {
215
+ count: (options?: QueryOptions<T["type"]>) => Promise<number>;
216
+ sum: <K extends keyof T["type"]>(field: K, options?: QueryOptions<T["type"]>) => Promise<number>;
217
+ average: <K extends keyof T["type"]>(field: K, options?: QueryOptions<T["type"]>) => Promise<number | null>;
218
+ };
219
+ documentRef: T["documentRef"];
220
+ create: (data: Partial<T["type"]>) => Promise<T["type"] & {
221
+ docId: string;
222
+ }>;
223
+ set: (...args: [
224
+ ...Parameters<T["documentRef"]>,
225
+ Partial<T["type"]>,
226
+ {
227
+ merge?: boolean;
228
+ }?
229
+ ]) => Promise<T["type"]>;
230
+ update: T["update"];
231
+ delete: (...args: Parameters<T["documentRef"]>) => Promise<void>;
232
+ batch: {
233
+ create: () => {
234
+ batch: WriteBatch;
235
+ set: (...args: [
236
+ ...Parameters<T["documentRef"]>,
237
+ Partial<T["type"]>,
238
+ {
239
+ merge?: boolean;
240
+ }?
241
+ ]) => void;
242
+ update: (...args: [...Parameters<T["documentRef"]>, Partial<T["type"]>]) => void;
243
+ delete: (...args: Parameters<T["documentRef"]>) => void;
244
+ commit: () => Promise<void>;
245
+ };
246
+ };
247
+ transaction: {
248
+ run: <R>(updateFunction: (transaction: {
249
+ get: (...args: Parameters<T["documentRef"]>) => Promise<T["type"] | null>;
250
+ set: (...args: [
251
+ ...Parameters<T["documentRef"]>,
252
+ Partial<T["type"]>,
253
+ {
254
+ merge?: boolean;
255
+ }?
256
+ ]) => void;
257
+ update: (...args: [...Parameters<T["documentRef"]>, Partial<T["type"]>]) => void;
258
+ delete: (...args: Parameters<T["documentRef"]>) => void;
259
+ raw: Transaction;
260
+ }) => Promise<R>) => Promise<R>;
261
+ };
262
+ bulk: {
263
+ set: (items: Array<{
264
+ docRef: DocumentReference;
265
+ data: Partial<T["type"]>;
266
+ merge?: boolean;
267
+ }>) => Promise<void>;
268
+ update: (items: Array<{
269
+ docRef: DocumentReference;
270
+ data: Partial<T["type"]>;
271
+ }>) => Promise<void>;
272
+ delete: (docRefs: DocumentReference[]) => Promise<void>;
273
+ };
274
+ populate: <K extends keyof NonNullable<T["relationalKeys"]>>(document: T["type"], relationKey: K | K[]) => Promise<T["type"] & {
275
+ populated: UnionToIntersection<K extends keyof NonNullable<T["relationalKeys"]> ? ExtractPopulatedFromRelation<NonNullable<T["relationalKeys"]>[K]> : Record<string, never>>;
276
+ }>;
277
+ };
278
+
279
+ /**
280
+ * Helper to create a typed repository configuration with literal type preservation
281
+ * Uses currying pattern to allow type parameter inference
282
+ * @template T - The data model type
283
+ * @returns Builder function that accepts repository configuration with withRelations method
284
+ * @example
285
+ * ```typescript
286
+ * const mapping = {
287
+ * users: createRepositoryConfig<UserModel>()({
288
+ * path: "users",
289
+ * foreignKeys: ["docId", "email"] as const,
290
+ * queryKeys: ["isActive"] as const,
291
+ * refCb: (db, docId: string) => db.collection("users").doc(docId),
292
+ * }),
293
+ * posts: createRepositoryConfig<PostModel>()({
294
+ * path: "posts",
295
+ * foreignKeys: ["docId", "userId"] as const,
296
+ * queryKeys: ["status"] as const,
297
+ * refCb: (db, docId: string) => db.collection("posts").doc(docId),
298
+ * }).withRelations<typeof mapping>()({
299
+ * userId: { repo: "users", key: "docId", type: "one" as const }
300
+ * })
301
+ * };
302
+ * ```
303
+ */
304
+ declare function createRepositoryConfig<T>(): <const TForeignKeys extends readonly (keyof T)[], const TQueryKeys extends readonly (keyof T)[], const TIsGroup extends boolean, TRefCb = undefined>(config: {
305
+ path: string;
306
+ isGroup: TIsGroup;
307
+ foreignKeys: TForeignKeys;
308
+ queryKeys: TQueryKeys;
309
+ refCb: TRefCb;
310
+ }) => RepositoryConfig<T, TForeignKeys, TQueryKeys, TIsGroup, TRefCb, {}>;
311
+ /**
312
+ * Helper type to resolve a single relation configuration
313
+ * Extracts the target model type from the mapping
314
+ */
315
+ type ResolveRelation<TMapping, TRelationConfig> = TRelationConfig extends {
316
+ repo: infer R;
317
+ key: infer FK;
318
+ type: infer RT;
319
+ } ? R extends keyof TMapping ? TMapping[R] extends {
320
+ type: infer TTarget;
321
+ } ? RelationConfig<R & string, FK & string, RT & ("one" | "many"), TTarget> : never : never : never;
322
+ /**
323
+ * Helper to add relations to a repository mapping with full type validation
324
+ * Validates that repo names and foreign keys exist in the mapping
325
+ * @template TMapping - The complete repository mapping for validation
326
+ * @template TRelations - Relations configuration with strict typing
327
+ * @param mapping - The base repository mapping
328
+ * @param relations - Relations configuration for each repository
329
+ * @returns Updated mapping with relations and full type safety
330
+ * @example
331
+ * ```typescript
332
+ * const mapping = {
333
+ * users: createRepositoryConfig<UserModel>()({ ... }),
334
+ * posts: createRepositoryConfig<PostModel>()({ ... }),
335
+ * };
336
+ *
337
+ * const mappingWithRelations = buildRepositoryRelations(mapping, {
338
+ * posts: {
339
+ * userId: { repo: "users", key: "docId", type: "one" as const }
340
+ * }
341
+ * });
342
+ *
343
+ * const repos = createRepositoryMapping(db, mappingWithRelations);
344
+ * ```
345
+ */
346
+ declare function buildRepositoryRelations<TMapping extends Record<string, any>, const TRelations extends {
347
+ [K in keyof TMapping]?: TMapping[K] extends RepositoryConfig<infer T, any, any, any, any, any> ? {
348
+ [RK in keyof T]?: {
349
+ [R in keyof TMapping]: TMapping[R] extends RepositoryConfig<infer TTargetModel, infer TForeignKeys, any, any, any, any> ? {
350
+ repo: R;
351
+ key: TForeignKeys[number];
352
+ type: "one" | "many";
353
+ } : never;
354
+ }[keyof TMapping];
355
+ } : never;
356
+ }>(mapping: TMapping, relations: TRelations): {
357
+ [K in keyof TMapping]: K extends keyof TRelations ? TMapping[K] extends RepositoryConfig<infer T, infer TForeignKeys, infer TQueryKeys, infer TIsGroup, infer TRefCb, any> ? RepositoryConfig<T, TForeignKeys, TQueryKeys, TIsGroup, TRefCb, {
358
+ [RK in keyof TRelations[K]]: ResolveRelation<TMapping, TRelations[K][RK]>;
359
+ }> : TMapping[K] : TMapping[K];
360
+ };
361
+ /**
362
+ * Repository mapping class that manages Firestore repositories with type safety
363
+ * @template T - Record of repository configurations
364
+ */
365
+ declare class RepositoryMapping<T extends Record<string, any>> {
366
+ private db;
367
+ private repositoryCache;
368
+ private mapping;
369
+ private allRepositories;
370
+ /**
371
+ * Creates a new RepositoryMapping instance
372
+ * @param db - Firestore instance from firebase-admin
373
+ * @param mapping - Repository configuration mapping
374
+ */
375
+ constructor(db: Firestore, mapping: T);
376
+ /**
377
+ * Initialize all repositories in two passes to handle circular dependencies
378
+ * @private
379
+ */
380
+ private initializeRepositories;
381
+ /**
382
+ * Gets a repository (already initialized)
383
+ * @template K - Repository key
384
+ * @param key - Repository identifier
385
+ * @returns Configured repository instance
386
+ */
387
+ getRepository<K extends keyof T>(key: K): ConfiguredRepository<T[K]>;
388
+ }
389
+ /**
390
+ * Helper function to create a RepositoryMapping instance with full typing
391
+ * @template T - Record of repository configurations
392
+ * @param db - Firestore instance from firebase-admin
393
+ * @param mapping - Repository configurations
394
+ * @returns RepositoryMapping instance with repository access via getters
395
+ * @example
396
+ * ```typescript
397
+ * import * as admin from 'firebase-admin';
398
+ *
399
+ * admin.initializeApp();
400
+ * const db = admin.firestore();
401
+ *
402
+ * const repos = createRepositoryMapping(db, {
403
+ * users: createRepositoryConfig<UserModel>()({
404
+ * path: "users",
405
+ * isGroup: false,
406
+ * foreignKeys: ["docId", "email"] as const,
407
+ * queryKeys: ["isActive"] as const,
408
+ * refCb: (db, docId: string) => db.collection("users").doc(docId),
409
+ * }),
410
+ * });
411
+ *
412
+ * // Access repositories directly
413
+ * const user = await repos.users.get.byDocId("123");
414
+ * ```
415
+ */
416
+ declare function createRepositoryMapping<T extends Record<string, any>>(db: Firestore, mapping: T): RepositoryMapping<T> & {
417
+ [K in keyof T]: ConfiguredRepository<T[K]>;
418
+ };
419
+
420
+ export { type ConfiguredRepository, type ExtractDocumentRefSignature, type ExtractUpdateSignature, type GenerateGetMethods, type GenerateQueryMethods, type GetResult, type PaginationOptions, type PaginationResult, type QueryOptions, type RelationConfig, type RelationalKeys, type RepositoryConfig, RepositoryMapping, type WhereClause, applyQueryOptions as applyPaginationQueryOptions, buildAndExecuteQuery, buildRepositoryRelations, createPaginationIterator, createRepositoryConfig, createRepositoryMapping, executePaginatedQuery };