Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Store metadata for solidus resources #5951

Open
wants to merge 15 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions api/app/controllers/spree/api/base_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@ class BaseController < ActionController::Base
class_attribute :admin_line_item_attributes
self.admin_line_item_attributes = [:price, :variant_id, :sku]

class_attribute :private_metadata_attributes
self.private_metadata_attributes = [{ private_metadata: {} }]

attr_accessor :current_api_user

before_action :load_user
Expand All @@ -44,6 +47,14 @@ def permitted_line_item_attributes
end
end

def permitted_product_attributes
if can?(:admin, Spree::Product)
super + private_metadata_attributes
else
super
end
end

def load_user
@current_api_user ||= Spree.user_class.find_by(spree_api_key: api_key.to_s)
end
Expand Down
8 changes: 8 additions & 0 deletions api/app/helpers/spree/api/api_helpers.rb
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,14 @@ def variant_attributes
end
end

def product_attributes
if can?(:admin, Spree::Product)
Spree::Api::Config.product_attributes + [:private_metadata]
else
Spree::Api::Config.product_attributes
end
end

def total_on_hand_for(object)
object.total_on_hand.finite? ? object.total_on_hand : nil
end
Expand Down
38 changes: 22 additions & 16 deletions api/lib/spree/api_configuration.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,14 @@ class ApiConfiguration < Preferences::Configuration
preference :product_attributes, :array, default: [
:id, :name, :description, :available_on,
:slug, :meta_description, :meta_keywords, :shipping_category_id,
:taxon_ids, :total_on_hand, :meta_title
:taxon_ids, :total_on_hand, :meta_title, :public_metadata
]

preference :product_property_attributes, :array, default: [:id, :product_id, :property_id, :value, :property_name]

preference :variant_attributes, :array, default: [
:id, :name, :sku, :weight, :height, :width, :depth, :is_master,
:slug, :description, :track_inventory
:slug, :description, :track_inventory, :private_metadata, :public_metadata
]

preference :image_attributes, :array, default: [
Expand All @@ -36,34 +36,36 @@ class ApiConfiguration < Preferences::Configuration
:covered_by_store_credit, :display_total_applicable_store_credit,
:order_total_after_store_credit, :display_order_total_after_store_credit,
:total_applicable_store_credit, :display_total_available_store_credit,
:display_store_credit_remaining_after_capture, :canceler_id
:display_store_credit_remaining_after_capture, :canceler_id, :private_metadata, :public_metadata
]

preference :line_item_attributes, :array, default: [:id, :quantity, :price, :variant_id]
preference :line_item_attributes, :array, default: [:id, :quantity, :price, :variant_id,
:private_metadata, :public_metadata]

preference :option_type_attributes, :array, default: [:id, :name, :presentation, :position]

preference :payment_attributes, :array, default: [
:id, :source_type, :source_id, :amount, :display_amount,
:payment_method_id, :state, :avs_response, :created_at,
:updated_at
:updated_at, :private_metadata, :public_metadata
]

preference :payment_method_attributes, :array, default: [:id, :name, :description]

preference :shipment_attributes, :array, default: [:id, :tracking, :tracking_url, :number, :cost, :shipped_at, :state]
preference :shipment_attributes, :array, default: [:id, :tracking, :tracking_url, :number, :cost, :shipped_at, :state,
:private_metadata, :public_metadata]

preference :taxonomy_attributes, :array, default: [:id, :name]
preference :taxonomy_attributes, :array, default: [:id, :name, :private_metadata, :public_metadata]

preference :taxon_attributes, :array, default: [
:id, :name, :pretty_name, :permalink, :parent_id,
:taxonomy_id
:taxonomy_id, :private_metadata, :public_metadata
]

preference :address_attributes, :array, default: [
:id, :name, :address1, :address2, :city, :zipcode, :phone, :company,
:alternative_phone, :country_id, :country_iso, :state_id, :state_name,
:state_text
:state_text, :private_metadata, :public_metadata
]

