@elysiajs/openapi 1.3.2

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/index.mjs ADDED
@@ -0,0 +1,612 @@
1
+ // src/index.ts
2
+ import { Elysia } from "elysia";
3
+
4
+ // src/swagger/index.ts
5
+ function isSchemaObject(schema) {
6
+ return "type" in schema || "properties" in schema || "items" in schema;
7
+ }
8
+ function isDateTimeProperty(key, schema) {
9
+ return (key === "createdAt" || key === "updatedAt") && "anyOf" in schema && Array.isArray(schema.anyOf);
10
+ }
11
+ function transformDateProperties(schema) {
12
+ if (!isSchemaObject(schema) || typeof schema !== "object" || schema === null)
13
+ return schema;
14
+ const newSchema = { ...schema };
15
+ Object.entries(newSchema).forEach(([key, value]) => {
16
+ if (isSchemaObject(value)) {
17
+ if (isDateTimeProperty(key, value)) {
18
+ const dateTimeFormat = value.anyOf?.find(
19
+ (item) => isSchemaObject(item) && item.format === "date-time"
20
+ );
21
+ if (dateTimeFormat) {
22
+ const dateTimeSchema = {
23
+ type: "string",
24
+ format: "date-time",
25
+ default: dateTimeFormat.default
26
+ };
27
+ newSchema[key] = dateTimeSchema;
28
+ }
29
+ } else {
30
+ ;
31
+ newSchema[key] = transformDateProperties(value);
32
+ }
33
+ }
34
+ });
35
+ return newSchema;
36
+ }
37
+ var SwaggerUIRender = (info, config) => {
38
+ const {
39
+ version = "latest",
40
+ theme = `https://unpkg.com/swagger-ui-dist@${version ?? "latest"}/swagger-ui.css`,
41
+ cdn = `https://unpkg.com/swagger-ui-dist@${version}/swagger-ui-bundle.js`,
42
+ autoDarkMode = true,
43
+ ...rest
44
+ } = config;
45
+ const stringifiedOptions = JSON.stringify(
46
+ {
47
+ dom_id: "#swagger-ui",
48
+ ...rest
49
+ },
50
+ (_, value) => typeof value === "function" ? void 0 : value
51
+ );
52
+ const options = JSON.parse(stringifiedOptions);
53
+ if (options.components && options.components.schemas)
54
+ options.components.schemas = Object.fromEntries(
55
+ Object.entries(options.components.schemas).map(([key, schema]) => [
56
+ key,
57
+ transformDateProperties(schema)
58
+ ])
59
+ );
60
+ return `<!DOCTYPE html>
61
+ <html lang="en">
62
+ <head>
63
+ <meta charset="utf-8" />
64
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
65
+ <title>${info.title}</title>
66
+ <meta
67
+ name="description"
68
+ content="${info.description}"
69
+ />
70
+ <meta
71
+ name="og:description"
72
+ content="${info.description}"
73
+ />
74
+ ${autoDarkMode && typeof theme === "string" ? `<style>
75
+ @media (prefers-color-scheme: dark) {
76
+ body {
77
+ background-color: #222;
78
+ color: #faf9a;
79
+ }
80
+ .swagger-ui {
81
+ filter: invert(92%) hue-rotate(180deg);
82
+ }
83
+
84
+ .swagger-ui .microlight {
85
+ filter: invert(100%) hue-rotate(180deg);
86
+ }
87
+ }
88
+ </style>` : ""}
89
+ ${typeof theme === "string" ? `<link rel="stylesheet" href="${theme}" />` : `<link rel="stylesheet" media="(prefers-color-scheme: light)" href="${theme.light}" />
90
+ <link rel="stylesheet" media="(prefers-color-scheme: dark)" href="${theme.dark}" />`}
91
+ </head>
92
+ <body>
93
+ <div id="swagger-ui"></div>
94
+ <script src="${cdn}" crossorigin></script>
95
+ <script>
96
+ window.onload = () => {
97
+ window.ui = SwaggerUIBundle(${stringifiedOptions});
98
+ };
99
+ </script>
100
+ </body>
101
+ </html>`;
102
+ };
103
+
104
+ // src/scalar/index.ts
105
+ var elysiaCSS = `.light-mode {
106
+ --scalar-color-1: #2a2f45;
107
+ --scalar-color-2: #757575;
108
+ --scalar-color-3: #8e8e8e;
109
+ --scalar-color-accent: #f06292;
110
+
111
+ --scalar-background-1: #fff;
112
+ --scalar-background-2: #f6f6f6;
113
+ --scalar-background-3: #e7e7e7;
114
+
115
+ --scalar-border-color: rgba(0, 0, 0, 0.1);
116
+ }
117
+ .dark-mode {
118
+ --scalar-color-1: rgba(255, 255, 255, 0.9);
119
+ --scalar-color-2: rgba(156, 163, 175, 1);
120
+ --scalar-color-3: rgba(255, 255, 255, 0.44);
121
+ --scalar-color-accent: #f06292;
122
+
123
+ --scalar-background-1: #111728;
124
+ --scalar-background-2: #1e293b;
125
+ --scalar-background-3: #334155;
126
+ --scalar-background-accent: #f062921f;
127
+
128
+ --scalar-border-color: rgba(255, 255, 255, 0.1);
129
+ }
130
+
131
+ /* Document Sidebar */
132
+ .light-mode .t-doc__sidebar,
133
+ .dark-mode .t-doc__sidebar {
134
+ --scalar-sidebar-background-1: var(--scalar-background-1);
135
+ --scalar-sidebar-color-1: var(--scalar-color-1);
136
+ --scalar-sidebar-color-2: var(--scalar-color-2);
137
+ --scalar-sidebar-border-color: var(--scalar-border-color);
138
+
139
+ --scalar-sidebar-item-hover-background: var(--scalar-background-2);
140
+ --scalar-sidebar-item-hover-color: currentColor;
141
+
142
+ --scalar-sidebar-item-active-background: #f062921f;
143
+ --scalar-sidebar-color-active: var(--scalar-color-accent);
144
+
145
+ --scalar-sidebar-search-background: transparent;
146
+ --scalar-sidebar-search-color: var(--scalar-color-3);
147
+ --scalar-sidebar-search-border-color: var(--scalar-border-color);
148
+ }
149
+
150
+ /* advanced */
151
+ .light-mode {
152
+ --scalar-button-1: rgb(49 53 56);
153
+ --scalar-button-1-color: #fff;
154
+ --scalar-button-1-hover: rgb(28 31 33);
155
+
156
+ --scalar-color-green: #069061;
157
+ --scalar-color-red: #ef0006;
158
+ --scalar-color-yellow: #edbe20;
159
+ --scalar-color-blue: #0082d0;
160
+ --scalar-color-orange: #fb892c;
161
+ --scalar-color-purple: #5203d1;
162
+
163
+ --scalar-scrollbar-color: rgba(0, 0, 0, 0.18);
164
+ --scalar-scrollbar-color-active: rgba(0, 0, 0, 0.36);
165
+ }
166
+ .dark-mode {
167
+ --scalar-button-1: #f6f6f6;
168
+ --scalar-button-1-color: #000;
169
+ --scalar-button-1-hover: #e7e7e7;
170
+
171
+ --scalar-color-green: #a3ffa9;
172
+ --scalar-color-red: #ffa3a3;
173
+ --scalar-color-yellow: #fffca3;
174
+ --scalar-color-blue: #a5d6ff;
175
+ --scalar-color-orange: #e2ae83;
176
+ --scalar-color-purple: #d2a8ff;
177
+
178
+ --scalar-scrollbar-color: rgba(255, 255, 255, 0.24);
179
+ --scalar-scrollbar-color-active: rgba(255, 255, 255, 0.48);
180
+ }
181
+ .section-flare {
182
+ width: 100%;
183
+ height: 400px;
184
+ position: absolute;
185
+ }
186
+ .section-flare-item:first-of-type:before {
187
+ content: "";
188
+ position: absolute;
189
+ top: 0;
190
+ right: 0;
191
+ bottom: 0;
192
+ left: 0;
193
+ --stripes: repeating-linear-gradient(100deg, #fff 0%, #fff 0%, transparent 2%, transparent 12%, #fff 17%);
194
+ --stripesDark: repeating-linear-gradient(100deg, #000 0%, #000 0%, transparent 10%, transparent 12%, #000 17%);
195
+ --rainbow: repeating-linear-gradient(100deg, #60a5fa 10%, #e879f9 16%, #5eead4 22%, #60a5fa 30%);
196
+ contain: strict;
197
+ contain-intrinsic-size: 100vw 40vh;
198
+ background-image: var(--stripesDark), var(--rainbow);
199
+ background-size: 300%, 200%;
200
+ background-position: 50% 50%, 50% 50%;
201
+ filter: opacity(20%) saturate(200%);
202
+ -webkit-mask-image: radial-gradient(ellipse at 100% 0%, black 40%, transparent 70%);
203
+ mask-image: radial-gradient(ellipse at 100% 0%, black 40%, transparent 70%);
204
+ pointer-events: none;
205
+ }
206
+ .section-flare-item:first-of-type:after {
207
+ content: "";
208
+ position: absolute;
209
+ top: 0;
210
+ right: 0;
211
+ bottom: 0;
212
+ left: 0;
213
+ background-image: var(--stripes), var(--rainbow);
214
+ background-size: 200%, 100%;
215
+ background-attachment: fixed;
216
+ mix-blend-mode: difference;
217
+ background-image: var(--stripesDark), var(--rainbow);
218
+ pointer-events: none;
219
+ }
220
+ .light-mode .section-flare-item:first-of-type:after,
221
+ .light-mode .section-flare-item:first-of-type:before {
222
+ background-image: var(--stripes), var(--rainbow);
223
+ filter: opacity(4%) saturate(200%);
224
+ }`;
225
+ var ScalarRender = (info, config) => `<!doctype html>
226
+ <html>
227
+ <head>
228
+ <title>${info.title}</title>
229
+ <meta
230
+ name="description"
231
+ content="${info.description}"
232
+ />
233
+ <meta
234
+ name="og:description"
235
+ content="${info.description}"
236
+ />
237
+ <meta charset="utf-8" />
238
+ <meta
239
+ name="viewport"
240
+ content="width=device-width, initial-scale=1" />
241
+ <style>
242
+ body {
243
+ margin: 0;
244
+ }
245
+ </style>
246
+ <style>
247
+ ${config.customCss ?? elysiaCSS}
248
+ </style>
249
+ </head>
250
+ <body>
251
+ <script
252
+ id="api-reference"
253
+ data-url="${config.url}"
254
+ >
255
+ </script>
256
+ <script src="${config.cdn}" crossorigin></script>
257
+ </body>
258
+ </html>`;
259
+
260
+ // src/openapi.ts
261
+ import { t } from "elysia";
262
+ var capitalize = (word) => word.charAt(0).toUpperCase() + word.slice(1);
263
+ var toRef = (name) => t.Ref(`#/components/schemas/${name}`);
264
+ var toOperationId = (method, paths) => {
265
+ let operationId = method.toLowerCase();
266
+ if (!paths || paths === "/") return operationId + "Index";
267
+ for (const path of paths.split("/"))
268
+ operationId += path.includes(":") ? "By" + capitalize(path.replace(":", "")) : capitalize(path);
269
+ operationId = operationId.replace(/\?/g, "Optional");
270
+ return operationId;
271
+ };
272
+ var optionalParamsRegex = /(\/:\w+\?)/g;
273
+ var getPossiblePath = (path) => {
274
+ const optionalParams = path.match(optionalParamsRegex);
275
+ if (!optionalParams) return [path];
276
+ const originalPath = path.replace(/\?/g, "");
277
+ const paths = [originalPath];
278
+ for (let i = 0; i < optionalParams.length; i++) {
279
+ const newPath = path.replace(optionalParams[i], "");
280
+ paths.push(...getPossiblePath(newPath));
281
+ }
282
+ return paths;
283
+ };
284
+ function toOpenAPISchema(app, exclude, references) {
285
+ const {
286
+ methods: excludeMethods = ["OPTIONS"],
287
+ staticFile: excludeStaticFile = true,
288
+ tags: excludeTags
289
+ } = exclude ?? {};
290
+ const excludePaths = Array.isArray(exclude?.paths) ? exclude.paths : typeof exclude?.paths !== "undefined" ? [exclude.paths] : [];
291
+ const paths = /* @__PURE__ */ Object.create(null);
292
+ const routes = app.getGlobalRoutes();
293
+ if (references) {
294
+ if (!Array.isArray(references)) references = [references];
295
+ for (let i = 0; i < references.length; i++) {
296
+ const reference = references[i];
297
+ if (typeof reference === "function") references[i] = reference();
298
+ }
299
+ }
300
+ for (const route of routes) {
301
+ if (route.hooks?.detail?.hide) continue;
302
+ const method = route.method.toLowerCase();
303
+ if (excludeStaticFile && route.path.includes(".") || excludePaths.includes(route.path) || excludeMethods.includes(method))
304
+ continue;
305
+ const hooks = route.hooks ?? {};
306
+ if (references)
307
+ for (const reference of references) {
308
+ const refer = reference[route.path]?.[method];
309
+ if (!refer) continue;
310
+ if (!hooks.body && refer.body) hooks.body = refer.body;
311
+ if (!hooks.query && refer.query) hooks.query = refer.query;
312
+ if (!hooks.params && refer.params) hooks.params = refer.params;
313
+ if (!hooks.headers && refer.headers)
314
+ hooks.headers = refer.headers;
315
+ if (!hooks.response && refer.response) {
316
+ hooks.response = {};
317
+ for (const [status, schema] of Object.entries(
318
+ refer.response
319
+ ))
320
+ if (!hooks.response[status])
321
+ hooks.response[status] = schema;
322
+ }
323
+ }
324
+ if (excludeTags && hooks.detail.tags?.some((tag) => excludeTags?.includes(tag)))
325
+ continue;
326
+ const operation = {
327
+ ...hooks.detail
328
+ };
329
+ const parameters = [];
330
+ if (hooks.params) {
331
+ if (typeof hooks.params === "string")
332
+ hooks.params = toRef(hooks.params);
333
+ if (hooks.params.type === "object" && hooks.params.properties) {
334
+ for (const [paramName, paramSchema] of Object.entries(
335
+ hooks.params.properties
336
+ ))
337
+ parameters.push({
338
+ name: paramName,
339
+ in: "path",
340
+ required: true,
341
+ // Path parameters are always required
342
+ schema: paramSchema
343
+ });
344
+ }
345
+ }
346
+ if (hooks.query) {
347
+ if (typeof hooks.query === "string")
348
+ hooks.query = toRef(hooks.query);
349
+ if (hooks.query.type === "object" && hooks.query.properties) {
350
+ const required = hooks.query.required || [];
351
+ for (const [queryName, querySchema] of Object.entries(
352
+ hooks.query.properties
353
+ ))
354
+ parameters.push({
355
+ name: queryName,
356
+ in: "query",
357
+ required: required.includes(queryName),
358
+ schema: querySchema
359
+ });
360
+ }
361
+ }
362
+ if (hooks.headers) {
363
+ if (typeof hooks.headers === "string")
364
+ hooks.headers = toRef(hooks.headers);
365
+ if (hooks.headers.type === "object" && hooks.headers.properties) {
366
+ const required = hooks.headers.required || [];
367
+ for (const [headerName, headerSchema] of Object.entries(
368
+ hooks.headers.properties
369
+ ))
370
+ parameters.push({
371
+ name: headerName,
372
+ in: "header",
373
+ required: required.includes(headerName),
374
+ schema: headerSchema
375
+ });
376
+ }
377
+ }
378
+ if (hooks.cookie) {
379
+ if (typeof hooks.cookie === "string")
380
+ hooks.cookie = toRef(hooks.cookie);
381
+ if (hooks.cookie.type === "object" && hooks.cookie.properties) {
382
+ const required = hooks.cookie.required || [];
383
+ for (const [cookieName, cookieSchema] of Object.entries(
384
+ hooks.cookie.properties
385
+ ))
386
+ parameters.push({
387
+ name: cookieName,
388
+ in: "cookie",
389
+ required: required.includes(cookieName),
390
+ schema: cookieSchema
391
+ });
392
+ }
393
+ }
394
+ if (parameters.length > 0) operation.parameters = parameters;
395
+ if (hooks.body) {
396
+ if (typeof hooks.body === "string") hooks.body = toRef(hooks.body);
397
+ if (hooks.parse) {
398
+ const content = {};
399
+ const parsers = hooks.parse;
400
+ for (const parser of parsers) {
401
+ if (typeof parser.fn === "function") continue;
402
+ switch (parser.fn) {
403
+ case "text":
404
+ case "text/plain":
405
+ content["text/plain"] = { schema: hooks.body };
406
+ continue;
407
+ case "urlencoded":
408
+ case "application/x-www-form-urlencoded":
409
+ content["application/x-www-form-urlencoded"] = {
410
+ schema: hooks.body
411
+ };
412
+ continue;
413
+ case "json":
414
+ case "application/json":
415
+ content["application/json"] = { schema: hooks.body };
416
+ continue;
417
+ case "formdata":
418
+ case "multipart/form-data":
419
+ content["multipart/form-data"] = {
420
+ schema: hooks.body
421
+ };
422
+ continue;
423
+ }
424
+ }
425
+ operation.requestBody = { content, required: true };
426
+ } else {
427
+ operation.requestBody = {
428
+ content: {
429
+ "application/json": {
430
+ schema: hooks.body
431
+ },
432
+ "application/x-www-form-urlencoded": {
433
+ schema: hooks.body
434
+ },
435
+ "multipart/form-data": {
436
+ schema: hooks.body
437
+ }
438
+ },
439
+ required: true
440
+ };
441
+ }
442
+ }
443
+ if (hooks.response) {
444
+ operation.responses = {};
445
+ if (typeof hooks.response === "object" && !hooks.response.type && !hooks.response.$ref) {
446
+ for (let [status, schema] of Object.entries(hooks.response)) {
447
+ if (typeof schema === "string") schema = toRef(schema);
448
+ const { type, examples, $ref, ...options } = schema;
449
+ operation.responses[status] = {
450
+ description: `Response for status ${status}`,
451
+ ...options,
452
+ content: type === "void" || type === "null" || type === "undefined" ? schema : {
453
+ "application/json": {
454
+ schema
455
+ }
456
+ }
457
+ };
458
+ }
459
+ } else {
460
+ if (typeof hooks.response === "string")
461
+ hooks.response = toRef(hooks.response);
462
+ operation.responses["200"] = {
463
+ description: "Successful response",
464
+ content: {
465
+ "application/json": {
466
+ schema: hooks.response
467
+ }
468
+ }
469
+ };
470
+ }
471
+ }
472
+ for (let path of getPossiblePath(route.path)) {
473
+ const operationId = toOperationId(route.method, path);
474
+ path = path.replace(/:([^/]+)/g, "{$1}");
475
+ if (!paths[path]) paths[path] = {};
476
+ const current = paths[path];
477
+ if (method !== "all") {
478
+ current[method] = {
479
+ ...operation,
480
+ operationId
481
+ };
482
+ continue;
483
+ }
484
+ for (const method2 of [
485
+ "get",
486
+ "post",
487
+ "put",
488
+ "delete",
489
+ "patch",
490
+ "head",
491
+ "options",
492
+ "trace"
493
+ ])
494
+ current[method2] = {
495
+ ...operation,
496
+ operationId
497
+ };
498
+ }
499
+ }
500
+ const schemas = app.getGlobalDefinitions?.().type;
501
+ return {
502
+ components: {
503
+ schemas
504
+ },
505
+ paths
506
+ };
507
+ }
508
+ var withHeaders = (schema, headers) => Object.assign(schema, {
509
+ headers
510
+ });
511
+
512
+ // src/index.ts
513
+ var openapi = ({
514
+ enabled = true,
515
+ path = "/openapi",
516
+ provider = "scalar",
517
+ specPath = `${path}/json`,
518
+ documentation = {},
519
+ exclude,
520
+ swagger,
521
+ scalar,
522
+ references
523
+ } = {}) => {
524
+ if (!enabled) return new Elysia({ name: "@elysiajs/openapi" });
525
+ const info = {
526
+ title: "Elysia Documentation",
527
+ description: "Development documentation",
528
+ version: "0.0.0",
529
+ ...documentation.info
530
+ };
531
+ const relativePath = specPath.startsWith("/") ? specPath.slice(1) : specPath;
532
+ let totalRoutes = 0;
533
+ let cachedSchema;
534
+ const app = new Elysia({ name: "@elysiajs/openapi" }).use((app2) => {
535
+ if (provider === null) return app2;
536
+ return app2.get(
537
+ path,
538
+ new Response(
539
+ provider === "swagger-ui" ? SwaggerUIRender(info, {
540
+ url: relativePath,
541
+ dom_id: "#swagger-ui",
542
+ version: "latest",
543
+ autoDarkMode: true,
544
+ ...swagger
545
+ }) : ScalarRender(info, {
546
+ url: relativePath,
547
+ version: "latest",
548
+ cdn: `https://cdn.jsdelivr.net/npm/@scalar/api-reference@${scalar?.version ?? "latest"}/dist/browser/standalone.min.js`,
549
+ ...scalar,
550
+ _integration: "elysiajs"
551
+ }),
552
+ {
553
+ headers: {
554
+ "content-type": "text/html; charset=utf8"
555
+ }
556
+ }
557
+ ),
558
+ {
559
+ detail: {
560
+ hide: true
561
+ }
562
+ }
563
+ );
564
+ }).get(
565
+ specPath,
566
+ function openAPISchema() {
567
+ if (totalRoutes === app.routes.length) return cachedSchema;
568
+ totalRoutes = app.routes.length;
569
+ const {
570
+ paths,
571
+ components: { schemas }
572
+ } = toOpenAPISchema(app, exclude, references);
573
+ return cachedSchema = {
574
+ openapi: "3.0.3",
575
+ ...documentation,
576
+ tags: !exclude?.tags ? documentation.tags : documentation.tags?.filter(
577
+ (tag) => !exclude.tags?.includes(tag.name)
578
+ ),
579
+ info: {
580
+ title: "Elysia Documentation",
581
+ description: "Development documentation",
582
+ version: "0.0.0",
583
+ ...documentation.info
584
+ },
585
+ paths: {
586
+ ...paths,
587
+ ...documentation.paths
588
+ },
589
+ components: {
590
+ ...documentation.components,
591
+ schemas: {
592
+ ...schemas,
593
+ ...documentation.components?.schemas
594
+ }
595
+ }
596
+ };
597
+ },
598
+ {
599
+ detail: {
600
+ hide: true
601
+ }
602
+ }
603
+ );
604
+ return app;
605
+ };
606
+ var index_default = openapi;
607
+ export {
608
+ index_default as default,
609
+ openapi,
610
+ toOpenAPISchema,
611
+ withHeaders
612
+ };
@@ -0,0 +1,25 @@
1
+ import { type AnyElysia, type TSchema } from 'elysia';
2
+ import type { OpenAPIV3 } from 'openapi-types';
3
+ import type { TProperties } from '@sinclair/typebox';
4
+ import type { AdditionalReferences, ElysiaOpenAPIConfig } from './types';
5
+ export declare const capitalize: (word: string) => string;
6
+ /**
7
+ * Get all possible paths of a path with optional parameters
8
+ * @param {string} path
9
+ * @returns {string[]} paths
10
+ */
11
+ export declare const getPossiblePath: (path: string) => string[];
12
+ /**
13
+ * Converts Elysia routes to OpenAPI 3.0.3 paths schema
14
+ * @param routes Array of Elysia route objects
15
+ * @returns OpenAPI paths object
16
+ */
17
+ export declare function toOpenAPISchema(app: AnyElysia, exclude?: ElysiaOpenAPIConfig['exclude'], references?: AdditionalReferences): {
18
+ components: {
19
+ schemas: Record<string, TSchema>;
20
+ };
21
+ paths: OpenAPIV3.PathsObject<{}, {}>;
22
+ };
23
+ export declare const withHeaders: (schema: TSchema, headers: TProperties) => TSchema & {
24
+ headers: TProperties;
25
+ };