@webtypen/webframez-core 0.3.65 → 0.3.67

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -32,7 +32,8 @@ node_modules/@webtypen/webframez-core/agents/webframez_rules.md
32
32
  ```
33
33
 
34
34
  Die Regeln beschreiben unter anderem:
35
- - bevorzugte Nutzung von Model.objectId(...) statt eigener ObjectId-Resolver
35
+ - verbindliche Nutzung konkreter Model-Klassen fuer fachliche Datenzugriffe statt Collection-Wrappern
36
+ - Nutzung von Model.objectId(...) statt eigener ObjectId-Resolver
36
37
  - bevorzugte Nutzung der vorhandenen Helper wie StringFunctions, NumericFunctions und DateFunctions
37
38
  - Verwendung der Webframez-Abstraktionen fuer Request, Response, Middleware, Storage, Datatables und Routes
38
39
 
@@ -49,6 +50,8 @@ Uebernimm fuer alle Webframez-bezogenen Implementierungen die Konventionen aus:
49
50
  ./node_modules/@webtypen/webframez-core/agents/webframez_rules.md
50
51
 
51
52
  Insbesondere gilt:
53
+ - pro persistierter Entitaet eine Model-Klasse unter app/Models anlegen; Controller, Services und Screens greifen ueber diese Klassen zu
54
+ - keine DatabaseService.collection(...)-Wrapper oder eigene Cursor-/JSON-Persistenzschichten implementieren; fehlende atomare Operationen im Treiber ergaenzen
52
55
  - fuer ObjectId-Konvertierungen Model.objectId(...) oder die passende Model-Instanzmethode verwenden
53
56
  - vorhandene Webframez-Helper und Facades bevorzugen statt neue Utilities oder Resolver anzulegen
54
57
  - fuer HTTP-, Routing-, Middleware-, Storage- und Datatable-Code die Webframez-APIs und Konventionen beibehalten
@@ -60,6 +63,44 @@ Projektspezifische Ergaenzung:
60
63
 
61
64
  Wenn eine Regel aus der lokalen Projekt-AGENTS.md einer Regel aus der Paketdatei widerspricht, sollte die lokale Projektregel Vorrang haben.
62
65
 
66
+ ## Application basename
67
+
68
+ Set the public mount once in `config/application.ts`:
69
+
70
+ ```ts
71
+ export default {
72
+ router: {
73
+ get basename() { return process.env.BASENAME || ""; },
74
+ },
75
+ };
76
+ ```
77
+
78
+ Pass this config as `boot({ config: { application }, ... })`. For example,
79
+ `BASENAME=/my-app` mounts every registered route (including route groups and
80
+ module/API routes) below `/my-app`. Empty or `/` keeps the application at the
81
+ root. The legacy `boot({ basename })` option takes precedence when explicitly
82
+ provided, including `""` or `null`. The basename is normalized and validated at
83
+ boot; changing it requires restarting the application. Web and Lambda use the
84
+ same configuration.
85
+
86
+ `Route.path("/api/items")`, `appPath("/api/items")`, and
87
+ `res.redirect("/login")` include the basename. Already-prefixed URLs retain one
88
+ prefix, and external URLs, anchors, and relative URLs remain unchanged.
89
+ `Route.relativePath(...)` / `appRelativePath(...)` remove a matching basename
90
+ at a complete path boundary. Route registration always takes application-local
91
+ paths; do not pass `Route.path(...)` into `Route.get(...)`.
92
+
93
+ Node applications import these helpers from `@webtypen/webframez-core`.
94
+ Browser code can use the public, Node-free `@webtypen/webframez-core/routing`
95
+ entry. Rendering adapters provide the runtime basename to that shared API;
96
+ Core does not rewrite arbitrary HTML strings, native `fetch` calls or raw
97
+ `Location` headers. Use `appPath` for those application URLs.
98
+
99
+ `webframez-react` automatically adopts the Core basename for `Route.renderReact`,
100
+ including nested mounts and route groups. It publishes the basename for browser
101
+ navigation, forms and asset URLs. The build output directory remains independent
102
+ of the deployment path, so one artifact can be mounted at different basenames.
103
+
63
104
  ## Basic Web Setup
64
105
 
65
106
  ```ts
@@ -790,3 +831,24 @@ export const handler = (event: any, context: any) => {
790
831
  });
791
832
  };
