@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,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Functions: what every kind of server function shares, whatever runs it.
|
|
3
|
+
*
|
|
4
|
+
* A Function is declared once by its name and what runs it. A
|
|
5
|
+
* {@link Callable.Callable} is one kind of Function, run by a call from the
|
|
6
|
+
* client. Each kind has a module of its own, which owns its declaration, its
|
|
7
|
+
* handler and how it serves. This module holds only what they share: the
|
|
8
|
+
* kind, the name, and the {@link Handler} key a handler is provided under.
|
|
9
|
+
*
|
|
10
|
+
* Functions are deployed with `Functions.make` for a Function in no group,
|
|
11
|
+
* and `Functions.group` for the members of a
|
|
12
|
+
* {@link FunctionGroup.FunctionGroup}, from `@visiolab/effect-firebase-functions`.
|
|
13
|
+
*
|
|
14
|
+
* @stability unstable
|
|
15
|
+
* @since 0.3.0
|
|
16
|
+
*/
|
|
17
|
+
import type * as internal from "./internal/firebaseFunction.ts";
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* A Function: a server function's kind and its name.
|
|
21
|
+
*
|
|
22
|
+
* **Details**
|
|
23
|
+
*
|
|
24
|
+
* The name is the Function's identity. A Function in no group is deployed
|
|
25
|
+
* under its name, and a member of a group under `<group>-<name>`. It is a
|
|
26
|
+
* literal type that exports and group members are keyed on.
|
|
27
|
+
*
|
|
28
|
+
* The kind says what runs the Function, such as `"Callable"`, and decides how
|
|
29
|
+
* it is served and deployed.
|
|
30
|
+
*
|
|
31
|
+
* @stability unstable
|
|
32
|
+
* @category models
|
|
33
|
+
* @since 0.3.0
|
|
34
|
+
*/
|
|
35
|
+
export interface FirebaseFunction<out Kind extends string, out Name extends string> {
|
|
36
|
+
/** What runs the Function, such as `"Callable"`. */
|
|
37
|
+
readonly kind: Kind;
|
|
38
|
+
/** The Function's name, which a Function in no group is deployed under. */
|
|
39
|
+
readonly name: Name;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Any {@link FirebaseFunction}, whatever its kind and name.
|
|
44
|
+
*
|
|
45
|
+
* @stability unstable
|
|
46
|
+
* @category models
|
|
47
|
+
* @since 0.3.0
|
|
48
|
+
*/
|
|
49
|
+
export interface Any extends FirebaseFunction<string, string> {}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Marks the {@link Handler} of a Function in no group.
|
|
53
|
+
*
|
|
54
|
+
* @stability unstable
|
|
55
|
+
* @category models
|
|
56
|
+
* @since 0.3.0
|
|
57
|
+
*/
|
|
58
|
+
export interface NoGroup {
|
|
59
|
+
readonly [internal.NoGroupTypeId]: typeof internal.NoGroupTypeId;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* The service that holds the handler of the Function `Name`, as served in
|
|
64
|
+
* `Group`, whatever its kind.
|
|
65
|
+
*
|
|
66
|
+
* **Details**
|
|
67
|
+
*
|
|
68
|
+
* A handler is keyed by its group and its name, so the handler of `greet` in
|
|
69
|
+
* no group is `Handler<NoGroup, "greet">`, and that of `create` in the group
|
|
70
|
+
* `users` is `Handler<"users", "create">`. A Layer that serves a Function must
|
|
71
|
+
* provide its handler under exactly that key. Each kind's `toLayer`, such as
|
|
72
|
+
* {@link Callable.toLayer}, builds such a Layer.
|
|
73
|
+
*
|
|
74
|
+
* @see {@link HandlerFor} for the handler of a Function in no group
|
|
75
|
+
*
|
|
76
|
+
* @stability unstable
|
|
77
|
+
* @category models
|
|
78
|
+
* @since 0.3.0
|
|
79
|
+
*/
|
|
80
|
+
export interface Handler<Group, Name extends string> {
|
|
81
|
+
readonly [internal.HandlerTypeId]: {
|
|
82
|
+
readonly group: Group;
|
|
83
|
+
readonly name: Name;
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* The {@link Handler} of a Function in no group.
|
|
89
|
+
*
|
|
90
|
+
* @see {@link Handler} for the handler of a member of a group
|
|
91
|
+
*
|
|
92
|
+
* @stability unstable
|
|
93
|
+
* @category type-level
|
|
94
|
+
* @since 0.3.0
|
|
95
|
+
*/
|
|
96
|
+
export type HandlerFor<F extends Any> = Handler<NoGroup, F["name"]>;
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Function Groups: named sets of Functions of any kind that deploy under the
|
|
3
|
+
* group's name as a prefix.
|
|
4
|
+
*
|
|
5
|
+
* A member `create` of the group `users` deploys as `users-create`, whatever
|
|
6
|
+
* its kind. The server implements a member with its kind's `toLayer`, such as
|
|
7
|
+
* `Callable.toLayer(Users, "create", handler)`, and deploys the whole group
|
|
8
|
+
* with `Functions.group` from `@visiolab/effect-firebase-functions`. The
|
|
9
|
+
* client calls the group's Callables through `CallableClient.make(Users)`
|
|
10
|
+
* from `@visiolab/effect-firebase-client`.
|
|
11
|
+
*
|
|
12
|
+
* @example Declaring a group
|
|
13
|
+
* ```ts
|
|
14
|
+
* import { Schema } from "effect";
|
|
15
|
+
* import { Callable, FunctionGroup } from "@visiolab/effect-firebase/functions";
|
|
16
|
+
*
|
|
17
|
+
* const CreateUser = Callable.make("create", {
|
|
18
|
+
* payload: { name: Schema.String },
|
|
19
|
+
* success: Schema.String,
|
|
20
|
+
* });
|
|
21
|
+
* const DeleteUser = Callable.make("delete", { payload: { id: Schema.String } });
|
|
22
|
+
*
|
|
23
|
+
* class Users extends FunctionGroup.make("users").add(CreateUser, DeleteUser) {}
|
|
24
|
+
* ```
|
|
25
|
+
*
|
|
26
|
+
* @stability unstable
|
|
27
|
+
* @since 0.3.0
|
|
28
|
+
*/
|
|
29
|
+
import type { Pipeable } from "effect";
|
|
30
|
+
import type * as FirebaseFunction from "./FirebaseFunction.ts";
|
|
31
|
+
import * as internal from "./internal/functionGroup.ts";
|
|
32
|
+
|
|
33
|
+
const TypeId = internal.TypeId;
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* A Function Group: a name and the Functions it deploys under that name.
|
|
37
|
+
*
|
|
38
|
+
* **Details**
|
|
39
|
+
*
|
|
40
|
+
* A group is a class, so `class Users extends FunctionGroup.make("users")`
|
|
41
|
+
* gives it a name of its own in types and in hovers. Its name is
|
|
42
|
+
* `identifier`, since a class already has a `name` of its own.
|
|
43
|
+
*
|
|
44
|
+
* A group holds Functions of any kind, so a group can follow a domain, such
|
|
45
|
+
* as `posts`, rather than a kind. A Function's name is unique within its
|
|
46
|
+
* group. `.add` checks this when the module loads; the types don't. A
|
|
47
|
+
* Function can be in more than one group, and each group serves and
|
|
48
|
+
* implements it separately. Groups don't nest.
|
|
49
|
+
*
|
|
50
|
+
* A group is only a name and its members. It carries no middleware: attach a
|
|
51
|
+
* Callable Middleware to each Callable that needs it with
|
|
52
|
+
* {@link Callable.Callable.middleware}.
|
|
53
|
+
*
|
|
54
|
+
* @stability unstable
|
|
55
|
+
* @category models
|
|
56
|
+
* @since 0.3.0
|
|
57
|
+
*/
|
|
58
|
+
export interface FunctionGroup<
|
|
59
|
+
out Identifier extends string,
|
|
60
|
+
in out Functions extends FirebaseFunction.Any = never,
|
|
61
|
+
>
|
|
62
|
+
extends Pipeable.Pipeable {
|
|
63
|
+
new (_: never): {};
|
|
64
|
+
readonly [TypeId]: typeof TypeId;
|
|
65
|
+
/** The group's name: the prefix its members deploy under. */
|
|
66
|
+
readonly identifier: Identifier;
|
|
67
|
+
/** The group's members, keyed by their names. */
|
|
68
|
+
readonly functions: { readonly [F in Functions as F["name"]]: F };
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Adds Functions of any kind to the group.
|
|
72
|
+
*
|
|
73
|
+
* **Details**
|
|
74
|
+
*
|
|
75
|
+
* Returns a new group. Throws when a Function's name is already taken in
|
|
76
|
+
* the group, so a clash fails when the module loads, before anything
|
|
77
|
+
* deploys. The types don't catch a clash.
|
|
78
|
+
*/
|
|
79
|
+
add<const A extends ReadonlyArray<FirebaseFunction.Any>>(
|
|
80
|
+
...functions: A
|
|
81
|
+
): FunctionGroup<Identifier, Functions | A[number]>;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Any {@link FunctionGroup}, whatever its name and members.
|
|
86
|
+
*
|
|
87
|
+
* @stability unstable
|
|
88
|
+
* @category models
|
|
89
|
+
* @since 0.3.0
|
|
90
|
+
*/
|
|
91
|
+
export interface Any {
|
|
92
|
+
readonly [TypeId]: typeof TypeId;
|
|
93
|
+
readonly identifier: string;
|
|
94
|
+
readonly functions: { readonly [name: string]: FirebaseFunction.Any };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* The members of a {@link FunctionGroup}, as a union of Functions.
|
|
99
|
+
*
|
|
100
|
+
* **When to use**
|
|
101
|
+
*
|
|
102
|
+
* Use to read every member of a group. Pass the `Any` of a kind as `K`, such
|
|
103
|
+
* as `Members<typeof Users, Callable.Any>`, to keep only the members of that
|
|
104
|
+
* kind.
|
|
105
|
+
*
|
|
106
|
+
* @see {@link Names} for the names of the members
|
|
107
|
+
* @see {@link Member} for the member of one name
|
|
108
|
+
*
|
|
109
|
+
* @stability unstable
|
|
110
|
+
* @category type-level
|
|
111
|
+
* @since 0.3.0
|
|
112
|
+
*/
|
|
113
|
+
export type Members<G, K extends FirebaseFunction.Any = FirebaseFunction.Any> = G extends {
|
|
114
|
+
readonly functions: infer Record;
|
|
115
|
+
}
|
|
116
|
+
? Extract<Record[keyof Record], K>
|
|
117
|
+
: never;
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* The names of the members of a {@link FunctionGroup}.
|
|
121
|
+
*
|
|
122
|
+
* **When to use**
|
|
123
|
+
*
|
|
124
|
+
* Use to key something on a group's members. Pass the `Any` of a kind as `K`
|
|
125
|
+
* to keep only the names of the members of that kind.
|
|
126
|
+
*
|
|
127
|
+
* @see {@link Members} for the members themselves
|
|
128
|
+
*
|
|
129
|
+
* @stability unstable
|
|
130
|
+
* @category type-level
|
|
131
|
+
* @since 0.3.0
|
|
132
|
+
*/
|
|
133
|
+
export type Names<G, K extends FirebaseFunction.Any = FirebaseFunction.Any> = Members<G, K>["name"];
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* The member of a {@link FunctionGroup} named `Name`.
|
|
137
|
+
*
|
|
138
|
+
* **When to use**
|
|
139
|
+
*
|
|
140
|
+
* Use to read one member's type by its name. Pass the `Any` of a kind as `K`
|
|
141
|
+
* to get `never` unless the member is of that kind.
|
|
142
|
+
*
|
|
143
|
+
* @see {@link Members} for every member
|
|
144
|
+
*
|
|
145
|
+
* @stability unstable
|
|
146
|
+
* @category type-level
|
|
147
|
+
* @since 0.3.0
|
|
148
|
+
*/
|
|
149
|
+
export type Member<
|
|
150
|
+
G,
|
|
151
|
+
Name extends string,
|
|
152
|
+
K extends FirebaseFunction.Any = FirebaseFunction.Any,
|
|
153
|
+
> = Extract<Members<G, K>, { readonly name: Name }>;
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* The {@link FirebaseFunction.Handler}s that serving a {@link FunctionGroup}
|
|
157
|
+
* needs: one per member, keyed by the group.
|
|
158
|
+
*
|
|
159
|
+
* @stability unstable
|
|
160
|
+
* @category type-level
|
|
161
|
+
* @since 0.3.0
|
|
162
|
+
*/
|
|
163
|
+
export type Handlers<G extends Any> =
|
|
164
|
+
Names<G> extends infer Name
|
|
165
|
+
? Name extends string
|
|
166
|
+
? FirebaseFunction.Handler<G["identifier"], Name>
|
|
167
|
+
: never
|
|
168
|
+
: never;
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Checks whether a value is a {@link FunctionGroup}. A building block for the
|
|
172
|
+
* functions and client packages, which take a Function or a group. Most apps
|
|
173
|
+
* don't need it.
|
|
174
|
+
*
|
|
175
|
+
* @stability unstable
|
|
176
|
+
* @category guards
|
|
177
|
+
* @since 0.3.0
|
|
178
|
+
*/
|
|
179
|
+
export const isFunctionGroup: (u: unknown) => u is Any = internal.isFunctionGroup as never;
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Starts a Function Group with no members.
|
|
183
|
+
*
|
|
184
|
+
* **When to use**
|
|
185
|
+
*
|
|
186
|
+
* Use to collect related Functions, of any kind, under one prefix, and to
|
|
187
|
+
* deploy them as one. A Function that stands alone needs no group.
|
|
188
|
+
*
|
|
189
|
+
* **Details**
|
|
190
|
+
*
|
|
191
|
+
* Add members with `.add`. Extend the result as a class, so the group has a
|
|
192
|
+
* name of its own. Each member deploys as `<identifier>-<name>`.
|
|
193
|
+
*
|
|
194
|
+
* A client built from a Callable on its own, rather than from its group,
|
|
195
|
+
* calls the name without the prefix and fails with `not-found`. Nothing
|
|
196
|
+
* checks this at runtime.
|
|
197
|
+
*
|
|
198
|
+
* @example Serving one Callable in two groups
|
|
199
|
+
* ```ts
|
|
200
|
+
* import { Schema } from "effect";
|
|
201
|
+
* import { Callable, FunctionGroup } from "@visiolab/effect-firebase/functions";
|
|
202
|
+
*
|
|
203
|
+
* const GetUser = Callable.make("get", {
|
|
204
|
+
* payload: { id: Schema.String },
|
|
205
|
+
* success: Schema.String,
|
|
206
|
+
* });
|
|
207
|
+
*
|
|
208
|
+
* // Deploys as `users-get` and `admins-get`, each with a handler of its own.
|
|
209
|
+
* class Users extends FunctionGroup.make("users").add(GetUser) {}
|
|
210
|
+
* class Admins extends FunctionGroup.make("admins").add(GetUser) {}
|
|
211
|
+
* ```
|
|
212
|
+
*
|
|
213
|
+
* @see {@link Callable.make} for declaring a Callable member
|
|
214
|
+
* @see {@link Callable.toLayer} for implementing a Callable member
|
|
215
|
+
* @see {@link DocumentTrigger.created} for declaring a Document Trigger member
|
|
216
|
+
* @see {@link DocumentTrigger.toLayer} for implementing a Document Trigger member
|
|
217
|
+
* @see {@link Functions.group} for deploying the group
|
|
218
|
+
*
|
|
219
|
+
* @stability unstable
|
|
220
|
+
* @category constructors
|
|
221
|
+
* @since 0.3.0
|
|
222
|
+
*/
|
|
223
|
+
export const make = <const Identifier extends string>(
|
|
224
|
+
identifier: Identifier,
|
|
225
|
+
): FunctionGroup<Identifier> => internal.make({ identifier, functions: {} }) as never;
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Callables: server functions declared once and shared by server and client.
|
|
3
|
+
*
|
|
4
|
+
* @stability unstable
|
|
5
|
+
* @since 0.3.0
|
|
6
|
+
*/
|
|
7
|
+
export * as Callable from "./Callable.ts";
|
|
8
|
+
/**
|
|
9
|
+
* The error a Callable Client fails with when a call goes wrong in a way the
|
|
10
|
+
* contract doesn't declare.
|
|
11
|
+
*
|
|
12
|
+
* @stability unstable
|
|
13
|
+
* @since 0.3.0
|
|
14
|
+
*/
|
|
15
|
+
export * as CallableError from "./CallableError.ts";
|
|
16
|
+
/**
|
|
17
|
+
* Callable Middleware: what runs around every call to the Callables it is
|
|
18
|
+
* attached to, before the Payload is decoded.
|
|
19
|
+
*
|
|
20
|
+
* @stability unstable
|
|
21
|
+
* @since 0.3.0
|
|
22
|
+
*/
|
|
23
|
+
export * as CallableMiddleware from "./CallableMiddleware.ts";
|
|
24
|
+
/**
|
|
25
|
+
* The call a Callable is answering: who made it, from which app, and the raw
|
|
26
|
+
* Payload.
|
|
27
|
+
*
|
|
28
|
+
* @stability unstable
|
|
29
|
+
* @since 0.3.0
|
|
30
|
+
*/
|
|
31
|
+
export * as CallableRequest from "./CallableRequest.ts";
|
|
32
|
+
/**
|
|
33
|
+
* Document Triggers: server functions that run when a document of a
|
|
34
|
+
* collection changes.
|
|
35
|
+
*
|
|
36
|
+
* @stability unstable
|
|
37
|
+
* @since 0.3.0
|
|
38
|
+
*/
|
|
39
|
+
export * as DocumentTrigger from "./DocumentTrigger.ts";
|
|
40
|
+
/**
|
|
41
|
+
* Functions: what every kind of server function shares, whatever runs it.
|
|
42
|
+
*
|
|
43
|
+
* @stability unstable
|
|
44
|
+
* @since 0.3.0
|
|
45
|
+
*/
|
|
46
|
+
export * as FirebaseFunction from "./FirebaseFunction.ts";
|
|
47
|
+
/**
|
|
48
|
+
* Function Groups: named sets of Functions of any kind that deploy under the
|
|
49
|
+
* group's name as a prefix.
|
|
50
|
+
*
|
|
51
|
+
* @stability unstable
|
|
52
|
+
* @since 0.3.0
|
|
53
|
+
*/
|
|
54
|
+
export * as FunctionGroup from "./FunctionGroup.ts";
|
|
55
|
+
/**
|
|
56
|
+
* The Caller, the signed-in user making a call, and the built-in
|
|
57
|
+
* `Authenticated` middleware that provides it.
|
|
58
|
+
*
|
|
59
|
+
* @stability unstable
|
|
60
|
+
* @since 0.3.0
|
|
61
|
+
*/
|
|
62
|
+
export * as Caller from "./Caller.ts";
|
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
/** @internal */
|
|
2
|
+
import { Context, Effect, Layer, Pipeable, Schema } from "effect";
|
|
3
|
+
import * as HttpServerRequest from "effect/http/HttpServerRequest";
|
|
4
|
+
import * as CallableError from "../CallableError.ts";
|
|
5
|
+
import type * as CallableRequest from "../CallableRequest.ts";
|
|
6
|
+
import type * as CallableMiddleware from "./callableMiddleware.ts";
|
|
7
|
+
import * as callableRequest from "./callableRequest.ts";
|
|
8
|
+
import * as FirebaseFunction from "./firebaseFunction.ts";
|
|
9
|
+
|
|
10
|
+
/** @internal */
|
|
11
|
+
export const TypeId = "~@visiolab/effect-firebase/functions/Callable";
|
|
12
|
+
|
|
13
|
+
/** @internal */
|
|
14
|
+
export const kind = "Callable";
|
|
15
|
+
|
|
16
|
+
/** @internal */
|
|
17
|
+
export interface Fields extends FirebaseFunction.Fields {
|
|
18
|
+
readonly kind: typeof kind;
|
|
19
|
+
readonly name: string;
|
|
20
|
+
readonly payload: Schema.Top;
|
|
21
|
+
readonly success: Schema.Top;
|
|
22
|
+
readonly error: ReadonlySet<Schema.Top>;
|
|
23
|
+
readonly middlewares: ReadonlySet<CallableMiddleware.Key>;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const Proto = {
|
|
27
|
+
[TypeId]: TypeId,
|
|
28
|
+
middleware(this: Fields, middleware: CallableMiddleware.Key) {
|
|
29
|
+
return make({
|
|
30
|
+
kind,
|
|
31
|
+
name: this.name,
|
|
32
|
+
payload: this.payload,
|
|
33
|
+
success: this.success,
|
|
34
|
+
error: this.error,
|
|
35
|
+
middlewares: new Set([...this.middlewares, middleware]),
|
|
36
|
+
});
|
|
37
|
+
},
|
|
38
|
+
pipe() {
|
|
39
|
+
return Pipeable.pipeArguments(this, arguments);
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
/** @internal */
|
|
44
|
+
export const make = (fields: Fields): Fields => Object.assign(Object.create(Proto), fields);
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* A handler as it sits in the context: the services it was built with are
|
|
48
|
+
* already provided, so it needs only those of the call.
|
|
49
|
+
*
|
|
50
|
+
* @internal
|
|
51
|
+
*/
|
|
52
|
+
export type HandlerService = (payload: unknown) => Effect.Effect<unknown, unknown>;
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* What `toLayer` takes, with the types its signature has already checked
|
|
56
|
+
* erased.
|
|
57
|
+
*
|
|
58
|
+
* @internal
|
|
59
|
+
*/
|
|
60
|
+
export type HandlerInput = HandlerService | Effect.Effect<HandlerService>;
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* The key of the handler of the Callable `name` in `group`, or in no group.
|
|
64
|
+
*
|
|
65
|
+
* @internal
|
|
66
|
+
*/
|
|
67
|
+
export const handlerKey = (group: string | undefined, name: string) =>
|
|
68
|
+
FirebaseFunction.handlerKey<HandlerService>(group, name);
|
|
69
|
+
/** @internal */
|
|
70
|
+
export const toLayer = (group: string | undefined, name: string, handler: HandlerInput) =>
|
|
71
|
+
Layer.effect(
|
|
72
|
+
handlerKey(group, name),
|
|
73
|
+
Effect.gen(function* () {
|
|
74
|
+
const run = Effect.isEffect(handler) ? yield* handler : handler;
|
|
75
|
+
// The services the Layer was built with, under those of the call.
|
|
76
|
+
const built = yield* Effect.context<never>();
|
|
77
|
+
return (payload: unknown) =>
|
|
78
|
+
Effect.updateContext(run(payload), (call: Context.Context<never>) =>
|
|
79
|
+
Context.merge(built, call),
|
|
80
|
+
);
|
|
81
|
+
}),
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Schemas with their services erased: a Callable's codecs need none that the
|
|
86
|
+
* types track, and `Schema.Top` leaves them `unknown`.
|
|
87
|
+
*/
|
|
88
|
+
const codec = (schema: Schema.Top) => Schema.toCodecJson(schema as Schema.Codec<unknown>);
|
|
89
|
+
|
|
90
|
+
/** @internal */
|
|
91
|
+
export const getErrorCode = (schema: Schema.Top): CallableError.ErrorCode =>
|
|
92
|
+
Schema.resolveAnnotations(schema)?.callableErrorCode ?? "internal";
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* The name `callable` is deployed under: its own in no group, or
|
|
96
|
+
* `<group>-<name>` in `group`. The server exports it under this name, and the
|
|
97
|
+
* client calls it.
|
|
98
|
+
*
|
|
99
|
+
* @internal
|
|
100
|
+
*/
|
|
101
|
+
export const deployedName: (group: FirebaseFunction.Group | undefined, callable: Fields) => string =
|
|
102
|
+
FirebaseFunction.deployedName;
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* The attributes of the server's and the client's span of a call to
|
|
106
|
+
* `callable` in `group`, or in no group.
|
|
107
|
+
*
|
|
108
|
+
* @internal
|
|
109
|
+
*/
|
|
110
|
+
export const spanAttributes = (
|
|
111
|
+
group: FirebaseFunction.Group | undefined,
|
|
112
|
+
callable: Fields,
|
|
113
|
+
): Readonly<Record<string, string>> => ({
|
|
114
|
+
"rpc.system": "firebase_callable",
|
|
115
|
+
"rpc.method": callable.name,
|
|
116
|
+
...(group === undefined ? {} : { "rpc.service": group.identifier }),
|
|
117
|
+
"faas.name": deployedName(group, callable),
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* The schemas of every error a call to `callable` can be answered with: its
|
|
122
|
+
* declared errors, then those of its middleware.
|
|
123
|
+
*
|
|
124
|
+
* @internal
|
|
125
|
+
*/
|
|
126
|
+
export const errorSchemas = (callable: Fields): ReadonlyArray<Schema.Top> => [
|
|
127
|
+
...callable.error,
|
|
128
|
+
...[...callable.middlewares].flatMap((middleware) => [...middleware.error]),
|
|
129
|
+
];
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Turns a failure of the call into the answer that carries it: the code of
|
|
133
|
+
* the error schema it matches, a readable message, and the encoded error. A
|
|
134
|
+
* failure no schema matches, or one that doesn't encode, is a defect.
|
|
135
|
+
*/
|
|
136
|
+
const encodeFailure = (schemas: ReadonlyArray<Schema.Top>, error: unknown) =>
|
|
137
|
+
Effect.gen(function* () {
|
|
138
|
+
for (const schema of schemas) {
|
|
139
|
+
if (!Schema.is(schema as Schema.Codec<unknown>)(error)) continue;
|
|
140
|
+
const code = getErrorCode(schema);
|
|
141
|
+
const details = yield* Effect.orDie(Schema.encodeUnknownEffect(codec(schema))(error));
|
|
142
|
+
const fields = error as { readonly message?: unknown; readonly _tag?: unknown };
|
|
143
|
+
const message =
|
|
144
|
+
typeof fields.message === "string" && fields.message !== ""
|
|
145
|
+
? fields.message
|
|
146
|
+
: typeof fields._tag === "string"
|
|
147
|
+
? fields._tag
|
|
148
|
+
: code;
|
|
149
|
+
return new CallableError.ResponseError({ code, message, details });
|
|
150
|
+
}
|
|
151
|
+
return yield* Effect.die(error);
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* The failure of a raw Payload that doesn't decode, as it leaves the chain.
|
|
156
|
+
* Middleware sees it like any other failure.
|
|
157
|
+
*/
|
|
158
|
+
class PayloadDecodeError {
|
|
159
|
+
constructor(readonly cause: Schema.SchemaError) {}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* The middleware chain of `callable` in `group`, or in no group, around the
|
|
164
|
+
* Payload's decoding and the handler. `serve`, which `Functions` runs,
|
|
165
|
+
* and `handler` both run this one chain.
|
|
166
|
+
*
|
|
167
|
+
* The middleware implementations are read from the context once. The context
|
|
168
|
+
* the chain was read in sits under that of each call, and the call's request
|
|
169
|
+
* on top of both.
|
|
170
|
+
*
|
|
171
|
+
* @internal
|
|
172
|
+
*/
|
|
173
|
+
export const chain = (group: FirebaseFunction.Group | undefined, callable: Fields) =>
|
|
174
|
+
Effect.gen(function* () {
|
|
175
|
+
const handler = yield* Effect.service(handlerKey(group?.identifier, callable.name));
|
|
176
|
+
const context = yield* Effect.context<never>();
|
|
177
|
+
// Innermost first: a middleware attached later wraps the ones before it.
|
|
178
|
+
const implementations = [...callable.middlewares].map((key) => Context.getUnsafe(context, key));
|
|
179
|
+
const decode = Schema.decodeUnknownEffect(codec(callable.payload));
|
|
180
|
+
return (request: CallableRequest.CallableRequest): Effect.Effect<unknown, unknown> => {
|
|
181
|
+
let effect: Effect.Effect<unknown, unknown> = decode(request.data).pipe(
|
|
182
|
+
Effect.mapError((cause) => new PayloadDecodeError(cause)),
|
|
183
|
+
Effect.flatMap(handler),
|
|
184
|
+
);
|
|
185
|
+
for (const implementation of implementations) {
|
|
186
|
+
effect = implementation(effect, { callable });
|
|
187
|
+
}
|
|
188
|
+
return effect.pipe(
|
|
189
|
+
Effect.provideService(callableRequest.CallableRequest, request),
|
|
190
|
+
Effect.updateContext((call: Context.Context<never>) => Context.merge(context, call)),
|
|
191
|
+
);
|
|
192
|
+
};
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
/** @internal */
|
|
196
|
+
export const serve = (group: FirebaseFunction.Group | undefined, callable: Fields) =>
|
|
197
|
+
Effect.map(chain(group, callable), (run) => {
|
|
198
|
+
const encode = Schema.encodeUnknownEffect(codec(callable.success));
|
|
199
|
+
const schemas = errorSchemas(callable);
|
|
200
|
+
const fail = (reason: CallableError.ResponseError) =>
|
|
201
|
+
new CallableError.CallableError({
|
|
202
|
+
callable: deployedName(group, callable),
|
|
203
|
+
reason,
|
|
204
|
+
});
|
|
205
|
+
return (request: CallableRequest.CallableRequest) =>
|
|
206
|
+
run(request).pipe(
|
|
207
|
+
Effect.catch((error) =>
|
|
208
|
+
error instanceof PayloadDecodeError
|
|
209
|
+
? Effect.logDebug("The Payload of a Callable didn't decode", error.cause).pipe(
|
|
210
|
+
Effect.andThen(
|
|
211
|
+
Effect.fail(
|
|
212
|
+
fail(
|
|
213
|
+
new CallableError.ResponseError({
|
|
214
|
+
code: "invalid-argument",
|
|
215
|
+
message: "The Payload didn't decode",
|
|
216
|
+
details: undefined,
|
|
217
|
+
}),
|
|
218
|
+
),
|
|
219
|
+
),
|
|
220
|
+
),
|
|
221
|
+
)
|
|
222
|
+
: Effect.flatMap(encodeFailure(schemas, error), (reason) => Effect.fail(fail(reason))),
|
|
223
|
+
),
|
|
224
|
+
Effect.flatMap((value) => Effect.orDie(encode(value))),
|
|
225
|
+
);
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* The HTTP request of a call `handler` runs with no request given: a JSON
|
|
230
|
+
* `POST` of the encoded Payload to the deployed name.
|
|
231
|
+
*/
|
|
232
|
+
const syntheticRequest = (name: string, data: unknown) =>
|
|
233
|
+
HttpServerRequest.fromWeb(
|
|
234
|
+
new Request(`http://localhost/${name}`, {
|
|
235
|
+
method: "POST",
|
|
236
|
+
headers: { "content-type": "application/json" },
|
|
237
|
+
body: JSON.stringify(data),
|
|
238
|
+
}),
|
|
239
|
+
);
|
|
240
|
+
|
|
241
|
+
/** @internal */
|
|
242
|
+
export const handler = (group: FirebaseFunction.Group | undefined, callable: Fields) =>
|
|
243
|
+
Effect.map(chain(group, callable), (run) => {
|
|
244
|
+
const encode = Schema.encodeUnknownEffect(codec(callable.payload));
|
|
245
|
+
const name = deployedName(group, callable);
|
|
246
|
+
return (payload: unknown, input: CallableRequest.Input) =>
|
|
247
|
+
encode(payload).pipe(
|
|
248
|
+
Effect.orDie,
|
|
249
|
+
Effect.flatMap((data) =>
|
|
250
|
+
run({
|
|
251
|
+
auth:
|
|
252
|
+
input.auth === undefined
|
|
253
|
+
? undefined
|
|
254
|
+
: {
|
|
255
|
+
uid: input.auth.uid,
|
|
256
|
+
token: input.auth.token,
|
|
257
|
+
rawToken: input.auth.rawToken ?? "",
|
|
258
|
+
},
|
|
259
|
+
app: input.app,
|
|
260
|
+
instanceIdToken: input.instanceIdToken,
|
|
261
|
+
data,
|
|
262
|
+
rawRequest: input.rawRequest ?? syntheticRequest(name, data),
|
|
263
|
+
}),
|
|
264
|
+
),
|
|
265
|
+
// The Payload was encoded from its own type, so a Payload that doesn't
|
|
266
|
+
// decode back is a defect of its schema.
|
|
267
|
+
Effect.catchIf(
|
|
268
|
+
(error): error is PayloadDecodeError => error instanceof PayloadDecodeError,
|
|
269
|
+
(error) => Effect.die(error.cause),
|
|
270
|
+
),
|
|
271
|
+
);
|
|
272
|
+
});
|