@webtypen/webframez-core
v0.3.60
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-coreAgent-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:
- bevorzugte 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:
- 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.
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 }
}
}
}
}
});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 }));
}
});
};