@cravery/core 0.0.3 → 0.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (111) hide show
  1. package/dist/config/collections.d.ts +1 -0
  2. package/dist/config/collections.d.ts.map +1 -1
  3. package/dist/config/collections.js +1 -0
  4. package/dist/config/collections.js.map +1 -1
  5. package/dist/lib/ai/cost.d.ts +22 -0
  6. package/dist/lib/ai/cost.d.ts.map +1 -0
  7. package/dist/lib/ai/cost.js +45 -0
  8. package/dist/lib/ai/cost.js.map +1 -0
  9. package/dist/lib/ai/embedding.d.ts +4 -0
  10. package/dist/lib/ai/embedding.d.ts.map +1 -0
  11. package/dist/lib/ai/embedding.js +48 -0
  12. package/dist/lib/ai/embedding.js.map +1 -0
  13. package/dist/lib/ai/errors.d.ts +14 -0
  14. package/dist/lib/ai/errors.d.ts.map +1 -0
  15. package/dist/lib/ai/errors.js +40 -0
  16. package/dist/lib/ai/errors.js.map +1 -0
  17. package/dist/lib/ai/flow.d.ts +53 -0
  18. package/dist/lib/ai/flow.d.ts.map +1 -0
  19. package/dist/lib/ai/flow.js +60 -0
  20. package/dist/lib/ai/flow.js.map +1 -0
  21. package/dist/lib/ai/genkit.d.ts +4 -0
  22. package/dist/lib/ai/genkit.d.ts.map +1 -0
  23. package/dist/lib/ai/genkit.js +16 -0
  24. package/dist/lib/ai/genkit.js.map +1 -0
  25. package/dist/lib/ai/image.d.ts +7 -0
  26. package/dist/lib/ai/image.d.ts.map +1 -0
  27. package/dist/lib/ai/image.js +23 -0
  28. package/dist/lib/ai/image.js.map +1 -0
  29. package/dist/lib/ai/index.d.ts +7 -0
  30. package/dist/lib/ai/index.d.ts.map +1 -0
  31. package/dist/lib/ai/index.js +23 -0
  32. package/dist/lib/ai/index.js.map +1 -0
  33. package/dist/lib/firebase.d.ts +1 -1
  34. package/dist/lib/firebase.d.ts.map +1 -1
  35. package/dist/lib/index.d.ts +2 -5
  36. package/dist/lib/index.d.ts.map +1 -1
  37. package/dist/lib/index.js +2 -5
  38. package/dist/lib/index.js.map +1 -1
  39. package/dist/lib/repository/errors.d.ts +20 -0
  40. package/dist/lib/repository/errors.d.ts.map +1 -0
  41. package/dist/lib/repository/errors.js +33 -0
  42. package/dist/lib/repository/errors.js.map +1 -0
  43. package/dist/lib/repository/firestore.repository.d.ts +50 -0
  44. package/dist/lib/repository/firestore.repository.d.ts.map +1 -0
  45. package/dist/lib/repository/firestore.repository.js +407 -0
  46. package/dist/lib/repository/firestore.repository.js.map +1 -0
  47. package/dist/lib/repository/index.d.ts +7 -0
  48. package/dist/lib/repository/index.d.ts.map +1 -0
  49. package/dist/lib/repository/index.js +23 -0
  50. package/dist/lib/repository/index.js.map +1 -0
  51. package/dist/lib/repository/profile.repository.d.ts +12 -0
  52. package/dist/lib/repository/profile.repository.d.ts.map +1 -0
  53. package/dist/lib/repository/profile.repository.js +39 -0
  54. package/dist/lib/repository/profile.repository.js.map +1 -0
  55. package/dist/lib/repository/rtdb.repository.d.ts +21 -0
  56. package/dist/lib/repository/rtdb.repository.d.ts.map +1 -0
  57. package/dist/lib/repository/rtdb.repository.js +55 -0
  58. package/dist/lib/repository/rtdb.repository.js.map +1 -0
  59. package/dist/lib/repository/settings.repository.d.ts +10 -0
  60. package/dist/lib/repository/settings.repository.d.ts.map +1 -0
  61. package/dist/lib/repository/settings.repository.js +30 -0
  62. package/dist/lib/repository/settings.repository.js.map +1 -0
  63. package/dist/lib/repository/user.repository.d.ts +11 -0
  64. package/dist/lib/repository/user.repository.d.ts.map +1 -0
  65. package/dist/lib/repository/user.repository.js +36 -0
  66. package/dist/lib/repository/user.repository.js.map +1 -0
  67. package/dist/types/index.d.ts +2 -1
  68. package/dist/types/index.d.ts.map +1 -1
  69. package/dist/types/index.js +2 -1
  70. package/dist/types/index.js.map +1 -1
  71. package/dist/types/moderation.d.ts +0 -1
  72. package/dist/types/moderation.d.ts.map +1 -1
  73. package/dist/types/profile.d.ts +1 -1
  74. package/dist/types/profile.d.ts.map +1 -1
  75. package/dist/types/profile.js.map +1 -1
  76. package/dist/types/recipe.d.ts +0 -1
  77. package/dist/types/recipe.d.ts.map +1 -1
  78. package/dist/types/repository.d.ts +83 -0
  79. package/dist/types/repository.d.ts.map +1 -0
  80. package/dist/types/repository.js +6 -0
  81. package/dist/types/repository.js.map +1 -0
  82. package/dist/types/settings.d.ts +141 -0
  83. package/dist/types/settings.d.ts.map +1 -0
  84. package/dist/types/settings.js +70 -0
  85. package/dist/types/settings.js.map +1 -0
  86. package/dist/types/subscription.d.ts +0 -1
  87. package/dist/types/subscription.d.ts.map +1 -1
  88. package/dist/types/user.d.ts +1 -1
  89. package/dist/types/user.d.ts.map +1 -1
  90. package/package.json +1 -1
  91. package/src/config/collections.ts +1 -0
  92. package/src/lib/{cost.ts → ai/cost.ts} +1 -1
  93. package/src/lib/{embedding.ts → ai/embedding.ts} +2 -2
  94. package/src/lib/{flow.ts → ai/flow.ts} +3 -3
  95. package/src/lib/{image.ts → ai/image.ts} +1 -2
  96. package/src/lib/ai/index.ts +6 -0
  97. package/src/lib/index.ts +2 -5
  98. package/src/lib/repository/errors.ts +37 -0
  99. package/src/lib/repository/firestore.repository.ts +607 -0
  100. package/src/lib/repository/index.ts +6 -0
  101. package/src/lib/repository/profile.repository.ts +51 -0
  102. package/src/lib/repository/rtdb.repository.ts +68 -0
  103. package/src/lib/repository/settings.repository.ts +38 -0
  104. package/src/lib/repository/user.repository.ts +44 -0
  105. package/src/types/index.ts +2 -1
  106. package/src/types/profile.ts +2 -0
  107. package/src/types/repository.ts +115 -0
  108. package/src/types/settings.ts +85 -0
  109. package/src/types/user.ts +2 -0
  110. /package/src/{types/error.ts → lib/ai/errors.ts} +0 -0
  111. /package/src/lib/{genkit.ts → ai/genkit.ts} +0 -0
