From a4619dd9abdac7e7f6a0444ad684ec26e9c2a1db Mon Sep 17 00:00:00 2001 From: Kononnable Date: Sun, 27 Oct 2019 17:17:42 +0100 Subject: [PATCH] model customization tests --- src/ModelCustomization.ts | 6 + src/models/Entity.ts | 3 + src/templates/entity.mst | 17 +- test/integration/runTestsFromPath.test.ts | 2 +- .../modelCustomization.test.ts | 567 ++++++++++++++++++ 5 files changed, 589 insertions(+), 6 deletions(-) create mode 100644 test/modelCustomization/modelCustomization.test.ts diff --git a/src/ModelCustomization.ts b/src/ModelCustomization.ts index 6e0563d..10edab1 100644 --- a/src/ModelCustomization.ts +++ b/src/ModelCustomization.ts @@ -84,6 +84,12 @@ function addImportsAndGenerationOptions( entity.schema = undefined; entity.database = undefined; } + if (generationOptions.activeRecord) { + entity.activeRecord = true; + } + if (generationOptions.generateConstructor) { + entity.generateConstructor = true; + } }); return dbModel; } diff --git a/src/models/Entity.ts b/src/models/Entity.ts index 019aa20..8c189f9 100644 --- a/src/models/Entity.ts +++ b/src/models/Entity.ts @@ -14,5 +14,8 @@ export type Entity = { relationIds: RelationId[]; relations: Relation[]; indices: Index[]; + // TODO: move to sub-object or use handlebars helpers(?) fileImports: string[]; + activeRecord?: true; + generateConstructor?: true; }; diff --git a/src/templates/entity.mst b/src/templates/entity.mst index 3a6aea7..a4c6933 100644 --- a/src/templates/entity.mst +++ b/src/templates/entity.mst @@ -6,29 +6,36 @@ import { {{toEntityName .}} } from './{{toFileName .}}' {{/inline}} {{#*inline "Column"}} {{#generated}}@PrimaryGeneratedColumn({ type:"{{type}}", {{/generated}}{{^generated}}@Column("{{type}}",{ {{#primary}}primary:{{primary}},{{/primary}}{{/generated}}{{json options}}{{#default}},default: {{.}},{{/default}} }) -{{toPropertyName tscName}}:{{tscType}}; +{{printPropertyVisibility}}{{toPropertyName tscName}}{{strictMode}}:{{tscType}}; {{/inline}} {{#*inline "Relation"}} @{{relationType}}(()=>{{toEntityName relatedTable}},{{toEntityName relatedTable}}=>{{toEntityName relatedTable}}.{{toPropertyName relatedField}}{{#if relationOptions}},{ {{json relationOptions}} }{{/if}}) {{#if joinColumnOptions}}@JoinColumn([{{json joinColumnOptions}}]){{/if}} {{#if joinTableOptions}}@JoinTable({ {{json joinTableOptions}} }){{/if}} -{{toPropertyName fieldName}}:{{toRelation (toEntityName relatedTable) relationType}}; +{{printPropertyVisibility}}{{toPropertyName fieldName}}{{strictMode}}:{{toRelation (toEntityName relatedTable) relationType}}; {{/inline}} {{#*inline "RelationId"}} @RelationId(({{toPropertyName entityName}}:{{toEntityName entityName}})=>{{toPropertyName entityName}}.{{toPropertyName relationField}}) -{{toPropertyName fieldName}}:{{fieldType}}; +{{printPropertyVisibility}}{{toPropertyName fieldName}}{{strictMode}}:{{fieldType}}; +{{/inline}} +{{#*inline "Constructor"}} +{{printPropertyVisibility}}constructor(init?: Partial<{{toEntityName entityName}}>) { + {{#activeRecord}}super(); + {{/activeRecord}}Object.assign(this, init); +} {{/inline}} {{#*inline "Entity"}} {{#indices}}{{> Index}}{{/indices~}} -@Entity("{{sqlName}}",{schema:"{{schema}}",database:"{{database}}"}) -export class {{toEntityName tscName}} { +@Entity("{{sqlName}}"{{#schema}} ,{schema:"{{.}}"{{#if ../database}}, database:"{{../database}}"{{/if}} } {{/schema}}) +export class {{toEntityName tscName}}{{#activeRecord}} extends BaseEntity{{/activeRecord}} { {{#columns}}{{> Column}}{{/columns~}} {{#relations}}{{> Relation}}{{/relations~}} {{#relationIds}}{{> RelationId entityName=../tscName}}{{/relationIds~}} +{{#if generateConstructor}}{{>Constructor entityName=tscName}}{{/if~}} } {{/inline}} diff --git a/test/integration/runTestsFromPath.test.ts b/test/integration/runTestsFromPath.test.ts index e86fa39..b84cb12 100644 --- a/test/integration/runTestsFromPath.test.ts +++ b/test/integration/runTestsFromPath.test.ts @@ -199,7 +199,7 @@ function compareGeneratedFiles(filesOrgPathTS: string, filesGenPath: string) { }); } -function compileGeneratedModel(filesGenPath: string, drivers: string[]) { +export function compileGeneratedModel(filesGenPath: string, drivers: string[]) { const currentDirectoryFiles: string[] = []; drivers.forEach(driver => { const entitiesPath = path.resolve(filesGenPath, driver, "entities"); diff --git a/test/modelCustomization/modelCustomization.test.ts b/test/modelCustomization/modelCustomization.test.ts new file mode 100644 index 0000000..5ba0ff1 --- /dev/null +++ b/test/modelCustomization/modelCustomization.test.ts @@ -0,0 +1,567 @@ +import * as path from "path"; +import * as fs from "fs-extra"; +import * as chai from "chai"; +import * as chaiSubset from "chai-subset"; +import { Entity } from "../../src/models/Entity"; +import modelCustomizationPhase from "../../src/ModelCustomization"; +import IGenerationOptions from "../../src/IGenerationOptions"; +import modelGenerationPhase from "../../src/ModelGeneration"; +import IConnectionOptions from "../../src/IConnectionOptions"; +import { compileGeneratedModel } from "../integration/runTestsFromPath.test"; + +chai.use(chaiSubset); +const { expect } = chai; +describe("Model customization phase", async () => { + const generateSampleData: () => Entity[] = () => [ + { + columns: [ + { + generated: true, + type: "integer", + options: { name: "id" }, + tscName: "id", + tscType: "number", + primary: true + }, + { + type: "character varying", + options: { name: "name" }, + tscName: "name", + tscType: "string" + } + ], + indices: [ + { + columns: ["id"], + options: { unique: true }, + name: "PK_6571d08cfb2f1ab06c3aab425a6", + primary: true + } + ], + relations: [ + { + fieldName: "Post", + relatedField: "authorId", + relatedTable: "Post", + relationType: "OneToOne" + } + ], + relationIds: [], + sqlName: "PostAuthor", + tscName: "PostAuthor", + database: "", + schema: "public", + fileImports: ["Post"] + }, + { + columns: [ + { + generated: true, + type: "integer", + options: { name: "id" }, + tscName: "id", + tscType: "number", + primary: true + }, + { + type: "character varying", + options: { name: "title" }, + tscName: "title", + tscType: "string" + }, + { + type: "character varying", + options: { name: "text" }, + tscName: "text", + tscType: "string" + } + ], + indices: [ + { + columns: ["authorId"], + options: { unique: true }, + name: "REL_cef8d6e8edb69c82e5f10bb402" + }, + { + columns: ["id"], + options: { unique: true }, + name: "PK_c4d3b3dcd73db0b0129ea829f9f", + primary: true + } + ], + relations: [ + { + fieldName: "authorId", + relatedField: "Post", + joinColumnOptions: [ + { name: "authorId", referencedColumnName: "id" } + ], + relatedTable: "PostAuthor", + relationType: "OneToOne" + } + ], + relationIds: [], + sqlName: "Post", + tscName: "Post", + database: "", + schema: "public", + fileImports: ["PostAuthor"] + } + ]; + + const resultsPath = path.resolve(process.cwd(), `output`); + const generateGenerationOptions = () => { + const generationOptions = new IGenerationOptions(); + generationOptions.resultsPath = resultsPath; + return generationOptions; + }; + const clearGenerationDir = () => { + fs.ensureDirSync(resultsPath); + fs.emptyDirSync(resultsPath); + }; + describe("case-file", () => { + it("PascalCase", () => { + const data = generateSampleData(); + const generationOptions = generateGenerationOptions(); + clearGenerationDir(); + + generationOptions.convertCaseFile = "pascal"; + const customizedModel = modelCustomizationPhase( + data, + generationOptions, + {} + ); + modelGenerationPhase( + new IConnectionOptions(), + generationOptions, + customizedModel + ); + const filesGenPath = path.resolve(resultsPath, "entities"); + const files = fs.readdirSync(filesGenPath).sort(); + expect(files[0]).to.equal("Post.ts"); + expect(files[1]).to.equal("PostAuthor.ts"); + compileGeneratedModel(generationOptions.resultsPath, [""]); + }); + it("camelCase", () => { + const data = generateSampleData(); + const generationOptions = generateGenerationOptions(); + clearGenerationDir(); + + generationOptions.convertCaseFile = "camel"; + const customizedModel = modelCustomizationPhase( + data, + generationOptions, + {} + ); + modelGenerationPhase( + new IConnectionOptions(), + generationOptions, + customizedModel + ); + const filesGenPath = path.resolve(resultsPath, "entities"); + const files = fs.readdirSync(filesGenPath).sort(); + expect(files[0]).to.equal("post.ts"); + expect(files[1]).to.equal("postAuthor.ts"); + compileGeneratedModel(generationOptions.resultsPath, [""]); + }); + }); + describe("case-entity", () => { + it("PascalCase", () => { + const data = generateSampleData(); + const generationOptions = generateGenerationOptions(); + clearGenerationDir(); + + generationOptions.convertCaseEntity = "pascal"; + const customizedModel = modelCustomizationPhase( + data, + generationOptions, + {} + ); + modelGenerationPhase( + new IConnectionOptions(), + 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.contain("class Post {"); + expect(postAuthorContent).to.contain("class PostAuthor {"); + + compileGeneratedModel(generationOptions.resultsPath, [""]); + }); + it("camelCase", () => { + const data = generateSampleData(); + const generationOptions = generateGenerationOptions(); + clearGenerationDir(); + + generationOptions.convertCaseEntity = "camel"; + const customizedModel = modelCustomizationPhase( + data, + generationOptions, + {} + ); + modelGenerationPhase( + new IConnectionOptions(), + 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.contain("class post {"); + expect(postAuthorContent).to.contain("class postAuthor {"); + + compileGeneratedModel(generationOptions.resultsPath, [""]); + }); + }); + describe("case-property", async () => { + it("PascalCase", () => { + const data = generateSampleData(); + const generationOptions = generateGenerationOptions(); + clearGenerationDir(); + + generationOptions.convertCaseProperty = "pascal"; + const customizedModel = modelCustomizationPhase( + data, + generationOptions, + {} + ); + modelGenerationPhase( + new IConnectionOptions(), + 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.contain("Title: string;"); + expect(postAuthorContent).to.contain("Post: Post;"); + + compileGeneratedModel(generationOptions.resultsPath, [""]); + }); + it("camelCase", () => { + const data = generateSampleData(); + const generationOptions = generateGenerationOptions(); + clearGenerationDir(); + + generationOptions.convertCaseProperty = "camel"; + const customizedModel = modelCustomizationPhase( + data, + generationOptions, + {} + ); + modelGenerationPhase( + new IConnectionOptions(), + 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.contain("title: string;"); + expect(postAuthorContent).to.contain("post: Post;"); + + compileGeneratedModel(generationOptions.resultsPath, [""]); + }); + }); + describe("property-visibility", () => { + it("public", () => { + const data = generateSampleData(); + const generationOptions = generateGenerationOptions(); + clearGenerationDir(); + + generationOptions.propertyVisibility = "public"; + const customizedModel = modelCustomizationPhase( + data, + generationOptions, + {} + ); + modelGenerationPhase( + new IConnectionOptions(), + 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(" public title: string"); + expect(postAuthorContent).to.have.string(" public post: Post;"); + + compileGeneratedModel(generationOptions.resultsPath, [""]); + }); + it("none", () => { + const data = generateSampleData(); + const generationOptions = generateGenerationOptions(); + clearGenerationDir(); + + generationOptions.propertyVisibility = "none"; + const customizedModel = modelCustomizationPhase( + data, + generationOptions, + {} + ); + modelGenerationPhase( + new IConnectionOptions(), + 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(" title: string"); + expect(postAuthorContent).to.have.string(" post: Post;"); + + compileGeneratedModel(generationOptions.resultsPath, [""]); + }); + }); + it("lazy", async () => { + const data = generateSampleData(); + const generationOptions = generateGenerationOptions(); + clearGenerationDir(); + + generationOptions.lazy = true; + const customizedModel = modelCustomizationPhase( + data, + generationOptions, + {} + ); + modelGenerationPhase( + new IConnectionOptions(), + 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("lazy: true"); + expect(postContent).to.have.string("Promise;"); + expect(postAuthorContent).to.have.string("lazy: true"); + expect(postAuthorContent).to.have.string("Promise"); + + compileGeneratedModel(generationOptions.resultsPath, [""]); + }); + it("activeRecord", async () => { + const data = generateSampleData(); + const generationOptions = generateGenerationOptions(); + clearGenerationDir(); + + generationOptions.activeRecord = true; + const customizedModel = modelCustomizationPhase( + data, + generationOptions, + {} + ); + modelGenerationPhase( + new IConnectionOptions(), + 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 BaseEntity ` + ); + expect(postAuthorContent).to.have.string( + `export class PostAuthor extends BaseEntity ` + ); + + compileGeneratedModel(generationOptions.resultsPath, [""]); + }); + it("skipSchema", async () => { + const data = generateSampleData(); + const generationOptions = generateGenerationOptions(); + clearGenerationDir(); + + generationOptions.skipSchema = true; + const customizedModel = modelCustomizationPhase( + data, + generationOptions, + {} + ); + modelGenerationPhase( + new IConnectionOptions(), + 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(`@Entity("Post")`); + expect(postAuthorContent).to.have.string(`@Entity("PostAuthor")`); + + compileGeneratedModel(generationOptions.resultsPath, [""]); + }); + it("generateConstructor", async () => { + const data = generateSampleData(); + const generationOptions = generateGenerationOptions(); + clearGenerationDir(); + + generationOptions.generateConstructor = true; + const customizedModel = modelCustomizationPhase( + data, + generationOptions, + {} + ); + modelGenerationPhase( + new IConnectionOptions(), + 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(`constructor(init?: Partial)`); + expect(postContent).to.have.string(`Object.assign(this, init);`); + expect(postAuthorContent).to.have.string( + `constructor(init?: Partial)` + ); + expect(postAuthorContent).to.have.string(`Object.assign(this, init);`); + + compileGeneratedModel(generationOptions.resultsPath, [""]); + }); + it("generateConstructor with activeRecord", async () => { + const data = generateSampleData(); + const generationOptions = generateGenerationOptions(); + clearGenerationDir(); + + generationOptions.generateConstructor = true; + generationOptions.activeRecord = true; + const customizedModel = modelCustomizationPhase( + data, + generationOptions, + {} + ); + modelGenerationPhase( + new IConnectionOptions(), + 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(`constructor(init?: Partial)`); + expect(postContent).to.have.string(`super();`); + expect(postContent).to.have.string(`Object.assign(this, init);`); + expect(postAuthorContent).to.have.string( + `constructor(init?: Partial)` + ); + expect(postAuthorContent).to.have.string(`super();`); + expect(postAuthorContent).to.have.string(`Object.assign(this, init);`); + + compileGeneratedModel(generationOptions.resultsPath, [""]); + }); + describe("strictMode", () => { + it("!", async () => { + const data = generateSampleData(); + const generationOptions = generateGenerationOptions(); + clearGenerationDir(); + + generationOptions.strictMode = "!"; + const customizedModel = modelCustomizationPhase( + data, + generationOptions, + {} + ); + modelGenerationPhase( + new IConnectionOptions(), + 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(`id!: number;`); + expect(postContent).to.have.string(`title!: string;`); + expect(postContent).to.have.string(`text!: string;`); + expect(postContent).to.have.string(`author!: PostAuthor;`); + expect(postAuthorContent).to.have.string(`id!: number;`); + expect(postAuthorContent).to.have.string(`name!: string;`); + expect(postAuthorContent).to.have.string(`post!: Post;`); + + compileGeneratedModel(generationOptions.resultsPath, [""]); + }); + it("?", async () => { + const data = generateSampleData(); + const generationOptions = generateGenerationOptions(); + clearGenerationDir(); + + generationOptions.strictMode = "?"; + const customizedModel = modelCustomizationPhase( + data, + generationOptions, + {} + ); + modelGenerationPhase( + new IConnectionOptions(), + 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(`id?: number;`); + expect(postContent).to.have.string(`title?: string;`); + expect(postContent).to.have.string(`text?: string;`); + expect(postContent).to.have.string(`author?: PostAuthor;`); + expect(postAuthorContent).to.have.string(`id?: number;`); + expect(postAuthorContent).to.have.string(`name?: string;`); + expect(postAuthorContent).to.have.string(`post?: Post;`); + + compileGeneratedModel(generationOptions.resultsPath, [""]); + }); + }); +});