792
833
  ```
834
+
835
+
836
+ ## Inline text and icons
837
+
838
+ `InlineTextDefinition` is a JSON-safe text contract exported by Webframez Core and
839
+ structurally compatible with Native Design. Strings and existing locale maps stay
840
+ valid. Arrays preserve order and explicit whitespace:
841
+
842
+ ```ts
843
+ [{ type: "icon", name: "ubuntu" }, " Ubuntu 24.04"]
844
+ ```
845
+
846
+ Suite screen titles/subtitles, Card/CardHeader, Alert and Text accept this content
847
+ (`SuiteTextDefinition`). In React, use `<InlineText value={content} />` in other
848
+ text slots; `ScreenTitle` also accepts the array directly. On React Native, place
849
+ InlineText inside Text. Icons use the registered Native Design Icon component.
850
+ Built-in names include `linux`, `linux-pinguin` and `ubuntu` (native OS icons use
851
+ a penguin glyph fallback). Set `accessibilityLabel` only when the icon adds meaning
852
+ not already present in adjacent text. Optional `size` is a positive number.
853
+ Content is rendered as text and icons, never HTML. Plain navigation labels retain
854
+ the text representation; their separate `icon` property controls navigation icons.
@@ -43,9 +43,13 @@ Vermeide Importe aus internen Pfaden wie src/..., dist/... oder einzelnen Deep-I
43
43
  1. Fuer ObjectId-Konvertierungen immer Model.objectId(...) oder die Instanzmethode objectId(...) verwenden, wenn man sich im Kontext eines Models befindet.
44
44
  2. Keine redundanten resolveObjectId, parseObjectId, normalizeObjectId, toObjectId oder aehnlichen Hilfsmethoden erzeugen, wenn objectId(...) ausreicht.
45
45
  3. Wenn kein konkretes Model passt, ist DBConnection.objectId(...) die zweite Wahl. Ein projektspezifischer Resolver ist nicht die erste Wahl.
46
- 4. Fuer Standardabfragen bevorzugt die vorhandenen Model-Methoden verwenden: where(...), orderBy(...), first(), get(), paginate(), aggregate().
47
- 5. Beziehungen ueber hasOne(...), hasMany(...) und hasManyArray(...) abbilden statt manuelle Lade- oder Resolve-Methoden pro Beziehung zu schreiben.
48
- 6. Model-Klassen sollen die Webframez-Konventionen fuer __table, __primaryKey und Mapping respektieren.
46
+ 4. Fachliche Datenzugriffe muessen ueber konkrete Model-Klassen erfolgen: pro persistierter Entitaet eine Klasse unter app/Models mit __table und gegebenenfalls __primaryKey / __connection. Models von Anfang an anlegen und in Controllern, Services, Screens und Formular-Hooks verwenden. Keine DatabaseService.collection(...)-Wrapper und keine eigenen Collection-/Cursor-/JSON-Persistenzschichten im Projekt.
47
+ 5. Fuer Abfragen die Model-Methoden where(...), orderBy(...), first(), get(), paginate(), aggregate() verwenden. Fuer Einzelobjekte save(), update() und delete() verwenden. Vorbelegte IDs bei neuen Objekten beachten: Core Model.save() behandelt einen vorhandenen Primaerschluessel als Update; eine dokumentierte Insert-/create-Methode des Treibers nutzen, wenn die ID vor dem Insert feststeht.
48
+ 6. Falls eine notwendige Faehigkeit fehlt (z.B. atomare bedingte Updates, Upserts, Projektionen oder Bulk-Inserts), diese generisch im Datenbanktreiber bzw. Framework ergaenzen. Keine Read-Modify-Save-Schleifen fuer Token-Rotation, Zaehler oder einmalige Codes; Filter und Aenderung muessen atomar ausgefuehrt werden.
49
+ 7. Direkte DBConnection-/Treiberzugriffe sind auf Verbindungsverwaltung, Migrationen und Framework-Adapter begrenzt. Wenn ein bestehender Framework-Controller einen Datenbank-Handle verlangt, diesen nur an der Adaptergrenze weiterreichen. Geschaeftslogik bleibt bei Models.
50
+ 8. Model-Mapping, Projekt-/Tenantfilter, versteckte Felder, Datentypen und bestehende Daten beim Wechsel des Treibers testen. Raw-Aggregationen liefern Datenobjekte; normale Model-Abfragen liefern Model-Instanzen.
51
+ 9. Beziehungen ueber hasOne(...), hasMany(...) und hasManyArray(...) abbilden statt manuelle Lade- oder Resolve-Methoden pro Beziehung zu schreiben.
52
+ 10. Model-Klassen sollen die Webframez-Konventionen fuer __table, __primaryKey und Mapping respektieren.
49
53
 
50
54
  Bevorzugt:
51
55
 
@@ -234,3 +238,13 @@ Wenn Codex zwischen Eigenbau und Framework-Funktion waehlen muss, gilt standardm
234
238
  3. Erst zuletzt eine neue Abstraktion einfuehren.
235
239
 
236
240
  Wenn eine neue Abstraktion doch noetig ist, muss im Code klar erkennbar sein, warum die vorhandene Webframez-Funktion nicht ausreicht.
241
+
242
+ ## Suite navigation in generated projects
243
+
244
+ Use actual router Link components for navigation, with href and browser modifiers
245
+ preserved. Keep the Suite provider, renderer and AdminLayout mounted within a
246
+ project; never key them by pathname or hide them behind per-page context loading.
247
+ Key only changing page content, and reset the provider at project/account boundaries.
248
+ Configure Native Design's `suite.navigation.linkComponent` at the application
249
+ boundary. Keep server schemas serializable and authorization checks on every API.
250
+ Verify persistent layout state during navigation, not just HTTP status codes.
@@ -8,6 +8,8 @@ export type DataBuilderSchema = {
8
8
  afterSave?: any;
9
9
  beforeDelete?: any;
10
10
  afterDelete?: any;
11
+ /** Override persistence for nested/model-backed records; authorization and hooks still run. */
12
+ deleteHandler?: (element: any, req: Request) => Promise<void>;
11
13
  getAggregation?: any;
12
14
  events?: {
13
15
  [key: string]: any;
@@ -553,7 +553,10 @@ class DataBuilder {
553
553
  catch (e) {
554
554
  throw e;
555
555
  }
556
- yield db.collection(collection).deleteOne({ _id: element._id });
556
+ if (typeof type.schema.deleteHandler === "function")
557
+ yield type.schema.deleteHandler(element, req);
558
+ else
559
+ yield db.collection(collection).deleteOne({ _id: element._id });
557
560
  try {
558
561
  if (typeof type.schema.afterDelete === "function") {
559
562
  yield type.schema.afterDelete(element, req);
@@ -68,12 +68,14 @@ class DBConnectionFacade {
68
68
  }
69
69
  runQuery(query, options) {
70
70
  return __awaiter(this, void 0, void 0, function* () {
71
- const connection = yield this.getConnection();
71
+ const mapping = query.modelMapping;
72
+ const model = typeof mapping === "function" ? new mapping() : mapping;
73
+ const connection = yield this.getConnection(model === null || model === void 0 ? void 0 : model.__connection);
72
74
  const data = yield connection.driver.handleQueryBuilder(connection.client, query);
73
75
  if (options && options.raw) {
74
76
  return data;
75
77
  }
76
- if (!data) {
78
+ if (data === null || data === undefined) {
77
79
  return null;
78
80
  }
79
81
  // @ToDo: Model-Mapping
@@ -299,15 +299,16 @@ class Model {
299
299
  primaryKey: this.__primaryKey,
300
300
  filter: { [this.__primaryKey]: this[this.__primaryKey] },
301
301
  data: this.getModelData(),
302
- });
302
+ }, this.__connection);
303
303
  }
304
304
  else {
305
305
  // Insert
306
306
  status = yield DBConnection_1.DBConnection.execute({
307
307
  type: "insertOne",
308
308
  table: this.__table,
309
+ primaryKey: this.__primaryKey,
309
310
  data: this.getModelData(),
310
- });
311
+ }, this.__connection);
311
312
  }
312
313
  // onModelSave
313
314
  const driver = yield DBConnection_1.DBConnection.getDriver(this.__connection);
@@ -329,7 +330,7 @@ class Model {
329
330
  primaryKey: this.__primaryKey,
330
331
  filter: { [this.__primaryKey]: this[this.__primaryKey] },
331
332
  data: updateData,
332
- });
333
+ }, this.__connection);
333
334
  });
334
335
  }
335
336
  /**
@@ -37,7 +37,7 @@ class LambdaApplication {
37
37
  Router_1.Router.init({
38
38
  mode: "aws-lambda",
39
39
  kernel: options && options.kernel ? options.kernel : null,
40
- basename: options && options.basename ? options.basename : null,
40
+ basename: options === null || options === void 0 ? void 0 : options.basename,
41
41
  routesFunction: options && options.routesFunction ? options.routesFunction : null,
42
42
  tempDir: options && options.tempDir ? options.tempDir : null,
43
43
  });
@@ -0,0 +1,9 @@
1
+ /** JSON-safe inline content, structurally shared with Native Design/Webframez Core. */
2
+ export type InlineTextIcon = {
3
+ type: "icon";
4
+ name: string;
5
+ /** Omit for decorative icons next to equivalent text. */
6
+ accessibilityLabel?: string;
7
+ size?: number;
8
+ };
9
+ export type InlineTextDefinition = string | Record<string, string> | readonly (string | Record<string, string> | InlineTextIcon)[];
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -8,3 +8,5 @@ export type DueAutomationExecution = {
8
8
  };
9
9
  export declare function getDueAutomationExecutions(executions: any[], since: any, until: any, timezone?: string, identifier?: string | null): DueAutomationExecution[];
10
10
  export declare function matchesAutomationExecutions(executions: any[] | undefined, date: Date | string | undefined, timezone?: string): boolean;
11
+ /** Next wall-clock automation occurrence, preserving the scheduler's timezone/DST semantics. */
12
+ export declare function nextAutomationExecution(executions: any[], after: Date | string, timezone?: string): Date | null;
@@ -3,7 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.matchesAutomationExecutions = exports.getDueAutomationExecutions = void 0;
6
+ exports.nextAutomationExecution = exports.matchesAutomationExecutions = exports.getDueAutomationExecutions = void 0;
7
7
  const moment_timezone_1 = __importDefault(require("moment-timezone"));
8
8
  const DateFunctions_1 = require("../Functions/DateFunctions");
9
9
  function pushIfDue(out, entry, since, until) {
@@ -124,3 +124,19 @@ function matchesAutomationExecutions(executions, date, timezone = "Europe/Berlin
124
124
  return getDueAutomationExecutions(executions, since, until, timezone).some((execution) => execution.dueAt.isSame(dueAt));
125
125
  }
126
126
  exports.matchesAutomationExecutions = matchesAutomationExecutions;
127
+ /** Next wall-clock automation occurrence, preserving the scheduler's timezone/DST semantics. */
128
+ function nextAutomationExecution(executions, after, timezone = "Europe/Berlin") {
129
+ const since = (0, moment_timezone_1.default)(after).tz(timezone);
130
+ if (!since.isValid() || !moment_timezone_1.default.tz.zone(timezone))
131
+ throw new Error("Invalid automation time or timezone");
132
+ // Short windows avoid materializing months of minute/hourly occurrences just to find one.
133
+ const windows = [[1, "minutes"], [1, "hours"], [1, "days"], [8, "days"], [2, "months"]];
134
+ for (const [amount, unit] of windows) {
135
+ const due = getDueAutomationExecutions(executions, since, since.clone().add(amount, unit), timezone);
136
+ const first = due.reduce((best, entry) => !best || entry.dueAt.isBefore(best.dueAt) ? entry : best, null);
137
+ if (first)
138
+ return first.dueAt.toDate();
139
+ }
140
+ return null;
141
+ }
142
+ exports.nextAutomationExecution = nextAutomationExecution;
@@ -0,0 +1,13 @@
1
+ /** Embedded worker for applications whose database must stay in one process (e.g. DuckDB).
2
+ * Uses the same queue_jobs records and BaseQueueJob handlers as console workers. */
3
+ export declare class Queue {
4
+ private static timer;
5
+ private static running;
6
+ private static abort;
7
+ private static worker;
8
+ private static collection;
9
+ static enqueue(jobclass: string, props?: Record<string, any>): Promise<any>;
10
+ static start(jobclasses: string[], intervalMs?: number): void;
11
+ static stop(): Promise<void>;
12
+ static runOnce(jobclasses: string[]): Promise<void>;
13
+ }
@@ -0,0 +1,80 @@
1
+ "use strict";
2
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
+ return new (P || (P = Promise))(function (resolve, reject) {
5
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
9
+ });
10
+ };
11
+ var __importDefault = (this && this.__importDefault) || function (mod) {
12
+ return (mod && mod.__esModule) ? mod : { "default": mod };
13
+ };
14
+ Object.defineProperty(exports, "__esModule", { value: true });
15
+ exports.Queue = void 0;
16
+ const crypto_1 = __importDefault(require("crypto"));
17
+ const DBConnection_1 = require("../Database/DBConnection");
18
+ const QueueJobsRegisty_1 = require("./QueueJobsRegisty");
19
+ /** Embedded worker for applications whose database must stay in one process (e.g. DuckDB).
20
+ * Uses the same queue_jobs records and BaseQueueJob handlers as console workers. */
21
+ class Queue {
22
+ static collection() {
23
+ return __awaiter(this, void 0, void 0, function* () { return (yield DBConnection_1.DBConnection.getConnection()).client.db(null).collection("queue_jobs"); });
24
+ }
25
+ static enqueue(jobclass, props = {}) {
26
+ return __awaiter(this, void 0, void 0, function* () {
27
+ QueueJobsRegisty_1.QueueJobsRegisty.getJobOrFail(jobclass);
28
+ const jobs = yield this.collection(), id = props._id || crypto_1.default.randomUUID();
29
+ yield jobs.updateOne({ _id: id }, { $setOnInsert: Object.assign(Object.assign({}, props), { _id: id, jobclass, status: "pending", created_at: new Date(), not_before: null, worker: null }) }, { upsert: true });
30
+ return jobs.findOne({ _id: id });
31
+ });
32
+ }
33
+ static start(jobclasses, intervalMs = 1000) {
34
+ if (this.timer)
35
+ return;
36
+ const tick = () => { if (!this.running)
37
+ this.running = this.runOnce(jobclasses).catch(() => { }).finally(() => { this.running = undefined; }); };
38
+ this.timer = setInterval(tick, intervalMs);
39
+ this.timer.unref();
40
+ tick();
41
+ }
42
+ static stop() {
43
+ var _a;
44
+ return __awaiter(this, void 0, void 0, function* () { if (this.timer)
45
+ clearInterval(this.timer); this.timer = undefined; (_a = this.abort) === null || _a === void 0 ? void 0 : _a.abort(); yield this.running; });
46
+ }
47
+ static runOnce(jobclasses) {
48
+ return __awaiter(this, void 0, void 0, function* () {
49
+ const jobs = yield this.collection(), now = new Date();
50
+ yield jobs.updateMany({ status: "running", embedded_queue: true, lease_until: { $lt: now } }, { $set: { status: "pending", worker: null, recovered: true } });
51
+ const token = crypto_1.default.randomUUID();
52
+ const claimed = yield jobs.findOneAndUpdate({ status: "pending", jobclass: { $in: jobclasses }, $or: [{ not_before: null }, { not_before: { $lte: now } }] }, { $set: { status: "running", worker: this.worker, embedded_queue: true, lease_token: token, lease_until: new Date(Date.now() + 60000), started_at: now } }, { sort: { priority: -1, created_at: 1 }, returnDocument: "after" });
53
+ const job = (claimed === null || claimed === void 0 ? void 0 : claimed._id) ? claimed : claimed === null || claimed === void 0 ? void 0 : claimed.value;
54
+ if (!job)
55
+ return;
56
+ const filter = { _id: job._id, lease_token: token, status: "running" };
57
+ const controller = new AbortController();
58
+ this.abort = controller;
59
+ const heartbeat = setInterval(() => { void jobs.updateOne(filter, { $set: { lease_until: new Date(Date.now() + 60000) } }).then((result) => { if (!result.matchedCount)
60
+ controller.abort(); }).catch(() => controller.abort()); }, 15000);
61
+ heartbeat.unref();
62
+ try {
63
+ const Handler = QueueJobsRegisty_1.QueueJobsRegisty.getJobOrFail(job.jobclass), handler = new Handler();
64
+ const result = yield handler.handle(Object.assign(Object.assign({}, job), { signal: controller.signal }));
65
+ yield jobs.updateOne(filter, { $set: (result === null || result === void 0 ? void 0 : result.__execute_again) ? { status: "pending", not_before: result.__execute_again, worker: null } : { status: "finished", ended_at: new Date(), worker: null } });
66
+ }
67
+ catch (error) {
68
+ // Handler owns log redaction. Do not persist raw exception output or credentials here.
69
+ yield jobs.updateOne(filter, { $set: { status: "failed", ended_at: new Date(), worker: null, error: "Queue job failed. See the domain status for details." } });
70
+ }
71
+ finally {
72
+ clearInterval(heartbeat);
73
+ if (this.abort === controller)
74
+ this.abort = undefined;
75
+ }
76
+ });
77
+ }
78
+ }
79
+ exports.Queue = Queue;
80
+ Queue.worker = "embedded-" + crypto_1.default.randomUUID();
@@ -20,6 +20,8 @@ export declare class Response {
20
20
  * @returns Response
21
21
  */
22
22
  setServerResponse(res: ServerResponse): this;
23
+ /** Redirect to an application URL, retaining external destinations unchanged. */
24
+ redirect(location: string, status?: number): Response;
23
25
  /**
24
26
  * Set the http-status-code
25
27
  *
@@ -67,8 +69,8 @@ export declare class Response {
67
69
  * @param mimeType
68
70
  * @returns void
69
71
  */
70
- stream(req: Request, filepath: string, filename: string, mimeType: string): Promise<unknown>;
71
- download(filepath: string, options?: any): Promise<unknown>;
72
+ stream(req: Request, filepath: string, filename: string, mimeType: string): Promise<Response | undefined>;
73
+ download(filepath: string, options?: any): Promise<void>;
72
74
  end(): this;
73
75
  registerEvent(eventKey: string, func: any): Promise<void>;
74
76
  /**
@@ -13,7 +13,9 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
13
13
  };
14
14
  Object.defineProperty(exports, "__esModule", { value: true });
15
15
  exports.Response = void 0;
16
+ const routing_1 = require("../routing");
16
17
  const fs_1 = __importDefault(require("fs"));
18
+ const promises_1 = require("stream/promises");
17
19
  const path_1 = __importDefault(require("path"));
18
20
  const mime_types_1 = __importDefault(require("mime-types"));
19
21
  class Response {
@@ -36,6 +38,10 @@ class Response {
36
38
  this.res = res;
37
39
  return this;
38
40
  }
41
+ /** Redirect to an application URL, retaining external destinations unchanged. */
42
+ redirect(location, status = 302) {
43
+ return this.status(status).header("Location", (0, routing_1.appPath)(location)).send("");
44
+ }
39
45
  /**
40
46
  * Set the http-status-code
41
47
  *
@@ -129,83 +135,54 @@ class Response {
129
135
  * @returns void
130
136
  */
131
137
  stream(req, filepath, filename, mimeType) {
132
- var _a;
138
+ var _a, _b;
133
139
  return __awaiter(this, void 0, void 0, function* () {
134
- const stats = fs_1.default.statSync(filepath);
135
- const range = ((_a = req.req) === null || _a === void 0 ? void 0 : _a.headers.range) || "";
140
+ if (!this.res)
141
+ throw new Error("Native Response-Objekt nicht verfügbar");
142
+ const stats = yield fs_1.default.promises.stat(filepath);
136
143
  const total = stats.size;
137
- let start = undefined;
138
- let end = undefined;
144
+ const range = String(((_a = req.headers) === null || _a === void 0 ? void 0 : _a.range) || ((_b = req.message) === null || _b === void 0 ? void 0 : _b.headers.range) || "");
145
+ let start = 0, end = total - 1;
139
146
  if (range) {
140
- var parts = range.replace(/bytes=/, "").split("-");
141
- var partialstart = parts[0];
142
- var partialend = parts[1];
143
- start = parseInt(partialstart, 10);
144
- end = partialend ? parseInt(partialend, 10) : total - 1;
145
- var chunksize = end - start + 1;
146
- this.status(206);
147
- this.header("Content-Range", "bytes " + start + "-" + end + "/" + total);
148
- this.header("Accept-Ranges", "bytes");
149
- this.header("Content-Length", chunksize.toString());
150
- this.header("Content-Type", mimeType);
151
- this.header("Content-Disposition", `attachment; filename="${filename}"`);
152
- }
153
- else {
154
- this.status(200);
155
- this.header("Accept-Ranges", "bytes");
156
- this.header("Content-Length", stats.size.toString());
157
- this.header("Content-Type", mimeType);
158
- this.header("Content-Disposition", `attachment; filename="${filename}"`);
159
- }
160
- // @ts-ignore
161
- var readStream = fs_1.default.createReadStream(filepath, { start: start, end: end });
162
- readStream.on("error", (streamError) => {
163
- console.error("WebframezStreamError:", streamError);
164
- });
165
- readStream.on("end", () => { });
166
- // Prüfe ob res.res verfügbar ist
167
- if (!this.res) {
168
- throw new Error("Native Response-Objekt nicht verfügbar");
147
+ const match = /^bytes=(\d*)-(\d*)$/.exec(range);
148
+ if (!match || (!match[1] && !match[2])) {
149
+ return this.status(416).header("Content-Range", `bytes */${total}`).end();
150
+ }
151
+ if (!match[1]) {
152
+ const suffix = Number(match[2]);
153
+ start = Math.max(0, total - suffix);
154
+ if (suffix === 0)
155
+ start = total;
156
+ }
157
+ else {
158
+ start = Number(match[1]);
159
+ end = match[2] ? Math.min(Number(match[2]), total - 1) : total - 1;
160
+ }
161
+ if (!Number.isSafeInteger(start) || !Number.isSafeInteger(end) || start >= total || end < start) {
162
+ return this.status(416).header("Content-Range", `bytes */${total}`).end();
163
+ }
164
+ this.status(206).header("Content-Range", `bytes ${start}-${end}/${total}`);
169
165
  }
170
- readStream.pipe(this.res);
171
- return new Promise((resolve) => {
172
- readStream.on("end", resolve);
173
- readStream.on("error", resolve);
174
- });
166
+ this.header("Accept-Ranges", "bytes");
167
+ this.header("Content-Length", String(range ? end - start + 1 : total));
168
+ this.header("Content-Type", mimeType);
169
+ this.header("Content-Disposition", `attachment; filename="${filename.replace(/["\r\n\\]/g, "_")}"`);
170
+ if (total === 0)
171
+ return this.end();
172
+ yield (0, promises_1.pipeline)(fs_1.default.createReadStream(filepath, { start, end }), this.res);
175
173
  });
