@adminforth/dashboard 1.1.0 → 1.2.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 (39) hide show
  1. package/README.md +11 -28
  2. package/custom/model/dashboard.types.ts +236 -42
  3. package/custom/runtime/DashboardRuntime.vue +2 -1
  4. package/custom/runtime/WidgetRenderer.vue +2 -1
  5. package/custom/skills/adminforth-dashboard/SKILL.md +4 -4
  6. package/custom/widgets/chart/ChartWidget.vue +45 -12
  7. package/custom/widgets/chart/chart.types.ts +83 -0
  8. package/custom/widgets/gauge-card/GaugeCardWidget.vue +7 -38
  9. package/custom/widgets/kpi-card/KpiCardWidget.vue +6 -8
  10. package/custom/widgets/pivot-table/PivotTableWidget.vue +8 -10
  11. package/custom/widgets/table/TableWidget.vue +1 -1
  12. package/dist/custom/model/dashboard.types.d.ts +25 -1
  13. package/dist/custom/model/dashboard.types.js +133 -42
  14. package/dist/custom/model/dashboard.types.ts +236 -42
  15. package/dist/custom/queries/useDashboardConfig.d.ts +0 -2
  16. package/dist/custom/queries/useWidgetData.d.ts +0 -2
  17. package/dist/custom/runtime/DashboardRuntime.vue +2 -1
  18. package/dist/custom/runtime/WidgetRenderer.vue +2 -1
  19. package/dist/custom/skills/adminforth-dashboard/SKILL.md +4 -4
  20. package/dist/custom/widgets/chart/ChartWidget.vue +45 -12
  21. package/dist/custom/widgets/chart/chart.types.d.ts +15 -0
  22. package/dist/custom/widgets/chart/chart.types.js +46 -0
  23. package/dist/custom/widgets/chart/chart.types.ts +83 -0
  24. package/dist/custom/widgets/gauge-card/GaugeCardWidget.vue +7 -38
  25. package/dist/custom/widgets/kpi-card/KpiCardWidget.vue +6 -8
  26. package/dist/custom/widgets/pivot-table/PivotTableWidget.vue +8 -10
  27. package/dist/custom/widgets/table/TableWidget.vue +1 -1
  28. package/dist/endpoint/widgets.js +20 -3
  29. package/dist/schema/api.d.ts +0 -240
  30. package/dist/schema/widget.d.ts +0 -132
  31. package/dist/schema/widget.js +30 -16
  32. package/dist/services/widgetConfigValidator.js +9 -49
  33. package/dist/services/widgetDataService.d.ts +0 -9
  34. package/dist/services/widgetDataService.js +12 -30
  35. package/endpoint/widgets.ts +26 -3
  36. package/package.json +1 -1
  37. package/schema/widget.ts +34 -17
  38. package/services/widgetConfigValidator.ts +10 -57
  39. package/services/widgetDataService.ts +10 -45
@@ -1,6 +1,9 @@
1
1
  <script setup lang="ts">
2
2
  import { computed, watch } from 'vue'
3
3
  import { useWidgetData } from '../../queries/useWidgetData.js'
4
+ import {
5
+ normalizeGaugeCardWidgetConfig,
6
+ } from '../../model/dashboard.types.js'
4
7
  import type { DashboardWidgetConfig, DashboardWidgetTableData } from '../../model/dashboard.types.js'
5
8
  import { CHART_COLORS, formatChartValue, toFiniteNumber } from '../chart/chart.utils.js'
6
9
 
@@ -18,40 +21,6 @@ const {
18
21
  refetch,
19
22
  } = useWidgetData(dashboardSlugRef, widgetIdRef)
20
23
 