@@ -0,0 +1,607 @@
1
+ import {
2
+ Firestore,
3
+ CollectionReference,
4
+ DocumentData,
5
+ Query,
6
+ FieldValue,
7
+ Transaction,
8
+ WriteBatch,
9
+ Timestamp,
10
+ QueryDocumentSnapshot,
11
+ } from "firebase-admin/firestore";
12
+ import type {
13
+ IRepository,
14
+ QueryOptions,
15
+ PaginatedResult,
16
+ CursorPaginatedResult,
17
+ WhereClause,
18
+ TransactionCallback,
19
+ RepositoryConfig,
20
+ } from "../../types/repository";
21
+ import { RepositoryError, RepositoryErrorCode } from "./errors";
22
+
23
+ const FIRESTORE_IN_QUERY_LIMIT = 10;
24
+ const FIRESTORE_BATCH_WRITE_LIMIT = 500;
25
+
26
+ export abstract class FirestoreRepository<T extends { id: string }>
27
+ implements IRepository<T>
28
+ {
29
+ protected readonly collection: CollectionReference<DocumentData>;
30
+ protected readonly config: Required<RepositoryConfig>;
31
+
32
+ constructor(
33
+ protected readonly firestore: Firestore,
34
+ protected readonly collectionName: string,
35
+ config?: RepositoryConfig,
36
+ ) {
37
+ this.collection = firestore.collection(collectionName);
38
+ this.config = {
39
+ enableTimestamps: true,
40
+ enableSoftDelete: false,
41
+ validateOnWrite: false,
42
+ ...config,
43
+ };
44
+ }
45
+
46
+ protected toDocument(entity: Partial<T>): DocumentData {
47
+ return { ...entity };
48
+ }
49
+
50
+ protected fromDocument(id: string, data: DocumentData): T {
51
+ return { id, ...data } as T;
52
+ }
53
+
54
+ protected validate(_data: Partial<T>): void {
55
+ }
56
+
57
+ async findById(id: string): Promise<T | null> {
58
+ const doc = await this.collection.doc(id).get();
59
+ if (!doc.exists) return null;
60
+
61
+ const data = doc.data()!;
62
+ if (this.config.enableSoftDelete && data.deletedAt) {
63
+ return null;
64
+ }
65
+
66
+ return this.fromDocument(doc.id, data);
67
+ }
68
+
69
+ async findByIds(ids: string[]): Promise<T[]> {
70
+ if (ids.length === 0) return [];
71
+
72
+ const results: T[] = [];
73
+ const chunks = this.chunk(ids, FIRESTORE_IN_QUERY_LIMIT);
74
+
75
+ for (const chunk of chunks) {
76
+ const refs = chunk.map((id) => this.collection.doc(id));
77
+ const docs = await this.firestore.getAll(...refs);
78
+
79
+ for (const doc of docs) {
80
+ if (doc.exists) {
81
+ const data = doc.data()!;
82
+ if (this.config.enableSoftDelete && data.deletedAt) {
83
+ continue;
84
+ }
85
+ results.push(this.fromDocument(doc.id, data));
86
+ }
87
+ }
88
+ }
89
+
90
+ return results;
91
+ }
92
+
93
+ async findAll(options?: QueryOptions): Promise<T[]> {
94
+ let query: Query = this.collection;
95
+
96
+ if (this.config.enableSoftDelete) {
97
+ query = query.where("deletedAt", "==", null);
98
+ }
99
+
100
+ query = this.applyQueryOptions(query, options);
101
+ const snapshot = await query.get();
102
+ return snapshot.docs.map((doc: QueryDocumentSnapshot<DocumentData>) =>
103
+ this.fromDocument(doc.id, doc.data()),
104
+ );
105
+ }
106
+
107
+ async findWhere(
108
+ clauses: WhereClause[],
109
+ options?: QueryOptions,
110
+ ): Promise<T[]> {
111
+ let query = this.applyWhereClauses(this.collection, clauses);
112
+
113
+ if (this.config.enableSoftDelete) {
114
+ query = query.where("deletedAt", "==", null);
115
+ }
116
+
117
+ query = this.applyQueryOptions(query, options);
118
+ const snapshot = await query.get();
119
+ return snapshot.docs.map((doc: QueryDocumentSnapshot<DocumentData>) =>
120
+ this.fromDocument(doc.id, doc.data()),
121
+ );
122
+ }
123
+
124
+ async findPaginated(
125
+ clauses?: WhereClause[],
126
+ options?: QueryOptions,
127
+ ): Promise<PaginatedResult<T>> {
128
+ let query: Query = clauses
129
+ ? this.applyWhereClauses(this.collection, clauses)
130
+ : this.collection;
131
+
132
+ if (this.config.enableSoftDelete) {
133
+ query = query.where("deletedAt", "==", null);
134
+ }
135
+
136
+ const countSnapshot = await query.count().get();
137
+ const total = countSnapshot.data().count;
138
+
139
+ query = this.applyQueryOptions(query, options);
140
+ const snapshot = await query.get();
141
+ const data = snapshot.docs.map((doc: QueryDocumentSnapshot<DocumentData>) =>
142
+ this.fromDocument(doc.id, doc.data()),
143
+ );
144
+
145
+ const offset = options?.offset ?? 0;
146
+ return {
147
+ data,
148
+ total,
149
+ hasMore: offset + data.length < total,
150
+ };
151
+ }
152
+
153
+ async findPaginatedCursor(
154
+ clauses?: WhereClause[],
155
+ options?: QueryOptions & { cursor?: string },
156
+ ): Promise<CursorPaginatedResult<T>> {
157
+ let query: Query = clauses
158
+ ? this.applyWhereClauses(this.collection, clauses)
159
+ : this.collection;
160
+
161
+ if (this.config.enableSoftDelete) {
162
+ query = query.where("deletedAt", "==", null);
163
+ }
164
+
165
+ if (options?.orderBy) {
166
+ query = query.orderBy(options.orderBy.field, options.orderBy.direction);
167
+ }
168
+
169
+ if (options?.cursor) {
170
+ const cursorDoc = await this.collection.doc(options.cursor).get();
171
+ if (cursorDoc.exists) {
172
+ query = query.startAfter(cursorDoc);
173
+ }
174
+ }
175
+
176
+ const limit = options?.limit ?? 20;
177
+ query = query.limit(limit + 1);
178
+
179
+ const snapshot = await query.get();
180
+ const docs = snapshot.docs;
181
+ const hasMore = docs.length > limit;
182
+
183
+ if (hasMore) docs.pop();
184
+
185
+ const data = docs.map((doc: QueryDocumentSnapshot<DocumentData>) =>
186
+ this.fromDocument(doc.id, doc.data()),
187
+ );
188
+ const nextCursor =
189
+ hasMore && docs.length > 0 ? docs[docs.length - 1].id : null;
190
+
191
+ return { data, nextCursor, hasMore };
192
+ }
193
+
194
+ async exists(id: string): Promise<boolean> {
195
+ const doc = await this.collection.doc(id).get();
196
+ if (!doc.exists) return false;
197
+
198
+ if (this.config.enableSoftDelete) {
199
+ const data = doc.data()!;
200
+ return !data.deletedAt;
201
+ }
202
+
203
+ return true;
204
+ }
205
+
206
+ async count(clauses?: WhereClause[]): Promise<number> {
207
+ let query: Query = clauses
208
+ ? this.applyWhereClauses(this.collection, clauses)
209
+ : this.collection;
210
+
211
+ if (this.config.enableSoftDelete) {
212
+ query = query.where("deletedAt", "==", null);
213
+ }
214
+
215
+ const snapshot = await query.count().get();
216
+ return snapshot.data().count;
217
+ }
218
+
219
+ async create(data: Omit<T, "id" | "createdAt" | "updatedAt" | "deletedAt">, id?: string): Promise<T> {
220
+ if (this.config.validateOnWrite) {
221
+ this.validate(data as Partial<T>);
222
+ }
223
+
224
+ const docRef = id ? this.collection.doc(id) : this.collection.doc();
225
+ const now = Timestamp.now();
226
+
227
+ let docData = this.toDocument({
228
+ ...data,
229
+ id: docRef.id,
230
+ } as Partial<T>);
231
+
232
+ if (this.config.enableTimestamps) {
233
+ docData = {
234
+ ...docData,
235
+ createdAt: now,
236
+ updatedAt: now,
237
+ };
238
+ }
239
+
240
+ if (this.config.enableSoftDelete) {
241
+ docData = {
242
+ ...docData,
243
+ deletedAt: null,
244
+ };
245
+ }
246
+
247
+ await docRef.set(docData);
248
+ return this.fromDocument(docRef.id, docData);
249
+ }
250
+
251
+ async update(id: string, data: Partial<Omit<T, "id" | "createdAt" | "updatedAt" | "deletedAt">>): Promise<T> {
252
+ if (this.config.validateOnWrite) {
253
+ this.validate(data as Partial<T>);
254
+ }
255
+
256
+ const docRef = this.collection.doc(id);
257
+ let updateData = this.toDocument(data as Partial<T>);
258
+
259
+ if (this.config.enableTimestamps) {
260
+ updateData = {
261
+ ...updateData,
262
+ updatedAt: Timestamp.now(),
263
+ };
264
+ }
265
+
266
+ await docRef.update(updateData);
267
+ const updated = await docRef.get();
268
+
269
+ if (!updated.exists) {
270
+ throw new RepositoryError(
271
+ RepositoryErrorCode.NOT_FOUND,
272
+ `Document ${id} not found after update`,
273
+ { collection: this.collectionName, id },
274
+ );
275
+ }
276
+
277
+ return this.fromDocument(updated.id, updated.data()!);
278
+ }
279
+
280
+ async delete(id: string): Promise<void> {
281
+ if (this.config.enableSoftDelete) {
282
+ await this.collection.doc(id).update({
283
+ deletedAt: Timestamp.now(),
284
+ updatedAt: Timestamp.now(),
285
+ });
286
+ } else {
287
+ await this.collection.doc(id).delete();
288
+ }
289
+ }
290
+
291
+ async createMany(items: Omit<T, "id" | "createdAt" | "updatedAt" | "deletedAt">[]): Promise<T[]> {
292
+ if (items.length === 0) return [];
293
+
294
+ const results: T[] = [];
295
+ const chunks = this.chunk(items, FIRESTORE_BATCH_WRITE_LIMIT);
296
+
297
+ for (const chunk of chunks) {
298
+ const batch = this.firestore.batch();
299
+ const now = Timestamp.now();
300
+ const chunkResults: T[] = [];
301
+
302
+ for (const item of chunk) {
303
+ if (this.config.validateOnWrite) {
304
+ this.validate(item as Partial<T>);
305
+ }
306
+
307
+ const docRef = this.collection.doc();
308
+ let docData = this.toDocument({
309
+ ...item,
310
+ id: docRef.id,
311
+ } as Partial<T>);
312
+
313
+ if (this.config.enableTimestamps) {
314
+ docData = { ...docData, createdAt: now, updatedAt: now };
315
+ }
316
+
317
+ if (this.config.enableSoftDelete) {
318
+ docData = { ...docData, deletedAt: null };
319
+ }
320
+
321
+ batch.set(docRef, docData);
322
+ chunkResults.push(this.fromDocument(docRef.id, docData));
323
+ }
324
+
325
+ await batch.commit();
326
+ results.push(...chunkResults);
327
+ }
328
+
329
+ return results;
330
+ }
331
+
332
+ async updateMany(
333
+ updates: Array<{ id: string; data: Partial<Omit<T, "id" | "createdAt" | "updatedAt" | "deletedAt">> }>,
334
+ ): Promise<void> {
335
+ if (updates.length === 0) return;
336
+
337
+ const chunks = this.chunk(updates, FIRESTORE_BATCH_WRITE_LIMIT);
338
+
339
+ for (const chunk of chunks) {
340
+ const batch = this.firestore.batch();
341
+ const now = Timestamp.now();
342
+
343
+ for (const { id, data } of chunk) {
344
+ if (this.config.validateOnWrite) {
345
+ this.validate(data as Partial<T>);
346
+ }
347
+
348
+ const docRef = this.collection.doc(id);
349
+ let updateData = this.toDocument(data as Partial<T>);
350
+
351
+ if (this.config.enableTimestamps) {
352
+ updateData = { ...updateData, updatedAt: now };
353
+ }
354
+
355
+ batch.update(docRef, updateData);
356
+ }
357
+
358
+ await batch.commit();
359
+ }
360
+ }
361
+
362
+ async deleteMany(ids: string[]): Promise<void> {
363
+ if (ids.length === 0) return;
364
+
365
+ const chunks = this.chunk(ids, FIRESTORE_BATCH_WRITE_LIMIT);
366
+
367
+ for (const chunk of chunks) {
368
+ const batch = this.firestore.batch();
369
+ const now = Timestamp.now();
370
+
371
+ for (const id of chunk) {
372
+ const docRef = this.collection.doc(id);
373
+
374
+ if (this.config.enableSoftDelete) {
375
+ batch.update(docRef, {
376
+ deletedAt: now,
377
+ updatedAt: now,
378
+ });
379
+ } else {
380
+ batch.delete(docRef);
381
+ }
382
+ }
383
+
384
+ await batch.commit();
385
+ }
386
+ }
387
+
388
+ async increment(
389
+ id: string,
390
+ field: keyof T,
391
+ value: number = 1,
392
+ ): Promise<void> {
393
+ const updateData: DocumentData = {
394
+ [field as string]: FieldValue.increment(value),
395
+ };
396
+
397
+ if (this.config.enableTimestamps) {
398
+ updateData.updatedAt = Timestamp.now();
399
+ }
400
+
401
+ await this.collection.doc(id).update(updateData);
402
+ }
403
+
404
+ async decrement(
405
+ id: string,
406
+ field: keyof T,
407
+ value: number = 1,
408
+ ): Promise<void> {
409
+ await this.increment(id, field, -value);
410
+ }
411
+
412
+ async runTransaction<R>(callback: TransactionCallback<R>): Promise<R> {
413
+ return this.firestore.runTransaction(callback);
414
+ }
415
+
416
+ getFirestore(): Firestore {
417
+ return this.firestore;
418
+ }
419
+
420
+ batch(): WriteBatch {
421
+ return this.firestore.batch();
422
+ }
423
+
424
+ protected applyWhereClauses(query: Query, clauses: WhereClause[]): Query {
425
+ let q = query;
426
+
427
+ for (const clause of clauses) {
428
+ if (
429
+ (clause.op === "in" ||
430
+ clause.op === "not-in" ||
431
+ clause.op === "array-contains-any") &&
432
+ Array.isArray(clause.value) &&
433
+ clause.value.length > FIRESTORE_IN_QUERY_LIMIT
434
+ ) {
435
+ throw new RepositoryError(
436
+ RepositoryErrorCode.QUERY_LIMIT_EXCEEDED,
437
+ `${clause.op} queries support max ${FIRESTORE_IN_QUERY_LIMIT} values. ` +
438
+ `Got ${clause.value.length}. Use multiple queries and merge results.`,
439
+ { field: clause.field, op: clause.op, valueCount: clause.value.length },
440
+ );
441
+ }
442
+
443
+ q = q.where(clause.field, clause.op, clause.value);
444
+ }
445
+
446
+ return q;
447
+ }
448
+
449
+ protected applyQueryOptions(query: Query, options?: QueryOptions): Query {
450
+ let q = query;
451
+
452
+ if (options?.orderBy) {
453
+ q = q.orderBy(options.orderBy.field, options.orderBy.direction);
454
+ }
455
+ if (options?.offset) {
456
+ q = q.offset(options.offset);
457
+ }
458
+ if (options?.limit) {
459
+ q = q.limit(options.limit);
460
+ }
461
+
462
+ return q;
463
+ }
464
+
465
+ protected chunk<U>(array: U[], size: number): U[][] {
466
+ const chunks: U[][] = [];
467
+ for (let i = 0; i < array.length; i += size) {
468
+ chunks.push(array.slice(i, i + size));
469
+ }
470
+ return chunks;
471
+ }
472
+
473
+ protected async getInTransaction(
474
+ transaction: Transaction,
475
+ id: string,
476
+ ): Promise<T | null> {
477
+ const doc = await transaction.get(this.collection.doc(id));
478
+ if (!doc.exists) return null;
479
+
480
+ const data = doc.data()!;
481
+ if (this.config.enableSoftDelete && data.deletedAt) {
482
+ return null;
483
+ }
484
+
485
+ return this.fromDocument(doc.id, data);
486
+ }
487
+
488
+ protected setInTransaction(
489
+ transaction: Transaction,
490
+ id: string,
491
+ data: Partial<T>,
492
+ ): void {
493
+ let docData = this.toDocument(data);
494
+ const now = Timestamp.now();
495
+
496
+ if (this.config.enableTimestamps) {
497
+ docData = { ...docData, createdAt: now, updatedAt: now };
498
+ }
499
+
500
+ if (this.config.enableSoftDelete) {
501
+ docData = { ...docData, deletedAt: null };
502
+ }
503
+
504
+ transaction.set(this.collection.doc(id), docData);
505
+ }
506
+
507
+ protected updateInTransaction(
508
+ transaction: Transaction,
509
+ id: string,
510
+ data: Partial<T>,
511
+ ): void {
512
+ let docData = this.toDocument(data);
513
+
514
+ if (this.config.enableTimestamps) {
515
+ docData = { ...docData, updatedAt: Timestamp.now() };
516
+ }
517
+
518
+ transaction.update(this.collection.doc(id), docData);
519
+ }
520
+
521
+ protected deleteInTransaction(transaction: Transaction, id: string): void {
522
+ if (this.config.enableSoftDelete) {
523
+ const now = Timestamp.now();
524
+ transaction.update(this.collection.doc(id), {
525
+ deletedAt: now,
526
+ updatedAt: now,
527
+ });
528
+ } else {
529
+ transaction.delete(this.collection.doc(id));
530
+ }
531
+ }
532
+
533
+ createInBatch(batch: WriteBatch, data: Omit<T, "id" | "createdAt" | "updatedAt" | "deletedAt">, id?: string): T {
534
+ if (this.config.validateOnWrite) {
535
+ this.validate(data as Partial<T>);
536
+ }
537
+
538
+ const docRef = id ? this.collection.doc(id) : this.collection.doc();
539
+ const now = Timestamp.now();
540
+
541
+ let docData = this.toDocument({
542
+ ...data,
543
+ id: docRef.id,
544
+ } as Partial<T>);
545
+
546
+ if (this.config.enableTimestamps) {
547
+ docData = { ...docData, createdAt: now, updatedAt: now };
548
+ }
549
+
550
+ if (this.config.enableSoftDelete) {
551
+ docData = { ...docData, deletedAt: null };
552
+ }
553
+
554
+ batch.set(docRef, docData);
555
+ return this.fromDocument(docRef.id, docData);
556
+ }
557
+
558
+ setInBatch(batch: WriteBatch, id: string, data: Partial<Omit<T, "id" | "createdAt" | "updatedAt" | "deletedAt">>): void {
559
+ if (this.config.validateOnWrite) {
560
+ this.validate(data as Partial<T>);
561
+ }
562
+
563
+ const docRef = this.collection.doc(id);
564
+ const now = Timestamp.now();
565
+
566
+ let docData = this.toDocument(data as Partial<T>);
567
+
568
+ if (this.config.enableTimestamps) {
569
+ docData = { ...docData, createdAt: now, updatedAt: now };
570
+ }
571
+
572
+ if (this.config.enableSoftDelete) {
573
+ docData = { ...docData, deletedAt: null };
574
+ }
575
+
576
+ batch.set(docRef, docData);
577
+ }
578
+
579
+ updateInBatch(batch: WriteBatch, id: string, data: Partial<Omit<T, "id" | "createdAt" | "updatedAt" | "deletedAt">>): void {
580
+ if (this.config.validateOnWrite) {
581
+ this.validate(data as Partial<T>);
582
+ }
583
+
584
+ const docRef = this.collection.doc(id);
585
+ let updateData = this.toDocument(data as Partial<T>);
586
+
587
+ if (this.config.enableTimestamps) {
588
+ updateData = { ...updateData, updatedAt: Timestamp.now() };
589
+ }
590
+
591
+ batch.update(docRef, updateData);
592
+ }
593
+
594
+ deleteInBatch(batch: WriteBatch, id: string): void {
595
+ const docRef = this.collection.doc(id);
596
+
597
+ if (this.config.enableSoftDelete) {
598
+ const now = Timestamp.now();
599
+ batch.update(docRef, {
600
+ deletedAt: now,
601
+ updatedAt: now,
602
+ });
603
+ } else {
604
+ batch.delete(docRef);
605
+ }
606
+ }
607
+ }
@@ -0,0 +1,6 @@
1
+ export * from "./errors";
2
+ export * from "./firestore.repository";
3
+ export * from "./rtdb.repository";
4
+ export * from "./user.repository";
5
+ export * from "./profile.repository";
6
+ export * from "./settings.repository";
@@ -0,0 +1,51 @@
1
+ import { Firestore } from "firebase-admin/firestore";
2
+ import { FirestoreRepository } from "./firestore.repository";
3
+ import type { Profile } from "../../types/profile";
4
+ import { ProfileSchema } from "../../types/profile";
5
+ import { Collections } from "../../config/collections";
6
+ import { RepositoryError, RepositoryErrorCode } from "./errors";
7
+
8
+ export class ProfileRepository extends FirestoreRepository<Profile> {
9
+ constructor(firestore: Firestore) {
10
+ super(firestore, Collections.Profiles, {
11
+ enableTimestamps: true,
12
+ enableSoftDelete: true,
13
+ validateOnWrite: true,
14
+ });
15
+ }
16
+
17
+ protected validate(data: Partial<Profile>): void {
18
+ const result = ProfileSchema.partial().safeParse(data);
19
+ if (!result.success) {
20
+ throw new RepositoryError(
21
+ RepositoryErrorCode.VALIDATION_ERROR,
22
+ "Profile validation failed",
23
+ { errors: result.error.issues },
24
+ );
25
+ }
26
+ }
27
+
28
+ async findByHandle(handle: string): Promise<Profile | null> {
29
+ const results = await this.findWhere(
30
+ [{ field: "handle", op: "==", value: handle }],
31
+ { limit: 1 },
32
+ );
33
+ return results[0] ?? null;
34
+ }
35
+
36
+ async findByUserId(userId: string): Promise<Profile | null> {
37
+ return this.findById(userId);
38
+ }
39
+
40
+ async createWithUserId(
41
+ userId: string,
42
+ data: Omit<Profile, "id">,
43
+ ): Promise<Profile> {
44
+ return this.create(data, userId);
45
+ }
46
+
47
+ async handleExists(handle: string): Promise<boolean> {
48
+ const profile = await this.findByHandle(handle);
49
+ return profile !== null;
50
+ }
51
+ }