Skip to content
Draft
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
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ Firebase integration for [Effect](https://effect.website). Provides schemas, mod
| [effect-firebase](./packages/effect-firebase) | Core schemas, models, and query builder |
| [@effect-firebase/admin](./packages/admin) | Firebase Admin SDK + Cloud Functions |
| [@effect-firebase/client](./packages/client) | Firebase Client SDK |
| [@effect-firebase/genkit](./packages/genkit) | Genkit + `onCallGenkit` bridge |
| [@effect-firebase/mock](./packages/mock) | In-memory mock for testing |

## Installation
Expand All @@ -25,6 +26,7 @@ npm install effect-firebase effect
# Pick one or more SDK packages:
npm install @effect-firebase/admin firebase-admin firebase-functions
npm install @effect-firebase/client firebase
npm install @effect-firebase/genkit genkit firebase-functions
npm install --save-dev @effect-firebase/mock
```

Expand Down
8 changes: 4 additions & 4 deletions packages/admin/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,17 +31,17 @@
"devDependencies": {
"effect": "^4.0.0-beta.70",
"effect-firebase": "workspace:*",
"express": "4.21.2",
"firebase": "^12.10.0",
"firebase-admin": "^13.7.0",
"firebase-functions": "^7.1.0",
"express": "4.21.2"
"firebase-functions": "^7.1.0"
},
"peerDependencies": {
"effect": "^4.0.0-beta.70",
"effect-firebase": "workspace:*",
"express": "^4.0.0",
"firebase-admin": "^13.0.0",
"firebase-functions": "^7.0.0",
"express": "^4.0.0"
"firebase-functions": "^7.0.0"
},
"publishConfig": {
"access": "public",
Expand Down
2 changes: 1 addition & 1 deletion packages/admin/src/lib/functions/on-call.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import {
CallableRequest,
CallableResponse,
} from 'firebase-functions/https';
import { run, Runtime } from './run.js';
import { run, Runtime } from 'effect-firebase';
import { logger } from 'firebase-functions';
import {
CallableContext,
Expand Down
2 changes: 1 addition & 1 deletion packages/admin/src/lib/functions/on-document-created.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import {
} from 'firebase-functions/v2/firestore';
import { CloudFunction } from 'firebase-functions/v2';
import { ParamsOf } from 'firebase-functions';
import { run, Runtime } from './run.js';
import { run, Runtime } from 'effect-firebase';
import { logger } from 'firebase-functions';
import { decodeDocumentData } from './decode-document-data.js';

Expand Down
2 changes: 1 addition & 1 deletion packages/admin/src/lib/functions/on-document-deleted.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import {
} from 'firebase-functions/v2/firestore';
import { CloudFunction } from 'firebase-functions/v2';
import { ParamsOf } from 'firebase-functions';
import { run, Runtime } from './run.js';
import { run, Runtime } from 'effect-firebase';
import { logger } from 'firebase-functions';
import { decodeDocumentData } from './decode-document-data.js';

Expand Down
2 changes: 1 addition & 1 deletion packages/admin/src/lib/functions/on-document-updated.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import {
} from 'firebase-functions/v2/firestore';
import { CloudFunction } from 'firebase-functions/v2';
import { ParamsOf } from 'firebase-functions';
import { run, Runtime } from './run.js';
import { run, Runtime } from 'effect-firebase';
import { logger } from 'firebase-functions';
import { decodeDocumentData } from './decode-document-data.js';

Expand Down
2 changes: 1 addition & 1 deletion packages/admin/src/lib/functions/on-document-written.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import {
} from 'firebase-functions/v2/firestore';
import { CloudFunction } from 'firebase-functions/v2';
import { ParamsOf } from 'firebase-functions';
import { run, Runtime } from './run.js';
import { run, Runtime } from 'effect-firebase';
import { logger } from 'firebase-functions';
import { decodeDocumentData } from './decode-document-data.js';

Expand Down
2 changes: 1 addition & 1 deletion packages/admin/src/lib/functions/on-message-published.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import {
MessagePublishedData,
} from 'firebase-functions/v2/pubsub';
import { CloudEvent, CloudFunction } from 'firebase-functions/v2';
import { run, Runtime } from './run.js';
import { run, Runtime } from 'effect-firebase';
import { logger } from 'firebase-functions';

interface MessagePublishedEffectOptions<R> extends PubSubOptions {
Expand Down
2 changes: 1 addition & 1 deletion packages/admin/src/lib/functions/on-request.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import {
Request,
} from 'firebase-functions/https';
import { type Response } from 'express';
import { run, Runtime } from './run.js';
import { run, Runtime } from 'effect-firebase';
import { logger } from 'firebase-functions';
import { parseBody, sendJson } from './on-request-helpers.js';

Expand Down
2 changes: 1 addition & 1 deletion packages/admin/src/lib/functions/on-task-dispatched.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import {
TaskQueueOptions,
} from 'firebase-functions/v2/tasks';
import { logger } from 'firebase-functions';
import { run, Runtime } from './run.js';
import { run, Runtime } from 'effect-firebase';

interface TaskDispatchedEffectOptions<R> extends TaskQueueOptions {
runtime: Runtime<R>;
Expand Down
2 changes: 2 additions & 0 deletions packages/effect-firebase/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
export * from './lib/runtime.js';

export * as FirestoreSchema from './lib/firestore/schema/schema.js';
export * as Firestore from './lib/firestore/firestore.js';
export * from './lib/firestore/firestore-service.js';
Expand Down
71 changes: 71 additions & 0 deletions packages/genkit/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
# @effect-firebase/genkit

Bridge [Effect](https://effect.website) into [Genkit](https://genkit.dev) and Firebase Cloud Functions' `onCallGenkit`.

> [!WARNING]
> Under heavy development. APIs may change.

## Install

```bash
npm install @effect-firebase/genkit effect effect-firebase genkit firebase-functions
```

## What this gives you

- **`makeTool`** — convert an Effect `Tool` (with `Schema` parameters / success / failure) into a Genkit `ToolAction`. Effect schemas are converted to JSON Schema for the model.
- **`onCallGenkitEffect`** — schema-driven callable backed by a Genkit flow, mirroring `onCallEffect` from `@effect-firebase/admin`. Typed Effect handlers, optional input/output decoding via `Schema`, tools wired in via Genkit.

## Quickstart

```ts
import { genkit } from 'genkit';
import { googleAI } from '@genkit-ai/googleai';
import { Schema, Effect, Tool } from 'effect';
import { FunctionsRuntime } from '@effect-firebase/admin';
import { makeTool, onCallGenkitEffect } from '@effect-firebase/genkit';

const ai = genkit({ plugins: [googleAI()] });
const runtime = FunctionsRuntime.Default();

const GetWeather = Tool.make('getWeather', {
description: 'Get current weather for a city',
parameters: Schema.Struct({ city: Schema.String }),
success: Schema.Struct({ tempC: Schema.Number }),
failure: Schema.Struct({ reason: Schema.Literal('city_not_found') }),
});

const getWeather = makeTool(ai, GetWeather,
({ city }) => Effect.succeed({ tempC: 21 }),
{ runtime }
);

export const summarize = onCallGenkitEffect(ai, {
name: 'summarize',
region: 'europe-north1',
runtime,
tools: [getWeather],
inputSchema: Schema.Struct({ text: Schema.String }),
outputSchema: Schema.Struct({ summary: Schema.String }),
}, ({ text }) => Effect.gen(function* () {
const { text: summary } = yield* Effect.promise(() =>
ai.generate({ prompt: `Summarize: ${text}`, tools: [getWeather] })
);
return { summary };
}));
```

## Failure handling

If your Effect `Tool` declares a `failureSchema`, typed failures from the
handler are encoded through that schema and thrown as a `GenkitError` with
`status: 'FAILED_PRECONDITION'` and the encoded payload on `detail`. Defects
(unexpected errors) surface as `GenkitError` with `status: 'INTERNAL'`.

## Notes

- Genkit's `FlowConfig` does not accept JSON Schema (only Zod). Input and
output validation for `onCallGenkitEffect` runs through Effect's `Schema`
inside the handler — the flow itself is registered without a Zod schema.
- `firebase-functions` is an optional peer dependency. Skip installing it if
you only need `makeTool` and not the callable wrapper.
9 changes: 9 additions & 0 deletions packages/genkit/eslint.config.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import baseConfig from '../../eslint.config.mjs';

export default [
...baseConfig,
{
files: ['**/*.ts', '**/*.js'],
rules: {},
}
];
52 changes: 52 additions & 0 deletions packages/genkit/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
{
"name": "@effect-firebase/genkit",
"version": "0.11.0",
"private": false,
"license": "MIT",
"repository": {
"type": "git",
"url": "https://github.com/fwal/effect-firebase",
"directory": "packages/genkit"
},
"type": "module",
"main": "./dist/index.js",
"module": "./dist/index.js",
"types": "./dist/index.d.ts",
"exports": {
"./package.json": "./package.json",
".": {
"@effect-firebase/source": "./src/index.ts",
"types": "./dist/index.d.ts",
"import": "./dist/index.js",
"default": "./dist/index.js"
}
},
"files": [
"dist",
"!**/*.tsbuildinfo"
],
"dependencies": {
"tslib": "^2.3.0"
},
"devDependencies": {
"effect": "^4.0.0-beta.70",
"effect-firebase": "workspace:*",
"firebase-functions": "^7.1.0",
"genkit": "^1.34.0"
},
"peerDependencies": {
"effect": "^4.0.0-beta.70",
"effect-firebase": "workspace:*",
"firebase-functions": "^7.0.0",
"genkit": "^1.0.0"
},
"peerDependenciesMeta": {
"firebase-functions": {
"optional": true
}
},
"publishConfig": {
"access": "public",
"provenance": true
}
}
2 changes: 2 additions & 0 deletions packages/genkit/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './lib/tool.js';
export * from './lib/on-call.js';
101 changes: 101 additions & 0 deletions packages/genkit/src/lib/on-call.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import { describe, expect, it } from 'vitest';
import { Effect, Layer, ManagedRuntime, Schema } from 'effect';
import { genkit, GenkitError } from 'genkit';
import { onCallGenkitEffect } from './on-call.js';

const runtime = () => ManagedRuntime.make(Layer.empty);

const findFlow = (ai: ReturnType<typeof genkit>, name: string) => {
const flow = ai.flows.find((f) => f.__action.name === name);
if (!flow) throw new Error(`Flow '${name}' was not registered`);
return flow;
};

describe('onCallGenkitEffect', () => {
it('registers a flow on the Genkit instance', () => {
const ai = genkit({});
onCallGenkitEffect(
ai,
{
name: 'echo',
runtime: runtime(),
inputSchema: Schema.Struct({ text: Schema.String }),
outputSchema: Schema.Struct({ echoed: Schema.String }),
},
({ text }) => Effect.succeed({ echoed: text })
);

expect(ai.flows.some((f) => f.__action.name === 'echo')).toBe(true);
});

it('decodes input, runs the handler, and encodes output', async () => {
const ai = genkit({});
onCallGenkitEffect(
ai,
{
name: 'upper',
runtime: runtime(),
inputSchema: Schema.Struct({ text: Schema.String }),
outputSchema: Schema.Struct({ text: Schema.String }),
},
({ text }) => Effect.succeed({ text: text.toUpperCase() })
);

const result = await findFlow(ai, 'upper')({ text: 'hello' });
expect(result).toEqual({ text: 'HELLO' });
});

it('rejects with a SchemaError when input does not decode', async () => {
const ai = genkit({});
onCallGenkitEffect(
ai,
{
name: 'strict',
runtime: runtime(),
inputSchema: Schema.Struct({ n: Schema.Number }),
outputSchema: Schema.Struct({ n: Schema.Number }),
},
({ n }) => Effect.succeed({ n: n + 1 })
);

await expect(
findFlow(ai, 'strict')({ n: 'not-a-number' })
).rejects.toThrow();
});

it('passes a user-thrown GenkitError through unchanged (no FiberFailure wrapper)', async () => {
const ai = genkit({});
const userError = new GenkitError({
status: 'PERMISSION_DENIED',
message: 'not allowed',
});
onCallGenkitEffect(
ai,
{
name: 'denied',
runtime: runtime(),
inputSchema: Schema.Struct({ x: Schema.Number }),
outputSchema: Schema.Struct({ x: Schema.Number }),
},
() => Effect.fail(userError)
);

const rejection = await findFlow(ai, 'denied')({ x: 1 }).catch(
(e: unknown) => e
);
expect(rejection).toBe(userError);
expect((rejection as GenkitError).status).toBe('PERMISSION_DENIED');
});

it('accepts a handler with no schemas (raw input pass-through)', async () => {
const ai = genkit({});
onCallGenkitEffect(
ai,
{ name: 'raw', runtime: runtime() },
(input) => Effect.succeed({ received: input })
);

const result = await findFlow(ai, 'raw')({ anything: true });
expect(result).toEqual({ received: { anything: true } });
});
});
Loading
Loading