npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

moleculer-web-uws

v0.1.1

Published

The `moleculer-web-uws` is a fast API gateway service for Moleculer based on µWebSockets.js server. Use it to publish your services over HTTP and WebSockets.

Downloads

5

Readme

An API Gateway for Moleculer framework

The moleculer-web-uws is a fast API gateway service for Moleculer based on µWebSockets.js server. Use it to publish your services over HTTP and WebSockets.

Features

  • HTTP & HTTPS
  • WebSockets with pub/sub support
  • Serve static files
  • Multiple routes
  • File uploading
  • Multiple body parsers (json, urlencoded, text)
  • CORS headers
  • Rate limiter
  • Global before & after call hooks
  • Buffer & Stream handling
  • Support authentication
  • Support authorization

Install

npm install moleculer-web-uws --save

Usage

Run with default settings

This example uses the API Gateway service with default settings. You can access all services (internal services not included) via http://localhost:3000/

let { ServiceBroker } = require('moleculer');
let ApiGateway = require('moleculer-web-uws');

let broker = new ServiceBroker({ logger: console });

// Create a service
broker.createService({
    name: 'test',

    actions: {
        hello: {
            rest: 'GET /hello',

            handler(ctx) {
                return 'Hello API Gateway!'
            }
        }
    }
});

// Load API Gateway
broker.createService(ApiGateway);

// Start server
broker.start();

Test URLs:

Call test.hello action: http://localhost:3000/test/hello

Documentation

Routes

You define a HTTP route in the action definition of your service using the rest property.

broker.createService({
    name: 'test',

    actions: {
        hello: {
            // Define your route
            rest: 'GET /hello',

            handler(ctx) {
                return 'Hello API Gateway!'
            }
        }
    }
});

You can also use the shorthand definition, in which the HTTP method is resolved to GET

broker.createService({
    name: 'test',

    actions: {
        hello: {
            // Call this action with `GET /test/hello`
            rest: '/hello',

            handler(ctx) {
                ...
            }
        }
    }
});

The route's url is resolved to /service-name/route-path

The service-name is the value passed to rest parameter in the schema settings object definition of your service or if no value is passed, the service-name is resolved to the specified name of your service. This is to avoid any collisions with other service's routes.

broker.createService({
    name: 'test',

    settings: {
        // this becomes the service-name if defined
        rest: 'alttest'
    }

    actions: {
        hello: {
            rest: '/hello',

            handler(ctx) {
                ...
            }
        }
    }
});

The route-path is the value passed to rest parameter in the schema settings object definition of your service or if no value is passed, the service-name is resolved to the specified name of your service. This is to avoid any collisions with other service's routes.

CORS

You can use CORS headers in the api gateway service

broker.createService({
    name: 'apigateway',

    mixins: [APIGateway],

    settings: {
        // Global CORS settings for all routes
        cors: {
            // Configures the Access-Control-Allow-Origin CORS header.
            origin: '*', // Can be an array of origins
            // Configures the Access-Control-Allow-Methods CORS header.
            methods: ['GET', 'OPTIONS', 'POST', 'PUT', 'DELETE'],
            // Configures the Access-Control-Allow-Headers CORS header.
            allowedHeaders: [],
            // Configures the Access-Control-Expose-Headers CORS header.
            exposedHeaders: [],
            // Configures the Access-Control-Allow-Credentials CORS header.
            credentials: false,
            // Configures the Access-Control-Max-Age CORS header.
            maxAge: 3600
        },
    }
});

You can also use Route specific CORS settings (overwrites global settings)

broker.createService({
    name: 'test',

    actions: {
        hello: {
            rest: {
                path: '/routewithcors',
                cors: {
                    origin: ['http://localhost:3000', 'https://localhost:4000'],
                    methods: ['GET'],
                    credentials: true
                },
            },

            handler(ctx) {
                ...
            }
        }
    }
});

Body-Parser

The Api Gateway implements json, urlencoded, text and multipart (File Uploading) by default. Only one of these can be used per route. To use a body-parser, you must add it to the rest definition.

broker.createService({
    name: 'test',

    actions: {
        hello: {
            rest: 'GET json /hello', // Valid body-parsers are; json, urlencoded, text, multipart

            params: {
                name: 'string' // This should be passed in json format
            }

            handler(ctx) {
                const { name } = ctx.params;
                ...
            }
        }
    }
});

Please note, the parsed body is accessible through ctx.params as an object with named properties as defined by the client.

Named Parameters

To use a named parameter, define it in the form route-path/:parametername. The parameter is accessed through ctx.params where the parametername is the property name.

