Ruby On Rails - DB Structure for saving CRUD operation parameter/payload - ruby-on-rails

What I want
I want to make a form to save "CRUD operation Infomation itself" in my database.
e.g.)
①create new row {"name": "Tom", "price": 200}
②find a row WHERE {"age": 30}
③delete row WHERE {"name": "John"}
④update the row WHERE {"name": "Ted"} SET {"price": 300}
My idea
To realize above, I created two models.
①CrudOperation
t.string :crud_type # this should be one of CRUD ("create", "read", "update", "delete")
t.string :target_database
②CrudOperationParameter
t.integer :crud_operation_id # reference
t.string :key
t.value :value
CrudOperation has many CrudOperationParameter-s.
Problem
My models seem to work well EXCEPT crud_type is "update".
like that
①create new row {"name": "Tom", "price": 200}
**CrudOperation**
id: 1
crud_type: "create"
target_database: "XXXX"
**CrudOperationParameter**
crud_operation_id: 1
key: "name"
value "Tom"
**Another CrudOperationParameter**
crud_operation_id: 1
key: "price"
value "200"
But when it comes to registering the CrudOperation with update type, the problem occurs.
④update the row WHERE {"name": "Ted"} SET {"price": 300}
**CrudOperation**
id: 1
crud_type: "update"
target_database: "XXXX"
**CrudOperationParameter**
crud_operation_id: 1
key: "name"
value "Ted"
**Another CrudOperationParameter**
crud_operation_id: 1
key: "price"
value "300"
Since CrudOperationParameter has only key-value column,
I cannot identify that this CrudOperationParameter is for WHERE clause or SET clause in UPDATE Statement.
Could you teach me better DB schema to save these kinds of data?

What you have is basically a half-baked version of the Entity Attribute Value pattern (or anti-pattern depending on who you are asking).
If I really had to I would set it up as:
class CrudOperation < ApplicationRecord
has_many :crud_operation_parameters
accepts_nested_attributes_for :crud_operation_parameters
enum crud_type: {
create: "create",
read: "read",
update: "update",
delete: "delete"
}
# Convenience setter that maps a hash of attributes
# into a an array of key value attibutes suitible for `accepts_nested_attributes_for`
# and sets the nested attributes
# #return [Array]
def parameters=(hash)
self.crud_operation_parameters_attributes = hash.map do |key, value|
{
key: key,
value: value
}
end
end
end
class CrudOperationParameter < ApplicationRecord
belongs_to :crud_operation
end
# 1. create new row {"name": "Tom", "price": 200}
CrudOperation.create!(
crud_type: :create,
parameters: {
name: "Tom",
price: 200
}
)
# 1. create new row {"name": "Tom", "price": 200}
CrudOperation.create!(
crud_type: :update,
parameters: {
name: "Tom",
price: 200
}
)
I mentioned that its a half baked attempt since you're lacking the Attribute table where you would normalize the definitions of attributes and store stuff like type information. Your solution instead has a string column with tons of duplicates that can become denormalized.
But modern RDBMS systems have column types such as JSON, JSONB and HSTORE which can be used instead of EAV to store data which does not fit a given schema.
Unlike EAV you don't have to store all the attributes in a single column type (usually a string) and typecast or create a bunch of attribute tables to store different types of attributes (such as StringCrudOperationParameter and FloatCrudOperationParameter).
With JSONB on Postgres I would set it up as:
# rails g model crud_operation crud_type:string payload:jsonb conditions:jsonb
class CrudOperation < ApplicationRecord
enum crud_type: {
create: "create",
read: "read",
update: "update",
delete: "delete"
}
end
# 1. create new row {"name": "Tom", "price": 200}
CrudOperation.create!(
crud_type: :create,
payload: {
name: "Tom",
price: 200
}
)
# 2. find a row WHERE {"age": 30}
CrudOperation.create!(
crud_type: :read,
conditions: {
age: 30
}
)
# 3. delete row WHERE {"name": "John"}
CrudOperation.create!(
crud_type: :delete,
conditions: {
name: "John"
}
)
# 4. update the row WHERE {"name": "Ted"} SET {"price": 300}
CrudOperation.create!(
crud_type: :update,
conditions: {
name: "John"
},
payload: {
price: 300
}
)

