@visiolab/effect-firebase 0.0.0-stage → 0.3.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.
Files changed (140) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +81 -2
  3. package/dist/_virtual/_rolldown/runtime.mjs +12 -0
  4. package/dist/app/App.d.mts +27 -0
  5. package/dist/app/App.mjs +27 -0
  6. package/dist/app/index.d.mts +2 -0
  7. package/dist/app/index.mjs +1 -0
  8. package/dist/firestore/Collection.d.mts +423 -0
  9. package/dist/firestore/Collection.mjs +133 -0
  10. package/dist/firestore/CollectionGroup.d.mts +164 -0
  11. package/dist/firestore/CollectionGroup.mjs +84 -0
  12. package/dist/firestore/DocumentPath.d.mts +95 -0
  13. package/dist/firestore/DocumentPath.mjs +71 -0
  14. package/dist/firestore/DocumentSnapshot.d.mts +239 -0
  15. package/dist/firestore/DocumentSnapshot.mjs +134 -0
  16. package/dist/firestore/FieldValue.d.mts +332 -0
  17. package/dist/firestore/FieldValue.mjs +256 -0
  18. package/dist/firestore/Firestore.d.mts +1165 -0
  19. package/dist/firestore/Firestore.mjs +274 -0
  20. package/dist/firestore/FirestoreModel.d.mts +1232 -0
  21. package/dist/firestore/FirestoreModel.mjs +586 -0
  22. package/dist/firestore/FirestoreSchema.d.mts +567 -0
  23. package/dist/firestore/FirestoreSchema.mjs +419 -0
  24. package/dist/firestore/FirestoreValue.d.mts +145 -0
  25. package/dist/firestore/FirestoreValue.mjs +87 -0
  26. package/dist/firestore/GeoPoint.d.mts +90 -0
  27. package/dist/firestore/GeoPoint.mjs +75 -0
  28. package/dist/firestore/Patch.d.mts +73 -0
  29. package/dist/firestore/Patch.mjs +57 -0
  30. package/dist/firestore/QuerySnapshot.d.mts +107 -0
  31. package/dist/firestore/QuerySnapshot.mjs +43 -0
  32. package/dist/firestore/Timestamp.d.mts +273 -0
  33. package/dist/firestore/Timestamp.mjs +255 -0
  34. package/dist/firestore/index.d.mts +14 -0
  35. package/dist/firestore/index.mjs +14 -0
  36. package/dist/firestore/internal/collection.mjs +156 -0
  37. package/dist/firestore/internal/collectionGroup.mjs +64 -0
  38. package/dist/firestore/internal/documentPath.d.mts +4 -0
  39. package/dist/firestore/internal/documentPath.mjs +42 -0
  40. package/dist/firestore/internal/documentSnapshot.mjs +63 -0
  41. package/dist/firestore/internal/fieldValue.d.mts +11 -0
  42. package/dist/firestore/internal/fieldValue.mjs +88 -0
  43. package/dist/firestore/internal/firestore.mjs +145 -0
  44. package/dist/firestore/internal/firestoreError.d.mts +29 -0
  45. package/dist/firestore/internal/firestoreError.mjs +30 -0
  46. package/dist/firestore/internal/firestoreModel.mjs +719 -0
  47. package/dist/firestore/internal/firestoreSchema.mjs +123 -0
  48. package/dist/firestore/internal/firestoreValue.mjs +108 -0
  49. package/dist/firestore/internal/firestoreValueWalk.d.mts +58 -0
  50. package/dist/firestore/internal/firestoreValueWalk.mjs +223 -0
  51. package/dist/firestore/internal/geoPoint.d.mts +4 -0
  52. package/dist/firestore/internal/geoPoint.mjs +39 -0
  53. package/dist/firestore/internal/patch.mjs +19 -0
  54. package/dist/firestore/internal/querySnapshot.mjs +24 -0
  55. package/dist/firestore/internal/schemaAST.mjs +35 -0
  56. package/dist/firestore/internal/snapshotExtra.mjs +35 -0
  57. package/dist/firestore/internal/streamFromListener.mjs +12 -0
  58. package/dist/firestore/internal/timestamp.d.mts +4 -0
  59. package/dist/firestore/internal/timestamp.mjs +66 -0
  60. package/dist/firestore/internal/timestampSchema.mjs +31 -0
  61. package/dist/firestore/internal/transactionFromCallback.mjs +66 -0
  62. package/dist/functions/Callable.d.mts +654 -0
  63. package/dist/functions/Callable.mjs +450 -0
  64. package/dist/functions/CallableError.d.mts +297 -0
  65. package/dist/functions/CallableError.mjs +256 -0
  66. package/dist/functions/CallableMiddleware.d.mts +213 -0
  67. package/dist/functions/CallableMiddleware.mjs +52 -0
  68. package/dist/functions/CallableRequest.d.mts +166 -0
  69. package/dist/functions/CallableRequest.mjs +18 -0
  70. package/dist/functions/Caller.d.mts +103 -0
  71. package/dist/functions/Caller.mjs +115 -0
  72. package/dist/functions/DocumentTrigger.d.mts +795 -0
  73. package/dist/functions/DocumentTrigger.mjs +566 -0
  74. package/dist/functions/FirebaseFunction.d.mts +79 -0
  75. package/dist/functions/FirebaseFunction.mjs +4 -0
  76. package/dist/functions/FunctionGroup.d.mts +175 -0
  77. package/dist/functions/FunctionGroup.mjs +64 -0
  78. package/dist/functions/index.d.mts +9 -0
  79. package/dist/functions/index.mjs +9 -0
  80. package/dist/functions/internal/callable.mjs +167 -0
  81. package/dist/functions/internal/callableMiddleware.d.mts +19 -0
  82. package/dist/functions/internal/callableMiddleware.mjs +15 -0
  83. package/dist/functions/internal/callableRequest.mjs +6 -0
  84. package/dist/functions/internal/documentTrigger.d.mts +10 -0
  85. package/dist/functions/internal/documentTrigger.mjs +73 -0
  86. package/dist/functions/internal/firebaseFunction.d.mts +5 -0
  87. package/dist/functions/internal/firebaseFunction.mjs +24 -0
  88. package/dist/functions/internal/functionGroup.mjs +36 -0
  89. package/package.json +66 -3
  90. package/src/app/App.ts +24 -0
  91. package/src/app/index.ts +7 -0
  92. package/src/firestore/Collection.ts +520 -0
  93. package/src/firestore/CollectionGroup.ts +245 -0
  94. package/src/firestore/DocumentPath.ts +114 -0
  95. package/src/firestore/DocumentSnapshot.ts +308 -0
  96. package/src/firestore/FieldValue.ts +375 -0
  97. package/src/firestore/Firestore.ts +1432 -0
  98. package/src/firestore/FirestoreModel.ts +1884 -0
  99. package/src/firestore/FirestoreSchema.ts +643 -0
  100. package/src/firestore/FirestoreValue.ts +207 -0
  101. package/src/firestore/GeoPoint.ts +105 -0
  102. package/src/firestore/Patch.ts +117 -0
  103. package/src/firestore/QuerySnapshot.ts +150 -0
  104. package/src/firestore/Timestamp.ts +302 -0
  105. package/src/firestore/index.ts +67 -0
  106. package/src/firestore/internal/collection.ts +246 -0
  107. package/src/firestore/internal/collectionGroup.ts +81 -0
  108. package/src/firestore/internal/documentPath.ts +53 -0
  109. package/src/firestore/internal/documentSnapshot.ts +117 -0
  110. package/src/firestore/internal/fieldValue.ts +119 -0
  111. package/src/firestore/internal/firestore.ts +322 -0
  112. package/src/firestore/internal/firestoreError.ts +36 -0
  113. package/src/firestore/internal/firestoreModel.ts +1242 -0
  114. package/src/firestore/internal/firestoreSchema.ts +218 -0
  115. package/src/firestore/internal/firestoreValue.ts +202 -0
  116. package/src/firestore/internal/firestoreValueWalk.ts +349 -0
  117. package/src/firestore/internal/geoPoint.ts +59 -0
  118. package/src/firestore/internal/patch.ts +21 -0
  119. package/src/firestore/internal/querySnapshot.ts +39 -0
  120. package/src/firestore/internal/schemaAST.ts +83 -0
  121. package/src/firestore/internal/snapshotExtra.ts +47 -0
  122. package/src/firestore/internal/streamFromListener.ts +27 -0
  123. package/src/firestore/internal/timestamp.ts +119 -0
  124. package/src/firestore/internal/timestampSchema.ts +42 -0
  125. package/src/firestore/internal/transactionFromCallback.ts +106 -0
  126. package/src/functions/Callable.ts +855 -0
  127. package/src/functions/CallableError.ts +348 -0
  128. package/src/functions/CallableMiddleware.ts +279 -0
  129. package/src/functions/CallableRequest.ts +207 -0
  130. package/src/functions/Caller.ts +141 -0
  131. package/src/functions/DocumentTrigger.ts +927 -0
  132. package/src/functions/FirebaseFunction.ts +96 -0
  133. package/src/functions/FunctionGroup.ts +225 -0
  134. package/src/functions/index.ts +62 -0
  135. package/src/functions/internal/callable.ts +272 -0
  136. package/src/functions/internal/callableMiddleware.ts +54 -0
  137. package/src/functions/internal/callableRequest.ts +8 -0
  138. package/src/functions/internal/documentTrigger.ts +143 -0
  139. package/src/functions/internal/firebaseFunction.ts +58 -0
  140. package/src/functions/internal/functionGroup.ts +47 -0
