diff --git a/data-filter-resolver.d.ts b/data-filter-resolver.d.ts index 1d0f244..21b116d 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 5ba3577..2074b60 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,26 @@ 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 + 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); + }); }; var filter; diff --git a/jest.setup.js b/jest.setup.js index cecd238..67d710b 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 2e10438..7560882 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 1b3694c..5d8fa67 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 7bc9eb0..087b817 100644 Binary files a/spec/test2/db/local.db and b/spec/test2/db/local.db differ