From 86719b19728a514284e760bd8f3519be4ace82a5 Mon Sep 17 00:00:00 2001 From: Anmol1696 Date: Tue, 12 May 2026 13:25:36 +0800 Subject: [PATCH 1/2] refactor: rename email functions to send-verification-link and send-email Match upstream constructive's purpose-driven names. Function dirs and k8s service names use plain identifiers; namespaced task identifiers (email:send_verification_link, email:send_email) live in worker env config (JOBS_SUPPORTED, INTERNAL_GATEWAY_DEVELOPMENT_MAP) and DB-inserted jobs from constructive-db. - functions/send-email-link -> functions/send-verification-link - functions/simple-email -> functions/send-email - handler.json: add optional taskIdentifier field; generator now keys JOBS_SUPPORTED and the gateway map on taskIdentifier (defaults to name) - HandlerManifest + FnRegistryEntry types: add taskIdentifier - job/service: workspace:^ deps on local generated packages so symlinks win over upstream npm versions - k8s/base + overlays + interweb flat manifests: rename services, fix env vars, set namespaced task identifiers - e2e + integration tests: rename, assert on namespaced task identifiers - docs sweep: README, CLAUDE.md, AGENTS.md, DEVELOPMENT.md, docs/, k8s/ - handlers keep backward-compat env-var fallbacks (SIMPLE_EMAIL_DRY_RUN, SEND_EMAIL_LINK_DRY_RUN) during the transition --- AGENTS.md | 10 +- CLAUDE.md | 20 +-- DEVELOPMENT.md | 26 +-- Makefile | 4 +- README.md | 30 ++-- docs/plan/00-overview.md | 2 +- docs/plan/01-docker-ci.md | 6 +- docs/plan/02-testing-strategy.md | 22 +-- docs/plan/03-function-registry.md | 59 +++---- docs/plan/04-env-handling.md | 62 +++---- docs/portable-functions-toolkit.md | 2 +- docs/skills/adding-functions.md | 33 +--- docs/skills/local-dev-skaffold.md | 30 ++-- docs/spec/function-templating.md | 28 ++-- .../__tests__/handler.test.ts | 8 +- .../{simple-email => send-email}/handler.json | 5 +- .../{simple-email => send-email}/handler.ts | 17 +- .../__tests__/handler.graphql.test.ts | 4 +- .../__tests__/handler.test.ts | 2 +- .../handler.json | 3 +- .../handler.ts | 13 +- .../types.d.ts | 0 job/service/package.json | 4 +- job/service/src/index.ts | 8 +- job/service/src/registry.ts | 2 +- k8s/ARCHITECTURE.md | 8 +- k8s/SECRETS-ARCHITECTURE.md | 2 +- .../constructive/knative-job-service.yaml | 8 +- .../{simple-email.yaml => send-email.yaml} | 8 +- ...-link.yaml => send-verification-link.yaml} | 8 +- k8s/base/kustomization.yaml | 4 +- ...mple-email.yaml => delete-send-email.yaml} | 2 +- k8s/overlays/ci/kustomization.yaml | 2 +- k8s/overlays/local-simple/config.yaml | 4 +- .../constructive/knative-job-service.yaml | 4 +- k8s/overlays/local/kustomization.yaml | 16 +- packages/fn-client/__tests__/client.test.ts | 4 +- packages/fn-generator/README.md | 2 +- .../fn-generator/src/builders/configmap.ts | 6 +- .../fn-generator/src/builders/manifest.ts | 16 +- packages/fn-types/src/index.ts | 24 ++- packages/fn-types/src/manifest.ts | 5 + packages/fn-types/src/registry.ts | 4 +- pnpm-lock.yaml | 154 +++--------------- scripts/dev.ts | 11 +- scripts/docker-build.ts | 4 +- scripts/generate.ts | 12 +- skaffold.yaml | 40 ++--- tests/e2e/README.md | 2 +- tests/e2e/__tests__/job-processing.test.ts | 2 +- ...ail.e2e.test.ts => send-email.e2e.test.ts} | 20 +-- ....ts => send-verification-link.e2e.test.ts} | 20 +-- tests/integration/job-registry.test.ts | 12 +- 53 files changed, 354 insertions(+), 450 deletions(-) rename functions/{simple-email => send-email}/__tests__/handler.test.ts (95%) rename functions/{simple-email => send-email}/handler.json (60%) rename functions/{simple-email => send-email}/handler.ts (85%) rename functions/{send-email-link => send-verification-link}/__tests__/handler.graphql.test.ts (85%) rename functions/{send-email-link => send-verification-link}/__tests__/handler.test.ts (96%) rename functions/{send-email-link => send-verification-link}/handler.json (83%) rename functions/{send-email-link => send-verification-link}/handler.ts (97%) rename functions/{send-email-link => send-verification-link}/types.d.ts (100%) rename k8s/base/functions/{simple-email.yaml => send-email.yaml} (91%) rename k8s/base/functions/{send-email-link.yaml => send-verification-link.yaml} (90%) rename k8s/overlays/ci/{delete-simple-email.yaml => delete-send-email.yaml} (77%) rename tests/e2e/__tests__/{simple-email.e2e.test.ts => send-email.e2e.test.ts} (71%) rename tests/e2e/__tests__/{send-email-link.e2e.test.ts => send-verification-link.e2e.test.ts} (65%) diff --git a/AGENTS.md b/AGENTS.md index c0941b5..a2cf4af 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -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", @@ -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:** @@ -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 diff --git a/CLAUDE.md b/CLAUDE.md index 4b179c6..8ab94dc 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -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 @@ -91,8 +91,8 @@ Edit `functions//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 @@ -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 @@ -133,15 +133,15 @@ kubectl describe pod -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 ``` @@ -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":"

hello