@@ -0,0 +1,1432 @@
1
+ /**
2
+ * Reading and writing Firestore documents as an Effect service.
3
+ *
4
+ * {@link Firestore} is the portable service: it reads, listens to, writes and
5
+ * updates documents, and runs transactions, the same way on every backend.
6
+ * Every call is addressed by a reference built from a
7
+ * {@link Collection.Collection}, whose {@link FirestoreModel} encodes what is
8
+ * written and decodes what is read.
9
+ *
10
+ * - Define documents with {@link FirestoreModel.Class} and where they live with
11
+ * {@link Collection.make}.
12
+ * - Provide the service with {@link AdminFirestore.layer} on a server, or
13
+ * {@link ClientFirestore.layer} in a browser or app. Each layer also provides
14
+ * its richer service, {@link AdminFirestore.AdminFirestore} or
15
+ * {@link ClientFirestore.ClientFirestore}, as the same object.
16
+ * - Implement a backend of your own with {@link make}.
17
+ *
18
+ * @example Reading and writing a document
19
+ * ```ts
20
+ * import { Effect, Schema } from "effect";
21
+ * import { Collection, DocumentSnapshot, Firestore, FirestoreModel } from "@visiolab/effect-firebase/firestore";
22
+ *
23
+ * class User extends FirestoreModel.Class<User>("User")({ name: Schema.String }) {}
24
+ *
25
+ * const Users = Collection.make("users", User);
26
+ *
27
+ * const program = Effect.gen(function* () {
28
+ * const firestore = yield* Firestore.Firestore;
29
+ * const alice = Users.doc(Users.id.make("alice"));
30
+ *
31
+ * yield* firestore.setDoc(alice, { name: "Alice" });
32
+ *
33
+ * const snapshot = yield* firestore.getDoc(alice);
34
+ * if (DocumentSnapshot.isExisting(snapshot)) {
35
+ * const user = yield* snapshot.data;
36
+ * yield* Effect.log(user.name);
37
+ * }
38
+ * });
39
+ * ```
40
+ *
41
+ * @stability unstable
42
+ * @since 0.2.0
43
+ */
44
+ import { Context } from "effect";
45
+ import type { Effect, Schema, Stream } from "effect";
46
+ import type * as Result from "effect/Result";
47
+ import type * as Collection from "./Collection.ts";
48
+ import type * as CollectionGroup from "./CollectionGroup.ts";
49
+ import type * as DocumentSnapshot from "./DocumentSnapshot.ts";
50
+ import type * as FirestoreModel from "./FirestoreModel.ts";
51
+ import type { FirestoreValue } from "./FirestoreValue.ts";
52
+ import * as firestore from "./internal/firestore.ts";
53
+ import type { FirestoreError, UndefinedCollectionError } from "./internal/firestoreError.ts";
54
+ import * as internalStreamFromListener from "./internal/streamFromListener.ts";
55
+ import * as internalTransactionFromCallback from "./internal/transactionFromCallback.ts";
56
+ import type * as QuerySnapshot from "./QuerySnapshot.ts";
57
+
58
+ const TypeId = firestore.TypeId;
59
+
60
+ const TransactionTypeId = firestore.TransactionTypeId;
61
+
62
+ export {
63
+ /**
64
+ * The error every Firestore call fails with when the backend fails it.
65
+ *
66
+ * It carries the backend's original error as its `cause`, and its `code`
67
+ * where the SDK names one, such as `permission-denied`. The client SDK
68
+ * always names it. The Admin SDK reports gRPC statuses by number, so there
69
+ * `code` is often absent.
70
+ *
71
+ * @stability unstable
72
+ * @category errors
73
+ * @since 0.2.0
74
+ */
75
+ FirestoreError,
76
+ /**
77
+ * The error a read of a {@link CollectionGroup.CollectionGroup} fails with
78
+ * when it finds a document at a path none of the group's collections define.
79
+ * For example, a root-level `posts` document when the group is made of
80
+ * `users/{id}/posts`.
81
+ *
82
+ * **Details**
83
+ *
84
+ * It is separate from {@link FirestoreError} because the call to Firestore
85
+ * succeeded: the data contradicts the model. It is an error rather than a
86
+ * defect because the database contents decide whether it happens, so
87
+ * correct code cannot rule it out. If you treat it as a bug, `Effect.orDie`
88
+ * it.
89
+ *
90
+ * @stability unstable
91
+ * @category errors
92
+ * @since 0.2.0
93
+ */
94
+ UndefinedCollectionError,
95
+ } from "./internal/firestoreError.ts";
96
+
97
+ /**
98
+ * The data {@link With.setDoc} and {@link With.addDoc} take for a document in
99
+ * `C`: the `create` shape of the collection's model.
100
+ *
101
+ * @see {@link Update} for the data {@link With.updateDoc} takes
102
+ *
103
+ * @stability unstable
104
+ * @category models
105
+ * @since 0.2.0
106
+ */
107
+ export type Create<C extends Collection.Any> = Collection.DocumentSchema<C>["create"]["Type"];
108
+
109
+ /**
110
+ * The schema an update to a document in `C` is encoded through in `Mode`: the
111
+ * model's `update` schema, or its `updateUnchecked` schema.
112
+ *
113
+ * @see {@link Update} for the patch type itself
114
+ *
115
+ * @stability unstable
116
+ * @category models
117
+ * @since 0.2.0
118
+ */
119
+ export type UpdateSchema<
120
+ C extends Collection.Any,
121
+ Mode extends FirestoreModel.UpdateMode = "checked",
122
+ > = Mode extends "unchecked"
123
+ ? Collection.DocumentSchema<C>["updateUnchecked"]
124
+ : Collection.DocumentSchema<C>["update"];
125
+
126
+ /**
127
+ * The patch {@link With.updateDoc} takes for a document in `C` in `Mode`.
128
+ *
129
+ * @see {@link Create} for the data {@link With.setDoc} takes
130
+ * @see {@link FirestoreModel.UpdateMode} for which patches each mode accepts
131
+ *
132
+ * @stability unstable
133
+ * @category models
134
+ * @since 0.2.0
135
+ */
136
+ export type Update<
137
+ C extends Collection.Any,
138
+ Mode extends FirestoreModel.UpdateMode = "checked",
139
+ > = UpdateSchema<C, Mode>["Type"];
140
+
141
+ /**
142
+ * Options for {@link With.updateDoc}.
143
+ *
144
+ * @stability unstable
145
+ * @category models
146
+ * @since 0.2.0
147
+ */
148
+ export interface UpdateOptions<Mode extends FirestoreModel.UpdateMode = "checked"> {
149
+ /**
150
+ * Which patches the update accepts.
151
+ *
152
+ * **When to use**
153
+ *
154
+ * Leave it at `"checked"`, the default, which only accepts patches that
155
+ * leave every document readable. Use `"unchecked"` to write into an optional
156
+ * struct or a union member's key, when you know the document holds it, for
157
+ * example from an earlier read in the same transaction.
158
+ *
159
+ * @see {@link FirestoreModel.UpdateMode} for exactly which patches each mode accepts
160
+ */
161
+ readonly mode?: Mode | undefined;
162
+ }
163
+
164
+ /**
165
+ * Options for {@link With.runTransaction}.
166
+ *
167
+ * @stability unstable
168
+ * @category models
169
+ * @since 0.2.0
170
+ */
171
+ export interface TransactionOptions {
172
+ /**
173
+ * How many times the body runs before a transaction that keeps conflicting
174
+ * with other writes gives up. Defaults to the SDK's own default, 5.
175
+ */
176
+ readonly maxAttempts?: number | undefined;
177
+ }
178
+
179
+ /**
180
+ * The transaction a {@link With.runTransaction} body reads and writes
181
+ * through.
182
+ *
183
+ * **When to use**
184
+ *
185
+ * Ask for it in a helper that must run inside a transaction. Requiring it in
186
+ * the type says the helper belongs in a transaction and may run more than
187
+ * once.
188
+ *
189
+ * **Details**
190
+ *
191
+ * It follows Firestore's transaction rules: every read comes before every
192
+ * write, and writes are queued and only sent when the body finishes, so they
193
+ * cannot be read back inside the transaction.
194
+ *
195
+ * It reads one document at a time, never a whole collection, because the
196
+ * client SDK's transactions only read documents.
197
+ *
198
+ * @see {@link AdminFirestore.AdminTransaction} for an Admin SDK transaction that can also read a whole collection
199
+ *
200
+ * @example Moving a counter inside a transaction
201
+ * ```ts
202
+ * import { Effect, Schema } from "effect";
203
+ * import { Collection, DocumentSnapshot, Firestore, FirestoreModel } from "@visiolab/effect-firebase/firestore";
204
+ *
205
+ * class Counter extends FirestoreModel.Class<Counter>("Counter")({ count: Schema.Finite }) {}
206
+ *
207
+ * const Counters = Collection.make("counters", Counter);
208
+ *
209
+ * const bump = (id: string) =>
210
+ * Effect.gen(function* () {
211
+ * const transaction = yield* Firestore.Transaction;
212
+ * const ref = Counters.doc(Counters.id.make(id));
213
+ * const snapshot = yield* transaction.getDoc(ref);
214
+ * const count = DocumentSnapshot.isExisting(snapshot) ? (yield* snapshot.data).count : 0;
215
+ * yield* transaction.setDoc(ref, { count: count + 1 });
216
+ * });
217
+ *
218
+ * const program = Effect.gen(function* () {
219
+ * const firestore = yield* Firestore.Firestore;
220
+ * yield* firestore.runTransaction(bump("visits"));
221
+ * });
222
+ * ```
223
+ *
224
+ * @stability unstable
225
+ * @category models
226
+ * @since 0.2.0
227
+ */
228
+ export interface Transaction extends TransactionBase {
229
+ readonly [TransactionTypeId]: typeof TransactionTypeId;
230
+ }
231
+
232
+ /**
233
+ * What every transaction can do: the reads and writes of {@link Transaction},
234
+ * without the type id that makes a type a {@link Transaction}.
235
+ *
236
+ * **When to use**
237
+ *
238
+ * Extend it to type a backend's richer transaction, as
239
+ * {@link AdminFirestore.AdminTransaction} does, and put {@link Transaction}
240
+ * back in that service's key. Application code asks for {@link Transaction}.
241
+ *
242
+ * **Details**
243
+ *
244
+ * A richer transaction that extended {@link Transaction} would be assignable
245
+ * to it, so every Firestore would drop it from a transaction body's
246
+ * requirements along with {@link Transaction}, and nothing would check that
247
+ * it is provided.
248
+ *
249
+ * @see {@link Transaction} for the portable transaction
250
+ *
251
+ * @stability unstable
252
+ * @category models
253
+ * @since 0.3.0
254
+ */
255
+ export interface TransactionBase {
256
+ /**
257
+ * Reads one document as part of the transaction. Works like
258
+ * {@link With.getDoc}.
259
+ *
260
+ * Every read must come before the transaction's first write.
261
+ */
262
+ readonly getDoc: <C extends Collection.Any>(
263
+ ref: Collection.DocumentReference<C>,
264
+ ) => Effect.Effect<DocumentSnapshot.DocumentSnapshot<C>, FirestoreError>;
265
+
266
+ /**
267
+ * Queues a write of one whole document. Works like {@link With.setDoc}.
268
+ */
269
+ readonly setDoc: <C extends Collection.Any>(
270
+ ref: Collection.DocumentReference<C>,
271
+ data: Create<C>,
272
+ ) => Effect.Effect<
273
+ void,
274
+ FirestoreError | Schema.SchemaError,
275
+ Collection.DocumentSchema<C>["create"]["EncodingServices"]
276
+ >;
277
+
278
+ /**
279
+ * Queues an update of one document. Works like {@link With.updateDoc}.
280
+ */
281
+ readonly updateDoc: <
282
+ C extends Collection.Any,
283
+ Mode extends FirestoreModel.UpdateMode = "checked",
284
+ >(
285
+ ref: Collection.DocumentReference<C>,
286
+ data: Update<C, Mode>,
287
+ options?: UpdateOptions<Mode>,
288
+ ) => Effect.Effect<
289
+ void,
290
+ FirestoreError | Schema.SchemaError,
291
+ Collection.DocumentSchema<C>["update"]["EncodingServices"]
292
+ >;
293
+ }
294
+
295
+ /**
296
+ * Service key for the {@link Transaction} a {@link With.runTransaction} body
297
+ * runs in.
298
+ *
299
+ * @stability unstable
300
+ * @category services
301
+ * @since 0.2.0
302
+ */
303
+ export const Transaction = Context.Service<Transaction>(
304
+ "@visiolab/effect-firebase/firestore/Firestore/Transaction",
305
+ );
306
+
307
+ /**
308
+ * The trailing parameters a {@link With} read or listen takes: none when
309
+ * `Options` is `never`, as on the portable {@link Firestore}, and an optional
310
+ * `options` otherwise.
311
+ *
312
+ * @stability unstable
313
+ * @category models
314
+ * @since 0.3.0
315
+ */
316
+ export type OptionsParameter<Options> = [Options] extends [never] ? [] : [options?: Options];
317
+
318
+ /**
319
+ * The Firestore service with a backend's additions: the options its reads and
320
+ * listens take, the extra fields its snapshots carry, and further services a
321
+ * transaction body can ask for.
322
+ *
323
+ * **When to use**
324
+ *
325
+ * Use it to type a backend's richer service, as
326
+ * {@link AdminFirestore.AdminFirestore} and
327
+ * {@link ClientFirestore.ClientFirestore} do, or for what {@link make}
328
+ * returns. Application code asks for {@link Firestore}, or for a backend's own
329
+ * service where it needs that backend's features.
330
+ *
331
+ * **Details**
332
+ *
333
+ * - `ReadOptions`: what {@link With.getDoc} and {@link With.getDocs} take
334
+ * besides a reference, such as the client's read `source`. None by default.
335
+ * - `ListenOptions`: what {@link With.streamDoc} and {@link With.streamDocs}
336
+ * take besides a reference. None by default.
337
+ * - `Extra`: what a backend knows about a read beyond the portable snapshot,
338
+ * such as the client's `metadata`. It is on every document snapshot, on
339
+ * each document of a query snapshot, and on the query snapshot itself.
340
+ * - `TransactionServices`: services a {@link With.runTransaction} body can ask
341
+ * for besides {@link Transaction}, such as a backend's own richer
342
+ * transaction. A service's type must not be assignable to
343
+ * {@link Transaction}, so extend {@link TransactionBase}, give it a type id
344
+ * of its own, and put {@link Transaction} back in its key's service type,
345
+ * as {@link AdminFirestore.AdminTransaction} does. A type that is
346
+ * assignable to {@link Transaction}, or to another backend's service, would
347
+ * be dropped from the body's requirements along with it, so nothing would
348
+ * check that it is provided.
349
+ *
350
+ * TypeScript does not compare `TransactionServices` when one `With` is
351
+ * assigned to another, so a backend should test that a transaction body can
352
+ * ask for each service it claims to offer.
353
+ *
354
+ * @see {@link Firestore} for the portable service, which is this with every parameter at its default
355
+ *
356
+ * @stability unstable
357
+ * @category models
358
+ * @since 0.3.0
359
+ */
360
+ export interface With<
361
+ ReadOptions = never,
362
+ ListenOptions = never,
363
+ Extra extends object = {},
364
+ TransactionServices = never,
365
+ > {
366
+ /**
367
+ * Reads one document.
368
+ *
369
+ * **When to use**
370
+ *
371
+ * Use to read a document once. A missing document is not a failure: you get
372
+ * a {@link DocumentSnapshot.Missing} snapshot, and
373
+ * {@link DocumentSnapshot.isExisting} tells the two cases apart.
374
+ *
375
+ * **Details**
376
+ *
377
+ * The snapshot's data is decoded through the collection's model only when
378
+ * you run its `data` effect, so a document that does not match the model
379
+ * fails there, not here.
380
+ *
381
+ * @see {@link With.streamDoc} to keep receiving the document as it changes
382
+ * @see {@link With.getDocs} to read every document in a collection
383
+ *
384
+ * @example Reading a document
385
+ * ```ts
386
+ * import { Effect, Schema } from "effect";
387
+ * import { Collection, DocumentSnapshot, Firestore, FirestoreModel } from "@visiolab/effect-firebase/firestore";
388
+ *
389
+ * class User extends FirestoreModel.Class<User>("User")({ name: Schema.String }) {}
390
+ *
391
+ * const Users = Collection.make("users", User);
392
+ *
393
+ * const program = Effect.gen(function* () {
394
+ * const firestore = yield* Firestore.Firestore;
395
+ * const snapshot = yield* firestore.getDoc(Users.doc(Users.id.make("alice")));
396
+ * if (DocumentSnapshot.isMissing(snapshot)) return undefined;
397
+ * return yield* snapshot.data;
398
+ * });
399
+ * ```
400
+ */
401
+ readonly getDoc: <C extends Collection.Any>(
402
+ ref: Collection.DocumentReference<C>,
403
+ ...options: OptionsParameter<ReadOptions>
404
+ ) => Effect.Effect<DocumentSnapshot.DocumentSnapshot<C> & Extra, FirestoreError>;
405
+
406
+ /**
407
+ * Reads every document in a collection, or in a
408
+ * {@link CollectionGroup.CollectionGroup}.
409
+ *
410
+ * **When to use**
411
+ *
412
+ * Use to read a whole collection once. Pass a collection reference to read
413
+ * one collection under one parent, or a Collection Group to read every
414
+ * collection of the group under every parent.
415
+ *
416
+ * **Details**
417
+ *
418
+ * Returns a {@link QuerySnapshot.QuerySnapshot} whose `docs` are
419
+ * {@link DocumentSnapshot.Existing} snapshots, one per document found. An
420
+ * empty collection reads as a snapshot with no `docs`.
421
+ *
422
+ * Reading a group matches each document by its path to the group's
423
+ * collection it lives in, and addresses and decodes it as that collection's.
424
+ * A document under the group's collection id at a path none of its
425
+ * collections define fails the read with an
426
+ * {@link UndefinedCollectionError}. Reading a collection reference fails
427
+ * with {@link FirestoreError} only.
428
+ *
429
+ * @see {@link With.streamDocs} to keep receiving the documents as they change
430
+ * @see {@link With.getDoc} to read one document
431
+ *
432
+ * @example Reading a collection
433
+ * ```ts
434
+ * import { Effect, Schema } from "effect";
435
+ * import { Collection, Firestore, FirestoreModel } from "@visiolab/effect-firebase/firestore";
436
+ *
437
+ * class User extends FirestoreModel.Class<User>("User")({ name: Schema.String }) {}
438
+ *
439
+ * const Users = Collection.make("users", User);
440
+ *
441
+ * const program = Effect.gen(function* () {
442
+ * const firestore = yield* Firestore.Firestore;
443
+ * const snapshot = yield* firestore.getDocs(Users.collection());
444
+ * return yield* Effect.forEach(snapshot.docs, (doc) => doc.data);
445
+ * });
446
+ * ```
447
+ */
448
+ readonly getDocs: {
449
+ <C extends Collection.Any>(
450
+ ref: Collection.CollectionReference<C>,
451
+ ...options: OptionsParameter<ReadOptions>
452
+ ): Effect.Effect<QuerySnapshot.QuerySnapshot<C, Extra> & Extra, FirestoreError>;
453
+ <G extends CollectionGroup.Any>(
454
+ group: G,
455
+ ...options: OptionsParameter<ReadOptions>
456
+ ): Effect.Effect<
457
+ QuerySnapshot.QuerySnapshot<CollectionGroup.Members<G>, Extra> & Extra,
458
+ FirestoreError | UndefinedCollectionError
459
+ >;
460
+ };
461
+
462
+ /**
463
+ * Listens to one document, emitting a snapshot each time it changes.
464
+ *
465
+ * **When to use**
466
+ *
467
+ * Use to keep a view of a document up to date. For a one-off read, use
468
+ * {@link With.getDoc}.
469
+ *
470
+ * **Details**
471
+ *
472
+ * The first snapshot is the document as it is when the listener attaches. A
473
+ * document that is not there, or is deleted later, comes through as
474
+ * {@link DocumentSnapshot.Missing}.
475
+ *
476
+ * Each snapshot is the whole document, so a reader slower than the changes
477
+ * only sees the latest one: a snapshot not pulled before the next arrives is
478
+ * dropped.
479
+ *
480
+ * The stream never ends on its own. Each run attaches its own listener, which
481
+ * stays until the stream is interrupted. The stream fails with a
482
+ * {@link FirestoreError} when Firestore drops the listener.
483
+ *
484
+ * @see {@link With.getDoc} to read the document once
485
+ * @see {@link With.streamDocs} to listen to a whole collection
486
+ *
487
+ * @example Logging each change to a document
488
+ * ```ts
489
+ * import { Effect, Schema, Stream } from "effect";
490
+ * import { Collection, DocumentSnapshot, Firestore, FirestoreModel } from "@visiolab/effect-firebase/firestore";
491
+ *
492
+ * class User extends FirestoreModel.Class<User>("User")({ name: Schema.String }) {}
493
+ *
494
+ * const Users = Collection.make("users", User);
495
+ *
496
+ * const program = Effect.gen(function* () {
497
+ * const firestore = yield* Firestore.Firestore;
498
+ * yield* firestore.streamDoc(Users.doc(Users.id.make("alice"))).pipe(
499
+ * Stream.filter(DocumentSnapshot.isExisting),
500
+ * Stream.mapEffect((snapshot) => snapshot.data),
501
+ * Stream.runForEach((user) => Effect.log(user.name)),
502
+ * );
503
+ * });
504
+ * ```
505
+ */
506
+ readonly streamDoc: <C extends Collection.Any>(
507
+ ref: Collection.DocumentReference<C>,
508
+ ...options: OptionsParameter<ListenOptions>
509
+ ) => Stream.Stream<DocumentSnapshot.DocumentSnapshot<C> & Extra, FirestoreError>;
510
+
511
+ /**
512
+ * Listens to every document in a collection, or in a
513
+ * {@link CollectionGroup.CollectionGroup}, emitting all of them each time
514
+ * any of them changes.
515
+ *
516
+ * **When to use**
517
+ *
518
+ * Use to keep a view of a whole collection up to date. For a one-off read,
519
+ * use {@link With.getDocs}.
520
+ *
521
+ * **Details**
522
+ *
523
+ * Each element is a {@link QuerySnapshot.QuerySnapshot} of every document
524
+ * there at that moment, as {@link With.getDocs} would return it. As with
525
+ * {@link With.streamDoc}, a snapshot not pulled before the next arrives is
526
+ * dropped, and the stream never ends on its own.
527
+ *
528
+ * Listening to a group matches each document to its collection as
529
+ * {@link With.getDocs} does. The stream fails with an
530
+ * {@link UndefinedCollectionError} at the first snapshot holding a document
531
+ * at a path none of the group's collections define. Listening to a
532
+ * collection reference fails with {@link FirestoreError} only.
533
+ *
534
+ * @see {@link With.getDocs} to read the documents once
535
+ * @see {@link With.streamDoc} to listen to one document
536
+ */
537
+ readonly streamDocs: {
538
+ <C extends Collection.Any>(
539
+ ref: Collection.CollectionReference<C>,
540
+ ...options: OptionsParameter<ListenOptions>
541
+ ): Stream.Stream<QuerySnapshot.QuerySnapshot<C, Extra> & Extra, FirestoreError>;
542
+ <G extends CollectionGroup.Any>(
543
+ group: G,
544
+ ...options: OptionsParameter<ListenOptions>
545
+ ): Stream.Stream<
546
+ QuerySnapshot.QuerySnapshot<CollectionGroup.Members<G>, Extra> & Extra,
547
+ FirestoreError | UndefinedCollectionError
548
+ >;
549
+ };
550
+
551
+ /**
552
+ * Writes a whole document under an id you choose, replacing whatever was
553
+ * there.
554
+ *
555
+ * **When to use**
556
+ *
557
+ * Use to create a document at a known id, or to overwrite one completely.
558
+ *
559
+ * **Details**
560
+ *
561
+ * The data is the model's `create` shape and is written whole: fields left
562
+ * out are removed, not kept. The model asks for every field, so this is
563
+ * rarely a surprise.
564
+ *
565
+ * The data is encoded before anything is sent, so a value that does not
566
+ * encode fails with a `SchemaError` and nothing is written.
567
+ *
568
+ * @see {@link With.addDoc} to write under an id Firestore picks
569
+ * @see {@link With.updateDoc} to change some fields and keep the rest
570
+ *
571
+ * @example Writing a document
572
+ * ```ts
573
+ * import { Effect, Schema } from "effect";
574
+ * import { Collection, Firestore, FirestoreModel } from "@visiolab/effect-firebase/firestore";
575
+ *
576
+ * class User extends FirestoreModel.Class<User>("User")({ name: Schema.String }) {}
577
+ *
578
+ * const Users = Collection.make("users", User);
579
+ *
580
+ * const program = Effect.gen(function* () {
581
+ * const firestore = yield* Firestore.Firestore;
582
+ * yield* firestore.setDoc(Users.doc(Users.id.make("alice")), { name: "Alice" });
583
+ * });
584
+ * ```
585
+ */
586
+ readonly setDoc: <C extends Collection.Any>(
587
+ ref: Collection.DocumentReference<C>,
588
+ data: Create<C>,
589
+ ) => Effect.Effect<
590
+ void,
591
+ FirestoreError | Schema.SchemaError,
592
+ Collection.DocumentSchema<C>["create"]["EncodingServices"]
593
+ >;
594
+
595
+ /**
596
+ * Writes a new document under an id Firestore picks, and returns its
597
+ * reference.
598
+ *
599
+ * **When to use**
600
+ *
601
+ * Use to create a document when its id does not matter.
602
+ *
603
+ * **Details**
604
+ *
605
+ * The data is the model's `create` shape, as for {@link With.setDoc}, and
606
+ * is encoded before anything is sent.
607
+ *
608
+ * @see {@link With.setDoc} to write under an id you choose
609
+ *
610
+ * @example Adding a document
611
+ * ```ts
612
+ * import { Effect, Schema } from "effect";
613
+ * import { Collection, Firestore, FirestoreModel } from "@visiolab/effect-firebase/firestore";
614
+ *
615
+ * class Post extends FirestoreModel.Class<Post>("Post")({ title: Schema.String }) {}
616
+ *
617
+ * const Posts = Collection.make("posts", Post);
618
+ *
619
+ * const program = Effect.gen(function* () {
620
+ * const firestore = yield* Firestore.Firestore;
621
+ * const ref = yield* firestore.addDoc(Posts.collection(), { title: "Hello" });
622
+ * yield* Effect.log(`Added ${ref.id}`);
623
+ * });
624
+ * ```
625
+ */
626
+ readonly addDoc: <C extends Collection.Any>(
627
+ ref: Collection.CollectionReference<C>,
628
+ data: Create<C>,
629
+ ) => Effect.Effect<
630
+ Collection.DocumentReference<C>,
631
+ FirestoreError | Schema.SchemaError,
632
+ Collection.DocumentSchema<C>["create"]["EncodingServices"]
633
+ >;
634
+
635
+ /**
636
+ * Changes some fields of an existing document and keeps the rest.
637
+ *
638
+ * **When to use**
639
+ *
640
+ * Use to change part of a document without reading and rewriting it, for
641
+ * example with {@link FieldValue.increment} or
642
+ * {@link FieldValue.serverTimestamp}.
643
+ *
644
+ * **Details**
645
+ *
646
+ * The patch is encoded through the model's `update` schema, or its
647
+ * `updateUnchecked` schema when `mode` is `"unchecked"`. A nested struct in
648
+ * the patch is written field by field at its paths. Wrap it in
649
+ * {@link Patch.replace} to write it whole.
650
+ *
651
+ * The document must exist: Firestore fails the update with a
652
+ * {@link FirestoreError} when it does not, and also rejects an empty patch.
653
+ *
654
+ * Both modes encode the same fields at the same paths, so they require the
655
+ * same services.
656
+ *
657
+ * @see {@link With.setDoc} to write a whole document, existing or not
658
+ * @see {@link UpdateOptions.mode} for when to use `"unchecked"`
659
+ *
660
+ * @example Updating a field
661
+ * ```ts
662
+ * import { Effect, Schema } from "effect";
663
+ * import { Collection, FieldValue, Firestore, FirestoreModel } from "@visiolab/effect-firebase/firestore";
664
+ *
665
+ * class Post extends FirestoreModel.Class<Post>("Post")({
666
+ * title: Schema.String,
667
+ * views: FirestoreModel.Number(Schema.Finite),
668
+ * }) {}
669
+ *
670
+ * const Posts = Collection.make("posts", Post);
671
+ *
672
+ * const program = Effect.gen(function* () {
673
+ * const firestore = yield* Firestore.Firestore;
674
+ * yield* firestore.updateDoc(Posts.doc(Posts.id.make("hello")), {
675
+ * views: FieldValue.increment(1),
676
+ * });
677
+ * });
678
+ * ```
679
+ */
680
+ readonly updateDoc: <
681
+ C extends Collection.Any,
682
+ Mode extends FirestoreModel.UpdateMode = "checked",
683
+ >(
684
+ ref: Collection.DocumentReference<C>,
685
+ data: Update<C, Mode>,
686
+ options?: UpdateOptions<Mode>,
687
+ ) => Effect.Effect<
688
+ void,
689
+ FirestoreError | Schema.SchemaError,
690
+ Collection.DocumentSchema<C>["update"]["EncodingServices"]
691
+ >;
692
+
693
+ /**
694
+ * Runs `effect` in a transaction, providing the {@link Transaction} it reads
695
+ * and writes through.
696
+ *
697
+ * **When to use**
698
+ *
699
+ * Use when writes depend on what was read, and must not be based on data
700
+ * another write has changed in the meantime.
701
+ *
702
+ * **Details**
703
+ *
704
+ * When another write conflicts, the SDK discards the queued writes and runs
705
+ * `effect` again from the start. Anything else `effect` does, such as
706
+ * sending an email, writing through the service rather than the
707
+ * transaction, or running another transaction, happens once per attempt, so
708
+ * do it after the transaction instead.
709
+ *
710
+ * A `FirestoreError` is retried where the SDK would retry it: a conflict, or
711
+ * a passing outage such as the database being briefly unavailable. That
712
+ * holds for any `FirestoreError` `effect` fails with, including one from a
713
+ * call outside the transaction. Any other failure, such as an error of your
714
+ * own, a defect, or a write that does not encode, rolls the transaction back
715
+ * and fails with exactly that error.
716
+ *
717
+ * A transaction run inside another is independent: it commits when it is
718
+ * done, whatever happens to the outer one.
719
+ *
720
+ * Interrupting it interrupts `effect`, rolls the transaction back, and
721
+ * returns once the rollback is done.
722
+ *
723
+ * A backend that offers `TransactionServices` provides the same transaction
724
+ * object under each of them, so `effect` does not need them from its caller.
725
+ *
726
+ * @see {@link Transaction} for what the body can read and write
727
+ * @see {@link TransactionOptions} for how many attempts it makes
728
+ *
729
+ * @example Running a transaction
730
+ * ```ts
731
+ * import { Effect, Schema } from "effect";
732
+ * import { Collection, DocumentSnapshot, Firestore, FirestoreModel } from "@visiolab/effect-firebase/firestore";
733
+ *
734
+ * class Account extends FirestoreModel.Class<Account>("Account")({ balance: Schema.Finite }) {}
735
+ *
736
+ * const Accounts = Collection.make("accounts", Account);
737
+ *
738
+ * const withdraw = (id: string, amount: number) =>
739
+ * Effect.gen(function* () {
740
+ * const firestore = yield* Firestore.Firestore;
741
+ * yield* firestore.runTransaction(
742
+ * Effect.gen(function* () {
743
+ * const transaction = yield* Firestore.Transaction;
744
+ * const ref = Accounts.doc(Accounts.id.make(id));
745
+ * const snapshot = yield* transaction.getDoc(ref);
746
+ * if (DocumentSnapshot.isMissing(snapshot)) return yield* Effect.fail("NoSuchAccount" as const);
747
+ * const { balance } = yield* snapshot.data;
748
+ * if (balance < amount) return yield* Effect.fail("InsufficientFunds" as const);
749
+ * yield* transaction.updateDoc(ref, { balance: balance - amount });
750
+ * }),
751
+ * { maxAttempts: 3 },
752
+ * );
753
+ * });
754
+ * ```
755
+ */
756
+ readonly runTransaction: <A, E, R>(
757
+ effect: Effect.Effect<A, E, R>,
758
+ options?: TransactionOptions,
759
+ ) => Effect.Effect<A, E | FirestoreError, Exclude<R, Transaction | TransactionServices>>;
760
+ }
761
+
762
+ /**
763
+ * The portable Firestore service for a single app, which every backend
764
+ * provides.
765
+ *
766
+ * **When to use**
767
+ *
768
+ * Ask for it in code that should run on any backend, the Admin SDK or the
769
+ * client SDK. Its methods are documented on {@link With}.
770
+ *
771
+ * @see {@link With} for each method, and for what a backend's richer service adds
772
+ * @see {@link AdminFirestore.AdminFirestore} for the Admin SDK's richer service
773
+ * @see {@link ClientFirestore.ClientFirestore} for the client SDK's richer service
774
+ *
775
+ * @stability unstable
776
+ * @category models
777
+ * @since 0.2.0
778
+ */
779
+ export interface Firestore extends With {
780
+ readonly [TypeId]: typeof TypeId;
781
+ }
782
+
783
+ /**
784
+ * Service key for the portable {@link Firestore}.
785
+ *
786
+ * Provide it with {@link AdminFirestore.layer} or
787
+ * {@link ClientFirestore.layer}.
788
+ *
789
+ * @stability unstable
790
+ * @category services
791
+ * @since 0.2.0
792
+ */
793
+ export const Firestore = Context.Service<Firestore>(
794
+ "@visiolab/effect-firebase/firestore/Firestore",
795
+ );
796
+
797
+ /**
798
+ * What a {@link MakeOptions} read or listen hook for many documents reads:
799
+ * one collection, by its path, or every collection with one collection id,
800
+ * wherever it sits.
801
+ *
802
+ * **Details**
803
+ *
804
+ * {@link make} builds it from the collection reference or
805
+ * {@link CollectionGroup.CollectionGroup} the caller passed, and keeps the
806
+ * group itself, so a hook only turns it into its backend's query.
807
+ *
808
+ * @stability unstable
809
+ * @category models
810
+ * @since 0.3.0
811
+ */
812
+ export type CollectionSelector =
813
+ | {
814
+ readonly _tag: "Collection";
815
+ /** The collection's path, such as `users/alice/posts`. */
816
+ readonly path: string;
817
+ }
818
+ | {
819
+ readonly _tag: "CollectionGroup";
820
+ /** The collection id every collection read shares, such as `posts`. */
821
+ readonly id: string;
822
+ };
823
+
824
+ /**
825
+ * A document's data as a {@link MakeOptions} hook reads and writes it: a map
826
+ * of Firestore Values.
827
+ *
828
+ * **Details**
829
+ *
830
+ * For a write it is either a whole document, for `setDoc` and `addDoc`, or a
831
+ * map from field paths such as `address.city` to the values written there, for
832
+ * `updateDoc`.
833
+ *
834
+ * @stability unstable
835
+ * @category models
836
+ * @since 0.3.0
837
+ */
838
+ export type DocumentData = { readonly [field: string]: FirestoreValue };
839
+
840
+ /**
841
+ * The `extra` field of what a {@link MakeOptions} read hook returns: what the
842
+ * backend knows about the read beyond the portable snapshot.
843
+ *
844
+ * **Details**
845
+ *
846
+ * Each field of `extra` is put on the snapshot and shown when it prints, as
847
+ * {@link DocumentSnapshot.make} does. Its fields must not clash with the
848
+ * snapshot's own.
849
+ *
850
+ * - When `Extra` has required fields, `extra` is required, so a snapshot never
851
+ * claims extra data its read did not return.
852
+ * - When every field is optional, `extra` may be left out.
853
+ * - When `Extra` has no fields, `extra` must be left out, so a hook cannot
854
+ * return extra data the snapshots do not carry.
855
+ *
856
+ * @stability unstable
857
+ * @category models
858
+ * @since 0.3.0
859
+ */
860
+ export type ReadExtra<Extra extends object> = [keyof Extra] extends [never]
861
+ ? { readonly extra?: undefined }
862
+ : {} extends Extra
863
+ ? { readonly extra?: Extra | undefined }
864
+ : { readonly extra: Extra };
865
+
866
+ /**
867
+ * What a {@link MakeOptions} hook returns for one document, with the `Extra`
868
+ * the backend knows about it.
869
+ *
870
+ * @see {@link QueryDocumentRead} for one document of many
871
+ * @see {@link ReadExtra} for its `extra`
872
+ *
873
+ * @stability unstable
874
+ * @category models
875
+ * @since 0.3.0
876
+ */
877
+ export type DocumentRead<Extra extends object = {}> = {
878
+ /**
879
+ * The document's data, or `undefined` when it is not there. Data that is not
880
+ * made of Firestore Values fails the read with a {@link FirestoreError}.
881
+ */
882
+ readonly data: DocumentData | undefined;
883
+ } & ReadExtra<Extra>;
884
+
885
+ /**
886
+ * What a {@link MakeOptions} hook returns for one document of many: always one
887
+ * that exists, with the `Extra` the backend knows about it.
888
+ *
889
+ * @see {@link DocumentRead} for a single document, which may be missing
890
+ * @see {@link ReadExtra} for its `extra`
891
+ *
892
+ * @stability unstable
893
+ * @category models
894
+ * @since 0.3.0
895
+ */
896
+ export type QueryDocumentRead<Extra extends object = {}> = {
897
+ /**
898
+ * The document's full path, such as `users/alice/posts/hello`. It matches a
899
+ * document of a Collection Group to the collection it lives in.
900
+ */
901
+ readonly path: string;
902
+ /**
903
+ * The document's data. Data that is not made of Firestore Values fails the
904
+ * read with a {@link FirestoreError}.
905
+ */
906
+ readonly data: DocumentData;
907
+ } & ReadExtra<Extra>;
908
+
909
+ /**
910
+ * What a {@link MakeOptions} hook returns for many documents, with the
911
+ * `Extra` the backend knows about each of them and about the read as a whole.
912
+ *
913
+ * **Details**
914
+ *
915
+ * The read's own `extra` is put on the {@link QuerySnapshot.QuerySnapshot}, as
916
+ * {@link QuerySnapshot.make} does.
917
+ *
918
+ * @see {@link ReadExtra} for its `extra`
919
+ *
920
+ * @stability unstable
921
+ * @category models
922
+ * @since 0.3.0
923
+ */
924
+ export type QueryRead<Extra extends object = {}> = {
925
+ /** Every document read. */
926
+ readonly docs: ReadonlyArray<QueryDocumentRead<Extra>>;
927
+ } & ReadExtra<Extra>;
928
+
929
+ /**
930
+ * The reads and writes of one transaction attempt, which a
931
+ * {@link RunTransaction.run} hook hands to the body. {@link make} builds the
932
+ * {@link Transaction} the body uses from them.
933
+ *
934
+ * **Details**
935
+ *
936
+ * Writes in a transaction are queued until it commits, so a backend may
937
+ * complete `setDoc` and `updateDoc` straight away.
938
+ *
939
+ * @stability unstable
940
+ * @category models
941
+ * @since 0.3.0
942
+ */
943
+ export interface TransactionOperations {
944
+ /** Reads the document at `path` as part of the transaction. */
945
+ readonly getDoc: (path: string) => Effect.Effect<DocumentRead, FirestoreError>;
946
+ /** Queues a write of the whole document at `path`. */
947
+ readonly setDoc: (path: string, data: DocumentData) => Effect.Effect<void, FirestoreError>;
948
+ /** Queues an update of the document at `path`, with `data` keyed by field path. */
949
+ readonly updateDoc: (path: string, data: DocumentData) => Effect.Effect<void, FirestoreError>;
950
+ /**
951
+ * Reads many documents as part of the transaction, for a backend that can.
952
+ *
953
+ * When it is given, the transaction {@link make} builds has a `getDocs` that
954
+ * works like {@link With.getDocs}, which {@link RunTransaction.context} can
955
+ * offer under a key of its own.
956
+ */
957
+ readonly getDocs?:
958
+ | ((selector: CollectionSelector) => Effect.Effect<QueryRead, FirestoreError>)
959
+ | undefined;
960
+ }
961
+
962
+ /**
963
+ * The transaction {@link make} builds from `Operations` and hands to
964
+ * {@link RunTransaction.context}: a {@link Transaction}, plus a `getDocs` that
965
+ * works like {@link With.getDocs} when `Operations` has `getDocs`.
966
+ *
967
+ * @stability unstable
968
+ * @category models
969
+ * @since 0.3.0
970
+ */
971
+ export type BuiltTransaction<Operations extends TransactionOperations> = Transaction &
972
+ (Operations extends { readonly getDocs: NonNullable<TransactionOperations["getDocs"]> }
973
+ ? { readonly getDocs: Firestore["getDocs"] }
974
+ : {});
975
+
976
+ /**
977
+ * The {@link MakeOptions.runTransaction} hook: how a backend runs a
978
+ * transaction, and which further services a body can find the transaction
979
+ * under.
980
+ *
981
+ * **Details**
982
+ *
983
+ * `Operations` is what the backend can do in a transaction. It decides
984
+ * whether the transaction {@link make} builds can read many documents.
985
+ *
986
+ * @see {@link transactionFromCallback} to build one for an SDK that runs transactions as a callback
987
+ *
988
+ * @stability unstable
989
+ * @category models
990
+ * @since 0.3.0
991
+ */
992
+ export interface RunTransaction<
993
+ Operations extends TransactionOperations = TransactionOperations,
994
+ TransactionServices = never,
995
+ > {
996
+ /**
997
+ * Runs `body` in a transaction, once per attempt, with that attempt's
998
+ * operations. Fails with what the body failed with, or with a
999
+ * {@link FirestoreError} of its own. Backs {@link With.runTransaction}.
1000
+ *
1001
+ * The body needs nothing from context: everything the caller's effect
1002
+ * needs, including the transaction's span, is already provided, so it may
1003
+ * run on a runtime of its own, such as inside an SDK callback.
1004
+ */
1005
+ readonly run: <A, E>(
1006
+ body: (operations: Operations) => Effect.Effect<A, E>,
1007
+ options: TransactionHookOptions,
1008
+ ) => Effect.Effect<A, E | FirestoreError>;
1009
+
1010
+ /**
1011
+ * Further services a transaction body can find the transaction under,
1012
+ * besides {@link Transaction}, such as a backend's own richer transaction.
1013
+ *
1014
+ * It is handed the transaction {@link make} built for the attempt, and each
1015
+ * service holds that same object, so the body reads and writes through one
1016
+ * transaction whichever key it asks for.
1017
+ */
1018
+ readonly context?:
1019
+ | ((transaction: BuiltTransaction<Operations>) => Context.Context<TransactionServices>)
1020
+ | undefined;
1021
+ }
1022
+
1023
+ /**
1024
+ * The options a {@link RunTransaction.run} hook is handed.
1025
+ *
1026
+ * @stability unstable
1027
+ * @category options
1028
+ * @since 0.3.0
1029
+ */
1030
+ export interface TransactionHookOptions {
1031
+ /**
1032
+ * Only present when the caller set it, so a backend that passes these
1033
+ * options on unchanged keeps its SDK's default otherwise. The client SDK,
1034
+ * for one, turns retrying off when `maxAttempts` is present but
1035
+ * `undefined`.
1036
+ */
1037
+ readonly maxAttempts?: number;
1038
+ }
1039
+
1040
+ /**
1041
+ * The hooks {@link make} builds a Firestore service from.
1042
+ *
1043
+ * **Details**
1044
+ *
1045
+ * Each hook works in Firestore's own terms: document paths, a
1046
+ * {@link CollectionSelector}, and {@link DocumentData}. It converts between
1047
+ * its backend's values and Firestore Values. Each returns an `Effect`, or a
1048
+ * `Stream` for listening, that fails with a {@link FirestoreError} built from
1049
+ * the backend's error, with its `code` and `cause`. {@link make} passes that
1050
+ * error on unchanged.
1051
+ *
1052
+ * - `ReadOptions` and `ListenOptions`: what a backend's reads and listens
1053
+ * take on top of the portable ones, such as the client's read source. The
1054
+ * hook receives whatever the caller passed, or `undefined`.
1055
+ * - `Extra`: what the read and listen hooks know about a read beyond the
1056
+ * portable snapshot, such as the client's `metadata`. It is inferred from
1057
+ * `getDoc`, and `getDocs`, `streamDoc` and `streamDocs` must return the
1058
+ * same, or the hooks do not compile.
1059
+ * - `Operations` and `TransactionServices`: those of the
1060
+ * {@link RunTransaction} hook.
1061
+ *
1062
+ * @stability unstable
1063
+ * @category options
1064
+ * @since 0.3.0
1065
+ */
1066
+ export interface MakeOptions<
1067
+ ReadOptions = never,
1068
+ ListenOptions = never,
1069
+ Extra extends object = {},
1070
+ Operations extends TransactionOperations = TransactionOperations,
1071
+ TransactionServices = never,
1072
+ > {
1073
+ /** Reads the document at `path`. Backs {@link With.getDoc}. */
1074
+ readonly getDoc: (
1075
+ path: string,
1076
+ options: ReadOptions | undefined,
1077
+ ) => Effect.Effect<DocumentRead<Extra>, FirestoreError>;
1078
+
1079
+ /**
1080
+ * Reads every document `selector` names. Backs {@link With.getDocs}, which
1081
+ * matches each document of a Collection Group to its collection.
1082
+ */
1083
+ readonly getDocs: (
1084
+ selector: CollectionSelector,
1085
+ options: ReadOptions | undefined,
1086
+ ) => Effect.Effect<QueryRead<NoInfer<Extra>>, FirestoreError>;
1087
+
1088
+ /**
1089
+ * Writes the whole document at `path`, replacing whatever was there. Backs
1090
+ * {@link With.setDoc}.
1091
+ */
1092
+ readonly setDoc: (path: string, data: DocumentData) => Effect.Effect<void, FirestoreError>;
1093
+
1094
+ /**
1095
+ * Updates the document at `path`, with `data` keyed by field path. Backs
1096
+ * {@link With.updateDoc}.
1097
+ */
1098
+ readonly updateDoc: (path: string, data: DocumentData) => Effect.Effect<void, FirestoreError>;
1099
+
1100
+ /**
1101
+ * Writes a document under a new id in the collection at `path`, and returns
1102
+ * that id. Backs {@link With.addDoc}.
1103
+ */
1104
+ readonly addDoc: (path: string, data: DocumentData) => Effect.Effect<string, FirestoreError>;
1105
+
1106
+ /**
1107
+ * Listens to the document at `path`, emitting a read each time it changes.
1108
+ * Backs {@link With.streamDoc}.
1109
+ *
1110
+ * Each run of the stream should attach its own listener and detach it when
1111
+ * the run stops, keeping only the latest read for a reader that falls
1112
+ * behind. {@link streamFromListener} does this for an SDK listener.
1113
+ */
1114
+ readonly streamDoc: (
1115
+ path: string,
1116
+ options: ListenOptions | undefined,
1117
+ ) => Stream.Stream<DocumentRead<NoInfer<Extra>>, FirestoreError>;
1118
+
1119
+ /**
1120
+ * Listens to every document `selector` names, as
1121
+ * {@link MakeOptions.streamDoc} listens to one. Backs
1122
+ * {@link With.streamDocs}.
1123
+ */
1124
+ readonly streamDocs: (
1125
+ selector: CollectionSelector,
1126
+ options: ListenOptions | undefined,
1127
+ ) => Stream.Stream<QueryRead<NoInfer<Extra>>, FirestoreError>;
1128
+
1129
+ /**
1130
+ * Runs a transaction, and offers it under further services. Backs
1131
+ * {@link With.runTransaction}.
1132
+ *
1133
+ * @see {@link RunTransaction} for what it has to do
1134
+ * @see {@link transactionFromCallback} to build it for an SDK that runs transactions as a callback
1135
+ */
1136
+ readonly runTransaction: RunTransaction<Operations, TransactionServices>;
1137
+ }
1138
+
1139
+ /**
1140
+ * Builds a Firestore service from one backend's hooks.
1141
+ *
1142
+ * **When to use**
1143
+ *
1144
+ * Use to implement a backend, such as a new SDK or an in-memory fake for
1145
+ * tests. Applications use a backend's layer instead, such as
1146
+ * {@link AdminFirestore.layer} or {@link ClientFirestore.layer}.
1147
+ *
1148
+ * **Details**
1149
+ *
1150
+ * The hooks only move Firestore Values to and from the backend. `make`
1151
+ * handles everything above them, the same way for every backend:
1152
+ *
1153
+ * - encoding writes through the collection's model, in its `create` shape or
1154
+ * the update shape for the requested mode
1155
+ * - decoding reads into snapshots, with any extra the hooks return
1156
+ * - matching each document of a Collection Group to its collection
1157
+ * - tracing spans
1158
+ * - providing the {@link Transaction} a transaction body runs in
1159
+ *
1160
+ * The return type is inferred from the hooks: a {@link With} whose reads and
1161
+ * listens take the options the hooks take, whose snapshots carry the extra
1162
+ * they return, and whose transaction bodies get the services the transaction
1163
+ * hook's `context` offers. It is always the portable {@link Firestore} as
1164
+ * well, so a backend can provide it under {@link Firestore}.
1165
+ *
1166
+ * A backend's richer service extends {@link With}, not {@link Firestore}, has a
1167
+ * type id of its own, and its key's service type adds {@link Firestore} back,
1168
+ * as {@link AdminFirestore.AdminFirestore} does. Its type then stays apart from
1169
+ * {@link Firestore}, so providing a {@link Firestore} alone does not satisfy
1170
+ * code that asks for the richer one.
1171
+ *
1172
+ * @see {@link transactionFromCallback} to build the `runTransaction` hook from an SDK callback
1173
+ * @see {@link streamFromListener} to build the `streamDoc` and `streamDocs` hooks from an SDK listener
1174
+ *
1175
+ * @example An in-memory backend
1176
+ * ```ts
1177
+ * import { Effect, Layer, Stream } from "effect";
1178
+ * import { Firestore } from "@visiolab/effect-firebase/firestore";
1179
+ *
1180
+ * const documents = new Map<string, Firestore.DocumentData>();
1181
+ *
1182
+ * const getDoc = (path: string) => Effect.sync(() => ({ data: documents.get(path) }));
1183
+ * const setDoc = (path: string, data: Firestore.DocumentData) =>
1184
+ * Effect.sync(() => void documents.set(path, data));
1185
+ * const updateDoc = (path: string, data: Firestore.DocumentData) =>
1186
+ * Effect.sync(() => void documents.set(path, { ...documents.get(path), ...data }));
1187
+ * const getDocs = (selector: Firestore.CollectionSelector) =>
1188
+ * Effect.sync(() => ({
1189
+ * docs: [...documents]
1190
+ * .filter(([path]) => selector._tag === "Collection" && path.startsWith(`${selector.path}/`))
1191
+ * .map(([path, data]) => ({ path, data })),
1192
+ * }));
1193
+ *
1194
+ * const firestore = Firestore.make({
1195
+ * getDoc,
1196
+ * getDocs,
1197
+ * setDoc,
1198
+ * updateDoc,
1199
+ * addDoc: (path, data) => {
1200
+ * const id = crypto.randomUUID();
1201
+ * return Effect.as(setDoc(`${path}/${id}`, data), id);
1202
+ * },
1203
+ * streamDoc: (path) => Stream.fromEffect(getDoc(path)),
1204
+ * streamDocs: (selector) => Stream.fromEffect(getDocs(selector)),
1205
+ * runTransaction: { run: (body) => body({ getDoc, setDoc, updateDoc }) },
1206
+ * });
1207
+ *
1208
+ * const FirestoreInMemory = Layer.succeed(Firestore.Firestore, firestore);
1209
+ * ```
1210
+ *
1211
+ * @stability unstable
1212
+ * @category constructors
1213
+ * @since 0.3.0
1214
+ */
1215
+ export const make: <
1216
+ ReadOptions = never,
1217
+ ListenOptions = never,
1218
+ Extra extends object = {},
1219
+ Operations extends TransactionOperations = TransactionOperations,
1220
+ TransactionServices = never,
1221
+ >(
1222
+ options: MakeOptions<ReadOptions, ListenOptions, Extra, Operations, TransactionServices>,
1223
+ ) => With<ReadOptions, ListenOptions, Extra, TransactionServices> & Firestore = (options) =>
1224
+ firestore.make(options, Transaction);
1225
+
1226
+ /**
1227
+ * Options for {@link transactionFromCallback}: how to drive an SDK that runs
1228
+ * a transaction as a callback and retries it.
1229
+ *
1230
+ * **Details**
1231
+ *
1232
+ * `T` is the SDK's own transaction object, handed to each attempt. It is
1233
+ * inferred from `operations` alone, so annotate that parameter, as in
1234
+ * `operations: (tx: Admin.Transaction) => ...`. `Operations` and
1235
+ * `TransactionServices` are those of the {@link RunTransaction} hook it
1236
+ * builds.
1237
+ *
1238
+ * The fields may come in any order, with one exception: put `context` after
1239
+ * an `operations` that builds its object inline with unannotated functions,
1240
+ * such as `getDoc: (path) => ...`. TypeScript types `context` as soon as it
1241
+ * reaches it, and only learns `Operations` from such an `operations` once it
1242
+ * reaches that.
1243
+ *
1244
+ * @stability unstable
1245
+ * @category options
1246
+ * @since 0.3.0
1247
+ */
1248
+ export interface TransactionFromCallbackOptions<
1249
+ T,
1250
+ Operations extends TransactionOperations = TransactionOperations,
1251
+ TransactionServices = never,
1252
+ > {
1253
+ /**
1254
+ * Runs `callback` in a transaction and retries it by the SDK's own rules,
1255
+ * such as the Admin SDK's `firestore.runTransaction(callback, options)`.
1256
+ * `options` are the hook's own, passed on unchanged, so `maxAttempts` is
1257
+ * only present when the caller set it.
1258
+ *
1259
+ * The callback takes whatever the SDK passes it and hands it to
1260
+ * `operations` as the SDK's transaction. It is typed `unknown` rather than
1261
+ * `T` because TypeScript would settle `T` from `run` before seeing
1262
+ * `operations` whenever `run` comes first.
1263
+ */
1264
+ readonly run: <A>(
1265
+ callback: (transaction: unknown) => Promise<A>,
1266
+ options: TransactionHookOptions,
1267
+ ) => Promise<A>;
1268
+ /** The reads and writes of one attempt, over the SDK's transaction. */
1269
+ readonly operations: (transaction: T) => Operations;
1270
+ /**
1271
+ * Builds the error for a rejection of `run` that does not come from the
1272
+ * body, such as a failed commit, with the SDK's `code` and `cause`.
1273
+ */
1274
+ readonly onError: (cause: unknown) => FirestoreError;
1275
+ /**
1276
+ * Further services a body can find the transaction under.
1277
+ *
1278
+ * @see {@link RunTransaction.context}
1279
+ */
1280
+ readonly context?: RunTransaction<Operations, TransactionServices>["context"];
1281
+ }
1282
+
1283
+ /**
1284
+ * Builds the {@link MakeOptions.runTransaction} hook for an SDK that runs a
1285
+ * transaction as a callback and retries it. It is the counterpart of Effect's
1286
+ * `SqlClient.makeWithTransaction`.
1287
+ *
1288
+ * **When to use**
1289
+ *
1290
+ * Use when implementing a backend over an SDK with a
1291
+ * `runTransaction(callback)` API, such as the Admin or client SDK.
1292
+ *
1293
+ * **Details**
1294
+ *
1295
+ * Each attempt runs the body over that attempt's operations. What the
1296
+ * callback throws for a failed body decides whether the SDK retries it:
1297
+ *
1298
+ * - A {@link FirestoreError} with a `cause` throws that cause, so the SDK
1299
+ * retries by its own rules, as for a read answered with `ABORTED`.
1300
+ * - Any other failure throws a marker without a `code`, which the SDK never
1301
+ * retries, even when the body's own error carries a `code`.
1302
+ *
1303
+ * A failure the SDK returns without retrying comes back as the body's own
1304
+ * error, unwrapped. Anything else the SDK rejects with goes through
1305
+ * `onError`. Interrupting the transaction interrupts the body and waits for
1306
+ * the SDK to roll back.
1307
+ *
1308
+ * `Operations` is inferred from `operations`, so `context` receives a
1309
+ * transaction with `getDocs` exactly when `operations` has one.
1310
+ *
1311
+ * @see {@link make} for the service this hook is part of
1312
+ * @see {@link streamFromListener} for the matching helper for listeners
1313
+ *
1314
+ * @example Wrapping an SDK's runTransaction
1315
+ * ```ts
1316
+ * import { Effect } from "effect";
1317
+ * import { Firestore } from "@visiolab/effect-firebase/firestore";
1318
+ *
1319
+ * interface SdkTransaction {
1320
+ * get(path: string): Promise<Firestore.DocumentData | undefined>;
1321
+ * set(path: string, data: Firestore.DocumentData): void;
1322
+ * update(path: string, data: Firestore.DocumentData): void;
1323
+ * }
1324
+ *
1325
+ * declare const db: {
1326
+ * runTransaction<A>(callback: (tx: SdkTransaction) => Promise<A>, options?: object): Promise<A>;
1327
+ * };
1328
+ *
1329
+ * const fail = (cause: unknown) => new Firestore.FirestoreError({ message: "Transaction failed", cause });
1330
+ *
1331
+ * const runTransaction = Firestore.transactionFromCallback({
1332
+ * run: (callback, options) => db.runTransaction(callback, options),
1333
+ * operations: (tx: SdkTransaction) => ({
1334
+ * getDoc: (path: string) =>
1335
+ * Effect.tryPromise({ try: () => tx.get(path), catch: fail }).pipe(
1336
+ * Effect.map((data) => ({ data })),
1337
+ * ),
1338
+ * setDoc: (path: string, data: Firestore.DocumentData) => Effect.sync(() => tx.set(path, data)),
1339
+ * updateDoc: (path: string, data: Firestore.DocumentData) =>
1340
+ * Effect.sync(() => tx.update(path, data)),
1341
+ * }),
1342
+ * onError: fail,
1343
+ * });
1344
+ * ```
1345
+ *
1346
+ * @stability unstable
1347
+ * @category constructors
1348
+ * @since 0.3.0
1349
+ */
1350
+ export const transactionFromCallback: <
1351
+ T,
1352
+ Operations extends TransactionOperations,
1353
+ TransactionServices = never,
1354
+ >(
1355
+ options: TransactionFromCallbackOptions<T, Operations, TransactionServices>,
1356
+ ) => RunTransaction<Operations, TransactionServices> =
1357
+ internalTransactionFromCallback.transactionFromCallback;
1358
+
1359
+ /**
1360
+ * Options for {@link streamFromListener}: how to listen through an SDK
1361
+ * listener.
1362
+ *
1363
+ * `S` is the snapshot the SDK's listener receives, and `A` what the stream
1364
+ * emits for it.
1365
+ *
1366
+ * @stability unstable
1367
+ * @category options
1368
+ * @since 0.3.0
1369
+ */
1370
+ export interface StreamFromListenerOptions<S, A> {
1371
+ /** Attaches a listener, and returns the function that detaches it. */
1372
+ readonly subscribe: (
1373
+ onNext: (snapshot: S) => void,
1374
+ onError: (error: unknown) => void,
1375
+ ) => () => void;
1376
+ /** Builds the error the stream fails with from what the listener received. */
1377
+ readonly onError: (error: unknown) => FirestoreError;
1378
+ /** Maps a snapshot the listener received to what the stream emits. */
1379
+ readonly map: (snapshot: S) => Result.Result<A, FirestoreError>;
1380
+ }
1381
+
1382
+ /**
1383
+ * Builds a listen hook's stream, such as {@link MakeOptions.streamDoc}, from
1384
+ * an SDK listener that returns a function to stop listening.
1385
+ *
1386
+ * **When to use**
1387
+ *
1388
+ * Use when implementing a backend over an SDK with an `onSnapshot`-style
1389
+ * listener.
1390
+ *
1391
+ * **Details**
1392
+ *
1393
+ * Each run of the stream attaches its own listener through `subscribe`, and
1394
+ * detaches it when the run stops. A reader that falls behind only sees the
1395
+ * latest snapshot.
1396
+ *
1397
+ * `map` turns each snapshot into what the stream emits. A snapshot it fails on
1398
+ * fails the stream instead of throwing into the SDK. An error the listener
1399
+ * receives fails the stream with what `onError` builds from it.
1400
+ *
1401
+ * @see {@link make} for the service this hook is part of
1402
+ * @see {@link transactionFromCallback} for the matching helper for transactions
1403
+ *
1404
+ * @example Wrapping an SDK listener
1405
+ * ```ts
1406
+ * import { Result } from "effect";
1407
+ * import { Firestore } from "@visiolab/effect-firebase/firestore";
1408
+ *
1409
+ * declare const sdk: {
1410
+ * onSnapshot(
1411
+ * path: string,
1412
+ * onNext: (data: Firestore.DocumentData | undefined) => void,
1413
+ * onError: (error: unknown) => void,
1414
+ * ): () => void;
1415
+ * };
1416
+ *
1417
+ * const streamDoc = (path: string) =>
1418
+ * Firestore.streamFromListener({
1419
+ * subscribe: (onNext, onError) => sdk.onSnapshot(path, onNext, onError),
1420
+ * onError: (cause) =>
1421
+ * new Firestore.FirestoreError({ message: `Failed to listen to "${path}"`, cause }),
1422
+ * map: (data) => Result.succeed({ data }),
1423
+ * });
1424
+ * ```
1425
+ *
1426
+ * @stability unstable
1427
+ * @category constructors
1428
+ * @since 0.3.0
1429
+ */
1430
+ export const streamFromListener: <S, A>(
1431
+ options: StreamFromListenerOptions<S, A>,
1432
+ ) => Stream.Stream<A, FirestoreError> = internalStreamFromListener.streamFromListener;