@saltcorn/copilot 0.7.5 → 0.8.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/actions/generate-js-action.js +43 -5
- package/actions/generate-tables.js +281 -96
- package/actions/generate-trigger.js +61 -0
- package/actions/generate-workflow.js +89 -37
- package/actions/install-plugin-action.js +103 -0
- package/agent-skills/database-design.js +139 -87
- package/agent-skills/install-plugin.js +111 -0
- package/agent-skills/js-action.js +183 -0
- package/agent-skills/pagegen.js +19 -6
- package/agent-skills/registry-editor.js +911 -0
- package/agent-skills/triggergen.js +263 -0
- package/agent-skills/viewgen.js +431 -29
- package/agent-skills/workflow.js +52 -2
- package/app-constructor/common.js +12 -0
- package/app-constructor/errors.js +102 -0
- package/app-constructor/feedback-action.js +175 -0
- package/app-constructor/feedback.js +112 -0
- package/app-constructor/progress.js +116 -0
- package/app-constructor/prompts.js +120 -0
- package/app-constructor/requirements.js +156 -0
- package/app-constructor/run_task.js +146 -0
- package/app-constructor/schema.js +199 -0
- package/app-constructor/taskchart.js +70 -0
- package/app-constructor/tasks.js +585 -0
- package/app-constructor/tools.js +81 -0
- package/app-constructor/view.js +209 -0
- package/builder-gen.js +590 -68
- package/builder-schema.js +26 -6
- package/chat-copilot.js +1 -0
- package/common.js +20 -0
- package/copilot-as-agent.js +7 -1
- package/index.js +23 -1
- package/js-code-gen.js +65 -0
- package/package.json +1 -1
- package/relation-paths.js +236 -0
- package/tests/builder-gen.test.js +56 -0
|
@@ -5,7 +5,7 @@ const Table = require("@saltcorn/data/models/table");
|
|
|
5
5
|
const Field = require("@saltcorn/data/models/field");
|
|
6
6
|
const { apply, removeAllWhiteSpace } = require("@saltcorn/data/utils");
|
|
7
7
|
const { getActionConfigFields } = require("@saltcorn/data/plugin-helper");
|
|
8
|
-
const { a,
|
|
8
|
+
const { a, script, div, domReady } = require("@saltcorn/markup/tags");
|
|
9
9
|
const { fieldProperties, getPromptFromTemplate } = require("../common");
|
|
10
10
|
|
|
11
11
|
class GenerateJsAction {
|
|
@@ -59,12 +59,49 @@ class GenerateJsAction {
|
|
|
59
59
|
when_trigger,
|
|
60
60
|
trigger_table,
|
|
61
61
|
}) {
|
|
62
|
+
const lineCount = (action_javascript_code.match(/\n/g) || []).length + 1;
|
|
63
|
+
const height = Math.min(Math.max(lineCount * 20, 80), 300);
|
|
64
|
+
const editorId = `monaco-js-${Math.random().toString(36).slice(2)}`;
|
|
62
65
|
return (
|
|
63
|
-
div(
|
|
64
|
-
|
|
66
|
+
div(
|
|
67
|
+
{ class: "mb-3" },
|
|
68
|
+
`<strong>${action_name}</strong>${when_trigger ? `: ${when_trigger}` : ""}${
|
|
65
69
|
trigger_table ? ` on ${trigger_table}` : ""
|
|
66
|
-
}
|
|
67
|
-
) +
|
|
70
|
+
}`,
|
|
71
|
+
) +
|
|
72
|
+
div({
|
|
73
|
+
id: editorId,
|
|
74
|
+
style: `height: ${height}px; border: 1px solid #ccc;`,
|
|
75
|
+
}) +
|
|
76
|
+
// div(
|
|
77
|
+
// { class: `mt-3 ${action_description ? "" : "d-none"}` },
|
|
78
|
+
// action_description || "",
|
|
79
|
+
// ) +
|
|
80
|
+
script(
|
|
81
|
+
domReady(`
|
|
82
|
+
(function() {
|
|
83
|
+
var container = document.getElementById('${editorId}');
|
|
84
|
+
if (!container) return;
|
|
85
|
+
var code = ${JSON.stringify(action_javascript_code)};
|
|
86
|
+
function createEditor() {
|
|
87
|
+
monaco.editor.create(container, {
|
|
88
|
+
value: code,
|
|
89
|
+
language: 'javascript',
|
|
90
|
+
theme: typeof _sc_lightmode !== 'undefined' && _sc_lightmode === 'dark' ? 'vs-dark' : 'vs',
|
|
91
|
+
readOnly: true,
|
|
92
|
+
minimap: { enabled: false },
|
|
93
|
+
scrollBeyondLastLine: false,
|
|
94
|
+
automaticLayout: true,
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
if (typeof enable_monaco === 'function') {
|
|
98
|
+
enable_monaco([container], createEditor);
|
|
99
|
+
} else if (typeof monaco !== 'undefined') {
|
|
100
|
+
createEditor();
|
|
101
|
+
}
|
|
102
|
+
})();
|
|
103
|
+
`),
|
|
104
|
+
)
|
|
68
105
|
);
|
|
69
106
|
}
|
|
70
107
|
static async execute(
|
|
@@ -85,6 +122,7 @@ class GenerateJsAction {
|
|
|
85
122
|
}
|
|
86
123
|
const trigger = await Trigger.create({
|
|
87
124
|
name: action_name,
|
|
125
|
+
description: action_description,
|
|
88
126
|
when_trigger: when_trigger || "Never",
|
|
89
127
|
table_id,
|
|
90
128
|
action: "run_js_code",
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
const { getState } = require("@saltcorn/data/db/state");
|
|
2
|
+
const db = require("@saltcorn/data/db");
|
|
2
3
|
const WorkflowStep = require("@saltcorn/data/models/workflow_step");
|
|
3
4
|
const Trigger = require("@saltcorn/data/models/trigger");
|
|
4
5
|
const Table = require("@saltcorn/data/models/table");
|
|
@@ -11,9 +12,9 @@ const { fieldProperties } = require("../common");
|
|
|
11
12
|
class GenerateTables {
|
|
12
13
|
static title = "Generate Tables";
|
|
13
14
|
static function_name = "generate_tables";
|
|
14
|
-
static description = "Generate database tables";
|
|
15
|
+
static description = "Generate or update database tables";
|
|
15
16
|
|
|
16
|
-
static
|
|
17
|
+
static field_type_config_schema() {
|
|
17
18
|
const types = Object.values(getState().types);
|
|
18
19
|
const fieldTypeCfg = types.map((ty) => {
|
|
19
20
|
const properties = {
|
|
@@ -55,6 +56,92 @@ class GenerateTables {
|
|
|
55
56
|
data_type: { type: "string", enum: ["File"] },
|
|
56
57
|
},
|
|
57
58
|
});
|
|
59
|
+
return fieldTypeCfg;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
static field_item_schema() {
|
|
63
|
+
const fieldTypeCfg = this.field_type_config_schema();
|
|
64
|
+
return {
|
|
65
|
+
type: "object",
|
|
66
|
+
required: ["name", "label", "type_and_configuration", "importance"],
|
|
67
|
+
properties: {
|
|
68
|
+
name: {
|
|
69
|
+
type: "string",
|
|
70
|
+
description:
|
|
71
|
+
"The field name. Must be a valid identifier in both SQL and JavaScript, all lower case, snake_case (underscore instead of spaces)",
|
|
72
|
+
},
|
|
73
|
+
label: {
|
|
74
|
+
type: "string",
|
|
75
|
+
description:
|
|
76
|
+
"A human-readable label for the field. Should be short, 1-4 words, can have spaces and mixed case.",
|
|
77
|
+
},
|
|
78
|
+
not_null: {
|
|
79
|
+
type: "boolean",
|
|
80
|
+
description:
|
|
81
|
+
"A value is required and the field will be NOT NULL in the database",
|
|
82
|
+
},
|
|
83
|
+
unique: {
|
|
84
|
+
type: "boolean",
|
|
85
|
+
description:
|
|
86
|
+
"The value is unique - different rows must have different values for this field",
|
|
87
|
+
},
|
|
88
|
+
type_and_configuration: { anyOf: fieldTypeCfg },
|
|
89
|
+
importance: {
|
|
90
|
+
type: "number",
|
|
91
|
+
description:
|
|
92
|
+
"How important is this field if only some fields can be displayed to the user. From 1 (least important) to 10 (most important).",
|
|
93
|
+
},
|
|
94
|
+
calculated: {
|
|
95
|
+
type: "boolean",
|
|
96
|
+
description:
|
|
97
|
+
"Whether this is a calculated field. Calculated fields derive their value from a JavaScript expression rather than being entered directly. Set to true to make this a calculated field, then provide expression. Use stored=false for virtual fields computed on-the-fly, or stored=true for materialized fields persisted in the database. Do NOT use calculated=true for child-table aggregations (counts, sums) — use aggregation=true instead.",
|
|
98
|
+
},
|
|
99
|
+
stored: {
|
|
100
|
+
type: "boolean",
|
|
101
|
+
description:
|
|
102
|
+
"For calculated fields only: true means the value is stored/materialized in the database; false (default) means computed on-the-fly. Stored calculated fields support joinfield syntax in expressions. Only set when calculated=true.",
|
|
103
|
+
},
|
|
104
|
+
expression: {
|
|
105
|
+
type: "string",
|
|
106
|
+
description:
|
|
107
|
+
"For calculated fields: a JavaScript expression returning the field value. References other fields in the same row by name (e.g. 'price * quantity'). For stored calculated fields, use joinfield syntax to access related-table fields: 'foreignKeyField.targetField' (e.g. 'author_id.full_name') or two-level: 'fkField.throughFkField.deepField'. Only set when calculated=true. Do NOT use this for child-table aggregations — use aggregation=true instead.",
|
|
108
|
+
},
|
|
109
|
+
aggregation: {
|
|
110
|
+
type: "boolean",
|
|
111
|
+
description:
|
|
112
|
+
"Set to true to create a stored aggregation field that counts or sums child-table records. Use this for any field that counts or aggregates rows from a child table (e.g. counting packing items, summing quantities). When aggregation=true, also set aggregate_function, child_table, child_fk_field, aggregate_field, and optionally aggregate_where. Do not set calculated or expression when using aggregation.",
|
|
113
|
+
},
|
|
114
|
+
aggregate_function: {
|
|
115
|
+
type: "string",
|
|
116
|
+
enum: ["Count", "Sum", "Avg", "CountUnique", "Max", "Min"],
|
|
117
|
+
description:
|
|
118
|
+
"Aggregation function to apply. Use 'Count' to count rows, 'Sum' to sum a numeric field. Only set when aggregation=true.",
|
|
119
|
+
},
|
|
120
|
+
child_table: {
|
|
121
|
+
type: "string",
|
|
122
|
+
description:
|
|
123
|
+
"Name of the child table whose rows are aggregated. Only set when aggregation=true.",
|
|
124
|
+
},
|
|
125
|
+
child_fk_field: {
|
|
126
|
+
type: "string",
|
|
127
|
+
description:
|
|
128
|
+
"Name of the foreign-key field on the child table that references this (parent) table. Only set when aggregation=true.",
|
|
129
|
+
},
|
|
130
|
+
aggregate_field: {
|
|
131
|
+
type: "string",
|
|
132
|
+
description:
|
|
133
|
+
"Field on the child table to aggregate. For Count use 'id'. For Sum/Avg/Max/Min use the numeric field name. Only set when aggregation=true.",
|
|
134
|
+
},
|
|
135
|
+
aggregate_where: {
|
|
136
|
+
type: "string",
|
|
137
|
+
description:
|
|
138
|
+
"Optional JavaScript expression to filter which child rows are included, evaluated in the context of a child row. Examples: 'completed' (boolean field is true), 'status === \"done\"'. Only set when aggregation=true and a subset of child rows is needed.",
|
|
139
|
+
},
|
|
140
|
+
},
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
static json_schema() {
|
|
58
145
|
return {
|
|
59
146
|
type: "object",
|
|
60
147
|
required: ["tables"],
|
|
@@ -71,43 +158,7 @@ class GenerateTables {
|
|
|
71
158
|
},
|
|
72
159
|
fields: {
|
|
73
160
|
type: "array",
|
|
74
|
-
items:
|
|
75
|
-
type: "object",
|
|
76
|
-
required: [
|
|
77
|
-
"name",
|
|
78
|
-
"label",
|
|
79
|
-
"type_and_configuration",
|
|
80
|
-
"importance",
|
|
81
|
-
],
|
|
82
|
-
properties: {
|
|
83
|
-
name: {
|
|
84
|
-
type: "string",
|
|
85
|
-
description:
|
|
86
|
-
"The field name. Must be a valid identifier in both SQL and JavaScript, all lower case, snake_case (underscore instead of spaces)",
|
|
87
|
-
},
|
|
88
|
-
label: {
|
|
89
|
-
type: "string",
|
|
90
|
-
description:
|
|
91
|
-
"A human-readable label for the field. Should be short, 1-4 words, can have spaces and mixed case.",
|
|
92
|
-
},
|
|
93
|
-
not_null: {
|
|
94
|
-
type: "boolean",
|
|
95
|
-
description:
|
|
96
|
-
"A value is required and the field will be NOT NULL in the database",
|
|
97
|
-
},
|
|
98
|
-
unique: {
|
|
99
|
-
type: "boolean",
|
|
100
|
-
description:
|
|
101
|
-
"The value is unique - different rows must have different values for this field",
|
|
102
|
-
},
|
|
103
|
-
type_and_configuration: { anyOf: fieldTypeCfg },
|
|
104
|
-
importance: {
|
|
105
|
-
type: "number",
|
|
106
|
-
description:
|
|
107
|
-
"How important is this field if only some fields can be displayed to the user. From 1 (least important) to 10 (most important).",
|
|
108
|
-
},
|
|
109
|
-
},
|
|
110
|
-
},
|
|
161
|
+
items: this.field_item_schema(),
|
|
111
162
|
},
|
|
112
163
|
},
|
|
113
164
|
},
|
|
@@ -120,42 +171,100 @@ class GenerateTables {
|
|
|
120
171
|
const tableLines = [];
|
|
121
172
|
const tables = await Table.find({});
|
|
122
173
|
tables.forEach((table) => {
|
|
123
|
-
const fieldLines = table.fields.map(
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
174
|
+
const fieldLines = table.fields.map((f) => {
|
|
175
|
+
const virtualTag =
|
|
176
|
+
f.calculated && !f.stored ? " (virtual, read-only)" : "";
|
|
177
|
+
return ` * ${f.name} with type: ${f.pretty_type.replace(
|
|
178
|
+
"Key to",
|
|
179
|
+
"ForeignKey referencing"
|
|
180
|
+
)}${virtualTag}.${f.description ? ` ${f.description}` : ""}`;
|
|
181
|
+
});
|
|
130
182
|
tableLines.push(
|
|
131
183
|
`${table.name}${
|
|
132
184
|
table.description ? `: ${table.description}.` : "."
|
|
133
185
|
} Contains the following fields:\n${fieldLines.join("\n")}`
|
|
134
186
|
);
|
|
135
187
|
});
|
|
136
|
-
return `Use the generate_tables tool to
|
|
188
|
+
return `Use the generate_tables tool to create new database tables or to add/update fields on existing ones.
|
|
137
189
|
|
|
138
|
-
Do not call
|
|
139
|
-
|
|
140
|
-
tables.
|
|
190
|
+
Do not call generate_tables more than once. Use a single call even when working with multiple
|
|
191
|
+
tables. Include all tables — new and existing — in that one call.
|
|
141
192
|
|
|
142
193
|
The argument to generate_tables is an array of tables, each with an array of fields. You do not
|
|
143
|
-
need to specify a primary key
|
|
144
|
-
|
|
194
|
+
need to specify a primary key; a primary key called id with auto-incrementing integers is
|
|
195
|
+
automatically generated.
|
|
196
|
+
|
|
197
|
+
## New vs existing tables
|
|
198
|
+
|
|
199
|
+
If a table does not yet exist it will be created with all the specified fields.
|
|
200
|
+
|
|
201
|
+
If a table already exists, include it in the generate_tables call anyway with the fields you
|
|
202
|
+
want to add or update. The system will automatically add new fields and update the settings of
|
|
203
|
+
existing fields — it will not recreate or drop the table.
|
|
204
|
+
|
|
205
|
+
If a user requests creating a table with certain fields and the table already exists, automatically add any missing fields to that table. Do not ask the user for confirmation or prompt them again—just proceed with the table update.
|
|
206
|
+
|
|
207
|
+
If a table has a ForeignKey field that references another table which does not yet exist in the
|
|
208
|
+
database, include that referenced table in the same generate_tables call. Infer reasonable
|
|
209
|
+
fields for it from context.
|
|
145
210
|
|
|
146
|
-
|
|
147
|
-
that already exists, explain that generate_tables can only add new tables (not modify existing
|
|
148
|
-
ones) and omit those tables from the tool call.
|
|
211
|
+
## Calculated fields
|
|
149
212
|
|
|
150
|
-
|
|
213
|
+
Use calculated fields when the value should be derived from an expression rather than entered
|
|
214
|
+
directly. Set calculated=true and provide an expression (a JavaScript expression evaluated in
|
|
215
|
+
the context of the row — field names are available as variables).
|
|
216
|
+
|
|
217
|
+
Examples: 'price * quantity', 'first_name + " " + last_name', 'year - birth_year'
|
|
218
|
+
|
|
219
|
+
Choose between stored and non-stored:
|
|
220
|
+
- stored=false (default): value computed on-the-fly; no database column created.
|
|
221
|
+
Good for simple derivations from fields in the same table.
|
|
222
|
+
- stored=true: value persisted in the database and updated on writes. Required if you need to
|
|
223
|
+
sort/filter by the calculated value or if the expression references joined (related) tables.
|
|
224
|
+
|
|
225
|
+
For stored calculated fields, joinfield syntax lets expressions reference related-table fields:
|
|
226
|
+
- Single join: 'foreignKeyField.targetField' (e.g. 'author_id.full_name')
|
|
227
|
+
- Two-level join: 'fkField.throughFkField.deepField' (e.g. 'order_id.customer_id.country')
|
|
228
|
+
|
|
229
|
+
## Aggregation fields
|
|
230
|
+
|
|
231
|
+
For fields that count or aggregate rows from a child table (e.g. counting related records),
|
|
232
|
+
use aggregation=true instead of calculated=true. Aggregation fields are always stored.
|
|
233
|
+
|
|
234
|
+
Required properties when aggregation=true:
|
|
235
|
+
- aggregate_function: 'Count', 'Sum', 'Avg', 'CountUnique', 'Max', or 'Min'
|
|
236
|
+
- child_table: name of the child table
|
|
237
|
+
- child_fk_field: name of the FK field on the child table pointing back to this table
|
|
238
|
+
- aggregate_field: field on the child table to aggregate ('id' for Count)
|
|
239
|
+
- aggregate_where: (optional) JS expression to filter child rows, e.g. 'completed' to count
|
|
240
|
+
only rows where completed=true
|
|
241
|
+
|
|
242
|
+
Example — count completed packing items for a trip:
|
|
243
|
+
{ name: 'packed_count', label: 'Packed Count', type_and_configuration: {data_type: 'Integer'},
|
|
244
|
+
aggregation: true, aggregate_function: 'Count', child_table: 'packing_items',
|
|
245
|
+
child_fk_field: 'trip_id', aggregate_field: 'id', aggregate_where: 'completed' }
|
|
246
|
+
|
|
247
|
+
Do NOT use calculated=true with a hand-written expression for child-table counts — the
|
|
248
|
+
expression has no access to child rows and will always return 0.
|
|
249
|
+
|
|
250
|
+
The type_and_configuration.data_type for a calculated field should reflect the return type of
|
|
251
|
+
the expression (e.g. Integer, Float, String, Bool).
|
|
252
|
+
|
|
253
|
+
## Existing tables
|
|
254
|
+
|
|
255
|
+
The database already contains the following tables:
|
|
151
256
|
|
|
152
257
|
${tableLines.join("\n\n")}
|
|
153
258
|
|
|
154
259
|
`;
|
|
155
260
|
}
|
|
156
|
-
|
|
261
|
+
|
|
262
|
+
static render_html({ tables }, delay) {
|
|
157
263
|
const sctables = this.process_tables(tables);
|
|
158
264
|
const mmdia = buildMermaidMarkup(sctables);
|
|
265
|
+
if (delay === true) {
|
|
266
|
+
return pre({ class: "mermaid", "mm-src": mmdia });
|
|
267
|
+
}
|
|
159
268
|
return (
|
|
160
269
|
pre({ class: "mermaid" }, mmdia) +
|
|
161
270
|
script(
|
|
@@ -164,8 +273,8 @@ class GenerateTables {
|
|
|
164
273
|
mermaid.initialize({ startOnLoad: false });
|
|
165
274
|
mermaid.run({ querySelector: ".mermaid" });
|
|
166
275
|
});
|
|
167
|
-
`)
|
|
168
|
-
)
|
|
276
|
+
`)
|
|
277
|
+
)
|
|
169
278
|
);
|
|
170
279
|
}
|
|
171
280
|
|
|
@@ -184,49 +293,125 @@ class GenerateTables {
|
|
|
184
293
|
});
|
|
185
294
|
}
|
|
186
295
|
|
|
296
|
+
static async execute_add_or_update_fields({ table_name, fields }, req) {
|
|
297
|
+
const table = Table.findOne({ name: table_name });
|
|
298
|
+
if (!table) throw new Error(`Table "${table_name}" not found`);
|
|
299
|
+
|
|
300
|
+
const existingFieldMap = new Map();
|
|
301
|
+
(table.fields || []).forEach((f) => {
|
|
302
|
+
if (f?.name) existingFieldMap.set(f.name.toLowerCase(), f);
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
const sanitized = Array.isArray(fields)
|
|
306
|
+
? fields.filter((f) => (f?.name || "").toLowerCase() !== "id")
|
|
307
|
+
: [];
|
|
308
|
+
|
|
309
|
+
const added = [];
|
|
310
|
+
const updated = [];
|
|
311
|
+
|
|
312
|
+
for (const f of sanitized) {
|
|
313
|
+
const fname = (f?.name || "").toLowerCase();
|
|
314
|
+
if (!fname) continue;
|
|
315
|
+
const processed = this.process_field(f, []);
|
|
316
|
+
const existing = existingFieldMap.get(fname);
|
|
317
|
+
if (!existing) {
|
|
318
|
+
processed.table = table;
|
|
319
|
+
await Field.create(processed);
|
|
320
|
+
added.push(f.name);
|
|
321
|
+
} else {
|
|
322
|
+
// Only update non-structural properties; skip if type would change
|
|
323
|
+
const existingType = existing.type?.name ?? existing.type;
|
|
324
|
+
if (existingType === processed.type) {
|
|
325
|
+
const fieldUpdates = {
|
|
326
|
+
label: f.label,
|
|
327
|
+
attributes: processed.attributes,
|
|
328
|
+
};
|
|
329
|
+
if (f.calculated !== undefined)
|
|
330
|
+
fieldUpdates.calculated = !!f.calculated;
|
|
331
|
+
if (f.stored !== undefined) fieldUpdates.stored = !!f.stored;
|
|
332
|
+
if (f.expression !== undefined)
|
|
333
|
+
fieldUpdates.expression = f.expression;
|
|
334
|
+
await db.update("_sc_fields", fieldUpdates, existing.id);
|
|
335
|
+
updated.push(f.name);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
Trigger.emitEvent(
|
|
341
|
+
"AppChange",
|
|
342
|
+
`Fields updated on ${table_name}`,
|
|
343
|
+
req?.user,
|
|
344
|
+
{ entity_type: "Table", entity_names: [table_name] }
|
|
345
|
+
);
|
|
346
|
+
return { added, updated };
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
static process_field(f, allTablesList = []) {
|
|
350
|
+
if (f.aggregation) {
|
|
351
|
+
const { data_type } = f.type_and_configuration || {
|
|
352
|
+
data_type: "Integer",
|
|
353
|
+
};
|
|
354
|
+
return {
|
|
355
|
+
name: f.name,
|
|
356
|
+
label: f.label,
|
|
357
|
+
type: data_type,
|
|
358
|
+
calculated: true,
|
|
359
|
+
stored: true,
|
|
360
|
+
expression: "__aggregation",
|
|
361
|
+
attributes: {
|
|
362
|
+
aggregate: f.aggregate_function || "Count",
|
|
363
|
+
agg_field: `${f.aggregate_field || "id"}@${data_type}`,
|
|
364
|
+
agg_relation: `${f.child_table}.${f.child_fk_field}`,
|
|
365
|
+
table: f.child_table,
|
|
366
|
+
ref: f.child_fk_field,
|
|
367
|
+
aggwhere: f.aggregate_where || "",
|
|
368
|
+
importance: f.importance,
|
|
369
|
+
},
|
|
370
|
+
};
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
const { data_type, reference_table, ...attributes } =
|
|
374
|
+
f.type_and_configuration || { data_type: "String" };
|
|
375
|
+
let type = data_type;
|
|
376
|
+
const scattributes = { ...attributes, importance: f.importance };
|
|
377
|
+
if (data_type === "ForeignKey") {
|
|
378
|
+
type = `Key to ${reference_table}`;
|
|
379
|
+
const refTableHere = allTablesList.find(
|
|
380
|
+
(t) => t.table_name === reference_table
|
|
381
|
+
);
|
|
382
|
+
if (refTableHere) {
|
|
383
|
+
const strFields = (refTableHere.fields || []).filter(
|
|
384
|
+
(rf) => rf.type_and_configuration?.data_type === "String"
|
|
385
|
+
);
|
|
386
|
+
if (strFields.length) {
|
|
387
|
+
const maxImp = strFields.reduce((prev, current) =>
|
|
388
|
+
prev && prev.importance > current.importance ? prev : current
|
|
389
|
+
);
|
|
390
|
+
if (maxImp) scattributes.summary_field = maxImp.name;
|
|
391
|
+
}
|
|
392
|
+
} else if (reference_table === "users") {
|
|
393
|
+
scattributes.summary_field = "email";
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
return {
|
|
397
|
+
...f,
|
|
398
|
+
type,
|
|
399
|
+
required: f.not_null,
|
|
400
|
+
calculated: f.calculated || false,
|
|
401
|
+
stored: f.stored || false,
|
|
402
|
+
expression: f.expression,
|
|
403
|
+
attributes: scattributes,
|
|
404
|
+
};
|
|
405
|
+
}
|
|
406
|
+
|
|
187
407
|
static process_tables(tables) {
|
|
188
408
|
return tables.map((table) => {
|
|
189
409
|
const sanitizedFields = Array.isArray(table.fields)
|
|
190
|
-
? table.fields.filter(
|
|
191
|
-
(f) => (f?.name || "").toLowerCase() !== "id"
|
|
192
|
-
)
|
|
410
|
+
? table.fields.filter((f) => (f?.name || "").toLowerCase() !== "id")
|
|
193
411
|
: [];
|
|
194
412
|
return new Table({
|
|
195
413
|
name: table.table_name,
|
|
196
|
-
fields: sanitizedFields.map((f) =>
|
|
197
|
-
const { data_type, reference_table, ...attributes } =
|
|
198
|
-
f.type_and_configuration;
|
|
199
|
-
let type = data_type;
|
|
200
|
-
const scattributes = { ...attributes, importance: f.importance };
|
|
201
|
-
if (data_type === "ForeignKey") {
|
|
202
|
-
type = `Key to ${reference_table}`;
|
|
203
|
-
let refTableHere = tables.find(
|
|
204
|
-
(t) => t.table_name === reference_table
|
|
205
|
-
);
|
|
206
|
-
if (refTableHere) {
|
|
207
|
-
const strFields = refTableHere.fields.filter(
|
|
208
|
-
(f) => f.type_and_configuration.data_type === "String"
|
|
209
|
-
);
|
|
210
|
-
if (strFields.length) {
|
|
211
|
-
const maxImp = strFields.reduce(function (prev, current) {
|
|
212
|
-
return prev && prev.importance > current.importance
|
|
213
|
-
? prev
|
|
214
|
-
: current;
|
|
215
|
-
});
|
|
216
|
-
if (maxImp) scattributes.summary_field = maxImp.name;
|
|
217
|
-
}
|
|
218
|
-
} else if (reference_table === "users") {
|
|
219
|
-
scattributes.summary_field = "email";
|
|
220
|
-
}
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
return {
|
|
224
|
-
...f,
|
|
225
|
-
type,
|
|
226
|
-
required: f.not_null,
|
|
227
|
-
attributes: scattributes,
|
|
228
|
-
};
|
|
229
|
-
}),
|
|
414
|
+
fields: sanitizedFields.map((f) => this.process_field(f, tables)),
|
|
230
415
|
});
|
|
231
416
|
});
|
|
232
417
|
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
const Trigger = require("@saltcorn/data/models/trigger");
|
|
2
|
+
const Table = require("@saltcorn/data/models/table");
|
|
3
|
+
const { a, div, pre } = require("@saltcorn/markup/tags");
|
|
4
|
+
|
|
5
|
+
class GenerateTrigger {
|
|
6
|
+
static title = "Generate Trigger";
|
|
7
|
+
static function_name = "generate_trigger";
|
|
8
|
+
static description =
|
|
9
|
+
"Generate a Saltcorn trigger with any available action type";
|
|
10
|
+
|
|
11
|
+
static render_html({
|
|
12
|
+
action_name,
|
|
13
|
+
action_type,
|
|
14
|
+
when_trigger,
|
|
15
|
+
trigger_table,
|
|
16
|
+
action_config,
|
|
17
|
+
}) {
|
|
18
|
+
const summary =
|
|
19
|
+
`<strong>${action_name}</strong> — ${action_type}` +
|
|
20
|
+
(when_trigger ? `: ${when_trigger}` : "") +
|
|
21
|
+
(trigger_table ? ` on ${trigger_table}` : "");
|
|
22
|
+
const configSection =
|
|
23
|
+
action_config && Object.keys(action_config).length
|
|
24
|
+
? pre({ class: "mt-2" }, JSON.stringify(action_config, null, 2))
|
|
25
|
+
: "";
|
|
26
|
+
return div({ class: "mb-3" }, summary) + configSection;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
static async execute(
|
|
30
|
+
{ action_name, action_type, when_trigger, trigger_table, action_config },
|
|
31
|
+
req,
|
|
32
|
+
) {
|
|
33
|
+
let table_id;
|
|
34
|
+
if (trigger_table) {
|
|
35
|
+
const table = Table.findOne({ name: trigger_table });
|
|
36
|
+
if (!table) return { postExec: `Table not found: ${trigger_table}` };
|
|
37
|
+
table_id = table.id;
|
|
38
|
+
}
|
|
39
|
+
const trigger = await Trigger.create({
|
|
40
|
+
name: action_name,
|
|
41
|
+
when_trigger: when_trigger || "Never",
|
|
42
|
+
table_id,
|
|
43
|
+
action: action_type,
|
|
44
|
+
configuration: action_config || {},
|
|
45
|
+
});
|
|
46
|
+
Trigger.emitEvent("AppChange", `Trigger ${trigger.name}`, req?.user, {
|
|
47
|
+
entity_type: "Trigger",
|
|
48
|
+
entity_name: trigger.name,
|
|
49
|
+
});
|
|
50
|
+
return {
|
|
51
|
+
postExec:
|
|
52
|
+
"Trigger created. " +
|
|
53
|
+
a(
|
|
54
|
+
{ target: "_blank", href: `/actions/configure/${trigger.id}` },
|
|
55
|
+
"Configure trigger.",
|
|
56
|
+
),
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
module.exports = GenerateTrigger;
|