Docyrus Custom Query
A custom query record (tenant_custom_query) is a saved, named, parameterized
query template. Its body is DSQL (logical SQL over appSlug.dataSourceSlug
tables — the dsql_query column, successor to the legacy raw-SQL query), plus
declared output fields, declared runtime filters (parameters), and optional
pivot/calc config. Unlike an ad-hoc dsql query, a saved record is reusable,
parameterized, and callable from app code.
Two API surfaces back these records, both reached through docyrus dsql
subcommands:
- CRUD of the record → dev/architect API, scoped to an app.
- Run (execute) → reports API, by query id, with runtime filter values.
For the record/column shape, fields/filters JSON structure, {{filter}}
operators, and the run contract, see
references/custom-query-record-reference.md.
Workflow
Follow in order.
Confirm auth.
docyrus auth who --json
No session → docyrus auth login.
Author the DSQL body first. Use docyrus-dsql-query-design to discover
schema and write/validate the SELECT. Run it ad-hoc until correct:
docyrus dsql query "select t.id, t.subject, t.status from base.task t limit 5"
Only save a query once the raw DSQL returns what you expect.
Parameterize it. Replace hard-coded predicates with {{filter}} bindings
and decide the output fields. Absent filters compile to 1=1, so one body
serves any subset of parameters:
select t.id, t.subject, t.status
from base.task t
where {{filter FILTERS.status "t.status"}}
order by t.created_on desc
Create the record. name, fields, and one of --dsqlQuery/--query
are required by the API; DSQL-first means --dsqlQuery:
docyrus dsql create-custom-query --appSlug base \
--name "Tasks by status" \
--dsqlQuery 'select t.id, t.subject, t.status from base.task t where {{filter FILTERS.status "t.status"}} order by t.created_on desc' \
--fields '[{"slug":"id","name":"ID","type":"text"},{"slug":"subject","name":"Subject","type":"text"},{"slug":"status","name":"Status","type":"text"}]' \
--filters '[{"slug":"status","name":"Status","type":"text"}]'
Grab the returned id — every other command needs it.
Test / run it. Verify the template compiles and returns rows. Inspect the
compiled SQL first, then run for real:
# See the SQL the template produced (no execution)
docyrus dsql run-custom-query --queryId <id> \
--filters '{"logic":"and","rules":[{"field":"status","operator":"eq","value":"open"}]}' \
--debug true
# Actually run it
docyrus dsql run-custom-query --queryId <id> \
--filters '{"logic":"and","rules":[{"field":"status","operator":"eq","value":"open"}]}'
Each run rule's field is what {{filter FILTERS.<field> ...}} reads. Result
is { data: rows, meta: { count, compiledQuery } }.
Iterate & manage. get-custom-query to inspect, update-custom-query to
change the body/fields/filters, list-custom-query to find ids,
delete-custom-query to soft-archive.
Wire it into a frontend (optional) — see Consuming from a frontend.
Command cheat-sheet
All under docyrus dsql. CRUD needs an app selector (--appId or
--appSlug); run needs only --queryId.
# List saved records for an app
docyrus dsql list-custom-query --appSlug base
# Get one record (full body, fields, filters)
docyrus dsql get-custom-query --appSlug base --queryId <id>
# Create (name + fields + dsqlQuery required; JSON flags take JSON strings)
docyrus dsql create-custom-query --appSlug base --name "…" \
--dsqlQuery '…' --fields '[…]' --filters '[…]'
# Update (partial — only the flags you pass change)
docyrus dsql update-custom-query --appSlug base --queryId <id> --name "…"
# Delete (soft-archive)
docyrus dsql delete-custom-query --appSlug base --queryId <id>
# Run (execute). --debug = compiled SQL only; --simulate = EXPLAIN ANALYZE
docyrus dsql run-custom-query --queryId <id> --filters '{…}' [--offset N] [--debug true] [--simulate true]
Write-command flags: --name, --description, --dsqlQuery (→ dsql_query,
preferred), --query (legacy raw SQL), --fields (JSON), --filters (JSON),
--calculations (JSON), --defaultColumns (JSON), --defaultRows (JSON),
--balanceQuery, --ownerProductId. Anything not covered by a flag — or a whole
payload — can go through --data '<json>' / --from-file <path.json>; flags are
merged over that base.
Consuming from a frontend
Saved custom queries have no generated collection helper. Run one directly
with RestApiClient.runCustomQuery(id, options) from @docyrus/api-client:
import type { RestApiClient } from "@docyrus/api-client";
// options is the run body: { offset?, filters?, debug?, simulate? }
const result = await client.runCustomQuery<{
data: Array<Record<string, unknown>>;
meta: { count: number };
}>(customQueryId, {
filters: {
logic: "and",
rules: [{ field: "status", operator: "eq", value: "open" }],
},
});
const rows = result.data; // the result rows
const total = result.meta.count; // total row count
- The runtime filter
rules[].field must match a {{filter FILTERS.<field> …}}
binding in the saved query — that's how a UI parameter reaches the SQL.
- The call returns the
{ data, meta } envelope; read .data for rows.
- Build filter groups with the exported
prepareFilterQueryForApi helper when you
need the query-string form for other endpoints.
Critical rules
- DSQL-first. Put the query in
dsql_query (--dsqlQuery). It runs through
the DSQL runner over appSlug.dataSourceSlug tables and follows every
docyrus-dsql-query-design rule (read-only, alias tables, qualify columns). The
legacy raw-SQL query is only for pre-existing records.
- Author the DSQL before saving. Validate with
dsql query first; a saved
record with a broken body just fails at run time.
name + fields + a query body are required on create. Keep fields[].slug
in lockstep with the SELECT list.
- Parameters bind through
{{filter}}. Declared filters do nothing unless
the body references them; run-time values arrive in the run body's filters
group keyed by field. Never string-concatenate user input into the SQL — use
{{filter FILTERS.x "col"}}, which escapes and validates.
--debug / --simulate before trusting output. --debug true returns the
compiled SQL without executing; --simulate true returns EXPLAIN ANALYZE. Both
return data: [] with the SQL in meta.compiledQuery.
- CRUD is app-scoped; run is not.
create/get/update/delete/list-custom-query
need --appId/--appSlug; run-custom-query needs only --queryId.
- Delete is soft.
delete-custom-query archives the record; it stops
appearing in list/get but is not physically removed.
- Frontend uses
runCustomQuery. There is no collection hook for saved
queries — call RestApiClient.runCustomQuery(id, options) and wire it into your
own data-fetching layer.
References
- references/custom-query-record-reference.md —
record columns,
fields/filters JSON structure, Handlebars {{filter}}
binding and operators, the run request/response contract, and ownership/RLS.
1---2name: docyrus-custom-query3description: Create, read, update, delete, run, and consume saved Docyrus custom query records (tenant_custom_query) — reusable, parameterized SQL query templates whose body is DSQL — using the `docyrus dsql *-custom-query` CLI commands, and call them from a frontend via `@docyrus/api-client`'s `RestApiClient.runCustomQuery`. Use when the user wants to save a DSQL query as a reusable named report/template, parameterize it with runtime filters (Handlebars `{{filter}}` binding), test/run a saved query (`--debug` compiled SQL, `--simulate` EXPLAIN ANALYZE), manage the lifecycle of custom query records, or run a saved custom query from a React/frontend app. Triggers on "custom query", "saved query", "query template", "reusable report", "parameterized query", "run a saved query", "runCustomQuery", `docyrus dsql create-custom-query`, `docyrus dsql run-custom-query`, `list/get/update/delete-custom-query`, or wiring a saved query into an app. For authoring the DSQL body itself and ad-hoc one-off queries, see docyrus-dsql-query-de4---5
6# Docyrus Custom Query
7
8A **custom query record** (`tenant_custom_query`) is a saved, named, parameterized
9query template. Its body is **DSQL** (logical SQL over `appSlug.dataSourceSlug`
10tables — the `dsql_query` column, successor to the legacy raw-SQL `query`), plus
11declared output `fields`, declared runtime `filters` (parameters), and optional
12pivot/calc config. Unlike an ad-hoc `dsql query`, a saved record is reusable,
13parameterized, and callable from app code.
14
15Two API surfaces back these records, both reached through `docyrus dsql`
16subcommands:
17- **CRUD** of the record → dev/architect API, scoped to an app.
18- **Run** (execute) → reports API, by query id, with runtime filter values.
19
20For the record/column shape, `fields`/`filters` JSON structure, `{{filter}}`
21operators, and the run contract, see
22[references/custom-query-record-reference.md](references/custom-query-record-reference.md).
23
24## Workflow
25
26Follow in order.
27
281. **Confirm auth.**
29 ```bash
30 docyrus auth who --json
31 ```
32 No session → `docyrus auth login`.
33
342. **Author the DSQL body first.** Use **docyrus-dsql-query-design** to discover
35 schema and write/validate the `SELECT`. Run it ad-hoc until correct:
36 ```bash
37 docyrus dsql query "select t.id, t.subject, t.status from base.task t limit 5"
38 ```
39 Only save a query once the raw DSQL returns what you expect.
40
413. **Parameterize it.** Replace hard-coded predicates with `{{filter}}` bindings
42 and decide the output `fields`. Absent filters compile to `1=1`, so one body
43 serves any subset of parameters:
44 ```sql
45 select t.id, t.subject, t.status
46 from base.task t
47 where {{filter FILTERS.status "t.status"}}
48 order by t.created_on desc
49 ```
50
514. **Create the record.** `name`, `fields`, and one of `--dsqlQuery`/`--query`
52 are required by the API; DSQL-first means `--dsqlQuery`:
53 ```bash
54 docyrus dsql create-custom-query --appSlug base \
55 --name "Tasks by status" \
56 --dsqlQuery 'select t.id, t.subject, t.status from base.task t where {{filter FILTERS.status "t.status"}} order by t.created_on desc' \
57 --fields '[{"slug":"id","name":"ID","type":"text"},{"slug":"subject","name":"Subject","type":"text"},{"slug":"status","name":"Status","type":"text"}]' \
58 --filters '[{"slug":"status","name":"Status","type":"text"}]'
59 ```
60 Grab the returned `id` — every other command needs it.
61
625. **Test / run it.** Verify the template compiles and returns rows. Inspect the
63 compiled SQL first, then run for real:
64 ```bash
65 # See the SQL the template produced (no execution)
66 docyrus dsql run-custom-query --queryId <id> \
67 --filters '{"logic":"and","rules":[{"field":"status","operator":"eq","value":"open"}]}' \
68 --debug true
69
70 # Actually run it
71 docyrus dsql run-custom-query --queryId <id> \
72 --filters '{"logic":"and","rules":[{"field":"status","operator":"eq","value":"open"}]}'
73 ```
74 Each run rule's `field` is what `{{filter FILTERS.<field> ...}}` reads. Result
75 is `{ data: rows, meta: { count, compiledQuery } }`.
76
776. **Iterate & manage.** `get-custom-query` to inspect, `update-custom-query` to
78 change the body/fields/filters, `list-custom-query` to find ids,
79 `delete-custom-query` to soft-archive.
80
817. **Wire it into a frontend** (optional) — see [Consuming from a frontend](#consuming-from-a-frontend).
82
83## Command cheat-sheet
84
85All under `docyrus dsql`. CRUD needs an app selector (`--appId` **or**
86`--appSlug`); run needs only `--queryId`.
87
88```bash
89# List saved records for an app
90docyrus dsql list-custom-query --appSlug base
91
92# Get one record (full body, fields, filters)
93docyrus dsql get-custom-query --appSlug base --queryId <id>
94
95# Create (name + fields + dsqlQuery required; JSON flags take JSON strings)
96docyrus dsql create-custom-query --appSlug base --name "…" \
97 --dsqlQuery '…' --fields '[…]' --filters '[…]'
98
99# Update (partial — only the flags you pass change)
100docyrus dsql update-custom-query --appSlug base --queryId <id> --name "…"
101
102# Delete (soft-archive)
103docyrus dsql delete-custom-query --appSlug base --queryId <id>
104
105# Run (execute). --debug = compiled SQL only; --simulate = EXPLAIN ANALYZE
106docyrus dsql run-custom-query --queryId <id> --filters '{…}' [--offset N] [--debug true] [--simulate true]
107```
108
109Write-command flags: `--name`, `--description`, `--dsqlQuery` (→ `dsql_query`,
110preferred), `--query` (legacy raw SQL), `--fields` (JSON), `--filters` (JSON),
111`--calculations` (JSON), `--defaultColumns` (JSON), `--defaultRows` (JSON),
112`--balanceQuery`, `--ownerProductId`. Anything not covered by a flag — or a whole
113payload — can go through `--data '<json>'` / `--from-file <path.json>`; flags are
114merged over that base.
115
116## Consuming from a frontend
117
118Saved custom queries have **no generated collection helper**. Run one directly
119with `RestApiClient.runCustomQuery(id, options)` from `@docyrus/api-client`:
120
121```ts
122import type { RestApiClient } from "@docyrus/api-client";
123
124// options is the run body: { offset?, filters?, debug?, simulate? }
125const result = await client.runCustomQuery<{
126 data: Array<Record<string, unknown>>;
127 meta: { count: number };
128}>(customQueryId, {
129 filters: {
130 logic: "and",
131 rules: [{ field: "status", operator: "eq", value: "open" }],
132 },
133});
134
135const rows = result.data; // the result rows
136const total = result.meta.count; // total row count
137```
138
139- The runtime filter `rules[].field` must match a `{{filter FILTERS.<field> …}}`
140 binding in the saved query — that's how a UI parameter reaches the SQL.
141- The call returns the `{ data, meta }` envelope; read `.data` for rows.
142- Build filter groups with the exported `prepareFilterQueryForApi` helper when you
143 need the query-string form for other endpoints.
144
145## Critical rules
146
147- **DSQL-first.** Put the query in `dsql_query` (`--dsqlQuery`). It runs through
148 the DSQL runner over `appSlug.dataSourceSlug` tables and follows every
149 docyrus-dsql-query-design rule (read-only, alias tables, qualify columns). The
150 legacy raw-SQL `query` is only for pre-existing records.
151- **Author the DSQL before saving.** Validate with `dsql query` first; a saved
152 record with a broken body just fails at run time.
153- **`name` + `fields` + a query body are required on create.** Keep `fields[].slug`
154 in lockstep with the `SELECT` list.
155- **Parameters bind through `{{filter}}`.** Declared `filters` do nothing unless
156 the body references them; run-time values arrive in the run body's `filters`
157 group keyed by `field`. Never string-concatenate user input into the SQL — use
158 `{{filter FILTERS.x "col"}}`, which escapes and validates.
159- **`--debug` / `--simulate` before trusting output.** `--debug true` returns the
160 compiled SQL without executing; `--simulate true` returns EXPLAIN ANALYZE. Both
161 return `data: []` with the SQL in `meta.compiledQuery`.
162- **CRUD is app-scoped; run is not.** `create/get/update/delete/list-custom-query`
163 need `--appId`/`--appSlug`; `run-custom-query` needs only `--queryId`.
164- **Delete is soft.** `delete-custom-query` archives the record; it stops
165 appearing in list/get but is not physically removed.
166- **Frontend uses `runCustomQuery`.** There is no collection hook for saved
167 queries — call `RestApiClient.runCustomQuery(id, options)` and wire it into your
168 own data-fetching layer.
169
170## References
171
172- **[references/custom-query-record-reference.md](references/custom-query-record-reference.md)** —
173 record columns, `fields`/`filters` JSON structure, Handlebars `{{filter}}`
174 binding and operators, the run request/response contract, and ownership/RLS.