Skip to content

Commit c918edf

Browse files
authored
validate json queries (#103)
* validate json queries * use logger * 2.9.1
1 parent dea7ab1 commit c918edf

6 files changed

Lines changed: 240 additions & 29 deletions

File tree

jest.setup.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ require('dotenv').config();
22
const { JsonLogger } = require('@themost/json-logger');
33
const { TraceUtils } = require('@themost/common');
44
process.env.NODE_ENV = 'development';
5-
TraceUtils.useLogger(new JsonLogger());
5+
TraceUtils.useLogger(new JsonLogger({
6+
format: 'raw'
7+
}));
68
/* global jest */
79
jest.setTimeout(30000);

package-lock.json

Lines changed: 6 additions & 6 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@themost/sqlite",
3-
"version": "2.9.0",
3+
"version": "2.9.1",
44
"description": "MOST Web Framework SQLite Adapter",
55
"main": "dist/index.js",
66
"types": "dist/index.d.ts",
@@ -48,7 +48,7 @@
4848
"@rollup/plugin-babel": "^5.3.1",
4949
"@rollup/plugin-commonjs": "^22.0.0",
5050
"@themost/common": "^2.11.0",
51-
"@themost/data": "^2.18.1",
51+
"@themost/data": "^2.18.2",
5252
"@themost/json-logger": "^1.1.0",
5353
"@themost/peers": "^1.0.2",
5454
"@themost/query": "^2.14.7",

spec/QueryExpression.selectJson.spec.js

Lines changed: 206 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@ import {MemberExpression, MethodCallExpression, QueryEntity, QueryExpression, Qu
44
import { SqliteFormatter } from '../src';
55
import SimpleOrderSchema from './config/models/SimpleOrder.json';
66
import {TestApplication} from './TestApplication';
7+
import { TraceUtils } from '@themost/common';
8+
import { DataPermissionEventListener } from '@themost/data';
9+
import { promisify } from 'util';
10+
const beforeExecuteAsync = promisify(DataPermissionEventListener.prototype.beforeExecute);
711

812
/**
913
* @param { import('../src').SqliteAdapter } db
@@ -328,9 +332,7 @@ describe('SqlFormatter', () => {
328332
select.push({
329333
customer: {
330334
$jsonObject: [
331-
'familyName',
332335
new QueryField('familyName').from('customer'),
333-
'givenName',
334336
new QueryField('givenName').from('customer'),
335337
]
336338
}
@@ -369,18 +371,14 @@ describe('SqlFormatter', () => {
369371
select.push({
370372
customer: {
371373
$jsonObject: [
372-
'familyName',
373374
new QueryField('familyName').from('customers'),
374-
'givenName',
375375
new QueryField('givenName').from('customers'),
376376
]
377377
}
378378
}, {
379379
orderStatus: {
380380
$jsonObject: [
381-
'name',
382381
new QueryField('name').from('orderStatusTypes'),
383-
'alternateName',
384382
new QueryField('alternateName').from('orderStatusTypes'),
385383
]
386384
}
@@ -401,4 +399,206 @@ describe('SqlFormatter', () => {
401399
});
402400
});
403401

402+
it('should use json queries for expand entities', async () => {
403+
// set context user
404+
context.user = {
405+
name: 'james.may@example.com'
406+
};
407+
let start= new Date().getTime();
408+
const items = await context.model('Order').asQueryable().select(
409+
'id', 'orderDate', 'orderStatus', 'customer', 'orderedItem'
410+
).expand('customer', 'orderStatus', 'orderedItem').getItems();
411+
let end = new Date().getTime();
412+
TraceUtils.log('Elapsed time: ' + (end-start) + 'ms');
413+
expect(items.length).toBeTruthy();
414+
// create ad-hoc query
415+
const { viewAdapter: Orders } = context.model('Order');
416+
const { viewAdapter: People } = context.model('Person');
417+
const { viewAdapter: Products } = context.model('Product');
418+
const { viewAdapter: OrderStatusTypes } = context.model('OrderStatusType');
419+
const personAttributes = context.model('Person').select().query.$select[People].map((x) => {
420+
return x.from('customer');
421+
});
422+
const productAttributes = context.model('Product').select().query.$select[Products].map((x) => {
423+
return x.from('orderedItem');
424+
});
425+
const orderStatusAttributes = context.model('OrderStatusType').select().query.$select[OrderStatusTypes].map((x) => {
426+
return x.from('orderStatus');
427+
});
428+
const q = new QueryExpression().select(
429+
new QueryField('id').from(Orders),
430+
new QueryField('orderDate').from(Orders),
431+
new QueryField({
432+
customer: {
433+
$jsonObject: personAttributes
434+
}
435+
}),
436+
new QueryField({
437+
product: {
438+
$jsonObject: productAttributes
439+
}
440+
}),
441+
new QueryField({
442+
orderStatus: {
443+
$jsonObject: orderStatusAttributes
444+
}
445+
})
446+
).from(Orders).join(new QueryEntity(People).as('customer')).with(
447+
new QueryExpression().where(
448+
new QueryField('customer').from(Orders)
449+
).equal(
450+
new QueryField('id').from('customer')
451+
)
452+
).join(new QueryEntity(Products).as('orderedItem')).with(
453+
new QueryExpression().where(
454+
new QueryField('orderedItem').from(Orders)
455+
).equal(
456+
new QueryField('id').from('orderedItem')
457+
)
458+
).join(new QueryEntity(OrderStatusTypes).as('orderStatus')).with(
459+
new QueryExpression().where(
460+
new QueryField('orderStatus').from(Orders)
461+
).equal(
462+
new QueryField('id').from('orderStatus')
463+
)
464+
).where(new QueryField('email').from('customer')).equal(context.user.name);
465+
466+
start= new Date().getTime();
467+
const customerOrders = await context.db.executeAsync(q, []);
468+
end = new Date().getTime();
469+
TraceUtils.log('Elapsed time: ' + (end-start) + 'ms');
470+
expect(customerOrders.length).toBeTruthy();
471+
expect(items.length).toEqual(customerOrders.length);
472+
});
473+
474+
it('should use json queries and validate permission', async () => {
475+
// set context user
476+
context.user = {
477+
name: 'james.may@example.com'
478+
};
479+
const queryOrders = context.model('Order').asQueryable().select().flatten();
480+
const { viewAdapter: Orders } = queryOrders.model;
481+
expect(queryOrders).toBeTruthy();
482+
// prepare query for customer
483+
const queryPeople = context.model('Person').asQueryable().select().flatten();
484+
await beforeExecuteAsync({
485+
model: queryPeople.model,
486+
emitter: queryPeople,
487+
query: queryPeople.query,
488+
});
489+
expect(queryPeople).toBeTruthy();
490+
// prepare query for order status
491+
const queryOrderStatus = context.model('OrderStatusType').asQueryable().select().flatten();
492+
await beforeExecuteAsync({
493+
model: queryOrderStatus.model,
494+
emitter: queryOrderStatus,
495+
query: queryOrderStatus.query,
496+
});
497+
// prepare query for ordered item
498+
const queryProducts = context.model('Product').asQueryable().select().flatten();
499+
await beforeExecuteAsync({
500+
model: queryProducts.model,
501+
emitter: queryProducts,
502+
query: queryProducts.query,
503+
});
504+
505+
// phase 1: join customers in order to get customer as json object
506+
const { viewAdapter: People } = queryPeople.model;
507+
// select customer as json object
508+
const selectCustomer = new QueryField({
509+
customer: {
510+
$jsonObject: queryPeople.query.$select[People].map((x) => {
511+
return x.from('customer');
512+
})
513+
}
514+
});
515+
// remove select arguments from nested query and push a wildcard select
516+
// important note: this operation reduces the size of the subquery used for join entity
517+
queryPeople.query.$select[People] = [new QueryField(`${People}.*`)];
518+
// join entity
519+
queryOrders.query.join(queryPeople.query.as('customer')).with(
520+
new QueryExpression().where(
521+
new QueryField('customer').from(Orders)
522+
).equal(
523+
new QueryField('id').from('customer')
524+
)
525+
)
526+
// append customer json object
527+
528+
const selectOrders = queryOrders.query.$select[Orders];
529+
// remove previoulsy selected customer field
530+
let removeIndex = selectOrders.findIndex((x) => x instanceof QueryField && x.$name === `${Orders}.customer`);
531+
if (removeIndex >= 0) {
532+
selectOrders.splice(removeIndex, 1);
533+
}
534+
// add customer json object
535+
selectOrders.push(selectCustomer);
536+
537+
// phase 2: join ordered items in order to get ordered item as json object
538+
const { viewAdapter: Products } = queryProducts.model;
539+
// select ordered item as json object
540+
const selectOrderedItem = new QueryField({
541+
orderedItem: {
542+
$jsonObject: queryProducts.query.$select[Products].map((x) => {
543+
return x.from('orderedItem');
544+
})
545+
}
546+
});
547+
// remove select arguments from nested query and push a wildcard select
548+
// important note: this operation reduces the size of the subquery used for join entity
549+
queryProducts.query.$select[Products] = [new QueryField(`${Products}.*`)];
550+
// join entity
551+
queryOrders.query.join(queryProducts.query.as('orderedItem')).with(
552+
new QueryExpression().where(
553+
new QueryField('orderedItem').from(Orders)
554+
).equal(
555+
new QueryField('id').from('orderedItem')
556+
)
557+
)
558+
removeIndex = selectOrders.findIndex((x) => x instanceof QueryField && x.$name === `${Orders}.orderedItem`);
559+
if (removeIndex >= 0) {
560+
selectOrders.splice(removeIndex, 1);
561+
}
562+
// add ordered json object
563+
selectOrders.push(selectOrderedItem);
564+
565+
// phase 3: join order status in order to get order status as json object
566+
const { viewAdapter: OrderStatusTypes } = queryOrderStatus.model;
567+
// select order status as json object
568+
const selectOrderStatus = new QueryField({
569+
orderStatus: {
570+
$jsonObject: queryOrderStatus.query.$select[OrderStatusTypes].map((x) => {
571+
return x.from('orderStatus');
572+
})
573+
}
574+
});
575+
// remove select arguments from nested query and push a wildcard select
576+
// important note: this operation reduces the size of the subquery used for join entity
577+
queryOrderStatus.query.$select[OrderStatusTypes] = [new QueryField(`${OrderStatusTypes}.*`)];
578+
// join entity
579+
queryOrders.query.join(queryOrderStatus.query.as('orderStatus')).with(
580+
new QueryExpression().where(
581+
new QueryField('orderStatus').from(Orders)
582+
).equal(
583+
new QueryField('id').from('orderStatus')
584+
)
585+
);
586+
removeIndex = selectOrders.findIndex((x) => x instanceof QueryField && x.$name === `${Orders}.orderStatus`);
587+
if (removeIndex >= 0) {
588+
selectOrders.splice(removeIndex, 1);
589+
}
590+
// add order status json object
591+
selectOrders.push(selectOrderStatus);
592+
593+
let start= new Date().getTime();
594+
const items = await queryOrders.getItems();
595+
let end = new Date().getTime();
596+
TraceUtils.log('Elapsed time: ' + (end-start) + 'ms');
597+
expect(items.length).toBeTruthy();
598+
for (const item of items) {
599+
expect(item.customer).toBeInstanceOf(Object);
600+
expect(item.orderedItem).toBeInstanceOf(Object);
601+
}
602+
});
603+
404604
});

spec/db/local.db

56 KB
Binary file not shown.

src/SqliteFormatter.js

Lines changed: 23 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
// MOST Web Framework Codename Zero Gravity Copyright (c) 2017-2022, THEMOST LP
22

33
import { sprintf } from 'sprintf-js';
4-
import { SqlFormatter } from '@themost/query';
4+
import { SqlFormatter, QueryField } from '@themost/query';
55
const REGEXP_SINGLE_QUOTE=/\\'/g;
66
const SINGLE_QUOTE_ESCAPE ='\'\'';
77
const REGEXP_DOUBLE_QUOTE=/\\"/g;
@@ -337,24 +337,33 @@ class SqliteFormatter extends SqlFormatter {
337337
}
338338
}
339339

340-
/**
341-
* @param {...*} expr
342-
*/
343-
// eslint-disable-next-line no-unused-vars
344-
$json(expr) {
345-
const args = Array.from(arguments);
346-
return this.$jsonObject(...args);
347-
}
348-
349340
/**
350341
* @param {...*} expr
351342
*/
352343
// eslint-disable-next-line no-unused-vars
353344
$jsonObject(expr) {
354-
const args = Array.from(arguments).map((arg) => {
355-
return this.escape(arg)
356-
});
357-
return `json_object(${args.join(',')})`;
345+
// expected an array of QueryField objects
346+
const args = Array.from(arguments).reduce((previous, current) => {
347+
// get the first key of the current object
348+
let [name] = Object.keys(current);
349+
let value;
350+
// if the name is not a string then throw an error
351+
if (typeof name !== 'string') {
352+
throw new Error('Invalid json object expression. The attribute name cannot be determined.');
353+
}
354+
// if the given name is a dialect function (starts with $) then use the current value as is
355+
// otherwise create a new QueryField object
356+
if (name.startsWith('$')) {
357+
value = new QueryField(current[name]);
358+
name = value.getName();
359+
} else {
360+
value = current instanceof QueryField ? new QueryField(current[name]) : current[name];
361+
}
362+
// escape json attribute name and value
363+
previous.push(this.escape(name), this.escape(value));
364+
return previous;
365+
}, []);
366+
return `json_object(${args.join(',')})`;;
358367
}
359368
}
360369

0 commit comments

Comments
 (0)