"}'::json ); ``` @@ -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 diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md index 6f07c9a..90db15b 100644 --- a/DEVELOPMENT.md +++ b/DEVELOPMENT.md @@ -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) | @@ -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 \ @@ -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 | @@ -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`. @@ -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 @@ -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 | diff --git a/Makefile b/Makefile index a27eedb..95b3248 100644 --- a/Makefile +++ b/Makefile @@ -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 $* @@ -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=$* diff --git a/README.md b/README.md index d061e19..69ed015 100644 --- a/README.md +++ b/README.md @@ -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` @@ -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 diff --git a/docs/plan/00-overview.md b/docs/plan/00-overview.md index 16a7fc7..64bc82f 100644 --- a/docs/plan/00-overview.md +++ b/docs/plan/00-overview.md @@ -86,7 +86,7 @@ Generated packages symlink `handler.ts` and `*.d.ts` back to `functions//` - [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 diff --git a/docs/plan/01-docker-ci.md b/docs/plan/01-docker-ci.md index d2cd8c2..9aab9e2 100644 --- a/docs/plan/01-docker-ci.md +++ b/docs/plan/01-docker-ci.md @@ -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//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) @@ -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//simple-email-fn:latest` - - Images appear at `ghcr.io//send-email-link-fn:latest` + - Images appear at `ghcr.io//send-email-fn:latest` + - Images appear at `ghcr.io//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. diff --git a/docs/plan/02-testing-strategy.md b/docs/plan/02-testing-strategy.md index 5ccd631..c7dc376 100644 --- a/docs/plan/02-testing-strategy.md +++ b/docs/plan/02-testing-strategy.md @@ -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 @@ -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()`. @@ -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; @@ -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; }); @@ -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'; @@ -285,7 +285,7 @@ const mockSiteData = { } }; -describe('send-email-link handler', () => { +describe('send-verification-link handler', () => { let handler: any; beforeEach(async () => { @@ -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', @@ -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 | diff --git a/docs/plan/03-function-registry.md b/docs/plan/03-function-registry.md index 05a089f..255b8fa 100644 --- a/docs/plan/03-function-registry.md +++ b/docs/plan/03-function-registry.md @@ -1,36 +1,37 @@ # WS3: Dynamic Function Registry **Branch**: `feat/function-registry` -**Dependencies**: None — can start immediately -**Approach**: HTTP-based dynamic discovery (functions self-register) +**Status**: superseded — the manifest-driven approach landed in commit `a0cfd7e` (`feat(job-service): replace hardcoded function registry with manifest loader`). The original "hardcoded registry" problem this WS proposed to solve no longer exists. -## Context +> Historical note. The text below describes the pre-`a0cfd7e` state for posterity. Today, `job/service/src/registry.ts::loadFunctionRegistry()` reads `generated/functions-manifest.json` (or `FUNCTIONS_REGISTRY` / `FUNCTIONS_MANIFEST_PATH` env vars), and `FunctionName` in `job/service/src/types.ts` is just `string`. + +## Context (historical) ### Problem -The function registry in `job/service/src/index.ts` is hardcoded: +The function registry in `job/service/src/index.ts` was hardcoded: ```typescript -// job/service/src/index.ts lines 29-43 +// job/service/src/index.ts lines 29-43 (pre-a0cfd7e) const functionRegistry: Record = { - 'simple-email': { - moduleName: '@constructive-io/simple-email-fn', + 'send-email': { + moduleName: '@constructive-io/send-email-fn', defaultPort: 8081 }, - 'send-email-link': { - moduleName: '@constructive-io/send-email-link-fn', + 'send-verification-link': { + moduleName: '@constructive-io/send-verification-link-fn', defaultPort: 8082 } }; ``` -And the `FunctionName` type is a hardcoded union: +And the `FunctionName` type was a hardcoded union: ```typescript -// job/service/src/types.ts line 1 -export type FunctionName = 'simple-email' | 'send-email-link'; +// job/service/src/types.ts line 1 (pre-a0cfd7e) +export type FunctionName = 'send-email' | 'send-verification-link'; ``` -Functions are loaded in-process via `createRequire` (`index.ts:56-71`). Adding a new function requires editing 3 files. The current architecture tightly couples the service to specific function packages. +Functions were loaded in-process via `createRequire` (`index.ts:56-71`). Adding a new function required editing 3 files. That architecture tightly coupled the service to specific function packages. ### Goal @@ -43,7 +44,7 @@ Replace the hardcoded registry with HTTP-based dynamic discovery: each function | +-------------+-------------+ | | | - job-service simple-email send-email-link + job-service send-email send-verification-link | | | 1. Start HTTP 2. Listen 2. Listen server on :8080 on :8080 @@ -56,12 +57,12 @@ Replace the hardcoded registry with HTTP-based dynamic discovery: each function +<------------+<------------+ | Registry stores: - simple-email -> http://simple-email:8080 - send-email-link -> http://send-email-link:8080 + send-email -> http://send-email:8080 + send-verification-link -> http://send-verification-link:8080 | - Worker picks job (task_identifier="simple-email") - -> registry.resolve("simple-email") - -> HTTP POST http://simple-email:8080 + Worker picks job (task_identifier="send-email") + -> registry.resolve("send-email") + -> HTTP POST http://send-email:8080 ``` ## Phased Implementation @@ -342,7 +343,7 @@ When imported as a module (e.g., tests), only the app is created. When run stand | Variable | Description | Example | |----------|-------------|---------| | `FUNCTION_REGISTRY_URL` | Registration endpoint on job-service | `http://job-service:8080/functions/register` | -| `FUNCTION_SELF_URL` | This function's externally-reachable URL | `http://simple-email:8080` | +| `FUNCTION_SELF_URL` | This function's externally-reachable URL | `http://send-email:8080` | #### Phase 3 files @@ -395,8 +396,8 @@ Remove: `FunctionServiceConfig`, `FunctionsOptions`, `StartedFunction` Remove individual function deps (service no longer loads them): ``` -"@constructive-io/send-email-link-fn": "workspace:^" -"@constructive-io/simple-email-fn": "workspace:^" +"@constructive-io/send-verification-link-fn": "workspace:^" +"@constructive-io/send-email-fn": "workspace:^" ``` #### 4d. Update `docker-compose.yml` @@ -424,29 +425,29 @@ services: ports: - "8080:8080" - simple-email: + send-email: build: context: . dockerfile: Dockerfile.dev - command: node generated/simple-email/dist/index.js + command: node generated/send-email/dist/index.js environment: PORT: "8080" FUNCTION_REGISTRY_URL: "http://job-service:8080/functions/register" - FUNCTION_SELF_URL: "http://simple-email:8080" + FUNCTION_SELF_URL: "http://send-email:8080" depends_on: - job-service ports: - "8081:8080" - send-email-link: + send-verification-link: build: context: . dockerfile: Dockerfile.dev - command: node generated/send-email-link/dist/index.js + command: node generated/send-verification-link/dist/index.js environment: PORT: "8080" FUNCTION_REGISTRY_URL: "http://job-service:8080/functions/register" - FUNCTION_SELF_URL: "http://send-email-link:8080" + FUNCTION_SELF_URL: "http://send-verification-link:8080" GRAPHQL_URL: "http://api:5000/graphql" depends_on: - job-service @@ -483,7 +484,7 @@ docker-compose up --build # Verify registration: curl http://localhost:8080/functions -# → [{ "name": "simple-email", ... }, { "name": "send-email-link", ... }] +# → [{ "name": "send-email", ... }, { "name": "send-verification-link", ... }] # Verify registration API: curl -X POST http://localhost:8080/functions/register \ diff --git a/docs/plan/04-env-handling.md b/docs/plan/04-env-handling.md index 575df92..88fbb9d 100644 --- a/docs/plan/04-env-handling.md +++ b/docs/plan/04-env-handling.md @@ -10,16 +10,22 @@ Functions read `process.env` directly and use `parseEnvBoolean()` from `@pgpmjs/env`: -**simple-email** (`functions/simple-email/handler.ts` lines 30-31): +**send-email** (`functions/send-email/handler.ts`): ```typescript -const isDryRun = parseEnvBoolean(process.env.SIMPLE_EMAIL_DRY_RUN) ?? false; +const isDryRun = + parseEnvBoolean(process.env.SEND_EMAIL_DRY_RUN) ?? + parseEnvBoolean(process.env.SIMPLE_EMAIL_DRY_RUN) ?? // legacy fallback + false; const useSmtp = parseEnvBoolean(process.env.EMAIL_SEND_USE_SMTP) ?? false; ``` -Also reads: `process.env.SMTP_FROM`, `process.env.MAILGUN_FROM` (lines 45-50). +Also reads: `process.env.SMTP_FROM`, `process.env.MAILGUN_FROM`. -**send-email-link** (`functions/send-email-link/handler.ts` lines 73-74): +**send-verification-link** (`functions/send-verification-link/handler.ts`): ```typescript -const isDryRun = parseEnvBoolean(env.SEND_EMAIL_LINK_DRY_RUN) ?? false; +const isDryRun = + parseEnvBoolean(env.SEND_VERIFICATION_LINK_DRY_RUN) ?? + parseEnvBoolean(env.SEND_EMAIL_LINK_DRY_RUN) ?? // legacy fallback + false; const useSmtp = parseEnvBoolean(env.EMAIL_SEND_USE_SMTP) ?? false; ``` Also reads: `env.GRAPHQL_URL` (via fn-runtime), `env.LOCAL_APP_PORT` (line 152). @@ -52,7 +58,7 @@ K8s manifests inject env vars via: 2. `secretRef` — secrets (pg-credentials, mailgun-credentials) 3. Explicit `env` entries — function-specific overrides -Example from `k8s/base/functions/simple-email.yaml`: +Example from `k8s/base/functions/send-email.yaml`: ```yaml envFrom: - secretRef: @@ -107,11 +113,11 @@ interface FunctionManifest { ### 2. Update handler.json files -#### `functions/simple-email/handler.json` +#### `functions/send-email/handler.json` ```json { - "name": "simple-email", + "name": "send-email", "version": "1.1.0", "type": "node-graphql", "description": "Simple Knative email function that sends emails directly from job payload", @@ -122,7 +128,7 @@ interface FunctionManifest { "simple-smtp-server": "^0.3.0" }, "env": { - "SIMPLE_EMAIL_DRY_RUN": { + "SEND_EMAIL_DRY_RUN": { "type": "boolean", "default": "false", "description": "Skip actual email sending (log only)" @@ -164,11 +170,11 @@ interface FunctionManifest { } ``` -#### `functions/send-email-link/handler.json` +#### `functions/send-verification-link/handler.json` ```json { - "name": "send-email-link", + "name": "send-verification-link", "version": "1.1.0", "type": "node-graphql", "description": "Sends invite, password reset, and verification emails", @@ -183,7 +189,7 @@ interface FunctionManifest { "simple-smtp-server": "^0.3.0" }, "env": { - "SEND_EMAIL_LINK_DRY_RUN": { + "SEND_VERIFICATION_LINK_DRY_RUN": { "type": "boolean", "default": "false", "description": "Skip actual email sending (log only)" @@ -336,14 +342,14 @@ In `main()`, after the template file processing loop, add: ### Generated output example -For `simple-email`, `generated/simple-email/.env.example`: +For `send-email`, `generated/send-email/.env.example`: ``` -# Environment variables for simple-email +# Environment variables for send-email # Generated from handler.json — do not edit directly # Skip actual email sending (log only) -SIMPLE_EMAIL_DRY_RUN=false +SEND_EMAIL_DRY_RUN=false # Use SMTP transport instead of Mailgun/Postmaster EMAIL_SEND_USE_SMTP=false @@ -370,14 +376,14 @@ SMTP_PASS= MAILGUN_FROM= ``` -For `send-email-link`, the generated `index.ts` would start with: +For `send-verification-link`, the generated `index.ts` would start with: ```typescript // --- Auto-generated env validation --- const _requiredEnv = ["GRAPHQL_URL"]; const _missingEnv = _requiredEnv.filter(k => !process.env[k]); if (_missingEnv.length > 0) { - throw new Error('Missing required environment variables for send-email-link: ' + _missingEnv.join(', ')); + throw new Error('Missing required environment variables for send-verification-link: ' + _missingEnv.join(', ')); } // --- End env validation --- import { createFunctionServer } from '@constructive-io/fn-runtime'; @@ -414,11 +420,11 @@ This is NOT part of the current workstream but the schema supports it. | Action | File | |--------|------| | Modify | `scripts/generate.ts` — add `EnvVarDeclaration` interface, update `FunctionManifest`, add `generateEnvExample()`, add `injectEnvValidation()`, update `processTemplateFile()` for index.ts | -| Modify | `functions/simple-email/handler.json` — add `env` field | -| Modify | `functions/send-email-link/handler.json` — add `env` field | +| Modify | `functions/send-email/handler.json` — add `env` field | +| Modify | `functions/send-verification-link/handler.json` — add `env` field | | Modify | `functions/example/handler.json` — add `env: {}` | -| Generated | `generated/simple-email/.env.example` | -| Generated | `generated/send-email-link/.env.example` | +| Generated | `generated/send-email/.env.example` | +| Generated | `generated/send-verification-link/.env.example` | ## Verification @@ -428,17 +434,17 @@ rm -rf generated/ pnpm generate # 2. Check .env.example files -cat generated/simple-email/.env.example +cat generated/send-email/.env.example # Should list all env vars with descriptions and defaults -cat generated/send-email-link/.env.example +cat generated/send-verification-link/.env.example # Should include GRAPHQL_URL marked as REQUIRED # 3. Check env validation injection -head -10 generated/send-email-link/index.ts +head -10 generated/send-verification-link/index.ts # Should start with _requiredEnv validation block -head -5 generated/simple-email/index.ts +head -5 generated/send-email/index.ts # Should NOT have validation block (no required vars) head -5 generated/example/index.ts @@ -450,9 +456,9 @@ pnpm build # All packages should compile (validation code is valid TypeScript) # 5. Test validation at runtime -GRAPHQL_URL="" node generated/send-email-link/dist/index.js -# Should throw: "Missing required environment variables for send-email-link: GRAPHQL_URL" +GRAPHQL_URL="" node generated/send-verification-link/dist/index.js +# Should throw: "Missing required environment variables for send-verification-link: GRAPHQL_URL" -GRAPHQL_URL=http://localhost:3000 PORT=9999 node generated/send-email-link/dist/index.js +GRAPHQL_URL=http://localhost:3000 PORT=9999 node generated/send-verification-link/dist/index.js # Should start without error (Ctrl+C to stop) ``` diff --git a/docs/portable-functions-toolkit.md b/docs/portable-functions-toolkit.md index 655e3f3..d819b4b 100644 --- a/docs/portable-functions-toolkit.md +++ b/docs/portable-functions-toolkit.md @@ -109,7 +109,7 @@ You can also run the workflow with `workflow_dispatch` (default `dry_run: true`) - [ ] **Job-registry tests**: `pnpm exec jest tests/integration/job-registry.test.ts` — six cases pass. - [ ] **Brasilia E2E**: with the live k8s stack running (`make skaffold-dev`), `pnpm test:e2e` still picks up jobs end-to-end. - [ ] **Scratch repo**: in a fresh `/tmp/test-fn-app` repo, `pnpm add -D @constructive-io/fn-cli && pnpm add @constructive-io/fn-runtime`, add `functions/hello/handler.{json,ts}`, run `fn generate && fn build && fn manifest`. Confirm output is sensible and `docker build -f generated/hello/Dockerfile .` succeeds. -- [ ] **Hub integration**: in `constructive-hub/istanbul`, `pnpm bootstrap && pnpm start` still launches `send-email-link` and processes a job (the hub does not yet consume the new toolkit; this confirms Wave 1-3 didn't regress the existing submodule path). +- [ ] **Hub integration**: in `constructive-hub/istanbul`, `pnpm bootstrap && pnpm start` still launches `send-verification-link` and processes a job (the hub does not yet consume the new toolkit; this confirms Wave 1-3 didn't regress the existing submodule path). ## Deferred follow-ups (not in this branch) diff --git a/docs/skills/adding-functions.md b/docs/skills/adding-functions.md index 1ea7023..7b3bd85 100644 --- a/docs/skills/adding-functions.md +++ b/docs/skills/adding-functions.md @@ -10,7 +10,7 @@ description: Step-by-step guide for adding a new serverless function to the cons - Node.js 22+, pnpm 10+ - Understanding of the `FunctionHandler` type from `@constructive-io/fn-runtime` -**Reference implementations:** See `functions/simple-email/` (env vars, external packages, dry-run mode) and `functions/send-email-link/` (GraphQL queries, context usage) as working examples. +**Reference implementations:** See `functions/send-email/` (env vars, external packages, dry-run mode) and `functions/send-verification-link/` (GraphQL queries, context usage) as working examples. ## Step 1: Create handler.json @@ -82,26 +82,7 @@ If your function imports modules that need TypeScript type stubs, add a `types.d declare module '@some-untyped-package'; ``` -## Step 3: Register with the job service - -Update `job/service/src/types.ts` — add the function name to the `FunctionName` union: - -```typescript -export type FunctionName = 'simple-email' | 'send-email-link' | ''; -``` - -Update `job/service/src/index.ts` — add an entry to `functionRegistry`: - -```typescript -'': { - moduleName: '@constructive-io/-fn', - defaultPort: -}, -``` - -The `moduleName` is the generated workspace package name (`@constructive-io/-fn`). The `defaultPort` must match the port in `handler.json`. - -## Step 4: Run generate +## Step 3: Run generate ```bash pnpm generate @@ -123,14 +104,14 @@ It also updates: - `k8s/overlays/local-simple/job-service.yaml` — adds function to JOBS_SUPPORTED and gateway map - `generated/functions-manifest.json` — function registry used by dev.ts -## Step 5: Install and build +## Step 4: Install and build ```bash pnpm install # picks up the new workspace package pnpm build # builds all packages including the new function ``` -## Step 6: Add unit tests +## Step 5: Add unit tests Create `functions//__tests__/handler.test.ts`: @@ -168,7 +149,7 @@ Use `tests/helpers/mock-context.ts` to create test contexts. If your function us Run: `pnpm test:unit` -## Step 7: Add e2e test +## Step 6: Add e2e test Create `tests/e2e/__tests__/.e2e.test.ts`: @@ -212,7 +193,7 @@ describe('E2E: ', () => { **Important:** The e2e test filename must match the function name (`.e2e.test.ts`) for the CI matrix to pick it up automatically. -## Step 8: Test locally +## Step 7: Test locally ### Option A: Docker Compose + local Node (fastest iteration) @@ -227,7 +208,7 @@ pnpm dev:fn --only= # run just your function make skaffold-dev- # deploys infra + just your function ``` -## Step 9: Verify CI will work +## Step 8: Verify CI will work The following CI workflows auto-discover functions — no manual edits needed: diff --git a/docs/skills/local-dev-skaffold.md b/docs/skills/local-dev-skaffold.md index 398e835..3159277 100644 --- a/docs/skills/local-dev-skaffold.md +++ b/docs/skills/local-dev-skaffold.md @@ -28,7 +28,7 @@ make dev # Start all functions as local Node processes make dev-fn # Or start just one function -node --experimental-strip-types scripts/dev.ts --only=simple-email +node --experimental-strip-types scripts/dev.ts --only=send-email ``` Functions run as local Node processes with hot-reload via `tsx --watch`. Infrastructure runs in Docker containers. @@ -41,9 +41,9 @@ Best for: testing k8s manifests, job-service integration, e2e tests. ```bash # Single function (infra + just that function) -make skaffold-dev-simple-email +make skaffold-dev-send-email # or directly: -skaffold dev -p simple-email +skaffold dev -p send-email # All functions make skaffold-dev @@ -62,8 +62,8 @@ Each function has its own Skaffold profile that deploys only the shared infrastr | Profile | What it deploys | Command | |---------|----------------|---------| -| `simple-email` | infra + simple-email | `skaffold dev -p simple-email` | -| `send-email-link` | infra + send-email-link | `skaffold dev -p send-email-link` | +| `send-email` | infra + send-email | `skaffold dev -p send-email` | +| `send-verification-link` | infra + send-verification-link | `skaffold dev -p send-verification-link` | | `local-simple` | infra + all functions | `skaffold dev -p local-simple` | | `local` | Knative + all functions | `skaffold dev -p local` | @@ -78,8 +78,8 @@ Ports are defined in each function's `handler.json` (`port` field). Check `gener | Job Service | 8080 | fixed | | PostgreSQL | 5432 | fixed | | Constructive Server | 3002 | fixed | -| simple-email | 8081 | handler.json | -| send-email-link | 8082 | handler.json | +| send-email | 8081 | handler.json | +| send-verification-link | 8082 | handler.json | ## Hot Reload @@ -109,7 +109,7 @@ All pods should be `Running` (except `constructive-db` which completes and shows kubectl logs -n constructive-functions -l app=knative-job-service -f # Specific function -kubectl logs -n constructive-functions -l app=simple-email -f +kubectl logs -n constructive-functions -l app=send-email -f # Constructive server kubectl logs -n constructive-functions -l app=constructive-server -f @@ -148,7 +148,7 @@ SELECT id, name FROM metaschema_public.database; -- Manually insert a test job SELECT * FROM app_jobs.add_job( (SELECT id FROM metaschema_public.database LIMIT 1), - 'simple-email'::text, + 'send-email'::text, '{"to":"test@example.com","subject":"test","html":"

