@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,302 @@
1
+ /**
2
+ * Firestore timestamps, independent of either SDK.
3
+ *
4
+ * Firestore stores an instant as whole seconds since the Unix epoch plus a
5
+ * non-negative count of nanoseconds, and each SDK reads it back as its own
6
+ * `Timestamp` class. This module owns a portable version of that value, so a
7
+ * document schema works the same under the Admin SDK and the client SDK.
8
+ *
9
+ * Its schema is {@link FirestoreSchema.Timestamp}.
10
+ *
11
+ * @see {@link FirestoreSchema.Timestamp} for storing a `Timestamp` in a document
12
+ * @see {@link FirestoreModel.Timestamp} for a timestamp field the server may fill in
13
+ *
14
+ * @stability unstable
15
+ * @since 0.2.0
16
+ */
17
+ import type { DateTime, Effect, Equal, Inspectable, Option, Order as order } from "effect";
18
+ import * as timestamp from "./internal/timestamp.ts";
19
+
20
+ const TypeId = timestamp.TypeId;
21
+
22
+ /**
23
+ * An instant, stored the way Firestore stores it.
24
+ *
25
+ * **When to use**
26
+ *
27
+ * Use when a document is read, written back, and has to come out unchanged.
28
+ * Firestore keeps timestamps to the microsecond, which `DateTime.Utc` and
29
+ * `Date` cannot hold, so this is the only representation that round-trips
30
+ * exactly.
31
+ *
32
+ * **Details**
33
+ *
34
+ * Two timestamps are equal when their `seconds` and `nanoseconds` are.
35
+ * Firestore addresses instants from the start of year 1 to the end of year
36
+ * 9999, and every constructor here checks for that range.
37
+ *
38
+ * @see {@link FirestoreSchema.Timestamp} for the schema of this value
39
+ *
40
+ * @stability unstable
41
+ * @category models
42
+ * @since 0.2.0
43
+ */
44
+ export interface Timestamp extends Equal.Equal, Inspectable.Inspectable {
45
+ readonly [TypeId]: typeof TypeId;
46
+ /** Seconds since the Unix epoch, negative before it. */
47
+ readonly seconds: number;
48
+ /** The fraction of that second, from 0 to 999,999,999. */
49
+ readonly nanoseconds: number;
50
+ }
51
+
52
+ /**
53
+ * Checks whether `u` is a {@link Timestamp} built by this module.
54
+ *
55
+ * An SDK's own `Timestamp` is not one.
56
+ *
57
+ * @stability unstable
58
+ * @category guards
59
+ * @since 0.2.0
60
+ */
61
+ export const isTimestamp: (u: unknown) => u is Timestamp = timestamp.isTimestamp;
62
+
63
+ /**
64
+ * Builds a timestamp from seconds and nanoseconds, throwing on values
65
+ * Firestore cannot store.
66
+ *
67
+ * **When to use**
68
+ *
69
+ * Use for values known to be valid, such as constants. Use {@link make} for
70
+ * values you have not checked.
71
+ *
72
+ * **Details**
73
+ *
74
+ * Throws unless `seconds` is an integer from year 1 to year 9999, and
75
+ * `nanoseconds` is an integer from 0 to 999,999,999.
76
+ *
77
+ * @example Building a timestamp
78
+ * ```ts
79
+ * import { Timestamp } from "@visiolab/effect-firebase/firestore";
80
+ *
81
+ * const epoch = Timestamp.makeUnsafe(0, 0);
82
+ * ```
83
+ *
84
+ * @see {@link make} for the same check returning an `Option`
85
+ * @see {@link fromMillisUnsafe} for building one from milliseconds
86
+ *
87
+ * @stability unstable
88
+ * @category constructors
89
+ * @since 0.2.0
90
+ */
91
+ export const makeUnsafe: (seconds: number, nanoseconds: number) => Timestamp = timestamp.makeUnsafe;
92
+
93
+ /**
94
+ * Builds a timestamp from seconds and nanoseconds, or `None` for values
95
+ * Firestore cannot store.
96
+ *
97
+ * **When to use**
98
+ *
99
+ * Use when the fields come from outside the program, such as a request body,
100
+ * and may be invalid.
101
+ *
102
+ * **Details**
103
+ *
104
+ * Returns `None` unless `seconds` is an integer from year 1 to year 9999, and
105
+ * `nanoseconds` is an integer from 0 to 999,999,999.
106
+ *
107
+ * @example Building a timestamp from untrusted input
108
+ * ```ts
109
+ * import { Option } from "effect";
110
+ * import { Timestamp } from "@visiolab/effect-firebase/firestore";
111
+ *
112
+ * const valid = Timestamp.make(1_700_000_000, 500); // Option.some(...)
113
+ * const invalid = Timestamp.make(0, -1); // Option.none()
114
+ *
115
+ * Option.isSome(valid); // true
116
+ * Option.isNone(invalid); // true
117
+ * ```
118
+ *
119
+ * @see {@link makeUnsafe} for the same check throwing instead
120
+ * @see {@link fromMillis} for building one from milliseconds
121
+ *
122
+ * @stability unstable
123
+ * @category constructors
124
+ * @since 0.2.0
125
+ */
126
+ export const make: (seconds: number, nanoseconds: number) => Option.Option<Timestamp> =
127
+ timestamp.make;
128
+
129
+ /**
130
+ * Builds a timestamp from milliseconds since the Unix epoch, throwing outside
131
+ * the range Firestore stores.
132
+ *
133
+ * **When to use**
134
+ *
135
+ * Use with millisecond values known to be in range, such as `Date.now()`.
136
+ *
137
+ * @example Building a timestamp from milliseconds
138
+ * ```ts
139
+ * import { Timestamp } from "@visiolab/effect-firebase/firestore";
140
+ *
141
+ * const timestamp = Timestamp.fromMillisUnsafe(Date.now());
142
+ * ```
143
+ *
144
+ * @see {@link fromMillis} for the same check returning an `Option`
145
+ * @see {@link makeUnsafe} for building one from seconds and nanoseconds
146
+ *
147
+ * @stability unstable
148
+ * @category constructors
149
+ * @since 0.2.0
150
+ */
151
+ export const fromMillisUnsafe: (millis: number) => Timestamp = timestamp.fromMillisUnsafe;
152
+
153
+ /**
154
+ * Builds a timestamp from milliseconds since the Unix epoch, or `None` outside
155
+ * the range Firestore stores.
156
+ *
157
+ * **When to use**
158
+ *
159
+ * Use with millisecond values that may be out of range or not finite.
160
+ *
161
+ * @see {@link fromMillisUnsafe} for the same check throwing instead
162
+ * @see {@link toMillis} for the reverse conversion
163
+ *
164
+ * @stability unstable
165
+ * @category constructors
166
+ * @since 0.2.0
167
+ */
168
+ export const fromMillis: (millis: number) => Option.Option<Timestamp> = timestamp.fromMillis;
169
+
170
+ /**
171
+ * Builds a timestamp from a `DateTime.Utc`, throwing outside the range
172
+ * Firestore stores.
173
+ *
174
+ * **When to use**
175
+ *
176
+ * Use with a `DateTime` known to fall between year 1 and year 9999, which
177
+ * covers almost every real date.
178
+ *
179
+ * **Details**
180
+ *
181
+ * A `DateTime.Utc` only carries milliseconds, so the result has no finer
182
+ * digits.
183
+ *
184
+ * @example Converting a DateTime
185
+ * ```ts
186
+ * import { DateTime } from "effect";
187
+ * import { Timestamp } from "@visiolab/effect-firebase/firestore";
188
+ *
189
+ * const timestamp = Timestamp.fromDateTimeUnsafe(DateTime.makeUnsafe("2024-01-01T00:00:00Z"));
190
+ * ```
191
+ *
192
+ * @see {@link fromDateTime} for the same check returning an `Option`
193
+ * @see {@link toDateTime} for the reverse conversion
194
+ *
195
+ * @stability unstable
196
+ * @category constructors
197
+ * @since 0.2.0
198
+ */
199
+ export const fromDateTimeUnsafe: (dateTime: DateTime.Utc) => Timestamp =
200
+ timestamp.fromDateTimeUnsafe;
201
+
202
+ /**
203
+ * Builds a timestamp from a `DateTime.Utc`, or `None` outside the range
204
+ * Firestore stores.
205
+ *
206
+ * **When to use**
207
+ *
208
+ * Use with a `DateTime` that may fall outside year 1 to year 9999, such as one
209
+ * parsed from user input.
210
+ *
211
+ * **Details**
212
+ *
213
+ * A `DateTime.Utc` only carries milliseconds, so the result has no finer
214
+ * digits.
215
+ *
216
+ * @see {@link fromDateTimeUnsafe} for the same check throwing instead
217
+ * @see {@link toDateTime} for the reverse conversion
218
+ *
219
+ * @stability unstable
220
+ * @category constructors
221
+ * @since 0.2.0
222
+ */
223
+ export const fromDateTime: (dateTime: DateTime.Utc) => Option.Option<Timestamp> =
224
+ timestamp.fromDateTime;
225
+
226
+ /**
227
+ * The current instant, read from Effect's `Clock`, to the millisecond.
228
+ *
229
+ * **When to use**
230
+ *
231
+ * Use when the time this program sees is the one to store, and when a test
232
+ * should control it through `TestClock`.
233
+ *
234
+ * **Details**
235
+ *
236
+ * This is the local clock, not the database's. Clients with skewed clocks
237
+ * write different times for the same moment.
238
+ *
239
+ * @example Reading the current time
240
+ * ```ts
241
+ * import { Effect } from "effect";
242
+ * import { Timestamp } from "@visiolab/effect-firebase/firestore";
243
+ *
244
+ * const program = Effect.gen(function* () {
245
+ * const now = yield* Timestamp.now;
246
+ * return Timestamp.toMillis(now);
247
+ * });
248
+ * ```
249
+ *
250
+ * @see {@link FieldValue.serverTimestamp} for the time the server applies the write
251
+ *
252
+ * @stability unstable
253
+ * @category constructors
254
+ * @since 0.2.0
255
+ */
256
+ export const now: Effect.Effect<Timestamp> = timestamp.now;
257
+
258
+ /**
259
+ * Converts a timestamp to milliseconds since the Unix epoch, dropping the
260
+ * finer digits.
261
+ *
262
+ * @see {@link fromMillis} for the reverse conversion
263
+ * @see {@link toDateTime} for converting to a `DateTime.Utc`
264
+ *
265
+ * @stability unstable
266
+ * @category conversions
267
+ * @since 0.2.0
268
+ */
269
+ export const toMillis: (self: Timestamp) => number = timestamp.toMillis;
270
+
271
+ /**
272
+ * Converts a timestamp to a `DateTime.Utc`, dropping the digits finer than a
273
+ * millisecond.
274
+ *
275
+ * @see {@link fromDateTime} for the reverse conversion
276
+ * @see {@link toMillis} for converting to milliseconds
277
+ *
278
+ * @stability unstable
279
+ * @category conversions
280
+ * @since 0.2.0
281
+ */
282
+ export const toDateTime: (self: Timestamp) => DateTime.Utc = timestamp.toDateTime;
283
+
284
+ /**
285
+ * Orders timestamps from earliest to latest, at full precision.
286
+ *
287
+ * @example Sorting timestamps
288
+ * ```ts
289
+ * import { Array } from "effect";
290
+ * import { Timestamp } from "@visiolab/effect-firebase/firestore";
291
+ *
292
+ * const sorted = Array.sort(
293
+ * [Timestamp.makeUnsafe(2, 0), Timestamp.makeUnsafe(1, 500)],
294
+ * Timestamp.Order,
295
+ * );
296
+ * ```
297
+ *
298
+ * @stability unstable
299
+ * @category ordering
300
+ * @since 0.2.0
301
+ */
302
+ export const Order: order.Order<Timestamp> = timestamp.Order;
@@ -0,0 +1,67 @@
1
+ /**
2
+ * Firestore.
3
+ *
4
+ * @stability unstable
5
+ * @since 0.2.0
6
+ */
7
+ export * as Collection from "./Collection.ts";
8
+ /**
9
+ * @stability unstable
10
+ * @since 0.2.0
11
+ */
12
+ export * as CollectionGroup from "./CollectionGroup.ts";
13
+ /**
14
+ * @stability unstable
15
+ * @since 0.2.0
16
+ */
17
+ export * as DocumentPath from "./DocumentPath.ts";
18
+ /**
19
+ * @stability unstable
20
+ * @since 0.2.0
21
+ */
22
+ export * as DocumentSnapshot from "./DocumentSnapshot.ts";
23
+ /**
24
+ * @stability unstable
25
+ * @since 0.2.0
26
+ */
27
+ export * as FieldValue from "./FieldValue.ts";
28
+ /**
29
+ * @stability unstable
30
+ * @since 0.2.0
31
+ */
32
+ export * as Firestore from "./Firestore.ts";
33
+ /**
34
+ * @stability unstable
35
+ * @since 0.2.0
36
+ */
37
+ export * as FirestoreModel from "./FirestoreModel.ts";
38
+ /**
39
+ * @stability unstable
40
+ * @since 0.2.0
41
+ */
42
+ export * as FirestoreSchema from "./FirestoreSchema.ts";
43
+ /**
44
+ * @stability unstable
45
+ * @since 0.2.0
46
+ */
47
+ export * as FirestoreValue from "./FirestoreValue.ts";
48
+ /**
49
+ * @stability unstable
50
+ * @since 0.2.0
51
+ */
52
+ export * as GeoPoint from "./GeoPoint.ts";
53
+ /**
54
+ * @stability unstable
55
+ * @since 0.2.0
56
+ */
57
+ export * as Patch from "./Patch.ts";
58
+ /**
59
+ * @stability unstable
60
+ * @since 0.2.0
61
+ */
62
+ export * as QuerySnapshot from "./QuerySnapshot.ts";
63
+ /**
64
+ * @stability unstable
65
+ * @since 0.2.0
66
+ */
67
+ export * as Timestamp from "./Timestamp.ts";
@@ -0,0 +1,246 @@
1
+ import { Equal, Hash, Inspectable, Option, Predicate, Schema } from "effect";
2
+ import type * as Collection from "../Collection.ts";
3
+ import type * as FirestoreModel from "../FirestoreModel.ts";
4
+ import * as documentSnapshot from "./documentSnapshot.ts";
5
+
6
+ /** @internal */
7
+ export const CollectionTypeId = "~@visiolab/effect-firebase/firestore/Collection";
8
+
9
+ /** @internal */
10
+ export const CollectionReferenceTypeId = "~@visiolab/effect-firebase/firestore/CollectionReference";
11
+
12
+ /** @internal */
13
+ export const DocumentReferenceTypeId = "~@visiolab/effect-firebase/firestore/DocumentReference";
14
+
15
+ /** @internal */
16
+ export const isCollection = (u: unknown): u is Collection.Any =>
17
+ Predicate.hasProperty(u, CollectionTypeId);
18
+
19
+ /** @internal */
20
+ export const isCollectionReference = (
21
+ u: unknown,
22
+ ): u is Collection.CollectionReference<Collection.Any> =>
23
+ Predicate.hasProperty(u, CollectionReferenceTypeId);
24
+
25
+ /** @internal */
26
+ export const isDocumentReference = (
27
+ u: unknown,
28
+ ): u is Collection.DocumentReference<Collection.Any> =>
29
+ Predicate.hasProperty(u, DocumentReferenceTypeId);
30
+
31
+ const encoder = new TextEncoder();
32
+
33
+ /** Rejects the segments Firestore cannot address, before they reach a path. */
34
+ const validateSegment = (segment: string): void => {
35
+ const reject = (reason: string): never => {
36
+ throw new Error(`Invalid Firestore collection id "${segment}": ${reason}`);
37
+ };
38
+ if (segment.length === 0) reject("it must not be empty");
39
+ if (segment.includes("/")) reject('it must not contain "/"');
40
+ if (segment === "." || segment === "..") reject('it must not be "." or ".."');
41
+ if (/^__.*__$/.test(segment)) reject('it must not match "__.*__"');
42
+ if (encoder.encode(segment).length > 1500) reject("it must be at most 1500 bytes");
43
+ };
44
+
45
+ /**
46
+ * Brands an id schema with the path addressing it.
47
+ *
48
+ * The path is only known at runtime, and `Schema.brand` rejects an open `string`
49
+ * key. Narrowing the key to a placeholder literal keeps the real path on the
50
+ * annotation: the static brand is erased where `makeCollection` takes the schema
51
+ * as `Schema.Top`, and the public id types are built above it.
52
+ */
53
+ const brandId = (path: ReadonlyArray<string>) =>
54
+ Schema.String.pipe(Schema.brand(path.join("/") as "~firestore/collection-path"));
55
+
56
+ /** Interleaves the segments with the ids addressing them: `a/1/b/2/c`. */
57
+ const joinPath = (segments: ReadonlyArray<string>, ancestors: ReadonlyArray<string>): string => {
58
+ let path = segments[0];
59
+ for (let i = 0; i < ancestors.length; i++) path += `/${ancestors[i]}/${segments[i + 1]}`;
60
+ return path;
61
+ };
62
+
63
+ /**
64
+ * The untyped shapes behind the exported ones.
65
+ *
66
+ * Everything below works on plain segments and ids. The public types are what
67
+ * make a path only buildable out of the ids that address it, so the runtime has
68
+ * no work left to do beyond joining strings.
69
+ */
70
+ interface CollectionImpl {
71
+ readonly path: ReadonlyArray<string>;
72
+ readonly segment: string;
73
+ readonly schema: FirestoreModel.Any;
74
+ readonly id: Schema.Top;
75
+ contains(value: unknown): boolean;
76
+ subcollection(segment: string, schema: FirestoreModel.Any): CollectionImpl;
77
+ collection(...ancestors: ReadonlyArray<string>): CollectionReferenceImpl;
78
+ doc(...ids: ReadonlyArray<string>): DocumentReferenceImpl;
79
+ }
80
+
81
+ interface CollectionReferenceImpl {
82
+ readonly collection: CollectionImpl;
83
+ readonly ancestors: ReadonlyArray<string>;
84
+ readonly id: string;
85
+ readonly path: string;
86
+ doc(id: string): DocumentReferenceImpl;
87
+ }
88
+
89
+ interface DocumentReferenceImpl {
90
+ readonly collection: CollectionImpl;
91
+ readonly ancestors: ReadonlyArray<string>;
92
+ readonly id: string;
93
+ readonly path: string;
94
+ readonly parent: CollectionReferenceImpl;
95
+ }
96
+
97
+ const CollectionProto = {
98
+ ...Inspectable.BaseProto,
99
+ [CollectionTypeId]: CollectionTypeId,
100
+ toJSON(this: CollectionImpl) {
101
+ return { _id: "Collection", path: this.path.join("/") };
102
+ },
103
+ subcollection(this: CollectionImpl, segment: string, schema: FirestoreModel.Any): CollectionImpl {
104
+ validateSegment(segment);
105
+ const path = [...this.path, segment];
106
+ return makeCollection(path, schema, brandId(path));
107
+ },
108
+ collection(this: CollectionImpl, ...ancestors: ReadonlyArray<string>): CollectionReferenceImpl {
109
+ return makeCollectionReference(this, ancestors);
110
+ },
111
+ doc(this: CollectionImpl, ...ids: ReadonlyArray<string>): DocumentReferenceImpl {
112
+ const parent = makeCollectionReference(this, ids.slice(0, -1));
113
+ return makeDocumentReference(parent, ids[ids.length - 1]);
114
+ },
115
+ };
116
+
117
+ /**
118
+ * The target is asserted as the prototype rather than as the finished shape, so
119
+ * that the result type is inferred from the fields below. The return annotation
120
+ * then catches a field that is missing, misspelled or of the wrong type -
121
+ * asserting the finished shape up front would check none of the three.
122
+ */
123
+ const makeCollection = (
124
+ path: ReadonlyArray<string>,
125
+ schema: FirestoreModel.Any,
126
+ id: Schema.Top,
127
+ ): CollectionImpl => {
128
+ const self: CollectionImpl = Object.assign(
129
+ Object.create(CollectionProto) as typeof CollectionProto,
130
+ {
131
+ path,
132
+ segment: path[path.length - 1],
133
+ schema,
134
+ id,
135
+ // An own closure rather than a method on the prototype, so that it can be
136
+ // passed on unbound, as in `docs.filter(Posts.contains)`. The definition
137
+ // is compared by reference, as references compare theirs in `Equal`.
138
+ contains: (value: unknown): boolean =>
139
+ isDocumentReference(value)
140
+ ? value.collection === self
141
+ : documentSnapshot.isDocumentSnapshot(value) && value.ref.collection === self,
142
+ },
143
+ );
144
+ return self;
145
+ };
146
+
147
+ const CollectionReferenceProto = {
148
+ ...Inspectable.BaseProto,
149
+ [CollectionReferenceTypeId]: CollectionReferenceTypeId,
150
+ toJSON(this: CollectionReferenceImpl) {
151
+ return { _id: "CollectionReference", path: this.path };
152
+ },
153
+ // The definition is compared by reference: it is written once and named, and
154
+ // `Equal.equals` would otherwise walk the whole schema behind it.
155
+ [Equal.symbol](this: CollectionReferenceImpl, that: unknown): boolean {
156
+ return (
157
+ isCollectionReference(that) && this.path === that.path && this.collection === that.collection
158
+ );
159
+ },
160
+ [Hash.symbol](this: CollectionReferenceImpl): number {
161
+ return Hash.combine(Hash.string(this.path), Hash.random(this.collection));
162
+ },
163
+ doc(this: CollectionReferenceImpl, id: string): DocumentReferenceImpl {
164
+ return makeDocumentReference(this, id);
165
+ },
166
+ };
167
+
168
+ const makeCollectionReference = (
169
+ collection: CollectionImpl,
170
+ ancestors: ReadonlyArray<string>,
171
+ ): CollectionReferenceImpl =>
172
+ Object.assign(Object.create(CollectionReferenceProto) as typeof CollectionReferenceProto, {
173
+ collection,
174
+ ancestors,
175
+ id: collection.segment,
176
+ path: joinPath(collection.path, ancestors),
177
+ });
178
+
179
+ const DocumentReferenceProto = {
180
+ ...Inspectable.BaseProto,
181
+ [DocumentReferenceTypeId]: DocumentReferenceTypeId,
182
+ toJSON(this: DocumentReferenceImpl) {
183
+ return { _id: "DocumentReference", path: this.path };
184
+ },
185
+ // The definition is compared by reference: it is written once and named, and
186
+ // `Equal.equals` would otherwise walk the whole schema behind it.
187
+ [Equal.symbol](this: DocumentReferenceImpl, that: unknown): boolean {
188
+ return (
189
+ isDocumentReference(that) && this.path === that.path && this.collection === that.collection
190
+ );
191
+ },
192
+ [Hash.symbol](this: DocumentReferenceImpl): number {
193
+ return Hash.combine(Hash.string(this.path), Hash.random(this.collection));
194
+ },
195
+ };
196
+
197
+ /** A document is always built from the collection holding it, which it keeps. */
198
+ const makeDocumentReference = (
199
+ parent: CollectionReferenceImpl,
200
+ id: string,
201
+ ): DocumentReferenceImpl =>
202
+ Object.assign(Object.create(DocumentReferenceProto) as typeof DocumentReferenceProto, {
203
+ collection: parent.collection,
204
+ ancestors: parent.ancestors,
205
+ id,
206
+ path: `${parent.path}/${id}`,
207
+ parent,
208
+ });
209
+
210
+ /** @internal */
211
+ export const make = <const Segment extends string, S extends FirestoreModel.Any>(
212
+ segment: Segment,
213
+ schema: S,
214
+ ): Collection.Collection<readonly [readonly [Segment, S]]> => {
215
+ validateSegment(segment);
216
+ const path = [segment];
217
+ return makeCollection(path, schema, brandId(path)) as unknown as Collection.Collection<
218
+ readonly [readonly [Segment, S]]
219
+ >;
220
+ };
221
+
222
+ /** @internal */
223
+ export const fromPath = <C extends Collection.Any>(
224
+ collection: C,
225
+ path: string,
226
+ ): Option.Option<Collection.DocumentReference<C>> => {
227
+ const self = collection as unknown as CollectionImpl;
228
+ const parts = path.split("/");
229
+ if (parts.length !== self.path.length * 2) return Option.none();
230
+ const ids: Array<string> = [];
231
+ for (let i = 0; i < self.path.length; i++) {
232
+ const id = parts[2 * i + 1];
233
+ if (parts[2 * i] !== self.path[i] || id.length === 0) return Option.none();
234
+ ids.push(id);
235
+ }
236
+ return Option.some(self.doc(...ids) as unknown as Collection.DocumentReference<C>);
237
+ };
238
+
239
+ /**
240
+ * The path a document in `collection` is found at, with `{id}` standing in for
241
+ * each id: `users/{id}/posts/{id}`.
242
+ *
243
+ * @internal
244
+ */
245
+ export const pattern = (collection: Collection.Any): string =>
246
+ (collection as unknown as CollectionImpl).path.map((segment) => `${segment}/{id}`).join("/");
@@ -0,0 +1,81 @@
1
+ import { Inspectable, Option, Predicate, Result } from "effect";
2
+ import type * as Collection from "../Collection.ts";
3
+ import type * as CollectionGroup from "../CollectionGroup.ts";
4
+ import { UndefinedCollectionError } from "./firestoreError.ts";
5
+ import * as collection from "./collection.ts";
6
+
7
+ /** @internal */
8
+ export const TypeId = "~@visiolab/effect-firebase/firestore/CollectionGroup";
9
+
10
+ /** @internal */
11
+ export const isCollectionGroup = (u: unknown): u is CollectionGroup.Any =>
12
+ Predicate.hasProperty(u, TypeId);
13
+
14
+ /**
15
+ * Where `member`'s collections sit, with `{id}` standing in for each ancestor
16
+ * id: `users/{id}/posts`.
17
+ */
18
+ const memberPattern = (member: Collection.Any): string =>
19
+ collection.pattern(member).slice(0, -"/{id}".length);
20
+
21
+ const CollectionGroupProto = {
22
+ ...Inspectable.BaseProto,
23
+ [TypeId]: TypeId,
24
+ toJSON(this: CollectionGroup.Any) {
25
+ return { _id: "CollectionGroup", members: this.members.map(memberPattern) };
26
+ },
27
+ };
28
+
29
+ /**
30
+ * Rejects the groups that would not mean one thing: one with nothing in it,
31
+ * one whose collections Firestore could not read under one collection id, and
32
+ * one with two collections at the same path, which no document could be told
33
+ * apart between.
34
+ */
35
+ const validate = (members: ReadonlyArray<Collection.Any>): void => {
36
+ const reject = (reason: string): never => {
37
+ throw new Error(`Invalid collection group: ${reason}`);
38
+ };
39
+ if (members.length === 0) reject("it must hold at least one collection");
40
+ const [first] = members;
41
+ const seen = new Set<string>();
42
+ for (const member of members) {
43
+ if (member.segment !== first.segment) {
44
+ reject(
45
+ `"${memberPattern(first)}" and "${memberPattern(member)}" do not share their collection id`,
46
+ );
47
+ }
48
+ const pattern = memberPattern(member);
49
+ if (seen.has(pattern)) reject(`it has more than one member at "${pattern}"`);
50
+ seen.add(pattern);
51
+ }
52
+ };
53
+
54
+ /** @internal */
55
+ export const make = <const Members extends readonly [Collection.Any, ...Array<Collection.Any>]>(
56
+ ...valid: CollectionGroup.Valid<Members>
57
+ ): CollectionGroup.CollectionGroup<Members[number]> => {
58
+ // The type checker has refused anything that is not one of `Members`, but
59
+ // a caller that got past it is checked again here.
60
+ const members = valid as ReadonlyArray<unknown> as Members;
61
+ validate(members);
62
+ return Object.assign(Object.create(CollectionGroupProto) as typeof CollectionGroupProto, {
63
+ members,
64
+ id: members[0].segment as string,
65
+ }) as CollectionGroup.CollectionGroup<Members[number]>;
66
+ };
67
+
68
+ /** @internal */
69
+ export const documentAt = <G extends CollectionGroup.Any>(
70
+ group: G,
71
+ path: string,
72
+ ): Result.Result<CollectionGroup.DocumentReference<G>, UndefinedCollectionError> => {
73
+ // The members are read as `Collection.Any`, so the reference found in one is
74
+ // typed back by the member it was found in.
75
+ for (const member of group.members) {
76
+ const ref = collection.fromPath(member, path);
77
+ if (Option.isSome(ref))
78
+ return Result.succeed(ref.value as CollectionGroup.DocumentReference<G>);
79
+ }
80
+ return Result.fail(new UndefinedCollectionError({ path, collectionId: group.id }));
81
+ };