refactor: bare structure for new backend architecture and runtime; no behavior changes yet

This commit is contained in:
Haileyesus
2026-03-12 14:17:12 +03:00
parent b54cdf8168
commit e67738c9fc
40 changed files with 10316 additions and 9 deletions

View File

@@ -0,0 +1,21 @@
export type OpenApiPlan = {
status: 'planned';
description: string;
nextSteps: string[];
examples: Record<string, string>;
};
export const openApiPlan: OpenApiPlan = {
status: 'planned',
description: 'Day 1 placeholder for the shared OpenAPI registry and document builder.',
nextSteps: [
'Register global tags for auth, projects, files, git, taskmaster, agent, and providers.',
'Promote the endpoint inventory into explicit request and response schemas.',
'Publish /api/openapi.json and Swagger UI once schemas are in place.',
],
examples: {
authTag: 'Auth',
projectsTag: 'Projects',
providerTag: 'Providers',
},
};

View File

@@ -0,0 +1,36 @@
import type { ApiErrorShape, ApiMeta, ApiSuccessShape } from '../types/http.js';
export function createApiMeta(requestId?: string, startedAt?: string): ApiMeta {
return {
requestId,
startedAt,
};
}
export function createApiSuccessResponse<TData>(
data: TData,
meta?: ApiMeta
): ApiSuccessShape<TData> {
return {
success: true,
data,
meta,
};
}
export function createApiErrorResponse(
code: string,
message: string,
meta?: ApiMeta,
details?: unknown
): ApiErrorShape {
return {
success: false,
error: {
code,
message,
details,
},
meta,
};
}

View File

@@ -0,0 +1,9 @@
import type { NextFunction, Request, RequestHandler, Response } from 'express';
export function asyncHandler(
handler: (req: Request, res: Response, next: NextFunction) => Promise<unknown>
): RequestHandler {
return (req, res, next) => {
void Promise.resolve(handler(req, res, next)).catch(next);
};
}

View File

@@ -0,0 +1,30 @@
import type { NextFunction, Request, Response } from 'express';
import { AppError } from '../utils/app-error.js';
import { logger } from '../utils/logger.js';
import { createApiErrorResponse, createApiMeta } from './api-response.js';
import { getRequestContext } from './request-context.js';
export function errorHandler(
error: Error,
req: Request,
res: Response,
_next: NextFunction
): void {
const appError = error instanceof AppError ? error : new AppError(error.message);
const context = getRequestContext(req);
const payload = createApiErrorResponse(
appError.code,
appError.message,
createApiMeta(context?.requestId, context?.startedAt),
appError.details
);
logger.error(appError.message, {
code: appError.code,
statusCode: appError.statusCode,
requestId: context?.requestId,
});
res.status(appError.statusCode).json(payload);
}

View File

@@ -0,0 +1,15 @@
import type { Request, Response } from 'express';
import { createApiErrorResponse, createApiMeta } from './api-response.js';
import { getRequestContext } from './request-context.js';
export function notFoundHandler(req: Request, res: Response): void {
const context = getRequestContext(req);
const payload = createApiErrorResponse(
'NOT_FOUND',
`Route not found: ${req.originalUrl}`,
createApiMeta(context?.requestId, context?.startedAt)
);
res.status(404).json(payload);
}

View File

@@ -0,0 +1,27 @@
import { randomUUID } from 'crypto';
import type { NextFunction, Request, Response } from 'express';
import type { RequestContext } from '../types/http.js';
type RequestWithContext = Request & {
context?: RequestContext;
};
export function getRequestContext(req: Request): RequestContext | undefined {
return (req as RequestWithContext).context;
}
// give every request a context with a unique ID and timestamp for tracking purposes
export function requestContextMiddleware(req: Request, res: Response, next: NextFunction): void {
const requestId = randomUUID();
const startedAt = new Date().toISOString();
const context: RequestContext = {
requestId,
startedAt,
};
(req as RequestWithContext).context = context;
(res.locals as Record<string, unknown>).requestId = requestId;
next();
}

View File

@@ -0,0 +1,19 @@
import { WebSocketServer } from 'ws';
export type RuntimePaths = {
serverSrcDir: string;
serverDir: string;
projectRoot: string;
legacyRuntimePath: string;
bootstrapEntrypointPath: string;
};
export type AppLocals = {
requestId?: string;
wss?: WebSocketServer;
};
export type ServerApplication = {
runtimePaths: RuntimePaths;
start: () => Promise<void>;
};

View File

@@ -0,0 +1,71 @@
import type { Request } from 'express';
export type TransportKind = 'http' | 'sse' | 'ws';
/**
* Meta information about an API response, such as request ID and timing details.
* Different from RequestContext which is the internal server-side context for an
* incoming request.
*/
export type ApiMeta = {
requestId?: string;
startedAt?: string;
};
export type ApiSuccessShape<TData = unknown> = {
success: true;
data: TData;
meta?: ApiMeta;
};
export type ApiErrorShape = {
success: false;
error: {
code: string;
message: string;
details?: unknown;
};
meta?: ApiMeta;
};
/**
* Internal server-side context for an incoming request.
* It's the source object. It's different from ApiMeta which is
* meant for API responses.
*/
export type RequestContext = {
requestId: string;
startedAt: string;
};
export type AuthenticatedUser = {
id: number | string;
username?: string;
[key: string]: unknown;
};
export type AuthenticatedRequest = Request & {
context?: RequestContext;
user?: AuthenticatedUser;
};
export type EndpointInventoryRecord = {
transport: TransportKind;
method: string;
path: string;
tag: string;
authMode: string;
sourceFile: string;
sourceLine: number;
purpose: string;
consumerFiles: string[];
inputs: {
pathParams: string[];
queryParams: string[];
bodyHints: string[];
};
successShape: string;
errorShape: string;
sideEffects: string[];
priority: 'high' | 'medium' | 'low';
};

View File

@@ -0,0 +1,19 @@
export type AppErrorOptions = {
code?: string;
statusCode?: number;
details?: unknown;
};
export class AppError extends Error {
readonly code: string;
readonly statusCode: number;
readonly details?: unknown;
constructor(message: string, options: AppErrorOptions = {}) {
super(message);
this.name = 'AppError';
this.code = options.code ?? 'INTERNAL_ERROR';
this.statusCode = options.statusCode ?? 500;
this.details = options.details;
}
}

View File

@@ -0,0 +1,32 @@
type LoggerLevel = 'info' | 'warn' | 'error';
function formatMetadata(metadata?: Record<string, unknown>): string {
if (!metadata || Object.keys(metadata).length === 0) {
return '';
}
return ` ${JSON.stringify(metadata)}`;
}
function write(level: LoggerLevel, message: string, metadata?: Record<string, unknown>): void {
const prefix = `[server:${level}]`;
const formatted = `${prefix} ${message}${formatMetadata(metadata)}`;
if (level === 'error') {
console.error(formatted);
return;
}
if (level === 'warn') {
console.warn(formatted);
return;
}
console.log(formatted);
}
export const logger = {
info: (message: string, metadata?: Record<string, unknown>) => write('info', message, metadata),
warn: (message: string, metadata?: Record<string, unknown>) => write('warn', message, metadata),
error: (message: string, metadata?: Record<string, unknown>) => write('error', message, metadata),
};