Class: Spree::Api::V3::Admin::ImportsController

Inherits:
ResourceController show all
Includes:
ActiveStorage::SetCurrent
Defined in:
app/controllers/spree/api/v3/admin/imports_controller.rb

Overview

See docs/plans/5.6-admin-spa-csv-import.md.

There is no standalone imports scope: an import is a bulk write of the records it creates, so every action — including reads, which expose the uploaded data — maps to the write scope of the imported resource (Spree::Imports::Customers => write_customers; see Spree::Import.required_scope). The index is filtered to the types the key can write.

Constant Summary

Constants inherited from BaseController

BaseController::RATE_LIMIT_RESPONSE

Constants included from Idempotent

Idempotent::IDEMPOTENCY_HEADER, Idempotent::IDEMPOTENCY_TTL, Idempotent::MAX_KEY_LENGTH, Idempotent::MUTATING_METHODS

Constants included from ErrorHandler

ErrorHandler::ERROR_CODES

Constants included from JwtAuthentication

JwtAuthentication::JWT_AUDIENCE_ADMIN, JwtAuthentication::JWT_AUDIENCE_STORE, JwtAuthentication::JWT_ISSUER, JwtAuthentication::USER_TYPE_ADMIN, JwtAuthentication::USER_TYPE_CUSTOMER

Instance Method Summary collapse

Methods inherited from ResourceController

#destroy, #index, #show, #update

Methods included from Spree::Api::V3::ApiKeyAuthentication

#authenticate_api_key!, #authenticate_secret_key!

Methods included from JwtAuthentication

#authenticate_user, #require_authentication!

Instance Method Details

#complete_mappingObject

PATCH /api/v3/admin/imports/:id/complete_mapping

Applies the submitted mappings atomically, then transitions out of mapping (which enqueues row creation + processing). 422 when required schema fields remain unmapped or a file column is assigned twice.



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
# File 'app/controllers/spree/api/v3/admin/imports_controller.rb', line 60

def complete_mapping
  @resource = find_resource
  authorize_resource!(@resource, :update)

  unless @resource.mapping?
    return render_error(
      code: Spree::Api::V3::ErrorHandler::ERROR_CODES[:validation_error],
      message: 'Import is not in the mapping state',
      status: :unprocessable_content
    )
  end

  apply_mappings!(@resource)

  if @resource.mapping_done?
    @resource.complete_mapping!
    render json: serialize_resource(@resource)
  else
    missing = @resource.required_fields - @resource.mappings.mapped.pluck(:schema_field)
    render_error(
      code: Spree::Api::V3::ErrorHandler::ERROR_CODES[:validation_error],
      message: "Required fields are not mapped: #{missing.join(', ')}",
      status: :unprocessable_content,
      details: { missing_required_fields: missing }
    )
  end
rescue ActiveRecord::RecordInvalid => e
  render_validation_error(e.record.errors)
end

#createObject

POST /api/v3/admin/imports

attachment is an ActiveStorage signed blob id obtained from POST /api/v3/admin/direct_uploads. On success the import advances straight into mapping (auto-assigning file columns from the CSV headers), so the response already carries the mapping payload.



26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
# File 'app/controllers/spree/api/v3/admin/imports_controller.rb', line 26

def create
  @resource = build_resource
  authorize_resource!(@resource, :create)

  if @resource.save
    begin
      @resource.start_mapping!
    rescue ::CSV::MalformedCSVError, EncodingError => e
      @resource.update_columns(status: 'failed', processing_errors: e.message, updated_at: Time.current)
      return render_error(
        code: Spree::Api::V3::ErrorHandler::ERROR_CODES[:validation_error],
        message: "Could not parse CSV: #{e.message}",
        status: :unprocessable_content
      )
    end

    render json: serialize_resource(@resource), status: :created
  else
    render_errors(@resource.errors)
  end
rescue ActiveSupport::MessageVerifier::InvalidSignature
  render_error(
    code: Spree::Api::V3::ErrorHandler::ERROR_CODES[:validation_error],
    message: 'Invalid attachment signed id',
    status: :unprocessable_content
  )
end

#downloadObject

GET /api/v3/admin/imports/:id/download

Streams the originally uploaded CSV — the audit trail for what was actually imported. Same inline-streaming rationale as ExportsController#download: a signed ActiveStorage URL neither survives the SPA's /api/*-only dev proxy nor carries the JWT.



116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
# File 'app/controllers/spree/api/v3/admin/imports_controller.rb', line 116

def download
  @resource = find_resource
  authorize_resource!(@resource, :show)

  attachment = @resource.attachment
  unless attachment.attached?
    return render_error(
      code: Spree::Api::V3::ErrorHandler::ERROR_CODES[:validation_error],
      message: 'Import has no attached file',
      status: :unprocessable_content
    )
  end

  send_data(
    attachment.download,
    filename: attachment.filename.to_s,
    type: attachment.content_type || 'text/csv',
    disposition: 'attachment'
  )
end

#retry_failed_rowsObject

PATCH /api/v3/admin/imports/:id/retry_failed_rows

Re-dispatches processing over the rows still failed (the dispatcher's pending_and_failed scope picks them up). 422 unless the import is completed with failures.



95
96
97
98
99
100
101
102
103
104
105
106
107
108
# File 'app/controllers/spree/api/v3/admin/imports_controller.rb', line 95

def retry_failed_rows
  @resource = find_resource
  authorize_resource!(@resource, :update)

  if @resource.retry_failed_rows
    render json: serialize_resource(@resource)
  else
    render_error(
      code: Spree::Api::V3::ErrorHandler::ERROR_CODES[:validation_error],
      message: 'Import has no failed rows to retry',
      status: :unprocessable_content
    )
  end
end

#templateObject

GET /api/v3/admin/imports/template?type=Spree::Imports::Products

CSV header row for the type's schema (including the metafield columns available for the model) — the "Download template" link in the admin dashboard.



142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
# File 'app/controllers/spree/api/v3/admin/imports_controller.rb', line 142

def template
  klass = resolve_import_type(params[:type])

  unless klass
    return render_error(
      code: Spree::Api::V3::ErrorHandler::ERROR_CODES[:validation_error],
      message: 'Unknown import type',
      status: :unprocessable_content
    )
  end

  headers = klass.new.schema_fields.map { |field| field[:name] }
  send_data ::CSV.generate_line(headers),
            filename: "#{klass.name.demodulize.underscore}_import_template.csv",
            type: 'text/csv',
            disposition: 'attachment'
end