@saltcorn/sql 0.5.0 → 0.5.2

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/agent-skill.js CHANGED
@@ -4,10 +4,12 @@ const Form = require("@saltcorn/data/models/form");
4
4
  const Table = require("@saltcorn/data/models/table");
5
5
  const View = require("@saltcorn/data/models/view");
6
6
  const Trigger = require("@saltcorn/data/models/trigger");
7
+ const FieldRepeat = require("@saltcorn/data/models/fieldrepeat");
7
8
  const { getState } = require("@saltcorn/data/db/state");
8
9
  const db = require("@saltcorn/data/db");
9
10
  const { eval_expression } = require("@saltcorn/data/models/expression");
10
11
  const { interpolate } = require("@saltcorn/data/utils");
12
+ const { features } = require("@saltcorn/data/db/state");
11
13
 
12
14
  //const { fieldProperties } = require("./helpers");
13
15
 
@@ -28,17 +30,22 @@ class SQLQuerySkill {
28
30
  const is_sqlite = db.isSQLite;
29
31
 
30
32
  const phValues = [];
31
- (this.query_parameters || "")
32
- .split(",")
33
- .filter((s) => s)
34
- .forEach((sp0) => {
35
- const sp = sp0.trim();
36
- if (sp.startsWith("user.")) {
37
- phValues.push(eval_expression(sp, {}, user));
38
- } else if (typeof row[sp] === "undefined") phValues.push(null);
39
- else phValues.push(row[sp]);
33
+ if (this.mode === "Preload into system prompt") {
34
+ (this.query_parameters || "")
35
+ .split(",")
36
+ .filter((s) => s)
37
+ .forEach((sp0) => {
38
+ const sp = sp0.trim();
39
+ if (sp.startsWith("user.")) {
40
+ phValues.push(eval_expression(sp, {}, user));
41
+ } else if (typeof row[sp] === "undefined") phValues.push(null);
42
+ else phValues.push(row[sp]);
43
+ });
44
+ } else {
45
+ (this.toolargs || []).forEach((arg) => {
46
+ phValues.push(row[arg.name]);
40
47
  });
41
-
48
+ }
42
49
  const client = is_sqlite ? db : await db.getClient();
43
50
  await client.query(`BEGIN;`);
44
51
  if (!is_sqlite) {
@@ -77,23 +84,64 @@ class SQLQuerySkill {
77
84
  label: "Mode",
78
85
  type: "String",
79
86
  required: true,
80
- attributes: { options: [/*"Tool",*/ "Preload into system prompt"] },
87
+ attributes: {
88
+ options: features.nested_fieldrepeats
89
+ ? ["Preload into system prompt", "Tool"]
90
+ : ["Preload into system prompt"],
91
+ },
92
+ },
93
+ {
94
+ name: "tool_name",
95
+ label: "Tool name",
96
+ type: "String",
97
+ showIf: { mode: "Tool" },
98
+ },
99
+ {
100
+ name: "tool_description",
101
+ label: "Tool description",
102
+ type: "String",
103
+ showIf: { mode: "Tool" },
81
104
  },
105
+
82
106
  {
83
107
  name: "sql",
84
108
  label: "SQL",
85
109
  input_type: "code",
86
110
  attributes: { mode: "text/x-sql" },
87
111
  sublabel:
88
- "Refer to row parameters in the order below with <code>$1</code>, <code>$2</code> etc",
112
+ "Refer to query parameters with <code>$1</code>, <code>$2</code> etc",
89
113
  },
114
+ { input_type: "section_header", label: "Query parameters" },
115
+ new FieldRepeat({
116
+ name: "toolargs",
117
+ showIf: { mode: "Tool" },
118
+ fields: [
119
+ {
120
+ name: "name",
121
+ label: "Name",
122
+ type: "String",
123
+ },
124
+ {
125
+ name: "description",
126
+ label: "Description",
127
+ type: "String",
128
+ },
129
+ {
130
+ name: "argtype",
131
+ label: "Type",
132
+ type: "String",
133
+ required: true,
134
+ attributes: { options: ["string", "number", "integer", "boolean"] },
135
+ },
136
+ ],
137
+ }),
90
138
  {
91
139
  name: "query_parameters",
92
140
  label: "Query parameters",
93
141
  sublabel:
94
142
  "Comma separated list of variables to use as SQL query parameters. User variables can be used as <code>user.id</code> etc",
95
143
  type: "String",
96
- //showIf: { mode: "Tool" },
144
+ showIf: { mode: "Preload into system prompt" },
97
145
  },
98
146
  {
99
147
  name: "add_sys_prompt",
@@ -111,6 +159,38 @@ class SQLQuerySkill {
111
159
  },
112
160
  ];
113
161
  }
162
+
163
+ provideTools = () => {
164
+ if (this.mode === "Preload into system prompt") return null;
165
+ let properties = {};
166
+ (this.toolargs || []).forEach((arg) => {
167
+ properties[arg.name] = {
168
+ description: arg.description,
169
+ type: arg.argtype,
170
+ };
171
+ });
172
+ return {
173
+ type: "function",
174
+ process: async (row, { req }) => {
175
+ return await this.runQuery({ triggering_row: row });
176
+ },
177
+ /*renderToolCall({ phrase }, { req }) {
178
+ return div({ class: "border border-primary p-2 m-2" }, phrase);
179
+ },*/
180
+ renderToolResponse: async (response, { req }) => {
181
+ return div({ class: "border border-success p-2 m-2" }, response);
182
+ },
183
+ function: {
184
+ name: this.tool_name,
185
+ description: this.tool_description,
186
+ parameters: {
187
+ type: "object",
188
+ required: (this.toolargs || []).map((a) => a.name),
189
+ properties,
190
+ },
191
+ },
192
+ };
193
+ };
114
194
  }
115
195
 
116
196
  module.exports = SQLQuerySkill;
package/index.js CHANGED
@@ -5,6 +5,7 @@ const Table = require("@saltcorn/data/models/table");
5
5
  const FieldRepeat = require("@saltcorn/data/models/fieldrepeat");
6
6
  const Workflow = require("@saltcorn/data/models/workflow");
7
7
  const { eval_expression } = require("@saltcorn/data/models/expression");
8
+ const { interpolate } = require("@saltcorn/data/utils");
8
9
  const {
9
10
  text,
10
11
  div,
@@ -107,20 +108,24 @@ const run = async (
107
108
  );
108
109
  }
109
110
  qres = await client.query(sql, phValues);
110
- } catch (e) {
111
- throw e;
112
111
  } finally {
113
112
  await client.query(`ROLLBACK;`);
114
113
  if (!is_sqlite) client.release(true);
115
114
  }
116
115
  switch (output_type) {
117
116
  case "HTML":
118
- const template = _.template(html_code || "", {
117
+ /*const template = _.template(html_code || "", {
119
118
  evaluate: /\{\{#(.+?)\}\}/g,
120
119
  interpolate: /\{\{([^#].+?)\}\}/g,
121
120
  });
122
-
123
- return template({ rows: qres.rows });
121
+ console.log("template", viewname, state, html_code);*/
122
+ return interpolate(
123
+ html_code,
124
+ { rows: qres.rows },
125
+ req.user,
126
+ `HTML code interpolation in view ${viewname}`
127
+ );
128
+ //return template();
124
129
 
125
130
  case "JSON":
126
131
  return `<pre>${JSON.stringify(qres.rows, null, 2)}</pre>`;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@saltcorn/sql",
3
- "version": "0.5.0",
3
+ "version": "0.5.2",
4
4
  "description": "Actions and views based on SQL",
5
5
  "main": "index.js",
6
6
  "dependencies": {
package/table-provider.js CHANGED
@@ -46,6 +46,13 @@ const configuration_workflow = (req) =>
46
46
  }
47
47
  },
48
48
  },
49
+ {
50
+ label: "Ignore where/order",
51
+ sublabel:
52
+ "Always use this SQL directly without attempting to modify it",
53
+ type: "Bool",
54
+ name: "ignore_where",
55
+ },
49
56
  ],
50
57
  });
