From 3d41f8e73a410d8478c3fed9b9b6ff9bee552966 Mon Sep 17 00:00:00 2001 From: Kyriakos Barbounakis Date: Tue, 4 Feb 2025 10:26:46 +0200 Subject: [PATCH 1/8] resolving join attributes of type json --- data-model.js | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/data-model.js b/data-model.js index 1913e95..63dc18e 100644 --- a/data-model.js +++ b/data-model.js @@ -779,8 +779,13 @@ function filterInternal(params, callback) { var arrExpr = []; if (_.isArray(expr)) arrExpr.push.apply(arrExpr, expr); - else - arrExpr.push(expr); + else { + if (expr.$expand) { + arrExpr.push.apply(arrExpr, expr.$expand); + } else { + arrExpr.push(expr); + } + } arrExpr.forEach(function(y) { var joinExpr = $joinExpressions.find(function(x) { if (x.$entity && x.$entity.$as) { @@ -791,6 +796,9 @@ function filterInternal(params, callback) { if (_.isNil(joinExpr)) $joinExpressions.push(y); }); + if (expr.$select) { + return cb(null, expr.$select); + } } } catch (err) { From c4e83fb7016dbf80c537a6904a6de3b1e52b725b Mon Sep 17 00:00:00 2001 From: Kyriakos Barbounakis Date: Tue, 4 Feb 2025 14:10:25 +0200 Subject: [PATCH 2/8] use open data parser for $select, $orderby and $groupby system query options --- data-model.js | 202 +++++++++++++++++++++++++++++++++----------------- 1 file changed, 135 insertions(+), 67 deletions(-) diff --git a/data-model.js b/data-model.js index 63dc18e..bafbe88 100644 --- a/data-model.js +++ b/data-model.js @@ -834,82 +834,150 @@ function filterInternal(params, callback) { } try { - parser.parse(filter, function(err, query) { - if (err) { - callback(err); - } - else { - //create a DataQueryable instance - var q = new DataQueryable(self); - q.query.$where = query; - if ($joinExpressions.length>0) - q.query.$expand = $joinExpressions; - //prepare - q.query.prepare(); - - if (typeof params === 'object') { - //apply query parameters - var select = params.$select, - skip = params.$skip || 0, - orderBy = params.$orderby || params.$order, - groupBy = params.$groupby || params.$group, - expand = params.$expand, - levels = parseInt(params.$levels), - top = params.$top || params.$take; - //select fields - if (typeof select === 'string') { - q.select.apply(q, select.split(',').map(function(x) { - return x.replace(/^\s+|\s+$/g, ''); - })); + // backward compatibility: create a new open data parser (without any resolver) + // for splitting tokens and get list of attributes to select, order by, group by etc + // for this operation we are using a new instance of OpenDataParser and execute parseSelectSequence, + // parseOrderBySequence and parseGroupBySequence methods + // each of these methods will return a list of tokens which are going to be passed to query + const alternateParser = new OpenDataParser(); + async.series([ + function(cb) { + return parser.parse(filter, cb); + }, + function (cb) { + // use parseSelectSequence to split tokens + var select = params.$select; + if (select == null) { + return cb(null, []); + } + return alternateParser.parseSelectSequence(select, function(err, result) { + if (err) { + return cb(err); + } + if (Array.isArray(result)) { + // get tokens + var tokens = result.map(function(x) { + return x.source; + }); + return cb(null, tokens); } - //apply group by fields - if (typeof groupBy === 'string') { - q.groupBy.apply(q, groupBy.split(',').map(function(x) { - return x.replace(/^\s+|\s+$/g, ''); - })); + return cb(null, []); + }); + }, + function (cb) { + // use parseSelectSequence to split tokens + var orderBy = params.$orderby || params.$orderBy || params.$order; + if (orderBy == null) { + return cb(null, []); + } + return alternateParser.parseOrderBySequence(orderBy, function(err, result) { + if (err) { + return cb(err); } - if ((typeof levels === 'number') && !isNaN(levels)) { - //set expand levels - q.levels(levels); + if (Array.isArray(result)) { + // get tokens + var tokens = result.map(function(x) { + return x.source; + }); + return cb(null, tokens); } - //set $skip - q.skip(skip); - if (top) - q.query.take(top); - //set caching - if (params.$cache && self.caching === 'conditional') { - q.cache(true); + return cb(null, []); + }); + }, + function (cb) { + // use parseSelectSequence to split tokens + var groupBy = params.$groupby || params.$groupBy || params.$group; + if (groupBy == null) { + return cb(null, []); + } + return alternateParser.parseGroupBySequence(groupBy, function(err, result) { + if (err) { + return cb(err); } - //set $orderby - if (orderBy) { - orderBy.split(',').map(function(x) { - return x.replace(/^\s+|\s+$/g, ''); - }).forEach(function(x) { - if (/\s+desc$/i.test(x)) { - q.orderByDescending(x.replace(/\s+desc$/i, '')); - } - else if (/\s+asc/i.test(x)) { - q.orderBy(x.replace(/\s+asc/i, '')); - } - else { - q.orderBy(x); - } + if (Array.isArray(result)) { + // get tokens + var tokens = result.map(function(x) { + return x.source; }); + return cb(null, tokens); } - if (expand) { - var matches = resolver.testExpandExpression(expand); - if (matches && matches.length>0) { - q.expand.apply(q, matches); + return cb(null, []); + }); + } + ], function(err, results) { + if (err) { + callback(err); + } else { + try { + var query = results[0]; + var selectArgs = results[1]; + var orderByArgs = results[2]; + var groupByArgs = results[3]; + //create a DataQueryable instance + var q = new DataQueryable(self); + q.query.$where = query; + if ($joinExpressions.length>0) + q.query.$expand = $joinExpressions; + //prepare + q.query.prepare(); + if (typeof params === 'object') { + //apply query parameters + var skip = params.$skip || 0; + var expand = params.$expand; + var levels = parseInt(params.$levels); + var top = params.$top || params.$take; + //select fields + if (selectArgs.length>0) { + q.select.apply(q, selectArgs); + } + //apply group by fields + if (groupByArgs.length>0) { + q.groupBy.apply(q, groupByArgs); + } + if ((typeof levels === 'number') && !isNaN(levels)) { + //set expand levels + q.levels(levels); + } + //set $skip + q.skip(skip); + if (top) { + q.query.take(top); + } + //set caching + if (params.$cache && self.caching === 'conditional') { + q.cache(true); + } + //set $orderby + if (orderByArgs.length) { + orderByArgs.map(function(x) { + return x.replace(/^\s+|\s+$/g, ''); + }).forEach(function(x) { + if (/\s+desc$/i.test(x)) { + q.orderByDescending(x.replace(/\s+desc$/i, '')); + } + else if (/\s+asc/i.test(x)) { + q.orderBy(x.replace(/\s+asc/i, '')); + } + else { + q.orderBy(x); + } + }); + } + if (expand) { + var matches = resolver.testExpandExpression(expand); + if (matches && matches.length>0) { + q.expand.apply(q, matches); + } } + //return + callback(null, q); + } else { + //and finally return DataQueryable instance + callback(null, q); } - //return - callback(null, q); - } - else { - //and finally return DataQueryable instance - callback(null, q); + } catch (err) { + return callback(err); } - } }); } From fe0ab788abc16926037761eeda234c2422bcba45 Mon Sep 17 00:00:00 2001 From: Kyriakos Barbounakis Date: Tue, 4 Feb 2025 16:10:43 +0200 Subject: [PATCH 3/8] use open data parser on DataModel.filter() --- data-model.js | 49 +++++++++++------------------- spec/DataModelFilterParser.spec.ts | 39 ++++++++++++++++++++++++ 2 files changed, 56 insertions(+), 32 deletions(-) diff --git a/data-model.js b/data-model.js index bafbe88..8ff7c82 100644 --- a/data-model.js +++ b/data-model.js @@ -850,16 +850,12 @@ function filterInternal(params, callback) { if (select == null) { return cb(null, []); } - return alternateParser.parseSelectSequence(select, function(err, result) { + return parser.parseSelectSequence(select, function(err, result) { if (err) { return cb(err); } if (Array.isArray(result)) { - // get tokens - var tokens = result.map(function(x) { - return x.source; - }); - return cb(null, tokens); + return cb(null, result); } return cb(null, []); }); @@ -870,16 +866,12 @@ function filterInternal(params, callback) { if (orderBy == null) { return cb(null, []); } - return alternateParser.parseOrderBySequence(orderBy, function(err, result) { + return parser.parseOrderBySequence(orderBy, function(err, result) { if (err) { return cb(err); } if (Array.isArray(result)) { - // get tokens - var tokens = result.map(function(x) { - return x.source; - }); - return cb(null, tokens); + return cb(null, result); } return cb(null, []); }); @@ -890,16 +882,12 @@ function filterInternal(params, callback) { if (groupBy == null) { return cb(null, []); } - return alternateParser.parseGroupBySequence(groupBy, function(err, result) { + return parser.parseGroupBySequence(groupBy, function(err, result) { if (err) { return cb(err); } if (Array.isArray(result)) { - // get tokens - var tokens = result.map(function(x) { - return x.source; - }); - return cb(null, tokens); + return cb(null, result); } return cb(null, []); }); @@ -927,12 +915,19 @@ function filterInternal(params, callback) { var levels = parseInt(params.$levels); var top = params.$top || params.$take; //select fields + var { viewAdapter: collection } = self; if (selectArgs.length>0) { - q.select.apply(q, selectArgs); + q.query.$select = { + [collection]: selectArgs.map(function(selectArg) { + return selectArg.exprOf(); + }) + }; } //apply group by fields if (groupByArgs.length>0) { - q.groupBy.apply(q, groupByArgs); + q.query.$group = groupByArgs.map(function(groupByArg) { + return groupByArg.exprOf(); + }); } if ((typeof levels === 'number') && !isNaN(levels)) { //set expand levels @@ -949,18 +944,8 @@ function filterInternal(params, callback) { } //set $orderby if (orderByArgs.length) { - orderByArgs.map(function(x) { - return x.replace(/^\s+|\s+$/g, ''); - }).forEach(function(x) { - if (/\s+desc$/i.test(x)) { - q.orderByDescending(x.replace(/\s+desc$/i, '')); - } - else if (/\s+asc/i.test(x)) { - q.orderBy(x.replace(/\s+asc/i, '')); - } - else { - q.orderBy(x); - } + q.query.$order = orderByArgs.map(function(orderByArg) { + return orderByArg.exprOf(); }); } if (expand) { diff --git a/spec/DataModelFilterParser.spec.ts b/spec/DataModelFilterParser.spec.ts index 8f73492..c348416 100644 --- a/spec/DataModelFilterParser.spec.ts +++ b/spec/DataModelFilterParser.spec.ts @@ -113,4 +113,43 @@ describe('DataModelFilterParser', () => { }); }); + + it('should parse select filter with expression', async () => { + await TestUtils.executeInTransaction(context, async () => { + delete context.user; + const Orders = context.model('Order'); + let q = await Orders.filterAsync({ + $select: 'orderedItem/name as product,round(orderedItem/price,2) as price, max(orderDate) as lastOrderDate', + $filter: 'orderedItem/category eq \'Laptops\'', + $orderBy: 'orderedItem/price desc', + $groupBy: 'orderedItem/name,orderedItem/price' + }) + let items: {product: string, price: number, lastOrderDate: Date}[] = await q.take(25).getItems(); + expect(items.length).toBeFalsy(); + context.user = { + name: 'aaron.matthews@example.com' + } + q = await Orders.filterAsync({ + $select: 'orderedItem/name as product,round(orderedItem/price,2) as price, max(orderDate) as lastOrderDate', + //$filter: 'orderedItem/category eq \'Laptops\'', + $orderBy: 'orderedItem/price desc', + $groupBy: 'orderedItem/name,orderedItem/price' + }) + items = await q.getItems(); + expect(items.length).toBeTruthy(); + // get orders + const orders = await context.model('Order').select( + 'orderedItem/name as product', + 'orderDate' + ).getItems(); + for (const item of items) { + const order = orders.sort((a, b) => { + return a.orderDate < b.orderDate ? 1 : -1; + }).find(o => o.product === item.product); + expect(order).toBeTruthy(); + expect(order.orderDate).toEqual(item.lastOrderDate); + } + }); + }); + }); From c6b274c0b3bf9fcb3ebd6ed921490e0a690674a8 Mon Sep 17 00:00:00 2001 From: Kyriakos Barbounakis Date: Wed, 5 Feb 2025 10:26:51 +0200 Subject: [PATCH 4/8] validate data view filter --- data-model.js | 139 +++++++++++++++------------- spec/DataAttributeResolver.spec.ts | 16 ---- spec/DataView.spec.ts | 111 ++++++++++++++++++++++ spec/TestTemplates.spec.ts | 31 +++++++ spec/test2/config/models/Order.json | 3 + 5 files changed, 219 insertions(+), 81 deletions(-) create mode 100644 spec/DataView.spec.ts create mode 100644 spec/TestTemplates.spec.ts diff --git a/data-model.js b/data-model.js index 8ff7c82..fff1f92 100644 --- a/data-model.js +++ b/data-model.js @@ -745,18 +745,67 @@ DataModel.prototype.asQueryable = function() { function filterInternal(params, callback) { var self = this; var parser = OpenDataParser.create(), $joinExpressions = [], view; - if (typeof params !== 'undefined' && params !== null && typeof params.$select === 'string') { - //split select - var arr = params.$select.split(','); - if (arr.length===1) { - //try to get data view - view = self.dataviews(arr[0]); + if (params && typeof params.$select === 'string') { + if (/^(\w+)$/g.test(params.$select)) { + // try to get data view + view = self.dataviews(params.$select); + if (view) { + const viewParams =Object.assign({}, params); + // define view attributes + var $select = view.fields.map(function(x) { + if (x.property) { + return sprintf('%s as %s', x.name, x.property); + } + return x.name; + }).join(','); + Object.assign(viewParams, { + $select + }); + if (view.levels) { + Object.assign(viewParams, { $levels: view.levels }); + } + // assign view + Object.assign(viewParams, { $view: view.name }); + return self.filter(viewParams, function(err, q) { + if (err) { + return callback(err); + } + // assign view to query + Object.assign(q, { $view: view }); + // return query + return callback(null, q); + }); + } } } + // important: if $view parameter is defined then try to get view + // in order to validate member expressions + if (params && typeof params.$view === 'string') { + view = self.getDataView(params.$view); + } parser.resolveMember = function(member, cb) { if (view) { - var field = view.fields.find(function(x) { return x.property === member }); - if (field) { member = field.name; } + var field = view.fields.find(function(x) { + if (typeof x.property === 'string') { + return x.name === member || x.property === member; + } + return x.name === member; + }); + if (field) { + member = field.name; + } else { + var memberParts = member.split('/'); + field = view.fields.find(function(x) { + if (typeof x.property === 'string') { + return x.property === memberParts[0]; + } + return x.name === memberParts[0]; + }); + if (field == null) { + // throw exception for invalid usage of field + throw new DataError('E_INVALID_ATTR', 'The specified attribute is not valid at the context of a pre-defined object view.', null, self.name, member); + } + } } var attr = self.field(member); if (attr) @@ -834,12 +883,6 @@ function filterInternal(params, callback) { } try { - // backward compatibility: create a new open data parser (without any resolver) - // for splitting tokens and get list of attributes to select, order by, group by etc - // for this operation we are using a new instance of OpenDataParser and execute parseSelectSequence, - // parseOrderBySequence and parseGroupBySequence methods - // each of these methods will return a list of tokens which are going to be passed to query - const alternateParser = new OpenDataParser(); async.series([ function(cb) { return parser.parse(filter, cb); @@ -850,15 +893,7 @@ function filterInternal(params, callback) { if (select == null) { return cb(null, []); } - return parser.parseSelectSequence(select, function(err, result) { - if (err) { - return cb(err); - } - if (Array.isArray(result)) { - return cb(null, result); - } - return cb(null, []); - }); + return parser.parseSelectSequence(select, cb); }, function (cb) { // use parseSelectSequence to split tokens @@ -866,15 +901,7 @@ function filterInternal(params, callback) { if (orderBy == null) { return cb(null, []); } - return parser.parseOrderBySequence(orderBy, function(err, result) { - if (err) { - return cb(err); - } - if (Array.isArray(result)) { - return cb(null, result); - } - return cb(null, []); - }); + return parser.parseOrderBySequence(orderBy, cb); }, function (cb) { // use parseSelectSequence to split tokens @@ -882,31 +909,21 @@ function filterInternal(params, callback) { if (groupBy == null) { return cb(null, []); } - return parser.parseGroupBySequence(groupBy, function(err, result) { - if (err) { - return cb(err); - } - if (Array.isArray(result)) { - return cb(null, result); - } - return cb(null, []); - }); + return parser.parseGroupBySequence(groupBy, cb); } ], function(err, results) { if (err) { callback(err); } else { try { - var query = results[0]; - var selectArgs = results[1]; - var orderByArgs = results[2]; - var groupByArgs = results[3]; + var [where, select, orderBy, groupBy] = results; //create a DataQueryable instance var q = new DataQueryable(self); - q.query.$where = query; - if ($joinExpressions.length>0) + q.query.$where = where; + if ($joinExpressions.length>0) { q.query.$expand = $joinExpressions; - //prepare + } + // use prepare statement to allow further processing q.query.prepare(); if (typeof params === 'object') { //apply query parameters @@ -916,16 +933,16 @@ function filterInternal(params, callback) { var top = params.$top || params.$take; //select fields var { viewAdapter: collection } = self; - if (selectArgs.length>0) { + if (select.length>0) { q.query.$select = { - [collection]: selectArgs.map(function(selectArg) { + [collection]: select.map(function(selectArg) { return selectArg.exprOf(); }) }; } //apply group by fields - if (groupByArgs.length>0) { - q.query.$group = groupByArgs.map(function(groupByArg) { + if (groupBy.length>0) { + q.query.$group = groupBy.map(function(groupByArg) { return groupByArg.exprOf(); }); } @@ -943,8 +960,8 @@ function filterInternal(params, callback) { q.cache(true); } //set $orderby - if (orderByArgs.length) { - q.query.$order = orderByArgs.map(function(orderByArg) { + if (orderBy.length) { + q.query.$order = orderBy.map(function(orderByArg) { return orderByArg.exprOf(); }); } @@ -954,10 +971,10 @@ function filterInternal(params, callback) { q.expand.apply(q, matches); } } - //return - callback(null, q); + + return callback(null, q); } else { - //and finally return DataQueryable instance + // and finally return an instance of DataQueryable callback(null, q); } } catch (err) { @@ -976,14 +993,6 @@ function filterInternal(params, callback) { * @param {String|{$filter:string=, $skip:number=, $levels:number=, $top:number=, $take:number=, $order:string=, $inlinecount:string=, $expand:string=,$select:string=, $orderby:string=, $group:string=, $groupby:string=}} params - A string that represents an open data filter or an object with open data parameters * @param {Function=} callback - A callback function where the first argument will contain the Error object if an error occurred, or null otherwise. The second argument will contain an instance of DataQueryable class. * @returns Promise|* - * @example - context.model('Order').filter(context.params, function(err,q) { - if (err) { return callback(err); } - q.take(10, function(err, result) { - if (err) { return callback(err); } - callback(null, result); - }); - }); */ DataModel.prototype.filter = function(params, callback) { if (typeof callback === 'function') { diff --git a/spec/DataAttributeResolver.spec.ts b/spec/DataAttributeResolver.spec.ts index 0e27705..6d07db5 100644 --- a/spec/DataAttributeResolver.spec.ts +++ b/spec/DataAttributeResolver.spec.ts @@ -112,21 +112,5 @@ describe('DataAttributeResolver', () => { expect(items.value.length).toBeGreaterThan(0); }); }); - - - it('should get nested item', async () => { - await TestUtils.executeInTransaction(context, async () => { - const product = await context.model('Product').asQueryable().silent().getItem(); - product.productImage = { - url: '/images/products/abc.png' - } - await context.model('Product').silent().save(product); - Object.assign(context, { - user: null - }); - let item = await context.model('Product').where('id').equal(product.id).getItem(); - expect(item.productImage).toBeTruthy(); - }); - }); }); diff --git a/spec/DataView.spec.ts b/spec/DataView.spec.ts new file mode 100644 index 0000000..b7f0863 --- /dev/null +++ b/spec/DataView.spec.ts @@ -0,0 +1,111 @@ +import { resolve } from 'path'; +import { DataContext } from '../index'; +import { TestApplication } from './TestApplication'; +import { TestUtils } from './adapter/TestUtils'; +import cloneDeep from 'lodash/cloneDeep'; +const executeInTransaction = TestUtils.executeInTransaction; + +interface DataContextWithUser extends DataContext { + user: any +} + +const contributor = { + "enabled": 1, + "name": "michael.barret@example.com", + "description": "Michael Barret", + "groups": [ + { + "name": "Contributors" + } + ] + }; + +function getNewContributor() { + return { + "enabled": 1, + "name": "michael.barret@example.com", + "description": "Michael Barret", + "groups": [ + { + "name": "Contributors" + } + ] + } +} + +describe('DataView', () => { + let app: TestApplication; + let context: DataContextWithUser; + beforeAll(async () => { + app = new TestApplication(resolve(__dirname, 'test2')); + context = app.createContext() as DataContextWithUser; + }); + afterAll(async () => { + await context.finalizeAsync(); + await app.finalize(); + }); + afterEach(() => { + delete context.user; + }); + + it('should validate view pre-defined privileges', async () => { + await executeInTransaction(context, async () => { + const newUser = getNewContributor(); + await context.model('User').silent().save(newUser); + Object.assign(context, { + user: { + name: newUser.name + } + }); + const Orders = context.model('Order'); + const q = await Orders.filterAsync({ + $select: 'Delivered', + $orderby: 'orderDate desc', + }); + const items = await q.getList(); + expect(items).toBeTruthy(); + expect(Array.isArray(items.value)).toBeTruthy(); + expect(items.value.length).toBeGreaterThan(0); + for (const item of items.value) { + expect(item.orderStatus.id).toEqual(1); + } + }); + }); + + it('should try to use an attribute which is not included in view', async () => { + await executeInTransaction(context, async () => { + const newUser = getNewContributor(); + await context.model('User').silent().save(newUser); + Object.assign(context, { + user: { + name: newUser.name + } + }); + const Orders = context.model('Order'); + await expect(Orders.filterAsync({ + $select: 'Delivered', + $filter: 'customer/jobTitle eq \'Civil Engineer\'' + })).rejects.toThrow('The specified attribute is not valid at the context of a pre-defined object view.'); + }); + }); + + it('should try to use an attribute having alias', async () => { + await executeInTransaction(context, async () => { + const newUser = getNewContributor(); + await context.model('User').silent().save(newUser); + Object.assign(context, { + user: { + name: newUser.name + } + }); + const Orders = context.model('Order'); + const q = await Orders.filterAsync({ + $select: 'Delivered', + $filter: 'customerFamilyName eq \'Chapman\'' + }); + const items = await q.getList(); + expect(items.value.length).toBeGreaterThan(0); + }); + }); + +}); \ No newline at end of file diff --git a/spec/TestTemplates.spec.ts b/spec/TestTemplates.spec.ts new file mode 100644 index 0000000..cf02913 --- /dev/null +++ b/spec/TestTemplates.spec.ts @@ -0,0 +1,31 @@ +import { resolve } from 'path'; +import { DataContext } from '../index'; +import { TestApplication } from './TestApplication'; +import { TestUtils } from './adapter/TestUtils'; +const executeInTransaction = TestUtils.executeInTransaction; + +interface DataContextWithUser extends DataContext { + user: any +} + +describe('TestTemplate', () => { + let app: TestApplication; + let context: DataContextWithUser; + beforeAll(async () => { + app = new TestApplication(resolve(__dirname, 'test2')); + context = app.createContext() as DataContextWithUser; + }); + afterAll(async () => { + await context.finalizeAsync(); + await app.finalize(); + }); + afterEach(() => { + delete context.user; + }); + + it('should test something', async () => { + await executeInTransaction(context, async () => { + + }); + }); +}); \ No newline at end of file diff --git a/spec/test2/config/models/Order.json b/spec/test2/config/models/Order.json index 5c2bfa7..3a71ffd 100644 --- a/spec/test2/config/models/Order.json +++ b/spec/test2/config/models/Order.json @@ -178,6 +178,9 @@ { "name": "orderDate" }, + { + "name": "orderStatus" + }, { "name": "orderedItem" }, From 8bb8090ea1200c4ea013d269c93afd1cb90240df Mon Sep 17 00:00:00 2001 From: Kyriakos Barbounakis Date: Wed, 5 Feb 2025 11:04:27 +0200 Subject: [PATCH 5/8] validate data views --- spec/DataView.spec.ts | 77 ++++++++++++++++++++++++++++ spec/test2/config/models/Group.json | 5 ++ spec/test2/config/models/Order.json | 36 +++++++++++++ spec/test2/config/models/Person.json | 38 ++++++++++++++ 4 files changed, 156 insertions(+) diff --git a/spec/DataView.spec.ts b/spec/DataView.spec.ts index b7f0863..7c59dad 100644 --- a/spec/DataView.spec.ts +++ b/spec/DataView.spec.ts @@ -33,6 +33,19 @@ function getNewContributor() { } } +function getNewAgent() { + return { + "enabled": 1, + "name": "jenna.borrows@example.com", + "description": "Jenna Borrows", + "groups": [ + { + "name": "Agents" + } + ] + } +} + describe('DataView', () => { let app: TestApplication; let context: DataContextWithUser; @@ -108,4 +121,68 @@ describe('DataView', () => { }); }); + it('should try to expand a view attribute using a specific child view', async () => { + await executeInTransaction(context, async () => { + await context.model('Group').silent().save({ + "name": "Agents", + "alternateName": "agents", + "description": "Site Agents" + }); + const newUser = getNewAgent(); + await context.model('User').silent().save(newUser); + Object.assign(context, { + user: { + name: newUser.name + } + }); + const Orders = context.model('Order'); + let q = await Orders.filterAsync({ + $select: 'Processing', + $expand: 'customer($select=summary)' + }); + let items = await q.getList(); + expect(items.value.length).toBeGreaterThan(0); + for (const item of items.value) { + expect(item.customer).toBeTruthy(); + expect(item.customer.familyName).toBeTruthy(); + } + q = await Orders.filterAsync({ + $select: 'Processing', + $expand: 'customer' + }); + items = await q.getList(); + expect(items.value.length).toBeGreaterThan(0); + for (const item of items.value) { + expect(item.customer).toBeFalsy(); + } + }); + }); + + it('should try to expand a view attribute using a an attribute which is not included in child view', async () => { + await executeInTransaction(context, async () => { + await context.model('Group').silent().save({ + "name": "Agents", + "alternateName": "agents", + "description": "Site Agents" + }); + const newUser = getNewAgent(); + await context.model('User').silent().save(newUser); + Object.assign(context, { + user: { + name: newUser.name + } + }); + const Orders = context.model('Order'); + let q = await Orders.filterAsync({ + $select: 'Processing', + $expand: 'customer($select=familyName,jobTitle)' + }); + let items = await q.getList(); + expect(items.value.length).toBeGreaterThan(0); + for (const item of items.value) { + expect(item.customer).toBeFalsy(); + } + }); + }); + }); \ No newline at end of file diff --git a/spec/test2/config/models/Group.json b/spec/test2/config/models/Group.json index fc6d98c..d8b50ad 100644 --- a/spec/test2/config/models/Group.json +++ b/spec/test2/config/models/Group.json @@ -52,6 +52,11 @@ "name": "Contributors", "alternateName": "contributors", "description": "Site Contributors" + }, + { + "name": "Agents", + "alternateName": "agents", + "description": "Site Agents" } ] } diff --git a/spec/test2/config/models/Order.json b/spec/test2/config/models/Order.json index 3a71ffd..caa7710 100644 --- a/spec/test2/config/models/Order.json +++ b/spec/test2/config/models/Order.json @@ -201,6 +201,42 @@ } ] }, + { + "name": "Processing", + "title": "Processing Orders", + "filter": "orderStatus eq 6", + "order": "dateCreated desc", + "fields": [ + { + "name": "id" + }, + { + "name": "orderDate" + }, + { + "name": "orderStatus" + }, + { + "name": "orderedItem" + }, + { + "name": "customer", + "type": "Person" + } + ], + "privileges": [ + { + "mask": 1, + "account": "Contributors", + "type": "global" + }, + { + "mask": 1, + "account": "Agents", + "type": "global" + } + ] + }, { "name": "Latest", "title": "Latest Orders", diff --git a/spec/test2/config/models/Person.json b/spec/test2/config/models/Person.json index 0fea223..ccbc29a 100644 --- a/spec/test2/config/models/Person.json +++ b/spec/test2/config/models/Person.json @@ -179,6 +179,44 @@ } } ], + "views": [ + { + "name": "Summary", + "fields": [ + { + "name": "id" + }, + { + "name": "familyName" + }, + { + "name": "givenName" + } + ], + "privileges": [ + { + "mask": 15, + "type": "global" + }, + { + "mask": 15, + "type": "global", + "account": "Administrators" + }, + { + "mask": 1, + "type": "global", + "account": "Contributors" + }, + { + "mask": 1, + "type": "global", + "account": "Agents" + } + ] + + } + ], "privileges": [ { "mask": 15, From ca5df04277471d58660935ec76bcb21098464f3c Mon Sep 17 00:00:00 2001 From: Kyriakos Barbounakis Date: Wed, 5 Feb 2025 14:36:07 +0200 Subject: [PATCH 6/8] extract view params --- data-model.js | 39 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 38 insertions(+), 1 deletion(-) diff --git a/data-model.js b/data-model.js index fff1f92..37d338f 100644 --- a/data-model.js +++ b/data-model.js @@ -752,7 +752,21 @@ function filterInternal(params, callback) { if (view) { const viewParams =Object.assign({}, params); // define view attributes - var $select = view.fields.map(function(x) { + var $select = view.fields.filter((x) => { + const member = x.name.split('/'); + if (member.length === 1) { + var attribute = self.getAttribute(member[0]); + if (attribute) { + var many = typeof attribute.many === 'boolean' ? attribute.many : false; + if (many) { + return attribute.multiplicity === 'ZeroOrOne'; + } + return !many; + } + } + // todo: check for nested attributes with many-to-many association + return true; + }).map(function(x) { if (x.property) { return sprintf('%s as %s', x.name, x.property); } @@ -761,6 +775,29 @@ function filterInternal(params, callback) { Object.assign(viewParams, { $select }); + // get auto-expand attributes + var $expand = view.fields.filter((x) => { + const member = x.name.split('/'); + if (member.length === 1) { + var attribute = self.getAttribute(member[0]); + if (attribute) { + var many = typeof attribute.many === 'boolean' ? attribute.many && attribute.expandable : false; + if (many) { + return attribute.multiplicity !== 'ZeroOrOne'; + } + return many; + } + } + // todo: check for nested attributes with many-to-many association + return false; + }).map(function(x) { + return x.name; + }).join(','); + if ($expand.length) { + Object.assign(viewParams, { + $expand + }); + } if (view.levels) { Object.assign(viewParams, { $levels: view.levels }); } From 47066ddfc8470811881ff9ad5d95b2f81c5e1753 Mon Sep 17 00:00:00 2001 From: Kyriakos Barbounakis Date: Thu, 6 Feb 2025 07:50:00 +0200 Subject: [PATCH 7/8] update data mapping extensions --- data-mapping-extensions.js | 4 ++-- data-model.js | 33 +++++++++++++++------------------ 2 files changed, 17 insertions(+), 20 deletions(-) diff --git a/data-mapping-extensions.js b/data-mapping-extensions.js index fc80f8c..0a7d567 100644 --- a/data-mapping-extensions.js +++ b/data-mapping-extensions.js @@ -528,7 +528,7 @@ class DataMappingExtender { }); return resolve(); }).catch(function(err) { - return resolve(err); + return reject(err); }); }); }); @@ -977,7 +977,7 @@ class DataMappingOptimizedExtender extends DataMappingExtender { }); return resolve(); }).catch(function(err) { - return resolve(err); + return reject(err); }); }); }); diff --git a/data-model.js b/data-model.js index 37d338f..e0d6dbb 100644 --- a/data-model.js +++ b/data-model.js @@ -2,7 +2,7 @@ // noinspection ES6ConvertVarToLetConst var _ = require('lodash'); -var {cloneDeep} = require('lodash'); +var cloneDeep = require('lodash/cloneDeep'); var {sprintf} = require('sprintf-js'); var Symbol = require('symbol'); var path = require('path'); @@ -738,30 +738,27 @@ DataModel.prototype.asQueryable = function() { /** * @private * @this DataModel - * @param {*} params + * @param {*} filterParams * @param {Function} callback * @returns {*} */ -function filterInternal(params, callback) { +function filterInternal(filterParams, callback) { var self = this; var parser = OpenDataParser.create(), $joinExpressions = [], view; + var params = cloneDeep(filterParams); if (params && typeof params.$select === 'string') { - if (/^(\w+)$/g.test(params.$select)) { + if (/^(\w+)$/.test(params.$select)) { // try to get data view view = self.dataviews(params.$select); if (view) { const viewParams =Object.assign({}, params); // define view attributes - var $select = view.fields.filter((x) => { + var $select = view.fields.filter(function(x) { const member = x.name.split('/'); if (member.length === 1) { var attribute = self.getAttribute(member[0]); if (attribute) { - var many = typeof attribute.many === 'boolean' ? attribute.many : false; - if (many) { - return attribute.multiplicity === 'ZeroOrOne'; - } - return !many; + return typeof attribute.many === 'boolean' ? !attribute.many : true; } } // todo: check for nested attributes with many-to-many association @@ -776,16 +773,12 @@ function filterInternal(params, callback) { $select }); // get auto-expand attributes - var $expand = view.fields.filter((x) => { + var $expand = view.fields.filter(function(x) { const member = x.name.split('/'); if (member.length === 1) { var attribute = self.getAttribute(member[0]); if (attribute) { - var many = typeof attribute.many === 'boolean' ? attribute.many && attribute.expandable : false; - if (many) { - return attribute.multiplicity !== 'ZeroOrOne'; - } - return many; + return typeof attribute.many === 'boolean' ? attribute.many && attribute.expandable : false; } } // todo: check for nested attributes with many-to-many association @@ -956,7 +949,9 @@ function filterInternal(params, callback) { var [where, select, orderBy, groupBy] = results; //create a DataQueryable instance var q = new DataQueryable(self); - q.query.$where = where; + if (where) { + q.query.$where = where; + } if ($joinExpressions.length>0) { q.query.$expand = $joinExpressions; } @@ -976,6 +971,8 @@ function filterInternal(params, callback) { return selectArg.exprOf(); }) }; + } else { + q.select(); } //apply group by fields if (groupBy.length>0) { @@ -984,7 +981,7 @@ function filterInternal(params, callback) { }); } if ((typeof levels === 'number') && !isNaN(levels)) { - //set expand levels + // set expand levels q.levels(levels); } //set $skip From 28e8a2e497472e8fe258de9103c5cf418af4510e Mon Sep 17 00:00:00 2001 From: Kyriakos Barbounakis Date: Thu, 6 Feb 2025 11:45:39 +0200 Subject: [PATCH 8/8] get values when passing objects (#204) * get values when passing objects * 2.6.79 --- data-mapping-extensions.js | 40 +++++++++++++++++++++++++++++++++++--- package-lock.json | 4 ++-- package.json | 2 +- 3 files changed, 40 insertions(+), 6 deletions(-) diff --git a/data-mapping-extensions.js b/data-mapping-extensions.js index 0a7d567..ae0350d 100644 --- a/data-mapping-extensions.js +++ b/data-mapping-extensions.js @@ -5,6 +5,7 @@ var {QueryEntity} = require('@themost/query'); var {QueryField} = require('@themost/query'); var Q = require('q'); var {hasOwnProperty} = require('./has-own-property'); +var {isObjectDeep} = require('./is-object'); class DataMappingExtender { constructor(mapping) { @@ -342,9 +343,20 @@ class DataMappingExtender { if (_.isNil(childField)) { return reject('The specified field cannot be found on child model'); } + var childFieldType = thisQueryable.model.context.model(childField.type); var values = _.intersection(_.map(_.filter(arr, function(x) { return hasOwnProperty(x, keyField); - }), function (x) { return x[keyField];})); + }), function (x) { return x[keyField];})).map(function(x) { + if (isObjectDeep(x)) { + if (childFieldType) { + return x[childFieldType.primaryKey]; + } + throw new Error('The child item is an object but its type cannot determined.'); + } + return x; + }).filter(function(x) { + return x != null; + }); if (values.length===0) { return resolve(); } @@ -421,9 +433,20 @@ class DataMappingExtender { return reject('The specified field cannot be found on parent model'); } var keyField = parentField.property || parentField.name; + var parentFieldType = thisQueryable.model.context.model(parentField.type); var values = _.intersection(_.map(_.filter(arr, function(x) { return hasOwnProperty(x, keyField); - }), function (x) { return x[keyField];})); + }), function (x) { return x[keyField];})).map(function(x) { + if (isObjectDeep(x)) { + if (parentFieldType) { + return x[parentFieldType.primaryKey]; + } + throw new Error('The parent item is an object but its type cannot determined.'); + } + return x; + }).filter(function(x) { + return x != null; + }); if (values.length===0) { return resolve(); } @@ -872,11 +895,22 @@ class DataMappingOptimizedExtender extends DataMappingExtender { return reject('The specified field cannot be found on parent model'); } var keyField = parentField.property || parentField.name; + var parentFieldType = thisQueryable.model.context.model(parentField.type); var values = _.intersection(_.map(_.filter(arr, function(x) { return hasOwnProperty(x, keyField); }), function (x) { return x[keyField]; - })); + })).map(function(x) { + if (isObjectDeep(x)) { + if (parentFieldType) { + return x[parentFieldType.primaryKey]; + } + throw new Error('The parent item is an object but its type cannot determined.'); + } + return x; + }).filter(function(x) { + return x != null; + });; if (values.length===0) { return resolve(); } diff --git a/package-lock.json b/package-lock.json index c56d9de..f8fd14a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@themost/data", - "version": "2.6.78", + "version": "2.6.79", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@themost/data", - "version": "2.6.78", + "version": "2.6.79", "license": "BSD-3-Clause", "dependencies": { "@themost/events": "^1.3.0", diff --git a/package.json b/package.json index c6cdfc7..d3d1f36 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@themost/data", - "version": "2.6.78", + "version": "2.6.79", "description": "MOST Web Framework Codename Blueshift - Data module", "main": "index.js", "types": "index.d.ts",