├── log
└── .keep
├── tmp
├── .keep
└── pids
│ └── .keep
├── vendor
└── .keep
├── lib
├── assets
│ └── .keep
└── tasks
│ └── .keep
├── public
├── favicon.ico
├── apple-touch-icon.png
├── apple-touch-icon-precomposed.png
├── robots.txt
├── 500.html
├── 422.html
└── 404.html
├── test
├── helpers
│ └── .keep
├── models
│ └── .keep
├── controllers
│ └── .keep
├── integration
│ └── .keep
├── fixtures
│ └── files
│ │ └── .keep
└── test_helper.rb
├── app
├── assets
│ ├── images
│ │ └── .keep
│ ├── config
│ │ └── manifest.js
│ └── stylesheets
│ │ └── application.css
├── models
│ ├── concerns
│ │ └── .keep
│ ├── application_record.rb
│ ├── specialization.rb
│ ├── campaign.rb
│ ├── campaigns_player.rb
│ ├── spell.rb
│ ├── characters_spell.rb
│ ├── characters_feat.rb
│ ├── skill.rb
│ ├── characters_skill.rb
│ ├── characters_flaw.rb
│ ├── characters_virtue.rb
│ ├── characters_specialization.rb
│ ├── inventory.rb
│ ├── player.rb
│ ├── inventories_item.rb
│ ├── feat.rb
│ ├── flaw.rb
│ ├── virtue.rb
│ ├── item.rb
│ └── character.rb
├── controllers
│ ├── concerns
│ │ └── .keep
│ └── application_controller.rb
├── helpers
│ └── application_helper.rb
├── domain
│ ├── types.rb
│ ├── query_builder
│ │ ├── nodes
│ │ │ ├── wheres
│ │ │ │ ├── base.rb
│ │ │ │ ├── not.rb
│ │ │ │ ├── and.rb
│ │ │ │ ├── or.rb
│ │ │ │ └── boolean_operator.rb
│ │ │ ├── limit.rb
│ │ │ ├── order_by.rb
│ │ │ ├── simple.rb
│ │ │ ├── base.rb
│ │ │ ├── select.rb
│ │ │ ├── select_distinct.rb
│ │ │ └── joins
│ │ │ │ ├── includes.rb
│ │ │ │ ├── inner_join.rb
│ │ │ │ ├── left_outer_join.rb
│ │ │ │ └── includes_references.rb
│ │ ├── characters
│ │ │ └── nodes
│ │ │ │ ├── veteran.rb
│ │ │ │ ├── elite_stat.rb
│ │ │ │ ├── id_in.rb
│ │ │ │ ├── buff.rb
│ │ │ │ └── legendary.rb
│ │ ├── feats
│ │ │ └── nodes
│ │ │ │ └── name_in.rb
│ │ ├── flaws
│ │ │ └── nodes
│ │ │ │ └── title_in.rb
│ │ ├── spells
│ │ │ └── nodes
│ │ │ │ ├── name_in.rb
│ │ │ │ └── level_in.rb
│ │ ├── virtues
│ │ │ └── nodes
│ │ │ │ └── title_in.rb
│ │ ├── campaigns
│ │ │ └── nodes
│ │ │ │ ├── title_in.rb
│ │ │ │ └── time_window.rb
│ │ ├── specializations
│ │ │ └── nodes
│ │ │ │ └── name_in.rb
│ │ └── builders
│ │ │ ├── default.rb
│ │ │ ├── wheres.rb
│ │ │ └── base.rb
│ └── queries
│ │ ├── helpers
│ │ ├── wheres.rb
│ │ └── joins
│ │ │ └── filter.rb
│ │ ├── base.rb
│ │ ├── characters
│ │ ├── character_spells_query.rb
│ │ └── index_query.rb
│ │ └── campaigns
│ │ └── index_query.rb
└── views
│ └── layouts
│ └── application.html.erb
├── .ruby-version
├── .rspec
├── .gitignore
├── config
├── master.key
├── boot.rb
├── environment.rb
├── routes.rb
├── initializers
│ ├── filter_parameter_logging.rb
│ ├── permissions_policy.rb
│ ├── assets.rb
│ ├── inflections.rb
│ └── content_security_policy.rb
├── credentials.yml.enc
├── locales
│ └── en.yml
├── application.rb
├── puma.rb
├── environments
│ ├── test.rb
│ ├── development.rb
│ └── production.rb
└── database.yml
├── bin
├── rake
├── rails
├── setup
└── bundle
├── config.ru
├── spec
├── factories
│ ├── feats.rb
│ ├── flaws.rb
│ ├── specializations.rb
│ ├── virtues.rb
│ ├── spells.rb
│ ├── campaigns.rb
│ └── characters.rb
├── queries
│ ├── shared_context
│ │ ├── campaigns.rb
│ │ └── characters.rb
│ ├── campaigns
│ │ └── index_query_spec.rb
│ └── characters
│ │ └── index_query_spec.rb
├── rails_helper.rb
└── spec_helper.rb
├── CODEOWNERS
├── Rakefile
├── LICENSE
├── Gemfile
├── db
├── seeds.rb
├── schema.rb
└── migrate
│ └── 20230414172406_create_starting_schema.rb
├── Gemfile.lock
└── README.md
/log/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tmp/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/vendor/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/lib/assets/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/lib/tasks/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/test/helpers/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/test/models/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tmp/pids/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/app/assets/images/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/test/controllers/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/test/integration/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/.ruby-version:
--------------------------------------------------------------------------------
1 | ruby-3.2.0
2 |
--------------------------------------------------------------------------------
/app/models/concerns/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/apple-touch-icon.png:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/test/fixtures/files/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/.rspec:
--------------------------------------------------------------------------------
1 | --require spec_helper
2 |
--------------------------------------------------------------------------------
/app/controllers/concerns/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /log/*
2 | /tmp/*
3 | .idea
--------------------------------------------------------------------------------
/public/apple-touch-icon-precomposed.png:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/config/master.key:
--------------------------------------------------------------------------------
1 | 5aabedae418fafb4d938131128ff86e1
--------------------------------------------------------------------------------
/app/helpers/application_helper.rb:
--------------------------------------------------------------------------------
1 | module ApplicationHelper
2 | end
3 |
--------------------------------------------------------------------------------
/app/domain/types.rb:
--------------------------------------------------------------------------------
1 | require 'dry-types'
2 |
3 | module Types
4 | include Dry.Types
5 | end
6 |
--------------------------------------------------------------------------------
/app/assets/config/manifest.js:
--------------------------------------------------------------------------------
1 | //= link_tree ../images
2 | //= link_directory ../stylesheets .css
3 |
--------------------------------------------------------------------------------
/app/controllers/application_controller.rb:
--------------------------------------------------------------------------------
1 | class ApplicationController < ActionController::Base
2 | end
3 |
--------------------------------------------------------------------------------
/app/models/application_record.rb:
--------------------------------------------------------------------------------
1 | class ApplicationRecord < ActiveRecord::Base
2 | primary_abstract_class
3 | end
4 |
--------------------------------------------------------------------------------
/bin/rake:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 | require_relative "../config/boot"
3 | require "rake"
4 | Rake.application.run
5 |
--------------------------------------------------------------------------------
/public/robots.txt:
--------------------------------------------------------------------------------
1 | # See https://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file
2 |
--------------------------------------------------------------------------------
/config/boot.rb:
--------------------------------------------------------------------------------
1 | ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__)
2 |
3 | require "bundler/setup" # Set up gems listed in the Gemfile.
4 |
--------------------------------------------------------------------------------
/bin/rails:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 | APP_PATH = File.expand_path("../config/application", __dir__)
3 | require_relative "../config/boot"
4 | require "rails/commands"
5 |
--------------------------------------------------------------------------------
/config/environment.rb:
--------------------------------------------------------------------------------
1 | # Load the Rails application.
2 | require_relative "application"
3 |
4 | # Initialize the Rails application.
5 | Rails.application.initialize!
6 |
--------------------------------------------------------------------------------
/config.ru:
--------------------------------------------------------------------------------
1 | # This file is used by Rack-based servers to start the application.
2 |
3 | require_relative "config/environment"
4 |
5 | run Rails.application
6 | Rails.application.load_server
7 |
--------------------------------------------------------------------------------
/spec/factories/feats.rb:
--------------------------------------------------------------------------------
1 | FactoryBot.define do
2 | factory :feat, class: 'Feat' do
3 | name { 'dungeon delving' }
4 | description { 'gets bonuses in dungeons'}
5 | end
6 | end
7 |
--------------------------------------------------------------------------------
/spec/factories/flaws.rb:
--------------------------------------------------------------------------------
1 | FactoryBot.define do
2 | factory :flaw, class: 'Flaw' do
3 | title { 'forgetful' }
4 | description { '-2 to memory' }
5 | modify_strength { -2 }
6 | end
7 | end
8 |
--------------------------------------------------------------------------------
/spec/factories/specializations.rb:
--------------------------------------------------------------------------------
1 | FactoryBot.define do
2 | factory :specialization, class: 'Specialization' do
3 | name { 'Ranger' }
4 | description { 'likes nature and ranged weapons' }
5 | end
6 | end
7 |
--------------------------------------------------------------------------------
/spec/factories/virtues.rb:
--------------------------------------------------------------------------------
1 | FactoryBot.define do
2 | factory :virtue, class: 'Virtue' do
3 | title { 'great strength' }
4 | description { '+2 to strength' }
5 | modify_strength { 2 }
6 | end
7 | end
8 |
--------------------------------------------------------------------------------
/spec/factories/spells.rb:
--------------------------------------------------------------------------------
1 | FactoryBot.define do
2 | factory :spell, class: 'Spell' do
3 | name { 'greater illusion' }
4 | description { 'produces a powerful illusion effect' }
5 | level { 4 }
6 | end
7 | end
8 |
--------------------------------------------------------------------------------
/CODEOWNERS:
--------------------------------------------------------------------------------
1 | # Lines starting with '#' are comments.
2 | # Each line is a file pattern followed by one or more owners.
3 |
4 | # These owners will be the default owners for everything in the repo.
5 | * @justinddaniel @JohnP42
6 |
--------------------------------------------------------------------------------
/config/routes.rb:
--------------------------------------------------------------------------------
1 | Rails.application.routes.draw do
2 | # Define your application routes per the DSL in https://guides.rubyonrails.org/routing.html
3 |
4 | # Defines the root path route ("/")
5 | # root "articles#index"
6 | end
7 |
--------------------------------------------------------------------------------
/spec/factories/campaigns.rb:
--------------------------------------------------------------------------------
1 | FactoryBot.define do
2 | factory :campaign, class: 'Campaign' do
3 | title { Faker::Book.title }
4 | description { Faker::Quote.fortune_cookie }
5 | start_date { Time.zone.now }
6 | end
7 | end
8 |
--------------------------------------------------------------------------------
/Rakefile:
--------------------------------------------------------------------------------
1 | # Add your own tasks in files placed in lib/tasks ending in .rake,
2 | # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake.
3 |
4 | require_relative "config/application"
5 |
6 | Rails.application.load_tasks
7 |
--------------------------------------------------------------------------------
/app/domain/query_builder/nodes/wheres/base.rb:
--------------------------------------------------------------------------------
1 | module QueryBuilder
2 | module Nodes
3 | module Wheres
4 | class Base < ::QueryBuilder::Nodes::Base
5 | include QueryBuilder::Nodes::Simple
6 | end
7 | end
8 | end
9 | end
10 |
--------------------------------------------------------------------------------
/app/domain/query_builder/nodes/limit.rb:
--------------------------------------------------------------------------------
1 | module QueryBuilder
2 | module Nodes
3 | class Limit < QueryBuilder::Nodes::Base
4 | param :numeric
5 |
6 | def accept(state)
7 | state.limit(numeric)
8 | end
9 | end
10 | end
11 | end
12 |
--------------------------------------------------------------------------------
/app/domain/query_builder/nodes/order_by.rb:
--------------------------------------------------------------------------------
1 | module QueryBuilder
2 | module Nodes
3 | class OrderBy < QueryBuilder::Nodes::Base
4 | param :sort
5 |
6 | def accept(state)
7 | state.order(sort)
8 | end
9 | end
10 | end
11 | end
12 |
--------------------------------------------------------------------------------
/app/domain/queries/helpers/wheres.rb:
--------------------------------------------------------------------------------
1 | module Queries
2 | module Helpers
3 | module Wheres
4 | def wheres_operator(operator, *clauses)
5 | "QueryBuilder::Nodes::Wheres::#{operator.to_s.camelize}".constantize.new(*clauses)
6 | end
7 | end
8 | end
9 | end
10 |
--------------------------------------------------------------------------------
/app/domain/query_builder/nodes/simple.rb:
--------------------------------------------------------------------------------
1 | module QueryBuilder
2 | module Nodes
3 | module Simple
4 | def clause
5 | raise NotImplementedError
6 | end
7 |
8 | def accept(state)
9 | state.merge(clause)
10 | end
11 | end
12 | end
13 | end
14 |
--------------------------------------------------------------------------------
/app/domain/query_builder/nodes/base.rb:
--------------------------------------------------------------------------------
1 | module QueryBuilder
2 | module Nodes
3 | class Base
4 | extend Dry::Initializer
5 |
6 | def accept(_state)
7 | raise NotImplementedError
8 | end
9 |
10 | def valid?
11 | true
12 | end
13 | end
14 | end
15 | end
16 |
--------------------------------------------------------------------------------
/app/domain/query_builder/characters/nodes/veteran.rb:
--------------------------------------------------------------------------------
1 | module QueryBuilder
2 | module Characters
3 | module Nodes
4 | class Veteran < ::QueryBuilder::Nodes::Wheres::Base
5 | def clause
6 | Character.where('experience > 10000')
7 | end
8 | end
9 | end
10 | end
11 | end
12 |
--------------------------------------------------------------------------------
/app/domain/query_builder/nodes/select.rb:
--------------------------------------------------------------------------------
1 | module QueryBuilder
2 | module Nodes
3 | class Select < ::QueryBuilder::Nodes::Base
4 | include QueryBuilder::Nodes::Simple
5 |
6 | param :relation
7 | param :columns
8 |
9 | def clause
10 | relation.select(columns)
11 | end
12 | end
13 | end
14 | end
15 |
--------------------------------------------------------------------------------
/app/domain/query_builder/characters/nodes/elite_stat.rb:
--------------------------------------------------------------------------------
1 | module QueryBuilder
2 | module Characters
3 | module Nodes
4 | class EliteStat < QueryBuilder::Nodes::Wheres::Base
5 | param :stat_name
6 |
7 | def clause
8 | Character.where("#{stat_name} >= 18")
9 | end
10 | end
11 | end
12 | end
13 | end
14 |
--------------------------------------------------------------------------------
/app/domain/query_builder/nodes/select_distinct.rb:
--------------------------------------------------------------------------------
1 | module QueryBuilder
2 | module Nodes
3 | class SelectDistinct < ::QueryBuilder::Nodes::Base
4 | include QueryBuilder::Nodes::Simple
5 |
6 | param :relation
7 | param :columns
8 |
9 | def clause
10 | relation.select(columns).distinct
11 | end
12 | end
13 | end
14 | end
15 |
--------------------------------------------------------------------------------
/app/domain/query_builder/nodes/wheres/not.rb:
--------------------------------------------------------------------------------
1 | module QueryBuilder
2 | module Nodes
3 | module Wheres
4 | class Not < ::QueryBuilder::Nodes::Wheres::Base
5 | param :nested
6 |
7 | delegate :valid?, to: :nested
8 |
9 | def clause
10 | nested.clause.invert_where
11 | end
12 | end
13 | end
14 | end
15 | end
16 |
--------------------------------------------------------------------------------
/app/views/layouts/application.html.erb:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Rails2023Showcase
5 |
6 | <%= csrf_meta_tags %>
7 | <%= csp_meta_tag %>
8 |
9 | <%= stylesheet_link_tag "application" %>
10 |
11 |
12 |
13 | <%= yield %>
14 |
15 |
16 |
--------------------------------------------------------------------------------
/app/domain/query_builder/characters/nodes/id_in.rb:
--------------------------------------------------------------------------------
1 | module QueryBuilder
2 | module Characters
3 | module Nodes
4 | class IdIn < QueryBuilder::Nodes::Wheres::Base
5 | param :ids
6 |
7 | def clause
8 | Spell.where(id: ids)
9 | end
10 |
11 | def valid?
12 | ids.present?
13 | end
14 | end
15 | end
16 | end
17 | end
18 |
--------------------------------------------------------------------------------
/app/domain/query_builder/feats/nodes/name_in.rb:
--------------------------------------------------------------------------------
1 | module QueryBuilder
2 | module Feats
3 | module Nodes
4 | class NameIn < QueryBuilder::Nodes::Wheres::Base
5 | param :names
6 |
7 | def clause
8 | Feat.where(name: names)
9 | end
10 |
11 | def valid?
12 | names.present?
13 | end
14 | end
15 | end
16 | end
17 | end
18 |
--------------------------------------------------------------------------------
/app/domain/query_builder/nodes/joins/includes.rb:
--------------------------------------------------------------------------------
1 | module QueryBuilder
2 | module Nodes
3 | module Joins
4 | class Includes < QueryBuilder::Nodes::Base
5 | include QueryBuilder::Nodes::Simple
6 |
7 | param :table1
8 | param :table2
9 |
10 | def clause
11 | table1.includes(table2)
12 | end
13 | end
14 | end
15 | end
16 | end
17 |
--------------------------------------------------------------------------------
/app/domain/query_builder/flaws/nodes/title_in.rb:
--------------------------------------------------------------------------------
1 | module QueryBuilder
2 | module Flaws
3 | module Nodes
4 | class TitleIn < QueryBuilder::Nodes::Wheres::Base
5 | param :titles
6 |
7 | def clause
8 | Flaw.where(title: titles)
9 | end
10 |
11 | def valid?
12 | titles.present?
13 | end
14 | end
15 | end
16 | end
17 | end
18 |
--------------------------------------------------------------------------------
/app/domain/query_builder/nodes/joins/inner_join.rb:
--------------------------------------------------------------------------------
1 | module QueryBuilder
2 | module Nodes
3 | module Joins
4 | class InnerJoin < ::QueryBuilder::Nodes::Base
5 | include QueryBuilder::Nodes::Simple
6 |
7 | param :table1
8 | param :table2
9 |
10 | def clause
11 | table1.joins(table2)
12 | end
13 | end
14 | end
15 | end
16 | end
17 |
--------------------------------------------------------------------------------
/app/domain/query_builder/spells/nodes/name_in.rb:
--------------------------------------------------------------------------------
1 | module QueryBuilder
2 | module Spells
3 | module Nodes
4 | class NameIn < QueryBuilder::Nodes::Wheres::Base
5 | param :names
6 |
7 | def clause
8 | Spell.where(name: names)
9 | end
10 |
11 | def valid?
12 | names.present?
13 | end
14 | end
15 | end
16 | end
17 | end
18 |
--------------------------------------------------------------------------------
/app/domain/query_builder/nodes/wheres/and.rb:
--------------------------------------------------------------------------------
1 | module QueryBuilder
2 | module Nodes
3 | module Wheres
4 | class And < QueryBuilder::Nodes::Wheres::Base
5 | include QueryBuilder::Nodes::Wheres::BooleanOperator
6 |
7 | def initialize(*child_nodes)
8 | @builder = Builder.new(operator: :and).add(*child_nodes)
9 | end
10 | end
11 | end
12 | end
13 | end
14 |
--------------------------------------------------------------------------------
/app/domain/query_builder/spells/nodes/level_in.rb:
--------------------------------------------------------------------------------
1 | module QueryBuilder
2 | module Spells
3 | module Nodes
4 | class LevelIn < QueryBuilder::Nodes::Wheres::Base
5 | param :levels
6 |
7 | def clause
8 | Spell.where(level: levels)
9 | end
10 |
11 | def valid?
12 | levels.present?
13 | end
14 | end
15 | end
16 | end
17 | end
18 |
--------------------------------------------------------------------------------
/app/domain/query_builder/nodes/joins/left_outer_join.rb:
--------------------------------------------------------------------------------
1 | module QueryBuilder
2 | module Nodes
3 | module Joins
4 | class LeftOuterJoin < QueryBuilder::Nodes::Base
5 | include QueryBuilder::Nodes::Simple
6 |
7 | param :table1
8 | param :table2
9 |
10 | def clause
11 | table1.left_joins(table2)
12 | end
13 | end
14 | end
15 | end
16 | end
17 |
--------------------------------------------------------------------------------
/app/domain/query_builder/virtues/nodes/title_in.rb:
--------------------------------------------------------------------------------
1 | module QueryBuilder
2 | module Virtues
3 | module Nodes
4 | class TitleIn < QueryBuilder::Nodes::Wheres::Base
5 | param :titles
6 |
7 | def clause
8 | Virtue.where(title: titles)
9 | end
10 |
11 | def valid?
12 | titles.present?
13 | end
14 | end
15 | end
16 | end
17 | end
18 |
--------------------------------------------------------------------------------
/app/domain/query_builder/campaigns/nodes/title_in.rb:
--------------------------------------------------------------------------------
1 | module QueryBuilder
2 | module Campaigns
3 | module Nodes
4 | class TitleIn < QueryBuilder::Nodes::Wheres::Base
5 | param :titles
6 |
7 | def clause
8 | Campaign.where(title: titles)
9 | end
10 |
11 | def valid?
12 | titles.present?
13 | end
14 | end
15 | end
16 | end
17 | end
18 |
--------------------------------------------------------------------------------
/app/domain/query_builder/nodes/wheres/or.rb:
--------------------------------------------------------------------------------
1 | module QueryBuilder
2 | module Nodes
3 | module Wheres
4 | class Or < QueryBuilder::Nodes::Wheres::Base
5 | include QueryBuilder::Nodes::Wheres::BooleanOperator
6 |
7 | def initialize(*child_nodes)
8 | @builder = Builder.new(operator: :or).add(*child_nodes)
9 | super
10 | end
11 | end
12 | end
13 | end
14 | end
15 |
--------------------------------------------------------------------------------
/app/domain/query_builder/specializations/nodes/name_in.rb:
--------------------------------------------------------------------------------
1 | module QueryBuilder
2 | module Specializations
3 | module Nodes
4 | class NameIn < QueryBuilder::Nodes::Wheres::Base
5 | param :names
6 |
7 | def clause
8 | Specialization.where(name: names)
9 | end
10 |
11 | def valid?
12 | names.present?
13 | end
14 | end
15 | end
16 | end
17 | end
18 |
--------------------------------------------------------------------------------
/app/models/specialization.rb:
--------------------------------------------------------------------------------
1 | # == Schema Information
2 | #
3 | # Table name: specializations
4 | #
5 | # id :bigint not null, primary key
6 | # name :string not null
7 | # description :text not null
8 | # properties :json
9 | # created_at :datetime not null
10 | # updated_at :datetime not null
11 | #
12 | class Specialization < ApplicationRecord
13 | end
14 |
--------------------------------------------------------------------------------
/app/domain/query_builder/nodes/joins/includes_references.rb:
--------------------------------------------------------------------------------
1 | module QueryBuilder
2 | module Nodes
3 | module Joins
4 | class IncludesReferences < QueryBuilder::Nodes::Base
5 | include QueryBuilder::Nodes::Simple
6 |
7 | param :table1
8 | param :table2
9 |
10 | def clause
11 | table1.includes(table2).references(table2)
12 | end
13 | end
14 | end
15 | end
16 | end
17 |
--------------------------------------------------------------------------------
/app/models/campaign.rb:
--------------------------------------------------------------------------------
1 | # == Schema Information
2 | #
3 | # Table name: campaigns
4 | #
5 | # id :bigint not null, primary key
6 | # title :string not null
7 | # description :text
8 | # start_date :datetime not null
9 | # created_at :datetime not null
10 | # updated_at :datetime not null
11 | #
12 | class Campaign < ApplicationRecord
13 | has_many :characters
14 | end
15 |
--------------------------------------------------------------------------------
/test/test_helper.rb:
--------------------------------------------------------------------------------
1 | ENV["RAILS_ENV"] ||= "test"
2 | require_relative "../config/environment"
3 | require "rails/test_help"
4 |
5 | class ActiveSupport::TestCase
6 | # Run tests in parallel with specified workers
7 | parallelize(workers: :number_of_processors)
8 |
9 | # Setup all fixtures in test/fixtures/*.yml for all tests in alphabetical order.
10 | fixtures :all
11 |
12 | # Add more helper methods to be used by all tests here...
13 | end
14 |
--------------------------------------------------------------------------------
/app/models/campaigns_player.rb:
--------------------------------------------------------------------------------
1 | # == Schema Information
2 | #
3 | # Table name: campaigns_players
4 | #
5 | # id :bigint not null, primary key
6 | # campaign_id :integer not null
7 | # player_id :integer not null
8 | # created_at :datetime not null
9 | # updated_at :datetime not null
10 | #
11 | class CampaignsPlayer < ApplicationRecord
12 | belongs_to :campaign
13 | belongs_to :player
14 | end
15 |
--------------------------------------------------------------------------------
/app/models/spell.rb:
--------------------------------------------------------------------------------
1 | # == Schema Information
2 | #
3 | # Table name: spells
4 | #
5 | # id :bigint not null, primary key
6 | # name :string not null
7 | # description :text not null
8 | # level :integer default(1), not null
9 | # effects :json
10 | # created_at :datetime not null
11 | # updated_at :datetime not null
12 | #
13 | class Spell < ApplicationRecord
14 | end
15 |
--------------------------------------------------------------------------------
/config/initializers/filter_parameter_logging.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | # Configure parameters to be filtered from the log file. Use this to limit dissemination of
4 | # sensitive information. See the ActiveSupport::ParameterFilter documentation for supported
5 | # notations and behaviors.
6 | Rails.application.config.filter_parameters += [
7 | :passw, :secret, :token, :_key, :crypt, :salt, :certificate, :otp, :ssn
8 | ]
9 |
--------------------------------------------------------------------------------
/config/initializers/permissions_policy.rb:
--------------------------------------------------------------------------------
1 | # Define an application-wide HTTP permissions policy. For further
2 | # information see https://developers.google.com/web/updates/2018/06/feature-policy
3 | #
4 | # Rails.application.config.permissions_policy do |f|
5 | # f.camera :none
6 | # f.gyroscope :none
7 | # f.microphone :none
8 | # f.usb :none
9 | # f.fullscreen :self
10 | # f.payment :self, "https://secure.example.com"
11 | # end
12 |
--------------------------------------------------------------------------------
/config/credentials.yml.enc:
--------------------------------------------------------------------------------
1 | EllA+heQjdKEO9mW4R88BjyUX3AUB0SeQH1CWpNwTW3BJcFllPkjR5BVzPJX2yl7lIZX/wysyLKUB+aF/F6XEXA3rUNv6UzfspYP3apj0+BnD8FB+xTEhqbYdK4zvDzFXZosyZG5q4SGdX83RxjDmej25yYo7E7RuzUMMeUyLHo4IE4yehv6mGu0VOdxDZZy2W8Fw0mLyPShEOdCjINc6jXfaMsWguKKHsGF0JcOlVT7atQ9eVJDty0kUzh8di344LyKUbvmTAERZZqohmUMnMOQ22b0XEZGDViutC2Pc9i5GGjUAhDqTSEcriLqYu7qWrAleyGC6dgncZZ5twp1ekLq4OBHHpWqf9oq6+f/8poLmNYThwm8FoRMjsVU0ueDdVExfFHEqtr+6LpWVDQWukULrd56nP6lLNeO--D4kbu+ll54+5LNQC--tVHv/Ykil04zIc4tEmpXYQ==
--------------------------------------------------------------------------------
/app/models/characters_spell.rb:
--------------------------------------------------------------------------------
1 | # == Schema Information
2 | #
3 | # Table name: characters_spells
4 | #
5 | # id :bigint not null, primary key
6 | # character_id :integer not null
7 | # spell_id :integer not null
8 | # backstory :text
9 | # modifications :json
10 | #
11 | class CharactersSpell < ApplicationRecord
12 | self.table_name = 'characters_spells'
13 |
14 | belongs_to :character
15 | belongs_to :spell
16 | end
17 |
--------------------------------------------------------------------------------
/app/domain/query_builder/builders/default.rb:
--------------------------------------------------------------------------------
1 | module QueryBuilder
2 | module Builders
3 | class Default < ::QueryBuilder::Builders::Base
4 | option :initial_state
5 |
6 | private
7 |
8 | def reduce(&block)
9 | nodes.select(&:valid?).reduce(initial_state, &block)
10 | end
11 |
12 | def base_node_class
13 | ::QueryBuilder::Nodes::Base
14 | end
15 |
16 | def visit(state, node)
17 | node.accept(state)
18 | end
19 | end
20 | end
21 | end
22 |
--------------------------------------------------------------------------------
/app/models/characters_feat.rb:
--------------------------------------------------------------------------------
1 | # == Schema Information
2 | #
3 | # Table name: characters_feats
4 | #
5 | # id :bigint not null, primary key
6 | # feat_id :integer not null
7 | # character_id :integer not null
8 | # backstory :text
9 | # modifications :json
10 | # created_at :datetime not null
11 | # updated_at :datetime not null
12 | #
13 | class CharactersFeat < ApplicationRecord
14 | belongs_to :character
15 | belongs_to :feat
16 | end
17 |
--------------------------------------------------------------------------------
/app/models/skill.rb:
--------------------------------------------------------------------------------
1 | # == Schema Information
2 | #
3 | # Table name: skills
4 | #
5 | # id :bigint not null, primary key
6 | # name :string not null
7 | # description :text not null
8 | # core :boolean
9 | # primary_stat :string
10 | # secondary_stat :string
11 | # tertiary_stat :string
12 | # requirements :json
13 | # created_at :datetime not null
14 | # updated_at :datetime not null
15 | #
16 | class Skill < ApplicationRecord
17 | end
18 |
--------------------------------------------------------------------------------
/app/domain/query_builder/builders/wheres.rb:
--------------------------------------------------------------------------------
1 | module QueryBuilder
2 | module Builders
3 | class Wheres < ::QueryBuilder::Builders::Base
4 | private
5 |
6 | def base_node_class
7 | QueryBuilder::Nodes::Wheres::Base
8 | end
9 |
10 | def reduce(&block)
11 | valid_nodes = nodes.select(&:valid?)
12 | raise 'no valid nodes' if valid_nodes.empty?
13 | head_node, *tail_nodes = valid_nodes
14 | tail_nodes.reduce(head_node.clause, &block)
15 | end
16 | end
17 | end
18 | end
19 |
--------------------------------------------------------------------------------
/app/domain/queries/helpers/joins/filter.rb:
--------------------------------------------------------------------------------
1 | module Queries
2 | module Helpers
3 | module Joins
4 | module Filter
5 | def apply_joins
6 | tables.select { |table_name| filters[table_name].present? }.each do |table_name|
7 | builder.add(::QueryBuilder::Nodes::Joins::InnerJoin.new(initial_state, table_name))
8 | end
9 | end
10 |
11 | def tables
12 | %i(characters flaws virtues feats spells skills specializations)
13 | end
14 | end
15 | end
16 | end
17 | end
18 |
--------------------------------------------------------------------------------
/app/models/characters_skill.rb:
--------------------------------------------------------------------------------
1 | # == Schema Information
2 | #
3 | # Table name: characters_skills
4 | #
5 | # id :bigint not null, primary key
6 | # character_id :integer not null
7 | # skill_id :integer not null
8 | # trained_level :integer
9 | # backstory :text
10 | # modifications :json
11 | # created_at :datetime not null
12 | # updated_at :datetime not null
13 | #
14 | class CharactersSkill < ApplicationRecord
15 | belongs_to :character
16 | belongs_to :skill
17 | end
18 |
--------------------------------------------------------------------------------
/app/domain/query_builder/campaigns/nodes/time_window.rb:
--------------------------------------------------------------------------------
1 | module QueryBuilder
2 | module Campaigns
3 | module Nodes
4 | class TimeWindow < QueryBuilder::Nodes::Wheres::Base
5 | option :start_date_lteq, proc(&:presence)
6 | option :start_date_gteq, proc(&:presence)
7 |
8 | def clause
9 | Campaign.where(start_date: start_date_gteq..start_date_lteq)
10 | end
11 |
12 | def valid?
13 | start_date_gteq.present? || start_date_lteq.present?
14 | end
15 | end
16 | end
17 | end
18 | end
19 |
--------------------------------------------------------------------------------
/app/models/characters_flaw.rb:
--------------------------------------------------------------------------------
1 | # == Schema Information
2 | #
3 | # Table name: characters_flaws
4 | #
5 | # id :bigint not null, primary key
6 | # character_id :integer not null
7 | # flaw_id :integer not null
8 | # permanence :integer default(3)
9 | # backstory :text
10 | # modifications :json
11 | # created_at :datetime not null
12 | # updated_at :datetime not null
13 | #
14 | class CharactersFlaw < ApplicationRecord
15 | belongs_to :character
16 | belongs_to :flaw
17 | end
18 |
--------------------------------------------------------------------------------
/config/initializers/assets.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | # Version of your assets, change this if you want to expire all your assets.
4 | Rails.application.config.assets.version = "1.0"
5 |
6 | # Add additional assets to the asset load path.
7 | # Rails.application.config.assets.paths << Emoji.images_path
8 |
9 | # Precompile additional assets.
10 | # application.js, application.css, and all non-JS/CSS in the app/assets
11 | # folder are already added.
12 | # Rails.application.config.assets.precompile += %w( admin.js admin.css )
13 |
--------------------------------------------------------------------------------
/app/models/characters_virtue.rb:
--------------------------------------------------------------------------------
1 | # == Schema Information
2 | #
3 | # Table name: characters_virtues
4 | #
5 | # id :bigint not null, primary key
6 | # character_id :integer not null
7 | # virtue_id :integer not null
8 | # permanence :integer default(3)
9 | # backstory :text
10 | # modifications :json
11 | # created_at :datetime not null
12 | # updated_at :datetime not null
13 | #
14 | class CharactersVirtue < ApplicationRecord
15 | belongs_to :character
16 | belongs_to :virtue
17 | end
18 |
--------------------------------------------------------------------------------
/spec/factories/characters.rb:
--------------------------------------------------------------------------------
1 | FactoryBot.define do
2 | factory :character, class: 'Character' do
3 | name { Faker::FunnyName.name }
4 | experience { 1000 }
5 | size { 'medium' }
6 | species { 'dwarf' }
7 | strength { 10 }
8 | agility { 10 }
9 | health { 10 }
10 | reasoning { 10 }
11 | memory { 10 }
12 | intuition { 10 }
13 | beauty { 10 }
14 |
15 | trait :legendary do
16 | strength { 18 }
17 | experience { 15000 }
18 | end
19 |
20 | trait :with_campaign do
21 | campaign
22 | end
23 | end
24 | end
25 |
--------------------------------------------------------------------------------
/app/domain/query_builder/nodes/wheres/boolean_operator.rb:
--------------------------------------------------------------------------------
1 | module QueryBuilder
2 | module Nodes
3 | module Wheres
4 | module BooleanOperator
5 | class Builder < QueryBuilder::Builders::Wheres
6 | option :operator
7 |
8 | private
9 |
10 | def visit(state, node)
11 | state.send(operator, node.clause)
12 | end
13 | end
14 |
15 | def valid?
16 | @builder.valid?
17 | end
18 |
19 | def clause
20 | @builder.build
21 | end
22 | end
23 | end
24 | end
25 | end
26 |
--------------------------------------------------------------------------------
/app/models/characters_specialization.rb:
--------------------------------------------------------------------------------
1 | # == Schema Information
2 | #
3 | # Table name: characters_specializations
4 | #
5 | # id :bigint not null, primary key
6 | # character_id :integer not null
7 | # specialization_id :integer not null
8 | # trained_levels :integer default(1), not null
9 | # backstory :text
10 | # modifications :json
11 | #
12 | class CharactersSpecialization < ApplicationRecord
13 | self.table_name = 'characters_specializations'
14 |
15 | belongs_to :character
16 | belongs_to :specialization
17 | end
18 |
--------------------------------------------------------------------------------
/app/models/inventory.rb:
--------------------------------------------------------------------------------
1 | # == Schema Information
2 | #
3 | # Table name: inventories
4 | #
5 | # id :bigint not null, primary key
6 | # character_id :integer
7 | # resource_id :bigint
8 | # resource_type :string
9 | # title :string
10 | # description :text
11 | # weight_limit :float
12 | # item_limit :integer
13 | # special_effects :json
14 | # created_at :datetime not null
15 | # updated_at :datetime not null
16 | #
17 | class Inventory < ApplicationRecord
18 | self.table_name = 'inventories'
19 |
20 | has_many :inventories_items
21 | end
22 |
--------------------------------------------------------------------------------
/app/models/player.rb:
--------------------------------------------------------------------------------
1 | # == Schema Information
2 | #
3 | # Table name: players
4 | #
5 | # id :bigint not null, primary key
6 | # first_name :string
7 | # last_name :string
8 | # middle_name :string
9 | # preferred_name :string
10 | # pronouns :string
11 | # username :string not null
12 | # email :string not null
13 | # created_at :datetime not null
14 | # updated_at :datetime not null
15 | #
16 | class Player < ApplicationRecord
17 | has_many :characters
18 | has_many :campaigns_players
19 | has_many :campaigns, through: :campaigns_players
20 | end
21 |
--------------------------------------------------------------------------------
/config/initializers/inflections.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | # Add new inflection rules using the following format. Inflections
4 | # are locale specific, and you may define rules for as many different
5 | # locales as you wish. All of these examples are active by default:
6 | # ActiveSupport::Inflector.inflections(:en) do |inflect|
7 | # inflect.plural /^(ox)$/i, "\\1en"
8 | # inflect.singular /^(ox)en/i, "\\1"
9 | # inflect.irregular "person", "people"
10 | # inflect.uncountable %w( fish sheep )
11 | # end
12 |
13 | # These inflection rules are supported but not enabled by default:
14 | # ActiveSupport::Inflector.inflections(:en) do |inflect|
15 | # inflect.acronym "RESTful"
16 | # end
17 |
--------------------------------------------------------------------------------
/app/models/inventories_item.rb:
--------------------------------------------------------------------------------
1 | # == Schema Information
2 | #
3 | # Table name: inventories_items
4 | #
5 | # id :bigint not null, primary key
6 | # inventory_id :integer not null
7 | # item_id :integer not null
8 | # equipped :boolean default(FALSE)
9 | # condition :integer default(100)
10 | # modifications :json
11 | # price :decimal(, )
12 | # price_unit :string default("gp")
13 | # weight :float
14 | # created_at :datetime not null
15 | # updated_at :datetime not null
16 | #
17 | class InventoriesItem < ApplicationRecord
18 | self.table_name = 'inventories_items'
19 |
20 | belongs_to :inventory
21 | belongs_to :item
22 | end
23 |
--------------------------------------------------------------------------------
/spec/queries/shared_context/campaigns.rb:
--------------------------------------------------------------------------------
1 | RSpec.shared_context 'with common database records' do
2 | let!(:specified_campaign) { create(:campaign, title: 'The adventures of Stinky') }
3 | let!(:generic_campaign) { create(:campaign) }
4 | let!(:old_campaign) { create(:campaign, start_date: 2.years.ago) }
5 | let!(:older_campaign) { create(:campaign, start_date: 10.years.ago) }
6 | let!(:characters_campaign) { create(:campaign) }
7 |
8 | let!(:normal_character) { create(:character, campaign: characters_campaign) }
9 | let!(:legendary_character) { create(:character, :legendary, campaign: characters_campaign) }
10 | let(:all_campaigns) do
11 | [specified_campaign, generic_campaign, old_campaign, older_campaign, characters_campaign]
12 | end
13 | end
14 |
--------------------------------------------------------------------------------
/app/assets/stylesheets/application.css:
--------------------------------------------------------------------------------
1 | /*
2 | * This is a manifest file that'll be compiled into application.css, which will include all the files
3 | * listed below.
4 | *
5 | * Any CSS (and SCSS, if configured) file within this directory, lib/assets/stylesheets, or any plugin's
6 | * vendor/assets/stylesheets directory can be referenced here using a relative path.
7 | *
8 | * You're free to add application-wide styles to this file and they'll appear at the bottom of the
9 | * compiled file so the styles you add here take precedence over styles defined in any other CSS
10 | * files in this directory. Styles in this file should be added after the last require_* statement.
11 | * It is generally better to create a new file per style scope.
12 | *
13 | *= require_tree .
14 | *= require_self
15 | */
16 |
--------------------------------------------------------------------------------
/app/domain/query_builder/characters/nodes/buff.rb:
--------------------------------------------------------------------------------
1 | module QueryBuilder
2 | module Characters
3 | module Nodes
4 | class Buff < ::QueryBuilder::Nodes::Wheres::Base
5 | include Queries::Helpers::Wheres
6 | attr_reader :builder
7 |
8 | # rubocop:disable Lint/MissingSuper
9 | def initialize
10 | @builder = QueryBuilder::Builders::Wheres.new
11 | end
12 | # rubocop:enable Lint/MissingSuper
13 |
14 | def clause
15 | builder.add(wheres_operator(:or, *child_nodes))
16 | builder.build
17 | end
18 |
19 | def child_nodes
20 | %w(strength health agility reasoning memory intuition beauty).map do |stat_name|
21 | QueryBuilder::Characters::Nodes::EliteStat.new(stat_name)
22 | end
23 | end
24 | end
25 | end
26 | end
27 | end
28 |
--------------------------------------------------------------------------------
/app/domain/queries/base.rb:
--------------------------------------------------------------------------------
1 | module Queries
2 | class Base
3 | extend Dry::Initializer
4 | include Queries::Helpers::Joins::Filter
5 | include Queries::Helpers::Wheres
6 |
7 | option :builder, default: proc { ::QueryBuilder::Builders::Default.new(initial_state: initial_state) }
8 | option :filters, type: ::Types::Hash.constructor(&:to_h), default: proc { {} }
9 |
10 | def call
11 | apply
12 | builder.build
13 | end
14 |
15 | private
16 |
17 | def initial_state
18 | raise NotImplementedError
19 | end
20 |
21 | def apply
22 | raise NotImplementedError
23 | end
24 |
25 | def left_outer_join(model, table_name)
26 | QueryBuilder::Nodes::Joins::LeftOuterJoin.new(model, table_name)
27 | end
28 |
29 | def inner_join(model, table_name)
30 | QueryBuilder::Nodes::Joins::InnerJoin.new(model, table_name)
31 | end
32 | end
33 | end
34 |
--------------------------------------------------------------------------------
/app/models/feat.rb:
--------------------------------------------------------------------------------
1 | # == Schema Information
2 | #
3 | # Table name: feats
4 | #
5 | # id :bigint not null, primary key
6 | # name :string not null
7 | # description :text not null
8 | # modify_ac :integer default(0)
9 | # modify_hp :integer default(0)
10 | # modify_age :integer default(0)
11 | # modify_stamina :integer default(0)
12 | # modify_strength :integer default(0)
13 | # modify_agility :integer default(0)
14 | # modify_health :integer default(0)
15 | # modify_reasoning :integer default(0)
16 | # modify_memory :integer default(0)
17 | # modify_intuition :integer default(0)
18 | # modify_beauty :integer default(0)
19 | # created_at :datetime not null
20 | # updated_at :datetime not null
21 | #
22 | class Feat < ApplicationRecord
23 | end
24 |
--------------------------------------------------------------------------------
/app/domain/query_builder/builders/base.rb:
--------------------------------------------------------------------------------
1 | module QueryBuilder
2 | module Builders
3 | class Base
4 | extend Dry::Initializer
5 |
6 | option :nodes, type: ::Types::Array, default: proc { [] }
7 |
8 | def add(*nodes)
9 | nodes.each do |node|
10 | raise ArgumentError, "node #{node.inspect} is not a #{base_node_class}" unless node.is_a?(base_node_class)
11 | @nodes << node
12 | end
13 | self
14 | end
15 |
16 | def build
17 | reduce do |state, node|
18 | visit(state, node)
19 | end
20 | end
21 |
22 | def valid?
23 | nodes.any?(&:valid?)
24 | end
25 |
26 | private
27 |
28 | def reduce
29 | raise NotImplementedError
30 | end
31 |
32 | def base_node_class
33 | raise NotImplementedError
34 | end
35 |
36 | def visit(_state, _node)
37 | raise NotImplementedError
38 | end
39 | end
40 | end
41 | end
42 |
--------------------------------------------------------------------------------
/app/domain/query_builder/characters/nodes/legendary.rb:
--------------------------------------------------------------------------------
1 | module QueryBuilder
2 | module Characters
3 | module Nodes
4 | class Legendary < QueryBuilder::Nodes::Wheres::Base
5 | include Queries::Helpers::Wheres
6 |
7 | attr_reader :builder, :legendary
8 |
9 | # rubocop:disable Lint/MissingSuper
10 | def initialize(legendary)
11 | @legendary = legendary
12 | @builder = QueryBuilder::Builders::Wheres.new
13 | end
14 | # rubocop:enable Lint/MissingSuper
15 |
16 | def clause
17 | builder.add(wheres_operator(:and, *and_filters))
18 | builder.build
19 | end
20 |
21 | def and_filters
22 | [
23 | QueryBuilder::Characters::Nodes::Veteran.new,
24 | QueryBuilder::Characters::Nodes::Buff.new
25 | ]
26 | end
27 |
28 | def valid?
29 | legendary.present?
30 | end
31 | end
32 | end
33 | end
34 | end
35 |
--------------------------------------------------------------------------------
/config/locales/en.yml:
--------------------------------------------------------------------------------
1 | # Files in the config/locales directory are used for internationalization
2 | # and are automatically loaded by Rails. If you want to use locales other
3 | # than English, add the necessary files in this directory.
4 | #
5 | # To use the locales, use `I18n.t`:
6 | #
7 | # I18n.t "hello"
8 | #
9 | # In views, this is aliased to just `t`:
10 | #
11 | # <%= t("hello") %>
12 | #
13 | # To use a different locale, set it with `I18n.locale`:
14 | #
15 | # I18n.locale = :es
16 | #
17 | # This would use the information in config/locales/es.yml.
18 | #
19 | # The following keys must be escaped otherwise they will not be retrieved by
20 | # the default I18n backend:
21 | #
22 | # true, false, on, off, yes, no
23 | #
24 | # Instead, surround them with single quotes.
25 | #
26 | # en:
27 | # "true": "foo"
28 | #
29 | # To learn more, please read the Rails Internationalization guide
30 | # available at https://guides.rubyonrails.org/i18n.html.
31 |
32 | en:
33 | hello: "Hello world"
34 |
--------------------------------------------------------------------------------
/app/domain/queries/characters/character_spells_query.rb:
--------------------------------------------------------------------------------
1 | module Queries
2 | module Characters
3 | class CharacterSpellsQuery < Queries::Base
4 | option :filters, type: ::Types::Hash.constructor { |v| v.to_h }, default: proc { {} }
5 | option :limit, type: ::Types::Integer, default: proc { 1000 }
6 |
7 | def apply
8 | builder
9 | .add(QueryBuilder::Nodes::Joins::InnerJoin.new(initial_state, :spells))
10 | .add(wheres_operator(:and, *and_filters))
11 | .add(QueryBuilder::Nodes::Limit.new(limit))
12 | end
13 |
14 | private
15 |
16 | def initial_state
17 | ::Character
18 | end
19 |
20 | def and_filters
21 | [
22 | QueryBuilder::Characters::Nodes::IdIn.new(filters[:ids]),
23 | QueryBuilder::Spells::Nodes::LevelIn.new(spell_filters[:levels])
24 | ]
25 | end
26 |
27 | def spell_filters
28 | filters[:spells] || {}
29 | end
30 | end
31 | end
32 | end
33 |
--------------------------------------------------------------------------------
/app/models/flaw.rb:
--------------------------------------------------------------------------------
1 | # == Schema Information
2 | #
3 | # Table name: flaws
4 | #
5 | # id :bigint not null, primary key
6 | # title :string not null
7 | # description :text not null
8 | # magnitude :string
9 | # modify_ac :integer default(0)
10 | # modify_hp :integer default(0)
11 | # modify_age :integer default(0)
12 | # modify_stamina :integer default(0)
13 | # modify_strength :integer default(0)
14 | # modify_agility :integer default(0)
15 | # modify_health :integer default(0)
16 | # modify_reasoning :integer default(0)
17 | # modify_memory :integer default(0)
18 | # modify_intuition :integer default(0)
19 | # modify_beauty :integer default(0)
20 | # special_effects :json
21 | # created_at :datetime not null
22 | # updated_at :datetime not null
23 | #
24 | class Flaw < ApplicationRecord
25 | end
26 |
--------------------------------------------------------------------------------
/app/models/virtue.rb:
--------------------------------------------------------------------------------
1 | # == Schema Information
2 | #
3 | # Table name: virtues
4 | #
5 | # id :bigint not null, primary key
6 | # title :string not null
7 | # description :text not null
8 | # magnitude :string
9 | # modify_ac :integer default(0)
10 | # modify_hp :integer default(0)
11 | # modify_age :integer default(0)
12 | # modify_stamina :integer default(0)
13 | # modify_strength :integer default(0)
14 | # modify_agility :integer default(0)
15 | # modify_health :integer default(0)
16 | # modify_reasoning :integer default(0)
17 | # modify_memory :integer default(0)
18 | # modify_intuition :integer default(0)
19 | # modify_beauty :integer default(0)
20 | # special_effects :json
21 | # created_at :datetime not null
22 | # updated_at :datetime not null
23 | #
24 | class Virtue < ApplicationRecord
25 | end
26 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2023 G2
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/bin/setup:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 | require "fileutils"
3 |
4 | # path to your application root.
5 | APP_ROOT = File.expand_path("..", __dir__)
6 |
7 | def system!(*args)
8 | system(*args) || abort("\n== Command #{args} failed ==")
9 | end
10 |
11 | FileUtils.chdir APP_ROOT do
12 | # This script is a way to set up or update your development environment automatically.
13 | # This script is idempotent, so that you can run it at any time and get an expectable outcome.
14 | # Add necessary setup steps to this file.
15 |
16 | puts "== Installing dependencies =="
17 | system! "gem install bundler --conservative"
18 | system("bundle check") || system!("bundle install")
19 |
20 | # puts "\n== Copying sample files =="
21 | # unless File.exist?("config/database.yml")
22 | # FileUtils.cp "config/database.yml.sample", "config/database.yml"
23 | # end
24 |
25 | puts "\n== Preparing database =="
26 | system! "bin/rails db:prepare"
27 |
28 | puts "\n== Removing old logs and tempfiles =="
29 | system! "bin/rails log:clear tmp:clear"
30 |
31 | puts "\n== Restarting application server =="
32 | system! "bin/rails restart"
33 | end
34 |
--------------------------------------------------------------------------------
/config/initializers/content_security_policy.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | # Define an application-wide content security policy.
4 | # See the Securing Rails Applications Guide for more information:
5 | # https://guides.rubyonrails.org/security.html#content-security-policy-header
6 |
7 | # Rails.application.configure do
8 | # config.content_security_policy do |policy|
9 | # policy.default_src :self, :https
10 | # policy.font_src :self, :https, :data
11 | # policy.img_src :self, :https, :data
12 | # policy.object_src :none
13 | # policy.script_src :self, :https
14 | # policy.style_src :self, :https
15 | # # Specify URI for violation reports
16 | # # policy.report_uri "/csp-violation-report-endpoint"
17 | # end
18 | #
19 | # # Generate session nonces for permitted importmap and inline scripts
20 | # config.content_security_policy_nonce_generator = ->(request) { request.session.id.to_s }
21 | # config.content_security_policy_nonce_directives = %w(script-src)
22 | #
23 | # # Report violations without enforcing the policy.
24 | # # config.content_security_policy_report_only = true
25 | # end
26 |
--------------------------------------------------------------------------------
/Gemfile:
--------------------------------------------------------------------------------
1 | source "https://rubygems.org"
2 | git_source(:github) { |repo| "https://github.com/#{repo}.git" }
3 |
4 | ruby "3.2.0"
5 |
6 | # Bundle edge Rails instead: gem "rails", github: "rails/rails", branch: "main"
7 | gem "rails", "~> 7.0.4", ">= 7.0.4.3"
8 |
9 | # The original asset pipeline for Rails [https://github.com/rails/sprockets-rails]
10 | gem "sprockets-rails"
11 |
12 | # Use postgresql as the database for Active Record
13 | gem "pg", "~> 1.1"
14 |
15 | # Use the Puma web server [https://github.com/puma/puma]
16 | gem "puma", "~> 5.0"
17 |
18 | # Windows does not include zoneinfo files, so bundle the tzinfo-data gem
19 | gem "tzinfo-data", platforms: %i[ mingw mswin x64_mingw jruby ]
20 |
21 | gem 'dry-container'
22 | gem 'dry-initializer-rails', require: 'dry-initializer-rails'
23 | gem 'dry-types'
24 |
25 | group :development, :test do
26 | # See https://guides.rubyonrails.org/debugging_rails_applications.html#debugging-with-the-debug-gem
27 | gem "debug", platforms: %i[ mri mingw x64_mingw ]
28 | gem 'faker'
29 | gem 'pry'
30 | gem 'pry-rails'
31 | gem 'rspec-rails'
32 | gem 'factory_bot_rails'
33 | end
34 |
35 | group :development do
36 | gem 'annotate'
37 | # Speed up commands on slow machines / big apps [https://github.com/rails/spring]
38 | # gem "spring"
39 | end
40 |
41 |
--------------------------------------------------------------------------------
/config/application.rb:
--------------------------------------------------------------------------------
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 Rails2023Showcase
22 | class Application < Rails::Application
23 | # Initialize configuration defaults for originally generated Rails version.
24 | config.load_defaults 7.0
25 |
26 | # Configuration for the application, engines, and railties goes here.
27 | #
28 | # These settings can be overridden in specific environments using the files
29 | # in config/environments, which are processed later.
30 | #
31 | # config.time_zone = "Central Time (US & Canada)"
32 | # config.eager_load_paths << Rails.root.join("extras")
33 |
34 | # Don't generate system test files.
35 | config.generators.system_tests = nil
36 | end
37 | end
38 |
--------------------------------------------------------------------------------
/app/models/item.rb:
--------------------------------------------------------------------------------
1 | # == Schema Information
2 | #
3 | # Table name: items
4 | #
5 | # id :bigint not null, primary key
6 | # name :string not null
7 | # description :text
8 | # hardness :integer
9 | # durability :integer
10 | # weight :float
11 | # size :string
12 | # modify_ac :integer default(0)
13 | # modify_hp :integer default(0)
14 | # modify_age :integer default(0)
15 | # modify_stamina :integer default(0)
16 | # modify_strength :integer default(0)
17 | # modify_agility :integer default(0)
18 | # modify_health :integer default(0)
19 | # modify_reasoning :integer default(0)
20 | # modify_memory :integer default(0)
21 | # modify_intuition :integer default(0)
22 | # modify_beauty :integer default(0)
23 | # equippable :boolean default(FALSE)
24 | # consummable :boolean default(FALSE)
25 | # weapon :boolean default(FALSE)
26 | # armor :boolean default(FALSE)
27 | # special_effects :json
28 | # requirements :json
29 | # created_at :datetime not null
30 | # updated_at :datetime not null
31 | #
32 | class Item < ApplicationRecord
33 | end
34 |
--------------------------------------------------------------------------------
/spec/queries/campaigns/index_query_spec.rb:
--------------------------------------------------------------------------------
1 | require 'rails_helper'
2 | require_relative '../shared_context/campaigns'
3 |
4 | describe Queries::Campaigns::IndexQuery do
5 | subject(:query) { described_class.new filters: }
6 |
7 | let(:filters) do
8 | {
9 | characters: characters_filters
10 | }
11 | end
12 | let(:characters_filters) { {} }
13 |
14 | include_context 'with common database records'
15 |
16 | describe '#call' do
17 | context 'with no filters' do
18 | it 'returns a list of campaigns up to the limit size' do
19 | expect(query.call).to eq all_campaigns
20 | end
21 | end
22 |
23 | context 'with a specific time window of campaigns' do
24 | let(:filters) do
25 | {
26 | start_date_gteq: 1.day.ago,
27 | characters: characters_filters
28 | }
29 | end
30 |
31 | it 'returns only campaigns with the time window' do
32 | expect(query.call).to eq(all_campaigns - [old_campaign, older_campaign])
33 | end
34 | end
35 |
36 | context 'with a filter for campaigns that include legendary characters' do
37 | let(:characters_filters) { { legendary: true } }
38 |
39 | it 'returns only campaigns that include a legendary character' do
40 | expect(query.call).to eq [characters_campaign]
41 | end
42 | end
43 | end
44 | end
45 |
--------------------------------------------------------------------------------
/app/domain/queries/campaigns/index_query.rb:
--------------------------------------------------------------------------------
1 | module Queries
2 | module Campaigns
3 | class IndexQuery < Queries::Base
4 |
5 | option :filters, type: ::Types::Hash.constructor { |v| v.to_h }, default: proc { {} }
6 | option :limit, type: ::Types::Integer, default: proc { 50 }
7 | option :order_by, type: ::Types::String, default: proc { 'id ASC' }
8 |
9 | def apply
10 | apply_joins
11 | builder
12 | .add(QueryBuilder::Nodes::SelectDistinct.new(initial_state, %i(id title)))
13 | .add(wheres_operator(:and, *and_filters))
14 | .add(QueryBuilder::Nodes::OrderBy.new(order_by))
15 | .add(QueryBuilder::Nodes::Limit.new(limit))
16 | end
17 |
18 | def initial_state
19 | ::Campaign
20 | end
21 |
22 | private
23 |
24 | def and_filters
25 | [
26 | QueryBuilder::Campaigns::Nodes::TitleIn.new(filters[:titles]),
27 | QueryBuilder::Campaigns::Nodes::TimeWindow.new(**time_window),
28 | QueryBuilder::Characters::Nodes::Legendary.new(characters_filters[:legendary])
29 | ]
30 | end
31 |
32 | def characters_filters
33 | filters[:characters] || {}
34 | end
35 |
36 | def time_window
37 | { start_date_lteq: filters[:start_date_lteq], start_date_gteq: filters[:start_date_gteq] }
38 | end
39 | end
40 | end
41 | end
42 |
--------------------------------------------------------------------------------
/spec/queries/shared_context/characters.rb:
--------------------------------------------------------------------------------
1 | RSpec.shared_context 'with full character suite' do
2 | let(:newbie) { create(:character) }
3 | let(:spellcaster) { create(:character) }
4 | let(:flawed_ranger) { create(:character) }
5 | let(:virtuous_paladin) { create(:character) }
6 | let(:dungeoneer) { create(:character) }
7 | let(:the_legend) { create(:character, :legendary) }
8 |
9 | let!(:all_characters) do
10 | [newbie, spellcaster, flawed_ranger, virtuous_paladin, dungeoneer, the_legend]
11 | end
12 |
13 | let(:greater_spell) { create(:spell, name: 'greater healing', level: 4) }
14 | let(:lesser_spell) { create(:spell, name: 'lesser healing', level: 2) }
15 | let(:dungeoneer_feat) { create(:feat, name: 'dungeoneering') }
16 | let(:virtue) { create(:virtue, title: 'great strength') }
17 | let(:flaw) { create(:flaw, title: 'forgetful') }
18 | let(:specialization) { create(:specialization, name: 'Ranger') }
19 |
20 | let!(:characters_spell1) { CharactersSpell.create(character: spellcaster, spell: greater_spell) }
21 | let!(:characters_spell2) { CharactersSpell.create(character: the_legend, spell: lesser_spell) }
22 | let!(:characters_feat) { CharactersFeat.create(character: dungeoneer, feat: dungeoneer_feat) }
23 | let!(:characters_virtue) { CharactersVirtue.create(character: virtuous_paladin, virtue: virtue) }
24 | let!(:characters_flaw) { CharactersFlaw.create(character: flawed_ranger, flaw: flaw) }
25 | let!(:characters_specialization) { CharactersSpecialization.create(character: flawed_ranger, specialization:) }
26 | end
27 |
--------------------------------------------------------------------------------
/app/domain/queries/characters/index_query.rb:
--------------------------------------------------------------------------------
1 | module Queries
2 | module Characters
3 | class IndexQuery < Queries::Base
4 | option :limit, type: ::Types::Integer, default: proc { 50 }
5 | option :order_by, type: ::Types::String, default: proc { 'name ASC' }
6 |
7 | private
8 |
9 | def apply
10 | apply_joins
11 | builder
12 | .add(wheres_operator(:and, *and_filters))
13 | .add(QueryBuilder::Nodes::OrderBy.new(order_by))
14 | .add(QueryBuilder::Nodes::Limit.new(limit))
15 | end
16 |
17 | def initial_state
18 | ::Character
19 | end
20 |
21 | def and_filters
22 | [
23 | QueryBuilder::Spells::Nodes::LevelIn.new(spells_filters[:levels]),
24 | QueryBuilder::Spells::Nodes::NameIn.new(spells_filters[:names]),
25 | QueryBuilder::Flaws::Nodes::TitleIn.new(flaws_filters[:titles]),
26 | QueryBuilder::Virtues::Nodes::TitleIn.new(virtues_filters[:titles]),
27 | QueryBuilder::Feats::Nodes::NameIn.new(feats_filters[:names]),
28 | QueryBuilder::Specializations::Nodes::NameIn.new(specializations_filters[:names]),
29 | QueryBuilder::Characters::Nodes::Legendary.new(filters[:legendary])
30 | ]
31 | end
32 |
33 | def spells_filters
34 | filters[:spells] || {}
35 | end
36 |
37 | def flaws_filters
38 | filters[:flaws] || {}
39 | end
40 |
41 | def virtues_filters
42 | filters[:virtues] || {}
43 | end
44 |
45 | def feats_filters
46 | filters[:feats] || {}
47 | end
48 |
49 | def specializations_filters
50 | filters[:specializations] || {}
51 | end
52 | end
53 | end
54 | end
55 |
--------------------------------------------------------------------------------
/app/models/character.rb:
--------------------------------------------------------------------------------
1 | # == Schema Information
2 | #
3 | # Table name: characters
4 | #
5 | # id :bigint not null, primary key
6 | # name :string not null
7 | # version :integer default(1), not null
8 | # origin_story :text
9 | # player_id :integer
10 | # campaign_id :integer
11 | # age :integer
12 | # gender :string
13 | # size :string not null
14 | # species :string not null
15 | # height :float
16 | # weight :float
17 | # experience :integer
18 | # strength :integer not null
19 | # agility :integer not null
20 | # health :integer not null
21 | # reasoning :integer not null
22 | # memory :integer not null
23 | # intuition :integer not null
24 | # beauty :integer not null
25 | # created_at :datetime not null
26 | # updated_at :datetime not null
27 | #
28 | class Character < ApplicationRecord
29 | belongs_to :campaign, dependent: :destroy, optional: true
30 | belongs_to :players, dependent: :destroy, optional: true
31 | has_many :characters_spells
32 | has_many :spells, through: :characters_spells
33 | has_many :characters_feats
34 | has_many :feats, through: :characters_feats
35 | has_many :characters_flaws
36 | has_many :flaws, through: :characters_flaws
37 | has_many :characters_virtues
38 | has_many :virtues, through: :characters_virtues
39 | has_many :characters_skills
40 | has_many :skills, through: :characters_skills
41 | has_many :characters_specializations
42 | has_many :specializations, through: :characters_specializations
43 | has_one :inventory
44 | has_many :inventories_items, through: :inventory
45 | has_many :items, through: :inventories_items
46 | end
47 |
--------------------------------------------------------------------------------
/public/500.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | We're sorry, but something went wrong (500)
5 |
6 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
We're sorry, but something went wrong.
62 |
63 |
If you are the application owner check the logs for more information.
64 |
65 |
66 |
67 |
--------------------------------------------------------------------------------
/config/puma.rb:
--------------------------------------------------------------------------------
1 | # Puma can serve each request in a thread from an internal thread pool.
2 | # The `threads` method setting takes two numbers: a minimum and maximum.
3 | # Any libraries that use thread pools should be configured to match
4 | # the maximum value specified for Puma. Default is set to 5 threads for minimum
5 | # and maximum; this matches the default thread size of Active Record.
6 | #
7 | max_threads_count = ENV.fetch("RAILS_MAX_THREADS") { 5 }
8 | min_threads_count = ENV.fetch("RAILS_MIN_THREADS") { max_threads_count }
9 | threads min_threads_count, max_threads_count
10 |
11 | # Specifies the `worker_timeout` threshold that Puma will use to wait before
12 | # terminating a worker in development environments.
13 | #
14 | worker_timeout 3600 if ENV.fetch("RAILS_ENV", "development") == "development"
15 |
16 | # Specifies the `port` that Puma will listen on to receive requests; default is 3000.
17 | #
18 | port ENV.fetch("PORT") { 3000 }
19 |
20 | # Specifies the `environment` that Puma will run in.
21 | #
22 | environment ENV.fetch("RAILS_ENV") { "development" }
23 |
24 | # Specifies the `pidfile` that Puma will use.
25 | pidfile ENV.fetch("PIDFILE") { "tmp/pids/server.pid" }
26 |
27 | # Specifies the number of `workers` to boot in clustered mode.
28 | # Workers are forked web server processes. If using threads and workers together
29 | # the concurrency of the application would be max `threads` * `workers`.
30 | # Workers do not work on JRuby or Windows (both of which do not support
31 | # processes).
32 | #
33 | # workers ENV.fetch("WEB_CONCURRENCY") { 2 }
34 |
35 | # Use the `preload_app!` method when specifying a `workers` number.
36 | # This directive tells Puma to first boot the application and load code
37 | # before forking the application. This takes advantage of Copy On Write
38 | # process behavior so workers use less memory.
39 | #
40 | # preload_app!
41 |
42 | # Allow puma to be restarted by `bin/rails restart` command.
43 | plugin :tmp_restart
44 |
--------------------------------------------------------------------------------
/public/422.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | The change you wanted was rejected (422)
5 |
6 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
The change you wanted was rejected.
62 |
Maybe you tried to change something you didn't have access to.
63 |
64 |
If you are the application owner check the logs for more information.
65 |
66 |
67 |
68 |
--------------------------------------------------------------------------------
/public/404.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | The page you were looking for doesn't exist (404)
5 |
6 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
The page you were looking for doesn't exist.
62 |
You may have mistyped the address or the page may have moved.
63 |
64 |
If you are the application owner check the logs for more information.
65 |
66 |
67 |
68 |
--------------------------------------------------------------------------------
/config/environments/test.rb:
--------------------------------------------------------------------------------
1 | require "active_support/core_ext/integer/time"
2 |
3 | # The test environment is used exclusively to run your application's
4 | # test suite. You never need to work with it otherwise. Remember that
5 | # your test database is "scratch space" for the test suite and is wiped
6 | # and recreated between test runs. Don't rely on the data there!
7 |
8 | Rails.application.configure do
9 | # Settings specified here will take precedence over those in config/application.rb.
10 |
11 | # Turn false under Spring and add config.action_view.cache_template_loading = true.
12 | config.cache_classes = true
13 |
14 | # Eager loading loads your whole application. When running a single test locally,
15 | # this probably isn't necessary. It's a good idea to do in a continuous integration
16 | # system, or in some way before deploying your code.
17 | config.eager_load = ENV["CI"].present?
18 |
19 | # Configure public file server for tests with Cache-Control for performance.
20 | config.public_file_server.enabled = true
21 | config.public_file_server.headers = {
22 | "Cache-Control" => "public, max-age=#{1.hour.to_i}"
23 | }
24 |
25 | # Show full error reports and disable caching.
26 | config.consider_all_requests_local = true
27 | config.action_controller.perform_caching = false
28 | config.cache_store = :null_store
29 |
30 | # Raise exceptions instead of rendering exception templates.
31 | config.action_dispatch.show_exceptions = false
32 |
33 | # Disable request forgery protection in test environment.
34 | config.action_controller.allow_forgery_protection = false
35 |
36 | # Print deprecation notices to the stderr.
37 | config.active_support.deprecation = :stderr
38 |
39 | # Raise exceptions for disallowed deprecations.
40 | config.active_support.disallowed_deprecation = :raise
41 |
42 | # Tell Active Support which deprecation messages to disallow.
43 | config.active_support.disallowed_deprecation_warnings = []
44 |
45 | # Raises error for missing translations.
46 | # config.i18n.raise_on_missing_translations = true
47 |
48 | # Annotate rendered view with file names.
49 | # config.action_view.annotate_rendered_view_with_filenames = true
50 | end
51 |
--------------------------------------------------------------------------------
/config/environments/development.rb:
--------------------------------------------------------------------------------
1 | require "active_support/core_ext/integer/time"
2 |
3 | Rails.application.configure do
4 | # Settings specified here will take precedence over those in config/application.rb.
5 |
6 | # In the development environment your application's code is reloaded any time
7 | # it changes. This slows down response time but is perfect for development
8 | # since you don't have to restart the web server when you make code changes.
9 | config.cache_classes = false
10 |
11 | # Do not eager load code on boot.
12 | config.eager_load = false
13 |
14 | # Show full error reports.
15 | config.consider_all_requests_local = true
16 |
17 | # Enable server timing
18 | config.server_timing = true
19 |
20 | # Enable/disable caching. By default caching is disabled.
21 | # Run rails dev:cache to toggle caching.
22 | if Rails.root.join("tmp/caching-dev.txt").exist?
23 | config.action_controller.perform_caching = true
24 | config.action_controller.enable_fragment_cache_logging = true
25 |
26 | config.cache_store = :memory_store
27 | config.public_file_server.headers = {
28 | "Cache-Control" => "public, max-age=#{2.days.to_i}"
29 | }
30 | else
31 | config.action_controller.perform_caching = false
32 |
33 | config.cache_store = :null_store
34 | end
35 |
36 | # Print deprecation notices to the Rails logger.
37 | config.active_support.deprecation = :log
38 |
39 | # Raise exceptions for disallowed deprecations.
40 | config.active_support.disallowed_deprecation = :raise
41 |
42 | # Tell Active Support which deprecation messages to disallow.
43 | config.active_support.disallowed_deprecation_warnings = []
44 |
45 | # Raise an error on page load if there are pending migrations.
46 | config.active_record.migration_error = :page_load
47 |
48 | # Highlight code that triggered database queries in logs.
49 | config.active_record.verbose_query_logs = true
50 |
51 | # Suppress logger output for asset requests.
52 | config.assets.quiet = true
53 |
54 | # Raises error for missing translations.
55 | # config.i18n.raise_on_missing_translations = true
56 |
57 | # Annotate rendered view with file names.
58 | # config.action_view.annotate_rendered_view_with_filenames = true
59 |
60 | # Uncomment if you wish to allow Action Cable access from any origin.
61 | # config.action_cable.disable_request_forgery_protection = true
62 | end
63 |
--------------------------------------------------------------------------------
/spec/queries/characters/index_query_spec.rb:
--------------------------------------------------------------------------------
1 | require 'rails_helper'
2 | require_relative '../shared_context/characters'
3 |
4 | describe Queries::Characters::IndexQuery do
5 | subject(:query) { described_class.new filters: }
6 |
7 | let(:filters) do
8 | {}
9 | end
10 |
11 | include_context 'with full character suite'
12 |
13 | describe '#call' do
14 | context 'with no filters' do
15 | it 'returns an ActiveRecord Relation of characters up to the limit size' do
16 | expect(query.call).to include(*all_characters)
17 | end
18 | end
19 |
20 | context 'with spells filters' do
21 | let(:filters) do
22 | { spells: { names: ['greater healing', 'lesser healing'] } }
23 | end
24 |
25 | it 'returns only characters that have one of the spells' do
26 | expect(query.call).to contain_exactly(spellcaster, the_legend)
27 | end
28 | end
29 |
30 | context 'with flaws filters' do
31 | let(:filters) do
32 | { flaws: { titles: ['forgetful'] } }
33 | end
34 |
35 | it 'returns only characters that have one of the flaws' do
36 | expect(query.call).to contain_exactly(flawed_ranger)
37 | end
38 | end
39 |
40 | context 'with virtues filters' do
41 | let(:filters) do
42 | { virtues: { titles: ['great strength'] } }
43 | end
44 |
45 | it 'returns only characters that have one of the virtues' do
46 | expect(query.call).to contain_exactly(virtuous_paladin)
47 | end
48 | end
49 |
50 | context 'with feats filters' do
51 | let(:filters) do
52 | { feats: { names: ['dungeoneering'] } }
53 | end
54 |
55 | it 'returns only characters that have one of the feats' do
56 | expect(query.call).to contain_exactly(dungeoneer)
57 | end
58 | end
59 |
60 | context 'with specializations filters' do
61 | let(:filters) do
62 | { specializations: { names: ['Ranger'] } }
63 | end
64 |
65 | it 'returns only characters that have one of the feats' do
66 | expect(query.call).to contain_exactly(flawed_ranger)
67 | end
68 | end
69 |
70 | context 'with legendary filter' do
71 | let(:filters) do
72 | { legendary: true }
73 | end
74 |
75 | it 'returns only legendary characters' do
76 | expect(query.call).to contain_exactly(the_legend)
77 | end
78 | end
79 | end
80 | end
81 |
--------------------------------------------------------------------------------
/spec/rails_helper.rb:
--------------------------------------------------------------------------------
1 | # This file is copied to spec/ when you run 'rails generate rspec:install'
2 | require 'spec_helper'
3 | ENV['RAILS_ENV'] ||= 'test'
4 | require_relative '../config/environment'
5 | # Prevent database truncation if the environment is production
6 | abort("The Rails environment is running in production mode!") if Rails.env.production?
7 | require 'rspec/rails'
8 | # Add additional requires below this line. Rails is not loaded until this point!
9 |
10 | # Requires supporting ruby files with custom matchers and macros, etc, in
11 | # spec/support/ and its subdirectories. Files matching `spec/**/*_spec.rb` are
12 | # run as spec files by default. This means that files in spec/support that end
13 | # in _spec.rb will both be required and run as specs, causing the specs to be
14 | # run twice. It is recommended that you do not name files matching this glob to
15 | # end with _spec.rb. You can configure this pattern with the --pattern
16 | # option on the command line or in ~/.rspec, .rspec or `.rspec-local`.
17 | #
18 | # The following line is provided for convenience purposes. It has the downside
19 | # of increasing the boot-up time by auto-requiring all files in the support
20 | # directory. Alternatively, in the individual `*_spec.rb` files, manually
21 | # require only the support files necessary.
22 | #
23 | # Dir[Rails.root.join('spec', 'support', '**', '*.rb')].sort.each { |f| require f }
24 |
25 | # Checks for pending migrations and applies them before tests are run.
26 | # If you are not using ActiveRecord, you can remove these lines.
27 | begin
28 | ActiveRecord::Migration.maintain_test_schema!
29 | rescue ActiveRecord::PendingMigrationError => e
30 | abort e.to_s.strip
31 | end
32 | RSpec.configure do |config|
33 | # Remove this line if you're not using ActiveRecord or ActiveRecord fixtures
34 | config.fixture_path = "#{::Rails.root}/spec/fixtures"
35 |
36 | # If you're not using ActiveRecord, or you'd prefer not to run each of your
37 | # examples within a transaction, remove the following line or assign false
38 | # instead of true.
39 | config.use_transactional_fixtures = true
40 |
41 | # You can uncomment this line to turn off ActiveRecord support entirely.
42 | # config.use_active_record = false
43 |
44 | # RSpec Rails can automatically mix in different behaviours to your tests
45 | # based on their file location, for example enabling you to call `get` and
46 | # `post` in specs under `spec/controllers`.
47 | #
48 | # You can disable this behaviour by removing the line below, and instead
49 | # explicitly tag your specs with their type, e.g.:
50 | #
51 | # RSpec.describe UsersController, type: :controller do
52 | # # ...
53 | # end
54 | #
55 | # The different available types are documented in the features, such as in
56 | # https://relishapp.com/rspec/rspec-rails/docs
57 | config.infer_spec_type_from_file_location!
58 |
59 | # Filter lines from Rails gems in backtraces.
60 | config.filter_rails_from_backtrace!
61 | config.include FactoryBot::Syntax::Methods
62 | # arbitrary gems may also be filtered via:
63 | # config.filter_gems_from_backtrace("gem name")
64 | end
65 |
--------------------------------------------------------------------------------
/config/environments/production.rb:
--------------------------------------------------------------------------------
1 | require "active_support/core_ext/integer/time"
2 |
3 | Rails.application.configure do
4 | # Settings specified here will take precedence over those in config/application.rb.
5 |
6 | # Code is not reloaded between requests.
7 | config.cache_classes = true
8 |
9 | # Eager load code on boot. This eager loads most of Rails and
10 | # your application in memory, allowing both threaded web servers
11 | # and those relying on copy on write to perform better.
12 | # Rake tasks automatically ignore this option for performance.
13 | config.eager_load = true
14 |
15 | # Full error reports are disabled and caching is turned on.
16 | config.consider_all_requests_local = false
17 | config.action_controller.perform_caching = true
18 |
19 | # Ensures that a master key has been made available in either ENV["RAILS_MASTER_KEY"]
20 | # or in config/master.key. This key is used to decrypt credentials (and other encrypted files).
21 | # config.require_master_key = true
22 |
23 | # Disable serving static files from the `/public` folder by default since
24 | # Apache or NGINX already handles this.
25 | config.public_file_server.enabled = ENV["RAILS_SERVE_STATIC_FILES"].present?
26 |
27 | # Compress CSS using a preprocessor.
28 | # config.assets.css_compressor = :sass
29 |
30 | # Do not fallback to assets pipeline if a precompiled asset is missed.
31 | config.assets.compile = false
32 |
33 | # Enable serving of images, stylesheets, and JavaScripts from an asset server.
34 | # config.asset_host = "http://assets.example.com"
35 |
36 | # Specifies the header that your server uses for sending files.
37 | # config.action_dispatch.x_sendfile_header = "X-Sendfile" # for Apache
38 | # config.action_dispatch.x_sendfile_header = "X-Accel-Redirect" # for NGINX
39 |
40 | # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies.
41 | # config.force_ssl = true
42 |
43 | # Include generic and useful information about system operation, but avoid logging too much
44 | # information to avoid inadvertent exposure of personally identifiable information (PII).
45 | config.log_level = :info
46 |
47 | # Prepend all log lines with the following tags.
48 | config.log_tags = [ :request_id ]
49 |
50 | # Use a different cache store in production.
51 | # config.cache_store = :mem_cache_store
52 |
53 | # Enable locale fallbacks for I18n (makes lookups for any locale fall back to
54 | # the I18n.default_locale when a translation cannot be found).
55 | config.i18n.fallbacks = true
56 |
57 | # Don't log any deprecations.
58 | config.active_support.report_deprecations = false
59 |
60 | # Use default logging formatter so that PID and timestamp are not suppressed.
61 | config.log_formatter = ::Logger::Formatter.new
62 |
63 | # Use a different logger for distributed setups.
64 | # require "syslog/logger"
65 | # config.logger = ActiveSupport::TaggedLogging.new(Syslog::Logger.new "app-name")
66 |
67 | if ENV["RAILS_LOG_TO_STDOUT"].present?
68 | logger = ActiveSupport::Logger.new(STDOUT)
69 | logger.formatter = config.log_formatter
70 | config.logger = ActiveSupport::TaggedLogging.new(logger)
71 | end
72 |
73 | # Do not dump schema after migrations.
74 | config.active_record.dump_schema_after_migration = false
75 | end
76 |
--------------------------------------------------------------------------------
/bin/bundle:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 | # frozen_string_literal: true
3 |
4 | #
5 | # This file was generated by Bundler.
6 | #
7 | # The application 'bundle' is installed as part of a gem, and
8 | # this file is here to facilitate running it.
9 | #
10 |
11 | require "rubygems"
12 |
13 | m = Module.new do
14 | module_function
15 |
16 | def invoked_as_script?
17 | File.expand_path($0) == File.expand_path(__FILE__)
18 | end
19 |
20 | def env_var_version
21 | ENV["BUNDLER_VERSION"]
22 | end
23 |
24 | def cli_arg_version
25 | return unless invoked_as_script? # don't want to hijack other binstubs
26 | return unless "update".start_with?(ARGV.first || " ") # must be running `bundle update`
27 | bundler_version = nil
28 | update_index = nil
29 | ARGV.each_with_index do |a, i|
30 | if update_index && update_index.succ == i && a =~ Gem::Version::ANCHORED_VERSION_PATTERN
31 | bundler_version = a
32 | end
33 | next unless a =~ /\A--bundler(?:[= ](#{Gem::Version::VERSION_PATTERN}))?\z/
34 | bundler_version = $1
35 | update_index = i
36 | end
37 | bundler_version
38 | end
39 |
40 | def gemfile
41 | gemfile = ENV["BUNDLE_GEMFILE"]
42 | return gemfile if gemfile && !gemfile.empty?
43 |
44 | File.expand_path("../Gemfile", __dir__)
45 | end
46 |
47 | def lockfile
48 | lockfile =
49 | case File.basename(gemfile)
50 | when "gems.rb" then gemfile.sub(/\.rb$/, gemfile)
51 | else "#{gemfile}.lock"
52 | end
53 | File.expand_path(lockfile)
54 | end
55 |
56 | def lockfile_version
57 | return unless File.file?(lockfile)
58 | lockfile_contents = File.read(lockfile)
59 | return unless lockfile_contents =~ /\n\nBUNDLED WITH\n\s{2,}(#{Gem::Version::VERSION_PATTERN})\n/
60 | Regexp.last_match(1)
61 | end
62 |
63 | def bundler_requirement
64 | @bundler_requirement ||=
65 | env_var_version ||
66 | cli_arg_version ||
67 | bundler_requirement_for(lockfile_version)
68 | end
69 |
70 | def bundler_requirement_for(version)
71 | return "#{Gem::Requirement.default}.a" unless version
72 |
73 | bundler_gem_version = Gem::Version.new(version)
74 |
75 | bundler_gem_version.approximate_recommendation
76 | end
77 |
78 | def load_bundler!
79 | ENV["BUNDLE_GEMFILE"] ||= gemfile
80 |
81 | activate_bundler
82 | end
83 |
84 | def activate_bundler
85 | gem_error = activation_error_handling do
86 | gem "bundler", bundler_requirement
87 | end
88 | return if gem_error.nil?
89 | require_error = activation_error_handling do
90 | require "bundler/version"
91 | end
92 | return if require_error.nil? && Gem::Requirement.new(bundler_requirement).satisfied_by?(Gem::Version.new(Bundler::VERSION))
93 | warn "Activating bundler (#{bundler_requirement}) failed:\n#{gem_error.message}\n\nTo install the version of bundler this project requires, run `gem install bundler -v '#{bundler_requirement}'`"
94 | exit 42
95 | end
96 |
97 | def activation_error_handling
98 | yield
99 | nil
100 | rescue StandardError, LoadError => e
101 | e
102 | end
103 | end
104 |
105 | m.load_bundler!
106 |
107 | if m.invoked_as_script?
108 | load Gem.bin_path("bundler", "bundle")
109 | end
110 |
--------------------------------------------------------------------------------
/config/database.yml:
--------------------------------------------------------------------------------
1 | # PostgreSQL. Versions 9.3 and up are supported.
2 | #
3 | # Install the pg driver:
4 | # gem install pg
5 | # On macOS with Homebrew:
6 | # gem install pg -- --with-pg-config=/usr/local/bin/pg_config
7 | # On macOS with MacPorts:
8 | # gem install pg -- --with-pg-config=/opt/local/lib/postgresql84/bin/pg_config
9 | # On Windows:
10 | # gem install pg
11 | # Choose the win32 build.
12 | # Install PostgreSQL and put its /bin directory on your path.
13 | #
14 | # Configure Using Gemfile
15 | # gem "pg"
16 | #
17 | default: &default
18 | adapter: postgresql
19 | prepared_statements: false
20 | username: postgres
21 | password: postgres
22 | host: <%= ENV['DB_HOST'] || 'localhost' %>
23 | pool: <%= ENV.fetch('RAILS_MAX_THREADS',5) %>
24 | timeout: 5000
25 |
26 | development:
27 | <<: *default
28 | database: rails_2023_showcase_development
29 |
30 | # The specified database role being used to connect to postgres.
31 | # To create additional roles in postgres see `$ createuser --help`.
32 | # When left blank, postgres will use the default role. This is
33 | # the same name as the operating system user running Rails.
34 | #username: rails_2023_showcase
35 |
36 | # The password associated with the postgres role (username).
37 | #password:
38 |
39 | # Connect on a TCP socket. Omitted by default since the client uses a
40 | # domain socket that doesn't need configuration. Windows does not have
41 | # domain sockets, so uncomment these lines.
42 | #host: localhost
43 |
44 | # The TCP port the server listens on. Defaults to 5432.
45 | # If your server runs on a different port number, change accordingly.
46 | #port: 5432
47 |
48 | # Schema search path. The server defaults to $user,public
49 | #schema_search_path: myapp,sharedapp,public
50 |
51 | # Minimum log levels, in increasing order:
52 | # debug5, debug4, debug3, debug2, debug1,
53 | # log, notice, warning, error, fatal, and panic
54 | # Defaults to warning.
55 | #min_messages: notice
56 |
57 | # Warning: The database defined as "test" will be erased and
58 | # re-generated from your development database when you run "rake".
59 | # Do not set this db to the same as development or production.
60 | test:
61 | <<: *default
62 | database: rails_2023_showcase_test
63 |
64 | # As with config/credentials.yml, you never want to store sensitive information,
65 | # like your database password, in your source code. If your source code is
66 | # ever seen by anyone, they now have access to your database.
67 | #
68 | # Instead, provide the password or a full connection URL as an environment
69 | # variable when you boot the app. For example:
70 | #
71 | # DATABASE_URL="postgres://myuser:mypass@localhost/somedatabase"
72 | #
73 | # If the connection URL is provided in the special DATABASE_URL environment
74 | # variable, Rails will automatically merge its configuration values on top of
75 | # the values provided in this file. Alternatively, you can specify a connection
76 | # URL environment variable explicitly:
77 | #
78 | # production:
79 | # url: <%= ENV["MY_APP_DATABASE_URL"] %>
80 | #
81 | # Read https://guides.rubyonrails.org/configuring.html#configuring-a-database
82 | # for a full overview on how database connection configuration can be specified.
83 | #
84 | production:
85 | <<: *default
86 | database: rails_2023_showcase_production
87 | username: rails_2023_showcase
88 | password: <%= ENV["RAILS_2023_SHOWCASE_DATABASE_PASSWORD"] %>
89 |
--------------------------------------------------------------------------------
/spec/spec_helper.rb:
--------------------------------------------------------------------------------
1 | # This file was generated by the `rails generate rspec:install` command. Conventionally, all
2 | # specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`.
3 | # The generated `.rspec` file contains `--require spec_helper` which will cause
4 | # this file to always be loaded, without a need to explicitly require it in any
5 | # files.
6 | #
7 | # Given that it is always loaded, you are encouraged to keep this file as
8 | # light-weight as possible. Requiring heavyweight dependencies from this file
9 | # will add to the boot time of your test suite on EVERY test run, even for an
10 | # individual file that may not need all of that loaded. Instead, consider making
11 | # a separate helper file that requires the additional dependencies and performs
12 | # the additional setup, and require it from the spec files that actually need
13 | # it.
14 | #
15 | # See https://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration
16 | RSpec.configure do |config|
17 | # rspec-expectations config goes here. You can use an alternate
18 | # assertion/expectation library such as wrong or the stdlib/minitest
19 | # assertions if you prefer.
20 | config.expect_with :rspec do |expectations|
21 | # This option will default to `true` in RSpec 4. It makes the `description`
22 | # and `failure_message` of custom matchers include text for helper methods
23 | # defined using `chain`, e.g.:
24 | # be_bigger_than(2).and_smaller_than(4).description
25 | # # => "be bigger than 2 and smaller than 4"
26 | # ...rather than:
27 | # # => "be bigger than 2"
28 | expectations.include_chain_clauses_in_custom_matcher_descriptions = true
29 | end
30 |
31 | # rspec-mocks config goes here. You can use an alternate test double
32 | # library (such as bogus or mocha) by changing the `mock_with` option here.
33 | config.mock_with :rspec do |mocks|
34 | # Prevents you from mocking or stubbing a method that does not exist on
35 | # a real object. This is generally recommended, and will default to
36 | # `true` in RSpec 4.
37 | mocks.verify_partial_doubles = true
38 | end
39 |
40 | # This option will default to `:apply_to_host_groups` in RSpec 4 (and will
41 | # have no way to turn it off -- the option exists only for backwards
42 | # compatibility in RSpec 3). It causes shared context metadata to be
43 | # inherited by the metadata hash of host groups and examples, rather than
44 | # triggering implicit auto-inclusion in groups with matching metadata.
45 | config.shared_context_metadata_behavior = :apply_to_host_groups
46 |
47 | # The settings below are suggested to provide a good initial experience
48 | # with RSpec, but feel free to customize to your heart's content.
49 | =begin
50 | # This allows you to limit a spec run to individual examples or groups
51 | # you care about by tagging them with `:focus` metadata. When nothing
52 | # is tagged with `:focus`, all examples get run. RSpec also provides
53 | # aliases for `it`, `describe`, and `context` that include `:focus`
54 | # metadata: `fit`, `fdescribe` and `fcontext`, respectively.
55 | config.filter_run_when_matching :focus
56 |
57 | # Allows RSpec to persist some state between runs in order to support
58 | # the `--only-failures` and `--next-failure` CLI options. We recommend
59 | # you configure your source control system to ignore this file.
60 | config.example_status_persistence_file_path = "spec/examples.txt"
61 |
62 | # Limits the available syntax to the non-monkey patched syntax that is
63 | # recommended. For more details, see:
64 | # https://relishapp.com/rspec/rspec-core/docs/configuration/zero-monkey-patching-mode
65 | config.disable_monkey_patching!
66 |
67 | # Many RSpec users commonly either run the entire suite or an individual
68 | # file, and it's useful to allow more verbose output when running an
69 | # individual spec file.
70 | if config.files_to_run.one?
71 | # Use the documentation formatter for detailed output,
72 | # unless a formatter has already been configured
73 | # (e.g. via a command-line flag).
74 | config.default_formatter = "doc"
75 | end
76 |
77 | # Print the 10 slowest examples and example groups at the
78 | # end of the spec run, to help surface which specs are running
79 | # particularly slow.
80 | config.profile_examples = 10
81 |
82 | # Run specs in random order to surface order dependencies. If you find an
83 | # order dependency and want to debug it, you can fix the order by providing
84 | # the seed, which is printed after each run.
85 | # --seed 1234
86 | config.order = :random
87 |
88 | # Seed global randomization in this process using the `--seed` CLI option.
89 | # Setting this allows you to use `--seed` to deterministically reproduce
90 | # test failures related to randomization by passing the same `--seed` value
91 | # as the one that triggered the failure.
92 | Kernel.srand config.seed
93 | =end
94 | end
95 |
--------------------------------------------------------------------------------
/db/seeds.rb:
--------------------------------------------------------------------------------
1 | size_proc = proc { %w(tiny small medium large).sample }
2 | stat_proc = proc { (8..18).to_a.sample }
3 | exp_proc = proc { [0, 0.2, 0.5, 1, 2, 3].sample * rand(10000) }
4 | measurements_proc = proc do
5 | height = 4 + (rand * 3)
6 | weight = 20 * height + (rand * 25 * height)
7 | { height:, weight: }
8 | end
9 | genders_proc = proc do
10 | (%w(male male male male female female female female) + [nil]).sample || Faker::Gender.type
11 | end
12 |
13 | 100.times do
14 | Campaign.create do |c|
15 | c.title = "#{Faker::Book.title}"
16 | c.description = Faker::Quote.fortune_cookie
17 | c.start_date = (rand * 1000).days.ago
18 | end
19 | end
20 |
21 | campaign_ids = Campaign.all.pluck(:id)
22 |
23 | 500.times do
24 | Character.create do |c|
25 | c.campaign_id = campaign_ids.sample
26 | c.name = Faker::FunnyName.name
27 | c.age = 16 + ([0, 1, 1, 1, 1, 2, 2, 3].sample * rand(20))
28 | c.gender = genders_proc.call
29 | c.size = size_proc.call
30 | c.species = Faker::Games::DnD.race
31 |
32 | measurements = measurements_proc.call
33 |
34 | c.height = measurements[:height]
35 | c.weight = measurements[:weight]
36 |
37 | c.experience = exp_proc.call
38 |
39 | %w(strength agility health reasoning memory intuition beauty).each do |stat|
40 | num = stat_proc.call
41 | c.send("#{stat}=", num)
42 | end
43 | end
44 | end
45 |
46 | feat_name_proc = proc do
47 | adj = Faker::Adjective.positive
48 | verb = %w(fighting devling dodging exploring casting defending fleeing bartering healing speaking).sample
49 | "#{adj} #{verb}"
50 | end
51 |
52 | 30.times do
53 | Feat.create do |f|
54 | f.name = feat_name_proc.call
55 | f.description = Faker::Quote.fortune_cookie
56 | end
57 | end
58 |
59 | attributes = %w(strength agility health reasoning memory intuition beauty)
60 |
61 | modify_stat_proc = proc do |operand|
62 | operand ||= "+"
63 | 0.send(operand, rand(4))
64 | rescue ZeroDivisionError
65 | 0
66 | end
67 |
68 | flaw_proc = proc do
69 | adj = Faker::Adjective.negative
70 | attr = attributes.sample
71 | mod = modify_stat_proc.call(:-)
72 | { attr:, title: "#{adj} #{attr}", mod:}
73 | end
74 |
75 | 100.times do
76 | Flaw.create do |f|
77 | attrs = flaw_proc.call
78 | mod = attrs[:mod]
79 | attr = attrs[:attr]
80 |
81 | f.title = attrs[:title]
82 | f.send("modify_#{attr}=", mod)
83 | f.description = Faker::Quote.fortune_cookie
84 | end
85 | end
86 |
87 | virtue_proc = proc do
88 | adj = Faker::Adjective.positive
89 | attr = attributes.sample
90 | mod = modify_stat_proc.call(:+)
91 | { attr:, title: "#{adj} #{attr}", mod:}
92 | end
93 |
94 | 100.times do
95 | Virtue.create do |f|
96 | attrs = virtue_proc.call
97 | mod = attrs[:mod]
98 | attr = attrs[:attr]
99 |
100 | f.title = attrs[:title]
101 | f.send("modify_#{attr}=", mod)
102 | f.description = Faker::Quote.fortune_cookie
103 | end
104 | end
105 |
106 | flaw_ids = Flaw.all.pluck(:id)
107 | feat_ids = Feat.all.pluck(:id)
108 | virtue_ids = Virtue.all.pluck(:id)
109 |
110 | Character.all.find_each do |character|
111 | character_id = character.id
112 | CharactersFeat.create(character_id:, feat_id: feat_ids.sample)
113 | CharactersVirtue.create(character_id:, virtue_id: virtue_ids.sample)
114 | CharactersFlaw.create(character_id:, flaw_id: flaw_ids.sample)
115 | end
116 |
117 | adj1 = %w(lesser standard plus_one plus_two greater greatest)
118 | adj2 = %w(bronze iron gold mithril glass dragonscale silver)
119 | potion_names = %w(invisibility strength agility health memory intuition reasoning beauty healing resistance dodging)
120 | armor_names = %w(helmet shield breastplate chainmail leggins greaves)
121 | weapon_names = %w(sword spear longbow shortbow crossbow greatsword dagger axe greataxe lance)
122 |
123 | adj1.each do |modifier|
124 | adj2.each do |material|
125 | weapon_names.each do |type|
126 | name = "#{modifier} #{material} #{type}"
127 | Item.create(name:, equippable: true, weapon: true)
128 | end
129 | end
130 | end
131 |
132 | adj1.each do |modifier|
133 | adj2.each do |material|
134 | armor_names.each do |type|
135 | name = "#{modifier} #{material} #{type}"
136 | Item.create(name:, equippable: true, armor: true)
137 | end
138 | end
139 | end
140 |
141 | adj1.each do |modifier|
142 | potion_names.each do |type|
143 | name = "#{modifier} potion of #{type}"
144 | Item.create(name:, consummable: true)
145 | end
146 | end
147 |
148 | weapons_ids = Item.where(weapon: true).pluck(:id)
149 | armors_ids = Item.where(armor: true).pluck(:id)
150 | potions_ids = Item.where(consummable: true).pluck(:id)
151 |
152 | spell_names = %w(warding healing summoning transformation fire ice lightning illusion)
153 | adj1 = %w(lesser minor power greater masterful legendary)
154 |
155 | spell_names.each do |type|
156 | adj1.each.with_index(1) do |mod, idx|
157 | name = "#{mod} spell of #{type}"
158 | Spell.create(name: name, description: name, level: idx)
159 | end
160 | end
161 |
162 | spell_ids = Spell.all.pluck(:id)
163 |
164 | Character.all.find_each do |c|
165 | character_id = c.id
166 |
167 | CharactersSpell.create(spell_id: spell_ids.sample, character_id:)
168 | CharactersSpell.find_or_create_by(spell_id: spell_ids.sample, character_id:)
169 |
170 | inventory = Inventory.create(character_id:)
171 | inventory_id = inventory.id
172 |
173 | InventoriesItem.create(inventory_id:, item_id: weapons_ids.sample)
174 | InventoriesItem.create(inventory_id:, item_id: armors_ids.sample)
175 | InventoriesItem.find_or_create_by(inventory_id:, item_id: armors_ids.sample)
176 | InventoriesItem.create(inventory_id:, item_id: weapons_ids.sample)
177 | InventoriesItem.create(inventory_id:, item_id: potions_ids.sample)
178 | InventoriesItem.create(inventory_id:, item_id: potions_ids.sample)
179 | end
180 |
--------------------------------------------------------------------------------
/Gemfile.lock:
--------------------------------------------------------------------------------
1 | GEM
2 | remote: https://rubygems.org/
3 | specs:
4 | actioncable (7.0.4.3)
5 | actionpack (= 7.0.4.3)
6 | activesupport (= 7.0.4.3)
7 | nio4r (~> 2.0)
8 | websocket-driver (>= 0.6.1)
9 | actionmailbox (7.0.4.3)
10 | actionpack (= 7.0.4.3)
11 | activejob (= 7.0.4.3)
12 | activerecord (= 7.0.4.3)
13 | activestorage (= 7.0.4.3)
14 | activesupport (= 7.0.4.3)
15 | mail (>= 2.7.1)
16 | net-imap
17 | net-pop
18 | net-smtp
19 | actionmailer (7.0.4.3)
20 | actionpack (= 7.0.4.3)
21 | actionview (= 7.0.4.3)
22 | activejob (= 7.0.4.3)
23 | activesupport (= 7.0.4.3)
24 | mail (~> 2.5, >= 2.5.4)
25 | net-imap
26 | net-pop
27 | net-smtp
28 | rails-dom-testing (~> 2.0)
29 | actionpack (7.0.4.3)
30 | actionview (= 7.0.4.3)
31 | activesupport (= 7.0.4.3)
32 | rack (~> 2.0, >= 2.2.0)
33 | rack-test (>= 0.6.3)
34 | rails-dom-testing (~> 2.0)
35 | rails-html-sanitizer (~> 1.0, >= 1.2.0)
36 | actiontext (7.0.4.3)
37 | actionpack (= 7.0.4.3)
38 | activerecord (= 7.0.4.3)
39 | activestorage (= 7.0.4.3)
40 | activesupport (= 7.0.4.3)
41 | globalid (>= 0.6.0)
42 | nokogiri (>= 1.8.5)
43 | actionview (7.0.4.3)
44 | activesupport (= 7.0.4.3)
45 | builder (~> 3.1)
46 | erubi (~> 1.4)
47 | rails-dom-testing (~> 2.0)
48 | rails-html-sanitizer (~> 1.1, >= 1.2.0)
49 | activejob (7.0.4.3)
50 | activesupport (= 7.0.4.3)
51 | globalid (>= 0.3.6)
52 | activemodel (7.0.4.3)
53 | activesupport (= 7.0.4.3)
54 | activerecord (7.0.4.3)
55 | activemodel (= 7.0.4.3)
56 | activesupport (= 7.0.4.3)
57 | activestorage (7.0.4.3)
58 | actionpack (= 7.0.4.3)
59 | activejob (= 7.0.4.3)
60 | activerecord (= 7.0.4.3)
61 | activesupport (= 7.0.4.3)
62 | marcel (~> 1.0)
63 | mini_mime (>= 1.1.0)
64 | activesupport (7.0.4.3)
65 | concurrent-ruby (~> 1.0, >= 1.0.2)
66 | i18n (>= 1.6, < 2)
67 | minitest (>= 5.1)
68 | tzinfo (~> 2.0)
69 | annotate (3.2.0)
70 | activerecord (>= 3.2, < 8.0)
71 | rake (>= 10.4, < 14.0)
72 | builder (3.2.4)
73 | coderay (1.1.3)
74 | concurrent-ruby (1.2.2)
75 | crass (1.0.6)
76 | date (3.3.3)
77 | debug (1.7.2)
78 | irb (>= 1.5.0)
79 | reline (>= 0.3.1)
80 | diff-lcs (1.5.0)
81 | dry-container (0.11.0)
82 | concurrent-ruby (~> 1.0)
83 | dry-core (1.0.0)
84 | concurrent-ruby (~> 1.0)
85 | zeitwerk (~> 2.6)
86 | dry-inflector (1.0.0)
87 | dry-initializer (3.1.1)
88 | dry-initializer-rails (3.1.1)
89 | dry-initializer (>= 2.4, < 4)
90 | rails (> 3.0)
91 | dry-logic (1.5.0)
92 | concurrent-ruby (~> 1.0)
93 | dry-core (~> 1.0, < 2)
94 | zeitwerk (~> 2.6)
95 | dry-types (1.7.1)
96 | concurrent-ruby (~> 1.0)
97 | dry-core (~> 1.0)
98 | dry-inflector (~> 1.0)
99 | dry-logic (~> 1.4)
100 | zeitwerk (~> 2.6)
101 | erubi (1.12.0)
102 | factory_bot (6.2.1)
103 | activesupport (>= 5.0.0)
104 | factory_bot_rails (6.2.0)
105 | factory_bot (~> 6.2.0)
106 | railties (>= 5.0.0)
107 | faker (2.19.0)
108 | i18n (>= 1.6, < 2)
109 | globalid (1.1.0)
110 | activesupport (>= 5.0)
111 | i18n (1.12.0)
112 | concurrent-ruby (~> 1.0)
113 | io-console (0.6.0)
114 | irb (1.6.4)
115 | reline (>= 0.3.0)
116 | loofah (2.20.0)
117 | crass (~> 1.0.2)
118 | nokogiri (>= 1.5.9)
119 | mail (2.8.1)
120 | mini_mime (>= 0.1.1)
121 | net-imap
122 | net-pop
123 | net-smtp
124 | marcel (1.0.2)
125 | method_source (1.0.0)
126 | mini_mime (1.1.2)
127 | minitest (5.18.0)
128 | net-imap (0.3.4)
129 | date
130 | net-protocol
131 | net-pop (0.1.2)
132 | net-protocol
133 | net-protocol (0.2.1)
134 | timeout
135 | net-smtp (0.3.3)
136 | net-protocol
137 | nio4r (2.5.9)
138 | nokogiri (1.14.3-x86_64-darwin)
139 | racc (~> 1.4)
140 | pg (1.4.6)
141 | pry (0.14.2)
142 | coderay (~> 1.1)
143 | method_source (~> 1.0)
144 | pry-rails (0.3.9)
145 | pry (>= 0.10.4)
146 | puma (5.6.5)
147 | nio4r (~> 2.0)
148 | racc (1.6.2)
149 | rack (2.2.6.4)
150 | rack-test (2.1.0)
151 | rack (>= 1.3)
152 | rails (7.0.4.3)
153 | actioncable (= 7.0.4.3)
154 | actionmailbox (= 7.0.4.3)
155 | actionmailer (= 7.0.4.3)
156 | actionpack (= 7.0.4.3)
157 | actiontext (= 7.0.4.3)
158 | actionview (= 7.0.4.3)
159 | activejob (= 7.0.4.3)
160 | activemodel (= 7.0.4.3)
161 | activerecord (= 7.0.4.3)
162 | activestorage (= 7.0.4.3)
163 | activesupport (= 7.0.4.3)
164 | bundler (>= 1.15.0)
165 | railties (= 7.0.4.3)
166 | rails-dom-testing (2.0.3)
167 | activesupport (>= 4.2.0)
168 | nokogiri (>= 1.6)
169 | rails-html-sanitizer (1.5.0)
170 | loofah (~> 2.19, >= 2.19.1)
171 | railties (7.0.4.3)
172 | actionpack (= 7.0.4.3)
173 | activesupport (= 7.0.4.3)
174 | method_source
175 | rake (>= 12.2)
176 | thor (~> 1.0)
177 | zeitwerk (~> 2.5)
178 | rake (13.0.6)
179 | reline (0.3.3)
180 | io-console (~> 0.5)
181 | rspec-core (3.12.1)
182 | rspec-support (~> 3.12.0)
183 | rspec-expectations (3.12.2)
184 | diff-lcs (>= 1.2.0, < 2.0)
185 | rspec-support (~> 3.12.0)
186 | rspec-mocks (3.12.5)
187 | diff-lcs (>= 1.2.0, < 2.0)
188 | rspec-support (~> 3.12.0)
189 | rspec-rails (6.0.1)
190 | actionpack (>= 6.1)
191 | activesupport (>= 6.1)
192 | railties (>= 6.1)
193 | rspec-core (~> 3.11)
194 | rspec-expectations (~> 3.11)
195 | rspec-mocks (~> 3.11)
196 | rspec-support (~> 3.11)
197 | rspec-support (3.12.0)
198 | sprockets (4.2.0)
199 | concurrent-ruby (~> 1.0)
200 | rack (>= 2.2.4, < 4)
201 | sprockets-rails (3.4.2)
202 | actionpack (>= 5.2)
203 | activesupport (>= 5.2)
204 | sprockets (>= 3.0.0)
205 | thor (1.2.1)
206 | timeout (0.3.2)
207 | tzinfo (2.0.6)
208 | concurrent-ruby (~> 1.0)
209 | websocket-driver (0.7.5)
210 | websocket-extensions (>= 0.1.0)
211 | websocket-extensions (0.1.5)
212 | zeitwerk (2.6.7)
213 |
214 | PLATFORMS
215 | x86_64-darwin-22
216 |
217 | DEPENDENCIES
218 | annotate
219 | debug
220 | dry-container
221 | dry-initializer-rails
222 | dry-types
223 | factory_bot_rails
224 | faker
225 | pg (~> 1.1)
226 | pry
227 | pry-rails
228 | puma (~> 5.0)
229 | rails (~> 7.0.4, >= 7.0.4.3)
230 | rspec-rails
231 | sprockets-rails
232 | tzinfo-data
233 |
234 | RUBY VERSION
235 | ruby 3.2.0p0
236 |
237 | BUNDLED WITH
238 | 2.4.3
239 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # README
2 |
3 | To use this repo:
4 |
5 | 1. Fork your own copy of this repo and clone it. Change directories in your terminal to the project name.
6 | 2. Run `bundle install` in your terminal. (Note: Ruby and is a dependency. Make sure you have version 3.2.0 installed)
7 | 3. Run `rails db:create` in your terminal to create your development and test database.
8 | 4. Run `rails db:migrate` to construct your database schema.
9 | 5. Run `rails db:seed` to seed your database with records
10 | (Note: this may take a while. If it is taking too long then consider editing the seeds file to create fewer records)
11 | 6. Run `rspec spec` to see test results.
12 |
13 | # Overview
14 |
15 | This repository is intended to demonstrate G2's query builder / visitor pattern ("builder pattern"). This builder
16 | pattern sits atop ActiveRecord as a higher level abstraction that incorporates domain specific logic. With the builder
17 | pattern it is possible to gracefully construct composable queries that deal well with conditional branches and are
18 | highly modular, and thus able to deal gracefully with extensions or modifications in response to new features or
19 | changes to business logic. The remainder of this readme serves as an explanation of the builder pattern and how to use
20 | it.
21 |
22 | ## Background and Database Schema
23 |
24 | We have deliberately left out a frontend to this as the builder pattern is implementation agnostic: it can serve any
25 | frontend that can make calls to a Rails backend. G2 uses graphql with this pattern but this is not required.
26 |
27 | We have left our demonstration schema relatively simple (but not too simple!) to make our queries easier to understand.
28 | The details are not very important; it is sufficient to know that this follows a 'star schema' pattern with characters
29 | as the center table connecting to a number of associated tables, including feats, flaws, spells, specializations,
30 | campaigns, and virtues, to name a few. This resembles a character management system for classic tabletop role playing
31 | games.
32 |
33 | Many software engineers love RPGs and will appreciate the schema, but if it seems unfamiliar to you then fear not!
34 | Our schema is for demonstration purposes only and it is not necessary to understand the nuances of RPGs to grasp
35 | the builder pattern this repo demonstrates.
36 |
37 | ## Query classes
38 |
39 | The best way to learn the builder pattern is to play with specific examples. Find `Queries::Characters::IndexQuery`
40 | and read it over. The specs are also a good source of documentation.
41 | Run `rspec spec/queries/characters/index_query_spec.rb` in your terminal to see the test output.
42 |
43 | The naming of the class gives us a hint at its purpose: it runs a query that returns a list of characters (specifically,
44 | an ActiveRecord Relation). The specific characters returned will depend on the filters passed in.
45 |
46 | Our class inherits some of its behavior from its base class `Queries::Base`. The builder pattern is object oriented
47 | and follows all of the usual principles of good OOP design. It uses inheritance in accordance with the Liskov principle:
48 | all query classes follow the same basic interface where their public method is #call, which returns the results of our
49 | query. The implementation details of the #call method are left up to the descendent class, but all of them add nodes to
50 | their builder and delegate the building of the query to the builder.
51 |
52 | The builder is injected as a dependency into the query class, and uses `QueryBuilder::Builders::Default` by default.
53 | This default can be overriden with a different builder that uses its own algorithm, and this is thus one of the many
54 | places our query classes maintain their composability. More on the builder later.
55 |
56 | The query class takes another input, filters. These can be strongly typed but that is an implementation detail. At
57 | its core the filters are just a nested hash that give the conditions the user has demanded of our query. Some classes
58 | take other inputs, including order and limit parameters commonly. But the builder and filters are the most important
59 | inputs for the builder pattern.
60 |
61 | We can see in `Queries::Characters::IndexQuery` that we are adding nodes to the builder and that this builder will
62 | perform some operation on them. But how does `IndexQuery` know what nodes to add in response to the filters it is given?
63 | Where is the conditional logic in this class? IndexQuery does not know what nodes to add and does not have any
64 | conditional branching in it. Those tasks are delegated elsewhere, and that is the beauty of it!
65 |
66 | The query classes can be thought of as providing the "raw material" of the query. They pass all of the nodes that the
67 | builder *might* need to construct the query. Unnecessary nodes will be eliminated when the builder visits each node with
68 | its accumulated state, and the node will decide if is to be included based on its state and the filters passed to it.
69 |
70 | Our query class is thus left free to tell a story in the nodes it contains. It is also open to easy extension: if our
71 | query needs another conditional branch or a join to another table then we can simply add the new node(s) needed,
72 | confident that the nodes and builder will handle the logic. Wrapping our various logic and clauses in Node classes also
73 | makes our code easily maintainable: do not underestimate the advantage of searchability of named classes in an editor
74 | and the ease of modifying a node if a hotfix or a change in business logic warrants it.
75 |
76 | ## Builders
77 |
78 | We next turn our attention to the builders. Find `QueryBuilder::Builders::Default` and read over this class. Its
79 | public interface is inherited from `QueryBuilder::Builders::Base`. The builder stores two crucial pieces of state:
80 | the `initial_state` and the `nodes`. Our initial_state is the head of our ActiveRecord query and is passed in from
81 | the query class. And `nodes` is an array of typed nodes where the type is specified in `base_node_class`. Our builder
82 | starts with the `nodes` array empty and has nodes added to it through the `#add` method.
83 |
84 | The query class will call `#build` on the builder once it has added all of the nodes. And the build method is where
85 | things really get interesting. Our central algorithm is revealed: the builder uses `reduce`. And it does so in unique
86 | fashion: in addition to accumulating state it eliminates invalid nodes and extracts clauses by `visiting` each node.
87 | The logic is thus mostly delegated to nodes: each node decides if it is valid based on its own logic and the state it
88 | receives; the node also decides what to do with the state (accumulated ActiveRecord query) it receives. The `builder
89 | pattern` is thus also a `visitor pattern` in that a builder visits its nodes.
90 |
91 | ## Nodes
92 |
93 | The term `nodes` will be familiar to most programmers. At its most simple a node is simply a data structure that is part
94 | of a larger link or tree structure (as in an abstract syntax tree or a query). That is also true of nodes in the builder
95 | pattern with the crucial distinction that they can also encapsulate higher domain logic and abstractions. Nodes in the
96 | builder pattern are thus not data primitives; they are named classes that hold state, can contain logic, and either
97 | resolve to an ActiveRecord clause or contain other nodes.
98 |
99 | We can start with a simpler example, `QueryBuilder::Campaigns::Nodes::TitleIn`. The naming of this class describes its
100 | purpose: it concerns the `campaigns` table, and the `TitleIn` class name makes us think of a `where title in _` clause.
101 | It takes one parameter when initialized: `titles`. These `titles` may or may not be present in filters, and thus this
102 | node exposes a `#valid?` method that checks if titles are present. Its other public method is more subtle. When a
103 | builder "visits" a node it typically checks if it is valid and calls `#accept` on the node, passing it accumulated
104 | state. In this particular node the `#accept` method simply merges a clause into the ActiveRecord query.
105 | Its clause is, unsurprisingly, `Campaign.where(title: titles)`.
106 |
107 | This node is fairly typical of the builder pattern: it defines the conditions where it is valid, accepts a filter
108 | param ("titles" in this case), and merges a clause when visited by the builder.
109 |
110 | Some nodes are not tied to a specific table but are excellent tools for use in many queries. See the common node
111 | `QueryBuilder::Nodes::Joins::InnerJoin` for a good example.
112 |
113 | ### Nested Nodes
114 |
115 | Special attention should be paid to another variety of node, however, and these are the nodes that nest other nodes. Two
116 | of these are common tools that are quite useful: `QueryBuilder::Nodes::Wheres::And` and its sibling `Or` class in the
117 | same module. These nodes can nest nodes that resolve to clauses contained in `where and` or `where or` clauses,
118 | respectively.
119 |
120 | One of the great advantages of nested nodes is the ability to group higher level domain specific logic that are often
121 | key elements of our business. And the `QueryBuilder::Characters::Nodes::Legendary` node is a perfect illustration of
122 | this. Its story is that our (hypothetical) product manager knows that our clients highly value being able to search for
123 | characters of "legendary status". These characters have reached a certain threshold of experience and progression in one
124 | of their key stats.
125 |
126 | The `Legendary` node gracefully handles the nested `where` clauses this requires, which include both `or` and `and`
127 | operators.
128 |
129 | As a consequence our domain logic for legendary characters is:
130 | 1. well documented in our codebase
131 | 2. easily searchable
132 | 3. reusabled in other queries
133 | 4. maintainable: it can be modified, if necessary, and ensure that all queries using it follow the same domain logic.
134 |
135 | The last point is one of the main necessities that inspired our builder pattern invention: in the past we found that
136 | working with lower level abstractions, such as SQL or vanilla ActiveRecord, often led to painful regressions as it
137 | was quite difficult to find where common logic was used in different queries.
138 |
139 | ## Putting it all together
140 |
141 | The builder pattern thus uses 3 main objects: queries, builders, and nodes. Queries are the public interface with the
142 | rest of our app that desires output for a query. Queries accept filters and encapsulate the raw material (nodes) of
143 | our query. They add nodes to their builder.
144 |
145 | The builder accumulates nodes and performs a reduce algorithm on its complete collection of nodes; invalid nodes that
146 | do not meet filtering criteria are eliminated, and state is accumulated through the visiting of nodes.
147 |
148 | Nodes determine if they are valid given filter params and state passed to them; a valid node produces a clause merged
149 | to the ActiveRecord query or calls on nested nodes to produce clauses. Nodes can thus be nested ad infinitum if needed.
150 |
151 | Together these domain objects give us a builder pattern that can compose queries of any needed complexity while keeping
152 | our code clean, dry, and maintainable. The examples in this repo are relatively simple for learning purposes. We at G2
153 | have built much more complex queries and have found that this pattern has stood the test of time.
154 |
155 | We hope you enjoy this repo and find the inspiration to make this builder pattern your own.
156 |
157 | With kind regards,
158 |
159 | Justin Daniel and the G2 engineering team
160 |
--------------------------------------------------------------------------------
/db/schema.rb:
--------------------------------------------------------------------------------
1 | # This file is auto-generated from the current state of the database. Instead
2 | # of editing this file, please use the migrations feature of Active Record to
3 | # incrementally modify your database, and then regenerate this schema definition.
4 | #
5 | # This file is the source Rails uses to define your schema when running `bin/rails
6 | # db:schema:load`. When creating a new database, `bin/rails db:schema:load` tends to
7 | # be faster and is potentially less error prone than running all of your
8 | # migrations from scratch. Old migrations may fail to apply correctly if those
9 | # migrations use external dependencies or application code.
10 | #
11 | # It's strongly recommended that you check this file into your version control system.
12 |
13 | ActiveRecord::Schema[7.0].define(version: 2023_04_14_172406) do
14 | # These are extensions that must be enabled in order to support this database
15 | enable_extension "plpgsql"
16 |
17 | create_table "campaigns", force: :cascade do |t|
18 | t.string "title", null: false
19 | t.text "description"
20 | t.datetime "start_date", null: false
21 | t.datetime "created_at", null: false
22 | t.datetime "updated_at", null: false
23 | t.index ["title"], name: "index_campaigns_on_title"
24 | end
25 |
26 | create_table "campaigns_players", force: :cascade do |t|
27 | t.integer "campaign_id", null: false
28 | t.integer "player_id", null: false
29 | t.datetime "created_at", null: false
30 | t.datetime "updated_at", null: false
31 | t.index ["campaign_id", "player_id"], name: "index_campaigns_players_on_unique", unique: true
32 | t.index ["campaign_id"], name: "index_campaigns_players_on_campaign_id"
33 | t.index ["player_id"], name: "index_campaigns_players_on_player_id"
34 | end
35 |
36 | create_table "characters", force: :cascade do |t|
37 | t.string "name", null: false
38 | t.integer "version", default: 1, null: false
39 | t.text "origin_story"
40 | t.integer "player_id"
41 | t.integer "campaign_id"
42 | t.integer "age"
43 | t.string "gender"
44 | t.string "size", null: false
45 | t.string "species", null: false
46 | t.float "height"
47 | t.float "weight"
48 | t.integer "experience"
49 | t.integer "strength", null: false
50 | t.integer "agility", null: false
51 | t.integer "health", null: false
52 | t.integer "reasoning", null: false
53 | t.integer "memory", null: false
54 | t.integer "intuition", null: false
55 | t.integer "beauty", null: false
56 | t.datetime "created_at", null: false
57 | t.datetime "updated_at", null: false
58 | t.index ["campaign_id"], name: "index_characters_on_campaign_id"
59 | t.index ["player_id"], name: "index_characters_on_player_id"
60 | end
61 |
62 | create_table "characters_feats", force: :cascade do |t|
63 | t.integer "feat_id", null: false
64 | t.integer "character_id", null: false
65 | t.text "backstory"
66 | t.json "modifications"
67 | t.datetime "created_at", null: false
68 | t.datetime "updated_at", null: false
69 | t.index ["character_id"], name: "index_character_feats_on_character_id"
70 | t.index ["feat_id"], name: "index_character_feats_on_feat_id"
71 | end
72 |
73 | create_table "characters_flaws", force: :cascade do |t|
74 | t.integer "character_id", null: false
75 | t.integer "flaw_id", null: false
76 | t.integer "permanence", default: 3
77 | t.text "backstory"
78 | t.json "modifications"
79 | t.datetime "created_at", null: false
80 | t.datetime "updated_at", null: false
81 | t.index ["character_id"], name: "index_character_flaws_on_character_id"
82 | t.index ["flaw_id"], name: "index_character_flaws_on_flaw_id"
83 | end
84 |
85 | create_table "characters_skills", force: :cascade do |t|
86 | t.integer "character_id", null: false
87 | t.integer "skill_id", null: false
88 | t.integer "trained_level"
89 | t.text "backstory"
90 | t.json "modifications"
91 | t.datetime "created_at", null: false
92 | t.datetime "updated_at", null: false
93 | t.index ["character_id"], name: "index_character_skills_on_character_id"
94 | t.index ["skill_id"], name: "index_character_skills_on_skill_id"
95 | end
96 |
97 | create_table "characters_specializations", force: :cascade do |t|
98 | t.integer "character_id", null: false
99 | t.integer "specialization_id", null: false
100 | t.integer "trained_levels", default: 1, null: false
101 | t.text "backstory"
102 | t.json "modifications"
103 | t.index ["character_id", "specialization_id"], name: "index_char_spec_on_unique_fields", unique: true
104 | t.index ["specialization_id"], name: "index_char_spec_on_spec_id"
105 | end
106 |
107 | create_table "characters_spells", force: :cascade do |t|
108 | t.integer "character_id", null: false
109 | t.integer "spell_id", null: false
110 | t.text "backstory"
111 | t.json "modifications"
112 | t.index ["character_id", "spell_id"], name: "index_char_spells_on_unique_fields", unique: true
113 | t.index ["spell_id"], name: "index_char_spells_on_spell_id"
114 | end
115 |
116 | create_table "characters_virtues", force: :cascade do |t|
117 | t.integer "character_id", null: false
118 | t.integer "virtue_id", null: false
119 | t.integer "permanence", default: 3
120 | t.text "backstory"
121 | t.json "modifications"
122 | t.datetime "created_at", null: false
123 | t.datetime "updated_at", null: false
124 | t.index ["character_id"], name: "index_character_virtues_on_character_id"
125 | t.index ["virtue_id"], name: "index_character_virtues_on_virtue_id"
126 | end
127 |
128 | create_table "feats", force: :cascade do |t|
129 | t.string "name", null: false
130 | t.text "description", null: false
131 | t.integer "modify_ac", default: 0
132 | t.integer "modify_hp", default: 0
133 | t.integer "modify_age", default: 0
134 | t.integer "modify_stamina", default: 0
135 | t.integer "modify_strength", default: 0
136 | t.integer "modify_agility", default: 0
137 | t.integer "modify_health", default: 0
138 | t.integer "modify_reasoning", default: 0
139 | t.integer "modify_memory", default: 0
140 | t.integer "modify_intuition", default: 0
141 | t.integer "modify_beauty", default: 0
142 | t.datetime "created_at", null: false
143 | t.datetime "updated_at", null: false
144 | end
145 |
146 | create_table "flaws", force: :cascade do |t|
147 | t.string "title", null: false
148 | t.text "description", null: false
149 | t.string "magnitude"
150 | t.integer "modify_ac", default: 0
151 | t.integer "modify_hp", default: 0
152 | t.integer "modify_age", default: 0
153 | t.integer "modify_stamina", default: 0
154 | t.integer "modify_strength", default: 0
155 | t.integer "modify_agility", default: 0
156 | t.integer "modify_health", default: 0
157 | t.integer "modify_reasoning", default: 0
158 | t.integer "modify_memory", default: 0
159 | t.integer "modify_intuition", default: 0
160 | t.integer "modify_beauty", default: 0
161 | t.json "special_effects"
162 | t.datetime "created_at", null: false
163 | t.datetime "updated_at", null: false
164 | end
165 |
166 | create_table "inventories", force: :cascade do |t|
167 | t.integer "character_id"
168 | t.bigint "resource_id"
169 | t.string "resource_type"
170 | t.string "title"
171 | t.text "description"
172 | t.float "weight_limit"
173 | t.integer "item_limit"
174 | t.json "special_effects"
175 | t.datetime "created_at", null: false
176 | t.datetime "updated_at", null: false
177 | t.index ["character_id"], name: "index_inventories_on_character_id"
178 | end
179 |
180 | create_table "inventories_items", force: :cascade do |t|
181 | t.integer "inventory_id", null: false
182 | t.integer "item_id", null: false
183 | t.boolean "equipped", default: false
184 | t.integer "condition", default: 100
185 | t.json "modifications"
186 | t.decimal "price"
187 | t.string "price_unit", default: "gp"
188 | t.float "weight"
189 | t.datetime "created_at", null: false
190 | t.datetime "updated_at", null: false
191 | t.index ["inventory_id"], name: "index_inventory_items_on_inventory_id"
192 | t.index ["item_id"], name: "index_inventory_items_on_item_id"
193 | end
194 |
195 | create_table "items", force: :cascade do |t|
196 | t.string "name", null: false
197 | t.text "description"
198 | t.integer "hardness"
199 | t.integer "durability"
200 | t.float "weight"
201 | t.string "size"
202 | t.integer "modify_ac", default: 0
203 | t.integer "modify_hp", default: 0
204 | t.integer "modify_age", default: 0
205 | t.integer "modify_stamina", default: 0
206 | t.integer "modify_strength", default: 0
207 | t.integer "modify_agility", default: 0
208 | t.integer "modify_health", default: 0
209 | t.integer "modify_reasoning", default: 0
210 | t.integer "modify_memory", default: 0
211 | t.integer "modify_intuition", default: 0
212 | t.integer "modify_beauty", default: 0
213 | t.boolean "equippable", default: false
214 | t.boolean "consummable", default: false
215 | t.boolean "weapon", default: false
216 | t.boolean "armor", default: false
217 | t.json "special_effects"
218 | t.json "requirements"
219 | t.datetime "created_at", null: false
220 | t.datetime "updated_at", null: false
221 | end
222 |
223 | create_table "players", force: :cascade do |t|
224 | t.string "first_name"
225 | t.string "last_name"
226 | t.string "middle_name"
227 | t.string "preferred_name"
228 | t.string "pronouns"
229 | t.string "username", null: false
230 | t.string "email", null: false
231 | t.datetime "created_at", null: false
232 | t.datetime "updated_at", null: false
233 | end
234 |
235 | create_table "skills", force: :cascade do |t|
236 | t.string "name", null: false
237 | t.text "description", null: false
238 | t.boolean "core"
239 | t.string "primary_stat"
240 | t.string "secondary_stat"
241 | t.string "tertiary_stat"
242 | t.json "requirements"
243 | t.datetime "created_at", null: false
244 | t.datetime "updated_at", null: false
245 | end
246 |
247 | create_table "specializations", force: :cascade do |t|
248 | t.string "name", null: false
249 | t.text "description", null: false
250 | t.json "properties"
251 | t.datetime "created_at", null: false
252 | t.datetime "updated_at", null: false
253 | end
254 |
255 | create_table "spells", force: :cascade do |t|
256 | t.string "name", null: false
257 | t.text "description", null: false
258 | t.integer "level", default: 1, null: false
259 | t.json "effects"
260 | t.datetime "created_at", null: false
261 | t.datetime "updated_at", null: false
262 | end
263 |
264 | create_table "virtues", force: :cascade do |t|
265 | t.string "title", null: false
266 | t.text "description", null: false
267 | t.string "magnitude"
268 | t.integer "modify_ac", default: 0
269 | t.integer "modify_hp", default: 0
270 | t.integer "modify_age", default: 0
271 | t.integer "modify_stamina", default: 0
272 | t.integer "modify_strength", default: 0
273 | t.integer "modify_agility", default: 0
274 | t.integer "modify_health", default: 0
275 | t.integer "modify_reasoning", default: 0
276 | t.integer "modify_memory", default: 0
277 | t.integer "modify_intuition", default: 0
278 | t.integer "modify_beauty", default: 0
279 | t.json "special_effects"
280 | t.datetime "created_at", null: false
281 | t.datetime "updated_at", null: false
282 | end
283 |
284 | add_foreign_key "campaigns_players", "campaigns"
285 | add_foreign_key "campaigns_players", "players"
286 | add_foreign_key "characters", "campaigns"
287 | add_foreign_key "characters", "players"
288 | add_foreign_key "characters_feats", "characters"
289 | add_foreign_key "characters_feats", "feats"
290 | add_foreign_key "characters_flaws", "characters"
291 | add_foreign_key "characters_flaws", "flaws"
292 | add_foreign_key "characters_skills", "characters"
293 | add_foreign_key "characters_skills", "skills"
294 | add_foreign_key "characters_specializations", "characters"
295 | add_foreign_key "characters_specializations", "specializations"
296 | add_foreign_key "characters_spells", "characters"
297 | add_foreign_key "characters_spells", "spells"
298 | add_foreign_key "characters_virtues", "characters"
299 | add_foreign_key "characters_virtues", "virtues"
300 | add_foreign_key "inventories", "characters"
301 | add_foreign_key "inventories_items", "inventories"
302 | add_foreign_key "inventories_items", "items"
303 | end
304 |
--------------------------------------------------------------------------------
/db/migrate/20230414172406_create_starting_schema.rb:
--------------------------------------------------------------------------------
1 | class CreateStartingSchema < ActiveRecord::Migration[7.0]
2 | def change
3 | create_table 'campaigns', force: :cascade do |t|
4 | t.string 'title', null: false
5 | t.text 'description'
6 | t.datetime 'start_date', precision: 6, null: false
7 | t.datetime 'created_at', precision: 6, null: false
8 | t.datetime 'updated_at', precision: 6, null: false
9 | t.index 'title'
10 | end
11 |
12 | create_table 'players', force: :cascade do |t|
13 | t.string 'first_name'
14 | t.string 'last_name'
15 | t.string 'middle_name'
16 | t.string 'preferred_name'
17 | t.string 'pronouns'
18 | t.string 'username', null: false
19 | t.string 'email', null: false
20 | t.datetime 'created_at', precision: 6, null: false
21 | t.datetime 'updated_at', precision: 6, null: false
22 | end
23 |
24 | create_table 'campaigns_players', force: :cascade do |t|
25 | t.integer 'campaign_id', null: false
26 | t.integer 'player_id', null: false
27 | t.datetime 'created_at', precision: 6, null: false
28 | t.datetime 'updated_at', precision: 6, null: false
29 | t.index ['campaign_id'], name: 'index_campaigns_players_on_campaign_id'
30 | t.index ['player_id'], name: 'index_campaigns_players_on_player_id'
31 | t.index %w[campaign_id player_id], unique: true, name: 'index_campaigns_players_on_unique'
32 | end
33 |
34 | create_table 'characters', force: :cascade do |t|
35 | t.string 'name', null: false
36 | t.integer 'version', default: 1, null: false
37 | t.text 'origin_story'
38 | t.integer 'player_id'
39 | t.integer 'campaign_id'
40 | t.integer 'age'
41 | t.string 'gender'
42 | t.string 'size', null: false
43 | t.string 'species', null: false
44 | t.float 'height'
45 | t.float 'weight'
46 | t.integer 'experience'
47 | t.integer 'strength', null: false
48 | t.integer 'agility', null: false
49 | t.integer 'health', null: false
50 | t.integer 'reasoning', null: false
51 | t.integer 'memory', null: false
52 | t.integer 'intuition', null: false
53 | t.integer 'beauty', null: false
54 | t.datetime 'created_at', precision: 6, null: false
55 | t.datetime 'updated_at', precision: 6, null: false
56 | t.index ['campaign_id'], name: 'index_characters_on_campaign_id'
57 | t.index ['player_id'], name: 'index_characters_on_player_id'
58 | end
59 |
60 | create_table 'specializations', force: :cascade do |t|
61 | t.string 'name', null: false
62 | t.text 'description', null: false
63 | t.json 'properties'
64 | t.datetime 'created_at', precision: 6, null: false
65 | t.datetime 'updated_at', precision: 6, null: false
66 | end
67 |
68 | create_table 'feats', force: :cascade do |t|
69 | t.string 'name', null: false
70 | t.text 'description', null: false
71 | t.integer 'modify_ac', default: 0
72 | t.integer 'modify_hp', default: 0
73 | t.integer 'modify_age', default: 0
74 | t.integer 'modify_stamina', default: 0
75 | t.integer 'modify_strength', default: 0
76 | t.integer 'modify_agility', default: 0
77 | t.integer 'modify_health', default: 0
78 | t.integer 'modify_reasoning', default: 0
79 | t.integer 'modify_memory', default: 0
80 | t.integer 'modify_intuition', default: 0
81 | t.integer 'modify_beauty', default: 0
82 | t.datetime 'created_at', precision: 6, null: false
83 | t.datetime 'updated_at', precision: 6, null: false
84 | end
85 |
86 | create_table 'flaws', force: :cascade do |t|
87 | t.string 'title', null: false
88 | t.text 'description', null: false
89 | t.string 'magnitude'
90 | t.integer 'modify_ac', default: 0
91 | t.integer 'modify_hp', default: 0
92 | t.integer 'modify_age', default: 0
93 | t.integer 'modify_stamina', default: 0
94 | t.integer 'modify_strength', default: 0
95 | t.integer 'modify_agility', default: 0
96 | t.integer 'modify_health', default: 0
97 | t.integer 'modify_reasoning', default: 0
98 | t.integer 'modify_memory', default: 0
99 | t.integer 'modify_intuition', default: 0
100 | t.integer 'modify_beauty', default: 0
101 | t.json 'special_effects'
102 | t.datetime 'created_at', precision: 6, null: false
103 | t.datetime 'updated_at', precision: 6, null: false
104 | end
105 |
106 | create_table 'skills', force: :cascade do |t|
107 | t.string 'name', null: false
108 | t.text 'description', null: false
109 | t.boolean 'core'
110 | t.string 'primary_stat'
111 | t.string 'secondary_stat'
112 | t.string 'tertiary_stat'
113 | t.json 'requirements'
114 | t.datetime 'created_at', precision: 6, null: false
115 | t.datetime 'updated_at', precision: 6, null: false
116 | end
117 |
118 | create_table 'spells', force: :cascade do |t|
119 | t.string 'name', null: false
120 | t.text 'description', null: false
121 | t.integer 'level', null: false, default: 1
122 | t.json 'effects'
123 | t.datetime 'created_at', precision: 6, null: false
124 | t.datetime 'updated_at', precision: 6, null: false
125 | end
126 |
127 | create_table 'virtues', force: :cascade do |t|
128 | t.string 'title', null: false
129 | t.text 'description', null: false
130 | t.string 'magnitude'
131 | t.integer 'modify_ac', default: 0
132 | t.integer 'modify_hp', default: 0
133 | t.integer 'modify_age', default: 0
134 | t.integer 'modify_stamina', default: 0
135 | t.integer 'modify_strength', default: 0
136 | t.integer 'modify_agility', default: 0
137 | t.integer 'modify_health', default: 0
138 | t.integer 'modify_reasoning', default: 0
139 | t.integer 'modify_memory', default: 0
140 | t.integer 'modify_intuition', default: 0
141 | t.integer 'modify_beauty', default: 0
142 | t.json 'special_effects'
143 | t.datetime 'created_at', precision: 6, null: false
144 | t.datetime 'updated_at', precision: 6, null: false
145 | end
146 |
147 | create_table 'characters_feats', force: :cascade do |t|
148 | t.integer 'feat_id', null: false
149 | t.integer 'character_id', null: false
150 | t.text 'backstory'
151 | t.json 'modifications'
152 | t.datetime 'created_at', precision: 6, null: false
153 | t.datetime 'updated_at', precision: 6, null: false
154 | t.index ['character_id'], name: 'index_character_feats_on_character_id'
155 | t.index ['feat_id'], name: 'index_character_feats_on_feat_id'
156 | end
157 |
158 | create_table 'characters_flaws', force: :cascade do |t|
159 | t.integer 'character_id', null: false
160 | t.integer 'flaw_id', null: false
161 | t.integer 'permanence', default: 3
162 | t.text 'backstory'
163 | t.json 'modifications'
164 | t.datetime 'created_at', precision: 6, null: false
165 | t.datetime 'updated_at', precision: 6, null: false
166 | t.index ['character_id'], name: 'index_character_flaws_on_character_id'
167 | t.index ['flaw_id'], name: 'index_character_flaws_on_flaw_id'
168 | end
169 |
170 | create_table 'characters_skills', force: :cascade do |t|
171 | t.integer 'character_id', null: false
172 | t.integer 'skill_id', null: false
173 | t.integer 'trained_level'
174 | t.text 'backstory'
175 | t.json 'modifications'
176 | t.datetime 'created_at', precision: 6, null: false
177 | t.datetime 'updated_at', precision: 6, null: false
178 | t.index ['character_id'], name: 'index_character_skills_on_character_id'
179 | t.index ['skill_id'], name: 'index_character_skills_on_skill_id'
180 | end
181 |
182 | create_table 'characters_virtues', force: :cascade do |t|
183 | t.integer 'character_id', null: false
184 | t.integer 'virtue_id', null: false
185 | t.integer 'permanence', default: 3
186 | t.text 'backstory'
187 | t.json 'modifications'
188 | t.datetime 'created_at', precision: 6, null: false
189 | t.datetime 'updated_at', precision: 6, null: false
190 | t.index ['character_id'], name: 'index_character_virtues_on_character_id'
191 | t.index ['virtue_id'], name: 'index_character_virtues_on_virtue_id'
192 | end
193 |
194 | create_table 'characters_specializations' do |t|
195 | t.integer 'character_id', null: false
196 | t.integer 'specialization_id', null: false
197 | t.integer 'trained_levels', null: false, default: 1
198 | t.text 'backstory'
199 | t.json 'modifications'
200 | t.index ['specialization_id'], name: 'index_char_spec_on_spec_id'
201 | t.index %w(character_id specialization_id), unique: true, name: 'index_char_spec_on_unique_fields'
202 | end
203 |
204 | create_table 'characters_spells' do |t|
205 | t.integer 'character_id', null: false
206 | t.integer 'spell_id', null: false
207 | t.text 'backstory'
208 | t.json 'modifications'
209 | t.index ['spell_id'], name: 'index_char_spells_on_spell_id'
210 | t.index %w(character_id spell_id), unique: true, name: 'index_char_spells_on_unique_fields'
211 | end
212 |
213 | create_table 'items', force: :cascade do |t|
214 | t.string 'name', null: false
215 | t.text 'description'
216 | t.integer 'hardness'
217 | t.integer 'durability'
218 | t.float 'weight'
219 | t.string 'size'
220 | t.integer 'modify_ac', default: 0
221 | t.integer 'modify_hp', default: 0
222 | t.integer 'modify_age', default: 0
223 | t.integer 'modify_stamina', default: 0
224 | t.integer 'modify_strength', default: 0
225 | t.integer 'modify_agility', default: 0
226 | t.integer 'modify_health', default: 0
227 | t.integer 'modify_reasoning', default: 0
228 | t.integer 'modify_memory', default: 0
229 | t.integer 'modify_intuition', default: 0
230 | t.integer 'modify_beauty', default: 0
231 | t.boolean 'equippable', default: false
232 | t.boolean 'consummable', default: false
233 | t.boolean 'weapon', default: false
234 | t.boolean 'armor', default: false
235 | t.json 'special_effects'
236 | t.json 'requirements'
237 | t.datetime 'created_at', precision: 6, null: false
238 | t.datetime 'updated_at', precision: 6, null: false
239 | end
240 |
241 | create_table 'inventories', force: :cascade do |t|
242 | t.integer 'character_id'
243 | t.bigint 'resource_id'
244 | t.string 'resource_type'
245 | t.string 'title'
246 | t.text 'description'
247 | t.float 'weight_limit'
248 | t.integer 'item_limit'
249 | t.json 'special_effects'
250 | t.datetime 'created_at', precision: 6, null: false
251 | t.datetime 'updated_at', precision: 6, null: false
252 | t.index ['character_id'], name: 'index_inventories_on_character_id'
253 | end
254 |
255 | create_table 'inventories_items', force: :cascade do |t|
256 | t.integer 'inventory_id', null: false
257 | t.integer 'item_id', null: false
258 | t.boolean 'equipped', default: false
259 | t.integer 'condition', default: 100
260 | t.json 'modifications'
261 | t.decimal 'price'
262 | t.string 'price_unit', default: 'gp'
263 | t.float 'weight'
264 | t.datetime 'created_at', precision: 6, null: false
265 | t.datetime 'updated_at', precision: 6, null: false
266 | t.index ['inventory_id'], name: 'index_inventory_items_on_inventory_id'
267 | t.index ['item_id'], name: 'index_inventory_items_on_item_id'
268 | end
269 |
270 | add_foreign_key 'campaigns_players', 'campaigns'
271 | add_foreign_key 'campaigns_players', 'players'
272 | add_foreign_key 'characters_feats', 'characters'
273 | add_foreign_key 'characters_feats', 'feats'
274 | add_foreign_key 'characters_flaws', 'characters'
275 | add_foreign_key 'characters_flaws', 'flaws'
276 | add_foreign_key 'characters_skills', 'characters'
277 | add_foreign_key 'characters_skills', 'skills'
278 | add_foreign_key 'characters_virtues', 'characters'
279 | add_foreign_key 'characters_virtues', 'virtues'
280 | add_foreign_key 'characters_spells', 'characters'
281 | add_foreign_key 'characters_spells', 'spells'
282 | add_foreign_key 'characters_specializations', 'characters'
283 | add_foreign_key 'characters_specializations', 'specializations'
284 | add_foreign_key 'characters', 'campaigns'
285 | add_foreign_key 'characters', 'players'
286 | add_foreign_key 'inventories', 'characters'
287 | add_foreign_key 'inventories_items', 'inventories'
288 | add_foreign_key 'inventories_items', 'items'
289 | end
290 | end
291 |
--------------------------------------------------------------------------------