@webtypen/webframez-core 0.3.64 → 0.3.66
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 +25 -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/Datatable/Datatable.d.ts +13 -0
- package/dist/Datatable/Datatable.js +84 -0
- package/dist/Datatable/DatatableController.js +12 -0
- 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 +2 -2
- package/dist/Router/Response.js +41 -69
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1 -0
- package/package.json +1 -1
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
|
|
@@ -790,3 +793,24 @@ export const handler = (event: any, context: any) => {
|
|
|
790
793
|
});
|
|
791
794
|
};
|
|
792
795
|
```
|
|
796
|
+
|
|
797
|
+
|
|
798
|
+
## Inline text and icons
|
|
799
|
+
|
|
800
|
+
`InlineTextDefinition` is a JSON-safe text contract exported by Webframez Core and
|
|
801
|
+
structurally compatible with Native Design. Strings and existing locale maps stay
|
|
802
|
+
valid. Arrays preserve order and explicit whitespace:
|
|
803
|
+
|
|
804
|
+
```ts
|
|
805
|
+
[{ type: "icon", name: "ubuntu" }, " Ubuntu 24.04"]
|
|
806
|
+
```
|
|
807
|
+
|
|
808
|
+
Suite screen titles/subtitles, Card/CardHeader, Alert and Text accept this content
|
|
809
|
+
(`SuiteTextDefinition`). In React, use `<InlineText value={content} />` in other
|
|
810
|
+
text slots; `ScreenTitle` also accepts the array directly. On React Native, place
|
|
811
|
+
InlineText inside Text. Icons use the registered Native Design Icon component.
|
|
812
|
+
Built-in names include `linux`, `linux-pinguin` and `ubuntu` (native OS icons use
|
|
813
|
+
a penguin glyph fallback). Set `accessibilityLabel` only when the icon adds meaning
|
|
814
|
+
not already present in adjacent text. Optional `size` is a positive number.
|
|
815
|
+
Content is rendered as text and icons, never HTML. Plain navigation labels retain
|
|
816
|
+
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
|
/**
|
|
@@ -1,4 +1,14 @@
|
|
|
1
1
|
import { Request } from "../Router/Request";
|
|
2
|
+
export interface DatatableModelSelectorConfig {
|
|
3
|
+
value?: string;
|
|
4
|
+
label?: string;
|
|
5
|
+
subtitle?: string;
|
|
6
|
+
}
|
|
7
|
+
export interface NormalizedDatatableModelSelectorConfig {
|
|
8
|
+
value: string;
|
|
9
|
+
label: string;
|
|
10
|
+
subtitle?: string;
|
|
11
|
+
}
|
|
2
12
|
export declare class Datatable {
|
|
3
13
|
collection: string | Function;
|
|
4
14
|
aggregation: {
|
|
@@ -26,6 +36,7 @@ export declare class Datatable {
|
|
|
26
36
|
defaultUnwind?: string | null;
|
|
27
37
|
disablePerPageConfig: boolean;
|
|
28
38
|
selectable: boolean;
|
|
39
|
+
modelSelector: boolean | DatatableModelSelectorConfig;
|
|
29
40
|
selectableFunctions: {
|
|
30
41
|
[key: string]: {
|
|
31
42
|
label: string;
|
|
@@ -38,6 +49,8 @@ export declare class Datatable {
|
|
|
38
49
|
};
|
|
39
50
|
} | null;
|
|
40
51
|
getInit(req: Request): Promise<any>;
|
|
52
|
+
getModelSelectorConfig(): NormalizedDatatableModelSelectorConfig | null;
|
|
53
|
+
getModelSelectorPreview(req: Request): Promise<any>;
|
|
41
54
|
getSelectableFunctionsDef(req: Request): Promise<{
|
|
42
55
|
label: string;
|
|
43
56
|
icon: string | undefined;
|
|
@@ -8,12 +8,24 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
|
|
|
8
8
|
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
9
9
|
});
|
|
10
10
|
};
|
|
11
|
+
var __rest = (this && this.__rest) || function (s, e) {
|
|
12
|
+
var t = {};
|
|
13
|
+
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
|
|
14
|
+
t[p] = s[p];
|
|
15
|
+
if (s != null && typeof Object.getOwnPropertySymbols === "function")
|
|
16
|
+
for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
|
|
17
|
+
if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
|
|
18
|
+
t[p[i]] = s[p[i]];
|
|
19
|
+
}
|
|
20
|
+
return t;
|
|
21
|
+
};
|
|
11
22
|
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
12
23
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
13
24
|
};
|
|
14
25
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
26
|
exports.Datatable = void 0;
|
|
16
27
|
const moment_1 = __importDefault(require("moment"));
|
|
28
|
+
const mongodb_1 = require("mongodb");
|
|
17
29
|
const DBConnection_1 = require("../Database/DBConnection");
|
|
18
30
|
const NumericFunctions_1 = require("../Functions/NumericFunctions");
|
|
19
31
|
class Datatable {
|
|
@@ -32,6 +44,7 @@ class Datatable {
|
|
|
32
44
|
this.defaultUnwind = null;
|
|
33
45
|
this.disablePerPageConfig = false;
|
|
34
46
|
this.selectable = false;
|
|
47
|
+
this.modelSelector = false;
|
|
35
48
|
this.selectableFunctions = null;
|
|
36
49
|
}
|
|
37
50
|
getInit(req) {
|
|
@@ -41,6 +54,10 @@ class Datatable {
|
|
|
41
54
|
selectable: this.selectable ? true : false,
|
|
42
55
|
selectableFunctions: yield this.getSelectableFunctionsDef(req),
|
|
43
56
|
};
|
|
57
|
+
const modelSelector = this.getModelSelectorConfig();
|
|
58
|
+
if (modelSelector) {
|
|
59
|
+
out.modelSelector = modelSelector;
|
|
60
|
+
}
|
|
44
61
|
const promises = [];
|
|
45
62
|
promises.push(new Promise((resolve) => __awaiter(this, void 0, void 0, function* () {
|
|
46
63
|
out.filter = yield this.getFilter(req);
|
|
@@ -78,6 +95,73 @@ class Datatable {
|
|
|
78
95
|
return out;
|
|
79
96
|
});
|
|
80
97
|
}
|
|
98
|
+
getModelSelectorConfig() {
|
|
99
|
+
if (!this.modelSelector) {
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
const config = this.modelSelector === true ? {} : this.modelSelector;
|
|
103
|
+
const subtitle = typeof config.subtitle === "string" && config.subtitle.trim() !== "" ? config.subtitle : undefined;
|
|
104
|
+
return Object.assign({ value: typeof config.value === "string" && config.value.trim() !== "" ? config.value.trim() : "_id", label: typeof config.label === "string" && config.label.trim() !== "" ? config.label.trim() : "_id" }, (subtitle ? { subtitle: subtitle.trim() } : {}));
|
|
105
|
+
}
|
|
106
|
+
getModelSelectorPreview(req) {
|
|
107
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
108
|
+
const config = this.getModelSelectorConfig();
|
|
109
|
+
if (!config) {
|
|
110
|
+
throw new Error("Datatable is not enabled for model selection.");
|
|
111
|
+
}
|
|
112
|
+
const rawValue = req.body.value;
|
|
113
|
+
if (rawValue === undefined || rawValue === null || rawValue === "") {
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
let value = rawValue;
|
|
117
|
+
if (config.value === "_id") {
|
|
118
|
+
if (rawValue instanceof mongodb_1.ObjectId) {
|
|
119
|
+
value = rawValue;
|
|
120
|
+
}
|
|
121
|
+
else if (typeof rawValue === "string" && mongodb_1.ObjectId.isValid(rawValue)) {
|
|
122
|
+
value = new mongodb_1.ObjectId(rawValue);
|
|
123
|
+
}
|
|
124
|
+
else {
|
|
125
|
+
return null;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
const normalizeStages = (stages) => (stages !== null && stages !== void 0 ? stages : []).map((stage) => {
|
|
129
|
+
if (!stage || typeof stage !== "object" || !Object.prototype.hasOwnProperty.call(stage, "skipStats")) {
|
|
130
|
+
return stage;
|
|
131
|
+
}
|
|
132
|
+
const { skipStats: _skipStats } = stage, normalized = __rest(stage, ["skipStats"]);
|
|
133
|
+
return normalized;
|
|
134
|
+
});
|
|
135
|
+
const aggregation = [
|
|
136
|
+
...normalizeStages(yield this.getAggregation(req)),
|
|
137
|
+
...normalizeStages(yield this.getSubAggregation(req)),
|
|
138
|
+
{ $match: { [config.value]: value } },
|
|
139
|
+
{ $limit: 1 },
|
|
140
|
+
];
|
|
141
|
+
const connection = yield DBConnection_1.DBConnection.getConnection();
|
|
142
|
+
const results = yield connection.client
|
|
143
|
+
.db(null)
|
|
144
|
+
.collection(yield this.getCollection(req))
|
|
145
|
+
.aggregate(aggregation, {
|
|
146
|
+
collation: {
|
|
147
|
+
locale: "de",
|
|
148
|
+
strength: 2,
|
|
149
|
+
},
|
|
150
|
+
})
|
|
151
|
+
.toArray();
|
|
152
|
+
if (!results[0]) {
|
|
153
|
+
return null;
|
|
154
|
+
}
|
|
155
|
+
let result = results[0];
|
|
156
|
+
if (this.onRow) {
|
|
157
|
+
result = yield this.onRow(result);
|
|
158
|
+
}
|
|
159
|
+
if (this.onAttributes) {
|
|
160
|
+
result.__cellattr = yield this.onAttributes(result);
|
|
161
|
+
}
|
|
162
|
+
return result;
|
|
163
|
+
});
|
|
164
|
+
}
|
|
81
165
|
getSelectableFunctionsDef(req) {
|
|
82
166
|
return __awaiter(this, void 0, void 0, function* () {
|
|
83
167
|
if (!this.selectableFunctions) {
|
|
@@ -43,6 +43,18 @@ class DatatableController extends Controller_1.Controller {
|
|
|
43
43
|
});
|
|
44
44
|
}
|
|
45
45
|
const table = new tableClass();
|
|
46
|
+
if (req.body.mode === "model-selector-preview") {
|
|
47
|
+
if (!table.getModelSelectorConfig()) {
|
|
48
|
+
return res.status(403).send({
|
|
49
|
+
status: "error",
|
|
50
|
+
message: "Datatable is not enabled for model selection.",
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
return res.send({
|
|
54
|
+
status: "success",
|
|
55
|
+
data: yield table.getModelSelectorPreview(req),
|
|
56
|
+
});
|
|
57
|
+
}
|
|
46
58
|
if (req.body.type === "stats" || req.body.mode === "stats") {
|
|
47
59
|
return res.send({
|
|
48
60
|
status: "success",
|
|
@@ -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();
|
|
@@ -67,8 +67,8 @@ export declare class Response {
|
|
|
67
67
|
* @param mimeType
|
|
68
68
|
* @returns void
|
|
69
69
|
*/
|
|
70
|
-
stream(req: Request, filepath: string, filename: string, mimeType: string): Promise<
|
|
71
|
-
download(filepath: string, options?: any): Promise<
|
|
70
|
+
stream(req: Request, filepath: string, filename: string, mimeType: string): Promise<Response | undefined>;
|
|
71
|
+
download(filepath: string, options?: any): Promise<void>;
|
|
72
72
|
end(): this;
|
|
73
73
|
registerEvent(eventKey: string, func: any): Promise<void>;
|
|
74
74
|
/**
|
package/dist/Router/Response.js
CHANGED
|
@@ -14,6 +14,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
14
14
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
15
|
exports.Response = void 0;
|
|
16
16
|
const fs_1 = __importDefault(require("fs"));
|
|
17
|
+
const promises_1 = require("stream/promises");
|
|
17
18
|
const path_1 = __importDefault(require("path"));
|
|
18
19
|
const mime_types_1 = __importDefault(require("mime-types"));
|
|
19
20
|
class Response {
|
|
@@ -129,83 +130,54 @@ class Response {
|
|
|
129
130
|
* @returns void
|
|
130
131
|
*/
|
|
131
132
|
stream(req, filepath, filename, mimeType) {
|
|
132
|
-
var _a;
|
|
133
|
+
var _a, _b;
|
|
133
134
|
return __awaiter(this, void 0, void 0, function* () {
|
|
134
|
-
|
|
135
|
-
|
|
135
|
+
if (!this.res)
|
|
136
|
+
throw new Error("Native Response-Objekt nicht verfügbar");
|
|
137
|
+
const stats = yield fs_1.default.promises.stat(filepath);
|
|
136
138
|
const total = stats.size;
|
|
137
|
-
|
|
138
|
-
let end =
|
|
139
|
+
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) || "");
|
|
140
|
+
let start = 0, end = total - 1;
|
|
139
141
|
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");
|
|
142
|
+
const match = /^bytes=(\d*)-(\d*)$/.exec(range);
|
|
143
|
+
if (!match || (!match[1] && !match[2])) {
|
|
144
|
+
return this.status(416).header("Content-Range", `bytes */${total}`).end();
|
|
145
|
+
}
|
|
146
|
+
if (!match[1]) {
|
|
147
|
+
const suffix = Number(match[2]);
|
|
148
|
+
start = Math.max(0, total - suffix);
|
|
149
|
+
if (suffix === 0)
|
|
150
|
+
start = total;
|
|
151
|
+
}
|
|
152
|
+
else {
|
|
153
|
+
start = Number(match[1]);
|
|
154
|
+
end = match[2] ? Math.min(Number(match[2]), total - 1) : total - 1;
|
|
155
|
+
}
|
|
156
|
+
if (!Number.isSafeInteger(start) || !Number.isSafeInteger(end) || start >= total || end < start) {
|
|
157
|
+
return this.status(416).header("Content-Range", `bytes */${total}`).end();
|
|
158
|
+
}
|
|
159
|
+
this.status(206).header("Content-Range", `bytes ${start}-${end}/${total}`);
|
|
169
160
|
}
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
161
|
+
this.header("Accept-Ranges", "bytes");
|
|
162
|
+
this.header("Content-Length", String(range ? end - start + 1 : total));
|
|
163
|
+
this.header("Content-Type", mimeType);
|
|
164
|
+
this.header("Content-Disposition", `attachment; filename="${filename.replace(/["\r\n\\]/g, "_")}"`);
|
|
165
|
+
if (total === 0)
|
|
166
|
+
return this.end();
|
|
167
|
+
yield (0, promises_1.pipeline)(fs_1.default.createReadStream(filepath, { start, end }), this.res);
|
|
175
168
|
});
|
|
176
169
|
}
|
|
177
170
|
download(filepath, options) {
|
|
178
171
|
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
|
-
});
|
|
172
|
+
if (!this.res)
|
|
173
|
+
throw new Error("Cannot download: Original response object is missing");
|
|
174
|
+
const stats = yield fs_1.default.promises.stat(filepath);
|
|
175
|
+
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";
|
|
176
|
+
const filename = String((options === null || options === void 0 ? void 0 : options.filename) || path_1.default.basename(filepath)).replace(/["\r\n\\]/g, "_");
|
|
177
|
+
this.header("Content-Disposition", `${(options === null || options === void 0 ? void 0 : options.inline) ? "inline" : "attachment"}; filename="${filename}"`);
|
|
178
|
+
this.header("Content-Type", mimeType);
|
|
179
|
+
this.header("Content-Length", String(stats.size));
|
|
180
|
+
yield (0, promises_1.pipeline)(fs_1.default.createReadStream(filepath), this.res);
|
|
209
181
|
});
|
|
210
182
|
}
|
|
211
183
|
end() {
|
package/dist/index.d.ts
CHANGED
|
@@ -61,3 +61,5 @@ 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";
|
package/dist/index.js
CHANGED
|
@@ -77,3 +77,4 @@ __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);
|
package/package.json
CHANGED