Class: Skadi::DashboardValidator

Inherits:
ActiveModel::Validator
  • Object
show all
Defined in:
app/models/skadi/dashboard_validator.rb

Overview

Validates that a Dashboard's configuration matches the DashboardConfig type in app/frontend/types.d.ts

Defined Under Namespace

Classes: ArrayOf, Context, Filter, OneOf

Constant Summary collapse

FILTER_TYPES =
{
  boolean: {
    operators: [ "=", "!=", "empty", "not empty" ],
  },
  date: {
    operators: [ "=", ">", ">=", "<=", "<", "!=", "empty", "not empty" ],
  },
  number: {
    operators: [ "=", ">", ">=", "<=", "<", "!=", "empty", "not empty" ],
  },
  one_of: {
    operators: [ "=", "!=", "empty", "not empty" ],
  },
  string: {
    operators: [ "=", "!=", "like", "not like", "empty", "not empty" ],
  },
}
COMMON_DATASET_FIELDS =
{
  id: :string,
  label: :string,
  visible: :boolean?,
  axis: OneOf.new(allowed_values: %w[left right].freeze, allow_missing: true),
}

Class Method Summary collapse

Instance Method Summary collapse

Class Method Details

.typesObject



33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
# File 'app/models/skadi/dashboard_validator.rb', line 33

def self.types
  return @types if defined?(@types)

  @types = {
    DashboardTabConfig: {
      id: :string,
      title: :string,
      description: :string?,
      date_from: :date?,
      date_to: :date?,
      children: ArrayOf.new(:ChartConfig),
    },
    ChartConfig: {
      id: :string,
      type: OneOf.new(allowed_values: %w[bar line table].freeze).freeze,
      title: :string,
      description: :string?,
      time_series: OneOf.new(allowed_values: %w[daily weekly monthly].freeze, allow_missing: true).freeze,
      date_from: :date?,
      date_to: :date?,
      datasets: ArrayOf.new(:Dataset, 1),
    },
    percentageDataset: {
      **COMMON_DATASET_FIELDS,
      type: "percentage",

      numerator: :dataset_id,
      denominator: :dataset_id,
    },
    sqlDataset: {
      **COMMON_DATASET_FIELDS,
      type: "sql",

      sql: :sql,
    },
  }

  Schema.database_schema.each do |dataset_name, schema|
    dataset_type = COMMON_DATASET_FIELDS.dup
    dataset_type[:type] = dataset_name.to_s

    select_fields = []
    split_by_fields = []
    filters = {}

    schema[:fields].each do |field, field_config|
      select_fields << field.to_s if field_config[:select]
      split_by_fields << field.to_s if field_config[:split]
      next unless field_config[:filter]

      if field_config[:type] == :one_of
        filters[field] = OneOf.new(allowed_values: field_config[:options], allow_missing: true)
      else
        filters[field] = field_config[:type] || :string
      end
    end

    schema[:belongs_to]&.keys&.each do |belongs_to_table|
      Schema.database_schema[belongs_to_table][:fields].each do |field, field_config|
       select_fields << "#{belongs_to_table}.#{field}" if field_config[:select]
      end
    end

    dataset_type[:count_by] = OneOf.new(allowed_values: [
      dataset_name.to_s,
      # This may result in duplication if the default is overridden, but it will still function correctly
      *schema[:counts]&.keys&.map(&:to_s),
      *schema[:belongs_to]&.keys&.flat_map do |belongs_to_table|
        [
          belongs_to_table.to_s,
          *Schema.database_schema[belongs_to_table][:counts]&.keys&.map(&:to_s),
        ]
      end,
      nil,
    ], allow_missing: true)

    dataset_type[:selects] = ArrayOf.new(OneOf.new(select_fields, false)) if select_fields.any?
    dataset_type[:split_by] = ArrayOf.new(OneOf.new(split_by_fields, false)) if split_by_fields.any?
    dataset_type[:filters] = ArrayOf.new(Filter.new(filters)) if filters.any?

    @types[:"#{dataset_name}Dataset"] = dataset_type
  end

  # Second pass to add the validations for belongs_to fields
  Schema.database_schema.each do |dataset_name, schema|
    next unless schema[:belongs_to].is_a?(Hash) && schema[:belongs_to].any?

    dataset_type = @types[:"#{dataset_name}Dataset"]

    dataset_type[:belongs_to?] = {}
    schema[:belongs_to].keys.each do |association|
      target_table = @types[:"#{association}Dataset"]

      # Double check that this association is actually in the schema
      next unless target_table.is_a?(Hash)

      association_type = { required: :boolean? }
      association_type[:split_by] = target_table[:split_by] if target_table[:split_by]
      association_type[:filters] = target_table[:filters] if target_table[:filters]

      dataset_type[:belongs_to?][:"#{association}?"] = association_type
    end
  end

  return @types
end

Instance Method Details

#validate(record) ⇒ Object



142
143
144
145
146
# File 'app/models/skadi/dashboard_validator.rb', line 142

def validate(record)
  context = Context.new(record, nil, [])

  validate_type(ArrayOf.new(:DashboardTabConfig, 1), record.configuration, "configuration", context:)
end