Skip to content

Commit

Permalink
select and query unknown json attributes (#190)
Browse files Browse the repository at this point in the history
* select and query nested json attributes

* resolve attribute as  $jsonGet method

* 2.17.2
  • Loading branch information
kbarbounakis authored Jan 16, 2025
1 parent 9417bbe commit 18c0d31
Show file tree
Hide file tree
Showing 7 changed files with 240 additions and 20 deletions.
35 changes: 23 additions & 12 deletions OnJsonAttribute.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,14 @@ const {eachSeries} = require('async');
const {DataConfigurationStrategy} = require('./data-configuration');
const {DataError} = require('@themost/common');

function isJSON(str) {
try {
JSON.parse(str);
} catch (e) {
return false;
}
return true;
}

function edmTypeToJsonType(edmType) {
switch (edmType) {
Expand Down Expand Up @@ -179,13 +187,12 @@ class OnJsonAttribute {
*/
static afterSelect(event, callback) {
const jsonAttributes = event.model.attributes.filter((attr) => {
return attr.type === 'Json' && attr.additionalType != null;
}).map((attr) => {
return attr.name
});
return attr.type === 'Json' && attr.model === event.model.name;
}).map((attr) => attr.name);
if (jsonAttributes.length === 0) {
return callback();
}

let select = [];
const { viewAdapter: entity } = event.model;
if (event.emitter && event.emitter.query && event.emitter.query.$select) {
Expand Down Expand Up @@ -226,13 +233,17 @@ class OnJsonAttribute {
while(index < matches.length) {
let attribute = nextModel.getAttribute(matches[index]);
if (attribute && attribute.type === 'Json') {
// get next model
nextModel = event.model.context.model(attribute.additionalType)
// if this is the last match
if (index + 1 === matches.length) {
// add attribute
prev.push(matches[index]);
// and exit
prev.push(key);
break;
}
if (attribute.additionalType) {
// get next model
nextModel = event.model.context.model(attribute.additionalType)
} else {
// add last part
prev.push(key);
// and exit loop
break;
}
} else {
Expand All @@ -258,8 +269,8 @@ class OnJsonAttribute {
attributes.forEach((name) => {
if (Object.prototype.hasOwnProperty.call(item, name)) {
const value = item[name];
if (typeof value === 'string') {
item[name] = JSON.parse(value);
if (typeof value === 'string') {
item[name] = isJSON(value) ? JSON.parse(value) : value;
}
}
});
Expand Down
11 changes: 9 additions & 2 deletions data-model.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ var DataObjectAssociationListener = dataAssociations.DataObjectAssociationListen
var {DataModelView} = require('./data-model-view');
var {DataFilterResolver} = require('./data-filter-resolver');
var Q = require('q');
var {SequentialEventEmitter} = require('@themost/common');
var {SequentialEventEmitter,Args} = require('@themost/common');
var {LangUtils} = require('@themost/common');
var {TraceUtils} = require('@themost/common');
var {DataError} = require('@themost/common');
Expand All @@ -43,6 +43,7 @@ var { OnJsonAttribute } = require('./OnJsonAttribute');
var { isObjectDeep } = require('./is-object');
var { DataStateValidatorListener } = require('./data-state-validator');
var resolver = require('./data-expand-resolver');
var { isArrayLikeObject } = require('lodash/isArrayLikeObject');
/**
* @this DataModel
* @param {DataField} field
Expand Down Expand Up @@ -1577,7 +1578,13 @@ function cast_(obj, state) {
if (mapping == null) {
var {[name]: value} = obj;
if (x.type === 'Json') {
result[x.name] = isObjectDeep(value) ? JSON.stringify(value) : null;
if (value == null) {
result[x.name] = null;
} else {
var isObjectOrArray = isObjectDeep(value) || isArrayLikeObject(value);
Args.check(isObjectOrArray, new DataError('ERR_VALUE','Invalid attribute value. Expected a valid object or an array.', null, self.name, x.name));
result[x.name] = JSON.stringify(value);
}
} else {
result[x.name] = value;
}
Expand Down
17 changes: 15 additions & 2 deletions data-queryable.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ var _ = require('lodash');
var {TextUtils} = require('@themost/common');
var {DataMappingExtender} = require('./data-mapping-extensions');
var {DataAssociationMapping} = require('./types');
var {DataError} = require('@themost/common');
var {QueryField, Expression} = require('@themost/query');
var {DataError, Args} = require('@themost/common');
var {QueryField, Expression, MethodCallExpression} = require('@themost/query');
var {QueryEntity} = require('@themost/query');
var {QueryUtils} = require('@themost/query');
var Q = require('q');
Expand All @@ -15,6 +15,7 @@ var { DataAttributeResolver } = require('./data-attribute-resolver');
var { DataExpandResolver } = require('./data-expand-resolver');
var {instanceOf} = require('./instance-of');
var { DataValueResolver } = require('./data-value-resolver');

/**
* @param {DataQueryable} target
*/
Expand All @@ -24,7 +25,19 @@ function resolveJoinMember(target) {
* @type {Array}
*/
var fullyQualifiedMember = event.fullyQualifiedMember.split('.');
// validate first member
const attribute = target.model.getAttribute(fullyQualifiedMember[0]);
var expr = DataAttributeResolver.prototype.resolveNestedAttribute.call(target, fullyQualifiedMember.join('/'));
if (attribute && attribute.type === 'Json') {
Args.check(expr.$value != null, 'Invalid expression. Expected a JSON expression.');
var [method] = Object.keys(expr.$value); // get method name
var methodWithoutSign = method.replace(/\$/g, '');
var { [method]: args } = expr.$value;
Object.assign(event, {
member: new MethodCallExpression(methodWithoutSign, args)
});
return;
}
if (instanceOf(expr, QueryField)) {
var member = expr.$name.split('.');
Object.assign(event, {
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

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

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@themost/data",
"version": "2.17.1",
"version": "2.17.2",
"description": "MOST Web Framework Codename Blueshift - Data module",
"main": "index.js",
"scripts": {
Expand Down
184 changes: 184 additions & 0 deletions spec/JsonAttribute.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,181 @@ describe('JsonAttribute', () => {
});
});

it('should update and get unknown json attribute', async () => {
await TestUtils.executeInTransaction(context, async () => {
const Products = context.model('Product').silent();
let item = await Products.where('name').equal('Apple MacBook Air (13.3-inch, 2013 Version)').getItem()
expect(item).toBeTruthy();
item.extraAttributes = {
cpu: {
brand: 'Intel',
model: 'Core i5'
},
ram: '8GB'
}
await Products.save(item);
expect(item.dateCreated).toBeTruthy();
item = await Products.where('name').equal('Apple MacBook Air (13.3-inch, 2013 Version)')
.select('extraAttributes/cpu as cpu').getItem();
expect(item).toBeTruthy();
expect(item.cpu).toStrictEqual(
{
brand: 'Intel',
model: 'Core i5'
}
);
item = await Products.where('name').equal('Apple MacBook Air (13.3-inch, 2013 Version)').getItem();
expect(item).toBeTruthy();
expect(item.extraAttributes).toBeTruthy();
});
});

it('should update and get unknown json attribute value', async () => {
await TestUtils.executeInTransaction(context, async () => {
const Products = context.model('Product').silent();
let item = await Products.where('name').equal('Apple MacBook Air (13.3-inch, 2013 Version)').getItem()
expect(item).toBeTruthy();
item.extraAttributes = {
cpu: {
brand: 'Intel',
model: 'Core i5'
},
ram: '8GB'
}
await Products.save(item);
expect(item.dateCreated).toBeTruthy();
item = await Products.where('name').equal('Apple MacBook Air (13.3-inch, 2013 Version)')
.select('extraAttributes/cpu/brand as cpuBrand').getItem();
expect(item).toBeTruthy();
expect(item.cpuBrand).toBe('Intel');
item = await Products.where('name').equal('Apple MacBook Air (13.3-inch, 2013 Version)').getItem();
expect(item).toBeTruthy();
expect(item.extraAttributes).toBeTruthy();
});
});

it('should update and get unknown json attribute value (without alias)', async () => {
await TestUtils.executeInTransaction(context, async () => {
const Products = context.model('Product').silent();
let item = await Products.where('name').equal('Apple MacBook Air (13.3-inch, 2013 Version)').getItem()
expect(item).toBeTruthy();
item.extraAttributes = {
cpu: {
brand: 'Intel',
model: 'Core i5'
},
ram: '8GB'
}
await Products.save(item);
expect(item.dateCreated).toBeTruthy();
item = await Products.where('name').equal('Apple MacBook Air (13.3-inch, 2013 Version)')
.select('extraAttributes/cpu/brand').getItem();
expect(item).toBeTruthy();
expect(item.extraAttributes_cpu_brand).toBe('Intel');
});
});

it('should update and get unknown json attribute value (with closure)', async () => {
await TestUtils.executeInTransaction(context, async () => {
const Products = context.model('Product').silent();
let item = await Products.where('name').equal('Apple MacBook Air (13.3-inch, 2013 Version)').getItem()
expect(item).toBeTruthy();
item.extraAttributes = {
cpu: {
brand: 'Intel',
model: 'Core i5'
},
ram: '8GB'
}
await Products.save(item);
expect(item.dateCreated).toBeTruthy();
item = await Products.where('name').equal('Apple MacBook Air (13.3-inch, 2013 Version)')
.select((x: any) => {
return {
brand: x.extraAttributes.cpu.brand
}
}).getItem();
expect(item).toBeTruthy();
expect(item.brand).toBe('Intel');
});
});

it('should query and get unknown json attribute value (with closure)', async () => {
await TestUtils.executeInTransaction(context, async () => {
const Products = context.model('Product').silent();
let item = await Products.where('name').equal('Apple MacBook Air (13.3-inch, 2013 Version)').getItem()
expect(item).toBeTruthy();
item.extraAttributes = {
cpu: {
brand: 'Intel',
model: 'Core i5'
},
ram: '8GB'
}
await Products.save(item);
expect(item.dateCreated).toBeTruthy();
item = await Products.where('name').equal('Apple MacBook Air (13.3-inch, 2013 Version)')
.where((x: any) => {
return x.extraAttributes.cpu.brand === 'Intel'
}).getItem();
expect(item).toBeTruthy();
expect(item.name).toBe('Apple MacBook Air (13.3-inch, 2013 Version)');
});
});

it('should update and get unknown json attribute value (with closure and function)', async () => {
await TestUtils.executeInTransaction(context, async () => {
const Products = context.model('Product').silent();
let item = await Products.where('name').equal('Apple MacBook Air (13.3-inch, 2013 Version)').getItem()
expect(item).toBeTruthy();
item.extraAttributes = {
cpu: {
brand: 'Intel',
model: 'Core i5'
},
ram: '8GB'
}
await Products.save(item);
expect(item.dateCreated).toBeTruthy();
item = await Products.where('name').equal('Apple MacBook Air (13.3-inch, 2013 Version)')
.select((x: any) => {
return {
brand: x.extraAttributes.cpu.brand.toUpperCase()
}
}).getItem();
expect(item).toBeTruthy();
expect(item.brand).toBe('INTEL');
});
});

it('should update and get unknown json object value (with closure)', async () => {
await TestUtils.executeInTransaction(context, async () => {
const Products = context.model('Product').silent();
let item = await Products.where('name').equal('Apple MacBook Air (13.3-inch, 2013 Version)').getItem()
expect(item).toBeTruthy();
item.extraAttributes = {
cpu: {
brand: 'Intel',
model: 'Core i5'
},
ram: '8GB'
}
await Products.save(item);
expect(item.dateCreated).toBeTruthy();
item = await Products.where('name').equal('Apple MacBook Air (13.3-inch, 2013 Version)')
.select((x: any) => {
return {
cpu: x.extraAttributes.cpu
}
}).getItem();
expect(item).toBeTruthy();
expect(item.cpu).toStrictEqual({
brand: 'Intel',
model: 'Core i5'
});
});
});

it('should update json structured value', async () => {
await TestUtils.executeInTransaction(context, async () => {
const Products = context.model('Product').silent();
Expand All @@ -69,6 +244,15 @@ describe('JsonAttribute', () => {
.select('metadata/audience/name as audienceName').getItem();
expect(item).toBeTruthy();
expect(item.audienceName).toBe('New customers');
item = await Products.where((x: any) => {
return x.name === 'Apple MacBook Air (13.3-inch, 2013 Version)'
}).select((x: any) => {
return {
audienceName: x.metadata.audience.name
}
}).getItem();
expect(item).toBeTruthy();
expect(item.audienceName).toBe('New customers');
});
});

Expand Down
7 changes: 6 additions & 1 deletion spec/test2/config/models/Product.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
"hidden": false,
"sealed": false,
"abstract": false,
"version": "2.2",
"version": "2.2.2",
"inherits": "Thing",
"caching":"conditional",
"fields": [
Expand Down Expand Up @@ -229,6 +229,11 @@
"many": false,
"type": "Json",
"additionalType": "ProductMetadata"
},
{
"name": "extraAttributes",
"many": false,
"type": "Json"
}
],
"constraints": [
Expand Down

0 comments on commit 18c0d31

Please sign in to comment.