@matthieumordrel/chart-studio 0.3.0 → 0.5.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +10 -378
- package/dist/_internal.d.mts +9 -0
- package/dist/_internal.mjs +9 -0
- package/dist/core/chart-builder-controls.mjs +141 -0
- package/dist/core/chart-capabilities.d.mts +5 -0
- package/dist/core/chart-capabilities.mjs +9 -0
- package/dist/core/config-utils.mjs +2 -1
- package/dist/core/dashboard.types.d.mts +220 -0
- package/dist/core/data-label-defaults.d.mts +92 -0
- package/dist/core/data-label-defaults.mjs +78 -0
- package/dist/core/data-model.types.d.mts +196 -0
- package/dist/core/dataset-builder.types.d.mts +51 -0
- package/dist/core/dataset-chart-metadata.d.mts +8 -0
- package/dist/core/dataset-chart-metadata.mjs +4 -0
- package/dist/core/date-range-presets.d.mts +43 -1
- package/dist/core/date-range-presets.mjs +2 -2
- package/dist/core/date-utils.d.mts +26 -0
- package/dist/core/define-dashboard.d.mts +8 -0
- package/dist/core/define-dashboard.mjs +156 -0
- package/dist/core/define-data-model.d.mts +11 -0
- package/dist/core/define-data-model.mjs +327 -0
- package/dist/core/define-dataset.d.mts +13 -0
- package/dist/core/define-dataset.mjs +111 -0
- package/dist/core/formatting.d.mts +49 -0
- package/dist/core/formatting.mjs +32 -10
- package/dist/core/index.d.mts +19 -0
- package/dist/core/infer-columns.mjs +28 -2
- package/dist/core/materialized-view.mjs +580 -0
- package/dist/core/materialized-view.types.d.mts +223 -0
- package/dist/core/metric-utils.d.mts +18 -2
- package/dist/core/metric-utils.mjs +1 -1
- package/dist/core/model-chart.mjs +242 -0
- package/dist/core/model-chart.types.d.mts +199 -0
- package/dist/core/model-inference.mjs +169 -0
- package/dist/core/model-inference.types.d.mts +71 -0
- package/dist/core/pipeline.mjs +32 -1
- package/dist/core/schema-builder.mjs +28 -158
- package/dist/core/schema-builder.types.d.mts +2 -49
- package/dist/core/types.d.mts +61 -10
- package/dist/core/use-chart-options.d.mts +35 -8
- package/dist/core/use-chart-resolvers.mjs +13 -3
- package/dist/core/use-chart.d.mts +16 -12
- package/dist/core/use-chart.mjs +137 -35
- package/dist/core/use-dashboard.d.mts +190 -0
- package/dist/core/use-dashboard.mjs +551 -0
- package/dist/index.d.mts +14 -4
- package/dist/index.mjs +8 -2
- package/package.json +10 -41
- package/LICENSE +0 -21
- package/dist/core/define-chart-schema.d.mts +0 -38
- package/dist/core/define-chart-schema.mjs +0 -39
- package/dist/ui/chart-axis-ticks.mjs +0 -65
- package/dist/ui/chart-canvas.d.mts +0 -33
- package/dist/ui/chart-canvas.mjs +0 -779
- package/dist/ui/chart-context.d.mts +0 -99
- package/dist/ui/chart-context.mjs +0 -115
- package/dist/ui/chart-date-range-badge.d.mts +0 -20
- package/dist/ui/chart-date-range-badge.mjs +0 -49
- package/dist/ui/chart-date-range-panel.d.mts +0 -18
- package/dist/ui/chart-date-range-panel.mjs +0 -126
- package/dist/ui/chart-date-range.d.mts +0 -20
- package/dist/ui/chart-date-range.mjs +0 -67
- package/dist/ui/chart-debug.d.mts +0 -21
- package/dist/ui/chart-debug.mjs +0 -173
- package/dist/ui/chart-dropdown.mjs +0 -92
- package/dist/ui/chart-filters-panel.d.mts +0 -26
- package/dist/ui/chart-filters-panel.mjs +0 -132
- package/dist/ui/chart-filters.d.mts +0 -18
- package/dist/ui/chart-filters.mjs +0 -48
- package/dist/ui/chart-group-by-selector.d.mts +0 -16
- package/dist/ui/chart-group-by-selector.mjs +0 -32
- package/dist/ui/chart-metric-panel.d.mts +0 -25
- package/dist/ui/chart-metric-panel.mjs +0 -172
- package/dist/ui/chart-metric-selector.d.mts +0 -16
- package/dist/ui/chart-metric-selector.mjs +0 -50
- package/dist/ui/chart-select.mjs +0 -61
- package/dist/ui/chart-source-switcher.d.mts +0 -24
- package/dist/ui/chart-source-switcher.mjs +0 -56
- package/dist/ui/chart-time-bucket-selector.d.mts +0 -17
- package/dist/ui/chart-time-bucket-selector.mjs +0 -37
- package/dist/ui/chart-toolbar-overflow.d.mts +0 -28
- package/dist/ui/chart-toolbar-overflow.mjs +0 -231
- package/dist/ui/chart-toolbar.d.mts +0 -33
- package/dist/ui/chart-toolbar.mjs +0 -60
- package/dist/ui/chart-type-selector.d.mts +0 -19
- package/dist/ui/chart-type-selector.mjs +0 -168
- package/dist/ui/chart-x-axis-selector.d.mts +0 -16
- package/dist/ui/chart-x-axis-selector.mjs +0 -28
- package/dist/ui/index.d.mts +0 -19
- package/dist/ui/index.mjs +0 -18
- package/dist/ui/percent-stacked.mjs +0 -36
- package/dist/ui/theme.css +0 -67
- package/dist/ui/toolbar-types.d.mts +0 -7
- package/dist/ui/toolbar-types.mjs +0 -83
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { ChartType, ChartTypeConfig, DefinedChartSchema, FiltersConfig, GroupByConfig, InferableFieldKey, MetricConfig, ResolvedFilterColumnIdFromSchema, ResolvedGroupByColumnIdFromSchema, ResolvedMetricColumnIdFromSchema, ResolvedXAxisColumnIdFromSchema, TimeBucket, TimeBucketConfig, XAxisConfig } from "./types.mjs";
|
|
2
|
+
import { ColumnHelper, ColumnsFromEntries, MetricBuilder, MetricBuilderConfig, SchemaColumnEntry, SchemaFromBuilder, SelectableControlBuilder, SelectableControlBuilderConfig, ValidateColumnEntries } from "./schema-builder.types.mjs";
|
|
3
|
+
import { DATASET_CHART_METADATA, DatasetChartMetadata } from "./dataset-chart-metadata.mjs";
|
|
4
|
+
|
|
5
|
+
//#region src/core/dataset-builder.types.d.ts
|
|
6
|
+
type NonEmptyReadonlyArray<TValue> = readonly [TValue, ...TValue[]];
|
|
7
|
+
type DatasetColumnsContext<TColumns extends Record<string, unknown> | undefined> = [TColumns] extends [undefined] ? undefined : {
|
|
8
|
+
columns?: TColumns;
|
|
9
|
+
};
|
|
10
|
+
type DefinedDatasetChartSchema<TRow, TColumns extends Record<string, unknown> | undefined, TXAxis extends XAxisConfig<any> | undefined, TGroupBy extends GroupByConfig<any> | undefined, TFilters extends FiltersConfig<any> | undefined, TMetric extends MetricConfig<any> | undefined, TChartType extends ChartTypeConfig | undefined, TTimeBucket extends TimeBucketConfig | undefined, TConnectNulls extends boolean | undefined, TChartId extends string | undefined = undefined, TOwner = unknown> = DefinedChartSchema<TRow, SchemaFromBuilder<TColumns, TXAxis, TGroupBy, TFilters, TMetric, TChartType, TTimeBucket, TConnectNulls>> & {
|
|
11
|
+
readonly [DATASET_CHART_METADATA]: DatasetChartMetadata<TChartId, TOwner>;
|
|
12
|
+
};
|
|
13
|
+
type DatasetKey<TRow> = NonEmptyReadonlyArray<InferableFieldKey<TRow>>;
|
|
14
|
+
type DefinedDataset<TRow, TColumns extends Record<string, unknown> | undefined = undefined, TKey extends DatasetKey<TRow> | undefined = undefined> = {
|
|
15
|
+
readonly key?: TKey;
|
|
16
|
+
readonly columns?: TColumns;
|
|
17
|
+
chart<const TChartId extends string | undefined = undefined>(id?: TChartId): DatasetChartBuilder<TRow, TColumns, undefined, undefined, undefined, undefined, undefined, undefined, undefined, TChartId, DefinedDataset<TRow, TColumns, TKey>>;
|
|
18
|
+
validateData(data: readonly TRow[]): void;
|
|
19
|
+
build(): DefinedDataset<TRow, TColumns, TKey>;
|
|
20
|
+
readonly __datasetBrand: 'dataset-definition';
|
|
21
|
+
};
|
|
22
|
+
type DatasetDefinition<TRow, TColumns extends Record<string, unknown> | undefined = undefined, TKey extends DatasetKey<TRow> | undefined = undefined> = {
|
|
23
|
+
build(): DefinedDataset<TRow, TColumns, TKey>;
|
|
24
|
+
};
|
|
25
|
+
type ResolvedDatasetFromDefinition<TDataset> = TDataset extends DatasetDefinition<any, any, any> ? ReturnType<TDataset['build']> : never;
|
|
26
|
+
type DatasetRow<TDataset> = TDataset extends DefinedDataset<infer TRow, any, any> ? TRow : never;
|
|
27
|
+
type DatasetColumns<TDataset> = TDataset extends DefinedDataset<any, infer TColumns, any> ? TColumns : never;
|
|
28
|
+
type DatasetKeyIds<TDataset> = TDataset extends DefinedDataset<any, any, infer TKey> ? TKey : never;
|
|
29
|
+
type SingleDatasetKeyId<TDataset> = DatasetKeyIds<TDataset> extends readonly [infer TKeyId extends string] ? TKeyId : never;
|
|
30
|
+
type DatasetChartBuilder<TRow, TColumns extends Record<string, unknown> | undefined = undefined, TXAxis extends XAxisConfig<any> | undefined = undefined, TGroupBy extends GroupByConfig<any> | undefined = undefined, TFilters extends FiltersConfig<any> | undefined = undefined, TMetric extends MetricConfig<any> | undefined = undefined, TChartType extends ChartTypeConfig | undefined = undefined, TTimeBucket extends TimeBucketConfig | undefined = undefined, TConnectNulls extends boolean | undefined = undefined, TChartId extends string | undefined = undefined, TOwner = unknown> = {
|
|
31
|
+
xAxis<const TBuilder extends SelectableControlBuilder<ResolvedXAxisColumnIdFromSchema<TRow, DatasetColumnsContext<TColumns>>, true>>(defineXAxis: (xAxis: SelectableControlBuilder<ResolvedXAxisColumnIdFromSchema<TRow, DatasetColumnsContext<TColumns>>, true>) => TBuilder): DatasetChartBuilder<TRow, TColumns, SelectableControlBuilderConfig<TBuilder>, TGroupBy, TFilters, TMetric, TChartType, TTimeBucket, TConnectNulls, TChartId, TOwner>;
|
|
32
|
+
groupBy<const TBuilder extends SelectableControlBuilder<ResolvedGroupByColumnIdFromSchema<TRow, DatasetColumnsContext<TColumns>>, true>>(defineGroupBy: (groupBy: SelectableControlBuilder<ResolvedGroupByColumnIdFromSchema<TRow, DatasetColumnsContext<TColumns>>, true>) => TBuilder): DatasetChartBuilder<TRow, TColumns, TXAxis, SelectableControlBuilderConfig<TBuilder>, TFilters, TMetric, TChartType, TTimeBucket, TConnectNulls, TChartId, TOwner>;
|
|
33
|
+
filters<const TBuilder extends SelectableControlBuilder<ResolvedFilterColumnIdFromSchema<TRow, DatasetColumnsContext<TColumns>>, false>>(defineFilters: (filters: SelectableControlBuilder<ResolvedFilterColumnIdFromSchema<TRow, DatasetColumnsContext<TColumns>>, false>) => TBuilder): DatasetChartBuilder<TRow, TColumns, TXAxis, TGroupBy, SelectableControlBuilderConfig<TBuilder>, TMetric, TChartType, TTimeBucket, TConnectNulls, TChartId, TOwner>;
|
|
34
|
+
metric<const TBuilder extends MetricBuilder<ResolvedMetricColumnIdFromSchema<TRow, DatasetColumnsContext<TColumns>>, any, any, any>>(defineMetric: (metric: MetricBuilder<ResolvedMetricColumnIdFromSchema<TRow, DatasetColumnsContext<TColumns>>>) => TBuilder): DatasetChartBuilder<TRow, TColumns, TXAxis, TGroupBy, TFilters, MetricBuilderConfig<TBuilder>, TChartType, TTimeBucket, TConnectNulls, TChartId, TOwner>;
|
|
35
|
+
chartType<const TBuilder extends SelectableControlBuilder<ChartType, true>>(defineChartType: (chartType: SelectableControlBuilder<ChartType, true>) => TBuilder): DatasetChartBuilder<TRow, TColumns, TXAxis, TGroupBy, TFilters, TMetric, SelectableControlBuilderConfig<TBuilder>, TTimeBucket, TConnectNulls, TChartId, TOwner>;
|
|
36
|
+
timeBucket<const TBuilder extends SelectableControlBuilder<TimeBucket, true>>(defineTimeBucket: (timeBucket: SelectableControlBuilder<TimeBucket, true>) => TBuilder): DatasetChartBuilder<TRow, TColumns, TXAxis, TGroupBy, TFilters, TMetric, TChartType, SelectableControlBuilderConfig<TBuilder>, TConnectNulls, TChartId, TOwner>;
|
|
37
|
+
connectNulls<const TValue extends boolean>(value: TValue): DatasetChartBuilder<TRow, TColumns, TXAxis, TGroupBy, TFilters, TMetric, TChartType, TTimeBucket, TValue, TChartId, TOwner>;
|
|
38
|
+
build(): DefinedDatasetChartSchema<TRow, TColumns, TXAxis, TGroupBy, TFilters, TMetric, TChartType, TTimeBucket, TConnectNulls, TChartId, TOwner>;
|
|
39
|
+
readonly [DATASET_CHART_METADATA]: DatasetChartMetadata<TChartId, TOwner>;
|
|
40
|
+
};
|
|
41
|
+
type DatasetChartDefinition<TRow, TColumns extends Record<string, unknown> | undefined = undefined, TXAxis extends XAxisConfig<any> | undefined = undefined, TGroupBy extends GroupByConfig<any> | undefined = undefined, TFilters extends FiltersConfig<any> | undefined = undefined, TMetric extends MetricConfig<any> | undefined = undefined, TChartType extends ChartTypeConfig | undefined = undefined, TTimeBucket extends TimeBucketConfig | undefined = undefined, TConnectNulls extends boolean | undefined = undefined, TChartId extends string | undefined = undefined, TOwner = unknown> = DatasetChartBuilder<TRow, TColumns, TXAxis, TGroupBy, TFilters, TMetric, TChartType, TTimeBucket, TConnectNulls, TChartId, TOwner> | DefinedDatasetChartSchema<TRow, TColumns, TXAxis, TGroupBy, TFilters, TMetric, TChartType, TTimeBucket, TConnectNulls, TChartId, TOwner>;
|
|
42
|
+
interface DatasetBuilder<TRow, TColumns extends Record<string, unknown> | undefined = undefined, TKey extends DatasetKey<TRow> | undefined = undefined> extends DatasetDefinition<TRow, TColumns, TKey> {
|
|
43
|
+
key<const TFieldId extends InferableFieldKey<TRow>>(id: TFieldId): DatasetBuilder<TRow, TColumns, readonly [TFieldId]>;
|
|
44
|
+
key<const TKeyIds extends DatasetKey<TRow>>(ids: TKeyIds): DatasetBuilder<TRow, TColumns, TKeyIds>;
|
|
45
|
+
columns: TColumns extends undefined ? <const TEntries extends readonly SchemaColumnEntry<TRow>[]>(defineColumns: (columns: ColumnHelper<TRow>) => TEntries & ValidateColumnEntries<TEntries>) => DatasetBuilder<TRow, ColumnsFromEntries<TRow, TEntries>, TKey> : never;
|
|
46
|
+
chart<const TChartId extends string | undefined = undefined>(id?: TChartId): DatasetChartBuilder<TRow, TColumns, undefined, undefined, undefined, undefined, undefined, undefined, undefined, TChartId, DefinedDataset<TRow, TColumns, TKey>>;
|
|
47
|
+
validateData(data: readonly TRow[]): void;
|
|
48
|
+
build(): DefinedDataset<TRow, TColumns, TKey>;
|
|
49
|
+
}
|
|
50
|
+
//#endregion
|
|
51
|
+
export { DatasetBuilder, DatasetChartBuilder, DatasetChartDefinition, DatasetColumns, DatasetDefinition, DatasetKey, DatasetKeyIds, DatasetRow, DefinedDataset, DefinedDatasetChartSchema, ResolvedDatasetFromDefinition, SingleDatasetKeyId };
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
//#region src/core/dataset-chart-metadata.d.ts
|
|
2
|
+
declare const DATASET_CHART_METADATA: unique symbol;
|
|
3
|
+
type DatasetChartMetadata<TChartId extends string | undefined = string | undefined, TOwner = unknown> = {
|
|
4
|
+
readonly dataset: TOwner;
|
|
5
|
+
readonly chartId: TChartId;
|
|
6
|
+
};
|
|
7
|
+
//#endregion
|
|
8
|
+
export { DATASET_CHART_METADATA, DatasetChartMetadata };
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { DateRangeFilter, TimeBucket } from "./types.mjs";
|
|
2
|
+
|
|
1
3
|
//#region src/core/date-range-presets.d.ts
|
|
2
4
|
/**
|
|
3
5
|
* All recognised date range preset identifiers.
|
|
@@ -8,5 +10,45 @@
|
|
|
8
10
|
* - Calendar presets — aligned to calendar boundaries
|
|
9
11
|
*/
|
|
10
12
|
type DateRangePresetId = 'auto' | 'all-time' | 'last-7-days' | 'last-30-days' | 'last-3-months' | 'last-12-months' | 'quarter-to-date' | 'year-to-date' | 'last-year';
|
|
13
|
+
type DateRangePreset = {
|
|
14
|
+
id: DateRangePresetId;
|
|
15
|
+
label: string; /** Optional tooltip description shown on hover. */
|
|
16
|
+
description?: string; /** Build the filter for this preset. `null` = no date filtering (all time). */
|
|
17
|
+
buildFilter: () => DateRangeFilter | null;
|
|
18
|
+
};
|
|
19
|
+
/**
|
|
20
|
+
* Ordered list of all date range presets shown in the UI.
|
|
21
|
+
*
|
|
22
|
+
* Layout hint (2-column grid):
|
|
23
|
+
* Auto | All time
|
|
24
|
+
* Last 7 days | Last 30 days
|
|
25
|
+
* Last 3 months | Last 12 months
|
|
26
|
+
* Quarter to date | Year to date
|
|
27
|
+
* Last year |
|
|
28
|
+
*/
|
|
29
|
+
declare const DATE_RANGE_PRESETS: readonly DateRangePreset[];
|
|
30
|
+
/**
|
|
31
|
+
* Map a time bucket to a sensible default date range.
|
|
32
|
+
*
|
|
33
|
+
* - `day` → last 30 days (enough for a meaningful daily trend)
|
|
34
|
+
* - `week` → last 3 months (~13 weeks)
|
|
35
|
+
* - `month` → last 12 months
|
|
36
|
+
* - `quarter` → all time (null)
|
|
37
|
+
* - `year` → all time (null)
|
|
38
|
+
*/
|
|
39
|
+
declare function autoFilterForBucket(bucket: TimeBucket): DateRangeFilter | null;
|
|
40
|
+
/**
|
|
41
|
+
* Compute the effective `DateRangeFilter` for a given preset.
|
|
42
|
+
*
|
|
43
|
+
* For `'auto'`, this uses the provided `timeBucket` to derive the range.
|
|
44
|
+
* For all other presets, the filter is computed from the preset definition.
|
|
45
|
+
*
|
|
46
|
+
* @returns The resolved filter, or `null` for "all time".
|
|
47
|
+
*/
|
|
48
|
+
declare function resolvePresetFilter(presetId: DateRangePresetId, timeBucket: TimeBucket): DateRangeFilter | null;
|
|
49
|
+
/**
|
|
50
|
+
* Get the human-readable label for a preset ID.
|
|
51
|
+
*/
|
|
52
|
+
declare function getPresetLabel(presetId: DateRangePresetId): string;
|
|
11
53
|
//#endregion
|
|
12
|
-
export { DateRangePresetId };
|
|
54
|
+
export { DATE_RANGE_PRESETS, DateRangePreset, DateRangePresetId, autoFilterForBucket, getPresetLabel, resolvePresetFilter };
|
|
@@ -111,7 +111,7 @@ function autoFilterForBucket(bucket) {
|
|
|
111
111
|
* @returns The resolved filter, or `null` for "all time".
|
|
112
112
|
*/
|
|
113
113
|
function resolvePresetFilter(presetId, timeBucket) {
|
|
114
|
-
if (presetId === "auto") return autoFilterForBucket(timeBucket);
|
|
114
|
+
if (presetId === "auto") return autoFilterForBucket(timeBucket) ?? null;
|
|
115
115
|
return DATE_RANGE_PRESETS.find((p) => p.id === presetId)?.buildFilter() ?? null;
|
|
116
116
|
}
|
|
117
117
|
/**
|
|
@@ -149,4 +149,4 @@ function lastYear() {
|
|
|
149
149
|
};
|
|
150
150
|
}
|
|
151
151
|
//#endregion
|
|
152
|
-
export { DATE_RANGE_PRESETS, getPresetLabel, resolvePresetFilter };
|
|
152
|
+
export { DATE_RANGE_PRESETS, autoFilterForBucket, getPresetLabel, resolvePresetFilter };
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { DateColumn, DateRangeFilter } from "./types.mjs";
|
|
2
|
+
|
|
3
|
+
//#region src/core/date-utils.d.ts
|
|
4
|
+
/**
|
|
5
|
+
* Filter data by a date range on a specific date column.
|
|
6
|
+
* Both bounds are inclusive (to is extended to end of day).
|
|
7
|
+
*
|
|
8
|
+
* @param data - Raw data items
|
|
9
|
+
* @param dateColumn - The date column to filter on
|
|
10
|
+
* @param filter - Date range filter with from/to bounds
|
|
11
|
+
* @returns Filtered data items within the date range
|
|
12
|
+
*/
|
|
13
|
+
declare function filterByDateRange<T>(data: readonly T[], dateColumn: DateColumn<T>, filter: DateRangeFilter): T[];
|
|
14
|
+
/**
|
|
15
|
+
* Compute the min/max date range from data for a given date column.
|
|
16
|
+
*
|
|
17
|
+
* @param data - Data items to scan
|
|
18
|
+
* @param dateColumn - The date column to extract dates from
|
|
19
|
+
* @returns Object with min and max dates (both null if no valid dates)
|
|
20
|
+
*/
|
|
21
|
+
declare function computeDateRange<T>(data: readonly T[], dateColumn: DateColumn<T>): {
|
|
22
|
+
min: Date | null;
|
|
23
|
+
max: Date | null;
|
|
24
|
+
};
|
|
25
|
+
//#endregion
|
|
26
|
+
export { computeDateRange, filterByDateRange };
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { DataModelDefinition } from "./data-model.types.mjs";
|
|
2
|
+
import { DashboardBuilder, DashboardDefinition, DashboardInputModel, ResolvedDashboardFromDefinition } from "./dashboard.types.mjs";
|
|
3
|
+
|
|
4
|
+
//#region src/core/define-dashboard.d.ts
|
|
5
|
+
declare function resolveDashboardDefinition<TDashboard extends DashboardDefinition<any, any, any>>(dashboard: TDashboard): ResolvedDashboardFromDefinition<TDashboard>;
|
|
6
|
+
declare function defineDashboard<TModel extends DataModelDefinition<any, any, any, any>>(model: TModel): DashboardBuilder<DashboardInputModel<TModel>>;
|
|
7
|
+
//#endregion
|
|
8
|
+
export { defineDashboard, resolveDashboardDefinition };
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import { DATASET_CHART_METADATA } from "./dataset-chart-metadata.mjs";
|
|
2
|
+
//#region src/core/define-dashboard.ts
|
|
3
|
+
function humanizeId(id) {
|
|
4
|
+
return id.replace(/[_-]+/g, " ").replace(/([a-z0-9])([A-Z])/g, "$1 $2").trim().split(/\s+/).map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join(" ");
|
|
5
|
+
}
|
|
6
|
+
function assertUniqueId(collection, kind, id) {
|
|
7
|
+
if (id in collection) throw new Error(`Duplicate ${kind} id: "${id}"`);
|
|
8
|
+
}
|
|
9
|
+
function getDatasetChartMetadata(chart) {
|
|
10
|
+
if (chart && typeof chart === "object" && DATASET_CHART_METADATA in chart) return chart[DATASET_CHART_METADATA];
|
|
11
|
+
if (chart && typeof chart === "object" && "build" in chart && typeof chart.build === "function") {
|
|
12
|
+
const built = chart.build();
|
|
13
|
+
if (built && typeof built === "object" && DATASET_CHART_METADATA in built) return built[DATASET_CHART_METADATA];
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
function findModelDatasetId(model, dataset) {
|
|
17
|
+
return Object.entries(model.datasets).find(([, candidate]) => candidate === dataset)?.[0];
|
|
18
|
+
}
|
|
19
|
+
function isMaterializedView(source) {
|
|
20
|
+
return !!source && typeof source === "object" && "__materializedViewBrand" in source && "materialization" in source && "build" in source && typeof source.build === "function";
|
|
21
|
+
}
|
|
22
|
+
function resolveChartDataSource(model, datasetOrView) {
|
|
23
|
+
const datasetId = findModelDatasetId(model, datasetOrView);
|
|
24
|
+
if (datasetId) return {
|
|
25
|
+
kind: "dataset",
|
|
26
|
+
datasetId
|
|
27
|
+
};
|
|
28
|
+
if (!isMaterializedView(datasetOrView)) return;
|
|
29
|
+
const view = datasetOrView;
|
|
30
|
+
const baseDatasetId = view.materialization.baseDataset;
|
|
31
|
+
if (!(baseDatasetId in model.datasets)) return;
|
|
32
|
+
return {
|
|
33
|
+
kind: "materialized-view",
|
|
34
|
+
datasetId: baseDatasetId,
|
|
35
|
+
view
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
function createDefinedDashboard(state) {
|
|
39
|
+
let cachedDashboard;
|
|
40
|
+
const build = () => {
|
|
41
|
+
if (cachedDashboard) return cachedDashboard;
|
|
42
|
+
const definedDashboard = {
|
|
43
|
+
model: state.model,
|
|
44
|
+
charts: state.charts,
|
|
45
|
+
sharedFilters: state.sharedFilters,
|
|
46
|
+
build() {
|
|
47
|
+
return definedDashboard;
|
|
48
|
+
},
|
|
49
|
+
__dashboardBrand: "dashboard-definition"
|
|
50
|
+
};
|
|
51
|
+
cachedDashboard = definedDashboard;
|
|
52
|
+
return definedDashboard;
|
|
53
|
+
};
|
|
54
|
+
return build();
|
|
55
|
+
}
|
|
56
|
+
function createDashboardBuilder(state) {
|
|
57
|
+
let cachedDashboard;
|
|
58
|
+
return {
|
|
59
|
+
chart(id, chart) {
|
|
60
|
+
assertUniqueId(state.charts, "dashboard chart", id);
|
|
61
|
+
const metadata = getDatasetChartMetadata(chart);
|
|
62
|
+
if (!metadata) throw new Error(`Dashboard chart "${id}" must come from defineDataset(...).chart(...).`);
|
|
63
|
+
if (metadata.chartId && metadata.chartId !== id) throw new Error(`Dashboard chart "${id}" does not match the chart authoring id "${metadata.chartId}".`);
|
|
64
|
+
const dataSource = resolveChartDataSource(state.model, metadata.dataset);
|
|
65
|
+
if (!dataSource) throw new Error(`Dashboard chart "${id}" references a dataset or materialized view that is not registered in this data model.`);
|
|
66
|
+
return createDashboardBuilder({
|
|
67
|
+
...state,
|
|
68
|
+
charts: {
|
|
69
|
+
...state.charts,
|
|
70
|
+
[id]: {
|
|
71
|
+
id,
|
|
72
|
+
datasetId: dataSource.datasetId,
|
|
73
|
+
schema: chart,
|
|
74
|
+
dataSource
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
},
|
|
79
|
+
sharedFilter(id, config) {
|
|
80
|
+
assertUniqueId(state.sharedFilters, "shared filter", id);
|
|
81
|
+
if (config === void 0) {
|
|
82
|
+
const attribute = state.model.attributes[id];
|
|
83
|
+
if (!attribute) throw new Error(`Unknown model attribute id: "${id}"`);
|
|
84
|
+
return createDashboardBuilder({
|
|
85
|
+
...state,
|
|
86
|
+
sharedFilters: {
|
|
87
|
+
...state.sharedFilters,
|
|
88
|
+
[id]: {
|
|
89
|
+
id,
|
|
90
|
+
kind: "select",
|
|
91
|
+
label: humanizeId(id),
|
|
92
|
+
source: {
|
|
93
|
+
kind: "attribute",
|
|
94
|
+
dataset: attribute.source.dataset,
|
|
95
|
+
key: attribute.source.key,
|
|
96
|
+
label: attribute.source.label
|
|
97
|
+
},
|
|
98
|
+
targets: attribute.targets,
|
|
99
|
+
attribute: id
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
if (config.kind === "select") return createDashboardBuilder({
|
|
105
|
+
...state,
|
|
106
|
+
sharedFilters: {
|
|
107
|
+
...state.sharedFilters,
|
|
108
|
+
[id]: {
|
|
109
|
+
id,
|
|
110
|
+
kind: "select",
|
|
111
|
+
label: config.label ?? humanizeId(id),
|
|
112
|
+
source: {
|
|
113
|
+
kind: "column",
|
|
114
|
+
dataset: config.source.dataset,
|
|
115
|
+
column: config.source.column
|
|
116
|
+
},
|
|
117
|
+
targets: config.targets ?? [{
|
|
118
|
+
dataset: config.source.dataset,
|
|
119
|
+
column: config.source.column
|
|
120
|
+
}]
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
if (!Array.isArray(config.targets) || config.targets.length === 0) throw new Error(`Dashboard shared date-range filter "${id}" requires at least one target.`);
|
|
125
|
+
return createDashboardBuilder({
|
|
126
|
+
...state,
|
|
127
|
+
sharedFilters: {
|
|
128
|
+
...state.sharedFilters,
|
|
129
|
+
[id]: {
|
|
130
|
+
id,
|
|
131
|
+
kind: "date-range",
|
|
132
|
+
label: config.label ?? humanizeId(id),
|
|
133
|
+
targets: config.targets
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
});
|
|
137
|
+
},
|
|
138
|
+
build() {
|
|
139
|
+
if (cachedDashboard) return cachedDashboard;
|
|
140
|
+
cachedDashboard = createDefinedDashboard(state);
|
|
141
|
+
return cachedDashboard;
|
|
142
|
+
}
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
function resolveDashboardDefinition(dashboard) {
|
|
146
|
+
return dashboard.build();
|
|
147
|
+
}
|
|
148
|
+
function defineDashboard(model) {
|
|
149
|
+
return createDashboardBuilder({
|
|
150
|
+
model: model.build(),
|
|
151
|
+
charts: {},
|
|
152
|
+
sharedFilters: {}
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
//#endregion
|
|
156
|
+
export { defineDashboard, resolveDashboardDefinition };
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { DataModelBuilder } from "./data-model.types.mjs";
|
|
2
|
+
|
|
3
|
+
//#region src/core/define-data-model.d.ts
|
|
4
|
+
/**
|
|
5
|
+
* Define one linked data model for dataset registration, relationships,
|
|
6
|
+
* associations, reusable model-level filter attributes, and explicit
|
|
7
|
+
* Phase 7 materialized views.
|
|
8
|
+
*/
|
|
9
|
+
declare function defineDataModel(): DataModelBuilder<{}, {}, {}, {}>;
|
|
10
|
+
//#endregion
|
|
11
|
+
export { defineDataModel };
|
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
import { resolveDatasetDefinition, validateDatasetData } from "./define-dataset.mjs";
|
|
2
|
+
import { createMaterializationStartBuilder } from "./materialized-view.mjs";
|
|
3
|
+
import { attachModelRuntimeMetadata, getModelRuntimeMetadata, inferModelAttributes, inferModelRelationships, rewriteInferredRelationshipError } from "./model-inference.mjs";
|
|
4
|
+
import { compileModelChart } from "./model-chart.mjs";
|
|
5
|
+
//#region src/core/define-data-model.ts
|
|
6
|
+
function formatValue(value) {
|
|
7
|
+
if (value instanceof Date) return value.toISOString();
|
|
8
|
+
return String(value);
|
|
9
|
+
}
|
|
10
|
+
function buildFingerprint(value) {
|
|
11
|
+
if (value instanceof Date) return `date:${value.toISOString()}`;
|
|
12
|
+
return `${typeof value}:${String(value)}`;
|
|
13
|
+
}
|
|
14
|
+
function getSingleDatasetKey(dataset, datasetId) {
|
|
15
|
+
if (!dataset.key || dataset.key.length !== 1) throw new Error(`Dataset "${datasetId}" must declare exactly one key to participate in relationships or associations.`);
|
|
16
|
+
return dataset.key[0];
|
|
17
|
+
}
|
|
18
|
+
function assertUniqueId(collection, kind, id) {
|
|
19
|
+
if (id in collection) throw new Error(`Duplicate ${kind} id: "${id}"`);
|
|
20
|
+
}
|
|
21
|
+
function getDatasetOrThrow(datasets, datasetId) {
|
|
22
|
+
const dataset = datasets[datasetId];
|
|
23
|
+
if (!dataset) throw new Error(`Unknown dataset id: "${datasetId}"`);
|
|
24
|
+
return dataset;
|
|
25
|
+
}
|
|
26
|
+
function createKeyLookup(datasetId, keyId, rows) {
|
|
27
|
+
const values = /* @__PURE__ */ new Set();
|
|
28
|
+
rows.forEach((row, index) => {
|
|
29
|
+
const value = row[keyId];
|
|
30
|
+
if (value == null) throw new Error(`Dataset "${datasetId}" key "${keyId}" is missing a value at row ${index}.`);
|
|
31
|
+
values.add(buildFingerprint(value));
|
|
32
|
+
});
|
|
33
|
+
return {
|
|
34
|
+
keyId,
|
|
35
|
+
values
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
function validateRelationshipData(relationship, data, keyLookups) {
|
|
39
|
+
const toRows = data[relationship.to.dataset];
|
|
40
|
+
const sourceLookup = keyLookups[relationship.from.dataset]?.[relationship.from.key];
|
|
41
|
+
if (!sourceLookup) throw new Error(`Relationship "${relationship.id}" requires dataset "${relationship.from.dataset}" data.`);
|
|
42
|
+
if (!Array.isArray(toRows)) throw new Error(`Relationship "${relationship.id}" requires dataset "${relationship.to.dataset}" data.`);
|
|
43
|
+
toRows.forEach((row, index) => {
|
|
44
|
+
const value = row[relationship.to.column];
|
|
45
|
+
if (value == null) return;
|
|
46
|
+
if (!sourceLookup.values.has(buildFingerprint(value))) throw new Error(`Relationship "${relationship.id}" has an orphan foreign key "${formatValue(value)}" in dataset "${relationship.to.dataset}" column "${relationship.to.column}" at row ${index}.`);
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
function validateExplicitAssociationData(association, associationId, fromDatasetId, toDatasetId, fromLookup, toLookup) {
|
|
50
|
+
association.data.forEach((row, index) => {
|
|
51
|
+
const fromValue = row[association.columns.from];
|
|
52
|
+
const toValue = row[association.columns.to];
|
|
53
|
+
if (fromValue == null) throw new Error(`Association "${associationId}" is missing "${association.columns.from}" at edge row ${index}.`);
|
|
54
|
+
if (toValue == null) throw new Error(`Association "${associationId}" is missing "${association.columns.to}" at edge row ${index}.`);
|
|
55
|
+
if (!fromLookup.values.has(buildFingerprint(fromValue))) throw new Error(`Association "${associationId}" has an orphan "${association.columns.from}" value "${formatValue(fromValue)}" for dataset "${fromDatasetId}".`);
|
|
56
|
+
if (!toLookup.values.has(buildFingerprint(toValue))) throw new Error(`Association "${associationId}" has an orphan "${association.columns.to}" value "${formatValue(toValue)}" for dataset "${toDatasetId}".`);
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
function validateDerivedAssociationData(association, state, data, keyLookups) {
|
|
60
|
+
const derivedEdge = association.edge;
|
|
61
|
+
if (derivedEdge.kind !== "derived") return;
|
|
62
|
+
const deriveDatasetId = derivedEdge.deriveFrom.dataset;
|
|
63
|
+
const deriveRows = data[deriveDatasetId];
|
|
64
|
+
const deriveKeyId = getSingleDatasetKey(getDatasetOrThrow(state.datasets, deriveDatasetId), deriveDatasetId);
|
|
65
|
+
const oppositeDatasetId = deriveDatasetId === association.from.dataset ? association.to.dataset : association.from.dataset;
|
|
66
|
+
const oppositeKeyId = oppositeDatasetId === association.from.dataset ? association.from.key : association.to.key;
|
|
67
|
+
const oppositeLookup = keyLookups[oppositeDatasetId]?.[oppositeKeyId];
|
|
68
|
+
if (!oppositeLookup) throw new Error(`Association "${association.id}" requires dataset "${oppositeDatasetId}" data.`);
|
|
69
|
+
if (!Array.isArray(deriveRows)) throw new Error(`Association "${association.id}" requires dataset "${deriveDatasetId}" data.`);
|
|
70
|
+
deriveRows.forEach((row, index) => {
|
|
71
|
+
if (row[deriveKeyId] == null) throw new Error(`Association "${association.id}" source dataset "${deriveDatasetId}" is missing key "${deriveKeyId}" at row ${index}.`);
|
|
72
|
+
const rawValues = derivedEdge.deriveFrom.values(row) ?? [];
|
|
73
|
+
if (!Array.isArray(rawValues)) throw new Error(`Association "${association.id}" deriveFrom.values(...) must return an array.`);
|
|
74
|
+
rawValues.forEach((value) => {
|
|
75
|
+
if (value == null) throw new Error(`Association "${association.id}" deriveFrom.values(...) returned an empty key value.`);
|
|
76
|
+
if (!oppositeLookup.values.has(buildFingerprint(value))) throw new Error(`Association "${association.id}" has an orphan derived key "${formatValue(value)}" targeting dataset "${oppositeDatasetId}".`);
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
function validateAssociationData(association, state, data, keyLookups) {
|
|
81
|
+
const fromLookup = keyLookups[association.from.dataset]?.[association.from.key];
|
|
82
|
+
const toLookup = keyLookups[association.to.dataset]?.[association.to.key];
|
|
83
|
+
if (!fromLookup || !toLookup) throw new Error(`Association "${association.id}" requires both endpoint datasets to be present.`);
|
|
84
|
+
if (association.edge.kind === "explicit") {
|
|
85
|
+
validateExplicitAssociationData(association.edge, association.id, association.from.dataset, association.to.dataset, fromLookup, toLookup);
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
validateDerivedAssociationData(association, state, data, keyLookups);
|
|
89
|
+
}
|
|
90
|
+
function validateModelRuntimeData(state, data) {
|
|
91
|
+
const keyLookups = {};
|
|
92
|
+
const requiredKeysByDataset = /* @__PURE__ */ new Map();
|
|
93
|
+
const addRequiredKey = (datasetId, keyId) => {
|
|
94
|
+
if (!keyId) return;
|
|
95
|
+
const existing = requiredKeysByDataset.get(datasetId);
|
|
96
|
+
if (existing) {
|
|
97
|
+
existing.add(keyId);
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
requiredKeysByDataset.set(datasetId, new Set([keyId]));
|
|
101
|
+
};
|
|
102
|
+
Object.entries(state.datasets).forEach(([datasetId, dataset]) => {
|
|
103
|
+
const rows = data[datasetId];
|
|
104
|
+
if (!Array.isArray(rows)) throw new Error(`Missing dataset data for "${datasetId}".`);
|
|
105
|
+
validateDatasetData(dataset, rows, datasetId);
|
|
106
|
+
});
|
|
107
|
+
Object.values(state.relationships).forEach((relationship) => {
|
|
108
|
+
addRequiredKey(relationship.from.dataset, relationship.from.key);
|
|
109
|
+
});
|
|
110
|
+
Object.values(state.associations).forEach((association) => {
|
|
111
|
+
addRequiredKey(association.from.dataset, association.from.key);
|
|
112
|
+
addRequiredKey(association.to.dataset, association.to.key);
|
|
113
|
+
});
|
|
114
|
+
Object.values(state.attributes).forEach((attribute) => {
|
|
115
|
+
addRequiredKey(attribute.source.dataset, attribute.source.key);
|
|
116
|
+
});
|
|
117
|
+
Object.entries(state.datasets).forEach(([datasetId, dataset]) => {
|
|
118
|
+
if (dataset.key && dataset.key.length === 1) addRequiredKey(datasetId, dataset.key[0]);
|
|
119
|
+
});
|
|
120
|
+
requiredKeysByDataset.forEach((keyIds, datasetId) => {
|
|
121
|
+
const rows = data[datasetId];
|
|
122
|
+
if (!Array.isArray(rows)) throw new Error(`Missing dataset data for "${datasetId}".`);
|
|
123
|
+
keyLookups[datasetId] = {};
|
|
124
|
+
keyIds.forEach((keyId) => {
|
|
125
|
+
keyLookups[datasetId][keyId] = createKeyLookup(datasetId, keyId, rows);
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
Object.values(state.relationships).forEach((relationship) => {
|
|
129
|
+
validateRelationshipData(relationship, data, keyLookups);
|
|
130
|
+
});
|
|
131
|
+
Object.values(state.associations).forEach((association) => {
|
|
132
|
+
validateAssociationData(association, state, data, keyLookups);
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
function createDefinedDataModel(state) {
|
|
136
|
+
let cachedModel;
|
|
137
|
+
const build = () => {
|
|
138
|
+
if (cachedModel) return cachedModel;
|
|
139
|
+
const definedModel = {
|
|
140
|
+
datasets: state.datasets,
|
|
141
|
+
relationships: state.relationships,
|
|
142
|
+
associations: state.associations,
|
|
143
|
+
attributes: state.attributes,
|
|
144
|
+
materialize(id, defineView) {
|
|
145
|
+
return defineView(createMaterializationStartBuilder(id, definedModel));
|
|
146
|
+
},
|
|
147
|
+
chart(id, defineChart) {
|
|
148
|
+
return compileModelChart(definedModel, id, defineChart);
|
|
149
|
+
},
|
|
150
|
+
validateData(data) {
|
|
151
|
+
try {
|
|
152
|
+
validateModelRuntimeData(state, data);
|
|
153
|
+
} catch (error) {
|
|
154
|
+
rewriteInferredRelationshipError(getModelRuntimeMetadata(definedModel), error);
|
|
155
|
+
}
|
|
156
|
+
},
|
|
157
|
+
build() {
|
|
158
|
+
return definedModel;
|
|
159
|
+
},
|
|
160
|
+
__dataModelBrand: "data-model-definition"
|
|
161
|
+
};
|
|
162
|
+
attachModelRuntimeMetadata(definedModel, { inferredRelationships: new Map([...state.inferredRelationshipIds].map((relationshipId) => {
|
|
163
|
+
const relationship = state.relationships[relationshipId];
|
|
164
|
+
return [relationshipId, {
|
|
165
|
+
id: relationshipId,
|
|
166
|
+
fromDataset: relationship.from.dataset,
|
|
167
|
+
fromKey: relationship.from.key,
|
|
168
|
+
toDataset: relationship.to.dataset,
|
|
169
|
+
toColumn: relationship.to.column
|
|
170
|
+
}];
|
|
171
|
+
})) });
|
|
172
|
+
cachedModel = definedModel;
|
|
173
|
+
return definedModel;
|
|
174
|
+
};
|
|
175
|
+
return build();
|
|
176
|
+
}
|
|
177
|
+
function createDataModelBuilder(state = {
|
|
178
|
+
datasets: {},
|
|
179
|
+
relationships: {},
|
|
180
|
+
associations: {},
|
|
181
|
+
attributes: {},
|
|
182
|
+
inferredRelationshipIds: /* @__PURE__ */ new Set()
|
|
183
|
+
}) {
|
|
184
|
+
let cachedModel;
|
|
185
|
+
return {
|
|
186
|
+
dataset(id, dataset) {
|
|
187
|
+
assertUniqueId(state.datasets, "dataset", id);
|
|
188
|
+
const resolvedDataset = resolveDatasetDefinition(dataset);
|
|
189
|
+
if (!resolvedDataset.key || resolvedDataset.key.length === 0) throw new Error(`Dataset "${id}" must declare a .key() before being added to a data model.`);
|
|
190
|
+
return createDataModelBuilder({
|
|
191
|
+
...state,
|
|
192
|
+
datasets: {
|
|
193
|
+
...state.datasets,
|
|
194
|
+
[id]: resolvedDataset
|
|
195
|
+
}
|
|
196
|
+
});
|
|
197
|
+
},
|
|
198
|
+
relationship(id, config) {
|
|
199
|
+
assertUniqueId(state.relationships, "relationship", id);
|
|
200
|
+
const fromKeyId = getSingleDatasetKey(getDatasetOrThrow(state.datasets, config.from.dataset), config.from.dataset);
|
|
201
|
+
getDatasetOrThrow(state.datasets, config.to.dataset);
|
|
202
|
+
if (config.from.key !== fromKeyId) throw new Error(`Relationship "${id}" must use declared key "${fromKeyId}" from dataset "${config.from.dataset}".`);
|
|
203
|
+
return createDataModelBuilder({
|
|
204
|
+
...state,
|
|
205
|
+
relationships: {
|
|
206
|
+
...state.relationships,
|
|
207
|
+
[id]: {
|
|
208
|
+
kind: "relationship",
|
|
209
|
+
id,
|
|
210
|
+
from: config.from,
|
|
211
|
+
to: config.to,
|
|
212
|
+
reverse: {
|
|
213
|
+
dataset: config.to.dataset,
|
|
214
|
+
column: config.to.column,
|
|
215
|
+
to: {
|
|
216
|
+
dataset: config.from.dataset,
|
|
217
|
+
key: config.from.key
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
});
|
|
223
|
+
},
|
|
224
|
+
association(id, config) {
|
|
225
|
+
assertUniqueId(state.associations, "association", id);
|
|
226
|
+
const fromDataset = getDatasetOrThrow(state.datasets, config.from.dataset);
|
|
227
|
+
const toDataset = getDatasetOrThrow(state.datasets, config.to.dataset);
|
|
228
|
+
const fromKeyId = getSingleDatasetKey(fromDataset, config.from.dataset);
|
|
229
|
+
const toKeyId = getSingleDatasetKey(toDataset, config.to.dataset);
|
|
230
|
+
if (config.from.key !== fromKeyId) throw new Error(`Association "${id}" must use declared key "${fromKeyId}" from dataset "${config.from.dataset}".`);
|
|
231
|
+
if (config.to.key !== toKeyId) throw new Error(`Association "${id}" must use declared key "${toKeyId}" from dataset "${config.to.dataset}".`);
|
|
232
|
+
if ("deriveFrom" in config) {
|
|
233
|
+
if (config.deriveFrom.dataset !== config.from.dataset && config.deriveFrom.dataset !== config.to.dataset) throw new Error(`Association "${id}" deriveFrom.dataset must match either "${config.from.dataset}" or "${config.to.dataset}".`);
|
|
234
|
+
}
|
|
235
|
+
return createDataModelBuilder({
|
|
236
|
+
...state,
|
|
237
|
+
associations: {
|
|
238
|
+
...state.associations,
|
|
239
|
+
[id]: {
|
|
240
|
+
kind: "association",
|
|
241
|
+
id,
|
|
242
|
+
from: config.from,
|
|
243
|
+
to: config.to,
|
|
244
|
+
reverse: {
|
|
245
|
+
dataset: config.to.dataset,
|
|
246
|
+
key: config.to.key,
|
|
247
|
+
to: {
|
|
248
|
+
dataset: config.from.dataset,
|
|
249
|
+
key: config.from.key
|
|
250
|
+
}
|
|
251
|
+
},
|
|
252
|
+
edge: "deriveFrom" in config ? {
|
|
253
|
+
kind: "derived",
|
|
254
|
+
deriveFrom: config.deriveFrom
|
|
255
|
+
} : {
|
|
256
|
+
kind: "explicit",
|
|
257
|
+
data: config.data,
|
|
258
|
+
columns: config.columns
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
});
|
|
263
|
+
},
|
|
264
|
+
attribute(id, config) {
|
|
265
|
+
assertUniqueId(state.attributes, "attribute", id);
|
|
266
|
+
return createDataModelBuilder({
|
|
267
|
+
...state,
|
|
268
|
+
attributes: {
|
|
269
|
+
...state.attributes,
|
|
270
|
+
[id]: {
|
|
271
|
+
id,
|
|
272
|
+
kind: config.kind,
|
|
273
|
+
source: config.source,
|
|
274
|
+
targets: config.targets
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
});
|
|
278
|
+
},
|
|
279
|
+
infer(options) {
|
|
280
|
+
const nextRelationships = options.relationships === true ? inferModelRelationships(state.datasets, state.relationships, new Set(options.exclude ?? [])) : {
|
|
281
|
+
relationships: state.relationships,
|
|
282
|
+
metadata: { inferredRelationships: /* @__PURE__ */ new Map() }
|
|
283
|
+
};
|
|
284
|
+
const nextAttributes = options.attributes === true ? inferModelAttributes(state.datasets, nextRelationships.relationships, state.attributes) : state.attributes;
|
|
285
|
+
return createDataModelBuilder({
|
|
286
|
+
...state,
|
|
287
|
+
relationships: nextRelationships.relationships,
|
|
288
|
+
attributes: nextAttributes,
|
|
289
|
+
inferredRelationshipIds: new Set([...state.inferredRelationshipIds, ...nextRelationships.metadata.inferredRelationships.keys()])
|
|
290
|
+
});
|
|
291
|
+
},
|
|
292
|
+
chart(id, defineChart) {
|
|
293
|
+
const model = cachedModel ?? createDefinedDataModel(state);
|
|
294
|
+
cachedModel = model;
|
|
295
|
+
return compileModelChart(model, id, defineChart);
|
|
296
|
+
},
|
|
297
|
+
materialize(id, defineView) {
|
|
298
|
+
const model = cachedModel ?? createDefinedDataModel(state);
|
|
299
|
+
cachedModel = model;
|
|
300
|
+
return defineView(createMaterializationStartBuilder(id, model));
|
|
301
|
+
},
|
|
302
|
+
validateData(data) {
|
|
303
|
+
try {
|
|
304
|
+
validateModelRuntimeData(state, data);
|
|
305
|
+
} catch (error) {
|
|
306
|
+
const model = cachedModel ?? createDefinedDataModel(state);
|
|
307
|
+
cachedModel = model;
|
|
308
|
+
rewriteInferredRelationshipError(getModelRuntimeMetadata(model), error);
|
|
309
|
+
}
|
|
310
|
+
},
|
|
311
|
+
build() {
|
|
312
|
+
if (cachedModel) return cachedModel;
|
|
313
|
+
cachedModel = createDefinedDataModel(state);
|
|
314
|
+
return cachedModel;
|
|
315
|
+
}
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
/**
|
|
319
|
+
* Define one linked data model for dataset registration, relationships,
|
|
320
|
+
* associations, reusable model-level filter attributes, and explicit
|
|
321
|
+
* Phase 7 materialized views.
|
|
322
|
+
*/
|
|
323
|
+
function defineDataModel() {
|
|
324
|
+
return createDataModelBuilder();
|
|
325
|
+
}
|
|
326
|
+
//#endregion
|
|
327
|
+
export { defineDataModel };
|