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
8 changes: 5 additions & 3 deletions package-lock.json

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

3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@themost/sqlite",
"version": "2.9.1",
"version": "2.9.2",
"description": "MOST Web Framework SQLite Adapter",
"main": "dist/index.js",
"types": "dist/index.d.ts",
Expand Down Expand Up @@ -30,6 +30,7 @@
"dependencies": {
"@themost/events": "^1.5.0",
"async": "^2.6.4",
"lodash": "^4.17.21",
"sprintf-js": "^1.1.2",
"sqlite3": "^5.1.7",
"unzipper": "^0.12.3"
Expand Down
115 changes: 113 additions & 2 deletions spec/QueryExpression.selectJson.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,9 @@ async function createSimpleOrders(db) {
const { source } = SimpleOrderSchema;
const exists = await db.table(source).existsAsync();
if (!exists) {
await db.table(source).createAsync(SimpleOrderSchema.fields);
await db.table(source).createAsync(SimpleOrderSchema.fields);
} else {
return;
}
// get some orders
const orders = await db.executeAsync(
Expand Down Expand Up @@ -61,7 +63,19 @@ async function createSimpleOrders(db) {
return {id, streetAddress, postalCode, addressLocality, addressCountry, telephone };
}), []
);
// get

const shuffleArray = (array) => {
for (let i = array.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[array[i], array[j]] = [array[j], array[i]];
}
return array;
};

const getRandomItems = (array, numItems) => {
const shuffledArray = shuffleArray([...array]);
return shuffledArray.slice(0, numItems);
};
const items = orders.map((order) => {
const { orderDate, discount, discountCode, orderNumber, paymentDue,
dateCreated, dateModified, createdBy, modifiedBy } = order;
Expand All @@ -73,6 +87,8 @@ async function createSimpleOrders(db) {
customer.address = postalAddresses.find((x) => x.id === customer.address);
delete customer.address?.id;
}
// get 2 random payment methods
const additionalPaymentMethods = getRandomItems(paymentMethods, 2);
return {
orderDate,
discount,
Expand All @@ -82,6 +98,7 @@ async function createSimpleOrders(db) {
orderStatus,
orderedItem,
paymentMethod,
additionalPaymentMethods,
customer,
dateCreated,
dateModified,
Expand Down Expand Up @@ -601,4 +618,98 @@ describe('SqlFormatter', () => {
}
});


it('should return json arrays', async () => {
// set context user
context.user = {
name: 'alexis.rees@example.com'
};

const queryPeople = context.model('Person').asQueryable().select(
'id', 'familyName', 'givenName', 'jobTitle', 'email'
).flatten();
await beforeExecuteAsync({
model: queryPeople.model,
emitter: queryPeople,
query: queryPeople.query,
});
const { viewAdapter: People } = queryPeople.model;
const queryOrders = context.model('Order').asQueryable().select(
'id', 'orderDate', 'orderStatus', 'orderedItem', 'customer'
).flatten();
const { viewAdapter: Orders } = queryOrders.model;
// prepare query for each customer
queryOrders.query.where(
new QueryField('customer').from(Orders)
).equal(
new QueryField('id').from(People)
);
const selectPeople = queryPeople.query.$select[People];
// add orders as json array
selectPeople.push({
orders: {
$jsonArray: [
queryOrders.query
]
}
});
const start= new Date().getTime();
const items = await queryPeople.take(50).getItems();
const end = new Date().getTime();
TraceUtils.log('Elapsed time: ' + (end-start) + 'ms');
expect(items.length).toBeTruthy();
for (const item of items) {
expect(Array.isArray(item.orders)).toBeTruthy();
for (const order of item.orders) {
expect(order.customer).toEqual(item.id);
}

}
});

it('should parse string as json array', async () => {
// set context user
context.user = {
name: 'alexis.rees@example.com'
};
const { viewAdapter: People } = context.model('Person');
const query = new QueryExpression().select(
'id', 'familyName', 'givenName', 'jobTitle', 'email',
new QueryField({
tags: {
$jsonArray: [
new QueryField({
$value: '[ "user", "customer", "admin" ]'
})
]
}
})
).from(People).where('email').equal(context.user.name);
const [item] = await context.db.executeAsync(query);
expect(item).toBeTruthy();
});

it('should parse array as json array', async () => {
// set context user
context.user = {
name: 'alexis.rees@example.com'
};
const { viewAdapter: People } = context.model('Person');
const query = new QueryExpression().select(
'id', 'familyName', 'givenName', 'jobTitle', 'email',
new QueryField({
tags: {
$jsonArray: [
{
$value: [ 'user', 'customer', 'admin' ]
}
]
}
})
).from(People).where('email').equal(context.user.name);
const [item] = await context.db.executeAsync(query);
expect(item).toBeTruthy();
expect(Array.isArray(item.tags)).toBeTruthy();
expect(item.tags).toEqual([ 'user', 'customer', 'admin' ]);
});
});
7 changes: 7 additions & 0 deletions spec/config/models/SimpleOrder.json
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,13 @@
"type": "Integer",
"calculation": "javascript:return this.user();",
"readonly": true
},
{
"name": "additionalPaymentMethods",
"type": "Json",
"additionalType": "PaymentMethod",
"expandable": true,
"many": true
}
],
"views": [
Expand Down
Binary file modified spec/db/local.db
Binary file not shown.
2 changes: 1 addition & 1 deletion src/SqliteAdapter.js
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ function onReceivingJsonObject(event) {
if (typeof key !== 'string') {
return false;
}
return x[key].$jsonObject != null || x[key].$json != null;
return x[key].$jsonObject != null || x[key].$jsonGroupArray != null || x[key].$jsonArray != null;
}).map((x) => {
return Object.keys(x)[0];
});
Expand Down
78 changes: 77 additions & 1 deletion src/SqliteFormatter.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import { sprintf } from 'sprintf-js';
import { SqlFormatter, QueryField } from '@themost/query';
import { isObjectDeep } from './isObjectDeep';
const REGEXP_SINGLE_QUOTE=/\\'/g;
const SINGLE_QUOTE_ESCAPE ='\'\'';
const REGEXP_DOUBLE_QUOTE=/\\"/g;
Expand Down Expand Up @@ -52,6 +53,19 @@ class SqliteFormatter extends SqlFormatter {
if (value instanceof Date) {
return this.escapeDate(value);
}
// serialize array of objects as json array
if (Array.isArray(value)) {
// find first non-object value
const index = value.filter((x) => {
return x != null;
}).findIndex((x) => {
return isObjectDeep(x) === false;
});
// if all values are objects
if (index === -1) {
return this.escape(JSON.stringify(value)); // return as json array
}
}
let res = super.escape.bind(this)(value, unquoted);
if (typeof value === 'string') {
if (REGEXP_SINGLE_QUOTE.test(res))
Expand Down Expand Up @@ -279,7 +293,7 @@ class SqliteFormatter extends SqlFormatter {
* @param {*} expr
* @return {string}
*/
$jsonArray(expr) {
$jsonEach(expr) {
return `json_each(${this.escapeName(expr)})`;
}

Expand Down Expand Up @@ -365,6 +379,68 @@ class SqliteFormatter extends SqlFormatter {
}, []);
return `json_object(${args.join(',')})`;;
}

/**
* @param {{ $jsonGet: Array<*> }} expr
*/
$jsonGroupArray(expr) {
const [key] = Object.keys(expr);
if (key !== '$jsonObject') {
throw new Error('Invalid json group array expression. Expected a json object expression');
}
return `json_group_array(${this.escape(expr)})`;
}

/**
* @param {import('@themost/query').QueryExpression} expr
*/
$jsonArray(expr) {
if (expr == null) {
throw new Error('The given query expression cannot be null');
}
if (expr instanceof QueryField) {
// escape expr as field and waiting for parsing results as json array
return this.escape(expr);
}
// trear expr as select expression
if (expr.$select) {
// get select fields
const args = Object.keys(expr.$select).reduce((previous, key) => {
previous.push.apply(previous, expr.$select[key]);
return previous;
}, []);
const [key] = Object.keys(expr.$select);
// prepare select expression to return json array
expr.$select[key] = [
{
$jsonGroupArray: [ // use json_group_array function
{
$jsonObject: args // use json_object function
}
]
}
];
return `(${this.format(expr)})`;
}
// treat expression as query field
if (Object.prototype.hasOwnProperty.call(expr, '$name')) {
return this.escape(expr);
}
// treat expression as value
if (Object.prototype.hasOwnProperty.call(expr, '$value')) {
if (Array.isArray(expr.$value)) {
return this.escape(JSON.stringify(expr.$value));
}
return this.escape(expr);
}
if (Object.prototype.hasOwnProperty.call(expr, '$literal')) {
if (Array.isArray(expr.$literal)) {
return this.escape(JSON.stringify(expr.$literal));
}
return this.escape(expr);
}
throw new Error('Invalid json array expression. Expected a valid select expression');
}
}

export {
Expand Down
41 changes: 41 additions & 0 deletions src/isObjectDeep.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import isPlainObject from 'lodash/isPlainObject';
import isObjectLike from 'lodash/isObjectLike';
import isNative from 'lodash/isNative';

const objectToString = Function.prototype.toString.call(Object);

function isObjectDeep(any) {
// check if it is a plain object
let result = isPlainObject(any);
if (result) {
return result;
}
// check if it's object
if (isObjectLike(any) === false) {
return false;
}
// get prototype
let proto = Object.getPrototypeOf(any);
// if prototype exists, try to validate prototype recursively
while(proto != null) {
// get constructor
const Ctor = Object.prototype.hasOwnProperty.call(proto, 'constructor')
&& proto.constructor;
// check if constructor is native object constructor
result = (typeof Ctor == 'function') && (Ctor instanceof Ctor)
&& Function.prototype.toString.call(Ctor) === objectToString;
// if constructor is not object constructor and belongs to a native class
if (result === false && isNative(Ctor) === true) {
// return false
return false;
}
// otherwise. get parent prototype and continue
proto = Object.getPrototypeOf(proto);
}
// finally, return result
return result;
}

export {
isObjectDeep
}