preference :country_attributes, :array, default: [:id, :iso_name, :iso, :iso3, :name, :numcode]
Expand All @@ -81,11 +83,13 @@ class ApiConfiguration < Preferences::Configuration
]

preference :customer_return_attributes, :array, default: [
:id, :number, :stock_location_id, :created_at, :updated_at
:id, :number, :stock_location_id, :created_at, :updated_at, :private_metadata,
:public_metadata
]

preference :return_authorization_attributes, :array, default: [
:id, :number, :state, :order_id, :memo, :created_at, :updated_at
:id, :number, :state, :order_id, :memo, :created_at, :updated_at,
:private_metadata, :public_metadata
]

preference :creditcard_attributes, :array, default: [
Expand All @@ -96,16 +100,18 @@ class ApiConfiguration < Preferences::Configuration
:id, :month, :year, :cc_type, :last_digits, :name
]

preference :user_attributes, :array, default: [:id, :email, :created_at, :updated_at]
preference :user_attributes, :array, default: [:id, :email, :created_at, :updated_at,
:private_metadata, :public_metadata]

preference :property_attributes, :array, default: [:id, :name, :presentation]

preference :stock_location_attributes, :array, default: [
:id, :name, :address1, :address2, :city, :state_id, :state_name,
:country_id, :zipcode, :phone, :active
:country_id, :zipcode, :phone, :active, :private_metadata, :public_metadata
]

preference :stock_movement_attributes, :array, default: [:id, :quantity, :stock_item_id]
preference :stock_movement_attributes, :array, default: [:id, :quantity, :stock_item_id,
:private_metadata, :public_metadata]

