@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,206 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { PrismApp } from '../app/PrismApp.ts';
|
|
3
|
+
import { validateApp, validateAppOrThrow } from '../app/validateApp.ts';
|
|
4
|
+
|
|
5
|
+
describe('validateApp', () => {
|
|
6
|
+
describe('duplicate operationId detection', () => {
|
|
7
|
+
it('should pass when all operationIds are unique', () => {
|
|
8
|
+
const app = new PrismApp({
|
|
9
|
+
services: [
|
|
10
|
+
{
|
|
11
|
+
name: 'test-service',
|
|
12
|
+
endpoints: [
|
|
13
|
+
{
|
|
14
|
+
method: 'GET',
|
|
15
|
+
path: '/users',
|
|
16
|
+
operationId: 'getUsers',
|
|
17
|
+
handler: async () => [],
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
method: 'POST',
|
|
21
|
+
path: '/users',
|
|
22
|
+
operationId: 'createUser',
|
|
23
|
+
handler: async () => ({}),
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
method: 'GET',
|
|
27
|
+
path: '/users/:id',
|
|
28
|
+
operationId: 'getUserById',
|
|
29
|
+
handler: async () => ({}),
|
|
30
|
+
},
|
|
31
|
+
],
|
|
32
|
+
},
|
|
33
|
+
],
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
const result = validateApp(app);
|
|
37
|
+
|
|
38
|
+
expect(result.valid).toBe(true);
|
|
39
|
+
expect(result.errors).toHaveLength(0);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('should fail when endpoints have duplicate explicit operationIds', () => {
|
|
43
|
+
const app = new PrismApp({
|
|
44
|
+
services: [
|
|
45
|
+
{
|
|
46
|
+
name: 'test-service',
|
|
47
|
+
endpoints: [
|
|
48
|
+
{
|
|
49
|
+
method: 'GET',
|
|
50
|
+
path: '/users',
|
|
51
|
+
operationId: 'getUsers',
|
|
52
|
+
handler: async () => [],
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
method: 'GET',
|
|
56
|
+
path: '/customers',
|
|
57
|
+
operationId: 'getUsers', // Duplicate!
|
|
58
|
+
handler: async () => [],
|
|
59
|
+
},
|
|
60
|
+
],
|
|
61
|
+
},
|
|
62
|
+
],
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
const result = validateApp(app);
|
|
66
|
+
|
|
67
|
+
expect(result.valid).toBe(false);
|
|
68
|
+
expect(result.errors).toHaveLength(1);
|
|
69
|
+
expect(result.errors[0].message).toContain('Duplicate operationIds');
|
|
70
|
+
expect(result.errors[0].message).toContain('getUsers');
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it('should detect duplicates across multiple services', () => {
|
|
74
|
+
const app = new PrismApp({
|
|
75
|
+
services: [
|
|
76
|
+
{
|
|
77
|
+
name: 'service-a',
|
|
78
|
+
endpoints: [
|
|
79
|
+
{
|
|
80
|
+
method: 'GET',
|
|
81
|
+
path: '/items',
|
|
82
|
+
operationId: 'listItems',
|
|
83
|
+
handler: async () => [],
|
|
84
|
+
},
|
|
85
|
+
],
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
name: 'service-b',
|
|
89
|
+
endpoints: [
|
|
90
|
+
{
|
|
91
|
+
method: 'GET',
|
|
92
|
+
path: '/products',
|
|
93
|
+
operationId: 'listItems', // Duplicate from service-a
|
|
94
|
+
handler: async () => [],
|
|
95
|
+
},
|
|
96
|
+
],
|
|
97
|
+
},
|
|
98
|
+
],
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
const result = validateApp(app);
|
|
102
|
+
|
|
103
|
+
expect(result.valid).toBe(false);
|
|
104
|
+
expect(result.errors[0].message).toContain('listItems');
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it('should pass when auto-generated operationIds are unique', () => {
|
|
108
|
+
const app = new PrismApp({
|
|
109
|
+
services: [
|
|
110
|
+
{
|
|
111
|
+
name: 'test-service',
|
|
112
|
+
endpoints: [
|
|
113
|
+
{
|
|
114
|
+
method: 'GET',
|
|
115
|
+
path: '/users',
|
|
116
|
+
handler: async () => [], // Will auto-generate "getUsers"
|
|
117
|
+
},
|
|
118
|
+
{
|
|
119
|
+
method: 'POST',
|
|
120
|
+
path: '/users',
|
|
121
|
+
handler: async () => ({}), // Will auto-generate "postUsers"
|
|
122
|
+
},
|
|
123
|
+
],
|
|
124
|
+
},
|
|
125
|
+
],
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
const result = validateApp(app);
|
|
129
|
+
|
|
130
|
+
expect(result.valid).toBe(true);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it('should pass for empty app', () => {
|
|
134
|
+
const app = new PrismApp();
|
|
135
|
+
|
|
136
|
+
const result = validateApp(app);
|
|
137
|
+
|
|
138
|
+
expect(result.valid).toBe(true);
|
|
139
|
+
expect(result.errors).toHaveLength(0);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it('should pass for app with services but no endpoints', () => {
|
|
143
|
+
const app = new PrismApp({
|
|
144
|
+
services: [
|
|
145
|
+
{
|
|
146
|
+
name: 'empty-service',
|
|
147
|
+
endpoints: [],
|
|
148
|
+
},
|
|
149
|
+
],
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
const result = validateApp(app);
|
|
153
|
+
|
|
154
|
+
expect(result.valid).toBe(true);
|
|
155
|
+
});
|
|
156
|
+
});
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
describe('validateAppOrThrow', () => {
|
|
160
|
+
it('should not throw for valid app', () => {
|
|
161
|
+
const app = new PrismApp({
|
|
162
|
+
services: [
|
|
163
|
+
{
|
|
164
|
+
name: 'test-service',
|
|
165
|
+
endpoints: [
|
|
166
|
+
{
|
|
167
|
+
method: 'GET',
|
|
168
|
+
path: '/users',
|
|
169
|
+
operationId: 'getUsers',
|
|
170
|
+
handler: async () => [],
|
|
171
|
+
},
|
|
172
|
+
],
|
|
173
|
+
},
|
|
174
|
+
],
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
expect(() => validateAppOrThrow(app)).not.toThrow();
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
it('should throw for app with duplicate operationIds', () => {
|
|
181
|
+
const app = new PrismApp({
|
|
182
|
+
services: [
|
|
183
|
+
{
|
|
184
|
+
name: 'test-service',
|
|
185
|
+
endpoints: [
|
|
186
|
+
{
|
|
187
|
+
method: 'GET',
|
|
188
|
+
path: '/users',
|
|
189
|
+
operationId: 'duplicateId',
|
|
190
|
+
handler: async () => [],
|
|
191
|
+
},
|
|
192
|
+
{
|
|
193
|
+
method: 'GET',
|
|
194
|
+
path: '/items',
|
|
195
|
+
operationId: 'duplicateId',
|
|
196
|
+
handler: async () => [],
|
|
197
|
+
},
|
|
198
|
+
],
|
|
199
|
+
},
|
|
200
|
+
],
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
expect(() => validateAppOrThrow(app)).toThrow('PrismApp validation failed');
|
|
204
|
+
expect(() => validateAppOrThrow(app)).toThrow('Duplicate operationIds');
|
|
205
|
+
});
|
|
206
|
+
});
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import type { EndpointDefinition } from '../web/ExpressEndpointSetup.ts';
|
|
2
|
+
import type { ServiceDefinition } from '../ServiceDefinition.ts';
|
|
3
|
+
import { callEndpoint, type CallEndpointOptions } from './callEndpoint.ts';
|
|
4
|
+
|
|
5
|
+
export interface PrismAppConfig {
|
|
6
|
+
name?: string;
|
|
7
|
+
description?: string;
|
|
8
|
+
services?: ServiceDefinition[];
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function endpointKey(method: string, path: string): string {
|
|
12
|
+
return `${method} ${path}`;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export class PrismApp {
|
|
16
|
+
endpointMap: Map<string, EndpointDefinition>;
|
|
17
|
+
services: ServiceDefinition[];
|
|
18
|
+
name: string;
|
|
19
|
+
description: string;
|
|
20
|
+
|
|
21
|
+
constructor(config: PrismAppConfig = {}) {
|
|
22
|
+
this.name = config.name ?? 'Prism App';
|
|
23
|
+
this.description = config.description ?? '';
|
|
24
|
+
this.services = [];
|
|
25
|
+
this.endpointMap = new Map();
|
|
26
|
+
|
|
27
|
+
// Register initial services if provided
|
|
28
|
+
if (config.services) {
|
|
29
|
+
for (const service of config.services) {
|
|
30
|
+
this.addService(service);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Add a service to the app. Endpoints from the service will be registered
|
|
37
|
+
* and available for routing.
|
|
38
|
+
*/
|
|
39
|
+
addService(service: ServiceDefinition): void {
|
|
40
|
+
this.services.push(service);
|
|
41
|
+
|
|
42
|
+
if (service.endpoints) {
|
|
43
|
+
for (const endpoint of service.endpoints) {
|
|
44
|
+
const key = endpointKey(endpoint.method, endpoint.path);
|
|
45
|
+
this.endpointMap.set(key, endpoint);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
getAllServices(): ServiceDefinition[] {
|
|
51
|
+
return this.services;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
getEndpoint(method: string, path: string): EndpointDefinition | undefined {
|
|
55
|
+
const key = endpointKey(method, path);
|
|
56
|
+
return this.endpointMap.get(key);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
matchEndpoint(method: string, path: string): { endpoint: EndpointDefinition; params: Record<string, string> } | undefined {
|
|
60
|
+
// First try exact match
|
|
61
|
+
const key = endpointKey(method, path);
|
|
62
|
+
const exactMatch = this.endpointMap.get(key);
|
|
63
|
+
if (exactMatch) {
|
|
64
|
+
return { endpoint: exactMatch, params: {} };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Try to match path patterns with parameters
|
|
68
|
+
for (const [endpointKey, endpoint] of this.endpointMap.entries()) {
|
|
69
|
+
if (!endpointKey.startsWith(method + ' ')) {
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const endpointPath = endpoint.path;
|
|
74
|
+
const match = this.matchPath(endpointPath, path);
|
|
75
|
+
|
|
76
|
+
if (match) {
|
|
77
|
+
return { endpoint, params: match };
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return undefined;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
private matchPath(pattern: string, path: string): Record<string, string> | null {
|
|
85
|
+
// Convert Express-style path parameters (:param) to regex with named groups
|
|
86
|
+
const paramNames: string[] = [];
|
|
87
|
+
const regexString = pattern
|
|
88
|
+
.replace(/:([^/]+)/g, (_, paramName) => {
|
|
89
|
+
paramNames.push(paramName);
|
|
90
|
+
return '([^/]+)';
|
|
91
|
+
})
|
|
92
|
+
.replace(/\//g, '\\/');
|
|
93
|
+
|
|
94
|
+
const regex = new RegExp(`^${regexString}$`);
|
|
95
|
+
const match = path.match(regex);
|
|
96
|
+
|
|
97
|
+
if (!match) {
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Extract parameter values
|
|
102
|
+
const params: Record<string, string> = {};
|
|
103
|
+
for (let i = 0; i < paramNames.length; i++) {
|
|
104
|
+
params[paramNames[i]] = match[i + 1];
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return params;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
listAllEndpoints(): EndpointDefinition[] {
|
|
111
|
+
return Array.from(this.endpointMap.values());
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
callEndpoint(options: CallEndpointOptions): Promise<any> {
|
|
115
|
+
return callEndpoint(this, options);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { PrismApp } from './PrismApp.ts';
|
|
2
|
+
import type { ServiceDefinition } from '../ServiceDefinition.ts';
|
|
3
|
+
import { isHttpError, ResponseSchemaValidationError, SchemaValidationError } from '../Errors.ts';
|
|
4
|
+
|
|
5
|
+
export interface CallEndpointOptions {
|
|
6
|
+
method: string;
|
|
7
|
+
path: string;
|
|
8
|
+
input?: any;
|
|
9
|
+
onResponseSchemaFail?: (error: any, result: any) => void;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Call an endpoint programmatically without going through HTTP
|
|
14
|
+
* This is useful for testing or for calling endpoints from scripts/tools
|
|
15
|
+
*/
|
|
16
|
+
export async function callEndpoint(app: PrismApp, options: CallEndpointOptions) {
|
|
17
|
+
// Find the endpoint using pattern matching
|
|
18
|
+
const match = app.matchEndpoint(options.method, options.path);
|
|
19
|
+
|
|
20
|
+
if (!match) {
|
|
21
|
+
throw new Error(`Endpoint not found: ${options.method} ${options.path}`);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const { endpoint, params } = match;
|
|
25
|
+
|
|
26
|
+
// Merge path parameters with input data
|
|
27
|
+
let input = { ...params, ...(options.input || {}) };
|
|
28
|
+
|
|
29
|
+
// Validate input if schema is provided
|
|
30
|
+
if (endpoint.requestSchema) {
|
|
31
|
+
const validationResult = endpoint.requestSchema.safeParse(input);
|
|
32
|
+
if (!validationResult.success) {
|
|
33
|
+
throw new SchemaValidationError('Schema validation failed', validationResult.error.issues);
|
|
34
|
+
}
|
|
35
|
+
input = validationResult.data;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Call the handler
|
|
39
|
+
const result = await endpoint.handler(input);
|
|
40
|
+
|
|
41
|
+
// Validate output if schema is provided
|
|
42
|
+
if (endpoint.responseSchema) {
|
|
43
|
+
const validationResult = endpoint.responseSchema.safeParse(result);
|
|
44
|
+
if (!validationResult.success) {
|
|
45
|
+
const error = new ResponseSchemaValidationError('Response schema validation failed', validationResult.error.issues);
|
|
46
|
+
if (options.onResponseSchemaFail) {
|
|
47
|
+
options.onResponseSchemaFail(error, result);
|
|
48
|
+
} else {
|
|
49
|
+
throw error;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return result;
|
|
55
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import type { PrismApp } from './PrismApp.ts';
|
|
2
|
+
import { getEffectiveOperationId } from '../endpoints/createEndpoint.ts';
|
|
3
|
+
|
|
4
|
+
export interface ValidationError {
|
|
5
|
+
message: string;
|
|
6
|
+
endpoints?: string[];
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface ValidationResult {
|
|
10
|
+
valid: boolean;
|
|
11
|
+
errors: ValidationError[];
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Validates a PrismApp configuration, checking for issues like duplicate operationIds.
|
|
16
|
+
* This should be run at server startup to catch configuration errors early.
|
|
17
|
+
*/
|
|
18
|
+
export function validateApp(app: PrismApp): ValidationResult {
|
|
19
|
+
const errors: ValidationError[] = [];
|
|
20
|
+
|
|
21
|
+
// Check for duplicate operationIds
|
|
22
|
+
const duplicateError = checkDuplicateOperationIds(app);
|
|
23
|
+
if (duplicateError) {
|
|
24
|
+
errors.push(duplicateError);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return {
|
|
28
|
+
valid: errors.length === 0,
|
|
29
|
+
errors,
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Checks for duplicate operationIds across all endpoints.
|
|
35
|
+
* Returns an error if duplicates are found, null otherwise.
|
|
36
|
+
*/
|
|
37
|
+
function checkDuplicateOperationIds(app: PrismApp): ValidationError | null {
|
|
38
|
+
const endpoints = app.listAllEndpoints();
|
|
39
|
+
const operationIdToEndpoints = new Map<string, string[]>();
|
|
40
|
+
|
|
41
|
+
for (const endpoint of endpoints) {
|
|
42
|
+
const operationId = getEffectiveOperationId(endpoint);
|
|
43
|
+
const endpointKey = `${endpoint.method} ${endpoint.path}`;
|
|
44
|
+
|
|
45
|
+
const existing = operationIdToEndpoints.get(operationId) || [];
|
|
46
|
+
existing.push(endpointKey);
|
|
47
|
+
operationIdToEndpoints.set(operationId, existing);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Find duplicates
|
|
51
|
+
const duplicates: string[] = [];
|
|
52
|
+
for (const [operationId, endpointKeys] of operationIdToEndpoints) {
|
|
53
|
+
if (endpointKeys.length > 1) {
|
|
54
|
+
duplicates.push(`operationId "${operationId}" is used by: ${endpointKeys.join(', ')}`);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (duplicates.length > 0) {
|
|
59
|
+
return {
|
|
60
|
+
message: `Duplicate operationIds found. Each endpoint must have a unique operationId.\n${duplicates.join('\n')}`,
|
|
61
|
+
endpoints: duplicates,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Validates the app and throws an error if validation fails.
|
|
70
|
+
* Use this at server startup to ensure the app is properly configured.
|
|
71
|
+
*/
|
|
72
|
+
export function validateAppOrThrow(app: PrismApp): void {
|
|
73
|
+
const result = validateApp(app);
|
|
74
|
+
if (!result.valid) {
|
|
75
|
+
const errorMessages = result.errors.map(e => e.message).join('\n\n');
|
|
76
|
+
throw new Error(`PrismApp validation failed:\n${errorMessages}`);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
|
|
2
|
+
/*
|
|
3
|
+
* An 'auth source' is a verified authentication token or session that has been
|
|
4
|
+
* extracted from a request. This represents the source of identity for authorization
|
|
5
|
+
* decisions. Examples: a validated session cookie, a verified API key, etc.
|
|
6
|
+
*/
|
|
7
|
+
export interface AuthSource {
|
|
8
|
+
type: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface CookieAuthSource extends AuthSource {
|
|
12
|
+
type: 'cookie';
|
|
13
|
+
sessionId: string;
|
|
14
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import type { CookieAuthSource, AuthSource } from './AuthSource.ts';
|
|
2
|
+
import type { Resource } from './Resource.ts';
|
|
3
|
+
|
|
4
|
+
// Base permission type - applications can extend this
|
|
5
|
+
export type Permission = string;
|
|
6
|
+
|
|
7
|
+
export interface UserPermissions {
|
|
8
|
+
userId: string;
|
|
9
|
+
permissions: Permission[];
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/*
|
|
13
|
+
* Authorization
|
|
14
|
+
*
|
|
15
|
+
* Contains all of the authorization data for a single request.
|
|
16
|
+
* Stored on every RequestContext.
|
|
17
|
+
*
|
|
18
|
+
* This includes:
|
|
19
|
+
* - The verified 'auth sources' which are the original source of all authorization.
|
|
20
|
+
* - The 'resources' which this request has been granted access to.
|
|
21
|
+
* - The 'permissions' which are fine-level permissions that the request has been granted.
|
|
22
|
+
*/
|
|
23
|
+
export class Authorization {
|
|
24
|
+
private resources: Map<Resource['type'], Resource>;
|
|
25
|
+
private authSources: AuthSource[];
|
|
26
|
+
private userPermissions?: UserPermissions;
|
|
27
|
+
|
|
28
|
+
constructor(resources: Resource[] = [], authSources: AuthSource[] = []) {
|
|
29
|
+
this.resources = new Map();
|
|
30
|
+
for (const resource of resources) {
|
|
31
|
+
this.resources.set(resource.type, resource);
|
|
32
|
+
}
|
|
33
|
+
this.authSources = [...authSources];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
addResource(resource: Resource): void {
|
|
37
|
+
this.resources.set(resource.type, resource);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
hasResource(type: Resource['type']): boolean {
|
|
41
|
+
return this.resources.has(type);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
getResource(type: Resource['type']): Resource | undefined {
|
|
45
|
+
return this.resources.get(type);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
getAllResources(): Resource[] {
|
|
49
|
+
return Array.from(this.resources.values());
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
addAuthSource(authSource: AuthSource): void {
|
|
53
|
+
this.authSources.push(authSource);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
getAuthSources(): AuthSource[] {
|
|
57
|
+
return [...this.authSources];
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
getCookieAuthSource(): CookieAuthSource | undefined {
|
|
61
|
+
return this.authSources.find((c): c is CookieAuthSource => c.type === 'cookie');
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
setUserPermissions(userPermissions: UserPermissions): void {
|
|
65
|
+
this.userPermissions = userPermissions;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
getUserPermissions(): UserPermissions | undefined {
|
|
69
|
+
return this.userPermissions;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
hasPermission(permission: Permission): boolean {
|
|
73
|
+
if (!this.userPermissions) {
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
return this.userPermissions.permissions.includes(permission);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { ServiceDefinition } from '../ServiceDefinition.ts';
|
|
2
|
+
import type { LoadDatabaseFn, MigrationBehavior } from '@facetlayer/sqlite-wrapper';
|
|
3
|
+
|
|
4
|
+
export interface DatabaseInitializationOptions {
|
|
5
|
+
migrationBehavior: MigrationBehavior;
|
|
6
|
+
databasePath: string;
|
|
7
|
+
services?: ServiceDefinition[];
|
|
8
|
+
loadDatabase: LoadDatabaseFn;
|
|
9
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { ServiceDefinition } from '../ServiceDefinition.ts';
|
|
2
|
+
import type { LoadDatabaseFn, MigrationBehavior } from '@facetlayer/sqlite-wrapper';
|
|
3
|
+
|
|
4
|
+
/*
|
|
5
|
+
* getStatementsForDatabase
|
|
6
|
+
*
|
|
7
|
+
* Returns all of the SQL statements for a given database name from the given services.
|
|
8
|
+
*/
|
|
9
|
+
export function getStatementsForDatabase(
|
|
10
|
+
databaseName: string,
|
|
11
|
+
services: ServiceDefinition[]
|
|
12
|
+
): string[] {
|
|
13
|
+
return (services || [])
|
|
14
|
+
.map(service => {
|
|
15
|
+
const databases = service.databases as any;
|
|
16
|
+
return databases?.[databaseName]?.statements || [];
|
|
17
|
+
})
|
|
18
|
+
.flat();
|
|
19
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { logWarn } from "../logging/index.ts";
|
|
2
|
+
import type { EndpointDefinition } from "../web/ExpressEndpointSetup.ts";
|
|
3
|
+
import { validateEndpointForOpenapi } from "../web/openapi/validateServicesForOpenapi.ts";
|
|
4
|
+
import { isValidOperationId } from "./getEffectiveOperationId.ts";
|
|
5
|
+
|
|
6
|
+
export { getEffectiveOperationId, isValidOperationId, generateOperationIdFromPath } from "./getEffectiveOperationId.ts";
|
|
7
|
+
|
|
8
|
+
export function createEndpoint(
|
|
9
|
+
definition: EndpointDefinition
|
|
10
|
+
): EndpointDefinition {
|
|
11
|
+
// Validate operationId if explicitly provided
|
|
12
|
+
if (definition.operationId !== undefined && !isValidOperationId(definition.operationId)) {
|
|
13
|
+
logWarn(`Misconfigured endpoint ${definition.path}: operationId "${definition.operationId}" is not allowed`);
|
|
14
|
+
|
|
15
|
+
return {
|
|
16
|
+
...definition,
|
|
17
|
+
handler: () => {
|
|
18
|
+
throw new Error(`Misconfigured endpoint ${definition.path}: operationId "${definition.operationId}" is not allowed. Use a descriptive unique identifier.`);
|
|
19
|
+
},
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const validationResult = validateEndpointForOpenapi(definition);
|
|
24
|
+
if (validationResult?.error) {
|
|
25
|
+
logWarn(`Misconfigured endpoint ${definition.path}: ${validationResult.error.errorMessage}`);
|
|
26
|
+
// Remove invalid schemas so they don't break OpenAPI generation for the entire service.
|
|
27
|
+
// The endpoint will still work, but won't appear correctly in OpenAPI docs.
|
|
28
|
+
return {
|
|
29
|
+
...definition,
|
|
30
|
+
requestSchema: undefined,
|
|
31
|
+
responseSchema: undefined,
|
|
32
|
+
handler: () => {
|
|
33
|
+
throw new Error(`Misconfigured endpoint ${definition.path}: ${validationResult.error.errorMessage}`);
|
|
34
|
+
},
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return definition;
|
|
39
|
+
}
|