@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,63 @@
|
|
|
1
|
+
import express from 'express';
|
|
2
|
+
|
|
3
|
+
export interface CorsConfig {
|
|
4
|
+
/** Base URL for web application (e.g., 'example.com' or 'https://example.com') */
|
|
5
|
+
webBaseUrl?: string;
|
|
6
|
+
/** Allow any localhost origin (http://localhost:*) for local development */
|
|
7
|
+
allowLocalhost?: boolean;
|
|
8
|
+
/**
|
|
9
|
+
* @deprecated Use `allowLocalhost` instead. This field controls both test endpoints and localhost CORS.
|
|
10
|
+
* When `allowLocalhost` is set, it takes precedence for CORS behavior.
|
|
11
|
+
*/
|
|
12
|
+
enableTestEndpoints?: boolean;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function setACAOHeader(res: express.Response, reqOrigin: string, config: CorsConfig) {
|
|
16
|
+
const webBaseUrl = config.webBaseUrl;
|
|
17
|
+
|
|
18
|
+
if (!reqOrigin) {
|
|
19
|
+
// No origin header - probably a same-origin request.
|
|
20
|
+
if (webBaseUrl) {
|
|
21
|
+
res.header('Access-Control-Allow-Origin', webBaseUrl);
|
|
22
|
+
}
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Origin header is present - check for whitelisted cases
|
|
27
|
+
if (webBaseUrl) {
|
|
28
|
+
const allowedOrigins = [`https://${webBaseUrl}`];
|
|
29
|
+
if (allowedOrigins.includes(reqOrigin)) {
|
|
30
|
+
// Matches whitelist
|
|
31
|
+
res.header('Access-Control-Allow-Origin', reqOrigin);
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Allow any localhost port for local development
|
|
37
|
+
const localhostAllowed = config.allowLocalhost ?? config.enableTestEndpoints;
|
|
38
|
+
if (localhostAllowed && reqOrigin.startsWith('http://localhost:')) {
|
|
39
|
+
res.header('Access-Control-Allow-Origin', reqOrigin);
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function corsMiddleware(config: CorsConfig = {}) {
|
|
45
|
+
return (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
|
46
|
+
setACAOHeader(res, req.headers.origin, config);
|
|
47
|
+
res.header('Access-Control-Allow-Credentials', 'true');
|
|
48
|
+
res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS, PATCH');
|
|
49
|
+
res.header(
|
|
50
|
+
'Access-Control-Allow-Headers',
|
|
51
|
+
'Origin, X-Requested-With, Content-Type, Accept, Authorization, Cookie, Cache-Control'
|
|
52
|
+
);
|
|
53
|
+
res.header('Access-Control-Max-Age', '86400');
|
|
54
|
+
|
|
55
|
+
// Handle preflight requests
|
|
56
|
+
if (req.method === 'OPTIONS') {
|
|
57
|
+
res.sendStatus(200);
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
next();
|
|
62
|
+
};
|
|
63
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { NextFunction, Request, Response } from 'express';
|
|
2
|
+
|
|
3
|
+
/*
|
|
4
|
+
localhostOnlyMiddleware
|
|
5
|
+
|
|
6
|
+
Special middleware that restricts an endpoint so that it can only be used by localhost client.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export function localhostOnlyMiddleware(req: Request, res: Response, next: NextFunction) {
|
|
10
|
+
const allowedIPs = ['127.0.0.1', '::1', '::ffff:127.0.0.1']; // localhost variations
|
|
11
|
+
|
|
12
|
+
const clientIP = req.ip || req.connection?.remoteAddress;
|
|
13
|
+
|
|
14
|
+
if (!allowedIPs.includes(clientIP)) {
|
|
15
|
+
return res.status(404).json({ error: 'Not found' });
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
next();
|
|
19
|
+
}
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* OpenAPI
|
|
3
|
+
*
|
|
4
|
+
* Generates OpenAPI schema from service definitions.
|
|
5
|
+
* This module transforms endpoint definitions into a complete OpenAPI 3.1.0 specification.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import {
|
|
9
|
+
OpenAPIRegistry,
|
|
10
|
+
OpenApiGeneratorV31,
|
|
11
|
+
type RouteConfig,
|
|
12
|
+
extendZodWithOpenApi,
|
|
13
|
+
} from '@asteasolutions/zod-to-openapi';
|
|
14
|
+
import swaggerUi from 'swagger-ui-express';
|
|
15
|
+
import type { OpenAPIObject } from 'openapi3-ts/oas31';
|
|
16
|
+
import z from 'zod';
|
|
17
|
+
import type { ServiceDefinition } from '../../ServiceDefinition.ts';
|
|
18
|
+
import { PrismApp } from '../../app/PrismApp.ts';
|
|
19
|
+
import express, { type Request, type Response } from 'express';
|
|
20
|
+
import { captureError } from '@facetlayer/streams'
|
|
21
|
+
import { validateServicesForOpenapi } from './validateServicesForOpenapi.ts';
|
|
22
|
+
import { getEffectiveOperationId } from '../../endpoints/createEndpoint.ts';
|
|
23
|
+
|
|
24
|
+
export { validateServicesForOpenapi as validateEndpointForOpenapi } from './validateServicesForOpenapi.ts';
|
|
25
|
+
|
|
26
|
+
type RequestConfig = RouteConfig['request'];
|
|
27
|
+
|
|
28
|
+
export interface OpenAPIConfig {
|
|
29
|
+
enable: boolean
|
|
30
|
+
enableSwagger?: boolean
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export type ParseExpressPathForOpenAPIResult = {
|
|
34
|
+
openApiPath: string;
|
|
35
|
+
pathParams: string[];
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
export interface OpenAPIDocumentInfo {
|
|
39
|
+
version: string;
|
|
40
|
+
title: string;
|
|
41
|
+
description: string;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Globally modify Zod to support the .openapi() helper method.
|
|
45
|
+
extendZodWithOpenApi(z);
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Transforms path parameters in an Express-style URL path (e.g. :pathParameter) into their OpenAPI equivalents (e.g. {pathParameter})
|
|
49
|
+
* Returns the transformed OpenAPI-style URL path along with any path parameters found.
|
|
50
|
+
*
|
|
51
|
+
* @param {string} expressApiPath - An Express-style URL path
|
|
52
|
+
* @returns {ParseExpressPathForOpenAPIResult} - the transformed OpenAPI-style URL path with any path parameters found
|
|
53
|
+
*/
|
|
54
|
+
export function parseExpressPathForOpenAPI(expressApiPath: string): ParseExpressPathForOpenAPIResult {
|
|
55
|
+
const expressApiPathParts: string[] = expressApiPath.split('/');
|
|
56
|
+
const pathParams: string[] = [];
|
|
57
|
+
const openApiPathParts: string[] = [];
|
|
58
|
+
|
|
59
|
+
for (const part of expressApiPathParts) {
|
|
60
|
+
if (part.startsWith(':')) {
|
|
61
|
+
pathParams.push(part.substring(1));
|
|
62
|
+
openApiPathParts.push(`{${part.substring(1)}}`);
|
|
63
|
+
} else {
|
|
64
|
+
openApiPathParts.push(part);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return {
|
|
69
|
+
openApiPath: openApiPathParts.join('/'),
|
|
70
|
+
pathParams,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Generates the Open API Schema for all services.
|
|
76
|
+
*
|
|
77
|
+
* @param {ServiceDefinition[]} services - Array of service definitions to generate OpenAPI schema for
|
|
78
|
+
* @param {OpenAPIDocumentInfo} documentInfo - Metadata for the OpenAPI document
|
|
79
|
+
* @returns {OpenAPIObject} - the Open API schema for all service definitions.
|
|
80
|
+
*/
|
|
81
|
+
export function generateOpenAPISchema(
|
|
82
|
+
services: ServiceDefinition[],
|
|
83
|
+
documentInfo: OpenAPIDocumentInfo
|
|
84
|
+
): OpenAPIObject {
|
|
85
|
+
const registry: OpenAPIRegistry = new OpenAPIRegistry();
|
|
86
|
+
|
|
87
|
+
// Register all endpoints from all services
|
|
88
|
+
for (const service of services) {
|
|
89
|
+
const endpoints = service.endpoints || [];
|
|
90
|
+
|
|
91
|
+
for (const endpoint of endpoints) {
|
|
92
|
+
const { openApiPath, pathParams }: ParseExpressPathForOpenAPIResult =
|
|
93
|
+
parseExpressPathForOpenAPI(endpoint.path);
|
|
94
|
+
const requestConfig: RequestConfig = {};
|
|
95
|
+
|
|
96
|
+
// Specify path parameters: Extract path parameters from the route path and create a Zod schema from it.
|
|
97
|
+
if (pathParams.length > 0) {
|
|
98
|
+
// TODO: specify stricter typing for path parameters (e.g. enums/numeric values)
|
|
99
|
+
const pathParamsSchema: Record<string, z.ZodString> = {};
|
|
100
|
+
for (const param of pathParams) {
|
|
101
|
+
pathParamsSchema[param] = z.string();
|
|
102
|
+
}
|
|
103
|
+
requestConfig.params = z.object(pathParamsSchema);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Specify body parameters: If the endpoint provides a request schema, use that as the body parameter
|
|
107
|
+
if (endpoint.requestSchema) {
|
|
108
|
+
requestConfig.body = {
|
|
109
|
+
content: {
|
|
110
|
+
'application/json': {
|
|
111
|
+
schema: endpoint.requestSchema,
|
|
112
|
+
},
|
|
113
|
+
},
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
registry.registerPath({
|
|
118
|
+
method: endpoint.method.toLowerCase() as any,
|
|
119
|
+
path: openApiPath,
|
|
120
|
+
description: endpoint.description || `${endpoint.method} ${endpoint.path}`,
|
|
121
|
+
operationId: getEffectiveOperationId(endpoint),
|
|
122
|
+
request: requestConfig,
|
|
123
|
+
responses: {
|
|
124
|
+
200: {
|
|
125
|
+
description: 'Success',
|
|
126
|
+
content: {
|
|
127
|
+
'application/json': {
|
|
128
|
+
schema: endpoint.responseSchema ?? z.any(),
|
|
129
|
+
},
|
|
130
|
+
},
|
|
131
|
+
},
|
|
132
|
+
/*
|
|
133
|
+
400: {
|
|
134
|
+
description: 'Bad Request - Schema validation failed',
|
|
135
|
+
content: {
|
|
136
|
+
'application/json': {
|
|
137
|
+
schema: z.object({
|
|
138
|
+
error: z.string(),
|
|
139
|
+
details: z.array(z.any()),
|
|
140
|
+
}),
|
|
141
|
+
},
|
|
142
|
+
},
|
|
143
|
+
},
|
|
144
|
+
401: {
|
|
145
|
+
description: 'Unauthorized',
|
|
146
|
+
content: {
|
|
147
|
+
'application/json': {
|
|
148
|
+
schema: z.object({
|
|
149
|
+
message: z.string(),
|
|
150
|
+
details: z.any().optional(),
|
|
151
|
+
}),
|
|
152
|
+
},
|
|
153
|
+
},
|
|
154
|
+
},
|
|
155
|
+
500: {
|
|
156
|
+
description: 'Internal Server Error',
|
|
157
|
+
content: {
|
|
158
|
+
'application/json': {
|
|
159
|
+
schema: z.object({
|
|
160
|
+
message: z.string(),
|
|
161
|
+
}),
|
|
162
|
+
},
|
|
163
|
+
},
|
|
164
|
+
},
|
|
165
|
+
*/
|
|
166
|
+
},
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const generator: OpenApiGeneratorV31 = new OpenApiGeneratorV31(
|
|
172
|
+
registry.definitions
|
|
173
|
+
|
|
174
|
+
/*
|
|
175
|
+
.concat([
|
|
176
|
+
{
|
|
177
|
+
type: 'component',
|
|
178
|
+
componentType: 'securitySchemes',
|
|
179
|
+
name: 'bearer_auth',
|
|
180
|
+
component: {
|
|
181
|
+
type: 'http',
|
|
182
|
+
scheme: 'bearer',
|
|
183
|
+
bearerFormat: 'JWT',
|
|
184
|
+
},
|
|
185
|
+
},
|
|
186
|
+
])
|
|
187
|
+
*/
|
|
188
|
+
);
|
|
189
|
+
|
|
190
|
+
return generator.generateDocument({
|
|
191
|
+
openapi: '3.1.0',
|
|
192
|
+
info: {
|
|
193
|
+
version: documentInfo.version,
|
|
194
|
+
title: documentInfo.title,
|
|
195
|
+
description: documentInfo.description,
|
|
196
|
+
},
|
|
197
|
+
servers: [
|
|
198
|
+
{ url: '/api', description: 'API server' },
|
|
199
|
+
],
|
|
200
|
+
security: [
|
|
201
|
+
{
|
|
202
|
+
bearer_auth: [],
|
|
203
|
+
},
|
|
204
|
+
],
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
export function setupSwaggerUI(app: express.Application | express.Router, openApiJsonPath: string = '/openapi.json'): void {
|
|
209
|
+
const router = app as express.Router;
|
|
210
|
+
// Serve Swagger UI on /swagger
|
|
211
|
+
router.use(
|
|
212
|
+
'/swagger',
|
|
213
|
+
swaggerUi.serve,
|
|
214
|
+
swaggerUi.setup(null, {
|
|
215
|
+
swaggerOptions: {
|
|
216
|
+
url: openApiJsonPath,
|
|
217
|
+
},
|
|
218
|
+
})
|
|
219
|
+
);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
export function mountOpenAPIEndpoints(config: OpenAPIConfig, target: express.Application | express.Router, prismApp: PrismApp): void {
|
|
223
|
+
const router = target as express.Router;
|
|
224
|
+
router.get('/openapi.json', (req: Request, res: Response) => {
|
|
225
|
+
const services = prismApp.getAllServices();
|
|
226
|
+
try {
|
|
227
|
+
res.json(generateOpenAPISchema(services, {
|
|
228
|
+
version: '1.0.0',
|
|
229
|
+
title: prismApp.name,
|
|
230
|
+
description: prismApp.description,
|
|
231
|
+
}));
|
|
232
|
+
} catch (error) {
|
|
233
|
+
|
|
234
|
+
const validationResult = validateServicesForOpenapi(services);
|
|
235
|
+
|
|
236
|
+
console.error("/openapi.json failed to generate schema", {
|
|
237
|
+
cause: captureError(error),
|
|
238
|
+
problemEndpoints: validationResult.problemEndpoints,
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
res.status(500).json({ error: "Internal server error" });
|
|
242
|
+
}
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
if (config.enableSwagger) {
|
|
246
|
+
setupSwaggerUI(router, '/api/openapi.json');
|
|
247
|
+
}
|
|
248
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import type { ServiceDefinition } from "../../ServiceDefinition.ts";
|
|
2
|
+
import type { EndpointDefinition } from "../ExpressEndpointSetup.ts";
|
|
3
|
+
import { generateOpenAPISchema } from "./OpenAPI.ts";
|
|
4
|
+
import { captureError, type ErrorDetails } from "@facetlayer/streams";
|
|
5
|
+
|
|
6
|
+
export interface EndpointValidationResult {
|
|
7
|
+
error: ErrorDetails
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface FailedEndpoint {
|
|
11
|
+
serviceName: string;
|
|
12
|
+
path: string;
|
|
13
|
+
method: string;
|
|
14
|
+
error: ErrorDetails;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface ServicesValidationResult {
|
|
18
|
+
problemEndpoints: FailedEndpoint[];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Validates a single endpoint for OpenAPI schema generation compatibility.
|
|
24
|
+
*
|
|
25
|
+
* @param serviceName - Name of the service containing the endpoint
|
|
26
|
+
* @param endpoint - The endpoint definition to validate
|
|
27
|
+
* @returns ProblematicEndpoint if validation fails, null if endpoint is valid
|
|
28
|
+
*/
|
|
29
|
+
export function validateEndpointForOpenapi(
|
|
30
|
+
endpoint: EndpointDefinition
|
|
31
|
+
): EndpointValidationResult {
|
|
32
|
+
const testService: ServiceDefinition = {
|
|
33
|
+
name: 'test',
|
|
34
|
+
endpoints: [endpoint],
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
try {
|
|
38
|
+
generateOpenAPISchema([testService], {
|
|
39
|
+
version: '1.0.0',
|
|
40
|
+
title: 'Test',
|
|
41
|
+
description: 'Test',
|
|
42
|
+
});
|
|
43
|
+
return null;
|
|
44
|
+
} catch (error) {
|
|
45
|
+
return { error: captureError(error) };
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Finds endpoints that will fail OpenAPI schema generation.
|
|
51
|
+
* Tests each endpoint individually to identify which ones have unsupported Zod types.
|
|
52
|
+
*
|
|
53
|
+
* @param services - Array of service definitions to check
|
|
54
|
+
* @returns ValidationResult containing any problematic endpoints
|
|
55
|
+
*/
|
|
56
|
+
export function validateServicesForOpenapi(services: ServiceDefinition[]): ServicesValidationResult {
|
|
57
|
+
const problemEndpoints: FailedEndpoint[] = [];
|
|
58
|
+
|
|
59
|
+
for (const service of services) {
|
|
60
|
+
const endpoints = service.endpoints || [];
|
|
61
|
+
|
|
62
|
+
for (const endpoint of endpoints) {
|
|
63
|
+
const endpointResult = validateEndpointForOpenapi(endpoint);
|
|
64
|
+
if (endpointResult?.error) {
|
|
65
|
+
problemEndpoints.push({
|
|
66
|
+
serviceName: service.name,
|
|
67
|
+
path: endpoint.path,
|
|
68
|
+
method: endpoint.method,
|
|
69
|
+
error: endpointResult.error,
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return { problemEndpoints };
|
|
76
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { AsyncLocalStorage } from 'async_hooks';
|
|
2
|
+
import type { NextFunction, Request, Response } from 'express';
|
|
3
|
+
import { v4 as uuidv4 } from 'uuid';
|
|
4
|
+
import { Authorization } from '../authorization/Authorization.ts';
|
|
5
|
+
import type { RequestContext } from '../RequestContext.ts';
|
|
6
|
+
import { requestContextStorage } from '../RequestContext.ts';
|
|
7
|
+
|
|
8
|
+
export function requestContextMiddleware(req: Request, res: Response, next: NextFunction): void {
|
|
9
|
+
const requestId = uuidv4();
|
|
10
|
+
const startTime = Date.now();
|
|
11
|
+
|
|
12
|
+
const context: RequestContext = {
|
|
13
|
+
requestId,
|
|
14
|
+
startTime,
|
|
15
|
+
req,
|
|
16
|
+
res,
|
|
17
|
+
auth: new Authorization(),
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
res.setHeader('X-Request-ID', requestId);
|
|
21
|
+
|
|
22
|
+
requestContextStorage.run(context, () => {
|
|
23
|
+
next();
|
|
24
|
+
});
|
|
25
|
+
}
|
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"permissions": {
|
|
3
|
-
"allow": [
|
|
4
|
-
"Bash(git mv:*)",
|
|
5
|
-
"Bash(prism-endpoint:*)",
|
|
6
|
-
"Bash(pnpm build:*)",
|
|
7
|
-
"Bash(pnpm install:*)",
|
|
8
|
-
"Bash(pnpm typecheck:*)",
|
|
9
|
-
"Skill(vibe-code-cleanup)",
|
|
10
|
-
"Bash(node build.mts:*)",
|
|
11
|
-
"Bash(pnpm test:*)",
|
|
12
|
-
"Bash(node dist/cli.js:*)",
|
|
13
|
-
"Bash(mkdir:*)",
|
|
14
|
-
"Bash(node /Users/andy/node-libraries/prism-framework-tools/dist/cli.js generate-api-clients:*)",
|
|
15
|
-
"Bash(pnpm local:install:*)"
|
|
16
|
-
],
|
|
17
|
-
"deny": [],
|
|
18
|
-
"ask": []
|
|
19
|
-
}
|
|
20
|
-
}
|
package/CHANGELOG
DELETED
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
|
|
2
|
-
0.4.0
|
|
3
|
-
- Renamed from @facetlayer/prism-framework-tools to @facetlayer/prism-framework
|
|
4
|
-
|
|
5
|
-
0.3.0
|
|
6
|
-
- Add config file support (.prism.qc) with parent directory search
|
|
7
|
-
- Fix API client type generation issues
|
|
8
|
-
- Add generate-api-clients-config documentation
|
|
9
|
-
- Add endpoint-tools and env-files documentation
|
|
10
|
-
- Safety checks for Zod schemas incompatible with OpenAPI
|
|
11
|
-
- Improve getting-started docs
|
|
12
|
-
|
|
13
|
-
0.2.5
|
|
14
|
-
- Add list-docs and get-doc (with doc-files-helper)
|
|
15
|
-
|
|
16
|
-
0.2.4
|
|
17
|
-
- Handle JSONish params in 'call' command
|
|
18
|
-
- generate-api-clients now uses --out param
|
|
19
|
-
|
|
20
|
-
0.2.3
|
|
21
|
-
- Fix for list-endpoints
|
|
22
|
-
|
|
23
|
-
0.2.1
|
|
24
|
-
- Add list-endpoints command
|
|
25
|
-
- Call endpoint - don't fail on response schema failure
|
|
26
|
-
|
|
27
|
-
0.1.0
|
|
28
|
-
- initial version
|
package/CLAUDE.md
DELETED
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
# prism-framework
|
|
2
|
-
|
|
3
|
-
Base library and CLI tools for the Prism app framework ecosystem.
|
|
4
|
-
|
|
5
|
-
## Important Files and Directories
|
|
6
|
-
|
|
7
|
-
### Source Code (`src/`)
|
|
8
|
-
- `cli.ts` - Main CLI entry point, uses yargs for argument parsing
|
|
9
|
-
- `call-command.ts` - Logic for calling endpoints, handles JSON parsing of arguments
|
|
10
|
-
- `generate-api-clients.ts` - Generates TypeScript types from OpenAPI schema
|
|
11
|
-
- `list-endpoints-command.ts` - Lists available endpoints from the API server
|
|
12
|
-
- `loadEnv.ts` - Environment variable loading and validation
|
|
13
|
-
- `getPorts.ts` - Port number utilities
|
|
14
|
-
|
|
15
|
-
### Documentation (`docs/`)
|
|
16
|
-
|
|
17
|
-
Markdown files for documentation.
|
|
18
|
-
|
|
19
|
-
Run `doc-files list-docs` to understand the format.
|
|
20
|
-
|
|
21
|
-
- `getting-started.md` - Setup guide for Prism Framework projects
|
|
22
|
-
- `run-endpoint-tool.md` - Detailed CLI usage documentation
|
|
23
|
-
- `env-files.md` - Environment configuration strategy
|
|
24
|
-
|
|
25
|
-
### Tests (`test/`)
|
|
26
|
-
- `call-command.test.ts` - Unit tests for argument parsing logic
|
|
27
|
-
|
|
28
|
-
### Build Output
|
|
29
|
-
- `dist/cli.js` - Compiled CLI executable (ES modules)
|
|
30
|
-
|
|
31
|
-
## Build Commands
|
|
32
|
-
|
|
33
|
-
```bash
|
|
34
|
-
pnpm build # Build the project
|
|
35
|
-
pnpm test # Run tests with Vitest
|
|
36
|
-
pnpm typecheck # TypeScript type checking
|
|
37
|
-
```
|
|
38
|
-
|
|
39
|
-
## Key Dependencies
|
|
40
|
-
|
|
41
|
-
- `yargs` - CLI argument parsing
|
|
42
|
-
- `dotenv` - Environment variable loading
|
|
43
|
-
- `@facetlayer/doc-files-helper` - Documentation file management
|
|
44
|
-
- `@facetlayer/prism-framework-api` - Prism API framework types
|
package/build.mts
DELETED
|
@@ -1,96 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from 'vitest';
|
|
2
|
-
import { parseNamedArgs } from '../src/call-command';
|
|
3
|
-
|
|
4
|
-
describe('parseNamedArgs', () => {
|
|
5
|
-
describe('basic values', () => {
|
|
6
|
-
it('should pass through simple string values', () => {
|
|
7
|
-
const result = parseNamedArgs({ name: 'John', email: 'john@example.com' });
|
|
8
|
-
expect(result).toEqual({ name: 'John', email: 'john@example.com' });
|
|
9
|
-
});
|
|
10
|
-
|
|
11
|
-
it('should pass through non-string values', () => {
|
|
12
|
-
const result = parseNamedArgs({ count: 42, active: true });
|
|
13
|
-
expect(result).toEqual({ count: 42, active: true });
|
|
14
|
-
});
|
|
15
|
-
});
|
|
16
|
-
|
|
17
|
-
describe('JSON parsing', () => {
|
|
18
|
-
it('should parse JSON object strings', () => {
|
|
19
|
-
const result = parseNamedArgs({ config: '{"timeout": 30}' });
|
|
20
|
-
expect(result).toEqual({ config: { timeout: 30 } });
|
|
21
|
-
});
|
|
22
|
-
|
|
23
|
-
it('should parse JSON array strings', () => {
|
|
24
|
-
const result = parseNamedArgs({ items: '["a", "b", "c"]' });
|
|
25
|
-
expect(result).toEqual({ items: ['a', 'b', 'c'] });
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
it('should handle whitespace around JSON', () => {
|
|
29
|
-
const result = parseNamedArgs({ data: ' {"key": "value"} ' });
|
|
30
|
-
expect(result).toEqual({ data: { key: 'value' } });
|
|
31
|
-
});
|
|
32
|
-
|
|
33
|
-
it('should keep invalid JSON as string', () => {
|
|
34
|
-
const result = parseNamedArgs({ bad: '{not valid json}' });
|
|
35
|
-
expect(result).toEqual({ bad: '{not valid json}' });
|
|
36
|
-
});
|
|
37
|
-
|
|
38
|
-
it('should not parse strings that only start with { or [', () => {
|
|
39
|
-
const result = parseNamedArgs({ text: '{hello world' });
|
|
40
|
-
expect(result).toEqual({ text: '{hello world' });
|
|
41
|
-
});
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
describe('nested objects from yargs', () => {
|
|
45
|
-
it('should parse JSON strings inside nested objects', () => {
|
|
46
|
-
// Yargs creates nested objects from dot notation before we see them
|
|
47
|
-
const result = parseNamedArgs({
|
|
48
|
-
schema: {
|
|
49
|
-
name: 'test-schema',
|
|
50
|
-
statements: '["CREATE TABLE test (id INT)"]'
|
|
51
|
-
}
|
|
52
|
-
});
|
|
53
|
-
expect(result).toEqual({
|
|
54
|
-
schema: {
|
|
55
|
-
name: 'test-schema',
|
|
56
|
-
statements: ['CREATE TABLE test (id INT)']
|
|
57
|
-
}
|
|
58
|
-
});
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
it('should handle deeply nested objects with JSON strings', () => {
|
|
62
|
-
const result = parseNamedArgs({
|
|
63
|
-
config: {
|
|
64
|
-
database: {
|
|
65
|
-
options: '{"timeout": 30, "retries": 3}'
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
});
|
|
69
|
-
expect(result).toEqual({
|
|
70
|
-
config: {
|
|
71
|
-
database: {
|
|
72
|
-
options: { timeout: 30, retries: 3 }
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
});
|
|
76
|
-
});
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
describe('real-world example from test.sh', () => {
|
|
80
|
-
it('should handle the migration command args', () => {
|
|
81
|
-
// Yargs parses --schema.name and --schema.statements into nested object
|
|
82
|
-
const result = parseNamedArgs({
|
|
83
|
-
schema: {
|
|
84
|
-
name: 'test-schema-v2',
|
|
85
|
-
statements: '["CREATE TABLE test_products (id INTEGER PRIMARY KEY, name TEXT, price REAL)"]'
|
|
86
|
-
}
|
|
87
|
-
});
|
|
88
|
-
expect(result).toEqual({
|
|
89
|
-
schema: {
|
|
90
|
-
name: 'test-schema-v2',
|
|
91
|
-
statements: ['CREATE TABLE test_products (id INTEGER PRIMARY KEY, name TEXT, price REAL)']
|
|
92
|
-
}
|
|
93
|
-
});
|
|
94
|
-
});
|
|
95
|
-
});
|
|
96
|
-
});
|
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from 'vitest';
|
|
2
|
-
import { convertToExpressPath } from '../src/generate-api-clients';
|
|
3
|
-
|
|
4
|
-
describe('convertToExpressPath', () => {
|
|
5
|
-
it('should convert single path parameter', () => {
|
|
6
|
-
expect(convertToExpressPath('/users/{id}')).toBe('/users/:id');
|
|
7
|
-
});
|
|
8
|
-
|
|
9
|
-
it('should convert multiple path parameters', () => {
|
|
10
|
-
expect(convertToExpressPath('/users/{userId}/posts/{postId}')).toBe('/users/:userId/posts/:postId');
|
|
11
|
-
});
|
|
12
|
-
|
|
13
|
-
it('should handle paths without parameters', () => {
|
|
14
|
-
expect(convertToExpressPath('/users')).toBe('/users');
|
|
15
|
-
expect(convertToExpressPath('/api/health')).toBe('/api/health');
|
|
16
|
-
});
|
|
17
|
-
|
|
18
|
-
it('should handle root path', () => {
|
|
19
|
-
expect(convertToExpressPath('/')).toBe('/');
|
|
20
|
-
});
|
|
21
|
-
|
|
22
|
-
it('should handle complex parameter names', () => {
|
|
23
|
-
expect(convertToExpressPath('/designs/{designId}/nodes/{nodeId}')).toBe('/designs/:designId/nodes/:nodeId');
|
|
24
|
-
});
|
|
25
|
-
|
|
26
|
-
it('should handle parameter at the end of path', () => {
|
|
27
|
-
expect(convertToExpressPath('/api/items/{id}')).toBe('/api/items/:id');
|
|
28
|
-
});
|
|
29
|
-
|
|
30
|
-
it('should handle parameter with underscores', () => {
|
|
31
|
-
expect(convertToExpressPath('/api/{user_id}/profile')).toBe('/api/:user_id/profile');
|
|
32
|
-
});
|
|
33
|
-
});
|