@dforge-core/dforge-mcp 0.1.0-rc.8 → 0.1.1

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 (53) hide show
  1. package/CHANGELOG.md +132 -0
  2. package/README.md +84 -27
  3. package/dist/server.js +2957 -616
  4. package/docs/creating-modules.md +11 -4
  5. package/package.json +11 -6
  6. package/resources/docs/conventions.md +22 -16
  7. package/resources/docs/dsl-reference.md +767 -0
  8. package/resources/schemas/entity.schema.json +8 -1
  9. package/resources/schemas/print-templates.schema.json +82 -0
  10. package/resources/schemas/reports.schema.json +4 -0
  11. package/resources/schemas/triggers.schema.json +59 -0
  12. package/skills/dforge-mcp-author/SKILL.md +269 -69
  13. package/skills/dforge-mcp-author/examples/matrix-budget/README.md +43 -0
  14. package/skills/dforge-mcp-author/examples/matrix-budget/entities/budget_category.json +24 -0
  15. package/skills/dforge-mcp-author/examples/matrix-budget/entities/budget_line.json +56 -0
  16. package/skills/dforge-mcp-author/examples/matrix-budget/manifest.json +30 -0
  17. package/skills/dforge-mcp-author/examples/matrix-budget/security/roles.json +9 -0
  18. package/skills/dforge-mcp-author/examples/matrix-budget/seed-data/01-categories.json +8 -0
  19. package/skills/dforge-mcp-author/examples/matrix-budget/ui/data_views.json +42 -0
  20. package/skills/dforge-mcp-author/examples/simple-todo/README.md +38 -0
  21. package/skills/dforge-mcp-author/examples/simple-todo/entities/todo_item.json +83 -0
  22. package/skills/dforge-mcp-author/examples/simple-todo/entities/todo_list.json +43 -0
  23. package/skills/dforge-mcp-author/examples/simple-todo/logic/actions/mark_done.dsl +6 -0
  24. package/skills/dforge-mcp-author/examples/simple-todo/manifest.json +32 -0
  25. package/skills/dforge-mcp-author/examples/simple-todo/security/roles.json +10 -0
  26. package/skills/dforge-mcp-author/examples/simple-todo/seed-data/01-lists.json +17 -0
  27. package/skills/dforge-mcp-author/examples/simple-todo/ui/actions.json +11 -0
  28. package/skills/dforge-mcp-author/examples/simple-todo/ui/data_views.json +35 -0
  29. package/skills/dforge-mcp-author/examples/simple-todo/ui/menus.json +28 -0
  30. package/skills/dforge-mcp-author/references/action-dsl.md +397 -0
  31. package/skills/dforge-mcp-author/references/column-types.md +168 -0
  32. package/skills/dforge-mcp-author/references/conventions.md +177 -0
  33. package/skills/dforge-mcp-author/references/data-migration.md +270 -0
  34. package/skills/dforge-mcp-author/references/data-views.md +243 -0
  35. package/skills/dforge-mcp-author/references/excel-import.md +61 -0
  36. package/skills/dforge-mcp-author/references/field-types.md +144 -0
  37. package/skills/dforge-mcp-author/references/filters.md +326 -0
  38. package/skills/dforge-mcp-author/references/flags.md +73 -0
  39. package/skills/dforge-mcp-author/references/formulas.md +206 -0
  40. package/skills/dforge-mcp-author/references/jobs.md +149 -0
  41. package/skills/dforge-mcp-author/references/manifest.md +123 -0
  42. package/skills/dforge-mcp-author/references/menus.md +164 -0
  43. package/skills/dforge-mcp-author/references/number-sequences.md +117 -0
  44. package/skills/dforge-mcp-author/references/print-templates.md +159 -0
  45. package/skills/dforge-mcp-author/references/queries.md +312 -0
  46. package/skills/dforge-mcp-author/references/reports.md +398 -0
  47. package/skills/dforge-mcp-author/references/schema-import.md +331 -0
  48. package/skills/dforge-mcp-author/references/security.md +244 -0
  49. package/skills/dforge-mcp-author/references/settings.md +120 -0
  50. package/skills/dforge-mcp-author/references/traits.md +153 -0
  51. package/skills/dforge-mcp-author/references/translations.md +158 -0
  52. package/skills/dforge-mcp-author/references/validation-checklist.md +183 -0
  53. package/skills/dforge-mcp-author/scripts/xlsx_to_model.py +198 -0
