studio-engine 0.10.0 → 0.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +39 -0
- data/Gemfile +16 -14
- data/app/controllers/studio/models_controller.rb +33 -0
- data/app/models/studio/model_page.rb +105 -0
- data/app/views/studio/models/show.html.erb +32 -0
- data/lib/studio/version.rb +1 -1
- data/lib/studio.rb +10 -0
- data/studio-engine.gemspec +1 -1
- metadata +13 -7
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 61bf440c6e7573b35e8d7a680851d2535b37b08690f36c0f8c42402a8a1fcc64
|
|
4
|
+
data.tar.gz: e79e46ab92bcd13bf5655526c199c1d7a1b17610d815dc240a5af7250455b9e1
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 64d04f5527318728c090ad318d9d5f652a12b1e8eadc6f665eccd4a0c4a0aadff6d1c7dfaecdc5c2a438c08b648a34ba78f1869dc8df1b80c6d1ccc9728f457d
|
|
7
|
+
data.tar.gz: bba2fac4175596d6448dcc9611c9c4740f021fcfb467c09986a20d0d506e4c37cc933257ac6e0d23deb262a0fcf98baa3fe3bce104a8985e85562451ddab406f
|
data/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,45 @@ The format is [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). This pro
|
|
|
4
4
|
|
|
5
5
|
## Unreleased
|
|
6
6
|
|
|
7
|
+
### Added
|
|
8
|
+
- **Model-page protocol (v1)** — a reusable, admin-only per-record inspector,
|
|
9
|
+
drawn into every consuming app via `Studio.routes`:
|
|
10
|
+
`/models/:model/:id` renders one record as pretty JSON plus a copy/paste
|
|
11
|
+
rails-console command to reload it, and `/models/:model/random` bounces to a
|
|
12
|
+
random record of that model. New `Studio::ModelsController` +
|
|
13
|
+
`app/views/studio/models/show.html.erb` (theme-token styled, reuses the
|
|
14
|
+
`components/copy_button` partial). The protocol object `Studio::ModelPage`
|
|
15
|
+
ships an **empty registry** — no model is reachable until a host opts in with
|
|
16
|
+
`Studio::ModelPage.register("release", Release, lookup: :slug)` in an
|
|
17
|
+
initializer (mirrors the admin-models host-registry pattern; the engine never
|
|
18
|
+
constantizes host models by name). Gated with `require_admin`.
|
|
19
|
+
|
|
20
|
+
## 0.11.0 — 2026-06-24
|
|
21
|
+
|
|
22
|
+
### Changed
|
|
23
|
+
- **Rails 8.1 support** — widened the gemspec `rails` bound from
|
|
24
|
+
`">= 7.0", "< 8.0"` to `">= 7.2", "< 8.2"`, so consuming apps can migrate to
|
|
25
|
+
Rails 8.1 independently (the engine no longer pins the ecosystem to Rails 7).
|
|
26
|
+
This release is the gate for the McRitchie Rails 8.1 train — `mcritchie-studio`
|
|
27
|
+
and `turf-monster` can bundle Rails 8.1 only once this version is published.
|
|
28
|
+
The lower bound moves to 7.2 (the oldest Rails the apps actually run) since
|
|
29
|
+
Rails 8 requires Ruby >= 3.2 and the ecosystem is already on Ruby 3.3.11.
|
|
30
|
+
|
|
31
|
+
### Added
|
|
32
|
+
- **`test/dummy` Rails app + `test/integration/engine_rails_8_1_boot_test.rb`** —
|
|
33
|
+
boots the engine inside a real Rails app and exercises its Rails-version-
|
|
34
|
+
sensitive surfaces (the Railtie registration, Zeitwerk autoload wiring, the
|
|
35
|
+
`Studio.routes` DSL, and the `ErrorLog` / `ThemeSetting` / `Sluggable`
|
|
36
|
+
ActiveRecord models). Re-run this against any new Rails line before widening
|
|
37
|
+
the bound again. The engine's full unit suite + this boot test are green
|
|
38
|
+
against Rails 8.1.3 (ActiveSupport/ActiveRecord 8.1, Zeitwerk 2.8, minitest 6).
|
|
39
|
+
|
|
40
|
+
### Fixed
|
|
41
|
+
- `bin/release-check` now prefers the `ruby@3.3` toolchain (was pinned to the
|
|
42
|
+
retired `ruby@3.1`, which cannot resolve Rails 8.1) and runs the full test
|
|
43
|
+
list, including the new Rails 8.1 boot test and the previously-omitted
|
|
44
|
+
`link_token` / sticky-header / admin-models-table tests.
|
|
45
|
+
|
|
7
46
|
## 0.10.0 — 2026-06-24
|
|
8
47
|
|
|
9
48
|
### Added
|
data/Gemfile
CHANGED
|
@@ -2,17 +2,19 @@ source "https://rubygems.org"
|
|
|
2
2
|
|
|
3
3
|
gemspec
|
|
4
4
|
|
|
5
|
-
# nokogiri 1.18.10
|
|
6
|
-
#
|
|
7
|
-
#
|
|
8
|
-
#
|
|
9
|
-
#
|
|
10
|
-
#
|
|
11
|
-
#
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
#
|
|
15
|
-
#
|
|
16
|
-
#
|
|
17
|
-
#
|
|
18
|
-
#
|
|
5
|
+
# nokogiri: historically pinned to 1.18.10 here while the ecosystem was on
|
|
6
|
+
# Ruby 3.1.7 (nokogiri >= 1.19.x requires Ruby 3.2+). The ecosystem is now on
|
|
7
|
+
# Ruby 3.3.11, so a plain `bundle update` resolves the patched nokogiri
|
|
8
|
+
# (>= 1.19.3, the line that fixed 3 CVEs: 1 HIGH regex-backtracking + 2 MEDIUM
|
|
9
|
+
# XSLT / xmlC14NExecute). This gem still does NOT declare nokogiri as a runtime
|
|
10
|
+
# dependency — it arrives via Rails in the consuming app. Tracked in
|
|
11
|
+
# SECURITY-AUDIT-2026-05-17.md.
|
|
12
|
+
|
|
13
|
+
group :development, :test do
|
|
14
|
+
# SQLite drives the test/dummy Rails app's in-memory database (see
|
|
15
|
+
# test/integration/engine_rails_8_1_boot_test.rb), which boots the engine
|
|
16
|
+
# against a real Rails app so we catch Rails-version incompatibilities in the
|
|
17
|
+
# engine's ActiveRecord/Railtie surfaces. Not a runtime dependency — the
|
|
18
|
+
# consuming apps bring their own database adapter. Rails 8.1 needs sqlite3 ~> 2.1.
|
|
19
|
+
gem "sqlite3", ">= 2.1"
|
|
20
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Studio
|
|
4
|
+
# Serves the model-page protocol (see Studio::ModelPage). Admin-only: raw record
|
|
5
|
+
# JSON can carry internal fields, so this is an operator/debug surface, not a
|
|
6
|
+
# public one. require_authentication is inherited from the host app's
|
|
7
|
+
# ApplicationController (Studio::ErrorHandling); require_admin gates on top.
|
|
8
|
+
class ModelsController < ApplicationController
|
|
9
|
+
before_action :require_admin
|
|
10
|
+
before_action :load_page
|
|
11
|
+
|
|
12
|
+
# GET /models/:model/:id — one record as JSON + a copy/paste console command.
|
|
13
|
+
def show
|
|
14
|
+
head :not_found unless @page.record
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# GET /models/:model/random — bounce to a random record's page (fresh sample).
|
|
18
|
+
def random
|
|
19
|
+
record = @page.random_record
|
|
20
|
+
return head(:not_found) unless record
|
|
21
|
+
|
|
22
|
+
redirect_to studio_model_path(@page.model_key, @page.identifier_for(record))
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
private
|
|
26
|
+
|
|
27
|
+
def load_page
|
|
28
|
+
@page = Studio::ModelPage.new(params[:model], params[:id])
|
|
29
|
+
rescue Studio::ModelPage::UnknownModel
|
|
30
|
+
head :not_found
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
module Studio
|
|
6
|
+
# The model-page protocol (v1).
|
|
7
|
+
#
|
|
8
|
+
# A reusable, per-record inspector. Given a whitelisted model key and a record
|
|
9
|
+
# identifier, it produces the two things a v1 model page renders — the record
|
|
10
|
+
# as pretty-printed JSON and a copy/paste rails-console command that reloads it
|
|
11
|
+
# — plus the lookup a "random sample" jump needs. The controller and view stay
|
|
12
|
+
# thin; enabling a model is a one-line registration.
|
|
13
|
+
#
|
|
14
|
+
# The engine ships an EMPTY registry: no model is reachable until a host app
|
|
15
|
+
# opts in. Each app registers its own models in an initializer, e.g.
|
|
16
|
+
#
|
|
17
|
+
# Studio::ModelPage.register("release", Release, lookup: :slug)
|
|
18
|
+
#
|
|
19
|
+
# This mirrors the admin-models pattern — the host hands the engine live class
|
|
20
|
+
# references; the engine never constantizes host models by name. An unknown key
|
|
21
|
+
# raises UnknownModel, which the controller renders as 404.
|
|
22
|
+
class ModelPage
|
|
23
|
+
class UnknownModel < StandardError; end
|
|
24
|
+
|
|
25
|
+
Entry = Struct.new(:model, :lookup, keyword_init: true)
|
|
26
|
+
|
|
27
|
+
class << self
|
|
28
|
+
# Enable a model. `model` is the live AR class; `lookup` is the column its
|
|
29
|
+
# page URL is keyed on (usually its to_param backing). Returns the key.
|
|
30
|
+
def register(key, model, lookup: :slug)
|
|
31
|
+
registry[key.to_s] = Entry.new(model: model, lookup: lookup.to_sym)
|
|
32
|
+
key.to_s
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def registered?(key)
|
|
36
|
+
registry.key?(key.to_s)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def keys
|
|
40
|
+
registry.keys
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Clears the registry — for tests and boot-time re-registration.
|
|
44
|
+
def reset!
|
|
45
|
+
registry.clear
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def entry(key)
|
|
49
|
+
registry.fetch(key.to_s) { raise UnknownModel, "unknown model: #{key.inspect}" }
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
private
|
|
53
|
+
|
|
54
|
+
def registry
|
|
55
|
+
@registry ||= {}
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
attr_reader :model_key, :identifier
|
|
60
|
+
|
|
61
|
+
# identifier is the record's lookup-key value from the URL (nil for /random,
|
|
62
|
+
# which addresses the model as a whole rather than one record).
|
|
63
|
+
def initialize(model_key, identifier = nil)
|
|
64
|
+
@entry = self.class.entry(model_key)
|
|
65
|
+
@model_key = model_key.to_s
|
|
66
|
+
@identifier = identifier
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def model
|
|
70
|
+
@entry.model
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def lookup_key
|
|
74
|
+
@entry.lookup
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# The record this page addresses (nil when the identifier matches nothing).
|
|
78
|
+
def record
|
|
79
|
+
return @record if defined?(@record)
|
|
80
|
+
|
|
81
|
+
@record = model.find_by(lookup_key => identifier)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# A random record of this model, for the "random sample" jump (nil if empty).
|
|
85
|
+
def random_record
|
|
86
|
+
model.order(::Arel.sql("RANDOM()")).first
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# The copy/paste rails-console command that reloads this record, e.g.
|
|
90
|
+
# Release.find_by(slug: "rel-20260707-a1b2c3")
|
|
91
|
+
def console_command
|
|
92
|
+
%(#{model.name}.find_by(#{lookup_key}: #{identifier.inspect}))
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# The record serialized as pretty JSON — the page's primary payload.
|
|
96
|
+
def json
|
|
97
|
+
::JSON.pretty_generate(record.as_json)
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# The lookup-key value to route to for a given record (used by /random).
|
|
101
|
+
def identifier_for(record)
|
|
102
|
+
record.public_send(lookup_key)
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
<%# Model-page protocol (v1) — a uniform record inspector: the record as pretty
|
|
2
|
+
JSON plus a copy/paste rails-console command to reload it, with a jump to a
|
|
3
|
+
random sample record. Admin-only surface (Studio::ModelsController). The copy
|
|
4
|
+
button reuses the engine components/copy_button partial. %>
|
|
5
|
+
<% model_label = @page.model.model_name.human %>
|
|
6
|
+
<% content_for(:title, "#{model_label} - Model") %>
|
|
7
|
+
|
|
8
|
+
<div class="max-w-7xl mx-auto px-4 py-8 space-y-6">
|
|
9
|
+
<header class="flex flex-col gap-3 md:flex-row md:items-end md:justify-between">
|
|
10
|
+
<div>
|
|
11
|
+
<p class="text-xs text-muted uppercase tracking-wide font-semibold">Model</p>
|
|
12
|
+
<h1 class="text-heading text-2xl font-bold mt-1"><%= model_label %></h1>
|
|
13
|
+
<p class="text-muted text-sm mt-1 font-mono"><%= @page.identifier %></p>
|
|
14
|
+
</div>
|
|
15
|
+
<%= link_to "🎲 Random sample", studio_model_random_path(@page.model_key), class: "btn btn-outline" %>
|
|
16
|
+
</header>
|
|
17
|
+
|
|
18
|
+
<%# The copy/paste line to reload this record in rails console. %>
|
|
19
|
+
<section class="card p-4 space-y-2">
|
|
20
|
+
<p class="text-xs text-muted uppercase tracking-wide font-semibold">Rails console</p>
|
|
21
|
+
<%= render "components/copy_button", text: @page.console_command %>
|
|
22
|
+
</section>
|
|
23
|
+
|
|
24
|
+
<%# The record serialized as pretty JSON — the protocol's primary payload. %>
|
|
25
|
+
<section class="card overflow-hidden">
|
|
26
|
+
<div class="flex items-center justify-between px-4 py-2 bg-surface-alt border-b border-subtle">
|
|
27
|
+
<p class="text-xs text-muted uppercase tracking-wide font-semibold">JSON</p>
|
|
28
|
+
<span class="font-mono text-xs text-muted"><%= @page.model.name %></span>
|
|
29
|
+
</div>
|
|
30
|
+
<pre class="overflow-x-auto px-4 py-3 text-xs font-mono text-heading leading-relaxed bg-inset"><%= @page.json %></pre>
|
|
31
|
+
</section>
|
|
32
|
+
</div>
|
data/lib/studio/version.rb
CHANGED
data/lib/studio.rb
CHANGED
|
@@ -309,6 +309,16 @@ module Studio
|
|
|
309
309
|
get "admin/email_images", to: "studio/email_images#index", as: :admin_email_images
|
|
310
310
|
patch "admin/email_images/:variant", to: "studio/email_images#update", as: :admin_email_image,
|
|
311
311
|
constraints: { variant: /[a-z_]+/ }
|
|
312
|
+
|
|
313
|
+
# Model-page protocol (v1) — a reusable per-record inspector. Drawn into
|
|
314
|
+
# every consuming app: /models/:model/:id renders one record as pretty JSON
|
|
315
|
+
# plus a copy/paste rails-console command; /models/:model/random bounces to
|
|
316
|
+
# a random record of that model. Admin-only (Studio::ModelsController).
|
|
317
|
+
# Ships an EMPTY registry — a host enables a model in an initializer with
|
|
318
|
+
# `Studio::ModelPage.register("release", Release, lookup: :slug)`. `random`
|
|
319
|
+
# is drawn BEFORE `:id` so it is not captured as a record identifier.
|
|
320
|
+
get "models/:model/random", to: "studio/models#random", as: :studio_model_random
|
|
321
|
+
get "models/:model/:id", to: "studio/models#show", as: :studio_model
|
|
312
322
|
end
|
|
313
323
|
end
|
|
314
324
|
end
|
data/studio-engine.gemspec
CHANGED
|
@@ -21,7 +21,7 @@ Gem::Specification.new do |spec|
|
|
|
21
21
|
spec.files = Dir["lib/**/*", "app/**/*", "config/**/*", "db/**/*", "tailwind/**/*", "Gemfile", "studio-engine.gemspec", "README.md", "CHANGELOG.md", "LICENSE"]
|
|
22
22
|
spec.require_paths = ["lib"]
|
|
23
23
|
|
|
24
|
-
spec.add_dependency "rails", ">= 7.
|
|
24
|
+
spec.add_dependency "rails", ">= 7.2", "< 8.2"
|
|
25
25
|
spec.add_dependency "tailwindcss-rails", "~> 4.5"
|
|
26
26
|
spec.add_dependency "faker", ">= 2.0", "< 4.0"
|
|
27
27
|
spec.add_dependency "solid_queue", ">= 1.0", "< 2.0"
|
metadata
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: studio-engine
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.12.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Alex McRitchie
|
|
8
|
+
autorequire:
|
|
8
9
|
bindir: bin
|
|
9
10
|
cert_chain: []
|
|
10
|
-
date:
|
|
11
|
+
date: 2026-07-08 00:00:00.000000000 Z
|
|
11
12
|
dependencies:
|
|
12
13
|
- !ruby/object:Gem::Dependency
|
|
13
14
|
name: rails
|
|
@@ -15,20 +16,20 @@ dependencies:
|
|
|
15
16
|
requirements:
|
|
16
17
|
- - ">="
|
|
17
18
|
- !ruby/object:Gem::Version
|
|
18
|
-
version: '7.
|
|
19
|
+
version: '7.2'
|
|
19
20
|
- - "<"
|
|
20
21
|
- !ruby/object:Gem::Version
|
|
21
|
-
version: '8.
|
|
22
|
+
version: '8.2'
|
|
22
23
|
type: :runtime
|
|
23
24
|
prerelease: false
|
|
24
25
|
version_requirements: !ruby/object:Gem::Requirement
|
|
25
26
|
requirements:
|
|
26
27
|
- - ">="
|
|
27
28
|
- !ruby/object:Gem::Version
|
|
28
|
-
version: '7.
|
|
29
|
+
version: '7.2'
|
|
29
30
|
- - "<"
|
|
30
31
|
- !ruby/object:Gem::Version
|
|
31
|
-
version: '8.
|
|
32
|
+
version: '8.2'
|
|
32
33
|
- !ruby/object:Gem::Dependency
|
|
33
34
|
name: tailwindcss-rails
|
|
34
35
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -187,6 +188,7 @@ files:
|
|
|
187
188
|
- app/controllers/studio/email_images_controller.rb
|
|
188
189
|
- app/controllers/studio/links_controller.rb
|
|
189
190
|
- app/controllers/studio/local_emails_controller.rb
|
|
191
|
+
- app/controllers/studio/models_controller.rb
|
|
190
192
|
- app/controllers/theme_settings_controller.rb
|
|
191
193
|
- app/helpers/studio/admin_models_table_helper.rb
|
|
192
194
|
- app/helpers/studio_email_delivery_helper.rb
|
|
@@ -204,6 +206,7 @@ files:
|
|
|
204
206
|
- app/models/studio/email_delivery.rb
|
|
205
207
|
- app/models/studio/enumeral.rb
|
|
206
208
|
- app/models/studio/link.rb
|
|
209
|
+
- app/models/studio/model_page.rb
|
|
207
210
|
- app/models/theme_setting.rb
|
|
208
211
|
- app/services/google_oauth_validator.rb
|
|
209
212
|
- app/services/magic_link.rb
|
|
@@ -258,6 +261,7 @@ files:
|
|
|
258
261
|
- app/views/studio/modals/blocks/_processing_card.html.erb
|
|
259
262
|
- app/views/studio/modals/blocks/_progress_countdown.html.erb
|
|
260
263
|
- app/views/studio/modals/blocks/_success_card.html.erb
|
|
264
|
+
- app/views/studio/models/show.html.erb
|
|
261
265
|
- app/views/theme_settings/edit.html.erb
|
|
262
266
|
- app/views/user_mailer/magic_link.html.erb
|
|
263
267
|
- app/views/user_mailer/magic_link.text.erb
|
|
@@ -293,6 +297,7 @@ metadata:
|
|
|
293
297
|
source_code_uri: https://github.com/amcritchie/studio-engine/tree/main
|
|
294
298
|
bug_tracker_uri: https://github.com/amcritchie/studio-engine/issues
|
|
295
299
|
changelog_uri: https://github.com/amcritchie/studio-engine/blob/main/CHANGELOG.md
|
|
300
|
+
post_install_message:
|
|
296
301
|
rdoc_options: []
|
|
297
302
|
require_paths:
|
|
298
303
|
- lib
|
|
@@ -307,7 +312,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
307
312
|
- !ruby/object:Gem::Version
|
|
308
313
|
version: '0'
|
|
309
314
|
requirements: []
|
|
310
|
-
rubygems_version:
|
|
315
|
+
rubygems_version: 3.5.22
|
|
316
|
+
signing_key:
|
|
311
317
|
specification_version: 4
|
|
312
318
|
summary: Shared Rails engine providing auth, SSO, error logging, theming, and S3-backed
|
|
313
319
|
image caching
|