@chipsgg/openservice

1.0.2 • Public • Published

Openservice Microservice Framework

A modern microservice framework to improve your application development, testing, deployment and maintenance.

Install

npm install -g @chipsgg/openservice

Run

You must have a configuration file, env, or json and your service code. See the API section for more information.

openservice config.js

QuickStart

Quick reference for getting started.

Service Definition

Services are designed as closures. Return your services api as an object of functions.

module.exports = async (config, services, emit) => {
  //initialize service here...

  //return service api
  return {
    ping() {
      return "pong";
    },
  };
};

Config Definition

An example configuration that shows some basic setup.

//in config.js
module.exports = {
  //the name of the service, used for logging
  name: "helloworld",
  //define all paths where service files can be found
  paths: [process.cwd(), __dirname],
  //defines which services to start and in what order
  //service startup is blocking and will fail the entire app if
  //one service fails
  start: ["helloworld.service"],
  //defines your named transports
  transports: {
    //the name of your local transport is the key 'local'
    local: {
      //openservice includes a local stream transport if your app is completely contained as a single process
      require: "openservice/transports/local",
    },
  },
  //helloworld is the namespace of this group of services
  //services must belong to a namespace.
  helloworld: {
    //service is the name of this service, it would be accessed like helloworld.service
    service: {
      //uses a file called helloWorld.js, you are telling openservice how to find the file
      require: "helloWorld",
      //uses the local transport
      transport: "local",
      //has no service dependencies (clients)
      clients: [],
      //has no injected configuration
      config: {},
    },
  },
};

Secrets & ENV

Openservice will create a json object from environment variables which get merged into your config.

helloworld.service.config.secret=12345
{
  helloworld: {
    service: {
      config: {
        secret: 12345;
      }
    }
  }
}

Starting Service

From the command line: openservice config.js

Or from within a js file

const OpenService = require('openservice')
const config = require('./config')

