@dforge-core/dforge-mcp 0.2.47 → 0.2.48
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 +34 -0
- package/package.json +2 -1
- package/skills/dforge-1c-import/scripts/README.md +27 -0
- package/skills/dforge-1c-import/scripts/lib/__pycache__/attrs.cpython-313.pyc +0 -0
- package/skills/dforge-1c-import/scripts/lib/__pycache__/delta.cpython-313.pyc +0 -0
- package/skills/dforge-1c-import/scripts/lib/__pycache__/meta.cpython-313.pyc +0 -0
- package/skills/dforge-1c-import/scripts/lib/__pycache__/migrate.cpython-313.pyc +0 -0
- package/skills/dforge-1c-import/scripts/lib/__pycache__/paths.cpython-313.pyc +0 -0
- package/skills/dforge-1c-import/scripts/lib/__pycache__/posted.cpython-313.pyc +0 -0
- package/skills/dforge-1c-import/scripts/lib/__pycache__/posting.cpython-313.pyc +0 -0
- package/skills/dforge-1c-import/scripts/lib/__pycache__/seedstate.cpython-313.pyc +0 -0
- package/skills/dforge-1c-import/scripts/lib/__pycache__/store.cpython-313.pyc +0 -0
- package/skills/dforge-1c-import/scripts/lib/__pycache__/totals.cpython-313.pyc +0 -0
- package/skills/dforge-1c-import/scripts/lib/__pycache__/transactions.cpython-313.pyc +0 -0
- package/skills/dforge-1c-import/scripts/lib/delta.py +40 -12
- package/skills/dforge-1c-import/scripts/lib/totals.py +2 -4
- package/skills/dforge-1c-import/scripts/lib/transactions.py +28 -0
- package/skills/dforge-mcp-author/references/action-dsl.md +48 -45
- package/skills/dforge-mcp-author/references/formulas.md +6 -5
- package/skills/dforge-mcp-author/references/jobs.md +1 -1
- package/skills/dforge-mcp-author/references/registers.md +2 -0
- package/skills/dforge-mcp-author/references/security.md +2 -0
- package/skills/dforge-mcp-author/references/validation-checklist.md +6 -8
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,40 @@ the release workflow, so committed `package.json` versions are placeholders.
|
|
|
6
6
|
|
|
7
7
|
## Unreleased
|
|
8
8
|
|
|
9
|
+
**1C migration readiness and regressions.** `delta --apply` now reports per-module
|
|
10
|
+
`ready` / `partial` status and exits 2 if state, totals or imported markers remain
|
|
11
|
+
unsettled; retained retry markers are settled on a later run even without new rows.
|
|
12
|
+
Dry runs do not certify readiness. `pnpm test:1c` covers conversion precision,
|
|
13
|
+
reference/date semantics, failed settlement/retry and the per-entity reload restart
|
|
14
|
+
contract using synthetic fixtures and mocked database boundaries. The loader remains
|
|
15
|
+
a non-atomic migration-target reload; its restart procedure is documented.
|
|
16
|
+
A new CI job (`.github/workflows/onec-tests.yml`) runs it on changes to the 1C scripts or tests. A module's retry
|
|
17
|
+
markers (`df1c.unsettled`) are now cleared only for the entities it settled, not for the whole module.
|
|
18
|
+
Placeholder, balance-state and yearly totals writers roll back failed transactions before
|
|
19
|
+
restoring autocommit; settlement continues to the next module only after connection
|
|
20
|
+
recovery, and failed recovery stops the command. Strict transaction-state tests cover
|
|
21
|
+
original-error preservation, partial yearly commits, continuation and unrecoverable connections.
|
|
22
|
+
Jobs authoring/schema guidance now describes `idempotencyKey` as reserved and not enforced.
|
|
23
|
+
|
|
24
|
+
**Action scripts bypass entity rights.** `action-dsl.md` and `security.md` now state that `insert()` / `update()` /
|
|
25
|
+
`delete()` / `query()` in an action script check neither entity rights nor column security nor the folder's row filter
|
|
26
|
+
(core #1580): only `E` on the action is checked, so treat `E` as write access to everything the script touches.
|
|
27
|
+
|
|
28
|
+
**`canExecute` is enforced in full on the server.** `action.execute` now computes arithmetic, `IN`, `BETWEEN`, formula
|
|
29
|
+
functions, a bare boolean column, `[ref].[col]` navigation and extension columns in SQL, instead of letting them through, and
|
|
30
|
+
`TODAY()` names the requester's day. A condition it cannot compute refuses the action. Install rejects `$[setting]`,
|
|
31
|
+
`SUM` / `COUNT` / `AVG` and unknown function names in `canExecute`, so a module that used them stops installing: read
|
|
32
|
+
settings and count children in `execute:` instead (a count belongs in a `G` column). `references/action-dsl.md`
|
|
33
|
+
(`canExecute:`), `formulas.md` and `validation-checklist.md` are rewritten to match. The button still evaluates over the
|
|
34
|
+
grid row, which has no extension columns and throws on `[ref].[col]`, so keep both out of a gate that must also disable the
|
|
35
|
+
button. **Needs platform 1.28.12.**
|
|
36
|
+
|
|
37
|
+
**Registers: `postAction` / `unpostAction`.** `references/registers.md` documents the register-binding column params that make a direct
|
|
38
|
+
`data.insert` / `data.update` flipping the state flag need `E` on the post or unpost action, and the "May edit is not may
|
|
39
|
+
post" pitfall: declare both and grant `E` only to the roles that may post. Each direction is checked only when its own key
|
|
40
|
+
is set; actions and DSL `update()` are not re-checked. A binding with neither gets a pack / validate warning, silenced by
|
|
41
|
+
`"editorMayPost": true`. **Needs a platform release after 1.28.12 (not in it).**
|
|
42
|
+
|
|
9
43
|
## 0.2.46 — 2026-10-29
|
|
10
44
|
|
|
11
45
|
Requires `@dforge-core/dforge-cli` 0.2.37 and `@dforge-core/metadata` 0.0.47; the vendored `dforge://schema/data-views` now accepts the grouping keys.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dforge-core/dforge-mcp",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.48",
|
|
4
4
|
"description": "MCP server for dForge module authoring. Exposes 35 scaffold/patch/validate/pack/install tools and schema resources so AI agents (Claude Code, Cursor, Zed) can create and ship dForge modules.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"homepage": "https://github.com/iash44/dForge-core",
|
|
@@ -42,6 +42,7 @@
|
|
|
42
42
|
"sync-schemas": "node scripts/vendor-schemas.cjs",
|
|
43
43
|
"typecheck": "tsc --noEmit",
|
|
44
44
|
"test": "vitest run",
|
|
45
|
+
"test:1c": "python3 -m unittest discover -s test/onec -v",
|
|
45
46
|
"install-skills": "node scripts/install-skills.mjs"
|
|
46
47
|
}
|
|
47
48
|
}
|
|
@@ -162,3 +162,30 @@ reproducible.
|
|
|
162
162
|
This copy is kept in step with the port it was developed on (`tools/lib`, `tools/1c.py`
|
|
163
163
|
there). Fixes made in a port go back to the library here. A project's own files never
|
|
164
164
|
come here.
|
|
165
|
+
|
|
166
|
+
### Regression checks and restart contract
|
|
167
|
+
|
|
168
|
+
From the MCP repository root: `pnpm test:1c` (Python 3, standard library only).
|
|
169
|
+
These tests use synthetic inputs and mocked database boundaries, never a tenant or
|
|
170
|
+
source infobase. They cover conversions, COPY encoding, settlement failure/retry and
|
|
171
|
+
the initial loader's committed-per-entity restart contract; they do not replace
|
|
172
|
+
Parquet/DuckDB/PostgreSQL integration or real source-to-target reconciliation.
|
|
173
|
+
|
|
174
|
+
`delta --apply` prints each module's `ready` or `partial` status. Exit 0 means all
|
|
175
|
+
selected modules settled; exit 2 means data may have committed but settlement is
|
|
176
|
+
incomplete (state/totals drift or markers not converged, including `--no-mark`
|
|
177
|
+
when markers are needed). Resolve the reported problem and rerun `--apply`;
|
|
178
|
+
persisted unsettled entities are checked even when the new delta has no changes.
|
|
179
|
+
Dry runs report `dry_run` and do not certify readiness. Settlement write failures
|
|
180
|
+
roll back the current transaction before restoring autocommit; ordinary failures
|
|
181
|
+
report `partial` and allow the next module to run on a clean connection. Failure to
|
|
182
|
+
rollback or restore the connection stops the command immediately. Previously
|
|
183
|
+
committed entities/years remain committed; a rollback does not undo the whole import.
|
|
184
|
+
Exit 0 does not replace the wider migration reconciliation.
|
|
185
|
+
|
|
186
|
+
`migrate run --truncate` is a destructive reload into an isolated migration target,
|
|
187
|
+
not an atomic refresh of a live application: clearing commits before loading and
|
|
188
|
+
each entity commits separately. If interrupted, keep users out, repair the cause,
|
|
189
|
+
rerun the same complete selection with `--truncate` from the same export, then
|
|
190
|
+
reconcile again. Do not use an append rerun to recover a partially loaded target.
|
|
191
|
+
For an active target, load/reconcile a separate database and switch only afterward.
|
|
Binary file
|
|
@@ -56,6 +56,7 @@ taken for a deletion. While 1C is the system of record, nothing is entered in th
|
|
|
56
56
|
from __future__ import print_function
|
|
57
57
|
|
|
58
58
|
import argparse
|
|
59
|
+
import collections
|
|
59
60
|
import datetime
|
|
60
61
|
import re
|
|
61
62
|
import struct
|
|
@@ -65,6 +66,7 @@ import time
|
|
|
65
66
|
import migrate
|
|
66
67
|
import paths
|
|
67
68
|
import store
|
|
69
|
+
import transactions
|
|
68
70
|
|
|
69
71
|
SCHEMA = "df1c"
|
|
70
72
|
KEYS_IN_LIST = 500
|
|
@@ -515,6 +517,14 @@ def begin(cur):
|
|
|
515
517
|
|
|
516
518
|
# --------------------------------------------------------------------------- after a module
|
|
517
519
|
|
|
520
|
+
SettleResult = collections.namedtuple("SettleResult", "status notes")
|
|
521
|
+
|
|
522
|
+
|
|
523
|
+
def settlement_exit_code(outcomes, apply):
|
|
524
|
+
"""Dry runs only estimate; an applied but unfinished module must block cutover."""
|
|
525
|
+
return 2 if apply and any(o.status != "ready" for o in outcomes) else 0
|
|
526
|
+
|
|
527
|
+
|
|
518
528
|
def settle(pg, module, results, apply, mark_documents):
|
|
519
529
|
"""What a module needs once its rows moved: placeholders, balances, markers."""
|
|
520
530
|
import posted
|
|
@@ -526,11 +536,11 @@ def settle(pg, module, results, apply, mark_documents):
|
|
|
526
536
|
notes = []
|
|
527
537
|
settled = True
|
|
528
538
|
if apply and touched:
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
539
|
+
with transactions.writing(pg):
|
|
540
|
+
cur = pg.cursor()
|
|
541
|
+
cur.execute("SET LOCAL session_replication_role = 'replica'")
|
|
542
|
+
filled = migrate.fill_missing(pg, schemas, verbose=False)
|
|
543
|
+
pg.commit()
|
|
534
544
|
if filled:
|
|
535
545
|
notes.append("%d placeholder(s)" % sum(f[4] for f in filled))
|
|
536
546
|
|
|
@@ -539,9 +549,8 @@ def settle(pg, module, results, apply, mark_documents):
|
|
|
539
549
|
if reg.journal_entity not in touched:
|
|
540
550
|
continue
|
|
541
551
|
if apply:
|
|
542
|
-
pg
|
|
543
|
-
|
|
544
|
-
pg.autocommit = True
|
|
552
|
+
with transactions.writing(pg):
|
|
553
|
+
rows, written, _ = seedstate.seed(pg, reg)
|
|
545
554
|
off = seedstate.drift(pg.cursor(), reg)
|
|
546
555
|
settled = settled and not off
|
|
547
556
|
notes.append("reseeded %s.%s: %d tuple(s), check %s"
|
|
@@ -587,8 +596,9 @@ def settle(pg, module, results, apply, mark_documents):
|
|
|
587
596
|
else:
|
|
588
597
|
notes.append("would converge markers on %d document table(s)" % len(work))
|
|
589
598
|
if apply and touched and settled:
|
|
590
|
-
pg.cursor().execute("DELETE FROM %s.unsettled WHERE module = %%s
|
|
591
|
-
|
|
599
|
+
pg.cursor().execute("DELETE FROM %s.unsettled WHERE module = %%s AND entity = ANY(%%s)"
|
|
600
|
+
% SCHEMA, (module, sorted(touched)))
|
|
601
|
+
return SettleResult("dry_run" if not apply else ("ready" if settled else "partial"), notes)
|
|
592
602
|
|
|
593
603
|
|
|
594
604
|
# --------------------------------------------------------------------------- command
|
|
@@ -733,6 +743,7 @@ def main():
|
|
|
733
743
|
print("-" * len(header))
|
|
734
744
|
totals = [0, 0, 0, 0, 0.0]
|
|
735
745
|
all_notes = []
|
|
746
|
+
outcomes = []
|
|
736
747
|
for module in modules:
|
|
737
748
|
results = []
|
|
738
749
|
for m, job in plan:
|
|
@@ -753,7 +764,20 @@ def main():
|
|
|
753
764
|
res.owners if kind in ("lines", "movements") else "",
|
|
754
765
|
res.changed, res.new, res.deleted, res.read, res.compare, res.write))
|
|
755
766
|
sys.stdout.flush()
|
|
756
|
-
|
|
767
|
+
try:
|
|
768
|
+
outcome = settle(pg, module, results, args.apply, jwt if args.apply else None)
|
|
769
|
+
except transactions.ConnectionRecoveryError:
|
|
770
|
+
raise # Never continue on a connection whose cleanup failed.
|
|
771
|
+
except Exception as failure:
|
|
772
|
+
if not args.apply:
|
|
773
|
+
raise
|
|
774
|
+
transactions.recover(pg)
|
|
775
|
+
outcome = SettleResult("partial", ["settlement FAILED: %s: %s"
|
|
776
|
+
% (type(failure).__name__, failure)])
|
|
777
|
+
outcomes.append(outcome)
|
|
778
|
+
all_notes.append("%s: status %s%s" % (module, outcome.status,
|
|
779
|
+
"" if args.apply else " (estimate only, not a readiness verdict)"))
|
|
780
|
+
for note in outcome.notes:
|
|
757
781
|
all_notes.append("%s: %s" % (module, note))
|
|
758
782
|
print("-" * len(header))
|
|
759
783
|
print("%d entities changed %d new %d deleted %d (owners/recorders replaced %d)"
|
|
@@ -763,7 +787,11 @@ def main():
|
|
|
763
787
|
print(" " + note)
|
|
764
788
|
print("(rows of an entity with nothing changed and under 2s are not listed;"
|
|
765
789
|
" lines/movements count rows replaced as new/deleted)")
|
|
766
|
-
|
|
790
|
+
code = settlement_exit_code(outcomes, args.apply)
|
|
791
|
+
if code:
|
|
792
|
+
print("delta incomplete (%s): do not cut over; resolve the notes and rerun --apply."
|
|
793
|
+
% ", ".join(m for m, o in zip(modules, outcomes) if o.status != "ready"))
|
|
794
|
+
return code
|
|
767
795
|
|
|
768
796
|
|
|
769
797
|
if __name__ == "__main__":
|
|
@@ -50,6 +50,7 @@ import sys
|
|
|
50
50
|
import time
|
|
51
51
|
|
|
52
52
|
import migrate
|
|
53
|
+
import transactions
|
|
53
54
|
|
|
54
55
|
GRANULARITIES = ("day", "month", "quarter", "year")
|
|
55
56
|
|
|
@@ -334,8 +335,7 @@ def rebuild(pg, plan):
|
|
|
334
335
|
|
|
335
336
|
insert = ("INSERT INTO %s (%s) VALUES (%s)"
|
|
336
337
|
% (plan.totals, ", ".join(key + cols), ", ".join(["%s"] * (len(key) + len(cols)))))
|
|
337
|
-
pg
|
|
338
|
-
try:
|
|
338
|
+
with transactions.writing(pg):
|
|
339
339
|
for year in years:
|
|
340
340
|
cur.execute("SET LOCAL session_replication_role = 'replica'")
|
|
341
341
|
cur.execute("DELETE FROM %s WHERE %s >= %%s AND %s < %%s"
|
|
@@ -344,8 +344,6 @@ def rebuild(pg, plan):
|
|
|
344
344
|
if by_year.get(year):
|
|
345
345
|
cur.executemany(insert, by_year[year])
|
|
346
346
|
pg.commit()
|
|
347
|
-
finally:
|
|
348
|
-
pg.autocommit = True
|
|
349
347
|
print("%-28s %7d rows written over %d year(s) checks %.0fs, fold %.0fs, write %.0fs"
|
|
350
348
|
% (name(plan), len(rows), len(years), scanned - started, read - scanned,
|
|
351
349
|
time.time() - read))
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"""Transaction cleanup shared by the migration settlement writers."""
|
|
2
|
+
from contextlib import contextmanager
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class ConnectionRecoveryError(RuntimeError):
|
|
6
|
+
"""The connection cannot safely be reused; abort rather than try another module."""
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def recover(pg):
|
|
10
|
+
"""Rollback before changing mode: psycopg2 forbids it in an active transaction."""
|
|
11
|
+
try:
|
|
12
|
+
pg.rollback()
|
|
13
|
+
pg.autocommit = True
|
|
14
|
+
except Exception as error:
|
|
15
|
+
raise ConnectionRecoveryError("cannot restore migration connection: %s" % error) from error
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@contextmanager
|
|
19
|
+
def writing(pg):
|
|
20
|
+
"""Writers commit explicitly; on any failure, rollback before restoring autocommit."""
|
|
21
|
+
pg.autocommit = False
|
|
22
|
+
try:
|
|
23
|
+
yield
|
|
24
|
+
except BaseException:
|
|
25
|
+
recover(pg)
|
|
26
|
+
raise
|
|
27
|
+
else:
|
|
28
|
+
pg.autocommit = True
|
|
@@ -117,53 +117,54 @@ Two consequences worth designing around:
|
|
|
117
117
|
- **`canExecute: false` is the idiom for an action only automation may fire.** The
|
|
118
118
|
server refuses it through `action.execute`; triggers and scheduled jobs invoke
|
|
119
119
|
the script engine directly and are unaffected.
|
|
120
|
-
- **
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
`[
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
120
|
+
- **The server evaluates the whole formula, and refuses what it cannot read.**
|
|
121
|
+
(Needs platform 1.28.12; older platforms let these through, so the gate is not enforced there.)
|
|
122
|
+
Comparisons between fields and literals (with `AND` / `OR` / `NOT`, a bare
|
|
123
|
+
`true` / `false`) are checked in-process; anything richer — arithmetic, `IN`,
|
|
124
|
+
`BETWEEN`, formula functions, a bare boolean field, `[ref].[col]`
|
|
125
|
+
navigation — is computed in SQL over the record, by the same translator as formula
|
|
126
|
+
columns (chained hops are followed up to a depth cap; a null ref gives `NULL`, which
|
|
127
|
+
counts as not true). A condition the server cannot evaluate (`$[setting]` references, set
|
|
128
|
+
aggregates `SUM` / `COUNT` / `AVG`, an unknown function name, a typo included)
|
|
129
|
+
**refuses the action** with `ACTION_EXECUTION_FAIL` ("cannot be evaluated by the
|
|
130
|
+
server") instead of allowing it. A `NULL` result counts as not true.
|
|
131
|
+
Comparisons themselves are null-safe and two-valued (see `formulas.md`), so an empty
|
|
132
|
+
field does not make `NOT ([x] = 'A')` unknown.
|
|
133
|
+
- **The client has gaps of its own, and its failures enable the button.** It
|
|
134
|
+
throws (and its `catch` **enables** the button) on **ref navigation**
|
|
135
|
+
(`[ref].target`), on **`SUM` / `COUNT` / `AVG`** (deliberately not formula
|
|
136
|
+
functions — counting is a Generated `G` column's job) and on **any name it does
|
|
137
|
+
not know, a typo included** (install refuses the last two, see below).
|
|
138
|
+
- **`$[setting]` references** resolve to `null` in the client (an ordered comparison
|
|
139
|
+
against null is `false`, so it tends to *disable* the button) and are refused by
|
|
140
|
+
the server and at install (platform 1.28.12 and later). Keep settings out of `canExecute` and read them in `execute:` instead.
|
|
141
|
+
- **`TODAY()`** names the requester's calendar day on the server too (their time
|
|
142
|
+
zone: the request's, else the user profile's, else the workspace's, else UTC; a profile zone of
|
|
143
|
+
`UTC` counts as unset). The button is evaluated in the browser and is meant to name the same
|
|
144
|
+
day; `execute:` stays the authority.
|
|
145
|
+
- **Install refuses** a `canExecute` that uses `$[setting]`, `SUM` / `COUNT` / `AVG`
|
|
146
|
+
or an unknown function name (a typo included), since the server could never run
|
|
147
|
+
the action. Read settings and count children in `execute:` instead.
|
|
148
|
+
|
|
149
|
+
| construct | client | server |
|
|
150
|
+
|---|---|---|
|
|
151
|
+
| comparisons between fields and literals, `AND` / `OR` / `NOT`, bare `true` / `false` | evaluates | evaluates |
|
|
152
|
+
| arithmetic, `IN`, `BETWEEN`, formula functions, bare boolean field | evaluates | evaluates (SQL) |
|
|
153
|
+
| `[ref].[col]` navigation | **throws, button enabled** | evaluates (SQL) |
|
|
154
|
+
| `SUM` / `COUNT` / `AVG`, unknown function (a typo included) | **throws, button enabled** | **refused at install**; if one is already installed, `action.execute` refuses it |
|
|
155
|
+
| `$[setting]` references | evaluates as null | **refused at install**; if already installed, `action.execute` refuses it |
|
|
156
|
+
|
|
157
|
+
Both evaluators agreeing is the healthy shape; the one to design around is a
|
|
158
|
+
construct the client throws on, which leaves a button that is enabled and then
|
|
159
|
+
refused. Count with a `G` column and compare the scalar (`[detail_count] > 0`);
|
|
160
|
+
denormalise instead of navigating; put anything genuinely unexpressible in
|
|
161
|
+
`execute:` behind `error()`.
|
|
160
162
|
|
|
161
163
|
> Never make `canExecute` the *only* guard for something that must hold. Between
|
|
162
|
-
> the
|
|
163
|
-
>
|
|
164
|
-
>
|
|
165
|
-
>
|
|
166
|
-
> first write.
|
|
164
|
+
> the enqueue-only check on background runs and the fact that the client evaluates
|
|
165
|
+
> over the grid row (which does not carry extension columns) while the server
|
|
166
|
+
> evaluates over a full record load, the authoritative check belongs in
|
|
167
|
+
> `execute:` — validate and `error()` before the first write.
|
|
167
168
|
|
|
168
169
|
```
|
|
169
170
|
canExecute:
|
|
@@ -226,6 +227,8 @@ var note = params[note] // no quotes around param name in this DSL
|
|
|
226
227
|
|
|
227
228
|
### Data operations
|
|
228
229
|
|
|
230
|
+
**No entity rights are checked inside a script.** `insert()`, `update()`, `delete()` and `query()` do not look at the signed-in user's `S`/`I`/`U`/`D` on the entity, nor at column security or the folder's row filter. Only `E` on the action is checked, when it is run. A user with `S` only on an entity can still change it through an action they may execute (core #1580). Treat `E` on an action as write access to everything its script touches; to keep a log free of changes, grant `E` on no action that writes to it.
|
|
231
|
+
|
|
229
232
|
| Function | Returns | Description |
|
|
230
233
|
|---|---|---|
|
|
231
234
|
| `insert('entity', { field: value, … })` | Inserted row object | Insert a new record. Returns the full row including auto-filled columns (PK, audit, number sequences). Access returned fields with `result.field_name`. |
|
|
@@ -11,11 +11,12 @@ Formulas are used in:
|
|
|
11
11
|
`F` column otherwise: it must parse, call only implemented functions, and not
|
|
12
12
|
aggregate over a 1:N set. See `security.md` → Column-level security.
|
|
13
13
|
- **Action `canExecute:` blocks** — availability checks. Evaluated on both sides
|
|
14
|
-
and **enforced** by `action.execute
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
section in
|
|
14
|
+
and **enforced** by `action.execute`: the server computes the whole formula
|
|
15
|
+
(functions, arithmetic, navigation included) and **refuses** the call when
|
|
16
|
+
it cannot read it (`$[setting]` reads, `SUM` / `COUNT` / `AVG`, unknown
|
|
17
|
+
functions). The client throws on navigation, set aggregates and unknown names,
|
|
18
|
+
which leaves the button enabled. See the `canExecute:` section in
|
|
19
|
+
[action-dsl.md](action-dsl.md).
|
|
19
20
|
- **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.
|
|
20
21
|
- **Filter expressions** (partially)
|
|
21
22
|
- **Validation expressions** (partially)
|
|
@@ -33,7 +33,7 @@ Lives in: `logic/jobs.json` at the module root. Schema: [`docs/schemas/jobs.sche
|
|
|
33
33
|
| `timeout` | Yes | | Hard timeout in seconds. Range `(0, 3600]`. **No implicit default.** `timeout > 300` requires `class: "long_running"`. |
|
|
34
34
|
| `class` | No | `"standard"` | `"standard"` (≤ 300 s) or `"long_running"` (≤ 3600 s). Explicit opt-in for heavy jobs. |
|
|
35
35
|
| `concurrency` | No | `1` | Max parallel runs `[1, 5]`. `1` = single-instance. |
|
|
36
|
-
| `idempotencyKey` | No | |
|
|
36
|
+
| `idempotencyKey` | No | | **Not enforced yet.** Accepted and stored; the Scheduler does not read it. Scheduled slots deduplicate `(job, scheduled_at)`, not business keys across slots. Guard business idempotency inside the action; manual triggers skip the slot check. |
|
|
37
37
|
| `timeZone` | No | tenant tz | IANA name. Falls back to `auth.tenant.time_zone`. |
|
|
38
38
|
| `params` | No | `{}` | Static parameters passed to the action as `__params`. Manifest-fixed — an admin cannot change them. For tenant-configurable values use module settings, resolved from the job's folder (below). |
|
|
39
39
|
| `enabled` | No | `true` | `false` fully disables — no scheduler, no manual trigger. |
|
|
@@ -188,6 +188,7 @@ A register column is a `bool` state column. `A` writes `balance`, `turnover` and
|
|
|
188
188
|
| `lineDateField` | On a line posting, a line column dating that line's movement instead of `dateField`; an empty one falls back to `dateField`. With `periodEntity` each line's period must be open, judged by the movement's stamped date (line's or document's, after `dateTrunc`); lines skipped by `skipPostingWhenEmpty` don't count. On a `recordChangesOnly` info register, one line per key (several dates on a key are refused). A `date` or zoneless `timestamp`, in `lockedLineFields` (and `groupLinesBy` when grouped), not with `periodDimField`. |
|
|
189
189
|
| `skipPostingWhenEmpty` | Dimensions whose emptiness means "this row posts nothing". The exception, see below. |
|
|
190
190
|
| `lockedFields` / `lockedLineFields` | Columns frozen while posted. `["*"]` freezes the whole row. |
|
|
191
|
+
| `postAction` / `unpostAction` | Params of the binding column (an `A` or `L` column), codes of this entity's actions that post and unpost the document. A direct `data.insert` / `data.update` that turns the state flag on or off needs `E` on the one for its direction, as the button does; actions and DSL `update()` are not re-checked. Each direction is gated on its own: with only one declared, the other direction needs just Insert/Update. A binding with neither gets a pack / validate warning, silenced by `"editorMayPost": true` (not allowed together with an action name). A name no action declared for the entity in `ui/actions.json` carries is refused by `module pack` / validate and by the write. **Unreleased: not in platform 1.28.12.** |
|
|
191
192
|
| `movementMapping` | Journal attribute column to document or line column. |
|
|
192
193
|
|
|
193
194
|
### `map` value forms
|
|
@@ -286,6 +287,7 @@ For a history loaded from another system, an `accounting` register may declare `
|
|
|
286
287
|
- **One constant for two resources.** Two resources mapped to the same constant read as two resources storing into one column and are refused.
|
|
287
288
|
- **Period missing from the calendar.** With `periodEntity`, a month that has no row in the period entity counts as **closed**, and posting into it fails (`DF003: Period 1999-04 does not exist`). Seed or load the periods before the documents that post into them.
|
|
288
289
|
- **A summed column that isn't a number.** A resource of a `balance`, `turnover` or `accounting` register, and a `totals` cache's columns, must be a scalar number. The editor and language server warn (`registers/column-not-numeric`, metadata 0.0.46) on `varchar`, `integer[]` and the like. An `info` register's resources are returned, not summed, and may be any type.
|
|
290
|
+
- **"May edit" is not "may post".** Without `postAction` / `unpostAction` a role with Insert/Update on the document can post it through the API (`data.update` with the state flag) though it cannot run the Post action. **Unreleased: not in platform 1.28.12**, so do not declare these keys against that platform. Declare both (declaring one leaves the other direction open), and grant `E` on the actions (`"action:post_entry": "E"`) only to the roles that may post. Each name must be declared for that entity in the `ui/actions.json` of the package that owns the column; `module pack` refuses any other, including an action a bridge adds to the host's entity. `E` guards only the direct API path: an action with `E` that calls `update()` on the state flag can still post, so guard that inside its `execute:` (check the role or state and `error()`).
|
|
289
291
|
- **Missing or unlocked `dateField`.** Required once the register has `journalDate` or `period`, and it must be in `lockedFields`.
|
|
290
292
|
- **Key over dimensions.** The journal, a subordinate `info` register's records and an `autoCreate` state table are written by the platform. Each needs **one** surrogate primary key of type `bigint` (`identity`/`cuid`) or `uuid`. A key over dimensions, a multi-column key, a text key and `int4` are refused.
|
|
291
293
|
- **State column narrower than the journal column.** It must hold the journal's value exactly, or unposting cannot take back what posting added.
|
|
@@ -81,6 +81,8 @@ Example:
|
|
|
81
81
|
|---|---|
|
|
82
82
|
| `E` | Execute (run the action/report/stored procedure, access the folder) |
|
|
83
83
|
|
|
84
|
+
**`E` on an action is not limited by the user's entity rights.** The gate is on running the action, not on the statements inside it: `insert()` / `update()` / `delete()` / `query()` in its script work on an entity where the user has only `S`. Entity rights apply to the grid, the card and the API, not to action scripts (core #1580).
|
|
85
|
+
|
|
84
86
|
### Common combinations
|
|
85
87
|
|
|
86
88
|
| Rights | Meaning |
|
|
@@ -122,14 +122,12 @@ For each action:
|
|
|
122
122
|
- [ ] DSL file exists at `logic/actions/<script>.dsl`, where `<script>` is the bare filename in the action's `script` field (no path, no `.dsl` extension)
|
|
123
123
|
- [ ] DSL file has `params:`, `canExecute:`, and `execute:` blocks
|
|
124
124
|
- [ ] `canExecute:` is a valid formula expression
|
|
125
|
-
- [ ]
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
`NOT [is_blocked]` are not enforced (a field is only an operand). Write
|
|
132
|
-
`[is_active] = true` / `NOT ([is_blocked] = true)`
|
|
125
|
+
- [ ] `canExecute:` uses only constructs the server can evaluate — fields,
|
|
126
|
+
literals, comparisons, arithmetic, `IN`, formula functions, a bare boolean
|
|
127
|
+
field. `$[setting]` reads, `SUM` / `COUNT` / `AVG` and unknown function
|
|
128
|
+
names are **refused at install**. `[ref].[col]` works on the
|
|
129
|
+
server but the client throws and **enables** the button: denormalise it
|
|
130
|
+
into a column, and count with a `G` column
|
|
133
131
|
- [ ] Anything that MUST hold is also checked in `execute:` with `error()` — a
|
|
134
132
|
`canExecute` alone is never the authoritative guard
|
|
135
133
|
- [ ] `execute:` uses only documented built-in functions
|