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

Y24-190: Support Limber with Submission creation #4555

Merged
merged 10 commits into from
Jan 8, 2025
159 changes: 137 additions & 22 deletions app/resources/api/v2/submission_resource.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,53 +2,168 @@

module Api
module V2
# @todo This documentation does not yet include a detailed description of what this resource represents.
# @todo This documentation does not yet include detailed descriptions for relationships, attributes and filters.
# @todo This documentation does not yet include any example usage of the API via cURL or similar.
#
# @note This resource is immutable: its endpoint will not accept `POST`, `PATCH`, or `DELETE` requests.
# Provides a JSON:API representation of {Submission} which represents a collection of {Order}s submitted by a
# {User}. The initial state of a {Submission} is `building`, during which time, {Order}s are added to it.
# If the `and_submit` attribute is set to `true`, the new submission will be transitioned to the `pending` state
# after validation is applied.
#
# @note This resource cannot be modified after creation: its endpoint will not accept `PATCH` requests.
# @note Access this resource via the `/api/v2/submissions/` endpoint.
#
# Provides a JSON:API representation of {Submission}.
# @example POST request
# POST /api/v2/submissions/
# {
# "data": {
# "type": "submissions",
# "relationships": {
# "orders": {
# "data": [
# {
# "type": "orders",
# "id": "123"
# },
# {
# "type": "orders",
# "id": "456"
# }
# ]
# },
# "user": {
# "data": {
# "type": "users",
# "id": "123"
# }
# }
# }
# }
# }
#
# @example GET request for all Submission resources
# GET /api/v2/submissions/
#
# @example GET request for a Submission with ID 123
# GET /api/v2/submissions/123/
#
# For more information about JSON:API see the [JSON:API Specifications](https://jsonapi.org/format/)
# or look at the [JSONAPI::Resources](http://jsonapi-resources.com/) package for Sequencescape's implementation
# of the JSON:API standard.
class SubmissionResource < BaseResource
# Constants...

immutable

# model_name / model_hint if required

default_includes :uuid_object, :sequencing_requests

# Associations:

###
# Attributes
###

# CAUTION:
# See app/controllers/api/v2/submissions_controller.rb
# for field filtering, otherwise newly added attributes
# will not show by default.
attribute :uuid, readonly: true
attribute :name, write_once: true
attribute :state, readonly: true

# @!attribute [r] created_at
# @return [DateTime] the date and time the {Submission} was created as an ISO8601 string.
attribute :created_at, readonly: true

# @!attribute [r] updated_at
# @return [DateTime] the date and time the {Submission} was last updated as an ISO8601 string.
attribute :updated_at, readonly: true
attribute :used_tags, write_once: true

# @!attribute [rw] lanes_of_sequencing
# @return [Integer] the number of lanes of sequencing requested in the Submission.
# This can only be written once on creation.
attribute :lanes_of_sequencing, write_once: true

def lanes_of_sequencing
_model.sequencing_requests.size
end

# @!attribute [rw] name
# @return [String] the name of the {Submission}.
# This can only be written once on creation.
attribute :name, write_once: true

# @!attribute [w] order_uuids
# This is declared for convenience where the {Order} resources are not available to set as a relationship.
# Setting this attribute alongside the `orders` relationship will prefer the relationship value.
yoldas marked this conversation as resolved.
Show resolved Hide resolved
# @deprecated Use the `orders` relationship instead.
# @param value [Array<String>] the UUID of the {Order} resources associated with this {Submission}.
# @return [Void]
# @see #orders
attribute :order_uuids, writeonly: true

def order_uuids=(value)
@model.orders = value.map { |uuid| Order.with_uuid(uuid).first }
end

# @!attribute [r] state
# @return [String] a string version of the state name for this {Submission}.
# This is one of `building`, `pending`, `processing`, `ready`, `failed` or `cancelled`.
attribute :state, readonly: true

# @!attribute [rw] used_tags
# @return [String] the tags that were used in this {Submission}.
# This can only be written once on creation.
attribute :used_tags, write_once: true

# @!attribute [w] user_uuid
# This is declared for convenience where the {User} is not available to set as a relationship.
# Setting this attribute alongside the `user` relationship will prefer the relationship value.
# @deprecated Use the `user` relationship instead.
yoldas marked this conversation as resolved.
Show resolved Hide resolved
# @param value [Array<String>] the UUID of the {User} who created the {Submission}.
# @return [Void]
# @see #user
attribute :user_uuid, writeonly: true