Related

Rails permit nested attribute

I am working on rails 6 with ruby-2.6.5 and i am working on the API. I am using nested attributes for my order as follows:-
orders_controller.rb
# frozen_string_literal: true
module Api
module V1
class OrdersController < Api::V1::ApiApplicationController
before_action :validate_token
def create
debugger
order = OrderInteractor.create(order_params, #user_id)
if order.save
render json: { 'message' => 'Order Placed' }, status: :ok
else
render_errors(order)
end
end
private
def order_params
params.require(:data)
.require(:attributes)
.require(:order)
.permit(:user_id, :address_id, :total_price, :payment_status,
:order_number, :delivery_time_slot,
order_details_attributes:
%i[price quantity order_detail_status product_id
order_number variant_id],
payment_details_attributes:
%i[payment_data payment_id])
end
end
end
end
Api Request:-
{
"data": {
"attributes": {
"order": {
"address_id": "82",
"delivery_time_slot": "5:00 PM - 8:00 PM(Today)",
"order_details_attributes": [{
"price": "76.0",
"product_id": "46",
"quantity": "4",
"variant_id": "47"
}, {
"price": "9.9",
"product_id": "30",
"quantity": "1",
"variant_id": "29"
}],
"payment_details_attributes": [{
"payment_data": {
"data": {
"nameValuePairs": {
"razorpay_payment_id": "pay_HiHceX2p6450Wa",
"org_logo": "",
"org_name": "Razorpay Software Private Ltd",
"checkout_logo": "https://cdn.razorpay.com/logo.png",
"custom_branding": false
}
},
"paymentId": "pay_HiHceX2p6450Wa",
"userContact": "+916494949494",
"userEmail": "dailyferia#gmail.com"
}
}],
"total_price": "354"
}
},
"type": "orders"
}
}
While placing order i am getting the error Unpermitted parameter: :payment_data but it's working fine for the order_details. Please help me to fix it? I also tried the below ways to fix it but nothing worked:-
payment_details_attributes: %i[:payment_data payment_id]) and `payment_details_attributes: ['payment_data', 'payment_id'])`
Your payment_data is a complex object, rather than the scalars that are found in your order_details_attributes
You will need to add more to the permitted parameters, I believe the simplest solution would be:
payment_details_attributes: [payment_data: {}]
This should accept all parameters under payment_details_attributes, but it would also permit any other keys as well. You may want to be more strict and only allow the parameters specified above, in which case you could do:
payment_details_attributes: [
payment_data: {
data: {
nameValuePairs:
%i[razorpay_payment_id org_logo org_name checkout_logo custom_branding]
},
:paymentId, :userContact, :userEmail
}
]
which should restrict the parameters to just the format used in your example.
A few other notes:
You have %i[payment_data payment_id] in your original sample, but there is no payment_id in your payload. The attribute in the sample is paymentId, and on top of that, it is an attribute of the payment_data, not the payment_details_attributes
you wouldn't use %i and a colon, the %i is a shorthand for creating an array of ruby symbols, so %i[:payment_data payment_id] would create the array [:":payment_data", :payment_id] (note the extra colon at the beginning of payment_data)
Lastly, I haven't tested my code above, so there could be a syntax or other error, but hopefully this points you in the right direction.

Can Angular2 Factory fromJson Handle Embedded Fields

