├── .devops ├── dev-CD.yml └── sit-CD.yml ├── .dockerignore ├── .env.dist ├── .eslintignore ├── .eslintrc ├── .github ├── .githooks │ ├── pre-commit │ ├── pre-push │ └── prepare-commit-msg ├── pull_request_template.md └── workflows │ ├── ci.yml │ ├── codeql-analysis.yml │ ├── deploy.yml │ ├── docker-package.yml │ ├── manual-deployment.yml │ ├── release.yml │ ├── scheduled-deployment-prod.yml │ ├── scheduled-deployment.yml │ └── scheduled-docker-image.yml ├── .gitignore ├── .prettierrc ├── .scripts └── check-i18n.js ├── .vscode ├── extensions.json └── settings.json ├── CHANGELOG.md ├── CHANGELOG ├── CHANGELOG_alpha.md ├── CHANGELOG_beta.md └── CHANGELOG_next.md ├── CI └── deploy.sh ├── Dockerfile ├── LICENSE ├── README.md ├── TODO.md ├── __tests__ ├── helpers │ └── database-helpers.ts ├── old │ ├── authentication.setup.ts │ ├── download │ │ └── index.test-tbd.ts │ ├── global.setup.ts │ ├── integration.test.ts │ ├── jest.setup.ts │ ├── models │ │ ├── aggregation.test.ts │ │ ├── apiConfiguration.test.ts │ │ ├── application.test.ts │ │ ├── channel.test.ts │ │ ├── client.test.ts │ │ ├── dashboard.test.ts │ │ ├── distributionList.test.ts │ │ ├── form.test.ts │ │ ├── group.test.ts │ │ ├── layer.test.ts │ │ ├── layout.test.ts │ │ ├── notification.test.ts │ │ ├── page.test.ts │ │ ├── permission.test.ts │ │ ├── positionAttribute.test.ts │ │ ├── positionAttributeCategory.test.ts │ │ ├── pullJob.test.ts │ │ ├── record.test.ts │ │ ├── referenceData.test.ts │ │ ├── resource.test.ts │ │ ├── role.test.ts │ │ ├── step.test.ts │ │ ├── template.test.ts │ │ ├── user.test.ts │ │ ├── version.test.ts │ │ └── workflow.test.ts │ ├── schema │ │ ├── mutation │ │ │ ├── addAggregation.test.ts │ │ │ ├── addApiConfiguration.test.ts │ │ │ ├── addDashboard.test.ts │ │ │ ├── addDistributionList.test.ts │ │ │ ├── addForm.test.ts │ │ │ ├── addGroup.test.ts │ │ │ ├── addLayer.test.ts │ │ │ ├── addLayout.test.ts │ │ │ ├── addPage.test.ts │ │ │ ├── addPositionAttribute.test.ts │ │ │ ├── addPositionAttributeCategory.test.ts │ │ │ └── editLayer.test.ts │ │ └── query │ │ │ ├── apiConfiguration.test.ts │ │ │ ├── apiConfigurations.test.ts │ │ │ ├── application.test.ts │ │ │ ├── applications.test.ts │ │ │ ├── channels.test.ts │ │ │ ├── dashboard.test.ts │ │ │ ├── dashboards.test.ts │ │ │ ├── form.test.ts │ │ │ ├── forms.test.ts │ │ │ ├── group.test.ts │ │ │ ├── groups.test.ts │ │ │ ├── layer.test.ts │ │ │ ├── layers.test.ts │ │ │ ├── me.test.ts │ │ │ ├── notifications.test.ts │ │ │ ├── page.test.ts │ │ │ ├── pages.test.ts │ │ │ ├── pullJob.test.ts │ │ │ ├── record.test.ts │ │ │ ├── recordHistory.test.ts │ │ │ ├── records.test.ts │ │ │ ├── referenceData.test.ts │ │ │ ├── referenceDatas.test.ts │ │ │ ├── resource.test.ts │ │ │ ├── resources.test.ts │ │ │ ├── role.test.ts │ │ │ ├── roles.test.ts │ │ │ ├── step.test.ts │ │ │ ├── steps.test.ts │ │ │ ├── user.test.ts │ │ │ ├── users.test.ts │ │ │ ├── workflow.test.ts │ │ │ └── workflows.test.ts │ ├── server.setup.ts │ ├── server │ │ └── middlewares │ │ │ ├── cors.test.ts │ │ │ └── rateLimit.test.ts │ ├── services │ │ └── page.service.test.ts │ └── utils │ │ ├── server │ │ ├── __snapshots__ │ │ │ └── checkConfig.test.ts.snap │ │ └── checkConfig.test.ts │ │ └── validators │ │ ├── validateApi.test.ts │ │ ├── validateEmail.test.ts │ │ └── validateName.test.ts └── unit-tests │ ├── routes │ └── activity │ │ ├── activity.controller.spec.ts │ │ └── activity.service.spec.ts │ ├── schema │ └── mutation │ │ ├── addApplication.mutation.spec.ts │ │ ├── addPage.mutation.spec.ts │ │ ├── addStep.mutation.spec.ts │ │ ├── editApplication.mutation.spec.ts │ │ ├── editDashboard.mutation.spec.ts │ │ └── editPage.mutation.spec.ts │ └── utils │ ├── form │ └── checkRecordValidation.spec.ts │ ├── models │ └── deletion.spec.ts │ ├── schema │ └── resolvers │ │ └── Query │ │ └── getSortOrder.spec.ts │ ├── user │ ├── fetchGroups.spec.ts │ └── getAutoAssignedRoles.spec.ts │ └── validators │ ├── validateApi.spec.ts │ └── validateName.spec.ts ├── config ├── custom-environment-variables.js ├── default.js ├── oort.js ├── test.js ├── who-dev.js ├── who-prod.js ├── who-test.js └── who.js ├── docker-compose.dist.yml ├── docker-compose.test.yml ├── docker-compose.yml ├── exclude-list.txt ├── jest.config.ts ├── migrations ├── 1663751971645-init.ts ├── 1663763614173-set-missing-timestamps.ts ├── 1663832104713-remove-standalone-forms.ts ├── 1663832321292-generate-layouts.ts ├── 1663832564479-generate-aggregations.ts ├── 1663832608220-generate-graphqltype-names.ts ├── 1664441608216-placeholders.ts ├── 1666880011416-remove-dataset-from-grids.ts ├── 1667217028452-update-attach-to-record-actions.ts ├── 1669104825942-generate-templates.ts ├── 1669105270797-generate-distribution-lists.ts ├── 1669285812934-fix-attach-to-record.ts ├── 1669623288983-add-manage-custom-notifications-permission.ts ├── 1675953127117-convert-group-aggregations-to-arrays.ts ├── 1677015012121-update-reference-data-field-types.ts ├── 1678374386633-layerpermissions.ts ├── 1684107791320-convert-static-cards-to-text-widgets.ts ├── 1684161314765-add-manage-distribution-list-and-template-permissions.ts ├── 1684803344240-update-fields-permission-type.ts ├── 1687184194639-remove-old-forms.ts ├── 1688033668444-update-auto-modify-grid-actions.ts ├── 1690822866516-populate-records-with-related-documents.ts ├── 1700653450166-set-contextualfilter-to-dashboard-level.ts ├── 1707479406412-set-geographic-extent-as-array.ts ├── 1717667736486-add-download-records-permission.ts ├── 1738146929016-update-applications-distribution-lists.ts └── 1738155669737-update-applications-email-templates.ts ├── package-lock.json ├── package.json ├── rabbitmq └── enabled_plugins ├── release.config.js ├── setup-hooks.sh ├── src ├── abstractions │ ├── api-error.ts │ └── base.controller.ts ├── assets │ └── emails │ │ ├── app-invitation │ │ ├── html.pug │ │ └── subject.pug │ │ ├── create-account-to-app │ │ ├── html.pug │ │ └── subject.pug │ │ ├── create-account │ │ ├── html.pug │ │ └── subject.pug │ │ └── style.css ├── const │ ├── aggregation.ts │ ├── calculatedFields.ts │ ├── channels.ts │ ├── defaultRecordFields.ts │ ├── enumTypes.ts │ ├── fieldTypes.ts │ ├── permissions.ts │ ├── placeholders.ts │ └── protectedNames.ts ├── i18n │ ├── en.json │ └── test.json ├── index.ts ├── migrations │ ├── database.helper.ts │ ├── index.ts │ ├── stateStore.ts │ ├── template.ts │ └── ts-compiler.js ├── models │ ├── actionButton.model.ts │ ├── activityLog.model.ts │ ├── aggregation.model.ts │ ├── apiConfiguration.model.ts │ ├── application.model.ts │ ├── channel.model.ts │ ├── client.model.ts │ ├── customNotification.model.ts │ ├── customTemplate.model.ts │ ├── dashboard.model.ts │ ├── distributionList.model.ts │ ├── draftRecord.model.ts │ ├── emailDistributionList.model.ts │ ├── emailNotification.model.ts │ ├── form.model.ts │ ├── group.model.ts │ ├── history.model.ts │ ├── index.ts │ ├── layer.model.ts │ ├── layout.model.ts │ ├── migration.model.ts │ ├── notification.model.ts │ ├── page.model.ts │ ├── permission.model.ts │ ├── positionAttribute.model.ts │ ├── positionAttributeCategory.model.ts │ ├── pullJob.model.ts │ ├── record.model.ts │ ├── referenceData.model.ts │ ├── resource.model.ts │ ├── role.model.ts │ ├── step.model.ts │ ├── template.model.ts │ ├── user.model.ts │ ├── version.model.ts │ └── workflow.model.ts ├── oort.config.ts ├── routes │ ├── activity │ │ ├── activity.controller.ts │ │ └── activity.service.ts │ ├── download │ │ └── index.ts │ ├── email │ │ └── index.ts │ ├── gis │ │ └── index.ts │ ├── index.ts │ ├── notification │ │ └── index.ts │ ├── permissions │ │ └── index.ts │ ├── proxy │ │ └── index.ts │ ├── roles │ │ └── index.ts │ ├── style │ │ └── index.ts │ ├── summarycards │ │ └── index.ts │ └── upload │ │ └── index.ts ├── schema │ ├── index.ts │ ├── inputs │ │ ├── aggregation.input.ts │ │ ├── button-action.input.ts │ │ ├── customNotification.input.ts │ │ ├── dashboard-filter.input.ts │ │ ├── distributionList.input.ts │ │ ├── emailNotification.input.ts │ │ ├── index.ts │ │ ├── layer.input.ts │ │ ├── layout.input.ts │ │ ├── pageContext.input.ts │ │ ├── position-attribute.input.ts │ │ ├── template.input.ts │ │ ├── user-profile.input.ts │ │ └── user.input.ts │ ├── mutation │ │ ├── addAggregation.mutation.ts │ │ ├── addApiConfiguration.mutation.ts │ │ ├── addApplication.mutation.ts │ │ ├── addChannel.mutation.ts │ │ ├── addCustomNotification.mutation.ts │ │ ├── addCustomTemplate.mutation.ts │ │ ├── addDashboard.mutation.ts │ │ ├── addDashboardTemplate.mutation.ts │ │ ├── addDistributionList.mutation.ts │ │ ├── addDraftRecord.mutation.ts │ │ ├── addEmailDistributionList.mutation.ts │ │ ├── addEmailNotification.mutation.ts │ │ ├── addEmailTemplate.mutation.ts │ │ ├── addForm.mutation.ts │ │ ├── addGroup.mutation.ts │ │ ├── addLayer.mutation.ts │ │ ├── addLayout.mutation.ts │ │ ├── addPage.mutation.ts │ │ ├── addPositionAttribute.mutation.ts │ │ ├── addPositionAttributeCategory.mutation.ts │ │ ├── addPullJob.mutation.ts │ │ ├── addRecord.mutation.ts │ │ ├── addReferenceData.mutation.ts │ │ ├── addRole.mutation.ts │ │ ├── addRoleToUsers.mutation.ts │ │ ├── addStep.mutation.ts │ │ ├── addSubscription.mutation.ts │ │ ├── addUsers.mutation.ts │ │ ├── addWorkflow.mutation.ts │ │ ├── convertRecord.mutation.ts │ │ ├── deleteAggregation.mutation.ts │ │ ├── deleteApiConfiguration.mutation.ts │ │ ├── deleteApplication.mutation.ts │ │ ├── deleteChannel.mutation.ts │ │ ├── deleteCustomNotification.mutation.ts │ │ ├── deleteDashboard.mutation.ts │ │ ├── deleteDashboardTemplates.mutation.ts │ │ ├── deleteDistributionList.mutation.ts │ │ ├── deleteDraftRecord.mutation.ts │ │ ├── deleteEmailDistributionList.mutation.ts │ │ ├── deleteEmailNotification.mutation.ts │ │ ├── deleteForm.mutation.ts │ │ ├── deleteGroup.mutation.ts │ │ ├── deleteLayer.mutation.ts │ │ ├── deleteLayout.mutation.ts │ │ ├── deletePage.mutation.ts │ │ ├── deletePositionAttributeCategory.mutation.ts │ │ ├── deletePullJob.mutation.ts │ │ ├── deleteRecord.mutation.ts │ │ ├── deleteRecords.mutation.ts │ │ ├── deleteReferenceData.mutation.ts │ │ ├── deleteResource.mutation.ts │ │ ├── deleteRole.mutation.ts │ │ ├── deleteStep.mutation.ts │ │ ├── deleteSubscription.mutation.ts │ │ ├── deleteTemplate.mutation.ts │ │ ├── deleteUsers.mutation.ts │ │ ├── deleteUsersFromApplication.mutation.ts │ │ ├── deleteWorkflow.mutation.ts │ │ ├── duplicateApplication.mutation.ts │ │ ├── duplicatePage.mutation.ts │ │ ├── editAggregation.mutation.ts │ │ ├── editApiConfiguration.mutation.ts │ │ ├── editApplication.mutation.ts │ │ ├── editChannel.mutation.ts │ │ ├── editCustomNotification.mutation.ts │ │ ├── editCustomTemplate.mutation.ts │ │ ├── editDashboard.mutation.ts │ │ ├── editDistributionList.mutation.ts │ │ ├── editDraftRecord.mutation.ts │ │ ├── editEmailDistributionList.mutation.ts │ │ ├── editEmailNotification.mutation.ts │ │ ├── editForm.mutation.ts │ │ ├── editLayer.mutation.ts │ │ ├── editLayout.mutation.ts │ │ ├── editPage.mutation.ts │ │ ├── editPageContext.mutation.ts │ │ ├── editPositionAttributeCategory.mutation.ts │ │ ├── editPullJob.mutation.ts │ │ ├── editRecord.mutation.ts │ │ ├── editRecords.mutation.ts │ │ ├── editReferenceData.mutation.ts │ │ ├── editResource.mutation.ts │ │ ├── editRole.mutation.ts │ │ ├── editStep.mutation.ts │ │ ├── editSubscription.mutation.ts │ │ ├── editTemplate.mutation.ts │ │ ├── editUser.mutation.ts │ │ ├── editUserProfile.mutation.ts │ │ ├── editWorkflow.mutation.ts │ │ ├── fetchGroups.mutation.ts │ │ ├── index.ts │ │ ├── publish.mutation.ts │ │ ├── publishNotification.mutation.ts │ │ ├── restorePage.mutation.ts │ │ ├── restoreRecord.mutation.ts │ │ ├── seeNotification.mutation.ts │ │ ├── seeNotifications.mutation.ts │ │ └── toggleApplicationLock.mutation.ts │ ├── query │ │ ├── apiConfiguration.query.ts │ │ ├── apiConfigurations.query.ts │ │ ├── application.query.ts │ │ ├── applications.query.ts │ │ ├── channels.query.ts │ │ ├── customTemplates.query.ts │ │ ├── dashboard.query.ts │ │ ├── dashboards.query.ts │ │ ├── draftRecords.query.ts │ │ ├── emailDistributionList.query.ts │ │ ├── emailNotification.query.ts │ │ ├── emailNotifications.query.ts │ │ ├── form.query.ts │ │ ├── forms.query.ts │ │ ├── group.query.ts │ │ ├── groups.query.ts │ │ ├── index.ts │ │ ├── layer.query.ts │ │ ├── layers.query.ts │ │ ├── me.query.ts │ │ ├── notifications.query.ts │ │ ├── page.query.ts │ │ ├── pages.query.ts │ │ ├── permissions.query.ts │ │ ├── positionAttributes.query.ts │ │ ├── pullJobs.query.ts │ │ ├── record.query.ts │ │ ├── recordHistory.query.ts │ │ ├── records.query.ts │ │ ├── recordsAggregation.query.ts │ │ ├── referenceData.query.ts │ │ ├── referenceDataAggregation.query.ts │ │ ├── referenceDatas.query.ts │ │ ├── resource.query.ts │ │ ├── resources.query.ts │ │ ├── role.query.ts │ │ ├── roles.query.ts │ │ ├── rolesFromApplications.query.ts │ │ ├── step.query.ts │ │ ├── steps.query.ts │ │ ├── types.query.ts │ │ ├── user.query.ts │ │ ├── users.query.ts │ │ ├── workflow.query.ts │ │ └── workflows.query.ts │ ├── shared │ │ ├── auth-check.util.ts │ │ └── index.ts │ ├── subscription │ │ ├── applicationEdited.subscription.ts │ │ ├── applicationUnlocked.subscription.ts │ │ ├── index.ts │ │ ├── notification.subscription.ts │ │ └── recordAdded.subscription.ts │ └── types │ │ ├── access.type.ts │ │ ├── aggregation.type.ts │ │ ├── apiConfiguration.type.ts │ │ ├── application.type.ts │ │ ├── channel.type.ts │ │ ├── customNotification.type.ts │ │ ├── customTemplate.type.ts │ │ ├── dashboard.type.ts │ │ ├── dataset.type.ts │ │ ├── distributionList.type.ts │ │ ├── draftRecord.type.ts │ │ ├── emailDistribution.type.ts │ │ ├── emailNotification.type.ts │ │ ├── form.type.ts │ │ ├── geospatial.type.ts │ │ ├── group.type.ts │ │ ├── historyVersion.type.ts │ │ ├── index.ts │ │ ├── layer.type.ts │ │ ├── layout.type.ts │ │ ├── metadata.type.ts │ │ ├── notification.type.ts │ │ ├── page.type.ts │ │ ├── pagination.type.ts │ │ ├── permission.type.ts │ │ ├── positionAttribute.type.ts │ │ ├── positionAttributeCategory.type.ts │ │ ├── pullJob.type.ts │ │ ├── record.type.ts │ │ ├── referenceData.type.ts │ │ ├── resource.type.ts │ │ ├── role.type.ts │ │ ├── step.type.ts │ │ ├── subscription.type.ts │ │ ├── template.type.ts │ │ ├── user.type.ts │ │ ├── version.type.ts │ │ └── workflow.type.ts ├── security │ ├── defineUserAbility.ts │ ├── extendAbilityForApplication.ts │ ├── extendAbilityForContent.ts │ ├── extendAbilityForPage.ts │ ├── extendAbilityForRecords.ts │ └── extendAbilityForStep.ts ├── server │ ├── EIOSOwnernshipMapping.ts │ ├── apollo │ │ ├── context.ts │ │ ├── dataSources.ts │ │ ├── onConnect.ts │ │ └── queries │ │ │ └── introspection.query.ts │ ├── common-services.ts │ ├── customNotificationScheduler.ts │ ├── database.ts │ ├── index.ts │ ├── middlewares │ │ ├── auth.ts │ │ ├── cors.ts │ │ ├── graphql.ts │ │ ├── index.ts │ │ ├── rateLimit.ts │ │ ├── rest.ts │ │ └── winston.ts │ ├── pubsub.ts │ ├── pubsubSafe.ts │ ├── pullJobScheduler.ts │ ├── redis.ts │ └── subscriberSafe.ts ├── services │ ├── form.service.ts │ ├── logger.service.ts │ └── page.service.ts ├── setup │ ├── clearLayouts.ts │ └── init.ts ├── types │ ├── filter.ts │ ├── index.ts │ ├── permission.ts │ └── route-definition.ts └── utils │ ├── aggregation │ ├── buildCalculatedFieldPipeline.ts │ ├── buildPipeline.ts │ ├── buildReferenceDataAggregation.ts │ ├── expressionFromString.ts │ └── setDisplayText.ts │ ├── context │ ├── getContextData.ts │ └── getNewDashboardName.ts │ ├── date │ ├── getICULocale.ts │ └── getTimeWithTimezone.ts │ ├── email │ ├── gridEmailBuilder.ts │ ├── index.ts │ └── sendEmail.ts │ ├── files │ ├── copyFolder.ts │ ├── csvBuilder.ts │ ├── deleteFolder.ts │ ├── downloadFile.ts │ ├── extractGridData.ts │ ├── fileBuilder.ts │ ├── format.helper.ts │ ├── getColumns.ts │ ├── getColumnsFromMeta.ts │ ├── getRows.ts │ ├── getRowsFromMeta.ts │ ├── getUploadColumns.ts │ ├── historyBuilder.ts │ ├── index.ts │ ├── loadRow.ts │ ├── resourceExporter.ts │ ├── templateBuilder.ts │ ├── uploadFile.ts │ └── xlsBuilder.ts │ ├── filter │ ├── getDateForMongo.ts │ ├── getFilter.ts │ ├── getFormFilter.ts │ ├── getFormPermissionFilter.ts │ ├── getTimeForMongo.ts │ └── index.ts │ ├── form │ ├── addField.ts │ ├── checkDefaultFields.ts │ ├── checkRecordExpressions.ts │ ├── checkRecordTriggers.ts │ ├── checkRecordValidation.ts │ ├── extractFields.ts │ ├── findDuplicateFields.ts │ ├── getAccessibleFields.ts │ ├── getDisplayText.ts │ ├── getFieldType.ts │ ├── getNextId.ts │ ├── getNextQuestion.ts │ ├── getOwnership.ts │ ├── getParentQuestion.ts │ ├── getPreviousQuestion.ts │ ├── getQuestion.ts │ ├── getQuestionPosition.ts │ ├── index.ts │ ├── metadata.helper.ts │ ├── removeField.ts │ ├── replaceField.ts │ └── transformRecord.ts │ ├── geojson │ └── generateGeoJson.ts │ ├── gis │ └── getCountryPolygons.ts │ ├── history │ ├── index.ts │ └── recordHistory.ts │ ├── models │ └── deletion.ts │ ├── notification │ ├── blobStorage.ts │ └── util.ts │ ├── proxy │ ├── authManagement.ts │ ├── getChoices.ts │ └── index.ts │ ├── query │ └── queryBuilder.ts │ ├── referenceData │ └── referenceDataFilter.util.ts │ ├── schema │ ├── buildSchema.ts │ ├── buildTypes.ts │ ├── errors │ │ └── checkPageSize.util.ts │ ├── getStructures.ts │ ├── introspection │ │ ├── getConnectionType.ts │ │ ├── getFieldName.ts │ │ ├── getFieldType.ts │ │ ├── getFields.ts │ │ ├── getMetaTypes.ts │ │ ├── getReversedFields.ts │ │ ├── getSchema.ts │ │ ├── getTypeFromKey.ts │ │ ├── getTypes.ts │ │ └── isRelationshipField.ts │ └── resolvers │ │ ├── Entity │ │ ├── getReferenceDataResolver.ts │ │ └── index.ts │ │ ├── Meta │ │ ├── getMetaCheckboxResolver.ts │ │ ├── getMetaDropdownResolver.ts │ │ ├── getMetaFieldResolver.ts │ │ ├── getMetaOwnerResolver.ts │ │ ├── getMetaRadiogroupResolver.ts │ │ ├── getMetaReferenceDataResolver.ts │ │ ├── getMetaTagboxResolver.ts │ │ ├── getMetaUsersResolver.ts │ │ └── index.ts │ │ ├── Query │ │ ├── all.ts │ │ ├── getFilter.ts │ │ ├── getSearchFilter.ts │ │ ├── getSortAggregation.ts │ │ ├── getSortField.ts │ │ ├── getSortOrder.ts │ │ ├── getStyle.ts │ │ ├── meta.ts │ │ └── single.ts │ │ └── index.ts │ ├── server │ └── checkConfig.util.ts │ ├── user │ ├── fetchGroups.ts │ ├── getAutoAssignedRoles.ts │ ├── index.ts │ ├── sendUserInvitation.ts │ ├── updateUserAttributes.ts │ ├── updateUserGroups.ts │ └── userManagement.ts │ └── validators │ ├── index.ts │ ├── validateApi.ts │ └── validateName.ts ├── tsconfig.build.json └── tsconfig.json /.dockerignore: -------------------------------------------------------------------------------- 1 | .git 2 | .docker 3 | .github 4 | .vscode 5 | .husky 6 | CI 7 | node_modules 8 | Dockerfile 9 | .env.dev 10 | .env.dist 11 | .gitignore 12 | README.md 13 | rabbitmq 14 | docker-compose.* 15 | __tests__ 16 | coverage 17 | -------------------------------------------------------------------------------- /.env.dist: -------------------------------------------------------------------------------- 1 | # It seems that .env file is automatically fetched in priority from the root of the folder from which the terminal is ran. 2 | # Make sure to open the terminal / VScode instance from the folder of the project and to not have another .env file above in the folder hierarchy, or it could be used instead. 3 | 4 | # NODE ENV 5 | NODE_ENV= 6 | NODE_CONFIG_ENV= 7 | 8 | # SERVER CONFIGURATION 9 | SERVER_URL= 10 | SERVER_ALLOWED_ORIGINS=[] 11 | SERVER_PROTECTED_SHORTCUTS=[] 12 | 13 | # DATABASE CONFIGURATION 14 | DB_PROVIDER= 15 | DB_PREFIX= 16 | DB_USER= 17 | DB_PASS= 18 | DB_HOST= 19 | DB_PORT= 20 | DB_NAME= 21 | 22 | # EMAIL CONFIGURATION 23 | MAIL_HOST= 24 | MAIL_PORT= 25 | MAIL_FROM= 26 | MAIL_REPLY_TO= 27 | MAIL_FROM_PREFIX= 28 | MAIL_USER= 29 | MAIL_PASS= 30 | 31 | # AZURE FUNCTION MAIL CONFIGURATION 32 | MAIL_SERVERLESS_URL= 33 | MAIL_SERVERLESS_KEY= 34 | 35 | # BLOB STORAGE FOR EMAIL ASSETS 36 | MAIL_BLOB_STORAGE_CONTAINER= 37 | MAIL_BLOB_STORAGE_CONNECTION_STRING= 38 | 39 | # AUTH CONFIGURATION 40 | AUTH_PROVIDER= 41 | AUTH_URL= 42 | AUTH_REALM= 43 | AUTH_CLIENT_ID= 44 | AUTH_TENANT_ID= 45 | AUTH_ALLOWED_ISSUERS=[] 46 | 47 | # ENCRYPTION 48 | ENCRYPTION_KEY= 49 | 50 | # BLOB STORAGE 51 | BLOB_STORAGE_CONNECTION_STRING= 52 | 53 | # RABBIT_MQ CONFIGURATION 54 | RABBITMQ_ERLANG_COOKIE= 55 | RABBITMQ_DEFAULT_USER= 56 | RABBITMQ_DEFAULT_PASS= 57 | RABBITMQ_APPLICATION= 58 | RABBITMQ_HOST= 59 | RABBITMQ_PORT= 60 | 61 | # MODULES 62 | FRONT_OFFICE_URI= 63 | BACK_OFFICE_URI= 64 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | src/migrations/template.ts 3 | logs 4 | __tests__/old 5 | -------------------------------------------------------------------------------- /.github/.githooks/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | npm run check-i18n 4 | 5 | git add src/i18n/en.json 6 | git add src/i18n/test.json 7 | -------------------------------------------------------------------------------- /.github/.githooks/pre-push: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # Run linting before pushing 4 | npm run lint 5 | 6 | # Exit with an error code if linting fails 7 | if [ $? -ne 0 ]; then 8 | echo "Linting failed. Aborting push." 9 | exit 1 10 | fi 11 | -------------------------------------------------------------------------------- /.github/.githooks/prepare-commit-msg: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | NAME=$(git branch | grep '*' | sed 's/* //' | sed 's/^AB//') 3 | 4 | if [ $(echo $NAME | grep -E -o '^#[0-9]{5}$') ] 5 | then 6 | echo $(cat "$1") "$NAME" > "$1" 7 | else 8 | echo "$(cat $1) > $1" 9 | fi -------------------------------------------------------------------------------- /.github/workflows/deploy.yml: -------------------------------------------------------------------------------- 1 | name: Back-end CD 2 | 3 | on: 4 | repository_dispatch: 5 | types: [CD] 6 | 7 | jobs: 8 | deploy: 9 | name: Deploy 🚀 10 | runs-on: ubuntu-latest 11 | environment: ${{ github.event.client_payload.environment }} 12 | 13 | steps: 14 | - name: Checkout 15 | uses: actions/checkout@v4 16 | with: 17 | ref: ${{ github.event.client_payload.ref }} 18 | - name: Setup SSH Keys and known_hosts 19 | uses: webfactory/ssh-agent@v0.7.0 20 | with: 21 | ssh-private-key: ${{ secrets.SSH_PRIVATE_KEY }} 22 | - name: Update Docker 23 | run: ./CI/deploy.sh 24 | env: 25 | SSH_PASS: ${{ secrets.SSH_PASS }} 26 | CONNECTION: ${{ secrets.CONNECTION }} 27 | REMOTE_PATH: ${{ secrets.REMOTE_PATH }} 28 | -------------------------------------------------------------------------------- /.github/workflows/manual-deployment.yml: -------------------------------------------------------------------------------- 1 | name: Manual CD 2 | 3 | on: 4 | workflow_dispatch: 5 | inputs: 6 | environment: 7 | type: environment 8 | required: true 9 | 10 | jobs: 11 | deploy: 12 | name: 'Deploy' 13 | runs-on: ubuntu-latest 14 | environment: 15 | name: ${{ github.event.inputs.environment }} 16 | steps: 17 | - name: Checkout 18 | uses: actions/checkout@v4 19 | - name: Dispatch CD Event 20 | uses: peter-evans/repository-dispatch@v3 21 | with: 22 | token: ${{ secrets.CD_ACCESS_TOKEN }} 23 | event-type: CD 24 | client-payload: '{"environment": "${{ github.event.inputs.environment }}", "ref": "${{ github.ref }}"}' 25 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | - next 8 | workflow_dispatch: 9 | 10 | concurrency: 11 | group: ${{ github.workflow }}-${{ github.ref }} 12 | cancel-in-progress: true 13 | 14 | jobs: 15 | release: 16 | name: Release 17 | runs-on: ubuntu-latest 18 | # needs: [build, prettier] 19 | steps: 20 | - name: Checkout 21 | uses: actions/checkout@v4 22 | with: 23 | fetch-depth: 0 24 | persist-credentials: false 25 | - name: Check branch 26 | run: | 27 | VALID_BRANCHES=("main" "next" "beta" "alpha") 28 | BRANCH_NAME=$(basename ${{ github.ref }}) 29 | 30 | if [[ ! " ${VALID_BRANCHES[@]} " =~ " ${BRANCH_NAME} " ]]; then 31 | echo "Invalid branch name: $BRANCH_NAME" 32 | echo "Valid branch names are: ${VALID_BRANCHES[@]}" 33 | exit 1 34 | fi 35 | - name: Setup Node.js 36 | uses: actions/setup-node@v4 37 | with: 38 | node-version: 'lts/*' 39 | - name: Install dependencies 40 | run: npm ci 41 | - name: Release 42 | env: 43 | GITHUB_TOKEN: ${{ secrets.SEMANTIC_RELEASE_TOKEN }} 44 | run: npx semantic-release 45 | -------------------------------------------------------------------------------- /.github/workflows/scheduled-deployment-prod.yml: -------------------------------------------------------------------------------- 1 | name: Trigger CD - PROD 2 | 3 | on: 4 | schedule: 5 | - cron: '0 0 1 * *' 6 | 7 | jobs: 8 | deploy: 9 | name: 'Deploy' 10 | runs-on: ubuntu-latest 11 | strategy: 12 | matrix: 13 | include: 14 | - environment: OORT_PROD 15 | ref: next 16 | environment: 17 | name: ${{ matrix.environment }} 18 | steps: 19 | - name: Checkout 20 | uses: actions/checkout@v4 21 | - name: Dispatch CD Event 22 | uses: peter-evans/repository-dispatch@v3 23 | with: 24 | token: ${{ secrets.CD_ACCESS_TOKEN }} 25 | event-type: CD 26 | client-payload: '{"environment": "${{ matrix.environment }}", "ref": "${{ matrix.ref }}"}' 27 | -------------------------------------------------------------------------------- /.github/workflows/scheduled-deployment.yml: -------------------------------------------------------------------------------- 1 | name: Trigger CD 2 | 3 | on: 4 | schedule: 5 | - cron: '0 0 * * *' 6 | 7 | jobs: 8 | deploy: 9 | name: 'Deploy' 10 | runs-on: ubuntu-latest 11 | strategy: 12 | matrix: 13 | include: 14 | - environment: OORT_DEV 15 | ref: alpha 16 | environment: 17 | name: ${{ matrix.environment }} 18 | steps: 19 | - name: Checkout 20 | uses: actions/checkout@v4 21 | - name: Dispatch CD Event 22 | uses: peter-evans/repository-dispatch@v3 23 | with: 24 | token: ${{ secrets.CD_ACCESS_TOKEN }} 25 | event-type: CD 26 | client-payload: '{"environment": "${{ matrix.environment }}", "ref": "${{ matrix.ref }}"}' 27 | -------------------------------------------------------------------------------- /.github/workflows/scheduled-docker-image.yml: -------------------------------------------------------------------------------- 1 | name: Trigger Docker Image Update 2 | 3 | on: 4 | schedule: 5 | - cron: '0 0 * * 0' #At 00:00 on every Sunday. 6 | 7 | jobs: 8 | deploy: 9 | name: 'Update docker latest image' 10 | runs-on: ubuntu-latest 11 | strategy: 12 | matrix: 13 | include: 14 | - ref: main 15 | - ref: next 16 | - ref: beta 17 | - ref: alpha 18 | steps: 19 | - name: Dispatch DOCKER Event 20 | uses: peter-evans/repository-dispatch@v3 21 | with: 22 | token: ${{ secrets.CD_ACCESS_TOKEN }} 23 | event-type: DOCKER 24 | client-payload: '{"ref": "${{ matrix.ref }}" }' 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Environment 2 | .env 3 | .env.dev 4 | /config/local.* 5 | 6 | # Docker 7 | .docker/ 8 | 9 | # GraphQL 10 | schema.graphql 11 | 12 | # Build 13 | node_modules/ 14 | dist/ 15 | build/ 16 | .DS_STORE 17 | .idea/ 18 | coverage/ 19 | 20 | # Migration, not used anymore, but should remain until merged in main 21 | .migrate 22 | logs/ 23 | config/local.js 24 | 25 | # IDE - VSCode 26 | .vscode/* 27 | !.vscode/settings.json 28 | !.vscode/tasks.json 29 | !.vscode/launch.json 30 | !.vscode/extensions.json 31 | 32 | # Files 33 | files/* 34 | 35 | __blobstorage__/* 36 | __azurite_db_blob* 37 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "useTabs": false, 4 | "tabWidth": 2, 5 | "endOfLine": "lf" 6 | } 7 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "jannisx11.batch-rename-extension", 4 | "streetsidesoftware.code-spell-checker", 5 | "dbaeumer.vscode-eslint", 6 | "streetsidesoftware.code-spell-checker-french", 7 | "waderyan.gitblame", 8 | "lokalise.i18n-ally", 9 | "esbenp.prettier-vscode", 10 | "ms-azuretools.vscode-docker" 11 | ] 12 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "i18n-ally.localesPaths": [ 3 | "src/i18n" 4 | ], 5 | "i18n-ally.keystyle": "nested" 6 | } 7 | -------------------------------------------------------------------------------- /CI/deploy.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | echo -e "Stopping docker containers..." 6 | CMD="cd $REMOTE_PATH && echo $SSH_PASS | sudo -S docker-compose pull api" 7 | ssh -oStrictHostKeyChecking=no -o PubkeyAuthentication=yes $CONNECTION "$CMD" 8 | 9 | echo -e "Stopping docker containers..." 10 | CMD="cd $REMOTE_PATH && echo $SSH_PASS | sudo -S docker-compose up -d --no-deps api" 11 | ssh -oStrictHostKeyChecking=no -o PubkeyAuthentication=yes $CONNECTION "$CMD" 12 | 13 | echo -e "Deployed!" 14 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:18-alpine as base 2 | 3 | WORKDIR /home/node/app 4 | 5 | COPY package*.json ./ 6 | 7 | RUN npm i --ignore-scripts 8 | 9 | COPY . . 10 | 11 | RUN mkdir -p files 12 | 13 | FROM base as production 14 | 15 | ENV NODE_PATH=./build 16 | ENV NODE_CONFIG_DIR=/home/node/app/config 17 | 18 | RUN npm run build 19 | -------------------------------------------------------------------------------- /TODO.md: -------------------------------------------------------------------------------- 1 | # TODO 2 | 3 | List there all actions to do ( eg: update to perform on some environments when releasing them on different branch ). 4 | 5 | ## Done in 2.x.x 6 | [ ] - Update of Apollo 7 | [ ] - Update of CASL 8 | -------------------------------------------------------------------------------- /__tests__/helpers/database-helpers.ts: -------------------------------------------------------------------------------- 1 | import { MongoMemoryServer } from 'mongodb-memory-server'; 2 | import mongoose from 'mongoose'; 3 | 4 | /** 5 | * Database helpers for tests. 6 | */ 7 | export class DatabaseHelpers { 8 | private server: MongoMemoryServer; 9 | 10 | /** 11 | * Connect to the database 12 | */ 13 | public async connect() { 14 | this.server = await MongoMemoryServer.create(); 15 | const uri = this.server.getUri(); 16 | 17 | await mongoose.connect(uri); 18 | await mongoose.connection.syncIndexes(); 19 | } 20 | 21 | /** 22 | * Disconnect from the database. 23 | */ 24 | public async disconnect() { 25 | await mongoose.disconnect(); 26 | await this.server.stop(); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /__tests__/old/authentication.setup.ts: -------------------------------------------------------------------------------- 1 | import fetch from 'node-fetch'; 2 | import config from 'config'; 3 | 4 | /** 5 | * Gets the token from MSAL. 6 | * 7 | * @returns azure token. 8 | */ 9 | export async function acquireToken(): Promise { 10 | const url = `${config.get('auth.url')}/realms/${config.get( 11 | 'auth.realm' 12 | )}/protocol/openid-connect/token`; 13 | 14 | const params = new URLSearchParams(); 15 | params.append('grant_type', 'password'); 16 | params.append('client_id', config.get('auth.clientId')); 17 | params.append('username', 'dummy@dummy.com'); 18 | params.append('password', 'password'); 19 | 20 | const response = await fetch(url, { 21 | method: 'POST', 22 | body: params, 23 | headers: { 24 | 'Content-Type': 'application/x-www-form-urlencoded', 25 | }, 26 | }); 27 | 28 | const data = await response.json(); 29 | 30 | return data.access_token; 31 | } 32 | -------------------------------------------------------------------------------- /__tests__/old/global.setup.ts: -------------------------------------------------------------------------------- 1 | import 'tsconfig-paths/register'; 2 | import dotenv from 'dotenv'; 3 | dotenv.config(); 4 | import { startDatabase, initDatabase, stopDatabase } from '@server/database'; 5 | import config from 'config'; 6 | import { logger } from '@services/logger.service'; 7 | 8 | /** Executes before all tests */ 9 | export default async () => { 10 | if (config.util.getEnv('NODE_ENV') !== 'production') { 11 | await startDatabase(); 12 | logger.log({ level: 'info', message: '📶 Connected to database' }); 13 | await initDatabase(); 14 | await stopDatabase(); 15 | } 16 | }; 17 | -------------------------------------------------------------------------------- /__tests__/old/jest.setup.ts: -------------------------------------------------------------------------------- 1 | // This line is important, as it will prevent the delete folder method to use actual logic 2 | // As we don't want to test if deletion of azure files work or not, we skip it 3 | // Mocking for this module must be done before any other imports 4 | jest.mock('@utils/files/deleteFolder'); 5 | 6 | import { startDatabase } from '../src/server/database'; 7 | 8 | // Execute before each file. 9 | beforeAll(async () => { 10 | await startDatabase(); 11 | }, 20000); 12 | 13 | afterAll(async () => { 14 | // await stopDatabase(); 15 | // Avoid memory issue 16 | if (global.gc) global.gc(); 17 | }); 18 | -------------------------------------------------------------------------------- /__tests__/old/models/channel.test.ts: -------------------------------------------------------------------------------- 1 | import { Channel, Notification } from '@models'; 2 | import { faker } from '@faker-js/faker'; 3 | 4 | /** 5 | * Test Channel Model. 6 | */ 7 | describe('Channel models tests', () => { 8 | let channel; 9 | test('test with correct data', async () => { 10 | for (let i = 0; i < 10; i++) { 11 | channel = await new Channel({ 12 | title: faker.random.alpha(10), 13 | }).save(); 14 | expect(channel._id).toBeDefined(); 15 | } 16 | }); 17 | 18 | test('test Channel with duplicate title', async () => { 19 | const duplicateApiConfig = { 20 | title: channel.title, 21 | }; 22 | expect(async () => 23 | new Channel(duplicateApiConfig).save() 24 | ).rejects.toMatchObject({ 25 | code: 11000, 26 | }); 27 | }); 28 | 29 | test('test with blank channel name field', async () => { 30 | for (let i = 0; i < 1; i++) { 31 | const channelData = { 32 | title: '', 33 | }; 34 | await expect(async () => new Channel(channelData).save()).rejects.toThrow( 35 | Error 36 | ); 37 | } 38 | }); 39 | 40 | test('test channel delete', async () => { 41 | const channelData = await new Channel({ 42 | title: faker.random.alpha(10), 43 | }).save(); 44 | 45 | for (let i = 0; i < 10; i++) { 46 | await new Notification({ 47 | action: 'channel created', 48 | channel: channelData._id, 49 | }).save(); 50 | } 51 | 52 | const isDelete = await Channel.deleteOne({ _id: channelData._id }); 53 | expect(isDelete.acknowledged).toEqual(true); 54 | expect(isDelete.deletedCount).toEqual(1); 55 | }); 56 | }); 57 | -------------------------------------------------------------------------------- /__tests__/old/models/client.test.ts: -------------------------------------------------------------------------------- 1 | import { Client, Role } from '@models'; 2 | import { faker } from '@faker-js/faker'; 3 | 4 | /** 5 | * Test Client Model. 6 | */ 7 | describe('Client models tests', () => { 8 | let client: Client; 9 | test('test with correct data', async () => { 10 | const roleDetail = await Role.findOne(); 11 | 12 | for (let i = 0; i < 1; i++) { 13 | const inputData = { 14 | name: faker.name.fullName(), 15 | roles: [roleDetail._id], 16 | clientId: faker.datatype.uuid(), 17 | oid: faker.datatype.uuid(), 18 | }; 19 | client = await new Client(inputData).save(); 20 | expect(client._id).toBeDefined(); 21 | } 22 | }); 23 | 24 | test('test Client with duplicate clientId', async () => { 25 | const inputData = { 26 | name: faker.name.fullName(), 27 | clientId: client.clientId, 28 | }; 29 | expect(async () => new Client(inputData).save()).rejects.toMatchObject({ 30 | code: 11000, 31 | }); 32 | }); 33 | 34 | test('test client with duplicate oid field', async () => { 35 | const roleDetail = await Role.findOne(); 36 | const clientData = { 37 | name: faker.name.fullName(), 38 | roles: [roleDetail._id], 39 | clientId: faker.datatype.uuid(), 40 | oid: client.oid, 41 | }; 42 | expect(async () => new Client(clientData).save()).rejects.toThrow(Error); 43 | }); 44 | }); 45 | -------------------------------------------------------------------------------- /__tests__/old/models/dashboard.test.ts: -------------------------------------------------------------------------------- 1 | import { Dashboard } from '@models'; 2 | import { faker } from '@faker-js/faker'; 3 | 4 | /** 5 | * Test Dashboard Model. 6 | */ 7 | describe('Dashboard models tests', () => { 8 | test('test with correct data', async () => { 9 | for (let i = 0; i < 1; i++) { 10 | const dashboard = await new Dashboard({ 11 | name: faker.word.adjective(), 12 | }).save(); 13 | expect(dashboard._id).toBeDefined(); 14 | expect(dashboard).toHaveProperty('createdAt'); 15 | expect(dashboard).toHaveProperty('modifiedAt'); 16 | } 17 | }); 18 | 19 | test('test with object value of dashboard name field ', async () => { 20 | const dashboardData = { 21 | name: faker.science.unit(), 22 | }; 23 | expect(async () => new Dashboard(dashboardData).save()).rejects.toThrow( 24 | Error 25 | ); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /__tests__/old/models/distributionList.test.ts: -------------------------------------------------------------------------------- 1 | import { Application } from '@models'; 2 | import { faker } from '@faker-js/faker'; 3 | 4 | /** 5 | * Test Distribution List Model. 6 | */ 7 | describe('Distribution List models tests', () => { 8 | test('test with correct data', async () => { 9 | for (let i = 0; i < 1; i++) { 10 | const distributionListData = []; 11 | for (let j = 0; j < 10; j++) { 12 | distributionListData.push({ 13 | name: faker.name.fullName(), 14 | emails: new Array(10).fill(faker.internet.email()), 15 | }); 16 | } 17 | 18 | const application = await new Application({ 19 | name: faker.word.adjective(), 20 | distributionLists: distributionListData, 21 | }).save(); 22 | expect(application._id).toBeDefined(); 23 | expect(application).toHaveProperty('createdAt'); 24 | expect(application).toHaveProperty('modifiedAt'); 25 | } 26 | }); 27 | 28 | test('test with object value of disctribution name field ', async () => { 29 | const distributionListData = [ 30 | { 31 | name: faker.science.unit(), 32 | emails: new Array(10).fill(faker.internet.email()), 33 | }, 34 | ]; 35 | 36 | const applicationData = { 37 | name: faker.word.adjective(), 38 | distributionLists: distributionListData, 39 | }; 40 | expect(async () => new Application(applicationData).save()).rejects.toThrow( 41 | Error 42 | ); 43 | }); 44 | }); 45 | -------------------------------------------------------------------------------- /__tests__/old/models/group.test.ts: -------------------------------------------------------------------------------- 1 | import { Group } from '@models'; 2 | import { faker } from '@faker-js/faker'; 3 | 4 | /** 5 | * Test Group Model. 6 | */ 7 | describe('Group models tests', () => { 8 | test('test with correct data', async () => { 9 | for (let i = 0; i < 1; i++) { 10 | const group = await new Group({ 11 | title: faker.random.alpha(10), 12 | description: faker.commerce.productDescription(), 13 | oid: faker.datatype.uuid(), 14 | }).save(); 15 | expect(group._id).toBeDefined(); 16 | expect(group).toHaveProperty('createdAt'); 17 | expect(group).toHaveProperty('modifiedAt'); 18 | } 19 | }); 20 | 21 | test('test with blank channel name field', async () => { 22 | const groupData = { 23 | title: faker.science.unit(), 24 | description: faker.commerce.productDescription(), 25 | oid: faker.datatype.uuid(), 26 | }; 27 | expect(async () => new Group(groupData).save()).rejects.toThrow(Error); 28 | }); 29 | }); 30 | -------------------------------------------------------------------------------- /__tests__/old/models/permission.test.ts: -------------------------------------------------------------------------------- 1 | import { Permission } from '@models'; 2 | import { faker } from '@faker-js/faker'; 3 | 4 | /** 5 | * Test Permission Model. 6 | */ 7 | describe('Permission models tests', () => { 8 | let permission: Permission; 9 | test('test with globel permission', async () => { 10 | for (let i = 0; i < 1; i++) { 11 | permission = await new Permission({ 12 | type: faker.random.word(), 13 | global: true, 14 | }).save(); 15 | expect(permission._id).toBeDefined(); 16 | } 17 | }); 18 | 19 | test('test with local permission', async () => { 20 | for (let i = 0; i < 1; i++) { 21 | const permissionData = await new Permission({ 22 | type: faker.random.word(), 23 | global: false, 24 | }).save(); 25 | expect(permissionData._id).toBeDefined(); 26 | } 27 | }); 28 | 29 | test('test permission with duplicate type', async () => { 30 | const duplicatePermission = { 31 | type: permission.type, 32 | global: true, 33 | }; 34 | expect(async () => 35 | new Permission(duplicatePermission).save() 36 | ).rejects.toMatchObject({ 37 | code: 11000, 38 | }); 39 | }); 40 | 41 | test('test with blank type permission', async () => { 42 | for (let i = 0; i < 1; i++) { 43 | const inputData = { 44 | type: '', 45 | global: false, 46 | }; 47 | expect(async () => new Permission(inputData).save()).rejects.toThrow( 48 | Error 49 | ); 50 | } 51 | }); 52 | }); 53 | -------------------------------------------------------------------------------- /__tests__/old/models/positionAttribute.test.ts: -------------------------------------------------------------------------------- 1 | import { 2 | PositionAttributeCategory, 3 | Application, 4 | PositionAttribute, 5 | } from '@models'; 6 | import { faker } from '@faker-js/faker'; 7 | import { status } from '@const/enumTypes'; 8 | 9 | /** 10 | * Test Position Attribute Model. 11 | */ 12 | 13 | beforeAll(async () => { 14 | await new Application({ 15 | name: faker.internet.userName(), 16 | status: status.pending, 17 | }).save(); 18 | }); 19 | 20 | describe('PositionAttribute models tests', () => { 21 | test('test PositionAttribute model with correct data', async () => { 22 | const application = await Application.findOne(); 23 | const attrCatg = await new PositionAttributeCategory({ 24 | title: faker.word.adjective(), 25 | application: application._id, 26 | }).save(); 27 | for (let i = 0; i < 1; i++) { 28 | const positionAttribute = await new PositionAttribute({ 29 | value: faker.word.adjective(), 30 | category: attrCatg._id, 31 | }).save(); 32 | expect(positionAttribute._id).toBeDefined(); 33 | } 34 | }); 35 | 36 | test('test PositionAttribute with invalid value', async () => { 37 | const application = await Application.find(); 38 | const attrCatg = await new PositionAttributeCategory({ 39 | title: faker.word.adjective(), 40 | application: application[application.length - 1]._id, 41 | }).save(); 42 | for (let i = 0; i < 1; i++) { 43 | const inputData = { 44 | value: faker.science.unit(), 45 | category: attrCatg._id, 46 | }; 47 | expect(async () => 48 | new PositionAttribute(inputData).save() 49 | ).rejects.toThrow(Error); 50 | } 51 | }); 52 | }); 53 | -------------------------------------------------------------------------------- /__tests__/old/models/template.test.ts: -------------------------------------------------------------------------------- 1 | import { Application } from '@models'; 2 | import { status } from '@const/enumTypes'; 3 | import { faker } from '@faker-js/faker'; 4 | 5 | /** 6 | * Test Template Model. 7 | */ 8 | describe('Template models tests', () => { 9 | test('test Template model with correct', async () => { 10 | for (let i = 0; i < 1; i++) { 11 | const templates = []; 12 | for (let j = 0; j < 1; j++) { 13 | templates.push({ 14 | name: faker.random.alpha(10), 15 | type: 'email', 16 | content: faker.commerce.productDescription(), 17 | }); 18 | } 19 | 20 | const inputData = { 21 | name: faker.internet.userName(), 22 | status: status.pending, 23 | templates: templates, 24 | }; 25 | const saveData = await new Application(inputData).save(); 26 | expect(saveData._id).toBeDefined(); 27 | expect(saveData).toHaveProperty('createdAt'); 28 | expect(saveData).toHaveProperty('modifiedAt'); 29 | } 30 | }); 31 | 32 | test('test Template model with wrong name', async () => { 33 | const templates = []; 34 | for (let j = 0; j < 1; j++) { 35 | templates.push({ 36 | name: faker.science.unit(), 37 | type: 'email', 38 | content: faker.commerce.productDescription(), 39 | }); 40 | } 41 | const inputData = { 42 | name: faker.internet.userName(), 43 | status: status.pending, 44 | templates: templates, 45 | }; 46 | expect(async () => new Application(inputData).save()).rejects.toThrow( 47 | Error 48 | ); 49 | }); 50 | }); 51 | -------------------------------------------------------------------------------- /__tests__/old/models/version.test.ts: -------------------------------------------------------------------------------- 1 | import { Version, Form, Record } from '@models'; 2 | 3 | /** 4 | * Test Version Model. 5 | */ 6 | describe('Version models tests', () => { 7 | test('test Version model with correct data with form structure', async () => { 8 | const forms = await Form.find(); 9 | const promises = forms.map((form) => { 10 | return new Version({ 11 | data: form.structure, 12 | }).save(); 13 | }); 14 | 15 | const versions = await Promise.all(promises); 16 | 17 | versions.forEach((version) => { 18 | expect(version._id).toBeDefined(); 19 | expect(version).toHaveProperty('createdAt'); 20 | }); 21 | }); 22 | 23 | test('test Version model with correct data with record data', async () => { 24 | const records = await Record.find(); 25 | for (let i = 0; i < records.length; i++) { 26 | const version = await new Version({ 27 | data: records[i].data, 28 | }).save(); 29 | expect(version._id).toBeDefined(); 30 | expect(version).toHaveProperty(['createdAt']); 31 | } 32 | }); 33 | }); 34 | -------------------------------------------------------------------------------- /__tests__/old/schema/query/apiConfigurations.test.ts: -------------------------------------------------------------------------------- 1 | import schema from '../../../src/schema'; 2 | import { acquireToken } from '../../authentication.setup'; 3 | import { SafeTestServer } from '../../server.setup'; 4 | import { ApiConfiguration, Role, User } from '@models'; 5 | import supertest from 'supertest'; 6 | 7 | let server: SafeTestServer; 8 | let request: supertest.SuperTest; 9 | let token: string; 10 | 11 | beforeAll(async () => { 12 | server = new SafeTestServer(); 13 | await server.start(schema); 14 | request = supertest(server.app); 15 | token = `Bearer ${await acquireToken()}`; 16 | }); 17 | 18 | /** 19 | * Test ApiConfigurations query. 20 | */ 21 | describe('ApiConfigurations query tests', () => { 22 | const query = '{ apiConfigurations { totalCount, edges { node { id } } } }'; 23 | 24 | test('query with admin user returns expected number of apiConfigurations', async () => { 25 | const count = await ApiConfiguration.countDocuments(); 26 | const admin = await Role.findOne({ title: 'admin' }); 27 | await User.updateOne( 28 | { username: 'dummy@dummy.com' }, 29 | { roles: [admin._id] } 30 | ); 31 | const response = await request 32 | .post('/graphql') 33 | .send({ query }) 34 | .set('Authorization', token) 35 | .set('Accept', 'application/json'); 36 | expect(response.body.errors).toBeUndefined(); 37 | expect(response.body).toHaveProperty([ 38 | 'data', 39 | 'apiConfigurations', 40 | 'totalCount', 41 | ]); 42 | expect(response.body.data.apiConfigurations.totalCount).toEqual(count); 43 | }); 44 | }); 45 | -------------------------------------------------------------------------------- /__tests__/old/schema/query/layers.test.ts: -------------------------------------------------------------------------------- 1 | import { Layer, Role, User } from '@models'; 2 | 3 | import supertest from 'supertest'; 4 | import schema from '../../../src/schema'; 5 | import { SafeTestServer } from '../../server.setup'; 6 | import { acquireToken } from '../../authentication.setup'; 7 | 8 | let server: SafeTestServer; 9 | let request: supertest.SuperTest; 10 | let token: string; 11 | 12 | beforeAll(async () => { 13 | server = new SafeTestServer(); 14 | await server.start(schema); 15 | request = supertest(server.app); 16 | token = `Bearer ${await acquireToken()}`; 17 | }); 18 | 19 | /** 20 | * Test Layers query. 21 | */ 22 | describe('Layers query tests', () => { 23 | const query = '{ layers { id, name } }'; 24 | 25 | test('query with admin user returns expected number of layers', async () => { 26 | const count = await Layer.countDocuments(); 27 | const admin = await Role.findOne({ title: 'admin' }); 28 | await User.updateOne( 29 | { username: 'dummy@dummy.com' }, 30 | { roles: [admin._id] } 31 | ); 32 | const response = await request 33 | .post('/graphql') 34 | .send({ query }) 35 | .set('Authorization', token) 36 | .set('Accept', 'application/json'); 37 | 38 | expect(response.body.errors).toBeUndefined(); 39 | expect(response.body).toHaveProperty(['data', 'layers']); 40 | expect(response.body.data?.layers.length).toEqual(count); 41 | response.body.data?.layers.forEach((prop) => { 42 | expect(prop).toHaveProperty('name'); 43 | }); 44 | }); 45 | }); 46 | -------------------------------------------------------------------------------- /__tests__/old/schema/query/me.test.ts: -------------------------------------------------------------------------------- 1 | import { User } from '../../../src/models'; 2 | 3 | import supertest from 'supertest'; 4 | import schema from '../../../src/schema'; 5 | import { SafeTestServer } from '../../server.setup'; 6 | import { acquireToken } from '../../authentication.setup'; 7 | 8 | let server: SafeTestServer; 9 | let request: supertest.SuperTest; 10 | let token: string; 11 | 12 | beforeAll(async () => { 13 | server = new SafeTestServer(); 14 | await server.start(schema); 15 | request = supertest(server.app); 16 | token = `Bearer ${await acquireToken()}`; 17 | }); 18 | 19 | /** 20 | * Test ME query. 21 | */ 22 | describe('ME query tests', () => { 23 | const query = '{ me { id username } }'; 24 | 25 | test('query with no token returns error', async () => { 26 | const response = await request 27 | .post('/graphql') 28 | .send({ query }) 29 | .set('Accept', 'application/json'); 30 | expect(response.body).toHaveProperty(['errors']); 31 | }); 32 | 33 | test('query with token should return user info', async () => { 34 | const user = await User.findOne({ username: 'dummy@dummy.com' }); 35 | const response = await request 36 | .post('/graphql') 37 | .send({ query }) 38 | .set('Authorization', token) 39 | .set('Accept', 'application/json'); 40 | expect(response.body.errors).toBeUndefined(); 41 | expect(response.body).toHaveProperty(['data']); 42 | expect(response.body.data?.me.id).toEqual(user.id); 43 | expect(response.body.data?.me.username).toEqual(user.username); 44 | }); 45 | }); 46 | -------------------------------------------------------------------------------- /__tests__/old/schema/query/referenceDatas.test.ts: -------------------------------------------------------------------------------- 1 | import { ReferenceData, Role, User } from '@models'; 2 | 3 | import supertest from 'supertest'; 4 | import schema from '../../../src/schema'; 5 | import { SafeTestServer } from '../../server.setup'; 6 | import { acquireToken } from '../../authentication.setup'; 7 | 8 | let server: SafeTestServer; 9 | let request: supertest.SuperTest; 10 | let token: string; 11 | 12 | beforeAll(async () => { 13 | server = new SafeTestServer(); 14 | await server.start(schema); 15 | request = supertest(server.app); 16 | token = `Bearer ${await acquireToken()}`; 17 | }); 18 | /** 19 | * Test ReferenceDatas query. 20 | */ 21 | describe('ReferenceDatas query tests', () => { 22 | const query = '{ referenceDatas { totalCount, edges { node { id } } } }'; 23 | 24 | test('query with admin user returns expected number of referenceDatas', async () => { 25 | const count = await ReferenceData.countDocuments(); 26 | const admin = await Role.findOne({ title: 'admin' }); 27 | await User.updateOne( 28 | { username: 'dummy@dummy.com' }, 29 | { roles: [admin._id] } 30 | ); 31 | const response = await request 32 | .post('/graphql') 33 | .send({ query }) 34 | .set('Authorization', token) 35 | .set('Accept', 'application/json'); 36 | 37 | expect(response.body.errors).toBeUndefined(); 38 | expect(response.body).toHaveProperty([ 39 | 'data', 40 | 'referenceDatas', 41 | 'totalCount', 42 | ]); 43 | expect(response.body.data?.referenceDatas.totalCount).toEqual(count); 44 | }); 45 | }); 46 | -------------------------------------------------------------------------------- /__tests__/old/schema/query/roles.test.ts: -------------------------------------------------------------------------------- 1 | import { Role, User } from '@models'; 2 | 3 | import supertest from 'supertest'; 4 | import schema from '../../../src/schema'; 5 | import { SafeTestServer } from '../../server.setup'; 6 | import { acquireToken } from '../../authentication.setup'; 7 | 8 | let server: SafeTestServer; 9 | let request: supertest.SuperTest; 10 | let token: string; 11 | 12 | beforeAll(async () => { 13 | server = new SafeTestServer(); 14 | await server.start(schema); 15 | request = supertest(server.app); 16 | token = `Bearer ${await acquireToken()}`; 17 | }); 18 | 19 | /** 20 | * Test Roles query. 21 | */ 22 | describe('Roles query tests', () => { 23 | const query = '{ roles { id, title } }'; 24 | 25 | test('query with admin user returns expected number of roles', async () => { 26 | const count = await Role.countDocuments({ application: null }); 27 | const admin = await Role.findOne({ title: 'admin' }); 28 | await User.updateOne( 29 | { username: 'dummy@dummy.com' }, 30 | { roles: [admin._id] } 31 | ); 32 | const response = await request 33 | .post('/graphql') 34 | .send({ query }) 35 | .set('Authorization', token) 36 | .set('Accept', 'application/json'); 37 | 38 | expect(response.body.errors).toBeUndefined(); 39 | expect(response.body).toHaveProperty(['data', 'roles']); 40 | expect(response.body.data?.roles.length).toEqual(count); 41 | response.body.data?.roles.forEach((prop) => { 42 | expect(prop).toHaveProperty('title'); 43 | }); 44 | }); 45 | }); 46 | -------------------------------------------------------------------------------- /__tests__/old/schema/query/users.test.ts: -------------------------------------------------------------------------------- 1 | import { User, Role } from '@models'; 2 | 3 | import supertest from 'supertest'; 4 | import schema from '../../../src/schema'; 5 | import { SafeTestServer } from '../../server.setup'; 6 | import { acquireToken } from '../../authentication.setup'; 7 | 8 | let server: SafeTestServer; 9 | let request: supertest.SuperTest; 10 | let token: string; 11 | 12 | beforeAll(async () => { 13 | server = new SafeTestServer(); 14 | await server.start(schema); 15 | request = supertest(server.app); 16 | token = `Bearer ${await acquireToken()}`; 17 | }); 18 | 19 | /** 20 | * Test Users query. 21 | */ 22 | describe('Users query tests', () => { 23 | const query = '{ users { totalCount, edges { node { username }} } }'; 24 | 25 | test('query with admin user returns expected number of users', async () => { 26 | const count = await User.countDocuments(); 27 | const admin = await Role.findOne({ title: 'admin' }); 28 | await User.updateOne( 29 | { username: 'dummy@dummy.com' }, 30 | { roles: [admin._id] } 31 | ); 32 | const response = await request 33 | .post('/graphql') 34 | .send({ query }) 35 | .set('Authorization', token) 36 | .set('Accept', 'application/json'); 37 | 38 | expect(response.body.errors).toBeUndefined(); 39 | expect(response.body).toHaveProperty(['data', 'users', 'totalCount']); 40 | expect(response.body.data.users.totalCount).toEqual(count); 41 | response.body.data.users.edges.forEach((prop) => { 42 | expect(prop.node).toHaveProperty('username'); 43 | }); 44 | }); 45 | }); 46 | -------------------------------------------------------------------------------- /__tests__/old/server/middlewares/rateLimit.test.ts: -------------------------------------------------------------------------------- 1 | import { rateLimitMiddleware } from '@server/middlewares'; 2 | import express from 'express'; 3 | import supertest from 'supertest'; 4 | import config from 'config'; 5 | 6 | /** Generate a basic application */ 7 | const app = express(); 8 | app.use(rateLimitMiddleware); 9 | 10 | app.get('', (req, res) => { 11 | res.statusCode = 200; 12 | res.end(); 13 | }); 14 | /** Mock requests */ 15 | const request = supertest(app); 16 | 17 | describe('RateLimit middleware', () => { 18 | describe('Many requests', () => { 19 | test('Should send an error when limit is reached', async () => { 20 | const rateLimit = Number(config.get('server.rateLimit.max')); 21 | for (let i = 0; i < rateLimit; i++) { 22 | const response = await request.get(''); 23 | expect(response.status).toBe(200); 24 | } 25 | const response = await request.get(''); 26 | expect(response.status).toBe(429); 27 | }); 28 | }); 29 | }); 30 | -------------------------------------------------------------------------------- /__tests__/old/utils/server/__snapshots__/checkConfig.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Check config util method Incorrect keys fail Empty string should fail 1`] = `"Process.exit(undefined)"`; 4 | 5 | exports[`Check config util method Incorrect keys fail Missing key should fail 1`] = `"Process.exit(undefined)"`; 6 | 7 | exports[`Check config util method Incorrect keys fail Null should fail 1`] = `"Process.exit(undefined)"`; 8 | 9 | exports[`Check config util method Incorrect keys fail Undefined should fail 1`] = `"Process.exit(undefined)"`; 10 | -------------------------------------------------------------------------------- /__tests__/old/utils/validators/validateApi.test.ts: -------------------------------------------------------------------------------- 1 | import { validateApi } from '@utils/validators'; 2 | import { faker } from '@faker-js/faker'; 3 | import { GraphQLError } from 'graphql'; 4 | 5 | /** 6 | * Test API validator. 7 | */ 8 | describe('API validator tests', () => { 9 | describe('Correct api name should return true', () => { 10 | const name = Array.from({ length: 100 }, () => faker.word.adjective()); 11 | test.each(name)('Random api name should pass', (string: string) => { 12 | const test = () => validateApi(string); 13 | expect(test).not.toThrow(); 14 | }); 15 | }); 16 | 17 | describe('Incorrect api should throw error', () => { 18 | const strings = new Array(1).fill(faker.internet.email()); 19 | test.each(strings)( 20 | 'Name with @ or . should throw error', 21 | (name: string) => { 22 | const test = () => validateApi(name); 23 | expect(test).toThrow(GraphQLError); 24 | } 25 | ); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /__tests__/old/utils/validators/validateEmail.test.ts: -------------------------------------------------------------------------------- 1 | import * as EmailValidator from 'email-validator'; 2 | import { faker } from '@faker-js/faker'; 3 | 4 | /** 5 | * Test email validator. 6 | */ 7 | describe('Email validator tests', () => { 8 | describe('Correct email should return true', () => { 9 | const emails = Array.from({ length: 100 }).map(() => 10 | faker.internet.email() 11 | ); 12 | test.each(emails)( 13 | 'Random valid email should return true', 14 | (email: string) => { 15 | expect(EmailValidator.validate(email)).toEqual(true); 16 | } 17 | ); 18 | 19 | const complexEmails = Array.from({ length: 100 }).map(() => 20 | faker.internet.email(undefined, undefined, undefined, { 21 | allowSpecialCharacters: true, 22 | }) 23 | ); 24 | test.each(complexEmails)( 25 | 'Random valid email with special characters should return true', 26 | (email: string) => { 27 | expect(EmailValidator.validate(email)).toEqual(true); 28 | } 29 | ); 30 | }); 31 | 32 | describe('Random strings should return false', () => { 33 | const strings = Array.from({ length: 100 }).map(() => 34 | faker.internet.userName() 35 | ); 36 | test.each(strings)('Random string should return false', (text: string) => { 37 | expect(EmailValidator.validate(text)).toEqual(false); 38 | }); 39 | }); 40 | }); 41 | -------------------------------------------------------------------------------- /__tests__/unit-tests/utils/schema/resolvers/Query/getSortOrder.spec.ts: -------------------------------------------------------------------------------- 1 | import decodeSortOrder from '@utils/schema/resolvers/Query/getSortOrder'; 2 | 3 | describe('decodeSortOrder', () => { 4 | it('should return 1 for ascending order', () => { 5 | const result = decodeSortOrder('asc'); 6 | expect(result).toBe(1); 7 | }); 8 | 9 | it('should return -1 for descending order', () => { 10 | const result = decodeSortOrder('desc'); 11 | expect(result).toBe(-1); 12 | }); 13 | }); 14 | -------------------------------------------------------------------------------- /__tests__/unit-tests/utils/validators/validateApi.spec.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLError } from 'graphql'; 2 | import i18next from 'i18next'; 3 | import { validateApi } from '@utils/validators'; 4 | 5 | // Mock i18next translation 6 | jest.mock('i18next', () => ({ 7 | t: jest.fn((key: string) => key), // Returns the key itself for simplicity 8 | })); 9 | 10 | describe('validateApi', () => { 11 | beforeEach(() => { 12 | jest.clearAllMocks(); 13 | }); 14 | 15 | it('should not throw an error for valid API names', () => { 16 | const validNames = ['ApiName', 'Another-Name', 'api_name']; 17 | 18 | validNames.forEach((name) => { 19 | expect(() => validateApi(name)).not.toThrow(); 20 | }); 21 | }); 22 | 23 | it('should throw a GraphQLError for invalid API names', () => { 24 | const invalidNames = ['Invalid Name', '123Invalid', 'Invalid@Name', '']; 25 | 26 | invalidNames.forEach((name) => { 27 | expect(() => validateApi(name)).toThrow(GraphQLError); 28 | expect(() => validateApi(name)).toThrow( 29 | i18next.t('common.errors.invalidGraphQLName') 30 | ); 31 | }); 32 | }); 33 | 34 | it('should call i18next.t with the correct key on error', () => { 35 | const invalidName = 'Invalid Name'; 36 | 37 | try { 38 | validateApi(invalidName); 39 | } catch (e) { 40 | expect(i18next.t).toHaveBeenCalledWith( 41 | 'common.errors.invalidGraphQLName' 42 | ); 43 | } 44 | }); 45 | }); 46 | -------------------------------------------------------------------------------- /config/oort.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Configuration of back-office 3 | * Use https://www.npmjs.com/package/config package. 4 | */ 5 | module.exports = { 6 | email: { 7 | sendInvite: true, 8 | }, 9 | user: { 10 | groups: { 11 | local: true, 12 | }, 13 | attributes: { 14 | local: true, 15 | }, 16 | }, 17 | }; 18 | -------------------------------------------------------------------------------- /config/test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Configuration of back-office 3 | * Use https://www.npmjs.com/package/config package. 4 | */ 5 | 6 | module.exports = { 7 | database: { 8 | provider: 'docker', 9 | prefix: 'mongodb', 10 | host: 'mongodb_test', 11 | port: '27017', 12 | name: 'test', 13 | user: 'root', 14 | pass: '123', 15 | }, 16 | auth: { 17 | url: 'https://id-dev.oortcloud.tech/auth', 18 | clientId: 'ci-client', 19 | realm: 'oort', 20 | provider: 'keycloak', 21 | }, 22 | logger: { 23 | keep: false, 24 | }, 25 | }; 26 | -------------------------------------------------------------------------------- /config/who.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Standard configuration. 3 | * Use https://www.npmjs.com/package/config package. 4 | */ 5 | module.exports = { 6 | server: { 7 | rateLimit: { 8 | enable: false, 9 | max: 500, 10 | }, 11 | }, 12 | email: { 13 | sendInvite: false, 14 | }, 15 | user: { 16 | groups: { 17 | local: true, 18 | }, 19 | attributes: { 20 | local: true, 21 | }, 22 | useMicrosoftGraph: true, 23 | }, 24 | }; 25 | -------------------------------------------------------------------------------- /docker-compose.test.yml: -------------------------------------------------------------------------------- 1 | version: "3.7" 2 | 3 | services: 4 | test-server: 5 | build: 6 | context: . 7 | dockerfile: Dockerfile 8 | target: base 9 | volumes: 10 | - ./:/home/node/app/ 11 | container_name: ems-ui-poc-api-test 12 | expose: 13 | - 4000 14 | ports: 15 | - 4000:4000 16 | command: npm run test 17 | depends_on: 18 | - mongodb_test 19 | links: 20 | - mongodb_test 21 | environment: 22 | - NODE_CONFIG_ENV=test 23 | - NODE_OPTIONS=--max-old-space-size=8192 24 | 25 | mongodb_test: 26 | image: mongo:6 27 | restart: unless-stopped 28 | environment: 29 | - MONGO_INITDB_ROOT_USERNAME=root 30 | - MONGO_INITDB_ROOT_PASSWORD=123 31 | ports: 32 | - 27017:27017 33 | volumes: 34 | - mongodb_test:/data/db 35 | command: mongod --quiet --logpath /dev/null 36 | 37 | volumes: 38 | mongodb_test: 39 | driver: local 40 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.7' 2 | services: 3 | api: 4 | container_name: ems-ui-poc-api 5 | restart: always 6 | env_file: 7 | - .env 8 | build: 9 | context: . 10 | dockerfile: Dockerfile 11 | target: base 12 | ports: 13 | - '3000:3000' 14 | - '9229:9229' 15 | expose: 16 | - '3000' 17 | - '9229' 18 | volumes: 19 | - ./config:/home/node/app/config 20 | - ./src:/home/node/app/src 21 | - .docker/api/logs:/home/node/app/logs 22 | command: npm run dev 23 | -------------------------------------------------------------------------------- /exclude-list.txt: -------------------------------------------------------------------------------- 1 | .env 2 | .env.dev 3 | .env.dist 4 | schema.graphql 5 | __tests__ 6 | coverage 7 | .husky 8 | .github 9 | .vscode 10 | -------------------------------------------------------------------------------- /jest.config.ts: -------------------------------------------------------------------------------- 1 | import { pathsToModuleNameMapper } from 'ts-jest'; 2 | 3 | /** Duplicate the paths found in tsconfig. */ 4 | const paths = { 5 | '@const/*': ['const/*'], 6 | '@models': ['models'], 7 | '@models/*': ['models/*'], 8 | '@routes/*': ['routes/*'], 9 | '@schema/*': ['schema/*'], 10 | '@security/*': ['security/*'], 11 | '@server/*': ['server/*'], 12 | '@services/*': ['services/*'], 13 | '@utils/*': ['utils/*'], 14 | '@lib/*': ['lib/*'], 15 | }; 16 | 17 | module.exports = { 18 | preset: 'ts-jest', 19 | testEnvironment: 'node', 20 | setupFiles: ['dotenv/config'], 21 | roots: ['./__tests__'], 22 | testMatch: ['**/__tests__/**/*.spec.ts'], 23 | collectCoverageFrom: ['src/**/*.{js,jsx,ts,tsx}'], 24 | modulePathIgnorePatterns: ['./__tests__/old'], 25 | // A map from regular expressions to module names or to arrays of module names that allow to stub out resources with a single module 26 | moduleNameMapper: pathsToModuleNameMapper(paths, { 27 | prefix: '/src/', 28 | }), 29 | }; 30 | -------------------------------------------------------------------------------- /migrations/1663751971645-init.ts: -------------------------------------------------------------------------------- 1 | import { startDatabaseForMigration } from '../src/migrations/database.helper'; 2 | import { initDatabase } from '../src/server/database'; 3 | 4 | /** Migration description */ 5 | export const description = 'First migration, to initialize the database.'; 6 | 7 | /** 8 | * Use to init migrate up. 9 | * 10 | * @returns just migrate data. 11 | */ 12 | export const up = async () => { 13 | await startDatabaseForMigration(); 14 | try { 15 | await initDatabase(); 16 | } catch (err) { 17 | throw err; 18 | } 19 | }; 20 | 21 | /** 22 | * Use to init migrate down. 23 | * 24 | * @returns just migrate data. 25 | */ 26 | export const down = async () => { 27 | /* 28 | Code you downgrade script here! 29 | */ 30 | }; 31 | -------------------------------------------------------------------------------- /migrations/1663832608220-generate-graphqltype-names.ts: -------------------------------------------------------------------------------- 1 | import { startDatabaseForMigration } from '../src/migrations/database.helper'; 2 | import { Form, ReferenceData } from '../src/models'; 3 | import { logger } from '../src/services/logger.service'; 4 | 5 | /** Migration description */ 6 | export const description = 7 | 'Add graphql type names to form & reference data objects'; 8 | 9 | /** 10 | * Use to graphqltypenames migrate up. 11 | * 12 | * @returns just migrate data. 13 | */ 14 | export const up = async () => { 15 | await startDatabaseForMigration(); 16 | const forms = await Form.find({ graphQLTypeName: { $exists: false } }).select( 17 | 'name' 18 | ); 19 | for (const form of forms) { 20 | await form.updateOne({ 21 | graphQLTypeName: Form.getGraphQLTypeName(form.name), 22 | }); 23 | } 24 | 25 | // update reference data 26 | const referenceDatas = await ReferenceData.find({ 27 | graphQLTypeName: { $exists: false }, 28 | }).select('name'); 29 | for (const referenceData of referenceDatas) { 30 | await referenceData.updateOne({ 31 | graphQLTypeName: ReferenceData.getGraphQLTypeName(referenceData.name), 32 | }); 33 | } 34 | 35 | logger.info('\nMigration complete'); 36 | }; 37 | 38 | /** 39 | * Use to graphqltypenames migrate down. 40 | * 41 | * @returns just migrate data. 42 | */ 43 | export const down = async () => { 44 | /* 45 | Code you downgrade script here! 46 | */ 47 | }; 48 | -------------------------------------------------------------------------------- /migrations/1669623288983-add-manage-custom-notifications-permission.ts: -------------------------------------------------------------------------------- 1 | import { Permission } from '@models/permission.model'; 2 | import { logger } from '@services/logger.service'; 3 | import { startDatabaseForMigration } from '../src/migrations/database.helper'; 4 | 5 | /** Migration description */ 6 | export const description = 'Add manage notifications permission'; 7 | 8 | /** 9 | * Sample function of up migration 10 | * 11 | * @returns just migrate data. 12 | */ 13 | export const up = async () => { 14 | await startDatabaseForMigration(); 15 | const type = 'can_manage_custom_notifications'; 16 | 17 | // check if permission already exists 18 | const permissionExists = await Permission.exists({ type, global: false }); 19 | if (permissionExists) { 20 | logger.info(`${type} permission already exists`); 21 | return; 22 | } 23 | 24 | const permission = new Permission({ 25 | type, 26 | global: false, 27 | }); 28 | await permission.save(); 29 | logger.info(`${type} application's permission created`); 30 | }; 31 | 32 | /** 33 | * Sample function of down migration 34 | * 35 | * @returns just migrate data. 36 | */ 37 | export const down = async () => { 38 | /* 39 | Code you downgrade script here! 40 | */ 41 | }; 42 | -------------------------------------------------------------------------------- /migrations/1678374386633-layerpermissions.ts: -------------------------------------------------------------------------------- 1 | import permissions from '@const/permissions'; 2 | import { Permission } from '@models'; 3 | import { logger } from '@services/logger.service'; 4 | import { startDatabaseForMigration } from '../src/migrations/database.helper'; 5 | 6 | /** Migration description */ 7 | export const description = 'Add new layer permissions'; 8 | 9 | /** 10 | * Sample function of up migration 11 | * 12 | * @returns just migrate data. 13 | */ 14 | export const up = async () => { 15 | await startDatabaseForMigration(); 16 | const typesToAdd = [permissions.canSeeLayer, permissions.canManageLayer]; 17 | 18 | for (const type of typesToAdd) { 19 | // check if permission already exists 20 | const permissionExists = await Permission.exists({ 21 | type, 22 | global: true, 23 | }); 24 | if (permissionExists) { 25 | logger.info(`${type} permission already exists`); 26 | return; 27 | } 28 | 29 | const permission = new Permission({ 30 | type, 31 | global: true, 32 | }); 33 | await permission.save(); 34 | logger.info(`${type} permission created`); 35 | } 36 | }; 37 | 38 | /** 39 | * Sample function of down migration 40 | * 41 | * @returns just migrate data. 42 | */ 43 | export const down = async () => { 44 | /* 45 | Code you downgrade script here! 46 | */ 47 | }; 48 | -------------------------------------------------------------------------------- /migrations/1684161314765-add-manage-distribution-list-and-template-permissions.ts: -------------------------------------------------------------------------------- 1 | import { Permission } from '@models/permission.model'; 2 | import { logger } from '@services/logger.service'; 3 | import { startDatabaseForMigration } from '../src/migrations/database.helper'; 4 | 5 | /** Migration description */ 6 | export const description = 'Add new notification permissions'; 7 | 8 | /** 9 | * Sample function of up migration 10 | * 11 | * @returns just migrate data. 12 | */ 13 | export const up = async () => { 14 | await startDatabaseForMigration(); 15 | const types = ['can_manage_distribution_lists', 'can_manage_templates']; 16 | 17 | for (const type of types) { 18 | // check if permission already exists 19 | const permissionExists = await Permission.exists({ type, global: false }); 20 | if (permissionExists) { 21 | logger.info(`${type} permission already exists`); 22 | return; 23 | } 24 | 25 | const permission = new Permission({ 26 | type, 27 | global: false, 28 | }); 29 | await permission.save(); 30 | logger.info(`${type} application's permission created`); 31 | } 32 | }; 33 | 34 | /** 35 | * Sample function of down migration 36 | * 37 | * @returns just migrate data. 38 | */ 39 | export const down = async () => { 40 | /* 41 | Code you downgrade script here! 42 | */ 43 | }; 44 | -------------------------------------------------------------------------------- /rabbitmq/enabled_plugins: -------------------------------------------------------------------------------- 1 | [rabbitmq_management]. -------------------------------------------------------------------------------- /release.config.js: -------------------------------------------------------------------------------- 1 | /** github ref */ 2 | const ref = process.env.GITHUB_REF; 3 | /** Name of the branch */ 4 | const branch = ref.split('/').pop(); 5 | /** If main branch, then use main changelog. Otherwise, use other changelogs. */ 6 | const changelog = 7 | 'main' === branch ? 'CHANGELOG.md' : `CHANGELOG/CHANGELOG_${branch}.md`; 8 | 9 | module.exports = { 10 | branches: [ 11 | '+([0-9])?(.{+([0-9]),x}).x', 12 | 'main', 13 | { 14 | name: 'next', 15 | prerelease: 'rc', 16 | }, 17 | { 18 | name: 'beta', 19 | prerelease: true, 20 | }, 21 | { 22 | name: 'alpha', 23 | prerelease: true, 24 | }, 25 | ], 26 | plugins: [ 27 | '@semantic-release/commit-analyzer', 28 | '@semantic-release/release-notes-generator', 29 | [ 30 | '@semantic-release/changelog', 31 | { 32 | changelogFile: changelog, 33 | }, 34 | ], 35 | [ 36 | '@semantic-release/npm', 37 | { 38 | npmPublish: false, 39 | }, 40 | ], 41 | [ 42 | '@semantic-release/github', 43 | { 44 | successComment: false, 45 | }, 46 | ], 47 | [ 48 | '@semantic-release/git', 49 | { 50 | assets: [ 51 | changelog, 52 | 'package.json', 53 | 'package-lock.json', 54 | 'npm-shrinkwrap.json', 55 | ], 56 | }, 57 | ], 58 | ], 59 | }; 60 | -------------------------------------------------------------------------------- /setup-hooks.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Configure Git to set the path for hooks 4 | git config --local core.hooksPath .github/.githooks 5 | 6 | echo "Git hooks have been set up successfully!" 7 | -------------------------------------------------------------------------------- /src/abstractions/api-error.ts: -------------------------------------------------------------------------------- 1 | import { ReasonPhrases, StatusCodes } from 'http-status-codes'; 2 | 3 | /** Error interface */ 4 | export interface Error { 5 | status: number; 6 | fields: { 7 | name: { 8 | message: string; 9 | }; 10 | }; 11 | message: string; 12 | name: string; 13 | } 14 | 15 | /** Api Error class */ 16 | class ApiError extends Error implements Error { 17 | public status = 500; 18 | 19 | public success = false; 20 | 21 | public fields: { name: { message: string } } = { name: { message: '' } }; 22 | 23 | /** 24 | * Api Error class 25 | * 26 | * @param msg Message 27 | * @param statusCode Http status code number 28 | * @param name Error name 29 | */ 30 | constructor( 31 | msg: string, 32 | statusCode: number, 33 | name: string = ReasonPhrases.INTERNAL_SERVER_ERROR 34 | ) { 35 | super(); 36 | this.message = msg; 37 | this.status = statusCode; 38 | this.name = 39 | statusCode === StatusCodes.NOT_FOUND ? ReasonPhrases.NOT_FOUND : name; 40 | } 41 | } 42 | 43 | export default ApiError; 44 | -------------------------------------------------------------------------------- /src/abstractions/base.controller.ts: -------------------------------------------------------------------------------- 1 | import { Response } from 'express'; 2 | import { StatusCodes } from 'http-status-codes'; 3 | import { RouteDefinition } from '../types/route-definition'; 4 | 5 | /** 6 | * Provides services common to all API methods 7 | */ 8 | export default abstract class BaseController { 9 | public abstract routes(): RouteDefinition[]; 10 | 11 | /** 12 | * Global method to send API response. 13 | * 14 | * @param res Express response 15 | * @param statusCode http status code 16 | */ 17 | public send(res: Response, statusCode: number = StatusCodes.OK): void { 18 | let obj = {}; 19 | obj = res.locals.data; 20 | res.status(statusCode).send(obj); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/assets/emails/app-invitation/html.pug: -------------------------------------------------------------------------------- 1 | doctype html 2 | html 3 | head 4 | block head 5 | meta(charset="utf-8") 6 | meta(name="viewport", content="width=device-width") 7 | meta(http-equiv="X-UA-Compatible", content="IE=edge") 8 | meta(name="x-apple-disable-message-reformatting") 9 | link(rel="stylesheet", href="style.css", data-inline) 10 | body 11 | div.frame.center 12 | img.head(src="https://doc.oortcloud.tech/img/logo.png" alt="Oort - Data management system") 13 | h3.head 14 | | #{senderName} shared the 15 | span.oort-color #{appName} 16 | | application with you 17 | 18 | p #{senderName} shared with you the #{appName} application on Oort. 19 | p You can access this application by clicking the button below: 20 | p 21 | a.button(href=url target="_blank") Open the application 22 | p 23 | strong Note: 24 | | If you get a 404 page, make sure you’re signed with your account. 25 | | You can also access the application page directly at 26 | a(href=url) #{url} 27 | | . 28 | p.footer 29 | | Oort Cloud Technology ⋅ 30 | | Calle Penas, 70, 28410 Manzanares el Real ⋅ 31 | | Madrid Spain - B87964946 32 | -------------------------------------------------------------------------------- /src/assets/emails/app-invitation/subject.pug: -------------------------------------------------------------------------------- 1 | = `[OORT] ${senderName} shared the ${appName} application with you` -------------------------------------------------------------------------------- /src/assets/emails/create-account-to-app/subject.pug: -------------------------------------------------------------------------------- 1 | = `[OORT] ${senderName} has invited you to join the ${appName} application` -------------------------------------------------------------------------------- /src/assets/emails/create-account/html.pug: -------------------------------------------------------------------------------- 1 | doctype html 2 | html 3 | head 4 | block head 5 | meta(charset="utf-8") 6 | meta(name="viewport", content="width=device-width") 7 | meta(http-equiv="X-UA-Compatible", content="IE=edge") 8 | meta(name="x-apple-disable-message-reformatting") 9 | link(rel="stylesheet", href="style.css", data-inline) 10 | body 11 | div.frame.center 12 | img.head(src="https://doc.oortcloud.tech/img/logo.png" alt="Oort - Data management system") 13 | h3.head 14 | | #{senderName} has invited you to join the 15 | span.oort-color Oort 16 | | platform 17 | 18 | p #{senderName} has invited you to join Oort, a data management platform. 19 | p You can now create your account by clicking the button below: 20 | p 21 | a.button(href=url target="_blank") Access the website 22 | p 23 | | Then click on 24 | em Register 25 | | , validate your account, and enjoy all the features of Oort! 26 | 27 | p 28 | strong Note: 29 | | You can only create your account during the 7 days following the 30 | | reception of this email. If the invitation has expired, please ask 31 | | the owner of the application to send you another invitation. 32 | p 33 | | You can also access the website directly at 34 | a(href=url) #{url} 35 | | . 36 | 37 | p.footer 38 | | Oort Cloud Technology ⋅ 39 | | Calle Penas, 70, 28410 Manzanares el Real ⋅ 40 | | Madrid Spain - B87964946 41 | -------------------------------------------------------------------------------- /src/assets/emails/create-account/subject.pug: -------------------------------------------------------------------------------- 1 | = `[OORT] ${senderName} has invited you to join the Oort platform` -------------------------------------------------------------------------------- /src/assets/emails/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | box-sizing: border-box; 3 | font-family: 'Segoe UI', Helvetica, Arial, sans-serif; 4 | font-size: 14px; 5 | line-height: 1.5; 6 | color: #24292e; 7 | margin: 0; 8 | } 9 | 10 | div.frame { 11 | max-width: 400px; 12 | margin: 2rem auto; 13 | padding: 1rem; 14 | border: #ddd solid 1px; 15 | border-radius: 5px; 16 | background-color: white; 17 | } 18 | .large { 19 | max-width: 500px !important; 20 | } 21 | 22 | img.head { 23 | max-width: 220px; 24 | margin: 1rem 0 2rem; 25 | } 26 | 27 | h3.head { 28 | margin-top: 0; 29 | margin-bottom: 15px; 30 | font-size: 20px; 31 | font-weight: 600; 32 | line-height: 1.25; 33 | } 34 | 35 | p { 36 | margin-top: 0; 37 | margin-bottom: 10px; 38 | } 39 | 40 | li { 41 | color: #6f51ae; 42 | font-weight: bold; 43 | } 44 | li > span { 45 | color: #24292e; 46 | font-weight: initial; 47 | } 48 | 49 | .center { 50 | text-align: center; 51 | } 52 | 53 | .oort-color, a { 54 | color: #6f51ae; 55 | } 56 | 57 | .button { 58 | background-color: #6f51ae; 59 | color: white; 60 | text-decoration-line: none; 61 | font-weight: 500; 62 | line-height: 4; 63 | white-space: nowrap; 64 | vertical-align: middle; 65 | border-radius: 0.5em; 66 | padding: .8em 1em; 67 | } 68 | 69 | .footer { 70 | margin-top: 40px; 71 | color: #6a737d; 72 | font-size: 12px; 73 | } 74 | -------------------------------------------------------------------------------- /src/const/channels.ts: -------------------------------------------------------------------------------- 1 | /** List of default notifications channels */ 2 | const channels = { 3 | applications: 'applications', 4 | }; 5 | 6 | export default channels; 7 | -------------------------------------------------------------------------------- /src/const/fieldTypes.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/naming-convention */ 2 | /** List of multiselect field types */ 3 | export const MULTISELECT_TYPES: string[] = [ 4 | 'checkbox', 5 | 'tagbox', 6 | 'owner', 7 | 'users', 8 | ]; 9 | 10 | /** List of date field types */ 11 | export const DATE_TYPES: string[] = ['date']; 12 | 13 | /** List of date with time field types */ 14 | export const DATETIME_TYPES: string[] = ['datetime', 'datetime-local']; 15 | -------------------------------------------------------------------------------- /src/const/permissions.ts: -------------------------------------------------------------------------------- 1 | /** Admin permissions */ 2 | const permissions = { 3 | canSeeResources: 'can_see_resources', 4 | canSeeForms: 'can_see_forms', 5 | canSeeUsers: 'can_see_users', 6 | canSeeRoles: 'can_see_roles', 7 | canSeeGroups: 'can_see_groups', 8 | canSeeApplications: 'can_see_applications', 9 | canCreateApplications: 'can_create_applications', 10 | canCreateForms: 'can_create_forms', 11 | canCreateResources: 'can_create_resources', 12 | canManageApiConfigurations: 'can_manage_api_configurations', 13 | canManageApplications: 'can_manage_applications', 14 | canManageForms: 'can_manage_forms', 15 | canManageResources: 'can_manage_resources', 16 | canManageTemplates: 'can_manage_templates', 17 | canManageDistributionLists: 'can_manage_distribution_lists', 18 | canManageLayer: 'can_manage_layer', 19 | canSeeLayer: 'can_see_layer', 20 | canManageCustomNotifications: 'can_manage_custom_notifications', // Deprecated 21 | canSeeEmailNotifications: 'can_see_email_notifications', 22 | canUpdateEmailNotifications: 'can_update_email_notifications', 23 | canManageEmailNotifications: 'can_manage_email_notifications', 24 | canCreateEmailNotifications: 'can_create_email_notifications', 25 | }; 26 | 27 | export default permissions; 28 | -------------------------------------------------------------------------------- /src/const/placeholders.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/naming-convention */ 2 | /** Enum definition for placeholder text */ 3 | export enum Placeholder { 4 | TODAY = '{{today}}', 5 | DATASET = '{{dataset}}', 6 | NOW = '{{now}}', 7 | LAST_UPDATE = '{{lastUpdate}}', 8 | } 9 | 10 | /** Regex to detect placeholder usage. */ 11 | export const BASE_PLACEHOLDER_REGEX = new RegExp('{{.*?}}'); 12 | 13 | /** Regex expression that matches 'today + number of days' */ 14 | export const REGEX_TODAY_PLUS = new RegExp('{{today ?\\+ ?\\d+}}'); 15 | 16 | /** Regex expression that matches 'today - number of days' */ 17 | export const REGEX_TODAY_MINUS = new RegExp('{{today ?\\- ?\\d+}}'); 18 | 19 | /** 20 | * Tests whether value is using the {{today +-int}} placeholder 21 | * 22 | * @param value value to test 23 | * @returns true if using {{today}} 24 | */ 25 | export const isUsingTodayPlaceholder = (value: any) => { 26 | return ( 27 | value === Placeholder.TODAY || 28 | REGEX_TODAY_MINUS.test(value) || 29 | REGEX_TODAY_PLUS.test(value) 30 | ); 31 | }; 32 | 33 | /** 34 | * Tests whether value is using the {{now}} placeholder 35 | * 36 | * @param value value to test 37 | * @returns true if using {{now}} 38 | */ 39 | export const isUsingNowPlaceholder = (value: any) => { 40 | return value === Placeholder.NOW; 41 | }; 42 | 43 | /** 44 | * Extract string contained into brackets used for placeholders. 45 | * 46 | * @param str input string containing placeholder syntax 47 | * @returns string contained into brackets. 48 | */ 49 | export const extractStringFromBrackets = (str: string): string => { 50 | return str.substring(2, str.length - 2); 51 | }; 52 | -------------------------------------------------------------------------------- /src/const/protectedNames.ts: -------------------------------------------------------------------------------- 1 | /** List of protected names which a user cannot use */ 2 | const protectedNames = [ 3 | 'access', 4 | 'application', 5 | 'channel', 6 | 'dashboard', 7 | 'form', 8 | 'index', 9 | 'notification', 10 | 'page', 11 | 'permission', 12 | 'record', 13 | 'resource', 14 | 'role', 15 | 'step', 16 | 'user', 17 | 'version', 18 | 'workflow', 19 | ]; 20 | 21 | export default protectedNames; 22 | -------------------------------------------------------------------------------- /src/migrations/database.helper.ts: -------------------------------------------------------------------------------- 1 | import { startDatabase } from '@server/database'; 2 | import dotenv from 'dotenv'; 3 | dotenv.config(); 4 | 5 | /** 6 | * Use to create database connection. 7 | * 8 | * @returns database connection 9 | */ 10 | export const startDatabaseForMigration = async () => { 11 | await startDatabase({ 12 | // autoReconnect: true, 13 | // reconnectInterval: 5000, 14 | // reconnectTries: 3, 15 | // poolSize: 10, 16 | }); 17 | }; 18 | -------------------------------------------------------------------------------- /src/migrations/index.ts: -------------------------------------------------------------------------------- 1 | import mongoose from 'mongoose'; 2 | import MongoStorage from './stateStore'; 3 | import { load } from 'migrate'; 4 | import { startDatabase } from '@server/database'; 5 | 6 | /** Command, up or down */ 7 | const command = process.argv[2] || 'up'; 8 | /** Migration to target */ 9 | const targetMigration = process.argv[3] || undefined; 10 | 11 | /** 12 | * Run migrations 13 | */ 14 | async function runMigrations() { 15 | const storage = new MongoStorage(); 16 | await startDatabase(); 17 | 18 | load({ stateStore: storage }, (err: Error | null, set) => { 19 | if (err) throw err; 20 | const callback = (error: Error | null) => { 21 | if (error) { 22 | console.error(`Migration ${command} failed:`, error); 23 | } else { 24 | console.log(`Migration ${command} successfully finished`); 25 | } 26 | mongoose.disconnect(); 27 | }; 28 | switch (command) { 29 | case 'up': 30 | if (targetMigration) { 31 | set.up(targetMigration, callback); 32 | } else { 33 | set.up(callback); 34 | } 35 | break; 36 | case 'down': 37 | if (targetMigration) { 38 | set.down(targetMigration, callback); 39 | } else { 40 | set.down(callback); 41 | } 42 | break; 43 | default: 44 | console.error('Unknown command'); 45 | mongoose.disconnect(); 46 | return; 47 | } 48 | }); 49 | } 50 | 51 | runMigrations().catch((err) => console.error(err)); 52 | -------------------------------------------------------------------------------- /src/migrations/template.ts: -------------------------------------------------------------------------------- 1 | import { startDatabaseForMigration } from '../src/migrations/database.helper'; 2 | 3 | /** Migration description */ 4 | export const description = '***'; 5 | 6 | /** 7 | * Sample function of up migration 8 | * 9 | * @returns just migrate data. 10 | */ 11 | export const up = async () => { 12 | await startDatabaseForMigration(); 13 | /* 14 | Code your update script here! 15 | */ 16 | }; 17 | 18 | /** 19 | * Sample function of down migration 20 | * 21 | * @returns just migrate data. 22 | */ 23 | export const down = async () => { 24 | /* 25 | Code you downgrade script here! 26 | */ 27 | }; 28 | -------------------------------------------------------------------------------- /src/migrations/ts-compiler.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-var-requires */ 2 | /* eslint-disable import/no-extraneous-dependencies */ 3 | /* eslint-disable jsdoc/require-jsdoc */ 4 | /* eslint-disable eol-last */ 5 | /* eslint-disable prettier/prettier */ 6 | const tsNode = require('ts-node'); 7 | require('tsconfig-paths/register'); 8 | module.exports = tsNode.register; -------------------------------------------------------------------------------- /src/models/activityLog.model.ts: -------------------------------------------------------------------------------- 1 | import { AccessibleRecordModel, accessibleRecordsPlugin } from '@casl/mongoose'; 2 | import mongoose, { Schema, Document } from 'mongoose'; 3 | 4 | /** Mongoose activity log schema declaration */ 5 | export interface ActivityLog extends Document { 6 | kind: 'ActivityLog'; 7 | userId: mongoose.Types.ObjectId; 8 | eventType: string; 9 | metadata: any; 10 | username: string; 11 | attributes: any; 12 | createdAt: Date; 13 | } 14 | 15 | /** Activity log documents interface declaration */ 16 | const schema = new Schema( 17 | { 18 | userId: Schema.Types.ObjectId, 19 | eventType: String, 20 | username: String, 21 | metadata: Schema.Types.Mixed, 22 | attributes: Schema.Types.Mixed, 23 | }, 24 | { 25 | timestamps: { createdAt: 'createdAt' }, 26 | } 27 | ); 28 | 29 | schema.plugin(accessibleRecordsPlugin); 30 | 31 | /** Mongoose activity log model */ 32 | // eslint-disable-next-line @typescript-eslint/no-redeclare 33 | export const ActivityLog = mongoose.model< 34 | ActivityLog, 35 | AccessibleRecordModel 36 | >('ActivityLog', schema); 37 | -------------------------------------------------------------------------------- /src/models/aggregation.model.ts: -------------------------------------------------------------------------------- 1 | import mongoose, { Schema } from 'mongoose'; 2 | 3 | /** Mongoose aggregation schema declaration */ 4 | export const aggregationSchema = new Schema( 5 | { 6 | name: String, 7 | sourceFields: mongoose.Schema.Types.Mixed, 8 | pipeline: mongoose.Schema.Types.Mixed, 9 | }, 10 | { 11 | timestamps: { createdAt: 'createdAt', updatedAt: 'modifiedAt' }, 12 | } 13 | ); 14 | 15 | /** Aggregation documents interface declaration */ 16 | export interface Aggregation extends Document { 17 | kind: 'Aggregation'; 18 | name?: string; 19 | createdAt?: Date; 20 | modifiedAt?: Date; 21 | sourceFields?: any; 22 | pipeline?: any; 23 | } 24 | -------------------------------------------------------------------------------- /src/models/channel.model.ts: -------------------------------------------------------------------------------- 1 | import { AccessibleRecordModel, accessibleRecordsPlugin } from '@casl/mongoose'; 2 | import mongoose, { Schema, Document } from 'mongoose'; 3 | import { addOnBeforeDeleteMany } from '@utils/models/deletion'; 4 | import { Notification } from './notification.model'; 5 | 6 | /** Channel documents interface declaration */ 7 | export interface Channel extends Document { 8 | kind: 'Channel'; 9 | title?: string; 10 | application?: any; 11 | form?: any; 12 | } 13 | 14 | /** Mongoose channel schema declaration */ 15 | const channelSchema = new Schema({ 16 | title: { 17 | type: String, 18 | required: true, 19 | }, 20 | application: { 21 | type: mongoose.Types.ObjectId, 22 | ref: 'Application', 23 | }, 24 | form: { 25 | type: mongoose.Types.ObjectId, 26 | ref: 'Form', 27 | }, 28 | }); 29 | 30 | // handle cascading deletion for channels 31 | addOnBeforeDeleteMany(channelSchema, async (channels) => { 32 | // Delete linked notifications 33 | await Notification.deleteMany({ channel: { $in: channels } }); 34 | }); 35 | 36 | channelSchema.index({ title: 1, application: 1, form: 1 }, { unique: true }); 37 | channelSchema.plugin(accessibleRecordsPlugin); 38 | 39 | /** Mongoose channel model definition */ 40 | // eslint-disable-next-line @typescript-eslint/no-redeclare 41 | export const Channel = mongoose.model>( 42 | 'Channel', 43 | channelSchema 44 | ); 45 | -------------------------------------------------------------------------------- /src/models/client.model.ts: -------------------------------------------------------------------------------- 1 | import { AccessibleRecordModel, accessibleRecordsPlugin } from '@casl/mongoose'; 2 | import mongoose, { Schema, Document } from 'mongoose'; 3 | import { AppAbility } from '@security/defineUserAbility'; 4 | import { PositionAttribute } from './positionAttribute.model'; 5 | 6 | /** Mongoose client schema declaration */ 7 | const clientSchema = new Schema({ 8 | name: String, 9 | azureRoles: [String], 10 | clientId: String, 11 | oid: String, 12 | roles: [ 13 | { 14 | type: mongoose.Schema.Types.ObjectId, 15 | ref: 'Role', 16 | }, 17 | ], 18 | groups: [ 19 | { 20 | type: mongoose.Schema.Types.ObjectId, 21 | ref: 'Group', 22 | }, 23 | ], 24 | positionAttributes: { 25 | type: [PositionAttribute.schema], 26 | }, 27 | }); 28 | 29 | /** Client documents interface declaration */ 30 | export interface Client extends Document { 31 | kind: 'Client'; 32 | name?: string; 33 | azureRoles?: string[]; 34 | clientId?: string; 35 | oid?: string; 36 | groups?: any[]; 37 | roles?: any[]; 38 | positionAttributes?: PositionAttribute[]; 39 | ability?: AppAbility; 40 | } 41 | 42 | clientSchema.index( 43 | { oid: 1 }, 44 | { unique: true, partialFilterExpression: { oid: { $type: 'string' } } } 45 | ); 46 | clientSchema.index({ clientId: 1 }, { unique: true }); 47 | clientSchema.plugin(accessibleRecordsPlugin); 48 | 49 | /** Mongoose client model definition */ 50 | // eslint-disable-next-line @typescript-eslint/no-redeclare 51 | export const Client = mongoose.model>( 52 | 'Client', 53 | clientSchema 54 | ); 55 | -------------------------------------------------------------------------------- /src/models/distributionList.model.ts: -------------------------------------------------------------------------------- 1 | import { Schema, Document } from 'mongoose'; 2 | 3 | /** Mongoose distribution list schema declaration */ 4 | export const distributionListSchema = new Schema( 5 | { 6 | name: String, 7 | emails: [String], 8 | }, 9 | { 10 | timestamps: { createdAt: 'createdAt', updatedAt: 'modifiedAt' }, 11 | } 12 | ); 13 | 14 | /** distribution list documents interface declaration */ 15 | export interface DistributionList extends Document { 16 | kind: 'DistributionList'; 17 | name?: string; 18 | emails?: string[]; 19 | createdAt?: Date; 20 | modifiedAt?: Date; 21 | } 22 | -------------------------------------------------------------------------------- /src/models/group.model.ts: -------------------------------------------------------------------------------- 1 | import { AccessibleRecordModel, accessibleRecordsPlugin } from '@casl/mongoose'; 2 | import mongoose, { Schema, Document } from 'mongoose'; 3 | 4 | /** Mongoose group schema definition */ 5 | const groupSchema = new Schema( 6 | { 7 | title: String, 8 | description: String, 9 | oid: String, 10 | // TODO: add roles array (out of scope for this ticket) 11 | }, 12 | { 13 | timestamps: { createdAt: 'createdAt', updatedAt: 'modifiedAt' }, 14 | } 15 | ); 16 | 17 | /** Group documents interface definition */ 18 | export interface Group extends Document { 19 | kind: 'Group'; 20 | title: string; 21 | oid: string; 22 | description: string; 23 | createdAt: Date; 24 | modifiedAt: Date; 25 | } 26 | 27 | groupSchema.plugin(accessibleRecordsPlugin); 28 | 29 | /** Mongoose group model definition */ 30 | // eslint-disable-next-line @typescript-eslint/no-redeclare 31 | export const Group = mongoose.model>( 32 | 'Group', 33 | groupSchema 34 | ); 35 | -------------------------------------------------------------------------------- /src/models/history.model.ts: -------------------------------------------------------------------------------- 1 | import { Version } from './version.model'; 2 | 3 | export type Change = { 4 | type: 'add' | 'remove' | 'modify'; 5 | displayType: string; 6 | field: string; 7 | displayName: string; 8 | old?: any; 9 | new?: any; 10 | }; 11 | 12 | export type RecordHistory = { 13 | createdAt: Date; 14 | createdBy: string; 15 | changes: Change[]; 16 | version?: Version; 17 | }[]; 18 | 19 | export type RecordHistoryMeta = { 20 | form: string; 21 | record: string; 22 | fields: string; 23 | fromDate: string; 24 | toDate: string; 25 | exportDate: string; 26 | }; 27 | -------------------------------------------------------------------------------- /src/models/index.ts: -------------------------------------------------------------------------------- 1 | export * from './actionButton.model'; 2 | export * from './apiConfiguration.model'; 3 | export * from './application.model'; 4 | export * from './channel.model'; 5 | export * from './client.model'; 6 | export * from './dashboard.model'; 7 | export * from './form.model'; 8 | export * from './layout.model'; 9 | export * from './notification.model'; 10 | export * from './page.model'; 11 | export * from './permission.model'; 12 | export * from './positionAttribute.model'; 13 | export * from './positionAttributeCategory.model'; 14 | export * from './pullJob.model'; 15 | export * from './record.model'; 16 | export * from './referenceData.model'; 17 | export * from './resource.model'; 18 | export * from './role.model'; 19 | export * from './step.model'; 20 | export * from './user.model'; 21 | export * from './version.model'; 22 | export * from './workflow.model'; 23 | export * from './history.model'; 24 | export * from './group.model'; 25 | export * from './aggregation.model'; 26 | export * from './template.model'; 27 | export * from './distributionList.model'; 28 | export * from './customNotification.model'; 29 | export * from './layer.model'; 30 | export * from './draftRecord.model'; 31 | export * from './emailNotification.model'; 32 | export * from './activityLog.model'; 33 | export * from './emailDistributionList.model'; 34 | -------------------------------------------------------------------------------- /src/models/layout.model.ts: -------------------------------------------------------------------------------- 1 | import mongoose, { Schema, Document } from 'mongoose'; 2 | 3 | /** Mongoose layout schema declaration */ 4 | export const layoutSchema = new Schema( 5 | { 6 | name: String, 7 | query: { 8 | type: mongoose.Schema.Types.Mixed, 9 | }, 10 | display: { 11 | type: mongoose.Schema.Types.Mixed, 12 | }, 13 | }, 14 | { 15 | timestamps: { createdAt: 'createdAt', updatedAt: 'modifiedAt' }, 16 | } 17 | ); 18 | 19 | /** Layout documents interface declaration */ 20 | export interface Layout extends Document { 21 | kind: 'Layout'; 22 | name?: string; 23 | createdAt?: Date; 24 | modifiedAt?: Date; 25 | query?: any; 26 | display?: any; 27 | } 28 | -------------------------------------------------------------------------------- /src/models/migration.model.ts: -------------------------------------------------------------------------------- 1 | import mongoose from 'mongoose'; 2 | 3 | /** Model for the migration */ 4 | const migrationSchema = new mongoose.Schema({ 5 | title: { type: String }, 6 | timestamp: { type: Number }, 7 | description: { type: String }, 8 | }); 9 | 10 | /** Mongo model for the migration table */ 11 | export const Migration = mongoose.model('Migration', migrationSchema); 12 | -------------------------------------------------------------------------------- /src/models/notification.model.ts: -------------------------------------------------------------------------------- 1 | import { AccessibleRecordModel, accessibleRecordsPlugin } from '@casl/mongoose'; 2 | import mongoose, { Schema, Document } from 'mongoose'; 3 | 4 | /** Mongoose notification schema declaration */ 5 | const notificationSchema = new Schema( 6 | { 7 | action: String, 8 | content: mongoose.Schema.Types.Mixed, 9 | channel: { 10 | type: mongoose.Schema.Types.ObjectId, 11 | ref: 'Channel', 12 | required: true, 13 | }, 14 | seenBy: { 15 | type: [mongoose.Schema.Types.ObjectId], 16 | ref: 'User', 17 | }, 18 | }, 19 | { 20 | timestamps: { createdAt: 'createdAt', updatedAt: 'modifiedAt' }, 21 | } 22 | ); 23 | 24 | // notificationSchema.index( 25 | // { createdAt: 1 }, 26 | // { expireAfterSeconds: 3600 * 24 * 30 } 27 | // ); // After 60 days, the notification is erased 28 | 29 | /** Notification documents interface declaration */ 30 | export interface Notification extends Document { 31 | kind: 'Notification'; 32 | action: string; 33 | content: any; 34 | createdAt: Date; 35 | channel: any; 36 | seenBy: any[]; 37 | } 38 | 39 | notificationSchema.plugin(accessibleRecordsPlugin); 40 | 41 | /** Mongoose notification model definition */ 42 | // eslint-disable-next-line @typescript-eslint/no-redeclare 43 | export const Notification = mongoose.model< 44 | Notification, 45 | AccessibleRecordModel 46 | >('Notification', notificationSchema); 47 | -------------------------------------------------------------------------------- /src/models/permission.model.ts: -------------------------------------------------------------------------------- 1 | import { AccessibleRecordModel, accessibleRecordsPlugin } from '@casl/mongoose'; 2 | import mongoose, { Schema, Document } from 'mongoose'; 3 | 4 | /** Mongoose permission schema declaration */ 5 | const permissionSchema = new Schema({ 6 | type: { 7 | type: String, 8 | required: true, 9 | }, 10 | global: Boolean, 11 | }); 12 | 13 | permissionSchema.index({ type: 1, global: 1 }, { unique: true }); 14 | 15 | /** Permission documents interface declaration */ 16 | export interface Permission extends Document { 17 | kind: 'Permission'; 18 | type?: string; 19 | global?: boolean; 20 | } 21 | 22 | permissionSchema.plugin(accessibleRecordsPlugin); 23 | 24 | /** Mongoose permission model definition */ 25 | // eslint-disable-next-line @typescript-eslint/no-redeclare 26 | export const Permission = mongoose.model< 27 | Permission, 28 | AccessibleRecordModel 29 | >('Permission', permissionSchema); 30 | -------------------------------------------------------------------------------- /src/models/positionAttribute.model.ts: -------------------------------------------------------------------------------- 1 | import { AccessibleRecordModel, accessibleRecordsPlugin } from '@casl/mongoose'; 2 | import mongoose, { Schema, Document } from 'mongoose'; 3 | 4 | /** Mongoose position attribute schema declaration */ 5 | const positionAttributeSchema = new Schema({ 6 | value: String, 7 | category: { 8 | type: mongoose.Schema.Types.ObjectId, 9 | ref: 'PositionAttributeCategory', 10 | }, 11 | }); 12 | 13 | /** Position attribute documents interface declaration */ 14 | export interface PositionAttribute extends Document { 15 | kind: 'PositionAttribute'; 16 | value?: string; 17 | category?: any; 18 | usersCount?: number; 19 | } 20 | 21 | positionAttributeSchema.plugin(accessibleRecordsPlugin); 22 | 23 | /** Mongoose position attribute model definition */ 24 | // eslint-disable-next-line @typescript-eslint/no-redeclare 25 | export const PositionAttribute = mongoose.model< 26 | PositionAttribute, 27 | AccessibleRecordModel 28 | >('PositionAttribute', positionAttributeSchema); 29 | -------------------------------------------------------------------------------- /src/models/positionAttributeCategory.model.ts: -------------------------------------------------------------------------------- 1 | import { AccessibleRecordModel, accessibleRecordsPlugin } from '@casl/mongoose'; 2 | import mongoose, { Schema, Document } from 'mongoose'; 3 | 4 | /** Mongoose position attribute category schema declaration */ 5 | const positionAttributeCategorySchema = new Schema({ 6 | title: { 7 | type: String, 8 | required: true, 9 | }, 10 | application: { 11 | type: mongoose.Types.ObjectId, 12 | required: true, 13 | ref: 'Application', 14 | }, 15 | }); 16 | 17 | /** Position attribute category documents interface declaration */ 18 | export interface PositionAttributeCategory extends Document { 19 | kind: 'PositionAttributeCategory'; 20 | title?: string; 21 | application?: any; 22 | } 23 | 24 | positionAttributeCategorySchema.index( 25 | { title: 1, application: 1 }, 26 | { unique: true } 27 | ); 28 | positionAttributeCategorySchema.plugin(accessibleRecordsPlugin); 29 | 30 | /** Mongoose position attribute category model definition */ 31 | // eslint-disable-next-line @typescript-eslint/no-redeclare 32 | export const PositionAttributeCategory = mongoose.model< 33 | PositionAttributeCategory, 34 | AccessibleRecordModel 35 | >('PositionAttributeCategory', positionAttributeCategorySchema); 36 | -------------------------------------------------------------------------------- /src/models/role.model.ts: -------------------------------------------------------------------------------- 1 | import { AccessibleRecordModel, accessibleRecordsPlugin } from '@casl/mongoose'; 2 | import mongoose, { Schema, Document } from 'mongoose'; 3 | 4 | /** Mongoose role schema definition */ 5 | const roleSchema = new Schema({ 6 | title: String, 7 | description: String, 8 | application: { 9 | type: mongoose.Schema.Types.ObjectId, 10 | ref: 'Application', 11 | }, 12 | permissions: { 13 | type: [mongoose.Schema.Types.ObjectId], 14 | ref: 'Permission', 15 | }, 16 | channels: { 17 | type: [mongoose.Schema.Types.ObjectId], 18 | ref: 'Channel', 19 | }, 20 | autoAssignment: [mongoose.Schema.Types.Mixed], 21 | }); 22 | 23 | roleSchema.index({ title: 1, application: 1 }, { unique: true }); 24 | 25 | /** Role documents interface definition */ 26 | export interface Role extends Document { 27 | kind: 'Role'; 28 | title: string; 29 | description: string; 30 | application: any; 31 | permissions: any[]; 32 | channels: any[]; 33 | autoAssignment: any[]; 34 | } 35 | 36 | roleSchema.plugin(accessibleRecordsPlugin); 37 | 38 | /** Mongoose role model*/ 39 | // eslint-disable-next-line @typescript-eslint/no-redeclare 40 | export const Role = mongoose.model>( 41 | 'Role', 42 | roleSchema 43 | ); 44 | -------------------------------------------------------------------------------- /src/models/template.model.ts: -------------------------------------------------------------------------------- 1 | import mongoose, { Schema, Document } from 'mongoose'; 2 | 3 | /** Mongoose template schema declaration */ 4 | export const templateSchema = new Schema( 5 | { 6 | name: String, 7 | type: String, 8 | content: { 9 | type: mongoose.Schema.Types.Mixed, 10 | }, 11 | }, 12 | { 13 | timestamps: { createdAt: 'createdAt', updatedAt: 'modifiedAt' }, 14 | } 15 | ); 16 | 17 | /** template documents interface declaration */ 18 | export interface Template extends Document { 19 | kind: 'Template'; 20 | type?: 'email'; // In the case we add other types of templates in the future 21 | name?: string; 22 | content?: any; 23 | createdAt?: Date; 24 | modifiedAt?: Date; 25 | } 26 | -------------------------------------------------------------------------------- /src/models/version.model.ts: -------------------------------------------------------------------------------- 1 | import { AccessibleRecordModel, accessibleRecordsPlugin } from '@casl/mongoose'; 2 | import mongoose, { Schema, Document } from 'mongoose'; 3 | 4 | /** Mongoose version schema declaration */ 5 | const versionSchema = new Schema( 6 | { 7 | data: mongoose.Schema.Types.Mixed, 8 | createdBy: { type: mongoose.Schema.Types.ObjectId, ref: 'User' }, 9 | }, 10 | { 11 | timestamps: { createdAt: 'createdAt' }, 12 | } 13 | ); 14 | 15 | /** Version documents interface declaration */ 16 | export interface Version extends Document { 17 | kind: 'Version'; 18 | createdAt?: Date; 19 | data?: any; 20 | createdBy?: any; 21 | } 22 | 23 | versionSchema.plugin(accessibleRecordsPlugin); 24 | 25 | /** Mongoose version model declaration */ 26 | // eslint-disable-next-line @typescript-eslint/no-redeclare 27 | export const Version = mongoose.model>( 28 | 'Version', 29 | versionSchema 30 | ); 31 | -------------------------------------------------------------------------------- /src/oort.config.ts: -------------------------------------------------------------------------------- 1 | /** Config file to customize the Oort instance */ 2 | // eslint-disable-next-line @typescript-eslint/naming-convention 3 | export enum AuthenticationType { 4 | azureAD = 'azure', 5 | keycloak = 'keycloak', 6 | } 7 | -------------------------------------------------------------------------------- /src/routes/permissions/index.ts: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import config from 'config'; 3 | import { logger } from '@services/logger.service'; 4 | 5 | /** 6 | * Routes for permissions 7 | */ 8 | const router = express.Router(); 9 | 10 | /** Return configuration of permissions */ 11 | router.get('/configuration', async (req: any, res) => { 12 | try { 13 | const data = { 14 | groups: { 15 | local: config.get('user.groups.local'), 16 | }, 17 | attributes: { 18 | local: config.get('user.attributes.local'), 19 | }, 20 | }; 21 | return res.status(200).send(data); 22 | } catch (err) { 23 | logger.error(err.message, { stack: err.stack }); 24 | return res.status(500).send(req.t('common.errors.internalServerError')); 25 | } 26 | }); 27 | 28 | /** Return available attributes */ 29 | router.get('/attributes', async (req: any, res) => { 30 | try { 31 | const data = config.get('user.attributes.list') || []; 32 | return res.status(200).send(data); 33 | } catch (err) { 34 | logger.error(err.message, { stack: err.stack }); 35 | return res.status(500).send(req.t('common.errors.internalServerError')); 36 | } 37 | }); 38 | 39 | export default router; 40 | -------------------------------------------------------------------------------- /src/schema/index.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLSchema } from 'graphql'; 2 | import Mutation from './mutation'; 3 | import Query from './query'; 4 | import Subscription from './subscription'; 5 | 6 | export default new GraphQLSchema({ 7 | query: Query, 8 | mutation: Mutation, 9 | subscription: Subscription, 10 | }); 11 | -------------------------------------------------------------------------------- /src/schema/inputs/aggregation.input.ts: -------------------------------------------------------------------------------- 1 | import { 2 | GraphQLInputObjectType, 3 | GraphQLNonNull, 4 | GraphQLString, 5 | GraphQLList, 6 | } from 'graphql'; 7 | import GraphQLJSON from 'graphql-type-json'; 8 | 9 | /** Aggregation type for queries/mutations argument */ 10 | export type AggregationArgs = { 11 | name: string; 12 | sourceFields: string[]; 13 | pipeline: any; 14 | mapping?: any; 15 | }; 16 | 17 | /** GraphQL Input Type of Aggregation */ 18 | export const AggregationInputType = new GraphQLInputObjectType({ 19 | name: 'AggregationInputType', 20 | fields: () => ({ 21 | name: { type: new GraphQLNonNull(GraphQLString) }, 22 | // dataSource: { type: new GraphQLNonNull(GraphQLID) }, 23 | sourceFields: { type: new GraphQLList(GraphQLString) }, 24 | pipeline: { type: new GraphQLList(GraphQLJSON) }, 25 | // mapping: { type: GraphQLJSON }, 26 | }), 27 | }); 28 | -------------------------------------------------------------------------------- /src/schema/inputs/customNotification.input.ts: -------------------------------------------------------------------------------- 1 | import { 2 | GraphQLInputObjectType, 3 | GraphQLNonNull, 4 | GraphQLString, 5 | GraphQLID, 6 | } from 'graphql'; 7 | import { Types } from 'mongoose'; 8 | 9 | /** Custom Notification type for queries/mutations argument */ 10 | export type CustomNotificationArgs = { 11 | name: string; 12 | description?: string; 13 | schedule: string; 14 | notificationType: string; 15 | resource: string | Types.ObjectId; 16 | layout: string | Types.ObjectId; 17 | template: string | Types.ObjectId; 18 | recipients: string; 19 | recipientsType: string; 20 | // eslint-disable-next-line @typescript-eslint/naming-convention 21 | notification_status?: string; 22 | }; 23 | 24 | /** GraphQL custom notification query input type definition */ 25 | // eslint-disable-next-line @typescript-eslint/naming-convention 26 | export const CustomNotificationInputType = new GraphQLInputObjectType({ 27 | name: 'CustomNotificationInputType', 28 | fields: () => ({ 29 | name: { type: new GraphQLNonNull(GraphQLString) }, 30 | description: { type: GraphQLString }, 31 | schedule: { type: new GraphQLNonNull(GraphQLString) }, 32 | notificationType: { type: new GraphQLNonNull(GraphQLString) }, 33 | resource: { type: new GraphQLNonNull(GraphQLID) }, 34 | layout: { type: new GraphQLNonNull(GraphQLID) }, 35 | template: { type: new GraphQLNonNull(GraphQLID) }, 36 | recipients: { type: new GraphQLNonNull(GraphQLString) }, 37 | recipientsType: { type: new GraphQLNonNull(GraphQLString) }, 38 | // notification_status: { type: new GraphQLNonNull(GraphQLString) }, 39 | }), 40 | }); 41 | -------------------------------------------------------------------------------- /src/schema/inputs/dashboard-filter.input.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLInputObjectType, GraphQLString, GraphQLBoolean } from 'graphql'; 2 | import GraphQLJSON from 'graphql-type-json'; 3 | 4 | /** PositionAttribute type for queries/mutations argument */ 5 | export type FilterArgs = { 6 | variant?: string; 7 | show?: boolean; 8 | closable?: boolean; 9 | structure?: any; 10 | position?: string; 11 | }; 12 | 13 | /** GraphQL position attribute input type definition */ 14 | export const DashboardFilterInputType = new GraphQLInputObjectType({ 15 | name: 'DashboardFilterInputType', 16 | fields: () => ({ 17 | variant: { type: GraphQLString }, 18 | show: { type: GraphQLBoolean }, 19 | closable: { type: GraphQLBoolean }, 20 | structure: { type: GraphQLJSON }, 21 | position: { type: GraphQLString }, 22 | }), 23 | }); 24 | -------------------------------------------------------------------------------- /src/schema/inputs/distributionList.input.ts: -------------------------------------------------------------------------------- 1 | import { 2 | GraphQLInputObjectType, 3 | GraphQLList, 4 | GraphQLNonNull, 5 | GraphQLString, 6 | } from 'graphql'; 7 | 8 | /** DistributionList type for queries/mutations argument */ 9 | export type DistributionListArgs = { 10 | name: string; 11 | emails: string[]; 12 | }; 13 | 14 | /** GraphQL distribution list query input type definition */ 15 | // eslint-disable-next-line @typescript-eslint/naming-convention 16 | export const DistributionListInputType = new GraphQLInputObjectType({ 17 | name: 'DistributionListInputType', 18 | fields: () => ({ 19 | name: { type: new GraphQLNonNull(GraphQLString) }, 20 | emails: { type: new GraphQLNonNull(new GraphQLList(GraphQLString)) }, 21 | }), 22 | }); 23 | -------------------------------------------------------------------------------- /src/schema/inputs/index.ts: -------------------------------------------------------------------------------- 1 | export * from './position-attribute.input'; 2 | export * from './user-profile.input'; 3 | export * from './user.input'; 4 | export * from './layout.input'; 5 | export * from './layer.input'; 6 | export * from './pageContext.input'; 7 | export * from './aggregation.input'; 8 | export * from './customNotification.input'; 9 | export * from './distributionList.input'; 10 | export * from './template.input'; 11 | -------------------------------------------------------------------------------- /src/schema/inputs/pageContext.input.ts: -------------------------------------------------------------------------------- 1 | import { 2 | GraphQLInputObjectType, 3 | GraphQLNonNull, 4 | GraphQLString, 5 | GraphQLID, 6 | } from 'graphql'; 7 | import { Types } from 'mongoose'; 8 | 9 | /** Aggregation type for queries/mutations argument */ 10 | export type PageContextArgs = { 11 | refData?: string | Types.ObjectId; 12 | resource: Types.ObjectId; 13 | displayField: string; 14 | }; 15 | 16 | /** GraphQL Input Type for the page context */ 17 | export const PageContextInputType = new GraphQLInputObjectType({ 18 | name: 'PageContextInputType', 19 | fields: () => ({ 20 | refData: { type: GraphQLID }, 21 | resource: { type: GraphQLID }, 22 | displayField: { type: new GraphQLNonNull(GraphQLString) }, 23 | }), 24 | }); 25 | -------------------------------------------------------------------------------- /src/schema/inputs/position-attribute.input.ts: -------------------------------------------------------------------------------- 1 | import { 2 | GraphQLInputObjectType, 3 | GraphQLNonNull, 4 | GraphQLString, 5 | GraphQLID, 6 | } from 'graphql'; 7 | 8 | /** PositionAttribute type for queries/mutations argument */ 9 | export type PositionAttributeArgs = { 10 | value: string; 11 | category: string; 12 | }; 13 | 14 | /** GraphQL position attribute input type definition */ 15 | export const PositionAttributeInputType = new GraphQLInputObjectType({ 16 | name: 'PositionAttributeInputType', 17 | fields: () => ({ 18 | value: { type: new GraphQLNonNull(GraphQLString) }, 19 | category: { type: new GraphQLNonNull(GraphQLID) }, 20 | }), 21 | }); 22 | -------------------------------------------------------------------------------- /src/schema/inputs/template.input.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLInputObjectType, GraphQLNonNull, GraphQLString } from 'graphql'; 2 | import GraphQLJSON from 'graphql-type-json'; 3 | 4 | /** Template type for queries/mutations argument */ 5 | export type TemplateArgs = { 6 | name: string; 7 | type: string; 8 | content: any; 9 | }; 10 | 11 | /** GraphQL template query input type definition */ 12 | export const TemplateInputType = new GraphQLInputObjectType({ 13 | name: 'TemplateInputType', 14 | fields: () => ({ 15 | name: { type: new GraphQLNonNull(GraphQLString) }, 16 | type: { type: new GraphQLNonNull(GraphQLString) }, 17 | content: { type: new GraphQLNonNull(GraphQLJSON) }, 18 | }), 19 | }); 20 | -------------------------------------------------------------------------------- /src/schema/inputs/user-profile.input.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLID, GraphQLInputObjectType, GraphQLString } from 'graphql'; 2 | import GraphQLJSON from 'graphql-type-json'; 3 | 4 | /** UserProfile type for queries/mutations argument */ 5 | export type UserProfileArgs = { 6 | favoriteApp?: string; 7 | name?: string; 8 | firstName?: string; 9 | lastName?: string; 10 | attributes?: any; 11 | }; 12 | 13 | /** GraphQL user profile input type definition */ 14 | export const UserProfileInputType = new GraphQLInputObjectType({ 15 | name: 'UserProfileInputType', 16 | fields: () => ({ 17 | favoriteApp: { type: GraphQLID }, 18 | name: { type: GraphQLString }, 19 | firstName: { type: GraphQLString }, 20 | lastName: { type: GraphQLString }, 21 | attributes: { type: GraphQLJSON }, 22 | }), 23 | }); 24 | -------------------------------------------------------------------------------- /src/schema/inputs/user.input.ts: -------------------------------------------------------------------------------- 1 | import { 2 | GraphQLID, 3 | GraphQLInputObjectType, 4 | GraphQLList, 5 | GraphQLNonNull, 6 | GraphQLString, 7 | } from 'graphql'; 8 | import { PositionAttributeInputType } from './position-attribute.input'; 9 | import { Types } from 'mongoose'; 10 | import { PositionAttribute } from '@models/positionAttribute.model'; 11 | 12 | /** User type for queries/mutations argument */ 13 | export type UserArgs = { 14 | email: string; 15 | role: string | Types.ObjectId; 16 | positionAttributes?: PositionAttribute[]; 17 | }; 18 | 19 | /** 20 | * GraphQL Input Type of User. 21 | */ 22 | export const UserInputType = new GraphQLInputObjectType({ 23 | name: 'UserInputType', 24 | fields: () => ({ 25 | email: { type: new GraphQLNonNull(GraphQLString) }, 26 | role: { type: new GraphQLNonNull(GraphQLID) }, 27 | positionAttributes: { type: new GraphQLList(PositionAttributeInputType) }, 28 | }), 29 | }); 30 | -------------------------------------------------------------------------------- /src/schema/mutation/addLayer.mutation.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLError, GraphQLNonNull } from 'graphql'; 2 | import { Layer } from '@models'; 3 | import { LayerType } from '../../schema/types'; 4 | import { AppAbility } from '@security/defineUserAbility'; 5 | import LayerInputType from '@schema/inputs/layer.input'; 6 | import { graphQLAuthCheck } from '@schema/shared'; 7 | import { logger } from '@services/logger.service'; 8 | 9 | /** 10 | * Add new layer. 11 | * Throw an error if user not connected and not permission to create. 12 | */ 13 | export default { 14 | type: LayerType, 15 | args: { 16 | layer: { type: new GraphQLNonNull(LayerInputType) }, 17 | }, 18 | async resolve(parent, args, context) { 19 | graphQLAuthCheck(context); 20 | try { 21 | const user = context.user; 22 | const ability: AppAbility = user.ability; 23 | 24 | const layer = new Layer(args.layer); 25 | if (ability.can('create', layer)) { 26 | return await layer.save(); 27 | } else { 28 | throw new GraphQLError( 29 | context.i18next.t('common.errors.permissionNotGranted') 30 | ); 31 | } 32 | } catch (err) { 33 | logger.error(err.message, { stack: err.stack }); 34 | if (err instanceof GraphQLError) { 35 | throw new GraphQLError(err.message); 36 | } 37 | throw new GraphQLError( 38 | context.i18next.t('common.errors.internalServerError') 39 | ); 40 | } 41 | }, 42 | }; 43 | -------------------------------------------------------------------------------- /src/schema/mutation/deleteDraftRecord.mutation.ts: -------------------------------------------------------------------------------- 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 | }; 40 | -------------------------------------------------------------------------------- /src/schema/mutation/deleteLayer.mutation.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLError, GraphQLNonNull, GraphQLID } from 'graphql'; 2 | import { Layer } from '@models'; 3 | import { LayerType } from '../../schema/types'; 4 | import { AppAbility } from '@security/defineUserAbility'; 5 | import { graphQLAuthCheck } from '@schema/shared'; 6 | import { logger } from '@services/logger.service'; 7 | 8 | /** 9 | * Edit new layer. 10 | * Throw an error if user not connected and not permission to create. 11 | */ 12 | export default { 13 | type: LayerType, 14 | args: { 15 | id: { type: new GraphQLNonNull(GraphQLID) }, 16 | }, 17 | async resolve(parent, args, context) { 18 | graphQLAuthCheck(context); 19 | try { 20 | const user = context.user; 21 | const layer = await Layer.findById(args.id); 22 | const ability: AppAbility = user.ability; 23 | 24 | if (ability.can('delete', layer)) { 25 | // delete layer 26 | await layer.deleteOne(); 27 | return layer; 28 | } 29 | throw new GraphQLError( 30 | context.i18next.t('common.errors.permissionNotGranted') 31 | ); 32 | } catch (err) { 33 | logger.error(err.message, { stack: err.stack }); 34 | if (err instanceof GraphQLError) { 35 | throw new GraphQLError(err.message); 36 | } 37 | throw new GraphQLError( 38 | context.i18next.t('common.errors.internalServerError') 39 | ); 40 | } 41 | }, 42 | }; 43 | -------------------------------------------------------------------------------- /src/schema/mutation/deleteRole.mutation.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLNonNull, GraphQLID, GraphQLError } from 'graphql'; 2 | import { Role } from '@models'; 3 | import { AppAbility } from '@security/defineUserAbility'; 4 | import { RoleType } from '../types'; 5 | import { logger } from '@services/logger.service'; 6 | import { accessibleBy } from '@casl/mongoose'; 7 | import { graphQLAuthCheck } from '@schema/shared'; 8 | import { Types } from 'mongoose'; 9 | import { Context } from '@server/apollo/context'; 10 | 11 | /** Arguments for the deleteRole mutation */ 12 | type DeleteRoleArgs = { 13 | id: string | Types.ObjectId; 14 | }; 15 | 16 | /** 17 | * Deletes a role. 18 | * Throws an error if not logged or authorized. 19 | */ 20 | export default { 21 | type: RoleType, 22 | args: { 23 | id: { type: new GraphQLNonNull(GraphQLID) }, 24 | }, 25 | async resolve(parent, args: DeleteRoleArgs, context: Context) { 26 | graphQLAuthCheck(context); 27 | try { 28 | const ability: AppAbility = context.user.ability; 29 | const filters = Role.find(accessibleBy(ability, 'delete').Role) 30 | .where({ _id: args.id }) 31 | .getFilter(); 32 | const role = await Role.findOneAndDelete(filters); 33 | if (role) { 34 | return role; 35 | } else { 36 | throw new GraphQLError( 37 | context.i18next.t('common.errors.permissionNotGranted') 38 | ); 39 | } 40 | } catch (err) { 41 | logger.error(err.message, { stack: err.stack }); 42 | if (err instanceof GraphQLError) { 43 | throw new GraphQLError(err.message); 44 | } 45 | throw new GraphQLError( 46 | context.i18next.t('common.errors.internalServerError') 47 | ); 48 | } 49 | }, 50 | }; 51 | -------------------------------------------------------------------------------- /src/schema/mutation/deleteUsers.mutation.ts: -------------------------------------------------------------------------------- 1 | import { 2 | GraphQLNonNull, 3 | GraphQLID, 4 | GraphQLError, 5 | GraphQLList, 6 | GraphQLInt, 7 | } from 'graphql'; 8 | import { User } from '@models'; 9 | import { AppAbility } from '@security/defineUserAbility'; 10 | import { logger } from '@services/logger.service'; 11 | import { graphQLAuthCheck } from '@schema/shared'; 12 | import { Types } from 'mongoose'; 13 | import { Context } from '@server/apollo/context'; 14 | 15 | /** Arguments for the deleteUsers mutation */ 16 | type DeleteUsersArgs = { 17 | ids: string[] | Types.ObjectId[]; 18 | }; 19 | 20 | /** 21 | * Delete a user. 22 | * Throw an error if not logged or authorized. 23 | */ 24 | export default { 25 | type: GraphQLInt, 26 | args: { 27 | ids: { type: new GraphQLNonNull(new GraphQLList(GraphQLID)) }, 28 | }, 29 | async resolve(parent, args: DeleteUsersArgs, context: Context) { 30 | graphQLAuthCheck(context); 31 | try { 32 | const user = context.user; 33 | const ability: AppAbility = user.ability; 34 | if (ability.can('delete', 'User')) { 35 | const result = await User.deleteMany({ _id: { $in: args.ids } }); 36 | return result.deletedCount; 37 | } else { 38 | throw new GraphQLError( 39 | context.i18next.t('common.errors.permissionNotGranted') 40 | ); 41 | } 42 | } catch (err) { 43 | logger.error(err.message, { stack: err.stack }); 44 | if (err instanceof GraphQLError) { 45 | throw new GraphQLError(err.message); 46 | } 47 | throw new GraphQLError( 48 | context.i18next.t('common.errors.internalServerError') 49 | ); 50 | } 51 | }, 52 | }; 53 | -------------------------------------------------------------------------------- /src/schema/mutation/editLayer.mutation.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLError, GraphQLNonNull, GraphQLID } from 'graphql'; 2 | import { Layer } from '@models'; 3 | import { LayerType } from '../../schema/types'; 4 | import { AppAbility } from '@security/defineUserAbility'; 5 | import LayerInputType from '@schema/inputs/layer.input'; 6 | import { graphQLAuthCheck } from '@schema/shared'; 7 | import { logger } from '@services/logger.service'; 8 | 9 | /** 10 | * Edit new layer. 11 | * Throw an error if user not connected and not permission to create. 12 | */ 13 | export default { 14 | type: LayerType, 15 | args: { 16 | id: { type: new GraphQLNonNull(GraphQLID) }, 17 | layer: { type: new GraphQLNonNull(LayerInputType) }, 18 | }, 19 | async resolve(parent, args, context) { 20 | graphQLAuthCheck(context); 21 | try { 22 | const user = context.user; 23 | 24 | if (args.type !== 'GroupLayer' && args.sublayers) { 25 | // todo(translate) 26 | throw new GraphQLError('Only group layers can have sublayers'); 27 | } 28 | 29 | const ability: AppAbility = user.ability; 30 | const layer = await Layer.findById(args.id); 31 | 32 | if (ability.can('update', layer)) { 33 | return await Layer.findByIdAndUpdate(args.id, args.layer); 34 | } 35 | throw new GraphQLError( 36 | context.i18next.t('common.errors.permissionNotGranted') 37 | ); 38 | } catch (err) { 39 | logger.error(err.message, { stack: err.stack }); 40 | if (err instanceof GraphQLError) { 41 | throw new GraphQLError(err.message); 42 | } 43 | throw new GraphQLError( 44 | context.i18next.t('common.errors.internalServerError') 45 | ); 46 | } 47 | }, 48 | }; 49 | -------------------------------------------------------------------------------- /src/schema/mutation/restorePage.mutation.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLNonNull, GraphQLID, GraphQLError } from 'graphql'; 2 | import { PageType } from '../types'; 3 | import { Page } from '@models'; 4 | import extendAbilityForPage from '@security/extendAbilityForPage'; 5 | import { logger } from '@services/logger.service'; 6 | import { graphQLAuthCheck } from '@schema/shared'; 7 | 8 | /** 9 | * Restore archived page. 10 | * Page model should automatically restore associated content. 11 | */ 12 | export default { 13 | type: PageType, 14 | args: { 15 | id: { type: new GraphQLNonNull(GraphQLID) }, 16 | }, 17 | async resolve(parent, args, context) { 18 | graphQLAuthCheck(context); 19 | try { 20 | const user = context.user; 21 | // Find page 22 | const page = await Page.findById(args.id); 23 | 24 | // Check access 25 | const ability = await extendAbilityForPage(user, page); 26 | if (ability.cannot('update', page)) { 27 | throw new GraphQLError( 28 | context.i18next.t('common.errors.permissionNotGranted') 29 | ); 30 | } 31 | 32 | // restore page 33 | if (page && page.archived) { 34 | return await Page.findByIdAndUpdate( 35 | args.id, 36 | { 37 | archived: false, 38 | archivedAt: null, 39 | }, 40 | { new: true } 41 | ); 42 | } 43 | } catch (err) { 44 | logger.error(err.message, { stack: err.stack }); 45 | if (err instanceof GraphQLError) { 46 | throw new GraphQLError(err.message); 47 | } 48 | throw new GraphQLError( 49 | context.i18next.t('common.errors.internalServerError') 50 | ); 51 | } 52 | }, 53 | }; 54 | -------------------------------------------------------------------------------- /src/schema/query/apiConfiguration.query.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLNonNull, GraphQLID, GraphQLError } from 'graphql'; 2 | import { ApiConfigurationType } from '../types'; 3 | import { ApiConfiguration } from '@models'; 4 | import { logger } from '@services/logger.service'; 5 | import { graphQLAuthCheck } from '@schema/shared'; 6 | import { Types } from 'mongoose'; 7 | import { Context } from '@server/apollo/context'; 8 | 9 | /** Arguments for the apiConfiguration query */ 10 | type ApiConfigurationArgs = { 11 | id: string | Types.ObjectId; 12 | }; 13 | 14 | /** 15 | * Return api configuration from id if available for the logged user. 16 | * Throw GraphQL error if not logged. 17 | */ 18 | export default { 19 | type: ApiConfigurationType, 20 | args: { 21 | id: { type: new GraphQLNonNull(GraphQLID) }, 22 | }, 23 | async resolve(parent, args: ApiConfigurationArgs, context: Context) { 24 | graphQLAuthCheck(context); 25 | try { 26 | const ability = context.user.ability; 27 | if (ability.can('read', 'ApiConfiguration')) { 28 | return await ApiConfiguration.findById(args.id); 29 | } else { 30 | throw new GraphQLError( 31 | context.i18next.t('common.errors.permissionNotGranted') 32 | ); 33 | } 34 | } catch (err) { 35 | logger.error(err.message, { stack: err.stack }); 36 | if (err instanceof GraphQLError) { 37 | throw new GraphQLError(err.message); 38 | } 39 | throw new GraphQLError( 40 | context.i18next.t('common.errors.internalServerError') 41 | ); 42 | } 43 | }, 44 | }; 45 | -------------------------------------------------------------------------------- /src/schema/query/channels.query.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLError, GraphQLID, GraphQLList } from 'graphql'; 2 | import { Channel } from '@models'; 3 | import { ChannelType } from '../types'; 4 | import { logger } from '@services/logger.service'; 5 | import { accessibleBy } from '@casl/mongoose'; 6 | import { graphQLAuthCheck } from '@schema/shared'; 7 | import { Types } from 'mongoose'; 8 | import { Context } from '@server/apollo/context'; 9 | 10 | /** Arguments for the channels query */ 11 | type ChannelsArgs = { 12 | application?: string | Types.ObjectId; 13 | }; 14 | 15 | /** 16 | * List all channels available. 17 | * TODO : not working 18 | */ 19 | export default { 20 | type: new GraphQLList(ChannelType), 21 | args: { 22 | application: { type: GraphQLID }, 23 | }, 24 | async resolve(parent, args: ChannelsArgs, context: Context) { 25 | graphQLAuthCheck(context); 26 | try { 27 | const ability = context.user.ability; 28 | const channels = args.application 29 | ? await Channel.find({ 30 | application: args.application, 31 | ...accessibleBy(ability, 'read').Channel, 32 | }) 33 | : await Channel.find(accessibleBy(ability, 'read').Channel); 34 | return channels; 35 | } catch (err) { 36 | logger.error(err.message, { stack: err.stack }); 37 | if (err instanceof GraphQLError) { 38 | throw new GraphQLError(err.message); 39 | } 40 | throw new GraphQLError( 41 | context.i18next.t('common.errors.internalServerError') 42 | ); 43 | } 44 | }, 45 | }; 46 | -------------------------------------------------------------------------------- /src/schema/query/draftRecords.query.ts: -------------------------------------------------------------------------------- 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 | }; 44 | -------------------------------------------------------------------------------- /src/schema/query/groups.query.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLBoolean, GraphQLID, GraphQLError, GraphQLList } from 'graphql'; 2 | import { Group } from '@models'; 3 | import { GroupType } from '../types'; 4 | import { AppAbility } from '@security/defineUserAbility'; 5 | import { logger } from '@services/logger.service'; 6 | import { accessibleBy } from '@casl/mongoose'; 7 | import { graphQLAuthCheck } from '@schema/shared'; 8 | import { Types } from 'mongoose'; 9 | import { Context } from '@server/apollo/context'; 10 | 11 | /** Arguments for the groups query */ 12 | type GroupsArgs = { 13 | all?: boolean; 14 | application?: string | Types.ObjectId; 15 | }; 16 | 17 | /** 18 | * Lists groups. 19 | * Throws error if user is not logged or does not have permission 20 | */ 21 | export default { 22 | type: new GraphQLList(GroupType), 23 | args: { 24 | all: { type: GraphQLBoolean }, 25 | application: { type: GraphQLID }, 26 | }, 27 | async resolve(parent, args: GroupsArgs, context: Context) { 28 | graphQLAuthCheck(context); 29 | try { 30 | const ability: AppAbility = context.user.ability; 31 | const groups = await Group.find(accessibleBy(ability, 'read').Group); 32 | return groups; 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 | }; 44 | -------------------------------------------------------------------------------- /src/schema/query/layer.query.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLError, GraphQLNonNull, GraphQLID } from 'graphql'; 2 | import { LayerType } from '../types'; 3 | import { Layer } from '@models'; 4 | import { graphQLAuthCheck } from '@schema/shared'; 5 | import { logger } from '@services/logger.service'; 6 | 7 | /** 8 | * List all layers. 9 | * Throw GraphQL error if not logged and if not permission to access. 10 | */ 11 | export default { 12 | type: LayerType, 13 | args: { 14 | id: { type: new GraphQLNonNull(GraphQLID) }, 15 | }, 16 | async resolve(parent, args, context) { 17 | graphQLAuthCheck(context); 18 | try { 19 | return await Layer.findById(args.id); 20 | } catch (err) { 21 | logger.error(err.message, { stack: err.stack }); 22 | if (err instanceof GraphQLError) { 23 | throw new GraphQLError(err.message); 24 | } 25 | throw new GraphQLError( 26 | context.i18next.t('common.errors.internalServerError') 27 | ); 28 | } 29 | }, 30 | }; 31 | -------------------------------------------------------------------------------- /src/schema/query/me.query.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLError } from 'graphql'; 2 | import { UserType } from '../types'; 3 | import { logger } from '@services/logger.service'; 4 | import { graphQLAuthCheck } from '@schema/shared'; 5 | import { Context } from '@server/apollo/context'; 6 | 7 | /** 8 | * Return user from logged user id. 9 | * Throw GraphQL error if not logged. 10 | */ 11 | export default { 12 | type: UserType, 13 | resolve: async (parent, args, context: Context) => { 14 | graphQLAuthCheck(context); 15 | try { 16 | return context.user; 17 | } catch (err) { 18 | logger.error(err.message, { stack: err.stack }); 19 | if (err instanceof GraphQLError) { 20 | throw new GraphQLError(err.message); 21 | } 22 | throw new GraphQLError( 23 | context.i18next.t('common.errors.internalServerError') 24 | ); 25 | } 26 | }, 27 | }; 28 | -------------------------------------------------------------------------------- /src/schema/query/page.query.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLNonNull, GraphQLID, GraphQLError } from 'graphql'; 2 | import { PageType } from '../types'; 3 | import { Page } from '@models'; 4 | import extendAbilityForPage from '@security/extendAbilityForPage'; 5 | import { logger } from '@services/logger.service'; 6 | import { graphQLAuthCheck } from '@schema/shared'; 7 | import { Types } from 'mongoose'; 8 | import { Context } from '@server/apollo/context'; 9 | 10 | /** Arguments for the page query */ 11 | type PageArgs = { 12 | id: string | Types.ObjectId; 13 | }; 14 | 15 | /** 16 | * Return page from id if available for the logged user. 17 | * Throw GraphQL error if not logged. 18 | */ 19 | export default { 20 | type: PageType, 21 | args: { 22 | id: { type: new GraphQLNonNull(GraphQLID) }, 23 | }, 24 | async resolve(parent, args: PageArgs, context: Context) { 25 | graphQLAuthCheck(context); 26 | try { 27 | const user = context.user; 28 | // get data 29 | const page = await Page.findById(args.id); 30 | 31 | // check ability 32 | const ability = await extendAbilityForPage(user, page); 33 | if (ability.cannot('read', page)) { 34 | throw new GraphQLError( 35 | context.i18next.t('common.errors.permissionNotGranted') 36 | ); 37 | } 38 | 39 | return page; 40 | } catch (err) { 41 | logger.error(err.message, { stack: err.stack }); 42 | if (err instanceof GraphQLError) { 43 | throw new GraphQLError(err.message); 44 | } 45 | throw new GraphQLError( 46 | context.i18next.t('common.errors.internalServerError') 47 | ); 48 | } 49 | }, 50 | }; 51 | -------------------------------------------------------------------------------- /src/schema/query/pages.query.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLError, GraphQLList } from 'graphql'; 2 | import { PageType } from '../types'; 3 | import { Application, Page } from '@models'; 4 | import { AppAbility } from '@security/defineUserAbility'; 5 | import extendAbilityForPage from '@security/extendAbilityForPage'; 6 | import { logger } from '@services/logger.service'; 7 | import { accessibleBy } from '@casl/mongoose'; 8 | import { graphQLAuthCheck } from '@schema/shared'; 9 | import { Context } from '@server/apollo/context'; 10 | 11 | /** 12 | * List all pages available for the logged user. 13 | * Throw GraphQL error if not logged. 14 | */ 15 | export default { 16 | type: new GraphQLList(PageType), 17 | async resolve(parent, args, context: Context) { 18 | graphQLAuthCheck(context); 19 | try { 20 | const user = context.user; 21 | // create ability object for all pages 22 | let ability: AppAbility = user.ability; 23 | const applications = await Application.find( 24 | accessibleBy(ability, 'read').Application 25 | ); 26 | for (const application of applications) { 27 | ability = await extendAbilityForPage(user, application, ability); 28 | } 29 | 30 | // return the pages 31 | return await Page.find(accessibleBy(ability, 'read').Page); 32 | } catch (err) { 33 | logger.error(err.message, { stack: err.stack }); 34 | if (err instanceof GraphQLError) { 35 | throw new GraphQLError(err.message); 36 | } 37 | throw new GraphQLError( 38 | context.i18next.t('common.errors.internalServerError') 39 | ); 40 | } 41 | }, 42 | }; 43 | -------------------------------------------------------------------------------- /src/schema/query/permissions.query.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLList, GraphQLBoolean, GraphQLError } from 'graphql'; 2 | import { Permission } from '@models'; 3 | import { PermissionType } from '../types'; 4 | import { logger } from '@services/logger.service'; 5 | import { graphQLAuthCheck } from '@schema/shared'; 6 | import { Context } from '@server/apollo/context'; 7 | 8 | /** Arguments for the permissions query */ 9 | type PermissionsArgs = { 10 | application?: boolean; 11 | }; 12 | 13 | /** 14 | * List permissions. 15 | * Throw GraphQL error if not logged. 16 | */ 17 | export default { 18 | type: new GraphQLList(PermissionType), 19 | args: { 20 | application: { type: GraphQLBoolean }, 21 | }, 22 | async resolve(parent, args: PermissionsArgs, context: Context) { 23 | // Check that user is authenticated 24 | graphQLAuthCheck(context); 25 | try { 26 | if (args.application) { 27 | // Query application scoped permissions 28 | const permissions = await Permission.find({ global: false }); 29 | return permissions; 30 | } 31 | // Query admin permissions 32 | const permissions = await Permission.find({ global: true }); 33 | return permissions; 34 | } catch (err) { 35 | logger.error(err.message, { stack: err.stack }); 36 | if (err instanceof GraphQLError) { 37 | throw new GraphQLError(err.message); 38 | } 39 | throw new GraphQLError( 40 | context.i18next.t('common.errors.internalServerError') 41 | ); 42 | } 43 | }, 44 | }; 45 | -------------------------------------------------------------------------------- /src/schema/query/records.query.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLError, GraphQLList } from 'graphql'; 2 | import { RecordType } from '../types'; 3 | import { Record } from '@models'; 4 | import extendAbilityForRecords from '@security/extendAbilityForRecords'; 5 | import { getAccessibleFields } from '@utils/form'; 6 | import { logger } from '@services/logger.service'; 7 | import { accessibleBy } from '@casl/mongoose'; 8 | import { graphQLAuthCheck } from '@schema/shared'; 9 | import { Context } from '@server/apollo/context'; 10 | 11 | /** 12 | * List all records available for the logged user. 13 | * Throw GraphQL error if not logged. 14 | */ 15 | export default { 16 | type: new GraphQLList(RecordType), 17 | async resolve(parent, args, context: Context) { 18 | graphQLAuthCheck(context); 19 | try { 20 | const user = context.user; 21 | const ability = await extendAbilityForRecords(user); 22 | // Return the records 23 | const records = await Record.find(accessibleBy(ability, 'read').Record); 24 | return getAccessibleFields(records, ability); 25 | } catch (err) { 26 | logger.error(err.message, { stack: err.stack }); 27 | if (err instanceof GraphQLError) { 28 | throw new GraphQLError(err.message); 29 | } 30 | throw new GraphQLError( 31 | context.i18next.t('common.errors.internalServerError') 32 | ); 33 | } 34 | }, 35 | }; 36 | -------------------------------------------------------------------------------- /src/schema/query/referenceData.query.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLNonNull, GraphQLID, GraphQLError } from 'graphql'; 2 | import { ReferenceDataType } from '../types'; 3 | import { ReferenceData } from '@models'; 4 | import { logger } from '@services/logger.service'; 5 | import { graphQLAuthCheck } from '@schema/shared'; 6 | import { Types } from 'mongoose'; 7 | import { Context } from '@server/apollo/context'; 8 | 9 | /** Arguments for the referenceData query */ 10 | type ReferenceDataArgs = { 11 | id: string | Types.ObjectId; 12 | }; 13 | 14 | /** 15 | * Return Reference Data from id if available for the logged user. 16 | * Throw GraphQL error if not logged. 17 | */ 18 | export default { 19 | type: ReferenceDataType, 20 | args: { 21 | id: { type: new GraphQLNonNull(GraphQLID) }, 22 | }, 23 | async resolve(parent, args: ReferenceDataArgs, context: Context) { 24 | graphQLAuthCheck(context); 25 | try { 26 | return await ReferenceData.findById(args.id); 27 | } catch (err) { 28 | logger.error(err.message, { stack: err.stack }); 29 | if (err instanceof GraphQLError) { 30 | throw new GraphQLError(err.message); 31 | } 32 | throw new GraphQLError( 33 | context.i18next.t('common.errors.internalServerError') 34 | ); 35 | } 36 | }, 37 | }; 38 | -------------------------------------------------------------------------------- /src/schema/query/resource.query.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLNonNull, GraphQLID, GraphQLError } from 'graphql'; 2 | import { ResourceType } from '../types'; 3 | import { Resource } from '@models'; 4 | import { AppAbility } from '@security/defineUserAbility'; 5 | import { logger } from '@services/logger.service'; 6 | import { graphQLAuthCheck } from '@schema/shared'; 7 | import { Types } from 'mongoose'; 8 | import { Context } from '@server/apollo/context'; 9 | 10 | /** Arguments for the resource query */ 11 | type ResourceArgs = { 12 | id: string | Types.ObjectId; 13 | }; 14 | 15 | /** 16 | * Return resource from id if available for the logged user. 17 | * Throw GraphQL error if not logged. 18 | */ 19 | export default { 20 | type: ResourceType, 21 | args: { 22 | id: { type: new GraphQLNonNull(GraphQLID) }, 23 | }, 24 | async resolve(parent, args: ResourceArgs, context: Context) { 25 | graphQLAuthCheck(context); 26 | try { 27 | const user = context.user; 28 | const ability: AppAbility = user.ability; 29 | const resource = await Resource.findOne({ _id: args.id }); 30 | 31 | if (ability.cannot('read', resource)) { 32 | throw new GraphQLError( 33 | context.i18next.t('common.errors.permissionNotGranted') 34 | ); 35 | } 36 | return resource; 37 | } catch (err) { 38 | logger.error(err.message, { stack: err.stack }); 39 | if (err instanceof GraphQLError) { 40 | throw new GraphQLError(err.message); 41 | } 42 | throw new GraphQLError( 43 | context.i18next.t('common.errors.internalServerError') 44 | ); 45 | } 46 | }, 47 | }; 48 | -------------------------------------------------------------------------------- /src/schema/query/rolesFromApplications.query.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLList, GraphQLID, GraphQLError, GraphQLNonNull } from 'graphql'; 2 | import { Role } from '@models'; 3 | import { RoleType } from '../types'; 4 | import { logger } from '@services/logger.service'; 5 | import { graphQLAuthCheck } from '@schema/shared'; 6 | import { Types } from 'mongoose'; 7 | import { Context } from '@server/apollo/context'; 8 | 9 | /** Arguments for the rolesFromApplications query */ 10 | type RolesFromApplicationsArgs = { 11 | applications: string[] | Types.ObjectId[]; 12 | }; 13 | 14 | /** 15 | * List passed applications roles if user is logged, but only title and id. 16 | * Throw GraphQL error if not logged. 17 | */ 18 | export default { 19 | type: new GraphQLList(RoleType), 20 | args: { 21 | applications: { type: new GraphQLNonNull(new GraphQLList(GraphQLID)) }, 22 | }, 23 | async resolve(parent, args: RolesFromApplicationsArgs, context: Context) { 24 | graphQLAuthCheck(context); 25 | try { 26 | const roles = await Role.find({ 27 | application: { $in: args.applications }, 28 | }).select('id title application'); 29 | return roles; 30 | } catch (err) { 31 | logger.error(err.message, { stack: err.stack }); 32 | if (err instanceof GraphQLError) { 33 | throw new GraphQLError(err.message); 34 | } 35 | throw new GraphQLError( 36 | context.i18next.t('common.errors.internalServerError') 37 | ); 38 | } 39 | }, 40 | }; 41 | -------------------------------------------------------------------------------- /src/schema/query/step.query.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLNonNull, GraphQLID, GraphQLError } from 'graphql'; 2 | import { StepType } from '../types'; 3 | import { Step } from '@models'; 4 | import extendAbilityForStep from '@security/extendAbilityForStep'; 5 | import { logger } from '@services/logger.service'; 6 | import { graphQLAuthCheck } from '@schema/shared'; 7 | import { Types } from 'mongoose'; 8 | import { Context } from '@server/apollo/context'; 9 | 10 | /** Arguments for the step query */ 11 | type StepArgs = { 12 | id: string | Types.ObjectId; 13 | }; 14 | 15 | /** 16 | * Returns step from id if available for the logged user. 17 | * Throw GraphQL error if not logged. 18 | */ 19 | export default { 20 | type: StepType, 21 | args: { 22 | id: { type: new GraphQLNonNull(GraphQLID) }, 23 | }, 24 | async resolve(parent, args: StepArgs, context: Context) { 25 | graphQLAuthCheck(context); 26 | try { 27 | const user = context.user; 28 | // get data and check permissions 29 | const step = await Step.findById(args.id); 30 | const ability = await extendAbilityForStep(user, step); 31 | if (ability.cannot('read', step)) { 32 | throw new GraphQLError( 33 | context.i18next.t('common.errors.permissionNotGranted') 34 | ); 35 | } 36 | 37 | return step; 38 | } catch (err) { 39 | logger.error(err.message, { stack: err.stack }); 40 | if (err instanceof GraphQLError) { 41 | throw new GraphQLError(err.message); 42 | } 43 | throw new GraphQLError( 44 | context.i18next.t('common.errors.internalServerError') 45 | ); 46 | } 47 | }, 48 | }; 49 | -------------------------------------------------------------------------------- /src/schema/query/steps.query.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLError, GraphQLList } from 'graphql'; 2 | import { StepType } from '../types'; 3 | import { Step } from '@models'; 4 | import { AppAbility } from '@security/defineUserAbility'; 5 | import { logger } from '@services/logger.service'; 6 | import { accessibleBy } from '@casl/mongoose'; 7 | import { graphQLAuthCheck } from '@schema/shared'; 8 | import { Context } from '@server/apollo/context'; 9 | 10 | /** 11 | * List all steps available for the logged user. 12 | * Throw GraphQL error if not logged. 13 | */ 14 | export default { 15 | type: new GraphQLList(StepType), 16 | async resolve(parent, args, context: Context) { 17 | graphQLAuthCheck(context); 18 | try { 19 | const ability: AppAbility = context.user.ability; 20 | const steps = await Step.find(accessibleBy(ability, 'read').Step); 21 | return steps; 22 | } catch (err) { 23 | logger.error(err.message, { stack: err.stack }); 24 | if (err instanceof GraphQLError) { 25 | throw new GraphQLError(err.message); 26 | } 27 | throw new GraphQLError( 28 | context.i18next.t('common.errors.internalServerError') 29 | ); 30 | } 31 | }, 32 | }; 33 | -------------------------------------------------------------------------------- /src/schema/query/types.query.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLError, GraphQLObjectType } from 'graphql'; 2 | import { logger } from '@services/logger.service'; 3 | import { graphQLAuthCheck } from '@schema/shared'; 4 | import { Context } from '@server/apollo/context'; 5 | import GraphQLJSON from 'graphql-type-json'; 6 | import { introspectionResult } from '@server/index'; 7 | 8 | /** 9 | * Return the types to be used in the query builder. 10 | * Throw GraphQL error if not logged. 11 | */ 12 | export default { 13 | type: new GraphQLObjectType({ 14 | name: 'types', 15 | fields: () => ({ 16 | availableQueries: { 17 | type: GraphQLJSON, 18 | }, 19 | userFields: { 20 | type: GraphQLJSON, 21 | }, 22 | }), 23 | }), 24 | resolve: async (parent, args, context: Context) => { 25 | graphQLAuthCheck(context); 26 | try { 27 | return introspectionResult; 28 | } catch (err) { 29 | logger.error(err.message, { stack: err.stack }); 30 | if (err instanceof GraphQLError) { 31 | throw new GraphQLError(err.message); 32 | } 33 | throw new GraphQLError( 34 | context.i18next.t('common.errors.internalServerError') 35 | ); 36 | } 37 | }, 38 | }; 39 | -------------------------------------------------------------------------------- /src/schema/query/workflows.query.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLList, GraphQLError } from 'graphql'; 2 | import { Workflow } from '@models'; 3 | import { WorkflowType } from '../types'; 4 | import { AppAbility } from '@security/defineUserAbility'; 5 | import { logger } from '@services/logger.service'; 6 | import { accessibleBy } from '@casl/mongoose'; 7 | import { graphQLAuthCheck } from '@schema/shared'; 8 | import { Context } from '@server/apollo/context'; 9 | 10 | /** 11 | * List all workflows available for the logged user. 12 | * Throw GraphQL error if not logged. 13 | */ 14 | export default { 15 | type: new GraphQLList(WorkflowType), 16 | async resolve(parent, args, context: Context) { 17 | graphQLAuthCheck(context); 18 | try { 19 | const ability: AppAbility = context.user.ability; 20 | return await Workflow.find(accessibleBy(ability, 'read').Workflow); 21 | } catch (err) { 22 | logger.error(err.message, { stack: err.stack }); 23 | if (err instanceof GraphQLError) { 24 | throw new GraphQLError(err.message); 25 | } 26 | throw new GraphQLError( 27 | context.i18next.t('common.errors.internalServerError') 28 | ); 29 | } 30 | }, 31 | }; 32 | -------------------------------------------------------------------------------- /src/schema/shared/auth-check.util.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLError } from 'graphql'; 2 | 3 | /** 4 | * Authentication check done in GraphQL requests to determine if user is connected or not 5 | * 6 | * @param context GraphQLContext (todo: type) 7 | */ 8 | export const graphQLAuthCheck = (context: any) => { 9 | // Authentication check 10 | const user = context.user; 11 | if (!user) { 12 | throw new GraphQLError(context.i18next.t('common.errors.userNotLogged')); 13 | } 14 | }; 15 | 16 | export default graphQLAuthCheck; 17 | -------------------------------------------------------------------------------- /src/schema/shared/index.ts: -------------------------------------------------------------------------------- 1 | export * from './auth-check.util'; 2 | -------------------------------------------------------------------------------- /src/schema/subscription/applicationEdited.subscription.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLID } from 'graphql'; 2 | import { RedisPubSub } from 'graphql-redis-subscriptions'; 3 | import { withFilter } from 'graphql-subscriptions'; 4 | import pubsub from '../../server/pubsub'; 5 | import { ApplicationType } from '../types'; 6 | import { graphQLAuthCheck } from '@schema/shared'; 7 | import { Types } from 'mongoose'; 8 | import { Context } from '@server/apollo/context'; 9 | 10 | /** Arguments for the applicationEdited subscription */ 11 | type ApplicationEditedArgs = { 12 | id?: string | Types.ObjectId; 13 | }; 14 | 15 | /** 16 | * Subscription to detect if application is being edited. 17 | */ 18 | export default { 19 | type: ApplicationType, 20 | args: { 21 | id: { type: GraphQLID }, 22 | }, 23 | subscribe: async (parent, args: ApplicationEditedArgs, context: Context) => { 24 | graphQLAuthCheck(context); 25 | const subscriber: RedisPubSub = await pubsub(); 26 | const user = context.user; 27 | return withFilter( 28 | () => subscriber.asyncIterator('app_edited'), 29 | (payload, variables) => { 30 | if (variables.id) { 31 | return ( 32 | payload.application._id === variables.id && 33 | payload.user !== user._id.toString() 34 | ); 35 | } 36 | return false; 37 | } 38 | )(parent, args, context); 39 | }, 40 | resolve: (payload) => { 41 | return payload.application; 42 | }, 43 | }; 44 | -------------------------------------------------------------------------------- /src/schema/subscription/applicationUnlocked.subscription.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLID } from 'graphql'; 2 | import { RedisPubSub } from 'graphql-redis-subscriptions'; 3 | import { withFilter } from 'graphql-subscriptions'; 4 | import pubsub from '../../server/pubsub'; 5 | import { ApplicationType } from '../types'; 6 | import { graphQLAuthCheck } from '@schema/shared'; 7 | import { Types } from 'mongoose'; 8 | import { Context } from '@server/apollo/context'; 9 | 10 | /** Arguments for the applicationUnlocked subscription */ 11 | type ApplicationUnlockedArgs = { 12 | id?: string | Types.ObjectId; 13 | }; 14 | 15 | /** 16 | * Subscription to detect if application is unlocked. 17 | */ 18 | export default { 19 | type: ApplicationType, 20 | args: { 21 | id: { type: GraphQLID }, 22 | }, 23 | subscribe: async ( 24 | parent, 25 | args: ApplicationUnlockedArgs, 26 | context: Context 27 | ) => { 28 | graphQLAuthCheck(context); 29 | const subscriber: RedisPubSub = await pubsub(); 30 | return withFilter( 31 | () => subscriber.asyncIterator('app_lock'), 32 | (payload, variables) => { 33 | if (variables.id) { 34 | return payload.application._id === variables.id; 35 | } 36 | return false; 37 | } 38 | )(parent, args, context); 39 | }, 40 | resolve: (payload) => { 41 | return payload.application; 42 | }, 43 | }; 44 | -------------------------------------------------------------------------------- /src/schema/subscription/index.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLObjectType } from 'graphql'; 2 | import applicationUnlocked from './applicationUnlocked.subscription'; 3 | import applicationEdited from './applicationEdited.subscription'; 4 | import notification from './notification.subscription'; 5 | import recordAdded from './recordAdded.subscription'; 6 | 7 | /** GraphQL subscriptions type definition */ 8 | const Subscription = new GraphQLObjectType({ 9 | name: 'Subscription', 10 | fields: { 11 | applicationUnlocked, 12 | applicationEdited, 13 | notification, 14 | recordAdded, 15 | }, 16 | }); 17 | 18 | export default Subscription; 19 | -------------------------------------------------------------------------------- /src/schema/subscription/notification.subscription.ts: -------------------------------------------------------------------------------- 1 | import { RedisPubSub } from 'graphql-redis-subscriptions'; 2 | import pubsub from '../../server/pubsub'; 3 | import { User } from '@models'; 4 | import { NotificationType } from '../types'; 5 | import { Context } from '@server/apollo/context'; 6 | 7 | /** 8 | * Subscription to detect new notifications. 9 | * TODO: rethink how logs are created in the system. 10 | */ 11 | export default { 12 | type: NotificationType, 13 | subscribe: async (parent, args, context: Context) => { 14 | // Subscribe to channels available in user's roles 15 | const subscriber: RedisPubSub = await pubsub(); 16 | const user: User = context.user; 17 | return subscriber.asyncIterator( 18 | user.roles.map((role) => role.channels.map((x) => String(x._id))).flat() 19 | ); 20 | }, 21 | }; 22 | -------------------------------------------------------------------------------- /src/schema/subscription/recordAdded.subscription.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLID } from 'graphql'; 2 | import { withFilter } from 'graphql-subscriptions'; 3 | import { RecordType } from '../types'; 4 | import pubsub from '../../server/pubsub'; 5 | import { RedisPubSub } from 'graphql-redis-subscriptions'; 6 | import { Types } from 'mongoose'; 7 | import { Context } from '@server/apollo/context'; 8 | 9 | /** Arguments for the recordAdded subscription */ 10 | type RecordAddedArgs = { 11 | resource?: string | Types.ObjectId; 12 | form?: string | Types.ObjectId; 13 | }; 14 | /** 15 | * Subscription to detect addition of record. 16 | */ 17 | export default { 18 | type: RecordType, 19 | args: { 20 | resource: { type: GraphQLID }, 21 | form: { type: GraphQLID }, 22 | }, 23 | subscribe: async (parent, args: RecordAddedArgs, context: Context) => { 24 | const subscriber: RedisPubSub = await pubsub(); 25 | return withFilter( 26 | () => subscriber.asyncIterator('record_added'), 27 | (payload, variables) => { 28 | if (variables.resource) { 29 | return payload.recordAdded.resource === variables.resource; 30 | } 31 | if (variables.form) { 32 | return payload.recordAdded.form === variables.form; 33 | } 34 | return true; 35 | } 36 | )(parent, args, context); 37 | }, 38 | }; 39 | -------------------------------------------------------------------------------- /src/schema/types/aggregation.type.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLID, GraphQLObjectType, GraphQLString } from 'graphql'; 2 | import GraphQLJSON from 'graphql-type-json'; 3 | import { Connection } from './pagination.type'; 4 | 5 | /** 6 | * GraphQL Aggregation type. 7 | */ 8 | export const AggregationType = new GraphQLObjectType({ 9 | name: 'Aggregation', 10 | fields: () => ({ 11 | id: { 12 | type: GraphQLID, 13 | resolve(parent) { 14 | return parent._id ? parent._id : parent.id; 15 | }, 16 | }, 17 | name: { type: GraphQLString }, 18 | // dataSource: { type: GraphQLID }, 19 | sourceFields: { type: GraphQLJSON }, 20 | pipeline: { type: GraphQLJSON }, 21 | // mapping: { type: GraphQLJSON }, 22 | createdAt: { type: GraphQLString }, 23 | modifiedAt: { type: GraphQLString }, 24 | }), 25 | }); 26 | 27 | /** GraphQL aggragatiom connection type definition */ 28 | export const AggregationConnectionType = Connection(AggregationType); 29 | -------------------------------------------------------------------------------- /src/schema/types/customNotification.type.ts: -------------------------------------------------------------------------------- 1 | import { 2 | GraphQLID, 3 | GraphQLObjectType, 4 | GraphQLString, 5 | GraphQLBoolean, 6 | } from 'graphql'; 7 | import GraphQLJSON from 'graphql-type-json'; 8 | import { Connection } from './pagination.type'; 9 | 10 | /** 11 | * GraphQL Template type. 12 | */ 13 | export const CustomNotificationType = new GraphQLObjectType({ 14 | name: 'CustomNotification', 15 | fields: () => ({ 16 | id: { 17 | type: GraphQLID, 18 | resolve(parent) { 19 | return parent._id ? parent._id : parent.id; 20 | }, 21 | }, 22 | name: { type: GraphQLString }, 23 | description: { type: GraphQLString }, 24 | schedule: { type: GraphQLString }, 25 | notificationType: { type: GraphQLString }, 26 | resource: { type: GraphQLID }, 27 | layout: { type: GraphQLID }, 28 | template: { type: GraphQLID }, 29 | recipients: { type: GraphQLJSON }, 30 | enabled: { type: GraphQLBoolean }, 31 | lastExecution: { type: GraphQLString }, 32 | createdAt: { type: GraphQLString }, 33 | modifiedAt: { type: GraphQLString }, 34 | status: { type: GraphQLString }, 35 | recipientsType: { type: GraphQLString }, 36 | }), 37 | }); 38 | 39 | /** Custom Notification connection type */ 40 | export const CustomNotificationConnectionConnectionType = Connection( 41 | CustomNotificationType 42 | ); 43 | -------------------------------------------------------------------------------- /src/schema/types/customTemplate.type.ts: -------------------------------------------------------------------------------- 1 | import { 2 | GraphQLID, 3 | GraphQLObjectType, 4 | GraphQLString, 5 | GraphQLInt, 6 | GraphQLBoolean, 7 | } from 'graphql'; 8 | import GraphQLJSON from 'graphql-type-json'; 9 | import { Connection } from './pagination.type'; 10 | /** 11 | * GraphQL DistributionList type. 12 | */ 13 | export const CustomTemplateType = new GraphQLObjectType({ 14 | name: 'CustomTemplate', 15 | fields: () => ({ 16 | id: { 17 | type: GraphQLID, 18 | resolve(parent) { 19 | return parent._id ? parent._id : parent.id; 20 | }, 21 | }, 22 | name: { type: GraphQLString }, 23 | subject: { type: GraphQLString }, 24 | header: { type: GraphQLJSON }, 25 | body: { type: GraphQLJSON }, 26 | banner: { type: GraphQLJSON }, 27 | footer: { type: GraphQLJSON }, 28 | isDeleted: { type: GraphQLInt }, 29 | createdBy: { type: GraphQLJSON }, 30 | applicationId: { type: GraphQLID }, 31 | isFromEmailNotification: { type: GraphQLBoolean }, 32 | }), 33 | }); 34 | 35 | /** Custom Template connection type */ 36 | export const CustomTemplateConnectionType = Connection(CustomTemplateType); 37 | -------------------------------------------------------------------------------- /src/schema/types/distributionList.type.ts: -------------------------------------------------------------------------------- 1 | import { 2 | GraphQLID, 3 | GraphQLList, 4 | GraphQLObjectType, 5 | GraphQLString, 6 | } from 'graphql'; 7 | 8 | /** 9 | * GraphQL DistributionList type. 10 | */ 11 | export const DistributionListType = new GraphQLObjectType({ 12 | name: 'DistributionList', 13 | fields: () => ({ 14 | id: { 15 | type: GraphQLID, 16 | resolve(parent) { 17 | return parent._id ? parent._id : parent.id; 18 | }, 19 | }, 20 | name: { type: GraphQLString }, 21 | emails: { type: new GraphQLList(GraphQLString) }, 22 | }), 23 | }); 24 | -------------------------------------------------------------------------------- /src/schema/types/emailDistribution.type.ts: -------------------------------------------------------------------------------- 1 | import { 2 | GraphQLID, 3 | GraphQLList, 4 | GraphQLObjectType, 5 | GraphQLString, 6 | GraphQLInt, 7 | } from 'graphql'; 8 | import GraphQLJSON from 'graphql-type-json'; 9 | import { Connection } from './pagination.type'; 10 | import { QueryType } from './emailNotification.type'; 11 | 12 | /** 13 | * Defines filter used as part of distribution list. 14 | */ 15 | export const DistributionListSource = new GraphQLObjectType({ 16 | name: 'DistributionListSource', 17 | fields: () => ({ 18 | resource: { type: GraphQLString }, 19 | reference: { type: GraphQLString }, 20 | commonServiceFilter: { type: GraphQLJSON }, 21 | query: { type: QueryType }, 22 | inputEmails: { 23 | type: new GraphQLList(GraphQLString), 24 | resolve: (parent) => parent?.inputEmails ?? [], // Ensure it always returns an array 25 | }, 26 | }), 27 | }); 28 | 29 | /** 30 | * GraphQL DistributionList type. 31 | */ 32 | export const EmailDistributionListType = new GraphQLObjectType({ 33 | name: 'QuickEmailDistributionList', 34 | fields: () => ({ 35 | id: { 36 | type: GraphQLID, 37 | resolve(parent) { 38 | return parent._id ? parent._id : parent.id; 39 | }, 40 | }, 41 | name: { type: GraphQLString }, 42 | to: { type: DistributionListSource }, 43 | cc: { type: DistributionListSource }, 44 | bcc: { type: DistributionListSource }, 45 | isDeleted: { type: GraphQLInt }, 46 | createdBy: { type: GraphQLJSON }, 47 | applicationId: { type: GraphQLID }, 48 | }), 49 | }); 50 | 51 | /** Email Notification connection type */ 52 | export const EmailDistributionConnectionType = Connection( 53 | EmailDistributionListType 54 | ); 55 | -------------------------------------------------------------------------------- /src/schema/types/geospatial.type.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLObjectType } from 'graphql'; 2 | import GraphQLJSON from 'graphql-type-json'; 3 | import { GeospatialEnumType } from '@const/enumTypes'; 4 | 5 | /** GraphQL GeoSpatial type definition */ 6 | export const GeospatialType = new GraphQLObjectType({ 7 | name: 'Geospatial', 8 | fields: () => ({ 9 | type: { type: GeospatialEnumType }, 10 | coordinates: { type: GraphQLJSON }, 11 | }), 12 | }); 13 | -------------------------------------------------------------------------------- /src/schema/types/group.type.ts: -------------------------------------------------------------------------------- 1 | import { 2 | GraphQLObjectType, 3 | GraphQLID, 4 | GraphQLString, 5 | GraphQLInt, 6 | } from 'graphql'; 7 | import { User } from '@models'; 8 | 9 | /** GraphQL Group type definition */ 10 | export const GroupType = new GraphQLObjectType({ 11 | name: 'Group', 12 | fields: () => ({ 13 | id: { 14 | type: GraphQLID, 15 | resolve(parent) { 16 | return parent._id; 17 | }, 18 | }, 19 | title: { type: GraphQLString }, 20 | description: { type: GraphQLString }, 21 | usersCount: { 22 | type: GraphQLInt, 23 | async resolve(parent) { 24 | const users = await User.find({ groups: parent.id }).count(); 25 | return users; 26 | }, 27 | }, 28 | }), 29 | }); 30 | -------------------------------------------------------------------------------- /src/schema/types/historyVersion.type.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLObjectType, GraphQLString, GraphQLList } from 'graphql'; 2 | import { GraphQLDateTime } from 'graphql-scalars'; 3 | import { VersionType } from './version.type'; 4 | 5 | /** 6 | * GraphQL Object Type of Single history change. 7 | */ 8 | const changeType = new GraphQLObjectType({ 9 | name: 'Change', 10 | fields: () => ({ 11 | type: { type: GraphQLString }, 12 | displayType: { type: GraphQLString }, 13 | field: { type: GraphQLString }, 14 | displayName: { type: GraphQLString }, 15 | old: { type: GraphQLString }, 16 | new: { type: GraphQLString }, 17 | }), 18 | }); 19 | 20 | /** 21 | * GraphQL Object Type of History entry. 22 | */ 23 | export const HistoryVersionType = new GraphQLObjectType({ 24 | name: 'HistoryVersion', 25 | fields: () => ({ 26 | createdAt: { type: GraphQLDateTime }, 27 | createdBy: { type: GraphQLString }, 28 | changes: { type: new GraphQLList(changeType) }, 29 | version: { type: VersionType }, 30 | }), 31 | }); 32 | -------------------------------------------------------------------------------- /src/schema/types/index.ts: -------------------------------------------------------------------------------- 1 | export * from './access.type'; 2 | export * from './apiConfiguration.type'; 3 | export * from './application.type'; 4 | export * from './channel.type'; 5 | export * from './dashboard.type'; 6 | export * from './form.type'; 7 | export * from './notification.type'; 8 | export * from './page.type'; 9 | export * from './permission.type'; 10 | export * from './positionAttribute.type'; 11 | export * from './positionAttributeCategory.type'; 12 | export * from './pullJob.type'; 13 | export * from './record.type'; 14 | export * from './referenceData.type'; 15 | export * from './resource.type'; 16 | export * from './role.type'; 17 | export * from './step.type'; 18 | export * from './user.type'; 19 | export * from './version.type'; 20 | export * from './workflow.type'; 21 | export * from './pagination.type'; 22 | export * from './layout.type'; 23 | export * from './historyVersion.type'; 24 | export * from './group.type'; 25 | export * from './aggregation.type'; 26 | export * from './template.type'; 27 | export * from './distributionList.type'; 28 | export * from './customNotification.type'; 29 | export * from './metadata.type'; 30 | export * from './layer.type'; 31 | export * from './draftRecord.type'; 32 | export * from './emailNotification.type'; 33 | -------------------------------------------------------------------------------- /src/schema/types/layout.type.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLID, GraphQLObjectType, GraphQLString } from 'graphql'; 2 | import GraphQLJSON from 'graphql-type-json'; 3 | import { Connection } from './pagination.type'; 4 | 5 | /** 6 | * GraphQL Layout type. 7 | */ 8 | export const LayoutType = new GraphQLObjectType({ 9 | name: 'Layout', 10 | fields: () => ({ 11 | id: { 12 | type: GraphQLID, 13 | resolve(parent) { 14 | return parent._id ? parent._id : parent.id; 15 | }, 16 | }, 17 | name: { type: GraphQLString }, 18 | createdAt: { type: GraphQLString }, 19 | query: { type: GraphQLJSON }, 20 | display: { type: GraphQLJSON }, 21 | }), 22 | }); 23 | 24 | /** GraphQL layout connection type definition */ 25 | export const LayoutConnectionType = Connection(LayoutType); 26 | -------------------------------------------------------------------------------- /src/schema/types/notification.type.ts: -------------------------------------------------------------------------------- 1 | import { 2 | GraphQLObjectType, 3 | GraphQLString, 4 | GraphQLID, 5 | GraphQLList, 6 | } from 'graphql'; 7 | import GraphQLJSON from 'graphql-type-json'; 8 | import { AppAbility } from 'security/defineUserAbility'; 9 | import { Channel, User } from '@models'; 10 | import { ChannelType } from './channel.type'; 11 | import { UserType } from './user.type'; 12 | import { Connection } from './pagination.type'; 13 | import { accessibleBy } from '@casl/mongoose'; 14 | 15 | /** GraphQL notification type definition */ 16 | export const NotificationType = new GraphQLObjectType({ 17 | name: 'Notification', 18 | fields: () => ({ 19 | id: { 20 | type: GraphQLID, 21 | resolve(parent) { 22 | return parent._id; 23 | }, 24 | }, 25 | action: { type: GraphQLString }, 26 | content: { type: GraphQLJSON }, 27 | createdAt: { type: GraphQLString }, 28 | channel: { 29 | type: ChannelType, 30 | async resolve(parent) { 31 | const channel = await Channel.findById(parent.channel); 32 | return channel; 33 | }, 34 | }, 35 | seenBy: { 36 | type: new GraphQLList(UserType), 37 | async resolve(parent, args, context) { 38 | const ability: AppAbility = context?.user.ability; 39 | const users = await User.find(accessibleBy(ability, 'read').User) 40 | .where('_id') 41 | .in(parent.seenBy); 42 | return users; 43 | }, 44 | }, 45 | }), 46 | }); 47 | 48 | /** GraphQL notification connection type definition */ 49 | export const NotificationConnectionType = Connection(NotificationType); 50 | -------------------------------------------------------------------------------- /src/schema/types/permission.type.ts: -------------------------------------------------------------------------------- 1 | import { 2 | GraphQLObjectType, 3 | GraphQLID, 4 | GraphQLString, 5 | GraphQLBoolean, 6 | } from 'graphql'; 7 | 8 | /** GraphQL permission type definition */ 9 | export const PermissionType = new GraphQLObjectType({ 10 | name: 'Permission', 11 | fields: () => ({ 12 | id: { type: GraphQLID }, 13 | type: { type: GraphQLString }, 14 | global: { type: GraphQLBoolean }, 15 | }), 16 | }); 17 | -------------------------------------------------------------------------------- /src/schema/types/positionAttribute.type.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLInt, GraphQLObjectType, GraphQLString } from 'graphql'; 2 | import { PositionAttributeCategory, User } from '@models'; 3 | import { PositionAttributeCategoryType } from './positionAttributeCategory.type'; 4 | 5 | /** GraphQL position attribute type definition */ 6 | export const PositionAttributeType = new GraphQLObjectType({ 7 | name: 'PositionAttribute', 8 | fields: () => ({ 9 | value: { type: GraphQLString }, 10 | category: { 11 | type: PositionAttributeCategoryType, 12 | async resolve(parent) { 13 | const category = await PositionAttributeCategory.findById( 14 | parent.category 15 | ); 16 | return category; 17 | }, 18 | }, 19 | usersCount: { 20 | type: GraphQLInt, 21 | async resolve(parent) { 22 | const count = await User.find({ 23 | positionAttributes: { 24 | $elemMatch: { value: parent.value, category: parent.category }, 25 | }, 26 | }).count(); 27 | return count; 28 | }, 29 | }, 30 | }), 31 | }); 32 | -------------------------------------------------------------------------------- /src/schema/types/positionAttributeCategory.type.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLObjectType, GraphQLID, GraphQLString } from 'graphql'; 2 | import { AppAbility } from '@security/defineUserAbility'; 3 | import { Application } from '@models'; 4 | import { ApplicationType } from './application.type'; 5 | import { accessibleBy } from '@casl/mongoose'; 6 | 7 | /** GraphQL position attribute category type definition */ 8 | export const PositionAttributeCategoryType = new GraphQLObjectType({ 9 | name: 'PositionAttributeCategory', 10 | fields: () => ({ 11 | id: { type: GraphQLID }, 12 | title: { type: GraphQLString }, 13 | application: { 14 | type: ApplicationType, 15 | resolve(parent, args, context) { 16 | const ability: AppAbility = context.user.ability; 17 | const application = Application.findOne({ 18 | _id: parent.application, 19 | ...accessibleBy(ability, 'read').Application, 20 | }); 21 | return application; 22 | }, 23 | }, 24 | }), 25 | }); 26 | -------------------------------------------------------------------------------- /src/schema/types/subscription.type.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLObjectType, GraphQLString } from 'graphql'; 2 | import { Channel, Form } from '@models'; 3 | import { ChannelType } from './channel.type'; 4 | import { FormType } from './form.type'; 5 | import { AppAbility } from '@security/defineUserAbility'; 6 | import { accessibleBy } from '@casl/mongoose'; 7 | 8 | /** GraphQL SubscriptionT type definition */ 9 | export const SubscriptionType = new GraphQLObjectType({ 10 | name: 'ApplicationSubscription', 11 | fields: () => ({ 12 | routingKey: { type: GraphQLString }, 13 | title: { type: GraphQLString }, 14 | convertTo: { 15 | type: FormType, 16 | async resolve(parent, args, context) { 17 | const ability: AppAbility = context.user.ability; 18 | const form = await Form.findOne({ 19 | _id: parent.convertTo, 20 | ...accessibleBy(ability, 'read').Form, 21 | }); 22 | return form; 23 | }, 24 | }, 25 | channel: { 26 | type: ChannelType, 27 | async resolve(parent, args, context) { 28 | const ability: AppAbility = context.user.ability; 29 | const channel = await Channel.findOne({ 30 | _id: parent.channel, 31 | ...accessibleBy(ability, 'read').Channel, 32 | }); 33 | return channel; 34 | }, 35 | }, 36 | }), 37 | }); 38 | -------------------------------------------------------------------------------- /src/schema/types/template.type.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLID, GraphQLObjectType, GraphQLString } from 'graphql'; 2 | import GraphQLJSON from 'graphql-type-json'; 3 | 4 | /** 5 | * GraphQL Template type. 6 | */ 7 | export const TemplateType = new GraphQLObjectType({ 8 | name: 'Template', 9 | fields: () => ({ 10 | id: { 11 | type: GraphQLID, 12 | resolve(parent) { 13 | return parent._id ? parent._id : parent.id; 14 | }, 15 | }, 16 | name: { type: GraphQLString }, 17 | type: { type: GraphQLString }, 18 | content: { type: GraphQLJSON }, 19 | }), 20 | }); 21 | -------------------------------------------------------------------------------- /src/schema/types/version.type.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLObjectType, GraphQLID, GraphQLString } from 'graphql'; 2 | import { AppAbility } from '@security/defineUserAbility'; 3 | import GraphQLJSON from 'graphql-type-json'; 4 | import { UserType } from '../types'; 5 | import { User } from '@models'; 6 | import { accessibleBy } from '@casl/mongoose'; 7 | 8 | /** GraphQL Version type definition */ 9 | export const VersionType = new GraphQLObjectType({ 10 | name: 'Version', 11 | fields: () => ({ 12 | id: { 13 | type: GraphQLID, 14 | resolve(parent) { 15 | return parent._id; 16 | }, 17 | }, 18 | createdAt: { type: GraphQLString }, 19 | data: { type: GraphQLJSON }, 20 | createdBy: { 21 | type: UserType, 22 | async resolve(parent, args, context) { 23 | const ability: AppAbility = context.user.ability; 24 | const user = await User.findOne({ 25 | _id: parent.createdBy, 26 | ...accessibleBy(ability, 'read').User, 27 | }); 28 | return user; 29 | }, 30 | }, 31 | }), 32 | }); 33 | -------------------------------------------------------------------------------- /src/server/apollo/onConnect.ts: -------------------------------------------------------------------------------- 1 | import i18next from 'i18next'; 2 | import { graphqlMiddleware } from '../middlewares'; 3 | import { GraphQLError } from 'graphql'; 4 | import { Context } from 'graphql-ws'; 5 | import { IncomingMessage } from 'http'; 6 | 7 | /** 8 | * Gets the middleware function promise 9 | * 10 | * @param ctx Context object 11 | * @returns middleware function promise 12 | */ 13 | export default (ctx: Context) => { 14 | const { connectionParams } = ctx; 15 | // Check if request is present in the extra object 16 | if ( 17 | connectionParams.authToken && 18 | typeof ctx.extra === 'object' && 19 | 'request' in ctx.extra && 20 | ctx.extra.request instanceof IncomingMessage 21 | ) { 22 | const request = ctx.extra.request; 23 | request.headers.authorization = `Bearer ${connectionParams.authToken}`; 24 | return new Promise((res) => { 25 | graphqlMiddleware(request, {} as any, () => { 26 | res(request); 27 | }); 28 | }); 29 | } 30 | 31 | throw new GraphQLError( 32 | i18next.t('common.errors.authenticationTokenNotFound'), 33 | { 34 | extensions: { 35 | code: 'UNAUTHENTICATED', 36 | }, 37 | } 38 | ); 39 | }; 40 | -------------------------------------------------------------------------------- /src/server/common-services.ts: -------------------------------------------------------------------------------- 1 | import Axios from 'axios'; 2 | import { 3 | AxiosCacheInstance, 4 | setupCache, 5 | buildKeyGenerator, 6 | } from 'axios-cache-interceptor'; 7 | 8 | let axios: AxiosCacheInstance; 9 | 10 | /** 11 | * Create a dedicated axios instance for Common Services 12 | * Add cache mechanism 13 | * Cached requests expire after 5 minutes. 14 | * 15 | * @returns Common Services axios instance 16 | */ 17 | export default () => { 18 | if (!axios) { 19 | const instance = Axios.create(); 20 | axios = setupCache(instance, { 21 | methods: ['get', 'post'], 22 | // Avoid using cache control set to false 23 | interpretHeader: false, 24 | // Generate an unique key, based on all parameters listed 25 | generateKey: buildKeyGenerator((request) => ({ 26 | method: request.method, 27 | baseURL: request.baseURL, 28 | params: request.params, 29 | url: request.url, 30 | data: request.data, 31 | custom: request.headers.Authorization, 32 | })), 33 | }); 34 | } 35 | return axios; 36 | }; 37 | -------------------------------------------------------------------------------- /src/server/middlewares/cors.ts: -------------------------------------------------------------------------------- 1 | import cors from 'cors'; 2 | import i18next from 'i18next'; 3 | import config from 'config'; 4 | 5 | /** 6 | * Must be stored in config file, as an array of strings 7 | */ 8 | const allowedOrigins: string[] = config.get('server.allowedOrigins'); 9 | 10 | /** The cors middleware */ 11 | export const corsMiddleware = cors({ 12 | origin: (origin, callback) => { 13 | if (!origin) return callback(null, true); 14 | if (allowedOrigins.indexOf(origin) === -1) { 15 | const msg = i18next.t('server.middlewares.cors.errors.invalidCORS'); 16 | return callback(new Error(`${msg}: ${origin}`), false); 17 | } 18 | return callback(null, true); 19 | }, 20 | }); 21 | -------------------------------------------------------------------------------- /src/server/middlewares/graphql.ts: -------------------------------------------------------------------------------- 1 | import passport from 'passport'; 2 | import defineUserAbility from '@security/defineUserAbility'; 3 | import { AuthenticationType } from '../../oort.config'; 4 | import config from 'config'; 5 | 6 | /** Authentication strategy */ 7 | const strategy = 8 | config.get('auth.provider') === AuthenticationType.azureAD 9 | ? 'oauth-bearer' 10 | : 'keycloak'; 11 | 12 | /** 13 | * Defines the user's abilities in the request user object 14 | * 15 | * @param req HTTP request 16 | * @param res HTTP response 17 | * @param next Callback argument to the middleware function 18 | */ 19 | export const graphqlMiddleware = (req, res, next) => { 20 | passport.authenticate(strategy, { session: true }, (err, user) => { 21 | if (user) { 22 | req.user = user; 23 | // Define the rights of the user 24 | req.user.ability = defineUserAbility(user); 25 | // req.user.isAdmin = user.roles 26 | // ? user.roles.some((x) => !x.application) 27 | // : false; 28 | } 29 | next(); 30 | })(req, res, next); 31 | }; 32 | -------------------------------------------------------------------------------- /src/server/middlewares/index.ts: -------------------------------------------------------------------------------- 1 | export * from './auth'; 2 | export * from './cors'; 3 | export * from './graphql'; 4 | export * from './rest'; 5 | export * from './rateLimit'; 6 | -------------------------------------------------------------------------------- /src/server/middlewares/rateLimit.ts: -------------------------------------------------------------------------------- 1 | import rateLimit from 'express-rate-limit'; 2 | import config from 'config'; 3 | 4 | /** 5 | * Rate limit middleware. Prevent a IP to call too many times the API. 6 | */ 7 | export const rateLimitMiddleware = rateLimit({ 8 | windowMs: config.get('server.rateLimit.windowMs'), 9 | max: config.get('server.rateLimit.max'), 10 | }); 11 | -------------------------------------------------------------------------------- /src/server/middlewares/rest.ts: -------------------------------------------------------------------------------- 1 | import passport from 'passport'; 2 | import defineUserAbility from '@security/defineUserAbility'; 3 | import { AuthenticationType } from '../../oort.config'; 4 | import i18next from 'i18next'; 5 | import config from 'config'; 6 | 7 | /** Authentication strategy */ 8 | const strategy = 9 | config.get('auth.provider') === AuthenticationType.azureAD 10 | ? 'oauth-bearer' 11 | : 'keycloak'; 12 | 13 | /** 14 | * Defines the user's abilities in the request context 15 | * 16 | * @param req HTTP request 17 | * @param res HTTP response 18 | * @param next Callback argument to the middleware function 19 | */ 20 | export const restMiddleware = (req, res, next) => { 21 | passport.authenticate(strategy, { session: true }, (err, user) => { 22 | if (user) { 23 | req.context = { user }; 24 | // req.context.user = user; 25 | // Define the rights of the user 26 | req.context.user.ability = defineUserAbility(user); 27 | next(); 28 | } else { 29 | res.status(401).send(i18next.t('common.errors.userNotLogged')); 30 | } 31 | })(req, res, next); 32 | }; 33 | -------------------------------------------------------------------------------- /src/server/pubsub.ts: -------------------------------------------------------------------------------- 1 | import { logger } from '@services/logger.service'; 2 | import config from 'config'; 3 | import { RedisPubSub } from 'graphql-redis-subscriptions'; 4 | import Redis from 'ioredis'; 5 | 6 | let pubsub: RedisPubSub; 7 | 8 | /** 9 | * Get a redis client. 10 | * 11 | * @returns Redis client. 12 | */ 13 | const getClient = () => { 14 | const client = new Redis(config.get('redis.url'), { 15 | password: config.get('redis.password'), 16 | showFriendlyErrorStack: true, 17 | lazyConnect: true, 18 | maxRetriesPerRequest: 5, 19 | }); 20 | client.on('connect', () => { 21 | logger.info('Connected to redis instance'); 22 | }); 23 | client.on('ready', function () { 24 | logger.info('Redis instance is ready'); 25 | }); 26 | client.on('error', function (e) { 27 | logger.error(`Error connecting to redis: "${e}"`); 28 | }); 29 | client.on('disconnect', () => { 30 | logger.info('Disconnected from redis instance'); 31 | }); 32 | return client; 33 | }; 34 | 35 | /** 36 | * GraphQL exchanges 37 | * 38 | * @returns a Redis publish/subscribe instance 39 | */ 40 | export default async () => { 41 | if (!pubsub) { 42 | pubsub = new RedisPubSub({ 43 | publisher: getClient(), 44 | subscriber: getClient(), 45 | }); 46 | } 47 | return pubsub; 48 | }; 49 | -------------------------------------------------------------------------------- /src/server/pubsubSafe.ts: -------------------------------------------------------------------------------- 1 | import { logger } from '@services/logger.service'; 2 | import config from 'config'; 3 | import { RedisPubSub } from 'graphql-redis-subscriptions'; 4 | import Redis from 'ioredis'; 5 | 6 | let pubsub: RedisPubSub; 7 | 8 | /** 9 | * Get a redis client. 10 | * 11 | * @returns Redis client. 12 | */ 13 | const getClient = () => { 14 | const client = new Redis(config.get('redis.url'), { 15 | password: config.get('redis.password'), 16 | showFriendlyErrorStack: true, 17 | lazyConnect: true, 18 | maxRetriesPerRequest: 5, 19 | }); 20 | client.on('connect', () => { 21 | logger.info('Connected to redis instance'); 22 | }); 23 | client.on('ready', function () { 24 | logger.info('Redis instance is ready'); 25 | }); 26 | client.on('error', function (e) { 27 | logger.error(`Error connecting to redis: "${e}"`); 28 | }); 29 | client.on('disconnect', () => { 30 | logger.info('Disconnected from redis instance'); 31 | }); 32 | return client; 33 | }; 34 | 35 | /** 36 | * GraphQL exchanges 37 | * 38 | * @returns a Redis publish/subscribe instance 39 | */ 40 | export default async () => { 41 | if (!pubsub) { 42 | pubsub = new RedisPubSub({ 43 | publisher: getClient(), 44 | subscriber: getClient(), 45 | }); 46 | } 47 | return pubsub; 48 | }; 49 | -------------------------------------------------------------------------------- /src/server/redis.ts: -------------------------------------------------------------------------------- 1 | import { RedisClientType, createClient } from 'redis'; 2 | import { logger } from '@services/logger.service'; 3 | import config from 'config'; 4 | 5 | let client: RedisClientType; 6 | 7 | /** 8 | * Get a redis client. 9 | * 10 | * @returns a Redis client instance. 11 | */ 12 | export default async () => { 13 | if (!client && config.get('redis.url')) { 14 | client = createClient({ 15 | url: config.get('redis.url'), 16 | password: config.get('redis.password'), 17 | }); 18 | 19 | client.on('error', (error) => { 20 | logger.error(`REDIS: ${error}`); 21 | }); 22 | client.on('disconnect', (error) => { 23 | logger.info(`REDIS: ${error}`); 24 | client = null; 25 | }); 26 | 27 | await client.connect(); 28 | } 29 | 30 | return client; 31 | }; 32 | -------------------------------------------------------------------------------- /src/setup/init.ts: -------------------------------------------------------------------------------- 1 | import mongoose from 'mongoose'; 2 | import { initDatabase, startDatabase } from '../server/database'; 3 | import { logger } from '../services/logger.service'; 4 | 5 | // Start database and init, uploading default records 6 | startDatabase(); 7 | mongoose.connection.once('open', async () => { 8 | await initDatabase(); 9 | await mongoose.connection.close(); 10 | logger.info('connection closed'); 11 | }); 12 | -------------------------------------------------------------------------------- /src/types/index.ts: -------------------------------------------------------------------------------- 1 | export * from './permission'; 2 | export * from './filter'; 3 | -------------------------------------------------------------------------------- /src/types/permission.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Define the enum for ResourcePermission 3 | */ 4 | export enum resourcePermission { 5 | CREATE_RECORDS = 'canCreateRecords', 6 | SEE_RECORDS = 'canSeeRecords', 7 | UPDATE_RECORDS = 'canUpdateRecords', 8 | DELETE_RECORDS = 'canDeleteRecords', 9 | DOWNLOAD_RECORDS = 'canDownloadRecords', 10 | } 11 | -------------------------------------------------------------------------------- /src/types/route-definition.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response, NextFunction, RequestHandler } from 'express'; 2 | 3 | /** 4 | * Route definition interface. 5 | */ 6 | export interface RouteDefinition { 7 | path: string; 8 | method: 'get' | 'post' | 'put' | 'patch' | 'delete'; 9 | handler: (req: Request, res: Response, next: NextFunction) => void; 10 | middlewares?: RequestHandler[]; 11 | } 12 | -------------------------------------------------------------------------------- /src/utils/context/getNewDashboardName.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ApiConfiguration, 3 | Dashboard, 4 | Page, 5 | Record, 6 | ReferenceData, 7 | } from '@models'; 8 | import { CustomAPI } from '@server/apollo/dataSources'; 9 | import { get } from 'lodash'; 10 | import { Types } from 'mongoose'; 11 | 12 | /** 13 | * Get the name of the new dashboard, based on the context. 14 | * 15 | * @param dashboard The dashboard being duplicated 16 | * @param context The context of the dashboard 17 | * @param id The id of the record or element 18 | * @param dataSources The data sources 19 | * @returns The name of the new dashboard 20 | */ 21 | export const getNewDashboardName = async ( 22 | dashboard: Dashboard, 23 | context: Page['context'], 24 | id: string | Types.ObjectId, 25 | dataSources: any 26 | ) => { 27 | if ('refData' in context && context.refData) { 28 | // Get items from reference data 29 | const referenceData = await ReferenceData.findById(context.refData); 30 | const apiConfiguration = await ApiConfiguration.findById( 31 | referenceData.apiConfiguration 32 | ); 33 | const data = apiConfiguration 34 | ? await ( 35 | dataSources[apiConfiguration.name] as CustomAPI 36 | ).getReferenceDataItems(referenceData, apiConfiguration) 37 | : referenceData.data; 38 | 39 | const item = data.find((x) => get(x, referenceData.valueField) == id); 40 | return get(item, context.displayField); 41 | } else if ('resource' in context && context.resource) { 42 | const record = await Record.findById(id); 43 | return `${record.data[context.displayField]}`; 44 | } 45 | 46 | // Default return, should never happen 47 | return dashboard.name; 48 | }; 49 | -------------------------------------------------------------------------------- /src/utils/date/getTimeWithTimezone.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Create a string displaying the time of passed date in the following format: 3 | * HH:MM UTC+N with N standing for the offset of locale timezone with UTC. 4 | * 5 | * @param date Input date. 6 | * @param locale Locale needed to compute the correct time format. 7 | * @returns Time string. 8 | */ 9 | const getTimeWithTimezone = (date: Date, locale: string): string => { 10 | const offset = -date.getTimezoneOffset() / 60; 11 | const operator = offset > 0 ? '+' : '-'; 12 | return `${date.toLocaleTimeString(locale)} UTC${ 13 | offset ? operator : '' 14 | }${Math.abs(offset)}`; 15 | }; 16 | 17 | export default getTimeWithTimezone; 18 | -------------------------------------------------------------------------------- /src/utils/email/index.ts: -------------------------------------------------------------------------------- 1 | export * from './gridEmailBuilder'; 2 | export * from './sendEmail'; 3 | -------------------------------------------------------------------------------- /src/utils/files/csvBuilder.ts: -------------------------------------------------------------------------------- 1 | import { Parser } from 'json2csv'; 2 | import get from 'lodash/get'; 3 | 4 | /** 5 | * Builds a CSV file. 6 | * 7 | * @param columns Array of objects with a name property that will match the data, and optionally a label that will be the column title on the exported file 8 | * @param data Array of objects, that will be transformed into the rows of the csv. Each object should have [key, value] as [column's name, corresponding value]. 9 | * @returns response with file attached. 10 | */ 11 | export default (columns: any[], data) => { 12 | // Create a string array with the columns' labels or names as fallback, then construct the parser from it 13 | const fields = columns.flatMap((x) => ({ label: x.title, value: x.name })); 14 | const json2csv = new Parser({ fields }); 15 | 16 | const tempCsv = []; 17 | 18 | // Build an object for each row, and push it in an array 19 | for (const row of data) { 20 | const temp = {}; 21 | for (const field of columns) { 22 | if (field.subColumns) { 23 | temp[field.name] = get(row, field.name, []).length; 24 | } else { 25 | temp[field.name] = get(row, field.name, null); 26 | } 27 | } 28 | tempCsv.push(temp); 29 | } 30 | // Generate the file by parsing the data, set the response parameters and send it 31 | const csv = json2csv.parse(tempCsv); 32 | return csv; 33 | }; 34 | -------------------------------------------------------------------------------- /src/utils/files/deleteFolder.ts: -------------------------------------------------------------------------------- 1 | import { BlobServiceClient } from '@azure/storage-blob'; 2 | import { logger } from '@services/logger.service'; 3 | import config from 'config'; 4 | 5 | /** Azure storage connection string */ 6 | const AZURE_STORAGE_CONNECTION_STRING: string = config.get( 7 | 'blobStorage.connectionString' 8 | ); 9 | 10 | /** 11 | * Delete blob folder in azure blob storage 12 | * 13 | * @param containerName container name 14 | * @param folder folder name 15 | * @returns folder delete as promise 16 | */ 17 | export const deleteFolder = async ( 18 | containerName: string, 19 | folder: string 20 | ): Promise => { 21 | const blobServiceClient = BlobServiceClient.fromConnectionString( 22 | AZURE_STORAGE_CONNECTION_STRING 23 | ); 24 | const containerClient = blobServiceClient.getContainerClient(containerName); 25 | const promises: Promise[] = []; 26 | const blobNames: string[] = []; 27 | for await (const blob of containerClient.listBlobsFlat({ prefix: folder })) { 28 | blobNames.push(blob.name); 29 | //promises.push(containerClient.deleteBlob(blob.name)); 30 | } 31 | for (const blobName of new Set(blobNames)) { 32 | const blockBlobClient = containerClient.getBlockBlobClient(blobName); 33 | promises.push( 34 | blockBlobClient.exists().then((value) => { 35 | if (value) { 36 | containerClient 37 | .deleteBlob(blobName) 38 | .then(() => logger.info(`File ${blobName} successfully removed.`)); 39 | } else { 40 | logger.info(`File ${blobName} does not exist.`); 41 | } 42 | }) 43 | ); 44 | } 45 | return Promise.all(promises).catch((err) => { 46 | throw new Error(err.message); 47 | }); 48 | }; 49 | -------------------------------------------------------------------------------- /src/utils/files/format.helper.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Remove special characters of name that are not allowed 3 | * 4 | * @param name name to process 5 | * @returns name without restricted character 6 | */ 7 | export const formatFilename = (name: string): string => { 8 | const regex = /[:*?\\/[\]]/g; 9 | return name.replace(regex, ''); 10 | }; 11 | -------------------------------------------------------------------------------- /src/utils/files/index.ts: -------------------------------------------------------------------------------- 1 | export * from './fileBuilder'; 2 | export * from './uploadFile'; 3 | export * from './downloadFile'; 4 | export * from './templateBuilder'; 5 | export * from './getColumns'; 6 | export * from './getUploadColumns'; 7 | export * from './getRows'; 8 | export * from './loadRow'; 9 | export * from './getColumnsFromMeta'; 10 | export * from './getRowsFromMeta'; 11 | export * from './extractGridData'; 12 | -------------------------------------------------------------------------------- /src/utils/filter/getDateForMongo.ts: -------------------------------------------------------------------------------- 1 | import { 2 | extractStringFromBrackets, 3 | REGEX_TODAY_PLUS, 4 | REGEX_TODAY_MINUS, 5 | isUsingTodayPlaceholder, 6 | isUsingNowPlaceholder, 7 | } from '../../const/placeholders'; 8 | 9 | /** 10 | * Gets from input date value the three dates used for filtering. 11 | * 12 | * @param value input date value 13 | * @returns calculated day, beginning of day, and ending of day 14 | */ 15 | export const getDateForMongo = ( 16 | value: any 17 | ): { startDate: Date; endDate: Date } => { 18 | // today's date 19 | let startDate: Date; 20 | if (isUsingTodayPlaceholder(value)) { 21 | // Using {{today}} 22 | startDate = new Date(); 23 | startDate.setHours(0, 0, 0, 0); 24 | // today + number of days 25 | if (REGEX_TODAY_PLUS.test(value)) { 26 | const difference = parseInt( 27 | extractStringFromBrackets(value).split('+')[1] 28 | ); 29 | startDate.setDate(startDate.getDate() + difference); 30 | // today - number of days 31 | } else if (REGEX_TODAY_MINUS.test(value)) { 32 | const difference = -parseInt( 33 | extractStringFromBrackets(value).split('-')[1] 34 | ); 35 | startDate.setDate(startDate.getDate() + difference); 36 | } 37 | } else if (isUsingNowPlaceholder(value)) { 38 | // Using {{now}} 39 | startDate = new Date(); 40 | } else { 41 | // Other dates 42 | startDate = new Date(value); 43 | } 44 | const endDate = new Date(startDate); 45 | // Should set endDate to the same day than startDate, at 23:59:59:999 46 | endDate.setDate(startDate.getDate() + 1); 47 | endDate.setMilliseconds(-1); 48 | return { startDate, endDate }; 49 | }; 50 | -------------------------------------------------------------------------------- /src/utils/filter/getFormPermissionFilter.ts: -------------------------------------------------------------------------------- 1 | import mongoose from 'mongoose'; 2 | import { Form, Resource, User } from '@models'; 3 | import getFilter from '../schema/resolvers/Query/getFilter'; 4 | 5 | /** 6 | * Creates a Mongo filter for a specific permission on a form, for an user. 7 | * 8 | * @param user user to get permission for 9 | * @param object object (form / resource) to get permission on 10 | * @param permission name of the permission to get filter for 11 | * @returns Mongo permission filter. 12 | */ 13 | export const getFormPermissionFilter = ( 14 | user: User, 15 | object: Form | Resource, 16 | permission: string 17 | ): any[] => { 18 | const roles = user.roles.map((x) => new mongoose.Types.ObjectId(x._id)); 19 | const permissionFilters = []; 20 | const permissionArray = object.permissions[permission]; 21 | if (permissionArray && permissionArray.length) { 22 | permissionArray.forEach((x) => { 23 | if (!x.role || roles.some((role) => role.equals(x.role))) { 24 | const filter = {}; 25 | Object.assign( 26 | filter, 27 | x.access && 28 | getFilter(x.access, object.fields, { 29 | resourceFieldsById: { 30 | [object instanceof Form ? object.resource : object.id]: 31 | object.fields, 32 | }, 33 | }) 34 | ); 35 | permissionFilters.push(filter); 36 | } 37 | }); 38 | } 39 | return permissionFilters; 40 | }; 41 | -------------------------------------------------------------------------------- /src/utils/filter/getTimeForMongo.ts: -------------------------------------------------------------------------------- 1 | import { Placeholder } from '../../const/placeholders'; 2 | 3 | /** 4 | * Gets from input time value a time value display. 5 | * 6 | * @param value record value 7 | * @returns calculated time 8 | */ 9 | export const getTimeForMongo = (value: any): Date => { 10 | if (value === Placeholder.NOW) { 11 | return new Date( 12 | new Date().toLocaleString('en-US', { 13 | timeZone: 'Europe/Berlin', 14 | }) 15 | ); 16 | } else if (value?.match(/^\d\d:\d\d$/)) { 17 | const hours = value.slice(0, 2); 18 | const minutes = value.slice(3); 19 | return new Date(Date.UTC(1970, 0, 1, hours, minutes)); 20 | } else { 21 | return new Date(value); 22 | } 23 | }; 24 | -------------------------------------------------------------------------------- /src/utils/filter/index.ts: -------------------------------------------------------------------------------- 1 | export * from './getFormPermissionFilter'; 2 | export * from './getFormFilter'; 3 | -------------------------------------------------------------------------------- /src/utils/form/checkDefaultFields.ts: -------------------------------------------------------------------------------- 1 | import { defaultRecordFieldsFlat } from '@const/defaultRecordFields'; 2 | import { GraphQLError } from 'graphql'; 3 | import i18next from 'i18next'; 4 | 5 | /** 6 | * Check if any default field is used 7 | * 8 | * @param fields list of fields 9 | */ 10 | export const checkDefaultFields = (fields: any[]): void => { 11 | const defaultFields = defaultRecordFieldsFlat; 12 | for (const field of fields) { 13 | if (defaultFields.includes(field.name)) { 14 | throw new GraphQLError( 15 | i18next.t('mutations.form.edit.errors.defaultFieldDuplicated', { 16 | name: field.name, 17 | }) 18 | ); 19 | } 20 | } 21 | }; 22 | 23 | export default checkDefaultFields; 24 | -------------------------------------------------------------------------------- /src/utils/form/checkRecordExpressions.ts: -------------------------------------------------------------------------------- 1 | import { Form, Record } from '@models'; 2 | import * as Survey from 'survey-knockout'; 3 | 4 | /** 5 | * Force the evaluation of record expressions 6 | * 7 | * @param form Current form 8 | * @param record edited record 9 | * @returns Updated record data 10 | */ 11 | export const checkRecordExpressions = (form: Form, record: Record): Record => { 12 | // Instantiate survey from form 13 | const survey = new Survey.Model(form.structure); 14 | 15 | // Setting this variable forces the expressions to evaluate. It has not other purpose. 16 | survey.setVariable('__forcingExpressionsEvaluation', 1); 17 | 18 | // Fill the survey data with current record data to allow expressions to fetch values of other questions. 19 | survey.data = { ...survey.data, ...record.data }; 20 | 21 | record.data = survey.data; 22 | return record; 23 | }; 24 | 25 | export default checkRecordExpressions; 26 | -------------------------------------------------------------------------------- /src/utils/form/checkRecordTriggers.ts: -------------------------------------------------------------------------------- 1 | import { Form, Record } from '@models'; 2 | import * as Survey from 'survey-knockout'; 3 | 4 | /** 5 | * Check record triggered values, so inline edition ( draft edition ) can indicate changes on the data 6 | * 7 | * @param record edited record 8 | * @param newData record updated data 9 | * @param form template to use 10 | * @param context graphQLContext 11 | * @returns New record data 12 | */ 13 | export const checkRecordTriggers = ( 14 | record: Record, 15 | newData: any, 16 | form: Form, 17 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 18 | context 19 | ): Record => { 20 | // Necessary to fix 401 errors if we have choicesByUrl targeting self API. 21 | // passTokenForChoicesByUrl(context); 22 | // Avoid the choices by url to be called, as it could freeze system depending on the choices 23 | (Survey.ChoicesRestful as any).getCachedItemsResult = () => true; 24 | const survey = new Survey.Model(form.structure); 25 | Survey.settings.commentPrefix = '_comment'; 26 | survey.data = { ...record.data, ...newData }; 27 | const triggers = survey.toJSON().triggers; 28 | if (triggers) { 29 | survey.runTriggers(); 30 | } 31 | const updatedRecord = new Record(record); 32 | updatedRecord.data = survey.data; 33 | return updatedRecord; 34 | }; 35 | -------------------------------------------------------------------------------- /src/utils/form/findDuplicateFields.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLError } from 'graphql/error'; 2 | import i18next from 'i18next'; 3 | 4 | /** 5 | * Checks duplication of name in fields array. 6 | * Throw duplication error if duplication exists. 7 | * 8 | * @param fields Question fields array 9 | */ 10 | export const findDuplicateFields = (fields): void => { 11 | const names = fields.map((x) => x.name); 12 | const duplication = names.filter( 13 | (item, index) => names.indexOf(item) !== index 14 | ); 15 | if (duplication.length > 0) { 16 | throw new GraphQLError( 17 | i18next.t('utils.form.findDuplicateFields.errors.dataFieldDuplicated', { 18 | name: duplication[0], 19 | }) 20 | ); 21 | } 22 | }; 23 | -------------------------------------------------------------------------------- /src/utils/form/getNextQuestion.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Gets the next question, from a question name. 3 | * 4 | * @param structure parent structure. 5 | * @param name question name. 6 | * @returns next question if exists. 7 | */ 8 | export const getNextQuestion = (structure: any, name: string): any => { 9 | if (structure.pages) { 10 | for (const page of structure.pages) { 11 | const question = getNextQuestion(page, name); 12 | if (question) return question; 13 | } 14 | } else if (structure.elements) { 15 | for (const elementIndex in structure.elements) { 16 | const element = structure.elements[elementIndex]; 17 | if (element.type === 'panel') { 18 | if (element.name === name) return element; 19 | const question = getNextQuestion(element, name); 20 | if (question) return question; 21 | } else { 22 | if (element.valueName === name) { 23 | // Return previous question 24 | if (Number(elementIndex) + 1 < structure.elements.length) { 25 | return structure.elements[Number(elementIndex) + 1]; 26 | } else { 27 | return null; 28 | } 29 | } 30 | } 31 | } 32 | } 33 | }; 34 | -------------------------------------------------------------------------------- /src/utils/form/getOwnership.ts: -------------------------------------------------------------------------------- 1 | /** Name of the owner field type */ 2 | const OWNER_FIELD_TYPE = 'owner'; 3 | 4 | /** The owner interface definition */ 5 | interface Owner { 6 | roles?: string[]; 7 | positionAttributes?: { value: string; category: string }[]; 8 | } 9 | 10 | /** 11 | * Check if the parent form has an owner field, check if this field is filled. Then fill createdBy properties using this field. 12 | * 13 | * @param fields fields of the parent form 14 | * @param data data passed to edit the record 15 | * @returns Roles that own the record, if any. 16 | */ 17 | export const getOwnership = (fields: any, data: any): Owner => { 18 | const ownership: Owner = {}; 19 | const ownerField = fields.find((x) => x.type === OWNER_FIELD_TYPE); 20 | if (ownerField && data[ownerField.name]) { 21 | ownership.roles = data[ownerField.name]; 22 | return ownership; 23 | } else { 24 | return null; 25 | } 26 | }; 27 | -------------------------------------------------------------------------------- /src/utils/form/getParentQuestion.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ReliefApplications/ems-backend/f254005f7f567899861bfa3b10bf31c4f5e1e3a6/src/utils/form/getParentQuestion.ts -------------------------------------------------------------------------------- /src/utils/form/getPreviousQuestion.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Gets the previous question, from a question name. 3 | * 4 | * @param structure parent structure. 5 | * @param name question name. 6 | * @returns Previous question if exists. 7 | */ 8 | export const getPreviousQuestion = (structure: any, name: string): any => { 9 | if (structure.pages) { 10 | for (const page of structure.pages) { 11 | const question = getPreviousQuestion(page, name); 12 | if (question) return question; 13 | } 14 | } else if (structure.elements) { 15 | for (const elementIndex in structure.elements) { 16 | const element = structure.elements[elementIndex]; 17 | if (element.type === 'panel') { 18 | if (element.name === name) return element; 19 | const question = getPreviousQuestion(element, name); 20 | if (question) return question; 21 | } else { 22 | if (element.valueName === name) { 23 | // Return previous question 24 | if (Number(elementIndex) - 1 >= 0) { 25 | return structure.elements[Number(elementIndex) - 1]; 26 | } else { 27 | return null; 28 | } 29 | } 30 | } 31 | } 32 | } 33 | }; 34 | -------------------------------------------------------------------------------- /src/utils/form/getQuestion.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Based on the passed field, find the corresponfing question in a structure and return it. 3 | * Function by induction. 4 | * 5 | * @param structure structure of the form to search on 6 | * @param name name of the field to search for 7 | * @returns question definition 8 | */ 9 | export const getQuestion = (structure: any, name: string): any => { 10 | // Loop on elements to find the right question 11 | if (structure.pages) { 12 | for (const page of structure.pages) { 13 | const question = getQuestion(page, name); 14 | if (question) return question; 15 | } 16 | } else if (structure.elements) { 17 | for (const elementIndex in structure.elements) { 18 | const element = structure.elements[elementIndex]; 19 | if (element.type === 'panel') { 20 | if (element.name === name) return element; 21 | const question = getQuestion(element, name); 22 | if (question) return question; 23 | } else { 24 | if (element.valueName === name) { 25 | // Return question 26 | return element; 27 | } 28 | } 29 | } 30 | } 31 | }; 32 | -------------------------------------------------------------------------------- /src/utils/form/getQuestionPosition.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Gets the question position in a structure, from a question name. 3 | * 4 | * @param structure parent structure. 5 | * @param name question name. 6 | * @returns parent structure and index of the question in it. 7 | */ 8 | export const getQuestionPosition = (structure: any, name: string): any => { 9 | // Loop on elements to find the right question 10 | if (structure.pages) { 11 | for (const page of structure.pages) { 12 | const questionPosition = getQuestionPosition(page, name); 13 | if (questionPosition && questionPosition.parent) return questionPosition; 14 | } 15 | } else if (structure.elements) { 16 | for (const elementIndex in structure.elements) { 17 | const element = structure.elements[elementIndex]; 18 | if (element.type === 'panel') { 19 | if (element.name === name) 20 | return { parent: structure, index: Number(elementIndex) }; 21 | const questionPosition = getQuestionPosition(element, name); 22 | if (questionPosition && questionPosition.parent) 23 | return questionPosition; 24 | } else { 25 | if (element.valueName === name) { 26 | // Return question 27 | return { parent: structure, index: Number(elementIndex) }; 28 | } 29 | } 30 | } 31 | } 32 | }; 33 | -------------------------------------------------------------------------------- /src/utils/form/index.ts: -------------------------------------------------------------------------------- 1 | // === FIELDS === 2 | export * from './removeField'; 3 | export * from './addField'; 4 | export * from './replaceField'; 5 | export * from './findDuplicateFields'; 6 | export * from './extractFields'; 7 | export * from './getFieldType'; 8 | 9 | // === RECORDS === 10 | export * from './transformRecord'; 11 | export * from './getOwnership'; 12 | export * from './getNextId'; 13 | export * from './getDisplayText'; 14 | export * from './checkRecordValidation'; 15 | export * from './getAccessibleFields'; 16 | export * from './checkRecordTriggers'; 17 | export * from './checkRecordExpressions'; 18 | -------------------------------------------------------------------------------- /src/utils/form/removeField.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Remove field from structure and depending on the field name passed. 3 | * Function by induction. 4 | * 5 | * @param structure structure of the form to edit 6 | * @param name name of the field to search for 7 | * @returns {boolean} status of request. 8 | */ 9 | export const removeField = (structure: any, name: string): boolean => { 10 | // Loop on elements to find the right question 11 | if (structure.pages) { 12 | for (const page of structure.pages) { 13 | if (removeField(page, name)) return true; 14 | } 15 | } else if (structure.elements) { 16 | for (const elementIndex in structure.elements) { 17 | const element = structure.elements[elementIndex]; 18 | if (element.type === 'panel') { 19 | if (removeField(element, name)) return true; 20 | } else { 21 | if (element.valueName === name) { 22 | // Remove from structure 23 | structure.elements.splice(elementIndex, 1); 24 | return true; 25 | } 26 | } 27 | } 28 | } 29 | }; 30 | -------------------------------------------------------------------------------- /src/utils/geojson/generateGeoJson.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Get GeoJson array object size in the bytes 3 | * 4 | * @param geoJsonData is the array of object of geo json 5 | * @param sizeType is the which type in you need to return size 6 | * @returns size of GeoJson array object in bytes or KiloBytes 7 | */ 8 | export const getGeoJsonSize = (geoJsonData, sizeType) => { 9 | switch (sizeType) { 10 | case 'Bytes': 11 | return new TextEncoder().encode(JSON.stringify(geoJsonData)).length; 12 | case 'KB': //KiloBytes 13 | return ( 14 | new TextEncoder().encode(JSON.stringify(geoJsonData)).length * 0.000977 15 | ); 16 | } 17 | }; 18 | -------------------------------------------------------------------------------- /src/utils/history/index.ts: -------------------------------------------------------------------------------- 1 | export * from './recordHistory'; 2 | -------------------------------------------------------------------------------- /src/utils/proxy/index.ts: -------------------------------------------------------------------------------- 1 | export * from './authManagement'; 2 | export * from './getChoices'; 3 | -------------------------------------------------------------------------------- /src/utils/schema/buildSchema.ts: -------------------------------------------------------------------------------- 1 | import { makeExecutableSchema, mergeSchemas } from '@graphql-tools/schema'; 2 | import { getResolvers } from './resolvers'; 3 | import schema from '../../schema'; 4 | import { GraphQLSchema } from 'graphql'; 5 | import { getStructures, getReferenceDatas } from './getStructures'; 6 | import { Form } from '@models'; 7 | import { logger } from '../../services/logger.service'; 8 | import buildTypes from './buildTypes'; 9 | 10 | /** 11 | * Build a new GraphQL schema to add to the default one, providing API for the resources / forms. 12 | * 13 | * @returns GraphQL schema built from the active resources / forms of the database. 14 | */ 15 | const buildSchema = async (): Promise => { 16 | try { 17 | const structures = await getStructures(); 18 | const referenceDatas = await getReferenceDatas(); 19 | 20 | const typeDefs = await buildTypes(); 21 | 22 | const forms = (await Form.find({}).select('name resource')) as { 23 | name: string; 24 | resource?: string; 25 | }[]; 26 | 27 | const resolvers = getResolvers(structures, forms, referenceDatas); 28 | 29 | // Add resolvers to the types definition. 30 | const builtSchema = makeExecutableSchema({ 31 | typeDefs, 32 | resolvers, 33 | }); 34 | 35 | // Merge default schema and form / resource schema. 36 | const graphQLSchema = mergeSchemas({ 37 | schemas: [schema, builtSchema], 38 | }); 39 | 40 | logger.info('🔨 Schema built'); 41 | 42 | return graphQLSchema; 43 | } catch (err) { 44 | logger.error(err.message, { stack: err.stack }); 45 | return schema; 46 | } 47 | }; 48 | 49 | export default buildSchema; 50 | -------------------------------------------------------------------------------- /src/utils/schema/buildTypes.ts: -------------------------------------------------------------------------------- 1 | import { printSchema } from 'graphql'; 2 | import { getSchema } from './introspection/getSchema'; 3 | import { getReferenceDatas, getStructures } from './getStructures'; 4 | import { logger } from '../../services/logger.service'; 5 | 6 | /** 7 | * Build GraphQL types from the active resources / forms stored in the database. 8 | * 9 | * @returns The built GraphQL types 10 | */ 11 | const buildTypes = async (): Promise => { 12 | try { 13 | const structures = await getStructures(); 14 | const referenceDatas = await getReferenceDatas(); 15 | 16 | const typeDefs = printSchema(getSchema(structures, referenceDatas)); 17 | logger.info('🔨 Types generated.'); 18 | 19 | return typeDefs; 20 | } catch (err) { 21 | logger.error(err.message, { stack: err.stack }); 22 | return; 23 | } 24 | }; 25 | 26 | export default buildTypes; 27 | -------------------------------------------------------------------------------- /src/utils/schema/errors/checkPageSize.util.ts: -------------------------------------------------------------------------------- 1 | import config from 'config'; 2 | import { GraphQLError } from 'graphql'; 3 | import i18next from 'i18next'; 4 | 5 | /** 6 | * Check pagination maximum limit of data. 7 | * Throw pagination maximum limit cross than error. 8 | * 9 | * @param maxLimit Question maxLimit number 10 | */ 11 | export const checkPageSize = (maxLimit: number): void => { 12 | const maxPaginationLimit = config.get('server.pagination.limit'); 13 | if (maxLimit > maxPaginationLimit) { 14 | throw new GraphQLError( 15 | i18next.t('common.errors.maximumPaginationLimit', { 16 | paginationLimit: maxPaginationLimit, 17 | }) 18 | ); 19 | } 20 | }; 21 | 22 | export default checkPageSize; 23 | -------------------------------------------------------------------------------- /src/utils/schema/getStructures.ts: -------------------------------------------------------------------------------- 1 | import { Form, ReferenceData, Resource } from '@models'; 2 | 3 | /** Interface definition for the structure of a schema */ 4 | export interface SchemaStructure { 5 | _id: string; 6 | name: string; 7 | fields: any[]; 8 | } 9 | 10 | /** 11 | * Get id / name and fields of forms / resources in database 12 | * 13 | * @returns list of schema structures from forms / resources in database 14 | */ 15 | export const getStructures = async (): Promise => { 16 | // Get all resources 17 | const resources = (await Resource.find({}).select( 18 | 'name fields' 19 | )) as SchemaStructure[]; 20 | 21 | // Get all active forms 22 | const forms = (await Form.find({ 23 | core: { $ne: true }, 24 | status: 'active', 25 | }).select('name fields')) as SchemaStructure[]; 26 | 27 | // Get all resources and clear names 28 | const structures = resources.concat(forms); 29 | structures.forEach((x) => (x.name = Form.getGraphQLTypeName(x.name))); 30 | 31 | return structures; 32 | }; 33 | 34 | /** 35 | * Get necessary information from Reference Data. 36 | * Avoid reference data with no fields. 37 | * 38 | * @returns list of schema structures from reference data in database 39 | */ 40 | export const getReferenceDatas = async (): Promise => { 41 | const referenceDatas = await ReferenceData.find({ 42 | 'fields.0': { $exists: true }, 43 | }).populate({ 44 | path: 'apiConfiguration', 45 | model: 'ApiConfiguration', 46 | select: { name: 1, endpoint: 1, graphQLEndpoint: 1 }, 47 | }); 48 | return referenceDatas.map((x) => { 49 | x.name = ReferenceData.getGraphQLTypeName(x.name); 50 | return x; 51 | }); 52 | }; 53 | -------------------------------------------------------------------------------- /src/utils/schema/introspection/getConnectionType.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLID, GraphQLInt, GraphQLList, GraphQLObjectType } from 'graphql'; 2 | import { Edge, PageInfo } from '../../../schema/types'; 3 | 4 | /** 5 | * Gets the GraphQL connection type definition for a given element type 6 | * 7 | * @param itemType the element type 8 | * @returns GraphQL connection type definition 9 | */ 10 | export const Connection = (itemType: any) => { 11 | return new GraphQLObjectType({ 12 | name: `${itemType.name}Connection`, 13 | fields: () => ({ 14 | totalCount: { type: GraphQLInt }, 15 | edges: { type: new GraphQLList(Edge(itemType)) }, 16 | pageInfo: { type: PageInfo }, 17 | _source: { type: GraphQLID }, 18 | }), 19 | }); 20 | }; 21 | 22 | /** 23 | * Gets the name for the GraphQL connection 24 | * 25 | * @param name Entity 26 | * @returns The name for the GraphQL connection 27 | */ 28 | export const getGraphQLConnectionTypeName = (name: string) => { 29 | return name + 'Connection'; 30 | }; 31 | 32 | /** 33 | * Gets an Array of connection GraphQL types 34 | * 35 | * @param types An array of connection objects 36 | * @returns The array of GraphQL connection types 37 | */ 38 | const getConnectionTypes = (types: any[]) => { 39 | return types.map((x) => { 40 | return Connection(x); 41 | }); 42 | }; 43 | 44 | export default getConnectionTypes; 45 | -------------------------------------------------------------------------------- /src/utils/schema/introspection/getFieldName.ts: -------------------------------------------------------------------------------- 1 | import { Field } from './getFieldType'; 2 | 3 | /** Enum of possible field name extensions, making link between datasources */ 4 | // eslint-disable-next-line @typescript-eslint/naming-convention 5 | export enum NameExtension { 6 | resource = '_id', 7 | resources = '_ids', 8 | referenceData = '_ref', 9 | } 10 | 11 | /** 12 | * Get GraphQL name from field definition. 13 | * 14 | * @param field field definition. 15 | * @returns GraphQL name. 16 | */ 17 | const getFieldName = (field: Field): string => { 18 | const name = field.name.trim().split('-').join('_'); 19 | if (field.resource) { 20 | return field.type === 'resources' 21 | ? `${name}${NameExtension.resources}` 22 | : `${name}${NameExtension.resource}`; 23 | } 24 | if (field.referenceData) { 25 | return `${name}${NameExtension.referenceData}`; 26 | } 27 | return name; 28 | }; 29 | 30 | export default getFieldName; 31 | -------------------------------------------------------------------------------- /src/utils/schema/introspection/getReversedFields.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Filters out the fields not linked to a specified resource 3 | * 4 | * @param fields definition of structure fields 5 | * @param id resource id 6 | * @returns array of fields linked to the resource 7 | */ 8 | export default (fields, id): any[] => { 9 | return fields.filter( 10 | (x) => x.resource && x.resource === id.toString() && x.relatedName 11 | ); 12 | }; 13 | -------------------------------------------------------------------------------- /src/utils/schema/introspection/getTypes.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLObjectType, GraphQLString } from 'graphql'; 2 | import { ReferenceData } from '@models'; 3 | import { SchemaStructure } from '../getStructures'; 4 | import { getFields } from './getFields'; 5 | 6 | /** 7 | * Get GraphQL types from the structures. 8 | * 9 | * @param structures definition of forms / resources structures. 10 | * @returns array of GraphQL types of the structures. 11 | */ 12 | const getTypes = (structures: SchemaStructure[]) => { 13 | return structures.map( 14 | (x) => 15 | new GraphQLObjectType({ 16 | name: x.name, 17 | fields: getFields(x.fields), 18 | }) 19 | ); 20 | }; 21 | 22 | /** 23 | * Get GraphQL types from the reference data. 24 | * 25 | * @param referenceDatas list of all referenceDatas 26 | * @returns array of GraphQL types of the referenceDatas. 27 | */ 28 | export const getReferenceDatasTypes = (referenceDatas: ReferenceData[]) => 29 | referenceDatas.map( 30 | (x) => 31 | new GraphQLObjectType({ 32 | name: x.name, 33 | fields: x.fields.reduce((o: any, field) => { 34 | o[field.graphQLFieldName] = { type: GraphQLString }; 35 | return o; 36 | }, {}), 37 | }) 38 | ); 39 | export default getTypes; 40 | -------------------------------------------------------------------------------- /src/utils/schema/introspection/isRelationshipField.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Checks if field indicates a relationship with another document 3 | * 4 | * @param fieldName The field name 5 | * @returns If the field indicates a relationship with another document 6 | */ 7 | export const isRelationshipField = (fieldName) => 8 | fieldName.endsWith('_id') || 9 | fieldName.endsWith('_ids') || 10 | fieldName.endsWith('_ref'); 11 | -------------------------------------------------------------------------------- /src/utils/schema/resolvers/Meta/getMetaCheckboxResolver.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Return checkbox meta resolver. 3 | * 4 | * @param field field definition. 5 | * @returns Checkbox resolver. 6 | */ 7 | const getMetaCheckboxResolver = (field: any) => { 8 | if (field.choices) { 9 | const choices = field.choices.map((x) => { 10 | return { 11 | text: x.text ? x.text : x, 12 | value: x.value ? x.value : x, 13 | }; 14 | }); 15 | return Object.assign(field, { choices }); 16 | } else { 17 | return field; 18 | } 19 | }; 20 | 21 | export default getMetaCheckboxResolver; 22 | -------------------------------------------------------------------------------- /src/utils/schema/resolvers/Meta/getMetaDropdownResolver.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Return dropdown meta resolver. 3 | * 4 | * @param field field definition. 5 | * @returns Dropdown resolver. 6 | */ 7 | const getMetaDropdownResolver = (field: any) => { 8 | if (field.choices) { 9 | const choices = field.choices.map((x) => { 10 | return { 11 | text: x.text ? x.text : x, 12 | value: x.value ? x.value : x, 13 | }; 14 | }); 15 | return Object.assign(field, { choices }); 16 | } else { 17 | return field; 18 | } 19 | }; 20 | 21 | export default getMetaDropdownResolver; 22 | -------------------------------------------------------------------------------- /src/utils/schema/resolvers/Meta/getMetaFieldResolver.ts: -------------------------------------------------------------------------------- 1 | import getMetaCheckboxResolver from './getMetaCheckboxResolver'; 2 | import getMetaDropdownResolver from './getMetaDropdownResolver'; 3 | import getMetaOwnerResolver from './getMetaOwnerResolver'; 4 | import getMetaUsersResolver from './getMetaUsersResolver'; 5 | import getMetaRadioResolver from './getMetaRadiogroupResolver'; 6 | import getMetaTagboxResolver from './getMetaTagboxResolver'; 7 | 8 | /** 9 | * Return GraphQL resolver of the field, based on its type. 10 | * 11 | * @param field field definition. 12 | * @returns resolver of the field. 13 | */ 14 | const getMetaFieldResolver = (field: any) => { 15 | switch (field.type) { 16 | case 'dropdown': { 17 | return getMetaDropdownResolver(field); 18 | } 19 | case 'radiogroup': { 20 | return getMetaRadioResolver(field); 21 | } 22 | case 'checkbox': { 23 | return getMetaCheckboxResolver(field); 24 | } 25 | case 'tagbox': { 26 | return getMetaTagboxResolver(field); 27 | } 28 | case 'users': { 29 | return getMetaUsersResolver(field); 30 | } 31 | case 'owner': { 32 | return getMetaOwnerResolver(field); 33 | } 34 | default: { 35 | return field; 36 | } 37 | } 38 | }; 39 | 40 | export default getMetaFieldResolver; 41 | -------------------------------------------------------------------------------- /src/utils/schema/resolvers/Meta/getMetaOwnerResolver.ts: -------------------------------------------------------------------------------- 1 | import { Role } from '@models'; 2 | import mongoose from 'mongoose'; 3 | 4 | /** 5 | * Return owner meta resolver. 6 | * 7 | * @param field field definition. 8 | * @returns Owner resolver. 9 | */ 10 | const getMetaOwnerResolver = async (field: any) => { 11 | const roles = await Role.find({ 12 | application: { 13 | $in: field.applications.map((x) => new mongoose.Types.ObjectId(x)), 14 | }, 15 | }) 16 | .select('id title application') 17 | .populate({ 18 | path: 'application', 19 | model: 'Application', 20 | }); 21 | return Object.assign(field, { 22 | choices: roles.map((x) => { 23 | return { 24 | text: `${x.application.name} - ${x.title}`, 25 | value: x.id, 26 | }; 27 | }), 28 | }); 29 | }; 30 | 31 | export default getMetaOwnerResolver; 32 | -------------------------------------------------------------------------------- /src/utils/schema/resolvers/Meta/getMetaRadiogroupResolver.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Return radiogroup meta resolver. 3 | * 4 | * @param field field definition. 5 | * @returns Radiogroup resolver. 6 | */ 7 | const getMetaRadiogroupResolver = (field: any) => { 8 | if (field.choices) { 9 | const choices = field.choices.map((x) => { 10 | return { 11 | text: x.text ? x.text : x, 12 | value: x.value ? x.value : x, 13 | }; 14 | }); 15 | return Object.assign(field, { choices }); 16 | } else { 17 | return field; 18 | } 19 | }; 20 | 21 | export default getMetaRadiogroupResolver; 22 | -------------------------------------------------------------------------------- /src/utils/schema/resolvers/Meta/getMetaTagboxResolver.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Return tagbox meta resolver. 3 | * 4 | * @param field field definition. 5 | * @returns Tagbox resolver. 6 | */ 7 | const getMetaTagboxResolver = (field: any) => { 8 | return Object.assign(field, { 9 | ...(field.choices && { 10 | choices: field.choices.map((x) => { 11 | return { 12 | text: x.text ? x.text : x, 13 | value: x.value ? x.value : x, 14 | }; 15 | }), 16 | }), 17 | }); 18 | }; 19 | 20 | export default getMetaTagboxResolver; 21 | -------------------------------------------------------------------------------- /src/utils/schema/resolvers/Query/getSortOrder.ts: -------------------------------------------------------------------------------- 1 | import { SortOrder } from 'mongoose'; 2 | 3 | /** 4 | * Decodes the sort order string 5 | * 6 | * @param sortOrder The sort order string 7 | * @returns The decoded sorted order 8 | */ 9 | export default (sortOrder: string): SortOrder => (sortOrder === 'asc' ? 1 : -1); 10 | -------------------------------------------------------------------------------- /src/utils/schema/resolvers/Query/single.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLError } from 'graphql'; 2 | import { Record } from '@models'; 3 | import { logger } from '@services/logger.service'; 4 | import { graphQLAuthCheck } from '@schema/shared'; 5 | 6 | /** 7 | * Returns a resolver that fetches a record if the users logged 8 | * or throws an error if not 9 | * 10 | * @returns A resolver function that fetches a record by id 11 | */ 12 | export default () => 13 | async (_, { id, data }, context) => { 14 | graphQLAuthCheck(context); 15 | try { 16 | const record = await Record.findOne({ _id: id, archived: { $ne: true } }); 17 | if (data) { 18 | record.data = data; 19 | } 20 | return record; 21 | } catch (err) { 22 | logger.error(err.message, { stack: err.stack }); 23 | if (err instanceof GraphQLError) { 24 | throw new GraphQLError(err.message); 25 | } 26 | throw new GraphQLError( 27 | context.i18next.t('common.errors.internalServerError') 28 | ); 29 | } 30 | }; 31 | -------------------------------------------------------------------------------- /src/utils/server/checkConfig.util.ts: -------------------------------------------------------------------------------- 1 | import { logger } from '@services/logger.service'; 2 | import config from 'config'; 3 | import { isNil } from 'lodash'; 4 | 5 | /** List all mandatory config keys */ 6 | const mandatoryConfigKeys = [ 7 | // server 8 | 'server.url', 9 | 'server.allowedOrigins', 10 | 'server.protectedShortcuts', 11 | // front-office 12 | 'frontOffice.uri', 13 | // back-office 14 | 'backOffice.uri', 15 | // database 16 | 'database.provider', 17 | 'database.prefix', 18 | 'database.host', 19 | 'database.name', 20 | 'database.user', 21 | 'database.pass', 22 | ]; 23 | 24 | /** 25 | * Check config is valid 26 | */ 27 | export const checkConfig = () => { 28 | try { 29 | for (const key of mandatoryConfigKeys) { 30 | const value = config.get(key); 31 | if (isNil(value) || (typeof value === 'string' && value.length === 0)) { 32 | throw new Error(`Configuration property ${key} is null or undefined`); 33 | } 34 | } 35 | } catch (err) { 36 | logger.error(err.message); 37 | process.exit(); 38 | } 39 | }; 40 | -------------------------------------------------------------------------------- /src/utils/user/index.ts: -------------------------------------------------------------------------------- 1 | export * from './sendUserInvitation'; 2 | export * from './userManagement'; 3 | export * from './fetchGroups'; 4 | -------------------------------------------------------------------------------- /src/utils/validators/index.ts: -------------------------------------------------------------------------------- 1 | export * from './validateName'; 2 | export * from './validateApi'; 3 | -------------------------------------------------------------------------------- /src/utils/validators/validateApi.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLError } from 'graphql'; 2 | import i18next from 'i18next'; 3 | 4 | /** 5 | * Checks that API name is valid. 6 | * API name should only consist of alphanumeric characters. 7 | * 8 | * @param name value to test 9 | */ 10 | export const validateApi = (name: string): void => { 11 | if (!/^[A-Za-z-_]+$/i.test(name)) { 12 | throw new GraphQLError(i18next.t('common.errors.invalidGraphQLName')); 13 | } 14 | }; 15 | -------------------------------------------------------------------------------- /tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "./build" 5 | }, 6 | "exclude": [ 7 | "node_modules", 8 | "build", 9 | // there's a wrong link in this file, needed for migration 10 | "src/migrations/template.ts", 11 | // exclude test files 12 | "jest.config.ts", 13 | "__tests__/**/*" 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "module": "commonjs", 5 | "outDir": "./build", 6 | // "strict": true, 7 | "moduleResolution": "node", 8 | "baseUrl": "./src", 9 | "paths": { 10 | "@const/*": ["const/*"], 11 | "@models": ["models"], 12 | "@models/*": ["models/*"], 13 | "@routes/*": ["routes/*"], 14 | "@schema/*": ["schema/*"], 15 | "@security/*": ["security/*"], 16 | "@server/*": ["server/*"], 17 | "@services/*": ["services/*"], 18 | "@utils/*": ["utils/*"] 19 | }, 20 | "esModuleInterop": true, 21 | "skipLibCheck": true, 22 | "forceConsistentCasingInFileNames": true, 23 | }, 24 | "exclude": [ 25 | "node_modules", 26 | "build", 27 | "src/migrations/template.ts", // there's a wrong link in this file, needed for migration 28 | "__tests__/old" // old tests 29 | ], 30 | } 31 | --------------------------------------------------------------------------------