@dforge-core/dforge-mcp 0.1.0-rc.10 → 0.1.0-rc.11
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/dist/server.js
CHANGED
|
@@ -31381,8 +31381,12 @@ function modulePaths(moduleDir) {
|
|
|
31381
31381
|
menus: path3.join(root, "ui", "menus.json"),
|
|
31382
31382
|
actions: path3.join(root, "ui", "actions.json"),
|
|
31383
31383
|
reports: path3.join(root, "ui", "reports.json"),
|
|
31384
|
+
queries: path3.join(root, "ui", "queries.json"),
|
|
31385
|
+
printTemplates: path3.join(root, "ui", "print_templates.json"),
|
|
31384
31386
|
roles: path3.join(root, "security", "roles.json"),
|
|
31385
31387
|
jobs: path3.join(root, "logic", "jobs.json"),
|
|
31388
|
+
triggers: path3.join(root, "logic", "triggers.json"),
|
|
31389
|
+
webhooks: path3.join(root, "logic", "webhooks.json"),
|
|
31386
31390
|
settings: path3.join(root, "settings.json")
|
|
31387
31391
|
};
|
|
31388
31392
|
}
|
|
@@ -31932,6 +31936,158 @@ function computeDepth(folder, current = 1) {
|
|
|
31932
31936
|
return childDepths.length === 0 ? current : Math.max(...childDepths);
|
|
31933
31937
|
}
|
|
31934
31938
|
|
|
31939
|
+
// src/tools/behavior.ts
|
|
31940
|
+
var triggerAddSchema = {
|
|
31941
|
+
moduleDir: external_exports.string(),
|
|
31942
|
+
code: external_exports.string().regex(/^[a-z][a-z0-9_]*$/).describe("Trigger code, unique within the module."),
|
|
31943
|
+
entity: external_exports.string().describe(
|
|
31944
|
+
"Entity whose events fire this trigger. Cross-module dotted form supported ('fin.invoice')."
|
|
31945
|
+
),
|
|
31946
|
+
event: external_exports.enum(["insert", "update", "delete", "status_change", "any"]).describe(
|
|
31947
|
+
"insert/update/delete are obvious. status_change fires only when status field changes value. 'any' fires for all insert/update/delete."
|
|
31948
|
+
),
|
|
31949
|
+
action: external_exports.string().describe("Action code to invoke. Cross-module dotted form ('module.action') supported."),
|
|
31950
|
+
description: external_exports.string().optional(),
|
|
31951
|
+
condition: external_exports.string().optional().describe(
|
|
31952
|
+
"Optional formula expression (same shape as canExecute) using `[field]` syntax. Single-line boolean. If omitted, trigger fires on every matching event."
|
|
31953
|
+
),
|
|
31954
|
+
params: external_exports.record(external_exports.string(), external_exports.unknown()).optional().describe(
|
|
31955
|
+
"Static params merged into the action invocation alongside auto-injected record_id."
|
|
31956
|
+
),
|
|
31957
|
+
async: external_exports.boolean().default(false).describe(
|
|
31958
|
+
"When true, the action runs in background (recommended for slow actions). When false, runs in the same transaction \u2014 action failure rolls back the original DB change."
|
|
31959
|
+
)
|
|
31960
|
+
};
|
|
31961
|
+
function triggerAdd(args) {
|
|
31962
|
+
const { paths, manifest } = loadManifest(args.moduleDir);
|
|
31963
|
+
const file2 = readJsonOrDefault(
|
|
31964
|
+
paths.triggers,
|
|
31965
|
+
{ triggers: [] }
|
|
31966
|
+
);
|
|
31967
|
+
if (file2.triggers.some((t) => t.code === args.code)) {
|
|
31968
|
+
throw new Error(`Trigger '${args.code}' already exists.`);
|
|
31969
|
+
}
|
|
31970
|
+
const entry = {
|
|
31971
|
+
code: args.code,
|
|
31972
|
+
entity: args.entity,
|
|
31973
|
+
event: args.event,
|
|
31974
|
+
action: args.action,
|
|
31975
|
+
async: args.async
|
|
31976
|
+
};
|
|
31977
|
+
if (args.description) entry.description = args.description;
|
|
31978
|
+
if (args.condition) entry.condition = args.condition;
|
|
31979
|
+
if (args.params) entry.params = args.params;
|
|
31980
|
+
file2.triggers.push(entry);
|
|
31981
|
+
return makeResult(
|
|
31982
|
+
`Added trigger '${args.code}' on ${args.entity}.${args.event} \u2192 action '${args.action}'${args.async ? " (async)" : ""}.`,
|
|
31983
|
+
{
|
|
31984
|
+
[rel(paths.root, paths.triggers)]: jsonText(file2),
|
|
31985
|
+
"manifest.json": jsonText(withTodayStamp(manifest))
|
|
31986
|
+
}
|
|
31987
|
+
);
|
|
31988
|
+
}
|
|
31989
|
+
var jobAddSchema = {
|
|
31990
|
+
moduleDir: external_exports.string(),
|
|
31991
|
+
code: external_exports.string().regex(/^[a-z][a-z0-9_]*$/).describe("Job code, unique within the module."),
|
|
31992
|
+
action: external_exports.string().describe(
|
|
31993
|
+
"Action code to invoke on schedule. MUST NOT use record-context (`[field]`) syntax \u2014 scheduled jobs run as system user with no current record. Wrap record-context actions in a thin action that queries first."
|
|
31994
|
+
),
|
|
31995
|
+
schedule: external_exports.string().describe(
|
|
31996
|
+
"5-field cron expression (minute hour day-of-month month day-of-week). Evaluated in the tenant's time zone (auth.tenant.time_zone) or per-job override."
|
|
31997
|
+
),
|
|
31998
|
+
timeout: external_exports.number().int().min(1).max(3600).describe(
|
|
31999
|
+
"Hard kill timeout in seconds. Required. If > 300, you must also set `class: 'long_running'`."
|
|
32000
|
+
),
|
|
32001
|
+
description: external_exports.string().optional(),
|
|
32002
|
+
concurrency: external_exports.number().int().min(1).default(1).describe("Max overlapping executions. Default 1 (skip new fire if previous still running)."),
|
|
32003
|
+
jobClass: external_exports.enum(["standard", "long_running"]).default("standard").describe(
|
|
32004
|
+
"Required to be 'long_running' when timeout > 300. Affects scheduler thread-pool routing."
|
|
32005
|
+
),
|
|
32006
|
+
paused: external_exports.boolean().default(false)
|
|
32007
|
+
};
|
|
32008
|
+
function jobAdd(args) {
|
|
32009
|
+
if (args.timeout > 300 && args.jobClass !== "long_running") {
|
|
32010
|
+
throw new Error(
|
|
32011
|
+
"Jobs with timeout > 300s must set jobClass: 'long_running' \u2014 see logic/jobs.json convention."
|
|
32012
|
+
);
|
|
32013
|
+
}
|
|
32014
|
+
const { paths, manifest } = loadManifest(args.moduleDir);
|
|
32015
|
+
const file2 = readJsonOrDefault(
|
|
32016
|
+
paths.jobs,
|
|
32017
|
+
{ jobs: [] }
|
|
32018
|
+
);
|
|
32019
|
+
if (file2.jobs.some((j) => j.code === args.code)) {
|
|
32020
|
+
throw new Error(`Job '${args.code}' already exists.`);
|
|
32021
|
+
}
|
|
32022
|
+
const entry = {
|
|
32023
|
+
code: args.code,
|
|
32024
|
+
action: args.action,
|
|
32025
|
+
schedule: args.schedule,
|
|
32026
|
+
timeout: args.timeout,
|
|
32027
|
+
concurrency: args.concurrency,
|
|
32028
|
+
class: args.jobClass
|
|
32029
|
+
};
|
|
32030
|
+
if (args.description) entry.description = args.description;
|
|
32031
|
+
if (args.paused) entry.paused = true;
|
|
32032
|
+
file2.jobs.push(entry);
|
|
32033
|
+
return makeResult(
|
|
32034
|
+
`Added job '${args.code}' (schedule '${args.schedule}', timeout ${args.timeout}s) \u2192 action '${args.action}'.`,
|
|
32035
|
+
{
|
|
32036
|
+
[rel(paths.root, paths.jobs)]: jsonText(file2),
|
|
32037
|
+
"manifest.json": jsonText(withTodayStamp(manifest))
|
|
32038
|
+
}
|
|
32039
|
+
);
|
|
32040
|
+
}
|
|
32041
|
+
var webhookAddSchema = {
|
|
32042
|
+
moduleDir: external_exports.string(),
|
|
32043
|
+
code: external_exports.string().regex(/^[a-z][a-z0-9_]*$/).describe("Subscription code, unique within the module."),
|
|
32044
|
+
entity: external_exports.string().describe("Entity whose events fire this webhook."),
|
|
32045
|
+
event: external_exports.enum(["insert", "update", "delete", "status_change", "any"]).describe("Same event taxonomy as triggers."),
|
|
32046
|
+
endpointUrl: external_exports.string().url().describe("Destination URL \u2014 receives POST with the payload."),
|
|
32047
|
+
description: external_exports.string().optional(),
|
|
32048
|
+
condition: external_exports.string().optional().describe("Optional formula filter; fires only when true. Same shape as trigger condition."),
|
|
32049
|
+
payload: external_exports.object({
|
|
32050
|
+
include: external_exports.array(external_exports.string()).optional().describe("Whitelist of field codes to include. Omit to send all fields."),
|
|
32051
|
+
exclude: external_exports.array(external_exports.string()).optional().describe("Blacklist (applied after include). Use for stripping sensitive cols."),
|
|
32052
|
+
includeOld: external_exports.boolean().optional().describe(
|
|
32053
|
+
"For update/status_change events, also include the pre-change record as `old`."
|
|
32054
|
+
)
|
|
32055
|
+
}).optional().describe(
|
|
32056
|
+
"Payload shape. If omitted, sends full new record. The platform always wraps in `{ event, entity, record, old?, occurred_at }`."
|
|
32057
|
+
),
|
|
32058
|
+
headers: external_exports.record(external_exports.string(), external_exports.string()).optional().describe(
|
|
32059
|
+
"Extra HTTP headers (e.g. authentication tokens). Use `getSecret(...)` semantics: header values starting with `$secret:` are resolved at fire time."
|
|
32060
|
+
)
|
|
32061
|
+
};
|
|
32062
|
+
function webhookAdd(args) {
|
|
32063
|
+
const { paths, manifest } = loadManifest(args.moduleDir);
|
|
32064
|
+
const file2 = readJsonOrDefault(
|
|
32065
|
+
paths.webhooks,
|
|
32066
|
+
{ subscriptions: [] }
|
|
32067
|
+
);
|
|
32068
|
+
if (file2.subscriptions.some((s) => s.code === args.code)) {
|
|
32069
|
+
throw new Error(`Webhook subscription '${args.code}' already exists.`);
|
|
32070
|
+
}
|
|
32071
|
+
const entry = {
|
|
32072
|
+
code: args.code,
|
|
32073
|
+
entity: args.entity,
|
|
32074
|
+
event: args.event,
|
|
32075
|
+
endpointUrl: args.endpointUrl
|
|
32076
|
+
};
|
|
32077
|
+
if (args.description) entry.description = args.description;
|
|
32078
|
+
if (args.condition) entry.condition = args.condition;
|
|
32079
|
+
if (args.payload) entry.payload = args.payload;
|
|
32080
|
+
if (args.headers) entry.headers = args.headers;
|
|
32081
|
+
file2.subscriptions.push(entry);
|
|
32082
|
+
return makeResult(
|
|
32083
|
+
`Added webhook subscription '${args.code}' on ${args.entity}.${args.event} \u2192 POST ${args.endpointUrl}.`,
|
|
32084
|
+
{
|
|
32085
|
+
[rel(paths.root, paths.webhooks)]: jsonText(file2),
|
|
32086
|
+
"manifest.json": jsonText(withTodayStamp(manifest))
|
|
32087
|
+
}
|
|
32088
|
+
);
|
|
32089
|
+
}
|
|
32090
|
+
|
|
31935
32091
|
// src/resources/index.ts
|
|
31936
32092
|
var fs5 = __toESM(require("fs"));
|
|
31937
32093
|
var path7 = __toESM(require("path"));
|
|
@@ -31994,7 +32150,17 @@ var resources = [
|
|
|
31994
32150
|
schema(
|
|
31995
32151
|
"webhooks",
|
|
31996
32152
|
"Webhooks JSON schema",
|
|
31997
|
-
"JSON Schema for
|
|
32153
|
+
"JSON Schema for logic/webhooks.json \u2014 outbound webhook subscriptions: entity + event filter \u2192 POST to endpointUrl with selected fields."
|
|
32154
|
+
),
|
|
32155
|
+
schema(
|
|
32156
|
+
"triggers",
|
|
32157
|
+
"Triggers JSON schema",
|
|
32158
|
+
"JSON Schema for logic/triggers.json \u2014 intra-tenant DB-event \u2192 action invocation: entity + event + optional condition formula \u2192 fires an action (sync or async)."
|
|
32159
|
+
),
|
|
32160
|
+
schema(
|
|
32161
|
+
"print-templates",
|
|
32162
|
+
"Print templates JSON schema",
|
|
32163
|
+
"JSON Schema for ui/print_templates.json \u2014 Liquid HTML print templates and reusable snippets, bound to entities for the print menu."
|
|
31998
32164
|
),
|
|
31999
32165
|
schema(
|
|
32000
32166
|
"settings",
|
|
@@ -32146,10 +32312,28 @@ server.tool(
|
|
|
32146
32312
|
);
|
|
32147
32313
|
server.tool(
|
|
32148
32314
|
"dforge_action_add",
|
|
32149
|
-
"PHASE
|
|
32315
|
+
"PHASE 2: Add a DSL action targeting an entity. Writes logic/actions/<code>.dsl plus an entry in ui/actions.json. **Load dforge://docs/dsl before authoring.**",
|
|
32150
32316
|
actionAddSchema,
|
|
32151
32317
|
envelope(actionAdd)
|
|
32152
32318
|
);
|
|
32319
|
+
server.tool(
|
|
32320
|
+
"dforge_trigger_add",
|
|
32321
|
+
"PHASE 2: Add a trigger that fires an action on a DB event (insert/update/delete/status_change/any) optionally gated by a condition formula. Appends to logic/triggers.json. **Use trigger when the platform should react to data changes WITHOUT user interaction**; use jobs for cron-driven; use webhooks for outbound HTTP.",
|
|
32322
|
+
triggerAddSchema,
|
|
32323
|
+
envelope(triggerAdd)
|
|
32324
|
+
);
|
|
32325
|
+
server.tool(
|
|
32326
|
+
"dforge_job_add",
|
|
32327
|
+
"PHASE 2: Schedule an existing action to fire on a 5-field cron. Appends to logic/jobs.json. Action must NOT use record-context (`[field]`) syntax \u2014 scheduled jobs run as system user with no current record.",
|
|
32328
|
+
jobAddSchema,
|
|
32329
|
+
envelope(jobAdd)
|
|
32330
|
+
);
|
|
32331
|
+
server.tool(
|
|
32332
|
+
"dforge_webhook_add",
|
|
32333
|
+
"PHASE 2: Subscribe an outbound HTTP endpoint to a DB event. Appends to logic/webhooks.json. Use for integrations with external systems (Slack, Zapier, custom dashboards).",
|
|
32334
|
+
webhookAddSchema,
|
|
32335
|
+
envelope(webhookAdd)
|
|
32336
|
+
);
|
|
32153
32337
|
server.tool(
|
|
32154
32338
|
"dforge_view_add",
|
|
32155
32339
|
"PHASE 4: Add a data view to ui/data_views.json. viewType-specific viewConfig is supplied verbatim \u2014 pull dforge://schema/data-views first to know the shape.",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dforge-core/dforge-mcp",
|
|
3
|
-
"version": "0.1.0-rc.
|
|
3
|
+
"version": "0.1.0-rc.11",
|
|
4
4
|
"description": "MCP server for dForge module authoring. Exposes scaffold/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",
|
|
@@ -539,7 +539,7 @@ seed-data/
|
|
|
539
539
|
|
|
540
540
|
### 7. Scheduled Jobs (`logic/jobs.json`)
|
|
541
541
|
|
|
542
|
-
Cron-driven action fires. Each entry pairs an existing action (declared in `ui/actions.json`) with a 5-field cron expression; the `dForge.Scheduler` worker fires it on schedule. Full reference: [
|
|
542
|
+
Cron-driven action fires. Each entry pairs an existing action (declared in `ui/actions.json`) with a 5-field cron expression; the `dForge.Scheduler` worker fires it on schedule. Full reference: [Scheduled Jobs](../business-logic/jobs.md). Manifest schema: [`jobs.schema.json`](../schemas/jobs.schema.json).
|
|
543
543
|
|
|
544
544
|
```json
|
|
545
545
|
{
|
|
@@ -926,7 +926,7 @@ When creating a module, ensure:
|
|
|
926
926
|
- [ ] Constraints have clear user-facing `message` values
|
|
927
927
|
- [ ] Check constraint `expression` uses standard SQL subset (test in PostgreSQL first)
|
|
928
928
|
- [ ] Number sequences declared as `numberSequence` on entity definitions (auto-fills on INSERT, never manual counting)
|
|
929
|
-
- [ ] Print templates defined in `ui/print_templates.json` with HTML files in `print_templates/`
|
|
929
|
+
- [ ] Print templates defined in `ui/print_templates.json` with HTML files in `print_templates/` (see [Print Templates](../ui/print-templates.md))
|
|
930
930
|
- [ ] Scheduled jobs (if any) declared in `logic/jobs.json` with 5-field cron, explicit `timeout`, and `class: "long_running"` for any `timeout > 300`
|
|
931
931
|
|
|
932
932
|
---
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"title": "dForge Module Print Templates",
|
|
4
|
+
"description": "Print template declarations for a dForge module package (ui/print_templates.json). Each key is the template_cd; the value declares either a printable template (default) or a reusable snippet pulled into other templates via {% include 'module_cd.snippet_cd' %}.",
|
|
5
|
+
"type": "object",
|
|
6
|
+
"additionalProperties": {
|
|
7
|
+
"type": "object",
|
|
8
|
+
"properties": {
|
|
9
|
+
"type": {
|
|
10
|
+
"type": "string",
|
|
11
|
+
"enum": ["template", "snippet"],
|
|
12
|
+
"default": "template",
|
|
13
|
+
"description": "'template' (default) — a full printable form bound to an entity. 'snippet' — a reusable Liquid partial, module-scoped, no entity binding, included from other templates."
|
|
14
|
+
},
|
|
15
|
+
"entityCd": {
|
|
16
|
+
"type": "string",
|
|
17
|
+
"description": "Entity code this template renders. Required when type='template'; ignored when type='snippet'. Cross-module form 'module.entity' is supported (e.g. fin-ch ships a template for fin.invoice)."
|
|
18
|
+
},
|
|
19
|
+
"description": {
|
|
20
|
+
"type": "string",
|
|
21
|
+
"description": "Human-readable label shown in the print menu."
|
|
22
|
+
},
|
|
23
|
+
"template": {
|
|
24
|
+
"type": "string",
|
|
25
|
+
"description": "Inline Liquid HTML. Either 'template' or 'file' must be set; 'file' takes precedence when both are present."
|
|
26
|
+
},
|
|
27
|
+
"file": {
|
|
28
|
+
"type": "string",
|
|
29
|
+
"description": "Relative path under the module's print_templates/ directory (e.g. 'invoice.html'). Loaded at install time."
|
|
30
|
+
},
|
|
31
|
+
"css": {
|
|
32
|
+
"type": "string",
|
|
33
|
+
"description": "Inline CSS, or a filename ending in .css (resolved relative to print_templates/). Snippets typically inherit CSS from the host template and omit this."
|
|
34
|
+
},
|
|
35
|
+
"pageSettings": {
|
|
36
|
+
"type": "object",
|
|
37
|
+
"description": "Default page geometry applied when the user prints from the UI. Stored as jsonb on the row; the editor lets users tweak per-template.",
|
|
38
|
+
"properties": {
|
|
39
|
+
"size": {
|
|
40
|
+
"type": "string",
|
|
41
|
+
"enum": ["A4", "letter", "legal"],
|
|
42
|
+
"default": "A4"
|
|
43
|
+
},
|
|
44
|
+
"orientation": {
|
|
45
|
+
"type": "string",
|
|
46
|
+
"enum": ["portrait", "landscape"],
|
|
47
|
+
"default": "portrait"
|
|
48
|
+
},
|
|
49
|
+
"margins": {
|
|
50
|
+
"type": "object",
|
|
51
|
+
"properties": {
|
|
52
|
+
"top": { "type": "string", "description": "CSS length, e.g. '15mm'." },
|
|
53
|
+
"right": { "type": "string" },
|
|
54
|
+
"bottom": { "type": "string" },
|
|
55
|
+
"left": { "type": "string" }
|
|
56
|
+
},
|
|
57
|
+
"additionalProperties": false
|
|
58
|
+
}
|
|
59
|
+
},
|
|
60
|
+
"additionalProperties": false
|
|
61
|
+
}
|
|
62
|
+
},
|
|
63
|
+
"additionalProperties": false,
|
|
64
|
+
"allOf": [
|
|
65
|
+
{
|
|
66
|
+
"if": { "properties": { "type": { "const": "snippet" } }, "required": ["type"] },
|
|
67
|
+
"then": {
|
|
68
|
+
"description": "Snippets must not declare entityCd and their template_cd must not contain '.' (collides with {% include 'module_cd.snippet_cd' %})."
|
|
69
|
+
},
|
|
70
|
+
"else": {
|
|
71
|
+
"required": ["entityCd"]
|
|
72
|
+
}
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
"anyOf": [
|
|
76
|
+
{ "required": ["file"] },
|
|
77
|
+
{ "required": ["template"] }
|
|
78
|
+
]
|
|
79
|
+
}
|
|
80
|
+
]
|
|
81
|
+
}
|
|
82
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
3
|
+
"$id": "https://dforge.dev/schemas/triggers.schema.json",
|
|
4
|
+
"title": "dForge Module Triggers",
|
|
5
|
+
"description": "Trigger definitions for a dForge module (logic/triggers.json). A trigger watches an entity for an event (insert/update/delete/status_change/any), evaluates an optional formula condition using `[field]` syntax, and fires an action if true. Different from scheduled jobs (cron-driven) and webhooks (outbound HTTP) — triggers are intra-tenant DB-event-driven action invocations.",
|
|
6
|
+
"type": "object",
|
|
7
|
+
"required": ["triggers"],
|
|
8
|
+
"properties": {
|
|
9
|
+
"triggers": {
|
|
10
|
+
"type": "array",
|
|
11
|
+
"items": { "$ref": "#/$defs/trigger" }
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"additionalProperties": false,
|
|
15
|
+
"$defs": {
|
|
16
|
+
"trigger": {
|
|
17
|
+
"type": "object",
|
|
18
|
+
"required": ["code", "entity", "event", "action"],
|
|
19
|
+
"properties": {
|
|
20
|
+
"code": {
|
|
21
|
+
"type": "string",
|
|
22
|
+
"pattern": "^[a-z][a-z0-9_]*$",
|
|
23
|
+
"description": "Trigger code, unique within the module."
|
|
24
|
+
},
|
|
25
|
+
"description": {
|
|
26
|
+
"type": "string",
|
|
27
|
+
"description": "Human-readable description of what the trigger does and when."
|
|
28
|
+
},
|
|
29
|
+
"entity": {
|
|
30
|
+
"type": "string",
|
|
31
|
+
"description": "Entity whose events this trigger watches. May be cross-module via dot notation (e.g. 'fin.invoice')."
|
|
32
|
+
},
|
|
33
|
+
"event": {
|
|
34
|
+
"type": "string",
|
|
35
|
+
"enum": ["insert", "update", "delete", "status_change", "any"],
|
|
36
|
+
"description": "DB event that fires this trigger. 'status_change' only fires when the entity's status field actually changes value. 'any' fires for all insert/update/delete."
|
|
37
|
+
},
|
|
38
|
+
"condition": {
|
|
39
|
+
"type": "string",
|
|
40
|
+
"description": "Optional formula expression — same shape as canExecute in action DSL. Uses `[field]` syntax to reference the affected record. Single-line, evaluates to boolean. If omitted, trigger fires for every matching event."
|
|
41
|
+
},
|
|
42
|
+
"action": {
|
|
43
|
+
"type": "string",
|
|
44
|
+
"description": "Action code to invoke. The action must NOT use record-context (`[field]`) syntax in a way that requires the original triggering record — the trigger passes the record id via params. Cross-module form 'module.action' is supported."
|
|
45
|
+
},
|
|
46
|
+
"params": {
|
|
47
|
+
"type": "object",
|
|
48
|
+
"description": "Optional static parameters passed to the action at invocation time. Merged with the runtime-injected `record_id` / `record_old` (if event uses old-record diff)."
|
|
49
|
+
},
|
|
50
|
+
"async": {
|
|
51
|
+
"type": "boolean",
|
|
52
|
+
"default": false,
|
|
53
|
+
"description": "When true, the action runs in the background — the triggering transaction commits before the action starts. Recommended for slow actions (emails, external API calls). When false, the action runs in the same transaction; failures roll back the original DB change."
|
|
54
|
+
}
|
|
55
|
+
},
|
|
56
|
+
"additionalProperties": false
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
@@ -175,15 +175,55 @@ A field is **batchable** only if ALL of these are true: scalar primitive (string
|
|
|
175
175
|
|
|
176
176
|
**Exit criteria:** every entity has at least PK + audit traits + 1 user-visible field; FK references resolve; manifest's `entities` map reflects reality.
|
|
177
177
|
|
|
178
|
-
## Phase 2 —
|
|
178
|
+
## Phase 2 — Behavior (optional sub-steps)
|
|
179
179
|
|
|
180
180
|
**Preconditions:** Phase 1 complete.
|
|
181
181
|
|
|
182
|
-
|
|
182
|
+
Phase 2 covers four kinds of behavior — all optional, all individually skip-able. Each fires action logic, but the **trigger** differs:
|
|
183
183
|
|
|
184
|
-
|
|
184
|
+
| Sub-step | Fires when | File | Tool | Use when |
|
|
185
|
+
|---|---|---|---|---|
|
|
186
|
+
| 2a Actions | user clicks a button | `ui/actions.json` + `logic/actions/*.dsl` | `dforge_action_add` | bulk operations, business workflows, anything that needs user input via params |
|
|
187
|
+
| 2b Triggers | DB event happens (insert/update/delete/status_change) | `logic/triggers.json` | `dforge_trigger_add` | reactive automation: "when X happens, do Y" without user action |
|
|
188
|
+
| 2c Scheduled jobs | cron timer | `logic/jobs.json` | `dforge_job_add` | periodic work: nightly cleanup, daily summary, hourly poll |
|
|
189
|
+
| 2d Webhooks | DB event happens → POSTs to external URL | `logic/webhooks.json` | `dforge_webhook_add` | integrations: Slack notifications, Zapier, external dashboards, audit log shipping |
|
|
185
190
|
|
|
186
|
-
|
|
191
|
+
Skip a sub-step entirely if the user has no need for it. Do NOT fabricate behavior to fill a sub-step. Phase 2 can be completely skipped for pure CRUD modules.
|
|
192
|
+
|
|
193
|
+
### 2a. Actions — user-triggered
|
|
194
|
+
|
|
195
|
+
**Load `dforge://docs/dsl`** (the full action DSL reference — block structure, all 30 built-in functions, field-access syntax, batch-mode rules, JS subset, common patterns, anti-patterns) before authoring any DSL. `dforge://docs/conventions` is broader module-level guidance and does NOT cover the DSL grammar.
|
|
196
|
+
|
|
197
|
+
Call `dforge_action_add` per action — one at a time — with the full DSL body. Confirm with the user before each call.
|
|
198
|
+
|
|
199
|
+
### 2b. Triggers — DB-event-driven
|
|
200
|
+
|
|
201
|
+
**Load `dforge://schema/triggers`** for the shape; also re-read the trigger formula rules in `dforge://docs/dsl` (trigger conditions use the same syntax as `canExecute:`: single-line `[field] op value` formulas).
|
|
202
|
+
|
|
203
|
+
For each trigger, propose: entity + event + (optional) condition formula + target action + async flag. Use `dforge_trigger_add`. Triggers reference EXISTING actions — make sure the action exists first (Phase 2a or scaffolded admin actions).
|
|
204
|
+
|
|
205
|
+
**Async vs sync:** `async: true` runs the action in the background after the triggering transaction commits — recommended for slow actions (emails, external API calls). `async: false` runs in the same transaction; action failure rolls back the original DB change.
|
|
206
|
+
|
|
207
|
+
### 2c. Scheduled jobs — cron-driven
|
|
208
|
+
|
|
209
|
+
**Load `dforge://schema/jobs`**.
|
|
210
|
+
|
|
211
|
+
Constraints baked into the tool:
|
|
212
|
+
- Action MUST NOT use record-context (`[field]`) syntax — jobs run as system user with NO current record. Wrap any record-context action in a thin job-friendly action that uses `query()` to fetch the records it needs.
|
|
213
|
+
- `timeout` is required, ≤ 3600s.
|
|
214
|
+
- If `timeout > 300`, you MUST set `jobClass: 'long_running'`.
|
|
215
|
+
|
|
216
|
+
Use `dforge_job_add` per job.
|
|
217
|
+
|
|
218
|
+
### 2d. Webhooks — outbound HTTP
|
|
219
|
+
|
|
220
|
+
**Load `dforge://schema/webhooks`**.
|
|
221
|
+
|
|
222
|
+
For each webhook: entity + event + endpoint URL + (optional) condition + (optional) payload shape (include/exclude/includeOld). Use `dforge_webhook_add`.
|
|
223
|
+
|
|
224
|
+
For authenticated endpoints: put bearer tokens / API keys behind `getSecret()` (configure secret in module's secrets), reference in headers as `"Authorization": "$secret:<secret_cd>"` — the platform resolves at fire time.
|
|
225
|
+
|
|
226
|
+
**Exit criteria for Phase 2:** every action / trigger / job / webhook you added is intended (user-requested, not fabricated to fill space) and references existing entities + actions. Compilation is validated at install in Phase 6.
|
|
187
227
|
|
|
188
228
|
## Phase 3 — Views (required) + Reports (optional)
|
|
189
229
|
|