broker.createService({
    name: 'test',

    actions: {
        hello: {
            rest: 'GET /hello/:name',

            handler(ctx) {
                const { name } = ctx.params;
                ...
            }
        }
    }
});

You can also use multiple named parameters as follows.

broker.createService({
    name: 'test',

    actions: {
        hello: {
            rest: 'GET /hello/:name/:age',

            handler(ctx) {
                const { name, age } = ctx.params;
                ...
            }
        }
    }
});

Authorization

To implement authorization. Do 2 things to enable it.

  1. Set authorize: true in your routes
  2. Define the authorize action in the api gateway service.

The returned value will be set to the ctx.meta.authz property. You can use it in your actions to get the authorized user entity.

broker.createService({
    name: 'test',

    actions: {
        hello: {
            rest: {
                path: 'GET /hello',

                // First thing
                authorize: true
            },

            handler(ctx) {
                const { id, username, name } = ctx.meta.authz;
                ...
            }
        }
    }
});
let ApiGateway = require('moleculer-web-uws');
const { UnAuthorisedError } = require('moleculer-web').Errors;

broker.createService({
    name: 'apigateway',

    mixins: [APIGateway],

    settings: {
        ...
    },

    actions: {
        // Second thing
        async authorize(ctx) {
            const { req, res } = ctx.params;
            // Define your authorization logic, like calling an auth service like so
            let tokenId;

            if (req.headers.authorization) {
                const parts = req.headers.authorization.split(' ');
                if (parts[0]/** type */ === 'Bearer') tokenId = parts[1];
            }

            if (tokenId === '12345') {
                // Ensure that that you return every information you might need later in
                // your service's action so that you can access them as ctx.meta.authz
                return { id: 1, username: 'john.doe', name: 'John Doe' };
            }

            throw new UnAuthorizedError('Token Id not detected');
        }
    }
});

Please note that the module will not throw an UnAuthorizedError on your behalf. You have to throw it yourself if authorization fails.

Authentication

To enable authentication, you need to do something similar to what is describe in the Authorization paragraph. Also in this case you have to:

  1. Set authenticate: true in your routes
  2. Define your custom authenticate action in the api gateway service

The returned value will be set to the ctx.meta.auth property. You can use it in your actions to get the logged in user entity.

broker.createService({
    name: 'test',

    actions: {
        hello: {
            rest: {
                path: 'GET /hello',

                // First thing
                authenticate: true
            },

            handler(ctx) {
                const { id, username, name } = ctx.meta.auth;
                ...
            }
        }
    }
});

Example

let ApiGateway = require('moleculer-web-uws');

broker.createService({
    name: 'apigateway',

    mixins: [APIGateway],

    settings: {
        ...
    },

    actions: {
        // Second thing
        async authentication(ctx) {
            const { req, res } = ctx.params;
            let accessToken = req.query['access_token'];

            if (accessToken) {
                if (accessToken === '12345') {
                    // valid credentials. It will be set to `ctx.meta.auth`
                    return { id: 1, username: 'john.doe', name: 'John Doe' };
                }

                // invalid credentials
                throw new Error('Could not login user!');
            } else {
                // anonymous user
                return null;
            }
        }
    }
});

File Uploading

To upload a file(s), use the multipart property and no other body-parser should should be defined on the same action.

broker.createService({
    name: 'test',

    actions: {
        hello: {
            rest: {
                path: 'POST /upload',

                multipart: {
                    fileSize: 100000, // Size in bytes that should not be exceeded

                    files: 1, // Number of files expected

                    fields: 3, // Number of fields that should not be exceeded.

                    onFileSizeLimit: (sizeOfReceivedFile, sizeExpected) => {
                        ...
                    },

                    onFieldsLimit: (numFieldsReceived, numFieldsExpected) => {
                        ...
                    },

                    onFilesLimit: (numFilesReceived, numFilesExpected) => {
                        ...
                    }
                }
            },

            handler(ctx) {
                // ctx.param will be an object of the form {fieldname, data, type, filename} or an array of
                // such objects if more than 1 file is received.
                const files = ctx.params;
                const { fieldname, data, type, filename, size } = file;

                // The fields are passed as an array of the form [fieldname => value]
                const fields = ctx.meta.$multipart;

                // Do something with the received file(s) and field(s)
                ...
            }
        }
    }
});

Please note, the data property is a ReadableStream of the file data hence should be handled as a stream.

A global multipart options can be defined in the API Gateway as follows.

let ApiGateway = require('moleculer-web-uws');

