File
|
baseConfigDirectory
(Optional)
|
|
Type
|
string
|
import chalk from 'chalk';
import { createServer, IncomingMessage, Server, ServerResponse } from 'http';
import { createServer as createNetServer } from 'net';
import type { DynamicFormConfiguration } from '@allianz/taly-core/schemas';
import { updateDynamicFormConfig } from './update-dynamic-form-config';
export const DF_EDITOR_SERVER_DEFAULT_PORT = 4300;
interface DfEditPayload {
pageId: string;
buildingBlockId: string;
config: DynamicFormConfiguration;
}
export interface DfEditorServerOptions {
configDirectory: string;
baseConfigDirectory?: string;
port: number;
}
export async function startDfEditorServer(
options: DfEditorServerOptions
): Promise<() => Promise<void>> {
const server = createServer((req, res) => {
setCorsHeaders(res);
if (req.method === 'OPTIONS') {
res.writeHead(204);
res.end();
return;
}
if (req.method === 'GET' && req.url === '/api/ping') {
return handlePing(res);
}
if (req.method === 'POST' && req.url === '/api/dynamic-form') {
return handleEditDynamicForm(req, res, options);
}
sendJson(res, 404, { error: 'Not found' });
});
await new Promise<void>((resolve, reject) => {
server.on('error', (err: NodeJS.ErrnoException) => {
if (err.code === 'EADDRINUSE') {
console.error(
chalk.red(
`Dynamic Form editor server port ${options.port} is already in use. Please choose a different port or stop the process using it.`
)
);
}
reject(err);
});
server.listen(options.port, () => {
console.log(`🚀 Dynamic Form editor server listening on http://localhost:${options.port}`);
resolve();
});
});
return () => closeServer(server);
}
function handlePing(res: ServerResponse): void {
sendJson(res, 200, { status: 'ok' });
}
function handleEditDynamicForm(
req: IncomingMessage,
res: ServerResponse,
options: DfEditorServerOptions
): void {
parseJsonBody<DfEditPayload>(req)
.then((body) => {
if (!body.pageId || !body.buildingBlockId || typeof body.config !== 'object') {
sendJson(res, 400, {
error:
'Payload must include "pageId" (string), "buildingBlockId" (string) and "config" (object).'
});
return;
}
try {
updateDynamicFormConfig(
{
configDirectory: options.configDirectory,
baseConfigDirectory: options.baseConfigDirectory
},
{ pageId: body.pageId, buildingBlockId: body.buildingBlockId, config: body.config }
);
sendJson(res, 200, { success: true });
} catch (err) {
const message = err instanceof Error ? err.message : 'Unknown error';
const isNotFound = message.includes('not found');
sendJson(res, isNotFound ? 404 : 500, { error: message });
}
})
.catch(() => {
sendJson(res, 400, { error: 'Invalid JSON body.' });
});
}
function parseJsonBody<T>(req: IncomingMessage): Promise<T> {
return new Promise((resolve, reject) => {
const chunks: Buffer[] = [];
req.on('data', (chunk: Buffer) => chunks.push(chunk));
req.on('end', () => {
try {
resolve(JSON.parse(Buffer.concat(chunks).toString()));
} catch (err) {
reject(err);
}
});
req.on('error', reject);
});
}
function sendJson(res: ServerResponse, statusCode: number, data: unknown): void {
res.writeHead(statusCode, { 'Content-Type': 'application/json' });
res.end(JSON.stringify(data));
}
function setCorsHeaders(res: ServerResponse): void {
res.setHeader('Access-Control-Allow-Origin', '*');
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
}
function isPortAvailable(port: number): Promise<boolean> {
return new Promise((resolve) => {
const server = createNetServer();
server.once('error', () => resolve(false));
server.once('listening', () => {
server.close(() => resolve(true));
});
server.listen(port);
});
}
export async function findFreePort(): Promise<number> {
const maxAttempts = 100;
let port = DF_EDITOR_SERVER_DEFAULT_PORT;
for (let attempt = 0; attempt < maxAttempts; attempt++) {
if (await isPortAvailable(port)) {
return port;
}
port++;
}
throw new Error(
`Could not find a free port after ${maxAttempts} attempts (tried ${DF_EDITOR_SERVER_DEFAULT_PORT}–${
port - 1
}).`
);
}
function closeServer(server: Server): Promise<void> {
return new Promise((resolve, reject) => {
server.close((err) => {
if (err) {
reject(err);
} else {
resolve();
}
});
});
}