@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,53 @@
1
+ import { Equal, Hash, Inspectable, Option, Predicate } from "effect";
2
+ import type * as DocumentPath from "../DocumentPath.ts";
3
+
4
+ /** @internal */
5
+ export const TypeId = "~@visiolab/effect-firebase/firestore/DocumentPath";
6
+
7
+ /** A collection id, then a document id, and so on: an even number of non-empty segments. */
8
+ const addressesDocument = (path: string): boolean => {
9
+ const segments = path.split("/");
10
+ return segments.length % 2 === 0 && segments.every((segment) => segment.length > 0);
11
+ };
12
+
13
+ /** @internal */
14
+ export const isDocumentPath = (u: unknown): u is DocumentPath.DocumentPath =>
15
+ Predicate.hasProperty(u, TypeId) &&
16
+ u[TypeId] === TypeId &&
17
+ Predicate.hasProperty(u, "path") &&
18
+ Predicate.isString(u.path);
19
+
20
+ const Proto: Omit<DocumentPath.DocumentPath, "path"> = {
21
+ ...Inspectable.BaseProto,
22
+ [TypeId]: TypeId,
23
+ toJSON(this: DocumentPath.DocumentPath) {
24
+ return { _id: "DocumentPath", path: this.path };
25
+ },
26
+ [Equal.symbol](this: DocumentPath.DocumentPath, that: unknown): boolean {
27
+ return isDocumentPath(that) && this.path === that.path;
28
+ },
29
+ [Hash.symbol](this: DocumentPath.DocumentPath): number {
30
+ return Hash.string(this.path);
31
+ },
32
+ };
33
+
34
+ /**
35
+ * Builds the value without checking `path`, for one that already addresses a
36
+ * document, such as a reference's own.
37
+ *
38
+ * @internal
39
+ */
40
+ export const makeUnchecked = (path: string): DocumentPath.DocumentPath =>
41
+ Object.assign(Object.create(Proto) as typeof Proto, { path });
42
+
43
+ /** @internal */
44
+ export const make = (path: string): Option.Option<DocumentPath.DocumentPath> =>
45
+ addressesDocument(path) ? Option.some(makeUnchecked(path)) : Option.none();
46
+
47
+ /** @internal */
48
+ export const makeUnsafe = (path: string): DocumentPath.DocumentPath => {
49
+ if (!addressesDocument(path)) {
50
+ throw new Error(`Invalid Firestore document path "${path}": it must address a document`);
51
+ }
52
+ return makeUnchecked(path);
53
+ };
@@ -0,0 +1,117 @@
1
+ import { Effect, Inspectable, Predicate, Schema } from "effect";
2
+ import type * as Collection from "../Collection.ts";
3
+ import type * as DocumentSnapshot from "../DocumentSnapshot.ts";
4
+ import * as firestoreValue from "./firestoreValue.ts";
5
+ import { attach, extraToJson, type WithExtra } from "./snapshotExtra.ts";
6
+
7
+ /** @internal */
8
+ export const TypeId = "~@visiolab/effect-firebase/firestore/DocumentSnapshot";
9
+
10
+ /** @internal */
11
+ export const isDocumentSnapshot = (
12
+ u: unknown,
13
+ ): u is DocumentSnapshot.DocumentSnapshot<Collection.Any> => Predicate.hasProperty(u, TypeId);
14
+
15
+ /** @internal */
16
+ export const isExisting = <C extends Collection.Any>(
17
+ self: DocumentSnapshot.DocumentSnapshot<C>,
18
+ ): self is DocumentSnapshot.Existing<C> => self._tag === "Existing";
19
+
20
+ /** @internal */
21
+ export const isMissing = <C extends Collection.Any>(
22
+ self: DocumentSnapshot.DocumentSnapshot<C>,
23
+ ): self is DocumentSnapshot.Missing<C> => self._tag === "Missing";
24
+
25
+ /**
26
+ * The untyped shapes behind the exported ones.
27
+ *
28
+ * The schema is held as one that decodes with no services, which is what is
29
+ * left of the collection's own once its type is erased: `Existing<C>` is what
30
+ * puts the real ones back.
31
+ */
32
+ interface ExistingImpl extends WithExtra {
33
+ readonly ref: {
34
+ readonly path: string;
35
+ readonly collection: { readonly schema: Schema.Decoder<unknown> };
36
+ };
37
+ readonly id: string;
38
+ readonly raw: unknown;
39
+ }
40
+
41
+ interface MissingImpl extends WithExtra {
42
+ readonly ref: Collection.DocumentReference<Collection.Any>;
43
+ readonly id: string;
44
+ }
45
+
46
+ const ExistingProto = {
47
+ ...Inspectable.BaseProto,
48
+ [TypeId]: TypeId,
49
+ _tag: "Existing" as const,
50
+ toJSON(this: ExistingImpl) {
51
+ return {
52
+ _id: "DocumentSnapshot",
53
+ _tag: "Existing",
54
+ ref: this.ref.path,
55
+ raw: Inspectable.toJson(this.raw),
56
+ ...extraToJson(this),
57
+ };
58
+ },
59
+ // A getter rather than a field, so that nothing is built until it is asked
60
+ // for. It suspends as well, because a schema that decodes synchronously
61
+ // decodes when it is called rather than when it is run, which would decode
62
+ // on reading `data` and replay that one result on every run.
63
+ // `raw` holds Firestore Values, which the SDK mapped its own values to, so it
64
+ // is decoded through the codec derived from the model rather than the model
65
+ // itself. Derivation is memoized, so this costs nothing after the first read.
66
+ get data(): Effect.Effect<unknown, Schema.SchemaError> {
67
+ const self = this as unknown as ExistingImpl;
68
+ return Effect.suspend(() =>
69
+ Schema.decodeUnknownEffect(firestoreValue.toCodecFirestore(self.ref.collection.schema))(
70
+ self.raw,
71
+ ),
72
+ );
73
+ },
74
+ };
75
+
76
+ const MissingProto = {
77
+ ...Inspectable.BaseProto,
78
+ [TypeId]: TypeId,
79
+ _tag: "Missing" as const,
80
+ toJSON(this: MissingImpl) {
81
+ return { _id: "DocumentSnapshot", _tag: "Missing", ref: this.ref.path, ...extraToJson(this) };
82
+ },
83
+ };
84
+
85
+ /** @internal */
86
+ export const existing = <C extends Collection.Any>(
87
+ ref: Collection.DocumentReference<C>,
88
+ data: unknown,
89
+ extra?: object,
90
+ ): DocumentSnapshot.Existing<C> => {
91
+ const self = Object.assign(Object.create(ExistingProto) as typeof ExistingProto, {
92
+ ref,
93
+ id: ref.id,
94
+ raw: data,
95
+ });
96
+ return attach(self, extra) as unknown as DocumentSnapshot.Existing<C>;
97
+ };
98
+
99
+ /** @internal */
100
+ export const missing = <C extends Collection.Any>(
101
+ ref: Collection.DocumentReference<C>,
102
+ extra?: object,
103
+ ): DocumentSnapshot.Missing<C> => {
104
+ const self: MissingImpl = Object.assign(Object.create(MissingProto) as typeof MissingProto, {
105
+ ref,
106
+ id: ref.id,
107
+ });
108
+ return attach(self, extra) as unknown as DocumentSnapshot.Missing<C>;
109
+ };
110
+
111
+ /** @internal */
112
+ export const make = <C extends Collection.Any>(
113
+ ref: Collection.DocumentReference<C>,
114
+ data: unknown,
115
+ extra?: object,
116
+ ): DocumentSnapshot.DocumentSnapshot<C> =>
117
+ data === undefined ? missing(ref, extra) : existing(ref, data, extra);
@@ -0,0 +1,119 @@
1
+ import { Equal, Hash, Inspectable, Predicate } from "effect";
2
+ import type * as FieldValue from "../FieldValue.ts";
3
+
4
+ /** @internal */
5
+ export const ServerTimestampTypeId =
6
+ "~@visiolab/effect-firebase/firestore/FieldValue/ServerTimestamp";
7
+
8
+ const ServerTimestampProto: FieldValue.ServerTimestamp = {
9
+ ...Inspectable.BaseProto,
10
+ [ServerTimestampTypeId]: ServerTimestampTypeId,
11
+ toJSON() {
12
+ return { _id: "ServerTimestamp" };
13
+ },
14
+ };
15
+
16
+ /** @internal */
17
+ export const serverTimestamp: FieldValue.ServerTimestamp = Object.create(
18
+ ServerTimestampProto,
19
+ ) as FieldValue.ServerTimestamp;
20
+
21
+ /** @internal */
22
+ export const isServerTimestamp = (u: unknown): u is FieldValue.ServerTimestamp =>
23
+ Predicate.hasProperty(u, ServerTimestampTypeId);
24
+
25
+ /** @internal */
26
+ export const IncrementTypeId = "~@visiolab/effect-firebase/firestore/FieldValue/Increment";
27
+
28
+ /** @internal */
29
+ export const isIncrement = (u: unknown): u is FieldValue.Increment =>
30
+ Predicate.hasProperty(u, IncrementTypeId);
31
+
32
+ const IncrementProto: Omit<FieldValue.Increment, "by"> = {
33
+ ...Inspectable.BaseProto,
34
+ [IncrementTypeId]: IncrementTypeId,
35
+ toJSON(this: FieldValue.Increment) {
36
+ return { _id: "Increment", by: this.by };
37
+ },
38
+ [Equal.symbol](this: FieldValue.Increment, that: unknown): boolean {
39
+ return isIncrement(that) && this.by === that.by;
40
+ },
41
+ [Hash.symbol](this: FieldValue.Increment): number {
42
+ return Hash.combine(Hash.string(IncrementTypeId), Hash.number(this.by));
43
+ },
44
+ };
45
+
46
+ /** @internal */
47
+ export const increment = (by: number): FieldValue.Increment => {
48
+ if (!Number.isFinite(by)) {
49
+ throw new Error(`Invalid Firestore increment: by must be a finite number, got ${by}`);
50
+ }
51
+ return Object.assign(Object.create(IncrementProto) as typeof IncrementProto, { by });
52
+ };
53
+
54
+ /** @internal */
55
+ export const decrement = (by: number): FieldValue.Increment => increment(-by);
56
+
57
+ /** @internal */
58
+ export const DeleteFieldTypeId = "~@visiolab/effect-firebase/firestore/FieldValue/DeleteField";
59
+
60
+ const DeleteFieldProto: FieldValue.DeleteField = {
61
+ ...Inspectable.BaseProto,
62
+ [DeleteFieldTypeId]: DeleteFieldTypeId,
63
+ toJSON() {
64
+ return { _id: "DeleteField" };
65
+ },
66
+ };
67
+
68
+ /** @internal */
69
+ export const deleteField: FieldValue.DeleteField = Object.create(
70
+ DeleteFieldProto,
71
+ ) as FieldValue.DeleteField;
72
+
73
+ /** @internal */
74
+ export const isDeleteField = (u: unknown): u is FieldValue.DeleteField =>
75
+ Predicate.hasProperty(u, DeleteFieldTypeId);
76
+
77
+ /** @internal */
78
+ export const ArrayUnionTypeId = "~@visiolab/effect-firebase/firestore/FieldValue/ArrayUnion";
79
+
80
+ const ArrayUnionProto: Omit<FieldValue.ArrayUnion<unknown>, "elements"> = {
81
+ ...Inspectable.BaseProto,
82
+ [ArrayUnionTypeId]: ArrayUnionTypeId,
83
+ toJSON(this: FieldValue.ArrayUnion<unknown>) {
84
+ return {
85
+ _id: "ArrayUnion",
86
+ elements: this.elements.map((element) => Inspectable.toJson(element)),
87
+ };
88
+ },
89
+ };
90
+
91
+ /** @internal */
92
+ export const isArrayUnion = (u: unknown): u is FieldValue.ArrayUnion<unknown> =>
93
+ Predicate.hasProperty(u, ArrayUnionTypeId);
94
+
95
+ /** @internal */
96
+ export const arrayUnion = <A>(...elements: ReadonlyArray<A>): FieldValue.ArrayUnion<A> =>
97
+ Object.assign(Object.create(ArrayUnionProto) as typeof ArrayUnionProto, { elements });
98
+
99
+ /** @internal */
100
+ export const ArrayRemoveTypeId = "~@visiolab/effect-firebase/firestore/FieldValue/ArrayRemove";
101
+
102
+ const ArrayRemoveProto: Omit<FieldValue.ArrayRemove<unknown>, "elements"> = {
103
+ ...Inspectable.BaseProto,
104
+ [ArrayRemoveTypeId]: ArrayRemoveTypeId,
105
+ toJSON(this: FieldValue.ArrayRemove<unknown>) {
106
+ return {
107
+ _id: "ArrayRemove",
108
+ elements: this.elements.map((element) => Inspectable.toJson(element)),
109
+ };
110
+ },
111
+ };
112
+
113
+ /** @internal */
114
+ export const isArrayRemove = (u: unknown): u is FieldValue.ArrayRemove<unknown> =>
115
+ Predicate.hasProperty(u, ArrayRemoveTypeId);
116
+
117
+ /** @internal */
118
+ export const arrayRemove = <A>(...elements: ReadonlyArray<A>): FieldValue.ArrayRemove<A> =>
119
+ Object.assign(Object.create(ArrayRemoveProto) as typeof ArrayRemoveProto, { elements });
@@ -0,0 +1,322 @@
1
+ import { Context, Effect, Result, Schema, Stream } from "effect";
2
+ import type * as Collection from "../Collection.ts";
3
+ import type * as CollectionGroup from "../CollectionGroup.ts";
4
+ import type * as DocumentSnapshot from "../DocumentSnapshot.ts";
5
+ import type * as Firestore from "../Firestore.ts";
6
+ import type * as FirestoreModel from "../FirestoreModel.ts";
7
+ import type * as QuerySnapshot from "../QuerySnapshot.ts";
8
+ import * as collectionGroup from "./collectionGroup.ts";
9
+ import * as documentSnapshot from "./documentSnapshot.ts";
10
+ import * as firestoreValue from "./firestoreValue.ts";
11
+ import * as firestoreValueWalk from "./firestoreValueWalk.ts";
12
+ import * as querySnapshot from "./querySnapshot.ts";
13
+
14
+ /** @internal */
15
+ export const TypeId = "~@visiolab/effect-firebase/firestore/Firestore";
16
+
17
+ /** @internal */
18
+ export const TransactionTypeId = "~@visiolab/effect-firebase/firestore/Firestore/Transaction";
19
+
20
+ /** What `getDocs` and `streamDocs` read from: a collection, or a group. */
21
+ type Source = Collection.CollectionReference<Collection.Any> | CollectionGroup.Any;
22
+
23
+ /** The last segment of a document's path: its id. */
24
+ const idOf = (path: string): string => path.slice(path.lastIndexOf("/") + 1);
25
+
26
+ /**
27
+ * What a hook is handed for `source`, and how each document it read is
28
+ * addressed: in the collection read, by its id, or, for a group, matched by
29
+ * its path to the collection of the group it lives in.
30
+ */
31
+ const resolve = (
32
+ source: Source,
33
+ ): {
34
+ readonly selector: Firestore.CollectionSelector;
35
+ readonly refOf: (
36
+ path: string,
37
+ ) => Result.Result<
38
+ Collection.DocumentReference<Collection.Any>,
39
+ Firestore.UndefinedCollectionError
40
+ >;
41
+ } =>
42
+ collectionGroup.isCollectionGroup(source)
43
+ ? {
44
+ selector: { _tag: "CollectionGroup", id: source.id },
45
+ refOf: (path) => collectionGroup.documentAt(source, path),
46
+ }
47
+ : {
48
+ selector: { _tag: "Collection", path: source.path },
49
+ refOf: (path) => Result.succeed(source.doc(source.collection.id.make(idOf(path)))),
50
+ };
51
+
52
+ // The snapshot constructors put each field of the `extra` they are handed on
53
+ // the snapshot, but are typed without it, so the two below say it is there.
54
+
55
+ /** A snapshot of the document at `ref`, as a hook read it, with its extra. */
56
+ const documentSnapshotOf = <C extends Collection.Any, Extra extends object>(
57
+ ref: Collection.DocumentReference<C>,
58
+ read: Firestore.DocumentRead<Extra>,
59
+ ): Result.Result<DocumentSnapshot.DocumentSnapshot<C> & Extra, Firestore.FirestoreError> =>
60
+ Result.map(
61
+ firestoreValueWalk.checkDocument(ref.path, read.data),
62
+ (data) =>
63
+ documentSnapshot.make(ref, data, read.extra) as DocumentSnapshot.DocumentSnapshot<C> & Extra,
64
+ );
65
+
66
+ /**
67
+ * A snapshot of the documents a hook read, each addressed by `refOf`, with the
68
+ * extra of each and of the read as a whole.
69
+ */
70
+ const querySnapshotOf = <Extra extends object>(
71
+ refOf: ReturnType<typeof resolve>["refOf"],
72
+ read: Firestore.QueryRead<Extra>,
73
+ ): Result.Result<
74
+ QuerySnapshot.QuerySnapshot<Collection.Any, Extra> & Extra,
75
+ Firestore.FirestoreError | Firestore.UndefinedCollectionError
76
+ > =>
77
+ Result.map(
78
+ Result.all(
79
+ read.docs.map((doc) =>
80
+ Result.flatMap(refOf(doc.path), (ref) =>
81
+ Result.map(firestoreValueWalk.checkDocument(ref.path, doc.data), (data) =>
82
+ documentSnapshot.existing(ref, data, doc.extra),
83
+ ),
84
+ ),
85
+ ),
86
+ ),
87
+ (docs) =>
88
+ querySnapshot.make(docs, read.extra) as QuerySnapshot.QuerySnapshot<Collection.Any, Extra> &
89
+ Extra,
90
+ );
91
+
92
+ /**
93
+ * Turns what is written, in the shape `schema` takes, into Firestore Values,
94
+ * through the codec derived from `schema`. Whatever services the schema
95
+ * encodes through are left for the caller to provide.
96
+ */
97
+ const encode = <S extends Schema.Top>(schema: S, data: S["Type"]) =>
98
+ Schema.encodeEffect(firestoreValue.toCodecFirestore(schema))(data);
99
+
100
+ /** A document, in its model's `create` shape. */
101
+ const encodeCreate = <C extends Collection.Any>(collection: C, data: Firestore.Create<C>) => {
102
+ // Named so that the codec is derived from the schema `C` was defined with
103
+ // rather than from `Schema.Top`, which keeps the services it encodes through
104
+ // in the type instead of erasing them to `unknown`.
105
+ const schema: Collection.DocumentSchema<C>["create"] = collection.schema.create;
106
+ // A model is a struct, so it encodes to a map.
107
+ return encode(schema, data) as Effect.Effect<
108
+ Firestore.DocumentData,
109
+ Schema.SchemaError,
110
+ Collection.DocumentSchema<C>["create"]["EncodingServices"]
111
+ >;
112
+ };
113
+
114
+ /**
115
+ * A patch, in its model's update shape for `mode`, which encodes to a map from
116
+ * the field paths it writes at to what it writes there.
117
+ */
118
+ const encodeUpdate = <C extends Collection.Any, Mode extends FirestoreModel.UpdateMode>(
119
+ collection: C,
120
+ data: Firestore.Update<C, Mode>,
121
+ mode: Mode | undefined,
122
+ ) => {
123
+ const schema = (
124
+ mode === "unchecked" ? collection.schema.updateUnchecked : collection.schema.update
125
+ ) as Firestore.UpdateSchema<C, Mode>;
126
+ // Both modes encode the same fields at the same paths, so they ask for the
127
+ // same services.
128
+ return encode(schema, data) as Effect.Effect<
129
+ Firestore.DocumentData,
130
+ Schema.SchemaError,
131
+ Collection.DocumentSchema<C>["update"]["EncodingServices"]
132
+ >;
133
+ };
134
+
135
+ /**
136
+ * Where reads and writes go: straight to the database, or into one attempt at
137
+ * a transaction, through the hooks of {@link Firestore.MakeOptions}, with
138
+ * `getDocs` only where the backend can read many documents there. `name`
139
+ * prefixes the span of each.
140
+ */
141
+ interface Target<ReadOptions, Extra extends object>
142
+ extends
143
+ Pick<Firestore.MakeOptions<ReadOptions, never, Extra>, "getDoc" | "setDoc" | "updateDoc">,
144
+ Partial<Pick<Firestore.MakeOptions<ReadOptions, never, Extra>, "getDocs">> {
145
+ readonly name: string;
146
+ }
147
+
148
+ /**
149
+ * The reads and writes a {@link Firestore.Firestore} and a transaction share,
150
+ * made against `target`. Each runs in a span named after it, so traces read
151
+ * the same whichever backend answered.
152
+ */
153
+ const operations = <ReadOptions, Extra extends object>(target: Target<ReadOptions, Extra>) => {
154
+ const span = (name: string) => Effect.withSpan(`${target.name}.${name}`);
155
+
156
+ const getDocs = target.getDocs;
157
+
158
+ return {
159
+ getDoc: <C extends Collection.Any>(
160
+ ref: Collection.DocumentReference<C>,
161
+ options?: ReadOptions,
162
+ ) =>
163
+ Effect.flatMap(target.getDoc(ref.path, options), (read) =>
164
+ Effect.fromResult(documentSnapshotOf(ref, read)),
165
+ ).pipe(span("getDoc")),
166
+
167
+ getDocs:
168
+ getDocs === undefined
169
+ ? undefined
170
+ : (source: Source, options?: ReadOptions) => {
171
+ const { selector, refOf } = resolve(source);
172
+ return Effect.flatMap(getDocs(selector, options), (read) =>
173
+ Effect.fromResult(querySnapshotOf(refOf, read)),
174
+ ).pipe(span("getDocs"));
175
+ },
176
+
177
+ setDoc: <C extends Collection.Any>(
178
+ ref: Collection.DocumentReference<C>,
179
+ data: Firestore.Create<C>,
180
+ ) =>
181
+ Effect.flatMap(encodeCreate(ref.collection, data), (encoded) =>
182
+ target.setDoc(ref.path, encoded),
183
+ ).pipe(span("setDoc")),
184
+
185
+ updateDoc: <C extends Collection.Any, Mode extends FirestoreModel.UpdateMode = "checked">(
186
+ ref: Collection.DocumentReference<C>,
187
+ data: Firestore.Update<C, Mode>,
188
+ options?: Firestore.UpdateOptions<Mode>,
189
+ ) =>
190
+ Effect.flatMap(encodeUpdate(ref.collection, data, options?.mode), (encoded) =>
191
+ target.updateDoc(ref.path, encoded),
192
+ ).pipe(span("updateDoc")),
193
+ };
194
+ };
195
+
196
+ /**
197
+ * The transaction a body reads and writes through, over the operations of one
198
+ * attempt.
199
+ */
200
+ const transactionOf = <Operations extends Firestore.TransactionOperations>(
201
+ transaction: Operations,
202
+ ): Firestore.BuiltTransaction<Operations> => {
203
+ const readMany = transaction.getDocs;
204
+ const { getDoc, getDocs, setDoc, updateDoc } = operations<never, {}>({
205
+ name: "Firestore.Transaction",
206
+ getDoc: (path) => transaction.getDoc(path),
207
+ getDocs: readMany === undefined ? undefined : (selector) => readMany(selector),
208
+ setDoc: transaction.setDoc,
209
+ updateDoc: transaction.updateDoc,
210
+ });
211
+ // `getDocs` is there exactly where `Operations` has one, which is what
212
+ // `BuiltTransaction` says, though the compiler cannot follow it into the
213
+ // conditional type.
214
+ const portable = { [TransactionTypeId]: TransactionTypeId, getDoc, setDoc, updateDoc } as const;
215
+ return (
216
+ getDocs === undefined ? portable : { ...portable, getDocs }
217
+ ) as Firestore.BuiltTransaction<Operations>;
218
+ };
219
+
220
+ /**
221
+ * The key a transaction body finds its {@link Firestore.Transaction} under is
222
+ * handed in by the public module that defines it, so that this one only
223
+ * imports it as a type and the two do not import each other.
224
+ *
225
+ * @internal
226
+ */
227
+ export const make = <
228
+ ReadOptions,
229
+ ListenOptions,
230
+ Extra extends object,
231
+ Operations extends Firestore.TransactionOperations,
232
+ TransactionServices,
233
+ >(
234
+ options: Firestore.MakeOptions<
235
+ ReadOptions,
236
+ ListenOptions,
237
+ Extra,
238
+ Operations,
239
+ TransactionServices
240
+ >,
241
+ Transaction: typeof Firestore.Transaction,
242
+ ): Firestore.With<ReadOptions, ListenOptions, Extra, TransactionServices> & Firestore.Firestore => {
243
+ const { getDoc, getDocs, setDoc, updateDoc } = operations<ReadOptions, Extra>({
244
+ name: "Firestore",
245
+ getDoc: options.getDoc,
246
+ getDocs: options.getDocs,
247
+ setDoc: options.setDoc,
248
+ updateDoc: options.updateDoc,
249
+ });
250
+
251
+ const addDoc = <C extends Collection.Any>(
252
+ ref: Collection.CollectionReference<C>,
253
+ data: Firestore.Create<C>,
254
+ ) =>
255
+ Effect.flatMap(encodeCreate(ref.collection, data), (encoded) =>
256
+ Effect.map(options.addDoc(ref.path, encoded), (id) => ref.doc(ref.collection.id.make(id))),
257
+ ).pipe(Effect.withSpan("Firestore.addDoc"));
258
+
259
+ // Each read is decoded as it is pulled, so a reader that falls behind only
260
+ // decodes the latest one the hook kept.
261
+ const streamDoc = <C extends Collection.Any>(
262
+ ref: Collection.DocumentReference<C>,
263
+ listenOptions?: ListenOptions,
264
+ ) =>
265
+ Stream.mapEffect(options.streamDoc(ref.path, listenOptions), (read) =>
266
+ Effect.fromResult(documentSnapshotOf(ref, read)),
267
+ ).pipe(Stream.withSpan("Firestore.streamDoc"));
268
+
269
+ const streamDocs = (source: Source, listenOptions?: ListenOptions) => {
270
+ const { selector, refOf } = resolve(source);
271
+ return Stream.mapEffect(options.streamDocs(selector, listenOptions), (read) =>
272
+ Effect.fromResult(querySnapshotOf(refOf, read)),
273
+ ).pipe(Stream.withSpan("Firestore.streamDocs"));
274
+ };
275
+
276
+ const runTransaction = Effect.fn("Firestore.runTransaction")(function* <A, E, R>(
277
+ effect: Effect.Effect<A, E, R>,
278
+ transactionOptions?: Firestore.TransactionOptions,
279
+ ) {
280
+ // Captured inside the span, so the body's own spans hang off it, and
281
+ // handed to the body whole, so it needs nothing from wherever the hook
282
+ // runs it.
283
+ const context =
284
+ yield* Effect.context<Exclude<R, Firestore.Transaction | TransactionServices>>();
285
+ const offer = options.runTransaction.context;
286
+ const maxAttempts = transactionOptions?.maxAttempts;
287
+
288
+ return yield* options.runTransaction.run(
289
+ (operations) => {
290
+ // One transaction, under every tag, so that helpers asking for any of
291
+ // them read and write through it alike.
292
+ const transaction = transactionOf(operations);
293
+ const withTransaction = Context.add(context, Transaction, transaction);
294
+ return Effect.provideContext(
295
+ effect,
296
+ (offer === undefined
297
+ ? withTransaction
298
+ : Context.merge(withTransaction, offer(transaction))) as Context.Context<R>,
299
+ );
300
+ },
301
+ // Left out rather than `undefined` where it is not set, since some
302
+ // backends read `undefined` as no retries at all.
303
+ maxAttempts === undefined ? {} : { maxAttempts },
304
+ );
305
+ });
306
+
307
+ const service: Firestore.With<ReadOptions, ListenOptions, Extra, TransactionServices> = {
308
+ getDoc,
309
+ getDocs: getDocs as Firestore.With<ReadOptions, never, Extra>["getDocs"],
310
+ setDoc,
311
+ updateDoc,
312
+ addDoc,
313
+ streamDoc,
314
+ streamDocs: streamDocs as Firestore.With<never, ListenOptions, Extra>["streamDocs"],
315
+ runTransaction,
316
+ };
317
+ // Every backend's service is a portable Firestore too: its reads and listens
318
+ // only add optional options, its snapshots only add fields, and its
319
+ // transaction bodies need no more than a portable one's. The compiler cannot
320
+ // check that for unknown type parameters.
321
+ return { [TypeId]: TypeId, ...service } as typeof service & Firestore.Firestore;
322
+ };
@@ -0,0 +1,36 @@
1
+ /**
2
+ * The errors of the Firestore module, defined apart from it so that the
3
+ * modules it builds on can raise them without importing it back: the public
4
+ * module imports its implementation, and an import cycle through it would
5
+ * leave these unset while a bundle evaluates.
6
+ */
7
+ import { Schema } from "effect";
8
+
9
+ /** @internal */
10
+ export class FirestoreError extends Schema.TaggedError<
11
+ FirestoreError,
12
+ { readonly brand: unique symbol }
13
+ >()("FirestoreError", {
14
+ message: Schema.String,
15
+ /**
16
+ * The code the SDK reported, where it reported one it can be named by.
17
+ *
18
+ * The client SDK names every failure, such as `permission-denied`. The Admin
19
+ * SDK answers for Firestore over gRPC, which numbers its statuses rather than
20
+ * naming them, so there this is often absent even where something went wrong
21
+ * at the database.
22
+ */
23
+ code: Schema.optional(Schema.String),
24
+ cause: Schema.optional(Schema.Defect()),
25
+ }) {}
26
+
27
+ /** @internal */
28
+ export class UndefinedCollectionError extends Schema.TaggedError<
29
+ UndefinedCollectionError,
30
+ { readonly brand: unique symbol }
31
+ >()("UndefinedCollectionError", {
32
+ /** The full path of the document no collection of the group defines. */
33
+ path: Schema.String,
34
+ /** The collection id of the group that was read. */
35
+ collectionId: Schema.String,
36
+ }) {}