Skip to content

Commit e5339a3

Browse files
committed
Merge branch '2.0.x' into 2.1.x
2 parents 3980e22 + e2fd9e8 commit e5339a3

File tree

10 files changed

+229
-78
lines changed

10 files changed

+229
-78
lines changed

CHANGELOG/CHANGELOG_beta.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
# [2.0.0-beta.11](https://github.com/ReliefApplications/oort-backend/compare/v2.0.0-beta.10...v2.0.0-beta.11) (2023-05-15)
2+
3+
4+
### Bug Fixes
5+
6+
* problems regarding calculated fields ([0c457eb](https://github.com/ReliefApplications/oort-backend/commit/0c457eb0b94ee3730e96602b0b09e4df2fbec78e))
7+
8+
9+
### Features
10+
11+
* add manage distribution list permission migration ([03b14c0](https://github.com/ReliefApplications/oort-backend/commit/03b14c0f9309596fc7fec79ca14f3b789250250f))
12+
13+
# [2.0.0-beta.10](https://github.com/ReliefApplications/oort-backend/compare/v2.0.0-beta.9...v2.0.0-beta.10) (2023-05-15)
14+
15+
16+
### Features
17+
18+
* use column width to generate email table ([a1f9c81](https://github.com/ReliefApplications/oort-backend/commit/a1f9c81c366da1d2586b13910b3015827884e63b))

migrations/1663832564479-generate-aggregations.ts

Lines changed: 111 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,43 @@
11
import { startDatabaseForMigration } from '../src/utils/migrations/database.helper';
22
import get from 'lodash/get';
33
import set from 'lodash/set';
4-
import { contentType } from '@const/enumTypes';
5-
import { Application, Dashboard, Page, Aggregation, Form } from '../src/models';
4+
import {
5+
Application,
6+
Dashboard,
7+
Aggregation,
8+
Form,
9+
Step,
10+
Page,
11+
Workflow,
12+
} from '../src/models';
613
import { logger } from '../src/services/logger.service';
714

15+
/**
16+
* Get parent application from dashboard. Including dashboard in step.
17+
*
18+
* @param dashboardId Id of the dashboard.
19+
* @param applications List of populated applications.
20+
* @param populatedApplications List of fully populated applications.
21+
* @returns Parent application.
22+
*/
23+
const getApplication = (
24+
dashboardId: any,
25+
applications: Application[],
26+
populatedApplications: Application[]
27+
): Application => {
28+
const application = applications.find((app) =>
29+
app.pages.some((page) => (page as Page)?.content?.equals(dashboardId))
30+
);
31+
if (!!application) return application;
32+
return populatedApplications.find((app) =>
33+
app.pages.some((page) =>
34+
((page as Page)?.content as Workflow)?.steps?.some((step) =>
35+
(step as Step)?.content?.equals(dashboardId)
36+
)
37+
)
38+
);
39+
};
40+
841
/**
942
* Use to aggregations migrate up.
1043
*
@@ -13,89 +46,99 @@ import { logger } from '../src/services/logger.service';
1346
export const up = async () => {
1447
await startDatabaseForMigration();
1548
try {
49+
const dashboards = await Dashboard.find({});
1650
const applications = await Application.find()
1751
.populate({
1852
path: 'pages',
1953
model: 'Page',
2054
})
2155
.select('name pages');
22-
23-
for (const application of applications) {
24-
if (application.pages.length > 0) {
25-
// Update dashboard pages
26-
const dashboards = await Dashboard.find({
27-
_id: {
28-
$in: application.pages
29-
.filter((x: Page) => x.type === contentType.dashboard)
30-
.map((x: any) => x.content),
56+
const populatedApplications = await Application.find()
57+
.populate({
58+
path: 'pages',
59+
model: 'Page',
60+
populate: {
61+
path: 'content',
62+
model: 'Workflow',
63+
populate: {
64+
path: 'steps',
65+
model: 'Step',
3166
},
32-
});
33-
for (const dashboard of dashboards) {
34-
if (!!dashboard.structure) {
35-
let index = 0;
36-
for (const widget of dashboard.structure) {
37-
if (
38-
widget &&
39-
widget.component == 'chart' &&
40-
get(widget, 'settings.chart.aggregation', null) &&
41-
!get(widget, 'settings.chart.aggregationId', null)
42-
) {
43-
if (widget.settings?.chart.aggregation.dataSource) {
44-
const aggregation: Aggregation = get(
45-
widget,
46-
'settings.chart.aggregation',
47-
null
48-
);
49-
aggregation.name = `${widget.settings.title} - ${application.name}`;
50-
const dataSourceId = get(
51-
widget,
52-
'settings.chart.aggregation.dataSource',
53-
null
54-
);
67+
},
68+
})
69+
.select('name pages');
70+
for (const dashboard of dashboards) {
71+
if (!!dashboard.structure) {
72+
let index = 0;
73+
for (const widget of dashboard.structure) {
74+
if (
75+
widget &&
76+
widget.component == 'chart' &&
77+
get(widget, 'settings.chart.aggregation', null) &&
78+
!get(widget, 'settings.chart.aggregationId', null)
79+
) {
80+
if (widget.settings?.chart.aggregation.dataSource) {
81+
const aggregation: Aggregation = get(
82+
widget,
83+
'settings.chart.aggregation',
84+
null
85+
);
86+
const application = await getApplication(
87+
dashboard._id,
88+
applications,
89+
populatedApplications
90+
);
91+
aggregation.name = `${widget.settings.title} - ${application?.name}`;
92+
const dataSourceId = get(
93+
widget,
94+
'settings.chart.aggregation.dataSource',
95+
null
96+
);
5597

56-
if (dataSourceId) {
57-
//get form and resource
58-
const form = await Form.findById(dataSourceId).populate(
59-
'resource'
60-
);
98+
if (dataSourceId) {
99+
//get form and resource
100+
const form = await Form.findById(dataSourceId).populate(
101+
'resource'
102+
);
61103

62-
if (form) {
63-
form.resource.aggregations.push(aggregation);
104+
if (form) {
105+
form.resource.aggregations.push(aggregation);
64106

65-
//save aggregation object in the resource
66-
const resource = await form.resource.save();
107+
//save aggregation object in the resource
108+
const resource = await form.resource.save();
109+
logger.info(
110+
`Add Aggregation ${aggregation.name} to Resource ${resource.name}`
111+
);
67112

68-
set(widget, 'settings.resource', resource.id);
69-
set(
70-
widget,
71-
'settings.chart.aggregationId',
72-
resource.aggregations.pop().id
73-
);
74-
set(
75-
widget,
76-
'settings.chart.mapping',
77-
get(widget, 'settings.chart.aggregation.mapping', null)
78-
);
113+
set(widget, 'settings.resource', resource.id);
114+
set(
115+
widget,
116+
'settings.chart.aggregationId',
117+
resource.aggregations.pop().id
118+
);
119+
set(
120+
widget,
121+
'settings.chart.mapping',
122+
get(widget, 'settings.chart.aggregation.mapping', null)
123+
);
79124

80-
dashboard.structure[index] = widget;
125+
dashboard.structure[index] = widget;
81126

82-
//add aggregation id in the dashboard documents
83-
await Dashboard.findByIdAndUpdate(
84-
dashboard._id,
85-
{
86-
structure: dashboard.structure,
87-
},
88-
{ new: true }
89-
);
90-
} else {
91-
logger.info('skip: related resource / form not found');
92-
}
93-
}
127+
//add aggregation id in the dashboard documents
128+
await Dashboard.findByIdAndUpdate(
129+
dashboard._id,
130+
{
131+
structure: dashboard.structure,
132+
},
133+
{ new: true }
134+
);
135+
} else {
136+
logger.info('skip: related resource / form not found');
94137
}
95138
}
96-
index++;
97139
}
98140
}
141+
index++;
99142
}
100143
}
101144
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import { Permission } from '@models/permission.model';
2+
import { logger } from '@services/logger.service';
3+
import { startDatabaseForMigration } from '../src/utils/migrations/database.helper';
4+
5+
/**
6+
* Sample function of up migration
7+
*
8+
* @returns just migrate data.
9+
*/
10+
export const up = async () => {
11+
await startDatabaseForMigration();
12+
const types = ['can_manage_distribution_lists', 'can_manage_templates'];
13+
14+
for (const type in types) {
15+
// check if permission already exists
16+
const permissionExists = await Permission.exists({ type, global: false });
17+
if (permissionExists) {
18+
logger.info(`${type} permission already exists`);
19+
return;
20+
}
21+
22+
const permission = new Permission({
23+
type,
24+
global: false,
25+
});
26+
await permission.save();
27+
logger.info(`${type} application's permission created`);
28+
}
29+
};
30+
31+
/**
32+
* Sample function of down migration
33+
*
34+
* @returns just migrate data.
35+
*/
36+
export const down = async () => {
37+
/*
38+
Code you downgrade script here!
39+
*/
40+
};

src/i18n/en.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -220,6 +220,7 @@
220220
"resource": {
221221
"edit": {
222222
"errors": {
223+
"calculatedFieldTooLong": "The expression is too big.",
223224
"field": {
224225
"missingReadPermissionOnResource": "Please provide permission to read or create records before setting visible fields.",
225226
"missingWritePermissionOnResource": "Please provide permission to update or create records before setting editable fields.",

src/i18n/test.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -220,6 +220,7 @@
220220
"resource": {
221221
"edit": {
222222
"errors": {
223+
"calculatedFieldTooLong": "******",
223224
"field": {
224225
"missingReadPermissionOnResource": "******",
225226
"missingWritePermissionOnResource": "******",

src/routes/email/index.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,13 @@ const generateEmail = async (req, res) => {
3939
if (args.attachment || args.body.includes(Placeholder.DATASET)) {
4040
await extractGridData(args, req.headers.authorization)
4141
.then((x) => {
42-
columns = x.columns;
42+
columns = x.columns.map((column: any) => {
43+
const field = args.fields.find((y: any) => y.name === column.name);
44+
if (field && field.width) {
45+
column.width = field.width;
46+
}
47+
return column;
48+
});
4349
rows = x.rows;
4450
})
4551
.catch((err) => logger.error(err.message, { stack: err.stack }));

src/schema/mutation/editResource.mutation.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { buildTypes } from '@utils/schema';
1212
import { AppAbility } from '@security/defineUserAbility';
1313
import { get, has, isArray, isEqual } from 'lodash';
1414
import { logger } from '@services/logger.service';
15+
import buildCalculatedFieldPipeline from '@utils/aggregation/buildCalculatedFieldPipeline';
1516

1617
/** Simple resource permission change type */
1718
type SimplePermissionChange =
@@ -684,6 +685,22 @@ export default {
684685
// Update calculated fields
685686
if (args.calculatedField) {
686687
const calculatedField: CalculatedFieldChange = args.calculatedField;
688+
689+
// Check if calculated field expression is too long
690+
if (calculatedField.add || calculatedField.update) {
691+
const expression =
692+
calculatedField.add?.expression ??
693+
calculatedField.update?.expression;
694+
const pipeline = buildCalculatedFieldPipeline(expression, '');
695+
if (pipeline[0].$facet.calcFieldFacet.length > 50) {
696+
throw new GraphQLError(
697+
context.i18next.t(
698+
'mutations.resource.edit.errors.calculatedFieldTooLong'
699+
)
700+
);
701+
}
702+
}
703+
687704
// Add new calculated field
688705
if (calculatedField.add) {
689706
const expression = getExpressionFromString(
@@ -776,6 +793,7 @@ export default {
776793
);
777794
} catch (err) {
778795
logger.error(err.message, { stack: err.stack });
796+
if (err instanceof GraphQLError) throw err;
779797
throw new GraphQLError(
780798
context.i18next.t('common.errors.internalServerError')
781799
);

src/utils/aggregation/buildCalculatedFieldPipeline.ts

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,7 @@ const resolveSingleOperator = (
125125

126126
const getValueString = () => {
127127
const value = getSimpleOperatorValue(operator);
128-
if (value) return value;
128+
if (!isNil(value)) return value; // check that not null or undefined, so 0 works
129129

130130
// if is an expression, add to dependencies array,
131131
// that will be resolved before, since will be appended
@@ -188,7 +188,7 @@ const resolveDoubleOperator = (
188188
const getValueString = (i: number) => {
189189
const selectedOperator = i === 1 ? operator1 : operator2;
190190
const value = getSimpleOperatorValue(selectedOperator);
191-
if (value) return value;
191+
if (!isNil(value)) return value; // check that not null or undefined, so 0 works
192192

193193
// if is an expression, add to dependencies array,
194194
// that will be resolved before, since will be appended
@@ -415,7 +415,22 @@ const buildCalculatedFieldPipeline = (
415415
name: string
416416
): any[] => {
417417
const operation = getExpressionFromString(expression);
418-
return buildPipeline(operation, name);
418+
const pipeline = buildPipeline(operation, name);
419+
return [
420+
{
421+
$facet: {
422+
calcFieldFacet: pipeline,
423+
},
424+
},
425+
{
426+
$unwind: '$calcFieldFacet',
427+
},
428+
{
429+
$replaceRoot: {
430+
newRoot: '$calcFieldFacet',
431+
},
432+
},
433+
];
419434
};
420435

421436
export default buildCalculatedFieldPipeline;

src/utils/email/gridEmailBuilder.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,9 @@ const datasetToHTML = (columns: any[], rows: any[]): string => {
124124
table += '<tr>';
125125
for (const column of columns) {
126126
const colspan = column.subColumns?.length || 1;
127-
table += `<th colspan="${colspan}" style="background-color: #008dc9; color: white; text-align: center; border: 1px solid black;"><b>`;
127+
table += `<th colspan="${colspan}" style="background-color: #008dc9; color: white; text-align: center; border: 1px solid black${
128+
column.width ? `; width: ${column.width}px` : ''
129+
}"><b>`;
128130
table += column.title;
129131
table += '</b></th>';
130132
}

0 commit comments

Comments
 (0)