Skip to content

Commit 515fb70

Browse files
authored
feat: add filters support on related data (#658)
1 parent 4691fff commit 515fb70

File tree

2 files changed

+189
-44
lines changed

2 files changed

+189
-44
lines changed

src/services/has-many-getter.js

Lines changed: 83 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -4,27 +4,58 @@ const Interface = require('forest-express');
44
const orm = require('../utils/orm');
55
const QueryBuilder = require('./query-builder');
66
const SearchBuilder = require('./search-builder');
7+
const FiltersParser = require('./filters-parser');
78
const CompositeKeysManager = require('./composite-keys-manager');
89
const extractRequestedFields = require('./requested-fields-extractor');
10+
const Operators = require('../utils/operators');
911

10-
function HasManyGetter(model, association, opts, params) {
11-
const queryBuilder = new QueryBuilder(model, opts, params);
12-
const schema = Interface.Schemas.schemas[association.name];
13-
const primaryKeyModel = _.keys(model.primaryKeys)[0];
14-
15-
function getFieldNamesRequested() {
16-
return extractRequestedFields(params.fields, association, Interface.Schemas.schemas);
12+
class HasManyGetter {
13+
constructor(model, association, options, params) {
14+
this.model = model;
15+
this.association = association;
16+
this.params = params;
17+
this.queryBuilder = new QueryBuilder(model, options, params);
18+
this.schema = Interface.Schemas.schemas[association.name];
19+
[this.primaryKeyModel] = _.keys(model.primaryKeys);
20+
this.operators = Operators.getInstance(options);
21+
this.filtersParser = new FiltersParser(this.schema, params.timezone, options);
22+
this.fieldNamesRequested = extractRequestedFields(
23+
params.fields, association, Interface.Schemas.schemas,
24+
);
25+
this.searchBuilder = new SearchBuilder(
26+
association,
27+
options,
28+
params,
29+
this.fieldNamesRequested,
30+
);
1731
}
1832

19-
const fieldNamesRequested = getFieldNamesRequested();
20-
const searchBuilder = new SearchBuilder(association, opts, params, fieldNamesRequested);
21-
const where = searchBuilder.perform(params.associationName);
22-
const include = queryBuilder.getIncludes(association, fieldNamesRequested);
33+
async buildWhereConditions({ associationName, search, filters }) {
34+
const { AND } = this.operators;
35+
const where = { [AND]: [] };
36+
37+
if (search) {
38+
const searchCondition = this.searchBuilder.perform(associationName);
39+
where[AND].push(searchCondition);
40+
}
41+
42+
43+
if (filters) {
44+
const formattedFilters = await this.filtersParser.perform(filters);
45+
where[AND].push(formattedFilters);
46+
}
2347

24-
function findQuery(queryOptions) {
48+
return where;
49+
}
50+
51+
async findQuery(queryOptions) {
2552
if (!queryOptions) { queryOptions = {}; }
53+
const { associationName, recordId } = this.params;
54+
55+
const where = await this.buildWhereConditions(this.params);
56+
const include = this.queryBuilder.getIncludes(this.association, this.fieldNamesRequested);
2657

27-
return orm.findRecord(model, params.recordId, {
58+
const record = await orm.findRecord(this.model, recordId, {
2859
order: queryOptions.order,
2960
subQuery: false,
3061
offset: queryOptions.offset,
@@ -35,61 +66,69 @@ function HasManyGetter(model, association, opts, params) {
3566
// and we don't need the parent's attributes
3667
attributes: [],
3768
include: [{
38-
model: association,
39-
as: params.associationName,
69+
model: this.association,
70+
as: associationName,
4071
scope: false,
4172
required: false,
4273
where,
4374
include,
4475
}],
45-
})
46-
.then((record) => ((record && record[params.associationName]) || []));
76+
});
77+
78+
return (record && record[associationName]) || [];
4779
}
4880

49-
function getCount() {
50-
return model.count({
51-
where: { [primaryKeyModel]: params.recordId },
81+
async count() {
82+
const { associationName, recordId } = this.params;
83+
const where = await this.buildWhereConditions(this.params);
84+
const include = this.queryBuilder.getIncludes(this.association, this.fieldNamesRequested);
85+
86+
return this.model.count({
87+
where: { [this.primaryKeyModel]: recordId },
5288
include: [{
53-
model: association,
54-
as: params.associationName,
89+
model: this.association,
90+
as: associationName,
5591
where,
5692
required: true,
5793
scope: false,
94+
include,
5895
}],
5996
});
6097
}
6198

62-
function getRecords() {
99+
async getRecords() {
100+
const { associationName } = this.params;
101+
63102
const queryOptions = {
64-
order: queryBuilder.getOrder(params.associationName, schema),
65-
offset: queryBuilder.getSkip(),
66-
limit: queryBuilder.getLimit(),
103+
order: this.queryBuilder.getOrder(associationName, this.schema),
104+
offset: this.queryBuilder.getSkip(),
105+
limit: this.queryBuilder.getLimit(),
67106
};
68107

69-
return findQuery(queryOptions)
70-
.then((records) => P.map(records, (record) => {
71-
if (schema.isCompositePrimary) {
72-
record.forestCompositePrimary = new CompositeKeysManager(association, schema, record)
73-
.createCompositePrimary();
74-
}
108+
const records = await this.findQuery(queryOptions);
109+
return P.map(records, (record) => {
110+
if (this.schema.isCompositePrimary) {
111+
record.forestCompositePrimary = new CompositeKeysManager(
112+
this.association, this.schema, record,
113+
)
114+
.createCompositePrimary();
115+
}
75116

76-
return record;
77-
}));
117+
return record;
118+
});
78119
}
79120

80-
this.perform = () =>
81-
getRecords()
82-
.then((records) => {
83-
let fieldsSearched = null;
121+
async perform() {
122+
const records = await this.getRecords();
84123

85-
if (params.search) {
86-
fieldsSearched = searchBuilder.getFieldsSearched();
87-
}
124+
let fieldsSearched = null;
88125

89-
return [records, fieldsSearched];
90-
});
126+
if (this.params.search) {
127+
fieldsSearched = this.searchBuilder.getFieldsSearched();
128+
}
91129

92-
this.count = getCount;
130+
return [records, fieldsSearched];
131+
}
93132
}
94133

95134
module.exports = HasManyGetter;
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
import Sequelize from 'sequelize';
2+
import Interface from 'forest-express';
3+
import HasManyGetter from '../../src/services/has-many-getter';
4+
import Operators from '../../src/utils/operators';
5+
import { sequelizePostgres } from '../databases';
6+
7+
describe('services > HasManyGetter', () => {
8+
const sequelizeOptions = {
9+
sequelize: Sequelize,
10+
Sequelize,
11+
connections: { sequelize: sequelizePostgres.createConnection() },
12+
};
13+
const { AND, OR, GT } = Operators.getInstance(sequelizeOptions);
14+
const timezone = 'Europe/Paris';
15+
16+
describe('buildWhereConditions', () => {
17+
const associationName = 'users';
18+
const model = { name: 'cars' };
19+
const association = {
20+
name: 'users',
21+
rawAttributes: [{ field: 'name', type: 'String' }],
22+
sequelize: Sequelize,
23+
};
24+
Interface.Schemas = {
25+
schemas: {
26+
users: { fields: [{ field: 'name', type: 'String', columnName: 'name' }] },
27+
cars: { fields: [{ field: 'type' }] },
28+
},
29+
};
30+
31+
describe('with no filters and search in params', () => {
32+
it('should build an empty where condition', async () => {
33+
expect.assertions(1);
34+
35+
const hasManyGetter = new HasManyGetter(model, association, sequelizeOptions, { timezone });
36+
const whereConditions = await hasManyGetter.buildWhereConditions({ });
37+
expect(whereConditions).toStrictEqual({ [AND]: [] });
38+
});
39+
});
40+
41+
describe('with filters in params', () => {
42+
it('should build a where condition containing the provided filters formatted', async () => {
43+
expect.assertions(1);
44+
const filters = '{ "field": "id", "operator": "greater_than", "value": 1 }';
45+
46+
const hasManyGetter = new HasManyGetter(
47+
model, association, sequelizeOptions, { filters, timezone },
48+
);
49+
const whereConditions = await hasManyGetter.buildWhereConditions({ filters });
50+
expect(whereConditions).toStrictEqual({ [AND]: [{ id: { [GT]: 1 } }] });
51+
});
52+
});
53+
54+
describe('with search in params', () => {
55+
it('should build a where condition containing the provided search', async () => {
56+
expect.assertions(1);
57+
const search = 'test';
58+
59+
const hasManyGetter = new HasManyGetter(
60+
model, association, sequelizeOptions, { search, timezone },
61+
);
62+
const whereConditions = await hasManyGetter
63+
.buildWhereConditions({ search, associationName });
64+
65+
expect(whereConditions).toStrictEqual({
66+
[AND]: [{
67+
[OR]: expect.arrayContaining([
68+
expect.objectContaining({
69+
attribute: { args: [{ col: 'users.name' }], fn: 'lower' },
70+
comparator: ' LIKE ',
71+
logic: { args: ['%test%'], fn: 'lower' },
72+
}),
73+
]),
74+
}],
75+
});
76+
});
77+
});
78+
79+
describe('with filters and search in params', () => {
80+
it('should build a where condition containing the provided filters and search', async () => {
81+
expect.assertions(1);
82+
const filters = '{ "field": "id", "operator": "greater_than", "value": 1 }';
83+
const search = 'test';
84+
85+
const hasManyGetter = new HasManyGetter(
86+
model, association, sequelizeOptions, { filters, search, timezone },
87+
);
88+
const whereConditions = await hasManyGetter
89+
.buildWhereConditions({ search, filters, associationName });
90+
expect(whereConditions).toStrictEqual({
91+
[AND]: [{
92+
[OR]: expect.arrayContaining([
93+
expect.objectContaining({
94+
attribute: { args: [{ col: 'users.name' }], fn: 'lower' },
95+
comparator: ' LIKE ',
96+
logic: { args: ['%test%'], fn: 'lower' },
97+
}),
98+
]),
99+
}, {
100+
id: { [GT]: 1 },
101+
}],
102+
});
103+
});
104+
});
105+
});
106+
});

0 commit comments

Comments
 (0)