def user_uuid=(value)
@model.user = User.with_uuid(value).first
end

# @!attribute [r] uuid
# @return [String] the UUID of the {Submission}.
attribute :uuid, readonly: true

###
# Relationships
###

# @!attribute [rw] user
# Setting this relationship alongside the `user_uuid` attribute will override the attribute value.
# The {User} who created the {Submission}.
# @return [Api::V2::UserResource]
# @note This relationship is required.
has_one :user, class_name: 'User'

# @!attribute [rw] orders
# Setting this relationship alongside the `orders_uuids` attribute will override the attribute value.
# The {Order} resources which are associated with the {Submission}.
# @return [Array<Api::V2::OrderResource>]
has_many :orders, class_name: 'Order'

###
# Filters
###

# @!method filter_uuid
# Filter the {Submission} resources by UUID.
# @example GET request with UUID filter
# GET /api/v2/submissions?filter[uuid]=12345678-1234-1234-1234-123456789012
filter :uuid, apply: ->(records, value, _options) { records.with_uuid(value) }

# Custom methods
# These shouldn't be used for business logic, and a more about
# I/O and isolating implementation details.
def lanes_of_sequencing
_model.sequencing_requests.size
###
# Post-create state changer
###

after_replace_fields :submit!
yoldas marked this conversation as resolved.
Show resolved Hide resolved

def submit!
return unless @and_submit

@model.built!
end

# Class method overrides
attribute :and_submit, writeonly: true
attr_writer :and_submit # Stored so that the after_replace_fields callback knows whether to submit the submission.
end
end
end
2 changes: 1 addition & 1 deletion config/routes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@
jsonapi_resources :studies
jsonapi_resources :submission_pools
jsonapi_resources :submission_templates
jsonapi_resources :submissions
jsonapi_resources :submissions, except: %i[update]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Curious about that restriction and also state being read only.
Wondering if we need to be able to directly update submission state. Or if's all done indirectly through requests...

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This pull request takes an endpoint that could not previously be created or updated (i.e. it was immutable) and adds a create POST endpoint while still leaving the update PATCH endpoint turned off. If we need to update state, it's not needed so far, but could be enabled carefully in future. immutable in the resource effectively adds , only: %i[show] to the route without explicitly writing it. I think it's a poor choice by the gem creators and they should expect you to just add that annotation to the routes file instead so it's explicit. As it is, you cannot tell from the routes alone which are immutable and which are full access.

jsonapi_resources :tag_group_adapter_types
jsonapi_resources :tag_groups
jsonapi_resources :tag_layout_templates
Expand Down
4 changes: 2 additions & 2 deletions spec/requests/api/v2/shared_examples/requests.rb
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@
end

shared_examples 'a GET request including a has_one relationship' do |related_name|
before { api_get "#{base_endpoint}/#{resource.id}?include=#{related_name}" }
before { api_get "#{base_endpoint}/#{resource.id}?include=#{related_name}&fields[#{resource_type}]=#{related_name}" }
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what's going on with this change (and the one below)?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm yeah that isn't clear from these changes alone is it? The SubmissionsController has a dubious method override in it which modifies the fields being returned by the API to only be the [uuid,name,state,created_at,updated_at] set if you didn't specify. As such, our tests for including relationships fail for ones that aren't in that set. I don't think there should be a default set of fields that you have to override in your request, but it's an existing endpoint and I don't want to break usage somewhere. So instead I make the request in the test explicitly ask for the one relationship to be included in the response (and no other fields for that matter). As that's the only field we want to check for anyway, it's more optimal, but that's not why I implemented it this way as I agree it's ugly and should be unnecessary for any other resource.


it 'responds with a success http code' do
expect(response).to have_http_status(:success)
Expand All @@ -65,7 +65,7 @@
end

shared_examples 'a GET request including a has_many relationship' do |related_name|
before { api_get "#{base_endpoint}/#{resource.id}?include=#{related_name}" }
before { api_get "#{base_endpoint}/#{resource.id}?include=#{related_name}&fields[#{resource_type}]=#{related_name}" }

it 'responds with a success http code' do
expect(response).to have_http_status(:success)
Expand Down
Loading
Loading