@pikku/cli 0.6.6

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.
Files changed (95) hide show
  1. package/CHANGELOG.md +445 -0
  2. package/README.md +3 -0
  3. package/bin/pikku-all.ts +110 -0
  4. package/bin/pikku-channels-map.ts +53 -0
  5. package/bin/pikku-channels.ts +57 -0
  6. package/bin/pikku-fetch.ts +54 -0
  7. package/bin/pikku-function-types.ts +76 -0
  8. package/bin/pikku-nextjs.test.ts +279 -0
  9. package/bin/pikku-nextjs.ts +113 -0
  10. package/bin/pikku-openapi.ts +71 -0
  11. package/bin/pikku-routes-map.ts +54 -0
  12. package/bin/pikku-routes.ts +55 -0
  13. package/bin/pikku-scheduler.ts +61 -0
  14. package/bin/pikku-schemas.ts +51 -0
  15. package/bin/pikku-websocket.ts +54 -0
  16. package/bin/pikku.ts +26 -0
  17. package/cli.schema.json +212 -0
  18. package/dist/bin/pikku-all.d.ts +4 -0
  19. package/dist/bin/pikku-all.js +71 -0
  20. package/dist/bin/pikku-channels-map.d.ts +5 -0
  21. package/dist/bin/pikku-channels-map.js +27 -0
  22. package/dist/bin/pikku-channels.d.ts +5 -0
  23. package/dist/bin/pikku-channels.js +32 -0
  24. package/dist/bin/pikku-fetch.d.ts +6 -0
  25. package/dist/bin/pikku-fetch.js +25 -0
  26. package/dist/bin/pikku-function-types.d.ts +6 -0
  27. package/dist/bin/pikku-function-types.js +32 -0
  28. package/dist/bin/pikku-nextjs.d.ts +7 -0
  29. package/dist/bin/pikku-nextjs.js +40 -0
  30. package/dist/bin/pikku-openapi.d.ts +5 -0
  31. package/dist/bin/pikku-openapi.js +41 -0
  32. package/dist/bin/pikku-routes-map.d.ts +5 -0
  33. package/dist/bin/pikku-routes-map.js +27 -0
  34. package/dist/bin/pikku-routes.d.ts +5 -0
  35. package/dist/bin/pikku-routes.js +33 -0
  36. package/dist/bin/pikku-scheduler.d.ts +5 -0
  37. package/dist/bin/pikku-scheduler.js +32 -0
  38. package/dist/bin/pikku-schemas.d.ts +5 -0
  39. package/dist/bin/pikku-schemas.js +27 -0
  40. package/dist/bin/pikku-websocket.d.ts +6 -0
  41. package/dist/bin/pikku-websocket.js +25 -0
  42. package/dist/bin/pikku.d.ts +2 -0
  43. package/dist/bin/pikku.js +23 -0
  44. package/dist/src/channels/serialize-channels.d.ts +3 -0
  45. package/dist/src/channels/serialize-channels.js +19 -0
  46. package/dist/src/channels/serialize-typed-channel-map.d.ts +3 -0
  47. package/dist/src/channels/serialize-typed-channel-map.js +87 -0
  48. package/dist/src/channels/serialize-websocket-wrapper.d.ts +1 -0
  49. package/dist/src/channels/serialize-websocket-wrapper.js +61 -0
  50. package/dist/src/core/serialize-import-map.d.ts +2 -0
  51. package/dist/src/core/serialize-import-map.js +23 -0
  52. package/dist/src/core/serialize-pikku-types.d.ts +4 -0
  53. package/dist/src/core/serialize-pikku-types.js +48 -0
  54. package/dist/src/http/serialize-fetch-wrapper.d.ts +1 -0
  55. package/dist/src/http/serialize-fetch-wrapper.js +57 -0
  56. package/dist/src/http/serialize-route-imports.d.ts +1 -0
  57. package/dist/src/http/serialize-route-imports.js +13 -0
  58. package/dist/src/http/serialize-route-meta.d.ts +2 -0
  59. package/dist/src/http/serialize-route-meta.js +6 -0
  60. package/dist/src/http/serialize-typed-route-map.d.ts +3 -0
  61. package/dist/src/http/serialize-typed-route-map.js +107 -0
  62. package/dist/src/inspector-glob.d.ts +2 -0
  63. package/dist/src/inspector-glob.js +12 -0
  64. package/dist/src/nextjs/serialize-nextjs-wrapper.d.ts +1 -0
  65. package/dist/src/nextjs/serialize-nextjs-wrapper.js +149 -0
  66. package/dist/src/openapi/openapi-spec-generator.d.ts +79 -0
  67. package/dist/src/openapi/openapi-spec-generator.js +135 -0
  68. package/dist/src/pikku-cli-config.d.ts +31 -0
  69. package/dist/src/pikku-cli-config.js +113 -0
  70. package/dist/src/scheduler/serialize-schedulers.d.ts +3 -0
  71. package/dist/src/scheduler/serialize-schedulers.js +22 -0
  72. package/dist/src/schema/schema-generator.d.ts +5 -0
  73. package/dist/src/schema/schema-generator.js +79 -0
  74. package/dist/src/utils.d.ts +34 -0
  75. package/dist/src/utils.js +109 -0
  76. package/dist/tsconfig.tsbuildinfo +1 -0
  77. package/package.json +42 -0
  78. package/run-tests.sh +53 -0
  79. package/src/channels/serialize-channels.ts +34 -0
  80. package/src/channels/serialize-typed-channel-map.ts +133 -0
  81. package/src/channels/serialize-websocket-wrapper.ts +61 -0
  82. package/src/core/serialize-import-map.ts +33 -0
  83. package/src/core/serialize-pikku-types.ts +53 -0
  84. package/src/http/serialize-fetch-wrapper.ts +57 -0
  85. package/src/http/serialize-route-imports.ts +24 -0
  86. package/src/http/serialize-route-meta.ts +10 -0
  87. package/src/http/serialize-typed-route-map.ts +147 -0
  88. package/src/inspector-glob.ts +27 -0
  89. package/src/nextjs/serialize-nextjs-wrapper.ts +156 -0
  90. package/src/openapi/openapi-spec-generator.ts +229 -0
  91. package/src/pikku-cli-config.ts +189 -0
  92. package/src/scheduler/serialize-schedulers.ts +43 -0
  93. package/src/schema/schema-generator.ts +117 -0
  94. package/src/utils.ts +219 -0
  95. package/tsconfig.json +21 -0
