From 13c717eab02d36dcafe2039622fed7969144bf81 Mon Sep 17 00:00:00 2001 From: Kyriakos Barbounakis Date: Mon, 31 Aug 2026 13:26:55 +0300 Subject: [PATCH 1/2] get additional join expressions when resolving the results of a method --- data-filter-resolver.d.ts | 15 ++-- data-model.js | 19 +++-- jest.setup.js | 3 +- spec/DataPrivileges.spec.ts | 113 ++++++++++++++++++++++++++++- spec/test2/config/models/User.json | 28 +++++++ spec/test2/db/local.db | Bin 1245184 -> 1249280 bytes 6 files changed, 160 insertions(+), 18 deletions(-) diff --git a/data-filter-resolver.d.ts b/data-filter-resolver.d.ts index 1d0f244f..21b116d3 100644 --- a/data-filter-resolver.d.ts +++ b/data-filter-resolver.d.ts @@ -1,11 +1,10 @@ // MOST Web Framework 2.0 Codename Blueshift BSD-3-Clause license Copyright (c) 2017-2022, THEMOST LP All rights reserved export declare class DataFilterResolver { - resolveMember(member: string, callback: (err?: Error, res?: any) => void); - resolveMethod(name: string, args: Array, callback: (err?: Error, res?: any) => void); - me(callback: (err?: Error, res?: any) => void); - user(callback: (err?: Error, res?: any) => void); - now(callback: (err?: Error, res?: Date) => void); - today(callback: (err?: Error, res?: Date) => void); - lang(callback: (err?: Error, res?: string) => void); - + resolveMember(member: string, callback: (err?: Error, res?: any) => void): void; + resolveMethod(name: string, args: Array, callback: (err?: Error, res?: any) => void): void; + me(callback: (err?: Error, res?: any) => void): void; + user(callback: (err?: Error, res?: any) => void): void; + now(callback: (err?: Error, res?: Date) => void): void; + today(callback: (err?: Error, res?: Date) => void): void; + lang(callback: (err?: Error, res?: string) => void): void; } diff --git a/data-model.js b/data-model.js index 5ba35770..d704ba02 100644 --- a/data-model.js +++ b/data-model.js @@ -1,5 +1,5 @@ // MOST Web Framework 2.0 Codename Blueshift BSD-3-Clause license Copyright (c) 2017-2022, THEMOST LP All rights reserved -// noinspection ES6ConvertVarToLetConst +// noinspection ES6ConvertVarToLetConst,JSUnresolvedReference var _ = require('lodash'); var {cloneDeep} = require('lodash'); @@ -8,7 +8,7 @@ var Symbol = require('symbol'); var path = require('path'); var pluralize = require('pluralize'); var async = require('async'); -var {QueryUtils, Expression} = require('@themost/query'); +var {QueryUtils, Expression, MemberExpression} = require('@themost/query'); var {OpenDataParser} = require('@themost/query'); var types = require('./types'); var {DataAssociationMapping} = require('./types'); @@ -800,10 +800,17 @@ function filterInternal(params, callback) { DataFilterResolver.prototype.resolveMember.call(self, member, cb); }; parser.resolveMethod = function(name, args, cb) { - if (typeof self.resolveMethod === 'function') - self.resolveMethod.call(self, name, args, cb); - else - DataFilterResolver.prototype.resolveMethod.call(self, name, args, cb); + var resolveMethod = (typeof self.resolveMethod === 'function') ? self.resolveMethod : DataFilterResolver.prototype.resolveMethod; + resolveMethod.call(self, name, args, function(err, result) { + if (err) { return cb(err); } + if (result instanceof MemberExpression) { + if (Object.prototype.hasOwnProperty.call(result, '$expand')) { + // handle $expand property + $joinExpressions.push(...result.$expand); + } + } + return cb(null, result); + }); }; var filter; diff --git a/jest.setup.js b/jest.setup.js index cecd238a..67d710bc 100644 --- a/jest.setup.js +++ b/jest.setup.js @@ -1,7 +1,8 @@ const { TraceUtils } = require('@themost/common'); const { JsonLogger } = require('@themost/json-logger'); TraceUtils.useLogger(new JsonLogger({ - format: 'raw' + format: 'raw', + level: 'debug', })); /* env */ process.env.NODE_ENV = 'development'; diff --git a/spec/DataPrivileges.spec.ts b/spec/DataPrivileges.spec.ts index 2e10438c..75608826 100644 --- a/spec/DataPrivileges.spec.ts +++ b/spec/DataPrivileges.spec.ts @@ -1,10 +1,73 @@ -import { resolve } from 'path'; -import { DataContext } from '../index'; -import { TestApplication } from './TestApplication'; +// noinspection JSUnusedLocalSymbols + +import {resolve} from 'path'; +import {DataAssociationMapping, DataContext, DataObjectTag} from '../index'; +import {TestApplication} from './TestApplication'; +import {DataFilterResolver} from '@themost/data'; +import {MemberExpression, QueryEntity, QueryExpression, QueryField} from '@themost/query'; + +declare class DataFilterResolverWithExtensions extends DataFilterResolver { + regions(): Promise; +} describe('Permissions', () => { let app: TestApplication; let context: DataContext; + + function regions(callback: (err?: Error, res?: any) => void) { + return this.context.model('User').asQueryable() + .select('id', 'name') + .where('name').equal(this.context.user.name) + .expand('userRegions').silent().getItem().then((user: any) => { + const values = (user && user.userRegions || []); + return callback(null, values); + }).catch((err: Error) => { + return callback(err); + }); + } + + function userRegions(callback: (err?: Error, res?: any) => void) { + const Users = this.context.model('User'); + const { viewAdapter: UserView } = Users; + const property: DataObjectTag = Users.convert({}).property('userRegions'); + const { viewAdapter: UserRegionView } = property.getBaseModel(); + const mapping: DataAssociationMapping = property.mapping; + void property.migrate((err: Error) => { + if (err) { + return callback(err); + } + const query = new QueryExpression().select( + new QueryField(mapping.associationValueField).from('userRegions') + ).from('Any') + .join(new QueryEntity(UserView).as('userRegions_Users')) + .with( + new QueryExpression().where( + new QueryField('name').from('userRegions_Users') + ).equal( + this.context.user.name + ) + ) + .join(new QueryEntity(UserRegionView).as('userRegions')).with( + new QueryExpression().where( + new QueryField(mapping.associationObjectField).from('userRegions') + ).equal( + new QueryField('id').from('userRegions_Users') + ) + ); + const { $expand } = query; + return callback(null, Object.assign(new MemberExpression(`userRegions.${mapping.associationValueField}`), { + $expand + })); + }); + } + + if (Object.prototype.hasOwnProperty.call(DataFilterResolver.prototype, 'regions') === false) { + Object.assign(DataFilterResolver.prototype, { + regions, + userRegions + }) + } + beforeAll(async () => { app = new TestApplication(resolve(__dirname, 'test2')); context = app.createContext(); @@ -125,4 +188,48 @@ describe('Permissions', () => { }); + it('should get items based on user access', async () => { + context.user = { + name: 'jane.keene@example.com' + }; + const q = await context.model('Person').filterAsync( + { $filter: 'address/addressRegion eq regions()'} + ); + + const user = await context.model('User').asQueryable() + .select('id', 'name') + .where('name').equal(context.user.name) + .expand('userRegions').silent().getItem(); + + expect(q).toBeTruthy(); + const items = await q.getItems(); + expect(items).toBeTruthy(); + expect(items.length).toBeTruthy(); + for (const item of items) { + expect(user.userRegions.some((region: string) => region === item.address.addressRegion)).toBeTruthy(); + } + }); + + it('should get items using queryable method', async () => { + context.user = { + name: 'jane.keene@example.com' + }; + const q = await context.model('Person').filterAsync( + { $filter: 'address/addressRegion eq userRegions()'} + ); + + const user = await context.model('User').asQueryable() + .select('id', 'name') + .where('name').equal(context.user.name) + .expand('userRegions').silent().getItem(); + + expect(q).toBeTruthy(); + const items = await q.getItems(); + expect(items).toBeTruthy(); + expect(items.length).toBeTruthy(); + for (const item of items) { + expect(user.userRegions.some((region: string) => region === item.address.addressRegion)).toBeTruthy(); + } + }); + }); diff --git a/spec/test2/config/models/User.json b/spec/test2/config/models/User.json index 1b3694c4..5d8fa676 100644 --- a/spec/test2/config/models/User.json +++ b/spec/test2/config/models/User.json @@ -137,6 +137,34 @@ "@id": "https://themost.io/schemas/userReviews", "name": "userReviews", "type": "UserReview" + }, + { + "@id": "https://themost.io/schemas/region", + "name": "userRegions", + "type": "Text", + "many": true, + "mapping": { + "associationType": "junction", + "associationAdapter": "UserRegions", + "associationObjectField": "user", + "associationValueField": "region", + "privileges": [ + { + "mask": 15, + "type": "global" + }, + { + "mask": 15, + "type": "global", + "account": "Administrators" + }, + { + "mask": 1, + "type": "self", + "filter": "user eq me()" + } + ] + } } ], "privileges": [ diff --git a/spec/test2/db/local.db b/spec/test2/db/local.db index 7bc9eb035a8cdf213c0be0f13525bf38b20b3b7e..087b8178d9cffdf2e45395656c15a3b70afaa613 100644 GIT binary patch delta 9943 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 delta 4033 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 A761SM From 238fbd880be292fd9c5ed0e619ce375f7c7b364a Mon Sep 17 00:00:00 2001 From: Kyriakos Barbounakis Date: Mon, 31 Aug 2026 16:23:10 +0300 Subject: [PATCH 2/2] avoid duplicate joins --- data-model.js | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/data-model.js b/data-model.js index d704ba02..2074b608 100644 --- a/data-model.js +++ b/data-model.js @@ -806,7 +806,16 @@ function filterInternal(params, callback) { if (result instanceof MemberExpression) { if (Object.prototype.hasOwnProperty.call(result, '$expand')) { // handle $expand property - $joinExpressions.push(...result.$expand); + result.$expand.forEach(function(expand) { + // try to find if the expand expression already exists in $joinExpressions + var joinExpr = $joinExpressions.find(function(x) { + return expand.$entity.$as === x.$entity.$as; + }); + if (joinExpr == null) { + $joinExpressions.push(expand); + } + }); + } } return cb(null, result);