@docyrus/docyrus 0.0.15 → 0.0.17

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 (41) hide show
  1. package/README.md +25 -2
  2. package/main.js +1028 -540
  3. package/main.js.map +4 -4
  4. package/package.json +2 -1
  5. package/resources/pi-agent/prompts/agent-system.md +25 -0
  6. package/resources/pi-agent/prompts/coder-append-system.md +19 -0
  7. package/resources/pi-agent/skills/docyrus-ai/SKILL.md +28 -0
  8. package/resources/pi-agent/skills/docyrus-api-dev/SKILL.md +161 -0
  9. package/resources/pi-agent/skills/docyrus-api-dev/references/api-client.md +349 -0
  10. package/resources/pi-agent/skills/docyrus-api-dev/references/authentication.md +238 -0
  11. package/resources/pi-agent/skills/docyrus-api-dev/references/data-source-query-guide.md +2059 -0
  12. package/resources/pi-agent/skills/docyrus-api-dev/references/formula-design-guide-llm.md +320 -0
  13. package/resources/pi-agent/skills/docyrus-api-dev/references/query-and-formulas.md +592 -0
  14. package/resources/pi-agent/skills/docyrus-api-doctor/SKILL.md +70 -0
  15. package/resources/pi-agent/skills/docyrus-api-doctor/references/checklist-details.md +588 -0
  16. package/resources/pi-agent/skills/docyrus-app-dev/SKILL.md +159 -0
  17. package/resources/pi-agent/skills/docyrus-app-dev/references/api-client-and-auth.md +275 -0
  18. package/resources/pi-agent/skills/docyrus-app-dev/references/collections-and-patterns.md +352 -0
  19. package/resources/pi-agent/skills/docyrus-app-dev/references/data-source-query-guide.md +2059 -0
  20. package/resources/pi-agent/skills/docyrus-app-dev/references/formula-design-guide-llm.md +320 -0
  21. package/resources/pi-agent/skills/docyrus-app-dev/references/query-guide.md +525 -0
  22. package/resources/pi-agent/skills/docyrus-app-ui-design/SKILL.md +466 -0
  23. package/resources/pi-agent/skills/docyrus-app-ui-design/references/component-selection-guide.md +602 -0
  24. package/resources/pi-agent/skills/docyrus-app-ui-design/references/icon-usage-guide.md +463 -0
  25. package/resources/pi-agent/skills/docyrus-app-ui-design/references/preferred-components-catalog.md +242 -0
  26. package/resources/pi-agent/skills/docyrus-apps/SKILL.md +54 -0
  27. package/resources/pi-agent/skills/docyrus-architect/SKILL.md +174 -0
  28. package/resources/pi-agent/skills/docyrus-architect/references/custom-query-guide.md +410 -0
  29. package/resources/pi-agent/skills/docyrus-architect/references/data-source-query-guide.md +2059 -0
  30. package/resources/pi-agent/skills/docyrus-architect/references/formula-design-guide-llm.md +320 -0
  31. package/resources/pi-agent/skills/docyrus-architect/references/formula-reference.md +145 -0
  32. package/resources/pi-agent/skills/docyrus-auth/SKILL.md +100 -0
  33. package/resources/pi-agent/skills/docyrus-cli-app/SKILL.md +279 -0
  34. package/resources/pi-agent/skills/docyrus-cli-app/references/cli-manifest.md +532 -0
  35. package/resources/pi-agent/skills/docyrus-cli-app/references/list-query-examples.md +248 -0
  36. package/resources/pi-agent/skills/docyrus-curl/SKILL.md +32 -0
  37. package/resources/pi-agent/skills/docyrus-discover/SKILL.md +63 -0
  38. package/resources/pi-agent/skills/docyrus-ds/SKILL.md +95 -0
  39. package/resources/pi-agent/skills/docyrus-env/SKILL.md +21 -0
  40. package/resources/pi-agent/skills/docyrus-studio/SKILL.md +369 -0
  41. package/resources/pi-agent/skills/docyrus-tui/SKILL.md +15 -0
