0.0.16 • Published 6 years ago

nsoap-express v0.0.16

Weekly downloads
1
License
MIT
Repository
-
Last release
6 years ago

NSOAP: Native Syntax Object Access Protocol

NSOAP is a Remote Procedure Call (RPC) and URL convention that uses familiar JavaScript syntax for method invocation and parameter passing. In addition to web services, NSOAP conventions can also be used for client-side routing in React, Angular etc. The NSOAP project provides routers for Express, Koa and React. Contributions are invited for other frameworks and languages.

Attempting to explain it without code is futile. Let's go straight to the examples.

Choose your framework

Installation

npm install nsoap-express

Initializing your App: The App Object

The App Object (myApp in the following example) contains the "routes" the application will respond to.

const express = require("express");
const nsoap = require("nsoap-express");

const app = express();

const myApp = {
  addTwoNumbers(x, y) {
    return x + y;
  },
  sayHello(name) {
    return `Hello ${name}!`
  }
}

app.use(nsoap(myApp));

Invoking Functions

Invoking a function that adds two numbers looks like plain JavaScript.

curl "http://www.example.com/addTwoNumbers(10,20)"

Arguments can be strings, numbers or booleans. If they are strings, they must still be valid JavaScript identifiers.

# numeric argument
curl "http://www.example.com/addTwoNumbers(10,20)"
# boolean argument
curl "http://www.example.com/findAll(true)"
# string argument
curl "http://www.example.com/search(thomas)"

Use parameters.

# numeric
curl "http://www.example.com/addTwoNumbers(x,y)?x=10&y=20"
# string
curl "http://www.example.com/search(x)?x=thomas"

Parameters may be passed via Querystring, Body, Headers or Cookies.

# Using POST
curl -H "Content-Type: application/json" -X POST -d '{"x":10,"y":20}' "http://www.example.com/addTwoNumbers(x,y)"

If the argument is passed via the Querystring and it contains spaces or other characters, you will need to quote and encode them. They can only be passed via parameter variables. Hence we recommend using querystrings only for numbers, booleans and simple strings.

# Have spaces? Must quote and encode.
# %22 is double quote, %20 is space
# x = "thomas jacob"
curl "http://www.example.com/search(x)?x=%22thomas%20jacob%22"

Returning a response

Handlers can return the value to be sent to the client. The router will take care of sending it as an HTTP Response.

const myApp = {
  addTwoNumbers(x, y) {
    return x + y;
  },
}

In case there is an error, throw an exception.

const myApp = {
  addTwoNumbers(x, y) {
    if (typeof  x === "undefined" || typeof y === "undefined") {
      throw new Error("Invalid value.")
    } else {
      return x + y;
    }
  },
}

On the server, use GET, POST, PUT whatever.

NSOAP urls are callable via all HTTP methods. It is strongly suggested that methods are not restricted this way. However, if you do want to enforce HTTP methods on certain handlers, read the section Raw Request and Response Handling.

# Using GET
curl "http://www.example.com/addTwoNumbers(10,20)"
# Using POST
curl -H "Content-Type: application/json" -X POST -d '{"x":10,"y":20}' "http://www.example.com/addTwoNumbers(x,y)"

Organizing code with Namespaces

Invoke a function defined on an object. This allows organizing the code into namespaces similar to directories.

const myApp = {
  math: {
    square(x) {
      return x * x;
    }
  }
}
curl "http://www.example.com/math.square(20)"
# OR
curl "http://www.example.com/math.square(x)?x=20"
# returns 400

Optional Parenthesis

Parenthesis may be omitted if the function can be called without arguments.

const myApp = {
  default() {
    return "Hello";
  }
}
curl "http://www.example.com/default"
# is the same as
curl http://www.example.com/default()

Default Functions

Applications can specify the default function to be called when the url omits the function name. The default is "index".

const myApp = {
  index() {
    return "Hello";
  }
}
# Assuming that the default function name has not been changed
curl "http://www.example.com/"

To specify an alternate default function, use options while creating the router.

const myApp = {
  myDefaultFunc() {
    return "Hello";
  }
}

const options = { index: "myDefaultFunc" }
app.use(nsoap(myApp, options));

Promises

If the handler returns a Promise, the resolved value of the Promise is sent back as the response.

const myApp = {
  greeting() {
    return Promise.resolve("Hello");
  }
}
# Returns Hello
curl "http://www.example.com/"

Function Chaining

Chained function calls work the same way you expect it to work. The following url invokes the getAccounts function on the result of the getCustomer function. If the function returns a Promise (or a Future), the Promise is resolved and the subsequent function or object is accessed on the resolved value.

curl "http://www.example.com/getCustomer(100).getAccounts(2017)"
#OR
curl "http://www.example.com/getCustomer(custId).getAccounts(year)?custId=100&year=2017"

Parameter Type Inference

NSOAP supports parameter type inference for strings, numbers and booleans. In the following example, the function parameters are inferred as string, number, boolean and number.

