@xh/hoist 77.0.0-SNAPSHOT.1761257771095 → 77.0.0-SNAPSHOT.1761672695220

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.
Files changed (32) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/admin/jsonsearch/impl/JsonSearchImplModel.ts +1 -1
  3. package/admin/tabs/activity/tracking/ActivityTrackingModel.ts +1 -1
  4. package/admin/tabs/cluster/instances/logs/LogDisplayModel.ts +1 -1
  5. package/admin/tabs/cluster/instances/logs/levels/LogLevelDialogModel.ts +1 -1
  6. package/admin/tabs/general/config/ConfigPanelModel.ts +1 -1
  7. package/admin/tabs/monitor/editor/MonitorEditorDialog.ts +1 -1
  8. package/admin/tabs/userData/jsonblob/JsonBlobModel.ts +1 -1
  9. package/admin/tabs/userData/prefs/UserPreferenceModel.ts +1 -1
  10. package/admin/tabs/userData/prefs/editor/PrefEditorModel.ts +1 -1
  11. package/admin/tabs/userData/users/UserModel.ts +1 -0
  12. package/build/types/core/AppSpec.d.ts +14 -7
  13. package/build/types/data/Field.d.ts +18 -9
  14. package/build/types/data/Store.d.ts +2 -1
  15. package/build/types/data/cube/Query.d.ts +1 -1
  16. package/build/types/data/cube/ViewRowData.d.ts +2 -0
  17. package/cmp/chart/impl/copyToClipboard.ts +14 -8
  18. package/cmp/treemap/TreeMap.ts +4 -14
  19. package/cmp/treemap/TreeMapModel.ts +2 -2
  20. package/core/AppSpec.ts +14 -7
  21. package/data/Field.ts +24 -20
  22. package/data/Store.ts +21 -8
  23. package/data/cube/Query.ts +1 -1
  24. package/data/cube/ViewRowData.ts +3 -0
  25. package/data/cube/row/AggregateRow.ts +1 -0
  26. package/data/cube/row/BucketRow.ts +1 -0
  27. package/data/cube/row/LeafRow.ts +1 -1
  28. package/format/FormatNumber.ts +2 -2
  29. package/kit/highcharts/index.ts +2 -2
  30. package/package.json +1 -1
  31. package/promise/Promise.ts +3 -3
  32. package/tsconfig.tsbuildinfo +1 -1
package/CHANGELOG.md CHANGED
@@ -2,6 +2,27 @@
2
2
 
3
3
  ## 77.0.0-SNAPSHOT - unreleased
4
4
 
5
+ ### 💥 Breaking Changes
6
+
7
+ * The `disableXssProtection` flag supported by `AppSpec` and `FieldSpec` has been removed and
8
+ replaced with its opposite, `enableXssProtection`, now an opt-in feature.
9
+ * While store-based XSS protection via DomPurify is still available to apps that can display
10
+ untrusted or potentially malicious data, this is an uncommon use case for Hoist apps and was
11
+ deemed to not provide enough benefit relative to potential performance pitfalls for most
12
+ applications. In addition, the core change to React-based AG Grid rendering has reduced the
13
+ attack surface for such exploits relative to when this system was first implemented.
14
+ * Apps that were previously opting-out via `disableXssProtection` should simply remove that
15
+ flag. Apps for which this protection remains important should enable at either the app level
16
+ or for selected Fields and/or Stores.
17
+
18
+ ## 76.2.0 - 2025-10-22
19
+
20
+ ### ⚙️ Technical
21
+
22
+ * Performance improvements to Store for large data sets.
23
+ * New property `cubeRowType` on `ViewRowData` supports identifying bucketed rows.
24
+ * `waitFor` can accept a null value for a timeout.
25
+
5
26
  ## 76.1.0 - 2025-10-17
6
27
 
7
28
  ### 🎁 New Features
