mobileFX | Architecture | Diagram
ASP.js brings Classic ASP back to life over Node.js … with some modern and innovative twists!
ASP.js is not confined to the mere mimesis of Classic ASP for reasons of legacy; it ascends beyond imitation. It reveals itself as a layered ecosystem, grounded in the archē of a database-first philosophy — where data is the primordial substance from which all logic flows. Upon this foundation rises a resilient full-stack application architecture, harmonized with cross-tier, end-to-end instruments of debugging, monitoring and security. In this way, the framework empowers the developer with the clarity, speed, and discipline required for the creation of data-driven business applications, uniting continuity with innovation into a single, coherent whole.
| Layer | Frameworks | Level |
|---|---|---|
| 🟩 Web Layer |
IISX / SITEX / HTTPX / ASPX Low-level web server runtime, emulating Classic ASP on Node.js. |
Low |
| 🟨 Data Access Layer |
SQLX Native dataset engine with SHAPE queries, pivots, and master–detail semantics. |
Mid-Low |
| 🟧 ORM Business Layer |
ORMX Modern ORM framework with field to property wrapping, and dynamic detail binding. |
Mid-High |
| 🟦 Application Layer |
Backstage Metadata-driven web framework that feels native, powered by Emscripten datasets. |
High |
At every layer you can choose how close to the metal — or how high-level — you want to work. And across them all, ASP.js delivers a robust end-to-end multi-tier Debugger. Step seamlessly from browser JavaScript → server logic → back to client. No other framework can do this. Unlike Angular, React, or Vue, which bury you in boilerplate, REST glue, and fractured tooling, ASP.js gives you one coherent stack where security, datasets, and UI are built-in.
Regardless of whether it fits your present needs or becomes part of your stack, take a moment to recognize the endeavor: an earnest effort to carry something old into the new, preserving its essence while opening paths to fresh creation.
Classic ASP Language Support
.asp and
.inc files.
<% … %> script blocks and
embedded JavaScript.
Ctrl+Click navigation for includes and server-side
references.
ASP Runtime Emulation
Application,
Session, Request,
Response, and Server objects.
Integrated Web Server & Site Manager
IISX.js) hosting
multiple sites on one root domain.
BANX.js), and rate limiting
(RATEX.js).
State-of-the-Art Debugging
Customizable Debugger Settings
Project & Certificate Management
launch.json and project layout for
immediate debugging.
mobileFX | Architecture | Diagram
The extension enhances Visual Studio Code by:
.asp, .inc, .jspp, and
.jobj files.
workspaceContains
triggers for ASP files).
Requirements
You can create a production-level ASP host in seconds! From the
Command Palette select New ASP Project command and
provide the top-level domain name of your project.
The wizard will generate the entire project structure, including a
mock CA root authority and self-signed SSL certificates for your
tests. During project initialization the extension will copy ASP.js
Runtime package, and a free version of SQLX, the RDBMS Runtime package
for APS.js that offers read-only database access. When the project is
created, simply select Debug ASP an start debugging!
Table of Contents
EventItem)
Write() (required
fields, formats, FK sanity)
Contents
The ASP Emulation Framework is a Node.js-based platform that recreates and extends the classic ASP programming model, while embedding modern web standards, security practices, and developer productivity features. This guide is written for developers, system architects, and researchers who seek both theoretical grounding and practical know-how. It emphasizes scientific validity, presenting each module with its objectives and innovations, and demonstrates how the framework unifies hosting, runtime, ORM, and MMV layers into a cohesive whole.
Request,
Response, Session,
Application) with
Proxy-based dictionaries.
The framework is designed in tiered architecture, with clear separation of concerns:
Hosting & Site Management
Security & Compliance
ASP Runtime
Data Access & ORM
Utilities
Backstage Framework
Monitoring & Analytics
Developer Experience
At each tier, the framework introduces innovations that go beyond legacy ASP or conventional Node.js stacks, resulting in a platform that is simultaneously familiar, secure, and future-ready.
Contents
Objectives
SNICallback.
HTTP Proxy & Routing Model
Host and selects the correct
site context; TLS certificates are chosen
dynamically via SNICallback, enabling wildcard/per-host certs and per-site TLS settings.
Site runs
an internal Node HTTP server on a private port
(assigned automatically if not specified).
SiteManager creates this server and wires it to the
site’s WebServer handler.
target: 127.0.0.1:<sitePort>). Forwarded requests include x-forwarded-for,
x-forwarded-proto, and normalized
host headers to preserve client identity and scheme.
Errors are handled gracefully (500 fallback), and request bodies are
streamed (including buffered POST).
SiteManager can register Proxy hosts
mapping a public hostname to another internal port, applying the
same forwarding headers and default path rewrites when configured.
TLS & Certificate Handling
SNICallback. For each
hostname, SiteManager constructs a
tls.createSecureContext(...) from the site’s SSL
options (min/max TLS versions and chain material). This allows
SAN/wildcard deployments and fine-grained settings
per site.
Early-Listener Security (pre-application)
Host or bare-IP hosts are treated as
violations and reported to BlockManager (reason
INVALID_HOST), then short-circuited with 400/403.
SiteManager subscribes to
WebServer’s block events
(emitted on policy violations like RATE_LIMIT,
METHOD_NOT_ALLOWED, etc.) and forwards them to
BlockManager (AddViolation), enabling
centralized, reason-based autoban strategies
(zero-tolerance and rate-limit bans).
IISX.js for
consistency across all sites.
Operational Resilience & DX
uncaughtException / unhandledRejection log
to crash.log to avoid silent failures.
SiteManager logs each
host → internal port routing to aid troubleshooting
and ops visibility.
💡 Why It Matters
IISX.jscentralizes TLS, routing, and edge security for every hosted site. By performing host/ban checks and flood detection at the listener, it reduces load on application layers and provides a single place to enforce organizational security posture.
Objectives
Site defined in SITEX.js, handling every
incoming HTTP(S) request.
Innovations
ASP-Aware Request Pipeline
.asp pages are compiled into JavaScript modules and
executed via ASPRuntime (from
ASPX.js).
HTTPX.js orchestrates request flow and policy
enforcement, then invokes the runtime for execution.
Integrated Security Enforcement (per-site)
/.git/, .. traversal).
Web Application Firewall (WAF) Features
Performance & Streaming
SITEX.js.
Session & Authentication Hooks
SITEX level.
Developer Productivity
ERRX.js exceptions →
logs + DB.
Request.Form, Response.Cookies).
💡Why It Matters >
HTTPX.jstransforms a plain Node.js HTTP server into a full ASP-style web server with enterprise-grade security and modern web capabilities (CORS, CSP, streaming).
It is the execution layer that turns per-site definitions (SITEX.js) into real runtime behavior, ensuring every request is both safe and efficient.
Objectives
HTTPX.js instance enforces
security.
Innovations
Per-Site Security Container:
Each
Site object is not just a server wrapper but a
security contract. HTTPX.js consults the site
definition to enforce CSP, CORS, CSRF, and input filtering rules.
CORS (Cross-Origin Resource Sharing):
Fine-grained per-site CORS policies:
GET, POST,
etc.)
CSP (Content Security Policy):
Each site can
define strict CSP rules to mitigate XSS and injection:
Encryption Layer:
Whitelisting & Blacklisting:
.exe,
.asp by default, etc.).
Anti-Abuse Controls:
WebAuthn Integration:
Full integration with
@simplewebauthn/server, enabling
passwordless authentication at the site level,
with challenge/response state stored per-site.
Proxy Abstraction:
Proxy objects describe upstream targets.Developer Productivity:
💡Why It Matters Where
IISX.jsis the multi-tenant orchestrator andHTTPX.jsis the execution engine,SITEX.jsis the policy brain. It centralizes all per-site rules so that developers can configure powerful security, CORS, CSP, encryption, and access controls without re-writing server logic.
Contents
Objectives
Innovations
Auto Ban Reasons
Standardized Auto Ban reasons as defined in BANX.js, forming the basis for ZeroTolerance and RateLimit strategies.
| Reason | Description |
|---|---|
| FLOOD_ATTACK | Excessive requests intended to overwhelm the service (DoS / DDoS flood). |
| ACCESS_DENIED | Attempt to access forbidden resources (unauthorized). |
| METHOD_NOT_ALLOWED |
Usage of disallowed HTTP methods (e.g., TRACE,
CONNECT).
|
| HIDDEN_PATH_ACCESS | Probing hidden or sensitive paths. |
| INVALID_HOST |
Requests with invalid or unrecognized
Host headers.
|
| INVALID_INPUT | Malformed or suspicious request input detected. |
| FRAUD_DETECTION | Fraud patterns detected (e.g., identity or payment fraud signals). |
| PHISHING | Activity indicative of phishing attempts. |
| EXTENSION_DENIED | Requests targeting blacklisted file extensions. |
| DIRECTORY_DENIED | Access attempts to forbidden directories. |
| DIRECTORY_TRAVERSAL |
Path traversal attempts (../) to break out of
root.
|
| DIRECTORY_INDEXING | Unauthorized directory listing attempts. |
| INVALID_SITE | Requests for invalid/unknown site in multi-host setups. |
| INVALID_URL | Malformed or illegal URL structures. |
| INVALID_MIME | Suspicious or disallowed MIME types. |
| INVALID_USER | Invalid or unauthorized user references. |
| RATE_LIMIT | Excessive request rate within a sliding window. |
| INVALID_CSRF | Invalid or missing CSRF tokens in sensitive operations. |
| INVALID_ENDPOINT | Access to endpoints not defined/allowed by the application. |
Strategy-Based Ban Engine
| Strategy | Trigger logic | Ban duration | Reset / forgiveness | Default reasons |
|---|---|---|---|---|
| ZeroTolerance |
Ban when
violations >= maxViolations (counter-based).
|
banDuration (often
Infinity for permanent).
|
Optional resetAfter per config (e.g., none for
permanent, 24h for medium severity).
|
High-severity set (e.g., FLOOD_ATTACK,
ACCESS_DENIED, METHOD_NOT_ALLOWED,
HIDDEN_PATH_ACCESS, INVALID_HOST,
INVALID_INPUT, FRAUD_DETECTION)
and medium sets (e.g., PHISHING,
EXTENSION_DENIED,
DIRECTORY_DENIED,
DIRECTORY_TRAVERSAL,
DIRECTORY_INDEXING, INVALID_SITE),
plus softer set (INVALID_URL,
INVALID_MIME, INVALID_USER).
|
| RateLimitBan (aka “TimeBan” alias maps here) |
Ban if requests within sliding
timeWindow exceed
maxRequests (timestamp windowed).
|
banDuration (e.g., 5 minutes). |
Typical resetAfter forgives burst after
inactivity (e.g., 10 minutes).
|
RATE_LIMIT, INVALID_CSRF,
INVALID_ENDPOINT.
|
IBanStrategy interface.
shouldBan) and
ban durations (temporary vs permanent).
Per-IP Violation Tracking
resetAfter)
so well-behaved clients recover over time.
Centralized Enforcement
BlockManager creates an
AutoBan record, stores it, and adds it to the
in-memory blocklist.
IISX.js), saving server resources.
Persistent Storage
SQLX.js /
ORMX.js).
Fine-Grained Ban Reasons
AUTOBAN_REASONS (e.g.,
FLOOD_ATTACK, INVALID_CSRF,
DIRECTORY_TRAVERSAL, PHISHING).
💡 Why It Matters
BANX.jsacts as the first line of defense against hostile traffic. By separating strategy definition, violation tracking, and ban enforcement, it achieves a balance of flexibility and robustness. Developers gain a security layer that adapts to both high-severity attacks (immediate bans) and low-and-slow abuses (rate-limited bans), while preserving forensic data for compliance and audits.
Objectives
Innovations
maxRequests, windowMs,
slidingWindow, cleanupInterval,
aligned with SITEX policies.
HTTPX.js early in the request
pipeline; emits block events with reason codes.
IISX.js subscribes to these events and forwards
violations to BANX.js for strategic autoban
escalation.
💡 Why It Matters
RATEX.jsis the first pressure valve in the stack: cheap, accurate throttling that shields application code from surges while feeding precise signals toBANX.js. This two-stage design (throttle → escalate) preserves good traffic, absorbs bursts gracefully, and clamps down on abuse only when warranted.
Objectives
Innovations (Defense-in-Depth)
Edge & Transport (IISX.js)
Per-Site Security Policy (SITEX.js)
Request Pipeline Enforcement (HTTPX.js)
Abuse Control & Escalation (RATEX.js + BANX.js)
FLOOD_ATTACK, INVALID_CSRF,
DIRECTORY_TRAVERSAL) enabling targeted responses.
Data & Input Hygiene (UTILX.js + ORMX/SQLX)
Observability, Audit, and Compliance (ERRX.js)
Configurability & Least Privilege
💡 Why It Matters
Security isn’t a bolt-on: it is embedded at every layer—from the listener to the data model. Early rejection of bad traffic preserves capacity; per-site policies reduce blast radius; rate-limit + autoban provide proportionate responses; and uniform logging produces audit-grade evidence. The result is a platform that is secure by default, observable, and adaptable to evolving threats.
Contents
The ASPX.js module is the heart of the framework: it
emulates the classic ASP runtime model on top of Node.js.
It
provides all the familiar ASP intrinsic objects (Request,
Response, Session, Application,
Server) while introducing a modern execution and
debugging context.
By combining JavaScript’s async model with ASP semantics, ASPX.js allows developers to run ASP pages compiled into pure JavaScript, while still coding against the objects and idioms of Classic ASP. This duality is further enhanced with a debugger integration that can seamlessly map between the compiled JavaScript lines and the original ASP source.
Objectives
Request object using Node’s HTTP
request as the foundation.
Form,
QueryString, Cookies,
ServerVariables) and request metadata.
Object Model & Lifecycle
new ASPRequest(site, req, ctx)).
Form,
QueryString, HashString,
Cookies, ServerVariables) through
proxy wrappers, enabling classic ASP-style
indexing.
TotalBytes,
BinaryRead, Body, and upload helpers
(FilesCount, File(i)).
Security & Enforcement
ASPFileUpload objects; others are rejected early.
HTTP_* projections, ensuring consistency for filtering
and logging.
Request.Block() integrates
directly with BANX/RATEX, marking abusive requests at the object
level.
ASPRuntime.ValidateCSRFToken.
Innovations Beyond Classic ASP
HashString) as
a first-class collection, extending Classic ASP’s request model for
modern single-page apps.
Differences vs Classic ASP
HashString collection (not present in Classic
ASP).
ServerVariables with HTTP_* names.
💡 Why It Matters
ASPRequestbridges raw Node.js HTTP with the emulated ASP world. It ensures that every input — from query strings to uploads — is normalized, validated, and secured before reaching page logic.
Developers get the familiar ergonomics of Classic ASP, but with modern enhancements like hash parsing, proactive upload filtering, and direct security hooks, making request handling both safer and more expressive.
Objectives
Response object on top of Node’s
ServerResponse.
Object Model & Lifecycle
new ASPResponse(ctx, res))
and attached to the ASPContext.
Write(), End(), Flush(),
Clear() for output control.
AddHeader(), RemoveHeader(),
Status, ContentType for HTTP control.
Cookies collection for per-request cookie setting.
Buffer and Charset properties for
output management.
Security & Enforcement
HttpOnly,
Secure, and SameSite attributes by
default, aligning with per-site TLS and CSRF requirements.
Content-Length) unless explicitly
allowed; blocks unsafe values.
Innovations Beyond Classic ASP
SITEX.js site definitions.
Response ensures uniform error presentation, enriched
by ERRX.js context.
Differences vs Classic ASP
Response left CSRF, CSP, and secure headers
entirely to the developer — here they are
framework-enforced.
Write() with modern async file streaming.
ERRX.js, ensuring
consistent logging and client feedback.
💡 Why It Matters
ASPResponsetransforms output from a simple write buffer into a security-enforcing boundary. Developers can use familiar ASP-style methods, but every response is automatically wrapped in modern protections — secure cookies, CSRF defense, strict headers. This duality makes the response both ergonomic for legacy ASP developers and robust for contemporary security requirements.
Objectives
Session with
predictable lifecycle and
per-user state across requests.
Object Model & Lifecycle
ASPRuntime.GetSession() under an
async mutex to serialize creation/lookup and avoid
races; SID resolved from cookie or minted as UUIDv4;
Session_OnStart/OnEnd from global.asa are
invoked.
m_Started,
m_LastVisit, Uptime,
IdleTime, ExpiresIn with human-readable
*String/*Ago accessors;
Update() refreshes last visit and initializes start.
IdleTime > Timeout or
JIT flag.
site.session_timeout_min) and converted internally.
Surface & Data Structures
Contents and
StaticObjects are ASPDictionary instances
exposed via proxies for ASP-style indexing.
AuditUser,
IPv4, UserAgent, and
mobile emulation toggle bound to site
(EmulateMobile).
CodePage,
LCID.
m_CSRFtoken) used by
ASPRuntime.ValidateCSRFToken and injected into forms by
ASPResponse.
Security & Cryptography
ENCRYPTION_KEY/IV).
Encrypt(string) →
base64url for URL/cookie safety;
Decrypt(string) reverses; robust error logging includes
session ID (not plaintext).
Operational Diagnostics & Jobs
CreateJob,
UpdateJob, GetJobStatus manage per-session
long-running tasks with progress/status/timestamps (running → done).
StartedAgo,
UptimeString, IdleTimeString,
ExpiresInString aid admin UIs and auditing.
Integration Points
ASPResponse.End() sets cookies for
SID and csrf_token with secure attributes;
forms written via Response.Write() auto-inject hidden
CSRF input.
ASPRuntime.ValidateCSRFToken() validates against
Session.CSRFToken and may signal BAN pipeline per site
policy.
EmulateMobile is bridged to site config for UI
adaptation.
Innovations Beyond Classic ASP
Differences vs Classic ASP
💡 Why It Matters
ASPSessionis more than a key–value bag: it’s a security-aware, observable, and operational nucleus for each user. Ephemeral per-session crypto, CSRF coupling, JIT mode, and built-in job tracking let apps coordinate long tasks and protect state without custom scaffolding. The result is lower ceremony, stronger guarantees, and clearer insight into session behavior at scale.
Objectives
Application object as a
global, per-site state container.
Application_OnStart,
Application_OnEnd) defined in global.asa.
Object Model & Lifecycle
ASPContext.
ASPDictionary internally, with a
proxy wrapper to allow ASP-style indexing
(Application("Key")).
Contents — main key–value store.StaticObjects — declared objects initialized at
startup.
Lock() / UnLock() — concurrency
control for multi-request mutation.
Application_OnStart executes once
when the site starts.
Application_OnEnd executes once
during shutdown.
Security & Isolation
Application object belongs to a single
SITEX.js site and is isolated from
others — no cross-site contamination in multi-tenant hosting.
Innovations Beyond Classic ASP
Application scope was tied to the entire IIS pool.
Here, it is strictly scoped to a single site definition in
SITEX.js.
Request, Session, and
ServerVariables, while keeping VBScript-style indexing.
Differences vs Classic ASP
Application was global to the entire
application pool; here it is explicitly per site.
💡 Why It Matters
ASPApplicationprovides a safe, per-site global memory space that Classic ASP developers expect, but redesigned for a multi-tenant, async Node.js world. It enables caching, counters, and configuration sh
Objectives
Server object as the
utility and environment façade inside the ASP
runtime.
ScriptTimeout),
safe object creation, encoding helpers,
virtual→physical path resolution, and facilities
for persisting generated scripts.
Object Model & Surface
ASPServer per
site/runtime; exposed to every page via the ASP execution context.
SQLX.Dataset) instead
of arbitrary COM; returns null on failure.
for spaces and numeric entities
for all other characters).
/scripts/<sha256>.js under the
site, using atomic temp-file rename and directory auto-creation.
Transfer(url) /
Execute(url) present as stubs (the processor handles
include/transfer semantics).
Design & Safety Properties
SaveScript uses
content hashing for idempotent filenames, creates
the /scripts folder if missing, writes to a
.tmp then renames to avoid partial writes, and returns
a stable public URL for client reuse (good for
caching/CDN).
HTMLEncode eliminates raw special characters in HTML
contexts; URLEncode aligns with modern URL rules to
avoid double-encoding pitfalls.
Innovations Beyond Classic ASP
MapPath: always resolves
relative to the site’s root, removing ambiguity and
reducing attack surface compared to machine-wide roots.
SQLX.Dataset), yielding portability and fewer privilege
risks.
Differences vs Classic ASP
Server.CreateObject could instantiate any registered
COM class; here it’s curated and portable (datasets
and framework objects).
MapPath semantics are stricter: always
bounded to the site’s path, with normalization safeguards.
Execute / Transfer behavior:
not performed directly by Server.*; the
processor handles page inclusion/transfer during
compilation/execution.
HTMLEncode emits
numeric entities broadly (including spaces as
), and URL encoding follows modern JavaScript
routines, which may differ from legacy IIS quirks.
💡 Why It Matters
ASPServeris the developer’s toolbelt inside ASP pages—now redesigned for security and portability. By constraining object creation, hardening path resolution, and offering content-addressed script persistence, it turns common tasks into safe, deterministic operations. The result is a familiar surface for Classic ASP developers that maps cleanly to modern Node.js practices without inheriting legacy risks.
Objectives
Architecture at a Glance
ASPContext per
request and wires Request, Response,
Session, Application, Server.
.asp.
global.asa hooks (Application_OnStart/End,
Session_OnStart/End) and per-request enter/leave
callbacks.
Request Lifecycle (high level)
HTTPX.js passes a
normalized request to ASPRuntime.
ASPRuntime creates
ASPContext and attaches intrinsics
(Request/Response/Session/Application/Server).
HTTPX.js).
ASPContext (sync/async allowed).
Compilation & Caching Pipeline
ASPContext.
Debugger & Mapping Integration
.asp, the debugger UI can show the
original ASP line.
Concurrency & Determinism
ASPContext per request; no sharing of mutable state
except via Application (guarded by locks).
Security Responsibilities
Session.CSRFToken; failure short-circuits the request
and can signal the autoban/rate-limit pipeline.
Response so tokens and secure cookie attributes
are consistently applied.
Error Handling & Observability
Exception (ERRX) enriched with URL, IP, user-agent, and
source mapping back to ASP line.
Performance Characteristics
Request.
Compatibility Notes vs Classic ASP
Server.CreateObject is
curated (framework objects) rather than arbitrary
COM activation.
Innovations Beyond Classic ASP
.asp.
💡 Why It Matters
ASPRuntimeis the brain of the framework: it preserves Classic ASP’s developer ergonomics while delivering modern guarantees—async safety, strong security defaults, and source-accurate debugging. By centralizing compilation, policy, and lifecycle, it makes page execution predictable, observable, and fast, turning legacy semantics into a production-grade Node.js runtime. The result is a platform where classic patterns and contemporary engineering co-exist without compromise.
Objectives
Request,
Response, Session,
Application, Server).
Object Model & Lifecycle
ASPRuntime at the start of every request.
Request (ASPRequest)Response (ASPResponse)Session (ASPSession, created or restored if SID
cookie present)
Application (ASPApplication, per site)Server (ASPServer, per site)ctx into the compiled ASP page function.
ctx is also registered in
globalThis.__asp_debug_ctx[ctxid] so the VSCode
debugger can resolve locals.
Responsibilities
executionContext proxy used in the
with(executionContext){…} wrapper so page code sees
Request, Response, etc. as globals.
async() toggle, finalization hooks, and context cleanup
after the response flush.
__asp_debug_ctx entries after a
short timer.
Innovations Beyond Classic ASP
Proxy over ctx +
globalThis ensures names resolve predictably and are
debugger-visible.
ctxid.
ctx.async()), a feature that never existed
in Classic ASP.
Differences vs Classic ASP
ctx and a proxy
wrapper.
💡 Why It Matters
ASPContextis the spinal cord of ASP.js: it carries every intrinsic, tracks per-request state, and enables source-accurate debugging.
By combining classic semantics with proxy-based scope control and async awareness, it ensures each request is isolated, debuggable, and secure, forming the foundation on which the ASP emulation rests.
Objectives
Error into a
richer Exception class suitable for
Classic ASP emulation and enterprise-grade diagnostics.
Response, tracked in sessions, and audited
by site policies.
Exception Model
Exception derives from
Error, adding structured fields:
Name, Message,
Stack (from Error)
Site, URL, Method,
UserAgent, IPv4 (request context)
InnerException (nesting support)Severity, Category,
Tags (classification for logging/alerting)
toString(), toJSON(),
logToFile(), logToDatabase().
.asp lines.
Integration Points
Exception and passed through
ERRX.
ASPX.js.
Innovations Beyond Classic ASP
Err object only exposed number/description. Here,
exceptions carry full context (URL, method, user identity).
InnerException).
Differences vs Classic ASP
Err.Number,
Err.Description, Err.Source. No structured
logging or stack traces.
💡 Why It Matters
ERRX.jsturns error handling into a predictable, structured, and observable part of the framework. Developers gain richer diagnostics than Classic ASP ever provided, with direct source mapping, contextual logging, and full lifecycle integration. The result: faster debugging in development and stronger audit/compliance guarantees in production.
Contents
EventItem)
Write() (required
fields, formats, FK sanity)
Objectives
Provide a native-performance database layer (Windows-only for now) with a Node.js bridge to compiled SQLX Data Access Engine.
Expose an ADO-like Dataset: cursor-based,
hierarchical recordsets, metadata-rich DataFields,
and master–detail relations.
Sample SQLX Dataset Code:
// Create an instance of the SQLX.Dataset object for database operations
const dataset = Server.CreateObject("SQLX.Dataset");
// Map the relative path to the SQLite database file to an absolute path
const database_path = Server.MapPath("../.data/database.db");
// Open the database and execute a SELECT query on the TEST table
if (dataset.open(database_path, "SELECT * FROM TEST"))
{
// Start generating an HTML table to display the data
Response.Write("<table class='DATAGRID' rules='all'>");
// Generate the table header
Response.Write("<thead>");
Response.Write("<tr>");
for (let i = 0; i < dataset.fieldCount; i++)
{
// Access field metadata by index
const f = dataset.fieldByIndex(i);
// Write the field name as a table header
Response.Write("<th nowrap>" + f.NAME + "</th>");
}
Response.Write("</tr>");
Response.Write("</thead>");
// Generate the table body, iterate through each record in the dataset
Response.Write("<tbody>");
while (!dataset.DB_EOF)
{
Response.Write("<tr>");
for (let i = 0; i < dataset.fieldCount; i++)
{
// Access the field value for the current row by index
const f = dataset.fieldByIndex(i);
// Write the field value as a table cell
Response.Write("<td nowrap>" + f.value + "</td>");
}
Response.Write("</tr>");
// Move to the next row in the dataset
dataset.moveNext();
}
Response.Write("</tbody>");
// Close the table
Response.Write("</table>");
}
Offer simple entry points for one-shot queries
and commands (Run, Exec) while enabling
advanced dataset behaviors (constraints, lookups, caching,
auditing).
SQLX Run and Exec methods:
/**
* Executes a SQL query and returns a result set.
*
* @param {string} SQL - The SQL query string to execute.
* @param {string} [ConnectionString] - Optional connection string; uses default if not provided.
* @returns {object} Dataset result set object.
* @throws {Exception} If the query fails to open.
*/
function Run(SQL, ConnectionString)
{
const rs = new SQLX.Dataset();
ConnectionString = SQLX.Database.NormalizeConnectionString(ConnectionString);
if (!rs.open(ConnectionString, SQL))
throw new Exception(`SQLX.Run failed ${rs.getLastError()}`);
return rs;
}
/**
* Executes a parameterized SQL command (e.g. INSERT, UPDATE, DELETE).
*
* @param {string} SQL - The SQL command string with parameter placeholders.
* @param {any[]} Params - Array of array of parameter values [ [a,b,c] ]
* @param {string} [ConnectionString] - Optional connection string; uses default if not provided.
* @throws {Exception} If the connection or execution fails.
*/
function Exec(SQL, Params, ConnectionString)
{
const rs = new SQLX.Dataset();
ConnectionString = SQLX.Database.NormalizeConnectionString(ConnectionString);
if (!rs.open(ConnectionString))
throw new Exception(`Exec failed ${rs.getLastError()}`);
if (!rs.exec(SQL, Params))
throw new Exception(`SQLX.Exec failed ${rs.getLastError()}`);
}
Centralize driver lifecycle (load, cache, backup) and error logging to the Exceptions table (no throws).
Architecture & Drivers
Native Extension Loader: SQLX.js normalizes
connection strings, selects the correct .node build
(CPU/Node ABI), and loads the native engine.
SQLX Native Extension Loader fragment:
/**
* Loads the platform-specific SQLX native module for Windows.
* Logs errors to console if loading fails.
*/
LoadNativeModule()
{
if (process.platform !== "win32")
{
console.error("SQLX native module is only available for Windows.");
return;
}
let arch = process.arch === 'ia32' ? 'x86' : process.arch;
const version = process.versions.node;
try
{
const filename = path.join(__dirname, '..', 'bin', `SQLX_v${version}_${arch}.node`);
this.m_native = require(filename);
}
catch (error)
{
console.error(`Failed to load SQLX native module for Node.js ${version} (${arch})`, error);
}
}
Driver Surface: Declares multiple drivers
(SQLite, MySQL, MSSQL,
Oracle, Firebase, OLEDBX)
but only SQLite is implemented at
present; others are stubbed for forward compatibility.
Driver Cache: Utilities for clearing and inspecting the cache (e.g., multiple connections/connection pooling reuse).
Backup: Provides a consistent backup routine for supported drivers (SQLite implemented).
Core Types (from the native API)
SQLX Native Node.js Dataset Class:
Dataset – the primary, cursor-based
recordset with ADO-like navigation and editing semantics.
export const SQLITE_COLUMN_FLAGS: {
IS_UNIQUE: number;
IS_KEYCOLUMN: number;
IS_SEARCHABLE: number;
IS_COMPUTED: number;
IS_AUTOINCREMENT: number;
IS_SORTABLE: number;
IS_WRITABLE: number;
IS_SEARCHABLE_LIKE: number;
IS_NULLABLE: number;
IS_ROWID: number;
IS_FIXED_LENGTH: number;
HAS_DEFAULT: number;
UI_HIDDEN: number;
UI_CLIENT: number;
UI_EDITABLE: number;
};
export const RECORD_STATUS: {
RECORD_NO_RECORD: number;
RECORD_UNCHANGED: number;
RECORD_CHANGED: number;
RECORD_INSERTED: number;
RECORD_DELETED: number;
RECORD_EDITABLE: number;
RECORD_ALL: number;
};
// Dataset Interface
export class Dataset
{
// --- Main methods ---
/** Returns the numeric driver identifier for the dataset */
getDriver(): number;
/** Sets the dataset codepage (character encoding) */
setCodePage(codepage: number): number;
/** Adds a new column definition to the dataset with extended metadata options */
AddColumn(
ordinal: number,
name: string,
type?: number,
precision?: number,
scale?: number,
length?: number,
flags?: number,
baseColumnName?: string,
baseTableName?: string,
id?: string,
lcid?: number,
size?: number,
declaredType?: string,
lookupTable?: string,
lookupIdColumn?: string,
lookupValueColumn?: string,
sqliteDataType?: number,
defaultValue?: string
): boolean;
/** Adds a new record (row) to the dataset */
AddRecord(values: any[] | any[][]): boolean;
/** Opens a schema from a connection string with a given schema type */
openSchema(connectionString: string, schema: number): boolean;
/** Creates a backup of the database from srcFile to dstFile */
backup(srcFile: string, dstFile: string): boolean;
/** Opens a dataset with optional SQL and metadata/shape information */
open(connectionString: string, sql?: string, metadataOnly?: boolean, shapeInfo?: string | object):
boolean;
/** Opens a shaped dataset (hierarchical data) with metadata options */
shape(connectionString: string, shapeInfo: string, metadataOnly?: boolean): boolean;
/** Returns a detail dataset (child table) by name */
detail(detailName: string): Dataset | null;
/** Reopens the dataset using the original parameters */
reopen(): boolean;
/** Executes a SELECT SQL statement and loads results */
select(sql: string): boolean;
/** Executes an SQL statement with optional parameter values */
exec(sql: string, params?: any[]): boolean;
/** Re-queries the dataset, optionally preserving the bookmark position */
requery(useBookmark?: boolean): boolean;
/** Sorts dataset records by field (name or index), ascending or descending */
sort(field?: string | number, ascending?: boolean): boolean;
/** Closes the dataset */
close(): boolean;
/** Creates an audit table for change tracking */
CreateAuditTable(connectionString?: string, auditTable?: string): boolean;
/** Creates triggers for auditing multiple tables */
CreateAuditTriggers(tableNames: string[]): boolean;
/** Adds an index on given fields */
addIndex(fields: string): boolean;
/** Removes an index by name */
removeIndex(indexName: string): boolean;
/** Rebuilds indexes in the dataset */
reindex(): boolean;
/** Locates a record by field name and value */
locate(fieldName: string, fieldValue: string | number): boolean;
// --- Record navigation ---
/** Moves cursor to a specific record index */
move(recordIndex: number): boolean;
/** Moves cursor to the first editable record */
moveFirst(): boolean;
/** Moves cursor to the last editable record */
moveLast(): boolean;
/** Moves cursor to the next editable record */
moveNext(): boolean;
/** Moves cursor to the previous editable record */
movePrev(): boolean;
/** Returns true if the dataset cursor has moved */
moved(): boolean;
/** Moves cursor to the first record (editable or deleted) */
first(): boolean;
/** Moves cursor to the last record (editable or deleted) */
last(): boolean;
/** Moves cursor to the next record (editable or deleted) */
next(): boolean;
// --- Record status ---
/** Returns current record status */
status(): number;
// --- Field access ---
/** Returns a DataField object by index or name */
field(indexOrName: number | string): DataField;
/** Returns a DataField object by numeric index */
fieldByIndex(index: number): DataField;
/** Returns a DataField object by field name */
fieldByName(name: string): DataField;
/** Gets the raw string value for a record/field */
getValue(recordIndex: number, fieldIndex: number): string;
/** Gets a typed value with optional conversions for dates, decimals, or JSON */
getTypedValue(recordIndex: number, fieldIndex: number, returnDates?: boolean, returnDecimalsAsStrings?: boolean, returnParsedJSON?: boolean): any;
/** Updates a field value in a record */
putValue(recordIndex: number, fieldIndex: number, value: any): boolean;
// --- Record operations ---
/** Returns lookup constraints for a column */
getLookupConstraints(ordinal: number, type?: string): object;
/** Appends a new record */
insertRecord(): boolean;
/** Deletes the current record */
deleteRecord(): number;
/** Deletes multiple records by index */
deleteRecords(indexes: number[]): boolean;
/** Deletes records matching a field value, returns the number of records deleted */
deleteRecordsByValue(fieldName: string, fieldValue: any): number;
/** Applies updates to the underlying storage */
update(): boolean;
/** Normalizes dataset by removing deleted records and clearing changed flags on rest */
normalize(): boolean;
/** Invalidates cached lookup values */
invalidateLookupCache(): void;
// --- Transactions ---
/** Begins a transaction */
transactionBegin(): boolean;
/** Commits the current transaction */
transactionCommit(): boolean;
/** Rolls back transaction to a given savepoint */
transactionRollback(savepoint: string): boolean;
// --- Bookmarking ---
/** Returns a bookmark buffer for the current record */
getBookmark(): ArrayBuffer;
/** Restores dataset position using a bookmark */
setBookmark(buffer: string | ArrayBuffer): boolean;
/** Returns true if current record has been modified */
modified(): boolean;
// --- Diagnostics ---
/** Returns the last error message */
getLastError(): string;
// --- Record counts / matrix ---
/** Returns total number of records, optionally filtered by status */
recordCount(status?: number): number;
/** Maps a row index to a record index given optional status
* It is intented to map editable records to visible datagrid rows
*/
row_to_record(rowIndex: number, status?: number): number;
/** Gets a value by row/column matrix position */
getMatrix(row: number, col: number): string;
/** Updates a value in the row/column matrix */
putMatrix(row: number, col: number, value: string): boolean;
// --- Import/Export ---
/** Loads dataset contents from a Base64 string */
loadBase64(base64String: string): boolean;
/** Exports dataset to Base64 encoded buffer */
toBase64(): ArrayBuffer;
/** Loads dataset contents from a byte buffer */
loadBytes(buffer: ArrayBuffer): boolean;
/** Exports dataset to raw bytes */
toBytes(): ArrayBuffer;
/** Exports dataset to ArrayBuffer */
toArrayBuffer(): ArrayBuffer;
/** Opens dataset directly from ArrayBuffer */
openFromArrayBuffer(buffer: ArrayBuffer): boolean;
// --- Special ---
/** Applies lookup filters to a related dataset */
setLookupFilters(datasetName: string, filters: object): void;
// --- Properties ---
/** Number of fields in the dataset */
readonly fieldCount: number;
/** True if positioned before the first record */
readonly DB_BOF: boolean;
/** True if positioned after the last record */
readonly DB_EOF: boolean;
/** Current record index */
readonly recordIndex: number;
/** If true, updates changed records inside transactions */
UpdateInTransaction: boolean;
/** If true, dataset is requeried automatically on update */
RequeryOnUpdate: boolean;
/** Enables lookup constraints based on metadata */
EnableMetadataLookupConstraints: boolean;
/** Enables caching of lookup values */
EnableLookupCache: boolean;
/** Arbitrary user signature stored with operations */
UserSignature: string;
/** Name of the audit table used for tracking changes */
AuditTable: string;
/** User identifier used in auditing */
AuditUser: string;
/** Placeholder string used for NULL values */
StringForNull: string;
/** If true, date fields return native Date values */
ReturnDateValues: boolean;
/** If true, decimals are returned as strings */
ReturnDecimalsAsStrings: boolean;
/** If true, JSON fields are parsed into objects */
ReturnParsedJSON: boolean;
}
Record Navigation: move-first/last,
next/prev; RecordCount, EOF/BOF.
Sample Code with Dataset cursor navigation:
// Create the native SQLX Dataset
const ds = new SQLX.Dataset();
ds.stringForNull = 'null';
ds.setCodePage(CP_UTF8);
ds.EnableMetadataLookupConstraints = false;
// Open a Database connection
if (ds.open(ConnectionString, SQL))
{
// Move to first record
ds.moveFirst();
// Loop on all records
while (!ds.DB_EOF)
{
// Load filed by name
const id = ds.fieldByName('id').value;
const value = ds.fieldByName('value').value;
// Load all other field values
for (let i = 0; i < ds.fieldCount; i++)
{
const field = ds.field(i);
const name = field.NAME.toLowerCase();
}
// Move cursor
ds.moveNext();
}
}
Editing Model: add/edit/delete;
RecordStatus flags (unchanged, added, modified,
deleted); UpdateInTransaction,
RequeryOnUpdate.
Sample Code with Dataset CRUD operations:
const dataset = new SQLX.Dataset();
dataset.open(ConnectionString, SQL);
dataset.insertRecord();
dataset.fieldByName(originalFieldName).value = newValue;
dataset.moveLast();
dataset.delete();
Hierarchies: master–detail datasets (SHAPE-like) with joined detail sets and foreign-key constraints driven by metadata.
SHAPE SQL command Syntax:
SHAPE
(
SELECT * FROM [Users] WHERE [WHERE]; -- WHERE [WHERE] and semicolon are required
)
AS [Users]
APPEND
(
( SELECT * FROM [UserCredentials] WHERE [WHERE]; ) -- WHERE [WHERE] and semicolon are required
RELATE [UserCredentials].[UserID] TO [Users].[UserID]
AS [UserCredentials]
);
Using SHAPE SQL with SQLX Datasets:
const SQL = `SHAPE (...);`;
const master = new SQLX.Dataset();
// read ALL master and ALL detail records
master.open(ConnectionString, SQL);
for (master.moveFirst(); !master.DB_EOF; master.moveNext()) {
const userId = master.fieldByName('UserID').intValue;
const userName = master.fieldByName('UserName').value;
// obtain the detail dataset by its SHAPE alias
const detail = master.detail('[UserCredentials]');
// iterate detail record for the current master record
for (detail.moveFirst(); !detail.DB_EOF; detail.moveNext()) {
const credId = detail.fieldByName('CredentialID').intValue;
const provider = detail.fieldByName('Provider').value;
const hash = detail.fieldByName('PasswordHash').value;
console.log(`[${userId}:${userName}] -> cred ${credId} @ ${provider} (${hash.slice(0, 8)}...)`);
}
}
Metadata-Rich Fields: each column is a
DataField with type, size/length, precision/scale,
flags, constraints, UI hints (format, validation,
control type, CSS), and
lookup descriptors (lookup table/id/value).
Native Node.js DataField Class:
// DataField Interface
export class DataField
{
value: string | number | boolean | Date | object | null; // Current field value, varies by DB type
lookupValue: string | null; // Display value from lookup table (if any)
readonly isNull: boolean; // True if the field is NULL
readonly intValue: number; // Value coerced to signed integer
readonly uintValue: number; // Value coerced to unsigned integer
readonly floatValue: number; // Value coerced to float (single precision)
readonly doubleValue: number; // Value coerced to double (double precision)
readonly boolValue: boolean; // Value coerced to boolean
readonly ORDINAL: number; // Column ordinal position in dataset
readonly ID: string; // Unique metadata identifier
readonly NAME: string; // Column name as defined in query/schema
readonly BASECATALOGNAME: string; // Originating catalog name (if provided)
readonly BASESCHEMANAME: string; // Originating schema name
readonly BASETABLENAME: string; // Originating table name
readonly BASECOLUMNNAME: string; // Originating column name in base table
readonly TYPE: number; // SQLX_VARTYPE enum (internal variable type)
readonly SQLX_DATATYPE: number; // SQLite internal type code
readonly DECLARED_TYPE: string; // Declared SQL type (TEXT, INT, etc.)
readonly LCID: number; // Locale identifier for collation
readonly PRECISION: number; // Numeric precision (total digits)
readonly SCALE: number; // Numeric scale (digits after decimal)
readonly SIZE: number; // Storage size in bytes
readonly LENGTH: number; // Maximum length of string/binary data
readonly FLAGS: number; // Column flags (nullable, PK, etc.)
readonly DEFAULTVALUE: string; // Default value expression (if defined)
readonly CHECK_CONSTRAINTS: string; // SQL CHECK constraint(s) definition
readonly UI_HINT: string; // UI rendering hint (e.g., “multiline”)
readonly UI_FORMAT: string; // Display format string (e.g., “dd/MM/yyyy”)
readonly UI_VALIDATION: string; // Validation rules/regex for UI
readonly UI_CONTROL: string; // Suggested UI control type (textbox, select)
readonly UI_CSS: string; // CSS class hints for UI styling
readonly LOOKUP_TABLE: string; // Related lookup table name
readonly LOOKUP_ID_COLUMN: string; // Column holding lookup key
readonly LOOKUP_VALUE_COLUMN: string; // Column holding lookup display text
}
Lookup Resolution: optional metadata-driven lookups (FK → caption/value) with an enable/disable switch and lookup cache (per-dataset) for performance.
⚡For defining Lookups in SQLite databases please download and use mobileFX SQLite Express.
Audit & Identity: AuditTable,
AuditUser, UserSignature surface
audit-friendly writes and change attribution.
this.dataset = new Dataset;
this.dataset.UserSignature = Session.SessionID; // User-defined Data Integrity Tail
this.dataset.AuditUser = Session.AuditUser; // User-defined Data Access Tag
Return Modes: ReturnDateValues (Date
objects), ReturnDecimalsAsStrings,
ReturnParsedJSON for typed or pre-parsed values.
String Nulls: StringForNull to
standardize UI rendering of DB nulls.
Constraints:
EnableMetadataLookupConstraints enforces foreign-key
validations from metadata before committing edits.
DataField – typed field descriptor
bound to a dataset column.
UI_HINT,
UI_FORMAT, UI_VALIDATION,
UI_CONTROL, UI_CSS.
LOOKUP_TABLE,
LOOKUP_ID_COLUMN,
LOOKUP_VALUE_COLUMN enabling FK captioning.
Datastream – streaming transport for
datasets (used by Backstage).
Enums & Flags: DriverType,
RecordStatus, ColumnFlags (readonly,
identity, computed, nullable, etc.).
Convenience Layer
Run(sql, [conn]) – Execute a query and
return a Dataset ready for navigation (single
resultset).
Exec(sql, params, [conn]) – Execute
commands (INSERT/UPDATE/DELETE), return affected counts/keys.
Exception facility into the
Exceptions table and do not throw;
callers should check return status where applicable.
Editing & Commit Semantics
UpdateInTransaction) and
post-commit requery (RequeryOnUpdate)
keep the dataset synchronized.
Interoperability with ORMX.js
Dataset directly for
reporting, data-entry forms, and hierarchical views;
use ORMX for domain-model code and business rules.
Limitations & Current Scope
💡 Why It Matters
SQLX.jsdelivers an ADO-like developer experience—hierarchical, cursor-based datasets with rich metadata—on a modern Node.js stack. It pairs UI-aware fields, lookup resolution, constraints, and auditing with a fast native engine, letting you build data-heavy screens and workflows without hand-rolling boilerplate. By separating the dataset engine (SQLX) from the entity layer (ORMX), the framework keeps performance predictable while giving you a clean path to higher-level modeling.
The native SQLX engine implements an ADO-inspired Dataset with rich DataField metadata and a high-performance SQLite driver. It provides cursor navigation, in-place editing, master–detail hierarchies, UI/validation hints on columns, metadata-driven lookups, a pivot virtual table with SQL macros, and zero-ceremony auditing.
close(); no leaked
handles.
UpdateInTransaction wraps
multi-record changes atomically.
Cursor & editing surface (selected)
BOF/EOF, RecordCount,
MoveFirst/Last/Next/Prev.
AddNew, Edit,
Delete, field assignment through
DataFields.
RecordStatus flags (Unchanged,
Added, Modified,
Deleted).
RequeryOnUpdate (post-commit sync),
Requery (manual).
Hierarchies
Each column is a DataField with
database facts + UI semantics:
Type normalization
SQL types normalized to a compact
SQLX_VARTYPE (integers, reals, currency,
date/time, text, blob, JSON,
HTML5, ENCRYPTED,
ENUMFLAGS, etc.).
SQLX Data Types:
// Compatibility with ADO
const SQLX_DBTYPEENUM =
{
SQLX_DBTYPE_EMPTY: 0,
SQLX_DBTYPE_NULL: 1,
SQLX_DBTYPE_I2: 2,
SQLX_DBTYPE_I4: 3,
SQLX_DBTYPE_R4: 4,
SQLX_DBTYPE_R8: 5,
SQLX_DBTYPE_CY: 6,
SQLX_DBTYPE_DATE: 7,
SQLX_DBTYPE_BSTR: 8,
SQLX_DBTYPE_IDISPATCH: 9,
SQLX_DBTYPE_ERROR: 10,
SQLX_DBTYPE_BOOL: 11,
SQLX_DBTYPE_VARIANT: 12,
SQLX_DBTYPE_IUNKNOWN: 13,
SQLX_DBTYPE_DECIMAL: 14,
SQLX_DBTYPE_UI1: 17,
SQLX_DBTYPE_ARRAY: 0x2000,
SQLX_DBTYPE_BYREF: 0x4000,
SQLX_DBTYPE_I1: 16,
SQLX_DBTYPE_UI2: 18,
SQLX_DBTYPE_UI4: 19,
SQLX_DBTYPE_I8: 20,
SQLX_DBTYPE_UI8: 21,
SQLX_DBTYPE_GUID: 72,
SQLX_DBTYPE_VECTOR: 0x1000,
SQLX_DBTYPE_RESERVED: 0x8000,
SQLX_DBTYPE_BYTES: 128,
SQLX_DBTYPE_STR: 129,
SQLX_DBTYPE_WSTR: 130,
SQLX_DBTYPE_NUMERIC: 131,
SQLX_DBTYPE_UDT: 132,
SQLX_DBTYPE_DBDATE: 133,
SQLX_DBTYPE_DBTIME: 134,
SQLX_DBTYPE_DBTIMESTAMP: 135,
SQLX_DBTYPE_HTML5: 136, // Special
SQLX_DBTYPE_JSON: 137, // Special
SQLX_DBTYPE_ENUMFLAGS: 148, // Special
SQLX_DBTYPE_ENCRYPTED: 149, // Special
};
// SQLX Vartypes
const SQLX_VARTYPE =
{
SQLX_VARTYPE_I1: SQLX_DBTYPEENUM.SQLX_DBTYPE_I1,
SQLX_VARTYPE_I2: SQLX_DBTYPEENUM.SQLX_DBTYPE_I2,
SQLX_VARTYPE_I4: SQLX_DBTYPEENUM.SQLX_DBTYPE_I4,
SQLX_VARTYPE_I8: SQLX_DBTYPEENUM.SQLX_DBTYPE_I8,
SQLX_VARTYPE_UI1: SQLX_DBTYPEENUM.SQLX_DBTYPE_UI1,
SQLX_VARTYPE_UI2: SQLX_DBTYPEENUM.SQLX_DBTYPE_UI2,
SQLX_VARTYPE_UI4: SQLX_DBTYPEENUM.SQLX_DBTYPE_UI4,
SQLX_VARTYPE_UI8: SQLX_DBTYPEENUM.SQLX_DBTYPE_UI8,
SQLX_VARTYPE_R4: SQLX_DBTYPEENUM.SQLX_DBTYPE_R4,
SQLX_VARTYPE_R8: SQLX_DBTYPEENUM.SQLX_DBTYPE_R8,
SQLX_VARTYPE_CY: SQLX_DBTYPEENUM.SQLX_DBTYPE_CY,
SQLX_VARTYPE_DECIMAL: SQLX_DBTYPEENUM.SQLX_DBTYPE_DECIMAL,
SQLX_VARTYPE_BOOL: SQLX_DBTYPEENUM.SQLX_DBTYPE_BOOL,
SQLX_VARTYPE_DATE: SQLX_DBTYPEENUM.SQLX_DBTYPE_DBDATE,
SQLX_VARTYPE_TIME: SQLX_DBTYPEENUM.SQLX_DBTYPE_DBTIME,
SQLX_VARTYPE_DATETIME: SQLX_DBTYPEENUM.SQLX_DBTYPE_DATE,
SQLX_VARTYPE_TIMESTAMP: SQLX_DBTYPEENUM.SQLX_DBTYPE_DBTIMESTAMP,
SQLX_VARTYPE_BLOB: SQLX_DBTYPEENUM.SQLX_DBTYPE_BYTES,
SQLX_VARTYPE_HTML5: SQLX_DBTYPEENUM.SQLX_DBTYPE_HTML5,
SQLX_VARTYPE_JSON: SQLX_DBTYPEENUM.SQLX_DBTYPE_JSON,
SQLX_VARTYPE_ENUMFLAGS: SQLX_DBTYPEENUM.SQLX_DBTYPE_ENUMFLAGS,
SQLX_VARTYPE_ENCRYPTED: SQLX_DBTYPEENUM.SQLX_DBTYPE_ENCRYPTED,
SQLX_VARTYPE_STRING: SQLX_DBTYPEENUM.SQLX_DBTYPE_WSTR,
};
Precision/scale/length carried alongside the normalized type.
Storage class recorded (SQLite: INTEGER,
REAL, TEXT, BLOB,
NULL).
Constraints & flags
UI hints
UI_HINT, UI_FORMAT,
UI_VALIDATION, UI_CONTROL,
UI_CSS → drive auto-form generation, formatting,
and validation in Backstage or custom UIs.
Lookup descriptors
LOOKUP_TABLE, LOOKUP_ID_COLUMN,
LOOKUP_VALUE_COLUMN (+ optional filters).
Connection lifecycle
Context UDFs
CURRENT_USER() bound to the
current dataset for auditing/attribution.
Encoding & pragmas
Transactions
Local (id,value) cache per field
Forward resolution (id → caption)
Reverse resolution (caption → id)
Commit-time checks
EnableMetadataLookupConstraints is on,
unresolved FKs block commit.
One-liner pivot in SQL
FROM PIVOT(Source, RowKey, PivotKey, Value) in
statements.
memdb.pivot backed
by a custom VTab implementation for the duration of the query.
Column synthesis
PivotKey values become
columns; Value cells are
aggregated row-wise.
Projection macros
PIVOT_COLUMNS(alias.* [, Ex...]) → expands to the
generated pivot columns.
PIVOT_SUM/AVG/MIN/MAX/COUNT/COUNT_NONNULL/COUNT_POSITIVE(alias.*
[, Ex...])
→ row aggregations over the synthesized columns.
Contracts & limits
PIVOT(...) per statement.LIMIT 0 to keep discovery
cheap.
SQLX SQLite Driver PIVOT SQL Syntax:
-- PIVOT(SourceTable, GroupByColumn, PivotColumn, AggregationColumn or 1 for count)
WITH P AS (
SELECT * FROM PIVOT(CampaignResults, CampaignID, Value, 1)
)
SELECT
C.[Name],
PIVOT_COLUMNS(P.*, CampaignID),
PIVOT_SUM(P.*, CampaignID, [no]) AS [Positive],
PIVOT_SUM(P.*, CampaignID) AS [Total]
FROM P
JOIN Campaigns C ON C.[ID] = P.[CampaignID];
Actual post-processed SQL executed by SQLite:
-- Create memory database and run custom SQLite PIVOT_ENGINE
ATTACH ':memory:' AS memdb;
CREATE VIRTUAL TABLE memdb.pivot USING PIVOT_ENGINE(
CampaignResults,
CampaignID,
Value,
1
);
WITH P AS (
SELECT * FROM memdb.pivot
)
SELECT
C.[Name],
-- PIVOT_COLUMNS(P.*, CampaignID) => select all pivot columns except CampaignID
[P].[yes], [P].[no], [P].[maybe],
-- PIVOT_SUM(P.*, CampaignID, [no]) AS [Positive] => Sum all pivot columns except CampaignID and [no]
(COALESCE([P].[yes],0) + COALESCE([P].[maybe],0)) AS [Positive],
-- PIVOT_SUM(P.*, CampaignID) AS [Total] => Sum all pivot columns except CampaignID
(COALESCE([P].[yes],0) + COALESCE([P].[no],0) + COALESCE([P].[maybe],0)) AS [Total]
FROM P
JOIN Campaigns C ON C.[ID] = P.[CampaignID];
-- Epilogue
DROP TABLE IF EXISTS memdb.pivot;
DETACH memdb;
Audit table synthesis
AuditLog with:
AuditID, Date, AuditUser,
Operation, TableName,
PrimaryKey, Changes.
Trigger generation
INSERT/UPDATE/DELETE triggers per target
table.
User stamping
CURRENT_USER() from the driver to
attribute changes to the active dataset/user.
Operations coverage
UpdateInTransaction; failures roll back
atomically.
💡 Why It Matters
SQLX’s native core combines strict metadata, smart caches, and SQLite extensions (UDFs, VTabs) to deliver a dataset that’s both UI-aware and transaction-safe. Lookups render foreign keys as human-readable values without extra queries; pivots become a single SQL construct; auditing is synthesized with readable diffs and user attribution. The result is a high-leverage data layer that preserves ADO ergonomics while exploiting modern SQLite capabilities.
ORMBase lifts SQLX datasets into ergonomic JavaScript
objects using three core mechanisms:
An instantiated ORM object has a dual nature: it
represents the current record view and it is also a
generator usable in for…of to stream
records.
ORM Master-Detail Business Object Sample:
class User extends SQLX.ORMBase
{
constructor()
{
super();
this.meta.Class = User;
this.meta.TableName = '[Users]';
this.meta.ID = '[Users].[UserID]';
this.meta.Limit = 0;
this.AddDetail(UserCredentials,
'[UserCredentials]',
'[UserCredentials].[ID]',
[],
'[Users].[UserID]',
'[UserCredentials].[UserID]');
this.Meta();
}
get HasCredentials()
{
for (const cred of this.UserCredentials)
return true;
return false;
}
InsertUserCredential({ UserID, CredentialID, PublicKey, SignCount, Transports })
{
const cred = new UserCredentials();
cred.Insert();
cred.UserID = UserID;
cred.CredentialID = CredentialID;
cred.PublicKey = PublicKey;
cred.SignCount = SignCount;
cred.Transports = Transports;
cred.Write();
}
};
ORM objects serialize their bound SQLX datasets into a binary Datastream for fast round-trips. This keeps metadata, rows, state (insert/update/delete), and constraints in one compact payload.
On the client-side (browser), we use an EMSCRIPTEN port of the Node.js Native Dataset. Thus, we can rely on same Dataset API on both the server and the client tiers.
// Client performs GET/POST to ModelData,
// the function returns a binary Datastream with the requested model
async function ModelData() {
const { Arguments, Filters: rawFilters = [] } = Request.Body
? JSON.parse(Request.Body)
: {}
const Filters = rawFilters.map((f) => new SQLX.FILTER(f))
const modelId = Request.ServerVariables('HTTP_X_SQLX_MODEL_ID')
const property = Request.ServerVariables('HTTP_X_SQLX_MODEL_PROPERTY')
let model = new MyApp.CreateModel(modelId, Arguments)
if (Filters.length) model.meta.Filters = [...model.meta.Filters, ...Filters]
// If model set length is small, prefetch
if (model.Length <= 1) await model.Read()
// Resolve nested property if requested (e.g. master-detail)
if (property && property !== 'undefined')
model = property.split('.').reduce((o, k) => o?.[k], model)
// Serialize dataset (data + metadata + state)
Response.ContentType = 'application/octet-stream'
Response.BinaryWrite(model.dataset.toBytes())
Response.End()
}
// Client performs POST to ModelUpdate,
// accepts a binary Datastream with changes;
// applies constraints and returns requery
async function ModelUpdate() {
const ab = Request.BinaryRead() // ArrayBuffer
const dataset = Server.CreateObject('SQLX.Dataset')
dataset.UserSignature = Session.SessionID
dataset.AuditUser = Session.AuditUser
if (!dataset.loadBytes(ab)) throw new Error('Invalid DataStream')
dataset.RequeryOnUpdate = true // return fresh rows after update
dataset.EnableMetadataLookupConstraints = true // enforce FK, unique, ranges, etc.
if (!(await dataset.update())) throw new Error(dataset.getLastError())
// Send updated snapshot
Response.ContentType = 'application/octet-stream'
Response.BinaryWrite(dataset.toBytes())
Response.End()
}
// Browse: POST to Backstage,
// Receive binary Datastream,
// Write Datastream to Emscripten Dataset,
// Refresh UI bindings.
async function Browse(dataset, options = {}) {
dataset.FetchOptions = options // remember last fetch options
try {
// Build URL + search params (e.g. paging, search form fields)
const url = new URL(
`${this.options.URL}?method=${options.method || 'data'}`, window.location.origin
)
if (options.search)
for (const [k, v] of Object.entries(options.search))
url.searchParams.append(k, v)
// Pass model/property via headers to the Backstage endpoint
const headers = {
'Content-Type': 'application/json',
'x-csrf-token': this.options.CSRF,
'x-sqlx-model-id': options.modelId, // e.g. 'Reservation'
'x-sqlx-model-property': options.property || '', // e.g. 'Details.Payments'
...options.headers
}
// Optional server-side arguments/filters
const body = JSON.stringify({
Arguments: options.arguments || {},
Filters: options.filters || [] // client-proposed filters (server still enforces)
})
const res = await fetch(url.toString(), { method: 'POST', headers, body })
if (!res.ok) {
if (res.status === 520) throw new Error((await res.json()).message)
throw new Error(`HTTP ${res.status}`)
}
// Hydrate dataset from binary stream
const ui8 = new Uint8Array(await res.arrayBuffer())
if (!(await dataset.write(ui8))) throw new Error('Invalid DataStream')
// Repaint DataGrids, forms, etc.
await this.RefreshBindings(dataset)
} catch (e) {
w2utils.notify(('Fetch failed\n' + e.message).replace(/\n/g, '<br>'), { error: true })
}
return this
}
// Save: POST dataset deltas as binary to Backstage,
// Server applies constraints and returns re-queried data snapshot.
async function Save(dataset) {
try {
// Read current change set (minimal deltas) as Uint8Array
const ui8 = dataset.read()
const res = await fetch(`${this.options.URL}?method=update`, {
method: 'POST',
headers: {
'x-csrf-token': this.options.CSRF,
'Content-Type': 'application/octet-stream'
},
body: ui8.buffer
})
if (!res.ok) {
if (res.status === 520) throw new Error((await res.json()).message)
throw new Error(`HTTP ${res.status}`)
}
// Reload dataset from server’s post-update snapshot (with RequeryOnUpdate)
const uui8 = new Uint8Array(await res.arrayBuffer())
await dataset.write(uui8)
// Refresh UI Data Bindings
await this.RefreshBindings(
dataset,
/*rebind*/ false,
/*preserveSelection*/ true
)
w2utils.notify(`Saved OK. Fetched ${dataset.recordCount} records.`, { timeout: 2000 })
} catch (e) {
w2utils.notify(('Update failed\n' + e.message).replace(/\n/g, '<br>'), { error: true })
}
return this
}
CustomSQL are assembled into a final
SELECT.
IN,
LIKE, IS NULL/NOT NULL, and a
DATE(...) wrapper that is
auto-selected from the column datatype (DATE/TIME/DATETIME) or
forced by the operator.
WHERE 1=1 (stand-alone reads) or
the placeholder form
WHERE [WHERE] when composing detail queries (so the
clause can be injected into a parent template).
WHERE [view.WHERE] → substitutes the generated
WHERE with the proper table/view aliasing.
WHERE [WHERE] → generic substitution for templates
that expect a clause insertion point.
SELECT plus
APPEND (…) RELATE <detail> TO <master> AS
<DetailName>.
FieldNames collection.
RecordIndex was requested).
ID convenience property is
exposed when a key column exists.
For every declared detail:
Get<DetailName>At(index) returns a
stand-alone child ORM bound to the specific detail
record and linked back via Parent.
<DetailName> exposes
a live active-record façade bound to the detail
dataset and provides an
iterator that yields
detached child ORM instances record-by-record.
Parent reference, enabling navigation and edits
without re-opening datasets.
Every ORM instance is returned through a Proxy to enforce shape and enable advanced behavior:
class ORMBase extends EventEmitter
{
/** @type {import('./@types/Dataset').CocoDataset} */
dataset = null;
/** @type {boolean} Wrap an existing dataset */
LINKED_DATASET = false;
/** @type {DESCRIPTOR} */
meta = null;
/** @type {AI_DESCRIPTOR} */
openai = null;
/** @type {boolean} suppresses sealing when setting dynamic properties */
__internalDefine = false;
/***************************************************************************************
* Initializes a new instance of the ORM base class.
* - Sets up metadata, dataset, and details as non-enumerable properties.
* - Ensures that a valid connection string is defined before proceeding.
*
* @param {string} TableName - The name of the table associated with the ORM instance.
* @param {string} ID - The primary key column name for the table.
* @param {FILTER[]} [Filters=[]] - An array of filters to apply to the table.
* @throws {Exception} - If the global `Database.ConnectionString` is not defined.
*/
constructor(...args)
{
super();
// Detect inheritance signature: `super(this, TableName, ID, Filters)`
if (args[0] && typeof (args[0]) === "function" && args[0].prototype instanceof ORMBase)
{
const [Class, TableName, ID, Filters = []] = args;
this.__constructor(Class, TableName, ID, Filters);
}
else
{
// Stand-alone signature: `new ORMBase(TableName, ID, Filters)`
const [TableName, ID, Filters = []] = args;
this.__constructor(ORMBase, TableName, ID, Filters);
}
this.__sealed = false;
// Wrap the object in a Proxy to prevent adding new properties
return new Proxy(this, {
get(target, prop, receiver)
{
if (prop === "$proxy_target") return target;
// Enable `for (let record of instance)` by handling Symbol.iterator
// Only inject an iterator if target hasn't defined one itself
if (prop === Symbol.iterator && !Reflect.has(target, Symbol.iterator))
{
return function* ()
{
target.First();
while (!target.EOF)
{
yield target;
target.Next();
}
};
}
return Reflect.get(target, prop, receiver);
},
set(target, prop, value, receiver)
{
if (target.__sealed && !(prop in target))
{
const name = target.constructor.name;
const msg = `⛔ Data Field "${prop}" not in ORM ${name} instance.`;
console.log(msg);
throw new Exception(msg);
}
// Respect accessors and prototype chain
return Reflect.set(target, prop, value, receiver);
},
deleteProperty(target, prop)
{
const name = target.constructor.name;
const msg = `⛔ Cannot delete Data Field property "${prop}" from ORM ${name} instance.`;
console.log(msg);
throw new Exception(msg);
},
ownKeys(target)
{
return Reflect.ownKeys(target);
},
getOwnPropertyDescriptor(target, prop)
{
return Reflect.getOwnPropertyDescriptor(target, prop);
},
has(target, prop)
{
return Reflect.has(target, prop);
},
defineProperty(target, prop, descriptor)
{
// 1) Always allow symbols (debug hooks, proxy escape hatch, etc.)
if (typeof prop === 'symbol')
{
return Reflect.defineProperty(target, prop, descriptor);
}
// 2) Allow known internal properties used by dynamic detail objects
if (prop === 'Parent' || prop === '__MasterID' || prop === '__DetailID')
{
return Reflect.defineProperty(target, prop, descriptor);
}
// 3) Allow framework-initiated defines while building wrappers
if (target.__internalDefine === true)
{
return Reflect.defineProperty(target, prop, descriptor);
}
// 4) If redefining an existing configurable property, allow it
const existing = Reflect.getOwnPropertyDescriptor(target, prop);
if (existing && existing.configurable)
{
return Reflect.defineProperty(target, prop, descriptor);
}
// 5) Enforce sealing for any *new* non-symbol props when sealed
if (target.__sealed && !(prop in target))
{
const name = target.constructor.name;
throw new Exception(`⛔ Data Field "${String(prop)}" not in ORM ${name} instance.`);
}
return Reflect.defineProperty(target, prop, descriptor);
},
});
...
}
}
get trap supplies
Symbol.iterator so the instance itself can be used in
for…of, streaming records by moving the cursor
(constant memory footprint).
__internalDefine = true),
defineProperty is allowed. After sealing, only:
Parent,
__MasterID, __DetailID),
$proxy_target can be read to access the underlying
object if needed by tooling.
This Proxy is essential: it preserves a stable, metadata-driven object model while still supporting streaming iteration and controlled augmentation for details.
Before/while reading:
Exception flow (with context); the native layer focuses
on logging (not throwing) so operations are
recorded in the Exceptions table.
💡 Why It Matters You write business objects, not plumbing: columns become safe properties with rules derived from metadata; details behave like typed collections. The Proxy gives you the best of both worlds: a sealed, predictable object model and generator-style iteration for streaming large result sets. The SQL engine handles real-world composition (nested filters, alias-aware templates, SHAPE), while lookup/caching integrates cleanly with UI and validation.
class Country extends SQLX.ORMBase {
constructor() {
super()
this.meta.Class = Country
this.meta.TableName = '[Countries]'
this.meta.ID = '[Countries].[CountryID]'
this.meta.Orders = [new SQLX.ORDER_BY('[Countries].[Name]', 'ASC')]
this.meta.Limit = 0
this.Meta() // load metadata only (no rows yet)
}
};
// read & stream rows with dual-nature iterator
const countries = new Country()
countries.ReadAll() // open dataset
for (const c of countries) {
// uses Proxy+iterator over cursor
console.log(c.CountryID, c.Name)
}
class Mailer extends SQLX.ORMBase {
constructor(nameOrId) {
super()
this.meta.Class = Mailer
this.meta.TableName = '[EmailAccounts]'
this.meta.ID = '[EmailAccounts].[EmailID]'
this.meta.Limit = 0
if (typeof nameOrId === 'number') {
this.ReadByID(nameOrId)
} else if (typeof nameOrId === 'string') {
this.meta.Filters = [
new SQLX.FILTER('[EmailAccounts].[Identifier]', '=', nameOrId),
new SQLX.FILTER('[EmailAccounts].[IsActive]', '=', '1')
]
this.Read()
} else {
this.ReadAll()
}
}
};
class Artist extends SQLX.ORMBase {
constructor() {
super()
this.meta.Class = Artist
this.meta.TableName = '[Artists]'
this.meta.ID = '[Artists].[ArtistID]'
this.meta.Limit = 0
this.Meta() // define getters/setters for columns
}
};
const artist = new Artist()
// INSERT
artist.Insert()
artist.ArtistName = 'New Artist' // setter writes to dataset column (editable-only)
artist.Write() // commit
// UPDATE (load → edit → write)
artist.ReadByID(1)
artist.Edit()
artist.Genre = 'House'
artist.Write()
EventItem)
class EventItem extends SQLX.ORMBase {
constructor() {
super()
this.meta.Class = EventItem
this.meta.TableName = '[EventItems]'
this.meta.ID = '[EventItems].[EventItemID]'
this.meta.Limit = 0
// JOIN Items
this.meta.Joins = [
new SQLX.JOIN('INNER', '[Items]', [
new SQLX.FILTER('[EventItems].[ItemID]', '=', '[Items].[ItemID]')
])
]
// Calculated field (server-side expression)
this.meta.Calculated = [
`
COALESCE((
SELECT SUM(1 + COALESCE(SPLITCOUNT([OtherNames], '|'), 0))
FROM [Reservations]
WHERE [Reservations].[Status] = 'CAPTURED'
AND [Reservations].[EventID] = [EventItems].[EventID]
AND [Reservations].[ItemType] = [Items].[Type]
), 0) AS [ReservedTickets]
`
]
this.Meta()
}
};
// Example: read items for one event date (via lookup filter)
const items = new EventItem()
items.meta.LookupFilters = {
'[EventItems].[EventID]': [
new SQLX.FILTER('[Events].[EventDate]', 'LIKE', `%${FISCAL_YEAR}%`)
]
}
items.ReadAll()
for (const it of items) {
console.log(it.EventItemID, it.ItemID, it.ReservedTickets)
}
This mirrors your Event model with two details and extra
options.
class Event extends SQLX.ORMBase {
constructor(idOrDate) {
super()
this.meta.Class = Event
this.meta.TableName = '[Events]'
this.meta.ID = '[Events].[EventID]'
this.meta.Limit = 0
this.meta.Filters = [
new SQLX.FILTER('[Events].[EventDate]', 'LIKE', `%${FISCAL_YEAR}%`)
]
this.meta.Orders = [new SQLX.ORDER_BY('[Events].[EventDate]', 'ASC')]
// Detail 1: EventItems joined with Items
const D1 = this.AddDetail(
EventItem,
'[EventItems]',
'[EventItems].[EventItemID]',
[],
'[Events].[EventID]',
'[EventItems].[EventID]'
)
D1.LookupFilters = {
'[EventItems].[EventID]': [
new SQLX.FILTER('[Events].[EventDate]', 'LIKE', `%${FISCAL_YEAR}%`)
]
}
D1.Orders = [new SQLX.ORDER_BY('[Items].[Sort]', 'ASC')]
D1.Joins = [
new SQLX.JOIN('INNER', '[Items]', [
new SQLX.FILTER('[EventItems].[ItemID]', '=', '[Items].[ItemID]')
])
]
D1.Fields = ['[EventItems].*', '[Items].*']
// Detail 2: EventArtists
const D2 = this.AddDetail(
EventArtist,
'[EventArtists]',
'[EventArtists].[EventArtistsID]',
[],
'[Events].[EventID]',
'[EventArtists].[EventID]'
)
D2.LookupFilters = {
'[EventArtists].[EventID]': [
new SQLX.FILTER('[Events].[EventDate]', 'LIKE', `%${FISCAL_YEAR}%`)
]
}
if (typeof idOrDate === 'number') this.ReadByID(idOrDate)
else if (typeof idOrDate === 'string') this.ReadByDate(idOrDate)
else this.Meta()
}
ReadByDate(date) {
this.meta.ID_VALUE = null
this.meta.Filters = [
new SQLX.FILTER(
'[Events].[EventDate]',
'DATE',
new Date(date).toISOString().split('T')[0]
)
]
this.Read()
}
};
// Iterate masters and details (dual-nature object + iterable detail proxies)
const events = new Event()
events.ReadAll()
for (const ev of events) {
console.log('Event:', ev.EventID, ev.EventDate)
for (const row of ev.EventItems) {
// detail iterator (shared handle)
console.log(' Item:', row.ItemID, row.Item?.Type, row.ReservedTickets)
}
for (const ea of ev.EventArtists) {
console.log(' Artist:', ea.ArtistID, ea.Artist?.ArtistName)
}
}
class Settings extends SQLX.ORMBase {
constructor() {
super()
this.meta.Class = Settings
this.meta.TableName = '[Settings]'
this.meta.ID = '[Settings].[ID]'
this.meta.Limit = 0
this.meta.isDictionary = true
this.meta.KEY = 'Name'
this.meta.VALUE = 'Value'
this.ReadAll()
this.RegisterVars() // publish key→value into ReplaceVars pipeline / globals
}
}
class CampaignResultsPivot extends SQLX.ORMBase {
constructor() {
super()
this.meta.Class = CampaignResultsPivot
this.meta.TableName = '[CampaignResultsPivot]'
this.meta.ID = '[CampaignResultsPivot].[CampaignID]'
this.meta.IsView = true
this.meta.Limit = 0
this.SetCustomSQL(`
WITH P AS ( SELECT * FROM PIVOT(CampaignResults, CampaignID, Value, 1) )
SELECT C.[Name],
PIVOT_COLUMNS(P.*, CampaignID),
PIVOT_SUM(P.*, CampaignID, [no]) AS [Positive],
PIVOT_SUM(P.*, CampaignID) AS [Total]
FROM P JOIN Campaigns C ON C.[ID] = P.[CampaignID];
`)
this.Meta()
}
}
Write() (required fields,
formats, FK sanity)
class Artist extends SQLX.ORMBase {
constructor() {
super()
this.meta.Class = Artist
this.meta.TableName = '[Artists]'
this.meta.ID = '[Artists].[ArtistID]'
this.meta.Limit = 0
this.Meta() // define dynamic getters/setters
}
// Minimal, explicit validation (add your own rules here)
validate() {
const errs = []
// Required fields (example)
if (!this.ArtistName || !this.ArtistName.trim())
errs.push('ArtistName is required.')
// Format check
if (this.Website && !/^https?:\/\//i.test(this.Website))
errs.push('Website must be http(s) URL.')
// FK presence (optional, SHAPE/constraints also guard this on commit)
if (this.GenreID == null) errs.push('Genre is required.')
// Uniqueness (simple example check)
const dup = SQLX.Run(
`
SELECT 1 FROM [Artists] WHERE [ArtistName] = ? AND [ArtistID] <> ? LIMIT 1
`,
this.ArtistName,
this.ArtistID || 0
)
if (!dup.EOF) errs.push('ArtistName must be unique.')
if (errs.length) throw new Error(errs.join('\n'))
}
}
// Usage
const a = new Artist()
a.Insert()
a.ArtistName = 'New Artist'
a.GenreID = 3
a.Website = 'https://example.com'
a.validate() // throws if invalid
a.Write() // commit (FK/lookup constraints will also enforce DB rules)
Notes
Write().
Lazy getter (no JOIN; fetch on demand)
class Item extends SQLX.ORMBase {
constructor() {
super()
this.meta.Class = Item
this.meta.TableName = '[Items]'
this.meta.ID = '[Items].[ItemID]'
this.Meta()
}
}
class EventItem extends SQLX.ORMBase {
constructor() {
super()
this.meta.Class = EventItem
this.meta.TableName = '[EventItems]'
this.meta.ID = '[EventItems].[EventItemID]'
this.meta.Limit = 0
this.Meta() // dynamic field wrappers
}
// Linked object: load when accessed
get Item() {
if (this.ItemID == null) return null
const it = new Item()
it.ReadByID(this.ItemID)
return it
}
}
// Usage
const evItems = new EventItem()
evItems.ReadAll()
for (const ei of evItems) {
console.log(ei.EventItemID, ei.Item?.Type) // Item fetched on demand
}
JOIN-backed fields (no extra fetch; fields already present)
class EventItemWithJoin extends SQLX.ORMBase {
constructor() {
super()
this.meta.Class = EventItemWithJoin
this.meta.TableName = '[EventItems]'
this.meta.ID = '[EventItems].[EventItemID]'
this.meta.Joins = [
new SQLX.JOIN('INNER', '[Items]', [
new SQLX.FILTER('[EventItems].[ItemID]', '=', '[Items].[ItemID]')
])
]
this.Meta()
}
// “Linked” view via already-joined columns (no ReadByID needed)
get ItemType() {
return this['Items.Type']
}
}
// Usage
const evItems = new EventItemWithJoin()
evItems.ReadAll()
for (const ei of evItems) {
console.log(ei.EventItemID, ei.ItemType) // zero extra queries
}
When to use which
Contents
Objectives
Module Surface (Exports & Augmentations)
String.prototype.$(context) — simple string templating
driven by an ORM/Model instance. Syntax:
"Hello ${Class.prop.path}".$(obj). If the
root token before the first dot doesn’t match
context.constructor.name, the token resolves to
"". Nested property access walks the object graph
(parts.reduce(...)).
NameCase(str) — normalize a person’s name: trim,
collapse spaces, strip diacritics, remove non‑letters, and
title‑case words.
SanitizeStringValue(input) — conservative static checks
to reject risky inputs before composing dynamic SQL.
StringifyValue(value, sanitize = true) — SQL‑oriented
stringifier for null, booleans, numbers/bigints,
strings, Date, and arrays.
Caption(name) — convert codey identifiers to
human‑friendly captions (underscores, camelCase, acronyms, special
terms).
Design Notes & Behaviors
String templating with String.prototype.$(context)
${...} but are strictly scoped: the
first segment must equal the model’s class name,
e.g. ${Reservation.Event.Date} only resolves when
context.constructor.name === "Reservation".
"" instead
of throwing.
Name normalization with NameCase(str)
" jóse-luis pérez " →
"Jose Luis Perez", "ANNA-MARIA" →
"Anna Maria".
'' for falsy or non‑string inputs.Input hardening with SanitizeStringValue(input)
"".\n, \r) and
common SQL injection motifs (e.g.,
DROP TABLE, UNION SELECT, obfuscated
multiline matches within ~10 chars).
' OR '1'='1),
<script>…</script>,
javascript:, HTML comments, and
any HTML tag payload.
"INVALID ...". Callers should treat this as a hard
failure and avoid rendering the result to the user.
SQL stringification with
StringifyValue(value, sanitize = true)
null → NULLboolean → '1' | '0' (SQLite‑friendly)
number|bigint → plain numeric stringstring → (optionally) sanitize →
escape ' → escape control chars (\n,
\r, \t) → wrap in single quotes
Date → 'YYYY-MM-DD HH:MM:SS' (local time
of the Date object)
Array → '(v1, v2, ...)' using recursive
StringifyValue; [] → '(NULL)'
TypeError("Unsupported value type: ...")
"INVALID ..." marker, a
TypeError is thrown to force callers to handle
rejection upstream.
Caption generation with Caption(name)
'('), punctuation‑heavy strings, and special cases
('w2cname').
1_2 → 1.2, _ →
space, camelCase splits, acronym re‑join, word capitalization, and
known replacements: HTML, CSS,
RDBMS, GUID, SSL,
PEM, IP, IPv4,
IPv6, HTTP, UI.
"Field" →
"<b>Data Field</b>",
"Render" →
"<b>Data Format</b>",
"Editable" → "Data Field Editor",
"btn_now" → "Now Button".
Usage Examples
String templating
class Reservation { constructor() { this.Event = { EventDate: '2025-09-01', Venue: 'Cavo' }; } }
const r = new Reservation();
"Event: ${Reservation.Event.EventDate} @ ${Reservation.Event.Venue}".$(r); // "Event: 2025-09-01 @ Cavo"
"Wrong: ${Order.Id}".$(r); // ""
Safe value building for ad‑hoc SQL
const whereName = StringifyValue("O'Reilly"); // '\'O\'\'Reilly\''
const whereIds = StringifyValue([1, 2, 3]); // '(1, 2, 3)'
const when = StringifyValue(new Date(2025, 7, 25)); // '2025-08-25 00:00:00' (example)
Defensive sanitization
const s = SanitizeStringValue("UNION SELECT password FROM Users");
if (s.startsWith("INVALID")) { throw new Error(s); } // "INVALID STRING: SQL INJECTION"
Captioning fields for UI
Caption("customer_name"); // "Customer Name"
Caption("shortMonths"); // "Short Months"
Caption("userGUID"); // "User GUID"
Caption("(Already Formatted)");// "(Already Formatted)"
Integration & Best Practices
StringifyValue() when
expanding filter literals for ad‑hoc segments,
but prefer parameterized queries when available.
String.prototype.$ for
read‑only rendering; avoid feeding its output back
into SQL.
SanitizeStringValue() on
user‑controlled strings as early as possible
(controllers/forms), fail fast on "INVALID ...".
NameCase() is
English‑oriented; names with non‑Latin scripts will be stripped—use
only when appropriate.
Caveats & Edge Cases
String.prototype.$: silently returns "" on
class mismatch or missing property; if you need strict failures,
wrap it and throw on empty.
StringifyValue(Date): uses the Date’s
local time—ensure consistency with your DB timezone strategy.
SanitizeStringValue: heuristic by design; it
does not replace proper query parameterization and
database permissions.
💡 Why It Matters
UTILX.jscentralizes tiny but high‑leverage primitives that recur across the stack: templating for labels, consistent UI captions, strict value formatting for SQL, and an opinionated string sanitizer. Keeping these concerns small, predictable, and reused reduces boilerplate, aligns behavior between modules, and shrinks the surface for injection bugs—especially in those few places where dynamic SQL is unavoidable.
Contents
Objectives
Architecture at a Glance
Server-side
default.asp.
backstage.inc) that defines UI layouts, remote
function calls, rights enforcement, and CSRF/session security.
commands.inc) bound to
datasets or ORM objects.
webauthn.asp,
gmail_authorize.asp, oauth2.asp)
provide authentication and integration without weakening the
session model.
Client-side
databinding.js.
datagrid.js)
that can transform a dataset into any business view:
grid, form, search, list, calendar, email, or chart.
Is Backstage MVC Framework?
ASP.js Backstage is not a textbook MVC (Model–View–Controller) framework like ASP.NET MVC or Ruby on Rails. It’s more of a metadata-driven runtime framework that borrows some MVC ideas but extends them in a different direction:
Model layer is largely handled by your SQLX and ORMX stack. Data comes in through datasets, shapes, pivots, and ORM proxies. These expose both relational and object-like views of data, with metadata for constraints, lookups, and detail bindings.
Controller layer in Backstage is implicit rather than explicit. Instead of you writing controllers that map HTTP routes to business logic, Backstage uses metadata and conventions to auto-wire data binding, actions, and UI events. It’s closer to a declarative configuration than imperative controllers.
View layer is highly dynamic and metadata-driven. Backstage takes dataset/ORM metadata and renders DataGrids, forms, toolbars, calendars, list views, charts, etc. automatically, respecting master–detail hierarchies and UI hints. This is not the same as templated views, but it fulfills the “V” role in MVC.
In other words Backstage is closer to a “Model–Metadata–View” (MMV) framework than a strict MVC. Controllers aren’t written by hand; they’re synthesized from metadata, conventions, and runtime binding logic.
Innovations Beyond SPA Frameworks
No REST, No GraphQL, No Glue Code Backstage eliminates the typical boilerplate stack (controllers, serializers, REST endpoints, Redux/Vuex state managers). The UI talks directly to datasets — securely and declaratively.
Metadata is the Contract In Angular or Vue, you
spend weeks crafting forms and grids by hand. In Backstage, a few
metadata lines in backstage.inc generate the full
CRUD interface, including rights filtering, CSRF tokens, and
encrypted Remote Call IDs.
ASP Dual Debugger (Cross-Tier) Debug at both levels — client and server — in a single session:
fetch, the debugger
jumps into the server-side ASP.js/Node handler.
Unified Security Model SPA frameworks rely on patching security later (JWTs, CSRF tokens, CORS). Backstage bakes security into every layer: ASP Sessions, CSRF enforcement, encrypted function identifiers, auto-ban hooks, audit logs.
Differences vs Classic ASP & Modern SPA Frameworks
vs Classic ASP:
vs Modern SPA (Angular, Vue, React Admin):
💡 Why It Matters Backstage flips the script: instead of wasting weeks wiring up REST APIs, controllers, reducers, and Angular forms, you focus only on your models and metadata. The framework gives you:
- A complete secure backoffice in hours, not months.
- A cross-tier debugger that follows execution seamlessly from browser → server → browser again.
- End-to-end architecture where server, client, and security are not patched together — they are designed as one. Backstage is not just another framework; it’s a radical acceleration layer for building business-critical applications with less code, more security, and unmatched debug visibility.
Objectives
backstage.inc to load UI metadata after
authentication succeeds.
Lifecycle
Session check
default.asp verifies if a session
token is present and valid.
webauthn.asp).
Model preload
Users, UserRights,
Settings, Audit).
Metadata load
backstage.inc is included, which drives
the rendering of the user interface.
Security Considerations
Innovations Beyond SPA Frameworks
default.asp.
Differences vs Classic ASP & Modern SPA Frameworks
💡 Why It Matters
default.aspensures that every Backstage request starts secure and consistent: sessions are validated, models are ready, metadata is loaded. Developers don’t waste time wiring login checks, preload calls, or boilerplate state hydration. Instead, you immediately step into building business logic — with security already guaranteed.
Purpose
What backstage.inc Produces
nodes[]) that the client renders with w2ui; clicking a
node creates the full view it describes.
CAN_WRITE, CAN_DELETE, etc.) that the
client uses to shape toolbars/buttons at runtime
(e.g., hide “Add/Delete” if not allowed).
type: 'master'|'detail', model, method: 'meta'|'fetch', optional
chartOptions, etc.). The client creates
Emscripten‑backed datasets, fetches
metadata or data, and wires master→detail
invalidation.
boundControl) or static (HTML body
provided by server). Switching tabs instantiates or swaps
controllers/datasets on demand.
chartOptions, the client adds a dedicated
Chart layout/panel beside the grid.
Security & Transport
x-csrf-token) that backstage.inc injects into the
binder options. The client automatically attaches it on all
requests.
x-sqlx-model-id (and when needed
x-sqlx-model-property) so the server can route safely
to the intended model surface—no free‑form routing from the browser.
options.id back—never
a human name. (Backstage design; IDs flow through
ServerCall.)
Runtime Behavior (how the client consumes the metadata)
UI_CONFIG. Node clicks call
CreateModelView(node) which upgrades legacy nodes if
needed, instantiates master datasets,
auto‑discovers detail datasets, and renders/binds
panels.
UI_ACCESS so “Add/Delete/Save” appear
only when permitted. Advanced search/date range pickers and
“Load By Value” overlays are added when the dataset
advertises them.
DefaultReportToolbar entries render as interactive
filters; selecting values can open a
model picker popup backed by another dataset.
group open the JSONEditor bound to the
emitted UI object—useful for admins to tweak live config safely.
Remote Function Calls (RFC)
server payload triggers ServerCall(),
which posts to ?method=<opaque id> with CSRF +
model headers. It can also attach search params or a
raw payload.
postRecords is set—no bespoke client code.
${{SELECTED_RECORD_IDS}}
and ${{FIELD_ID}} variables, shows a popup, collects
values, and posts a structured payload.
server-job-progress, and shows a progress dialog with
percentage + status until completion or error.
Content-Disposition: attachment, the client
auto‑downloads the file (e.g., “Export to Excel”, “Print PDFs”).
message (toast) or eval (client
instruction) executed after success (e.g., refresh panel).
Developer Ergonomics
Differences vs Classic ASP & Modern SPA Frameworks
vs Classic ASP
.asp pages,
Backstage declares
layouts, datasets, toolbars, and actions in one
metadata document the client renders verbatim.
vs Modern SPA (Angular, Vue, React Admin)
💡 Why It Matters With backstage.inc you don’t “code a screen,” you declare a screen. The client binder turns that declaration into a secure, rights‑aware UI with datasets, grids, charts, and RFC buttons—complete with CSRF, session‑encrypted action IDs, progress for long jobs, and file downloads. You get enterprise‑grade wiring (master→detail, search overlays, JSON config editors) without writing glue, and it all plays perfectly with Backstage’s dual debugger: step a toolbar click, hop into the server handler, and step right back into the client when the response lands. That’s RAD without compromise.
Purpose
backstage.inc (toolbar/menu actions, context actions).
eval post‑actions), file streams (with
Content‑Disposition: attachment), or orchestration for
long‑running jobs with progress polling.
Execution Flow (end‑to‑end)
Client action → opaque method A toolbar/menu item
carries a session‑encrypted RFC ID. On click, the
binder posts ?method=<opaque-id> with headers:
x-csrf-token, x-sqlx-model-id (and
optionally x-sqlx-model-property), and a fresh
x-job-id for tracking async tasks.
Payload shaping (client) Depending on metadata, the binder may attach:
${{SELECTED_RECORD_IDS}} and
${{FIELD_ID}} substitution.
payload for filters/criteria.
Dispatch (server)
commands.inc validates CSRF + session, resolves the
opaque ID to a handler function, instantiates the
target Model, and executes business logic under
current user rights. (Design per binder
contract.)
Response
message toasts, may execute eval, and
refreshes views.
Content‑Disposition: attachment—the client saves it
automatically.
x-job-id,
start background work, and feed progress via
the server-job-progress endpoint. The client shows
a progress dialog and polls until
done/error.
Handler Shape (typical patterns)
message/eval
(e.g., “Mailed 125 customers”).
x-job-id; Output:
immediate ACK; progress served via
server-job-progress; final JSON/attachments returned when ready.
Input Types (what the client can send you)
payload JSON (arbitrary object).
search map
appended to URL).
Output Types (what you can return)
{ message?: string, eval?: string, data?: any } (client
runs eval, shows toasts, refreshes).
Content‑Disposition: attachment; filename="..." and an
appropriate Content‑Type. Client will save it.
server-job-progress (e.g.,
{ progress: 37.5, message: "Rendering page 15/40" });
client renders a modal progress bar.
Security & Auditing
backstage.inc. (Prevents probing.)
x-sqlx-model-id/x-sqlx-model-property
headers (prevents routing outside authorized surfaces).
Developer Ergonomics
x-job-id, update the progress endpoint while you work.
The UI dialog and bar are automatic.
Differences vs Classic ASP & Modern SPA Frameworks
vs Classic ASP
commands.inc, not mixed into page markup.
vs Modern SPA (Angular, Vue, React Admin)
💡 Why It Matters
commands.incis where business power meets framework discipline. You expose operations as small, testable functions; the framework supplies the rest—secure dispatch, CSRF, model scoping, selected‑row payloads, server‑rendered forms, streaming exports, and long‑task progress. Compared to Classic ASP or modern SPA stacks, it’s dramatically less boilerplate, more secure, and easier to reason about—and with Backstage’s cross‑tier Dual Debugger you can click a button, step into the server handler, and land back in the client as soon as the response arrives.
Objectives
Endpoint Roles
webauthn.asp
default.asp.
gmail_authorize.asp / oauth2.asp
Security Model (Cross‑Cutting)
Operational Behaviors
default.asp, which
preloads models and
loads metadata; the UI renders according to the
user’s rights.
Innovations Beyond SPA Frameworks
Differences vs Classic ASP & Modern SPA Frameworks
💡 Why It Matters Authentication and integration are where many admin apps grow fragile. Backstage’s dedicated endpoints establish a single, durable trust boundary (ASP Session + CSRF + opaque actions) and keep OAuth tokens server‑side only. You get WebAuthn‑grade security, simpler code, and fewer moving parts—so you can focus on business features without risking your security posture.
Objectives
Runtime Flow
{ URL, CSRF, model } and renders the
initial node. The binder manages the master
w2layout and w2sidebar, and
measures performance for diagnostics.
Differences vs Classic ASP & Modern SPA Frameworks
💡 Why It Matters A one-shot loader plus a smart binder means instant boot, fewer moving parts, and zero “app wiring” code. You go from login → fully rendered, secured backoffice in one hop.
Objectives
Key Responsibilities
?method=<opaque id> with
x-csrf-token, x-sqlx-model-id, and a
unique x-job-id; can render a
server-provided HTML form (Base64) to collect
additional inputs before sending; supports
async jobs with progress polling.
Differences vs Classic ASP & Modern SPA Frameworks
💡 Why It Matters The binder is where Backstage earns its speed: one engine takes a secure, session-shaped JSON and materializes a complete application—layouts, filters, charts, and all—while keeping calls safe (CSRF, scoped headers) and surfacing long-task progress out of the box.
Objectives
Surface & Options
DataGrid focuses on UI/interaction; actual record
binding comes from the WebASM dataset. Key
capabilities and view modes are summarized in the header docs.
DataGridOptions configures everything from
hidden/read-only columns to list/calendar/email fields, chart menu,
filters, toolbar items, pagination, and more.
Notable Behaviors
Multi‑view powerhouse (one controller, many faces). Renders the same dataset as a classic Grid, Form, Search Form, List View, Calendar View, Email View with minimap, and Chart View (ECharts) — all switchable without re‑binding.
Form & entry UX that auto‑fits itself. Smart
label/value sizing and tabbed forms (DATAFORM + tabs) with
canvas-based text measurement to align labels, plus field‑type
cues (e.g., JSON/HTML fields get a CODE class).
Inline editing that feels native. Click‑to‑edit cells with input/checkbox/textarea/contenteditable, double‑click to open editors, and bulk paste across multiple selected rows (grid mode) with debounce.
Keyboard navigation like a desktop app. Arrow Up/Down to move focus, PageUp/PageDown by visible block, Ctrl+Home/End to jump, Enter handling, Esc hides overlays, Tab/Shift+Tab across fields, Ctrl/Cmd+F opens Find, Ctrl/Cmd+0 clears current field.
Selection you can trust. API to get/set selected rows; preserves focus, repositions dataset, and keeps the focused row “on top” in selection arrays.
Strong column ergonomics. Drag‑reorder columns (with live DOM moves), resize with handles, auto‑fit or fit‑to‑width, sticky columns, hide/unhide, and persisted widths/sort in localStorage.
Context menus everywhere (and smart).
Copy & export that respects your tools. One‑click copy as Markdown (great for docs/ChatGPT), copy as Excel (tab‑delimited), and export to .xlsx (SheetJS) with auto‑sized columns and friendly filenames.
Search that matches how analysts think. A generated Search Form supports multi‑value OR groups, proper type coercion (numeric/date), and saves form state for rebuilds.
List View that actually sells the data. Image or icon thumbnails (auto‑detects URLs vs icon classes), captions + secondary text, ribbons with color mapping, tooltips, min‑height tuning, custom item classes, and click/dblclick hooks.
Calendar View for time‑series catalogs. Scans min/max date, builds per‑month calendars, drops record thumbnails into cells, adapts to mobile (days‑per‑row), and supports ResizeObserver.
Email View for casework threads. Full page-per-email rendering with a vertical minimap colored by tier; click to scroll; lazy auto‑fetch for message bodies; re‑enables text selection if it was disabled.
Charting that’s not an afterthought (ECharts).
Lookups that scale from dropdown to popup. Uses lookup constraints to show an inline w2menu or opens a model‑driven popup DataGrid (read‑only, with filters) to pick records — with record‑status checks and helpful popups when selection isn’t valid.
Uploads where you need them. Field‑level UploadBox support (double‑click the cell) — the controller wires the right record id and delegates to the binder’s upload UI.
Mobile niceties (use it on a phone, really).
contextmenu and
suppresses native callouts/selection for a true desktop‑like
feel.
Overlay hygiene & scrolling. Any scroll or
Escape hides w2ui overlays safely, cleaning up
data-field-w2ui attributes to prevent stale UI.
Focus that follows the data. Moving the dataset
cursor updates selection, keeps active_field_ordinal,
and selects text in the right editor, with mobile exceptions
handled.
Differences vs Classic ASP & Modern SPA Frameworks
💡 Why It Matters DataGrid isn’t just a table — it’s the Backstage “view engine.” With one controller you jump from grid to form to calendar/email/chart without losing binding, selection, or context. Analysts get real keyboard power and one‑click exports; developers get rich context menus, bulk‑edit & paste, lookup popups, uploads, and first‑class charting with sensible defaults. It feels like a desktop client… in a browser… wired to your WebASM dataset.
Objectives
sqlxems.js) that powers high‑performance binding to
controllers.
Role in the Stack
Dataset,
VectorInt), the backbone of all binding operations.
Differences vs Classic ASP & Modern SPA Frameworks
💡 Why It Matters These helpers are purpose-built for Backstage’s binder and controllers. They enable complex screens (nested layouts, filters, calendars, multiselect search) to be declared in metadata and realized faithfully, without bespoke component code each time.
Focus: CreateLayout (from databinding.js)
Objectives
backstage.inc) into live
w2layout instances, and attach the right
controller (DataGrid, or plain HTML tab) to each
panel.
What CreateLayout Does
boundControl. If none exists, the
panel is rendered as plain HTML (static content or
a custom render(container) callback).
master._dataset.detail(name)) and calls
HookDataset so the controller can bind to a live
dataset.
DataGrid controller is created
with merged options from the dataset spec and the
bound control. The toolbar is prepared (preflight
spec) or instantiated at runtime, depending on
phase.
Bind), and the controller is
optionally refreshed (focus/reposition controls are
honored). References are stored on both the panel and controller for
later cleanup.
layout (e.g., a side
ChartLayout) is visited recursively so its own
panels/toolbars/tabs become active.
RenderLayout(parent, panelType, layout) converts
logical panels to concrete w2layout configs, builds
the layout, and relinks runtime panels/controllers
back to their design‑time descriptors.
ChartLayout is encountered, the
binder injects report toolbars (top on desktop,
bottom on mobile) bound to the master dataset for
immediate analytics.
_panel, _controller); the binder installs
onClick so it can either
render a plain tab or
rehydrate/bind a data‑bound tab on demand,
including re‑building the toolbar instance.
Key Helpers Around CreateLayout
CreatePanel(panel, …, hint) Handles
both preflight (spec only) and
runtime (live toolbar instance) phases; merges
options, instantiates the controller, binds dataset, refreshes UI,
and respects the active tab selection.
RenderPlainTab(panel, tab, isRuntime)
Cleans any controller/toolbar, mounts HTML (inline
or fetched via URL) or calls a
custom render function, and optionally attaches a
tab‑level toolbar.
RebuildPanelToolbar(panel, …, isRuntime)
In design time it stores a spec; at runtime it
swaps a live w2toolbar into the
panel’s slot and keeps a handle for controller refreshes.
DestroyPanel / DestroyLayout
Properly unbinds controllers, drops toolbars,
clears HTML, and walks nested layouts to avoid leaks; used before
rebuilding or when navigating to a new node.
Algorithm (condensed)
layout.panels:
boundControl (panel‑level or tab‑level).
RenderPlainTab.layout,
recurse (CreateLayout(child)).
RenderLayout to construct
w2layout, install
ChartLayout toolbars if present, and
relink descriptors ↔ runtime objects.
Master→Detail Cohesion
master.detail(name); any controllers bound to those
details are re‑created or
re‑bound to reflect the current record—no stale
data.
Quality‑of‑Life Details
Layout1,
Layout2, …).
Differences vs Classic ASP & Modern SPA Frameworks
CreateLayout) materializes
nested layouts, data‑bound controllers, and
live toolbars, while tab switching and long‑view
lifecycles are handled uniformly by the binder—no component‑specific
glue.
💡 Why It Matters
CreateLayoutis where Backstage’s metadata becomes a living app. With one pass it resolves bound controls, hooks datasets, binds controllers, renders nested layouts, wires tabs, installs report toolbars, and guarantees detail views track the master—then tears it all down cleanly when you navigate. It’s the reason you don’t need a SPA router, store, or hand‑written wiring: the binder does it once, the same way, everywhere, and your team ships features instead of scaffolding.
The Grid View is the default presentation of datasets in Backstage. It offers:
The Form View transforms a single record into a structured form:
The Search Form View provides query building:
List View displays records as rich cards or thumbnails:
Calendar View organizes records over time:
The Email View renders dataset records as styled message pages:
The Chart View brings visual analytics to datasets using ECharts:
Objectives
ServerCall, Fetch, Meta,
Save) that handles headers, payload shaping, long‑task
progress, file downloads, and UI feedback.
Client–Server Mechanics (What the binder actually does)
Fetch and
Meta call Browse, which constructs a URL
(?method=fetch|meta), appends optional
search params, and sends JSON with
headers including x-csrf-token. On success, the returned ArrayBuffer is written
straight into the WebASM dataset and the UI is refreshed.
Save posts the dataset
binary diff (application/octet-stream)
to ?method=update with x-csrf-token. The
response replaces the dataset and triggers a UI refresh.
ServerCall(dataset, options) posts to
?method=<opaque id> with CSRF,
model scoping, and (optionally)
job id for long tasks. It can attach
selected record IDs, and/or values from a
server‑generated HTML form (Base64) after
placeholder substitution.
Headers, Tokens & Scoping
x-csrf-token: this.options.CSRF).
x-sqlx-model-id (and optionally
x-sqlx-model-property) so the server routes to the correct surface—no free‑form
endpoints.
options.id).
Payload Shaping & Selected Records
postRecords is set,
ServerCall automatically sends the
selected row IDs from the active grid. If a
server‑form is included
(postForm.html), the HTML is decoded,
${{SELECTED_RECORD_IDS}} is
substituted, field placeholders are replaced using
the current dataset values, and the collected form data is posted.
Long‑Running Tasks (Progress UI)
options.async, the binder generates a
x-job-id, fires the POST, opens a modal progress dialog,
and polls
?method=server-job-progress with
x-job-id (+ CSRF). It updates text and a progress bar
until done/error, then closes and returns the final
response.
Binary Responses (File Downloads)
Content‑Disposition: attachment, the binder converts
the response to a Blob, creates a temporary link,
and downloads the file with the suggested
filename—no extra client code needed.
Result Handling & Post‑Actions
After a JSON response, the binder will:
eval any
client instruction returned by the server
(e.g., refresh a panel).
message is
provided.
Errors & Guardrails
520 server errors are parsed and shown; other statuses
raise a descriptive error.
Master→Detail Security & Consistency
Developer Tips (Using it well)
postRecords: true; the binder
will attach the selected IDs automatically.
async: true; you’ll get progress UI
for free via x-job-id.
💡 Why It Matters Backstage’s binder turns security into a contract: every call ships CSRF, model scope, and opaque action IDs; long jobs get progress UI; files download cleanly; errors surface consistently. You write business logic—the plumbing is guaranteed.
This hands-on mini-project shows how to build an RSVP feature with ASP.js Backstage on top of SQLX/ORMX. You’ll create the database, models, email templates, an endpoint to capture clicks, and a Backstage node (metadata) that renders a full back-office UI with a pivoted overview and a chart.
Flow (high level):
Back-office user triggers an RSVP campaign (or your app does it automatically).
The system sends templated emails containing “answer buttons” (YES / MAYBE / NO).
Each button points to an endpoint like
/endpoints/survey.asp?type=rsvp&campaign=…&guid=…&answer=yes.
When a customer clicks, the endpoint
writes a row in CampaignResults.
The UI shows:
Campaigns.CampaignResults for
the selected campaign.
CampaignResultsPivot)
summarizing answers.
📄 server.js – Application entry point: initializes SiteManager and DB
📄 package.json – Node.js project manifest
📄 config.json – Web server configuration: ports, SSL, AutoBan, Security
📂 node_modules – Installed Node.js dependencies
┣ 📂 @mobilefx – mobileFX framework modules
┃ ┗ 📂 aspx – Core ASPX runtime (IISX, SITEX, HTTPX, etc.)
┗ 📂 @mycompany – Your company’s custom modules
┗ 📂 myapp – Your application package
┗ 📄 Model.js – ORM models and business logic
📂 .ssl – SSL/TLS certificates and private keys
┣ 📂 .intermediate – Intermediate CA certificates (for trust chain)
┃ ┣ 📄 AAA_Certificate_Services.crt – AAA Certificate Services intermediate certificate
┃ ┣ 📄 Sectigo_RSA_Domain_Validation.crt – Sectigo RSA Domain Validation intermediate certificate
┃ ┗ 📄 USERTrust_RSA_Authority.crt – USERTrust RSA Certification Authority intermediate
┣ 📂 backstage.example.com – SSL/TLS material for Backstage back-office
┃ ┣ 📄 CACert.crt – Intermediate CA certificates (for trust chain)
┃ ┣ 📄 backstage_example_com.pem – Site certificate
┃ ┗ 📄 backstage_example_com.key – Site private key
┗ 📂 example.com – SSL/TLS material for public website
┃ ┣ 📄 CACert.crt – Intermediate CA certificates (for trust chain)
┣ 📄 www_example_com.pem – Site certificate
┗ 📄 www_example_com.key – Site private key
📂 sites – Root folder for hosted sites
┣ 📂 .data – SQLite data stores
┃ ┣ 📄 ASPX.db – ASPX internal DB (sessions, AutoBan, Audit)
┃ ┗ 📄 MyDatabase.db – Application DB (business data)
┣ 📂 backstage.example.com – Backstage back-office web application
┃ ┣ 📂 .inc – Server-side include files (metadata & logic)
┃ ┃ ┣ 📄 backstage.inc – Application metadata: layouts, datasets
┃ ┃ ┣ 📄 commands.inc – Remote function call (RFC) handlers
┃ ┃ ┗ 📄 login.inc – Authentication logic (WebAuthn)
┃ ┣ 📄 default.asp – Bootstrap ASP page for Backstage client UI
┃ ┣ 📄 manifest.json – Web App Manifest (PWA metadata: name, theme, icons)
┃ ┗ 📄 favicon.ico – Backstage site favicon
┗ 📂 example.com – Example frontend site
┣ 📄 default.asp – Simple ASP.js demo application page
┣ 📄 enpoint.asp – RSVP Endpoint
┗ 📄 favicon.ico – Example site favicon
This is the entry point of your Backstage application. Here we define the database connection string, import our framework, initialize the site manager, and schedule background jobs.
// Import core ASPX classes
const { SiteManager, Site, Proxy } = require("@mobilefx/aspx");
// Set default SQLite connection
global.SQLX.Database.ConnectionString = `${__dirname}/sites/.data/MyDatabase.db`;
// Import your framework
global.Cavo = require("@mycompany/myapp");
// Initialize Site Manager
const siteManager = new SiteManager();
// Start all configured sites
siteManager.Start();
// Create CronJob instance
global.cronJobsInst = new Cavo.CronJob();
// Schedule all active cron jobs
global.cronJobsInst.ScheduleJobs();
This JSON file defines how your Backstage server will run. It sets up the second database (ASPX.db) used by ASP.js framework itself, security checks, ports, proxies, and site definitions (including SSL certificates). The options section configures defaults like AutoBan, CSRF checks, and allowed directories/extensions, while the sites array maps each hostname to its corresponding web root and SSL setup. In this example, we’re hosting backstage.example.com over HTTPS with its own certificate.
{
"crudpads": [],
"options": {
"database": "sites/.data/ASPX.db",
"autoban": {
"blockListDatabase": "sites/.data/ASPX.db",
"learningMode": false
},
"httpPort": 80,
"httpsPort": 443,
"internalPort": 10000,
"localhostMode": true,
"siteDefaults": {
"allowedDirectories": null,
"allowedExtensions": null,
"enable_csrf_check": false,
"securityChecks": [
"FLOOD_ATTACK",
"METHOD_NOT_ALLOWED",
"INVALID_URL",
"INVALID_MIME",
"DIRECTORY_DENIED",
"DIRECTORY_INDEXING",
"HIDDEN_PATH_ACCESS",
"EXTENSION_DENIED",
"ACCESS_DENIED",
"INVALID_HOST",
"INVALID_SITE"
]
},
"webRoot": "sites"
},
"proxies": [],
"sites": [
{
"HTTP_404_URL": "/page-404.asp",
"aliases": [
{
"host": "www.example.com",
"ssl": {
"ca": [
".ssl/example.com/CACert.crt"
],
"cert": ".ssl/example.com/www_example_com.pem",
"key": ".ssl/example.com/www_example_com.key"
}
}
],
"host": "example.com",
"path": "example.com",
"ssl": {
"ca": [
".ssl/example.com/CACert.crt"
],
"cert": ".ssl/example.com/www_example_com.pem",
"key": ".ssl/example.com/www_example_com.key"
}
},
{
"host": "backstage.example.com",
"path": "backstage.example.com",
"ssl": {
"ca": [
".ssl/backstage.example.com/CACert.crt"
],
"cert": ".ssl/backstage.example.com/backstage_example_com.pem",
"key": ".ssl/backstage.example.com/backstage_example_com.key"
}
}
]
}
This schema defines the core tables needed for the RSVP feature:
Campaigns – stores each RSVP campaign, including its name, creation date, status (IsActive), reach/engagement counters, and the HTML messages shown to participants.
CampaignResults – records individual responses (YES / NO / MAYBE) linked back to a campaign. A unique index enforces that each (CampaignID, ReservationGUID, Tracker) combination can only appear once, preventing duplicate answers.
Settings – a generic key/value store for runtime configuration values (e.g., thresholds, system options).
Templates – reusable HTML templates for emails or engagement messages, referenced by campaigns.
Together these tables provide a full RSVP lifecycle: define a campaign, send messages, capture responses, and store results safely for reporting and pivots in Backstage.
💡Use mobileFX SQLite Express or any SQLite shell to run this DDL. The unique index prevents duplicate answers for the same
(CampaignID, ReservationGUID, Tracker).
CREATE TABLE IF NOT EXISTS [Campaigns] (
[ID] INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL ON CONFLICT ROLLBACK UNIQUE ON CONFLICT ROLLBACK ,
[IsActive] BOOLEAN NOT NULL ON CONFLICT ROLLBACK DEFAULT (1) ,
[Name] VARCHAR(50) NOT NULL ON CONFLICT ROLLBACK ,
[Created] DATETIME NOT NULL ON CONFLICT ROLLBACK DEFAULT (GETDATETIME()) ,
[ReachCount] INTEGER NOT NULL ON CONFLICT ROLLBACK DEFAULT (0) ,
[EngagementCount] INTEGER NOT NULL ON CONFLICT ROLLBACK DEFAULT (0) ,
[EmailMessage] HTML5 NOT NULL ON CONFLICT ROLLBACK ,
[EngagementMessage] HTML5 NOT NULL ON CONFLICT ROLLBACK ,
[ErrorMessage] HTML5 NOT NULL ON CONFLICT ROLLBACK );
CREATE TABLE IF NOT EXISTS [CampaignResults] (
[ID] INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL ON CONFLICT ROLLBACK UNIQUE ON CONFLICT ROLLBACK ,
[CampaignID] INTEGER NOT NULL ON CONFLICT ROLLBACK REFERENCES [Campaigns] ([ID]) ON UPDATE CASCADE ON DELETE CASCADE , --[CMETA:eyJMT09LVVBfVkFMVUVfQ09MVU1OIjogIltOYW1lXSJ9]
[ResponseDate] DATETIME NOT NULL ON CONFLICT ROLLBACK DEFAULT (GETDATETIME()) ,
[ReservationGUID] VARCHAR(50) NOT NULL ON CONFLICT ROLLBACK ,
[Tracker] VARCHAR(50) NOT NULL ON CONFLICT ROLLBACK ,
[Value] VARCHAR(50) NOT NULL ON CONFLICT ROLLBACK );
CREATE INDEX [INDEX_CampaignResults_CampaignID__Campaigns_ID] ON [CampaignResults] ([CampaignID]);
CREATE UNIQUE INDEX idx_campaignresults ON CampaignResults (CampaignID, ReservationGUID, Tracker);
CREATE TABLE IF NOT EXISTS [Settings] (
[ID] INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL ON CONFLICT ROLLBACK UNIQUE ON CONFLICT ROLLBACK ,
[Name] VARCHAR(50) NOT NULL ON CONFLICT ROLLBACK ,
[Value] VARCHAR NOT NULL ON CONFLICT ROLLBACK );
CREATE TABLE IF NOT EXISTS [Templates] (
[TemplateID] INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL ON CONFLICT ROLLBACK UNIQUE ON CONFLICT ROLLBACK ,
[Name] VARCHAR(50) NOT NULL ON CONFLICT ROLLBACK ,
[Text] HTML5 NOT NULL ON CONFLICT ROLLBACK );
The Templates table holds the HTML fragments that are injected into outgoing RSVP emails. Each template uses placeholders (${{Message}}) that will be substituted with campaign-specific text at runtime.
RSVP Template – the main invitation message with custom tags for Yes / Maybe / No. These tags will be converted by the endpoint into actionable links (/endpoints/survey.asp?...).
Thank You Template – returned to the participant after a valid click.
Error Template – returned if the link is invalid, expired, or has already been used.
The DOMAIN_NAME (eg. backstage.example.com) constant
defines the base URL where the survey endpoint is hosted — make sure
it includes a trailing slash so generated links resolve correctly.
RSVP Template:
<div style="font-family:Arial,sans-serif">
<p>${'{'}{Message}{'}'}</p>
<p>Please respond:</p>
<survey value="yes" style="background-color:#28a745;width:160px;border-radius:6px">I’m coming</survey>
<survey value="maybe" style="width:160px;border-radius:6px">Maybe</survey>
<survey value="no" style="background-color:#dc3545;width:160px;border-radius:6px">Can’t</survey>
</div>
Thank you Template:
<div style="font-family:Arial,sans-serif">
<h2>Thanks!</h2>
<p>${'{'}{Message}{'}'}</p>
</div>
Error Template:
```html
<div style="font-family:Arial,sans-serif">
<h2>Sorry!</h2>
<p>${'{'}{Message}{'}'}</p>
</div>
const DOMAIN_NAME = 'https://backstage.example.com/'; // trailing slash required
These ORM classes map directly to the RSVP schema and provide the business logic on top. They use SQLX.ORMBase to expose tables as live objects, automatically loading metadata, constraints, and detail relationships.
Templates – wraps [Templates] and
exposes template rows as a dictionary (Name → Text).
Automatically loads them into globalThis so you can
use template names directly in code.
Settings – same dictionary pattern for
[Settings], making global configuration available
through key/value pairs.
Campaign – the master table for RSVP campaigns.
It includes a detail relation to
CampaignResults and methods for sending email
(RSVP_Request) and handling clicks
(RSVP_Response). The class handles:
<survey> tags.
CampaignResults – detail table holding the raw responses (one row per click).
CampaignResultsPivot – a virtual “pivot view”
that aggregates results. Using PIVOT, it expands
values (yes, maybe, no)
into columns and computes totals. This drives Backstage charts and
summary grids.
Together, these models allow you to send campaigns, capture results, enforce rules, and display analytics — all using the declarative ORM style rather than raw SQL.
const { SQLX } = require('@mobilefx/aspx');
const crypto = require('crypto');
class Templates extends SQLX.ORMBase
{
constructor()
{
super();
this.meta.Class = Templates;
this.meta.TableName = '[Templates]';
this.meta.ID = '[Templates].[TemplateID]';
this.meta.Limit = 0;
this.meta.isDictionary = true;
this.meta.KEY = 'Name';
this.meta.VALUE = 'Text';
this.ReadAll();
this.RegisterVars(); // Load records as key-value pairs in globalThis
}
};
class Settings extends SQLX.ORMBase
{
constructor()
{
super();
this.meta.Class = Settings;
this.meta.TableName = '[Settings]';
this.meta.ID = '[Settings].[ID]';
this.meta.Limit = 0;
this.meta.isDictionary = true;
this.meta.KEY = 'Name';
this.meta.VALUE = 'Value';
this.ReadAll();
this.RegisterVars();
}
};
class Campaign extends SQLX.ORMBase {
constructor() {
super();
this.meta.Class = Campaign;
this.meta.TableName = '[Campaigns]';
this.meta.ID = '[Campaigns].[ID]';
this.meta.Limit = 0;
this.AddDetail(
CampaignResults,
"[CampaignResults]",
"[CampaignResults].[ID]",
[],
"[Campaigns].[ID]",
"[CampaignResults].[CampaignID]"
);
this.Meta();
}
static RSVP_Request(options) {
const mailer = new Mailer(options.Mailer || 'RSVP');
if (!mailer) throw new Exception('RSVP Mailer not found');
const tEmail = globalThis[options.TemplateEmail];
const tThanks = globalThis[options.TemplateThankYou];
const tError = globalThis[options.TemplateError];
if (!tEmail) throw new Exception(`Template ${options.TemplateEmail} not found`);
if (!tThanks) throw new Exception(`Template ${options.TemplateThankYou} not found`);
if (!tError) throw new Exception(`Template ${options.TemplateError} not found`);
const campaignHash = crypto.createHash('md5').update(options.Campaign, 'utf8').digest('hex');
const vars = Object.assign({}, options.Vars || {});
const trackerVal = vars[options.TrackerKey || 'Tracker'] || '';
const guidVal = vars[options.GuidKey || 'GUID'] || '';
// Load or create campaign
const campaign = new Campaign();
campaign.ReadByName(options.Campaign);
if (campaign.EOF) {
campaign.Insert();
campaign.Name = options.Campaign;
// Build the email body from the template, convert <survey> tags to buttons
let html = String(tEmail).replace('${{Message}}', options.MessageEmail || '');
html = html.replace(/(?:\s*<survey[\s\S]*?<\/survey>\s*)+/gi, (group) => {
const rxTag = /<survey\b([^>]*)>([\s\S]*?)<\/survey>/gi;
const btns = [];
let m;
while ((m = rxTag.exec(group)) !== null) {
const attrs = m[1] || '';
const label = (m[2] || '').trim();
const vm = /(?:^|\s)value\s*=\s*"([^"]+)"/i.exec(attrs);
const value = encodeURIComponent((vm && vm[1] || '').trim());
const sm = /(?:^|\s)style\s*=\s*"([^"]+)"/i.exec(attrs);
const style = {};
if (sm) {
sm[1].split(';').forEach(rule => {
const [k, v] = rule.split(':').map(s => s && s.trim());
if (k && v) style[k.toLowerCase()] = v;
});
}
const val = decodeURIComponent(value).toLowerCase();
const bg = style['background-color'] || (val === 'yes' ? '#28a745' : val === 'no' ? '#dc3545' : '#007BFF');
const color = style['color'] || '#ffffff';
const width = style['width'] || '160px';
const radius = style['border-radius'] || '4px';
const url =
`${DOMAIN_NAME}endpoints/survey.asp?type=rsvp` +
`&campaign=${campaignHash}` +
`&tracker=${encodeURIComponent(trackerVal)}` +
`&guid=${encodeURIComponent(guidVal)}` +
`&answer=${value}`;
btns.push({ label, bg, color, width, radius, url });
}
let fragment = '<table border="0" cellspacing="0" cellpadding="0" style="margin:16px 0;"><tr>';
btns.forEach((b, i) => {
if (i > 0) fragment += '<td style="width:24px;"> </td>';
fragment += `
<td align="center">
<!--[if mso]>
<v:roundrect xmlns:v="urn:schemas-microsoft-com:vml" xmlns:w="urn:schemas-microsoft-com:office:word"
href="${b.url}" style="height:40px;v-text-anchor:middle;width:${b.width};"
arcsize="10%" stroke="f" fillcolor="${b.bg}">
<w:anchorlock/>
<center style="color:${b.color};font-family:Arial,sans-serif;font-size:14px;font-weight:bold;">
${b.label}
</center>
</v:roundrect>
<![endif]-->
<!--[if !mso]><!-- -->
<a href="${b.url}" target="_blank"
style="background-color:${b.bg};border-radius:${b.radius};color:${b.color};display:inline-block;
font-family:Arial,sans-serif;font-size:14px;font-weight:bold;line-height:40px;text-align:center;
text-decoration:none;width:${b.width};-webkit-text-size-adjust:none;">
${b.label}
</a>
<!--<![endif]-->
</td>`;
});
fragment += '</tr></table>';
return fragment;
});
// Persist templates at campaign level
campaign.EmailMessage = html;
campaign.EngagementMessage = String(tThanks || options.MessageThankYou || '').replace('${{Message}}', options.MessageThankYou || '');
campaign.ErrorMessage = String(tError || options.ErrorMessage || '').replace('${{Message}}', options.ErrorMessage || '');
campaign.Write();
}
// Personalize and send using ORM's substitution
const personalized = campaign.ReplaceVars(campaign.EmailMessage, vars);
// If your mailer/stack supports extracting inline attachments from HTML, keep this;
// otherwise, pass options.attachments straight through.
const attachments = options.attachments || [];
mailer.Send({
to: options.To,
subject: options.Subject || 'Invitation',
html: personalized,
attachments
});
}
static RSVP_Response() {
try {
const type = Request.QueryString("type");
const name = Request.QueryString("campaign"); // md5 of Campaign.Name
const tracker = Request.QueryString("tracker"); // arbitrary tracker
const answer = Request.QueryString("answer"); // yes/no/maybe
const guid = Request.QueryString("guid"); // arbitrary GUID (optional)
if (type !== 'rsvp') throw new Exception('Invalid RSVP Campaign');
// Find active campaign by md5(Name)
const campaign = new Campaign();
campaign.meta.Filters = [
new SQLX.FILTER("md5\\([Campaigns].[Name]\\)", "=", name),
new SQLX.FILTER("[Campaigns].[IsActive]", "=", 1)
];
campaign.Read();
if (campaign.EOF) throw new Exception(`Invalid RSVP Campaign '${name}'`);
// Count engagement
campaign.EngagementCount++;
campaign.Write();
// Insert row (unique index prevents duplicates)
try {
SQLX.Exec(
'INSERT INTO CampaignResults (CampaignID, ReservationGUID, Tracker, Value) VALUES (?, ?, ?, ?)',
[[campaign.ID, guid || '', tracker || '', answer || '']]
);
} catch (e) {
throw new Exception(`Duplicate RSVP not allowed (${tracker || 'tracker'})`);
}
// Render Thank-You using campaign-level substitution only
const ctx = { GUID: guid || '', Tracker: tracker || '', Answer: (answer || '').toUpperCase(), Campaign: campaign.Name };
const body = campaign.ReplaceVars(campaign.EngagementMessage, ctx);
Response.AddHeader('Content-Type', 'text/html; charset=utf-8');
Response.Write(body);
} catch (e) {
Response.AddHeader('Content-Type', 'text/html; charset=utf-8');
Response.Write(e.message);
}
}
}
// Raw clicks (detail of Campaign)
class CampaignResults extends SQLX.ORMBase
{
constructor()
{
super();
this.meta.Class = CampaignResults;
this.meta.TableName = '[CampaignResults]';
this.meta.ID = '[CampaignResults].[ID]';
this.meta.Limit = 0;
this.Meta();
}
}
// Pivoted overview (yes/no/maybe counts per campaign)
class CampaignResultsPivot extends SQLX.ORMBase
{
CUSTOM_SQL = `
WITH P AS (
SELECT * FROM PIVOT(CampaignResults, CampaignID, Value, 1)
)
SELECT
C.[Name],
PIVOT_COLUMNS(P.*, CampaignID),
PIVOT_SUM(P.*, CampaignID, [no]) AS [Positive],
PIVOT_SUM(P.*, CampaignID) AS [Total]
FROM P
JOIN Campaigns C ON C.[ID] = P.[CampaignID];
`;
constructor()
{
super();
this.meta.Class = CampaignResultsPivot;
this.meta.TableName = '[CampaignResultsPivot]';
this.meta.ID = '[CampaignResultsPivot].[CampaignID]';
this.meta.IsView = true;
this.meta.Limit = 0;
this.SetCustomSQL(this.CUSTOM_SQL);
this.Meta();
}
}
PIVOT(CampaignResults, CampaignID, Value, 1) groups by
CampaignID, spreads distinct
Value (yes/no/maybe…) into columns, and fills with
1 (counts). PIVOT_COLUMNS expands those
columns, PIVOT_SUM(...,[no]) sums a specific column,
PIVOT_SUM(...) totals all pivots.
<%
const { Campaign } = require('@mycompany/myapp');
Campaign.RSVP_Response();
Response.End();
%>
This Backstage JSON node defines master and detail datasets, and binds them to DataGrids on a complex yet intuitive UI layout. The UI is divided in a main master area on the top and the details area on the bottom. Details are assigned to tabbed-panels.
Add the following to backstage.inc:
{
id: 'rsvp-campaigns',
text: 'RSVP Campaigns',
icon: SVG('button-choice'),
datasets: [
{ model: 'Campaign', type: 'master', name: 'Campaign', options: { showFilters: true, showAdvancedFetch: true } },
{ model: 'Campaign', type: 'detail', master: 'Campaign', name: '[CampaignResults]', options: { showFilters: true } },
{ model: 'CampaignResultsPivot', type: 'master', name: 'CampaignResultsPivot', options: { showFilters: true, readOnly: true } },
],
layout: {
panels: [
{
type: 'main',
style: 'overflow:hidden',
resizable: true,
hidden: false,
layout: {
panels: [
{
type: 'main',
style: 'overflow:hidden',
resizable: true,
hidden: false,
boundControl: {
dataset: 'Campaign',
options: { type: 'grid' }
}
},
{
type: 'bottom',
size: '70%',
style: 'overflow:hidden',
resizable: true,
hidden: false,
tabs: {
active: '[CampaignResults]',
tabs: [
{
id: '[CampaignResults]',
text: 'Results',
icon: 'fa fa-list',
boundControl: {
dataset: '[CampaignResults]',
options: { type: 'grid' }
}
},
{
id: 'tab-overview',
text: 'Overview',
icon: 'fa fa-info-circle',
boundControl: {
dataset: 'CampaignResultsPivot',
options: { type: 'grid', readOnly: true }
}
},
{
id: 'tab-insights',
text: 'Insights',
icon: 'fa fa-bar-chart',
boundControl: {
dataset: 'CampaignResultsPivot',
toolbar: false,
options: { type: 'chart', readOnly: true,
chartOptions: {
type: 'bar',
transpose: false,
stack: false,
showLegend: true,
labels: true,
xField: 0,
xAxisName: 'Campaign',
dimensionName: 'Name',
ySeries: [1, 2, 3],
seriesColors: {
yes: '#17751aff',
maybe: '#aa4400ff',
no: '#6e110aff',
}
}
}
}
}
]
}
}
]
}
}
]
}
},
TODO
// Fire a single email
await Campaign.RSVP_Request({
Mailer: 'RSVP',
Campaign: 'Opening Night 2025-09-05',
TemplateEmail: 'RSVP_TemplateEmail',
TemplateThankYou: 'RSVP_TemplateThankYou',
TemplateError: 'RSVP_TemplateError',
Tracker: 'ReservationID',
Subject: 'Can you make it?',
MessageEmail: 'We’d love to see you!',
MessageThankYou: 'Thanks for responding!',
ErrorMessage: 'This invite is no longer valid.',
email: 'customer@example.com'
});
Idempotency: That unique index guarantees a
single answer per
(CampaignID, ReservationGUID, Tracker). Your
endpoint’s try/catch reports duplicates cleanly.
Security:
md5(Campaign.Name) in the
link masks your internal keys.
Deactivation: Set IsActive = 0 to
instantly stop accepting clicks.
Deliverability: Keep button styles inline and include the VML fallback (already done) for Outlook.
Internationalization: Your
<survey> labels can be localized; the stored
Value is what analytics/pivot uses
(yes/no/maybe/other).
Metrics: EngagementCount increments
on every click (visit). Use COUNT(*) on
CampaignResults for unique submission counts.
You now have a replicable, full-stack example: DB → models → emails → endpoint → Backstage UI with pivot & chart. Paste the code as shown, wire the single endpoint, and you’ve got a working RSVP pipeline that your team can extend safely.
Contents
Built-In Telemetry
ASP.js embeds monitoring at the database schema level. Instead of requiring third-party APM agents or custom log shippers, the framework writes structured telemetry directly into first-party SQLite tables. This means:
Core Structures
UserSessions Tracks active sessions per host
and per time bucket (DateSlot). Provides the
baseline for traffic analytics and
capacity planning.
UserAgents Captures parsed client fingerprints (browser, platform, version, OS, device, bot flag). Enables compatibility tracking, bot detection, and QA regression baselines.
ExceptionsLog Records every unhandled error with full context: message, stack, file/line, HTTP request, session/user identity, form data, and cookies. Each entry is a self-contained incident capsule that supports root cause analysis without guesswork.
Benefits
Audit as a Database Primitive
ASP.js treats auditing as a schema-level concern.
Triggers on sensitive tables populate AuditLog with
readable diffs, ensuring every insert/update/delete is
attributable.
AuditLog
AuditUser, Operation,
TableName, PrimaryKey, and
Changes.
TRIGGER_<Table>_INSERT_AUDIT,
TRIGGER_<Table>_DELETE_AUDIT and
TRIGGER_<Table>_UPDATE_AUDIT.
CURRENT_USER(),
GETDATETIME(), DIFF_IF_CHANGED())
to automate attribution.
This means developers don’t need to manually instrument updates — the database itself records “who did what, when, and how”.
Security Controls
AutoBanBlockList
ExceptionsLog as Security Log
Benefits
H()), CSRF, and AutoBan, many attacks are stopped
at the database boundary.
Core Idea
ASP.js extends its schema-first philosophy to access control. Rights are expressed as relational data, not code paths, enabling administrators to configure, audit, and evolve security without touching business logic.
Structures
Users Store account data, credentials, PINs,
and encrypted OAuth tokens. Link each user to a role via
UserRoleID.
UserRoles Define business roles (Administrator, Manager, Agent). Lightweight but meaningful groupings.
UserAsset Abstract both
menu entries and reports as
“assets”. Reports are linked to Analytics.ID.
UserRoleRights The heart of RBAC. Defines
per-role permissions on each asset (ReadAccess,
WriteAccess, DeleteAccess,
AIAssistant).
Benefits
AIAssistant) can be added without redesign.
Core Idea
While AutoBan blocks offenders,
NetworkWhitelist defines
who is allowed in. It enforces a proactive
zero-trust perimeter for Backstage access. Unlike
a raw blocklist, the whitelist can also hold
network masks, enabling controlled access for
trusted external services (e.g., search engine crawlers, social
media bots).
Structure
TTL for dynamic hosts.
IsStatic marks permanent entries; dynamic entries
are resolved on schedule.
Features
66.249.64.0/19 for Googlebot). This ensures
Facebook, Instagram, Google crawlers and other
approved bots can reach the site without whitelisting every
single IP.
Benefits
💡 Why It Matters The whitelist doesn’t just protect internal staff — it also governs external actors. By supporting masks and subnets, ASP.js lets admins safely admit essential crawlers (Facebook, Instagram, Google) without opening the floodgates. This provides measurable, auditable bot access and ensures security and marketing can coexist in the same operational framework.
Core Idea
Backstage doubles as a self-service BI platform.
Reports are stored in the Analytics table, promoted
to assets (UserAsset), and governed by RBAC
(UserRoleRights). This makes analytics a first-class
feature, not an integration.
Structures
Analytics Each report has metadata
(Name, Category, Icon,
Description), SQL text,
ChartOptions JSON, and an optional
ConnectionString.
Integration Every report is also a
UserAsset of type Report, with
access rights assigned in UserRoleRights.
Runtime Behavior
ChartOptions, and binds the result to a DataGrid or
Chart view.
Benefits
💡 Why It Matters ASP.js doesn’t bolt on monitoring, auditing, or analytics — they are part of the schema contract. Sessions, user agents, exceptions, audits, bans, rights, whitelists, and reports are all first-class tables. This makes DevOps and compliance dramatically cheaper: dashboards, audits, and BI reports are just SQL away. By elevating these concerns to the database, ASP.js ensures observability, governance, and intelligence are defaults, not afterthoughts.
Contents
Dual Debugger
Visual Studio Code Integration
backstage.inc and
reload to instantly see new layouts, toolbars, or dataset
bindings.
Logging & Diagnostics
ERRX.js.
Custom Actions
commands.inc, expose
them as toolbar items in backstage.inc, and they
become instantly available to users — secured by CSRF, sessions,
and sealed IDs (H()).
Custom Views & Controllers
render(container) function to host bespoke widgets
(maps, KPIs, specialized charts).
DataGrid controller for 95% of cases, but
drop to custom rendering when needed.
Scheduled Jobs
Integrations
Reusable Models
Users,
Support, Reservations, etc.) across
applications.
Classic IIS / Node Hybrid
Containerized Deployment
Cloud-Native
Serverless Mode (future-ready)
commands.inc are naturally idempotent;
they can be refactored into serverless functions (AWS Lambda,
Azure Functions) with minimal adjustments.
On-Prem Enterprise
💡 Why It Matters Developer experience isn’t just about APIs — it’s about flow. With ASP.js and Backstage you get a debugger that spans client and server, a metadata contract that renders complete screens, extension points that don’t require glue code, and deployment models that fit everything from legacy IIS to Kubernetes. The result: developers spend less time wiring, more time shipping.
Contents
ASP.js recreates the Classic ASP runtime model in modern Node.js while extending it with async execution, debugger integration, and hardened security.
ASPX.js provides the runtime substrate,
delivering familiar objects (Request,
Response, Session,
Application, Server) but augmented with
hash parsing, upload hardening, and CSRF hooks.
ERRX.js centralizes error handling with structured exception objects and consistent propagation across server and client tiers.
SQLX.js establishes the data plane, wrapping SQLite with pivot macros, shape queries, and a dataset model that powers master–detail and reporting scenarios.
ORMX.js builds on SQLX to generate models dynamically, wrapping fields and details with proxies to give entities both record and generator semantics.
UTILX.js provides utility primitives — string substitution, name normalization, safe SQL value stringification, and caption generation — ensuring consistency across layers.
Backstage Framework unifies the stack:
Taken together, ASP.js is not just a compatibility layer — it is a secure, metadata-driven RAD environment for building backoffice and operational apps with less code, more consistency, and stronger guarantees.
💡 Why It Matters ASP.js began as an emulation of Classic ASP but has evolved into a full-stack application framework where runtime, data, models, utilities, and UI converge. It modernizes familiar patterns without losing their simplicity, while adding the rigor, security, and debugging depth demanded by today’s applications. The journey ahead is about making this ecosystem even more expressive, visual, and intelligent, so developers can deliver robust business software at unprecedented speed.
Access to the source code of the ASP.js VSCode Extension, ASP.js Runtime Framework and SQLX Native Data Access requires obtaining a Source Code License, enabling advanced customization and full transparency. For more information, contact info@mobilefx.com.