I'm trying to flatten foreign key data into a class from a JSON feed. I added the field to the fromJson factory method and it doesn't error out on the browser console(Dartium). When I display it, the field is blank so it looks like it's not getting through, which isn't a surprise. I can't find any documentation on the web for the method. This is my JSON data:
{
"id": 386,
"artist_id": 57,
"label_id": 5,
"style_id": 61,
"title": "A Flower is a Lovesome Thing",
"catalog": "OJCCD-235",
"alternate_catalog": null,
"recording_date": "1957-04-01",
"notes": null,
"penguin": "**(*)",
"category": "jazz",
"label": {
"label_name": "Fantasy"
}
},
This is the method:
factory Record.fromJson(Map<String, dynamic> record) =>
new Record(_toInt(record['id']),
record['title'],
record['catalog'],
record['artist_id'],
record['label_id'],
record['style_id'],
record['alternate_catalog'],
DateTime.parse(record['recording_date']),
record['notes'],
record['penguin'],
record['category'],
record['label_name']
);
This is the invocation:
HttpRequest response = await HttpRequest.request(
url, requestHeaders: headers);
List data = JSON.decode(response.responseText);
final records = data
.map((value) => new Record.fromJson(value))
.toList();
return records;
I've also tried label:label_name in the from Json method. Is it possible to continue to use fromJson to instantiate the object? Is there documentation anywhere that would explain fromJson? I've found some, but it says almost nothing. I'm also looking into flattening it in the Rails serializer or, as a last resort creating a view in the database. As you may notice, I have two other foreigns keys yet to be handled.
Plan B
Günter's answer fixes the problem on the client side. There's also a Rails solution if any one reading would prefer. It requires Active Model Seriializer. Here the pertinent part:
class RecordSerializer < ActiveModel::Serializer
attributes :id, :artist_id, :label_id, :style_id, :title, :catalog, :alternate_catalog,
:recording_date, :notes, :penguin, :category, :label_name
def label_name
object.label.name
end
end
The instruction object.label.name retrieves the name value from the label table. This is the resulting JSON:
{
"id": 386,
"artist_id": 57,
"label_id": 5,
"style_id": 61,
"title": "A Flower is a Lovesome Thing",
"catalog": "OJCCD-235",
"alternate_catalog": null,
"recording_date": "1957-04-01",
"notes": null,
"penguin": "**(*)",
"category": "jazz",
"label_name": "Fantasy"
},
Not entirely sure I understand the question but I guess this is what you're looking for
record['label']['label_name']

Rails Model Syntax Confusion

I came across this code in Rails app using mongodb:
"""
Folder format:
{
name: <folder name>,
stocks: [
{
name: <stock name>,
id: <stock id>,
qty: <stock quantity>
}
]
}
"""
def format_with_folders(stocks)
fmap = stock_folder_map
res = stocks.group_by {|s| fmap[s["id"]] }.collect {|fname, ss|
{
"name" => fname,
"stocks" => ss
}
}
new(folders: res)
end
def stock_folder_map
res = {}
folders.each { |ff|
ff.stocks.each { |s|
res[s["id"]] = ff["name"]
}
}
return res
end
end
The doubts are:
1) What does the code inside triple quote signify? Is is a commented code?
2)What would be the right format to use this code inside a ruby script?
First of all, the triple quoted string is often used as a comment, and that is the case here.
To get this to work outside of the class, you would need create a folders method that returns an array of folders in the correct structure. You could do something like this:
Folder = Struct.new(:name, :stocks)
def folders
[
Folder.new(
"Folder 1",
[
{ "name" => "stock name", "id" => "stock id", "qty" => 3 },
{ "name" => "stock name", "id" => "stock id", "qty" => 5 }
]
),
Folder.new(
"Folder 2",
[
{ "name" => "stock name", "id" => "stock id", "qty" => 2 },
{ "name" => "stock name", "id" => "stock id", "qty" => 1 }
]
)
]
end
def format_with_folders(stocks)
# ...
end
def stock_folder_map
# ...
end
The folders method returns an array of Folder objects, which both have a name and stocks attribute. Stocks are an array of hashes.
In Ruby, if you have multiple string literals next to each other, they get concatenated at parse time:
'foo' "bar"
# => 'foobar'
This is a feature inspired by C.
So, what you have there is three string literals next to each other. The first string literal is the empty string:
""
Then comes another string literal:
"
Folder format:
{
name: <folder name>,
stocks: [
{
name: <stock name>,
id: <stock id>,
qty: <stock quantity>
}
]
}
"
And lastly, there is a third string literal which is again empty:
""
At parse time, this will be concatenated into a single string literal:
"
Folder format:
{
name: <folder name>,
stocks: [
{
name: <stock name>,
id: <stock id>,
qty: <stock quantity>
}
]
}
"
And since this string object isn't referenced by anything, isn't assigned to any variable, isn't returned from any method or block, it will just get immediately garbage collected.
In other words: the entire thing is a no-op, it's dead code. A sufficiently smart Ruby compiler (such as JRuby or Rubinius) will probably completely eliminate it, compile it into nothing.

