Skip to content
83 changes: 83 additions & 0 deletions src/models/draftRecord.model.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import { AccessibleRecordModel, AccessibleFieldsModel } from '@casl/mongoose';
import mongoose, { Schema } from 'mongoose';
import { User } from './user.model';
import { Form } from './form.model';

/** Draft record documents interface declaration */
// eslint-disable-next-line deprecation/deprecation
export interface DraftRecord {
kind: 'DraftRecord';
form: any;
_form: Form;
resource: any;
createdAt: Date;
modifiedAt: Date;
data: any;
createdBy?: any;
_createdBy?: User;
lastUpdateForm?: any;
_lastUpdateForm?: Form;
}

/** Mongoose record schema declaration */
const draftRecordSchema = new Schema<DraftRecord>(
{
form: {
type: mongoose.Schema.Types.ObjectId,
ref: 'Form',
required: true,
},
_form: {
type: mongoose.Schema.Types.Mixed,
required: true,
},
lastUpdateForm: {
type: mongoose.Schema.Types.ObjectId,
ref: 'Form',
},
_lastUpdateForm: {
type: mongoose.Schema.Types.Mixed,
},
resource: {
type: mongoose.Schema.Types.ObjectId,
ref: 'Resource',
required: false,
},
createdBy: {
user: {
type: mongoose.Schema.Types.ObjectId,
ref: 'User',
},
roles: {
type: [mongoose.Schema.Types.ObjectId],
ref: 'Role',
},
positionAttributes: [
{
value: String,
category: {
type: mongoose.Schema.Types.ObjectId,
ref: 'PositionAttributeCategory',
},
},
],
},
_createdBy: {
type: mongoose.Schema.Types.Mixed,
},
data: {
type: mongoose.Schema.Types.Mixed,
required: true,
},
},
{
timestamps: { createdAt: 'createdAt', updatedAt: 'modifiedAt' },
}
);

/** Mongoose draft record model definition */
// eslint-disable-next-line @typescript-eslint/no-redeclare
export const DraftRecord = mongoose.model<
DraftRecord,
AccessibleFieldsModel<DraftRecord> & AccessibleRecordModel<DraftRecord>
>('DraftRecord', draftRecordSchema);
1 change: 1 addition & 0 deletions src/models/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,4 @@ export * from './template.model';
export * from './distributionList.model';
export * from './customNotification.model';
export * from './layer.model';
export * from './draftRecord.model';
97 changes: 97 additions & 0 deletions src/schema/mutation/addDraftRecord.mutation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import extendAbilityForRecords from '@security/extendAbilityForRecords';
import { transformRecord, getOwnership } from '@utils/form';
import { GraphQLID, GraphQLNonNull, GraphQLError } from 'graphql';
import { DraftRecordType } from '../types';
import { logger } from '@services/logger.service';
import { graphQLAuthCheck } from '@schema/shared';
import { Context } from '@server/apollo/context';
import GraphQLJSON from 'graphql-type-json';
import { DraftRecord, Form } from '@models';
import { Types } from 'mongoose';

/** Arguments for the addDraftRecord mutation */
type AddDraftRecordArgs = {
form?: string | Types.ObjectId;
data: any;
};

/**
* Add a record to a form, if user authorized.
* Throw a GraphQL error if not logged or authorized, or form not found.
* TODO: we have to check form by form for that.
*/
export default {
type: DraftRecordType,
args: {
form: { type: GraphQLID },
data: { type: new GraphQLNonNull(GraphQLJSON) },
},
async resolve(parent, args: AddDraftRecordArgs, context: Context) {
graphQLAuthCheck(context);
try {
const user = context.user;

// Get the form
const form = await Form.findById(args.form);
if (!form) {
throw new GraphQLError(context.i18next.t('common.errors.dataNotFound'));
}
// Check the ability with permissions for this form
const ability = await extendAbilityForRecords(user, form);
if (ability.cannot('create', 'Record')) {
throw new GraphQLError(
context.i18next.t('common.errors.permissionNotGranted')
);
}

// Create the record instance
transformRecord(args.data, form.fields);
const record = new DraftRecord({
form: args.form,
data: args.data,
resource: form.resource ? form.resource : null,
createdBy: {
user: user._id.toString(),
roles: user.roles.map((x) => x._id),
positionAttributes: user.positionAttributes.map((x) => {
return {
value: x.value,
category: x.category._id,
};
}),
},
lastUpdateForm: form.id,
_createdBy: {
user: {
_id: context.user._id,
name: context.user.name,
username: context.user.username,
},
},
_form: {
_id: form._id,
name: form.name,
},
_lastUpdateForm: {
_id: form._id,
name: form.name,
},
});
// Update the createdBy property if we pass some owner data
const ownership = getOwnership(form.fields, args.data);
if (ownership) {
record.createdBy = { ...record.createdBy, ...ownership };
}
await record.save();
return record;
} catch (err) {
logger.error(err.message, { stack: err.stack });
if (err instanceof GraphQLError) {
throw new GraphQLError(err.message);
}
throw new GraphQLError(
context.i18next.t('common.errors.internalServerError')
);
}
},
};
39 changes: 39 additions & 0 deletions src/schema/mutation/deleteDraftRecord.mutation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { GraphQLNonNull, GraphQLID, GraphQLError } from 'graphql';
import { logger } from '@services/logger.service';
import { graphQLAuthCheck } from '@schema/shared';
import { Context } from '@server/apollo/context';
import { DraftRecordType } from '../types';
import { DraftRecord } from '@models';
import { Types } from 'mongoose';

