Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 9 additions & 2 deletions functions/example/handler.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
import type { FunctionHandler } from '@constructive-io/fn-runtime';
import type { FunctionContext, FunctionHandler } from '@constructive-io/fn-runtime';

const handler: FunctionHandler = async (params: any) => {
type ExampleParams = {
throw?: boolean;
};

const handler: FunctionHandler<ExampleParams> = async (
params: ExampleParams,
_context: FunctionContext
) => {
if (params.throw) {
throw new Error('THROWN_ERROR');
}
Expand Down
11 changes: 11 additions & 0 deletions functions/export-metaschema/handler.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"name": "export-metaschema",
"version": "1.0.0",
"type": "node-pgpm",
"description": "Exports database metaschema migrations via pgpm export",
"dependencies": {
"@constructive-io/fn-core": "workspace:^",
"@pgpmjs/core": "^6.2.0",
"pg-cache": "^3.1.0"
}
}
109 changes: 109 additions & 0 deletions functions/export-metaschema/handler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import type { PgpmFunctionContext, PgpmFunctionHandler } from '@constructive-io/fn-pgpm-runtime';
import { DEFAULT_DATABASE_NAME } from '@constructive-io/fn-core';
import { exportMigrations } from '@pgpmjs/core';
import { getPgPool, pgCache } from 'pg-cache';
import { resolve } from 'path';

type ExportMetaschemaParams = {
dbname?: string;
databaseName: string;
author?: string;
extensionName?: string;
metaExtensionName?: string;
schema_names?: string[];
outdir?: string;
skipSchemaRenaming?: boolean;
username?: string;
repoName?: string;
};

const handler: PgpmFunctionHandler<ExportMetaschemaParams> = async (
params: ExportMetaschemaParams,
context: PgpmFunctionContext
) => {
const { project, options, log, env } = context;

// Resolve database name: params > PGDATABASE env > default
const dbname = params.dbname || env.PGDATABASE || DEFAULT_DATABASE_NAME;

log.info('[export-metaschema] Connecting to database', { dbname });

const pgPool = getPgPool({ database: dbname });

// Discover database_id from metaschema
const dbsResult = await pgPool.query(
'SELECT id, name FROM metaschema_public.database WHERE name = $1',
[params.databaseName]
);

if (!dbsResult.rows.length) {
throw new Error(`Database '${params.databaseName}' not found in metaschema_public.database`);
}

const targetRow = dbsResult.rows[0];

const databaseName = targetRow.name;
const database_ids = [targetRow.id];

// Discover schemas if not provided
let schema_names = params.schema_names;
if (!schema_names?.length) {
const schemasResult = await pgPool.query(
'SELECT schema_name FROM metaschema_public.schema WHERE database_id = $1',
[database_ids[0]]
);
schema_names = schemasResult.rows.map((r: any) => r.schema_name);
}

if (!schema_names?.length) {
throw new Error(`No schemas found for database '${databaseName}'`);
}

const author = params.author || 'Constructive <developers@constructive.io>';
const extensionName = params.extensionName || databaseName;
const metaExtensionName = params.metaExtensionName || `${databaseName}-service`;
// Default username/repoName to avoid interactive prompts from scaffoldTemplate
const username = params.username || 'constructive-io';
const repoName = params.repoName || extensionName;

log.info('[export-metaschema] Starting export', {
dbname,
databaseName,
database_ids,
extensionName,
schema_names
});

project.ensureWorkspace();
project.resetCwd(project.workspacePath);

const outdir = params.outdir ?? resolve(project.workspacePath, 'packages/');

await exportMigrations({
project,
options,
dbInfo: {
dbname,
databaseName,
database_ids
},
author,
outdir,
schema_names,
extensionName,
metaExtensionName,
username,
repoName,
skipSchemaRenaming: params.skipSchemaRenaming
});

// exportMigrationsToDisk calls pgPool.end() which kills the cached pool.
// Evict the dead pool from pg-cache so the next request gets a fresh one.
pgCache.delete(dbname);

log.info('[export-metaschema] Export complete', { outdir });

return { complete: true, outdir, extensionName, metaExtensionName };
};

export default handler;
7 changes: 5 additions & 2 deletions functions/send-email-link/handler.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { FunctionHandler } from '@constructive-io/fn-runtime';
import type { FunctionContext, FunctionHandler } from '@constructive-io/fn-runtime';
import type { GraphQLClient } from 'graphql-request';
import gql from 'graphql-tag';
import { generate } from '@launchql/mjml';
Expand Down Expand Up @@ -269,7 +269,10 @@ const sendEmailLink = async (
};
};

