@dforge-core/dforge-mcp 0.1.0-rc.12 → 0.1.0-rc.13

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 (43) hide show
  1. package/CHANGELOG.md +132 -0
  2. package/README.md +75 -21
  3. package/dist/server.js +2405 -272
  4. package/docs/creating-modules.md +7 -3
  5. package/package.json +11 -6
  6. package/resources/docs/conventions.md +12 -9
  7. package/resources/schemas/entity.schema.json +4 -0
  8. package/resources/schemas/reports.schema.json +4 -0
  9. package/skills/dforge-mcp-author/SKILL.md +220 -91
  10. package/skills/dforge-mcp-author/examples/simple-todo/README.md +38 -0
  11. package/skills/dforge-mcp-author/examples/simple-todo/entities/todo_item.json +83 -0
  12. package/skills/dforge-mcp-author/examples/simple-todo/entities/todo_list.json +43 -0
  13. package/skills/dforge-mcp-author/examples/simple-todo/logic/actions/mark_done.dsl +6 -0
  14. package/skills/dforge-mcp-author/examples/simple-todo/manifest.json +32 -0
  15. package/skills/dforge-mcp-author/examples/simple-todo/security/roles.json +10 -0
  16. package/skills/dforge-mcp-author/examples/simple-todo/seed-data/01-lists.json +17 -0
  17. package/skills/dforge-mcp-author/examples/simple-todo/ui/actions.json +11 -0
  18. package/skills/dforge-mcp-author/examples/simple-todo/ui/data_views.json +35 -0
  19. package/skills/dforge-mcp-author/examples/simple-todo/ui/menus.json +28 -0
  20. package/skills/dforge-mcp-author/references/action-dsl.md +397 -0
  21. package/skills/dforge-mcp-author/references/column-types.md +168 -0
  22. package/skills/dforge-mcp-author/references/conventions.md +177 -0
  23. package/skills/dforge-mcp-author/references/data-migration.md +270 -0
  24. package/skills/dforge-mcp-author/references/data-views.md +192 -0
  25. package/skills/dforge-mcp-author/references/excel-import.md +61 -0
  26. package/skills/dforge-mcp-author/references/field-types.md +144 -0
  27. package/skills/dforge-mcp-author/references/filters.md +326 -0
  28. package/skills/dforge-mcp-author/references/flags.md +73 -0
  29. package/skills/dforge-mcp-author/references/formulas.md +206 -0
  30. package/skills/dforge-mcp-author/references/jobs.md +149 -0
  31. package/skills/dforge-mcp-author/references/manifest.md +123 -0
  32. package/skills/dforge-mcp-author/references/menus.md +164 -0
  33. package/skills/dforge-mcp-author/references/number-sequences.md +117 -0
  34. package/skills/dforge-mcp-author/references/print-templates.md +159 -0
  35. package/skills/dforge-mcp-author/references/queries.md +312 -0
  36. package/skills/dforge-mcp-author/references/reports.md +398 -0
  37. package/skills/dforge-mcp-author/references/schema-import.md +331 -0
  38. package/skills/dforge-mcp-author/references/security.md +244 -0
  39. package/skills/dforge-mcp-author/references/settings.md +120 -0
  40. package/skills/dforge-mcp-author/references/traits.md +153 -0
  41. package/skills/dforge-mcp-author/references/translations.md +158 -0
  42. package/skills/dforge-mcp-author/references/validation-checklist.md +182 -0
  43. package/skills/dforge-mcp-author/scripts/xlsx_to_model.py +198 -0