curl "http://www.example.com/search(Jeswin,20,true,x)?x=100"

Case-sensitivity

NSOAP parameter parsing is case-sensitive. So the following will not assign 100 to the parameter 'x'.

# Error. 'x' is not the same as 'X'
curl "http://www.example.com/squareRoot(x)?X=100"

Advanced Default Functions

If the return value is an object, and it contains a property with the same name as the default function name, and if the value of the property is a function, it is invoked.

That may sound confusing, let's look at an example. Assume that the default function name is unchanged (ie, "index"). The addTwoNumbers() function returns an object with a property named "index" which is a function. Since it matches the default function name, it is invoked.

const myApp = {
  addTwoNumbers(x, y) {
    return {
      index() {
        return x + y;
      }
    }
  },
}
curl "http://www.example.com/addTwoNumbers(10,20)"
# returns 30

This allows for more powerful chained functions. The following app can now respond to the urls /getCustomer(1) and /getCustomer(1).getTotalPurchases()

const myApp = {
  getCustomer(id) {
    return {
      index() {
        return id === 1 ? { name: "Jeswin" } : { name: "Thomas" }
      },
      totals() {
        return id === 1 ? 100 : 200;
      }
    }
  },
}
# will return { name: "Jeswin" }
curl "http://www.example.com/getCustomer(1)"

# will return 100
curl "http://www.example.com/getCustomer(1).totals"

Raw Request and Response Handling

Sometimes, you might want to access the request and response objects directly. You could do that by returning a function as the result. In the following example, the handler 'getCustomerName' has full access to the request and response objects.

const myApp = {
  getCustomerName(id) {
    return (req, res) => {
      res.send(id === 1 ? "Jeswin" : "Thomas")
    }
  },
}
# will return "Jeswin"
curl "http://www.example.com/getCustomerName(1)"

HTTP Headers and Cookies

Values defined in HTTP Headers and Cookies are given the same treatment as values passed via querystring or the body. They key is taken as the parameter name and the corresponding value is assigned.

# returns 400
curl --header "x:20" "http://www.example.com/math.square(x)"

Security

NSOAP is HTTP method agnostic. This means that a method can be called via GET, POST or whatever. If you wish to allow only specific HTTP methods, you need to use Raw Request and Response Handling.

const myApp = {
  greeting(id) {
    return (req, res) => {
      if (req.method === "POST") {
        res.send("Hello")
      } else {
        res.status(404).send("Sorry, nothing to see here.")
      }
    }
  },
}

