@nestia/sdk
Version:
Nestia SDK and Swagger generator
308 lines (281 loc) • 10.3 kB
text/typescript
import fs from "fs";
import path from "path";
import { HashSet, Pair, TreeMap } from "tstl";
import { IMetadataDictionary } from "typia/lib/schemas/metadata/IMetadataDictionary";
import { INestiaConfig } from "./INestiaConfig";
import { AccessorAnalyzer } from "./analyses/AccessorAnalyzer";
import { ConfigAnalyzer } from "./analyses/ConfigAnalyzer";
import { PathAnalyzer } from "./analyses/PathAnalyzer";
import { ReflectControllerAnalyzer } from "./analyses/ReflectControllerAnalyzer";
import { TypedHttpRouteAnalyzer } from "./analyses/TypedHttpRouteAnalyzer";
import { TypedWebSocketRouteAnalyzer } from "./analyses/TypedWebSocketRouteAnalyzer";
import { E2eGenerator } from "./generates/E2eGenerator";
import { SdkGenerator } from "./generates/SdkGenerator";
import { SwaggerGenerator } from "./generates/SwaggerGenerator";
import { INestiaProject } from "./structures/INestiaProject";
import { IReflectController } from "./structures/IReflectController";
import { IReflectOperationError } from "./structures/IReflectOperationError";
import { ITypedApplication } from "./structures/ITypedApplication";
import { ITypedHttpRoute } from "./structures/ITypedHttpRoute";
import { ITypedWebSocketRoute } from "./structures/ITypedWebSocketRoute";
import { IOperationMetadata } from "./transformers/IOperationMetadata";
import { StringUtil } from "./utils/StringUtil";
import { VersioningStrategy } from "./utils/VersioningStrategy";
export class NestiaSdkApplication {
public constructor(private readonly config: INestiaConfig) {}
public async all(): Promise<void> {
if (!this.config.output && !this.config.swagger?.output)
throw new Error(
[
"Error on NestiaApplication.all(): nothing to generate, configure at least one property of below:",
"",
" - INestiaConfig.output",
" - INestiaConfig.swagger.output",
" - INestiaConfig.openai.output",
].join("\n"),
);
print_title("Nestia All Generator");
await this.generate({
generate: async (app) => {
if (this.config.output) {
await SdkGenerator.generate(app);
if (this.config.e2e) await E2eGenerator.generate(app);
}
if (this.config.swagger) await SwaggerGenerator.generate(app);
},
validate: this.config.output ? SdkGenerator.validate : undefined,
});
}
public async e2e(): Promise<void> {
if (!this.config.output)
throw new Error(
"Error on NestiaApplication.e2e(): configure INestiaConfig.output property.",
);
else if (!this.config.e2e)
throw new Error(
"Error on NestiaApplication.e2e(): configure INestiaConfig.e2e property.",
);
const validate =
(title: string) =>
async (location: string): Promise<void> => {
const parent: string = path.resolve(location + "/..");
const stats: fs.Stats = await fs.promises.lstat(parent);
if (stats.isDirectory() === false)
throw new Error(
`Error on NestiaApplication.e2e(): output directory of ${title} does not exists.`,
);
};
await validate("sdk")(this.config.output);
await validate("e2e")(this.config.e2e);
print_title("Nestia E2E Generator");
await this.generate({
generate: async (app) => {
await SdkGenerator.generate(app);
await E2eGenerator.generate(app);
},
});
}
public async sdk(): Promise<void> {
if (!this.config.output)
throw new Error(
"Error on NestiaApplication.sdk(): configure INestiaConfig.output property.",
);
const parent: string = path.resolve(this.config.output + "/..");
const stats: fs.Stats = await fs.promises.lstat(parent);
if (stats.isDirectory() === false)
throw new Error(
"Error on NestiaApplication.sdk(): output directory does not exists.",
);
print_title("Nestia SDK Generator");
await this.generate({
generate: SdkGenerator.generate,
validate: SdkGenerator.validate,
});
}
public async swagger(): Promise<void> {
if (!this.config.swagger?.output)
throw new Error(
`Error on NestiaApplication.swagger(): configure INestiaConfig.swagger property.`,
);
const parsed: path.ParsedPath = path.parse(this.config.swagger.output);
const directory: string = !!parsed.ext
? path.resolve(parsed.dir)
: this.config.swagger.output;
const stats: fs.Stats = await fs.promises.lstat(directory);
if (stats.isDirectory() === false)
throw new Error(
"Error on NestiaApplication.swagger(): output directory does not exists.",
);
print_title("Nestia Swagger Generator");
await this.generate({
generate: SwaggerGenerator.generate,
});
}
private async generate(props: {
generate: (app: ITypedApplication) => Promise<void>;
validate?: (app: ITypedApplication) => IReflectOperationError[];
}): Promise<void> {
//----
// ANALYZE REFLECTS
//----
const unique: WeakSet<any> = new WeakSet();
const project: INestiaProject = {
config: this.config,
input: await ConfigAnalyzer.input(this.config),
checker: null!,
errors: [],
warnings: [],
};
console.log("Analyzing reflections");
const controllers: IReflectController[] = project.input.controllers
.map((c) =>
ReflectControllerAnalyzer.analyze({ project, controller: c, unique }),
)
.filter((c): c is IReflectController => c !== null);
if (project.warnings.length)
report({
type: "warning",
errors: project.warnings,
});
if (project.errors.length)
return report({
type: "error",
errors: project.errors,
});
const agg: number = (() => {
const set: HashSet<Pair<string, string>> = new HashSet();
for (const controller of controllers)
for (const controllerPath of controller.paths)
for (const operation of controller.operations)
for (const operationPath of operation.paths)
set.insert(
new Pair(
`${controllerPath}/${operationPath}`,
operation.protocol === "http" ? operation.method : "",
),
);
return set.size();
})();
console.log(` - controllers: #${controllers.length}`);
console.log(` - paths: #${agg}`);
console.log(
` - routes: #${controllers
.map(
(c) =>
c.paths.length *
c.operations.map((f) => f.paths.length).reduce((a, b) => a + b, 0),
)
.reduce((a, b) => a + b, 0)}`,
);
//----
// ANALYZE TYPESCRIPT CODE
//----
console.log("Analyzing source codes");
// METADATA COMPONENTS
const collection: IMetadataDictionary =
TypedHttpRouteAnalyzer.dictionary(controllers);
// CONVERT TO TYPED OPERATIONS
const globalPrefix: string = project.input.globalPrefix?.prefix ?? "";
const routes: Array<ITypedHttpRoute | ITypedWebSocketRoute> = [];
for (const c of controllers)
for (const o of c.operations) {
const pathList: Set<string> = new Set();
const versions: string[] = VersioningStrategy.merge(project)([
...(c.versions ?? []),
...(o.versions ?? []),
]);
for (const v of versions)
for (const prefix of wrapPaths(c.prefixes))
for (const cPath of wrapPaths(c.paths))
for (const filePath of wrapPaths(o.paths))
pathList.add(
PathAnalyzer.join(globalPrefix, v, prefix, cPath, filePath),
);
if (o.protocol === "http")
routes.push(
...TypedHttpRouteAnalyzer.analyze({
controller: c,
errors: project.errors,
dictionary: collection,
operation: o,
paths: Array.from(pathList),
}),
);
else if (o.protocol === "websocket")
routes.push(
...TypedWebSocketRouteAnalyzer.analyze({
controller: c,
operation: o,
paths: Array.from(pathList),
}),
);
}
AccessorAnalyzer.analyze(routes);
if (props.validate !== undefined)
project.errors.push(
...props.validate({
project,
collection,
routes,
}),
);
if (project.errors.length)
return report({
type: "error",
errors: project.errors,
});
await props.generate({
project,
collection,
routes,
});
}
}
const print_title = (str: string): void => {
console.log("-----------------------------------------------------------");
console.log(` ${str}`);
console.log("-----------------------------------------------------------");
};
const report = (props: {
type: "error" | "warning";
errors: IReflectOperationError[];
}): void => {
const map: TreeMap<
IReflectOperationError.Key,
Array<string | IOperationMetadata.IError>
> = new TreeMap();
for (const e of props.errors)
map.take(new IReflectOperationError.Key(e), () => []).push(...e.contents);
console.log("");
print_title(`Nestia ${StringUtil.capitalize(props.type)} Report`);
for (const {
first: { error },
second: contents,
} of map) {
if (error.contents.length === 0) continue;
const location: string = path.relative(process.cwd(), error.file);
const message: string = [
`${location} - `,
error.class,
...(error.function !== null ? [`.${error.function}()`] : [""]),
...(error.from !== null ? [` from ${error.from}`] : [""]),
":\n",
contents
.map((c) => {
if (typeof c === "string") return ` - ${c}`;
else
return [
c.accessor
? ` - ${c.name}: `
: ` - ${c.name} (${c.accessor}): `,
...c.messages.map((msg) => ` - ${msg}`),
].join("\n");
})
.join("\n"),
].join("");
if (props.type === "error") throw new Error(message);
else console.log(message);
}
};
const wrapPaths = (paths: string[]): string[] =>
paths.length === 0 ? [""] : paths;