このコミットが含まれているのは:
2026-10-28 00:28:29 +09:00
コミット d6cf992622
27個のファイルの変更、630行の追加、60行の削除
+83 -2
ファイルの表示
@@ -44,12 +44,31 @@ class CountsCondition
result.find { _1[:key] == 'tags_json' }.merge!(type: :string, multiple: true, collection: true,
operators: CountsResourceDefinition::OPERATORS.fetch(:string))
end
result + [{ key: 'user.id', type: :integer, nullable: true,
result += [{ key: 'user.id', type: :integer, nullable: true,
labels: ['history:actor', 'counts:columns.resourceId'],
operators: CountsResourceDefinition::OPERATORS.fetch(:integer) },
{ key: 'user.name', type: :string, nullable: true,
labels: ['history:actor', 'history:name'],
operators: CountsResourceDefinition::OPERATORS.fetch(:string) }]
result.each do |field|
next if field[:type] == :interval
if field[:multiple]
field[:operators] = field[:operators] - ['in', 'not_in'] + ['any', 'all', 'none']
end
field[:input_types] = ['single', 'multiple']
if ['category', 'tag_category'].include?(field[:key].split('.').last)
field[:candidate_source] = 'categories'
field[:choices] = Tag.categories.keys.map { |value|
{ value:, label: "common:categories.#{ value }" }
}
elsif definition.resource == 'posts' && ['tags', 'tags_json'].include?(field[:key])
field[:candidate_source] = 'tags'
field[:set_type] = 'tag_reference'
end
field[:input_types] << 'list' if field[:candidate_source]
end
result
end
def initialize(population)
@@ -61,11 +80,17 @@ class CountsCondition
def apply(scope, entry)
raise ArgumentError unless entry.is_a?(Hash)
raise ArgumentError if (entry.keys - ['field', 'operator', 'value', 'end', 'blank']).any?
allowed = ['field', 'operator', 'value', 'end', 'blank', 'input', 'values']
raise ArgumentError if (entry.keys - allowed).any?
field = @definition.filter_metadata.find { _1[:key] == entry.fetch('field') }
raise ArgumentError unless field && field[:operators].include?(entry.fetch('operator'))
raise ArgumentError unless [true, false, nil].include?(entry['blank'])
input = entry.fetch('input', 'single')
raise ArgumentError unless ['single', 'multiple', 'list'].include?(input)
if input == 'list' && !field[:candidate_source]
raise ArgumentError
end
values = [entry['value'], entry['end']]
raise ArgumentError unless values.all? { _1.nil? || (_1.is_a?(String) && _1.bytesize <= 8192) }
if entry['operator'] != 'range' && !values[1].nil? && values[1] != ''
@@ -84,6 +109,10 @@ class CountsCondition
end
return scope.where(Arel.sql(sql))
end
if ['in', 'not_in', 'any', 'all', 'none'].include?(entry['operator'])
return set_condition(scope, entry, field)
end
raise ArgumentError if entry.key?('values') || input != 'single'
return scope if values.all? { _1.nil? || _1 == '' }
parsed = values.map { parse(_1, type) }
@@ -122,6 +151,58 @@ class CountsCondition
private
def set_condition(scope, entry, field)
return scope unless entry.key?('values')
values = entry['values']
raise ArgumentError unless values.is_a?(Array) && values.length <= 100
raise ArgumentError if [entry['value'], entry['end']].any? { !_1.nil? && _1 != '' }
parsed = values.map do |value|
unless (value.is_a?(String) || value.is_a?(Numeric) || [true, false].include?(value))
raise ArgumentError
end
raise ArgumentError if value.to_s.bytesize > 8192
if field[:set_type] == 'tag_reference'
raise ArgumentError unless value.is_a?(String) &&
value.match?(/\A(?:internal|external):[1-9][0-9]{0,17}\z/)
value
else
raise ArgumentError if value == '' && ![:string, :enum].include?(field[:type])
result = value == '' ? '' : parse(value.to_s, field[:type])
validate_enum!(field[:key], result) if field[:type] == :enum
result
end
end
raise ArgumentError unless parsed.uniq == parsed
operator = entry['operator']
if parsed.empty?
return ['in', 'any'].include?(operator) ? scope.none : scope
end
source, expression = source_for(field[:key])
if field[:set_type] == 'tag_reference'
source = post_tag_source
id = "COALESCE(#{ scalar('tag_snapshot.item', 'tag_id', :integer) }, " +
"#{ scalar('tag_snapshot.item', 'external_tag_id', :integer) })"
expression = "CONCAT(CASE WHEN #{ scalar('tag_snapshot.item', 'tag_id') } IS NULL " +
"THEN 'external:' ELSE 'internal:' END, #{ id })"
end
predicates = parsed.map do |_value|
clause = "#{ expression } = ?"
clause = "EXISTS (SELECT 1 FROM #{ source } WHERE #{ clause })" if source
clause
end
conjunction = operator == 'all' ? ' AND ' : ' OR '
predicate = "(#{ predicates.join(conjunction) })"
if ['not_in', 'none'].include?(operator)
predicate = source ? "NOT #{ predicate }" :
"(NOT #{ predicate } OR #{ blank_sql(expression, field[:type]) })"
end
scope.where(predicate, *parsed)
end
def parse(value, type)
return nil if value.nil? || value == ''