@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.
- package/LICENSE +21 -0
- package/README.md +81 -2
- package/dist/_virtual/_rolldown/runtime.mjs +12 -0
- package/dist/app/App.d.mts +27 -0
- package/dist/app/App.mjs +27 -0
- package/dist/app/index.d.mts +2 -0
- package/dist/app/index.mjs +1 -0
- package/dist/firestore/Collection.d.mts +423 -0
- package/dist/firestore/Collection.mjs +133 -0
- package/dist/firestore/CollectionGroup.d.mts +164 -0
- package/dist/firestore/CollectionGroup.mjs +84 -0
- package/dist/firestore/DocumentPath.d.mts +95 -0
- package/dist/firestore/DocumentPath.mjs +71 -0
- package/dist/firestore/DocumentSnapshot.d.mts +239 -0
- package/dist/firestore/DocumentSnapshot.mjs +134 -0
- package/dist/firestore/FieldValue.d.mts +332 -0
- package/dist/firestore/FieldValue.mjs +256 -0
- package/dist/firestore/Firestore.d.mts +1165 -0
- package/dist/firestore/Firestore.mjs +274 -0
- package/dist/firestore/FirestoreModel.d.mts +1232 -0
- package/dist/firestore/FirestoreModel.mjs +586 -0
- package/dist/firestore/FirestoreSchema.d.mts +567 -0
- package/dist/firestore/FirestoreSchema.mjs +419 -0
- package/dist/firestore/FirestoreValue.d.mts +145 -0
- package/dist/firestore/FirestoreValue.mjs +87 -0
- package/dist/firestore/GeoPoint.d.mts +90 -0
- package/dist/firestore/GeoPoint.mjs +75 -0
- package/dist/firestore/Patch.d.mts +73 -0
- package/dist/firestore/Patch.mjs +57 -0
- package/dist/firestore/QuerySnapshot.d.mts +107 -0
- package/dist/firestore/QuerySnapshot.mjs +43 -0
- package/dist/firestore/Timestamp.d.mts +273 -0
- package/dist/firestore/Timestamp.mjs +255 -0
- package/dist/firestore/index.d.mts +14 -0
- package/dist/firestore/index.mjs +14 -0
- package/dist/firestore/internal/collection.mjs +156 -0
- package/dist/firestore/internal/collectionGroup.mjs +64 -0
- package/dist/firestore/internal/documentPath.d.mts +4 -0
- package/dist/firestore/internal/documentPath.mjs +42 -0
- package/dist/firestore/internal/documentSnapshot.mjs +63 -0
- package/dist/firestore/internal/fieldValue.d.mts +11 -0
- package/dist/firestore/internal/fieldValue.mjs +88 -0
- package/dist/firestore/internal/firestore.mjs +145 -0
- package/dist/firestore/internal/firestoreError.d.mts +29 -0
- package/dist/firestore/internal/firestoreError.mjs +30 -0
- package/dist/firestore/internal/firestoreModel.mjs +719 -0
- package/dist/firestore/internal/firestoreSchema.mjs +123 -0
- package/dist/firestore/internal/firestoreValue.mjs +108 -0
- package/dist/firestore/internal/firestoreValueWalk.d.mts +58 -0
- package/dist/firestore/internal/firestoreValueWalk.mjs +223 -0
- package/dist/firestore/internal/geoPoint.d.mts +4 -0
- package/dist/firestore/internal/geoPoint.mjs +39 -0
- package/dist/firestore/internal/patch.mjs +19 -0
- package/dist/firestore/internal/querySnapshot.mjs +24 -0
- package/dist/firestore/internal/schemaAST.mjs +35 -0
- package/dist/firestore/internal/snapshotExtra.mjs +35 -0
- package/dist/firestore/internal/streamFromListener.mjs +12 -0
- package/dist/firestore/internal/timestamp.d.mts +4 -0
- package/dist/firestore/internal/timestamp.mjs +66 -0
- package/dist/firestore/internal/timestampSchema.mjs +31 -0
- package/dist/firestore/internal/transactionFromCallback.mjs +66 -0
- package/dist/functions/Callable.d.mts +654 -0
- package/dist/functions/Callable.mjs +450 -0
- package/dist/functions/CallableError.d.mts +297 -0
- package/dist/functions/CallableError.mjs +256 -0
- package/dist/functions/CallableMiddleware.d.mts +213 -0
- package/dist/functions/CallableMiddleware.mjs +52 -0
- package/dist/functions/CallableRequest.d.mts +166 -0
- package/dist/functions/CallableRequest.mjs +18 -0
- package/dist/functions/Caller.d.mts +103 -0
- package/dist/functions/Caller.mjs +115 -0
- package/dist/functions/DocumentTrigger.d.mts +795 -0
- package/dist/functions/DocumentTrigger.mjs +566 -0
- package/dist/functions/FirebaseFunction.d.mts +79 -0
- package/dist/functions/FirebaseFunction.mjs +4 -0
- package/dist/functions/FunctionGroup.d.mts +175 -0
- package/dist/functions/FunctionGroup.mjs +64 -0
- package/dist/functions/index.d.mts +9 -0
- package/dist/functions/index.mjs +9 -0
- package/dist/functions/internal/callable.mjs +167 -0
- package/dist/functions/internal/callableMiddleware.d.mts +19 -0
- package/dist/functions/internal/callableMiddleware.mjs +15 -0
- package/dist/functions/internal/callableRequest.mjs +6 -0
- package/dist/functions/internal/documentTrigger.d.mts +10 -0
- package/dist/functions/internal/documentTrigger.mjs +73 -0
- package/dist/functions/internal/firebaseFunction.d.mts +5 -0
- package/dist/functions/internal/firebaseFunction.mjs +24 -0
- package/dist/functions/internal/functionGroup.mjs +36 -0
- package/package.json +66 -3
- package/src/app/App.ts +24 -0
- package/src/app/index.ts +7 -0
- package/src/firestore/Collection.ts +520 -0
- package/src/firestore/CollectionGroup.ts +245 -0
- package/src/firestore/DocumentPath.ts +114 -0
- package/src/firestore/DocumentSnapshot.ts +308 -0
- package/src/firestore/FieldValue.ts +375 -0
- package/src/firestore/Firestore.ts +1432 -0
- package/src/firestore/FirestoreModel.ts +1884 -0
- package/src/firestore/FirestoreSchema.ts +643 -0
- package/src/firestore/FirestoreValue.ts +207 -0
- package/src/firestore/GeoPoint.ts +105 -0
- package/src/firestore/Patch.ts +117 -0
- package/src/firestore/QuerySnapshot.ts +150 -0
- package/src/firestore/Timestamp.ts +302 -0
- package/src/firestore/index.ts +67 -0
- package/src/firestore/internal/collection.ts +246 -0
- package/src/firestore/internal/collectionGroup.ts +81 -0
- package/src/firestore/internal/documentPath.ts +53 -0
- package/src/firestore/internal/documentSnapshot.ts +117 -0
- package/src/firestore/internal/fieldValue.ts +119 -0
- package/src/firestore/internal/firestore.ts +322 -0
- package/src/firestore/internal/firestoreError.ts +36 -0
- package/src/firestore/internal/firestoreModel.ts +1242 -0
- package/src/firestore/internal/firestoreSchema.ts +218 -0
- package/src/firestore/internal/firestoreValue.ts +202 -0
- package/src/firestore/internal/firestoreValueWalk.ts +349 -0
- package/src/firestore/internal/geoPoint.ts +59 -0
- package/src/firestore/internal/patch.ts +21 -0
- package/src/firestore/internal/querySnapshot.ts +39 -0
- package/src/firestore/internal/schemaAST.ts +83 -0
- package/src/firestore/internal/snapshotExtra.ts +47 -0
- package/src/firestore/internal/streamFromListener.ts +27 -0
- package/src/firestore/internal/timestamp.ts +119 -0
- package/src/firestore/internal/timestampSchema.ts +42 -0
- package/src/firestore/internal/transactionFromCallback.ts +106 -0
- package/src/functions/Callable.ts +855 -0
- package/src/functions/CallableError.ts +348 -0
- package/src/functions/CallableMiddleware.ts +279 -0
- package/src/functions/CallableRequest.ts +207 -0
- package/src/functions/Caller.ts +141 -0
- package/src/functions/DocumentTrigger.ts +927 -0
- package/src/functions/FirebaseFunction.ts +96 -0
- package/src/functions/FunctionGroup.ts +225 -0
- package/src/functions/index.ts +62 -0
- package/src/functions/internal/callable.ts +272 -0
- package/src/functions/internal/callableMiddleware.ts +54 -0
- package/src/functions/internal/callableRequest.ts +8 -0
- package/src/functions/internal/documentTrigger.ts +143 -0
- package/src/functions/internal/firebaseFunction.ts +58 -0
- 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
|
+
}) {}
|