diff --git a/src/create/index.js b/src/create/index.js index 152431c..6c9b458 100644 --- a/src/create/index.js +++ b/src/create/index.js @@ -6,17 +6,19 @@ const addService = require("./service"); +const addMixin = require("./mixin"); /** * Yargs command */ module.exports = { command: ["create", "", ""], - describe: `Create a Moleculer service `, + describe: `Create a Moleculer service or mixin `, + builder(yargs) { yargs.options({ typescript: { - describe: "Create service for typescript", + describe: "Create typescript file", type: "boolean", default: false, }, @@ -27,6 +29,9 @@ module.exports = { switch (fileType) { case "service": return addService(opts); + + case "mixin": + return addMixin(opts); } }, }; diff --git a/src/create/mixin/index.js b/src/create/mixin/index.js new file mode 100644 index 0000000..30a4a5d --- /dev/null +++ b/src/create/mixin/index.js @@ -0,0 +1,124 @@ +/* + * moleculer-cli + * Copyright (c) 2021 MoleculerJS (https://github.com/moleculerjs/moleculer-cli) + * MIT Licensed + */ + +const fs = require("fs"); +const path = require("path"); +const inquirer = require("inquirer"); +const render = require("consolidate").handlebars.render; +const ui = new inquirer.ui.BottomBar(); + +const { fail } = require("../../utils"); + +module.exports = async (opts) => { + const values = Object.assign({}, opts); + const _typescript = values.typescript ? true : false; + const name = opts._[2]; + + return ( + Promise.resolve() + .then(() => { + const answers_options = [ + { + type: "input", + name: "mixinFolder", + message: "Mixin directory", + default: "./mixins", + async validate(input) { + if (!fs.existsSync(path.resolve(input))) { + ui.log.write(`The ${input} doesn't exists!`); + fail("Aborted"); + } + return true; + }, + }, + ]; + + if (!name) + answers_options.push({ + type: "input", + name: "mixinName", + message: "Mixin name", + default: "test", + }); + + return inquirer.prompt(answers_options).then((answers) => { + answers.name = answers.mixinName; + answers.mixinName = answers.mixinName || name; + answers.mixinName = answers.mixinName.replace( + /[^\w\s]/gi, + "-" + ); + + answers.className = answers.mixinName + .replace(/(\w)(\w*)/g, function (g0, g1, g2) { + return g1.toUpperCase() + g2.toLowerCase(); + }) + .replace(/[^\w\s]/gi, ""); + + Object.assign(values, answers); + const { mixinFolder, mixinName } = values; + const file_name = `${mixinName.toLowerCase()}.mixin${ + _typescript ? ".ts" : ".js" + }`; + const newMixinPath = path.join( + mixinFolder, + `${mixinName.toLowerCase()}.mixin${ + _typescript ? ".ts" : ".js" + }` + ); + + if (fs.existsSync(newMixinPath)) { + return inquirer + .prompt([ + { + type: "confirm", + name: "sure", + message: `The file ${file_name} already exists! Do you want to overwrite it?`, + default: false, + }, + ]) + .then(({ sure }) => { + if (!sure) fail("Aborted"); + }); + } + }); + }) + .then(() => { + const templatePath = _typescript + ? path.join( + __dirname, + "moleculer-db.typescript.mixin.template" + ) + : path.join(__dirname, "moleculer-db.mixin.template"); + const template = fs.readFileSync(templatePath, "utf8"); + return new Promise((resolve, reject) => { + render(template, values, async function (err, res) { + if (err) return reject(err); + + const { mixinFolder, mixinName } = values; + const newMixinPath = path.join( + mixinFolder, + `${mixinName.toLowerCase()}.mixin${ + _typescript ? ".ts" : ".js" + }` + ); + + fs.writeFileSync( + path.resolve(`${newMixinPath}`), + res, + "utf8" + ); + + resolve(); + }); + }); + }) + + // Error handler + .catch((err) => fail(err)) + ); +}; + diff --git a/src/create/mixin/moleculer-db.mixin.template b/src/create/mixin/moleculer-db.mixin.template new file mode 100644 index 0000000..146c2d6 --- /dev/null +++ b/src/create/mixin/moleculer-db.mixin.template @@ -0,0 +1,68 @@ +"use strict"; + +const fs = require("fs"); +const mongoose = require("mongoose"); // npm i mongoose -S + +module.exports = (collection, modelSchema) => { + const schema = { + actions: { + create: { + rest: "POST /", + async handler(ctx) { + const { params } = ctx; + console.log(this.adapter); + this.adapter.create(params, (err, saved) => { + if (err) this.logger.error(err); + this.logger.info(saved); + }); + }, + }, + update: { + rest: "PUT /:id", + async handler(ctx) { + const { params } = ctx; + this.adapter.findOneAndUpdate( + { _id: params.id }, + params, + (err, saved) => { + if (err) this.logger.error(err); + this.logger.info(saved); + } + ); + }, + }, + list: { + rest: "GET /", + async handler(ctx) { + const { params } = ctx; + return this.adapter.find({}); + }, + }, + + delete: { + rest: "DELETE /:id", + async handler(ctx) { + const { params } = ctx; + this.adapter.deleteOne({ _id: params.id }); + }, + }, + }, + methods: { + _connect() { + return mongoose + .connect("mongodb://localhost:27017/test") + .then(() => this.logger.info("Connected")) + .catch((err) => this.logger.error(err)); + }, + }, + + async started() { + this._connect(); + if (!this.adapter) { + this.adapter = mongoose.model(collection, modelSchema); + } + }, + }; + + return schema; +}; diff --git a/src/create/mixin/moleculer-db.typescript.mixin.template b/src/create/mixin/moleculer-db.typescript.mixin.template new file mode 100644 index 0000000..93e88db --- /dev/null +++ b/src/create/mixin/moleculer-db.typescript.mixin.template @@ -0,0 +1,70 @@ +import { Context, Service, ServiceSchema } from "moleculer"; +import { Document, Schema, connect, model } from "mongoose"; + +export default class {{className}}Connection implements Partial, ThisType{ + + private collection: string; + private modelSchema: Schema; + private schema: Partial & ThisType; + + public constructor(public collectionName: string, modelSchema: Schema) { + this.collection = collectionName; + this.modelSchema = modelSchema; + this.schema = { + actions: { + create: { + rest: "POST /", + async handler(ctx: Context) { + const { params } = ctx; + this.adapter.create(params, (err: Error, saved: Document) => { + if (err) {this.logger.error(err);}; + this.logger.info(saved); + }); + }, + }, + update: { + rest: "PUT /:id", + async handler(ctx: Context<{id: string}>) { + const { params } = ctx; + this.adapter.findOneAndUpdate( + { _id: params.id }, + params, + (err: Error, saved: Document) => { + if (err) {this.logger.error(err);} + this.logger.info(saved); + } + ); + }, + }, + list: { + rest: "GET /", + async handler(ctx: Context) { + return this.adapter.find({}); + }, + }, + + delete: { + rest: "DELETE /:id", + async handler(ctx: Context<{id: string}>) { + const { params } = ctx; + this.adapter.deleteOne({ _id: params.id }); + }, + }, + }, + methods: { + connectDb() { + return connect("mongodb://localhost:27017/test") + .then(() => this.logger.info("Connected")) + .catch((err: Error) => this.logger.error(err)); + }, + }, + async started(){ + this.connectDb(); + }, + }; + } + public start(){ + this.schema.adapter = model(this.collection, this.modelSchema); + return this.schema; + } +}; diff --git a/test/e2e/create/mixin/create.spec.js b/test/e2e/create/mixin/create.spec.js new file mode 100644 index 0000000..a847613 --- /dev/null +++ b/test/e2e/create/mixin/create.spec.js @@ -0,0 +1,99 @@ +const YargsPromise = require("yargs-promise"); +const yargs = require("yargs"); +const inquirer = require("inquirer"); +const path = require("path"); +const fs = require("fs"); +const create = require("../../../../src/create"); +const answers = require("./create_answers.json"); +const answers_ts = require("./create_answers_ts.json"); +const tmp = path.resolve(__dirname, "../../../../tmp"); + +describe("test create", () => { + beforeAll(() => { + if (!fs.existsSync(tmp)) { + fs.mkdirSync(tmp,{mode: 0o777}); + } + }); + afterEach(() => { + jest.clearAllMocks(); + }); + + afterAll(() => { + fs.rmdirSync(tmp, {recursive: true}); + }); + + it("create js mixin", () => { + const _path = `../../../../${answers.mixinFolder}/${answers.mixinName}.mixin.js`; + const pathAbsoluteFile = path.resolve(__dirname, _path); + const mockFileAbsolute = path.resolve( + __dirname, + `./mocks/${answers.mixinName}.mixin.js` + ); + + jest.mock("inquirer"); + inquirer.prompt = jest.fn().mockResolvedValue(answers); + yargs + .usage("Usage: $0 [options]") + .version() + .command(create) + .help().argv; + const parser = new YargsPromise(yargs); + return parser + .parse(`create mixin ${answers.mixinName}`) + .then(({ data, argv }) => { + if (!fs.existsSync(pathAbsoluteFile)) { + throw new Error("file not exist"); + } + + expect(fs.existsSync(pathAbsoluteFile)).toBeTruthy(); + expect(fs.readFileSync(pathAbsoluteFile)).toEqual( + fs.readFileSync(mockFileAbsolute) + ); + + fs.unlinkSync(pathAbsoluteFile); + }) + .catch(({ error, argv }) => { + throw new Error(error); + }); + }); + + it("create ts mixin", () => { + const _path = `../../../../${answers.mixinFolder}/${answers.mixinName}.mixin.ts`; + const pathAbsoluteFile = path.resolve(__dirname, _path); + const mockFileAbsolute = path.resolve( + __dirname, + `./mocks/${answers.mixinName}.mixin.ts` + ); + + jest.mock("inquirer"); + inquirer.prompt = jest.fn().mockResolvedValue(answers); + yargs + .usage("Usage: $0 [options]") + .version() + .command(create) + .default("--typescript", true) + .help().argv; + const parser = new YargsPromise(yargs); + return parser + .parse(`create mixin ${answers_ts.mixinName}`) + .then(({ data, argv }) => { + if (!fs.existsSync(pathAbsoluteFile)) { + fs.unlinkSync(pathAbsoluteFile); + throw new Error("file not exist"); + } + + expect(fs.existsSync(pathAbsoluteFile)).toBeTruthy(); + expect(fs.readFileSync(pathAbsoluteFile)).toEqual( + fs.readFileSync(mockFileAbsolute) + ); + fs.unlinkSync(pathAbsoluteFile); + + }) + .catch(({ error, argv }) => { + fs.unlinkSync(pathAbsoluteFile); + + throw new Error(error); + }); + }); +}); + diff --git a/test/e2e/create/mixin/create_answers.json b/test/e2e/create/mixin/create_answers.json new file mode 100644 index 0000000..4ea4d18 --- /dev/null +++ b/test/e2e/create/mixin/create_answers.json @@ -0,0 +1,4 @@ +{ + "mixinFolder": "tmp", + "mixinName": "ola" +} diff --git a/test/e2e/create/mixin/create_answers_ts.json b/test/e2e/create/mixin/create_answers_ts.json new file mode 100644 index 0000000..5c732b3 --- /dev/null +++ b/test/e2e/create/mixin/create_answers_ts.json @@ -0,0 +1,6 @@ +{ + "mixinFolder": "tmp", + "mixinName": "ola", + "typescript": true +} + diff --git a/test/e2e/create/mixin/mocks/ola.mixin.js b/test/e2e/create/mixin/mocks/ola.mixin.js new file mode 100644 index 0000000..146c2d6 --- /dev/null +++ b/test/e2e/create/mixin/mocks/ola.mixin.js @@ -0,0 +1,68 @@ +"use strict"; + +const fs = require("fs"); +const mongoose = require("mongoose"); // npm i mongoose -S + +module.exports = (collection, modelSchema) => { + const schema = { + actions: { + create: { + rest: "POST /", + async handler(ctx) { + const { params } = ctx; + console.log(this.adapter); + this.adapter.create(params, (err, saved) => { + if (err) this.logger.error(err); + this.logger.info(saved); + }); + }, + }, + update: { + rest: "PUT /:id", + async handler(ctx) { + const { params } = ctx; + this.adapter.findOneAndUpdate( + { _id: params.id }, + params, + (err, saved) => { + if (err) this.logger.error(err); + this.logger.info(saved); + } + ); + }, + }, + list: { + rest: "GET /", + async handler(ctx) { + const { params } = ctx; + return this.adapter.find({}); + }, + }, + + delete: { + rest: "DELETE /:id", + async handler(ctx) { + const { params } = ctx; + this.adapter.deleteOne({ _id: params.id }); + }, + }, + }, + methods: { + _connect() { + return mongoose + .connect("mongodb://localhost:27017/test") + .then(() => this.logger.info("Connected")) + .catch((err) => this.logger.error(err)); + }, + }, + + async started() { + this._connect(); + if (!this.adapter) { + this.adapter = mongoose.model(collection, modelSchema); + } + }, + }; + + return schema; +}; diff --git a/test/e2e/create/mixin/mocks/ola.mixin.ts b/test/e2e/create/mixin/mocks/ola.mixin.ts new file mode 100644 index 0000000..1aacc39 --- /dev/null +++ b/test/e2e/create/mixin/mocks/ola.mixin.ts @@ -0,0 +1,70 @@ +import { Context, Service, ServiceSchema } from "moleculer"; +import { Document, Schema, connect, model } from "mongoose"; + +export default class OlaConnection implements Partial, ThisType{ + + private collection: string; + private modelSchema: Schema; + private schema: Partial & ThisType; + + public constructor(public collectionName: string, modelSchema: Schema) { + this.collection = collectionName; + this.modelSchema = modelSchema; + this.schema = { + actions: { + create: { + rest: "POST /", + async handler(ctx: Context) { + const { params } = ctx; + this.adapter.create(params, (err: Error, saved: Document) => { + if (err) {this.logger.error(err);}; + this.logger.info(saved); + }); + }, + }, + update: { + rest: "PUT /:id", + async handler(ctx: Context<{id: string}>) { + const { params } = ctx; + this.adapter.findOneAndUpdate( + { _id: params.id }, + params, + (err: Error, saved: Document) => { + if (err) {this.logger.error(err);} + this.logger.info(saved); + } + ); + }, + }, + list: { + rest: "GET /", + async handler(ctx: Context) { + return this.adapter.find({}); + }, + }, + + delete: { + rest: "DELETE /:id", + async handler(ctx: Context<{id: string}>) { + const { params } = ctx; + this.adapter.deleteOne({ _id: params.id }); + }, + }, + }, + methods: { + connectDb() { + return connect("mongodb://localhost:27017/test") + .then(() => this.logger.info("Connected")) + .catch((err: Error) => this.logger.error(err)); + }, + }, + async started(){ + this.connectDb(); + }, + }; + } + public start(){ + this.schema.adapter = model(this.collection, this.modelSchema); + return this.schema; + } +};