preference :stock_item_attributes, :array, default: [
:id, :count_on_hand, :backorderable, :stock_location_id,
Expand All @@ -127,12 +133,12 @@ def promotion_attributes=(value)
preference :store_attributes, :array, default: [
:id, :name, :url, :meta_description, :meta_keywords, :seo_title,
:mail_from_address, :default_currency, :code, :default, :available_locales,
:bcc_email
:bcc_email, :private_metadata, :public_metadata
]

preference :store_credit_history_attributes, :array, default: [
:display_amount, :display_user_total_amount, :display_action,
:display_event_date, :display_remaining_amount
:display_event_date, :display_remaining_amount, :private_metadata, :public_metadata
]

preference :variant_property_attributes, :array, default: [
Expand Down
2 changes: 1 addition & 1 deletion api/spec/requests/spree/api/customer_returns_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ module Spree::Api
it "can learn how to create a new customer return" do
get spree.new_api_order_customer_return_path(order)

expect(json_response["attributes"]).to eq(["id", "number", "stock_location_id", "created_at", "updated_at"])
expect(json_response["attributes"]).to eq(["id", "number", "stock_location_id", "created_at", "updated_at", "private_metadata", "public_metadata"])
end

it "can update a customer return" do
Expand Down
2 changes: 1 addition & 1 deletion api/spec/requests/spree/api/line_items_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ module Spree::Api

it "can learn how to create a new line item" do
get spree.new_api_order_line_item_path(order)
expect(json_response["attributes"]).to eq(["quantity", "price", "variant_id"])
expect(json_response["attributes"]).to eq(["quantity", "price", "variant_id", "private_metadata", "public_metadata"])
required_attributes = json_response["required_attributes"]
expect(required_attributes).to include("quantity", "variant_id")
end
Expand Down
2 changes: 1 addition & 1 deletion api/spec/requests/spree/api/payments_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ module Spree::Api
let!(:attributes) {
[:id, :source_type, :source_id, :amount, :display_amount,
:payment_method_id, :state, :avs_response,
:created_at, :updated_at]
:created_at, :updated_at, :private_metadata, :public_metadata]
}

before do
Expand Down
65 changes: 64 additions & 1 deletion api/spec/requests/spree/api/products_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,11 @@ module Spree::Api
end
end

it "cannot see private_metadata" do
get spree.api_product_path(product)
expect(json_response).not_to have_key('private_metadata')
end

it "retrieves a list of products" do
get spree.api_products_path
expect(json_response["products"].first).to have_attributes(show_attributes)
Expand Down Expand Up @@ -206,6 +211,52 @@ module Spree::Api
it_behaves_like "modifying product actions are restricted"
end

context "when the user is not admin but has ability to create and update products" do
# Define custom authorization to grant permissions
custom_authorization! do |_|
can [:create, :update], Spree::Product
end

let(:product_data_with_private_metadata) do
{
name: "The Other Product",
price: 19.99,
shipping_category_id: create(:shipping_category).id,
public_metadata: { 'Company' => 'Sample Company' },
private_metadata: { 'Serial_number' => 'Sn12345' }
}
end

let(:product_update_data_with_private_metadata) do
{
name: "Updated Product",
price: 29.99,
private_metadata: { 'Serial_number' => 'Sn98765' }
}
end

it "allows creating products with public metadata but not private metadata" do
post spree.api_products_path, params: { product: product_data_with_private_metadata }

expect(json_response['public_metadata']).to eq({ "Company" => "Sample Company" })
expect(json_response).not_to have_key('private_metadata')

created_product = Spree::Product.last
expect(created_product.private_metadata).to eq({})
end

it "allows updating products but ignores private metadata" do
product = create(:product)

put spree.api_product_path(product), params: { product: product_update_data_with_private_metadata }

expect(json_response['name']).to eq( "Updated Product" )
expect(json_response).not_to have_key('private_metadata')
product.reload
expect(product.private_metadata).to eq({})
end
end

context "as an admin" do
let(:taxon_1) { create(:taxon) }
let(:taxon_2) { create(:taxon) }
Expand Down Expand Up @@ -242,7 +293,9 @@ module Spree::Api
post spree.api_products_path, params: {
product: { name: "The Other Product",
price: 19.99,
shipping_category_id: create(:shipping_category).id }
shipping_category_id: create(:shipping_category).id,
public_metadata: { 'Company' => 'Sample Company' },
private_metadata: { 'Serial_number' => 'Sn12345' } }
}
expect(json_response).to have_attributes(base_attributes)
expect(response.status).to eq(201)
Expand Down Expand Up @@ -351,6 +404,11 @@ module Spree::Api
expect(response.status).to eq(200)
end

it "can update a product private_metadta" do
put spree.api_product_path(product), params: { product: { private_metadata: { 'product_number' => 'PN345678' } } }
expect(response.status).to eq(200)
end

it "can create new option types on a product" do
put spree.api_product_path(product), params: { product: { option_types: ['shape', 'color'] } }
expect(json_response['option_types'].count).to eq(2)
Expand Down Expand Up @@ -417,6 +475,11 @@ module Spree::Api
expect(response.status).to eq(204)
expect(product.reload.deleted_at).not_to be_nil
end

it "can see private_metadata" do
get spree.api_product_path(product)
expect(json_response).to have_key('private_metadata')
end
end
end
end
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ module Spree::Api

