Skip to content

Commit d1b1e3b

Browse files
LenaelleLestelafsAntoineRelief
authored
feat: Ability to save draft record (#802)
--------- Co-authored-by: estelafs <[email protected]> Co-authored-by: Antoine Hurard <[email protected]>
1 parent 62e6db7 commit d1b1e3b

File tree

10 files changed

+420
-0
lines changed

10 files changed

+420
-0
lines changed

src/models/draftRecord.model.ts

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
import { AccessibleRecordModel, AccessibleFieldsModel } from '@casl/mongoose';
2+
import mongoose, { Schema } from 'mongoose';
3+
import { User } from './user.model';
4+
import { Form } from './form.model';
5+
6+
/** Draft record documents interface declaration */
7+
// eslint-disable-next-line deprecation/deprecation
8+
export interface DraftRecord {
9+
kind: 'DraftRecord';
10+
form: any;
11+
_form: Form;
12+
resource: any;
13+
createdAt: Date;
14+
modifiedAt: Date;
15+
data: any;
16+
createdBy?: any;
17+
_createdBy?: User;
18+
lastUpdateForm?: any;
19+
_lastUpdateForm?: Form;
20+
}
21+
22+
/** Mongoose record schema declaration */
23+
const draftRecordSchema = new Schema<DraftRecord>(
24+
{
25+
form: {
26+
type: mongoose.Schema.Types.ObjectId,
27+
ref: 'Form',
28+
required: true,
29+
},
30+
_form: {
31+
type: mongoose.Schema.Types.Mixed,
32+
required: true,
33+
},
34+
lastUpdateForm: {
35+
type: mongoose.Schema.Types.ObjectId,
36+
ref: 'Form',
37+
},
38+
_lastUpdateForm: {
39+
type: mongoose.Schema.Types.Mixed,
40+
},
41+
resource: {
42+
type: mongoose.Schema.Types.ObjectId,
43+
ref: 'Resource',
44+
required: false,
45+
},
46+
createdBy: {
47+
user: {
48+
type: mongoose.Schema.Types.ObjectId,
49+
ref: 'User',
50+
},
51+
roles: {
52+
type: [mongoose.Schema.Types.ObjectId],
53+
ref: 'Role',
54+
},
55+
positionAttributes: [
56+
{
57+
value: String,
58+
category: {
59+
type: mongoose.Schema.Types.ObjectId,
60+
ref: 'PositionAttributeCategory',
61+
},
62+
},
63+
],
64+
},
65+
_createdBy: {
66+
type: mongoose.Schema.Types.Mixed,
67+
},
68+
data: {
69+
type: mongoose.Schema.Types.Mixed,
70+
required: true,
71+
},
72+
},
73+
{
74+
timestamps: { createdAt: 'createdAt', updatedAt: 'modifiedAt' },
75+
}
76+
);
77+
78+
/** Mongoose draft record model definition */
79+
// eslint-disable-next-line @typescript-eslint/no-redeclare
80+
export const DraftRecord = mongoose.model<
81+
DraftRecord,
82+
AccessibleFieldsModel<DraftRecord> & AccessibleRecordModel<DraftRecord>
83+
>('DraftRecord', draftRecordSchema);

src/models/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,3 +26,4 @@ export * from './template.model';
2626
export * from './distributionList.model';
2727
export * from './customNotification.model';
2828
export * from './layer.model';
29+
export * from './draftRecord.model';
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
import extendAbilityForRecords from '@security/extendAbilityForRecords';
2+
import { transformRecord, getOwnership } from '@utils/form';
3+
import { GraphQLID, GraphQLNonNull, GraphQLError } from 'graphql';
4+
import { DraftRecordType } from '../types';
5+
import { logger } from '@services/logger.service';
6+
import { graphQLAuthCheck } from '@schema/shared';
7+
import { Context } from '@server/apollo/context';
8+
import GraphQLJSON from 'graphql-type-json';
9+
import { DraftRecord, Form } from '@models';
10+
import { Types } from 'mongoose';
11+
12+
/** Arguments for the addDraftRecord mutation */
13+
type AddDraftRecordArgs = {
14+
form?: string | Types.ObjectId;
15+
data: any;
16+
};
17+
18+
/**
19+
* Add a record to a form, if user authorized.
20+
* Throw a GraphQL error if not logged or authorized, or form not found.
21+
* TODO: we have to check form by form for that.
22+
*/
23+
export default {
24+
type: DraftRecordType,
25+
args: {
26+
form: { type: GraphQLID },
27+
data: { type: new GraphQLNonNull(GraphQLJSON) },
28+
},
29+
async resolve(parent, args: AddDraftRecordArgs, context: Context) {
30+
graphQLAuthCheck(context);
31+
try {
32+
const user = context.user;
33+
34+
// Get the form
35+
const form = await Form.findById(args.form);
36+
if (!form) {
37+
throw new GraphQLError(context.i18next.t('common.errors.dataNotFound'));
38+
}
39+
// Check the ability with permissions for this form
40+
const ability = await extendAbilityForRecords(user, form);
41+
if (ability.cannot('create', 'Record')) {
42+
throw new GraphQLError(
43+
context.i18next.t('common.errors.permissionNotGranted')
44+
);
45+
}
46+
47+
// Create the record instance
48+
transformRecord(args.data, form.fields);
49+
const record = new DraftRecord({
50+
form: args.form,
51+
data: args.data,
52+
resource: form.resource ? form.resource : null,
53+
createdBy: {
54+
user: user._id.toString(),
55+
roles: user.roles.map((x) => x._id),
56+
positionAttributes: user.positionAttributes.map((x) => {
57+
return {
58+
value: x.value,
59+
category: x.category._id,
60+
};
61+
}),
62+
},
63+
lastUpdateForm: form.id,
64+
_createdBy: {
65+
user: {
66+
_id: context.user._id,
67+
name: context.user.name,
68+
username: context.user.username,
69+
},
70+
},
71+
_form: {
72+
_id: form._id,
73+
name: form.name,
74+
},
75+
_lastUpdateForm: {
76+
_id: form._id,
77+
name: form.name,
78+
},
79+
});
80+
// Update the createdBy property if we pass some owner data
81+
const ownership = getOwnership(form.fields, args.data);
82+
if (ownership) {
83+
record.createdBy = { ...record.createdBy, ...ownership };
84+
}
85+
await record.save();
86+
return record;
87+
} catch (err) {
88+
logger.error(err.message, { stack: err.stack });
89+
if (err instanceof GraphQLError) {
90+
throw new GraphQLError(err.message);
91+
}
92+
throw new GraphQLError(
93+
context.i18next.t('common.errors.internalServerError')
94+
);
95+
}
96+
},
97+
};
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import { GraphQLNonNull, GraphQLID, GraphQLError } from 'graphql';
2+
import { logger } from '@services/logger.service';
3+
import { graphQLAuthCheck } from '@schema/shared';
4+
import { Context } from '@server/apollo/context';
5+
import { DraftRecordType } from '../types';
6+
import { DraftRecord } from '@models';
7+
import { Types } from 'mongoose';
8+
9+
/** Arguments for the deleteRecord mutation */
10+
type DeleteDraftRecordArgs = {
11+
id: string | Types.ObjectId;
12+
};
13+
14+
/**
15+
* Hard-deletes a draft record. Every user can delete their own drafts
16+
* Throw an error if not logged.
17+
*/
18+
export default {
19+
type: DraftRecordType,
20+
args: {
21+
id: { type: new GraphQLNonNull(GraphQLID) },
22+
},
23+
async resolve(parent, args: DeleteDraftRecordArgs, context: Context) {
24+
graphQLAuthCheck(context);
25+
try {
26+
// Get draft Record and associated form
27+
const draftRecord = await DraftRecord.findById(args.id);
28+
return await DraftRecord.findByIdAndDelete(draftRecord._id);
29+
} catch (err) {
30+
logger.error(err.message, { stack: err.stack });
31+
if (err instanceof GraphQLError) {
32+
throw new GraphQLError(err.message);
33+
}
34+
throw new GraphQLError(
35+
context.i18next.t('common.errors.internalServerError')
36+
);
37+
}
38+
},
39+
};
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import { GraphQLNonNull, GraphQLID, GraphQLError } from 'graphql';
2+
import { graphQLAuthCheck } from '@schema/shared';
3+
import { logger } from '@services/logger.service';
4+
import { Context } from '@server/apollo/context';
5+
import { DraftRecordType } from '@schema/types';
6+
import { transformRecord } from '@utils/form';
7+
import { DraftRecord, Form } from '@models';
8+
import GraphQLJSON from 'graphql-type-json';
9+
import { Types } from 'mongoose';
10+
11+
/** Arguments for the editRecord mutation */
12+
type EditRecordArgs = {
13+
id: string | Types.ObjectId;
14+
data?: any;
15+
};
16+
17+
/**
18+
* Edit an existing draft record.
19+
*/
20+
export default {
21+
type: DraftRecordType,
22+
args: {
23+
id: { type: new GraphQLNonNull(GraphQLID) },
24+
data: { type: GraphQLJSON },
25+
},
26+
async resolve(parent, args: EditRecordArgs, context: Context) {
27+
graphQLAuthCheck(context);
28+
try {
29+
if (!args.data) {
30+
throw new GraphQLError(
31+
context.i18next.t('mutations.record.edit.errors.invalidArguments')
32+
);
33+
}
34+
35+
// Get old draft record and form
36+
const oldDraftRecord: DraftRecord = await DraftRecord.findById(args.id);
37+
const form: Form = await Form.findById(oldDraftRecord.form);
38+
39+
if (!oldDraftRecord || !form) {
40+
throw new GraphQLError(context.i18next.t('common.errors.dataNotFound'));
41+
}
42+
43+
transformRecord(args.data, form.fields);
44+
const update: any = {
45+
data: { ...oldDraftRecord.data, ...args.data },
46+
lastUpdateForm: form,
47+
_lastUpdateForm: {
48+
_id: form._id,
49+
name: form.name,
50+
},
51+
};
52+
const draftRecord = DraftRecord.findByIdAndUpdate(args.id, update, {
53+
new: true,
54+
});
55+
return await draftRecord;
56+
} catch (err) {
57+
logger.error(err.message, { stack: err.stack });
58+
if (err instanceof GraphQLError) {
59+
throw new GraphQLError(err.message);
60+
}
61+
throw new GraphQLError(
62+
context.i18next.t('common.errors.internalServerError')
63+
);
64+
}
65+
},
66+
};

src/schema/mutation/index.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,9 @@ import deleteLayer from './deleteLayer.mutation';
8585
import editPageContext from './editPageContext.mutation';
8686
import addDashboardWithContext from './addDashboardWithContext.mutation';
8787
import restorePage from './restorePage.mutation';
88+
import addDraftRecord from './addDraftRecord.mutation';
89+
import deleteDraftRecord from './deleteDraftRecord.mutation';
90+
import editDraftRecord from './editDraftRecord.mutation';
8891

8992
/** GraphQL mutation definition */
9093
const Mutation = new GraphQLObjectType({
@@ -176,6 +179,9 @@ const Mutation = new GraphQLObjectType({
176179
editLayer,
177180
deleteLayer,
178181
restorePage,
182+
addDraftRecord,
183+
deleteDraftRecord,
184+
editDraftRecord,
179185
},
180186
});
181187

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import { GraphQLNonNull, GraphQLID, GraphQLError, GraphQLList } from 'graphql';
2+
import { logger } from '@services/logger.service';
3+
import { graphQLAuthCheck } from '@schema/shared';
4+
import { Context } from '@server/apollo/context';
5+
import { DraftRecordType } from '../types';
6+
import { DraftRecord } from '@models';
7+
import { Types } from 'mongoose';
8+
9+
type DraftRecordsArgs = {
10+
form: string | Types.ObjectId;
11+
};
12+
13+
/**
14+
* List all draft records available for the logged user.
15+
* Throw GraphQL error if not logged.
16+
*/
17+
export default {
18+
type: new GraphQLList(DraftRecordType),
19+
args: {
20+
form: { type: new GraphQLNonNull(GraphQLID) },
21+
},
22+
async resolve(parent, args: DraftRecordsArgs, context: Context) {
23+
// Authentication check
24+
graphQLAuthCheck(context);
25+
try {
26+
const user = context.user;
27+
//Only get draft records created by current user
28+
const draftRecords = await DraftRecord.find({
29+
'createdBy.user': user._id.toString(),
30+
form: args.form,
31+
});
32+
return draftRecords;
33+
} catch (err) {
34+
logger.error(err.message, { stack: err.stack });
35+
if (err instanceof GraphQLError) {
36+
throw new GraphQLError(err.message);
37+
}
38+
throw new GraphQLError(
39+
context.i18next.t('common.errors.internalServerError')
40+
);
41+
}
42+
},
43+
};

src/schema/query/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ import group from './group.query';
3636
import groups from './groups.query';
3737
import layers from './layers.query';
3838
import layer from './layer.query';
39+
import draftRecords from './draftRecords.query';
3940

4041
/** GraphQL query type definition */
4142
const Query = new GraphQLObjectType({
@@ -78,6 +79,7 @@ const Query = new GraphQLObjectType({
7879
recordHistory,
7980
layers,
8081
layer,
82+
draftRecords,
8183
},
8284
});
8385

0 commit comments

Comments
 (0)