open_blog 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/CHANGELOG.md +52 -0
- data/LICENSE.txt +21 -0
- data/README.md +101 -0
- data/app/assets/builds/open_blog/blog.css +2 -0
- data/app/assets/javascripts/open_blog/controllers/code_copy_controller.js +40 -0
- data/app/assets/javascripts/open_blog/controllers/reading_progress_controller.js +36 -0
- data/app/assets/javascripts/open_blog/controllers/search_controller.js +49 -0
- data/app/assets/javascripts/open_blog/controllers/share_controller.js +48 -0
- data/app/assets/javascripts/open_blog/controllers/theme_controller.js +51 -0
- data/app/assets/javascripts/open_blog/controllers/toc_controller.js +33 -0
- data/app/controllers/concerns/open_blog/reader_controller.rb +49 -0
- data/app/controllers/open_blog/api/v1/adoptions_controller.rb +27 -0
- data/app/controllers/open_blog/api/v1/approvals_controller.rb +15 -0
- data/app/controllers/open_blog/api/v1/authors_controller.rb +62 -0
- data/app/controllers/open_blog/api/v1/base_controller.rb +100 -0
- data/app/controllers/open_blog/api/v1/categories_controller.rb +33 -0
- data/app/controllers/open_blog/api/v1/connections_controller.rb +45 -0
- data/app/controllers/open_blog/api/v1/doctor_controller.rb +13 -0
- data/app/controllers/open_blog/api/v1/faq_extractions_controller.rb +13 -0
- data/app/controllers/open_blog/api/v1/findings_controller.rb +13 -0
- data/app/controllers/open_blog/api/v1/images_controller.rb +22 -0
- data/app/controllers/open_blog/api/v1/pages_controller.rb +67 -0
- data/app/controllers/open_blog/api/v1/posts_controller.rb +114 -0
- data/app/controllers/open_blog/api/v1/previews_controller.rb +13 -0
- data/app/controllers/open_blog/api/v1/records_controller.rb +13 -0
- data/app/controllers/open_blog/api/v1/redirects_controller.rb +51 -0
- data/app/controllers/open_blog/api/v1/report_controller.rb +20 -0
- data/app/controllers/open_blog/api/v1/resources_controller.rb +39 -0
- data/app/controllers/open_blog/api/v1/series_controller.rb +32 -0
- data/app/controllers/open_blog/api/v1/tags_controller.rb +12 -0
- data/app/controllers/open_blog/api/v1/views_controller.rb +19 -0
- data/app/controllers/open_blog/application_controller.rb +5 -0
- data/app/controllers/open_blog/authors_controller.rb +14 -0
- data/app/controllers/open_blog/categories_controller.rb +15 -0
- data/app/controllers/open_blog/errors_controller.rb +7 -0
- data/app/controllers/open_blog/feeds_controller.rb +22 -0
- data/app/controllers/open_blog/mcp_controller.rb +38 -0
- data/app/controllers/open_blog/media_controller.rb +20 -0
- data/app/controllers/open_blog/pages_controller.rb +12 -0
- data/app/controllers/open_blog/posts_controller.rb +57 -0
- data/app/controllers/open_blog/previews_controller.rb +35 -0
- data/app/controllers/open_blog/search_controller.rb +32 -0
- data/app/controllers/open_blog/series_controller.rb +15 -0
- data/app/controllers/open_blog/sitemaps_controller.rb +10 -0
- data/app/controllers/open_blog/tags_controller.rb +15 -0
- data/app/helpers/open_blog/content_helper.rb +79 -0
- data/app/helpers/open_blog/dates_helper.rb +18 -0
- data/app/helpers/open_blog/faq_helper.rb +59 -0
- data/app/helpers/open_blog/head_helper.rb +75 -0
- data/app/helpers/open_blog/icons_helper.rb +10 -0
- data/app/helpers/open_blog/notices_helper.rb +47 -0
- data/app/helpers/open_blog/pagination_helper.rb +18 -0
- data/app/helpers/open_blog/structured_data_helper.rb +63 -0
- data/app/helpers/open_blog/theme_helper.rb +52 -0
- data/app/helpers/open_blog/url_helper.rb +58 -0
- data/app/jobs/open_blog/publish_scheduled_post_job.rb +14 -0
- data/app/models/concerns/open_blog/immutable.rb +13 -0
- data/app/models/concerns/open_blog/immutable_image_attachment.rb +43 -0
- data/app/models/concerns/open_blog/previewable.rb +38 -0
- data/app/models/open_blog/api_token.rb +34 -0
- data/app/models/open_blog/application_record.rb +5 -0
- data/app/models/open_blog/approval.rb +19 -0
- data/app/models/open_blog/author.rb +11 -0
- data/app/models/open_blog/baseline.rb +26 -0
- data/app/models/open_blog/category.rb +10 -0
- data/app/models/open_blog/connection_declaration.rb +16 -0
- data/app/models/open_blog/faq.rb +22 -0
- data/app/models/open_blog/image.rb +45 -0
- data/app/models/open_blog/page.rb +23 -0
- data/app/models/open_blog/page_view.rb +7 -0
- data/app/models/open_blog/post.rb +132 -0
- data/app/models/open_blog/publication.rb +17 -0
- data/app/models/open_blog/redirect.rb +16 -0
- data/app/models/open_blog/revision.rb +12 -0
- data/app/models/open_blog/series.rb +10 -0
- data/app/models/open_blog/tag.rb +11 -0
- data/app/models/open_blog/tagging.rb +7 -0
- data/app/queries/open_blog/api_post_query.rb +54 -0
- data/app/serializers/open_blog/adoption_result_serializer.rb +9 -0
- data/app/serializers/open_blog/author_serializer.rb +10 -0
- data/app/serializers/open_blog/category_serializer.rb +7 -0
- data/app/serializers/open_blog/doctor_serializer.rb +7 -0
- data/app/serializers/open_blog/error_serializer.rb +7 -0
- data/app/serializers/open_blog/extraction_result_serializer.rb +7 -0
- data/app/serializers/open_blog/findings_serializer.rb +13 -0
- data/app/serializers/open_blog/image_serializer.rb +9 -0
- data/app/serializers/open_blog/page_serializer.rb +9 -0
- data/app/serializers/open_blog/post_serializer.rb +59 -0
- data/app/serializers/open_blog/preview_serializer.rb +11 -0
- data/app/serializers/open_blog/records_serializer.rb +28 -0
- data/app/serializers/open_blog/redirect_serializer.rb +7 -0
- data/app/serializers/open_blog/series_serializer.rb +7 -0
- data/app/serializers/open_blog/tag_serializer.rb +7 -0
- data/app/serializers/open_blog/write_result_serializer.rb +9 -0
- data/app/views/open_blog/attachments/_blob.html.erb +1 -0
- data/app/views/open_blog/feeds/atom.xml.builder +27 -0
- data/app/views/open_blog/sitemaps/show.xml.builder +9 -0
- data/config/locales/open_blog.en.yml +95 -0
- data/config/routes.rb +52 -0
- data/db/migrate/20261003012512_create_open_blog_authors.rb +16 -0
- data/db/migrate/20261003012513_create_open_blog_categories.rb +13 -0
- data/db/migrate/20261003012514_create_open_blog_series.rb +11 -0
- data/db/migrate/20261003012515_create_open_blog_images.rb +17 -0
- data/db/migrate/20261003012516_create_open_blog_posts.rb +47 -0
- data/db/migrate/20261003012517_create_open_blog_faqs.rb +13 -0
- data/db/migrate/20261003012518_create_open_blog_tags.rb +17 -0
- data/db/migrate/20261003012519_create_open_blog_redirects.rb +14 -0
- data/db/migrate/20261003012520_create_open_blog_revisions.rb +14 -0
- data/db/migrate/20261003012521_create_open_blog_approvals.rb +21 -0
- data/db/migrate/20261003012522_create_open_blog_publications.rb +18 -0
- data/db/migrate/20261003012523_create_open_blog_baselines.rb +24 -0
- data/db/migrate/20261003012524_create_open_blog_connection_declarations.rb +13 -0
- data/db/migrate/20261003031410_add_body_image_manifest_to_open_blog_posts.rb +7 -0
- data/db/migrate/20261003164418_create_open_blog_api_tokens.rb +15 -0
- data/db/migrate/20261003175454_create_open_blog_pages.rb +16 -0
- data/db/migrate/20261003182919_create_open_blog_page_views.rb +11 -0
- data/lib/generators/open_blog/admin_suite/admin_suite_generator.rb +27 -0
- data/lib/generators/open_blog/admin_suite/templates/author_resource.rb.tt +29 -0
- data/lib/generators/open_blog/admin_suite/templates/blog_portal.rb.tt +16 -0
- data/lib/generators/open_blog/admin_suite/templates/category_resource.rb.tt +28 -0
- data/lib/generators/open_blog/admin_suite/templates/initializer.rb.tt +19 -0
- data/lib/generators/open_blog/admin_suite/templates/page_resource.rb.tt +32 -0
- data/lib/generators/open_blog/admin_suite/templates/post_resource.rb.tt +58 -0
- data/lib/generators/open_blog/generator_support.rb +64 -0
- data/lib/generators/open_blog/install/install_generator.rb +188 -0
- data/lib/generators/open_blog/install/templates/initializer.rb.tt +16 -0
- data/lib/generators/open_blog/install/templates/sample_cover.png +0 -0
- data/lib/generators/open_blog/install/templates/sample_post.md +54 -0
- data/lib/generators/open_blog/install/templates/theme/blog.css +193 -0
- data/lib/generators/open_blog/install/templates/theme/open_blog_theme.css +82 -0
- data/lib/generators/open_blog/install/templates/theme/syntax.css +358 -0
- data/lib/generators/open_blog/install/templates/theme/theme.css +120 -0
- data/lib/generators/open_blog/install/templates/views/layouts/open_blog.html.erb +16 -0
- data/lib/generators/open_blog/install/templates/views/open_blog/authors/show.html.erb +17 -0
- data/lib/generators/open_blog/install/templates/views/open_blog/categories/show.html.erb +13 -0
- data/lib/generators/open_blog/install/templates/views/open_blog/errors/not_found.html.erb +11 -0
- data/lib/generators/open_blog/install/templates/views/open_blog/errors/preview_expired.html.erb +5 -0
- data/lib/generators/open_blog/install/templates/views/open_blog/icons/_check.svg.erb +1 -0
- data/lib/generators/open_blog/install/templates/views/open_blog/icons/_copy.svg.erb +1 -0
- data/lib/generators/open_blog/install/templates/views/open_blog/icons/_link.svg.erb +1 -0
- data/lib/generators/open_blog/install/templates/views/open_blog/icons/_monitor.svg.erb +1 -0
- data/lib/generators/open_blog/install/templates/views/open_blog/icons/_moon.svg.erb +1 -0
- data/lib/generators/open_blog/install/templates/views/open_blog/icons/_share.svg.erb +1 -0
- data/lib/generators/open_blog/install/templates/views/open_blog/icons/_sun.svg.erb +1 -0
- data/lib/generators/open_blog/install/templates/views/open_blog/pages/show.html.erb +5 -0
- data/lib/generators/open_blog/install/templates/views/open_blog/posts/_faq.html.erb +1 -0
- data/lib/generators/open_blog/install/templates/views/open_blog/posts/_highlights.html.erb +7 -0
- data/lib/generators/open_blog/install/templates/views/open_blog/posts/_preview_bar.html.erb +1 -0
- data/lib/generators/open_blog/install/templates/views/open_blog/posts/_series_nav.html.erb +12 -0
- data/lib/generators/open_blog/install/templates/views/open_blog/posts/_tags.html.erb +5 -0
- data/lib/generators/open_blog/install/templates/views/open_blog/posts/_toc.html.erb +1 -0
- data/lib/generators/open_blog/install/templates/views/open_blog/posts/index.html.erb +13 -0
- data/lib/generators/open_blog/install/templates/views/open_blog/posts/show.html.erb +29 -0
- data/lib/generators/open_blog/install/templates/views/open_blog/search/show.html.erb +13 -0
- data/lib/generators/open_blog/install/templates/views/open_blog/series/show.html.erb +13 -0
- data/lib/generators/open_blog/install/templates/views/open_blog/shared/_author_box.html.erb +6 -0
- data/lib/generators/open_blog/install/templates/views/open_blog/shared/_breadcrumbs.html.erb +1 -0
- data/lib/generators/open_blog/install/templates/views/open_blog/shared/_call_to_action.html.erb +7 -0
- data/lib/generators/open_blog/install/templates/views/open_blog/shared/_featured_card.html.erb +11 -0
- data/lib/generators/open_blog/install/templates/views/open_blog/shared/_footer.html.erb +11 -0
- data/lib/generators/open_blog/install/templates/views/open_blog/shared/_header.html.erb +7 -0
- data/lib/generators/open_blog/install/templates/views/open_blog/shared/_pagination.html.erb +1 -0
- data/lib/generators/open_blog/install/templates/views/open_blog/shared/_post_card.html.erb +9 -0
- data/lib/generators/open_blog/install/templates/views/open_blog/shared/_related_posts.html.erb +10 -0
- data/lib/generators/open_blog/install/templates/views/open_blog/shared/_search.html.erb +9 -0
- data/lib/generators/open_blog/install/templates/views/open_blog/shared/_share.html.erb +13 -0
- data/lib/generators/open_blog/install/templates/views/open_blog/shared/_sidebar.html.erb +38 -0
- data/lib/generators/open_blog/install/templates/views/open_blog/shared/_theme_toggle.html.erb +10 -0
- data/lib/generators/open_blog/install/templates/views/open_blog/tags/show.html.erb +13 -0
- data/lib/generators/open_blog/views/views_generator.rb +21 -0
- data/lib/open_blog/actor.rb +7 -0
- data/lib/open_blog/admin_suite.rb +73 -0
- data/lib/open_blog/adopt/contract.rb +152 -0
- data/lib/open_blog/adopt/snapshot.rb +35 -0
- data/lib/open_blog/adopt.rb +159 -0
- data/lib/open_blog/api_fields.rb +40 -0
- data/lib/open_blog/approve.rb +18 -0
- data/lib/open_blog/authentication.rb +18 -0
- data/lib/open_blog/build_css.rb +53 -0
- data/lib/open_blog/configuration.rb +185 -0
- data/lib/open_blog/configuration_error.rb +4 -0
- data/lib/open_blog/content_guard.rb +77 -0
- data/lib/open_blog/doctor/files.rb +61 -0
- data/lib/open_blog/doctor/javascript.rb +57 -0
- data/lib/open_blog/doctor.rb +177 -0
- data/lib/open_blog/engine.rb +45 -0
- data/lib/open_blog/errors.rb +40 -0
- data/lib/open_blog/faq_extraction.rb +184 -0
- data/lib/open_blog/findings/body_checks.rb +58 -0
- data/lib/open_blog/findings/check.rb +19 -0
- data/lib/open_blog/findings/link_check.rb +78 -0
- data/lib/open_blog/findings/record_checks.rb +94 -0
- data/lib/open_blog/findings/registry.rb +18 -0
- data/lib/open_blog/findings.rb +27 -0
- data/lib/open_blog/image_fetch.rb +115 -0
- data/lib/open_blog/image_import.rb +164 -0
- data/lib/open_blog/image_resolution.rb +171 -0
- data/lib/open_blog/instructions.rb +7 -0
- data/lib/open_blog/json_feed.rb +36 -0
- data/lib/open_blog/label_policy.rb +41 -0
- data/lib/open_blog/markdown_view.rb +57 -0
- data/lib/open_blog/mcp/definition.rb +28 -0
- data/lib/open_blog/mcp/dispatcher.rb +33 -0
- data/lib/open_blog/mcp/image_input.rb +30 -0
- data/lib/open_blog/mcp/registry.rb +139 -0
- data/lib/open_blog/mcp/schemas.rb +72 -0
- data/lib/open_blog/mcp.rb +24 -0
- data/lib/open_blog/not_found.rb +4 -0
- data/lib/open_blog/operation.rb +36 -0
- data/lib/open_blog/page_views.rb +72 -0
- data/lib/open_blog/pagination.rb +18 -0
- data/lib/open_blog/plain_text.rb +36 -0
- data/lib/open_blog/policy_url.rb +9 -0
- data/lib/open_blog/post_attributes.rb +172 -0
- data/lib/open_blog/post_identity.rb +28 -0
- data/lib/open_blog/publish.rb +7 -0
- data/lib/open_blog/reader_dates.rb +21 -0
- data/lib/open_blog/reader_page.rb +21 -0
- data/lib/open_blog/reader_queries.rb +90 -0
- data/lib/open_blog/record_release.rb +66 -0
- data/lib/open_blog/redirect_target.rb +58 -0
- data/lib/open_blog/remove.rb +43 -0
- data/lib/open_blog/renderer/highlighter.rb +44 -0
- data/lib/open_blog/renderer/markdown.rb +14 -0
- data/lib/open_blog/renderer/rich_text.rb +19 -0
- data/lib/open_blog/renderer/sanitizer.rb +72 -0
- data/lib/open_blog/renderer/scrubber.rb +35 -0
- data/lib/open_blog/renderer/transform.rb +115 -0
- data/lib/open_blog/renderer.rb +51 -0
- data/lib/open_blog/result.rb +11 -0
- data/lib/open_blog/revision_payload.rb +67 -0
- data/lib/open_blog/rich_text_guard.rb +26 -0
- data/lib/open_blog/sample.rb +56 -0
- data/lib/open_blog/save_draft.rb +7 -0
- data/lib/open_blog/search.rb +21 -0
- data/lib/open_blog/sitemap_entries.rb +66 -0
- data/lib/open_blog/surface_report/expected_content.rb +27 -0
- data/lib/open_blog/surface_report/http.rb +92 -0
- data/lib/open_blog/surface_report/page_checks.rb +303 -0
- data/lib/open_blog/surface_report/reach_checks.rb +141 -0
- data/lib/open_blog/surface_report/record_checks.rb +248 -0
- data/lib/open_blog/surface_report/robots.rb +49 -0
- data/lib/open_blog/surface_report.rb +223 -0
- data/lib/open_blog/syntax_css.rb +55 -0
- data/lib/open_blog/unpublish.rb +13 -0
- data/lib/open_blog/version.rb +3 -0
- data/lib/open_blog/write_post.rb +205 -0
- data/lib/open_blog.rb +91 -0
- data/lib/tasks/open_blog.rake +71 -0
- data/skills/open-blog-adopt/SKILL.md +12 -0
- data/skills/open-blog-install/SKILL.md +10 -0
- data/skills/open-blog-policy-pages/SKILL.md +10 -0
- data/skills/open-blog-publish/SKILL.md +14 -0
- data/skills/open-blog-report/SKILL.md +10 -0
- data/skills/open-blog-update/SKILL.md +16 -0
- metadata +515 -0
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
module OpenBlog
|
|
2
|
+
module Api
|
|
3
|
+
module V1
|
|
4
|
+
class AuthorsController < ResourcesController
|
|
5
|
+
def index
|
|
6
|
+
require_scope!(:read)
|
|
7
|
+
list(Author.all, :authors, AuthorSerializer)
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def create
|
|
11
|
+
require_scope!(:write)
|
|
12
|
+
save_author(Author.new, status: :created)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def update
|
|
16
|
+
require_scope!(:write)
|
|
17
|
+
save_author(Author.find(params[:id]), status: :ok)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
private
|
|
21
|
+
|
|
22
|
+
def save_author(record, status:)
|
|
23
|
+
input = input_fields!(*ApiFields::AUTHOR)
|
|
24
|
+
text_fields!(input, :name, :slug, :type, :bio, :url, :host_reference)
|
|
25
|
+
if input.key?(:profile_urls)
|
|
26
|
+
invalid!(:profile_urls) unless input[:profile_urls].is_a?(Array) && input[:profile_urls].all? { |url| url.is_a?(String) }
|
|
27
|
+
end
|
|
28
|
+
avatar_sent = input.key?(:avatar)
|
|
29
|
+
avatar = input.delete(:avatar)
|
|
30
|
+
input[:author_type] = input.delete(:type) if input.key?(:type)
|
|
31
|
+
record.assign_attributes(input)
|
|
32
|
+
raise ActiveRecord::RecordInvalid, record unless record.valid?
|
|
33
|
+
prepared, image = prepare_avatar(avatar) if avatar_sent && avatar
|
|
34
|
+
Author.transaction do
|
|
35
|
+
record.reload.lock! if record.persisted?
|
|
36
|
+
record.assign_attributes(input)
|
|
37
|
+
image = prepared.persist(uploaded_by: actor.name) if prepared
|
|
38
|
+
if avatar_sent
|
|
39
|
+
invalid!(:avatar) if image && !image.file.attached?
|
|
40
|
+
record.avatar = image&.file&.blob
|
|
41
|
+
end
|
|
42
|
+
record.save!
|
|
43
|
+
end
|
|
44
|
+
render json: AuthorSerializer.call(record), status: status
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def prepare_avatar(input)
|
|
48
|
+
invalid!(:avatar) unless input.is_a?(Hash)
|
|
49
|
+
unknown = input.keys - %i[image_id signed_id url]
|
|
50
|
+
raise Error::UnknownField.new(details: unknown.map { |field| "avatar.#{field}" }) if unknown.any?
|
|
51
|
+
invalid!(:avatar) unless input.size == 1
|
|
52
|
+
if input.key?(:image_id)
|
|
53
|
+
invalid!(:avatar) unless input[:image_id].is_a?(Integer)
|
|
54
|
+
[ nil, Image.find(input[:image_id]) ]
|
|
55
|
+
else
|
|
56
|
+
[ ImageImport.prepare(input), nil ]
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
module OpenBlog
|
|
2
|
+
module Api
|
|
3
|
+
module V1
|
|
4
|
+
class BaseController < ActionController::API
|
|
5
|
+
include ActionController::HttpAuthentication::Token::ControllerMethods
|
|
6
|
+
before_action :prevent_caching!
|
|
7
|
+
before_action :authenticate!
|
|
8
|
+
before_action :limit_requests!
|
|
9
|
+
wrap_parameters false
|
|
10
|
+
|
|
11
|
+
rescue_from OpenBlog::Error, with: :render_error
|
|
12
|
+
rescue_from ActiveRecord::RecordNotFound do
|
|
13
|
+
render_error(Error::NotFound.new)
|
|
14
|
+
end
|
|
15
|
+
rescue_from ActionDispatch::Http::Parameters::ParseError, ActionController::BadRequest do
|
|
16
|
+
render_error(Error::ValidationFailed.new(details: [ "body" ]))
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# Rails 8.0 has no limiter scope keyword. Only the exact current API
|
|
20
|
+
# controller prefix is replaced; all other cache keys pass through.
|
|
21
|
+
class SharedLimitStore
|
|
22
|
+
def initialize(store, controller_path)
|
|
23
|
+
@store = store
|
|
24
|
+
@prefix = "rate-limit:#{controller_path}:"
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def increment(key, amount, **options)
|
|
28
|
+
normalized = key.start_with?(@prefix) ? "rate-limit:open_blog/api:#{key.delete_prefix(@prefix)}" : key
|
|
29
|
+
@store.increment(normalized, amount, **options)
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Used by trusted Ruby callers on a fresh controller instance. This state
|
|
34
|
+
# is never read from HTTP parameters, headers or a caller-provided Rack env.
|
|
35
|
+
def dispatch_internal(action, request, response, actor:)
|
|
36
|
+
raise Error::Unauthenticated unless actor
|
|
37
|
+
@internal_actor = actor
|
|
38
|
+
dispatch(action, request, response)
|
|
39
|
+
ensure
|
|
40
|
+
@internal_actor = nil
|
|
41
|
+
end
|
|
42
|
+
private :dispatch_internal
|
|
43
|
+
|
|
44
|
+
private
|
|
45
|
+
|
|
46
|
+
attr_reader :actor
|
|
47
|
+
|
|
48
|
+
def prevent_caching!
|
|
49
|
+
response.headers["Cache-Control"] = "no-store"
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def authenticate!
|
|
53
|
+
@actor = @internal_actor || Authentication.actor_for(request)
|
|
54
|
+
raise Error::Unauthenticated unless actor
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def require_scope!(scope)
|
|
58
|
+
raise Error::ScopeRequired.new(details: [ scope.to_s ]) unless actor.scopes.include?(scope.to_s)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def limit_requests!
|
|
62
|
+
return if @internal_actor
|
|
63
|
+
options = { to: OpenBlog.config.api_rate_limit.fetch(:to), within: OpenBlog.config.api_rate_limit.fetch(:within),
|
|
64
|
+
by: -> { actor.id }, with: -> { raise Error::RateLimited }, store: OpenBlog.config.rate_limit_store, name: nil }
|
|
65
|
+
if method(:rate_limiting).parameters.any? { |kind, name| kind == :keyreq && name == :scope }
|
|
66
|
+
options[:scope] = "open_blog/api"
|
|
67
|
+
else
|
|
68
|
+
options[:store] = SharedLimitStore.new(options[:store], controller_path)
|
|
69
|
+
end
|
|
70
|
+
rate_limiting(**options)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def input_fields!(*allowed)
|
|
74
|
+
input = request.query_parameters.merge(request.request_parameters)
|
|
75
|
+
unknown = input.keys.map(&:to_s) - allowed.flatten.map(&:to_s)
|
|
76
|
+
raise Error::UnknownField.new(details: unknown) if unknown.any?
|
|
77
|
+
input.deep_symbolize_keys
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def find_post!
|
|
81
|
+
identity = (params[:post_id] || params[:id]).to_s
|
|
82
|
+
post = Post.find_by(id: identity) if identity.match?(/\A\d+\z/)
|
|
83
|
+
post ||= Post.find_by(slug: identity)
|
|
84
|
+
post || raise(Error::NotFound)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def render_result(result, status: nil)
|
|
88
|
+
return render_error(result.error) unless result.success?
|
|
89
|
+
status ||= result.post&.scheduled? ? :accepted : (result.created ? :created : :ok)
|
|
90
|
+
render json: WriteResultSerializer.call(result, base_url: request.base_url), status: status
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def render_error(error)
|
|
94
|
+
prevent_caching!
|
|
95
|
+
render json: ErrorSerializer.call(error), status: error.status
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
module OpenBlog
|
|
2
|
+
module Api
|
|
3
|
+
module V1
|
|
4
|
+
class CategoriesController < ResourcesController
|
|
5
|
+
def index
|
|
6
|
+
require_scope!(:read)
|
|
7
|
+
list(Category.all, :categories, CategorySerializer)
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def create
|
|
11
|
+
require_scope!(:write)
|
|
12
|
+
render json: CategorySerializer.call(Category.create!(attributes)), status: :created
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def update
|
|
16
|
+
require_scope!(:write)
|
|
17
|
+
record = Category.find(params[:id])
|
|
18
|
+
record.update!(attributes)
|
|
19
|
+
render json: CategorySerializer.call(record)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
private
|
|
23
|
+
|
|
24
|
+
def attributes
|
|
25
|
+
input = input_fields!(*ApiFields::CATEGORY)
|
|
26
|
+
text_fields!(input, :name, :slug, :description)
|
|
27
|
+
invalid!(:position) if input.key?(:position) && (!input[:position].is_a?(Integer) || !(-2_147_483_648..2_147_483_647).cover?(input[:position]))
|
|
28
|
+
input
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
module OpenBlog
|
|
2
|
+
module Api
|
|
3
|
+
module V1
|
|
4
|
+
class ConnectionsController < BaseController
|
|
5
|
+
def create
|
|
6
|
+
require_scope!(:publish)
|
|
7
|
+
input = input_fields!(*ApiFields::CONNECTION)
|
|
8
|
+
validate_input!(input)
|
|
9
|
+
now = Time.current
|
|
10
|
+
result = Operation.run(post: find_post!, actor: actor.name, now: now) do |post, _created|
|
|
11
|
+
post.connection_declarations.create!(input.merge(declared_on: input.fetch(:declared_on, now.to_date), recorded_by: actor.name))
|
|
12
|
+
{ revision: nil, publication: nil, approval: nil }
|
|
13
|
+
end
|
|
14
|
+
render_result(result, status: :ok)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
private
|
|
18
|
+
|
|
19
|
+
def validate_input!(input)
|
|
20
|
+
invalid!(:connections) unless input[:connections].is_a?(Array)
|
|
21
|
+
input[:connections].each do |entry|
|
|
22
|
+
invalid!(:connections) unless entry.is_a?(Hash)
|
|
23
|
+
unknown = entry.keys - %i[party relation]
|
|
24
|
+
raise Error::UnknownField.new(details: unknown.map { |key| "connections.#{key}" }) if unknown.any?
|
|
25
|
+
invalid!(:connections) unless %i[party relation].all? { |key| entry[key].is_a?(String) && entry[key].present? }
|
|
26
|
+
end
|
|
27
|
+
invalid!(:third_party_paid) unless [ true, false ].include?(input[:third_party_paid])
|
|
28
|
+
invalid!(:declared_by) unless input[:declared_by].is_a?(String) && input[:declared_by].present?
|
|
29
|
+
if input.key?(:declared_on)
|
|
30
|
+
invalid!(:declared_on) unless input[:declared_on].is_a?(String) && input[:declared_on].match?(/\A\d{4}-\d{2}-\d{2}\z/)
|
|
31
|
+
begin
|
|
32
|
+
input[:declared_on] = Date.iso8601(input[:declared_on])
|
|
33
|
+
rescue Date::Error
|
|
34
|
+
invalid!(:declared_on)
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def invalid!(field)
|
|
40
|
+
raise Error::ValidationFailed.new(details: [ field.to_s ])
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
module OpenBlog
|
|
2
|
+
module Api
|
|
3
|
+
module V1
|
|
4
|
+
class FaqExtractionsController < BaseController
|
|
5
|
+
def create
|
|
6
|
+
require_scope!(:read)
|
|
7
|
+
input = input_fields!(*ApiFields::EXTRACTION)
|
|
8
|
+
render json: ExtractionResultSerializer.call(FaqExtraction.call(body: input[:body], standalone_questions: input.fetch(:standalone_questions, [])))
|
|
9
|
+
end
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
module OpenBlog
|
|
2
|
+
module Api
|
|
3
|
+
module V1
|
|
4
|
+
class ImagesController < BaseController
|
|
5
|
+
def create
|
|
6
|
+
require_scope!(:write)
|
|
7
|
+
input = input_fields!(*ApiFields::IMAGE)
|
|
8
|
+
raise Error::ImageNotPermitted unless input.size == 1
|
|
9
|
+
image = if input.key?(:url)
|
|
10
|
+
Image.from_url(input[:url], uploaded_by: actor.name)
|
|
11
|
+
else
|
|
12
|
+
file = input[:file]
|
|
13
|
+
raise Error::ImageNotPermitted unless file.is_a?(ActionDispatch::Http::UploadedFile)
|
|
14
|
+
Image.from_upload(file.tempfile, filename: file.original_filename,
|
|
15
|
+
content_type: file.content_type, uploaded_by: actor.name)
|
|
16
|
+
end
|
|
17
|
+
render json: ImageSerializer.call(image), status: :created
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
module OpenBlog
|
|
2
|
+
module Api
|
|
3
|
+
module V1
|
|
4
|
+
class PagesController < ResourcesController
|
|
5
|
+
def index
|
|
6
|
+
require_scope!(:read)
|
|
7
|
+
list(Page.all, :pages, ->(page) { PageSerializer.call(page, base_url: request.base_url) })
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def show
|
|
11
|
+
require_scope!(:read)
|
|
12
|
+
input_fields!
|
|
13
|
+
page = Page.find_by!(kind: params[:kind])
|
|
14
|
+
render json: PageSerializer.call(page, base_url: request.base_url)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def update
|
|
18
|
+
require_scope!(:write)
|
|
19
|
+
input = page_attributes
|
|
20
|
+
now = Time.current
|
|
21
|
+
attempts = 0
|
|
22
|
+
begin
|
|
23
|
+
page, created = Page.transaction(requires_new: true) do
|
|
24
|
+
current = Page.lock.find_by(kind: params[:kind]) || Page.new(kind: params[:kind])
|
|
25
|
+
require_scope!(:publish) if current.status == "published" || input[:status] == "published"
|
|
26
|
+
created = current.new_record?
|
|
27
|
+
current.assign_attributes(input)
|
|
28
|
+
current.approved_on = now.to_date if input[:approved_by].present? && !input.key?(:approved_on)
|
|
29
|
+
current.save!
|
|
30
|
+
[ current, created ]
|
|
31
|
+
end
|
|
32
|
+
rescue ActiveRecord::RecordNotUnique, ActiveRecord::RecordInvalid => error
|
|
33
|
+
collision = error.is_a?(ActiveRecord::RecordNotUnique) ||
|
|
34
|
+
(error.record.new_record? && error.record.errors.details[:kind].any? { |detail| detail[:error] == :taken })
|
|
35
|
+
raise unless collision
|
|
36
|
+
attempts += 1
|
|
37
|
+
retry if attempts == 1
|
|
38
|
+
raise Error::ValidationFailed.new(details: [ "kind" ])
|
|
39
|
+
end
|
|
40
|
+
render json: PageSerializer.call(page, base_url: request.base_url), status: created ? :created : :ok
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
private
|
|
44
|
+
|
|
45
|
+
def page_attributes
|
|
46
|
+
input = input_fields!(*ApiFields::SITE_PAGE)
|
|
47
|
+
%i[title body slug].each do |field|
|
|
48
|
+
invalid!(field) if input.key?(field) && !input[field].is_a?(String)
|
|
49
|
+
end
|
|
50
|
+
invalid!(:approved_by) if input.key?(:approved_by) && !input[:approved_by].nil? && !input[:approved_by].is_a?(String)
|
|
51
|
+
invalid!(:status) if input.key?(:status) && !%w[draft published].include?(input[:status])
|
|
52
|
+
if input.key?(:approved_on) && !input[:approved_on].nil?
|
|
53
|
+
value = input[:approved_on]
|
|
54
|
+
invalid!(:approved_on) unless value.is_a?(String) && value.match?(/\A\d{4}-\d{2}-\d{2}\z/)
|
|
55
|
+
begin
|
|
56
|
+
input[:approved_on] = Date.iso8601(value)
|
|
57
|
+
rescue Date::Error
|
|
58
|
+
invalid!(:approved_on)
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
input[:body_markdown] = input.delete(:body) if input.key?(:body)
|
|
62
|
+
input
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
module OpenBlog
|
|
2
|
+
module Api
|
|
3
|
+
module V1
|
|
4
|
+
class PostsController < BaseController
|
|
5
|
+
WRITE_FIELDS = ApiFields::POST_WRITE
|
|
6
|
+
NESTED_FIELDS = ApiFields::POST_NESTED
|
|
7
|
+
|
|
8
|
+
def index
|
|
9
|
+
require_scope!(:read)
|
|
10
|
+
render json: ApiPostQuery.call(input_fields!(*ApiPostQuery::FILTERS), base_url: request.base_url)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def show
|
|
14
|
+
require_scope!(:read)
|
|
15
|
+
input_fields!
|
|
16
|
+
render json: PostSerializer.call(find_post!, base_url: request.base_url)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def create
|
|
20
|
+
attributes = write_attributes(publish_field: true)
|
|
21
|
+
publishing = attributes.delete(:publish) == true
|
|
22
|
+
operation = publishing ? Publish : SaveDraft
|
|
23
|
+
render_result operation.call(attributes, actor: actor.name, authorize: authorization(publishing: publishing))
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def update
|
|
27
|
+
attributes = write_attributes
|
|
28
|
+
result = WritePost.call(attributes, post: find_post!, actor: actor.name, now: Time.current,
|
|
29
|
+
publish: :preserve, authorize: authorization)
|
|
30
|
+
render_result(result, status: :ok)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def publish
|
|
34
|
+
require_scope!(:publish)
|
|
35
|
+
render_result Publish.call(write_attributes, post: find_post!, actor: actor.name,
|
|
36
|
+
authorize: authorization(publishing: true))
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def unpublish
|
|
40
|
+
require_scope!(:publish)
|
|
41
|
+
input_fields!
|
|
42
|
+
render_result Unpublish.call(find_post!, actor: actor.name)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def destroy
|
|
46
|
+
require_scope!(:publish)
|
|
47
|
+
attributes = input_fields!(*ApiFields::REMOVE)
|
|
48
|
+
if attributes[:redirect_to].is_a?(String) && attributes[:redirect_to].match?(Post::SLUG_PATTERN)
|
|
49
|
+
attributes[:redirect_to] = "#{OpenBlog.mount_path.chomp('/')}/#{attributes[:redirect_to]}"
|
|
50
|
+
end
|
|
51
|
+
result = Remove.call(find_post!, actor: actor.name, **attributes)
|
|
52
|
+
if result.success? && result.post.destroyed?
|
|
53
|
+
head :no_content
|
|
54
|
+
else
|
|
55
|
+
render_result(result, status: :ok)
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
private
|
|
60
|
+
|
|
61
|
+
def authorization(publishing: false)
|
|
62
|
+
->(post) { require_scope!(publishing || post.scheduled? || post.published? ? :publish : :write) }
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def write_attributes(publish_field: false)
|
|
66
|
+
attributes = input_fields!(*(publish_field ? WRITE_FIELDS + [ :publish ] : WRITE_FIELDS))
|
|
67
|
+
if attributes.key?(:publish) && ![ true, false ].include?(attributes[:publish])
|
|
68
|
+
raise Error::ValidationFailed.new(details: [ "publish" ])
|
|
69
|
+
end
|
|
70
|
+
if attributes.key?(:author) && !attributes[:author].is_a?(String) && !attributes[:author].is_a?(Hash)
|
|
71
|
+
raise Error::ValidationFailed.new(details: [ "author" ])
|
|
72
|
+
end
|
|
73
|
+
NESTED_FIELDS.each do |field, allowed|
|
|
74
|
+
values = field == :faq && attributes[field].is_a?(Array) ? attributes[field] : [ attributes[field] ]
|
|
75
|
+
values.each do |value|
|
|
76
|
+
next unless value.is_a?(Hash)
|
|
77
|
+
unknown = value.keys - allowed
|
|
78
|
+
raise Error::UnknownField.new(details: unknown.map { |key| "#{field}.#{key}" }) if unknown.any?
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
connections = attributes.dig(:connections, :connections) if attributes[:connections].is_a?(Hash)
|
|
82
|
+
if connections.is_a?(Array)
|
|
83
|
+
connections.each do |entry|
|
|
84
|
+
next unless entry.is_a?(Hash)
|
|
85
|
+
unknown = entry.keys - %i[party relation]
|
|
86
|
+
raise Error::UnknownField.new(details: unknown.map { |key| "connections.connections.#{key}" }) if unknown.any?
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
ApiFields.validate_post_images!(attributes)
|
|
90
|
+
validate_connections!(attributes[:connections]) if attributes.key?(:connections)
|
|
91
|
+
attributes
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def validate_connections!(input)
|
|
95
|
+
invalid = -> { raise Error::ValidationFailed.new(details: [ "connections" ]) }
|
|
96
|
+
invalid.call unless input.is_a?(Hash) && input[:connections].is_a?(Array)
|
|
97
|
+
invalid.call unless input[:declared_by].is_a?(String) && input[:declared_by].present?
|
|
98
|
+
input[:connections].each do |entry|
|
|
99
|
+
invalid.call unless entry.is_a?(Hash) && %i[party relation].all? { |key| entry[key].is_a?(String) && entry[key].present? }
|
|
100
|
+
end
|
|
101
|
+
if input.key?(:declared_on)
|
|
102
|
+
value = input[:declared_on]
|
|
103
|
+
invalid.call unless value.is_a?(String) && value.match?(/\A\d{4}-\d{2}-\d{2}\z/)
|
|
104
|
+
begin
|
|
105
|
+
input[:declared_on] = Date.iso8601(value)
|
|
106
|
+
rescue Date::Error
|
|
107
|
+
invalid.call
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
end
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
module OpenBlog
|
|
2
|
+
module Api
|
|
3
|
+
module V1
|
|
4
|
+
class RedirectsController < ResourcesController
|
|
5
|
+
def index
|
|
6
|
+
require_scope!(:read)
|
|
7
|
+
list(Redirect.all, :redirects, RedirectSerializer)
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def create
|
|
11
|
+
require_scope!(:publish)
|
|
12
|
+
input = input_fields!(*ApiFields::REDIRECT)
|
|
13
|
+
text_fields!(input, :old_path, :new_path, :occurred_on)
|
|
14
|
+
old = input[:old_path]
|
|
15
|
+
invalid!(:old_path) unless old.is_a?(String) && old.start_with?("/") && !old.start_with?("//") && !old.match?(/[\s?#]/)
|
|
16
|
+
invalid!(:post_id) if input.key?(:post_id) && !input[:post_id].nil? && !input[:post_id].is_a?(Integer)
|
|
17
|
+
date = input.key?(:occurred_on) ? parse_date(input[:occurred_on]) : Date.current
|
|
18
|
+
record = Redirect.transaction do
|
|
19
|
+
Post.lock.find(input[:post_id]) if input[:post_id]
|
|
20
|
+
RedirectTarget.synchronize do
|
|
21
|
+
prefix = "#{OpenBlog.mount_path.chomp('/')}/"
|
|
22
|
+
slug = old.delete_prefix(prefix)
|
|
23
|
+
raise Error::SlugReserved if old.start_with?(prefix) && Post.exists?(slug: slug)
|
|
24
|
+
target = RedirectTarget.call(input[:new_path], from: old)
|
|
25
|
+
row = Redirect.create!(old_path: old, new_path: target, occurred_on: date, source: "manual", post_id: input[:post_id])
|
|
26
|
+
Redirect.where(new_path: old).where.not(id: row.id).find_each { |incoming| incoming.update!(new_path: target) }
|
|
27
|
+
row
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
render json: RedirectSerializer.call(record), status: :created
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def destroy
|
|
34
|
+
require_scope!(:publish)
|
|
35
|
+
input_fields!
|
|
36
|
+
RedirectTarget.synchronize { Redirect.find(params[:id]).destroy! }
|
|
37
|
+
head :no_content
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
private
|
|
41
|
+
|
|
42
|
+
def parse_date(value)
|
|
43
|
+
invalid!(:occurred_on) unless value.is_a?(String) && value.match?(/\A\d{4}-\d{2}-\d{2}\z/)
|
|
44
|
+
Date.iso8601(value)
|
|
45
|
+
rescue Date::Error
|
|
46
|
+
invalid!(:occurred_on)
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
module OpenBlog
|
|
2
|
+
module Api
|
|
3
|
+
module V1
|
|
4
|
+
class ReportController < BaseController
|
|
5
|
+
def show
|
|
6
|
+
require_scope!(:read)
|
|
7
|
+
input = input_fields!(*ApiFields::REPORT, :format)
|
|
8
|
+
format = input.delete(:format) || params[:format]
|
|
9
|
+
raise Error::ValidationFailed.new(details: [ "format" ]) unless format.nil? || %w[json text].include?(format)
|
|
10
|
+
report = SurfaceReport.run(**input)
|
|
11
|
+
if format == "text" || (format.nil? && request.accepts.first == Mime[:text])
|
|
12
|
+
render plain: report.to_text
|
|
13
|
+
else
|
|
14
|
+
render json: report.to_h
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
module OpenBlog
|
|
2
|
+
module Api
|
|
3
|
+
module V1
|
|
4
|
+
class ResourcesController < BaseController
|
|
5
|
+
rescue_from ActiveRecord::RecordInvalid do |error|
|
|
6
|
+
render_error(Error::ValidationFailed.new(details: error.record.errors.attribute_names.map(&:to_s)))
|
|
7
|
+
end
|
|
8
|
+
rescue_from ActiveRecord::RecordNotUnique do
|
|
9
|
+
render_error(Error::ValidationFailed.new(details: [ "slug" ]))
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
private
|
|
13
|
+
|
|
14
|
+
def list(relation, name, serializer)
|
|
15
|
+
input = input_fields!(*ApiFields::PAGE)
|
|
16
|
+
page = integer_parameter(input, :page, 1)
|
|
17
|
+
per_page = [ integer_parameter(input, :per_page, 25), 100 ].min
|
|
18
|
+
render json: { name => relation.order(:id).offset((page - 1) * per_page).limit(per_page).map { |record| serializer.call(record) },
|
|
19
|
+
page: page, per_page: per_page, total: relation.count }
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def integer_parameter(input, name, default)
|
|
23
|
+
return default unless input.key?(name)
|
|
24
|
+
value = input[name]
|
|
25
|
+
invalid!(name) unless (value.is_a?(String) || value.is_a?(Integer)) && value.to_s.match?(/\A[1-9]\d{0,8}\z/)
|
|
26
|
+
value.to_i
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def text_fields!(input, *fields)
|
|
30
|
+
fields.each { |field| invalid!(field) if input.key?(field) && !input[field].nil? && !input[field].is_a?(String) }
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def invalid!(field)
|
|
34
|
+
raise Error::ValidationFailed.new(details: [ field.to_s ])
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
module OpenBlog
|
|
2
|
+
module Api
|
|
3
|
+
module V1
|
|
4
|
+
class SeriesController < ResourcesController
|
|
5
|
+
def index
|
|
6
|
+
require_scope!(:read)
|
|
7
|
+
list(Series.all, :series, SeriesSerializer)
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def create
|
|
11
|
+
require_scope!(:write)
|
|
12
|
+
render json: SeriesSerializer.call(Series.create!(attributes)), status: :created
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def update
|
|
16
|
+
require_scope!(:write)
|
|
17
|
+
record = Series.find(params[:id])
|
|
18
|
+
record.update!(attributes)
|
|
19
|
+
render json: SeriesSerializer.call(record)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
private
|
|
23
|
+
|
|
24
|
+
def attributes
|
|
25
|
+
input = input_fields!(*ApiFields::SERIES)
|
|
26
|
+
text_fields!(input, :name, :slug, :description)
|
|
27
|
+
input
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|