querylane 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 (68) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +84 -0
  3. data/CONTRIBUTING.md +118 -0
  4. data/LICENSE +21 -0
  5. data/README.md +287 -0
  6. data/SECURITY.md +29 -0
  7. data/app/assets/stylesheets/querylane.css +300 -0
  8. data/app/helpers/querylane/search_helper.rb +111 -0
  9. data/app/javascript/querylane/controllers/box_controller.js +170 -0
  10. data/app/javascript/querylane/controllers/options_controller.js +54 -0
  11. data/app/javascript/querylane/controllers/results_controller.js +137 -0
  12. data/app/javascript/querylane/controllers/suggest_controller.js +187 -0
  13. data/app/javascript/querylane/index.js +33 -0
  14. data/app/javascript/querylane/overlay.js +46 -0
  15. data/app/javascript/querylane/package.json +7 -0
  16. data/app/views/querylane/_chips.html.erb +15 -0
  17. data/app/views/querylane/_denied.html.erb +6 -0
  18. data/app/views/querylane/_empty.html.erb +17 -0
  19. data/app/views/querylane/_facets.html.erb +30 -0
  20. data/app/views/querylane/_first_load.html.erb +15 -0
  21. data/app/views/querylane/_form.html.erb +46 -0
  22. data/app/views/querylane/_group.html.erb +21 -0
  23. data/app/views/querylane/_help.html.erb +29 -0
  24. data/app/views/querylane/_options.html.erb +32 -0
  25. data/app/views/querylane/_options_row.html.erb +51 -0
  26. data/app/views/querylane/_outcome.html.erb +16 -0
  27. data/app/views/querylane/_pager.html.erb +19 -0
  28. data/app/views/querylane/_preview.html.erb +39 -0
  29. data/app/views/querylane/_record.html.erb +16 -0
  30. data/app/views/querylane/_related.html.erb +17 -0
  31. data/app/views/querylane/_results.html.erb +66 -0
  32. data/app/views/querylane/_rows.html.erb +13 -0
  33. data/app/views/querylane/_saved.html.erb +31 -0
  34. data/app/views/querylane/_suggest_box.html.erb +45 -0
  35. data/app/views/querylane/_suggestions.html.erb +47 -0
  36. data/app/views/querylane/_toolbar.html.erb +28 -0
  37. data/app/views/querylane/_unavailable.html.erb +7 -0
  38. data/app/views/querylane/_writes.html.erb +16 -0
  39. data/config/importmap.rb +34 -0
  40. data/config/locales/querylane.en.yml +136 -0
  41. data/lib/querylane/ast.rb +17 -0
  42. data/lib/querylane/chips.rb +85 -0
  43. data/lib/querylane/compiler.rb +52 -0
  44. data/lib/querylane/configuration.rb +49 -0
  45. data/lib/querylane/context.rb +11 -0
  46. data/lib/querylane/controller.rb +85 -0
  47. data/lib/querylane/definition.rb +447 -0
  48. data/lib/querylane/doctor.rb +52 -0
  49. data/lib/querylane/engine.rb +61 -0
  50. data/lib/querylane/errors.rb +27 -0
  51. data/lib/querylane/filter.rb +75 -0
  52. data/lib/querylane/group_token.rb +33 -0
  53. data/lib/querylane/parser.rb +178 -0
  54. data/lib/querylane/presenter.rb +160 -0
  55. data/lib/querylane/preview.rb +45 -0
  56. data/lib/querylane/query.rb +183 -0
  57. data/lib/querylane/request.rb +66 -0
  58. data/lib/querylane/result.rb +90 -0
  59. data/lib/querylane/rewriter.rb +245 -0
  60. data/lib/querylane/saving.rb +49 -0
  61. data/lib/querylane/scope.rb +53 -0
  62. data/lib/querylane/search.rb +347 -0
  63. data/lib/querylane/searchers/meilisearch.rb +226 -0
  64. data/lib/querylane/suggest.rb +67 -0
  65. data/lib/querylane/version.rb +5 -0
  66. data/lib/querylane.rb +60 -0
  67. data/lib/tasks/querylane.rake +14 -0
  68. metadata +132 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: f62396550c8585a01958050e34f4e31628ddb53ad02cf97074900e0882a6e510
