From 79c041b8ca6a78db198e71a4b0ec1dbba224acbd Mon Sep 17 00:00:00 2001 From: Cisco Guillaume Date: Mon, 21 Dec 2020 08:42:44 +0100 Subject: [PATCH 1/5] fix(sequelize): fix error when working with column table including parenthesis (#520) --- .../analyzer/sequelize-tables-analyzer.js | 5 ++- services/dumper.js | 7 +++- templates/app/models/sequelize-model.hbs | 2 +- .../parenthesis.expected.json | 31 +++++++++++++++ .../parenthesis_underscored.expected.json | 39 +++++++++++++++++++ ...parenthesis_underscored_true.expected.json | 39 +++++++++++++++++++ .../dumper-output/parenthesis.expected.js | 27 +++++++++++++ .../parenthesis_underscored.expected.js | 32 +++++++++++++++ .../parenthesis_underscored_true.expected.js | 33 ++++++++++++++++ test-fixtures/mssql/parenthesis_table.sql | 5 +++ .../mssql/parenthesis_underscored_table.sql | 6 +++ test-fixtures/mysql/parenthesis_table.sql | 5 +++ .../mysql/parenthesis_underscored_table.sql | 6 +++ test-fixtures/postgres/parenthesis_table.sql | 5 +++ .../parenthesis_underscored_table.sql | 6 +++ .../database-analyzer-sequelize.test.js | 20 ++++++++++ test/services/dumper/dumper-sequelize.test.js | 36 +++++++++++++++++ 17 files changed, 301 insertions(+), 3 deletions(-) create mode 100644 test-expected/sequelize/db-analysis-output/parenthesis.expected.json create mode 100644 test-expected/sequelize/db-analysis-output/parenthesis_underscored.expected.json create mode 100644 test-expected/sequelize/db-analysis-output/parenthesis_underscored_true.expected.json create mode 100644 test-expected/sequelize/dumper-output/parenthesis.expected.js create mode 100644 test-expected/sequelize/dumper-output/parenthesis_underscored.expected.js create mode 100644 test-expected/sequelize/dumper-output/parenthesis_underscored_true.expected.js create mode 100644 test-fixtures/mssql/parenthesis_table.sql create mode 100644 test-fixtures/mssql/parenthesis_underscored_table.sql create mode 100644 test-fixtures/mysql/parenthesis_table.sql create mode 100644 test-fixtures/mysql/parenthesis_underscored_table.sql create mode 100644 test-fixtures/postgres/parenthesis_table.sql create mode 100644 test-fixtures/postgres/parenthesis_underscored_table.sql diff --git a/services/analyzer/sequelize-tables-analyzer.js b/services/analyzer/sequelize-tables-analyzer.js index a3e15125..3c2b7359 100644 --- a/services/analyzer/sequelize-tables-analyzer.js +++ b/services/analyzer/sequelize-tables-analyzer.js @@ -322,7 +322,10 @@ async function createTableSchema(columnTypeGetter, { defaultValue = Sequelize.literal(defaultValue); } - const name = _.camelCase(columnName); + // NOTICE: sequelize considers column name with parenthesis as raw Attributes + // do not try to camelCase the name for avoiding sequelize issues + const hasParenthesis = columnName.includes('(') || columnName.includes(')'); + const name = hasParenthesis ? columnName : _.camelCase(columnName); let isRequired = !columnInfo.allowNull; if (isTechnicalTimestamp({ name, type })) { isRequired = false; diff --git a/services/dumper.js b/services/dumper.js index f5920ba7..79ae450b 100755 --- a/services/dumper.js +++ b/services/dumper.js @@ -217,14 +217,19 @@ class Dumper { const fieldsDefinition = fields.map((field) => { const expectedConventionalColumnName = underscored ? _.snakeCase(field.name) : field.name; + // NOTICE: sequelize considers column name with parenthesis as raw Attributes + // only set as unconventional name if underscored is true for adding special field attribute + // and avoid sequelize issues + const hasParenthesis = field.nameColumn && (field.nameColumn.includes('(') || field.nameColumn.includes(')')); const nameColumnUnconventional = field.nameColumn !== expectedConventionalColumnName - || (underscored && /[1-9]/g.test(field.name)); + || (underscored && (/[1-9]/g.test(field.name) || hasParenthesis)); const safeDefaultValue = this.getSafeDefaultValue(field); return { ...field, ref: field.ref && Dumper.getModelNameFromTableName(field.ref), nameColumnUnconventional, + hasParenthesis, safeDefaultValue, // NOTICE: needed to keep falsy default values in template hasSafeDefaultValue: !_.isNil(safeDefaultValue), diff --git a/templates/app/models/sequelize-model.hbs b/templates/app/models/sequelize-model.hbs index 50d62737..231264bb 100644 --- a/templates/app/models/sequelize-model.hbs +++ b/templates/app/models/sequelize-model.hbs @@ -6,7 +6,7 @@ module.exports = (sequelize, DataTypes) => { // Learn more here: https://docs.forestadmin.com/documentation/v/v6/reference-guide/models/enrich-your-models#declaring-a-new-field-in-a-model const {{modelVariableName}} = sequelize.define('{{modelName}}', { {{#each fields as |field|}} - {{field.name}}: { + {{#if field.hasParenthesis}}'{{/if}}{{field.name}}{{#if field.hasParenthesis}}'{{/if}}: { type: DataTypes.{{{field.type}}},{{#if field.nameColumnUnconventional}} field: '{{field.nameColumn}}',{{/if}}{{#if field.primaryKey}} primaryKey: true,{{/if}}{{#if field.hasSafeDefaultValue}} diff --git a/test-expected/sequelize/db-analysis-output/parenthesis.expected.json b/test-expected/sequelize/db-analysis-output/parenthesis.expected.json new file mode 100644 index 00000000..210491a3 --- /dev/null +++ b/test-expected/sequelize/db-analysis-output/parenthesis.expected.json @@ -0,0 +1,31 @@ +{ + "parenthesis": { + "fields": [ + { + "name": "id", + "nameColumn": "id", + "type": "INTEGER", + "primaryKey": true, + "defaultValue": null, + "isRequired": true + }, + { + "name": "Ingredients (Kcal/100g)", + "nameColumn": "Ingredients (Kcal/100g)", + "type": "STRING", + "primaryKey": false, + "defaultValue": null, + "isRequired": false + } + ], + "references": [], + "primaryKeys": ["id"], + "options": { + "hasIdColumn": false, + "hasPrimaryKeys": true, + "isJunction": false, + "timestamps": false, + "underscored": false + } + } +} diff --git a/test-expected/sequelize/db-analysis-output/parenthesis_underscored.expected.json b/test-expected/sequelize/db-analysis-output/parenthesis_underscored.expected.json new file mode 100644 index 00000000..a5fae78b --- /dev/null +++ b/test-expected/sequelize/db-analysis-output/parenthesis_underscored.expected.json @@ -0,0 +1,39 @@ +{ + "parenthesis_underscored": { + "fields": [ + { + "name": "id", + "nameColumn": "id", + "type": "INTEGER", + "primaryKey": true, + "defaultValue": null, + "isRequired": true + }, + { + "name": "Ingredients (Kcal/100g)", + "nameColumn": "Ingredients (Kcal/100g)", + "type": "STRING", + "primaryKey": false, + "defaultValue": null, + "isRequired": false + }, + { + "name": "ingredientWeight", + "nameColumn": "ingredient_weight", + "type": "INTEGER", + "primaryKey": false, + "defaultValue": null, + "isRequired": true + } + ], + "references": [], + "primaryKeys": ["id"], + "options": { + "hasIdColumn": false, + "hasPrimaryKeys": true, + "isJunction": false, + "timestamps": false, + "underscored": false + } + } +} diff --git a/test-expected/sequelize/db-analysis-output/parenthesis_underscored_true.expected.json b/test-expected/sequelize/db-analysis-output/parenthesis_underscored_true.expected.json new file mode 100644 index 00000000..56e3bbc6 --- /dev/null +++ b/test-expected/sequelize/db-analysis-output/parenthesis_underscored_true.expected.json @@ -0,0 +1,39 @@ +{ + "parenthesis_underscored_true": { + "fields": [ + { + "name": "id", + "nameColumn": "id", + "type": "INTEGER", + "primaryKey": true, + "defaultValue": null, + "isRequired": true + }, + { + "name": "Ingredients (Kcal/100g)", + "nameColumn": "Ingredients (Kcal/100g)", + "type": "STRING", + "primaryKey": false, + "defaultValue": null, + "isRequired": false + }, + { + "name": "ingredientWeight", + "nameColumn": "ingredient_weight", + "type": "INTEGER", + "primaryKey": false, + "defaultValue": null, + "isRequired": true + } + ], + "references": [], + "primaryKeys": ["id"], + "options": { + "hasIdColumn": false, + "hasPrimaryKeys": true, + "isJunction": false, + "timestamps": false, + "underscored": true + } + } +} diff --git a/test-expected/sequelize/dumper-output/parenthesis.expected.js b/test-expected/sequelize/dumper-output/parenthesis.expected.js new file mode 100644 index 00000000..f388571f --- /dev/null +++ b/test-expected/sequelize/dumper-output/parenthesis.expected.js @@ -0,0 +1,27 @@ +// This model was generated by Lumber. However, you remain in control of your models. +// Learn how here: https://docs.forestadmin.com/documentation/v/v6/reference-guide/models/enrich-your-models +module.exports = (sequelize, DataTypes) => { + const { Sequelize } = sequelize; + // This section contains the fields of your model, mapped to your table's columns. + // Learn more here: https://docs.forestadmin.com/documentation/v/v6/reference-guide/models/enrich-your-models#declaring-a-new-field-in-a-model + const Parenthesis = sequelize.define('parenthesis', { + id: { + type: DataTypes.INTEGER, + primaryKey: true, + allowNull: false, + }, + 'Ingredients (Kcal/100g)': { + type: DataTypes.STRING, + }, + }, { + tableName: 'parenthesis', + timestamps: false, + schema: process.env.DATABASE_SCHEMA, + }); + + // This section contains the relationships for this model. See: https://docs.forestadmin.com/documentation/v/v6/reference-guide/relationships#adding-relationships. + Parenthesis.associate = (models) => { + }; + + return Parenthesis; +}; diff --git a/test-expected/sequelize/dumper-output/parenthesis_underscored.expected.js b/test-expected/sequelize/dumper-output/parenthesis_underscored.expected.js new file mode 100644 index 00000000..77e2647f --- /dev/null +++ b/test-expected/sequelize/dumper-output/parenthesis_underscored.expected.js @@ -0,0 +1,32 @@ +// This model was generated by Lumber. However, you remain in control of your models. +// Learn how here: https://docs.forestadmin.com/documentation/v/v6/reference-guide/models/enrich-your-models +module.exports = (sequelize, DataTypes) => { + const { Sequelize } = sequelize; + // This section contains the fields of your model, mapped to your table's columns. + // Learn more here: https://docs.forestadmin.com/documentation/v/v6/reference-guide/models/enrich-your-models#declaring-a-new-field-in-a-model + const ParenthesisUnderscored = sequelize.define('parenthesisUnderscored', { + id: { + type: DataTypes.INTEGER, + primaryKey: true, + allowNull: false, + }, + 'Ingredients (Kcal/100g)': { + type: DataTypes.STRING, + }, + ingredientWeight: { + type: DataTypes.INTEGER, + field: 'ingredient_weight', + allowNull: false, + }, + }, { + tableName: 'parenthesis_underscored', + timestamps: false, + schema: process.env.DATABASE_SCHEMA, + }); + + // This section contains the relationships for this model. See: https://docs.forestadmin.com/documentation/v/v6/reference-guide/relationships#adding-relationships. + ParenthesisUnderscored.associate = (models) => { + }; + + return ParenthesisUnderscored; +}; diff --git a/test-expected/sequelize/dumper-output/parenthesis_underscored_true.expected.js b/test-expected/sequelize/dumper-output/parenthesis_underscored_true.expected.js new file mode 100644 index 00000000..7c4eeed2 --- /dev/null +++ b/test-expected/sequelize/dumper-output/parenthesis_underscored_true.expected.js @@ -0,0 +1,33 @@ +// This model was generated by Lumber. However, you remain in control of your models. +// Learn how here: https://docs.forestadmin.com/documentation/v/v6/reference-guide/models/enrich-your-models +module.exports = (sequelize, DataTypes) => { + const { Sequelize } = sequelize; + // This section contains the fields of your model, mapped to your table's columns. + // Learn more here: https://docs.forestadmin.com/documentation/v/v6/reference-guide/models/enrich-your-models#declaring-a-new-field-in-a-model + const ParenthesisUnderscoredTrue = sequelize.define('parenthesisUnderscoredTrue', { + id: { + type: DataTypes.INTEGER, + primaryKey: true, + allowNull: false, + }, + 'Ingredients (Kcal/100g)': { + type: DataTypes.STRING, + field: 'Ingredients (Kcal/100g)', + }, + ingredientWeight: { + type: DataTypes.INTEGER, + allowNull: false, + }, + }, { + tableName: 'parenthesis_underscored_true', + underscored: true, + timestamps: false, + schema: process.env.DATABASE_SCHEMA, + }); + + // This section contains the relationships for this model. See: https://docs.forestadmin.com/documentation/v/v6/reference-guide/relationships#adding-relationships. + ParenthesisUnderscoredTrue.associate = (models) => { + }; + + return ParenthesisUnderscoredTrue; +}; diff --git a/test-fixtures/mssql/parenthesis_table.sql b/test-fixtures/mssql/parenthesis_table.sql new file mode 100644 index 00000000..d8dfdfd9 --- /dev/null +++ b/test-fixtures/mssql/parenthesis_table.sql @@ -0,0 +1,5 @@ +CREATE TABLE [dbo].parenthesis_table ( + id INT NOT NULL, + [Ingredients (Kcal/100g)] VARCHAR(100) , + PRIMARY KEY (id) +); diff --git a/test-fixtures/mssql/parenthesis_underscored_table.sql b/test-fixtures/mssql/parenthesis_underscored_table.sql new file mode 100644 index 00000000..54481bf6 --- /dev/null +++ b/test-fixtures/mssql/parenthesis_underscored_table.sql @@ -0,0 +1,6 @@ +CREATE TABLE [dbo].parenthesis_underscored_table ( + id INT NOT NULL, + [Ingredients (Kcal/100g)] VARCHAR(100), + ingredient_weight INT NOT NULL, + PRIMARY KEY (id) +); diff --git a/test-fixtures/mysql/parenthesis_table.sql b/test-fixtures/mysql/parenthesis_table.sql new file mode 100644 index 00000000..4ff277a8 --- /dev/null +++ b/test-fixtures/mysql/parenthesis_table.sql @@ -0,0 +1,5 @@ +CREATE TABLE parenthesis_table ( + id INT NOT NULL, + `Ingredients (Kcal/100g)` VARCHAR(100), + PRIMARY KEY (id) +); diff --git a/test-fixtures/mysql/parenthesis_underscored_table.sql b/test-fixtures/mysql/parenthesis_underscored_table.sql new file mode 100644 index 00000000..874882bf --- /dev/null +++ b/test-fixtures/mysql/parenthesis_underscored_table.sql @@ -0,0 +1,6 @@ +CREATE TABLE parenthesis_underscored_table ( + id INT NOT NULL, + `Ingredients (Kcal/100g)` VARCHAR(100), + ingredient_weight INT NOT NULL, + PRIMARY KEY (id) +); diff --git a/test-fixtures/postgres/parenthesis_table.sql b/test-fixtures/postgres/parenthesis_table.sql new file mode 100644 index 00000000..3d2de76a --- /dev/null +++ b/test-fixtures/postgres/parenthesis_table.sql @@ -0,0 +1,5 @@ +CREATE TABLE parenthesis_table ( + id INT NOT NULL, + "Ingredients (Kcal/100g)" VARCHAR, + PRIMARY KEY (id) +); diff --git a/test-fixtures/postgres/parenthesis_underscored_table.sql b/test-fixtures/postgres/parenthesis_underscored_table.sql new file mode 100644 index 00000000..a2c73a7f --- /dev/null +++ b/test-fixtures/postgres/parenthesis_underscored_table.sql @@ -0,0 +1,6 @@ +CREATE TABLE parenthesis_underscored_table ( + id INT NOT NULL, + "Ingredients (Kcal/100g)" VARCHAR, + ingredient_weight INT NOT NULL, + PRIMARY KEY (id) +); diff --git a/test/services/database-analyzer/database-analyzer-sequelize.test.js b/test/services/database-analyzer/database-analyzer-sequelize.test.js index 7a6788be..4a51c187 100644 --- a/test/services/database-analyzer/database-analyzer-sequelize.test.js +++ b/test/services/database-analyzer/database-analyzer-sequelize.test.js @@ -89,6 +89,26 @@ describe('services > database analyser > Sequelize', () => { expect(result.underscored_no_fields.options.underscored).toStrictEqual(true); }, TIMEOUT); + it('should not set underscored to true if parenthesis in column name', async () => { + expect.assertions(1); + const sequelizeHelper = new SequelizeHelper(); + const databaseConnection = await sequelizeHelper.connect(connectionUrl); + await sequelizeHelper.dropAndCreate('parenthesis_table'); + const result = await performDatabaseAnalysis(databaseConnection); + await sequelizeHelper.close(); + expect(result.parenthesis_table.options.underscored).toStrictEqual(false); + }, TIMEOUT); + + it('should not set underscored to true if parenthesis in column name and underscored field', async () => { + expect.assertions(1); + const sequelizeHelper = new SequelizeHelper(); + const databaseConnection = await sequelizeHelper.connect(connectionUrl); + await sequelizeHelper.dropAndCreate('parenthesis_underscored_table'); + const result = await performDatabaseAnalysis(databaseConnection); + await sequelizeHelper.close(); + expect(result.parenthesis_underscored_table.options.underscored).toStrictEqual(false); + }, TIMEOUT); + it('should handle conflicts between references alias', async () => { expect.assertions(3); const sequelizeHelper = new SequelizeHelper(); diff --git a/test/services/dumper/dumper-sequelize.test.js b/test/services/dumper/dumper-sequelize.test.js index 41920069..da45883a 100644 --- a/test/services/dumper/dumper-sequelize.test.js +++ b/test/services/dumper/dumper-sequelize.test.js @@ -7,6 +7,9 @@ const belongsToModel = require('../../../test-expected/sequelize/db-analysis-out const otherAssociationsModel = require('../../../test-expected/sequelize/db-analysis-output/users.expected.json'); const exportModel = require('../../../test-expected/sequelize/db-analysis-output/export.expected.json'); const defaultValuesModel = require('../../../test-expected/sequelize/db-analysis-output/default-values.expected.js'); +const parenthesisColumnName = require('../../../test-expected/sequelize/db-analysis-output/parenthesis.expected.json'); +const parenthesisColumnNameUnderscored = require('../../../test-expected/sequelize/db-analysis-output/parenthesis_underscored.expected.json'); +const parenthesisColumnNameUnderscoredTrue = require('../../../test-expected/sequelize/db-analysis-output/parenthesis_underscored_true.expected.json'); const Dumper = require('../../../services/dumper'); @@ -50,6 +53,39 @@ describe('services > dumper > sequelize', () => { cleanOutput(); }); + it('should generate a model file with correct parenthesis field', async () => { + expect.assertions(1); + const dumper = getDumper(); + await dumper.dump(parenthesisColumnName); + const generatedFile = fs.readFileSync('./test-output/sequelize/models/parenthesis.js', 'utf8'); + const expectedFile = fs.readFileSync('./test-expected/sequelize/dumper-output/parenthesis.expected.js', 'utf-8'); + + expect(generatedFile).toStrictEqual(expectedFile); + cleanOutput(); + }); + + it('should generate a model file with correct parenthesis field and correct underscored fields', async () => { + expect.assertions(1); + const dumper = getDumper(); + await dumper.dump(parenthesisColumnNameUnderscored); + const generatedFile = fs.readFileSync('./test-output/sequelize/models/parenthesis-underscored.js', 'utf8'); + const expectedFile = fs.readFileSync('./test-expected/sequelize/dumper-output/parenthesis_underscored.expected.js', 'utf-8'); + + expect(generatedFile).toStrictEqual(expectedFile); + cleanOutput(); + }); + + it('should generate a model file with correct parenthesis field and underscored true', async () => { + expect.assertions(1); + const dumper = getDumper(); + await dumper.dump(parenthesisColumnNameUnderscoredTrue); + const generatedFile = fs.readFileSync('./test-output/sequelize/models/parenthesis-underscored-true.js', 'utf8'); + const expectedFile = fs.readFileSync('./test-expected/sequelize/dumper-output/parenthesis_underscored_true.expected.js', 'utf-8'); + + expect(generatedFile).toStrictEqual(expectedFile); + cleanOutput(); + }); + it('should generate a model file with hasMany, hasOne and belongsToMany', async () => { expect.assertions(1); const dumper = getDumper(); From d5cf38bf711a607a921bca16edffa285870e54f6 Mon Sep 17 00:00:00 2001 From: Forest Date: Mon, 21 Dec 2020 07:49:53 +0000 Subject: [PATCH 2/5] chore(release): 3.10.5 [skip ci] ## [3.10.5](https://github.com/ForestAdmin/lumber/compare/v3.10.4...v3.10.5) (2020-12-21) ### Bug Fixes * **sequelize:** fix error when working with column table including parenthesis ([#520](https://github.com/ForestAdmin/lumber/issues/520)) ([79c041b](https://github.com/ForestAdmin/lumber/commit/79c041b8ca6a78db198e71a4b0ec1dbba224acbd)) --- CHANGELOG.md | 7 +++++++ package.json | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 96302d05..524d624a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +## [3.10.5](https://github.com/ForestAdmin/lumber/compare/v3.10.4...v3.10.5) (2020-12-21) + + +### Bug Fixes + +* **sequelize:** fix error when working with column table including parenthesis ([#520](https://github.com/ForestAdmin/lumber/issues/520)) ([79c041b](https://github.com/ForestAdmin/lumber/commit/79c041b8ca6a78db198e71a4b0ec1dbba224acbd)) + ## [3.10.4](https://github.com/ForestAdmin/lumber/compare/v3.10.3...v3.10.4) (2020-12-01) diff --git a/package.json b/package.json index 760f76a7..fb615e1b 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "lumber-cli", "description": "Create your Forest Admin API in minutes. Admin API backend based on a database schema", - "version": "3.10.4", + "version": "3.10.5", "main": "lumber.js", "scripts": { "lint": "./node_modules/eslint/bin/eslint.js ./*.js .eslint-bin deserializers serializers services test utils", From dd675caabee25cb2f5e6daf7b8c7e7b89c5aff36 Mon Sep 17 00:00:00 2001 From: Arnaud Besnier Date: Mon, 21 Dec 2020 16:07:48 +0100 Subject: [PATCH 3/5] chore(coverage): fix code climate coverage reporting (#523) --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 9fd33149..7d9690ab 100644 --- a/.travis.yml +++ b/.travis.yml @@ -24,7 +24,7 @@ jobs: # NOTICE: Handles code coverage reporting to Code Climate before_install: - docker-compose up -d - - curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 > ./cc-test-reporter + - curl -L $CC_TEST_REPORTER_URL > ./cc-test-reporter - chmod +x ./cc-test-reporter after_script: - ./cc-test-reporter after-build --exit-code $TRAVIS_TEST_RESULT From ec486c1119e9f611d113456fa0a03c5a67ddebde Mon Sep 17 00:00:00 2001 From: Jeff LADIRAY Date: Mon, 4 Jan 2021 15:05:48 +0100 Subject: [PATCH 4/5] refactor(dumper): update mkdirp to 1.0.4 to remove bluebird dependency in dumper (#526) --- package.json | 2 +- services/dumper.js | 7 ++----- yarn.lock | 5 +++++ 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index fb615e1b..029412c4 100644 --- a/package.json +++ b/package.json @@ -33,7 +33,7 @@ "inquirer": "^6.2.0", "jsonapi-serializer": "^3.4.1", "lodash": "4.17.19", - "mkdirp": "^0.5.1", + "mkdirp": "^1.0.4", "mongodb": "3.6.3", "mysql2": "2.2.5", "pg": "8.2.1", diff --git a/services/dumper.js b/services/dumper.js index 79ae450b..2204ef07 100755 --- a/services/dumper.js +++ b/services/dumper.js @@ -1,8 +1,7 @@ -const P = require('bluebird'); const fs = require('fs'); const os = require('os'); const _ = require('lodash'); -const mkdirpSync = require('mkdirp'); +const mkdirp = require('mkdirp'); const Handlebars = require('handlebars'); const chalk = require('chalk'); const { plural, singular } = require('pluralize'); @@ -12,8 +11,6 @@ const logger = require('./logger'); const toValidPackageName = require('../utils/to-valid-package-name'); require('../handlerbars/loader'); -const mkdirp = P.promisify(mkdirpSync); - const DEFAULT_PORT = 3310; const DEFAULT_VALUE_TYPES_TO_STRINGIFY = [ @@ -356,7 +353,7 @@ class Dumper { mkdirp(this.modelsPath), ]; - await P.all(directories); + await Promise.all(directories); const modelNames = Object.keys(schema) .sort((a, b) => a.localeCompare(b, undefined, { sensitivity: 'base' })); diff --git a/yarn.lock b/yarn.lock index ddb754b7..1593e699 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5813,6 +5813,11 @@ mkdirp@^0.5.0, mkdirp@^0.5.1, mkdirp@^0.5.5, mkdirp@~0.5.0: dependencies: minimist "^1.2.5" +mkdirp@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e" + integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw== + modify-values@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/modify-values/-/modify-values-1.0.1.tgz#b3939fa605546474e3e3e3c63d64bd43b4ee6022" From 27a9573c2e1c8e467b31e7c9dce47b7d60ec900b Mon Sep 17 00:00:00 2001 From: Jeff LADIRAY Date: Tue, 5 Jan 2021 10:16:56 +0100 Subject: [PATCH 5/5] refactor(dumper): refactor dumper to use dependency injection (#528) --- context/init.js | 9 + lumber-generate.js | 7 +- services/dumper.js | 291 +++++----- test/services/dumper/dumper-mongo.test.js | 30 +- test/services/dumper/dumper-sequelize.test.js | 48 +- test/services/dumper/dumper-sql.test.js | 18 +- test/services/dumper/dumper.test.js | 48 +- test/services/dumper/dumper.unit.test.js | 539 ++++++++++++++++++ utils/strings.js | 4 + 9 files changed, 772 insertions(+), 222 deletions(-) create mode 100644 test/services/dumper/dumper.unit.test.js diff --git a/context/init.js b/context/init.js index e3ca582e..f2ade41e 100644 --- a/context/init.js +++ b/context/init.js @@ -4,7 +4,10 @@ const chalk = require('chalk'); const fs = require('fs'); const os = require('os'); const inquirer = require('inquirer'); +const mkdirp = require('mkdirp'); +const Handlebars = require('handlebars'); const Database = require('../services/database'); +const Dumper = require('../services/dumper'); const logger = require('../services/logger'); const terminator = require('../utils/terminator'); const Api = require('../services/api'); @@ -25,8 +28,10 @@ const authenticatorHelper = require('../utils/authenticator-helper'); * os: import('os'); * chalk: import('chalk'); * inquirer: import('inquirer'); + * mkdirp: import('mkdirp'); * mongodb: import('mongodb'); * Sequelize: import('sequelize'); + * Handlebars: import('handlebars'); * }} Dependencies * * @typedef {{ @@ -37,6 +42,7 @@ const authenticatorHelper = require('../utils/authenticator-helper'); * @typedef {{ * logger: import('../services/logger'); * database: import('../services/database'); + * dumper: import('../services/dumper'); * api: import('../services/api'); * authenticator: import('../services/authenticator'); * }} Services @@ -62,8 +68,10 @@ function initDependencies(context) { context.addInstance('os', os); context.addInstance('chalk', chalk); context.addInstance('inquirer', inquirer); + context.addInstance('mkdirp', mkdirp); context.addInstance('Sequelize', Sequelize); context.addInstance('mongodb', mongodb); + context.addInstance('Handlebars', Handlebars); } /** @@ -80,6 +88,7 @@ function initUtils(context) { function initServices(context) { context.addInstance('logger', logger); context.addClass(Database); + context.addClass(Dumper); context.addClass(Api); context.addClass(Authenticator); } diff --git a/lumber-generate.js b/lumber-generate.js index 4b8016c4..6ca7f16d 100755 --- a/lumber-generate.js +++ b/lumber-generate.js @@ -7,17 +7,17 @@ initContext(context); const spinners = require('./services/spinners'); const DatabaseAnalyzer = require('./services/analyzer/database-analyzer'); -const Dumper = require('./services/dumper'); const CommandGenerateConfigGetter = require('./services/command-generate-config-getter'); const eventSender = require('./services/event-sender'); const ProjectCreator = require('./services/project-creator'); const { terminate } = require('./utils/terminator'); const { ERROR_UNEXPECTED } = require('./utils/messages'); -const { logger, authenticator } = context.inject(); +const { logger, authenticator, dumper } = context.inject(); if (!authenticator) throw new Error('Missing dependency authenticator'); if (!logger) throw new Error('Missing dependency logger'); +if (!dumper) throw new Error('Missing dependency dumper'); program .description('Generate a backend application with an ORM/ODM configured') @@ -62,8 +62,7 @@ program const spinner = spinners.add('dumper', { text: 'Creating your project files' }); logger.spinner = spinner; - const dumper = new Dumper(config); - await dumper.dump(schema); + await dumper.dump(schema, config); spinner.succeed(); logger.success(`Hooray, ${chalk.green('installation success')}!`); diff --git a/services/dumper.js b/services/dumper.js index 2204ef07..c31f80c0 100755 --- a/services/dumper.js +++ b/services/dumper.js @@ -1,73 +1,76 @@ -const fs = require('fs'); -const os = require('os'); const _ = require('lodash'); -const mkdirp = require('mkdirp'); -const Handlebars = require('handlebars'); -const chalk = require('chalk'); const { plural, singular } = require('pluralize'); -const Sequelize = require('sequelize'); const stringUtils = require('../utils/strings'); -const logger = require('./logger'); const toValidPackageName = require('../utils/to-valid-package-name'); require('../handlerbars/loader'); const DEFAULT_PORT = 3310; - -const DEFAULT_VALUE_TYPES_TO_STRINGIFY = [ - `${Sequelize.DataTypes.ARRAY}`, - `${Sequelize.DataTypes.CITEXT}`, - `${Sequelize.DataTypes.DATE}`, - `${Sequelize.DataTypes.ENUM}`, - `${Sequelize.DataTypes.JSONB}`, - `${Sequelize.DataTypes.STRING}`, - `${Sequelize.DataTypes.TEXT}`, - `${Sequelize.DataTypes.UUID}`, -]; - class Dumper { - constructor(config) { - this.config = config; - - this.path = `${process.cwd()}/${this.config.appName}`; - this.routesPath = `${this.path}/routes`; - this.forestPath = `${this.path}/forest`; - this.publicPath = `${this.path}/public`; - this.viewPath = `${this.path}/views`; - this.modelsPath = `${this.path}/models`; - this.middlewaresPath = `${this.path}/middlewares`; + constructor({ + fs, + chalk, + env, + os, + Sequelize, + Handlebars, + logger, + mkdirp, + }) { + this.fs = fs; + this.chalk = chalk; + this.env = env; + this.os = os; + this.Sequelize = Sequelize; + this.Handlebars = Handlebars; + this.logger = logger; + this.mkdirp = mkdirp; + + this.DEFAULT_VALUE_TYPES_TO_STRINGIFY = [ + `${Sequelize.DataTypes.ARRAY}`, + `${Sequelize.DataTypes.CITEXT}`, + `${Sequelize.DataTypes.DATE}`, + `${Sequelize.DataTypes.ENUM}`, + `${Sequelize.DataTypes.JSONB}`, + `${Sequelize.DataTypes.STRING}`, + `${Sequelize.DataTypes.TEXT}`, + `${Sequelize.DataTypes.UUID}`, + ]; } - static isLinuxBasedOs() { - return os.platform() === 'linux'; + isLinuxBasedOs() { + return this.os.platform() === 'linux'; } - writeFile(filePath, content) { - fs.writeFileSync(filePath, content); - logger.log(` ${chalk.green('create')} ${filePath.substring(this.path.length + 1)}`); + writeFile(absoluteProjectPath, relativeFilePath, content) { + this.fs.writeFileSync(`${absoluteProjectPath}/${relativeFilePath}`, content); + this.logger.log(` ${this.chalk.green('create')} ${relativeFilePath}`); } - copyTemplate(from, to) { - const newFrom = `${__dirname}/../templates/app/${from}`; - this.writeFile(to, fs.readFileSync(newFrom, 'utf-8')); + copyTemplate(absoluteProjectPath, relativeFromPath, relativeToPath) { + const newFrom = `${__dirname}/../templates/app/${relativeFromPath}`; + this.writeFile(absoluteProjectPath, relativeToPath, this.fs.readFileSync(newFrom, 'utf-8')); } - copyHandleBarsTemplate({ source, target, context }) { - function handlebarsTemplate(templatePath) { - return Handlebars.compile( - fs.readFileSync(`${__dirname}/../templates/${templatePath}`, 'utf-8'), - { noEscape: true }, - ); + copyHandleBarsTemplate({ + projectPath, + source, + target, + context, + }) { + const handlebarsTemplate = (templatePath) => this.Handlebars.compile( + this.fs.readFileSync(`${__dirname}/../templates/${templatePath}`, 'utf-8'), + { noEscape: true }, + ); + + if (!(source && target && context && projectPath)) { + throw new Error('Missing argument (projectPath, source, target or context).'); } - if (!(source && target && context)) { - throw new Error('Missing argument (source, target or context).'); - } - - this.writeFile(`${this.path}/${target}`, handlebarsTemplate(source)(context)); + this.writeFile(projectPath, target, handlebarsTemplate(source)(context)); } - writePackageJson() { - const orm = this.config.dbDialect === 'mongodb' ? 'mongoose' : 'sequelize'; + writePackageJson(projectPath, { dbDialect, appName }) { + const orm = dbDialect === 'mongodb' ? 'mongoose' : 'sequelize'; const dependencies = { 'body-parser': '1.19.0', chalk: '~1.1.3', @@ -83,62 +86,62 @@ class Dumper { sequelize: '~5.15.1', }; - if (this.config.dbDialect) { - if (this.config.dbDialect.includes('postgres')) { + if (dbDialect) { + if (dbDialect.includes('postgres')) { dependencies.pg = '~8.2.2'; - } else if (this.config.dbDialect === 'mysql') { + } else if (dbDialect === 'mysql') { dependencies.mysql2 = '~2.2.5'; - } else if (this.config.dbDialect === 'mssql') { + } else if (dbDialect === 'mssql') { dependencies.tedious = '^6.4.0'; - } else if (this.config.dbDialect === 'mongodb') { + } else if (dbDialect === 'mongodb') { delete dependencies.sequelize; dependencies.mongoose = '~5.8.2'; } } const pkg = { - name: toValidPackageName(this.config.appName), + name: toValidPackageName(appName), version: '0.0.1', private: true, scripts: { start: 'node ./server.js' }, dependencies, }; - this.writeFile(`${this.path}/package.json`, `${JSON.stringify(pkg, null, 2)}\n`); + this.writeFile(projectPath, 'package.json', `${JSON.stringify(pkg, null, 2)}\n`); } static tableToFilename(table) { return _.kebabCase(table); } - getDatabaseUrl() { + static getDatabaseUrl(config) { let connectionString; - if (this.config.dbConnectionUrl) { - connectionString = this.config.dbConnectionUrl; + if (config.dbConnectionUrl) { + connectionString = config.dbConnectionUrl; } else { - let protocol = this.config.dbDialect; - let port = `:${this.config.dbPort}`; + let protocol = config.dbDialect; + let port = `:${config.dbPort}`; let password = ''; - if (this.config.dbDialect === 'mongodb' && this.config.mongodbSrv) { + if (config.dbDialect === 'mongodb' && config.mongodbSrv) { protocol = 'mongodb+srv'; port = ''; } - if (this.config.dbPassword) { + if (config.dbPassword) { // NOTICE: Encode password string in case of special chars. - password = `:${encodeURIComponent(this.config.dbPassword)}`; + password = `:${encodeURIComponent(config.dbPassword)}`; } - connectionString = `${protocol}://${this.config.dbUser}${password}@${this.config.dbHostname}${port}/${this.config.dbName}`; + connectionString = `${protocol}://${config.dbUser}${password}@${config.dbHostname}${port}/${config.dbName}`; } return connectionString; } - isDatabaseLocal() { - const databaseUrl = this.getDatabaseUrl(); + static isDatabaseLocal(config) { + const databaseUrl = Dumper.getDatabaseUrl(config); return databaseUrl.includes('127.0.0.1') || databaseUrl.includes('localhost'); } @@ -146,37 +149,39 @@ class Dumper { return /^http:\/\/(?:localhost|127\.0\.0\.1)$/.test(url); } - getPort() { - return this.config.appPort || DEFAULT_PORT; + static getPort(config) { + return config.appPort || DEFAULT_PORT; } - getApplicationUrl() { - const hostUrl = /^https?:\/\//.test(this.config.appHostname) - ? this.config.appHostname - : `http://${this.config.appHostname}`; + static getApplicationUrl(config) { + const hostUrl = /^https?:\/\//.test(config.appHostname) + ? config.appHostname + : `http://${config.appHostname}`; return Dumper.isLocalUrl(hostUrl) - ? `${hostUrl}:${this.getPort()}` + ? `${hostUrl}:${Dumper.getPort(config)}` : hostUrl; } - writeDotEnv() { + writeDotEnv(projectPath, config) { + const databaseUrl = Dumper.getDatabaseUrl(config); const context = { - databaseUrl: this.getDatabaseUrl(), - ssl: this.config.ssl || 'false', - dbSchema: this.config.dbSchema, - hostname: this.config.appHostname, - port: this.getPort(), - forestEnvSecret: this.config.forestEnvSecret, - forestAuthSecret: this.config.forestAuthSecret, + databaseUrl, + ssl: config.ssl || 'false', + dbSchema: config.dbSchema, + hostname: config.appHostname, + port: Dumper.getPort(config), + forestEnvSecret: config.forestEnvSecret, + forestAuthSecret: config.forestAuthSecret, hasDockerDatabaseUrl: false, - applicationUrl: this.getApplicationUrl(), + applicationUrl: Dumper.getApplicationUrl(config), }; - if (!Dumper.isLinuxBasedOs()) { - context.dockerDatabaseUrl = this.getDatabaseUrl().replace('localhost', 'host.docker.internal'); + if (!this.isLinuxBasedOs()) { + context.dockerDatabaseUrl = databaseUrl.replace('localhost', 'host.docker.internal'); context.hasDockerDatabaseUrl = true; } this.copyHandleBarsTemplate({ + projectPath, source: 'app/env.hbs', target: '.env', context, @@ -184,19 +189,19 @@ class Dumper { } static getModelNameFromTableName(table) { - return stringUtils.camelCase(stringUtils.transformToSafeString(table)); + return stringUtils.transformToCamelCaseSafeString(table); } - getSafeDefaultValue(field) { + getSafeDefaultValue(dbDialect, field) { // NOTICE: in case of SQL dialect, ensure default value is directly usable in template // as a JS value. let safeDefaultValue = field.defaultValue; - if (this.config.dbDialect !== 'mongodb') { - if (typeof safeDefaultValue === 'object' && safeDefaultValue instanceof Sequelize.Utils.Literal) { + if (dbDialect !== 'mongodb') { + if (typeof safeDefaultValue === 'object' && safeDefaultValue instanceof this.Sequelize.Utils.Literal) { safeDefaultValue = `Sequelize.literal('${safeDefaultValue.val}')`; } else if (!_.isNil(safeDefaultValue)) { if (_.some( - DEFAULT_VALUE_TYPES_TO_STRINGIFY, + this.DEFAULT_VALUE_TYPES_TO_STRINGIFY, // NOTICE: Uses `startsWith` as composite types may vary (eg: `ARRAY(DataTypes.INTEGER)`) (dataType) => _.startsWith(field.type, dataType), )) { @@ -209,7 +214,7 @@ class Dumper { return safeDefaultValue; } - writeModel(table, fields, references, options = {}) { + writeModel(projectPath, config, table, fields, references, options = {}) { const { underscored } = options; const fieldsDefinition = fields.map((field) => { @@ -220,7 +225,7 @@ class Dumper { const hasParenthesis = field.nameColumn && (field.nameColumn.includes('(') || field.nameColumn.includes(')')); const nameColumnUnconventional = field.nameColumn !== expectedConventionalColumnName || (underscored && (/[1-9]/g.test(field.name) || hasParenthesis)); - const safeDefaultValue = this.getSafeDefaultValue(field); + const safeDefaultValue = this.getSafeDefaultValue(config.dbDialect, field); return { ...field, @@ -240,7 +245,8 @@ class Dumper { })); this.copyHandleBarsTemplate({ - source: `app/models/${this.config.dbDialect === 'mongodb' ? 'mongo' : 'sequelize'}-model.hbs`, + projectPath, + source: `app/models/${config.dbDialect === 'mongodb' ? 'mongo' : 'sequelize'}-model.hbs`, target: `models/${Dumper.tableToFilename(table)}.js`, context: { modelName: Dumper.getModelNameFromTableName(table), @@ -249,18 +255,19 @@ class Dumper { fields: fieldsDefinition, references: referencesDefinition, ...options, - schema: this.config.dbSchema, - dialect: this.config.dbDialect, + schema: config.dbSchema, + dialect: config.dbDialect, noId: !options.hasIdColumn && !options.hasPrimaryKeys, }, }); } - writeRoute(modelName) { + writeRoute(projectPath, config, modelName) { const modelNameDasherized = _.kebabCase(modelName); const readableModelName = _.startCase(modelName); this.copyHandleBarsTemplate({ + projectPath, source: 'app/routes/route.hbs', target: `routes/${Dumper.tableToFilename(modelName)}.js`, context: { @@ -268,37 +275,40 @@ class Dumper { modelNameDasherized, modelNameReadablePlural: plural(readableModelName), modelNameReadableSingular: singular(readableModelName), - isMongoDB: this.config.dbDialect === 'mongodb', + isMongoDB: config.dbDialect === 'mongodb', }, }); } - writeForestCollection(table) { + writeForestCollection(projectPath, config, table) { this.copyHandleBarsTemplate({ + projectPath, source: 'app/forest/collection.hbs', target: `forest/${Dumper.tableToFilename(table)}.js`, context: { - isMongoDB: this.config.dbDialect === 'mongodb', + isMongoDB: config.dbDialect === 'mongodb', table: Dumper.getModelNameFromTableName(table), }, }); } - writeAppJs() { + writeAppJs(projectPath, config) { this.copyHandleBarsTemplate({ + projectPath, source: 'app/app.hbs', target: 'app.js', context: { - isMongoDB: this.config.dbDialect === 'mongodb', - forestUrl: process.env.FOREST_URL, + isMongoDB: config.dbDialect === 'mongodb', + forestUrl: this.env.FOREST_URL, }, }); } - writeModelsIndex() { - const { dbDialect } = this.config; + writeModelsIndex(projectPath, config) { + const { dbDialect } = config; this.copyHandleBarsTemplate({ + projectPath, source: 'app/models/index.hbs', target: 'models/index.js', context: { @@ -309,48 +319,53 @@ class Dumper { }); } - writeDockerfile() { + writeDockerfile(projectPath) { this.copyHandleBarsTemplate({ + projectPath, source: 'app/Dockerfile.hbs', target: 'Dockerfile', context: {}, }); } - writeDockerCompose() { - const databaseUrl = `\${${Dumper.isLinuxBasedOs() ? 'DATABASE_URL' : 'DOCKER_DATABASE_URL'}}`; - const forestUrl = process.env.FOREST_URL ? `\${FOREST_URL-${process.env.FOREST_URL}}` : false; + writeDockerCompose(projectPath, config) { + const databaseUrl = `\${${this.isLinuxBasedOs() ? 'DATABASE_URL' : 'DOCKER_DATABASE_URL'}}`; + const forestUrl = this.env.FOREST_URL ? `\${FOREST_URL-${this.env.FOREST_URL}}` : false; this.copyHandleBarsTemplate({ + projectPath, source: 'app/docker-compose.hbs', target: 'docker-compose.yml', context: { - containerName: _.snakeCase(this.config.appName), + containerName: _.snakeCase(config.appName), databaseUrl, - dbSchema: this.config.dbSchema, + dbSchema: config.dbSchema, forestUrl, - network: (Dumper.isLinuxBasedOs() && this.isDatabaseLocal()) ? 'host' : null, + network: (this.isLinuxBasedOs() && Dumper.isDatabaseLocal(config)) ? 'host' : null, }, }); } - writeForestAdminMiddleware() { + writeForestAdminMiddleware(projectPath, config) { this.copyHandleBarsTemplate({ + projectPath, source: 'app/middlewares/forestadmin.hbs', target: 'middlewares/forestadmin.js', - context: { isMongoDB: this.config.dbDialect === 'mongodb' }, + context: { isMongoDB: config.dbDialect === 'mongodb' }, }); } // NOTICE: Generate files in alphabetical order to ensure a nice generation console logs display. - async dump(schema) { + async dump(schema, config) { + const projectPath = `${process.cwd()}/${config.appName}`; + const directories = [ - mkdirp(this.path), - mkdirp(this.routesPath), - mkdirp(this.forestPath), - mkdirp(this.viewPath), - mkdirp(this.publicPath), - mkdirp(this.middlewaresPath), - mkdirp(this.modelsPath), + this.mkdirp(projectPath), + this.mkdirp(`${projectPath}/routes`), + this.mkdirp(`${projectPath}/forest`), + this.mkdirp(`${projectPath}/public`), + this.mkdirp(`${projectPath}/views`), + this.mkdirp(`${projectPath}/models`), + this.mkdirp(`${projectPath}/middlewares`), ]; await Promise.all(directories); @@ -358,41 +373,41 @@ class Dumper { const modelNames = Object.keys(schema) .sort((a, b) => a.localeCompare(b, undefined, { sensitivity: 'base' })); - modelNames.forEach(this.writeForestCollection.bind(this)); + modelNames.forEach((modelName) => this.writeForestCollection(projectPath, config, modelName)); - this.writeForestAdminMiddleware(); - this.copyTemplate('middlewares/welcome.hbs', `${this.path}/middlewares/welcome.js`); + this.writeForestAdminMiddleware(projectPath, config); + this.copyTemplate(projectPath, 'middlewares/welcome.hbs', 'middlewares/welcome.js'); - this.writeModelsIndex(); + this.writeModelsIndex(projectPath, config); modelNames.forEach((modelName) => { const { fields, references, options } = schema[modelName]; const safeReferences = references.map((reference) => ({ ...reference, ref: Dumper.getModelNameFromTableName(reference.ref), })); - this.writeModel(modelName, fields, safeReferences, options); + this.writeModel(projectPath, config, modelName, fields, safeReferences, options); }); - this.copyTemplate('public/favicon.png', `${this.path}/public/favicon.png`); + this.copyTemplate(projectPath, 'public/favicon.png', 'public/favicon.png'); modelNames.forEach((modelName) => { // HACK: If a table name is "sessions" the generated routes will conflict with Forest Admin // internal session creation route. As a workaround, we don't generate the route file. // TODO: Remove the if condition, once the routes paths refactored to prevent such conflict. if (modelName !== 'sessions') { - this.writeRoute(modelName); + this.writeRoute(projectPath, config, modelName); } }); - this.copyTemplate('views/index.hbs', `${this.path}/views/index.html`); - this.copyTemplate('dockerignore.hbs', `${this.path}/.dockerignore`); - this.writeDotEnv(); - this.copyTemplate('gitignore.hbs', `${this.path}/.gitignore`); - this.writeAppJs(); - this.writeDockerCompose(); - this.writeDockerfile(); - this.writePackageJson(); - this.copyTemplate('server.hbs', `${this.path}/server.js`); + this.copyTemplate(projectPath, 'views/index.hbs', 'views/index.html'); + this.copyTemplate(projectPath, 'dockerignore.hbs', '.dockerignore'); + this.writeDotEnv(projectPath, config); + this.copyTemplate(projectPath, 'gitignore.hbs', '.gitignore'); + this.writeAppJs(projectPath, config); + this.writeDockerCompose(projectPath, config); + this.writeDockerfile(projectPath); + this.writePackageJson(projectPath, config); + this.copyTemplate(projectPath, 'server.hbs', '/server.js'); } } diff --git a/test/services/dumper/dumper-mongo.test.js b/test/services/dumper/dumper-mongo.test.js index 6b70f322..1949e870 100644 --- a/test/services/dumper/dumper-mongo.test.js +++ b/test/services/dumper/dumper-mongo.test.js @@ -13,26 +13,32 @@ const subDocumentsNotUsingIds = require('../../../test-expected/mongo/db-analysi const subDocumentsUsingIds = require('../../../test-expected/mongo/db-analysis-output/sub-documents-using-ids.expected'); const subDocumentUsingIds = require('../../../test-expected/mongo/db-analysis-output/sub-document-using-ids.expected'); const Dumper = require('../../../services/dumper'); +const context = require('../../../context'); +const initContext = require('../../../context/init'); + +initContext(context); function getDumper() { - return new Dumper({ - appName: 'test-output/mongo', - dbDialect: 'mongodb', - dbConnectionUrl: 'mongodb://localhost:27017', - ssl: false, - dbSchema: 'public', - appHostname: 'localhost', - appPort: 1654, - }); + return new Dumper(context.inject()); } +const CONFIG = { + appName: 'test-output/mongo', + dbDialect: 'mongodb', + dbConnectionUrl: 'mongodb://localhost:27017', + ssl: false, + dbSchema: 'public', + appHostname: 'localhost', + appPort: 1654, +}; + function cleanOutput() { rimraf.sync('./test-output/mongo'); } async function getGeneratedFileFromPersonModel(model) { const dumper = getDumper(); - await dumper.dump(model); + await dumper.dump(model, CONFIG); return fs.readFileSync('./test-output/mongo/models/persons.js', 'utf8'); } @@ -40,7 +46,7 @@ describe('services > dumper > MongoDB', () => { it('should generate a simple model file', async () => { expect.assertions(1); const dumper = getDumper(); - await dumper.dump(simpleModel); + await dumper.dump(simpleModel, CONFIG); const generatedFile = fs.readFileSync('./test-output/mongo/models/films.js', 'utf8'); const expectedFile = fs.readFileSync('./test-expected/mongo/dumper-output/simple.expected.js', 'utf-8'); @@ -51,7 +57,7 @@ describe('services > dumper > MongoDB', () => { it('should generate a model file with hasMany', async () => { expect.assertions(1); const dumper = getDumper(); - await dumper.dump(hasManyModel); + await dumper.dump(hasManyModel, CONFIG); const generatedFile = fs.readFileSync('./test-output/mongo/models/films.js', 'utf8'); const expectedFile = fs.readFileSync('./test-expected/mongo/dumper-output/hasmany.expected.js', 'utf-8'); diff --git a/test/services/dumper/dumper-sequelize.test.js b/test/services/dumper/dumper-sequelize.test.js index da45883a..07a9db6a 100644 --- a/test/services/dumper/dumper-sequelize.test.js +++ b/test/services/dumper/dumper-sequelize.test.js @@ -10,22 +10,28 @@ const defaultValuesModel = require('../../../test-expected/sequelize/db-analysis const parenthesisColumnName = require('../../../test-expected/sequelize/db-analysis-output/parenthesis.expected.json'); const parenthesisColumnNameUnderscored = require('../../../test-expected/sequelize/db-analysis-output/parenthesis_underscored.expected.json'); const parenthesisColumnNameUnderscoredTrue = require('../../../test-expected/sequelize/db-analysis-output/parenthesis_underscored_true.expected.json'); +const context = require('../../../context'); +const initContext = require('../../../context/init'); + +initContext(context); const Dumper = require('../../../services/dumper'); function getDumper() { - return new Dumper({ - appName: 'test-output/sequelize', - dbDialect: 'postgres', - dbConnectionUrl: 'postgres://localhost:27017', - ssl: false, - dbSchema: 'public', - appHostname: 'localhost', - appPort: 1654, - db: true, - }); + return new Dumper(context.inject()); } +const CONFIG = { + appName: 'test-output/sequelize', + dbDialect: 'postgres', + dbConnectionUrl: 'postgres://localhost:27017', + ssl: false, + dbSchema: 'public', + appHostname: 'localhost', + appPort: 1654, + db: true, +}; + function cleanOutput() { rimraf.sync('./test-output/sequelize'); } @@ -34,7 +40,7 @@ describe('services > dumper > sequelize', () => { it('should generate a simple model file', async () => { expect.assertions(1); const dumper = getDumper(); - await dumper.dump(simpleModel); + await dumper.dump(simpleModel, CONFIG); const generatedFile = fs.readFileSync('./test-output/sequelize/models/customers.js', 'utf8'); const expectedFile = fs.readFileSync('./test-expected/sequelize/dumper-output/customers.expected.js', 'utf-8'); @@ -45,7 +51,7 @@ describe('services > dumper > sequelize', () => { it('should generate a model file with belongsTo associations', async () => { expect.assertions(1); const dumper = getDumper(); - await dumper.dump(belongsToModel); + await dumper.dump(belongsToModel, CONFIG); const generatedFile = fs.readFileSync('./test-output/sequelize/models/addresses.js', 'utf8'); const expectedFile = fs.readFileSync('./test-expected/sequelize/dumper-output/addresses.expected.js', 'utf-8'); @@ -56,7 +62,7 @@ describe('services > dumper > sequelize', () => { it('should generate a model file with correct parenthesis field', async () => { expect.assertions(1); const dumper = getDumper(); - await dumper.dump(parenthesisColumnName); + await dumper.dump(parenthesisColumnName, CONFIG); const generatedFile = fs.readFileSync('./test-output/sequelize/models/parenthesis.js', 'utf8'); const expectedFile = fs.readFileSync('./test-expected/sequelize/dumper-output/parenthesis.expected.js', 'utf-8'); @@ -67,7 +73,7 @@ describe('services > dumper > sequelize', () => { it('should generate a model file with correct parenthesis field and correct underscored fields', async () => { expect.assertions(1); const dumper = getDumper(); - await dumper.dump(parenthesisColumnNameUnderscored); + await dumper.dump(parenthesisColumnNameUnderscored, CONFIG); const generatedFile = fs.readFileSync('./test-output/sequelize/models/parenthesis-underscored.js', 'utf8'); const expectedFile = fs.readFileSync('./test-expected/sequelize/dumper-output/parenthesis_underscored.expected.js', 'utf-8'); @@ -78,7 +84,7 @@ describe('services > dumper > sequelize', () => { it('should generate a model file with correct parenthesis field and underscored true', async () => { expect.assertions(1); const dumper = getDumper(); - await dumper.dump(parenthesisColumnNameUnderscoredTrue); + await dumper.dump(parenthesisColumnNameUnderscoredTrue, CONFIG); const generatedFile = fs.readFileSync('./test-output/sequelize/models/parenthesis-underscored-true.js', 'utf8'); const expectedFile = fs.readFileSync('./test-expected/sequelize/dumper-output/parenthesis_underscored_true.expected.js', 'utf-8'); @@ -89,7 +95,7 @@ describe('services > dumper > sequelize', () => { it('should generate a model file with hasMany, hasOne and belongsToMany', async () => { expect.assertions(1); const dumper = getDumper(); - await dumper.dump(otherAssociationsModel); + await dumper.dump(otherAssociationsModel, CONFIG); const generatedFile = fs.readFileSync('./test-output/sequelize/models/users.js', 'utf8'); const expectedFile = fs.readFileSync('./test-expected/sequelize/dumper-output/users.expected.js', 'utf-8'); @@ -100,7 +106,7 @@ describe('services > dumper > sequelize', () => { it('should still generate a model file when reserved word is used', async () => { expect.assertions(2); const dumper = getDumper(); - await dumper.dump(exportModel); + await dumper.dump(exportModel, CONFIG); const generatedModelFile = fs.readFileSync('./test-output/sequelize/models/export.js', 'utf8'); const generatedRouteFile = fs.readFileSync('./test-output/sequelize/routes/export.js', 'utf8'); const expectedModelFile = fs.readFileSync('./test-expected/sequelize/dumper-output/export.expected.js', 'utf-8'); @@ -114,7 +120,7 @@ describe('services > dumper > sequelize', () => { it('should generate a model with default values', async () => { expect.assertions(1); const dumper = getDumper(); - await dumper.dump(defaultValuesModel); + await dumper.dump(defaultValuesModel, CONFIG); const generatedFile = fs.readFileSync('./test-output/sequelize/models/default-values.js', 'utf8'); const expectedFile = fs.readFileSync('./test-expected/sequelize/dumper-output/default-values.expected.js', 'utf-8'); @@ -125,7 +131,7 @@ describe('services > dumper > sequelize', () => { it('should generate the model index file', async () => { expect.assertions(1); const dumper = getDumper(); - await dumper.dump(simpleModel); + await dumper.dump(simpleModel, CONFIG); const generatedFile = fs.readFileSync('./test-output/sequelize/models/index.js', 'utf8'); const expectedFile = fs.readFileSync('./test-expected/sequelize/dumper-output/index.expected.js', 'utf-8'); @@ -142,7 +148,7 @@ describe('services > dumper > sequelize', () => { osStub.returns('linux'); const dumper = getDumper(); - await dumper.dump(simpleModel); + await dumper.dump(simpleModel, CONFIG); osStub.restore(); @@ -161,7 +167,7 @@ describe('services > dumper > sequelize', () => { osStub.returns('darwin'); const dumper = getDumper(); - await dumper.dump(simpleModel); + await dumper.dump(simpleModel, CONFIG); osStub.restore(); diff --git a/test/services/dumper/dumper-sql.test.js b/test/services/dumper/dumper-sql.test.js index d070d66a..a547318a 100644 --- a/test/services/dumper/dumper-sql.test.js +++ b/test/services/dumper/dumper-sql.test.js @@ -2,10 +2,16 @@ const rimraf = require('rimraf'); const fs = require('fs'); const renderingModel = require('../../../test-expected/sequelize/db-analysis-output/renderings.expected.json'); +const context = require('../../../context'); +const initContext = require('../../../context/init'); + const Dumper = require('../../../services/dumper'); const TYPE_CAST = 'databaseOptions.dialectOptions.typeCast'; +initContext(context); +const injectedContext = context.inject(); + function cleanOutput() { rimraf.sync('./test-output/mssql'); rimraf.sync('./test-output/mysql'); @@ -25,8 +31,8 @@ describe('services > dumper > SQL', () => { appPort: 1654, }; - const dumper = new Dumper(config); - await dumper.dump({}); + const dumper = new Dumper(injectedContext); + await dumper.dump({}, config); } it('should force type casting for boolean in /models/index.js file', async () => { @@ -51,8 +57,8 @@ describe('services > dumper > SQL', () => { appPort: 1654, }; - const dumper = new Dumper(config); - await dumper.dump({}); + const dumper = new Dumper(injectedContext); + await dumper.dump({}, config); } it('should not force type casting in /models/index.js file', async () => { @@ -77,8 +83,8 @@ describe('services > dumper > SQL', () => { appPort: 1654, }; - const dumper = new Dumper(config); - await dumper.dump(renderingModel); + const dumper = new Dumper(injectedContext); + await dumper.dump(renderingModel, config); } it('should generate a model file', async () => { diff --git a/test/services/dumper/dumper.test.js b/test/services/dumper/dumper.test.js index 8c2c9768..4270a073 100644 --- a/test/services/dumper/dumper.test.js +++ b/test/services/dumper/dumper.test.js @@ -3,6 +3,11 @@ const sinon = require('sinon'); const os = require('os'); const rimraf = require('rimraf'); const Dumper = require('../../../services/dumper'); +const context = require('../../../context'); +const initContext = require('../../../context/init'); + +initContext(context); +const injectedContext = context.inject(); const DOCKER_COMPOSE_FILE_LOCATION = './test-output/Linux/docker-compose.yml'; const DOT_ENV_FILE_LOCATION = './test-output/Linux/.env'; @@ -30,8 +35,8 @@ async function createLinuxDump(overrides = {}) { ...overrides, }; - const dumper = new Dumper(config); - await dumper.dump({}); + const dumper = new Dumper(injectedContext); + await dumper.dump({}, config); } describe('services > dumper', () => { @@ -163,43 +168,4 @@ describe('services > dumper', () => { }); }); }); - - describe('getDatabaseUrl', () => { - it('should return the connection string if no dbConnectionUrl is provided', () => { - expect.assertions(1); - - const config = { - dbDialect: 'mysql', - dbPort: 3306, - dbUser: 'root', - dbPassword: 'password', - dbHostname: 'localhost', - dbName: 'forest', - }; - - const dumper = new Dumper(config); - const databaseUrl = dumper.getDatabaseUrl(); - - expect(databaseUrl).toStrictEqual('mysql://root:password@localhost:3306/forest'); - }); - - it('should remove the port if mongodbSrv is provided', () => { - expect.assertions(1); - - const config = { - dbDialect: 'mongodb', - dbPort: 3306, - mongodbSrv: true, - dbUser: 'root', - dbPassword: 'password', - dbHostname: 'localhost', - dbName: 'forest', - }; - - const dumper = new Dumper(config); - const databaseUrl = dumper.getDatabaseUrl(); - - expect(databaseUrl).toStrictEqual('mongodb+srv://root:password@localhost/forest'); - }); - }); }); diff --git a/test/services/dumper/dumper.unit.test.js b/test/services/dumper/dumper.unit.test.js new file mode 100644 index 00000000..38cb806f --- /dev/null +++ b/test/services/dumper/dumper.unit.test.js @@ -0,0 +1,539 @@ +const chalk = require('chalk'); +const Dumper = require('../../../services/dumper'); + +const SequelizeMock = { + DataTypes: {}, +}; + +const ABSOLUTE_PROJECT_PATH = '/absolute/project/path'; +const RELATIVE_FILE_PATH = 'some/folder/relative-file.js'; + +function createDumper(contextOverride = {}) { + return new Dumper({ + Sequelize: SequelizeMock, + chalk, + mkdirp: () => {}, + ...contextOverride, + }); +} + +describe('services > dumper (unit)', () => { + describe('isLinuxBasedOs', () => { + it('should return true on linux', () => { + expect.assertions(1); + + const dumper = createDumper({ + os: { + platform: jest.fn().mockReturnValue('linux'), + }, + }); + + expect(dumper.isLinuxBasedOs()).toStrictEqual(true); + }); + + it('should return false on other OS', () => { + expect.assertions(1); + + const dumper = createDumper({ + os: { + platform: jest.fn().mockReturnValue('windows'), + }, + }); + + expect(dumper.isLinuxBasedOs()).toStrictEqual(false); + }); + }); + + describe('writeFile', () => { + const context = { + logger: { + log: jest.fn(), + }, + fs: { + writeFileSync: jest.fn(), + }, + }; + createDumper(context).writeFile(ABSOLUTE_PROJECT_PATH, RELATIVE_FILE_PATH, 'content'); + + it('should call writeFileSync to write the file', () => { + expect.assertions(2); + + expect(context.fs.writeFileSync).toHaveBeenCalledTimes(1); + expect(context.fs.writeFileSync).toHaveBeenCalledWith(`${ABSOLUTE_PROJECT_PATH}/${RELATIVE_FILE_PATH}`, 'content'); + }); + + it('should call the logger to display a log message', () => { + expect.assertions(2); + + expect(context.logger.log).toHaveBeenCalledTimes(1); + expect(context.logger.log).toHaveBeenCalledWith(` ${chalk.green('create')} ${RELATIVE_FILE_PATH}`); + }); + }); + + describe('copyTemplate', () => { + it('should call writeFile with computed parameters', () => { + expect.assertions(2); + + const dumper = createDumper({ + fs: { + readFileSync: jest.fn().mockReturnValue('content'), + }, + }); + const writeFileSpy = jest.spyOn(dumper, 'writeFile').mockImplementation(() => {}); + dumper.copyTemplate(ABSOLUTE_PROJECT_PATH, 'from.js', 'to.js'); + + expect(writeFileSpy).toHaveBeenCalledTimes(1); + expect(writeFileSpy).toHaveBeenCalledWith(ABSOLUTE_PROJECT_PATH, 'to.js', 'content'); + }); + }); + + describe('copyHandleBarsTemplate', () => { + const context = { + Handlebars: { + compile: () => jest.fn().mockReturnValue('content'), + }, + fs: { + readFileSync: jest.fn(), + }, + }; + const dumper = createDumper(context); + + describe('with missing parameters', () => { + it('should throw an error', () => { + expect.assertions(1); + + expect(() => dumper.copyHandleBarsTemplate({})) + .toThrow('Missing argument (projectPath, source, target or context).'); + }); + }); + + describe('with all the required parameters', () => { + it('should call writeFile with computed parameters', () => { + expect.assertions(2); + + const writeFileSpy = jest.spyOn(dumper, 'writeFile').mockImplementation(() => {}); + dumper.copyHandleBarsTemplate({ + projectPath: ABSOLUTE_PROJECT_PATH, + source: 'from.js', + target: 'to.js', + context: {}, + }); + + expect(writeFileSpy).toHaveBeenCalledTimes(1); + expect(writeFileSpy).toHaveBeenCalledWith(ABSOLUTE_PROJECT_PATH, 'to.js', 'content'); + }); + }); + }); + + describe('writePackageJson', () => { + it('should call write file with a valid package.json file content', () => { + expect.assertions(6); + + const dumper = createDumper({}); + const writeFileSpy = jest.spyOn(dumper, 'writeFile').mockImplementation(() => {}); + dumper.writePackageJson(ABSOLUTE_PROJECT_PATH, { + dbDialect: 'none', + appName: 'test', + }); + + const fileContent = writeFileSpy.mock.calls[0][2]; + expect(writeFileSpy).toHaveBeenCalledTimes(1); + expect(() => JSON.parse(fileContent)).not.toThrow(); + + const parsedPackageJson = JSON.parse(fileContent); + + expect(parsedPackageJson.name).toStrictEqual('test'); + expect(parsedPackageJson.version).toStrictEqual('0.0.1'); + expect(parsedPackageJson.private).toStrictEqual(true); + expect(parsedPackageJson.scripts).toStrictEqual({ start: 'node ./server.js' }); + }); + + describe('with specific database dialect', () => { + const getPackageJSONContentFromDialect = (dbDialect) => { + const dumper = createDumper({}); + const writeFileSpy = jest.spyOn(dumper, 'writeFile').mockImplementation(() => {}); + dumper.writePackageJson(ABSOLUTE_PROJECT_PATH, { + dbDialect, + appName: 'test', + }); + + return writeFileSpy.mock.calls[0][2]; + }; + + it('undefined: it should not add any dbs connector', () => { + expect.assertions(4); + + const packageJson = getPackageJSONContentFromDialect(undefined); + + expect(packageJson).not.toContain('pg'); + expect(packageJson).not.toContain('mysql2'); + expect(packageJson).not.toContain('tedious'); + expect(packageJson).not.toContain('mongoose'); + }); + + it('postgres: it should add pg dependency', () => { + expect.assertions(1); + + expect(getPackageJSONContentFromDialect('postgres')).toContain('pg'); + }); + + it('mysql: it should add mysql2 dependency', () => { + expect.assertions(1); + + expect(getPackageJSONContentFromDialect('mysql')).toContain('mysql2'); + }); + + it('mssql: it should add tedious dependency', () => { + expect.assertions(1); + + expect(getPackageJSONContentFromDialect('mssql')).toContain('tedious'); + }); + + it('mongodb: it should add mongoose dependency', () => { + expect.assertions(1); + + expect(getPackageJSONContentFromDialect('mongodb')).toContain('mongoose'); + }); + }); + }); + + describe('tableToFilename', () => { + it('should return a kebab case version of the given parameter', () => { + expect.assertions(3); + + expect(Dumper.tableToFilename('test')).toStrictEqual('test'); + expect(Dumper.tableToFilename('testSomething')).toStrictEqual('test-something'); + expect(Dumper.tableToFilename('test_something_else')).toStrictEqual('test-something-else'); + }); + }); + + describe('getDatabaseUrl', () => { + it('should return the dbConnectionUrl if provided', () => { + expect.assertions(1); + + const config = { + dbConnectionUrl: 'mysql://root:password@localhost:3306/forest', + }; + + expect(Dumper.getDatabaseUrl(config)).toStrictEqual(config.dbConnectionUrl); + }); + + it('should return the connection string if no dbConnectionUrl is provided', () => { + expect.assertions(1); + + const config = { + dbDialect: 'mysql', + dbPort: 3306, + dbUser: 'root', + dbHostname: 'localhost', + dbName: 'forest', + }; + + expect(Dumper.getDatabaseUrl(config)).toStrictEqual('mysql://root@localhost:3306/forest'); + }); + + it('should remove the port if mongodbSrv is provided', () => { + expect.assertions(1); + + const config = { + dbDialect: 'mongodb', + dbPort: 27017, + mongodbSrv: true, + dbUser: 'root', + dbPassword: 'password', + dbHostname: 'localhost', + dbName: 'forest', + }; + + expect(Dumper.getDatabaseUrl(config)).toStrictEqual('mongodb+srv://root:password@localhost/forest'); + }); + }); + + describe('isDatabaseLocal', () => { + it('should return true for a config referring to a database hosted locally', () => { + expect.assertions(1); + + const dbConnectionUrl = 'mongodb+srv://root:password@localhost/forest'; + + expect(Dumper.isDatabaseLocal({ dbConnectionUrl })).toStrictEqual(true); + }); + + it('should return false for a config referring to a database not hosted locally', () => { + expect.assertions(1); + + const dbConnectionUrl = 'mongodb+srv://root:password@somewhere.intheworld.com/forest'; + + expect(Dumper.isDatabaseLocal({ dbConnectionUrl })).toStrictEqual(false); + }); + }); + + describe('isLocalUrl', () => { + it('should return true for a local url', () => { + expect.assertions(1); + + expect(Dumper.isLocalUrl('http://localhost')).toStrictEqual(true); + }); + + it('should return false for not local url', () => { + expect.assertions(1); + + expect(Dumper.isLocalUrl('http://somewhere.else.intheworld.com')).toStrictEqual(false); + }); + }); + + describe('getPort', () => { + it('should return the given for config containing appPort', () => { + expect.assertions(1); + + expect(Dumper.getPort({ appPort: 1234 })).toStrictEqual(1234); + }); + + it('should return the default port for config not containing appPort', () => { + expect.assertions(1); + + expect(Dumper.getPort({})).toStrictEqual(3310); + }); + }); + + describe('getApplicationUrl', () => { + describe('when no protocol is specified', () => { + it('should prefix the host name with http://', () => { + expect.assertions(1); + + expect(Dumper.getApplicationUrl({ appHostname: 'somewhere.not.local.com' })).toStrictEqual('http://somewhere.not.local.com'); + }); + }); + + describe('when https? protocol is specified', () => { + it('should append the port to the given hostname for local application url', () => { + expect.assertions(1); + + expect(Dumper.getApplicationUrl({ + appHostname: 'http://localhost', + appPort: 1234, + })).toStrictEqual('http://localhost:1234'); + }); + + it('should return the appHostname already defined', () => { + expect.assertions(1); + + expect(Dumper.getApplicationUrl({ + appHostname: 'https://somewhere.com', + })).toStrictEqual('https://somewhere.com'); + }); + }); + }); + + describe('writeDotEnv', () => { + const config = { + dbConnectionUrl: 'mongodb://root:password@localhost:27017/forest', + ssl: true, + appHostname: 'localhost', + forestEnvSecret: 'someEnvSecret', + forestAuthSecret: 'someAuthSecret', + }; + + describe('on a linux based os', () => { + it('should compute the handlebars context from the given config with no dockerDatabaseUrl', () => { + expect.assertions(2); + + const dumper = createDumper(); + const copyHandlebarsTemplateSpy = jest.spyOn(dumper, 'copyHandleBarsTemplate').mockImplementation(); + jest.spyOn(dumper, 'isLinuxBasedOs').mockReturnValue(true); + dumper.writeDotEnv(ABSOLUTE_PROJECT_PATH, config); + + expect(copyHandlebarsTemplateSpy).toHaveBeenCalledTimes(1); + expect(copyHandlebarsTemplateSpy).toHaveBeenCalledWith({ + projectPath: ABSOLUTE_PROJECT_PATH, + source: 'app/env.hbs', + target: '.env', + context: { + databaseUrl: config.dbConnectionUrl, + ssl: config.ssl, + dbSchema: undefined, + hostname: config.appHostname, + port: 3310, + forestEnvSecret: config.forestEnvSecret, + forestAuthSecret: config.forestAuthSecret, + hasDockerDatabaseUrl: false, + applicationUrl: 'http://localhost:3310', + }, + }); + }); + }); + + describe('on a non-linux based os', () => { + it('should compute the handlebars context from the given config with a dockerDatabaseUrl', () => { + expect.assertions(2); + + const dumper = createDumper(); + const copyHandlebarsTemplateSpy = jest.spyOn(dumper, 'copyHandleBarsTemplate').mockImplementation(); + jest.spyOn(dumper, 'isLinuxBasedOs').mockReturnValue(false); + dumper.writeDotEnv(ABSOLUTE_PROJECT_PATH, config); + + expect(copyHandlebarsTemplateSpy).toHaveBeenCalledTimes(1); + expect(copyHandlebarsTemplateSpy).toHaveBeenCalledWith({ + projectPath: ABSOLUTE_PROJECT_PATH, + source: 'app/env.hbs', + target: '.env', + context: { + databaseUrl: config.dbConnectionUrl, + ssl: config.ssl, + dbSchema: undefined, + dockerDatabaseUrl: 'mongodb://root:password@host.docker.internal:27017/forest', + hostname: config.appHostname, + port: 3310, + forestEnvSecret: config.forestEnvSecret, + forestAuthSecret: config.forestAuthSecret, + hasDockerDatabaseUrl: true, + applicationUrl: 'http://localhost:3310', + }, + }); + }); + }); + }); + + describe('writeDockerfile', () => { + it('should call the copyHandleBarsTemplate with an empty context', () => { + expect.assertions(1); + + const dumper = createDumper(); + const copyHandlebarsTemplateSpy = jest.spyOn(dumper, 'copyHandleBarsTemplate').mockImplementation(); + dumper.writeDockerfile(ABSOLUTE_PROJECT_PATH, { dbDialect: 'mongodb' }); + + expect(copyHandlebarsTemplateSpy).toHaveBeenCalledWith({ + projectPath: ABSOLUTE_PROJECT_PATH, + source: 'app/Dockerfile.hbs', + target: 'Dockerfile', + context: {}, + }); + }); + }); + + describe('writeDockerCompose', () => { + describe('when an environment variable FOREST_URL is provided', () => { + it('should have called copyHandlebarsTemplate with a valid forestUrl is context', () => { + expect.assertions(1); + + const dumper = createDumper({ + env: { + FOREST_URL: 'https://something.com', + }, + }); + jest.spyOn(dumper, 'isLinuxBasedOs').mockReturnValue(true); + const copyHandlebarsTemplateSpy = jest.spyOn(dumper, 'copyHandleBarsTemplate').mockImplementation(); + dumper.writeDockerCompose(ABSOLUTE_PROJECT_PATH, {}); + const handlebarContext = copyHandlebarsTemplateSpy.mock.calls[0][0].context; + + // eslint-disable-next-line no-template-curly-in-string + expect(handlebarContext.forestUrl).toStrictEqual('${FOREST_URL-https://something.com}'); + }); + }); + + describe('when no environment variable FOREST_URL is provided', () => { + it('should have called copyHandlebarsTemplate with a valid forestUrl is context', () => { + expect.assertions(1); + + const dumper = createDumper({ env: {} }); + jest.spyOn(dumper, 'isLinuxBasedOs').mockReturnValue(true); + const copyHandlebarsTemplateSpy = jest.spyOn(dumper, 'copyHandleBarsTemplate').mockImplementation(); + dumper.writeDockerCompose(ABSOLUTE_PROJECT_PATH, {}); + const handlebarContext = copyHandlebarsTemplateSpy.mock.calls[0][0].context; + + expect(handlebarContext.forestUrl).toStrictEqual(false); + }); + }); + }); + + describe('writeForestAdminMiddleware', () => { + describe('on mongodb', () => { + it('should compute the handlebars context', () => { + expect.assertions(1); + + const dumper = createDumper(); + const copyHandlebarsTemplateSpy = jest.spyOn(dumper, 'copyHandleBarsTemplate').mockImplementation(); + dumper.writeForestAdminMiddleware(ABSOLUTE_PROJECT_PATH, { dbDialect: 'mongodb' }); + + expect(copyHandlebarsTemplateSpy).toHaveBeenCalledWith({ + projectPath: ABSOLUTE_PROJECT_PATH, + source: 'app/middlewares/forestadmin.hbs', + target: 'middlewares/forestadmin.js', + context: { isMongoDB: true }, + }); + }); + }); + + describe('on sql based DBS', () => { + it('should compute the handlebars context', () => { + expect.assertions(1); + + const dumper = createDumper(); + const copyHandlebarsTemplateSpy = jest.spyOn(dumper, 'copyHandleBarsTemplate').mockImplementation(); + dumper.writeForestAdminMiddleware(ABSOLUTE_PROJECT_PATH, { dbDialect: 'mysql' }); + + expect(copyHandlebarsTemplateSpy).toHaveBeenCalledWith({ + projectPath: ABSOLUTE_PROJECT_PATH, + source: 'app/middlewares/forestadmin.hbs', + target: 'middlewares/forestadmin.js', + context: { isMongoDB: false }, + }); + }); + }); + }); + + describe('dump', () => { + it('should call all the mandatory functions required to generate a complete project', async () => { + expect.assertions(17); + + const dumper = createDumper({ + os: { + platform: () => jest.fn().mockReturnValue('linux'), + }, + }); + const writeForestCollectionSpy = jest.spyOn(dumper, 'writeForestCollection').mockImplementation(); + const writeForestAdminMiddlewareSpy = jest.spyOn(dumper, 'writeForestAdminMiddleware').mockImplementation(); + const writeModelsIndexSpy = jest.spyOn(dumper, 'writeModelsIndex').mockImplementation(); + const writeModelSpy = jest.spyOn(dumper, 'writeModel').mockImplementation(); + const writeRouteSpy = jest.spyOn(dumper, 'writeRoute').mockImplementation(); + const writeDotEnvSpy = jest.spyOn(dumper, 'writeDotEnv').mockImplementation(); + const writeAppJsSpy = jest.spyOn(dumper, 'writeAppJs').mockImplementation(); + const writeDockerComposeSpy = jest.spyOn(dumper, 'writeDockerCompose').mockImplementation(); + const writeDockerfileSpy = jest.spyOn(dumper, 'writeDockerfile').mockImplementation(); + const writePackageJsonSpy = jest.spyOn(dumper, 'writePackageJson').mockImplementation(); + const copyTemplateSpy = jest.spyOn(dumper, 'copyTemplate').mockImplementation(); + + const schema = { + testModel: { fields: {}, references: [], options: {} }, + }; + const config = { + appName: 'test-output/unit-test-dumper', + }; + await dumper.dump(schema, config); + + const projectPath = `${process.cwd()}/test-output/unit-test-dumper`; + + // Files associated with each models of the schema + expect(writeModelSpy).toHaveBeenCalledWith(projectPath, config, 'testModel', {}, [], {}); + expect(writeRouteSpy).toHaveBeenCalledWith(projectPath, config, 'testModel'); + expect(writeForestCollectionSpy).toHaveBeenCalledWith(projectPath, config, 'testModel'); + + // General app files, based on config + expect(writeForestAdminMiddlewareSpy).toHaveBeenCalledWith(projectPath, config); + expect(writeModelsIndexSpy).toHaveBeenCalledWith(projectPath, config); + expect(writeDotEnvSpy).toHaveBeenCalledWith(projectPath, config); + expect(writeAppJsSpy).toHaveBeenCalledWith(projectPath, config); + expect(writeDockerComposeSpy).toHaveBeenCalledWith(projectPath, config); + expect(writeDockerfileSpy).toHaveBeenCalledWith(projectPath); + expect(writePackageJsonSpy).toHaveBeenCalledWith(projectPath, config); + + // Copied files + expect(copyTemplateSpy).toHaveBeenCalledTimes(6); + expect(copyTemplateSpy).toHaveBeenCalledWith(projectPath, 'middlewares/welcome.hbs', 'middlewares/welcome.js'); + expect(copyTemplateSpy).toHaveBeenCalledWith(projectPath, 'public/favicon.png', 'public/favicon.png'); + expect(copyTemplateSpy).toHaveBeenCalledWith(projectPath, 'views/index.hbs', 'views/index.html'); + expect(copyTemplateSpy).toHaveBeenCalledWith(projectPath, 'dockerignore.hbs', '.dockerignore'); + expect(copyTemplateSpy).toHaveBeenCalledWith(projectPath, 'gitignore.hbs', '.gitignore'); + expect(copyTemplateSpy).toHaveBeenCalledWith(projectPath, 'server.hbs', '/server.js'); + }); + }); +}); diff --git a/utils/strings.js b/utils/strings.js index 905dd589..2f1fd5fd 100644 --- a/utils/strings.js +++ b/utils/strings.js @@ -42,4 +42,8 @@ module.exports = { } return input; }, + + transformToCamelCaseSafeString(input) { + return this.camelCase(this.transformToSafeString(input)); + }, };