Skip to content

Commit fe1f2cc

Browse files
committed
add template for node-graphql functions
1 parent 272763a commit fe1f2cc

12 files changed

Lines changed: 482 additions & 121 deletions

File tree

AGENTS.md

Lines changed: 44 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ This guide helps AI agents quickly navigate the constructive-functions workspace
66

77
See [docs/spec/function-templating.md](docs/spec/function-templating.md) for the full specification.
88

9-
Functions use a **templating system**: developers write `handler.ts` + `handler.json` in `functions/`, and `scripts/generate.ts` produces workspace packages in `generated/` with all boilerplate.
9+
Functions use a **template-based system**: developers write `handler.ts` + `handler.json` in `functions/`, and `scripts/generate.ts` copies template files from `templates/<type>/` into `generated/` with placeholder replacement and dependency merging.
1010

1111
## Quick Start
1212

@@ -22,14 +22,23 @@ pnpm build # Build all packages + functions
2222
functions/ # User-authored source (git tracked)
2323
<name>/
2424
handler.ts # Business logic (default export)
25-
handler.json # Metadata + dependencies
25+
handler.json # Metadata + dependencies + template type
2626
*.d.ts # Optional type declarations
2727
28+
templates/ # Template definitions (git tracked)
29+
node-graphql/ # Default template type
30+
package.json # Base package.json with {{placeholders}}
31+
tsconfig.json # Static compiler config
32+
index.ts # Entry point template with {{name}}
33+
Dockerfile # Per-function production Docker build
34+
k8s/
35+
knative-service.yaml # Base Knative Service manifest
36+
2837
generated/ # Generated workspace packages (gitignored)
2938
<name>/
30-
package.json # Workspace package
31-
tsconfig.json # Compiler config
32-
index.ts # Entry point
39+
package.json # Merged from template + handler.json deps
40+
tsconfig.json # Copied from template (+ .d.ts includes)
41+
index.ts # Copied from template with {{name}} replaced
3342
handler.ts # Symlink -> functions/<name>/handler.ts
3443
dist/ # Compiled output
3544
@@ -43,9 +52,8 @@ job/
4352
service/ # Orchestrator (loads functions + worker + scheduler)
4453
4554
scripts/
46-
generate.ts # Generator script (runs via Node's native type stripping)
47-
48-
k8s/ # Kubernetes manifests and overlays
55+
generate.ts # Template-based generator (copies + merges + replaces)
56+
docker-build.ts # Per-function Docker image builder
4957
```
5058

5159
## Function Pattern
@@ -67,21 +75,45 @@ const handler: FunctionHandler = async (params, context) => {
6775
export default handler;
6876
```
6977

78+
## handler.json Schema
79+
80+
```json
81+
{
82+
"name": "send-email-link",
83+
"version": "1.1.0",
84+
"description": "Sends invite, password reset, and verification emails",
85+
"type": "node-graphql",
86+
"dependencies": {
87+
"graphql-tag": "^2.12.6"
88+
}
89+
}
90+
```
91+
92+
- `type` selects the template from `templates/<type>/` (default: `"node-graphql"`)
93+
- `dependencies` are merged into the template's base package.json
94+
7095
## Entry Points
7196

7297
- Function handlers: `functions/*/handler.ts`
7398
- Generated entry points: `generated/*/index.ts` (compiled to `generated/*/dist/index.js`)
7499
- Job orchestrator: `job/service/src/index.ts`
75100
- Generator script: `scripts/generate.ts`
101+
- Docker builder: `scripts/docker-build.ts`
76102

77103
## Common Workflows
78104

79105
**Add a new function:**
80-
1. Create `functions/<name>/handler.json` with name, version, dependencies
106+
1. Create `functions/<name>/handler.json` with name, version, type, dependencies
81107
2. Create `functions/<name>/handler.ts` with default export
82108
3. Run `pnpm generate && pnpm install && pnpm build`
83109
4. Add to function registry in `job/service/src/index.ts` if needed
84110

