Skip to content

perf: Use aggregation pipeline with $graphLookup for recursive role and auth queries #8563

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 11 commits into
base: alpha
Choose a base branch
from
4 changes: 3 additions & 1 deletion spec/ParseRole.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -188,7 +188,9 @@ describe('Parse Role testing', () => {

// 1 call for the 1st layer of roles
// 1 call for the 2nd layer
expect(getAllRolesSpy.calls.count()).toEqual(2);
expect(getAllRolesSpy.calls.count()).toEqual(
process.env.PARSE_SERVER_TEST_DB === 'postgres' ? 2 : 1
);
done();
})
.catch(() => {
Expand Down
154 changes: 126 additions & 28 deletions src/Auth.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import Deprecator from './Deprecator/Deprecator';
import { logger } from './logger';
import RestQuery from './RestQuery';
import RestWrite from './RestWrite';
import MongoStorageAdapter from './Adapters/Storage/Mongo/MongoStorageAdapter';

// An Auth object tells you who is requesting something and whether
// the master key was used.
Expand Down Expand Up @@ -139,7 +140,6 @@ const getAuthForSessionToken = async function ({
limit: 1,
include: 'user',
};
const RestQuery = require('./RestQuery');
const query = new RestQuery(config, master(config), '_Session', { sessionToken }, restOptions);
results = (await query.execute()).results;
} else {
Expand Down Expand Up @@ -183,7 +183,6 @@ var getAuthForLegacySessionToken = function ({ config, sessionToken, installatio
var restOptions = {
limit: 1,
};
const RestQuery = require('./RestQuery');
var query = new RestQuery(config, master(config), '_User', { sessionToken }, restOptions);
return query.execute().then(response => {
var results = response.results;
Expand Down Expand Up @@ -221,17 +220,113 @@ Auth.prototype.getRolesForUser = async function () {
//Stack all Parse.Role
const results = [];
if (this.config) {
const restWhere = {
users: {
__type: 'Pointer',
className: '_User',
objectId: this.user.id,
},
};
const RestQuery = require('./RestQuery');
await new RestQuery(this.config, master(this.config), '_Role', restWhere, {}).each(result =>
results.push(result)
);
if (this.config.database.adapter instanceof MongoStorageAdapter) {
const prefix = this.config.databaseAdapter._collectionPrefix || '';
const result = await new RestQuery(
this.config,
master(this.config),
'_Join:users:_Role',
{},
{
pipeline: [
{
$match: {
relatedId: this.user.id,
},
},
{
$graphLookup: {
from: `${prefix}_Join:roles:_Role`,
startWith: '$owningId',
connectFromField: 'owningId',
connectToField: 'relatedId',
as: 'childRolePath',
},
},
{
$facet: {
directRoles: [
{
$lookup: {
from: `${prefix}_Role`,
localField: 'owningId',
foreignField: '_id',
as: 'Roles',
},
},
{
$unwind: {
path: '$Roles',
},
},
{
$replaceRoot: {
newRoot: {
$ifNull: ['$Roles', { $literal: {} }],
},
},
},
{
$project: {
name: 1,
},
},
],
childRoles: [
{
$lookup: {
from: `${prefix}_Role`,
localField: 'childRolePath.owningId',
foreignField: '_id',
as: 'Roles',
},
},
{
$unwind: {
path: '$Roles',
},
},
{
$replaceRoot: {
newRoot: {
$ifNull: ['$Roles', { $literal: {} }],
},
},
},
{
$project: {
name: 1,
},
},
],
},
},
],
}
).execute();
const { directRoles, childRoles } = result.results[0] || {
directRoles: [],
childRoles: [],
};
const roles = [...directRoles, ...childRoles];
for (const role of roles) {
const roleName = `role:${role.name}`;
if (!results.includes(roleName)) {
results.push(role);
}
}
} else {
const restWhere = {
users: {
__type: 'Pointer',
className: '_User',
objectId: this.user.id,
},
};
await new RestQuery(this.config, master(this.config), '_Role', restWhere, {}).each(result =>
results.push(result)
);
}
} else {
await new Parse.Query(Parse.Role)
.equalTo('users', this.user)
Expand All @@ -257,25 +352,29 @@ Auth.prototype._loadRoles = async function () {
this.userRoles = [];
this.fetchedRoles = true;
this.rolePromise = null;

this.cacheRoles();
return this.userRoles;
}

const rolesMap = results.reduce(
(m, r) => {
m.names.push(r.name);
m.ids.push(r.objectId);
return m;
},
{ ids: [], names: [] }
);
if (typeof results[0] === 'object') {
const rolesMap = results.reduce(
(m, r) => {
m.names.push(r.name);
m.ids.push(r.objectId);
return m;
},
{ ids: [], names: [] }
);

// run the recursive finding
const roleNames = await this._getAllRolesNamesForRoleIds(rolesMap.ids, rolesMap.names);
this.userRoles = roleNames.map(r => {
return 'role:' + r;
});
} else {
this.userRoles = results;
}

// run the recursive finding
const roleNames = await this._getAllRolesNamesForRoleIds(rolesMap.ids, rolesMap.names);
this.userRoles = roleNames.map(r => {
return 'role:' + r;
});
this.fetchedRoles = true;
this.rolePromise = null;
this.cacheRoles();
Expand Down Expand Up @@ -322,7 +421,6 @@ Auth.prototype.getRolesByIds = async function (ins) {
};
});
const restWhere = { roles: { $in: roles } };
const RestQuery = require('./RestQuery');
await new RestQuery(this.config, master(this.config), '_Role', restWhere, {}).each(result =>
results.push(result)
);
Expand Down
2 changes: 1 addition & 1 deletion src/Controllers/DatabaseController.js
Original file line number Diff line number Diff line change
Expand Up @@ -1293,7 +1293,7 @@ class DatabaseController {
return this.adapter.distinct(className, schema, query, distinct);
}
} else if (pipeline) {
if (!classExists) {
if (!classExists && className.slice(0, 5) !== '_Join') {
return [];
} else {
return this.adapter.aggregate(
Expand Down