@genesislcap/pbc-reporting-ui 14.396.3 → 14.397.0

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 (73) hide show
  1. package/dist/dts/new/main/edit-config/col-filters/col-filters-grid.helpers.d.ts.map +1 -1
  2. package/dist/dts/new/main/edit-config/datasource-config/datasource-config-item.d.ts +6 -2
  3. package/dist/dts/new/main/edit-config/datasource-config/datasource-config-item.d.ts.map +1 -1
  4. package/dist/dts/new/main/edit-config/datasource-config/datasource-config-item.styles.d.ts.map +1 -1
  5. package/dist/dts/new/main/edit-config/datasource-config/datasource-config-item.template.d.ts.map +1 -1
  6. package/dist/dts/new/main/edit-config/datasource-config/datasources-config-container.helpers.d.ts.map +1 -1
  7. package/dist/dts/new/main/edit-config/datasource-config/datasources-config-container.template.d.ts.map +1 -1
  8. package/dist/dts/new/main/edit-config/shared/datasource-data-base-component.helpers.ts.d.ts.map +1 -1
  9. package/dist/dts/new/store/slices/datasources-config.d.ts +20 -6
  10. package/dist/dts/new/store/slices/datasources-config.d.ts.map +1 -1
  11. package/dist/dts/new/store/slices/types.d.ts +19 -2
  12. package/dist/dts/new/store/slices/types.d.ts.map +1 -1
  13. package/dist/dts/new/store/store.d.ts +133 -35
  14. package/dist/dts/new/store/store.d.ts.map +1 -1
  15. package/dist/dts/new/types/misc.d.ts +4 -2
  16. package/dist/dts/new/types/misc.d.ts.map +1 -1
  17. package/dist/dts/new/utils/alias-generator.d.ts +8 -0
  18. package/dist/dts/new/utils/alias-generator.d.ts.map +1 -0
  19. package/dist/dts/new/utils/alias-generator.test.d.ts +2 -0
  20. package/dist/dts/new/utils/alias-generator.test.d.ts.map +1 -0
  21. package/dist/dts/new/utils/index.d.ts +1 -0
  22. package/dist/dts/new/utils/index.d.ts.map +1 -1
  23. package/dist/dts/new/utils/tooltip.d.ts +3 -0
  24. package/dist/dts/new/utils/tooltip.d.ts.map +1 -1
  25. package/dist/dts/new/utils/transformers.d.ts +1 -1
  26. package/dist/dts/new/utils/transformers.d.ts.map +1 -1
  27. package/dist/esm/new/main/edit-config/col-filters/col-filters-grid.helpers.js +3 -1
  28. package/dist/esm/new/main/edit-config/col-filters/col-filters-grid.helpers.test.js +71 -6
  29. package/dist/esm/new/main/edit-config/col-rename-alias/col-rename-alias-grid.helpers.test.js +32 -4
  30. package/dist/esm/new/main/edit-config/data-transforms-derived-fields/data-transforms.helpers.test.js +10 -2
  31. package/dist/esm/new/main/edit-config/datasource-config/datasource-config-item.js +100 -10
  32. package/dist/esm/new/main/edit-config/datasource-config/datasource-config-item.styles.js +6 -0
  33. package/dist/esm/new/main/edit-config/datasource-config/datasource-config-item.template.js +38 -3
  34. package/dist/esm/new/main/edit-config/datasource-config/datasources-config-container.helpers.js +11 -7
  35. package/dist/esm/new/main/edit-config/datasource-config/datasources-config-container.helpers.test.js +29 -22
  36. package/dist/esm/new/main/edit-config/datasource-config/datasources-config-container.template.js +3 -0
  37. package/dist/esm/new/main/edit-config/shared/datasource-data-base-component.helpers.ts.js +16 -8
  38. package/dist/esm/new/main/edit-config/shared/datasource-data-base-component.test.js +13 -2
  39. package/dist/esm/new/main/edit-config/tabbed-datasource-container/tabbed-datasource-container.template.js +1 -1
  40. package/dist/esm/new/store/slices/datasources-config.js +50 -11
  41. package/dist/esm/new/store/slices/types.js +1 -0
  42. package/dist/esm/new/utils/alias-generator.js +16 -0
  43. package/dist/esm/new/utils/alias-generator.test.js +36 -0
  44. package/dist/esm/new/utils/index.js +1 -0
  45. package/dist/esm/new/utils/tooltip.js +16 -0
  46. package/dist/esm/new/utils/transformers.js +20 -6
  47. package/dist/esm/new/utils/transformers.test.js +61 -11
  48. package/dist/esm/new/utils/validators.test.js +35 -21
  49. package/dist/tsconfig.tsbuildinfo +1 -1
  50. package/package.json +22 -22
  51. package/src/new/main/edit-config/col-filters/col-filters-grid.helpers.test.ts +76 -6
  52. package/src/new/main/edit-config/col-filters/col-filters-grid.helpers.ts +4 -1
  53. package/src/new/main/edit-config/col-rename-alias/col-rename-alias-grid.helpers.test.ts +32 -4
  54. package/src/new/main/edit-config/data-transforms-derived-fields/data-transforms.helpers.test.ts +10 -2
  55. package/src/new/main/edit-config/datasource-config/datasource-config-item.styles.ts +6 -0
  56. package/src/new/main/edit-config/datasource-config/datasource-config-item.template.ts +62 -3
  57. package/src/new/main/edit-config/datasource-config/datasource-config-item.ts +107 -8
  58. package/src/new/main/edit-config/datasource-config/datasources-config-container.helpers.test.ts +32 -23
  59. package/src/new/main/edit-config/datasource-config/datasources-config-container.helpers.ts +18 -10
  60. package/src/new/main/edit-config/datasource-config/datasources-config-container.template.ts +6 -0
  61. package/src/new/main/edit-config/shared/datasource-data-base-component.helpers.ts.ts +21 -11
  62. package/src/new/main/edit-config/shared/datasource-data-base-component.test.ts +14 -2
  63. package/src/new/main/edit-config/tabbed-datasource-container/tabbed-datasource-container.template.ts +1 -1
  64. package/src/new/store/slices/datasources-config.ts +71 -16
  65. package/src/new/store/slices/types.ts +22 -4
  66. package/src/new/types/misc.ts +9 -2
  67. package/src/new/utils/alias-generator.test.ts +44 -0
  68. package/src/new/utils/alias-generator.ts +18 -0
  69. package/src/new/utils/index.ts +1 -0
  70. package/src/new/utils/tooltip.ts +19 -0
  71. package/src/new/utils/transformers.test.ts +73 -11
  72. package/src/new/utils/transformers.ts +30 -6
  73. package/src/new/utils/validators.test.ts +35 -21