4
+ data.tar.gz: 68f1c601f5a1083d1fd6711db29c87fde0a994962091578cb7f58c5b0b12f98d
5
+ SHA512:
6
+ metadata.gz: 8c35bb1029b86f33ca864824980c847c532a0a6b5ad4ee9bcd44a53c6ec3370fded089c1146025cf4db3d17736baf5c1383d8ee31edd030c677e3fb78b43b460
7
+ data.tar.gz: 82f3e9d07e46f33876573d5f4227070965f352e3ec67bf479c90f9a98e457b5e017cdb822e1e7355af17a2e208b9a22b479acf474c546c60078cee98474a4b9e
data/CHANGELOG.md ADDED
@@ -0,0 +1,84 @@
1
+ # Changelog
2
+
3
+ All notable changes to querylane are listed here. The format follows
4
+ [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and the project uses
5
+ [Semantic Versioning](https://semver.org/). Before 1.0, a minor version may
6
+ change public APIs; each entry says how.
7
+
8
+ ## [Unreleased]
9
+
10
+ ## [0.1.0] - 2026-10-20
11
+
12
+ The first release.
13
+
14
+ ### Added
15
+
16
+ - importmap-rails support (Propshaft or Sprockets): `config/importmap.rb` pins
17
+ `querylane` and each module by its file name (`querylane/overlay.js`,
18
+ `querylane/controllers/*.js`), none preloaded; the engine adds
19
+ `app/javascript` to the asset load path and, when importmap-rails is loaded,
20
+ appends the gem's map to `config.importmap.paths` and the modules to
21
+ `config.importmap.cache_sweepers`, but only when the host has its own
22
+ `config/importmap.rb`. Hosts pin `@hotwired/stimulus` and
23
+ `@github/combobox-nav`. The `querylane.css` precompile entry is added only
24
+ under Sprockets.
25
+ - The `querylane_set_query` stream action is handled through
26
+ `turbo:before-stream-render` instead of `StreamActions`, so querylane never
27
+ imports `@hotwired/turbo` and works with turbo-rails' bundled Turbo.
28
+ - The modules import each other as `querylane/<file>.js` instead of by relative
29
+ path (ADR 0003), so one set of files serves importmap-rails, webpack and
30
+ esbuild aliases, and the npm package (`package.json` now exports `./*`).
31
+ Hand-written import maps need `querylane` and one `querylane/` prefix entry.
32
+
33
+ - Query language: words, `key:value` operators, quoted phrases with `\"` and `\\`
34
+ escapes, implicit AND, `OR` (binding tighter than AND), `-` and `NOT`,
35
+ parentheses. Bounded by length, nesting depth and term count. Errors carry a
36
+ character range, a "did you mean" suggestion and a one-click **Fix it**.
37
+ - `Querylane::Definition.build` DSL: `index`, `text`, `filter` (normalize,
38
+ format, sensitive, masks `:none`/`:all`/`:last4`/`:count`), `flags` (attribute
39
+ or per-index rules, aliases), `facet` (flag or filter, value subsets, `top`,
40
+ `show`, "Find…" search), `sort`, `per`, `retrieve`, `highlight`,
41
+ `interpreter` (with "… instead" alternatives), `parked`, `min_text_length`,
42
+ `group` (`valid`, `limit`, `order`), `saved`, `option`, `panel`, `example` and
43
+ `hook`. Declarations are validated at build time.
44
+ - Required `Querylane::Scope` with a permission filter per index;
45
+ `Scope.unrestricted` and `Scope.denied`. Backend-neutral `Querylane::Filter`
46
+ (`eq`, `in`, `null`, `missing`, `and`, `or`, `not`, `everything`, `nothing`,
47
+ `raw`).
48
+ - Query-filter and results hooks with a `Querylane::Context`; results hooks
49
+ declare `:preserving` or `:changing` cardinality. Hooks fail closed.
50
+ - Meilisearch searcher (Meilisearch 1.13+, `meilisearch` gem `~> 0.33.0`):
51
+ federated hits across indexes, one batched call for exact totals and
52
+ disjunctive facet counts, per-group record counts, highlight markers. Fails
53
+ closed with an `:unavailable` state; a failed count batch keeps the hits.
54
+ - `rake querylane:doctor[Definition]` checks index settings.
55
+ - `Querylane::Controller`: `querylane_search`, `querylane_preview`,
56
+ `querylane_related`, `querylane_suggest` and `querylane_frame?`, with a guard
57
+ that refuses query text in a GET URL and sends `Cache-Control: no-store`.
58
+ - View helpers and partials (Bootstrap 5 classes): `querylane_form` (native
59
+ input with a colored, masking overlay; optional help text),
60
+ `querylane_results` (chips, error with Fix it, facets, toolbar, pager, grouped
61
+ rows, footer), `querylane_suggest_box` (header dropdown or ⌘K / Ctrl-K
62
+ palette) with `querylane_suggestions`, and `querylane_related` ("more records"
63
+ and the Space preview).
64
+ - States: first load with examples, too short, empty (widen, drop, relax),
65
+ invalid, unavailable, denied.
66
+ - Options panel that reads the query into fields and writes it back, with a live
67
+ masked "Writes" preview.
68
+ - Saved searches through a host store (`all`, `create`, `use` with `context:`);
69
+ queries with sensitive terms, typed or interpreted, are refused.
70
+ - `Querylane::Presenter` base class for host rows: columns, cells, status,
71
+ flags, Open action or reason (with "+N more"), group titles, suggestions.
72
+ - Encrypted, expiring group tokens; group keys never reach the page.
73
+ - Stimulus controllers (`querylane--box`, `--results`, `--options`,
74
+ `--suggest`) and the `querylane_set_query` Turbo Stream action, registered with
75
+ `registerControllers(application)`. Revisions drop stale responses; Turbo
76
+ snapshots never keep query text.
77
+ - `querylane.css` themed by `--ql-*` custom properties; safe for SassC
78
+ compressors.
79
+ - English translations under `querylane.*`.
80
+ - `search.querylane` and `saved_search_failed.querylane` instrumentation, without
81
+ query text. The query parameter and `compose` are filtered from logs.
82
+
83
+ [Unreleased]: https://github.com/clearstackio/querylane/compare/v0.1.0...HEAD
84
+ [0.1.0]: https://github.com/clearstackio/querylane/releases/tag/v0.1.0
data/CONTRIBUTING.md ADDED
@@ -0,0 +1,118 @@
1
+ # Contributing
2
+
3
+ Thanks for working on querylane. This page covers setup, the checks every change
4
+ must pass, the conventions, and how the docs are organized. Read
5
+ [docs/architecture.md](docs/architecture.md) before a larger change.
6
+
7
+ ## Setup
8
+
9
+ Tools are pinned in `mise.toml` (Ruby 3.3, Node 22, FracturedJSON).
10
+
11
+ ```sh
12
+ mise install
13
+ bundle install
14
+ npm install # Turbo, Stimulus, combobox-nav, Playwright (dev only)
15
+ npx playwright install chromium # for system specs
16
+ ```
17
+
18
+ Most specs need a Meilisearch 1.13+ server:
19
+
20
+ ```sh
21
+ export MEILISEARCH_URL=http://localhost:7700
22
+ export MEILISEARCH_API_KEY=your-key
23
+ ```
24
+
25
+ The suite drops and recreates the indexes `querylane_spec_staff` and
26
+ `querylane_spec_contractors` on that server.
27
+
28
+ ## Running the checks
29
+
30
+ ```sh
31
+ bundle exec rspec # everything; :meilisearch examples skip without a server
32
+ bundle exec rspec spec/querylane # unit specs only
33
+ bin/lint # StandardRB, then FracturedJSON on tracked JSON files
34
+ bin/lint fix # apply StandardRB fixes and reformat JSON
35
+ bundle exec rake # the suite and StandardRB, as CI runs it
36
+ ```
37
+
38
+ Run them through mise if your shell does not activate it:
39
+ `mise exec -- bundle exec rspec`, `mise exec -- bin/lint`.
40
+
41
+ `bin/lint` checks JSON files known to git (`git ls-files`), outside `spec/dummy`.
42
+
43
+ ## The dummy app
44
+
45
+ `spec/dummy` is the reference host: a definition (`app/searches/dummy_search.rb`),
46
+ a presenter, a controller using every controller call, an in-memory saved-search
47
+ store, the header box and palette (`/elsewhere`), and an import-map layout that
48
+ serves the gem's JavaScript straight from `app/javascript`. Request and system
49
+ specs run against it. Keep it working and keep it showing every feature: it is
50
+ also the example the guides quote.
51
+
52
+ ## Conventions
53
+
54
+ - **Style:** StandardRB. Do not add a `.rubocop.yml`.
55
+ - **One concern per file.** Public API first, then private helpers.
56
+ - **Header comments** on every class and module: what it is for and how it fits
57
+ the flow. Document public methods with YARD (`@param`, `@return`).
58
+ - **Ruby decides, partials draw.** No markup built in Ruby; presenters and
59
+ helpers return data or render partials.
60
+ - **CSS** must pass SassC compressors: no `min()`, `max()`, `clamp()` or
61
+ space-separated `rgb()`. Use `font-variant-caps`, never `text-transform`. Keep
62
+ `[hidden]` winning over display rules.
63
+ - **JavaScript** is plain ES modules that import each other as
64
+ `querylane/<file>.js`, never by relative path. Never start
65
+ Stimulus or change Turbo settings from the gem. Use `textContent`, never
66
+ `innerHTML` with data.
67
+ - **Privacy rules are invariants.** Query text travels by POST, responses are
68
+ `no-store`, sensitive values are masked wherever they are shown, group keys
69
+ never reach HTML, saving refuses sensitive terms. A change that weakens one
70
+ needs a decision record.
71
+ - **Tests:** every behavior gets a spec at the right layer: unit
72
+ (`spec/querylane/`), integration against Meilisearch (`spec/integration/`),
73
+ request (`spec/requests/`) or browser (`spec/system/`).
74
+ - **Changelog:** add a line under `[Unreleased]` in [CHANGELOG.md](CHANGELOG.md)
75
+ for anything a host would notice.
76
+ - **CI:** GitHub Actions (`.github/workflows/ci.yml`): `rake` on Ruby 3.3, 3.4
77
+ and 4.0 against Meilisearch 1.13 and headless Chromium. Actions are pinned to
78
+ commit SHAs. FracturedJSON is not run in CI; run `bin/lint` before pushing.
79
+ - **Security:** no secrets in the repo; see [SECURITY.md](SECURITY.md).
80
+
81
+ ## Releasing
82
+
83
+ Releases go to rubygems.org from GitHub Actions with
84
+ [trusted publishing](https://guides.rubygems.org/trusted-publishing/) (no API
85
+ key) and a Sigstore attestation (`.github/workflows/release.yml`, in the
86
+ `rubygems` environment).
87
+
88
+ Before the first release, an owner of the gem adds a pending trusted publisher
89
+ on rubygems.org (Profile → Trusted publishers): gem `querylane`, repository
90
+ owner `clearstackio`, repository `querylane`, workflow `release.yml`,
91
+ environment `rubygems`. After the first push it becomes the gem's trusted
92
+ publisher.
93
+
94
+ 1. Update `VERSION` in `lib/querylane/version.rb` and run `bundle install` so
95
+ `Gemfile.lock` follows.
96
+ 2. Move the `[Unreleased]` entries in `CHANGELOG.md` under the new version and
97
+ date, and update the compare links at the bottom.
98
+ 3. Commit, then tag and push: `git tag v0.1.0 && git push origin master v0.1.0`.
99
+
100
+ The workflow runs the full CI suite on the tagged commit (Meilisearch and
101
+ Chromium included), checks the tag matches `VERSION`, then builds, pushes and
102
+ attests the gem. A failing suite publishes nothing.
103
+
104
+ ## Documentation
105
+
106
+ | Where | What |
107
+ | --- | --- |
108
+ | [README.md](README.md) | The front door: pitch, install, quick start |
109
+ | [docs/guides/](docs/README.md) | One guide per topic, for people using the gem |
110
+ | [docs/architecture.md](docs/architecture.md) | How the gem works, for contributors |
111
+ | [docs/decisions/](docs/decisions/README.md) | Decision records (ADRs) |
112
+ | [docs/spikes/](docs/spikes/meilisearch-1.13.md) | Evidence from backend experiments |
113
+ | [AGENTS.md](AGENTS.md) | Instructions for coding agents |
114
+
115
+ When you change behavior, update the guide that describes it in the same change.
116
+ Examples in the guides should match the dummy app or a spec. Record a decision
117
+ that is hard to reverse as a new ADR in `docs/decisions/` and list it in its
118
+ README.
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Ajaya Agrawalla
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,287 @@
1
+ # querylane
2
+
3
+ querylane is a Rails engine that adds a Gmail-style search box to a Rails app.
4
+ People type plain words and operators (`smith team:ops -is:terminated`). The
5
+ server parses the text, searches one or more Meilisearch indexes inside a
6
+ permission scope, and renders the results with the engine's partials and
7
+ Stimulus controllers. The query text is the only search state: facets, chips
8
+ and the options panel all rewrite the text in the box, so what you see in the
9
+ box is always what was searched.
10
+
11
+ [![Build Status](https://github.com/clearstackio/querylane/actions/workflows/ci.yml/badge.svg)](https://github.com/clearstackio/querylane/actions)
12
+
13
+ ## Features
14
+
15
+ - A small query language: words, `key:value` operators, quotes, `OR`, `-`/`NOT`,
16
+ parentheses. Errors point at the exact characters, suggest a fix ("Did you mean
17
+ `status:`?") and offer **Fix it**.
18
+ - An interpreter hook that reads bare tokens as operators (`5432` as a last four),
19
+ with "… instead" buttons for other readings.
20
+ - "Searching for" chips with × to remove a term. Sensitive values are masked.
21
+ - Facets with counts (OR within a facet, AND across facets) that tick and untick
22
+ terms in the query text. A "Find…" box for long facet lists.
23
+ - Toolbar with count, sort, page size and pager.
24
+ - Result rows grouped by an attribute (for example one person), drawn from your
25
+ `Querylane::Presenter`. "N more records" for a group and a Space-key preview.
26
+ - An options panel that turns the query into fields and back.
27
+ - A header search box with suggestions and a ⌘K / Ctrl-K palette.
28
+ - Saved searches through a store you provide.
29
+ - Clear states: first load, too short, empty (with widen, drop and relax buttons),
30
+ invalid, unavailable and access denied.
31
+ - Keyboard support: `/`, ↑ ↓, Enter, Space, Esc, Tab, ⌘K.
32
+ - Privacy defaults: queries travel by POST only, responses are `no-store`, query
33
+ parameters are filtered from logs, group handles are encrypted, and saved
34
+ searches refuse sensitive values.
35
+
36
+ ## Requirements
37
+
38
+ | | Version |
39
+ | --- | --- |
40
+ | Ruby | 3.3 or newer |
41
+ | Rails | 8.1 (`~> 8.1.0`) |
42
+ | Meilisearch server | 1.13 or newer |
43
+ | `meilisearch` gem | `~> 0.33.0` (you add it to your Gemfile) |
44
+ | JavaScript peers | `@hotwired/turbo` 8, `@hotwired/stimulus` 3.2, `@github/combobox-nav` 3 |
45
+ | CSS | Bootstrap 5 (the partials use its button, form and pagination classes) |
46
+ | Browsers | Current browsers with the Popover API and `<dialog>` |
47
+
48
+ Turbo Drive may be on or off. The widgets only use Turbo Frames and one custom
49
+ Turbo Stream action.
50
+
51
+ ## Installation
52
+
53
+ Add the gem and the Meilisearch client to your Gemfile:
54
+
55
+ ```ruby
56
+ gem "querylane"
57
+ gem "meilisearch", "~> 0.33.0"
58
+ ```
59
+
60
+ To try unreleased work, point the gem at the repository
61
+ (`gem "querylane", github: "clearstackio/querylane", branch: "master"`) and use a
62
+ local checkout without reinstalling:
63
+ `bundle config local.querylane /path/to/querylane`.
64
+
65
+ ### JavaScript
66
+
67
+ Install the peer packages in your app:
68
+
69
+ ```sh
70
+ npm install @hotwired/turbo @hotwired/stimulus @github/combobox-nav
71
+ ```
72
+
73
+ The gem ships ES modules in `app/javascript/querylane/`. Your bundler must find
74
+ them, and must resolve the peers to your app's own copies (two copies of Turbo
75
+ would mean the custom stream action is registered on the wrong one).
76
+
77
+ **jsbundling with webpack.** Ask Bundler where the gem lives, so local overrides
78
+ and lockfile revisions use the same files:
79
+
80
+ ```js
81
+ // webpack.config.js
82
+ const { execFileSync } = require("child_process")
83
+ const path = require("path")
84
+
85
+ const querylaneRoot = execFileSync("bundle", ["exec", "ruby", "-e", "print Gem.loaded_specs['querylane'].full_gem_path"],
86
+ { cwd: __dirname, encoding: "utf8" }).trim()
87
+
88
+ module.exports = {
89
+ // ...your config
90
+ resolve: {
91
+ alias: {
92
+ querylane: path.join(querylaneRoot, "app/javascript/querylane"),
93
+ "@hotwired/stimulus": path.resolve(__dirname, "node_modules/@hotwired/stimulus"),
94
+ "@hotwired/turbo": path.resolve(__dirname, "node_modules/@hotwired/turbo"),
95
+ "@github/combobox-nav": path.resolve(__dirname, "node_modules/@github/combobox-nav")
96
+ }
97
+ }
98
+ }
99
+ ```
100
+
101
+ Webpack's watcher picks up edits to the gem. Restart it when the gem path changes.
102
+
103
+ **importmap-rails (Propshaft or Sprockets).** Nothing to configure for the
104
+ gem's own modules: when your app has a `config/importmap.rb`, the engine adds
105
+ `app/javascript` to the asset load path and draws the gem's
106
+ [`config/importmap.rb`](config/importmap.rb) into your app's map. That map pins
107
+ `querylane` and each module by its file name (`querylane/overlay.js`,
108
+ `querylane/controllers/box_controller.js`, ...), none of them preloaded, so
109
+ pages without a search box fetch nothing. Your app pins the peers, so the page
110
+ shares one copy of each with your own code:
111
+
112
+ ```ruby
113
+ # config/importmap.rb
114
+ pin "@hotwired/stimulus", to: "stimulus.min.js" # stimulus-rails
115
+ pin "@github/combobox-nav" # bin/importmap pin @github/combobox-nav
116
+ ```
117
+
118
+ Turbo stays as your app loads it (turbo-rails' `turbo.min.js` pin is fine).
119
+ querylane never imports Turbo: it handles its one custom stream action through
120
+ the `turbo:before-stream-render` event, which every Turbo 8 copy dispatches.
121
+
122
+ On Sprockets, the modules are precompiled as they are. If your app sets
123
+ `config.assets.js_compressor`, it must parse ES module syntax (`import` and
124
+ `export`), or `assets:precompile` fails on querylane's files. Propshaft does not
125
+ compress.
126
+
127
+ **How the modules import each other.** As `querylane/overlay.js` and so on,
128
+ never by relative path: a relative import would miss the digested URLs
129
+ importmap-rails serves. The same names resolve under the webpack alias above,
130
+ an esbuild alias, the npm package in `app/javascript/querylane`, and a
131
+ hand-written import map with one `"querylane/"` prefix entry.
132
+
133
+ **Import maps without importmap-rails.** Serve the gem's `app/javascript`
134
+ directory and the peer packages as ES modules, then map `querylane` and the
135
+ `querylane/` prefix. The dummy app in [`spec/dummy`](spec/dummy/config/application.rb) does exactly
136
+ this with `Rack::Static` and an inline import map; see
137
+ [Getting started](docs/guides/getting-started.md#javascript).
138
+
139
+ Then register the controllers with your Stimulus application:
140
+
141
+ ```js
142
+ import { registerControllers } from "querylane"
143
+
144
+ registerControllers(application)
145
+ ```
146
+
147
+ `registerControllers` registers `querylane--box`, `querylane--results`,
148
+ `querylane--options` and `querylane--suggest`. Importing `querylane` also adds
149
+ the `querylane_set_query` Turbo Stream action. The gem never starts Stimulus and
150
+ never changes Turbo settings.
151
+
152
+ ### Stylesheet
153
+
154
+ ```erb
155
+ <%= stylesheet_link_tag "querylane" %>
156
+ ```
157
+
158
+ It is plain CSS and works with Sprockets and Propshaft. Theme it with the
159
+ `--ql-*` custom properties ([Theming](docs/guides/theming.md)).
160
+
161
+ ## Quick start
162
+
163
+ A complete, working version of this example lives in [`spec/dummy`](spec/dummy).
164
+
165
+ **1. Point querylane at Meilisearch.**
166
+
167
+ ```ruby
168
+ # config/initializers/querylane.rb
169
+ Rails.application.config.to_prepare do
170
+ Querylane.configure do |config|
171
+ config.searcher = -> { Querylane::Searchers::Meilisearch.new(client: Meilisearch::Client.new(ENV["MEILISEARCH_URL"], ENV["MEILISEARCH_API_KEY"])) }
172
+ end
173
+ end
174
+ ```
175
+
176
+ **2. Describe the search.**
177
+
178
+ ```ruby
179
+ # app/searches/people_search.rb
180
+ module PeopleSearch
181
+ def self.definition
182
+ @definition ||= Querylane::Definition.build(:people) do |d|
183
+ d.index :staff, name: "staff"
184
+ d.text :name, attributes: %i[full_name], default: true, label: "Name contains %{value}"
185
+ d.filter :team, attribute: :team, label: "Team %{value}"
186
+ d.flags :is, alias: :status, attribute: :status_label, label: "Status",
187
+ values: {active: "Active", terminated: "Terminated"}
188
+ d.facet :status, flag: :is
189
+ d.group :person
190
+ end
191
+ end
192
+ end
193
+ ```
194
+
195
+ **3. Search in a controller.**
196
+
197
+ ```ruby
198
+ # config/routes.rb
199
+ get "people", to: "people#index"
200
+ post "people/search", to: "people#search", as: :people_search
201
+ post "people/open", to: "people#open", as: :open_person # the row's Open button posts here
202
+
203
+ # app/controllers/people_controller.rb
204
+ class PeopleController < ApplicationController
205
+ include Querylane::Controller
206
+
207
+ def index
208
+ @result = querylane_search(PeopleSearch.definition, scope: scope)
209
+ end
210
+
211
+ def search
212
+ @result = querylane_search(PeopleSearch.definition, scope: scope)
213
+ if querylane_frame?(:people)
214
+ render partial: "people/results", locals: {result: @result}
215
+ else
216
+ render :index
217
+ end
218
+ end
219
+
220
+ private
221
+
222
+ # What this user may see. Built on the server, never from params.
223
+ def scope = Querylane::Scope.new(staff: Querylane::Filter.eq(:company_id, current_user.company_id))
224
+ end
225
+ ```
226
+
227
+ **4. Draw it.**
228
+
229
+ ```erb
230
+ <%# app/views/people/index.html.erb %>
231
+ <%= querylane_form @result, id: :people, url: people_search_path %>
232
+ <%= render "people/results", result: @result %>
233
+
234
+ <%# app/views/people/_results.html.erb %>
235
+ <%= querylane_results result, id: :people, presenter: PersonPresenter %>
236
+ ```
237
+
238
+ **5. Say how a record looks.**
239
+
240
+ ```ruby
241
+ # app/presenters/person_presenter.rb
242
+ class PersonPresenter < Querylane::Presenter
243
+ def columns = [Column.new(:team, "Team", 120)]
244
+
245
+ def cells(hit) = [Cell.new(hit.document["team"], :strong)]
246
+
247
+ def status(hit)
248
+ (hit.document["status_label"] == "active") ? Badge.new("Active", :success) : Badge.new("Terminated", :neutral)
249
+ end
250
+
251
+ # Open is a full-page POST to the URL, with these params.
252
+ def outcome(hit) = Action.new("Open", view.open_person_path, {id: hit.id}, :primary)
253
+ end
254
+ ```
255
+
256
+ **6. Check the index settings.**
257
+
258
+ ```sh
259
+ bin/rails "querylane:doctor[PeopleSearch.definition]"
260
+ ```
261
+
262
+ The doctor lists every attribute the definition needs that the index does not
263
+ declare (searchable, filterable, sortable, typo tolerance, facet order).
264
+
265
+ ## Learn more
266
+
267
+ | Guide | What it covers |
268
+ | --- | --- |
269
+ | [Getting started](docs/guides/getting-started.md) | An end-to-end first page: routes, frames, index settings, JavaScript, the doctor |
270
+ | [Definition](docs/guides/definition.md) | Every DSL method: operators, facets, sorts, interpreter, groups, panel, hooks |
271
+ | [Query language](docs/guides/query-language.md) | What people can type, precedence, errors, wildcards, interpreted tokens |
272
+ | [Widgets](docs/guides/widgets.md) | Each widget, its helper, params, frames and keys |
273
+ | [Presenter](docs/guides/presenter.md) | How result rows, groups and suggestions are drawn |
274
+ | [Controller](docs/guides/controller.md) | `querylane_search`, `_preview`, `_related`, `_suggest`, frames, guards |
275
+ | [Scope and hooks](docs/guides/scope-and-hooks.md) | Permission scopes, the Filter API, query-filter and results hooks |
276
+ | [Saved searches](docs/guides/saved-searches.md) | The store contract and what is refused |
277
+ | [Theming](docs/guides/theming.md) | CSS custom properties, partial overrides, translations |
278
+ | [Security](docs/guides/security.md) | The privacy model and what the host must do |
279
+ | [Testing](docs/guides/testing.md) | Testing a host app, and running the gem's suite |
280
+ | [Architecture](docs/architecture.md) | How a request flows through the gem (for contributors) |
281
+
282
+ Also: [CHANGELOG](CHANGELOG.md), [CONTRIBUTING](CONTRIBUTING.md),
283
+ [SECURITY](SECURITY.md), [all docs](docs/README.md).
284
+
285
+ ## License
286
+
287
+ MIT. See [LICENSE](LICENSE).
data/SECURITY.md ADDED
@@ -0,0 +1,29 @@
1
+ # Security policy
2
+
3
+ ## Reporting a vulnerability
4
+
5
+ Report it privately through
6
+ [GitHub's private vulnerability reporting](https://github.com/clearstackio/querylane/security/advisories/new),
7
+ or email ajaya@clearstack.io. Please do not open a public issue.
8
+
9
+ We aim to acknowledge a report within three working days, and to publish a
10
+ fix and a GitHub security advisory once it is confirmed.
11
+
12
+ Include the querylane version (or commit), how to reproduce the problem, and
13
+ what an attacker could see or do.
14
+
15
+ ## Supported versions
16
+
17
+ querylane is pre-1.0. Only the latest release is supported.
18
+
19
+ ## The privacy model
20
+
21
+ querylane's guarantees, and the duties it leaves to the host app (building the
22
+ scope on the server, filtering a renamed query parameter, keeping request bodies
23
+ out of APM), are described in [docs/guides/security.md](docs/guides/security.md).
24
+
25
+ ## Releases
26
+
27
+ Releases are published to rubygems.org from GitHub Actions with trusted
28
+ publishing (no long-lived API key) and carry a Sigstore attestation. Any other
29
+ push needs MFA (`rubygems_mfa_required`).