it "can learn how to create a new return authorization" do
get spree.new_api_order_return_authorization_path(order)
expect(json_response["attributes"]).to eq(["id", "number", "state", "order_id", "memo", "created_at", "updated_at"])
expect(json_response["attributes"]).to eq(["id", "number", "state", "order_id", "memo", "created_at", "updated_at", "private_metadata", "public_metadata"])
required_attributes = json_response["required_attributes"]
expect(required_attributes).to include("order")
end
Expand Down
6 changes: 6 additions & 0 deletions api/spec/requests/spree/api/stores_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ module Spree::Api
"mail_from_address" => "[email protected]",
"bcc_email" => nil,
"default_currency" => nil,
"private_metadata"=>{},
"public_metadata"=>{},
"code" => store.code,
"default" => true,
"available_locales" => ["en"]
Expand All @@ -49,6 +51,8 @@ module Spree::Api
"mail_from_address" => "[email protected]",
"bcc_email" => nil,
"default_currency" => nil,
"private_metadata"=>{},
"public_metadata"=>{},
"code" => non_default_store.code,
"default" => false,
"available_locales" => ["en"]
Expand All @@ -67,6 +71,8 @@ module Spree::Api
"seo_title" => nil,
"mail_from_address" => "[email protected]",
"bcc_email" => nil,
"private_metadata"=>{},
"public_metadata"=>{},
"default_currency" => nil,
"code" => store.code,
"default" => true,
Expand Down
2 changes: 1 addition & 1 deletion api/spec/requests/spree/api/taxonomies_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ module Spree::Api
let(:taxonomy) { create(:taxonomy) }
let(:taxon) { create(:taxon, name: "Ruby", taxonomy:) }
let(:taxon2) { create(:taxon, name: "Rails", taxonomy:) }
let(:attributes) { [:id, :name] }
let(:attributes) { [:id, :name, :private_metadata, :public_metadata] }

before do
stub_authentication!
Expand Down
2 changes: 1 addition & 1 deletion api/spec/requests/spree/api/taxons_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ module Spree::Api
let!(:taxon) { create(:taxon, name: "Ruby", parent: taxonomy.root, taxonomy:) }
let!(:taxon2) { create(:taxon, name: "Rails", parent: taxon, taxonomy:) }
let!(:rails_v3_2_2) { create(:taxon, name: "3.2.2", parent: taxon2, taxonomy:) }
let(:attributes) { ["id", "name", "pretty_name", "permalink", "parent_id", "taxonomy_id"] }
let(:attributes) { ["id", "name", "pretty_name", "permalink", "parent_id", "taxonomy_id", "private_metadata", "public_metadata"] }

before do
stub_authentication!
Expand Down
2 changes: 1 addition & 1 deletion api/spec/requests/spree/api/users_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ module Spree::Api
describe 'Users', type: :request do
let(:user) { create(:user, spree_api_key: SecureRandom.hex) }
let(:stranger) { create(:user, email: '[email protected]') }
let(:attributes) { [:id, :email, :created_at, :updated_at] }
let(:attributes) { [:id, :email, :created_at, :updated_at, :private_metadata, :public_metadata] }

context "as a normal user" do
it "can get own details" do
Expand Down
52 changes: 52 additions & 0 deletions core/app/models/concerns/spree/metadata.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
# frozen_string_literal: true

module Spree
module Metadata
extend ActiveSupport::Concern

included do
store :public_metadata, coder: JSON
store :private_metadata, coder: JSON

validate :validate_metadata_limits
end

private

def validate_metadata_limits
%i[public_metadata private_metadata].each { |column| validate_metadata_column(column) }
end

def validate_metadata_column(column)
config = Spree::Config
metadata = send(column)

# Check for maximum number of keys
validate_metadata_keys_count(metadata, column, config.max_keys)

# Check for maximum key and value size
metadata.each do |key, value|
validate_metadata_key(key, column, config.max_key_length)
validate_metadata_value(key, value, column, config.max_value_length)
end
end

def validate_metadata_keys_count(metadata, column, max_keys)
return unless metadata.keys.count > max_keys

errors.add(column, "must not have more than #{max_keys} keys")
end

def validate_metadata_key(key, column, max_key_length)
return unless key.to_s.length > max_key_length

errors.add(column, "key '#{key}' exceeds #{max_key_length} characters")
end

def validate_metadata_value(key, value, column, max_value_length)
return unless value.to_s.length > max_value_length

errors.add(column, "value for key '#{key}' exceeds #{max_value_length} characters")
end
end
end
Loading