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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +84 -0
- data/CONTRIBUTING.md +118 -0
- data/LICENSE +21 -0
- data/README.md +287 -0
- data/SECURITY.md +29 -0
- data/app/assets/stylesheets/querylane.css +300 -0
- data/app/helpers/querylane/search_helper.rb +111 -0
- data/app/javascript/querylane/controllers/box_controller.js +170 -0
- data/app/javascript/querylane/controllers/options_controller.js +54 -0
- data/app/javascript/querylane/controllers/results_controller.js +137 -0
- data/app/javascript/querylane/controllers/suggest_controller.js +187 -0
- data/app/javascript/querylane/index.js +33 -0
- data/app/javascript/querylane/overlay.js +46 -0
- data/app/javascript/querylane/package.json +7 -0
- data/app/views/querylane/_chips.html.erb +15 -0
- data/app/views/querylane/_denied.html.erb +6 -0
- data/app/views/querylane/_empty.html.erb +17 -0
- data/app/views/querylane/_facets.html.erb +30 -0
- data/app/views/querylane/_first_load.html.erb +15 -0
- data/app/views/querylane/_form.html.erb +46 -0
- data/app/views/querylane/_group.html.erb +21 -0
- data/app/views/querylane/_help.html.erb +29 -0
- data/app/views/querylane/_options.html.erb +32 -0
- data/app/views/querylane/_options_row.html.erb +51 -0
- data/app/views/querylane/_outcome.html.erb +16 -0
- data/app/views/querylane/_pager.html.erb +19 -0
- data/app/views/querylane/_preview.html.erb +39 -0
- data/app/views/querylane/_record.html.erb +16 -0
- data/app/views/querylane/_related.html.erb +17 -0
- data/app/views/querylane/_results.html.erb +66 -0
- data/app/views/querylane/_rows.html.erb +13 -0
- data/app/views/querylane/_saved.html.erb +31 -0
- data/app/views/querylane/_suggest_box.html.erb +45 -0
- data/app/views/querylane/_suggestions.html.erb +47 -0
- data/app/views/querylane/_toolbar.html.erb +28 -0
- data/app/views/querylane/_unavailable.html.erb +7 -0
- data/app/views/querylane/_writes.html.erb +16 -0
- data/config/importmap.rb +34 -0
- data/config/locales/querylane.en.yml +136 -0
- data/lib/querylane/ast.rb +17 -0
- data/lib/querylane/chips.rb +85 -0
- data/lib/querylane/compiler.rb +52 -0
- data/lib/querylane/configuration.rb +49 -0
- data/lib/querylane/context.rb +11 -0
- data/lib/querylane/controller.rb +85 -0
- data/lib/querylane/definition.rb +447 -0
- data/lib/querylane/doctor.rb +52 -0
- data/lib/querylane/engine.rb +61 -0
- data/lib/querylane/errors.rb +27 -0
- data/lib/querylane/filter.rb +75 -0
- data/lib/querylane/group_token.rb +33 -0
- data/lib/querylane/parser.rb +178 -0
- data/lib/querylane/presenter.rb +160 -0
- data/lib/querylane/preview.rb +45 -0
- data/lib/querylane/query.rb +183 -0
- data/lib/querylane/request.rb +66 -0
- data/lib/querylane/result.rb +90 -0
- data/lib/querylane/rewriter.rb +245 -0
- data/lib/querylane/saving.rb +49 -0
- data/lib/querylane/scope.rb +53 -0
- data/lib/querylane/search.rb +347 -0
- data/lib/querylane/searchers/meilisearch.rb +226 -0
- data/lib/querylane/suggest.rb +67 -0
- data/lib/querylane/version.rb +5 -0
- data/lib/querylane.rb +60 -0
- data/lib/tasks/querylane.rake +14 -0
- 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
|
+
[](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`).
|