ORT-NODEJS(1)

NAME
ort-nodejs - generate node.js module

SYNOPSIS
ort-nodejs [-ev] [-N db] [config...]

DESCRIPTION
Accepts ort(5) config files, defaulting to standard input, and generates a Node module in TypeScript. Its arguments are as follows:

-e  Embedded output. Does not emit import statements, nor are top-level namespaces, classes, and functions marked as export. This is useful for embedding instead of using as a module. This flag may be deprecated in the future.

-v  Output Validation object and types.

-N bd
    Disable production of output, which may be b and/or d. The b flag suppresses output of the foundational Data structures, while d suppresses the Module and Database access methods. This flag is used when creating multiple output files.

The output requires only the "typescript", "better-sqlite3", "bcrypt", and optionally "validator" dependencies. The output is documented using the typedoc(1) language.

  Module
An application context is initialised with the top-level function. It throws an exception on database error.

ort(dbname: string, args?: ortargs): ortdb
    Creates an application-wide database instance at dbname, an existing sqlite3 database usually created with ort-sql(1). The optional args consists of configuration parameters. If provided, it must define the bcrypt_cost property with a valid number of encryption rounds. If args is not given, the number of rounds defaults to 10.

The ortdb object has the following properties and methods:

connect(): ortctx
    Connect to the database. This should be invoked for each request. In applications not having a request, this corresponds to a single operator sequence. If roles are enabled, the connection will begin in the "default" role.

args: ortargs
    Instance-wide configuration. If not set by ort(), is set to the default values.

version: string
    The version of ort-nodejs used to create the module.

vstamp: number
    The numeric version of ort-nodejs used to create the module.

Each sequence of database interaction begins with the connect() method from the application-wide instance. The resulting ortctx object has the following role methods:

db_role(newrole: string): void
    If roles are enabled, move from the current role to newrole. If the role is the same as the current role, this does nothing. Roles may only transition to ancestor roles, not descendant roles or siblings, or any other non-ancestor roles. The only exception is when leaving "default" or entering "none". This does not return failure: on role violation, it invokes process.abort().

db_role_current(): string
    If roles are enabled, get the currently-assigned role. If db_role() hasn't yet been called, this will be "default".

The following ortctx methods should be used for transaction management. These throw an exception on database error.

db_trans_open_immediate(id: number): void
    Open a transaction with a unique identifier id. This is the preferred way of creating database transactions. The transaction immediately enters unshared lock mode (single writer, readers allowed).

db_trans_open_deferred(id: number): void
    Open a transaction with a unique identifier id. The transaction locks the database on first access with shared locks (no writes allowed, reads allowed) on queries and unshared locks (single writer, reads allowed) on modification.

db_trans_open_exclusive(id: number): void
    Open a transaction with a unique identifier id. The transaction locks exclusively, preventing all other access.

db_trans_commit(id: number): void
    Commit a transaction opened by db_trans_open_immediate(), db_trans_open_deferred(), or db_trans_open_exclusive() with identifier id.

db_trans_rollback(id: number): void
    Roll-back a transaction opened by db_trans_open_immediate(), db_trans_open_deferred(), or db_trans_open_exclusive() with identifier id.

Any insert statements are output as follows. Let "foo" be the name of the exemplar structure in this and subsequent prototypes. Throws an exception on database error.

db_foo_insert(ARGS): bigint
    Insert a row and return its identifier or -1 on constraint violation. This accepts all native fields ARGS as parameters excluding rowid, which is automatically set by the database. If any fields are specified as null, they may be passed as null values.

Query statements count, iterate, list, search are output as follows. These throw an exception on database error.

db_foo_count(): bigint
    Like db_foo_count_xxxx() but returning a count of all rows.

db_foo_count_xxxx(ARGS): bigint
    Like db_foo_get_xxxx(), but returning a count of responses.

db_foo_count_by_xxxx_op1_yy_zz_op2(ARGS): bigint
    Like db_foo_get_by_xxxx_op1_yy_zz_op2(), but returning a count of responses.

db_foo_get_xxxx(ARGS): ortns.foo|null
    The search statement named "xxxx". The function accepts variables for all binary-operator fields to check (i.e., all except for those checking for null).