Rails to_json - exact order of existing columns

I have a task to form JSON data for jqGrid. It requires a special format:
{
total: 50,
page:"1",
records: "1500",
rows: [
{ 20, "{2ae39c44-ca9d-4565-9e05-bbd875c1579c}", "Description 1"},
{ 23, "{e1aaf69d-1040-4afa-8995-fd15c3a591b3}", "Description 2"},
{ 25, "{e3df29c7-ef34-46ba-bf66-7838aca7c137}", "Description 3"},
{ 29, "{768ec164-28e5-4614-a259-63257b79e8e0}", "Description 4"}
]
}
So the basic rules for "rows" are: do not generate root object name, list fields without their names, list fields in exact order to bind to corresponding columns.
Can I force to_json method to modify output as I need?
Currently the to_json produces:
myobjs : [
myobj : { id: 20, uuid: "{2ae39c44-ca9d-4565-9e05-bbd875c1579c}", name: "Description 1"},
myobj : { id: 20, uuid: "{e1aaf69d-1040-4afa-8995-fd15c3a591b3}", name: "Description 2"},
myobj : { id: 20, uuid: "{e3df29c7-ef34-46ba-bf66-7838aca7c137}", name: "Description 3"},
myobj : { id: 20, uuid: "{768ec164-28e5-4614-a259-63257b79e8e0}", name: "Description 4"}
]
You can't do it with a model-level to_json call, you'll need to build an intermediary data representation as #Paul said. Something like:
class MyObj
def to_json
[id, uuid, name]
end
end
And then in the controller:
class MyController < ApplicationController
def grid_data
objs = MyObj.all
json_data = {
:total => objs.count,
:page => 1,
:records => 1500,
:rows => objs.collect {|o| o.to_json}
}
... send json as usual ...
end
end
Note that I set your model up to generate an array, not a hash as you specified, as I think you copied that wrong - your JSON example above is not valid. { 20, 'foo', 'bar' } is not valid JSON as "{...}" represents a hash, which must be keyed, and is not ordered.

MongoDB Mongoid delete value from array

