@genesislcap/pbc-reporting-ui 14.353.3 → 14.353.4

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.
@@ -0,0 +1,487 @@
1
+ import { assert, sinon, suite } from '@genesislcap/foundation-testing';
2
+ import type { Genesis } from '../types';
3
+ import { buildDatasourceName, transformFromServerPayload } from './transformers';
4
+
5
+ const TransformFromServerPayload = suite('transformFromServerPayload');
6
+
7
+ TransformFromServerPayload.before.each(() => {
8
+ sinon.restore();
9
+ });
10
+
11
+ TransformFromServerPayload('cleans up orphaned fields from INCLUDE_COLUMNS', async () => {
12
+ const mockGetSchema = sinon.stub();
13
+ const validSchema: Genesis.JSONSchema7 = {
14
+ properties: {
15
+ COUNTERPARTY_NAME: { genesisType: 'STRING' },
16
+ AMOUNT: { genesisType: 'DOUBLE' },
17
+ },
18
+ };
19
+ mockGetSchema.resolves(validSchema);
20
+
21
+ const payload: Genesis.ServerReportConfig = {
22
+ ID: 'test-id',
23
+ NAME: 'Test Report',
24
+ SCHEDULES: [],
25
+ DATA_SOURCES: [
26
+ {
27
+ KEY: 'COUNTERPARTY',
28
+ INPUT_TYPE: 'REQ_REP',
29
+ NAME: 'COUNTERPARTY',
30
+ OUTPUT_TYPE: 'TABLE',
31
+ TRANSFORMER_CONFIGURATION: {
32
+ INCLUDE_COLUMNS: ['COUNTERPARTY_NAME', 'AMOUNT', 'NAME'], // NAME is orphaned
33
+ },
34
+ },
35
+ ],
36
+ DESTINATION_IDS: [],
37
+ OUTPUT_DIRECTORY: '/reports',
38
+ };
39
+
40
+ const result = await transformFromServerPayload(payload, mockGetSchema);
41
+
42
+ const datasourceKey = buildDatasourceName('COUNTERPARTY', 'REQ_REP');
43
+ const cleanedConfig = result.datasourceConfig[datasourceKey].TRANSFORMER_CONFIGURATION;
44
+
45
+ assert.equal(cleanedConfig.INCLUDE_COLUMNS?.length, 2);
46
+ assert.ok(cleanedConfig.INCLUDE_COLUMNS?.includes('COUNTERPARTY_NAME'));
47
+ assert.ok(cleanedConfig.INCLUDE_COLUMNS?.includes('AMOUNT'));
48
+ assert.ok(!cleanedConfig.INCLUDE_COLUMNS?.includes('NAME'));
49
+ });
50
+
51
+ TransformFromServerPayload('cleans up orphaned fields from COLUMN_RENAMES', async () => {
52
+ const mockGetSchema = sinon.stub();
53
+ const validSchema: Genesis.JSONSchema7 = {
54
+ properties: {
55
+ COUNTERPARTY_NAME: { genesisType: 'STRING' },
56
+ AMOUNT: { genesisType: 'DOUBLE' },
57
+ },
58
+ };
59
+ mockGetSchema.resolves(validSchema);
60
+
61
+ const payload: Genesis.ServerReportConfig = {
62
+ ID: 'test-id',
63
+ NAME: 'Test Report',
64
+ SCHEDULES: [],
65
+ DATA_SOURCES: [
66
+ {
67
+ KEY: 'COUNTERPARTY',
68
+ INPUT_TYPE: 'REQ_REP',
69
+ NAME: 'COUNTERPARTY',
70
+ OUTPUT_TYPE: 'TABLE',
71
+ TRANSFORMER_CONFIGURATION: {
72
+ COLUMN_RENAMES: {
73
+ COUNTERPARTY_NAME: 'Counterparty Name',
74
+ AMOUNT: 'Amount',
75
+ NAME: 'Old Name', // orphaned
76
+ },
77
+ },
78
+ },
79
+ ],
80
+ DESTINATION_IDS: [],
81
+ OUTPUT_DIRECTORY: '/reports',
82
+ };
83
+
84
+ const result = await transformFromServerPayload(payload, mockGetSchema);
85
+
86
+ const datasourceKey = buildDatasourceName('COUNTERPARTY', 'REQ_REP');
87
+ const cleanedConfig = result.datasourceConfig[datasourceKey].TRANSFORMER_CONFIGURATION;
88
+
89
+ assert.equal(Object.keys(cleanedConfig.COLUMN_RENAMES || {}).length, 2);
90
+ assert.equal(cleanedConfig.COLUMN_RENAMES?.['COUNTERPARTY_NAME'], 'Counterparty Name');
91
+ assert.equal(cleanedConfig.COLUMN_RENAMES?.['AMOUNT'], 'Amount');
92
+ assert.ok(!cleanedConfig.COLUMN_RENAMES?.['NAME']);
93
+ });
94
+
95
+ TransformFromServerPayload('cleans up orphaned fields from COLUMN_FORMATS', async () => {
96
+ const mockGetSchema = sinon.stub();
97
+ const validSchema: Genesis.JSONSchema7 = {
98
+ properties: {
99
+ COUNTERPARTY_NAME: { genesisType: 'STRING' },
100
+ AMOUNT: { genesisType: 'DOUBLE' },
101
+ },
102
+ };
103
+ mockGetSchema.resolves(validSchema);
104
+
105
+ const payload: Genesis.ServerReportConfig = {
106
+ ID: 'test-id',
107
+ NAME: 'Test Report',
108
+ SCHEDULES: [],
109
+ DATA_SOURCES: [
110
+ {
111
+ KEY: 'COUNTERPARTY',
112
+ INPUT_TYPE: 'REQ_REP',
113
+ NAME: 'COUNTERPARTY',
114
+ OUTPUT_TYPE: 'TABLE',
115
+ TRANSFORMER_CONFIGURATION: {
116
+ COLUMN_FORMATS: {
117
+ COUNTERPARTY_NAME: 'text',
118
+ AMOUNT: '#,##0.00',
119
+ NAME: 'text', // orphaned
120
+ },
121
+ },
122
+ },
123
+ ],
124
+ DESTINATION_IDS: [],
125
+ OUTPUT_DIRECTORY: '/reports',
126
+ };
127
+
128
+ const result = await transformFromServerPayload(payload, mockGetSchema);
129
+
130
+ const datasourceKey = buildDatasourceName('COUNTERPARTY', 'REQ_REP');
131
+ const cleanedConfig = result.datasourceConfig[datasourceKey].TRANSFORMER_CONFIGURATION;
132
+
133
+ assert.equal(Object.keys(cleanedConfig.COLUMN_FORMATS || {}).length, 2);
134
+ assert.equal(cleanedConfig.COLUMN_FORMATS?.['COUNTERPARTY_NAME'], 'text');
135
+ assert.equal(cleanedConfig.COLUMN_FORMATS?.['AMOUNT'], '#,##0.00');
136
+ assert.ok(!cleanedConfig.COLUMN_FORMATS?.['NAME']);
137
+ });
138
+
139
+ TransformFromServerPayload('cleans up orphaned fields from COLUMN_TRANSFORMS', async () => {
140
+ const mockGetSchema = sinon.stub();
141
+ const validSchema: Genesis.JSONSchema7 = {
142
+ properties: {
143
+ COUNTERPARTY_NAME: { genesisType: 'STRING' },
144
+ AMOUNT: { genesisType: 'DOUBLE' },
145
+ },
146
+ };
147
+ mockGetSchema.resolves(validSchema);
148
+
149
+ const payload: Genesis.ServerReportConfig = {
150
+ ID: 'test-id',
151
+ NAME: 'Test Report',
152
+ SCHEDULES: [],
153
+ DATA_SOURCES: [
154
+ {
155
+ KEY: 'COUNTERPARTY',
156
+ INPUT_TYPE: 'REQ_REP',
157
+ NAME: 'COUNTERPARTY',
158
+ OUTPUT_TYPE: 'TABLE',
159
+ TRANSFORMER_CONFIGURATION: {
160
+ COLUMN_TRANSFORMS: {
161
+ COUNTERPARTY_NAME: { TYPE: 'BINARY_EXPRESSION', LEFT: {}, OPERATION: 'AND', RIGHT: {} },
162
+ AMOUNT: { TYPE: 'METHOD_EXPRESSION', METHOD: 'TRIM', PARAMETERS: [] },
163
+ NAME: { TYPE: 'BINARY_EXPRESSION', LEFT: {}, OPERATION: 'AND', RIGHT: {} }, // orphaned
164
+ } as any,
165
+ },
166
+ },
167
+ ],
168
+ DESTINATION_IDS: [],
169
+ OUTPUT_DIRECTORY: '/reports',
170
+ };
171
+
172
+ const result = await transformFromServerPayload(payload, mockGetSchema);
173
+
174
+ const datasourceKey = buildDatasourceName('COUNTERPARTY', 'REQ_REP');
175
+ const cleanedConfig = result.datasourceConfig[datasourceKey].TRANSFORMER_CONFIGURATION;
176
+
177
+ assert.equal(Object.keys(cleanedConfig.COLUMN_TRANSFORMS || {}).length, 2);
178
+ assert.ok(cleanedConfig.COLUMN_TRANSFORMS?.['COUNTERPARTY_NAME']);
179
+ assert.ok(cleanedConfig.COLUMN_TRANSFORMS?.['AMOUNT']);
180
+ assert.ok(!cleanedConfig.COLUMN_TRANSFORMS?.['NAME']);
181
+ });
182
+
183
+ TransformFromServerPayload('handles schema fetch failure gracefully', async () => {
184
+ const mockGetSchema = sinon.stub();
185
+ const consoleWarnStub = sinon.stub(console, 'warn');
186
+ mockGetSchema.rejects(new Error('Schema fetch failed'));
187
+
188
+ const originalConfig = {
189
+ INCLUDE_COLUMNS: ['NAME', 'AMOUNT'],
190
+ COLUMN_RENAMES: { NAME: 'Name' },
191
+ };
192
+
193
+ const payload: Genesis.ServerReportConfig = {
194
+ ID: 'test-id',
195
+ NAME: 'Test Report',
196
+ SCHEDULES: [],
197
+ DATA_SOURCES: [
198
+ {
199
+ KEY: 'COUNTERPARTY',
200
+ INPUT_TYPE: 'REQ_REP',
201
+ NAME: 'COUNTERPARTY',
202
+ OUTPUT_TYPE: 'TABLE',
203
+ TRANSFORMER_CONFIGURATION: originalConfig,
204
+ },
205
+ ],
206
+ DESTINATION_IDS: [],
207
+ OUTPUT_DIRECTORY: '/reports',
208
+ };
209
+
210
+ const result = await transformFromServerPayload(payload, mockGetSchema);
211
+
212
+ const datasourceKey = buildDatasourceName('COUNTERPARTY', 'REQ_REP');
213
+ const cleanedConfig = result.datasourceConfig[datasourceKey].TRANSFORMER_CONFIGURATION;
214
+
215
+ // Should keep original config when schema fetch fails
216
+ assert.equal(JSON.stringify(cleanedConfig), JSON.stringify(originalConfig));
217
+ assert.ok(consoleWarnStub.calledOnce);
218
+ assert.ok(consoleWarnStub.calledWithMatch(sinon.match.string, sinon.match.instanceOf(Error)));
219
+
220
+ consoleWarnStub.restore();
221
+ });
222
+
223
+ TransformFromServerPayload('handles multiple datasources independently', async () => {
224
+ const mockGetSchema = sinon.stub();
225
+ mockGetSchema
226
+ .onFirstCall()
227
+ .resolves({
228
+ properties: {
229
+ COUNTERPARTY_NAME: { genesisType: 'STRING' },
230
+ },
231
+ } as Genesis.JSONSchema7)
232
+ .onSecondCall()
233
+ .resolves({
234
+ properties: {
235
+ TRADE_ID: { genesisType: 'STRING' },
236
+ },
237
+ } as Genesis.JSONSchema7);
238
+
239
+ const payload: Genesis.ServerReportConfig = {
240
+ ID: 'test-id',
241
+ NAME: 'Test Report',
242
+ SCHEDULES: [],
243
+ DATA_SOURCES: [
244
+ {
245
+ KEY: 'COUNTERPARTY',
246
+ INPUT_TYPE: 'REQ_REP',
247
+ NAME: 'COUNTERPARTY',
248
+ OUTPUT_TYPE: 'TABLE',
249
+ TRANSFORMER_CONFIGURATION: {
250
+ INCLUDE_COLUMNS: ['COUNTERPARTY_NAME', 'NAME'], // NAME is orphaned
251
+ },
252
+ },
253
+ {
254
+ KEY: 'TRADE',
255
+ INPUT_TYPE: 'REQ_REP',
256
+ NAME: 'TRADE',
257
+ OUTPUT_TYPE: 'TABLE',
258
+ TRANSFORMER_CONFIGURATION: {
259
+ INCLUDE_COLUMNS: ['TRADE_ID', 'OLD_FIELD'], // OLD_FIELD is orphaned
260
+ },
261
+ },
262
+ ],
263
+ DESTINATION_IDS: [],
264
+ OUTPUT_DIRECTORY: '/reports',
265
+ };
266
+
267
+ const result = await transformFromServerPayload(payload, mockGetSchema);
268
+
269
+ const counterpartyKey = buildDatasourceName('COUNTERPARTY', 'REQ_REP');
270
+ const tradeKey = buildDatasourceName('TRADE', 'REQ_REP');
271
+
272
+ assert.equal(
273
+ result.datasourceConfig[counterpartyKey].TRANSFORMER_CONFIGURATION.INCLUDE_COLUMNS?.length,
274
+ 1,
275
+ );
276
+ assert.ok(
277
+ result.datasourceConfig[counterpartyKey].TRANSFORMER_CONFIGURATION.INCLUDE_COLUMNS?.includes(
278
+ 'COUNTERPARTY_NAME',
279
+ ),
280
+ );
281
+
282
+ assert.equal(
283
+ result.datasourceConfig[tradeKey].TRANSFORMER_CONFIGURATION.INCLUDE_COLUMNS?.length,
284
+ 1,
285
+ );
286
+ assert.ok(
287
+ result.datasourceConfig[tradeKey].TRANSFORMER_CONFIGURATION.INCLUDE_COLUMNS?.includes(
288
+ 'TRADE_ID',
289
+ ),
290
+ );
291
+ });
292
+
293
+ TransformFromServerPayload('handles empty transformer configuration', async () => {
294
+ const mockGetSchema = sinon.stub();
295
+ const validSchema: Genesis.JSONSchema7 = {
296
+ properties: {
297
+ COUNTERPARTY_NAME: { genesisType: 'STRING' },
298
+ },
299
+ };
300
+ mockGetSchema.resolves(validSchema);
301
+
302
+ const payload: Genesis.ServerReportConfig = {
303
+ ID: 'test-id',
304
+ NAME: 'Test Report',
305
+ SCHEDULES: [],
306
+ DATA_SOURCES: [
307
+ {
308
+ KEY: 'COUNTERPARTY',
309
+ INPUT_TYPE: 'REQ_REP',
310
+ NAME: 'COUNTERPARTY',
311
+ OUTPUT_TYPE: 'TABLE',
312
+ TRANSFORMER_CONFIGURATION: {},
313
+ },
314
+ ],
315
+ DESTINATION_IDS: [],
316
+ OUTPUT_DIRECTORY: '/reports',
317
+ };
318
+
319
+ const result = await transformFromServerPayload(payload, mockGetSchema);
320
+
321
+ const datasourceKey = buildDatasourceName('COUNTERPARTY', 'REQ_REP');
322
+ const cleanedConfig = result.datasourceConfig[datasourceKey].TRANSFORMER_CONFIGURATION;
323
+
324
+ assert.equal(JSON.stringify(cleanedConfig), JSON.stringify({}));
325
+ });
326
+
327
+ TransformFromServerPayload('handles missing schema properties', async () => {
328
+ const mockGetSchema = sinon.stub();
329
+ const schemaWithoutProperties: Genesis.JSONSchema7 = {};
330
+ mockGetSchema.resolves(schemaWithoutProperties);
331
+
332
+ const payload: Genesis.ServerReportConfig = {
333
+ ID: 'test-id',
334
+ NAME: 'Test Report',
335
+ SCHEDULES: [],
336
+ DATA_SOURCES: [
337
+ {
338
+ KEY: 'COUNTERPARTY',
339
+ INPUT_TYPE: 'REQ_REP',
340
+ NAME: 'COUNTERPARTY',
341
+ OUTPUT_TYPE: 'TABLE',
342
+ TRANSFORMER_CONFIGURATION: {
343
+ INCLUDE_COLUMNS: ['NAME', 'AMOUNT'],
344
+ },
345
+ },
346
+ ],
347
+ DESTINATION_IDS: [],
348
+ OUTPUT_DIRECTORY: '/reports',
349
+ };
350
+
351
+ const result = await transformFromServerPayload(payload, mockGetSchema);
352
+
353
+ const datasourceKey = buildDatasourceName('COUNTERPARTY', 'REQ_REP');
354
+ const cleanedConfig = result.datasourceConfig[datasourceKey].TRANSFORMER_CONFIGURATION;
355
+
356
+ // All fields should be removed when schema has no properties
357
+ assert.equal(cleanedConfig.INCLUDE_COLUMNS?.length, 0);
358
+ });
359
+
360
+ TransformFromServerPayload('preserves base config correctly', async () => {
361
+ const mockGetSchema = sinon.stub();
362
+ const validSchema: Genesis.JSONSchema7 = {
363
+ properties: {
364
+ COUNTERPARTY_NAME: { genesisType: 'STRING' },
365
+ },
366
+ };
367
+ mockGetSchema.resolves(validSchema);
368
+
369
+ const payload: Genesis.ServerReportConfig = {
370
+ ID: 'test-id',
371
+ NAME: 'Test Report',
372
+ DESCRIPTION: 'Test Description',
373
+ OUTPUT_FORMAT: 'CSV',
374
+ FILE_NAME: 'test.csv',
375
+ AUTO_DISPATCH: true,
376
+ DESTINATION_IDS: ['dest1', 'dest2'],
377
+ OUTPUT_DIRECTORY: '/reports',
378
+ DOCUMENT_TEMPLATE_ID: 'template-123',
379
+ SCHEDULES: [
380
+ {
381
+ CRON_EXPRESSION: '0 0 0 * * ?',
382
+ TIME_ZONE: 'UTC',
383
+ },
384
+ ],
385
+ DATA_SOURCES: [
386
+ {
387
+ KEY: 'COUNTERPARTY',
388
+ INPUT_TYPE: 'REQ_REP',
389
+ NAME: 'COUNTERPARTY',
390
+ OUTPUT_TYPE: 'TABLE',
391
+ TRANSFORMER_CONFIGURATION: {},
392
+ },
393
+ ],
394
+ };
395
+
396
+ const result = await transformFromServerPayload(payload, mockGetSchema);
397
+
398
+ assert.equal(result.baseConfig.ID, 'test-id');
399
+ assert.equal(result.baseConfig.NAME, 'Test Report');
400
+ assert.equal(result.baseConfig.DESCRIPTION, 'Test Description');
401
+ assert.equal(result.baseConfig.OUTPUT_FORMAT, 'CSV');
402
+ assert.equal(result.baseConfig.FILE_NAME, 'test.csv');
403
+ assert.equal(result.baseConfig.AUTO_DISPATCH, true);
404
+ assert.equal(
405
+ JSON.stringify(result.baseConfig.DESTINATION_IDS),
406
+ JSON.stringify(['dest1', 'dest2']),
407
+ );
408
+ assert.equal(result.baseConfig.OUTPUT_DIRECTORY, '/reports');
409
+ assert.equal(result.baseConfig.DOCUMENT_TEMPLATE_ID, 'template-123');
410
+ assert.ok(result.baseConfig.SCHEDULE);
411
+ assert.equal(result.baseConfig.SCHEDULE?.CRON_EXPRESSION, '0 0 0 * * ?');
412
+ assert.equal(result.baseConfig.SCHEDULE?.TIME_ZONE, 'UTC');
413
+ });
414
+
415
+ TransformFromServerPayload('handles all transformer config types together', async () => {
416
+ const mockGetSchema = sinon.stub();
417
+ const validSchema: Genesis.JSONSchema7 = {
418
+ properties: {
419
+ COUNTERPARTY_NAME: { genesisType: 'STRING' },
420
+ AMOUNT: { genesisType: 'DOUBLE' },
421
+ },
422
+ };
423
+ mockGetSchema.resolves(validSchema);
424
+
425
+ const payload: Genesis.ServerReportConfig = {
426
+ ID: 'test-id',
427
+ NAME: 'Test Report',
428
+ SCHEDULES: [],
429
+ DATA_SOURCES: [
430
+ {
431
+ KEY: 'COUNTERPARTY',
432
+ INPUT_TYPE: 'REQ_REP',
433
+ NAME: 'COUNTERPARTY',
434
+ OUTPUT_TYPE: 'TABLE',
435
+ TRANSFORMER_CONFIGURATION: {
436
+ INCLUDE_COLUMNS: ['COUNTERPARTY_NAME', 'AMOUNT', 'NAME'], // NAME orphaned
437
+ COLUMN_RENAMES: {
438
+ COUNTERPARTY_NAME: 'Counterparty',
439
+ NAME: 'Old Name', // orphaned
440
+ },
441
+ COLUMN_FORMATS: {
442
+ AMOUNT: '#,##0.00',
443
+ NAME: 'text', // orphaned
444
+ },
445
+ COLUMN_TRANSFORMS: {
446
+ COUNTERPARTY_NAME: { TYPE: 'BINARY_EXPRESSION', LEFT: {}, OPERATION: 'AND', RIGHT: {} },
447
+ NAME: { TYPE: 'METHOD_EXPRESSION', METHOD: 'TRIM', PARAMETERS: [] }, // orphaned
448
+ } as any,
449
+ ROW_FILTERS: {
450
+ TYPE: 'PREDICATE_EXPRESSION' as const,
451
+ OPERATION: 'AND' as const,
452
+ EXPRESSIONS: [],
453
+ },
454
+ },
455
+ },
456
+ ],
457
+ DESTINATION_IDS: [],
458
+ OUTPUT_DIRECTORY: '/reports',
459
+ };
460
+
461
+ const result = await transformFromServerPayload(payload, mockGetSchema);
462
+
463
+ const datasourceKey = buildDatasourceName('COUNTERPARTY', 'REQ_REP');
464
+ const cleanedConfig = result.datasourceConfig[datasourceKey].TRANSFORMER_CONFIGURATION;
465
+
466
+ // INCLUDE_COLUMNS should only have valid fields
467
+ assert.equal(cleanedConfig.INCLUDE_COLUMNS?.length, 2);
468
+ assert.ok(cleanedConfig.INCLUDE_COLUMNS?.includes('COUNTERPARTY_NAME'));
469
+ assert.ok(cleanedConfig.INCLUDE_COLUMNS?.includes('AMOUNT'));
470
+
471
+ // COLUMN_RENAMES should only have valid fields
472
+ assert.equal(Object.keys(cleanedConfig.COLUMN_RENAMES || {}).length, 1);
473
+ assert.equal(cleanedConfig.COLUMN_RENAMES?.['COUNTERPARTY_NAME'], 'Counterparty');
474
+
475
+ // COLUMN_FORMATS should only have valid fields
476
+ assert.equal(Object.keys(cleanedConfig.COLUMN_FORMATS || {}).length, 1);
477
+ assert.equal(cleanedConfig.COLUMN_FORMATS?.['AMOUNT'], '#,##0.00');
478
+
479
+ // COLUMN_TRANSFORMS should only have valid fields
480
+ assert.equal(Object.keys(cleanedConfig.COLUMN_TRANSFORMS || {}).length, 1);
481
+ assert.ok(cleanedConfig.COLUMN_TRANSFORMS?.['COUNTERPARTY_NAME']);
482
+
483
+ // ROW_FILTERS should be preserved
484
+ assert.ok(cleanedConfig.ROW_FILTERS);
485
+ });
486
+
487
+ TransformFromServerPayload.run();
@@ -1,4 +1,6 @@
1
+ import type { JSONSchema7 } from 'json-schema';
1
2
  import type { BaseConfig, DatasourceInputTypes, DatasourceName, ReportingConfig } from '../store';
