Skip to content

Commit a76928a

Browse files
committed
feat: can now archive application pages and restore them
1 parent a09f2c8 commit a76928a

File tree

12 files changed

+260
-6
lines changed

12 files changed

+260
-6
lines changed

config/default.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,4 +171,7 @@ module.exports = {
171171
logger: {
172172
keep: true,
173173
},
174+
archive: {
175+
expires: 60 * 60 * 24 * 30, // Default archive time, in seconds. Set to 30 days
176+
},
174177
};

src/models/dashboard.model.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ export interface Dashboard extends Document {
1919
structure?: any;
2020
showFilter?: boolean;
2121
buttons?: Button[];
22+
archived: boolean;
23+
archivedAt?: Date;
2224
}
2325

2426
/** Mongoose button schema declaration */
@@ -40,6 +42,14 @@ const dashboardSchema = new Schema<Dashboard>(
4042
structure: mongoose.Schema.Types.Mixed,
4143
showFilter: Boolean,
4244
buttons: [buttonSchema],
45+
archived: {
46+
type: Boolean,
47+
default: false,
48+
},
49+
archivedAt: {
50+
type: Date,
51+
expires: 2592000,
52+
},
4353
},
4454
{
4555
timestamps: { createdAt: 'createdAt', updatedAt: 'modifiedAt' },

src/models/page.model.ts

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { Workflow } from './workflow.model';
1010
import { Record } from './record.model';
1111
import { Resource } from './resource.model';
1212
import { ReferenceData } from './referenceData.model';
13+
import { has } from 'lodash';
1314

1415
/** Interface for the page context */
1516
export type PageContextT = (
@@ -49,6 +50,8 @@ export interface Page extends Document {
4950
canDelete?: (mongoose.Types.ObjectId | Role)[];
5051
};
5152
visible: boolean;
53+
archived: boolean;
54+
archivedAt?: Date;
5255
}
5356

5457
/** Mongoose page schema declaration */
@@ -109,12 +112,101 @@ const pageSchema = new Schema<Page>(
109112
type: Boolean,
110113
default: true,
111114
},
115+
archived: {
116+
type: Boolean,
117+
default: false,
118+
},
119+
archivedAt: {
120+
type: Date,
121+
expires: 2592000,
122+
},
112123
},
113124
{
114125
timestamps: { createdAt: 'createdAt', updatedAt: 'modifiedAt' },
115126
}
116127
);
117128

