diff --git a/.narval.yml b/.narval.yml index 6c3d62e..f0d0d2f 100644 --- a/.narval.yml +++ b/.narval.yml @@ -152,6 +152,9 @@ docker-containers: - name: service-container build: node-image bind: *bind + - name: module-container + build: node-image + bind: *bind - name: test-container build: node-image bind: *bind @@ -345,6 +348,91 @@ suites: command: test/functional/commands/start-cli.sh env: *documentation-example-docker-env test: *documentation-example-test + coverage: *disable-coverage + functional-plugin: + - name: plugin-no-controller-provided + describe: should print an api key valid to launch a pairing command from controller + before: *clean + services: &plugin-service + - name: domapic-service + abort-on-error: true + local: + <<: *local-service-auth + env: + <<: *local-service-auth-env + fixture: plugin + docker: + <<: *docker-service-auth + env: + <<: *docker-service-auth-env + fixture: plugin + test: + <<: *functional-test + specs: + - test/functional/specs/no-controller-api-key.specs.js + - test/functional/specs/not-connected.specs.js + coverage: *disable-coverage + - name: plugin-api + describe: plugin apis should work as expected + before: *clean + services: *plugin-service + test: + <<: *functional-test + specs: + - test/functional/specs/auth-api.specs.js + - test/functional/plugin-specs/about-api.specs.js + - test/functional/plugin-specs/config-api.specs.js + - test/functional/plugin-specs/events-api.specs.js + - test/functional/plugin-specs/controller-interface.specs.js + coverage: *disable-coverage + - name: plugin-connection-api-unavailable + describe: should try to connect to controller when calling to connection api, and return server unavailable if controller is unavailable + before: *clean + services: + - name: domapic-service + abort-on-error: true + local: + <<: *local-service + env: + <<: *local-service-env + fixture: plugin + docker: + <<: *docker-service + env: + <<: *docker-service-env + fixture: plugin + test: + <<: *functional-test + specs: + - test/functional/specs/no-controller-api-key.specs.js + - test/functional/specs/not-connected.specs.js + - test/functional/specs/connection-api-unavailable.specs.js + - test/functional/specs/not-connected.specs.js + coverage: *disable-coverage + - name: plugin-cli + describe: plugin example should work as expected when started using cli + before: *clean + services: + - name: domapic-service + abort-on-error: true + local: + <<: *local-service-auth + command: test/functional/commands/start-plugin-cli.sh + env: + <<: *local-service-auth-env + fixture: plugin + docker: + <<: *docker-service-auth + command: test/functional/commands/start-plugin-cli.sh + env: + <<: *docker-service-auth-env + fixture: plugin + test: + <<: *functional-test + specs: + - test/functional/plugin-specs/about-api.specs.js + - test/functional/plugin-specs/config-api.specs.js + coverage: *disable-coverage end-to-end: - name: service-connection describe: Service should connect to controller when started first time if connection options are provided @@ -573,3 +661,261 @@ suites: - test/end-to-end/specs/not-connected.specs.js - test/end-to-end/specs/console-not-registered.specs.js coverage: *disable-coverage + end-to-end-plugin: + - name: plugin-connection + describe: Plugin should connect to controller when started first time if connection options are provided + before: *e2e-clean + services: &plugin-e2e-services + - *e2e-mongodb-service + - name: controller + docker: + <<: *e2e-controller-service-docker + env: + <<: *e2e-controller-service-docker-env + fixture: plugin + local: + <<: *e2e-controller-service-local + env: + <<: *e2e-controller-service-local-env + fixture: plugin + - name: service + docker: + <<: *e2e-service-service-docker + env: + <<: *e2e-service-service-docker-env + fixture: plugin + service_name: example-plugin + local: + <<: *e2e-service-service-local + env: + <<: *e2e-service-service-local-env + fixture: plugin + service_name: example-plugin + test: &plugin-e2e-test + local: + <<: *e2e-test-local + env: + <<: *e2e-test-local-env + fixture: plugin + service_name: example-plugin + docker: + <<: *e2e-test-docker + env: + <<: *e2e-test-docker-env + fixture: plugin + service_name: example-plugin + specs: + - test/end-to-end/specs/service-connection.specs.js + - test/end-to-end/plugin-specs/plugin-registered.specs.js + coverage: *disable-coverage + - name: plugin-reconnection + describe: Plugin should connect again to controller when restarted + services: *plugin-e2e-services + test: *plugin-e2e-test + coverage: *disable-coverage + - name: plugin-reconnection-no-config + describe: Plugin should connect again to controller when restarted, even when no config is provided + services: + - *e2e-mongodb-service + - name: controller + docker: + <<: *e2e-controller-service-docker + env: + <<: *e2e-controller-service-docker-env + fixture: plugin + local: + <<: *e2e-controller-service-local + env: + <<: *e2e-controller-service-local-env + fixture: plugin + - name: service + docker: + <<: *e2e-service-service-docker + command: test/end-to-end/commands/start-service-no-config.sh + env: + <<: *e2e-service-service-docker-env + fixture: plugin + service_name: example-plugin + local: + <<: *e2e-service-service-local + command: test/end-to-end/commands/start-service-no-config.sh + env: + <<: *e2e-service-service-local-env + fixture: plugin + service_name: example-plugin + test: *plugin-e2e-test + coverage: *disable-coverage + - name: plugin-name-repeated + describe: Plugin should not connect to controller if service name is already defined + before: + local: + command: test/end-to-end/commands/clean-service-storage.sh + env: + service_name: example-plugin + domapic_path: .test + services: + - *e2e-mongodb-service + - name: controller + docker: + <<: *e2e-controller-service-docker + env: + <<: *e2e-controller-service-docker-env + fixture: plugin + local: + <<: *e2e-controller-service-local + env: + <<: *e2e-controller-service-local-env + fixture: plugin + - name: service + docker: + <<: *e2e-service-service-docker + env: + <<: *e2e-service-service-docker-env + fixture: plugin + service_name: example-plugin + domapic_path: .shared/.domapic + local: + <<: *e2e-service-service-local + env: + <<: *e2e-service-service-local-env + fixture: plugin + service_name: example-plugin + test: + <<: *plugin-e2e-test + specs: + - test/end-to-end/specs/service-repeated.specs.js + - test/end-to-end/specs/not-connected.specs.js + coverage: *disable-coverage + - name: plugin-connection-api + describe: Plugin should connect to controller when using connection api + before: *e2e-clean + services: &plugin-e2e-services-no-controller-config + - *e2e-mongodb-service + - name: controller + docker: + <<: *e2e-controller-service-docker + env: + <<: *e2e-controller-service-docker-env + fixture: plugin + local: + <<: *e2e-controller-service-local + env: + <<: *e2e-controller-service-local-env + fixture: plugin + - name: service + docker: + <<: *e2e-service-service-docker + command: test/end-to-end/commands/start-service-no-controller-config.sh + env: + <<: *e2e-service-service-docker-env + fixture: plugin + service_name: example-plugin + local: + <<: *e2e-service-service-local + command: test/end-to-end/commands/start-service-no-controller-config.sh + env: + <<: *e2e-service-service-local-env + fixture: plugin + service_name: example-plugin + test: + <<: *e2e-test + specs: + - test/end-to-end/specs/not-connected.specs.js + - test/end-to-end/plugin-specs/plugin-not-registered.specs.js + - test/end-to-end/specs/connection-api.specs.js + - test/end-to-end/specs/service-connection.specs.js + - test/end-to-end/plugin-specs/plugin-registered.specs.js + coverage: *disable-coverage + - name: plugin-connection-api-wrong-controller-api-key + describe: Plugin should not connect to controller when using connection api with wrong controller apiKey + before: *e2e-clean + services: *plugin-e2e-services-no-controller-config + test: + <<: *plugin-e2e-test + specs: + - test/end-to-end/specs/connection-api-wrong-key.specs.js + - test/end-to-end/specs/not-connected.specs.js + - test/end-to-end/plugin-specs/plugin-not-registered.specs.js + coverage: *disable-coverage + - name: plugin-events + describe: Plugin should receive controller events, and controller interface should work as expected. + before: *e2e-clean + services: + - *e2e-mongodb-service + - name: controller + docker: + <<: *e2e-controller-service-docker + env: + <<: *e2e-controller-service-docker-env + fixture: plugin-events + local: + <<: *e2e-controller-service-local + env: + <<: *e2e-controller-service-local-env + fixture: plugin-events + - name: service + docker: + <<: *e2e-service-service-docker + env: + <<: *e2e-service-service-docker-env + fixture: plugin-events + service_name: example-plugin + local: + <<: *e2e-service-service-local + env: + <<: *e2e-service-service-local-env + fixture: plugin-events + service_name: example-plugin + - name: module + docker: + container: module-container + command: test/end-to-end/commands/start-service.sh + wait-on: + timeout: 120000 + resources: + - tcp:service-container:3000 + env: + controller_host_name: controller-container + service_host_name: module-container + service_port: 3200 + service_name: console-module + fixture: console + domapic_path: .shared + local: + command: test/end-to-end/commands/start-service.sh + wait-on: tcp:localhost:3100 + env: + controller_host_name: localhost + service_host_name: localhost + service_port: 3200 + service_name: console-module + fixture: console + domapic_path: .test + test: &plugin-e2e-test + local: + <<: *e2e-test-local + wait-on: + timeout: 120000 + resources: + - tcp:localhost:3200 + env: + <<: *e2e-test-local-env + fixture: plugin-events + service_name: example-plugin + domapic_path: .test + docker: + <<: *e2e-test-docker + wait-on: + timeout: 120000 + resources: + - tcp:module-container:3200 + env: + <<: *e2e-test-docker-env + fixture: plugin-events + service_name: example-plugin + domapic_path: .shared + specs: + - test/end-to-end/specs/service-connection.specs.js + - test/end-to-end/plugin-specs/plugin-registered.specs.js + - test/end-to-end/plugin-specs/plugin-events.specs.js + coverage: *disable-coverage diff --git a/CHANGELOG.md b/CHANGELOG.md index e40de7f..9f00623 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,17 @@ and this project adheres to [Semantic Versioning](http://semver.org/). ### Fixed ### Removed +## [1.0.0-alpha.2] - 2018-11-20 +### BREAKING CHANGES +- Changed Controller uris to adapt them to Controller version 1.0.0-alpha.9 +- Expose event emitter in events object, instead of module object directly + +### Added +- Add plugin Constructor +- Add events api for plugins +- Add controller api interface for plugins +- Expose extendOpenApi and addOperations methods to module and plugin + ## [1.0.0-alpha.1] - 2018-11-4 ### Added - First fully functional pre-release diff --git a/README.md b/README.md index b02c5bb..6c561a0 100644 --- a/README.md +++ b/README.md @@ -2,11 +2,11 @@ # Domapic Service -> Node.js base for Domapic Modules +> Node.js base for creating [Domapic][website-url] Modules and Plugins. [![Build status][travisci-image]][travisci-url] [![js-standard-style][standard-image]][standard-url] -[![NPM dependencies][npm-dependencies-image]][npm-dependencies-url] [![Last commit][last-commit-image]][last-commit-url] [![Last release][release-image]][release-url] +[![NPM dependencies][npm-dependencies-image]][npm-dependencies-url] [![Last commit][last-commit-image]][last-commit-url] [![NPM downloads][npm-downloads-image]][npm-downloads-url] [![Website][website-image]][website-url] [![License][license-image]][license-url] @@ -14,25 +14,28 @@ ## Table of Contents -* [Introduction](#introduction) -* [Creating a module](#creating-your-module) - * [Abilities](#registering-abilities) - * [Action](#action) - * [State](#state) - * [Event](#event) +* [Modules](#modules) + * [Creating a module](#creating-a-module) + * [Abilities](#registering-abilities) + * [Action](#action) + * [State](#state) + * [Event](#event) +* [Plugins](#plugins) + * [Creating a plugin](#creating-a-plugin) + * [Controller events listeners](#controller-events-listeners) + * [Controller interface](#controller-interface) * [Start the server](#start-the-server) * [Process and logs management](#process-and-logs-management) * [Connecting with Domapic Controller](#connecting-with-domapic-controller) * [Options](#options) * [Custom options](#custom-options) * [Security](#security) +* [Extending API](#extending-api) * [Logs](#logs) --- -## Introduction - -Node.js base for creating [Domapic][website-url] Modules. +## Modules A Domapic Module is an action-event-state based REST API Micro-service. Can be, as example, a little program that controls a relay, has an `action` to switch it, triggers an `event` when changes, and an `state` that can be consulted. Modules can be paired with a [Domapic Controller][domapic-controller-url], and be controlled through it, making them interact automatically ones with anothers, or with plugins, such as third-party integrations, as [_Amazon's Alexa_][alexa-url], [_Apple's HomeKit_][homekit-url], etc. @@ -44,7 +47,7 @@ If you are going to __publish your module, add the `domapic-module` suffix to th > Above, an example of two modules in a [Domapic System][website-url]. Now, the relay can be controlled using the web or mobile applications, or interacting with ["Alexa"][alexa-url] or ["HomeKit"][homekit-url]. Automatisms can be configured in the [Domapic Controller Web UI][domapic-controller-url] to make the [_Phillips Hue_][hue-url] bulb be switched off automatically when the relay bulb is switched on, for example. -## Creating a module +### Creating a module Modules can be created with few lines of code. Here is an example of a module controlling a fake relay: @@ -56,7 +59,7 @@ Modules can be created with few lines of code. Here is an example of a module co "version": "1.0.0", "description": "Domapic module controlling a relay", "dependencies": { - "domapic-service": "1.0.0-alpha.1" + "domapic-service": "1.0.0-alpha.2" } } ``` @@ -87,7 +90,7 @@ domapic.createModule({ packagePath: path.resolve(__dirname) }) description: 'Switch on/off the relay', handler: newStatus => { status = newStatus - module.emit('switch', status) + module.events.emit('switch', status) return Promise.resolve(status) } } @@ -106,15 +109,19 @@ domapic.createModule({ packagePath: path.resolve(__dirname) }) * `options` `` containing: * `packagePath` `` Path to the folder where the module's `package.json` is. * `customConfig` `` Domapic provides some common [configuration options](#options). Custom options for a module can be added defining them in this property. In this way, for example, you can add a "gpio" option, that will be received when the module instance is started, and can be modified through arguments in the start command: `npm start -- --gpio=12`. For further info about defining custom configuration options, please refer to the ["custom options" chapter](#custom-options) -* Returns a module instance, containing: - * `tracer` `` containing methods for tracing with different log levels (`log`, `trace`, `debug`, `info`, `warn`, `error`). Read the ["traces" chapter in the domapic-base documentation][domapic-base-url] for further info. - * `config` `` containing methods for getting and setting configuration. - * `get([key])` - Returns a promise resolved with the module configuration. Resolved with specific property value if argument `key` is provided. - * `set(key [, value])` - Sets `value` for provided `key` into module configuration. Returns a promise. - * `register(abilitiesData)` - Register provided abilities into the module. Read the [abilities](#abilities) chapter for further info. - * `start` - Starts the server. -## Abilities +Returns a module instance, containing: + +* `tracer` `` containing methods for tracing with different log levels (`log`, `trace`, `debug`, `info`, `warn`, `error`). Read the ["traces" chapter in the domapic-base documentation][domapic-base-url] for further info. +* `config` `` containing methods for getting and setting configuration. + * `get([key])` - Returns a promise resolved with the module configuration. Resolved with specific property value if argument `key` is provided. + * `set(key [, value])` - Sets `value` for provided `key` into module configuration. Returns a promise. +* `api` - Object containing methods for [extending the built-in api](#extending-api). +* `register(abilitiesData)` - Register provided abilities into the module. Read the [abilities](#abilities) chapter for further info. +* `start` - Starts the server. +* `events`- [Node.js emitter object][nodejs-events-url]. Used to emit abilities events to the controller. + +### Abilities Each Module can has many abilities, each one with its own action, state and event. Continuing with the example above, the module could have another ability called "toggle", that would change the status based on current status, without needing to receive any data. @@ -174,13 +181,139 @@ In this example, the action's `handler` method returned `Promise.resolve(true)`. #### Event -When [ability](#abilities) has an `event` property defined, the `emit` method of the module can be used to trigger the ability event, passing the correspondant data. This will produce module calling to controller api to inform about the trigered event. +When [ability](#abilities) has an `event` property defined, the `emit` method of the module's `events` object can be used to emit the ability event, passing the correspondant data. This will produce module calling to controller api to inform about the trigered event. ```js -module.emit('switch', true) +module.events.emit('switch', true) ``` In this example, the module will call to the correspondant controller api resource, passing `true` as data. +## Plugins + +A Domapic Plugin is an action-event-state based REST API Micro-service that can "extend" the Domapic Controller functionality. Once it is connected with the Controller, it will receive events each time an entity is created, modified or deleted in the Controller. Plugins will be informed too about all module events or actions received by the Controller. Plugins also have an interface to the Controller, that allows to perform actions such as consult module states, dispatch module actions, create users, etc. + +If you are going to __publish your plugin, add the `domapic-plugin` suffix to the name__, in order to allow npm users finding it easily searching in the website that keyword. + +### Creating a Plugin + +Here is an example of a plugin implementation that receives all Controller events, and gets all modules' states once it is connected: + +```json +{ + "name": "example-domapic-plugin", + "version": "1.0.0", + "description": "Domapic plugin that receives all Controller events and print logs", + "dependencies": { + "domapic-service": "1.0.0-alpha.2" + } +} +``` + +> server.js file: +```js +const path = require('path') +const domapic = require('domapic-service') + +domapic.createPlugin({ packagePath: path.resolve(__dirname) }) + .then(async plugin => { + plugin.events.on('*', eventData => { + plugin.tracer.info(`Received ${eventData.entity}:${eventData.operation} event. Data: ${JSON.stringify(eventData.data)}`) + }) + + plugin.events.on('connection', async () => { + const abilities = await plugin.controller.abilities.get() + abilities.map(async ability => { + if (ability.state) { + const state = await plugin.controller.abilities.state(ability._id) + console.log(`Ability "${ability.name}" of module "${ability._module}" has state "${state.data}"`) + } + }) + }) + + return plugin.start() + }) +``` + +> When started and connected with Controller, the plugin will receive all Controller events an print their data. It will also request all registered abilities and print their current states. + +#### `createPlugin(options)` + +* `options` `` containing: + * `packagePath` `` Path to the folder where the plugin's `package.json` is. + * `customConfig` `` Domapic provides some common [configuration options](#options). Custom options for a plugin can be added defining them in this property. For further info about defining custom configuration options, please refer to the ["custom options" chapter](#custom-options) + +Returns a plugin instance, containing: + +* `tracer` `` containing methods for tracing with different log levels (`log`, `trace`, `debug`, `info`, `warn`, `error`). Read the ["traces" chapter in the domapic-base documentation][domapic-base-url] for further info. +* `config` `` containing methods for getting and setting configuration. + * `get([key])` - Returns a promise resolved with the plugin configuration. Resolved with specific property value if argument `key` is provided. + * `set(key [, value])` - Sets `value` for provided `key` into module configuration. Returns a promise. +* `api` - Object containing methods for [extending the built-in api](#extending-api). +* `start` - Starts the server. +* `events`- [Node.js emitter object][nodejs-events-url]. Used to subscribe to events from the Controller. +* `controller` - Object containing an interface to make requests to Controller. Read the [Controller interface chapter for further info](#controller-interface). + +### Controller events listeners + +Controller emit all module received events and actions, as well as other internal entities events to all registered plugins. +From a Plugin, you can subscribe to an specific entity events, or to an specific operation, etc. + +```js +plugin.events.on('ability:created', eventData => { + console.log('A new ability has been created with data:') + console.log(eventData.data) +}) +``` + +All available events are: + +* ability:updated +* ability:created +* ability:deleted +* ability:action +* ability:event +* service:created +* service:updated +* service:deleted +* user:created +* user:updated +* user:deleted + +Received event data has the next format, where `data` contains all details about the performed operation: + +```json +{ + "entity": "ability", + "operation": "created", + "data": {} +} +``` + +Wildcards are available for subscribing to all events of an specific `entity`, or to all entities of an specific `operation`, or to all events: + +* `*` - All events +* `service:*` - All events of "service" entity. +* `*:created` - All operations "created" of any entity. + +### Controller interface + +A Domapic Plugin provides an interface that allows to perform operations into the Controller. All methods returns a Promise, and are available under the `plugin.controller` object, which contains: + +* `users` - Interface for Controller's "user" entities: + * `me()` - Returns data about plugin user + * `get([id][,filter])` - Returns users data. Because of security reasons, only "operator" users will be returned. Request can be filtered providing an specific user id as \, or an \ containing any other api supported filter (such as `{name:'foo-name'}`). + * `create(userData)` - Creates and user, and returns the new `id`. Only creating users with "operator" role is supported. +* `services`- Interface for Controller's "service" entities: + * `get([id][,filter])` - Returns services data. Request can be filtered providing an specific service id as \, or an \ containing any other api supported filter (such as `{type:'module'}`). +* `abilities`- Interface for Controller's "ability" entities: + * `get([id][,filter])` - Returns abilities data. Request can be filtered providing an specific ability id as \, or an \ containing any other api supported filter (such as `{service:'foo-module-id'}`). + * `state(id)` - Returns state of provided ability. + * `action(id, data)` - Dispatches ability action with provided data. +* `logs` - Interface for Controller's "log" entity: + * `get()` - Returns Controller logs with all modules' actions and events. + +Consult the Controller Swagger interface to get more info about supported filters (queries) and requested data for each api interface. + ## Start the server First of all, remember to start the server programatically after adding the abilities in your code: @@ -242,7 +375,7 @@ You can define __custom CLI commands__ for your module too. For further info rea ## Connecting with Domapic Controller -Connect your module with [Domapic Controller][domapic-controller-url] inside your local network to get the most of it. +Connect your module or plugin with [Domapic Controller][domapic-controller-url] inside your local network to get the most of it. Doing this, you'll can use the Domapic Controller Web Interface to control all your modules, and make them interact through automatisms. Domapic plugins will have access to the module at same time, so you´ll can control it with your voice using the _Amazon's Alexa_ plugin, or the _Homebridge plugin_, for example. @@ -258,11 +391,11 @@ In this way, the module will connect automatically with controller when started. ### Connect using controller web ui -The connection can be executed using the provided api as well, through the controller web ui. When the module is started, an api key for connecting is displayed in logs: +The connection can be executed using the provided api as well, through the controller web ui. When the module or plugin is started, an api key for connecting is displayed in logs: > Try adding connection from Controller, using the next service Api Key: xxxxx-foo-api-key-xxxx -Use the controller web user interface to authorize the connection with your module using the specified api key. +Use the controller web user interface to authorize the connection with your module or plugin using the specified api key. ## Options @@ -273,7 +406,7 @@ Domapic-service provides a set of command line options that are available when s npm start -- --help ``` -* `name` - Service instance name. You can start many instances of the same module defining different names for each one. +* `name` - Service instance name. You can start many instances of the same module or plugin defining different names for each one. * `port` - Http port used by the server. Default is 3000. * `hostName` - Hostname for the server. * `sslCert` - Path to an ssl certificate. The server will start using https protocol if provided. @@ -357,6 +490,16 @@ relay start --initialStatus=true # The module will be started, and "initialStatus" value in config will be true ``` +## Extending API + +The built-in api of Modules and Plugins can be extended to implement your own api resources. For this purpose, the `api` object is provided to module and plugins instances. This object contains methods: + +* `extendOpenApi(openApiDefinition)` +* `addOperations(operationsDefinitions)` + +All custom api methods implemented with these methods will require authentication as well as built-in api methods, as long as you remember to define the `"security": [{"apiKey": []}]` property to each openapi path definition. + +For further info about how to define api resources using these methods, please refer to the ["Adding api resources" chapter of the Domapic-base documentation](https://npmjs.com/domapic-base#adding-api-resources) ## Security @@ -419,3 +562,5 @@ Server logs are saved too into a daily file. These files are rotated automatical [domapic-base-url]: https://npmjs.com/domapic-base [pm2-log-rotate-url]: https://github.com/keymetrics/pm2-logrotate [pm2-url]: http://pm2.keymetrics.io/ +[nodejs-events-url]: https://nodejs.org/api/events.html + diff --git a/index.js b/index.js index 1662f94..8b9d902 100644 --- a/index.js +++ b/index.js @@ -4,19 +4,23 @@ const Promise = require('bluebird') const domapic = require('domapic-base') const options = require('./lib/options') -const ServiceHandler = require('./lib/ServiceHandler') +const serviceHandlers = require('./lib/serviceHandlers') +const { SERVICE_TYPES } = require('./lib/utils') -const createModule = moduleOptions => domapic.Service(options.extendWith(moduleOptions)) - .then(service => { - const serviceHandler = new ServiceHandler(service) - return Promise.all([ - serviceHandler.addConnectionApi(), - serviceHandler.addSecurityApi(), - serviceHandler.addSecurity() - ]).then(() => Promise.resolve(serviceHandler.publicMethods)) - }) +const ServiceCreator = function (Builder, type) { + return serviceOptions => domapic.Service(options.extendWith(serviceOptions, type)) + .then(service => { + const serviceHandler = new Builder(service) + return serviceHandler.init() + .then(() => Promise.resolve(serviceHandler.publicMethods)) + }) +} + +const createModule = new ServiceCreator(serviceHandlers.Module, SERVICE_TYPES.MODULE) +const createPlugin = new ServiceCreator(serviceHandlers.Plugin, SERVICE_TYPES.PLUGIN) module.exports = { createModule, + createPlugin, cli: domapic.cli } diff --git a/lib/Abilities.js b/lib/Abilities.js index 47a6f13..f25cce9 100644 --- a/lib/Abilities.js +++ b/lib/Abilities.js @@ -114,12 +114,12 @@ const Abilities = function (service, connection) { const EventHandler = function (name, dataSchema) { const validateData = new DataValidator(dataSchema, name, 'event') - const normalizedEventName = service.utils.services.normalizeName(name) + const normalizedAbilityName = service.utils.services.normalizeName(name) return function (result) { const data = { data: result } return validateData(data) .then(() => { - return connection.sendEvent(normalizedEventName, data) + return connection.sendAbilityEvent(normalizedAbilityName, data) }) .catch((error) => { return service.tracer.error(templates.compiled.sendEventError({ diff --git a/lib/ApiClient.js b/lib/ApiClient.js new file mode 100644 index 0000000..4747d48 --- /dev/null +++ b/lib/ApiClient.js @@ -0,0 +1,111 @@ +'use strict' + +const _ = require('lodash') +const uris = require('./uris') + +const ApiClient = function (service, url, apiKey) { + const client = new service.client.Connection(url, { + apiKey + }) + + const getCreatedId = response => Promise.resolve(response.headers.location.split('/').pop()) + + const getAuthTokens = filter => client.get(uris.query(uris.authTokens(), filter)) + + const createApiKey = data => client.post(uris.authApiKey(), data) + + const getUsers = filter => { + if (filter) { + return client.get(uris.query(uris.users(), filter)) + } + return client.get(uris.query(uris.users())) + } + + const getUser = id => client.get(uris.user(id)) + + const getOperatorUsers = filter => { + if (_.isString(filter)) { + return getUser(filter) + } + const query = { + ...filter, + role: 'operator' + } + return client.get(uris.query(uris.users(), query)) + } + + const createUser = data => client.post(uris.users(), data) + .then(getCreatedId) + + const createOperatorUser = data => client.post(uris.users(), { ...data, role: 'operator' }) + .then(getCreatedId) + + const getUserMe = () => client.get(uris.usersMe()) + + const getService = id => client.get(uris.service(id)) + + const getServices = filter => { + if (_.isString(filter)) { + return getService(filter) + } + if (filter) { + return client.get(uris.query(uris.services(), filter)) + } + return client.get(uris.services()) + } + + const createService = data => client.post(uris.services(), data) + .then(getCreatedId) + + const updateService = (id, data) => client.patch(uris.service(id), data) + + const getAbility = id => client.get(uris.ability(id)) + + const getAbilities = filter => { + if (_.isString(filter)) { + return getAbility(filter) + } + if (filter) { + return client.get(uris.query(uris.abilities(), filter)) + } + return client.get(uris.abilities()) + } + + const createAbility = data => client.post(uris.abilities(), data) + .then(getCreatedId) + + const updateAbility = (id, data) => client.patch(uris.ability(id), data) + + const deleteAbility = id => client.delete(uris.ability(id)) + + const getAbilityState = id => client.get(uris.abilityState(id)) + + const sendAbilityEvent = (id, data) => client.post(uris.abilityEvent(id), data) + + const sendAbilityAction = (id, data) => client.post(uris.abilityAction(id), data) + + const getLogs = () => client.get(uris.logs()) + + return { + getAuthTokens, + createApiKey, + getUsers, + getOperatorUsers, + createUser, + createOperatorUser, + getUserMe, + getServices, + createService, + updateService, + getAbilities, + createAbility, + updateAbility, + deleteAbility, + getAbilityState, + sendAbilityEvent, + sendAbilityAction, + getLogs + } +} + +module.exports = ApiClient diff --git a/lib/Client.js b/lib/Client.js index 22645d2..569e01f 100644 --- a/lib/Client.js +++ b/lib/Client.js @@ -6,7 +6,8 @@ const deepEqual = require('deep-equal') const templates = require('./templates') const events = require('./events') -const uris = require('./uris') +const ApiClient = require('./ApiClient') +const { SERVICE_TYPES } = require('./utils') const OMIT_IN_ABILITIES_COMPARATION = [ '_id', @@ -14,7 +15,7 @@ const OMIT_IN_ABILITIES_COMPARATION = [ 'actionDescription', 'stateDescription', 'eventDescription', - '_module', + '_service', '_user', 'createdAt', 'updatedAt' @@ -27,16 +28,12 @@ const Client = function (service, controllerData, serviceData, serviceAbilities) connected: false } - let client = new service.client.Connection(controllerData.url, { - apiKey: controllerData.apiKey - }) - - const setConnected = function () { + const setConnected = function (result) { if (!state.connected) { state.connected = true events.emit('connection', true) } - return Promise.resolve() + return Promise.resolve(result) } const setDisconnected = function () { @@ -60,18 +57,44 @@ const Client = function (service, controllerData, serviceData, serviceAbilities) .catch(setDisconnectedAndReject) } + const ErrorHandledApiClient = function (client) { + const wrappedClient = {} + _.each(client, (method, methodName) => { + wrappedClient[methodName] = (pathParams, queryParams) => { + return updateConnectionState(method(pathParams, queryParams)) + } + }) + + return wrappedClient + } + + const ResponseHandledApiClient = function (apiKey) { + const client = new ApiClient(service, controllerData.url, apiKey) + const wrappedClient = {} + _.each(client, (method, methodName) => { + wrappedClient[methodName] = (pathParams, queryParams) => { + return method(pathParams, queryParams) + .then(response => Promise.resolve(response && response.body ? response.body : response)) + } + }) + + return wrappedClient + } + + let apiClient = new ResponseHandledApiClient(controllerData.apiKey) + let errorHandledApiClient = new ErrorHandledApiClient(apiClient) + const addService = function () { return service.tracer.debug(templates.compiled.registeringService()) .then(() => { - return client.post(uris.modules(), { + return apiClient.createService({ processId: serviceData.id, description: serviceData.description, package: serviceData.package, version: serviceData.version, apiKey: serviceData.apiKey, - url: serviceData.url - }).then(response => { - return Promise.resolve(response.headers.location.split('/').pop()) + url: serviceData.url, + type: serviceData.type }) }) } @@ -79,7 +102,7 @@ const Client = function (service, controllerData, serviceData, serviceAbilities) const updateServiceInfo = function (registeredService) { return service.tracer.debug(templates.compiled.updatingServiceInfo()) .then(() => { - return client.patch(uris.module(registeredService._id), { + return apiClient.updateService(registeredService._id, { description: serviceData.description, package: serviceData.package, version: serviceData.version, @@ -94,9 +117,8 @@ const Client = function (service, controllerData, serviceData, serviceAbilities) const checkServiceExists = () => { return service.tracer.debug(templates.compiled.checkingControllerRegisteredService()) - .then(() => client.get(uris.modules())) - .then(response => { - const remoteData = response.body + .then(() => apiClient.getServices()) + .then(remoteData => { // TODO, add a query filter to api ?name=x const registeredService = remoteData.find(remoteService => remoteService.name === serviceData.name) @@ -123,6 +145,9 @@ const Client = function (service, controllerData, serviceData, serviceAbilities) } const checkAbilityCorrespondence = function (remoteAbility) { + if (remoteAbility._service !== controllerData.serviceId) { + return Promise.resolve() + } let correspondence = false _.each(abilities, (ability) => { if (correspondence) { @@ -139,7 +164,7 @@ const Client = function (service, controllerData, serviceData, serviceAbilities) return service.tracer.debug(templates.compiled.updatingAbility({ name: ability.name })).then(() => { - return client.patch(uris.ability(ability._id), { + return apiClient.updateAbility(ability._id, { description: ability.description, actionDescription: ability.actionDescription, stateDescription: ability.stateDescription, @@ -154,7 +179,7 @@ const Client = function (service, controllerData, serviceData, serviceAbilities) return service.tracer.debug(templates.compiled.deletingAbility({ name: remoteAbility.name })).then(() => { - return client.delete(uris.ability(remoteAbility._id)) + return apiClient.deleteAbility(remoteAbility._id) }) } } @@ -165,13 +190,10 @@ const Client = function (service, controllerData, serviceData, serviceAbilities) } return service.tracer.debug(templates.compiled.creatingAbility({ name: ability.name - })).then(() => { - return client.post(uris.abilities(), ability) - .then(response => { - ability._id = response.headers.location.split('/').pop() - return Promise.resolve() - }) - }) + })).then(() => apiClient.createAbility(ability).then(id => { + ability._id = id + return Promise.resolve(id) + })) } const registerNewAbilities = function () { @@ -179,13 +201,16 @@ const Client = function (service, controllerData, serviceData, serviceAbilities) } const registerAbilities = function () { + if (serviceData.type !== SERVICE_TYPES.MODULE) { + return Promise.resolve() + } return service.tracer.debug(templates.compiled.gettingRegisteredAbilities()) - .then(() => client.get(uris.query(uris.abilities(), { - module: controllerData.serviceId - }))) - .then((remoteAbilities) => { + .then(() => apiClient.getAbilities({ + service: controllerData.serviceId + })) + .then(remoteAbilities => { return service.tracer.debug(templates.compiled.checkingRemoteAbilities()) - .then(() => Promise.map(remoteAbilities.body, checkAbilityCorrespondence)) + .then(() => Promise.map(remoteAbilities, checkAbilityCorrespondence)) }).then(() => { return registerNewAbilities() }) @@ -193,20 +218,18 @@ const Client = function (service, controllerData, serviceData, serviceAbilities) const registerUser = function () { return service.tracer.debug(templates.compiled.registeringServiceUser()) - .then(() => client.post(uris.users(), { + .then(() => apiClient.createUser({ name: serviceData.name, - role: 'module' - }).then(response => { + role: serviceData.type + }).then(id => { return Promise.resolve({ - _id: response.headers.location.split('/').pop() + _id: id }) - }) - ) + })) } const getCurrentLoggedUser = function () { - return client.get(uris.usersMe()) - .then(response => Promise.resolve(response.body)) + return apiClient.getUserMe() } const checkUserId = function (loggedUserData) { @@ -217,21 +240,20 @@ const Client = function (service, controllerData, serviceData, serviceAbilities) } const getControllerUser = function () { - return client.get(uris.query(uris.users(), { + return apiClient.getUsers({ name: serviceData.name, - role: 'module' - })) - .then(response => { - if (response.body.length) { - return service.tracer.debug(templates.compiled.serviceUserAlreadyRegistered(response.body[0])) - .then(() => checkUserId(response.body[0])) - } - return registerUser() - }) + role: serviceData.type + }).then(response => { + if (response.length) { + return service.tracer.debug(templates.compiled.serviceUserAlreadyRegistered(response[0])) + .then(() => checkUserId(response[0])) + } + return registerUser() + }) } const getServiceUser = function () { - const role = 'module' + const role = serviceData.type return getCurrentLoggedUser() .then(loggedUser => { if (loggedUser.role === role) { @@ -247,21 +269,20 @@ const Client = function (service, controllerData, serviceData, serviceAbilities) const getApiKey = function (userData) { return service.tracer.debug(templates.compiled.gettingUserApiKey()) - .then(() => client.get(uris.query(uris.authTokens(), { + .then(() => apiClient.getAuthTokens({ type: 'apiKey', user: userData._id - }))) + })) .then(response => { - if (response.body.length) { - return Promise.resolve(response.body[0].token) + if (response.length) { + return Promise.resolve(response[0].token) } return service.tracer.debug(templates.compiled.addingUserApiKey()) - .then(() => client.post(uris.authApiKey(), { + .then(() => apiClient.createApiKey({ user: userData._id + }).then(response => { + return Promise.resolve(response.apiKey) })) - .then(response => { - return Promise.resolve(response.body.apiKey) - }) }) } @@ -270,9 +291,8 @@ const Client = function (service, controllerData, serviceData, serviceAbilities) .then(userData => { return getApiKey(userData) .then(apiKey => { - client = new service.client.Connection(controllerData.url, { - apiKey: apiKey - }) + apiClient = new ResponseHandledApiClient(apiKey) + errorHandledApiClient = new ErrorHandledApiClient(apiClient) return service.tracer.debug(templates.compiled.loggingIntoController({ ...userData, apiKey @@ -303,16 +323,17 @@ const Client = function (service, controllerData, serviceData, serviceAbilities) ) } - const sendEvent = function (name, data) { + const sendAbilityEvent = function (name, data) { const abilityId = abilities.find(ability => ability.name === name)._id - return updateConnectionState( - client.post(uris.abilityEventHandler(abilityId), data) - ) + return errorHandledApiClient.sendAbilityEvent(abilityId, data) } + const getApi = () => errorHandledApiClient + return { - connect: connect, - sendEvent: sendEvent + connect, + sendAbilityEvent, + getApi } } diff --git a/lib/Connection.js b/lib/Connection.js index 9a824c5..1a707f6 100644 --- a/lib/Connection.js +++ b/lib/Connection.js @@ -13,6 +13,7 @@ const connectionApi = require('./api/connection.json') const SERVICE_ID_KEY = 'serviceId' const Connection = function (service, security) { + let _type let _serviceIdPromise let _client let _abilities = [] @@ -82,7 +83,8 @@ const Connection = function (service, security) { version: getVersion(), description: getDescription(), apiKey: security.getApiKeyForController(), - url: getServiceUrl() + url: getServiceUrl(), + type: _type }) } @@ -163,10 +165,10 @@ const Connection = function (service, security) { return Promise.reject(new service.errors.ServerUnavailable(templates.compiled.notConnected())) } - const sendEvent = function (name, data) { + const sendAbilityEvent = function (abilityName, data) { return getClient() .then((client) => { - return client.sendEvent(name, data) + return client.sendAbilityEvent(abilityName, data) }) } @@ -200,11 +202,46 @@ const Connection = function (service, security) { }) } + const setType = function (type) { + _type = type + } + + const ControllerClient = function (method) { + return (pathParams, queryParams) => { + return getClient() + .catch(() => { + return Promise.reject(new service.errors.ServerUnavailable(templates.compiled.controllerClientnotConnected())) + }) + .then(client => client.getApi()[method](pathParams, queryParams)) + } + } + + const controllerClient = { + users: { + me: new ControllerClient('getUserMe'), + get: new ControllerClient('getOperatorUsers'), + create: new ControllerClient('createOperatorUser') + }, + services: { + get: new ControllerClient('getServices') + }, + abilities: { + get: new ControllerClient('getAbilities'), + state: new ControllerClient('getAbilityState'), + action: new ControllerClient('sendAbilityAction') + }, + logs: { + get: new ControllerClient('getLogs') + } + } + return { - addApi: addApi, - connect: connect, - sendEvent: sendEvent, - addAbility: addAbility + addApi, + connect, + setType, + sendAbilityEvent, + addAbility, + controllerClient } } diff --git a/lib/ControllerEvents.js b/lib/ControllerEvents.js new file mode 100644 index 0000000..eb13630 --- /dev/null +++ b/lib/ControllerEvents.js @@ -0,0 +1,32 @@ +'use strict' + +const events = require('./events') +const eventsApi = require('./api/events.json') + +const ControllerEvents = function (service, connection) { + const eventsHandler = function (params, body, res, user) { + events.emit(`${body.entity}:${body.operation}`, body) + events.emit('*', body) + events.emit(`${body.entity}:*`, body) + events.emit(`*:${body.operation}`, body) + res.status(201) + return Promise.resolve() + } + + const addApi = function () { + return service.server.extendOpenApi(eventsApi) + .then(() => { + return service.server.addOperations({ + events: { + handler: eventsHandler + } + }) + }) + } + + return { + addApi + } +} + +module.exports = ControllerEvents diff --git a/lib/ServiceHandler.js b/lib/ServiceHandler.js deleted file mode 100644 index abfba2b..0000000 --- a/lib/ServiceHandler.js +++ /dev/null @@ -1,86 +0,0 @@ -'use strict' - -const _ = require('lodash') -const Promise = require('bluebird') - -const Abilities = require('./Abilities') -const Connection = require('./Connection') -const Security = require('./Security') -const events = require('./events') -const templates = require('./templates') - -const ServiceHandler = function (service) { - const security = new Security(service) - const connection = new Connection(service, security) - const abilities = new Abilities(service, connection) - let started = false - - const getControllerStorageData = function () { - return service.storage.get() - .then(storage => { - if (storage.controllerData) { - return Promise.resolve({ - controller: storage.controllerData.url, - userId: storage.controllerData.userId, - controllerApiKey: storage.controllerData.apiKey - }) - } - return Promise.reject(service.errors.NotFound(templates.compiled.noControllerFoundInStorage)) - }) - } - - const getControllerConfigData = function () { - return service.config.get() - .then((config) => { - return Promise.resolve({ - controller: config.controller, - controllerApiKey: config.controllerApiKey - }) - }) - } - - const connect = function () { - return getControllerStorageData() - .then(storageControllerData => connection.connect(storageControllerData) - .catch(() => getControllerConfigData() - .then(controllerData => connection.connect({ - ...controllerData, - userId: storageControllerData.userId - })) - ) - ) - .catch(() => getControllerConfigData() - .then(controllerData => connection.connect(controllerData)) - ) - .catch(() => Promise.resolve()) - } - - const start = function () { - started = true - return service.server.start() - .then(connect) - } - - const register = function (abilitiesDefinitions) { - if (started === false) { - return abilities.register(abilitiesDefinitions) - } - return Promise.reject(service.errors.BadImplementation(templates.compiled.registerAbilitiesServerStarted())) - } - - const publicMethods = _.extend({ - tracer: service.tracer, - config: service.config, - start: start, - register: register - }, events) - - return { - addConnectionApi: connection.addApi, - addSecurityApi: security.addApi, - addSecurity: security.addApiKeyAuth, - publicMethods: publicMethods - } -} - -module.exports = ServiceHandler diff --git a/lib/api/events.json b/lib/api/events.json new file mode 100644 index 0000000..0177a68 --- /dev/null +++ b/lib/api/events.json @@ -0,0 +1,72 @@ +{ + "tags": [{ + "name": "events", + "description": "Events from Domapic Controller" + }], + "components": { + "schemas": { + "Event": { + "description": "Domapic Controller Event", + "type": "object", + "properties": { + "entity": { + "description": "Controller entity to which event is related", + "type": "string", + "enum": ["ability", "service", "user"] + }, + "operation": { + "description": "Operation applied to entity", + "enum": ["created", "updated", "deleted", "action", "event"] + }, + "data": { + "description": "Data containing details about entity operation", + "type": "object" + } + }, + "required": ["entity", "operation", "data"], + "additionalProperties": false, + "example": { + "entity": "user", + "operation": "created", + "data": { + "_id": "1223123", + "name": "Foo user", + "email": "foo-email@foo-domain.com", + "role": "admin", + "createdAt": "2018-07-28T17:13:08.718Z", + "updatedAt": "2018-07-28T17:13:09.730Z" + } + } + } + } + }, + "paths": { + "/events": { + "post": { + "tags": ["events"], + "summary": "Receives Controller events", + "description": "Receives all events emitted by Controller", + "operationId": "events", + "requestBody": { + "description": "Event data", + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Event" + } + } + } + }, + "responses": { + "201": { + "description": "Event success" + } + }, + "security": [{ + "apiKey": [] + }] + } + } + } +} diff --git a/lib/options.js b/lib/options.js index 908f5d0..d9742dd 100644 --- a/lib/options.js +++ b/lib/options.js @@ -16,7 +16,7 @@ const customConfig = { } } -const extendWith = function (options = {}) { +const extendWith = function (options = {}, type) { _.each(options.customConfig, (config, key) => { if (customConfig[key]) { throw new Error(templates.core.compiled.cli.overwriteOptionError({ @@ -29,7 +29,7 @@ const extendWith = function (options = {}) { return { ...options, - type: 'module' + type } } diff --git a/lib/serviceHandlers.js b/lib/serviceHandlers.js new file mode 100644 index 0000000..3a18b5d --- /dev/null +++ b/lib/serviceHandlers.js @@ -0,0 +1,150 @@ +'use strict' + +const Promise = require('bluebird') + +const Abilities = require('./Abilities') +const Connection = require('./Connection') +const ControllerEvents = require('./ControllerEvents') +const Security = require('./Security') +const events = require('./events') +const templates = require('./templates') +const { SERVICE_TYPES } = require('./utils') + +class ServiceHandler { + constructor (service) { + this.service = service + this.security = new Security(this.service) + this.connection = new Connection(this.service, this.security) + this.started = false + + this.addConnectionApi = this.connection.addApi + this.addSecurityApi = this.security.addApi + this.addSecurity = this.security.addApiKeyAuth + + this.start = this.start.bind(this) + this.connect = this.connect.bind(this) + } + + getControllerStorageData () { + return this.service.storage.get() + .then(storage => { + if (storage.controllerData) { + return Promise.resolve({ + controller: storage.controllerData.url, + userId: storage.controllerData.userId, + controllerApiKey: storage.controllerData.apiKey + }) + } + return Promise.reject(this.service.errors.NotFound(templates.compiled.noControllerFoundInStorage)) + }) + } + + getControllerConfigData () { + return this.service.config.get() + .then(config => { + return Promise.resolve({ + controller: config.controller, + controllerApiKey: config.controllerApiKey + }) + }) + } + + connect () { + return this.getControllerStorageData() + .then(storageControllerData => this.connection.connect(storageControllerData) + .catch(() => this.getControllerConfigData() + .then(controllerData => this.connection.connect({ + ...controllerData, + userId: storageControllerData.userId + })) + ) + ) + .catch(() => this.getControllerConfigData() + .then(controllerData => this.connection.connect(controllerData)) + ) + .catch(() => Promise.resolve()) + } + + start () { + this.started = true + return this.service.server.start() + .then(this.connect) + } + + get baseMethods () { + return { + tracer: this.service.tracer, + config: this.service.config, + api: { + extendOpenApi: this.service.server.extendOpenApi, + addOperations: this.service.server.addOperations + }, + start: this.start, + events + } + } +} + +class Module extends ServiceHandler { + constructor (service) { + super(service) + this.connection.setType(SERVICE_TYPES.MODULE) + this.abilities = new Abilities(this.service, this.connection) + this.register = this.register.bind(this) + this.init = this.init.bind(this) + } + + register (abilitiesDefinitions) { + if (this.started === false) { + return this.abilities.register(abilitiesDefinitions) + } + return Promise.reject(this.service.errors.BadImplementation(templates.compiled.registerAbilitiesServerStarted())) + } + + init () { + return Promise.all([ + this.addConnectionApi(), + this.addSecurityApi(), + this.addSecurity() + ]) + } + + get publicMethods () { + return { + ...this.baseMethods, + register: this.register + } + } +} + +class Plugin extends ServiceHandler { + constructor (service) { + super(service) + this.connection.setType(SERVICE_TYPES.PLUGIN) + this.controllerEvents = new ControllerEvents(this.service, this.connection) + this.init = this.init.bind(this) + + this.controller = this.connection.controllerClient + } + + init () { + return Promise.all([ + this.addConnectionApi(), + this.controllerEvents.addApi(), + this.addSecurityApi(), + this.addSecurity() + ]) + } + + get publicMethods () { + return { + ...this.baseMethods, + controller: this.controller + } + } +} + +module.exports = { + Module, + Plugin +} diff --git a/lib/templates.js b/lib/templates.js index d86c86f..ed74f9c 100644 --- a/lib/templates.js +++ b/lib/templates.js @@ -32,6 +32,7 @@ const templates = { noControllerApiKeyConfigured: 'No controller api key was defined. Use the --controllerApiKey option to define it', errorConnecting: 'Error connecting to Controller:', notConnected: 'The service is not connected to Controller', + controllerClientnotConnected: 'Controller api interface not available. The service is not connected', connected: 'Connection success with Domapic Controller at {{url}}', gettingRegisteredAbilities: 'Getting registered abilities from controller', diff --git a/lib/uris.js b/lib/uris.js index 89cf452..3f1f533 100644 --- a/lib/uris.js +++ b/lib/uris.js @@ -12,8 +12,9 @@ const EVENT_HANDLER_PATH = 'event' // Controller const AUTH = 'auth' -const MODULES = 'modules' +const SERVICES = 'services' const USERS = 'users' +const LOGS = 'logs' const normalizeName = name => kebabCase(name) @@ -29,18 +30,23 @@ const abilityActionHandler = name => resolveUri(ABILITIES, normalizeName(name), const authTokens = () => resolveUri(AUTH, 'tokens') const authApiKey = () => resolveUri(AUTH, 'apiKey') -const modules = () => MODULES -const moduleUri = id => resolveUri(MODULES, id) +const services = () => SERVICES +const serviceUri = id => resolveUri(SERVICES, id) const abilities = () => ABILITIES const ability = id => resolveUri(ABILITIES, id) -const abilityEventHandler = id => resolveUri(ABILITIES, id, EVENT_HANDLER_PATH) +const abilityEvent = id => resolveUri(ABILITIES, id, EVENT_HANDLER_PATH) +const abilityState = id => resolveUri(ABILITIES, id, STATE_HANDLER_PATH) +const abilityAction = id => resolveUri(ABILITIES, id, ACTION_HANDLER_PATH) const users = () => USERS const usersMe = () => resolveUri(USERS, 'me') +const user = id => resolveUri(USERS, id) + +const logs = () => LOGS // Helpers -const query = (baseUri, queryParams) => resolveUri(baseUri, `?${querystring.stringify(omitBy(queryParams, isUndefined))}`) +const query = (baseUri, queryParams) => `${baseUri}?${querystring.stringify(omitBy(queryParams, isUndefined))}` module.exports = { // modules @@ -49,13 +55,17 @@ module.exports = { // Controller authTokens, authApiKey, - modules, - module: moduleUri, + services, + service: serviceUri, abilities, ability, - abilityEventHandler, + abilityEvent, + abilityState, + abilityAction, users, + user, usersMe, + logs, // Helpers query } diff --git a/lib/utils.js b/lib/utils.js new file mode 100644 index 0000000..daa49a0 --- /dev/null +++ b/lib/utils.js @@ -0,0 +1,8 @@ +const SERVICE_TYPES = { + MODULE: 'module', + PLUGIN: 'plugin' +} + +module.exports = { + SERVICE_TYPES +} diff --git a/package-lock.json b/package-lock.json index 88c0ba6..cd20e00 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "domapic-service", - "version": "1.0.0-alpha.1", + "version": "1.0.0-alpha.2", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 38261ed..50881ea 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "domapic-service", - "version": "1.0.0-alpha.1", + "version": "1.0.0-alpha.2", "description": "Base for Domapic Node.js services", "main": "index.js", "keywords": [ diff --git a/sonar-project.properties b/sonar-project.properties index 038ed1d..f504dae 100644 --- a/sonar-project.properties +++ b/sonar-project.properties @@ -1,6 +1,6 @@ sonar.organization=domapic sonar.projectKey=domapic-service -sonar.projectVersion=1.0.0-alpha.1 +sonar.projectVersion=1.0.0-alpha.2 sonar.sources=. sonar.exclusions=node_modules/**,test/end-to-end/fixtures/** diff --git a/test/end-to-end/commands/local-clean.sh b/test/end-to-end/commands/local-clean.sh index eb2e650..6d96e66 100755 --- a/test/end-to-end/commands/local-clean.sh +++ b/test/end-to-end/commands/local-clean.sh @@ -3,6 +3,7 @@ pm2 flush pm2 delete controller pm2 delete relay-domapic-module +pm2 delete foo-service ./test/end-to-end/commands/clean-db.sh diff --git a/test/end-to-end/commands/start-controller.sh b/test/end-to-end/commands/start-controller.sh index ef0fda1..e5366a8 100755 --- a/test/end-to-end/commands/start-controller.sh +++ b/test/end-to-end/commands/start-controller.sh @@ -1,5 +1,5 @@ #!/usr/bin/env bash -cd test/end-to-end/fixtures/${fixture} +cd test/end-to-end/fixtures/controller rm -rf ../../../../${domapic_path}/.domapic/controller/logs npm i npm run domapic-controller start controller -- --hostName=${controller_host_name} --path=../../../../${domapic_path} --db=${db_uri} --save --authDisabled diff --git a/test/end-to-end/fixtures/console-ability-changed/package.json b/test/end-to-end/fixtures/console-ability-changed/package.json index d9b3ee0..3003067 100644 --- a/test/end-to-end/fixtures/console-ability-changed/package.json +++ b/test/end-to-end/fixtures/console-ability-changed/package.json @@ -4,11 +4,5 @@ "description": "Example of Node.js Domapic module with another ability", "dependencies": { "domapic-service": "file:../../../../" - }, - "scripts": { - "domapic-controller": "node_modules/.bin/domapic-controller" - }, - "devDependencies": { - "domapic-controller": "1.0.0-alpha.7" } } diff --git a/test/end-to-end/fixtures/console-changed/package.json b/test/end-to-end/fixtures/console-changed/package.json index 7b7a62a..3faf7cc 100644 --- a/test/end-to-end/fixtures/console-changed/package.json +++ b/test/end-to-end/fixtures/console-changed/package.json @@ -4,11 +4,5 @@ "description": "Example of Node.js Domapic module with changed properties", "dependencies": { "domapic-service": "file:../../../../" - }, - "scripts": { - "domapic-controller": "node_modules/.bin/domapic-controller" - }, - "devDependencies": { - "domapic-controller": "1.0.0-alpha.7" } } diff --git a/test/end-to-end/fixtures/console-changed/server.js b/test/end-to-end/fixtures/console-changed/server.js index 0bebebd..9dd83c3 100644 --- a/test/end-to-end/fixtures/console-changed/server.js +++ b/test/end-to-end/fixtures/console-changed/server.js @@ -12,7 +12,7 @@ domapic.createModule({ const consoleLog = function (data) { lastCharacter = data console.log(`Printing into console: ${data}`) - service.emit('console', data) + service.events.emit('console', data) } return service.register({ diff --git a/test/end-to-end/fixtures/console/package.json b/test/end-to-end/fixtures/console/package.json index c1ddee3..7af2608 100644 --- a/test/end-to-end/fixtures/console/package.json +++ b/test/end-to-end/fixtures/console/package.json @@ -4,11 +4,5 @@ "description": "Example of Node.js Domapic module that handles console", "dependencies": { "domapic-service": "file:../../../../" - }, - "scripts": { - "domapic-controller": "node_modules/.bin/domapic-controller" - }, - "devDependencies": { - "domapic-controller": "1.0.0-alpha.7" } } diff --git a/test/end-to-end/fixtures/console/server.js b/test/end-to-end/fixtures/console/server.js index 6f1d168..a402031 100644 --- a/test/end-to-end/fixtures/console/server.js +++ b/test/end-to-end/fixtures/console/server.js @@ -12,7 +12,7 @@ domapic.createModule({ const consoleLog = function (data) { lastCharacter = data console.log(`Printing into console: ${data}`) - service.emit('console', data) + service.events.emit('console', data) } return service.register({ diff --git a/test/end-to-end/fixtures/controller/package.json b/test/end-to-end/fixtures/controller/package.json new file mode 100644 index 0000000..62bf6b2 --- /dev/null +++ b/test/end-to-end/fixtures/controller/package.json @@ -0,0 +1,11 @@ +{ + "name": "example-domapic-controller", + "version": "1.0.0", + "description": "Domapic controller for testing purposes", + "scripts": { + "domapic-controller": "node_modules/.bin/domapic-controller" + }, + "dependencies": { + "domapic-controller": "1.0.0-alpha.9" + } +} diff --git a/test/end-to-end/fixtures/plugin-events/openapi.json b/test/end-to-end/fixtures/plugin-events/openapi.json new file mode 100644 index 0000000..64b748b --- /dev/null +++ b/test/end-to-end/fixtures/plugin-events/openapi.json @@ -0,0 +1,73 @@ +{ + "tags": [{ + "name": "controller", + "description": "Controller interface" + }], + "components": { + "schemas": { + "Command": { + "description": "Command to be sent to controller", + "type": "object", + "properties": { + "entity": { + "description": "Controller entity", + "type": "string", + "enum": ["users", "services", "abilities", "logs"] + }, + "operation": { + "description": "Entity operation", + "type": "string", + "enum": ["me", "get", "create", "state", "action"] + }, + "id": { + "description": "Entity id", + "type": "string" + }, + "data": { + "description": "Operation data", + "type": "object" + }, + "filter": { + "description": "Operation filter", + "type": "object" + } + }, + "required": ["entity", "operation"], + "additionalProperties": false, + "example": { + "entity": "users", + "operation": "me" + } + } + } + }, + "paths": { + "/controller": { + "post": { + "tags": ["controller"], + "summary": "Controller command", + "description": "Sends a command to controller, using plugin command interface", + "operationId": "controller", + "requestBody": { + "description": "Command data", + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Command" + } + } + } + }, + "responses": { + "200": { + "description": "Controller command success" + } + }, + "security": [{ + "apiKey": [] + }] + } + } + } +} diff --git a/test/end-to-end/fixtures/plugin-events/package.json b/test/end-to-end/fixtures/plugin-events/package.json new file mode 100644 index 0000000..b3b4e59 --- /dev/null +++ b/test/end-to-end/fixtures/plugin-events/package.json @@ -0,0 +1,9 @@ +{ + "name": "example-domapic-plugin", + "version": "1.0.0", + "description": "Domapic plugin for testing purposes", + "dependencies": { + "domapic-service": "file:../../../../", + "lodash": "4.17.11" + } +} diff --git a/test/end-to-end/fixtures/plugin-events/server.js b/test/end-to-end/fixtures/plugin-events/server.js new file mode 100644 index 0000000..ef0ba67 --- /dev/null +++ b/test/end-to-end/fixtures/plugin-events/server.js @@ -0,0 +1,53 @@ +'use strict' + +const _ = require('lodash') +const path = require('path') +const domapic = require('../../../../index') + +const openapi = require('./openapi.json') + +domapic.createPlugin({ + packagePath: path.resolve(__dirname) +}).then(async plugin => { + plugin.events.on(`*`, data => { + plugin.tracer.info(`Received ${data.entity}:${data.operation} event. Data: ${JSON.stringify(data.data)}`) + }) + + plugin.api.extendOpenApi(openapi) + + plugin.api.addOperations({ + controller: { + handler: (params, body, res) => { + let promise + if (body.filter) { + promise = plugin.controller[body.entity][body.operation](body.filter) + } else if (body.id) { + promise = plugin.controller[body.entity][body.operation](body.id, body.data) + } else { + promise = plugin.controller[body.entity][body.operation](body.data) + } + + return promise.then(result => { + if (_.isString(result)) { + return Promise.resolve({ + _id: result + }) + } + return Promise.resolve(result || {}) + }) + } + } + }) + + plugin.events.on('connection', async () => { + const abilities = await plugin.controller.abilities.get() + abilities.map(async ability => { + if (ability.state) { + const state = await plugin.controller.abilities.state(ability._id) + console.log(`Ability "${ability.name}" of module "${ability._module}" has state "${state.data}"`) + } + }) + }) + + return plugin.start() +}) diff --git a/test/end-to-end/fixtures/plugin/options.js b/test/end-to-end/fixtures/plugin/options.js new file mode 100644 index 0000000..9a89a2d --- /dev/null +++ b/test/end-to-end/fixtures/plugin/options.js @@ -0,0 +1,7 @@ +module.exports = { + exampleOption: { + type: 'boolean', + describe: 'Plugin option example', + default: false + } +} diff --git a/test/end-to-end/fixtures/plugin/package.json b/test/end-to-end/fixtures/plugin/package.json new file mode 100644 index 0000000..b379b40 --- /dev/null +++ b/test/end-to-end/fixtures/plugin/package.json @@ -0,0 +1,8 @@ +{ + "name": "example-domapic-plugin", + "version": "1.0.0", + "description": "Domapic plugin for testing purposes", + "dependencies": { + "domapic-service": "file:../../../../" + } +} diff --git a/test/end-to-end/fixtures/plugin/server.js b/test/end-to-end/fixtures/plugin/server.js new file mode 100644 index 0000000..e7b944b --- /dev/null +++ b/test/end-to-end/fixtures/plugin/server.js @@ -0,0 +1,15 @@ +'use strict' + +const path = require('path') +const domapic = require('../../../../index') + +const options = require('./options') + +domapic.createPlugin({ + packagePath: path.resolve(__dirname), + customConfig: options +}).then(async plugin => { + let exampleOption = await plugin.config.get('exampleOption') + await plugin.tracer.debug(`example option: ${exampleOption}`) + return plugin.start() +}) diff --git a/test/end-to-end/plugin-specs/plugin-events.specs.js b/test/end-to-end/plugin-specs/plugin-events.specs.js new file mode 100644 index 0000000..9ac4a8b --- /dev/null +++ b/test/end-to-end/plugin-specs/plugin-events.specs.js @@ -0,0 +1,209 @@ + +const test = require('narval') + +const utils = require('../specs/utils') + +test.describe('plugin controller interface and events', function () { + this.timeout(20000) + let serviceConnection + let userId + let consoleServiceId + let consoleAbilityId + + const requestController = (entity, operation, options = {}) => { + const body = { + entity, + operation + } + if (options.filter) { + body.filter = options.filter + } + if (options.id) { + body.id = options.id + } + if (options.data) { + body.data = options.data + } + return serviceConnection.request('/controller', { + method: 'POST', + body + }) + } + + test.before(() => { + return utils.readStorage() + .then(data => { + return Promise.resolve(data.apiKeys.find(apiKeyData => apiKeyData.user === 'controller').key) + }) + .then(apiKey => { + serviceConnection = new utils.ServiceConnection(apiKey) + }) + }) + + test.describe('controller interface for getting current logged user', () => { + test.it('should return data of plugin user', () => { + return requestController('users', 'me').then(response => { + return Promise.all([ + test.expect(response.statusCode).to.equal(200), + test.expect(response.body.name).to.equal('example-plugin'), + test.expect(response.body.role).to.equal('plugin') + ]) + }) + }) + }) + + test.describe('controller interface for creating users', () => { + test.it('should return id of created user', () => { + return requestController('users', 'create', { + data: { + name: 'foo-operator-name', + email: 'foo-operator-email@foo-email.com', + password: 'foo-operator-password' + } + }).then(response => { + userId = response.body._id + return Promise.all([ + test.expect(response.statusCode).to.equal(200), + test.expect(response.body._id).to.not.be.undefined() + ]) + }) + }) + + test.it('should have received an event from controller about created user', () => { + return utils.serviceLogs() + .then(logs => { + return Promise.all([ + test.expect(logs).to.contain(`Received user:created event. Data: {"_id":"${userId}"`) + ]) + }) + }) + }) + + test.describe('controller interface for getting users', () => { + test.it('should return user when passing id', () => { + return requestController('users', 'get', { + id: userId + }).then(response => { + return Promise.all([ + test.expect(response.statusCode).to.equal(200), + test.expect(response.body._id).to.equal(userId) + ]) + }) + }) + + test.it('should return all operator users when no passing filter', () => { + return requestController('users', 'get').then(response => { + const noOperator = response.body.find(user => user.role !== 'operator') + return Promise.all([ + test.expect(response.statusCode).to.equal(200), + test.expect(response.body.length).to.equal(1), + test.expect(noOperator).to.be.undefined() + ]) + }) + }) + }) + + test.describe('controller interface for getting services', () => { + test.it('should return all services', () => { + return requestController('services', 'get').then(response => { + const plugin = response.body.find(user => user.name === 'example-plugin') + const console = response.body.find(user => user.name === 'console-module') + consoleServiceId = console._id + return Promise.all([ + test.expect(response.statusCode).to.equal(200), + test.expect(response.body.length).to.equal(2), + test.expect(plugin).to.not.be.undefined(), + test.expect(console).to.not.be.undefined() + ]) + }) + }) + }) + + test.describe('controller interface for getting abilities', () => { + test.it('should return all abilities', () => { + return requestController('abilities', 'get').then(response => { + const console = response.body.find(ability => ability._service === consoleServiceId) + consoleAbilityId = console._id + return Promise.all([ + test.expect(response.statusCode).to.equal(200), + test.expect(response.body.length).to.equal(1), + test.expect(console).to.not.be.undefined() + ]) + }) + }) + }) + + test.describe('controller interface for getting ability state', () => { + test.it('should return ability state', () => { + return requestController('abilities', 'state', { + id: consoleAbilityId + }).then(response => { + return Promise.all([ + test.expect(response.statusCode).to.equal(200), + test.expect(response.body.data).to.equal('') + ]) + }) + }) + }) + + test.describe('when using controller interface for dispatching ability action', () => { + test.it('should return ability action response', () => { + return requestController('abilities', 'action', { + id: consoleAbilityId, + data: { + data: 'a' + } + }).then(response => { + return Promise.all([ + test.expect(response.statusCode).to.equal(200) + ]) + }) + }) + + test.it('should have received an event from controller about dispatched action', () => { + return utils.serviceLogs(500) + .then(logs => { + return Promise.all([ + test.expect(logs).to.contain(`Received ability:action event. Data: {"_id":"${consoleAbilityId}","data":"a"}`) + ]) + }) + }) + + test.it('should have received an event from controller about triggered event', () => { + return utils.serviceLogs() + .then(logs => { + return Promise.all([ + test.expect(logs).to.contain(`Received ability:event event. Data: {"_id":"${consoleAbilityId}","data":"a"}`) + ]) + }) + }) + + test.it('should have changed ability state', () => { + return requestController('abilities', 'state', { + id: consoleAbilityId + }).then(response => { + return Promise.all([ + test.expect(response.statusCode).to.equal(200), + test.expect(response.body.data).to.equal('a') + ]) + }) + }) + }) + + test.describe('controller interface for getting logs', () => { + test.it('should return logs', () => { + return requestController('logs', 'get').then(response => { + const action = response.body.find(log => log.type === 'action') + const event = response.body.find(log => log.type === 'event') + return Promise.all([ + test.expect(response.statusCode).to.equal(200), + test.expect(response.body.length).to.equal(2), + test.expect(action.data).to.equal('a'), + test.expect(event.data).to.equal('a'), + test.expect(event._ability).to.equal(consoleAbilityId), + test.expect(action._ability).to.equal(consoleAbilityId) + ]) + }) + }) + }) +}) diff --git a/test/end-to-end/plugin-specs/plugin-not-registered.specs.js b/test/end-to-end/plugin-specs/plugin-not-registered.specs.js new file mode 100644 index 0000000..b956314 --- /dev/null +++ b/test/end-to-end/plugin-specs/plugin-not-registered.specs.js @@ -0,0 +1,25 @@ + +const test = require('narval') + +const utils = require('../specs/utils') + +test.describe('when connection with controller is successful', function () { + this.timeout(10000) + + const controllerConnection = new utils.ControllerConnection() + + test.it('plugin user should not be registered in controller', () => { + return controllerConnection.request('/users') + .then(response => { + const user = response.body.find(user => user.name === 'example-plugin') + return test.expect(user).to.be.undefined() + }) + }) + + test.it('plugin have no services registered in controller', () => { + return controllerConnection.request('/services') + .then(response => { + return test.expect(response.body.length).to.equal(0) + }) + }) +}) diff --git a/test/end-to-end/plugin-specs/plugin-registered.specs.js b/test/end-to-end/plugin-specs/plugin-registered.specs.js new file mode 100644 index 0000000..f5c4991 --- /dev/null +++ b/test/end-to-end/plugin-specs/plugin-registered.specs.js @@ -0,0 +1,33 @@ + +const test = require('narval') + +const utils = require('../specs/utils') + +test.describe('when connection with controller is successful', function () { + let serviceUserId + this.timeout(10000) + + const controllerConnection = new utils.ControllerConnection() + + test.it('plugin user should be registered in controller', () => { + return controllerConnection.request('/users') + .then(response => { + const user = response.body.find(user => user.name === 'example-plugin') + serviceUserId = user._id + return test.expect(user.role).to.equal('plugin') + }) + }) + + test.it('plugin should be registered in controller', () => { + return controllerConnection.request('/services') + .then(response => { + const service = response.body.find(service => service.name === 'example-plugin') + return Promise.all([ + test.expect(service._user).to.equal(serviceUserId), + test.expect(service.package).to.equal('example-domapic-plugin'), + test.expect(service.version).to.equal('1.0.0'), + test.expect(service.description).to.equal('Domapic plugin for testing purposes') + ]) + }) + }) +}) diff --git a/test/end-to-end/specs/console-ability-changed.specs.js b/test/end-to-end/specs/console-ability-changed.specs.js index 6330afa..f5bca75 100644 --- a/test/end-to-end/specs/console-ability-changed.specs.js +++ b/test/end-to-end/specs/console-ability-changed.specs.js @@ -20,7 +20,7 @@ test.describe('when connection with controller was successful', function () { }) test.it('console module should be registered in controller', () => { - return controllerConnection.request('/modules') + return controllerConnection.request('/services') .then(response => { const service = response.body.find(service => service.name === 'console') serviceId = service._id @@ -39,7 +39,7 @@ test.describe('when connection with controller was successful', function () { const ability = response.body.find(ability => ability.name === 'stdout') return Promise.all([ test.expect(response.body.length).to.equal(1), - test.expect(ability._module).to.equal(serviceId), + test.expect(ability._service).to.equal(serviceId), test.expect(ability._user).to.equal(serviceUserId), test.expect(ability.event).to.equal(false), test.expect(ability.action).to.equal(true), diff --git a/test/end-to-end/specs/console-changed.specs.js b/test/end-to-end/specs/console-changed.specs.js index bb537d3..622721d 100644 --- a/test/end-to-end/specs/console-changed.specs.js +++ b/test/end-to-end/specs/console-changed.specs.js @@ -20,7 +20,7 @@ test.describe('when connection with controller is successful', function () { }) test.it('console module should be registered in controller', () => { - return controllerConnection.request('/modules') + return controllerConnection.request('/services') .then(response => { const service = response.body.find(service => service.name === 'console') serviceId = service._id @@ -39,7 +39,7 @@ test.describe('when connection with controller is successful', function () { const ability = response.body.find(ability => ability.name === 'console') return Promise.all([ test.expect(response.body.length).to.equal(1), - test.expect(ability._module).to.equal(serviceId), + test.expect(ability._service).to.equal(serviceId), test.expect(ability._user).to.equal(serviceUserId), test.expect(ability.event).to.equal(true), test.expect(ability.action).to.equal(true), diff --git a/test/end-to-end/specs/console-not-registered.specs.js b/test/end-to-end/specs/console-not-registered.specs.js index 4eb879f..1860e19 100644 --- a/test/end-to-end/specs/console-not-registered.specs.js +++ b/test/end-to-end/specs/console-not-registered.specs.js @@ -17,7 +17,7 @@ test.describe('when connection with controller failed', function () { }) test.it('console have no services registered in controller', () => { - return controllerConnection.request('/modules') + return controllerConnection.request('/services') .then(response => { return test.expect(response.body.length).to.equal(0) }) diff --git a/test/end-to-end/specs/console-registered.specs.js b/test/end-to-end/specs/console-registered.specs.js index d422bb0..a25205b 100644 --- a/test/end-to-end/specs/console-registered.specs.js +++ b/test/end-to-end/specs/console-registered.specs.js @@ -20,7 +20,7 @@ test.describe('when connection with controller is successful', function () { }) test.it('console module should be registered in controller', () => { - return controllerConnection.request('/modules') + return controllerConnection.request('/services') .then(response => { const service = response.body.find(service => service.name === 'console') serviceId = service._id @@ -39,7 +39,7 @@ test.describe('when connection with controller is successful', function () { const ability = response.body.find(ability => ability.name === 'console') return Promise.all([ test.expect(response.body.length).to.equal(1), - test.expect(ability._module).to.equal(serviceId), + test.expect(ability._service).to.equal(serviceId), test.expect(ability._user).to.equal(serviceUserId), test.expect(ability.event).to.equal(true), test.expect(ability.action).to.equal(true), diff --git a/test/end-to-end/specs/service-connection.specs.js b/test/end-to-end/specs/service-connection.specs.js index 0cbf545..c837388 100644 --- a/test/end-to-end/specs/service-connection.specs.js +++ b/test/end-to-end/specs/service-connection.specs.js @@ -5,10 +5,10 @@ const testUtils = require('narval/utils') const utils = require('./utils') test.describe('when service starts and connection options are provided', function () { - this.timeout(10000) + this.timeout(20000) test.it('should have connected to the controller', () => { - return utils.waitOnestimatedStartTime(5000) + return utils.waitOnestimatedStartTime(10000) .then(() => { return testUtils.logs.combined('service') .then((log) => { diff --git a/test/end-to-end/specs/service-repeated.specs.js b/test/end-to-end/specs/service-repeated.specs.js index 0f74499..1e5e6cc 100644 --- a/test/end-to-end/specs/service-repeated.specs.js +++ b/test/end-to-end/specs/service-repeated.specs.js @@ -4,7 +4,7 @@ const testUtils = require('narval/utils') const utils = require('./utils') -test.describe('when module name is already defined in controller', function () { +test.describe('when service name is already defined in controller', function () { this.timeout(10000) test.it('should log a connection error', () => { diff --git a/test/end-to-end/specs/utils.js b/test/end-to-end/specs/utils.js index d39624b..aab1ce2 100644 --- a/test/end-to-end/specs/utils.js +++ b/test/end-to-end/specs/utils.js @@ -9,7 +9,7 @@ const requestPromise = require('request-promise') const SERVICE_HOST = process.env.service_host_name const SERVICE_PORT = process.env.service_port const DOMAPIC_PATH = process.env.domapic_path -const SERVICE_NAME = process.env.fixture +const SERVICE_NAME = process.env.service_name const ESTIMATED_START_TIME = 1000 const CONTROLLER_URL = `http://${process.env.controller_host_name}:3000` @@ -47,7 +47,7 @@ const request = function (uri, options = {}) { } const readStorage = function (file = 'storage') { - return readFile(path.resolve(__dirname, '..', '..', '..', DOMAPIC_PATH, '.domapic', SERVICE_NAME, file, 'service.json')) + return readFile(path.resolve(__dirname, '..', '..', '..', DOMAPIC_PATH, '.domapic', SERVICE_NAME || 'foo-service', file, 'service.json')) .then((data) => { return Promise.resolve(JSON.parse(data)) }) @@ -130,8 +130,8 @@ class ControllerConnection { } class ServiceConnection { - constructor () { - this._apiKey = null + constructor (apiKey) { + this._apiKey = apiKey } async getApiKey () { @@ -155,6 +155,11 @@ class ServiceConnection { } } +const serviceLogs = (time = 200) => { + return waitOnestimatedStartTime(time) + .then(() => testUtils.logs.combined('service')) +} + module.exports = { waitOnestimatedStartTime, request, @@ -166,5 +171,6 @@ module.exports = { getControllerApiKey, getServiceApiKey, ControllerConnection, - ServiceConnection + ServiceConnection, + serviceLogs } diff --git a/test/functional/commands/local-clean.sh b/test/functional/commands/local-clean.sh index c8d8884..1e115bc 100755 --- a/test/functional/commands/local-clean.sh +++ b/test/functional/commands/local-clean.sh @@ -3,5 +3,6 @@ pm2 flush pm2 delete controller pm2 delete relay-domapic-module +pm2 delete foo-service ./test/functional/commands/clean.sh diff --git a/test/functional/commands/start-plugin-cli.sh b/test/functional/commands/start-plugin-cli.sh new file mode 100755 index 0000000..9d6fae8 --- /dev/null +++ b/test/functional/commands/start-plugin-cli.sh @@ -0,0 +1,5 @@ +#!/usr/bin/env bash + +cd test/functional/fixtures/${fixture} +npm run plugin start -- --name=foo-service --hostName=${service_host_name} --path=../../../../${domapic_path} --port=${service_port} ${service_extra_options} +npm run plugin logs diff --git a/test/functional/fixtures/console-auth-disabled/server.js b/test/functional/fixtures/console-auth-disabled/server.js index e375a5f..f5c79bd 100644 --- a/test/functional/fixtures/console-auth-disabled/server.js +++ b/test/functional/fixtures/console-auth-disabled/server.js @@ -13,7 +13,7 @@ domapic.createModule({ lastCharacter = data service.tracer.info('Console Called:', data) .then(() => { - service.emit('console', data) + service.events.emit('console', data) }) } diff --git a/test/functional/fixtures/console/server.js b/test/functional/fixtures/console/server.js index 11f9bb0..5be43dd 100644 --- a/test/functional/fixtures/console/server.js +++ b/test/functional/fixtures/console/server.js @@ -13,7 +13,7 @@ domapic.createModule({ lastCharacter = data service.tracer.info('Console Called:', data) .then(() => { - service.emit('console', data) + service.events.emit('console', data) }) } diff --git a/test/functional/fixtures/example/server.js b/test/functional/fixtures/example/server.js index 452bbe3..a51f583 100644 --- a/test/functional/fixtures/example/server.js +++ b/test/functional/fixtures/example/server.js @@ -28,7 +28,7 @@ domapic.createModule({ description: 'Switch on/off the relay', handler: newStatus => { status = newStatus - module.emit('switch', status) + module.events.emit('switch', status) return status } } diff --git a/test/functional/fixtures/multiple-abilities/server.js b/test/functional/fixtures/multiple-abilities/server.js index 7a16f3c..3fd43bd 100644 --- a/test/functional/fixtures/multiple-abilities/server.js +++ b/test/functional/fixtures/multiple-abilities/server.js @@ -13,7 +13,7 @@ domapic.createModule({ lastCharacter = data service.tracer.info('Console Called:', data) .then(() => { - service.emit(ability, data) + service.events.emit(ability, data) }) } diff --git a/test/functional/fixtures/plugin/cli.js b/test/functional/fixtures/plugin/cli.js new file mode 100755 index 0000000..89eac87 --- /dev/null +++ b/test/functional/fixtures/plugin/cli.js @@ -0,0 +1,12 @@ +#!/usr/bin/env node + +const path = require('path') +const domapic = require('../../../../index') + +const options = require('./options') + +domapic.cli({ + packagePath: path.resolve(__dirname), + script: path.resolve(__dirname, 'server.js'), + customConfig: options +}) diff --git a/test/functional/fixtures/plugin/options.js b/test/functional/fixtures/plugin/options.js new file mode 100644 index 0000000..9a89a2d --- /dev/null +++ b/test/functional/fixtures/plugin/options.js @@ -0,0 +1,7 @@ +module.exports = { + exampleOption: { + type: 'boolean', + describe: 'Plugin option example', + default: false + } +} diff --git a/test/functional/fixtures/plugin/package.json b/test/functional/fixtures/plugin/package.json new file mode 100644 index 0000000..57fe0bc --- /dev/null +++ b/test/functional/fixtures/plugin/package.json @@ -0,0 +1,12 @@ +{ + "name": "example-domapic-plugin", + "version": "1.0.0", + "description": "Domapic plugin for testing purposes", + "dependencies": { + "domapic-service": "file:../../../../", + "lodash": "4.17.11" + }, + "scripts": { + "plugin": "./cli.js" + } +} diff --git a/test/functional/fixtures/plugin/server.js b/test/functional/fixtures/plugin/server.js new file mode 100644 index 0000000..6f42152 --- /dev/null +++ b/test/functional/fixtures/plugin/server.js @@ -0,0 +1,62 @@ +'use strict' + +const path = require('path') + +const _ = require('lodash') +const domapic = require('../../../../index') +const options = require('./options') + +const eventsToTest = { + user: { + created: 'A new user has been added', + updated: 'User has been updated', + deleted: 'User has been deleted' + }, + service: { + created: 'A new service has been added', + updated: 'Service has been updated', + deleted: 'Sser has been deleted' + }, + ability: { + created: 'A new ability has been added', + updated: 'Ability has been updated', + deleted: 'Ability has been deleted', + action: 'Ability action has been dispatched', + event: 'Ability event has been triggered' + } +} + +domapic.createPlugin({ + packagePath: path.resolve(__dirname), + customConfig: options +}).then(async plugin => { + let exampleOption = await plugin.config.get('exampleOption') + await plugin.tracer.debug(`example option: ${exampleOption}`) + let operationEvents = [] + + _.each(eventsToTest, (eventData, entity) => { + plugin.events.on(`${entity}:*`, data => { + plugin.tracer.info(`Received ${entity}:* event. Operation ${data.operation}, data: ${data.data.fooData}`) + }) + _.each(eventData, (trace, operation) => { + if (!operationEvents.includes(operation)) { + operationEvents.push(operation) + plugin.events.on(`*:${operation}`, data => { + plugin.tracer.info(`Received *:${operation} event. Entity ${data.entity}, data: ${data.data.fooData}`) + }) + } + plugin.events.on(`${entity}:${operation}`, data => { + plugin.tracer.info(trace, data) + }) + }) + }) + + plugin.events.on('*', data => { + plugin.tracer.info(`Received * event. Entity ${data.entity}, operation ${data.operation}, data: ${data.data.fooData}`) + }) + + plugin.controller.users.me() + .catch(err => plugin.tracer.error(err.message)) + + return plugin.start() +}) diff --git a/test/functional/plugin-specs/about-api.specs.js b/test/functional/plugin-specs/about-api.specs.js new file mode 100644 index 0000000..1e885fd --- /dev/null +++ b/test/functional/plugin-specs/about-api.specs.js @@ -0,0 +1,34 @@ + +const test = require('narval') + +const utils = require('../specs/utils') + +test.describe('about api', function () { + this.timeout(10000) + let connection + + test.before(() => { + return utils.waitOnestimatedStartTime(2000) + .then(() => { + connection = new utils.Connection() + return Promise.resolve() + }) + }) + + test.it('should return plugin information', () => { + return connection.request('/about', { + method: 'GET' + }).then((response) => { + return Promise.all([ + test.expect(response.statusCode).to.equal(200), + test.expect(response.body).to.deep.equal({ + name: 'foo-service', + type: 'plugin', + package: 'example-domapic-plugin', + description: 'Domapic plugin for testing purposes', + version: '1.0.0' + }) + ]) + }) + }) +}) diff --git a/test/functional/plugin-specs/config-api.specs.js b/test/functional/plugin-specs/config-api.specs.js new file mode 100644 index 0000000..fc579d6 --- /dev/null +++ b/test/functional/plugin-specs/config-api.specs.js @@ -0,0 +1,36 @@ +const path = require('path') +const test = require('narval') + +const utils = require('../specs/utils') + +test.describe('config api', function () { + this.timeout(10000) + let connection + + test.before(() => { + return utils.waitOnestimatedStartTime(2000) + .then(() => { + connection = new utils.Connection() + return Promise.resolve() + }) + }) + + test.it('should return plugin configuration', () => { + return connection.request('/config', { + method: 'GET' + }).then((response) => { + return Promise.all([ + test.expect(response.statusCode).to.equal(200), + test.expect(response.body).to.deep.equal({ + color: true, + logLevel: 'info', + port: parseInt(utils.SERVICE_PORT, 10), + exampleOption: false, + authDisabled: [], + hostName: utils.SERVICE_HOST, + path: path.resolve(__dirname, '..', '..', '..', utils.DOMAPIC_PATH) + }) + ]) + }) + }) +}) diff --git a/test/functional/plugin-specs/controller-interface.specs.js b/test/functional/plugin-specs/controller-interface.specs.js new file mode 100644 index 0000000..22ec7ba --- /dev/null +++ b/test/functional/plugin-specs/controller-interface.specs.js @@ -0,0 +1,14 @@ + +const test = require('narval') + +const utils = require('../specs/utils') + +test.describe('interface to controller api', function () { + test.it(`should trace an error when plugin is not connected to controller`, () => { + return utils.serviceLogs().then(logs => { + return Promise.all([ + test.expect(logs).to.include('Controller api interface not available. The service is not connected') + ]) + }) + }) +}) diff --git a/test/functional/plugin-specs/events-api.specs.js b/test/functional/plugin-specs/events-api.specs.js new file mode 100644 index 0000000..f19fb95 --- /dev/null +++ b/test/functional/plugin-specs/events-api.specs.js @@ -0,0 +1,71 @@ + +const test = require('narval') + +const _ = require('lodash') + +const utils = require('../specs/utils') + +const eventsToTest = { + user: { + created: 'A new user has been added', + updated: 'User has been updated', + deleted: 'User has been deleted' + }, + service: { + created: 'A new service has been added', + updated: 'Service has been updated', + deleted: 'Sser has been deleted' + }, + ability: { + created: 'A new ability has been added', + updated: 'Ability has been updated', + deleted: 'Ability has been deleted', + action: 'Ability action has been dispatched', + event: 'Ability event has been triggered' + } +} + +test.describe('events api', function () { + this.timeout(10000) + let connection + + test.before(() => utils.waitOnestimatedStartTime(2000) + .then(() => { + connection = new utils.Connection() + return Promise.resolve() + }) + ) + + _.each(eventsToTest, (eventData, entity) => { + _.each(eventData, (trace, operation) => { + test.it(`should emit "${entity}:${operation}", "*:${operation}" and "${entity}:*" events when entity is "${entity}" and operation is "${operation}"`, () => { + const fooData = `Data: ${entity}:${operation}` + const globalEvent = `Received * event. Entity ${entity}, operation ${operation}, data: ${fooData}` + const entityEvent = `Received ${entity}:* event. Operation ${operation}, data: ${fooData}` + const operationEvent = `Received *:${operation} event. Entity ${entity}, data: ${fooData}` + + return connection.request('/events', { + method: 'POST', + body: { + entity, + operation, + data: { + fooData + } + } + }).then(response => { + return utils.serviceLogs().then(logs => { + return Promise.all([ + test.expect(response.statusCode).to.equal(201), + test.expect(logs).to.include(trace), + test.expect(logs).to.include(fooData), + test.expect(logs).to.include(globalEvent), + test.expect(logs).to.include(entityEvent), + test.expect(logs).to.include(operationEvent) + ]) + }) + }) + }) + }) + }) +}) diff --git a/test/functional/specs/about-api.specs.js b/test/functional/specs/about-api.specs.js index 2055379..dbcef10 100644 --- a/test/functional/specs/about-api.specs.js +++ b/test/functional/specs/about-api.specs.js @@ -15,7 +15,7 @@ test.describe('about api', function () { }) }) - test.it('should return service information', () => { + test.it('should return module information', () => { return connection.request('/about', { method: 'GET' }).then((response) => { diff --git a/test/functional/specs/auth-api.specs.js b/test/functional/specs/auth-api.specs.js index e15b1f0..450a442 100644 --- a/test/functional/specs/auth-api.specs.js +++ b/test/functional/specs/auth-api.specs.js @@ -14,10 +14,21 @@ const findApiKey = function (property, value) { }) } -test.describe('auth api', () => { +test.describe('auth api', function () { + this.timeout(10000) + let connection + + test.before(() => { + return utils.waitOnestimatedStartTime(2000) + .then(() => { + connection = new utils.Connection() + return Promise.resolve() + }) + }) + test.describe('post method', () => { test.it('should add a new api key with the provided data', () => { - return utils.request('/auth/apikey', { + return connection.request('/auth/apikey', { method: 'POST', body: { user: 'foo-user', @@ -37,7 +48,7 @@ test.describe('auth api', () => { }) test.it('should add another key for the same user if the provided user already exists', () => { - return utils.request('/auth/apikey', { + return connection.request('/auth/apikey', { method: 'POST', body: { user: 'foo-user', @@ -63,7 +74,7 @@ test.describe('auth api', () => { }) test.it('should return an error if the provided reference already exists', () => { - return utils.request('/auth/apikey', { + return connection.request('/auth/apikey', { method: 'POST', body: { user: 'foo-user-2', @@ -87,7 +98,7 @@ test.describe('auth api', () => { test.it('should delete the provided api key', () => { return findApiKey('reference', 'foo-api-key-2') .then((apiKey) => { - return utils.request(`/auth/apikey/${apiKey.key}`, { + return connection.request(`/auth/apikey/${apiKey.key}`, { method: 'DELETE' }).then((response) => { return PromiseB.props({ @@ -104,7 +115,7 @@ test.describe('auth api', () => { }) test.it('should return an error if the provided key does not exist', () => { - return utils.request(`/auth/apikey/fakeApiKey`, { + return connection.request(`/auth/apikey/fakeApiKey`, { method: 'DELETE' }).then((response) => { return test.expect(response.statusCode).to.equal(422) diff --git a/test/functional/specs/config-api.specs.js b/test/functional/specs/config-api.specs.js index 3a05b5d..f28bcc0 100644 --- a/test/functional/specs/config-api.specs.js +++ b/test/functional/specs/config-api.specs.js @@ -15,7 +15,7 @@ test.describe('config api', function () { }) }) - test.it('should return service configuration', () => { + test.it('should return module configuration', () => { return connection.request('/config', { method: 'GET' }).then((response) => { diff --git a/test/functional/specs/utils.js b/test/functional/specs/utils.js index c00b75c..7555c43 100644 --- a/test/functional/specs/utils.js +++ b/test/functional/specs/utils.js @@ -2,6 +2,7 @@ const path = require('path') const fs = require('fs') +const testUtils = require('narval/utils') const requestPromise = require('request-promise') @@ -75,8 +76,12 @@ class Connection { } } +const serviceLogs = (time = 200) => waitOnestimatedStartTime(time) + .then(() => testUtils.logs.combined('domapic-service')) + module.exports = { waitOnestimatedStartTime, + serviceLogs, request, readStorage, Connection,