@@ -0,0 +1,117 @@
1
+ # Number Sequences Reference
2
+
3
+ dForge has built-in **automatic document numbering**. Declare a sequence on an entity, and any INSERT that leaves the target column empty gets the next value assigned automatically — no DSL, no manual counter, no race conditions.
4
+
5
+ ## Declaring on an entity
6
+
7
+ ```json
8
+ {
9
+ "description": "Invoices",
10
+ "dbObject": "invoice",
11
+ "toString": "{number}",
12
+ "traits": ["identity", "audit"],
13
+ "numberSequence": {
14
+ "column": "number",
15
+ "pattern": "INV-{yyyy}-{seq:4}",
16
+ "resetPeriod": "year"
17
+ },
18
+ "fields": {
19
+ "number": {
20
+ "dbDatatype": "varchar",
21
+ "fieldTypeCd": "text",
22
+ "flags": "VEM",
23
+ "maxLen": 50,
24
+ "orderNum": 10,
25
+ "description": "Invoice Number"
26
+ },
27
+ /* ... */
28
+ }
29
+ }
30
+ ```
31
+
32
+ The platform:
33
+
34
+ 1. On INSERT, checks if the `number` column is empty.
35
+ 2. If empty, atomically increments the counter and formats the value.
36
+ 3. Writes it into the column before the row is committed.
37
+
38
+ No code needed on your side. No DSL. No race conditions (the counter uses a database-level atomic operation).
39
+
40
+ ## Pattern placeholders
41
+
42
+ | Placeholder | Meaning | Example |
43
+ |---|---|---|
44
+ | `{yyyy}` | 4-digit year | `2026` |
45
+ | `{yy}` | 2-digit year | `26` |
46
+ | `{mm}` | 2-digit month | `04` |
47
+ | `{dd}` | 2-digit day | `08` |
48
+ | `{seq:N}` | Zero-padded counter with N digits | `{seq:3}` → `001`, `002`, ... |
49
+
50
+ You can combine them freely:
51
+
52
+ ```
53
+ "INV-{yyyy}-{seq:4}" → "INV-2026-0001"
54
+ "Q{yyyy}{mm}-{seq:3}" → "Q202604-001"
55
+ "{dd}/{mm}/{yyyy}-{seq:2}" → "08/04/2026-01"
56
+ "PO-{seq:6}" → "PO-000001"
57
+ ```
58
+
59
+ ## Reset periods
60
+
61
+ `resetPeriod` controls when the counter resets to 1:
62
+
63
+ | Value | Resets |
64
+ |---|---|
65
+ | `"never"` | Counter never resets (monotonic forever) |
66
+ | `"year"` | Counter resets on Jan 1 |
67
+ | `"month"` | Counter resets on the 1st of each month |
68
+ | `"day"` | Counter resets daily |
69
+
70
+ If the pattern uses `{yyyy}` but `resetPeriod` is `"never"`, you get the same number each year — probably not what you want. Match the reset period to the pattern.
71
+
72
+ ## Prefix resolution via settings
73
+
74
+ For multi-tenant or multi-folder setups, prefixes can come from module settings instead of being hardcoded. Declare a setting:
75
+
76
+ ```json
77
+ // settings.json
78
+ {
79
+ "invoice_prefix": {
80
+ "fieldTypeCd": "text",
81
+ "defaultValue": "INV",
82
+ "label": "Invoice Prefix"
83
+ }
84
+ }
85
+ ```
86
+
87
+ And reference it in the pattern:
88
+
89
+ ```
90
+ "pattern": "$[invoice_prefix]-{yyyy}-{seq:4}"
91
+ ```
92
+
93
+ Folder-scoped settings mean different folders can have different prefixes (e.g. `US-INV-2026-0001` in the US folder, `EU-INV-2026-0001` in the EU folder).
94
+
95
+ ## Cross-module sequences
96
+
97
+ Sequences can be called from other modules via `nextNumber('<module>.<entity>')` in DSL actions:
98
+
99
+ ```
100
+ execute:
101
+ invoice_num = nextNumber("fin.invoice")
102
+ notify("Next invoice number: " + invoice_num)
103
+ ```
104
+
105
+ But most of the time, you don't need this — the automatic INSERT behaviour is enough.
106
+
107
+ ## Storage
108
+
109
+ Counters are stored in `dForge.number_sequence` (definitions) and `dForge.number_sequence_counter` (atomic values). Don't touch these tables directly.
110
+
111
+ ## Common mistakes
112
+
113
+ - Declaring a number column without a `numberSequence` and manually counting in a DSL — **unnecessary**. Use the platform.
114
+ - Using `{seq}` without a width — the counter has no padding. Use `{seq:N}`.
115
+ - Mismatching `resetPeriod` and placeholders — e.g. `resetPeriod: "month"` with a pattern like `{yyyy}-{seq:4}` (no month) means Jan and Feb start at 1 producing duplicate numbers.
116
+ - Trying to set the number column manually on INSERT — it will be overwritten (unless you provide a non-null value, in which case the sequence respects it).
117
+ - Forgetting `numberSequence` declaration and expecting it to "just work" — needs to be declared per entity.
@@ -0,0 +1,159 @@
1
+ # Print Templates Reference
2
+
3
+ dForge ships Liquid-based print templates. A record + its child sets are rendered to HTML, which the browser prints or saves as PDF.
4
+
5
+ Lives in: `ui/print_templates.json` + HTML/CSS files under `print_templates/`. Schema: [`docs/schemas/print_templates.schema.json`](../../../docs/schemas/print_templates.schema.json). Full guide: [`docs/ui/print-templates.md`](../../../docs/ui/print-templates.md). QR + snippets: [`docs/ui/print-qr-codes.md`](../../../docs/ui/print-qr-codes.md).
6
+
7
+ ## Two render paths
8
+
9
+ | Path | Engine | Used by |
10
+ | ----------- | --------------- | -------------------------------------------------------- |
11
+ | **Server** | Scriban (Liquid) | `print.render` RPC — final print / save-as-PDF |
12
+ | **Client** | LiquidJS | Editor preview — fast iteration |
13
+
14
+ Filters are mirrored on both sides so the preview matches the print.
15
+
16
+ ## Manifest structure
17
+
18
+ ```json
19
+ {
20
+ "invoice": {
21
+ "entityCd": "invoice",
22
+ "description": "Invoice Print Form",
23
+ "file": "invoice.html",
24
+ "css": "invoice.css",
25
+ "pageSettings": {
26
+ "size": "A4",
27
+ "orientation": "portrait",
28
+ "margins": { "top": "15mm", "right": "15mm", "bottom": "15mm", "left": "15mm" }
29
+ }
30
+ },
31
+ "footer": {
32
+ "type": "snippet",
33
+ "description": "Shared invoice footer block",
34
+ "file": "footer.html"
35
+ }
36
+ }
37
+ ```
38
+
39
+ The key (`"invoice"`, `"footer"`) becomes `template_cd` on the row.
40
+
41
+ ## Field reference
42
+
43
+ | Field | Required | Notes |
44
+ | -------------- | --------------- | ---------------------------------------------------------------------------------------------------------------------------------- |
45
+ | `type` | No | `"template"` (default) or `"snippet"`. |
46
+ | `entityCd` | Yes (templates) | Entity code. Cross-module form `"module.entity"` supported (bridge modules). Must be omitted on snippets. |
47
+ | `description` | No | Label shown in the print menu. |
48
+ | `template` | One of these | Inline Liquid HTML. |
49
+ | `file` | One of these | Path relative to `print_templates/`. |
50
+ | `css` | No | Inline CSS or a `.css` filename under `print_templates/`. Snippets typically inherit and omit. |
51
+ | `pageSettings` | No | `{ size, orientation, margins }`. `size` ∈ `A4` / `letter` / `legal`. `orientation` ∈ `portrait` / `landscape`. Margins are CSS lengths. |
52
+
53
+ ## Template context (what you can read in Liquid)
54
+
55
+ Every column on the entity is a top-level variable. Display formatting is pre-applied for dropdown / radio / flags / user columns.
56
+
57
+ | Variable | What it holds |
58
+ | ----------------------- | ------------------------------------------------------------------------------------------------------ |
59
+ | `{{ <column> }}` | Display value: dropdown labels resolved, user names resolved, refs resolved to their display column |
60
+ | `{{ _fmt.<column> }}` | Locale-aware **string**: `currency` → `"$1,234.56"`, `date` → `"5/22/2026"`, `bool` → `"Yes"`/`"No"` |
61
+ | `{{ _raw.<column> }}` | Original code / id (use for CSS classes, predicates, wire-format payloads) |
62
+ | `{{ _color.<column> }}` | Option color hex for single-value dropdown/radio columns |
63
+ | `{{ _settings.<cd> }}` | Folder-resolved module setting. Image/file settings come as base64 data URIs (drop into `<img src>`). |
64
+ | `{{ _today }}` | Typed `DateTimeOffset` (UTC); `{{ _fmt._today }}` is the pre-formatted string |
65
+ | `{{ <set_name> }}` | Child set — array. Iterate with `{% for x in <set_name> %}`. Only loaded if the template iterates it. |
66
+ | `{{ _css }}` | The CSS string attached to the template (rarely needed in template body) |
67
+
68
+ Inside a child-set loop, the same shape applies per row: `line._fmt.<col>`, `line._raw.<col>`, `line._color.<col>`.
69
+
70
+ ## Custom filters
71
+
72
+ | Filter | Example | Output |
73
+ | ----------------------------------------------- | ------------------------------------------------------ | ---------------------------- |
74
+ | `format_currency: <currency>, <digits?>` | `{{ amount | format_currency: 'USD' }}` | `"$1,234.56"` (en-US) |
75
+ | `format_number: <decimals?>` | `{{ rate | format_number: 4 }}` | `"0.0125"` |
76
+ | `format_date: <strftime>` | `{{ invoice_date | format_date: '%B %d, %Y' }}` | `"May 22, 2026"` |
77
+ | `qr_code: <size?>, <ecc?>, <margin?>, <format?>`| `{{ payment_url | qr_code: 200, 'M' }}` | `data:image/png;base64,…` |
78
+
79
+ `format_date` supports strftime: `%Y %m %d %B %b %H %M %S %I %p %A %a %e`.
80
+
81
+ ## Snippets
82
+
83
+ Module-scoped Liquid partials. Pull one in via `{% include 'module_cd.snippet_cd' %}`. Snippets see all variables from the host template by default; pass overrides with `with`:
84
+
85
+ ```liquid
86
+ {% include 'fin-sepa.epc_qr_block' %}
87
+ {% include 'fin-sepa.epc_qr_block' with currency: 'EUR' %}
88
+ ```
89
+
90
+ Declare a snippet with `"type": "snippet"`. No `entityCd`. The `template_cd` must not contain `.` (collides with the `module_cd.snippet_cd` separator).
91
+
92
+ Full mechanics + an EPC SEPA QR worked example: [`docs/ui/print-qr-codes.md`](../../../docs/ui/print-qr-codes.md).
93
+
94
+ ## Module package layout
95
+
96
+ ```
97
+ my_module/
98
+ ├── manifest.json
99
+ ├── ui/
100
+ │ └── print_templates.json
101
+ └── print_templates/
102
+ ├── invoice.html
103
+ ├── invoice.css
104
+ └── footer.html (snippet)
105
+ ```
106
+
107
+ ## Example template
108
+
109
+ ```liquid
110
+ <div class="invoice-header">
111
+ {% if _settings.company_logo %}
112
+ <img class="company-logo" src="{{ _settings.company_logo }}" />
113
+ {% endif %}
114
+ <h1>INVOICE {{ invoice_number }}</h1>
115
+ <p>Date: {{ _fmt.invoice_date }} · Due: {{ _fmt.due_date }}</p>
116
+ <p>Status: <span class="badge" style="--c: {{ _color.status }}">{{ status }}</span></p>
117
+ </div>
118
+
119
+ <table>
120
+ {% for line in lines %}
121
+ <tr>
122
+ <td>{{ line.description }}</td>
123
+ <td>{{ line.quantity }}</td>
124
+ <td>{{ line._fmt.unit_price }}</td>
125
+ <td>{{ line._fmt.line_total }}</td>
126
+ </tr>
127
+ {% endfor %}
128
+ </table>
129
+
130
+ <p>Total: <strong>{{ _fmt.total_amount }}</strong></p>
131
+
132
+ {% if amount_paid > 0 %}
133
+ <p>Paid: -{{ _fmt.amount_paid }}</p>
134
+ <p>Due: {{ _fmt.amount_due }}</p>
135
+ {% endif %}
136
+
137
+ {% include 'fin.footer' %}
138
+ ```
139
+
140
+ ## Common mistakes
141
+
142
+ - **Using `_fmt` in a predicate.** `{% if _fmt.amount_paid > 0 %}` compares strings. Use the bare reference: `{% if amount_paid > 0 %}`.
143
+ - **Forgetting `_raw` for codes.** Direct `{{ status }}` is the label (`"Partially Paid"`). For CSS classes / wire-format payloads / predicates against codes, use `{{ _raw.status }}` (`"P"`).
144
+ - **Manually formatting dates/currency.** Don't reach for `format_currency` when the column is already `fieldTypeCd: "currency"` — `_fmt.<col>` already matches the form. Use the filter only for derived values.
145
+ - **Referencing a child set outside a `{% for %}`.** The platform only loads sets that appear in a `for` loop. `{{ lines.size }}` won't trigger the load.
146
+ - **Snippet `template_cd` with a dot.** `{% include 'my_module.foo.bar' %}` — the resolver splits on the last dot only, so `my_module.foo` becomes the module name and breaks. No dots in snippet codes.
147
+ - **Dropping CSS in `<style>` blocks inline.** The renderer wires `<style>` for you; declare CSS in the separate `.css` file (or inline string via the `css` field).
148
+ - **Trying to use `TODAY()` / `NOW()` in a formula column expecting it to render in print.** The print-time formula evaluator skips those. Pre-compute via a formula column at insert time, or use `{{ _today | format_date: '...' }}` directly in Liquid.
149
+ - **Module-installed template renamed in editor.** The editor preserves the original `template_cd` so `{% include %}` references stay intact — don't try to work around it.
150
+
151
+ Module-installed print-template rows can't be deleted from the editor — uninstall the owning module to remove them.
152
+
153
+ ## Reference
154
+
155
+ - Full developer guide: [`docs/ui/print-templates.md`](../../../docs/ui/print-templates.md)
156
+ - QR codes + snippets guide: [`docs/ui/print-qr-codes.md`](../../../docs/ui/print-qr-codes.md)
157
+ - JSON Schema: [`docs/schemas/print_templates.schema.json`](../../../docs/schemas/print_templates.schema.json)
158
+ - Reference modules: [`modules/fin/`](../../../modules/fin/), [`modules/fin-ch/`](../../../modules/fin-ch/) (Swiss QR-bill), [`modules/crm/`](../../../modules/crm/), [`modules/wms/`](../../../modules/wms/), [`modules/hr/`](../../../modules/hr/)
159
+ - Source: `server/src/dForge.Api/Services/PrintTemplateService.cs`, `server/src/dForge.Admin/Services/ModuleInstall/PrintTemplateRegistrar.cs`
@@ -0,0 +1,312 @@
1
+ # Pre-Built Queries Reference
2
+
3
+ Modules can ship **pre-built saved queries** — lightweight, explorable analytics that appear in the Query Builder alongside user-created queries. Think of them as "light reports" that are easier to author than full reports.
4
+
5
+ Lives in: `ui/queries.json`
6
+
7
+ ## When to use queries vs reports vs views
8
+
9
+ | Need | Use |
10
+ |---|---|
11
+ | Show entity records with filters/sort | **Data view** (`ui/data_views.json`) |
12
+ | Simple aggregation — "totals by X" | **Pre-built query** (`ui/queries.json`) |
13
+ | Ad-hoc exploration by the user | **Query Builder** (runtime, no module authoring needed) |
14
+ | Complex multi-dataset visualizations with charts/KPIs | **Report** (`ui/reports.json`) |
15
+ | Full SQL with window functions, CTEs, cross-schema | **Report with stored procedure** |
16
+
17
+ Queries fill the gap between views (flat, no aggregation) and reports (heavy, need layout config).
18
+
19
+ ## Structure
20
+
21
+ ```json
22
+ {
23
+ "overdue_invoices": {
24
+ "name": "Overdue Invoices",
25
+ "description": "All invoices past due date, not yet paid",
26
+ "entityCode": "invoice",
27
+ "query": {
28
+ "entityCd": "invoice",
29
+ "columns": [
30
+ "invoice_number",
31
+ "customer",
32
+ "total_amount",
33
+ "amount_due",
34
+ "due_date",
35
+ "days_overdue",
36
+ "owner_id"
37
+ ],
38
+ "filter": {
39
+ "g": "and",
40
+ "i": [
41
+ { "c": "status", "o": "!=", "v": "Paid" },
42
+ { "c": "status", "o": "!=", "v": "Cancelled" },
43
+ { "c": "due_date", "o": "f", "v": { "fn": "isPast" } }
44
+ ]
45
+ },
46
+ "sort": [
47
+ { "c": "days_overdue", "d": "desc" }
48
+ ]
49
+ }
50
+ },
51
+ "revenue_by_customer": {
52
+ "name": "Revenue by Customer",
53
+ "description": "Total invoiced revenue grouped by customer",
54
+ "entityCode": "invoice",
55
+ "query": {
56
+ "entityCd": "invoice",
57
+ "columns": [
58
+ { "c": "customer", "agg": "group", "alias": "Customer" },
59
+ { "c": "total_amount", "agg": "sum", "alias": "Total Revenue" },
60
+ { "c": "invoice_number", "agg": "count", "alias": "Invoice Count" }
61
+ ],
62
+ "filter": {
63
+ "c": "status", "o": "!=", "v": "Cancelled"
64
+ },
65
+ "sort": [
66
+ { "c": "total_amount", "d": "desc" }
67
+ ]
68
+ }
69
+ }
70
+ }
71
+ ```
72
+
73
+ ## Query JSON format
74
+
75
+ The `query` object uses the **exact same format** as the Query Builder produces at runtime. This means you can:
76
+ 1. Design a query in the Query Builder UI
77
+ 2. Copy the JSON
78
+ 3. Paste it into `queries.json`
79
+
80
+ ### Flat mode (no aggregation)
81
+
82
+ ```json
83
+ {
84
+ "entityCd": "contact",
85
+ "columns": ["first_name", "last_name", "email", "account", "owner_id"],
86
+ "filter": { "c": "email", "o": "!null" },
87
+ "sort": [{ "c": "last_name", "d": "asc" }]
88
+ }
89
+ ```
90
+
91
+ Columns are plain strings — field codes or reference paths (`"account.name"`, `"project.client.short_name"`).
92
+
93
+ ### Grouped mode (with aggregation)
94
+
95
+ ```json
96
+ {
97
+ "entityCd": "opportunity",
98
+ "columns": [
99
+ { "c": "stage", "agg": "group", "alias": "Stage" },
100
+ { "c": "owner_id", "agg": "group", "alias": "Owner" },
101
+ { "c": "total_amount", "agg": "sum", "alias": "Total Value" },
102
+ { "c": "opportunity_id", "agg": "count", "alias": "Deal Count" },
103
+ { "c": "total_amount", "agg": "avg", "alias": "Avg Deal Size" }
104
+ ],
105
+ "filter": {
106
+ "c": "stage", "o": "!in", "v": ["Closed Won", "Closed Lost"]
107
+ },
108
+ "sort": [{ "c": "total_amount", "d": "desc" }]
109
+ }
110
+ ```
111
+
112
+ In grouped mode, each column is an object with:
113
+ - `c` — column code (or reference path)
114
+ - `agg` — aggregation: `group` (dimension), `sum`, `count`, `avg`, `min`, `max`
115
+ - `alias` — display name for the result column
116
+
117
+ At least one column must be `"agg": "group"` and at least one must be an aggregate.
118
+
119
+ ### Reference paths in columns
120
+
121
+ Columns can traverse references up to 2 levels deep:
122
+
123
+ ```json
124
+ "columns": [
125
+ "invoice_number",
126
+ "customer.account_name",
127
+ "customer.industry",
128
+ "owner_id"
129
+ ]
130
+ ```
131
+
132
+ The query engine automatically JOINs the referenced entities.
133
+
134
+ ### Filters
135
+
136
+ Filters use the standard JSON filter format (see `references/filters.md`): `{ "c": column, "o": operator, "v": value }`.
137
+
138
+ ### Dynamic date and user filters (function operator)
139
+
140
+ Instead of hardcoded date values, use the **function operator** `"f"` with a `{ "fn": "..." }` value. This evaluates at query time on the server.
141
+
142
+ **A function IS the whole condition — it bakes in the comparison.** You cannot pass a function as the value of `<`, `>`, `=`, `!=`, `between`, etc. Functions are ONLY valid with the `"o": "f"` operator, and each function encodes its own comparison logic (for example, `isPast` already means `column < NOW()`).
143
+
144
+ | Function | Applies to | Description |
145
+ |---|---|---|
146
+ | `isToday` | date, timestamp | Current date |
147
+ | `isYesterday` | date, timestamp | Previous day |
148
+ | `isThisWeek` | date, timestamp | Current ISO week |
149
+ | `isThisMonth` | date, timestamp | Current month |
150
+ | `isThisYear` | date, timestamp | Current year |
151
+ | `isPast` | date, timestamp | Before now |
152
+ | `isFuture` | date, timestamp | After now |
153
+ | `isCurrentUser` | guid | Matches the logged-in user |
154
+
155
+ **Example — overdue items (due date is in the past):**
156
+
157
+ ```json
158
+ { "c": "due_date", "o": "f", "v": { "fn": "isPast" } }
159
+ ```
160
+
161
+ **Example — records created this month:**
162
+
163
+ ```json
164
+ { "c": "created_date", "o": "f", "v": { "fn": "isThisMonth" } }
165
+ ```
166
+
167
+ **Example — assigned to current user:**
168
+
169
+ ```json
170
+ { "c": "owner_id", "o": "f", "v": { "fn": "isCurrentUser" } }
171
+ ```
172
+
173
+ ### Sort
174
+
175
+ ```json
176
+ "sort": [
177
+ { "c": "total_amount", "d": "desc" },
178
+ { "c": "customer", "d": "asc" }
179
+ ]
180
+ ```
181
+
182
+ `d` is direction: `"asc"` or `"desc"`.
183
+
184
+ ## Wrapper properties
185
+
186
+ Each query in `queries.json` has:
187
+
188
+ | Property | Required | Description |
189
+ |---|---|---|
190
+ | `name` | Yes | Display name in the query list |
191
+ | `description` | Recommended | Help text |
192
+ | `entityCode` | Yes | The root entity this query targets |
193
+ | `query` | Yes | The query JSON (flat or grouped format above) |
194
+
195
+ ## Menu integration
196
+
197
+ Queries are not in menu and not integrated into menu, they have separate section in sidebar, nothing need to be configured additionally.
198
+
199
+ ## Translations
200
+
201
+ Query names and descriptions are translated in `translations/en-US.json`:
202
+
203
+ ```json
204
+ {
205
+ "queries": {
206
+ "overdue_invoices": {
207
+ "name": "Overdue Invoices",
208
+ "description": "All invoices past due date, not yet paid"
209
+ },
210
+ "revenue_by_customer": {
211
+ "name": "Revenue by Customer",
212
+ "description": "Total invoiced revenue grouped by customer"
213
+ }
214
+ }
215
+ }
216
+ ```
217
+
218
+ ## Manifest declaration
219
+
220
+ ```json
221
+ {
222
+ "queries": "./ui/queries.json"
223
+ }
224
+ ```
225
+
226
+ ## Example queries for common module types
227
+
228
+ ### CRM — Pipeline by stage and owner
229
+ ```json
230
+ {
231
+ "entityCd": "opportunity",
232
+ "columns": [
233
+ { "c": "stage", "agg": "group", "alias": "Stage" },
234
+ { "c": "owner_id", "agg": "group", "alias": "Owner" },
235
+ { "c": "total_amount", "agg": "sum", "alias": "Total Value" },
236
+ { "c": "opportunity_id", "agg": "count", "alias": "Count" }
237
+ ],
238
+ "sort": [{ "c": "total_amount", "d": "desc" }]
239
+ }
240
+ ```
241
+
242
+ ### HR — Headcount by department
243
+ ```json
244
+ {
245
+ "entityCd": "employee",
246
+ "columns": [
247
+ { "c": "department", "agg": "group", "alias": "Department" },
248
+ { "c": "employee_id", "agg": "count", "alias": "Headcount" },
249
+ { "c": "salary", "agg": "avg", "alias": "Avg Salary" }
250
+ ],
251
+ "filter": { "c": "status", "o": "=", "v": "Active" },
252
+ "sort": [{ "c": "employee_id", "d": "desc" }]
253
+ }
254
+ ```
255
+
256
+ ### Finance — Receivables aging summary
257
+ ```json
258
+ {
259
+ "entityCd": "invoice",
260
+ "columns": [
261
+ "invoice_number",
262
+ "customer",
263
+ "total_amount",
264
+ "amount_due",
265
+ "due_date",
266
+ "days_overdue",
267
+ "status"
268
+ ],
269
+ "filter": {
270
+ "g": "and",
271
+ "i": [
272
+ { "c": "status", "o": "!in", "v": ["Paid", "Cancelled"] },
273
+ { "c": "amount_due", "o": ">", "v": 0 }
274
+ ]
275
+ },
276
+ "sort": [{ "c": "days_overdue", "d": "desc" }]
277
+ }
278
+ ```
279
+
280
+ ### WMS — Low stock items
281
+ ```json
282
+ {
283
+ "entityCd": "stock",
284
+ "columns": [
285
+ "product",
286
+ "warehouse",
287
+ "quantity",
288
+ "reserved_qty",
289
+ "available_qty"
290
+ ],
291
+ "filter": { "c": "available_qty", "o": "<", "v": 10 },
292
+ "sort": [{ "c": "available_qty", "d": "asc" }]
293
+ }
294
+ ```
295
+
296
+ ## Rules
297
+
298
+ - The `query` object must use the same JSON format as the Query Builder produces.
299
+ - Filters use the standard `c`/`o`/`v` format (see `references/filters.md`).
300
+ - Module queries are **read-only** for users — they can run them and "Copy to My Queries" to modify.
301
+ - Security is automatic — the query execution path respects entity, column, and row-level security.
302
+ - Grouped queries need at least one `"agg": "group"` column and one aggregate column.
303
+ - Reference paths go up to 2 levels deep (e.g. `"customer.country"` works, deeper may not).
304
+
305
+ ## Common mistakes
306
+
307
+ - Putting SQL in the `query` object — **wrong**. Queries use the JSON format, not raw SQL. For raw SQL, use stored procedures in reports.
308
+ - Using `"column"` instead of `"c"` in columns/filters — **wrong**. Use the short keys.
309
+ - Forgetting `"agg"` on columns in grouped mode — every column needs an aggregation assignment.
310
+ - Referencing columns from unrelated entities — queries can only traverse the root entity's references.
311
+ - Using `@TODAY`, `@CURRENT_USER`, or any `@placeholder` syntax in filter values — **wrong**. Queries do not support parameter substitution. Use the function operator instead: `{ "c": "due_date", "o": "f", "v": { "fn": "isPast" } }`.
312
+ - Using a function as the value of a comparison operator (`<`, `>`, `=`, `!=`, `between`) — **wrong**. Functions are standalone conditions, valid only with `"o": "f"` (full detail + examples under "Dynamic date and user filters" above). `{ "c": "due_date", "o": "<", "v": { "fn": "isToday" } }` is invalid; use `{ "c": "due_date", "o": "f", "v": { "fn": "isPast" } }`.