@@ -73,7 +73,7 @@ export class JsonSearchImplModel extends HoistModel {
73
73
  override onLinked() {
74
74
  this.gridModel = new GridModel({
75
75
  ...this.gridModelConfig,
76
- emptyText: 'No matches found...',
76
+ emptyText: 'No matches found.',
77
77
  selModel: 'single'
78
78
  });
79
79
 
@@ -395,7 +395,7 @@ export class ActivityTrackingModel extends HoistModel implements ActivityDetailP
395
395
  treeStyle: TreeStyle.HIGHLIGHTS_AND_BORDERS,
396
396
  autosizeOptions: {mode: 'managed', includeCollapsedChildren: true},
397
397
  exportOptions: {filename: exportFilename('activity-summary')},
398
- emptyText: 'No activity reported...',
398
+ emptyText: 'No activity reported.',
399
399
  sortBy: ['cubeLabel'],
400
400
  expandLevel: 1,
401
401
  levelLabels: () => ['Total', ...this.groupingChooserModel.valueDisplayNames],
@@ -136,7 +136,7 @@ export class LogDisplayModel extends HoistModel {
136
136
  hideHeaders: true,
137
137
  rowBorders: false,
138
138
  sizingMode: 'tiny',
139
- emptyText: 'No log entries found...',
139
+ emptyText: 'No log entries found.',
140
140
  sortBy: 'rowNum|asc',
141
141
  autosizeOptions: {mode: 'disabled'},
142
142
  store: {
@@ -51,7 +51,7 @@ export class LogLevelDialogModel extends HoistModel {
51
51
  showRefreshButton: true,
52
52
  store: {
53
53
  url: 'rest/logLevelAdmin',
54
- fieldDefaults: {disableXssProtection: true},
54
+ fieldDefaults: {enableXssProtection: false},
55
55
  fields: [
56
56
  {
57
57
  name: 'name',
@@ -57,7 +57,7 @@ export class ConfigPanelModel extends HoistModel {
57
57
  store: new RestStore({
58
58
  url: 'rest/configAdmin',
59
59
  reloadLookupsOnLoad: true,
60
- fieldDefaults: {disableXssProtection: true},
60
+ fieldDefaults: {enableXssProtection: false},
61
61
  fields: [
62
62
  {...(Col.name.field as FieldSpec), required},
63
63
  {
@@ -51,7 +51,7 @@ const modelSpec: RestGridConfig = {
51
51
  showRefreshButton: true,
52
52
  store: {
53
53
  url: 'rest/monitorAdmin',
54
- fieldDefaults: {disableXssProtection: true},
54
+ fieldDefaults: {enableXssProtection: false},
55
55
  fields: [
56
56
  {...(MCol.code.field as FieldSpec), required},
57
57
  MCol.metricUnit.field,
@@ -51,7 +51,7 @@ export class JsonBlobModel extends HoistModel {
51
51
  store: {
52
52
  url: 'rest/jsonBlobAdmin',
53
53
  reloadLookupsOnLoad: true,
54
- fieldDefaults: {disableXssProtection: true},
54
+ fieldDefaults: {enableXssProtection: false},
55
55
  fields: [
56
56
  {...(JBCol.token.field as FieldSpec), editable: false},
57
57
  JBCol.owner.field,
@@ -34,7 +34,7 @@ export class UserPreferenceModel extends HoistModel {
34
34
  store: {
35
35
  url: 'rest/userPreferenceAdmin',
36
36
  reloadLookupsOnLoad: true,
37
- fieldDefaults: {disableXssProtection: true},
37
+ fieldDefaults: {enableXssProtection: false},
38
38
  fields: [
39
39
  {
40
40
  ...(Col.name.field as FieldSpec),
@@ -47,7 +47,7 @@ export class PrefEditorModel extends HoistModel {
47
47
  store: {
48
48
  url: 'rest/preferenceAdmin',
49
49
  reloadLookupsOnLoad: true,
50
- fieldDefaults: {disableXssProtection: true},
50
+ fieldDefaults: {enableXssProtection: false},
51
51
  fields: [
52
52
  {...(Col.name.field as FieldSpec), required},
53
53
  {
@@ -25,6 +25,7 @@ export class UserModel extends HoistModel {
25
25
  makeObservable(this);
26
26
 
27
27
  this.gridModel = new GridModel({
28
+ emptyText: 'No users found.',
28
29
  persistWith: this.persistWith,
29
30
  colChooserModel: true,
30
31
  enableExport: true,
@@ -58,12 +58,19 @@ export declare class AppSpec<T extends HoistAppModel = HoistAppModel> {
58
58
  */
59
59
  disableWebSockets?: boolean;
60
60
  /**
61
- * True to disable Field-level XSS protection by default across all Stores/Fields in the app.
62
- * For use with secure, internal apps that do not display arbitrary/external user input and
63
- * have tight performance tolerances and/or load very large record sets.
64
- * @see FieldSpec.disableXssProtection
61
+ * True to enable Field-level XSS protection by default across all Stores/Fields in the app.
62
+ * Available as an extra precaution for use with apps that might display arbitrary input from
63
+ * untrusted or external users. This feature does exact a minor performance penalty during data
64
+ * parsing, which can be significant in aggregate for very large stores containing records with
65
+ * many `string` fields.
66
+ *
67
+ * Note: this flag and its default behavior was changed as of Hoist v77 to be `false`, i.e.
68
+ * Store-level XSS protection *disabled* by default, in keeping with Hoist's primary use-case:
69
+ * building secured internal apps with large datasets and tight performance tolerances.
70
+ *
71
+ * @see FieldSpec.enableXssProtection
65
72
  */
66
- disableXssProtection?: boolean;
73
+ enableXssProtection?: boolean;
67
74
  /**
68
75
  * True to show a login form on initialization when not authenticated. Default is `false` as
69
76
  * most Hoist applications are expected to use OAuth or SSO for authn.
@@ -111,7 +118,7 @@ export declare class AppSpec<T extends HoistAppModel = HoistAppModel> {
111
118
  trackAppLoad?: boolean;
112
119
  /** @deprecated - use {@link AppSpec.disableWebSockets} instead. */
113
120
  webSocketsEnabled?: boolean;
114
- constructor({ authModelClass, checkAccess, clientAppCode, clientAppName, componentClass, containerClass, disableWebSockets, disableXssProtection, enableLoginForm, enableLogout, idlePanel, isMobileApp, lockoutMessage, lockoutPanel, loginMessage, modelClass, showBrowserContextMenu, trackAppLoad, webSocketsEnabled }: {
121
+ constructor({ authModelClass, checkAccess, clientAppCode, clientAppName, componentClass, containerClass, disableWebSockets, enableXssProtection, enableLoginForm, enableLogout, idlePanel, isMobileApp, lockoutMessage, lockoutPanel, loginMessage, modelClass, showBrowserContextMenu, trackAppLoad, webSocketsEnabled }: {
115
122
  authModelClass?: typeof HoistAuthModel;
116
123
  checkAccess: any;
117
124
  clientAppCode?: string;
@@ -119,7 +126,7 @@ export declare class AppSpec<T extends HoistAppModel = HoistAppModel> {
119
126
  componentClass: any;
120
127
  containerClass: any;
121
128
  disableWebSockets?: boolean;
122
- disableXssProtection?: boolean;
129
+ enableXssProtection?: boolean;
123
130
  enableLoginForm?: boolean;
124
131
  enableLogout?: boolean;
125
132
  idlePanel?: any;
@@ -17,15 +17,24 @@ export interface FieldSpec {
17
17
  /** Rules to apply to this field. */
18
18
  rules?: RuleLike[];
19
19
  /**
20
- * True to disable built-in XSS (cross-site scripting) protection, applied by default to all
21
- * incoming String values using {@link https://github.com/cure53/DOMPurify | DOMPurify}.
20
+ * True to enable built-in XSS (cross-site scripting) protection to all incoming String values
21
+ * using {@link https://github.com/cure53/DOMPurify | DOMPurify}.
22
22
  *
23
23
  * DOMPurify provides fast escaping of dangerous HTML, scripting, and other content that can be
24
24
  * used to execute XSS attacks, while allowing common and expected HTML and style tags.
25
25
  *
26
- * Please contact XH if you find yourself needing to disable this protection!
26
+ * This feature does exact a minor performance penalty during data parsing, which can be
27
+ * significant in aggregate for very large stores containing records with many `string` fields.
28
+ *
29
+ * For extra safety, apps which are open to potentially-untrusted users or display other
30
+ * potentially dangerous string content can opt into this setting app-wide via
31
+ * {@link AppSpec.enableXssProtection}. Field-level setting will override any app-level default.
32
+ *
33
+ * Note: this flag and its default behavior was changed as of Hoist v77 to be `false`, i.e.
34
+ * Store-level XSS protection *disabled* by default, in keeping with Hoist's primary use-case:
35
+ * building secured internal apps with large datasets and tight performance tolerances.
27
36
  */
28
- disableXssProtection?: boolean;
37
+ enableXssProtection?: boolean;
29
38
  }
30
39
  /** Metadata for an individual data field within a {@link StoreRecord}. */
31
40
  export declare class Field {
@@ -35,8 +44,8 @@ export declare class Field {
35
44
  readonly displayName: string;
36
45
  readonly defaultValue: any;
37
46
  readonly rules: Rule[];
38
- readonly disableXssProtection: boolean;
39
- constructor({ name, type, displayName, defaultValue, rules, disableXssProtection }: FieldSpec);
47
+ readonly enableXssProtection: boolean;
48
+ constructor({ name, type, displayName, defaultValue, rules, enableXssProtection }: FieldSpec);
40
49
  parseVal(val: any): any;
41
50
  isEqual(val1: any, val2: any): boolean;
42
51
  private processRuleSpecs;
@@ -46,11 +55,11 @@ export declare class Field {
46
55
  * @param val - raw value to parse.
47
56
  * @param type - data type of the field to use for possible conversion.
48
57
  * @param defaultValue - typed value to return if `val` undefined or null.
49
- * @param disableXssProtection - true to disable XSS (cross-site scripting) protection.
50
- * @see {@link FieldConfig} docs for additional details.
58
+ * @param enableXssProtection - true to enable XSS (cross-site scripting) protection.
59
+ * See {@link FieldSpec.enableXssProtection} for additional details.
51
60
  * @returns resulting value, potentially parsed or cast as per type.
52
61
  */
53
- export declare function parseFieldValue(val: any, type: FieldType, defaultValue?: any, disableXssProtection?: boolean): any;
62
+ export declare function parseFieldValue(val: any, type: FieldType, defaultValue?: any, enableXssProtection?: boolean): any;
54
63
  /** Data types for Fields used within Hoist Store Records and Cubes. */
55
64
  export declare const FieldType: Readonly<{
56
65
  TAGS: "tags";
@@ -12,7 +12,7 @@ export interface StoreConfig {
12
12
  * Default configs applied to `Field` instances constructed internally by this Store.
13
13
  * @see FieldSpec
14
14
  */
15
- fieldDefaults?: any;
15
+ fieldDefaults?: Omit<FieldSpec, 'name'>;
16
16
  /**
17
17
  * Specification for producing an immutable unique id for each record. May be provided as
18
18
  * either a string property name (default is 'id') or a function that receives the raw data
@@ -401,6 +401,7 @@ export declare class Store extends HoistBase {
401
401
  private rebuildFiltered;
402
402
  private createRecord;
403
403
  private createRecords;
404
+ private get summaryRecordIds();
404
405
  private parseRaw;
405
406
  private parseUpdate;
406
407
  private createDataDefaults;
@@ -81,7 +81,7 @@ export interface QueryConfig {
81
81
  *
82
82
  * This can be used to break selected aggregations into sub-groups dynamically, without having
83
83
  * to define another dimension in the Cube and have it apply to all aggregations. See the
84
- * {@link BucketSpec} interface for additional information.
84
+ * {@link BucketSpecFn} type and {@link BucketSpec} interface for additional information.
85
85
  *
86
86
  * Defaults to {@link Cube.bucketSpecFn}.
87
87
  */
@@ -7,6 +7,8 @@ export declare class ViewRowData {
7
7
  constructor(id: string);
8
8
  /** Unique id. */
9
9
  id: string;
10
+ /** Denotes a type for the row */
11
+ cubeRowType: 'leaf' | 'aggregate' | 'bucket';
10
12
  /**
11
13
  * Label of the row. The dimension value or, for leaf rows. the underlying cubeId.
12
14
  * Suitable for display, although apps will typically wish to customize leaf row rendering.
@@ -24,14 +24,14 @@ export function installCopyToClipboard(Highcharts) {
24
24
  try {
25
25
  const blobPromise = convertChartToPngAsync(this),
26
26
  clipboardItemInput = new window.ClipboardItem({
27
- // Safari requires an unresolved promise. See https://bugs.webkit.org/show_bug.cgi?id=222262 for discussion
27
+ // Safari requires an unresolved promise. See https://bugs.webkit.org/show_bug.cgi?id=222262 for discussion
28
28
  'image/png': Highcharts.isSafari ? blobPromise : await blobPromise
29
29
  });
30
30
  await window.navigator.clipboard.write([clipboardItemInput]);
31
31
  XH.successToast('Chart copied to clipboard');
32
32
  } catch (e) {
33
33
  XH.handleException(e, {showAlert: false, logOnServer: true});
34
- XH.dangerToast('Error: Chart could not be copied. This error has been logged.');
34
+ XH.dangerToast('Error: Chart could not be copied. This error has been logged.');
35
35
  }
36
36
  }
37
37
  });
@@ -41,8 +41,14 @@ export function installCopyToClipboard(Highcharts) {
41
41
  // Implementation
42
42
  //------------------
43
43
  async function convertChartToPngAsync(chart) {
44
- // v12 replacement for getSVGForLocalExport
45
- const svg = chart.getSVG(),
44
+ const svg = await new Promise((resolve, reject) =>
45
+ chart.getSVGForLocalExport(
46
+ chart.options.exporting,
47
+ {},
48
+ () => reject('Cannot fallback to export server'),
49
+ svg => resolve(svg)
50
+ )
51
+ ),
46
52
  svgUrl = svgToDataUrl(svg),
47
53
  pngDataUrl = await svgUrlToPngDataUrlAsync(svgUrl),
48
54
  ret = await loadBlob(pngDataUrl);
@@ -59,7 +65,7 @@ function memoryCleanup(svgUrl) {
59
65
  }
60
66
 
61
67
  /**
62
- * Convert dataUri to blob
68
+ * Convert dataUri converted to blob
63
69
  */
64
70
  async function loadBlob(dataUrl) {
65
71
  const fetched = await fetch(dataUrl);
@@ -78,7 +84,7 @@ function svgToDataUrl(svg) {
78
84
  try {
79
85
  // Safari requires data URI since it doesn't allow navigation to blob
80
86
  // URLs.
81
- // foreignObjects don't work well in Blobs in Chrome (#14780).
87
+ // foreignObjects dont work well in Blobs in Chrome (#14780).
82
88
  if (!isWebKitButNotChrome && svg.indexOf('<foreignObject') === -1) {
83
89
  return domurl.createObjectURL(
84
90
  new window.Blob([svg], {
@@ -88,12 +94,12 @@ function svgToDataUrl(svg) {
88
94
  }
89
95
  } catch (e) {}
90
96
 
91
- // Safari, Firefox, or SVGs with foreignObject fallback
97
+ // safari, firefox, or svgs with foreignObect returns this
92
98
  return 'data:image/svg+xml;charset=UTF-8,' + encodeURIComponent(svg);
93
99
  }
94
100
 
95
101
  /**
96
- * Get PNG data:URL from image URL.
102
+ * Get PNG data:URL from image URL. Pass in callbacks to handle results.
97
103
  */
98
104
  async function svgUrlToPngDataUrlAsync(imageURL, scale = 1) {
99
105
  const img = new window.Image(),
@@ -172,6 +172,8 @@ class TreeMapLocalModel extends HoistModel {
172
172
  this.prevConfig = cloneDeep(chartCfg);
173
173
  this.createChart(config);
174
174
  }
175
+
176
+ this.updateLabelVisibility();
175
177
  }
176
178
 
177
179
  createChart(config) {
@@ -193,25 +195,13 @@ class TreeMapLocalModel extends HoistModel {
193
195
 
194
196
  assign(config.chart, parentDims, {renderTo: chartElem});
195
197
  this.withDebug(['Creating new TreeMap', `${newData.length} records`], () => {
196
- this.chart = Highcharts.chart(config, () => {
197
- this.updateLabelVisibility();
198
- });
198
+ this.chart = Highcharts.chart(config);
199
199
  });
200
200
  }
201
201
 
202
202
  @logWithDebug
203
203
  reloadSeriesData(newData) {
204
- if (!this.chart) return;
205
-
206
- this.chart.series[0].setData(newData, true, false);
207
-
208
- // Use an event handler to trigger label updates
209
- // This approach was required when `cluster` series option is enabled
210
- const onRedraw = () => {
211
- this.updateLabelVisibility();
212
- Highcharts.removeEvent(this.chart, 'redraw', onRedraw);
213
- };
214
- Highcharts.addEvent(this.chart, 'redraw', onRedraw);
204
+ this.chart?.series[0].setData(newData, true, false);
215
205
  }
216
206
 
217
207
  startResize = ({width, height}) => {
@@ -465,7 +465,7 @@ export class TreeMapModel extends HoistModel {
465
465
  //----------------------
466
466
  defaultOnClick = (record, e) => {
467
467
  const {gridModel} = this;
468
- if (!gridModel || !record) return;
468
+ if (!gridModel) return;
469
469
 
470
470
  // Select nodes in grid
471
471
  const {selModel} = gridModel;
@@ -477,7 +477,7 @@ export class TreeMapModel extends HoistModel {
477
477
  };
478
478
 
479
479
  defaultOnDoubleClick = record => {
480
- if (!this.gridModel?.treeMode || isEmpty(record?.children)) return;
480
+ if (!this.gridModel?.treeMode || isEmpty(record.children)) return;
481
481
  this.toggleNodeExpanded(record.treePath);
482
482
  };
483
483
  }
package/core/AppSpec.ts CHANGED
@@ -71,12 +71,19 @@ export class AppSpec<T extends HoistAppModel = HoistAppModel> {
71
71
  disableWebSockets?: boolean;
72
72
 
73
73
  /**
74
- * True to disable Field-level XSS protection by default across all Stores/Fields in the app.
75
- * For use with secure, internal apps that do not display arbitrary/external user input and
76
- * have tight performance tolerances and/or load very large record sets.
77
- * @see FieldSpec.disableXssProtection
74
+ * True to enable Field-level XSS protection by default across all Stores/Fields in the app.
75
+ * Available as an extra precaution for use with apps that might display arbitrary input from
76
+ * untrusted or external users. This feature does exact a minor performance penalty during data
77
+ * parsing, which can be significant in aggregate for very large stores containing records with
78
+ * many `string` fields.
79
+ *
80
+ * Note: this flag and its default behavior was changed as of Hoist v77 to be `false`, i.e.
81
+ * Store-level XSS protection *disabled* by default, in keeping with Hoist's primary use-case:
82
+ * building secured internal apps with large datasets and tight performance tolerances.
83
+ *
84
+ * @see FieldSpec.enableXssProtection
78
85
  */
79
- disableXssProtection?: boolean;
86
+ enableXssProtection?: boolean;
80
87
 
81
88
  /**
82
89
  * True to show a login form on initialization when not authenticated. Default is `false` as
@@ -144,7 +151,7 @@ export class AppSpec<T extends HoistAppModel = HoistAppModel> {
144
151
  componentClass,
145
152
  containerClass,
146
153
  disableWebSockets = false,
147
- disableXssProtection = false,
154
+ enableXssProtection = false,
148
155
  enableLoginForm = false,
149
156
  enableLogout = false,
150
157
  idlePanel = null,
@@ -191,7 +198,7 @@ export class AppSpec<T extends HoistAppModel = HoistAppModel> {
191
198
  this.componentClass = componentClass;
192
199
  this.containerClass = containerClass;
193
200
  this.disableWebSockets = disableWebSockets;
194
- this.disableXssProtection = disableXssProtection;
201
+ this.enableXssProtection = enableXssProtection;
195
202
  this.enableLoginForm = enableLoginForm;
196
203
  this.enableLogout = enableLogout;
197
204
  this.idlePanel = idlePanel;
package/data/Field.ts CHANGED
@@ -36,15 +36,24 @@ export interface FieldSpec {
36
36
  rules?: RuleLike[];
37
37
 
38
38
  /**
39
- * True to disable built-in XSS (cross-site scripting) protection, applied by default to all
40
- * incoming String values using {@link https://github.com/cure53/DOMPurify | DOMPurify}.
39
+ * True to enable built-in XSS (cross-site scripting) protection to all incoming String values
40
+ * using {@link https://github.com/cure53/DOMPurify | DOMPurify}.
41
41
  *
42
42
  * DOMPurify provides fast escaping of dangerous HTML, scripting, and other content that can be
43
43
  * used to execute XSS attacks, while allowing common and expected HTML and style tags.
44
44
  *
45
- * Please contact XH if you find yourself needing to disable this protection!
45
+ * This feature does exact a minor performance penalty during data parsing, which can be
46
+ * significant in aggregate for very large stores containing records with many `string` fields.
47
+ *
48
+ * For extra safety, apps which are open to potentially-untrusted users or display other
49
+ * potentially dangerous string content can opt into this setting app-wide via
50
+ * {@link AppSpec.enableXssProtection}. Field-level setting will override any app-level default.
51
+ *
52
+ * Note: this flag and its default behavior was changed as of Hoist v77 to be `false`, i.e.
53
+ * Store-level XSS protection *disabled* by default, in keeping with Hoist's primary use-case:
54
+ * building secured internal apps with large datasets and tight performance tolerances.
46
55
  */
47
- disableXssProtection?: boolean;
56
+ enableXssProtection?: boolean;
48
57
  }
49
58
 
50
59
  /** Metadata for an individual data field within a {@link StoreRecord}. */
@@ -58,7 +67,7 @@ export class Field {
58
67
  readonly displayName: string;
59
68
  readonly defaultValue: any;
60
69
  readonly rules: Rule[];
61
- readonly disableXssProtection: boolean;
70
+ readonly enableXssProtection: boolean;
62
71
 
63
72
  constructor({
64
73
  name,
@@ -66,19 +75,19 @@ export class Field {
66
75
  displayName,
67
76
  defaultValue = null,
68
77
  rules = [],
69
- disableXssProtection = XH.appSpec.disableXssProtection
78
+ enableXssProtection = XH.appSpec.enableXssProtection
70
79
  }: FieldSpec) {
71
80
  this.name = name;
72
81
  this.type = type;
73
82
  this.displayName = withDefault(displayName, genDisplayName(name));
74
83
  this.defaultValue = defaultValue;
75
84
  this.rules = this.processRuleSpecs(rules);
76
- this.disableXssProtection = disableXssProtection;
85
+ this.enableXssProtection = enableXssProtection;
77
86
  }
78
87
 
79
88
  parseVal(val: any): any {
80
- const {type, defaultValue, disableXssProtection} = this;
81
- return parseFieldValue(val, type, defaultValue, disableXssProtection);
89
+ const {type, defaultValue, enableXssProtection} = this;
90
+ return parseFieldValue(val, type, defaultValue, enableXssProtection);
82
91
  }
83
92
 
84
93
  isEqual(val1: any, val2: any): boolean {
@@ -102,35 +111,30 @@ export class Field {
102
111
  * @param val - raw value to parse.
103
112
  * @param type - data type of the field to use for possible conversion.
104
113
  * @param defaultValue - typed value to return if `val` undefined or null.
105
- * @param disableXssProtection - true to disable XSS (cross-site scripting) protection.
106
- * @see {@link FieldConfig} docs for additional details.
114
+ * @param enableXssProtection - true to enable XSS (cross-site scripting) protection.
115
+ * See {@link FieldSpec.enableXssProtection} for additional details.
107
116
  * @returns resulting value, potentially parsed or cast as per type.
108
117
  */
109
118
  export function parseFieldValue(
110
119
  val: any,
111
120
  type: FieldType,
112
121
  defaultValue: any = null,
113
- disableXssProtection = XH.appSpec.disableXssProtection
122
+ enableXssProtection: boolean = XH.appSpec.enableXssProtection
114
123
  ): any {
115
124
  if (val === undefined || val === null) val = defaultValue;
116
125
  if (val === null) return val;
117
126
 
118
- const sanitizeValue = v => {
119
- if (disableXssProtection || !isString(v)) return v;
120
- return DOMPurify.sanitize(v);
121
- };
122
-
123
127
  switch (type) {
124
128
  case 'tags':
125
129
  val = castArray(val);
126
130
  val = val.map(v => {
127
- v = sanitizeValue(v);
131
+ v = !enableXssProtection || !isString(v) ? v : DOMPurify.sanitize(v);
128
132
  return v.toString();
129
133
  });
130
134
  return val;
131
135
  case 'auto':
132
136
  case 'json':
133
- return sanitizeValue(val);
137
+ return !enableXssProtection || !isString(val) ? val : DOMPurify.sanitize(val);
134
138
  case 'int':
135
139
  val = toNumber(val);
136
140
  return isFinite(val) ? Math.trunc(val) : null;
@@ -140,7 +144,7 @@ export function parseFieldValue(
140
144
  return !!val;
141
145
  case 'pwd':
142
146
  case 'string':
143
- val = sanitizeValue(val);
147
+ val = !enableXssProtection || !isString(val) ? val : DOMPurify.sanitize(val);
144
148
  return val.toString();
145
149
  case 'date':
146
150
  return isDate(val) ? val : new Date(val);
package/data/Store.ts CHANGED
@@ -44,7 +44,7 @@ export interface StoreConfig {
44
44
  * Default configs applied to `Field` instances constructed internally by this Store.
45
45
  * @see FieldSpec
46
46
  */
47
- fieldDefaults?: any;
47
+ fieldDefaults?: Omit<FieldSpec, 'name'>;
48
48
 
49
49
  /**
50
50
  * Specification for producing an immutable unique id for each record. May be provided as
@@ -978,17 +978,20 @@ export class Store extends HoistBase {
978
978
  this.summaryRecords = null;
979
979
  }
980
980
 
981
- private parseFields(fields: any[], defaults: any): Field[] {
981
+ private parseFields(
982
+ fields: Array<string | FieldSpec | Field>,
983
+ defaults: Omit<FieldSpec, 'name'>
984
+ ): Field[] {
982
985
  const ret = fields.map(f => {
983
986
  if (f instanceof Field) return f;
984
987
 
985
- if (isString(f)) f = {name: f};
988
+ let fieldSpec: FieldSpec = isString(f) ? {name: f} : f;
986
989
 
987
990
  if (!isEmpty(defaults)) {
988
- f = defaultsDeep({}, f, defaults);
991
+ fieldSpec = defaultsDeep({}, fieldSpec, defaults);
989
992
  }
990
993
 
991
- return new this.defaultFieldClass(f);
994
+ return new this.defaultFieldClass(fieldSpec);
992
995
  });
993
996
 
994
997
  throwIf(
@@ -1041,26 +1044,36 @@ export class Store extends HoistBase {
1041
1044
  return ret;
1042
1045
  }
1043
1046
 
1044
- private createRecords(rawData: PlainObject[], parent: StoreRecord, recordMap = new Map()) {
1047
+ private createRecords(
1048
+ rawData: PlainObject[],
1049
+ parent: StoreRecord,
1050
+ recordMap: Map<StoreRecordId, StoreRecord> = new Map(),
1051
+ summaryRecordIds: Set<StoreRecordId> = this.summaryRecordIds
1052
+ ) {
1045
1053
  const {loadTreeData, loadTreeDataFrom} = this;
1054
+
1046
1055
  rawData.forEach(raw => {
1047
1056
  const rec = this.createRecord(raw, parent),
1048
1057
  {id} = rec;
1049
1058
 
1050
1059
  throwIf(
1051
- recordMap.has(id) || this.summaryRecords?.some(it => it.id === id),
1060
+ recordMap.has(id) || summaryRecordIds.has(id),
1052
1061
  `ID ${id} is not unique. Use the 'Store.idSpec' config to resolve a unique ID for each record.`
1053
1062
  );
1054
1063
 
1055
1064
  recordMap.set(id, rec);
1056
1065
 
1057
1066
  if (loadTreeData && raw[loadTreeDataFrom]) {
1058
- this.createRecords(raw[loadTreeDataFrom], rec, recordMap);
1067
+ this.createRecords(raw[loadTreeDataFrom], rec, recordMap, summaryRecordIds);
1059
1068
  }
1060
1069
  });
1061
1070
  return recordMap;
1062
1071
  }
1063
1072
 
1073
+ private get summaryRecordIds(): Set<StoreRecordId> {
1074
+ return new Set(this.summaryRecords?.map(it => it.id) ?? []);
1075
+ }
1076
+
1064
1077
  private parseRaw(data: PlainObject): PlainObject {
1065
1078
  // a) create/prepare the data object
1066
1079
  const ret = Object.create(this._dataDefaults);
@@ -109,7 +109,7 @@ export interface QueryConfig {
109
109
  *
110
110
  * This can be used to break selected aggregations into sub-groups dynamically, without having
111
111
  * to define another dimension in the Cube and have it apply to all aggregations. See the
112
- * {@link BucketSpec} interface for additional information.
112
+ * {@link BucketSpecFn} type and {@link BucketSpec} interface for additional information.
113
113
  *
114
114
  * Defaults to {@link Cube.bucketSpecFn}.
115
115
  */
@@ -19,6 +19,9 @@ export class ViewRowData {
19
19
  /** Unique id. */
20
20
  id: string;
21
21
 
22
+ /** Denotes a type for the row */
23
+ cubeRowType: 'leaf' | 'aggregate' | 'bucket';
24
+
22
25
  /**
23
26
  * Label of the row. The dimension value or, for leaf rows. the underlying cubeId.
24
27
  * Suitable for display, although apps will typically wish to customize leaf row rendering.
@@ -38,6 +38,7 @@ export class AggregateRow extends BaseRow {
38
38
 
39
39
  this.dim = dim;
40
40
  this.dimName = dimName;
41
+ this.data.cubeRowType = 'aggregate';
41
42
  this.data.cubeLabel = strVal;
42
43
  this.data.cubeDimension = dimName;
43
44