From 3849601719feab454168b270f6148e46299913f4 Mon Sep 17 00:00:00 2001 From: MwanPygmay Date: Tue, 21 Nov 2023 09:18:28 +0100 Subject: [PATCH 1/5] writing field details as query, should change it for rest afterwards --- src/schema/query/fieldDetails.query.ts | 120 +++++++++++++++++++++++++ src/schema/query/index.ts | 2 + 2 files changed, 122 insertions(+) create mode 100644 src/schema/query/fieldDetails.query.ts diff --git a/src/schema/query/fieldDetails.query.ts b/src/schema/query/fieldDetails.query.ts new file mode 100644 index 000000000..a8c5420f2 --- /dev/null +++ b/src/schema/query/fieldDetails.query.ts @@ -0,0 +1,120 @@ +import { + GraphQLNonNull, + GraphQLError, + GraphQLString, + GraphQLID, +} from 'graphql'; +import { Form, Record } from '@models'; +import extendAbilityForRecords from '@security/extendAbilityForRecords'; +import { logger } from '@services/logger.service'; +import { graphQLAuthCheck } from '@schema/shared'; +import { Context } from '@server/apollo/context'; +import { AppAbility } from '@security/defineUserAbility'; +import { subject } from '@casl/ability'; +import GraphQLJSON from 'graphql-type-json'; + +/** + * Filters data from a record according to the user's ability + * keeps only fields the user has access to + * + * @param record Record to filter data from + * @param ability User ability object + * @param field field to get data from + * @param context Apollo context + * @returns Record with only accessible data + */ +const getFieldValueFromRecord = ( + record: Record, + ability: AppAbility, + field: string, + context: Context +) => { + if (ability.cannot('read', subject('Record', record), `data.${field}`)) { + throw new GraphQLError( + context.i18next.t('common.errors.permissionNotGranted') + ); + } + return record.data[field]; +}; + +/** + * Filters data from a (list of) record(s) according to the user's ability + * keeps only fields the user has access to + * + * @param element Record/Records to filter data from + * @param ability User ability object + * @param field field to test + * @param context Apollo context + * @returns Record/Records with only accessible data + */ +export function getFieldValue( + element: Record | Record[], + ability: AppAbility, + field: string, + context: Context +) { + return Array.isArray(element) + ? element.map((r) => getFieldValueFromRecord(r, ability, field, context)) + : getFieldValueFromRecord(element, ability, field, context); +} + +/** + * Return record from id if available for the logged user. + * Throw GraphQL error if not logged. + */ +export default { + type: GraphQLJSON, + args: { + form: { type: new GraphQLNonNull(GraphQLID) }, + field: { type: new GraphQLNonNull(GraphQLString) }, + }, + async resolve(parent, args, context: Context) { + graphQLAuthCheck(context); + try { + const user = context.user; + // Get the form and the record + const records = await Record.where({ form: args.form }); + const form = await Form.findById(args.form); + const field = form.fields.find((f) => f.name === args.field); + + // Check ability + const ability = await extendAbilityForRecords(user, form); + + // Return the record + const valuesForField = getFieldValue( + records, + ability, + field.name, + context + ); + + console.log('field type', field.type); + switch (field.type) { + case 'numeric': + return { + details: [Math.min(...valuesForField), Math.max(...valuesForField)], + }; + case 'time': + case 'date': + return { + details: [ + new Date(Math.min(...valuesForField)), + new Date(Math.max(...valuesForField)), + ], + }; + case 'text': + return { details: valuesForField }; + default: + return new GraphQLError('unsupported type'); + } + } 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') + ); + } + }, +}; diff --git a/src/schema/query/index.ts b/src/schema/query/index.ts index d667a103c..452c993db 100644 --- a/src/schema/query/index.ts +++ b/src/schema/query/index.ts @@ -2,6 +2,7 @@ import { GraphQLObjectType } from 'graphql'; import resources from './resources.query'; import resource from './resource.query'; import notifications from './notifications.query'; +import fieldDetails from './fieldDetails.query'; import forms from './forms.query'; import form from './form.query'; import records from './records.query'; @@ -49,6 +50,7 @@ const Query = new GraphQLObjectType({ channels, dashboard, dashboards, + fieldDetails, form, forms, group, From 174e1240659a7d67617410c189ea1f4b881742e3 Mon Sep 17 00:00:00 2001 From: MwanPygmay Date: Tue, 21 Nov 2023 12:56:51 +0100 Subject: [PATCH 2/5] changed the query to just get min and max or examples, but should limit the weight of the query --- src/schema/query/fieldDetails.query.ts | 164 +++++++++++++------------ 1 file changed, 84 insertions(+), 80 deletions(-) diff --git a/src/schema/query/fieldDetails.query.ts b/src/schema/query/fieldDetails.query.ts index a8c5420f2..386eba384 100644 --- a/src/schema/query/fieldDetails.query.ts +++ b/src/schema/query/fieldDetails.query.ts @@ -1,62 +1,12 @@ -import { - GraphQLNonNull, - GraphQLError, - GraphQLString, - GraphQLID, -} from 'graphql'; -import { Form, Record } from '@models'; +import { GraphQLNonNull, GraphQLError, GraphQLID } from 'graphql'; +import { Record } from '@models'; import extendAbilityForRecords from '@security/extendAbilityForRecords'; import { logger } from '@services/logger.service'; import { graphQLAuthCheck } from '@schema/shared'; import { Context } from '@server/apollo/context'; -import { AppAbility } from '@security/defineUserAbility'; import { subject } from '@casl/ability'; import GraphQLJSON from 'graphql-type-json'; - -/** - * Filters data from a record according to the user's ability - * keeps only fields the user has access to - * - * @param record Record to filter data from - * @param ability User ability object - * @param field field to get data from - * @param context Apollo context - * @returns Record with only accessible data - */ -const getFieldValueFromRecord = ( - record: Record, - ability: AppAbility, - field: string, - context: Context -) => { - if (ability.cannot('read', subject('Record', record), `data.${field}`)) { - throw new GraphQLError( - context.i18next.t('common.errors.permissionNotGranted') - ); - } - return record.data[field]; -}; - -/** - * Filters data from a (list of) record(s) according to the user's ability - * keeps only fields the user has access to - * - * @param element Record/Records to filter data from - * @param ability User ability object - * @param field field to test - * @param context Apollo context - * @returns Record/Records with only accessible data - */ -export function getFieldValue( - element: Record | Record[], - ability: AppAbility, - field: string, - context: Context -) { - return Array.isArray(element) - ? element.map((r) => getFieldValueFromRecord(r, ability, field, context)) - : getFieldValueFromRecord(element, ability, field, context); -} +import { Types } from 'mongoose'; /** * Return record from id if available for the logged user. @@ -66,46 +16,100 @@ export default { type: GraphQLJSON, args: { form: { type: new GraphQLNonNull(GraphQLID) }, - field: { type: new GraphQLNonNull(GraphQLString) }, + field: { type: new GraphQLNonNull(GraphQLJSON) }, }, async resolve(parent, args, context: Context) { graphQLAuthCheck(context); try { const user = context.user; - // Get the form and the record - const records = await Record.where({ form: args.form }); - const form = await Form.findById(args.form); - const field = form.fields.find((f) => f.name === args.field); - // Check ability - const ability = await extendAbilityForRecords(user, form); - - // Return the record - const valuesForField = getFieldValue( - records, - ability, - field.name, - context - ); + const ability = await extendAbilityForRecords(user); + const field = args.field; - console.log('field type', field.type); + let min, max: Record[]; switch (field.type) { case 'numeric': - return { - details: [Math.min(...valuesForField), Math.max(...valuesForField)], - }; + max = await Record.find({ form: args.form }) + .sort({ [`data.${field.name}`]: -1 }) + .limit(1); + min = await Record.find({ form: args.form }) + .sort({ [`data.${field.name}`]: 1 }) + .limit(1); + if (max.length == 0) { + //if there is a max, there is a min + return []; + } + if ( + ability.cannot( + 'read', + subject('Record', max[0]), + `data.${field.name}` + ) + ) { + throw new GraphQLError( + context.i18next.t('common.errors.permissionNotGranted') + ); + } + return [min[0].data[field.name], max[0].data[field.name]]; case 'time': case 'date': - return { - details: [ - new Date(Math.min(...valuesForField)), - new Date(Math.max(...valuesForField)), - ], - }; + max = await Record.find({ form: args.form }) + .sort({ [`data.${field.name}`]: -1 }) + .limit(1); + + min = await Record.find({ form: args.form }) + .sort({ [`data.${field.name}`]: 1 }) + .limit(1); + + if (max.length == 0) { + //if there is a max, there is a min + return []; + } + if ( + ability.cannot( + 'read', + subject('Record', max[0]), + `data.${field.name}` + ) + ) { + throw new GraphQLError( + context.i18next.t('common.errors.permissionNotGranted') + ); + } + return [ + new Date(min[0].data[field.name]), + new Date(max[0].data[field.name]), + ]; case 'text': - return { details: valuesForField }; + //We get the 5 most common values from the database + const mostFrequentValues = await Record.aggregate([ + { $match: { form: new Types.ObjectId(args.form) } }, + { + $group: { + _id: `$data.${field.name}`, + count: { $sum: 1 }, + }, + }, + { $sort: { count: -1 } }, + { $limit: 5 }, + ]); + if (mostFrequentValues.length == 0) { + return []; + } + if ( + ability.cannot( + 'read', + subject('Record', mostFrequentValues[0]), + '_id' + ) + ) { + throw new GraphQLError( + context.i18next.t('common.errors.permissionNotGranted') + ); + } + return mostFrequentValues.map((item) => item._id); default: - return new GraphQLError('unsupported type'); + return new GraphQLError('Unsupported type'); } } catch (err) { logger.error(err.message, { stack: err.stack }); From d3d711a69ca42c27c5f7a7a9b6452fecec5fc8e3 Mon Sep 17 00:00:00 2001 From: MwanPygmay Date: Tue, 21 Nov 2023 15:40:24 +0100 Subject: [PATCH 3/5] change form to resource as it is easier of access for filtering --- src/schema/query/fieldDetails.query.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/schema/query/fieldDetails.query.ts b/src/schema/query/fieldDetails.query.ts index 386eba384..ff98cd7de 100644 --- a/src/schema/query/fieldDetails.query.ts +++ b/src/schema/query/fieldDetails.query.ts @@ -15,7 +15,7 @@ import { Types } from 'mongoose'; export default { type: GraphQLJSON, args: { - form: { type: new GraphQLNonNull(GraphQLID) }, + resource: { type: new GraphQLNonNull(GraphQLID) }, field: { type: new GraphQLNonNull(GraphQLJSON) }, }, async resolve(parent, args, context: Context) { @@ -29,10 +29,10 @@ export default { let min, max: Record[]; switch (field.type) { case 'numeric': - max = await Record.find({ form: args.form }) + max = await Record.find({ resource: args.resource }) .sort({ [`data.${field.name}`]: -1 }) .limit(1); - min = await Record.find({ form: args.form }) + min = await Record.find({ resource: args.resource }) .sort({ [`data.${field.name}`]: 1 }) .limit(1); if (max.length == 0) { @@ -53,11 +53,11 @@ export default { return [min[0].data[field.name], max[0].data[field.name]]; case 'time': case 'date': - max = await Record.find({ form: args.form }) + max = await Record.find({ resource: args.resource }) .sort({ [`data.${field.name}`]: -1 }) .limit(1); - min = await Record.find({ form: args.form }) + min = await Record.find({ resource: args.resource }) .sort({ [`data.${field.name}`]: 1 }) .limit(1); @@ -83,7 +83,7 @@ export default { case 'text': //We get the 5 most common values from the database const mostFrequentValues = await Record.aggregate([ - { $match: { form: new Types.ObjectId(args.form) } }, + { $match: { resource: new Types.ObjectId(args.resource) } }, { $group: { _id: `$data.${field.name}`, From cb7a09c5757ef4508f7c8c8be5cc2b02413a2d39 Mon Sep 17 00:00:00 2001 From: estelafs Date: Tue, 21 Nov 2023 18:23:54 -0300 Subject: [PATCH 4/5] added time case --- src/schema/query/fieldDetails.query.ts | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/src/schema/query/fieldDetails.query.ts b/src/schema/query/fieldDetails.query.ts index ff98cd7de..070c0268c 100644 --- a/src/schema/query/fieldDetails.query.ts +++ b/src/schema/query/fieldDetails.query.ts @@ -52,6 +52,28 @@ export default { } return [min[0].data[field.name], max[0].data[field.name]]; case 'time': + max = await Record.find({ resource: args.resource }) + .sort({ [`data.${field.name}`]: -1 }) + .limit(1); + min = await Record.find({ resource: args.resource }) + .sort({ [`data.${field.name}`]: 1 }) + .limit(1); + if (max.length == 0) { + //if there is a max, there is a min + return []; + } + if ( + ability.cannot( + 'read', + subject('Record', max[0]), + `data.${field.name}` + ) + ) { + throw new GraphQLError( + context.i18next.t('common.errors.permissionNotGranted') + ); + } + return [min[0].data[field.name], max[0].data[field.name]]; case 'date': max = await Record.find({ resource: args.resource }) .sort({ [`data.${field.name}`]: -1 }) From 88b6505b5005e9ed1522623e7544a561d862b60b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pac=C3=B4me=20Rivier?= Date: Wed, 22 Nov 2023 10:45:25 +0100 Subject: [PATCH 5/5] refactor code --- src/schema/query/fieldDetails.query.ts | 51 +------------------------- 1 file changed, 1 insertion(+), 50 deletions(-) diff --git a/src/schema/query/fieldDetails.query.ts b/src/schema/query/fieldDetails.query.ts index 070c0268c..35abba3fc 100644 --- a/src/schema/query/fieldDetails.query.ts +++ b/src/schema/query/fieldDetails.query.ts @@ -29,60 +29,14 @@ export default { let min, max: Record[]; switch (field.type) { case 'numeric': - max = await Record.find({ resource: args.resource }) - .sort({ [`data.${field.name}`]: -1 }) - .limit(1); - min = await Record.find({ resource: args.resource }) - .sort({ [`data.${field.name}`]: 1 }) - .limit(1); - if (max.length == 0) { - //if there is a max, there is a min - return []; - } - if ( - ability.cannot( - 'read', - subject('Record', max[0]), - `data.${field.name}` - ) - ) { - throw new GraphQLError( - context.i18next.t('common.errors.permissionNotGranted') - ); - } - return [min[0].data[field.name], max[0].data[field.name]]; case 'time': - max = await Record.find({ resource: args.resource }) - .sort({ [`data.${field.name}`]: -1 }) - .limit(1); - min = await Record.find({ resource: args.resource }) - .sort({ [`data.${field.name}`]: 1 }) - .limit(1); - if (max.length == 0) { - //if there is a max, there is a min - return []; - } - if ( - ability.cannot( - 'read', - subject('Record', max[0]), - `data.${field.name}` - ) - ) { - throw new GraphQLError( - context.i18next.t('common.errors.permissionNotGranted') - ); - } - return [min[0].data[field.name], max[0].data[field.name]]; case 'date': max = await Record.find({ resource: args.resource }) .sort({ [`data.${field.name}`]: -1 }) .limit(1); - min = await Record.find({ resource: args.resource }) .sort({ [`data.${field.name}`]: 1 }) .limit(1); - if (max.length == 0) { //if there is a max, there is a min return []; @@ -98,10 +52,7 @@ export default { context.i18next.t('common.errors.permissionNotGranted') ); } - return [ - new Date(min[0].data[field.name]), - new Date(max[0].data[field.name]), - ]; + return [min[0].data[field.name], max[0].data[field.name]]; case 'text': //We get the 5 most common values from the database const mostFrequentValues = await Record.aggregate([