debug-mcp 0.1.2
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/.rspec +3 -0
- data/CHANGELOG.md +83 -0
- data/LICENSE +21 -0
- data/README.ja.md +383 -0
- data/README.md +384 -0
- data/examples/01_simple_bug.rb +43 -0
- data/examples/02_data_pipeline.rb +93 -0
- data/examples/03_recursion.rb +96 -0
- data/examples/RAILS_SCENARIOS.md +350 -0
- data/examples/SCENARIOS.md +142 -0
- data/examples/rails_test_app/setup.sh +428 -0
- data/examples/rails_test_app/testapp/.dockerignore +10 -0
- data/examples/rails_test_app/testapp/.ruby-version +1 -0
- data/examples/rails_test_app/testapp/Dockerfile +23 -0
- data/examples/rails_test_app/testapp/Gemfile +17 -0
- data/examples/rails_test_app/testapp/README.md +65 -0
- data/examples/rails_test_app/testapp/Rakefile +6 -0
- data/examples/rails_test_app/testapp/app/assets/images/.keep +0 -0
- data/examples/rails_test_app/testapp/app/assets/stylesheets/application.css +1 -0
- data/examples/rails_test_app/testapp/app/controllers/application_controller.rb +4 -0
- data/examples/rails_test_app/testapp/app/controllers/concerns/.keep +0 -0
- data/examples/rails_test_app/testapp/app/controllers/dashboard_controller.rb +38 -0
- data/examples/rails_test_app/testapp/app/controllers/health_controller.rb +11 -0
- data/examples/rails_test_app/testapp/app/controllers/orders_controller.rb +100 -0
- data/examples/rails_test_app/testapp/app/controllers/posts_controller.rb +82 -0
- data/examples/rails_test_app/testapp/app/controllers/sessions_controller.rb +25 -0
- data/examples/rails_test_app/testapp/app/controllers/users_controller.rb +44 -0
- data/examples/rails_test_app/testapp/app/helpers/application_helper.rb +2 -0
- data/examples/rails_test_app/testapp/app/models/application_record.rb +3 -0
- data/examples/rails_test_app/testapp/app/models/comment.rb +8 -0
- data/examples/rails_test_app/testapp/app/models/concerns/.keep +0 -0
- data/examples/rails_test_app/testapp/app/models/order.rb +56 -0
- data/examples/rails_test_app/testapp/app/models/order_item.rb +16 -0
- data/examples/rails_test_app/testapp/app/models/post.rb +29 -0
- data/examples/rails_test_app/testapp/app/models/user.rb +34 -0
- data/examples/rails_test_app/testapp/app/services/order_report_service.rb +40 -0
- data/examples/rails_test_app/testapp/app/views/layouts/application.html.erb +28 -0
- data/examples/rails_test_app/testapp/app/views/pwa/manifest.json.erb +22 -0
- data/examples/rails_test_app/testapp/app/views/pwa/service-worker.js +26 -0
- data/examples/rails_test_app/testapp/bin/ci +6 -0
- data/examples/rails_test_app/testapp/bin/dev +2 -0
- data/examples/rails_test_app/testapp/bin/rails +4 -0
- data/examples/rails_test_app/testapp/bin/rake +4 -0
- data/examples/rails_test_app/testapp/bin/setup +35 -0
- data/examples/rails_test_app/testapp/config/application.rb +42 -0
- data/examples/rails_test_app/testapp/config/boot.rb +3 -0
- data/examples/rails_test_app/testapp/config/ci.rb +14 -0
- data/examples/rails_test_app/testapp/config/database.yml +32 -0
- data/examples/rails_test_app/testapp/config/environment.rb +5 -0
- data/examples/rails_test_app/testapp/config/environments/development.rb +54 -0
- data/examples/rails_test_app/testapp/config/environments/production.rb +67 -0
- data/examples/rails_test_app/testapp/config/environments/test.rb +42 -0
- data/examples/rails_test_app/testapp/config/initializers/content_security_policy.rb +29 -0
- data/examples/rails_test_app/testapp/config/initializers/filter_parameter_logging.rb +8 -0
- data/examples/rails_test_app/testapp/config/initializers/inflections.rb +16 -0
- data/examples/rails_test_app/testapp/config/locales/en.yml +31 -0
- data/examples/rails_test_app/testapp/config/puma.rb +39 -0
- data/examples/rails_test_app/testapp/config/routes.rb +34 -0
- data/examples/rails_test_app/testapp/config.ru +6 -0
- data/examples/rails_test_app/testapp/db/migrate/20260216002916_create_users.rb +12 -0
- data/examples/rails_test_app/testapp/db/migrate/20260216002919_create_posts.rb +13 -0
- data/examples/rails_test_app/testapp/db/migrate/20260216002922_create_comments.rb +11 -0
- data/examples/rails_test_app/testapp/db/migrate/20260222000001_create_orders.rb +14 -0
- data/examples/rails_test_app/testapp/db/migrate/20260222000002_create_order_items.rb +13 -0
- data/examples/rails_test_app/testapp/db/schema.rb +71 -0
- data/examples/rails_test_app/testapp/db/seeds.rb +85 -0
- data/examples/rails_test_app/testapp/docker-compose.yml +21 -0
- data/examples/rails_test_app/testapp/docker-entrypoint.sh +10 -0
- data/examples/rails_test_app/testapp/lib/tasks/.keep +0 -0
- data/examples/rails_test_app/testapp/log/.keep +0 -0
- data/examples/rails_test_app/testapp/public/400.html +135 -0
- data/examples/rails_test_app/testapp/public/404.html +135 -0
- data/examples/rails_test_app/testapp/public/406-unsupported-browser.html +135 -0
- data/examples/rails_test_app/testapp/public/422.html +135 -0
- data/examples/rails_test_app/testapp/public/500.html +135 -0
- data/examples/rails_test_app/testapp/public/icon.png +0 -0
- data/examples/rails_test_app/testapp/public/icon.svg +3 -0
- data/examples/rails_test_app/testapp/public/robots.txt +1 -0
- data/examples/rails_test_app/testapp/script/.keep +0 -0
- data/examples/rails_test_app/testapp/storage/.keep +0 -0
- data/examples/rails_test_app/testapp/tmp/.keep +0 -0
- data/examples/rails_test_app/testapp/tmp/pids/.keep +0 -0
- data/examples/rails_test_app/testapp/tmp/storage/.keep +0 -0
- data/examples/rails_test_app/testapp/vendor/.keep +0 -0
- data/exe/debug-mcp +39 -0
- data/exe/debug-rails +127 -0
- data/lib/debug_mcp/client_cleanup.rb +102 -0
- data/lib/debug_mcp/code_safety_analyzer.rb +124 -0
- data/lib/debug_mcp/debug_client.rb +1143 -0
- data/lib/debug_mcp/exit_message_builder.rb +112 -0
- data/lib/debug_mcp/pending_http_helper.rb +25 -0
- data/lib/debug_mcp/rails_helper.rb +155 -0
- data/lib/debug_mcp/server.rb +364 -0
- data/lib/debug_mcp/session_manager.rb +436 -0
- data/lib/debug_mcp/stop_event_annotator.rb +152 -0
- data/lib/debug_mcp/tcp_session_discovery.rb +226 -0
- data/lib/debug_mcp/tools/connect.rb +669 -0
- data/lib/debug_mcp/tools/continue_execution.rb +161 -0
- data/lib/debug_mcp/tools/disconnect.rb +169 -0
- data/lib/debug_mcp/tools/evaluate_code.rb +354 -0
- data/lib/debug_mcp/tools/finish.rb +84 -0
- data/lib/debug_mcp/tools/get_context.rb +217 -0
- data/lib/debug_mcp/tools/get_source.rb +193 -0
- data/lib/debug_mcp/tools/inspect_object.rb +107 -0
- data/lib/debug_mcp/tools/list_debug_sessions.rb +60 -0
- data/lib/debug_mcp/tools/list_files.rb +189 -0
- data/lib/debug_mcp/tools/list_paused_sessions.rb +108 -0
- data/lib/debug_mcp/tools/next.rb +70 -0
- data/lib/debug_mcp/tools/rails_info.rb +200 -0
- data/lib/debug_mcp/tools/rails_model.rb +362 -0
- data/lib/debug_mcp/tools/rails_routes.rb +186 -0
- data/lib/debug_mcp/tools/read_file.rb +214 -0
- data/lib/debug_mcp/tools/remove_breakpoint.rb +173 -0
- data/lib/debug_mcp/tools/run_debug_command.rb +55 -0
- data/lib/debug_mcp/tools/run_script.rb +293 -0
- data/lib/debug_mcp/tools/set_breakpoint.rb +206 -0
- data/lib/debug_mcp/tools/step.rb +67 -0
- data/lib/debug_mcp/tools/trigger_request.rb +515 -0
- data/lib/debug_mcp/version.rb +5 -0
- data/lib/debug_mcp.rb +40 -0
- metadata +251 -0
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
class OrdersController < ApplicationController
|
|
2
|
+
# GET /orders
|
|
3
|
+
def index
|
|
4
|
+
@orders = Order.includes(:user, :order_items).order(created_at: :desc)
|
|
5
|
+
render json: @orders.map { |o| order_json(o) }
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
# GET /orders/:id
|
|
9
|
+
# Scenario 1: total_cents may be off by 1 cent due to floating point truncation
|
|
10
|
+
def show
|
|
11
|
+
@order = Order.includes(:order_items).find(params[:id])
|
|
12
|
+
render json: order_json(@order).merge(
|
|
13
|
+
items: @order.order_items.map { |item| item_json(item) }
|
|
14
|
+
)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# POST /orders
|
|
18
|
+
def create
|
|
19
|
+
@order = Order.new(order_params)
|
|
20
|
+
if @order.save
|
|
21
|
+
render json: order_json(@order), status: :created
|
|
22
|
+
else
|
|
23
|
+
render json: { errors: @order.errors.full_messages }, status: :unprocessable_entity
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# PATCH /orders/:id
|
|
28
|
+
# Scenario 2: updating a cancelled order triggers recalculate_total
|
|
29
|
+
# which computes total from zero-quantity items, resulting in total_cents = 0
|
|
30
|
+
def update
|
|
31
|
+
@order = Order.unscoped.find(params[:id])
|
|
32
|
+
if @order.update(order_params)
|
|
33
|
+
render json: order_json(@order)
|
|
34
|
+
else
|
|
35
|
+
render json: { errors: @order.errors.full_messages }, status: :unprocessable_entity
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# POST /orders/:id/cancel
|
|
40
|
+
# Scenario 2: cancelling zeroes item quantities via after_save callback
|
|
41
|
+
def cancel
|
|
42
|
+
@order = Order.find(params[:id])
|
|
43
|
+
@order.status = :cancelled
|
|
44
|
+
if @order.save
|
|
45
|
+
render json: { message: "Order ##{@order.id} cancelled", order: order_json(@order) }
|
|
46
|
+
else
|
|
47
|
+
render json: { errors: @order.errors.full_messages }, status: :unprocessable_entity
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# GET /orders/user_orders?user_id=3&status=cancelled
|
|
52
|
+
# Scenario 4: default_scope creates contradictory SQL
|
|
53
|
+
# WHERE status != 3 AND status = 3 -> always empty
|
|
54
|
+
def user_orders
|
|
55
|
+
orders = Order.where(user_id: params[:user_id])
|
|
56
|
+
orders = orders.where(status: params[:status]) if params[:status].present?
|
|
57
|
+
render json: {
|
|
58
|
+
user_id: params[:user_id],
|
|
59
|
+
status_filter: params[:status],
|
|
60
|
+
count: orders.count,
|
|
61
|
+
orders: orders.map { |o| order_json(o) }
|
|
62
|
+
}
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# GET /orders/report
|
|
66
|
+
# Scenario 3: report drops orders with deleted users silently
|
|
67
|
+
def report
|
|
68
|
+
render json: OrderReportService.generate
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
private
|
|
72
|
+
|
|
73
|
+
def order_params
|
|
74
|
+
params.permit(:user_id, :status, :discount_code, :discount_percent)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def order_json(order)
|
|
78
|
+
{
|
|
79
|
+
id: order.id,
|
|
80
|
+
user_id: order.user_id,
|
|
81
|
+
user_name: order.user&.name,
|
|
82
|
+
status: order.status,
|
|
83
|
+
total_cents: order.total_cents,
|
|
84
|
+
discount_code: order.discount_code,
|
|
85
|
+
discount_percent: order.discount_percent,
|
|
86
|
+
completed_at: order.completed_at,
|
|
87
|
+
items_count: order.order_items.size
|
|
88
|
+
}
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def item_json(item)
|
|
92
|
+
{
|
|
93
|
+
id: item.id,
|
|
94
|
+
product_name: item.product_name,
|
|
95
|
+
quantity: item.quantity,
|
|
96
|
+
unit_price: item.unit_price,
|
|
97
|
+
tax_rate: item.tax_rate
|
|
98
|
+
}
|
|
99
|
+
end
|
|
100
|
+
end
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
class PostsController < ApplicationController
|
|
2
|
+
before_action :set_post, only: [:show, :update]
|
|
3
|
+
|
|
4
|
+
def index
|
|
5
|
+
@posts = Post.published.includes(:user).recent
|
|
6
|
+
render json: @posts.as_json(include: { user: { only: [:id, :name] } })
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def show
|
|
10
|
+
render json: @post.as_json(
|
|
11
|
+
include: {
|
|
12
|
+
user: { only: [:id, :name] },
|
|
13
|
+
comments: { include: { user: { only: [:id, :name] } } }
|
|
14
|
+
}
|
|
15
|
+
)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def create
|
|
19
|
+
@post = Post.new(post_params)
|
|
20
|
+
if @post.save
|
|
21
|
+
render json: @post, status: :created
|
|
22
|
+
else
|
|
23
|
+
render json: { errors: @post.errors.full_messages }, status: :unprocessable_entity
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def update
|
|
28
|
+
if @post.update(post_params)
|
|
29
|
+
render json: @post
|
|
30
|
+
else
|
|
31
|
+
render json: { errors: @post.errors.full_messages }, status: :unprocessable_entity
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# 人気記事の取得
|
|
36
|
+
def trending
|
|
37
|
+
posts = Post.trending
|
|
38
|
+
render json: posts.map { |post|
|
|
39
|
+
{
|
|
40
|
+
id: post.id,
|
|
41
|
+
title: post.title,
|
|
42
|
+
author: post.user.name,
|
|
43
|
+
comments_count: post.comments.size
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# 検索エンドポイント(公開済み記事のみ対象)
|
|
49
|
+
def search
|
|
50
|
+
query = params[:q].to_s
|
|
51
|
+
scope = Post.published
|
|
52
|
+
|
|
53
|
+
# ユーザーでフィルタリング
|
|
54
|
+
if params[:user_id].present?
|
|
55
|
+
scope = Post.by_user(params[:user_id])
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
scope = scope.where("title LIKE ?", "%#{query}%") if query.present?
|
|
59
|
+
|
|
60
|
+
results = scope.map do |post|
|
|
61
|
+
{
|
|
62
|
+
id: post.id,
|
|
63
|
+
title: post.title,
|
|
64
|
+
author: post.user.name,
|
|
65
|
+
status: post.status,
|
|
66
|
+
comments_count: post.comments.count
|
|
67
|
+
}
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
render json: results
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
private
|
|
74
|
+
|
|
75
|
+
def set_post
|
|
76
|
+
@post = Post.find(params[:id])
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def post_params
|
|
80
|
+
params.permit(:title, :body, :status, :user_id, :published_at)
|
|
81
|
+
end
|
|
82
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
class SessionsController < ApplicationController
|
|
2
|
+
def create
|
|
3
|
+
user = User.find_by(email: params[:email])
|
|
4
|
+
if user
|
|
5
|
+
session[:user_id] = user.id
|
|
6
|
+
render json: { message: "Logged in", user: user.as_json(only: [:id, :name, :email, :role]) }
|
|
7
|
+
else
|
|
8
|
+
render json: { error: "Invalid email" }, status: :unauthorized
|
|
9
|
+
end
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def show
|
|
13
|
+
if session[:user_id]
|
|
14
|
+
user = User.find(session[:user_id])
|
|
15
|
+
render json: { logged_in: true, user: user.as_json(only: [:id, :name, :email, :role]) }
|
|
16
|
+
else
|
|
17
|
+
render json: { logged_in: false }
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def destroy
|
|
22
|
+
session.delete(:user_id)
|
|
23
|
+
render json: { message: "Logged out" }
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
class UsersController < ApplicationController
|
|
2
|
+
before_action :set_user, only: [:show, :update, :destroy]
|
|
3
|
+
|
|
4
|
+
def index
|
|
5
|
+
@users = User.all
|
|
6
|
+
render json: @users
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def show
|
|
10
|
+
render json: @user.as_json(include: { posts: { only: [:id, :title, :status] } })
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def create
|
|
14
|
+
@user = User.new(user_params)
|
|
15
|
+
if @user.save
|
|
16
|
+
render json: @user, status: :created
|
|
17
|
+
else
|
|
18
|
+
render json: { errors: @user.errors.full_messages }, status: :unprocessable_entity
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def update
|
|
23
|
+
if @user.update(user_params)
|
|
24
|
+
render json: @user
|
|
25
|
+
else
|
|
26
|
+
render json: { errors: @user.errors.full_messages }, status: :unprocessable_entity
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def destroy
|
|
31
|
+
@user.destroy
|
|
32
|
+
head :no_content
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
private
|
|
36
|
+
|
|
37
|
+
def set_user
|
|
38
|
+
@user = User.find(params[:id])
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def user_params
|
|
42
|
+
params.permit(:name, :email, :role, :active)
|
|
43
|
+
end
|
|
44
|
+
end
|
|
File without changes
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
class Order < ApplicationRecord
|
|
2
|
+
default_scope { where.not(status: :cancelled) }
|
|
3
|
+
|
|
4
|
+
belongs_to :user, optional: true
|
|
5
|
+
has_many :order_items, dependent: :destroy
|
|
6
|
+
|
|
7
|
+
enum :status, { cart: 0, pending: 1, completed: 2, cancelled: 3 }
|
|
8
|
+
|
|
9
|
+
before_save :recalculate_total
|
|
10
|
+
after_save :archive_items_if_cancelled
|
|
11
|
+
|
|
12
|
+
validates :status, presence: true
|
|
13
|
+
|
|
14
|
+
scope :completed, -> { where(status: :completed) }
|
|
15
|
+
|
|
16
|
+
def self.revenue_stats
|
|
17
|
+
{
|
|
18
|
+
total_revenue: completed.sum(:total_cents),
|
|
19
|
+
average_order: completed.average(:total_cents)&.round || 0,
|
|
20
|
+
order_count: completed.count
|
|
21
|
+
}
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
private
|
|
25
|
+
|
|
26
|
+
# Scenario 1:
|
|
27
|
+
def recalculate_total
|
|
28
|
+
return if order_items.empty?
|
|
29
|
+
|
|
30
|
+
# SQL SUM returns BigDecimal
|
|
31
|
+
subtotal = order_items.sum("quantity * unit_price")
|
|
32
|
+
|
|
33
|
+
# Ruby Enumerable#sum with .to_f produces Float
|
|
34
|
+
tax_total = order_items.to_a.sum do |item|
|
|
35
|
+
item.quantity * item.unit_price.to_f * item.tax_rate.to_f
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
computed_total = subtotal + tax_total
|
|
39
|
+
|
|
40
|
+
if discount_percent.to_f > 0
|
|
41
|
+
computed_total = computed_total * (100 - discount_percent.to_f) / 100
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Bug: .to_i truncates instead of .round, losing up to 1 cent
|
|
45
|
+
self.total_cents = (computed_total * 100).to_i
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Scenario 2: zeroes item quantities on cancel
|
|
49
|
+
# This creates a "time bomb": any subsequent save triggers recalculate_total
|
|
50
|
+
# which recomputes total from zero-quantity items, resulting in total_cents = 0
|
|
51
|
+
def archive_items_if_cancelled
|
|
52
|
+
if saved_change_to_status? && cancelled?
|
|
53
|
+
order_items.update_all(quantity: 0)
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
class OrderItem < ApplicationRecord
|
|
2
|
+
belongs_to :order
|
|
3
|
+
|
|
4
|
+
validates :product_name, presence: true
|
|
5
|
+
validates :quantity, presence: true, numericality: { greater_than_or_equal_to: 0 }
|
|
6
|
+
validates :unit_price, presence: true, numericality: { greater_than_or_equal_to: 0 }
|
|
7
|
+
validates :tax_rate, presence: true, numericality: { greater_than_or_equal_to: 0 }
|
|
8
|
+
|
|
9
|
+
def line_total
|
|
10
|
+
quantity * unit_price
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def tax_amount
|
|
14
|
+
line_total * tax_rate
|
|
15
|
+
end
|
|
16
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
class Post < ApplicationRecord
|
|
2
|
+
belongs_to :user
|
|
3
|
+
has_many :comments, dependent: :destroy
|
|
4
|
+
|
|
5
|
+
validates :title, presence: true, length: { maximum: 100 }
|
|
6
|
+
validates :body, presence: true
|
|
7
|
+
|
|
8
|
+
enum :status, { draft: 0, published: 1, archived: 2 }
|
|
9
|
+
|
|
10
|
+
scope :published, -> { where(status: :published) }
|
|
11
|
+
scope :drafts, -> { where(status: :draft) }
|
|
12
|
+
scope :recent, -> { order(published_at: :desc) }
|
|
13
|
+
scope :by_user, ->(user_id) { where(user_id: user_id) }
|
|
14
|
+
|
|
15
|
+
# コメント数の多い人気記事を取得(パフォーマンス向上のためメモ化)
|
|
16
|
+
def self.trending(limit = 5)
|
|
17
|
+
@trending_posts ||= published
|
|
18
|
+
.left_joins(:comments)
|
|
19
|
+
.group(:id)
|
|
20
|
+
.order("COUNT(comments.id) DESC")
|
|
21
|
+
.limit(limit)
|
|
22
|
+
.includes(:user)
|
|
23
|
+
.to_a
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def summary
|
|
27
|
+
body.to_s.truncate(100)
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
class User < ApplicationRecord
|
|
2
|
+
has_many :posts, dependent: :destroy
|
|
3
|
+
has_many :comments, dependent: :destroy
|
|
4
|
+
has_many :orders
|
|
5
|
+
|
|
6
|
+
validates :name, presence: true, length: { maximum: 50 }
|
|
7
|
+
validates :email, presence: true, uniqueness: true,
|
|
8
|
+
format: { with: /\A[^@\s]+@[^@\s]+\z/ }
|
|
9
|
+
|
|
10
|
+
enum :role, { guest: 0, member: 1, editor: 2, admin: 3 }
|
|
11
|
+
|
|
12
|
+
scope :active, -> { where(active: true) }
|
|
13
|
+
scope :admins, -> { where(role: :admin) }
|
|
14
|
+
scope :recent, -> { order(created_at: :desc) }
|
|
15
|
+
|
|
16
|
+
# NULLロールのユーザーが作られないようにデフォルトを設定
|
|
17
|
+
before_save :ensure_default_role
|
|
18
|
+
|
|
19
|
+
def display_name
|
|
20
|
+
"#{name} (#{role})"
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# 管理者のみが他ユーザーの情報を編集可能
|
|
24
|
+
def editable_by?(editor)
|
|
25
|
+
editor.admin? || editor == self
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
private
|
|
29
|
+
|
|
30
|
+
def ensure_default_role
|
|
31
|
+
# role_before_type_castで内部整数値を確認し、未設定なら初期値を設定
|
|
32
|
+
self.role = :member if role_before_type_cast.nil? || role_before_type_cast == 0
|
|
33
|
+
end
|
|
34
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
class OrderReportService
|
|
2
|
+
# Scenario 3: silent exception swallowing causes data loss
|
|
3
|
+
# When order.user is nil (deleted user), order.user.name raises NoMethodError
|
|
4
|
+
# The rescue => e catches it and drops the order from the report
|
|
5
|
+
# This causes summary.total_orders != orders.length
|
|
6
|
+
|
|
7
|
+
def self.generate
|
|
8
|
+
orders = Order.completed.includes(:user, :order_items)
|
|
9
|
+
|
|
10
|
+
report = {
|
|
11
|
+
generated_at: Time.current,
|
|
12
|
+
summary: {
|
|
13
|
+
total_orders: orders.count,
|
|
14
|
+
total_revenue: orders.sum(:total_cents)
|
|
15
|
+
},
|
|
16
|
+
orders: []
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
orders.each do |order|
|
|
20
|
+
serialized = serialize_order(order)
|
|
21
|
+
report[:orders] << serialized if serialized
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
report
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def self.serialize_order(order)
|
|
28
|
+
{
|
|
29
|
+
id: order.id,
|
|
30
|
+
customer: order.user.name,
|
|
31
|
+
total_cents: order.total_cents,
|
|
32
|
+
items_count: order.order_items.size,
|
|
33
|
+
completed_at: order.completed_at
|
|
34
|
+
}
|
|
35
|
+
rescue => e
|
|
36
|
+
Rails.logger.warn("Failed to serialize order ##{order.id}: #{e.message}")
|
|
37
|
+
nil
|
|
38
|
+
end
|
|
39
|
+
private_class_method :serialize_order
|
|
40
|
+
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html>
|
|
3
|
+
<head>
|
|
4
|
+
<title><%= content_for(:title) || "Testapp" %></title>
|
|
5
|
+
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
6
|
+
<meta name="apple-mobile-web-app-capable" content="yes">
|
|
7
|
+
<meta name="application-name" content="Testapp">
|
|
8
|
+
<meta name="mobile-web-app-capable" content="yes">
|
|
9
|
+
<%= csrf_meta_tags %>
|
|
10
|
+
<%= csp_meta_tag %>
|
|
11
|
+
|
|
12
|
+
<%= yield :head %>
|
|
13
|
+
|
|
14
|
+
<%# Enable PWA manifest for installable apps (make sure to enable in config/routes.rb too!) %>
|
|
15
|
+
<%#= tag.link rel: "manifest", href: pwa_manifest_path(format: :json) %>
|
|
16
|
+
|
|
17
|
+
<link rel="icon" href="/icon.png" type="image/png">
|
|
18
|
+
<link rel="icon" href="/icon.svg" type="image/svg+xml">
|
|
19
|
+
<link rel="apple-touch-icon" href="/icon.png">
|
|
20
|
+
|
|
21
|
+
<%# Includes all stylesheet files in app/assets/stylesheets %>
|
|
22
|
+
<%= stylesheet_link_tag "application" %>
|
|
23
|
+
</head>
|
|
24
|
+
|
|
25
|
+
<body>
|
|
26
|
+
<%= yield %>
|
|
27
|
+
</body>
|
|
28
|
+
</html>
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "Testapp",
|
|
3
|
+
"icons": [
|
|
4
|
+
{
|
|
5
|
+
"src": "/icon.png",
|
|
6
|
+
"type": "image/png",
|
|
7
|
+
"sizes": "512x512"
|
|
8
|
+
},
|
|
9
|
+
{
|
|
10
|
+
"src": "/icon.png",
|
|
11
|
+
"type": "image/png",
|
|
12
|
+
"sizes": "512x512",
|
|
13
|
+
"purpose": "maskable"
|
|
14
|
+
}
|
|
15
|
+
],
|
|
16
|
+
"start_url": "/",
|
|
17
|
+
"display": "standalone",
|
|
18
|
+
"scope": "/",
|
|
19
|
+
"description": "Testapp.",
|
|
20
|
+
"theme_color": "red",
|
|
21
|
+
"background_color": "red"
|
|
22
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
// Add a service worker for processing Web Push notifications:
|
|
2
|
+
//
|
|
3
|
+
// self.addEventListener("push", async (event) => {
|
|
4
|
+
// const { title, options } = await event.data.json()
|
|
5
|
+
// event.waitUntil(self.registration.showNotification(title, options))
|
|
6
|
+
// })
|
|
7
|
+
//
|
|
8
|
+
// self.addEventListener("notificationclick", function(event) {
|
|
9
|
+
// event.notification.close()
|
|
10
|
+
// event.waitUntil(
|
|
11
|
+
// clients.matchAll({ type: "window" }).then((clientList) => {
|
|
12
|
+
// for (let i = 0; i < clientList.length; i++) {
|
|
13
|
+
// let client = clientList[i]
|
|
14
|
+
// let clientPath = (new URL(client.url)).pathname
|
|
15
|
+
//
|
|
16
|
+
// if (clientPath == event.notification.data.path && "focus" in client) {
|
|
17
|
+
// return client.focus()
|
|
18
|
+
// }
|
|
19
|
+
// }
|
|
20
|
+
//
|
|
21
|
+
// if (clients.openWindow) {
|
|
22
|
+
// return clients.openWindow(event.notification.data.path)
|
|
23
|
+
// }
|
|
24
|
+
// })
|
|
25
|
+
// )
|
|
26
|
+
// })
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
require "fileutils"
|
|
3
|
+
|
|
4
|
+
APP_ROOT = File.expand_path("..", __dir__)
|
|
5
|
+
|
|
6
|
+
def system!(*args)
|
|
7
|
+
system(*args, exception: true)
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
FileUtils.chdir APP_ROOT do
|
|
11
|
+
# This script is a way to set up or update your development environment automatically.
|
|
12
|
+
# This script is idempotent, so that you can run it at any time and get an expectable outcome.
|
|
13
|
+
# Add necessary setup steps to this file.
|
|
14
|
+
|
|
15
|
+
puts "== Installing dependencies =="
|
|
16
|
+
system("bundle check") || system!("bundle install")
|
|
17
|
+
|
|
18
|
+
# puts "\n== Copying sample files =="
|
|
19
|
+
# unless File.exist?("config/database.yml")
|
|
20
|
+
# FileUtils.cp "config/database.yml.sample", "config/database.yml"
|
|
21
|
+
# end
|
|
22
|
+
|
|
23
|
+
puts "\n== Preparing database =="
|
|
24
|
+
system! "bin/rails db:prepare"
|
|
25
|
+
system! "bin/rails db:reset" if ARGV.include?("--reset")
|
|
26
|
+
|
|
27
|
+
puts "\n== Removing old logs and tempfiles =="
|
|
28
|
+
system! "bin/rails log:clear tmp:clear"
|
|
29
|
+
|
|
30
|
+
unless ARGV.include?("--skip-server")
|
|
31
|
+
puts "\n== Starting development server =="
|
|
32
|
+
STDOUT.flush # flush the output before exec(2) so that it displays
|
|
33
|
+
exec "bin/dev"
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
require_relative "boot"
|
|
2
|
+
|
|
3
|
+
require "rails"
|
|
4
|
+
# Pick the frameworks you want:
|
|
5
|
+
require "active_model/railtie"
|
|
6
|
+
# require "active_job/railtie"
|
|
7
|
+
require "active_record/railtie"
|
|
8
|
+
# require "active_storage/engine"
|
|
9
|
+
require "action_controller/railtie"
|
|
10
|
+
# require "action_mailer/railtie"
|
|
11
|
+
# require "action_mailbox/engine"
|
|
12
|
+
# require "action_text/engine"
|
|
13
|
+
require "action_view/railtie"
|
|
14
|
+
# require "action_cable/engine"
|
|
15
|
+
# require "rails/test_unit/railtie"
|
|
16
|
+
|
|
17
|
+
# Require the gems listed in Gemfile, including any gems
|
|
18
|
+
# you've limited to :test, :development, or :production.
|
|
19
|
+
Bundler.require(*Rails.groups)
|
|
20
|
+
|
|
21
|
+
module Testapp
|
|
22
|
+
class Application < Rails::Application
|
|
23
|
+
# Initialize configuration defaults for originally generated Rails version.
|
|
24
|
+
config.load_defaults 8.1
|
|
25
|
+
|
|
26
|
+
# Please, add to the `ignore` list any other `lib` subdirectories that do
|
|
27
|
+
# not contain `.rb` files, or that should not be reloaded or eager loaded.
|
|
28
|
+
# Common ones are `templates`, `generators`, or `middleware`, for example.
|
|
29
|
+
config.autoload_lib(ignore: %w[assets tasks])
|
|
30
|
+
|
|
31
|
+
# Configuration for the application, engines, and railties goes here.
|
|
32
|
+
#
|
|
33
|
+
# These settings can be overridden in specific environments using the files
|
|
34
|
+
# in config/environments, which are processed later.
|
|
35
|
+
#
|
|
36
|
+
# config.time_zone = "Central Time (US & Canada)"
|
|
37
|
+
# config.eager_load_paths << Rails.root.join("extras")
|
|
38
|
+
|
|
39
|
+
# Don't generate system test files.
|
|
40
|
+
config.generators.system_tests = nil
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# Run using bin/ci
|
|
2
|
+
|
|
3
|
+
CI.run do
|
|
4
|
+
step "Setup", "bin/setup --skip-server"
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
# Optional: set a green GitHub commit status to unblock PR merge.
|
|
8
|
+
# Requires the `gh` CLI and `gh extension install basecamp/gh-signoff`.
|
|
9
|
+
# if success?
|
|
10
|
+
# step "Signoff: All systems go. Ready for merge and deploy.", "gh signoff"
|
|
11
|
+
# else
|
|
12
|
+
# failure "Signoff: CI failed. Do not merge or deploy.", "Fix the issues and try again."
|
|
13
|
+
# end
|
|
14
|
+
end
|