broker.createService({
    name: 'apigateway',

    mixins: [APIGateway],

    settings: {
        multipart: {
            fileSize: 100000, // Size in bytes, that should not be exceeded

            files: 1, // Number of files expected

            fields: 3, // Number of fields that should not be exceeded.

            onFileSizeLimit: (size, sizeExpected) => {
                ...
            },

            onFieldsLimit: (numFields, numFieldsExpected) => {
                ...
            },

            onFilesLimit: (numFiles, numFilesExpected) => {
                ...
            }
        }
    }
});

You can use the shorthand definition where multipart is passed as the body-parser. The multipart settings defined in the API Gateway will be used instead.

broker.createService({
    name: 'test',

    actions: {
        hello: {
            rest: 'POST multipart /upload',

            handler(ctx) {
                ...
            }
        }
    }
});

Static Files

To access static files, define static settings in the API Gateway.

let ApiGateway = require('moleculer-web-uws');

broker.createService({
    name: 'apigateway',

    mixins: [APIGateway],

    settings: {
        static: {
            // The url through which the file will be accessible as `/assets/:filename.ext`
            rest: '/assets',

            // Path to the directory where the files are stored
            folder: '/assets',

            // Defaults to false
            cache: true,

            // Custom headers to pass to response headers
            // Optional
            headers: {},

            // Defaults to false
            etag: true,
        }
    }
});

The Static File server is published as a separate service.

Whitelist

Service actions with the rest property in their definition are whitelisted by default. If the rest property is not defined or the service action's visibility is not public, it'll not be whitelisted to be accessed through the API Gateway.

Please note, routes are not defined within the API Gateway, but within the action definition of your service's schema.

WebSockets

To use WebSockets, ws setting must be defined in the API Gateway as follows.

let ApiGateway = require('moleculer-web-uws');

broker.createService({
  name: 'apigateway',

  mixins: [ApiGateway],

  settings: {
    ws: {
        path: '/*',

        compression: 0,

        idleTimeout: 0,

        maxBackPressure: 1024 * 1024,

        maxPayloadLength: 16 * 1024,

        keepAlive: {
            // Amount of seconds after which a PING message is sent to the client
            interval: 5000,

            // The message to be sent as a PING to the WebSocket client.
            // Can be any value, as long as the client will be able to identify it as a PING control message
            // from the server.
            ping: new Uint8Array([50]),

            // The message to be received from the WebSocket client as a PONG control message.
            // Can be a Uint8Array or integer(Will be converted to TypedArray)
            pong: new Uint8Array([50]),
        },

        upgrade: (res, req, context) => {
            ...
        },

        open: (socket) => {
            ...
        },

        message: async (app, socket, message, isBinary, topic) => {
            ...
        }
    },
  }
});

The path property MUST be passed. It is the url over which the WebSocket Connection is upgraded. The rest of the properties are optional. If not defined, the are resolved to their default values.

compression

What permessage-deflate compression to use. Can be DISABLED, SHARED_COMPRESSOR or any of DEDICATED_COMPRESSOR_xxxKB. Defaults to SHARED_COMPRESSOR. Read more at uWebSockets.js

idleTimeout

Maximum amount of seconds that may pass without sending or getting a message. Connection is closed if this timeout passes. Disable by using 0. Defaults to 120. Read more at uWebSockets.js

maxBackPressure

Maximum length of allowed backpressure per socket when publishing or sending messages. Defaults to 1024 * 1024. Read more at uWebSockets.js

maxPayloadLength

Maximum length of received message. If a client tries to send a message larger than this, the connection is immediately closed. Defaults to 16 * 1024. Read more at uWebSockets.js

You can also whitelist an action to be accessed over WebSockets by including the ws property in its definition. The action is routed through a PUB/SUB protocol in which the WebSocket client specifies the topic to publish the message to. If this topic is matched, the received message from the client is passed to the action as ctx.params.

broker.createService({
  name: 'test',

  actions: {
    draw: {
      ws: {
        // If not defined, the topic is resolved to `service-name/action-name`
        topic: 'tool/draw',

        // The result of this action will be published to all clients subscibed to this topic.
        // Optional
        publish: true,

        // The result of this action will be sent to the client who sent the message
        // Optional
        send: true,

        // If defined and is a callback, a client will only be subscribed to the topic if the callback returns true.
        // Optional
        condition: false // Should return a truthy result
      },

      handler(ctx) {
        const message = ctx.params;
        const connectionContext = ctx.meta.c_ctx;
        ...
        // Do something with the message and return the results(Optional)
      }
    }
  }
})

License

moleculer-web-uws is available under the MIT license.

Contact

Copyright (c) 2021 Jimmie Lovell