@visiolab/effect-firebase 0.0.0-stage → 0.3.1

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 +72 -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,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
+ });