@dboio/cli 0.11.4 → 0.13.2

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 (48) hide show
  1. package/README.md +126 -3
  2. package/bin/dbo.js +4 -0
  3. package/package.json +1 -1
  4. package/plugins/claude/dbo/.claude-plugin/plugin.json +1 -1
  5. package/plugins/claude/dbo/commands/dbo.md +65 -244
  6. package/plugins/claude/dbo/docs/_audit_required/API/all.md +40 -0
  7. package/plugins/claude/dbo/docs/_audit_required/API/app.md +38 -0
  8. package/plugins/claude/dbo/docs/_audit_required/API/athenticate.md +26 -0
  9. package/plugins/claude/dbo/docs/_audit_required/API/cache.md +29 -0
  10. package/plugins/claude/dbo/docs/_audit_required/API/content.md +14 -0
  11. package/plugins/claude/dbo/docs/_audit_required/API/data_source.md +28 -0
  12. package/plugins/claude/dbo/docs/_audit_required/API/email.md +18 -0
  13. package/plugins/claude/dbo/docs/_audit_required/API/input.md +25 -0
  14. package/plugins/claude/dbo/docs/_audit_required/API/instance.md +28 -0
  15. package/plugins/claude/dbo/docs/_audit_required/API/log.md +8 -0
  16. package/plugins/claude/dbo/docs/_audit_required/API/media.md +12 -0
  17. package/plugins/claude/dbo/docs/_audit_required/API/output_by_entity.md +12 -0
  18. package/plugins/claude/dbo/docs/_audit_required/API/upload.md +7 -0
  19. package/plugins/claude/dbo/docs/_audit_required/dbo-api-syntax.md +1487 -0
  20. package/plugins/claude/dbo/docs/_audit_required/dbo-problems-code.md +111 -0
  21. package/plugins/claude/dbo/docs/_audit_required/dbo-problems-performance.md +109 -0
  22. package/plugins/claude/dbo/docs/_audit_required/dbo-problems-syntax.md +97 -0
  23. package/plugins/claude/dbo/docs/_audit_required/dbo-product-market.md +119 -0
  24. package/plugins/claude/dbo/docs/_audit_required/dbo-white-paper.md +125 -0
  25. package/plugins/claude/dbo/docs/dbo-cheat-sheet.md +323 -0
  26. package/plugins/claude/dbo/docs/dbo-cli-readme.md +2222 -0
  27. package/plugins/claude/dbo/docs/dbo-core-entities.md +878 -0
  28. package/plugins/claude/dbo/docs/dbo-output-customsql.md +677 -0
  29. package/plugins/claude/dbo/docs/dbo-output-query.md +967 -0
  30. package/plugins/claude/dbo/skills/cli/SKILL.md +62 -246
  31. package/src/commands/add.js +366 -62
  32. package/src/commands/build.js +102 -0
  33. package/src/commands/clone.js +602 -139
  34. package/src/commands/diff.js +4 -0
  35. package/src/commands/init.js +16 -2
  36. package/src/commands/input.js +3 -1
  37. package/src/commands/mv.js +12 -4
  38. package/src/commands/push.js +265 -70
  39. package/src/commands/rm.js +16 -3
  40. package/src/commands/run.js +81 -0
  41. package/src/lib/config.js +39 -0
  42. package/src/lib/delta.js +7 -1
  43. package/src/lib/diff.js +24 -2
  44. package/src/lib/filenames.js +120 -41
  45. package/src/lib/ignore.js +6 -0
  46. package/src/lib/input-parser.js +13 -4
  47. package/src/lib/scripts.js +232 -0
  48. package/src/migrations/006-remove-uid-companion-filenames.js +181 -0