I have mongodb document like this and want to remove one element form unpublished array
which is in resource_well
{
"_id": ObjectId("4fa77c808d0a6c287d00000a"),
"production_status": "Unscheduled",
"projected_air_date": null,
"published": false,
"resource_well": {
"published": [
],
"unpublished": {
"0": ObjectId("4fa795038d0a6c327e00000e"),
"1": ObjectId("4fa795318d0a6c327e00000f"),
"2": ObjectId("4fa795508d0a6c327e000011"),
"3": ObjectId("4fa796f48d0a6c327e000013")
}
},
"segment_status": "Draft",
}
Code in controller
segment = Segment.find(params[:segment_id])
# segment.resource_well[params['resource_well_type']].class => Array
# segment.resource_well[params['resource_well_type']].inspect => [BSON::ObjectId('4fa795038d0a6c327e00000e'), BSON::ObjectId('4fa795318d0a6c327e00000f'), BSON::ObjectId('4fa795508d0a6c327e000011'), BSON::ObjectId('4fa796f48d0a6c327e000013')]
segment.resource_well[params['resource_well_type']].delete(params[:asset_ids])
segment.save
Not able remove an element form array
Your ODM (or ORM) provides associations for you so that you can take advantage of having the ODM manage the links for you. So you should use the associations, otherwise you risk hanging yourself. Just make sure to specify the relationship correctly, and use the generated method named by the association and its methods, e.g. resource_well.unpublished <<, resource_well.unpublished.delete. The following models and tests work for me. The inconvenience is that the delete method on the association takes an object, e.g., other.delete(object) and not a string or conditions, so if you start with a string, you have to supply an object in order to delete it. Note that other.delete_all(conditions) or other.where(conditions).delete_all remove both actual documents as well as the association, which is not what you were looking for. Anyway, hope that this helps.
Models
class Segment
include Mongoid::Document
field :production_status, type: String
field :projected_air_date, type: Date
field :published, type: Boolean
field :segment_status, type: String
embeds_one :resource_well
end
class ResourceWell
include Mongoid::Document
embedded_in :segment
has_and_belongs_to_many :published, :class_name => 'Resource'
has_and_belongs_to_many :unpublished, :class_name => 'Resource'
end
class Resource
include Mongoid::Document
field :name, type: String
end
Test
require 'test_helper'
class Object
def to_pretty_json
JSON.pretty_generate(JSON.parse(self.to_json))
end
end
class SegmentTest < ActiveSupport::TestCase
def setup
Segment.delete_all
end
test 'resource_well unpublished delete' do
res = (0..3).collect{|i| Resource.create(name: "resource #{i}")}
seg = Segment.create(
production_status: 'Unscheduled',
projected_air_date: nil,
published: false,
resource_well: ResourceWell.new(unpublished: res[0..2]),
segment_status: 'Draft')
seg.resource_well.unpublished << res[3] #just an append example
puts seg.to_pretty_json
id = res[0]['_id'].to_s
puts "id: #{id}"
resource_obj = Resource.find(id)
puts "resource: #{resource_obj.inspect}"
Rails.logger.debug("delete: #{resource_obj.inspect}")
seg.resource_well.unpublished.delete(resource_obj)
puts Segment.find(:all).to_pretty_json
end
end
Result
# Running tests:
{
"_id": "4fa839197f11ba80a9000006",
"production_status": "Unscheduled",
"projected_air_date": null,
"published": false,
"resource_well": {
"_id": "4fa839197f11ba80a9000005",
"published_ids": [
],
"unpublished_ids": [
"4fa839197f11ba80a9000001",
"4fa839197f11ba80a9000002",
"4fa839197f11ba80a9000003",
"4fa839197f11ba80a9000004"
]
},
"segment_status": "Draft"
}
id: 4fa839197f11ba80a9000001
resource: #<Resource _id: 4fa839197f11ba80a9000001, _type: nil, name: "resource 0">
[
{
"_id": "4fa839197f11ba80a9000006",
"production_status": "Unscheduled",
"projected_air_date": null,
"published": false,
"resource_well": {
"_id": "4fa839197f11ba80a9000005",
"published_ids": [
],
"unpublished_ids": [
"4fa839197f11ba80a9000002",
"4fa839197f11ba80a9000003",
"4fa839197f11ba80a9000004"
]
},
"segment_status": "Draft"
}
]
.
Finished tests in 0.016026s, 62.3986 tests/s, 0.0000 assertions/s.
1 tests, 0 assertions, 0 failures, 0 errors, 0 skips
db.collection.update(
{_id : ObjectId("4fa77c808d0a6c287d00000a")},
{ $unset : { "resource_well.unpublished.1" : 1 }}
);
This will remove element [1] of the array.
Also you can read how to implement it using mongoid: http://mongoid.org/docs/upgrading.html

Resources