@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 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. Do not pass Expo `--clear` on normal cached builds.
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 promises = [];
61
- promises.push(new Promise((resolve) => __awaiter(this, void 0, void 0, function* () {
62
- out.filter = yield this.getFilter(req);
63
- resolve(true);
64
- })));
65
- promises.push(new Promise((resolve) => __awaiter(this, void 0, void 0, function* () {
66
- const cols = [];
67
- const colsData = yield this.getColumns(req);
68
- if (colsData && Object.keys(colsData).length > 0) {
69
- for (let i in colsData) {
70
- cols.push(Object.assign(Object.assign({}, colsData[i]), { key: i }));
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
- restApi(req: Request, res: Response): Promise<Response>;
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 (!req.body._table || req.body._table.trim() === "") {
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 = DatatableRegistry_1.DatatableRegistry.getTable(req.body._table);
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.init_request) {
33
- const table = new tableClass();
34
- return res.send({
35
- status: "success",
36
- data: yield table.getInit(req),
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 (!req.body._table || req.body._table.trim() === "") {
40
- return res.status(404).send({
41
- status: "error",
42
- message: "Missing table-key ...",
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 = (_a = table.selectableFunctions) === null || _a === void 0 ? void 0 : _a[req.body.apiFunction];
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 (!req.body._table || req.body._table.trim() === "") {
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 = DatatableRegistry_1.DatatableRegistry.getTable(req.body._table);
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.getColumns(req);
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;
@@ -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. */
@@ -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 });
@@ -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
  */
@@ -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; } });
@@ -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.97",
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",