Skip to content

Commit

Permalink
feat: check scopes on services (#700)
Browse files Browse the repository at this point in the history
  • Loading branch information
romain-gilliotte authored and Romain Gilliotte committed May 31, 2021
1 parent 9ac641e commit 67510dc
Show file tree
Hide file tree
Showing 16 changed files with 1,142 additions and 1,042 deletions.
4 changes: 2 additions & 2 deletions src/services/has-many-getter.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ import PrimaryKeysManager from './primary-keys-manager';
import ResourcesGetter from './resources-getter';

class HasManyGetter extends ResourcesGetter {
constructor(model, association, lianaOptions, params) {
super(association, lianaOptions, params);
constructor(model, association, lianaOptions, params, user) {
super(association, lianaOptions, params, user);

this._parentModel = model.unscoped();
}
Expand Down
92 changes: 55 additions & 37 deletions src/services/leaderboard-stat-getter.js
Original file line number Diff line number Diff line change
@@ -1,77 +1,95 @@
import _ from 'lodash';
import { Schemas } from 'forest-express';
import { Schemas, scopeManager } from 'forest-express';
import Orm from '../utils/orm';
import Operators from '../utils/operators';
import QueryUtils from '../utils/query';
import { InvalidParameterError } from './errors';
import QueryOptions from './query-options';

function getAggregateField({
aggregateField, schemaRelationship, modelRelationship,
aggregateField, parentSchema, parentModel,
}) {
// NOTICE: As MySQL cannot support COUNT(table_name.*) syntax, fieldName cannot be '*'.
const fieldName = aggregateField
|| schemaRelationship.primaryKeys[0]
|| schemaRelationship.fields[0].field;
return `${modelRelationship.name}.${Orm.getColumnName(schemaRelationship, fieldName)}`;
|| parentSchema.primaryKeys[0]
|| parentSchema.fields[0].field;
return `${parentModel.name}.${Orm.getColumnName(parentSchema, fieldName)}`;
}

async function getSequelizeOptionsForModel(model, user, timezone) {
const queryOptions = new QueryOptions(model);
const scopeFilters = await scopeManager.getScopeForUser(user, model.name, true);
await queryOptions.filterByConditionTree(scopeFilters, timezone);
return queryOptions.sequelizeOptions;
}

/**
* @param {import('sequelize').Model} model
* @param {import('sequelize').Model} modelRelationship
* @param {import('sequelize').Model} childModel
* @param {import('sequelize').Model} parentModel
* @param {{
* label_field: string;
* aggregate: string;
* aggregate_field: string;
* }} params
*/
function LeaderboardStatGetter(model, modelRelationship, params) {
function LeaderboardStatGetter(childModel, parentModel, params, user) {
const labelField = params.label_field;
const aggregate = params.aggregate.toUpperCase();
const { limit } = params;
const schema = Schemas.schemas[model.name];
const schemaRelationship = Schemas.schemas[modelRelationship.name];
let associationAs = schema.name;
const childSchema = Schemas.schemas[childModel.name];
const parentSchema = Schemas.schemas[parentModel.name];
let associationAs = childSchema.name;
const associationFound = _.find(
modelRelationship.associations,
(association) => association.target.name === model.name,
parentModel.associations,
(association) => association.target.name === childModel.name,
);

const aggregateField = getAggregateField({
aggregateField: params.aggregate_field,
schemaRelationship,
modelRelationship,
parentSchema,
parentModel,
});

if (!associationFound) {
throw new InvalidParameterError(`Association ${model.name} not found`);
throw new InvalidParameterError(`Association ${childModel.name} not found`);
}

if (associationFound.as) {
associationAs = associationFound.as;
}

const labelColumn = Orm.getColumnName(schema, labelField);
const labelColumn = Orm.getColumnName(childSchema, labelField);
const groupBy = `${associationAs}.${labelColumn}`;

this.perform = async () => {
const records = await modelRelationship
.unscoped()
.findAll({
attributes: [
[model.sequelize.col(groupBy), 'key'],
[model.sequelize.fn(aggregate, model.sequelize.col(aggregateField)), 'value'],
],
includeIgnoreAttributes: false,
include: [{
model,
attributes: [labelField],
as: associationAs,
required: true,
}],
subQuery: false,
group: groupBy,
order: [[model.sequelize.literal('value'), 'DESC']],
limit,
raw: true,
});
const { timezone } = params;
const operators = Operators.getInstance({ Sequelize: parentModel.sequelize.constructor });
const parentSequelizeOptions = await getSequelizeOptionsForModel(parentModel, user, timezone);
const childSequelizeOptions = await getSequelizeOptionsForModel(childModel, user, timezone);

const queryOptions = QueryUtils.bubbleWheresInPlace(operators, {
attributes: [
[childModel.sequelize.col(groupBy), 'key'],
[childModel.sequelize.fn(aggregate, childModel.sequelize.col(aggregateField)), 'value'],
],
where: parentSequelizeOptions.where,
includeIgnoreAttributes: false,
include: [{
model: childModel,
attributes: [labelField],
as: associationAs,
required: true,
where: childSequelizeOptions.where,
include: childSequelizeOptions.include || [],
}, ...(parentSequelizeOptions.include || [])],
subQuery: false,
group: groupBy,
order: [[childModel.sequelize.literal('value'), 'DESC']],
limit,
raw: true,
});

const records = await parentModel.findAll(queryOptions);

return {
value: records.map((data) => ({
Expand Down
7 changes: 5 additions & 2 deletions src/services/line-stat-getter.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { Schemas } from 'forest-express';
import { Schemas, scopeManager } from 'forest-express';
import _ from 'lodash';
import moment from 'moment';
import { isMSSQL, isMySQL, isSQLite } from '../utils/database';
import Orm from '../utils/orm';
import QueryOptions from './query-options';

function LineStatGetter(model, params, options) {
function LineStatGetter(model, params, options, user) {
const schema = Schemas.schemas[model.name];
const timeRange = params.time_range.toLowerCase();

Expand Down Expand Up @@ -198,8 +198,11 @@ ${groupByDateFieldFormated}), 'yyyy-MM-dd 00:00:00')`);

this.perform = async () => {
const { filters, timezone } = params;
const scopeFilters = await scopeManager.getScopeForUser(user, model.name, true);

const queryOptions = new QueryOptions(model, { includeRelations: true });
await queryOptions.filterByConditionTree(filters, timezone);
await queryOptions.filterByConditionTree(scopeFilters, timezone);

const sequelizeOptions = {
...queryOptions.sequelizeOptions,
Expand Down
7 changes: 5 additions & 2 deletions src/services/pie-stat-getter.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Schemas } from 'forest-express';
import { Schemas, scopeManager } from 'forest-express';
import _ from 'lodash';
import moment from 'moment';
import { isMSSQL } from '../utils/database';
Expand All @@ -9,7 +9,7 @@ import QueryOptions from './query-options';
const ALIAS_GROUP_BY = 'forest_alias_groupby';
const ALIAS_AGGREGATE = 'forest_alias_aggregate';

function PieStatGetter(model, params, options) {
function PieStatGetter(model, params, options, user) {
const needsDateOnlyFormating = isVersionLessThan4(options.Sequelize);

const schema = Schemas.schemas[model.name];
Expand Down Expand Up @@ -82,8 +82,11 @@ function PieStatGetter(model, params, options) {

this.perform = async () => {
const { filters, timezone } = params;
const scopeFilters = await scopeManager.getScopeForUser(user, model.name, true);

const queryOptions = new QueryOptions(model, { includeRelations: true });
await queryOptions.filterByConditionTree(filters, timezone);
await queryOptions.filterByConditionTree(scopeFilters, timezone);

const sequelizeOptions = {
...queryOptions.sequelizeOptions,
Expand Down
20 changes: 12 additions & 8 deletions src/services/resource-creator.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,16 @@ const associationRecord = require('../utils/association-record');
const isPrimaryKeyAForeignKey = require('../utils/is-primary-key-a-foreign-key');

class ResourceCreator {
constructor(model, params) {
constructor(model, params, body, user) {
this.model = model;
this.params = params;
this.body = body;
this.schema = Interface.Schemas.schemas[model.name];
this.user = user;
}

async _getTargetKey(name, association) {
const primaryKey = this.params[name];
const primaryKey = this.body[name];

let targetKey = primaryKey;
if (typeof primaryKey !== 'undefined' && association.targetKey !== 'id') {
Expand All @@ -31,7 +33,7 @@ class ResourceCreator {
const targetKey = await this._getTargetKey(name, association);
const primaryKeyIsAForeignKey = isPrimaryKeyAForeignKey(association);
if (primaryKeyIsAForeignKey) {
record[association.source.primaryKeyAttribute] = this.params[name];
record[association.source.primaryKeyAttribute] = this.body[name];
}
return record[setterName](targetKey, { save: false });
}
Expand All @@ -46,7 +48,7 @@ class ResourceCreator {
setterName = `add${_.upperFirst(name)}`;
}
if (setterName) {
return record[setterName](this.params[name]);
return record[setterName](this.body[name]);
}
return null;
}
Expand All @@ -61,7 +63,7 @@ class ResourceCreator {

async perform() {
// buildInstance
const recordCreated = this.model.build(this.params);
const recordCreated = this.model.build(this.body);

// handleAssociationsBeforeSave
await this._handleSave(recordCreated, this._makePromisesBeforeSave);
Expand All @@ -83,9 +85,11 @@ class ResourceCreator {
new PrimaryKeysManager(this.model).annotateRecords([record]);

// return makeResourceGetter()
return new ResourceGetter(this.model, {
recordId: record[this.schema.idField],
}).perform();
return new ResourceGetter(
this.model,
{ ...this.params, recordId: record[this.schema.idField] },
this.user,
).perform();
}
}

Expand Down
8 changes: 7 additions & 1 deletion src/services/resource-getter.js
Original file line number Diff line number Diff line change
@@ -1,16 +1,22 @@
import { scopeManager } from 'forest-express';
import createError from 'http-errors';
import PrimaryKeysManager from './primary-keys-manager';
import QueryOptions from './query-options';

class ResourceGetter {
constructor(model, params) {
constructor(model, params, user) {
this._model = model.unscoped();
this._params = params;
this._user = user;
}

async perform() {
const { timezone } = this._params;
const scopeFilters = await scopeManager.getScopeForUser(this._user, this._model.name, true);

const queryOptions = new QueryOptions(this._model, { includeRelations: true });
await queryOptions.filterByIds([this._params.recordId]);
await queryOptions.filterByConditionTree(scopeFilters, timezone);

const record = await this._model.findOne(queryOptions.sequelizeOptions);
if (!record) {
Expand Down
10 changes: 3 additions & 7 deletions src/services/resource-remover.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,9 @@ import ResourcesRemover from './resources-remover';
/**
* Kept for retro-compatibility with forest-express.
*/
class ResourceRemover {
constructor(model, params) {
this.remover = new ResourcesRemover(model, [params.recordId]);
}

perform() {
return this.remover.perform();
class ResourceRemover extends ResourcesRemover {
constructor(model, params, user) {
super(model, params, [params.recordId], user);
}
}

Expand Down
14 changes: 12 additions & 2 deletions src/services/resource-updater.js
Original file line number Diff line number Diff line change
@@ -1,17 +1,23 @@
import { scopeManager } from 'forest-express';
import { ErrorHTTP422 } from './errors';
import QueryOptions from './query-options';
import ResourceGetter from './resource-getter';

class ResourceUpdater {
constructor(model, params, newRecord) {
constructor(model, params, newRecord, user) {
this._model = model.unscoped();
this._params = params;
this._newRecord = newRecord;
this._user = user;
}

async perform() {
const { timezone } = this._params;
const scopeFilters = await scopeManager.getScopeForUser(this._user, this._model.name, true);

const queryOptions = new QueryOptions(this._model);
await queryOptions.filterByIds([this._params.recordId]);
await queryOptions.filterByConditionTree(scopeFilters, timezone);

const record = await this._model.findOne(queryOptions.sequelizeOptions);
if (record) {
Expand All @@ -25,7 +31,11 @@ class ResourceUpdater {
}
}

return new ResourceGetter(this._model, { recordId: this._params.recordId }).perform();
return new ResourceGetter(
this._model,
{ ...this.params, recordId: this._params.recordId },
this._user,
).perform();
}
}

Expand Down
6 changes: 3 additions & 3 deletions src/services/resources-exporter.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,16 @@ const HasManyGetter = require('./has-many-getter');
const BATCH_INITIAL_PAGE = 1;
const BATCH_SIZE = 1000;

function ResourcesExporter(model, options, params, association) {
function ResourcesExporter(model, options, params, association, user) {
const primaryKeys = _.keys((association || model).primaryKeys);
params.sort = primaryKeys[0] || 'id';
params.page = { size: BATCH_SIZE };

function getter() {
if (association) {
return new HasManyGetter(model, association, options, params);
return new HasManyGetter(model, association, options, params, user);
}
return new ResourcesGetter(model, options, params);
return new ResourcesGetter(model, options, params, user);
}

function retrieveBatch(dataSender, pageNumber) {
Expand Down
12 changes: 8 additions & 4 deletions src/services/resources-getter.js
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
import { Schemas, scopeManager } from 'forest-express';
import _ from 'lodash';
import { Schemas } from 'forest-express';
import PrimaryKeysManager from './primary-keys-manager';
import QueryOptions from './query-options';
import extractRequestedFields from './requested-fields-extractor';

class ResourcesGetter {
constructor(model, lianaOptions, params) {
// The lianaOptions argument is kept for retrocompatibility w/ forest-express.
constructor(model, lianaOptions, params, user) {
// lianaOptions is kept for compatibility with forest-express-mongoose
this._model = model.unscoped();
this._params = params ?? lianaOptions;
this._params = params;
this._user = user;
}

async perform() {
Expand Down Expand Up @@ -55,10 +56,13 @@ class ResourcesGetter {
} = this._params;

const requestedFields = extractRequestedFields(fields, this._model, Schemas.schemas);
const scopeFilters = await scopeManager.getScopeForUser(this._user, this._model.name, true);

const queryOptions = new QueryOptions(this._model, { tableAlias });
await queryOptions.requireFields(requestedFields);
await queryOptions.search(search, searchExtended);
await queryOptions.filterByConditionTree(filters, timezone);
await queryOptions.filterByConditionTree(scopeFilters, timezone);
await queryOptions.segment(segment);
await queryOptions.segmentQuery(segmentQuery);

Expand Down
Loading

0 comments on commit 67510dc

Please sign in to comment.