111+
**Build Docker images:**
112+
```bash
113+
make docker-build # build all function images
114+
make docker-build-send-email-link # build single function image
115+
```
116+
85117
**Local development with Docker:**
86118
```bash
87119
make dev # docker compose up (postgres + job-service)
@@ -90,7 +122,7 @@ make dev-down # docker compose down
90122

91123
**Regenerate after changing handler.json:**
92124
```bash
93-
pnpm generate # Regenerates package.json, tsconfig, index.ts, symlinks
125+
pnpm generate # Copies templates, merges deps, replaces placeholders
94126
pnpm install # Picks up any new dependencies
95127
pnpm build # Recompile
96128
```
@@ -102,3 +134,5 @@ pnpm build # Recompile
102134
- `loadFunctionApp()` in job/service resolves modules by name (e.g. `@constructive-io/simple-email-fn`)
103135
- GraphQL clients require `GRAPHQL_URL` env var and `X-Database-Id` header
104136
- The `generated/` directory is entirely gitignored
137+
- Templates use `{{name}}`, `{{version}}`, `{{description}}` placeholders
138+
- Generator supports `--only=<name>` for single-function generation

Dockerfile.dev

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,10 @@ RUN npm install -g pnpm@10.12.2
44

55
WORKDIR /usr/src/app
66

7-
# Copy config files and generate script
7+
# Copy config files, generate script, and templates
88
COPY package.json pnpm-workspace.yaml pnpm-lock.yaml tsconfig.json ./
99
COPY scripts/ scripts/
10+
COPY templates/ templates/
1011

1112
# Copy all package/job manifests for caching
1213
COPY packages/fn-app/package.json packages/fn-app/

Makefile

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
.PHONY: build clean lint generate dev dev-build dev-down
1+
.PHONY: build clean lint generate dev dev-build dev-down docker-build
22

33
build:
44
pnpm run build
@@ -20,3 +20,10 @@ dev:
2020

2121
dev-down:
2222
docker compose down
23+
24+
docker-build:
25+
pnpm run docker:build
26+
27+
# Build a single function image: make docker-build-send-email-link
28+
docker-build-%:
29+
node --experimental-strip-types scripts/docker-build.ts --only=$*

docs/spec/function-templating.md

Lines changed: 100 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,9 @@
22

33
## Overview
44

5-
The function templating system reduces each Constructive function to two user-authored files (`handler.ts` + `handler.json`) and generates all boilerplate (package.json, tsconfig, entry point) into a separate `generated/` directory.
5+
The function templating system reduces each Constructive function to two user-authored files (`handler.ts` + `handler.json`) and generates all boilerplate into a separate `generated/` directory by copying and processing template files.
66

7-
The `fn-runtime` package provides the runtime wiring: Express server via `fn-app`, per-request GraphQL clients, structured logging, and job metadata extraction.
7+
Templates live in `templates/<type>/` as real, readable files with `{{placeholder}}` tokens. The generator copies them, replaces placeholders, merges dependencies, and symlinks handler source. The `fn-runtime` package provides the runtime wiring: Express server via `fn-app`, per-request GraphQL clients, structured logging, and job metadata extraction.
88

99
## Directory Structure
1010

@@ -13,7 +13,7 @@ constructive-functions/
1313
functions/ # User-authored source (git tracked)
1414
example/
1515
handler.ts # Business logic
16-
handler.json # Metadata + dependencies
16+
handler.json # Metadata + dependencies + template type
1717
simple-email/
1818
handler.ts
1919
handler.json
@@ -22,15 +22,22 @@ constructive-functions/
2222
handler.json
2323
types.d.ts # Optional type declarations
2424
25+
templates/ # Template definitions (git tracked)
26+
node-graphql/ # Default template type
27+
package.json # Base package.json with {{name}}, {{version}}, {{description}}
28+
tsconfig.json # Static compiler config
29+
index.ts # Entry point with {{name}} placeholder
30+
Dockerfile # Per-function production Docker build
31+
k8s/
32+
knative-service.yaml # Base Knative Service manifest
33+
2534
generated/ # All generated (gitignored)
2635
example/
27-
package.json # Workspace package
28-
tsconfig.json # Compiler config
29-
index.ts # Entry point (wires runtime + handler)
36+
package.json # Merged from template + handler.json
37+
tsconfig.json # Copied from template (+ .d.ts includes)
38+
index.ts # Copied from template with {{name}} replaced
3039
handler.ts # Symlink -> ../../functions/example/handler.ts
3140
dist/ # tsc output
32-
index.js
33-
handler.js
3441
simple-email/
3542
...same structure...
3643
send-email-link/
@@ -47,7 +54,8 @@ constructive-functions/
4754
service/ # Orchestrator (loads functions + worker + scheduler)
4855
4956
scripts/
50-
generate.ts # Generator script (runs via Node's native type stripping)
57+
generate.ts # Template-based generator
58+
docker-build.ts # Per-function Docker image builder
5159
```
5260

