develoz-rails 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (172) hide show
  1. checksums.yaml +7 -0
  2. data/.github/workflows/ci.yml +31 -0
  3. data/.github/workflows/release.yml +459 -0
  4. data/.rubocop.yml +52 -0
  5. data/.sisyphus/notepads/develoz-rails-template/learnings.md +855 -0
  6. data/.tool-versions +2 -0
  7. data/CHANGELOG.md +28 -0
  8. data/CONTRIBUTING.md +120 -0
  9. data/README.md +76 -0
  10. data/Rakefile +18 -0
  11. data/config/generators.yml +105 -0
  12. data/docs/guide/installation.md +574 -0
  13. data/exe/develoz +6 -0
  14. data/install +98 -0
  15. data/lib/develoz/canonical_fetcher.rb +56 -0
  16. data/lib/develoz/cli.rb +84 -0
  17. data/lib/develoz/generators/base.rb +134 -0
  18. data/lib/develoz/manifest.rb +44 -0
  19. data/lib/develoz/options.rb +55 -0
  20. data/lib/develoz/version.rb +5 -0
  21. data/lib/develoz/version_resolver.rb +103 -0
  22. data/lib/develoz.rb +15 -0
  23. data/lib/generators/develoz/active_resource/active_resource_generator.rb +25 -0
  24. data/lib/generators/develoz/active_resource/templates/application_resource.rb.tt +7 -0
  25. data/lib/generators/develoz/active_resource/templates/example_resource.rb.tt +5 -0
  26. data/lib/generators/develoz/admin/admin_generator.rb +44 -0
  27. data/lib/generators/develoz/admin/templates/admin_base_controller.rb.tt +7 -0
  28. data/lib/generators/develoz/admin/templates/admin_layout.html.erb.tt +38 -0
  29. data/lib/generators/develoz/admin/templates/dashboard_controller.rb.tt +7 -0
  30. data/lib/generators/develoz/admin/templates/dashboard_index.html.erb.tt +2 -0
  31. data/lib/generators/develoz/admin/templates/routes.rb.tt +3 -0
  32. data/lib/generators/develoz/agents_docs/agents_docs_generator.rb +50 -0
  33. data/lib/generators/develoz/agents_docs/templates/.github/pull_request_template.md.tt +13 -0
  34. data/lib/generators/develoz/agents_docs/templates/AGENTS.md.tt +36 -0
  35. data/lib/generators/develoz/agents_docs/templates/docs/development.md.tt +121 -0
  36. data/lib/generators/develoz/agents_docs/templates/docs/performance.md.tt +46 -0
  37. data/lib/generators/develoz/agents_docs/templates/docs/testing.md.tt +92 -0
  38. data/lib/generators/develoz/agents_docs/templates/spec/cassettes/Example_API/fetches_data_from_an_external_API.yml.tt +23 -0
  39. data/lib/generators/develoz/agents_docs/templates/spec/requests/example_api_spec.rb.tt +14 -0
  40. data/lib/generators/develoz/agents_docs/templates/spec/support/faraday.rb.tt +19 -0
  41. data/lib/generators/develoz/agents_docs/templates/spec/support/vcr.rb.tt +11 -0
  42. data/lib/generators/develoz/api/api_generator.rb +55 -0
  43. data/lib/generators/develoz/api/templates/app/blueprints/example_blueprint.rb.tt +6 -0
  44. data/lib/generators/develoz/api/templates/app/controllers/api/v1/base_controller.rb.tt +96 -0
  45. data/lib/generators/develoz/api/templates/config/initializers/blueprinter.rb.tt +16 -0
  46. data/lib/generators/develoz/api/templates/config/initializers/rswag_api.rb.tt +15 -0
  47. data/lib/generators/develoz/api/templates/config/initializers/rswag_ui.rb.tt +17 -0
  48. data/lib/generators/develoz/api/templates/spec/requests/api/v1/examples_spec.rb.tt +25 -0
  49. data/lib/generators/develoz/auth/auth_generator.rb +82 -0
  50. data/lib/generators/develoz/auth/templates/app/controllers/concerns/authentication.rb.tt +53 -0
  51. data/lib/generators/develoz/auth/templates/app/controllers/passwords_controller.rb.tt +35 -0
  52. data/lib/generators/develoz/auth/templates/app/controllers/sessions_controller.rb.tt +22 -0
  53. data/lib/generators/develoz/auth/templates/app/mailers/passwords_mailer.rb.tt +8 -0
  54. data/lib/generators/develoz/auth/templates/app/models/current.rb.tt +5 -0
  55. data/lib/generators/develoz/auth/templates/app/models/user.rb.tt +7 -0
  56. data/lib/generators/develoz/auth/templates/app/views/passwords/edit.html.erb.tt +9 -0
  57. data/lib/generators/develoz/auth/templates/app/views/passwords_mailer/reset.html.erb.tt +6 -0
  58. data/lib/generators/develoz/auth/templates/app/views/sessions/new.html.erb.tt +11 -0
  59. data/lib/generators/develoz/auth/templates/db/migrate/create_users.rb.tt +13 -0
  60. data/lib/generators/develoz/auth/templates/spec/requests/passwords_spec.rb.tt +60 -0
  61. data/lib/generators/develoz/auth/templates/spec/requests/sessions_spec.rb.tt +37 -0
  62. data/lib/generators/develoz/ci/ci_generator.rb +66 -0
  63. data/lib/generators/develoz/ci/templates/.github/workflows/ci.yml.tt +18 -0
  64. data/lib/generators/develoz/ci/templates/.haml-lint.yml.tt +12 -0
  65. data/lib/generators/develoz/ci/templates/.markdownlint.json.tt +10 -0
  66. data/lib/generators/develoz/ci/templates/.reek.yml.tt +28 -0
  67. data/lib/generators/develoz/ci/templates/.rubocop.yml.tt +22 -0
  68. data/lib/generators/develoz/ci/templates/.stylelintrc.json.tt +4 -0
  69. data/lib/generators/develoz/ci/templates/.yamllint.tt +14 -0
  70. data/lib/generators/develoz/ci/templates/Rakefile.tt +73 -0
  71. data/lib/generators/develoz/ci/templates/bin/ci.tt +6 -0
  72. data/lib/generators/develoz/ci/templates/biome.json.tt +41 -0
  73. data/lib/generators/develoz/ci/templates/config/ci.rb.tt +23 -0
  74. data/lib/generators/develoz/concerns/concerns_generator.rb +48 -0
  75. data/lib/generators/develoz/concerns/templates/app/models/concerns/configurable.rb.tt +120 -0
  76. data/lib/generators/develoz/concerns/templates/app/models/concerns/optimized_finders.rb.tt +93 -0
  77. data/lib/generators/develoz/concerns/templates/app/models/concerns/searchable_concern.rb.tt +31 -0
  78. data/lib/generators/develoz/concerns/templates/app/models/concerns/transitionable.rb.tt +144 -0
  79. data/lib/generators/develoz/concerns/templates/app/models/configuration.rb.tt +104 -0
  80. data/lib/generators/develoz/concerns/templates/db/migrate/add_status_transitions.rb.tt +30 -0
  81. data/lib/generators/develoz/concerns/templates/db/migrate/create_configurations.rb.tt +25 -0
  82. data/lib/generators/develoz/concerns/templates/spec/models/concerns/configurable_spec.rb.tt +172 -0
  83. data/lib/generators/develoz/concerns/templates/spec/models/concerns/optimized_finders_spec.rb.tt +200 -0
  84. data/lib/generators/develoz/concerns/templates/spec/models/concerns/searchable_concern_spec.rb.tt +84 -0
  85. data/lib/generators/develoz/concerns/templates/spec/models/concerns/transitionable_spec.rb.tt +234 -0
  86. data/lib/generators/develoz/concerns/templates/spec/models/configuration_spec.rb.tt +223 -0
  87. data/lib/generators/develoz/database/database_generator.rb +44 -0
  88. data/lib/generators/develoz/database/templates/config/database.yml.tt +53 -0
  89. data/lib/generators/develoz/database/templates/config/initializers/pg_search.rb.tt +5 -0
  90. data/lib/generators/develoz/database/templates/db/migrate/create_pg_extensions.rb.tt +13 -0
  91. data/lib/generators/develoz/db_backup/db_backup_generator.rb +47 -0
  92. data/lib/generators/develoz/db_backup/templates/backup_rake.tt +48 -0
  93. data/lib/generators/develoz/db_backup/templates/bin_db_backup.tt +25 -0
  94. data/lib/generators/develoz/db_backup/templates/compose_service.tt +14 -0
  95. data/lib/generators/develoz/doc_specs/doc_specs_generator.rb +33 -0
  96. data/lib/generators/develoz/doc_specs/templates/bin/generate-docs.tt +392 -0
  97. data/lib/generators/develoz/doc_specs/templates/lib/tasks/docs_check.rake.tt +8 -0
  98. data/lib/generators/develoz/doc_specs/templates/spec/support/doc_screenshot_helper.rb.tt +17 -0
  99. data/lib/generators/develoz/doc_specs/templates/spec/system/example_doc_spec.rb.tt +38 -0
  100. data/lib/generators/develoz/docker/docker_generator.rb +54 -0
  101. data/lib/generators/develoz/docker/templates/bin_dev.tt +5 -0
  102. data/lib/generators/develoz/docker/templates/bin_docker_entrypoint.tt +8 -0
  103. data/lib/generators/develoz/docker/templates/bin_run.tt +152 -0
  104. data/lib/generators/develoz/docker/templates/bin_setup.tt +15 -0
  105. data/lib/generators/develoz/docker/templates/docker-compose.yml.tt +59 -0
  106. data/lib/generators/develoz/docker/templates/dockerfile_dev.tt +24 -0
  107. data/lib/generators/develoz/docs_render/docs_render_generator.rb +46 -0
  108. data/lib/generators/develoz/docs_render/templates/app/assets/stylesheets/documentation.scss.tt +145 -0
  109. data/lib/generators/develoz/docs_render/templates/app/controllers/docs_controller.rb.tt +7 -0
  110. data/lib/generators/develoz/docs_render/templates/app/javascript/docs.js.tt +15 -0
  111. data/lib/generators/develoz/docs_render/templates/app/models/document.rb.tt +189 -0
  112. data/lib/generators/develoz/docs_render/templates/app/views/docs/show.html.erb.tt +7 -0
  113. data/lib/generators/develoz/docs_render/templates/config/initializers/redcarpet_rouge.rb.tt +16 -0
  114. data/lib/generators/develoz/frontend_core/frontend_core_generator.rb +40 -0
  115. data/lib/generators/develoz/frontend_core/templates/.annotaterb.yml.tt +16 -0
  116. data/lib/generators/develoz/frontend_core/templates/config/importmap.rb.tt +12 -0
  117. data/lib/generators/develoz/frontend_core/templates/config/initializers/pagy.rb.tt +10 -0
  118. data/lib/generators/develoz/install/install_generator.rb +51 -0
  119. data/lib/generators/develoz/kamal/kamal_generator.rb +59 -0
  120. data/lib/generators/develoz/kamal/templates/accessories_postgres.yml.tt +14 -0
  121. data/lib/generators/develoz/kamal/templates/deploy.yml.tt +43 -0
  122. data/lib/generators/develoz/kamal/templates/dockerfile_prod.tt +54 -0
  123. data/lib/generators/develoz/kamal/templates/kamal_secrets.tt +22 -0
  124. data/lib/generators/develoz/maintenance/maintenance_generator.rb +29 -0
  125. data/lib/generators/develoz/maintenance/templates/app/tasks/maintenance/example_task.rb.tt +20 -0
  126. data/lib/generators/develoz/maintenance/templates/lib/tasks/maintenance_counters.rake.tt +27 -0
  127. data/lib/generators/develoz/push/push_generator.rb +61 -0
  128. data/lib/generators/develoz/push/templates/app/javascript/pwa/subscription.js.tt +57 -0
  129. data/lib/generators/develoz/push/templates/app/models/push_subscription.rb.tt +14 -0
  130. data/lib/generators/develoz/push/templates/app/services/push_notification_service.rb.tt +36 -0
  131. data/lib/generators/develoz/push/templates/app/views/pwa/sw_push_handlers.js.tt +49 -0
  132. data/lib/generators/develoz/push/templates/constants_env.tt +5 -0
  133. data/lib/generators/develoz/push/templates/db/migrate/create_push_subscriptions.rb.tt +15 -0
  134. data/lib/generators/develoz/pwa/pwa_generator.rb +46 -0
  135. data/lib/generators/develoz/pwa/templates/app/controllers/pwa_controller.rb.tt +17 -0
  136. data/lib/generators/develoz/pwa/templates/app/javascript/pwa/registration.js.tt +25 -0
  137. data/lib/generators/develoz/pwa/templates/app/views/pwa/manifest.json.erb.tt +24 -0
  138. data/lib/generators/develoz/pwa/templates/app/views/pwa/offline.html.erb.tt +51 -0
  139. data/lib/generators/develoz/pwa/templates/app/views/pwa/service-worker.js.tt +109 -0
  140. data/lib/generators/develoz/pwa/templates/routes.rb.tt +3 -0
  141. data/lib/generators/develoz/solid/solid_generator.rb +52 -0
  142. data/lib/generators/develoz/solid/templates/app/jobs/application_job.rb.tt +7 -0
  143. data/lib/generators/develoz/solid/templates/config/cable.yml.tt +10 -0
  144. data/lib/generators/develoz/solid/templates/config/cache.yml.tt +16 -0
  145. data/lib/generators/develoz/solid/templates/config/initializers/mission_control.rb.tt +8 -0
  146. data/lib/generators/develoz/solid/templates/config/initializers/solid.rb.tt +7 -0
  147. data/lib/generators/develoz/solid/templates/config/queue.yml.tt +18 -0
  148. data/lib/generators/develoz/solid/templates/config/recurring.yml.tt +20 -0
  149. data/lib/generators/develoz/strict_loading/strict_loading_generator.rb +17 -0
  150. data/lib/generators/develoz/strict_loading/templates/config/initializers/strict_loading.rb.tt +13 -0
  151. data/lib/generators/develoz/testing/templates/rspec.tt +2 -0
  152. data/lib/generators/develoz/testing/templates/rspec_parallel.tt +2 -0
  153. data/lib/generators/develoz/testing/templates/spec/rails_helper.rb.tt +32 -0
  154. data/lib/generators/develoz/testing/templates/spec/spec_helper.rb.tt +31 -0
  155. data/lib/generators/develoz/testing/testing_generator.rb +39 -0
  156. data/lib/generators/develoz/tooling/templates/constants.rb.tt +17 -0
  157. data/lib/generators/develoz/tooling/templates/env.example.tt +4 -0
  158. data/lib/generators/develoz/tooling/templates/vscode/extensions.json.tt +8 -0
  159. data/lib/generators/develoz/tooling/templates/vscode/settings.json.tt +26 -0
  160. data/lib/generators/develoz/tooling/templates/vscode/tasks.json.tt +26 -0
  161. data/lib/generators/develoz/tooling/tooling_generator.rb +33 -0
  162. data/lib/generators/develoz/ui/templates/bin/setup_develoz_ui.tt +20 -0
  163. data/lib/generators/develoz/ui/ui_generator.rb +57 -0
  164. data/lib/generators/develoz/versioning/templates/app/helpers/application_helper.rb.tt +7 -0
  165. data/lib/generators/develoz/versioning/templates/app/views/shared/_app_version.html.erb.tt +3 -0
  166. data/lib/generators/develoz/versioning/templates/spec/helpers/application_helper_spec.rb.tt +11 -0
  167. data/lib/generators/develoz/versioning/versioning_generator.rb +48 -0
  168. data/lib/tasks/canonical.rake +27 -0
  169. data/templates/.keep +0 -0
  170. data/templates/CANONICAL_SOURCES.md +93 -0
  171. data/templates/pull_request_template.md.tt +13 -0
  172. metadata +383 -0