hello

"}'::json ); ``` @@ -156,7 +156,7 @@ SELECT * FROM app_jobs.add_job( ### 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 ``` @@ -164,7 +164,7 @@ kubectl exec -it -n constructive-functions deploy/knative-job-service -- sh ```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 ``` ## Common Issues @@ -216,7 +216,7 @@ make skaffold-dev # 2. In another terminal, run a specific function's e2e test PGHOST=localhost PGPORT=5432 PGUSER=postgres PGPASSWORD="$POSTGRES_PASSWORD" PGDATABASE=constructive \ - pnpm jest tests/e2e/__tests__/simple-email.e2e.test.ts tests/e2e/__tests__/job-queue.test.ts + pnpm jest tests/e2e/__tests__/send-email.e2e.test.ts tests/e2e/__tests__/job-queue.test.ts # Or run all e2e tests pnpm test:e2e @@ -225,9 +225,9 @@ pnpm test:e2e For single-function isolation (matches CI behavior): ```bash -# Deploy only simple-email -skaffold dev -p simple-email +# Deploy only send-email +skaffold dev -p send-email # Run only its tests -pnpm jest tests/e2e/__tests__/simple-email.e2e.test.ts tests/e2e/__tests__/job-queue.test.ts +pnpm jest tests/e2e/__tests__/send-email.e2e.test.ts tests/e2e/__tests__/job-queue.test.ts ``` diff --git a/docs/spec/function-templating.md b/docs/spec/function-templating.md index 535ae9c..8288664 100644 --- a/docs/spec/function-templating.md +++ b/docs/spec/function-templating.md @@ -14,10 +14,10 @@ constructive-functions/ example/ handler.ts # Business logic handler.json # Metadata + dependencies + template type - simple-email/ + send-email/ handler.ts handler.json - send-email-link/ + send-verification-link/ handler.ts handler.json types.d.ts # Optional type declarations @@ -38,11 +38,11 @@ constructive-functions/ index.ts # Copied from template with {{name}} replaced handler.ts # Symlink -> ../../functions/example/handler.ts dist/ # tsc output - simple-email/ + send-email/ ...same structure... - send-email-link/ + send-verification-link/ ...same structure... - types.d.ts # Symlink -> ../../functions/send-email-link/types.d.ts + types.d.ts # Symlink -> ../../functions/send-verification-link/types.d.ts packages/ fn-app/ # Express app factory with job callback handling @@ -82,7 +82,7 @@ Manifest with name, version, template type, and extra npm dependencies: ```json { - "name": "send-email-link", + "name": "send-verification-link", "version": "1.1.0", "description": "Sends invite, password reset, and verification emails", "type": "node-graphql", @@ -212,10 +212,10 @@ The template Dockerfile (`templates/node-graphql/Dockerfile`) uses three stages: pnpm docker:build # Build a single function -make docker-build-send-email-link +make docker-build-send-verification-link # Build with custom tag -node --experimental-strip-types scripts/docker-build.ts --only=send-email-link --tag=abc1234 +node --experimental-strip-types scripts/docker-build.ts --only=send-verification-link --tag=abc1234 ``` Image naming: `ghcr.io/constructive-io/-fn:` @@ -248,7 +248,7 @@ make dev-down # docker compose down ```bash make docker-build # build all -make docker-build-send-email-link # build one +make docker-build-send-verification-link # build one ``` ## Data Flow @@ -297,17 +297,17 @@ PostgreSQL job status updated | Variable | Function | Description | |---|---|---| -| `SEND_EMAIL_LINK_DRY_RUN` | send-email-link | Skip actual email sending | -| `SIMPLE_EMAIL_DRY_RUN` | simple-email | Skip actual email sending | +| `SEND_VERIFICATION_LINK_DRY_RUN` | send-verification-link | Skip actual email sending (legacy `SEND_EMAIL_LINK_DRY_RUN` still honored) | +| `SEND_EMAIL_DRY_RUN` | send-email | Skip actual email sending (legacy `SIMPLE_EMAIL_DRY_RUN` still honored) | | `EMAIL_SEND_USE_SMTP` | both | Use SMTP instead of Mailgun | -| `DEFAULT_DATABASE_ID` | send-email-link | Fallback when X-Database-Id header is missing | -| `LOCAL_APP_PORT` | send-email-link | Port for localhost URLs in dry-run mode | +| `DEFAULT_DATABASE_ID` | send-verification-link | Fallback when X-Database-Id header is missing | +| `LOCAL_APP_PORT` | send-verification-link | Port for localhost URLs in dry-run mode | ## Compatibility The generated packages maintain full backward compatibility with: - **job/service `loadFunctionApp()`**: Expects module to export an app with `.listen()`. The generated `index.ts` exports exactly this via `createFunctionServer()` which wraps `createJobApp()`. -- **Package names**: `@constructive-io/simple-email-fn`, `@constructive-io/send-email-link-fn` remain unchanged. +- **Package names**: published as `@constructive-io/send-email-fn` and `@constructive-io/send-verification-link-fn` (matches upstream `constructive`). - **K8s manifests**: `node generated//dist/index.js` works as container CMD. - **Callback protocol**: fn-runtime delegates to fn-app which handles all callback logic. diff --git a/functions/simple-email/__tests__/handler.test.ts b/functions/send-email/__tests__/handler.test.ts similarity index 95% rename from functions/simple-email/__tests__/handler.test.ts rename to functions/send-email/__tests__/handler.test.ts index 720a35b..7de81e2 100644 --- a/functions/simple-email/__tests__/handler.test.ts +++ b/functions/send-email/__tests__/handler.test.ts @@ -5,15 +5,15 @@ const loadHandler = () => { return mod.default ?? mod; }; -describe('simple-email handler', () => { +describe('send-email handler', () => { beforeEach(() => { jest.resetModules(); - process.env.SIMPLE_EMAIL_DRY_RUN = 'false'; + process.env.SEND_EMAIL_DRY_RUN = 'false'; process.env.EMAIL_SEND_USE_SMTP = 'false'; }); afterEach(() => { - delete process.env.SIMPLE_EMAIL_DRY_RUN; + delete process.env.SEND_EMAIL_DRY_RUN; delete process.env.EMAIL_SEND_USE_SMTP; delete process.env.MAILGUN_FROM; delete process.env.SMTP_FROM; @@ -118,7 +118,7 @@ describe('simple-email handler', () => { describe('dry-run mode', () => { beforeEach(() => { - process.env.SIMPLE_EMAIL_DRY_RUN = 'true'; + process.env.SEND_EMAIL_DRY_RUN = 'true'; }); it('returns { complete: true } without sending', async () => { diff --git a/functions/simple-email/handler.json b/functions/send-email/handler.json similarity index 60% rename from functions/simple-email/handler.json rename to functions/send-email/handler.json index 4890c5d..61d4576 100644 --- a/functions/simple-email/handler.json +++ b/functions/send-email/handler.json @@ -1,9 +1,10 @@ { - "name": "simple-email", + "name": "send-email", "version": "1.6.4", "type": "node-graphql", "port": 8081, - "description": "Simple Knative email function that sends emails directly from job payload", + "taskIdentifier": "email:send_email", + "description": "Knative email function that sends emails directly from job payload", "dependencies": { "@constructive-io/postmaster": "^1.5.2", "@pgpmjs/env": "^2.15.3", diff --git a/functions/simple-email/handler.ts b/functions/send-email/handler.ts similarity index 85% rename from functions/simple-email/handler.ts rename to functions/send-email/handler.ts index 445a8f0..62e81ea 100644 --- a/functions/simple-email/handler.ts +++ b/functions/send-email/handler.ts @@ -1,10 +1,10 @@ import type { FunctionHandler } from '@constructive-io/fn-runtime'; -import { send as sendSmtp } from 'simple-smtp-server'; import { send as sendPostmaster } from '@constructive-io/postmaster'; import { parseEnvBoolean } from '@pgpmjs/env'; import { createLogger } from '@pgpmjs/logger'; +import { send as sendSmtp } from 'simple-smtp-server'; -type SimpleEmailPayload = { +type SendEmailPayload = { to: string; subject: string; html?: string; @@ -17,8 +17,8 @@ const isNonEmptyString = (value: unknown): value is string => typeof value === 'string' && value.trim().length > 0; const getRequiredField = ( - payload: SimpleEmailPayload, - field: keyof SimpleEmailPayload + payload: SendEmailPayload, + field: keyof SendEmailPayload ) => { const value = payload[field]; if (!isNonEmptyString(value)) { @@ -27,11 +27,14 @@ const getRequiredField = ( return value; }; -const isDryRun = parseEnvBoolean(process.env.SIMPLE_EMAIL_DRY_RUN) ?? false; +const isDryRun = + parseEnvBoolean(process.env.SEND_EMAIL_DRY_RUN) ?? + parseEnvBoolean(process.env.SIMPLE_EMAIL_DRY_RUN) ?? + false; const useSmtp = parseEnvBoolean(process.env.EMAIL_SEND_USE_SMTP) ?? false; -const logger = createLogger('simple-email'); +const logger = createLogger('send-email'); -const handler: FunctionHandler = async (params) => { +const handler: FunctionHandler = async (params) => { const to = getRequiredField(params, 'to'); const subject = getRequiredField(params, 'subject'); diff --git a/functions/send-email-link/__tests__/handler.graphql.test.ts b/functions/send-verification-link/__tests__/handler.graphql.test.ts similarity index 85% rename from functions/send-email-link/__tests__/handler.graphql.test.ts rename to functions/send-verification-link/__tests__/handler.graphql.test.ts index f305671..db6fc44 100644 --- a/functions/send-email-link/__tests__/handler.graphql.test.ts +++ b/functions/send-verification-link/__tests__/handler.graphql.test.ts @@ -1,5 +1,5 @@ /** - * GraphQL integration tests for send-email-link handler. + * GraphQL integration tests for send-verification-link handler. * * These tests use @constructive-io/graphql-test to run against * a real PostGraphile + Postgres instance. They require: @@ -12,7 +12,7 @@ * pnpm add -D @constructive-io/graphql-test pgsql-test */ -describe('send-email-link handler (GraphQL integration)', () => { +describe('send-verification-link handler (GraphQL integration)', () => { it.todo('invite_email fetches inviter and sends email with correct URL'); it.todo('forgot_password constructs correct reset URL with tokens'); it.todo('email_verification constructs correct verify URL'); diff --git a/functions/send-email-link/__tests__/handler.test.ts b/functions/send-verification-link/__tests__/handler.test.ts similarity index 96% rename from functions/send-email-link/__tests__/handler.test.ts rename to functions/send-verification-link/__tests__/handler.test.ts index 35412c1..610839d 100644 --- a/functions/send-email-link/__tests__/handler.test.ts +++ b/functions/send-verification-link/__tests__/handler.test.ts @@ -1,6 +1,6 @@ import { createMockContext } from '../../../tests/helpers/mock-context'; -describe('send-email-link handler (validation)', () => { +describe('send-verification-link handler (validation)', () => { let handler: any; beforeEach(() => { diff --git a/functions/send-email-link/handler.json b/functions/send-verification-link/handler.json similarity index 83% rename from functions/send-email-link/handler.json rename to functions/send-verification-link/handler.json index 6b29a54..91b6561 100644 --- a/functions/send-email-link/handler.json +++ b/functions/send-verification-link/handler.json @@ -1,8 +1,9 @@ { - "name": "send-email-link", + "name": "send-verification-link", "version": "2.6.4", "type": "node-graphql", "port": 8082, + "taskIdentifier": "email:send_verification_link", "description": "Sends invite, password reset, and verification emails", "dependencies": { "@constructive-io/postmaster": "^1.5.2", diff --git a/functions/send-email-link/handler.ts b/functions/send-verification-link/handler.ts similarity index 97% rename from functions/send-email-link/handler.ts rename to functions/send-verification-link/handler.ts index bf44223..4e81bd7 100644 --- a/functions/send-email-link/handler.ts +++ b/functions/send-verification-link/handler.ts @@ -1,10 +1,10 @@ import type { FunctionHandler } from '@constructive-io/fn-runtime'; +import { send as sendPostmaster } from '@constructive-io/postmaster'; +import { generate } from '@launchql/mjml'; +import { parseEnvBoolean } from '@pgpmjs/env'; import type { GraphQLClient } from 'graphql-request'; import gql from 'graphql-tag'; -import { generate } from '@launchql/mjml'; -import { send as sendPostmaster } from '@constructive-io/postmaster'; import { send as sendSmtp } from 'simple-smtp-server'; -import { parseEnvBoolean } from '@pgpmjs/env'; // Use plural connections with the `where` filter (graphile-connection-filter) // rather than `condition:` or singular-by-PK. The constructive server's @@ -83,7 +83,10 @@ const sendEmailLink = async ( context: SendEmailContext ) => { const { client, meta, databaseId, env, log } = context; - const isDryRun = parseEnvBoolean(env.SEND_EMAIL_LINK_DRY_RUN) ?? false; + const isDryRun = + parseEnvBoolean(env.SEND_VERIFICATION_LINK_DRY_RUN) ?? + parseEnvBoolean(env.SEND_EMAIL_LINK_DRY_RUN) ?? + false; const useSmtp = parseEnvBoolean(env.EMAIL_SEND_USE_SMTP) ?? false; const validateForType = (): { missing?: string } | null => { @@ -290,7 +293,7 @@ const handler: FunctionHandler = async (params, context) => { return { error: 'Missing X-Database-Id header or DEFAULT_DATABASE_ID' }; } - log.info('[send-email-link] Processing request', { + log.info('[send-verification-link] Processing request', { email_type: params.email_type, databaseId }); diff --git a/functions/send-email-link/types.d.ts b/functions/send-verification-link/types.d.ts similarity index 100% rename from functions/send-email-link/types.d.ts rename to functions/send-verification-link/types.d.ts diff --git a/job/service/package.json b/job/service/package.json index da1517c..888935b 100644 --- a/job/service/package.json +++ b/job/service/package.json @@ -22,8 +22,8 @@ "@constructive-io/knative-job-fn": "workspace:^", "@constructive-io/knative-job-server": "workspace:^", "@constructive-io/knative-job-worker": "workspace:^", - "@constructive-io/send-email-link-fn": "^2.6.4", - "@constructive-io/simple-email-fn": "^1.6.4", + "@constructive-io/send-email-fn": "workspace:^", + "@constructive-io/send-verification-link-fn": "workspace:^", "@pgpmjs/env": "^2.15.3", "@pgpmjs/logger": "^2.4.3", "async-retry": "1.3.3", diff --git a/job/service/src/index.ts b/job/service/src/index.ts index 7ebd3f6..f85d5b4 100644 --- a/job/service/src/index.ts +++ b/job/service/src/index.ts @@ -17,6 +17,10 @@ import type { Server as HttpServer } from 'http'; import { createRequire } from 'module'; import { Client } from 'pg'; +import { + FunctionRegistryEntry, + loadFunctionRegistry +} from './registry'; import { FunctionName, FunctionServiceConfig, @@ -25,10 +29,6 @@ import { KnativeJobsSvcResult, StartedFunction } from './types'; -import { - FunctionRegistryEntry, - loadFunctionRegistry -} from './registry'; const functionRegistry = loadFunctionRegistry(); diff --git a/job/service/src/registry.ts b/job/service/src/registry.ts index 1a129fc..249e373 100644 --- a/job/service/src/registry.ts +++ b/job/service/src/registry.ts @@ -4,7 +4,7 @@ * Sources, in priority order: * 1. FUNCTIONS_REGISTRY env var * Format: "name:moduleName:port,..." (port optional) - * Example: "simple-email:@org/simple-email-fn:8081,foo:@org/foo-fn" + * Example: "send-email:@org/send-email-fn:8081,foo:@org/foo-fn" * 2. FUNCTIONS_MANIFEST_PATH env var pointing to a JSON file with shape * { functions: [{ name, dir, port, type, moduleName? }] } * 3. Default file: /generated/functions-manifest.json diff --git a/k8s/ARCHITECTURE.md b/k8s/ARCHITECTURE.md index f335b19..16fb63b 100644 --- a/k8s/ARCHITECTURE.md +++ b/k8s/ARCHITECTURE.md @@ -20,7 +20,7 @@ This document summarizes the Kubernetes layout under `k8s/`, highlights strength - `dashboard` (Next.js UI) — dev/staging. - `pgadmin` (DB admin UI) — dev/staging/local. - `knative-job-service` (job orchestration for functions). - - Function workloads (`simple-email`, `send-email-link`, `knative-job-example`, `python-example`) — Knative Services in `local`/`dev`/`staging`, plain Deployments in `local-simple`. + - Function workloads (`send-email`, `send-verification-link`, `knative-job-example`, `python-example`) — Knative Services in `local`/`dev`/`staging`, plain Deployments in `local-simple`. The repo-root `Makefile` wires this together via `make skaffold-dev` / `make skaffold-dev-knative` (local), and `k8s/scripts/setup` plus the kustomize overlays for remote environments. @@ -115,7 +115,7 @@ This is what `make skaffold-dev` deploys. ### Function workloads -- `base/functions/simple-email.yaml`, `base/functions/send-email-link.yaml`: +- `base/functions/send-email.yaml`, `base/functions/send-verification-link.yaml`: - Knative Service in the overlay's app namespace (`constructive-functions` for `local`, `interweb` for `dev`/`staging`). - Runs `node generated//dist/index.js` from the constructive-functions image. - Uses Mailgun credentials from `mailgun-credentials` secret. @@ -130,7 +130,7 @@ This is what `make skaffold-dev` deploys. - DB config from `constructive` ConfigMap and `pg-credentials` secret. - Job configuration: - `JOBS_SUPPORT_ANY=false`. - - `JOBS_SUPPORTED=simple-email`. + - `JOBS_SUPPORTED=email:send_email`. - Internal callback URL: - `INTERNAL_JOBS_CALLBACK_URL=http://knative-job-service..svc.cluster.local:8080` (namespace varies by overlay). - Knative gateway targets: @@ -213,7 +213,7 @@ For `local-simple` (the recommended laptop path), use `make skaffold-dev` from t - Enable CNPG backup resources for object‑storage snapshots. 3. **Refine Knative job routing** - - Standardize on calling `kourier-internal.kourier-system.svc.cluster.local` and distributing requests using the `Host` header set to the Knative Service URL (e.g. `simple-email.interweb.svc.cluster.local`). + - Standardize on calling `kourier-internal.kourier-system.svc.cluster.local` and distributing requests using the `Host` header set to the Knative Service URL (e.g. `send-email.interweb.svc.cluster.local`). - Avoid hard‑coding revision names in env vars. 4. **Ingress & secrets hygiene** diff --git a/k8s/SECRETS-ARCHITECTURE.md b/k8s/SECRETS-ARCHITECTURE.md index 25bcd24..d49a78b 100644 --- a/k8s/SECRETS-ARCHITECTURE.md +++ b/k8s/SECRETS-ARCHITECTURE.md @@ -91,7 +91,7 @@ The **secret values never enter the meta tables**; only references + metadata do ### 3. Jobs / Functions (e.g. Knative) -Jobs and functions (like `simple-email`) use secrets **just‑in‑time**: +Jobs and functions (like `send-email`) use secrets **just‑in‑time**: - For email functions: 1. The job runner (e.g. `knative-job-service`) looks up the app’s `mailgun` secret metadata via API/DB. diff --git a/k8s/base/constructive/knative-job-service.yaml b/k8s/base/constructive/knative-job-service.yaml index dc27129..3cf2fb4 100644 --- a/k8s/base/constructive/knative-job-service.yaml +++ b/k8s/base/constructive/knative-job-service.yaml @@ -48,7 +48,7 @@ spec: - name: JOBS_SUPPORT_ANY value: "false" - name: JOBS_SUPPORTED - value: "simple-email,send-email-link" + value: "email:send_email,email:send_verification_link" - name: JOBS_CALLBACK_HOST value: "knative-job-service.interweb.svc.cluster.local" - name: JOBS_CALLBACK_BASE_URL @@ -58,13 +58,13 @@ spec: - name: KNATIVE_SERVICE_URL value: "interweb.svc.cluster.local" - name: INTERNAL_GATEWAY_URL - value: "http://simple-email.interweb.svc.cluster.local" - + value: "http://send-verification-link.interweb.svc.cluster.local" + - name: NODE_ENV value: "development" - name: INTERNAL_GATEWAY_DEVELOPMENT_MAP - value: '{"simple-email":"http://simple-email.interweb.svc.cluster.local","send-email-link":"http://send-email-link.interweb.svc.cluster.local"}' + value: '{"email:send_email":"http://send-email.interweb.svc.cluster.local","email:send_verification_link":"http://send-verification-link.interweb.svc.cluster.local"}' # Optional: stable hostname for logging/coordination - name: HOSTNAME diff --git a/k8s/base/functions/simple-email.yaml b/k8s/base/functions/send-email.yaml similarity index 91% rename from k8s/base/functions/simple-email.yaml rename to k8s/base/functions/send-email.yaml index 47ff1ed..092fd31 100644 --- a/k8s/base/functions/simple-email.yaml +++ b/k8s/base/functions/send-email.yaml @@ -1,9 +1,9 @@ apiVersion: serving.knative.dev/v1 kind: Service metadata: - name: simple-email + name: send-email labels: - app.kubernetes.io/name: simple-email + app.kubernetes.io/name: send-email app.kubernetes.io/component: function app.kubernetes.io/part-of: constructive-jobs networking.knative.dev/visibility: cluster-local @@ -11,7 +11,7 @@ spec: template: metadata: labels: - app.kubernetes.io/name: simple-email + app.kubernetes.io/name: send-email app.kubernetes.io/component: function app.kubernetes.io/part-of: constructive-jobs annotations: @@ -30,7 +30,7 @@ spec: imagePullPolicy: Always command: ["node"] - args: ["functions/simple-email/dist/index.js"] + args: ["functions/send-email/dist/index.js"] ports: - containerPort: 8080 diff --git a/k8s/base/functions/send-email-link.yaml b/k8s/base/functions/send-verification-link.yaml similarity index 90% rename from k8s/base/functions/send-email-link.yaml rename to k8s/base/functions/send-verification-link.yaml index 1c5a106..b36c0ba 100644 --- a/k8s/base/functions/send-email-link.yaml +++ b/k8s/base/functions/send-verification-link.yaml @@ -1,9 +1,9 @@ apiVersion: serving.knative.dev/v1 kind: Service metadata: - name: send-email-link + name: send-verification-link labels: - app.kubernetes.io/name: send-email-link + app.kubernetes.io/name: send-verification-link app.kubernetes.io/component: function app.kubernetes.io/part-of: constructive-jobs networking.knative.dev/visibility: cluster-local @@ -11,7 +11,7 @@ spec: template: metadata: labels: - app.kubernetes.io/name: send-email-link + app.kubernetes.io/name: send-verification-link app.kubernetes.io/component: function app.kubernetes.io/part-of: constructive-jobs annotations: @@ -30,7 +30,7 @@ spec: imagePullPolicy: Always command: ["node"] - args: ["functions/send-email-link/dist/index.js"] + args: ["functions/send-verification-link/dist/index.js"] ports: - containerPort: 8080 diff --git a/k8s/base/kustomization.yaml b/k8s/base/kustomization.yaml index eae9b4f..d7c2eec 100644 --- a/k8s/base/kustomization.yaml +++ b/k8s/base/kustomization.yaml @@ -21,8 +21,8 @@ resources: - ./constructive/knative-job-service.yaml # Function workloads - - ./functions/simple-email.yaml - - ./functions/send-email-link.yaml + - ./functions/send-email.yaml + - ./functions/send-verification-link.yaml # Required Secrets are intentionally not committed in the shared base. Create # pg-credentials, postgres-superuser, pgadmin-credentials, mailgun-credentials, diff --git a/k8s/overlays/ci/delete-simple-email.yaml b/k8s/overlays/ci/delete-send-email.yaml similarity index 77% rename from k8s/overlays/ci/delete-simple-email.yaml rename to k8s/overlays/ci/delete-send-email.yaml index 36e1cda..dfc0821 100644 --- a/k8s/overlays/ci/delete-simple-email.yaml +++ b/k8s/overlays/ci/delete-send-email.yaml @@ -1,5 +1,5 @@ apiVersion: serving.knative.dev/v1 kind: Service metadata: - name: simple-email + name: send-email $patch: delete diff --git a/k8s/overlays/ci/kustomization.yaml b/k8s/overlays/ci/kustomization.yaml index 640a92b..ad82ab9 100644 --- a/k8s/overlays/ci/kustomization.yaml +++ b/k8s/overlays/ci/kustomization.yaml @@ -30,4 +30,4 @@ patches: version: v1 kind: Deployment name: minio - # constructive-server-admin and simple-email are kept running for e2e tests + # constructive-server-admin and send-email are kept running for e2e tests diff --git a/k8s/overlays/local-simple/config.yaml b/k8s/overlays/local-simple/config.yaml index a31f2d9..c54f2c0 100644 --- a/k8s/overlays/local-simple/config.yaml +++ b/k8s/overlays/local-simple/config.yaml @@ -22,5 +22,5 @@ data: LOG_TIMESTAMP: "true" - SIMPLE_EMAIL_DRY_RUN: "true" - SEND_EMAIL_LINK_DRY_RUN: "true" + SEND_EMAIL_DRY_RUN: "true" + SEND_VERIFICATION_LINK_DRY_RUN: "true" diff --git a/k8s/overlays/local/constructive/knative-job-service.yaml b/k8s/overlays/local/constructive/knative-job-service.yaml index 34d7ad7..7dcbc4c 100644 --- a/k8s/overlays/local/constructive/knative-job-service.yaml +++ b/k8s/overlays/local/constructive/knative-job-service.yaml @@ -19,9 +19,9 @@ spec: - name: JOBS_CALLBACK_BASE_URL value: "http://knative-job-service.constructive-functions.svc.cluster.local:8080/callback" - name: INTERNAL_GATEWAY_URL - value: "http://simple-email.constructive-functions.svc.cluster.local" + value: "http://send-verification-link.constructive-functions.svc.cluster.local" - name: INTERNAL_GATEWAY_DEVELOPMENT_MAP - value: '{"simple-email":"http://simple-email.constructive-functions.svc.cluster.local","send-email-link":"http://send-email-link.constructive-functions.svc.cluster.local"}' + value: '{"email:send_email":"http://send-email.constructive-functions.svc.cluster.local","email:send_verification_link":"http://send-verification-link.constructive-functions.svc.cluster.local"}' resources: requests: cpu: "50m" diff --git a/k8s/overlays/local/kustomization.yaml b/k8s/overlays/local/kustomization.yaml index bdd8f68..35f58af 100644 --- a/k8s/overlays/local/kustomization.yaml +++ b/k8s/overlays/local/kustomization.yaml @@ -24,8 +24,8 @@ resources: - ../../base/constructive/dashboard.yaml - ../../base/constructive/knative-job-service.yaml # Functions - - ../../base/functions/simple-email.yaml - - ../../base/functions/send-email-link.yaml + - ../../base/functions/send-email.yaml + - ../../base/functions/send-verification-link.yaml patches: # Skaffold image replacement: only rewrite function images, not server/dashboard/db-job @@ -37,7 +37,7 @@ patches: group: serving.knative.dev version: v1 kind: Service - name: simple-email + name: send-email - patch: |- - op: replace path: /spec/template/spec/containers/0/image @@ -46,7 +46,7 @@ patches: group: serving.knative.dev version: v1 kind: Service - name: send-email-link + name: send-verification-link - patch: |- - op: replace path: /spec/template/spec/containers/0/image @@ -68,21 +68,21 @@ patches: - op: add path: /spec/template/spec/containers/0/env/- value: - name: SIMPLE_EMAIL_DRY_RUN + name: SEND_EMAIL_DRY_RUN value: "true" target: group: serving.knative.dev version: v1 kind: Service - name: simple-email + name: send-email - patch: |- - op: add path: /spec/template/spec/containers/0/env/- value: - name: SEND_EMAIL_LINK_DRY_RUN + name: SEND_VERIFICATION_LINK_DRY_RUN value: "true" target: group: serving.knative.dev version: v1 kind: Service - name: send-email-link + name: send-verification-link diff --git a/packages/fn-client/__tests__/client.test.ts b/packages/fn-client/__tests__/client.test.ts index dcae14f..1a1dbdc 100644 --- a/packages/fn-client/__tests__/client.test.ts +++ b/packages/fn-client/__tests__/client.test.ts @@ -22,8 +22,8 @@ describe('FnClient', () => { const client = new FnClient({ rootDir: tmpRoot }); const fns = client.discover(); const names = fns.map((f) => f.name); - expect(names).toContain('simple-email'); - expect(names).toContain('send-email-link'); + expect(names).toContain('send-email'); + expect(names).toContain('send-verification-link'); expect(fns.every((f) => f.port > 0 && f.port !== 8080)).toBe(true); }); diff --git a/packages/fn-generator/README.md b/packages/fn-generator/README.md index edfc126..1dd881e 100644 --- a/packages/fn-generator/README.md +++ b/packages/fn-generator/README.md @@ -25,7 +25,7 @@ const generator = new FnGenerator({ // One-shot generator.generate(); // all functions -generator.generate({ only: 'simple-email' }); // single +generator.generate({ only: 'send-email' }); // single generator.generate({ packagesOnly: true }); // skip k8s/skaffold // Or assemble manifests yourself diff --git a/packages/fn-generator/src/builders/configmap.ts b/packages/fn-generator/src/builders/configmap.ts index 079eae7..09b3dbe 100644 --- a/packages/fn-generator/src/builders/configmap.ts +++ b/packages/fn-generator/src/builders/configmap.ts @@ -18,9 +18,11 @@ export const buildConfigMap = (args: { namespace: string; }): Manifest => { const targetFns = args.target ? [args.target] : args.fns; + const taskId = (fn: FunctionInfo): string => + (fn.manifest.taskIdentifier as string | undefined) ?? fn.name; const gatewayMap: Record = {}; for (const fn of targetFns) { - gatewayMap[fn.name] = `http://${fn.name}.${args.namespace}.svc.cluster.local`; + gatewayMap[taskId(fn)] = `http://${fn.name}.${args.namespace}.svc.cluster.local`; } const template = fs.readFileSync( @@ -28,7 +30,7 @@ export const buildConfigMap = (args: { 'utf-8' ); const yaml = renderTemplate(template, { - jobs_supported: targetFns.map((fn) => fn.name).join(','), + jobs_supported: targetFns.map(taskId).join(','), gateway_map: JSON.stringify(gatewayMap), }); diff --git a/packages/fn-generator/src/builders/manifest.ts b/packages/fn-generator/src/builders/manifest.ts index f64b56f..0344606 100644 --- a/packages/fn-generator/src/builders/manifest.ts +++ b/packages/fn-generator/src/builders/manifest.ts @@ -10,12 +10,16 @@ export const buildManifestJson = ( outputDir: string ): Manifest => { const data = { - functions: fns.map((fn) => ({ - name: fn.name, - dir: fn.dir, - port: fn.port, - type: fn.type, - })), + functions: fns.map((fn) => { + const taskIdentifier = fn.manifest.taskIdentifier as string | undefined; + return { + name: fn.name, + dir: fn.dir, + port: fn.port, + type: fn.type, + ...(taskIdentifier ? { taskIdentifier } : {}), + }; + }), }; return { kind: 'file', diff --git a/packages/fn-types/src/index.ts b/packages/fn-types/src/index.ts index 14433b4..942be5f 100644 --- a/packages/fn-types/src/index.ts +++ b/packages/fn-types/src/index.ts @@ -1,21 +1,17 @@ export type { - FunctionHandler, - FunctionContext, - FunctionLogger, - ServerOptions -} from './runtime'; - -export type { HandlerManifest } from './manifest'; - -export type { FnRegistry, FnRegistryEntry } from './registry'; - -export type { + DockerOptions, FnConfig, FnPreset, - K8sTarget, K8sOptions, K8sResourceQuantities, - DockerOptions + K8sTarget } from './config'; - export { defineConfig } from './config'; +export type { HandlerManifest } from './manifest'; +export type { FnRegistry, FnRegistryEntry } from './registry'; +export type { + FunctionContext, + FunctionHandler, + FunctionLogger, + ServerOptions +} from './runtime'; diff --git a/packages/fn-types/src/manifest.ts b/packages/fn-types/src/manifest.ts index 721e69f..f58227a 100644 --- a/packages/fn-types/src/manifest.ts +++ b/packages/fn-types/src/manifest.ts @@ -6,6 +6,11 @@ export interface HandlerManifest { type?: string; /** HTTP port the function listens on. Auto-assigned if omitted. */ port?: number; + /** + * Task identifier used by the job worker to dispatch this function + * (e.g. "email:send_verification_link"). Defaults to `name` when omitted. + */ + taskIdentifier?: string; /** Extra dependencies merged into the generated package's package.json. */ dependencies?: Record; [key: string]: unknown; diff --git a/packages/fn-types/src/registry.ts b/packages/fn-types/src/registry.ts index c10c0e3..ddce86b 100644 --- a/packages/fn-types/src/registry.ts +++ b/packages/fn-types/src/registry.ts @@ -1,5 +1,5 @@ export interface FnRegistryEntry { - /** Function name from handler.json (e.g., 'simple-email', 'knative-job-example'). */ + /** Function name from handler.json (e.g., 'send-email', 'knative-job-example'). */ name: string; /** Directory under functions/ containing the source. May differ from `name`. */ dir: string; @@ -7,6 +7,8 @@ export interface FnRegistryEntry { port: number; /** Template type (e.g., 'node-graphql', 'python'). */ type: string; + /** Task identifier used by the job worker (defaults to `name` when omitted). */ + taskIdentifier?: string; /** npm-style module ID for dynamic require (in-process dispatch, optional). */ moduleName?: string; /** HTTP URL for remote dispatch (cluster job-service, optional). */ diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index dba77bb..661e94a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -73,7 +73,7 @@ importers: specifier: ^5.1.6 version: 5.9.3 - generated/send-email-link: + generated/send-email: dependencies: '@constructive-io/fn-runtime': specifier: workspace:^ @@ -81,24 +81,12 @@ importers: '@constructive-io/postmaster': specifier: ^1.5.2 version: 1.6.2 - '@launchql/mjml': - specifier: 0.1.1 - version: 0.1.1(@babel/core@7.28.5)(react-dom@16.14.0(react@16.14.0))(react-is@18.3.1)(react@16.14.0) - '@launchql/styled-email': - specifier: 0.1.0 - version: 0.1.0(@babel/core@7.28.5)(react-dom@16.14.0(react@16.14.0))(react-is@18.3.1)(react@16.14.0) '@pgpmjs/env': specifier: ^2.15.3 version: 2.17.0 '@pgpmjs/logger': specifier: ^2.4.3 version: 2.5.2 - graphql-request: - specifier: ^7.1.2 - version: 7.4.0(graphql@16.12.0) - graphql-tag: - specifier: ^2.12.6 - version: 2.12.6(graphql@16.12.0) simple-smtp-server: specifier: ^0.7.3 version: 0.7.3 @@ -113,7 +101,7 @@ importers: specifier: ^5.1.6 version: 5.9.3 - generated/simple-email: + generated/send-verification-link: dependencies: '@constructive-io/fn-runtime': specifier: workspace:^ @@ -121,12 +109,24 @@ importers: '@constructive-io/postmaster': specifier: ^1.5.2 version: 1.6.2 + '@launchql/mjml': + specifier: 0.1.1 + version: 0.1.1(@babel/core@7.28.5)(react-dom@16.14.0(react@16.14.0))(react-is@18.3.1)(react@16.14.0) + '@launchql/styled-email': + specifier: 0.1.0 + version: 0.1.0(@babel/core@7.28.5)(react-dom@16.14.0(react@16.14.0))(react-is@18.3.1)(react@16.14.0) '@pgpmjs/env': specifier: ^2.15.3 version: 2.17.0 '@pgpmjs/logger': specifier: ^2.4.3 version: 2.5.2 + graphql-request: + specifier: ^7.1.2 + version: 7.4.0(graphql@16.12.0) + graphql-tag: + specifier: ^2.12.6 + version: 2.12.6(graphql@16.12.0) simple-smtp-server: specifier: ^0.7.3 version: 0.7.3 @@ -198,12 +198,12 @@ importers: '@constructive-io/knative-job-worker': specifier: workspace:^ version: link:../worker - '@constructive-io/send-email-link-fn': - specifier: ^2.6.4 - version: 2.6.4(@babel/core@7.28.5)(graphql@16.12.0)(react-dom@16.14.0(react@16.14.0))(react@16.14.0) - '@constructive-io/simple-email-fn': - specifier: ^1.6.4 - version: 1.6.4 + '@constructive-io/send-email-fn': + specifier: workspace:^ + version: link:../../generated/send-email + '@constructive-io/send-verification-link-fn': + specifier: workspace:^ + version: link:../../generated/send-verification-link '@pgpmjs/env': specifier: ^2.15.3 version: 2.17.0 @@ -599,18 +599,9 @@ packages: '@constructive-io/job-utils@2.5.4': resolution: {integrity: sha512-QyLMb1TS+PDlrelO223x8GOqYXtuGdAHYsL+pXq6e0vIH45xNkUcBcl/yQhz4Ey1shBbTVsV2Dc21wmkxY1RIg==} - '@constructive-io/knative-job-fn@1.5.2': - resolution: {integrity: sha512-aS/5+F48njTBGdiv3RBDvnGK/aK7pidKgywkWd0Cpwl35SORZ+cDhARPdioFWABxQcq/Kc/o9mF6zBriIUUEGA==} - '@constructive-io/postmaster@1.6.2': resolution: {integrity: sha512-trZQHFxPRdgaHUCKMIVhMFi59XTTQxuZ31AE1/BaE1onJsm24rmh1ewzNfe45IeAKWRjSSu7JvzCX6gVlR+8bA==} - '@constructive-io/send-email-link-fn@2.6.4': - resolution: {integrity: sha512-NuQ2AXY33bs9CNgZ2nsfZ4FMtsYyorJOiENLk3dUg/oVdTKFNQflkVeImzGPbHGI/6mfDYwavTeLKwS8ZS/CLg==} - - '@constructive-io/simple-email-fn@1.6.4': - resolution: {integrity: sha512-vRRqgmGkVmwVarbrfC9nZfMLGKCUYqY5dxpDY4uafK4eoBW7MwvWHeBBjIJ9ELUojKpzdSs0bTC7anVbTx0Hyw==} - '@emnapi/core@1.8.1': resolution: {integrity: sha512-AvT9QFpxK0Zd8J0jopedNm+w/2fIzvtPKPjqyw9jwvBaReTTqPBk9Hixaz7KbjimP+QNz605/XnjFcDAL2pqBg==} @@ -1275,6 +1266,7 @@ packages: '@ungap/structured-clone@1.3.0': resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==} + deprecated: Potential CWE-502 - Update to 1.3.1 or higher '@unrs/resolver-binding-android-arm-eabi@1.11.1': resolution: {integrity: sha512-ppLRUgHVaGRWUx0R0Ut06Mjo9gBaBkg3v/8AxusGLhsIotbBLuRk51rAzqLC8gq6NyyAojEXglNjzf6R948DNw==} @@ -3330,9 +3322,6 @@ packages: simple-smtp-server@0.7.3: resolution: {integrity: sha512-Bd+pIE/NJzz1+ZdMoWz4B1Z2kyatxwi3p3UVKdkL1n5+TPlJGMbnnuqtKnV3+JLPv9gh0xeskHiHo7WYQMAYzg==} - simple-smtp-server@0.8.4: - resolution: {integrity: sha512-5g3MKQAGqO4+YqEtt6wD0KFSCdilZXysoB0dgS0tKYk6c19giQlmuwzS3lNu9FgZI7ZeKv7smT+FlJQIq/M7gQ==} - sisteransi@1.0.5: resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==} @@ -3913,13 +3902,6 @@ snapshots: transitivePeerDependencies: - pg-native - '@constructive-io/knative-job-fn@1.5.2': - dependencies: - '@pgpmjs/logger': 2.5.2 - express: 5.2.1 - transitivePeerDependencies: - - supports-color - '@constructive-io/postmaster@1.6.2': dependencies: 12factor-env: 1.6.2 @@ -3928,38 +3910,6 @@ snapshots: transitivePeerDependencies: - debug - '@constructive-io/send-email-link-fn@2.6.4(@babel/core@7.28.5)(graphql@16.12.0)(react-dom@16.14.0(react@16.14.0))(react@16.14.0)': - dependencies: - '@constructive-io/knative-job-fn': 1.5.2 - '@constructive-io/postmaster': 1.6.2 - '@launchql/mjml': 0.1.1(@babel/core@7.28.5)(react-dom@16.14.0(react@16.14.0))(react@16.14.0) - '@launchql/styled-email': 0.1.0(@babel/core@7.28.5)(react-dom@16.14.0(react@16.14.0))(react@16.14.0) - '@pgpmjs/env': 2.17.0 - '@pgpmjs/logger': 2.5.2 - graphql-request: 7.4.0(graphql@16.12.0) - graphql-tag: 2.12.6(graphql@16.12.0) - simple-smtp-server: 0.8.4 - transitivePeerDependencies: - - '@babel/core' - - debug - - encoding - - graphql - - react - - react-dom - - react-is - - supports-color - - '@constructive-io/simple-email-fn@1.6.4': - dependencies: - '@constructive-io/knative-job-fn': 1.5.2 - '@constructive-io/postmaster': 1.6.2 - '@pgpmjs/env': 2.17.0 - '@pgpmjs/logger': 2.5.2 - simple-smtp-server: 0.8.4 - transitivePeerDependencies: - - debug - - supports-color - '@emnapi/core@1.8.1': dependencies: '@emnapi/wasi-threads': 1.1.0 @@ -4526,20 +4476,6 @@ snapshots: - encoding - react-is - '@launchql/mjml@0.1.1(@babel/core@7.28.5)(react-dom@16.14.0(react@16.14.0))(react@16.14.0)': - dependencies: - '@babel/runtime': 7.28.4 - mjml: 4.7.1 - mjml-react: 1.0.59(mjml@4.7.1)(react-dom@16.14.0(react@16.14.0))(react@16.14.0) - react: 16.14.0 - react-dom: 16.14.0(react@16.14.0) - styled-components: 5.3.11(@babel/core@7.28.5)(react-dom@16.14.0(react@16.14.0))(react@16.14.0) - styled-system: 5.1.5 - transitivePeerDependencies: - - '@babel/core' - - encoding - - react-is - '@launchql/styled-email@0.1.0(@babel/core@7.28.5)(react-dom@16.14.0(react@16.14.0))(react-is@18.3.1)(react@16.14.0)': dependencies: '@babel/runtime': 7.28.4 @@ -4553,19 +4489,6 @@ snapshots: - encoding - react-is - '@launchql/styled-email@0.1.0(@babel/core@7.28.5)(react-dom@16.14.0(react@16.14.0))(react@16.14.0)': - dependencies: - '@babel/runtime': 7.28.4 - juice: 7.0.0 - react: 16.14.0 - react-dom: 16.14.0(react@16.14.0) - styled-components: 5.3.11(@babel/core@7.28.5)(react-dom@16.14.0(react@16.14.0))(react@16.14.0) - styled-system: 5.1.5 - transitivePeerDependencies: - - '@babel/core' - - encoding - - react-is - '@napi-rs/wasm-runtime@0.2.12': dependencies: '@emnapi/core': 1.8.1 @@ -5063,18 +4986,6 @@ snapshots: - '@babel/core' - supports-color - babel-plugin-styled-components@2.1.4(@babel/core@7.28.5)(styled-components@5.3.11(@babel/core@7.28.5)(react-dom@16.14.0(react@16.14.0))(react@16.14.0))(supports-color@5.5.0): - dependencies: - '@babel/helper-annotate-as-pure': 7.27.3 - '@babel/helper-module-imports': 7.27.1(supports-color@5.5.0) - '@babel/plugin-syntax-jsx': 7.27.1(@babel/core@7.28.5) - lodash: 4.17.21 - picomatch: 2.3.1 - styled-components: 5.3.11(@babel/core@7.28.5)(react-dom@16.14.0(react@16.14.0))(react@16.14.0) - transitivePeerDependencies: - - '@babel/core' - - supports-color - babel-preset-current-node-syntax@1.2.0(@babel/core@7.28.5): dependencies: '@babel/core': 7.28.5 @@ -7580,12 +7491,6 @@ snapshots: '@pgpmjs/types': 2.21.0 nodemailer: 6.10.1 - simple-smtp-server@0.8.4: - dependencies: - '@pgpmjs/env': 2.17.0 - '@pgpmjs/types': 2.21.0 - nodemailer: 6.10.1 - sisteransi@1.0.5: {} slash@3.0.0: {} @@ -7664,23 +7569,6 @@ snapshots: transitivePeerDependencies: - '@babel/core' - styled-components@5.3.11(@babel/core@7.28.5)(react-dom@16.14.0(react@16.14.0))(react@16.14.0): - dependencies: - '@babel/helper-module-imports': 7.27.1(supports-color@5.5.0) - '@babel/traverse': 7.28.5(supports-color@5.5.0) - '@emotion/is-prop-valid': 1.4.0 - '@emotion/stylis': 0.8.5 - '@emotion/unitless': 0.7.5 - babel-plugin-styled-components: 2.1.4(@babel/core@7.28.5)(styled-components@5.3.11(@babel/core@7.28.5)(react-dom@16.14.0(react@16.14.0))(react@16.14.0))(supports-color@5.5.0) - css-to-react-native: 3.2.0 - hoist-non-react-statics: 3.3.2 - react: 16.14.0 - react-dom: 16.14.0(react@16.14.0) - shallowequal: 1.1.0 - supports-color: 5.5.0 - transitivePeerDependencies: - - '@babel/core' - styled-system@5.1.5: dependencies: '@styled-system/background': 5.1.2 diff --git a/scripts/dev.ts b/scripts/dev.ts index b4d3e83..f7f6177 100644 --- a/scripts/dev.ts +++ b/scripts/dev.ts @@ -3,7 +3,7 @@ // // Usage: // node --experimental-strip-types scripts/dev.ts -// node --experimental-strip-types scripts/dev.ts --only=send-email-link +// node --experimental-strip-types scripts/dev.ts --only=send-verification-link const fs = require('fs') as typeof import('fs'); const path = require('path') as typeof import('path'); @@ -17,6 +17,7 @@ interface FunctionEntry { name: string; dir: string; port: number; + taskIdentifier?: string; } interface FunctionsManifest { @@ -60,7 +61,8 @@ const sharedEnv: Record = { SMTP_HOST: 'localhost', SMTP_PORT: '1025', LOCAL_APP_PORT: '3000', - SEND_EMAIL_LINK_DRY_RUN: 'true', + SEND_VERIFICATION_LINK_DRY_RUN: 'true', + SEND_EMAIL_DRY_RUN: 'true', }; // --- Process definitions (built from manifest) --- @@ -93,15 +95,16 @@ const onlyName: string | undefined = onlyArg?.split('=')[1]; // --- Job-service specific env (built from manifest) --- function getJobServiceEnv(): Record { + const taskId = (fn: FunctionEntry): string => fn.taskIdentifier ?? fn.name; const gatewayMap: Record = {}; for (const fn of manifest.functions) { - gatewayMap[fn.name] = `http://localhost:${fn.port}`; + gatewayMap[taskId(fn)] = `http://localhost:${fn.port}`; } return { JOBS_SCHEMA: 'app_jobs', JOBS_SUPPORT_ANY: 'false', - JOBS_SUPPORTED: manifest.functions.map((fn) => fn.name).join(','), + JOBS_SUPPORTED: manifest.functions.map(taskId).join(','), HOSTNAME: 'knative-job-service-local', INTERNAL_JOBS_CALLBACK_PORT: '8080', INTERNAL_JOBS_CALLBACK_URL: 'http://localhost:8080/callback', diff --git a/scripts/docker-build.ts b/scripts/docker-build.ts index ed7de45..4ea5f56 100644 --- a/scripts/docker-build.ts +++ b/scripts/docker-build.ts @@ -5,8 +5,8 @@ // // Usage: // node --experimental-strip-types scripts/docker-build.ts --all -// node --experimental-strip-types scripts/docker-build.ts --only=send-email-link -// node --experimental-strip-types scripts/docker-build.ts --only=simple-email --tag=abc1234 +// node --experimental-strip-types scripts/docker-build.ts --only=send-verification-link +// node --experimental-strip-types scripts/docker-build.ts --only=send-email --tag=abc1234 const fs = require('fs') as typeof import('fs'); const path = require('path') as typeof import('path'); diff --git a/scripts/generate.ts b/scripts/generate.ts index ea6cde7..c4f0385 100644 --- a/scripts/generate.ts +++ b/scripts/generate.ts @@ -22,6 +22,9 @@ interface FunctionManifest { description?: string; type?: string; port?: number; + // Optional task identifier used by the job worker to dispatch this function + // (e.g. "email:send_verification_link"). Defaults to `name` when omitted. + taskIdentifier?: string; dependencies?: Record; } @@ -182,6 +185,7 @@ interface FunctionInfo { dir: string; port: number; type: string; + taskIdentifier?: string; } const K8S_TEMPLATES_DIR: string = path.resolve(TEMPLATES_DIR, 'k8s'); @@ -200,14 +204,15 @@ function renderTemplate(template: string, vars: Record): string function generateFunctionsConfigMap(fns: FunctionInfo[], perFunction?: FunctionInfo): void { const targetFns = perFunction ? [perFunction] : fns; + const taskId = (fn: FunctionInfo) => fn.taskIdentifier ?? fn.name; const gatewayMap: Record = {}; for (const fn of targetFns) { - gatewayMap[fn.name] = `http://${fn.name}.${K8S_NAMESPACE}.svc.cluster.local`; + gatewayMap[taskId(fn)] = `http://${fn.name}.${K8S_NAMESPACE}.svc.cluster.local`; } const template = readTemplate('functions-configmap.yaml'); const yaml = renderTemplate(template, { - jobs_supported: targetFns.map((fn) => fn.name).join(','), + jobs_supported: targetFns.map(taskId).join(','), gateway_map: JSON.stringify(gatewayMap), }); @@ -410,6 +415,9 @@ function main(): void { dir, port: allManifests[i].port, type: allManifests[i].type || DEFAULT_TEMPLATE, + ...(allManifests[i].taskIdentifier + ? { taskIdentifier: allManifests[i].taskIdentifier } + : {}), })), }; diff --git a/skaffold.yaml b/skaffold.yaml index d2173ea..32d0dc7 100644 --- a/skaffold.yaml +++ b/skaffold.yaml @@ -105,7 +105,7 @@ profiles: namespace: constructive-functions port: 3000 localPort: 3002 - - name: send-email-link + - name: send-email build: artifacts: - image: constructive-functions @@ -123,17 +123,17 @@ profiles: paths: - k8s/overlays/local-simple rawYaml: - - generated/send-email-link/k8s/local-deployment.yaml - - generated/send-email-link/k8s/functions-configmap.yaml + - generated/send-email/k8s/local-deployment.yaml + - generated/send-email/k8s/functions-configmap.yaml deploy: kubectl: defaultNamespace: constructive-functions portForward: - resourceType: service - resourceName: send-email-link + resourceName: send-email namespace: constructive-functions port: 80 - localPort: 8082 + localPort: 8081 - resourceType: service resourceName: knative-job-service namespace: constructive-functions @@ -149,7 +149,7 @@ profiles: namespace: constructive-functions port: 3000 localPort: 3002 - - name: simple-email + - name: send-verification-link build: artifacts: - image: constructive-functions @@ -167,17 +167,17 @@ profiles: paths: - k8s/overlays/local-simple rawYaml: - - generated/simple-email/k8s/local-deployment.yaml - - generated/simple-email/k8s/functions-configmap.yaml + - generated/send-verification-link/k8s/local-deployment.yaml + - generated/send-verification-link/k8s/functions-configmap.yaml deploy: kubectl: defaultNamespace: constructive-functions portForward: - resourceType: service - resourceName: simple-email + resourceName: send-verification-link namespace: constructive-functions port: 80 - localPort: 8081 + localPort: 8082 - resourceType: service resourceName: knative-job-service namespace: constructive-functions @@ -225,8 +225,8 @@ profiles: rawYaml: - generated/example/k8s/local-deployment.yaml - generated/python-example/k8s/local-deployment.yaml - - generated/send-email-link/k8s/local-deployment.yaml - - generated/simple-email/k8s/local-deployment.yaml + - generated/send-email/k8s/local-deployment.yaml + - generated/send-verification-link/k8s/local-deployment.yaml - generated/functions-configmap.yaml deploy: kubectl: @@ -243,15 +243,15 @@ profiles: port: 80 localPort: 8084 - resourceType: service - resourceName: send-email-link + resourceName: send-email namespace: constructive-functions port: 80 - localPort: 8082 + localPort: 8081 - resourceType: service - resourceName: simple-email + resourceName: send-verification-link namespace: constructive-functions port: 80 - localPort: 8081 + localPort: 8082 - resourceType: service resourceName: knative-job-service namespace: constructive-functions @@ -298,15 +298,15 @@ profiles: port: 80 localPort: 8084 - resourceType: service - resourceName: send-email-link + resourceName: send-email namespace: constructive-functions port: 80 - localPort: 8082 + localPort: 8081 - resourceType: service - resourceName: simple-email + resourceName: send-verification-link namespace: constructive-functions port: 80 - localPort: 8081 + localPort: 8082 - resourceType: service resourceName: knative-job-service namespace: constructive-functions diff --git a/tests/e2e/README.md b/tests/e2e/README.md index 7ec063c..13d643d 100644 --- a/tests/e2e/README.md +++ b/tests/e2e/README.md @@ -9,7 +9,7 @@ All of the following must be running and accessible: - **PostgreSQL** — port-forwarded to `localhost:5432` - **constructive-server** — the GraphQL API server - **knative-job-service** — the job worker that picks up and dispatches jobs -- **simple-email** / **send-email-link** — the function deployments +- **send-email** / **send-verification-link** — the function deployments - **Database seeded** — `constructive-db` job must have completed (schemas + pgpm packages deployed) The simplest way to get everything running: diff --git a/tests/e2e/__tests__/job-processing.test.ts b/tests/e2e/__tests__/job-processing.test.ts index 42e32d2..6f69f8f 100644 --- a/tests/e2e/__tests__/job-processing.test.ts +++ b/tests/e2e/__tests__/job-processing.test.ts @@ -2,7 +2,7 @@ * Job Queue Inspection (end-to-end via k8s) * * Shared utility test for inspecting queue state. Individual function e2e tests - * are in per-function files: simple-email.e2e.test.ts, send-email-link.e2e.test.ts + * are in per-function files: send-email.e2e.test.ts, send-verification-link.e2e.test.ts */ import { getTestConnections, diff --git a/tests/e2e/__tests__/simple-email.e2e.test.ts b/tests/e2e/__tests__/send-email.e2e.test.ts similarity index 71% rename from tests/e2e/__tests__/simple-email.e2e.test.ts rename to tests/e2e/__tests__/send-email.e2e.test.ts index c1ce9e1..a9cedfb 100644 --- a/tests/e2e/__tests__/simple-email.e2e.test.ts +++ b/tests/e2e/__tests__/send-email.e2e.test.ts @@ -1,12 +1,12 @@ /** - * E2E: simple-email function + * E2E: send-email function * - * Assumes skaffold is running with simple-email deployed: - * skaffold dev -p simple-email (single function) - * make skaffold-dev (all functions) + * Assumes skaffold is running with send-email deployed: + * skaffold dev -p send-email (single function) + * make skaffold-dev (all functions) * * Inserts a job into the queue and verifies the job service dispatches it - * to the simple-email function which processes it. + * to the send-email function which processes it. */ import { getTestConnections, @@ -16,9 +16,9 @@ import { } from '../utils/db'; import { addJob, waitForJobComplete, deleteTestJobs } from '../utils/jobs'; -const TEST_PREFIX = 'k8s-e2e-simple-email'; +const TEST_PREFIX = 'k8s-e2e-send-email'; -describe('E2E: simple-email', () => { +describe('E2E: send-email', () => { let pg: TestClient; let databaseId: string; @@ -33,15 +33,15 @@ describe('E2E: simple-email', () => { await closeConnections(); }); - it('should process a simple-email job from the queue', async () => { - const job = await addJob(pg, databaseId, 'simple-email', { + it('should process an email:send_email job from the queue', async () => { + const job = await addJob(pg, databaseId, 'email:send_email', { to: 'test@example.com', subject: `${TEST_PREFIX} test email`, html: '

