Skip to content
Merged
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
10 changes: 5 additions & 5 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ export default handler;

```json
{
"name": "send-email-link",
"name": "send-verification-link",
"version": "1.1.0",
"description": "Sends invite, password reset, and verification emails",
"type": "node-graphql",
Expand Down Expand Up @@ -111,7 +111,7 @@ export default handler;
**Build Docker images:**
```bash
make docker-build # build all function images
make docker-build-send-email-link # build single function image
make docker-build-send-verification-link # build single function image
```

**Local development with Docker:**
Expand All @@ -129,9 +129,9 @@ pnpm build # Recompile

## Key Details

- Each function declares its port in `handler.json` (`simple-email` 8081, `send-email-link` 8082, `knative-job-example` 8083, `python-example` 8084); the job service uses 8080
- Email functions support dry-run via `SIMPLE_EMAIL_DRY_RUN` / `SEND_EMAIL_LINK_DRY_RUN`
- `loadFunctionApp()` in job/service resolves modules by name (e.g. `@constructive-io/simple-email-fn`)
- Each function declares its port in `handler.json` (`send-email` 8081, `send-verification-link` 8082, `knative-job-example` 8083, `python-example` 8084); the job service uses 8080
- Email functions support dry-run via `SEND_EMAIL_DRY_RUN` / `SEND_VERIFICATION_LINK_DRY_RUN` (legacy `SIMPLE_EMAIL_DRY_RUN` / `SEND_EMAIL_LINK_DRY_RUN` still honored as fallback)
- `loadFunctionApp()` in job/service resolves modules by name (e.g. `@constructive-io/send-email-fn`)
- GraphQL clients require `GRAPHQL_URL` env var and `X-Database-Id` header
- The `generated/` directory is entirely gitignored
- Templates use `{{name}}`, `{{version}}`, `{{description}}` placeholders
Expand Down
20 changes: 10 additions & 10 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Constructive Functions

Serverless function workloads (simple-email, send-email-link) with a job queue system deployed via Kubernetes.
Serverless function workloads (send-email, send-verification-link) with a job queue system deployed via Kubernetes.

## Project Structure

Expand Down Expand Up @@ -91,8 +91,8 @@ Edit `functions/<name>/handler.ts` → Skaffold syncs the file into the containe
|---------|------------|
| PostgreSQL | 5432 |
| Job Service | 8080 |
| simple-email | 8081 |
| send-email-link | 8082 |
| send-email | 8081 |
| send-verification-link | 8082 |

## Debugging K8s Pods

Expand All @@ -111,8 +111,8 @@ All pods should show `Running` (except `constructive-db` which should be `Comple
kubectl logs -n constructive-functions -l app=knative-job-service -f

# Function logs
kubectl logs -n constructive-functions -l app=simple-email -f
kubectl logs -n constructive-functions -l app=send-email-link -f
kubectl logs -n constructive-functions -l app=send-email -f
kubectl logs -n constructive-functions -l app=send-verification-link -f

# Constructive server logs
kubectl logs -n constructive-functions -l app=constructive-server -f
Expand All @@ -133,15 +133,15 @@ kubectl describe pod <pod-name> -n constructive-functions
```bash
kubectl port-forward -n constructive-functions svc/postgres 5432:5432
kubectl port-forward -n constructive-functions svc/knative-job-service 8080:8080
kubectl port-forward -n constructive-functions svc/simple-email 8081:80
kubectl port-forward -n constructive-functions svc/send-email-link 8082:80
kubectl port-forward -n constructive-functions svc/send-email 8081:80
kubectl port-forward -n constructive-functions svc/send-verification-link 8082:80
kubectl port-forward -n constructive-functions svc/constructive-server 3002:3000
```

### Exec into a pod

```bash
kubectl exec -it -n constructive-functions deploy/simple-email -- sh
kubectl exec -it -n constructive-functions deploy/send-email -- sh
kubectl exec -it -n constructive-functions deploy/knative-job-service -- sh
```

