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

Commit

Permalink
Support abstract class codegen.
Browse files Browse the repository at this point in the history
The idea is to use the generated classes as abstract classes. Custom domain behaviors (i.e. methods, validations, and lifecycles) may be located in another class that inherits the generated model. This could benefit us from

1. avoiding polluting when running codegen after migration on existing codes.
1. have lean models without the boilerplates.
1. share common behaviors across multiple models.

3 new flags:

- `extendAbstractClass` - string. Similar to `activeRecord` that `extends BaseEntity`. Now it supports custom abstract class inheritance by passing a relative path as string to `extendAbstractClass`.
- `exportAbstractClass` - boolean. Export the generated models as an abstract class without `@Entity`
- `skipRelationships` - boolean. Skip generating relationship columns. Because currently there is a [bug from TypeORM failing at foreign key embedded columns in abstract classes](typeorm/typeorm#3132).

Close: #288
  • Loading branch information
ivawzh committed Aug 10, 2020
1 parent 82c2122 commit 894b641
Show file tree
Hide file tree
Showing 7 changed files with 232 additions and 8 deletions.
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

0 comments on commit 894b641

Please sign in to comment.