@webtypen/webframez-core
v0.3.70
Published
The core package of the typescript based webframez (web-framework) by webtypen.de
Readme
@webtypen/webframez-core
TypeScript-first backend framework core for Node.js.
This README reflects the current API in this repository and focuses on:
- Routing
- ApiScopes and ApiFunctions
- Datatables
- DataBuilder
- Console Commands
- Queue and Jobs
Requirements
- Node.js
>= 20(recommended) - TypeScript for development
Installation
npm i @webtypen/webframez-coreDatabase drivers (from 0.3.69)
The core has no MongoDB/BSON dependency. HTTP routing and database-independent commands work without installing or registering a database driver. Database access requires a registered driver; missing configuration or capabilities fail explicitly.
For MongoDB, use @webtypen/webframez-dbdriver-mongodb >=0.0.11 and register
MongoDBDriver with DBDrivers.register("mongodb", MongoDBDriver) before database
features are used. Upgrade the driver before the core. Applications that directly
import MongoDB must declare their own mongodb dependency.
Driver authors implement idAdapter for ID creation, normalization, validation and
comparison. ID conversion does not open a connection. Model.objectId(...) keeps
its existing name and driver-defined return value for compatibility.
Datatables, DataBuilder, pagination, queues and notifications use the optional
documentStore(client): DocumentDatabase capability. Its filters, updates and
aggregation stages use MongoDB-style document operators; another driver must
translate them or report that this capability is unsupported. This is not a SQL
query abstraction. findOneAndUpdate must atomically select/update one record and
return the document or null, never a native result wrapper. No read-then-write
fallback is allowed for queue or notification claims.
Verification after npm run build:
node --test -r ts-node/register/transpile-only src/Database/DatabaseAdapter.spec.ts src/Commands/QueueWorkerCommand.spec.ts src/Notifications/NotificationService.spec.tsThe tests include an HTTP server with MongoDB, BSON and all database-driver imports blocked, and a numeric-ID test driver that never opens a database connection.
Agent-Regeln fuer Codex und Copilot
Das Paket enthaelt unter agents/webframez_rules.md empfohlene Arbeitsregeln fuer AI-Coding-Agents in Webframez-Projekten.
Nach der Installation liegt die Datei im Paket unter:
node_modules/@webtypen/webframez-core/agents/webframez_rules.mdDie Regeln beschreiben unter anderem:
- verbindliche Nutzung konkreter Model-Klassen fuer fachliche Datenzugriffe statt Collection-Wrappern
- Nutzung von Model.objectId(...) statt eigener ObjectId-Resolver
- bevorzugte Nutzung der vorhandenen Helper wie StringFunctions, NumericFunctions und DateFunctions
- Verwendung der Webframez-Abstraktionen fuer Request, Response, Middleware, Storage, Datatables und Routes
Wichtig: AGENTS.md-Dateien haben keine universell standardisierte Import-Funktion. Die praktikable Variante ist deshalb, die Regeldatei aus dem Paket in der AGENTS.md des eigenen Projekts explizit zu referenzieren und projektspezifische Regeln lokal zu ergaenzen.
Beispiel fuer eine eigene AGENTS.md im Consumer-Projekt:
# AGENTS.md
Dieses Projekt verwendet @webtypen/webframez-core.
Uebernimm fuer alle Webframez-bezogenen Implementierungen die Konventionen aus:
./node_modules/@webtypen/webframez-core/agents/webframez_rules.md
Insbesondere gilt:
- pro persistierter Entitaet eine Model-Klasse unter app/Models anlegen; Controller, Services und Screens greifen ueber diese Klassen zu
- keine DatabaseService.collection(...)-Wrapper oder eigene Cursor-/JSON-Persistenzschichten implementieren; fehlende atomare Operationen im Treiber ergaenzen
- fuer ObjectId-Konvertierungen Model.objectId(...) oder die passende Model-Instanzmethode verwenden
- vorhandene Webframez-Helper und Facades bevorzugen statt neue Utilities oder Resolver anzulegen
- fuer HTTP-, Routing-, Middleware-, Storage- und Datatable-Code die Webframez-APIs und Konventionen beibehalten
Projektspezifische Ergaenzung:
- dieses Projekt verwendet fuer Admin-Routen zusaetzlich die Middleware "admin-auth"
- Antworten im Backoffice sollen das Format { status, message, data } einhaltenWenn eine Regel aus der lokalen Projekt-AGENTS.md einer Regel aus der Paketdatei widerspricht, sollte die lokale Projektregel Vorrang haben.
Application basename
Set the public mount once in config/application.ts:
export default {
router: {
get basename() { return process.env.BASENAME || ""; },
},
};Pass this config as boot({ config: { application }, ... }). For example,
BASENAME=/my-app mounts every registered route (including route groups and
module/API routes) below /my-app. Empty or / keeps the application at the
root. The legacy boot({ basename }) option takes precedence when explicitly
provided, including "" or null. The basename is normalized and validated at
boot; changing it requires restarting the application. Web and Lambda use the
same configuration.
Route.path("/api/items"), appPath("/api/items"), and
res.redirect("/login") include the basename. Already-prefixed URLs retain one
prefix, and external URLs, anchors, and relative URLs remain unchanged.
Route.relativePath(...) / appRelativePath(...) remove a matching basename
at a complete path boundary. Route registration always takes application-local
paths; do not pass Route.path(...) into Route.get(...).
Node applications import these helpers from @webtypen/webframez-core.
Browser code can use the public, Node-free @webtypen/webframez-core/routing
entry. Rendering adapters provide the runtime basename to that shared API;
Core does not rewrite arbitrary HTML strings, native fetch calls or raw
Location headers. Use appPath for those application URLs.
webframez-react automatically adopts the Core basename for Route.renderReact,
including nested mounts and route groups. It publishes the basename for browser
navigation, forms and asset URLs. The build output directory remains independent
of the deployment path, so one artifact can be mounted at different basenames.
Basic Web Setup
import { BaseKernelWeb, Request, Response, Route, WebApplication } from "@webtypen/webframez-core";
class Kernel extends BaseKernelWeb {
static controller = {
TestController: class {
async index(req: Request, res: Response) {
return res.send({ status: "ok" });
}
}
};
static middleware = {
auth: async (next: Function, reject: Function, req: Request, res: Response) => {
const isAllowed = true;
if (!isAllowed) {
return reject(new Error("Unauthorized"));
}
next(true);
}
};
}
const app = new WebApplication();
app.boot({
kernel: Kernel,
port: 3000,
basename: null,
routesFunction: () => {
Route.get("/", "TestController@index");
}
});WebApplication.boot(...) supports:
kernelportbasenameroutesFunctionmodulesconfigdatatablesapiScopeson the kernel or module providersjobsmodeonBoot
Routing
Register Routes
Route.get("/", "HomeController@index");
Route.post("/login", "AuthController@login");
Route.put("/users/:id", "UserController@update");
Route.delete("/users/:id", "UserController@delete");
Route.patch("/users/:id", "UserController@patch");You can also register inline handlers:
Route.get("/health", (req: Request, res: Response) => {
return res.send({ status: "ok" });
});Path Parameters and Wildcards
Supported path patterns:
- Required parameter:
/:id - Optional parameter:
/:id? - Single wildcard:
/* - Catch-all wildcard:
/**
Matched values are available in req.params.
Route Groups
Route.group({ prefix: "/admin", middleware: ["auth"] }, () => {
Route.get("/dashboard", "AdminController@dashboard");
Route.group({ prefix: "/users" }, () => {
Route.get("/:id", "AdminUserController@details");
});
});Route Middleware
Attach middleware by key via route options:
Route.get("/me", "AccountController@me", { middleware: ["auth"] });Route Domain Filter
You can restrict routes (or groups) to specific domains:
Route.group({ domains: ["websites.simplebis.com", "*.local.dev"] }, () => {
Route.get("/", "HomeController@index");
});
Route.get("/api/health", "HealthController@index", {
domains: ["api.example.com"]
});Domain matching also works behind reverse proxies (x-forwarded-host is respected).
If a wildcard domain matches, the extracted wildcard value is available in
req.routeDomainWildcard. The matched request hostname is available in
req.routeDomainMatch.
Middleware signature:
(next, reject, req, res) => {
// allow:
next(true);
// abort with error:
// reject(new Error("Forbidden"));
}Extend the Route Facade
You can add custom route registration helpers:
Route.extend("jsonGet", (route) => {
return (path: string, component: any, options?: any) => {
route.get(path, async (req: Request, res: Response) => {
res.header("Content-Type", "application/json");
return component(req, res);
}, options);
};
});
(Route as any).jsonGet("/x", (req: Request, res: Response) => res.send({ ok: true }));Request and Response
Request
Request includes:
method,url,headers,rawHeadersbody,bodyPlainquery,queryRawparamsrouteDomainMatch,routeDomainWildcardfilesmessage(nativeIncomingMessage)
Response
Common helpers:
res.status(201);
res.header("X-Test", "1");
res.send({ status: "success" });Also available:
sendCsv(...)download(filepath, options?)stream(req, filepath, filename, mimeType)registerEvent("after", fn)
ApiScopes and ApiFunctions
ApiScopes group callable ApiFunctions and can automatically expose them as normal HTTP routes. MCP exposure is intentionally not implemented in core; install @webtypen/webframez-ai and use APIFunctionsMCPServer when ApiFunctions should be exposed as MCP tools.
Define ApiFunctions
import { ApiFunction, ApiFunctionRequest, ApiFunctionResponse } from "@webtypen/webframez-core";
class CurrentUserFunction extends ApiFunction {
key = "current-user";
description = "Returns the current user.";
requestMethod = "GET";
params = {
includePermissions: { type: "boolean", default: false }
};
async handle(apiRequest: ApiFunctionRequest) {
return new ApiFunctionResponse({
user: apiRequest.context.user,
includePermissions: apiRequest.params.includePermissions
});
}
}requestMethod may be "GET", "POST", "PUT", "PATCH", "DELETE" or null.
Functions with requestMethod = null are not registered as normal HTTP routes, but they can still be used by higher-level integrations such as APIFunctionsMCPServer in @webtypen/webframez-ai.
Define ApiScopes
import { ApiScope, Request, Response } from "@webtypen/webframez-core";
class BackofficeApiScope extends ApiScope {
key = "backoffice";
apiBasePath = "/api/backoffice";
functions = [CurrentUserFunction];
async middleware(req: Request, _res: Response, abort: (message?: any, status?: number) => never) {
if (!req.headers.authorization) {
return abort("Unauthorized", 401);
}
return {
user: { id: "..." }
};
}
}For every function with a non-null request method, core registers:
apiBasePath + "/" + function.keyExample: GET /api/backoffice/current-user.
The scope middleware runs before the function. Its return value is passed to the function as apiRequest.context. Calling abort(message, status) stops execution and returns an error response with that status.
Group ApiScopes
Use ApiScopesGroup to share middleware across multiple scopes:
import { ApiScope, ApiScopesGroup, Request, Response } from "@webtypen/webframez-core";
class BackofficeApiScope extends ApiScope {
key = "backoffice";
apiBasePath = "/api/backoffice";
functions = [CurrentUserFunction];
async middleware(_req: Request, _res: Response, _abort: (message?: any, status?: number) => never) {
return {
scope: "backoffice"
};
}
async groupMiddleware(_req: Request, _res: Response, _abort: (message?: any, status?: number) => never) {
return {
reachedThroughGroup: true
};
}
}
class BackofficeApiScopesGroup extends ApiScopesGroup {
key = "backoffice-group";
apiScopes = [BackofficeApiScope];
async middleware(req: Request, _res: Response, abort: (message?: any, status?: number) => never) {
if (!req.headers.authorization) {
return abort("Unauthorized", 401);
}
return {
user: { id: "..." }
};
}
}When a scope is reached through a group, middleware runs in this order:
ApiScopesGroup.middleware()
ApiScope.middleware()
ApiScope.groupMiddleware()The context objects are merged in that order, so later scope-specific values can override group values. groupMiddleware() only runs when the scope was registered through an ApiScopesGroup.
Register ApiScopes
Register scopes on the web kernel:
import { BaseKernelWeb } from "@webtypen/webframez-core";
class Kernel extends BaseKernelWeb {
static apiScopes = [BackofficeApiScope, BackofficeApiScopesGroup];
}Modules can register scopes as instance properties:
import { ModuleProvider } from "@webtypen/webframez-core";
class BackofficeModuleProvider extends ModuleProvider {
apiScopes = [BackofficeApiScope, BackofficeApiScopesGroup];
}Params and Validation
GET functions validate params from req.query; all other request methods validate params from req.body.
Supported descriptor types include:
stringnumber/floatinteger/intboolean/boolObjectIdoptionarrayobject
Defaults are applied before required checks. Unknown types are passed through unchanged but still respect required and default.
Datatables
Use Datatable + DatatableRegistry + DatatableController.
Define a Datatable
import { Datatable, Request } from "@webtypen/webframez-core";
export class UsersTable extends Datatable {
collection = "users";
perPage = 25;
columns = {
name: { label: "Name" },
email: { label: "E-Mail" }
};
filter = {
name: { type: "text", mapping: "name" }
};
aggregation = async (req: Request) => {
return [{ $sort: { created_at: -1 } }];
};
}Register Datatables
app.boot({
// ...
datatables: {
users: UsersTable
}
});Datatable Endpoints
Use DatatableController in your routes:
import { DatatableController, Route } from "@webtypen/webframez-core";
Route.post("/api/datatable", "DatatableController@restApi");
Route.post("/api/datatable/export", "DatatableController@tableExport");restApi supports:
- init requests (
init_request) - paginated data
- selectable functions (
apiFunction)
tableExport uses your table exports definition.
DataBuilder
Use DataBuilder + DataBuilderController for schema-driven CRUD flows.
Define and Register Types
import { DataBuilder } from "@webtypen/webframez-core";
const builder = new DataBuilder();
builder.registerType({
key: "user",
singular: "User",
plural: "Users",
schema: {
version: "1.0.0",
collection: "users",
fields: {
email: { type: "text", required: true, unique: { match: {} } },
age: { type: "integer" },
default_values: {
type: "array",
schema: {
key: { type: "string", required: true },
name: { type: "string", required: true }
}
},
target: {
type: "option",
optionsMapping: { from: "default_values", value: "key", label: "name" }
},
mixed_target: {
type: "option",
optionsMapping: [
{ from: "default_values", value: "key", label: "name", valuePrefix: "@defaults.", labelPrefix: "Standard: " },
{ from: "features_targets", value: "key", label: "name", labelPrefix: "Ziel: " },
{ from: "@parent.attributes", value: "key", label: "name" }
]
},
licenses: {
type: "array",
toggable: true,
previewLabel: "{{ name }}",
schema: {
key: { type: "string", required: true },
name: { type: "string", required: true }
}
}
}
}
});Forms may group their complete layout in tabs. Tab fields still address the same
root data object; tabs only change presentation. appearance: "plain" avoids an
additional card around a full-form tab layout.
forms: {
main: {
fields: [{
type: "tabs",
appearance: "plain",
tabs: [
{ key: "general", title: "General", icon: "settings", fields: [{ field: "email" }] },
{ key: "defaults", title: "Defaults", icon: "database", fields: [{ field: "default_values" }] }
]
}]
}
}option fields can use static options: [{ value, label }] or a client-side
optionsMapping. The mapping can be a single object or an array of objects with
{ from: string; value: string; label: string; valuePrefix?: string; labelPrefix?: string }. Multiple
mapping entries are aggregated in order. @parent.<field> source paths resolve
schema-aware to the closest parent schema context defining <field>, for example
features_groups[].licenses[].adds[].key with @parent.attributes resolves to
features_groups[].attributes.
Array fields can set toggable: true to let the frontend collapse entries. Use
previewLabel with the same template syntax to render a label in the entry title
row. toggable and previewLabel may be defined on the schema field or the form
layout entry; form layout options override schema options.
You can also use:
registerModelType(...)registerFieldType(...)
Expose the REST API
import { DataBuilderController, Route } from "@webtypen/webframez-core";
class MyDataBuilderController extends DataBuilderController {
constructor() {
super(builder);
}
}
// Register controller in kernel, then:
Route.post("/api/builder", "MyDataBuilderController@restApi");__builder_rest_api actions:
typedetailsdetails-newdatasavedeleteapi-autocomplete
Console Commands
Use ConsoleApplication with BaseKernelConsole.
import { BaseKernelConsole, ConsoleApplication, ConsoleCommand } from "@webtypen/webframez-core";
class HelloCommand extends ConsoleCommand {
static signature = "hello";
static description = "Print a greeting";
async handle() {
this.success("Hello world");
}
}
class KernelConsole extends BaseKernelConsole {
static commands = [HelloCommand];
}
const app = new ConsoleApplication();
app.boot({
kernel: KernelConsole,
config: {}
});ConsoleCommand helpers include:
getArguments(),getOptions(),getOption(...)ask(...)write,writeln,info,warning,success,error- progress helpers (
progress,progressIncrement,progressFinish)
Queue and Jobs
Queue workers run through console commands and use queue_jobs storage.
Create a Job
import { BaseQueueJob } from "@webtypen/webframez-core";
export class SendMailJob extends BaseQueueJob {
async handle(job: any) {
this.log("Sending mail", job.payload);
// do work...
// optional delayed re-run:
// return this.executeAgain(5, "minutes");
}
}Create a new queue entry:
await SendMailJob.create({
payload: { userId: "..." },
priority: 5,
worker: null
});Register Jobs
app.boot({
// ConsoleApplication or WebApplication
jobs: [SendMailJob]
});Queue Config
Configure workers in config.queue:
export default {
workers: {
default: {
is_active: true,
jobclasses: ["SendMailJob"],
automation: [
{
jobclass: "SendMailJob",
executions: [
["every_x_mins", 15],
["daily", "08:00"],
["every_hour", 5],
["mondays", "09:30"]
]
}
]
}
}
};Built-in Queue Commands
queue:startqueue:statusqueue:stopqueue:logqueue:workerqueue:worker:autorestart
Additional built-in command:
build
Backups
Backups are configured through config.backup, usually by importing config/backup.ts next to your database and queue configs. Core ships the local filesystem output driver and a generic adapter registry.
export default {
defaults: {
workDir: "storage/backups/.work",
outputDir: "storage/backups",
filename: "{key}_{date}_{time}",
zip: true,
zipDriver: "auto",
zipCompressionLevel: 6,
cleanupWorkDir: true,
retention: {
keepLast: 10,
maxAgeDays: 30,
runAfterBackup: true
}
},
types: {
daily_full: {
is_active: true,
files: [
{
from: "storage",
to: "storage",
include: ["**/*"],
exclude: ["queue/**", "backups/**"]
}
],
databases: [
{
connection: "default",
to: "database/default",
options: {}
}
],
outputs: [
{
driver: "local",
path: "storage/backups/daily",
retention: {
keepLast: 14,
maxAgeDays: 60,
runAfterBackup: true
}
}
],
automation: {
worker: "worker_1",
executions: [["daily", "02:00"]],
priority: 10
}
}
}
};Built-in backup commands:
backup:listbackup:run <key> [--dry-run] [--channel=local] [--silent] [--log-interval=1000]backup:queue <key> [--worker=worker_1] [--priority=10]backup:cleanup <key> [--channel=local] [--dry-run]
ZIP artifacts use the system zip binary by default when available and fall back to the built-in Node.js ZIP writer. Set zipDriver: "system" to require system zip, zipDriver: "node" to force the built-in writer, or zipDriver: "auto" for fallback behavior. zipCompressionLevel accepts 0 through 9. Backup runs log file-scan and copy progress every 1000 files by default; change this with --log-interval=250 or suppress progress with --silent.
Remote outputs are optional packages and must be registered by the application before the backup config is used:
import { BackupOutputDrivers } from "@webtypen/webframez-core";
import { GoogleDriveBackupOutputDriver } from "@webtypen/webframez-backups-googledrive";
import { OneDriveBackupOutputDriver } from "@webtypen/webframez-backups-onedrive";
import { SftpBackupOutputDriver } from "@webtypen/webframez-backups-sftp";
import { FtpBackupOutputDriver } from "@webtypen/webframez-backups-ftp";
BackupOutputDrivers.register("google_drive", GoogleDriveBackupOutputDriver);
BackupOutputDrivers.register("one_drive", OneDriveBackupOutputDriver);
BackupOutputDrivers.register("sftp", SftpBackupOutputDriver);
BackupOutputDrivers.register("ftp", FtpBackupOutputDriver);
export default {
types: {
daily_google_drive: {
is_active: true,
files: [{ from: "storage", to: "storage", exclude: ["backups/**", "queue/**"] }],
outputs: [
{
driver: "google_drive",
folderId: process.env.GOOGLE_DRIVE_BACKUP_FOLDER_ID,
auth: {
keyFile: process.env.GOOGLE_APPLICATION_CREDENTIALS
},
retention: {
keepLast: 30,
maxAgeDays: 90,
runAfterBackup: true
}
}
],
automation: {
worker: "worker_1",
executions: [["daily", "03:00"], ["daily", "12:00"]]
}
}
}
};Available adapter packages:
@webtypen/webframez-backups-googledrive@webtypen/webframez-backups-onedrive@webtypen/webframez-backups-sftp@webtypen/webframez-backups-ftp
Database backups are delegated to the configured database driver via backup(client, options). Implement that hook in your driver to write dump files into options.targetDir.
Modules
You can load module providers via modules in WebApplication.boot(...).
A module provider can:
- register controllers/middleware
- define
boot() - define
routes()
Lambda Mode
LambdaApplication uses the same router in AWS Lambda mode:
import { LambdaApplication } from "@webtypen/webframez-core";
const app = new LambdaApplication();
export const handler = (event: any, context: any) => {
return app.boot(event, context, {
kernel: Kernel,
routesFunction: () => {
Route.get("/status", (req, res) => res.send({ ok: true }));
}
});
};Inline text and icons
InlineTextDefinition is a JSON-safe text contract exported by Webframez Core and
structurally compatible with Native Design. Strings and existing locale maps stay
valid. Arrays preserve order and explicit whitespace:
[{ type: "icon", name: "ubuntu" }, " Ubuntu 24.04"]Suite screen titles/subtitles, Card/CardHeader, Alert and Text accept this content
(SuiteTextDefinition). In React, use <InlineText value={content} /> in other
text slots; ScreenTitle also accepts the array directly. On React Native, place
InlineText inside Text. Icons use the registered Native Design Icon component.
Built-in names include linux, linux-pinguin and ubuntu (native OS icons use
a penguin glyph fallback). Set accessibilityLabel only when the icon adds meaning
not already present in adjacent text. Optional size is a positive number.
Content is rendered as text and icons, never HTML. Plain navigation labels retain
the text representation; their separate icon property controls navigation icons.