@@ -0,0 +1,57 @@
1
+ export const serializeFetchWrapper = (routesMapPath: string) => {
2
+ return `
3
+ import { CorePikkuFetch, HTTPMethod } from '@pikku/fetch'
4
+ import type { RoutesMap, RouteHandlerOf, RoutesWithMethod } from '${routesMapPath}'
5
+
6
+ export class PikkuFetch extends AbstractPikkuFetch {
7
+ public async post<Route extends RoutesWithMethod<'POST'>>(
8
+ route: Route,
9
+ data: RouteHandlerOf<Route, 'POST'>['input'] = null,
10
+ options?: Omit<RequestInit, 'body'>
11
+ ): Promise<RouteHandlerOf<Route, 'POST'>['output']> {
12
+ return super.api(route, 'POST', data, options);
13
+ }
14
+
15
+ public async get<Route extends RoutesWithMethod<'GET'>>(
16
+ route: Route,
17
+ data: RouteHandlerOf<Route, 'GET'>['input'] = null,
18
+ options?: Omit<RequestInit, 'body'>
19
+ ): Promise<RouteHandlerOf<Route, 'GET'>['output']> {
20
+ return super.api(route, 'GET', data, options);
21
+ }
22
+
23
+ public async patch<Route extends RoutesWithMethod<'PATCH'>>(
24
+ route: Route,
25
+ data: RouteHandlerOf<Route, 'PATCH'>['input'] = null,
26
+ options?: Omit<RequestInit, 'body'>
27
+ ): Promise<RouteHandlerOf<Route, 'PATCH'>['output']> {
28
+ return super.api(route, 'PATCH', data, options);
29
+ }
30
+
31
+ public async head<Route extends RoutesWithMethod<'HEAD'>>(
32
+ route: Route,
33
+ data: RouteHandlerOf<Route, 'HEAD'>['input'] = null,
34
+ options?: Omit<RequestInit, 'body'>
35
+ ): Promise<RouteHandlerOf<Route, 'HEAD'>['output']> {
36
+ return super.api(route, 'HEAD', data, options);
37
+ }
38
+
39
+ public async delete<Route extends RoutesWithMethod<'DELETE'>>(
40
+ route: Route,
41
+ data: RouteHandlerOf<Route, 'DELETE'>['input'] = null,
42
+ options?: Omit<RequestInit, 'body'>
43
+ ): Promise<RouteHandlerOf<Route, 'DELETE'>['output']> {
44
+ return super.api(route, 'DELETE', data, options);
45
+ }
46
+
47
+ public async fetch<
48
+ Route extends keyof RoutesMap,
49
+ Method extends keyof RoutesMap[Route]
50
+ >(route: Route, method: Method, data: RouteHandlerOf<Route, Method>['input'], options?: Omit<RequestInit, 'body'>): Promise<Response> {
51
+ return await super.fetch(route, method as HTTPMethod, data, options)
52
+ }
53
+ }
54
+
55
+ export const pikkuFetch = new PikkuFetch()
56
+ `
57
+ }
@@ -0,0 +1,24 @@
1
+ import { getFileImportRelativePath } from '../utils.js'
2
+
3
+ export const serializeRoutes = (
4
+ outputPath: string,
5
+ filesWithRoutes: Set<string>,
6
+ packageMappings: Record<string, string> = {}
7
+ ) => {
8
+ const serializedOutput: string[] = [
9
+ '/* The files with an addRoute function call */',
10
+ ]
11
+
12
+ Array.from(filesWithRoutes)
13
+ .sort()
14
+ .forEach((path) => {
15
+ const filePath = getFileImportRelativePath(
16
+ outputPath,
17
+ path,
18
+ packageMappings
19
+ )
20
+ serializedOutput.push(`import '${filePath}'`)
21
+ })
22
+
23
+ return serializedOutput.join('\n')
24
+ }
@@ -0,0 +1,10 @@
1
+ import type { HTTPRoutesMeta } from '@pikku/core/http'
2
+
3
+ export const serializeHTTPRoutesMeta = (routesMeta: HTTPRoutesMeta) => {
4
+ const serializedOutput: string[] = []
5
+ serializedOutput.push("import { setHTTPRoutesMeta } from '@pikku/core/http'")
6
+ serializedOutput.push(
7
+ `setHTTPRoutesMeta(${JSON.stringify(routesMeta, null, 2)})`
8
+ )
9
+ return serializedOutput.join('\n')
10
+ }
@@ -0,0 +1,147 @@
1
+ import { HTTPRoutesMeta } from '@pikku/core/http'
2
+ import { serializeImportMap } from '../core/serialize-import-map.js'
3
+ import { MetaInputTypes, TypesMap } from '@pikku/inspector'
4
+
5
+ export const serializeTypedRoutesMap = (
6
+ relativeToPath: string,
7
+ packageMappings: Record<string, string>,
8
+ typesMap: TypesMap,
9
+ routesMeta: HTTPRoutesMeta,
10
+ metaTypes: MetaInputTypes
11
+ ) => {
12
+ const requiredTypes = new Set<string>()
13
+ const serializedCustomTypes = generateCustomTypes(typesMap, requiredTypes)
14
+ const serializedMetaTypes = generateMetaTypes(metaTypes, typesMap)
15
+ const serializedImportMap = serializeImportMap(
16
+ relativeToPath,
17
+ packageMappings,
18
+ typesMap,
19
+ requiredTypes
20
+ )
21
+ const serializedRoutes = generateRoutes(routesMeta, typesMap, requiredTypes)
22
+
23
+ return `/**
24
+ * This provides the structure needed for typescript to be aware of routes and their return types
25
+ */
26
+
27
+ ${serializedImportMap}
28
+ ${serializedCustomTypes}
29
+ ${serializedMetaTypes}
30
+
31
+ interface RouteHandler<I, O> {
32
+ input: I;
33
+ output: O;
34
+ }
35
+
36
+ ${serializedRoutes}
37
+
38
+ export type RouteHandlerOf<Route extends keyof RoutesMap, Method extends keyof RoutesMap[Route]> =
39
+ RoutesMap[Route][Method] extends { input: infer I; output: infer O }
40
+ ? RouteHandler<I, O>
41
+ : never;
42
+
43
+ export type RoutesWithMethod<Method extends string> = {
44
+ [Route in keyof RoutesMap]: Method extends keyof RoutesMap[Route] ? Route : never;
45
+ }[keyof RoutesMap];
46
+ `
47
+ }
48
+
49
+ function generateCustomTypes(typesMap: TypesMap, requiredTypes: Set<string>) {
50
+ return `
51
+ // Custom types are those that are defined directly within generics
52
+ // or are broken into simpler types
53
+ ${Array.from(typesMap.customTypes.entries())
54
+ .map(([name, { type, references }]) => {
55
+ references.forEach((name) => {
56
+ const originalName = typesMap.getTypeMeta(name).originalName
57
+ requiredTypes.add(originalName)
58
+ })
59
+ return `export type ${name} = ${type}`
60
+ })
61
+ .join('\n')}`
62
+ }
63
+
64
+ function generateRoutes(
65
+ routesMeta: HTTPRoutesMeta,
66
+ typesMap: TypesMap,
67
+ requiredTypes: Set<string>
68
+ ) {
69
+ // Initialize an object to collect routes
70
+ const routesObj: Record<
71
+ string,
72
+ Record<string, { inputType: string; outputType: string }>
73
+ > = {}
74
+
75
+ for (const meta of routesMeta) {
76
+ const { route, method, input, output } = meta
77
+
78
+ // Initialize the route entry if it doesn't exist
79
+ if (!routesObj[route]) {
80
+ routesObj[route] = {}
81
+ }
82
+
83
+ // Store the input and output types separately for RouteHandler
84
+ const inputType = input ? typesMap.getTypeMeta(input).uniqueName : 'null'
85
+ const outputType = output ? typesMap.getTypeMeta(output).uniqueName : 'null'
86
+
87
+ requiredTypes.add(inputType)
88
+ requiredTypes.add(outputType)
89
+
90
+ // Add method entry
91
+ routesObj[route][method] = {
92
+ inputType,
93
+ outputType,
94
+ }
95
+ }
96
+
97
+ // Build the routes object as a string
98
+ let routesStr = 'export type RoutesMap = {\n'
99
+
100
+ for (const [routePath, methods] of Object.entries(routesObj)) {
101
+ routesStr += ` readonly '${routePath}': {\n`
102
+ for (const [method, handler] of Object.entries(methods)) {
103
+ routesStr += ` readonly ${method.toUpperCase()}: RouteHandler<${handler.inputType}, ${handler.outputType}>,\n`
104
+ }
105
+ routesStr += ' },\n'
106
+ }
107
+
108
+ routesStr += '};'
109
+
110
+ return routesStr
111
+ }
112
+
113
+ const generateMetaTypes = (metaTypes: MetaInputTypes, typesMap: TypesMap) => {
114
+ const nameToTypeMap = Array.from(metaTypes.entries()).reduce<
115
+ Map<string, string>
116
+ >((result, [_name, { query, body, params }]) => {
117
+ const { uniqueName } = typesMap.getTypeMeta(_name)
118
+ const queryType =
119
+ query && query.length > 0
120
+ ? `Pick<${uniqueName}, '${query?.join("' | '")}'>`
121
+ : undefined
122
+ if (queryType) {
123
+ result.set(`${uniqueName}Query`, queryType)
124
+ }
125
+ const paramsType =
126
+ params && params.length > 0
127
+ ? `Pick<${uniqueName}, '${params.join("' | '")}'>`
128
+ : undefined
129
+ if (paramsType) {
130
+ result.set(`${uniqueName}Params`, paramsType)
131
+ }
132
+ const bodyType =
133
+ (body && body.length > 0) || (params && params.length > 0)
134
+ ? `Omit<${uniqueName}, '${[...new Set([...(query || []), ...(params || [])])].join("' | '")}'>`
135
+ : uniqueName!
136
+ if (bodyType) {
137
+ result.set(`${uniqueName}Body`, bodyType)
138
+ }
139
+ return result
140
+ }, new Map())
141
+
142
+ return `
143
+ // The '& {}' is a workaround for not directly refering to a type since it confuses typescript
144
+ ${Array.from(nameToTypeMap.entries())
145
+ .map(([name, type]) => `export type ${name} = ${type} & {}`)
146
+ .join('\n')}`
147
+ }
@@ -0,0 +1,27 @@
1
+ import * as path from 'path'
2
+ import { glob } from 'glob'
3
+ import { InspectorState, inspect } from '@pikku/inspector'
4
+ import { logCommandInfoAndTime } from './utils.js'
5
+
6
+ export const inspectorGlob = async (
7
+ rootDir: string,
8
+ routeDirectories: string[]
9
+ ) => {
10
+ let result: InspectorState
11
+ await logCommandInfoAndTime(
12
+ 'Inspecting codebase',
13
+ 'Inspected codebase',
14
+ [false],
15
+ async () => {
16
+ const routeFiles = (
17
+ await Promise.all(
18
+ routeDirectories.map((dir) =>
19
+ glob(`${path.join(rootDir, dir)}/**/*.ts`)
20
+ )
21
+ )
22
+ ).flat()
23
+ result = await inspect(routeFiles)
24
+ }
25
+ )
26
+ return result!
27
+ }
@@ -0,0 +1,156 @@
1
+ export const serializeNextJsWrapper = (
2
+ routesPath: string,
3
+ routesMapPath: string,
4
+ schemasPath: string,
5
+ configImport: string,
6
+ singleServicesFactoryImport: string,
7
+ sessionServicesImport: string
8
+ ) => {
9
+ return `'server-only'
10
+
11
+ /**
12
+ * This file provides a wrapper around the PikkuNextJS class to allow for methods to be type checked against your routes.
13
+ * This ensures type safety for route handling methods when integrating with the \`@pikku/core\` framework.
14
+ */
15
+ import { PikkuNextJS } from '@pikku/next'
16
+ import type { RoutesMap, RouteHandlerOf, RoutesWithMethod } from '${routesMapPath}'
17
+
18
+ ${configImport}
19
+ ${singleServicesFactoryImport}
20
+ ${sessionServicesImport}
21
+
22
+ import '${routesPath}'
23
+ import '${schemasPath}'
24
+
25
+ let _pikku: PikkuNextJS | undefined
26
+
27
+ /**
28
+ * Initializes and returns an instance of PikkuNextJS with helper methods for handling route requests.
29
+ *
30
+ * @returns An object containing methods for making dynamic and static action requests.
31
+ */
32
+ export const pikku = () => {
33
+ if (!_pikku) {
34
+ _pikku = new PikkuNextJS(
35
+ createConfig as any,
36
+ createSingletonServices as any,
37
+ createSessionServices
38
+ )
39
+ }
40
+
41
+ /**
42
+ * Makes a dynamic action request for a specified route and method.
43
+ * Dynamic requests may access headers and cookies, making them unsuitable for precompile stages.
44
+ *
45
+ * @template Route - The route key from the RoutesMap.
46
+ * @template Method - The method key from the specified route.
47
+ * @param route - The route to make the request to.
48
+ * @param method - The method to be used for the request.
49
+ * @param data - The input data for the request, defaults to null.
50
+ * @returns A promise that resolves to the output of the route handler.
51
+ */
52
+ const dynamicActionRequest = async <
53
+ Route extends keyof RoutesMap,
54
+ Method extends keyof RoutesMap[Route]
55
+ >(
56
+ route: Route,
57
+ method: Method,
58
+ data: RouteHandlerOf<Route, Method>['input'] = null
59
+ ): Promise<RouteHandlerOf<Route, Method>['output']> => {
60
+ return _pikku!.actionRequest(route, method, data as any)
61
+ }
62
+
63
+ /**
64
+ * Makes a static action request for a specified route and method.
65
+ * Static requests do not depend on headers or cookies and are suitable for precompile stages.
66
+ *
67
+ * @template Route - The route key from the RoutesMap.
68
+ * @template Method - The method key from the specified route.
69
+ * @param route - The route to make the request to.
70
+ * @param method - The method to be used for the request.
71
+ * @param data - The input data for the request, defaults to null.
72
+ * @returns A promise that resolves to the output of the route handler.
73
+ */
74
+ const staticActionRequest = async <
75
+ Route extends keyof RoutesMap,
76
+ Method extends keyof RoutesMap[Route]
77
+ >(
78
+ route: Route,
79
+ method: Method,
80
+ data: RouteHandlerOf<Route, Method>['input'] = null
81
+ ): Promise<RouteHandlerOf<Route, Method>['output']> => {
82
+ return _pikku!.staticActionRequest(route, method, data as any)
83
+ }
84
+
85
+ /**
86
+ * Makes a dynamic POST request for a specified route.
87
+ */
88
+ const dynamicPost = <Route extends RoutesWithMethod<'POST'>>(
89
+ route: Route,
90
+ data: RouteHandlerOf<Route, 'POST'>['input'] = null
91
+ ): Promise<RouteHandlerOf<Route, 'POST'>['output']> => {
92
+ return dynamicActionRequest(route, 'POST', data)
93
+ }
94
+
95
+ /**
96
+ * Makes a dynamic GET request for a specified route.
97
+ */
98
+ const dynamicGet = <Route extends RoutesWithMethod<'GET'>>(
99
+ route: Route,
100
+ data: RouteHandlerOf<Route, 'GET'>['input'] = null
101
+ ): Promise<RouteHandlerOf<Route, 'GET'>['output']> => {
102
+ return dynamicActionRequest(route, 'GET', data)
103
+ }
104
+
105
+ /**
106
+ * Makes a dynamic PATCH request for a specified route.
107
+ */
108
+ const dynamicPatch = <Route extends RoutesWithMethod<'PATCH'>>(
109
+ route: Route,
110
+ data: RouteHandlerOf<Route, 'PATCH'>['input'] = null
111
+ ): Promise<RouteHandlerOf<Route, 'PATCH'>['output']> => {
112
+ return dynamicActionRequest(route, 'PATCH', data)
113
+ }
114
+
115
+ /**
116
+ * Makes a dynamic DELETE request for a specified route.
117
+ */
118
+ const dynamicDel = <Route extends RoutesWithMethod<'DELETE'>>(
119
+ route: Route,
120
+ data: RouteHandlerOf<Route, 'DELETE'>['input'] = null
121
+ ): Promise<RouteHandlerOf<Route, 'DELETE'>['output']> => {
122
+ return dynamicActionRequest(route, 'DELETE', data)
123
+ }
124
+
125
+ // Static
126
+
127
+ /**
128
+ * Makes a static POST request for a specified route.
129
+ */
130
+ const staticPost = <Route extends RoutesWithMethod<'POST'>>(
131
+ route: Route,
132
+ data: RouteHandlerOf<Route, 'POST'>['input'] = null
133
+ ): Promise<RouteHandlerOf<Route, 'POST'>['output']> => {
134
+ return staticActionRequest(route, 'POST', data)
135
+ }
136
+
137
+ /**
138
+ * Makes a static GET request for a specified route.
139
+ */
140
+ const staticGet = <Route extends RoutesWithMethod<'GET'>>(
141
+ route: Route,
142
+ data: RouteHandlerOf<Route, 'GET'>['input'] = null
143
+ ): Promise<RouteHandlerOf<Route, 'GET'>['output']> => {
144
+ return staticActionRequest(route, 'GET', data)
145
+ }
146
+
147
+ return {
148
+ get: dynamicGet,
149
+ post: dynamicPost,
150
+ patch: dynamicPatch,
151
+ del: dynamicDel,
152
+ staticGet,
153
+ staticPost
154
+ }
155
+ }`
156
+ }
@@ -0,0 +1,229 @@
1
+ import { getErrors } from '@pikku/core/errors'
2
+ import { HTTPRoutesMeta } from '@pikku/core/http'
3
+ import _convertSchema from '@openapi-contrib/json-schema-to-openapi-schema'
4
+ const convertSchema =
5
+ 'default' in _convertSchema ? (_convertSchema.default as any) : _convertSchema
6
+
7
+ interface OpenAPISpec {
8
+ openapi: string
9
+ info: {
10
+ title: string
11
+ version: string
12
+ description?: string
13
+ termsOfService?: string
14
+ contact?: {
15
+ name?: string
16
+ url?: string
17
+ email?: string
18
+ }
19
+ license?: {
20
+ name: string
21
+ url?: string
22
+ }
23
+ }
24
+ servers: { url: string; description?: string }[]
25
+ paths: Record<string, any>
26
+ components: {
27
+ schemas: Record<string, any>
28
+ responses?: Record<string, any>
29
+ parameters?: Record<string, any>
30
+ examples?: Record<string, any>
31
+ requestBodies?: Record<string, any>
32
+ headers?: Record<string, any>
33
+ securitySchemes?: Record<string, any>
34
+ }
35
+ security?: { [key: string]: any[] }[]
36
+ tags?: { name: string; description?: string }[]
37
+ externalDocs?: {
38
+ description?: string
39
+ url: string
40
+ }
41
+ }
42
+
43
+ export interface OpenAPISpecInfo {
44
+ info: {
45
+ title: string
46
+ version: string
47
+ description: string
48
+ termsOfService?: string
49
+ contact?: {
50
+ name?: string
51
+ url?: string
52
+ email?: string
53
+ }
54
+ license?: {
55
+ name: string
56
+ url?: string
57
+ }
58
+ }
59
+ servers: { url: string; description?: string }[]
60
+ tags?: { name: string; description?: string }[]
61
+ externalDocs?: {
62
+ description?: string
63
+ url: string
64
+ }
65
+ securitySchemes?: Record<string, any>
66
+ security?: { [key: string]: any[] }[]
67
+ }
68
+
69
+ const getErrorResponseForConstructorName = (constructorName: string) => {
70
+ const foundError = Array.from(getErrors().entries()).find(
71
+ ([e]) => e.name === constructorName
72
+ )
73
+ if (foundError) {
74
+ return foundError[1]
75
+ }
76
+ return undefined
77
+ }
78
+
79
+ const convertSchemasToBodyPayloads = async (
80
+ routesMeta: HTTPRoutesMeta,
81
+ schemas: Record<string, any>
82
+ ) => {
83
+ const requiredSchemas = new Set(
84
+ routesMeta
85
+ .map(({ inputTypes, output }) => [inputTypes?.body, output])
86
+ .flat()
87
+ .filter((schema) => !!schema)
88
+ )
89
+ const convertedEntries = await Promise.all(
90
+ Object.entries(schemas).map(async ([key, schema]) => {
91
+ if (requiredSchemas.has(key)) {
92
+ const convertedSchema = await convertSchema(schema, {
93
+ convertUnreferencedDefinitions: false,
94
+ dereference: { circular: 'ignore' },
95
+ })
96
+ return [key, convertedSchema]
97
+ }
98
+ return
99
+ })
100
+ )
101
+ return Object.fromEntries(convertedEntries.filter((s) => !!s))
102
+ }
103
+
104
+ export async function generateOpenAPISpec(
105
+ routeMeta: HTTPRoutesMeta,
106
+ schemas: Record<string, any>,
107
+ additionalInfo: OpenAPISpecInfo
108
+ ): Promise<OpenAPISpec> {
109
+ const paths: Record<string, any> = {}
110
+
111
+ routeMeta.forEach((meta) => {
112
+ const { route, method, inputTypes, output, params, query, docs } = meta
113
+ const path = route.replace(/:(\w+)/g, '{$1}') // Convert ":param" to "{param}"
114
+
115
+ if (!paths[path]) {
116
+ paths[path] = {}
117
+ }
118
+
119
+ const responses = {}
120
+ docs?.errors?.forEach((error) => {
121
+ const errorResponse = getErrorResponseForConstructorName(error)
122
+ if (errorResponse) {
123
+ responses[errorResponse.status] = {
124
+ description: errorResponse.message,
125
+ }
126
+ }
127
+ })
128
+
129
+ const operation: any = {
130
+ description:
131
+ docs?.description ||
132
+ `This endpoint handles the ${method.toUpperCase()} request for the route ${route}.`,
133
+ tags: docs?.tags || [route.split('/')[1] || 'default'],
134
+ parameters: [],
135
+ responses: {
136
+ ...responses,
137
+ '200': {
138
+ description: 'Successful response',
139
+ content: output
140
+ ? {
141
+ 'application/json': {
142
+ schema:
143
+ typeof output === 'string' &&
144
+ ['boolean', 'string', 'number'].includes(output)
145
+ ? { type: output }
146
+ : { $ref: `#/components/schemas/${output}` },
147
+ },
148
+ }
149
+ : undefined,
150
+ },
151
+ },
152
+ }
153
+
154
+ const bodyType = inputTypes?.body
155
+ if (bodyType) {
156
+ operation.requestBody = {
157
+ required: true,
158
+ content: {
159
+ 'application/json': {
160
+ schema:
161
+ typeof bodyType === 'string' &&
162
+ ['boolean', 'string', 'number'].includes(bodyType)
163
+ ? { type: bodyType }
164
+ : { $ref: `#/components/schemas/${bodyType}` },
165
+ },
166
+ },
167
+ }
168
+ }
169
+
170
+ if (params) {
171
+ operation.parameters = params.map((param) => ({
172
+ name: param,
173
+ in: 'path',
174
+ required: true,
175
+ schema: { type: 'string' },
176
+ }))
177
+ }
178
+
179
+ if (query) {
180
+ operation.parameters.push(
181
+ ...query.map((query) => ({
182
+ name: query,
183
+ in: 'query',
184
+ required: false,
185
+ schema: { type: 'string' },
186
+ }))
187
+ )
188
+ }
189
+
190
+ paths[path][method] = operation
191
+ })
192
+
193
+ return {
194
+ openapi: '3.1.0',
195
+ info: additionalInfo.info,
196
+ servers: additionalInfo.servers,
197
+ paths,
198
+ components: {
199
+ schemas: await convertSchemasToBodyPayloads(routeMeta, schemas),
200
+ responses: {},
201
+ parameters: {},
202
+ examples: {},
203
+ requestBodies: {},
204
+ headers: {},
205
+ securitySchemes: additionalInfo.securitySchemes || {
206
+ ApiKeyAuth: {
207
+ type: 'apiKey',
208
+ in: 'header',
209
+ name: 'x-api-key',
210
+ },
211
+ BearerAuth: {
212
+ type: 'http',
213
+ scheme: 'bearer',
214
+ },
215
+ },
216
+ },
217
+ security: additionalInfo.security || [
218
+ {
219
+ ApiKeyAuth: [],
220
+ },
221
+ {
222
+ BearerAuth: [],
223
+ },
224
+ ],
225
+ tags: additionalInfo.tags,
226
+ externalDocs: additionalInfo.externalDocs,
227
+ // definitions
228
+ }
229
+ }