@@ -127,9 +127,22 @@ export function datasourceNameFromDisplay(input: string): DatasourceName {
127
127
  .replace(/[)]/g, '')
128
128
  .split('(')
129
129
  .map((x) => x.trim());
130
- return (datasourceInputFromDisplay(parts[1] as Display.DatasourceInputTypes) +
131
- '_' +
132
- parts[0]) as DatasourceName;
130
+ const type = datasourceInputFromDisplay(parts[1] as Display.DatasourceInputTypes);
131
+ const name = parts[0];
132
+
133
+ const match = Object.entries(selectors.datasourceConfig.getAllConfigSet()).find(
134
+ ([_, ds]) => ds.KEY === name && ds.INPUT_TYPE === type,
135
+ );
136
+
137
+ if (!match) {
138
+ /**
139
+ * This can occur when the datasource is first being initialized and the name
140
+ * hasn't yet been persisted to the store.
141
+ */
142
+ return `${type}_${name}_${name}` as DatasourceName;
143
+ }
144
+
145
+ return match[0] as DatasourceName;
133
146
  }
134
147
 
135
148
  /**
@@ -198,14 +211,11 @@ export async function fetchDatasourceSpecs(
198
211
 
199
212
  try {
200
213
  return await Promise.all(
201
- Object.values(selectors.datasourceConfig.getAllConfigSet())
202
- .filter(
203
- ({ NAME, INPUT_TYPE }) =>
204
- INPUT_TYPE === 'REQ_REP' && `${INPUT_TYPE}_${NAME}` === datasourceName,
205
- )
206
- .map(async ({ NAME }) => ({
207
- spec: await getSchema(NAME),
208
- name: datasourceNameForDisplay(NAME, 'REQ_REP'),
214
+ Object.entries(selectors.datasourceConfig.getAllConfigSet())
215
+ .filter(([key, ds]) => ds.INPUT_TYPE === 'REQ_REP' && key === datasourceName)
216
+ .map(async ([_, { KEY, NAME }]) => ({
217
+ spec: await getSchema(NAME), // Fetch schema by Resource Name
218
+ name: datasourceNameForDisplay(KEY, 'REQ_REP'), // Display Alias
209
219
  })),
210
220
  );
211
221
  } catch (e) {
@@ -42,11 +42,15 @@ export type SelectRendererParams = Parameters<SelectRenderer['init']>[typeof FIR
42
42
  // DatasourceNameFromDisplay tests
43
43
  const DatasourceNameFromDisplayTests = suite('datasourceNameFromDisplay()');
44
44
 
45
+ DatasourceNameFromDisplayTests.before.each(() => {
46
+ sinon.restore();
47
+ });
48
+
45
49
  DatasourceNameFromDisplayTests(
46
50
  'Correctly builds datasource key from Snapshot (REQ_REP) input',
47
51
  () => {
48
52
  const input = 'myService (Snapshot)';
49
- const expected = 'REQ_REP_myService';
53
+ const expected = 'REQ_REP_myService_myService';
50
54
  assert.equal(datasourceNameFromDisplay(input), expected);
51
55
  },
52
56
  );
@@ -55,7 +59,7 @@ DatasourceNameFromDisplayTests(
55
59
  'Correctly builds datasource key from Data Pipeline (DATA_PIPELINE) input',
56
60
  () => {
57
61
  const input = 'dataFlow (Data Pipeline)';
58
- const expected = 'DATA_PIPELINE_dataFlow';
62
+ const expected = 'DATA_PIPELINE_dataFlow_dataFlow';
59
63
  assert.equal(datasourceNameFromDisplay(input), expected);
60
64
  },
61
65
  );
@@ -101,6 +105,8 @@ LookupColumnIsIncluded('returns true when column is in included', () => {
101
105
  NAME: 'Test Datasource',
102
106
  INPUT_TYPE: 'REQ_REP' as const,
103
107
  OUTPUT_TYPE: 'TABLE' as const,
108
+ GROUPING_STRATEGY: 'NONE' as const,
109
+ GROUP_KEY: null,
104
110
  TRANSFORMER_CONFIGURATION: { INCLUDE_COLUMNS: ['testColumn', 'otherColumn'] },
105
111
  };
106
112
 
@@ -122,6 +128,8 @@ LookupColumnIsIncluded('returns false when column is not in filters', () => {
122
128
  NAME: 'Test Datasource',
123
129
  INPUT_TYPE: 'REQ_REP' as const,
124
130
  OUTPUT_TYPE: 'TABLE' as const,
131
+ GROUPING_STRATEGY: 'NONE' as const,
132
+ GROUP_KEY: null,
125
133
  TRANSFORMER_CONFIGURATION: { INCLUDE_COLUMNS: ['testColumn', 'otherColumn'] },
126
134
  };
127
135
 
@@ -143,6 +151,8 @@ LookupColumnIsIncluded('returns false when INCLUDE_COLUMNS is undefined', () =>
143
151
  NAME: 'Test Datasource',
144
152
  INPUT_TYPE: 'REQ_REP' as const,
145
153
  OUTPUT_TYPE: 'TABLE' as const,
154
+ GROUPING_STRATEGY: 'NONE' as const,
155
+ GROUP_KEY: null,
146
156
  TRANSFORMER_CONFIGURATION: { INCLUDE_COLUMNS: undefined },
147
157
  };
148
158
 
@@ -164,6 +174,8 @@ LookupColumnIsIncluded('returns false when INCLUDE_COLUMNS is empty array', () =
164
174
  NAME: 'Test Datasource',
165
175
  INPUT_TYPE: 'REQ_REP' as const,
166
176
  OUTPUT_TYPE: 'TABLE' as const,
177
+ GROUPING_STRATEGY: 'NONE' as const,
178
+ GROUP_KEY: null,
167
179
  TRANSFORMER_CONFIGURATION: { INCLUDE_COLUMNS: [] },
168
180
  };
169
181
 
@@ -11,7 +11,7 @@ export const template = html<TabbedDatasourceContainer>`
11
11
  (_) => Object.entries(selectors.datasourceConfig.getAllConfigSet()),
12
12
  html<[DatasourceName, DatasourceConfig[DatasourceName]], TabbedDatasourceContainer>`
13
13
  <rapid-tab @click=${(x, c) => c.parent.handleDatasourceChanged(x[0])}>
14
- ${(x) => x[1].NAME}
14
+ ${(x) => x[1].KEY}
15
15
  </rapid-tab>
16
16
  `,
17
17
  )}
@@ -6,6 +6,7 @@ import { buildDatasourceName } from '../../utils';
6
6
  import { getDefaultFormat } from '../../utils/format-utils';
7
7
  import {
8
8
  DatasourceConfig,
9
+ DatasourceGrouping,
9
10
  DatasourceName,
10
11
  DatasourceOutputTypes,
11
12
  deselectedColumns,
@@ -21,18 +22,20 @@ export const datasourceSlice = createSlice({
21
22
  initDatasourceConfiguration(
22
23
  state: DatasourceConfig,
23
24
  action: PayloadAction<{
24
- base: Pick<DatasourceConfig[DatasourceName], 'KEY' | 'INPUT_TYPE'>;
25
+ base: Pick<DatasourceConfig[DatasourceName], 'KEY' | 'NAME' | 'INPUT_TYPE'>;
25
26
  fields: string[];
26
27
  }>,
27
28
  ) {
28
- const { KEY, INPUT_TYPE } = action.payload.base;
29
- const key: DatasourceName = buildDatasourceName(KEY, INPUT_TYPE);
29
+ const { KEY, NAME, INPUT_TYPE } = action.payload.base;
30
+ const key: DatasourceName = buildDatasourceName(KEY, NAME, INPUT_TYPE);
30
31
  state[key] = {
31
32
  KEY: KEY,
32
33
  INPUT_TYPE: INPUT_TYPE,
33
- NAME: KEY,
34
+ NAME: NAME,
34
35
  OUTPUT_TYPE: 'TABLE',
35
36
  TRANSFORMER_CONFIGURATION: {},
37
+ GROUPING_STRATEGY: 'NONE',
38
+ GROUP_KEY: null,
36
39
  };
37
40
  action.payload.fields.forEach((field) => {
38
41
  datasourceSlice.caseReducers.setColumnRename(state, {
@@ -54,11 +57,43 @@ export const datasourceSlice = createSlice({
54
57
  });
55
58
  });
56
59
  },
57
- updateDatasourceKey(
60
+ reorderDatasource(
58
61
  state: DatasourceConfig,
59
- action: PayloadAction<{ key: DatasourceName; newKey: string }>,
62
+ action: PayloadAction<{ key: DatasourceName; newIndex: number }>,
60
63
  ) {
61
- state[action.payload.key].KEY = action.payload.newKey;
64
+ const { key, newIndex } = action.payload;
65
+ const entries = Object.entries(state);
66
+ const currentIndex = entries.findIndex(([k]) => k === key);
67
+
68
+ if (currentIndex === -1) return state;
69
+
70
+ const [item] = entries.splice(currentIndex, 1);
71
+ entries.splice(newIndex, 0, item);
72
+
73
+ return Object.fromEntries(entries) as DatasourceConfig;
74
+ },
75
+ renameDatasource(
76
+ state: DatasourceConfig,
77
+ action: PayloadAction<{ key: DatasourceName; newName: string }>,
78
+ ) {
79
+ const { key: oldKey, newName } = action.payload;
80
+ const datasource = state[oldKey];
81
+ if (!datasource) {
82
+ return;
83
+ }
84
+ const newKey = buildDatasourceName(newName, datasource.NAME, datasource.INPUT_TYPE);
85
+ if (state[newKey] && newKey !== oldKey) {
86
+ return;
87
+ }
88
+
89
+ return Object.fromEntries(
90
+ Object.entries(state).map(([k, v]) => {
91
+ if (k === oldKey) {
92
+ return [newKey, { ...v, KEY: newName }];
93
+ }
94
+ return [k, v];
95
+ }),
96
+ ) as DatasourceConfig;
62
97
  },
63
98
  updateDatasourceOutputType(
64
99
  state: DatasourceConfig,
@@ -100,22 +135,26 @@ export const datasourceSlice = createSlice({
100
135
  column: string;
101
136
  included: boolean;
102
137
  type?: Genesis.GenesisFieldTypes;
138
+ setDefaultFormat?: boolean;
103
139
  }>,
104
140
  ) {
105
- const { key, column, included, type } = action.payload;
141
+ const { key, column, included, type, setDefaultFormat = true } = action.payload;
106
142
  if (included) {
107
143
  state[key].TRANSFORMER_CONFIGURATION.INCLUDE_COLUMNS = [
108
144
  ...(state[key].TRANSFORMER_CONFIGURATION.INCLUDE_COLUMNS ?? []),
109
145
  column,
110
146
  ];
111
- datasourceSlice.caseReducers.setColumnFormat(state, {
112
- ...action,
113
- payload: {
114
- key,
115
- column,
116
- format: getDefaultFormat(type),
117
- },
118
- });
147
+ const defaultFormat = getDefaultFormat(type);
148
+ if (defaultFormat && setDefaultFormat) {
149
+ datasourceSlice.caseReducers.setColumnFormat(state, {
150
+ ...action,
151
+ payload: {
152
+ key,
153
+ column,
154
+ format: defaultFormat,
155
+ },
156
+ });
157
+ }
119
158
  } else {
120
159
  state[key].TRANSFORMER_CONFIGURATION.INCLUDE_COLUMNS = (
121
160
  state[key].TRANSFORMER_CONFIGURATION.INCLUDE_COLUMNS ?? []
@@ -220,6 +259,22 @@ export const datasourceSlice = createSlice({
220
259
  const { [column]: _, ...rest } = state[key].TRANSFORMER_CONFIGURATION?.COLUMN_FORMATS ?? {};
221
260
  state[key].TRANSFORMER_CONFIGURATION.COLUMN_FORMATS = rest;
222
261
  },
262
+ setGrouping(
263
+ state: DatasourceConfig,
264
+ action: PayloadAction<{
265
+ key: DatasourceName;
266
+ grouping: Exclude<DatasourceGrouping, { GROUPING_STRATEGY: 'NONE' }>;
267
+ }>,
268
+ ) {
269
+ const { key, grouping } = action.payload;
270
+ state[key].GROUPING_STRATEGY = grouping.GROUPING_STRATEGY;
271
+ state[key].GROUP_KEY = grouping.GROUP_KEY;
272
+ },
273
+ deleteGrouping(state: DatasourceConfig, action: PayloadAction<{ key: DatasourceName }>) {
274
+ const { key } = action.payload;
275
+ state[key].GROUPING_STRATEGY = 'NONE';
276
+ state[key].GROUP_KEY = null;
277
+ },
223
278
  },
224
279
  selectors: {
225
280
  getAllConfigSet(state: DatasourceConfig) {
@@ -16,9 +16,27 @@ export const datasourceOutputs = [
16
16
  ] as const;
17
17
  export type DatasourceOutputTypes = (typeof datasourceOutputs)[number];
18
18
 
19
- // Key to uniquely identify each data source, must be keyed on the input type as
20
- // the names are not unique between the different data sources
21
- export type DatasourceName = `${DatasourceInputTypes}_${string}`;
19
+ export const datasourceGroupingStrategies = ['NONE', 'MULTI_SHEET', 'LOOKUP_TABLE'] as const;
20
+ export type DatasourceGroupingStrategy = (typeof datasourceGroupingStrategies)[number];
21
+ export type DatasourceGrouping =
22
+ | {
23
+ GROUPING_STRATEGY: Extract<DatasourceGroupingStrategy, 'NONE'>;
24
+ GROUP_KEY: null;
25
+ }
26
+ | {
27
+ GROUPING_STRATEGY: Extract<DatasourceGroupingStrategy, 'MULTI_SHEET' | 'LOOKUP_TABLE'>;
28
+ GROUP_KEY: string;
29
+ };
30
+
31
+ /**
32
+ * Server KEY => This is an alias (this needs to be unique). This is presented as "Name" input on the UI, rapid-text-field
33
+ * Server NAME => The name of the datasource (matches req_rep). This is presented as rapid-select
34
+ * DatasoureName to uniquely identify each data source, must be keyed on the input type as
35
+ * the names are not unique between the different data sources
36
+ */
37
+ export type AliasKey = string;
38
+ export type ServerDatasourceName = string;
39
+ export type DatasourceName = `${DatasourceInputTypes}_${ServerDatasourceName}_${AliasKey}`;
22
40
 
