From d2067d69dcbd980b4b55f546fda00cd1a63cad96 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bruno=20Tann=C3=BAs?= <39497117+brunotannus@users.noreply.github.com> Date: Thu, 15 Feb 2024 21:12:46 -0300 Subject: [PATCH 1/5] back-end first working version --- package-lock.json | 35 +- package.json | 3 +- .../mutation/generateRecords.mutation.ts | 104 +++++ src/schema/mutation/index.ts | 2 + src/utils/form/generateData.ts | 407 ++++++++++++++++++ src/utils/form/index.ts | 1 + 6 files changed, 541 insertions(+), 11 deletions(-) create mode 100644 src/schema/mutation/generateRecords.mutation.ts create mode 100644 src/utils/form/generateData.ts diff --git a/package-lock.json b/package-lock.json index 298003627..9398a417a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -36,6 +36,7 @@ "express-rate-limit": "^6.4.0", "express-session": "^1.17.3", "express-winston": "^4.2.0", + "geojson-random": "^0.5.0", "graphql": "^16.6.0", "graphql-amqp-subscriptions": "^1.2.1", "graphql-scalars": "^1.22.2", @@ -9255,7 +9256,6 @@ "version": "2.3.0", "resolved": "https://registry.npmjs.org/from2/-/from2-2.3.0.tgz", "integrity": "sha512-OMcX/4IC/uqEPVgGeyfN22LJk6AZrMkRZHxcHBMBvHScDGgwTm2GT2Wkgtocyd3JfZffjj2kYUDXXII0Fk9W0g==", - "dev": true, "dependencies": { "inherits": "^2.0.1", "readable-stream": "^2.0.0" @@ -9264,14 +9264,12 @@ "node_modules/from2/node_modules/isarray": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", - "dev": true + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==" }, "node_modules/from2/node_modules/readable-stream": { "version": "2.3.8", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", - "dev": true, "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", @@ -9285,14 +9283,12 @@ "node_modules/from2/node_modules/safe-buffer": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "dev": true + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" }, "node_modules/from2/node_modules/string_decoder": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "dev": true, "dependencies": { "safe-buffer": "~5.1.0" } @@ -9408,6 +9404,18 @@ "deep-equal": "^1.0.0" } }, + "node_modules/geojson-random": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/geojson-random/-/geojson-random-0.5.0.tgz", + "integrity": "sha512-a4j6KJCC/ZhufwiXMxuoXrJiOxwwBQ0Y0DEcGmytnSJC11AkWHSKSFOw/ovh6QFp8n9XIDXhQXAvArRCvi2Y4A==", + "dependencies": { + "from2": "^2.1.0", + "geojson-stream": "0.1.0" + }, + "bin": { + "geojson-random": "geojson-random" + } + }, "node_modules/geojson-rbush": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/geojson-rbush/-/geojson-rbush-3.2.0.tgz", @@ -9433,6 +9441,15 @@ "quickselect": "^2.0.0" } }, + "node_modules/geojson-stream": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/geojson-stream/-/geojson-stream-0.1.0.tgz", + "integrity": "sha512-svSg5fFXPaTiqzEBGXScA+nISaeC9rLvku2PH+wM5LToATUw2bLIrvls43ymnT9Xnp51nBPVyK9m4Af40KpJ7w==", + "dependencies": { + "JSONStream": "^1.0.0", + "through": "^2.3.4" + } + }, "node_modules/get-caller-file": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", @@ -11647,7 +11664,6 @@ "version": "1.3.5", "resolved": "https://registry.npmjs.org/JSONStream/-/JSONStream-1.3.5.tgz", "integrity": "sha512-E+iruNOY8VV9s4JEbe1aNEm6MiszPRr/UfcHMz0TQh1BXSxHK+ASV1R6W4HpjBhSeS+54PIsAMCBmwD06LLsqQ==", - "dev": true, "dependencies": { "jsonparse": "^1.2.0", "through": ">=2.2.7 <3" @@ -22814,8 +22830,7 @@ "node_modules/through": { "version": "2.3.8", "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", - "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==", - "dev": true + "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==" }, "node_modules/through2": { "version": "4.0.2", diff --git a/package.json b/package.json index 051dd4d45..079a82439 100644 --- a/package.json +++ b/package.json @@ -39,9 +39,9 @@ "@azure/storage-blob": "^12.5.0", "@casl/ability": "^6.5.0", "@casl/mongoose": "^7.2.1", + "@graphql-tools/schema": "^10.0.0", "@turf/helpers": "^6.5.0", "@turf/turf": "^6.5.0", - "@graphql-tools/schema": "^10.0.0", "@ucast/mongo2js": "^1.3.3", "amqplib": "^0.8.0", "axios": "^1.4.0", @@ -61,6 +61,7 @@ "express-rate-limit": "^6.4.0", "express-session": "^1.17.3", "express-winston": "^4.2.0", + "geojson-random": "^0.5.0", "graphql": "^16.6.0", "graphql-amqp-subscriptions": "^1.2.1", "graphql-scalars": "^1.22.2", diff --git a/src/schema/mutation/generateRecords.mutation.ts b/src/schema/mutation/generateRecords.mutation.ts new file mode 100644 index 000000000..19bc06ace --- /dev/null +++ b/src/schema/mutation/generateRecords.mutation.ts @@ -0,0 +1,104 @@ +import { GraphQLID, GraphQLNonNull, GraphQLError, GraphQLList } from 'graphql'; +import GraphQLJSON from 'graphql-type-json'; +import { RecordType } from '../types'; +import { Form, Record, Notification, Channel } from '@models'; +import { + transformRecord, + getOwnership, + getNextId, + generateData, +} from '@utils/form'; +import extendAbilityForRecords from '@security/extendAbilityForRecords'; +import pubsub from '../../server/pubsub'; +import { getFormPermissionFilter } from '@utils/filter'; +import { logger } from '@services/logger.service'; +import { graphQLAuthCheck } from '@schema/shared'; +import { Types } from 'mongoose'; +import { Context } from '@server/apollo/context'; + +/** Arguments for the addRecord mutation */ +type GenerateRecordArgs = { + 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: new GraphQLList(RecordType), + args: { + form: { type: GraphQLID }, + data: { type: GraphQLJSON }, + }, + async resolve(parent, args: GenerateRecordArgs, context: Context) { + // check permissions etc + graphQLAuthCheck(context); + try { + const recordsNumber = args.data.recordsNumber; + const formId = args.form; + const fields = args.data.fieldsForm; + const user = context.user; + let records = []; + + const form = await Form.findById(formId); + if (!form) + throw new GraphQLError(context.i18next.t('common.errors.dataNotFound')); + + for (let i = 0; i < recordsNumber; i++) { + const { incrementalId, incID } = await getNextId( + String(form.resource ? form.resource : args.form) + ); + const generatedData = await generateData(fields, form.fields); + console.log(JSON.stringify(generatedData, null, 2)); + const record = new Record({ + incrementalId, + incID, + form: formId, + data: generatedData, + resource: form.resource ? form.resource : null, + createdBy: { + user: user._id, + 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, + }, + }); + records.push(record); + await record.save(); + } + // send the notification to the channel + return records; + } 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/mutation/index.ts b/src/schema/mutation/index.ts index a823192f8..31c0fd032 100644 --- a/src/schema/mutation/index.ts +++ b/src/schema/mutation/index.ts @@ -12,6 +12,7 @@ import deleteRecord from './deleteRecord.mutation'; import deleteRecords from './deleteRecords.mutation'; import convertRecord from './convertRecord.mutation'; import restoreRecord from './restoreRecord.mutation'; +import generateRecords from './generateRecords.mutation'; import addDashboard from './addDashboard.mutation'; import editDashboard from './editDashboard.mutation'; import deleteDashboard from './deleteDashboard.mutation'; @@ -104,6 +105,7 @@ const Mutation = new GraphQLObjectType({ addPositionAttributeCategory, addPullJob, addRecord, + generateRecords, addReferenceData, addRole, addRoleToUsers, diff --git a/src/utils/form/generateData.ts b/src/utils/form/generateData.ts new file mode 100644 index 000000000..4f5fbf9e6 --- /dev/null +++ b/src/utils/form/generateData.ts @@ -0,0 +1,407 @@ +import { faker } from '@faker-js/faker'; +import { Record, Application, Role, User } from '@models'; +var randomGeoJSON = require('geojson-random'); + +export const generateData = async (fields: any, structure: any) => { + let data = {}; + await Promise.all( + fields.map(async (field: any) => { + if (field.include) { + if (field.setDefault) { + data[field.field] = field.default; + } else { + const questionStructure = structure.find( + (x: any) => x.name === field.field + ); + switch (questionStructure.type) { + case 'text': + switch (field.option) { + case 'month': + data[field.field] = faker.date.month(); + break; + case 'password': + data[field.field] = faker.internet.password(); + break; + case 'range': + data[field.field] = faker.datatype.number({ + min: 0, + max: 100, + }); + break; + case 'week': + data[field.field] = + faker.datatype.number({ min: 2000, max: 2030 }) + + '-W' + + faker.datatype.number({ min: 1, max: 52 }); + break; + case 'sentence': + default: + data[field.field] = faker.lorem.sentence(); + break; + } + break; + case 'radiogroup': + case 'dropdown': + const questionSingleChoices = questionStructure.choices; + data[field.field] = + questionSingleChoices[ + faker.datatype.number({ + min: 0, + max: questionSingleChoices.length - 1, + }) + ].value; + break; + case 'tagbox': + case 'checkbox': + let choices = []; + const questionMultipleChoices = questionStructure.choices; + questionMultipleChoices.forEach((choice: any) => { + if (faker.datatype.boolean()) { + choices.push(choice.value); + } + }); + if (choices.length === 0) { + choices.push( + questionMultipleChoices[ + faker.datatype.number({ + min: 0, + max: questionMultipleChoices.length - 1, + }) + ].value + ); + } + data[field.field] = choices; + break; + case 'boolean': + data[field.field] = faker.datatype.boolean(); + break; + case 'multipletext': + let items = {}; + structure + .find((x: any) => x.name === field.field) + .items.forEach((item: any) => { + items[item.name] = faker.lorem.sentence(); + }); + data[field.field] = items; + break; + case 'matrix': + let matrixItems = {}; + questionStructure.rows.forEach((row: any) => { + matrixItems[row.name] = + questionStructure.columns[ + faker.datatype.number({ + min: 0, + max: questionStructure.columns.length - 1, + }) + ].name; + }); + data[field.field] = matrixItems; + break; + case 'matrixdropdown': + let matrixDropdownItems = {}; + questionStructure.rows.forEach((row: any) => { + matrixDropdownItems[row.name] = {}; + questionStructure.columns.forEach((column: any) => { + const columnChoices = + column.choices ?? questionStructure.choices; + switch (column.type) { + case null: + case 'dropdown': + case 'radiogroup': + matrixDropdownItems[row.name][column.name] = + columnChoices[ + faker.datatype.number({ + min: 0, + max: columnChoices.length - 1, + }) + ].value; + break; + case 'checkbox': + case 'tagbox': + let choices = []; + columnChoices.forEach((choice: any) => { + if (faker.datatype.boolean()) { + choices.push(choice.value); + } + }); + if (choices.length === 0) { + choices.push( + columnChoices[ + faker.datatype.number({ + min: 0, + max: columnChoices.length - 1, + }) + ].value + ); + } + matrixDropdownItems[row.name][column.name] = choices; + break; + case 'boolean': + matrixDropdownItems[row.name][column.name] = + faker.datatype.boolean(); + break; + case 'text': + case 'comment': + matrixDropdownItems[row.name][column.name] = + faker.lorem.sentence(); + break; + case 'rating': + matrixDropdownItems[row.name][column.name] = + faker.datatype.number({ min: 1, max: 5 }); + break; + case 'expression': + // i dont know + break; + default: + break; + } + }); + }); + data[field.field] = matrixDropdownItems; + break; + case 'matrixdynamic': + let matrixDynamicItems = []; + for ( + let i = 0; + i < faker.datatype.number({ min: 1, max: 5 }); + i++ + ) { + let matrixDynamicItem = {}; + questionStructure.columns.forEach((column: any) => { + const columnChoices = + column.choices ?? questionStructure.choices; + switch (column.type) { + case null: + case 'dropdown': + case 'radiogroup': + matrixDynamicItem[column.name] = + columnChoices[ + faker.datatype.number({ + min: 0, + max: columnChoices.length - 1, + }) + ].value; + break; + case 'checkbox': + case 'tagbox': + let choices = []; + columnChoices.forEach((choice: any) => { + if (faker.datatype.boolean()) { + choices.push(choice.value); + } + }); + if (choices.length === 0) { + choices.push( + columnChoices[ + faker.datatype.number({ + min: 0, + max: columnChoices.length - 1, + }) + ].value + ); + } + matrixDynamicItem[column.name] = choices; + break; + case 'boolean': + matrixDynamicItem[column.name] = faker.datatype.boolean(); + break; + case 'text': + case 'comment': + matrixDynamicItem[column.name] = faker.lorem.sentence(); + break; + case 'rating': + matrixDynamicItem[column.name] = faker.datatype.number({ + min: 1, + max: 5, + }); + break; + case 'expression': + // i dont know + break; + default: + break; + } + }); + matrixDynamicItems.push(matrixDynamicItem); + } + data[field.field] = matrixDynamicItems; + break; + case 'expression': + // i dont know + break; + case 'resource': + const record = await Record.findOne({ + resource: questionStructure.resource, + archived: { $ne: true }, + }).skip( + faker.datatype.number({ + min: 0, + max: + (await Record.countDocuments({ + resource: questionStructure.resource, + archived: { $ne: true }, + })) - 1, + }) + ); + data[field.field] = record.id; + break; + case 'resources': + const records = await Record.find({ + resource: questionStructure.resource, + archived: { $ne: true }, + }).skip( + faker.datatype.number({ + min: 0, + max: + (await Record.countDocuments({ + resource: questionStructure.resource, + archived: { $ne: true }, + })) - 1, + }) + ); + data[field.field] = records.map((x) => x.id); + break; + case 'owner': + let roles = []; + // Using Promise.all to wait for all async operations to complete + await Promise.all( + questionStructure.applications?.map( + async (application: any) => { + const tempRoles = await Role.find({ + application, + }); + + tempRoles.forEach((role: any) => { + if (faker.datatype.boolean()) { + roles.push(role.id); + } + }); + if ( + questionStructure.isRequired && + roles.length === 0 && + tempRoles.length > 0 + ) { + roles.push( + tempRoles[ + faker.datatype.number({ + min: 0, + max: tempRoles.length - 1, + }) + ].id + ); + } + } + ) + ); + data[field.field] = roles; + break; + case 'users': + let users = []; + if (questionStructure.applications) { + await Promise.all( + questionStructure.applications.map( + async (application: any) => { + const tempRoles = await Role.find({ + application, + }); + for (const role of tempRoles) { + const tempUsers = await User.find({ + roles: role.id, + }); + for (const user of tempUsers) { + if ( + faker.datatype.boolean() && + !users.includes(user.id) + ) { + users.push(user.id); + } + } + if ( + questionStructure.isRequired && + users.length === 0 && + tempUsers.length > 0 + ) { + users.push( + tempUsers[ + faker.datatype.number({ + min: 0, + max: tempUsers.length - 1, + }) + ].id + ); + } + } + } + ) + ); + } else { + const tempUsers = await User.find(); + for (const user of tempUsers) { + if (faker.datatype.boolean()) { + users.push(user.id); + } + } + if ( + questionStructure.isRequired && + users.length === 0 && + tempUsers.length > 0 + ) { + users.push( + tempUsers[ + faker.datatype.number({ + min: 0, + max: tempUsers.length - 1, + }) + ].id + ); + } + } + data[field.field] = users; + break; + case 'geospatial': + data[field.field] = randomGeoJSON.point(1).features[0]; + break; + case 'color': + data[field.field] = faker.internet.color(); + break; + case 'date': + data[field.field] = new Date( + faker.date + .between( + '2020-01-01T00:00:00.000Z', + '2030-01-01T00:00:00.000Z' + ) + .setHours(0, 0, 0, 0) + ).toISOString(); + break; + case 'datetime-local': + data[field.field] = faker.date + .between('2020-01-01T00:00:00.000Z', '2030-01-01T00:00:00.000Z') + .toISOString(); + break; + case 'email': + data[field.field] = faker.internet.email(); + break; + case 'numeric': + data[field.field] = faker.datatype.number({ min: 0, max: 1000 }); + break; + case 'tel': + data[field.field] = faker.phone.number(); + break; + case 'time': + data[field.field] = faker.date + .between('2020-01-01T00:00:00.000Z', '2030-01-01T00:00:00.000Z') + .toISOString(); + break; + case 'url': + data[field.field] = faker.internet.url(); + break; + default: + break; + } + } + } + }) + ); + return data; +}; diff --git a/src/utils/form/index.ts b/src/utils/form/index.ts index f7d424a8c..5365f87e2 100644 --- a/src/utils/form/index.ts +++ b/src/utils/form/index.ts @@ -14,3 +14,4 @@ export * from './getDisplayText'; export * from './checkRecordValidation'; export * from './getAccessibleFields'; export * from './checkRecordTriggers'; +export * from './generateData'; From 3e738c84b640d89aed29daae8f49a55017637071 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bruno=20Tann=C3=BAs?= <39497117+brunotannus@users.noreply.github.com> Date: Fri, 16 Feb 2024 14:02:43 -0300 Subject: [PATCH 2/5] second working version, everything seems to be working --- .../mutation/generateRecords.mutation.ts | 47 ++++++++----- src/utils/form/generateData.ts | 66 +++++++++++++++---- src/utils/form/getMatrixFieldStructure.ts | 32 +++++++++ 3 files changed, 117 insertions(+), 28 deletions(-) create mode 100644 src/utils/form/getMatrixFieldStructure.ts diff --git a/src/schema/mutation/generateRecords.mutation.ts b/src/schema/mutation/generateRecords.mutation.ts index 19bc06ace..428c4fad7 100644 --- a/src/schema/mutation/generateRecords.mutation.ts +++ b/src/schema/mutation/generateRecords.mutation.ts @@ -1,31 +1,23 @@ -import { GraphQLID, GraphQLNonNull, GraphQLError, GraphQLList } from 'graphql'; +import { GraphQLID, GraphQLError, GraphQLList } from 'graphql'; import GraphQLJSON from 'graphql-type-json'; import { RecordType } from '../types'; import { Form, Record, Notification, Channel } from '@models'; -import { - transformRecord, - getOwnership, - getNextId, - generateData, -} from '@utils/form'; +import { getOwnership, getNextId, generateData } from '@utils/form'; import extendAbilityForRecords from '@security/extendAbilityForRecords'; import pubsub from '../../server/pubsub'; -import { getFormPermissionFilter } from '@utils/filter'; import { logger } from '@services/logger.service'; import { graphQLAuthCheck } from '@schema/shared'; import { Types } from 'mongoose'; import { Context } from '@server/apollo/context'; -/** Arguments for the addRecord mutation */ +/** Arguments for the generateRecords mutation */ type GenerateRecordArgs = { 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. + * Generate up to 50 records using user input or random data */ export default { type: new GraphQLList(RecordType), @@ -47,12 +39,19 @@ export default { 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') + ); + } + for (let i = 0; i < recordsNumber; i++) { const { incrementalId, incID } = await getNextId( String(form.resource ? form.resource : args.form) ); - const generatedData = await generateData(fields, form.fields); - console.log(JSON.stringify(generatedData, null, 2)); + const generatedData = await generateData(fields, form); const record = new Record({ incrementalId, incID, @@ -87,9 +86,27 @@ export default { }, }); records.push(record); + // 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(); } - // send the notification to the channel + + // send notifications to channel + const channel = await Channel.findOne({ form: form._id }); + if (channel) { + const notification = new Notification({ + action: `${recordsNumber} generated records - ${form.name}`, + content: records, + channel: channel.id, + seenBy: [], + }); + await notification.save(); + const publisher = await pubsub(); + publisher.publish(channel.id, { notification }); + } return records; } catch (err) { logger.error(err.message, { stack: err.stack }); diff --git a/src/utils/form/generateData.ts b/src/utils/form/generateData.ts index 4f5fbf9e6..52b4ad02d 100644 --- a/src/utils/form/generateData.ts +++ b/src/utils/form/generateData.ts @@ -1,9 +1,12 @@ import { faker } from '@faker-js/faker'; -import { Record, Application, Role, User } from '@models'; +import { Record, Role, User } from '@models'; +import { getMatrixFieldStructure } from './getMatrixFieldStructure'; var randomGeoJSON = require('geojson-random'); - -export const generateData = async (fields: any, structure: any) => { +/** Generate data for one record */ +export const generateData = async (fields: any, form: any) => { let data = {}; + const structure = form.fields; + const structureForMatrix = form.structure; await Promise.all( fields.map(async (field: any) => { if (field.include) { @@ -17,7 +20,13 @@ export const generateData = async (fields: any, structure: any) => { case 'text': switch (field.option) { case 'month': - data[field.field] = faker.date.month(); + data[field.field] = faker.date + .between( + '2020-01-01T00:00:00.000Z', + '2030-01-01T00:00:00.000Z' + ) + .toISOString() + .slice(0, 7); break; case 'password': data[field.field] = faker.internet.password(); @@ -101,11 +110,26 @@ export const generateData = async (fields: any, structure: any) => { let matrixDropdownItems = {}; questionStructure.rows.forEach((row: any) => { matrixDropdownItems[row.name] = {}; - questionStructure.columns.forEach((column: any) => { + getMatrixFieldStructure( + field.field, + structureForMatrix + ).columns.forEach((column: any) => { + const matrixDropdownChoices = column.choices?.map((item) => ({ + value: item, + })); const columnChoices = - column.choices ?? questionStructure.choices; - switch (column.type) { + matrixDropdownChoices ?? questionStructure.choices; + switch (column.cellType) { case null: + case undefined: + matrixDropdownItems[row.name][column.name] = + questionStructure.choices[ + faker.datatype.number({ + min: 0, + max: questionStructure.choices.length - 1, + }) + ].value; + break; case 'dropdown': case 'radiogroup': matrixDropdownItems[row.name][column.name] = @@ -167,11 +191,26 @@ export const generateData = async (fields: any, structure: any) => { i++ ) { let matrixDynamicItem = {}; - questionStructure.columns.forEach((column: any) => { + getMatrixFieldStructure( + field.field, + structureForMatrix + ).columns.forEach((column: any) => { + const matrixDynamicChoices = column.choices?.map((item) => ({ + value: item, + })); const columnChoices = - column.choices ?? questionStructure.choices; - switch (column.type) { + matrixDynamicChoices ?? questionStructure.choices; + switch (column.cellType) { case null: + case undefined: + matrixDynamicItem[column.name] = + questionStructure.choices[ + faker.datatype.number({ + min: 0, + max: questionStructure.choices.length - 1, + }) + ].value; + break; case 'dropdown': case 'radiogroup': matrixDynamicItem[column.name] = @@ -389,9 +428,10 @@ export const generateData = async (fields: any, structure: any) => { data[field.field] = faker.phone.number(); break; case 'time': - data[field.field] = faker.date - .between('2020-01-01T00:00:00.000Z', '2030-01-01T00:00:00.000Z') - .toISOString(); + data[field.field] = + new Date(0).toISOString().slice(0, 11) + + faker.datatype.datetime().toISOString().slice(11, 16) + + ':00.000Z'; break; case 'url': data[field.field] = faker.internet.url(); diff --git a/src/utils/form/getMatrixFieldStructure.ts b/src/utils/form/getMatrixFieldStructure.ts new file mode 100644 index 000000000..bd1fdfcf3 --- /dev/null +++ b/src/utils/form/getMatrixFieldStructure.ts @@ -0,0 +1,32 @@ +import { result } from 'lodash'; + +/** + * Function to get a survey structure with 1 field given it's name + * + * @param fieldName Field name + * @returns The survey structure + */ +export const getMatrixFieldStructure = ( + fieldName: string, + structure: any +): any => { + let resultStructure: any = {}; + + function traverse(structure: any) { + if (structure && typeof structure === 'object') { + if (structure.hasOwnProperty('name') && structure.name === fieldName) { + resultStructure = structure; + } + + for (const key in structure) { + if (structure.hasOwnProperty(key)) { + traverse(structure[key]); + } + } + } + } + + traverse(JSON.parse(structure ?? '')); + + return resultStructure; +}; From aa222cb1f973b1eb5253bf9687890db206b0ed28 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bruno=20Tann=C3=BAs?= <39497117+brunotannus@users.noreply.github.com> Date: Mon, 19 Feb 2024 16:52:32 -0300 Subject: [PATCH 3/5] Dynamic panels, backend reestructure --- package-lock.json | 35 +- package.json | 1 - src/utils/form/generateData.ts | 788 +++++++++++----------- src/utils/form/getMatrixFieldStructure.ts | 32 - 4 files changed, 403 insertions(+), 453 deletions(-) delete mode 100644 src/utils/form/getMatrixFieldStructure.ts diff --git a/package-lock.json b/package-lock.json index 9398a417a..298003627 100644 --- a/package-lock.json +++ b/package-lock.json @@ -36,7 +36,6 @@ "express-rate-limit": "^6.4.0", "express-session": "^1.17.3", "express-winston": "^4.2.0", - "geojson-random": "^0.5.0", "graphql": "^16.6.0", "graphql-amqp-subscriptions": "^1.2.1", "graphql-scalars": "^1.22.2", @@ -9256,6 +9255,7 @@ "version": "2.3.0", "resolved": "https://registry.npmjs.org/from2/-/from2-2.3.0.tgz", "integrity": "sha512-OMcX/4IC/uqEPVgGeyfN22LJk6AZrMkRZHxcHBMBvHScDGgwTm2GT2Wkgtocyd3JfZffjj2kYUDXXII0Fk9W0g==", + "dev": true, "dependencies": { "inherits": "^2.0.1", "readable-stream": "^2.0.0" @@ -9264,12 +9264,14 @@ "node_modules/from2/node_modules/isarray": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==" + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "dev": true }, "node_modules/from2/node_modules/readable-stream": { "version": "2.3.8", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dev": true, "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", @@ -9283,12 +9285,14 @@ "node_modules/from2/node_modules/safe-buffer": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true }, "node_modules/from2/node_modules/string_decoder": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, "dependencies": { "safe-buffer": "~5.1.0" } @@ -9404,18 +9408,6 @@ "deep-equal": "^1.0.0" } }, - "node_modules/geojson-random": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/geojson-random/-/geojson-random-0.5.0.tgz", - "integrity": "sha512-a4j6KJCC/ZhufwiXMxuoXrJiOxwwBQ0Y0DEcGmytnSJC11AkWHSKSFOw/ovh6QFp8n9XIDXhQXAvArRCvi2Y4A==", - "dependencies": { - "from2": "^2.1.0", - "geojson-stream": "0.1.0" - }, - "bin": { - "geojson-random": "geojson-random" - } - }, "node_modules/geojson-rbush": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/geojson-rbush/-/geojson-rbush-3.2.0.tgz", @@ -9441,15 +9433,6 @@ "quickselect": "^2.0.0" } }, - "node_modules/geojson-stream": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/geojson-stream/-/geojson-stream-0.1.0.tgz", - "integrity": "sha512-svSg5fFXPaTiqzEBGXScA+nISaeC9rLvku2PH+wM5LToATUw2bLIrvls43ymnT9Xnp51nBPVyK9m4Af40KpJ7w==", - "dependencies": { - "JSONStream": "^1.0.0", - "through": "^2.3.4" - } - }, "node_modules/get-caller-file": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", @@ -11664,6 +11647,7 @@ "version": "1.3.5", "resolved": "https://registry.npmjs.org/JSONStream/-/JSONStream-1.3.5.tgz", "integrity": "sha512-E+iruNOY8VV9s4JEbe1aNEm6MiszPRr/UfcHMz0TQh1BXSxHK+ASV1R6W4HpjBhSeS+54PIsAMCBmwD06LLsqQ==", + "dev": true, "dependencies": { "jsonparse": "^1.2.0", "through": ">=2.2.7 <3" @@ -22830,7 +22814,8 @@ "node_modules/through": { "version": "2.3.8", "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", - "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==" + "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==", + "dev": true }, "node_modules/through2": { "version": "4.0.2", diff --git a/package.json b/package.json index 079a82439..0e273b070 100644 --- a/package.json +++ b/package.json @@ -61,7 +61,6 @@ "express-rate-limit": "^6.4.0", "express-session": "^1.17.3", "express-winston": "^4.2.0", - "geojson-random": "^0.5.0", "graphql": "^16.6.0", "graphql-amqp-subscriptions": "^1.2.1", "graphql-scalars": "^1.22.2", diff --git a/src/utils/form/generateData.ts b/src/utils/form/generateData.ts index 52b4ad02d..c02263cee 100644 --- a/src/utils/form/generateData.ts +++ b/src/utils/form/generateData.ts @@ -1,382 +1,352 @@ import { faker } from '@faker-js/faker'; import { Record, Role, User } from '@models'; -import { getMatrixFieldStructure } from './getMatrixFieldStructure'; -var randomGeoJSON = require('geojson-random'); /** Generate data for one record */ export const generateData = async (fields: any, form: any) => { let data = {}; - const structure = form.fields; - const structureForMatrix = form.structure; - await Promise.all( - fields.map(async (field: any) => { - if (field.include) { - if (field.setDefault) { - data[field.field] = field.default; - } else { - const questionStructure = structure.find( - (x: any) => x.name === field.field + let fieldUserAction = ''; + const questionsStructure = JSON.parse(form.structure).pages.reduce( + (acc: any, page: any) => acc.concat(page.elements), + [] + ); + console.log('struc', JSON.stringify(questionsStructure, null, 2)); + /** Generate data for one field */ + const _generateFieldData = async (questionStructure: any): Promise => { + switch (questionStructure.type) { + case 'text': + switch (questionStructure.inputType) { + case 'color': + return faker.internet.color(); + case 'date': + return new Date( + faker.date + .between('2020-01-01T00:00:00.000Z', '2030-01-01T00:00:00.000Z') + .setHours(0, 0, 0, 0) + ).toISOString(); + case 'datetime-local': + return faker.date + .between('2020-01-01T00:00:00.000Z', '2030-01-01T00:00:00.000Z') + .toISOString(); + case 'email': + return faker.internet.email(); + case 'number': + return faker.datatype.number({ min: 0, max: 1000 }); + case 'tel': + return faker.phone.number(); + case 'time': + return ( + new Date(0).toISOString().slice(0, 11) + + faker.datatype.datetime().toISOString().slice(11, 16) + + ':00.000Z' + ); + case 'url': + return faker.internet.url(); + case 'month': + return faker.date + .between('2020-01-01T00:00:00.000Z', '2030-01-01T00:00:00.000Z') + .toISOString() + .slice(0, 7); + case 'password': + return faker.internet.password(); + case 'range': + return faker.datatype.number({ + min: 0, + max: 100, + }); + case 'week': + return ( + faker.datatype.number({ min: 2000, max: 2030 }) + + '-W' + + faker.datatype.number({ min: 1, max: 52 }) + ); + case undefined: + default: + return faker.lorem.sentence(); + } + case 'comment': + return faker.lorem.paragraph(); + case 'radiogroup': + case 'dropdown': + // If the choices are set with values != texts, we only want the values + // If the choices are set with values == texts, choices is an array of strings + const questionSingleChoices = questionStructure.choices.map((item) => + typeof item === 'object' ? item.value : item + ); + return questionSingleChoices[ + faker.datatype.number({ + min: 0, + max: questionSingleChoices.length - 1, + }) + ]; + case 'tagbox': + case 'checkbox': + let choices = []; + const questionMultipleChoices = questionStructure.choices.map((item) => + typeof item === 'object' ? item.value : item + ); + questionMultipleChoices.forEach((choice: any) => { + if (faker.datatype.boolean()) { + choices.push(choice); + } + }); + if (choices.length === 0) { + choices.push( + questionMultipleChoices[ + faker.datatype.number({ + min: 0, + max: questionMultipleChoices.length - 1, + }) + ] ); - switch (questionStructure.type) { - case 'text': - switch (field.option) { - case 'month': - data[field.field] = faker.date - .between( - '2020-01-01T00:00:00.000Z', - '2030-01-01T00:00:00.000Z' - ) - .toISOString() - .slice(0, 7); - break; - case 'password': - data[field.field] = faker.internet.password(); - break; - case 'range': - data[field.field] = faker.datatype.number({ - min: 0, - max: 100, - }); - break; - case 'week': - data[field.field] = - faker.datatype.number({ min: 2000, max: 2030 }) + - '-W' + - faker.datatype.number({ min: 1, max: 52 }); - break; - case 'sentence': - default: - data[field.field] = faker.lorem.sentence(); - break; - } - break; - case 'radiogroup': - case 'dropdown': - const questionSingleChoices = questionStructure.choices; - data[field.field] = - questionSingleChoices[ + } + return choices; + case 'boolean': + return faker.datatype.boolean(); + case 'multipletext': + let items = {}; + questionStructure.items.forEach((item: any) => { + items[item.name] = faker.lorem.sentence(); + }); + return items; + case 'matrix': + let matrixItems = {}; + questionStructure.rows.forEach((row: any) => { + matrixItems[row.value] = + questionStructure.columns[ + faker.datatype.number({ + min: 0, + max: questionStructure.columns.length - 1, + }) + ].value; + }); + return matrixItems; + case 'matrixdropdown': + let matrixDropdownItems = {}; + const questionChoices1 = questionStructure.choices.map((item) => + typeof item === 'object' ? item.value : item + ); + questionStructure.rows.forEach((row: any) => { + matrixDropdownItems[row.value] = {}; + questionStructure.columns.forEach((column: any) => { + const matrixDropdownChoices = column.choices?.map((item) => + typeof item === 'object' ? item.value : item + ); + // If choices are set for the column, use them, otherwise use the choices set for the question + const columnChoices = matrixDropdownChoices ?? questionChoices1; + switch (column.cellType) { + case null: + case undefined: // Undefined(default) is a dropdown which always uses the question choices + matrixDropdownItems[row.value][column.name] = questionChoices1[ faker.datatype.number({ min: 0, - max: questionSingleChoices.length - 1, + max: questionChoices1.length - 1, }) - ].value; - break; - case 'tagbox': - case 'checkbox': - let choices = []; - const questionMultipleChoices = questionStructure.choices; - questionMultipleChoices.forEach((choice: any) => { - if (faker.datatype.boolean()) { - choices.push(choice.value); - } - }); - if (choices.length === 0) { - choices.push( - questionMultipleChoices[ + ].map((item) => (typeof item === 'object' ? item.value : item)); + break; + case 'dropdown': + case 'radiogroup': + matrixDropdownItems[row.value][column.name] = + columnChoices[ faker.datatype.number({ min: 0, - max: questionMultipleChoices.length - 1, + max: columnChoices.length - 1, }) - ].value - ); - } - data[field.field] = choices; - break; - case 'boolean': - data[field.field] = faker.datatype.boolean(); - break; - case 'multipletext': - let items = {}; - structure - .find((x: any) => x.name === field.field) - .items.forEach((item: any) => { - items[item.name] = faker.lorem.sentence(); + ]; + break; + case 'checkbox': + case 'tagbox': + let choices = []; + columnChoices.forEach((choice: any) => { + if (faker.datatype.boolean()) { + choices.push(choice); + } }); - data[field.field] = items; - break; - case 'matrix': - let matrixItems = {}; - questionStructure.rows.forEach((row: any) => { - matrixItems[row.name] = - questionStructure.columns[ + if (choices.length === 0) { + choices.push( + columnChoices[ + faker.datatype.number({ + min: 0, + max: columnChoices.length - 1, + }) + ] + ); + } + matrixDropdownItems[row.value][column.name] = choices; + break; + case 'boolean': + matrixDropdownItems[row.value][column.name] = + faker.datatype.boolean(); + break; + case 'text': + case 'comment': + matrixDropdownItems[row.value][column.name] = + faker.lorem.sentence(); + break; + case 'rating': + matrixDropdownItems[row.value][column.name] = + faker.datatype.number({ min: 1, max: 5 }); + break; + case 'expression': + // i dont know + default: + break; + } + }); + }); + return matrixDropdownItems; + case 'matrixdynamic': + let matrixDynamicItems = []; + const questionChoices2 = questionStructure.choices.map((item) => + typeof item === 'object' ? item.value : item + ); + // Since we don't know the number of rows, we'll generate a random number of rows between 1 and 5 + for (let i = 0; i < faker.datatype.number({ min: 1, max: 5 }); i++) { + let matrixDynamicItem = {}; + questionStructure.columns.forEach((column: any) => { + const matrixDynamicChoices = column.choices?.map((item) => + typeof item === 'object' ? item.value : item + ); + const columnChoices = matrixDynamicChoices ?? questionChoices2; + switch (column.cellType) { + case null: + case undefined: + matrixDynamicItem[column.name] = + questionChoices2[ faker.datatype.number({ min: 0, - max: questionStructure.columns.length - 1, + max: questionChoices2.length - 1, }) - ].name; - }); - data[field.field] = matrixItems; - break; - case 'matrixdropdown': - let matrixDropdownItems = {}; - questionStructure.rows.forEach((row: any) => { - matrixDropdownItems[row.name] = {}; - getMatrixFieldStructure( - field.field, - structureForMatrix - ).columns.forEach((column: any) => { - const matrixDropdownChoices = column.choices?.map((item) => ({ - value: item, - })); - const columnChoices = - matrixDropdownChoices ?? questionStructure.choices; - switch (column.cellType) { - case null: - case undefined: - matrixDropdownItems[row.name][column.name] = - questionStructure.choices[ - faker.datatype.number({ - min: 0, - max: questionStructure.choices.length - 1, - }) - ].value; - break; - case 'dropdown': - case 'radiogroup': - matrixDropdownItems[row.name][column.name] = - columnChoices[ - faker.datatype.number({ - min: 0, - max: columnChoices.length - 1, - }) - ].value; - break; - case 'checkbox': - case 'tagbox': - let choices = []; - columnChoices.forEach((choice: any) => { - if (faker.datatype.boolean()) { - choices.push(choice.value); - } - }); - if (choices.length === 0) { - choices.push( - columnChoices[ - faker.datatype.number({ - min: 0, - max: columnChoices.length - 1, - }) - ].value - ); - } - matrixDropdownItems[row.name][column.name] = choices; - break; - case 'boolean': - matrixDropdownItems[row.name][column.name] = - faker.datatype.boolean(); - break; - case 'text': - case 'comment': - matrixDropdownItems[row.name][column.name] = - faker.lorem.sentence(); - break; - case 'rating': - matrixDropdownItems[row.name][column.name] = - faker.datatype.number({ min: 1, max: 5 }); - break; - case 'expression': - // i dont know - break; - default: - break; + ]; + break; + case 'dropdown': + case 'radiogroup': + matrixDynamicItem[column.name] = + columnChoices[ + faker.datatype.number({ + min: 0, + max: columnChoices.length - 1, + }) + ]; + break; + case 'checkbox': + case 'tagbox': + let choices = []; + columnChoices.forEach((choice: any) => { + if (faker.datatype.boolean()) { + choices.push(choice); } }); - }); - data[field.field] = matrixDropdownItems; - break; - case 'matrixdynamic': - let matrixDynamicItems = []; - for ( - let i = 0; - i < faker.datatype.number({ min: 1, max: 5 }); - i++ - ) { - let matrixDynamicItem = {}; - getMatrixFieldStructure( - field.field, - structureForMatrix - ).columns.forEach((column: any) => { - const matrixDynamicChoices = column.choices?.map((item) => ({ - value: item, - })); - const columnChoices = - matrixDynamicChoices ?? questionStructure.choices; - switch (column.cellType) { - case null: - case undefined: - matrixDynamicItem[column.name] = - questionStructure.choices[ - faker.datatype.number({ - min: 0, - max: questionStructure.choices.length - 1, - }) - ].value; - break; - case 'dropdown': - case 'radiogroup': - matrixDynamicItem[column.name] = - columnChoices[ - faker.datatype.number({ - min: 0, - max: columnChoices.length - 1, - }) - ].value; - break; - case 'checkbox': - case 'tagbox': - let choices = []; - columnChoices.forEach((choice: any) => { - if (faker.datatype.boolean()) { - choices.push(choice.value); - } - }); - if (choices.length === 0) { - choices.push( - columnChoices[ - faker.datatype.number({ - min: 0, - max: columnChoices.length - 1, - }) - ].value - ); - } - matrixDynamicItem[column.name] = choices; - break; - case 'boolean': - matrixDynamicItem[column.name] = faker.datatype.boolean(); - break; - case 'text': - case 'comment': - matrixDynamicItem[column.name] = faker.lorem.sentence(); - break; - case 'rating': - matrixDynamicItem[column.name] = faker.datatype.number({ - min: 1, - max: 5, - }); - break; - case 'expression': - // i dont know - break; - default: - break; - } + if (choices.length === 0) { + choices.push( + columnChoices[ + faker.datatype.number({ + min: 0, + max: columnChoices.length - 1, + }) + ] + ); + } + matrixDynamicItem[column.name] = choices; + break; + case 'boolean': + matrixDynamicItem[column.name] = faker.datatype.boolean(); + break; + case 'text': + case 'comment': + matrixDynamicItem[column.name] = faker.lorem.sentence(); + break; + case 'rating': + matrixDynamicItem[column.name] = faker.datatype.number({ + min: 1, + max: 5, }); - matrixDynamicItems.push(matrixDynamicItem); - } - data[field.field] = matrixDynamicItems; - break; - case 'expression': + break; + case 'expression': // i dont know - break; - case 'resource': - const record = await Record.findOne({ + default: + break; + } + }); + matrixDynamicItems.push(matrixDynamicItem); + } + return matrixDynamicItems; + case 'expression': + // i dont know + case 'resource': + const record = await Record.findOne({ + resource: questionStructure.resource, + archived: { $ne: true }, + }).skip( + faker.datatype.number({ + min: 0, + max: + (await Record.countDocuments({ resource: questionStructure.resource, archived: { $ne: true }, - }).skip( - faker.datatype.number({ - min: 0, - max: - (await Record.countDocuments({ - resource: questionStructure.resource, - archived: { $ne: true }, - })) - 1, - }) - ); - data[field.field] = record.id; - break; - case 'resources': - const records = await Record.find({ + })) - 1, + }) + ); + return record.id; + + case 'resources': + const records = await Record.find({ + resource: questionStructure.resource, + archived: { $ne: true }, + }).skip( + faker.datatype.number({ + min: 0, + max: + (await Record.countDocuments({ resource: questionStructure.resource, archived: { $ne: true }, - }).skip( - faker.datatype.number({ - min: 0, - max: - (await Record.countDocuments({ - resource: questionStructure.resource, - archived: { $ne: true }, - })) - 1, - }) - ); - data[field.field] = records.map((x) => x.id); - break; - case 'owner': - let roles = []; - // Using Promise.all to wait for all async operations to complete - await Promise.all( - questionStructure.applications?.map( - async (application: any) => { - const tempRoles = await Role.find({ - application, - }); + })) - 1, + }) + ); + return records.map((x) => x.id); - tempRoles.forEach((role: any) => { - if (faker.datatype.boolean()) { - roles.push(role.id); - } - }); - if ( - questionStructure.isRequired && - roles.length === 0 && - tempRoles.length > 0 - ) { - roles.push( - tempRoles[ - faker.datatype.number({ - min: 0, - max: tempRoles.length - 1, - }) - ].id - ); - } - } - ) + case 'owner': + let roles = []; + await Promise.all( + questionStructure.applications?.map(async (application: any) => { + const tempRoles = await Role.find({ + application, + }); + tempRoles.forEach((role: any) => { + if (faker.datatype.boolean()) { + roles.push(role.id); + } + }); + if ( + questionStructure.isRequired && // If the question is required we push a random role if none are generated + roles.length === 0 && + tempRoles.length > 0 + ) { + roles.push( + tempRoles[ + faker.datatype.number({ + min: 0, + max: tempRoles.length - 1, + }) + ].id ); - data[field.field] = roles; - break; - case 'users': - let users = []; - if (questionStructure.applications) { - await Promise.all( - questionStructure.applications.map( - async (application: any) => { - const tempRoles = await Role.find({ - application, - }); - for (const role of tempRoles) { - const tempUsers = await User.find({ - roles: role.id, - }); - for (const user of tempUsers) { - if ( - faker.datatype.boolean() && - !users.includes(user.id) - ) { - users.push(user.id); - } - } - if ( - questionStructure.isRequired && - users.length === 0 && - tempUsers.length > 0 - ) { - users.push( - tempUsers[ - faker.datatype.number({ - min: 0, - max: tempUsers.length - 1, - }) - ].id - ); - } - } - } - ) - ); - } else { - const tempUsers = await User.find(); + } + }) + ); + return roles; + case 'users': + let users = []; + if (questionStructure.applications) { + // If an application is set in the question, we only get users with roles in that application + await Promise.all( + questionStructure.applications.map(async (application: any) => { + const tempRoles = await Role.find({ + application, + }); + for (const role of tempRoles) { + const tempUsers = await User.find({ + roles: role.id, + }); for (const user of tempUsers) { - if (faker.datatype.boolean()) { + if (faker.datatype.boolean() && !users.includes(user.id)) { users.push(user.id); } } @@ -395,53 +365,81 @@ export const generateData = async (fields: any, form: any) => { ); } } - data[field.field] = users; - break; - case 'geospatial': - data[field.field] = randomGeoJSON.point(1).features[0]; - break; - case 'color': - data[field.field] = faker.internet.color(); - break; - case 'date': - data[field.field] = new Date( - faker.date - .between( - '2020-01-01T00:00:00.000Z', - '2030-01-01T00:00:00.000Z' - ) - .setHours(0, 0, 0, 0) - ).toISOString(); - break; - case 'datetime-local': - data[field.field] = faker.date - .between('2020-01-01T00:00:00.000Z', '2030-01-01T00:00:00.000Z') - .toISOString(); - break; - case 'email': - data[field.field] = faker.internet.email(); - break; - case 'numeric': - data[field.field] = faker.datatype.number({ min: 0, max: 1000 }); - break; - case 'tel': - data[field.field] = faker.phone.number(); - break; - case 'time': - data[field.field] = - new Date(0).toISOString().slice(0, 11) + - faker.datatype.datetime().toISOString().slice(11, 16) + - ':00.000Z'; - break; - case 'url': - data[field.field] = faker.internet.url(); - break; - default: - break; + }) + ); + } else { + // If no application is set, we get any users + const tempUsers = await User.find(); + for (const user of tempUsers) { + if (faker.datatype.boolean()) { + users.push(user.id); + } } + if ( + questionStructure.isRequired && + users.length === 0 && + tempUsers.length > 0 + ) { + users.push( + tempUsers[ + faker.datatype.number({ + min: 0, + max: tempUsers.length - 1, + }) + ].id + ); + } + } + return users; + case 'geospatial': + return { + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [ + Number(faker.address.longitude()), + Number(faker.address.latitude()), + ], + }, + properties: {}, + }; + case 'paneldynamic': + let panelData = []; + for (let i = 0; i < faker.datatype.number({ min: 1, max: 5 }); i++) { + let panelItem = {}; + await Promise.all( + questionStructure.templateElements?.map( + async (panelQuestion: any) => { + panelItem[panelQuestion.name] = await _generateFieldData( + panelQuestion + ); + } + ) + ); + panelData.push(panelItem); + } + return panelData; + default: + return; + } + }; + await Promise.all( + fields.map(async (field: any) => { + const questionStructure = questionsStructure.find( + (obj: any) => obj.name === field.field + ); + const fieldUserAction = field.option; + if (field.include) { + if (field.setDefault) { + return field.default; // If default value is set, use it + } else { + data[questionStructure.name] = await _generateFieldData( + questionStructure + ); } } }) ); + console.log(JSON.stringify(data, null, 2)); return data; }; diff --git a/src/utils/form/getMatrixFieldStructure.ts b/src/utils/form/getMatrixFieldStructure.ts deleted file mode 100644 index bd1fdfcf3..000000000 --- a/src/utils/form/getMatrixFieldStructure.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { result } from 'lodash'; - -/** - * Function to get a survey structure with 1 field given it's name - * - * @param fieldName Field name - * @returns The survey structure - */ -export const getMatrixFieldStructure = ( - fieldName: string, - structure: any -): any => { - let resultStructure: any = {}; - - function traverse(structure: any) { - if (structure && typeof structure === 'object') { - if (structure.hasOwnProperty('name') && structure.name === fieldName) { - resultStructure = structure; - } - - for (const key in structure) { - if (structure.hasOwnProperty(key)) { - traverse(structure[key]); - } - } - } - } - - traverse(JSON.parse(structure ?? '')); - - return resultStructure; -}; From 3ecc300719691c186c2bdba0fd94e5ed85fa67aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bruno=20Tann=C3=BAs?= <39497117+brunotannus@users.noreply.github.com> Date: Tue, 20 Feb 2024 14:08:53 -0300 Subject: [PATCH 4/5] Expressions run ok --- src/utils/form/generateData.ts | 157 ++++++++++++++++++++------------- 1 file changed, 95 insertions(+), 62 deletions(-) diff --git a/src/utils/form/generateData.ts b/src/utils/form/generateData.ts index c02263cee..45fac2c67 100644 --- a/src/utils/form/generateData.ts +++ b/src/utils/form/generateData.ts @@ -3,65 +3,90 @@ import { Record, Role, User } from '@models'; /** Generate data for one record */ export const generateData = async (fields: any, form: any) => { let data = {}; - let fieldUserAction = ''; const questionsStructure = JSON.parse(form.structure).pages.reduce( (acc: any, page: any) => acc.concat(page.elements), [] ); - console.log('struc', JSON.stringify(questionsStructure, null, 2)); /** Generate data for one field */ - const _generateFieldData = async (questionStructure: any): Promise => { - switch (questionStructure.type) { + const _generateFieldData = async ( + questionStructure: any, + actions: any + ): Promise => { + const type = questionStructure.inputType ?? questionStructure.type; + switch (type) { case 'text': - switch (questionStructure.inputType) { - case 'color': - return faker.internet.color(); - case 'date': - return new Date( - faker.date - .between('2020-01-01T00:00:00.000Z', '2030-01-01T00:00:00.000Z') - .setHours(0, 0, 0, 0) - ).toISOString(); - case 'datetime-local': - return faker.date - .between('2020-01-01T00:00:00.000Z', '2030-01-01T00:00:00.000Z') - .toISOString(); - case 'email': - return faker.internet.email(); - case 'number': - return faker.datatype.number({ min: 0, max: 1000 }); - case 'tel': - return faker.phone.number(); - case 'time': - return ( - new Date(0).toISOString().slice(0, 11) + - faker.datatype.datetime().toISOString().slice(11, 16) + - ':00.000Z' - ); - case 'url': - return faker.internet.url(); - case 'month': - return faker.date - .between('2020-01-01T00:00:00.000Z', '2030-01-01T00:00:00.000Z') - .toISOString() - .slice(0, 7); - case 'password': - return faker.internet.password(); - case 'range': - return faker.datatype.number({ - min: 0, - max: 100, - }); - case 'week': - return ( - faker.datatype.number({ min: 2000, max: 2030 }) + - '-W' + - faker.datatype.number({ min: 1, max: 52 }) - ); - case undefined: - default: - return faker.lorem.sentence(); + return faker.lorem.sentence(); + case 'color': + return faker.internet.color(); + case 'date': + return ( + new Date( + faker.date.between( + actions.minDate ?? '2020-01-01T00:00:00.000Z', + actions.maxDate ?? '2030-01-01T00:00:00.000Z' + ) + ) + .toISOString() + .slice(0, 10) + 'T00:00:00.000Z' + ); + case 'datetime-local': + if (actions.maxDate) { + return new Date( + faker.date.between( + actions.minDate ?? '2020-01-01T00:00:00.000Z', + actions.maxDate + 'T23:59:00.000Z' + ) + ).toISOString(); } + return new Date( + faker.date.between( + actions.minDate ?? '2020-01-01T00:00:00.000Z', + '2030-01-01T00:00:00.000Z' + ) + ).toISOString(); + case 'email': + return faker.internet.email(); + case 'number': + return faker.datatype.number({ + min: actions.minNumber ?? 0, + max: actions.maxNumber ?? 1000, + }); + case 'tel': + return faker.phone.number(); + case 'time': + return ( + '1970-01-01T' + + new Date( + faker.date.between( + actions.minTime ?? '1970-01-01T00:00:00.000Z', + actions.maxTime ?? '1970-01-01T23:59:00.000Z' + ) + ) + .toISOString() + .slice(11, 16) + + ':00.000Z' + ); + case 'url': + return faker.internet.url(); + case 'month': + return faker.date + .between('2020-01-01T00:00:00.000Z', '2030-01-01T00:00:00.000Z') + .toISOString() + .slice(0, 7); + case 'password': + return faker.internet.password(); + case 'range': + return faker.datatype.number({ + min: 0, + max: 100, + }); + case 'week': + return ( + faker.datatype.number({ min: 2000, max: 2030 }) + + '-W' + + faker.datatype.number({ min: 1, max: 52 }) + ); + case undefined: case 'comment': return faker.lorem.paragraph(); case 'radiogroup': @@ -186,7 +211,7 @@ export const generateData = async (fields: any, form: any) => { faker.datatype.number({ min: 1, max: 5 }); break; case 'expression': - // i dont know + break; default: break; } @@ -261,7 +286,7 @@ export const generateData = async (fields: any, form: any) => { }); break; case 'expression': - // i dont know + break; default: break; } @@ -270,7 +295,7 @@ export const generateData = async (fields: any, form: any) => { } return matrixDynamicItems; case 'expression': - // i dont know + return; case 'resource': const record = await Record.findOne({ resource: questionStructure.resource, @@ -411,7 +436,8 @@ export const generateData = async (fields: any, form: any) => { questionStructure.templateElements?.map( async (panelQuestion: any) => { panelItem[panelQuestion.name] = await _generateFieldData( - panelQuestion + panelQuestion, + {} ); } ) @@ -420,7 +446,7 @@ export const generateData = async (fields: any, form: any) => { } return panelData; default: - return; + return faker.lorem.sentence(); } }; await Promise.all( @@ -428,15 +454,22 @@ export const generateData = async (fields: any, form: any) => { const questionStructure = questionsStructure.find( (obj: any) => obj.name === field.field ); - const fieldUserAction = field.option; + const actions = { + minDate: field.minDate, + maxDate: field.maxDate, + minNumber: field.minNumber, + maxNumber: field.maxNumber, + minTime: field.minTime, + maxTime: field.maxTime, + }; if (field.include) { if (field.setDefault) { return field.default; // If default value is set, use it - } else { - data[questionStructure.name] = await _generateFieldData( - questionStructure - ); } + data[questionStructure.name] = await _generateFieldData( + questionStructure, + actions + ); } }) ); From 9dc443e956bd8060caea9217e64feb0f546df9fa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bruno=20Tann=C3=BAs?= <39497117+brunotannus@users.noreply.github.com> Date: Thu, 22 Feb 2024 15:14:52 -0300 Subject: [PATCH 5/5] Choices from another question and reference data now working. Linting not done yet. --- src/utils/form/generateData.ts | 107 +++++++++++++++++++++++---------- 1 file changed, 74 insertions(+), 33 deletions(-) diff --git a/src/utils/form/generateData.ts b/src/utils/form/generateData.ts index 45fac2c67..b7815f6da 100644 --- a/src/utils/form/generateData.ts +++ b/src/utils/form/generateData.ts @@ -1,5 +1,5 @@ import { faker } from '@faker-js/faker'; -import { Record, Role, User } from '@models'; +import { Record, Role, User, ReferenceData } from '@models'; /** Generate data for one record */ export const generateData = async (fields: any, form: any) => { let data = {}; @@ -7,12 +7,52 @@ export const generateData = async (fields: any, form: any) => { (acc: any, page: any) => acc.concat(page.elements), [] ); + const _getChoicesFromQuestion = async (question: any) => { + const questionToBeCopied = questionsStructure.find( + (obj: any) => obj.name === question + ); + if (questionToBeCopied.referenceData) { + const refData = await ReferenceData.findOne({ + _id: questionToBeCopied.referenceData, + }); + if (refData) { + return refData.data.map((item) => item[refData.valueField]); + } + } else if (questionToBeCopied.choices) { + return questionToBeCopied.choices?.map((item) => + typeof item === 'object' ? item.value : item + ); + } + return question.choices?.map((item) => + typeof item === 'object' ? item.value : item + ); + }; /** Generate data for one field */ const _generateFieldData = async ( questionStructure: any, actions: any ): Promise => { const type = questionStructure.inputType ?? questionStructure.type; + let questionChoices = []; + if (questionStructure.referenceData) { + const refData = await ReferenceData.findOne({ + _id: questionStructure.referenceData, + }); + if (refData) { + const refDataChoices = refData.data.map( + (item) => item[refData.valueField] + ); + questionChoices = refDataChoices; + } + } else if (questionStructure.choicesFromQuestion) { + questionChoices = await _getChoicesFromQuestion( + questionStructure.choicesFromQuestion + ); + } else if (questionStructure.choices) { + questionChoices = questionStructure.choices?.map((item) => + typeof item === 'object' ? item.value : item + ); + } switch (type) { case 'text': return faker.lorem.sentence(); @@ -93,32 +133,26 @@ export const generateData = async (fields: any, form: any) => { case 'dropdown': // If the choices are set with values != texts, we only want the values // If the choices are set with values == texts, choices is an array of strings - const questionSingleChoices = questionStructure.choices.map((item) => - typeof item === 'object' ? item.value : item - ); - return questionSingleChoices[ + return questionChoices[ faker.datatype.number({ min: 0, - max: questionSingleChoices.length - 1, + max: questionChoices.length - 1, }) ]; case 'tagbox': case 'checkbox': let choices = []; - const questionMultipleChoices = questionStructure.choices.map((item) => - typeof item === 'object' ? item.value : item - ); - questionMultipleChoices.forEach((choice: any) => { + questionChoices.forEach((choice: any) => { if (faker.datatype.boolean()) { choices.push(choice); } }); if (choices.length === 0) { choices.push( - questionMultipleChoices[ + questionChoices[ faker.datatype.number({ min: 0, - max: questionMultipleChoices.length - 1, + max: questionChoices.length - 1, }) ] ); @@ -146,24 +180,28 @@ export const generateData = async (fields: any, form: any) => { return matrixItems; case 'matrixdropdown': let matrixDropdownItems = {}; - const questionChoices1 = questionStructure.choices.map((item) => - typeof item === 'object' ? item.value : item - ); questionStructure.rows.forEach((row: any) => { matrixDropdownItems[row.value] = {}; - questionStructure.columns.forEach((column: any) => { - const matrixDropdownChoices = column.choices?.map((item) => - typeof item === 'object' ? item.value : item - ); + questionStructure.columns.forEach(async (column: any) => { + let matrixDropdownChoices = []; + if (column.choicesFromQuestion) { + matrixDropdownChoices = await _getChoicesFromQuestion( + column.choicesFromQuestion + ); + } else { + matrixDropdownChoices = column.choices?.map((item) => + typeof item === 'object' ? item.value : item + ); + } // If choices are set for the column, use them, otherwise use the choices set for the question - const columnChoices = matrixDropdownChoices ?? questionChoices1; + const columnChoices = matrixDropdownChoices ?? questionChoices; switch (column.cellType) { case null: case undefined: // Undefined(default) is a dropdown which always uses the question choices - matrixDropdownItems[row.value][column.name] = questionChoices1[ + matrixDropdownItems[row.value][column.name] = questionChoices[ faker.datatype.number({ min: 0, - max: questionChoices1.length - 1, + max: questionChoices.length - 1, }) ].map((item) => (typeof item === 'object' ? item.value : item)); break; @@ -220,25 +258,29 @@ export const generateData = async (fields: any, form: any) => { return matrixDropdownItems; case 'matrixdynamic': let matrixDynamicItems = []; - const questionChoices2 = questionStructure.choices.map((item) => - typeof item === 'object' ? item.value : item - ); // Since we don't know the number of rows, we'll generate a random number of rows between 1 and 5 for (let i = 0; i < faker.datatype.number({ min: 1, max: 5 }); i++) { let matrixDynamicItem = {}; - questionStructure.columns.forEach((column: any) => { - const matrixDynamicChoices = column.choices?.map((item) => - typeof item === 'object' ? item.value : item - ); - const columnChoices = matrixDynamicChoices ?? questionChoices2; + questionStructure.columns.forEach(async (column: any) => { + let matrixDynamicChoices = []; + if (column.choicesFromQuestion) { + matrixDynamicChoices = await _getChoicesFromQuestion( + column.choicesFromQuestion + ); + } else { + matrixDynamicChoices = column.choices?.map((item) => + typeof item === 'object' ? item.value : item + ); + } + const columnChoices = matrixDynamicChoices ?? questionChoices; switch (column.cellType) { case null: case undefined: matrixDynamicItem[column.name] = - questionChoices2[ + questionChoices[ faker.datatype.number({ min: 0, - max: questionChoices2.length - 1, + max: questionChoices.length - 1, }) ]; break; @@ -473,6 +515,5 @@ export const generateData = async (fields: any, form: any) => { } }) ); - console.log(JSON.stringify(data, null, 2)); return data; };