genezio
Version:
Command line utility to interact with Genezio infrastructure.
601 lines (591 loc) • 23.7 kB
JavaScript
import Mustache from "mustache";
import { AstNodeType, } from "../../models/genezioModels.js";
import { TriggerType } from "../../projectConfiguration/yaml/models.js";
import { storageTs } from "../templates/nodeSdkTs.js";
import path from "path";
const TYPESCRIPT_RESERVED_WORDS = [
"abstract",
"as",
"asserts",
"async",
"await",
"boolean",
"break",
"case",
"catch",
"class",
"const",
"constructor",
"continue",
"declare",
"default",
"delete",
"do",
"else",
"enum",
"export",
"extends",
"false",
"finally",
"for",
"from",
"function",
"get",
"if",
"implements",
"import",
"in",
"infer",
"instanceof",
"interface",
"is",
"keyof",
"let",
"module",
"namespace",
"never",
"new",
"null",
"number",
"object",
"of",
"package",
"private",
"protected",
"public",
"readonly",
"require",
"global",
"return",
"set",
"static",
"string",
"super",
"switch",
"symbol",
"this",
"throw",
"true",
"try",
"type",
"typeof",
"unique",
"unknown",
"var",
"void",
"while",
"with",
"yield",
"async",
"await",
"of",
];
const indexTemplate = `/**
* This is an auto generated code. This code should not be modified since the file can be overwritten
* if new genezio commands are executed.
*/
{{#imports}}
import { {{#models}}{{{name}}}{{^last}}, {{/last}}{{/models}} } from "./{{{path}}}";
{{/imports}}
export { {{#exports}}{{{name}}}{{^last}}, {{/last}}{{/exports}} };
`;
const modelTemplate = `/**
* This is an auto generated code. This code should not be modified since the file can be overwritten
* if new genezio commands are executed.
*/
{{#imports}}
import { {{#models}}{{{name}}}{{^last}}, {{/last}}{{/models}} } from "./{{{path}}}";
{{/imports}}
{{#externalTypes}}
export {{{type}}}
{{/externalTypes}}
`;
const template = `/**
* This is an auto generated code. This code should not be modified since the file can be overwritten
* if new genezio commands are executed.
*/
import { Remote } from "genezio-remote";
{{#imports}}
import { {{#models}}{{{name}}}{{^last}}, {{/last}}{{/models}} } from "./{{{path}}}";
{{/imports}}
{{#hasGnzContext}}
import { StorageManager } from "./storage";
{{/hasGnzContext}}
{{#externalTypes}}
export {{{type}}}
{{/externalTypes}}
{{#classDocLines.length}}
/**
{{#classDocLines}}
* {{{.}}}
{{/classDocLines}}
*/
{{/classDocLines.length}}
export class {{{className}}} {
static remote = new Remote("{{{_url}}}");
{{#methods}}
{{#hasGnzContextAsFirstParameter}}
{{#methodDocLines.length}}
/**
{{#methodDocLines}}
* {{{.}}}
{{/methodDocLines}}
*/
{{/methodDocLines.length}}
static async {{{name}}}({{#parameters}}{{{name}}}{{^last}}, {{/last}}{{/parameters}}){{{returnType}}} {
return await {{{className}}}.remote.call({{{methodCaller}}} {"token": StorageManager.getStorage().getItem("token"), "isGnzContext": true}, {{#sendParameters}}{{{name}}}{{^last}}, {{/last}}{{/sendParameters}});
}
{{/hasGnzContextAsFirstParameter}}
{{^hasGnzContextAsFirstParameter}}
{{#methodDocLines.length}}
/**
{{#methodDocLines}}
* {{{.}}}
{{/methodDocLines}}
*/
{{/methodDocLines.length}}
static async {{{name}}}({{#parameters}}{{{name}}}{{^last}}, {{/last}}{{/parameters}}){{{returnType}}} {
return await {{{className}}}.remote.call({{{methodCaller}}}{{#sendParameters}}{{{name}}}{{^last}}, {{/last}}{{/sendParameters}});
}
{{/hasGnzContextAsFirstParameter}}
{{/methods}}
}
`;
class SdkGenerator {
async generateSdk(sdkGeneratorInput) {
const generateSdkOutput = {
files: [],
};
const modelViews = [];
const indexModel = {
imports: [],
exports: [],
};
for (const classInfo of sdkGeneratorInput.classesInfo) {
const externalTypes = [];
const _url = "%%%link_to_be_replace%%%";
const classConfiguration = classInfo.classConfiguration;
let classDefinition = undefined;
if (classInfo.program.body === undefined) {
continue;
}
for (const elem of classInfo.program.body) {
if (elem.type === AstNodeType.ClassDefinition) {
classDefinition = elem;
}
else {
externalTypes.push(elem);
}
}
if (classDefinition === undefined) {
continue;
}
// @ts-expect-error A refactor need to be performed here to avoid this error
const view = {
className: classDefinition.name,
classDocLines: classDefinition.docString?.replace(/\n+$/, "").split("\n") || [],
_url: _url,
methods: [],
externalTypes: [],
imports: [],
};
let exportClassChecker = false;
for (const methodDefinition of classDefinition.methods) {
const methodConfiguration = classConfiguration.methods.find((m) => m.name === methodDefinition.name);
const methodConfigurationType = methodConfiguration?.type || classConfiguration.type;
if (methodConfigurationType !== TriggerType.jsonrpc ||
classConfiguration.type !== TriggerType.jsonrpc) {
continue;
}
exportClassChecker = true;
// @ts-expect-error A refactor need to be performed here to avoid this error
const methodView = {
// methods start with lowercase
name: methodDefinition.name.charAt(0).toLowerCase() +
methodDefinition.name.slice(1),
parameters: [],
returnType: this.getReturnType(methodDefinition.returnType),
methodCaller: methodDefinition.params.length === 0
? `"${classDefinition.name}.${methodDefinition.name}"`
: `"${classDefinition.name}.${methodDefinition.name}", `,
methodDocLines: methodDefinition.docString?.replace(/\n+$/, "").split("\n") || [],
};
methodView.parameters = methodDefinition.params
.map((e) => {
// GnzContext is a special type used to pass auth token and other information to the remote call
if (e.paramType.type === AstNodeType.CustomNodeLiteral &&
e.paramType.rawValue === "GnzContext") {
methodView.hasGnzContextAsFirstParameter = true;
view.hasGnzContext = true;
return undefined;
}
return {
name: (TYPESCRIPT_RESERVED_WORDS.includes(e.name)
? e.name + "_"
: e.name) +
(e.optional ? "?" : "") +
": " +
this.getParamType(e.paramType) +
(e.defaultValue
? " = " +
(e.defaultValue.type === AstNodeType.StringLiteral
? "'" + e.defaultValue.value + "'"
: e.defaultValue.value)
: ""),
last: false,
};
})
.filter((e) => e !== undefined);
methodView.sendParameters = methodDefinition.params
.map((e) => {
// GnzContext is a special type used to pass auth token and other information to the remote call
if (e.paramType.type === AstNodeType.CustomNodeLiteral &&
e.paramType.rawValue === "GnzContext") {
methodView.hasGnzContextAsFirstParameter = true;
view.hasGnzContext = true;
return undefined;
}
return {
name: TYPESCRIPT_RESERVED_WORDS.includes(e.name)
? e.name + "_"
: e.name,
last: false,
};
})
.filter((e) => e !== undefined);
if (methodView.parameters.length > 0) {
methodView.parameters[methodView.parameters.length - 1].last = true;
methodView.sendParameters[methodView.sendParameters.length - 1].last = true;
}
view.methods.push(methodView);
}
for (const externalType of externalTypes) {
if (externalType.path) {
let currentView = undefined;
for (const parentType of externalTypes) {
const isUsed = this.isExternalTypeUsedByOtherType(externalType, parentType);
if (isUsed && parentType.path && parentType.path !== externalType.path) {
currentView = this.addViewIfNotExists(modelViews, parentType, view, classInfo, classDefinition);
}
const classPath = classInfo.classConfiguration.path.replace(/\\/g, "/");
if (currentView && !classPath.includes(externalType.path)) {
this.addImportToCurrentView(currentView, externalType);
}
}
if (this.isExternalTypeUsedInMethod(externalType, classDefinition.methods)) {
// @ts-expect-error A refactor need to be performed here to avoid this error
currentView = view;
const classPath = classDefinition.path?.replace(/\\/g, "/") ??
classInfo.classConfiguration.path.replace(/\\/g, "/");
if (currentView && !classPath.includes(externalType.path)) {
this.addImportToCurrentView(currentView, externalType);
}
}
currentView = this.addViewIfNotExists(modelViews, externalType, view, classInfo, classDefinition);
if (!currentView?.externalTypes.find(
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(e) => e.name === externalType.name)) {
currentView?.externalTypes.push({
type: this.generateExternalType(externalType),
// eslint-disable-next-line @typescript-eslint/no-explicit-any
name: externalType.name,
});
}
}
}
for (const modelView of modelViews) {
for (const importType of modelView.imports) {
if (importType.models.length > 0) {
importType.models[importType.models.length - 1].last = true;
}
}
}
for (const importType of view.imports) {
if (importType.models.length > 0) {
importType.models[importType.models.length - 1].last = true;
}
}
if (!exportClassChecker) {
continue;
}
this.addClassItemsToIndex(indexModel, classInfo.program.body, classDefinition.path ?? "", classDefinition.name);
const rawSdkClassName = `${classDefinition.name}.sdk.ts`;
const sdkClassName = rawSdkClassName.charAt(0).toLowerCase() + rawSdkClassName.slice(1);
generateSdkOutput.files.push({
path: sdkClassName,
data: Mustache.render(template, view),
className: classDefinition.name,
});
for (const modelView of modelViews) {
generateSdkOutput.files.push({
path: modelView.path + ".ts",
data: Mustache.render(modelTemplate, modelView),
className: "",
});
}
}
generateSdkOutput.files.push({
className: "StorageManager",
path: "storage.ts",
data: storageTs,
});
indexModel.imports.push({
path: "storage",
models: [{ name: "Storage" }, { name: "StorageManager" }],
});
indexModel.exports.push({ name: "Storage" }, { name: "StorageManager" });
if (indexModel.exports.length > 0) {
indexModel.exports[indexModel.exports.length - 1].last = true;
}
for (const importStatement of indexModel.imports) {
if (importStatement.models.length > 0) {
importStatement.models[importStatement.models.length - 1].last = true;
}
}
generateSdkOutput.files.push({
className: "index.ts",
path: "index.ts",
data: Mustache.render(indexTemplate, indexModel),
});
return generateSdkOutput;
}
getReturnType(returnType) {
if (!returnType) {
return "";
}
let value = this.getParamType(returnType);
if (returnType.type !== AstNodeType.PromiseType) {
value = `Promise<${value}>`;
}
return `: ${value}`;
}
getParamType(elem) {
switch (elem.type) {
case AstNodeType.CustomNodeLiteral:
return elem.rawValue;
case AstNodeType.StringLiteral:
return "string";
case AstNodeType.BigIntLiteral:
return "bigint";
case AstNodeType.IntegerLiteral:
case AstNodeType.FloatLiteral:
case AstNodeType.DoubleLiteral:
return "number";
case AstNodeType.BooleanLiteral:
return "boolean";
case AstNodeType.AnyLiteral:
return "any";
case AstNodeType.ArrayType:
return `Array<${this.getParamType(elem.generic)}>`;
case AstNodeType.PromiseType:
return `Promise<${this.getParamType(elem.generic)}>`;
case AstNodeType.Enum:
return elem.name;
case AstNodeType.TypeAlias:
return elem.name;
case AstNodeType.UnionType:
return elem.params
.map((e) => this.getParamType(e))
.join(" | ");
case AstNodeType.TypeLiteral:
return `{${elem.properties
.map((e) => {
if (e.type.type === AstNodeType.MapType &&
elem.properties.length === 1) {
return `[key: ${this.getParamType(e.type.genericKey)}]: ${this.getParamType(e.type.genericValue)}`;
}
else {
return `${e.name}${e.optional ? "?" : ""}: ${this.getParamType(e.type)}`;
}
})
.join(", ")}}`;
case AstNodeType.DateType:
return "Date";
case AstNodeType.MapType:
return `{[key: ${this.getParamType(elem.genericKey)}]: ${this.getParamType(elem.genericValue)}}`;
case AstNodeType.VoidLiteral:
return "void";
}
return "any";
}
generateExternalType(type) {
if (type.type === AstNodeType.TypeAlias) {
const typeAlias = type;
return `type ${typeAlias.name} = ${this.getParamType(typeAlias.aliasType)};`;
}
else if (type.type === AstNodeType.Enum) {
const enumType = type;
return `enum ${enumType.name} {${enumType.cases
.map((c) => {
if (c.type === AstNodeType.StringLiteral) {
return `${c.name} = "${c.value}"`;
}
else if (c.type === AstNodeType.DoubleLiteral) {
if (c.value !== undefined && c.value !== null) {
return `${c.name} = ${c.value}`;
}
else {
return `${c.name}`;
}
}
})
.join(", ")}}`;
}
else if (type.type === AstNodeType.StructLiteral) {
const typeAlias = type;
return `type ${typeAlias.name} = ${this.getParamType(typeAlias.typeLiteral)};`;
}
return "";
}
isExternalTypeUsedByOtherType(externalType, type) {
if (type.type === AstNodeType.TypeAlias) {
const typeAlias = type;
return this.isExternalTypeUsedByOtherType(externalType, typeAlias.aliasType);
}
else if (type.type === AstNodeType.Enum) {
return false;
}
else if (type.type === AstNodeType.StructLiteral) {
const typeAlias = type;
return this.isExternalTypeUsedByOtherType(externalType, typeAlias.typeLiteral);
}
else if (type.type === AstNodeType.ArrayType) {
return this.isExternalTypeUsedByOtherType(externalType, type.generic);
}
else if (type.type === AstNodeType.PromiseType) {
return this.isExternalTypeUsedByOtherType(externalType, type.generic);
}
else if (type.type === AstNodeType.UnionType) {
return type.params.some((e) => this.isExternalTypeUsedByOtherType(externalType, e));
}
else if (type.type === AstNodeType.TypeLiteral) {
return type.properties.some((e) => this.isExternalTypeUsedByOtherType(externalType, e.type));
}
else if (type.type === AstNodeType.DateType) {
return false;
}
else if (type.type === AstNodeType.CustomNodeLiteral) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
if (type.rawValue === externalType.name) {
return true;
}
return false;
}
else if (type.type === AstNodeType.StringLiteral) {
return false;
}
else if (type.type === AstNodeType.IntegerLiteral ||
type.type === AstNodeType.FloatLiteral ||
type.type === AstNodeType.DoubleLiteral) {
return false;
}
else if (type.type === AstNodeType.BooleanLiteral) {
return false;
}
else if (type.type === AstNodeType.AnyLiteral) {
return false;
}
else if (type.type === AstNodeType.MapType) {
return (this.isExternalTypeUsedByOtherType(externalType, type.genericKey) ||
this.isExternalTypeUsedByOtherType(externalType, type.genericValue));
}
return false;
}
isExternalTypeUsedInMethod(externalType, methods) {
return methods.some((m) => this.isExternalTypeUsedByOtherType(externalType, m.returnType) ||
m.params.some((p) => this.isExternalTypeUsedByOtherType(externalType, p.paramType)));
}
addImportToCurrentView(currentView, externalType) {
let found = false;
for (const importType of currentView.imports) {
if (importType.path === externalType.path &&
// eslint-disable-next-line @typescript-eslint/no-explicit-any
!importType.models.find((e) => e.name === externalType.name)) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
importType.models.push({ name: externalType.name });
found = true;
break;
}
}
if (!found) {
let relativePath = path.relative(currentView.path || ".", externalType.path || ".");
while (relativePath.substring(0, 3) == "../") {
relativePath = relativePath.substring(3);
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
if (!currentView.imports.find((e) => e.path === relativePath)) {
currentView.imports.push({
path: relativePath.replace(/\\/g, "/"),
// eslint-disable-next-line @typescript-eslint/no-explicit-any
models: [{ name: externalType.name }],
});
}
}
}
addClassItemsToIndex(indexModel, classItems, classPath, className) {
for (const originalClassItem of classItems) {
const classItem = { ...originalClassItem };
if (classItem.path === classPath) {
const rawSdkClassPath = `${className}.sdk`;
const sdkClassPath = rawSdkClassPath.charAt(0).toLowerCase() + rawSdkClassPath.slice(1);
classItem.path = sdkClassPath;
}
const index = indexModel.imports.findIndex((i) => i.path === classItem.path);
if (index !== -1) {
if (
// eslint-disable-next-line @typescript-eslint/no-explicit-any
indexModel.imports[index].models.find((i) => i.name === classItem.name)) {
continue;
}
else {
indexModel.imports[index].models.push({
// eslint-disable-next-line @typescript-eslint/no-explicit-any
name: classItem.name,
});
// eslint-disable-next-line @typescript-eslint/no-explicit-any
indexModel.exports.push({ name: classItem.name });
}
}
else {
indexModel.imports.push({
path: classItem.path || "",
// eslint-disable-next-line @typescript-eslint/no-explicit-any
models: [{ name: classItem.name }],
});
// eslint-disable-next-line @typescript-eslint/no-explicit-any
indexModel.exports.push({ name: classItem.name });
}
}
}
addViewIfNotExists(modelViews, type, classView, classInfo, classDefinition) {
let found = false;
let currentView = undefined;
for (const modelView of modelViews) {
if (modelView.path === type.path) {
currentView = modelView;
found = true;
break;
}
}
if (!found) {
const classPath = classDefinition?.path?.replace(/\\/g, "/") ??
classInfo.classConfiguration.path.replace(/\\/g, "/");
// @ts-expect-error A refactor need to be performed here to avoid this cast
if (!classPath.includes(type.path)) {
currentView = {
path: type.path || "",
externalTypes: [],
imports: [],
};
modelViews.push(currentView);
}
else {
// @ts-expect-error A refactor need to be performed here to avoid this cast
currentView = classView;
}
}
return currentView;
}
}
const supportedLanguages = ["ts", "typescript"];
export default { SdkGenerator, supportedLanguages };