@@ -0,0 +1,967 @@
1
+ # Output — Query
2
+
3
+ A **Query output** is a named, configurable SQL query stored in the dbo core database. Its structure maps directly to SQL: a base entity (FROM), output values (SELECT), join records (JOIN), and filter records (WHERE / HAVING). Rendered results can be styled through a template stored in a content record.
4
+
5
+ **Output type covered**: `Type = 'Query'` (default). For `CustomSQL` see `output-customsql.md`.
6
+
7
+
8
+ ---
9
+
10
+
11
+
12
+ ## Data Model
13
+
14
+ ### `output` table
15
+
16
+ Query metadata and FROM table
17
+
18
+ | Column | Attributes | Purpose | Notes |
19
+ | :------------------ | :------------------ | :------------------------------------- | :----------------------------------------------------------- |
20
+ | `AppID` | `SMALLINT NN FK` | Parent app | |
21
+ | `Name` | `VARCHAR(100) NN` | Display name | Also used as the asset filename |
22
+ | `UID` | `VARCHAR(22) NN AUTOGENERATED` | Stable identifier | Auto-generated on creation; cannot be updated. Used in API calls: `/api/output/{UID}` |
23
+ | `Type` | `ENUM` | Output rendering mode | `Query` type required |
24
+ | `BaseEntityID` | `BIGINT NN FK` | Table set in the FROM clause | Required for query output |
25
+ | `TemplateContentID` | `BIGINT FK` | Content record holding the template | Overridable per request via `_template` or an open embed block |
26
+ | `Limit` | `INT` | Rows returned | |
27
+ | `RowsPerPage` | `INT` | Rows per page when paginating | Use with `_page=N` at request time |
28
+ | `Public` | `BIT default:false` | Bypass authentication | When true, no login is required to execute |
29
+ | `AdhocFilters` | `BIT default:false` | Allow dynamic column filtering | When true, columns without pre-configured filters can be targeted with `_filter` |
30
+ | `Cache` | `BIT default:false` | Cache the result set | Output structure is always cached automatically; this flag caches the results set beyond runtime. Not required to acces `:cache` modifier on render tokens. |
31
+ | `Parameters` | `VARCHAR(200)` | Fixed parameters injected at execution | |
32
+ | `DataSourceID` | `BIGINT FK` | Table data source | If entity is present in multiple envirnment schemas, this will allow one to switch between connection strings |
33
+ | `CustomSQL` | `TEXT` | Custom SQL body | Must be null for Query type |
34
+ | `Active` | `BIT default:true` | Enable or disable this output | When false, requests return an asset-not-found exception |
35
+
36
+
37
+
38
+ ### `output_value` table
39
+
40
+ One output_value record per SELECT column
41
+
42
+
43
+ | Column | Attributes | Purpose | Notes |
44
+ | :----------------------------- | :----------------------------------------------------------- | :------------------------------------- | :----------------------------------------------------------- |
45
+ | `OutputID` | `BIGINT NN FK` | Parent output | |
46
+ | `UID` | `VARCHAR(22) NN AUTOGENERATED` | Stable identifier | Auto-generated on creation; cannot be updated |
47
+ | `OrderNumber` | `SMALLINT NN` | Column rendering order | |
48
+ | `Title` | `VARCHAR(100) NN` | Column label | Not available as a column reference |
49
+ | `ShortName` | `VARCHAR(45)` | Token reference key | `#{value@ShortName}`; if null, only UID references this column |
50
+ | `ReferencedEntityColumnID` | `BIGINT FK` | Column to SELECT | |
51
+ | `CustomSQL` | `LONGTEXT` | Custom expression | Accommodates `CASE` statements, format, and other computations in the SELECT |
52
+ | `OutputValueEntityColumnRelID` | `BIGINT FK` | Join context for display column | If null, defaults to base entity |
53
+ | `Hide` | `BIT default:false` | Exclude from SELECT output | Useful for columns that are desrired in the `WHERE`, `HAVING`, or `ORDER BY` but not desired the `SELECT` |
54
+ | `Aggregate` | `ENUM( 'Group', 'Count', 'Sum', 'Average', 'Min', 'Max')` | | |
55
+ | `Summary` | `ENUM( 'Count', 'Sum', 'Min', 'Max', 'Average')` | Row summary aggregate | Computed across all rows; renders in `<#_summary>` |
56
+ | `Shared` | `BIT default:false` | Shared value optimization | When true and all rows share the same value, renders once in `<#_shared>` |
57
+ | `Distinct` | `BIT default:false` | Add DISTINCT to SELECT and Aggregation | |
58
+ | `SortOrder` | `SMALLINT` | Sort priority | 1 = primary sort; null = unsorted |
59
+ | `SortType` | `ENUM( 'Ascending', 'Descending')` `default:ascending` | Sort type | |
60
+ | `Format` | `VARCHAR(30)` | Default value format | .NET format string (e.g. `C2`, `d`) — inline token modifier overrides per request |
61
+ | `EnforceSecurity` | `ENUM( 'Add', 'Edit', 'Delete', 'View', 'Execute', 'Associate')` | Auto-filter rows by user access | Restricts result rows to those the user can access for the given operation; see **EnforceSecurity** below |
62
+
63
+
64
+
65
+ ### EnforceSecurity
66
+ When set, dbo generates an automatic WHERE IN clause at query execution time. This restricts results to rows accessible to the logged in user for the specified operation (View, Add, Edit, Delete, Execute, Associate). This access is controlled by the security table. Security access is cached at request scope. The output must include the primary key of the source entity — if absent, dbo throws an error at query execution time. For more information see authenticate.md (pending).
67
+
68
+ > **Note**: `secure=false` on an embed bypasses the Execute permission check for that embed only. It does *not* suppress `EnforceSecurity` row filtering — the WHERE IN clause is generated at query-build time, before the embed's security bypass takes effect.
69
+
70
+ ###### #enf-sec
71
+
72
+
73
+
74
+
75
+ ### `output_value_entity_column_rel` table — one record per JOIN clause
76
+
77
+ Attach to the output (`OutputID`) to define joins shared across all output values.
78
+
79
+ | Column | Attributes | Purpose | Notes |
80
+ | :------------------------- | :-------------------------------------------------------------- | :---------------------- | :--------------------------------------------------------------- |
81
+ | `OutputID` | `BIGINT NN FK` | Parent output | |
82
+ | `OutputValueID` | `BIGINT FK` | must be null for `Query` type output. | This column is only for `Legacy` type output's per-column join system |
83
+ | `UID` | `VARCHAR(22) NN` | Stable identifier | Auto-generated on creation; cannot be updated |
84
+ | `SourceEntityColumnID` | `BIGINT NN FK` | Join from column | |
85
+ | `ReferencedEntityColumnID` | `BIGINT FK` | Join to column | dbo infers ON clause table from the column; NN for `Query` type |
86
+ | `JoinType` | `ENUM( 'InnerJoin', 'LeftOuterJoin', 'RightOuterJoin', 'FullOuterJoin') NN` | Join type | |
87
+ | `OrderNumber` | `BIGINT NN` | Join sequence (1-based) | Determines the order joins are applied |
88
+ | `SourceJoinID` | `BIGINT FK` | Join from a prior aliased join | Self-reference — see **Child joins** below |
89
+ | `Alias` | `VARCHAR(50)` | Explicit table alias | Used when two joins target the same table |
90
+ | `CustomSQL` | `VARCHAR(1000)` | Custom ON clause | Replaces the standard column-equality condition |
91
+
92
+ > **Note**: Join columns are not required to be PK/FK pairs— as in SQL, any columns of matching type can be joined (e.g., varchar to varchar).
93
+
94
+ **Child joins**: set `SourceJoinID` to the ID of a prior join record to join from it. `SourceJoinID` is **required** when a table has been joined to elsewhere in the query. Without it, the engine cannot determine which table alias is the intended source and throws an ambiguous-source error. For simple joins, where the source table appears only once, `SourceJoinID` may be omitted.
95
+
96
+
97
+
98
+ ### `output_value_filter` table — one record per WHERE / HAVING condition
99
+
100
+ Create a filter record linked to an output value to filter on a specific column.
101
+
102
+ | Column | Attributes | Purpose | Notes |
103
+ | :----------------- | :------------------------------------------------------------------------------------------------------------------ | :----------------------------- | :----------------------------------------------------------------------------------------------------------------------------- |
104
+ | `OutputID` | `BIGINT FK` | Output-level filter attachment | Mutually exclusive with `OutputValueID` |
105
+ | `OutputValueID` | `BIGINT FK` | Column-level filter attachment | Mutually exclusive with `OutputID` |
106
+ | `UID` | `VARCHAR(22) NN` | Stable identifier | Auto-generated on creation; cannot be updated |
107
+ | `OrderNumber` | `BIGINT NN` | Filter sequence (1-based) | No default — must be supplied on create |
108
+ | `ShortName` | `VARCHAR(45)` | URL parameter key | Required for prompted filters: `?_filter@ShortName=value` |
109
+ | `FilterType` | `ENUM( 'Exact', 'Contains', 'StartsWith', 'EndsWith', 'LessThan', 'LessThanOrEqualTo', 'GreaterThan', 'GreaterThanOrEqualTo') default:Exact` | Comparison type | |
110
+ | `PrimaryValue` | `VARCHAR(100)` | Stored filter value | Can be overriden when `Prompted` set to true. Preset strings also accepted; see **Filter Presets** |
111
+ | `Prompted` | `BIT default:false` | Accept URL parameter | When true, value can be set in query string — see **Filters** |
112
+ | `Required` | `BIT default:false` | Require a value | When true, if no prompted value is supplied at runtime, query throws exception |
113
+ | `Inclusive` | `BIT default:true` | Include or exclude | true = include matches (default); false = exclude matches (NOT) |
114
+ | `Operator` | `ENUM( 'OR', 'AND') default:OR` | Multi-value logic | Applies when multiple values are passed |
115
+ | `ApplyToAggregate` | `BIT default:false` | Filter aggregated column | When true, generates HAVING instead of WHERE |
116
+
117
+
118
+
119
+ **Output attached tokens**: Filters applied at the output level can pass values into the query without restricting the result set. Unlike row filters, these do not generate a WHERE clause — instead, the supplied value is available to calculated columns, SQL formulas, and CASE expressions within the output. Common uses include parameterized formatting, dynamic thresholds, and conditional logic that depends on request or session context.
120
+
121
+ **Filter value tokens:** Values passed to the query via `_filter` parameter are available inside CustomSQL expressions as filter_value tokens. The filter_value key uses filter uid or shortname as the reference: e.g. `#{filter_value@Shortname}`. These can be combined with output-level filters (set `OutputID` with no `OutputValueID`) which are not appended to the WHERE clause.
122
+
123
+
124
+
125
+ ---
126
+
127
+
128
+
129
+ ## Create
130
+
131
+ Submit via `POST /api/input/submit?_confirm=true`. All output records can be created in a single transaction using pairing keys (`add1`, `add2`, …). Cross-references within the batch use `=add1` syntax. See `input.md` for details.
132
+
133
+ Column references use UID, which in the dbo core are prettified as `entityName.ColumnName` . See `schema-catalog.md` for system entity column names.
134
+
135
+ ### Example — User list, filter by FirstName, join DefaultMedia, return FullPath
136
+
137
+ Five records in one transaction: output → two output values (FirstName, FullPath) → one join (user → media) → one filter (FirstName, prompted).
138
+
139
+ > **Note**: the owning app (`AppID`) must already exist — it cannot be created in the same transaction as the output records.
140
+
141
+ ```css
142
+ POST /api/input/submit?_confirm=true
143
+
144
+ # add1 — output record (FROM clause)
145
+ RowID:add1;column:output.AppID = [app.UID OR app.AppID]
146
+ RowID:add1;column:output.Name = User List
147
+ RowID:add1;column:output.Type = Query
148
+ RowID:add1;column:output.BaseEntityID = user
149
+
150
+ # add2 — output_value: FirstName (base entity, no join needed)
151
+ RowID:add2;column:output_value.OutputID = add1
152
+ RowID:add2;column:output_value.OrderNumber = 1
153
+ RowID:add2;column:output_value.Title = First Name
154
+ RowID:add2;column:output_value.ShortName = FirstName
155
+ RowID:add2;column:output_value.ReferencedEntityColumnID = user.FirstName
156
+
157
+ # add3 — output_value: FullPath (from joined media table)
158
+ RowID:add3;column:output_value.OutputID = add1
159
+ RowID:add3;column:output_value.OrderNumber = 2
160
+ RowID:add3;column:output_value.Title = Media Path
161
+ RowID:add3;column:output_value.ShortName = MediaPath
162
+ RowID:add3;column:output_value.ReferencedEntityColumnID = media.FullPath
163
+
164
+ # add4 — output_value_entity_column_rel: join user.DefaultMediaID → media.MediaID
165
+ RowID:add4;column:output_value_entity_column_rel.OutputID = add1
166
+ RowID:add4;column:output_value_entity_column_rel.SourceEntityColumnID = user.DefaultMediaID
167
+ RowID:add4;column:output_value_entity_column_rel.ReferencedEntityColumnID = media.MediaID
168
+ RowID:add4;column:output_value_entity_column_rel.JoinType = LeftOuterJoin
169
+ RowID:add4;column:output_value_entity_column_rel.OrderNumber = 1
170
+
171
+ # add5 — output_value_filter: filter FirstName by prompted URL param
172
+ RowID:add5;column:output_value_filter.OutputValueID = add2
173
+ RowID:add5;column:output_value_filter.ShortName = FirstName
174
+ RowID:add5;column:output_value_filter.FilterType = Contains
175
+ RowID:add5;column:output_value_filter.Prompted = 1
176
+ ```
177
+
178
+ Execute with filter: `GET /api/output/[uid]?_filter@FirstName:contains=a`
179
+
180
+ ---
181
+
182
+
183
+
184
+ ## Execute / Read
185
+
186
+ ```css
187
+ GET /api/output/[uid]
188
+ GET /api/o/[uid]
189
+ ```
190
+
191
+ ### Request parameters
192
+
193
+ | Parameter | Description |
194
+ | :------------------------------------ | :----------------------------------------------------------- |
195
+ | `_filter@ShortName=value` | Filter applies to pre-configured `Prompted` filter records |
196
+ | `_filter@ShortName:restrictive=value` | Cannot be overridden by any parameter at a higher level in the request stack |
197
+ | `_limit=N` | Number of rows returned |
198
+ | `_limit=M-N` | Range of rows returned |
199
+ | `_rowcount=false` | Omit pagination metadata from response — removes a second result table with `Offset`, `MaxRows`, and `TotalRowCount`; result rows are unchanged |
200
+ | `_template={uid\|name}` | Override template — pass a content UID or built-in template name (e.g., `json_indented`, `json`, `csv`, `xml`). See **System templates** |
201
+ | `_sort=ShortName:[asc\|desc]` | Sort column and direction |
202
+ | `_search@ShortName1,ShortName2=term` | Full-text search across specified columns. **See Performance caution —** `_search` |
203
+ | `_page=N` | Page number (when `RowsPerPage` is set) |
204
+ | `_rowsperpage=N` | Override `RowsPerPage` for this request |
205
+ | `_debug_sql=true` | Return the generated SQL as `text/plain` instead of executing — administrator only. See **_debug_sql** below |
206
+
207
+ > **Note**: `_filter` defaults to `:overridable` — inner embed values win over outer frame values. To lock a filter against embed override, use `_filter@Field:restrictive=value`. See **Request Stack** below.
208
+
209
+ **Performance caution — `_rowcount`**: adds `SQL_CALC_FOUND_ROWS` to the SELECT and runs a second `SELECT FOUND_ROWS()` after the main query. MySQL must count all rows matching the WHERE clause regardless of `LIMIT`, which defeats index range scans on large tables. Avoid on high-traffic or large-dataset outputs unless pagination is a hard requirement.
210
+
211
+ **Performance caution — `_search`**: generates a `CONCAT` of all specified columns (each cast to `VARCHAR`, null-handled) and applies `LIKE '%term%'` to the result. The leading wildcard prevents index use. Expensive on large tables — always pair with `_limit` to cap results. Use `@Shortname` to specify columns to reduce the CONCAT width.
212
+
213
+
214
+
215
+ ### Request Stack
216
+
217
+ When a content record renders, each `<#_embed>` brings its own URL parameters into scope. To resolve a request parameter, dbo works through the embed chain and applies the rule for that parameter's modifier. The original HTTP request is the outermost level; nested embeds build inward from there. Parameters from outer levels are available to inner ones — the modifier determines whether an inner value holds firm, yields to an outer one, or merges with values from multiple levels.
218
+
219
+ Stack depth is capped at 100 frames.
220
+
221
+ > **Note**: Pass `_debug:embeds=true` on any content request and view source to inspect the live stack. Each embed's output includes a `RequestStack:` block showing frame depth, entity identity, and the parameters each frame contributed:
222
+ >
223
+ > ```css
224
+ > REQUEST + _request@_debug:embeds=true
225
+ > \ content Name=my-page, UID=... +
226
+ > \ \ #_embed url=/api/output/[uid]?_limit=2 + _limit=2 ; _request@_limit=2
227
+ > \ \ \ output [UID=[uid], Name=my-output] +
228
+ > ```
229
+ >
230
+ > `\` marks frame depth. `+` separates the frame from its contributed parameters.
231
+
232
+ #### Modifier reference
233
+
234
+ | Modifier | Behavior | Default for |
235
+ |----------|----------|-------------|
236
+ | `:restrictive` | Param value cannot be replaced by an outer frame. | `_limit` |
237
+ | `:overridable` | Param value can be replaced by an outer frame. | `_filter`, `_display`, `_strict`, `_request` |
238
+ | `:expansive` | All frames contribute — values from every matching frame are merged. | — (opt-in only) |
239
+
240
+ Unmodified parameters inherit the default modifier for their type.
241
+
242
+ #### Usage patterns
243
+
244
+ **Lock a filter against embed override:**
245
+
246
+ ```http
247
+ GET /api/output/[uid]?_filter@Status:restrictive=active
248
+ ```
249
+
250
+ In this example, the `:restrictive` modifier locks `Status` at the request frame — no nested embed using the default `:overridable` can replace it. Without it, `_filter` defaults to `:overridable` and a nested embed targeting the same filter would take precedence.
251
+
252
+ **Allow a limit to be overridden by a nested embed:**
253
+
254
+ ```css
255
+ <#_embed url="/api/output/[uid]?_limit:overridable=10" name="my.list"/>
256
+ ```
257
+
258
+ The `:overridable` modifier marks this limit as a default — any higher-level caller providing `_limit` takes precedence. Without it, `_limit` defaults to `:restrictive` and no outer frame can replace it.
259
+
260
+ **Accumulate values across frames:**
261
+
262
+ ```
263
+ GET /api/output/[uid]?_filter@Tags:expansive=admin
264
+ ```
265
+
266
+ ```css
267
+ <#_embed url="/api/output/[uid]?_filter@Tags:expansive=active" name="my.list"/>
268
+ ```
269
+
270
+ Both values are applied — the resolved filter covers both `admin` and `active`.
271
+
272
+ > **Note**: The `output_value_filter.Operator` setting (`OR` / `AND`, default `OR`) controls how multiple prompted values are combined in SQL. This applies equally to values accumulated across frames via `:expansive` — cross-frame values merge into the same filter array and Operator governs their SQL combination.
273
+
274
+ ### Request Keys
275
+
276
+ URL parameters are available as request tokens regardless of which syntax is used. Any URL parameter `key=value` resolves at `#{request@key}`. The explicit form `_request@Key=value` is equivalent — both produce the same token on the request stack.
277
+
278
+ | Syntax | URL parameter | Token |
279
+ |--------|---------------|-------|
280
+ | Concise | `?status=active` | `#{request@status}` |
281
+ | Verbose | `?_request@Status=active` | `#{request@Status}` |
282
+ | Quoted reference | `?_filter@Status=active` | `#{request@"_filter@Status"}` |
283
+
284
+ Use the concise form for application-defined parameters. Use the verbose form when the key would otherwise be parsed as a system token type — for example, when the key begins with `_filter`, `_limit`, or another reserved prefix.
285
+
286
+ ###### #req-key
287
+
288
+
289
+ ### Request Tokens
290
+
291
+ `#{request@Key}` reads the value of URL parameter `Key` from the current request context. The parameter can be supplied in either the concise or verbose form — see **Request Keys**.
292
+
293
+ ```css
294
+ #{request@Status} ← reads ?Status= or ?_request@Status=
295
+ #{request@CustomerID!0} ← falls back to 0 if not present
296
+ #{request@Mode!#{session@DefaultMode}} ← token as default
297
+ ```
298
+
299
+ Parameters supplied on the outer request URL are available to inner embeds by default — they flow inward through the embed chain. A parameter set inside an embed is scoped to that embed and does not propagate outward.
300
+
301
+ When the full parameter key contains a reserved string— such as in `#{request@"_filter@Status"}` — quote the reference to prevent the parser from treating it as a property boundary. The quotes are stripped from the resolved reference; single quotes are equivalent.
302
+
303
+ > **Note**: All standard request parameters (`_filter`, `_sort`, `_limit`, `_template`, `_page`, `_rowsperpage`) are resolved via the stack on every output request.
304
+
305
+ ###### #req-tok
306
+
307
+ ### _debug_sql
308
+
309
+ Append `_debug_sql=true` to any output request to return the generated SQL instead of executing. The response is `text/plain` — no template rendering occurs.
310
+
311
+ Output format:
312
+
313
+ ```css
314
+ -- Parameters:
315
+
316
+ SET @ParamName := value;
317
+
318
+ -- Query:
319
+
320
+ SELECT ... FROM ... WHERE ... LIMIT ...;
321
+ SELECT 0 AS `Offset`, N AS `MaxRows`, FOUND_ROWS() AS `TotalRowCount`
322
+ ```
323
+
324
+ One declaration per filter parameter that has a value. Parameters with no value are omitted. Debug output always shows MySQL-style `SET @ParamName := value` syntax regardless of the actual data source type.
325
+
326
+ The second `SELECT` is a pagination metadata query present by default; omit with `_rowcount=false`.
327
+
328
+ Column aliases in the SELECT use the format `{outputValueUID}_{ShortName}`.
329
+
330
+ **Requires administrator access.** Non-administrators receive a 401 response.
331
+
332
+ ###### #deb-sql
333
+
334
+
335
+ ### System templates
336
+
337
+ Pass a system template identifier to `_template` to override the output's configured template for the request. `_template` takes precedence over the output record's `TemplateContentID`; pass a content UID to use a custom template instead.
338
+
339
+ | Identifier | Content-Type | Output structure | Notes |
340
+ |------|-------------|-----------------|-------|
341
+ | `json_indented` | `application/json` | Pretty-printed JSON array, one object per row | Direct serialization — native types preserved, column names deduplicated. Preferred for API consumption. |
342
+ | `json_raw` | `application/json` | Compact JSON array, one object per row | Same serialization path as `json_indented` — native types, no HTML escaping, no whitespace. |
343
+ | `html` | `text/html` | `<table>` with header row, one `<tr>` per row, summary row if configured | Renders `<#_summary>` when any output value has `Summary` set. |
344
+ | `csv` | `application/json`* | Header row + one row per result, comma-separated | *Content-Type header incorrectly returns `application/json`. Template-rendered; duplicate ShortNames produce duplicate columns. |
345
+ | `txt` | `text/plain` | Header row + one row per result, space-separated | Template-rendered; duplicate ShortNames produce duplicate columns. |
346
+ | `xml` | `text/xml` | *(empty)* | Returns HTTP 200 with empty body — template file is empty. Not usable. |
347
+
348
+ #### json_indented & json_raw
349
+
350
+ Both templates serialize the result set as a JSON array of row objects — no wrapper, no entity metadata. Column names are taken from each output value's `Title` field. Primary key columns and internal `Metadata:Count` columns are excluded.
351
+
352
+ ```json
353
+ [
354
+ { "FirstName": "Jane", "LastName": "Doe", "Email": "jane@example.com" },
355
+ { "FirstName": "John", "LastName": "Smith", "Email": "john@example.com" }
356
+ ]
357
+ ```
358
+
359
+ The default `json` template wraps the result in an entity object with structural metadata:
360
+
361
+ ```json
362
+ {
363
+ "recordState": "MultiRecord",
364
+ "metadata": { "entityID": 42, "uid": "...", "displayName": "User", ... },
365
+ "records": [ { ... } ]
366
+ }
367
+ ```
368
+
369
+ `json_indented` and `json_raw` use the same serialization path — the only difference is whitespace. Both preserve native types (numbers, nulls) without HTML escaping.
370
+
371
+ **`_format_values=true`**: pass alongside `_template=json_raw` to apply token formatting to cell values before serialization. Without it, raw database values are used directly.
372
+
373
+ > **Note**: If two output values share the same `Title`, their JSON keys collide — the last value wins. Ensure output value titles are unique when using these templates.
374
+
375
+ ###### #sys-tem
376
+
377
+
378
+
379
+ #### Custom template
380
+
381
+ Pass a content UID to load a content record as the template:
382
+
383
+ ```css
384
+ GET /api/output/[uid]?_template=[content-uid]
385
+ ```
386
+
387
+ The content record's `Extension` field determines the output format: `html` → HTML, `json` → JSON, `xml` → XML, `css` → CSS, `js` → JSON, anything else → plain text.
388
+
389
+ #### Priority
390
+
391
+ 1. `_template` request parameter (this section)
392
+ 2. `TemplateContentID` on the output record
393
+ 3. Framework default — JSON
394
+
395
+ ---
396
+
397
+
398
+
399
+ ## Embed Syntax
400
+
401
+ Outputs are embedded inside **content records** — content is the primary medium through which outputs are composed into pages. Output templates are themselves content records. See `content.md` (pending).
402
+
403
+
404
+
405
+ ### Closed embed
406
+
407
+ ```css
408
+ <#_embed url="/api/output/[uid]"
409
+ name="my.output"
410
+ target="MyOutput"/>
411
+ ```
412
+
413
+
414
+
415
+ ### Open embed
416
+
417
+ An open embed passes inline section markup to override the output's configured template for this embed instance. The template is format-agnostic — sections can contain markup, CSV, JSON, plain text, or any structured output.
418
+
419
+ ```css
420
+ <#_embed.list url="/api/output/[uid]" name="myoutput">
421
+ <#_row>
422
+ #{value@Name},#{value@Email}
423
+ </#_row>
424
+ <#_noresult>
425
+ no results
426
+ </#_noresult>
427
+ </#_embed.list>
428
+ ```
429
+
430
+ Any alphanumeric suffix after the `.` is the **serialization id**. A serialization id is required on any open embed that contains another open embed; it must match on open and close tags and be unique at the same nesting level.
431
+
432
+ ### Embed attribute reference
433
+
434
+ | Attribute | Required | Default | Notes |
435
+ | :---------------- | :------- | :------ | :----------------------------------------------------------- |
436
+ | `url` | yes | — | `/api/output/[uid]` or `/api/o/[uid]` |
437
+ | `name` | no | — | Static name for an embed |
438
+ | `target` | no | — | Named context for `:cached` tokens and `#{payload$...}`; comma-separated for multiple |
439
+ | `secure` | no | true | false bypasses access control for this request |
440
+ | `execute` | no | true | false skips the call entirely |
441
+ | `hide` | no | false | true executes query and caches results but does render a payload |
442
+ | `payload` | no | true | Controls whether the embed's rendered output is cached for `#{payload$...}` access. When `hide=true`, defaults to false — set `payload=true` explicitly to cache a hidden embed's output |
443
+ | `catch_exception` | no | — | Codes caught silently; true catches all |
444
+
445
+
446
+
447
+ ###### #emb-att
448
+
449
+
450
+ ---
451
+
452
+
453
+
454
+ ## Template Tags
455
+
456
+ A template is a content record (`TemplateContentID`) containing section markup. Sections render in this order:
457
+
458
+ | Step | Tag | Renders when | Frequency |
459
+ | :--- | :----------------- | :---------------------------------------- | :-------------------------------------------------------------------------------------------------------------------------------- |
460
+ | 1 | `<#_shared>` | Rows exist + shared columns defined | Once, before header |
461
+ | 2 | `<#_header>` | Defined in template | Once |
462
+ | 3 | `<#_row>` | Rows exist | Once per row |
463
+ | — | `<#_rowdelimiter>` | Rows exist | Between rows (not after last) |
464
+ | 4 | `<#_summary>` | Rows exist + summary values defined | Once, after last row |
465
+ | 5 | `<#_footer>` | Defined in template | Once |
466
+ | — | `<#_noresult>` | Zero rows | Once |
467
+ | — | `<#_empty>` | Zero rows | Once (after noresult if both defined) |
468
+
469
+ > **Note**: `<#cell>...</#cell>` defines a per-column template block. Use this pattern when writing a cross-query template that must work across outputs without referencing specific column names. Place `<#cell/>` inside `<#_row>` or `<#_header>` where column output should be injected. Within `<#cell>`, `#{title}` and `#{value}` render a column's title and returned value in this scope.
470
+
471
+ ### Value tag
472
+
473
+ `<#_value.ShortName>` outputs a column value by ShortName. It supports an open form with attributes for conditional rendering:
474
+
475
+ | Attribute | Notes |
476
+ | :--------- | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
477
+ | `value` | Comparison value; `_null` matches null database values (not empty strings) |
478
+ | `modifier` | Comparison type, logic, and inversion — comma-separated for multiple. Comparison (default: exact, case-insensitive): `contains`, `startswith`, `endswith`, `lessthan`, `lessthanorequalto`, `greaterthan`, `greaterthanorequalto`. Logic: `and` (all `value` constraints must match; default is OR). Inversion: `exclude` (flips the result). |
479
+
480
+ String renders block conditionally
481
+
482
+ ```css
483
+ -- renders WHEN Status IS NOT NULL
484
+ <#_value.Status>
485
+ status set
486
+ </#_value.Status>
487
+
488
+ -- renders WHEN Status IS NOT NULL
489
+ <#_value.Status modifier="exclude" value="_null">
490
+ #{value@Status}
491
+ </#_value.Status>
492
+
493
+ -- renders WHEN Status IS NULL
494
+ <#_value.Status value="_null">
495
+ No status set
496
+ </#_value.Status>
497
+
498
+ -- renders WHEN Price < 100
499
+ <#_value.Price modifier="lessthan" value="100">
500
+ under $100
501
+ </#_value.Price>
502
+
503
+ -- renders WHEN ( Status = 'active' OR Status = 'verified' )
504
+ <#_value.Status value="active,verified">
505
+ active and verified
506
+ </#_value.Status>
507
+ ```
508
+
509
+ ###### #tem-tag
510
+
511
+ ---
512
+
513
+
514
+
515
+ ## Data Tokens
516
+
517
+ ### Token syntax
518
+
519
+ ```css
520
+ #{type@reference$target!default:modifier;comment}
521
+ ```
522
+
523
+ > **Note**: type attribute must come first; after that order does not matter.
524
+
525
+ | Part | Symbol | Notes |
526
+ | :-------- | :----- | :----------------------------------------------------------- |
527
+ | type | — | See **Token type reference** below. Common: `value`, `id`, `uid`, `output` `session`, `request` |
528
+ | reference | `@` | Column ShortName, UID, output property, or named key |
529
+ | target | `$` | Named embed context — required with `:cached` |
530
+ | default | `!` | Fallback value if token resolves empty |
531
+ | modifier | `:` | Transforms output or controls resolution timing |
532
+ | comment | `;` | Unrendered string |
533
+
534
+ ###### #tok-syn
535
+
536
+
537
+
538
+ ### Token type reference
539
+
540
+ | Type | Reference syntax | Resolved in | Notes |
541
+ | :-------------------------- | :------------------------------------------------------------ | :--------------- | :------------------------------------------------------------------------------------------------- |
542
+ | `value` (`data.value`) | `@ShortName`, `@UID` | Result-set pass | Field value for current row |
543
+ | `id` (`data.id`) | — | Result-set pass | Numeric PK of current row's base entity |
544
+ | `uid` (`data.uid`) | — | Result-set pass | UID of current row's base entity |
545
+ | `output.row` | `@index` | Result-set pass | Current row number; `:global` for cross-page value |
546
+ | `output.row` | `@count:returned` | Result-set pass | Count of returned rows |
547
+ | `output.row` | `@count:global` | Result-set pass | Total rows across all pages; requires `RowsPerPage` |
548
+ | `output.page` | `@index` | Result-set pass | Current page number; requires `RowsPerPage` |
549
+ | `output.page` | `@count` | Result-set pass | Total page count; requires `RowsPerPage` |
550
+ | `output.[ColumnName]` | — | Result-set pass | Any property of the output record — e.g. `#{output.Name}`, `#{output.AppID}` |
551
+ | `output_value.[ColumnName]` | — | Result-set pass | Any property of the output_value record; requires `<#cell>` context |
552
+ | `summary` | `@ShortName` | Result-set pass | Aggregate value for the column; aggregate function configured in `output_value.Summary` |
553
+ | `payload` | `$Target` | Template pass | Rendered output of named embed; resolved after all embeds execute |
554
+ | `session` | `@UserID`, `@Email`, `@FirstName`, `@LastName`, `@uid`, others | Template pass | Resolved in TemplateContentID content render — see **Token scope** below |
555
+ | `request` | `@ParamName` | Template pass | URL query parameter — see **Token scope** below |
556
+ | `input` | `@add1.id`, `@add2.id`, … | Input batch | ID of a record created in the same input batch |
557
+ | `unique` | — | Template pass | Generates a GUID |
558
+
559
+
560
+ ###### #tok-typ
561
+
562
+
563
+
564
+
565
+ ### Token modifiers
566
+
567
+ Modifiers refine how a token resolves or renders. Multiple modifiers may be combined with comma separation. Unless noted, all modifiers apply to all token types.
568
+
569
+ | Modifier | Effect | Applies to |
570
+ | :--------------- | :--------------------------------------------------------------------------------------------------------------------------------- | :---------------------------- |
571
+ | `:cached$Target` | Defers resolution until all embeds finish executing; `$Target` names the embed context | `value`, `request`, `session` |
572
+ | `:raw` | Defers output injection to the final render pass via placeholder — does not re-parse token syntax within field values | all |
573
+ | `:no_escape` | Bypasses all format-based escaping (HTML, JSON, XML, CSV) — use for trusted raw output | all |
574
+ | `:escape_json` | Forces JSON-safe escaping regardless of output format — use when embedding a value in a JSON string in non-JSON output | all |
575
+ | `:escape_html` | Applies HTML encoding; deprecated — HTML output format already encodes by default | all |
576
+ | `:url_encode` | URL percent-encodes the value via `Uri.EscapeDataString()` | all |
577
+ | `:url_decode` | URL-decodes the value via `Uri.UnescapeDataString()` | all |
578
+ | `:truncate` | Truncates to 100 characters, appending "..." | all |
579
+ | `:global` | Returns value across all pages; requires `RowsPerPage` | `output.row` |
580
+ | `:uid` | 22-character base-62 UID (default `unique` format) | `unique` |
581
+ | `:guid` | Standard GUID format | `unique` |
582
+ | `:integer` | Auto-incrementing integer per render; combined with `:request`, returns a consistent random 6-digit number for the request | `unique` |
583
+ | `:request` | Caches the generated value for the duration of the request — same value returned on repeated use | `unique` |
584
+ | `:{format}` | .NET format string applied via `.ToString(format)` — see **Modifier Syntax** below | `value`, `request`, `summary` |
585
+
586
+ ###### #tok-mod
587
+
588
+
589
+
590
+
591
+ ### Value output escaping
592
+
593
+ By default, token values are escaped based on the output format: HTML output applies `HttpUtility.HtmlEncode()`, JSON output applies `JsonConvert.ToString()`, XML applies `SecurityElement.Escape()`, and plain-text applies no escaping. This is independent of token type — all types use the same format-driven escaping rules.
594
+
595
+ Three modifiers override this behavior:
596
+
597
+ **`:no_escape`** — Suppresses all format-based escaping and returns the raw value string. Use for trusted content that must render verbatim — for example, HTML markup stored in a field, or pre-escaped content injected inline.
598
+
599
+ **`:raw`** — Defers output injection to a final render pass by substituting a placeholder during template rendering, then replacing it with the resolved value after all rendering completes. Useful when a token in an embed URL would otherwise be replaced prematurely. Does not re-parse token syntax within field values — a database field containing a string like `#{content.UID}` is treated as literal text, not resolved.
600
+
601
+ **`:escape_json`** — Forces JSON-safe escaping regardless of the current output format. Use when embedding a value inside a JSON string literal in an HTML or plain-text context.
602
+
603
+ ###### #val-esc
604
+
605
+
606
+
607
+
608
+ ### Modifier Syntax
609
+
610
+ Any .NET format string used as a modifier formats the resolved value before output:
611
+
612
+ ```css
613
+ #{value@CreatedOn:d} → short date (e.g. 1/3/2024)
614
+ #{value@CreatedOn:M} → month and day (e.g. January 3)
615
+ #{request@Price:f2} → 12.50
616
+ #{summary@Count:f0} → 42
617
+ #{value:c2} → $12.50
618
+ ```
619
+
620
+ ###### #mod-syn
621
+
622
+
623
+
624
+
625
+ ### Row data
626
+
627
+ | Token | Canonical form | Returns |
628
+ | :----------------------------- | :------------------------ | :------------------------------------------ |
629
+ | `#{value@ShortName}` | `#{data.value@ShortName}` | Field value for the current row |
630
+ | `#{value@ShortName!fallback}` | — | With default if empty |
631
+ | `#{value@ShortName:no_escape}` | — | Raw unescaped value |
632
+ | `#{id}` | `#{data.id}` | Numeric PK of the current row's base entity |
633
+ | `#{uid}` | `#{data.uid}` | UID of the current row's base entity |
634
+
635
+ The short forms (`#{value@ShortName}`, `#{id}`, `#{uid}`) are backwards-compatibility aliases for `#{data.value@ShortName}`, `#{data.id}`, `#{data.uid}`.
636
+
637
+
638
+
639
+ ### Output metadata
640
+
641
+ | Token | Returns |
642
+ | :----------------------------- | :------------------------------------------------------------------------------------------ |
643
+ | `#{output.row@index}` | Current row number (within returned rows) |
644
+ | `#{output.row@index:global}` | Current row number (across all pages) |
645
+ | `#{output.row@count:returned}` | Total returned row count |
646
+ | `#{output.row@count:global}` | Total row count across all pages |
647
+ | `#{output.page@index}` | Current page number |
648
+ | `#{output.page@count}` | Total page count |
649
+ | `#{output.PropertyName}` | Any property of the output record — e.g. `#{output.Name}`, `#{output.UID}`, `#{output.AppID}` |
650
+
651
+
652
+
653
+ ### Output value metadata (column context)
654
+
655
+ | Token | Returns |
656
+ | :----------------------------- | :-------------------------------------- |
657
+ | `#{output_value.Title}` | Column title (`output_value.Title`) |
658
+ | `#{output_value.ShortName}` | Column ShortName |
659
+ | `#{output_value.UID}` | Column UID |
660
+ | `#{output_value.PropertyName}` | Any property of the output_value record |
661
+
662
+
663
+
664
+ ### Token scope
665
+
666
+ Token resolution happens in two sequential passes:
667
+
668
+ **Content render pass** (first): The template renders with full token access — session, request, site, meta, unique, payload, and include tokens all resolve. Their values are baked into the template string before row iteration begins.
669
+
670
+ **Output row render pass** (second): Section blocks (`<#_row>`, `<#_header>`, etc.) are processed per-row. This pass resolves `value`, `id`, `uid`, `output.*`, `output_value.*`, and `summary` tokens only. Session, request, and similar tokens are not re-processed here — they were already resolved in the first pass.
671
+
672
+ The practical consequence: `#{session@UserID}` anywhere in the template (including inside `<#_row>`) resolves to the same value for every row, baked in before the row loop runs. This is expected behavior for session data. For per-row variation, use `#{value@...}` tokens backed by output values.
673
+
674
+ ###### #tok-sco
675
+
676
+
677
+
678
+ ### Summary (use in `<#_summary>` or `<#_footer>`)
679
+
680
+ The `summary` token renders an aggregate value for a column. The aggregate function — `sum`, `count`, `min`, `max`, or `average` — is configured in the `output_value.Summary` field on the database record. It cannot be specified or overridden in the template.
681
+
682
+ ```css
683
+ #{summary@ShortName}
684
+ ```
685
+
686
+ `ShortName` references the output_value's ShortName. The aggregate runs over all returned rows. Place in `<#_summary>` (renders once after the last row, only when summary values are defined) or `<#_footer>`.
687
+
688
+
689
+
690
+ ### Cached embed reads
691
+
692
+ `:cached` defers token resolution until after all embeds execute. `$Target` is required — omitting it causes ambiguous resolution when multiple embeds are active. A token will only resolve when one value is available in the cached record set.
693
+
694
+ ```css
695
+ <#_embed url="/api/output/[uid]" target="MyOutput"/>
696
+ #{value@SomeField:cached$MyOutput}
697
+ ```
698
+
699
+
700
+
701
+ ### Payload tokens
702
+
703
+ `#{payload$Target}` accesses the rendered output of a named embed after execution. The embed must have a matching `target="..."` attribute.
704
+
705
+ | Token | Returns |
706
+ | :------------------ | :----------------------------------------------------------------------------------------- |
707
+ | `#{payload$Target}` | Full rendered output of the embed (HTML, JSON, or whatever the template produces) |
708
+
709
+ Payload tokens resolve in the same deferred pass as `:cached` value tokens — after all embeds have executed.
710
+
711
+ ```css
712
+ <#_embed url="/api/output/[uid]" target="UserData" hide="true" payload="true"/>
713
+
714
+ #{payload$UserData} → rendered output of the embed
715
+ ```
716
+
717
+ `hide="true"` is commonly paired with payload reads — the embed executes and its rendered output is available via `#{payload$Target}` without rendering at embed site. Useful if you want to pass an embed's payload as a filter value in another embed's URL, for example.
718
+
719
+ ###### #pay-tok
720
+
721
+ ---
722
+
723
+
724
+
725
+ ## Filters
726
+
727
+ ### Static filter
728
+
729
+ Set `PrimaryValue` on an `output_value_filter` record. Always applied — no URL parameter needed.
730
+
731
+
732
+
733
+ ### Prompted filter modifiers
734
+
735
+ Set `Prompted = 1` and `ShortName` on the filter record to enable URL parameter at request time:
736
+
737
+ ```css
738
+ ?_filter@ShortName=value
739
+ ```
740
+
741
+ Multiple values: `?_filter@Status=active,pending` — combined with `OR` (default) or `AND` per `Operator` field.
742
+
743
+ Combine with `Required = 1` on the filter record to throw a 500 error if no value is supplied.
744
+
745
+
746
+
747
+ ### Request stack and filter priority
748
+
749
+ When an output is embedded, its caller can pass filter parameters down to it. The **request stack** tracks these values across embed levels — an embed may pass filters to its embedded outputs, and those outputs may be embedded in turn.
750
+
751
+ The `:restrictive` and `:overridable` modifiers control how a filter value behaves within this stack:
752
+
753
+ | Modifier | Behavior |
754
+ | :------------- | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
755
+ | `:restrictive` | The filter value is a hard requirement. It cannot be replaced or removed by any other layer of the stack. Use when a security constraint or scoping rule must be enforced regardless of caller context. |
756
+ | `:overridable` | The filter value is a default. A higher layer of the request stack can replace it. Use when the output provides a sensible default that callers may legitimately need to change. |
757
+
758
+ When neither modifier is present, the default behavior is equivalent to `:overridable` — the value is applied unless overridden.
759
+
760
+ ```css
761
+ ?_filter@ShortName:restrictive=value
762
+ ?_filter@ShortName:overridable=value
763
+ ```
764
+
765
+ These modifiers are only meaningful when the output is invoked through an embed chain. For direct HTTP requests, there is no outer stack and the distinction has no effect.
766
+
767
+ ###### #req-sta
768
+
769
+
770
+
771
+ ### Ad-hoc filter modifiers
772
+
773
+ When `AdhocFilters = 1` on the output, filter type and behavior can be set inline in the URL key without pre-configured filter records. Modifiers are comma-separated after the colon; multiple values are comma-separated after `=`.
774
+
775
+ ```css
776
+ ?_filter@Field:mod1,mod2=value1,value2
777
+ ```
778
+
779
+ **Comparison modifiers**
780
+
781
+ | Modifier | SQL Equivalent |
782
+ | :---------------------- | :---------------------------- |
783
+ | `:exact` | `= '[FilterValue]'` (default) |
784
+ | `:contains` | `LIKE '%[FilterValue]%'` |
785
+ | `:startswith` | `LIKE '[FilterValue]%'` |
786
+ | `:endswith` | `LIKE '%[FilterValue]'` |
787
+ | `:lessthan` | `< [FilterValue]` |
788
+ | `:lessthanorequalto` | `<= [FilterValue]` |
789
+ | `:greaterthan` | `> [FilterValue]` |
790
+ | `:greaterthanorequalto` | `>= [FilterValue]` |
791
+
792
+ **Inclusion modifier** (maps to `Inclusive`):
793
+
794
+ | Modifier | Effect |
795
+ | :--------- | :---------------------------------------------- |
796
+ | `:exclude` | Excludes matching rows. The default is include. |
797
+
798
+ **Operator modifier** (maps to `Operator`, applies when multiple values supplied):
799
+
800
+ | Modifier | Effect |
801
+ | :------- | :--------------------------- |
802
+ | `:and` | All values must match |
803
+ | `:or` | Any value matches (default) |
804
+
805
+ Modifiers can be combined in any order:
806
+
807
+ ```css
808
+ ?_filter@Price:lessthan=100
809
+ ?_filter@Tags:contains,and=admin,active → Tags LIKE '%admin%' AND Tags LIKE '%active%'
810
+ ```
811
+
812
+
813
+
814
+ #### Multi-value behavior
815
+
816
+ Multiple values are comma-separated after `=`. The `:or` / `:and` modifier controls how they combine (default: OR).
817
+
818
+ ```css
819
+ ?_filter@Status=active,pending → Status = 'active' OR Status = 'pending'
820
+ ?_filter@Tags:contains,and=admin,active → Tags LIKE '%admin%' AND Tags LIKE '%active%'
821
+ ```
822
+
823
+ **Caution**: with `:exclude` and multiple values— `:exclude` with default OR combines as `NOT x OR NOT y`, since a single field cannot simultaneously equal two different values, this condition is always true — no rows are excluded at all. To exclude rows matching *any* value, use `:and,exclude`.
824
+
825
+ ```css
826
+ ?_filter@FirstName:contains,exclude=foo,bar → NOT LIKE '%foo%' OR NOT LIKE '%bar%' (no filtering)
827
+ ?_filter@FirstName:and,contains,exclude=foo,bar → NOT LIKE '%foo%' AND NOT LIKE '%bar%' (excludes rows containing either)
828
+ ```
829
+
830
+ ###### #mul-val
831
+
832
+
833
+
834
+ ### Cross-column filter logic
835
+
836
+ Each `output_value_filter` record generates one WHERE clause condition. Multiple records are ANDed together at the clause level — there is no parenthetical grouping and no way to express OR across separate filter records via the filter system alone. The `Operator` field (`AND`/`OR`) applies only within a single multi-value prompted filter.
837
+
838
+ To express cross-column OR, conditional grouping, or comparisons between columns, use a `CASE` command in `output_value.CustomSQL` and add a filter to that column.
839
+
840
+ ```sql
841
+ -- output_value.CustomSQL example CASE:
842
+ CASE
843
+ WHEN #{value@Amount} > #{value@Threshold}
844
+ THEN 1
845
+ ELSE 0
846
+ END
847
+ ```
848
+
849
+ > **Note**: this is an advanced pattern. For construction steps, schema details, and worked examples see the `building-guide.md`.
850
+
851
+
852
+
853
+ ### Filter presets
854
+
855
+ These preset values can be stored in `output_value_filter.PrimaryValue` or set as `_filter` parameter value.
856
+
857
+ | Preset | Resolves to |
858
+ | :----------------------------------------------------------- | :----------------------------------------------------------- |
859
+ | `CurrentUserID` | Current user's ID (unchanged under impersonation) |
860
+ | `LoggedInUserID` | Logged-in user's ID (may differ from logged-in user under impersonation) |
861
+ | `Now` | Current datetime |
862
+ | `Today`, `ThisWeek`, `ThisMonth`, `ThisYear`, `Yesterday`, `LastWeek`, `LastMonth`, `LastQuarter`, `LastYear` | Date presets generate SQL date ranges (`BETWEEN start AND end`) |
863
+ | `IpAddress` | Requester's IPV4 address |
864
+ | `SessionUID` | Current session UID |
865
+ | `CurrentSiteID` | Current site associated to content in request |
866
+ | `_security@view` | Row IDs the user has View permission for |
867
+ | `_security@add` | Row IDs the user has Add permission for |
868
+ | `_security@edit` | Row IDs the user has Edit permission for |
869
+ | `_security@delete` | Row IDs the user has Delete permission for |
870
+ | `_security@associate` | Row IDs the user has Associate permission for |
871
+ | `_security@execute` | Row IDs the user has Execute permission for |
872
+ | `_security@impersonate` | Row IDs the user has Impersonate permission for |
873
+
874
+ ###### #fil-pre
875
+
876
+ ## Pagination
877
+
878
+ Set `RowsPerPage` on the output record or via `_rowsperpage` parameter to control how many rows are visible per page. Pagination is activated by passing `?_page=N` at request time.
879
+
880
+
881
+
882
+ ---
883
+
884
+
885
+
886
+ ## Caching
887
+
888
+ `Cache = 1` caches the result set. Output structure (query config, joins, values) is cached automatically.
889
+
890
+ Output metadata is cached under two separate keys. `output:meta:[uid]` is populated during normal rendering. `secure:output:meta:[uid]` is populated when the DBO security system internally evaluates the output for row-level access control — external parameters are blocked in that mode to prevent request values from influencing security decisions. Both entries are independent and must be cleared explicitly:
891
+
892
+ ```css
893
+ GET /api/cache/refresh?_key=output:meta:[output.UID]
894
+ GET /api/cache/refresh?_key=secure:output:meta:[output.UID]
895
+ ```
896
+
897
+ `voidcache` is accepted on any API endpoint to flush all caches globally.
898
+
899
+ ```css
900
+ GET /api/output/[uid]?voidcache=true
901
+ ```
902
+
903
+ ---
904
+
905
+
906
+
907
+ ## Legacy
908
+
909
+ ### User-defined sections
910
+
911
+ `<#SectionName>` defines a named block that calling templates can target by name OR if `<#SectionName>` maps to a output_value.Shortname, will render if that row has a value :
912
+
913
+ ```css
914
+ <#TagName>
915
+ Some string which can comprise #{value@Shortname1}
916
+ </#TagName>
917
+ <#_row>
918
+ <#TagName/>
919
+ <#ShortName2>
920
+ #{value@ShortName2} has a value
921
+ </#ShortName2>
922
+ </#_row>
923
+ ```
924
+
925
+
926
+
927
+
928
+
929
+ ### Legacy token types
930
+
931
+ | Token | Current form | Notes |
932
+ | :----------------- | :----------------------------- | :------------------------------------------------------------------------------------------------------------------ |
933
+ | `#{entity_column}` | `#{data.entity_column.uid}` | Renders the EntityColumn UID for the current column in context; source-confirmed but behavior uncertain — do not use in new templates |
934
+ | `#{row}` | `#{output.row@index}` | Current row number; use explicit form |
935
+ | `#{page}` | `#{output.page@index}` | Current page number; use explicit form |
936
+ | `#{count}` | `#{output.row@count:returned}` | Returned row count; use explicit form |
937
+ | `#{column}` | `#{data.value@ShortName}` | Field value by column reference; use `#{value@ShortName}` |
938
+ | `#{title}` | `#{output_value.Title}` | Column title in `<#cell>` context; use explicit form |
939
+
940
+ ### Legacy columns
941
+
942
+ | Column | Table | Notes |
943
+ | :----------- | :------------- | :--------------------------------------------------------------------------------- |
944
+ | `MaxRows` | `output` | Superseded by `Limit`; both are read for backward compatibility |
945
+ | `DatePart` | `output_value` | Date component extraction; exists in schema, not actively supported |
946
+ | `OutputType` | `output_value` | Controls EntityColumn vs CustomSQL expression mode; exists in schema, not actively supported |
947
+
948
+ ### Legacy URL parameters
949
+
950
+ | Parameter | Superseded by | Notes |
951
+ | :----------- | :------------ | :----------------------------------------------- |
952
+ | `_maxrows=N` | `_limit=N` | Exact equivalent — use `_limit` |
953
+ | `_rows=M,N` | `_limit=M-N` | Row range using hyphen separator in current form |
954
+ | `_template=json` | `_template=json_indented` or `_template=json_raw` | Routes through the DBO content rendering pipeline — values serialized as strings, duplicate `ShortName` values produce duplicate keys, slower than direct serialization. Prefer `json_indented` or `json_raw`. |
955
+ | `_escape_html=false` | `:no_escape` modifier | Disabled HTML encoding globally for the request. HTML output only. Use `:no_escape` on specific tokens instead. |
956
+
957
+ ### Legacy embed syntax
958
+
959
+ `<EmbedToken>` and `<#embed>` were the predecessors to `<#_embed>`.
960
+
961
+ ```css
962
+ <!-- Legacy -->
963
+ <EmbedToken url="/api/output/[uid]" name="my.output" />
964
+
965
+ <!-- Legacy -->
966
+ <#embed url="/api/output/[uid]" name="my.output"/>
967
+ ```