@@ -0,0 +1,223 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails_helper"
4
+
5
+ RSpec.describe Configuration, type: :model do
6
+ let(:configurable_class) { stub_const("ConfigurationTarget", Struct.new(:id)) }
7
+
8
+ describe "validations" do
9
+ it "requires a key" do
10
+ configuration = described_class.new(value: {})
11
+
12
+ expect(configuration).not_to be_valid
13
+ expect(configuration.errors[:key]).to include("can't be blank")
14
+ end
15
+
16
+ it "requires the key to be unique within its configurable" do
17
+ target = configurable_class.new(1)
18
+ described_class.create!(key: "feature", value: true,
19
+ configurable_type: target.class.name, configurable_id: target.id)
20
+ duplicate = described_class.new(key: "feature", value: false,
21
+ configurable_type: target.class.name, configurable_id: target.id)
22
+
23
+ expect(duplicate).not_to be_valid
24
+ expect(duplicate.errors[:key]).to include("has already been taken")
25
+ end
26
+
27
+ it "allows the same key for a different configurable" do
28
+ described_class.create!(key: "feature", value: true,
29
+ configurable_type: "ConfigurationTarget", configurable_id: 1)
30
+ configuration = described_class.new(key: "feature", value: false,
31
+ configurable_type: "ConfigurationTarget", configurable_id: 2)
32
+
33
+ expect(configuration).to be_valid
34
+ end
35
+ end
36
+
37
+ describe "scopes" do
38
+ it "returns active configurations" do
39
+ active = described_class.create!(key: "active", value: true)
40
+ described_class.create!(key: "inactive", value: true, active: false)
41
+
42
+ expect(described_class.active).to contain_exactly(active)
43
+ end
44
+
45
+ it "separates global and individual configurations" do
46
+ global = described_class.create!(key: "global", value: true)
47
+ individual = described_class.create!(key: "individual", value: true,
48
+ configurable_type: "ConfigurationTarget", configurable_id: 1)
49
+ described_class.create!(key: "partial", value: true, configurable_type: "ConfigurationTarget")
50
+
51
+ expect(described_class.globals).to contain_exactly(global)
52
+ expect(described_class.individuals).to contain_exactly(individual)
53
+ end
54
+
55
+ it "finds configurations for an object" do
56
+ target = configurable_class.new(7)
57
+ matching = described_class.create!(key: "matching", value: true,
58
+ configurable_type: target.class.name, configurable_id: target.id)
59
+ described_class.create!(key: "other", value: true,
60
+ configurable_type: target.class.name, configurable_id: 8)
61
+
62
+ expect(described_class.for_configurable(target)).to contain_exactly(matching)
63
+ end
64
+
65
+ it "finds configurations for a type and id hash" do
66
+ matching = described_class.create!(key: "matching", value: true,
67
+ configurable_type: "ConfigurationTarget", configurable_id: 7)
68
+
69
+ expect(described_class.for_configurable(type: "ConfigurationTarget", id: 7)).to contain_exactly(matching)
70
+ end
71
+
72
+ it "treats nil as the global configurable" do
73
+ global = described_class.create!(key: "global", value: true)
74
+ described_class.create!(key: "individual", value: true,
75
+ configurable_type: "ConfigurationTarget", configurable_id: 7)
76
+
77
+ expect(described_class.for_configurable(nil)).to contain_exactly(global)
78
+ end
79
+
80
+ it "combines scoped and global configurations" do
81
+ target = configurable_class.new(7)
82
+ global = described_class.create!(key: "global", value: true)
83
+ scoped = described_class.create!(key: "scoped", value: true,
84
+ configurable_type: target.class.name, configurable_id: target.id)
85
+ described_class.create!(key: "other", value: true,
86
+ configurable_type: target.class.name, configurable_id: 8)
87
+
88
+ expect(described_class.all_for(target)).to contain_exactly(global, scoped)
89
+ end
90
+
91
+ it "normalizes blank configurable values to nil" do
92
+ configuration = described_class.create!(key: "global", value: true,
93
+ configurable_type: "", configurable_id: "")
94
+
95
+ expect(configuration).to have_attributes(configurable_type: nil, configurable_id: nil)
96
+ end
97
+ end
98
+
99
+ describe ".global_value" do
100
+ it "returns top-level and nested global values" do
101
+ value = { "nested" => { "enabled" => "true" }, "label" => "plain" }
102
+ described_class.create!(key: "features", value: value)
103
+
104
+ expect(described_class.global_value("features")).to eq(value)
105
+ expect(described_class.global_value("features.nested.enabled")).to eq("true")
106
+ end
107
+
108
+ it "returns nil for a missing key or a path through a scalar" do
109
+ described_class.create!(key: "features", value: { "label" => "plain" })
110
+
111
+ expect(described_class.global_value("missing.path")).to be_nil
112
+ expect(described_class.global_value("features.label.enabled")).to be_nil
113
+ end
114
+ end
115
+
116
+ describe ".feature_enabled?" do
117
+ it "casts the global value to a boolean" do
118
+ described_class.create!(key: "enabled_feature", value: "true")
119
+ described_class.create!(key: "disabled_feature", value: "false")
120
+
121
+ expect(described_class.feature_enabled?("enabled_feature")).to be(true)
122
+ expect(described_class.feature_enabled?("disabled_feature")).to be(false)
123
+ end
124
+ end
125
+
126
+ describe "#json_value" do
127
+ it "parses assigned JSON" do
128
+ configuration = described_class.new(key: "json", value: {})
129
+
130
+ configuration.json_value = '{"enabled":true}'
131
+
132
+ expect(configuration.value).to eq("enabled" => true)
133
+ end
134
+
135
+ it "ignores a blank assignment" do
136
+ configuration = described_class.new(key: "json", value: { "unchanged" => true })
137
+
138
+ configuration.json_value = ""
139
+
140
+ expect(configuration.value).to eq("unchanged" => true)
141
+ end
142
+
143
+ it "returns pretty JSON for a value" do
144
+ configuration = described_class.new(key: "json", value: { "enabled" => true })
145
+
146
+ expect(JSON.parse(configuration.json_value)).to eq("enabled" => true)
147
+ end
148
+
149
+ it "returns nil without a value" do
150
+ expect(described_class.new(key: "json", value: nil).json_value).to be_nil
151
+ end
152
+ end
153
+
154
+ describe "#yaml_value" do
155
+ it "parses and validates assigned YAML" do
156
+ configuration = described_class.new(key: "yaml", value: {})
157
+
158
+ configuration.yaml_value = "enabled: true\n"
159
+
160
+ expect(configuration).to be_valid
161
+ expect(configuration.value).to eq("enabled" => true)
162
+ expect(configuration.yaml_value).to eq("enabled: true\n")
163
+ end
164
+
165
+ it "ignores a blank assignment" do
166
+ configuration = described_class.new(key: "yaml", value: { "unchanged" => true })
167
+
168
+ configuration.yaml_value = ""
169
+
170
+ expect(configuration.value).to eq("unchanged" => true)
171
+ end
172
+
173
+ it "reports invalid YAML" do
174
+ configuration = described_class.new(key: "yaml", value: {})
175
+
176
+ configuration.yaml_value = "enabled: ["
177
+
178
+ expect(configuration).not_to be_valid
179
+ expect(configuration.errors[:yaml_value].first).to start_with("is not valid YAML:")
180
+ end
181
+
182
+ it "serializes the stored value when no raw YAML is present" do
183
+ configuration = described_class.new(key: "yaml", value: { "enabled" => true })
184
+
185
+ expect(YAML.safe_load(configuration.yaml_value)).to eq("enabled" => true)
186
+ end
187
+
188
+ it "returns nil without a stored or raw value" do
189
+ expect(described_class.new(key: "yaml", value: nil).yaml_value).to be_nil
190
+ end
191
+ end
192
+
193
+ describe "#configurable_display_name" do
194
+ it "returns nil without a configurable id" do
195
+ configuration = described_class.new(configurable_type: "User", configurable_id: nil)
196
+
197
+ expect(configuration.configurable_display_name).to be_nil
198
+ end
199
+
200
+ it "returns the id for an unsupported configurable type" do
201
+ configuration = described_class.new(configurable_type: "Other", configurable_id: "7")
202
+
203
+ expect(configuration.configurable_display_name).to eq("7")
204
+ end
205
+
206
+ it "returns the configurable display name when the record exists" do
207
+ display_record = Struct.new(:display_name).new("Ada Lovelace")
208
+ user_class = stub_const("User", Class.new)
209
+ user_class.define_singleton_method(:find_by) { |id:| id == "7" ? display_record : nil }
210
+ configuration = described_class.new(configurable_type: "User", configurable_id: "7")
211
+
212
+ expect(configuration.configurable_display_name).to eq("Ada Lovelace")
213
+ end
214
+
215
+ it "falls back to the id when the record does not exist" do
216
+ user_class = stub_const("User", Class.new)
217
+ user_class.define_singleton_method(:find_by) { |id:| nil }
218
+ configuration = described_class.new(configurable_type: "User", configurable_id: "missing")
219
+
220
+ expect(configuration.configurable_display_name).to eq("missing")
221
+ end
222
+ end
223
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "develoz"
4
+
5
+ module Develoz
6
+ module Generators
7
+ class DatabaseGenerator < Develoz::Generators::Base
8
+ def self.source_root
9
+ File.expand_path("templates", __dir__)
10
+ end
11
+
12
+ def add_database_gems
13
+ add_gem "pg"
14
+ add_gem "pg_search"
15
+ end
16
+
17
+ def create_database_config
18
+ template "config/database.yml.tt", "config/database.yml"
19
+ end
20
+
21
+ def create_pg_search_initializer
22
+ template "config/initializers/pg_search.rb.tt", "config/initializers/pg_search.rb"
23
+ end
24
+
25
+ def create_pg_extensions_migration
26
+ return if migration_exists?("create_pg_extensions")
27
+
28
+ template "db/migrate/create_pg_extensions.rb.tt",
29
+ "db/migrate/#{next_migration_timestamp}_create_pg_extensions.rb"
30
+ end
31
+
32
+ def ensure_postgres_tool_version
33
+ tool_versions = ".tool-versions"
34
+ path = File.join(destination_root, tool_versions)
35
+
36
+ if File.exist?(path)
37
+ inject_once(into: tool_versions, content: "postgres 18")
38
+ else
39
+ create_file(tool_versions, "postgres 18\n")
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,53 @@
1
+ # PostgreSQL 18+ configuration.
2
+ # Requires PostgreSQL 18 or newer.
3
+ default: &default
4
+ adapter: postgresql
5
+ encoding: unicode
6
+ pool: <%%= ENV.fetch("DATABASE_POOL", 10) %>
7
+ host: <%%= ENV.fetch("DATABASE_HOST", "localhost") %>
8
+ port: <%%= ENV.fetch("DATABASE_PORT", 5432) %>
9
+ username: <%%= ENV.fetch("DATABASE_USER", "postgres") %>
10
+ password: <%%= ENV.fetch("DATABASE_PASSWORD", "") %>
11
+
12
+ development:
13
+ primary:
14
+ <<: *default
15
+ database: <%= app_name %>_development
16
+ queue:
17
+ <<: *default
18
+ database: <%= app_name %>_queue_development
19
+ cache:
20
+ <<: *default
21
+ database: <%= app_name %>_cache_development
22
+ cable:
23
+ <<: *default
24
+ database: <%= app_name %>_cable_development
25
+
26
+ test:
27
+ primary:
28
+ <<: *default
29
+ database: <%= app_name %>_test<%%= ENV.fetch("TEST_ENV_NUMBER", "") %>
30
+ queue:
31
+ <<: *default
32
+ database: <%= app_name %>_queue_test<%= ENV.fetch("TEST_ENV_NUMBER", "") %>
33
+ cache:
34
+ <<: *default
35
+ database: <%= app_name %>_cache_test<%= ENV.fetch("TEST_ENV_NUMBER", "") %>
36
+ cable:
37
+ <<: *default
38
+ database: <%= app_name %>_cable_test<%= ENV.fetch("TEST_ENV_NUMBER", "") %>
39
+
40
+ production:
41
+ primary:
42
+ <<: *default
43
+ url: <%%= ENV.fetch("DATABASE_URL", "") %>
44
+ database: <%= app_name %>_production
45
+ queue:
46
+ <<: *default
47
+ database: <%= app_name %>_queue_production
48
+ cache:
49
+ <<: *default
50
+ database: <%= app_name %>_cache_production
51
+ cable:
52
+ <<: *default
53
+ database: <%= app_name %>_cable_production
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ # PgSearch is included in models via the searchable concern (see T15).
4
+ # This initializer is reserved for future global PgSearch configuration.
5
+ # https://github.com/Casecommons/pg_search
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ class CreatePgExtensions < ActiveRecord::Migration[8.0]
4
+ def up
5
+ execute "CREATE EXTENSION IF NOT EXISTS pg_trgm"
6
+ execute "CREATE EXTENSION IF NOT EXISTS unaccent"
7
+ end
8
+
9
+ def down
10
+ execute "DROP EXTENSION IF EXISTS unaccent"
11
+ execute "DROP EXTENSION IF EXISTS pg_trgm"
12
+ end
13
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "develoz"
4
+
5
+ module Develoz
6
+ module Generators
7
+ class DbBackupGenerator < Develoz::Generators::Base
8
+ def self.source_root
9
+ File.expand_path("templates", __dir__)
10
+ end
11
+
12
+ class_option :docker, type: :boolean, default: false
13
+
14
+ def create_backup_script
15
+ template "bin_db_backup.tt", "bin/db-backup"
16
+ chmod "bin/db-backup", 0o755
17
+ end
18
+
19
+ def create_backup_rake
20
+ template "backup_rake.tt", "lib/tasks/backup.rake"
21
+ end
22
+
23
+ def inject_compose_service
24
+ return unless options[:docker]
25
+
26
+ inject_once(
27
+ into: "docker-compose.yml",
28
+ after: /^services:\n/,
29
+ marker: "# db-backup service (develoz:db_backup)",
30
+ content: compose_service_content
31
+ )
32
+ end
33
+
34
+ def ensure_backups_gitignored
35
+ ensure_gitignore("/backups/")
36
+ end
37
+
38
+ private
39
+
40
+ def compose_service_content
41
+ template_path = File.join(self.class.source_root, "compose_service.tt")
42
+ raw = File.read(template_path)
43
+ ERB.new(raw, trim_mode: "-").result(binding)
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ namespace :backup do
4
+ desc "Create a gzipped PostgreSQL backup in ./backups (DATABASE_URL, BACKUP_RETENTION_DAYS)"
5
+ task create: :environment do
6
+ require "open3"
7
+
8
+ retention_days = ENV.fetch("BACKUP_RETENTION_DAYS", "7").to_i
9
+ backup_dir = Rails.root.join("backups")
10
+ FileUtils.mkdir_p(backup_dir)
11
+
12
+ timestamp = Time.current.strftime("%Y%m%d_%H%M%S")
13
+ backup_file = backup_dir.join("db_#{timestamp}.sql.gz")
14
+
15
+ puts "Creating backup: #{backup_file}"
16
+
17
+ Open3.popen3("pg_dump", ENV.fetch("DATABASE_URL")) do |_, stdout, _, wait|
18
+ Zlib::GzipWriter.open(backup_file) do |gz|
19
+ gz.write(stdout.read)
20
+ end
21
+ raise "pg_dump failed" unless wait.value.success?
22
+ end
23
+
24
+ puts "Backup created: #{backup_file}"
25
+
26
+ prune(backup_dir, retention_days)
27
+ end
28
+
29
+ desc "Prune backups older than BACKUP_RETENTION_DAYS (default: 7)"
30
+ task prune: :environment do
31
+ retention_days = ENV.fetch("BACKUP_RETENTION_DAYS", "7").to_i
32
+ prune(Rails.root.join("backups"), retention_days)
33
+ end
34
+
35
+ def prune(backup_dir, retention_days)
36
+ cutoff = retention_days.days.ago
37
+ removed = 0
38
+
39
+ Dir.glob(backup_dir.join("db_*.sql.gz")).each do |file|
40
+ next unless File.mtime(file) < cutoff
41
+
42
+ File.delete(file)
43
+ removed += 1
44
+ end
45
+
46
+ puts "Pruned #{removed} backup(s) older than #{retention_days} days."
47
+ end
48
+ end
@@ -0,0 +1,25 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ # bin/db-backup - PostgreSQL backup with retention
5
+ # Usage: bin/db-backup
6
+ # Env: DATABASE_URL, BACKUP_RETENTION_DAYS (default: 7)
7
+
8
+ RETENTION_DAYS="${BACKUP_RETENTION_DAYS:-7}"
9
+ BACKUP_DIR="${BACKUP_DIR:-./backups}"
10
+ TIMESTAMP="$(date +%Y%m%d_%H%M%S)"
11
+ BACKUP_FILE="${BACKUP_DIR}/db_${TIMESTAMP}.sql.gz"
12
+
13
+ mkdir -p "${BACKUP_DIR}"
14
+
15
+ echo "Creating backup: ${BACKUP_FILE}"
16
+
17
+ pg_dump "${DATABASE_URL}" | gzip > "${BACKUP_FILE}"
18
+
19
+ echo "Backup created: ${BACKUP_FILE}"
20
+
21
+ echo "Pruning backups older than ${RETENTION_DAYS} days..."
22
+ find "${BACKUP_DIR}" -name "db_*.sql.gz" -type f -mtime "+${RETENTION_DAYS}" -delete
23
+
24
+ echo "Done. Current backups:"
25
+ ls -1 "${BACKUP_DIR}"/db_*.sql.gz 2>/dev/null || echo " (none)"
@@ -0,0 +1,14 @@
1
+ # db-backup service (develoz:db_backup)
2
+ db-backup:
3
+ image: postgres:18
4
+ environment:
5
+ DATABASE_URL: ${DATABASE_URL:-postgres://postgres:postgres@postgres:5432/<%= app_name %>_development}
6
+ BACKUP_RETENTION_DAYS: ${BACKUP_RETENTION_DAYS:-7}
7
+ volumes:
8
+ - ./backups:/backups
9
+ - postgres_data:/var/lib/postgresql/data:ro
10
+ entrypoint: ["/bin/sh", "-c"]
11
+ command: ["while true; do bin/db-backup; sleep 21600; done"]
12
+ depends_on:
13
+ postgres:
14
+ condition: service_healthy
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "develoz"
4
+
5
+ module Develoz
6
+ module Generators
7
+ class DocSpecsGenerator < Develoz::Generators::Base
8
+ def self.source_root
9
+ File.expand_path("templates", __dir__)
10
+ end
11
+
12
+ def create_generate_docs_script
13
+ template "bin/generate-docs.tt", "bin/generate-docs"
14
+ dest = File.join(destination_root, "bin/generate-docs")
15
+ File.chmod(0o755, dest)
16
+ end
17
+
18
+ def create_doc_screenshot_helper
19
+ template "spec/support/doc_screenshot_helper.rb.tt",
20
+ "spec/support/doc_screenshot_helper.rb"
21
+ end
22
+
23
+ def create_docs_check_rake_task
24
+ template "lib/tasks/docs_check.rake.tt", "lib/tasks/docs_check.rake"
25
+ end
26
+
27
+ def create_example_system_spec
28
+ template "spec/system/example_doc_spec.rb.tt",
29
+ "spec/system/example_doc_spec.rb"
30
+ end
31
+ end
32
+ end
33
+ end