Skip to content

Commit 0de3592

Browse files
committed
feat(packages): extract fn-types; make fn-runtime/fn-app publishable
First wave of the portable-functions toolkit (Starship-style split). Establishes a single source-of-truth types package that the rest of the toolkit (fn-generator, fn-client, fn-cli — landing in later waves) will depend on. - New @constructive-io/fn-types@0.1.0 — runtime contract types (FunctionHandler, FunctionContext, ServerOptions), HandlerManifest, FnRegistry/FnRegistryEntry, FnConfig + defineConfig() helper for typed fn.config.ts files. No logic, no shell-out. - @constructive-io/fn-runtime@1.2.0 — drops private:true; now depends on fn-types and re-exports the runtime types from there. Source files import from @constructive-io/fn-types instead of local ./types.ts (which is removed). - @constructive-io/knative-job-fn@1.6.0 — drops private:true; ready to publish (rename to @constructive-io/fn-app deferred to a later wave to avoid churning internal imports). Verified: pnpm generate && pnpm install && pnpm build passes for all three existing functions (example, simple-email, send-email-link).
1 parent faafc85 commit 0de3592

16 files changed

Lines changed: 295 additions & 10 deletions

File tree

packages/fn-app/README.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,20 @@
11
# @constructive-io/knative-job-fn
2+
3+
Express app factory for Knative-style job functions. Provides:
4+
5+
- One POST `/` endpoint with JSON body parsing and request logging.
6+
- Per-request job context lifted from `X-Worker-Id`, `X-Job-Id`, `X-Database-Id`, `X-Callback-Url` headers.
7+
- An on-finish hook that POSTs `{status: "success" | "error"}` to the callback URL when set.
8+
- Centralized error middleware that emits an error callback and a 200 with `{message}` body (Knative requires 2xx for delivery acknowledgment).
9+
10+
Most users should depend on `@constructive-io/fn-runtime`, which wraps this with a typed handler and GraphQL clients. Use this package directly only if you need raw Express middleware control.
11+
12+
```ts
13+
import { createJobApp } from '@constructive-io/knative-job-fn';
14+
15+
const app = createJobApp();
16+
app.post('/', (req, res) => {
17+
res.json({ ok: true });
18+
});
19+
app.listen(8080);
20+
```

