Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions .github/workflows/test_node.yml
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,18 @@ jobs:
- uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0
with:
node-version: ${{ matrix.node-version }}
- uses: actions/setup-java@de7274f081f381c8f8158605e0321c36c376e2e6 # v6.0.1
with:
distribution: temurin
java-version: 21

- name: Cache Firebase Emulators
uses: actions/cache@d4323d4df104b026a6aa633fdb11d772146be0bf # v4.2.2
with:
path: ~/.cache/firebase/emulators
key: ${{ runner.os }}-firebase-emulators-${{ hashFiles('Node/effect-functions/firebase.json') }}

- run: npm install -g firebase-tools

- name: Cache npm
uses: actions/cache@d4323d4df104b026a6aa633fdb11d772146be0bf # v4.2.2
Expand Down
14 changes: 14 additions & 0 deletions Node/effect-functions/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
firebase-debug.log*
firebase-debug.*.log*
firestore-debug.log*

# Firebase cache
.firebase/

node_modules/
71 changes: 71 additions & 0 deletions Node/effect-functions/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
# Effect.ts (v4) with 2nd Gen Cloud Functions

This sample demonstrates how to build production-grade, type-safe Firebase 2nd Gen Cloud Functions using [Effect 4](https://effect.website) (`effect@^4.0.0`).

## What it demonstrates

- **2nd Gen Callable Function (`createTask`)**:
- Request validation and decoding defaults using `Schema.decodeUnknownEffect` and `Schema.withDecodingDefault`.
- Authentication verification and domain error modeling with `Schema.TaggedError` (yielded directly in `Effect.gen` / `Effect.fn` without `Effect.fail`).
- Parameterized configuration via `firebase-functions/params` (`defineInt`) enforcing per-user active task limits.
- Native Firestore auto-IDs (`collection.doc()`) and explicit `Schema.decodeUnknownEffect` validation on repository reads.
- Warm container reuse using a module-scoped `ManagedRuntime`.
- Exhaustive domain error mapping to `HttpsError` status codes via `Cause.findErrorOption`, while logging unexpected defects with `Cause.pretty`.

- **2nd Gen Firestore Trigger (`onTaskWritten`)**:
- Document snapshot parsing with `Schema.decodeUnknownEffect`.
- Traced state machine transition validation (`Effect.fn("validateStatusTransition")` + `InvalidTransitionError`), caught with `Effect.catchTag` and logged as a warning so Eventarc does not retry invalid transitions.
- Atomic idempotent audit log persistence and user stats updates via `WriteBatch.create()` keyed on `audit_logs/{event.id}`, catching `ALREADY_EXISTS` with `Effect.catchIf` so duplicate Eventarc deliveries execute stats mutations at most once without round-trip reads or transactions.
- Authoritative Eventarc CloudEvent timestamps via `event.time`.

- **`Context.Service` & `Layer` Dependency Injection**:
- Firestore repositories (`TaskRepository`, `UserStatsRepository`) defined with `Context.Service` and static `layer` definitions (`Layer.sync`), composed via `Layer.mergeAll`.
- Custom `Logger` layer built on `Logger.formatStructured` and `Logger.layer`, routing `Effect.logInfo`, `Effect.logWarning`, `Effect.annotateLogs`, and `Effect.withLogSpan` directly into `firebase-functions/logger` (`logger.write`) with structured `jsonPayload` attributes.

- **Emulator Testing with Vitest**:
- Integration tests using `firebase-functions-test` and Vitest running against the Firebase Local Emulator Suite.

## Prerequisites

- Node.js 20+
- [Firebase CLI](https://firebase.google.com/docs/cli) installed (`npm install -g firebase-tools`)
- Java Runtime Environment (JRE) for the Firebase Emulator Suite

## Setup

1. Navigate to the functions directory and install dependencies:

```bash
cd functions
npm install
```

2. Build the TypeScript source code:

```bash
npm run build
```

## Running Tests

Run the test suite inside the Firebase Local Emulator Suite (starts the Firestore emulator, runs Vitest, and tears down the emulator automatically):

```bash
npm test
```

## Local Development & Emulators

Start the Firebase Emulator Suite for both Functions and Firestore:

```bash
npm run serve
```

## Deployment

Deploy the functions to your Firebase project:

```bash
npm run deploy
```
24 changes: 24 additions & 0 deletions Node/effect-functions/firebase.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
{
"functions": {
"codebase": "effect-functions",
"source": "functions",
"predeploy": [
"npm --prefix \"$RESOURCE_DIR\" run build"
]
},
"firestore": {
"rules": "firestore.rules",
"indexes": "firestore.indexes.json"
},
"emulators": {
"functions": {
"port": 5001
},
"firestore": {
"port": 8080
},
"ui": {
"enabled": true
}
}
}
4 changes: 4 additions & 0 deletions Node/effect-functions/firestore.indexes.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"indexes": [],
"fieldOverrides": []
}
8 changes: 8 additions & 0 deletions Node/effect-functions/firestore.rules
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /{document=**} {
allow read, write: if request.auth != null;
}
}
}
8 changes: 8 additions & 0 deletions Node/effect-functions/functions/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# Compiled JavaScript files
lib/

# Dependency directory
node_modules/

# Logs
*.log
1 change: 1 addition & 0 deletions Node/effect-functions/functions/.npmrc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
only-built-dependencies = @firebase/util, @parcel/watcher, esbuild, protobufjs, unrs-resolver
29 changes: 29 additions & 0 deletions Node/effect-functions/functions/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
{
"name": "effect-functions",
"description": "An example of Firebase 2nd Gen Cloud Functions integrated with Effect.ts",
"main": "lib/index.js",
"scripts": {
"build": "tsc",
"compile": "cp ../../../tsconfig.template.json ./tsconfig-compile.json && tsc --project tsconfig-compile.json --module NodeNext --moduleResolution NodeNext --allowJs false --checkJs false",
"test": "firebase emulators:exec --project demo-effect-functions --only firestore \"vitest run\"",
"serve": "npm run build && firebase emulators:start --only functions,firestore",
"shell": "npm run build && firebase functions:shell",
"deploy": "firebase deploy --only functions",
"logs": "firebase functions:log"
},
"engines": {
"node": ">=20"
},
"dependencies": {
"effect": "^4.0.0",
"firebase-admin": "^14.2.0",
"firebase-functions": "^7.3.2"
},
"devDependencies": {
"@types/node": "^22.13.0",
"firebase-functions-test": "^3.5.0",
"typescript": "^5.9.0",
"vitest": "^2.1.0"
},
"private": true
}
Loading
Loading