51
58
  },
@@ -176,161 +183,168 @@ const runQuery = async (cfg, where, opts) => {
176
183
  const opt = {
177
184
  database: is_sqlite ? "SQLite" : "PostgreSQL",
178
185
  };
179
-
180
- const { ast } = parser.parse(sql, opt);
181
-
182
- const colNames = new Set((cfg?.columns || []).map((c) => c.name));
183
- let phIndex = 1;
186
+ let sqlQ;
184
187
  const phValues = [];
185
- //console.log(ast[0].columns);
186
- for (const k of Object.keys(where)) {
187
- if (!colNames.has(k)) continue;
188
- const sqlCol =
189
- ast[0].columns == "*"
190
- ? {
191
- type: "expr",
192
- expr: { type: "column_ref", table: null, column: k },
193
- as: null,
194
- }
195
- : (ast[0].columns || []).find(
196
- (c) => k === c.as || (!c.as && k === c.expr?.column)
197
- );
198
- const sqlExprCol =
199
- ast[0].columns == "*"
200
- ? {
201
- type: "expr",
202
- expr: { type: "column_ref", table: null, column: k },
203
- as: null,
204
- }
205
- : (ast[0].columns || []).find((c) => c.expr?.as == k);
206
- const sqlAggrCol = (ast[0].columns || []).find(
207
- (c) =>
208
- c.expr?.type === "aggr_func" &&
209
- c.expr?.name?.toUpperCase() === k.toUpperCase()
210
- );
188
+ if (cfg?.ignore_where) {
189
+ sqlQ = sql;
190
+ } else {
191
+ const { ast } = parser.parse(sql, opt);
211
192
 
212
- let left = sqlExprCol
213
- ? { ...sqlExprCol.expr, as: null }
214
- : sqlAggrCol
215
- ? { ...sqlAggrCol.expr }
216
- : {
217
- type: "column_ref",
218
- table: sqlCol?.expr?.table,
219
- column: sqlCol?.expr?.column || db.sqlsanitize(k),
220
- };
221
- //console.log({ k, sqlCol, sqlExprCol });
222
- if (!sqlCol) {
223
- const starCol = (ast[0].columns || []).find((c) => c.type === "star_ref");
224
- if (starCol)
225
- left = {
226
- type: "column_ref",
227
- table: starCol?.expr?.table,
228
- column: db.sqlsanitize(k),
229
- };
230
- }
231
- const newClause = {
232
- type: "binary_expr",
233
- operator: where[k]?.ilike && !sqlAggrCol ? "ILIKE" : "=",
234
- left,
235
- right: { type: "number", value: "$" + phIndex },
236
- };
237
- phIndex += 1;
238
- phValues.push(where[k]?.ilike ? where[k]?.ilike : where[k]);
239
- if (!sqlAggrCol) {
240
- if (!ast[0].where) ast[0].where = newClause;
241
- else {
242
- ast[0].where = {
243
- type: "binary_expr",
244
- operator: "AND",
245
- left: ast[0].where,
246
- right: newClause,
247
- };
193
+ const colNames = new Set((cfg?.columns || []).map((c) => c.name));
194
+ let phIndex = 1;
195
+ //console.log(ast[0].columns);
196
+ for (const k of Object.keys(where)) {
197
+ if (!colNames.has(k)) continue;
198
+ const sqlCol =
199
+ ast[0].columns == "*"
200
+ ? {
201
+ type: "expr",
202
+ expr: { type: "column_ref", table: null, column: k },
203
+ as: null,
204
+ }
205
+ : (ast[0].columns || []).find(
206
+ (c) => k === c.as || (!c.as && k === c.expr?.column)
207
+ );
208
+ const sqlExprCol =
209
+ ast[0].columns == "*"
210
+ ? {
211
+ type: "expr",
212
+ expr: { type: "column_ref", table: null, column: k },
213
+ as: null,
214
+ }
215
+ : (ast[0].columns || []).find((c) => c.expr?.as == k);
216
+ const sqlAggrCol = (ast[0].columns || []).find(
217
+ (c) =>
218
+ c.expr?.type === "aggr_func" &&
219
+ c.expr?.name?.toUpperCase() === k.toUpperCase()
220
+ );
221
+
222
+ let left = sqlExprCol
223
+ ? { ...sqlExprCol.expr, as: null }
224
+ : sqlAggrCol
225
+ ? { ...sqlAggrCol.expr }
226
+ : {
227
+ type: "column_ref",
228
+ table: sqlCol?.expr?.table,
229
+ column: sqlCol?.expr?.column || db.sqlsanitize(k),
230
+ };
231
+ //console.log({ k, sqlCol, sqlExprCol });
232
+ if (!sqlCol) {
233
+ const starCol = (ast[0].columns || []).find(
234
+ (c) => c.type === "star_ref"
235
+ );
236
+ if (starCol)
237
+ left = {
238
+ type: "column_ref",
239
+ table: starCol?.expr?.table,
240
+ column: db.sqlsanitize(k),
241
+ };
248
242
  }
249
- } else {
250
- if (!ast[0].having) ast[0].having = newClause;
251
- else {
252
- ast[0].having = {
253
- type: "binary_expr",
254
- operator: "AND",
255
- left: ast[0].having,
256
- right: newClause,
257
- };
243
+ const newClause = {
244
+ type: "binary_expr",
245
+ operator: where[k]?.ilike && !sqlAggrCol ? "ILIKE" : "=",
246
+ left,
247
+ right: { type: "number", value: "$" + phIndex },
248
+ };
249
+ phIndex += 1;
250
+ phValues.push(where[k]?.ilike ? where[k]?.ilike : where[k]);
251
+ if (!sqlAggrCol) {
252
+ if (!ast[0].where) ast[0].where = newClause;
253
+ else {
254
+ ast[0].where = {
255
+ type: "binary_expr",
256
+ operator: "AND",
257
+ left: ast[0].where,
258
+ right: newClause,
259
+ };
260
+ }
261
+ } else {
262
+ if (!ast[0].having) ast[0].having = newClause;
263
+ else {
264
+ ast[0].having = {
265
+ type: "binary_expr",
266
+ operator: "AND",
267
+ left: ast[0].having,
268
+ right: newClause,
269
+ };
270
+ }
258
271
  }
259
272
  }
260
- }
261
- if (where?.limit && where?.offset) {
262
- ast[0].limit = {
263
- seperator: "offset",
264
- value: [
265
- { type: "number", value: where.limit },
266
- { type: "number", value: where.offset },
267
- ],
268
- };
269
- } else if (opts?.limit && opts?.offset) {
270
- ast[0].limit = {
271
- seperator: "offset",
272
- value: [
273
- { type: "number", value: opts.limit },
274
- { type: "number", value: opts.offset },
275
- ],
276
- };
277
- } else if (where?.limit) {
278
- ast[0].limit = {
279
- seperator: "",
280
- value: [{ type: "number", value: where.limit }],
281
- };
282
- } else if (opts?.limit) {
283
- ast[0].limit = {
284
- seperator: "",
285
- value: [{ type: "number", value: opts.limit }],
286
- };
287
- }
288
- //console.log(ast[0]);
289
- //console.log(ast[0].orderby[[0]]);
273
+ if (where?.limit && where?.offset) {
274
+ ast[0].limit = {
275
+ seperator: "offset",
276
+ value: [
277
+ { type: "number", value: where.limit },
278
+ { type: "number", value: where.offset },
279
+ ],
280
+ };
281
+ } else if (opts?.limit && opts?.offset) {
282
+ ast[0].limit = {
283
+ seperator: "offset",
284
+ value: [
285
+ { type: "number", value: opts.limit },
286
+ { type: "number", value: opts.offset },
287
+ ],
288
+ };
289
+ } else if (where?.limit) {
290
+ ast[0].limit = {
291
+ seperator: "",
292
+ value: [{ type: "number", value: where.limit }],
293
+ };
294
+ } else if (opts?.limit) {
295
+ ast[0].limit = {
296
+ seperator: "",
297
+ value: [{ type: "number", value: opts.limit }],
298
+ };
299
+ }
300
+ //console.log(ast[0]);
301
+ //console.log(ast[0].orderby[[0]]);
290
302
 
291
- const orderBy = where?.orderBy || opts?.orderBy;
292
- const orderDesc = where?.orderDesc || opts?.orderDesc;
303
+ const orderBy = where?.orderBy || opts?.orderBy;
304
+ const orderDesc = where?.orderDesc || opts?.orderDesc;
293
305
 
294
- if (orderBy) {
295
- if (typeof orderBy === "string")
296
- ast[0].orderby = [
297
- {
298
- expr: {
299
- type: "column_ref",
300
- table: null,
301
- column: db.sqlsanitize(orderBy),
302
- },
303
- type: orderDesc ? "DESC" : "ASC",
304
- },
305
- ];
306
- else if (orderBy.operator) {
307
- const { operator, field, target } = orderBy;
308
- const fieldCol = (cfg.columns || []).find((c) => c.name === field);
309
- const type = getState().types[fieldCol?.type];
310
- const op = type?.distance_operators[operator];
311
- if (op?.type === "SqlBinOp") {
306
+ if (orderBy) {
307
+ if (typeof orderBy === "string")
312
308
  ast[0].orderby = [
313
309
  {
314
310
  expr: {
315
- type: "binary_expr",
316
- operator: op.name,
317
- left: {
318
- type: "column_ref",
319
- table: null,
320
- column: db.sqlsanitize(field),
321
- },
322
- right: {
323
- type: "number",
324
- value: "$" + phIndex,
325
- },
311
+ type: "column_ref",
312
+ table: null,
313
+ column: db.sqlsanitize(orderBy),
326
314
  },
327
315
  type: orderDesc ? "DESC" : "ASC",
328
316
  },
329
317
  ];
330
- phIndex += 1;
331
- phValues.push(target);
318
+ else if (orderBy.operator) {
319
+ const { operator, field, target } = orderBy;
320
+ const fieldCol = (cfg.columns || []).find((c) => c.name === field);
321
+ const type = getState().types[fieldCol?.type];
322
+ const op = type?.distance_operators[operator];
323
+ if (op?.type === "SqlBinOp") {
324
+ ast[0].orderby = [
325
+ {
326
+ expr: {
327
+ type: "binary_expr",
328
+ operator: op.name,
329
+ left: {
330
+ type: "column_ref",
331
+ table: null,
332
+ column: db.sqlsanitize(field),
333
+ },
334
+ right: {
335
+ type: "number",
336
+ value: "$" + phIndex,
337
+ },
338
+ },
339
+ type: orderDesc ? "DESC" : "ASC",
340
+ },
341
+ ];
342
+ phIndex += 1;
343
+ phValues.push(target);
344
+ }
332
345
  }
333
346
  }
347
+ sqlQ = parser.sqlify(ast, opt);
334
348
  }
335
349
  const client = is_sqlite ? db : await db.getClient();
336
350
  await client.query(`BEGIN;`);
@@ -339,8 +353,7 @@ const runQuery = async (cfg, where, opts) => {
339
353
  await client.query(`SET SESSION CHARACTERISTICS AS TRANSACTION READ ONLY;`);
340
354
  }
341
355
 
342
- const sqlQ = parser.sqlify(ast, opt);
343
- console.log({ sqlQ, phValues, opts });
356
+ //console.log({ sqlQ, phValues, opts });
344
357
  const qres = await client.query(sqlQ, phValues);
345
358
  qres.query = sqlQ;
346
359
  await client.query(`ROLLBACK;`);