gemstack-cli 0.1.0 → 0.2.1

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 (43) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +8 -0
  3. data/lib/gemstack/cli/add_generator.rb +2 -0
  4. data/lib/gemstack/cli/app_generator.rb +28 -3
  5. data/lib/gemstack/cli/commands/db.rb +4 -3
  6. data/lib/gemstack/cli/commands/jobs.rb +4 -4
  7. data/lib/gemstack/cli/deploy_generator.rb +40 -2
  8. data/lib/gemstack/cli/doctor.rb +39 -9
  9. data/lib/gemstack/cli/generator.rb +22 -0
  10. data/lib/gemstack/cli/job_generator.rb +1 -0
  11. data/lib/gemstack/cli/resource_generator.rb +1 -0
  12. data/lib/gemstack/cli.rb +3 -1
  13. data/templates/app/Gemfile.tt +5 -0
  14. data/templates/app/app/jobs/application_job.rb +9 -0
  15. data/templates/app/app/mailers/application_mailer.rb +8 -0
  16. data/templates/app/app/mailers/templates/dot_keep +0 -0
  17. data/templates/app/app/models/application_model.rb +19 -0
  18. data/templates/app/app/serializers/application_serializer.rb +6 -0
  19. data/templates/app/config/app.rb.tt +1 -1
  20. data/templates/app/config/database.yml.tt +61 -0
  21. data/templates/app/config/environments/development.rb.tt +35 -0
  22. data/templates/app/config/environments/production.rb.tt +40 -0
  23. data/templates/app/config/environments/test.rb.tt +21 -0
  24. data/templates/app/dot_env.example.tt +19 -0
  25. data/templates/app/dot_gitignore +2 -0
  26. data/templates/auth/app/mailers/auth_mailer.rb +1 -1
  27. data/templates/auth/app/models/auth_token.rb +1 -1
  28. data/templates/auth/app/models/user.rb +1 -1
  29. data/templates/auth/app/serializers/api_token_serializer.rb +1 -1
  30. data/templates/auth/app/serializers/new_api_token_serializer.rb +1 -1
  31. data/templates/auth/app/serializers/user_serializer.rb +1 -1
  32. data/templates/auth/db/migrations/%timestamp%_create_auth_tables.rb +8 -8
  33. data/templates/deploy/Dockerfile.tt +4 -3
  34. data/templates/deploy/compose.yaml.tt +51 -2
  35. data/templates/job/app/jobs/%file_name%.rb.tt +1 -1
  36. data/templates/resource/model/app/models/%file_name%.rb.tt +1 -1
  37. data/templates/resource/serializer/app/serializers/%file_name%_serializer.rb.tt +1 -1
  38. data/templates/storage/app/serializers/upload_serializer.rb +1 -1
  39. metadata +15 -9
  40. data/templates/app/config/environments/development.rb +0 -6
  41. data/templates/app/config/environments/production.rb +0 -8
  42. data/templates/app/config/environments/test.rb +0 -7
  43. data/templates/app/dot_env.example +0 -9
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 1a7919922a3f8ef313ffffa34f0a8884905d6f552772d99ef38908696640de5a
4
- data.tar.gz: 65dc878039db71201b6ab2514649e52d1f0b1ede7d4d0c52299cfa7c8e68b8c8
3
+ metadata.gz: 9d9243d3986f9d45d5aa9de84f699ea7fcac37504ed266d7998e5a4eab6ff484
4
+ data.tar.gz: 6cf9300583fb65e103c7b71b48859721863766837180a362365639ec0186c5fc
5
5
  SHA512:
