# Public query grammar shared with frontend/src/lib/queryState.ts. class QueryState SCHEMAS = { 'conditions' => ['field', 'operator', 'value', 'end', 'blank'], 'drill' => ['field', 'grain', 'value', 'value_type'], 'tag_refs' => ['name', 'tag_id', 'external_tag_id', 'negative', 'missing'], 'column_order' => ['value', 'value_type'], 'measures' => ['key', 'axis', 'value', 'value_type'] }.freeze def self.list(value) return [] if value.nil? || value == '' raise ArgumentError unless value.is_a?(String) && value.bytesize <= 32_768 values = value.split(/ /, -1) raise ArgumentError if values.length > 200 || values.any?(&:empty?) || values.uniq != values values end def self.valid_key?(key) root, index, field, extra = key.split('.', -1) !extra && SCHEMAS.key?(root) && index&.match?(/\A(?:0|[1-9][0-9]?)\z/) && SCHEMAS[root].include?(field) end def self.validate!(params, allowed) raise ArgumentError if params.length > 1000 params.each do |key, value| raise ArgumentError unless allowed.include?(key) || valid_key?(key) raise ArgumentError unless value.is_a?(String) && value.bytesize <= 131_072 end SCHEMAS.each_key { records(params, _1) } end def self.records(params, key) records = {} params.each do |name, value| next unless name.start_with?("#{ key }.") raise ArgumentError unless params.key?(key) && valid_key?(name) _root, index, field = name.split('.') record = records[index.to_i] ||= {} if ['blank', 'negative', 'missing'].include?(field) raise ArgumentError unless ['0', '1'].include?(value) value = value == '1' elsif ['tag_id', 'external_tag_id', 'axis'].include?(field) raise ArgumentError unless value.match?(/\A[0-9]{1,18}\z/) value = value.to_i end record[field] = value end raise ArgumentError if params.key?(key) && params[key] != '' raise ArgumentError unless records.keys.sort == (0...records.length).to_a records.sort.map do |_index, record| type = record.delete('value_type') if ['drill', 'measures', 'column_order'].include?(key) raise ArgumentError unless ['null', 'string', 'number', 'boolean'].include?(type) raise ArgumentError unless type == 'null' || record.key?('value') value = record['value'] record['value'] = case type when 'null' then nil when 'number' raise ArgumentError unless value&.match?(/\A-?[0-9]{1,18}\z/) Integer(value, 10) when 'boolean' raise ArgumentError unless ['true', 'false'].include?(value) value == 'true' else value end end record end end def self.axes(value) list(value).map do |token| field, grain, extra = token.split(':', -1) raise ArgumentError if extra || !field.match?(/\A[a-z_]+\z/) || grain == '' grain ? { 'field' => field, 'grain' => grain } : { 'field' => field } end end end