129+
// We need to declare it like that, otherwise we cannot access the 'this'.
130+
pageSchema.pre(['updateOne', 'findOneAndUpdate'], async function () {
131+
const update = this.getUpdate();
132+
if (has(update, 'archived')) {
133+
const page: Page = await this.clone().findOne();
134+
switch (page.type) {
135+
case contentType.workflow: {
136+
const workflow = await Workflow.findById(page.content);
137+
if (workflow) {
138+
// eslint-disable-next-line @typescript-eslint/dot-notation
139+
if (update['archived']) {
140+
await workflow.updateOne({
141+
archived: true,
142+
// eslint-disable-next-line @typescript-eslint/dot-notation
143+
archivedAt: update['archivedAt'],
144+
});
145+
} else {
146+
await workflow.updateOne({
147+
archived: false,
148+
archivedAt: null,
149+
});
150+
}
151+
}
152+
break;
153+
}
154+
case contentType.dashboard: {
155+
const dashboard = await Dashboard.findById(page.content);
156+
if (dashboard) {
157+
// eslint-disable-next-line @typescript-eslint/dot-notation
158+
if (update['archived']) {
159+
dashboard.archived = true;
160+
// eslint-disable-next-line @typescript-eslint/dot-notation
161+
dashboard.archivedAt = update['archivedAt'];
162+
await dashboard.save();
163+
} else {
164+
dashboard.archived = false;
165+
// eslint-disable-next-line @typescript-eslint/dot-notation
166+
dashboard.archivedAt = null;
167+
await dashboard.save();
168+
}
169+
}
170+
if (page.contentWithContext) {
171+
const dashboards: Dashboard[] = [];
172+
page.contentWithContext.forEach((item: any) => {
173+
if (item.content) {
174+
dashboards.push(item.content);
175+
}
176+
});
177+
// eslint-disable-next-line @typescript-eslint/dot-notation
178+
if (update['archived']) {
179+
await Dashboard.updateMany(
180+
{ _id: { $in: dashboards } },
181+
{
182+
$set: {
183+
archived: true,
184+
// eslint-disable-next-line @typescript-eslint/dot-notation
185+
archivedAt: update['archivedAt'],
186+
},
187+
}
188+
);
189+
} else {
190+
await Dashboard.updateMany(
191+
{ _id: { $in: dashboards } },
192+
{
193+
$set: {
194+
archived: false,
195+
archivedAt: null,
196+
},
197+
}
198+
);
199+
}
200+
}
201+
break;
202+
}
203+
default: {
204+
break;
205+
}
206+
}
207+
}
208+
});
209+
118210
// handle cascading deletion and references deletion for pages
119211
addOnBeforeDeleteMany(pageSchema, async (pages) => {
120212
// CASCADE DELETION

src/models/step.model.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ export interface Step extends Document {
2121
canSee?: any;
2222
canUpdate?: any;
2323
canDelete?: any;
24+
archived: boolean;
25+
archivedAt?: Date;
2426
}
2527

2628
/** Mongoose step schema definition */
@@ -53,6 +55,14 @@ const stepSchema = new Schema<Step>(
5355
},
5456
],
5557
},
58+
archived: {
59+
type: Boolean,
60+
default: false,
61+
},
62+
archivedAt: {
63+
type: Date,
64+
expires: 2592000,
65+
},
5666
},
5767
{
5868
timestamps: { createdAt: 'createdAt', updatedAt: 'modifiedAt' },

src/models/workflow.model.ts

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { AccessibleRecordModel, accessibleRecordsPlugin } from '@casl/mongoose';
22
import mongoose, { Schema, Document } from 'mongoose';
33
import { addOnBeforeDeleteMany } from '@utils/models/deletion';
44
import { Step } from './step.model';
5+
import { has } from 'lodash';
56

67
/** Workflow documents interface declaration */
78
export interface Workflow extends Document {
@@ -10,6 +11,8 @@ export interface Workflow extends Document {
1011
createdAt: Date;
1112
modifiedAt: Date;
1213
steps: any[];
14+
archived: boolean;
15+
archivedAt?: Date;
1316
}
1417

1518
/** Mongoose workflow schema declaration */
@@ -20,12 +23,57 @@ const workflowSchema = new Schema<Workflow>(
2023
type: [mongoose.Schema.Types.ObjectId],
2124
ref: 'Step',
2225
},
26+
archived: {
27+
type: Boolean,
28+
default: false,
29+
},
30+
archivedAt: {
31+
type: Date,
32+
expires: 2592000,
33+
},
2334
},
2435
{
2536
timestamps: { createdAt: 'createdAt', updatedAt: 'modifiedAt' },
2637
}
2738
);
2839