5361
## What Developers Write
@@ -70,20 +78,55 @@ export default handler;
7078

7179
### handler.json
7280

73-
Minimal manifest with name, version, and any extra npm dependencies:
81+
Manifest with name, version, template type, and extra npm dependencies:
7482

7583
```json
7684
{
7785
"name": "send-email-link",
7886
"version": "1.1.0",
7987
"description": "Sends invite, password reset, and verification emails",
88+
"type": "node-graphql",
8089
"dependencies": {
8190
"graphql-tag": "^2.12.6"
8291
}
8392
}
8493
```
8594

86-
Dependencies listed here are merged with `@constructive-io/fn-runtime` (always included) into the generated `package.json`.
95+
| Field | Required | Description |
96+
|---|---|---|
97+
| `name` | Yes | Function name (used in package name and k8s manifests) |
98+
| `version` | Yes | Semver version |
99+
| `description` | No | Description for package.json |
100+
| `type` | No | Template type from `templates/<type>/` (default: `"node-graphql"`) |
101+
| `dependencies` | No | Extra npm deps merged into template's base package.json |
102+
103+
## Templates
104+
105+
Templates live in `templates/<type>/` and contain real files with `{{placeholder}}` tokens.
106+
107+
### Placeholder Tokens
108+
109+
| Token | Replaced With |
110+
|---|---|
111+
| `{{name}}` | `manifest.name` from handler.json |
112+
| `{{version}}` | `manifest.version` from handler.json |
113+
| `{{description}}` | `manifest.description` from handler.json (or empty string) |
114+
115+
### Template Files
116+
117+
| File | Processing |
118+
|---|---|
119+
| `package.json` | Placeholder replacement + deep merge of handler.json `dependencies` |
120+
| `tsconfig.json` | Copied verbatim + `.d.ts` filenames appended to `include` |
121+
| `index.ts` | Placeholder replacement only |
122+
| `Dockerfile` | Used by `scripts/docker-build.ts` (not copied to generated/) |
123+
| `k8s/*.yaml` | Reference manifests for new functions (not copied to generated/) |
124+
125+
### Adding a New Template Type
126+
127+
1. Create `templates/<new-type>/` with package.json, tsconfig.json, index.ts
128+
2. Optionally add Dockerfile and k8s/ manifests
129+
3. Reference it in handler.json: `"type": "<new-type>"`
87130

88131
## fn-runtime Package
89132

