Rails: documenting post endpoint which consumes json params using swagger-blocks? - ruby-on-rails

Using swagger-blocks in rails, would i document a post endpoint which consumes a single json body such as:
{
"id":"1",
"name": "bill",
"age":"22"
}
No matter what I do, my tests keep saying that my setup is not valid Swagger 2.0 JSON schema.
Below is the code I am using to generate my documentation:
swagger_path '/list/add' do
operation :post do
key :summary, 'Add person to list'
parameter name: :id, in: :body, required: true, type: :string
parameter name: :name, in: :body, required: true, type: :string
parameter name: :age, in: :body, required: true, type: :string
response 200 do
key :description, 'Successfully added to list'
end
end
end

The JSON seems correct syntactically except that "id" should be generated automatically by default if you are trying to create. You may check this specification for violations.

Related

Resolving ruby params.permit syntax error

params.permit(
:type,
payload: [
user: [
:id, :email, :isAnonymous, :isAdmin, :firstName, :createdAt, :updatedAt, :phone, :inviteCode, :employeeId, :activated, :lastName,
:locked, :authVersion, :isCompanyAdmin, :isEditor, :isAnalyst, :isManager, :seenWebOnboarding, :language, :hidden, :deleted, :isSuperAdmin, :hasSignedInAsLearner,
:hasCreatedCourse, :hasInvitedAdmins, :isCompanyOwner, :sendActivationReminders, :timezone, :isUnsubscribedFromEngagementEmails, :authType, :activatedDate, :shouldShowGamificationOnboarding, :depositedPoints, :name,
:identifiers
],
course: [
:id, :customerId, :title, :courseKey, :public, :icon, :createdAt, :updatedAt, :courseImageUrl, :colour, :published, :type,
:requiresEnrolment, :isSample, :completionMessage, :completionButtonUrl, :completionButtonText, :isHidden, :learnersCount
],
parentGroup: [
:id, :customerId, :name, :createdAt, :updatedAt, :parentGroupId, :welcomePageConfigId, :selfSignupConfigId, :language, :deleted,
:ssoConfigId, :cookieConsentDisabled, :usersCount, :activatedUsersCount
],
completion: [
:overallAssessmentScore, :overallLessonScore, :scoresBreakdown, :courseStartDate, :courseCompletionDate,
:courseCompletionDurationInSeconds, :completionLanguage
]
],
:timestamp).to_h
end
This is the code that I'm having an issue with. When I try making a call to this endpoint, I run into this error.
syntax error, unexpected ')', expecting =>
:timestamp).to_h
^ excluded from capture: Not configured to send/capture in environment 'development'
I've noticed if I put the timestamp before the payload in the params.permit than this issue isn't there anymore. However, this is not how the request is going to formatted and I need to follow the structure of the request as it determines a hash later on in the code. Anyone know how to resolve this?
Even though you can do this in a method call, you can't have hash-style elements in a Ruby array. You need to have a formal hash inside of the array itself.
You need to split it out explicitly:
params.permit(
:type,
{
payload: [ ... ]
}
)
From further research, it seems that having a nested attributes must be permitted last. Makes sense as to why this only occurs when timestamp is behind payload.
I'm going to deal with this by just creating a new hash with the params later on with the correct structure.
source: https://smartlogic.io/blog/permitting-nested-arrays-using-strong-params-in-rails/

Converting Relay Node ID to Rails ID when updating related records?