const handler: FunctionHandler<SendEmailParams> = async (params, context) => {
const handler: FunctionHandler<SendEmailParams> = async (
params: SendEmailParams,
context: FunctionContext
) => {
const { client, meta, job, log, env } = context;

const databaseId = job.databaseId;
Expand Down
7 changes: 5 additions & 2 deletions functions/simple-email/handler.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { FunctionHandler } from '@constructive-io/fn-runtime';
import type { FunctionContext, 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';
Expand Down Expand Up @@ -31,7 +31,10 @@ const isDryRun = parseEnvBoolean(process.env.SIMPLE_EMAIL_DRY_RUN) ?? false;
const useSmtp = parseEnvBoolean(process.env.EMAIL_SEND_USE_SMTP) ?? false;
const logger = createLogger('simple-email');

const handler: FunctionHandler<SimpleEmailPayload> = async (params) => {
const handler: FunctionHandler<SimpleEmailPayload> = async (
params: SimpleEmailPayload,
_context: FunctionContext
) => {
const to = getRequiredField(params, 'to');
const subject = getRequiredField(params, 'subject');

Expand Down
4 changes: 4 additions & 0 deletions job/service/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,10 @@ const functionRegistry: Record<FunctionName, FunctionRegistryEntry> = {
'send-email-link': {
moduleName: '@constructive-io/send-email-link-fn',
defaultPort: 8082
},
'export-metaschema': {
moduleName: '@constructive-io/export-metaschema-fn',
defaultPort: 8083
}
};

Expand Down
2 changes: 1 addition & 1 deletion job/service/src/types.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
export type FunctionName = 'simple-email' | 'send-email-link';
export type FunctionName = 'simple-email' | 'send-email-link' | 'export-metaschema';

export type FunctionServiceConfig = {
name: FunctionName;
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
"access": "restricted"
},
"engines": {
"node": ">=18.17.0"
"node": ">=22.0.0"
},
"packageManager": "pnpm@10.12.2",
"scripts": {
Expand Down
20 changes: 20 additions & 0 deletions packages/fn-core/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
"name": "@constructive-io/fn-core",
"version": "1.0.0",
"description": "Shared core for Constructive function runtimes — base types, server factory, and request handling",
"author": "Constructive",
"private": true,
"main": "dist/index.js",
"types": "dist/index.d.ts",
"scripts": {
"build": "tsc -p tsconfig.json",
"clean": "rimraf dist"
},
"dependencies": {
"@constructive-io/knative-job-fn": "workspace:^"
},
"devDependencies": {
"@types/node": "^22.10.4",
"typescript": "^5.1.6"
}
}
12 changes: 12 additions & 0 deletions packages/fn-core/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
export { createServer, extractHeaders } from './server';
export type { ContextFactory } from './server';
export type {
JobMeta,
LogFn,
Env,
BaseContext,
BaseServerOptions,
RequestHeaders,
BaseFunctionHandler
} from './types';
export { DEFAULT_DATABASE_NAME } from './types';
36 changes: 36 additions & 0 deletions packages/fn-core/src/server.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { createJobApp } from '@constructive-io/knative-job-fn';
import type { BaseContext, BaseFunctionHandler, RequestHeaders } from './types';

export type ContextFactory<C extends BaseContext> = (
headers: RequestHeaders
) => C | Promise<C>;

export const extractHeaders = (req: any): RequestHeaders => ({
databaseId:
req.get('X-Database-Id') ||
req.get('x-database-id') ||
process.env.DEFAULT_DATABASE_ID,
workerId: req.get('X-Worker-Id') || req.get('x-worker-id'),
jobId: req.get('X-Job-Id') || req.get('x-job-id')
});

export const createServer = <C extends BaseContext>(
handler: BaseFunctionHandler<any, C, any>,
contextFactory: ContextFactory<C>
) => {
const app = createJobApp();

app.post('/', async (req: any, res: any, next: any) => {
try {
const headers = extractHeaders(req);
const context = await contextFactory(headers);
const params = req.body || {};
const result = await handler(params, context);
res.status(200).json(result);
} catch (err) {
next(err);
}
});

return app;
};
37 changes: 37 additions & 0 deletions packages/fn-core/src/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
export type JobMeta = {
jobId?: string;
workerId?: string;
databaseId?: string;
};

export type LogFn = {
info: (...args: any[]) => void;
error: (...args: any[]) => void;
warn: (...args: any[]) => void;
};

export type Env = Record<string, string | undefined>;

export type BaseContext = {
job: JobMeta;
log: LogFn;
env: Env;
};

export type BaseServerOptions = {
name?: string;
};

export type RequestHeaders = {
databaseId?: string;
workerId?: string;
jobId?: string;
};

export type BaseFunctionHandler<
P = unknown,
C extends BaseContext = BaseContext,
R = unknown
> = (params: P, context: C) => Promise<R> | R;

export const DEFAULT_DATABASE_NAME = 'constructive';
10 changes: 10 additions & 0 deletions packages/fn-core/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"outDir": "dist",
"rootDir": "src",
"declaration": true
},
"include": ["src/**/*.ts"],
"exclude": ["dist", "node_modules"]
}
24 changes: 24 additions & 0 deletions packages/fn-pgpm-runtime/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
{
"name": "@constructive-io/fn-pgpm-runtime",
"version": "1.0.0",
"description": "Runtime for pgpm-based Constructive functions — wraps handler in Express app with PgpmPackage, env options, and job callback support",
"author": "Constructive",
"private": true,
"main": "dist/index.js",
"types": "dist/index.d.ts",
"scripts": {
"build": "tsc -p tsconfig.json",
"clean": "rimraf dist"
},
"dependencies": {
"@constructive-io/fn-core": "workspace:^",
"@pgpmjs/core": "^6.2.0",
"@pgpmjs/env": "^2.11.0",
"@pgpmjs/logger": "^2.1.0",
"@pgpmjs/types": "^2.17.0"
},
"devDependencies": {
"@types/node": "^22.10.4",
"typescript": "^5.1.6"
}
}
26 changes: 26 additions & 0 deletions packages/fn-pgpm-runtime/src/context.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import type { Env, LogFn, RequestHeaders } from '@constructive-io/fn-core';
import type { PgpmPackage } from '@pgpmjs/core';
import type { PgpmOptions } from '@pgpmjs/types';
import type { PgpmFunctionContext } from './types';

export type PgpmServerResources = {
project: PgpmPackage;
options: PgpmOptions;
log: LogFn;
env: Env;
};

export const buildPgpmContext = (
headers: RequestHeaders,
resources: PgpmServerResources
): PgpmFunctionContext => ({
job: {
jobId: headers.jobId,
workerId: headers.workerId,
databaseId: headers.databaseId
},
project: resources.project,
options: resources.options,
log: resources.log,
env: resources.env
});
4 changes: 4 additions & 0 deletions packages/fn-pgpm-runtime/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export { createPgpmFunctionServer } from './server';
export { buildPgpmContext } from './context';
export type { PgpmServerResources } from './context';
export type { PgpmFunctionHandler, PgpmFunctionContext, PgpmServerOptions } from './types';
Loading