@@ -128,44 +171,61 @@ GraphQL clients are created per-request (databaseId varies per job). If `GRAPHQL
128171

129172
## Generator Script
130173

131-
`scripts/generate.ts` is a TypeScript script that runs during `preinstall` via Node's native type stripping (`--experimental-strip-types`).
174+
`scripts/generate.ts` runs during `preinstall` via Node's native type stripping (`--experimental-strip-types`). It uses only Node built-ins (no npm dependencies).
175+
176+
### How It Works
177+
178+
1. Discovers all `functions/*/handler.json` files
179+
2. For each function, resolves the template type (default: `node-graphql`)
180+
3. Copies template files (`package.json`, `tsconfig.json`, `index.ts`) into `generated/<name>/`
181+
4. Replaces `{{placeholder}}` tokens with values from handler.json
182+
5. Deep merges handler.json `dependencies` into template's `package.json`
183+
6. Appends `.d.ts` filenames to `tsconfig.json` `include` array
184+
7. Creates symlinks for `handler.ts` and any `.d.ts` files
132185

133-
For each `functions/*/handler.json` it generates into `generated/<name>/`:
186+
### CLI Flags
134187

135-
| File | Content |
188+
| Flag | Description |
136189
|---|---|
137-
| `package.json` | Workspace package: name from `@constructive-io/<name>-fn`, deps merged from handler.json |
138-
| `tsconfig.json` | Extends root tsconfig, compiles `index.ts` + `handler.ts` into `dist/` |
139-
| `index.ts` | Entry point: imports fn-runtime + handler, creates server, exports app |
140-
| `handler.ts` | Symlink to `../../functions/<name>/handler.ts` |
141-
| `*.d.ts` | Symlinks to any `.d.ts` files in the function directory |
190+
| `--only=<name>` | Generate only the specified function (used by per-function Dockerfile) |
142191

143-
### Generated index.ts
192+
### Idempotency
144193

145-
```typescript
146-
import { createFunctionServer } from '@constructive-io/fn-runtime';
147-
import handler from './handler';
194+
The generator uses `writeIfChanged()` — files are only written when content differs. Running `pnpm generate` twice produces no disk writes on the second run.
148195

149-
const app = createFunctionServer(handler, { name: "send-email-link" });
196+
## Docker Build
150197

151-
export default app;
198+
Each function can be built as an independent Docker image for production deployment.
152199

153-
if (require.main === module) {
154-
app.listen(Number(process.env.PORT || 8080));
155-
}
200+
### Per-Function Dockerfile
201+
202+
The template Dockerfile (`templates/node-graphql/Dockerfile`) uses three stages:
203+
204+
1. **build**: Copies monorepo, generates the single function, installs deps, builds
205+
2. **deploy**: Uses `pnpm deploy` to create a minimal production bundle
206+
3. **runtime**: Clean `node:22-alpine` image with only compiled output + production deps
207+
208+
### Building Images
209+
210+
```bash
211+
# Build all function images
212+
pnpm docker:build
213+
214+
# Build a single function
215+
make docker-build-send-email-link
216+
217+
# Build with custom tag
218+
node --experimental-strip-types scripts/docker-build.ts --only=send-email-link --tag=abc1234
156219
```
157220

158-
This preserves:
159-
- `export default app` pattern that job/service's `loadFunctionApp()` expects
160-
- `node dist/index.js` standalone execution for containers
161-
- Backward compatibility with existing K8s manifests
221+
Image naming: `ghcr.io/constructive-io/<name>-fn:<tag>`
162222

163223
## Workflow
164224

165225
### Build from scratch
166226

167227
```bash
168-
pnpm generate # Generate generated/<name>/ for all functions
228+
pnpm generate # Copy templates, merge deps, replace placeholders
169229
pnpm install # Resolve workspace deps (preinstall runs generate.ts automatically)
170230
pnpm build # Compile all packages + functions
171231
```
@@ -184,6 +244,13 @@ make dev # docker compose up (postgres + job-service with all functions
184244
make dev-down # docker compose down
185245
```
186246

247+
### Production Docker images
248+
249+
```bash
250+
make docker-build # build all
251+
make docker-build-send-email-link # build one
252+
```
253+
187254
## Data Flow
188255

189256
```

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,11 @@
1717
"packageManager": "pnpm@10.12.2",
1818
"scripts": {
1919
"generate": "node --experimental-strip-types scripts/generate.ts",
20+
"generate:all": "node --experimental-strip-types scripts/generate.ts",
2021
"preinstall": "node --experimental-strip-types scripts/generate.ts",
2122
"clean": "pnpm -r run clean",
2223
"build": "pnpm -r run build",
23-
"build:docker": "make build-docker",
24+
"docker:build": "node --experimental-strip-types scripts/docker-build.ts --all",
2425
"lint": "pnpm -r run lint"
2526
},
2627
"devDependencies": {

0 commit comments

Comments
 (0)