Be careful with Cookies. Ensure that you are protected against CSRF vulnerabilities. Our recommendation is that you use HTTP Headers (and don't use Cookies) if you're building a Single Page App or using AJAX to make calls.

# Use headers for passing tokens
curl --header "session-token:AD332DA12323AAA" "http://www.example.com/placeOrder(itemId, quantity, sessionToken)?itemId=200&quantity=3"

Order of searching parameters in a Request

NSOAP server-side routers must attempt to find parameter values in the following order

  • Headers
  • Querystring
  • Body
  • Cookies

This means that if a parameter say "x" is defined in the Headers and in the Body, the value found in Headers will take precedence.

Status Codes

Router will set the status code to 200 when the function executes without errors and 500 if an exception was thrown. If special status codes are needed (say HTTP 301 Permanent Redirect), applications should use Raw Request and Response Handling as in the following example.

const myApp = {
  getCustomer(id) {
    return (req, res) => {
      res.status(200).send("Hello")
    }
  },
}

Reading and writing Request and Response streams

To read or write streams, you can use Raw Request and Response Handling.

const myApp = {
  streamData() {
    return (req, res) => {
      const interval = setInterval(() => {
        res.write(JSON.stringify({ foo: Math.random() * 100, count: ++c }) + '\n');
        if (c === 10) {
          clearInterval(interval);
          res.end();
        }
      }, 1000);
    }
  },
}

Streaming responses with Generator Functions

Enable this by setting streamResponse in options. Response chunks are sent every time a yield is called. The headers need to be yielded first.

const myApp = {
  *streamingGreeting() {
    yield {
      "Content-Type": "text/plain",
      "Transfer-Encoding": "chunked"
    };
    yield "HELLO";
    yield "WORLD";
    return "!!!";
  }
}

//Set the streamResponse boolean
const options = { streamResponse: true }
app.use(nsoap(myApp, options));

You can also use asynchronous generator functions.

const myApp = {
  async *streamingGreeting() {
    yield {
      "Content-Type": "text/plain",
      "Transfer-Encoding": "chunked"
    };
    yield await Promise.resolve("HELLO");
    yield await Promise.resolve("WORLD");
    return "!!!";
  }
}

//Set the streamResponse boolean
const options = { streamResponse: true }
app.use(nsoap(myApp, options));

Advanced Options

Additional options may be pass in via the options object while initializing the router. You would probably not need any of these.

urlPrefix: string

Makes the router handle only urls prefixed with the specified path.

//...
const options = { urlPrefix: "/home" };
app.use(nsoap(myApp, options));

alwaysUseJSON: boolean

When the result of invoking a handler is a string, the router defaults to sending it as text instead of JSON. In this case, the result goes into response.text instead of response.body. The alwaysUseJSON setting can be used to force JSON responses even when the result is a string.

//...
const options = { alwaysUseJSON: true };
app.use(nsoap(myApp, options));

appendContext: boolean

Passes the ExpressJS context (request and response object) as a method parameter. By default the context is the object { req, res, isContext: () => true }

By doing this, you might be able to avoid Raw Request and Response Handling. Note that the context is passed in as the first argument.

const myApp = {
  addTwoNumbers(context, x, y) {
    if (context.req.method === "GET") {
      return x + y;
    }
  }
}

const options = { appendContext: true };
app.use(nsoap(myApp, options));

You can also choose to respond directly via the context. If so, ensure that you set context.handled to let the router know that the response needs no additional handling.

const myApp = {
  addTwoNumbers(context, x, y) {
    context.handled = true;
    context.res.status(200).send(`${x + y}`)
  }
}

const options = { appendContext: true };
app.use(nsoap(myApp, options));

createContext(context: Object) : Object

The context passed by setting appendContext can be altered by using the createContext option.

const myApp = {
  addTwoNumbers(context, x, y) {
    if (context.IS_ADMIN) {
      return x + y;
    }
  }
}

function createContext(context) {
  return { ...context, IS_ADMIN: req.connection.remoteAddress === "127.0.0.1" }
}

const options = { appendContext: true, createContext };
app.use(nsoap(myApp, options));

getBody(req: Request) : Object

Allows an app to alter the body parameter dictionary passed into the router. If getBody is not defined, the body is assumed to be req.body; which is what middleware like "bodyparser" do.

The following example adds additional parameters to the body dictionary which was already created by bodyparser.

function getBody(req) {
  return { ...req.body, newParam: 1, anotherParam: "TWO" }
}

const options = { getBody };
app.use(nsoap(myApp, options));

getCookies(req: Request) : Object

Allows an app to alter the Cookies parameter dictionary passed into the router. If getCookies is not defined, the body is assumed to be req.cookies; which is what middleware like "cookie-parser" do.

The following example adds additional parameters to the cookies dictionary which was already created by cookie-parser.

function getCookies(req) {
  return { ...req.cookies, newParam: 1, anotherParam: "TWO" }
}

const options = { getCookies };
app.use(nsoap(myApp, options));

parseQuery(query: Object) : Object

Allows an app to alter the querystring dictionary passed into NSOAP.

function parseQuery(query) {
  return { ...query, id: 100 }
}

const options = { parseQuery };
app.use(nsoap(myApp, options));

parseBody(body: Object) : Object

Allows an app to alter the body dictionary passed into NSOAP. This is the same dictionary that was previously constructed via getBody() or in its absense req.body.

function parseBody(body) {
  return { ...body, id: 100 }
}

const options = { parseBody };
app.use(nsoap(myApp, options));

parseHeaders(headers: Object) : Object

Allows an app to alter the headers dictionary passed into NSOAP.

function parseHeaders(headers) {
  return { ...headers, id: 100 }
}

const options = { parseHeaders };
app.use(nsoap(myApp, options));

parseCookies(cookies: Object) : Object

Allows an app to alter the cookies dictionary passed into NSOAP.

function parseCookies(cookies) {
  return { ...cookies, id: 100 }
}

const options = { parseCookies };
app.use(nsoap(myApp, options));

onResponseStream(req: Request, res: Response, next: nextMiddleware) : (val: Object) => void

onResponseStream and three sibling methods allow for custom Response Stream Handling. onResponseStreamHeader is called when the headers are received (first yield returns headers) from the generator function. onResponseStream is called for each subsequent yielded value. onResponseStreamEnd is called when the last value is returned by the generator. onResponseStreamError is called whenever there is an error.

const myApp = {
  *streamingGreeting() {
    yield {
      "Content-Type": "text/plain",
      "Transfer-Encoding": "chunked"
    };
    yield "HELLO";
    yield "WORLD";
    return "!!!";
  }
}

function onResponseStreamHeader(req, res) {
  return val => res.writeHead(200, val);
}

function onResponseStream(req, res) {
  return val => res.write(val);
}

function onResponseStreamEnd(req, res) {
  return val => res.end(val);
}

function onResponseStreamError(req, res, next) {
  return error => next(error);
}

const options = {
  onResponseStreamHeader,
  onResponseStream,
  onResponseStreamEnd,
  onResponseStreamError
};

app.use(nsoap(myApp, options));
0.0.16

6 years ago

0.0.15

7 years ago

0.0.14

7 years ago

0.0.13

7 years ago

0.0.12

7 years ago

0.0.11

7 years ago

0.0.10

7 years ago

0.0.8

7 years ago

0.0.7

7 years ago

0.0.5

7 years ago

0.0.3

7 years ago

0.0.2

7 years ago

0.0.1

7 years ago