bxo 0.0.5-dev.70 → 0.0.5-dev.72

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 CHANGED
@@ -93,6 +93,76 @@ app.use(cors({
93
93
  }));
94
94
  ```
95
95
 
96
+ ### OpenAPI Plugin
97
+
98
+ The OpenAPI plugin automatically generates OpenAPI 3.0 documentation with support for tags, security schemes, and comprehensive route metadata:
99
+
100
+ ```typescript
101
+ import { openapi } from "./plugins";
102
+
103
+ app.use(openapi({
104
+ path: "/docs", // Swagger UI endpoint
105
+ jsonPath: "/openapi.json", // OpenAPI JSON endpoint
106
+ defaultTags: ["API"], // Default tags for routes
107
+ securitySchemes: { // Define security schemes
108
+ bearerAuth: {
109
+ type: "http",
110
+ scheme: "bearer",
111
+ bearerFormat: "JWT",
112
+ description: "JWT token for authentication"
113
+ },
114
+ apiKeyAuth: {
115
+ type: "apiKey",
116
+ in: "header",
117
+ name: "X-API-Key",
118
+ description: "API key for authentication"
119
+ }
120
+ },
121
+ globalSecurity: [ // Global security requirements
122
+ { bearerAuth: [] },
123
+ { apiKeyAuth: [] }
124
+ ],
125
+ openapiConfig: { // Additional OpenAPI config
126
+ info: {
127
+ title: "My API",
128
+ version: "1.0.0",
129
+ description: "API description"
130
+ }
131
+ }
132
+ }));
133
+ ```
134
+
135
+ #### Route Metadata
136
+
137
+ Routes can include detailed metadata for better OpenAPI documentation:
138
+
139
+ ```typescript
140
+ app.get("/users/:id", (ctx) => {
141
+ const id = ctx.params.id
142
+ return { user: { id, name: "John Doe" } }
143
+ }, {
144
+ detail: {
145
+ tags: ["Users"], // Route tags for grouping
146
+ summary: "Get user by ID", // Operation summary
147
+ description: "Retrieve user details", // Detailed description
148
+ security: [{ bearerAuth: [] }], // Route-specific security
149
+ params: { // Parameter documentation
150
+ id: z.string().describe("User ID")
151
+ }
152
+ }
153
+ })
154
+ ```
155
+
156
+ #### Supported Metadata Fields
157
+
158
+ - `tags`: Array of tags for grouping operations
159
+ - `summary`: Short description of the operation
160
+ - `description`: Detailed description of the operation
161
+ - `security`: Security requirements for the route
162
+ - `params`: Path parameter schemas and descriptions
163
+ - `query`: Query parameter schemas
164
+ - `hidden`: Set to `true` to exclude from OpenAPI docs
165
+
96
166
  ### Creating Custom Plugins
97
167
 
98
168
  Plugins are just BXO instances with lifecycle hooks:
@@ -149,6 +219,7 @@ bun run ./example/cors-example.ts
149
219
  Check out the `example/` directory for more usage examples:
150
220
 
151
221
  - `cors-example.ts` - Demonstrates CORS plugin and lifecycle hooks
222
+ - `openapi-example.ts` - Demonstrates OpenAPI plugin with tags and security
152
223
  - `index.ts` - Basic routing example
153
224
 
154
225
  This project was created using `bun init` in bun v1.2.3. [Bun](https://bun.sh) is a fast all-in-one JavaScript runtime.
package/example/index.ts CHANGED
@@ -8,6 +8,7 @@ async function main() {
8
8
  bxo.default("/", index);
9
9
  bxo.default("/*", index);
10
10
 
11
+ // API routes with comprehensive metadata
11
12
  bxo.get("/api/get/:id", (ctx) => {
12
13
  return new Response(ctx.params.id + ctx.query.name, {
13
14
  headers: {
@@ -23,7 +24,16 @@ async function main() {
23
24
  name: z.string()
24
25
  })
25
26
  },
27
+ detail: {
28
+ tags: ["API"],
29
+ summary: "Get data by ID",
30
+ description: "Retrieve data using an ID and name query parameter",
31
+ params: {
32
+ id: z.string().describe("Unique identifier for the data")
33
+ }
34
+ }
26
35
  });
36
+
27
37
  bxo.post("/api/post", (ctx) => {
28
38
  console.log(ctx.body)
29
39
  return new Response("Hello" + ctx.body.name, {
@@ -33,8 +43,10 @@ async function main() {
33
43
  });
34
44
  }, {
35
45
  detail: {
46
+ tags: ["API"],
47
+ summary: "Create new data",
48
+ description: "Submit new data with name and avatar file",
36
49
  defaultContentType: "multipart/form-data"
37
-
38
50
  },
39
51
  body: z.object({
40
52
  name: z.string(),
@@ -49,10 +61,131 @@ async function main() {
49
61
  })
50
62
  }
51
63
  });
52
- bxo.use(openapi())
64
+
65
+ // Additional routes to showcase different features
66
+ bxo.get("/api/users", (ctx) => {
67
+ return ctx.json({ users: ["John", "Jane", "Bob"] });
68
+ }, {
69
+ detail: {
70
+ tags: ["Users"],
71
+ summary: "Get all users",
72
+ description: "Retrieve a list of all users in the system"
73
+ },
74
+ response: {
75
+ 200: z.object({
76
+ users: z.array(z.string())
77
+ })
78
+ }
79
+ });
80
+
81
+ bxo.get("/api/users/:id", (ctx) => {
82
+ const id = ctx.params.id;
83
+ return ctx.json({ user: { id, name: "John Doe", email: "john@example.com" } });
84
+ }, {
85
+ detail: {
86
+ tags: ["Users"],
87
+ summary: "Get user by ID",
88
+ description: "Retrieve a specific user by their unique identifier",
89
+ params: {
90
+ id: z.string().describe("User's unique identifier")
91
+ }
92
+ },
93
+ response: {
94
+ 200: z.object({
95
+ user: z.object({
96
+ id: z.string(),
97
+ name: z.string(),
98
+ email: z.string()
99
+ })
100
+ }),
101
+ 404: z.object({
102
+ error: z.string()
103
+ })
104
+ }
105
+ });
106
+
107
+ bxo.post("/api/users", (ctx) => {
108
+ const userData = ctx.body;
109
+ return ctx.json({ message: "User created", user: userData });
110
+ }, {
111
+ detail: {
112
+ tags: ["Users"],
113
+ summary: "Create new user",
114
+ description: "Create a new user account with the provided information"
115
+ },
116
+ body: z.object({
117
+ name: z.string().min(1, "Name is required"),
118
+ email: z.string().email("Invalid email format"),
119
+ age: z.number().min(18, "Must be at least 18 years old").optional()
120
+ }),
121
+ response: {
122
+ 201: z.object({
123
+ message: z.string(),
124
+ user: z.object({
125
+ name: z.string(),
126
+ email: z.string(),
127
+ age: z.number().optional()
128
+ })
129
+ }),
130
+ 400: z.object({
131
+ error: z.string(),
132
+ issues: z.array(z.any()).optional()
133
+ })
134
+ }
135
+ });
136
+
137
+ // Health check route
138
+ bxo.get("/health", (ctx) => {
139
+ return ctx.json({
140
+ status: "ok",
141
+ timestamp: new Date().toISOString(),
142
+ uptime: process.uptime()
143
+ });
144
+ }, {
145
+ detail: {
146
+ tags: ["System"],
147
+ summary: "Health check",
148
+ description: "Check the health status of the API server"
149
+ },
150
+ response: {
151
+ 200: z.object({
152
+ status: z.string(),
153
+ timestamp: z.string(),
154
+ uptime: z.number()
155
+ })
156
+ }
157
+ });
158
+
159
+ // Use OpenAPI plugin with enhanced configuration
160
+ bxo.use(openapi({
161
+ path: "/docs",
162
+ jsonPath: "/openapi.json",
163
+ defaultTags: ["API"],
164
+ securitySchemes: {
165
+ bearerAuth: {
166
+ type: "http",
167
+ scheme: "bearer",
168
+ bearerFormat: "JWT",
169
+ description: "JWT token for authentication"
170
+ },
171
+ apiKeyAuth: {
172
+ type: "apiKey",
173
+ in: "header",
174
+ name: "X-API-Key",
175
+ description: "API key for authentication"
176
+ }
177
+ },
178
+ globalSecurity: [
179
+ { bearerAuth: [] },
180
+ { apiKeyAuth: [] }
181
+ ],
182
+ openapiConfig: {}
183
+ }));
184
+
53
185
  bxo.start();
54
186
  console.log(`Server is running on http://localhost:${bxo.server?.port}`);
