@saltcorn/copilot 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2023 Saltcorn
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,3 @@
1
+ # Saltcorn copilot
2
+
3
+ AI assistant for building Saltcorn applications
@@ -0,0 +1,174 @@
1
+ const Field = require("@saltcorn/data/models/field");
2
+ const Table = require("@saltcorn/data/models/table");
3
+ const Form = require("@saltcorn/data/models/form");
4
+ const View = require("@saltcorn/data/models/view");
5
+ const Trigger = require("@saltcorn/data/models/trigger");
6
+ const db = require("@saltcorn/data/db");
7
+ const Workflow = require("@saltcorn/data/models/workflow");
8
+ const { renderForm } = require("@saltcorn/markup");
9
+ const { div, script, domReady, pre, code } = require("@saltcorn/markup/tags");
10
+ const { getState } = require("@saltcorn/data/db/state");
11
+ const {
12
+ getCompletion,
13
+ getPromptFromTemplate,
14
+ incompleteCfgMsg,
15
+ } = require("./common");
16
+
17
+ const get_state_fields = () => [];
18
+
19
+ const getForm = async ({ viewname, body, hasCode }) => {
20
+ const tables = await Table.find({});
21
+ const table_triggers = ["Insert", "Update", "Delete", "Validate"];
22
+
23
+ const hasChannel = Object.entries(getState().eventTypes)
24
+ .filter(([k, v]) => v.hasChannel)
25
+ .map(([k, v]) => k);
26
+ const fields = [
27
+ {
28
+ name: "name",
29
+ label: "Name",
30
+ type: "String",
31
+ required: true,
32
+ sublabel: "Name of action",
33
+ },
34
+ {
35
+ name: "when_trigger",
36
+ label: "When",
37
+ input_type: "select",
38
+ required: true,
39
+ options: Trigger.when_options.map((t) => ({ value: t, label: t })),
40
+ sublabel: "Event type which runs the trigger",
41
+ help: { topic: "Event types" },
42
+ attributes: {
43
+ explainers: {
44
+ Often: "Every 5 minutes",
45
+ Never:
46
+ "Not scheduled but can be run as an action from a button click",
47
+ },
48
+ },
49
+ },
50
+ {
51
+ name: "table_id",
52
+ label: "Table",
53
+ input_type: "select",
54
+ options: [...tables.map((t) => ({ value: t.id, label: t.name }))],
55
+ showIf: { when_trigger: table_triggers },
56
+ sublabel: "The table for which the trigger condition is checked.",
57
+ },
58
+ {
59
+ name: "channel",
60
+ label: "Channel",
61
+ type: "String",
62
+ sublabel: "Leave blank for all channels",
63
+ showIf: { when_trigger: hasChannel },
64
+ },
65
+ {
66
+ name: "prompt",
67
+ label: "Action description",
68
+ fieldview: "textarea",
69
+ sublabel: "What would you like this action to do?",
70
+ type: "String",
71
+ },
72
+ ...(hasCode
73
+ ? [
74
+ {
75
+ name: "code",
76
+ label: "Generated code",
77
+ fieldview: "textarea",
78
+ attributes: { mode: "application/javascript" },
79
+ input_type: "code",
80
+ },
81
+ ]
82
+ : []),
83
+ ];
84
+ const form = new Form({
85
+ action: `/view/${viewname}`,
86
+ fields,
87
+ submitLabel: body?.prompt ? "Re-generate code" : "Generate code",
88
+ additionalButtons: body?.prompt
89
+ ? [
90
+ {
91
+ label: "Save as trigger",
92
+ onclick: "save_as_action(this)",
93
+ class: "btn btn-primary",
94
+ afterSave: true,
95
+ },
96
+ ]
97
+ : undefined,
98
+ });
99
+ return form;
100
+ };
101
+
102
+ const js = (viewname) =>
103
+ script(`
104
+ function save_as_action(that) {
105
+ const form = $(that).closest('form');
106
+ view_post("${viewname}", "save_as_action", $(form).serialize())
107
+ }
108
+ `);
109
+
110
+ const run = async (table_id, viewname, cfg, state, { res, req }) => {
111
+ const form = await getForm({ viewname });
112
+ const cfgMsg = incompleteCfgMsg();
113
+ if (cfgMsg) return cfgMsg;
114
+ else return renderForm(form, req.csrfToken());
115
+ };
116
+
117
+ const runPost = async (
118
+ table_id,
119
+ viewname,
120
+ config,
121
+ state,
122
+ body,
123
+ { req, res }
124
+ ) => {
125
+ const form = await getForm({ viewname, body, hasCode: true });
126
+ form.validate(body);
127
+
128
+ form.hasErrors = false;
129
+ form.errors = {};
130
+
131
+ const fullPrompt = await getPromptFromTemplate(
132
+ "action-builder.txt",
133
+ form.values.prompt
134
+ );
135
+
136
+ const completion = await getCompletion("JavaScript", fullPrompt);
137
+
138
+ form.values.code = completion;
139
+ res.sendWrap("Action Builder Copilot", [
140
+ renderForm(form, req.csrfToken()),
141
+ js(viewname),
142
+ ]);
143
+ };
144
+
145
+ const save_as_action = async (table_id, viewname, config, body, { req }) => {
146
+ const form = await getForm({ viewname, body, hasCode: true });
147
+ form.validate(body);
148
+ if (!form.hasErrors) {
149
+ const { name, when_trigger, table_id, channel, prompt, code } = form.values;
150
+ await Trigger.create({
151
+ name,
152
+ when_trigger,
153
+ table_id,
154
+ channel,
155
+ description: prompt,
156
+ action: "run_js_code",
157
+ configuration: { code },
158
+ });
159
+
160
+ return { json: { success: "ok", notify: `Trigger ${name} created` } };
161
+ }
162
+ return { json: { error: "Form incomplete" } };
163
+ };
164
+
165
+ module.exports = {
166
+ name: "Action Builder Copilot",
167
+ display_state_form: false,
168
+ get_state_fields,
169
+ tableless: true,
170
+ singleton: true,
171
+ run,
172
+ runPost: runPost,
173
+ routes: { save_as_action },
174
+ };
package/common.js ADDED
@@ -0,0 +1,60 @@
1
+ const axios = require("axios");
2
+ const fsp = require("fs").promises;
3
+ const _ = require("underscore");
4
+ const path = require("path");
5
+ const Field = require("@saltcorn/data/models/field");
6
+ const Table = require("@saltcorn/data/models/table");
7
+ const Form = require("@saltcorn/data/models/form");
8
+ const View = require("@saltcorn/data/models/view");
9
+ const Trigger = require("@saltcorn/data/models/trigger");
10
+ const { getState } = require("@saltcorn/data/db/state");
11
+
12
+ const getPromptFromTemplate = async (tmplName, userPrompt, extraCtx = {}) => {
13
+ const tables = await Table.find({});
14
+ const context = {
15
+ Table,
16
+ tables,
17
+ View,
18
+ userTable: Table.findOne("users"),
19
+ scState: getState(),
20
+ userPrompt,
21
+ ...extraCtx,
22
+ };
23
+ const fp = path.join(__dirname, "prompts", tmplName);
24
+ const fileBuf = await fsp.readFile(fp);
25
+ const tmpl = fileBuf.toString();
26
+ const template = _.template(tmpl, {
27
+ evaluate: /\{\{#(.+?)\}\}/g,
28
+ interpolate: /\{\{([^#].+?)\}\}/g,
29
+ });
30
+ const prompt = template(context);
31
+ console.log("Full prompt:\n", prompt);
32
+ return prompt;
33
+ };
34
+
35
+ const getCompletion = async (language, prompt) => {
36
+ return getState().functions.llm_generate.run(prompt, {
37
+ systemPrompt: `You are a helpful code assistant. Your language of choice is ${language}. Do not include any explanation, just generate the code block itself.`,
38
+ });
39
+ };
40
+
41
+ const incompleteCfgMsg = () => {
42
+ const plugin_cfgs = getState().plugin_cfgs;
43
+
44
+ if (
45
+ !plugin_cfgs["@saltcorn/large-language-model"] &&
46
+ !plugin_cfgs["large-language-model"]
47
+ ) {
48
+ const modName = Object.keys(plugin_cfgs).find((m) =>
49
+ m.includes("large-language-model")
50
+ );
51
+ if (modName)
52
+ return `LLM module not configured. Please configure <a href="/plugins/configure/${encodeURIComponent(
53
+ modName
54
+ )}">here<a> before using copilot.`;
55
+ else
56
+ return `LLM module not configured. Please install and configure <a href="/plugins">here<a> before using copilot.`;
57
+ }
58
+ };
59
+
60
+ module.exports = { getCompletion, getPromptFromTemplate, incompleteCfgMsg };
@@ -0,0 +1,277 @@
1
+ const Field = require("@saltcorn/data/models/field");
2
+ const Table = require("@saltcorn/data/models/table");
3
+ const Form = require("@saltcorn/data/models/form");
4
+ const View = require("@saltcorn/data/models/view");
5
+ const Trigger = require("@saltcorn/data/models/trigger");
6
+ const { findType } = require("@saltcorn/data/models/discovery");
7
+ const { save_menu_items } = require("@saltcorn/data/models/config");
8
+ const db = require("@saltcorn/data/db");
9
+ const Workflow = require("@saltcorn/data/models/workflow");
10
+ const { renderForm } = require("@saltcorn/markup");
11
+ const { div, script, domReady, pre, code } = require("@saltcorn/markup/tags");
12
+ const { getState } = require("@saltcorn/data/db/state");
13
+ const {
14
+ getCompletion,
15
+ getPromptFromTemplate,
16
+ incompleteCfgMsg,
17
+ } = require("./common");
18
+ const { Parser } = require("node-sql-parser");
19
+ const parser = new Parser();
20
+ const { initial_config_all_fields } = require("@saltcorn/data/plugin-helper");
21
+
22
+ const get_state_fields = () => [];
23
+
24
+ const getForm = async ({ viewname, body, hasCode }) => {
25
+ const fields = [
26
+ {
27
+ name: "prompt",
28
+ label: "Database description",
29
+ fieldview: "textarea",
30
+ sublabel: "What would you like to design a database for?",
31
+ type: "String",
32
+ },
33
+
34
+ ...(hasCode
35
+ ? [
36
+ {
37
+ name: "code",
38
+ label: "Generated code",
39
+ fieldview: "textarea",
40
+ attributes: { mode: "application/javascript" },
41
+ input_type: "code",
42
+ },
43
+ {
44
+ name: "basic_views",
45
+ label: "Generate views",
46
+ sublabel:
47
+ "Also generate basic views (Show, Edit, List) for the generated tables",
48
+ type: "Bool",
49
+ },
50
+ ]
51
+ : []),
52
+ ];
53
+ const form = new Form({
54
+ action: `/view/${viewname}`,
55
+ fields,
56
+ submitLabel: body?.prompt
57
+ ? "Re-generate database design"
58
+ : "Generate database design",
59
+ additionalButtons: body?.prompt
60
+ ? [
61
+ {
62
+ label: "Save this database permanently",
63
+ onclick: "save_database(this)",
64
+ class: "btn btn-primary",
65
+ afterSave: true,
66
+ },
67
+ ]
68
+ : undefined,
69
+ });
70
+ return form;
71
+ };
72
+
73
+ const js = (viewname) =>
74
+ script(`
75
+ function save_database(that) {
76
+ const form = $(that).closest('form');
77
+ view_post("${viewname}", "save_database", $(form).serialize())
78
+ }
79
+ `);
80
+
81
+ const run = async (table_id, viewname, cfg, state, { res, req }) => {
82
+ const form = await getForm({ viewname });
83
+ const cfgMsg = incompleteCfgMsg();
84
+ if (cfgMsg) return cfgMsg;
85
+ else return renderForm(form, req.csrfToken());
86
+ };
87
+
88
+ const runPost = async (
89
+ table_id,
90
+ viewname,
91
+ config,
92
+ state,
93
+ body,
94
+ { req, res }
95
+ ) => {
96
+ const form = await getForm({ viewname, body, hasCode: true });
97
+ form.validate(body);
98
+
99
+ form.hasErrors = false;
100
+ form.errors = {};
101
+
102
+ const fullPrompt = await getPromptFromTemplate(
103
+ "database-designer.txt",
104
+ form.values.prompt
105
+ );
106
+ const completion = await getCompletion("SQL", fullPrompt);
107
+
108
+ form.values.code = completion;
109
+ res.sendWrap("Databse Designer Copilot", [
110
+ renderForm(form, req.csrfToken()),
111
+ js(viewname),
112
+ ]);
113
+ };
114
+
115
+ const moreTypes = {
116
+ decimal: "Float",
117
+ varchar: "String",
118
+ };
119
+
120
+ const save_database = async (table_id, viewname, config, body, { req }) => {
121
+ const form = await getForm({ viewname, body, hasCode: true });
122
+ form.validate(body);
123
+
124
+ if (!form.hasErrors) {
125
+ const genTables = [];
126
+ const { tableList, ast } = parser.parse(form.values.code, {
127
+ database: "PostgreSQL",
128
+ });
129
+ const tables_to_create = [];
130
+ for (const { type, keyword, create_definitions, table } of ast) {
131
+ if (type !== "create" || keyword !== "table" || !table?.length) continue;
132
+ const tblName = table[0].table;
133
+
134
+ const fields = [];
135
+ for (const {
136
+ column,
137
+ definition,
138
+ primary_key,
139
+ reference_definition,
140
+ } of create_definitions) {
141
+ if (primary_key) continue;
142
+ let type =
143
+ findType(definition.dataType.toLowerCase()) ||
144
+ moreTypes[definition.dataType.toLowerCase()];
145
+ if (reference_definition)
146
+ type = `Key to ${reference_definition.table[0].table}`;
147
+
148
+ fields.push({
149
+ name: column.column,
150
+ type,
151
+ });
152
+
153
+ //onsole.log(fld, definition.dataType);
154
+ }
155
+ tables_to_create.push({ name: tblName, fields });
156
+ }
157
+ for (const table of tables_to_create) await Table.create(table.name);
158
+
159
+ for (const tbl of tables_to_create) {
160
+ const table = Table.findOne({ name: tbl.name });
161
+
162
+ for (const field of tbl.fields) {
163
+ field.table = table;
164
+ //pick summary field
165
+ if (field.type === "Key to users") {
166
+ field.attributes = { summary_field: "email" };
167
+ } else if (field.type.startsWith("Key to ")) {
168
+ const reftable_name = field.type.replace("Key to ", "");
169
+ const reftable = tables_to_create.find(
170
+ (t) => t.name === reftable_name
171
+ );
172
+ const summary_field = reftable.fields.find(
173
+ (f) => f.type === "String"
174
+ );
175
+ if (summary_field)
176
+ field.attributes = { summary_field: summary_field.name };
177
+ else field.attributes = { summary_field: "id" };
178
+ }
179
+ await Field.create(field);
180
+ }
181
+ }
182
+
183
+ if (form.values.basic_views)
184
+ for (const { name } of tables_to_create) {
185
+ const table = Table.findOne({ name });
186
+
187
+ const list = await initial_view(table, "List");
188
+ const edit = await initial_view(table, "Edit");
189
+ const show = await initial_view(table, "Show");
190
+ await View.update(
191
+ {
192
+ configuration: {
193
+ ...list.configuration,
194
+ columns: [
195
+ ...list.configuration.columns,
196
+ {
197
+ type: "ViewLink",
198
+ view: `Own:Show ${name}`,
199
+ view_name: `Show ${name}`,
200
+ link_style: "",
201
+ view_label: "Show",
202
+ header_label: "Show",
203
+ },
204
+ {
205
+ type: "ViewLink",
206
+ view: `Own:Edit ${name}`,
207
+ view_name: `Edit ${name}`,
208
+ link_style: "",
209
+ view_label: "Edit",
210
+ header_label: "Edit",
211
+ },
212
+ {
213
+ type: "Action",
214
+ action_name: "Delete",
215
+ action_style: "btn-primary",
216
+ },
217
+ ],
218
+ view_to_create: `Edit ${name}`,
219
+ },
220
+ },
221
+ list.id
222
+ );
223
+ await View.update(
224
+ {
225
+ configuration: {
226
+ ...edit.configuration,
227
+ view_when_done: `List ${name}`,
228
+ destination_type: "View",
229
+ },
230
+ },
231
+ edit.id
232
+ );
233
+ await add_to_menu({
234
+ label: name,
235
+ type: "View",
236
+ min_role: 100,
237
+ viewname: `List ${name}`,
238
+ });
239
+ }
240
+
241
+ return { json: { success: "ok", notify: `Database created` } };
242
+ }
243
+ return { json: { error: "Form incomplete" } };
244
+ };
245
+ const add_to_menu = async (item) => {
246
+ const current_menu = getState().getConfigCopy("menu_items", []);
247
+ const existing = current_menu.findIndex((m) => m.label === item.label);
248
+ if (existing >= 0) current_menu[existing] = item;
249
+ else current_menu.push(item);
250
+ await save_menu_items(current_menu);
251
+ };
252
+ const initial_view = async (table, viewtemplate) => {
253
+ const configuration = await initial_config_all_fields(
254
+ viewtemplate === "Edit"
255
+ )({ table_id: table.id });
256
+ //console.log(configuration);
257
+ const name = `${viewtemplate} ${table.name}`;
258
+ const view = await View.create({
259
+ name,
260
+ configuration,
261
+ viewtemplate,
262
+ table_id: table.id,
263
+ min_role: 100,
264
+ });
265
+ return view;
266
+ };
267
+
268
+ module.exports = {
269
+ name: "Database Design Copilot",
270
+ display_state_form: false,
271
+ get_state_fields,
272
+ tableless: true,
273
+ singleton: true,
274
+ run,
275
+ runPost: runPost,
276
+ routes: { save_database },
277
+ };
package/index.js ADDED
@@ -0,0 +1,14 @@
1
+ const Workflow = require("@saltcorn/data/models/workflow");
2
+ const Form = require("@saltcorn/data/models/form");
3
+
4
+ const configuration_workflow = () =>
5
+ new Workflow({
6
+ steps: [],
7
+ });
8
+
9
+ module.exports = {
10
+ sc_plugin_api_version: 1,
11
+ //configuration_workflow,
12
+ dependencies: ["@saltcorn/large-language-model"],
13
+ viewtemplates: [require("./action-builder"), require("./database-designer")],
14
+ };
package/package.json ADDED
@@ -0,0 +1,31 @@
1
+ {
2
+ "name": "@saltcorn/copilot",
3
+ "version": "0.1.0",
4
+ "description": "AI assistant for building Saltcorn applications",
5
+ "main": "index.js",
6
+ "dependencies": {
7
+ "@saltcorn/data": "^0.9.0",
8
+ "axios": "0.16.2",
9
+ "underscore": "1.13.6",
10
+ "node-sql-parser": "4.15.0"
11
+ },
12
+ "author": "Tom Nielsen",
13
+ "license": "MIT",
14
+ "repository": "github:saltcorn/copilot",
15
+ "eslintConfig": {
16
+ "extends": "eslint:recommended",
17
+ "parserOptions": {
18
+ "ecmaVersion": 2020
19
+ },
20
+ "env": {
21
+ "node": true,
22
+ "es6": true
23
+ },
24
+ "rules": {
25
+ "no-unused-vars": "off",
26
+ "no-case-declarations": "off",
27
+ "no-empty": "warn",
28
+ "no-fallthrough": "warn"
29
+ }
30
+ }
31
+ }
@@ -0,0 +1,307 @@
1
+ Your code can can manipulate rows in the database, manipulate files, interact
2
+ with remote APIs, or issue directives for the user's display.
3
+
4
+ Your code can use await at the top level, and should do so whenever calling
5
+ database queries or other aynchronous code (see example below)
6
+
7
+ The variable `table` is the associated table (if any; note lowercase). If you want to access a different table,
8
+ use the `Table` variable (note uppercase) to access the Table class of table.
9
+
10
+ Example:
11
+
12
+ await table.insertRow({name: "Alex", age: 43})
13
+ const otherTable = Table.findOne({name: "Orders"})
14
+ await otherTable.deleteRows({id: order})
15
+
16
+
17
+ You can use the Table class to access database tables. Use this to create or delete tables and
18
+ their properties, or to query or change table rows.
19
+
20
+ To query, update, insert or delete rows in an existing table, first you should find the
21
+ table object with findOne.
22
+
23
+ Example:
24
+
25
+ Table.findOne({name: "Customers"}) // find the table with name "Customers"
26
+ Table.findOne("Customers") // find the table with name "Customers" (shortcut)
27
+ Table.findOne({ id: 5 }) // find the table with id=5
28
+ Table.findOne(5) // find the table with id=5 (shortcut)
29
+
30
+ Table.findOne is synchronous (no need to await), But the functions that query and manipulate
31
+ (such as insertRow, getRows, updateRow, deleteRows) rows are mostly asyncronous, so you can
32
+ put the await in front of the whole expression.
33
+
34
+ Example:
35
+ To count the number of rows in the customer table:
36
+
37
+ const nrows = await Table.findOne("Customers").countRows({})
38
+
39
+ Querying table rows
40
+
41
+ There are several methods you can use to retrieve rows in the database:
42
+
43
+ countRows: Count the number of rows in db table. The argument is a where-expression with conditions the
44
+ counted rows should match. countRows returns the number of matching rows wrapped in a promise.
45
+
46
+ countRows(where?): Promise<number>
47
+ Count amount of rows in db table
48
+
49
+ Parameters
50
+ Optional where: Where
51
+ Returns Promise<number>
52
+
53
+ Example of using countRows:
54
+ const bookTable = Table.findOne({name: "books"})
55
+
56
+ // Count the total number of rows in the books table
57
+ const totalNumberOfBooks = await bookTable.countRows({})
58
+
59
+ // Count the number of books where the cover_color field has the value is "Red"
60
+ const numberOfRedBooks = await bookTable.countRows({cover_color: "Red"})
61
+
62
+ // Count number of books with more than 500 pages
63
+ const numberOfLongBooks = await bookTable.countRows({pages: {gt: 500}})
64
+
65
+ getRows: Get all matching rows from the table in the database.
66
+
67
+ The arguments are the same as for getRow. The first argument is where-expression
68
+ with the conditions to match, and the second argument is an optional object and
69
+ allows you to set ordering and limit options. Keywords that can be used in the
70
+ second argument are orderBy, orderDesc, limit and offset.
71
+
72
+ getRows will return an array of rows matching the where-expression in the first
73
+ argument, wrapped in a Promise (use await to read the array).
74
+
75
+
76
+ getRows(where?, selopts?): Promise<Row[]>
77
+ Get rows from Table in db
78
+
79
+ Parameters
80
+ where: Where = {}
81
+ selopts: SelectOptions & ForUserRequest = {}
82
+ Returns Promise<Row[]>
83
+
84
+ Example of using getRows:
85
+
86
+ const bookTable = Table.findOne({name: "books"})
87
+
88
+ // get the rows in the book table with author = "Henrik Pontoppidan"
89
+ const myBooks = await bookTable.getRows({author: "Henrik Pontoppidan"})
90
+
91
+ // get the 3 most recent books written by "Henrik Pontoppidan" with more that 500 pages
92
+ const myBooks = await bookTable.getRows({author: "Henrik Pontoppidan", pages: {gt: 500}}, {orderBy: "published", orderDesc: true})
93
+
94
+ getRow: Get one row from the table in the database. The matching row will be returned in a promise -
95
+ use await to read the value. If no matching rule can be found, null will be returned. If more than one
96
+ row matches, the first found row will be returned.
97
+
98
+ The first argument to get row is a where-expression With the conditions the returned row should match.
99
+
100
+ The second document is optional and is an object that can modify the search. This is mainly useful in
101
+ case there is more than one matching row for the where-expression in the first argument and you want to
102
+ give an explicit order. For example, use {orderBy: "name"} as the second argument to pick the first
103
+ row by the name field, ordered ascending. {orderBy: "name", orderDesc: true} to order by name, descending
104
+
105
+ This is however rare and usually getRow is run with a single argument of a Where expression that uniquely
106
+ determines the row to return, if it exisits.
107
+
108
+ getRow(where?, selopts?): Promise<null | Row>
109
+ Get one row from table in db
110
+
111
+ Parameters
112
+ where: Where = {}
113
+ selopts: SelectOptions & ForUserRequest = {}
114
+ Returns Promise<null | Row>
115
+
116
+ Example of using getRow:
117
+ const bookTable = Table.findOne({name: "books"})
118
+
119
+ // get the row in the book table with id = 5
120
+ const myBook = await bookTable.getRow({id: 5})
121
+
122
+ // get the row for the last book published by Leo Tolstoy
123
+ const myBook = await bookTable.getRow({author: "Leo Tolstoy"}, {orderBy: "published", orderDesc: true})
124
+
125
+ getJoinedRows: To retrieve rows together with joinfields and aggregations
126
+
127
+ getJoinedRows(opts?): Promise<Row[]>
128
+ Get rows along with joined and aggregated fields. The argument to getJoinedRows is an object with several different possible fields, all of which are optional
129
+
130
+ where: A Where expression indicating the criterion to match
131
+ joinFields: An object with the joinfields to retrieve
132
+ aggregations: An object with the aggregations to retrieve
133
+ orderBy: A string with the name of the field to order by
134
+ orderDesc: If true, descending order
135
+ limit: A number with the maximum number of rows to retrieve
136
+ offset: The number of rows to skip in the result before returning rows
137
+ Parameters
138
+ Optional opts: any = {}
139
+ Returns Promise<Row[]>
140
+
141
+ Example of using getJoinedRows:
142
+
143
+ const patients = Table.findOne({ name: "patients" });
144
+ const patients_rows = await patients.getJoinedRows({
145
+ where: { age: { gt: 65 } },
146
+ orderBy: "id",
147
+ aggregations: {
148
+ avg_temp: {
149
+ table: "readings",
150
+ ref: "patient_id",
151
+ field: "temperature",
152
+ aggregate: "avg",
153
+ },
154
+ },
155
+ joinFields: {
156
+ pages: { ref: "favbook", target: "pages" },
157
+ author: { ref: "favbook", target: "author" },
158
+ },
159
+ });
160
+
161
+ These functions all take "Where expressions" which are JavaScript objects describing
162
+ the criterion to match to. Some examples:
163
+
164
+ { name: "Jim" }: Match all rows with name="Jim"
165
+ { name: { ilike: "im"} }: Match all rows where name contains "im" (case insensitive)
166
+ { name: /im/ }: Match all rows with name matching regular expression "im"
167
+ { age: { lt: 18 } }: Match all rows with age<18
168
+ { age: { lt: 18, equal: true } }: Match all rows with age<=18
169
+ { age: { gt: 18, lt: 65} }: Match all rows with 18<age<65
170
+ { name: { or: ["Harry", "Sally"] } }: Match all rows with name="Harry" or "Sally"
171
+ { or: [{ name: "Joe"}, { age: 37 }] }: Match all rows with name="Joe" or age=37
172
+ { not: { id: 5 } }: All rows except id=5
173
+ { id: { in: [1, 2, 3] } }: Rows with id 1, 2, or 3
174
+
175
+ There are two nearly identical functions for updating rows depending on how you want failures treated
176
+
177
+ updateRow Update a row in the database table, throws an exception if update is invalid
178
+
179
+ updateRow(v_in, id, user?, noTrigger?, resultCollector?, restore_of_version?, syncTimestamp?): Promise<string | void>
180
+ Update row
181
+
182
+ Parameters
183
+ v_in: any. columns with values to update
184
+
185
+ id: number. id value, table primary key
186
+
187
+ Optional user: Row
188
+ Optional noTrigger: boolean
189
+ Optional resultCollector: object
190
+ Optional restore_of_version: any
191
+ Optional syncTimestamp: Date
192
+
193
+ Example of using updateRow:
194
+
195
+ const bookTable = Table.findOne({name: "books"})
196
+
197
+ // get the row in the book table for Moby Dick
198
+ const moby_dick = await bookTable.getRow({title: "Moby Dick"})
199
+
200
+ // Update the read field to true and the rating field to 5 in the retrieved row.
201
+ await bookTable.updateRow({read: true, rating: 5}, moby_dick.id)
202
+
203
+ // if you want to update more than one row, you must first retrieve all the rows and
204
+ // then update them individually
205
+
206
+ const allBooks = await bookTable.getRows()
207
+ for(const book of allBooks) {
208
+ await bookTable.updateRow({price: book.price*0.8}, book.id)
209
+ }
210
+
211
+ tryUpdateRow Update a row, return an error message if update is invalid
212
+
213
+ There are two nearly identical functions for inserting a new row depending on how you want failures treated
214
+
215
+ insertRow insert a row, throws an exception if it is invalid
216
+ insertRow(v_in, user?, resultCollector?, noTrigger?, syncTimestamp?): Promise<any>
217
+ Insert row into the table. By passing in the user as the second argument, tt will check write rights. If a user object is not supplied, the insert goes ahead without checking write permissions.
218
+
219
+ Returns the primary key value of the inserted row.
220
+
221
+ This will throw an exception if the row does not conform to the table constraints. If you would like to insert a row with a function that can return an error message, use tryInsertRow instead.
222
+
223
+ Parameters
224
+ v_in: Row
225
+ Optional user: Row
226
+ Optional resultCollector: object
227
+ Optional noTrigger: boolean
228
+ Optional syncTimestamp: Date
229
+ Returns Promise<any>
230
+
231
+ Example of using insertRow:
232
+ await Table.findOne("People").insertRow({ name: "Jim", age: 35 })
233
+
234
+ tryInsertRow insert a row, return an error message if it is invalid
235
+
236
+ Use deleteRows to delete any number (zero, one or many) of rows matching a criterion. It uses the same where expression as the functions for querying rows
237
+ deleteRows(where, user?, noTrigger?): Promise<void>
238
+ Delete rows from table
239
+
240
+ Parameters
241
+ where: Where
242
+ condition
243
+
244
+ Optional user: Row
245
+ optional user, if null then no authorization will be checked
246
+
247
+ Optional noTrigger: boolean
248
+ Returns Promise<void>
249
+
250
+ The following tables are present in the database:
251
+
252
+ {{# for (const table of tables) { }}
253
+ {{ table.name }} table with name = "{{ table.name }}" which has the following fields:
254
+ {{# for (const field of table.fields) { }} - {{ field.name }} of type {{ field.pretty_type }}
255
+ {{# } }}
256
+ {{# } }}
257
+
258
+ The code may run in a context of a single row. In that case the variable called `row`
259
+ can be used to access its data as a JavaScript object. The primary key value for the current row
260
+ can be accessed with row.id
261
+
262
+ In addition to `table` and `Table`, you can use other functions/variables:
263
+
264
+ sleep: A small utility function to sleep for certain number of milliseconds. Use this with await
265
+
266
+ Example: `await sleep(1000)`
267
+
268
+ `fetch` and `fetchJSON`: Use these to make HTTP API calls. `fetch` is the standard JavaScript `fetch` (provided by
269
+ [node-fetch](https://www.npmjs.com/package/node-fetch#common-usage)). `fetchJSON` performs a fetch
270
+ and then reads its reponse to JSON
271
+
272
+ Example:
273
+
274
+ const response = await fetch('https://api.github.com/users/github');
275
+ const data = await response.json();
276
+
277
+ which is the same as
278
+
279
+ const data = await fetchJSON('https://api.github.com/users/github');
280
+
281
+ The logged-in user Is available in the `user` variable as a JavaScript object. The primary key for the
282
+ logged-in user is accessed as `user.id` and their email address as `user.email`.
283
+
284
+ Your code can with its return value give directives to the current page.
285
+ Valid return values are:
286
+
287
+ notify: Send a pop-up notification indicating success to the user. Example: return { notify: "Order completed!" }
288
+
289
+ error: Send a pop-up notification indicating error to the user. Example: return { error: "Invalid command!" }
290
+
291
+ goto: Navigate to a different URL. Example: return { goto: "https://saltcorn.com" }
292
+
293
+ reload_page: Request a page reload with the existing URL. Example: return { reload_page: true }
294
+
295
+ popup: Open a URL in a popup. Example: return { popup: `/view/Orders?id=${parent}`}
296
+
297
+ set_fields: If triggered from an edit view, set fields dynamically in the form. The
298
+ value should be an object with keys that are field variable names. Example:
299
+
300
+ return { set_fields: {
301
+ zidentifier: `${name.toUpperCase()}-${id}`
302
+ }
303
+ }
304
+
305
+ Write the JavaScript code to implement the following functionality:
306
+
307
+ {{ userPrompt }}
@@ -0,0 +1,18 @@
1
+ Your task is to design a database schema for the following application:
2
+
3
+ {{ userPrompt }}
4
+
5
+ Do not use natural or composite primary keys. Every table should have an auto incrementing primary key field called "id".
6
+
7
+ Your database currently already contains a table for users with the following definitions.
8
+
9
+ CREATE TABLE users (
10
+ {{# userTable.fields.forEach((field,ix)=>{ }} {{ field.name }} {{field.primary_key && field.type.name==="Integer"? "SERIAL PRIMARY KEY":field.sql_type}}{{ix===userTable.fields.length-1 ? '':',' }}
11
+ {{# }) }});
12
+
13
+ Do not include the users table in you schema, but you can include foreign key fields referencing the id field of the users table.
14
+
15
+ Write the SQL to define the tables for PostgreSQL for this application:
16
+
17
+ {{ userPrompt }}
18
+