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 asexport
. 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
(), ordb_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
(), ordb_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 asnull
, they may be passed asnull
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-nameddelete
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
The ort-nodejs
utility exits 0 on
success, and >0 if an error occurs.
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.