class PostImportsController < ApplicationController MAPPING_PROPERTIES = ['kind', 'value', 'columns'].freeze MAPPING_KINDS = ['columns', 'fixed'].freeze before_action :require_member! def parse source = if params[:format] == 'google_sheets' PostImportGoogleSheetsFetcher.fetch!(params[:source], rate_key: current_user.id) else params[:source] end format = params[:format] == 'google_sheets' ? 'csv' : params[:format] parsed = PostImportSourceParser.new( source:, format:, has_header: params[:has_header], json_path: params[:json_path]).parse render json: parsed rescue ArgumentError => e render_bad_request(e.message) end def preview parsed = parsed_params raw_url_column = params[:url_column].presence || (parsed[:columns].length == 1 ? 0 : nil) return render_bad_request('URL 列を選択してください.') if raw_url_column.nil? url_column = Integer(raw_url_column, exception: false) if url_column.nil? || url_column.negative? || url_column >= parsed[:columns].length return render_bad_request('URL 列が不正です.') end rows = PostImportPreviewer.new( parsed:, url_column:, mappings: mappings_params(parsed[:columns].length), existing: params[:existing]).preview render json: parsed.slice(:columns).merge(rows:) rescue ArgumentError => e render_bad_request(e.message) end def validate rows = validate_rows_params changed_row = Integer(params[:changed_row], exception: false) result = PostImportPreviewer.new( parsed: { columns: [], rows: [] }, url_column: 0, mappings: {}, existing: params[:existing]) .preview_rows(rows:, fetch_metadata: changed_row, metadata_cache: {}) render json: { rows: result } rescue ArgumentError => e render_bad_request(e.message) end def create result = PostImportRunner.new( actor: current_user, rows: create_rows_params, existing: params[:existing]).run render json: result, status: result[:created].positive? ? :created : :ok rescue ArgumentError => e render_bad_request(e.message) end private def require_member! return head :unauthorized unless current_user return if current_user.gte_member? head :forbidden end def parsed_params value = params.require(:parsed).to_unsafe_h.deep_transform_keys { _1.to_s.underscore } columns = Array(value['columns']) rows = Array(value['rows']) unless columns.all?(Hash) && rows.all?(Hash) raise ArgumentError, '解析結果の形式が不正です.' end raise ArgumentError, '取込件数が多すぎます.' if rows.length > PostImportSourceParser::MAX_ROWS raise ArgumentError, '列数が多すぎます.' if columns.length > 50 raise ArgumentError, '解析結果のセルが不正です.' unless rows.all? { |row| values = Array(row['values']) values.length <= columns.length && values.all? { |value| value.is_a?(String) && value.bytesize <= PostImportSourceParser::MAX_CELL_BYTES } } if rows.sum { Array(_1['values']).sum(&:bytesize) } > PostImportSourceParser::MAX_BYTES raise ArgumentError, '解析結果が大きすぎます.' end parsed_rows = rows.map do |row| source_row = Integer(row['source_row'], exception: false) raise ArgumentError, '元行番号が不正です.' if source_row.nil? || source_row <= 0 { source_row:, values: Array(row['values']) } end if parsed_rows.map { _1[:source_row] }.uniq.length != parsed_rows.length raise ArgumentError, '元行番号が重複しています.' end { columns: columns.map { |column| column.slice('key', 'label') }, rows: parsed_rows } end def mappings_params column_count mappings = params[:mappings] return { } if mappings.blank? raise ArgumentError, '列割当ての形式が不正です.' unless mappings.is_a?(ActionController::Parameters) raw = mappings.to_unsafe_h unknown_fields = raw.keys - PostImportPreviewer::FIELDS raise ArgumentError, '列割当て先が不正です.' if unknown_fields.present? permitted = mappings.permit(*PostImportPreviewer::FIELDS.map { |field| { field => [:kind, :value, { columns: [] }] } }).to_h raw.each_with_object({ }) do |(field, value), result| raise ArgumentError, '列割当ての形式が不正です.' unless value.is_a?(Hash) unless (value.keys - MAPPING_PROPERTIES).empty? raise ArgumentError, '列割当ての項目が不正です.' end value = permitted.fetch(field) kind = value['kind'].presence || 'columns' raise ArgumentError, '列割当ての種別が不正です.' unless kind.in?(MAPPING_KINDS) columns = value['columns'] raise ArgumentError, '列番号が不正です.' unless ( columns.nil? || (columns.is_a?(Array) && columns.all? { |index| parsed = Integer(index, exception: false) parsed && parsed >= 0 && parsed < column_count }) ) raise ArgumentError, '列番号が重複しています.' if Array(columns).uniq.length != Array(columns).length raise ArgumentError, '固定値が不正です.' if kind == 'fixed' && !value['value'].is_a?(String) result[field] = { 'kind' => kind, 'value' => value['value'].to_s, 'columns' => Array(columns).map { Integer(_1) } } end end def validate_rows_params rows = params[:rows] raise ArgumentError, '取込件数が多すぎます.' unless rows.is_a?(Array) raise ArgumentError, '取込件数が多すぎます.' if rows.length > PostImportSourceParser::MAX_ROWS normalised_rows = rows.map do |row| unless row.is_a?(Hash) || row.is_a?(ActionController::Parameters) raise ArgumentError, '行の形式が不正です.' end value = ActionController::Parameters.new(row).permit( :source_row, :sourceRow, :url, :metadata_url, :metadataUrl, attributes: {}, provenance: {}, tag_sources: {}, tagSources: {}, fetch_warnings: [], fetchWarnings: [], validation_warnings: [], validationWarnings: []) normalised = value.to_h.deep_transform_keys { _1.to_s.underscore } attributes = normalised.fetch('attributes', { }) provenance = normalised.fetch('provenance', { }) allowed = PostImportPreviewer::FIELDS unless attributes.is_a?(Hash) && provenance.is_a?(Hash) && (attributes.keys - allowed).empty? && (provenance.keys - (allowed + ['url'])).empty? raise ArgumentError, '行の形式が不正です.' end unless provenance.values.all? { _1.in?(['automatic', 'mapped', 'fixed', 'manual']) } raise ArgumentError, '値の由来が不正です.' end raise ArgumentError, 'セルが大きすぎます.' if [normalised['url'], *attributes.values].any? { |value| value.to_s.bytesize > PostImportSourceParser::MAX_CELL_BYTES } if normalised['tag_sources'].present? && (!normalised['tag_sources'].is_a?(Hash) || (normalised['tag_sources'].keys - ['automatic', 'mapped', 'fixed', 'manual']).present?) raise ArgumentError, 'タグ由来の形式が不正です.' end if (normalised['tag_sources'].present? && (!normalised['tag_sources'].values.all?(String) || normalised['tag_sources'].values.any? { |value| value.bytesize > PostImportSourceParser::MAX_CELL_BYTES } || (normalised['tag_sources'].values.sum(&:bytesize) > PostImportSourceParser::MAX_CELL_BYTES))) raise ArgumentError, 'タグ由来が大きすぎます.' end source_row = Integer(normalised['source_row'], exception: false) raise ArgumentError, '元行番号が不正です.' if source_row.nil? || source_row <= 0 normalised['source_row'] = source_row normalised.symbolize_keys end source_rows = normalised_rows.map { _1[:source_row] } if source_rows.uniq.length != source_rows.length raise ArgumentError, '元行番号が不正です.' end normalised_rows end def create_rows_params rows = params[:rows] raise ArgumentError, '取込行の形式が不正です.' unless rows.is_a?(Array) rows.map do |row| unless row.is_a?(Hash) || row.is_a?(ActionController::Parameters) raise ArgumentError, '取込行の形式が不正です.' end parameters = row.is_a?(ActionController::Parameters) ? row : ActionController::Parameters.new(row) parameters.permit( :source_row, :sourceRow, :url, :metadata_url, :metadataUrl, attributes: {}, provenance: {}, tag_sources: {}, tagSources: {}).to_h end end end