OpenService(config).then(([service])=>{
  console.log('Services started')
}).catch(err=>console.log(err)

Getting Started

Services

In this framework a service is a single function which returns some methods. You can imagine it as a library accessible over some kind of transport or inter process communication layer. The functions your service returns are the public functions accessible to any other service on the network.

async function(config, services, emit) => {}

  • config - This is a plain js object with options specified by the user. In practice these will map to environment variables to allow configuration of the service at run time.

  • services - this is a js object keyed by external service names. These represent external services which the current service is dependent on and are injected in when the service starts. Each service has a client which allows the developer to call functions in the typical async await pattern.

  • emit - this is a function which allows the service to emit messages for interested listeners. You will treat this similiar to a node event emitter in which you specify the topic and then the data.

A service has the option of returning serveral things:

  • nothing - service returns nothing on instantiation, this means nothing can call it externally

  • a function - service returns a single function which can be called by other services

  • a class - service returns a class or key value object of functions, this will be exposed to external services as its api

Service Example

//services are exposed as an asyncronous function with some standard parameters.
//this allows the service to take in all the information about the world it needs

module.exports = async (config, services, emit) => {
  //initialize service here...

  //return service api
  return {
    ping() {
      return "pong";
    },
  };
};

Service Clients API

The framework takes care of wrapping up your services and exposing them to the transport layer, and a client is created and injected into your service as a dependency if you specify a service needs it. Clients connect to the transport for you and create an interface to allow local-like interactions with an external service. You can call your external service as if it was a local library.

Service Example

// wallets.js
//imagine you have a service with some dependencies
//one of your clients is a users table with some on it.
module.exports = async (config, services, emit) => {
  const { users, wallets } = services;

  //services has users, wallets, notify clients
  async function withdraw(userid, amount) {
    //first make sure the user exists
    const user = await users.get(userid);

    //get withdraw from users wallet, which has same id as user
    const result = await wallets.withdraw(userid, amount);

    // emit like you would with native node event emitter api.
    emit("withdraw", {
      userid,
      amount,
    });

    return result;
  }

  return {
    withdraw,
  };
};

Listening to Events

All services can emit events using the openservice emit api, you can listen to them just like a normal node event mitter.

services.wallets.on('listen',(result,...arguments)=>{})

// Example in a statistics service:
module.exports = async (config, services) => {
  const { wallets } = services;

  //services has wallets
  const stats = {
    totalWithdrawn: 0,
  };

  wallets.on("withdraw", ({ userid, amount }) => {
    //update total withdrawn amount
    stats.totalWithdrawn += amount;
  });

  return {
    getStats() {
      return stats;
    },
  };
};

The Configuration File

Configs can be represented through env vars or through a JS object. The configuration tells openservice where to find your services files, what dependencies they need and any additional data the service needs. Configurations should be able to be merged together. In order to facilitate this theres a specific convention to define environment variables.

//this is the top level of the config file
module.exports = {
  name:string,    //required, gives a name for logs and errors for this service
  start:string[], //the services you want to start in this processs using the string path
  config:object,  //global configuration you want passed into every service defined
  paths:string[], //paths to where your service files are located
  transports:object, //key value of all available transports
}

Each individual service configuration is defined in the json object at a path. Services need to be namespaced in a top level directory in the config. This can be achieved like this.

require:string,   //the path of the service file
transport:string, //the name of the transport this service uses
clients:string[], //the path to any clients this service needs
config:object,    //configuration object passed into this service

See examples/basic/config.js or examples/advanced/config.js

Environment and Secrets

Environment variables are very important for configuring your services and this architecture accepts a convention for injecting variables into your service defintion. For example if you need to pass sensitive data you do not want committed to your project you can specify it in an .env file or your env variables. The convetion for injecting variables follows lodash's set interface. Furthermore the architecture will ignore any envs which begin with an uppercase, so only lowercase envs will be observed. In practice it produces something like this:

.env

express.cookieSecret=1234qwerty
users.username=admin
users.password=qwerty
auth.systemToken=abcdefg

config.js

{
  ...,
  users:{
   file:'./services/users',
   clients:[],
   //additional configuration for database table
   table:'users',
   //merged from env:
   username:'admin',
   password:'querty',
  },
  auth:{
   file:'./services/auth',
   clients:['users'],
   //merged from env:
   systemToken:abcdefg
  },
  express:{
   file:'./services/express',
   clients:['api'],
   port:80,
   //merged from env:
   cookieSecret:1234qwerty
  },
  ...
}

Service Multiplexing

Traditionally services are thought of as 1 to 1 to an application, kubenetes pod or docker container. Openservice discards that notion, it has no opinion on how many services you run in a single application. This allows flexibilty for economizing server usage by allowing bundling of many services together into a single container or pod. The downside is that this leads to some overhead with configuration specification, as these services are much like containers. Most of these specifications can be bundled with the source code as it defines mainly service names and dependencies.

Configuring Transports

Services typically need to talk to other services. This architecture requires the developer do this explicitly. Each application will require its own service definitions. These specify which services you want to run, where to find them, what you want to name them, how to configure them, and what dependencies. These are all specified in a json object.

module.exports = {
  //these are the services you want to run in your application. These names are what is exposed
  //in your service dependencies, so name carefully. You want your services names to be unique.
  //This also has a side effect of ordering the startup of each service. So place the most important
  //services first so that they can be relied on as dependents of other services. Otherwise
  //the application may deadlock on startup.
  services: ["users", "auth", "api", "express"],
  //here is where you specificy transport type. We are using a nats streaming driver.
  //it has certain configuration requirments which you pass in here.
  transport: "local",
  //all of these represent configuration for each service and get passed
  //in the service as the first argument "config".
  users: {
    //the service file is found here
    file: "./services/users",
    clients: [],
    //additional configuration for database table
    table: "users",
  },
  auth: {
    file: "./services/auth",
    clients: ["users"],
  },
  api: {
    file: "./services/api",
    //you can also specify remote services by name. Wallets could run in a seperate application
    //but is avaialble through the transport layer.
    clients: ["auth", "users", "wallets"],
  },
  express: {
    file: "./services/express",
    clients: ["api"],
    port: 80,
  },
};

Package Sidebar

Install

npm i @chipsgg/openservice

Weekly Downloads

114

Version

1.0.2

License

MIT

Unpacked Size

109 kB

Total Files

54

Last publish

Collaborators

  • tacyarg