janela 0.5.0 → 0.7.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.
data/lib/janela.rb CHANGED
@@ -20,6 +20,10 @@ module Janela
20
20
  # Something the request asked for is not allowed here: a renderer, a
21
21
  # granularity, a limit or a filter. Rendered as 400.
22
22
  class BadRequest < Error; end
23
+ # The host has not said what may be read. Not rendered as anything: it is a
24
+ # setup mistake rather than a data condition, and dressing it as a 404 would
25
+ # hide the one line that fixes it (ADR 032).
26
+ class Unscoped < Error; end
23
27
 
24
28
  # Janela's controllers inherit from the host's, so the host's authentication
25
29
  # and authorisation apply to dashboards with no configuration.
@@ -37,9 +41,33 @@ module Janela
37
41
  # is for (ADR 021).
38
42
  mattr_accessor :silenced_checks, default: []
39
43
 
40
- # Only models that declare a janela block are addressable over HTTP, keyed by
41
- # the route key that appears in pane URLs (orders, sales_orders). Names are
42
- # stored rather than classes so a reloaded model leaves nothing stale behind.
44
+ # What the host permits to be read, asked of its controller and never
45
+ # assumed. Janela refuses rather than reading everything, because a
46
+ # dashboard that quietly totals rows its reader may not see is the worst
47
+ # thing this library can do (ADR 032).
48
+ #
49
+ # Asked of the controller and never of self, because a helper runs on the
50
+ # view and a view cannot see a private controller method: when there were
51
+ # two copies of this question they disagreed about whether a host had
52
+ # answered it (#46). One copy, for that reason.
53
+ def self.scope(controller, model)
54
+ unless controller.respond_to?(:policy_scope, true)
55
+ raise Unscoped, "#{controller.class} defines no policy_scope, so Janela has not been " \
56
+ "told what may be read of #{model.name}. Define it on " \
57
+ "#{Janela.parent_controller}, returning the rows this visitor may see:\n\n" \
58
+ " private def policy_scope(model) = model.all\n\n" \
59
+ "That line says every visitor may read every row of every model on a " \
60
+ "dashboard. If that is not true here, return something narrower. " \
61
+ "UPGRADING.md has the steps and docs/multi-tenancy.md has the wiring."
62
+ end
63
+
64
+ controller.send(:policy_scope, model)
65
+ end
66
+
67
+ # A model that declares a janela block is addressable over HTTP, and so is a
68
+ # subclass of one, keyed by the route key that appears in pane URLs (orders,
69
+ # sales_orders). Names are stored rather than classes so a reloaded model
70
+ # leaves nothing stale behind.
43
71
  def self.registry
44
72
  @registry ||= {}
45
73
  end
@@ -48,6 +76,18 @@ module Janela
48
76
  registry[model.model_name.route_key] = model.name
49
77
  end
50
78
 
79
+ # A subclass registers itself as it is created (ADR 031), so unlike a
80
+ # declaration it is not a host writing a line of code. It never takes a
81
+ # route key another class already holds: a host that gives a subclass its
82
+ # parent's model_name, so the two share a route and a form, would otherwise
83
+ # find the parent's URL answering with a subset of its rows.
84
+ def self.register_subclass(model)
85
+ route_key = model.model_name.route_key
86
+ return if registry.key?(route_key) && registry[route_key] != model.name
87
+
88
+ register(model)
89
+ end
90
+
51
91
  # Every model that declares a janela block, for a form that offers a choice
52
92
  # of them. Eager loading first, because a model nobody has referenced yet has
53
93
  # not registered. A name that no longer resolves is left out rather than
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: janela
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.0
4
+ version: 0.7.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jay Killeen
@@ -136,6 +136,7 @@ files:
136
136
  - db/migrate/20260915000001_create_janela_snapshots.rb
137
137
  - db/migrate/20260916000001_create_janela_frames.rb
138
138
  - db/migrate/20260916000002_create_janela_panes.rb
139
+ - db/migrate/20260921000001_add_owner_to_janela_snapshots.rb
139
140
  - docs/decisions/001-built-to-be-forked.md
140
141
  - docs/decisions/002-measures-and-dimensions-over-ransack.md
141
142
  - docs/decisions/003-cross-filtering-with-turbo-frames.md
@@ -164,6 +165,12 @@ files:
164
165
  - docs/decisions/026-a-renderer-is-the-seam-and-html-comes-first.md
165
166
  - docs/decisions/027-the-gallery-is-a-host-page.md
166
167
  - docs/decisions/028-the-predicate-list-adr-025-named.md
168
+ - docs/decisions/029-a-panes-frame-is-identified-by-who-it-is.md
169
+ - docs/decisions/030-a-panes-src-belongs-to-turbo.md
170
+ - docs/decisions/031-a-subclass-inherits-the-dashboard.md
171
+ - docs/decisions/032-janela-will-not-read-a-model-it-cannot-scope.md
172
+ - docs/decisions/033-a-snapshot-is-told-who-owns-it.md
173
+ - docs/decisions/034-janela-will-not-freeze-a-scope-the-host-has-not-named.md
167
174
  - docs/decisions/INDEX.md
168
175
  - docs/multi-tenancy.md
169
176
  - docs/naming.md
@@ -187,12 +194,22 @@ metadata:
187
194
  bug_tracker_uri: https://github.com/retail-tasker/janela/issues
188
195
  rubygems_mfa_required: 'true'
189
196
  post_install_message: |
190
- Janela 0.5.0 bounds what a filter predicate can ask for (ADR 025). Most
191
- applications need do nothing: a click already writes eq or in, both still
192
- allowed. You need to act only if you pass a filter yourself using _cont,
193
- _matches, _start, _end or another predicate outside a dimension's
194
- allowlist, which now raises Janela::BadRequest instead of being quietly
195
- answered.
197
+ Janela 0.7.0 stops guessing what may be read. Two things now refuse
198
+ rather than defaulting to every row, and both are quick to answer.
199
+
200
+ 1. If your ApplicationController defines no policy_scope, every pane
201
+ raises Janela::Unscoped instead of totalling every row. Pundit
202
+ hosts and anyone following docs/multi-tenancy.md: nothing to do.
203
+ Everyone else writes one line (ADR 032).
204
+
205
+ 2. If you schedule Janela::SnapshotJob, it now needs to be told what
206
+ rows to freeze: pass scope: :model_default, or subclass it and
207
+ override scope_for. A job already on your queue was serialised
208
+ without that argument and will raise when it performs, so drain it
209
+ or re-enqueue (ADR 034).
210
+
211
+ If you use snapshots, there is also a migration: janela_snapshots
212
+ gains a nullable owner (ADR 033).
196
213
 
197
214
  Steps: UPGRADING.md in this gem, or
198
215
  https://github.com/retail-tasker/janela/blob/main/UPGRADING.md