Background:
I have a Location model that has_one Address and has_many Rooms. When I want to update a location, either by updating its name, its address or its rooms, I'm using the following InputObjects to do this:
module Types
# Input interface for creating locations
class LocationUpdateType < Types::BaseInputObject
argument :id, ID, required: false
argument :name, String, required: true
argument :address, AddressUpdateType, required: true, as: :address_attributes
argument :rooms, [RoomUpdateType], required: true, as: :rooms_attributes
end
class AddressUpdateType < Types::BaseInputObject
argument :id, ID, required: true
# not sure if I need this or not but it's commented out for now
# argument :location_id, ID, required: true
argument :street, String, required: true
argument :city, String, required: true
argument :state, String, required: true
argument :zip_code, String, required: true
# I'm not using this yet but I'm anticipating it because
# accepts_nested_attributes can use it as an indicator to destroy this address
argument :_destroy, Boolean, required: false
end
class RoomUpdateType < Types::BaseInputObject
argument :id, ID, required: false
# same thing with this ID.
# argument :location_id, ID, required: true
argument :name, String, required: true
# accepts_nested_attributes flag for destroying related room records.
# like above, I'm not using it yet but I plan to.
argument :_destroy, Boolean, required: false
end
end
When I make a GraphQL request, I'm getting the following in my logs:
Processing by GraphqlController#execute as JSON
Variables: {"input"=>
{"id"=>"TG9jYXRpb24tMzM=",
"location"=>
{"name"=>"A New Building",
"address"=>
{"city"=>"Anytown",
"id"=>"QWRkcmVzcy0zMw==",
"state"=>"CA",
"street"=>"444 New Rd Suite 4",
"zipCode"=>"93400"},
"rooms"=>[{"id"=>"Um9vbS00Mw==", "name"=>"New Room"}]}}}
mutation locationUpdate($input:LocationUpdateInput!) {
locationUpdate(input: $input) {
errors
location {
id
name
address {
city
id
state
street
zipCode
}
rooms {
id
name
}
}
}
}
Which makes sense, I don't want to use real IDs on the client but the obfuscated Relay Node IDs.
Problem:
When my request goes to be resolved I'm using this Mutation:
module Mutations
# Update a Location, its address and rooms.
class LocationUpdate < AdminMutation
null true
description 'Updates a locations for an account'
field :location, Types::LocationType, null: true
field :errors, [String], null: true
argument :id, ID, required: true
argument :location, Types::LocationUpdateType, required: true
def resolve(id:, location:)
begin
l = ApptSchema.object_from_id(id)
rescue StandardError
l = nil
end
return { location: nil, errors: ['Location not found'] } if l.blank?
print location.to_h
# return { location: nil }
# This is throwing an error because it doesn't like the Relay Node IDs.
l.update(location.to_h)
return { location: nil, errors: l.errors.full_messages } unless l.valid?
{ location: l }
end
end
end
When I print the hash that gets passed into this resolver I get the following:
{
:name=>"A New Building",
:address_attributes=>{
:id=>"QWRkcmVzcy0zMw==",
:street=>"444 New Rd Suite 4",
:city=>"Anytown",
:state=>"CA",
:zip_code=>"93400"
},
:rooms_attributes=>[{:id=>"Um9vbS00Mw==", :name=>"New Room"}]
}
When l.update runs, I get the following error:
Couldn't find Room with ID=Um9vbS00Mw== for Location with ID=33
This makes perfect sense to me because the Relay Node IDs aren't stored in the database so I guess I'm trying to figure out how to convert the room.id from a Relay Node ID, to the ID in the database.
Now, I could dig through the hash and use the ApptSchema.object_from_id and convert all the Relay Node IDs to Rails IDs but that requires a database hit for each one. I see the documentation for Connections listed here but this looks more like how to deal with queries and pagination.
Do I need to send the database IDs to the client if I plan on updating records with related records? Doesn't that defeat the purpose of Relay Node IDs? Is there a way to configure my Input object types to convert the Relay Node IDs to Rails IDs so I get the proper IDs in the hash sent to my resolver?
After lots of research, it seems that Relay UUIDs aren't really feasible when trying to update records through relationships. I think Relay assumes that you're using something like MongoDB which does this with their record's primary keys by default.
Using friendly_id as #Nuclearman has suggested seems to be the best way to obfuscate urls. What I've decided to do is to add friendly_id to records that I want to view in a "detail mode" like /posts/3aacmw9mudnoitrswkh9vdrt by creating a concern like this:
module Sluggable
extend ActiveSupport::Concern
included do
validates :slug, presence: true
extend FriendlyId
friendly_id :create_slug_id, use: :slugged
private def create_slug_id
# Try to see if the slug has already been created before generating a new one.
#create_slug_id ||= self.slug
#create_slug_id ||= SecureRandom.alphanumeric(24)
end
end
end
And then including the concern in any model you want to have friendly_ids in like so:
class Location < ApplicationRecord
include Sluggable
end
Then in your GraphQL type do something like this:
module Types
class LocationType < Types::BaseObject
field :id, ID, null: false
# Only include this for model types that have friendly_id
field :friendly_id String, null: false
field :name, String, null: false
# AddressType and RoomType classes won't have friendly_id because
# I'm not going to have a url like /location/3aacmw9mudn/address/oitrswkh9vdrt
# or /location/3aacmw9mudn/rooms/oitrswkh9vdrt
field :address, AddressType, null: false
field :rooms, [RoomType], null: false
end
end
module Types
class LocationUpdateType < Types::BaseInputObject
argument :name, String, required: true
argument :address, AddressUpdateType, required: true, as: :address_attributes
argument :rooms, [RoomUpdateType], required: true, as: :rooms_attributes
end
end
You might want to add friendly_id to all your models but my guess is you may only want it for "important" ones. Variables passed to the query no longer use UUIDs for dependent relationships but only for objects you're updating when finding them by ID like so:
{"input"=>
{"id"=>"3aacmw9mudn",
"location"=>
{"name"=>"A New Building",
"address"=>
{"city"=>"Anytown",
"id"=>"4",
"state"=>"CA",
"street"=>"444 New Rd Suite 4",
"zipCode"=>"93400"},
"rooms"=>[{"id"=>"13", "name"=>"New Room"}]}}}
And now your resolver might look something like:
module Mutations
# Update a Location, its address and rooms.
class LocationUpdate < AdminMutation
null true
description 'Updates a locations for an account'
field :location, Types::LocationType, null: true
field :errors, [String], null: true
argument :id, ID, required: true
argument :location, Types::LocationUpdateType, required: true
def resolve(id:, location:)
# Here, we're passing the friendly_id to this resolver (see above)
# not that actual ID of the record.
l = Location.friendly.find(id)
return { l: nil, errors: ['Location not found'] } if l.blank?
l.update(location.to_h)
return { location: nil, errors: l.errors.full_messages } unless l.valid?
{ location: l }
end
end
end