packages/fn-app/package.json

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,18 @@
11
{
22
"name": "@constructive-io/knative-job-fn",
3-
"version": "1.5.2",
4-
"description": "Express app factory for Knative job functions with callback handling",
3+
"version": "1.6.0",
4+
"description": "Express app factory for Knative job functions with callback handling.",
55
"author": "Constructive <developers@constructive.io>",
6-
"private": true,
6+
"license": "SEE LICENSE IN LICENSE",
77
"main": "dist/index.js",
8+
"types": "dist/index.d.ts",
9+
"files": [
10+
"dist",
11+
"README.md"
12+
],
13+
"publishConfig": {
14+
"access": "public"
15+
},
816
"scripts": {
917
"build": "makage build",
1018
"build:dev": "makage build --dev",

packages/fn-runtime/README.md

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
# @constructive-io/fn-runtime
2+
3+
Runtime contract for Constructive Functions: wraps a typed handler in an Express app with a built-in GraphQL client, structured logger, and Knative job-callback support.
4+
5+
This is the package handler authors import directly. The `@constructive-io/fn-cli` toolchain stamps out function packages that depend on this runtime.
6+
7+
## Usage
8+
9+
```ts
10+
import { createFunctionServer } from '@constructive-io/fn-runtime';
11+
import type { FunctionHandler } from '@constructive-io/fn-types';
12+
13+
type Payload = { to: string; subject: string };
14+
15+
const handler: FunctionHandler<Payload, { ok: true }> = async (params, ctx) => {
16+
ctx.log.info('processing', { to: params.to });
17+
return { ok: true };
18+
};
19+
20+
const app = createFunctionServer(handler, { name: 'my-function' });
21+
22+
if (require.main === module) {
23+
app.listen(Number(process.env.PORT || 8080));
24+
}
25+
26+
export default app;
27+
```
28+
29+
## What you get on `ctx`
30+
31+
- `ctx.job``{ jobId, workerId, databaseId }` lifted from `X-*` request headers.
32+
- `ctx.client` / `ctx.meta` — GraphQL clients, configured via `GRAPHQL_URL` / `META_GRAPHQL_URL` env. Lazily created when `databaseId` is present; throw a helpful error if used without configuration.
33+
- `ctx.log` — structured logger (`@pgpmjs/logger`).
34+
- `ctx.env``process.env` reference for convenience.
35+
36+
The HTTP contract (one POST `/`, callback-on-finish) and error handling come from `@constructive-io/knative-job-fn` underneath, so the function plays nicely with the Constructive job-service.

packages/fn-runtime/package.json

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,24 @@
11
{
22
"name": "@constructive-io/fn-runtime",
3-
"version": "1.1.0",
4-
"description": "Runtime for Constructive functions — wraps handler in Express app with GraphQL client, logging, and job callback support",
3+
"version": "1.2.0",
4+
"description": "Runtime for Constructive functions — wraps a handler in an Express app with GraphQL client, logging, and job callback support.",
55
"author": "Constructive <developers@constructive.io>",
6-
"private": true,
6+
"license": "SEE LICENSE IN LICENSE",
77
"main": "dist/index.js",
88
"types": "dist/index.d.ts",
9+
"files": [
10+
"dist",
11+
"README.md"
12+
],
13+
"publishConfig": {
14+
"access": "public"
15+
},
916
"scripts": {
1017
"build": "tsc -p tsconfig.json",
1118
"clean": "rimraf dist"
1219
},
1320
"dependencies": {
21+
"@constructive-io/fn-types": "workspace:^",
1422
"@constructive-io/knative-job-fn": "workspace:^",
1523
"@pgpmjs/logger": "^2.4.3",
1624
"graphql-request": "^7.1.2"

packages/fn-runtime/src/context.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { createLogger } from '@pgpmjs/logger';
2+
import type { FunctionContext } from '@constructive-io/fn-types';
23
import { createClients } from './graphql';
3-
import type { FunctionContext } from './types';
44

55
type RequestHeaders = {
66
databaseId?: string;

packages/fn-runtime/src/index.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,9 @@
11
export { createFunctionServer } from './server';
22
export { createClients } from './graphql';
33
export { buildContext } from './context';
4-
export type { FunctionHandler, FunctionContext, ServerOptions } from './types';
4+
export type {
5+
FunctionHandler,
6+
FunctionContext,
7+
FunctionLogger,
8+
ServerOptions
9+
} from '@constructive-io/fn-types';

packages/fn-runtime/src/server.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { createJobApp } from '@constructive-io/knative-job-fn';
2+
import type { FunctionHandler, ServerOptions } from '@constructive-io/fn-types';
23
import { buildContext } from './context';
3-
import type { FunctionHandler, ServerOptions } from './types';
44

55
export const createFunctionServer = (
66
handler: FunctionHandler<any, any>,

packages/fn-types/README.md

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
# @constructive-io/fn-types
2+
3+
Source-of-truth TypeScript types for the Constructive Functions toolkit.
4+
5+
This package has **no logic** — it's the contract every other `@constructive-io/fn-*` package depends on. Types are split into four areas:
6+
7+
- **Runtime**`FunctionHandler`, `FunctionContext`, `ServerOptions` (used by `@constructive-io/fn-runtime` and handler authors).
8+
- **Manifest**`HandlerManifest` (the shape of `functions/<name>/handler.json`).
9+
- **Config**`FnConfig`, `FnPreset`, `K8sOptions`, `DockerOptions`, plus a `defineConfig()` helper for `fn.config.ts` files.
10+
- **Registry**`FnRegistry`, `FnRegistryEntry` (manifest format consumed by `@constructive-io/fn-job-service`).
11+
12+
## Usage in `fn.config.ts`
13+
14+
```ts
15+
import { defineConfig } from '@constructive-io/fn-types';
16+
17+
export default defineConfig({
18+
functionsDir: 'functions',
19+
outputDir: 'generated',
20+
preset: 'jobs-bundle',
21+
registry: 'ghcr.io/my-org',
22+
k8s: { target: 'knative' }
23+
});
24+
```
25+
26+
## Usage in a handler
27+
28+
```ts
29+
import type { FunctionHandler } from '@constructive-io/fn-types';
30+
31+
const handler: FunctionHandler<{ to: string }, { ok: true }> = async (params, ctx) => {
32+
ctx.log.info('sending', params);
33+
return { ok: true };
34+
};
35+
36+
export default handler;
37+
```

packages/fn-types/package.json

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
{
2+
"name": "@constructive-io/fn-types",
3+
"version": "0.1.0",
4+
"description": "Source-of-truth TypeScript types for the Constructive Functions toolkit: handler manifests, FnConfig, runtime context, registry.",
5+
"author": "Constructive <developers@constructive.io>",
6+
"license": "SEE LICENSE IN LICENSE",
7+
"main": "dist/index.js",
8+
"types": "dist/index.d.ts",
9+
"files": [
10+
"dist",
11+
"README.md"
12+
],
13+
"publishConfig": {
14+
"access": "public"
15+
},
16+
"scripts": {
17+
"build": "tsc -p tsconfig.json",
18+
"clean": "rimraf dist",
19+
"lint": "eslint . --fix"
20+
},
21+
"dependencies": {
22+
"graphql-request": "^7.1.2"
23+
},
24+
"devDependencies": {
25+
"@types/node": "^22.10.4",
26+
"rimraf": "^5.0.5",
27+
"typescript": "^5.1.6"
28+
}
29+
}

packages/fn-types/src/config.ts

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
export type FnPreset = 'functions-only' | 'jobs-bundle';
2+
3+
export type K8sTarget = 'knative' | 'deployment';
4+
5+
export interface K8sResourceQuantities {
6+
cpu?: string;
7+
memory?: string;
8+
}
9+
10+
export interface K8sOptions {
11+
target?: K8sTarget;
12+
namespace?: string;
13+
imagePullSecrets?: string[];
14+
resources?: {
15+
requests?: K8sResourceQuantities;
16+
limits?: K8sResourceQuantities;
17+
};
18+
/** Knative-specific: containerConcurrency, timeoutSeconds, etc. */
19+
knative?: {
20+
containerConcurrency?: number;
21+
timeoutSeconds?: number;
22+
visibility?: 'cluster-local' | 'public';
23+
};
24+
}
25+
26+
export interface DockerOptions {
27+
/** Image registry, e.g. 'ghcr.io/my-org'. */
28+
registry?: string;
29+
/** Base image override; defaults to template's choice. */
30+
baseImage?: string;
31+
/** Generate one Dockerfile per function (true) vs one shared image (false). */
32+
perFunction?: boolean;
33+
}
34+
35+
export interface FnConfig {
36+
/** Directory containing per-function source: <dir>/<name>/handler.{json,ts,py}. Default: 'functions'. */
37+
functionsDir?: string;
38+
/** Output directory for generated artifacts. Default: 'generated'. */
39+
outputDir?: string;
40+
/** Bundle preset. Default: 'functions-only'. */
41+
preset?: FnPreset;
42+
/** Image registry default; per-function manifests can still override. */
43+
registry?: string;
44+
/** Default Kubernetes namespace for generated manifests. */
45+
namespace?: string;
46+
k8s?: K8sOptions;
47+
docker?: DockerOptions;
48+
/** Map of template type → module specifier resolved by the generator. */
49+
templates?: Record<string, string>;
50+
}
51+
52+
/** Identity helper for editor autocomplete in fn.config.ts files. */
53+
export const defineConfig = (config: FnConfig): FnConfig => config;

0 commit comments

Comments
 (0)