/** Arguments for the deleteRecord mutation */
type DeleteDraftRecordArgs = {
id: string | Types.ObjectId;
};

/**
* Hard-deletes a draft record. Every user can delete their own drafts
* Throw an error if not logged.
*/
export default {
type: DraftRecordType,
args: {
id: { type: new GraphQLNonNull(GraphQLID) },
},
async resolve(parent, args: DeleteDraftRecordArgs, context: Context) {
graphQLAuthCheck(context);
try {
// Get draft Record and associated form
const draftRecord = await DraftRecord.findById(args.id);
return await DraftRecord.findByIdAndDelete(draftRecord._id);
} catch (err) {
logger.error(err.message, { stack: err.stack });
if (err instanceof GraphQLError) {
throw new GraphQLError(err.message);
}
throw new GraphQLError(
context.i18next.t('common.errors.internalServerError')
);
}
},
};
66 changes: 66 additions & 0 deletions src/schema/mutation/editDraftRecord.mutation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { GraphQLNonNull, GraphQLID, GraphQLError } from 'graphql';
import { graphQLAuthCheck } from '@schema/shared';
import { logger } from '@services/logger.service';
import { Context } from '@server/apollo/context';
import { DraftRecordType } from '@schema/types';
import { transformRecord } from '@utils/form';
import { DraftRecord, Form } from '@models';
import GraphQLJSON from 'graphql-type-json';
import { Types } from 'mongoose';

/** Arguments for the editRecord mutation */
type EditRecordArgs = {
id: string | Types.ObjectId;
data?: any;
};

/**
* Edit an existing draft record.
*/
export default {
type: DraftRecordType,
args: {
id: { type: new GraphQLNonNull(GraphQLID) },
data: { type: GraphQLJSON },
},
async resolve(parent, args: EditRecordArgs, context: Context) {
graphQLAuthCheck(context);
try {
if (!args.data) {
throw new GraphQLError(
context.i18next.t('mutations.record.edit.errors.invalidArguments')
);
}

// Get old draft record and form
const oldDraftRecord: DraftRecord = await DraftRecord.findById(args.id);
const form: Form = await Form.findById(oldDraftRecord.form);

if (!oldDraftRecord || !form) {
throw new GraphQLError(context.i18next.t('common.errors.dataNotFound'));
}

transformRecord(args.data, form.fields);
const update: any = {
data: { ...oldDraftRecord.data, ...args.data },
lastUpdateForm: form,
_lastUpdateForm: {
_id: form._id,
name: form.name,
},
};
const draftRecord = DraftRecord.findByIdAndUpdate(args.id, update, {
new: true,
});
return await draftRecord;
} catch (err) {
logger.error(err.message, { stack: err.stack });
if (err instanceof GraphQLError) {
throw new GraphQLError(err.message);
}
throw new GraphQLError(
context.i18next.t('common.errors.internalServerError')
);
}
},
};
6 changes: 6 additions & 0 deletions src/schema/mutation/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,9 @@ import deleteLayer from './deleteLayer.mutation';
import editPageContext from './editPageContext.mutation';
import addDashboardWithContext from './addDashboardWithContext.mutation';
import restorePage from './restorePage.mutation';
import addDraftRecord from './addDraftRecord.mutation';
import deleteDraftRecord from './deleteDraftRecord.mutation';
import editDraftRecord from './editDraftRecord.mutation';

/** GraphQL mutation definition */
const Mutation = new GraphQLObjectType({
Expand Down Expand Up @@ -176,6 +179,9 @@ const Mutation = new GraphQLObjectType({
editLayer,
deleteLayer,
restorePage,
addDraftRecord,
deleteDraftRecord,
editDraftRecord,
},
});

Expand Down
43 changes: 43 additions & 0 deletions src/schema/query/draftRecords.query.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { GraphQLNonNull, GraphQLID, GraphQLError, GraphQLList } from 'graphql';
import { logger } from '@services/logger.service';
import { graphQLAuthCheck } from '@schema/shared';
import { Context } from '@server/apollo/context';
import { DraftRecordType } from '../types';
import { DraftRecord } from '@models';
import { Types } from 'mongoose';

type DraftRecordsArgs = {
form: string | Types.ObjectId;
};

/**
* List all draft records available for the logged user.
* Throw GraphQL error if not logged.
*/
export default {
type: new GraphQLList(DraftRecordType),
args: {
form: { type: new GraphQLNonNull(GraphQLID) },
},
async resolve(parent, args: DraftRecordsArgs, context: Context) {
// Authentication check
graphQLAuthCheck(context);
try {
const user = context.user;
//Only get draft records created by current user
const draftRecords = await DraftRecord.find({
'createdBy.user': user._id.toString(),
form: args.form,
});
return draftRecords;
} catch (err) {
logger.error(err.message, { stack: err.stack });
if (err instanceof GraphQLError) {
throw new GraphQLError(err.message);
}
throw new GraphQLError(
context.i18next.t('common.errors.internalServerError')
);
}
},
};
2 changes: 2 additions & 0 deletions src/schema/query/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import group from './group.query';
import groups from './groups.query';
import layers from './layers.query';
import layer from './layer.query';
import draftRecords from './draftRecords.query';

/** GraphQL query type definition */
const Query = new GraphQLObjectType({
Expand Down Expand Up @@ -78,6 +79,7 @@ const Query = new GraphQLObjectType({
recordHistory,
layers,
layer,
draftRecords,
},
});

Expand Down
Loading