176
174
  }
177
175
  download(filepath, options) {
178
176
  return __awaiter(this, void 0, void 0, function* () {
179
- return new Promise((resolve, reject) => {
180
- if (!this.res) {
181
- return reject("Cannot download: Original response object is missing");
182
- }
183
- if (!fs_1.default.existsSync(filepath)) {
184
- return reject("Cannot download: File '" + filepath + "' does not exist");
185
- }
186
- let mimeType = options && options.contentType ? options.contentType : "application/octet-stream";
187
- if (options && options.inline) {
188
- let tempMime = mime_types_1.default.lookup(filepath);
189
- if (tempMime) {
190
- mimeType = tempMime;
191
- }
192
- }
193
- const stats = fs_1.default.lstatSync(filepath);
194
- this.header("Content-Disposition", (options && options.inline ? "inline" : "attachment") +
195
- "; filename=" +
196
- (options && options.filename ? options.filename : path_1.default.basename(filepath)));
197
- this.header("Content-Type", mimeType);
198
- this.header("Content-Length", stats.size.toString());
199
- const readStream = fs_1.default.createReadStream(filepath);
200
- readStream.pipe(this.res);
201
- readStream.on("close", () => {
202
- resolve();
203
- });
204
- readStream.on("error", (streamErr) => {
205
- console.error(streamErr);
206
- reject(streamErr);
207
- });
208
- });
177
+ if (!this.res)
178
+ throw new Error("Cannot download: Original response object is missing");
179
+ const stats = yield fs_1.default.promises.stat(filepath);
180
+ const mimeType = (options === null || options === void 0 ? void 0 : options.contentType) || ((options === null || options === void 0 ? void 0 : options.inline) && mime_types_1.default.lookup(filepath)) || "application/octet-stream";
181
+ const filename = String((options === null || options === void 0 ? void 0 : options.filename) || path_1.default.basename(filepath)).replace(/["\r\n\\]/g, "_");
182
+ this.header("Content-Disposition", `${(options === null || options === void 0 ? void 0 : options.inline) ? "inline" : "attachment"}; filename="${filename}"`);
183
+ this.header("Content-Type", mimeType);
184
+ this.header("Content-Length", String(stats.size));
185
+ yield (0, promises_1.pipeline)(fs_1.default.createReadStream(filepath), this.res);
209
186
  });
210
187
  }
211
188
  end() {
@@ -3,6 +3,10 @@ type RouteRegistrationOptions = {
3
3
  };
4
4
  type RouteExtensionFactory = (route: RouteFacade) => (...args: any[]) => any;
5
5
  export declare class RouteFacade {
6
+ get basename(): string;
7
+ /** Public application URL, including the configured router basename. */
8
+ path(value: string): string;
9
+ relativePath(value: string): string;
6
10
  tempGroupPrefix: string | null;
7
11
  tempGroupMiddleware: string[] | null;
8
12
  tempGroupDomains: string[] | null;
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.Route = exports.RouteFacade = void 0;
4
+ const routing_1 = require("../routing");
4
5
  const Router_1 = require("./Router");
5
6
  class RouteFacade {
6
7
  constructor() {
@@ -24,6 +25,10 @@ class RouteFacade {
24
25
  Router_1.Router.register(method, (this.tempGroupPrefix ? this.tempGroupPrefix : "") + path, component, mergedOptions);
25
26
  };
26
27
  }
28
+ get basename() { return Router_1.Router.basename || ""; }
29
+ /** Public application URL, including the configured router basename. */
30
+ path(value) { return (0, routing_1.appPath)(value, this.basename); }
31
+ relativePath(value) { return (0, routing_1.appRelativePath)(value, this.basename); }
27
32
  normalizeStringArray(values) {
28
33
  if (!Array.isArray(values)) {
29
34
  return [];
@@ -13,6 +13,8 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
13
13
  };
14
14
  Object.defineProperty(exports, "__esModule", { value: true });
15
15
  exports.Router = void 0;
16
+ const Config_1 = require("../Config");
17
+ const routing_1 = require("../routing");
16
18
  const http_1 = require("http");
17
19
  const Response_1 = require("./Response");
18
20
  const Request_1 = require("./Request");
@@ -72,7 +74,9 @@ class RouterFacade {
72
74
  * Load the application-routes
73
75
  */
74
76
  init(options) {
75
- this.basename = options && options.basename ? options.basename : null;
77
+ this.basename = (0, routing_1.normalizeBasename)((options === null || options === void 0 ? void 0 : options.basename) !== undefined
78
+ ? options.basename : Config_1.Config.get("application.router.basename"));
79
+ globalThis.__WEBFRAMEZ_ROUTER_BASENAME__ = this.basename;
76
80
  this.kernel = options && options.kernel ? options.kernel : null;
77
81
  this.mode = options && options.mode ? options.mode : null;
78
82
  this.maxRequestBodySizeBytes =
@@ -71,7 +71,7 @@ class WebApplication {
71
71
  Router_1.Router.init({
72
72
  mode: options && options.mode ? options.mode : null,
73
73
  kernel: options && options.kernel ? options.kernel : null,
74
- basename: options && options.basename ? options.basename : null,
74
+ basename: options === null || options === void 0 ? void 0 : options.basename,
75
75
  routesFunction: options && options.routesFunction ? options.routesFunction : null,
76
76
  tempDir: options && options.tempDir ? options.tempDir : null,
77
77
  });
@@ -94,7 +94,7 @@ class WebApplication {
94
94
  res,
95
95
  kernel: options && options.kernel ? options.kernel : null,
96
96
  modulesLoader: this.modulesLoader,
97
- basename: options && options.basename ? options.basename : null,
97
+ basename: Router_1.Router.basename,
98
98
  options,
99
99
  });
100
100
  if (handled) {
@@ -149,7 +149,7 @@ class WebApplication {
149
149
  if (runtimeConsole && typeof runtimeConsole.log === "function") {
150
150
  runtimeConsole.log("Server started and listening on port " +
151
151
  port +
152
- (options && options.basename ? " (Basename: " + options && options.basename + ")" : ""));
152
+ (Router_1.Router.basename ? " (Basename: " + Router_1.Router.basename + ")" : ""));
153
153
  }
154
154
  if (options && options.onBoot) {
155
155
  options.onBoot(port);
package/dist/index.d.ts CHANGED
@@ -61,3 +61,6 @@ export * from "./Notifications/NotificationService";
61
61
  export * from "./Notifications/NotificationsOutputChannel";
62
62
  export * from "./Notifications/NotificationsOutputChannelsJob";
63
63
  export * from "./info";
64
+ export type { InlineTextDefinition, InlineTextIcon } from "./Presentation/InlineText";
65
+ export * from "./Queue/Queue";
66
+ export * from "./routing";
package/dist/index.js CHANGED
@@ -77,3 +77,5 @@ __exportStar(require("./Notifications/NotificationService"), exports);
77
77
  __exportStar(require("./Notifications/NotificationsOutputChannel"), exports);
78
78
  __exportStar(require("./Notifications/NotificationsOutputChannelsJob"), exports);
79
79
  __exportStar(require("./info"), exports);
80
+ __exportStar(require("./Queue/Queue"), exports);
81
+ __exportStar(require("./routing"), exports);
@@ -0,0 +1,5 @@
1
+ export declare function normalizeBasename(value?: string | null): string;
2
+ export declare function getBasename(): string;
3
+ /** Prefix application-absolute URLs once; preserve external and relative URLs. */
4
+ export declare function appPath(value: string, basename?: string): string;
5
+ export declare function appRelativePath(value: string, basename?: string): string;
@@ -0,0 +1,42 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.appRelativePath = exports.appPath = exports.getBasename = exports.normalizeBasename = void 0;
4
+ function normalizeBasename(value) {
5
+ if (value == null || value === "" || value === "/")
6
+ return "";
7
+ if (typeof value !== "string")
8
+ throw new Error("Router basename must be a string.");
9
+ const base = value.trim().replace(/\/+$/, "");
10
+ if (!base && value.trim().length <= 1)
11
+ return "";
12
+ if (!/^\/(?:[A-Za-z0-9_~-]+(?:\.[A-Za-z0-9_~-]+)*)(?:\/[A-Za-z0-9_~-]+(?:\.[A-Za-z0-9_~-]+)*)*$/.test(base)) {
13
+ throw new Error("Router basename must be empty or an absolute path such as /my-app.");
14
+ }
15
+ return base;
16
+ }
17
+ exports.normalizeBasename = normalizeBasename;
18
+ function getBasename() {
19
+ var _a, _b, _c, _d;
20
+ const runtime = globalThis;
21
+ return (_d = (_c = (_b = (_a = runtime.__WEBFRAMEZ_ROUTING_CONTEXT__) === null || _a === void 0 ? void 0 : _a.getStore()) !== null && _b !== void 0 ? _b : runtime.__RSC_BASENAME) !== null && _c !== void 0 ? _c : runtime.__WEBFRAMEZ_ROUTER_BASENAME__) !== null && _d !== void 0 ? _d : "";
22
+ }
23
+ exports.getBasename = getBasename;
24
+ /** Prefix application-absolute URLs once; preserve external and relative URLs. */
25
+ function appPath(value, basename = getBasename()) {
26
+ const base = normalizeBasename(basename);
27
+ if (!base || !value.startsWith("/") || value.startsWith("//") || hasBasename(value, base))
28
+ return value;
29
+ return base + value;
30
+ }
31
+ exports.appPath = appPath;
32
+ function appRelativePath(value, basename = getBasename()) {
33
+ const base = normalizeBasename(basename);
34
+ if (!base || !hasBasename(value, base))
35
+ return value;
36
+ const relative = value.slice(base.length);
37
+ return !relative || relative.startsWith("?") || relative.startsWith("#") ? "/" + relative : relative;
38
+ }
39
+ exports.appRelativePath = appRelativePath;
40
+ function hasBasename(value, base) {
41
+ return value === base || value.startsWith(base + "/") || value.startsWith(base + "?") || value.startsWith(base + "#");
42
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@webtypen/webframez-core",
3
- "version": "0.3.65",
3
+ "version": "0.3.67",
4
4
  "description": "The core package of the typescript based webframez (web-framework) by webtypen.de",
5
5
  "homepage": "https://webtypen.de/",
6
6
  "license": "MIT",
@@ -24,7 +24,9 @@
24
24
  "dist",
25
25
  "bin",
26
26
  "README.md",
27
- "agents"
27
+ "agents",
28
+ "routing.js",
29
+ "routing.d.ts"
28
30
  ],
29
31
  "scripts": {
30
32
  "build": "tsc",
package/routing.d.ts ADDED
@@ -0,0 +1 @@
1
+ export { normalizeBasename, getBasename, appPath, appRelativePath } from './dist/routing';
package/routing.js ADDED
@@ -0,0 +1,2 @@
1
+ // Public browser-safe entry point. Do not import the Node server bundle here.
2
+ module.exports = require('./dist/routing.js');