21
- type GaugeCardConfig = {
22
- value_field?: string
23
- valueField?: string
24
- min?: number | string
25
- max?: number | string
26
- min_field?: string
27
- minField?: string
28
- max_field?: string
29
- maxField?: string
30
- suffix?: string
31
- color?: string
32
- }
33
-
34
- function isRecord(value: unknown): value is Record<string, unknown> {
35
- return typeof value === 'object' && value !== null
36
- }
37
-
38
- function parseGaugeCardConfig(value: unknown): GaugeCardConfig | undefined {
39
- if (isRecord(value)) {
40
- return value as GaugeCardConfig
41
- }
42
-
43
- if (typeof value !== 'string') {
44
- return undefined
45
- }
46
-
47
- try {
48
- const parsed = JSON.parse(value) as unknown
49
- return isRecord(parsed) ? parsed as GaugeCardConfig : undefined
50
- } catch {
51
- return undefined
52
- }
53
- }
54
-
55
24
  function parseOptionalNumber(value: unknown): number | undefined {
56
25
  if (value === null || value === undefined || value === '') {
57
26
  return undefined
@@ -86,13 +55,13 @@ watch(
86
55
  { deep: true },
87
56
  )
88
57
 
89
- const gaugeConfig = computed(() => parseGaugeCardConfig(props.widget.gauge_card))
58
+ const gaugeConfig = computed(() => normalizeGaugeCardWidgetConfig(props.widget.gauge_card))
90
59
  const widgetData = computed(() => data.value?.data as DashboardWidgetTableData | null)
91
60
  const columns = computed(() => widgetData.value?.columns ?? [])
92
61
  const firstRow = computed(() => widgetData.value?.rows[0] ?? {})
93
- const valueField = computed(() => gaugeConfig.value?.value_field || gaugeConfig.value?.valueField || columns.value[0])
94
- const minField = computed(() => gaugeConfig.value?.min_field || gaugeConfig.value?.minField)
95
- const maxField = computed(() => gaugeConfig.value?.max_field || gaugeConfig.value?.maxField)
62
+ const valueField = computed(() => gaugeConfig.value?.valueField || columns.value[0])
63
+ const minField = computed(() => gaugeConfig.value?.minField)
64
+ const maxField = computed(() => gaugeConfig.value?.maxField)
96
65
  const minValue = computed(() => {
97
66
  const dynamicMin = minField.value ? parseOptionalNumber(firstRow.value[minField.value]) : undefined
98
67
  return dynamicMin ?? parseOptionalNumber(gaugeConfig.value?.min) ?? 0
@@ -1,6 +1,9 @@
1
1
  <script setup lang="ts">
2
2
  import { computed, watch } from 'vue'
3
3
  import { useWidgetData } from '../../queries/useWidgetData.js'
4
+ import {
5
+ normalizeKpiCardWidgetConfig,
6
+ } from '../../model/dashboard.types.js'
4
7
  import type { DashboardWidgetConfig, DashboardWidgetTableData } from '../../model/dashboard.types.js'
5
8
  import { formatChartValue, toFiniteNumber } from '../chart/chart.utils.js'
6
9
 
@@ -26,17 +29,12 @@ watch(
26
29
  { deep: true },
27
30
  )
28
31
 
29
- const kpiConfig = computed(() => props.widget.kpi_card as {
30
- value_field?: string
31
- label_field?: string
32
- prefix?: string
33
- suffix?: string
34
- } | undefined)
32
+ const kpiConfig = computed(() => normalizeKpiCardWidgetConfig(props.widget.kpi_card))
35
33
  const widgetData = computed(() => data.value?.data as DashboardWidgetTableData | null)
36
34
  const columns = computed(() => widgetData.value?.columns ?? [])
37
35
  const firstRow = computed(() => widgetData.value?.rows[0] ?? {})
38
- const valueField = computed(() => kpiConfig.value?.value_field || columns.value[0])
39
- const labelField = computed(() => kpiConfig.value?.label_field)
36
+ const valueField = computed(() => kpiConfig.value?.valueField || columns.value[0])
37
+ const labelField = computed(() => kpiConfig.value?.labelField)
40
38
  const value = computed(() => toFiniteNumber(firstRow.value[valueField.value]))
41
39
  const label = computed(() => labelField.value ? String(firstRow.value[labelField.value]) : props.widget.label)
42
40
  const formattedValue = computed(() => `${kpiConfig.value?.prefix ?? ''}${formatChartValue(value.value)}${kpiConfig.value?.suffix ?? ''}`)
@@ -1,6 +1,9 @@
1
1
  <script setup lang="ts">
2
2
  import { computed, watch } from 'vue'
3
3
  import { useWidgetData } from '../../queries/useWidgetData.js'
4
+ import {
5
+ normalizePivotTableWidgetConfig,
6
+ } from '../../model/dashboard.types.js'
4
7
  import type { DashboardWidgetConfig, DashboardWidgetData } from '../../model/dashboard.types.js'
5
8
  import { formatChartLabel, formatChartValue, toFiniteNumber } from '../chart/chart.utils.js'
6
9
 
@@ -26,20 +29,15 @@ watch(
26
29
  { deep: true },
27
30
  )
28
31
 
29
- const pivotConfig = computed(() => props.widget.pivot_table as {
30
- row_field?: string
31
- column_field?: string
32
- value_field?: string
33
- aggregation?: 'count' | 'sum'
34
- } | undefined)
32
+ const pivotConfig = computed(() => normalizePivotTableWidgetConfig(props.widget.pivot_table))
35
33
  const widgetData = computed(() => data.value?.data as DashboardWidgetData | null)
36
34
  const rows = computed(() => widgetData.value?.rows ?? [])
37
35
  const columns = computed(() => widgetData.value?.columns ?? [])
38
36
  const isAggregateData = computed(() => widgetData.value?.kind === 'aggregate')
39
- const shouldRenderAggregateMatrix = computed(() => isAggregateData.value && !pivotConfig.value?.column_field)
40
- const rowField = computed(() => pivotConfig.value?.row_field || (isAggregateData.value ? 'group' : columns.value[0]))
41
- const columnField = computed(() => pivotConfig.value?.column_field || columns.value[1])
42
- const valueField = computed(() => pivotConfig.value?.value_field || columns.value[2] || columns.value[1])
37
+ const shouldRenderAggregateMatrix = computed(() => isAggregateData.value && !pivotConfig.value?.columnField)
38
+ const rowField = computed(() => pivotConfig.value?.rowField || (isAggregateData.value ? 'group' : columns.value[0]))
39
+ const columnField = computed(() => pivotConfig.value?.columnField || columns.value[1])
40
+ const valueField = computed(() => pivotConfig.value?.valueField || columns.value[2] || columns.value[1])
43
41
  const aggregation = computed(() => pivotConfig.value?.aggregation || (valueField.value ? 'sum' : 'count'))
44
42
  const pivotColumnLabels = computed(() => {
45
43
  if (shouldRenderAggregateMatrix.value) {
@@ -132,7 +132,7 @@ const currentPage = ref(1)
132
132
  const currentPageInput = ref(1)
133
133
  const tableConfig = computed(() => props.widget.table as TableWidgetConfig | undefined)
134
134
  const isPaginationEnabled = computed(() => tableConfig.value?.pagination !== false)
135
- const pageSize = computed(() => tableConfig.value?.pageSize ?? props.widget.query?.limit ?? DEFAULT_PAGE_SIZE)
135
+ const pageSize = computed(() => tableConfig.value?.pageSize ?? DEFAULT_PAGE_SIZE)
136
136
  const dashboardSlugRef = computed(() => props.dashboardSlug)
137
137
  const widgetIdRef = computed(() => props.widget.id)
138
138
  const widgetDataRequest = computed(() => (
@@ -66,7 +66,6 @@ export type DashboardWidgetConfig = {
66
66
  kpi_card?: unknown;
67
67
  pivot_table?: unknown;
68
68
  gauge_card?: unknown;
69
- query?: unknown;
70
69
  };
71
70
  export type DashboardWidgetTableData = {
72
71
  kind?: 'table';
@@ -86,5 +85,30 @@ export type DashboardWidgetAggregateData = {
86
85
  values?: Record<string, unknown>;
87
86
  };
88
87
  export type DashboardWidgetData = DashboardWidgetTableData | DashboardWidgetAggregateData;
88
+ export type NormalizedKpiCardWidgetConfig = {
89
+ valueField?: string;
90
+ labelField?: string;
91
+ prefix?: string;
92
+ suffix?: string;
93
+ };
94
+ export type NormalizedGaugeCardWidgetConfig = {
95
+ valueField?: string;
96
+ min?: number | string;
97
+ max?: number | string;
98
+ minField?: string;
99
+ maxField?: string;
100
+ suffix?: string;
101
+ color?: string;
102
+ };
103
+ export type NormalizedPivotTableWidgetConfig = {
104
+ rowField?: string;
105
+ columnField?: string;
106
+ valueField?: string;
107
+ aggregation?: 'count' | 'sum';
108
+ };
89
109
  export declare function normalizeDashboardConfig(config: unknown): DashboardConfig;
90
110
  export declare function normalizeDashboardWidgetConfig(config: unknown): unknown;
111
+ export declare function serializeDashboardWidgetConfigForEditor(widget: DashboardWidgetConfig): Record<string, unknown>;
112
+ export declare function normalizeKpiCardWidgetConfig(value: unknown): NormalizedKpiCardWidgetConfig | undefined;
113
+ export declare function normalizeGaugeCardWidgetConfig(value: unknown): NormalizedGaugeCardWidgetConfig | undefined;
114
+ export declare function normalizePivotTableWidgetConfig(value: unknown): NormalizedPivotTableWidgetConfig | undefined;
@@ -2,6 +2,10 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.normalizeDashboardConfig = normalizeDashboardConfig;
4
4
  exports.normalizeDashboardWidgetConfig = normalizeDashboardWidgetConfig;
5
+ exports.serializeDashboardWidgetConfigForEditor = serializeDashboardWidgetConfigForEditor;
6
+ exports.normalizeKpiCardWidgetConfig = normalizeKpiCardWidgetConfig;
7
+ exports.normalizeGaugeCardWidgetConfig = normalizeGaugeCardWidgetConfig;
8
+ exports.normalizePivotTableWidgetConfig = normalizePivotTableWidgetConfig;
5
9
  function normalizeDashboardConfig(config) {
6
10
  const value = isRecord(config) ? config : {};
7
11
  return {
@@ -13,29 +17,78 @@ function normalizeDashboardConfig(config) {
13
17
  };
14
18
  }
15
19
  function normalizeDashboardWidgetConfig(config) {
16
- var _a;
17
20
  if (!isRecord(config)) {
18
21
  return config;
19
22
  }
20
23
  const normalized = Object.assign({}, config);
21
- const target = normalizeDashboardWidgetTarget((_a = normalized.target) !== null && _a !== void 0 ? _a : normalized.type);
22
- if (target && normalized.target === undefined) {
23
- normalized.target = target;
24
+ normalizeWidgetLayoutConfig(normalized);
25
+ if (normalized.table !== undefined) {
26
+ normalized.table = normalizeTableConfig(normalized.table);
24
27
  }
25
- if (target === 'kpi_card') {
26
- const kpiCardConfig = normalizeKpiCardConfig(normalized);
27
- if (kpiCardConfig !== undefined) {
28
- normalized.kpi_card = kpiCardConfig;
29
- }
28
+ if (normalized.data_source !== undefined) {
29
+ normalized.dataSource = normalizeWidgetDataSource(normalized.data_source);
30
30
  }
31
- if (target === 'gauge_card') {
32
- const gaugeCardConfig = normalizeGaugeCardConfig(normalized);
33
- if (gaugeCardConfig !== undefined) {
34
- normalized.gauge_card = gaugeCardConfig;
35
- }
31
+ const target = normalizeDashboardWidgetTarget(normalized.target);
32
+ if (target !== undefined) {
33
+ normalized.target = target;
36
34
  }
37
35
  return normalized;
38
36
  }
37
+ function serializeDashboardWidgetConfigForEditor(widget) {
38
+ const serialized = Object.assign({}, widget);
39
+ if (Object.prototype.hasOwnProperty.call(serialized, 'minWidth')) {
40
+ serialized.min_width = widget.minWidth;
41
+ delete serialized.minWidth;
42
+ }
43
+ if (Object.prototype.hasOwnProperty.call(serialized, 'maxWidth')) {
44
+ serialized.max_width = widget.maxWidth;
45
+ delete serialized.maxWidth;
46
+ }
47
+ if (widget.table !== undefined) {
48
+ serialized.table = serializeTableConfigForEditor(widget.table);
49
+ }
50
+ if (widget.dataSource !== undefined) {
51
+ serialized.data_source = serializeWidgetDataSourceForEditor(widget.dataSource);
52
+ delete serialized.dataSource;
53
+ }
54
+ return serialized;
55
+ }
56
+ function normalizeKpiCardWidgetConfig(value) {
57
+ const config = asWidgetConfigRecord(value);
58
+ if (!config) {
59
+ return undefined;
60
+ }
61
+ const valueField = getStringField(config, 'value_field');
62
+ const labelField = getStringField(config, 'label_field');
63
+ const prefix = getStringField(config, 'prefix');
64
+ const suffix = getStringField(config, 'suffix');
65
+ return Object.assign(Object.assign(Object.assign(Object.assign({}, (valueField !== undefined ? { valueField } : {})), (labelField !== undefined ? { labelField } : {})), (prefix !== undefined ? { prefix } : {})), (suffix !== undefined ? { suffix } : {}));
66
+ }
67
+ function normalizeGaugeCardWidgetConfig(value) {
68
+ const config = asWidgetConfigRecord(value);
69
+ if (!config) {
70
+ return undefined;
71
+ }
72
+ const valueField = getStringField(config, 'value_field');
73
+ const minField = getStringField(config, 'min_field');
74
+ const maxField = getStringField(config, 'max_field');
75
+ const suffix = getStringField(config, 'suffix');
76
+ const color = getStringField(config, 'color');
77
+ return Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign({}, (valueField !== undefined ? { valueField } : {})), (config.min !== undefined ? { min: config.min } : {})), (config.max !== undefined ? { max: config.max } : {})), (minField !== undefined ? { minField } : {})), (maxField !== undefined ? { maxField } : {})), (suffix !== undefined ? { suffix } : {})), (color !== undefined ? { color } : {}));
78
+ }
79
+ function normalizePivotTableWidgetConfig(value) {
80
+ const config = asWidgetConfigRecord(value);
81
+ if (!config) {
82
+ return undefined;
83
+ }
84
+ const rowField = getStringField(config, 'row_field');
85
+ const columnField = getStringField(config, 'column_field');
86
+ const valueField = getStringField(config, 'value_field');
87
+ const aggregation = config.aggregation === 'count' || config.aggregation === 'sum'
88
+ ? config.aggregation
89
+ : undefined;
90
+ return Object.assign(Object.assign(Object.assign(Object.assign({}, (rowField !== undefined ? { rowField } : {})), (columnField !== undefined ? { columnField } : {})), (valueField !== undefined ? { valueField } : {})), (aggregation !== undefined ? { aggregation } : {}));
91
+ }
39
92
  function normalizeDashboardWidgetTarget(value) {
40
93
  switch (value) {
41
94
  case 'empty':
@@ -49,46 +102,84 @@ function normalizeDashboardWidgetTarget(value) {
49
102
  return undefined;
50
103
  }
51
104
  }
52
- function normalizeKpiCardConfig(value) {
53
- const config = isRecord(value.kpi_card) ? Object.assign({}, value.kpi_card) : {};
54
- if (typeof value.valueField === 'string' && config.value_field === undefined) {
55
- config.value_field = value.valueField;
105
+ function normalizeWidgetLayoutConfig(value) {
106
+ if (value.min_width !== undefined) {
107
+ value.minWidth = value.min_width;
56
108
  }
57
- if (typeof value.labelField === 'string' && config.label_field === undefined) {
58
- config.label_field = value.labelField;
109
+ if (value.max_width !== undefined) {
110
+ value.maxWidth = value.max_width;
59
111
  }
60
- if (typeof value.prefix === 'string' && config.prefix === undefined) {
61
- config.prefix = value.prefix;
112
+ }
113
+ function normalizeTableConfig(value) {
114
+ if (!isRecord(value)) {
115
+ return value;
62
116
  }
63
- if (typeof value.suffix === 'string' && config.suffix === undefined) {
64
- config.suffix = value.suffix;
117
+ const normalized = Object.assign({}, value);
118
+ if (normalized.page_size !== undefined) {
119
+ normalized.pageSize = normalized.page_size;
65
120
  }
66
- return Object.keys(config).length ? config : value.kpi_card;
121
+ return normalized;
67
122
  }
68
- function normalizeGaugeCardConfig(value) {
69
- const config = isRecord(value.gauge_card) ? Object.assign({}, value.gauge_card) : {};
70
- if (typeof value.valueField === 'string' && config.value_field === undefined) {
71
- config.value_field = value.valueField;
123
+ function normalizeWidgetDataSource(value) {
124
+ if (!isRecord(value) || typeof value.type !== 'string') {
125
+ return value;
126
+ }
127
+ const resourceId = typeof value.resource_id === 'string'
128
+ ? value.resource_id
129
+ : undefined;
130
+ if (value.type === 'resource') {
131
+ return Object.assign(Object.assign(Object.assign(Object.assign({ type: 'resource' }, (resourceId !== undefined ? { resourceId } : {})), (value.columns !== undefined ? { columns: value.columns } : {})), (value.filters !== undefined ? { filters: value.filters } : {})), (value.sort !== undefined ? { sort: value.sort } : {}));
72
132
  }
73
- if (value.min !== undefined && config.min === undefined) {
74
- config.min = value.min;
133
+ if (value.type === 'aggregate') {
134
+ const groupBy = normalizeGroupByRule(value.group_by);
135
+ return Object.assign(Object.assign(Object.assign(Object.assign({ type: 'aggregate' }, (resourceId !== undefined ? { resourceId } : {})), (value.aggregations !== undefined ? { aggregations: value.aggregations } : {})), (groupBy !== undefined ? { groupBy } : {})), (value.filters !== undefined ? { filters: value.filters } : {}));
136
+ }
137
+ return value;
138
+ }
139
+ function normalizeGroupByRule(value) {
140
+ if (!isRecord(value) || typeof value.type !== 'string') {
141
+ return value;
75
142
  }
76
- if (value.max !== undefined && config.max === undefined) {
77
- config.max = value.max;
143
+ if (value.type === 'field') {
144
+ return Object.assign({ type: 'field' }, (value.field !== undefined ? { field: value.field } : {}));
78
145
  }
79
- if (typeof value.minField === 'string' && config.min_field === undefined) {
80
- config.min_field = value.minField;
146
+ if (value.type === 'date_trunc') {
147
+ return Object.assign(Object.assign(Object.assign({ type: 'date_trunc' }, (value.field !== undefined ? { field: value.field } : {})), (value.truncation !== undefined ? { truncation: value.truncation } : {})), (value.timezone !== undefined ? { timezone: value.timezone } : {}));
81
148
  }
82
- if (typeof value.maxField === 'string' && config.max_field === undefined) {
83
- config.max_field = value.maxField;
149
+ return value;
150
+ }
151
+ function serializeTableConfigForEditor(value) {
152
+ if (!isRecord(value)) {
153
+ return value;
84
154
  }
85
- if (typeof value.suffix === 'string' && config.suffix === undefined) {
86
- config.suffix = value.suffix;
155
+ const serialized = Object.assign({}, value);
156
+ if (Object.prototype.hasOwnProperty.call(serialized, 'pageSize')) {
157
+ serialized.page_size = serialized.pageSize;
158
+ delete serialized.pageSize;
87
159
  }
88
- if (typeof value.color === 'string' && config.color === undefined) {
89
- config.color = value.color;
160
+ return serialized;
161
+ }
162
+ function serializeWidgetDataSourceForEditor(value) {
163
+ if (value.type === 'resource') {
164
+ return Object.assign(Object.assign(Object.assign({ type: 'resource', resource_id: value.resourceId }, (value.columns !== undefined ? { columns: value.columns } : {})), (value.filters !== undefined ? { filters: value.filters } : {})), (value.sort !== undefined ? { sort: value.sort } : {}));
90
165
  }
91
- return Object.keys(config).length ? config : value.gauge_card;
166
+ return Object.assign(Object.assign({ type: 'aggregate', resource_id: value.resourceId, aggregations: value.aggregations }, (value.groupBy !== undefined ? { group_by: serializeGroupByRuleForEditor(value.groupBy) } : {})), (value.filters !== undefined ? { filters: value.filters } : {}));
167
+ }
168
+ function serializeGroupByRuleForEditor(value) {
169
+ if (value.type === 'field') {
170
+ return {
171
+ type: 'field',
172
+ field: value.field,
173
+ };
174
+ }
175
+ return Object.assign({ type: 'date_trunc', field: value.field, truncation: value.truncation }, (value.timezone !== undefined ? { timezone: value.timezone } : {}));
176
+ }
177
+ function asWidgetConfigRecord(value) {
178
+ return isRecord(value) ? value : undefined;
179
+ }
180
+ function getStringField(record, key) {
181
+ const value = record[key];
182
+ return typeof value === 'string' ? value : undefined;
92
183
  }
93
184
  function isRecord(value) {
94
185
  return typeof value === 'object' && value !== null;