@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.
- package/CHANGELOG.md +445 -0
- package/README.md +3 -0
- package/bin/pikku-all.ts +110 -0
- package/bin/pikku-channels-map.ts +53 -0
- package/bin/pikku-channels.ts +57 -0
- package/bin/pikku-fetch.ts +54 -0
- package/bin/pikku-function-types.ts +76 -0
- package/bin/pikku-nextjs.test.ts +279 -0
- package/bin/pikku-nextjs.ts +113 -0
- package/bin/pikku-openapi.ts +71 -0
- package/bin/pikku-routes-map.ts +54 -0
- package/bin/pikku-routes.ts +55 -0
- package/bin/pikku-scheduler.ts +61 -0
- package/bin/pikku-schemas.ts +51 -0
- package/bin/pikku-websocket.ts +54 -0
- package/bin/pikku.ts +26 -0
- package/cli.schema.json +212 -0
- package/dist/bin/pikku-all.d.ts +4 -0
- package/dist/bin/pikku-all.js +71 -0
- package/dist/bin/pikku-channels-map.d.ts +5 -0
- package/dist/bin/pikku-channels-map.js +27 -0
- package/dist/bin/pikku-channels.d.ts +5 -0
- package/dist/bin/pikku-channels.js +32 -0
- package/dist/bin/pikku-fetch.d.ts +6 -0
- package/dist/bin/pikku-fetch.js +25 -0
- package/dist/bin/pikku-function-types.d.ts +6 -0
- package/dist/bin/pikku-function-types.js +32 -0
- package/dist/bin/pikku-nextjs.d.ts +7 -0
- package/dist/bin/pikku-nextjs.js +40 -0
- package/dist/bin/pikku-openapi.d.ts +5 -0
- package/dist/bin/pikku-openapi.js +41 -0
- package/dist/bin/pikku-routes-map.d.ts +5 -0
- package/dist/bin/pikku-routes-map.js +27 -0
- package/dist/bin/pikku-routes.d.ts +5 -0
- package/dist/bin/pikku-routes.js +33 -0
- package/dist/bin/pikku-scheduler.d.ts +5 -0
- package/dist/bin/pikku-scheduler.js +32 -0
- package/dist/bin/pikku-schemas.d.ts +5 -0
- package/dist/bin/pikku-schemas.js +27 -0
- package/dist/bin/pikku-websocket.d.ts +6 -0
- package/dist/bin/pikku-websocket.js +25 -0
- package/dist/bin/pikku.d.ts +2 -0
- package/dist/bin/pikku.js +23 -0
- package/dist/src/channels/serialize-channels.d.ts +3 -0
- package/dist/src/channels/serialize-channels.js +19 -0
- package/dist/src/channels/serialize-typed-channel-map.d.ts +3 -0
- package/dist/src/channels/serialize-typed-channel-map.js +87 -0
- package/dist/src/channels/serialize-websocket-wrapper.d.ts +1 -0
- package/dist/src/channels/serialize-websocket-wrapper.js +61 -0
- package/dist/src/core/serialize-import-map.d.ts +2 -0
- package/dist/src/core/serialize-import-map.js +23 -0
- package/dist/src/core/serialize-pikku-types.d.ts +4 -0
- package/dist/src/core/serialize-pikku-types.js +48 -0
- package/dist/src/http/serialize-fetch-wrapper.d.ts +1 -0
- package/dist/src/http/serialize-fetch-wrapper.js +57 -0
- package/dist/src/http/serialize-route-imports.d.ts +1 -0
- package/dist/src/http/serialize-route-imports.js +13 -0
- package/dist/src/http/serialize-route-meta.d.ts +2 -0
- package/dist/src/http/serialize-route-meta.js +6 -0
- package/dist/src/http/serialize-typed-route-map.d.ts +3 -0
- package/dist/src/http/serialize-typed-route-map.js +107 -0
- package/dist/src/inspector-glob.d.ts +2 -0
- package/dist/src/inspector-glob.js +12 -0
- package/dist/src/nextjs/serialize-nextjs-wrapper.d.ts +1 -0
- package/dist/src/nextjs/serialize-nextjs-wrapper.js +149 -0
- package/dist/src/openapi/openapi-spec-generator.d.ts +79 -0
- package/dist/src/openapi/openapi-spec-generator.js +135 -0
- package/dist/src/pikku-cli-config.d.ts +31 -0
- package/dist/src/pikku-cli-config.js +113 -0
- package/dist/src/scheduler/serialize-schedulers.d.ts +3 -0
- package/dist/src/scheduler/serialize-schedulers.js +22 -0
- package/dist/src/schema/schema-generator.d.ts +5 -0
- package/dist/src/schema/schema-generator.js +79 -0
- package/dist/src/utils.d.ts +34 -0
- package/dist/src/utils.js +109 -0
- package/dist/tsconfig.tsbuildinfo +1 -0
- package/package.json +42 -0
- package/run-tests.sh +53 -0
- package/src/channels/serialize-channels.ts +34 -0
- package/src/channels/serialize-typed-channel-map.ts +133 -0
- package/src/channels/serialize-websocket-wrapper.ts +61 -0
- package/src/core/serialize-import-map.ts +33 -0
- package/src/core/serialize-pikku-types.ts +53 -0
- package/src/http/serialize-fetch-wrapper.ts +57 -0
- package/src/http/serialize-route-imports.ts +24 -0
- package/src/http/serialize-route-meta.ts +10 -0
- package/src/http/serialize-typed-route-map.ts +147 -0
- package/src/inspector-glob.ts +27 -0
- package/src/nextjs/serialize-nextjs-wrapper.ts +156 -0
- package/src/openapi/openapi-spec-generator.ts +229 -0
- package/src/pikku-cli-config.ts +189 -0
- package/src/scheduler/serialize-schedulers.ts +43 -0
- package/src/schema/schema-generator.ts +117 -0
- package/src/utils.ts +219 -0
- package/tsconfig.json +21 -0
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { getFileImportRelativePath } from '../utils.js';
|
|
2
|
+
export const serializeRoutes = (outputPath, filesWithRoutes, packageMappings = {}) => {
|
|
3
|
+
const serializedOutput = [
|
|
4
|
+
'/* The files with an addRoute function call */',
|
|
5
|
+
];
|
|
6
|
+
Array.from(filesWithRoutes)
|
|
7
|
+
.sort()
|
|
8
|
+
.forEach((path) => {
|
|
9
|
+
const filePath = getFileImportRelativePath(outputPath, path, packageMappings);
|
|
10
|
+
serializedOutput.push(`import '${filePath}'`);
|
|
11
|
+
});
|
|
12
|
+
return serializedOutput.join('\n');
|
|
13
|
+
};
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export const serializeHTTPRoutesMeta = (routesMeta) => {
|
|
2
|
+
const serializedOutput = [];
|
|
3
|
+
serializedOutput.push("import { setHTTPRoutesMeta } from '@pikku/core/http'");
|
|
4
|
+
serializedOutput.push(`setHTTPRoutesMeta(${JSON.stringify(routesMeta, null, 2)})`);
|
|
5
|
+
return serializedOutput.join('\n');
|
|
6
|
+
};
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import { HTTPRoutesMeta } from '@pikku/core/http';
|
|
2
|
+
import { MetaInputTypes, TypesMap } from '@pikku/inspector';
|
|
3
|
+
export declare const serializeTypedRoutesMap: (relativeToPath: string, packageMappings: Record<string, string>, typesMap: TypesMap, routesMeta: HTTPRoutesMeta, metaTypes: MetaInputTypes) => string;
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { serializeImportMap } from '../core/serialize-import-map.js';
|
|
2
|
+
export const serializeTypedRoutesMap = (relativeToPath, packageMappings, typesMap, routesMeta, metaTypes) => {
|
|
3
|
+
const requiredTypes = new Set();
|
|
4
|
+
const serializedCustomTypes = generateCustomTypes(typesMap, requiredTypes);
|
|
5
|
+
const serializedMetaTypes = generateMetaTypes(metaTypes, typesMap);
|
|
6
|
+
const serializedImportMap = serializeImportMap(relativeToPath, packageMappings, typesMap, requiredTypes);
|
|
7
|
+
const serializedRoutes = generateRoutes(routesMeta, typesMap, requiredTypes);
|
|
8
|
+
return `/**
|
|
9
|
+
* This provides the structure needed for typescript to be aware of routes and their return types
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
${serializedImportMap}
|
|
13
|
+
${serializedCustomTypes}
|
|
14
|
+
${serializedMetaTypes}
|
|
15
|
+
|
|
16
|
+
interface RouteHandler<I, O> {
|
|
17
|
+
input: I;
|
|
18
|
+
output: O;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
${serializedRoutes}
|
|
22
|
+
|
|
23
|
+
export type RouteHandlerOf<Route extends keyof RoutesMap, Method extends keyof RoutesMap[Route]> =
|
|
24
|
+
RoutesMap[Route][Method] extends { input: infer I; output: infer O }
|
|
25
|
+
? RouteHandler<I, O>
|
|
26
|
+
: never;
|
|
27
|
+
|
|
28
|
+
export type RoutesWithMethod<Method extends string> = {
|
|
29
|
+
[Route in keyof RoutesMap]: Method extends keyof RoutesMap[Route] ? Route : never;
|
|
30
|
+
}[keyof RoutesMap];
|
|
31
|
+
`;
|
|
32
|
+
};
|
|
33
|
+
function generateCustomTypes(typesMap, requiredTypes) {
|
|
34
|
+
return `
|
|
35
|
+
// Custom types are those that are defined directly within generics
|
|
36
|
+
// or are broken into simpler types
|
|
37
|
+
${Array.from(typesMap.customTypes.entries())
|
|
38
|
+
.map(([name, { type, references }]) => {
|
|
39
|
+
references.forEach((name) => {
|
|
40
|
+
const originalName = typesMap.getTypeMeta(name).originalName;
|
|
41
|
+
requiredTypes.add(originalName);
|
|
42
|
+
});
|
|
43
|
+
return `export type ${name} = ${type}`;
|
|
44
|
+
})
|
|
45
|
+
.join('\n')}`;
|
|
46
|
+
}
|
|
47
|
+
function generateRoutes(routesMeta, typesMap, requiredTypes) {
|
|
48
|
+
// Initialize an object to collect routes
|
|
49
|
+
const routesObj = {};
|
|
50
|
+
for (const meta of routesMeta) {
|
|
51
|
+
const { route, method, input, output } = meta;
|
|
52
|
+
// Initialize the route entry if it doesn't exist
|
|
53
|
+
if (!routesObj[route]) {
|
|
54
|
+
routesObj[route] = {};
|
|
55
|
+
}
|
|
56
|
+
// Store the input and output types separately for RouteHandler
|
|
57
|
+
const inputType = input ? typesMap.getTypeMeta(input).uniqueName : 'null';
|
|
58
|
+
const outputType = output ? typesMap.getTypeMeta(output).uniqueName : 'null';
|
|
59
|
+
requiredTypes.add(inputType);
|
|
60
|
+
requiredTypes.add(outputType);
|
|
61
|
+
// Add method entry
|
|
62
|
+
routesObj[route][method] = {
|
|
63
|
+
inputType,
|
|
64
|
+
outputType,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
// Build the routes object as a string
|
|
68
|
+
let routesStr = 'export type RoutesMap = {\n';
|
|
69
|
+
for (const [routePath, methods] of Object.entries(routesObj)) {
|
|
70
|
+
routesStr += ` readonly '${routePath}': {\n`;
|
|
71
|
+
for (const [method, handler] of Object.entries(methods)) {
|
|
72
|
+
routesStr += ` readonly ${method.toUpperCase()}: RouteHandler<${handler.inputType}, ${handler.outputType}>,\n`;
|
|
73
|
+
}
|
|
74
|
+
routesStr += ' },\n';
|
|
75
|
+
}
|
|
76
|
+
routesStr += '};';
|
|
77
|
+
return routesStr;
|
|
78
|
+
}
|
|
79
|
+
const generateMetaTypes = (metaTypes, typesMap) => {
|
|
80
|
+
const nameToTypeMap = Array.from(metaTypes.entries()).reduce((result, [_name, { query, body, params }]) => {
|
|
81
|
+
const { uniqueName } = typesMap.getTypeMeta(_name);
|
|
82
|
+
const queryType = query && query.length > 0
|
|
83
|
+
? `Pick<${uniqueName}, '${query?.join("' | '")}'>`
|
|
84
|
+
: undefined;
|
|
85
|
+
if (queryType) {
|
|
86
|
+
result.set(`${uniqueName}Query`, queryType);
|
|
87
|
+
}
|
|
88
|
+
const paramsType = params && params.length > 0
|
|
89
|
+
? `Pick<${uniqueName}, '${params.join("' | '")}'>`
|
|
90
|
+
: undefined;
|
|
91
|
+
if (paramsType) {
|
|
92
|
+
result.set(`${uniqueName}Params`, paramsType);
|
|
93
|
+
}
|
|
94
|
+
const bodyType = (body && body.length > 0) || (params && params.length > 0)
|
|
95
|
+
? `Omit<${uniqueName}, '${[...new Set([...(query || []), ...(params || [])])].join("' | '")}'>`
|
|
96
|
+
: uniqueName;
|
|
97
|
+
if (bodyType) {
|
|
98
|
+
result.set(`${uniqueName}Body`, bodyType);
|
|
99
|
+
}
|
|
100
|
+
return result;
|
|
101
|
+
}, new Map());
|
|
102
|
+
return `
|
|
103
|
+
// The '& {}' is a workaround for not directly refering to a type since it confuses typescript
|
|
104
|
+
${Array.from(nameToTypeMap.entries())
|
|
105
|
+
.map(([name, type]) => `export type ${name} = ${type} & {}`)
|
|
106
|
+
.join('\n')}`;
|
|
107
|
+
};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import * as path from 'path';
|
|
2
|
+
import { glob } from 'glob';
|
|
3
|
+
import { inspect } from '@pikku/inspector';
|
|
4
|
+
import { logCommandInfoAndTime } from './utils.js';
|
|
5
|
+
export const inspectorGlob = async (rootDir, routeDirectories) => {
|
|
6
|
+
let result;
|
|
7
|
+
await logCommandInfoAndTime('Inspecting codebase', 'Inspected codebase', [false], async () => {
|
|
8
|
+
const routeFiles = (await Promise.all(routeDirectories.map((dir) => glob(`${path.join(rootDir, dir)}/**/*.ts`)))).flat();
|
|
9
|
+
result = await inspect(routeFiles);
|
|
10
|
+
});
|
|
11
|
+
return result;
|
|
12
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const serializeNextJsWrapper: (routesPath: string, routesMapPath: string, schemasPath: string, configImport: string, singleServicesFactoryImport: string, sessionServicesImport: string) => string;
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
export const serializeNextJsWrapper = (routesPath, routesMapPath, schemasPath, configImport, singleServicesFactoryImport, sessionServicesImport) => {
|
|
2
|
+
return `'server-only'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* This file provides a wrapper around the PikkuNextJS class to allow for methods to be type checked against your routes.
|
|
6
|
+
* This ensures type safety for route handling methods when integrating with the \`@pikku/core\` framework.
|
|
7
|
+
*/
|
|
8
|
+
import { PikkuNextJS } from '@pikku/next'
|
|
9
|
+
import type { RoutesMap, RouteHandlerOf, RoutesWithMethod } from '${routesMapPath}'
|
|
10
|
+
|
|
11
|
+
${configImport}
|
|
12
|
+
${singleServicesFactoryImport}
|
|
13
|
+
${sessionServicesImport}
|
|
14
|
+
|
|
15
|
+
import '${routesPath}'
|
|
16
|
+
import '${schemasPath}'
|
|
17
|
+
|
|
18
|
+
let _pikku: PikkuNextJS | undefined
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Initializes and returns an instance of PikkuNextJS with helper methods for handling route requests.
|
|
22
|
+
*
|
|
23
|
+
* @returns An object containing methods for making dynamic and static action requests.
|
|
24
|
+
*/
|
|
25
|
+
export const pikku = () => {
|
|
26
|
+
if (!_pikku) {
|
|
27
|
+
_pikku = new PikkuNextJS(
|
|
28
|
+
createConfig as any,
|
|
29
|
+
createSingletonServices as any,
|
|
30
|
+
createSessionServices
|
|
31
|
+
)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Makes a dynamic action request for a specified route and method.
|
|
36
|
+
* Dynamic requests may access headers and cookies, making them unsuitable for precompile stages.
|
|
37
|
+
*
|
|
38
|
+
* @template Route - The route key from the RoutesMap.
|
|
39
|
+
* @template Method - The method key from the specified route.
|
|
40
|
+
* @param route - The route to make the request to.
|
|
41
|
+
* @param method - The method to be used for the request.
|
|
42
|
+
* @param data - The input data for the request, defaults to null.
|
|
43
|
+
* @returns A promise that resolves to the output of the route handler.
|
|
44
|
+
*/
|
|
45
|
+
const dynamicActionRequest = async <
|
|
46
|
+
Route extends keyof RoutesMap,
|
|
47
|
+
Method extends keyof RoutesMap[Route]
|
|
48
|
+
>(
|
|
49
|
+
route: Route,
|
|
50
|
+
method: Method,
|
|
51
|
+
data: RouteHandlerOf<Route, Method>['input'] = null
|
|
52
|
+
): Promise<RouteHandlerOf<Route, Method>['output']> => {
|
|
53
|
+
return _pikku!.actionRequest(route, method, data as any)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Makes a static action request for a specified route and method.
|
|
58
|
+
* Static requests do not depend on headers or cookies and are suitable for precompile stages.
|
|
59
|
+
*
|
|
60
|
+
* @template Route - The route key from the RoutesMap.
|
|
61
|
+
* @template Method - The method key from the specified route.
|
|
62
|
+
* @param route - The route to make the request to.
|
|
63
|
+
* @param method - The method to be used for the request.
|
|
64
|
+
* @param data - The input data for the request, defaults to null.
|
|
65
|
+
* @returns A promise that resolves to the output of the route handler.
|
|
66
|
+
*/
|
|
67
|
+
const staticActionRequest = async <
|
|
68
|
+
Route extends keyof RoutesMap,
|
|
69
|
+
Method extends keyof RoutesMap[Route]
|
|
70
|
+
>(
|
|
71
|
+
route: Route,
|
|
72
|
+
method: Method,
|
|
73
|
+
data: RouteHandlerOf<Route, Method>['input'] = null
|
|
74
|
+
): Promise<RouteHandlerOf<Route, Method>['output']> => {
|
|
75
|
+
return _pikku!.staticActionRequest(route, method, data as any)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Makes a dynamic POST request for a specified route.
|
|
80
|
+
*/
|
|
81
|
+
const dynamicPost = <Route extends RoutesWithMethod<'POST'>>(
|
|
82
|
+
route: Route,
|
|
83
|
+
data: RouteHandlerOf<Route, 'POST'>['input'] = null
|
|
84
|
+
): Promise<RouteHandlerOf<Route, 'POST'>['output']> => {
|
|
85
|
+
return dynamicActionRequest(route, 'POST', data)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Makes a dynamic GET request for a specified route.
|
|
90
|
+
*/
|
|
91
|
+
const dynamicGet = <Route extends RoutesWithMethod<'GET'>>(
|
|
92
|
+
route: Route,
|
|
93
|
+
data: RouteHandlerOf<Route, 'GET'>['input'] = null
|
|
94
|
+
): Promise<RouteHandlerOf<Route, 'GET'>['output']> => {
|
|
95
|
+
return dynamicActionRequest(route, 'GET', data)
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Makes a dynamic PATCH request for a specified route.
|
|
100
|
+
*/
|
|
101
|
+
const dynamicPatch = <Route extends RoutesWithMethod<'PATCH'>>(
|
|
102
|
+
route: Route,
|
|
103
|
+
data: RouteHandlerOf<Route, 'PATCH'>['input'] = null
|
|
104
|
+
): Promise<RouteHandlerOf<Route, 'PATCH'>['output']> => {
|
|
105
|
+
return dynamicActionRequest(route, 'PATCH', data)
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Makes a dynamic DELETE request for a specified route.
|
|
110
|
+
*/
|
|
111
|
+
const dynamicDel = <Route extends RoutesWithMethod<'DELETE'>>(
|
|
112
|
+
route: Route,
|
|
113
|
+
data: RouteHandlerOf<Route, 'DELETE'>['input'] = null
|
|
114
|
+
): Promise<RouteHandlerOf<Route, 'DELETE'>['output']> => {
|
|
115
|
+
return dynamicActionRequest(route, 'DELETE', data)
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Static
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Makes a static POST request for a specified route.
|
|
122
|
+
*/
|
|
123
|
+
const staticPost = <Route extends RoutesWithMethod<'POST'>>(
|
|
124
|
+
route: Route,
|
|
125
|
+
data: RouteHandlerOf<Route, 'POST'>['input'] = null
|
|
126
|
+
): Promise<RouteHandlerOf<Route, 'POST'>['output']> => {
|
|
127
|
+
return staticActionRequest(route, 'POST', data)
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Makes a static GET request for a specified route.
|
|
132
|
+
*/
|
|
133
|
+
const staticGet = <Route extends RoutesWithMethod<'GET'>>(
|
|
134
|
+
route: Route,
|
|
135
|
+
data: RouteHandlerOf<Route, 'GET'>['input'] = null
|
|
136
|
+
): Promise<RouteHandlerOf<Route, 'GET'>['output']> => {
|
|
137
|
+
return staticActionRequest(route, 'GET', data)
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return {
|
|
141
|
+
get: dynamicGet,
|
|
142
|
+
post: dynamicPost,
|
|
143
|
+
patch: dynamicPatch,
|
|
144
|
+
del: dynamicDel,
|
|
145
|
+
staticGet,
|
|
146
|
+
staticPost
|
|
147
|
+
}
|
|
148
|
+
}`;
|
|
149
|
+
};
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { HTTPRoutesMeta } from '@pikku/core/http';
|
|
2
|
+
interface OpenAPISpec {
|
|
3
|
+
openapi: string;
|
|
4
|
+
info: {
|
|
5
|
+
title: string;
|
|
6
|
+
version: string;
|
|
7
|
+
description?: string;
|
|
8
|
+
termsOfService?: string;
|
|
9
|
+
contact?: {
|
|
10
|
+
name?: string;
|
|
11
|
+
url?: string;
|
|
12
|
+
email?: string;
|
|
13
|
+
};
|
|
14
|
+
license?: {
|
|
15
|
+
name: string;
|
|
16
|
+
url?: string;
|
|
17
|
+
};
|
|
18
|
+
};
|
|
19
|
+
servers: {
|
|
20
|
+
url: string;
|
|
21
|
+
description?: string;
|
|
22
|
+
}[];
|
|
23
|
+
paths: Record<string, any>;
|
|
24
|
+
components: {
|
|
25
|
+
schemas: Record<string, any>;
|
|
26
|
+
responses?: Record<string, any>;
|
|
27
|
+
parameters?: Record<string, any>;
|
|
28
|
+
examples?: Record<string, any>;
|
|
29
|
+
requestBodies?: Record<string, any>;
|
|
30
|
+
headers?: Record<string, any>;
|
|
31
|
+
securitySchemes?: Record<string, any>;
|
|
32
|
+
};
|
|
33
|
+
security?: {
|
|
34
|
+
[key: string]: any[];
|
|
35
|
+
}[];
|
|
36
|
+
tags?: {
|
|
37
|
+
name: string;
|
|
38
|
+
description?: string;
|
|
39
|
+
}[];
|
|
40
|
+
externalDocs?: {
|
|
41
|
+
description?: string;
|
|
42
|
+
url: string;
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
export interface OpenAPISpecInfo {
|
|
46
|
+
info: {
|
|
47
|
+
title: string;
|
|
48
|
+
version: string;
|
|
49
|
+
description: string;
|
|
50
|
+
termsOfService?: string;
|
|
51
|
+
contact?: {
|
|
52
|
+
name?: string;
|
|
53
|
+
url?: string;
|
|
54
|
+
email?: string;
|
|
55
|
+
};
|
|
56
|
+
license?: {
|
|
57
|
+
name: string;
|
|
58
|
+
url?: string;
|
|
59
|
+
};
|
|
60
|
+
};
|
|
61
|
+
servers: {
|
|
62
|
+
url: string;
|
|
63
|
+
description?: string;
|
|
64
|
+
}[];
|
|
65
|
+
tags?: {
|
|
66
|
+
name: string;
|
|
67
|
+
description?: string;
|
|
68
|
+
}[];
|
|
69
|
+
externalDocs?: {
|
|
70
|
+
description?: string;
|
|
71
|
+
url: string;
|
|
72
|
+
};
|
|
73
|
+
securitySchemes?: Record<string, any>;
|
|
74
|
+
security?: {
|
|
75
|
+
[key: string]: any[];
|
|
76
|
+
}[];
|
|
77
|
+
}
|
|
78
|
+
export declare function generateOpenAPISpec(routeMeta: HTTPRoutesMeta, schemas: Record<string, any>, additionalInfo: OpenAPISpecInfo): Promise<OpenAPISpec>;
|
|
79
|
+
export {};
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { getErrors } from '@pikku/core/errors';
|
|
2
|
+
import _convertSchema from '@openapi-contrib/json-schema-to-openapi-schema';
|
|
3
|
+
const convertSchema = 'default' in _convertSchema ? _convertSchema.default : _convertSchema;
|
|
4
|
+
const getErrorResponseForConstructorName = (constructorName) => {
|
|
5
|
+
const foundError = Array.from(getErrors().entries()).find(([e]) => e.name === constructorName);
|
|
6
|
+
if (foundError) {
|
|
7
|
+
return foundError[1];
|
|
8
|
+
}
|
|
9
|
+
return undefined;
|
|
10
|
+
};
|
|
11
|
+
const convertSchemasToBodyPayloads = async (routesMeta, schemas) => {
|
|
12
|
+
const requiredSchemas = new Set(routesMeta
|
|
13
|
+
.map(({ inputTypes, output }) => [inputTypes?.body, output])
|
|
14
|
+
.flat()
|
|
15
|
+
.filter((schema) => !!schema));
|
|
16
|
+
const convertedEntries = await Promise.all(Object.entries(schemas).map(async ([key, schema]) => {
|
|
17
|
+
if (requiredSchemas.has(key)) {
|
|
18
|
+
const convertedSchema = await convertSchema(schema, {
|
|
19
|
+
convertUnreferencedDefinitions: false,
|
|
20
|
+
dereference: { circular: 'ignore' },
|
|
21
|
+
});
|
|
22
|
+
return [key, convertedSchema];
|
|
23
|
+
}
|
|
24
|
+
return;
|
|
25
|
+
}));
|
|
26
|
+
return Object.fromEntries(convertedEntries.filter((s) => !!s));
|
|
27
|
+
};
|
|
28
|
+
export async function generateOpenAPISpec(routeMeta, schemas, additionalInfo) {
|
|
29
|
+
const paths = {};
|
|
30
|
+
routeMeta.forEach((meta) => {
|
|
31
|
+
const { route, method, inputTypes, output, params, query, docs } = meta;
|
|
32
|
+
const path = route.replace(/:(\w+)/g, '{$1}'); // Convert ":param" to "{param}"
|
|
33
|
+
if (!paths[path]) {
|
|
34
|
+
paths[path] = {};
|
|
35
|
+
}
|
|
36
|
+
const responses = {};
|
|
37
|
+
docs?.errors?.forEach((error) => {
|
|
38
|
+
const errorResponse = getErrorResponseForConstructorName(error);
|
|
39
|
+
if (errorResponse) {
|
|
40
|
+
responses[errorResponse.status] = {
|
|
41
|
+
description: errorResponse.message,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
const operation = {
|
|
46
|
+
description: docs?.description ||
|
|
47
|
+
`This endpoint handles the ${method.toUpperCase()} request for the route ${route}.`,
|
|
48
|
+
tags: docs?.tags || [route.split('/')[1] || 'default'],
|
|
49
|
+
parameters: [],
|
|
50
|
+
responses: {
|
|
51
|
+
...responses,
|
|
52
|
+
'200': {
|
|
53
|
+
description: 'Successful response',
|
|
54
|
+
content: output
|
|
55
|
+
? {
|
|
56
|
+
'application/json': {
|
|
57
|
+
schema: typeof output === 'string' &&
|
|
58
|
+
['boolean', 'string', 'number'].includes(output)
|
|
59
|
+
? { type: output }
|
|
60
|
+
: { $ref: `#/components/schemas/${output}` },
|
|
61
|
+
},
|
|
62
|
+
}
|
|
63
|
+
: undefined,
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
};
|
|
67
|
+
const bodyType = inputTypes?.body;
|
|
68
|
+
if (bodyType) {
|
|
69
|
+
operation.requestBody = {
|
|
70
|
+
required: true,
|
|
71
|
+
content: {
|
|
72
|
+
'application/json': {
|
|
73
|
+
schema: typeof bodyType === 'string' &&
|
|
74
|
+
['boolean', 'string', 'number'].includes(bodyType)
|
|
75
|
+
? { type: bodyType }
|
|
76
|
+
: { $ref: `#/components/schemas/${bodyType}` },
|
|
77
|
+
},
|
|
78
|
+
},
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
if (params) {
|
|
82
|
+
operation.parameters = params.map((param) => ({
|
|
83
|
+
name: param,
|
|
84
|
+
in: 'path',
|
|
85
|
+
required: true,
|
|
86
|
+
schema: { type: 'string' },
|
|
87
|
+
}));
|
|
88
|
+
}
|
|
89
|
+
if (query) {
|
|
90
|
+
operation.parameters.push(...query.map((query) => ({
|
|
91
|
+
name: query,
|
|
92
|
+
in: 'query',
|
|
93
|
+
required: false,
|
|
94
|
+
schema: { type: 'string' },
|
|
95
|
+
})));
|
|
96
|
+
}
|
|
97
|
+
paths[path][method] = operation;
|
|
98
|
+
});
|
|
99
|
+
return {
|
|
100
|
+
openapi: '3.1.0',
|
|
101
|
+
info: additionalInfo.info,
|
|
102
|
+
servers: additionalInfo.servers,
|
|
103
|
+
paths,
|
|
104
|
+
components: {
|
|
105
|
+
schemas: await convertSchemasToBodyPayloads(routeMeta, schemas),
|
|
106
|
+
responses: {},
|
|
107
|
+
parameters: {},
|
|
108
|
+
examples: {},
|
|
109
|
+
requestBodies: {},
|
|
110
|
+
headers: {},
|
|
111
|
+
securitySchemes: additionalInfo.securitySchemes || {
|
|
112
|
+
ApiKeyAuth: {
|
|
113
|
+
type: 'apiKey',
|
|
114
|
+
in: 'header',
|
|
115
|
+
name: 'x-api-key',
|
|
116
|
+
},
|
|
117
|
+
BearerAuth: {
|
|
118
|
+
type: 'http',
|
|
119
|
+
scheme: 'bearer',
|
|
120
|
+
},
|
|
121
|
+
},
|
|
122
|
+
},
|
|
123
|
+
security: additionalInfo.security || [
|
|
124
|
+
{
|
|
125
|
+
ApiKeyAuth: [],
|
|
126
|
+
},
|
|
127
|
+
{
|
|
128
|
+
BearerAuth: [],
|
|
129
|
+
},
|
|
130
|
+
],
|
|
131
|
+
tags: additionalInfo.tags,
|
|
132
|
+
externalDocs: additionalInfo.externalDocs,
|
|
133
|
+
// definitions
|
|
134
|
+
};
|
|
135
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { OpenAPISpecInfo } from './openapi/openapi-spec-generator.js';
|
|
2
|
+
export interface PikkuCLICoreOutputFiles {
|
|
3
|
+
outDir?: string;
|
|
4
|
+
routesFile: string;
|
|
5
|
+
channelsFile: string;
|
|
6
|
+
schedulersFile: string;
|
|
7
|
+
schemaDirectory: string;
|
|
8
|
+
typesDeclarationFile: string;
|
|
9
|
+
routesMapDeclarationFile: string;
|
|
10
|
+
channelsMapDeclarationFile: string;
|
|
11
|
+
bootstrapFile: string;
|
|
12
|
+
}
|
|
13
|
+
export type PikkuCLIConfig = {
|
|
14
|
+
$schema?: string;
|
|
15
|
+
extends?: string;
|
|
16
|
+
rootDir: string;
|
|
17
|
+
routeDirectories: string[];
|
|
18
|
+
packageMappings: Record<string, string>;
|
|
19
|
+
supportsImportAttributes: boolean;
|
|
20
|
+
configDir: string;
|
|
21
|
+
tsconfig: string;
|
|
22
|
+
nextJSfile?: string;
|
|
23
|
+
fetchFile?: string;
|
|
24
|
+
websocketFile?: string;
|
|
25
|
+
openAPI?: {
|
|
26
|
+
outputFile: string;
|
|
27
|
+
additionalInfo: OpenAPISpecInfo;
|
|
28
|
+
};
|
|
29
|
+
} & PikkuCLICoreOutputFiles;
|
|
30
|
+
export declare const getPikkuCLIConfig: (configFile: string | undefined, requiredFields: Array<keyof PikkuCLIConfig>, exitProcess?: boolean) => Promise<PikkuCLIConfig>;
|
|
31
|
+
export declare const validateCLIConfig: (cliConfig: PikkuCLIConfig, required: Array<keyof PikkuCLIConfig>) => void;
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { join, dirname, resolve, isAbsolute } from 'path';
|
|
2
|
+
import { readdir, readFile } from 'fs/promises';
|
|
3
|
+
const CONFIG_DIR_FILES = ['nextJSfile', 'fetchFile', 'websocketFile'];
|
|
4
|
+
export const getPikkuCLIConfig = async (configFile = undefined, requiredFields, exitProcess = false) => {
|
|
5
|
+
const config = await _getPikkuCLIConfig(configFile, requiredFields, exitProcess);
|
|
6
|
+
return config;
|
|
7
|
+
};
|
|
8
|
+
const _getPikkuCLIConfig = async (configFile = undefined, requiredFields, exitProcess = false) => {
|
|
9
|
+
if (!configFile) {
|
|
10
|
+
let execDirectory = process.cwd();
|
|
11
|
+
const files = await readdir(execDirectory);
|
|
12
|
+
const file = files.find((file) => /pikku\.config\.(ts|js|json)$/.test(file));
|
|
13
|
+
if (!file) {
|
|
14
|
+
const errorMessage = '\nConfig file pikku.config.json not found\nExiting...';
|
|
15
|
+
if (exitProcess) {
|
|
16
|
+
console.error(errorMessage);
|
|
17
|
+
process.exit(1);
|
|
18
|
+
}
|
|
19
|
+
throw new Error(errorMessage);
|
|
20
|
+
}
|
|
21
|
+
configFile = join(execDirectory, file);
|
|
22
|
+
}
|
|
23
|
+
try {
|
|
24
|
+
let result;
|
|
25
|
+
const file = await readFile(configFile, 'utf-8');
|
|
26
|
+
const configDir = dirname(configFile);
|
|
27
|
+
const config = JSON.parse(file);
|
|
28
|
+
if (config.extends) {
|
|
29
|
+
const extendedConfig = await getPikkuCLIConfig(resolve(configDir, config.extends), [], exitProcess);
|
|
30
|
+
result = {
|
|
31
|
+
...extendedConfig,
|
|
32
|
+
...config,
|
|
33
|
+
configDir,
|
|
34
|
+
packageMappings: {
|
|
35
|
+
...extendedConfig.packageMappings,
|
|
36
|
+
...config.packageMappings,
|
|
37
|
+
},
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
else {
|
|
41
|
+
result = {
|
|
42
|
+
...config,
|
|
43
|
+
configDir,
|
|
44
|
+
packageMappings: config.packageMappings || {},
|
|
45
|
+
rootDir: config.rootDir
|
|
46
|
+
? resolve(configDir, config.rootDir)
|
|
47
|
+
: configDir,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
if (result.outDir) {
|
|
51
|
+
if (!result.schemaDirectory) {
|
|
52
|
+
result.schemaDirectory = join(result.outDir, 'pikku-schemas');
|
|
53
|
+
}
|
|
54
|
+
if (!result.routesFile) {
|
|
55
|
+
result.routesFile = join(result.outDir, 'pikku-routes.gen.ts');
|
|
56
|
+
}
|
|
57
|
+
if (!result.schedulersFile) {
|
|
58
|
+
result.schedulersFile = join(result.outDir, 'pikku-schedules.gen.ts');
|
|
59
|
+
}
|
|
60
|
+
if (!result.channelsFile) {
|
|
61
|
+
result.channelsFile = join(result.outDir, 'pikku-channels.gen.ts');
|
|
62
|
+
}
|
|
63
|
+
if (!result.typesDeclarationFile) {
|
|
64
|
+
result.typesDeclarationFile = join(result.outDir, 'pikku-types.gen.d.ts');
|
|
65
|
+
}
|
|
66
|
+
if (!result.routesMapDeclarationFile) {
|
|
67
|
+
result.routesMapDeclarationFile = join(result.outDir, 'pikku-routes-map.gen.d.ts');
|
|
68
|
+
}
|
|
69
|
+
if (!result.channelsMapDeclarationFile) {
|
|
70
|
+
result.channelsMapDeclarationFile = join(result.outDir, 'pikku-channels-map.gen.d.ts');
|
|
71
|
+
}
|
|
72
|
+
if (!result.bootstrapFile) {
|
|
73
|
+
result.bootstrapFile = join(result.outDir, 'pikku-bootstrap.gen.ts');
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
if (requiredFields.length > 0) {
|
|
77
|
+
validateCLIConfig(result, requiredFields);
|
|
78
|
+
}
|
|
79
|
+
for (const objectKey of Object.keys(result)) {
|
|
80
|
+
if (objectKey.endsWith('File') || objectKey.endsWith('Directory')) {
|
|
81
|
+
const relativeTo = CONFIG_DIR_FILES.includes(objectKey)
|
|
82
|
+
? result.configDir
|
|
83
|
+
: result.rootDir;
|
|
84
|
+
if (result[objectKey]) {
|
|
85
|
+
if (!isAbsolute(result[objectKey])) {
|
|
86
|
+
result[objectKey] = join(relativeTo, result[objectKey]);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
if (!isAbsolute(result.tsconfig)) {
|
|
92
|
+
result.tsconfig = join(result.rootDir, result.tsconfig);
|
|
93
|
+
}
|
|
94
|
+
return result;
|
|
95
|
+
}
|
|
96
|
+
catch (e) {
|
|
97
|
+
console.error(e);
|
|
98
|
+
console.error(`Config file not found: ${configFile}`);
|
|
99
|
+
process.exit(1);
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
export const validateCLIConfig = (cliConfig, required) => {
|
|
103
|
+
let errors = [];
|
|
104
|
+
for (const key of required) {
|
|
105
|
+
if (!cliConfig[key]) {
|
|
106
|
+
errors.push(key);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
if (errors.length > 0) {
|
|
110
|
+
console.error(`${errors.join(', ')} ${errors.length === 1 ? 'is' : 'are'} required in pikku.config.json`);
|
|
111
|
+
process.exit(1);
|
|
112
|
+
}
|
|
113
|
+
};
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import { ScheduledTasksMeta } from '@pikku/core/scheduler';
|
|
2
|
+
export declare const serializeSchedulers: (outputPath: string, filesWithScheduledTasks: Set<string>, packageMappings?: Record<string, string>) => string;
|
|
3
|
+
export declare const serializeSchedulerMeta: (scheduledTasksMeta: ScheduledTasksMeta) => string;
|