40+
workflowSchema.pre('updateOne', async function () {
41+
const update = this.getUpdate();
42+
if (has(update, 'archived')) {
43+
// Copy query to get workflow
44+
const workflow: Workflow = await this.clone().findOne();
45+
// eslint-disable-next-line @typescript-eslint/dot-notation
46+
if (update['archived']) {
47+
// Automatically archive related steps
48+
await Step.updateMany(
49+
{
50+
_id: { $in: workflow.steps },
51+
},
52+
{
53+
$set: {
54+
archived: true,
55+
// eslint-disable-next-line @typescript-eslint/dot-notation
56+
archivedAt: update['archivedAt'],
57+
},
58+
}
59+
);
60+
} else {
61+
// Automatically unarchive related steps
62+
await Step.updateMany(
63+
{
64+
_id: { $in: workflow.steps },
65+
},
66+
{
67+
$set: {
68+
archived: false,
69+
archivedAt: null,
70+
},
71+
}
72+
);
73+
}
74+
}
75+
});
76+
2977
// handle cascading deletion for workflows
3078
addOnBeforeDeleteMany(workflowSchema, async (workflows) => {
3179
const steps = workflows.reduce((acc, w) => acc.concat(w.steps), []);

src/schema/mutation/deletePage.mutation.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,16 @@ export default {
3535
}
3636

3737
// delete page
38-
await page.deleteOne();
38+
if (page.archived) {
39+
// If archived, hard delete it
40+
await page.deleteOne();
41+
} else {
42+
// Else, archive it
43+
await page.updateOne({
44+
archived: true,
45+
archivedAt: new Date(),
46+
});
47+
}
3948
return page;
4049
} catch (err) {
4150
logger.error(err.message, { stack: err.stack });

src/schema/mutation/deleteRecords.mutation.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,9 @@ export default {
5757
} else {
5858
const result = await Record.updateMany(
5959
{ _id: { $in: toDelete.map((x) => x._id) } },
60-
{ archived: true },
60+
{
61+
$set: { archived: true },
62+
},
6163
{ new: true }
6264
);
6365
return result.modifiedCount;

src/schema/mutation/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@ import editLayer from './editLayer.mutation';
8484
import deleteLayer from './deleteLayer.mutation';
8585
import editPageContext from './editPageContext.mutation';
8686
import addDashboardWithContext from './addDashboardWithContext.mutation';
87+
import restorePage from './restorePage.mutation';
8788

8889
/** GraphQL mutation definition */
8990
const Mutation = new GraphQLObjectType({
@@ -174,6 +175,7 @@ const Mutation = new GraphQLObjectType({
174175
addLayer,
175176
editLayer,
176177
deleteLayer,
178+
restorePage,
177179
},
178180
});
179181

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import { GraphQLNonNull, GraphQLID, GraphQLError } from 'graphql';
2+
import { PageType } from '../types';
3+
import { Page } from '@models';
4+
import extendAbilityForPage from '@security/extendAbilityForPage';
5+
import { logger } from '@services/logger.service';
6+
7+
/**
8+
* Restore archived page.
9+
* Page model should automatically restore associated content.
10+
*/
11+
export default {
12+
type: PageType,
13+
args: {
14+
id: { type: new GraphQLNonNull(GraphQLID) },
15+
},
16+
async resolve(parent, args, context) {
17+
try {
18+
// Authentication check
19+
const user = context.user;
20+
if (!user)
21+
throw new GraphQLError(
22+
context.i18next.t('common.errors.userNotLogged')
23+
);
24+
25+
// Find page
26+
const page = await Page.findById(args.id);
27+
28+
// Check access
29+
const ability = await extendAbilityForPage(user, page);
30+
if (ability.cannot('update', page)) {
31+
throw new GraphQLError(
32+
context.i18next.t('common.errors.permissionNotGranted')
33+
);
34+
}
35+
36+
// restore page
37+
if (page && page.archived) {
38+
return await Page.findByIdAndUpdate(
39+
args.id,
40+
{
41+
archived: false,
42+
archivedAt: null,
43+
},
44+
{ new: true }
45+
);
46+
}
47+
} catch (err) {
48+
logger.error(err.message, { stack: err.stack });
49+
if (err instanceof GraphQLError) {
50+
throw new GraphQLError(err.message);
51+
}
52+
throw new GraphQLError(
53+
context.i18next.t('common.errors.internalServerError')
54+
);
55+
}
56+
},
57+
};

src/schema/types/application.type.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,9 @@ export const ApplicationType = new GraphQLObjectType({
104104
},
105105
pages: {
106106
type: new GraphQLList(PageType),
107+
args: {
108+
archived: { type: GraphQLBoolean },
109+
},
107110
async resolve(parent: Application, args, context) {
108111
// Filter the pages based on the access given by app builders.
109112
const ability = await extendAbilityForPage(context.user, parent);
@@ -113,7 +116,13 @@ export const ApplicationType = new GraphQLObjectType({
113116
const pages = await Page.aggregate([
114117
{
115118
$match: {
116-
$and: [filter, { _id: { $in: parent.pages } }],
119+
$and: [
120+
filter,
121+
{ _id: { $in: parent.pages } },
122+
args.archived
123+
? { archived: true }
124+
: { archived: { $ne: true } },
125+
],
117126
},
118127
},
119128
{

0 commit comments

Comments
 (0)