db_foo_get_by_xxxx_op1_yy_zz_op2(ARGS): ortns.foo|null
    Like db_foo_get_xxxx(), but for (possibly-nested) structures. In the given example, "xxxx" is a field in the given structure with operation "op1" and "yy_zz" means a field "zz" in the nested structure "yy" with operation "op2".

db_foo_iterate(ARGS, cb): void
    Like db_foo_iterate_xxxx() but iterating over all rows.

db_foo_iterate_xxxx(ARGS, cb): void
    Like db_foo_get_xxxx(), but invoking a function callback per row. The cb callback accepts a single parameter of type ortns.foo and does not have a return value.

void db_foo_iterate_by_xxxx_op1_yy_zz_op2(ARGS, cb): void
    Like db_foo_get_by_xxxx_op1_yy_zz_op2(), but invoking a function callback for each retrieved result.

db_foo_list(): ortns.foo[]
    Like db_foo_list_xxxx() but allocating and filling a queue of all rows.

db_foo_list_xxxx(ARGS): ortns.foo[]
    Like db_foo_get_xxxx(), but producing an array of responses.

db_foo_list_by_xxxx_op1_yy_zz_op2(ARGS): ortns.foo[]
    Like db_foo_get_by_xxxx_op1_yy_zz_op2(), but producing a queue of responses.

Any update statements in the configuration are output as the following methods on ortctx. These throw an exception on database error.

db_foo_update_xxxx(ARGS): boolean
    Run the named update function "xxxx". The update functions are specified with update statements. The parameters passed to this function are first the fields to modify, then the fields that constrain which rows are updated. Update fields are only specified for operations for binary-operator constraints, i.e., those not checking for null status. Returns true on success, false on constraint failure.

db_foo_update_xx_mod_by_yy_op(ARGS): boolean
    Like db_foo_update_xxxx(), but using an un-named update statement modifying "xx" with modifier "mod" constrained by "yy" with operation "op". Either or both modifiers and constraints may be empty. If modifiers are empty, all fields are modified by setting. If constraints are empty, they and the preceding "by" are omitted.

Any delete statements in the configuration are output as the following methods on ortctx. These throw an exception on database error.

db_foo_delete_xxxx(ARGS): void
    Run the named delete function "xxxx". The ARGS passed to this function are the fields that constrain which rows are deleted. Parameters are only specified for operations for binary-operator constraints, i.e., those not checking for null status.

db_foo_delete_by_yy_op(ARGS): void
    Like db_foo_delete_xxxx(), but using an un-named delete statement constrained by "yy" with operation "op".

  Exceptions
All database functions will throw an exception if errors occur in talking to the underlying database. The exception object returned is of type SqliteError, inheriting from the generic Error. It is documented in the "better-sqlite3", but consists of two pertinent members:

code: string
    The extended sqlite3 error code in string format. For example, the SQLITE_IOERR_FSTAT(3) error code is rendered as the string "SQLITE_IOERR_FSTAT".

message: string
    A free-form message describing the error.

  Data structures
Any enumerations or bitfields (enum, bitf) in the configuration are output in the ortns namespace and named as themselves. Each enumeration item is serialised as a string property. Bitfields have two property per item: one for the bit index, one for the produced mask. These are prefixed by "BITI" and "BITF", respectively.

    namespace ortns {
    	enum anenum {
    		item2 = '3'
    	}
    	enum abitf {
    		BITI_item2 = '3',
    		BITF_item2 = '8',
    	}
    }

Each struct is output as an interface in the ortns namespace and named as itself followed by "Data", such as ortns.fooData. All items of the structure are mapped to properties with the following types:
bit | bigint
bits | bigint
blob | Buffer
date | bigint
email | string
enum | ortns.enum
epoch | bigint
int | bigint
password | string
real | number
text | string

Since bitfields are combinations of bits in their respective enumerations, they are represented by bigint and not the enumeration type.

If a field is marked as null, it will also be given the null type, such as bigint|null.

    namespace ortns {
            interface barData {
                    anint: bigint;
                    astring: string;
    		anulldate: bigint|null;
            }
    }

