grest.js
REST API framework for GNOME JavaScript. Talks JSON. Wraps libsoup, a native HTTP client/server library, and libgda, a data abstraction layer, with Promise-based plumbing.
Install
Grest is known to work on Gjs 1.55 with CommonJS runtime.
npm i -S grest
Usage
Routing is resourceful, model-centric. Entity classes are plain JS. Controllers extend Context
which resembles Koa, and have HTTP verbs (e.g. GET
) as method names.
const ServerListenOptions = importsgiSoup;const Context Route = ; { thishello = "world"; } async { await Promise; thisbody = ; } const App = Route; App;App;
Receving POST
In constructor, assign a sample body. Usually an array including a model example.
{ super; /** @type */ thisbody = ; } async { const greetings = thisbody; for const greeting of greetings greetinghello = "earth"; thisbody = greetings; }
Index
Your app self-documents at /
, keying example models by corresponding routes. Reads optional metadata from package.json
in current working directory. Omits repository link if private
is true.
Fetch
Makes a request with optional headers. Returns another Context.
const GLib = importsgiGLib;const Context = ; const base = "https://gitlab.gnome.org/api/v4/projects/GNOME%2Fgjs"; // Returns an array of issues.const path = "/issues"; const body = await Context; ;
Sending POST
Grest converts your body to JSON.
const base = "https://httpbin.org";const path = "/post"; const body = await Context;
Test
Check yourself with Gunit to get coverage.
// src/app/Greeting/GreetingController.test.js// Controller and entity are from the examples above. const Context Route = ;const test = ;const Greeting = ;const GreetingController = ; ;
Database
Assume you have a Product
table with the following schema:
( id varchar(64) not null primary key, name varchar(64) not null, price real)
Define an entity class to match your table:
{ thisid = ""; thisname = ""; thisprice = 0; }
Tell Grest where your db is, and give Route.server
an extra parameter:
const Db Route = ;const db = Db; // example.db in project rootconst services = db ;const routes = path: "/products" controller: ProductController ;const App = Route;App;App;
In-memory SQLite and other backends supported by Libgda can work too:
Db; // Grest parses database config from URL.Db; // When deploying, read your database config from an environment variable.Db;
For every request, Grest constructs your controller with your services as props:
/** @param {{ db: Db }} props */ { superprops; /** @type */ thisbody = ; thisrepo = propsdb; } // ...
Based on your entity class fields, Grest builds SQL from common queries, executing when you call await
:
/** * @example GET /products?name=not.in.(chair,table) * @example GET /products?limit=2&order=price.desc&price=gte.1 */async { thisbody = await thisrepo; // Or build your SELECT query programmatically, with a fluent chain: thisbody = await thisrepo namenot orderprice ;}
Whitelist or otherwise limit what a user can do:
/** @example DELETE /products?name=eq.chair */async { if !/^=eq\.[a-z0-9-]+$/ // Beginning digits, if any, define the HTTP response code. throw "403 Forbidden Delete Not By Name Or Price"; await thisrepo;}
Pass a JSON array as body when POSTing:
/** @example POST /products */async { await thisrepo; // Or CREATE manually: await thisrepo; // Won't do nulls, GDA_TYPE_NULL isn't usable through introspection.}
Wrap your PATCH body in an array as well, to reuse this.body
type:
/** @example PATCH [{ name: "armchair" }] /products?name=eq.chair */async { await thisrepo; // Doing an UPDATE manually: await thisrepo // New values. // WHERE conditions: name price;}
Db test shows how to make lower level SQL queries.
WebSocket
Grest optionally exposes your API through WebSocket, and lets users subscribe to receive a patch whenever you update the Product repo:
// ... // Whitelist entities that trigger a route refresh.ProductControllerwatch = Product; exportsProductController = ProductController;
Give Socket.watch
your routes and services in your entry point:
const services = db ; // Required.const App = Route;Socket;
Routes exposed to WebSocket can be same as HTTP, or a different set:
const App = Route; Socket;
Socket test shows how to set up the client side, and Patch test shows what subscribers recieve.
Logging
Goes to stdout and stderr by default. You can provide a custom logger instead:
const Context Db Route = ;const db = Db;const services = db log ; // Pass your logger as a service.const routes = path: "/products" controller: ProductController ;const App = Route;Socket;App;App; /** @param {Error?} error @param {Context?} context */ { if error ; else // ... }
For example, if you have a Log
entity and want to save the IP address:
const ip path protocol = context;if path !== "/logs" || protocol !== "websocket" // Avoid loop if watching. db;
Same fields are available as in controller:
// ... headers: key: string: string; id: string ip: string method: string path: string protocol: string query: string status: number userId: string // Unused internally. You can set in controller. // ...
Context toString()
returns Combined Log Format.
;// -> ::1 - - [12/Nov/2018:12:34:56 +0000] "GET /products?limit=3&name=not.in.(flowers)&offset=1&order=price.desc HTTP/1.1" 200 276 "-" "DuckDuckBot/1.0; (+http://duckduckgo.com/duckduckbot.html)"
License
MIT