6
- metadata.gz: 6d00e50a3e84430210d7810dba99a89ebce41a7c9ddac5ede6dfe15f76d78f96ad60f15fa2ad4b24fcb2f5e8e49da602f3f7d81f736c72d2239fb92b8b3f9a68
7
- data.tar.gz: 62a0bf6f9e50c21a636d27964a478aafd465f4ccf51f1012c94d57d8ad6e02e4822191bc87f928c5f3d76135ca968d55439a6adaefd801be94aa7555494d55c0
6
+ metadata.gz: 54009542d20d3bcc5ae67e2b89e19965c285e32b6400ca43e7ecf22af1f2bc3a8303803c2c1708cc967f5c50fed4c159e7807e5f41bdb9da702c87f9cc4e8229
7
+ data.tar.gz: 0acb8e759970a5154f11fae58d530c960575849ec4f93018f46b9a8ef70f1d43cfac289c0606ae16508efd16f843ade368b0fa49777e15caacdb42e42e9db22a
data/CHANGELOG.md CHANGED
@@ -1,5 +1,13 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.2.1
4
+
5
+ See the [GemStack changelog](https://github.com/gemstack-rb/gemstack/blob/main/CHANGELOG.md).
6
+
7
+ ## 0.2.0
8
+
9
+ See the [GemStack changelog](https://github.com/gemstack-rb/gemstack/blob/main/CHANGELOG.md).
10
+
3
11
  ## 0.1.0
4
12
 
5
13
  First release. See the [GemStack changelog](https://github.com/gemstack-rb/gemstack/blob/main/CHANGELOG.md).
@@ -70,6 +70,7 @@ module GemStack
70
70
 
71
71
  def add_storage
72
72
  add_gem("gemstack-storage")
73
+ ensure_base_classes(@root, :serializer)
73
74
  copy_feature_templates("storage")
74
75
  add_routes(%(post "/uploads", to: "uploads#create" # direct uploads (gemstack add storage)\n),
75
76
  marker: "uploads#create")
@@ -94,6 +95,7 @@ module GemStack
94
95
  add_gem("gemstack-mail")
95
96
  add_gem("gemstack-auth")
96
97
  JobGenerator.install_migration(@root, output: @output) # emails are sent from background jobs
98
+ ensure_base_classes(@root, :model, :serializer, :mailer)
97
99
  copy_feature_templates("auth", skip_migration: "*_create_auth_tables.rb")
98
100
  include_in_application_controller("GemStack::Auth::Controller")
99
101
  add_routes(AUTH_ROUTES, marker: "/auth/login")
@@ -6,6 +6,12 @@ module GemStack
6
6
  # framework infrastructure and deliberately no business resources.
7
7
  class AppGenerator < Generator
8
8
  NAME = /\A[a-z][a-z0-9_-]*\z/
9
+ # `--database` values (Rails' names) and aliases.
10
+ DATABASES = { "sqlite3" => "sqlite3", "sqlite" => "sqlite3", "postgresql" => "postgresql",
11
+ "postgres" => "postgresql", "pg" => "postgresql", "mysql2" => "mysql2", "mysql" => "mysql2",
12
+ "trilogy" => "trilogy" }.freeze
13
+ DRIVERS = { "sqlite3" => ["sqlite3", "~> 2.0"], "postgresql" => ["pg", "~> 1.5"],
14
+ "mysql2" => ["mysql2", "~> 0.5"], "trilogy" => ["trilogy", "~> 2.9"] }.freeze
9
15
 
10
16
  # The gems/ directory of the GemStack checkout this CLI runs from, if any.
11
17
  CHECKOUT = File.expand_path("../../../..", __dir__)
@@ -29,10 +35,27 @@ module GemStack
29
35
  def frontend? = !@options[:skip_frontend]
30
36
  def database? = !@options[:skip_database]
31
37
 
38
+ # sqlite3 (default), postgresql, mysql2 or trilogy.
39
+ def database_adapter
40
+ @database_adapter ||= DATABASES.fetch(@options[:database].to_s.downcase.then do |d|
41
+ d.empty? ? "sqlite3" : d
42
+ end) do
43
+ raise Thor::Error,
44
+ "Unknown --database #{@options[:database].inspect}. Use: sqlite3, postgresql, mysql2, trilogy"
45
+ end
46
+ end
47
+
48
+ def driver_gem = DRIVERS.fetch(database_adapter).first
49
+ def driver_version = DRIVERS.fetch(database_adapter).last
50
+ def database_name = name.tr("-", "_")
51
+
32
52
  def run
33
53
  validate!
54
+ database_adapter if database? # fail early on an unknown --database
34
55
  @output.puts("Creating GemStack application #{name} in #{destination}")
35
- render_directory("app", destination, skip: ->(rel) { !database? && rel.start_with?("db/") })
56
+ render_directory("app", destination, skip: lambda { |rel|
57
+ !database? && (rel.start_with?("db/", "app/models/") || rel == "config/database.yml.tt")
58
+ })
36
59
  render_directory("frontend", File.join(destination, "frontend")) if frontend?
37
60
  install unless @options[:skip_install]
38
61
  git_init unless @options[:skip_git]
@@ -63,16 +86,18 @@ module GemStack
63
86
 
64
87
  run_step("npm install", File.join(destination, "frontend"), "npm", "install", "--no-fund",
65
88
  "--no-audit")
89
+ # The TypeScript contract, so frontend/lib/api/generated exists from the start.
90
+ run_step("gemstack contract", destination, "bin/gemstack", "contract", "--quiet") if bundled
66
91
  end
67
92
 
68
- # Best effort: a missing or password-protected PostgreSQL shouldn't fail `new`.
93
+ # Best effort: a missing or password-protected database server shouldn't fail `new`.
69
94
  def create_database
70
95
  @output.puts(" #{"run".rjust(9)} gemstack db:create")
71
96
  ok = unbundled { system("bin/gemstack", "db:create", chdir: destination, out: File::NULL, err: File::NULL) }
72
97
  return if ok
73
98
 
74
99
  @output.puts(" #{"warning".rjust(9)} couldn't create the database — " \
75
- "set DATABASE_URL in .env, then run `gemstack db:create`")
100
+ "check config/database.yml (or set DATABASE_URL in .env), then run `gemstack db:create`")
76
101
  end
77
102
 
78
103
  def git_init
@@ -8,7 +8,7 @@ module GemStack
8
8
 
9
9
  class_option :environment, aliases: "-e", type: :string, desc: "Environment (default: GEMSTACK_ENV or development)"
10
10
 
11
- desc "db:create", "Create the database (config.db.url)"
11
+ desc "db:create", "Create the database (config/database.yml or DATABASE_URL)"
12
12
  def db_create
13
13
  with_database do
14
14
  result = DB::Tasks.create
@@ -112,8 +112,9 @@ module GemStack
112
112
  GemStack.boot! if boot
113
113
  yield
114
114
  rescue Sequel::DatabaseConnectionError => e
115
- abort("Can't connect to #{redact(DB.config.url)}: #{e.message.lines.first.strip}\n" \
116
- "Set DATABASE_URL (e.g. in .env) — see docs/models.md.")
115
+ settings = DB.settings
116
+ abort("Can't connect to #{DB::Configuration.describe(settings)} (from #{settings[:source]}): " \
117
+ "#{e.message.lines.first.strip}\nCheck config/database.yml or DATABASE_URL — see docs/database.md.")
117
118
  rescue GemStack::Error => e
118
119
  abort(e.message)
119
120
  end
@@ -25,7 +25,7 @@ module GemStack
25
25
  desc "jobs:status", "Show queued, scheduled, running and failed jobs per queue"
26
26
  def jobs_status
27
27
  with_jobs(quiet: true) do
28
- stats = Jobs::Adapters::Postgres.new.stats
28
+ stats = Jobs::Adapters::Database.new.stats
29
29
  return say("No jobs.") if stats.empty?
30
30
 
31
31
  rows = stats.sort.map { |queue, s| [queue, s[:ready], s[:scheduled], s[:running], s[:failed]].map(&:to_s) }
@@ -37,7 +37,7 @@ module GemStack
37
37
  method_option :limit, type: :numeric, default: 20
38
38
  def jobs_failed
39
39
  with_jobs(quiet: true) do
40
- failed = Jobs::Adapters::Postgres.new.failed(limit: options[:limit])
40
+ failed = Jobs::Adapters::Database.new.failed(limit: options[:limit])
41
41
  return say("No failed jobs.") if failed.empty?
42
42
 
43
43
  failed.each do |job|
@@ -51,7 +51,7 @@ module GemStack
51
51
  desc "jobs:retry [IDS...]", "Put failed jobs back on the queue (all failed jobs without IDS)"
52
52
  def jobs_retry(*ids)
53
53
  with_jobs(quiet: true) do
54
- count = Jobs::Adapters::Postgres.new.retry_failed(ids.empty? ? nil : ids.map(&:to_i))
54
+ count = Jobs::Adapters::Database.new.retry_failed(ids.empty? ? nil : ids.map(&:to_i))
55
55
  say("Re-queued #{count} job(s).")
56
56
  end
57
57
  end
@@ -59,7 +59,7 @@ module GemStack
59
59
  desc "jobs:discard [IDS...]", "Delete failed jobs (all failed jobs without IDS)"
60
60
  def jobs_discard(*ids)
61
61
  with_jobs(quiet: true) do
62
- count = Jobs::Adapters::Postgres.new.discard_failed(ids.empty? ? nil : ids.map(&:to_i))
62
+ count = Jobs::Adapters::Database.new.discard_failed(ids.empty? ? nil : ids.map(&:to_i))
63
63
  say("Deleted #{count} failed job(s).")
64
64
  end
65
65
  end
@@ -18,11 +18,49 @@ module GemStack
18
18
  def jobs?
19
19
  return true if gemfile.match?(/^\s*gem "gemstack-auth"/)
20
20
 
21
- gemfile.match?(/^\s*gem "gemstack-jobs"/) &&
22
- %w[app/jobs/*.rb app/mailers/*.rb].any? { |glob| !Dir.glob(File.join(@root, glob)).empty? }
21
+ gemfile.match?(/^\s*gem "gemstack-jobs"/) && Generator.background_work?(@root)
23
22
  end
24
23
 
25
24
  def mail? = gemfile.match?(/^\s*gem "gemstack-(mail|auth)"/)
25
+ def database? = gemfile.match?(/^\s*gem "gemstack-db"/)
26
+ def realtime? = gemfile.match?(/^\s*gem "gemstack-realtime"/)
27
+
28
+ # From config/database.yml (production, else the first adapter listed);
29
+ # apps without one use PostgreSQL, GemStack's original default.
30
+ def database_adapter
31
+ path = File.join(@root, "config/database.yml")
32
+ return "postgresql" unless File.file?(path)
33
+
34
+ text = File.read(path)
35
+ production = text[/^production:.*?(?=^\S|\z)/m].to_s
36
+ (production[/^\s+adapter:\s*(\w+)/, 1] || text[/^\s+adapter:\s*(\w+)/, 1] || "postgresql")
37
+ .then { |name| { "postgres" => "postgresql", "sqlite" => "sqlite3", "mysql" => "mysql2" }.fetch(name, name) }
38
+ end
39
+
40
+ def sqlite? = database_adapter == "sqlite3"
41
+ def mysql? = %w[mysql2 trilogy].include?(database_adapter)
42
+ def postgresql? = database_adapter == "postgresql"
43
+ # A Redis for realtime fan-out when the database can't do it (PostgreSQL uses NOTIFY).
44
+ def redis? = realtime? && !postgresql?
45
+
46
+ def build_packages
47
+ (%w[build-essential libyaml-dev
48
+ git] + { "postgresql" => ["libpq-dev"], "mysql2" => ["default-libmysqlclient-dev"] }.fetch(database_adapter,
49
+ [])).join(" ")
50
+ end
51
+
52
+ def runtime_packages
53
+ (%w[libyaml-0-2
54
+ curl] + { "postgresql" => ["libpq5"], "mysql2" => ["libmariadb3"] }.fetch(database_adapter, [])).join(" ")
55
+ end
56
+
57
+ def database_url
58
+ case database_adapter
59
+ when "sqlite3" then "sqlite3:/data/production.sqlite3"
60
+ when "postgresql" then "postgres://app:${POSTGRES_PASSWORD:?set POSTGRES_PASSWORD}@db:5432/app"
61
+ else "#{database_adapter}://app:${MYSQL_PASSWORD:?set MYSQL_PASSWORD}@db:3306/app"
62
+ end
63
+ end
26
64
 
27
65
  def ruby_version
28
66
  pinned = File.file?(File.join(@root, ".ruby-version")) && File.read(File.join(@root, ".ruby-version")).strip
@@ -35,6 +35,7 @@ module GemStack
35
35
  database_ok = check_database
36
36
  check_migrations_and_jobs if database_ok
37
37
  check_cache
38
+ check_realtime_broker
38
39
  check_contract if database_ok
39
40
  end
40
41
  check_production_settings if @production
@@ -130,20 +131,36 @@ module GemStack
130
131
  def check_database
131
132
  return true unless defined?(GemStack::DB)
132
133
 
133
- url = GemStack::DB.config.url
134
+ settings = GemStack::DB.settings
135
+ where = "#{GemStack::DB::Configuration.describe(settings)}, from #{settings[:source]}"
136
+ if settings[:adapter] == "sqlite" && !GemStack::DB::Tasks.exists?(settings)
137
+ problem("SQLite database file missing (#{where})", "run: gemstack db:create")
138
+ return false
139
+ end
134
140
  GemStack::DB.connection.test_connection
135
- pass("PostgreSQL reachable (#{redact(url)})")
141
+ pass("database reachable (#{where})")
136
142
  true
137
- rescue StandardError => e
143
+ rescue StandardError, LoadError => e
138
144
  message = e.message.lines.first.to_s.strip
139
145
  message = message.split(/FATAL:\s*/, 2).last if message.include?("FATAL:")
140
- hint = if message.include?("does not exist") then "run: gemstack db:create"
141
- else "is PostgreSQL running? Set DATABASE_URL (now #{redact(url.to_s)}) in .env or the environment"
146
+ hint = if message.match?(/does not exist|Unknown database/) then "run: gemstack db:create"
147
+ elsif e.is_a?(GemStack::ConfigurationError) then "fix config/database.yml or DATABASE_URL"
148
+ else "is the database server running? Check config/database.yml or DATABASE_URL (#{where})"
142
149
  end
143
- problem("PostgreSQL: #{message}", hint)
150
+ problem("database: #{message}", hint)
144
151
  false
145
152
  end
146
153
 
154
+ # MySQL/SQLite apps: a jobs worker is a separate process, and the memory
155
+ # broker can't carry its broadcasts to the web server.
156
+ def check_realtime_broker
157
+ return unless defined?(GemStack::Realtime) && defined?(GemStack::Jobs) && GemStack::Jobs.database_queue?
158
+ return unless GemStack.config.realtime.broker.to_s == "memory" && jobs_used?
159
+
160
+ caution("realtime uses the in-process memory broker",
161
+ "broadcasts from jobs won't reach the web server — set config.realtime.broker = :redis (REDIS_URL)")
162
+ end
163
+
147
164
  def check_migrations_and_jobs
148
165
  pending = GemStack::DB::Migrator.new.pending
149
166
  if pending.empty?
@@ -151,7 +168,7 @@ module GemStack
151
168
  else
152
169
  caution("#{pending.size} pending migration(s)", "run: gemstack db:migrate")
153
170
  end
154
- return unless defined?(GemStack::Jobs) && GemStack.config.jobs.adapter.to_s == "postgres"
171
+ return unless defined?(GemStack::Jobs) && GemStack::Jobs.database_queue?
155
172
  return unless jobs_used?
156
173
 
157
174
  if GemStack::DB.connection.table_exists?(:gemstack_jobs)
@@ -194,7 +211,7 @@ module GemStack
194
211
  elsif secret.length < 64 then problem("SECRET_KEY_BASE is too short", "use: openssl rand -hex 64")
195
212
  else pass("SECRET_KEY_BASE is set")
196
213
  end
197
- env_check("DATABASE_URL", "the production PostgreSQL URL") if defined?(GemStack::DB)
214
+ check_production_database if defined?(GemStack::DB)
198
215
  if defined?(GemStack::Mail) && GemStack.config.mail.delivery.to_s == "smtp"
199
216
  env_check("SMTP_URL", "e.g. smtp://user:password@smtp.example.com:587")
200
217
  end
@@ -205,6 +222,19 @@ module GemStack
205
222
  env_check("REDIS_URL", "the Redis used by the cache")
206
223
  end
207
224
 
225
+ def check_production_database
226
+ settings = GemStack::DB.settings
227
+ if settings[:source].start_with?("default")
228
+ problem("no production database configured",
229
+ "set DATABASE_URL, or a production section in config/database.yml")
230
+ elsif settings[:adapter] == "sqlite"
231
+ caution("SQLite in production (#{settings[:database]})",
232
+ "keep the file on a persistent volume and run a single server; back it up")
233
+ else
234
+ pass("production database: #{GemStack::DB::Configuration.describe(settings)} (#{settings[:source]})")
235
+ end
236
+ end
237
+
208
238
  def check_storage_settings
209
239
  return unless GemStack.config.storage.service.to_s == "s3"
210
240
 
@@ -260,7 +290,7 @@ module GemStack
260
290
  end
261
291
 
262
292
  def jobs_used?
263
- %w[app/jobs/*.rb app/mailers/*.rb].any? { |glob| !Dir.glob(path(glob)).empty? }
293
+ Generator.background_work?(@root)
264
294
  end
265
295
 
266
296
  def location(error)
@@ -59,6 +59,28 @@ module GemStack
59
59
  end
60
60
 
61
61
  # Every template file for a generator, with app overrides applied.
62
+ # The app's base classes (written by `gemstack new` since 0.2.1). Apps
63
+ # created earlier get the ones a generator's code inherits from.
64
+ BASE_CLASSES = {
65
+ model: "app/models/application_model.rb", serializer: "app/serializers/application_serializer.rb",
66
+ job: "app/jobs/application_job.rb", mailer: "app/mailers/application_mailer.rb"
67
+ }.freeze
68
+
69
+ # Whether the app has jobs or mailers of its own (base classes don't count).
70
+ def self.background_work?(root)
71
+ %w[app/jobs/*.rb app/mailers/*.rb].any? do |glob|
72
+ Dir.glob(File.join(root, glob)).any? { |file| !File.basename(file).start_with?("application_") }
73
+ end
74
+ end
75
+
76
+ def ensure_base_classes(root, *kinds)
77
+ kinds.each do |kind|
78
+ rel = BASE_CLASSES.fetch(kind)
79
+ target = File.join(root, rel)
80
+ write(target, File.read(File.join(template_root("app"), rel))) unless File.exist?(target)
81
+ end
82
+ end
83
+
62
84
  def template_files(name, override_root: nil)
63
85
  files = relative_files(template_root(name)).to_h { |rel| [rel, File.join(template_root(name), rel)] }
64
86
  if override_root
@@ -35,6 +35,7 @@ module GemStack
35
35
  end
36
36
 
37
37
  def run
38
+ ensure_base_classes(@root, :job)
38
39
  template_files("job", override_root: @root).each do |rel, source|
39
40
  target = File.join(@root, rel.delete_suffix(".tt").gsub("%file_name%", file_name))
40
41
  write(target, render(File.read(source), source))
@@ -34,6 +34,7 @@ module GemStack
34
34
  end
35
35
 
36
36
  def run
37
+ ensure_base_classes(@root, *(%i[model serializer] & @parts))
37
38
  @parts.each { |part| render_part(part) }
38
39
  add_routes if @parts.include?(:controller)
39
40
  self
data/lib/gemstack/cli.rb CHANGED
@@ -53,7 +53,9 @@ module GemStack
53
53
  method_option :skip_install, type: :boolean, default: false, desc: "Don't run bundle install / npm install"
54
54
  method_option :skip_git, type: :boolean, default: false, desc: "Don't initialise a git repository"
55
55
  method_option :skip_frontend, type: :boolean, default: false, desc: "API only: no Next.js frontend"
56
- method_option :skip_database, type: :boolean, default: false, desc: "No PostgreSQL / gemstack-db"
56
+ method_option :database, aliases: "-d", type: :string, default: "sqlite3",
57
+ desc: "sqlite3 (default), postgresql, mysql2 or trilogy"
58
+ method_option :skip_database, type: :boolean, default: false, desc: "No database / gemstack-db"
57
59
  method_option :gemstack_path, type: :string, desc: "Use GemStack from a local checkout (its gems/ directory)"
58
60
  def new(name)
59
61
  AppGenerator.new(name, options.transform_keys(&:to_sym)).run
@@ -14,6 +14,7 @@ path "<%= gemstack_path %>" do
14
14
  gem "gemstack-db"
15
15
  <% end -%>
16
16
  gem "gemstack-jobs"
17
+ gem "gemstack-mail"
17
18
  gem "gemstack-dev"
18
19
  gem "gemstack-http"
19
20
  gem "gemstack-schema"
@@ -24,8 +25,12 @@ gem "gemstack", "~> <%= version %>"
24
25
  gem "gemstack-db", "~> <%= version %>"
25
26
  <% end -%>
26
27
  gem "gemstack-jobs", "~> <%= version %>"
28
+ gem "gemstack-mail", "~> <%= version %>"
27
29
  <% end -%>
28
30
  gem "puma", ">= 6.4"
31
+ <% if database? -%>
32
+ gem "<%= driver_gem %>", "<%= driver_version %>" # the <%= database_adapter %> driver (config/database.yml)
33
+ <% end -%>
29
34
 
30
35
  # Any gem works here as usual, e.g.:
31
36
  # gem "brotli" # Brotli compression (gzip is used without it)
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ # The base class for this app's background jobs (docs/background-jobs.md):
4
+ # retry and discard rules declared here apply to every job.
5
+ class ApplicationJob < GemStack::Job
6
+ # retry_on Net::ReadTimeout, attempts: 5, wait: 30 # seconds, :exponential or ->(attempt) { … }
7
+ # discard_on GemStack::NotFound # e.g. the record was deleted before the job ran
8
+ # queue :default
9
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ # The base class for this app's mailers (docs/mail.md). Templates live in
4
+ # app/mailers/templates/<mailer>/<action>.{text,html}.erb.
5
+ class ApplicationMailer < GemStack::Mailer
6
+ # default from: "My App <hello@example.com>" # otherwise config.mail.default_from (MAIL_FROM)
7
+ # default reply_to: "support@example.com"
8
+ end
File without changes
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ # The base class for this app's models — add plugins, validations and helpers
4
+ # every model should share here. `class Product < ApplicationModel` maps to the
5
+ # products table (docs/models.md).
6
+ #
7
+ # Created with Class.new on purpose: `class ApplicationModel < GemStack::Model`
8
+ # would make Sequel look for an "application_models" table.
9
+ ApplicationModel = Class.new(GemStack::Model) # rubocop:disable Style/EmptyClassDefinition
10
+
11
+ class ApplicationModel
12
+ # Sequel plugins for every model, e.g.:
13
+ # plugin :dirty # previous values after an update (column_changes)
14
+
15
+ # Shared query helpers:
16
+ # dataset_module do
17
+ # def recent = order(Sequel.desc(:created_at))
18
+ # end
19
+ end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ # The base class for this app's serializers (docs/serialization.md): attributes
4
+ # and helpers defined here are shared by every serializer.
5
+ class ApplicationSerializer < GemStack::Serializer
6
+ end
@@ -23,7 +23,7 @@ GemStack.configure do |config|
23
23
  # config.filter_parameters += %w[iban] # masked in logs
24
24
  <% if database? -%>
25
25
  #
26
- # config.db.url = ENV["DATABASE_URL"] # default: postgres:///<name>_<env> (TEST_DATABASE_URL in test)
26
+ # Connection: config/database.yml (DATABASE_URL / TEST_DATABASE_URL override it).
27
27
  # config.db.pool_size = 5 # default: GEMSTACK_MAX_THREADS (one per Puma thread)
28
28
  # config.db.statement_timeout = 5_000 # ms
29
29
  <% end -%>
@@ -0,0 +1,61 @@
1
+ # Database connection (docs/database.md) — the same format as Rails' database.yml.
2
+ # DATABASE_URL (TEST_DATABASE_URL in tests) overrides this file when set, which
3
+ # is what most hosting platforms provide. Adapters: sqlite3, postgresql, mysql2,
4
+ # trilogy (add the matching gem: sqlite3, pg, mysql2, trilogy).
5
+ default: &default
6
+ <% if database_adapter == "sqlite3" -%>
7
+ adapter: sqlite3
8
+ pool: <%%= ENV.fetch("GEMSTACK_MAX_THREADS", 5) %>
9
+
10
+ development:
11
+ <<: *default
12
+ database: db/development.sqlite3
13
+
14
+ test:
15
+ <<: *default
16
+ database: db/test.sqlite3
17
+
18
+ # Keep the file on persistent storage (a volume); or set DATABASE_URL=sqlite3:/data/app.sqlite3.
19
+ production:
20
+ <<: *default
21
+ database: db/production.sqlite3
22
+ <% elsif database_adapter == "postgresql" -%>
23
+ adapter: postgresql
24
+ pool: <%%= ENV.fetch("GEMSTACK_MAX_THREADS", 5) %>
25
+ # Without host/username, the local server is used as your OS user.
26
+ # host: localhost
27
+ # username: <%= database_name %>
28
+ # password: <%%= ENV["DATABASE_PASSWORD"] %>
29
+
30
+ development:
31
+ <<: *default
32
+ database: <%= database_name %>_development
33
+
34
+ test:
35
+ <<: *default
36
+ database: <%= database_name %>_test
37
+
38
+ # In production, DATABASE_URL (postgres://user:password@host:5432/name) usually replaces this.
39
+ production:
40
+ <<: *default
41
+ database: <%= database_name %>_production
42
+ <% else -%>
43
+ adapter: <%= database_adapter %>
44
+ pool: <%%= ENV.fetch("GEMSTACK_MAX_THREADS", 5) %>
45
+ host: <%%= ENV.fetch("DATABASE_HOST", "127.0.0.1") %>
46
+ username: root
47
+ password: <%%= ENV["DATABASE_PASSWORD"] %>
48
+
49
+ development:
50
+ <<: *default
51
+ database: <%= database_name %>_development
52
+
53
+ test:
54
+ <<: *default
55
+ database: <%= database_name %>_test
56
+
57
+ # In production, DATABASE_URL (<%= database_adapter %>://user:password@host:3306/name) usually replaces this.
58
+ production:
59
+ <<: *default
60
+ database: <%= database_name %>_production
61
+ <% end -%>
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Development settings: applied after config/app.rb when GEMSTACK_ENV=development
4
+ # (the default). Each line shows the default value — uncomment one to change it.
5
+ # All settings: docs/configuration.md.
6
+ GemStack.configure do |config|
7
+ # ── Code ────────────────────────────────────────────────────────────────
8
+ # config.reload_code = true # reload app/ and config/routes.rb when they change
9
+ # config.eager_load = false # load app/ lazily, for a faster boot
10
+ # config.jit = nil # :yjit or :zjit to try Ruby's JIT locally
11
+
12
+ # ── Logging ─────────────────────────────────────────────────────────────
13
+ # config.logger.level = :debug # :debug, :info, :warn, :error (or GEMSTACK_LOG_LEVEL)
14
+ # config.logger.format = :pretty # readable lines; :json to see what production logs
15
+
16
+ # ── HTTP ────────────────────────────────────────────────────────────────
17
+ # config.http.show_exceptions = true # 500s carry the exception; browsers get an error page
18
+ # config.contract.docs = true # interactive API docs at /api/docs
19
+
20
+ # ── gemstack dev ────────────────────────────────────────────────────────
21
+ # config.dev.port = 3000 # the one public port (or PORT)
22
+ # config.dev.jobs_command = %w[bundle exec gemstack jobs] # nil: don't start a jobs worker
23
+ <% if database? -%>
24
+
25
+ # ── Database (connection: config/database.yml) ──────────────────────────
26
+ # config.db.log_queries = true # every SQL statement at DEBUG
27
+ # config.db.slow_query_ms = 500 # slower queries are logged at WARN
28
+ <% end -%>
29
+
30
+ # ── Cache ───────────────────────────────────────────────────────────────
31
+ # config.cache.store = :memory # :null to switch caching off, :redis to share it
32
+
33
+ # ── Mail ────────────────────────────────────────────────────────────────
34
+ # config.mail.delivery = :log # saved to tmp/mail (.eml + .html); :smtp really sends
35
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Production settings: applied after config/app.rb when GEMSTACK_ENV=production.
4
+ # Each line shows the default value — uncomment one to change it.
5
+ #
6
+ # Secrets and connections come from environment variables, never from files:
7
+ # SECRET_KEY_BASE, <% if database? %>DATABASE_URL, <% end %>SMTP_URL, MAIL_FROM, APP_URL, REDIS_URL…
8
+ # `gemstack doctor --production` checks what's missing (docs/deployment.md).
9
+ GemStack.configure do |config|
10
+ # ── Code ────────────────────────────────────────────────────────────────
11
+ # config.eager_load = true # load all code at boot (copy-on-write friendly with Puma workers)
12
+ # config.jit = :yjit # Ruby's JIT (or GEMSTACK_JIT=yjit|zjit|off)
13
+
14
+ # ── Logging ─────────────────────────────────────────────────────────────
15
+ # config.logger.level = :info
16
+ # config.logger.format = :json # one JSON object per line, with the request id
17
+
18
+ # ── HTTP ────────────────────────────────────────────────────────────────
19
+ # config.http.show_exceptions = false # never send exception details to clients
20
+ # config.http.hsts = "max-age=63072000; includeSubDomains" # on HTTPS requests; nil if your proxy sets it
21
+ # config.http.cors.origins = ["https://app.example.com"] # only if the frontend is on another domain
22
+ # config.http.max_body_size = 10 * 1024 * 1024 # larger requests get 413
23
+ <% if database? -%>
24
+
25
+ # ── Database ────────────────────────────────────────────────────────────
26
+ # config.db.pool_size = 5 # connections per process (GEMSTACK_MAX_THREADS)
27
+ # config.db.statement_timeout = 5_000 # ms; stop runaway queries (PostgreSQL, MySQL)
28
+ # config.db.slow_query_ms = 500 # slower queries are logged at WARN
29
+ <% end -%>
30
+
31
+ # ── Cache: :memory is per process — use Redis with several processes or servers ──
32
+ # config.cache.store = :redis # add gem "redis-client"; REDIS_URL
33
+
34
+ # ── Background jobs (`gemstack jobs`) ───────────────────────────────────
35
+ # config.jobs.concurrency = 5 # threads per worker (or GEMSTACK_JOB_CONCURRENCY)
36
+
37
+ # ── Mail ────────────────────────────────────────────────────────────────
38
+ # config.mail.delivery = :smtp # SMTP_URL=smtp://user:password@smtp.example.com:587
39
+ # config.mail.default_from = "App <hello@example.com>" # or MAIL_FROM
40
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Test settings: applied after config/app.rb when tests run (`gemstack test`).
4
+ # Each line shows the default value — uncomment one to change it.
5
+ GemStack.configure do |config|
6
+ # ── Logging ─────────────────────────────────────────────────────────────
7
+ # config.logger.level = :debug # logs are discarded in tests unless this (or GEMSTACK_LOG_LEVEL) is set
8
+
9
+ # ── HTTP ────────────────────────────────────────────────────────────────
10
+ # config.http.show_exceptions = true # a failing request's JSON includes the exception
11
+ <% if database? -%>
12
+
13
+ # ── Database (connection: the test section of config/database.yml, or TEST_DATABASE_URL) ──
14
+ # config.db.log_queries = false
15
+ <% end -%>
16
+
17
+ # ── Background work is recorded, not performed ──────────────────────────
18
+ # config.jobs.adapter = :test # assert_enqueued / perform_enqueued_jobs; :inline runs jobs at once
19
+ # config.mail.delivery = :test # assert_emails / last_email
20
+ # config.cache.store = :null # no caching, so tests can't leak into each other; :memory to test it
21
+ end
@@ -0,0 +1,19 @@
1
+ # Copy to .env for local development overrides. Never commit real secrets.
2
+ # Variables already set in your shell always take precedence.
3
+
4
+ # PORT=3000 # public development port (gemstack dev)
5
+ # GEMSTACK_LOG_LEVEL=debug
6
+
7
+ <% if database? -%>
8
+ # Database: config/database.yml. A URL here overrides it (hosting platforms set DATABASE_URL).
9
+ <% if database_adapter == "sqlite3" -%>
10
+ # DATABASE_URL=sqlite3:db/development.sqlite3
11
+ <% elsif database_adapter == "postgresql" -%>
12
+ # DATABASE_URL=postgres://user:password@localhost:5432/<%= database_name %>_development
13
+ # TEST_DATABASE_URL=postgres://user:password@localhost:5432/<%= database_name %>_test
14
+ <% else -%>
15
+ # DATABASE_PASSWORD=
16
+ # DATABASE_URL=<%= database_adapter %>://root:password@127.0.0.1:3306/<%= database_name %>_development
17
+ # TEST_DATABASE_URL=<%= database_adapter %>://root:password@127.0.0.1:3306/<%= database_name %>_test
18
+ <% end -%>
19
+ <% end -%>
@@ -1,6 +1,8 @@
1
1
  /.bundle/
2
2
  /log/
3
3
  /tmp/
4
+ /db/*.sqlite3
5
+ /db/*.sqlite3-*
4
6
  .env
5
7
  .env.local
6
8
  .env.*.local
@@ -2,7 +2,7 @@
2
2
 
3
3
  # Account emails. Templates: app/mailers/templates/auth_mailer/*.erb.
4
4
  # Links point at the Next.js pages (config.auth.app_url, APP_URL in production).
5
- class AuthMailer < GemStack::Mailer
5
+ class AuthMailer < ApplicationMailer
6
6
  def password_reset(user_id)
7
7
  @user = User[user_id] or return
8
8
  token, = GemStack::Auth::Tokens.issue(@user.id, purpose: "password_reset")
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # API tokens (and single-use reset/verification tokens) — see GemStack::Auth::Tokens.
4
- class AuthToken < GemStack::Model
4
+ class AuthToken < ApplicationModel
5
5
  field :purpose, :string, null: false
6
6
  field :name, :string, size: 100
7
7
  field :last_used_at, :datetime
@@ -2,7 +2,7 @@
2
2
 
3
3
  # Accounts. GemStack::Auth::User adds password hashing (Argon2id), email
4
4
  # normalization, validations and User.authenticate_by(email:, password:).
5
- class User < GemStack::Model
5
+ class User < ApplicationModel
6
6
  include GemStack::Auth::User
7
7
 
8
8
  field :email, :string, null: false, size: 254
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- class ApiTokenSerializer < GemStack::Serializer
3
+ class ApiTokenSerializer < ApplicationSerializer
4
4
  model AuthToken
5
5
  attributes :id, :name, :last_used_at, :expires_at, :created_at
6
6
  end
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # The response to creating an API token: the only time the token itself is shown.
4
- class NewApiTokenSerializer < GemStack::Serializer
4
+ class NewApiTokenSerializer < ApplicationSerializer
5
5
  Result = Data.define(:token, :api_token)
6
6
 
7
7
  attribute :token, :string
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # What the API shows about the signed-in user. Never add password_digest.
4
- class UserSerializer < GemStack::Serializer
4
+ class UserSerializer < ApplicationSerializer
5
5
  attributes :id, :email, :email_verified_at, :created_at
6
6
  end
@@ -5,8 +5,8 @@ Sequel.migration do
5
5
  change do
6
6
  create_table(:users) do
7
7
  primary_key :id, type: :Bignum
8
- column :email, :text, null: false, unique: true # stored lowercased
9
- column :password_digest, :text, null: false # Argon2id
8
+ String :email, null: false, unique: true # stored lowercased
9
+ String :password_digest, null: false # Argon2id
10
10
  column :email_verified_at, :timestamptz
11
11
  column :created_at, :timestamptz, null: false
12
12
  column :updated_at, :timestamptz, null: false
@@ -17,9 +17,9 @@ Sequel.migration do
17
17
  create_table(:sessions) do
18
18
  primary_key :id, type: :Bignum
19
19
  foreign_key :user_id, :users, type: :Bignum, null: false, on_delete: :cascade, index: true
20
- column :token_digest, :text, null: false, unique: true
20
+ String :token_digest, size: 64, null: false, unique: true
21
21
  column :ip, :inet
22
- column :user_agent, :text
22
+ String :user_agent
23
23
  column :created_at, :timestamptz, null: false
24
24
  column :last_seen_at, :timestamptz, null: false
25
25
  column :expires_at, :timestamptz, null: false, index: true
@@ -29,10 +29,10 @@ Sequel.migration do
29
29
  create_table(:auth_tokens) do
30
30
  primary_key :id, type: :Bignum
31
31
  foreign_key :user_id, :users, type: :Bignum, null: false, on_delete: :cascade, index: true
32
- column :purpose, :text, null: false
33
- column :token_digest, :text, null: false, unique: true
34
- column :name, :text
35
- column :email, :text
32
+ String :purpose, size: 30, null: false
33
+ String :token_digest, size: 64, null: false, unique: true
34
+ String :name
35
+ String :email
36
36
  column :created_at, :timestamptz, null: false
37
37
  column :last_used_at, :timestamptz
38
38
  column :expires_at, :timestamptz
@@ -14,7 +14,7 @@ ARG NODE_VERSION=<%= node_version %>
14
14
 
15
15
  # ── Ruby API ────────────────────────────────────────────────────────────────
16
16
  FROM ruby:${RUBY_VERSION}-slim AS api-build
17
- RUN apt-get update -qq && apt-get install --no-install-recommends -y build-essential libpq-dev libyaml-dev git \
17
+ RUN apt-get update -qq && apt-get install --no-install-recommends -y <%= build_packages %> \
18
18
  && rm -rf /var/lib/apt/lists/*
19
19
  WORKDIR /app
20
20
  ENV BUNDLE_DEPLOYMENT=1 BUNDLE_WITHOUT="development:test" BUNDLE_PATH=/usr/local/bundle
@@ -25,9 +25,10 @@ COPY . .
25
25
  RUN rm -rf frontend tmp/* log/* .env*
26
26
 
27
27
  FROM ruby:${RUBY_VERSION}-slim AS api
28
- RUN apt-get update -qq && apt-get install --no-install-recommends -y libpq5 libyaml-0-2 curl \
28
+ RUN apt-get update -qq && apt-get install --no-install-recommends -y <%= runtime_packages %> \
29
29
  && rm -rf /var/lib/apt/lists/* \
30
- && groupadd --system --gid 1000 app && useradd --system --uid 1000 --gid app --create-home app
30
+ && groupadd --system --gid 1000 app && useradd --system --uid 1000 --gid app --create-home app<% if sqlite? %> \
31
+ && mkdir -p /data && chown app:app /data<% end %>
31
32
  WORKDIR /app
32
33
  ENV GEMSTACK_ENV=production BUNDLE_DEPLOYMENT=1 BUNDLE_WITHOUT="development:test" BUNDLE_PATH=/usr/local/bundle \
33
34
  GEMSTACK_API_HOST=0.0.0.0 GEMSTACK_API_PORT=4000
@@ -1,16 +1,25 @@
1
1
  # Runs <%= app_name %> in production mode on this machine, behind Caddy on https://localhost:
2
2
  #
3
- # export SECRET_KEY_BASE=$(openssl rand -hex 64) POSTGRES_PASSWORD=$(openssl rand -hex 16)
3
+ # export SECRET_KEY_BASE=$(openssl rand -hex 64)<% if postgresql? %> POSTGRES_PASSWORD=$(openssl rand -hex 16)<% elsif mysql? %> MYSQL_PASSWORD=$(openssl rand -hex 16)<% end %>
4
4
  # docker compose up --build
5
5
  #
6
6
  # It's also a map of what a real deploy needs: one image per process type,
7
7
  # migrations before the API starts, and a proxy sending /api to Ruby.
8
+ <% if sqlite? -%>
9
+ # SQLite: the database file lives on the `data` volume, shared by the API, the
10
+ # jobs worker and migrations — keep them on one host.
11
+ <% end -%>
8
12
  name: <%= app_name.tr("_", "-") %>
9
13
 
10
14
  x-api-env: &api-env
11
15
  GEMSTACK_ENV: production
12
16
  SECRET_KEY_BASE: ${SECRET_KEY_BASE:?set SECRET_KEY_BASE (openssl rand -hex 64)}
13
- DATABASE_URL: postgres://app:${POSTGRES_PASSWORD:?set POSTGRES_PASSWORD}@db:5432/app
17
+ <% if database? -%>
18
+ DATABASE_URL: <%= database_url %>
19
+ <% end -%>
20
+ <% if redis? -%>
21
+ REDIS_URL: redis://redis:6379/0 # realtime fan-out between the API and the jobs worker
22
+ <% end -%>
14
23
  APP_URL: ${APP_URL:-https://localhost}
15
24
  <% if mail? -%>
16
25
  SMTP_URL: ${SMTP_URL:-} # e.g. smtp://user:pass@smtp.example.com:587 — emails retry until it is set
@@ -18,6 +27,7 @@ x-api-env: &api-env
18
27
  <% end -%>
19
28
 
20
29
  services:
30
+ <% if postgresql? -%>
21
31
  db:
22
32
  image: postgres:17
23
33
  environment:
@@ -30,21 +40,53 @@ services:
30
40
  interval: 2s
31
41
  retries: 30
32
42
 
43
+ <% elsif mysql? -%>
44
+ db:
45
+ image: mysql:8.4
46
+ environment:
47
+ MYSQL_DATABASE: app
48
+ MYSQL_USER: app
49
+ MYSQL_PASSWORD: ${MYSQL_PASSWORD:?set MYSQL_PASSWORD}
50
+ MYSQL_RANDOM_ROOT_PASSWORD: "yes"
51
+ volumes: [db:/var/lib/mysql]
52
+ healthcheck:
53
+ test: ["CMD-SHELL", "mysqladmin ping -h 127.0.0.1 -uapp -p$$MYSQL_PASSWORD --silent"]
54
+ interval: 3s
55
+ retries: 40
56
+
57
+ <% end -%>
58
+ <% if redis? -%>
59
+ redis:
60
+ image: redis:7
61
+ command: ["redis-server", "--save", "", "--appendonly", "no"]
62
+
63
+ <% end -%>
64
+ <% if database? -%>
33
65
  migrate:
34
66
  image: <%= app_name %>-api
35
67
  build: { context: ., target: api }
36
68
  healthcheck: { disable: true }
37
69
  command: ["bundle", "exec", "gemstack", "db:migrate"]
38
70
  environment: *api-env
71
+ <% if sqlite? -%>
72
+ volumes: [data:/data]
73
+ <% else -%>
39
74
  depends_on: { db: { condition: service_healthy } }
75
+ <% end -%>
40
76
 
77
+ <% end -%>
41
78
  api:
42
79
  image: <%= app_name %>-api
43
80
  build: { context: ., target: api }
44
81
  environment:
45
82
  <<: *api-env
46
83
  WEB_CONCURRENCY: ${WEB_CONCURRENCY:-2}
84
+ <% if sqlite? -%>
85
+ volumes: [data:/data]
86
+ <% end -%>
87
+ <% if database? -%>
47
88
  depends_on: { migrate: { condition: service_completed_successfully } }
89
+ <% end -%>
48
90
  <% if jobs? -%>
49
91
 
50
92
  jobs:
@@ -53,6 +95,9 @@ services:
53
95
  healthcheck: { disable: true }
54
96
  command: ["bundle", "exec", "gemstack", "jobs"]
55
97
  environment: *api-env
98
+ <% if sqlite? -%>
99
+ volumes: [data:/data]
100
+ <% end -%>
56
101
  depends_on: { migrate: { condition: service_completed_successfully } }
57
102
  <% end -%>
58
103
  <% if frontend? -%>
@@ -76,5 +121,9 @@ services:
76
121
  depends_on: [api<%= ", web" if frontend? %>]
77
122
 
78
123
  volumes:
124
+ <% if sqlite? -%>
125
+ data:
126
+ <% elsif database? -%>
79
127
  db:
128
+ <% end -%>
80
129
  caddy:
@@ -2,7 +2,7 @@
2
2
 
3
3
  # Enqueue with <%= class_name %>.perform_later(...) — arguments must be JSON values (pass ids, not records).
4
4
  # Runs in `gemstack jobs` (and in `gemstack dev`). Delivery is at-least-once: keep perform idempotent.
5
- class <%= class_name %> < GemStack::Job
5
+ class <%= class_name %> < ApplicationJob
6
6
  <% if queue_name -%>
7
7
  queue :<%= queue_name %>
8
8
  <% end -%>
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- class <%= class_name %> < GemStack::Model
3
+ class <%= class_name %> < ApplicationModel
4
4
  <% fields.each do |field| -%>
5
5
  <%= model_field(field) %>
6
6
  <% end -%>
@@ -2,6 +2,6 @@
2
2
 
3
3
  # What the API returns for a <%= class_name %>. Only listed attributes are exposed;
4
4
  # their types (from <%= class_name %>'s fields) drive the generated TypeScript types.
5
- class <%= class_name %>Serializer < GemStack::Serializer
5
+ class <%= class_name %>Serializer < ApplicationSerializer
6
6
  attributes :id, <%= fields.map { |f| ":#{f.column}" }.join(", ") %>, :created_at, :updated_at
7
7
  end
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # Where and how the browser sends the file (see UploadsController).
4
- class UploadSerializer < GemStack::Serializer
4
+ class UploadSerializer < ApplicationSerializer
5
5
  Result = Data.define(:signed_id, :url, :http_method, :headers)
6
6
 
7
7
  attribute :signed_id, :string
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: gemstack-cli
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Shoaib Malik
@@ -15,28 +15,28 @@ dependencies:
15
15
  requirements:
16
16
  - - '='
17
17
  - !ruby/object:Gem::Version
18
- version: 0.1.0
18
+ version: 0.2.1
19
19
  type: :runtime
20
20
  prerelease: false
21
21
  version_requirements: !ruby/object:Gem::Requirement
22
22
  requirements:
23
23
  - - '='
24
24
  - !ruby/object:Gem::Version
25
- version: 0.1.0
25
+ version: 0.2.1
26
26
  - !ruby/object:Gem::Dependency
27
27
  name: gemstack-dev
28
28
  requirement: !ruby/object:Gem::Requirement
29
29
  requirements:
30
30
  - - '='
31
31
  - !ruby/object:Gem::Version
32
- version: 0.1.0
32
+ version: 0.2.1
33
33
  type: :runtime
34
34
  prerelease: false
35
35
  version_requirements: !ruby/object:Gem::Requirement
36
36
  requirements:
37
37
  - - '='
38
38
  - !ruby/object:Gem::Version
39
- version: 0.1.0
39
+ version: 0.2.1
40
40
  - !ruby/object:Gem::Dependency
41
41
  name: thor
42
42
  requirement: !ruby/object:Gem::Requirement
@@ -80,17 +80,23 @@ files:
80
80
  - templates/app/Gemfile.tt
81
81
  - templates/app/README.md.tt
82
82
  - templates/app/app/controllers/application_controller.rb
83
+ - templates/app/app/jobs/application_job.rb
84
+ - templates/app/app/mailers/application_mailer.rb
85
+ - templates/app/app/mailers/templates/dot_keep
86
+ - templates/app/app/models/application_model.rb
87
+ - templates/app/app/serializers/application_serializer.rb
83
88
  - templates/app/bin/gemstack
84
89
  - templates/app/config.ru
85
90
  - templates/app/config/app.rb.tt
86
- - templates/app/config/environments/development.rb
87
- - templates/app/config/environments/production.rb
88
- - templates/app/config/environments/test.rb
91
+ - templates/app/config/database.yml.tt
92
+ - templates/app/config/environments/development.rb.tt
93
+ - templates/app/config/environments/production.rb.tt
94
+ - templates/app/config/environments/test.rb.tt
89
95
  - templates/app/config/puma.rb
90
96
  - templates/app/config/routes.rb
91
97
  - templates/app/db/migrations/dot_keep
92
98
  - templates/app/db/seeds.rb
93
- - templates/app/dot_env.example
99
+ - templates/app/dot_env.example.tt
94
100
  - templates/app/dot_gitignore
95
101
  - templates/app/dot_ruby-version.tt
96
102
  - templates/app/dot_tool-versions.tt
@@ -1,6 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- # Development-only settings. config/app.rb applies to every environment.
4
- GemStack.configure do |config|
5
- # config.http.show_exceptions = true # exception details in 500 responses (default in development)
6
- end
@@ -1,8 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- # Production-only settings. Defaults here: JSON logs, no exception details in
4
- # responses, eager loading, HSTS on HTTPS requests.
5
- GemStack.configure do |config|
6
- # config.logger.level = :info
7
- # config.http.cors.origins = ["https://app.example.com"] # only if the frontend is on another domain
8
- end
@@ -1,7 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- # Test-only settings. config/app.rb applies to every environment.
4
- GemStack.configure do |config|
5
- # Logs are discarded in tests unless GEMSTACK_LOG_LEVEL is set, e.g.:
6
- # config.logger.level = :debug
7
- end
@@ -1,9 +0,0 @@
1
- # Copy to .env for local development overrides. Never commit real secrets.
2
- # Variables already set in your shell always take precedence.
3
-
4
- # PORT=3000 # public development port (gemstack dev)
5
- # GEMSTACK_LOG_LEVEL=debug
6
-
7
- # PostgreSQL. Defaults: postgres:///<app>_development locally (current OS user).
8
- # DATABASE_URL=postgres://user:password@localhost:5432/myapp_development
9
- # TEST_DATABASE_URL=postgres://user:password@localhost:5432/myapp_test