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
4 changes: 3 additions & 1 deletion jest.setup.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ require('dotenv').config();
const { JsonLogger } = require('@themost/json-logger');
const { TraceUtils } = require('@themost/common');
process.env.NODE_ENV = 'development';
TraceUtils.useLogger(new JsonLogger());
TraceUtils.useLogger(new JsonLogger({
format: 'raw'
}));
/* global jest */
jest.setTimeout(30000);
12 changes: 6 additions & 6 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@themost/sqlite",
"version": "2.9.0",
"version": "2.9.1",
"description": "MOST Web Framework SQLite Adapter",
"main": "dist/index.js",
"types": "dist/index.d.ts",
Expand Down Expand Up @@ -48,7 +48,7 @@
"@rollup/plugin-babel": "^5.3.1",
"@rollup/plugin-commonjs": "^22.0.0",
"@themost/common": "^2.11.0",
"@themost/data": "^2.18.1",
"@themost/data": "^2.18.2",
"@themost/json-logger": "^1.1.0",
"@themost/peers": "^1.0.2",
"@themost/query": "^2.14.7",
Expand Down
212 changes: 206 additions & 6 deletions spec/QueryExpression.selectJson.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ import {MemberExpression, MethodCallExpression, QueryEntity, QueryExpression, Qu
import { SqliteFormatter } from '../src';
import SimpleOrderSchema from './config/models/SimpleOrder.json';
import {TestApplication} from './TestApplication';
import { TraceUtils } from '@themost/common';
import { DataPermissionEventListener } from '@themost/data';
import { promisify } from 'util';
const beforeExecuteAsync = promisify(DataPermissionEventListener.prototype.beforeExecute);

/**
* @param { import('../src').SqliteAdapter } db
Expand Down Expand Up @@ -328,9 +332,7 @@ describe('SqlFormatter', () => {
select.push({
customer: {
$jsonObject: [
'familyName',
new QueryField('familyName').from('customer'),
'givenName',
new QueryField('givenName').from('customer'),
]
}
Expand Down Expand Up @@ -369,18 +371,14 @@ describe('SqlFormatter', () => {
select.push({
customer: {
$jsonObject: [
'familyName',
new QueryField('familyName').from('customers'),
'givenName',
new QueryField('givenName').from('customers'),
]
}
}, {
orderStatus: {
$jsonObject: [
'name',
new QueryField('name').from('orderStatusTypes'),
'alternateName',
new QueryField('alternateName').from('orderStatusTypes'),
]
}
Expand All @@ -401,4 +399,206 @@ describe('SqlFormatter', () => {
});
});

it('should use json queries for expand entities', async () => {
// set context user
context.user = {
name: 'james.may@example.com'
};
let start= new Date().getTime();
const items = await context.model('Order').asQueryable().select(
'id', 'orderDate', 'orderStatus', 'customer', 'orderedItem'
).expand('customer', 'orderStatus', 'orderedItem').getItems();
let end = new Date().getTime();
TraceUtils.log('Elapsed time: ' + (end-start) + 'ms');
expect(items.length).toBeTruthy();
// create ad-hoc query
const { viewAdapter: Orders } = context.model('Order');
const { viewAdapter: People } = context.model('Person');
const { viewAdapter: Products } = context.model('Product');
const { viewAdapter: OrderStatusTypes } = context.model('OrderStatusType');
const personAttributes = context.model('Person').select().query.$select[People].map((x) => {
return x.from('customer');
});
const productAttributes = context.model('Product').select().query.$select[Products].map((x) => {
return x.from('orderedItem');
});
const orderStatusAttributes = context.model('OrderStatusType').select().query.$select[OrderStatusTypes].map((x) => {
return x.from('orderStatus');
});
const q = new QueryExpression().select(
new QueryField('id').from(Orders),
new QueryField('orderDate').from(Orders),
new QueryField({
customer: {
$jsonObject: personAttributes
}
}),
new QueryField({
product: {
$jsonObject: productAttributes
}
}),
new QueryField({
orderStatus: {
$jsonObject: orderStatusAttributes
}
})
).from(Orders).join(new QueryEntity(People).as('customer')).with(
new QueryExpression().where(
new QueryField('customer').from(Orders)
).equal(
new QueryField('id').from('customer')
)
).join(new QueryEntity(Products).as('orderedItem')).with(
new QueryExpression().where(
new QueryField('orderedItem').from(Orders)
).equal(
new QueryField('id').from('orderedItem')
)
).join(new QueryEntity(OrderStatusTypes).as('orderStatus')).with(
new QueryExpression().where(
new QueryField('orderStatus').from(Orders)
).equal(
new QueryField('id').from('orderStatus')
)
).where(new QueryField('email').from('customer')).equal(context.user.name);

start= new Date().getTime();
const customerOrders = await context.db.executeAsync(q, []);
end = new Date().getTime();
TraceUtils.log('Elapsed time: ' + (end-start) + 'ms');
expect(customerOrders.length).toBeTruthy();
expect(items.length).toEqual(customerOrders.length);
});

it('should use json queries and validate permission', async () => {
// set context user
context.user = {
name: 'james.may@example.com'
};
const queryOrders = context.model('Order').asQueryable().select().flatten();
const { viewAdapter: Orders } = queryOrders.model;
expect(queryOrders).toBeTruthy();
// prepare query for customer
const queryPeople = context.model('Person').asQueryable().select().flatten();
await beforeExecuteAsync({
model: queryPeople.model,
emitter: queryPeople,
query: queryPeople.query,
});
expect(queryPeople).toBeTruthy();
// prepare query for order status
const queryOrderStatus = context.model('OrderStatusType').asQueryable().select().flatten();
await beforeExecuteAsync({
model: queryOrderStatus.model,
emitter: queryOrderStatus,
query: queryOrderStatus.query,
});
// prepare query for ordered item
const queryProducts = context.model('Product').asQueryable().select().flatten();
await beforeExecuteAsync({
model: queryProducts.model,
emitter: queryProducts,
query: queryProducts.query,
});

// phase 1: join customers in order to get customer as json object
const { viewAdapter: People } = queryPeople.model;
// select customer as json object
const selectCustomer = new QueryField({
customer: {
$jsonObject: queryPeople.query.$select[People].map((x) => {
return x.from('customer');
})
}
});
// remove select arguments from nested query and push a wildcard select
// important note: this operation reduces the size of the subquery used for join entity
queryPeople.query.$select[People] = [new QueryField(`${People}.*`)];
// join entity
queryOrders.query.join(queryPeople.query.as('customer')).with(
new QueryExpression().where(
new QueryField('customer').from(Orders)
).equal(
new QueryField('id').from('customer')
)
)
// append customer json object

const selectOrders = queryOrders.query.$select[Orders];
// remove previoulsy selected customer field
let removeIndex = selectOrders.findIndex((x) => x instanceof QueryField && x.$name === `${Orders}.customer`);
if (removeIndex >= 0) {
selectOrders.splice(removeIndex, 1);
}
// add customer json object
selectOrders.push(selectCustomer);

// phase 2: join ordered items in order to get ordered item as json object
const { viewAdapter: Products } = queryProducts.model;
// select ordered item as json object
const selectOrderedItem = new QueryField({
orderedItem: {
$jsonObject: queryProducts.query.$select[Products].map((x) => {
return x.from('orderedItem');
})
}
});
// remove select arguments from nested query and push a wildcard select
// important note: this operation reduces the size of the subquery used for join entity
queryProducts.query.$select[Products] = [new QueryField(`${Products}.*`)];
// join entity
queryOrders.query.join(queryProducts.query.as('orderedItem')).with(
new QueryExpression().where(
new QueryField('orderedItem').from(Orders)
).equal(
new QueryField('id').from('orderedItem')
)
)
removeIndex = selectOrders.findIndex((x) => x instanceof QueryField && x.$name === `${Orders}.orderedItem`);
if (removeIndex >= 0) {
selectOrders.splice(removeIndex, 1);
}
// add ordered json object
selectOrders.push(selectOrderedItem);

// phase 3: join order status in order to get order status as json object
const { viewAdapter: OrderStatusTypes } = queryOrderStatus.model;
// select order status as json object
const selectOrderStatus = new QueryField({
orderStatus: {
$jsonObject: queryOrderStatus.query.$select[OrderStatusTypes].map((x) => {
return x.from('orderStatus');
})
}
});
// remove select arguments from nested query and push a wildcard select
// important note: this operation reduces the size of the subquery used for join entity
queryOrderStatus.query.$select[OrderStatusTypes] = [new QueryField(`${OrderStatusTypes}.*`)];
// join entity
queryOrders.query.join(queryOrderStatus.query.as('orderStatus')).with(
new QueryExpression().where(
new QueryField('orderStatus').from(Orders)
).equal(
new QueryField('id').from('orderStatus')
)
);
removeIndex = selectOrders.findIndex((x) => x instanceof QueryField && x.$name === `${Orders}.orderStatus`);
if (removeIndex >= 0) {
selectOrders.splice(removeIndex, 1);
}
// add order status json object
selectOrders.push(selectOrderStatus);

let start= new Date().getTime();
const items = await queryOrders.getItems();
let end = new Date().getTime();
TraceUtils.log('Elapsed time: ' + (end-start) + 'ms');
expect(items.length).toBeTruthy();
for (const item of items) {
expect(item.customer).toBeInstanceOf(Object);
expect(item.orderedItem).toBeInstanceOf(Object);
}
});

});
Binary file modified spec/db/local.db
Binary file not shown.
37 changes: 23 additions & 14 deletions src/SqliteFormatter.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// MOST Web Framework Codename Zero Gravity Copyright (c) 2017-2022, THEMOST LP

import { sprintf } from 'sprintf-js';
import { SqlFormatter } from '@themost/query';
import { SqlFormatter, QueryField } from '@themost/query';
const REGEXP_SINGLE_QUOTE=/\\'/g;
const SINGLE_QUOTE_ESCAPE ='\'\'';
const REGEXP_DOUBLE_QUOTE=/\\"/g;
Expand Down Expand Up @@ -337,24 +337,33 @@ class SqliteFormatter extends SqlFormatter {
}
}

/**
* @param {...*} expr
*/
// eslint-disable-next-line no-unused-vars
$json(expr) {
const args = Array.from(arguments);
return this.$jsonObject(...args);
}

/**
* @param {...*} expr
*/
// eslint-disable-next-line no-unused-vars
$jsonObject(expr) {
const args = Array.from(arguments).map((arg) => {
return this.escape(arg)
});
return `json_object(${args.join(',')})`;
// expected an array of QueryField objects
const args = Array.from(arguments).reduce((previous, current) => {
// get the first key of the current object
let [name] = Object.keys(current);
let value;
// if the name is not a string then throw an error
if (typeof name !== 'string') {
throw new Error('Invalid json object expression. The attribute name cannot be determined.');
}
// if the given name is a dialect function (starts with $) then use the current value as is
// otherwise create a new QueryField object
if (name.startsWith('$')) {
value = new QueryField(current[name]);
name = value.getName();
} else {
value = current instanceof QueryField ? new QueryField(current[name]) : current[name];
}
// escape json attribute name and value
previous.push(this.escape(name), this.escape(value));
return previous;
}, []);
return `json_object(${args.join(',')})`;;
}
}

Expand Down