23
41
  export type TransformerConfig = {
24
42
  COLUMN_RENAMES?: { [k: string]: string };
@@ -44,7 +62,7 @@ export type DatasourceConfig = {
44
62
  INPUT_TYPE: DatasourceInputTypes;
45
63
  OUTPUT_TYPE: DatasourceOutputTypes;
46
64
  TRANSFORMER_CONFIGURATION: TransformerConfig;
47
- };
65
+ } & DatasourceGrouping;
48
66
  };
49
67
 
50
68
  // BASE-CONFIG
@@ -1,5 +1,11 @@
1
1
  import type { JSONSchema7 as JSONShemaBase } from 'json-schema';
2
- import type { BaseConfig, DatasourceConfig, DatasourceName, Schedule } from '../store';
2
+ import type {
3
+ BaseConfig,
4
+ DatasourceConfig,
5
+ DatasourceName,
6
+ Schedule,
7
+ DatasourceGrouping,
8
+ } from '../store';
3
9
 
4
10
  export namespace Genesis {
5
11
  export const genesisFieldTypes = [
@@ -28,7 +34,8 @@ export namespace Genesis {
28
34
  };
29
35
 
30
36
  export type ServerReportConfig = Omit<BaseConfig, 'SCHEDULE'> & {
31
- DATA_SOURCES: DatasourceConfig[DatasourceName][];
37
+ DATA_SOURCES: (Omit<DatasourceConfig[DatasourceName], 'GROUPING_STRATEGY' | 'GROUP_KEY'> &
38
+ (Exclude<DatasourceGrouping, { GROUPING_STRATEGY: 'NONE' }> | {}))[];
32
39
  SCHEDULES: Schedule[];
33
40
  };
34
41
 
@@ -0,0 +1,44 @@
1
+ import { assert, suite } from '@genesislcap/foundation-testing';
2
+ import { generateUniqueAlias } from './alias-generator';
3
+
4
+ const GenerateUniqueAlias = suite('generateUniqueAlias');
5
+
6
+ GenerateUniqueAlias('returns the base alias if it does not exist', () => {
7
+ const existingAliases = new Set(['other']);
8
+ const alias = generateUniqueAlias('test', existingAliases);
9
+ assert.equal(alias, 'test');
10
+ });
11
+
12
+ GenerateUniqueAlias('increments the alias if it already exists', () => {
13
+ const existingAliases = new Set(['test']);
14
+ const alias = generateUniqueAlias('test', existingAliases);
15
+ assert.equal(alias, 'test_1');
16
+ });
17
+
18
+ GenerateUniqueAlias('increments the alias until it is unique', () => {
19
+ const existingAliases = new Set(['test', 'test_1']);
20
+ const alias = generateUniqueAlias('test', existingAliases);
21
+ assert.equal(alias, 'test_2');
22
+ });
23
+
24
+ GenerateUniqueAlias('handles case-insensitivity when checking for existing aliases', () => {
25
+ const existingAliases = new Set(['Test']);
26
+ const alias = generateUniqueAlias('test', existingAliases);
27
+ assert.equal(alias, 'test_1');
28
+ });
29
+
30
+ GenerateUniqueAlias('handles case-insensitivity for new alias generation', () => {
31
+ const existingAliases = new Set(['test']);
32
+ const alias = generateUniqueAlias('Test', existingAliases);
33
+ // Since input baseAlias is 'Test', it tries 'Test'. 'Test' matches 'test' case-insensitively.
34
+ // Next attempt 'Test_1'.
35
+ assert.equal(alias, 'Test_1');
36
+ });
37
+
38
+ GenerateUniqueAlias('handles mixed case existing aliases', () => {
39
+ const existingAliases = new Set(['TeSt', 'tEsT_1']);
40
+ const alias = generateUniqueAlias('test', existingAliases);
41
+ assert.equal(alias, 'test_2');
42
+ });
43
+
44
+ GenerateUniqueAlias.run();
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Generates a unique alias by appending a counter if the base alias already exists in the set.
3
+ * @param baseAlias The desired alias name (usually the resource name).
4
+ * @param existingAliases A Set of currently used aliases to check against.
5
+ * @returns A unique alias string.
6
+ */
7
+ export function generateUniqueAlias(baseAlias: string, existingAliases: Set<string>): string {
8
+ const existingAliasesLower = new Set(
9
+ Array.from(existingAliases).map((alias) => alias.toLowerCase()),
10
+ );
11
+ let uniqueAlias = baseAlias;
12
+ let counter = 1;
13
+ while (existingAliasesLower.has(uniqueAlias.toLowerCase())) {
14
+ uniqueAlias = `${baseAlias}_${counter}`;
15
+ counter += 1;
16
+ }
17
+ return uniqueAlias;
18
+ }
@@ -1,3 +1,4 @@
1
+ export * from './alias-generator';
1
2
  export * from './endpoint';
2
3
  export * from './formatters';
3
4
  export * from './notifications';
@@ -52,6 +52,11 @@ export namespace TOOLTIP {
52
52
  </ul>
53
53
  `;
54
54
 
55
+ export const DATASOURCES_DATASOURCE = `
56
+ <h3>Datasource</h3>
57
+ <p>This is the name of the server resource that data will be pulled from to generate the report.</p>
58
+ `;
59
+
55
60
  export const DATASOURCES_NAME = `
56
61
  <h3>Datasource name</h3>
57
62
  <p>Allows you to set a name for the datasource which must correspond with your template data references where the report is configured to use a template file.</p>
@@ -63,6 +68,20 @@ export namespace TOOLTIP {
63
68
  <p>Configures the method by which the server constructs the report configuration. Currently only TABLE is supported, which means the entire datasource is loaded into memory when generating the report using this report configuration.</p>
64
69
  `;
65
70
 
71
+ export const DATASOURCES_GROUPING_STRATEGY = `
72
+ <h3>Grouping Strategy</h3>
73
+ <p>Select "None" to disable grouping strategies, or select the strategy you'd like to use.</p>
74
+ <ul>
75
+ <li><strong>Multi-sheet</strong>: Group data for <code>JXLS</code> template Multi-sheet functionality, to split different groups across multiple excel worksheets.</li>
76
+ <li><strong>Lookup-table</strong>: Create a lookup table of key value pairs in the template data, where the group key field is the map key.</li>
77
+ </ul>
78
+ `;
79
+
80
+ export const DATASOURCES_GROUPING_KEY = `
81
+ <h3>Grouping Key</h3>
82
+ <p>Select the field for this datasource you want to operate the grouping strategy on.</p>
83
+ `;
84
+
66
85
  export const DELIVERY_TIMEZONE = `
67
86
  <h3>Timezone</h3>
68
87
  <p>Select the timezone to be used for this schedule.</p>
@@ -1,6 +1,10 @@
1
1
  import { assert, sinon, suite } from '@genesislcap/foundation-testing';
2
2
  import type { Genesis } from '../types';
3
- import { buildDatasourceName, transformFromServerPayload } from './transformers';
3
+ import {
4
+ buildDatasourceName,
5
+ transformFromServerPayload,
6
+ transformToServerPayload,
7
+ } from './transformers';
4
8
 
5
9
  const TransformFromServerPayload = suite('transformFromServerPayload');
6
10
 
@@ -39,7 +43,7 @@ TransformFromServerPayload('cleans up orphaned fields from INCLUDE_COLUMNS', asy
39
43
 
40
44
  const result = await transformFromServerPayload(payload, mockGetSchema);
41
45
 
42
- const datasourceKey = buildDatasourceName('COUNTERPARTY', 'REQ_REP');
46
+ const datasourceKey = buildDatasourceName('COUNTERPARTY', 'COUNTERPARTY', 'REQ_REP');
43
47
  const cleanedConfig = result.datasourceConfig[datasourceKey].TRANSFORMER_CONFIGURATION;
44
48
 
45
49
  assert.equal(cleanedConfig.INCLUDE_COLUMNS?.length, 2);
@@ -83,7 +87,7 @@ TransformFromServerPayload('cleans up orphaned fields from COLUMN_RENAMES', asyn
83
87
 
84
88
  const result = await transformFromServerPayload(payload, mockGetSchema);
85
89
 
86
- const datasourceKey = buildDatasourceName('COUNTERPARTY', 'REQ_REP');
90
+ const datasourceKey = buildDatasourceName('COUNTERPARTY', 'COUNTERPARTY', 'REQ_REP');
87
91
  const cleanedConfig = result.datasourceConfig[datasourceKey].TRANSFORMER_CONFIGURATION;
88
92
 
89
93
  assert.equal(Object.keys(cleanedConfig.COLUMN_RENAMES || {}).length, 2);
@@ -127,7 +131,7 @@ TransformFromServerPayload('cleans up orphaned fields from COLUMN_FORMATS', asyn
127
131
 
128
132
  const result = await transformFromServerPayload(payload, mockGetSchema);
129
133
 
130
- const datasourceKey = buildDatasourceName('COUNTERPARTY', 'REQ_REP');
134
+ const datasourceKey = buildDatasourceName('COUNTERPARTY', 'COUNTERPARTY', 'REQ_REP');
131
135
  const cleanedConfig = result.datasourceConfig[datasourceKey].TRANSFORMER_CONFIGURATION;
132
136
 
133
137
  assert.equal(Object.keys(cleanedConfig.COLUMN_FORMATS || {}).length, 2);
@@ -171,7 +175,7 @@ TransformFromServerPayload('cleans up orphaned fields from COLUMN_TRANSFORMS', a
171
175
 
172
176
  const result = await transformFromServerPayload(payload, mockGetSchema);
173
177
 
174
- const datasourceKey = buildDatasourceName('COUNTERPARTY', 'REQ_REP');
178
+ const datasourceKey = buildDatasourceName('COUNTERPARTY', 'COUNTERPARTY', 'REQ_REP');
175
179
  const cleanedConfig = result.datasourceConfig[datasourceKey].TRANSFORMER_CONFIGURATION;
176
180
 
177
181
  assert.equal(Object.keys(cleanedConfig.COLUMN_TRANSFORMS || {}).length, 2);
@@ -209,7 +213,7 @@ TransformFromServerPayload('handles schema fetch failure gracefully', async () =
209
213
 
210
214
  const result = await transformFromServerPayload(payload, mockGetSchema);
211
215
 
212
- const datasourceKey = buildDatasourceName('COUNTERPARTY', 'REQ_REP');
216
+ const datasourceKey = buildDatasourceName('COUNTERPARTY', 'COUNTERPARTY', 'REQ_REP');
213
217
  const cleanedConfig = result.datasourceConfig[datasourceKey].TRANSFORMER_CONFIGURATION;
214
218
 
215
219
  // Should keep original config when schema fetch fails
@@ -266,8 +270,8 @@ TransformFromServerPayload('handles multiple datasources independently', async (
266
270
 
267
271
  const result = await transformFromServerPayload(payload, mockGetSchema);
268
272
 
269
- const counterpartyKey = buildDatasourceName('COUNTERPARTY', 'REQ_REP');
270
- const tradeKey = buildDatasourceName('TRADE', 'REQ_REP');
273
+ const counterpartyKey = buildDatasourceName('COUNTERPARTY', 'COUNTERPARTY', 'REQ_REP');
274
+ const tradeKey = buildDatasourceName('TRADE', 'TRADE', 'REQ_REP');
271
275
 
272
276
  assert.equal(
273
277
  result.datasourceConfig[counterpartyKey].TRANSFORMER_CONFIGURATION.INCLUDE_COLUMNS?.length,
@@ -318,7 +322,7 @@ TransformFromServerPayload('handles empty transformer configuration', async () =
318
322
 
319
323
  const result = await transformFromServerPayload(payload, mockGetSchema);
320
324
 
321
- const datasourceKey = buildDatasourceName('COUNTERPARTY', 'REQ_REP');
325
+ const datasourceKey = buildDatasourceName('COUNTERPARTY', 'COUNTERPARTY', 'REQ_REP');
322
326
  const cleanedConfig = result.datasourceConfig[datasourceKey].TRANSFORMER_CONFIGURATION;
323
327
 
324
328
  assert.equal(JSON.stringify(cleanedConfig), JSON.stringify({}));
@@ -350,7 +354,7 @@ TransformFromServerPayload('handles missing schema properties', async () => {
350
354
 
351
355
  const result = await transformFromServerPayload(payload, mockGetSchema);
352
356
 
353
- const datasourceKey = buildDatasourceName('COUNTERPARTY', 'REQ_REP');
357
+ const datasourceKey = buildDatasourceName('COUNTERPARTY', 'COUNTERPARTY', 'REQ_REP');
354
358
  const cleanedConfig = result.datasourceConfig[datasourceKey].TRANSFORMER_CONFIGURATION;
355
359
 
356
360
  // All fields should be removed when schema has no properties
@@ -460,7 +464,7 @@ TransformFromServerPayload('handles all transformer config types together', asyn
460
464
 
461
465
  const result = await transformFromServerPayload(payload, mockGetSchema);
462
466
 
463
- const datasourceKey = buildDatasourceName('COUNTERPARTY', 'REQ_REP');
467
+ const datasourceKey = buildDatasourceName('COUNTERPARTY', 'COUNTERPARTY', 'REQ_REP');
464
468
  const cleanedConfig = result.datasourceConfig[datasourceKey].TRANSFORMER_CONFIGURATION;
465
469
 
466
470
  // INCLUDE_COLUMNS should only have valid fields
@@ -485,3 +489,61 @@ TransformFromServerPayload('handles all transformer config types together', asyn
485
489
  });
486
490
 
487
491
  TransformFromServerPayload.run();
492
+
493
+ const TransformToServerPayload = suite('transformToServerPayload');
494
+
495
+ TransformToServerPayload('omits grouping fields when strategy is NONE', () => {
496
+ const state: any = {
497
+ baseConfig: {
498
+ DESTINATION_IDS: [],
499
+ OUTPUT_DIRECTORY: '/reports',
500
+ },
501
+ datasourceConfig: {
502
+ REQ_REP_COUNTERPARTY_COUNTERPARTY: {
503
+ KEY: 'COUNTERPARTY',
504
+ INPUT_TYPE: 'REQ_REP',
505
+ NAME: 'COUNTERPARTY',
506
+ OUTPUT_TYPE: 'TABLE',
507
+ TRANSFORMER_CONFIGURATION: {},
508
+ GROUPING_STRATEGY: 'NONE',
509
+ GROUP_KEY: null,
510
+ },
511
+ },
512
+ };
513
+
514
+ const result = transformToServerPayload(state);
515
+ const ds = result.DATA_SOURCES[0];
516
+
517
+ assert.equal(ds.KEY, 'COUNTERPARTY');
518
+ assert.ok(!('GROUPING_STRATEGY' in ds));
519
+ assert.ok(!('GROUP_KEY' in ds));
520
+ });
521
+
522
+ TransformToServerPayload('includes grouping fields when strategy is not NONE', () => {
523
+ const state: any = {
524
+ baseConfig: {
525
+ DESTINATION_IDS: [],
526
+ OUTPUT_DIRECTORY: '/reports',
527
+ },
528
+ datasourceConfig: {
529
+ REQ_REP_COUNTERPARTY_COUNTERPARTY: {
530
+ KEY: 'COUNTERPARTY',
531
+ INPUT_TYPE: 'REQ_REP',
532
+ NAME: 'COUNTERPARTY',
533
+ OUTPUT_TYPE: 'TABLE',
534
+ TRANSFORMER_CONFIGURATION: {},
535
+ GROUPING_STRATEGY: 'MULTI_SHEET',
536
+ GROUP_KEY: 'some_key',
537
+ },
538
+ },
539
+ };
540
+
541
+ const result = transformToServerPayload(state);
542
+ const ds = result.DATA_SOURCES[0] as any;
543
+
544
+ assert.equal(ds.KEY, 'COUNTERPARTY');
545
+ assert.equal(ds.GROUPING_STRATEGY, 'MULTI_SHEET');
546
+ assert.equal(ds.GROUP_KEY, 'some_key');
547
+ });
548
+
549
+ TransformToServerPayload.run();
@@ -18,7 +18,14 @@ export function transformToServerPayload(state: ReportingConfig): Genesis.Server
18
18
  DOCUMENT_TEMPLATE_ID: restBaseConfig.DOCUMENT_TEMPLATE_ID
19
19
  ? restBaseConfig.DOCUMENT_TEMPLATE_ID
20
20
  : null,
21
- DATA_SOURCES: Object.values(datasourceConfig),
21
+ DATA_SOURCES: Object.values(datasourceConfig).map((ds) => {
22
+ // For NONE case we just don't send it to the server to be backwards compatible
23
+ if (ds.GROUPING_STRATEGY === 'NONE') {
24
+ const { GROUPING_STRATEGY, GROUP_KEY, ...rest } = ds;
25
+ return rest;
26
+ }
27
+ return ds;
28
+ }),
22
29
  };
23
30
  }
24
31
 
@@ -85,10 +92,18 @@ export async function transformFromServerPayload(
85
92
 
86
93
  const cleanedDatasources = await Promise.all(
87
94
  DATA_SOURCES.map(
88
- async ({ KEY, INPUT_TYPE, NAME: DS_NAME, OUTPUT_TYPE, TRANSFORMER_CONFIGURATION }) => {
95
+ async ({
96
+ KEY,
97
+ INPUT_TYPE,
98
+ NAME: DS_NAME,
99
+ OUTPUT_TYPE,
100
+ TRANSFORMER_CONFIGURATION,
101
+ ...rest
102
+ }) => {
89
103
  try {
90
- const schema = await getSchema(KEY);
104
+ const schema = await getSchema(DS_NAME);
91
105
  const validFields = new Set(Object.keys(schema.properties || {}));
106
+ const { GROUPING_STRATEGY, GROUP_KEY } = rest as any;
92
107
 
93
108
  const cleanedConfig = cleanupTransformerConfig(
94
109
  TRANSFORMER_CONFIGURATION || {},
@@ -101,16 +116,21 @@ export async function transformFromServerPayload(
101
116
  NAME: DS_NAME,
102
117
  OUTPUT_TYPE,
103
118
  TRANSFORMER_CONFIGURATION: cleanedConfig,
119
+ GROUPING_STRATEGY: GROUPING_STRATEGY || 'NONE',
120
+ GROUP_KEY: GROUP_KEY || null,
104
121
  };
105
122
  } catch (error) {
106
123
  // If schema fetch fails, log warning but keep original config
107
124
  console.warn(`Failed to fetch schema for ${KEY}, skipping cleanup:`, error);
125
+ const { GROUPING_STRATEGY, GROUP_KEY } = rest as any;
108
126
  return {
109
127
  KEY,
110
128
  INPUT_TYPE,
111
129
  NAME: DS_NAME,
112
130
  OUTPUT_TYPE,
113
131
  TRANSFORMER_CONFIGURATION: TRANSFORMER_CONFIGURATION || {},
132
+ GROUPING_STRATEGY: GROUPING_STRATEGY || 'NONE',
133
+ GROUP_KEY: GROUP_KEY || null,
114
134
  };
115
135
  }
116
136
  },
@@ -122,7 +142,7 @@ export async function transformFromServerPayload(
122
142
  datasourceConfig: cleanedDatasources.reduce(
123
143
  (acum, ds) => ({
124
144
  ...acum,
125
- [buildDatasourceName(ds.NAME, ds.INPUT_TYPE)]: ds,
145
+ [buildDatasourceName(ds.KEY, ds.NAME, ds.INPUT_TYPE)]: ds,
126
146
  }),
127
147
  {},
128
148
  ),
@@ -145,6 +165,10 @@ export function datasourceInputFromDisplay(
145
165
  else throw new Error(`datasourceInputFromDisplay - Uncaught case ${display}`);
146
166
  }
147
167
 
148
- export function buildDatasourceName(key: string, type: DatasourceInputTypes): DatasourceName {
149
- return `${type}_${key}`;
168
+ export function buildDatasourceName(
169
+ alias: string,
170
+ resourceName: string,
171
+ type: DatasourceInputTypes,
172
+ ): DatasourceName {
173
+ return `${type}_${resourceName}_${alias}`;
150
174
  }