Skip to content
This repository has been archived by the owner on Oct 10, 2022. It is now read-only.

Abstract class generation #289

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions src/IGenerationOptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,16 @@ export default interface IGenerationOptions {
propertyVisibility: "public" | "protected" | "private" | "none";
lazy: boolean;
activeRecord: boolean;
skipRelationships: boolean;
extendAbstractClass: string;
generateConstructor: boolean;
customNamingStrategyPath: string;
relationIds: boolean;
strictMode: "none" | "?" | "!";
skipSchema: boolean;
indexFile: boolean;
exportType: "named" | "default";
exportAbstractClass: boolean;
}

export const eolConverter = {
Expand All @@ -42,13 +45,16 @@ export function getDefaultGenerationOptions(): IGenerationOptions {
propertyVisibility: "none",
lazy: false,
activeRecord: false,
skipRelationships: false,
extendAbstractClass: "",
generateConstructor: false,
customNamingStrategyPath: "",
relationIds: false,
strictMode: "none",
skipSchema: false,
indexFile: false,
exportType: "named",
exportAbstractClass: false,
};
return generationOptions;
}
12 changes: 12 additions & 0 deletions src/ModelCustomization.ts
Original file line number Diff line number Diff line change
Expand Up @@ -219,9 +219,21 @@ function addImportsAndGenerationOptions(
if (generationOptions.activeRecord) {
entity.activeRecord = true;
}
if (generationOptions.skipRelationships) {
entity.skipRelationships = true;
}
if (generationOptions.extendAbstractClass) {
entity.extendAbstractClass = generationOptions.extendAbstractClass;
}
if (generationOptions.generateConstructor) {
entity.generateConstructor = true;
}
if (generationOptions.exportAbstractClass) {
entity.exportAbstractClass = true;
}
entity.generateSuper = !!(
entity.activeRecord || entity.extendAbstractClass
);
});
return dbModel;
}
Expand Down
2 changes: 1 addition & 1 deletion src/ModelGeneration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -231,7 +231,7 @@ function createHandlebarsHelpers(generationOptions: IGenerationOptions): void {
}
);
Handlebars.registerHelper("defaultExport", () =>
generationOptions.exportType === "default" ? "default" : ""
generationOptions.exportType === "default" ? " default" : ""
);
Handlebars.registerHelper("localImport", (entityName: string) =>
generationOptions.exportType === "default"
Expand Down
91 changes: 90 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,15 @@ function validateConfig(options: options): options {
false
);
options.generationOptions.relationIds = false;
} else if (
options.generationOptions.activeRecord &&
options.generationOptions.extendAbstractClass
) {
TomgUtils.LogError(
"Typeorm cannot use ActiveRecord and extend-abstract-class at the same time.",
false
);
options.generationOptions.activeRecord = false;
}
return options;
}
Expand Down Expand Up @@ -238,6 +247,24 @@ function checkYargsParameters(options: options): options {
default: options.generationOptions.activeRecord,
describe: "Use ActiveRecord syntax for generated models",
},
skipRelationships: {
alias: "skip-relationships",
boolean: true,
default: options.generationOptions.skipRelationships,
describe: "Skip relationship declarations",
},
extendAbstractClass: {
alias: "extend-abstract-class",
string: true,
default: options.generationOptions.extendAbstractClass,
describe: "Make generated models extend a custom abstract class",
},
exportAbstractClass: {
alias: "export-abstract-class",
boolean: true,
default: options.generationOptions.exportAbstractClass,
describe: "Export generated models as abstract classes",
},
namingStrategy: {
describe: "Use custom naming strategy",
default: options.generationOptions.customNamingStrategyPath,
Expand Down Expand Up @@ -307,6 +334,8 @@ function checkYargsParameters(options: options): options {
}
options.connectionOptions.skipTables = skipTables;
options.generationOptions.activeRecord = argv.a;
options.generationOptions.skipRelationships = argv.skipRelationships;
options.generationOptions.extendAbstractClass = argv.extendAbstractClass;
options.generationOptions.generateConstructor = argv.generateConstructor;
options.generationOptions.convertCaseEntity = argv.ce as IGenerationOptions["convertCaseEntity"];
options.generationOptions.convertCaseFile = argv.cf as IGenerationOptions["convertCaseFile"];
Expand All @@ -325,7 +354,7 @@ function checkYargsParameters(options: options): options {
options.generationOptions.exportType = argv.defaultExport
? "default"
: "named";

options.generationOptions.exportAbstractClass = argv.exportAbstractClass;
return options;
}

Expand Down Expand Up @@ -499,6 +528,19 @@ async function useInquirer(options: options): Promise<options> {
value: "activeRecord",
checked: options.generationOptions.activeRecord,
},
{
name: "Skip relationship declarations",
value: "skipRelationships",
checked:
options.generationOptions.skipRelationships,
},
{
name:
"Generated models extend a custom abstract class",
value: "extendAbstractClass",
checked:
options.generationOptions.extendAbstractClass,
},
{
name: "Use custom naming strategy",
value: "namingStrategy",
Expand Down Expand Up @@ -558,6 +600,12 @@ async function useInquirer(options: options): Promise<options> {
options.generationOptions.exportType ===
"default",
},
{
name: "Export generated models as abstract classes",
value: "exportAbstractClass",
checked:
options.generationOptions.exportAbstractClass,
},
],
message: "Available customizations",
name: "selected",
Expand Down Expand Up @@ -601,6 +649,44 @@ async function useInquirer(options: options): Promise<options> {
options.generationOptions.activeRecord = customizations.includes(
"activeRecord"
);
options.generationOptions.skipRelationships = customizations.includes(
"skipRelationships"
);
if (customizations.includes("extendAbstractClass")) {
const { extendAbstractClass } = await inquirer.prompt([
{
default: options.generationOptions.extendAbstractClass,
message: "Relative path to custom abstract class file:",
name: "extendAbstractClass",
type: "input",
validate(value) {
const valid = value === "" || fs.existsSync(value);
return (
valid ||
"Please enter a valid relative path to custom abstract class file"
);
},
},
]);

if (extendAbstractClass && extendAbstractClass !== "") {
const resultsAbsolutePath = path.join(
process.cwd(),
options.generationOptions.resultsPath
);
const abstractClassAbsolutePath = path.join(
process.cwd(),
options.generationOptions.resultsPath
);
const relativePath = path.relative(
abstractClassAbsolutePath,
resultsAbsolutePath
);
options.generationOptions.extendAbstractClass = relativePath;
} else {
options.generationOptions.extendAbstractClass = "";
}
}
options.generationOptions.relationIds = customizations.includes(
"relationId"
);
Expand All @@ -616,6 +702,9 @@ async function useInquirer(options: options): Promise<options> {
)
? "default"
: "named";
options.generationOptions.exportAbstractClass = customizations.includes(
"exportAbstractClass"
);

if (customizations.includes("namingStrategy")) {
const namingStrategyPath = (
Expand Down
4 changes: 4 additions & 0 deletions src/models/Entity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,5 +17,9 @@ export type Entity = {
// TODO: move to sub-object or use handlebars helpers(?)
fileImports: string[];
activeRecord?: true;
skipRelationships?: boolean;
extendAbstractClass?: string;
generateSuper?: boolean;
generateConstructor?: true;
exportAbstractClass?: true;
};
13 changes: 7 additions & 6 deletions src/templates/entity.mst
Original file line number Diff line number Diff line change
Expand Up @@ -26,22 +26,23 @@ import {{localImport (toEntityName .)}} from './{{toFileName .}}'
{{/inline}}
{{#*inline "Constructor"}}
{{printPropertyVisibility}}constructor(init?: Partial<{{toEntityName entityName}}>) {
{{#activeRecord}}super();
{{/activeRecord}}Object.assign(this, init);
{{#generateSuper}}super();
{{/generateSuper}}Object.assign(this, init);
}
{{/inline}}
{{#*inline "Entity"}}
{{#indices}}{{> Index}}{{/indices~}}
@Entity("{{sqlName}}"{{#schema}} ,{schema:"{{.}}"{{#if ../database}}, database:"{{../database}}"{{/if}} } {{/schema}})
export {{defaultExport}} class {{toEntityName tscName}}{{#activeRecord}} extends BaseEntity{{/activeRecord}} {
{{#unless exportAbstractClass}}@Entity("{{sqlName}}"{{#schema}} ,{schema:"{{.}}"{{#if ../database}}, database:"{{../database}}"{{/if}} } {{/schema}}){{/unless}}
export{{defaultExport}}{{#exportAbstractClass}} abstract{{/exportAbstractClass}} class {{toEntityName tscName}}{{#activeRecord}} extends BaseEntity{{/activeRecord}}{{#extendAbstractClass}} extends BaseClass{{/extendAbstractClass}} {

{{#columns}}{{> Column}}{{/columns~}}
{{#relations}}{{> Relation}}{{/relations~}}
{{#unless skipRelationships}}{{#relations}}{{> Relation}}{{/relations~}}{{/unless}}
{{#relationIds}}{{> RelationId entityName=../tscName}}{{/relationIds~}}
{{#if generateConstructor}}{{>Constructor entityName=tscName}}{{/if~}}
}
{{/inline}}
import {BaseEntity,Column,Entity,Index,JoinColumn,JoinTable,ManyToMany,ManyToOne,OneToMany,OneToOne,PrimaryColumn,PrimaryGeneratedColumn,RelationId} from "typeorm";
{{#fileImports}}{{> Import}}{{/fileImports}}
{{#unless skipRelationships}}{{#fileImports}}{{> Import}}{{/fileImports}}{{/unless}}
{{#extendAbstractClass}}import BaseClass from "{{.}}";{{/extendAbstractClass}}

{{> Entity}}
112 changes: 112 additions & 0 deletions test/modelCustomization/modelCustomization.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -451,6 +451,118 @@ describe("Model customization phase", async () => {

compileGeneratedModel(generationOptions.resultsPath, [""]);
});
it("skipRelationships", async () => {
const data = generateSampleData();
const generationOptions = generateGenerationOptions();
clearGenerationDir();

generationOptions.skipRelationships = true;
const customizedModel = modelCustomizationPhase(
data,
generationOptions,
{}
);
modelGenerationPhase(
getDefaultConnectionOptions(),
generationOptions,
customizedModel
);
const filesGenPath = path.resolve(resultsPath, "entities");
const postContent = fs
.readFileSync(path.resolve(filesGenPath, "Post.ts"))
.toString();
const postAuthorContent = fs
.readFileSync(path.resolve(filesGenPath, "PostAuthor.ts"))
.toString();
expect(postContent).to.not.have.string(
`JoinColumn`,
);
expect(postContent).to.not.have.string(
`import { PostAuthor } from "./PostAuthor";`,
);
expect(postAuthorContent).to.not.have.string(
`OneToMany`,
);
expect(postAuthorContent).to.not.have.string(
`import { Post } from "./Post";`,
);

compileGeneratedModel(generationOptions.resultsPath, [""]);
});
it("extendAbstractClass", async () => {
const data = generateSampleData();
const generationOptions = generateGenerationOptions();
clearGenerationDir();

generationOptions.extendAbstractClass = '../../test/integration/examples/sample28-abstract-class-inheritance/BaseClass';
const customizedModel = modelCustomizationPhase(
data,
generationOptions,
{}
);
modelGenerationPhase(
getDefaultConnectionOptions(),
generationOptions,
customizedModel
);
const filesGenPath = path.resolve(resultsPath, "entities");
const postContent = fs
.readFileSync(path.resolve(filesGenPath, "Post.ts"))
.toString();
const postAuthorContent = fs
.readFileSync(path.resolve(filesGenPath, "PostAuthor.ts"))
.toString();
expect(postContent).to.have.string(
`export class Post extends BaseClass `
);
expect(postAuthorContent).to.have.string(
`export class PostAuthor extends BaseClass `
);
expect(postContent).to.have.string(
`import BaseClass from "../../test/integration/examples/sample28-abstract-class-inheritance/BaseClass"`
);
expect(postAuthorContent).to.have.string(
`import BaseClass from "../../test/integration/examples/sample28-abstract-class-inheritance/BaseClass"`
);
});
it("exportAbstractClass", async () => {
const data = generateSampleData();
const generationOptions = generateGenerationOptions();
clearGenerationDir();

generationOptions.exportAbstractClass = true;
const customizedModel = modelCustomizationPhase(
data,
generationOptions,
{}
);
modelGenerationPhase(
getDefaultConnectionOptions(),
generationOptions,
customizedModel
);
const filesGenPath = path.resolve(resultsPath, "entities");
const postContent = fs
.readFileSync(path.resolve(filesGenPath, "Post.ts"))
.toString();
const postAuthorContent = fs
.readFileSync(path.resolve(filesGenPath, "PostAuthor.ts"))
.toString();
expect(postContent).to.have.string(
`export abstract class Post `
);
expect(postAuthorContent).to.have.string(
`export abstract class PostAuthor `
);
expect(postContent).to.not.have.string(
`@Entity`
);
expect(postAuthorContent).to.not.have.string(
`@Entity`
);

compileGeneratedModel(generationOptions.resultsPath, [""]);
});
it("skipSchema", async () => {
const data = generateSampleData();
const generationOptions = generateGenerationOptions();
Expand Down