Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog.txt
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
- Any `NEWACTION` with the `mcp: true` property will be available in the endpoint
- added a new method `JSONSchema.toAIObject(name, [description])`
- added a new method `JSONSchema.toAITool(name)`
- added `QueryBuilder.autoqueryjson($, column, schema, [defsort], [maxlimit])` that works like `autoquery()` but for properties stored in a JSON(B) column ; the builder uses the neutral name `json:column:property:type` and the database adapter translates it into SQL
- fixed `AIPARSER()` with tool calling
- added a new controller method `$.content(value, content_type)`
- added a new event `reset` in the FlowStream
Expand Down
84 changes: 84 additions & 0 deletions querybuilder.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// The MIT License
// Copyright 2023 (c) Peter Širka <petersirka@gmail.com>

const REG_JSONKEY = /[^a-z0-9_]/i;
const REG_FIELDS_CLEANER = /"|`|\||'|\s/g;

var LANGUAGE_SKIP = '_';
Expand Down Expand Up @@ -1192,6 +1193,89 @@ QBP.autoquery = function(query, schema, defsort, maxlimit) {
return t;
};

// Works like autoquery() but for properties stored in a JSON column
// Usage: .autoqueryjson($, 'data', 'id,name,email,age:Number,dtcreated:Date,tags:[string]', 'dtcreated_desc', 100)
// The builder doesn't generate any SQL. Fields, filters and sorting use the neutral name "json:column:property:type"
// and each database adapter translates it into its own syntax.
QBP.autoqueryjson = function($, column, schema, defsort, maxlimit) {

let t = this;
let query = $ && $.query && typeof($.query) === 'object' ? $.query : ($ || {});

if (!column || REG_JSONKEY.test(column))
throw new Error('QueryBuilder.autoqueryjson(): invalid column name "' + column + '"');

let key = 'QBJ' + column + '|' + schema;
let allowed = F.temporary.querybuilders[key];

if (!allowed) {

let fields = {};
let filter = [];

for (let field of schema.split(',').trim()) {

let k = field.split(':').trim();
let name = k[0];

if (!name || REG_JSONKEY.test(name))
continue;

let type = (k[1] || 'string').toLowerCase();
fields[name] = 'json:' + column + ':' + name + ':' + type;
filter.push({ name: name, type: type });
}

allowed = F.temporary.querybuilders[key] = { fields: fields, filter: filter };
}

let fieldscount = 0;

if (!t.options.fields)
t.options.fields = [];

if (query.fields) {
for (let field of query.fields.replace(REG_FIELDS_CLEANER, '').split(',')) {
if (allowed.fields[field]) {
t.options.fields.push(allowed.fields[field]);
fieldscount++;
}
}
}

if (!fieldscount) {
for (let item of allowed.filter)
t.options.fields.push(allowed.fields[item.name]);
}

for (let item of allowed.filter)
t.gridfilter(item.name, query, item.type, allowed.fields[item.name]);

if (query.sort) {

let count = 0;

for (let item of query.sort.split(',')) {
let index = item.lastIndexOf('_');
let name = index === -1 ? item : item.substring(0, index);

if (!allowed.fields[name])
continue;

t.sort(allowed.fields[name], index !== -1 && item.substring(index + 1).toLowerCase() === 'desc');
count++;
}

if (!count && defsort)
t.gridsort(defsort, allowed.fields);

} else if (defsort)
t.gridsort(defsort, allowed.fields);

maxlimit && t.paginate(query.page, query.limit, maxlimit);
return t;
};

QBP.query = function(value) {
this.filter.push({ type: 'query', value: value });
return this;
Expand Down
Loading