Rails Strong Parameter unpermited parameter for Rack::Multipart::UploadedFile

I am trying to send on my API endpoint a file. I am validating the inputs with Strong Parameters, so I have:
def postParams
ActionController::Parameters.new(params).permit(:foo, :bar, :cv_file)
end
params do
requires :foo, allow_blank: false, type: String
requires :bar, allow_blank: false, type: String
requires :cv_file, :type => Rack::Multipart::UploadedFile
end
And the result for this is Unpermitted parameter: cv_file.
If I change type for cv_file into String will work but for Rack::Multipart::UploadedFile type is not accepting.
Do I miss something? Maybe a header?
UPDATE
As I see there wa some problems with my postman, so I make the calls with curl now and I am back to 'Unpermitted parameter: cv_file'.
I see that this problem occur because the file is coming like:
<Hashie::Mash filename="CV 2017 June.pdf" head="Content-Disposition: form-data; name=\"cv_file\"; filename=\"CV 2017 June.pdf\"\r\nContent-Type: application/octet-stream\r\n" name="cv_file" tempfile=#<Tempfile:/tmp/RackMultipart20170614-11-1vhzirn.pd‌​f> type="application/octet-stream">
and not a "cv_file"=>#<ActionDispatch::Http::UploadedFile .....
Any idea?
UPDATE 2
I have found a solution but I don't think it's the best way, but you may understand the problem better with this type of solution:
def postParams
local_param = params.dup
upload = ActionDispatch::Http::UploadedFile.new(
tempfile: params[:csv][:tempfile],
filename: params[:csv][:filename],
type: params[:csv][:type],
headers: params[:csv][:head],
)
local_param[:csv] = upload
ActionController::Parameters.new(local_param).permit(:foo, :bar, :cv_file)
end
desc "Create new candidate"
params do
requires :foo, allow_blank: false, type: String
requires :bar, allow_blank: false, type: String
optional :cv_file,:type => File
end
post 'new' do
post_params = postParams
if(post_params[:candidate_cv])
# Process the CV file
post_params.delete :candidate_cv
end
Candidate.create!(post_params)
end