3
+ import type { TransformerConfig } from '../store/slices/types';
2
4
  import { Display, Genesis } from '../types';
3
5
 
4
6
  export function transformToServerPayload(state: ReportingConfig): Genesis.ServerReportConfig {
@@ -20,9 +22,41 @@ export function transformToServerPayload(state: ReportingConfig): Genesis.Server
20
22
  };
21
23
  }
22
24
 
25
+ /**
26
+ * Cleans up orphaned field references from transformer configuration
27
+ * by validating against current schema
28
+ */
29
+ function cleanupTransformerConfig(
30
+ config: TransformerConfig,
31
+ validFields: Set<string>,
32
+ ): TransformerConfig {
33
+ return {
34
+ INCLUDE_COLUMNS: config.INCLUDE_COLUMNS?.filter((field) => validFields.has(field)),
35
+ COLUMN_RENAMES: config.COLUMN_RENAMES
36
+ ? Object.fromEntries(
37
+ Object.entries(config.COLUMN_RENAMES).filter(([key]) => validFields.has(key)),
38
+ )
39
+ : undefined,
40
+ COLUMN_FORMATS: config.COLUMN_FORMATS
41
+ ? Object.fromEntries(
42
+ Object.entries(config.COLUMN_FORMATS).filter(([key]) => validFields.has(key)),
43
+ )
44
+ : undefined,
45
+ COLUMN_TRANSFORMS: config.COLUMN_TRANSFORMS
46
+ ? Object.fromEntries(
47
+ Object.entries(config.COLUMN_TRANSFORMS).filter(([key]) => validFields.has(key)),
48
+ )
49
+ : undefined,
50
+ ROW_FILTERS: config.ROW_FILTERS,
51
+ };
52
+ }
53
+
23
54
  // TODO: we'll suppress the bigints on the server so won't need to do this
