From 3f18d4179ddec292ac678f9e630247a0bca4c077 Mon Sep 17 00:00:00 2001 From: Kaushik Rishi Manchukonda Date: Mon, 25 Mar 2024 10:06:55 +0530 Subject: [PATCH 1/7] update documentation --- README.md | 14 ++---- template/README.md.js | 107 ++++++++++++++++++++++++++++++++++++++---- 2 files changed, 101 insertions(+), 20 deletions(-) diff --git a/README.md b/README.md index 2222e811..a9ab7db9 100644 --- a/README.md +++ b/README.md @@ -11,8 +11,8 @@ - [Specification requirements](#specification-requirements) - [Supported protocols](#supported-protocols) - [How to use the template](#how-to-use-the-template) - * [CLI](#cli) - * [Adding custom code / handlers](#adding-custom-code--handlers) + - [CLI](#cli) + - [Adding custom code / handlers](#adding-custom-code--handlers) - [Template configuration](#template-configuration) - [Development](#development) - [Contributors](#contributors) @@ -80,15 +80,9 @@ $ cd output # Build generated application $ npm i -# Start server -# To enable production settings start the server with "NODE_ENV=production npm start" -$ npm start +# Import the library and add your custom code and handlers -## -## Start the client -## - -#for testing your server you can use mqtt client. open a new terminal and install it using: +# for testing your server you can use mqtt client. open a new terminal and install it using: $ npm install mqtt -g #publish an invalid message. diff --git a/template/README.md.js b/template/README.md.js index 808523df..4a8f36f5 100644 --- a/template/README.md.js +++ b/template/README.md.js @@ -6,22 +6,109 @@ export default function readmeFile({asyncapi, params}) { ${ asyncapi.info().description() || '' } -## Running the server +## Set up your template 1. Install dependencies \`\`\`sh npm i \`\`\` ${(params.securityScheme && (asyncapi.server(params.server).protocol() === 'kafka' || asyncapi.server(params.server).protocol() === 'kafka-secure') && asyncapi.components().securityScheme(params.securityScheme).type() === 'X509') ? '1. (Optional) For X509 security provide files with all data required to establish secure connection using certificates. Place files like `ca.pem`, `service.cert`, `service.key` in the root of the project or the location that you explicitly specified during generation.' : ''} -1. Start the server with default configuration - \`\`\`sh - npm start - \`\`\` -1. (Optional) Start server with secure production configuration - \`\`\`sh - NODE_ENV=production npm start - \`\`\` -> NODE_ENV=production relates to \`config/common.yml\` that contains different configurations for different environments. Starting server without \`NODE_ENV\` applies default configuration while starting the server as \`NODE_ENV=production npm start\` applies default configuration supplemented by configuration settings called \`production\`.`} +## Use the generated template by adding custom code / handlers + +- use the \`client.registerMiddleware\` method as a bridge between the user-written handlers and the generated code. This can be used to register middlewares for specific methods on specific channels. + +> The AsyncAPI file used for the example is [here](https://bit.ly/asyncapi) + +\`\`\`js +// output refers to the generated template folder +// You require the generated server. Running this code starts the server +// App exposes API to send messages +const { client } = require("./output"); + +// to start the app +client.init(); + +// Generated handlers that we use to react on consumer / produced messages are attached to the client +// through which we can register middleware functions + +/** + * + * + * Example of how to process a message before it is sent to the broker + * + * + */ +function testPublish() { + // mosquitto_sub -h test.mosquitto.org -p 1883 -t "smartylighting/streetlights/1/0/action/12/turn/on" + + // Registering your custom logic in a channel-specific handler + // the passed handler function is called once the app sends a message to the channel + // For example \`client.app.send\` sends a message to some channel using and before it is sent, you want to perform some other actions + // in such a case, you can register middlewares like below + client.registerTurnOnMiddleware((message) => { // \`turnOn\` is the respective operationId + console.log("hitting the middleware before publishing the message"); + console.log( + \`sending turn on message to streetlight \${message.params.streetlightId}\`, + message.payload + ); + }); + + client.app.send( + { command: "off" }, + {}, + "smartylighting/streetlights/1/0/action/12/turn/on" + ); +} + + +/** + * + * + * Example of how to work with generated code as a consumer + * + * +*/ +function testSubscribe() { + // mosquitto_pub -h test.mosquitto.org -p 1883 -t "smartylighting/streetlights/1/0/event/101/lighting/measured" -m '{"lumens": 10}' + + // Writing your custom logic that should be triggered when your app receives as message from a given channel + // Registering your custom logic in a channel-specific handler + // the passed handler functions are called once the app gets message sent to the channel + + client.registerReceiveLightMeasurementMiddleware((message) => { // \`recieveLightMeasurement\` is the respective operationId + console.log("recieved in middleware 1", message.payload); + }); + + client.registerReceiveLightMeasurementMiddleware((message) => { + console.log("recieved in middleware 2", message.payload); + }); +} + +testPublish(); +testSubscribe(); + +/** + * + * + * Example of how to produce a message using API of generated app independently from the handlers + * + * +*/ + +(function myLoop (i) { + setTimeout(() => { + console.log('producing custom message'); + client.app.send({percentage: 1}, {}, 'smartylighting/streetlights/1/0/action/1/turn/on'); + if (--i) myLoop(i); + }, 1000); +}(3)); +\`\`\` + +You can run the above code and test the working of the handlers by sending a message using the mqtt cli / mosquitto broker software to the \`smartylighting/streetlights/1/0/event/123/lighting/measured\` channel using this command +\`mosquitto_pub -h test.mosquitto.org -p 1883 -t "smartylighting/streetlights/1/0/event/101/lighting/measured" -m '{"lumens": 10, "sentAt": "2017-06-07T12:34:32.000Z"}'\` +or +\`mqtt pub -t 'smartylighting/streetlights/1/0/event/123/lighting/measured' -h 'test.mosquitto.org' -m '{"id": 1, "lumens": 3, }'\` (if you are using the mqtt cli) +`} ; } From 46296fe2eef746eace767c3db434b706d10a2b6d Mon Sep 17 00:00:00 2001 From: Kaushik Rishi Manchukonda Date: Mon, 25 Mar 2024 10:15:15 +0530 Subject: [PATCH 2/7] update package.json to remove start script --- template/package.json.js | 3 --- 1 file changed, 3 deletions(-) diff --git a/template/package.json.js b/template/package.json.js index a2a8d2cf..d690acf8 100644 --- a/template/package.json.js +++ b/template/package.json.js @@ -38,9 +38,6 @@ export default function packageFile({ asyncapi, params }) { packageJSON = { ...packageJSON, main: './src/api', - scripts: { - start: 'node src/api/index.js', - }, dependencies, }; From d691d041db7704c1466ae6aa90115341632237eb Mon Sep 17 00:00:00 2001 From: Kaushik Rishi Manchukonda Date: Mon, 25 Mar 2024 10:16:11 +0530 Subject: [PATCH 3/7] update snapshot tests --- test/__snapshots__/integration.test.js.snap | 6 ------ 1 file changed, 6 deletions(-) diff --git a/test/__snapshots__/integration.test.js.snap b/test/__snapshots__/integration.test.js.snap index 2b0ba3e2..94a2741b 100644 --- a/test/__snapshots__/integration.test.js.snap +++ b/test/__snapshots__/integration.test.js.snap @@ -366,9 +366,6 @@ exports[`template integration tests for generated files using the generator and \\"version\\": \\"1.0.0\\", \\"description\\": \\"The Smartylighting Streetlights API allows you to remotely manage the city lights. ### Check out its awesome features: * Turn a specific streetlight on/off 🌃 * Dim a specific streetlight 😎 * Receive real-time information about environmental lighting conditions 📈\\", \\"main\\": \\"./src/api\\", - \\"scripts\\": { - \\"start\\": \\"node src/api/index.js\\" - }, \\"dependencies\\": { \\"chalk\\": \\"4.1.2\\", \\"dotenv\\": \\"8.1.0\\", @@ -748,9 +745,6 @@ exports[`template integration tests for generated files using the generator and \\"version\\": \\"1.0.0\\", \\"description\\": \\"The Smartylighting Streetlights API allows you to remotely manage the city lights. ### Check out its awesome features: * Turn a specific streetlight on/off 🌃 * Dim a specific streetlight 😎 * Receive real-time information about environmental lighting conditions 📈\\", \\"main\\": \\"./src/api\\", - \\"scripts\\": { - \\"start\\": \\"node src/api/index.js\\" - }, \\"dependencies\\": { \\"chalk\\": \\"4.1.2\\", \\"dotenv\\": \\"8.1.0\\", From a01fcf0726702bddaff06d9a9b028afdbb683100 Mon Sep 17 00:00:00 2001 From: Kaushik Rishi Manchukonda Date: Mon, 25 Mar 2024 22:17:25 +0530 Subject: [PATCH 4/7] add a test script for user to take reference from --- template/custom-handler-example/script.js | 98 +++++++++++++++++++++++ template/package.json.js | 3 + 2 files changed, 101 insertions(+) create mode 100644 template/custom-handler-example/script.js diff --git a/template/custom-handler-example/script.js b/template/custom-handler-example/script.js new file mode 100644 index 00000000..94606998 --- /dev/null +++ b/template/custom-handler-example/script.js @@ -0,0 +1,98 @@ +import { File } from '@asyncapi/generator-react-sdk'; + +export default function exampleCustomHandlerRegistrationScript() { + return + {` +// output refers to the generated template folder +// You require the generated server. Running this code starts the server +// App exposes API to send messages +const { client } = require("../"); // library is in the current directory + +client.init(); // starts the app + +// Generated handlers that we use to react on consumer / produced messages are attached to the client +// through which we can register middleware functions + +/** + * + * + * Example of how to process a message before it is sent to the broker + * + * Modify the following in the function \`testPublish\` according to your usecase + * 1. channel / topic name + * 2. tool used to publish message according to the procotol used (amqp/mqtt/etc ...) + * 3. name of the invocated middleware function (based on operation Id) + */ +function testPublish() { + // mosquitto_sub -h test.mosquitto.org -p 1883 -t "smartylighting/streetlights/1/0/action/12/turn/on" + + // Registering your custom logic in a channel-specific handler + // the passed handler function is called once the app sends a message to the channel + // For example \`client.app.send\` sends a message to some channel using and before it is sent, you want to perform some other actions + // in such a case, you can register middlewares like below + client.registerTurnOnMiddleware((message) => { // \`turnOn\` is the respective operationId + console.log("hitting the middleware before publishing the message"); + console.log( + \`sending turn on message to streetlight \${message.params.streetlightId}\`, + message.payload + ); + }); + + client.app.send( + { command: "off" }, + {}, + "smartylighting/streetlights/1/0/action/12/turn/on" + ); +} + + +/** + * + * + * Example of how to work with generated code as a consumer + * + * Modify the following in the function \`testSubscribe\` according to your usecase + * 1. channel / topic name + * 2. tool used to publish message according to the procotol used (amqp/mqtt/etc ...) + * 3. name of the invocated middleware function (based on operation Id) + * +*/ +function testSubscribe() { + // mosquitto_pub -h test.mosquitto.org -p 1883 -t "smartylighting/streetlights/1/0/event/101/lighting/measured" -m '{"lumens": 10}' + + // Writing your custom logic that should be triggered when your app receives as message from a given channel + // Registering your custom logic in a channel-specific handler + // the passed handler functions are called once the app gets message sent to the channel + + client.registerReceiveLightMeasurementMiddleware((message) => { // \`recieveLightMeasurement\` is the respective operationId + console.log("recieved in middleware 1", message.payload); + }); + + client.registerReceiveLightMeasurementMiddleware((message) => { + console.log("recieved in middleware 2", message.payload); + }); +} + +testPublish(); +testSubscribe(); + +/** + * + * + * Example of how to produce a message using API of generated app independently from the handlers + * Again, will have to modify the below according to your usecase: + * 1. payload + * 2. channel / topic name + * +*/ + +(function myLoop (i) { + setTimeout(() => { + console.log('producing custom message'); + client.app.send({percentage: 1}, {}, 'smartylighting/streetlights/1/0/action/1/turn/on'); + if (--i) myLoop(i); + }, 1000); +}(3)); + `} + ; +} \ No newline at end of file diff --git a/template/package.json.js b/template/package.json.js index d690acf8..3153926a 100644 --- a/template/package.json.js +++ b/template/package.json.js @@ -37,6 +37,9 @@ export default function packageFile({ asyncapi, params }) { packageJSON = { ...packageJSON, + scripts: { + 'test': 'node custom-handler-example/script.js' + }, main: './src/api', dependencies, }; From ed02253ae568b38d931f4409529b26b113d097d5 Mon Sep 17 00:00:00 2001 From: Kaushik Rishi Manchukonda Date: Mon, 25 Mar 2024 22:19:33 +0530 Subject: [PATCH 5/7] update snapshot tests --- test/__snapshots__/integration.test.js.snap | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/test/__snapshots__/integration.test.js.snap b/test/__snapshots__/integration.test.js.snap index 94a2741b..eb52a395 100644 --- a/test/__snapshots__/integration.test.js.snap +++ b/test/__snapshots__/integration.test.js.snap @@ -365,6 +365,9 @@ exports[`template integration tests for generated files using the generator and \\"name\\": \\"streetlights-mqtt-api\\", \\"version\\": \\"1.0.0\\", \\"description\\": \\"The Smartylighting Streetlights API allows you to remotely manage the city lights. ### Check out its awesome features: * Turn a specific streetlight on/off 🌃 * Dim a specific streetlight 😎 * Receive real-time information about environmental lighting conditions 📈\\", + \\"scripts\\": { + \\"test\\": \\"node custom-handler-example/script.js\\" + }, \\"main\\": \\"./src/api\\", \\"dependencies\\": { \\"chalk\\": \\"4.1.2\\", @@ -744,6 +747,9 @@ exports[`template integration tests for generated files using the generator and \\"name\\": \\"streetlights-mqtt-api\\", \\"version\\": \\"1.0.0\\", \\"description\\": \\"The Smartylighting Streetlights API allows you to remotely manage the city lights. ### Check out its awesome features: * Turn a specific streetlight on/off 🌃 * Dim a specific streetlight 😎 * Receive real-time information about environmental lighting conditions 📈\\", + \\"scripts\\": { + \\"test\\": \\"node custom-handler-example/script.js\\" + }, \\"main\\": \\"./src/api\\", \\"dependencies\\": { \\"chalk\\": \\"4.1.2\\", From c6dff0542fbdb1b554ead359b9b3bd53acf0303d Mon Sep 17 00:00:00 2001 From: Kaushik Rishi Manchukonda Date: Mon, 25 Mar 2024 22:22:30 +0530 Subject: [PATCH 6/7] fix eslint react issues --- template/custom-handler-example/script.js | 10 ++++------ template/package.json.js | 2 +- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/template/custom-handler-example/script.js b/template/custom-handler-example/script.js index 94606998..cc0ac4f3 100644 --- a/template/custom-handler-example/script.js +++ b/template/custom-handler-example/script.js @@ -1,9 +1,8 @@ import { File } from '@asyncapi/generator-react-sdk'; export default function exampleCustomHandlerRegistrationScript() { - return - {` -// output refers to the generated template folder + return + {`// output refers to the generated template folder // You require the generated server. Running this code starts the server // App exposes API to send messages const { client } = require("../"); // library is in the current directory @@ -92,7 +91,6 @@ testSubscribe(); client.app.send({percentage: 1}, {}, 'smartylighting/streetlights/1/0/action/1/turn/on'); if (--i) myLoop(i); }, 1000); -}(3)); - `} - ; +}(3));`} + ; } \ No newline at end of file diff --git a/template/package.json.js b/template/package.json.js index 3153926a..9cef196a 100644 --- a/template/package.json.js +++ b/template/package.json.js @@ -38,7 +38,7 @@ export default function packageFile({ asyncapi, params }) { packageJSON = { ...packageJSON, scripts: { - 'test': 'node custom-handler-example/script.js' + test: 'node custom-handler-example/script.js' }, main: './src/api', dependencies, From 972e224cc0f1874085d566284734238aa4aebc61 Mon Sep 17 00:00:00 2001 From: Kaushik Rishi Manchukonda Date: Mon, 29 Apr 2024 15:49:53 +0530 Subject: [PATCH 7/7] rewrite a couple files --- helpers/channels-topics.js | 8 +- hooks/beautify-output.js | 10 +- package-lock.json | 14 +- package.json | 15 +- template/README.md.js | 17 +- template/config/common.yml.js | 20 +-- template/custom-handler-example/script.js | 96 ----------- template/package.json.js | 2 +- template/src/api/handlers/handler.js | 187 ---------------------- template/src/api/index.js | 54 ++++--- template/src/api/routes/route.js | 136 ---------------- 11 files changed, 80 insertions(+), 479 deletions(-) delete mode 100644 template/custom-handler-example/script.js delete mode 100644 template/src/api/handlers/handler.js delete mode 100644 template/src/api/routes/route.js diff --git a/helpers/channels-topics.js b/helpers/channels-topics.js index 05772ee4..5d45577f 100644 --- a/helpers/channels-topics.js +++ b/helpers/channels-topics.js @@ -50,11 +50,8 @@ export function toHermesTopic(str) { return str.replace(/\{([^}]+)\}/g, ':$1'); } -export function channelNamesWithPublish(asyncapi) { - const result = []; - asyncapi.channelNames().forEach((name) => { - if (asyncapi.channel(name).hasPublish()) result.push(name); - }); +export function channelNamesWithReceive(asyncapi) { + const result = asyncapi.channels().filterByReceive().map(channel => channel.id()); return result; } @@ -69,6 +66,7 @@ export function port(url, defaultPort) { } export function stripProtocol(url) { + console.log(url); if (!url.includes('://')) { return url; } diff --git a/hooks/beautify-output.js b/hooks/beautify-output.js index 06de054c..94a43291 100644 --- a/hooks/beautify-output.js +++ b/hooks/beautify-output.js @@ -54,11 +54,11 @@ module.exports = { generator.targetDir, 'src/api/index.js' ); - const handlersPath = path.resolve(generator.targetDir, 'src/api/handlers'); - const routesPath = path.resolve(generator.targetDir, 'src/api/routes'); + // const handlersPath = path.resolve(generator.targetDir, 'src/api/handlers'); + // const routesPath = path.resolve(generator.targetDir, 'src/api/routes'); - beautifyAllOutputFiles(handlersPath); - beautifyAllOutputFiles(routesPath); - beautifySingleFile(entryPointFilePath); + // beautifyAllOutputFiles(handlersPath); + // beautifyAllOutputFiles(routesPath); + // beautifySingleFile(entryPointFilePath); }, }; diff --git a/package-lock.json b/package-lock.json index d1b201e4..c15a5798 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,19 +13,19 @@ "@asyncapi/generator-hooks": "^0.1.0", "@asyncapi/generator-react-sdk": "^1.0.14", "eslint-plugin-react": "^7.34.1", - "filenamify": "^4.1.0", + "filenamify": "^4.3.0", "js-beautify": "^1.15.1", - "lodash": "^4.17.15", + "lodash": "^4.17.21", "markdown-toc": "^1.2.0" }, "devDependencies": { - "@asyncapi/generator": "^1.17.13", - "eslint": "^8.7.0", + "@asyncapi/generator": "^1.17.12", + "eslint": "^8.57.0", "eslint-plugin-jest": "^25.7.0", "eslint-plugin-sonarjs": "^0.11.0", - "jest": "^27.3.1", - "node-fetch": "^2.6.1", - "rimraf": "^5.0.1" + "jest": "^27.5.1", + "node-fetch": "^2.7.0", + "rimraf": "^5.0.5" } }, "node_modules/@aashutoshrathi/word-wrap": { diff --git a/package.json b/package.json index f936b790..594293ff 100644 --- a/package.json +++ b/package.json @@ -35,21 +35,22 @@ "@asyncapi/generator-hooks": "^0.1.0", "@asyncapi/generator-react-sdk": "^1.0.14", "eslint-plugin-react": "^7.34.1", - "filenamify": "^4.1.0", + "filenamify": "^4.3.0", "js-beautify": "^1.15.1", - "lodash": "^4.17.15", + "lodash": "^4.17.21", "markdown-toc": "^1.2.0" }, "devDependencies": { - "@asyncapi/generator": "^1.17.13", - "eslint": "^8.7.0", + "@asyncapi/generator": "^1.17.12", + "eslint": "^8.57.0", "eslint-plugin-jest": "^25.7.0", "eslint-plugin-sonarjs": "^0.11.0", - "jest": "^27.3.1", - "node-fetch": "^2.6.1", - "rimraf": "^5.0.1" + "jest": "^27.5.1", + "node-fetch": "^2.7.0", + "rimraf": "^5.0.5" }, "generator": { + "apiVersion": "v3", "supportedProtocols": [ "amqp", "mqtt", diff --git a/template/README.md.js b/template/README.md.js index 4a8f36f5..599cd6f2 100644 --- a/template/README.md.js +++ b/template/README.md.js @@ -1,6 +1,21 @@ import { File } from '@asyncapi/generator-react-sdk'; export default function readmeFile({asyncapi, params}) { + const server = asyncapi.allServers().get(params.server); + const protocol = server.protocol(); + const security = server.security(); + + let hasSecuritySchemeX509 = false; + let securitySchemeType; + if (params.securityScheme && security && security.length > 0) { + const securityReq = security[0].all(); + if (securityReq && securityReq.length > 0) { + securitySchemeType = securityReq[0].scheme().type(); + } + } + + hasSecuritySchemeX509 = (params.securityScheme && (protocol === 'kafka' || protocol === 'kafka-secure') && securitySchemeType === 'X509'); + return {`# ${ asyncapi.info().title() } @@ -12,7 +27,7 @@ ${ asyncapi.info().description() || '' } \`\`\`sh npm i \`\`\` -${(params.securityScheme && (asyncapi.server(params.server).protocol() === 'kafka' || asyncapi.server(params.server).protocol() === 'kafka-secure') && asyncapi.components().securityScheme(params.securityScheme).type() === 'X509') ? '1. (Optional) For X509 security provide files with all data required to establish secure connection using certificates. Place files like `ca.pem`, `service.cert`, `service.key` in the root of the project or the location that you explicitly specified during generation.' : ''} +${hasSecuritySchemeX509 ? '1. (Optional) For X509 security provide files with all data required to establish secure connection using certificates. Place files like `ca.pem`, `service.cert`, `service.key` in the root of the project or the location that you explicitly specified during generation.' : ''} ## Use the generated template by adding custom code / handlers diff --git a/template/config/common.yml.js b/template/config/common.yml.js index 9bd54fb5..6263a4c5 100644 --- a/template/config/common.yml.js +++ b/template/config/common.yml.js @@ -1,11 +1,12 @@ import { File } from '@asyncapi/generator-react-sdk'; -import { camelCase, channelNamesWithPublish, dump, host, port, queueName, stripProtocol, toAmqpTopic, toKafkaTopic, toMqttTopic } from '../../helpers/index'; +import { camelCase, channelNamesWithReceive, dump, host, port, queueName, stripProtocol, toAmqpTopic, toKafkaTopic, toMqttTopic } from '../../helpers/index'; import { replaceServerVariablesWithValues } from '@asyncapi/generator-filters/src/customFilters'; export default function CommonConfigYAMLRender({ asyncapi, params }) { - const serverProtocol = asyncapi.server(params.server).protocol(); - const serverVariables = asyncapi.server(params.server).variables(); - const resolvedBrokerUrlWithReplacedVariables = replaceServerVariablesWithValues(asyncapi.server(params.server).url(), serverVariables); + const server = asyncapi.allServers().get(params.server); + const serverProtocol = server.protocol(); + const serverVariables = server.variables(); + const resolvedBrokerUrlWithReplacedVariables = replaceServerVariablesWithValues(server.url(), serverVariables); return ( @@ -47,7 +48,7 @@ function amqpBlock(url, asyncapi) { password: host: ${host(url)} port: - topics: ${dump(toAmqpTopic(channelNamesWithPublish(asyncapi)))} + topics: ${dump(toAmqpTopic(channelNamesWithReceive(asyncapi)))} queue: ${queueName(asyncapi.info().title(), asyncapi.info().version())} queueOptions: exclusive: false @@ -57,9 +58,10 @@ function amqpBlock(url, asyncapi) { } function mqttBlock(url, asyncapi, params) { + const server = asyncapi.allServers().get(params.server); return ` mqtt: - url: ${asyncapi.server(params.server).protocol()}://${stripProtocol(url)} - topics: ${dump(toMqttTopic(channelNamesWithPublish(asyncapi)))} + url: ${server.protocol()}://${stripProtocol(url)} + topics: ${dump(toMqttTopic(channelNamesWithReceive(asyncapi)))} qos: protocol: mqtt retain: @@ -75,7 +77,7 @@ function kafkaBlock(url, asyncapi) { consumerOptions: groupId: ${camelCase(asyncapi.info().title())} topics: - ${channelNamesWithPublish(asyncapi).map(topic => `- ${toKafkaTopic(topic)}`).join('\n')} + ${channelNamesWithReceive(asyncapi).map(topic => `- ${toKafkaTopic(topic)}`).join('\n')} topicSeparator: '__' topicPrefix: `; @@ -87,7 +89,7 @@ function kafkaProductionBlock(params, asyncapi) { ssl: rejectUnauthorized: true `; - if (params.securityScheme && asyncapi.components().securityScheme(params.securityScheme).type() !== 'X509') { + if (params.securityScheme && asyncapi.components().securitySchemes().get(params.securityScheme).type() !== 'X509') { productionBlock += ` sasl: mechanism: 'plain' username: diff --git a/template/custom-handler-example/script.js b/template/custom-handler-example/script.js deleted file mode 100644 index cc0ac4f3..00000000 --- a/template/custom-handler-example/script.js +++ /dev/null @@ -1,96 +0,0 @@ -import { File } from '@asyncapi/generator-react-sdk'; - -export default function exampleCustomHandlerRegistrationScript() { - return - {`// output refers to the generated template folder -// You require the generated server. Running this code starts the server -// App exposes API to send messages -const { client } = require("../"); // library is in the current directory - -client.init(); // starts the app - -// Generated handlers that we use to react on consumer / produced messages are attached to the client -// through which we can register middleware functions - -/** - * - * - * Example of how to process a message before it is sent to the broker - * - * Modify the following in the function \`testPublish\` according to your usecase - * 1. channel / topic name - * 2. tool used to publish message according to the procotol used (amqp/mqtt/etc ...) - * 3. name of the invocated middleware function (based on operation Id) - */ -function testPublish() { - // mosquitto_sub -h test.mosquitto.org -p 1883 -t "smartylighting/streetlights/1/0/action/12/turn/on" - - // Registering your custom logic in a channel-specific handler - // the passed handler function is called once the app sends a message to the channel - // For example \`client.app.send\` sends a message to some channel using and before it is sent, you want to perform some other actions - // in such a case, you can register middlewares like below - client.registerTurnOnMiddleware((message) => { // \`turnOn\` is the respective operationId - console.log("hitting the middleware before publishing the message"); - console.log( - \`sending turn on message to streetlight \${message.params.streetlightId}\`, - message.payload - ); - }); - - client.app.send( - { command: "off" }, - {}, - "smartylighting/streetlights/1/0/action/12/turn/on" - ); -} - - -/** - * - * - * Example of how to work with generated code as a consumer - * - * Modify the following in the function \`testSubscribe\` according to your usecase - * 1. channel / topic name - * 2. tool used to publish message according to the procotol used (amqp/mqtt/etc ...) - * 3. name of the invocated middleware function (based on operation Id) - * -*/ -function testSubscribe() { - // mosquitto_pub -h test.mosquitto.org -p 1883 -t "smartylighting/streetlights/1/0/event/101/lighting/measured" -m '{"lumens": 10}' - - // Writing your custom logic that should be triggered when your app receives as message from a given channel - // Registering your custom logic in a channel-specific handler - // the passed handler functions are called once the app gets message sent to the channel - - client.registerReceiveLightMeasurementMiddleware((message) => { // \`recieveLightMeasurement\` is the respective operationId - console.log("recieved in middleware 1", message.payload); - }); - - client.registerReceiveLightMeasurementMiddleware((message) => { - console.log("recieved in middleware 2", message.payload); - }); -} - -testPublish(); -testSubscribe(); - -/** - * - * - * Example of how to produce a message using API of generated app independently from the handlers - * Again, will have to modify the below according to your usecase: - * 1. payload - * 2. channel / topic name - * -*/ - -(function myLoop (i) { - setTimeout(() => { - console.log('producing custom message'); - client.app.send({percentage: 1}, {}, 'smartylighting/streetlights/1/0/action/1/turn/on'); - if (--i) myLoop(i); - }, 1000); -}(3));`} - ; -} \ No newline at end of file diff --git a/template/package.json.js b/template/package.json.js index 9cef196a..1bf1ed83 100644 --- a/template/package.json.js +++ b/template/package.json.js @@ -12,7 +12,7 @@ export default function packageFile({ asyncapi, params }) { 'node-yaml-config': '0.0.4', }; - const serverProtocol = asyncapi.server(params.server).protocol(); + const serverProtocol = asyncapi.allServers().get(params.server).protocol(); if (serverProtocol === 'mqtt' || serverProtocol === 'mqtts') { dependencies['hermesjs-mqtt'] = '2.x'; } else if (serverProtocol === 'kafka' || serverProtocol === 'kafka-secure') { diff --git a/template/src/api/handlers/handler.js b/template/src/api/handlers/handler.js deleted file mode 100644 index 6a2320e6..00000000 --- a/template/src/api/handlers/handler.js +++ /dev/null @@ -1,187 +0,0 @@ -/* eslint-disable indent */ -import { docline } from '@asyncapi/generator-filters/src/customFilters'; -import { - convertOpertionIdToMiddlewareFn, - convertToFilename, -} from '../../../../helpers/index'; -import { File } from '@asyncapi/generator-react-sdk'; - -const OPTIONS_MESSAGE_HEADERS_STRING = 'options.message.headers'; - -function publishHandler(channel) { - if (!channel.hasPublish()) { - return ''; - } - - const lambdaChannel = channel.publish().ext('x-lambda'); - const publishOperationId = channel.publish().id(); - const publishMessage = channel.publish().message(0); - - const exportedHandler = ` - /** - * Registers a middleware function for the ${publishOperationId} operation to be executed during request processing. - * - * Middleware functions have access to options object that you can use to access the message content and other helper functions - * - * @param {function} middlewareFn - The middleware function to be registered. - * @throws {TypeError} If middlewareFn is not a function. - */ - handler.${convertOpertionIdToMiddlewareFn( - channel.publish().id() - )} = (middlewareFn) => { - if (typeof middlewareFn !== 'function') { - throw new TypeError('middlewareFn must be a function'); - } - ${publishOperationId}Middlewares.push(middlewareFn); - } - `; - - const privateHandlerLogic = ` - /** - * ${channel.publish().summary() || ''} - * - * @param {object} options - * @param {object} options.message - ${ - publishMessage.headers() - ? Object.entries(publishMessage.headers().properties()) - .map(([fieldName, field]) => { - return docline(field, fieldName, OPTIONS_MESSAGE_HEADERS_STRING); - }) - .join('\n') - : '' - } - * - ${ - publishMessage.payload() - ? Object.entries(publishMessage.payload().properties()) - .map(([fieldName, field]) => { - return docline(field, fieldName, OPTIONS_MESSAGE_HEADERS_STRING); - }) - .join('\n') - : '' - } - */ - handler._${publishOperationId} = async ({message}) => { - ${ - lambdaChannel - ? ` - fetch('${lambdaChannel.url}}', { - method: "${lambdaChannel.method || 'POST'}", - body: JSON.stringify(${lambdaChannel.body}), - ${lambdaChannel.headers ? `headers: ${lambdaChannel.headers}` : ''} - }) - .then(res => res.json()) - .then(json => console.log(json)) - .catch(err => { throw err; });` - : `for (const middleware of ${publishOperationId}Middlewares) { - await middleware(message); - }` - } - }; - `; - - return ` - ${lambdaChannel ? 'const fetch = require("node-fetch");' : ''} - - const ${publishOperationId}Middlewares = []; - - ${exportedHandler} - - ${privateHandlerLogic} - `; -} - -function subscribeHandler(channel) { - if (!channel.hasSubscribe()) { - return ''; - } - - const subscribeOperationId = channel.subscribe().id(); - const subscribeMessage = channel.subscribe().message(0); - - const exportedHandler = ` - /** - * Registers a middleware function for the ${subscribeOperationId} operation to be executed during request processing. - * - * Middleware functions have access to options object that you can use to access the message content and other helper functions - * - * @param {function} middlewareFn - The middleware function to be registered. - * @throws {TypeError} If middlewareFn is not a function. - */ - handler.${convertOpertionIdToMiddlewareFn( - channel.subscribe().id() - )} = (middlewareFn) => { - if (typeof middlewareFn !== 'function') { - throw new TypeError('middlewareFn must be a function'); - } - ${subscribeOperationId}Middlewares.push(middlewareFn); - } - `; - - const privateHandlerLogic = ` - /** - * ${channel.subscribe().summary() || ''} - * - * @param {object} options - * @param {object} options.message - ${ - subscribeMessage.headers() - ? Object.entries(subscribeMessage.headers().properties()) - .map(([fieldName, field]) => { - return docline(field, fieldName, OPTIONS_MESSAGE_HEADERS_STRING); - }) - .join('\n') - : '' - } - * - ${ - subscribeMessage.payload() - ? Object.entries(subscribeMessage.payload().properties()) - .map(([fieldName, field]) => { - return docline(field, fieldName, OPTIONS_MESSAGE_HEADERS_STRING); - }) - .join('\n') - : '' - } - */ - handler._${subscribeOperationId} = async ({message}) => { - for (const middleware of ${subscribeOperationId}Middlewares) { - await middleware(message); - } - }; - `; - - return ` - const ${subscribeOperationId}Middlewares = []; - - ${exportedHandler} - - ${privateHandlerLogic} - `; -} - -export default function handlerRender({ - asyncapi, -}) { - const general = ` - const handler = module.exports = {}; - `; - - const channels = asyncapi.channels(); - - return Object.entries(channels).map(([channelName, channel]) => { - const hasPublish = channel.publish(); - const hasSubscribe = channel.hasSubscribe(); - - return ( - - {` - ${general} - ${hasPublish ? publishHandler(channel) : ''} - ${hasSubscribe ? subscribeHandler(channel) : ''} - `} - - ); - }); -} diff --git a/template/src/api/index.js b/template/src/api/index.js index 32027199..e7fdf3f3 100644 --- a/template/src/api/index.js +++ b/template/src/api/index.js @@ -2,7 +2,8 @@ import { File } from '@asyncapi/generator-react-sdk'; import { capitalize, getProtocol, getConfig, camelCase, convertToFilename, convertOpertionIdToMiddlewareFn } from '../../../helpers/index'; export default function indexEntrypointFile({asyncapi, params}) { - const protocol = asyncapi.server(params.server).protocol() === 'mqtts' ? 'mqtt' : asyncapi.server(params.server).protocol(); + const server = asyncapi.allServers().get(params.server); + const protocol = server.protocol() === 'mqtts' ? 'mqtt' : server.protocol(); const capitalizedProtocol = capitalize(getProtocol(protocol)); const standardImports = ` @@ -20,11 +21,12 @@ export default function indexEntrypointFile({asyncapi, params}) { const ${capitalizedProtocol}Adapter = require('hermesjs-${protocol}'); `; - const channelHandlerImports = Object.entries(asyncapi.channels()).map(([channelName, channel]) => { + const channelHandlerImports = asyncapi.channels().all().map(channel => { + const channelName = channel.address(); return `const ${camelCase(channelName)} = require('./routes/${convertToFilename(channelName)}.js');`; }).join('\n'); - const isSecurityEnabled = params.securityScheme && (asyncapi.server(params.server).protocol() === 'kafka' || asyncapi.server(params.server).protocol() === 'kafka-secure') && (asyncapi.components().securityScheme(params.securityScheme).type() === 'X509'); + const isSecurityEnabled = params.securityScheme && (server.protocol() === 'kafka' || server.protocol() === 'kafka-secure') && (asyncapi.components().securitySchemes().get(params.securityScheme).type() !== 'X509'); let securitySchemeImports = isSecurityEnabled ? ` const fs = require('fs') const certFilesDir = '${params.certFilesDir}'; @@ -44,18 +46,21 @@ export default function indexEntrypointFile({asyncapi, params}) { ` const channelsMiddleware = ` - ${Object.entries(asyncapi.channels()).map(([channelName, channel]) => { + ${asyncapi.channels().filterByReceive().map(channel => { + const channelAddress = channel.address(); let channelLogic = ''; - if (channel.hasPublish()) { - channelLogic += `console.log(cyan.bold.inverse(' SUB '), gray('Subscribed to'), yellow('${channelName}')); - app.use(${camelCase(channelName)});`; - } - if (channel.hasSubscribe()) { - channelLogic += `console.log(yellow.bold.inverse(' PUB '), gray('Will eventually publish to'), yellow('${channelName}')); - app.useOutbound(${camelCase(channelName)});`; - } + channelLogic += `console.log(cyan.bold.inverse(' SUB '), gray('Subscribed to'), yellow('${channelAddress}')); + app.use(${camelCase(channelAddress)});`; return channelLogic; - }).join('\n')}`; + }).join('\n')} + ${asyncapi.channels().filterBySend().map(channel => { + const channelAddress = channel.address(); + let channelLogic = ''; + channelLogic += `console.log(yellow.bold.inverse(' PUB '), gray('Will eventually publish to'), yellow('${channelAddress}')); + app.useOutbound(${camelCase(channelAddress)});`; + return channelLogic; + }).join('\n')} + `; const middlewares = ` app.addAdapter(${capitalizedProtocol}Adapter, serverConfig); @@ -71,7 +76,7 @@ export default function indexEntrypointFile({asyncapi, params}) { app.useOutbound(errorLogger); app.useOutbound(logger); app.useOutbound(json2string); - ` + `; const initFunction = ` function init() { @@ -87,17 +92,16 @@ export default function indexEntrypointFile({asyncapi, params}) { } `; - const handlers = Object.entries(asyncapi.channels()).map(([channelName, channel]) => { - let handler = ''; - if (channel.hasPublish()) { - handler += `${convertOpertionIdToMiddlewareFn(channel.publish().id())} : require('./handlers/${convertToFilename(channelName)}').${convertOpertionIdToMiddlewareFn(channel.publish().id())},`; - } - if (channel.hasSubscribe()) { - handler += `${convertOpertionIdToMiddlewareFn(channel.subscribe().id())} : require('./handlers/${convertToFilename(channelName)}').${convertOpertionIdToMiddlewareFn(channel.subscribe().id())},`; - } - return handler; - }).join('\n'); - + const handlers = asyncapi.channels() + .all() + .map( + (channel) => { + const channelAddress = channel.address(); + const operationId = channel.operations()[0]. + return `${convertOpertionIdToMiddlewareFn(channel.id())} : require('./handlers/${convertToFilename(channelAddress)}').${convertOpertionIdToMiddlewareFn(channel.id())},`; + }) + .join("\n"); + return {` ${imports} diff --git a/template/src/api/routes/route.js b/template/src/api/routes/route.js deleted file mode 100644 index d34f3c31..00000000 --- a/template/src/api/routes/route.js +++ /dev/null @@ -1,136 +0,0 @@ -/* eslint-disable indent */ -import { File } from '@asyncapi/generator-react-sdk'; -import { camelCase, convertToFilename, toHermesTopic } from '../../../../helpers/index'; - -function publishHandler(channel, channelName) { - if (!channel.hasPublish()) { - return ''; - } - - const publishOperationId = channel.publish().id(); - const publishMessage = channel.publish().message(0); - - return ` - ${channel.publish().summary() ? ` - /** - * ${ channel.publish().summary() } - */ - `: ''} - router.use('${toHermesTopic(channelName)}', async (message, next) => { - try { - ${channel.publish().hasMultipleMessages() - ? ` - /* - * TODO: If https://github.com/asyncapi/parser-js/issues/372 is addressed, simplify this - * code to just validate the message against the combined message schema which will - * include the \`oneOf\` in the JSON schema - let the JSON schema validator handle the - * oneOf semantics (rather than each generator having to emit conditional code) - */ - let nValidated = 0; - // For oneOf, only one message schema should match. - // Validate payload against each message and count those which validate - - ${ - Array.from(Array(channel.publish().messages().length).keys()).map(i => `try { - nValidated = await validateMessage(message.payload,'${ channelName }','${ channel.publish().message(i).name() }','publish', nValidated); - } catch { };`).join('\n') - } - - if (nValidated === 1) { - await ${camelCase(channelName)}Handler._${publishOperationId}({message}); - next() - } else { - throw new Error(\`\${nValidated} of ${ channel.publish().messages().length } message schemas matched when exactly 1 should match\`); - } - ` - : ` - await validateMessage(message.payload,'${ channelName }','${ publishMessage.name() }','publish'); - await ${camelCase(channelName)}Handler._${ publishOperationId }({message}); - next(); - ` - } - } catch (e) { - next(e); - } - }); - `; -} - -function subscribeHandler(channel, channelName) { - if (!channel.hasSubscribe()) { - return ''; - } - - const subscribeOperationId = channel.subscribe().id(); - const subscribeMessage = channel.subscribe().message(0); - - return ` - ${channel.subscribe().summary() ? ` - /** - * ${ channel.subscribe().summary() } - */ - `: ''} - router.use('${toHermesTopic(channelName)}', async (message, next) => { - try { - ${channel.subscribe().hasMultipleMessages() - ? ` - let nValidated = 0; - // For oneOf, only one message schema should match. - // Validate payload against each message and count those which validate - - ${ - Array.from(Array(channel.subscribe().messages().length).keys()).map(i => `try { - nValidated = await validateMessage(message.payload,'${ channelName }','${ channel.subscribe().message(i).name() }','subscribe', nValidated); - } catch { };`).join('\n') - } - - if (nValidated === 1) { - await ${camelCase(channelName)}Handler._${subscribeOperationId}({message}); - next() - } else { - throw new Error(\`\${nValidated} of ${ channel.subscribe().messages().length } message schemas matched when exactly 1 should match\`); - } - ` - : ` - await validateMessage(message.payload,'${ channelName }','${ subscribeMessage.name() }','subscribe'); - await ${camelCase(channelName)}Handler._${ subscribeOperationId }({message}); - next(); - ` - } - } catch (e) { - next(e); - } - }); - `; -} - -function routeCode(channel, channelName) { - const hasPublish = channel.publish(); - const hasSubscribe = channel.hasSubscribe(); - - const generalImport = ` - const Router = require('hermesjs/lib/router'); - const { validateMessage } = require('../../lib/message-validator'); - const router = new Router(); - const ${ camelCase(channelName) }Handler = require('../handlers/${convertToFilename(channelName)}'); - module.exports = router; - `; - - return ( - - {` - ${generalImport} - ${hasPublish ? publishHandler(channel, channelName): ''} - ${hasSubscribe ? subscribeHandler(channel, channelName): ''} - `} - - ); -} - -export default function routeRender({asyncapi}) { - const channels = asyncapi.channels(); - - return Object.entries(channels).map(([channelName, channel]) => { - return routeCode(channel, channelName); - }); -} \ No newline at end of file