Hello from k8s test

', }); expect(job.id).toBeDefined(); - console.log(`Added simple-email job: ${job.id}`); + console.log(`Added email:send_email job: ${job.id}`); const result = await waitForJobComplete(pg, job.id, { timeout: 30000 }); diff --git a/tests/e2e/__tests__/send-email-link.e2e.test.ts b/tests/e2e/__tests__/send-verification-link.e2e.test.ts similarity index 65% rename from tests/e2e/__tests__/send-email-link.e2e.test.ts rename to tests/e2e/__tests__/send-verification-link.e2e.test.ts index d841f8d..c62d46f 100644 --- a/tests/e2e/__tests__/send-email-link.e2e.test.ts +++ b/tests/e2e/__tests__/send-verification-link.e2e.test.ts @@ -1,12 +1,12 @@ /** - * E2E: send-email-link function + * E2E: send-verification-link function * - * Assumes skaffold is running with send-email-link deployed: - * skaffold dev -p send-email-link (single function) - * make skaffold-dev (all functions) + * Assumes skaffold is running with send-verification-link deployed: + * skaffold dev -p send-verification-link (single function) + * make skaffold-dev (all functions) * * Inserts a job into the queue and verifies the job service dispatches it - * to the send-email-link function which processes it. + * to the send-verification-link function which processes it. */ import { getTestConnections, @@ -16,9 +16,9 @@ import { } from '../utils/db'; import { addJob, waitForJobComplete, deleteTestJobs } from '../utils/jobs'; -const TEST_PREFIX = 'k8s-e2e-send-email-link'; +const TEST_PREFIX = 'k8s-e2e-send-verification-link'; -describe('E2E: send-email-link', () => { +describe('E2E: send-verification-link', () => { let pg: TestClient; let databaseId: string; @@ -33,8 +33,8 @@ describe('E2E: send-email-link', () => { await closeConnections(); }); - it('should process a send-email-link job from the queue', async () => { - const job = await addJob(pg, databaseId, 'send-email-link', { + it('should process an email:send_verification_link job from the queue', async () => { + const job = await addJob(pg, databaseId, 'email:send_verification_link', { email_type: 'forgot_password', email: `${TEST_PREFIX}@example.com`, reset_token: 'test-token-123', @@ -42,7 +42,7 @@ describe('E2E: send-email-link', () => { }); expect(job.id).toBeDefined(); - console.log(`Added send-email-link job: ${job.id}`); + console.log(`Added email:send_verification_link job: ${job.id}`); const result = await waitForJobComplete(pg, job.id, { timeout: 30000 }); diff --git a/tests/integration/job-registry.test.ts b/tests/integration/job-registry.test.ts index 09b2b45..98fa26a 100644 --- a/tests/integration/job-registry.test.ts +++ b/tests/integration/job-registry.test.ts @@ -27,18 +27,18 @@ describe('loadFunctionRegistry', () => { path.join(dir, 'functions-manifest.json'), JSON.stringify({ functions: [ - { name: 'simple-email', dir: 'simple-email', port: 8081, type: 'node-graphql' }, - { name: 'send-email-link', dir: 'send-email-link', port: 8082, type: 'node-graphql' }, + { name: 'send-email', dir: 'send-email', port: 8081, type: 'node-graphql' }, + { name: 'send-verification-link', dir: 'send-verification-link', port: 8082, type: 'node-graphql' }, ], }) ); const reg = loadFunctionRegistry({}, tmpDir); - expect(reg['simple-email']).toEqual({ - moduleName: '@constructive-io/simple-email-fn', + expect(reg['send-email']).toEqual({ + moduleName: '@constructive-io/send-email-fn', defaultPort: 8081, }); - expect(reg['send-email-link']).toEqual({ - moduleName: '@constructive-io/send-email-link-fn', + expect(reg['send-verification-link']).toEqual({ + moduleName: '@constructive-io/send-verification-link-fn', defaultPort: 8082, }); }); From 065f48d7a30e4bddea4b9761535340c3b4b6d856 Mon Sep 17 00:00:00 2001 From: Anmol1696 Date: Thu, 14 May 2026 11:28:51 +0800 Subject: [PATCH 2/2] ci: retrigger after constructive-db#1138 (.dockerignore fix) merged The previous E2E failures were unrelated to this PR. constructive-db .dockerignore had excluded application/ since commit a1ac9a5508 (2026-05-06), which made constructive-db-job:latest ship without the constructive PG extension's source. pgpm fell back to CREATE EXTENSION constructive on a postgres image that didn't have the .control file, so every PR's CI on this workflow had been red since 2026-05-10. Fix landed in constructive-io/constructive-db#1138. constructive-db-job :latest has been rebuilt; manual workflow_dispatch run 25839642302 went green on all three E2E jobs. This empty commit just retriggers PR-event checks so the PR status reflects reality.