@saltcorn/copilot 0.7.4 → 0.8.0
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-workflow.js +83 -36
- package/actions/install-plugin-action.js +103 -0
- package/agent-skills/app-constructor-context.js +25 -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/registry-editor.js +911 -0
- package/agent-skills/viewgen.js +302 -53
- 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 +58 -0
- package/app-constructor/requirements.js +156 -0
- package/app-constructor/run_task.js +128 -0
- package/app-constructor/schema.js +186 -0
- package/app-constructor/taskchart.js +70 -0
- package/app-constructor/tasks.js +401 -0
- package/app-constructor/tools.js +81 -0
- package/app-constructor/view.js +209 -0
- package/builder-gen.js +1505 -24
- package/builder-schema.js +706 -0
- package/common.js +20 -0
- package/copilot-as-agent.js +6 -1
- package/index.js +24 -1
- package/js-code-gen.js +65 -0
- package/package.json +1 -1
- package/standard-prompt.js +83 -0
- package/tests/builder-gen.test.js +56 -0
|
@@ -0,0 +1,911 @@
|
|
|
1
|
+
const Trigger = require("@saltcorn/data/models/trigger");
|
|
2
|
+
const Page = require("@saltcorn/data/models/page");
|
|
3
|
+
const View = require("@saltcorn/data/models/view");
|
|
4
|
+
const Table = require("@saltcorn/data/models/table");
|
|
5
|
+
const Field = require("@saltcorn/data/models/field");
|
|
6
|
+
const User = require("@saltcorn/data/models/user");
|
|
7
|
+
const Plugin = require("@saltcorn/data/models/plugin");
|
|
8
|
+
const Role = require("@saltcorn/data/models/role");
|
|
9
|
+
const WorkflowStep = require("@saltcorn/data/models/workflow_step");
|
|
10
|
+
const { getState } = require("@saltcorn/data/db/state");
|
|
11
|
+
|
|
12
|
+
class RegistryEditorSkill {
|
|
13
|
+
static skill_name = "Registry editor";
|
|
14
|
+
|
|
15
|
+
get skill_label() {
|
|
16
|
+
return "Registry editor";
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
constructor(cfg) {
|
|
20
|
+
Object.assign(this, cfg);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async systemPrompt() {
|
|
24
|
+
return `
|
|
25
|
+
All saltcorn application entities (tables, views, pages and triggers) are defined by JSON
|
|
26
|
+
objects. There are separate namespaces for each entity type, so it could be that there are
|
|
27
|
+
entities with the same name in different entity types.
|
|
28
|
+
|
|
29
|
+
If you need to find entities in the application, you can use the list_entities
|
|
30
|
+
tool which will enumerate all the entities of the specified type with some high level
|
|
31
|
+
details about them but not the full configuration details.
|
|
32
|
+
|
|
33
|
+
If you need to retrieve the JSON definition of an entity, use the get_entity tool. This will
|
|
34
|
+
return the JSON definition. It must be called with both the entity type and name as arguments.
|
|
35
|
+
|
|
36
|
+
If you need to set the JSON definition of an entity, use the set_entity tool It must be called
|
|
37
|
+
with both the entity type and name, and the new JSON definition as a string as arguments.
|
|
38
|
+
`;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
provideTools = () => {
|
|
42
|
+
return [
|
|
43
|
+
{
|
|
44
|
+
type: "function",
|
|
45
|
+
function: {
|
|
46
|
+
name: "list_entities",
|
|
47
|
+
description: "List entities in the app of a specified type",
|
|
48
|
+
parameters: {
|
|
49
|
+
type: "object",
|
|
50
|
+
required: ["entity_type"],
|
|
51
|
+
properties: {
|
|
52
|
+
entity_type: {
|
|
53
|
+
type: "string",
|
|
54
|
+
description: "The entity type of which to list all entities",
|
|
55
|
+
enum: [
|
|
56
|
+
"view",
|
|
57
|
+
"table",
|
|
58
|
+
"page",
|
|
59
|
+
"trigger",
|
|
60
|
+
"available-plugins",
|
|
61
|
+
"installed-plugins",
|
|
62
|
+
"system-configuration-keys",
|
|
63
|
+
"roles",
|
|
64
|
+
"viewtemplates",
|
|
65
|
+
"types",
|
|
66
|
+
],
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
process: async (input) => {
|
|
72
|
+
const tables = await Table.find({}, { cached: true });
|
|
73
|
+
const tableNames = {};
|
|
74
|
+
for (const table of tables) tableNames[table.id] = table.name;
|
|
75
|
+
switch (input.entity_type) {
|
|
76
|
+
case "roles":
|
|
77
|
+
return await User.get_roles();
|
|
78
|
+
case "system-configuration-keys": {
|
|
79
|
+
const cfgs = getState().configs;
|
|
80
|
+
return Object.keys(cfgs).map((k) => ({
|
|
81
|
+
key: k,
|
|
82
|
+
description: cfgs[k].description,
|
|
83
|
+
}));
|
|
84
|
+
}
|
|
85
|
+
case "available-plugins": {
|
|
86
|
+
const store_plugins = await Plugin.store_plugins_available();
|
|
87
|
+
const installed_plugins = await Plugin.find({});
|
|
88
|
+
const installed_names = new Set(
|
|
89
|
+
installed_plugins.map((p) => p.name),
|
|
90
|
+
);
|
|
91
|
+
return store_plugins.map((p) => ({
|
|
92
|
+
name: p.name,
|
|
93
|
+
description: p.description,
|
|
94
|
+
documentation_link: p.documentation_link,
|
|
95
|
+
installed: installed_names.has(p.name),
|
|
96
|
+
}));
|
|
97
|
+
}
|
|
98
|
+
case "installed-plugins":
|
|
99
|
+
const installed_plugins = await Plugin.find({});
|
|
100
|
+
return installed_plugins.map((p) => ({
|
|
101
|
+
name: p.name,
|
|
102
|
+
description: p.description,
|
|
103
|
+
documentation_link: p.documentation_link,
|
|
104
|
+
}));
|
|
105
|
+
case "view":
|
|
106
|
+
const allViews = await View.find();
|
|
107
|
+
return allViews.map((v) => ({
|
|
108
|
+
name: v.name,
|
|
109
|
+
viewtemplate: v.viewtemplate,
|
|
110
|
+
description: v.description,
|
|
111
|
+
table: v.table_id ? tableNames[v.table_id] : undefined,
|
|
112
|
+
}));
|
|
113
|
+
case "table":
|
|
114
|
+
return tables.map((v) => ({
|
|
115
|
+
name: v.name,
|
|
116
|
+
description: v.description,
|
|
117
|
+
}));
|
|
118
|
+
case "page":
|
|
119
|
+
const allPages = await Page.find({}, { cached: true });
|
|
120
|
+
return allPages.map((p) => ({
|
|
121
|
+
name: p.name,
|
|
122
|
+
description: p.description,
|
|
123
|
+
}));
|
|
124
|
+
case "viewtemplates": {
|
|
125
|
+
const viewtemplates = getState().viewtemplates;
|
|
126
|
+
return Object.keys(viewtemplates).map((name) => ({
|
|
127
|
+
name,
|
|
128
|
+
description: viewtemplates[name].description,
|
|
129
|
+
}));
|
|
130
|
+
}
|
|
131
|
+
case "types": {
|
|
132
|
+
const types = getState().types;
|
|
133
|
+
return Object.keys(types).map((name) => ({
|
|
134
|
+
name,
|
|
135
|
+
description: types[name].description,
|
|
136
|
+
}));
|
|
137
|
+
}
|
|
138
|
+
case "trigger":
|
|
139
|
+
const allTriggers = Trigger.find({});
|
|
140
|
+
return allTriggers.map((tr) => ({
|
|
141
|
+
name: tr.name,
|
|
142
|
+
description: tr.description,
|
|
143
|
+
action: tr.action,
|
|
144
|
+
when_trigger: tr.when_trigger,
|
|
145
|
+
table: tr.table_id ? tableNames[tr.table_id] : undefined,
|
|
146
|
+
}));
|
|
147
|
+
}
|
|
148
|
+
},
|
|
149
|
+
},
|
|
150
|
+
{
|
|
151
|
+
type: "function",
|
|
152
|
+
function: {
|
|
153
|
+
name: "get_entity",
|
|
154
|
+
description:
|
|
155
|
+
"Get the full JSON definition of an entity by name and entity type",
|
|
156
|
+
parameters: {
|
|
157
|
+
type: "object",
|
|
158
|
+
required: ["entity_type", "entity_name"],
|
|
159
|
+
properties: {
|
|
160
|
+
entity_type: {
|
|
161
|
+
type: "string",
|
|
162
|
+
description: "The type of the entity to retrieve",
|
|
163
|
+
enum: [
|
|
164
|
+
"view",
|
|
165
|
+
"table",
|
|
166
|
+
"page",
|
|
167
|
+
"trigger",
|
|
168
|
+
"plugin",
|
|
169
|
+
"system-configuration-value",
|
|
170
|
+
"type",
|
|
171
|
+
],
|
|
172
|
+
},
|
|
173
|
+
entity_name: {
|
|
174
|
+
type: "string",
|
|
175
|
+
description: "The name of the entity to retrieve",
|
|
176
|
+
},
|
|
177
|
+
},
|
|
178
|
+
},
|
|
179
|
+
},
|
|
180
|
+
process: async (input) => {
|
|
181
|
+
const tables = await Table.find({}, { cached: true });
|
|
182
|
+
const tableNames = {};
|
|
183
|
+
for (const table of tables) tableNames[table.id] = table.name;
|
|
184
|
+
|
|
185
|
+
const fmt = (entityType, name, value, schema) =>
|
|
186
|
+
`The definition of the "${name}" ${entityType} is:\n${JSON.stringify(value, null, 2)}\n\n` +
|
|
187
|
+
`JSON schema for set_entity (entity_type: "${entityType}"):\n${JSON.stringify(schema, null, 2)}`;
|
|
188
|
+
|
|
189
|
+
switch (input.entity_type) {
|
|
190
|
+
case "view": {
|
|
191
|
+
const view = await View.findOne({ name: input.entity_name });
|
|
192
|
+
if (!view) return `view not found`;
|
|
193
|
+
const value = {
|
|
194
|
+
name: view.name,
|
|
195
|
+
description: view.description,
|
|
196
|
+
viewtemplate: view.viewtemplate,
|
|
197
|
+
configuration: view.configuration,
|
|
198
|
+
min_role: view.min_role,
|
|
199
|
+
...(view.table_id ? { table: tableNames[view.table_id] } : {}),
|
|
200
|
+
...(view.exttable_name
|
|
201
|
+
? { exttable_name: view.exttable_name }
|
|
202
|
+
: {}),
|
|
203
|
+
menu_label: view.menu_label,
|
|
204
|
+
slug: view.slug,
|
|
205
|
+
attributes: view.attributes,
|
|
206
|
+
default_render_page: view.default_render_page,
|
|
207
|
+
};
|
|
208
|
+
const schema = {
|
|
209
|
+
name: { type: "string", description: "name of the view" },
|
|
210
|
+
viewtemplate: {
|
|
211
|
+
type: "string",
|
|
212
|
+
description:
|
|
213
|
+
"viewtemplate to use. Use list_entities viewtemplates to see options",
|
|
214
|
+
},
|
|
215
|
+
table: {
|
|
216
|
+
type: "string",
|
|
217
|
+
description: "name of the table (use this, not table_id)",
|
|
218
|
+
},
|
|
219
|
+
min_role: {
|
|
220
|
+
type: "number",
|
|
221
|
+
description:
|
|
222
|
+
"minimum role id required. 100=public, 80=user. Use list_entities roles for valid ids",
|
|
223
|
+
},
|
|
224
|
+
description: {
|
|
225
|
+
type: "string",
|
|
226
|
+
description: "optional description",
|
|
227
|
+
},
|
|
228
|
+
configuration: {
|
|
229
|
+
type: "object",
|
|
230
|
+
description: `viewtemplate-specific configuration for the '${view.viewtemplate}' viewtemplate`,
|
|
231
|
+
},
|
|
232
|
+
menu_label: {
|
|
233
|
+
type: "string",
|
|
234
|
+
description: "optional menu label to show in navigation",
|
|
235
|
+
},
|
|
236
|
+
default_render_page: {
|
|
237
|
+
type: "string",
|
|
238
|
+
description:
|
|
239
|
+
"optional page name to render instead of the view",
|
|
240
|
+
},
|
|
241
|
+
slug: {
|
|
242
|
+
type: "object",
|
|
243
|
+
description: "optional URL slug configuration",
|
|
244
|
+
},
|
|
245
|
+
attributes: {
|
|
246
|
+
type: "object",
|
|
247
|
+
description: "optional view attributes",
|
|
248
|
+
},
|
|
249
|
+
};
|
|
250
|
+
return fmt("view", view.name, value, schema);
|
|
251
|
+
}
|
|
252
|
+
case "table": {
|
|
253
|
+
const table = Table.findOne({ name: input.entity_name });
|
|
254
|
+
if (!table) return `table not found`;
|
|
255
|
+
const value = table.to_json;
|
|
256
|
+
const fieldSchema = {
|
|
257
|
+
type: "array",
|
|
258
|
+
description: "array of field definitions",
|
|
259
|
+
items: {
|
|
260
|
+
name: {
|
|
261
|
+
type: "string",
|
|
262
|
+
description: "field name (snake_case)",
|
|
263
|
+
},
|
|
264
|
+
label: { type: "string", description: "display label" },
|
|
265
|
+
type: {
|
|
266
|
+
type: "string",
|
|
267
|
+
description:
|
|
268
|
+
"field type name. Use list_entities types for valid options",
|
|
269
|
+
},
|
|
270
|
+
required: { type: "boolean" },
|
|
271
|
+
description: { type: "string" },
|
|
272
|
+
attributes: {
|
|
273
|
+
type: "object",
|
|
274
|
+
description:
|
|
275
|
+
"type-specific attributes. Use get_entity type <typename> for valid attributes",
|
|
276
|
+
},
|
|
277
|
+
},
|
|
278
|
+
};
|
|
279
|
+
const schema = {
|
|
280
|
+
name: { type: "string", description: "table name" },
|
|
281
|
+
description: { type: "string" },
|
|
282
|
+
min_role_read: {
|
|
283
|
+
type: "number",
|
|
284
|
+
description:
|
|
285
|
+
"role id for read access. Use list_entities roles",
|
|
286
|
+
},
|
|
287
|
+
min_role_write: {
|
|
288
|
+
type: "number",
|
|
289
|
+
description:
|
|
290
|
+
"role id for write access. Use list_entities roles",
|
|
291
|
+
},
|
|
292
|
+
versioned: {
|
|
293
|
+
type: "boolean",
|
|
294
|
+
description: "whether to keep row history",
|
|
295
|
+
},
|
|
296
|
+
ownership_formula: {
|
|
297
|
+
type: "string",
|
|
298
|
+
description: "formula to determine row ownership",
|
|
299
|
+
},
|
|
300
|
+
ownership_field_name: {
|
|
301
|
+
type: "string",
|
|
302
|
+
description: "field name that stores owner user id",
|
|
303
|
+
},
|
|
304
|
+
fields: fieldSchema,
|
|
305
|
+
constraints: {
|
|
306
|
+
type: "array",
|
|
307
|
+
description: "array of table constraints",
|
|
308
|
+
},
|
|
309
|
+
};
|
|
310
|
+
return fmt("table", table.name, value, schema);
|
|
311
|
+
}
|
|
312
|
+
case "system-configuration-value": {
|
|
313
|
+
const v = getState().getConfig(input.entity_name);
|
|
314
|
+
const cfgMeta = getState().configs[input.entity_name];
|
|
315
|
+
if (cfgMeta?.description) {
|
|
316
|
+
return `The value of "${input.entity_name}" is: ${JSON.stringify(v)}\n\nDescription: ${cfgMeta.description}`;
|
|
317
|
+
}
|
|
318
|
+
return v;
|
|
319
|
+
}
|
|
320
|
+
case "plugin": {
|
|
321
|
+
const plugin = await Plugin.findOne({ name: input.entity_name });
|
|
322
|
+
if (!plugin) return `plugin not found`;
|
|
323
|
+
return plugin;
|
|
324
|
+
}
|
|
325
|
+
case "page": {
|
|
326
|
+
const page = Page.findOne({ name: input.entity_name });
|
|
327
|
+
if (!page) return `page not found`;
|
|
328
|
+
const root_page_for_roles = await page.is_root_page_for_roles();
|
|
329
|
+
const value = {
|
|
330
|
+
name: page.name,
|
|
331
|
+
title: page.title,
|
|
332
|
+
description: page.description,
|
|
333
|
+
min_role: page.min_role,
|
|
334
|
+
layout: page.layout,
|
|
335
|
+
fixed_states: page.fixed_states,
|
|
336
|
+
menu_label: page.menu_label,
|
|
337
|
+
attributes: page.attributes,
|
|
338
|
+
root_page_for_roles,
|
|
339
|
+
};
|
|
340
|
+
const schema = {
|
|
341
|
+
name: { type: "string", description: "page name" },
|
|
342
|
+
title: {
|
|
343
|
+
type: "string",
|
|
344
|
+
description: "page title shown in browser tab",
|
|
345
|
+
},
|
|
346
|
+
description: { type: "string" },
|
|
347
|
+
min_role: {
|
|
348
|
+
type: "number",
|
|
349
|
+
description:
|
|
350
|
+
"minimum role id required. 100=public, 80=user. Use list_entities roles for valid ids",
|
|
351
|
+
},
|
|
352
|
+
layout: {
|
|
353
|
+
type: "object",
|
|
354
|
+
description: "page layout definition",
|
|
355
|
+
},
|
|
356
|
+
fixed_states: {
|
|
357
|
+
type: "object",
|
|
358
|
+
description: "fixed state values for embedded views",
|
|
359
|
+
},
|
|
360
|
+
menu_label: {
|
|
361
|
+
type: "string",
|
|
362
|
+
description: "optional menu label",
|
|
363
|
+
},
|
|
364
|
+
attributes: { type: "object" },
|
|
365
|
+
};
|
|
366
|
+
return fmt("page", page.name, value, schema);
|
|
367
|
+
}
|
|
368
|
+
case "trigger": {
|
|
369
|
+
const trigger = Trigger.findOne({ name: input.entity_name });
|
|
370
|
+
if (!trigger) return `trigger not found`;
|
|
371
|
+
const value = trigger.toJson;
|
|
372
|
+
const schema = {
|
|
373
|
+
name: { type: "string", description: "trigger name" },
|
|
374
|
+
action: {
|
|
375
|
+
type: "string",
|
|
376
|
+
description:
|
|
377
|
+
"action to run. Use list_entities actions or installed-plugins to find valid actions",
|
|
378
|
+
},
|
|
379
|
+
when_trigger: {
|
|
380
|
+
type: "string",
|
|
381
|
+
description:
|
|
382
|
+
"when to fire. Options: Insert, Update, Delete (require a table), Weekly, Daily, Hourly, Never (no table needed), or a custom event name. Use 'Never' for triggers that are called programmatically or on demand.",
|
|
383
|
+
},
|
|
384
|
+
table: {
|
|
385
|
+
type: "string",
|
|
386
|
+
description:
|
|
387
|
+
"table name — only required when when_trigger is Insert, Update, or Delete. Omit for Never, time-based, or event triggers.",
|
|
388
|
+
},
|
|
389
|
+
description: { type: "string" },
|
|
390
|
+
min_role: {
|
|
391
|
+
type: "number",
|
|
392
|
+
description: "minimum role id. Use list_entities roles",
|
|
393
|
+
},
|
|
394
|
+
configuration: {
|
|
395
|
+
type: "object",
|
|
396
|
+
description: "action-specific configuration",
|
|
397
|
+
},
|
|
398
|
+
};
|
|
399
|
+
return fmt("trigger", trigger.name, value, schema);
|
|
400
|
+
}
|
|
401
|
+
case "type": {
|
|
402
|
+
const types = getState().types;
|
|
403
|
+
const type = types[input.entity_name];
|
|
404
|
+
if (!type) return `type not found`;
|
|
405
|
+
const serializeField = (f) => {
|
|
406
|
+
const {
|
|
407
|
+
name,
|
|
408
|
+
label,
|
|
409
|
+
type: ftype,
|
|
410
|
+
description,
|
|
411
|
+
required,
|
|
412
|
+
options,
|
|
413
|
+
} = f;
|
|
414
|
+
return {
|
|
415
|
+
name,
|
|
416
|
+
label,
|
|
417
|
+
type: typeof ftype === "string" ? ftype : ftype?.name,
|
|
418
|
+
description,
|
|
419
|
+
required,
|
|
420
|
+
options,
|
|
421
|
+
};
|
|
422
|
+
};
|
|
423
|
+
const result = {
|
|
424
|
+
name: type.name,
|
|
425
|
+
description: type.description,
|
|
426
|
+
};
|
|
427
|
+
if (Array.isArray(type.attributes)) {
|
|
428
|
+
result.attributes = type.attributes.map(serializeField);
|
|
429
|
+
} else if (typeof type.attributes === "function") {
|
|
430
|
+
result.attributes = "dynamic (depends on table context)";
|
|
431
|
+
}
|
|
432
|
+
if (type.fieldviews) {
|
|
433
|
+
result.fieldviews = Object.entries(type.fieldviews).map(
|
|
434
|
+
([fvName, fv]) => {
|
|
435
|
+
const fvInfo = {
|
|
436
|
+
name: fvName,
|
|
437
|
+
description: fv.description,
|
|
438
|
+
isEdit: fv.isEdit || false,
|
|
439
|
+
isFilter: fv.isFilter || false,
|
|
440
|
+
};
|
|
441
|
+
if (Array.isArray(fv.configFields)) {
|
|
442
|
+
fvInfo.configFields = fv.configFields.map(serializeField);
|
|
443
|
+
} else if (typeof fv.configFields === "function") {
|
|
444
|
+
fvInfo.configFields = "dynamic";
|
|
445
|
+
}
|
|
446
|
+
return fvInfo;
|
|
447
|
+
},
|
|
448
|
+
);
|
|
449
|
+
}
|
|
450
|
+
return result;
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
},
|
|
454
|
+
},
|
|
455
|
+
{
|
|
456
|
+
type: "function",
|
|
457
|
+
function: {
|
|
458
|
+
name: "set_entity",
|
|
459
|
+
description:
|
|
460
|
+
"Save a new JSON definition for an entity. Creates a new entity or overwrites an existing entity, depending on whether an entity with this name and type already exists",
|
|
461
|
+
parameters: {
|
|
462
|
+
type: "object",
|
|
463
|
+
required: ["entity_type", "entity_name"],
|
|
464
|
+
properties: {
|
|
465
|
+
entity_type: {
|
|
466
|
+
type: "string",
|
|
467
|
+
description: "The type of the entity to set",
|
|
468
|
+
enum: [
|
|
469
|
+
"view",
|
|
470
|
+
"table",
|
|
471
|
+
"page",
|
|
472
|
+
"trigger",
|
|
473
|
+
"system-configuration-value",
|
|
474
|
+
"module-configuration",
|
|
475
|
+
"role",
|
|
476
|
+
],
|
|
477
|
+
},
|
|
478
|
+
entity_name: {
|
|
479
|
+
type: "string",
|
|
480
|
+
description: "The name of the entity to set",
|
|
481
|
+
},
|
|
482
|
+
entity_definition: {
|
|
483
|
+
type: "string",
|
|
484
|
+
description:
|
|
485
|
+
"The new entity definition JSON object, stringified",
|
|
486
|
+
},
|
|
487
|
+
},
|
|
488
|
+
},
|
|
489
|
+
},
|
|
490
|
+
process: async (input, ctx) => {
|
|
491
|
+
try {
|
|
492
|
+
const entityValue = JSON.parse(input.entity_definition);
|
|
493
|
+
const tables = await Table.find({}, { cached: true });
|
|
494
|
+
const tableNames = {};
|
|
495
|
+
for (const table of tables) tableNames[table.id] = table.name;
|
|
496
|
+
switch (input.entity_type) {
|
|
497
|
+
case "view": {
|
|
498
|
+
const {
|
|
499
|
+
table,
|
|
500
|
+
on_menu,
|
|
501
|
+
menu_label,
|
|
502
|
+
on_root_page,
|
|
503
|
+
viewname,
|
|
504
|
+
...viewNoTable
|
|
505
|
+
} = entityValue;
|
|
506
|
+
|
|
507
|
+
if (viewname && !viewNoTable.name) viewNoTable.name = viewname;
|
|
508
|
+
if (!viewNoTable.name) viewNoTable.name = input.entity_name;
|
|
509
|
+
|
|
510
|
+
if (typeof viewNoTable.table_id === "string") {
|
|
511
|
+
const t = Table.findOne({ name: viewNoTable.table_id });
|
|
512
|
+
if (!t) return `Table '${viewNoTable.table_id}' not found`;
|
|
513
|
+
viewNoTable.table_id = t.id;
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
const tableName =
|
|
517
|
+
table ||
|
|
518
|
+
entityValue.configuration?.table_name ||
|
|
519
|
+
entityValue.configuration?.exttable_name;
|
|
520
|
+
if (tableName && !viewNoTable.table_id) {
|
|
521
|
+
const thetable = Table.findOne({ name: tableName });
|
|
522
|
+
if (!thetable) return `Table '${tableName}' not found`;
|
|
523
|
+
viewNoTable.table_id = thetable.id;
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
if (!viewNoTable.min_role) viewNoTable.min_role = 100;
|
|
527
|
+
if (
|
|
528
|
+
viewNoTable.table_id == null &&
|
|
529
|
+
!viewNoTable.exttable_name &&
|
|
530
|
+
!entityValue.configuration?.exttable_name
|
|
531
|
+
)
|
|
532
|
+
return `View requires a table. Provide a 'table' field with the table name.`;
|
|
533
|
+
|
|
534
|
+
const existing = await View.findOne({
|
|
535
|
+
name: input.entity_name,
|
|
536
|
+
});
|
|
537
|
+
const oldValues = existing
|
|
538
|
+
? {
|
|
539
|
+
viewtemplate: existing.viewtemplate,
|
|
540
|
+
configuration: existing.configuration,
|
|
541
|
+
table_id: existing.table_id,
|
|
542
|
+
min_role: existing.min_role,
|
|
543
|
+
slug: existing.slug,
|
|
544
|
+
attributes: existing.attributes,
|
|
545
|
+
default_render_page: existing.default_render_page,
|
|
546
|
+
exttable_name: existing.exttable_name,
|
|
547
|
+
}
|
|
548
|
+
: null;
|
|
549
|
+
|
|
550
|
+
if (existing?.id) {
|
|
551
|
+
await View.update(viewNoTable, existing.id);
|
|
552
|
+
} else {
|
|
553
|
+
try {
|
|
554
|
+
await View.create(viewNoTable);
|
|
555
|
+
} catch (e) {
|
|
556
|
+
return `Error creating view: ${e.message}`;
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
await getState().refresh_views();
|
|
560
|
+
|
|
561
|
+
const errors = [];
|
|
562
|
+
const warnings = [];
|
|
563
|
+
const savedView = await View.findOne({
|
|
564
|
+
name: input.entity_name,
|
|
565
|
+
});
|
|
566
|
+
|
|
567
|
+
if (savedView?.viewtemplateObj?.configCheck) {
|
|
568
|
+
try {
|
|
569
|
+
const issues =
|
|
570
|
+
await savedView.viewtemplateObj.configCheck(savedView);
|
|
571
|
+
if (Array.isArray(issues) && issues.length > 0) {
|
|
572
|
+
errors.push(...issues);
|
|
573
|
+
} else if (!Array.isArray(issues)) {
|
|
574
|
+
if (Array.isArray(issues.errors)) {
|
|
575
|
+
errors.push(...issues.errors);
|
|
576
|
+
}
|
|
577
|
+
if (Array.isArray(issues.warnings)) {
|
|
578
|
+
warnings.push(...issues.warnings);
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
} catch (e) {
|
|
582
|
+
errors.push(`configCheck error: ${e.message}`);
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
if (errors.length === 0 && savedView) {
|
|
587
|
+
try {
|
|
588
|
+
const sfs = await savedView.get_state_fields();
|
|
589
|
+
const needsPk =
|
|
590
|
+
savedView.table_id &&
|
|
591
|
+
sfs.some((f) => f.primary_key || f.name === "id");
|
|
592
|
+
if (needsPk) {
|
|
593
|
+
const tbl = Table.findOne({ id: savedView.table_id });
|
|
594
|
+
const pk = tbl.pk_name;
|
|
595
|
+
const rows = await tbl.getRows(
|
|
596
|
+
{},
|
|
597
|
+
{ orderBy: "RANDOM()", limit: 1 },
|
|
598
|
+
);
|
|
599
|
+
if (rows.length > 0) {
|
|
600
|
+
await savedView.run(
|
|
601
|
+
{ [pk]: rows[0][pk] },
|
|
602
|
+
{ req: ctx?.req },
|
|
603
|
+
);
|
|
604
|
+
}
|
|
605
|
+
if (rows.length === 0 || sfs.every((f) => !f.required)) {
|
|
606
|
+
await savedView.run({}, { req: ctx?.req });
|
|
607
|
+
}
|
|
608
|
+
} else {
|
|
609
|
+
await savedView.run({}, { req: ctx?.req });
|
|
610
|
+
}
|
|
611
|
+
} catch (e) {
|
|
612
|
+
errors.push(`render error: ${e.message}`);
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
if (errors.length > 0) {
|
|
617
|
+
try {
|
|
618
|
+
if (oldValues) {
|
|
619
|
+
await View.update(oldValues, existing.id);
|
|
620
|
+
} else {
|
|
621
|
+
const newView = await View.findOne({
|
|
622
|
+
name: input.entity_name,
|
|
623
|
+
});
|
|
624
|
+
if (newView) await newView.delete();
|
|
625
|
+
}
|
|
626
|
+
await getState().refresh_views();
|
|
627
|
+
} catch (re) {
|
|
628
|
+
errors.push(`(rollback failed: ${re.message})`);
|
|
629
|
+
}
|
|
630
|
+
const msg = [
|
|
631
|
+
`Errors found, changes not applied:\n${errors.join("\n")}`,
|
|
632
|
+
];
|
|
633
|
+
if (warnings.length)
|
|
634
|
+
msg.push(`Warnings: ${warnings.join("\n")}`);
|
|
635
|
+
return msg.join("\n");
|
|
636
|
+
}
|
|
637
|
+
if (warnings.length)
|
|
638
|
+
return `Done (warnings: ${warnings.join("; ")})`;
|
|
639
|
+
break;
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
case "system-configuration-value":
|
|
643
|
+
await getState().setConfig(input.entity_name, entityValue);
|
|
644
|
+
await getState().refresh_config();
|
|
645
|
+
break;
|
|
646
|
+
|
|
647
|
+
case "page": {
|
|
648
|
+
const { root_page_for_roles, menu_label, ...pageSpec } =
|
|
649
|
+
entityValue;
|
|
650
|
+
|
|
651
|
+
if (!pageSpec.min_role) {
|
|
652
|
+
pageSpec.min_role = 100;
|
|
653
|
+
} else {
|
|
654
|
+
const roles = await User.get_roles();
|
|
655
|
+
const roleExists = roles.some(
|
|
656
|
+
(r) => r.id === pageSpec.min_role,
|
|
657
|
+
);
|
|
658
|
+
if (!roleExists) pageSpec.min_role = 100;
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
const existing = Page.findOne({ name: input.entity_name });
|
|
662
|
+
|
|
663
|
+
const oldValues = existing
|
|
664
|
+
? {
|
|
665
|
+
title: existing.title,
|
|
666
|
+
description: existing.description,
|
|
667
|
+
min_role: existing.min_role,
|
|
668
|
+
layout: existing.layout,
|
|
669
|
+
fixed_states: existing.fixed_states,
|
|
670
|
+
menu_label: existing.menu_label,
|
|
671
|
+
attributes: existing.attributes,
|
|
672
|
+
}
|
|
673
|
+
: null;
|
|
674
|
+
|
|
675
|
+
if (existing?.id) await Page.update(existing.id, pageSpec);
|
|
676
|
+
else {
|
|
677
|
+
try {
|
|
678
|
+
await Page.create(pageSpec);
|
|
679
|
+
} catch (e) {
|
|
680
|
+
return `Error creating page: ${e.message}`;
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
await getState().refresh_pages();
|
|
684
|
+
|
|
685
|
+
const errors = [];
|
|
686
|
+
try {
|
|
687
|
+
const savedPage = Page.findOne({ name: input.entity_name });
|
|
688
|
+
if (savedPage) await savedPage.run({}, { req: ctx?.req });
|
|
689
|
+
} catch (e) {
|
|
690
|
+
errors.push(`render error: ${e.message}`);
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
if (errors.length > 0) {
|
|
694
|
+
try {
|
|
695
|
+
if (oldValues) {
|
|
696
|
+
await Page.update(existing.id, oldValues);
|
|
697
|
+
} else {
|
|
698
|
+
const newPage = Page.findOne({ name: input.entity_name });
|
|
699
|
+
if (newPage) await newPage.delete();
|
|
700
|
+
}
|
|
701
|
+
await getState().refresh_pages();
|
|
702
|
+
} catch (re) {
|
|
703
|
+
errors.push(`(rollback failed: ${re.message})`);
|
|
704
|
+
}
|
|
705
|
+
return `Errors found, changes not applied:\n${errors.join("\n")}`;
|
|
706
|
+
}
|
|
707
|
+
break;
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
case "trigger": {
|
|
711
|
+
const existing = await Trigger.findOne({
|
|
712
|
+
name: entityValue.name || input.entity_name,
|
|
713
|
+
});
|
|
714
|
+
const { table, table_name, steps, ...tsNoTableName } =
|
|
715
|
+
entityValue;
|
|
716
|
+
if (table || table_name)
|
|
717
|
+
tsNoTableName.table_id = Table.findOne(
|
|
718
|
+
table || table_name,
|
|
719
|
+
)?.id;
|
|
720
|
+
|
|
721
|
+
// Pre-check action before saving
|
|
722
|
+
if (tsNoTableName.action) {
|
|
723
|
+
const action = getState().actions[tsNoTableName.action];
|
|
724
|
+
if (!action)
|
|
725
|
+
return `Action '${tsNoTableName.action}' not found`;
|
|
726
|
+
if (action.configCheck) {
|
|
727
|
+
try {
|
|
728
|
+
const tbl = tsNoTableName.table_id
|
|
729
|
+
? Table.findOne({ id: tsNoTableName.table_id })
|
|
730
|
+
: undefined;
|
|
731
|
+
const errs = await action.configCheck({
|
|
732
|
+
table: tbl,
|
|
733
|
+
...tsNoTableName,
|
|
734
|
+
});
|
|
735
|
+
console.log({ errs });
|
|
736
|
+
if (Array.isArray(errs) && errs.length > 0)
|
|
737
|
+
return `Errors found, changes not applied:\n${errs.join("\n")}`;
|
|
738
|
+
} catch (e) {
|
|
739
|
+
return `configCheck error: ${e.message}`;
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
let id;
|
|
745
|
+
if (existing) {
|
|
746
|
+
await Trigger.update(existing.id, tsNoTableName);
|
|
747
|
+
id = existing.id;
|
|
748
|
+
} else {
|
|
749
|
+
try {
|
|
750
|
+
const newTrigger = await Trigger.create(tsNoTableName);
|
|
751
|
+
id = newTrigger.id;
|
|
752
|
+
} catch (error) {
|
|
753
|
+
return `Error creating trigger: ${error.message}`;
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
if (entityValue.action === "Workflow" && entityValue.steps) {
|
|
757
|
+
await WorkflowStep.deleteForTrigger(id);
|
|
758
|
+
for (const step of entityValue.steps) {
|
|
759
|
+
await WorkflowStep.create({ ...step, trigger_id: id });
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
await getState().refresh_triggers();
|
|
763
|
+
break;
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
case "table": {
|
|
767
|
+
const {
|
|
768
|
+
id,
|
|
769
|
+
ownership_field_id,
|
|
770
|
+
ownership_field_name,
|
|
771
|
+
triggers,
|
|
772
|
+
constraints,
|
|
773
|
+
fields,
|
|
774
|
+
...updrow
|
|
775
|
+
} = entityValue;
|
|
776
|
+
|
|
777
|
+
const existing = Table.findOne({ name: input.entity_name });
|
|
778
|
+
if (existing) {
|
|
779
|
+
await existing.update(updrow);
|
|
780
|
+
} else {
|
|
781
|
+
await Table.create(input.entity_name, entityValue);
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
await getState().refresh_tables(true);
|
|
785
|
+
const _table = Table.findOne({ name: input.entity_name });
|
|
786
|
+
if (!_table)
|
|
787
|
+
throw new Error(
|
|
788
|
+
`Unable to find table '${input.entity_name}'`,
|
|
789
|
+
);
|
|
790
|
+
|
|
791
|
+
const exfields = _table.getFields();
|
|
792
|
+
if (!_table.provider_name)
|
|
793
|
+
for (const field of fields) {
|
|
794
|
+
const exfield = exfields.find((f) => f.name === field.name);
|
|
795
|
+
if (
|
|
796
|
+
!(
|
|
797
|
+
(_table.name === "users" &&
|
|
798
|
+
(field.name === "email" ||
|
|
799
|
+
field.name === "role_id")) ||
|
|
800
|
+
exfield
|
|
801
|
+
)
|
|
802
|
+
) {
|
|
803
|
+
if (_table.name === "users" && field.required)
|
|
804
|
+
await Field.create({
|
|
805
|
+
table: _table,
|
|
806
|
+
...field,
|
|
807
|
+
required: false,
|
|
808
|
+
});
|
|
809
|
+
else await Field.create({ table: _table, ...field });
|
|
810
|
+
} else if (
|
|
811
|
+
exfield &&
|
|
812
|
+
!(
|
|
813
|
+
_table.name === "users" &&
|
|
814
|
+
(field.name === "email" || field.name === "role_id")
|
|
815
|
+
) &&
|
|
816
|
+
exfield.type
|
|
817
|
+
) {
|
|
818
|
+
const { id: _id, table_id, ...fieldUpdrow } = field;
|
|
819
|
+
await exfield.update(fieldUpdrow);
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
const existing_constraints = _table.constraints;
|
|
824
|
+
for (const constraint of constraints || []) {
|
|
825
|
+
if (
|
|
826
|
+
!existing_constraints.find(
|
|
827
|
+
(excon) =>
|
|
828
|
+
excon.type === constraint.type &&
|
|
829
|
+
isEqual(excon.configuration, constraint.configuration),
|
|
830
|
+
)
|
|
831
|
+
)
|
|
832
|
+
await TableConstraint.create({
|
|
833
|
+
table: _table,
|
|
834
|
+
...constraint,
|
|
835
|
+
});
|
|
836
|
+
}
|
|
837
|
+
if (ownership_field_name) {
|
|
838
|
+
const owner_field = await Field.findOne({
|
|
839
|
+
table_id: _table.id,
|
|
840
|
+
name: ownership_field_name,
|
|
841
|
+
});
|
|
842
|
+
await _table.update({ ownership_field_id: owner_field.id });
|
|
843
|
+
}
|
|
844
|
+
await getState().refresh_tables();
|
|
845
|
+
|
|
846
|
+
// Check calculated fields
|
|
847
|
+
const calcErrors = [];
|
|
848
|
+
for (const field of _table
|
|
849
|
+
.getFields()
|
|
850
|
+
.filter((f) => f.calculated && f.expression)) {
|
|
851
|
+
try {
|
|
852
|
+
const rows = await _table.getRows({}, { limit: 1 });
|
|
853
|
+
if (rows.length > 0) {
|
|
854
|
+
const {
|
|
855
|
+
eval_expression,
|
|
856
|
+
} = require("@saltcorn/data/models/expression");
|
|
857
|
+
eval_expression(
|
|
858
|
+
field.expression,
|
|
859
|
+
rows[0],
|
|
860
|
+
ctx?.req?.user,
|
|
861
|
+
);
|
|
862
|
+
}
|
|
863
|
+
} catch (e) {
|
|
864
|
+
calcErrors.push(
|
|
865
|
+
`Calculated field '${field.name}': ${e.message}`,
|
|
866
|
+
);
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
if (calcErrors.length > 0)
|
|
870
|
+
return `Table saved but calculated field errors found:\n${calcErrors.join("\n")}`;
|
|
871
|
+
|
|
872
|
+
break;
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
case "module-configuration": {
|
|
876
|
+
const plugin = await Plugin.findOne({
|
|
877
|
+
name: input.entity_name,
|
|
878
|
+
});
|
|
879
|
+
if (!plugin) return `module not found: ${input.entity_name}`;
|
|
880
|
+
plugin.configuration = {
|
|
881
|
+
...(plugin.configuration || {}),
|
|
882
|
+
...entityValue,
|
|
883
|
+
};
|
|
884
|
+
await plugin.upsert();
|
|
885
|
+
getState().plugin_cfgs[input.entity_name] =
|
|
886
|
+
plugin.configuration;
|
|
887
|
+
await getState().refresh_plugins();
|
|
888
|
+
return "Module configuration updated";
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
case "role": {
|
|
892
|
+
const result = await Role.create({
|
|
893
|
+
role: input.entity_name,
|
|
894
|
+
...entityValue,
|
|
895
|
+
});
|
|
896
|
+
if (result?.error) return result.error;
|
|
897
|
+
await getState().refresh_roles();
|
|
898
|
+
return "Role created";
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
return "Done";
|
|
902
|
+
} catch (e) {
|
|
903
|
+
return `An error occurred: ${e?.message || e}`;
|
|
904
|
+
}
|
|
905
|
+
},
|
|
906
|
+
},
|
|
907
|
+
];
|
|
908
|
+
};
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
module.exports = RegistryEditorSkill;
|