@visiolab/effect-firebase-functions 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 +228 -2
- package/dist/Functions.d.mts +339 -0
- package/dist/Functions.mjs +213 -0
- package/dist/Functions.mjs.map +1 -0
- package/dist/_virtual/_rolldown/runtime.mjs +12 -0
- package/dist/index.d.mts +2 -0
- package/dist/index.mjs +1 -0
- package/dist/internal/callable.mjs +104 -0
- package/dist/internal/callable.mjs.map +1 -0
- package/dist/internal/documentTrigger.mjs +110 -0
- package/dist/internal/documentTrigger.mjs.map +1 -0
- package/dist/internal/kinds.mjs +19 -0
- package/dist/internal/kinds.mjs.map +1 -0
- package/dist/internal/member.mjs +12 -0
- package/dist/internal/member.mjs.map +1 -0
- package/dist/internal/runtime.mjs +79 -0
- package/dist/internal/runtime.mjs.map +1 -0
- package/dist/internal/serverSpan.mjs +187 -0
- package/dist/internal/serverSpan.mjs.map +1 -0
- package/dist/logger/CloudLogging.d.mts +92 -0
- package/dist/logger/CloudLogging.mjs +146 -0
- package/dist/logger/CloudLogging.mjs.map +1 -0
- package/dist/logger/index.d.mts +2 -0
- package/dist/logger/index.mjs +1 -0
- package/package.json +64 -3
- package/src/Functions.ts +482 -0
- package/src/index.ts +8 -0
- package/src/internal/callable.ts +156 -0
- package/src/internal/documentTrigger.ts +188 -0
- package/src/internal/kinds.ts +36 -0
- package/src/internal/member.ts +21 -0
- package/src/internal/runtime.ts +110 -0
- package/src/internal/serverSpan.ts +203 -0
- package/src/logger/CloudLogging.ts +152 -0
- package/src/logger/index.ts +8 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 VisioLab GmbH
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,229 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @visiolab/effect-firebase-functions
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Deploys the Functions of [`@visiolab/effect-firebase`](https://github.com/VisioLab/effect-firebase/tree/main/packages/effect-firebase) as Cloud Functions for Firebase, with `firebase-functions`. A Function is a server function declared once by its name and what runs it. Callables are the kind of Function the client calls, with `CallableClient` from [`@visiolab/effect-firebase-client`](https://github.com/VisioLab/effect-firebase/tree/main/packages/client).
|
|
4
|
+
|
|
5
|
+
```sh
|
|
6
|
+
pnpm add effect @visiolab/effect-firebase @visiolab/effect-firebase-functions firebase-functions
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
## Callable Functions
|
|
10
|
+
|
|
11
|
+
Declare a Callable once, in code the server and the client share:
|
|
12
|
+
|
|
13
|
+
```ts
|
|
14
|
+
import { Schema } from "effect";
|
|
15
|
+
import { Callable } from "@visiolab/effect-firebase/functions";
|
|
16
|
+
|
|
17
|
+
export const Greet = Callable.make("greet", {
|
|
18
|
+
payload: { name: Schema.String },
|
|
19
|
+
success: Schema.String,
|
|
20
|
+
});
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Implement it as a Layer, and export it with `Functions.make`:
|
|
24
|
+
|
|
25
|
+
```ts
|
|
26
|
+
import { Effect } from "effect";
|
|
27
|
+
import { Callable } from "@visiolab/effect-firebase/functions";
|
|
28
|
+
import { Functions } from "@visiolab/effect-firebase-functions";
|
|
29
|
+
import { Greet } from "./contract.ts";
|
|
30
|
+
|
|
31
|
+
const GreetLive = Callable.toLayer(Greet, ({ name }) => Effect.succeed(`Hello, ${name}!`));
|
|
32
|
+
|
|
33
|
+
export const { greet } = Functions.make(Greet, GreetLive, { region: "europe-west1" });
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
`Functions.make` dispatches on the Function's kind. For a Callable, the export is exactly what `onCall` returns, so plain `firebase deploy` deploys it, and the third argument is Firebase's `CallableOptions`, passed through unchanged.
|
|
37
|
+
|
|
38
|
+
- The Layer is built on the first call, not at deploy or when the module loads. Every Layer is built through one memo map per process, so concurrent first calls share one build.
|
|
39
|
+
- `minInstances` keeps an instance alive, but its Layer is still built on the first call.
|
|
40
|
+
- Finalizers of the Layer don't run when the instance shuts down.
|
|
41
|
+
- Give each export a Layer of its own where you can, so a cold instance builds only what its function needs.
|
|
42
|
+
- Call `setGlobalOptions` before any function is defined, as Firebase requires.
|
|
43
|
+
|
|
44
|
+
## Document Triggers
|
|
45
|
+
|
|
46
|
+
A Document Trigger runs when a document of a collection changes. Declare it from a name and a `Collection` definition, implement it with `DocumentTrigger.toLayer`, and deploy it with `Functions.make`:
|
|
47
|
+
|
|
48
|
+
```ts
|
|
49
|
+
import { Effect } from "effect";
|
|
50
|
+
import { DocumentTrigger } from "@visiolab/effect-firebase/functions";
|
|
51
|
+
import { Functions } from "@visiolab/effect-firebase-functions";
|
|
52
|
+
import { Posts } from "./collections.ts";
|
|
53
|
+
|
|
54
|
+
const OnPostCreated = DocumentTrigger.created("onPostCreated", Posts);
|
|
55
|
+
|
|
56
|
+
export const { onPostCreated } = Functions.make(
|
|
57
|
+
OnPostCreated,
|
|
58
|
+
DocumentTrigger.toLayer(OnPostCreated, (event) =>
|
|
59
|
+
Effect.gen(function* () {
|
|
60
|
+
const [authorId] = event.ref.ancestors;
|
|
61
|
+
const post = yield* event.snapshot.data;
|
|
62
|
+
yield* Effect.log(`${authorId} published ${post.title}`);
|
|
63
|
+
}),
|
|
64
|
+
),
|
|
65
|
+
{ retry: true },
|
|
66
|
+
);
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
- There is one constructor per change: `created` and `deleted` hand over `event.snapshot` (on `deleted`, the document as it was just before), `updated` hands over `event.before` and `event.after`, both existing, and `written` runs on all three, with `before` missing on a create and `after` missing on a delete. Narrow a `written` side with `DocumentSnapshot.isExisting` or `isMissing`.
|
|
70
|
+
- The trigger listens to the `(default)` database, at a path pattern derived from the collection: one wildcard per parent document and one for the document. The ids are on `event.ref`, typed.
|
|
71
|
+
- `event.snapshot.data` decodes through the collection's model when it runs; `event.snapshot.raw` holds the stored Firestore Values. The same holds for each existing side of a change.
|
|
72
|
+
- The options are the shared `firebase-functions` options plus `retry`.
|
|
73
|
+
- Any failure fails the event: a typed error, a defect, an interrupt, or a Layer that fails to build. Its Cause is logged at Error in the event's span, and the function throws. Firebase then delivers the event again when `retry` is set, and drops it otherwise. The library adds no retry of its own. Events are delivered at least once: key idempotent work on `event.id`.
|
|
74
|
+
- A Layer that fails to build is built again on the next event.
|
|
75
|
+
- The Functions emulator never delivers a failed event again, even with `retry`, and replaces the worker whose function threw.
|
|
76
|
+
- Add a trigger to a Function Group next to Callables to deploy it as `<group>-<name>` (see [Function Groups](#function-groups)).
|
|
77
|
+
- Test a handler in process with `DocumentTrigger.serve`, and an event built with `DocumentTrigger.createdEvent`, `updatedEvent`, `deletedEvent` or `writtenEvent` from `DocumentSnapshot.existing` and `DocumentSnapshot.missing`.
|
|
78
|
+
- The package takes `@visiolab/effect-firebase-admin` and `firebase-admin` as peer dependencies, to read event data as the Admin SDK reads do.
|
|
79
|
+
|
|
80
|
+
## Function Groups
|
|
81
|
+
|
|
82
|
+
Collect related Functions, of any kind, into a Function Group. Each member deploys under the group's name as a prefix:
|
|
83
|
+
|
|
84
|
+
```ts
|
|
85
|
+
import { Effect, Layer, Schema } from "effect";
|
|
86
|
+
import { Callable, FunctionGroup } from "@visiolab/effect-firebase/functions";
|
|
87
|
+
import { Functions } from "@visiolab/effect-firebase-functions";
|
|
88
|
+
|
|
89
|
+
const CreateUser = Callable.make("create", {
|
|
90
|
+
payload: { name: Schema.String },
|
|
91
|
+
success: Schema.String,
|
|
92
|
+
});
|
|
93
|
+
const DeleteUser = Callable.make("delete", { payload: { id: Schema.String } });
|
|
94
|
+
|
|
95
|
+
class Users extends FunctionGroup.make("users").add(CreateUser, DeleteUser) {}
|
|
96
|
+
|
|
97
|
+
const UsersLive = Layer.mergeAll(
|
|
98
|
+
Callable.toLayer(Users, "create", ({ name }) => Effect.succeed(name)),
|
|
99
|
+
Callable.toLayer(Users, "delete", () => Effect.void),
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
// Deploys `users-create` and `users-delete`.
|
|
103
|
+
export const { users } = Functions.group(Users, UsersLive, {
|
|
104
|
+
delete: { region: "europe-west1" },
|
|
105
|
+
});
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
A group can hold Callables and Document Triggers together, so you can group by domain rather than by kind:
|
|
109
|
+
|
|
110
|
+
```ts
|
|
111
|
+
import { Effect, Layer, Schema } from "effect";
|
|
112
|
+
import { Collection, FirestoreModel } from "@visiolab/effect-firebase/firestore";
|
|
113
|
+
import { Callable, DocumentTrigger, FunctionGroup } from "@visiolab/effect-firebase/functions";
|
|
114
|
+
import { Functions } from "@visiolab/effect-firebase-functions";
|
|
115
|
+
|
|
116
|
+
class Post extends FirestoreModel.Class<Post>("Post")({ title: Schema.String }) {}
|
|
117
|
+
|
|
118
|
+
const PostsCollection = Collection.make("posts", Post);
|
|
119
|
+
|
|
120
|
+
const Publish = Callable.make("publish", { payload: { id: Schema.String } });
|
|
121
|
+
const OnCreated = DocumentTrigger.created("onCreated", PostsCollection);
|
|
122
|
+
|
|
123
|
+
class Posts extends FunctionGroup.make("posts").add(Publish, OnCreated) {}
|
|
124
|
+
|
|
125
|
+
// Deploys `posts-publish` and `posts-onCreated`.
|
|
126
|
+
export const { posts } = Functions.group(
|
|
127
|
+
Posts,
|
|
128
|
+
Layer.mergeAll(
|
|
129
|
+
Callable.toLayer(Posts, "publish", () => Effect.void),
|
|
130
|
+
DocumentTrigger.toLayer(Posts, "onCreated", (event) => Effect.log(event.ref.id)),
|
|
131
|
+
),
|
|
132
|
+
{ publish: { cors: true }, onCreated: { retry: true } },
|
|
133
|
+
);
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
- Implement a member with `Callable.toLayer(Users, "create", handler)` or `DocumentTrigger.toLayer(Posts, "onCreated", handler)`. A Layer from the Function on its own doesn't serve it in the group.
|
|
137
|
+
- The options are a sparse map keyed by member name, each typed by the member's kind: `CallableOptions` for a Callable, and the shared options plus `retry` for a Document Trigger. The group has none of its own.
|
|
138
|
+
- A Function can be in more than one group. Each group serves and implements it separately.
|
|
139
|
+
- Names must be unique within a group. `.add` throws when a name is taken, so the clash fails when the module loads. The types don't catch it.
|
|
140
|
+
- Build the client from the group, `CallableClient.make(Users)`. It has a function for each of the group's Callables, and none for its Document Triggers. A client built from the member on its own calls the unprefixed name and gets `not-found`.
|
|
141
|
+
|
|
142
|
+
## Middleware and the Caller
|
|
143
|
+
|
|
144
|
+
Requirements such as "the caller is signed in" are Callable Middleware on the contract. The built-in `Caller.Authenticated` provides the `Caller` and fails a signed-out call with `CallableError.Unauthenticated`, before the Payload is decoded:
|
|
145
|
+
|
|
146
|
+
```ts
|
|
147
|
+
import { Effect, Layer, Schema } from "effect";
|
|
148
|
+
import { Callable, Caller } from "@visiolab/effect-firebase/functions";
|
|
149
|
+
import { Functions } from "@visiolab/effect-firebase-functions";
|
|
150
|
+
|
|
151
|
+
const WhoAmI = Callable.make("whoAmI", { success: Schema.String }).middleware(Caller.Authenticated);
|
|
152
|
+
|
|
153
|
+
const WhoAmILive = Callable.toLayer(WhoAmI, () =>
|
|
154
|
+
Effect.map(Effect.service(Caller.Caller), ({ uid }) => uid),
|
|
155
|
+
);
|
|
156
|
+
|
|
157
|
+
// `Live` provides the handler and every middleware implementation.
|
|
158
|
+
export const { whoAmI } = Functions.make(
|
|
159
|
+
WhoAmI,
|
|
160
|
+
Layer.merge(WhoAmILive, Caller.Authenticated.layer),
|
|
161
|
+
);
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
- Declare a middleware of your own with `CallableMiddleware.Service`. Its implementation wraps the call and reads the portable `CallableRequest`: `auth`, `app`, the raw `data`, and `rawRequest`, the HTTP request as Effect's `HttpServerRequest`, with the caller's IP address as `remoteAddress`. Every call provides it, so the Layer you serve needn't, even when a handler or middleware reads it. Its body streams Firebase's buffered bytes without copying them.
|
|
165
|
+
- The Firebase web SDK's `httpsCallable` can't send custom headers. A header of your own reaches `rawRequest` only from callers that don't use it, such as another server. Headers like `user-agent` and `accept-language` are there for every caller.
|
|
166
|
+
- A middleware attached later wraps the ones attached before it.
|
|
167
|
+
- The Payload is decoded inside the chain, so a middleware that catches every failure also catches a Payload that doesn't decode. The caller then no longer gets `invalid-argument`. Catch only the errors you mean to.
|
|
168
|
+
- `Functions` provides no middleware itself, so a `Live` without an implementation is a type error.
|
|
169
|
+
- Run a handler in process, with its chain and without the emulator, through `Callable.handler(WhoAmI)`. Pass a `rawRequest` built with `HttpServerRequest.fromWeb` to test a middleware that reads headers or the address; left out, it is a JSON `POST` of the Payload to `http://localhost/<deployed name>`.
|
|
170
|
+
|
|
171
|
+
### Middleware in a group
|
|
172
|
+
|
|
173
|
+
A Function Group carries no middleware. Attach a middleware to each member that needs it:
|
|
174
|
+
|
|
175
|
+
```ts
|
|
176
|
+
import { Effect, Layer, Schema } from "effect";
|
|
177
|
+
import { Callable, FunctionGroup, Caller } from "@visiolab/effect-firebase/functions";
|
|
178
|
+
import { Functions } from "@visiolab/effect-firebase-functions";
|
|
179
|
+
|
|
180
|
+
const Me = Callable.make("me", { success: Schema.String }).middleware(Caller.Authenticated);
|
|
181
|
+
|
|
182
|
+
class Accounts extends FunctionGroup.make("accounts").add(Me) {}
|
|
183
|
+
|
|
184
|
+
export const { accounts } = Functions.group(
|
|
185
|
+
Accounts,
|
|
186
|
+
Layer.merge(
|
|
187
|
+
Callable.toLayer(Accounts, "me", () =>
|
|
188
|
+
Effect.map(Effect.service(Caller.Caller), ({ uid }) => uid),
|
|
189
|
+
),
|
|
190
|
+
Caller.Authenticated.layer,
|
|
191
|
+
),
|
|
192
|
+
);
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
## Tracing
|
|
196
|
+
|
|
197
|
+
Every call runs in a `kind: "server"` span named `CallableFunction.<deployed name>`, such as `CallableFunction.users-create`, with the attributes `rpc.system: "firebase_callable"`, `rpc.method`, `rpc.service` (the group, for a member) and `faas.name`. Spans are exported only when the Layer you export provides a `Tracer`, for example from `@effect/opentelemetry`:
|
|
198
|
+
|
|
199
|
+
```ts
|
|
200
|
+
export const { users } = Functions.group(Users, Layer.mergeAll(UsersLive, TracingLive));
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
- The span covers the middleware, decoding the Payload, the handler and encoding the answer. On the call that builds the Layer, it covers the build too, and spans opened during the build are its children.
|
|
204
|
+
- Its parent is the trace context Firebase reads from the request (`X-Cloud-Trace-Context`, else `traceparent`), so it joins the platform's trace. In the emulator it is a root span.
|
|
205
|
+
- `CallableClient` opens a `kind: "client"` span per call. It isn't linked to the server span: the two land in separate traces.
|
|
206
|
+
- Neither span carries a uid. Add one where you want it with `Effect.annotateCurrentSpan("enduser.id", uid)`.
|
|
207
|
+
- Every event of a Document Trigger runs in a `kind: "server"` span named `DocumentTrigger.<deployed name>`, such as `DocumentTrigger.onPostCreated`, with the attributes `faas.name` and `faas.trigger: "datasource"`. It covers converting the event, the handler and, on the event that builds the Layer, the build. Its parent is the trace context Firebase reads from the event's `traceparent`.
|
|
208
|
+
|
|
209
|
+
## Logging
|
|
210
|
+
|
|
211
|
+
Every call and every Document Trigger event logs through `CloudLogging.layer`, which writes each Effect log as one Cloud Logging JSON line, in the format of Firebase's own `logger`:
|
|
212
|
+
|
|
213
|
+
- The trace, span id and sampled flag come from the current span, so concurrent calls and events on one instance each log under their own trace.
|
|
214
|
+
- Trace and Debug are `DEBUG`, Info `INFO`, Warn `WARNING`, Error `ERROR` and Fatal `CRITICAL`. Error and Fatal go to `console.error`, the rest to `console.log`.
|
|
215
|
+
- Log annotations become fields of the entry, other logged values go under `args` and log spans under `spans`.
|
|
216
|
+
- A Cause is appended to `message` with its stacks, so defects and failed Layer builds reach Error Reporting.
|
|
217
|
+
- Entries carry no uid. Add one where you want it with `Effect.annotateLogs({ uid })`.
|
|
218
|
+
- The project comes from `GCLOUD_PROJECT`, else `GOOGLE_CLOUD_PROJECT`. With neither, entries carry no trace fields.
|
|
219
|
+
- Logs written while the first call builds the Layer are held until the build is done, so they get the spans' final ids. Entries carry no timestamp of their own, so Cloud Logging stamps them with the time the build finished.
|
|
220
|
+
- Provide a `Logger.layer` in the Layer you export to log differently.
|
|
221
|
+
|
|
222
|
+
To log the same way outside a deployed Function, provide the layer yourself:
|
|
223
|
+
|
|
224
|
+
```ts
|
|
225
|
+
import { Effect } from "effect";
|
|
226
|
+
import { CloudLogging } from "@visiolab/effect-firebase-functions/logger";
|
|
227
|
+
|
|
228
|
+
const job = Effect.log("Cleaning up").pipe(Effect.provide(CloudLogging.layer));
|
|
229
|
+
```
|
|
@@ -0,0 +1,339 @@
|
|
|
1
|
+
import { Layer } from "effect";
|
|
2
|
+
import * as Callable from "@visiolab/effect-firebase/functions/Callable";
|
|
3
|
+
import * as Https from "firebase-functions/v2/https";
|
|
4
|
+
import { CloudFunction } from "firebase-functions/v2";
|
|
5
|
+
import * as DocumentTrigger from "@visiolab/effect-firebase/functions/DocumentTrigger";
|
|
6
|
+
import * as Firestore from "firebase-functions/v2/firestore";
|
|
7
|
+
import * as FirebaseFunction from "@visiolab/effect-firebase/functions/FirebaseFunction";
|
|
8
|
+
import * as FunctionGroup from "@visiolab/effect-firebase/functions/FunctionGroup";
|
|
9
|
+
declare namespace Functions_d_exports {
|
|
10
|
+
export { CallableExport, DocumentTriggerExport, DocumentTriggerOptions, Export, Exports, GroupExports, GroupOptions, GroupServices, Options, Services, group, make };
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* The value `onCall` returns for a Callable: what firebase-tools discovers and
|
|
14
|
+
* deploys.
|
|
15
|
+
*
|
|
16
|
+
* @see {@link Export} for the export of a Function of any kind
|
|
17
|
+
*
|
|
18
|
+
* @stability unstable
|
|
19
|
+
* @category models
|
|
20
|
+
* @since 0.3.0
|
|
21
|
+
*/
|
|
22
|
+
export type CallableExport = Https.CallableFunction<unknown, Promise<unknown>>;
|
|
23
|
+
/**
|
|
24
|
+
* The value an `onDocument*` provider returns for a Document Trigger, by the
|
|
25
|
+
* change it runs on: what firebase-tools discovers and deploys.
|
|
26
|
+
*
|
|
27
|
+
* **Details**
|
|
28
|
+
*
|
|
29
|
+
* A `created` trigger is exported with `onDocumentCreated`, a `deleted` one
|
|
30
|
+
* with `onDocumentDeleted`, an `updated` one with `onDocumentUpdated` and a
|
|
31
|
+
* `written` one with `onDocumentWritten`. The type is the `CloudFunction` of
|
|
32
|
+
* the event that provider hands over.
|
|
33
|
+
*
|
|
34
|
+
* @see {@link Export} for the export of a Function of any kind
|
|
35
|
+
*
|
|
36
|
+
* @stability unstable
|
|
37
|
+
* @category type-level
|
|
38
|
+
* @since 0.3.0
|
|
39
|
+
*/
|
|
40
|
+
export type DocumentTriggerExport<T extends DocumentTrigger.Any> = CloudFunction<Firestore.FirestoreEvent<DocumentTriggerEventData[T["event"]]>>;
|
|
41
|
+
/**
|
|
42
|
+
* The data of the event each `onDocument*` provider hands over, by the change
|
|
43
|
+
* it runs on.
|
|
44
|
+
*/
|
|
45
|
+
interface DocumentTriggerEventData {
|
|
46
|
+
readonly created: Firestore.QueryDocumentSnapshot | undefined;
|
|
47
|
+
readonly deleted: Firestore.QueryDocumentSnapshot | undefined;
|
|
48
|
+
readonly updated: Firestore.Change<Firestore.QueryDocumentSnapshot> | undefined;
|
|
49
|
+
readonly written: Firestore.Change<Firestore.DocumentSnapshot> | undefined;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* The deploy options of a Document Trigger: Firebase's `DocumentOptions`
|
|
53
|
+
* without what the library derives from the trigger. That leaves the options
|
|
54
|
+
* every Function shares, such as `region`, `memory` and `timeoutSeconds`,
|
|
55
|
+
* plus `retry`.
|
|
56
|
+
*
|
|
57
|
+
* **Details**
|
|
58
|
+
*
|
|
59
|
+
* The `document` path pattern comes from the trigger's collection, and the
|
|
60
|
+
* database is always `(default)`, the one `Firestore` reads and writes, so
|
|
61
|
+
* `document`, `database` and `namespace` are left out, as are the event
|
|
62
|
+
* filter options.
|
|
63
|
+
*
|
|
64
|
+
* With `retry: true`, Firebase delivers a failed event again until it
|
|
65
|
+
* succeeds or its retry window ends. Without it, a failed event is dropped.
|
|
66
|
+
* The library adds no retry of its own.
|
|
67
|
+
*
|
|
68
|
+
* @stability unstable
|
|
69
|
+
* @category models
|
|
70
|
+
* @since 0.3.0
|
|
71
|
+
*/
|
|
72
|
+
export type DocumentTriggerOptions = Omit<Firestore.DocumentOptions, "document" | "database" | "namespace" | "eventType" | "eventFilters" | "eventFilterPathPatterns" | "channel">;
|
|
73
|
+
/**
|
|
74
|
+
* The export of a Function, by its kind: a {@link CallableExport} for a
|
|
75
|
+
* Callable, a {@link DocumentTriggerExport} for a Document Trigger.
|
|
76
|
+
*
|
|
77
|
+
* @stability unstable
|
|
78
|
+
* @category type-level
|
|
79
|
+
* @since 0.3.0
|
|
80
|
+
*/
|
|
81
|
+
export type Export<F extends FirebaseFunction.Any> = F extends Callable.Any ? CallableExport : F extends DocumentTrigger.Any ? DocumentTriggerExport<F> : never;
|
|
82
|
+
/**
|
|
83
|
+
* The deploy options of a Function, by its kind: Firebase's `CallableOptions`
|
|
84
|
+
* for a Callable, passed to `onCall` unchanged, and
|
|
85
|
+
* {@link DocumentTriggerOptions} for a Document Trigger.
|
|
86
|
+
*
|
|
87
|
+
* @stability unstable
|
|
88
|
+
* @category type-level
|
|
89
|
+
* @since 0.3.0
|
|
90
|
+
*/
|
|
91
|
+
export type Options<F extends FirebaseFunction.Any> = F extends Callable.Any ? Https.CallableOptions : F extends DocumentTrigger.Any ? DocumentTriggerOptions : never;
|
|
92
|
+
/**
|
|
93
|
+
* What serving a Function under the handler key `H` requires, by its kind.
|
|
94
|
+
* For a Callable: its handler, the implementation of each of its middleware,
|
|
95
|
+
* and what its middleware chain still requires. For a Document Trigger: its
|
|
96
|
+
* handler.
|
|
97
|
+
*
|
|
98
|
+
* @stability unstable
|
|
99
|
+
* @category type-level
|
|
100
|
+
* @since 0.3.0
|
|
101
|
+
*/
|
|
102
|
+
export type Services<F extends FirebaseFunction.Any, H> = F extends Callable.Any ? Callable.ChainServices<F, H> : F extends DocumentTrigger.Any ? H : never;
|
|
103
|
+
/**
|
|
104
|
+
* What serving every member of a {@link FunctionGroup.FunctionGroup}
|
|
105
|
+
* requires: what each member requires, by its kind, under its handler key in
|
|
106
|
+
* the group.
|
|
107
|
+
*
|
|
108
|
+
* @stability unstable
|
|
109
|
+
* @category type-level
|
|
110
|
+
* @since 0.3.0
|
|
111
|
+
*/
|
|
112
|
+
export type GroupServices<G extends FunctionGroup.Any> = FunctionGroup.Names<G> extends (infer Name) ? Name extends string ? Services<FunctionGroup.Member<G, Name>, FirebaseFunction.Handler<G["identifier"], Name>> : never : never;
|
|
113
|
+
/**
|
|
114
|
+
* The exports of {@link make}: one export under the Function's name.
|
|
115
|
+
*
|
|
116
|
+
* @stability unstable
|
|
117
|
+
* @category models
|
|
118
|
+
* @since 0.3.0
|
|
119
|
+
*/
|
|
120
|
+
export type Exports<F extends FirebaseFunction.Any> = { readonly [K in F["name"]]: Export<F>; };
|
|
121
|
+
/**
|
|
122
|
+
* The exports of {@link group}: one export per member, keyed by the member's
|
|
123
|
+
* name, under the group's name.
|
|
124
|
+
*
|
|
125
|
+
* @stability unstable
|
|
126
|
+
* @category models
|
|
127
|
+
* @since 0.3.0
|
|
128
|
+
*/
|
|
129
|
+
export type GroupExports<G extends FunctionGroup.Any> = { readonly [K in G["identifier"]]: { readonly [N in FunctionGroup.Names<G>]: Export<FunctionGroup.Member<G, N>>; }; };
|
|
130
|
+
/**
|
|
131
|
+
* The options of {@link group}: each member's {@link Options}, typed by its
|
|
132
|
+
* kind and keyed by its name. Members left out get none.
|
|
133
|
+
*
|
|
134
|
+
* @stability unstable
|
|
135
|
+
* @category models
|
|
136
|
+
* @since 0.3.0
|
|
137
|
+
*/
|
|
138
|
+
export type GroupOptions<G extends FunctionGroup.Any> = { readonly [N in FunctionGroup.Names<G>]?: Options<FunctionGroup.Member<G, N>>; };
|
|
139
|
+
/**
|
|
140
|
+
* Deploys a Function in no group, whatever its kind.
|
|
141
|
+
*
|
|
142
|
+
* **When to use**
|
|
143
|
+
*
|
|
144
|
+
* Use to export a Function in no group. Destructure the result into the
|
|
145
|
+
* module's exports, so the export's name is the Function's.
|
|
146
|
+
*
|
|
147
|
+
* **Details**
|
|
148
|
+
*
|
|
149
|
+
* `live` must provide what the Function's kind needs and leave nothing
|
|
150
|
+
* required. It may provide more. Give each export a Layer of its own where you
|
|
151
|
+
* can: a cold instance then builds only what its own function needs.
|
|
152
|
+
*
|
|
153
|
+
* `options` are typed by the Function's kind (see {@link Options}).
|
|
154
|
+
*
|
|
155
|
+
* For a Callable, `live` provides its handler, from `Callable.toLayer`, and
|
|
156
|
+
* the implementation of each of its middleware, such as
|
|
157
|
+
* `Caller.Authenticated.layer`. `Functions` provides no middleware itself.
|
|
158
|
+
* `options` is Firebase's `CallableOptions`, and the export is exactly what
|
|
159
|
+
* `onCall` returns, so its `__endpoint` and `run` work as for any `onCall`
|
|
160
|
+
* function.
|
|
161
|
+
*
|
|
162
|
+
* For a Document Trigger, `live` provides its handler, from
|
|
163
|
+
* `DocumentTrigger.toLayer`. `options` are {@link DocumentTriggerOptions}:
|
|
164
|
+
* the shared options plus `retry`. The export is what the provider of the
|
|
165
|
+
* trigger's change (`onDocumentCreated`, `onDocumentUpdated`,
|
|
166
|
+
* `onDocumentDeleted` or `onDocumentWritten`) returns for the `(default)`
|
|
167
|
+
* database and the path pattern of the trigger's collection. Each event is
|
|
168
|
+
* converted into the trigger's event, its data read into Firestore Values as
|
|
169
|
+
* a read does, a side that doesn't exist as a `Missing` snapshot, and run
|
|
170
|
+
* through `DocumentTrigger.serve`, in a `kind: "server"` span named
|
|
171
|
+
* `DocumentTrigger.<deployed name>` with `faas.name` and
|
|
172
|
+
* `faas.trigger: "datasource"`, logging through `CloudLogging.layer`. Any
|
|
173
|
+
* failure, whether a typed error, a defect, an interrupt or a Layer that
|
|
174
|
+
* fails to build, is logged at Error in that span and makes the function
|
|
175
|
+
* throw, so Firebase delivers the event again when `retry` is set. A Layer
|
|
176
|
+
* that failed to build is built again on the next event.
|
|
177
|
+
*
|
|
178
|
+
* Each call runs the Callable's middleware chain, the one `Callable.handler`
|
|
179
|
+
* runs, with Firebase's request provided as the portable `CallableRequest`.
|
|
180
|
+
* Its `rawRequest` is Firebase's Express request as Effect's
|
|
181
|
+
* `HttpServerRequest`, with the caller's IP address as `remoteAddress`. Its
|
|
182
|
+
* body streams Firebase's buffered bytes without copying them. The Payload is
|
|
183
|
+
* decoded inside the chain, after the middleware.
|
|
184
|
+
*
|
|
185
|
+
* A failed call answers with an `HttpsError`:
|
|
186
|
+
*
|
|
187
|
+
* - A declared error, or an error of a middleware, is sent with its schema's
|
|
188
|
+
* code, the error's `message` (else its `_tag`) and the error encoded with
|
|
189
|
+
* `Schema.toCodecJson` as `details`, so the client decodes it back into its
|
|
190
|
+
* class. A middleware that rejects the call answers before the Payload is
|
|
191
|
+
* decoded.
|
|
192
|
+
* - A Payload that doesn't decode is answered with `invalid-argument`, a
|
|
193
|
+
* generic message and no details. It is logged at Debug.
|
|
194
|
+
* - A defect, or a success or error value that doesn't encode, is answered
|
|
195
|
+
* with a bare `internal`. Its Cause is logged at Error, with the logger of
|
|
196
|
+
* `live` if it provides one.
|
|
197
|
+
* - A Layer that fails to build answers the call with a bare `internal` and
|
|
198
|
+
* is logged at Error. The next call builds it again.
|
|
199
|
+
* - An interrupt of the handler is answered with `unavailable`. When the
|
|
200
|
+
* client disconnects, the handler is interrupted and the call is answered
|
|
201
|
+
* with `cancelled`, as far as the platform reports the disconnect.
|
|
202
|
+
*
|
|
203
|
+
* @example Exporting a Callable
|
|
204
|
+
* ```ts
|
|
205
|
+
* import { Effect } from "effect";
|
|
206
|
+
* import { Callable } from "@visiolab/effect-firebase/functions";
|
|
207
|
+
* import { Functions } from "@visiolab/effect-firebase-functions";
|
|
208
|
+
*
|
|
209
|
+
* const Ping = Callable.make("ping");
|
|
210
|
+
*
|
|
211
|
+
* export const { ping } = Functions.make(Ping, Callable.toLayer(Ping, () => Effect.void));
|
|
212
|
+
* ```
|
|
213
|
+
*
|
|
214
|
+
* @example Exporting a Document Trigger
|
|
215
|
+
* ```ts
|
|
216
|
+
* import { Effect, Schema } from "effect";
|
|
217
|
+
* import { Collection, FirestoreModel } from "@visiolab/effect-firebase/firestore";
|
|
218
|
+
* import { DocumentTrigger } from "@visiolab/effect-firebase/functions";
|
|
219
|
+
* import { Functions } from "@visiolab/effect-firebase-functions";
|
|
220
|
+
*
|
|
221
|
+
* class Post extends FirestoreModel.Class<Post>("Post")({ title: Schema.String }) {}
|
|
222
|
+
*
|
|
223
|
+
* const OnPostCreated = DocumentTrigger.created("onPostCreated", Collection.make("posts", Post));
|
|
224
|
+
*
|
|
225
|
+
* export const { onPostCreated } = Functions.make(
|
|
226
|
+
* OnPostCreated,
|
|
227
|
+
* DocumentTrigger.toLayer(OnPostCreated, (event) =>
|
|
228
|
+
* Effect.flatMap(event.snapshot.data, (post) => Effect.log(`New post: ${post.title}`)),
|
|
229
|
+
* ),
|
|
230
|
+
* { retry: true },
|
|
231
|
+
* );
|
|
232
|
+
* ```
|
|
233
|
+
*
|
|
234
|
+
* @see {@link group} for deploying the members of a Function Group
|
|
235
|
+
* @see {@link Callable.serve} for serving a Callable from a transport of your own
|
|
236
|
+
* @see {@link Callable.handler} for running a Callable's handler in process
|
|
237
|
+
* @see {@link DocumentTrigger.serve} for running a Document Trigger's event in process
|
|
238
|
+
* @see {@link CallableClient.make} for calling a Callable from the client
|
|
239
|
+
*
|
|
240
|
+
* @stability unstable
|
|
241
|
+
* @category constructors
|
|
242
|
+
* @since 0.3.0
|
|
243
|
+
*/
|
|
244
|
+
export declare const make: <F extends FirebaseFunction.Any, E>(fn: F, live: Layer.Layer<Services<F, FirebaseFunction.HandlerFor<F>>, E>, options?: Options<F>) => Exports<F>;
|
|
245
|
+
/**
|
|
246
|
+
* Deploys the members of a Function Group, each as a function of its own,
|
|
247
|
+
* whatever their kinds.
|
|
248
|
+
*
|
|
249
|
+
* **When to use**
|
|
250
|
+
*
|
|
251
|
+
* Use to export a Function Group. Destructure the result into the module's
|
|
252
|
+
* exports, so `export const { users } = Functions.group(Users, Live)` deploys
|
|
253
|
+
* `users-create`, `users-delete` and so on.
|
|
254
|
+
*
|
|
255
|
+
* **Details**
|
|
256
|
+
*
|
|
257
|
+
* `live` must provide what every member needs, by its kind, and leave nothing
|
|
258
|
+
* required. For a Callable member, that is its handler, from
|
|
259
|
+
* `Callable.toLayer(Users, name, handler)`, and the implementation of each of
|
|
260
|
+
* its middleware. A handler from `Callable.toLayer` of the Callable on its
|
|
261
|
+
* own doesn't serve it in the group. For a Document Trigger member, that is
|
|
262
|
+
* its handler, from `DocumentTrigger.toLayer(Posts, name, handler)`. `live`
|
|
263
|
+
* may provide more, such as the handlers of other groups.
|
|
264
|
+
*
|
|
265
|
+
* Each member is its own function, with a worker of its own. A worker builds
|
|
266
|
+
* `live` on the first call to its own function, and never for another
|
|
267
|
+
* member's. One `live` per group is fine. Split it when members need very
|
|
268
|
+
* different services, so a cold instance builds only what its function needs.
|
|
269
|
+
*
|
|
270
|
+
* `options` holds each member's {@link Options}, keyed by the member's name
|
|
271
|
+
* and typed by its kind. A Callable member's are Firebase's `CallableOptions`,
|
|
272
|
+
* passed to its `onCall` unchanged. A Document Trigger member's are
|
|
273
|
+
* {@link DocumentTriggerOptions}: the shared options plus `retry`. The group
|
|
274
|
+
* has no options of its own: set those once with `setGlobalOptions`.
|
|
275
|
+
*
|
|
276
|
+
* Each export is what the member's kind deploys, and fails as one from
|
|
277
|
+
* {@link make} does.
|
|
278
|
+
*
|
|
279
|
+
* @example Exporting a Function Group
|
|
280
|
+
* ```ts
|
|
281
|
+
* import { Effect, Layer, Schema } from "effect";
|
|
282
|
+
* import { Callable, FunctionGroup } from "@visiolab/effect-firebase/functions";
|
|
283
|
+
* import { Functions } from "@visiolab/effect-firebase-functions";
|
|
284
|
+
*
|
|
285
|
+
* const CreateUser = Callable.make("create", {
|
|
286
|
+
* payload: { name: Schema.String },
|
|
287
|
+
* success: Schema.String,
|
|
288
|
+
* });
|
|
289
|
+
* const DeleteUser = Callable.make("delete", { payload: { id: Schema.String } });
|
|
290
|
+
*
|
|
291
|
+
* class Users extends FunctionGroup.make("users").add(CreateUser, DeleteUser) {}
|
|
292
|
+
*
|
|
293
|
+
* const UsersLive = Layer.mergeAll(
|
|
294
|
+
* Callable.toLayer(Users, "create", ({ name }) => Effect.succeed(name)),
|
|
295
|
+
* Callable.toLayer(Users, "delete", () => Effect.void),
|
|
296
|
+
* );
|
|
297
|
+
*
|
|
298
|
+
* // Deploys `users-create` and `users-delete`.
|
|
299
|
+
* export const { users } = Functions.group(Users, UsersLive, {
|
|
300
|
+
* delete: { region: "europe-west1" },
|
|
301
|
+
* });
|
|
302
|
+
* ```
|
|
303
|
+
*
|
|
304
|
+
* @example Exporting a group of a Callable and a Document Trigger
|
|
305
|
+
* ```ts
|
|
306
|
+
* import { Effect, Layer, Schema } from "effect";
|
|
307
|
+
* import { Collection, FirestoreModel } from "@visiolab/effect-firebase/firestore";
|
|
308
|
+
* import { Callable, DocumentTrigger, FunctionGroup } from "@visiolab/effect-firebase/functions";
|
|
309
|
+
* import { Functions } from "@visiolab/effect-firebase-functions";
|
|
310
|
+
*
|
|
311
|
+
* class Post extends FirestoreModel.Class<Post>("Post")({ title: Schema.String }) {}
|
|
312
|
+
*
|
|
313
|
+
* const Publish = Callable.make("publish", { payload: { id: Schema.String } });
|
|
314
|
+
* const OnCreated = DocumentTrigger.created("onCreated", Collection.make("posts", Post));
|
|
315
|
+
*
|
|
316
|
+
* class Posts extends FunctionGroup.make("posts").add(Publish, OnCreated) {}
|
|
317
|
+
*
|
|
318
|
+
* // Deploys `posts-publish` and `posts-onCreated`.
|
|
319
|
+
* export const { posts } = Functions.group(
|
|
320
|
+
* Posts,
|
|
321
|
+
* Layer.mergeAll(
|
|
322
|
+
* Callable.toLayer(Posts, "publish", () => Effect.void),
|
|
323
|
+
* DocumentTrigger.toLayer(Posts, "onCreated", (event) => Effect.log(event.ref.id)),
|
|
324
|
+
* ),
|
|
325
|
+
* { publish: { cors: true }, onCreated: { retry: true } },
|
|
326
|
+
* );
|
|
327
|
+
* ```
|
|
328
|
+
*
|
|
329
|
+
* @see {@link make} for deploying a Function in no group
|
|
330
|
+
* @see {@link FunctionGroup.make} for declaring the group
|
|
331
|
+
* @see {@link CallableClient.make} for calling the group's Callables from the client
|
|
332
|
+
*
|
|
333
|
+
* @stability unstable
|
|
334
|
+
* @category constructors
|
|
335
|
+
* @since 0.3.0
|
|
336
|
+
*/
|
|
337
|
+
export declare const group: <G extends FunctionGroup.Any, E>(group: G, live: Layer.Layer<GroupServices<G>, E>, options?: GroupOptions<G>) => GroupExports<G>;
|
|
338
|
+
export { Functions_d_exports };
|
|
339
|
+
//# sourceMappingURL=Functions.d.mts.map
|