@btst/stack 1.6.0 → 1.7.0
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/dist/api/index.cjs +7 -1
- package/dist/api/index.d.cts +2 -2
- package/dist/api/index.d.mts +2 -2
- package/dist/api/index.d.ts +2 -2
- package/dist/api/index.mjs +7 -1
- package/dist/client/index.d.cts +1 -1
- package/dist/client/index.d.mts +1 -1
- package/dist/client/index.d.ts +1 -1
- package/dist/index.d.cts +1 -1
- package/dist/index.d.mts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/packages/better-stack/src/plugins/open-api/api/generator.cjs +300 -0
- package/dist/packages/better-stack/src/plugins/open-api/api/generator.mjs +284 -0
- package/dist/packages/better-stack/src/plugins/open-api/api/plugin.cjs +115 -0
- package/dist/packages/better-stack/src/plugins/open-api/api/plugin.mjs +113 -0
- package/dist/packages/better-stack/src/plugins/open-api/db.cjs +7 -0
- package/dist/packages/better-stack/src/plugins/open-api/db.mjs +5 -0
- package/dist/packages/better-stack/src/plugins/open-api/logo.cjs +8 -0
- package/dist/packages/better-stack/src/plugins/open-api/logo.mjs +6 -0
- package/dist/plugins/api/index.d.cts +2 -2
- package/dist/plugins/api/index.d.mts +2 -2
- package/dist/plugins/api/index.d.ts +2 -2
- package/dist/plugins/client/index.d.cts +2 -2
- package/dist/plugins/client/index.d.mts +2 -2
- package/dist/plugins/client/index.d.ts +2 -2
- package/dist/plugins/open-api/api/index.cjs +9 -0
- package/dist/plugins/open-api/api/index.d.cts +95 -0
- package/dist/plugins/open-api/api/index.d.mts +95 -0
- package/dist/plugins/open-api/api/index.d.ts +95 -0
- package/dist/plugins/open-api/api/index.mjs +2 -0
- package/dist/shared/{stack.ByOugz9d.d.cts → stack.CSce37mX.d.cts} +15 -2
- package/dist/shared/{stack.ByOugz9d.d.mts → stack.CSce37mX.d.mts} +15 -2
- package/dist/shared/{stack.ByOugz9d.d.ts → stack.CSce37mX.d.ts} +15 -2
- package/package.json +14 -1
- package/src/api/index.ts +14 -2
- package/src/plugins/open-api/api/generator.ts +433 -0
- package/src/plugins/open-api/api/index.ts +8 -0
- package/src/plugins/open-api/api/plugin.ts +243 -0
- package/src/plugins/open-api/db.ts +7 -0
- package/src/plugins/open-api/logo.ts +7 -0
- package/src/types.ts +15 -1
|
@@ -2,6 +2,18 @@ import { Route, createRouter } from '@btst/yar';
|
|
|
2
2
|
import { Adapter, DbPlugin, DatabaseDefinition } from '@btst/db';
|
|
3
3
|
import { Endpoint, Router } from 'better-call';
|
|
4
4
|
|
|
5
|
+
/**
|
|
6
|
+
* Context passed to backend plugins during route creation
|
|
7
|
+
* Provides access to all registered plugins for introspection (used by openAPI plugin)
|
|
8
|
+
*/
|
|
9
|
+
interface BetterStackContext {
|
|
10
|
+
/** All registered backend plugins */
|
|
11
|
+
plugins: Record<string, BackendPlugin<any>>;
|
|
12
|
+
/** The API base path (e.g., "/api/data") */
|
|
13
|
+
basePath: string;
|
|
14
|
+
/** The database adapter */
|
|
15
|
+
adapter: Adapter;
|
|
16
|
+
}
|
|
5
17
|
/**
|
|
6
18
|
* Backend plugin definition
|
|
7
19
|
* Defines API routes and data access for a feature
|
|
@@ -20,8 +32,9 @@ interface BackendPlugin<TRoutes extends Record<string, Endpoint> = Record<string
|
|
|
20
32
|
*
|
|
21
33
|
* @param adapter - Better DB adapter instance with methods:
|
|
22
34
|
* create, update, updateMany, delete, deleteMany, findOne, findMany, count
|
|
35
|
+
* @param context - Optional context with access to all plugins (for introspection)
|
|
23
36
|
*/
|
|
24
|
-
routes: (adapter: Adapter) => TRoutes;
|
|
37
|
+
routes: (adapter: Adapter, context?: BetterStackContext) => TRoutes;
|
|
25
38
|
dbPlugin: DbPlugin;
|
|
26
39
|
}
|
|
27
40
|
/**
|
|
@@ -129,4 +142,4 @@ type SitemapEntry = {
|
|
|
129
142
|
};
|
|
130
143
|
type Sitemap = Array<SitemapEntry>;
|
|
131
144
|
|
|
132
|
-
export type { BackendPlugin as B, ClientPlugin as C, PluginOverrides as P, Sitemap as S,
|
|
145
|
+
export type { BackendPlugin as B, ClientPlugin as C, PluginOverrides as P, Sitemap as S, BetterStackContext as a, PluginRoutes as b, ClientLibConfig as c, ClientLib as d, PrefixedPluginRoutes as e, BackendLibConfig as f, BackendLib as g };
|
|
@@ -2,6 +2,18 @@ import { Route, createRouter } from '@btst/yar';
|
|
|
2
2
|
import { Adapter, DbPlugin, DatabaseDefinition } from '@btst/db';
|
|
3
3
|
import { Endpoint, Router } from 'better-call';
|
|
4
4
|
|
|
5
|
+
/**
|
|
6
|
+
* Context passed to backend plugins during route creation
|
|
7
|
+
* Provides access to all registered plugins for introspection (used by openAPI plugin)
|
|
8
|
+
*/
|
|
9
|
+
interface BetterStackContext {
|
|
10
|
+
/** All registered backend plugins */
|
|
11
|
+
plugins: Record<string, BackendPlugin<any>>;
|
|
12
|
+
/** The API base path (e.g., "/api/data") */
|
|
13
|
+
basePath: string;
|
|
14
|
+
/** The database adapter */
|
|
15
|
+
adapter: Adapter;
|
|
16
|
+
}
|
|
5
17
|
/**
|
|
6
18
|
* Backend plugin definition
|
|
7
19
|
* Defines API routes and data access for a feature
|
|
@@ -20,8 +32,9 @@ interface BackendPlugin<TRoutes extends Record<string, Endpoint> = Record<string
|
|
|
20
32
|
*
|
|
21
33
|
* @param adapter - Better DB adapter instance with methods:
|
|
22
34
|
* create, update, updateMany, delete, deleteMany, findOne, findMany, count
|
|
35
|
+
* @param context - Optional context with access to all plugins (for introspection)
|
|
23
36
|
*/
|
|
24
|
-
routes: (adapter: Adapter) => TRoutes;
|
|
37
|
+
routes: (adapter: Adapter, context?: BetterStackContext) => TRoutes;
|
|
25
38
|
dbPlugin: DbPlugin;
|
|
26
39
|
}
|
|
27
40
|
/**
|
|
@@ -129,4 +142,4 @@ type SitemapEntry = {
|
|
|
129
142
|
};
|
|
130
143
|
type Sitemap = Array<SitemapEntry>;
|
|
131
144
|
|
|
132
|
-
export type { BackendPlugin as B, ClientPlugin as C, PluginOverrides as P, Sitemap as S,
|
|
145
|
+
export type { BackendPlugin as B, ClientPlugin as C, PluginOverrides as P, Sitemap as S, BetterStackContext as a, PluginRoutes as b, ClientLibConfig as c, ClientLib as d, PrefixedPluginRoutes as e, BackendLibConfig as f, BackendLib as g };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@btst/stack",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.7.0",
|
|
4
4
|
"description": "A composable, plugin-based library for building full-stack applications.",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -247,6 +247,16 @@
|
|
|
247
247
|
}
|
|
248
248
|
},
|
|
249
249
|
"./plugins/form-builder/css": "./dist/plugins/form-builder/style.css",
|
|
250
|
+
"./plugins/open-api/api": {
|
|
251
|
+
"import": {
|
|
252
|
+
"types": "./dist/plugins/open-api/api/index.d.ts",
|
|
253
|
+
"default": "./dist/plugins/open-api/api/index.mjs"
|
|
254
|
+
},
|
|
255
|
+
"require": {
|
|
256
|
+
"types": "./dist/plugins/open-api/api/index.d.cts",
|
|
257
|
+
"default": "./dist/plugins/open-api/api/index.cjs"
|
|
258
|
+
}
|
|
259
|
+
},
|
|
250
260
|
"./dist/*": "./dist/*",
|
|
251
261
|
"./ui/css": "./dist/ui/components.css",
|
|
252
262
|
"./package.json": "./package.json"
|
|
@@ -312,6 +322,9 @@
|
|
|
312
322
|
],
|
|
313
323
|
"plugins/form-builder/client/hooks": [
|
|
314
324
|
"./dist/plugins/form-builder/client/hooks/index.d.ts"
|
|
325
|
+
],
|
|
326
|
+
"plugins/open-api/api": [
|
|
327
|
+
"./dist/plugins/open-api/api/index.d.ts"
|
|
315
328
|
]
|
|
316
329
|
}
|
|
317
330
|
},
|
package/src/api/index.ts
CHANGED
|
@@ -3,6 +3,7 @@ import type {
|
|
|
3
3
|
BackendLibConfig,
|
|
4
4
|
BackendLib,
|
|
5
5
|
PrefixedPluginRoutes,
|
|
6
|
+
BetterStackContext,
|
|
6
7
|
} from "../types";
|
|
7
8
|
import { defineDb } from "@btst/db";
|
|
8
9
|
|
|
@@ -45,9 +46,19 @@ export function betterStack<
|
|
|
45
46
|
betterDbSchema = betterDbSchema.use(plugin.dbPlugin);
|
|
46
47
|
}
|
|
47
48
|
|
|
49
|
+
// Create the adapter instance once
|
|
50
|
+
const adapterInstance = adapter(betterDbSchema);
|
|
51
|
+
|
|
52
|
+
// Create context for plugins that need access to all plugins (e.g., openAPI)
|
|
53
|
+
const context: BetterStackContext = {
|
|
54
|
+
plugins,
|
|
55
|
+
basePath,
|
|
56
|
+
adapter: adapterInstance,
|
|
57
|
+
};
|
|
58
|
+
|
|
48
59
|
for (const [pluginKey, plugin] of Object.entries(plugins)) {
|
|
49
|
-
// Pass
|
|
50
|
-
const pluginRoutes = plugin.routes(
|
|
60
|
+
// Pass both adapter and context to plugin routes
|
|
61
|
+
const pluginRoutes = plugin.routes(adapterInstance, context);
|
|
51
62
|
|
|
52
63
|
// Prefix route keys with plugin name to avoid collisions
|
|
53
64
|
for (const [routeKey, endpoint] of Object.entries(pluginRoutes)) {
|
|
@@ -72,4 +83,5 @@ export type {
|
|
|
72
83
|
BackendPlugin,
|
|
73
84
|
BackendLibConfig,
|
|
74
85
|
BackendLib,
|
|
86
|
+
BetterStackContext,
|
|
75
87
|
} from "../types";
|
|
@@ -0,0 +1,433 @@
|
|
|
1
|
+
import type { Endpoint } from "better-call";
|
|
2
|
+
import type { BetterStackContext } from "../../../types";
|
|
3
|
+
import * as z from "zod";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* OpenAPI path operation object
|
|
7
|
+
*/
|
|
8
|
+
export interface PathOperation {
|
|
9
|
+
tags?: string[];
|
|
10
|
+
operationId?: string;
|
|
11
|
+
description?: string;
|
|
12
|
+
summary?: string;
|
|
13
|
+
parameters?: OpenAPIParameter[];
|
|
14
|
+
requestBody?: {
|
|
15
|
+
required?: boolean;
|
|
16
|
+
content: {
|
|
17
|
+
"application/json": {
|
|
18
|
+
schema: Record<string, any>;
|
|
19
|
+
};
|
|
20
|
+
};
|
|
21
|
+
};
|
|
22
|
+
responses: Record<string, any>;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface OpenAPIParameter {
|
|
26
|
+
name: string;
|
|
27
|
+
in: "query" | "path" | "header";
|
|
28
|
+
required?: boolean;
|
|
29
|
+
schema: Record<string, any>;
|
|
30
|
+
description?: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Convert :param to {param} for OpenAPI path format
|
|
35
|
+
*/
|
|
36
|
+
function toOpenApiPath(path: string): string {
|
|
37
|
+
return path
|
|
38
|
+
.split("/")
|
|
39
|
+
.map((part) => (part.startsWith(":") ? `{${part.slice(1)}}` : part))
|
|
40
|
+
.join("/");
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Get the primitive type from a Zod type
|
|
45
|
+
*/
|
|
46
|
+
function getTypeFromZodType(
|
|
47
|
+
zodType: z.ZodType<any>,
|
|
48
|
+
): "string" | "number" | "boolean" | "array" | "object" | "integer" {
|
|
49
|
+
// const typeName = zodType._zpiSkeleton?.type || zodType.constructor.name;
|
|
50
|
+
|
|
51
|
+
if (zodType instanceof z.ZodString) return "string";
|
|
52
|
+
if (zodType instanceof z.ZodNumber) return "number";
|
|
53
|
+
if (zodType instanceof z.ZodBoolean) return "boolean";
|
|
54
|
+
if (zodType instanceof z.ZodArray) return "array";
|
|
55
|
+
if (zodType instanceof z.ZodObject) return "object";
|
|
56
|
+
|
|
57
|
+
// Fallback based on type property if available
|
|
58
|
+
const type = (zodType as any).type;
|
|
59
|
+
if (type === "string") return "string";
|
|
60
|
+
if (type === "number") return "number";
|
|
61
|
+
if (type === "boolean") return "boolean";
|
|
62
|
+
if (type === "array") return "array";
|
|
63
|
+
if (type === "object") return "object";
|
|
64
|
+
|
|
65
|
+
return "string";
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Process a Zod type into an OpenAPI schema
|
|
70
|
+
*/
|
|
71
|
+
function processZodType(zodType: z.ZodType<any>): Record<string, any> {
|
|
72
|
+
// Handle optional - unwrap and process inner type
|
|
73
|
+
// Optionality is handled by the `required` array in parent object schemas,
|
|
74
|
+
// NOT by adding `nullable: true` (which would incorrectly allow null values)
|
|
75
|
+
if (zodType instanceof z.ZodOptional) {
|
|
76
|
+
const innerType =
|
|
77
|
+
(zodType as any)._def?.innerType || (zodType as any).unwrap?.();
|
|
78
|
+
if (innerType) {
|
|
79
|
+
return processZodType(innerType);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Handle nullable
|
|
84
|
+
if (zodType instanceof z.ZodNullable) {
|
|
85
|
+
const innerType =
|
|
86
|
+
(zodType as any)._def?.innerType || (zodType as any).unwrap?.();
|
|
87
|
+
if (innerType) {
|
|
88
|
+
const innerSchema = processZodType(innerType);
|
|
89
|
+
return {
|
|
90
|
+
...innerSchema,
|
|
91
|
+
nullable: true,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Handle default - unwrap and process inner type, including default value
|
|
97
|
+
if (zodType instanceof z.ZodDefault) {
|
|
98
|
+
const innerType = (zodType as any)._def?.innerType;
|
|
99
|
+
const defaultValue = (zodType as any)._def?.defaultValue?.();
|
|
100
|
+
if (innerType) {
|
|
101
|
+
const innerSchema = processZodType(innerType);
|
|
102
|
+
// Include the default value in the OpenAPI schema if it's JSON-serializable
|
|
103
|
+
if (defaultValue !== undefined) {
|
|
104
|
+
return {
|
|
105
|
+
...innerSchema,
|
|
106
|
+
default: defaultValue,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
return innerSchema;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Handle object
|
|
114
|
+
if (zodType instanceof z.ZodObject) {
|
|
115
|
+
const shape = (zodType as any).shape || (zodType as any)._def?.shape?.();
|
|
116
|
+
if (shape) {
|
|
117
|
+
const properties: Record<string, any> = {};
|
|
118
|
+
const required: string[] = [];
|
|
119
|
+
|
|
120
|
+
for (const [key, value] of Object.entries(shape)) {
|
|
121
|
+
if (value instanceof z.ZodType) {
|
|
122
|
+
properties[key] = processZodType(value);
|
|
123
|
+
if (!(value instanceof z.ZodOptional)) {
|
|
124
|
+
required.push(key);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return {
|
|
130
|
+
type: "object",
|
|
131
|
+
properties,
|
|
132
|
+
...(required.length > 0 ? { required } : {}),
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Handle array
|
|
138
|
+
if (zodType instanceof z.ZodArray) {
|
|
139
|
+
const elementType = (zodType as any)._def?.type || (zodType as any).element;
|
|
140
|
+
return {
|
|
141
|
+
type: "array",
|
|
142
|
+
items: elementType ? processZodType(elementType) : { type: "string" },
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Handle enum
|
|
147
|
+
if (zodType instanceof z.ZodEnum) {
|
|
148
|
+
const values = (zodType as any)._def?.values || (zodType as any).options;
|
|
149
|
+
return {
|
|
150
|
+
type: "string",
|
|
151
|
+
enum: values,
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Handle literal
|
|
156
|
+
if (zodType instanceof z.ZodLiteral) {
|
|
157
|
+
const value = (zodType as any)._def?.value || (zodType as any).value;
|
|
158
|
+
// Map JavaScript typeof to OpenAPI 3.1 types correctly
|
|
159
|
+
// Note: typeof null === "object" in JS, but OpenAPI 3.1 has "null" type
|
|
160
|
+
let type: string;
|
|
161
|
+
if (value === null) {
|
|
162
|
+
type = "null";
|
|
163
|
+
} else if (value === undefined) {
|
|
164
|
+
// undefined is not a valid JSON/OpenAPI value, treat as nullable
|
|
165
|
+
return { nullable: true };
|
|
166
|
+
} else {
|
|
167
|
+
type = typeof value;
|
|
168
|
+
}
|
|
169
|
+
return {
|
|
170
|
+
type,
|
|
171
|
+
const: value,
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Handle union
|
|
176
|
+
if (zodType instanceof z.ZodUnion) {
|
|
177
|
+
const options = (zodType as any)._def?.options || (zodType as any).options;
|
|
178
|
+
if (options && Array.isArray(options)) {
|
|
179
|
+
return {
|
|
180
|
+
oneOf: options.map((opt: z.ZodType<any>) => processZodType(opt)),
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Handle coerce types
|
|
186
|
+
if ((zodType as any)._def?.coerce) {
|
|
187
|
+
const innerType = (zodType as any)._def?.innerType;
|
|
188
|
+
if (innerType) {
|
|
189
|
+
return processZodType(innerType);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Default to primitive type
|
|
194
|
+
return {
|
|
195
|
+
type: getTypeFromZodType(zodType),
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Extract query parameters from endpoint options
|
|
201
|
+
*/
|
|
202
|
+
function getParameters(options: any): OpenAPIParameter[] {
|
|
203
|
+
const parameters: OpenAPIParameter[] = [];
|
|
204
|
+
|
|
205
|
+
// Handle query parameters
|
|
206
|
+
if (options.query instanceof z.ZodObject) {
|
|
207
|
+
const shape =
|
|
208
|
+
(options.query as any).shape || (options.query as any)._def?.shape?.();
|
|
209
|
+
if (shape) {
|
|
210
|
+
for (const [key, value] of Object.entries(shape)) {
|
|
211
|
+
if (value instanceof z.ZodType) {
|
|
212
|
+
parameters.push({
|
|
213
|
+
name: key,
|
|
214
|
+
in: "query",
|
|
215
|
+
required: !(value instanceof z.ZodOptional),
|
|
216
|
+
schema: processZodType(value),
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// Handle path parameters from params schema
|
|
224
|
+
if (options.params instanceof z.ZodObject) {
|
|
225
|
+
const shape =
|
|
226
|
+
(options.params as any).shape || (options.params as any)._def?.shape?.();
|
|
227
|
+
if (shape) {
|
|
228
|
+
for (const [key, value] of Object.entries(shape)) {
|
|
229
|
+
if (value instanceof z.ZodType) {
|
|
230
|
+
parameters.push({
|
|
231
|
+
name: key,
|
|
232
|
+
in: "path",
|
|
233
|
+
required: true,
|
|
234
|
+
schema: processZodType(value),
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
return parameters;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Extract request body schema from endpoint options
|
|
246
|
+
*
|
|
247
|
+
* Handles any Zod type as request body including:
|
|
248
|
+
* - ZodObject (most common)
|
|
249
|
+
* - ZodArray (batch/bulk operations)
|
|
250
|
+
* - ZodUnion (multiple accepted formats)
|
|
251
|
+
* - ZodOptional (optional body)
|
|
252
|
+
* - ZodNullable, ZodEnum, ZodLiteral, etc.
|
|
253
|
+
*/
|
|
254
|
+
function getRequestBody(
|
|
255
|
+
options: any,
|
|
256
|
+
): PathOperation["requestBody"] | undefined {
|
|
257
|
+
if (!options.body) return undefined;
|
|
258
|
+
|
|
259
|
+
// Handle any Zod type - processZodType already handles all Zod types
|
|
260
|
+
if (options.body instanceof z.ZodType) {
|
|
261
|
+
const schema = processZodType(options.body);
|
|
262
|
+
|
|
263
|
+
// Determine if body is required:
|
|
264
|
+
// - ZodOptional: not required
|
|
265
|
+
// - ZodNullable: required but can be null (nullable is set in schema)
|
|
266
|
+
// - Everything else: required
|
|
267
|
+
const isOptional = options.body instanceof z.ZodOptional;
|
|
268
|
+
|
|
269
|
+
return {
|
|
270
|
+
required: !isOptional,
|
|
271
|
+
content: {
|
|
272
|
+
"application/json": {
|
|
273
|
+
schema,
|
|
274
|
+
},
|
|
275
|
+
},
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
return undefined;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* Create a fresh error schema object to avoid circular references in JSON serialization
|
|
284
|
+
*/
|
|
285
|
+
function createErrorSchema(): Record<string, any> {
|
|
286
|
+
return {
|
|
287
|
+
type: "object",
|
|
288
|
+
properties: {
|
|
289
|
+
message: { type: "string" },
|
|
290
|
+
},
|
|
291
|
+
required: ["message"],
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Generate standard error responses (creates fresh objects to avoid circular refs)
|
|
297
|
+
*/
|
|
298
|
+
function getErrorResponses(): Record<string, any> {
|
|
299
|
+
return {
|
|
300
|
+
"400": {
|
|
301
|
+
description: "Bad Request",
|
|
302
|
+
content: { "application/json": { schema: createErrorSchema() } },
|
|
303
|
+
},
|
|
304
|
+
"401": {
|
|
305
|
+
description: "Unauthorized",
|
|
306
|
+
content: { "application/json": { schema: createErrorSchema() } },
|
|
307
|
+
},
|
|
308
|
+
"403": {
|
|
309
|
+
description: "Forbidden",
|
|
310
|
+
content: { "application/json": { schema: createErrorSchema() } },
|
|
311
|
+
},
|
|
312
|
+
"404": {
|
|
313
|
+
description: "Not Found",
|
|
314
|
+
content: { "application/json": { schema: createErrorSchema() } },
|
|
315
|
+
},
|
|
316
|
+
"500": {
|
|
317
|
+
description: "Internal Server Error",
|
|
318
|
+
content: { "application/json": { schema: createErrorSchema() } },
|
|
319
|
+
},
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* Generate OpenAPI 3.1 schema from Better Stack context
|
|
325
|
+
*/
|
|
326
|
+
export function generateOpenAPISchema(
|
|
327
|
+
context: BetterStackContext,
|
|
328
|
+
options?: { title?: string; description?: string; version?: string },
|
|
329
|
+
): Record<string, any> {
|
|
330
|
+
const paths: Record<string, Record<string, PathOperation>> = {};
|
|
331
|
+
const tags: Array<{ name: string; description: string }> = [];
|
|
332
|
+
|
|
333
|
+
// Iterate over all plugins
|
|
334
|
+
for (const [pluginKey, plugin] of Object.entries(context.plugins)) {
|
|
335
|
+
// Skip the open-api plugin itself
|
|
336
|
+
if (pluginKey === "openApi" || plugin.name === "open-api") {
|
|
337
|
+
continue;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// Get plugin routes
|
|
341
|
+
const pluginRoutes = plugin.routes(context.adapter, context);
|
|
342
|
+
|
|
343
|
+
// Create tag for this plugin
|
|
344
|
+
const tagName = pluginKey.charAt(0).toUpperCase() + pluginKey.slice(1);
|
|
345
|
+
tags.push({
|
|
346
|
+
name: tagName,
|
|
347
|
+
description: `${tagName} plugin endpoints`,
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
// Process each endpoint
|
|
351
|
+
for (const [routeKey, endpoint] of Object.entries(pluginRoutes)) {
|
|
352
|
+
const ep = endpoint as Endpoint;
|
|
353
|
+
|
|
354
|
+
// Access endpoint properties
|
|
355
|
+
const path = (ep as any).path;
|
|
356
|
+
const endpointOptions = (ep as any).options || {};
|
|
357
|
+
const method = (endpointOptions.method || "GET").toLowerCase();
|
|
358
|
+
|
|
359
|
+
if (!path) continue;
|
|
360
|
+
|
|
361
|
+
const openApiPath = toOpenApiPath(path);
|
|
362
|
+
|
|
363
|
+
// Initialize path object if needed
|
|
364
|
+
if (!paths[openApiPath]) {
|
|
365
|
+
paths[openApiPath] = {};
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
// Build operation object
|
|
369
|
+
const operation: PathOperation = {
|
|
370
|
+
tags: [tagName],
|
|
371
|
+
operationId: `${pluginKey}_${routeKey}`,
|
|
372
|
+
summary: endpointOptions.metadata?.openapi?.summary,
|
|
373
|
+
description: endpointOptions.metadata?.openapi?.description,
|
|
374
|
+
parameters: getParameters(endpointOptions),
|
|
375
|
+
responses: {
|
|
376
|
+
"200": {
|
|
377
|
+
description: "Successful response",
|
|
378
|
+
content: {
|
|
379
|
+
"application/json": {
|
|
380
|
+
schema: { type: "object" },
|
|
381
|
+
},
|
|
382
|
+
},
|
|
383
|
+
},
|
|
384
|
+
...getErrorResponses(),
|
|
385
|
+
},
|
|
386
|
+
};
|
|
387
|
+
|
|
388
|
+
// Add request body for POST/PUT/PATCH
|
|
389
|
+
if (["post", "put", "patch"].includes(method)) {
|
|
390
|
+
const requestBody = getRequestBody(endpointOptions);
|
|
391
|
+
if (requestBody) {
|
|
392
|
+
operation.requestBody = requestBody;
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
paths[openApiPath][method] = operation;
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
return {
|
|
401
|
+
openapi: "3.1.0",
|
|
402
|
+
info: {
|
|
403
|
+
title: options?.title || "Better Stack API",
|
|
404
|
+
description:
|
|
405
|
+
options?.description ||
|
|
406
|
+
"API Reference for your Better Stack application",
|
|
407
|
+
version: options?.version || "1.0.0",
|
|
408
|
+
},
|
|
409
|
+
servers: [
|
|
410
|
+
{
|
|
411
|
+
url: context.basePath,
|
|
412
|
+
description: "API Server",
|
|
413
|
+
},
|
|
414
|
+
],
|
|
415
|
+
tags,
|
|
416
|
+
paths,
|
|
417
|
+
components: {
|
|
418
|
+
securitySchemes: {
|
|
419
|
+
bearerAuth: {
|
|
420
|
+
type: "http",
|
|
421
|
+
scheme: "bearer",
|
|
422
|
+
description: "Bearer token authentication",
|
|
423
|
+
},
|
|
424
|
+
cookieAuth: {
|
|
425
|
+
type: "apiKey",
|
|
426
|
+
in: "cookie",
|
|
427
|
+
name: "session",
|
|
428
|
+
description: "Session cookie authentication",
|
|
429
|
+
},
|
|
430
|
+
},
|
|
431
|
+
},
|
|
432
|
+
};
|
|
433
|
+
}
|