janela 0.4.0 → 0.4.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c7fbc539879d1f11c15536c9305299ab9a754add8e26bb443cb582ea236823be
4
- data.tar.gz: 150f1292e5c2789cb378e6438c91fd94b766e10088f4daa4aea445ba63341a1b
3
+ metadata.gz: 00b7cab2c2ce57d7d9457affeb3d5f647b177f37d4f78349d4a12fe3fe298bd6
4
+ data.tar.gz: 5092f072ec8c4e282f09321fdff541e9eac613f557ae2f9697de830468c52778
5
5
  SHA512:
6
- metadata.gz: a6d3e5390adf6ac5882d621abc014986f0da17cadad2bca3ba3404d9d2f0d4c744130d7163be7271ddfc70c1404250d1a9a3aa09f274d88a44be57c93c1ac8d4
7
- data.tar.gz: 3d7d77e7022225e40c4c13f150818cfd646b0383400bbc59590014caa877b61894f6a1d0b4f056b4d438a1738b6c1489ead2d4ca60bb58caa9fa7554a15b8637
6
+ metadata.gz: 1bfea2cbc91408fa6c8879f2cd64b3c7cd51cc53b075da9268553ff9125716054afa68b8abc6f714e7b25c70a63f7a295f1c16353d5f31779a76ca326affbf6a
7
+ data.tar.gz: d9303b22989d5951d847bd56b3337f4576aa352c5887a2cc73b82d15e77d9967230a2984cd06bb2c428abed4f08065cd612461687abe7c91548cab4e88857669
data/CHANGELOG.md CHANGED
@@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [0.4.1] - 2026-09-17
9
+
10
+ ### Fixed
11
+
12
+ - `Janela::HostRoutes.forwarded` could return an empty set. Route loading is lazy, so calling it before anything had drawn the host's routes, such as from a host's own initializer, silently forwarded nothing rather than raising. It now draws the routes first if they are not already loaded (#37).
13
+
8
14
  ## [0.4.0] - 2026-09-17
9
15
 
10
16
  ### Added
@@ -122,6 +128,7 @@ First alpha, installed from GitHub for testing in a single host application.
122
128
  - Only models that declare a `janela` block are addressable over HTTP.
123
129
  - ADRs 001 to 004 in `docs/decisions/`, shipped inside the gem.
124
130
 
131
+ [0.4.1]: https://github.com/retail-tasker/janela/releases/tag/v0.4.1
125
132
  [0.4.0]: https://github.com/retail-tasker/janela/releases/tag/v0.4.0
126
133
  [0.3.0]: https://github.com/retail-tasker/janela/releases/tag/v0.3.0
127
134
  [0.2.1]: https://github.com/retail-tasker/janela/releases/tag/v0.2.1
data/README.md CHANGED
@@ -435,7 +435,7 @@ Deliberately out of scope: natural-language query, a separate data warehouse, a
435
435
 
436
436
  ## Status
437
437
 
438
- **v0.4.0 alpha.** The measures/dimensions DSL, time dimensions, cross-filtering with multi-selection, bar and line charts, pane URLs, shareable dashboard URLs, snapshots, database-backed frames, the engine's own pages for reading and editing them and the optional vitral theme work and are covered by unit and real-browser tests. Not yet built: a visual editor, drill-down on time panes, other chart types. Open work is in [GitHub Issues](https://github.com/retail-tasker/janela/issues).
438
+ **v0.4.1 alpha.** The measures/dimensions DSL, time dimensions, cross-filtering with multi-selection, bar and line charts, pane URLs, shareable dashboard URLs, snapshots, database-backed frames, the engine's own pages for reading and editing them and the optional vitral theme work and are covered by unit and real-browser tests. Not yet built: a visual editor, drill-down on time panes, other chart types. Open work is in [GitHub Issues](https://github.com/retail-tasker/janela/issues).
439
439
 
440
440
  ## Development
441
441
 
@@ -3,6 +3,7 @@ Date: 2026-09-15
3
3
  Status: Accepted
4
4
  Related: ADR 001, ADR 005, ADR 009, ADR 012, ADR 014
5
5
  Superseded in part by: ADR 012
6
+ Not implemented: the host skill, the install task and the agent definition
6
7
  Triggers:
7
8
  - writing or changing guidance for AI agents about using Janela
8
9
  - proposing that Janela ship an agent, a skill or an MCP surface to hosts
@@ -0,0 +1,131 @@
1
+ ---
2
+ Date: 2026-09-17
3
+ Status: Accepted
4
+ Related: ADR 002, ADR 005, ADR 006, ADR 007, ADR 008, ADR 015, ADR 024
5
+ Triggers:
6
+ - a filter reaching Ransack from a URL
7
+ - adding a predicate, or widening what a filter may contain
8
+ - a query with no LIMIT, or a grouped query over a high cardinality dimension
9
+ - deciding whether a bound belongs to Janela or to the host
10
+ Topics: security, ransack, urls, performance, cross-filtering
11
+ ---
12
+
13
+ # ADR 025: Janela Bounds What a Filter Can Ask For
14
+
15
+ ## Context
16
+
17
+ ADR 002 chose Ransack so that a host's filters are the Ransack params a
18
+ Rails developer already reads, and made a dimension declaration the
19
+ allowlist of attributes. That part holds. What it left open is which
20
+ *predicates* may be used on an allowed attribute, and issue #8 asked the
21
+ question.
22
+
23
+ Measured against the demo before writing this:
24
+
25
+ | Filter passed | Result today |
26
+ | --- | --- |
27
+ | `status_eq: "paid"` | 300.0 |
28
+ | `status_cont: "pai"` | 300.0 |
29
+ | `status_start: "p"` | 325.0 |
30
+ | `status_matches: "%aid"` | 300.0 |
31
+ | `customer_name_cont: "cm"` | 150.0 |
32
+ | `amount_gt: 60` | `BadRequest`, `amount` is not a dimension |
33
+ | `status_frobnicate: "x"` | `BadRequest`, no such predicate |
34
+
35
+ So the attribute allowlist works, and every predicate Ransack knows is
36
+ reachable on an allowed attribute, including through an association.
37
+ `Ransack::Predicate.names` has 62 entries. The sharpest is `_matches`,
38
+ which takes an arbitrary `LIKE` pattern, so a leading wildcard scan of an
39
+ indexed column is one URL edit away, in a page a host has already
40
+ authorised someone to read.
41
+
42
+ Two other bounds are missing. A grouped query with no `limit` carries no
43
+ `LIMIT` in SQL at all, so a breakdown by a high cardinality dimension
44
+ returns a row per value. And a single filter can carry any number of
45
+ values: `status_in` with 5001 of them was answered rather than refused,
46
+ which matters more since ADR 024 made `_in` the shape a click writes.
47
+
48
+ **The question worth deciding is whose job this is.** ADR 002
49
+ deliberately spent Ransack's familiarity on the host, and the obvious
50
+ objection to bounding anything here is that a host can restrict filtering
51
+ itself by defining `ransackable_attributes`. That objection is false, and
52
+ the measurement above is why: `ransackable_attributes` allows *columns*,
53
+ and Ransack has no per-attribute predicate allowlist. Once a host allows
54
+ a column for equality, it has allowed `_matches` on that column too.
55
+ There is nowhere else for this bound to live.
56
+
57
+ Options considered and rejected:
58
+
59
+ - **Leave it, and document it.** A dashboard is often the first page a
60
+ company exposes to people it would not give SQL to, and the library
61
+ hands them a pattern scan. Documenting a sharp edge is not the same as
62
+ not having one.
63
+ - **Restrict to `_eq` and `_in`,** as issue #8 proposed. It would delete
64
+ documented behaviour: ADR 006 allows `placed_on_gteq` and
65
+ `placed_on_lt` so a host can narrow a time range, and the README says
66
+ so. A range filter on a time dimension is a dashboard's ordinary
67
+ question, not an escape hatch.
68
+ - **A setting listing the allowed predicates.** Rejected on the test
69
+ ADR 021 and ADR 023 set: a setting earns itself when the judgement is
70
+ made once about the whole application and cannot be inferred. This one
71
+ can be inferred, from what kind of dimension is being filtered.
72
+
73
+ ## Decision
74
+
75
+ **Janela allows only the predicates a dashboard asks in, bounds what a
76
+ grouped query returns, and bounds how many values one filter may carry.**
77
+
78
+ **Predicates are allowed by the kind of dimension.** A categorical
79
+ dimension takes `eq`, `in` and `null`, which is exactly what a click
80
+ produces (ADR 024). A time dimension additionally takes `gteq`, `gt`,
81
+ `lteq` and `lt`, because narrowing a range is how a time dimension is
82
+ filtered (ADR 006). Anything else raises `Janela::BadRequest` naming the
83
+ attribute and the predicates that are allowed, the way a disallowed
84
+ attribute already does.
85
+
86
+ The check belongs beside the existing one in `Definition#filter`, which
87
+ already raises for a filter Ransack silently dropped. `Ransack::Predicate
88
+ .detect_from_string` splits a key into attribute and predicate, so this
89
+ is a comparison, not a parser.
90
+
91
+ **One ceiling, 1000, in both places.** A grouped query with no `limit`
92
+ gets `LIMIT 1000`, and a single filter may carry at most 1000 values.
93
+ 1000 is not a new number: ADR 007 already fixed it as the maximum a host
94
+ may ask for, so the ceiling is the existing maximum applied by default
95
+ rather than a second constant to reason about. A pane that hits the
96
+ ceiling is unreadable long before it is reached, so this is a bound on
97
+ harm, not on usefulness.
98
+
99
+ **Time panes keep their exemption.** ADR 007 exempted them from `limit`
100
+ because a time series shows its whole range, and gap filled buckets are
101
+ generated rather than returned by the database. The ceiling does not
102
+ apply to them. A host that wants fewer buckets narrows the range or
103
+ coarsens the granularity, which ADR 006 already allows.
104
+
105
+ **No setting is added.** A host that genuinely needs a pattern search
106
+ already has the documented way in: pass a pre-filtered relation through
107
+ `on:`, where it writes the condition itself in its own code, under its
108
+ own authorisation, rather than accepting one from a URL.
109
+
110
+ ## Consequences
111
+
112
+ - The reachable filter surface goes from 62 predicates to three on a
113
+ categorical dimension and seven on a time dimension. That is the point.
114
+ - **Breaking for a host that passes anything else.** A `_cont` or
115
+ `_matches` filter that works today raises `BadRequest` after this. It
116
+ needs an `UPGRADING.md` entry naming the allowed predicates and the
117
+ `on:` alternative, and the release carrying it is minor (ADR 015).
118
+ The doctor cannot find this one by reading source, because the filter
119
+ is usually built at runtime; the upgrade note is the whole mitigation.
120
+ - A stored snapshot holds the filters it was taken under (ADR 009). One
121
+ taken with a now disallowed predicate would raise when read. Whether to
122
+ refuse those at read time or leave stored results alone is not decided
123
+ here and should be settled while building this.
124
+ - The default ceiling changes a number a host can see: a breakdown over
125
+ more than 1000 values silently showed all of them and will now show
126
+ 1000. It is ordered by the measure, so what is cut is the smallest, and
127
+ a host that wants a different cut passes `limit`.
128
+ - What would change this: a host with a real need for a predicate not on
129
+ the list. The answer is to add that predicate to the list for that kind
130
+ of dimension, in an ADR that says which dashboard question needed it,
131
+ not to make the list configurable.
@@ -21,23 +21,24 @@ them when in doubt: `grep -l "Triggers:.*fork" docs/decisions/*.md`.
21
21
  | Topic | ADRs |
22
22
  |-------|------|
23
23
  | **Vision, scope, forkability** | 001, 010, 012 |
24
- | **Open-source & host-decoupling** | 001 |
25
- | **DSL & query layer** | 002, 006, 007 |
26
- | **Dependencies** | 002, 003, 004, 006, 017 |
27
- | **Authorisation** | 002, 003, 004, 009, 017, 019 |
28
- | **Performance & storage** | 007, 017 |
29
- | **Cross-filtering & Hotwire** | 003, 004, 005, 008 |
30
- | **Layouts & views** | 011, 012, 016, 018 |
31
- | **CSS & styling** | 016, 018 |
24
+ | **Open-source & host-decoupling** | 001, 022 |
25
+ | **DSL & query layer** | 002, 006, 007, 020, 025 |
26
+ | **Dependencies** | 002, 003, 004, 006, 017, 025 |
27
+ | **Authorisation** | 002, 003, 004, 009, 017, 019, 022 |
28
+ | **Performance & storage** | 007, 017, 025 |
29
+ | **Cross-filtering & Hotwire** | 003, 004, 005, 008, 024, 025 |
30
+ | **Layouts & views** | 011, 012, 016, 018, 020 |
31
+ | **CSS & styling** | 016, 018, 023 |
32
32
  | **Frames, panes & persistence** | 012, 013, 014, 019 |
33
- | **Naming rule** | 014 |
33
+ | **Naming rule** | 014, 023 |
34
34
  | **JavaScript delivery & charts** | 004, 006 |
35
- | **Time dimensions** | 006 |
36
- | **Routes, URLs & naming** | 005, 007, 008, 009, 011, 013 |
37
- | **Snapshots & publishing** | 009 |
38
- | **AI agents & guidance** | 010, 015 |
39
- | **Releases & upgrades** | 015 |
40
- | **Security** | 003 |
35
+ | **Time dimensions** | 006, 025 |
36
+ | **Routes, URLs & naming** | 005, 007, 008, 009, 011, 013, 022, 024, 025 |
37
+ | **Snapshots & publishing** | 009, 020 |
38
+ | **AI agents & guidance** | 010, 015, 021 |
39
+ | **Releases & upgrades** | 015, 021 |
40
+ | **Accessibility & keyboard** | 024 |
41
+ | **Security** | 003, 025 |
41
42
  | **Testing** | 003 |
42
43
 
43
44
  ## Chronological
@@ -68,7 +69,8 @@ them when in doubt: `grep -l "Triggers:.*fork" docs/decisions/*.md`.
68
69
  | 022 | A Host's Route Helpers Work Inside the Engine | 2026-09-16 | Accepted |
69
70
  | 023 | Vitral Is a Theme, Not the Stylesheet | 2026-09-16 | Accepted |
70
71
  | 024 | Selecting More Than One Value | 2026-09-16 | Accepted |
72
+ | 025 | Janela Bounds What a Filter Can Ask For | 2026-09-17 | Accepted |
71
73
 
72
74
  ## Next number
73
75
 
74
- Next ADR: 025
76
+ Next ADR: 026
@@ -25,6 +25,12 @@ module Janela
25
25
  end
26
26
 
27
27
  def self.forwarded(host: Rails.application.routes, engine: Janela::Engine.routes)
28
+ # Route loading is lazy, so a call before anything else has drawn the
29
+ # host's routes would otherwise see an empty set rather than an error
30
+ # (issue #37). This does not recurse: define! runs from
31
+ # after_routes_loaded, by which point the reloader has already marked
32
+ # itself loaded.
33
+ Rails.application.reload_routes_unless_loaded
28
34
  host.named_routes.helper_names - engine.named_routes.helper_names
29
35
  end
30
36
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Janela
4
- VERSION = "0.4.0"
4
+ VERSION = "0.4.1"
5
5
  end
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.4.0
4
+ version: 0.4.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jay Killeen
@@ -160,6 +160,7 @@ files:
160
160
  - docs/decisions/022-host-route-helpers-work-inside-the-engine.md
161
161
  - docs/decisions/023-vitral-is-a-theme-not-the-stylesheet.md
162
162
  - docs/decisions/024-selecting-more-than-one-value.md
163
+ - docs/decisions/025-janela-bounds-what-a-filter-can-ask-for.md
163
164
  - docs/decisions/INDEX.md
164
165
  - docs/multi-tenancy.md
165
166
  - docs/naming.md
@@ -182,15 +183,6 @@ metadata:
182
183
  changelog_uri: https://github.com/retail-tasker/janela/blob/main/CHANGELOG.md
183
184
  bug_tracker_uri: https://github.com/retail-tasker/janela/issues
184
185
  rubygems_mfa_required: 'true'
185
- post_install_message: |
186
- Janela 0.4.0 selects more than one value in a dimension. Most applications
187
- need do nothing. You need to act only if you override a pane view, where
188
- selected_value is now selected_values, or if something of yours reads
189
- Janela's URLs, where a click now writes q[field_in][] rather than
190
- q[field_eq]. Links already shared keep working.
191
-
192
- Steps: UPGRADING.md in this gem, or
193
- https://github.com/retail-tasker/janela/blob/main/UPGRADING.md
194
186
  rdoc_options: []
195
187
  require_paths:
196
188
  - lib