55
-
187
+ console.log(`OpenAPI documentation available at http://localhost:${bxo.server?.port}/docs`);
188
+ console.log(`OpenAPI JSON available at http://localhost:${bxo.server?.port}/openapi.json`);
56
189
  }
57
190
 
58
191
  main();
@@ -0,0 +1,132 @@
1
+ import BXO, { z } from "../src";
2
+ import { openapi } from "../plugins/openapi";
3
+
4
+ // Create a BXO app with OpenAPI plugin
5
+ const app = new BXO()
6
+ .use(openapi({
7
+ path: "/docs",
8
+ jsonPath: "/openapi.json",
9
+ defaultTags: ["API"],
10
+ securitySchemes: {
11
+ bearerAuth: {
12
+ type: "http",
13
+ scheme: "bearer",
14
+ bearerFormat: "JWT",
15
+ description: "JWT token for authentication"
16
+ },
17
+ apiKeyAuth: {
18
+ type: "apiKey",
19
+ in: "header",
20
+ name: "X-API-Key",
21
+ description: "API key for authentication"
22
+ }
23
+ },
24
+ globalSecurity: [
25
+ { bearerAuth: [] },
26
+ { apiKeyAuth: [] }
27
+ ],
28
+ openapiConfig: {
29
+ info: {
30
+ title: "My API with Security",
31
+ version: "1.0.0",
32
+ description: "An example API with OpenAPI documentation, tags, and security"
33
+ }
34
+ }
35
+ }))
36
+
37
+ // Example routes with tags and security
38
+ app.get("/users", (ctx) => {
39
+ return { users: [] }
40
+ }, {
41
+ detail: {
42
+ tags: ["Users"],
43
+ summary: "Get all users",
44
+ description: "Retrieve a list of all users",
45
+ security: [{ bearerAuth: [] }]
46
+ }
47
+ })
48
+
49
+ app.get("/users/:id", (ctx) => {
50
+ const id = ctx.params.id
51
+ return { user: { id, name: "John Doe" } }
52
+ }, {
53
+ detail: {
54
+ tags: ["Users"],
55
+ summary: "Get user by ID",
56
+ description: "Retrieve a specific user by their ID",
57
+ params: {
58
+ id: z.string().describe("User ID")
59
+ },
60
+ security: [{ apiKeyAuth: [] }]
61
+ }
62
+ })
63
+
64
+ app.post("/users", (ctx) => {
65
+ const body = ctx.body
66
+ return { message: "User created", user: body }
67
+ }, {
68
+ body: z.object({
69
+ name: z.string(),
70
+ email: z.string().email()
71
+ }),
72
+ detail: {
73
+ tags: ["Users"],
74
+ summary: "Create a new user",
75
+ description: "Create a new user with the provided information",
76
+ security: [{ bearerAuth: [] }]
77
+ }
78
+ })
79
+
80
+ app.get("/products", (ctx) => {
81
+ return { products: [] }
82
+ }, {
83
+ detail: {
84
+ tags: ["Products"],
85
+ summary: "Get all products",
86
+ description: "Retrieve a list of all products"
87
+ }
88
+ })
89
+
90
+ app.get("/products/:id", (ctx) => {
91
+ const id = ctx.params.id
92
+ return { product: { id, name: "Sample Product" } }
93
+ }, {
94
+ detail: {
95
+ tags: ["Products"],
96
+ summary: "Get product by ID",
97
+ description: "Retrieve a specific product by its ID",
98
+ params: {
99
+ id: z.string().describe("Product ID")
100
+ }
101
+ }
102
+ })
103
+
104
+ // Admin routes with different security
105
+ app.get("/admin/users", (ctx) => {
106
+ return { adminUsers: [] }
107
+ }, {
108
+ detail: {
109
+ tags: ["Admin"],
110
+ summary: "Get all users (Admin)",
111
+ description: "Admin-only endpoint to retrieve all users",
112
+ security: [{ bearerAuth: [] }, { apiKeyAuth: [] }]
113
+ }
114
+ })
115
+
116
+ // Health check route (no security required)
117
+ app.get("/health", (ctx) => {
118
+ return { status: "ok", timestamp: new Date().toISOString() }
119
+ }, {
120
+ detail: {
121
+ tags: ["System"],
122
+ summary: "Health check",
123
+ description: "Check if the API is running"
124
+ }
125
+ })
126
+
127
+ // Start the server
128
+ app.listen(3000, () => {
129
+ console.log("Server running on http://localhost:3000")
130
+ console.log("OpenAPI docs available at http://localhost:3000/docs")
131
+ console.log("OpenAPI JSON available at http://localhost:3000/openapi.json")
132
+ })
package/package.json CHANGED
@@ -5,7 +5,7 @@
5
5
  ".": "./src/index.ts",