Data objects returned by query methods are output as classes in the ortns namespace and named as in the configuration. Letting "foo" be an exemplar structure name, ortns.foo, the object consists of the following.

obj: ortns.fooData
    The read-only data itself.

export(): any
    Create an exportable object. Export rules are governed by the role in which the object was created. This is usually used with JSON.stringify() to output JSON objects.

The exported object, when converted into a string, is readable by applications using the ort-javascript(1) tool.

  Validation
If run with -v, ort-nodejs outputs validation functions for each native field type in an object ortvalid.ortValids, with a validator for each field. The fields (object properties) are named struct-field.

Validator functions are typed according to their mapped field types as described in Data structures: (value?: any) => TYPE|null, and accept the value (which may be undefined) of the request input. These return null when the input is undefined, undefined, null, fails any user-defined validation, or the following:
bit | not in 0–63
date | not ISO-8601 format
epoch | not 64-bit signed integer
int | not 64-bit signed integer
real | not 64-bit decimal number
email | not valid e-mail
bits | not contained in 64 bits

EXIT STATUS

EXAMPLES
The following example is a full web-server running on port 3000 using the Node framework. It uses the "express", framework for web requests, "validator" for input validation, "bcrypt" for passwords, and "better-sqlite3" for the database. It mandates the use of TypeScript instead of JavaScript. It needs only the npm(1) system installed and (depending on the operating system) a C/C++ compiler for native packages.

Begin a project (if not already begun) as follows:

    % cd myproject
    % npm init -y
    % npm install typescript better-sqlite3 express bcrypt
    % npm install @types/express @types/bcrypt @types/better-sqlite3
    % npx tsc --init

For validation:

    % npm install validator @types/validator

When installing "better-sqlite3" on OpenBSD, you may need to specify an alternate Python interpreter:

    % PYTHON=/usr/local/bin/python2.7 \
      npm install better-sqlite3

Older versions of OpenBSD required overriding CXX and CC with ports-installed versions for both "better-sqlite3" and "bcrypt".

Modify package.json to mandate the use of TypeScript instead of JavaScript:

    [...]
    "main": "index.ts",
    "scripts": {
      "tsc": "tsc"
    }
    [...]

Next, modify tsconfig.json to use a more up-to-date output type for JavaScript, otherwise many TypeScript security idioms will not be available.

    "target": "es2015",

Now use the following toy ort(5) configuration installed as myproject.ort:

    roles {
      role user;
    };
    struct user {
      field name text limit gt 0;
      field id int rowid;
      insert;
      search id: name id;
      roles default { all; };
    };

Compile the configuration as a module. This assumes that validation is also required.

    % mkdir modules
    % ort-nodejs -v myproject.ort > modules/ort.ts

Use the following simple application:

    import express from 'express';
    import { ort, ortns, ortctx, ortdb, ortvalid } from './modules/ort';
    const app: express.Application = express();
    const db: ortdb = ort('test.db');
    app.get("/put",
      function(req: express.Request, res: express.Response) {
        const ctx: ortctx = db.connect();
        const name: string|null = ortvalid.ortValids
          ['user-name'](req.query['user-name']);
        if (name === null)
          return res.status(400).send('bad');
        const id: bigint = ctx.db_user_insert(name);
        return res.send(id.toString());
      }
    );
    app.get("/get",
      function(req: express.Request, res: express.Response) {
        const ctx: ortctx = db.connect();
        const id: bigint|null = ortvalid.ortValids
          ['user-id'](req.query['user-id']);
        if (id === null)
          return res.status(400).send('bad');
        const obj: ortns.user|null = ctx.db_user_get_id(id);
        if (obj === null)
          return res.status(404).send('not found');
        return res.send(JSON.stringify(obj.export()));
      }
    );
    app.listen(3000, function() {
      console.log('Server is running.');
    });

Compile the application. This will create index.js.

    % npm run tsc

Make sure that the database exists. This should only be run once.

    % ort-sql db.ort | sqlite3 test.db

Lastly, run the project itself:

    % node index.js
    Server is running.

Making an HTTP request to "localhost:3000/get?user-id=nnn" will result in a display of the created user's identifier, while "localhost:3000/put?user-name=xxx" will create one.

SEE ALSO
node(1), npm(1), ort(5)