@@ -0,0 +1,326 @@
1
+ # Filter Format Reference
2
+
3
+ dForge uses **one canonical JSON filter format everywhere** — folder row filters, data view filters, API filters, report dataset filters, and user ad-hoc filters. Learn it once, use it everywhere.
4
+
5
+ This is the single filter format used across the entire dForge platform.
6
+
7
+ ## Basic structure
8
+
9
+ A filter is either a **single condition** or a **group** of conditions.
10
+
11
+ ### Condition — single comparison
12
+
13
+ ```json
14
+ { "c": "status", "o": "=", "v": "draft" }
15
+ ```
16
+
17
+ | Key | Type | Description |
18
+ |---|---|---|
19
+ | `c` | string | Column code (field name on the entity) |
20
+ | `o` | string | Operator (see table below) |
21
+ | `v` | any | Value to compare against |
22
+
23
+ ### Group — logical combination
24
+
25
+ ```json
26
+ {
27
+ "g": "and",
28
+ "i": [
29
+ { "c": "status", "o": "=", "v": "draft" },
30
+ { "c": "amount", "o": ">", "v": 1000 }
31
+ ]
32
+ }
33
+ ```
34
+
35
+ | Key | Type | Description |
36
+ |---|---|---|
37
+ | `g` | string | Group operator: `and`, `or`, `!and` (NAND), `!or` (NOR) |
38
+ | `i` | array | Items — conditions and/or nested groups |
39
+
40
+ Groups can nest to arbitrary depth.
41
+
42
+ ## Operators
43
+
44
+ ### Condition operators
45
+
46
+ | Operator | Description | Example value |
47
+ |---|---|---|
48
+ | `=` or `eq` | Equal | `"draft"` |
49
+ | `!=` or `notEq` | Not equal | `"cancelled"` |
50
+ | `>` or `gr` | Greater than | `1000` |
51
+ | `>=` or `grEq` | Greater than or equal | `1000` |
52
+ | `<` or `less` | Less than | `100` |
53
+ | `<=` or `lessEq` | Less than or equal | `100` |
54
+ | `between` or `btw` | Between (inclusive) | `[100, 500]` |
55
+ | `!between` or `nBetween` | Not between | `[100, 500]` |
56
+ | `contains` | Contains substring | `"acme"` |
57
+ | `!contains` or `nContain` | Does not contain | `"test"` |
58
+ | `start` | Starts with | `"INV-"` |
59
+ | `!start` or `nStart` | Does not start with | `"DRAFT-"` |
60
+ | `end` | Ends with | `".pdf"` |
61
+ | `!end` or `nEnd` | Does not end with | `".tmp"` |
62
+ | `mask` | Pattern match (SQL LIKE) | `"INV-2025-%"` |
63
+ | `!mask` or `nMask` | Does not match pattern | `"TEST-%"` |
64
+ | `null` or `empty` | Is null/empty | _(no `v` needed)_ |
65
+ | `!null` or `nEmpty` | Is not null | _(no `v` needed)_ |
66
+ | `in` | In a set | `["draft", "pending"]` |
67
+ | `!in` or `nIn` | Not in a set | `["cancelled", "deleted"]` |
68
+ | `eqRef` | Equal to another column | `"other_column_cd"` |
69
+ | `f` | Function (dynamic runtime value) | `{ "fn": "isToday" }` |
70
+
71
+ ### Function operator — dynamic runtime values
72
+
73
+ The function operator `f` evaluates a named function at query time instead of comparing against a static value. The value must be an object: `{ "fn": "<functionId>" }`.
74
+
75
+ | Function | Column types | Description |
76
+ |---|---|---|
77
+ | `isToday` | date, timestamp | `column = CURRENT_DATE` |
78
+ | `isYesterday` | date, timestamp | `column = CURRENT_DATE - 1` |
79
+ | `isThisWeek` | date, timestamp | Within current ISO week |
80
+ | `isThisMonth` | date, timestamp | Within current month |
81
+ | `isThisYear` | date, timestamp | Within current year |
82
+ | `isPast` | date, timestamp | `column < NOW()` |
83
+ | `isFuture` | date, timestamp | `column > NOW()` |
84
+ | `isCurrentUser` | guid | `column = <logged-in user ID>` |
85
+
86
+ ```json
87
+ { "c": "due_date", "o": "f", "v": { "fn": "isPast" } }
88
+ ```
89
+
90
+ ```json
91
+ { "c": "owner_id", "o": "f", "v": { "fn": "isCurrentUser" } }
92
+ ```
93
+
94
+ Function conditions require **no value input** — the function provides the comparison logic at runtime.
95
+
96
+ **Functions are valid only with the `f` operator.** They cannot be used as the value of `<`, `>`, `=`, `!=`, `between`, or any other comparison operator — each function already encodes its own comparison (e.g. `isPast` means "column < NOW()"). If you need to combine a dynamic-date check with another condition, put them as separate items in an `and`/`or` group:
97
+
98
+ ```json
99
+ {
100
+ "g": "and",
101
+ "i": [
102
+ { "c": "due_date", "o": "f", "v": { "fn": "isPast" } },
103
+ { "c": "status", "o": "!=", "v": "done" }
104
+ ]
105
+ }
106
+ ```
107
+
108
+ These are **wrong** and will be rejected:
109
+
110
+ ```json
111
+ { "c": "due_date", "o": "<", "v": "@TODAY" }
112
+ { "c": "due_date", "o": "<", "v": { "fn": "isToday" } }
113
+ ```
114
+
115
+ ### Group operators
116
+
117
+ | Operator | SQL equivalent |
118
+ |---|---|
119
+ | `and` | `A AND B AND C` |
120
+ | `or` | `A OR B OR C` |
121
+ | `!and` | `NOT (A AND B AND C)` — at least one is false |
122
+ | `!or` | `NOT (A OR B OR C)` — none are true |
123
+
124
+ ## Where filters are used
125
+
126
+ ### 1. Folder row filters (`ui/folders.json`)
127
+
128
+ Scope which records are visible in a subfolder. Used in `rowFilter` property on entity membership:
129
+
130
+ ```json
131
+ "stock": {
132
+ "viewName": "default",
133
+ "quickAdd": true,
134
+ "rowFilter": { "c": "warehouse_id", "o": "eq", "v": 2001 }
135
+ }
136
+ ```
137
+
138
+ Generates: only stock records where `warehouse_id = 2001` appear in this folder.
139
+
140
+ For compound filters on a folder:
141
+
142
+ ```json
143
+ "rowFilter": {
144
+ "g": "and",
145
+ "i": [
146
+ { "c": "warehouse_id", "o": "eq", "v": 2001 },
147
+ { "c": "status", "o": "!=", "v": "archived" }
148
+ ]
149
+ }
150
+ ```
151
+
152
+ ### 2. Data view filters (`ui/data_views.json`)
153
+
154
+ Pre-filter which records appear in a data view:
155
+
156
+ ```json
157
+ {
158
+ "active_leads": {
159
+ "viewType": "grid",
160
+ "label": "Active Leads",
161
+ "dataSources": [{
162
+ "entityCode": "lead",
163
+ "columns": ["name", "email", "stage", "owner_id"],
164
+ "filter": {
165
+ "g": "and",
166
+ "i": [
167
+ { "c": "stage", "o": "!=", "v": "Closed Lost" },
168
+ { "c": "stage", "o": "!=", "v": "Converted" }
169
+ ]
170
+ },
171
+ "sort": [{ "column_cd": "created_date", "direction": "desc" }]
172
+ }]
173
+ }
174
+ }
175
+ ```
176
+
177
+ ### 3. Report dataset filters
178
+
179
+ Same format inside report dataset definitions:
180
+
181
+ ```json
182
+ "datasets": {
183
+ "ds_pipeline": {
184
+ "entityCode": "opportunity",
185
+ "columns": ["stage", "total_amount"],
186
+ "filter": {
187
+ "g": "and",
188
+ "i": [
189
+ { "c": "stage", "o": "!in", "v": ["Closed Won", "Closed Lost"] },
190
+ { "c": "total_amount", "o": ">", "v": 0 }
191
+ ]
192
+ },
193
+ "groupBy": ["stage"],
194
+ "aggregations": {
195
+ "total_value": { "func": "sum", "column": "total_amount" }
196
+ }
197
+ }
198
+ }
199
+ ```
200
+
201
+ ### 4. API filters (`data.get` calls)
202
+
203
+ Same format passed as the `filter` parameter in RPC calls:
204
+
205
+ ```json
206
+ {
207
+ "method": "data.get",
208
+ "params": {
209
+ "entityCode": "contact",
210
+ "filter": { "c": "account_id", "o": "=", "v": "some-uuid" },
211
+ "columns": ["first_name", "last_name", "email"]
212
+ }
213
+ }
214
+ ```
215
+
216
+ ### 5. Action DSL (`query()` is different!)
217
+
218
+ **Important**: the `query()` function in action DSL uses **raw parameterized SQL**, NOT the JSON filter format:
219
+
220
+ ```javascript
221
+ // This is SQL, not a JSON filter
222
+ var results = query('SELECT * FROM crm.contact WHERE account_id = @accId AND status = @status', {
223
+ accId: [account_id],
224
+ status: 'active'
225
+ })
226
+ ```
227
+
228
+ The JSON filter format is only used in **declarative contexts** (folders, views, reports, API calls). Action DSL uses SQL directly.
229
+
230
+ ## Common examples
231
+
232
+ ### Filter by status (single condition)
233
+
234
+ ```json
235
+ { "c": "status", "o": "=", "v": "active" }
236
+ ```
237
+
238
+ ### Exclude multiple statuses
239
+
240
+ ```json
241
+ { "c": "status", "o": "!in", "v": ["cancelled", "deleted", "archived"] }
242
+ ```
243
+
244
+ ### Date range
245
+
246
+ ```json
247
+ {
248
+ "g": "and",
249
+ "i": [
250
+ { "c": "created_date", "o": ">=", "v": "2026-01-01" },
251
+ { "c": "created_date", "o": "<", "v": "2026-04-01" }
252
+ ]
253
+ }
254
+ ```
255
+
256
+ Or using `between`:
257
+
258
+ ```json
259
+ { "c": "amount", "o": "between", "v": [1000, 5000] }
260
+ ```
261
+
262
+ ### Non-null check
263
+
264
+ ```json
265
+ { "c": "email", "o": "!null" }
266
+ ```
267
+
268
+ ### String matching
269
+
270
+ ```json
271
+ { "c": "invoice_number", "o": "start", "v": "INV-2026-" }
272
+ ```
273
+
274
+ ### Complex: active records in sales or marketing
275
+
276
+ ```json
277
+ {
278
+ "g": "and",
279
+ "i": [
280
+ { "c": "is_active", "o": "=", "v": true },
281
+ {
282
+ "g": "or",
283
+ "i": [
284
+ { "c": "department", "o": "=", "v": "sales" },
285
+ { "c": "department", "o": "=", "v": "marketing" }
286
+ ]
287
+ }
288
+ ]
289
+ }
290
+ ```
291
+
292
+ ### Column-to-column comparison
293
+
294
+ ```json
295
+ { "c": "ship_date", "o": "eqRef", "v": "order_date" }
296
+ ```
297
+
298
+ Generates: `WHERE ship_date = order_date`
299
+
300
+ ## Filter composition at query time
301
+
302
+ All filter layers are **ANDed together** automatically:
303
+
304
+ ```
305
+ Effective filter = folder_row_filter AND view_filter AND api_filter AND user_ad_hoc_filter
306
+ ```
307
+
308
+ The user never sees a record unless it passes **all** layers. You don't need to duplicate folder filters in view filters — they compose automatically.
309
+
310
+ ## Rules
311
+
312
+ - **`null` filter** (or omitted) means no filtering — show everything the user has access to.
313
+ - **Single condition** is valid without a group wrapper.
314
+ - Use **short operators** (`=`, `!=`, `>`) or **enum keys** (`eq`, `notEq`, `gr`) — both work.
315
+ - For folder `rowFilter` in module packages, prefer the short compact form: `{ "c": "...", "o": "eq", "v": ... }`.
316
+ - Dates are ISO strings: `"2026-01-01"`.
317
+ - Arrays for `in`/`between`: `["a", "b"]` or `[100, 500]`.
318
+
319
+ ## Common mistakes
320
+
321
+ - Using the JSON filter format inside `query()` in DSL actions — **wrong**. `query()` uses raw SQL.
322
+ - Wrapping a single condition in a group unnecessarily — **valid but noisy**. A bare condition is fine.
323
+ - Using `"operator": "equals"` — **wrong**. Use `"o": "="` or `"o": "eq"`.
324
+ - Using `"column": "status"` — **wrong**. Use `"c": "status"`.
325
+ - Forgetting that `null` means "no filter" (not "filter by null") — to filter for null values, use `{ "c": "field", "o": "null" }`.
326
+ - Putting a `{ "fn": "..." }` object (or `@TODAY`/`@CURRENT_USER` placeholder) as the value of a normal comparison like `<`, `>`, `=` — **wrong**. Functions are standalone conditions that only work with `"o": "f"`.
@@ -0,0 +1,73 @@
1
+ # Column Flags Reference
2
+
3
+ Column flags are single-letter codes concatenated into a string. Order doesn't matter — `"VEM"` and `"EMV"` are equivalent.
4
+
5
+ Valid flags regex: `^[VIEMH]*$`
6
+
7
+ ## Flag letters
8
+
9
+ | Letter | Name | Meaning |
10
+ |---|---|---|
11
+ | `V` | Visible | Column appears in grid / form / card views. Omit to hide from standard UI. |
12
+ | `I` | Internal | Auto-managed by the platform. Used on PK columns, audit timestamps, system fields. Not shown in UI, not directly editable by users. |
13
+ | `E` | Editable | User can modify the value in the UI. Omit for read-only. |
14
+ | `M` | Mandatory | Required (NOT NULL + UI enforcement). |
15
+ | `H` | Hidden | Permanently hidden — never shown, not even in admin metadata views. Stronger than omitting `V`. |
16
+
17
+ **Only these 5 letters are valid.** The platform rejects any other characters.
18
+
19
+ ## Common column flag combinations
20
+
21
+ | Flags | Meaning | Use case |
22
+ |---|---|---|
23
+ | `"VEM"` | Visible, Editable, Mandatory | Required field shown in UI (most business fields) |
24
+ | `"VE"` | Visible, Editable, optional | Nullable field |
25
+ | `"V"` | Visible, read-only | Display-only, like formula columns or computed values |
26
+ | `"EM"` | Editable, Mandatory, **not visible** | Hidden FK columns in the FK+Reference pattern |
27
+ | `"I"` | Internal | Trait-provided columns (PK, audit timestamps). Platform-managed. |
28
+ | `""` (empty) | No flags | Rare — column exists but is invisible and not editable |
29
+
30
+ ## Rules
31
+
32
+ 1. **Hidden FK columns**: always `"EM"`. They must be writeable (E) and required (M), but never shown (no V).
33
+ 2. **Visible Reference columns**: always `"VEM"`. The user sees and edits the lookup picker.
34
+ 3. **Formula columns**: usually `"V"` (visible, read-only). The user never edits them.
35
+ 4. **Trait columns** (PK, audit): use `"I"` (internal). These are set automatically by `traits: ["identity", "audit"]`.
36
+ 5. **Don't use `I` on your own columns** — it's reserved for trait-provided and platform-managed columns.
37
+ 6. **Don't mix `I` with `V` or `E`** — internal columns are not user-facing.
38
+
39
+ ## Entity rights flags (different namespace — roles only)
40
+
41
+ These letters appear in role `rights` declarations, **NOT** on column flags. Don't confuse them:
42
+
43
+ | Letter | Permission | Used on |
44
+ |---|---|---|
45
+ | `S` | Select (read rows) | Entity in role `rights` |
46
+ | `I` | Insert (create rows) | Entity in role `rights` |
47
+ | `U` | Update (modify rows) | Entity in role `rights` |
48
+ | `D` | Delete (remove rows) | Entity in role `rights` |
49
+ | `C` | Clone (duplicate row) | Entity in role `rights` |
50
+ | `E` | Execute | Actions, reports, folders in role `rights` |
51
+
52
+ So `"SIUD"` on a role grants full CRUD; `"SI"` grants read + create only.
53
+
54
+ **Note**: the `I` in column flags (Internal) and the `I` in role rights (Insert) are different concepts using the same letter. Context makes them unambiguous — column flags go on entity fields, role rights go on `security/roles.json`.
55
+
56
+ ## How uniqueness, PKs, and search work
57
+
58
+ These are **NOT** flag letters. They're separate properties on the column definition:
59
+
60
+ | Need | How to declare | NOT this |
61
+ |---|---|---|
62
+ | Primary key | `"isPk": true` (or use `identity` trait) | NOT a `P` flag |
63
+ | Unique column | Add a unique index in the entity's `indexes` block, or `"isUnique": true` | NOT a `U` flag |
64
+ | Searchable | Configure in data view columns or search settings | NOT an `S` flag |
65
+
66
+ ## Mistakes to avoid
67
+
68
+ - Using `"U"` for unique — **not a valid flag**. Use `"isUnique": true` or indexes.
69
+ - Using `"S"` for searchable — **not a valid flag**.
70
+ - Using `"P"` for primary key — **not a valid flag**. Use `"isPk": true` or the `identity` trait.
71
+ - Using `"VEM"` on trait-provided columns (PK, audit) — **don't**. Traits set their own flags (`"I"`).
72
+ - Using `"I"` on business columns — **don't**. `I` means "platform-managed"; regular business columns use `V`/`E`/`M`.
73
+ - Using long names like `"Visible,Editable"` — **wrong**. Single letters concatenated: `"VE"`.
@@ -0,0 +1,206 @@
1
+ # Formula Engine Reference
2
+
3
+ Formulas are used in:
4
+
5
+ - **Formula columns** (`columnType: "F"`) — computed values
6
+ - **Action `canExecute:` blocks** — availability checks
7
+ - **Default values** — a setting's `formula` (e.g. `"formula": "TODAY()"`) or a formula (`F`) column. Note: entity *data* columns have **no** `default`/`defaultValue` key — model a default with an `F` column or set it in action/trigger logic.
8
+ - **Filter expressions** (partially)
9
+ - **Validation expressions** (partially)
10
+
11
+ > These are **formula** contexts: date helpers are uppercase `TODAY()` / `NOW()` here. Action
12
+ > `execute:` blocks are **not** a formula context — they run as JavaScript and use lowercase
13
+ > `now()` (see `action-dsl.md`).
14
+
15
+ The same grammar applies everywhere.
16
+
17
+ ## Basic syntax
18
+
19
+ - **Field references**: `[column_name]` — the value of another column on the same entity
20
+ - **Navigation**: `[reference_column].[field]` — traverse a reference to a related entity's field
21
+ - **Literals**: numbers (`42`, `3.14`), strings (`'hello'` or `"hello"`), booleans (`true`, `false`), null (`null`)
22
+ - **Operators**: `+`, `-`, `*`, `/`, `%`, `==`, `!=`, `<`, `>`, `<=`, `>=`, `AND`, `OR`, `NOT`
23
+ - **Function calls**: `FUNCTION_NAME(arg1, arg2, ...)` — uppercase by convention
24
+ - **Parentheses** for grouping
25
+
26
+ ## Examples
27
+
28
+ ```
29
+ [first_name] + ' ' + [last_name]
30
+
31
+ [quantity] * [unit_price]
32
+
33
+ [account].[name]
34
+
35
+ [account].[billing_address].[country]
36
+
37
+ [status] == "active" AND [balance] > 0
38
+
39
+ CASE([priority], "high", 3, "medium", 2, "low", 1, 0)
40
+
41
+ IF([total] > 1000, "large", "small")
42
+
43
+ COALESCE([nickname], [first_name], "Unknown")
44
+
45
+ FORMAT([created_date], "yyyy-MM-dd")
46
+ ```
47
+
48
+ ## Built-in functions
49
+
50
+ ### String functions
51
+
52
+ - `CONCAT(a, b, ...)` — concatenate strings
53
+ - `LEN(s)` — string length
54
+ - `UPPER(s)`, `LOWER(s)` — case conversion
55
+ - `TRIM(s)`, `LTRIM(s)`, `RTRIM(s)` — whitespace removal
56
+ - `SUBSTRING(s, start, len)` — substring
57
+ - `REPLACE(s, find, with)` — replace substring
58
+ - `CONTAINS(s, find)` — boolean substring check
59
+ - `STARTSWITH(s, prefix)`, `ENDSWITH(s, suffix)` — boolean
60
+ - `FORMAT(value, pattern)` — format dates/numbers
61
+
62
+ ### Number functions
63
+
64
+ - `ABS(n)`, `ROUND(n, digits)`, `FLOOR(n)`, `CEIL(n)`, `CEILING(n)`
65
+ - `MIN(a, b)`, `MAX(a, b)` — binary min/max
66
+ - `POW(base, exp)`, `SQRT(n)`
67
+ - `MOD(a, b)` — modulo
68
+
69
+ ### Date functions
70
+
71
+ - `TODAY()` — current date
72
+ - `NOW()` — current datetime with timezone
73
+ - `YEAR(d)`, `MONTH(d)`, `DAY(d)` — date parts
74
+ - `HOUR(dt)`, `MINUTE(dt)`, `SECOND(dt)` — time parts
75
+ - `DATEADD(d, count, unit)` — add time (unit: `day`, `month`, `year`, `hour`, `minute`)
76
+ - `DATEDIFF(a, b, unit)` — difference between dates
77
+
78
+ ### Logical functions
79
+
80
+ - `IF(cond, then, else)` — ternary
81
+ - `CASE(expr, val1, result1, val2, result2, ..., default)` — multi-branch
82
+ - `COALESCE(a, b, c, ...)` — first non-null
83
+ - `NULLIF(a, b)` — null if equal, else a
84
+ - `ISNULL(x)` — boolean null check
85
+
86
+ ### Aggregation (on set columns)
87
+
88
+ - `COUNT([set_column])` — count related rows
89
+ - `SUM([set_column].[field])` — sum a child field over a set
90
+ - `AVG`, `MIN`, `MAX` similarly
91
+
92
+ Put set aggregations in a **Formula (`F`) column** — they evaluate at query time and may reference
93
+ any child column, including the child's own formula columns, e.g. `SUM([lines].[line_total])`.
94
+
95
+ > ⛔ Do **not** put a `SUM([set].[field])` in a **Generated (`G`)** column unless `field` is a
96
+ > *physical* (`D`) column. A `G` aggregate is maintained by a DB trigger that reads the child's
97
+ > `OLD`/`NEW` physical values; aggregating a virtual `F` child fails at install with
98
+ > `db_error: column old.<field> does not exist`. See `column-types.md` → "Roll-up totals over child rows".
99
+
100
+ ## Navigation (dot notation)
101
+
102
+ `[reference].[field]` traverses a reference column to a target entity:
103
+
104
+ ```
105
+ [account].[name] -- contact's account's name
106
+ [owner].[email] -- owner user's email
107
+ [account].[primary_contact].[phone] -- chained
108
+ ```
109
+
110
+ Navigation works through `columnType: "R"` columns (reference columns). Chains of length 1 are **synchronous** and resolved instantly. Chains of length ≥ 2 are **asynchronous** — the formula engine resolves them after the initial data load.
111
+
112
+ ## Sync vs async formulas
113
+
114
+ - **Sync formula**: pure local math, no navigation, or one-level navigation that can be JOINed. Evaluated on load and on every edit to a dependency.
115
+ - **Async formula**: multi-hop navigation. Evaluated after the initial data load, re-evaluated when dependencies change.
116
+
117
+ You don't declare which is which — the engine detects it from the formula's AST.
118
+
119
+ ## Setting references
120
+
121
+ In modules that use settings, formulas can reference setting values with `$[SettingName]` syntax:
122
+
123
+ ```
124
+ [total] * $[VAT_Rate]
125
+ ```
126
+
127
+ ## Validation and CHECK constraints
128
+
129
+ Check constraints use a subset of the formula grammar. The server parses them to AST and converts to SQL. Common patterns:
130
+
131
+ ```
132
+ [quantity] > 0
133
+ [end_date] >= [start_date]
134
+ [email] LIKE '%@%'
135
+ ```
136
+
137
+ ## Examples — full formula columns
138
+
139
+ ### Full name
140
+
141
+ ```json
142
+ "full_name": {
143
+ "columnType": "F",
144
+ "fieldTypeCd": "text",
145
+ "baseDatatypeCd": "string",
146
+ "flags": "V",
147
+ "orderNum": 25,
148
+ "formula": "[first_name] + ' ' + [last_name]",
149
+ "description": "Full Name"
150
+ }
151
+ ```
152
+
153
+ ### Line total (quantity × price)
154
+
155
+ ```json
156
+ "line_total": {
157
+ "columnType": "F",
158
+ "fieldTypeCd": "currency",
159
+ "baseDatatypeCd": "number",
160
+ "flags": "V",
161
+ "orderNum": 60,
162
+ "formula": "[quantity] * [unit_price]",
163
+ "description": "Line Total"
164
+ }
165
+ ```
166
+
167
+ ### Customer country (via navigation)
168
+
169
+ ```json
170
+ "customer_country": {
171
+ "columnType": "F",
172
+ "fieldTypeCd": "text",
173
+ "baseDatatypeCd": "string",
174
+ "flags": "V",
175
+ "orderNum": 70,
176
+ "formula": "[account].[billing_country]",
177
+ "description": "Customer Country"
178
+ }
179
+ ```
180
+
181
+ ### Days since created
182
+
183
+ ```json
184
+ "days_open": {
185
+ "columnType": "F",
186
+ "fieldTypeCd": "number",
187
+ "baseDatatypeCd": "number",
188
+ "flags": "V",
189
+ "orderNum": 200,
190
+ "formula": "DATEDIFF([created_date], NOW(), 'day')",
191
+ "description": "Days Open"
192
+ }
193
+ ```
194
+
195
+ ## Common mistakes
196
+
197
+ - Using `column_name` without brackets — **wrong**. Always `[column_name]`.
198
+ - Inventing functions like `HAS_PERMISSION()`, `IS_ADMIN()`, `GET_USER()` — **do not exist**. Do not use unless confirmed to exist in the actual dForge engine.
199
+ - Using JavaScript syntax like `row.field` or `this.field` — **wrong**. Only `[field]`.
200
+ - Forgetting `baseDatatypeCd` on formula columns — **required**. Without it, filters and SQL don't work.
201
+ - Using SQL syntax like `SELECT`, `JOIN`, `WHERE` — **wrong**. Formulas are expressions, not queries.
202
+ - String concatenation with commas — **wrong**. Use `+` or `CONCAT()`.
203
+
204
+ ## Reference
205
+
206
+ This file covers the most common formula functions and patterns. If you encounter an edge case not covered here, ask the user to check their dForge version's formula documentation.