6
6
  "./plugins": "./plugins/index.ts"
7
7
  },
8
- "version": "0.0.5-dev.70",
8
+ "version": "0.0.5-dev.72",
9
9
  "type": "module",
10
10
  "devDependencies": {
11
11
  "@types/bun": "latest"
@@ -1,14 +1,22 @@
1
1
  import BXO, { z } from "../src";
2
- import { createDocument, type CreateDocumentOptions, type ZodOpenApiPathsObject } from "zod-openapi";
3
-
4
- class OpenApiConfig {
2
+ import { createDocument, type CreateDocumentOptions, type ZodOpenApiPathItemObject, type ZodOpenApiPathsObject, type ZodOpenApiSecuritySchemeObject } from "zod-openapi";
5
3
 
4
+ interface SecurityScheme extends ZodOpenApiSecuritySchemeObject {
5
+ type: "http" | "apiKey" | "oauth2" | "openIdConnect";
6
+ scheme?: "bearer" | "basic" | "digest" | "apikey";
7
+ bearerFormat?: string;
8
+ description?: string;
9
+ name?: string;
10
+ in?: "header" | "query" | "cookie";
6
11
  }
7
12
 
8
13
  interface OpenApiPluginConfig {
9
14
  path: string;
10
15
  jsonPath: string;
11
16
  openapiConfig: CreateDocumentOptions;
17
+ defaultTags?: string[];
18
+ securitySchemes?: Record<string, SecurityScheme>;
19
+ globalSecurity?: Array<Record<string, string[]>>;
12
20
  }
13
21
 
14
22
  const createOpenApiPaths = (app: BXO, config?: OpenApiPluginConfig): ZodOpenApiPathsObject => {
@@ -30,6 +38,60 @@ const createOpenApiPaths = (app: BXO, config?: OpenApiPluginConfig): ZodOpenApiP
30
38
  if (route.schema?.detail?.hidden) {
31
39
  continue
32
40
  }
41
+
42
+ // Extract tags from route metadata
43
+ const tags = route.schema?.detail?.tags ||
44
+ route.schema?.detail?.tag ||
45
+ config?.defaultTags ||
46
+ []
47
+
48
+ // Extract security requirements from route metadata
49
+ const routeSecurity = route.schema?.detail?.security ||
50
+ route.schema?.detail?.auth ||
51
+ undefined
52
+
53
+ // Extract operation summary and description
54
+ const summary = route.schema?.detail?.summary ||
55
+ route.schema?.detail?.title ||
56
+ `${method.toUpperCase()} ${route.path}`
57
+
58
+ const description = route.schema?.detail?.description ||
59
+ route.schema?.detail?.docs ||
60
+ undefined
61
+
62
+ // Extract parameters from route path
63
+ const parameters = []
64
+ const pathParams = route.path.match(/:\w+/g)
65
+ if (pathParams) {
66
+ for (const param of pathParams) {
67
+ const paramName = param.slice(1) // Remove the colon
68
+ const paramSchema = route.schema?.detail?.params?.[paramName] || z.string()
69
+ parameters.push({
70
+ name: paramName,
71
+ in: "path",
72
+ required: true,
73
+ schema: paramSchema
74
+ })
75
+ }
76
+ }
77
+
78
+ // Add query parameters if defined
79
+ if (route.schema?.query) {
80
+ const querySchema = route.schema?.query
81
+ if (querySchema && typeof querySchema === 'object' && 'shape' in querySchema) {
82
+ const queryShape = (querySchema as any).shape
83
+ for (const [key, schema] of Object.entries(queryShape)) {
84
+ const isOptional = schema instanceof z.ZodOptional
85
+ parameters.push({
86
+ name: key,
87
+ in: "query",
88
+ required: !isOptional, // Query params are typically optional
89
+ schema: schema as any
90
+ })
91
+ }
92
+ }
93
+ }
94
+
33
95
  const response = Object.entries(route.schema?.response || {}).map(([status, schema]) => {
34
96
  return ({
35
97
  400: status === "400" && !route.schema?.response?.[status] ? {
@@ -51,9 +113,15 @@ const createOpenApiPaths = (app: BXO, config?: OpenApiPluginConfig): ZodOpenApiP
51
113
  }
52
114
  })
53
115
  }).reduce((acc, curr) => ({ ...acc, ...curr }), {})
116
+
54
117
  paths[openapiPath] = {
55
118
  ...paths[openapiPath],
56
119
  [method]: {
120
+ tags: tags.length > 0 ? tags : undefined,
121
+ summary: summary,
122
+ description: description,
123
+ parameters: parameters.length > 0 ? parameters : undefined,
124
+ security: routeSecurity,
57
125
  requestBody: {
58
126
  content: {
59
127
  [contentType]: {
@@ -71,18 +139,20 @@ const createOpenApiPaths = (app: BXO, config?: OpenApiPluginConfig): ZodOpenApiP
71
139
  }
72
140
  }
73
141
  }
74
- }
142
+ } satisfies ZodOpenApiPathItemObject
75
143
  }
76
144
  return paths
77
145
  }
78
146
 
79
147
  export function openapi(_config?: OpenApiPluginConfig) {
80
148
  let config = _config
81
- !config && (config = { path: "/docs", openapiConfig: new OpenApiConfig(), jsonPath: "/openapi.json" })
149
+ !config && (config = { path: "/docs", openapiConfig: {}, jsonPath: "/openapi.json" })
82
150
  config.path = config.path || "/docs"
83
151
  config.jsonPath = config.jsonPath || "/openapi.json"
84
152
  config.openapiConfig = config.openapiConfig || {}
85
-
153
+ config.defaultTags = config.defaultTags || []
154
+ config.securitySchemes = config.securitySchemes || {}
155
+ config.globalSecurity = config.globalSecurity || []
86
156
 
87
157
  const bxo = new BXO()
88
158
  .get(config.jsonPath, (ctx, app) => {
@@ -93,6 +163,10 @@ export function openapi(_config?: OpenApiPluginConfig) {
93
163
  version: "1.0.0"
94
164
  },
95
165
  paths: createOpenApiPaths(app, config),
166
+ components: {
167
+ securitySchemes: Object.keys(config.securitySchemes || {}).length > 0 ? config.securitySchemes : undefined
168
+ },
169
+ security: (config.globalSecurity || []).length > 0 ? config.globalSecurity : undefined,
96
170
  ...config.openapiConfig
97
171
  })
98
172
  return new Response(JSON.stringify(paths), {