@dforge-core/dforge-mcp 0.1.0-rc.9 → 0.1.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.
- package/CHANGELOG.md +166 -0
- package/README.md +88 -29
- package/dist/server.js +2735 -351
- package/docs/creating-modules.md +15 -7
- package/package.json +11 -6
- package/resources/docs/conventions.md +22 -16
- package/resources/docs/dsl-reference.md +767 -0
- package/resources/schemas/entity.schema.json +8 -1
- package/resources/schemas/print-templates.schema.json +82 -0
- package/resources/schemas/reports.schema.json +4 -0
- package/resources/schemas/triggers.schema.json +59 -0
- package/skills/dforge-mcp-author/SKILL.md +284 -73
- package/skills/dforge-mcp-author/examples/matrix-budget/README.md +43 -0
- package/skills/dforge-mcp-author/examples/matrix-budget/entities/budget_category.json +24 -0
- package/skills/dforge-mcp-author/examples/matrix-budget/entities/budget_line.json +56 -0
- package/skills/dforge-mcp-author/examples/matrix-budget/manifest.json +30 -0
- package/skills/dforge-mcp-author/examples/matrix-budget/security/roles.json +9 -0
- package/skills/dforge-mcp-author/examples/matrix-budget/seed-data/01-categories.json +8 -0
- package/skills/dforge-mcp-author/examples/matrix-budget/ui/data_views.json +42 -0
- package/skills/dforge-mcp-author/examples/simple-todo/README.md +38 -0
- package/skills/dforge-mcp-author/examples/simple-todo/entities/todo_item.json +83 -0
- package/skills/dforge-mcp-author/examples/simple-todo/entities/todo_list.json +43 -0
- package/skills/dforge-mcp-author/examples/simple-todo/logic/actions/mark_done.dsl +6 -0
- package/skills/dforge-mcp-author/examples/simple-todo/manifest.json +32 -0
- package/skills/dforge-mcp-author/examples/simple-todo/security/roles.json +10 -0
- package/skills/dforge-mcp-author/examples/simple-todo/seed-data/01-lists.json +17 -0
- package/skills/dforge-mcp-author/examples/simple-todo/ui/actions.json +11 -0
- package/skills/dforge-mcp-author/examples/simple-todo/ui/data_views.json +35 -0
- package/skills/dforge-mcp-author/examples/simple-todo/ui/menus.json +28 -0
- package/skills/dforge-mcp-author/references/action-dsl.md +397 -0
- package/skills/dforge-mcp-author/references/column-types.md +168 -0
- package/skills/dforge-mcp-author/references/conventions.md +177 -0
- package/skills/dforge-mcp-author/references/data-migration.md +270 -0
- package/skills/dforge-mcp-author/references/data-views.md +245 -0
- package/skills/dforge-mcp-author/references/excel-import.md +61 -0
- package/skills/dforge-mcp-author/references/field-types.md +144 -0
- package/skills/dforge-mcp-author/references/filters.md +326 -0
- package/skills/dforge-mcp-author/references/flags.md +73 -0
- package/skills/dforge-mcp-author/references/formulas.md +206 -0
- package/skills/dforge-mcp-author/references/jobs.md +149 -0
- package/skills/dforge-mcp-author/references/manifest.md +123 -0
- package/skills/dforge-mcp-author/references/menus.md +164 -0
- package/skills/dforge-mcp-author/references/number-sequences.md +117 -0
- package/skills/dforge-mcp-author/references/print-templates.md +159 -0
- package/skills/dforge-mcp-author/references/queries.md +312 -0
- package/skills/dforge-mcp-author/references/reports.md +398 -0
- package/skills/dforge-mcp-author/references/schema-import.md +331 -0
- package/skills/dforge-mcp-author/references/security.md +244 -0
- package/skills/dforge-mcp-author/references/settings.md +120 -0
- package/skills/dforge-mcp-author/references/traits.md +153 -0
- package/skills/dforge-mcp-author/references/translations.md +158 -0
- package/skills/dforge-mcp-author/references/validation-checklist.md +184 -0
- 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.
|