@webtypen/webframez-core 0.3.97 → 0.3.101
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 +10 -1
- package/build-cache.d.ts +1 -1
- package/build-cache.js +12 -1
- package/dist/Datatable/Datatable.d.ts +14 -1
- package/dist/Datatable/Datatable.js +117 -27
- package/dist/Datatable/DatatableColumnPreferences.d.ts +30 -0
- package/dist/Datatable/DatatableColumnPreferences.js +63 -0
- package/dist/Datatable/DatatableController.d.ts +4 -1
- package/dist/Datatable/DatatableController.js +35 -20
- package/dist/Datatable/DatatablePreferencesStore.d.ts +15 -0
- package/dist/Datatable/DatatablePreferencesStore.js +59 -0
- package/dist/Router/Route.d.ts +2 -0
- package/dist/Router/Route.js +4 -0
- package/dist/Router/Router.d.ts +4 -0
- package/dist/Router/Router.js +15 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +5 -1
- package/docs/databuilder.md +5 -0
- package/docs/datatables.md +103 -0
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -996,7 +996,10 @@ Metro consumers can call
|
|
|
996
996
|
`require('@webtypen/webframez-core/build-cache').configureMetroCache(config)`.
|
|
997
997
|
It applies `WEBFRAMEZ_BUILD_MAX_WORKERS` and `WEBFRAMEZ_METRO_CACHE_DIRECTORY`,
|
|
998
998
|
preserving Expo's FileStore implementation and supporting old/new file-map option
|
|
999
|
-
names.
|
|
999
|
+
names. Transform and file-map caches are separated by a stable hash of sorted
|
|
1000
|
+
`EXPO_PUBLIC_*`, `NODE_ENV` and `BABEL_ENV` values. The cache version also includes
|
|
1001
|
+
this hash for custom/remote stores; public values are not written into path names.
|
|
1002
|
+
Private registry credentials do not participate in this build identity. Do not pass Expo `--clear` on normal cached builds.
|
|
1000
1003
|
|
|
1001
1004
|
|
|
1002
1005
|
### Reusable build workspaces
|
|
@@ -1024,3 +1027,9 @@ Asset builds use a stable staging directory so bundler caches can reuse module p
|
|
|
1024
1027
|
An exclusive per-output lock prevents concurrent builds overwriting that directory.
|
|
1025
1028
|
After a killed local build, remove its `.webframez-build/.building-*.lock` directory
|
|
1026
1029
|
only after confirming that no previous build is running. WSM Force Clear removes it.
|
|
1030
|
+
|
|
1031
|
+
### Explicit HEAD routes
|
|
1032
|
+
|
|
1033
|
+
`Route.head(path, handler, options)` registers metadata-only requests separately from GET.
|
|
1034
|
+
Group prefixes, middleware, domain filters and OPTIONS preflight resolution work as for other routes.
|
|
1035
|
+
Return headers with `res.header(...)` and finish with `res.end()`; no response body is needed.
|
package/build-cache.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export function configureMetroCache<T extends {cacheStores?: any; maxWorkers?: number}>(config:T, env?:Record<string,string | undefined>):T;
|
|
1
|
+
export function configureMetroCache<T extends {cacheVersion?: string; cacheStores?: any; maxWorkers?: number}>(config:T, env?:Record<string,string | undefined>):T;
|
package/build-cache.js
CHANGED
|
@@ -1,15 +1,26 @@
|
|
|
1
1
|
const path = require("node:path");
|
|
2
2
|
const fs = require("node:fs");
|
|
3
|
+
const crypto = require("node:crypto");
|
|
3
4
|
|
|
4
5
|
/** Preserve the platform's FileStore subclass (including Expo CSS handling). */
|
|
5
6
|
function configureMetroCache(config, env = process.env) {
|
|
7
|
+
// Expo inlines public values into transforms. Metro's persistent cache must
|
|
8
|
+
// not share those transforms between origins or feature configurations.
|
|
9
|
+
const publicEnvironment = Object.keys(env)
|
|
10
|
+
.filter(key => key.startsWith("EXPO_PUBLIC_") || key === "NODE_ENV" || key === "BABEL_ENV")
|
|
11
|
+
.sort()
|
|
12
|
+
.map(key => [key, env[key]]);
|
|
13
|
+
const environmentKey = crypto.createHash("sha256").update(JSON.stringify(publicEnvironment)).digest("hex");
|
|
14
|
+
config.cacheVersion = `${config.cacheVersion || "1.0"}:webframez-public-env-v1:${environmentKey}`;
|
|
15
|
+
|
|
16
|
+
|
|
6
17
|
if (env.WEBFRAMEZ_BUILD_MAX_WORKERS !== undefined) {
|
|
7
18
|
const workers = Number(env.WEBFRAMEZ_BUILD_MAX_WORKERS);
|
|
8
19
|
if (!Number.isInteger(workers) || workers < 1 || workers > 64) throw new Error("Invalid WEBFRAMEZ_BUILD_MAX_WORKERS");
|
|
9
20
|
config.maxWorkers = workers;
|
|
10
21
|
}
|
|
11
22
|
if (!env.WEBFRAMEZ_METRO_CACHE_DIRECTORY) return config;
|
|
12
|
-
const root = path.resolve(env.WEBFRAMEZ_METRO_CACHE_DIRECTORY);
|
|
23
|
+
const root = path.join(path.resolve(env.WEBFRAMEZ_METRO_CACHE_DIRECTORY), `env-${environmentKey}`);
|
|
13
24
|
const relocate = stores => stores.map((store, index) => {
|
|
14
25
|
// Leave remote and custom stores untouched; their constructors need other options.
|
|
15
26
|
if (!store || typeof store._root !== "string") return store;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Request } from "../Router/Request";
|
|
2
|
+
import { DatatableColumnContext, DatatableColumnPreferencesConfig } from "./DatatableColumnPreferences";
|
|
2
3
|
export interface DatatableModelSelectorConfig {
|
|
3
4
|
value?: string;
|
|
4
5
|
label?: string;
|
|
@@ -10,6 +11,9 @@ export interface NormalizedDatatableModelSelectorConfig {
|
|
|
10
11
|
subtitle?: string;
|
|
11
12
|
}
|
|
12
13
|
export declare class Datatable {
|
|
14
|
+
static columnPreferences: DatatableColumnPreferencesConfig | false;
|
|
15
|
+
columnPreferences?: DatatableColumnPreferencesConfig | false;
|
|
16
|
+
private columnStates;
|
|
13
17
|
collection: string | Function;
|
|
14
18
|
aggregation: {
|
|
15
19
|
[key: string]: any;
|
|
@@ -49,6 +53,15 @@ export declare class Datatable {
|
|
|
49
53
|
};
|
|
50
54
|
} | null;
|
|
51
55
|
getInit(req: Request): Promise<any>;
|
|
56
|
+
private loadColumnState;
|
|
57
|
+
private getColumnState;
|
|
58
|
+
saveColumnPreferences(req: Request, columns: unknown): Promise<any>;
|
|
59
|
+
resetColumnPreferences(req: Request): Promise<any>;
|
|
60
|
+
/** All definitions remain available through getColumns(); this returns the user's ordered layout. */
|
|
61
|
+
getVisibleColumns(req: Request): Promise<Record<string, any>>;
|
|
62
|
+
getVisibleColumnKeys(req: Request): Promise<string[]>;
|
|
63
|
+
isColumnVisible(req: Request, key: string): Promise<boolean>;
|
|
64
|
+
getColumnContext(req: Request): Promise<DatatableColumnContext>;
|
|
52
65
|
getModelSelectorConfig(): NormalizedDatatableModelSelectorConfig | null;
|
|
53
66
|
getModelSelectorPreview(req: Request): Promise<any>;
|
|
54
67
|
getSelectableFunctionsDef(req: Request): Promise<{
|
|
@@ -62,7 +75,7 @@ export declare class Datatable {
|
|
|
62
75
|
getAggregation(req: Request): Promise<any>;
|
|
63
76
|
getSubAggregation(req: Request): Promise<any>;
|
|
64
77
|
getCollection(req: Request): Promise<any>;
|
|
65
|
-
getData(req: Request): Promise<{
|
|
78
|
+
getData(req: Request, context?: DatatableColumnContext): Promise<{
|
|
66
79
|
page: number;
|
|
67
80
|
max_entries: any;
|
|
68
81
|
total_pages: number;
|
|
@@ -27,8 +27,11 @@ exports.Datatable = void 0;
|
|
|
27
27
|
const moment_1 = __importDefault(require("moment"));
|
|
28
28
|
const DBConnection_1 = require("../Database/DBConnection");
|
|
29
29
|
const NumericFunctions_1 = require("../Functions/NumericFunctions");
|
|
30
|
+
const DatatableColumnPreferences_1 = require("./DatatableColumnPreferences");
|
|
31
|
+
const DatatablePreferencesStore_1 = require("./DatatablePreferencesStore");
|
|
30
32
|
class Datatable {
|
|
31
33
|
constructor() {
|
|
34
|
+
this.columnStates = new WeakMap();
|
|
32
35
|
this.collection = "";
|
|
33
36
|
this.aggregation = null;
|
|
34
37
|
this.subAggregation = null;
|
|
@@ -57,26 +60,19 @@ class Datatable {
|
|
|
57
60
|
if (modelSelector) {
|
|
58
61
|
out.modelSelector = modelSelector;
|
|
59
62
|
}
|
|
60
|
-
const
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
})));
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
out.columns = cols;
|
|
74
|
-
resolve(true);
|
|
75
|
-
})));
|
|
76
|
-
promises.push(new Promise((resolve) => __awaiter(this, void 0, void 0, function* () {
|
|
77
|
-
out.onPressLink = yield this.getOnPressLink(req);
|
|
78
|
-
resolve(true);
|
|
79
|
-
})));
|
|
63
|
+
const [filter, state, onPressLink] = yield Promise.all([
|
|
64
|
+
this.getFilter(req), this.getColumnState(req), this.getOnPressLink(req),
|
|
65
|
+
]);
|
|
66
|
+
out.filter = filter;
|
|
67
|
+
out.columns = Object.entries(state.visible).map(([key, column]) => (Object.assign(Object.assign({}, column), { key })));
|
|
68
|
+
out.availableColumns = Object.entries(state.all).map(([key, column]) => (Object.assign(Object.assign({}, column), { key })));
|
|
69
|
+
out.columnPreferences = Boolean(state.scope) && Object.keys(state.all).length > 0;
|
|
70
|
+
out.columnSelection = out.columns.map((column) => {
|
|
71
|
+
var _a, _b;
|
|
72
|
+
return (Object.assign({ key: column.key }, (((_b = (_a = state.preferences) === null || _a === void 0 ? void 0 : _a.find((entry) => entry.key === column.key)) === null || _b === void 0 ? void 0 : _b.width)
|
|
73
|
+
? { width: state.preferences.find((entry) => entry.key === column.key).width } : {})));
|
|
74
|
+
});
|
|
75
|
+
out.onPressLink = onPressLink;
|
|
80
76
|
if (this.exports) {
|
|
81
77
|
out.export_definitions = [];
|
|
82
78
|
for (let key in this.exports) {
|
|
@@ -87,13 +83,105 @@ class Datatable {
|
|
|
87
83
|
});
|
|
88
84
|
}
|
|
89
85
|
}
|
|
90
|
-
yield Promise.all(promises);
|
|
91
86
|
if (typeof this.onInit === "function") {
|
|
92
87
|
yield this.onInit(req, out);
|
|
93
88
|
}
|
|
94
89
|
return out;
|
|
95
90
|
});
|
|
96
91
|
}
|
|
92
|
+
loadColumnState(req) {
|
|
93
|
+
var _a, _b;
|
|
94
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
95
|
+
const all = (_a = yield this.getColumns(req)) !== null && _a !== void 0 ? _a : {};
|
|
96
|
+
const config = (_b = this.columnPreferences) !== null && _b !== void 0 ? _b : Datatable.columnPreferences;
|
|
97
|
+
const store = new DatatablePreferencesStore_1.DatatablePreferencesStore(config || {});
|
|
98
|
+
const scope = config === false ? null : yield store.scope(req);
|
|
99
|
+
const preferences = scope ? yield store.load(scope) : null;
|
|
100
|
+
return { all, visible: DatatableColumnPreferences_1.DatatableColumnPreferences.apply(all, preferences), preferences, store, scope };
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
getColumnState(req) {
|
|
104
|
+
let state = this.columnStates.get(req);
|
|
105
|
+
if (!state) {
|
|
106
|
+
state = this.loadColumnState(req);
|
|
107
|
+
this.columnStates.set(req, state);
|
|
108
|
+
}
|
|
109
|
+
return state;
|
|
110
|
+
}
|
|
111
|
+
saveColumnPreferences(req, columns) {
|
|
112
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
113
|
+
const state = yield this.getColumnState(req);
|
|
114
|
+
if (!state.scope)
|
|
115
|
+
throw Object.assign(new Error("Spalteneinstellungen sind für diesen Benutzer nicht verfügbar."), { status: 403 });
|
|
116
|
+
let preferences;
|
|
117
|
+
try {
|
|
118
|
+
preferences = DatatableColumnPreferences_1.DatatableColumnPreferences.validate(columns, state.all);
|
|
119
|
+
}
|
|
120
|
+
catch (error) {
|
|
121
|
+
throw Object.assign(error, { status: 400 });
|
|
122
|
+
}
|
|
123
|
+
yield state.store.save(state.scope, preferences);
|
|
124
|
+
this.columnStates.delete(req);
|
|
125
|
+
return this.getInit(req);
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
resetColumnPreferences(req) {
|
|
129
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
130
|
+
const state = yield this.getColumnState(req);
|
|
131
|
+
if (!state.scope)
|
|
132
|
+
throw Object.assign(new Error("Spalteneinstellungen sind für diesen Benutzer nicht verfügbar."), { status: 403 });
|
|
133
|
+
yield state.store.reset(state.scope);
|
|
134
|
+
this.columnStates.delete(req);
|
|
135
|
+
return this.getInit(req);
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
/** All definitions remain available through getColumns(); this returns the user's ordered layout. */
|
|
139
|
+
getVisibleColumns(req) {
|
|
140
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
141
|
+
return (yield this.getColumnState(req)).visible;
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
getVisibleColumnKeys(req) {
|
|
145
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
146
|
+
return Object.keys(yield this.getVisibleColumns(req));
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
isColumnVisible(req, key) {
|
|
150
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
151
|
+
return Object.prototype.hasOwnProperty.call(yield this.getVisibleColumns(req), key);
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
getColumnContext(req) {
|
|
155
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r;
|
|
156
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
157
|
+
const { all, visible: columns } = yield this.getColumnState(req);
|
|
158
|
+
const columnKeys = Object.keys(columns);
|
|
159
|
+
const filters = (_a = yield this.getFilter(req)) !== null && _a !== void 0 ? _a : {};
|
|
160
|
+
const required = new Set(columnKeys);
|
|
161
|
+
for (const key of columnKeys)
|
|
162
|
+
required.add((_b = columns[key].mapping) !== null && _b !== void 0 ? _b : key);
|
|
163
|
+
for (const key of Object.keys((_d = (_c = req.body) === null || _c === void 0 ? void 0 : _c.sort) !== null && _d !== void 0 ? _d : {}))
|
|
164
|
+
required.add(key);
|
|
165
|
+
for (const key of Object.keys((_f = (_e = req.body) === null || _e === void 0 ? void 0 : _e.search) !== null && _f !== void 0 ? _f : {}))
|
|
166
|
+
required.add((_h = (_g = all[key]) === null || _g === void 0 ? void 0 : _g.mapping) !== null && _h !== void 0 ? _h : key);
|
|
167
|
+
for (const key of Object.keys((_k = (_j = req.body) === null || _j === void 0 ? void 0 : _j.filter) !== null && _k !== void 0 ? _k : {}))
|
|
168
|
+
required.add((_m = (_l = filters[key]) === null || _l === void 0 ? void 0 : _l.mapping) !== null && _m !== void 0 ? _m : key);
|
|
169
|
+
for (const [key, sum] of Object.entries((_p = (_o = req.body) === null || _o === void 0 ? void 0 : _o.sums) !== null && _p !== void 0 ? _p : {}))
|
|
170
|
+
required.add((_q = sum === null || sum === void 0 ? void 0 : sum.mapping) !== null && _q !== void 0 ? _q : key);
|
|
171
|
+
if (((_r = req.body) === null || _r === void 0 ? void 0 : _r.mode) === "model-selector-preview") {
|
|
172
|
+
const selector = this.getModelSelectorConfig();
|
|
173
|
+
for (const field of [selector === null || selector === void 0 ? void 0 : selector.value, selector === null || selector === void 0 ? void 0 : selector.label, selector === null || selector === void 0 ? void 0 : selector.subtitle])
|
|
174
|
+
if (field)
|
|
175
|
+
required.add(field);
|
|
176
|
+
}
|
|
177
|
+
const matches = (field) => Array.from(required).some(key => key === field || key.startsWith(field + ".") || field.startsWith(key + "."));
|
|
178
|
+
return {
|
|
179
|
+
columns, columnKeys,
|
|
180
|
+
isColumnVisible: key => Object.prototype.hasOwnProperty.call(columns, key),
|
|
181
|
+
isColumnRequired: key => { var _a; return matches(key) || (typeof ((_a = all[key]) === null || _a === void 0 ? void 0 : _a.mapping) === "string" && matches(all[key].mapping)); },
|
|
182
|
+
};
|
|
183
|
+
});
|
|
184
|
+
}
|
|
97
185
|
getModelSelectorConfig() {
|
|
98
186
|
if (!this.modelSelector) {
|
|
99
187
|
return null;
|
|
@@ -198,7 +286,7 @@ class Datatable {
|
|
|
198
286
|
if (this.defaultUnwind && this.defaultUnwind.trim() !== "") {
|
|
199
287
|
prefix.push({ $unwind: this.defaultUnwind });
|
|
200
288
|
}
|
|
201
|
-
const aggr = typeof this.aggregation === "function" ? yield this.aggregation(req) : this.aggregation;
|
|
289
|
+
const aggr = typeof this.aggregation === "function" ? yield this.aggregation(req, yield this.getColumnContext(req)) : this.aggregation;
|
|
202
290
|
if (this.autoApplyFilter && req.body.filter && typeof req.body.filter === "object") {
|
|
203
291
|
const filterMatch = yield this.getFilterMatch(req, req.body.filter);
|
|
204
292
|
if (this.autoApplyFilter === "begin") {
|
|
@@ -233,7 +321,7 @@ class Datatable {
|
|
|
233
321
|
}
|
|
234
322
|
getSubAggregation(req) {
|
|
235
323
|
return __awaiter(this, void 0, void 0, function* () {
|
|
236
|
-
return typeof this.subAggregation === "function" ? yield this.subAggregation(req) : this.subAggregation;
|
|
324
|
+
return typeof this.subAggregation === "function" ? yield this.subAggregation(req, yield this.getColumnContext(req)) : this.subAggregation;
|
|
237
325
|
});
|
|
238
326
|
}
|
|
239
327
|
getCollection(req) {
|
|
@@ -244,8 +332,9 @@ class Datatable {
|
|
|
244
332
|
return this.collection;
|
|
245
333
|
});
|
|
246
334
|
}
|
|
247
|
-
getData(req) {
|
|
335
|
+
getData(req, context) {
|
|
248
336
|
return __awaiter(this, void 0, void 0, function* () {
|
|
337
|
+
const columns = context !== null && context !== void 0 ? context : yield this.getColumnContext(req);
|
|
249
338
|
const page = req.body.page && parseInt(req.body.page) > 0 ? parseInt(req.body.page) : 0;
|
|
250
339
|
const aggregation = [];
|
|
251
340
|
const aggregationStats = [];
|
|
@@ -332,10 +421,10 @@ class Datatable {
|
|
|
332
421
|
if (this.onRow || this.onAttributes) {
|
|
333
422
|
for (let i in results) {
|
|
334
423
|
if (this.onRow) {
|
|
335
|
-
results[i] = yield this.onRow(results[i]);
|
|
424
|
+
results[i] = yield this.onRow(results[i], req, columns);
|
|
336
425
|
}
|
|
337
426
|
if (this.onAttributes) {
|
|
338
|
-
results[i].__cellattr = yield this.onAttributes(results[i]);
|
|
427
|
+
results[i].__cellattr = yield this.onAttributes(results[i], req, columns);
|
|
339
428
|
}
|
|
340
429
|
}
|
|
341
430
|
}
|
|
@@ -381,7 +470,7 @@ class Datatable {
|
|
|
381
470
|
sums: sums,
|
|
382
471
|
};
|
|
383
472
|
if (typeof this.onData === "function") {
|
|
384
|
-
yield this.onData(req, data);
|
|
473
|
+
yield this.onData(req, data, columns);
|
|
385
474
|
}
|
|
386
475
|
return data;
|
|
387
476
|
});
|
|
@@ -708,3 +797,4 @@ class Datatable {
|
|
|
708
797
|
}
|
|
709
798
|
}
|
|
710
799
|
exports.Datatable = Datatable;
|
|
800
|
+
Datatable.columnPreferences = {};
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { DatabaseId } from "../Database/DatabaseAdapter";
|
|
2
|
+
import type { Request } from "../Router/Request";
|
|
3
|
+
export type DatatableColumnWidth = {
|
|
4
|
+
mode: "auto";
|
|
5
|
+
} | {
|
|
6
|
+
mode: "px" | "percent";
|
|
7
|
+
value: number;
|
|
8
|
+
};
|
|
9
|
+
export interface DatatableColumnPreference {
|
|
10
|
+
key: string;
|
|
11
|
+
/** Omit to retain the current backend default. */
|
|
12
|
+
width?: DatatableColumnWidth;
|
|
13
|
+
}
|
|
14
|
+
export interface DatatableColumnPreferencesConfig {
|
|
15
|
+
collection?: string;
|
|
16
|
+
connection?: string;
|
|
17
|
+
/** Resolve exclusively from authenticated server context. Null disables persistence for this request. */
|
|
18
|
+
owner?: (req: Request) => Record<string, DatabaseId> | null | Promise<Record<string, DatabaseId> | null>;
|
|
19
|
+
}
|
|
20
|
+
export interface DatatableColumnContext {
|
|
21
|
+
columns: Record<string, any>;
|
|
22
|
+
columnKeys: string[];
|
|
23
|
+
isColumnVisible: (key: string) => boolean;
|
|
24
|
+
/** Includes visible mappings and fields used by filters, search, sorting or sums. */
|
|
25
|
+
isColumnRequired: (key: string) => boolean;
|
|
26
|
+
}
|
|
27
|
+
export declare class DatatableColumnPreferences {
|
|
28
|
+
static validate(value: unknown, columns: Record<string, any>): DatatableColumnPreference[];
|
|
29
|
+
static apply(columns: Record<string, any>, preferences: unknown): Record<string, any>;
|
|
30
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.DatatableColumnPreferences = void 0;
|
|
4
|
+
class DatatableColumnPreferences {
|
|
5
|
+
static validate(value, columns) {
|
|
6
|
+
if (!Array.isArray(value) || value.length === 0 || value.length > Object.keys(columns).length) {
|
|
7
|
+
throw new Error("Bitte mindestens eine gültige Spalte auswählen.");
|
|
8
|
+
}
|
|
9
|
+
const seen = new Set();
|
|
10
|
+
return value.map(entry => {
|
|
11
|
+
if (!entry || typeof entry.key !== "string" || !Object.prototype.hasOwnProperty.call(columns, entry.key) || seen.has(entry.key)) {
|
|
12
|
+
throw new Error("Unbekannte oder doppelte Spalte.");
|
|
13
|
+
}
|
|
14
|
+
seen.add(entry.key);
|
|
15
|
+
const preference = { key: entry.key };
|
|
16
|
+
if (entry.width !== undefined) {
|
|
17
|
+
const width = entry.width;
|
|
18
|
+
if ((width === null || width === void 0 ? void 0 : width.mode) === "auto") {
|
|
19
|
+
preference.width = { mode: "auto" };
|
|
20
|
+
}
|
|
21
|
+
else if (((width === null || width === void 0 ? void 0 : width.mode) === "px" || (width === null || width === void 0 ? void 0 : width.mode) === "percent") && typeof width.value === "number"
|
|
22
|
+
&& Number.isFinite(width.value) && width.value > 0 && width.value <= (width.mode === "percent" ? 100 : 10000)) {
|
|
23
|
+
preference.width = { mode: width.mode, value: width.value };
|
|
24
|
+
}
|
|
25
|
+
else {
|
|
26
|
+
throw new Error("Ungültige Spaltenbreite.");
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return preference;
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
static apply(columns, preferences) {
|
|
33
|
+
const defaults = () => Object.fromEntries(Object.entries(columns).filter(([, column]) => column.hidden !== true));
|
|
34
|
+
if (!Array.isArray(preferences))
|
|
35
|
+
return defaults();
|
|
36
|
+
const available = preferences.filter(entry => entry && typeof entry.key === "string" && Object.prototype.hasOwnProperty.call(columns, entry.key));
|
|
37
|
+
if (!available.length)
|
|
38
|
+
return defaults();
|
|
39
|
+
let validated;
|
|
40
|
+
try {
|
|
41
|
+
validated = this.validate(available, columns);
|
|
42
|
+
}
|
|
43
|
+
catch (_a) {
|
|
44
|
+
return defaults();
|
|
45
|
+
}
|
|
46
|
+
return Object.fromEntries(validated.map(preference => {
|
|
47
|
+
const column = Object.assign({}, columns[preference.key]);
|
|
48
|
+
if (preference.width) {
|
|
49
|
+
delete column.width;
|
|
50
|
+
delete column.minWidth;
|
|
51
|
+
delete column.maxWidth;
|
|
52
|
+
column.autogrow = preference.width.mode === "auto";
|
|
53
|
+
column.grow = preference.width.mode === "auto" ? 1 : 0;
|
|
54
|
+
if (preference.width.mode === "px")
|
|
55
|
+
column.width = preference.width.value;
|
|
56
|
+
if (preference.width.mode === "percent")
|
|
57
|
+
column.width = `${preference.width.value}%`;
|
|
58
|
+
}
|
|
59
|
+
return [preference.key, column];
|
|
60
|
+
}));
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
exports.DatatableColumnPreferences = DatatableColumnPreferences;
|
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import { Controller } from "../Controller/Controller";
|
|
2
2
|
import { Request } from "../Router/Request";
|
|
3
3
|
import { Response } from "../Router/Response";
|
|
4
|
+
import { Datatable } from "./Datatable";
|
|
4
5
|
export declare class DatatableController extends Controller {
|
|
5
|
-
|
|
6
|
+
protected getTableClass(req: Request): any;
|
|
7
|
+
protected configureTable(table: Datatable, req: Request): void | Promise<void>;
|
|
8
|
+
restApi(req: Request, res: Response): Promise<Response | undefined>;
|
|
6
9
|
tableExport(req: Request, res: Response): Promise<Response>;
|
|
7
10
|
}
|
|
@@ -13,36 +13,47 @@ exports.DatatableController = void 0;
|
|
|
13
13
|
const Controller_1 = require("../Controller/Controller");
|
|
14
14
|
const DatatableRegistry_1 = require("./DatatableRegistry");
|
|
15
15
|
class DatatableController extends Controller_1.Controller {
|
|
16
|
+
getTableClass(req) {
|
|
17
|
+
return DatatableRegistry_1.DatatableRegistry.getTable(req.body._table);
|
|
18
|
+
}
|
|
19
|
+
configureTable(table, req) { }
|
|
16
20
|
restApi(req, res) {
|
|
17
|
-
var _a;
|
|
21
|
+
var _a, _b, _c;
|
|
18
22
|
return __awaiter(this, void 0, void 0, function* () {
|
|
19
|
-
if (
|
|
23
|
+
if (typeof ((_a = req.body) === null || _a === void 0 ? void 0 : _a._table) !== "string" || req.body._table.trim() === "") {
|
|
20
24
|
return res.status(404).send({
|
|
21
25
|
status: "error",
|
|
22
26
|
message: "Missing table-key ...",
|
|
23
27
|
});
|
|
24
28
|
}
|
|
25
|
-
const tableClass =
|
|
29
|
+
const tableClass = this.getTableClass(req);
|
|
26
30
|
if (!tableClass) {
|
|
27
31
|
return res.status(404).send({
|
|
28
32
|
status: "error",
|
|
29
33
|
message: "Table '" + req.body._table + "' not found ...",
|
|
30
34
|
});
|
|
31
35
|
}
|
|
32
|
-
if (req.body.
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
});
|
|
36
|
+
if (req.body.export)
|
|
37
|
+
return this.tableExport(req, res);
|
|
38
|
+
const table = new tableClass();
|
|
39
|
+
yield this.configureTable(table, req);
|
|
40
|
+
if (req.body.init_request || req.body.mode === "columns-get") {
|
|
41
|
+
return res.send({ status: "success", data: yield table.getInit(req) });
|
|
38
42
|
}
|
|
39
|
-
if (
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
43
|
+
if (req.body.mode === "columns-save" || req.body.mode === "columns-reset") {
|
|
44
|
+
try {
|
|
45
|
+
const data = req.body.mode === "columns-save"
|
|
46
|
+
? yield table.saveColumnPreferences(req, req.body.columns)
|
|
47
|
+
: yield table.resetColumnPreferences(req);
|
|
48
|
+
return res.send({ status: "success", data });
|
|
49
|
+
}
|
|
50
|
+
catch (error) {
|
|
51
|
+
const status = error === null || error === void 0 ? void 0 : error.status;
|
|
52
|
+
if (status !== 400 && status !== 403)
|
|
53
|
+
throw error;
|
|
54
|
+
return res.status(status).send({ status: "error", message: error.message });
|
|
55
|
+
}
|
|
44
56
|
}
|
|
45
|
-
const table = new tableClass();
|
|
46
57
|
if (req.body.mode === "model-selector-preview") {
|
|
47
58
|
if (!table.getModelSelectorConfig()) {
|
|
48
59
|
return res.status(403).send({
|
|
@@ -62,7 +73,7 @@ class DatatableController extends Controller_1.Controller {
|
|
|
62
73
|
});
|
|
63
74
|
}
|
|
64
75
|
if (req.body.apiFunction && typeof req.body.apiFunction === "string" && req.body.apiFunction.trim() !== "") {
|
|
65
|
-
const func = (
|
|
76
|
+
const func = (_b = table.selectableFunctions) === null || _b === void 0 ? void 0 : _b[req.body.apiFunction];
|
|
66
77
|
if (!func || !func.handle) {
|
|
67
78
|
return res.status(404).send({
|
|
68
79
|
status: "error",
|
|
@@ -96,25 +107,28 @@ class DatatableController extends Controller_1.Controller {
|
|
|
96
107
|
message: e instanceof Error ? e.message : "Unknown error ...",
|
|
97
108
|
});
|
|
98
109
|
}
|
|
110
|
+
if (res.content !== undefined || ((_c = res.res) === null || _c === void 0 ? void 0 : _c.headersSent) || res.headersSent || res.writableEnded)
|
|
111
|
+
return;
|
|
99
112
|
return res.send({
|
|
100
113
|
status: "success",
|
|
101
114
|
});
|
|
102
115
|
}
|
|
103
116
|
return res.send({
|
|
104
117
|
status: "success",
|
|
105
|
-
data: yield table.getData(req),
|
|
118
|
+
data: yield table.getData(req, yield table.getColumnContext(req)),
|
|
106
119
|
});
|
|
107
120
|
});
|
|
108
121
|
}
|
|
109
122
|
tableExport(req, res) {
|
|
123
|
+
var _a;
|
|
110
124
|
return __awaiter(this, void 0, void 0, function* () {
|
|
111
|
-
if (
|
|
125
|
+
if (typeof ((_a = req.body) === null || _a === void 0 ? void 0 : _a._table) !== "string" || req.body._table.trim() === "") {
|
|
112
126
|
return res.status(404).send({
|
|
113
127
|
status: "error",
|
|
114
128
|
message: "Missing table-key ...",
|
|
115
129
|
});
|
|
116
130
|
}
|
|
117
|
-
const tableClass =
|
|
131
|
+
const tableClass = this.getTableClass(req);
|
|
118
132
|
if (!tableClass) {
|
|
119
133
|
return res.status(404).send({
|
|
120
134
|
status: "error",
|
|
@@ -122,6 +136,7 @@ class DatatableController extends Controller_1.Controller {
|
|
|
122
136
|
});
|
|
123
137
|
}
|
|
124
138
|
const table = new tableClass();
|
|
139
|
+
yield this.configureTable(table, req);
|
|
125
140
|
if (!req.body.export || !table.exports || (table.exports && !table.exports[req.body.export])) {
|
|
126
141
|
return res.status(404).send({
|
|
127
142
|
status: "error",
|
|
@@ -130,7 +145,7 @@ class DatatableController extends Controller_1.Controller {
|
|
|
130
145
|
}
|
|
131
146
|
const exp = table.exports[req.body.export];
|
|
132
147
|
const cols = [];
|
|
133
|
-
const colsData = yield table.
|
|
148
|
+
const colsData = yield table.getVisibleColumns(req);
|
|
134
149
|
if (colsData && Object.keys(colsData).length > 0) {
|
|
135
150
|
for (let i in colsData) {
|
|
136
151
|
cols.push(Object.assign(Object.assign({}, colsData[i]), { key: i }));
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { Request } from "../Router/Request";
|
|
2
|
+
import type { DatatableColumnPreference, DatatableColumnPreferencesConfig } from "./DatatableColumnPreferences";
|
|
3
|
+
/** Document-store adapter for per-owner table layouts. The deterministic ID makes first saves atomic. */
|
|
4
|
+
export declare class DatatablePreferencesStore {
|
|
5
|
+
private config;
|
|
6
|
+
constructor(config?: DatatableColumnPreferencesConfig);
|
|
7
|
+
scope(req: Request): Promise<{
|
|
8
|
+
table: any;
|
|
9
|
+
_id: string;
|
|
10
|
+
} | null>;
|
|
11
|
+
private collection;
|
|
12
|
+
load(scope: Record<string, any>): Promise<any>;
|
|
13
|
+
save(scope: Record<string, any>, columns: DatatableColumnPreference[]): Promise<void>;
|
|
14
|
+
reset(scope: Record<string, any>): Promise<void>;
|
|
15
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
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
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
exports.DatatablePreferencesStore = void 0;
|
|
13
|
+
const crypto_1 = require("crypto");
|
|
14
|
+
const DBConnection_1 = require("../Database/DBConnection");
|
|
15
|
+
/** Document-store adapter for per-owner table layouts. The deterministic ID makes first saves atomic. */
|
|
16
|
+
class DatatablePreferencesStore {
|
|
17
|
+
constructor(config = {}) {
|
|
18
|
+
this.config = config;
|
|
19
|
+
}
|
|
20
|
+
scope(req) {
|
|
21
|
+
var _a, _b, _c, _d;
|
|
22
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
23
|
+
const user = (_b = (_a = req.auth) === null || _a === void 0 ? void 0 : _a.user) !== null && _b !== void 0 ? _b : req.user;
|
|
24
|
+
const userId = user === null || user === void 0 ? void 0 : user[(_c = user.__primaryKey) !== null && _c !== void 0 ? _c : "_id"];
|
|
25
|
+
const owner = this.config.owner ? yield this.config.owner(req) : userId !== undefined && userId !== null ? { _user: userId } : null;
|
|
26
|
+
if (!owner || !Object.keys(owner).length || typeof ((_d = req.body) === null || _d === void 0 ? void 0 : _d._table) !== "string" || !req.body._table.trim())
|
|
27
|
+
return null;
|
|
28
|
+
if (Object.entries(owner).some(([key, value]) => ["_id", "table", "columns", "updated_at"].includes(key) || value === null || value === undefined)) {
|
|
29
|
+
throw new Error("Invalid datatable preference owner.");
|
|
30
|
+
}
|
|
31
|
+
const identity = Object.keys(owner).sort().map(key => [key, typeof owner[key] === "number" ? "number" : "id", String(owner[key])]);
|
|
32
|
+
const id = (0, crypto_1.createHash)("sha256").update(JSON.stringify([req.body._table, identity])).digest("hex");
|
|
33
|
+
return Object.assign(Object.assign({}, owner), { table: req.body._table, _id: id });
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
collection() {
|
|
37
|
+
var _a;
|
|
38
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
39
|
+
return (yield DBConnection_1.DBConnection.getDocumentStore(this.config.connection)).collection((_a = this.config.collection) !== null && _a !== void 0 ? _a : "users_datatables");
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
load(scope) {
|
|
43
|
+
var _a, _b;
|
|
44
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
45
|
+
return (_b = (_a = (yield (yield this.collection()).findOne(scope))) === null || _a === void 0 ? void 0 : _a.columns) !== null && _b !== void 0 ? _b : null;
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
save(scope, columns) {
|
|
49
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
50
|
+
yield (yield this.collection()).updateOne(scope, { $set: { columns, updated_at: new Date() } }, { upsert: true });
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
reset(scope) {
|
|
54
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
55
|
+
yield (yield this.collection()).deleteOne(scope);
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
exports.DatatablePreferencesStore = DatatablePreferencesStore;
|
package/dist/Router/Route.d.ts
CHANGED
|
@@ -27,6 +27,8 @@ export declare class RouteFacade {
|
|
|
27
27
|
* @param options
|
|
28
28
|
*/
|
|
29
29
|
get(path: string, component: any, options?: RouteRegistrationOptions): void;
|
|
30
|
+
/** Register a HEAD route, for metadata without a response body. */
|
|
31
|
+
head(path: string, component: any, options?: RouteRegistrationOptions): void;
|
|
30
32
|
/** Register the existing DataBuilder POST protocol with request-local configuration. */
|
|
31
33
|
databuilder(path: string, options?: DataBuilderRouteOptions): void;
|
|
32
34
|
/** Register CSRF, model login, logout and refresh using the browser auth contract. */
|
package/dist/Router/Route.js
CHANGED
|
@@ -77,6 +77,10 @@ class RouteFacade {
|
|
|
77
77
|
get(path, component, options) {
|
|
78
78
|
this.registerWithGroupContext("GET", path, component, options);
|
|
79
79
|
}
|
|
80
|
+
/** Register a HEAD route, for metadata without a response body. */
|
|
81
|
+
head(path, component, options) {
|
|
82
|
+
this.registerWithGroupContext("HEAD", path, component, options);
|
|
83
|
+
}
|
|
80
84
|
/** Register the existing DataBuilder POST protocol with request-local configuration. */
|
|
81
85
|
databuilder(path, options = {}) {
|
|
82
86
|
this.registerWithGroupContext("POST", path, (0, DataBuilderRoute_1.dataBuilderRoute)(options), { middleware: options.middleware, domains: options.domains });
|
package/dist/Router/Router.d.ts
CHANGED
|
@@ -43,6 +43,10 @@ declare class RouterFacade {
|
|
|
43
43
|
routesGET: {
|
|
44
44
|
[key: string]: RouteObject[];
|
|
45
45
|
};
|
|
46
|
+
/** Explicit HEAD routes, with the same middleware and domain matching as GET. */
|
|
47
|
+
routesHEAD: {
|
|
48
|
+
[key: string]: RouteObject[];
|
|
49
|
+
};
|
|
46
50
|
/**
|
|
47
51
|
* POST Route Store
|
|
48
52
|
*/
|
package/dist/Router/Router.js
CHANGED
|
@@ -53,6 +53,8 @@ class RouterFacade {
|
|
|
53
53
|
* GET Route Store
|
|
54
54
|
*/
|
|
55
55
|
this.routesGET = {};
|
|
56
|
+
/** Explicit HEAD routes, with the same middleware and domain matching as GET. */
|
|
57
|
+
this.routesHEAD = {};
|
|
56
58
|
/**
|
|
57
59
|
* POST Route Store
|
|
58
60
|
*/
|
|
@@ -86,6 +88,7 @@ class RouterFacade {
|
|
|
86
88
|
? options.maxRequestBodySizeBytes
|
|
87
89
|
: 10 * 1024 * 1024;
|
|
88
90
|
this.routesGET = {};
|
|
91
|
+
this.routesHEAD = {};
|
|
89
92
|
this.routesPOST = {};
|
|
90
93
|
this.routesPUT = {};
|
|
91
94
|
this.routesPATCH = {};
|
|
@@ -153,6 +156,12 @@ class RouterFacade {
|
|
|
153
156
|
}
|
|
154
157
|
this.routesGET[path].push(routeConf);
|
|
155
158
|
}
|
|
159
|
+
else if (type === "HEAD") {
|
|
160
|
+
if (!this.routesHEAD[path]) {
|
|
161
|
+
this.routesHEAD[path] = [];
|
|
162
|
+
}
|
|
163
|
+
this.routesHEAD[path].push(routeConf);
|
|
164
|
+
}
|
|
156
165
|
else if (type === "POST") {
|
|
157
166
|
if (!this.routesPOST[path]) {
|
|
158
167
|
this.routesPOST[path] = [];
|
|
@@ -196,6 +205,9 @@ class RouterFacade {
|
|
|
196
205
|
if (req.method === "GET") {
|
|
197
206
|
return this.dissolveRoute(this.routesGET, cleanUrl, req);
|
|
198
207
|
}
|
|
208
|
+
else if (req.method === "HEAD") {
|
|
209
|
+
return this.dissolveRoute(this.routesHEAD, cleanUrl, req);
|
|
210
|
+
}
|
|
199
211
|
else if (req.method === "POST") {
|
|
200
212
|
return this.dissolveRoute(this.routesPOST, cleanUrl, req);
|
|
201
213
|
}
|
|
@@ -212,6 +224,9 @@ class RouterFacade {
|
|
|
212
224
|
if (req.headers["access-control-request-method"] === "GET") {
|
|
213
225
|
return this.dissolveRoute(this.routesGET, cleanUrl, req);
|
|
214
226
|
}
|
|
227
|
+
else if (req.headers["access-control-request-method"] === "HEAD") {
|
|
228
|
+
return this.dissolveRoute(this.routesHEAD, cleanUrl, req);
|
|
229
|
+
}
|
|
215
230
|
else if (req.headers["access-control-request-method"] === "POST") {
|
|
216
231
|
return this.dissolveRoute(this.routesPOST, cleanUrl, req);
|
|
217
232
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -28,6 +28,8 @@ export * from "./Facades/Storage";
|
|
|
28
28
|
export * from "./DataBuilder/DataBuilder";
|
|
29
29
|
export * from "./DataBuilder/DataBuilderController";
|
|
30
30
|
export * from "./Datatable/Datatable";
|
|
31
|
+
export * from "./Datatable/DatatableColumnPreferences";
|
|
32
|
+
export * from "./Datatable/DatatablePreferencesStore";
|
|
31
33
|
export * from "./Datatable/DatatableRegistry";
|
|
32
34
|
export * from "./Datatable/DatatableController";
|
|
33
35
|
export * from "./Queue/BaseQueueJob";
|
|
@@ -76,3 +78,4 @@ export { ModelForm, ModelFormContext, ModelFormConstructor } from "./DataBuilder
|
|
|
76
78
|
export { Field, Validates, Forms, DataBuilderFieldOptions } from "./DataBuilder/decorators";
|
|
77
79
|
export { DataBuilderRouteOptions } from "./DataBuilder/DataBuilderRoute";
|
|
78
80
|
export { LegacyFieldTypeAdapter } from "./DataBuilder/LegacyFieldTypeAdapter";
|
|
81
|
+
export { defaultFormFields } from "./DataBuilder/formScope";
|
package/dist/index.js
CHANGED
|
@@ -14,7 +14,7 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
|
14
14
|
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
15
|
};
|
|
16
16
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
-
exports.LegacyFieldTypeAdapter = exports.Forms = exports.Validates = exports.Field = exports.ModelForm = void 0;
|
|
17
|
+
exports.defaultFormFields = exports.LegacyFieldTypeAdapter = exports.Forms = exports.Validates = exports.Field = exports.ModelForm = void 0;
|
|
18
18
|
__exportStar(require("./Config"), exports);
|
|
19
19
|
__exportStar(require("./Functions/DateFunctions"), exports);
|
|
20
20
|
__exportStar(require("./Functions/FileFunctions"), exports);
|
|
@@ -45,6 +45,8 @@ __exportStar(require("./Facades/Storage"), exports);
|
|
|
45
45
|
__exportStar(require("./DataBuilder/DataBuilder"), exports);
|
|
46
46
|
__exportStar(require("./DataBuilder/DataBuilderController"), exports);
|
|
47
47
|
__exportStar(require("./Datatable/Datatable"), exports);
|
|
48
|
+
__exportStar(require("./Datatable/DatatableColumnPreferences"), exports);
|
|
49
|
+
__exportStar(require("./Datatable/DatatablePreferencesStore"), exports);
|
|
48
50
|
__exportStar(require("./Datatable/DatatableRegistry"), exports);
|
|
49
51
|
__exportStar(require("./Datatable/DatatableController"), exports);
|
|
50
52
|
__exportStar(require("./Queue/BaseQueueJob"), exports);
|
|
@@ -96,3 +98,5 @@ Object.defineProperty(exports, "Validates", { enumerable: true, get: function ()
|
|
|
96
98
|
Object.defineProperty(exports, "Forms", { enumerable: true, get: function () { return decorators_1.Forms; } });
|
|
97
99
|
var LegacyFieldTypeAdapter_1 = require("./DataBuilder/LegacyFieldTypeAdapter");
|
|
98
100
|
Object.defineProperty(exports, "LegacyFieldTypeAdapter", { enumerable: true, get: function () { return LegacyFieldTypeAdapter_1.LegacyFieldTypeAdapter; } });
|
|
101
|
+
var formScope_1 = require("./DataBuilder/formScope");
|
|
102
|
+
Object.defineProperty(exports, "defaultFormFields", { enumerable: true, get: function () { return formScope_1.defaultFormFields; } });
|
package/docs/databuilder.md
CHANGED
|
@@ -32,6 +32,11 @@ Metadata is inherited without mutating parent definitions. Decorated definitions
|
|
|
32
32
|
|
|
33
33
|
Without registered forms or legacy `__forms`, decorated models get a default form containing their fields, including child layouts. Existing `__schema` still carries version, collection, hooks, aggregation, uniqueness, new-data and deletion behavior. Defining `static forms = () => [UserProfileForm]` is an alternative to `@Forms()`; the decorator takes precedence.
|
|
34
34
|
|
|
35
|
+
For dynamic sections inside an explicit layout, import `defaultFormFields` from
|
|
36
|
+
`@webtypen/webframez-core` and use `{ field: "attributes", fields: defaultFormFields(attributeSchema) }`.
|
|
37
|
+
It recursively supplies the `fields` layouts for object and array schemas, including
|
|
38
|
+
objects inside array entries. It does not modify the schema or populate data values.
|
|
39
|
+
|
|
35
40
|
## Server-side form classes
|
|
36
41
|
|
|
37
42
|
```ts
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# Datatable column preferences
|
|
2
|
+
|
|
3
|
+
`DatatableController@restApi` automatically restores the authenticated user's
|
|
4
|
+
column order, visibility and widths. `getColumns(req)` still returns all columns
|
|
5
|
+
allowed by the table definition. A column with `hidden: true` starts hidden and
|
|
6
|
+
can be added in the column editor. Hiding a column is presentation state, never
|
|
7
|
+
an authorization mechanism; omit unauthorized columns from `columns(req)`.
|
|
8
|
+
|
|
9
|
+
Native Design's Datatable and DataTableScreen show **Filter zurücksetzen** and
|
|
10
|
+
**Spalten bearbeiten** in their ellipsis menu when the backend supports this
|
|
11
|
+
protocol. The editor supports drag and drop (including keyboard/native actions),
|
|
12
|
+
hiding/adding columns, backend default widths, auto grow, pixels and percentages.
|
|
13
|
+
Saving refreshes rows without resetting filters. Cancel leaves the layout intact.
|
|
14
|
+
At least one column must remain visible. `features.columnPreferences = false`
|
|
15
|
+
disables the editor; explicit client `columns` overrides also disable it.
|
|
16
|
+
|
|
17
|
+
## Storage and scope
|
|
18
|
+
|
|
19
|
+
The default document collection is `users_datatables`. The owner comes from
|
|
20
|
+
`req.auth.user` or `req.user`, respecting its `__primaryKey` (default `_id`). No
|
|
21
|
+
owner means no persistence and no edit action. Records contain `_user`, `table`,
|
|
22
|
+
`columns` and `updated_at`. A deterministic `_id` provides atomic upserts without
|
|
23
|
+
creating indexes or running schema changes during requests.
|
|
24
|
+
|
|
25
|
+
Configure all tables via `Datatable.columnPreferences`, or assign a per-table
|
|
26
|
+
`columnPreferences` configuration in a controller's `configureTable` hook. Use
|
|
27
|
+
only authenticated server context for owner fields. The client cannot select a
|
|
28
|
+
collection or another owner. Connections can be selected with `connection`.
|
|
29
|
+
|
|
30
|
+
```ts
|
|
31
|
+
class EnvironmentDatatableController extends DatatableController {
|
|
32
|
+
protected configureTable(table: Datatable, req: Request) {
|
|
33
|
+
table.columnPreferences = {
|
|
34
|
+
collection: "environments_users_datatables",
|
|
35
|
+
owner: request => request.envUser?._id && request.env?._id
|
|
36
|
+
? { _environment_user: request.envUser._id, _env: request.env._id }
|
|
37
|
+
: null,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Set `columnPreferences = false` to opt out. Existing definitions keep their
|
|
44
|
+
widths until explicitly overridden. Old saved keys no longer present in the
|
|
45
|
+
request's allowed definitions are removed; if none remain, defaults are used.
|
|
46
|
+
New columns become available in the editor without altering a saved layout.
|
|
47
|
+
|
|
48
|
+
## Request protocol
|
|
49
|
+
|
|
50
|
+
All operations use the same authorized POST route and `_table` plus the host's
|
|
51
|
+
normal request context:
|
|
52
|
+
|
|
53
|
+
- `init_request: true` or `mode: "columns-get"`: returns effective `columns`,
|
|
54
|
+
`availableColumns`, `columnSelection` and the boolean `columnPreferences`.
|
|
55
|
+
- `mode: "columns-save", columns: [...]`: validates and saves the ordered visible
|
|
56
|
+
columns, then returns current initialization metadata.
|
|
57
|
+
- `mode: "columns-reset"`: removes the preference and returns backend defaults.
|
|
58
|
+
|
|
59
|
+
```json
|
|
60
|
+
{
|
|
61
|
+
"_table": "customers",
|
|
62
|
+
"mode": "columns-save",
|
|
63
|
+
"columns": [
|
|
64
|
+
{ "key": "email", "width": { "mode": "percent", "value": 40 } },
|
|
65
|
+
{ "key": "name", "width": { "mode": "auto" } },
|
|
66
|
+
{ "key": "created_at", "width": { "mode": "px", "value": 140 } }
|
|
67
|
+
]
|
|
68
|
+
}
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Omitting `width` follows the current backend default. Percentages are greater
|
|
72
|
+
than zero and at most 100; pixels are greater than zero and at most 10,000.
|
|
73
|
+
Unknown/duplicate columns or invalid widths return HTTP 400; a missing owner or
|
|
74
|
+
disabled persistence returns HTTP 403. Normal table/middleware authorization
|
|
75
|
+
must run for every operation. Exports use the visible columns in their saved order.
|
|
76
|
+
|
|
77
|
+
## Conditional lookups and custom data methods
|
|
78
|
+
|
|
79
|
+
Use `await this.getVisibleColumns(req)`, `getVisibleColumnKeys(req)` or
|
|
80
|
+
`isColumnVisible(req, "email")` anywhere in a custom data method. Reads are cached
|
|
81
|
+
per table instance and request, never across users or environments.
|
|
82
|
+
|
|
83
|
+
`aggregation(req, context)` and `subAggregation(req, context)` receive a
|
|
84
|
+
`DatatableColumnContext`. The controller also passes it as the second argument
|
|
85
|
+
to `getData(req, context)`, including overrides. `onData(req, result, context)`,
|
|
86
|
+
`onRow(row, req, context)` and `onAttributes(row, req, context)` receive it during
|
|
87
|
+
normal paginated loading. The context exposes `columns`, `columnKeys`,
|
|
88
|
+
`isColumnVisible(key)` and `isColumnRequired(key)`.
|
|
89
|
+
|
|
90
|
+
```ts
|
|
91
|
+
aggregation = async (req: Request, columns: DatatableColumnContext) => [
|
|
92
|
+
{ $match: { environment: req.env._id } },
|
|
93
|
+
...(columns.isColumnRequired("customer") ? [
|
|
94
|
+
{ $lookup: { from: "customers", localField: "customer_id", foreignField: "_id", as: "customer" } },
|
|
95
|
+
] : []),
|
|
96
|
+
];
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
Prefer `isColumnRequired` for query optimization: it also considers declared
|
|
100
|
+
mappings, active filter/search/sort fields and sums, as well as model-selector
|
|
101
|
+
preview fields. This avoids breaking a sort/filter on a hidden column. Dependencies
|
|
102
|
+
inside custom expressions, custom filters, exports or other calculated columns
|
|
103
|
+
remain the table author's responsibility.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@webtypen/webframez-core",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.101",
|
|
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",
|
|
@@ -36,7 +36,8 @@
|
|
|
36
36
|
"docs/queue-workers.md",
|
|
37
37
|
"docs/errors.md",
|
|
38
38
|
"build-cache.js",
|
|
39
|
-
"build-cache.d.ts"
|
|
39
|
+
"build-cache.d.ts",
|
|
40
|
+
"docs/datatables.md"
|
|
40
41
|
],
|
|
41
42
|
"scripts": {
|
|
42
43
|
"build": "tsc",
|