How do you use the ingest-attachment plugin with elasticsearch-rails?

I was previously using the mapper-attachments plugin that is now deprecated, which was fairly easy to use along with normal indexing. Now that ingest-attachment has replaced it and requires a pipeline, etc. it has become confusing on how to properly use this.
Lets say I have a model named Media, that has a file field containing the base64 encoded file. I have the following mappings in that file:
mapping '_source' => { :excludes => ['file'] } do
indexes :id, type: :long, index: :not_analyzed
indexes :name, type: :text
indexes :visibility, type: :integer, index: :not_analyzed
indexes :created_at, type: :date, include_in_all: false
indexes :updated_at, type: :date, include_in_all: false
# attachment specific mappings
indexes 'attachment.title', type: :text, store: 'yes'
indexes 'attachment.author', type: :text, store: 'yes'
indexes 'attachment.name', type: :text, store: 'yes'
indexes 'attachment.date', type: :date, store: 'yes'
indexes 'attachment.content_type', type: :text, store: 'yes'
indexes 'attachment.content_length', type: :integer, store: 'yes'
indexes 'attachment.content', term_vector: 'with_positions_offsets', type: :text, store: 'yes'
end
I have created an attachment pipeline via curl:
curl -XPUT 'localhost:9200/_ingest/pipeline/attachment' -d'
{
"description" : "Extract attachment information",
"processors" : [
{
"attachment" : {
"field" : "file"
}
}
]
}'
Now, previously a simple Media.last.__elasticsearch__.index_document would have been sufficient to index a record along with the actual file via the mapper-attachments plugin.
I'm not sure how to do this with ingest-attachment using a pipeline and the elasticsearch-rails gem.
I can do the following PUT via curl:
curl -XPUT 'localhost:9200/assets/media/68?pipeline=attachment' -d'
{ "file" : "my_really_long_encoded_file_string" }'
This will index the encoded file but obviously it doesn't index the rest of the model's data (or overwrites it completely if it was previously indexed). I don't really want to have to include every single model attribute along with the file in a curl command. Are there better or simpler ways of doing this? Am I just completely off with out pipelines and ingest are supposed to work?
Finally figured this out. I needed up to update the ES gems, specifically elasticsearch-api.
With the mappings and pipeline set as I have it, you can easily just do:
Media.last.__elasticsearch__.index_document pipeline: :attachment
or
Media.last.__elasticsearch__.update_document pipeline: :attachment
This will index everything correctly and your file will be properly parsed and indexed via the ingest pipeline.

How to conditionally require a parameter based on a previous parameter

I am working with apipie for my rails app API and have a parameter that needs to be conditionally required. If a user is hired, they need to have a hired_at date. For other reasons those must be 2 separate columns. I cannot just check for the presence of a hired_at date...
What I currently have is essentially the following:
param :person, Hash, desc: "person information", required: true do
param :name, String, desc: "person name", required: true
param :hired, [true, false], desc: "person has been hired", required: true
param :dates, Array, of: Hash, desc: "important dates", required: true do
param :hired_at, String, desc: "date of fire", required: <if hired == true >
end
end
the <if hired==true> is pseudocode. That is the logic I need there but I don't know how to implement it.

Resources