@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 +63 -1
- package/agents/webframez_rules.md +17 -3
- package/dist/DataBuilder/DataBuilder.d.ts +2 -0
- package/dist/DataBuilder/DataBuilder.js +4 -1
- package/dist/Database/DBConnection.js +4 -2
- package/dist/Database/Model.js +4 -3
- package/dist/LambdaApplication.js +1 -1
- package/dist/Presentation/InlineText.d.ts +9 -0
- package/dist/Presentation/InlineText.js +2 -0
- package/dist/Queue/AutomationSchedule.d.ts +2 -0
- package/dist/Queue/AutomationSchedule.js +17 -1
- package/dist/Queue/Queue.d.ts +13 -0
- package/dist/Queue/Queue.js +80 -0
- package/dist/Router/Response.d.ts +4 -2
- package/dist/Router/Response.js +46 -69
- package/dist/Router/Route.d.ts +4 -0
- package/dist/Router/Route.js +5 -0
- package/dist/Router/Router.js +5 -1
- package/dist/WebApplication.js +3 -3
- package/dist/index.d.ts +3 -0
- package/dist/index.js +2 -0
- package/dist/routing.d.ts +5 -0
- package/dist/routing.js +42 -0
- package/package.json +4 -2
- package/routing.d.ts +1 -0
- package/routing.js +2 -0
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
|
-
-
|
|
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.
|
|
47
|
-
5.
|
|
48
|
-
6.
|
|
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
|
-
|
|
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
|
|
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 (
|
|
78
|
+
if (data === null || data === undefined) {
|
|
77
79
|
return null;
|
|
78
80
|
}
|
|
79
81
|
// @ToDo: Model-Mapping
|
package/dist/Database/Model.js
CHANGED
|
@@ -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
|
|
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)[];
|
|
@@ -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<
|
|
71
|
-
download(filepath: string, options?: any): Promise<
|
|
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
|
/**
|
package/dist/Router/Response.js
CHANGED
|
@@ -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
|
-
|
|
135
|
-
|
|
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
|
-
|
|
138
|
-
let end =
|
|
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
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
this.header("Content-
|
|
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
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
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
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
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() {
|
package/dist/Router/Route.d.ts
CHANGED
|
@@ -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;
|
package/dist/Router/Route.js
CHANGED
|
@@ -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 [];
|
package/dist/Router/Router.js
CHANGED
|
@@ -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
|
|
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 =
|
package/dist/WebApplication.js
CHANGED
|
@@ -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
|
|
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:
|
|
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
|
-
(
|
|
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;
|
package/dist/routing.js
ADDED
|
@@ -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.
|
|
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