Expand All @@ -162,7 +162,7 @@ SELECT set_config('jwt.claims.database_id', (SELECT id::text FROM metaschema_pub

-- Manually insert a test job
SELECT * FROM app_jobs.add_job(
'simple-email'::text,
'send-email'::text,
'{"to":"test@example.com","subject":"test","html":"<p>hello</p>"}'::json
);
Comment on lines 163 to 167
```
Expand All @@ -171,7 +171,7 @@ SELECT * FROM app_jobs.add_job(

```bash
kubectl rollout restart -n constructive-functions deploy/knative-job-service
kubectl rollout restart -n constructive-functions deploy/simple-email
kubectl rollout restart -n constructive-functions deploy/send-email
```

### GHCR pull secret
Expand Down
26 changes: 13 additions & 13 deletions DEVELOPMENT.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,8 +58,8 @@ After this you should have built artifacts in:

| Package | Output |
|---------|--------|
| `generated/send-email-link/dist/` | Send-email-link function server |
| `generated/simple-email/dist/` | Simple-email function server |
| `generated/send-verification-link/dist/` | Send-verification-link function server |
| `generated/send-email/dist/` | Send-email function server |
| `generated/example/dist/` | knative-job-example function server |
| `generated/python-example/dist/` | Python example function server |
| `job/service/dist/` | Knative job service (worker + scheduler) |
Expand Down Expand Up @@ -112,20 +112,20 @@ This runs `scripts/dev.ts` which spawns local Node processes with env vars point
| Process | Port | Script |
|---------|------|--------|
| **job-service** | 8080 | `job/service/dist/run.js` |
| **simple-email** | 8081 | `generated/simple-email/dist/index.js` |
| **send-email-link** | 8082 | `generated/send-email-link/dist/index.js` |
| **send-email** | 8081 | `generated/send-email/dist/index.js` |
| **send-verification-link** | 8082 | `generated/send-verification-link/dist/index.js` |
| **knative-job-example** | 8083 | `generated/example/dist/index.js` |
| **python-example** | 8084 | `generated/python-example/...` (python entrypoint) |

To start a single function:

```bash
pnpm dev:fn -- --only=send-email-link
pnpm dev:fn -- --only=send-verification-link
```

### 4. Test a Function

Send a request to `send-email-link`:
Send a request to `send-verification-link`:

```bash
curl -X POST http://localhost:8082 \
Expand Down Expand Up @@ -176,8 +176,8 @@ make dev-down # Stop Docker infrastructure
| Mailpit SMTP | 1025 |
| Mailpit UI | 8025 |
| Job Service | 8080 |
| simple-email | 8081 |
| send-email-link | 8082 |
| send-email | 8081 |
| send-verification-link | 8082 |
| knative-job-example | 8083 |
| python-example | 8084 |

Expand All @@ -190,8 +190,8 @@ Docker Compose (infrastructure):

Local Node processes (functions):
job/service/dist/run.js (port 8080)
generated/simple-email/dist/index.js (port 8081)
generated/send-email-link/dist/index.js (port 8082)
generated/send-email/dist/index.js (port 8081)
generated/send-verification-link/dist/index.js (port 8082)
```

Infrastructure runs in Docker. Functions run as local Node processes from `generated/` — no Docker rebuild needed when function code changes. Edit `functions/*/handler.ts`, rebuild (`pnpm build`), restart `make dev-fn`.
Expand Down Expand Up @@ -252,7 +252,7 @@ make skaffold-dev
This runs `skaffold dev -p local-simple` which:
1. Builds the `constructive-functions` Docker image from `Dockerfile.dev`
2. Deploys infrastructure (postgres, minio, constructive-server, constructive-server-admin, db-setup, job-service) via kustomize
3. Deploys functions (simple-email, send-email-link) via generated rawYaml manifests
3. Deploys functions (send-email, send-verification-link) via generated rawYaml manifests
4. Sets up port-forwarding automatically
5. Watches `functions/**/*.ts` — edits are synced into running containers
6. `tsx --watch` inside each function container detects changes and restarts
Expand Down Expand Up @@ -283,8 +283,8 @@ Changes to runtime packages (`packages/fn-runtime`, `packages/fn-app`) or `packa

| Service | Local Port |
|---------|------------|
| simple-email | 8081 |
| send-email-link | 8082 |
| send-email | 8081 |
| send-verification-link | 8082 |
| knative-job-example | 8083 |
| python-example | 8084 |
| Job Service | 8080 |
Expand Down
4 changes: 2 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ setup-check:
skaffold-dev:
skaffold dev -p local-simple

# Single function: make skaffold-dev-simple-email
# Single function: make skaffold-dev-send-email
skaffold-dev-%:
skaffold dev -p $*

Expand All @@ -57,6 +57,6 @@ skaffold-dev-knative:
docker-build:
pnpm run docker:build

# Build a single function image: make docker-build-send-email-link
# Build a single function image: make docker-build-send-verification-link
docker-build-%:
node --experimental-strip-types scripts/docker-build.ts --only=$*
30 changes: 15 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,38 +27,38 @@ pnpm install # install dependencies (including generated packages)
pnpm build # build all packages and functions

make docker-build # build all function Docker images
make docker-build-simple-email # build a single function image
make docker-build-send-email-link
make docker-build-send-email # build a single function image
make docker-build-send-verification-link
```

## Functions

| Function | Port | Type | Image |
|----------|------|------|-------|
| `simple-email` | 8081 | node-graphql | `ghcr.io/constructive-io/constructive-functions/simple-email:latest` |
| `send-email-link` | 8082 | node-graphql | `ghcr.io/constructive-io/constructive-functions/send-email-link:latest` |
| `knative-job-example` | 8083 | node-graphql | `ghcr.io/constructive-io/constructive-functions/knative-job-example:latest` |
| `python-example` | 8084 | python | `ghcr.io/constructive-io/constructive-functions/python-example:latest` |
| `send-email` | 8081 | node-graphql | `ghcr.io/constructive-io/send-email-fn:latest` |
| `send-verification-link` | 8082 | node-graphql | `ghcr.io/constructive-io/send-verification-link-fn:latest` |
| `knative-job-example` | 8083 | node-graphql | `ghcr.io/constructive-io/knative-job-example-fn:latest` |
| `python-example` | 8084 | python | `ghcr.io/constructive-io/python-example-fn:latest` |

Port `8080` is reserved for the job service.

### `simple-email`
### `send-email`

Sends emails directly from a job payload.

- `SIMPLE_EMAIL_DRY_RUN` — if `true`, logs the payload instead of sending
- `SEND_EMAIL_DRY_RUN` — if `true`, logs the payload instead of sending
- `MAILGUN_API_KEY`, `MAILGUN_KEY`, `MAILGUN_DOMAIN`, `MAILGUN_FROM`, `MAILGUN_REPLY` — Mailgun config

### `send-email-link`
### `send-verification-link`

Sends invite, password reset, and verification emails (rendered via MJML).

- `SEND_EMAIL_LINK_DRY_RUN` — if `true`, logs the payload instead of sending
- `SEND_VERIFICATION_LINK_DRY_RUN` — if `true`, logs the payload instead of sending
- `DEFAULT_DATABASE_ID` — default database UUID
- `GRAPHQL_URL`, `META_GRAPHQL_URL` — GraphQL API endpoints
- `GRAPHQL_AUTH_TOKEN` — optional Bearer token for GraphQL requests
- `LOCAL_APP_PORT` — local port for dashboard links (e.g. `3000`)
- `MAILGUN_*` — same Mailgun config as `simple-email`
- `MAILGUN_*` — same Mailgun config as `send-email`

### `knative-job-example` / `python-example`

Expand Down Expand Up @@ -114,10 +114,10 @@ The `CI Test K8s` workflow (`.github/workflows/test-k8s-deployment.yaml`) runs o
Images are tagged with the GHCR prefix automatically:

```bash
docker push ghcr.io/constructive-io/constructive-functions/simple-email:latest
docker push ghcr.io/constructive-io/constructive-functions/send-email-link:latest
docker push ghcr.io/constructive-io/constructive-functions/knative-job-example:latest
docker push ghcr.io/constructive-io/constructive-functions/python-example:latest
docker push ghcr.io/constructive-io/send-email-fn:latest
docker push ghcr.io/constructive-io/send-verification-link-fn:latest
docker push ghcr.io/constructive-io/knative-job-example-fn:latest
docker push ghcr.io/constructive-io/python-example-fn:latest
```

- `make docker-build` — builds all function images
Expand Down
2 changes: 1 addition & 1 deletion docs/plan/00-overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ Generated packages symlink `handler.ts` and `*.d.ts` back to `functions/<name>/`
- [x] fn-runtime package (server, context, GraphQL clients, types)
- [x] fn-app package (Express factory with error middleware)
- [x] job/service (KnativeJobsSvc with function loading and job orchestration)
- [x] 3 functions: example, simple-email, send-email-link
- [x] 3 functions: example, send-email, send-verification-link
- [x] Docker compose dev setup
- [x] K8s base manifests and overlays

Expand Down
6 changes: 3 additions & 3 deletions docs/plan/01-docker-ci.md
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,7 @@ jobs:

**Dynamic matrix via discovery job**: The `discover` job reads `functions/*/handler.json` and outputs a JSON matrix. This means adding a new function only requires creating `functions/<name>/handler.json` — no workflow changes needed.

**`matrix.dir` vs `matrix.name`**: The directory name (e.g., `simple-email`) may differ from the handler.json `name` field (e.g., `knative-job-example` vs dir `example`). We pass both:
**`matrix.dir` vs `matrix.name`**: The directory name (e.g., `send-email`) may differ from the handler.json `name` field (e.g., `knative-job-example` vs dir `example`). We pass both:
- `matrix.dir` — used for `--only=` flag and Dockerfile path (generate.ts filters by directory name)
- `matrix.name` — used for image naming (from handler.json `name` field)

Expand All @@ -161,8 +161,8 @@ jobs:
- Each builds successfully but does NOT push (PR event)

2. **Push test**: Merge to main. Verify:
- Images appear at `ghcr.io/<owner>/simple-email-fn:latest`
- Images appear at `ghcr.io/<owner>/send-email-link-fn:latest`
- Images appear at `ghcr.io/<owner>/send-email-fn:latest`
- Images appear at `ghcr.io/<owner>/send-verification-link-fn:latest`
- Short SHA tags are applied

3. **New function test**: Add a new `functions/test-fn/handler.json` and verify it appears as a 4th matrix job automatically.
Expand Down
22 changes: 11 additions & 11 deletions docs/plan/02-testing-strategy.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,12 @@ Each function has:

### Handler testing challenges

**simple-email** (`functions/simple-email/handler.ts`):
**send-email** (`functions/send-email/handler.ts`):
- Lines 30-31: `isDryRun` and `useSmtp` read from `process.env` at **module load time** (top-level `const`)
- Tests must set env vars BEFORE importing the handler, or use `vi.stubEnv()` + dynamic `import()`
- Mocks needed: `sendPostmaster`, `sendSmtp` from imported modules

**send-email-link** (`functions/send-email-link/handler.ts`):
**send-verification-link** (`functions/send-verification-link/handler.ts`):
- Lines 73-74: `isDryRun` and `useSmtp` read from `context.env` (not process.env) — per-request, easier to test
- Makes GraphQL calls: `meta.request(GetDatabaseInfo)` and `client.request(GetUser)`
- Tests mock GraphQL client responses to control site/user data
Expand Down Expand Up @@ -157,7 +157,7 @@ describe('example handler', () => {
});
```

#### `functions/simple-email/__tests__/handler.test.ts`
#### `functions/send-email/__tests__/handler.test.ts`

Key challenge: `isDryRun` and `useSmtp` are module-level constants (lines 30-31). Use `vi.stubEnv()` and dynamic import with `vi.resetModules()`.

Expand All @@ -173,12 +173,12 @@ vi.mock('@constructive-io/postmaster', () => ({
send: vi.fn().mockResolvedValue(undefined)
}));

describe('simple-email handler', () => {
describe('send-email handler', () => {
let handler: any;

beforeEach(async () => {
vi.resetModules();
vi.stubEnv('SIMPLE_EMAIL_DRY_RUN', 'false');
vi.stubEnv('SEND_EMAIL_DRY_RUN', 'false');
vi.stubEnv('EMAIL_SEND_USE_SMTP', 'false');
const mod = await import('../handler');
handler = mod.default;
Expand Down Expand Up @@ -230,7 +230,7 @@ describe('simple-email handler', () => {
describe('dry-run mode', () => {
beforeEach(async () => {
vi.resetModules();
vi.stubEnv('SIMPLE_EMAIL_DRY_RUN', 'true');
vi.stubEnv('SEND_EMAIL_DRY_RUN', 'true');
const mod = await import('../handler');
handler = mod.default;
});
Expand All @@ -248,7 +248,7 @@ describe('simple-email handler', () => {
});
```

#### `functions/send-email-link/__tests__/handler.test.ts`
#### `functions/send-verification-link/__tests__/handler.test.ts`

```typescript
import { describe, it, expect, vi, beforeEach } from 'vitest';
Expand Down Expand Up @@ -285,7 +285,7 @@ const mockSiteData = {
}
};

describe('send-email-link handler', () => {
describe('send-verification-link handler', () => {
let handler: any;

beforeEach(async () => {
Expand Down Expand Up @@ -377,7 +377,7 @@ describe('send-email-link handler', () => {
it('logs but does not send when DRY_RUN is true', async () => {
const ctx = createMockContext({
metaResponse: mockSiteData,
env: { SEND_EMAIL_LINK_DRY_RUN: 'true' }
env: { SEND_VERIFICATION_LINK_DRY_RUN: 'true' }
});
const result = await handler({
email_type: 'forgot_password',
Expand Down Expand Up @@ -574,8 +574,8 @@ This layer requires both Docker images (WS1) and Knative services to be working.
| Create | `.github/workflows/test.yaml` |
| Create | `tests/unit/helpers/mock-context.ts` |
| Create | `functions/example/__tests__/handler.test.ts` |
| Create | `functions/simple-email/__tests__/handler.test.ts` |
| Create | `functions/send-email-link/__tests__/handler.test.ts` |
| Create | `functions/send-email/__tests__/handler.test.ts` |
| Create | `functions/send-verification-link/__tests__/handler.test.ts` |
| Create | `tests/integration/helpers/start-function.ts` |
| Create | `tests/integration/runtime.test.ts` |
| Modify | `package.json` — add vitest devDep + test scripts |
Expand Down
Loading
Loading