@facetlayer/prism-framework 0.4.0 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +176 -8
- package/dist/Errors.d.ts +38 -0
- package/dist/Errors.d.ts.map +1 -0
- package/dist/Metrics.d.ts +5 -0
- package/dist/Metrics.d.ts.map +1 -0
- package/dist/RequestContext.d.ts +17 -0
- package/dist/RequestContext.d.ts.map +1 -0
- package/dist/ServiceDefinition.d.ts +16 -0
- package/dist/ServiceDefinition.d.ts.map +1 -0
- package/dist/app/PrismApp.d.ts +31 -0
- package/dist/app/PrismApp.d.ts.map +1 -0
- package/dist/app/callEndpoint.d.ts +13 -0
- package/dist/app/callEndpoint.d.ts.map +1 -0
- package/dist/app/validateApp.d.ts +20 -0
- package/dist/app/validateApp.d.ts.map +1 -0
- package/dist/authorization/AuthSource.d.ts +8 -0
- package/dist/authorization/AuthSource.d.ts.map +1 -0
- package/dist/authorization/Authorization.d.ts +24 -0
- package/dist/authorization/Authorization.d.ts.map +1 -0
- package/dist/authorization/Resource.d.ts +5 -0
- package/dist/authorization/Resource.d.ts.map +1 -0
- package/dist/authorization/index.d.ts +5 -0
- package/dist/authorization/index.d.ts.map +1 -0
- package/dist/cli.js +1 -1
- package/dist/databases/DatabaseInitializationOptions.d.ts +9 -0
- package/dist/databases/DatabaseInitializationOptions.d.ts.map +1 -0
- package/dist/databases/DatabaseSetup.d.ts +3 -0
- package/dist/databases/DatabaseSetup.d.ts.map +1 -0
- package/dist/endpoints/createEndpoint.d.ts +4 -0
- package/dist/endpoints/createEndpoint.d.ts.map +1 -0
- package/dist/endpoints/getEffectiveOperationId.d.ts +19 -0
- package/dist/endpoints/getEffectiveOperationId.d.ts.map +1 -0
- package/dist/env/Env.d.ts +2 -0
- package/dist/env/Env.d.ts.map +1 -0
- package/dist/index.d.ts +34 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1364 -0
- package/dist/launch/launchConfig.d.ts +18 -0
- package/dist/launch/launchConfig.d.ts.map +1 -0
- package/dist/logging/index.d.ts +9 -0
- package/dist/logging/index.d.ts.map +1 -0
- package/dist/sse/ConnectionManager.d.ts +23 -0
- package/dist/sse/ConnectionManager.d.ts.map +1 -0
- package/dist/stdin/StdinServer.d.ts +38 -0
- package/dist/stdin/StdinServer.d.ts.map +1 -0
- package/dist/web/EndpointListing.d.ts +3 -0
- package/dist/web/EndpointListing.d.ts.map +1 -0
- package/dist/web/ExpressAppSetup.d.ts +18 -0
- package/dist/web/ExpressAppSetup.d.ts.map +1 -0
- package/dist/web/ExpressEndpointSetup.d.ts +31 -0
- package/dist/web/ExpressEndpointSetup.d.ts.map +1 -0
- package/dist/web/SseResponse.d.ts +15 -0
- package/dist/web/SseResponse.d.ts.map +1 -0
- package/dist/web/ViteIntegration.d.ts +19 -0
- package/dist/web/ViteIntegration.d.ts.map +1 -0
- package/dist/web/corsMiddleware.d.ts +14 -0
- package/dist/web/corsMiddleware.d.ts.map +1 -0
- package/dist/web/localhostOnlyMiddleware.d.ts +3 -0
- package/dist/web/localhostOnlyMiddleware.d.ts.map +1 -0
- package/dist/web/openapi/OpenAPI.d.ts +37 -0
- package/dist/web/openapi/OpenAPI.d.ts.map +1 -0
- package/dist/web/openapi/validateServicesForOpenapi.d.ts +32 -0
- package/dist/web/openapi/validateServicesForOpenapi.d.ts.map +1 -0
- package/dist/web/requestContextMiddleware.d.ts +3 -0
- package/dist/web/requestContextMiddleware.d.ts.map +1 -0
- package/docs/authorization.md +281 -0
- package/docs/cors-setup.md +172 -0
- package/docs/creating-services.md +220 -0
- package/docs/database-setup.md +134 -0
- package/docs/endpoint-tools.md +1 -11
- package/docs/env-files.md +12 -1
- package/docs/error-handling.md +70 -0
- package/docs/getting-started.md +22 -12
- package/docs/launch-configuration.md +223 -0
- package/docs/overview.md +62 -0
- package/docs/server-setup.md +144 -0
- package/docs/source-directory-organization.md +115 -0
- package/docs/stdin-protocol.md +176 -0
- package/package.json +42 -9
- package/src/Errors.ts +120 -0
- package/src/Metrics.ts +53 -0
- package/src/RequestContext.ts +36 -0
- package/src/ServiceDefinition.ts +35 -0
- package/src/__tests__/Authorization.test.ts +350 -0
- package/src/__tests__/Errors.test.ts +378 -0
- package/src/__tests__/ListEndpoints.test.ts +98 -0
- package/src/__tests__/PrismApp.test.ts +274 -0
- package/src/__tests__/RequestContext.test.ts +295 -0
- package/src/__tests__/SseResponse.test.ts +189 -0
- package/src/__tests__/StdinServer.test.ts +304 -0
- package/src/__tests__/corsMiddleware.test.ts +293 -0
- package/src/__tests__/createEndpoint.test.ts +412 -0
- package/src/__tests__/validateApp.test.ts +206 -0
- package/src/app/PrismApp.ts +117 -0
- package/src/app/callEndpoint.ts +55 -0
- package/src/app/validateApp.ts +78 -0
- package/src/authorization/AuthSource.ts +14 -0
- package/src/authorization/Authorization.ts +78 -0
- package/src/authorization/Resource.ts +8 -0
- package/src/authorization/index.ts +4 -0
- package/src/databases/DatabaseInitializationOptions.ts +9 -0
- package/src/databases/DatabaseSetup.ts +19 -0
- package/src/endpoints/createEndpoint.ts +39 -0
- package/src/endpoints/getEffectiveOperationId.ts +90 -0
- package/src/env/Env.ts +23 -0
- package/src/index.ts +78 -0
- package/src/launch/launchConfig.ts +59 -0
- package/src/list-endpoints-command.ts +1 -1
- package/src/logging/index.ts +25 -0
- package/src/sse/ConnectionManager.ts +79 -0
- package/src/stdin/StdinServer.ts +129 -0
- package/src/web/EndpointListing.ts +166 -0
- package/src/web/ExpressAppSetup.ts +125 -0
- package/src/web/ExpressEndpointSetup.ts +178 -0
- package/src/web/SseResponse.ts +78 -0
- package/src/web/ViteIntegration.ts +72 -0
- package/src/web/__tests__/OpenAPI.invalidZodSchemas.test.ts +250 -0
- package/src/web/corsMiddleware.ts +63 -0
- package/src/web/localhostOnlyMiddleware.ts +19 -0
- package/src/web/openapi/OpenAPI.ts +248 -0
- package/src/web/openapi/validateServicesForOpenapi.ts +76 -0
- package/src/web/requestContextMiddleware.ts +25 -0
- package/.claude/settings.local.json +0 -20
- package/CHANGELOG +0 -28
- package/CLAUDE.md +0 -44
- package/build.mts +0 -8
- package/test/call-command.test.ts +0 -96
- package/test/generate-api-clients.test.ts +0 -33
- package/test/generate-api-clients.test.ts.disabled +0 -75
- package/tsconfig.json +0 -21
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import type { EndpointDefinition } from "../web/ExpressEndpointSetup.ts";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Known bad values for operationId that indicate the user didn't set it properly.
|
|
5
|
+
* These are typically default function names that don't provide meaningful identification.
|
|
6
|
+
*/
|
|
7
|
+
const INVALID_OPERATION_IDS = new Set([
|
|
8
|
+
'handler',
|
|
9
|
+
'anonymous',
|
|
10
|
+
'',
|
|
11
|
+
]);
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Validates that an operationId is not a known bad value.
|
|
15
|
+
*/
|
|
16
|
+
export function isValidOperationId(operationId: string | undefined): boolean {
|
|
17
|
+
if (operationId === undefined) {
|
|
18
|
+
return true; // undefined is fine, we'll auto-generate
|
|
19
|
+
}
|
|
20
|
+
return !INVALID_OPERATION_IDS.has(operationId);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Converts a hyphenated string to camelCase.
|
|
25
|
+
* e.g., "undo-redo-state" -> "undoRedoState"
|
|
26
|
+
* Preserves existing casing when there are no hyphens.
|
|
27
|
+
* e.g., "userId" -> "userId"
|
|
28
|
+
*/
|
|
29
|
+
function toCamelCase(str: string): string {
|
|
30
|
+
if (!str.includes('-')) {
|
|
31
|
+
return str;
|
|
32
|
+
}
|
|
33
|
+
return str
|
|
34
|
+
.split('-')
|
|
35
|
+
.map((part, index) => {
|
|
36
|
+
if (index === 0) {
|
|
37
|
+
return part.toLowerCase();
|
|
38
|
+
}
|
|
39
|
+
return part.charAt(0).toUpperCase() + part.slice(1).toLowerCase();
|
|
40
|
+
})
|
|
41
|
+
.join('');
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Generates an operationId from the method and path.
|
|
46
|
+
* e.g., "GET /users/:id" -> "getUsers_id"
|
|
47
|
+
* e.g., "GET /users/:id/undo-redo-state" -> "getUsers_idUndoRedoState"
|
|
48
|
+
*/
|
|
49
|
+
export function generateOperationIdFromPath(method: string, path: string): string {
|
|
50
|
+
// Convert path to camelCase identifier
|
|
51
|
+
// e.g., "/users/:id/posts" -> "Users_idPosts"
|
|
52
|
+
const pathPart = path
|
|
53
|
+
.split('/')
|
|
54
|
+
.filter(Boolean)
|
|
55
|
+
.map((segment, index) => {
|
|
56
|
+
if (segment.startsWith(':') || segment.startsWith('{')) {
|
|
57
|
+
// Path parameter: :id -> _id, {id} -> _id
|
|
58
|
+
const paramName = segment.replace(/^[:{}]+|[}]+$/g, '');
|
|
59
|
+
return '_' + toCamelCase(paramName);
|
|
60
|
+
}
|
|
61
|
+
// Convert to camelCase and capitalize first letter
|
|
62
|
+
const camelCased = toCamelCase(segment);
|
|
63
|
+
return camelCased.charAt(0).toUpperCase() + camelCased.slice(1);
|
|
64
|
+
})
|
|
65
|
+
.join('');
|
|
66
|
+
|
|
67
|
+
return method.toLowerCase() + pathPart;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Gets the effective operationId for an endpoint using the priority:
|
|
72
|
+
* 1. Explicit operationId if defined
|
|
73
|
+
* 2. Handler function name if not a known bad value
|
|
74
|
+
* 3. Auto-generated from method + path
|
|
75
|
+
*/
|
|
76
|
+
export function getEffectiveOperationId(definition: EndpointDefinition): string {
|
|
77
|
+
// Priority 1: Explicit operationId
|
|
78
|
+
if (definition.operationId && isValidOperationId(definition.operationId)) {
|
|
79
|
+
return definition.operationId;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Priority 2: Handler function name (if valid)
|
|
83
|
+
const handlerName = definition.handler.name;
|
|
84
|
+
if (handlerName && isValidOperationId(handlerName)) {
|
|
85
|
+
return handlerName;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Priority 3: Auto-generate from path
|
|
89
|
+
return generateOperationIdFromPath(definition.method, definition.path);
|
|
90
|
+
}
|
package/src/env/Env.ts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { config } from 'dotenv';
|
|
2
|
+
import { existsSync } from 'fs';
|
|
3
|
+
import { dirname, join } from 'path';
|
|
4
|
+
import { z } from 'zod';
|
|
5
|
+
|
|
6
|
+
// Recursively find the nearest .env file
|
|
7
|
+
function findEnvFile(dir: string): string {
|
|
8
|
+
const envFile = join(dir, '.env');
|
|
9
|
+
if (existsSync(envFile)) {
|
|
10
|
+
return envFile;
|
|
11
|
+
}
|
|
12
|
+
const parentDir = dirname(dir);
|
|
13
|
+
if (parentDir === dir) {
|
|
14
|
+
throw new Error('No .env file found');
|
|
15
|
+
}
|
|
16
|
+
return findEnvFile(parentDir);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
config({
|
|
20
|
+
quiet: true,
|
|
21
|
+
path: findEnvFile(process.cwd()),
|
|
22
|
+
});
|
|
23
|
+
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
// Framework exports
|
|
2
|
+
export {
|
|
3
|
+
BadRequestError,
|
|
4
|
+
ConflictError,
|
|
5
|
+
ForbiddenError,
|
|
6
|
+
HttpError,
|
|
7
|
+
NotFoundError,
|
|
8
|
+
NotImplementedError,
|
|
9
|
+
ServiceUnavailableError,
|
|
10
|
+
UnauthorizedError,
|
|
11
|
+
ValidationError,
|
|
12
|
+
createErrorFromStatus,
|
|
13
|
+
isHttpError,
|
|
14
|
+
} from './Errors.ts';
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
export { createExpressApp, startServer } from './web/ExpressAppSetup.ts';
|
|
18
|
+
export type { ServerSetupConfig, WebConfig } from './web/ExpressAppSetup.ts';
|
|
19
|
+
|
|
20
|
+
export { getMetrics, recordHttpRequest, recordHttpResponse } from './Metrics.ts';
|
|
21
|
+
|
|
22
|
+
export type { RequestContext } from './RequestContext.ts';
|
|
23
|
+
export { getCurrentRequestContext, withRequestContext } from './RequestContext.ts';
|
|
24
|
+
export { requestContextMiddleware } from './web/requestContextMiddleware.ts';
|
|
25
|
+
|
|
26
|
+
export type { MiddlewareDefinition, ServiceDefinition } from './ServiceDefinition.ts';
|
|
27
|
+
|
|
28
|
+
// Authorization exports
|
|
29
|
+
export type { CookieAuthSource, AuthSource, Permission, Resource, UserPermissions } from './authorization/index.ts';
|
|
30
|
+
export { Authorization } from './authorization/index.ts';
|
|
31
|
+
|
|
32
|
+
// Web framework exports
|
|
33
|
+
export { corsMiddleware } from './web/corsMiddleware.ts';
|
|
34
|
+
export type { CorsConfig } from './web/corsMiddleware.ts';
|
|
35
|
+
export type { EndpointDefinition } from './web/ExpressEndpointSetup.ts';
|
|
36
|
+
export {
|
|
37
|
+
getRequestDataFromReq,
|
|
38
|
+
mountMiddleware,
|
|
39
|
+
mountMiddlewares,
|
|
40
|
+
mountPrismApp,
|
|
41
|
+
} from './web/ExpressEndpointSetup.ts';
|
|
42
|
+
export { localhostOnlyMiddleware } from './web/localhostOnlyMiddleware.ts';
|
|
43
|
+
export { SseResponse } from './web/SseResponse.ts';
|
|
44
|
+
export { createEndpoint } from './endpoints/createEndpoint.ts';
|
|
45
|
+
|
|
46
|
+
// OpenAPI exports
|
|
47
|
+
export type { OpenAPIDocumentInfo, ParseExpressPathForOpenAPIResult } from './web/openapi/OpenAPI.ts';
|
|
48
|
+
export { generateOpenAPISchema, parseExpressPathForOpenAPI } from './web/openapi/OpenAPI.ts';
|
|
49
|
+
|
|
50
|
+
// Endpoint listing exports
|
|
51
|
+
export { createListingEndpoints } from './web/EndpointListing.ts';
|
|
52
|
+
|
|
53
|
+
// SSE Connection Management exports
|
|
54
|
+
export { ConnectionManager } from './sse/ConnectionManager.ts';
|
|
55
|
+
|
|
56
|
+
// App exports
|
|
57
|
+
export { PrismApp as App } from './app/PrismApp.ts';
|
|
58
|
+
export type { PrismAppConfig as AppConfig } from './app/PrismApp.ts';
|
|
59
|
+
|
|
60
|
+
// Launch configuration exports
|
|
61
|
+
export type { LoggingSettings, LaunchConfig } from './launch/launchConfig.ts';
|
|
62
|
+
export { getDatabaseConfig, getLaunchConfig, getLoggingConfig, setLaunchConfig } from './launch/launchConfig.ts';
|
|
63
|
+
|
|
64
|
+
// Database setup exports
|
|
65
|
+
export type { DatabaseInitializationOptions } from './databases/DatabaseInitializationOptions.ts';
|
|
66
|
+
export type { MigrationBehavior } from '@facetlayer/sqlite-wrapper';
|
|
67
|
+
export { getStatementsForDatabase } from './databases/DatabaseSetup.ts';
|
|
68
|
+
|
|
69
|
+
// Endpoint calling exports
|
|
70
|
+
export type { CallEndpointOptions } from './app/callEndpoint.ts';
|
|
71
|
+
export { callEndpoint } from './app/callEndpoint.ts';
|
|
72
|
+
|
|
73
|
+
// Stdin/stdout protocol exports
|
|
74
|
+
export type { StdinRequest, StdinResponse, StdinServerConfig } from './stdin/StdinServer.ts';
|
|
75
|
+
export { startStdinServer } from './stdin/StdinServer.ts';
|
|
76
|
+
|
|
77
|
+
// Logging exports
|
|
78
|
+
export { setLogStderr } from './logging/index.ts';
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import type { DatabaseInitializationOptions } from '../databases/DatabaseInitializationOptions.ts';
|
|
2
|
+
import type { LoadDatabaseFn } from '@facetlayer/sqlite-wrapper';
|
|
3
|
+
|
|
4
|
+
/*
|
|
5
|
+
launchConfig
|
|
6
|
+
|
|
7
|
+
This is a global settings object that is set by the entry point when the app starts up. It contains
|
|
8
|
+
settings for logging and database initialization.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
export interface LoggingSettings {
|
|
12
|
+
databaseFilename: string;
|
|
13
|
+
enableConsoleLogging: boolean;
|
|
14
|
+
loadDatabase: LoadDatabaseFn;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface LaunchConfig {
|
|
18
|
+
logging?: LoggingSettings;
|
|
19
|
+
database?: {
|
|
20
|
+
[databaseName: string]: DatabaseInitializationOptions;
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
let _config: LaunchConfig | undefined;
|
|
25
|
+
|
|
26
|
+
export function getLaunchConfig(): LaunchConfig {
|
|
27
|
+
if (!_config) {
|
|
28
|
+
throw new Error('Launch config not initialized');
|
|
29
|
+
}
|
|
30
|
+
return _config;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function setLaunchConfig(config: LaunchConfig): void {
|
|
34
|
+
if (_config) {
|
|
35
|
+
throw new Error('Launch config already initialized');
|
|
36
|
+
}
|
|
37
|
+
_config = config;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function getDatabaseConfig(databaseName: string): DatabaseInitializationOptions {
|
|
41
|
+
if (!_config) {
|
|
42
|
+
throw new Error(
|
|
43
|
+
'Launch config not initialized (tried to getDatabaseConfig for: ' + databaseName + ')'
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (!_config.database[databaseName]) {
|
|
48
|
+
throw new Error('Database config not found (tried to getDatabaseConfig for: ' + databaseName + ')');
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return _config.database[databaseName];
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function getLoggingConfig(): LoggingSettings {
|
|
55
|
+
if (!_config) {
|
|
56
|
+
throw new Error('Launch config not initialized (in getLoggingConfig)');
|
|
57
|
+
}
|
|
58
|
+
return _config.logging;
|
|
59
|
+
}
|
|
@@ -7,7 +7,7 @@ export async function listEndpoints(baseUrl: string): Promise<void> {
|
|
|
7
7
|
try {
|
|
8
8
|
const response = await callEndpoint({
|
|
9
9
|
baseUrl,
|
|
10
|
-
positionalArgs: ['GET', '/endpoints.json'],
|
|
10
|
+
positionalArgs: ['GET', '/api/endpoints.json'],
|
|
11
11
|
namedArgs: {},
|
|
12
12
|
quiet: true,
|
|
13
13
|
});
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
let useStderr = false;
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* When enabled, logInfo writes to stderr instead of stdout.
|
|
5
|
+
* This is used in stdin protocol mode to avoid corrupting the JSON protocol on stdout.
|
|
6
|
+
*/
|
|
7
|
+
export function setLogStderr(enabled: boolean): void {
|
|
8
|
+
useStderr = enabled;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function logInfo(...args: any[]) {
|
|
12
|
+
if (useStderr) {
|
|
13
|
+
console.error(...args);
|
|
14
|
+
} else {
|
|
15
|
+
console.log(...args);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function logWarn(...args: any[]) {
|
|
20
|
+
console.warn(...args);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function logError(...args: any[]) {
|
|
24
|
+
console.error(...args);
|
|
25
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import type { SseResponse } from '../web/SseResponse.ts';
|
|
2
|
+
|
|
3
|
+
interface SetupOptions {
|
|
4
|
+
managerName: string;
|
|
5
|
+
logDebug?: (message: string) => void;
|
|
6
|
+
logError?: (message: string, error?: any) => void;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
interface Connection {
|
|
10
|
+
key: string;
|
|
11
|
+
sseResponse: SseResponse;
|
|
12
|
+
connectedAt: Date;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/*
|
|
16
|
+
* ConnectionManager
|
|
17
|
+
*
|
|
18
|
+
* This keeps an in-memory map of active connections. Each one corresponds to
|
|
19
|
+
* an active HTTP SSE request that is currently open.
|
|
20
|
+
*
|
|
21
|
+
* This class can be used to list active connections or to push a message to
|
|
22
|
+
* a connection.
|
|
23
|
+
*/
|
|
24
|
+
export class ConnectionManager<EventType extends object> {
|
|
25
|
+
private connections: Map<string, Connection[]> = new Map();
|
|
26
|
+
private managerName: string;
|
|
27
|
+
private logDebug: (message: string) => void;
|
|
28
|
+
private logError: (message: string, error?: any) => void;
|
|
29
|
+
|
|
30
|
+
constructor(options: SetupOptions) {
|
|
31
|
+
this.managerName = options.managerName;
|
|
32
|
+
this.logDebug = options.logDebug || (() => {});
|
|
33
|
+
this.logError = options.logError || (() => {});
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
addConnection(key: string, sseResponse: SseResponse): void {
|
|
37
|
+
this.logDebug(`${this.managerName} (ConnectionManager) adding connection for: ${key}`);
|
|
38
|
+
|
|
39
|
+
const connectionList = this.connections.get(key) || [];
|
|
40
|
+
const connection = {
|
|
41
|
+
key,
|
|
42
|
+
sseResponse,
|
|
43
|
+
connectedAt: new Date(),
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
connectionList.push(connection);
|
|
47
|
+
this.connections.set(key, connectionList);
|
|
48
|
+
|
|
49
|
+
// Clean up when connection closes
|
|
50
|
+
sseResponse.onClose(() => {
|
|
51
|
+
this.logDebug(`${this.managerName} (ConnectionManager) closed connection for: ${key}`);
|
|
52
|
+
|
|
53
|
+
const updatedList = this.connections.get(key)?.filter(c => c !== connection) || [];
|
|
54
|
+
if (updatedList.length === 0) {
|
|
55
|
+
this.connections.delete(key);
|
|
56
|
+
} else {
|
|
57
|
+
this.connections.set(key, updatedList);
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
getConnections(key: string): Connection[] {
|
|
63
|
+
return this.connections.get(key) || [];
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async postEvent(key: string, event: EventType): Promise<void> {
|
|
67
|
+
const connections = this.getConnections(key);
|
|
68
|
+
|
|
69
|
+
for (const connection of connections) {
|
|
70
|
+
if (connection.sseResponse.isOpen()) {
|
|
71
|
+
try {
|
|
72
|
+
connection.sseResponse.send(event as object);
|
|
73
|
+
} catch (error) {
|
|
74
|
+
this.logError('Error sending SSE event:', error);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { createInterface } from 'readline';
|
|
2
|
+
import { PrismApp } from '../app/PrismApp.ts';
|
|
3
|
+
import { isHttpError } from '../Errors.ts';
|
|
4
|
+
import { validateAppOrThrow } from '../app/validateApp.ts';
|
|
5
|
+
import { setLogStderr } from '../logging/index.ts';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* JSON message sent from the parent process to the stdin server.
|
|
9
|
+
*/
|
|
10
|
+
export interface StdinRequest {
|
|
11
|
+
/** Unique request ID for correlating responses. */
|
|
12
|
+
id: string;
|
|
13
|
+
/** HTTP method (GET, POST, PUT, DELETE, PATCH). */
|
|
14
|
+
method: string;
|
|
15
|
+
/** Endpoint path, e.g. "/users" or "/users/123". */
|
|
16
|
+
path: string;
|
|
17
|
+
/** Request body / input data. */
|
|
18
|
+
body?: any;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* JSON message sent back from the stdin server to the parent process.
|
|
23
|
+
*/
|
|
24
|
+
export interface StdinResponse {
|
|
25
|
+
/** Matches the request ID. */
|
|
26
|
+
id: string;
|
|
27
|
+
/** HTTP-style status code. */
|
|
28
|
+
status: number;
|
|
29
|
+
/** Response body. */
|
|
30
|
+
body: any;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface StdinServerConfig {
|
|
34
|
+
app: PrismApp;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Start processing requests from stdin and writing responses to stdout.
|
|
39
|
+
*
|
|
40
|
+
* Each line on stdin must be a JSON-encoded StdinRequest.
|
|
41
|
+
* Each response is a JSON-encoded StdinResponse written as a single line to stdout.
|
|
42
|
+
*
|
|
43
|
+
* When stdin closes, the process exits.
|
|
44
|
+
*/
|
|
45
|
+
export function startStdinServer(config: StdinServerConfig): void {
|
|
46
|
+
// Redirect logInfo to stderr so it doesn't corrupt the stdout JSON protocol
|
|
47
|
+
setLogStderr(true);
|
|
48
|
+
|
|
49
|
+
validateAppOrThrow(config.app);
|
|
50
|
+
|
|
51
|
+
const app = config.app;
|
|
52
|
+
|
|
53
|
+
const rl = createInterface({
|
|
54
|
+
input: process.stdin,
|
|
55
|
+
terminal: false,
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
function sendResponse(response: StdinResponse): void {
|
|
59
|
+
process.stdout.write(JSON.stringify(response) + '\n');
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function sendError(id: string | null, status: number, message: string, details?: any): void {
|
|
63
|
+
const response: StdinResponse = {
|
|
64
|
+
id: id ?? 'unknown',
|
|
65
|
+
status,
|
|
66
|
+
body: { message, ...(details ? { details } : {}) },
|
|
67
|
+
};
|
|
68
|
+
sendResponse(response);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
rl.on('line', async (line: string) => {
|
|
72
|
+
let request: StdinRequest;
|
|
73
|
+
|
|
74
|
+
// Parse the JSON request
|
|
75
|
+
try {
|
|
76
|
+
request = JSON.parse(line);
|
|
77
|
+
} catch {
|
|
78
|
+
sendError(null, 400, 'Invalid JSON');
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (!request.id || !request.method || !request.path) {
|
|
83
|
+
sendError(request?.id ?? null, 400, 'Missing required fields: id, method, path');
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
try {
|
|
88
|
+
const result = await app.callEndpoint({
|
|
89
|
+
method: request.method.toUpperCase(),
|
|
90
|
+
path: request.path,
|
|
91
|
+
input: request.body ?? {},
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
sendResponse({
|
|
95
|
+
id: request.id,
|
|
96
|
+
status: 200,
|
|
97
|
+
body: result,
|
|
98
|
+
});
|
|
99
|
+
} catch (error: any) {
|
|
100
|
+
if (isHttpError(error)) {
|
|
101
|
+
sendResponse({
|
|
102
|
+
id: request.id,
|
|
103
|
+
status: error.statusCode,
|
|
104
|
+
body: {
|
|
105
|
+
message: error.message,
|
|
106
|
+
...(error.details ? { details: error.details } : {}),
|
|
107
|
+
},
|
|
108
|
+
});
|
|
109
|
+
} else {
|
|
110
|
+
sendResponse({
|
|
111
|
+
id: request.id,
|
|
112
|
+
status: 500,
|
|
113
|
+
body: { message: error.message ?? 'Internal Server Error' },
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
rl.on('close', () => {
|
|
120
|
+
process.exit(0);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
// Signal readiness to the parent process
|
|
124
|
+
sendResponse({
|
|
125
|
+
id: '_ready',
|
|
126
|
+
status: 200,
|
|
127
|
+
body: { message: 'stdin server ready' },
|
|
128
|
+
});
|
|
129
|
+
}
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import { createEndpoint, type EndpointDefinition } from './ExpressEndpointSetup.ts';
|
|
2
|
+
import { readFileSync } from 'fs';
|
|
3
|
+
import { join } from 'path';
|
|
4
|
+
import type { Response } from 'express';
|
|
5
|
+
|
|
6
|
+
export function createListingEndpoints(endpoints: EndpointDefinition[]) {
|
|
7
|
+
return [
|
|
8
|
+
createEndpoint({
|
|
9
|
+
method: 'GET',
|
|
10
|
+
path: '/endpoints',
|
|
11
|
+
description: 'Lists all available endpoints in the system',
|
|
12
|
+
handler: () => {
|
|
13
|
+
const htmlContent = generateEndpointsHTML(endpoints);
|
|
14
|
+
|
|
15
|
+
return {
|
|
16
|
+
sendHttpResponse: (res: Response) => {
|
|
17
|
+
res.setHeader('Content-Type', 'text/html');
|
|
18
|
+
res.send(htmlContent);
|
|
19
|
+
},
|
|
20
|
+
};
|
|
21
|
+
},
|
|
22
|
+
}),
|
|
23
|
+
|
|
24
|
+
createEndpoint({
|
|
25
|
+
method: 'GET',
|
|
26
|
+
path: '/endpoints.json',
|
|
27
|
+
description: 'Lists all available endpoints in the system (JSON format)',
|
|
28
|
+
handler: () => {
|
|
29
|
+
const endpointsData = endpoints.map(endpoint => ({
|
|
30
|
+
method: endpoint.method,
|
|
31
|
+
path: endpoint.path,
|
|
32
|
+
description: endpoint.description || 'No description',
|
|
33
|
+
}));
|
|
34
|
+
|
|
35
|
+
return {
|
|
36
|
+
endpoints: endpointsData,
|
|
37
|
+
};
|
|
38
|
+
},
|
|
39
|
+
}),
|
|
40
|
+
|
|
41
|
+
createEndpoint({
|
|
42
|
+
method: 'GET',
|
|
43
|
+
path: '/changelog',
|
|
44
|
+
description: 'Serves the CHANGELOG.md file',
|
|
45
|
+
handler: () => {
|
|
46
|
+
const changelogPath = join(process.cwd(), 'CHANGELOG.md');
|
|
47
|
+
const changelogContent = readFileSync(changelogPath, 'utf8');
|
|
48
|
+
|
|
49
|
+
return {
|
|
50
|
+
sendHttpResponse: (res: Response) => {
|
|
51
|
+
res.setHeader('Content-Type', 'text/plain');
|
|
52
|
+
res.send(changelogContent);
|
|
53
|
+
},
|
|
54
|
+
};
|
|
55
|
+
},
|
|
56
|
+
}),
|
|
57
|
+
|
|
58
|
+
createEndpoint({
|
|
59
|
+
method: 'GET',
|
|
60
|
+
path: '/server-info',
|
|
61
|
+
description: 'Returns server information including name',
|
|
62
|
+
handler: () => {
|
|
63
|
+
return {
|
|
64
|
+
name: 'prism-framework',
|
|
65
|
+
};
|
|
66
|
+
},
|
|
67
|
+
}),
|
|
68
|
+
];
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function generateEndpointsHTML(endpoints: EndpointDefinition[]): string {
|
|
72
|
+
const endpointRows = endpoints
|
|
73
|
+
.map(
|
|
74
|
+
endpoint => `
|
|
75
|
+
<tr>
|
|
76
|
+
<td><code>${endpoint.method}</code></td>
|
|
77
|
+
<td><code>${endpoint.path}</code></td>
|
|
78
|
+
<td>${endpoint.description || 'No description'}</td>
|
|
79
|
+
</tr>
|
|
80
|
+
`
|
|
81
|
+
)
|
|
82
|
+
.join('');
|
|
83
|
+
|
|
84
|
+
return `
|
|
85
|
+
<!DOCTYPE html>
|
|
86
|
+
<html lang="en">
|
|
87
|
+
<head>
|
|
88
|
+
<meta charset="UTF-8">
|
|
89
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
90
|
+
<title>API Endpoints</title>
|
|
91
|
+
<style>
|
|
92
|
+
body {
|
|
93
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
94
|
+
max-width: 1200px;
|
|
95
|
+
margin: 0 auto;
|
|
96
|
+
padding: 20px;
|
|
97
|
+
background-color: #f5f5f5;
|
|
98
|
+
}
|
|
99
|
+
.container {
|
|
100
|
+
background: white;
|
|
101
|
+
border-radius: 8px;
|
|
102
|
+
padding: 30px;
|
|
103
|
+
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
|
|
104
|
+
}
|
|
105
|
+
h1 {
|
|
106
|
+
color: #333;
|
|
107
|
+
border-bottom: 3px solid #007acc;
|
|
108
|
+
padding-bottom: 10px;
|
|
109
|
+
}
|
|
110
|
+
table {
|
|
111
|
+
width: 100%;
|
|
112
|
+
border-collapse: collapse;
|
|
113
|
+
margin-top: 20px;
|
|
114
|
+
}
|
|
115
|
+
th, td {
|
|
116
|
+
text-align: left;
|
|
117
|
+
padding: 12px;
|
|
118
|
+
border-bottom: 1px solid #ddd;
|
|
119
|
+
}
|
|
120
|
+
th {
|
|
121
|
+
background-color: #f8f9fa;
|
|
122
|
+
font-weight: 600;
|
|
123
|
+
color: #495057;
|
|
124
|
+
}
|
|
125
|
+
tr:hover {
|
|
126
|
+
background-color: #f8f9fa;
|
|
127
|
+
}
|
|
128
|
+
code {
|
|
129
|
+
background-color: #e9ecef;
|
|
130
|
+
padding: 2px 6px;
|
|
131
|
+
border-radius: 3px;
|
|
132
|
+
font-family: 'Monaco', 'Consolas', monospace;
|
|
133
|
+
}
|
|
134
|
+
.method-get { color: #28a745; }
|
|
135
|
+
.method-post { color: #007bff; }
|
|
136
|
+
.method-put { color: #ffc107; }
|
|
137
|
+
.method-delete { color: #dc3545; }
|
|
138
|
+
.method-patch { color: #6f42c1; }
|
|
139
|
+
.count {
|
|
140
|
+
color: #6c757d;
|
|
141
|
+
font-size: 0.9em;
|
|
142
|
+
}
|
|
143
|
+
</style>
|
|
144
|
+
</head>
|
|
145
|
+
<body>
|
|
146
|
+
<div class="container">
|
|
147
|
+
<h1>API Endpoints</h1>
|
|
148
|
+
<p class="count">Total endpoints: <strong>${endpoints.length}</strong></p>
|
|
149
|
+
|
|
150
|
+
<table>
|
|
151
|
+
<thead>
|
|
152
|
+
<tr>
|
|
153
|
+
<th>Method</th>
|
|
154
|
+
<th>Path</th>
|
|
155
|
+
<th>Description</th>
|
|
156
|
+
</tr>
|
|
157
|
+
</thead>
|
|
158
|
+
<tbody>
|
|
159
|
+
${endpointRows}
|
|
160
|
+
</tbody>
|
|
161
|
+
</table>
|
|
162
|
+
</div>
|
|
163
|
+
</body>
|
|
164
|
+
</html>
|
|
165
|
+
`.trim();
|
|
166
|
+
}
|