@@ -0,0 +1,592 @@
1
+ # Docyrus Query & Formula Guide
2
+
3
+ ## Table of Contents
4
+
5
+ 1. [Query Payload Structure](#query-payload-structure)
6
+ 2. [Columns](#columns)
7
+ 3. [Filters](#filters)
8
+ 4. [Filter Operators Reference](#filter-operators-reference)
9
+ 5. [Order By](#order-by)
10
+ 6. [Pagination](#pagination)
11
+ 7. [Calculations (Aggregations)](#calculations)
12
+ 8. [Formulas — Simple](#formulas--simple)
13
+ 9. [Formulas — Block (AST)](#formulas--block-ast)
14
+ 10. [Formulas — Subquery](#formulas--subquery)
15
+ 11. [Block Kinds Reference](#block-kinds-reference)
16
+ 12. [Child Queries](#child-queries)
17
+ 13. [Pivot](#pivot)
18
+ 14. [Expand](#expand)
19
+ 15. [Allowed Functions](#allowed-functions)
20
+ 16. [Allowed Cast Types](#allowed-cast-types)
21
+ 17. [Complete Examples](#complete-examples)
22
+
23
+ ---
24
+
25
+ ## Query Payload Structure
26
+
27
+ Data source items are fetched via GET with a query payload:
28
+
29
+ ```
30
+ GET /v1/apps/{appSlug}/data-sources/{slug}/items
31
+ ```
32
+
33
+ The payload (`ZodSelectQueryPayload`) supports:
34
+
35
+ ```typescript
36
+ interface ISelectQueryParams {
37
+ columns?: string | null // comma-separated column names
38
+ filters?: IQueryFilterGroup | null // nested AND/OR filter groups
39
+ filterKeyword?: string | null // full-text search
40
+ orderBy?: string | object | object[] // sorting
41
+ limit?: number // default: 100
42
+ offset?: number // default: 0
43
+ fullCount?: boolean // return total count
44
+ calculations?: ICalculationRule[] | null // aggregations
45
+ formulas?: Record<string, IFormula> | null // computed virtual columns
46
+ childQueries?: Record<string, IChildQuery> | null
47
+ pivot?: { matrix, hideEmptyRows?, orderBy?, limit? } | null
48
+ expand?: string[] | null // expand relation/user/enum fields
49
+ expandTypes?: ('user' | 'enum' | 'relation')[] | null
50
+ distinctColumns?: string[] | null
51
+ queryMode?: 'OLTP' | 'OLAP' | 'EXPORT'
52
+ recordId?: string | null // fetch single record by ID
53
+ }
54
+ ```
55
+
56
+ **Critical**: Always send `columns`. Without it, only `id` is returned.
57
+
58
+ ---
59
+
60
+ ## Columns
61
+
62
+ **Type**: comma-separated string (server) or array (client interceptor converts).
63
+
64
+ ### Basic Selection
65
+ ```
66
+ "columns": "task_name, created_on, record_owner"
67
+ ```
68
+
69
+ ### Aliasing with `:`
70
+ ```
71
+ "columns": "ra:related_account"
72
+ → { "ra": { "id": "uuid", "name": "account name" } }
73
+ ```
74
+
75
+ ### Relation Expansion with `()`
76
+ ```
77
+ "columns": "task_name, related_account(name:account_name, phone:account_phone)"
78
+ → { "task_name": "...", "related_account": { "name": "...", "phone": "..." } }
79
+ ```
80
+
81
+ ### Spread with `...` (flatten to root)
82
+ ```
83
+ "columns": "task_name, ...related_account(account_name, phone:account_phone)"
84
+ → { "task_name": "...", "account_name": "...", "phone": "..." }
85
+ ```
86
+
87
+ ### Functions with `@`
88
+ ```
89
+ "columns": "...related_account(an:account_name@upper)"
90
+ → { "an": "ACCOUNT NAME" }
91
+ ```
92
+
93
+ ### Date Grouping Formulas
94
+
95
+ | Formula | Description |
96
+ |---------|-------------|
97
+ | `hours_of_today@field` | Group by hour for today |
98
+ | `days_of_week@field` | Group by day for current week |
99
+ | `days_of_month@field` | Group by day for current month |
100
+ | `weeks_of_month@field` | Group by week for current month |
101
+ | `months_of_year@field` | Group by month (YYYY-MM) |
102
+ | `quarters_of_year@field` | Group by quarter (YYYY-Q) |
103
+
104
+ ### to_char Formatting
105
+ ```
106
+ "columns": "day:to_char[DD/MM/YYYY]@created_on"
107
+ ```
108
+
109
+ ---
110
+
111
+ ## Filters
112
+
113
+ Recursive group structure:
114
+
115
+ ```typescript
116
+ interface IQueryFilterGroup {
117
+ rules: (IQueryFilterRule | IQueryFilterGroup)[]
118
+ combinator?: 'and' | 'or' // default: 'and'
119
+ not?: boolean // negate entire group
120
+ }
121
+
122
+ interface IQueryFilterRule {
123
+ field?: string
124
+ operator: string
125
+ value?: any
126
+ filterType?: string | null // NUMERIC, ALPHA, BOOL, DATE, DATETIME, RELATION, OWNER, etc.
127
+ }
128
+ ```
129
+
130
+ ### Basic AND
131
+ ```json
132
+ {
133
+ "filters": {
134
+ "combinator": "and",
135
+ "rules": [
136
+ { "field": "task_status", "operator": "=", "value": 1 },
137
+ { "field": "priority", "operator": ">=", "value": 3 }
138
+ ]
139
+ }
140
+ }
141
+ ```
142
+
143
+ ### Nested AND + OR
144
+ ```json
145
+ {
146
+ "filters": {
147
+ "combinator": "and",
148
+ "rules": [
149
+ { "field": "created_on", "operator": "between", "value": ["2025-10-01", "2025-11-01"] },
150
+ {
151
+ "combinator": "or",
152
+ "rules": [
153
+ { "field": "email", "operator": "empty" },
154
+ { "field": "phone", "operator": "not empty" }
155
+ ]
156
+ }
157
+ ]
158
+ }
159
+ }
160
+ ```
161
+
162
+ ### Filter by Related Record's Field
163
+ ```json
164
+ { "field": "rel_client/account_status", "operator": "=", "value": 2 }
165
+ ```
166
+
167
+ ### Negated Group
168
+ ```json
169
+ { "combinator": "and", "not": true, "rules": [{ "field": "status", "operator": "=", "value": "archived" }] }
170
+ ```
171
+
172
+ ---
173
+
174
+ ## Filter Operators Reference
175
+
176
+ ### Comparison
177
+ `=`, `!=`, `<>`, `>`, `<`, `>=`, `<=`, `between` (value: `[min, max]`)
178
+
179
+ ### Text
180
+ `like` (with `%` wildcards), `not like`, `starts with`, `ends with`
181
+
182
+ ### Collection
183
+ `in` (array), `not in` (array), `exists`, `contains any` (array), `contains all` (array), `not contains`
184
+
185
+ ### Null/Empty
186
+ `is`, `is not`, `empty`, `not empty`, `null`, `not null`
187
+
188
+ ### Boolean
189
+ `true`, `false`
190
+
191
+ ### User-Related (no value)
192
+ `active_user`, `not_active_user`, `in_active_user_scope`, `not_in_active_user_scope`, `in_role`, `not_in_role`, `in_team`, `not_in_team`, `in_active_user_team`, `not_in_active_user_team`, `in_unit`, `not_in_unit`, `in_sub_unit`, `not_in_sub_unit`, `shared_to_me`, `contains_active_user`, `not_contains_active_user`, `contains_member_of_active_user_team`
193
+
194
+ ### Date Shortcuts (no value)
195
+ `today`, `tomorrow`, `yesterday`, `last_7_days`, `last_15_days`, `last_30_days`, `last_60_days`, `last_90_days`, `last_120_days`, `next_7_days`, `next_15_days`, `next_30_days`, `next_60_days`, `next_90_days`, `next_120_days`, `last_week`, `this_week`, `next_week`, `last_month`, `this_month`, `next_month`, `before_today`, `after_today`, `last_year`, `this_year`, `next_year`, `first_quarter`, `second_quarter`, `third_quarter`, `fourth_quarter`, `last_3_months`, `last_6_months`
196
+
197
+ ### Dynamic Date (value = number)
198
+ `x_days_ago`, `x_days_later`, `before_last_x_days`, `in_last_x_days`, `after_last_x_days`, `in_next_x_days`
199
+
200
+ ---
201
+
202
+ ## Order By
203
+
204
+ ```json
205
+ // String
206
+ { "orderBy": "created_on DESC" }
207
+
208
+ // Multi-column string
209
+ { "orderBy": "firstname ASC, lastname DESC" }
210
+
211
+ // Object
212
+ { "orderBy": { "field": "created_on", "direction": "desc" } }
213
+
214
+ // Array
215
+ { "orderBy": [{ "field": "status", "direction": "asc" }, { "field": "created_on", "direction": "desc" }] }
216
+
217
+ // Related field
218
+ { "orderBy": "relation_field(field_name DESC), id ASC" }
219
+ ```
220
+
221
+ ---
222
+
223
+ ## Pagination
224
+
225
+ ```json
226
+ { "limit": 25, "offset": 50, "fullCount": true }
227
+ ```
228
+ Returns records 51–75. `fullCount: true` adds total count to response.
229
+
230
+ ---
231
+
232
+ ## Calculations
233
+
234
+ Aggregations. Selected `columns` become GROUP BY fields.
235
+
236
+ ```typescript
237
+ interface ICalculationRule {
238
+ func: 'count' | 'sum' | 'avg' | 'min' | 'max' | 'jsonb_agg' | 'json_agg' | 'array_agg'
239
+ field: string // 'id' for count, actual field for sum/avg/etc.
240
+ name?: string // result column alias
241
+ isDistinct?: boolean
242
+ minValue?: number // only aggregate values > this
243
+ maxValue?: number // only aggregate values < this
244
+ numberType?: 'bigint' | 'int' | 'decimal'
245
+ }
246
+ ```
247
+
248
+ ### Count per Group
249
+ ```json
250
+ {
251
+ "columns": "record_owner(name)",
252
+ "calculations": [{ "field": "id", "func": "count", "name": "task_count" }],
253
+ "filters": { "rules": [{ "field": "task_status", "operator": "=", "value": 1 }] }
254
+ }
255
+ ```
256
+
257
+ ### Multiple Aggregations
258
+ ```json
259
+ {
260
+ "columns": "category",
261
+ "calculations": [
262
+ { "field": "id", "func": "count", "name": "total" },
263
+ { "field": "amount", "func": "sum", "name": "totalAmount" },
264
+ { "field": "amount", "func": "avg", "name": "avgAmount" },
265
+ { "field": "amount", "func": "min", "name": "minAmount" },
266
+ { "field": "amount", "func": "max", "name": "maxAmount" }
267
+ ]
268
+ }
269
+ ```
270
+
271
+ ---
272
+
273
+ ## Formulas — Simple
274
+
275
+ Single-depth function call. Column refs use `{column_name}` syntax.
276
+
277
+ ```json
278
+ {
279
+ "columns": "id, name, formatted_date, full_name",
280
+ "formulas": {
281
+ "formatted_date": { "func": "to_char", "args": ["{created_on}", "DD/MM/YYYY"] },
282
+ "full_name": { "func": "concat", "args": ["{first_name}", " ", "{last_name}"] },
283
+ "upper_name": { "func": "upper", "args": ["{name}"] }
284
+ }
285
+ }
286
+ ```
287
+
288
+ **Rule**: Formula key must appear in `columns`.
289
+
290
+ ---
291
+
292
+ ## Formulas — Block (AST)
293
+
294
+ Composable expression tree. One root block in `inputs` array.
295
+
296
+ ```json
297
+ {
298
+ "columns": "id, name, my_formula",
299
+ "formulas": {
300
+ "my_formula": {
301
+ "inputs": [{ "kind": "...", ... }]
302
+ }
303
+ }
304
+ }
305
+ ```
306
+
307
+ ### Math Example: `quantity * unit_price`
308
+ ```json
309
+ {
310
+ "formulas": {
311
+ "line_total": {
312
+ "inputs": [{
313
+ "kind": "math", "op": "*",
314
+ "inputs": [
315
+ { "kind": "column", "name": "quantity" },
316
+ { "kind": "column", "name": "unit_price" }
317
+ ]
318
+ }]
319
+ }
320
+ }
321
+ }
322
+ ```
323
+
324
+ ### CASE Example: Categorize by Amount
325
+ ```json
326
+ {
327
+ "formulas": {
328
+ "tier": {
329
+ "inputs": [{
330
+ "kind": "case",
331
+ "cases": [
332
+ { "when": { "kind": "compare", "op": ">=", "left": { "kind": "column", "name": "amount" }, "right": { "kind": "literal", "literal": 100000 } }, "then": { "kind": "literal", "literal": "Enterprise" } },
333
+ { "when": { "kind": "compare", "op": ">=", "left": { "kind": "column", "name": "amount" }, "right": { "kind": "literal", "literal": 25000 } }, "then": { "kind": "literal", "literal": "Mid-Market" } }
334
+ ],
335
+ "else": { "kind": "literal", "literal": "SMB" }
336
+ }]
337
+ }
338
+ }
339
+ }
340
+ ```
341
+
342
+ ---
343
+
344
+ ## Formulas — Subquery
345
+
346
+ Correlated subquery against a child data source.
347
+
348
+ ```json
349
+ {
350
+ "columns": "id, name, active_deals",
351
+ "formulas": {
352
+ "active_deals": {
353
+ "from": "crm_deal",
354
+ "with": "account",
355
+ "filters": { "rules": [{ "field": "stage", "operator": "!=", "value": "lost" }] },
356
+ "inputs": [{ "kind": "aggregate", "name": "count", "inputs": [] }]
357
+ }
358
+ }
359
+ }
360
+ ```
361
+
362
+ - `from`: child table slug in `appSlug_tableSlug` format
363
+ - `with`: child field referencing parent `id` (string) or multi-field join (object)
364
+ - `filters`: optional WHERE on child table
365
+
366
+ ### Multi-Field Join
367
+ ```json
368
+ { "with": { "child_field1": "parent_field1", "child_field2": "parent_field2" } }
369
+ ```
370
+
371
+ ### Compatibility Wrapper
372
+ Can also use `expression` key:
373
+ ```json
374
+ { "formulas": { "count": { "expression": { "from": "...", "with": "...", "inputs": [...] } } } }
375
+ ```
376
+
377
+ ---
378
+
379
+ ## Block Kinds Reference
380
+
381
+ Every block has `kind` discriminator. Optional: `tz` (timezone), `cast` (type cast).
382
+
383
+ | Kind | Purpose | Key Props |
384
+ |------|---------|-----------|
385
+ | `literal` | Static value | `literal: string \| number \| boolean \| null \| array` |
386
+ | `column` | Table column ref | `name: string \| string[]` |
387
+ | `builtin` | SQL constant | `name: 'current_date' \| 'current_time' \| 'current_timestamp' \| 'now'` |
388
+ | `function` | Whitelisted SQL fn | `name: string, inputs?: block[]` |
389
+ | `math` | Arithmetic | `op: '+' \| '-' \| '*' \| '/' \| '%', inputs: block[]` (min 2) |
390
+ | `compare` | Comparison | `op: '=' \| '!=' \| '>' \| '<' \| '>=' \| '<=' \| 'like' \| 'ilike' \| 'in' \| 'not in', left: block, right: block` |
391
+ | `boolean` | Logical | `op: 'and' \| 'or' \| 'not', inputs: block[]` |
392
+ | `case` | Conditional | `cases: [{ when: block, then: block }], else?: block` |
393
+ | `aggregate` | Aggregate fn | `name: 'count' \| 'sum' \| 'avg' \| 'min' \| 'max' \| ..., distinct?: boolean, inputs: block[]` |
394
+ | `extract` | Date part | `part: 'year' \| 'month' \| 'day' \| 'hour' \| 'minute' \| 'second', inputs: [block]` |
395
+
396
+ ### Type Casting
397
+ ```json
398
+ { "kind": "column", "name": "price", "cast": "decimal" }
399
+ ```
400
+ → SQL: `("t0"."price")::decimal`
401
+
402
+ ### Timezone
403
+ ```json
404
+ { "kind": "function", "name": "now", "tz": "UTC" }
405
+ ```
406
+ → SQL: `now() at time zone 'UTC'`
407
+
408
+ ---
409
+
410
+ ## Child Queries
411
+
412
+ Fetch child records as nested JSON arrays per parent row.
413
+
414
+ ```json
415
+ {
416
+ "columns": "id, name, recent_orders",
417
+ "childQueries": {
418
+ "recent_orders": {
419
+ "from": "shop_order_item",
420
+ "using": "product",
421
+ "columns": "order_date, quantity, total_price",
422
+ "orderBy": "order_date DESC",
423
+ "limit": 5,
424
+ "filters": { "rules": [{ "field": "order_date", "operator": "last_30_days" }] }
425
+ }
426
+ }
427
+ }
428
+ ```
429
+
430
+ **Rules:**
431
+ - Key (e.g. `recent_orders`) must appear in parent `columns`
432
+ - `from`: `appSlug_slug` format
433
+ - `using`: field in **child** DS referencing parent's `id`
434
+ - Supports full query params: `columns`, `filters`, `calculations`, `orderBy`, `limit`
435
+
436
+ ---
437
+
438
+ ## Pivot
439
+
440
+ Cross-tab matrix queries with date range series.
441
+
442
+ ```json
443
+ {
444
+ "columns": "...order_status(status:name)",
445
+ "pivot": {
446
+ "matrix": [
447
+ {
448
+ "using": "created_on",
449
+ "columns": "day:to_char[DD/MM/YYYY]@created_on",
450
+ "dateRange": { "interval": "day", "min": "2025-09-01T00:00:00Z", "max": "2025-09-30T23:59:59Z" },
451
+ "spread": true
452
+ },
453
+ {
454
+ "using": "record_owner",
455
+ "columns": "userName:name",
456
+ "spread": true
457
+ }
458
+ ],
459
+ "hideEmptyRows": false,
460
+ "orderBy": "day ASC",
461
+ "limit": 1000
462
+ },
463
+ "calculations": [
464
+ { "field": "id", "func": "count", "name": "total" },
465
+ { "field": "amount", "func": "sum", "name": "totalSold" }
466
+ ]
467
+ }
468
+ ```
469
+
470
+ Date range intervals: `day`, `week`, `month`, `year`, `hour`, `minute`, `second`
471
+
472
+ ---
473
+
474
+ ## Expand
475
+
476
+ Return full objects for relation/user/enum fields:
477
+
478
+ ```json
479
+ { "expand": ["record_owner", "related_account", "status"] }
480
+ ```
481
+
482
+ ---
483
+
484
+ ## Allowed Functions
485
+
486
+ **String**: `length`, `lower`, `upper`, `substr`, `replace`, `concat`, `trim`, `ltrim`, `rtrim`, `btrim`, `split_part`, `initcap`, `reverse`, `strpos`, `lpad`, `rpad`
487
+
488
+ **Number**: `abs`, `ceil`, `floor`, `round`, `sqrt`, `power`, `mod`, `greatest`, `least`, `trunc`, `gcd`, `lcm`, `exp`, `ln`, `log`, `sign`
489
+
490
+ **Date/Time**: `now`, `age`, `date_part`, `date_trunc`, `extract`, `to_timestamp`, `to_char`, `to_date`, `to_time`, `make_date`, `make_time`, `make_timestamp`
491
+
492
+ **Utility**: `coalesce`
493
+
494
+ **JSON**: `jsonb_array_length`, `jsonb_extract_path`, `jsonb_extract_path_text`, `jsonb_build_object`, `json_build_object`, `jsonb_agg`, `json_agg`, `array_agg`
495
+
496
+ ---
497
+
498
+ ## Allowed Cast Types
499
+
500
+ `int`, `int[]`, `int2`, `int4`, `int8`, `bigint`, `real`, `float`, `float4`, `float8`, `numeric`, `double`, `decimal`, `money`, `timestamp`, `timestamptz`, `date`, `time`, `interval`, `bool`, `boolean`, `uuid`, `text` (and array variants)
501
+
502
+ ---
503
+
504
+ ## Complete Examples
505
+
506
+ ### Full-Featured Query
507
+ ```json
508
+ {
509
+ "columns": "id, task_name, ...record_owner(owner_name:name), ...related_account(account_name:name)",
510
+ "filters": {
511
+ "combinator": "and",
512
+ "rules": [
513
+ { "field": "task_status", "operator": "in", "value": [1, 2] },
514
+ { "field": "due_date", "operator": "in_next_x_days", "value": 7 },
515
+ { "field": "record_owner", "operator": "in_active_user_team" }
516
+ ]
517
+ },
518
+ "orderBy": "due_date ASC",
519
+ "limit": 50,
520
+ "fullCount": true
521
+ }
522
+ ```
523
+
524
+ ### Monthly Sales Report
525
+ ```json
526
+ {
527
+ "columns": "months_of_year@created_on, ...category(cat:name)",
528
+ "calculations": [
529
+ { "field": "id", "func": "count", "name": "order_count" },
530
+ { "field": "total_amount", "func": "sum", "name": "revenue" },
531
+ { "field": "total_amount", "func": "avg", "name": "avg_order" }
532
+ ],
533
+ "filters": { "rules": [
534
+ { "field": "created_on", "operator": "this_year" },
535
+ { "field": "order_status", "operator": "!=", "value": "cancelled" }
536
+ ]},
537
+ "orderBy": "months_of_year@created_on ASC"
538
+ }
539
+ ```
540
+
541
+ ### Computed Columns + Subquery
542
+ ```json
543
+ {
544
+ "columns": "id, name, profit_margin, active_deals",
545
+ "formulas": {
546
+ "profit_margin": {
547
+ "inputs": [{
548
+ "kind": "math", "op": "*",
549
+ "inputs": [
550
+ { "kind": "math", "op": "/", "inputs": [
551
+ { "kind": "math", "op": "-", "inputs": [
552
+ { "kind": "column", "name": "revenue" },
553
+ { "kind": "column", "name": "cost" }
554
+ ]},
555
+ { "kind": "column", "name": "revenue", "cast": "decimal" }
556
+ ]},
557
+ { "kind": "literal", "literal": 100 }
558
+ ]
559
+ }]
560
+ },
561
+ "active_deals": {
562
+ "from": "crm_deal", "with": "account",
563
+ "filters": { "rules": [{ "field": "stage", "operator": "!=", "value": "lost" }, { "field": "stage", "operator": "!=", "value": "won" }] },
564
+ "inputs": [{ "kind": "aggregate", "name": "count", "inputs": [] }]
565
+ }
566
+ },
567
+ "orderBy": "profit_margin DESC",
568
+ "limit": 20
569
+ }
570
+ ```
571
+
572
+ ### Customers with Nested Orders
573
+ ```json
574
+ {
575
+ "columns": "id, name, email, recent_orders, open_tickets",
576
+ "childQueries": {
577
+ "recent_orders": {
578
+ "from": "shop_order", "using": "customer",
579
+ "columns": "id, order_date, total_amount, ...status(status_label:name)",
580
+ "orderBy": "order_date DESC", "limit": 10,
581
+ "filters": { "rules": [{ "field": "order_date", "operator": "last_90_days" }] }
582
+ },
583
+ "open_tickets": {
584
+ "from": "support_ticket", "using": "customer",
585
+ "columns": "id, subject, priority, created_on",
586
+ "orderBy": "created_on DESC", "limit": 5,
587
+ "filters": { "rules": [{ "field": "status", "operator": "!=", "value": "closed" }] }
588
+ }
589
+ },
590
+ "limit": 25
591
+ }
592
+ ```
@@ -0,0 +1,70 @@
1
+ ---
2
+ name: docyrus-api-doctor
3
+ description: Run after making Docyrus API changes to catch bugs, performance issues, and code quality problems. Use when implementing or modifying code that uses Docyrus collection hooks (.list, .get, .create, .update, .delete), direct RestApiClient calls, query payloads with filters/calculations/formulas/childQueries/pivots, or TanStack Query integration with Docyrus data sources. Triggers on tasks involving Docyrus API logic, data fetching, mutations, or query payload construction.
4
+ ---
5
+
6
+ # Docyrus API Doctor
7
+
8
+ Post-implementation checklist for Docyrus API code. Run through each applicable check after writing or modifying API logic. Fix every issue found before considering the task complete.
9
+
10
+ **How to use:** After implementing API logic, scan the changed code against each check below. Skip checks that don't apply to the code at hand. For detailed explanations and fix examples, read `references/checklist-details.md`.
11
+
12
+ ---
13
+
14
+ ## BUG — Will Cause Errors
15
+
16
+ | # | Check | What to look for |
17
+ |---|-------|-----------------|
18
+ | B1 | **Missing `columns` parameter** | Any `.list()` or `.get()` call without a `columns` property. Without it, only `id` is returned. |
19
+ | B2 | **`limit: 0` in query payload** | `limit` set to `0` causes an API error. Remove `limit` entirely or set a positive integer. |
20
+ | B3 | **Child query key not in `columns`** | If `childQueries` defines key `orders`, the string `orders` must also appear in `columns`. |
21
+ | B4 | **Formula key not in `columns`** | If `formulas` defines key `total`, the string `total` must also appear in `columns`. |
22
+ | B5 | **Aggregation via `@` column syntax** | Using `count@field` or `sum@field` in columns. Use the `calculations` parameter instead. |
23
+ | B6 | **`distinctColumns` with `calculations`** | These are mutually exclusive. Use `calculations` for aggregation. |
24
+ | B7 | **Formula `extract` input count** | `extract` blocks must have exactly 1 input. |
25
+ | B8 | **Formula `not` operand count** | `not` blocks must have exactly 1 operand. |
26
+ | B9 | **Formula `and`/`or` operand count** | `and`/`or` blocks must have at least 2 operands. |
27
+ | B10 | **Formula math operand count** | Math operations (`+`, `-`, `*`, `/`, `%`) must have at least 2 operands. |
28
+ | B11 | **Formula `case` without `when`** | `case` expressions must have at least 1 `when` clause. |
29
+ | B12 | **Uncast literal in `jsonb_build_object`** | String literals inside `jsonb_build_object` need explicit `"cast": "text"` — auto-cast only works inside `concat`/`concat_ws`. |
30
+
31
+ ## PERFORMANCE — Degrades Speed or Wastes Resources
32
+
33
+ | # | Check | What to look for |
34
+ |---|-------|-----------------|
35
+ | P1 | **Unnecessary `limit` on aggregation queries** | When using `calculations` without needing raw rows, don't send `limit`. Aggregation returns a single grouped result naturally. |
36
+ | P2 | **`fullCount` just to get a total count** | If you only need the count (not the rows), use `calculations: [{ func: 'count', field: 'id', name: 'total' }]` instead of `fullCount: true` with row fetching. |
37
+ | P3 | **Unnecessary `columns` on calculation-only queries** | When using `calculations` and only reading aggregated values, don't send `columns`. The calculation result is returned without needing column selection. |
38
+ | P4 | **Over-fetching columns** | Columns selected in `columns` but never read in the consuming code. Only select what you render or process. |
39
+ | P5 | **Large `limit` without pagination** | `limit` > 200 without `offset`/`fullCount` pagination risks slow responses and high memory usage. |
40
+ | P6 | **Missing `expand` causing N+1** | Rendering relation/enum/user field `.name` but not including that field in `expand`. Without `expand`, you get only the ID. |
41
+ | P7 | **Fetching rows for existence checks** | Fetching records just to check if any exist. Use `calculations` count instead. |
42
+ | P8 | **Redundant overlapping queries** | Multiple queries on the same data source fetching overlapping columns that could be combined into one. |
43
+
44
+ ## CODE QUALITY — Causes Maintenance Pain
45
+
46
+ | # | Check | What to look for |
47
+ |---|-------|-----------------|
48
+ | Q1 | **Heavy `as` type assertions on responses** | Casting API responses with `as Array<...>` without runtime validation. Prefer typed collection return types or add validation. |
49
+ | Q2 | **Missing `enabled` on dependent queries** | `useQuery` that depends on a runtime value (e.g., `recordId`) but lacks `enabled: !!recordId`. The query fires with `undefined`. |
50
+ | Q3 | **No error handling on mutations** | `.create()`, `.update()`, `.delete()` calls without try/catch or error feedback to the user. |
51
+ | Q4 | **Missing query invalidation after mutations** | After create/update/delete, related query keys must be invalidated so lists refresh. |
52
+ | Q5 | **Serial cache invalidations** | Multiple `await queryClient.invalidateQueries(...)` in sequence. Use `Promise.all()` for independent invalidations. |
53
+ | Q6 | **Using deprecated `expandTypes`** | Replace with the `expand` parameter. |
54
+ | Q7 | **Hardcoded data source paths** | Raw `client.get('/v1/apps/base/data-sources/project/items')` instead of using generated collection hooks. |
55
+ | Q8 | **`distinctColumns` without `orderBy`** | `distinctColumns` requires `orderBy` to define which row wins per group. |
56
+
57
+ ---
58
+
59
+ ## How to Run
60
+
61
+ 1. Identify all files changed in this task that contain Docyrus API calls
62
+ 2. For each file, scan for `.list(`, `.get(`, `.create(`, `.update(`, `.delete(`, `client.get(`, `client.post(`, `client.patch(`, `client.delete(`
63
+ 3. Check each call site against the BUG checks (B1-B12)
64
+ 4. Check query payloads against PERFORMANCE checks (P1-P8)
65
+ 5. Check surrounding code (hooks, error handling, invalidation) against CODE QUALITY checks (Q1-Q8)
66
+ 6. Fix all issues found, starting with BUG category
67
+
68
+ ## References
69
+
70
+ - **`references/checklist-details.md`** — Detailed explanation, detection pattern, and before/after fix example for every check item