24
55
  // horrible destructing
25
- export function transformFromServerPaylaod(payload: Genesis.ServerReportConfig): ReportingConfig {
56
+ export async function transformFromServerPayload(
57
+ payload: Genesis.ServerReportConfig,
58
+ getSchema: (name: string) => Promise<JSONSchema7>,
59
+ ): Promise<ReportingConfig> {
26
60
  const {
27
61
  DATA_SOURCES,
28
62
  ID,
@@ -48,18 +82,44 @@ export function transformFromServerPaylaod(payload: Genesis.ServerReportConfig):
48
82
  DOCUMENT_TEMPLATE_ID,
49
83
  ...(SCHEDULES.length > 0 && { SCHEDULE: SCHEDULES[0] }),
50
84
  };
85
+
86
+ const cleanedDatasources = await Promise.all(
87
+ DATA_SOURCES.map(
88
+ async ({ KEY, INPUT_TYPE, NAME: DS_NAME, OUTPUT_TYPE, TRANSFORMER_CONFIGURATION }) => {
89
+ try {
90
+ const schema = await getSchema(KEY);
91
+ const validFields = new Set(Object.keys(schema.properties || {}));
92
+
93
+ const cleanedConfig = cleanupTransformerConfig(
94
+ TRANSFORMER_CONFIGURATION || {},
95
+ validFields,
96
+ );
97
+
98
+ return {
99
+ KEY,
100
+ INPUT_TYPE,
101
+ NAME: DS_NAME,
102
+ OUTPUT_TYPE,
103
+ TRANSFORMER_CONFIGURATION: cleanedConfig,
104
+ };
105
+ } catch (error) {
106
+ // If schema fetch fails, log warning but keep original config
107
+ console.warn(`Failed to fetch schema for ${KEY}, skipping cleanup:`, error);
108
+ return {
109
+ KEY,
110
+ INPUT_TYPE,
111
+ NAME: DS_NAME,
112
+ OUTPUT_TYPE,
113
+ TRANSFORMER_CONFIGURATION: TRANSFORMER_CONFIGURATION || {},
114
+ };
115
+ }
116
+ },
117
+ ),
118
+ );
119
+
51
120
  return {
52
121
  baseConfig,
53
-
54
- datasourceConfig: DATA_SOURCES.map(
55
- ({ KEY, INPUT_TYPE, NAME: DS_NAME, OUTPUT_TYPE, TRANSFORMER_CONFIGURATION }) => ({
56
- KEY,
57
- INPUT_TYPE,
58
- NAME: DS_NAME,
59
- OUTPUT_TYPE,
60
- TRANSFORMER_CONFIGURATION,
61
- }),
62
- ).reduce(
122
+ datasourceConfig: cleanedDatasources.reduce(
63
123
  (acum, ds) => ({
64
124
  ...acum,
65
125
  [buildDatasourceName(ds.NAME, ds.INPUT_TYPE)]: ds,