From 9debdfed16ef0538d75bfa8f33f18b779ff01ef1 Mon Sep 17 00:00:00 2001 From: Kyriakos Barbounakis Date: Fri, 17 Jan 2025 18:45:04 +0200 Subject: [PATCH] merge latest changes from master (#191) * merge latest change from master * merge OnJsonAttribute into lts * 2.6.73 --- OnJsonAttribute.js | 121 ++++++++++-------------- data-queryable.js | 11 +++ package-lock.json | 4 +- package.json | 2 +- spec/JsonAttribute.spec.ts | 182 ++++++++++++++++++++++++++++++++++--- 5 files changed, 229 insertions(+), 91 deletions(-) diff --git a/OnJsonAttribute.js b/OnJsonAttribute.js index 1de331b..ff5aed3 100644 --- a/OnJsonAttribute.js +++ b/OnJsonAttribute.js @@ -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) { @@ -46,7 +54,7 @@ class OnJsonAttribute { /** * @type {{edmtype: string,type:string}} */ - const dataType = dataTypes[attr.type]; + const dataType = attr.type !== 'Json' ? dataTypes[attr.type] : null; let type = 'object'; let assign = {}; if (dataType != null) { @@ -92,6 +100,16 @@ class OnJsonAttribute { } } + /** + * @param {import('./data-model').DataModel} model + * @returns {Array} + */ + static getJsonAttributes(model) { + return model.attributes.filter((attr) => { + return attr.type === 'Json' && attr.additionalType != null; + }); + } + /** * @param {import('./types').DataEventArgs} event * @param {function(err?:Error)} callback @@ -168,49 +186,10 @@ class OnJsonAttribute { * @returns void */ static afterSelect(event, callback) { - const anyJsonAttributes = event.model.attributes.filter((attr) => { + const jsonAttributes = event.model.attributes.filter((attr) => { return attr.type === 'Json' && attr.model === event.model.name; - }); - if (anyJsonAttributes.length === 0) { - return callback(); - } - const jsonAttributes = anyJsonAttributes.filter((attr) => { - return attr.additionalType != null; - }).map((attr) => { - return attr.name - }); - // if there are no json attributes with additional type + }).map((attr) => attr.name); if (jsonAttributes.length === 0) { - // get json attributes with no additional type - const unknownJsonAttributes = anyJsonAttributes.filter((attr) => { - return attr.additionalType == null; - }).map((attr) => { - return attr.name - }); - // parse json for each item - if (unknownJsonAttributes.length > 0) { - const parseUnknownJson = (item) => { - unknownJsonAttributes.forEach((name) => { - if (Object.prototype.hasOwnProperty.call(item, name)) { - const value = item[name]; - if (typeof value === 'string') { - item[name] = JSON.parse(value); - } - } - }); - }; - // iterate over result - const {result} = event; - if (result == null) { - return callback(); - } - if (Array.isArray(result)) { - result.forEach((item) => parseUnknownJson(item)); - } else { - // or parse json for single item - parseUnknownJson(result) - } - } return callback(); } @@ -248,40 +227,29 @@ class OnJsonAttribute { // split $name property by dot const matches = jsonGet.$name.split('.'); if (matches && matches.length > 1) { - if (jsonAttributes.indexOf(matches[1]) >= 0) { - // get json schema - if (matches.length > 2) { - // get attribute - const attribute = event.model.getAttribute(matches[1]); - if (attribute) { - // get additional model - const additionalModel = event.model.context.model(attribute.additionalType) - // and its json schema - let jsonSchema = OnJsonAttribute.getJsonSchema(additionalModel); - let index = 2; - // iterate over matches - while(index < matches.length) { - // get property - const property = jsonSchema.properties[matches[index]]; - // if property is an object - if (property && property.type === 'object') { - if (index + 1 === matches.length) { - prev.push(matches[index]); - break; - } - if (Array.isArray(property.properties)) { - jsonSchema = property.properties; - } else { - prev.push(matches[index]); - break; - } - } - index++; - } + let index = 1; + let nextModel = event.model; + // iterate over matches + while(index < matches.length) { + let attribute = nextModel.getAttribute(matches[index]); + if (attribute && attribute.type === 'Json') { + if (index + 1 === matches.length) { + 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 { - prev.push(matches[2]); + break; } + index++; } } } @@ -293,13 +261,16 @@ class OnJsonAttribute { if (select.length === 0) { attributes = jsonAttributes; } + if (attributes.length === 0) { + return callback(); + } // define json converter const parseJson = (item) => { 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; } } }); diff --git a/data-queryable.js b/data-queryable.js index 70ed6ef..f39eb38 100644 --- a/data-queryable.js +++ b/data-queryable.js @@ -27,7 +27,18 @@ function resolveJoinMember(target) { * @type {Array} */ var fullyQualifiedMember = event.fullyQualifiedMember.split('.'); + var 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, { diff --git a/package-lock.json b/package-lock.json index fe8aa6a..4b3a1c8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@themost/data", - "version": "2.6.72", + "version": "2.6.73", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@themost/data", - "version": "2.6.72", + "version": "2.6.73", "license": "BSD-3-Clause", "dependencies": { "@themost/events": "^1.3.0", diff --git a/package.json b/package.json index fee6504..b4b47be 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@themost/data", - "version": "2.6.72", + "version": "2.6.73", "description": "MOST Web Framework Codename Blueshift - Data module", "main": "index.js", "types": "index.d.ts", diff --git a/spec/JsonAttribute.spec.ts b/spec/JsonAttribute.spec.ts index 678efaa..aa86f5e 100644 --- a/spec/JsonAttribute.spec.ts +++ b/spec/JsonAttribute.spec.ts @@ -1,9 +1,7 @@ import {TestApplication} from './TestApplication'; import {DataContext} from '../types'; -import {resolve} from 'path'; import { TestUtils } from "./adapter/TestUtils"; -import pluralize from "pluralize"; -import { setSingularRules } from '../data-pluralize'; +import { resolve } from 'path'; describe('JsonAttribute', () => { @@ -49,13 +47,16 @@ describe('JsonAttribute', () => { }); }); - it('should update unknown json attribute', async () => { + 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: 'Intel Core i5', + cpu: { + brand: 'Intel', + model: 'Core i5' + }, ram: '8GB' } await Products.save(item); @@ -63,13 +64,165 @@ describe('JsonAttribute', () => { 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).toBe('Intel Core i5'); + 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 context.model('Product').asQueryable() + .where((x: any) => { + return x.name === 'Apple MacBook Air (13.3-inch, 2013 Version)' && + 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(); @@ -93,6 +246,15 @@ describe('JsonAttribute', () => { .select('metadata/audience/name as audienceName').getItem(); expect(item).toBeTruthy(); expect(item.audienceName).toBe('New customers'); + item = await Products.asQueryable().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'); }); }); @@ -135,12 +297,6 @@ describe('JsonAttribute', () => { }); }); - it('should use pluralize rules', async () => { - expect(pluralize.isSingular('metadata')).toBeFalsy(); - setSingularRules(pluralize); - expect(pluralize.isSingular('metadata')).toBeTruthy(); - }); - it('should select json attribute from $select', async () => { const Products = context.model('Product').silent(); const options = { @@ -180,4 +336,4 @@ describe('JsonAttribute', () => { }); }); -}); +}); \ No newline at end of file