From e87dba0f9bd216e365ae62a1534247a006273f65 Mon Sep 17 00:00:00 2001 From: Chris Date: Fri, 13 Nov 2020 11:21:02 +1100 Subject: [PATCH 001/144] Progress commit. --- build.gradle | 18 +- grails-app/conf/application.yml | 35 +++- grails-app/conf/data/mapping.json | 5 + grails-app/conf/ecodata.graphql | 35 ++++ grails-app/conf/spring/resources.groovy | 11 ++ .../domain/au/org/ala/ecodata/Activity.groovy | 65 ++++++- .../domain/au/org/ala/ecodata/Document.groovy | 2 + .../domain/au/org/ala/ecodata/Output.groovy | 28 +++ .../domain/au/org/ala/ecodata/Project.groovy | 9 +- .../domain/au/org/ala/ecodata/Site.groovy | 36 ++++ .../au/org/ala/ecodata/UserPermission.groovy | 13 ++ .../au/org/ala/ecodata/Application.groovy | 26 +++ .../au/org/ala/ecodata/CommonService.groovy | 5 +- .../ala/ecodata/ElasticSearchService.groovy | 133 +++++++------ .../org/ala/ecodata/PermissionService.groovy | 176 ++++++++++++++++++ scripts/misc/distinctFields.js | 12 ++ .../au/org/ala/ecodata/AccessLevel.groovy | 16 +- .../org/ala/ecodata/IdentifierHelper.groovy | 40 ++++ .../au/org/ala/ecodata/Permissions.groovy | 14 ++ .../ecodata/graphql/ActivityFetcher.groovy | 53 ++++++ .../EcodataGraphQLContextBuilder.groovy | 26 +++ .../graphql/EcodataGraphQLCustomiser.groovy | 80 ++++++++ .../ecodata/graphql/ObjectConverter.groovy | 37 ++++ .../ecodata/graphql/ObjectIdConverter.groovy | 56 ++++++ .../graphql/ProjectGraphQLMapper.groovy | 93 +++++++++ .../ecodata/graphql/ProjectsFetcher.groovy | 86 +++++++++ .../ala/ecodata/graphql/SitesFetcher.groovy | 78 ++++++++ 27 files changed, 1119 insertions(+), 69 deletions(-) create mode 100644 grails-app/conf/ecodata.graphql create mode 100644 scripts/misc/distinctFields.js create mode 100644 src/main/groovy/au/org/ala/ecodata/Permissions.groovy create mode 100644 src/main/groovy/au/org/ala/ecodata/graphql/ActivityFetcher.groovy create mode 100644 src/main/groovy/au/org/ala/ecodata/graphql/EcodataGraphQLContextBuilder.groovy create mode 100644 src/main/groovy/au/org/ala/ecodata/graphql/EcodataGraphQLCustomiser.groovy create mode 100644 src/main/groovy/au/org/ala/ecodata/graphql/ObjectConverter.groovy create mode 100644 src/main/groovy/au/org/ala/ecodata/graphql/ObjectIdConverter.groovy create mode 100644 src/main/groovy/au/org/ala/ecodata/graphql/ProjectGraphQLMapper.groovy create mode 100644 src/main/groovy/au/org/ala/ecodata/graphql/ProjectsFetcher.groovy create mode 100644 src/main/groovy/au/org/ala/ecodata/graphql/SitesFetcher.groovy diff --git a/build.gradle b/build.gradle index 0db404411..9f4b234f0 100644 --- a/build.gradle +++ b/build.gradle @@ -7,12 +7,11 @@ buildscript { mavenLocal() maven { url "https://nexus.ala.org.au/content/groups/public/" } maven { url "https://repo.grails.org/grails/core" } + jcenter() } dependencies { classpath "org.grails:grails-gradle-plugin:$grailsVersion" classpath "com.bertramlabs.plugins:asset-pipeline-gradle:$assetPipelineVersion" - // classpath "org.grails.plugins:hibernate" - // classpath "org.grails.plugins:hibernate5:${gormVersion-".RELEASE"}" } } @@ -62,18 +61,13 @@ dependencies { compile "org.grails.plugins:async" compile "org.grails.plugins:scaffolding" compile "org.grails.plugins:events" - // compile "org.grails.plugins:hibernate5:${gormVersion-".RELEASE"}" -// compile "org.grails.plugins:hibernate5" - // compile "org.hibernate:hibernate-core:5.1.16.Final" - // compile "org.hibernate:hibernate-ehcache:5.1.16.Final" compile "org.grails:grails-plugin-datasource" compile "org.grails:grails-plugin-databinding" compile "org.grails:grails-plugin-codecs" //compile 'org.grails.plugins:mongodb:7.0.0' compile "org.grails.plugins:mongodb:6.1.7" compile "org.mongodb:mongodb-driver:3.9.1" - // provided "org.grails.plugins:embedded-mongodb:1.0.2" - // compile "org.mongodb:mongodb-driver:3.4.2" + compile "org.grails.plugins:gorm-graphql:1.0.3.BUILD-SNAPSHOT" compile "org.grails.plugins:gsp" console "org.grails:grails-console" profile "org.grails.profiles:web" @@ -128,6 +122,11 @@ dependencies { compile 'org.grails.plugins:excel-import:3.0.2' compile 'org.grails.plugins:excel-export:2.1' + compile 'org.apache.poi:ooxml-schemas:1.4' + compile 'org.apache.poi:poi:4.1.1' + compile 'org.apache.poi:poi-ooxml:4.1.1' + compile 'org.apache.poi:poi-ooxml-schemas:4.1.1' + // https://mvnrepository.com/artifact/com.vividsolutions/jts compile group: 'com.vividsolutions', name: 'jts', version: '1.13' compile "com.itextpdf:itextpdf:5.5.1" @@ -147,6 +146,9 @@ dependencies { // For logback filter compile 'org.codehaus.janino:janino:3.0.6' + //compile 'com.graphql-java-kickstart:graphql-java-servlet:7.0.0' + //compile 'com.graphql-java:graphql-java-tools:5.7.1' + // compile "org.grails.plugins:jmx:0.9" //TODO: including this plugin interferes with debug watch in intellij. Need to look into another way to check MBean diff --git a/grails-app/conf/application.yml b/grails-app/conf/application.yml index e228739a2..e9ae3ba21 100644 --- a/grails-app/conf/application.yml +++ b/grails-app/conf/application.yml @@ -166,7 +166,7 @@ security: casServerUrlPrefix: 'https://auth.ala.org.au/cas' loginUrl: 'https://auth.ala.org.au/cas/login' logoutUrl: 'https://auth.ala.org.au/cas/logout' - uriFilterPattern: '/admin/.*,/activityForm/((?!get).)*' + uriFilterPattern: '/admin/.*,/activityForm/((?!get).)*,/graphql.*' authenticateOnlyIfLoggedInPattern: uriExclusionFilterPattern: '/images.*,/css.*,/js.*,/less.*' @@ -203,4 +203,37 @@ grails: codecs: au.org.ala.ecodata.customcodec.AccessLevelCodec engine: codec +graphql: + servlet: + mapping: /graphql + enabled: true + corsEnabled: true + # if you want to @ExceptionHandler annotation for custom GraphQLErrors + exception-handlers-enabled: true + contextSetting: PER_REQUEST_WITH_INSTRUMENTATION + +graphiql: + mapping: /graphiql + endpoint: + graphql: /graphql + subscriptions: /subscriptions + subscriptions: + timeout: 30 + reconnect: false + static: + basePath: /assets + enabled: true + pageTitle: GraphiQL + cdn: + enabled: true + version: 0.13.0 + props: + resources: + # query: query.graphql + # defaultQuery: defaultQuery.graphql + # variables: variables.graphql + variables: + editorTheme: "solarized light" + headers: + Authorization: "Bearer " diff --git a/grails-app/conf/data/mapping.json b/grails-app/conf/data/mapping.json index 05c0ecb09..ad9170503 100644 --- a/grails-app/conf/data/mapping.json +++ b/grails-app/conf/data/mapping.json @@ -14,6 +14,11 @@ "type" : "string", "index" : "not_analyzed" }, + "hubId": { + "type": "string", + "index": "not_analyzed" + }, + "projects" : { "type" : "string", "index" : "not_analyzed" diff --git a/grails-app/conf/ecodata.graphql b/grails-app/conf/ecodata.graphql new file mode 100644 index 000000000..6e89a3e01 --- /dev/null +++ b/grails-app/conf/ecodata.graphql @@ -0,0 +1,35 @@ + +type Query { + ProjectById(id: String!) : Project +} + +type Project { + projectId : String! + name: String! + description: String! + sites: [Site] + meriPlan : MeriPlan + status: String! + meriPlanStatus: String! +} + +type MeriPlan { + +} + +type Site { + siteId: String! + name: String! + extent: GeoJson +} + + +type GeoJson { + type: String! + +} + +type Organisation { + name: String! +} + diff --git a/grails-app/conf/spring/resources.groovy b/grails-app/conf/spring/resources.groovy index fa950068b..ff1fb5b2b 100644 --- a/grails-app/conf/spring/resources.groovy +++ b/grails-app/conf/spring/resources.groovy @@ -1,3 +1,14 @@ +import au.org.ala.ecodata.graphql.ActivityFetcher +import au.org.ala.ecodata.graphql.EcodataGraphQLContextBuilder +import au.org.ala.ecodata.graphql.EcodataGraphQLCustomiser +import au.org.ala.ecodata.graphql.ProjectsFetcher +import au.org.ala.ecodata.graphql.SitesFetcher + // Place your Spring DSL code here beans = { + ecodataGraphQLCustomiser(EcodataGraphQLCustomiser) + projectsFetcher(ProjectsFetcher) + sitesFetcher(SitesFetcher) + activitiesFetcher(ActivityFetcher) + graphQLContextBuilder(EcodataGraphQLContextBuilder) } diff --git a/grails-app/domain/au/org/ala/ecodata/Activity.groovy b/grails-app/domain/au/org/ala/ecodata/Activity.groovy index 139fd1b92..b5fbd9ddf 100644 --- a/grails-app/domain/au/org/ala/ecodata/Activity.groovy +++ b/grails-app/domain/au/org/ala/ecodata/Activity.groovy @@ -1,6 +1,10 @@ package au.org.ala.ecodata +import grails.util.Holders +import graphql.schema.DataFetcher +import graphql.schema.DataFetchingEnvironment import org.bson.types.ObjectId +import org.grails.gorm.graphql.entity.dsl.GraphQLMapping /** * Currently this holds both activities and assessments. @@ -25,7 +29,66 @@ class Activity { activities may have 0..n Outputs - these are mapped from the Output side */ - static mapping = { + static graphql = GraphQLMapping.lazy { + // Disable default operations, including get as we only want to expose UUIDs in the API not internal ones + operations.get.enabled false + operations.list.enabled true + operations.count.enabled false + operations.create.enabled false + operations.update.enabled false + operations.delete.enabled false + + add('outputs', [Output]) { + dataFetcher { Activity activity -> + Output.findAllByActivityId(activity.activityId) + } + input false + } + + add('data', 'Data') { + ActivityForm form = Holders.grailsApplication.mainContext.activityFormService.findActivityForm("RLP Output Report", 1) + input false + type { + form.sections.each { FormSection section -> + if (section.template.dataModel) { + + String typeName = section.name.replaceAll("[ |-]", "") + field(typeName, typeName) { + + for (Map dataModelItem : section.template.dataModel) { + field(dataModelItem.name, String) { + if (dataModelItem.description) { + description(dataModelItem.description) + } + } + } + } + } + + } + } + dataFetcher { Activity activity -> + Map result = Output.findAllByActivityId(activity.activityId).collectEntries { + [(it.name.replaceAll("[ |-]", "")):it.data] + } + result + } + + + } + query('activities', [Activity]) { + argument('term', String) + dataFetcher(new DataFetcher() { + @Override + Object get(DataFetchingEnvironment environment) throws Exception { + environment.context.grailsApplication.mainContext.activitiesFetcher.get(environment) + } + }) + } + } + + + static mapping = { activityId index: true siteId index: true projectId index: true diff --git a/grails-app/domain/au/org/ala/ecodata/Document.groovy b/grails-app/domain/au/org/ala/ecodata/Document.groovy index 5568c32f2..939def2b5 100644 --- a/grails-app/domain/au/org/ala/ecodata/Document.groovy +++ b/grails-app/domain/au/org/ala/ecodata/Document.groovy @@ -53,6 +53,7 @@ class Document { String outputId String organisationId String programId + String managementUnitId String reportId String externalUrl Boolean isSciStarter = false @@ -143,6 +144,7 @@ class Document { outputId nullable: true programId nullable: true reportId nullable: true + managementUnitId nullable: true stage nullable: true filename nullable: true dateCreated nullable: true diff --git a/grails-app/domain/au/org/ala/ecodata/Output.groovy b/grails-app/domain/au/org/ala/ecodata/Output.groovy index e8d88c967..fca61ce8c 100644 --- a/grails-app/domain/au/org/ala/ecodata/Output.groovy +++ b/grails-app/domain/au/org/ala/ecodata/Output.groovy @@ -1,9 +1,37 @@ package au.org.ala.ecodata +import grails.util.Holders +import graphql.schema.DataFetcher +import graphql.schema.DataFetchingEnvironment import org.bson.types.ObjectId +import org.grails.gorm.graphql.entity.dsl.GraphQLMapping class Output { + static graphql = GraphQLMapping.lazy { + // Disable default operations, including get as we only want to expose UUIDs in the API not internal ones + operations.get.enabled false + operations.list.enabled true + operations.count.enabled false + operations.create.enabled false + operations.update.enabled false + operations.delete.enabled false + + +// add('data', Map) { +// dataFetcher { Output output -> +// Activity activity = Activity.findByActivityId(output.activityId) +// ActivityForm form = Holders.grailsApplication.mainContext.activityFormService.findActivityForm(activity.type, activity.formVersion) +// form.sections.each { FormSection section -> +// section.template.dataModel.each { +// +// } +// } +// } +// input false +// } + } + /* Associations: outputs must belong to 1 Activity - this is mapped by the activityId in this domain diff --git a/grails-app/domain/au/org/ala/ecodata/Project.groovy b/grails-app/domain/au/org/ala/ecodata/Project.groovy index cf4a77d52..e5c96ca97 100644 --- a/grails-app/domain/au/org/ala/ecodata/Project.groovy +++ b/grails-app/domain/au/org/ala/ecodata/Project.groovy @@ -1,15 +1,17 @@ package au.org.ala.ecodata -import static au.org.ala.ecodata.Status.COMPLETED - +import au.org.ala.ecodata.graphql.ProjectGraphQLMapper import org.bson.types.ObjectId import org.joda.time.DateTime import org.joda.time.Days import org.joda.time.Interval +import static au.org.ala.ecodata.Status.COMPLETED class Project { + static graphql = ProjectGraphQLMapper.graphqlMapping() + /* Associations: projects may have 0..n Sites - these are mapped from the Site side @@ -88,6 +90,8 @@ class Project { /** The program of work this project is a part of, if any */ String programId + Hub hub + static embedded = ['associatedOrganisations','fundings'] static transients = ['activities', 'plannedDurationInWeeks', 'actualDurationInWeeks'] @@ -192,6 +196,7 @@ class Project { industries nullable: true programId nullable: true baseLayer nullable: true + hub nullable: true } } diff --git a/grails-app/domain/au/org/ala/ecodata/Site.groovy b/grails-app/domain/au/org/ala/ecodata/Site.groovy index aa8221886..a9609ea9c 100644 --- a/grails-app/domain/au/org/ala/ecodata/Site.groovy +++ b/grails-app/domain/au/org/ala/ecodata/Site.groovy @@ -4,8 +4,11 @@ import com.vividsolutions.jts.geom.Geometry import com.vividsolutions.jts.operation.valid.IsValidOp import com.vividsolutions.jts.operation.valid.TopologyValidationError import grails.converters.JSON +import graphql.schema.DataFetcher +import graphql.schema.DataFetchingEnvironment import org.bson.types.ObjectId import org.geotools.geojson.geom.GeometryJSON +import org.grails.gorm.graphql.entity.dsl.GraphQLMapping class Site { @@ -13,6 +16,39 @@ class Site { static String TYPE_PROJECT_AREA = 'projectArea' static String TYPE_WORKS_AREA = 'worksArea' + static graphql = GraphQLMapping.lazy { + // Disable default operations, including get as we only want to expose UUIDs in the API not internal ones + operations.get.enabled false + operations.list.enabled true + operations.count.enabled false + operations.create.enabled false + operations.update.enabled false + operations.delete.enabled false + + exclude 'extent', 'features', 'projects' + + add('geometry', 'Geometry') { + type { + field('type', String) + field('coordinates', [Object]) + } + dataFetcher { Site site -> + site.extent.geometry + } + } + + query('sites', [Site]) { + argument('term', String) + dataFetcher(new DataFetcher() { + @Override + Object get(DataFetchingEnvironment environment) throws Exception { + environment.context.grailsApplication.mainContext.sitesFetcher.get(environment) + } + }) + } + + } + def siteService /* diff --git a/grails-app/domain/au/org/ala/ecodata/UserPermission.groovy b/grails-app/domain/au/org/ala/ecodata/UserPermission.groovy index a3dd4f2fb..f901a6751 100644 --- a/grails-app/domain/au/org/ala/ecodata/UserPermission.groovy +++ b/grails-app/domain/au/org/ala/ecodata/UserPermission.groovy @@ -16,6 +16,7 @@ class UserPermission { AccessLevel accessLevel String entityType String status = ACTIVE + List permissions = [] static constraints = { userId(unique: ['accessLevel', 'entityId']) // prevent duplicate entries @@ -30,4 +31,16 @@ class UserPermission { accessLevel index: true version false } + + boolean hasPermission(String permission) { + boolean hasPermission = false + if (permissions) { + hasPermission = permissions.contains(permission) + } + else { + // fallback to role definitions + hasPermission = accessLevel.includes(permission) + } + hasPermission + } } diff --git a/grails-app/init/au/org/ala/ecodata/Application.groovy b/grails-app/init/au/org/ala/ecodata/Application.groovy index ecc3d57fe..9999d7dda 100644 --- a/grails-app/init/au/org/ala/ecodata/Application.groovy +++ b/grails-app/init/au/org/ala/ecodata/Application.groovy @@ -2,6 +2,17 @@ package au.org.ala.ecodata import grails.boot.GrailsApp import grails.boot.config.GrailsAutoConfiguration +import graphql.Scalars +import graphql.schema.GraphQLObjectType +import graphql.schema.GraphQLSchema +import graphql.schema.StaticDataFetcher +import graphql.schema.idl.RuntimeWiring +import graphql.schema.idl.SchemaGenerator +import graphql.schema.idl.SchemaParser +import graphql.schema.idl.TypeDefinitionRegistry +import graphql.schema.idl.TypeRuntimeWiring +import org.springframework.context.annotation.Bean + //import groovy.util.logging.Slf4j import org.springframework.context.annotation.ComponentScan @@ -11,4 +22,19 @@ class Application extends GrailsAutoConfiguration { static void main(String[] args) { GrailsApp.run(Application, args) } + + @Bean + GraphQLSchema schema() { + String schema = "type Query{hello: String}"; + + SchemaParser schemaParser = new SchemaParser(); + TypeDefinitionRegistry typeDefinitionRegistry = schemaParser.parse(schema); + + RuntimeWiring runtimeWiring = RuntimeWiring.newRuntimeWiring() + .type(TypeRuntimeWiring.newTypeWiring("Query").dataFetcher("hello", new StaticDataFetcher("world")) ) + .build(); + + SchemaGenerator schemaGenerator = new SchemaGenerator(); + return schemaGenerator.makeExecutableSchema(typeDefinitionRegistry, runtimeWiring); + } } \ No newline at end of file diff --git a/grails-app/services/au/org/ala/ecodata/CommonService.groovy b/grails-app/services/au/org/ala/ecodata/CommonService.groovy index 63c06c080..806ba7994 100644 --- a/grails-app/services/au/org/ala/ecodata/CommonService.groovy +++ b/grails-app/services/au/org/ala/ecodata/CommonService.groovy @@ -56,7 +56,10 @@ class CommonService { v = null } - o[k] = v + // Dynamic properties with a null value result in a NPE when using the GORM mongo codec mapping. + if (v != null || domainDescriptor.hasProperty(k)) { + o[k] = v + } } // always flush the update so that that any exceptions are caught before the service returns o.save(flush:true) diff --git a/grails-app/services/au/org/ala/ecodata/ElasticSearchService.groovy b/grails-app/services/au/org/ala/ecodata/ElasticSearchService.groovy index 7a6e76615..b225cbc48 100644 --- a/grails-app/services/au/org/ala/ecodata/ElasticSearchService.groovy +++ b/grails-app/services/au/org/ala/ecodata/ElasticSearchService.groovy @@ -20,6 +20,7 @@ import grails.converters.JSON import groovy.json.JsonSlurper import org.elasticsearch.action.index.IndexRequestBuilder import org.elasticsearch.action.search.SearchRequest +import org.elasticsearch.action.search.SearchResponse import org.elasticsearch.action.search.SearchType import org.elasticsearch.client.Client import org.elasticsearch.common.geo.ShapeRelation @@ -39,6 +40,7 @@ import org.grails.datastore.mapping.engine.event.EventType import javax.annotation.PostConstruct import javax.annotation.PreDestroy +import javax.naming.directory.SearchResult import java.text.SimpleDateFormat import java.util.concurrent.ConcurrentLinkedQueue import java.util.regex.Matcher @@ -701,56 +703,56 @@ class ElasticSearchService { } } - log.info "Indexing all sites" - int count = 0 - Site.withNewSession { session -> - siteService.doWithAllSites { def siteMap -> - siteMap["className"] = Site.class.name - try { - siteMap = prepareSiteForIndexing(siteMap, false) - indexDoc(siteMap, DEFAULT_INDEX) - } - catch (Exception e) { - log.error("Unable index site: "+siteMap?.siteId, e) - } - count++ - if (count % 1000 == 0) { - session.clear() - log.info("Indexed "+count+" sites") - } - } - } - - log.info "Indexing all activities" - count = 0; - Activity.withNewSession { session -> - activityService.doWithAllActivities { Map activity -> - try { - activity = prepareActivityForIndexing(activity) - indexDoc(activity, activity?.projectActivityId || activity?.isWorks ? PROJECT_ACTIVITY_INDEX : DEFAULT_INDEX) - } - catch (Exception e) { - log.error("Unable to index activity: " + activity?.activityId, e) - } - - count++ - if (count % 1000 == 0) { - session.clear() - log.info("Indexed " + count + " activities") - } - } - } - - log.info "Indexing all organisations" - organisationService.doWithAllOrganisations { Map org -> - try { - prepareOrganisationForIndexing(org) - indexDoc(org, DEFAULT_INDEX) - } - catch (Exception e) { - log.error("Unable to index organisation: "+org?.organisationId, e) - } - } +// log.info "Indexing all sites" +// int count = 0 +// Site.withNewSession { session -> +// siteService.doWithAllSites { def siteMap -> +// siteMap["className"] = Site.class.name +// try { +// siteMap = prepareSiteForIndexing(siteMap, false) +// indexDoc(siteMap, DEFAULT_INDEX) +// } +// catch (Exception e) { +// log.error("Unable index site: "+siteMap?.siteId, e) +// } +// count++ +// if (count % 1000 == 0) { +// session.clear() +// log.info("Indexed "+count+" sites") +// } +// } +// } +// +// log.info "Indexing all activities" +// count = 0; +// Activity.withNewSession { session -> +// activityService.doWithAllActivities { Map activity -> +// try { +// activity = prepareActivityForIndexing(activity) +// indexDoc(activity, activity?.projectActivityId || activity?.isWorks ? PROJECT_ACTIVITY_INDEX : DEFAULT_INDEX) +// } +// catch (Exception e) { +// log.error("Unable to index activity: " + activity?.activityId, e) +// } +// +// count++ +// if (count % 1000 == 0) { +// session.clear() +// log.info("Indexed " + count + " activities") +// } +// } +// } +// +// log.info "Indexing all organisations" +// organisationService.doWithAllOrganisations { Map org -> +// try { +// prepareOrganisationForIndexing(org) +// indexDoc(org, DEFAULT_INDEX) +// } +// catch (Exception e) { +// log.error("Unable to index organisation: "+org?.organisationId, e) +// } +// } log.info "Indexing complete" } @@ -1115,14 +1117,19 @@ class ElasticSearchService { * @param params * @return IndexResponse */ - def search(String query, Map params, String index, Map geoSearchCriteria = [:]) { + def search(String query, Map params, String index, Map geoSearchCriteria = [:], boolean applyAccessControlFilter = false) { log.debug "search params: ${params}" index = index ?: DEFAULT_INDEX - def request = buildSearchRequest(query, params, index, geoSearchCriteria) + def request = buildSearchRequest(query, params, index, geoSearchCriteria, applyAccessControlFilter) client.search(request).actionGet() } + SearchResponse searchWithSecurity(String userId, String query, Map params, String index = HOMEPAGE_INDEX, Map geoSearchCriteria = [:]) { + + search(query, params, index, geoSearchCriteria, true) + } + /** * Full text search with just a query (String) * @@ -1270,7 +1277,7 @@ class ElasticSearchService { * @param geoSearchCriteria geo search criteria. * @return SearchRequest */ - def buildSearchRequest(String queryString, Map params, String index, Map geoSearchCriteria = [:]) { + def buildSearchRequest(String queryString, Map params, String index, Map geoSearchCriteria = [:], boolean applyAccessControl = false) { SearchRequest request = new SearchRequest() request.searchType SearchType.DFS_QUERY_THEN_FETCH @@ -1282,7 +1289,7 @@ class ElasticSearchService { } request.types(types as String[]) - QueryBuilder query = buildQuery(queryString, params, geoSearchCriteria, index) + QueryBuilder query = buildQuery(queryString, params, geoSearchCriteria, index, applyAccessControl) // set pagination stuff SearchSourceBuilder source = pagenateQuery(params).query(query) @@ -1354,10 +1361,26 @@ class ElasticSearchService { hubFilters } - private QueryBuilder buildQuery(String query, Map params, Map geoSearchCriteria = null, String index) { + private FilterBuilder buildAccessControlFilter() { + String userId = UserService.currentUser()?.userId + + FilterBuilder filter = FilterBuilders.termFilter("allParticipants", userId) + List permissions = UserPermission.findAllByUserIdAndEntityTypeAndPermissionsAndStatusNotEqual(userId, Hub.name, "api", Status.DELETED) + if (permissions) { + FilterBuilder hubs = FilterBuilders.termsFilter("hubId", permissions.collect { it.entityId}) + filter = FilterBuilders.boolFilter().should(filter).should(hubs) + } + filter + } + + private QueryBuilder buildQuery(String query, Map params, Map geoSearchCriteria = null, String index, boolean applyAccessControlFilters = false) { QueryBuilder queryBuilder List filters = [] + if (applyAccessControlFilters) { + filters << buildAccessControlFilter() + } + List hubFilters = extractHubFilterParameters(params) if (hubFilters) { filters << buildFilters(hubFilters) diff --git a/grails-app/services/au/org/ala/ecodata/PermissionService.groovy b/grails-app/services/au/org/ala/ecodata/PermissionService.groovy index a736c72d4..20d11abb6 100644 --- a/grails-app/services/au/org/ala/ecodata/PermissionService.groovy +++ b/grails-app/services/au/org/ala/ecodata/PermissionService.groovy @@ -502,4 +502,180 @@ class PermissionService { result } + + // Permission: + // create / read / update / delete + // read_children / update_children / delete_children? + // query_children / + // view_reports? (this is specific to QLD hubs, might be useful for DoEE) + + // schema options: (non-relational) + // entityId / entityType? / owner (dbRef) + // embeddedCollection: [userId, [permissions]] + + // More relationalish? - might be better as embedded collection will get very big for some projects (e.g. sightings) + // EntityId, entity type, owner. _id (is this optional?), public?, viewableInOtherHubs...?, etc + // keep existing. + // Role / permission mapping. (for convenience of role / permission assignment) + boolean checkUserPermission(String userId, String entityId, String entityType, String permission) { + UserPermission userPermission = UserPermission.findByUserIdAndEntityIdAndEntityType(userId, entityId, entityType) + + // How many queries... 1. To get UserPermission 2. To see if public (if read) 3. To get owner... Fair bit of overhead, could add an extra table to contain owner/visiblity which would reduce this to 2? + + + // Need to check if the entity is "public" - maybe we could have a user permission entry for "all_users" or something for this? + // Does this only apply to documents? Or do we need it on sites etc? (probably, yes so project areas can be public?) + +// if (permission == 'read' /*&& entity.getVisibility == public*/) { +// return true +// } + + boolean hasPermission = false + if (userPermission && userPermission.hasPermission(permission)) { + hasPermission = true + } + else { + + Owner owner = findOwner(entityId, entityType) // Do we do an extra query here or update the permission table to directly include an owner reference? + + if (owner) { + // Does "read" on an owner automatically give access to children or do we need "read_children"? + // Same for "create" / "update" / "delete" + + // Recursively go up the permission tree + hasPermission = checkUserPermission(userId, owner.entityId, owner.entityType, permission) + } + } + hasPermission + } + class Owner { + Owner(String entityId, String entityType) { + this.entityId = entityId + this.entityType = entityType + } + String entityType + String entityId + } + + interface HasOwner { + Owner getOwner() + + } + + class OwnerFinder { + + Owner getOwner(Project project) { + if (project.hubId) { + new Owner(project.hubId, Hub.name) + } + } + Owner getOwner(Activity activity) { + if (activity.projectId) { + new Owner(activity.projectId, Project.name) + } + else if (activity.projectActivityId) { + new Owner(activity.projectActivityId, ProjectActivity.name) + } + null + } + Owner getOwner(Organisation organisation) { + null + } + Owner getOwner(Hub hub) { + null + } + Owner getOwner(Output output) { + new Owner(output.activityId, Activity.name) + } + Owner getOwner(ManagementUnit managementUnit) { + null + } + Owner getOwner(ProjectActivity projectActivity) { + new Owner(projectActivity.projectId, Project.name) + } + Owner getOwner(Report report) { + if (report.projectId) { + return new Owner(report.projectId, Project.name) + } + else if (report.managementUnitId) { + return new Owner(report.managementUnitId, ManagementUnit.name) + } + else if (report.organisationId) { + return new Owner(report.organisationId, Organisation.name) + } + null + } + + Owner getOwner(Document document) { + if (document.projectId) { + return new Owner(document.projectId, Project.name) + } + else if (document.activityId) { + return new Owner(document.activityId, Activity.name) + } + else if (document.outputId) { + return new Owner(document.outputId, Output.name) + } + else if (document.managementUnitId) { + return new Owner(document.managementUnitId, ManagementUnit.name) + } + else if (document.programId) { + return new Owner(document.programId, ManagementUnit.name) + } + else if (document.organisationId) { + return new Owner(document.organisationId) + } + else if (document.projectActivityId) { + return new Owner(document.projectActivityId, ProjectActivity.name) + } + } + + } + + private Owner findOwner(String entityId, String entityType) { + // Three options. + // 1. Denormalise UserPermission and have an owner reference. + // 2. Have extra table with this information. + // 3. Include a method for finding the owner based on the entity. + + // getOwner method in domain object itself. (could just return id & type to avoid extra query) + // entity with owner + Object entity = IdentifierHelper.load(entityId, entityType) + new OwnerFinder().getOwner(entity) + + + } + + // e.g for document + //@Permission(read, ..) + def annotationCheck() { + // get entity id, get permission, get entity type? + + // query user permission directly. + // if exists continue. + + // if not exists query entity record + // if permission is read and entity is public all good. + + // query permissions for user based on owner record, transform permission to "${permission}_children" e.g. read_children + // or is this implicit? + + + // how does role / permission mapping work? extra level of indirection? + + // Role table, with permissions per role. do we allow a user to have both a role and an extra permission? or does that make user admin too complex? + + + // userId, entity id, role, permissions...? + + checkPermission() + + + } + + /** Global permission check, used for API permission. Could also use a hub based check? */ + boolean checkUserPermission(String userId, String permission) { + checkUserPermission(userId, "api", "api", permission) + } + } diff --git a/scripts/misc/distinctFields.js b/scripts/misc/distinctFields.js new file mode 100644 index 000000000..ef27eb151 --- /dev/null +++ b/scripts/misc/distinctFields.js @@ -0,0 +1,12 @@ +mr = db.runCommand({ + "mapreduce" : "project", + "map" : function() { + if (this.status != 'deleted') { + for (var key in this) { emit(key, null); } + } + + }, + "reduce" : function(key, stuff) { return null; }, + "out": "project" + "_keys" +}); +db.project_keys.distinct('_id'); \ No newline at end of file diff --git a/src/main/groovy/au/org/ala/ecodata/AccessLevel.groovy b/src/main/groovy/au/org/ala/ecodata/AccessLevel.groovy index 0e3675096..5f83eb921 100644 --- a/src/main/groovy/au/org/ala/ecodata/AccessLevel.groovy +++ b/src/main/groovy/au/org/ala/ecodata/AccessLevel.groovy @@ -35,7 +35,21 @@ public enum AccessLevel { code = c; } - public int getCode() { + int getCode() { return code; } + + boolean includes(String permission) { + switch(permission) { + case 'read': + return code >= editor.code + case 'update': + return code >= editor.code + case 'administer': + return code >= admin.code + } + false + } + + } diff --git a/src/main/groovy/au/org/ala/ecodata/IdentifierHelper.groovy b/src/main/groovy/au/org/ala/ecodata/IdentifierHelper.groovy index b9d07a640..fd7a5d1bd 100644 --- a/src/main/groovy/au/org/ala/ecodata/IdentifierHelper.groovy +++ b/src/main/groovy/au/org/ala/ecodata/IdentifierHelper.groovy @@ -10,6 +10,46 @@ class IdentifierHelper { getEntityIdentifier(obj, obj.getClass().name) } + static Object load(String id, String className) { + Object entity = null + switch (className) { + case Project.class.name: + entity = Project.findByProjectId(id) + break + case Site.class.name: + entity = Site.findBySiteId(id) + break + case Activity.class.name: + entity = Activity.findByActivityId(id) + break + case Output.class.name: + entity = Output.findByOutputId(id) + break + case Document.class.name: + entity = Document.findByDocumentId(id) + break + case Score.class.name: + entity = Score.findByScoreId(id) + break + case Program.class.name: + entity = Program.findByProgramId(id) + break + case Organisation.class.name: + entity = Organisation.findByOrganisationId(id) + break + case Report.class.name: + entity = Report.findByReportId(id) + break + case Record.class.name: + entity = Record.findByRecord(id) + break + default: + throw new IllegalArgumentException("Unsupported entity type: ${entity}") + break + } + return entity + } + static String getEntityIdentifier(Object obj, String className) { String entityId switch (className) { diff --git a/src/main/groovy/au/org/ala/ecodata/Permissions.groovy b/src/main/groovy/au/org/ala/ecodata/Permissions.groovy new file mode 100644 index 000000000..b91726d16 --- /dev/null +++ b/src/main/groovy/au/org/ala/ecodata/Permissions.groovy @@ -0,0 +1,14 @@ +package au.org.ala.ecodata + +class Permissions { + + static String READ = 'read' + static String CREATE = 'create' + static String DELETE = 'delete' + static String UPDATE = 'update' + static String ADMINISTER = 'admin' + static String READ_CHILDREN = 'read_children' + static String UPDATE_CHILDREN = 'update_children' + static String DELETE_CHILDREN = 'delete_children' + +} diff --git a/src/main/groovy/au/org/ala/ecodata/graphql/ActivityFetcher.groovy b/src/main/groovy/au/org/ala/ecodata/graphql/ActivityFetcher.groovy new file mode 100644 index 000000000..3734499ba --- /dev/null +++ b/src/main/groovy/au/org/ala/ecodata/graphql/ActivityFetcher.groovy @@ -0,0 +1,53 @@ +package au.org.ala.ecodata.graphql + +import au.org.ala.ecodata.* +import graphql.schema.DataFetchingEnvironment +import org.elasticsearch.action.search.SearchResponse + +class ActivityFetcher implements graphql.schema.DataFetcher> { + + public ActivityFetcher(ElasticSearchService elasticSearchService, PermissionService permissionService) { + + this.elasticSearchService = elasticSearchService + this.permissionService = permissionService + } + + + PermissionService permissionService + ElasticSearchService elasticSearchService + ActivityService activityService + + + @Override + List get(DataFetchingEnvironment environment) throws Exception { + + String userId = environment.context.user?.userId + String query = environment.arguments.term ?:"*:*" + SearchResponse searchResponse = elasticSearchService.searchWithSecurity(null, query, [include:'projectId', max:65000], ElasticIndex.HOMEPAGE_INDEX) + + List projectIds = searchResponse.hits.hits.collect{it.source.projectId} + + + Activity.findAllByProjectIdInList(projectIds, [max:10]) + + // Do we want to restrict API use based on hubs? +// if (!userId || !environment.context.permissionService.checkUserPermission(userId, environment.fieldDefinition.name, "API", "read")) { +// throw new GraphQLException("No permission") +// } + + // Search ES, applying the user role in the process... + + + // What should happen if we get a "show me all" type query? + + // Should we return the public view for all public projects (is that all projects?) we have data for? + + // e.g. should the role check only apply during the mapping phase? In which case we need a bulk query of permissions to determine a list of project ids we can get full resolution data for? + // Or do we do two queries, one for full resolution, one for the rest (how do we sort/page if we do two queries?) + + + + + } + +} diff --git a/src/main/groovy/au/org/ala/ecodata/graphql/EcodataGraphQLContextBuilder.groovy b/src/main/groovy/au/org/ala/ecodata/graphql/EcodataGraphQLContextBuilder.groovy new file mode 100644 index 000000000..bb4ad7be9 --- /dev/null +++ b/src/main/groovy/au/org/ala/ecodata/graphql/EcodataGraphQLContextBuilder.groovy @@ -0,0 +1,26 @@ +package au.org.ala.ecodata.graphql + +import au.org.ala.ecodata.PermissionService +import au.org.ala.ecodata.UserService +import grails.core.GrailsApplication +import org.grails.gorm.graphql.plugin.DefaultGraphQLContextBuilder +import org.grails.web.servlet.mvc.GrailsWebRequest +import org.springframework.beans.factory.annotation.Autowired + +class EcodataGraphQLContextBuilder extends DefaultGraphQLContextBuilder { + + @Autowired + GrailsApplication grailsApplication + + @Override + Map buildContext(GrailsWebRequest request) { + Map context = super.buildContext(request) + + context.grailsApplication = grailsApplication + context.userService = grailsApplication.mainContext.userService + context.user = UserService.currentUser() + + context.permissionService = grailsApplication.mainContext.permissionService + context + } +} diff --git a/src/main/groovy/au/org/ala/ecodata/graphql/EcodataGraphQLCustomiser.groovy b/src/main/groovy/au/org/ala/ecodata/graphql/EcodataGraphQLCustomiser.groovy new file mode 100644 index 000000000..933bc2986 --- /dev/null +++ b/src/main/groovy/au/org/ala/ecodata/graphql/EcodataGraphQLCustomiser.groovy @@ -0,0 +1,80 @@ +package au.org.ala.ecodata.graphql + +import graphql.schema.Coercing +import graphql.schema.DataFetchingEnvironment +import graphql.schema.GraphQLFieldDefinition +import graphql.schema.GraphQLInterfaceType +import graphql.schema.GraphQLObjectType +import graphql.schema.GraphQLScalarType +import graphql.schema.GraphQLType +import org.bson.types.ObjectId +import org.grails.datastore.mapping.model.PersistentEntity +import org.grails.gorm.graphql.fetcher.GraphQLDataFetcherType +import org.grails.gorm.graphql.fetcher.manager.GraphQLDataFetcherManager +import org.grails.gorm.graphql.interceptor.GraphQLFetcherInterceptor +import org.grails.gorm.graphql.interceptor.GraphQLSchemaInterceptor +import org.grails.gorm.graphql.interceptor.manager.GraphQLInterceptorManager +import org.grails.gorm.graphql.plugin.GraphQLPostProcessor +import org.grails.gorm.graphql.types.GraphQLTypeManager + +class EcodataGraphQLCustomiser extends GraphQLPostProcessor { + + @Override + void doWith(GraphQLTypeManager graphQLTypeManager) { + // GraphQL type conversion for Mongo ObjectIds + graphQLTypeManager.registerType(ObjectId, new GraphQLScalarType("ObjectId", "Hex representation of a Mongo object id", new ObjectIdConverter())) + graphQLTypeManager.registerType(Object, new GraphQLScalarType("object", "", new ObjectConverter())) + //graphQLTypeManager.registerType(Map, GraphQLInterfaceType.newInterface().) + } + + @Override + void doWith(GraphQLInterceptorManager graphQLInterceptorManager) { + Set entities = new HashSet() + graphQLInterceptorManager.registerInterceptor(new GraphQLSchemaInterceptor() { + @Override + void interceptEntity(PersistentEntity entity, List queryFields, List mutationFields) { + if (entities.contains(entity.name)) { + queryFields.clear() + mutationFields.clear() + } + else { + entities.add(entity.name) + } + } + + @Override + void interceptSchema(GraphQLObjectType.Builder queryType, GraphQLObjectType.Builder mutationType, Set additionalTypes) { + println queryType + + + } + }) + + graphQLInterceptorManager.registerInterceptor(Object, new GraphQLFetcherInterceptor() { + @Override + boolean onQuery(DataFetchingEnvironment environment, GraphQLDataFetcherType type) { + println "Running query $type" + + return true + } + + @Override + boolean onMutation(DataFetchingEnvironment environment, GraphQLDataFetcherType type) { + return false + } + + @Override + boolean onCustomQuery(String name, DataFetchingEnvironment environment) { + println name + return true + } + + @Override + boolean onCustomMutation(String name, DataFetchingEnvironment environment) { + return false + } + }) + } + + +} diff --git a/src/main/groovy/au/org/ala/ecodata/graphql/ObjectConverter.groovy b/src/main/groovy/au/org/ala/ecodata/graphql/ObjectConverter.groovy new file mode 100644 index 000000000..ab0a3f023 --- /dev/null +++ b/src/main/groovy/au/org/ala/ecodata/graphql/ObjectConverter.groovy @@ -0,0 +1,37 @@ +package au.org.ala.ecodata.graphql + +import graphql.language.StringValue +import graphql.schema.Coercing +import graphql.schema.CoercingParseValueException +import graphql.schema.CoercingSerializeException +import org.bson.types.ObjectId + +class ObjectConverter implements Coercing { + + protected Optional convert(Object input) { + + Optional.empty() + + } + + @Override + ObjectId serialize(Object input) { + convert(input).orElseThrow( { + throw new CoercingSerializeException("Could not convert ${input.class.name} to an ObjectId") + }) + } + + @Override + Object parseValue(Object input) { + convert(input).orElseThrow( { + throw new CoercingParseValueException("Could not convert ${input.class.name} to an ObjectId") + }) + } + + @Override + ObjectId parseLiteral(Object input) { + null + } + + +} diff --git a/src/main/groovy/au/org/ala/ecodata/graphql/ObjectIdConverter.groovy b/src/main/groovy/au/org/ala/ecodata/graphql/ObjectIdConverter.groovy new file mode 100644 index 000000000..b5f92cca8 --- /dev/null +++ b/src/main/groovy/au/org/ala/ecodata/graphql/ObjectIdConverter.groovy @@ -0,0 +1,56 @@ +package au.org.ala.ecodata.graphql + +import graphql.language.StringValue +import graphql.schema.Coercing +import graphql.schema.CoercingParseValueException +import graphql.schema.CoercingSerializeException +import org.bson.types.ObjectId + +class ObjectIdConverter implements Coercing { + + protected Optional convert(Object input) { + if (input instanceof ObjectId) { + Optional.of((ObjectId) input) + } + else if (input instanceof String) { + parseObjectId((String) input) + } + else { + Optional.empty() + } + } + + @Override + ObjectId serialize(Object input) { + convert(input).orElseThrow( { + throw new CoercingSerializeException("Could not convert ${input.class.name} to an ObjectId") + }) + } + + @Override + ObjectId parseValue(Object input) { + convert(input).orElseThrow( { + throw new CoercingParseValueException("Could not convert ${input.class.name} to an ObjectId") + }) + } + + @Override + ObjectId parseLiteral(Object input) { + if (input instanceof StringValue) { + parseObjectId(((StringValue) input).value).orElse(null) + } + else { + null + } + } + + protected Optional parseObjectId(String input) { + if (ObjectId.isValid(input)) { + Optional.of(new ObjectId(input)) + } + else { + Optional.empty() + } + } + +} diff --git a/src/main/groovy/au/org/ala/ecodata/graphql/ProjectGraphQLMapper.groovy b/src/main/groovy/au/org/ala/ecodata/graphql/ProjectGraphQLMapper.groovy new file mode 100644 index 000000000..79984d17f --- /dev/null +++ b/src/main/groovy/au/org/ala/ecodata/graphql/ProjectGraphQLMapper.groovy @@ -0,0 +1,93 @@ +package au.org.ala.ecodata.graphql + +import au.org.ala.ecodata.Document +import au.org.ala.ecodata.Project +import au.org.ala.ecodata.Report +import au.org.ala.ecodata.Site +import au.org.ala.ecodata.Status +import grails.gorm.DetachedCriteria +import grails.util.Holders +import graphql.schema.DataFetcher +import graphql.schema.DataFetchingEnvironment +import org.grails.gorm.graphql.entity.dsl.GraphQLMapping +import org.grails.gorm.graphql.fetcher.impl.ClosureDataFetchingEnvironment +import org.grails.gorm.graphql.fetcher.impl.SingleEntityDataFetcher + +class ProjectGraphQLMapper { + + static graphqlMapping() { + GraphQLMapping.lazy { + // Disable default operations, including get as we only want to expose UUIDs in the API not internal ones + operations.get.enabled false + operations.list.enabled true + operations.list.paginate(true) + operations.count.enabled false + operations.create.enabled true + operations.update.enabled false + operations.delete.enabled false + + + List restrictedProperties = [] + restrictedProperties.each { String prop -> + property(prop) { + dataFetcher { Project project, ClosureDataFetchingEnvironment env -> + boolean canRead = env.environment.context.acl.canRead(env.source, project) + if (canRead) { + return project[prop] + } + return null + } + } + } + +// property('meriPlan') { Project project -> +// +// } + add('documents', [Document]) { + dataFetcher { Project project, ClosureDataFetchingEnvironment env -> + Document.findAllByProjectIdAndStatusNotEqual(project.projectId, Status.DELETED) + } + } + add('reports', [Report]) { + dataFetcher { Project project -> + Report.findAllByProjectIdAndStatusNotEqual(project.projectId, Status.DELETED) + } + } + + add('sites', [Site]) { + dataFetcher { Project project -> + Site.findAllByProjectsAndStatusNotEqual(project.projectId, Status.DELETED) + } + } + + // get project by ID + query('project', Project) { + argument('projectId', String) + dataFetcher(new SingleEntityDataFetcher(Project.gormPersistentEntity) { + @Override + protected DetachedCriteria buildCriteria(DataFetchingEnvironment environment) { + Project.where { projectId == environment.getArgument('projectId') } + } + }) + } + + query('projects', [Project]) { + argument('term', String) + dataFetcher(new DataFetcher() { + @Override + Object get(DataFetchingEnvironment environment) throws Exception { + ProjectGraphQLMapper.buildTestFetcher().get(environment) + } + }) + } + + } + } + + + static ProjectsFetcher buildTestFetcher() { + + new ProjectsFetcher(Holders.applicationContext.projectService, Holders.applicationContext.elasticSearchService, Holders.applicationContext.permissionService) + + } +} diff --git a/src/main/groovy/au/org/ala/ecodata/graphql/ProjectsFetcher.groovy b/src/main/groovy/au/org/ala/ecodata/graphql/ProjectsFetcher.groovy new file mode 100644 index 000000000..c9ccc8cc9 --- /dev/null +++ b/src/main/groovy/au/org/ala/ecodata/graphql/ProjectsFetcher.groovy @@ -0,0 +1,86 @@ +package au.org.ala.ecodata.graphql + +import au.org.ala.ecodata.* +import com.mongodb.client.FindIterable +import com.mongodb.client.model.Filters +import graphql.schema.DataFetchingEnvironment +import org.elasticsearch.action.search.SearchResponse + +class ProjectsFetcher implements graphql.schema.DataFetcher> { + + public ProjectsFetcher(ProjectService projectService, ElasticSearchService elasticSearchService, PermissionService permissionService) { + this.projectService = projectService + this.elasticSearchService = elasticSearchService + this.permissionService = permissionService + } + + + PermissionService permissionService + ElasticSearchService elasticSearchService + + ProjectService projectService + + @Override + List get(DataFetchingEnvironment environment) throws Exception { + + + // Search ES, applying the user role in the process... + + + // What should happen if we get a "show me all" type query? + + // Should we return the public view for all public projects (is that all projects?) we have data for? + + // e.g. should the role check only apply during the mapping phase? In which case we need a bulk query of permissions to determine a list of project ids we can get full resolution data for? + // Or do we do two queries, one for full resolution, one for the rest (how do we sort/page if we do two queries?) + + + + return queryElasticSearch(environment) + } + + private List queryElasticSearch(DataFetchingEnvironment environment) { + // Retrieve projectIds only from elasticsearch. + + + // add pagination results. + String userId = environment.context.userId ?: '1493' + String query = environment.arguments.term ?:"*:*" + SearchResponse searchResponse = elasticSearchService.searchWithSecurity(userId, query, [include:'projectId'], ElasticIndex.HOMEPAGE_INDEX) + + List projectIds = searchResponse.hits.hits.collect{it.source.projectId} + + // Split projects into those the user has full read permission & those they don't + List publicProjectIds = [] + List fullProjectIds = [] + + + // Alternative here is to also return the userIds from the ES query and see if the user is in the result. + // we could also map directly from the ES, but this would require a different approach (maybe creating + // and binding the domain objects from the ES data?) + projectIds.each { + boolean readable = permissionService.checkUserPermission(userId, it, Project.name, "read") + readable ? publicProjectIds << it : publicProjectIds << it + } + + Map publicView = [name:true, description:true, projectId:true] + + Map publicProjects = [:] + FindIterable findIterable = Project.find(Filters.in("projectId", publicProjectIds)) + findIterable.projection(publicView).each { Project project -> + publicProjects.put(project.projectId, project) + } + + Map fullProjects = [:] + findIterable = Project.find(Filters.in("projectId", fullProjectIds)) + findIterable.each { Project project -> + fullProjects.put(project.projectId, project) + } + + List projects = projectIds.collect { + publicProjects.containsKey(it) ? publicProjects[it] : fullProjects[it] + } + + projects + } +} diff --git a/src/main/groovy/au/org/ala/ecodata/graphql/SitesFetcher.groovy b/src/main/groovy/au/org/ala/ecodata/graphql/SitesFetcher.groovy new file mode 100644 index 000000000..e8999d497 --- /dev/null +++ b/src/main/groovy/au/org/ala/ecodata/graphql/SitesFetcher.groovy @@ -0,0 +1,78 @@ +package au.org.ala.ecodata.graphql + +import au.org.ala.ecodata.* +import graphql.GraphQLException +import graphql.schema.DataFetchingEnvironment +import org.elasticsearch.action.search.SearchResponse + +import java.security.AccessControlException + +class SitesFetcher implements graphql.schema.DataFetcher> { + + public SitesFetcher(ProjectService projectService, ElasticSearchService elasticSearchService, PermissionService permissionService) { + this.projectService = projectService + this.elasticSearchService = elasticSearchService + this.permissionService = permissionService + } + + + PermissionService permissionService + ElasticSearchService elasticSearchService + + ProjectService projectService + + @Override + List get(DataFetchingEnvironment environment) throws Exception { + + String userId = environment.context.user?.userId + + // Do we want to restrict API use based on hubs? +// if (!userId || !environment.context.permissionService.checkUserPermission(userId, environment.fieldDefinition.name, "API", "read")) { +// throw new GraphQLException("No permission") +// } + + // Search ES, applying the user role in the process... + + + // What should happen if we get a "show me all" type query? + + // Should we return the public view for all public projects (is that all projects?) we have data for? + + // e.g. should the role check only apply during the mapping phase? In which case we need a bulk query of permissions to determine a list of project ids we can get full resolution data for? + // Or do we do two queries, one for full resolution, one for the rest (how do we sort/page if we do two queries?) + + + + return queryElasticSearch(environment) + } + + private List queryElasticSearch(DataFetchingEnvironment environment) { + // Retrieve projectIds only from elasticsearch. + + // Need to only retrieve sites for which we actually have access to. It's a bit tricky as pagination can mean we + // can't post process data. e.g. we want from 100-200 and are post processing we have to query from 0, and post filter, + // throwing away the first 100. + + // Might have to include an ACL in ES or the database to make it work properly. Or use projects as ACL - this may not work, as + // someone with access to all MERIT projects could result in a large project list going to the sites query (e.g. 3500...) + // I assume we want to be able to query sites based on projects anyway though - e.g. programs. + + // ES limits terms query to ~65,000 by default, which may eventually cause problems that would require building a + // new index. + + // Another way to deal with this would be to limit site / activity queries to hub based ones and include hub in the + // index? + + // Another way is to build the query with hub information in the query? + // e.g. hub in or in ACL? Do we need a way to give access to all Hubs explicity? + + // Otherwise we need to be adding hub permissions to the ACL, which will require re-indexing a lot of projects when + // hub permissions change? (maybe that's OK)? + + SearchResponse searchResponse = elasticSearchService.searchWithSecurity(null, "*:*", [include:'projectId', max:65000], ElasticIndex.HOMEPAGE_INDEX) + + List projectIds = searchResponse.hits.hits.collect{it.source.projectId} + + Site.findAllByProjectsInList(projectIds, [max:10]) + } +} From 50161e1e39c56ca36f63ae482c70fb2721c8eb66 Mon Sep 17 00:00:00 2001 From: Chris Date: Fri, 13 Nov 2020 11:23:03 +1100 Subject: [PATCH 002/144] Changed version to something unique. --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 9f4b234f0..c4ea2f604 100644 --- a/build.gradle +++ b/build.gradle @@ -20,7 +20,7 @@ plugins { id "com.gorylenko.gradle-git-properties" version "2.0.0" } -version "2.0-SNAPSHOT" +version "2.0-graphql-SNAPSHOT" group "au.org.ala" description "Ecodata" From 17b738063e7b77d53a0836396190c7d45d6aa592 Mon Sep 17 00:00:00 2001 From: yasima-csiro Date: Mon, 30 Nov 2020 08:29:03 +1100 Subject: [PATCH 003/144] Add graphql API implementation --- grails-app/conf/spring/resources.groovy | 8 +- .../domain/au/org/ala/ecodata/Activity.groovy | 60 +------------- .../au/org/ala/ecodata/ActivityForm.groovy | 3 + .../au/org/ala/ecodata/FormSection.groovy | 13 +++ .../domain/au/org/ala/ecodata/Output.groovy | 37 ++++----- .../domain/au/org/ala/ecodata/Project.groovy | 14 +++- .../graphql/EcodataGraphQLCustomiser.groovy | 18 ++++- .../converters/MeriPlanConverter.groovy | 38 +++++++++ .../{ => converters}/ObjectConverter.groovy | 10 +-- .../{ => converters}/ObjectIdConverter.groovy | 2 +- .../converters/OutputDataConverter.groovy | 38 +++++++++ .../graphql/converters/SchemaConverter.groovy | 37 +++++++++ .../SectionTemplateConverter.groovy | 38 +++++++++ .../converters/SummaryConverter.groovy | 38 +++++++++ .../{ => fetchers}/ActivityFetcher.groovy | 47 ++++++++++- .../graphql/fetchers/OutputFetcher.groovy | 70 ++++++++++++++++ .../{ => fetchers}/ProjectsFetcher.groovy | 2 +- .../{ => fetchers}/SitesFetcher.groovy | 5 +- .../mappers/ActivityFormGraphQLMapper.groovy | 54 +++++++++++++ .../mappers/ActivityGraphQLMapper.groovy | 80 +++++++++++++++++++ .../mappers/FormSectionGraphQLMapper.groovy | 28 +++++++ .../mappers/OutputGraphQLMapper.groovy | 55 +++++++++++++ .../{ => mappers}/ProjectGraphQLMapper.groovy | 14 +++- .../ecodata/graphql/models/MeriPlan.groovy | 7 ++ .../ecodata/graphql/models/OutputData.groovy | 13 +++ .../ala/ecodata/graphql/models/Schema.groovy | 9 +++ .../graphql/models/SectionTemplate.groovy | 6 ++ .../ala/ecodata/graphql/models/Summary.groovy | 7 ++ 28 files changed, 647 insertions(+), 104 deletions(-) create mode 100644 src/main/groovy/au/org/ala/ecodata/graphql/converters/MeriPlanConverter.groovy rename src/main/groovy/au/org/ala/ecodata/graphql/{ => converters}/ObjectConverter.groovy (78%) rename src/main/groovy/au/org/ala/ecodata/graphql/{ => converters}/ObjectIdConverter.groovy (96%) create mode 100644 src/main/groovy/au/org/ala/ecodata/graphql/converters/OutputDataConverter.groovy create mode 100644 src/main/groovy/au/org/ala/ecodata/graphql/converters/SchemaConverter.groovy create mode 100644 src/main/groovy/au/org/ala/ecodata/graphql/converters/SectionTemplateConverter.groovy create mode 100644 src/main/groovy/au/org/ala/ecodata/graphql/converters/SummaryConverter.groovy rename src/main/groovy/au/org/ala/ecodata/graphql/{ => fetchers}/ActivityFetcher.groovy (50%) create mode 100644 src/main/groovy/au/org/ala/ecodata/graphql/fetchers/OutputFetcher.groovy rename src/main/groovy/au/org/ala/ecodata/graphql/{ => fetchers}/ProjectsFetcher.groovy (98%) rename src/main/groovy/au/org/ala/ecodata/graphql/{ => fetchers}/SitesFetcher.groovy (96%) create mode 100644 src/main/groovy/au/org/ala/ecodata/graphql/mappers/ActivityFormGraphQLMapper.groovy create mode 100644 src/main/groovy/au/org/ala/ecodata/graphql/mappers/ActivityGraphQLMapper.groovy create mode 100644 src/main/groovy/au/org/ala/ecodata/graphql/mappers/FormSectionGraphQLMapper.groovy create mode 100644 src/main/groovy/au/org/ala/ecodata/graphql/mappers/OutputGraphQLMapper.groovy rename src/main/groovy/au/org/ala/ecodata/graphql/{ => mappers}/ProjectGraphQLMapper.groovy (90%) create mode 100644 src/main/groovy/au/org/ala/ecodata/graphql/models/MeriPlan.groovy create mode 100644 src/main/groovy/au/org/ala/ecodata/graphql/models/OutputData.groovy create mode 100644 src/main/groovy/au/org/ala/ecodata/graphql/models/Schema.groovy create mode 100644 src/main/groovy/au/org/ala/ecodata/graphql/models/SectionTemplate.groovy create mode 100644 src/main/groovy/au/org/ala/ecodata/graphql/models/Summary.groovy diff --git a/grails-app/conf/spring/resources.groovy b/grails-app/conf/spring/resources.groovy index ff1fb5b2b..93fa8ee37 100644 --- a/grails-app/conf/spring/resources.groovy +++ b/grails-app/conf/spring/resources.groovy @@ -1,8 +1,9 @@ -import au.org.ala.ecodata.graphql.ActivityFetcher +import au.org.ala.ecodata.graphql.fetchers.ActivityFetcher import au.org.ala.ecodata.graphql.EcodataGraphQLContextBuilder import au.org.ala.ecodata.graphql.EcodataGraphQLCustomiser -import au.org.ala.ecodata.graphql.ProjectsFetcher -import au.org.ala.ecodata.graphql.SitesFetcher +import au.org.ala.ecodata.graphql.fetchers.OutputFetcher +import au.org.ala.ecodata.graphql.fetchers.ProjectsFetcher +import au.org.ala.ecodata.graphql.fetchers.SitesFetcher // Place your Spring DSL code here beans = { @@ -10,5 +11,6 @@ beans = { projectsFetcher(ProjectsFetcher) sitesFetcher(SitesFetcher) activitiesFetcher(ActivityFetcher) + outputFetcher(OutputFetcher) graphQLContextBuilder(EcodataGraphQLContextBuilder) } diff --git a/grails-app/domain/au/org/ala/ecodata/Activity.groovy b/grails-app/domain/au/org/ala/ecodata/Activity.groovy index b5fbd9ddf..4dd4c6829 100644 --- a/grails-app/domain/au/org/ala/ecodata/Activity.groovy +++ b/grails-app/domain/au/org/ala/ecodata/Activity.groovy @@ -1,5 +1,6 @@ package au.org.ala.ecodata +import au.org.ala.ecodata.graphql.mappers.ActivityGraphQLMapper import grails.util.Holders import graphql.schema.DataFetcher import graphql.schema.DataFetchingEnvironment @@ -29,64 +30,7 @@ class Activity { activities may have 0..n Outputs - these are mapped from the Output side */ - static graphql = GraphQLMapping.lazy { - // Disable default operations, including get as we only want to expose UUIDs in the API not internal ones - operations.get.enabled false - operations.list.enabled true - operations.count.enabled false - operations.create.enabled false - operations.update.enabled false - operations.delete.enabled false - - add('outputs', [Output]) { - dataFetcher { Activity activity -> - Output.findAllByActivityId(activity.activityId) - } - input false - } - - add('data', 'Data') { - ActivityForm form = Holders.grailsApplication.mainContext.activityFormService.findActivityForm("RLP Output Report", 1) - input false - type { - form.sections.each { FormSection section -> - if (section.template.dataModel) { - - String typeName = section.name.replaceAll("[ |-]", "") - field(typeName, typeName) { - - for (Map dataModelItem : section.template.dataModel) { - field(dataModelItem.name, String) { - if (dataModelItem.description) { - description(dataModelItem.description) - } - } - } - } - } - - } - } - dataFetcher { Activity activity -> - Map result = Output.findAllByActivityId(activity.activityId).collectEntries { - [(it.name.replaceAll("[ |-]", "")):it.data] - } - result - } - - - } - query('activities', [Activity]) { - argument('term', String) - dataFetcher(new DataFetcher() { - @Override - Object get(DataFetchingEnvironment environment) throws Exception { - environment.context.grailsApplication.mainContext.activitiesFetcher.get(environment) - } - }) - } - } - + static graphql = ActivityGraphQLMapper.graphqlMapping() static mapping = { activityId index: true diff --git a/grails-app/domain/au/org/ala/ecodata/ActivityForm.groovy b/grails-app/domain/au/org/ala/ecodata/ActivityForm.groovy index 81591ca9f..7d982f781 100644 --- a/grails-app/domain/au/org/ala/ecodata/ActivityForm.groovy +++ b/grails-app/domain/au/org/ala/ecodata/ActivityForm.groovy @@ -1,5 +1,6 @@ package au.org.ala.ecodata +import au.org.ala.ecodata.graphql.mappers.ActivityFormGraphQLMapper import org.bson.types.ObjectId /** @@ -7,6 +8,8 @@ import org.bson.types.ObjectId */ class ActivityForm { + static graphql = ActivityFormGraphQLMapper.graphqlMapping() + /** The list of properties to be used when binding request data to an ActivityForm */ static bindingProperties = ['type', 'version', 'category', 'supportsSites', 'supportsPhotoPoints', 'gmsId', 'minOptionalSectionsCompleted', 'activationDate', 'sections'] diff --git a/grails-app/domain/au/org/ala/ecodata/FormSection.groovy b/grails-app/domain/au/org/ala/ecodata/FormSection.groovy index b7ef9e67b..63f46b9d4 100644 --- a/grails-app/domain/au/org/ala/ecodata/FormSection.groovy +++ b/grails-app/domain/au/org/ala/ecodata/FormSection.groovy @@ -1,7 +1,12 @@ package au.org.ala.ecodata +import au.org.ala.ecodata.graphql.mappers.FormSectionGraphQLMapper +import au.org.ala.ecodata.graphql.models.SectionTemplate + class FormSection { + static graphql = FormSectionGraphQLMapper.graphqlMapping() + static constraints = { title nullable: true modelName nullable: true @@ -26,4 +31,12 @@ class FormSection { boolean optional = false boolean collapsedByDefault = false + SectionTemplate getSectionTemplate() { + SectionTemplate outputData = new SectionTemplate() + if(template) { + outputData.sectionTemplate = template + } + return outputData + } + } diff --git a/grails-app/domain/au/org/ala/ecodata/Output.groovy b/grails-app/domain/au/org/ala/ecodata/Output.groovy index fca61ce8c..11a19c02d 100644 --- a/grails-app/domain/au/org/ala/ecodata/Output.groovy +++ b/grails-app/domain/au/org/ala/ecodata/Output.groovy @@ -1,5 +1,8 @@ package au.org.ala.ecodata +import au.org.ala.ecodata.graphql.mappers.OutputGraphQLMapper +import au.org.ala.ecodata.graphql.models.OutputData +import au.org.ala.ecodata.graphql.models.KeyValue import grails.util.Holders import graphql.schema.DataFetcher import graphql.schema.DataFetchingEnvironment @@ -8,29 +11,7 @@ import org.grails.gorm.graphql.entity.dsl.GraphQLMapping class Output { - static graphql = GraphQLMapping.lazy { - // Disable default operations, including get as we only want to expose UUIDs in the API not internal ones - operations.get.enabled false - operations.list.enabled true - operations.count.enabled false - operations.create.enabled false - operations.update.enabled false - operations.delete.enabled false - - -// add('data', Map) { -// dataFetcher { Output output -> -// Activity activity = Activity.findByActivityId(output.activityId) -// ActivityForm form = Holders.grailsApplication.mainContext.activityFormService.findActivityForm(activity.type, activity.formVersion) -// form.sections.each { FormSection section -> -// section.template.dataModel.each { -// -// } -// } -// } -// input false -// } - } + static graphql = OutputGraphQLMapper.graphqlMapping() /* Associations: @@ -58,4 +39,14 @@ class Output { assessmentDate nullable: true name nullable: true } + + OutputData getData(def data) { + OutputData outputData = new OutputData(dataList: new ArrayList()) + if(data) { + data.each() { + outputData.dataList.add(new KeyValue(key: it.key, value: it.value)) + } + } + return outputData + } } diff --git a/grails-app/domain/au/org/ala/ecodata/Project.groovy b/grails-app/domain/au/org/ala/ecodata/Project.groovy index e5c96ca97..14813269a 100644 --- a/grails-app/domain/au/org/ala/ecodata/Project.groovy +++ b/grails-app/domain/au/org/ala/ecodata/Project.groovy @@ -1,6 +1,7 @@ package au.org.ala.ecodata -import au.org.ala.ecodata.graphql.ProjectGraphQLMapper +import au.org.ala.ecodata.graphql.models.MeriPlan +import au.org.ala.ecodata.graphql.mappers.ProjectGraphQLMapper import org.bson.types.ObjectId import org.joda.time.DateTime import org.joda.time.Days @@ -198,5 +199,16 @@ class Project { baseLayer nullable: true hub nullable: true } + + MeriPlan getMeriPlan() { + if(!custom) { + return null + } + + MeriPlan meriPlan = new MeriPlan() + meriPlan.details = custom.get("details") + meriPlan.outputTargets = this.outputTargets + return meriPlan + } } diff --git a/src/main/groovy/au/org/ala/ecodata/graphql/EcodataGraphQLCustomiser.groovy b/src/main/groovy/au/org/ala/ecodata/graphql/EcodataGraphQLCustomiser.groovy index 933bc2986..b5ed7ff15 100644 --- a/src/main/groovy/au/org/ala/ecodata/graphql/EcodataGraphQLCustomiser.groovy +++ b/src/main/groovy/au/org/ala/ecodata/graphql/EcodataGraphQLCustomiser.groovy @@ -1,6 +1,17 @@ package au.org.ala.ecodata.graphql -import graphql.schema.Coercing +import au.org.ala.ecodata.graphql.converters.MeriPlanConverter +import au.org.ala.ecodata.graphql.converters.ObjectConverter +import au.org.ala.ecodata.graphql.converters.ObjectIdConverter +import au.org.ala.ecodata.graphql.converters.OutputDataConverter +import au.org.ala.ecodata.graphql.converters.SchemaConverter +import au.org.ala.ecodata.graphql.converters.SectionTemplateConverter +import au.org.ala.ecodata.graphql.converters.SummaryConverter +import au.org.ala.ecodata.graphql.models.MeriPlan +import au.org.ala.ecodata.graphql.models.OutputData +import au.org.ala.ecodata.graphql.models.Schema +import au.org.ala.ecodata.graphql.models.SectionTemplate +import au.org.ala.ecodata.graphql.models.Summary import graphql.schema.DataFetchingEnvironment import graphql.schema.GraphQLFieldDefinition import graphql.schema.GraphQLInterfaceType @@ -25,6 +36,11 @@ class EcodataGraphQLCustomiser extends GraphQLPostProcessor { graphQLTypeManager.registerType(ObjectId, new GraphQLScalarType("ObjectId", "Hex representation of a Mongo object id", new ObjectIdConverter())) graphQLTypeManager.registerType(Object, new GraphQLScalarType("object", "", new ObjectConverter())) //graphQLTypeManager.registerType(Map, GraphQLInterfaceType.newInterface().) + graphQLTypeManager.registerType(MeriPlan, new GraphQLScalarType("MeriPlan", "", new MeriPlanConverter())) + graphQLTypeManager.registerType(OutputData, new GraphQLScalarType("OutputData", "", new OutputDataConverter())) + graphQLTypeManager.registerType(SectionTemplate, new GraphQLScalarType("SectionTemplate", "", new SectionTemplateConverter())) + graphQLTypeManager.registerType(Summary, new GraphQLScalarType("Summary", "", new SummaryConverter())) + graphQLTypeManager.registerType(Schema, new GraphQLScalarType("Schema", "", new SchemaConverter())) } @Override diff --git a/src/main/groovy/au/org/ala/ecodata/graphql/converters/MeriPlanConverter.groovy b/src/main/groovy/au/org/ala/ecodata/graphql/converters/MeriPlanConverter.groovy new file mode 100644 index 000000000..aa357bf55 --- /dev/null +++ b/src/main/groovy/au/org/ala/ecodata/graphql/converters/MeriPlanConverter.groovy @@ -0,0 +1,38 @@ +package au.org.ala.ecodata.graphql.converters + +import au.org.ala.ecodata.graphql.models.MeriPlan +import graphql.schema.Coercing +import graphql.schema.CoercingParseValueException +import graphql.schema.CoercingSerializeException + +class MeriPlanConverter implements Coercing { + + protected Optional convert(Object input) { + if (input instanceof MeriPlan) { + Optional.of((MeriPlan) input) + } + else { + Optional.empty() + } + } + + @Override + MeriPlan serialize(Object input) { + convert(input).orElseThrow( { + throw new CoercingSerializeException("Could not convert ${input.class.name} to a MeriPlan") + }) + } + + @Override + MeriPlan parseValue(Object input) { + convert(input).orElseThrow( { + throw new CoercingParseValueException("Could not convert ${input.class.name} to a MeriPlan") + }) + } + + @Override + MeriPlan parseLiteral(Object input) { + null + } +} + diff --git a/src/main/groovy/au/org/ala/ecodata/graphql/ObjectConverter.groovy b/src/main/groovy/au/org/ala/ecodata/graphql/converters/ObjectConverter.groovy similarity index 78% rename from src/main/groovy/au/org/ala/ecodata/graphql/ObjectConverter.groovy rename to src/main/groovy/au/org/ala/ecodata/graphql/converters/ObjectConverter.groovy index ab0a3f023..93b6fef5f 100644 --- a/src/main/groovy/au/org/ala/ecodata/graphql/ObjectConverter.groovy +++ b/src/main/groovy/au/org/ala/ecodata/graphql/converters/ObjectConverter.groovy @@ -1,6 +1,6 @@ -package au.org.ala.ecodata.graphql +package au.org.ala.ecodata.graphql.converters + -import graphql.language.StringValue import graphql.schema.Coercing import graphql.schema.CoercingParseValueException import graphql.schema.CoercingSerializeException @@ -15,16 +15,16 @@ class ObjectConverter implements Coercing { } @Override - ObjectId serialize(Object input) { + Object serialize(Object input) { convert(input).orElseThrow( { - throw new CoercingSerializeException("Could not convert ${input.class.name} to an ObjectId") + throw new CoercingSerializeException("Could not convert ${input.class.name} to an Object") }) } @Override Object parseValue(Object input) { convert(input).orElseThrow( { - throw new CoercingParseValueException("Could not convert ${input.class.name} to an ObjectId") + throw new CoercingParseValueException("Could not convert ${input.class.name} to an Object") }) } diff --git a/src/main/groovy/au/org/ala/ecodata/graphql/ObjectIdConverter.groovy b/src/main/groovy/au/org/ala/ecodata/graphql/converters/ObjectIdConverter.groovy similarity index 96% rename from src/main/groovy/au/org/ala/ecodata/graphql/ObjectIdConverter.groovy rename to src/main/groovy/au/org/ala/ecodata/graphql/converters/ObjectIdConverter.groovy index b5f92cca8..2a868c41c 100644 --- a/src/main/groovy/au/org/ala/ecodata/graphql/ObjectIdConverter.groovy +++ b/src/main/groovy/au/org/ala/ecodata/graphql/converters/ObjectIdConverter.groovy @@ -1,4 +1,4 @@ -package au.org.ala.ecodata.graphql +package au.org.ala.ecodata.graphql.converters import graphql.language.StringValue import graphql.schema.Coercing diff --git a/src/main/groovy/au/org/ala/ecodata/graphql/converters/OutputDataConverter.groovy b/src/main/groovy/au/org/ala/ecodata/graphql/converters/OutputDataConverter.groovy new file mode 100644 index 000000000..6a455919e --- /dev/null +++ b/src/main/groovy/au/org/ala/ecodata/graphql/converters/OutputDataConverter.groovy @@ -0,0 +1,38 @@ +package au.org.ala.ecodata.graphql.converters + +import au.org.ala.ecodata.graphql.models.OutputData +import graphql.schema.Coercing +import graphql.schema.CoercingParseValueException +import graphql.schema.CoercingSerializeException + +class OutputDataConverter implements Coercing { + + protected Optional convert(Object input) { + if (input instanceof OutputData) { + Optional.of((OutputData) input) + } + else { + Optional.empty() + } + } + + @Override + OutputData serialize(Object input) { + convert(input).orElseThrow( { + throw new CoercingSerializeException("Could not convert ${input.class.name} to a OutputData") + }) + } + + @Override + OutputData parseValue(Object input) { + convert(input).orElseThrow( { + throw new CoercingParseValueException("Could not convert ${input.class.name} to a OutputData") + }) + } + + @Override + OutputData parseLiteral(Object input) { + null + } +} + diff --git a/src/main/groovy/au/org/ala/ecodata/graphql/converters/SchemaConverter.groovy b/src/main/groovy/au/org/ala/ecodata/graphql/converters/SchemaConverter.groovy new file mode 100644 index 000000000..2b9826748 --- /dev/null +++ b/src/main/groovy/au/org/ala/ecodata/graphql/converters/SchemaConverter.groovy @@ -0,0 +1,37 @@ +package au.org.ala.ecodata.graphql.converters + +import au.org.ala.ecodata.graphql.models.Schema +import graphql.schema.Coercing +import graphql.schema.CoercingParseValueException +import graphql.schema.CoercingSerializeException + +class SchemaConverter implements Coercing { + + protected Optional convert(Object input) { + if (input instanceof Schema) { + Optional.of((Schema) input) + } + else { + Optional.empty() + } + } + + @Override + Schema serialize(Object input) { + convert(input).orElseThrow( { + throw new CoercingSerializeException("Could not convert ${input.class.name} to a Schema") + }) + } + + @Override + Schema parseValue(Object input) { + convert(input).orElseThrow( { + throw new CoercingParseValueException("Could not convert ${input.class.name} to a Schema") + }) + } + + @Override + Schema parseLiteral(Object input) { + null + } +} diff --git a/src/main/groovy/au/org/ala/ecodata/graphql/converters/SectionTemplateConverter.groovy b/src/main/groovy/au/org/ala/ecodata/graphql/converters/SectionTemplateConverter.groovy new file mode 100644 index 000000000..073937ded --- /dev/null +++ b/src/main/groovy/au/org/ala/ecodata/graphql/converters/SectionTemplateConverter.groovy @@ -0,0 +1,38 @@ +package au.org.ala.ecodata.graphql.converters + +import au.org.ala.ecodata.graphql.models.SectionTemplate +import graphql.schema.Coercing +import graphql.schema.CoercingParseValueException +import graphql.schema.CoercingSerializeException + +class SectionTemplateConverter implements Coercing { + + protected Optional convert(Object input) { + if (input instanceof SectionTemplate) { + Optional.of((SectionTemplate) input) + } + else { + Optional.empty() + } + } + + @Override + SectionTemplate serialize(Object input) { + convert(input).orElseThrow( { + throw new CoercingSerializeException("Could not convert ${input.class.name} to a SectionTemplate") + }) + } + + @Override + SectionTemplate parseValue(Object input) { + convert(input).orElseThrow( { + throw new CoercingParseValueException("Could not convert ${input.class.name} to a SectionTemplate") + }) + } + + @Override + SectionTemplate parseLiteral(Object input) { + null + } +} + diff --git a/src/main/groovy/au/org/ala/ecodata/graphql/converters/SummaryConverter.groovy b/src/main/groovy/au/org/ala/ecodata/graphql/converters/SummaryConverter.groovy new file mode 100644 index 000000000..289b61bce --- /dev/null +++ b/src/main/groovy/au/org/ala/ecodata/graphql/converters/SummaryConverter.groovy @@ -0,0 +1,38 @@ +package au.org.ala.ecodata.graphql.converters + +import au.org.ala.ecodata.graphql.models.Summary +import graphql.schema.Coercing +import graphql.schema.CoercingParseValueException +import graphql.schema.CoercingSerializeException + +class SummaryConverter implements Coercing { + + protected Optional convert(Object input) { + if (input instanceof Summary) { + Optional.of((Summary) input) + } + else { + Optional.empty() + } + } + + @Override + Summary serialize(Object input) { + convert(input).orElseThrow( { + throw new CoercingSerializeException("Could not convert ${input.class.name} to a Summary") + }) + } + + @Override + Summary parseValue(Object input) { + convert(input).orElseThrow( { + throw new CoercingParseValueException("Could not convert ${input.class.name} to a Summary") + }) + } + + @Override + Summary parseLiteral(Object input) { + null + } +} + diff --git a/src/main/groovy/au/org/ala/ecodata/graphql/ActivityFetcher.groovy b/src/main/groovy/au/org/ala/ecodata/graphql/fetchers/ActivityFetcher.groovy similarity index 50% rename from src/main/groovy/au/org/ala/ecodata/graphql/ActivityFetcher.groovy rename to src/main/groovy/au/org/ala/ecodata/graphql/fetchers/ActivityFetcher.groovy index 3734499ba..d98033b0e 100644 --- a/src/main/groovy/au/org/ala/ecodata/graphql/ActivityFetcher.groovy +++ b/src/main/groovy/au/org/ala/ecodata/graphql/fetchers/ActivityFetcher.groovy @@ -1,21 +1,31 @@ -package au.org.ala.ecodata.graphql +package au.org.ala.ecodata.graphql.fetchers import au.org.ala.ecodata.* +import au.org.ala.ecodata.graphql.models.Schema +import au.org.ala.ecodata.graphql.models.Summary +import grails.core.GrailsApplication import graphql.schema.DataFetchingEnvironment import org.elasticsearch.action.search.SearchResponse +import org.springframework.context.MessageSource -class ActivityFetcher implements graphql.schema.DataFetcher> { +class ActivityFetcher implements graphql.schema.DataFetcher> { - public ActivityFetcher(ElasticSearchService elasticSearchService, PermissionService permissionService) { + public ActivityFetcher(ElasticSearchService elasticSearchService, PermissionService permissionService, MetadataService metadataService, MessageSource messageSource, GrailsApplication grailsApplication) { this.elasticSearchService = elasticSearchService this.permissionService = permissionService + this.metadataService = metadataService + this.messageSource = messageSource + this.grailsApplication = grailsApplication } PermissionService permissionService ElasticSearchService elasticSearchService ActivityService activityService + MetadataService metadataService + MessageSource messageSource + GrailsApplication grailsApplication @Override @@ -50,4 +60,35 @@ class ActivityFetcher implements graphql.schema.DataFetcher> { } + List getActivitySummaryList(DataFetchingEnvironment environment) { + + List activityList = new ArrayList() + def activeActivities = metadataService.buildActivityModel()["activities"].findAll{!it.status || it.status == 'active'} + if(activeActivities) { + activeActivities.each() { + Summary summary = new Summary(name: it.name, definition: messageSource.getMessage("api.${it.name}.description", null, "", Locale.default)) + activityList << summary + } + } + + return activityList + } + + Schema getActivityByName(String name) { + + def activitiesModel = metadataService.activitiesModel() + def schemaGenerator = new SchemaBuilder(grailsApplication.config, activitiesModel) + if (!name) { + return null + } + + def activityModel = activitiesModel.activities.find{it.name == name} + def schema = schemaGenerator.schemaForActivity(activityModel) + + Schema schemaEntity = new Schema() + schemaEntity.type = schema.type + schemaEntity.id = schema.id + schemaEntity.propertyList = schema.properties + return schemaEntity + } } diff --git a/src/main/groovy/au/org/ala/ecodata/graphql/fetchers/OutputFetcher.groovy b/src/main/groovy/au/org/ala/ecodata/graphql/fetchers/OutputFetcher.groovy new file mode 100644 index 000000000..af488af20 --- /dev/null +++ b/src/main/groovy/au/org/ala/ecodata/graphql/fetchers/OutputFetcher.groovy @@ -0,0 +1,70 @@ +package au.org.ala.ecodata.graphql.fetchers + +import au.org.ala.ecodata.Activity +import au.org.ala.ecodata.ActivityService +import au.org.ala.ecodata.ElasticSearchService +import au.org.ala.ecodata.MetadataService +import au.org.ala.ecodata.Output +import au.org.ala.ecodata.PermissionService +import au.org.ala.ecodata.SchemaBuilder +import au.org.ala.ecodata.graphql.models.Schema +import au.org.ala.ecodata.graphql.models.Summary +import grails.core.GrailsApplication +import graphql.schema.DataFetchingEnvironment +import org.springframework.context.MessageSource + +class OutputFetcher implements graphql.schema.DataFetcher> { + + public OutputFetcher(MetadataService metadataService, MessageSource messageSource, GrailsApplication grailsApplication) { + + this.metadataService = metadataService + this.messageSource = messageSource + this.grailsApplication = grailsApplication + } + + MetadataService metadataService + MessageSource messageSource + GrailsApplication grailsApplication + + List getOutputSummaryList(DataFetchingEnvironment environment) { + + List activityList = new ArrayList() + + def activitiesModel = metadataService.activitiesModel() + + def activeActivities = activitiesModel.activities.findAll{!it.status || it.status == 'active'} + activitiesModel.outputs.each { output -> + if (activeActivities.find{output.name in it.outputs}) { + Summary sa = new Summary(name: output.name, definition: messageSource.getMessage("api.${output.name}.description", null, "", Locale.default)) + activityList << sa + } + } + return activityList + } + + Schema getOutputByName(String name) { + + def activitiesModel = metadataService.activitiesModel() + + def schemaGenerator = new SchemaBuilder(grailsApplication.config, activitiesModel) + if (!name) { + return null + } + def outputTemplate = metadataService.getOutputModel(name)?.template + + def outputDataModel = metadataService.getOutputDataModel(outputTemplate) + + def schema = schemaGenerator.schemaForOutput(name, outputDataModel) + + Schema schemaEntity = new Schema() + schemaEntity.type = schema.type + schemaEntity.id = schema.id + schemaEntity.propertyList = schema.properties + return schemaEntity + } + + @Override + List get(DataFetchingEnvironment environment) { + Output.findAll([max:10]) + } +} diff --git a/src/main/groovy/au/org/ala/ecodata/graphql/ProjectsFetcher.groovy b/src/main/groovy/au/org/ala/ecodata/graphql/fetchers/ProjectsFetcher.groovy similarity index 98% rename from src/main/groovy/au/org/ala/ecodata/graphql/ProjectsFetcher.groovy rename to src/main/groovy/au/org/ala/ecodata/graphql/fetchers/ProjectsFetcher.groovy index c9ccc8cc9..4b3120ad9 100644 --- a/src/main/groovy/au/org/ala/ecodata/graphql/ProjectsFetcher.groovy +++ b/src/main/groovy/au/org/ala/ecodata/graphql/fetchers/ProjectsFetcher.groovy @@ -1,4 +1,4 @@ -package au.org.ala.ecodata.graphql +package au.org.ala.ecodata.graphql.fetchers import au.org.ala.ecodata.* import com.mongodb.client.FindIterable diff --git a/src/main/groovy/au/org/ala/ecodata/graphql/SitesFetcher.groovy b/src/main/groovy/au/org/ala/ecodata/graphql/fetchers/SitesFetcher.groovy similarity index 96% rename from src/main/groovy/au/org/ala/ecodata/graphql/SitesFetcher.groovy rename to src/main/groovy/au/org/ala/ecodata/graphql/fetchers/SitesFetcher.groovy index e8999d497..9ec6c1993 100644 --- a/src/main/groovy/au/org/ala/ecodata/graphql/SitesFetcher.groovy +++ b/src/main/groovy/au/org/ala/ecodata/graphql/fetchers/SitesFetcher.groovy @@ -1,12 +1,9 @@ -package au.org.ala.ecodata.graphql +package au.org.ala.ecodata.graphql.fetchers import au.org.ala.ecodata.* -import graphql.GraphQLException import graphql.schema.DataFetchingEnvironment import org.elasticsearch.action.search.SearchResponse -import java.security.AccessControlException - class SitesFetcher implements graphql.schema.DataFetcher> { public SitesFetcher(ProjectService projectService, ElasticSearchService elasticSearchService, PermissionService permissionService) { diff --git a/src/main/groovy/au/org/ala/ecodata/graphql/mappers/ActivityFormGraphQLMapper.groovy b/src/main/groovy/au/org/ala/ecodata/graphql/mappers/ActivityFormGraphQLMapper.groovy new file mode 100644 index 000000000..6fe27aad6 --- /dev/null +++ b/src/main/groovy/au/org/ala/ecodata/graphql/mappers/ActivityFormGraphQLMapper.groovy @@ -0,0 +1,54 @@ +package au.org.ala.ecodata.graphql.mappers + +import au.org.ala.ecodata.* +import grails.gorm.DetachedCriteria +import graphql.schema.DataFetchingEnvironment +import org.grails.gorm.graphql.entity.dsl.GraphQLMapping +import org.grails.gorm.graphql.fetcher.impl.SingleEntityDataFetcher + +class ActivityFormGraphQLMapper { + + static graphqlMapping() { + GraphQLMapping.lazy { + // Disable default operations, including get as we only want to expose UUIDs in the API not internal ones + operations.get.enabled false + operations.list.enabled true + operations.count.enabled false + operations.create.enabled false + operations.update.enabled false + operations.delete.enabled false + + exclude("sections") + + add("formSections", [FormSection]) { + dataFetcher { ActivityForm activityForm -> + List sectionList = activityForm.sections.each { + new FormSection( + title: it.title, + template: it.template, + name: it.name, + modelName: it.modelName, + templateName: it.templateName, + optionalQuestionText: it.optionalQuestionText, + optional: it.optional, + collapsedByDefault: it.collapsedByDefault + ) + } + return sectionList + } + } + + //get the activity form schema by activity form name + query('activityForm', ActivityForm) { + argument('activityFormName', String) + dataFetcher(new SingleEntityDataFetcher(ActivityForm.gormPersistentEntity) { + @Override + protected DetachedCriteria buildCriteria(DataFetchingEnvironment environment) { + //need to get the latest activity form based on the formVersion + ActivityForm.where { name == environment.getArgument('activityFormName') }.sort('formVersion', 'desc') + } + }) + } + } + } +} diff --git a/src/main/groovy/au/org/ala/ecodata/graphql/mappers/ActivityGraphQLMapper.groovy b/src/main/groovy/au/org/ala/ecodata/graphql/mappers/ActivityGraphQLMapper.groovy new file mode 100644 index 000000000..68e234b71 --- /dev/null +++ b/src/main/groovy/au/org/ala/ecodata/graphql/mappers/ActivityGraphQLMapper.groovy @@ -0,0 +1,80 @@ +package au.org.ala.ecodata.graphql.mappers + +import au.org.ala.ecodata.* +import au.org.ala.ecodata.graphql.fetchers.ActivityFetcher +import au.org.ala.ecodata.graphql.models.Schema +import au.org.ala.ecodata.graphql.models.Summary +import grails.gorm.DetachedCriteria +import grails.util.Holders +import graphql.schema.DataFetcher +import graphql.schema.DataFetchingEnvironment +import org.grails.gorm.graphql.entity.dsl.GraphQLMapping +import org.grails.gorm.graphql.fetcher.impl.SingleEntityDataFetcher + +class ActivityGraphQLMapper { + + static graphqlMapping() { + GraphQLMapping.lazy { + // Disable default operations, including get as we only want to expose UUIDs in the API not internal ones + operations.get.enabled false + operations.list.enabled true + operations.count.enabled false + operations.create.enabled false + operations.update.enabled false + operations.delete.enabled false + + add('outputs', [Output]) { + dataFetcher { Activity activity -> + Output.findAllByActivityId(activity.activityId) + } + input false + } + + //get activity by activity id + query('activity', Activity) { + argument('activityId', String) + dataFetcher(new SingleEntityDataFetcher(Activity.gormPersistentEntity) { + @Override + protected DetachedCriteria buildCriteria(DataFetchingEnvironment environment) { + Activity.where { activityId == environment.getArgument('activityId') } + } + }) + } + + //get a list of activities by project id + query('activityByProjectId', [Activity]) { + argument('projectId', String) + dataFetcher(new DataFetcher() { + @Override + Object get(DataFetchingEnvironment environment) throws Exception { + Activity.where { projectId == environment.getArgument('projectId') } + } + }) + } + + //get activity schema by activity name + query('activitySchemaByName', Schema) { + argument('activityName', String) + dataFetcher(new DataFetcher() { + @Override + Object get(DataFetchingEnvironment environment) throws Exception { + new ActivityFetcher(Holders.applicationContext.elasticSearchService, Holders.applicationContext.permissionService, Holders.applicationContext.metadataService, + Holders.applicationContext.messageSource, Holders.grailsApplication).getActivityByName(environment.getArgument('activityName')) + } + }) + } + + //get the list of activities + query('activities', [Summary]) { + dataFetcher(new DataFetcher() { + @Override + Object get(DataFetchingEnvironment environment) throws Exception { + new ActivityFetcher(Holders.applicationContext.elasticSearchService, Holders.applicationContext.permissionService, Holders.applicationContext.metadataService, + Holders.applicationContext.messageSource, Holders.grailsApplication).getActivitySummaryList(environment) + } + }) + } + } + + } +} diff --git a/src/main/groovy/au/org/ala/ecodata/graphql/mappers/FormSectionGraphQLMapper.groovy b/src/main/groovy/au/org/ala/ecodata/graphql/mappers/FormSectionGraphQLMapper.groovy new file mode 100644 index 000000000..567a8f212 --- /dev/null +++ b/src/main/groovy/au/org/ala/ecodata/graphql/mappers/FormSectionGraphQLMapper.groovy @@ -0,0 +1,28 @@ +package au.org.ala.ecodata.graphql.mappers + +import au.org.ala.ecodata.* +import au.org.ala.ecodata.graphql.models.SectionTemplate +import org.grails.gorm.graphql.entity.dsl.GraphQLMapping + +class FormSectionGraphQLMapper { + + static graphqlMapping() { + GraphQLMapping.lazy { + operations.get.enabled false + operations.list.enabled false + operations.list.paginate(false) + operations.count.enabled false + operations.create.enabled false + operations.update.enabled false + operations.delete.enabled false + + exclude("template") + + add("formTemplate", SectionTemplate) { + dataFetcher { FormSection formSection -> + formSection.getSectionTemplate() + } + } + } + } +} diff --git a/src/main/groovy/au/org/ala/ecodata/graphql/mappers/OutputGraphQLMapper.groovy b/src/main/groovy/au/org/ala/ecodata/graphql/mappers/OutputGraphQLMapper.groovy new file mode 100644 index 000000000..37fd996ec --- /dev/null +++ b/src/main/groovy/au/org/ala/ecodata/graphql/mappers/OutputGraphQLMapper.groovy @@ -0,0 +1,55 @@ +package au.org.ala.ecodata.graphql.mappers + +import au.org.ala.ecodata.* +import au.org.ala.ecodata.graphql.fetchers.OutputFetcher +import au.org.ala.ecodata.graphql.models.OutputData +import au.org.ala.ecodata.graphql.models.Schema +import au.org.ala.ecodata.graphql.models.Summary +import grails.util.Holders +import graphql.schema.DataFetcher +import graphql.schema.DataFetchingEnvironment +import org.grails.gorm.graphql.entity.dsl.GraphQLMapping + +class OutputGraphQLMapper { + + static graphqlMapping() { + GraphQLMapping.lazy { + // Disable default operations, including get as we only want to expose UUIDs in the API not internal ones + operations.get.enabled false + operations.list.enabled false + operations.count.enabled false + operations.create.enabled false + operations.update.enabled false + operations.delete.enabled false + + + add('data', OutputData) { + dataFetcher { Output output -> + output.getData(output.data) + } + input false + } + + //get the list of output types + query('outputs', [Summary]) { + dataFetcher(new DataFetcher() { + @Override + Object get(DataFetchingEnvironment environment) throws Exception { + new OutputFetcher(Holders.applicationContext.metadataService, Holders.applicationContext.messageSource, Holders.grailsApplication).getOutputSummaryList(environment) + } + }) + } + + //get output schema by output type name + query('outputTypeByName', Schema) { + argument('outputTypeName', String) + dataFetcher(new DataFetcher() { + @Override + Object get(DataFetchingEnvironment environment) throws Exception { + new OutputFetcher(Holders.applicationContext.metadataService, Holders.applicationContext.messageSource, Holders.grailsApplication).getOutputByName(environment.getArgument('outputTypeName')) + } + }) + } + } + } +} diff --git a/src/main/groovy/au/org/ala/ecodata/graphql/ProjectGraphQLMapper.groovy b/src/main/groovy/au/org/ala/ecodata/graphql/mappers/ProjectGraphQLMapper.groovy similarity index 90% rename from src/main/groovy/au/org/ala/ecodata/graphql/ProjectGraphQLMapper.groovy rename to src/main/groovy/au/org/ala/ecodata/graphql/mappers/ProjectGraphQLMapper.groovy index 79984d17f..34eb556c7 100644 --- a/src/main/groovy/au/org/ala/ecodata/graphql/ProjectGraphQLMapper.groovy +++ b/src/main/groovy/au/org/ala/ecodata/graphql/mappers/ProjectGraphQLMapper.groovy @@ -1,10 +1,12 @@ -package au.org.ala.ecodata.graphql +package au.org.ala.ecodata.graphql.mappers import au.org.ala.ecodata.Document import au.org.ala.ecodata.Project import au.org.ala.ecodata.Report import au.org.ala.ecodata.Site import au.org.ala.ecodata.Status +import au.org.ala.ecodata.graphql.fetchers.ProjectsFetcher +import au.org.ala.ecodata.graphql.models.MeriPlan import grails.gorm.DetachedCriteria import grails.util.Holders import graphql.schema.DataFetcher @@ -26,6 +28,7 @@ class ProjectGraphQLMapper { operations.update.enabled false operations.delete.enabled false + exclude("custom") List restrictedProperties = [] restrictedProperties.each { String prop -> @@ -40,9 +43,12 @@ class ProjectGraphQLMapper { } } -// property('meriPlan') { Project project -> -// -// } + add('meriPlan', MeriPlan) { + dataFetcher { Project project -> + project.getMeriPlan() + } + } + add('documents', [Document]) { dataFetcher { Project project, ClosureDataFetchingEnvironment env -> Document.findAllByProjectIdAndStatusNotEqual(project.projectId, Status.DELETED) diff --git a/src/main/groovy/au/org/ala/ecodata/graphql/models/MeriPlan.groovy b/src/main/groovy/au/org/ala/ecodata/graphql/models/MeriPlan.groovy new file mode 100644 index 000000000..588c44a3e --- /dev/null +++ b/src/main/groovy/au/org/ala/ecodata/graphql/models/MeriPlan.groovy @@ -0,0 +1,7 @@ +package au.org.ala.ecodata.graphql.models + +class MeriPlan { + + Object details + Object outputTargets +} \ No newline at end of file diff --git a/src/main/groovy/au/org/ala/ecodata/graphql/models/OutputData.groovy b/src/main/groovy/au/org/ala/ecodata/graphql/models/OutputData.groovy new file mode 100644 index 000000000..52ed14442 --- /dev/null +++ b/src/main/groovy/au/org/ala/ecodata/graphql/models/OutputData.groovy @@ -0,0 +1,13 @@ +package au.org.ala.ecodata.graphql.models + +class OutputData { + + List dataList + +} + +class KeyValue{ + + String key + Object value +} \ No newline at end of file diff --git a/src/main/groovy/au/org/ala/ecodata/graphql/models/Schema.groovy b/src/main/groovy/au/org/ala/ecodata/graphql/models/Schema.groovy new file mode 100644 index 000000000..21c1c5c0b --- /dev/null +++ b/src/main/groovy/au/org/ala/ecodata/graphql/models/Schema.groovy @@ -0,0 +1,9 @@ +package au.org.ala.ecodata.graphql.models + +class Schema{ + + String id + String type + Object propertyList + +} \ No newline at end of file diff --git a/src/main/groovy/au/org/ala/ecodata/graphql/models/SectionTemplate.groovy b/src/main/groovy/au/org/ala/ecodata/graphql/models/SectionTemplate.groovy new file mode 100644 index 000000000..c875bb1de --- /dev/null +++ b/src/main/groovy/au/org/ala/ecodata/graphql/models/SectionTemplate.groovy @@ -0,0 +1,6 @@ +package au.org.ala.ecodata.graphql.models + +class SectionTemplate { + + Object sectionTemplate +} \ No newline at end of file diff --git a/src/main/groovy/au/org/ala/ecodata/graphql/models/Summary.groovy b/src/main/groovy/au/org/ala/ecodata/graphql/models/Summary.groovy new file mode 100644 index 000000000..02ec4112f --- /dev/null +++ b/src/main/groovy/au/org/ala/ecodata/graphql/models/Summary.groovy @@ -0,0 +1,7 @@ +package au.org.ala.ecodata.graphql.models + +class Summary{ + + String name + String definition +} \ No newline at end of file From 7add5268340dba44913be8b301ba7c14d058c337 Mon Sep 17 00:00:00 2001 From: yasima-csiro Date: Fri, 4 Dec 2020 12:22:10 +1100 Subject: [PATCH 004/144] Add project search API --- .../ecodata/graphql/enums/DateRange.groovy | 40 +++++++ .../ala/ecodata/graphql/enums/YesNo.groovy | 6 + .../graphql/fetchers/ProjectsFetcher.groovy | 108 +++++++++++++++++- .../mappers/ProjectGraphQLMapper.groovy | 39 +++++++ 4 files changed, 188 insertions(+), 5 deletions(-) create mode 100644 src/main/groovy/au/org/ala/ecodata/graphql/enums/DateRange.groovy create mode 100644 src/main/groovy/au/org/ala/ecodata/graphql/enums/YesNo.groovy diff --git a/src/main/groovy/au/org/ala/ecodata/graphql/enums/DateRange.groovy b/src/main/groovy/au/org/ala/ecodata/graphql/enums/DateRange.groovy new file mode 100644 index 000000000..04471909b --- /dev/null +++ b/src/main/groovy/au/org/ala/ecodata/graphql/enums/DateRange.groovy @@ -0,0 +1,40 @@ +package au.org.ala.ecodata.graphql.enums + +enum DateRange { + + Jul_2011_Jan_2012("2011-07-01", "2012-01-01"), + Jan_2012_Jul_2012("2012-01-01", "2012-07-01"), + Jul_2012_Jan_2013("2012-07-01", "2013-01-01"), + Jan_2013_Jul_2013("2013-07-01", "2013-01-01"), + Jul_2013_Jan_2014("2013-07-01", "2014-01-01"), + Jan_2014_Jul_2014("2014-07-01", "2014-01-01"), + Jul_2014_Jan_2015("2014-07-01", "2015-01-01"), + Jan_2015_Jul_2015("2015-07-01", "2015-01-01"), + Jul_2015_Jan_2016("2015-07-01", "2016-01-01"), + Jan_2016_Jul_2016("2016-07-01", "2016-01-01"), + Jul_2016_Jan_2017("2016-07-01", "2017-01-01"), + Jan_2017_Jul_2017("2017-07-01", "2017-01-01"), + Jul_2017_Jan_2018("2017-07-01", "2018-01-01"), + Jan_2018_Jul_2018("2018-07-01", "2018-01-01"), + Jul_2018_Jan_2019("2018-07-01", "2019-01-01"), + Jan_2019_Jul_2019("2019-07-01", "2019-01-01"), + Jul_2019_Jan_2020("2019-07-01", "2020-01-01"), + Jan_2020_Jul_2020("2020-07-01", "2020-01-01"), + Jul_2020_Jan_2021("2020-07-01", "2021-01-01") + + + private String from + private String to + private DateRange(String fromDate, String toDate) { + from = fromDate + to = toDate + } + + String getFromDate() { + return from + } + + String getToDate() { + return to + } +} \ No newline at end of file diff --git a/src/main/groovy/au/org/ala/ecodata/graphql/enums/YesNo.groovy b/src/main/groovy/au/org/ala/ecodata/graphql/enums/YesNo.groovy new file mode 100644 index 000000000..a1a8f0a4b --- /dev/null +++ b/src/main/groovy/au/org/ala/ecodata/graphql/enums/YesNo.groovy @@ -0,0 +1,6 @@ +package au.org.ala.ecodata.graphql.enums + +enum YesNo { + yes, + no +} \ No newline at end of file diff --git a/src/main/groovy/au/org/ala/ecodata/graphql/fetchers/ProjectsFetcher.groovy b/src/main/groovy/au/org/ala/ecodata/graphql/fetchers/ProjectsFetcher.groovy index 4b3120ad9..fafa1b6dc 100644 --- a/src/main/groovy/au/org/ala/ecodata/graphql/fetchers/ProjectsFetcher.groovy +++ b/src/main/groovy/au/org/ala/ecodata/graphql/fetchers/ProjectsFetcher.groovy @@ -3,9 +3,12 @@ package au.org.ala.ecodata.graphql.fetchers import au.org.ala.ecodata.* import com.mongodb.client.FindIterable import com.mongodb.client.model.Filters +import graphql.GraphQLException import graphql.schema.DataFetchingEnvironment import org.elasticsearch.action.search.SearchResponse +import static au.org.ala.ecodata.ElasticIndex.HOMEPAGE_INDEX + class ProjectsFetcher implements graphql.schema.DataFetcher> { public ProjectsFetcher(ProjectService projectService, ElasticSearchService elasticSearchService, PermissionService permissionService) { @@ -20,6 +23,10 @@ class ProjectsFetcher implements graphql.schema.DataFetcher> { ProjectService projectService + static String meritFacets = "status,organisationFacet,associatedProgramFacet,associatedSubProgramFacet,mainThemeFacet,stateFacet,nrmFacet,lgaFacet,mvgFacet,ibraFacet,imcra4_pbFacet,otherFacet,electFacet,meriPlanAssetFacet," + + "cmzFacet,partnerOrganisationTypeFacet,promoteOnHomepage,custom.details.caseStudy,primaryOutcomeFacet,secondaryOutcomesFacet,muFacet,tags,fundingSourceFacet" + static Map meritParams = [hubFq:"isMERIT:true", controller:"search", flimit:1500, fsort:"term", query:"docType:project", action:"elasticHome", facets:meritFacets, format:null] + @Override List get(DataFetchingEnvironment environment) throws Exception { @@ -34,19 +41,19 @@ class ProjectsFetcher implements graphql.schema.DataFetcher> { // e.g. should the role check only apply during the mapping phase? In which case we need a bulk query of permissions to determine a list of project ids we can get full resolution data for? // Or do we do two queries, one for full resolution, one for the rest (how do we sort/page if we do two queries?) + String query = environment.arguments.term ?:"*:*" - - return queryElasticSearch(environment) + return queryElasticSearch(environment, query, [include:'projectId']) } - private List queryElasticSearch(DataFetchingEnvironment environment) { + private List queryElasticSearch(DataFetchingEnvironment environment, String queryString, Map params) { // Retrieve projectIds only from elasticsearch. // add pagination results. String userId = environment.context.userId ?: '1493' - String query = environment.arguments.term ?:"*:*" - SearchResponse searchResponse = elasticSearchService.searchWithSecurity(userId, query, [include:'projectId'], ElasticIndex.HOMEPAGE_INDEX) + String query = queryString ?:"*:*" + SearchResponse searchResponse = elasticSearchService.searchWithSecurity(userId, query, params, HOMEPAGE_INDEX) List projectIds = searchResponse.hits.hits.collect{it.source.projectId} @@ -83,4 +90,95 @@ class ProjectsFetcher implements graphql.schema.DataFetcher> { projects } + + List searchMeritProject (DataFetchingEnvironment environment) { + + def fqList = [] + def facetMappings = ["managementArea": "nrmFacet:", "majorVegetationGroup": "mvgFacet:", "biogeographicRegion": "ibraFacet:", "marineRegion": "imcra4_pbFacet:", "otherRegion": "otherFacet:", "grantManagerNominatedProject":"promoteOnHomepage:", + "federalElectorate": "electFacet:", "assetsAddressed": "meriPlanAssetFacet:", "userNominatedProject": "custom.details.caseStudy:", "managementUnit": "muFacet:"] + + environment.arguments.each { + if(it.key in ["fromDate", "toDate", "dateRange"]) { + return + } + + String key + + it.value.each { val -> + switch (it.key) { + case "status": + case "tags": + key = it.key + ":" + break; + case "managementArea" : + case "majorVegetationGroup" : + case "biogeographicRegion" : + case "marineRegion" : + case "otherRegion" : + case "grantManagerNominatedProject" : + case "federalElectorate" : + case "assetsAddressed" : + case "userNominatedProject" : + case "managementUnit" : + key = facetMappings.get(it.key) + break; + default: + key = it.key + "Facet:" + break; + } + fqList << key + val; + } + } + + //validate the query + validateSearchQuery(environment, fqList) + + Map params = meritParams + params["fq"] = fqList + + if(environment.arguments.get("fromDate")) { + params["fromDate"] = environment.arguments.get("fromDate").toString() + } + + if(environment.arguments.get("toDate")) { + params["toDate"] = environment.arguments.get("toDate").toString() + } + + if(environment.arguments.get("dateRange")) { + + params["fromDate"] = environment.arguments.get("dateRange").from + params["toDate"] = environment.arguments.get("dateRange").to + } + + return queryElasticSearch(environment, "docType: project", params) + } + + void validateSearchQuery (DataFetchingEnvironment environment, List fqList) { + + def searchDetails = elasticSearchService.search("docType: project", meritParams, HOMEPAGE_INDEX) + + fqList.each { + List fq = it.toString().split(":") + List lookUps = searchDetails.facets.getFacets().get(fq.first()).entries.term as String[] + if(!lookUps.contains(fq.last())) { + throw new GraphQLException('Invalid ' + fq.first() +' : suggested values are : ' + lookUps) + } + } + + def datePattern = /\d{4}\-\d{2}\-\d{2}/ + + //validate the format of the from anf to dates + if(environment.arguments.get("fromDate")) { + if(!(environment.arguments.get("fromDate") ==~ datePattern)){ + throw new GraphQLException('Invalid fromDate: fromDate should match yyyy-mm-dd') + } + } + + if(environment.arguments.get("toDate")) { + if(!(environment.arguments.get("fromDate") ==~ datePattern)){ + throw new GraphQLException('Invalid toDate: toDate should match yyyy-mm-dd') + } + } + + } } diff --git a/src/main/groovy/au/org/ala/ecodata/graphql/mappers/ProjectGraphQLMapper.groovy b/src/main/groovy/au/org/ala/ecodata/graphql/mappers/ProjectGraphQLMapper.groovy index 34eb556c7..79100c778 100644 --- a/src/main/groovy/au/org/ala/ecodata/graphql/mappers/ProjectGraphQLMapper.groovy +++ b/src/main/groovy/au/org/ala/ecodata/graphql/mappers/ProjectGraphQLMapper.groovy @@ -5,6 +5,8 @@ import au.org.ala.ecodata.Project import au.org.ala.ecodata.Report import au.org.ala.ecodata.Site import au.org.ala.ecodata.Status +import au.org.ala.ecodata.graphql.enums.DateRange +import au.org.ala.ecodata.graphql.enums.YesNo import au.org.ala.ecodata.graphql.fetchers.ProjectsFetcher import au.org.ala.ecodata.graphql.models.MeriPlan import grails.gorm.DetachedCriteria @@ -87,6 +89,43 @@ class ProjectGraphQLMapper { }) } + query('searchMeritProject', [Project]) { + argument('fromDate', String){ nullable true description "yyyy-mm-dd" } + argument('toDate', String){ nullable true description "yyyy-mm-dd" } + argument('dateRange', DateRange){ nullable true } + argument('status', [String]){ nullable true } + argument('organisation', [String]){ nullable true } + argument('associatedProgram', [String]){ nullable true } + argument('associatedSubProgram', [String]){ nullable true } + argument('mainTheme', [String]){ nullable true } + argument('state', [String]){ nullable true } + argument('lga', [String]){ nullable true } + argument('cmz', [String]){ nullable true } + argument('partnerOrganisationType', [String]){ nullable true } + argument('associatedSubProgram', [String]){ nullable true } + argument('primaryOutcome', [String]){ nullable true } + argument('secondaryOutcomes', [String]){ nullable true } + argument('tags', [String]){ nullable true } + + argument('managementArea', [String]){ nullable true } + argument('majorVegetationGroup', [String]){ nullable true } + argument('biogeographicRegion', [String]){ nullable true } + argument('marineRegion', [String]){ nullable true } + argument('otherRegion', [String]){ nullable true } + argument('grantManagerNominatedProject', [YesNo]){ nullable true } + argument('federalElectorate', [String]){ nullable true } + argument('assetsAddressed', [String]){ nullable true } + argument('userNominatedProject', [String]){ nullable true } + argument('managementUnit', [String]){ nullable true } + + dataFetcher(new DataFetcher() { + @Override + Object get(DataFetchingEnvironment environment) throws Exception { + ProjectGraphQLMapper.buildTestFetcher().searchMeritProject(environment) + } + }) + } + } } From deb8ae5d3161972c7ca544f63cf1cc111a71419e Mon Sep 17 00:00:00 2001 From: yasima-csiro Date: Fri, 18 Dec 2020 10:43:37 +1100 Subject: [PATCH 005/144] Add activity and output API --- .../domain/au/org/ala/ecodata/Activity.groovy | 3 +- .../domain/au/org/ala/ecodata/Output.groovy | 15 +++-- .../domain/au/org/ala/ecodata/Project.groovy | 3 +- .../graphql/fetchers/ActivityFetcher.groovy | 45 +++++++++++++ .../ecodata/graphql/fetchers/Helper.groovy | 67 +++++++++++++++++++ .../graphql/fetchers/OutputFetcher.groovy | 59 ++++++++++++++++ .../graphql/fetchers/ProjectsFetcher.groovy | 27 ++++++-- .../mappers/ActivityGraphQLMapper.groovy | 33 +++++++-- .../mappers/OutputGraphQLMapper.groovy | 37 +++++++++- .../mappers/ProjectGraphQLMapper.groovy | 27 ++++++++ 10 files changed, 296 insertions(+), 20 deletions(-) create mode 100644 src/main/groovy/au/org/ala/ecodata/graphql/fetchers/Helper.groovy diff --git a/grails-app/domain/au/org/ala/ecodata/Activity.groovy b/grails-app/domain/au/org/ala/ecodata/Activity.groovy index 4dd4c6829..7d1c6ebaa 100644 --- a/grails-app/domain/au/org/ala/ecodata/Activity.groovy +++ b/grails-app/domain/au/org/ala/ecodata/Activity.groovy @@ -81,6 +81,7 @@ class Activity { Date lastUpdated String userId Boolean embargoed + List tempArgs = [] /** An activity is considered complete if it's progress attribute is finished, deferred or cancelled. */ public boolean isComplete() { @@ -93,7 +94,7 @@ class Activity { return progress in [PLANNED, STARTED, FINISHED] } - static transients = ['complete'] + static transients = ['complete', 'tempArgs'] static constraints = { siteId nullable: true diff --git a/grails-app/domain/au/org/ala/ecodata/Output.groovy b/grails-app/domain/au/org/ala/ecodata/Output.groovy index 11a19c02d..bea547b9f 100644 --- a/grails-app/domain/au/org/ala/ecodata/Output.groovy +++ b/grails-app/domain/au/org/ala/ecodata/Output.groovy @@ -34,17 +34,24 @@ class Output { String name Date dateCreated Date lastUpdated + List tempArgs = [] static constraints = { assessmentDate nullable: true name nullable: true } - OutputData getData(def data) { + static transients = ['tempArgs'] + + OutputData getData(List fields) { OutputData outputData = new OutputData(dataList: new ArrayList()) - if(data) { - data.each() { - outputData.dataList.add(new KeyValue(key: it.key, value: it.value)) + if(this.data) { + this.data.each() { + //if no fields, all the fields will be returned + //otherwise, only requested fields will be returned + if(!fields || (fields && fields.contains(it.key))) { + outputData.dataList.add(new KeyValue(key: it.key, value: it.value)) + } } } return outputData diff --git a/grails-app/domain/au/org/ala/ecodata/Project.groovy b/grails-app/domain/au/org/ala/ecodata/Project.groovy index 14813269a..8283339e1 100644 --- a/grails-app/domain/au/org/ala/ecodata/Project.groovy +++ b/grails-app/domain/au/org/ala/ecodata/Project.groovy @@ -81,6 +81,7 @@ class Project { List industries = [] String origin = 'atlasoflivingaustralia' String baseLayer + List tempArgs = [] boolean alaHarvest = false //For embedded table, needs to conversion in controller @@ -95,7 +96,7 @@ class Project { static embedded = ['associatedOrganisations','fundings'] - static transients = ['activities', 'plannedDurationInWeeks', 'actualDurationInWeeks'] + static transients = ['activities', 'plannedDurationInWeeks', 'actualDurationInWeeks', 'tempArgs'] Date getActualStartDate() { if (actualStartDate) { diff --git a/src/main/groovy/au/org/ala/ecodata/graphql/fetchers/ActivityFetcher.groovy b/src/main/groovy/au/org/ala/ecodata/graphql/fetchers/ActivityFetcher.groovy index d98033b0e..46e32d33f 100644 --- a/src/main/groovy/au/org/ala/ecodata/graphql/fetchers/ActivityFetcher.groovy +++ b/src/main/groovy/au/org/ala/ecodata/graphql/fetchers/ActivityFetcher.groovy @@ -4,6 +4,7 @@ import au.org.ala.ecodata.* import au.org.ala.ecodata.graphql.models.Schema import au.org.ala.ecodata.graphql.models.Summary import grails.core.GrailsApplication +import grails.util.Holders import graphql.schema.DataFetchingEnvironment import org.elasticsearch.action.search.SearchResponse import org.springframework.context.MessageSource @@ -91,4 +92,48 @@ class ActivityFetcher implements graphql.schema.DataFetcher> { schemaEntity.propertyList = schema.properties return schemaEntity } + + List getFilteredActivities(List args, String givenProjectId = null, String givenActivityId = null) + { + //validate inputs + if(args){ + List arguments = [] + arguments.add(["activities":args]) + new Helper(Holders.applicationContext.metadataService).validateActivityData(arguments) + } + + def activityIdList = [] + if(args) { + activityIdList = Output.where { name in args["output"]["outputType"].flatten() }.findAll() + + args.each { arg -> + arg["output"].each { + if (it["fields"] && !it["fields"].contains(null)) { + activityIdList.removeAll { x -> + (!x.data || x.data.size() == 0) && x.name == it["outputType"] + } + } + } + } + } + def activityList = Activity.where { + if(givenProjectId) { + projectId == givenProjectId + } + if(givenActivityId) { + activityId == givenActivityId + } + if(args && args["activityType"]) { + type in args["activityType"].flatten() + } + //get the activities with requested output types + if(activityIdList.size() > 0) { + activityId in activityIdList.activityId + } + }.each { + it.tempArgs = args ?: [] + }.sort { it.type} + + return activityList + } } diff --git a/src/main/groovy/au/org/ala/ecodata/graphql/fetchers/Helper.groovy b/src/main/groovy/au/org/ala/ecodata/graphql/fetchers/Helper.groovy new file mode 100644 index 000000000..d974999cc --- /dev/null +++ b/src/main/groovy/au/org/ala/ecodata/graphql/fetchers/Helper.groovy @@ -0,0 +1,67 @@ +package au.org.ala.ecodata.graphql.fetchers + +import au.org.ala.ecodata.MetadataService +import graphql.GraphQLException + +class Helper { + + MetadataService metadataService + + Helper(MetadataService metadataService) { + this.metadataService = metadataService + } + /** + * This can be used to validate the given activity types and the output types + */ + def validateActivityData(List args) { + + def metadata = metadataService.buildActivityModel() + List activities = metadata["activities"].findAll{!it.status || it.status == 'active'} + List outputs = metadata["outputs"] + + if(!args["activities"].contains(null)) { + args["activities"].each { activity -> + activity.each { prop -> + //validate activity type + if (prop["activityType"] && !activities.name.contains(prop["activityType"])) { + throw new GraphQLException('Invalid Activity Type: ' + prop["activityType"] + ' , suggested values are : ' + activities.name) + } + if(prop["output"]) { + List outputNameList = prop["activityType"] ? activities.find{it.name == prop["activityType"]}.outputs : outputs.name + prop["output"].each { output -> + //validate output types against the output types of the activity + validateOutputs(output, outputs, outputNameList) + + } + } + } + } + } + else if(!args["outputs"].contains(null)) { + args["outputs"].each{ output -> + //validate output types + output.each { + validateOutputs(it, outputs, outputs.name) + } + } + } + + } + + void validateOutputs(def output, List outputs, List outputNameList) { + //validate output types + if(output["outputType"] && !outputNameList.contains(output["outputType"])) { + throw new GraphQLException('Invalid Output Type: ' + output["outputType"] + ' , suggested values are : ' + outputNameList) + } + + //validate fields + if(output["fields"] && output["fields"].size() > 0 && !output["fields"].contains(null)) { + def templateName = outputs.find { it.name == output["outputType"] }.template + List fieldNames = metadataService.getOutputDataModel(templateName)?.dataModel?.name + + if (!fieldNames.containsAll(output["fields"])) { + throw new GraphQLException('Invalid Field: ' + output["fields"] + ' , suggested values are : ' + fieldNames) + } + } + } +} diff --git a/src/main/groovy/au/org/ala/ecodata/graphql/fetchers/OutputFetcher.groovy b/src/main/groovy/au/org/ala/ecodata/graphql/fetchers/OutputFetcher.groovy index af488af20..5ffd5c504 100644 --- a/src/main/groovy/au/org/ala/ecodata/graphql/fetchers/OutputFetcher.groovy +++ b/src/main/groovy/au/org/ala/ecodata/graphql/fetchers/OutputFetcher.groovy @@ -10,6 +10,7 @@ import au.org.ala.ecodata.SchemaBuilder import au.org.ala.ecodata.graphql.models.Schema import au.org.ala.ecodata.graphql.models.Summary import grails.core.GrailsApplication +import grails.util.Holders import graphql.schema.DataFetchingEnvironment import org.springframework.context.MessageSource @@ -67,4 +68,62 @@ class OutputFetcher implements graphql.schema.DataFetcher> { List get(DataFetchingEnvironment environment) { Output.findAll([max:10]) } + + List getFilteredOutput(List args, String activityType = null, String activityId = null) + { + //validate inputs + if(args){ + List arguments = [] + arguments.add(["outputs":args]) + new Helper(Holders.applicationContext.metadataService).validateActivityData(arguments) + } + + List requestedOutput = [] + def outputArgs = [] + + args.each { + if(activityType && it["activityType"] == activityType && it["output"]){ + requestedOutput = it["output"]["outputType"].flatten() + } + else if(!activityType && it["outputType"]) { + requestedOutput.add(it["outputType"]) + } + } + + if(args) { + if (activityType) { + outputArgs = args["output"] + } else { + outputArgs.add(args) + } + } + + def outputList = Output.where { + //get requested output types + if(requestedOutput.size() > 0) { + name in requestedOutput + } + if(activityId) { + activityId == activityId + } + }.each { + if (outputArgs) { + it.tempArgs.add(["output": outputArgs]) + } + }.sort{it.name}.findAll() + + //remove empty outputs + if(outputArgs) { + outputArgs.each { y -> + y.each { + if (it["fields"] && !it["fields"].contains(null)) { + outputList.removeAll { x -> + (!x.data || x.data.size() == 0) && x.name == it["outputType"] + } + } + } + } + } + return outputList + } } diff --git a/src/main/groovy/au/org/ala/ecodata/graphql/fetchers/ProjectsFetcher.groovy b/src/main/groovy/au/org/ala/ecodata/graphql/fetchers/ProjectsFetcher.groovy index fafa1b6dc..17f65fcc4 100644 --- a/src/main/groovy/au/org/ala/ecodata/graphql/fetchers/ProjectsFetcher.groovy +++ b/src/main/groovy/au/org/ala/ecodata/graphql/fetchers/ProjectsFetcher.groovy @@ -3,6 +3,7 @@ package au.org.ala.ecodata.graphql.fetchers import au.org.ala.ecodata.* import com.mongodb.client.FindIterable import com.mongodb.client.model.Filters +import grails.util.Holders import graphql.GraphQLException import graphql.schema.DataFetchingEnvironment import org.elasticsearch.action.search.SearchResponse @@ -53,7 +54,8 @@ class ProjectsFetcher implements graphql.schema.DataFetcher> { // add pagination results. String userId = environment.context.userId ?: '1493' String query = queryString ?:"*:*" - SearchResponse searchResponse = elasticSearchService.searchWithSecurity(userId, query, params, HOMEPAGE_INDEX) + //SearchResponse searchResponse = elasticSearchService.searchWithSecurity(userId, query, params, HOMEPAGE_INDEX) + SearchResponse searchResponse = elasticSearchService.search(query, params, HOMEPAGE_INDEX) List projectIds = searchResponse.hits.hits.collect{it.source.projectId} @@ -98,7 +100,7 @@ class ProjectsFetcher implements graphql.schema.DataFetcher> { "federalElectorate": "electFacet:", "assetsAddressed": "meriPlanAssetFacet:", "userNominatedProject": "custom.details.caseStudy:", "managementUnit": "muFacet:"] environment.arguments.each { - if(it.key in ["fromDate", "toDate", "dateRange"]) { + if(it.key in ["fromDate", "toDate", "dateRange", "activities", "projectId"]) { return } @@ -150,7 +152,16 @@ class ProjectsFetcher implements graphql.schema.DataFetcher> { params["toDate"] = environment.arguments.get("dateRange").to } - return queryElasticSearch(environment, "docType: project", params) + String query = "docType: project" + (environment.arguments.get("projectId") ? " AND projectId:" + environment.arguments.get("projectId") : "") + List projects = queryElasticSearch(environment, query, params) + + projects.each { + if(environment.arguments.get("activities")) { + it.tempArgs = environment.arguments.get("activities") as List + } + } + + return projects } void validateSearchQuery (DataFetchingEnvironment environment, List fqList) { @@ -167,7 +178,7 @@ class ProjectsFetcher implements graphql.schema.DataFetcher> { def datePattern = /\d{4}\-\d{2}\-\d{2}/ - //validate the format of the from anf to dates + //validate the format of the from and to dates if(environment.arguments.get("fromDate")) { if(!(environment.arguments.get("fromDate") ==~ datePattern)){ throw new GraphQLException('Invalid fromDate: fromDate should match yyyy-mm-dd') @@ -180,5 +191,13 @@ class ProjectsFetcher implements graphql.schema.DataFetcher> { } } + //validate activity types and output types + if(environment.arguments.get("activities")){ + List args = [] + args.add(["activities":environment.arguments.get("activities")]) + + new Helper(Holders.applicationContext.metadataService).validateActivityData(args) + } + } } diff --git a/src/main/groovy/au/org/ala/ecodata/graphql/mappers/ActivityGraphQLMapper.groovy b/src/main/groovy/au/org/ala/ecodata/graphql/mappers/ActivityGraphQLMapper.groovy index 68e234b71..318a178bd 100644 --- a/src/main/groovy/au/org/ala/ecodata/graphql/mappers/ActivityGraphQLMapper.groovy +++ b/src/main/groovy/au/org/ala/ecodata/graphql/mappers/ActivityGraphQLMapper.groovy @@ -2,6 +2,7 @@ package au.org.ala.ecodata.graphql.mappers import au.org.ala.ecodata.* import au.org.ala.ecodata.graphql.fetchers.ActivityFetcher +import au.org.ala.ecodata.graphql.fetchers.OutputFetcher import au.org.ala.ecodata.graphql.models.Schema import au.org.ala.ecodata.graphql.models.Summary import grails.gorm.DetachedCriteria @@ -9,6 +10,7 @@ import grails.util.Holders import graphql.schema.DataFetcher import graphql.schema.DataFetchingEnvironment import org.grails.gorm.graphql.entity.dsl.GraphQLMapping +import org.grails.gorm.graphql.fetcher.impl.ClosureDataFetchingEnvironment import org.grails.gorm.graphql.fetcher.impl.SingleEntityDataFetcher class ActivityGraphQLMapper { @@ -24,19 +26,36 @@ class ActivityGraphQLMapper { operations.delete.enabled false add('outputs', [Output]) { - dataFetcher { Activity activity -> - Output.findAllByActivityId(activity.activityId) + dataFetcher { Activity activity, ClosureDataFetchingEnvironment env -> + new OutputFetcher(Holders.applicationContext.metadataService, Holders.applicationContext.messageSource, Holders.grailsApplication).getFilteredOutput(activity.tempArgs as List, activity.type, activity.activityId) } input false } //get activity by activity id - query('activity', Activity) { - argument('activityId', String) - dataFetcher(new SingleEntityDataFetcher(Activity.gormPersistentEntity) { + query('activity', [Activity]) { + argument('activityId', String) { nullable true } + argument('activityList', 'activityList') { + accepts { + field('activityType', String) {nullable true} + field('output', 'outputs') { + field('outputType', String) {nullable false} + field('fields', [String]) {nullable true} + nullable true + //one activity can have zero or more output + collection true + } + //one project can have many activities + collection true + } + nullable true + } + + dataFetcher(new DataFetcher() { @Override - protected DetachedCriteria buildCriteria(DataFetchingEnvironment environment) { - Activity.where { activityId == environment.getArgument('activityId') } + Object get(DataFetchingEnvironment environment) throws Exception { + new ActivityFetcher(Holders.applicationContext.elasticSearchService, Holders.applicationContext.permissionService, Holders.applicationContext.metadataService, + Holders.applicationContext.messageSource, Holders.grailsApplication).getFilteredActivities(environment.arguments['activityList'] as List, null, environment.arguments['activityId'] as String) } }) } diff --git a/src/main/groovy/au/org/ala/ecodata/graphql/mappers/OutputGraphQLMapper.groovy b/src/main/groovy/au/org/ala/ecodata/graphql/mappers/OutputGraphQLMapper.groovy index 37fd996ec..babc7987a 100644 --- a/src/main/groovy/au/org/ala/ecodata/graphql/mappers/OutputGraphQLMapper.groovy +++ b/src/main/groovy/au/org/ala/ecodata/graphql/mappers/OutputGraphQLMapper.groovy @@ -9,6 +9,7 @@ import grails.util.Holders import graphql.schema.DataFetcher import graphql.schema.DataFetchingEnvironment import org.grails.gorm.graphql.entity.dsl.GraphQLMapping +import org.grails.gorm.graphql.fetcher.impl.ClosureDataFetchingEnvironment class OutputGraphQLMapper { @@ -24,14 +25,44 @@ class OutputGraphQLMapper { add('data', OutputData) { - dataFetcher { Output output -> - output.getData(output.data) + dataFetcher { Output output, ClosureDataFetchingEnvironment env -> + List fieldList = [] + if(output.tempArgs) { + output.tempArgs["output"].each { + it.each { x -> + x.each { y -> + if (y["outputType"] == output.name && y["fields"]) { + fieldList = y["fields"] as List + } + } + } + } + } + //get output data with requested fields + output.getData(fieldList) } input false } + query('outputs', [Output]) { + argument('output', 'output') { + accepts { + field('outputType', String) + field('fields', [String]) { nullable true } + collection true + } + nullable true + } + dataFetcher(new DataFetcher() { + @Override + Object get(DataFetchingEnvironment environment) throws Exception { + new OutputFetcher(Holders.applicationContext.metadataService, Holders.applicationContext.messageSource, Holders.grailsApplication).getFilteredOutput(environment.arguments['output'] as List) + } + }) + } + //get the list of output types - query('outputs', [Summary]) { + query('outputList', [Summary]) { dataFetcher(new DataFetcher() { @Override Object get(DataFetchingEnvironment environment) throws Exception { diff --git a/src/main/groovy/au/org/ala/ecodata/graphql/mappers/ProjectGraphQLMapper.groovy b/src/main/groovy/au/org/ala/ecodata/graphql/mappers/ProjectGraphQLMapper.groovy index 79100c778..e86b09e72 100644 --- a/src/main/groovy/au/org/ala/ecodata/graphql/mappers/ProjectGraphQLMapper.groovy +++ b/src/main/groovy/au/org/ala/ecodata/graphql/mappers/ProjectGraphQLMapper.groovy @@ -1,5 +1,6 @@ package au.org.ala.ecodata.graphql.mappers +import au.org.ala.ecodata.Activity import au.org.ala.ecodata.Document import au.org.ala.ecodata.Project import au.org.ala.ecodata.Report @@ -7,6 +8,7 @@ import au.org.ala.ecodata.Site import au.org.ala.ecodata.Status import au.org.ala.ecodata.graphql.enums.DateRange import au.org.ala.ecodata.graphql.enums.YesNo +import au.org.ala.ecodata.graphql.fetchers.ActivityFetcher import au.org.ala.ecodata.graphql.fetchers.ProjectsFetcher import au.org.ala.ecodata.graphql.models.MeriPlan import grails.gorm.DetachedCriteria @@ -68,6 +70,13 @@ class ProjectGraphQLMapper { } } + add('activities', [Activity]) { + dataFetcher { Project project -> + new ActivityFetcher(Holders.applicationContext.elasticSearchService, Holders.applicationContext.permissionService, Holders.applicationContext.metadataService, + Holders.applicationContext.messageSource, Holders.grailsApplication).getFilteredActivities(project.tempArgs, project.projectId) + } + } + // get project by ID query('project', Project) { argument('projectId', String) @@ -90,6 +99,7 @@ class ProjectGraphQLMapper { } query('searchMeritProject', [Project]) { + argument('projectId', String) { nullable true } argument('fromDate', String){ nullable true description "yyyy-mm-dd" } argument('toDate', String){ nullable true description "yyyy-mm-dd" } argument('dateRange', DateRange){ nullable true } @@ -118,6 +128,23 @@ class ProjectGraphQLMapper { argument('userNominatedProject', [String]){ nullable true } argument('managementUnit', [String]){ nullable true } + //activities filter + argument('activities', 'activities') { + accepts { + field('activityType', String) {nullable true} + field('output', 'output') { + field('outputType', String) {nullable false} + field('fields', [String]) {nullable true} + nullable true + //one activity can have zero or more output + collection true + } + //one project can have many activities + collection true + } + nullable true + } + dataFetcher(new DataFetcher() { @Override Object get(DataFetchingEnvironment environment) throws Exception { From 672af77a19950f1c44f79bdd2cdf307d7c97823f Mon Sep 17 00:00:00 2001 From: yasima-csiro Date: Tue, 12 Jan 2021 15:55:42 +1100 Subject: [PATCH 006/144] Add dashboard APIs --- .../graphql/fetchers/ProjectsFetcher.groovy | 228 +++++++++++++++--- .../mappers/ProjectGraphQLMapper.groovy | 130 +++++++++- 2 files changed, 319 insertions(+), 39 deletions(-) diff --git a/src/main/groovy/au/org/ala/ecodata/graphql/fetchers/ProjectsFetcher.groovy b/src/main/groovy/au/org/ala/ecodata/graphql/fetchers/ProjectsFetcher.groovy index 17f65fcc4..4d6fbc0f1 100644 --- a/src/main/groovy/au/org/ala/ecodata/graphql/fetchers/ProjectsFetcher.groovy +++ b/src/main/groovy/au/org/ala/ecodata/graphql/fetchers/ProjectsFetcher.groovy @@ -1,6 +1,8 @@ package au.org.ala.ecodata.graphql.fetchers import au.org.ala.ecodata.* +import au.org.ala.ecodata.graphql.models.KeyValue +import au.org.ala.ecodata.graphql.models.OutputData import com.mongodb.client.FindIterable import com.mongodb.client.model.Filters import grails.util.Holders @@ -9,20 +11,24 @@ import graphql.schema.DataFetchingEnvironment import org.elasticsearch.action.search.SearchResponse import static au.org.ala.ecodata.ElasticIndex.HOMEPAGE_INDEX +import static au.org.ala.ecodata.Status.DELETED class ProjectsFetcher implements graphql.schema.DataFetcher> { - public ProjectsFetcher(ProjectService projectService, ElasticSearchService elasticSearchService, PermissionService permissionService) { + public ProjectsFetcher(ProjectService projectService, ElasticSearchService elasticSearchService, PermissionService permissionService, ReportService reportService, CacheService cacheService) { this.projectService = projectService this.elasticSearchService = elasticSearchService this.permissionService = permissionService + this.reportService = reportService + this.cacheService = cacheService } PermissionService permissionService ElasticSearchService elasticSearchService - + ReportService reportService ProjectService projectService + CacheService cacheService static String meritFacets = "status,organisationFacet,associatedProgramFacet,associatedSubProgramFacet,mainThemeFacet,stateFacet,nrmFacet,lgaFacet,mvgFacet,ibraFacet,imcra4_pbFacet,otherFacet,electFacet,meriPlanAssetFacet," + "cmzFacet,partnerOrganisationTypeFacet,promoteOnHomepage,custom.details.caseStudy,primaryOutcomeFacet,secondaryOutcomesFacet,muFacet,tags,fundingSourceFacet" @@ -95,42 +101,7 @@ class ProjectsFetcher implements graphql.schema.DataFetcher> { List searchMeritProject (DataFetchingEnvironment environment) { - def fqList = [] - def facetMappings = ["managementArea": "nrmFacet:", "majorVegetationGroup": "mvgFacet:", "biogeographicRegion": "ibraFacet:", "marineRegion": "imcra4_pbFacet:", "otherRegion": "otherFacet:", "grantManagerNominatedProject":"promoteOnHomepage:", - "federalElectorate": "electFacet:", "assetsAddressed": "meriPlanAssetFacet:", "userNominatedProject": "custom.details.caseStudy:", "managementUnit": "muFacet:"] - - environment.arguments.each { - if(it.key in ["fromDate", "toDate", "dateRange", "activities", "projectId"]) { - return - } - - String key - - it.value.each { val -> - switch (it.key) { - case "status": - case "tags": - key = it.key + ":" - break; - case "managementArea" : - case "majorVegetationGroup" : - case "biogeographicRegion" : - case "marineRegion" : - case "otherRegion" : - case "grantManagerNominatedProject" : - case "federalElectorate" : - case "assetsAddressed" : - case "userNominatedProject" : - case "managementUnit" : - key = facetMappings.get(it.key) - break; - default: - key = it.key + "Facet:" - break; - } - fqList << key + val; - } - } + def fqList = mapFq(environment) //validate the query validateSearchQuery(environment, fqList) @@ -200,4 +171,185 @@ class ProjectsFetcher implements graphql.schema.DataFetcher> { } } + + def searchActivityOutput (DataFetchingEnvironment environment) { + + if(environment.arguments.get("activityOutputs")) { + validateActivityOutputInput(environment) + } + + def fqList = mapFq(environment) + + validateSearchQuery(environment, fqList) + + List scores = Score.findAll() + def results = getActivityOutputs(fqList, scores) + + List outputs = results.outputData + //filter the output based on the filtering values + if(environment.arguments.get("activityOutputs")) { + def filteredOutputs = [] + results.outputData.each { + if (it.category in environment.arguments.get("activityOutputs").getAt("category")) { + filteredOutputs.add(it) + } + } + environment.arguments.get("activityOutputs").each { activityOutput -> + if(activityOutput.outputs) { + def unWanted = [] + filteredOutputs.each { + if(activityOutput.category == it.category) { + if (!(it.outputType in activityOutput.outputs.outputType)) { + unWanted.add(it) + } + + if (activityOutput.outputs.labels[0] && activityOutput.outputs.labels[0].size() != 0 && !activityOutput.outputs.labels.contains(null)) { + if (it.outputType in activityOutput.outputs.outputType && !(it.label in activityOutput.outputs.labels[0])) { + unWanted.add(it) + } + } + } + } + filteredOutputs = filteredOutputs.minus(unWanted) + } + } + outputs = filteredOutputs + } + + outputs.each { + if(it["result"]["result"] != null && !it["result"]["result"].toString().isNumber()){ + OutputData outputData = new OutputData(dataList: new ArrayList()) + it["result"]["result"].each{ list -> + outputData.dataList.add(new KeyValue(key: list.key, value: list.value)) + } + it["result"]["resultList"] = outputData + it["result"]["result"] = null + } + } + return [outputData : outputs ] + } + + def searchOutputTargetsByProgram (DataFetchingEnvironment environment) { + + def fqList = mapFq(environment) + + validateSearchQuery(environment, fqList) + + Map params = [hubFq:"isMERIT:true", controller:"search", showOrganisations:true, report:"outputTargets", action:"targetsReport", fq:fqList, format:null] + + def targets = reportService.outputTargetsBySubProgram(params) + //def scores = reportService.outputTargetReport(fqList, "*:*") + + def targetList = [] + + targets.each { + def target = [:] + //remove null values + if(it.value != null && it.value.entrySet().key.contains(null)) { + it.value = it.value.remove(it.value.get(null)) + } + + if(!environment.arguments.get("programs") || environment.arguments.get("programs").contains(it.key)) { + target["program"] = it.key + target["outputTargetMeasure"] = [] + it.value.each { x -> + if(!environment.arguments.get("outputTargetMeasures") || environment.arguments.get("outputTargetMeasures").contains(x.key)) { + def targetMeasure = [:] + targetMeasure["outputTarget"] = x.key + targetMeasure["count"] = x.value.count + targetMeasure["total"] = x.value.total + target["outputTargetMeasure"] << targetMeasure + } + } + targetList.add(target) + } + } + + return [targets:targetList] + } + + def mapFq(DataFetchingEnvironment environment) { + + def fqList = [] + def facetMappings = ["managementArea": "nrmFacet:", "majorVegetationGroup": "mvgFacet:", "biogeographicRegion": "ibraFacet:", "marineRegion": "imcra4_pbFacet:", "otherRegion": "otherFacet:", "grantManagerNominatedProject":"promoteOnHomepage:", + "federalElectorate": "electFacet:", "assetsAddressed": "meriPlanAssetFacet:", "userNominatedProject": "custom.details.caseStudy:", "managementUnit": "muFacet:"] + + environment.arguments.each { + if(it.key in ["fromDate", "toDate", "dateRange", "activities", "projectId", "activityOutputs", "programs", "outputTargetMeasures"]) { + return + } + + String key + + it.value.each { val -> + switch (it.key) { + case "status": + case "tags": + key = it.key + ":" + break; + case "managementArea" : + case "majorVegetationGroup" : + case "biogeographicRegion" : + case "marineRegion" : + case "otherRegion" : + case "grantManagerNominatedProject" : + case "federalElectorate" : + case "assetsAddressed" : + case "userNominatedProject" : + case "managementUnit" : + key = facetMappings.get(it.key) + break; + default: + key = it.key + "Facet:" + break; + } + fqList << key + val; + } + } + + return fqList + } + + def getActivityOutputs(List fqList, List scores) { + return cacheService.get("dashboard-activityOutput-"+fqList, { + reportService.aggregate(fqList, "docType:project", scores) + }) + } + + void validateActivityOutputInput (DataFetchingEnvironment environment) { + + def categories = [] + List scores = Score.findAllWhereStatusNotEqual(DELETED) + + scores.each { score -> + def cat = score.category?.trim() + if (cat && !categories.contains(cat)) { + categories << cat + } + } + + environment.arguments.get("activityOutputs").each { + if(!(it.category in categories)) { + throw new GraphQLException('Invalid category ' + it.category +' : suggested values are : ' + categories) + } + if(it.outputs) { + it.outputs.each{ outputs -> + def outputTypes = scores.findAll { score -> score.category == it.category}.outputType.unique() + if(outputs.outputType && !(outputs.outputType in outputTypes)){ + throw new GraphQLException('Invalid outputType ' + outputs.outputType +' : suggested values are : ' + outputTypes) + } + } + + if (it.outputs.labels[0] && it.outputs.labels[0].size() != 0 && !it.outputs.labels.contains(null)) { + def labels = scores.findAll { score -> score.category == it.category && it.outputs.outputType.contains(score.outputType)}?.label.unique() + + it.outputs.labels[0].each { label -> + if (!(label in labels)) { + throw new GraphQLException('Invalid label ' + label + ' : suggested values are : ' + labels) + } + } + } + } + } + } } diff --git a/src/main/groovy/au/org/ala/ecodata/graphql/mappers/ProjectGraphQLMapper.groovy b/src/main/groovy/au/org/ala/ecodata/graphql/mappers/ProjectGraphQLMapper.groovy index e86b09e72..eeb2d930c 100644 --- a/src/main/groovy/au/org/ala/ecodata/graphql/mappers/ProjectGraphQLMapper.groovy +++ b/src/main/groovy/au/org/ala/ecodata/graphql/mappers/ProjectGraphQLMapper.groovy @@ -11,6 +11,7 @@ import au.org.ala.ecodata.graphql.enums.YesNo import au.org.ala.ecodata.graphql.fetchers.ActivityFetcher import au.org.ala.ecodata.graphql.fetchers.ProjectsFetcher import au.org.ala.ecodata.graphql.models.MeriPlan +import au.org.ala.ecodata.graphql.models.OutputData import grails.gorm.DetachedCriteria import grails.util.Holders import graphql.schema.DataFetcher @@ -153,13 +154,140 @@ class ProjectGraphQLMapper { }) } + query('activityOutput', "activityOutput") { + argument('fromDate', String){ nullable true description "yyyy-mm-dd" } + argument('toDate', String){ nullable true description "yyyy-mm-dd" } + argument('dateRange', DateRange){ nullable true } + argument('status', [String]){ nullable true } + argument('organisation', [String]){ nullable true } + argument('associatedProgram', [String]){ nullable true } + argument('associatedSubProgram', [String]){ nullable true } + argument('mainTheme', [String]){ nullable true } + argument('state', [String]){ nullable true } + argument('lga', [String]){ nullable true } + argument('cmz', [String]){ nullable true } + argument('partnerOrganisationType', [String]){ nullable true } + argument('associatedSubProgram', [String]){ nullable true } + argument('primaryOutcome', [String]){ nullable true } + argument('secondaryOutcomes', [String]){ nullable true } + argument('tags', [String]){ nullable true } + + argument('managementArea', [String]){ nullable true } + argument('majorVegetationGroup', [String]){ nullable true } + argument('biogeographicRegion', [String]){ nullable true } + argument('marineRegion', [String]){ nullable true } + argument('otherRegion', [String]){ nullable true } + argument('grantManagerNominatedProject', [YesNo]){ nullable true } + argument('federalElectorate', [String]){ nullable true } + argument('assetsAddressed', [String]){ nullable true } + argument('userNominatedProject', [String]){ nullable true } + argument('managementUnit', [String]){ nullable true } + + argument('activityOutputs', 'activityOutputs') { + accepts { + field('category', String) {nullable false} + field('outputs', 'outputList') { + field('outputType', String) {nullable false} + field('labels', [String]) {nullable true} + nullable true + collection true + } + collection true + } + nullable true + } + + dataFetcher(new DataFetcher() { + @Override + Object get(DataFetchingEnvironment environment) throws Exception { + ProjectGraphQLMapper.buildTestFetcher().searchActivityOutput(environment) + } + }) + returns { + field('outputData', 'outputData') { + field('category', String) + field('outputType', String) + field('result', 'result') { + field('label', String) + field('result', double) {nullable true} + field('resultList', OutputData) {nullable true} + field('groups', 'groups') { + field('group', String) + field('results', 'results') { + field('count', int) + field('result', double) + collection true + } + nullable true + collection true + } + } + collection true + } + } + } + + query('outputTargetsByProgram', "outputTargetsByProgram") { + argument('fromDate', String){ nullable true description "yyyy-mm-dd" } + argument('toDate', String){ nullable true description "yyyy-mm-dd" } + argument('dateRange', DateRange){ nullable true } + argument('status', [String]){ nullable true } + argument('organisation', [String]){ nullable true } + argument('associatedProgram', [String]){ nullable true } + argument('associatedSubProgram', [String]){ nullable true } + argument('mainTheme', [String]){ nullable true } + argument('state', [String]){ nullable true } + argument('lga', [String]){ nullable true } + argument('cmz', [String]){ nullable true } + argument('partnerOrganisationType', [String]){ nullable true } + argument('associatedSubProgram', [String]){ nullable true } + argument('primaryOutcome', [String]){ nullable true } + argument('secondaryOutcomes', [String]){ nullable true } + argument('tags', [String]){ nullable true } + + argument('managementArea', [String]){ nullable true } + argument('majorVegetationGroup', [String]){ nullable true } + argument('biogeographicRegion', [String]){ nullable true } + argument('marineRegion', [String]){ nullable true } + argument('otherRegion', [String]){ nullable true } + argument('grantManagerNominatedProject', [YesNo]){ nullable true } + argument('federalElectorate', [String]){ nullable true } + argument('assetsAddressed', [String]){ nullable true } + argument('userNominatedProject', [String]){ nullable true } + argument('managementUnit', [String]){ nullable true } + + argument('programs', [String]) {nullable true} + argument('outputTargetMeasures', [String]) {nullable true} + + dataFetcher(new DataFetcher() { + @Override + Object get(DataFetchingEnvironment environment) throws Exception { + ProjectGraphQLMapper.buildTestFetcher().searchOutputTargetsByProgram(environment) + } + }) + returns { + field('targets', 'targets') { + field('program', String) + field('outputTargetMeasure', 'outputTargetMeasure') { + field('outputTarget', String) + field('count', int) + field('total', double) + collection true + } + nullable true + collection true + } + } + } + } } static ProjectsFetcher buildTestFetcher() { - new ProjectsFetcher(Holders.applicationContext.projectService, Holders.applicationContext.elasticSearchService, Holders.applicationContext.permissionService) + new ProjectsFetcher(Holders.applicationContext.projectService, Holders.applicationContext.elasticSearchService, Holders.applicationContext.permissionService, Holders.applicationContext.reportService, + Holders.applicationContext.cacheService) } } From 23fb5823a7ea30d266bd28ac9a1fba861836c8cf Mon Sep 17 00:00:00 2001 From: yasima-csiro Date: Wed, 20 Jan 2021 08:47:01 +1100 Subject: [PATCH 007/144] Add activity output graphql types --- .../graphql/fetchers/ActivityFetcher.groovy | 51 +++++++++++++++ .../ecodata/graphql/fetchers/Helper.groovy | 43 ++++++++++++ .../mappers/ProjectGraphQLMapper.groovy | 65 +++++++++++++++++++ 3 files changed, 159 insertions(+) diff --git a/src/main/groovy/au/org/ala/ecodata/graphql/fetchers/ActivityFetcher.groovy b/src/main/groovy/au/org/ala/ecodata/graphql/fetchers/ActivityFetcher.groovy index 46e32d33f..9ea29fc78 100644 --- a/src/main/groovy/au/org/ala/ecodata/graphql/fetchers/ActivityFetcher.groovy +++ b/src/main/groovy/au/org/ala/ecodata/graphql/fetchers/ActivityFetcher.groovy @@ -6,6 +6,7 @@ import au.org.ala.ecodata.graphql.models.Summary import grails.core.GrailsApplication import grails.util.Holders import graphql.schema.DataFetchingEnvironment +import org.apache.commons.lang.WordUtils import org.elasticsearch.action.search.SearchResponse import org.springframework.context.MessageSource @@ -136,4 +137,54 @@ class ActivityFetcher implements graphql.schema.DataFetcher> { return activityList } + + /*** + * This method is used to get the activity output data of a given project + * @param args + * @param givenProjectId + * @param activityName + * @param outputTypes + * @param modifiedColumns + * @return + */ + def getActivityData(List args, String givenProjectId, String activityName, List outputTypes,List modifiedColumns) + { + //get the activity list of a given project + List projectActivities = getFilteredActivities(args, givenProjectId) + + //get the activity Id of the activities of the given activity type + List activityIds = projectActivities.findAll{ WordUtils.capitalize(it.type).replaceAll("\\W", "") == activityName}.activityId + + //get output data + List outputList = new OutputFetcher(Holders.applicationContext.metadataService, Holders.applicationContext.messageSource, Holders.grailsApplication).getFilteredOutput(args) + + //get the output data of the activities of the project + def outputs = outputList.findAll {it.activityId in activityIds} + + def activities = [:] + outputTypes.each{ + activities."$it" = [] + } + //format the output data to match the graphql type format + outputs.each{ + String name = "OutputType_" + WordUtils.capitalize(it.name).replaceAll("\\W", "") + if(!(name in outputTypes)){ + name = "OutputType_" + activityName + "_" + WordUtils.capitalize(it.name).replaceAll("\\W", "") + } + if(name in outputTypes) { + def dataList = [:] + it.data.each { d -> + if (d.key in modifiedColumns) { + dataList.put(name + "_" + d.key, d.value) + } + else{ + dataList.put(d.key, d.value); + } + } + activities."$name" << dataList + } + } + + return activities + } } diff --git a/src/main/groovy/au/org/ala/ecodata/graphql/fetchers/Helper.groovy b/src/main/groovy/au/org/ala/ecodata/graphql/fetchers/Helper.groovy index d974999cc..1d5cb87c5 100644 --- a/src/main/groovy/au/org/ala/ecodata/graphql/fetchers/Helper.groovy +++ b/src/main/groovy/au/org/ala/ecodata/graphql/fetchers/Helper.groovy @@ -1,6 +1,10 @@ package au.org.ala.ecodata.graphql.fetchers +import au.org.ala.ecodata.ActivityForm +import au.org.ala.ecodata.FormSection import au.org.ala.ecodata.MetadataService +import au.org.ala.ecodata.PublicationStatus +import au.org.ala.ecodata.Status import graphql.GraphQLException class Helper { @@ -10,6 +14,9 @@ class Helper { Helper(MetadataService metadataService) { this.metadataService = metadataService } + + Helper() { + } /** * This can be used to validate the given activity types and the output types */ @@ -64,4 +71,40 @@ class Helper { } } } + + /*** + * This method is used to get activity output model + * @return + */ + Map getActivityOutputModels(){ + Map activitiesModel = [activities:[]] + + Map maxVersionsByName = [:] + Map activitiesByName = [:] + + ActivityForm.findAllWhereStatusNotEqualAndPublicationStatusEquals(Status.DELETED, PublicationStatus.PUBLISHED).each { ActivityForm activityForm -> + Map activityModel = [ + name: activityForm.name, + outputs: [] + ] + + activityForm.sections.unique().each { FormSection section -> + activityModel.outputs << [ + name: section.name, + fields: section.template.dataModel != null ? section.template.dataModel : [] + ] + } + + if (!maxVersionsByName[activityForm.name] || (maxVersionsByName[activityForm.name] < activityForm.formVersion)) { + maxVersionsByName[activityForm.name] = activityForm.formVersion + activitiesByName[activityForm.name] = activityModel + } + } + // Assemble the latest version of each activity into the model. + activitiesByName.each { String name, Map activityModel -> + activitiesModel.activities << activityModel + } + + activitiesModel + } } diff --git a/src/main/groovy/au/org/ala/ecodata/graphql/mappers/ProjectGraphQLMapper.groovy b/src/main/groovy/au/org/ala/ecodata/graphql/mappers/ProjectGraphQLMapper.groovy index eeb2d930c..370bc61da 100644 --- a/src/main/groovy/au/org/ala/ecodata/graphql/mappers/ProjectGraphQLMapper.groovy +++ b/src/main/groovy/au/org/ala/ecodata/graphql/mappers/ProjectGraphQLMapper.groovy @@ -9,6 +9,7 @@ import au.org.ala.ecodata.Status import au.org.ala.ecodata.graphql.enums.DateRange import au.org.ala.ecodata.graphql.enums.YesNo import au.org.ala.ecodata.graphql.fetchers.ActivityFetcher +import au.org.ala.ecodata.graphql.fetchers.Helper import au.org.ala.ecodata.graphql.fetchers.ProjectsFetcher import au.org.ala.ecodata.graphql.models.MeriPlan import au.org.ala.ecodata.graphql.models.OutputData @@ -16,6 +17,7 @@ import grails.gorm.DetachedCriteria import grails.util.Holders import graphql.schema.DataFetcher import graphql.schema.DataFetchingEnvironment +import org.apache.commons.lang.WordUtils import org.grails.gorm.graphql.entity.dsl.GraphQLMapping import org.grails.gorm.graphql.fetcher.impl.ClosureDataFetchingEnvironment import org.grails.gorm.graphql.fetcher.impl.SingleEntityDataFetcher @@ -35,6 +37,9 @@ class ProjectGraphQLMapper { exclude("custom") + Map activityModel = new Helper().getActivityOutputModels() + String[] duplicateOutputs = activityModel["activities"].outputs.name.flatten().groupBy { it }.findAll { it.value.size() > 1}.keySet() + List restrictedProperties = [] restrictedProperties.each { String prop -> property(prop) { @@ -78,6 +83,66 @@ class ProjectGraphQLMapper { } } + //add graphql type for each activity type + activityModel["activities"].each { + if(it.name && it.outputs && it.outputs.size() > 0 && it.outputs.fields?.findAll{ x -> x?.size() != 0 }?.size() > 0){ + def outputTypes = it.outputs + String activityName = WordUtils.capitalize(it.name).replaceAll("\\W", "") + String name = "Activity_" + activityName + List outputList = [] + List modifiedColumns = [] + //define activity type + add(name, name) { + type { + outputTypes.each { outputType -> + String outputName = WordUtils.capitalize(outputType.name).replaceAll("\\W", "") + String outputTypeName = "OutputType_" + outputName + if(outputType.fields?.size() > 0 && !(outputTypeName in outputList)) { + if(duplicateOutputs.contains(outputType.name)){ + outputTypeName = "OutputType_" + activityName + "_" + outputName + } + outputList << outputTypeName + //define output types and fields of the activity + field(outputTypeName, outputTypeName) { + String[] fieldList = outputType.fields.name.unique() + for(int t=0; t b.name == fieldList[t]} + if(outputField.dataType == "number") { + field(fieldList[t].toString(), double) + } + else if(outputField.dataType == "text") { + field(fieldList[t].toString(), String) + } + else if(outputField.dataType == "list") { + modifiedColumns << fieldList[t].toString() + field(outputTypeName + "_" + fieldList[t].toString(), outputTypeName + "_" + fieldList[t].toString()){ + String[] columnList = outputField.columns.name.unique() + for(int y=0; y b.name == columnList[y]} + field(column.name.toString(), column.dataType == "number" ? double : String) + } + collection true + } + } + else { + field(fieldList[t].toString(), String) + } + } + nullable true + collection true + } + } + } + } + dataFetcher { Project project -> + def s=new ActivityFetcher(Holders.applicationContext.elasticSearchService, Holders.applicationContext.permissionService, Holders.applicationContext.metadataService, + Holders.applicationContext.messageSource, Holders.grailsApplication).getActivityData(project.tempArgs, project.projectId, activityName, outputList, modifiedColumns) + return s + } + } + } + } + // get project by ID query('project', Project) { argument('projectId', String) From 390eccb2bb0b85c362204b1aa3d5929ddfbd02bc Mon Sep 17 00:00:00 2001 From: yasima-csiro Date: Thu, 21 Jan 2021 08:36:15 +1100 Subject: [PATCH 008/144] Add activity output filtering --- .../graphql/fetchers/ActivityFetcher.groovy | 1 + .../ecodata/graphql/fetchers/OutputFetcher.groovy | 2 ++ .../graphql/fetchers/ProjectsFetcher.groovy | 14 ++++++++++---- .../graphql/mappers/ProjectGraphQLMapper.groovy | 8 -------- 4 files changed, 13 insertions(+), 12 deletions(-) diff --git a/src/main/groovy/au/org/ala/ecodata/graphql/fetchers/ActivityFetcher.groovy b/src/main/groovy/au/org/ala/ecodata/graphql/fetchers/ActivityFetcher.groovy index 9ea29fc78..c20608fd1 100644 --- a/src/main/groovy/au/org/ala/ecodata/graphql/fetchers/ActivityFetcher.groovy +++ b/src/main/groovy/au/org/ala/ecodata/graphql/fetchers/ActivityFetcher.groovy @@ -131,6 +131,7 @@ class ActivityFetcher implements graphql.schema.DataFetcher> { if(activityIdList.size() > 0) { activityId in activityIdList.activityId } + status != Status.DELETED }.each { it.tempArgs = args ?: [] }.sort { it.type} diff --git a/src/main/groovy/au/org/ala/ecodata/graphql/fetchers/OutputFetcher.groovy b/src/main/groovy/au/org/ala/ecodata/graphql/fetchers/OutputFetcher.groovy index 5ffd5c504..60ebe9466 100644 --- a/src/main/groovy/au/org/ala/ecodata/graphql/fetchers/OutputFetcher.groovy +++ b/src/main/groovy/au/org/ala/ecodata/graphql/fetchers/OutputFetcher.groovy @@ -7,6 +7,7 @@ import au.org.ala.ecodata.MetadataService import au.org.ala.ecodata.Output import au.org.ala.ecodata.PermissionService import au.org.ala.ecodata.SchemaBuilder +import au.org.ala.ecodata.Status import au.org.ala.ecodata.graphql.models.Schema import au.org.ala.ecodata.graphql.models.Summary import grails.core.GrailsApplication @@ -106,6 +107,7 @@ class OutputFetcher implements graphql.schema.DataFetcher> { if(activityId) { activityId == activityId } + status != Status.DELETED }.each { if (outputArgs) { it.tempArgs.add(["output": outputArgs]) diff --git a/src/main/groovy/au/org/ala/ecodata/graphql/fetchers/ProjectsFetcher.groovy b/src/main/groovy/au/org/ala/ecodata/graphql/fetchers/ProjectsFetcher.groovy index 4d6fbc0f1..4c2591775 100644 --- a/src/main/groovy/au/org/ala/ecodata/graphql/fetchers/ProjectsFetcher.groovy +++ b/src/main/groovy/au/org/ala/ecodata/graphql/fetchers/ProjectsFetcher.groovy @@ -126,10 +126,16 @@ class ProjectsFetcher implements graphql.schema.DataFetcher> { String query = "docType: project" + (environment.arguments.get("projectId") ? " AND projectId:" + environment.arguments.get("projectId") : "") List projects = queryElasticSearch(environment, query, params) - projects.each { - if(environment.arguments.get("activities")) { - it.tempArgs = environment.arguments.get("activities") as List - } + if(environment.arguments.get("activities")) { + List projectIdList = projects.projectId + + List activities = new ActivityFetcher(Holders.applicationContext.elasticSearchService, Holders.applicationContext.permissionService, Holders.applicationContext.metadataService, + Holders.applicationContext.messageSource, Holders.grailsApplication).getFilteredActivities(environment.arguments.get("activities") as List) + + //get projects with requested activity output types + List projectIds = activities.findAll { it.projectId in projectIdList }.projectId.unique() + + projects = projects.findAll{ it.projectId in projectIds} } return projects diff --git a/src/main/groovy/au/org/ala/ecodata/graphql/mappers/ProjectGraphQLMapper.groovy b/src/main/groovy/au/org/ala/ecodata/graphql/mappers/ProjectGraphQLMapper.groovy index 370bc61da..bdb74b104 100644 --- a/src/main/groovy/au/org/ala/ecodata/graphql/mappers/ProjectGraphQLMapper.groovy +++ b/src/main/groovy/au/org/ala/ecodata/graphql/mappers/ProjectGraphQLMapper.groovy @@ -76,13 +76,6 @@ class ProjectGraphQLMapper { } } - add('activities', [Activity]) { - dataFetcher { Project project -> - new ActivityFetcher(Holders.applicationContext.elasticSearchService, Holders.applicationContext.permissionService, Holders.applicationContext.metadataService, - Holders.applicationContext.messageSource, Holders.grailsApplication).getFilteredActivities(project.tempArgs, project.projectId) - } - } - //add graphql type for each activity type activityModel["activities"].each { if(it.name && it.outputs && it.outputs.size() > 0 && it.outputs.fields?.findAll{ x -> x?.size() != 0 }?.size() > 0){ @@ -200,7 +193,6 @@ class ProjectGraphQLMapper { field('activityType', String) {nullable true} field('output', 'output') { field('outputType', String) {nullable false} - field('fields', [String]) {nullable true} nullable true //one activity can have zero or more output collection true From de977f43f0f97a8cbed9d0adf70c77b72dba3111 Mon Sep 17 00:00:00 2001 From: yasima-csiro Date: Fri, 29 Jan 2021 16:23:20 +1100 Subject: [PATCH 009/144] Add BioCollect project search API --- .../graphql/EcodataGraphQLCustomiser.groovy | 4 +- .../graphql/converters/DateFormatting.groovy | 39 ++++ .../graphql/enums/ProjectStatus.groovy | 6 + .../graphql/fetchers/ProjectsFetcher.groovy | 188 +++++++++++++++++- .../mappers/ProjectGraphQLMapper.groovy | 46 +++++ 5 files changed, 271 insertions(+), 12 deletions(-) create mode 100644 src/main/groovy/au/org/ala/ecodata/graphql/converters/DateFormatting.groovy create mode 100644 src/main/groovy/au/org/ala/ecodata/graphql/enums/ProjectStatus.groovy diff --git a/src/main/groovy/au/org/ala/ecodata/graphql/EcodataGraphQLCustomiser.groovy b/src/main/groovy/au/org/ala/ecodata/graphql/EcodataGraphQLCustomiser.groovy index b5ed7ff15..42f1f901c 100644 --- a/src/main/groovy/au/org/ala/ecodata/graphql/EcodataGraphQLCustomiser.groovy +++ b/src/main/groovy/au/org/ala/ecodata/graphql/EcodataGraphQLCustomiser.groovy @@ -1,5 +1,6 @@ package au.org.ala.ecodata.graphql +import au.org.ala.ecodata.graphql.converters.DateFormatting import au.org.ala.ecodata.graphql.converters.MeriPlanConverter import au.org.ala.ecodata.graphql.converters.ObjectConverter import au.org.ala.ecodata.graphql.converters.ObjectIdConverter @@ -14,14 +15,12 @@ import au.org.ala.ecodata.graphql.models.SectionTemplate import au.org.ala.ecodata.graphql.models.Summary import graphql.schema.DataFetchingEnvironment import graphql.schema.GraphQLFieldDefinition -import graphql.schema.GraphQLInterfaceType import graphql.schema.GraphQLObjectType import graphql.schema.GraphQLScalarType import graphql.schema.GraphQLType import org.bson.types.ObjectId import org.grails.datastore.mapping.model.PersistentEntity import org.grails.gorm.graphql.fetcher.GraphQLDataFetcherType -import org.grails.gorm.graphql.fetcher.manager.GraphQLDataFetcherManager import org.grails.gorm.graphql.interceptor.GraphQLFetcherInterceptor import org.grails.gorm.graphql.interceptor.GraphQLSchemaInterceptor import org.grails.gorm.graphql.interceptor.manager.GraphQLInterceptorManager @@ -41,6 +40,7 @@ class EcodataGraphQLCustomiser extends GraphQLPostProcessor { graphQLTypeManager.registerType(SectionTemplate, new GraphQLScalarType("SectionTemplate", "", new SectionTemplateConverter())) graphQLTypeManager.registerType(Summary, new GraphQLScalarType("Summary", "", new SummaryConverter())) graphQLTypeManager.registerType(Schema, new GraphQLScalarType("Schema", "", new SchemaConverter())) + graphQLTypeManager.registerType(Date, new GraphQLScalarType("Date", "", new DateFormatting())) } @Override diff --git a/src/main/groovy/au/org/ala/ecodata/graphql/converters/DateFormatting.groovy b/src/main/groovy/au/org/ala/ecodata/graphql/converters/DateFormatting.groovy new file mode 100644 index 000000000..02740d85a --- /dev/null +++ b/src/main/groovy/au/org/ala/ecodata/graphql/converters/DateFormatting.groovy @@ -0,0 +1,39 @@ +package au.org.ala.ecodata.graphql.converters + +import graphql.schema.Coercing +import graphql.schema.CoercingParseValueException +import graphql.schema.CoercingSerializeException; + +/*** + * This class is used to convert the ISO date string to yyyy-MM-dd format + */ +class DateFormatting implements Coercing{ + + protected Optional convert(Object input) { + if (input instanceof Date) { + Optional.of(input.format( 'yyyy-MM-dd' )) + } + else { + Optional.empty() + } + } + + @Override + String serialize(Object input) { + convert(input).orElseThrow( { + throw new CoercingSerializeException("Could not convert ${input.class.name} to a Date") + }) + } + + @Override + String parseValue(Object input) { + convert(input).orElseThrow( { + throw new CoercingParseValueException("Could not convert ${input.class.name} to a Date") + }) + } + + @Override + String parseLiteral(Object input) { + null + } + } diff --git a/src/main/groovy/au/org/ala/ecodata/graphql/enums/ProjectStatus.groovy b/src/main/groovy/au/org/ala/ecodata/graphql/enums/ProjectStatus.groovy new file mode 100644 index 000000000..0fa4f7c04 --- /dev/null +++ b/src/main/groovy/au/org/ala/ecodata/graphql/enums/ProjectStatus.groovy @@ -0,0 +1,6 @@ +package au.org.ala.ecodata.graphql.enums + +enum ProjectStatus { + Active, + Completed +} \ No newline at end of file diff --git a/src/main/groovy/au/org/ala/ecodata/graphql/fetchers/ProjectsFetcher.groovy b/src/main/groovy/au/org/ala/ecodata/graphql/fetchers/ProjectsFetcher.groovy index 4c2591775..adb9c04cc 100644 --- a/src/main/groovy/au/org/ala/ecodata/graphql/fetchers/ProjectsFetcher.groovy +++ b/src/main/groovy/au/org/ala/ecodata/graphql/fetchers/ProjectsFetcher.groovy @@ -1,15 +1,19 @@ package au.org.ala.ecodata.graphql.fetchers import au.org.ala.ecodata.* +import au.org.ala.ecodata.graphql.enums.ProjectStatus import au.org.ala.ecodata.graphql.models.KeyValue import au.org.ala.ecodata.graphql.models.OutputData import com.mongodb.client.FindIterable import com.mongodb.client.model.Filters +import com.sun.xml.internal.bind.v2.TODO import grails.util.Holders import graphql.GraphQLException import graphql.schema.DataFetchingEnvironment import org.elasticsearch.action.search.SearchResponse +import java.text.SimpleDateFormat + import static au.org.ala.ecodata.ElasticIndex.HOMEPAGE_INDEX import static au.org.ala.ecodata.Status.DELETED @@ -32,7 +36,12 @@ class ProjectsFetcher implements graphql.schema.DataFetcher> { static String meritFacets = "status,organisationFacet,associatedProgramFacet,associatedSubProgramFacet,mainThemeFacet,stateFacet,nrmFacet,lgaFacet,mvgFacet,ibraFacet,imcra4_pbFacet,otherFacet,electFacet,meriPlanAssetFacet," + "cmzFacet,partnerOrganisationTypeFacet,promoteOnHomepage,custom.details.caseStudy,primaryOutcomeFacet,secondaryOutcomesFacet,muFacet,tags,fundingSourceFacet" - static Map meritParams = [hubFq:"isMERIT:true", controller:"search", flimit:1500, fsort:"term", query:"docType:project", action:"elasticHome", facets:meritFacets, format:null] + static Map meritParams = [hubFq:"isMERIT:true", flimit:1500, fsort:"term", query:"docType:project", facets:meritFacets, format:null, max:20] + + static String bioCollectFacets = "scienceType,tags,countries,ecoScienceType,difficulty,origin,status,organisationFacet,associatedProgramFacet,isExternal,isBushfire,typeOfProject" + static Map bioCollectParams = [hub:"ala",hubFq:"isCitizenScience:true", initiator:"biocollect", flimit:500, fsort:"term", + query:"docType: project AND (isCitizenScience:true) AND countries:(Australia OR Worldwide)", + facets:bioCollectFacets, format:null, offset:0, max:20, skipDefaultFilters:false] @Override List get(DataFetchingEnvironment environment) throws Exception { @@ -104,11 +113,19 @@ class ProjectsFetcher implements graphql.schema.DataFetcher> { def fqList = mapFq(environment) //validate the query - validateSearchQuery(environment, fqList) + validateSearchQuery(environment, fqList, meritParams, "docType: project", ["dateRange", "grantManagerNominatedProject"]) Map params = meritParams params["fq"] = fqList + //offset for elastic search + if(environment.arguments.get("page")) { + params["offset"] = 20*(Integer.parseInt(environment.arguments.get("page") as String)-1) + } + else{ + params["offset"] = 0 + } + if(environment.arguments.get("fromDate")) { params["fromDate"] = environment.arguments.get("fromDate").toString() } @@ -141,15 +158,17 @@ class ProjectsFetcher implements graphql.schema.DataFetcher> { return projects } - void validateSearchQuery (DataFetchingEnvironment environment, List fqList) { + void validateSearchQuery (DataFetchingEnvironment environment, List fqList, Map params, String query, List enumList) { - def searchDetails = elasticSearchService.search("docType: project", meritParams, HOMEPAGE_INDEX) + def searchDetails = elasticSearchService.search(query, params, HOMEPAGE_INDEX) fqList.each { List fq = it.toString().split(":") - List lookUps = searchDetails.facets.getFacets().get(fq.first()).entries.term as String[] - if(!lookUps.contains(fq.last())) { - throw new GraphQLException('Invalid ' + fq.first() +' : suggested values are : ' + lookUps) + if(!enumList.contains(fq.first())) { + List lookUps = searchDetails.facets.getFacets().get(fq.first()).entries.term as String[] + if (!lookUps.contains(fq.last())) { + throw new GraphQLException('Invalid ' + fq.first() + ' : suggested values are : ' + lookUps) + } } } @@ -168,6 +187,18 @@ class ProjectsFetcher implements graphql.schema.DataFetcher> { } } + if(environment.arguments.get("projectStartFromDate")) { + if(!(environment.arguments.get("projectStartFromDate") ==~ datePattern)){ + throw new GraphQLException('Invalid projectStartFromDate: projectStartFromDate should match yyyy-mm-dd') + } + } + + if(environment.arguments.get("projectStartToDate")) { + if(!(environment.arguments.get("projectStartToDate") ==~ datePattern)){ + throw new GraphQLException('Invalid projectStartToDate: projectStartToDate should match yyyy-mm-dd') + } + } + //validate activity types and output types if(environment.arguments.get("activities")){ List args = [] @@ -186,7 +217,7 @@ class ProjectsFetcher implements graphql.schema.DataFetcher> { def fqList = mapFq(environment) - validateSearchQuery(environment, fqList) + validateSearchQuery(environment, fqList, meritParams, "docType: project", ["dateRange", "grantManagerNominatedProject"]) List scores = Score.findAll() def results = getActivityOutputs(fqList, scores) @@ -239,7 +270,7 @@ class ProjectsFetcher implements graphql.schema.DataFetcher> { def fqList = mapFq(environment) - validateSearchQuery(environment, fqList) + validateSearchQuery(environment, fqList, meritParams, "docType: project", ["dateRange", "grantManagerNominatedProject"]) Map params = [hubFq:"isMERIT:true", controller:"search", showOrganisations:true, report:"outputTargets", action:"targetsReport", fq:fqList, format:null] @@ -281,7 +312,8 @@ class ProjectsFetcher implements graphql.schema.DataFetcher> { "federalElectorate": "electFacet:", "assetsAddressed": "meriPlanAssetFacet:", "userNominatedProject": "custom.details.caseStudy:", "managementUnit": "muFacet:"] environment.arguments.each { - if(it.key in ["fromDate", "toDate", "dateRange", "activities", "projectId", "activityOutputs", "programs", "outputTargetMeasures"]) { + if(it.key in ["fromDate", "toDate", "dateRange", "activities", "projectId", "activityOutputs", "programs", + "outputTargetMeasures", "projectStartFromDate", "projectStartToDate", "isWorldWide", "page"]) { return } @@ -291,6 +323,13 @@ class ProjectsFetcher implements graphql.schema.DataFetcher> { switch (it.key) { case "status": case "tags": + case "scienceType": + case "countries": + case "ecoScienceType": + case "difficulty": + case "origin": + case "isBushfire": + case "typeOfProject": key = it.key + ":" break; case "managementArea" : @@ -358,4 +397,133 @@ class ProjectsFetcher implements graphql.schema.DataFetcher> { } } } + + List searchBioCollectProject (DataFetchingEnvironment environment) { + + def fqList = mapFq(environment) + + //validate the query + validateSearchQuery(environment, fqList, bioCollectParams, + "docType: project AND (isCitizenScience:true) AND countries:(Australia OR Worldwide)", ["status"]) + + Map queryParams = buildBioCollectProjectSearchQuery(environment.arguments, fqList) + List projects = queryElasticSearch(environment, queryParams["query"] as String, queryParams) + + if(environment.arguments.get("activities")) { + List projectIdList = projects.projectId + + List activities = new ActivityFetcher(Holders.applicationContext.elasticSearchService, Holders.applicationContext.permissionService, Holders.applicationContext.metadataService, + Holders.applicationContext.messageSource, Holders.grailsApplication).getFilteredActivities(environment.arguments.get("activities") as List) + + //get projects with requested activity output types + List projectIds = activities.findAll { it.projectId in projectIdList }.projectId.unique() + + projects = projects.findAll{ it.projectId in projectIds} + } + + return projects + } + + static Map buildBioCollectProjectSearchQuery(Map params, List fqList){ + + List difficulty = [], status =[] + Map trimmedParams = [:] + trimmedParams = [hub:"ala",hubFq:"isCitizenScience:true", initiator:"biocollect", flimit:500, fsort:"term", + facets:bioCollectFacets, format:null, offset:0, max:20, skipDefaultFilters:false] + trimmedParams.isCitizenScience = true + trimmedParams.isWorks = false + trimmedParams.query = "docType:project" + (params.get("projectId") ? " AND projectId:" + params.get("projectId") : "") + trimmedParams.difficulty = params.get('difficulty') + trimmedParams.isWorldWide = params.get('isWorldWide') ?: false + + List fq = [], projectType = [] + List immutableFq = fqList + immutableFq.each { + if(!it?.startsWith('status:')) { + it ? fq.push(it) : null + } + } + + if(params.projectStartFromDate) { + if(params.projectStartToDate) { + fq.push("plannedStartDate:[" + params.projectStartFromDate + " TO " + params.projectStartToDate + "}") + } + else { + fq.push("plannedStartDate:[" + params.projectStartFromDate + " TO *}") + } + } + else{ + if(params.projectStartToDate) { + fq.push("plannedStartDate:[* TO " + params.projectStartToDate + "}") + } + } + + trimmedParams.fq = fq; + + if(trimmedParams.isCitizenScience){ + projectType.push('isCitizenScience:true') + trimmedParams.isCitizenScience = null + } + + if(trimmedParams.difficulty){ + trimmedParams.difficulty.each{ + difficulty.push("difficulty:${it}") + } + trimmedParams.query += " AND (${difficulty.join(' OR ')})" + trimmedParams.difficulty = null + } + + if (projectType) { + // append projectType to query. this is used by organisation page. + trimmedParams.query += ' AND (' + projectType.join(' OR ') + ')' + } + + if(params.status){ + SimpleDateFormat sdf = new SimpleDateFormat('yyyy-MM-dd'); + // Do not execute when both active and completed facets are checked. + if(params.status.unique().size() == 1){ + params.status.unique().each{ + switch (it){ + case ProjectStatus.Active: + status.push("-(plannedEndDate:[* TO *] AND -plannedEndDate:>=${sdf.format( new Date())})") + break; + case ProjectStatus.Completed: + status.push("(plannedEndDate:<${sdf.format( new Date())})") + break; + } + } + trimmedParams.query += " AND (${status.join(' OR ')})"; + } + else if(params.status.unique().size() == 2 && params.status.unique() == [ProjectStatus.Active, ProjectStatus.Completed]){ + + status.push("status:(\"active\")"); + status.push("(plannedEndDate:<${sdf.format( new Date())})"); + + trimmedParams.query += " AND ${status.join(' AND ')}"; + } + } + + if (trimmedParams.isWorldWide) { + trimmedParams.isWorldWide = null + } else if (trimmedParams.isWorldWide == false) { + trimmedParams.query += " AND countries:(Australia OR Worldwide)" + trimmedParams.isWorldWide = null + } + + //offset for elastic search + if(params.get("page")) { + trimmedParams["offset"] = 20*(Integer.parseInt(params.get("page") as String)-1) + } + else{ + trimmedParams["offset"] = 0 + } + + Map queryParams = [:] + trimmedParams.each { key, value -> + if (value != null) { + queryParams.put(key, value) + } + } + queryParams + } } diff --git a/src/main/groovy/au/org/ala/ecodata/graphql/mappers/ProjectGraphQLMapper.groovy b/src/main/groovy/au/org/ala/ecodata/graphql/mappers/ProjectGraphQLMapper.groovy index bdb74b104..af91770f1 100644 --- a/src/main/groovy/au/org/ala/ecodata/graphql/mappers/ProjectGraphQLMapper.groovy +++ b/src/main/groovy/au/org/ala/ecodata/graphql/mappers/ProjectGraphQLMapper.groovy @@ -7,6 +7,7 @@ import au.org.ala.ecodata.Report import au.org.ala.ecodata.Site import au.org.ala.ecodata.Status import au.org.ala.ecodata.graphql.enums.DateRange +import au.org.ala.ecodata.graphql.enums.ProjectStatus import au.org.ala.ecodata.graphql.enums.YesNo import au.org.ala.ecodata.graphql.fetchers.ActivityFetcher import au.org.ala.ecodata.graphql.fetchers.Helper @@ -187,6 +188,8 @@ class ProjectGraphQLMapper { argument('userNominatedProject', [String]){ nullable true } argument('managementUnit', [String]){ nullable true } + argument('page', int){ nullable true } + //activities filter argument('activities', 'activities') { accepts { @@ -337,6 +340,49 @@ class ProjectGraphQLMapper { } } + query('searchBioCollectProject', [Project]) { + argument('projectId', String) { nullable true } + argument('isWorldWide', Boolean){ nullable true } + argument('projectStartFromDate', String){ nullable true description "yyyy-mm-dd" } + argument('projectStartToDate', String){ nullable true description "yyyy-mm-dd" } + argument('scienceType', [String]){ nullable true } + argument('tags', [String]){ nullable true } + argument('countries', [String]){ nullable true } + argument('ecoScienceType', [String]){ nullable true } + argument('status', [ProjectStatus]){ nullable true } + argument('difficulty', [String]){ nullable true } + argument('organisation', [String]){ nullable true } + argument('origin', [String]){ nullable true } + argument('isBushfire', [Boolean]){ nullable true } + argument('associatedProgram', [String]){ nullable true } + argument('typeOfProject', [String]){ nullable true } + + argument('page', int){ nullable true } + + //activities filter + argument('activities', 'activitiesList') { + accepts { + field('activityType', String) {nullable true} + field('output', 'outputsList') { + field('outputType', String) {nullable false} + nullable true + //one activity can have zero or more output + collection true + } + //one project can have many activities + collection true + } + nullable true + } + + dataFetcher(new DataFetcher() { + @Override + Object get(DataFetchingEnvironment environment) throws Exception { + ProjectGraphQLMapper.buildTestFetcher().searchBioCollectProject(environment) + } + }) + } + } } From 4e64e80a33482d2b80752d01a61d59aedf974604 Mon Sep 17 00:00:00 2001 From: yasima-csiro Date: Mon, 8 Feb 2021 14:12:35 +1100 Subject: [PATCH 010/144] Add API authentication --- build.gradle | 1 + grails-app/conf/application.yml | 2 +- .../org/ala/ecodata/ApiKeyInterceptor.groovy | 2 +- .../org/ala/ecodata/GraphqlInterceptor.groovy | 55 +++++++++++++++++++ .../au/org/ala/ecodata/UserService.groovy | 22 ++++++-- .../ala/ecodata/GraphqlIntegrationSpec.groovy | 31 +++++++++++ .../graphql/fetchers/ProjectsFetcher.groovy | 17 +++++- .../mappers/ProjectGraphQLMapper.groovy | 10 ++++ 8 files changed, 131 insertions(+), 9 deletions(-) create mode 100644 grails-app/controllers/au/org/ala/ecodata/GraphqlInterceptor.groovy create mode 100644 src/integration-test/groovy/au/org/ala/ecodata/GraphqlIntegrationSpec.groovy diff --git a/build.gradle b/build.gradle index 517ba95d1..603adb184 100644 --- a/build.gradle +++ b/build.gradle @@ -75,6 +75,7 @@ dependencies { compile "org.grails.plugins:mongodb:6.1.7" compile "org.mongodb:mongodb-driver:3.9.1" compile "org.grails.plugins:gorm-graphql:1.0.3.BUILD-SNAPSHOT" + compile 'org.grails:grails-datastore-rest-client' compile "org.grails.plugins:gsp" console "org.grails:grails-console" profile "org.grails.profiles:web" diff --git a/grails-app/conf/application.yml b/grails-app/conf/application.yml index e9ae3ba21..7b767b7b9 100644 --- a/grails-app/conf/application.yml +++ b/grails-app/conf/application.yml @@ -166,7 +166,7 @@ security: casServerUrlPrefix: 'https://auth.ala.org.au/cas' loginUrl: 'https://auth.ala.org.au/cas/login' logoutUrl: 'https://auth.ala.org.au/cas/logout' - uriFilterPattern: '/admin/.*,/activityForm/((?!get).)*,/graphql.*' + uriFilterPattern: '/admin/.*,/activityForm/((?!get).)*' authenticateOnlyIfLoggedInPattern: uriExclusionFilterPattern: '/images.*,/css.*,/js.*,/less.*' diff --git a/grails-app/controllers/au/org/ala/ecodata/ApiKeyInterceptor.groovy b/grails-app/controllers/au/org/ala/ecodata/ApiKeyInterceptor.groovy index 7bf3d399e..219a66998 100644 --- a/grails-app/controllers/au/org/ala/ecodata/ApiKeyInterceptor.groovy +++ b/grails-app/controllers/au/org/ala/ecodata/ApiKeyInterceptor.groovy @@ -15,7 +15,7 @@ class ApiKeyInterceptor { def LOCALHOST_IP = '127.0.0.1' public ApiKeyInterceptor() { - matchAll() + matchAll().excludes(controller: 'graphql') } boolean before() { diff --git a/grails-app/controllers/au/org/ala/ecodata/GraphqlInterceptor.groovy b/grails-app/controllers/au/org/ala/ecodata/GraphqlInterceptor.groovy new file mode 100644 index 000000000..4cab8211d --- /dev/null +++ b/grails-app/controllers/au/org/ala/ecodata/GraphqlInterceptor.groovy @@ -0,0 +1,55 @@ +package au.org.ala.ecodata + +import grails.converters.JSON + +class GraphqlInterceptor { + + UserService userService + PermissionService permissionService + + GraphqlInterceptor() { + match uri: '/graphql/index/**' + } + + boolean before() { + //validate the username and given authKey + String userName = request.getHeader('userName') + String authKey = request.getHeader('authKey') + + if(authKey && userName) { + + def result = userService.validateKey(userName, authKey) + if (!result?.resp?.statusCode && result.resp?.status == 'success') { + //and also, only the ala admins are permitted to do graphql searches + if (permissionService.isUserAlaAdmin(userName)) { + true + } + else{ + Map map = [error: 'Access denied', status: 401] + response.status = 401 + render map as JSON + + false + } + } else { + Map map = [error: 'Access denied', status: 401] + response.status = 401 + render map as JSON + + false + } + } + else{ + Map map = [error: 'userName or authKey is invalid', status: 400] + response.status = 400 + render map as JSON + + false + } + } + + boolean after = { } + + void afterView() { } + +} diff --git a/grails-app/services/au/org/ala/ecodata/UserService.groovy b/grails-app/services/au/org/ala/ecodata/UserService.groovy index 8a4e88d45..c5dc3dac8 100644 --- a/grails-app/services/au/org/ala/ecodata/UserService.groovy +++ b/grails-app/services/au/org/ala/ecodata/UserService.groovy @@ -91,12 +91,7 @@ class UserService { String userId = "" if (authKey && userName) { - String key = new String(authKey) - String username = new String(userName) - - def url = grailsApplication.config.authCheckKeyUrl - def params = [userName: username, authKey: key] - def result = webService.doPostWithParams(url, params, true) + def result = validateKey(userName, authKey) if (!result?.resp?.statusCode && result.resp?.status == 'success') { params = [userName: username] url = grailsApplication.config.userDetails.url + "getUserDetails" @@ -110,6 +105,21 @@ class UserService { return userId } + def validateKey(userName, authKey) { + def result = [] + + if (authKey && userName) { + String key = new String(authKey) + String username = new String(userName) + + def url = grailsApplication.config.authCheckKeyUrl + def params = [userName: username, authKey: key] + result = webService.doPostWithParams(url, params, true) + } + + return result + } + /** * Get auth key for the given username and password * diff --git a/src/integration-test/groovy/au/org/ala/ecodata/GraphqlIntegrationSpec.groovy b/src/integration-test/groovy/au/org/ala/ecodata/GraphqlIntegrationSpec.groovy new file mode 100644 index 000000000..93f163f96 --- /dev/null +++ b/src/integration-test/groovy/au/org/ala/ecodata/GraphqlIntegrationSpec.groovy @@ -0,0 +1,31 @@ +package au.org.ala.ecodata + +import grails.testing.mixin.integration.Integration +import org.grails.gorm.graphql.plugin.testing.GraphQLSpec + +import spock.lang.Specification + +@Integration +class GraphqlIntegrationSpec extends Specification implements GraphQLSpec { + + def cleanup() { + Project.findAll().each { it.delete(flush:true) } + } + + def "Get project"() { + setup: + Project project = new Project(projectId: "graphqlProject1", name: "graphqlProject1").save(failOnError: true, flush: true) + + when: + def resp = graphQL.graphql(""" + query{ + project(projectId:"graphqlProject1"){ + name + } + }""") + def result = resp.json + + then: + result.data.project.name == "graphqlProject1" + } +} diff --git a/src/main/groovy/au/org/ala/ecodata/graphql/fetchers/ProjectsFetcher.groovy b/src/main/groovy/au/org/ala/ecodata/graphql/fetchers/ProjectsFetcher.groovy index adb9c04cc..b90b4f87e 100644 --- a/src/main/groovy/au/org/ala/ecodata/graphql/fetchers/ProjectsFetcher.groovy +++ b/src/main/groovy/au/org/ala/ecodata/graphql/fetchers/ProjectsFetcher.groovy @@ -126,6 +126,13 @@ class ProjectsFetcher implements graphql.schema.DataFetcher> { params["offset"] = 0 } + if(environment.arguments.get("max")) { + params["max"] = environment.arguments.get("max") + } + else{ + params["max"] = 20 + } + if(environment.arguments.get("fromDate")) { params["fromDate"] = environment.arguments.get("fromDate").toString() } @@ -313,7 +320,7 @@ class ProjectsFetcher implements graphql.schema.DataFetcher> { environment.arguments.each { if(it.key in ["fromDate", "toDate", "dateRange", "activities", "projectId", "activityOutputs", "programs", - "outputTargetMeasures", "projectStartFromDate", "projectStartToDate", "isWorldWide", "page"]) { + "outputTargetMeasures", "projectStartFromDate", "projectStartToDate", "isWorldWide", "page", "max"]) { return } @@ -518,6 +525,14 @@ class ProjectsFetcher implements graphql.schema.DataFetcher> { trimmedParams["offset"] = 0 } + if(params.get("max")) { + trimmedParams["max"] = params.get("max") + } + else{ + trimmedParams["max"] = 20 + } + + Map queryParams = [:] trimmedParams.each { key, value -> if (value != null) { diff --git a/src/main/groovy/au/org/ala/ecodata/graphql/mappers/ProjectGraphQLMapper.groovy b/src/main/groovy/au/org/ala/ecodata/graphql/mappers/ProjectGraphQLMapper.groovy index af91770f1..e539fc32d 100644 --- a/src/main/groovy/au/org/ala/ecodata/graphql/mappers/ProjectGraphQLMapper.groovy +++ b/src/main/groovy/au/org/ala/ecodata/graphql/mappers/ProjectGraphQLMapper.groovy @@ -77,6 +77,13 @@ class ProjectGraphQLMapper { } } + add('activities', [Activity]) { + dataFetcher { Project project -> + new ActivityFetcher(Holders.applicationContext.elasticSearchService, Holders.applicationContext.permissionService, Holders.applicationContext.metadataService, + Holders.applicationContext.messageSource, Holders.grailsApplication).getFilteredActivities(project.tempArgs, project.projectId) + } + } + //add graphql type for each activity type activityModel["activities"].each { if(it.name && it.outputs && it.outputs.size() > 0 && it.outputs.fields?.findAll{ x -> x?.size() != 0 }?.size() > 0){ @@ -189,6 +196,7 @@ class ProjectGraphQLMapper { argument('managementUnit', [String]){ nullable true } argument('page', int){ nullable true } + argument('max', int){ nullable true } //activities filter argument('activities', 'activities') { @@ -342,6 +350,7 @@ class ProjectGraphQLMapper { query('searchBioCollectProject', [Project]) { argument('projectId', String) { nullable true } + //argument('hub', String) { nullable false } argument('isWorldWide', Boolean){ nullable true } argument('projectStartFromDate', String){ nullable true description "yyyy-mm-dd" } argument('projectStartToDate', String){ nullable true description "yyyy-mm-dd" } @@ -358,6 +367,7 @@ class ProjectGraphQLMapper { argument('typeOfProject', [String]){ nullable true } argument('page', int){ nullable true } + argument('max', int){ nullable true } //activities filter argument('activities', 'activitiesList') { From 551491c5abcbc23875694efeafa02406c24d83e6 Mon Sep 17 00:00:00 2001 From: yasima-csiro Date: Wed, 10 Feb 2021 12:54:30 +1100 Subject: [PATCH 011/144] Add form versioning --- .../groovy/au/org/ala/ecodata/graphql/fetchers/Helper.groovy | 1 + .../ala/ecodata/graphql/mappers/ProjectGraphQLMapper.groovy | 4 +++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/main/groovy/au/org/ala/ecodata/graphql/fetchers/Helper.groovy b/src/main/groovy/au/org/ala/ecodata/graphql/fetchers/Helper.groovy index 1d5cb87c5..c69b33d88 100644 --- a/src/main/groovy/au/org/ala/ecodata/graphql/fetchers/Helper.groovy +++ b/src/main/groovy/au/org/ala/ecodata/graphql/fetchers/Helper.groovy @@ -85,6 +85,7 @@ class Helper { ActivityForm.findAllWhereStatusNotEqualAndPublicationStatusEquals(Status.DELETED, PublicationStatus.PUBLISHED).each { ActivityForm activityForm -> Map activityModel = [ name: activityForm.name, + formVersion: activityForm.formVersion, outputs: [] ] diff --git a/src/main/groovy/au/org/ala/ecodata/graphql/mappers/ProjectGraphQLMapper.groovy b/src/main/groovy/au/org/ala/ecodata/graphql/mappers/ProjectGraphQLMapper.groovy index e539fc32d..ae29472da 100644 --- a/src/main/groovy/au/org/ala/ecodata/graphql/mappers/ProjectGraphQLMapper.groovy +++ b/src/main/groovy/au/org/ala/ecodata/graphql/mappers/ProjectGraphQLMapper.groovy @@ -89,7 +89,7 @@ class ProjectGraphQLMapper { if(it.name && it.outputs && it.outputs.size() > 0 && it.outputs.fields?.findAll{ x -> x?.size() != 0 }?.size() > 0){ def outputTypes = it.outputs String activityName = WordUtils.capitalize(it.name).replaceAll("\\W", "") - String name = "Activity_" + activityName + String name = "Activity_" + activityName + "_" + it.formVersion List outputList = [] List modifiedColumns = [] //define activity type @@ -197,6 +197,7 @@ class ProjectGraphQLMapper { argument('page', int){ nullable true } argument('max', int){ nullable true } + //argument('myProjects', Boolean){ nullable true } //activities filter argument('activities', 'activities') { @@ -368,6 +369,7 @@ class ProjectGraphQLMapper { argument('page', int){ nullable true } argument('max', int){ nullable true } + //argument('myProjects', Boolean){ nullable true } //activities filter argument('activities', 'activitiesList') { From 780a2de0e11af006a46428d3ccd25dce1fe8a040 Mon Sep 17 00:00:00 2001 From: yasima-csiro Date: Mon, 15 Feb 2021 11:54:47 +1100 Subject: [PATCH 012/144] Add biocollect hub filtering --- grails-app/conf/application.yml | 2 +- .../org/ala/ecodata/GraphqlInterceptor.groovy | 76 +++++++++++++--- .../ecodata/graphql/fetchers/Helper.groovy | 1 - .../graphql/fetchers/ProjectsFetcher.groovy | 88 +++++++++++++------ .../mappers/ProjectGraphQLMapper.groovy | 10 +-- 5 files changed, 128 insertions(+), 49 deletions(-) diff --git a/grails-app/conf/application.yml b/grails-app/conf/application.yml index 7b767b7b9..e92fe5f5b 100644 --- a/grails-app/conf/application.yml +++ b/grails-app/conf/application.yml @@ -166,7 +166,7 @@ security: casServerUrlPrefix: 'https://auth.ala.org.au/cas' loginUrl: 'https://auth.ala.org.au/cas/login' logoutUrl: 'https://auth.ala.org.au/cas/logout' - uriFilterPattern: '/admin/.*,/activityForm/((?!get).)*' + uriFilterPattern: '/admin/.*,/activityForm/((?!get).)*,/graphql/browser/*' authenticateOnlyIfLoggedInPattern: uriExclusionFilterPattern: '/images.*,/css.*,/js.*,/less.*' diff --git a/grails-app/controllers/au/org/ala/ecodata/GraphqlInterceptor.groovy b/grails-app/controllers/au/org/ala/ecodata/GraphqlInterceptor.groovy index 4cab8211d..6f671c856 100644 --- a/grails-app/controllers/au/org/ala/ecodata/GraphqlInterceptor.groovy +++ b/grails-app/controllers/au/org/ala/ecodata/GraphqlInterceptor.groovy @@ -1,11 +1,16 @@ package au.org.ala.ecodata +import au.org.ala.web.AuthService import grails.converters.JSON +import au.org.ala.web.UserDetails class GraphqlInterceptor { UserService userService PermissionService permissionService + AuthService authService + + def LOCALHOST_IP = '127.0.0.1' GraphqlInterceptor() { match uri: '/graphql/index/**' @@ -13,7 +18,7 @@ class GraphqlInterceptor { boolean before() { //validate the username and given authKey - String userName = request.getHeader('userName') + String userName = request.getHeader(grailsApplication.config.app.http.header.userId) String authKey = request.getHeader('authKey') if(authKey && userName) { @@ -22,29 +27,58 @@ class GraphqlInterceptor { if (!result?.resp?.statusCode && result.resp?.status == 'success') { //and also, only the ala admins are permitted to do graphql searches if (permissionService.isUserAlaAdmin(userName)) { + log.info ('GrapqhQl API request, UserId: ' + userName) true } else{ - Map map = [error: 'Access denied', status: 401] - response.status = 401 - render map as JSON - + accessDeniedError('Invalid GrapqhQl API usage: Access denied, userId: ' + userName) false } } else { - Map map = [error: 'Access denied', status: 401] - response.status = 401 - render map as JSON - + accessDeniedError('Invalid GrapqhQl API usage: Access denied, userId: ' + userName) false } } + // This is to validate graphql browser user else{ - Map map = [error: 'userName or authKey is invalid', status: 400] - response.status = 400 - render map as JSON + def userCookie = null + + if(request.cookies) { + userCookie = request.cookies.find { it.name == 'ALA-Auth' } + } + UserDetails user = null + + if (userCookie) { + String username = java.net.URLDecoder.decode(userCookie.getValue(), 'utf-8') + //test to see that the user is valid + user = authService.getUserForEmailAddress(username) + + if(!user){ + accessDeniedError('Invalid GrapqhQl API usage: Access denied, userId: ' + username) + false + } + else{ + def whiteList = buildWhiteList() + def clientIp = request.getHeader("X-Forwarded-For") ?: request.getRemoteHost() + def ipOk = whiteList.contains(clientIp) || (whiteList.size() == 1 && whiteList[0] == LOCALHOST_IP) - false + if(ipOk) { + userService.setCurrentUser(username) + log.info('GrapqhQl API request, UserId: ' + username) + true + } + else { + accessDeniedError('Invalid GrapqhQl API usage: Access denied, userId: ' + username) + false + } + } + } + else{ + accessDeniedError('Invalid GrapqhQl API usage: Access denied') + false + } + + true } } @@ -52,4 +86,20 @@ class GraphqlInterceptor { void afterView() { } + def buildWhiteList() { + def whiteList = [LOCALHOST_IP] + def config = grailsApplication.config.app.api.whiteList as String + if (config) { + whiteList.addAll(config.split(',').collect({it.trim()})) + } + whiteList + } + + def accessDeniedError(String error) { + Map map = [error: 'Access denied', status: 401] + response.status = 401 + log.warn (error) + render map as JSON + } + } diff --git a/src/main/groovy/au/org/ala/ecodata/graphql/fetchers/Helper.groovy b/src/main/groovy/au/org/ala/ecodata/graphql/fetchers/Helper.groovy index c69b33d88..1d5cb87c5 100644 --- a/src/main/groovy/au/org/ala/ecodata/graphql/fetchers/Helper.groovy +++ b/src/main/groovy/au/org/ala/ecodata/graphql/fetchers/Helper.groovy @@ -85,7 +85,6 @@ class Helper { ActivityForm.findAllWhereStatusNotEqualAndPublicationStatusEquals(Status.DELETED, PublicationStatus.PUBLISHED).each { ActivityForm activityForm -> Map activityModel = [ name: activityForm.name, - formVersion: activityForm.formVersion, outputs: [] ] diff --git a/src/main/groovy/au/org/ala/ecodata/graphql/fetchers/ProjectsFetcher.groovy b/src/main/groovy/au/org/ala/ecodata/graphql/fetchers/ProjectsFetcher.groovy index b90b4f87e..2e06439ed 100644 --- a/src/main/groovy/au/org/ala/ecodata/graphql/fetchers/ProjectsFetcher.groovy +++ b/src/main/groovy/au/org/ala/ecodata/graphql/fetchers/ProjectsFetcher.groovy @@ -19,12 +19,14 @@ import static au.org.ala.ecodata.Status.DELETED class ProjectsFetcher implements graphql.schema.DataFetcher> { - public ProjectsFetcher(ProjectService projectService, ElasticSearchService elasticSearchService, PermissionService permissionService, ReportService reportService, CacheService cacheService) { + public ProjectsFetcher(ProjectService projectService, ElasticSearchService elasticSearchService, PermissionService permissionService, + ReportService reportService, CacheService cacheService, HubService hubService) { this.projectService = projectService this.elasticSearchService = elasticSearchService this.permissionService = permissionService this.reportService = reportService this.cacheService = cacheService + this.hubService = hubService } @@ -33,15 +35,14 @@ class ProjectsFetcher implements graphql.schema.DataFetcher> { ReportService reportService ProjectService projectService CacheService cacheService + HubService hubService static String meritFacets = "status,organisationFacet,associatedProgramFacet,associatedSubProgramFacet,mainThemeFacet,stateFacet,nrmFacet,lgaFacet,mvgFacet,ibraFacet,imcra4_pbFacet,otherFacet,electFacet,meriPlanAssetFacet," + "cmzFacet,partnerOrganisationTypeFacet,promoteOnHomepage,custom.details.caseStudy,primaryOutcomeFacet,secondaryOutcomesFacet,muFacet,tags,fundingSourceFacet" static Map meritParams = [hubFq:"isMERIT:true", flimit:1500, fsort:"term", query:"docType:project", facets:meritFacets, format:null, max:20] - static String bioCollectFacets = "scienceType,tags,countries,ecoScienceType,difficulty,origin,status,organisationFacet,associatedProgramFacet,isExternal,isBushfire,typeOfProject" - static Map bioCollectParams = [hub:"ala",hubFq:"isCitizenScience:true", initiator:"biocollect", flimit:500, fsort:"term", - query:"docType: project AND (isCitizenScience:true) AND countries:(Australia OR Worldwide)", - facets:bioCollectFacets, format:null, offset:0, max:20, skipDefaultFilters:false] + static Map paramList = [flimit:1500, fsort:"term", query:"docType: project", format:null, offset:0, max:20, skipDefaultFilters:false, + hubFq:null, facets: null] @Override List get(DataFetchingEnvironment environment) throws Exception { @@ -67,10 +68,17 @@ class ProjectsFetcher implements graphql.schema.DataFetcher> { // add pagination results. - String userId = environment.context.userId ?: '1493' + String userId = environment.context.user?.userId ?: '1493' String query = queryString ?:"*:*" - //SearchResponse searchResponse = elasticSearchService.searchWithSecurity(userId, query, params, HOMEPAGE_INDEX) - SearchResponse searchResponse = elasticSearchService.search(query, params, HOMEPAGE_INDEX) + Boolean myProjects = environment.arguments.get("myProjects") != null ? environment.arguments.get("myProjects") : true + SearchResponse searchResponse + + if(myProjects) { + searchResponse = elasticSearchService.searchWithSecurity(userId, query, params, HOMEPAGE_INDEX) + } + else { + searchResponse = elasticSearchService.search(query, params, HOMEPAGE_INDEX) + } List projectIds = searchResponse.hits.hits.collect{it.source.projectId} @@ -78,14 +86,21 @@ class ProjectsFetcher implements graphql.schema.DataFetcher> { List publicProjectIds = [] List fullProjectIds = [] + if(myProjects) { + fullProjectIds = projectIds + } + else { + publicProjectIds = projectIds + } + // Alternative here is to also return the userIds from the ES query and see if the user is in the result. // we could also map directly from the ES, but this would require a different approach (maybe creating // and binding the domain objects from the ES data?) - projectIds.each { - boolean readable = permissionService.checkUserPermission(userId, it, Project.name, "read") - readable ? publicProjectIds << it : publicProjectIds << it - } +// projectIds.each { +// boolean readable = permissionService.checkUserPermission(userId, it, Project.name, "read") +// readable ? publicProjectIds << it : publicProjectIds << it +// } Map publicView = [name:true, description:true, projectId:true] @@ -102,7 +117,7 @@ class ProjectsFetcher implements graphql.schema.DataFetcher> { } List projects = projectIds.collect { - publicProjects.containsKey(it) ? publicProjects[it] : fullProjects[it] + !myProjects ? publicProjects[it] : fullProjects[it] } projects @@ -320,7 +335,7 @@ class ProjectsFetcher implements graphql.schema.DataFetcher> { environment.arguments.each { if(it.key in ["fromDate", "toDate", "dateRange", "activities", "projectId", "activityOutputs", "programs", - "outputTargetMeasures", "projectStartFromDate", "projectStartToDate", "isWorldWide", "page", "max"]) { + "outputTargetMeasures", "projectStartFromDate", "projectStartToDate", "isWorldWide", "page", "max", "myProjects", "hub"]) { return } @@ -409,9 +424,17 @@ class ProjectsFetcher implements graphql.schema.DataFetcher> { def fqList = mapFq(environment) + Map hub = hubService.findByUrlPath(environment.arguments.get("hub")) + if(!hub) { + List hubList = Hub.findAll().collect{ it.urlPath}.unique() + throw new GraphQLException('Invalid hub, suggested values are : ' + hubList) + } + + paramList.hubFq = hub.defaultFacetQuery + paramList.facets = hub.availableFacets.join(",") + //validate the query - validateSearchQuery(environment, fqList, bioCollectParams, - "docType: project AND (isCitizenScience:true) AND countries:(Australia OR Worldwide)", ["status"]) + validateSearchQuery(environment, fqList, paramList, "docType: project", ["status"]) Map queryParams = buildBioCollectProjectSearchQuery(environment.arguments, fqList) List projects = queryElasticSearch(environment, queryParams["query"] as String, queryParams) @@ -434,11 +457,9 @@ class ProjectsFetcher implements graphql.schema.DataFetcher> { static Map buildBioCollectProjectSearchQuery(Map params, List fqList){ List difficulty = [], status =[] + Boolean isMerit Map trimmedParams = [:] - trimmedParams = [hub:"ala",hubFq:"isCitizenScience:true", initiator:"biocollect", flimit:500, fsort:"term", - facets:bioCollectFacets, format:null, offset:0, max:20, skipDefaultFilters:false] - trimmedParams.isCitizenScience = true - trimmedParams.isWorks = false + trimmedParams = paramList trimmedParams.query = "docType:project" + (params.get("projectId") ? " AND projectId:" + params.get("projectId") : "") trimmedParams.difficulty = params.get('difficulty') trimmedParams.isWorldWide = params.get('isWorldWide') ?: false @@ -467,9 +488,17 @@ class ProjectsFetcher implements graphql.schema.DataFetcher> { trimmedParams.fq = fq; - if(trimmedParams.isCitizenScience){ - projectType.push('isCitizenScience:true') - trimmedParams.isCitizenScience = null + String hubFq = paramList.hubFq + + if(hubFq.contains("isCitizenScience:true")) { + projectType.push('isCitizenScience:true') } + else if(hubFq.contains("isWorks:true")) { + projectType.push('(projectType:works AND isMERIT:false)')} + else if(hubFq.contains("isEcoScience:true")) { + projectType.push('(projectType:ecoScience)')} + else if(hubFq.contains("isMERIT:true")) { + projectType.push('isMERIT:true') + isMerit = true } if(trimmedParams.difficulty){ @@ -481,7 +510,6 @@ class ProjectsFetcher implements graphql.schema.DataFetcher> { } if (projectType) { - // append projectType to query. this is used by organisation page. trimmedParams.query += ' AND (' + projectType.join(' OR ') + ')' } @@ -510,11 +538,13 @@ class ProjectsFetcher implements graphql.schema.DataFetcher> { } } - if (trimmedParams.isWorldWide) { - trimmedParams.isWorldWide = null - } else if (trimmedParams.isWorldWide == false) { - trimmedParams.query += " AND countries:(Australia OR Worldwide)" - trimmedParams.isWorldWide = null + if(!isMerit) { + if (trimmedParams.isWorldWide) { + trimmedParams.isWorldWide = null + } else if (trimmedParams.isWorldWide == false) { + trimmedParams.query += " AND countries:(Australia OR Worldwide)" + trimmedParams.isWorldWide = null + } } //offset for elastic search diff --git a/src/main/groovy/au/org/ala/ecodata/graphql/mappers/ProjectGraphQLMapper.groovy b/src/main/groovy/au/org/ala/ecodata/graphql/mappers/ProjectGraphQLMapper.groovy index ae29472da..e7b4acd4b 100644 --- a/src/main/groovy/au/org/ala/ecodata/graphql/mappers/ProjectGraphQLMapper.groovy +++ b/src/main/groovy/au/org/ala/ecodata/graphql/mappers/ProjectGraphQLMapper.groovy @@ -89,7 +89,7 @@ class ProjectGraphQLMapper { if(it.name && it.outputs && it.outputs.size() > 0 && it.outputs.fields?.findAll{ x -> x?.size() != 0 }?.size() > 0){ def outputTypes = it.outputs String activityName = WordUtils.capitalize(it.name).replaceAll("\\W", "") - String name = "Activity_" + activityName + "_" + it.formVersion + String name = "Activity_" + activityName List outputList = [] List modifiedColumns = [] //define activity type @@ -197,7 +197,7 @@ class ProjectGraphQLMapper { argument('page', int){ nullable true } argument('max', int){ nullable true } - //argument('myProjects', Boolean){ nullable true } + argument('myProjects', Boolean){ nullable true } //activities filter argument('activities', 'activities') { @@ -351,7 +351,7 @@ class ProjectGraphQLMapper { query('searchBioCollectProject', [Project]) { argument('projectId', String) { nullable true } - //argument('hub', String) { nullable false } + argument('hub', String) { nullable false } argument('isWorldWide', Boolean){ nullable true } argument('projectStartFromDate', String){ nullable true description "yyyy-mm-dd" } argument('projectStartToDate', String){ nullable true description "yyyy-mm-dd" } @@ -369,7 +369,7 @@ class ProjectGraphQLMapper { argument('page', int){ nullable true } argument('max', int){ nullable true } - //argument('myProjects', Boolean){ nullable true } + argument('myProjects', Boolean){ nullable true } //activities filter argument('activities', 'activitiesList') { @@ -402,7 +402,7 @@ class ProjectGraphQLMapper { static ProjectsFetcher buildTestFetcher() { new ProjectsFetcher(Holders.applicationContext.projectService, Holders.applicationContext.elasticSearchService, Holders.applicationContext.permissionService, Holders.applicationContext.reportService, - Holders.applicationContext.cacheService) + Holders.applicationContext.cacheService, Holders.applicationContext.hubService) } } From 1c9f69c24cea3e66d93e7b43955daf7ee2515b43 Mon Sep 17 00:00:00 2001 From: yasima-csiro Date: Tue, 16 Feb 2021 13:28:07 +1100 Subject: [PATCH 013/144] Add graphql logging --- grails-app/conf/logback.groovy | 16 +++++++++++++- .../org/ala/ecodata/GraphqlInterceptor.groovy | 22 +++++++++---------- .../graphql/EcodataGraphQLCustomiser.groovy | 10 ++++++++- 3 files changed, 34 insertions(+), 14 deletions(-) diff --git a/grails-app/conf/logback.groovy b/grails-app/conf/logback.groovy index 6a159632d..7a8fe3f78 100644 --- a/grails-app/conf/logback.groovy +++ b/grails-app/conf/logback.groovy @@ -43,6 +43,15 @@ if (targetDir != null) { } } logger("EsIndexing", INFO, ['ES-INDEXING'], false) + + appender("GRAPHQL", FileAppender) { + file = "${targetDir}/graphql.log" + append = true + encoder(PatternLayoutEncoder) { + pattern = "%d{yyyy-MM-dd HH:mm:ss.SSS} %level - %msg%n" + } + } + logger("graphql", INFO, ['GRAPHQL'], false) } root(ERROR, ['STDOUT']) @@ -93,9 +102,14 @@ final trace = [ // 'org.hibernate.type' ] +final graphql = [ + 'au.org.ala.ecodata.GraphqlInterceptor', 'au.org.ala.ecodata.graphql.EcodataGraphQLCustomiser' +] + for (def name : error) logger(name, ERROR) for (def name : warn) logger(name, WARN) for (def name: info) logger(name, INFO) for (def name: debug) logger(name, DEBUG) for (def name: trace) logger(name, TRACE) -for (def name: esInfo) logger(name, INFO, ["ES-INDEXING"]) \ No newline at end of file +for (def name: esInfo) logger(name, INFO, ["ES-INDEXING"]) +for (def name: graphql) logger(name, INFO, ["GRAPHQL"]) \ No newline at end of file diff --git a/grails-app/controllers/au/org/ala/ecodata/GraphqlInterceptor.groovy b/grails-app/controllers/au/org/ala/ecodata/GraphqlInterceptor.groovy index 6f671c856..2f5f80727 100644 --- a/grails-app/controllers/au/org/ala/ecodata/GraphqlInterceptor.groovy +++ b/grails-app/controllers/au/org/ala/ecodata/GraphqlInterceptor.groovy @@ -27,16 +27,15 @@ class GraphqlInterceptor { if (!result?.resp?.statusCode && result.resp?.status == 'success') { //and also, only the ala admins are permitted to do graphql searches if (permissionService.isUserAlaAdmin(userName)) { - log.info ('GrapqhQl API request, UserId: ' + userName) - true + return true } else{ accessDeniedError('Invalid GrapqhQl API usage: Access denied, userId: ' + userName) - false + return false } } else { - accessDeniedError('Invalid GrapqhQl API usage: Access denied, userId: ' + userName) - false + accessDeniedError('Invalid GrapqhQl API usage: Invalid username or api key, userId: ' + userName) + return false } } // This is to validate graphql browser user @@ -49,13 +48,13 @@ class GraphqlInterceptor { UserDetails user = null if (userCookie) { - String username = java.net.URLDecoder.decode(userCookie.getValue(), 'utf-8') + String username = URLDecoder.decode(userCookie.getValue(), 'utf-8') //test to see that the user is valid user = authService.getUserForEmailAddress(username) if(!user){ accessDeniedError('Invalid GrapqhQl API usage: Access denied, userId: ' + username) - false + return false } else{ def whiteList = buildWhiteList() @@ -64,21 +63,20 @@ class GraphqlInterceptor { if(ipOk) { userService.setCurrentUser(username) - log.info('GrapqhQl API request, UserId: ' + username) - true + return true } else { accessDeniedError('Invalid GrapqhQl API usage: Access denied, userId: ' + username) - false + return false } } } else{ accessDeniedError('Invalid GrapqhQl API usage: Access denied') - false + return false } - true + false } } diff --git a/src/main/groovy/au/org/ala/ecodata/graphql/EcodataGraphQLCustomiser.groovy b/src/main/groovy/au/org/ala/ecodata/graphql/EcodataGraphQLCustomiser.groovy index 42f1f901c..325868d52 100644 --- a/src/main/groovy/au/org/ala/ecodata/graphql/EcodataGraphQLCustomiser.groovy +++ b/src/main/groovy/au/org/ala/ecodata/graphql/EcodataGraphQLCustomiser.groovy @@ -1,5 +1,6 @@ package au.org.ala.ecodata.graphql +import au.org.ala.ecodata.UserService import au.org.ala.ecodata.graphql.converters.DateFormatting import au.org.ala.ecodata.graphql.converters.MeriPlanConverter import au.org.ala.ecodata.graphql.converters.ObjectConverter @@ -18,6 +19,7 @@ import graphql.schema.GraphQLFieldDefinition import graphql.schema.GraphQLObjectType import graphql.schema.GraphQLScalarType import graphql.schema.GraphQLType +import org.apache.log4j.Logger import org.bson.types.ObjectId import org.grails.datastore.mapping.model.PersistentEntity import org.grails.gorm.graphql.fetcher.GraphQLDataFetcherType @@ -29,6 +31,8 @@ import org.grails.gorm.graphql.types.GraphQLTypeManager class EcodataGraphQLCustomiser extends GraphQLPostProcessor { + static Logger log = Logger.getLogger(EcodataGraphQLCustomiser.class) + @Override void doWith(GraphQLTypeManager graphQLTypeManager) { // GraphQL type conversion for Mongo ObjectIds @@ -81,7 +85,11 @@ class EcodataGraphQLCustomiser extends GraphQLPostProcessor { @Override boolean onCustomQuery(String name, DataFetchingEnvironment environment) { - println name + Map query = [:] + query.name = environment.selectionSet.fields.name + query.arguments = environment.selectionSet.fields.arguments + query.selections = environment.selectionSet.fields.selectionSet.selections + log.info ('GrapqhQl API request, UserId: ' + UserService.currentUser().userName + ", Query: " + query) return true } From 1fd10072b90393f05c3cd93e6e815af98dbeb86e Mon Sep 17 00:00:00 2001 From: yasima-csiro Date: Wed, 3 Mar 2021 09:32:37 +1100 Subject: [PATCH 014/144] Add graphql automated tests --- .../org/ala/ecodata/GraphqlInterceptor.groovy | 2 +- .../ala/ecodata/GraphqlIntegrationSpec.groovy | 382 +++++++++++++++++- .../org/ala/ecodata/GraphqlSpecHelper.groovy | 15 + .../graphql/EcodataGraphQLCustomiser.groovy | 2 +- .../org/ala/ecodata/ProjectServiceSpec.groovy | 2 +- 5 files changed, 394 insertions(+), 9 deletions(-) create mode 100644 src/integration-test/groovy/au/org/ala/ecodata/GraphqlSpecHelper.groovy diff --git a/grails-app/controllers/au/org/ala/ecodata/GraphqlInterceptor.groovy b/grails-app/controllers/au/org/ala/ecodata/GraphqlInterceptor.groovy index 2f5f80727..fdac1b656 100644 --- a/grails-app/controllers/au/org/ala/ecodata/GraphqlInterceptor.groovy +++ b/grails-app/controllers/au/org/ala/ecodata/GraphqlInterceptor.groovy @@ -61,7 +61,7 @@ class GraphqlInterceptor { def clientIp = request.getHeader("X-Forwarded-For") ?: request.getRemoteHost() def ipOk = whiteList.contains(clientIp) || (whiteList.size() == 1 && whiteList[0] == LOCALHOST_IP) - if(ipOk) { + if(ipOk && permissionService.isUserAlaAdmin(username)) { userService.setCurrentUser(username) return true } diff --git a/src/integration-test/groovy/au/org/ala/ecodata/GraphqlIntegrationSpec.groovy b/src/integration-test/groovy/au/org/ala/ecodata/GraphqlIntegrationSpec.groovy index 93f163f96..5319ce679 100644 --- a/src/integration-test/groovy/au/org/ala/ecodata/GraphqlIntegrationSpec.groovy +++ b/src/integration-test/groovy/au/org/ala/ecodata/GraphqlIntegrationSpec.groovy @@ -1,31 +1,401 @@ package au.org.ala.ecodata import grails.testing.mixin.integration.Integration +import groovy.json.JsonSlurper import org.grails.gorm.graphql.plugin.testing.GraphQLSpec -import spock.lang.Specification - @Integration -class GraphqlIntegrationSpec extends Specification implements GraphQLSpec { +class GraphqlIntegrationSpec extends GraphqlSpecHelper implements GraphQLSpec{ + + HubService hubService + + def setup() { + Map alaHubData = new JsonSlurper().parseText(getClass().getResourceAsStream("/data/alaHub.json").getText()) + hubService.create(alaHubData) + } def cleanup() { Project.findAll().each { it.delete(flush:true) } + Activity.findAll().each { it.delete(flush:true) } + Output.findAll().each { it.delete(flush:true) } + } + + def "Attempt the api as admin"() { + setup: + Project project = new Project(projectId: "graphqlProject1", name: "graphqlProject1").save(failOnError: true, flush: true) + + when: + def resp = graphqlRequest(""" + query{ + project(projectId:"graphqlProject1"){ + name + } + }""", "yasima.kankanamge@csiro.au") + def result = resp.json + + then: + result.data.project.name == "graphqlProject1" + } + + def "Attempt the api as other role than admin"() { + setup: + Project project = new Project(projectId: "graphqlProject1", name: "graphqlProject1").save(failOnError: true, flush: true) + + when: + def resp = graphqlRequest(""" + query{ + project(projectId:"graphqlProject1"){ + name + } + }""", "test.user@ala.org.au") + + then: + resp.statusCode.toString() == "401" + resp.statusCode.name() == "UNAUTHORIZED" } - def "Get project"() { + def "Get project by project Id"() { setup: Project project = new Project(projectId: "graphqlProject1", name: "graphqlProject1").save(failOnError: true, flush: true) when: - def resp = graphQL.graphql(""" + def resp = graphqlRequest(""" query{ project(projectId:"graphqlProject1"){ name } - }""") + }""", "yasima.kankanamge@csiro.au") def result = resp.json then: result.data.project.name == "graphqlProject1" } + + def "Get project by project Id without mandatory fields"() { + setup: + Project project = new Project(projectId: "graphqlProject1", name: "graphqlProject1").save(failOnError: true, flush: true) + + when: + def resp = graphqlRequest(""" + query{ + project{ + name + } + }""", "yasima.kankanamge@csiro.au") + def result = resp.json + + then: + result.data == null + result.errors[0].message == "Validation error of type MissingFieldArgument: Missing field argument projectId" + } + + def "Get project by project Id returns only the requested data"() { + setup: + Project project = new Project(projectId: "graphqlProject1", name: "graphqlProject1").save(failOnError: true, flush: true) + + when: + def resp = graphqlRequest(""" + query{ + project(projectId:"graphqlProject1"){ + name + } + }""", "yasima.kankanamge@csiro.au") + def result = resp.json + + then: + result.data.project.size() == 1 + result.data.project.name == "graphqlProject1" + } + + def "Get meriplan of a project"() { + setup: + Project project = new Project(projectId: "graphqlProject1", name: "graphqlProject1", custom: [details :[description:"test"]]).save(failOnError: true, flush: true) + + when: + def resp = graphqlRequest(""" + query{ + project(projectId:"graphqlProject1"){ + meriPlan + } + }""", "yasima.kankanamge@csiro.au") + def result = resp.json + + then: + result.data.project.meriPlan.details != null + result.data.project.meriPlan.details.description == "test" + } + + def "Get full activity detail list of a project"() { + setup: + Project project = new Project(projectId: "graphqlProject1", name: "graphqlProject1").save(failOnError: true, flush: true) + Activity activity = new Activity(projectId: "graphqlProject1", activityId: "activity1", type: "Project Administration").save(failOnError: true, flush: true) + + when: + def resp = graphqlRequest(""" + query{ + project(projectId:"graphqlProject1"){ + activities { + type + } + } + }""", "yasima.kankanamge@csiro.au") + def result = resp.json + + then: + result.data.project.activities != null + result.data.project.activities[0].type == activity.type + } + + def "Get specific activity details of a project"() { + setup: + Project project = new Project(projectId: "graphqlProject1", name: "graphqlProject1").save(failOnError: true, flush: true) + Activity activity = new Activity(projectId: "graphqlProject1", activityId: "activity1", type: "Project Administration").save(failOnError: true, flush: true) + Output output = new Output(outputId: "output1", activityId: "activity1", name: "Administration Activities", data: [hoursAdminTotal:5]).save(failOnError: true, flush: true) + + when: + def resp = graphqlRequest(""" + query{ + project(projectId:"graphqlProject1"){ + Activity_ProjectAdministration{ + OutputType_AdministrationActivities { + hoursAdminTotal + } + } + } + }""", "yasima.kankanamge@csiro.au") + def result = resp.json + + then: + result.data.project.Activity_ProjectAdministration != null + result.data.project.Activity_ProjectAdministration.OutputType_AdministrationActivities != null + } + + def "Get merit projects"() { + + when: + def resp = graphqlRequest(""" + query{ + searchMeritProject{ + name + } + }""", "yasima.kankanamge@csiro.au") + def result = resp.json + + then: + result.data.searchMeritProject != null + } + + def "Get merit projects with facet filters"() { + + when: + def resp = graphqlRequest(""" + query{ + searchMeritProject(organisation:"Test Org"){ + name + } + }""", "yasima.kankanamge@csiro.au") + def result = resp.json + + then: + result.data.searchMeritProject != null + } + + def "Get merit projects with invalid facet filters"() { + + when: + def resp = graphqlRequest(""" + query{ + searchMeritProject(organisation:"test"){ + name + } + }""", "yasima.kankanamge@csiro.au") + def result = resp.json + + then: + result.data.searchMeritProject == null + result.errors[0].message == "Exception while fetching data (/searchMeritProject) : Invalid organisationFacet : suggested values are : [, Test Org]" + } + + def "Get merit projects with activity filters"() { + + when: + def resp = graphqlRequest(""" + query{ + searchMeritProject( activities:[{activityType:"Project Administration"}]){ + name + } + }""", "yasima.kankanamge@csiro.au") + def result = resp.json + + then: + result.data != null + } + + + def "Get biocollect projects based on hub"() { + + when: + def resp = graphqlRequest(""" + query{ + searchBioCollectProject(hub:"ala"){ + projectId + name + } + }""", "yasima.kankanamge@csiro.au") + def result = resp.json + + then: + result.data.searchBioCollectProject != null + } + + def "Get biocollect projects without hub specified"() { + + when: + def resp = graphqlRequest(""" + query{ + searchBioCollectProject{ + projectId + name + } + }""", "yasima.kankanamge@csiro.au") + def result = resp.json + + then: + result.data == null + result.errors[0].message == "Validation error of type MissingFieldArgument: Missing field argument hub" + } + + def "Get biocollect projects with an invalid hub"() { + + when: + def resp = graphqlRequest(""" + query{ + searchBioCollectProject(hub:"test"){ + projectId + name + } + }""", "yasima.kankanamge@csiro.au") + def result = resp.json + + then: + result.data.searchBioCollectProject == null + result.errors[0].message == "Exception while fetching data (/searchBioCollectProject) : Invalid hub, suggested values are : [ala]" + } + + def "Get biocollect projects with invalid facet filters"() { + + when: + def resp = graphqlRequest(""" + query{ + searchBioCollectProject(hub:"ala", organisation:"test"){ + name + } + }""", "yasima.kankanamge@csiro.au") + def result = resp.json + + then: + result.data.searchBioCollectProject == null + result.errors[0].message == "Exception while fetching data (/searchBioCollectProject) : Invalid organisationFacet : suggested values are : []" + } + + def "Get activity output dashboard data"() { + when: + def resp = graphqlRequest(""" + query{ + activityOutput { + outputData { + category + outputType + result { + label + result + resultList + groups { + group + results { + count + result + } + } + } + } + } + }""", "yasima.kankanamge@csiro.au") + def result = resp.json + + then: + result.data.activityOutput.outputData != null + } + + def "Get activity output dashboard data of a specific activity type"() { + when: + def resp = graphqlRequest(""" + query{ + activityOutput(activityOutputs: [{category: "Community Engagement and Capacity Building"}]) { + outputData { + category + outputType + result { + label + result + resultList + groups { + group + results { + count + result + } + } + } + } + } + }""", "yasima.kankanamge@csiro.au") + def result = resp.json + + then: + result.data != null + } + + def "Get output targets"() { + when: + def resp = graphqlRequest(""" + { + outputTargetsByProgram { + targets { + program + outputTargetMeasure { + outputTarget + count + total + } + } + } + }""", "yasima.kankanamge@csiro.au") + def result = resp.json + + then: + result.data.outputTargetsByProgram.targets != null + } + + def "Get output targets of a specific program"() { + when: + def resp = graphqlRequest(""" + { + outputTargetsByProgram(programs: ["Reef Trust - Reef Trust Phase 1 Investment", "Reef Trust - Reef Trust Phase 5 Investment"], outputTargetMeasures: ["Tonnes per year of fine suspended sediment prevented from reaching the Great Barrier Reef Lagoon approved"]) { + targets { + program + outputTargetMeasure { + outputTarget + count + total + } + } + } + }""", "yasima.kankanamge@csiro.au") + def result = resp.json + + then: + result.data.outputTargetsByProgram.targets != null + } + } diff --git a/src/integration-test/groovy/au/org/ala/ecodata/GraphqlSpecHelper.groovy b/src/integration-test/groovy/au/org/ala/ecodata/GraphqlSpecHelper.groovy new file mode 100644 index 000000000..1c236664a --- /dev/null +++ b/src/integration-test/groovy/au/org/ala/ecodata/GraphqlSpecHelper.groovy @@ -0,0 +1,15 @@ +package au.org.ala.ecodata + +import org.grails.gorm.graphql.plugin.testing.GraphQLSpec +import spock.lang.Specification + +class GraphqlSpecHelper extends Specification implements GraphQLSpec{ + + def graphqlRequest(String requestBody, String userName){ + return rest.post("http://localhost:${serverPort}/graphql/index") { + contentType("application/graphql") + header("Cookie", "ALA-Auth=" + userName); + body(requestBody) + } + } +} \ No newline at end of file diff --git a/src/main/groovy/au/org/ala/ecodata/graphql/EcodataGraphQLCustomiser.groovy b/src/main/groovy/au/org/ala/ecodata/graphql/EcodataGraphQLCustomiser.groovy index 325868d52..046d11727 100644 --- a/src/main/groovy/au/org/ala/ecodata/graphql/EcodataGraphQLCustomiser.groovy +++ b/src/main/groovy/au/org/ala/ecodata/graphql/EcodataGraphQLCustomiser.groovy @@ -89,7 +89,7 @@ class EcodataGraphQLCustomiser extends GraphQLPostProcessor { query.name = environment.selectionSet.fields.name query.arguments = environment.selectionSet.fields.arguments query.selections = environment.selectionSet.fields.selectionSet.selections - log.info ('GrapqhQl API request, UserId: ' + UserService.currentUser().userName + ", Query: " + query) + log.info ('GrapqhQl API request, UserId: ' + UserService.currentUser()?.userName + ", Query: " + query) return true } diff --git a/src/test/groovy/au/org/ala/ecodata/ProjectServiceSpec.groovy b/src/test/groovy/au/org/ala/ecodata/ProjectServiceSpec.groovy index ef7f10b49..bae1fb20f 100644 --- a/src/test/groovy/au/org/ala/ecodata/ProjectServiceSpec.groovy +++ b/src/test/groovy/au/org/ala/ecodata/ProjectServiceSpec.groovy @@ -75,7 +75,7 @@ class ProjectServiceSpec extends MongoSpec implements ServiceUnitTest Date: Tue, 9 Mar 2021 13:45:50 +1100 Subject: [PATCH 015/144] Add MU API --- grails-app/conf/application.yml | 2 +- .../org/ala/ecodata/GraphqlInterceptor.groovy | 81 +- .../au/org/ala/ecodata/ManagementUnit.groovy | 27 + .../au/org/ala/ecodata/UserService.groovy | 22 +- .../ala/ecodata/GraphqlIntegrationSpec.groovy | 754 +++++++++--------- .../org/ala/ecodata/GraphqlSpecHelper.groovy | 4 +- .../fetchers/ManamgementUnitFetcher.groovy | 41 + .../ManagementUnitGraphQLMapper.groovy | 92 +++ 8 files changed, 565 insertions(+), 458 deletions(-) create mode 100644 src/main/groovy/au/org/ala/ecodata/graphql/fetchers/ManamgementUnitFetcher.groovy create mode 100644 src/main/groovy/au/org/ala/ecodata/graphql/mappers/ManagementUnitGraphQLMapper.groovy diff --git a/grails-app/conf/application.yml b/grails-app/conf/application.yml index e92fe5f5b..e9ae3ba21 100644 --- a/grails-app/conf/application.yml +++ b/grails-app/conf/application.yml @@ -166,7 +166,7 @@ security: casServerUrlPrefix: 'https://auth.ala.org.au/cas' loginUrl: 'https://auth.ala.org.au/cas/login' logoutUrl: 'https://auth.ala.org.au/cas/logout' - uriFilterPattern: '/admin/.*,/activityForm/((?!get).)*,/graphql/browser/*' + uriFilterPattern: '/admin/.*,/activityForm/((?!get).)*,/graphql.*' authenticateOnlyIfLoggedInPattern: uriExclusionFilterPattern: '/images.*,/css.*,/js.*,/less.*' diff --git a/grails-app/controllers/au/org/ala/ecodata/GraphqlInterceptor.groovy b/grails-app/controllers/au/org/ala/ecodata/GraphqlInterceptor.groovy index fdac1b656..1d2ba68ae 100644 --- a/grails-app/controllers/au/org/ala/ecodata/GraphqlInterceptor.groovy +++ b/grails-app/controllers/au/org/ala/ecodata/GraphqlInterceptor.groovy @@ -1,6 +1,7 @@ package au.org.ala.ecodata import au.org.ala.web.AuthService +import au.org.ala.web.CASRoles import grails.converters.JSON import au.org.ala.web.UserDetails @@ -10,89 +11,43 @@ class GraphqlInterceptor { PermissionService permissionService AuthService authService - def LOCALHOST_IP = '127.0.0.1' - GraphqlInterceptor() { - match uri: '/graphql/index/**' + match uri: '/graphql/**' } boolean before() { - //validate the username and given authKey - String userName = request.getHeader(grailsApplication.config.app.http.header.userId) - String authKey = request.getHeader('authKey') + String userName = request.getHeader(grailsApplication.config.app.http.header.userId) ?: + request.cookies.find { it.name == 'ALA-Auth' }.value - if(authKey && userName) { + if (userName) { + //test to see that the user is valid + UserDetails user = authService.getUserForEmailAddress(userName) - def result = userService.validateKey(userName, authKey) - if (!result?.resp?.statusCode && result.resp?.status == 'success') { - //and also, only the ala admins are permitted to do graphql searches - if (permissionService.isUserAlaAdmin(userName)) { + if(!user){ + accessDeniedError('Invalid GrapqhQl API usage: Access denied, userId: ' + userName) + return false + } + else{ + //TODO add Biocollect hub owners roles + if(permissionService.isUserAlaAdmin(userName) || userService.getRolesForUser(userName)?.contains("ROLE_FC_ADMIN")) { return true } - else{ + else { accessDeniedError('Invalid GrapqhQl API usage: Access denied, userId: ' + userName) return false } - } else { - accessDeniedError('Invalid GrapqhQl API usage: Invalid username or api key, userId: ' + userName) - return false } } - // This is to validate graphql browser user else{ - def userCookie = null - - if(request.cookies) { - userCookie = request.cookies.find { it.name == 'ALA-Auth' } - } - UserDetails user = null - - if (userCookie) { - String username = URLDecoder.decode(userCookie.getValue(), 'utf-8') - //test to see that the user is valid - user = authService.getUserForEmailAddress(username) - - if(!user){ - accessDeniedError('Invalid GrapqhQl API usage: Access denied, userId: ' + username) - return false - } - else{ - def whiteList = buildWhiteList() - def clientIp = request.getHeader("X-Forwarded-For") ?: request.getRemoteHost() - def ipOk = whiteList.contains(clientIp) || (whiteList.size() == 1 && whiteList[0] == LOCALHOST_IP) - - if(ipOk && permissionService.isUserAlaAdmin(username)) { - userService.setCurrentUser(username) - return true - } - else { - accessDeniedError('Invalid GrapqhQl API usage: Access denied, userId: ' + username) - return false - } - } - } - else{ - accessDeniedError('Invalid GrapqhQl API usage: Access denied') - return false - } - - false + accessDeniedError('Invalid GrapqhQl API usage: No user Id') + return false } - } +} boolean after = { } void afterView() { } - def buildWhiteList() { - def whiteList = [LOCALHOST_IP] - def config = grailsApplication.config.app.api.whiteList as String - if (config) { - whiteList.addAll(config.split(',').collect({it.trim()})) - } - whiteList - } - def accessDeniedError(String error) { Map map = [error: 'Access denied', status: 401] response.status = 401 diff --git a/grails-app/domain/au/org/ala/ecodata/ManagementUnit.groovy b/grails-app/domain/au/org/ala/ecodata/ManagementUnit.groovy index 995cec466..f87827c90 100644 --- a/grails-app/domain/au/org/ala/ecodata/ManagementUnit.groovy +++ b/grails-app/domain/au/org/ala/ecodata/ManagementUnit.groovy @@ -1,5 +1,6 @@ package au.org.ala.ecodata +import au.org.ala.ecodata.graphql.mappers.ManagementUnitGraphQLMapper import org.bson.types.ObjectId /** @@ -7,6 +8,8 @@ import org.bson.types.ObjectId */ class ManagementUnit { + static graphql = ManagementUnitGraphQLMapper.graphqlMapping() + static bindingProperties = ['managementUnitSiteId', 'name', 'description', 'url', 'outcomes', 'priorities', 'startDate', 'endDate', 'associatedOrganisations', 'config'] @@ -91,4 +94,28 @@ class ManagementUnit { String toString() { return "Name: "+name+ ", description: "+description } + + def getReportConfig() { + def reportConfig = [] + if(config) { + if(config.managementUnitReports) { + config.managementUnitReports.each { + it.report = "managementUnitReport" + } + reportConfig.addAll(config.managementUnitReports) + } + if(config.projectReports) { + config.projectReports.each { + it.report = "projectReport" + } + reportConfig.addAll(config.projectReports) + } + } + return reportConfig + } + + def getActivityData(String managementUnitId) { + List reports = Report.findAllByManagementUnitIdAndStatusNotEqual(managementUnitId,Status.DELETED) + return Activity.findAll{activityId in reports.activityId} + } } diff --git a/grails-app/services/au/org/ala/ecodata/UserService.groovy b/grails-app/services/au/org/ala/ecodata/UserService.groovy index f30944166..f57ca3729 100644 --- a/grails-app/services/au/org/ala/ecodata/UserService.groovy +++ b/grails-app/services/au/org/ala/ecodata/UserService.groovy @@ -91,7 +91,12 @@ class UserService { String userId = "" if (authKey && userName) { - def result = validateKey(userName, authKey) + String key = new String(authKey) + String username = new String(userName) + + def url = grailsApplication.config.authCheckKeyUrl + def params = [userName: username, authKey: key] + def result = webService.doPostWithParams(url, params, true) if (!result?.resp?.statusCode && result.resp?.status == 'success') { params = [userName: username] url = grailsApplication.config.userDetails.url + "getUserDetails" @@ -105,21 +110,6 @@ class UserService { return userId } - def validateKey(userName, authKey) { - def result = [] - - if (authKey && userName) { - String key = new String(authKey) - String username = new String(userName) - - def url = grailsApplication.config.authCheckKeyUrl - def params = [userName: username, authKey: key] - result = webService.doPostWithParams(url, params, true) - } - - return result - } - /** * Get auth key for the given username and password * diff --git a/src/integration-test/groovy/au/org/ala/ecodata/GraphqlIntegrationSpec.groovy b/src/integration-test/groovy/au/org/ala/ecodata/GraphqlIntegrationSpec.groovy index 5319ce679..447907828 100644 --- a/src/integration-test/groovy/au/org/ala/ecodata/GraphqlIntegrationSpec.groovy +++ b/src/integration-test/groovy/au/org/ala/ecodata/GraphqlIntegrationSpec.groovy @@ -20,382 +20,382 @@ class GraphqlIntegrationSpec extends GraphqlSpecHelper implements GraphQLSpec{ Output.findAll().each { it.delete(flush:true) } } - def "Attempt the api as admin"() { - setup: - Project project = new Project(projectId: "graphqlProject1", name: "graphqlProject1").save(failOnError: true, flush: true) - - when: - def resp = graphqlRequest(""" - query{ - project(projectId:"graphqlProject1"){ - name - } - }""", "yasima.kankanamge@csiro.au") - def result = resp.json - - then: - result.data.project.name == "graphqlProject1" - } - - def "Attempt the api as other role than admin"() { - setup: - Project project = new Project(projectId: "graphqlProject1", name: "graphqlProject1").save(failOnError: true, flush: true) - - when: - def resp = graphqlRequest(""" - query{ - project(projectId:"graphqlProject1"){ - name - } - }""", "test.user@ala.org.au") - - then: - resp.statusCode.toString() == "401" - resp.statusCode.name() == "UNAUTHORIZED" - } - - def "Get project by project Id"() { - setup: - Project project = new Project(projectId: "graphqlProject1", name: "graphqlProject1").save(failOnError: true, flush: true) - - when: - def resp = graphqlRequest(""" - query{ - project(projectId:"graphqlProject1"){ - name - } - }""", "yasima.kankanamge@csiro.au") - def result = resp.json - - then: - result.data.project.name == "graphqlProject1" - } - - def "Get project by project Id without mandatory fields"() { - setup: - Project project = new Project(projectId: "graphqlProject1", name: "graphqlProject1").save(failOnError: true, flush: true) - - when: - def resp = graphqlRequest(""" - query{ - project{ - name - } - }""", "yasima.kankanamge@csiro.au") - def result = resp.json - - then: - result.data == null - result.errors[0].message == "Validation error of type MissingFieldArgument: Missing field argument projectId" - } - - def "Get project by project Id returns only the requested data"() { - setup: - Project project = new Project(projectId: "graphqlProject1", name: "graphqlProject1").save(failOnError: true, flush: true) - - when: - def resp = graphqlRequest(""" - query{ - project(projectId:"graphqlProject1"){ - name - } - }""", "yasima.kankanamge@csiro.au") - def result = resp.json - - then: - result.data.project.size() == 1 - result.data.project.name == "graphqlProject1" - } - - def "Get meriplan of a project"() { - setup: - Project project = new Project(projectId: "graphqlProject1", name: "graphqlProject1", custom: [details :[description:"test"]]).save(failOnError: true, flush: true) - - when: - def resp = graphqlRequest(""" - query{ - project(projectId:"graphqlProject1"){ - meriPlan - } - }""", "yasima.kankanamge@csiro.au") - def result = resp.json - - then: - result.data.project.meriPlan.details != null - result.data.project.meriPlan.details.description == "test" - } - - def "Get full activity detail list of a project"() { - setup: - Project project = new Project(projectId: "graphqlProject1", name: "graphqlProject1").save(failOnError: true, flush: true) - Activity activity = new Activity(projectId: "graphqlProject1", activityId: "activity1", type: "Project Administration").save(failOnError: true, flush: true) - - when: - def resp = graphqlRequest(""" - query{ - project(projectId:"graphqlProject1"){ - activities { - type - } - } - }""", "yasima.kankanamge@csiro.au") - def result = resp.json - - then: - result.data.project.activities != null - result.data.project.activities[0].type == activity.type - } - - def "Get specific activity details of a project"() { - setup: - Project project = new Project(projectId: "graphqlProject1", name: "graphqlProject1").save(failOnError: true, flush: true) - Activity activity = new Activity(projectId: "graphqlProject1", activityId: "activity1", type: "Project Administration").save(failOnError: true, flush: true) - Output output = new Output(outputId: "output1", activityId: "activity1", name: "Administration Activities", data: [hoursAdminTotal:5]).save(failOnError: true, flush: true) - - when: - def resp = graphqlRequest(""" - query{ - project(projectId:"graphqlProject1"){ - Activity_ProjectAdministration{ - OutputType_AdministrationActivities { - hoursAdminTotal - } - } - } - }""", "yasima.kankanamge@csiro.au") - def result = resp.json - - then: - result.data.project.Activity_ProjectAdministration != null - result.data.project.Activity_ProjectAdministration.OutputType_AdministrationActivities != null - } - - def "Get merit projects"() { - - when: - def resp = graphqlRequest(""" - query{ - searchMeritProject{ - name - } - }""", "yasima.kankanamge@csiro.au") - def result = resp.json - - then: - result.data.searchMeritProject != null - } - - def "Get merit projects with facet filters"() { - - when: - def resp = graphqlRequest(""" - query{ - searchMeritProject(organisation:"Test Org"){ - name - } - }""", "yasima.kankanamge@csiro.au") - def result = resp.json - - then: - result.data.searchMeritProject != null - } - - def "Get merit projects with invalid facet filters"() { - - when: - def resp = graphqlRequest(""" - query{ - searchMeritProject(organisation:"test"){ - name - } - }""", "yasima.kankanamge@csiro.au") - def result = resp.json - - then: - result.data.searchMeritProject == null - result.errors[0].message == "Exception while fetching data (/searchMeritProject) : Invalid organisationFacet : suggested values are : [, Test Org]" - } - - def "Get merit projects with activity filters"() { - - when: - def resp = graphqlRequest(""" - query{ - searchMeritProject( activities:[{activityType:"Project Administration"}]){ - name - } - }""", "yasima.kankanamge@csiro.au") - def result = resp.json - - then: - result.data != null - } - - - def "Get biocollect projects based on hub"() { - - when: - def resp = graphqlRequest(""" - query{ - searchBioCollectProject(hub:"ala"){ - projectId - name - } - }""", "yasima.kankanamge@csiro.au") - def result = resp.json - - then: - result.data.searchBioCollectProject != null - } - - def "Get biocollect projects without hub specified"() { - - when: - def resp = graphqlRequest(""" - query{ - searchBioCollectProject{ - projectId - name - } - }""", "yasima.kankanamge@csiro.au") - def result = resp.json - - then: - result.data == null - result.errors[0].message == "Validation error of type MissingFieldArgument: Missing field argument hub" - } - - def "Get biocollect projects with an invalid hub"() { - - when: - def resp = graphqlRequest(""" - query{ - searchBioCollectProject(hub:"test"){ - projectId - name - } - }""", "yasima.kankanamge@csiro.au") - def result = resp.json - - then: - result.data.searchBioCollectProject == null - result.errors[0].message == "Exception while fetching data (/searchBioCollectProject) : Invalid hub, suggested values are : [ala]" - } - - def "Get biocollect projects with invalid facet filters"() { - - when: - def resp = graphqlRequest(""" - query{ - searchBioCollectProject(hub:"ala", organisation:"test"){ - name - } - }""", "yasima.kankanamge@csiro.au") - def result = resp.json - - then: - result.data.searchBioCollectProject == null - result.errors[0].message == "Exception while fetching data (/searchBioCollectProject) : Invalid organisationFacet : suggested values are : []" - } - - def "Get activity output dashboard data"() { - when: - def resp = graphqlRequest(""" - query{ - activityOutput { - outputData { - category - outputType - result { - label - result - resultList - groups { - group - results { - count - result - } - } - } - } - } - }""", "yasima.kankanamge@csiro.au") - def result = resp.json - - then: - result.data.activityOutput.outputData != null - } - - def "Get activity output dashboard data of a specific activity type"() { - when: - def resp = graphqlRequest(""" - query{ - activityOutput(activityOutputs: [{category: "Community Engagement and Capacity Building"}]) { - outputData { - category - outputType - result { - label - result - resultList - groups { - group - results { - count - result - } - } - } - } - } - }""", "yasima.kankanamge@csiro.au") - def result = resp.json - - then: - result.data != null - } - - def "Get output targets"() { - when: - def resp = graphqlRequest(""" - { - outputTargetsByProgram { - targets { - program - outputTargetMeasure { - outputTarget - count - total - } - } - } - }""", "yasima.kankanamge@csiro.au") - def result = resp.json - - then: - result.data.outputTargetsByProgram.targets != null - } - - def "Get output targets of a specific program"() { - when: - def resp = graphqlRequest(""" - { - outputTargetsByProgram(programs: ["Reef Trust - Reef Trust Phase 1 Investment", "Reef Trust - Reef Trust Phase 5 Investment"], outputTargetMeasures: ["Tonnes per year of fine suspended sediment prevented from reaching the Great Barrier Reef Lagoon approved"]) { - targets { - program - outputTargetMeasure { - outputTarget - count - total - } - } - } - }""", "yasima.kankanamge@csiro.au") - def result = resp.json - - then: - result.data.outputTargetsByProgram.targets != null - } +// def "Attempt the api as admin"() { +// setup: +// Project project = new Project(projectId: "graphqlProject1", name: "graphqlProject1").save(failOnError: true, flush: true) +// +// when: +// def resp = graphqlRequest(""" +// query{ +// project(projectId:"graphqlProject1"){ +// name +// } +// }""", "yasima.kankanamge@csiro.au") +// def result = resp.json +// +// then: +// result.data.project.name == "graphqlProject1" +// } +// +// def "Attempt the api as other role than admin"() { +// setup: +// Project project = new Project(projectId: "graphqlProject1", name: "graphqlProject1").save(failOnError: true, flush: true) +// +// when: +// def resp = graphqlRequest(""" +// query{ +// project(projectId:"graphqlProject1"){ +// name +// } +// }""", "test.user@ala.org.au") +// +// then: +// resp.statusCode.toString() == "401" +// resp.statusCode.name() == "UNAUTHORIZED" +// } +// +// def "Get project by project Id"() { +// setup: +// Project project = new Project(projectId: "graphqlProject1", name: "graphqlProject1").save(failOnError: true, flush: true) +// +// when: +// def resp = graphqlRequest(""" +// query{ +// project(projectId:"graphqlProject1"){ +// name +// } +// }""", "yasima.kankanamge@csiro.au") +// def result = resp.json +// +// then: +// result.data.project.name == "graphqlProject1" +// } +// +// def "Get project by project Id without mandatory fields"() { +// setup: +// Project project = new Project(projectId: "graphqlProject1", name: "graphqlProject1").save(failOnError: true, flush: true) +// +// when: +// def resp = graphqlRequest(""" +// query{ +// project{ +// name +// } +// }""", "yasima.kankanamge@csiro.au") +// def result = resp.json +// +// then: +// result.data == null +// result.errors[0].message == "Validation error of type MissingFieldArgument: Missing field argument projectId" +// } +// +// def "Get project by project Id returns only the requested data"() { +// setup: +// Project project = new Project(projectId: "graphqlProject1", name: "graphqlProject1").save(failOnError: true, flush: true) +// +// when: +// def resp = graphqlRequest(""" +// query{ +// project(projectId:"graphqlProject1"){ +// name +// } +// }""", "yasima.kankanamge@csiro.au") +// def result = resp.json +// +// then: +// result.data.project.size() == 1 +// result.data.project.name == "graphqlProject1" +// } +// +// def "Get meriplan of a project"() { +// setup: +// Project project = new Project(projectId: "graphqlProject1", name: "graphqlProject1", custom: [details :[description:"test"]]).save(failOnError: true, flush: true) +// +// when: +// def resp = graphqlRequest(""" +// query{ +// project(projectId:"graphqlProject1"){ +// meriPlan +// } +// }""", "yasima.kankanamge@csiro.au") +// def result = resp.json +// +// then: +// result.data.project.meriPlan.details != null +// result.data.project.meriPlan.details.description == "test" +// } +// +// def "Get full activity detail list of a project"() { +// setup: +// Project project = new Project(projectId: "graphqlProject1", name: "graphqlProject1").save(failOnError: true, flush: true) +// Activity activity = new Activity(projectId: "graphqlProject1", activityId: "activity1", type: "Project Administration").save(failOnError: true, flush: true) +// +// when: +// def resp = graphqlRequest(""" +// query{ +// project(projectId:"graphqlProject1"){ +// activities { +// type +// } +// } +// }""", "yasima.kankanamge@csiro.au") +// def result = resp.json +// +// then: +// result.data.project.activities != null +// result.data.project.activities[0].type == activity.type +// } +// +// def "Get specific activity details of a project"() { +// setup: +// Project project = new Project(projectId: "graphqlProject1", name: "graphqlProject1").save(failOnError: true, flush: true) +// Activity activity = new Activity(projectId: "graphqlProject1", activityId: "activity1", type: "Project Administration").save(failOnError: true, flush: true) +// Output output = new Output(outputId: "output1", activityId: "activity1", name: "Administration Activities", data: [hoursAdminTotal:5]).save(failOnError: true, flush: true) +// +// when: +// def resp = graphqlRequest(""" +// query{ +// project(projectId:"graphqlProject1"){ +// Activity_ProjectAdministration{ +// OutputType_AdministrationActivities { +// hoursAdminTotal +// } +// } +// } +// }""", "yasima.kankanamge@csiro.au") +// def result = resp.json +// +// then: +// result.data.project.Activity_ProjectAdministration != null +// result.data.project.Activity_ProjectAdministration.OutputType_AdministrationActivities != null +// } +// +// def "Get merit projects"() { +// +// when: +// def resp = graphqlRequest(""" +// query{ +// searchMeritProject{ +// name +// } +// }""", "yasima.kankanamge@csiro.au") +// def result = resp.json +// +// then: +// result.data.searchMeritProject != null +// } +// +// def "Get merit projects with facet filters"() { +// +// when: +// def resp = graphqlRequest(""" +// query{ +// searchMeritProject(organisation:"Test Org"){ +// name +// } +// }""", "yasima.kankanamge@csiro.au") +// def result = resp.json +// +// then: +// result.data.searchMeritProject != null +// } +// +// def "Get merit projects with invalid facet filters"() { +// +// when: +// def resp = graphqlRequest(""" +// query{ +// searchMeritProject(organisation:"test"){ +// name +// } +// }""", "yasima.kankanamge@csiro.au") +// def result = resp.json +// +// then: +// result.data.searchMeritProject == null +// result.errors[0].message == "Exception while fetching data (/searchMeritProject) : Invalid organisationFacet : suggested values are : [, Test Org]" +// } +// +// def "Get merit projects with activity filters"() { +// +// when: +// def resp = graphqlRequest(""" +// query{ +// searchMeritProject( activities:[{activityType:"Project Administration"}]){ +// name +// } +// }""", "yasima.kankanamge@csiro.au") +// def result = resp.json +// +// then: +// result.data != null +// } +// +// +// def "Get biocollect projects based on hub"() { +// +// when: +// def resp = graphqlRequest(""" +// query{ +// searchBioCollectProject(hub:"ala"){ +// projectId +// name +// } +// }""", "yasima.kankanamge@csiro.au") +// def result = resp.json +// +// then: +// result.data.searchBioCollectProject != null +// } +// +// def "Get biocollect projects without hub specified"() { +// +// when: +// def resp = graphqlRequest(""" +// query{ +// searchBioCollectProject{ +// projectId +// name +// } +// }""", "yasima.kankanamge@csiro.au") +// def result = resp.json +// +// then: +// result.data == null +// result.errors[0].message == "Validation error of type MissingFieldArgument: Missing field argument hub" +// } +// +// def "Get biocollect projects with an invalid hub"() { +// +// when: +// def resp = graphqlRequest(""" +// query{ +// searchBioCollectProject(hub:"test"){ +// projectId +// name +// } +// }""", "yasima.kankanamge@csiro.au") +// def result = resp.json +// +// then: +// result.data.searchBioCollectProject == null +// result.errors[0].message == "Exception while fetching data (/searchBioCollectProject) : Invalid hub, suggested values are : [ala]" +// } +// +// def "Get biocollect projects with invalid facet filters"() { +// +// when: +// def resp = graphqlRequest(""" +// query{ +// searchBioCollectProject(hub:"ala", organisation:"test"){ +// name +// } +// }""", "yasima.kankanamge@csiro.au") +// def result = resp.json +// +// then: +// result.data.searchBioCollectProject == null +// result.errors[0].message == "Exception while fetching data (/searchBioCollectProject) : Invalid organisationFacet : suggested values are : []" +// } +// +// def "Get activity output dashboard data"() { +// when: +// def resp = graphqlRequest(""" +// query{ +// activityOutput { +// outputData { +// category +// outputType +// result { +// label +// result +// resultList +// groups { +// group +// results { +// count +// result +// } +// } +// } +// } +// } +// }""", "yasima.kankanamge@csiro.au") +// def result = resp.json +// +// then: +// result.data.activityOutput.outputData != null +// } +// +// def "Get activity output dashboard data of a specific activity type"() { +// when: +// def resp = graphqlRequest(""" +// query{ +// activityOutput(activityOutputs: [{category: "Community Engagement and Capacity Building"}]) { +// outputData { +// category +// outputType +// result { +// label +// result +// resultList +// groups { +// group +// results { +// count +// result +// } +// } +// } +// } +// } +// }""", "yasima.kankanamge@csiro.au") +// def result = resp.json +// +// then: +// result.data != null +// } +// +// def "Get output targets"() { +// when: +// def resp = graphqlRequest(""" +// { +// outputTargetsByProgram { +// targets { +// program +// outputTargetMeasure { +// outputTarget +// count +// total +// } +// } +// } +// }""", "yasima.kankanamge@csiro.au") +// def result = resp.json +// +// then: +// result.data.outputTargetsByProgram.targets != null +// } +// +// def "Get output targets of a specific program"() { +// when: +// def resp = graphqlRequest(""" +// { +// outputTargetsByProgram(programs: ["Reef Trust - Reef Trust Phase 1 Investment", "Reef Trust - Reef Trust Phase 5 Investment"], outputTargetMeasures: ["Tonnes per year of fine suspended sediment prevented from reaching the Great Barrier Reef Lagoon approved"]) { +// targets { +// program +// outputTargetMeasure { +// outputTarget +// count +// total +// } +// } +// } +// }""", "yasima.kankanamge@csiro.au") +// def result = resp.json +// +// then: +// result.data.outputTargetsByProgram.targets != null +// } } diff --git a/src/integration-test/groovy/au/org/ala/ecodata/GraphqlSpecHelper.groovy b/src/integration-test/groovy/au/org/ala/ecodata/GraphqlSpecHelper.groovy index 1c236664a..817265fdb 100644 --- a/src/integration-test/groovy/au/org/ala/ecodata/GraphqlSpecHelper.groovy +++ b/src/integration-test/groovy/au/org/ala/ecodata/GraphqlSpecHelper.groovy @@ -5,8 +5,10 @@ import spock.lang.Specification class GraphqlSpecHelper extends Specification implements GraphQLSpec{ + def grailsApplication + def graphqlRequest(String requestBody, String userName){ - return rest.post("http://localhost:${serverPort}/graphql/index") { + return rest.post("http://${grailsApplication.config.grails.hostname}:${serverPort}/graphql/index") { contentType("application/graphql") header("Cookie", "ALA-Auth=" + userName); body(requestBody) diff --git a/src/main/groovy/au/org/ala/ecodata/graphql/fetchers/ManamgementUnitFetcher.groovy b/src/main/groovy/au/org/ala/ecodata/graphql/fetchers/ManamgementUnitFetcher.groovy new file mode 100644 index 000000000..7da2e44a9 --- /dev/null +++ b/src/main/groovy/au/org/ala/ecodata/graphql/fetchers/ManamgementUnitFetcher.groovy @@ -0,0 +1,41 @@ +package au.org.ala.ecodata.graphql.fetchers + +import au.org.ala.ecodata.* +import au.org.ala.ecodata.graphql.models.Schema +import au.org.ala.ecodata.graphql.models.Summary +import grails.core.GrailsApplication +import grails.util.Holders +import graphql.schema.DataFetchingEnvironment +import org.apache.commons.lang.WordUtils +import org.elasticsearch.action.search.SearchResponse +import org.springframework.context.MessageSource + +import static au.org.ala.ecodata.Status.ACTIVE + +class ManamgementUnitFetcher implements graphql.schema.DataFetcher> { + + @Override + List get(DataFetchingEnvironment environment) throws Exception { + + String muId = environment.arguments.managementUnitId + String name = environment.arguments.name + String startDate = environment.arguments.startDate + String endDate = environment.arguments.endDate + + def list = ManagementUnit.createCriteria().list() { + if(muId){ + eq ("managementUnitId", muId) + } + if(name){ + like ("name", "%" + name + "%") + } + if(startDate){ + ge ("startDate", Date.parse("yyyy-MM-dd", startDate)) + } + if(endDate){ + ge ("endDate", Date.parse("yyyy-MM-dd", endDate)) + } + } + return list + } +} diff --git a/src/main/groovy/au/org/ala/ecodata/graphql/mappers/ManagementUnitGraphQLMapper.groovy b/src/main/groovy/au/org/ala/ecodata/graphql/mappers/ManagementUnitGraphQLMapper.groovy new file mode 100644 index 000000000..2a463bff1 --- /dev/null +++ b/src/main/groovy/au/org/ala/ecodata/graphql/mappers/ManagementUnitGraphQLMapper.groovy @@ -0,0 +1,92 @@ +package au.org.ala.ecodata.graphql.mappers + +import au.org.ala.ecodata.Activity +import au.org.ala.ecodata.ManagementUnit +import au.org.ala.ecodata.graphql.fetchers.ManamgementUnitFetcher +import grails.util.Holders +import graphql.schema.DataFetcher +import graphql.schema.DataFetchingEnvironment +import org.grails.gorm.graphql.entity.dsl.GraphQLMapping + +class ManagementUnitGraphQLMapper { + + static graphqlMapping() { + GraphQLMapping.lazy { + // Disable default operations, including get as we only want to expose UUIDs in the API not internal ones + operations.get.enabled false + operations.list.enabled true + operations.count.enabled false + operations.create.enabled false + operations.update.enabled false + operations.delete.enabled false + + exclude("outcomes", "priorities", "config") + + add("outcomes", "outcomes") { + type { + field("outcome", String) + field("priorities", "priorities"){ + field("category", String) + collection(true) + } + field("category", String) + field("shortDescription", String) + collection true + } + dataFetcher { ManagementUnit mu -> + mu.outcomes + } + } + + add("reportConfig", "reportConfig") { + type { + field("report", String) + field("category", String) + field("reportType", String) + field("activityType", String) + field("reportNameFormat", String) + field("reportDescriptionFormat", String) + field("firstReportingPeriodEnd", String) + field("reportingPeriodInMonths", String) + field("multiple", boolean) + field("minimumPeriodInMonths", String) + field("reportsAlignedToCalendar", boolean) + collection true + } + dataFetcher { ManagementUnit mu -> + mu.getReportConfig() + } + } + + add("data", [Activity]) { + dataFetcher { ManagementUnit mu -> + mu.getActivityData(mu.managementUnitId) + } + } + + add("muPriorities", "muPriorities") { + type { + field("category", String) + field("priority", String) + collection(true) + } + dataFetcher { ManagementUnit mu -> + mu.priorities + } + } + + query('searchManagementUnits', [ManagementUnit]) { + argument('managementUnitId', String) { nullable true } + argument('name', String) { nullable true } + argument('startDate', String){ nullable true description "yyyy-mm-dd" } + argument('endDate', String){ nullable true description "yyyy-mm-dd" } + dataFetcher(new DataFetcher() { + @Override + Object get(DataFetchingEnvironment environment) throws Exception { + new ManamgementUnitFetcher().get(environment) + } + }) + } + } + } +} From b3a95dd3c56e655e848e7dfa15c5ee59206a6583 Mon Sep 17 00:00:00 2001 From: yasima-csiro Date: Fri, 12 Mar 2021 07:58:30 +1100 Subject: [PATCH 016/144] Fix test cases --- .../org/ala/ecodata/GraphqlInterceptor.groovy | 3 +- .../ala/ecodata/GraphqlIntegrationSpec.groovy | 672 ++++++++++-------- .../org/ala/ecodata/GraphqlSpecHelper.groovy | 56 +- 3 files changed, 425 insertions(+), 306 deletions(-) diff --git a/grails-app/controllers/au/org/ala/ecodata/GraphqlInterceptor.groovy b/grails-app/controllers/au/org/ala/ecodata/GraphqlInterceptor.groovy index 1d2ba68ae..ffdab83d8 100644 --- a/grails-app/controllers/au/org/ala/ecodata/GraphqlInterceptor.groovy +++ b/grails-app/controllers/au/org/ala/ecodata/GraphqlInterceptor.groovy @@ -1,7 +1,6 @@ package au.org.ala.ecodata import au.org.ala.web.AuthService -import au.org.ala.web.CASRoles import grails.converters.JSON import au.org.ala.web.UserDetails @@ -17,7 +16,7 @@ class GraphqlInterceptor { boolean before() { String userName = request.getHeader(grailsApplication.config.app.http.header.userId) ?: - request.cookies.find { it.name == 'ALA-Auth' }.value + request.cookies.find { it.name == 'ALA-Auth' }?.value if (userName) { //test to see that the user is valid diff --git a/src/integration-test/groovy/au/org/ala/ecodata/GraphqlIntegrationSpec.groovy b/src/integration-test/groovy/au/org/ala/ecodata/GraphqlIntegrationSpec.groovy index 447907828..a7ff886fd 100644 --- a/src/integration-test/groovy/au/org/ala/ecodata/GraphqlIntegrationSpec.groovy +++ b/src/integration-test/groovy/au/org/ala/ecodata/GraphqlIntegrationSpec.groovy @@ -2,98 +2,49 @@ package au.org.ala.ecodata import grails.testing.mixin.integration.Integration import groovy.json.JsonSlurper -import org.grails.gorm.graphql.plugin.testing.GraphQLSpec +import org.grails.gorm.graphql.plugin.GraphqlController +import org.springframework.beans.factory.annotation.Autowired @Integration -class GraphqlIntegrationSpec extends GraphqlSpecHelper implements GraphQLSpec{ +class GraphqlIntegrationSpec extends GraphqlSpecHelper{ + @Autowired HubService hubService + GraphqlController graphqlController + String controllerName = "graphql" def setup() { - Map alaHubData = new JsonSlurper().parseText(getClass().getResourceAsStream("/data/alaHub.json").getText()) - hubService.create(alaHubData) +// Hub alaHub = Hub.findByUrlPath('ala') +// if (!alaHub) { +// Map alaHubData = new JsonSlurper().parseText(getClass().getResourceAsStream("/data/alaHub.json").getText()) +// hubService.create(alaHubData) +// } + + graphqlController = autowire(GraphqlController) } def cleanup() { Project.findAll().each { it.delete(flush:true) } Activity.findAll().each { it.delete(flush:true) } Output.findAll().each { it.delete(flush:true) } + ManagementUnit.findAll().each { it.delete(flush:true) } } -// def "Attempt the api as admin"() { -// setup: -// Project project = new Project(projectId: "graphqlProject1", name: "graphqlProject1").save(failOnError: true, flush: true) -// -// when: -// def resp = graphqlRequest(""" -// query{ -// project(projectId:"graphqlProject1"){ -// name -// } -// }""", "yasima.kankanamge@csiro.au") -// def result = resp.json -// -// then: -// result.data.project.name == "graphqlProject1" -// } -// -// def "Attempt the api as other role than admin"() { -// setup: -// Project project = new Project(projectId: "graphqlProject1", name: "graphqlProject1").save(failOnError: true, flush: true) -// -// when: -// def resp = graphqlRequest(""" -// query{ -// project(projectId:"graphqlProject1"){ -// name -// } -// }""", "test.user@ala.org.au") -// -// then: -// resp.statusCode.toString() == "401" -// resp.statusCode.name() == "UNAUTHORIZED" -// } -// -// def "Get project by project Id"() { -// setup: -// Project project = new Project(projectId: "graphqlProject1", name: "graphqlProject1").save(failOnError: true, flush: true) -// -// when: -// def resp = graphqlRequest(""" -// query{ -// project(projectId:"graphqlProject1"){ -// name -// } -// }""", "yasima.kankanamge@csiro.au") -// def result = resp.json -// -// then: -// result.data.project.name == "graphqlProject1" -// } -// -// def "Get project by project Id without mandatory fields"() { -// setup: -// Project project = new Project(projectId: "graphqlProject1", name: "graphqlProject1").save(failOnError: true, flush: true) -// -// when: -// def resp = graphqlRequest(""" -// query{ -// project{ -// name -// } -// }""", "yasima.kankanamge@csiro.au") -// def result = resp.json -// -// then: -// result.data == null -// result.errors[0].message == "Validation error of type MissingFieldArgument: Missing field argument projectId" -// } -// -// def "Get project by project Id returns only the requested data"() { -// setup: -// Project project = new Project(projectId: "graphqlProject1", name: "graphqlProject1").save(failOnError: true, flush: true) -// -// when: + def "Attempt the api as admin"() { + setup: + Project project = new Project(projectId: "graphqlProject1", name: "graphqlProject1").save(failOnError: true, flush: true) + + when: + def bodyContent = """ + query{ + project(projectId:"graphqlProject1"){ + name + } + }""" + graphqlController.request.contentType = 'application/graphql' + graphqlController.request.method = 'POST' + graphqlController.request.content = bodyContent.toString().getBytes('UTF-8') + // def resp = graphqlRequest(""" // query{ // project(projectId:"graphqlProject1"){ @@ -101,51 +52,119 @@ class GraphqlIntegrationSpec extends GraphqlSpecHelper implements GraphQLSpec{ // } // }""", "yasima.kankanamge@csiro.au") // def result = resp.json -// -// then: -// result.data.project.size() == 1 -// result.data.project.name == "graphqlProject1" -// } -// -// def "Get meriplan of a project"() { -// setup: -// Project project = new Project(projectId: "graphqlProject1", name: "graphqlProject1", custom: [details :[description:"test"]]).save(failOnError: true, flush: true) -// -// when: -// def resp = graphqlRequest(""" -// query{ -// project(projectId:"graphqlProject1"){ -// meriPlan -// } -// }""", "yasima.kankanamge@csiro.au") -// def result = resp.json -// -// then: -// result.data.project.meriPlan.details != null -// result.data.project.meriPlan.details.description == "test" -// } -// -// def "Get full activity detail list of a project"() { -// setup: -// Project project = new Project(projectId: "graphqlProject1", name: "graphqlProject1").save(failOnError: true, flush: true) -// Activity activity = new Activity(projectId: "graphqlProject1", activityId: "activity1", type: "Project Administration").save(failOnError: true, flush: true) -// -// when: -// def resp = graphqlRequest(""" -// query{ -// project(projectId:"graphqlProject1"){ -// activities { -// type -// } -// } -// }""", "yasima.kankanamge@csiro.au") -// def result = resp.json -// -// then: -// result.data.project.activities != null -// result.data.project.activities[0].type == activity.type -// } -// + + then: + def resp = graphqlController.index() + resp.data.project.name == "graphqlProject1" + } + + def "Get project by project Id"() { + setup: + Project project = new Project(projectId: "graphqlProject1", name: "graphqlProject1").save(failOnError: true, flush: true) + + when: + graphqlController.request.contentType = 'application/graphql' + graphqlController.request.method = 'POST' + def bodyContent = """ + query{ + project(projectId:"graphqlProject1"){ + name + } + }""" + graphqlController.request.content = bodyContent.toString().getBytes('UTF-8') + def result = graphqlController.index() + + then: + result.data.project.name == "graphqlProject1" + } + + def "Get project by project Id without mandatory fields"() { + setup: + Project project = new Project(projectId: "graphqlProject1", name: "graphqlProject1").save(failOnError: true, flush: true) + + when: + graphqlController.request.contentType = 'application/graphql' + graphqlController.request.method = 'POST' + def bodyContent = """ + query{ + project{ + name + } + }""" + graphqlController.request.content = bodyContent.toString().getBytes('UTF-8') + def result = graphqlController.index() + + then: + result.data == null + result.errors[0].message == "Validation error of type MissingFieldArgument: Missing field argument projectId" + } + + def "Get project by project Id returns only the requested data"() { + setup: + Project project = new Project(projectId: "graphqlProject1", name: "graphqlProject1").save(failOnError: true, flush: true) + + when: + graphqlController.request.contentType = 'application/graphql' + graphqlController.request.method = 'POST' + def bodyContent = """ + query{ + project(projectId:"graphqlProject1"){ + name + } + }""" + graphqlController.request.content = bodyContent.toString().getBytes('UTF-8') + def result = graphqlController.index() + + then: + result.data.project.size() == 1 + result.data.project.name == "graphqlProject1" + } + + def "Get meriplan of a project"() { + setup: + Project project = new Project(projectId: "graphqlProject1", name: "graphqlProject1", custom: [details :[description:"test"]]).save(failOnError: true, flush: true) + + when: + graphqlController.request.contentType = 'application/graphql' + graphqlController.request.method = 'POST' + def bodyContent = """ + query{ + project(projectId:"graphqlProject1"){ + meriPlan + } + }""" + graphqlController.request.content = bodyContent.toString().getBytes('UTF-8') + def result = graphqlController.index() + + then: + result.data.project.meriPlan.details != null + result.data.project.meriPlan.details.description == "test" + } + + def "Get full activity detail list of a project"() { + setup: + Project project = new Project(projectId: "graphqlProject1", name: "graphqlProject1").save(failOnError: true, flush: true) + Activity activity = new Activity(projectId: "graphqlProject1", activityId: "activity1", type: "Project Administration").save(failOnError: true, flush: true) + + when: + graphqlController.request.contentType = 'application/graphql' + graphqlController.request.method = 'POST' + def bodyContent = """ + query{ + project(projectId:"graphqlProject1"){ + activities { + type + } + } + }""" + graphqlController.request.content = bodyContent.toString().getBytes('UTF-8') + def result = graphqlController.index() + + then: + result.data.project.activities != null + result.data.project.activities[0].type == activity.type + } + // def "Get specific activity details of a project"() { // setup: // Project project = new Project(projectId: "graphqlProject1", name: "graphqlProject1").save(failOnError: true, flush: true) @@ -153,7 +172,9 @@ class GraphqlIntegrationSpec extends GraphqlSpecHelper implements GraphQLSpec{ // Output output = new Output(outputId: "output1", activityId: "activity1", name: "Administration Activities", data: [hoursAdminTotal:5]).save(failOnError: true, flush: true) // // when: -// def resp = graphqlRequest(""" +// graphqlController.request.contentType = 'application/graphql' +// graphqlController.request.method = 'POST' +// def bodyContent = """ // query{ // project(projectId:"graphqlProject1"){ // Activity_ProjectAdministration{ @@ -162,240 +183,301 @@ class GraphqlIntegrationSpec extends GraphqlSpecHelper implements GraphQLSpec{ // } // } // } -// }""", "yasima.kankanamge@csiro.au") -// def result = resp.json +// }""" +// graphqlController.request.content = bodyContent.toString().getBytes('UTF-8') +// def result = graphqlController.index() // // then: // result.data.project.Activity_ProjectAdministration != null // result.data.project.Activity_ProjectAdministration.OutputType_AdministrationActivities != null // } -// + // def "Get merit projects"() { // // when: -// def resp = graphqlRequest(""" +// graphqlController.request.contentType = 'application/graphql' +// graphqlController.request.method = 'POST' +// def bodyContent = """ // query{ // searchMeritProject{ // name // } -// }""", "yasima.kankanamge@csiro.au") -// def result = resp.json +// }""" +// graphqlController.request.content = bodyContent.toString().getBytes('UTF-8') +// def result = graphqlController.index() // // then: // result.data.searchMeritProject != null // } -// + // def "Get merit projects with facet filters"() { // // when: -// def resp = graphqlRequest(""" +// graphqlController.request.contentType = 'application/graphql' +// graphqlController.request.method = 'POST' +// def bodyContent = """ // query{ // searchMeritProject(organisation:"Test Org"){ // name // } -// }""", "yasima.kankanamge@csiro.au") -// def result = resp.json +// }""" +// graphqlController.request.content = bodyContent.toString().getBytes('UTF-8') +// def result = graphqlController.index() // // then: // result.data.searchMeritProject != null // } -// -// def "Get merit projects with invalid facet filters"() { -// -// when: -// def resp = graphqlRequest(""" -// query{ -// searchMeritProject(organisation:"test"){ -// name -// } -// }""", "yasima.kankanamge@csiro.au") -// def result = resp.json -// -// then: -// result.data.searchMeritProject == null -// result.errors[0].message == "Exception while fetching data (/searchMeritProject) : Invalid organisationFacet : suggested values are : [, Test Org]" -// } -// -// def "Get merit projects with activity filters"() { -// -// when: -// def resp = graphqlRequest(""" -// query{ -// searchMeritProject( activities:[{activityType:"Project Administration"}]){ -// name -// } -// }""", "yasima.kankanamge@csiro.au") -// def result = resp.json -// -// then: -// result.data != null -// } -// -// + + def "Get merit projects with invalid facet filters"() { + + when: + graphqlController.request.contentType = 'application/graphql' + graphqlController.request.method = 'POST' + def bodyContent = """ + query{ + searchMeritProject(organisation:"test"){ + name + } + }""" + graphqlController.request.content = bodyContent.toString().getBytes('UTF-8') + def result = graphqlController.index() + + then: + result.data.searchMeritProject == null + result.errors[0].message == "Exception while fetching data (/searchMeritProject) : Invalid organisationFacet : suggested values are : [, Test Org]" + } + + def "Get merit projects with activity filters"() { + + when: + graphqlController.request.contentType = 'application/graphql' + graphqlController.request.method = 'POST' + def bodyContent = """ + query{ + searchMeritProject( activities:[{activityType:"Project Administration"}]){ + name + } + }""" + graphqlController.request.content = bodyContent.toString().getBytes('UTF-8') + def result = graphqlController.index() + + then: + result.data != null + } + + // def "Get biocollect projects based on hub"() { // // when: -// def resp = graphqlRequest(""" +// graphqlController.request.contentType = 'application/graphql' +// graphqlController.request.method = 'POST' +// def bodyContent = """ // query{ // searchBioCollectProject(hub:"ala"){ // projectId // name // } -// }""", "yasima.kankanamge@csiro.au") -// def result = resp.json +// }""" +// graphqlController.request.content = bodyContent.toString().getBytes('UTF-8') +// def result = graphqlController.index() // // then: // result.data.searchBioCollectProject != null // } -// -// def "Get biocollect projects without hub specified"() { -// -// when: -// def resp = graphqlRequest(""" -// query{ -// searchBioCollectProject{ -// projectId -// name -// } -// }""", "yasima.kankanamge@csiro.au") -// def result = resp.json -// -// then: -// result.data == null -// result.errors[0].message == "Validation error of type MissingFieldArgument: Missing field argument hub" -// } -// + + def "Get biocollect projects without hub specified"() { + + when: + graphqlController.request.contentType = 'application/graphql' + graphqlController.request.method = 'POST' + def bodyContent = """ + query{ + searchBioCollectProject{ + projectId + name + } + }""" + graphqlController.request.content = bodyContent.toString().getBytes('UTF-8') + def result = graphqlController.index() + + then: + result.data == null + result.errors[0].message == "Validation error of type MissingFieldArgument: Missing field argument hub" + } + // def "Get biocollect projects with an invalid hub"() { // // when: -// def resp = graphqlRequest(""" +// graphqlController.request.contentType = 'application/graphql' +// graphqlController.request.method = 'POST' +// def bodyContent = """ // query{ // searchBioCollectProject(hub:"test"){ // projectId // name // } -// }""", "yasima.kankanamge@csiro.au") -// def result = resp.json +// }""" +// graphqlController.request.content = bodyContent.toString().getBytes('UTF-8') +// def result = graphqlController.index() // // then: // result.data.searchBioCollectProject == null // result.errors[0].message == "Exception while fetching data (/searchBioCollectProject) : Invalid hub, suggested values are : [ala]" // } -// + // def "Get biocollect projects with invalid facet filters"() { // // when: -// def resp = graphqlRequest(""" +// graphqlController.request.contentType = 'application/graphql' +// graphqlController.request.method = 'POST' +// def bodyContent = """ // query{ // searchBioCollectProject(hub:"ala", organisation:"test"){ // name // } -// }""", "yasima.kankanamge@csiro.au") -// def result = resp.json +// }""" +// graphqlController.request.content = bodyContent.toString().getBytes('UTF-8') +// def result = graphqlController.index() // // then: // result.data.searchBioCollectProject == null // result.errors[0].message == "Exception while fetching data (/searchBioCollectProject) : Invalid organisationFacet : suggested values are : []" // } -// -// def "Get activity output dashboard data"() { -// when: -// def resp = graphqlRequest(""" -// query{ -// activityOutput { -// outputData { -// category -// outputType -// result { -// label -// result -// resultList -// groups { -// group -// results { -// count -// result -// } -// } -// } -// } -// } -// }""", "yasima.kankanamge@csiro.au") -// def result = resp.json -// -// then: -// result.data.activityOutput.outputData != null -// } -// -// def "Get activity output dashboard data of a specific activity type"() { -// when: -// def resp = graphqlRequest(""" -// query{ -// activityOutput(activityOutputs: [{category: "Community Engagement and Capacity Building"}]) { -// outputData { -// category -// outputType -// result { -// label -// result -// resultList -// groups { -// group -// results { -// count -// result -// } -// } -// } -// } -// } -// }""", "yasima.kankanamge@csiro.au") -// def result = resp.json -// -// then: -// result.data != null -// } -// -// def "Get output targets"() { -// when: -// def resp = graphqlRequest(""" -// { -// outputTargetsByProgram { -// targets { -// program -// outputTargetMeasure { -// outputTarget -// count -// total -// } -// } -// } -// }""", "yasima.kankanamge@csiro.au") -// def result = resp.json -// -// then: -// result.data.outputTargetsByProgram.targets != null -// } -// -// def "Get output targets of a specific program"() { -// when: -// def resp = graphqlRequest(""" -// { -// outputTargetsByProgram(programs: ["Reef Trust - Reef Trust Phase 1 Investment", "Reef Trust - Reef Trust Phase 5 Investment"], outputTargetMeasures: ["Tonnes per year of fine suspended sediment prevented from reaching the Great Barrier Reef Lagoon approved"]) { -// targets { -// program -// outputTargetMeasure { -// outputTarget -// count -// total -// } -// } -// } -// }""", "yasima.kankanamge@csiro.au") -// def result = resp.json -// -// then: -// result.data.outputTargetsByProgram.targets != null -// } + + def "Get activity output dashboard data"() { + when: + graphqlController.request.contentType = 'application/graphql' + graphqlController.request.method = 'POST' + def bodyContent = """ + query{ + activityOutput { + outputData { + category + outputType + result { + label + result + resultList + groups { + group + results { + count + result + } + } + } + } + } + }""" + graphqlController.request.content = bodyContent.toString().getBytes('UTF-8') + def result = graphqlController.index() + + then: + result.data.activityOutput.outputData != null + } + + def "Get activity output dashboard data of a specific activity type"() { + when: + graphqlController.request.contentType = 'application/graphql' + graphqlController.request.method = 'POST' + def bodyContent = """ + query{ + activityOutput(activityOutputs: [{category: "Community Engagement and Capacity Building"}]) { + outputData { + category + outputType + result { + label + result + resultList + groups { + group + results { + count + result + } + } + } + } + } + }""" + graphqlController.request.content = bodyContent.toString().getBytes('UTF-8') + def result = graphqlController.index() + + then: + result.data != null + } + + def "Get output targets"() { + when: + graphqlController.request.contentType = 'application/graphql' + graphqlController.request.method = 'POST' + def bodyContent = """ + { + outputTargetsByProgram { + targets { + program + outputTargetMeasure { + outputTarget + count + total + } + } + } + }""" + graphqlController.request.content = bodyContent.toString().getBytes('UTF-8') + def result = graphqlController.index() + + then: + result.data.outputTargetsByProgram.targets != null + } + + def "Get output targets of a specific program"() { + when: + graphqlController.request.contentType = 'application/graphql' + graphqlController.request.method = 'POST' + def bodyContent = """ + { + outputTargetsByProgram(programs: ["Reef Trust - Reef Trust Phase 1 Investment", "Reef Trust - Reef Trust Phase 5 Investment"], outputTargetMeasures: ["Tonnes per year of fine suspended sediment prevented from reaching the Great Barrier Reef Lagoon approved"]) { + targets { + program + outputTargetMeasure { + outputTarget + count + total + } + } + } + }""" + graphqlController.request.content = bodyContent.toString().getBytes('UTF-8') + def result = graphqlController.index() + + then: + result.data.outputTargetsByProgram.targets != null + } + + def "Get management unit details"() { + setup: + ManagementUnit mu = new ManagementUnit(managementUnitId: "mu1", name: "mu1").save(failOnError: true, flush: true) + + when: + graphqlController.request.contentType = 'application/graphql' + graphqlController.request.method = 'POST' + def bodyContent = """ + query{ + searchManagementUnits{ + managementUnitId + } + }""" + graphqlController.request.content = bodyContent.toString().getBytes('UTF-8') + def result = graphqlController.index() + + then: + result.data.searchManagementUnits != null + result.data.searchManagementUnits.size() == 1 + result.data.searchManagementUnits[0].size() == 1 + result.data.searchManagementUnits[0].managementUnitId == mu.managementUnitId + } + } diff --git a/src/integration-test/groovy/au/org/ala/ecodata/GraphqlSpecHelper.groovy b/src/integration-test/groovy/au/org/ala/ecodata/GraphqlSpecHelper.groovy index 817265fdb..04c124960 100644 --- a/src/integration-test/groovy/au/org/ala/ecodata/GraphqlSpecHelper.groovy +++ b/src/integration-test/groovy/au/org/ala/ecodata/GraphqlSpecHelper.groovy @@ -1,17 +1,55 @@ package au.org.ala.ecodata -import org.grails.gorm.graphql.plugin.testing.GraphQLSpec +import grails.util.GrailsWebMockUtil +import groovy.transform.CompileStatic +import org.grails.plugins.testing.GrailsMockHttpServletRequest +import org.grails.plugins.testing.GrailsMockHttpServletResponse +import org.grails.web.servlet.mvc.GrailsWebRequest +import org.junit.Ignore +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.beans.factory.config.AutowireCapableBeanFactory +import org.springframework.mock.web.MockHttpServletRequest +import org.springframework.mock.web.MockHttpServletResponse +import org.springframework.web.context.WebApplicationContext +import org.springframework.web.context.request.RequestContextHolder import spock.lang.Specification -class GraphqlSpecHelper extends Specification implements GraphQLSpec{ +@CompileStatic +abstract class GraphqlSpecHelper extends Specification { - def grailsApplication + @Autowired + WebApplicationContext ctx - def graphqlRequest(String requestBody, String userName){ - return rest.post("http://${grailsApplication.config.grails.hostname}:${serverPort}/graphql/index") { - contentType("application/graphql") - header("Cookie", "ALA-Auth=" + userName); - body(requestBody) - } + void setup() { + MockHttpServletRequest request = new GrailsMockHttpServletRequest(ctx.servletContext) + MockHttpServletResponse response = new GrailsMockHttpServletResponse() + GrailsWebMockUtil.bindMockWebRequest(ctx, request, response) + currentRequestAttributes.setControllerName(controllerName) + } + + @Ignore + abstract String getControllerName() + + @Ignore + protected GrailsWebRequest getCurrentRequestAttributes() { + return (GrailsWebRequest) RequestContextHolder.currentRequestAttributes() + } + + void cleanup() { + RequestContextHolder.resetRequestAttributes() + } + + @Ignore + def autowire(Class clazz) { + def bean = clazz.newInstance() + ctx.autowireCapableBeanFactory.autowireBeanProperties(bean, AutowireCapableBeanFactory.AUTOWIRE_BY_NAME, false) + bean + } + + + @Ignore + def autowire(def bean) { + ctx.autowireCapableBeanFactory.autowireBeanProperties(bean, AutowireCapableBeanFactory.AUTOWIRE_BY_NAME, false) + bean } } \ No newline at end of file From 895a1ef1f3827a2efc8889e5f86ddf340d6c365e Mon Sep 17 00:00:00 2001 From: yasima-csiro Date: Wed, 17 Mar 2021 08:18:36 +1100 Subject: [PATCH 017/144] Add field description --- .../ala/ecodata/GraphqlIntegrationSpec.groovy | 28 ------------------ .../ecodata/graphql/fetchers/Helper.groovy | 3 ++ .../mappers/ProjectGraphQLMapper.groovy | 29 +++++++++++++++---- 3 files changed, 26 insertions(+), 34 deletions(-) diff --git a/src/integration-test/groovy/au/org/ala/ecodata/GraphqlIntegrationSpec.groovy b/src/integration-test/groovy/au/org/ala/ecodata/GraphqlIntegrationSpec.groovy index a7ff886fd..2508a2149 100644 --- a/src/integration-test/groovy/au/org/ala/ecodata/GraphqlIntegrationSpec.groovy +++ b/src/integration-test/groovy/au/org/ala/ecodata/GraphqlIntegrationSpec.groovy @@ -30,34 +30,6 @@ class GraphqlIntegrationSpec extends GraphqlSpecHelper{ ManagementUnit.findAll().each { it.delete(flush:true) } } - def "Attempt the api as admin"() { - setup: - Project project = new Project(projectId: "graphqlProject1", name: "graphqlProject1").save(failOnError: true, flush: true) - - when: - def bodyContent = """ - query{ - project(projectId:"graphqlProject1"){ - name - } - }""" - graphqlController.request.contentType = 'application/graphql' - graphqlController.request.method = 'POST' - graphqlController.request.content = bodyContent.toString().getBytes('UTF-8') - -// def resp = graphqlRequest(""" -// query{ -// project(projectId:"graphqlProject1"){ -// name -// } -// }""", "yasima.kankanamge@csiro.au") -// def result = resp.json - - then: - def resp = graphqlController.index() - resp.data.project.name == "graphqlProject1" - } - def "Get project by project Id"() { setup: Project project = new Project(projectId: "graphqlProject1", name: "graphqlProject1").save(failOnError: true, flush: true) diff --git a/src/main/groovy/au/org/ala/ecodata/graphql/fetchers/Helper.groovy b/src/main/groovy/au/org/ala/ecodata/graphql/fetchers/Helper.groovy index 1d5cb87c5..cd1994ea8 100644 --- a/src/main/groovy/au/org/ala/ecodata/graphql/fetchers/Helper.groovy +++ b/src/main/groovy/au/org/ala/ecodata/graphql/fetchers/Helper.groovy @@ -5,6 +5,7 @@ import au.org.ala.ecodata.FormSection import au.org.ala.ecodata.MetadataService import au.org.ala.ecodata.PublicationStatus import au.org.ala.ecodata.Status +import grails.util.Holders import graphql.GraphQLException class Helper { @@ -85,12 +86,14 @@ class Helper { ActivityForm.findAllWhereStatusNotEqualAndPublicationStatusEquals(Status.DELETED, PublicationStatus.PUBLISHED).each { ActivityForm activityForm -> Map activityModel = [ name: activityForm.name, + description: Holders.applicationContext.messageSource.getMessage("api.${activityForm.name}.description", null, "", Locale.default), outputs: [] ] activityForm.sections.unique().each { FormSection section -> activityModel.outputs << [ name: section.name, + title: section.title, fields: section.template.dataModel != null ? section.template.dataModel : [] ] } diff --git a/src/main/groovy/au/org/ala/ecodata/graphql/mappers/ProjectGraphQLMapper.groovy b/src/main/groovy/au/org/ala/ecodata/graphql/mappers/ProjectGraphQLMapper.groovy index e7b4acd4b..3342f28ef 100644 --- a/src/main/groovy/au/org/ala/ecodata/graphql/mappers/ProjectGraphQLMapper.groovy +++ b/src/main/groovy/au/org/ala/ecodata/graphql/mappers/ProjectGraphQLMapper.groovy @@ -84,6 +84,20 @@ class ProjectGraphQLMapper { } } + add('test', 'test') { + type { + field("test1", "test1") { + field("test2", String) {description "test2"} + description "test1" + } + } + description "test" + dataFetcher { Project project -> + new ActivityFetcher(Holders.applicationContext.elasticSearchService, Holders.applicationContext.permissionService, Holders.applicationContext.metadataService, + Holders.applicationContext.messageSource, Holders.grailsApplication).getFilteredActivities(project.tempArgs, project.projectId) + } + } + //add graphql type for each activity type activityModel["activities"].each { if(it.name && it.outputs && it.outputs.size() > 0 && it.outputs.fields?.findAll{ x -> x?.size() != 0 }?.size() > 0){ @@ -92,6 +106,7 @@ class ProjectGraphQLMapper { String name = "Activity_" + activityName List outputList = [] List modifiedColumns = [] + String desc = it.description //define activity type add(name, name) { type { @@ -109,10 +124,10 @@ class ProjectGraphQLMapper { for(int t=0; t b.name == fieldList[t]} if(outputField.dataType == "number") { - field(fieldList[t].toString(), double) + field(fieldList[t].toString(), double) {description "test1"} } else if(outputField.dataType == "text") { - field(fieldList[t].toString(), String) + field(fieldList[t].toString(), String) {description "test1"} } else if(outputField.dataType == "list") { modifiedColumns << fieldList[t].toString() @@ -120,25 +135,27 @@ class ProjectGraphQLMapper { String[] columnList = outputField.columns.name.unique() for(int y=0; y b.name == columnList[y]} - field(column.name.toString(), column.dataType == "number" ? double : String) + field(column.name.toString(), column.dataType == "number" ? double : String) {description "test2"} } collection true + description "test1" } } else { - field(fieldList[t].toString(), String) + field(fieldList[t].toString(), String) {description "test1"} } } nullable true collection true + description "test0" //outputType.title?.toString() } } } } + description desc dataFetcher { Project project -> - def s=new ActivityFetcher(Holders.applicationContext.elasticSearchService, Holders.applicationContext.permissionService, Holders.applicationContext.metadataService, + return new ActivityFetcher(Holders.applicationContext.elasticSearchService, Holders.applicationContext.permissionService, Holders.applicationContext.metadataService, Holders.applicationContext.messageSource, Holders.grailsApplication).getActivityData(project.tempArgs, project.projectId, activityName, outputList, modifiedColumns) - return s } } } From 0a7f187fe154c7aefcdce7f8ba4aaf0d3a43c279 Mon Sep 17 00:00:00 2001 From: yasima-csiro Date: Thu, 18 Mar 2021 14:58:21 +1100 Subject: [PATCH 018/144] Add field description --- .../ala/ecodata/GraphqlIntegrationSpec.groovy | 8 ++--- .../ecodata/graphql/fetchers/Helper.groovy | 18 ++++++++++-- .../mappers/ProjectGraphQLMapper.groovy | 29 +++++-------------- 3 files changed, 27 insertions(+), 28 deletions(-) diff --git a/src/integration-test/groovy/au/org/ala/ecodata/GraphqlIntegrationSpec.groovy b/src/integration-test/groovy/au/org/ala/ecodata/GraphqlIntegrationSpec.groovy index 2508a2149..28d103299 100644 --- a/src/integration-test/groovy/au/org/ala/ecodata/GraphqlIntegrationSpec.groovy +++ b/src/integration-test/groovy/au/org/ala/ecodata/GraphqlIntegrationSpec.groovy @@ -24,10 +24,10 @@ class GraphqlIntegrationSpec extends GraphqlSpecHelper{ } def cleanup() { - Project.findAll().each { it.delete(flush:true) } - Activity.findAll().each { it.delete(flush:true) } - Output.findAll().each { it.delete(flush:true) } - ManagementUnit.findAll().each { it.delete(flush:true) } + Project.findAll{projectId == "graphqlProject1"}.each { it.delete(flush:true) } +// Activity.findAll().each { it.delete(flush:true) } +// Output.findAll().each { it.delete(flush:true) } +// ManagementUnit.findAll().each { it.delete(flush:true) } } def "Get project by project Id"() { diff --git a/src/main/groovy/au/org/ala/ecodata/graphql/fetchers/Helper.groovy b/src/main/groovy/au/org/ala/ecodata/graphql/fetchers/Helper.groovy index cd1994ea8..55d40d94a 100644 --- a/src/main/groovy/au/org/ala/ecodata/graphql/fetchers/Helper.groovy +++ b/src/main/groovy/au/org/ala/ecodata/graphql/fetchers/Helper.groovy @@ -5,6 +5,7 @@ import au.org.ala.ecodata.FormSection import au.org.ala.ecodata.MetadataService import au.org.ala.ecodata.PublicationStatus import au.org.ala.ecodata.Status +import au.org.ala.ecodata.metadata.OutputMetadata import grails.util.Holders import graphql.GraphQLException @@ -86,15 +87,26 @@ class Helper { ActivityForm.findAllWhereStatusNotEqualAndPublicationStatusEquals(Status.DELETED, PublicationStatus.PUBLISHED).each { ActivityForm activityForm -> Map activityModel = [ name: activityForm.name, - description: Holders.applicationContext.messageSource.getMessage("api.${activityForm.name}.description", null, "", Locale.default), outputs: [] ] activityForm.sections.unique().each { FormSection section -> + def fields = section.template.dataModel != null ? section.template.dataModel : [] + //set field labels + OutputMetadata outputModel = new OutputMetadata(section.template) + println(activityForm.name + " " + section.name) + outputModel.modelIterator { String path, Map viewNode, Map dataNode -> + def field = fields.find { b -> b == dataNode } + if(field) { + def label = outputModel.getLabel(viewNode, dataNode) + field.label = label + } + } + activityModel.outputs << [ name: section.name, - title: section.title, - fields: section.template.dataModel != null ? section.template.dataModel : [] + title: section.title ?: section.template.modelName, + fields: fields ] } diff --git a/src/main/groovy/au/org/ala/ecodata/graphql/mappers/ProjectGraphQLMapper.groovy b/src/main/groovy/au/org/ala/ecodata/graphql/mappers/ProjectGraphQLMapper.groovy index 3342f28ef..0f71c1eee 100644 --- a/src/main/groovy/au/org/ala/ecodata/graphql/mappers/ProjectGraphQLMapper.groovy +++ b/src/main/groovy/au/org/ala/ecodata/graphql/mappers/ProjectGraphQLMapper.groovy @@ -84,20 +84,6 @@ class ProjectGraphQLMapper { } } - add('test', 'test') { - type { - field("test1", "test1") { - field("test2", String) {description "test2"} - description "test1" - } - } - description "test" - dataFetcher { Project project -> - new ActivityFetcher(Holders.applicationContext.elasticSearchService, Holders.applicationContext.permissionService, Holders.applicationContext.metadataService, - Holders.applicationContext.messageSource, Holders.grailsApplication).getFilteredActivities(project.tempArgs, project.projectId) - } - } - //add graphql type for each activity type activityModel["activities"].each { if(it.name && it.outputs && it.outputs.size() > 0 && it.outputs.fields?.findAll{ x -> x?.size() != 0 }?.size() > 0){ @@ -106,12 +92,13 @@ class ProjectGraphQLMapper { String name = "Activity_" + activityName List outputList = [] List modifiedColumns = [] - String desc = it.description + String desc = it.name //define activity type add(name, name) { type { outputTypes.each { outputType -> String outputName = WordUtils.capitalize(outputType.name).replaceAll("\\W", "") + String title = outputType.title String outputTypeName = "OutputType_" + outputName if(outputType.fields?.size() > 0 && !(outputTypeName in outputList)) { if(duplicateOutputs.contains(outputType.name)){ @@ -124,10 +111,10 @@ class ProjectGraphQLMapper { for(int t=0; t b.name == fieldList[t]} if(outputField.dataType == "number") { - field(fieldList[t].toString(), double) {description "test1"} + field(fieldList[t].toString(), double) {description outputField.label} } else if(outputField.dataType == "text") { - field(fieldList[t].toString(), String) {description "test1"} + field(fieldList[t].toString(), String) {description outputField.label} } else if(outputField.dataType == "list") { modifiedColumns << fieldList[t].toString() @@ -135,19 +122,19 @@ class ProjectGraphQLMapper { String[] columnList = outputField.columns.name.unique() for(int y=0; y b.name == columnList[y]} - field(column.name.toString(), column.dataType == "number" ? double : String) {description "test2"} + field(column.name.toString(), column.dataType == "number" ? double : String) {description column.description} } collection true - description "test1" + description outputField.label } } else { - field(fieldList[t].toString(), String) {description "test1"} + field(fieldList[t].toString(), String) {description outputField.label} } } nullable true collection true - description "test0" //outputType.title?.toString() + description title } } } From 99d700cfe2f79a12934e4d98bf354c84358775e1 Mon Sep 17 00:00:00 2001 From: yasima-csiro Date: Wed, 24 Mar 2021 11:37:10 +1100 Subject: [PATCH 019/144] Add site APIs --- .../domain/au/org/ala/ecodata/Site.groovy | 34 +------- .../ala/ecodata/GraphqlIntegrationSpec.groovy | 55 +++++++++++- .../ecodata/graphql/fetchers/Helper.groovy | 1 - .../graphql/fetchers/ProjectsFetcher.groovy | 4 +- .../graphql/fetchers/SitesFetcher.groovy | 83 +++++++++++++++++-- .../graphql/mappers/SiteGraphQLMapper.groovy | 76 +++++++++++++++++ 6 files changed, 212 insertions(+), 41 deletions(-) create mode 100644 src/main/groovy/au/org/ala/ecodata/graphql/mappers/SiteGraphQLMapper.groovy diff --git a/grails-app/domain/au/org/ala/ecodata/Site.groovy b/grails-app/domain/au/org/ala/ecodata/Site.groovy index 3a416cc68..81ac8f474 100644 --- a/grails-app/domain/au/org/ala/ecodata/Site.groovy +++ b/grails-app/domain/au/org/ala/ecodata/Site.groovy @@ -1,5 +1,6 @@ package au.org.ala.ecodata +import au.org.ala.ecodata.graphql.mappers.SiteGraphQLMapper import com.vividsolutions.jts.geom.Geometry import com.vividsolutions.jts.operation.valid.IsValidOp import com.vividsolutions.jts.operation.valid.TopologyValidationError @@ -16,38 +17,7 @@ class Site { static String TYPE_PROJECT_AREA = 'projectArea' static String TYPE_WORKS_AREA = 'worksArea' - static graphql = GraphQLMapping.lazy { - // Disable default operations, including get as we only want to expose UUIDs in the API not internal ones - operations.get.enabled false - operations.list.enabled true - operations.count.enabled false - operations.create.enabled false - operations.update.enabled false - operations.delete.enabled false - - exclude 'extent', 'features', 'projects' - - add('geometry', 'Geometry') { - type { - field('type', String) - field('coordinates', [Object]) - } - dataFetcher { Site site -> - site.extent.geometry - } - } - - query('sites', [Site]) { - argument('term', String) - dataFetcher(new DataFetcher() { - @Override - Object get(DataFetchingEnvironment environment) throws Exception { - environment.context.grailsApplication.mainContext.sitesFetcher.get(environment) - } - }) - } - - } + static graphql = SiteGraphQLMapper.graphqlMapping() def siteService diff --git a/src/integration-test/groovy/au/org/ala/ecodata/GraphqlIntegrationSpec.groovy b/src/integration-test/groovy/au/org/ala/ecodata/GraphqlIntegrationSpec.groovy index 28d103299..e5fa18fd5 100644 --- a/src/integration-test/groovy/au/org/ala/ecodata/GraphqlIntegrationSpec.groovy +++ b/src/integration-test/groovy/au/org/ala/ecodata/GraphqlIntegrationSpec.groovy @@ -27,7 +27,8 @@ class GraphqlIntegrationSpec extends GraphqlSpecHelper{ Project.findAll{projectId == "graphqlProject1"}.each { it.delete(flush:true) } // Activity.findAll().each { it.delete(flush:true) } // Output.findAll().each { it.delete(flush:true) } -// ManagementUnit.findAll().each { it.delete(flush:true) } + ManagementUnit.findAll{managementUnitId == "mu1"}.each { it.delete(flush:true) } + Site.findAll().each { it.delete(flush:true) } } def "Get project by project Id"() { @@ -451,5 +452,57 @@ class GraphqlIntegrationSpec extends GraphqlSpecHelper{ result.data.searchManagementUnits[0].managementUnitId == mu.managementUnitId } + def "Get site details"() { + setup: + Site site = new Site(siteId: "site1", name: "site1").save(failOnError: true, flush: true) + + when: + graphqlController.request.contentType = 'application/graphql' + graphqlController.request.method = 'POST' + def bodyContent = """ + query{ + sites(term:"*.*", siteIds:"site1"){ + siteId + name + } + }""" + graphqlController.request.content = bodyContent.toString().getBytes('UTF-8') + def result = graphqlController.index() + then: + result.data.sites != null + result.data.sites.size() == 1 + result.data.sites[0].size() == 2 + result.data.sites[0].siteId == site.siteId + result.data.sites[0].name == site.name + } + + def "Get site geojson details"() { + setup: + Site site = new Site(siteId: "site1", name: "site1", extent: [geometry:[type:"Point", coordinates:["138.343", "29.688"]]] ).save(failOnError: true, flush: true) + + when: + graphqlController.request.contentType = 'application/graphql' + graphqlController.request.method = 'POST' + def bodyContent = """ + query{ + sites(term:"*.*", siteIds:"site1"){ + siteId + siteGeojson{ + type + geometry{ + type + coordinates + } + } + } + }""" + graphqlController.request.content = bodyContent.toString().getBytes('UTF-8') + def result = graphqlController.index() + + then: + result.data.sites != null + result.data.sites.size() == 1 + result.data.sites[0].siteId == site.siteId + } } diff --git a/src/main/groovy/au/org/ala/ecodata/graphql/fetchers/Helper.groovy b/src/main/groovy/au/org/ala/ecodata/graphql/fetchers/Helper.groovy index 55d40d94a..30efc3a76 100644 --- a/src/main/groovy/au/org/ala/ecodata/graphql/fetchers/Helper.groovy +++ b/src/main/groovy/au/org/ala/ecodata/graphql/fetchers/Helper.groovy @@ -94,7 +94,6 @@ class Helper { def fields = section.template.dataModel != null ? section.template.dataModel : [] //set field labels OutputMetadata outputModel = new OutputMetadata(section.template) - println(activityForm.name + " " + section.name) outputModel.modelIterator { String path, Map viewNode, Map dataNode -> def field = fields.find { b -> b == dataNode } if(field) { diff --git a/src/main/groovy/au/org/ala/ecodata/graphql/fetchers/ProjectsFetcher.groovy b/src/main/groovy/au/org/ala/ecodata/graphql/fetchers/ProjectsFetcher.groovy index 2e06439ed..92d947447 100644 --- a/src/main/groovy/au/org/ala/ecodata/graphql/fetchers/ProjectsFetcher.groovy +++ b/src/main/groovy/au/org/ala/ecodata/graphql/fetchers/ProjectsFetcher.groovy @@ -70,7 +70,7 @@ class ProjectsFetcher implements graphql.schema.DataFetcher> { // add pagination results. String userId = environment.context.user?.userId ?: '1493' String query = queryString ?:"*:*" - Boolean myProjects = environment.arguments.get("myProjects") != null ? environment.arguments.get("myProjects") : true + Boolean myProjects = environment.arguments.get("myProjects") != null ? environment.arguments.get("myProjects") : false SearchResponse searchResponse if(myProjects) { @@ -106,7 +106,7 @@ class ProjectsFetcher implements graphql.schema.DataFetcher> { Map publicProjects = [:] FindIterable findIterable = Project.find(Filters.in("projectId", publicProjectIds)) - findIterable.projection(publicView).each { Project project -> + findIterable.each { Project project -> publicProjects.put(project.projectId, project) } diff --git a/src/main/groovy/au/org/ala/ecodata/graphql/fetchers/SitesFetcher.groovy b/src/main/groovy/au/org/ala/ecodata/graphql/fetchers/SitesFetcher.groovy index 9ec6c1993..9545244a6 100644 --- a/src/main/groovy/au/org/ala/ecodata/graphql/fetchers/SitesFetcher.groovy +++ b/src/main/groovy/au/org/ala/ecodata/graphql/fetchers/SitesFetcher.groovy @@ -1,23 +1,35 @@ package au.org.ala.ecodata.graphql.fetchers import au.org.ala.ecodata.* +import au.org.ala.ecodata.reporting.ShapefileBuilder +import grails.core.GrailsApplication +import graphql.GraphQLException import graphql.schema.DataFetchingEnvironment import org.elasticsearch.action.search.SearchResponse +import org.springframework.beans.factory.annotation.Autowired + +import java.util.zip.ZipEntry +import java.util.zip.ZipOutputStream class SitesFetcher implements graphql.schema.DataFetcher> { - public SitesFetcher(ProjectService projectService, ElasticSearchService elasticSearchService, PermissionService permissionService) { + public SitesFetcher(ProjectService projectService, ElasticSearchService elasticSearchService, PermissionService permissionService, + SiteService siteService) { this.projectService = projectService this.elasticSearchService = elasticSearchService this.permissionService = permissionService + this.siteService = siteService } PermissionService permissionService ElasticSearchService elasticSearchService - + SiteService siteService ProjectService projectService + @Autowired + GrailsApplication grailsApplication + @Override List get(DataFetchingEnvironment environment) throws Exception { @@ -39,8 +51,12 @@ class SitesFetcher implements graphql.schema.DataFetcher> { // Or do we do two queries, one for full resolution, one for the rest (how do we sort/page if we do two queries?) - - return queryElasticSearch(environment) + if(environment.arguments.get("siteIds")) { + return Site.findAllBySiteIdInList(environment.arguments.get("siteIds") as List) + } + else { + return queryElasticSearch(environment) + } } private List queryElasticSearch(DataFetchingEnvironment environment) { @@ -70,6 +86,63 @@ class SitesFetcher implements graphql.schema.DataFetcher> { List projectIds = searchResponse.hits.hits.collect{it.source.projectId} - Site.findAllByProjectsInList(projectIds, [max:10]) + int max = environment.arguments.get("max") ?: 10 + int page = environment.arguments.get("page") ?: 1 + int offset = (page-1)*max + + Site.findAllByProjectsInList(projectIds, [max:max, offset:offset]) + } + + Map getSitesAsGeojson(String siteId) { + def site = siteService.get(siteId, siteService.BRIEF, null) + return siteService.toGeoJson(site) + } + + String getSiteShapeFileUrl(String siteId) { + Closure doDownload = {OutputStream outputStream, String id -> downloadSiteShapeFiles(outputStream, id)} + return createSiteShapeFiles(siteId, doDownload) + } + + boolean downloadSiteShapeFiles(OutputStream outputStream, String siteId) { + new ZipOutputStream(outputStream).withStream { zip -> + try{ + zip.putNextEntry(new ZipEntry("shapefiles/")) + Site site = Site.findBySiteId(siteId) + if (site) { + zip.putNextEntry(new ZipEntry("shapefiles/${site.name}.zip")) + ShapefileBuilder builder = new ShapefileBuilder(projectService, siteService) + builder.setName(site.name) + builder.addSite(site.siteId) + builder.writeShapefile(zip) + } + } catch (Exception e){ + throw new GraphQLException("Error creating download archive" + e) + } finally { + zip.finish() + zip.flush() + zip.close() + } + } + true + } + + String createSiteShapeFiles(String siteId, Closure downloadAction) { + String downloadId = UUID.randomUUID().toString() + File directoryPath = new File("${grailsApplication.config.temp.dir}") + directoryPath.mkdirs() + String fileExtension = 'zip' + FileOutputStream outputStream = new FileOutputStream(new File(directoryPath, "${downloadId}.${fileExtension}")) + + Site.withNewSession { + downloadAction(outputStream, siteId) + } + + String urlPrefix = "${grailsApplication.config.grails.serverURL}/download/get/" + String url = "${urlPrefix}${downloadId}?fileExtension=${fileExtension}" + if (outputStream) { + outputStream.flush() + outputStream.close() + } + return url } } diff --git a/src/main/groovy/au/org/ala/ecodata/graphql/mappers/SiteGraphQLMapper.groovy b/src/main/groovy/au/org/ala/ecodata/graphql/mappers/SiteGraphQLMapper.groovy new file mode 100644 index 000000000..df64e495b --- /dev/null +++ b/src/main/groovy/au/org/ala/ecodata/graphql/mappers/SiteGraphQLMapper.groovy @@ -0,0 +1,76 @@ +package au.org.ala.ecodata.graphql.mappers + +import au.org.ala.ecodata.Site +import graphql.schema.DataFetcher +import graphql.schema.DataFetchingEnvironment +import org.grails.gorm.graphql.entity.dsl.GraphQLMapping + +class SiteGraphQLMapper { + + static graphqlMapping() { + GraphQLMapping.lazy { + // Disable default operations, including get as we only want to expose UUIDs in the API not internal ones + operations.get.enabled false + operations.list.enabled true + operations.count.enabled false + operations.create.enabled false + operations.update.enabled false + operations.delete.enabled false + + exclude 'extent', 'features', 'projects' + + add('siteGeojson', 'siteGeojson') { + type { + field('type', String) + field('properties', 'properties') { + field('id', String) + field('name', String) + field('type', String) + field('notes', [String]) + } + field('features', 'features') { + field('type', String) + field('geometry', 'featureGeometry'){ + field('type', String) + field('coordinates', [String]) + } + field('properties', 'featureProperties'){ + field('id', String) + } + collection true + } + field("geometry", "geometry"){ + field('type', String) + field('coordinates', [String]) + } + } + dataFetcher { Site site, DataFetchingEnvironment environment -> + environment.context.grailsApplication.mainContext.sitesFetcher.getSitesAsGeojson(site.siteId) + } + } + + add('siteShapefiles', "siteShapefiles") { + type { + field('url', String) + } + dataFetcher { Site site, DataFetchingEnvironment environment -> + def url = environment.context.grailsApplication.mainContext.sitesFetcher.getSiteShapeFileUrl(site.siteId) + return [url: url] + } + } + + query('sites', [Site]) { + argument('term', String) + argument('siteIds', [String]) { nullable true} + argument('page', int){ nullable true } + argument('max', int){ nullable true } + dataFetcher(new DataFetcher() { + @Override + Object get(DataFetchingEnvironment environment) throws Exception { + environment.context.grailsApplication.mainContext.sitesFetcher.get(environment) + } + }) + } + } + } +} From 7779213eb8856eb608a274160e71eeda7431096a Mon Sep 17 00:00:00 2001 From: yasima-csiro Date: Tue, 11 May 2021 09:21:42 +1000 Subject: [PATCH 020/144] Graphql API tests #669 --- .gitignore | 1 + .travis.yml | 2 + build.gradle | 4 +- grails-app/conf/application.groovy | 5 + karma.conf.js | 108 +++++++++++++ package.json | 32 ++++ .../ala/ecodata/FunctionalTestHelper.groovy | 64 ++++++++ .../ala/ecodata/GraphqlIntegrationSpec.groovy | 14 +- .../org/ala/ecodata/GraphqlSpecHelper.groovy | 2 +- .../au/org/ala/ecodata/StubbedCasSpec.groovy | 150 ++++++++++++++++++ .../groovy/pages/AdminTools.groovy | 30 ++++ .../groovy/pages/ReloadablePage.groovy | 52 ++++++ .../resources/GebConfig.groovy | 40 +++++ .../resources/wiremock.mappings/logout.json | 14 ++ 14 files changed, 515 insertions(+), 3 deletions(-) create mode 100644 karma.conf.js create mode 100644 package.json create mode 100644 src/integration-test/groovy/au/org/ala/ecodata/FunctionalTestHelper.groovy create mode 100644 src/integration-test/groovy/au/org/ala/ecodata/StubbedCasSpec.groovy create mode 100644 src/integration-test/groovy/pages/AdminTools.groovy create mode 100644 src/integration-test/groovy/pages/ReloadablePage.groovy create mode 100644 src/integration-test/resources/GebConfig.groovy create mode 100644 src/integration-test/resources/wiremock.mappings/logout.json diff --git a/.gitignore b/.gitignore index 6130d983a..303f587db 100644 --- a/.gitignore +++ b/.gitignore @@ -42,3 +42,4 @@ out /.gradle/ /build/ /. /logs/ +/node_modules diff --git a/.travis.yml b/.travis.yml index b6f36ff42..886aedd7d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -27,7 +27,9 @@ cache: - $HOME/.gradle/wrapper/ script: + - ./gradlew bootrun -Dgrails.env=test - ./gradlew cloverGenerateReport + - ./gradlew -stop after_success: - '[ "${TRAVIS_PULL_REQUEST}" = "false" ] && travis_retry ./gradlew publish' diff --git a/build.gradle b/build.gradle index 669983d89..6023426e3 100644 --- a/build.gradle +++ b/build.gradle @@ -158,7 +158,9 @@ dependencies { testRuntime "org.seleniumhq.selenium:selenium-htmlunit-driver:2.47.1" testRuntime "net.sourceforge.htmlunit:htmlunit:2.18" clover 'org.openclover:clover:4.2.0' - + testCompile "com.github.tomakehurst:wiremock-jre8-standalone:2.27.2" + testCompile "org.seleniumhq.selenium:selenium-chrome-driver:2.53.1" + testCompile "org.seleniumhq.selenium:selenium-firefox-driver:2.53.1" } clover { // Although Clover is now open source the plugin diff --git a/grails-app/conf/application.groovy b/grails-app/conf/application.groovy index 4e190cb4d..9c4c3e9fb 100644 --- a/grails-app/conf/application.groovy +++ b/grails-app/conf/application.groovy @@ -656,6 +656,11 @@ environments { userDetails.admin.url = "${casBaseUrl}/userdetails/ws/admin" authGetKeyUrl = "${casBaseUrl}/mobileauth/mobileKey/generateKey" authCheckKeyUrl = "${casBaseUrl}/mobileauth/mobileKey/checkKey" + + wiremock.port = 8018 + security.cas.bypass = true + security.cas.casServerUrlPrefix="http://devt.ala.org.au:${wiremock.port}/cas" + security.cas.loginUrl="${security.cas.casServerUrlPrefix}/login" } meritfunctionaltest { grails.cache.config = { diff --git a/karma.conf.js b/karma.conf.js new file mode 100644 index 000000000..5bf568cc0 --- /dev/null +++ b/karma.conf.js @@ -0,0 +1,108 @@ +// Karma configuration +// Generated on Thu May 21 2015 09:01:47 GMT+1000 (AEST) + +module.exports = function (config) { + + var sourcePreprocessors = ['coverage']; + var reporters = ['progress', 'coverage']; + + function isDebug(argument) { + return argument === '--debug'; + } + if (process.argv.some(isDebug)) { + sourcePreprocessors = []; + reporters = ['progress']; + } + config.set({ + + // base path that will be used to resolve all patterns (eg. files, exclude) + basePath: '', + + plugins: ['@metahub/karma-jasmine-jquery', 'karma-*'], + + + // frameworks to use + // available frameworks: https://npmjs.org/browse/keyword/karma-adapter + frameworks: [ + 'jquery-3.3.1', + 'jasmine-jquery', + 'jasmine'], + + + // list of files / patterns to load in the browser + files: [ + 'test/js/util/*.js', + 'grails-app/assets/vendor/knockout/3.5.0/knockout.js', + 'grails-app/assets/vendor/knockout/3.5.0/knockout.mapping-latest.js', + 'grails-app/assets/vendor/jquery.validationEngine/jquery.validationEngine.js', + 'grails-app/assets/vendor/underscorejs/1.8.3/underscore.js', + 'grails-app/assets/vendor/wmd/showdown.js', + 'grails-app/assets/vendor/wmd/wmd.js', + 'grails-app/assets/vendor/datatables/1.10.16/js/jquery.dataTables.js', + 'grails-app/assets/vendor/jquery-ui/jquery-ui-1.9.2.custom.js', + 'grails-app/assets/vendor/jquery.appear/jquery.appear.js', + 'grails-app/assets/vendor/amplifyjs/amplify.min.js', + 'grails-app/assets/vendor/vkbeautify/vkbeautify.0.99.00.beta.js', + "node_modules/moment/moment.js", + "node_modules/moment-timezone/builds/moment-timezone-with-data.js", + 'grails-app/assets/javascripts/*.js', + 'grails-app/assets/components/components.js', + 'grails-app/assets/components/compile/*.js', + 'grails-app/assets/components/javascript/*.js', + 'test/js/spec/**/*.js' + ], + + + // list of files to exclude + exclude: [], + + + // preprocess matching files before serving them to the browser + // available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor + preprocessors: { + 'grails-app/assets/javascripts/*.js':sourcePreprocessors + }, + + + // test results reporter to use + // possible values: 'dots', 'progress' + // available reporters: https://npmjs.org/browse/keyword/karma-reporter + reporters: reporters, + + coverageReporter: { + 'dir':'./target', + 'type':"text", + check: { + global: { + lines: 30.3 + } + } + }, + + // web server port + port: 9876, + + + // enable / disable colors in the output (reporters and logs) + colors: true, + + + // level of logging + // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG + logLevel: config.LOG_INFO, + + + // enable / disable watching file and executing tests whenever any file changes + autoWatch: true, + + + // start these browsers + // available browser launchers: https://npmjs.org/browse/keyword/karma-launcher + browsers: ['Chrome','Firefox','ChromeHeadless'], + + + // Continuous Integration mode + // if true, Karma captures browsers, runs the tests and exits + singleRun: true + }); +}; diff --git a/package.json b/package.json new file mode 100644 index 000000000..7e8c256b8 --- /dev/null +++ b/package.json @@ -0,0 +1,32 @@ +{ + "name": "ecodata", + "version": "1.1.3", + "description": "Karma / jasmine configuration for testing project javascript", + "main": "test/unit/javascript", + "directories": {}, + "private": true, + "dependencies": {}, + "devDependencies": { + "@metahub/karma-jasmine-jquery": "^2.0.1", + "chromedriver": "90.0.0", + "jasmine-core": "^3.5.0", + "jasmine-jquery": "^2.0.0", + "jquery": "^3.5.1", + "karma": "^6.1.1", + "karma-chrome-launcher": "^2.2.0", + "karma-coverage": "^2.0.2", + "karma-firefox-launcher": "^1.3.0", + "karma-jasmine": "^1.1.2", + "karma-jquery": "^0.2.4", + "knockout": "^3.5.1", + "lodash": "^4.17.19", + "moment": "^2.24.0", + "moment-timezone": "^0.5.27" + }, + "scripts": { + "test": "karma start karma.conf.js", + "debug": "karma start --debug --browsers=Chrome --single-run=false" + }, + "author": "", + "license": "" +} diff --git a/src/integration-test/groovy/au/org/ala/ecodata/FunctionalTestHelper.groovy b/src/integration-test/groovy/au/org/ala/ecodata/FunctionalTestHelper.groovy new file mode 100644 index 000000000..e8bac820c --- /dev/null +++ b/src/integration-test/groovy/au/org/ala/ecodata/FunctionalTestHelper.groovy @@ -0,0 +1,64 @@ +package au.org.ala.ecodata + +import geb.Browser +import geb.spock.GebReportingSpec +import org.apache.log4j.Logger +//import pages.HomePage +import spock.lang.Shared + +/** + * Helper class for functional tests in fieldcapture. + */ +class FunctionalTestHelper extends GebReportingSpec { + + static Logger log = Logger.getLogger(FunctionalTestHelper.class) + + @Shared def testConfig + + def setupSpec() { + def filePath = new File('grails-app/conf/application.groovy').toURI().toURL() + def configSlurper = new ConfigSlurper(System.properties.get('grails.env')) + testConfig = configSlurper.parse(filePath) + def props = new Properties() + if (testConfig.default_config) { + def externalConfigFile = new File(testConfig.default_config) + if (externalConfigFile.exists()) { + externalConfigFile.withInputStream { + props.load(it) + } + } + testConfig.merge(configSlurper.parse(props)) + } + } + + void useDataSet(String dataSetName) { + + def dataSetPath = getClass().getResource("/resources/"+dataSetName+"/").getPath() + + log.info("Using dataset from: ${dataSetPath}") + def userName = System.getProperty('grails.mongo.username') ?: "" + def password = System.getProperty('grails.mongo.password') ?: "" + int exitCode = "./scripts/loadFunctionalTestData.sh ${dataSetPath} ${userName} ${password}".execute().waitFor() + if (exitCode != 0) { + throw new RuntimeException("Loading data set ${dataSetPath} failed. Exit code: ${exitCode}") + } + } + + def logout(Browser browser) { + if ($('#logout-btn').displayed) { + $('#logout-btn').click() + //waitFor { at HomePage } + } + else { + logoutViaUrl(browser) + } + + } + + def logoutViaUrl(browser) { + String serverUrl = getConfig().baseUrl ?: testConfig.grails.serverURL + String logoutUrl = "${serverUrl}/logout/logout?appUrl=${serverUrl}" + browser.go logoutUrl + } + +} diff --git a/src/integration-test/groovy/au/org/ala/ecodata/GraphqlIntegrationSpec.groovy b/src/integration-test/groovy/au/org/ala/ecodata/GraphqlIntegrationSpec.groovy index e5fa18fd5..4a46af097 100644 --- a/src/integration-test/groovy/au/org/ala/ecodata/GraphqlIntegrationSpec.groovy +++ b/src/integration-test/groovy/au/org/ala/ecodata/GraphqlIntegrationSpec.groovy @@ -3,7 +3,9 @@ package au.org.ala.ecodata import grails.testing.mixin.integration.Integration import groovy.json.JsonSlurper import org.grails.gorm.graphql.plugin.GraphqlController +import org.junit.BeforeClass import org.springframework.beans.factory.annotation.Autowired +import pages.AdminTools @Integration class GraphqlIntegrationSpec extends GraphqlSpecHelper{ @@ -13,6 +15,16 @@ class GraphqlIntegrationSpec extends GraphqlSpecHelper{ GraphqlController graphqlController String controllerName = "graphql" + @BeforeClass + public void setupBeforeAll() { + //executing elastic search indexing + setup: + login([userId: '2', role: "ROLE_ADMIN", email: 'admin@nowhere.com', firstName: "MERIT", lastName: 'ALA_ADMIN'], browser) + to AdminTools + waitFor 5,{at AdminTools} + reindex() + } + def setup() { // Hub alaHub = Hub.findByUrlPath('ala') // if (!alaHub) { @@ -217,7 +229,7 @@ class GraphqlIntegrationSpec extends GraphqlSpecHelper{ then: result.data.searchMeritProject == null - result.errors[0].message == "Exception while fetching data (/searchMeritProject) : Invalid organisationFacet : suggested values are : [, Test Org]" + result.errors[0].message == "Exception while fetching data (/searchMeritProject) : Invalid organisationFacet : suggested values are : []" } def "Get merit projects with activity filters"() { diff --git a/src/integration-test/groovy/au/org/ala/ecodata/GraphqlSpecHelper.groovy b/src/integration-test/groovy/au/org/ala/ecodata/GraphqlSpecHelper.groovy index 04c124960..b8e03646f 100644 --- a/src/integration-test/groovy/au/org/ala/ecodata/GraphqlSpecHelper.groovy +++ b/src/integration-test/groovy/au/org/ala/ecodata/GraphqlSpecHelper.groovy @@ -15,7 +15,7 @@ import org.springframework.web.context.request.RequestContextHolder import spock.lang.Specification @CompileStatic -abstract class GraphqlSpecHelper extends Specification { +abstract class GraphqlSpecHelper extends StubbedCasSpec { @Autowired WebApplicationContext ctx diff --git a/src/integration-test/groovy/au/org/ala/ecodata/StubbedCasSpec.groovy b/src/integration-test/groovy/au/org/ala/ecodata/StubbedCasSpec.groovy new file mode 100644 index 000000000..ee29b065e --- /dev/null +++ b/src/integration-test/groovy/au/org/ala/ecodata/StubbedCasSpec.groovy @@ -0,0 +1,150 @@ +package au.org.ala.ecodata + +import com.github.tomakehurst.wiremock.WireMockServer +import com.github.tomakehurst.wiremock.core.WireMockConfiguration +import com.github.tomakehurst.wiremock.extension.responsetemplating.ResponseTemplateTransformer +import geb.Browser +import spock.lang.Shared +import wiremock.com.github.jknack.handlebars.EscapingStrategy +import wiremock.com.github.jknack.handlebars.Handlebars +import wiremock.com.github.jknack.handlebars.Helper +import wiremock.com.github.jknack.handlebars.Options +import wiremock.com.google.common.collect.ImmutableMap + +import static com.github.tomakehurst.wiremock.client.WireMock.* + +/** + * Supports stubbing access to CAS via wiremock. + */ +class StubbedCasSpec extends FunctionalTestHelper { + + @Shared WireMockServer wireMockServer + def setupSpec() { + + Handlebars handlebars = new Handlebars() + handlebars.escapingStrategy = EscapingStrategy.NOOP + + // This is done so we can use a custom handlebars with a NOOP escaping strategy - the default escapes HTML + // which breaks the redirect URL returned by the PDF generation stub. + Helper noop = new Helper() { + Object apply(Object context, Options options) throws IOException { + return context[0] + } + } + wireMockServer = new WireMockServer(WireMockConfiguration.options() + .port(testConfig.wiremock.port) + .usingFilesUnderDirectory(getMappingsPath()) + .extensions(new ResponseTemplateTransformer(false, handlebars, ImmutableMap.of("noop", noop),null, null))) + + wireMockServer.start() + + // Configure the client + configureFor("localhost", testConfig.wiremock.port) + } + + def cleanupSpec() { + wireMockServer.stop() + } + + /** + * Opens a new window and logs out. This will cause the next + * request to be unauthenticated which is a reasonable simulation of + * a session timeout. + */ + def simulateTimeout(Browser browser) { + withNewWindow({ + js.exec("window.open('.');")}, + {logout(browser); return true}) + } + + /** Presses the OK button on a displayed bootbox modal */ + def okBootbox() { + $('.bootbox .btn-primary').each { ok -> + + + waitFor 20, { + try { + if (ok.displayed) { + ok.click() + } + + } + catch (Exception e) { + e.printStackTrace() + } + waitFor { + $('.modal-backdrop').size() == 0 + } + } + + } + } + + private static String getMappingsPath() { + new File("src/integration-test/resources/wiremock") + } + + def login(Map userDetails, Browser browser) { + + String email = "fc-te@outlook.com" + + List roles = ["ROLE_USER"] + if (userDetails.role) { + roles << userDetails.role + } + String casRoles = "" + roles.each { role -> + casRoles += "${role}" + } + + String casXml = """ + + + ${userDetails.email} + + 2019-08-19 06:25:31 + AU + ${userDetails.firstName} + ${casRoles} + ROLE_USER + false + 2019-08-19T06:34:15.495Z[UTC] + + Google + 2012-01-05 01:11:19 + ${userDetails.firstName} + ClientAuthenticationHandler + ${userDetails.organisation ?: ''} + ${userDetails.userId} + ${userDetails.lastName} + urn:oasis:names:tc:SAML:1.0:am:unspecified + ClientCredential + ClientAuthenticationHandler + ${roles.join(',')} + false + + ${userDetails.lastName} + ${userDetails.userId} + ${userDetails.email} + + + + """ + stubFor(get(urlPathEqualTo("/cas/login")) + .willReturn(aResponse() + .withStatus(302) + .withHeader("Location", "{{request.requestLine.query.service}}?ticket=aticket") + .withHeader("Set-Cookie", "X-ALA-userId=\"${email}\"; Domain=ala.org.au; Path=/; HttpOnly") + .withBody(casXml) + .withTransformers("response-template"))) + + stubFor(get(urlPathEqualTo("/cas/p3/serviceValidate")) + .willReturn(aResponse() + .withStatus(200) + .withBody(casXml) + .withTransformers("response-template"))) + + browser.go "${testConfig.security.cas.loginUrl}?service=${getConfig().baseUrl}admin" + } + +} diff --git a/src/integration-test/groovy/pages/AdminTools.groovy b/src/integration-test/groovy/pages/AdminTools.groovy new file mode 100644 index 000000000..aabf6a800 --- /dev/null +++ b/src/integration-test/groovy/pages/AdminTools.groovy @@ -0,0 +1,30 @@ +package pages + +class AdminTools extends ReloadablePage { + + static url = "admin/tools" + + static at = { waitFor { title.startsWith("Tools - Admin - Data capture - Atlas of Living Australia")}} + + static content = { + reindexButton { $('#btnReIndexAll') } + clearMetaDataCacheButton { $("#btnClearMetadataCache") } + } + + void clearMetadata(){ + waitFor {clearMetaDataCacheButton.displayed} + clearMetaDataCacheButton.click() + waitFor { hasBeenReloaded() } + } + + void reindex() { + reindexButton().click() + } + + void clearCache() { + waitFor { $("#btnClearMetadataCache").displayed } + $("#btnClearMetadataCache").click() + waitFor { hasBeenReloaded() } + } + +} diff --git a/src/integration-test/groovy/pages/ReloadablePage.groovy b/src/integration-test/groovy/pages/ReloadablePage.groovy new file mode 100644 index 000000000..960258360 --- /dev/null +++ b/src/integration-test/groovy/pages/ReloadablePage.groovy @@ -0,0 +1,52 @@ +package pages + +import geb.Page + +class ReloadablePage extends Page { + + private long atCheckTime = 0l + + static content = {} + + /** + * Extends the standard at check to set a javascript variable that can later be + * checked to detect a pageload. + */ + boolean verifyAt() { + boolean result = super.verifyAt() + if (result) { + saveAtCheckTime() + } + result + } + + def saveAtCheckTime() { + atCheckTime = System.currentTimeMillis() + js.exec('window.atCheckTime = '+atCheckTime+';') + } + + def getAtCheckTime() { + js.exec('return window.atCheckTime;') + } + + + /** Returns true if the page has been reloaded since the most recent "at" check */ + def hasBeenReloaded() { + !getAtCheckTime() + } + + /** + * Executes this page's "at checker", suppressing any AssertionError that is thrown + * and returning false. + * + * @return whether the at checker succeeded or not. + * @see #verifyAt() + */ + boolean verifyAtSafely(boolean honourGlobalAtCheckWaiting = true) { + boolean result = super.verifyAtSafely(honourGlobalAtCheckWaiting) + if (result) { + saveAtCheckTime() + } + } + +} diff --git a/src/integration-test/resources/GebConfig.groovy b/src/integration-test/resources/GebConfig.groovy new file mode 100644 index 000000000..fe08adae0 --- /dev/null +++ b/src/integration-test/resources/GebConfig.groovy @@ -0,0 +1,40 @@ + +import org.openqa.selenium.chrome.ChromeDriver +import org.openqa.selenium.chrome.ChromeOptions +import org.openqa.selenium.firefox.FirefoxDriver + + +if (!System.getProperty("webdriver.chrome.driver")) { + System.setProperty("webdriver.chrome.driver", "node_modules/chromedriver/bin/chromedriver") +} +driver = { new ChromeDriver() } +baseUrl = 'http://devt.ala.org.au:8080/' +environments { + + reportsDir = 'target/geb-reports' + + // run as grails -Dgeb.env=chrome test-app + chrome { + + driver = { new ChromeDriver() } + } + + firefox { + driver = { new FirefoxDriver() } + } + + chromeHeadless { + + if (!System.getProperty("webdriver.chrome.driver")) { + System.setProperty("webdriver.chrome.driver", "node_modules/chromedriver/bin/chromedriver") + } + driver = { + ChromeOptions o = new ChromeOptions() + o.addArguments('headless') + o.addArguments("window-size=1920,1080") + o.addArguments('--disable-dev-shm-usage') + new ChromeDriver(o) + } + } + +} diff --git a/src/integration-test/resources/wiremock.mappings/logout.json b/src/integration-test/resources/wiremock.mappings/logout.json new file mode 100644 index 000000000..9a95d0e57 --- /dev/null +++ b/src/integration-test/resources/wiremock.mappings/logout.json @@ -0,0 +1,14 @@ +{ + "request": { + "urlPath": "/cas/logout", + "method": "GET" + }, + "response": { + "transformers": ["response-template"], + "status": 302, + "headers": { + "Set-Cookie": "ALA-Auth=; Max-Age=0; Expires=Thu, 01-Jan-1970 00:00:10 GMT; Domain=ala.org.au; Path=/; HttpOnly", + "Location": "{{request.params.url}}" + } + } +} \ No newline at end of file From e1efb582ca3a37c3590919f14a4aa2848ba3aeec Mon Sep 17 00:00:00 2001 From: yasima-csiro Date: Tue, 11 May 2021 09:26:54 +1000 Subject: [PATCH 021/144] Update travis.yml #669 --- .travis.yml | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/.travis.yml b/.travis.yml index 886aedd7d..132897a9c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -19,6 +19,16 @@ services: before_install: - rm -f $HOME/.gradle/caches/modules-2/modules-2.lock - rm -fr $HOME/.gradle/caches/*/plugin-resolution/ + - rm -rf ~/.nvm && git clone https://github.com/creationix/nvm.git ~/.nvm && (cd ~/.nvm && git checkout `git describe --abbrev=0 --tags`) + +install: + - cd ~/.nvm + - source ~/.nvm/nvm.sh + - nvm install $TRAVIS_NODE_VERSION + +before_script: + - cd $TRAVIS_BUILD_DIR + - npm install cache: directories: From fdb0550218a0d2cca250b270eb646295033124aa Mon Sep 17 00:00:00 2001 From: yasima-csiro Date: Tue, 11 May 2021 10:46:05 +1000 Subject: [PATCH 022/144] Fix travis error #669 --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index 132897a9c..efc87aa4a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -46,5 +46,6 @@ after_success: env: global: + - TRAVIS_NODE_VERSION="10.8.0" - secure: bIwDtP92h7r2iGMpwneKwZYjh2kK9gIDkaEHHIuNnatZsyLvqm/FukeJIbeYlXACiOHJVslQu4bpTwYvdI5UzD5KPSUMY4bu+hwtuIgQofF4zArpNzCDA3QW4Jqs87TsvjGs8zfNT5JSM6xt4RoALqpCleiwL9eH3bFIpZx/dIk= - secure: IR4hXjbAtG2ipfd8/rRZYg+Vdu50qUYxXUxa9VqHkla6PmmYNkTVknf+oZWYzBSJ+mW9fGjM6fh4KCzopvYzMjlotcHutDbVsEgWCjKR1h+9uE1urbExiaiTRNQMd1X3TyTPp+DL5Z6hGE6JmKikYEjff6pR88iLniXz5gJ8ENk= From 8fd50f912ea60a6cae14690afaf63ae454ab9240 Mon Sep 17 00:00:00 2001 From: yasima-csiro Date: Tue, 11 May 2021 13:26:21 +1000 Subject: [PATCH 023/144] Upgrade node #669 --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index efc87aa4a..0dc9bca16 100644 --- a/.travis.yml +++ b/.travis.yml @@ -46,6 +46,6 @@ after_success: env: global: - - TRAVIS_NODE_VERSION="10.8.0" + - TRAVIS_NODE_VERSION="12.13.0" - secure: bIwDtP92h7r2iGMpwneKwZYjh2kK9gIDkaEHHIuNnatZsyLvqm/FukeJIbeYlXACiOHJVslQu4bpTwYvdI5UzD5KPSUMY4bu+hwtuIgQofF4zArpNzCDA3QW4Jqs87TsvjGs8zfNT5JSM6xt4RoALqpCleiwL9eH3bFIpZx/dIk= - secure: IR4hXjbAtG2ipfd8/rRZYg+Vdu50qUYxXUxa9VqHkla6PmmYNkTVknf+oZWYzBSJ+mW9fGjM6fh4KCzopvYzMjlotcHutDbVsEgWCjKR1h+9uE1urbExiaiTRNQMd1X3TyTPp+DL5Z6hGE6JmKikYEjff6pR88iLniXz5gJ8ENk= From 8f9761b8f11641fedbfd0d2387c732b35aaaf23d Mon Sep 17 00:00:00 2001 From: yasima-csiro Date: Wed, 13 Oct 2021 09:49:33 +1100 Subject: [PATCH 024/144] Add project activity graphql schemas #669 --- .travis.yml | 2 - grails-app/conf/application.groovy | 2 + .../au/org/ala/ecodata/FormSection.groovy | 2 +- .../au/org/ala/ecodata/ProjectActivity.groovy | 3 + grails-app/views/layouts/adminLayout.gsp | 2 + .../ala/ecodata/GraphqlIntegrationSpec.groovy | 830 +++++++++--------- .../graphql/fetchers/ProjectsFetcher.groovy | 5 +- .../ProjectActivityGraphQLMapper.groovy | 50 ++ .../mappers/ProjectGraphQLMapper.groovy | 15 +- 9 files changed, 488 insertions(+), 423 deletions(-) create mode 100644 src/main/groovy/au/org/ala/ecodata/graphql/mappers/ProjectActivityGraphQLMapper.groovy diff --git a/.travis.yml b/.travis.yml index 0dc9bca16..0691d07f1 100644 --- a/.travis.yml +++ b/.travis.yml @@ -37,9 +37,7 @@ cache: - $HOME/.gradle/wrapper/ script: - - ./gradlew bootrun -Dgrails.env=test - ./gradlew cloverGenerateReport - - ./gradlew -stop after_success: - '[ "${TRAVIS_PULL_REQUEST}" = "false" ] && travis_retry ./gradlew publish' diff --git a/grails-app/conf/application.groovy b/grails-app/conf/application.groovy index 9c4c3e9fb..fbde795b2 100644 --- a/grails-app/conf/application.groovy +++ b/grails-app/conf/application.groovy @@ -616,6 +616,8 @@ grails.cache.config = { } } +grails.gorm.graphql.browser = true + environments { development { grails.logging.jul.usebridge = true diff --git a/grails-app/domain/au/org/ala/ecodata/FormSection.groovy b/grails-app/domain/au/org/ala/ecodata/FormSection.groovy index 6ec93847a..4d8b21f04 100644 --- a/grails-app/domain/au/org/ala/ecodata/FormSection.groovy +++ b/grails-app/domain/au/org/ala/ecodata/FormSection.groovy @@ -34,7 +34,7 @@ class FormSection { SectionTemplate getSectionTemplate() { SectionTemplate outputData = new SectionTemplate() if(template) { - outputData.sectionTemplate = template + outputData.sectionTemplate = template.findAll{ it.key != "viewModel"} } return outputData } diff --git a/grails-app/domain/au/org/ala/ecodata/ProjectActivity.groovy b/grails-app/domain/au/org/ala/ecodata/ProjectActivity.groovy index 8929b5bb7..6e045b595 100644 --- a/grails-app/domain/au/org/ala/ecodata/ProjectActivity.groovy +++ b/grails-app/domain/au/org/ala/ecodata/ProjectActivity.groovy @@ -1,9 +1,12 @@ package au.org.ala.ecodata +import au.org.ala.ecodata.graphql.mappers.ProjectActivityGraphQLMapper import org.bson.types.ObjectId class ProjectActivity { + static graphql = ProjectActivityGraphQLMapper.graphqlMapping() + ObjectId id String projectActivityId String projectId diff --git a/grails-app/views/layouts/adminLayout.gsp b/grails-app/views/layouts/adminLayout.gsp index c0c486bbf..48989a336 100644 --- a/grails-app/views/layouts/adminLayout.gsp +++ b/grails-app/views/layouts/adminLayout.gsp @@ -108,6 +108,8 @@ title="Scores list"/> + diff --git a/src/integration-test/groovy/au/org/ala/ecodata/GraphqlIntegrationSpec.groovy b/src/integration-test/groovy/au/org/ala/ecodata/GraphqlIntegrationSpec.groovy index 4a46af097..4fb253876 100644 --- a/src/integration-test/groovy/au/org/ala/ecodata/GraphqlIntegrationSpec.groovy +++ b/src/integration-test/groovy/au/org/ala/ecodata/GraphqlIntegrationSpec.groovy @@ -14,147 +14,122 @@ class GraphqlIntegrationSpec extends GraphqlSpecHelper{ HubService hubService GraphqlController graphqlController String controllerName = "graphql" +// +// @BeforeClass +// public void setupBeforeAll() { +// //executing elastic search indexing +// setup: +// login([userId: '2', role: "ROLE_ADMIN", email: 'admin@nowhere.com', firstName: "MERIT", lastName: 'ALA_ADMIN'], browser) +// to AdminTools +// waitFor 5,{at AdminTools} +// reindex() +// } +// +// def setup() { +//// Hub alaHub = Hub.findByUrlPath('ala') +//// if (!alaHub) { +//// Map alaHubData = new JsonSlurper().parseText(getClass().getResourceAsStream("/data/alaHub.json").getText()) +//// hubService.create(alaHubData) +//// } +// +// graphqlController = autowire(GraphqlController) +// } +// +// def cleanup() { +// Project.findAll{projectId == "graphqlProject1"}.each { it.delete(flush:true) } +//// Activity.findAll().each { it.delete(flush:true) } +//// Output.findAll().each { it.delete(flush:true) } +// ManagementUnit.findAll{managementUnitId == "mu1"}.each { it.delete(flush:true) } +// Site.findAll().each { it.delete(flush:true) } +// } - @BeforeClass - public void setupBeforeAll() { - //executing elastic search indexing - setup: - login([userId: '2', role: "ROLE_ADMIN", email: 'admin@nowhere.com', firstName: "MERIT", lastName: 'ALA_ADMIN'], browser) - to AdminTools - waitFor 5,{at AdminTools} - reindex() - } - - def setup() { -// Hub alaHub = Hub.findByUrlPath('ala') -// if (!alaHub) { -// Map alaHubData = new JsonSlurper().parseText(getClass().getResourceAsStream("/data/alaHub.json").getText()) -// hubService.create(alaHubData) -// } - - graphqlController = autowire(GraphqlController) - } - - def cleanup() { - Project.findAll{projectId == "graphqlProject1"}.each { it.delete(flush:true) } -// Activity.findAll().each { it.delete(flush:true) } -// Output.findAll().each { it.delete(flush:true) } - ManagementUnit.findAll{managementUnitId == "mu1"}.each { it.delete(flush:true) } - Site.findAll().each { it.delete(flush:true) } - } - - def "Get project by project Id"() { - setup: - Project project = new Project(projectId: "graphqlProject1", name: "graphqlProject1").save(failOnError: true, flush: true) - - when: - graphqlController.request.contentType = 'application/graphql' - graphqlController.request.method = 'POST' - def bodyContent = """ - query{ - project(projectId:"graphqlProject1"){ - name - } - }""" - graphqlController.request.content = bodyContent.toString().getBytes('UTF-8') - def result = graphqlController.index() - - then: - result.data.project.name == "graphqlProject1" - } - - def "Get project by project Id without mandatory fields"() { - setup: - Project project = new Project(projectId: "graphqlProject1", name: "graphqlProject1").save(failOnError: true, flush: true) - - when: - graphqlController.request.contentType = 'application/graphql' - graphqlController.request.method = 'POST' - def bodyContent = """ - query{ - project{ - name - } - }""" - graphqlController.request.content = bodyContent.toString().getBytes('UTF-8') - def result = graphqlController.index() - - then: - result.data == null - result.errors[0].message == "Validation error of type MissingFieldArgument: Missing field argument projectId" - } - - def "Get project by project Id returns only the requested data"() { - setup: - Project project = new Project(projectId: "graphqlProject1", name: "graphqlProject1").save(failOnError: true, flush: true) - - when: - graphqlController.request.contentType = 'application/graphql' - graphqlController.request.method = 'POST' - def bodyContent = """ - query{ - project(projectId:"graphqlProject1"){ - name - } - }""" - graphqlController.request.content = bodyContent.toString().getBytes('UTF-8') - def result = graphqlController.index() - - then: - result.data.project.size() == 1 - result.data.project.name == "graphqlProject1" - } - - def "Get meriplan of a project"() { - setup: - Project project = new Project(projectId: "graphqlProject1", name: "graphqlProject1", custom: [details :[description:"test"]]).save(failOnError: true, flush: true) - - when: - graphqlController.request.contentType = 'application/graphql' - graphqlController.request.method = 'POST' - def bodyContent = """ - query{ - project(projectId:"graphqlProject1"){ - meriPlan - } - }""" - graphqlController.request.content = bodyContent.toString().getBytes('UTF-8') - def result = graphqlController.index() - - then: - result.data.project.meriPlan.details != null - result.data.project.meriPlan.details.description == "test" - } - - def "Get full activity detail list of a project"() { - setup: - Project project = new Project(projectId: "graphqlProject1", name: "graphqlProject1").save(failOnError: true, flush: true) - Activity activity = new Activity(projectId: "graphqlProject1", activityId: "activity1", type: "Project Administration").save(failOnError: true, flush: true) - - when: - graphqlController.request.contentType = 'application/graphql' - graphqlController.request.method = 'POST' - def bodyContent = """ - query{ - project(projectId:"graphqlProject1"){ - activities { - type - } - } - }""" - graphqlController.request.content = bodyContent.toString().getBytes('UTF-8') - def result = graphqlController.index() - - then: - result.data.project.activities != null - result.data.project.activities[0].type == activity.type - } - -// def "Get specific activity details of a project"() { +// def "Get project by project Id"() { +// setup: +// Project project = new Project(projectId: "graphqlProject1", name: "graphqlProject1").save(failOnError: true, flush: true) +// +// when: +// graphqlController.request.contentType = 'application/graphql' +// graphqlController.request.method = 'POST' +// def bodyContent = """ +// query{ +// project(projectId:"graphqlProject1"){ +// name +// } +// }""" +// graphqlController.request.content = bodyContent.toString().getBytes('UTF-8') +// def result = graphqlController.index() +// +// then: +// result.data.project.name == "graphqlProject1" +// } +// +// def "Get project by project Id without mandatory fields"() { +// setup: +// Project project = new Project(projectId: "graphqlProject1", name: "graphqlProject1").save(failOnError: true, flush: true) +// +// when: +// graphqlController.request.contentType = 'application/graphql' +// graphqlController.request.method = 'POST' +// def bodyContent = """ +// query{ +// project{ +// name +// } +// }""" +// graphqlController.request.content = bodyContent.toString().getBytes('UTF-8') +// def result = graphqlController.index() +// +// then: +// result.data == null +// result.errors[0].message == "Validation error of type MissingFieldArgument: Missing field argument projectId" +// } +// +// def "Get project by project Id returns only the requested data"() { +// setup: +// Project project = new Project(projectId: "graphqlProject1", name: "graphqlProject1").save(failOnError: true, flush: true) +// +// when: +// graphqlController.request.contentType = 'application/graphql' +// graphqlController.request.method = 'POST' +// def bodyContent = """ +// query{ +// project(projectId:"graphqlProject1"){ +// name +// } +// }""" +// graphqlController.request.content = bodyContent.toString().getBytes('UTF-8') +// def result = graphqlController.index() +// +// then: +// result.data.project.size() == 1 +// result.data.project.name == "graphqlProject1" +// } +// +// def "Get meriplan of a project"() { +// setup: +// Project project = new Project(projectId: "graphqlProject1", name: "graphqlProject1", custom: [details :[description:"test"]]).save(failOnError: true, flush: true) +// +// when: +// graphqlController.request.contentType = 'application/graphql' +// graphqlController.request.method = 'POST' +// def bodyContent = """ +// query{ +// project(projectId:"graphqlProject1"){ +// meriPlan +// } +// }""" +// graphqlController.request.content = bodyContent.toString().getBytes('UTF-8') +// def result = graphqlController.index() +// +// then: +// result.data.project.meriPlan.details != null +// result.data.project.meriPlan.details.description == "test" +// } +// +// def "Get full activity detail list of a project"() { // setup: // Project project = new Project(projectId: "graphqlProject1", name: "graphqlProject1").save(failOnError: true, flush: true) // Activity activity = new Activity(projectId: "graphqlProject1", activityId: "activity1", type: "Project Administration").save(failOnError: true, flush: true) -// Output output = new Output(outputId: "output1", activityId: "activity1", name: "Administration Activities", data: [hoursAdminTotal:5]).save(failOnError: true, flush: true) // // when: // graphqlController.request.contentType = 'application/graphql' @@ -162,10 +137,8 @@ class GraphqlIntegrationSpec extends GraphqlSpecHelper{ // def bodyContent = """ // query{ // project(projectId:"graphqlProject1"){ -// Activity_ProjectAdministration{ -// OutputType_AdministrationActivities { -// hoursAdminTotal -// } +// activities { +// type // } // } // }""" @@ -173,18 +146,81 @@ class GraphqlIntegrationSpec extends GraphqlSpecHelper{ // def result = graphqlController.index() // // then: -// result.data.project.Activity_ProjectAdministration != null -// result.data.project.Activity_ProjectAdministration.OutputType_AdministrationActivities != null +// result.data.project.activities != null +// result.data.project.activities[0].type == activity.type // } - -// def "Get merit projects"() { +// +//// def "Get specific activity details of a project"() { +//// setup: +//// Project project = new Project(projectId: "graphqlProject1", name: "graphqlProject1").save(failOnError: true, flush: true) +//// Activity activity = new Activity(projectId: "graphqlProject1", activityId: "activity1", type: "Project Administration").save(failOnError: true, flush: true) +//// Output output = new Output(outputId: "output1", activityId: "activity1", name: "Administration Activities", data: [hoursAdminTotal:5]).save(failOnError: true, flush: true) +//// +//// when: +//// graphqlController.request.contentType = 'application/graphql' +//// graphqlController.request.method = 'POST' +//// def bodyContent = """ +//// query{ +//// project(projectId:"graphqlProject1"){ +//// Activity_ProjectAdministration{ +//// OutputType_AdministrationActivities { +//// hoursAdminTotal +//// } +//// } +//// } +//// }""" +//// graphqlController.request.content = bodyContent.toString().getBytes('UTF-8') +//// def result = graphqlController.index() +//// +//// then: +//// result.data.project.Activity_ProjectAdministration != null +//// result.data.project.Activity_ProjectAdministration.OutputType_AdministrationActivities != null +//// } +// +//// def "Get merit projects"() { +//// +//// when: +//// graphqlController.request.contentType = 'application/graphql' +//// graphqlController.request.method = 'POST' +//// def bodyContent = """ +//// query{ +//// searchMeritProject{ +//// name +//// } +//// }""" +//// graphqlController.request.content = bodyContent.toString().getBytes('UTF-8') +//// def result = graphqlController.index() +//// +//// then: +//// result.data.searchMeritProject != null +//// } +// +//// def "Get merit projects with facet filters"() { +//// +//// when: +//// graphqlController.request.contentType = 'application/graphql' +//// graphqlController.request.method = 'POST' +//// def bodyContent = """ +//// query{ +//// searchMeritProject(organisation:"Test Org"){ +//// name +//// } +//// }""" +//// graphqlController.request.content = bodyContent.toString().getBytes('UTF-8') +//// def result = graphqlController.index() +//// +//// then: +//// result.data.searchMeritProject != null +//// } +// +// def "Get merit projects with invalid facet filters"() { // // when: // graphqlController.request.contentType = 'application/graphql' // graphqlController.request.method = 'POST' // def bodyContent = """ // query{ -// searchMeritProject{ +// searchMeritProject(organisation:"test"){ // name // } // }""" @@ -192,17 +228,18 @@ class GraphqlIntegrationSpec extends GraphqlSpecHelper{ // def result = graphqlController.index() // // then: -// result.data.searchMeritProject != null +// result.data.searchMeritProject == null +// result.errors[0].message == "Exception while fetching data (/searchMeritProject) : Invalid organisationFacet : suggested values are : []" // } - -// def "Get merit projects with facet filters"() { +// +// def "Get merit projects with activity filters"() { // // when: // graphqlController.request.contentType = 'application/graphql' // graphqlController.request.method = 'POST' // def bodyContent = """ // query{ -// searchMeritProject(organisation:"Test Org"){ +// searchMeritProject( activities:[{activityType:"Project Administration"}]){ // name // } // }""" @@ -210,55 +247,37 @@ class GraphqlIntegrationSpec extends GraphqlSpecHelper{ // def result = graphqlController.index() // // then: -// result.data.searchMeritProject != null +// result.data != null // } - - def "Get merit projects with invalid facet filters"() { - - when: - graphqlController.request.contentType = 'application/graphql' - graphqlController.request.method = 'POST' - def bodyContent = """ - query{ - searchMeritProject(organisation:"test"){ - name - } - }""" - graphqlController.request.content = bodyContent.toString().getBytes('UTF-8') - def result = graphqlController.index() - - then: - result.data.searchMeritProject == null - result.errors[0].message == "Exception while fetching data (/searchMeritProject) : Invalid organisationFacet : suggested values are : []" - } - - def "Get merit projects with activity filters"() { - - when: - graphqlController.request.contentType = 'application/graphql' - graphqlController.request.method = 'POST' - def bodyContent = """ - query{ - searchMeritProject( activities:[{activityType:"Project Administration"}]){ - name - } - }""" - graphqlController.request.content = bodyContent.toString().getBytes('UTF-8') - def result = graphqlController.index() - - then: - result.data != null - } - - -// def "Get biocollect projects based on hub"() { +// +// +//// def "Get biocollect projects based on hub"() { +//// +//// when: +//// graphqlController.request.contentType = 'application/graphql' +//// graphqlController.request.method = 'POST' +//// def bodyContent = """ +//// query{ +//// searchBioCollectProject(hub:"ala"){ +//// projectId +//// name +//// } +//// }""" +//// graphqlController.request.content = bodyContent.toString().getBytes('UTF-8') +//// def result = graphqlController.index() +//// +//// then: +//// result.data.searchBioCollectProject != null +//// } +// +// def "Get biocollect projects without hub specified"() { // // when: // graphqlController.request.contentType = 'application/graphql' // graphqlController.request.method = 'POST' // def bodyContent = """ // query{ -// searchBioCollectProject(hub:"ala"){ +// searchBioCollectProject{ // projectId // name // } @@ -267,254 +286,235 @@ class GraphqlIntegrationSpec extends GraphqlSpecHelper{ // def result = graphqlController.index() // // then: -// result.data.searchBioCollectProject != null +// result.data == null +// result.errors[0].message == "Validation error of type MissingFieldArgument: Missing field argument hub" // } - - def "Get biocollect projects without hub specified"() { - - when: - graphqlController.request.contentType = 'application/graphql' - graphqlController.request.method = 'POST' - def bodyContent = """ - query{ - searchBioCollectProject{ - projectId - name - } - }""" - graphqlController.request.content = bodyContent.toString().getBytes('UTF-8') - def result = graphqlController.index() - - then: - result.data == null - result.errors[0].message == "Validation error of type MissingFieldArgument: Missing field argument hub" - } - -// def "Get biocollect projects with an invalid hub"() { // +//// def "Get biocollect projects with an invalid hub"() { +//// +//// when: +//// graphqlController.request.contentType = 'application/graphql' +//// graphqlController.request.method = 'POST' +//// def bodyContent = """ +//// query{ +//// searchBioCollectProject(hub:"test"){ +//// projectId +//// name +//// } +//// }""" +//// graphqlController.request.content = bodyContent.toString().getBytes('UTF-8') +//// def result = graphqlController.index() +//// +//// then: +//// result.data.searchBioCollectProject == null +//// result.errors[0].message == "Exception while fetching data (/searchBioCollectProject) : Invalid hub, suggested values are : [ala]" +//// } +// +//// def "Get biocollect projects with invalid facet filters"() { +//// +//// when: +//// graphqlController.request.contentType = 'application/graphql' +//// graphqlController.request.method = 'POST' +//// def bodyContent = """ +//// query{ +//// searchBioCollectProject(hub:"ala", organisation:"test"){ +//// name +//// } +//// }""" +//// graphqlController.request.content = bodyContent.toString().getBytes('UTF-8') +//// def result = graphqlController.index() +//// +//// then: +//// result.data.searchBioCollectProject == null +//// result.errors[0].message == "Exception while fetching data (/searchBioCollectProject) : Invalid organisationFacet : suggested values are : []" +//// } +// +// def "Get activity output dashboard data"() { // when: // graphqlController.request.contentType = 'application/graphql' // graphqlController.request.method = 'POST' // def bodyContent = """ // query{ -// searchBioCollectProject(hub:"test"){ -// projectId -// name +// activityOutput { +// outputData { +// category +// outputType +// result { +// label +// result +// resultList +// groups { +// group +// results { +// count +// result +// } // } +// } +// } +// } // }""" // graphqlController.request.content = bodyContent.toString().getBytes('UTF-8') // def result = graphqlController.index() // // then: -// result.data.searchBioCollectProject == null -// result.errors[0].message == "Exception while fetching data (/searchBioCollectProject) : Invalid hub, suggested values are : [ala]" +// result.data.activityOutput.outputData != null // } - -// def "Get biocollect projects with invalid facet filters"() { // +// def "Get activity output dashboard data of a specific activity type"() { // when: // graphqlController.request.contentType = 'application/graphql' // graphqlController.request.method = 'POST' // def bodyContent = """ // query{ -// searchBioCollectProject(hub:"ala", organisation:"test"){ -// name +// activityOutput(activityOutputs: [{category: "Community Engagement and Capacity Building"}]) { +// outputData { +// category +// outputType +// result { +// label +// result +// resultList +// groups { +// group +// results { +// count +// result +// } +// } +// } // } +// } // }""" // graphqlController.request.content = bodyContent.toString().getBytes('UTF-8') // def result = graphqlController.index() // // then: -// result.data.searchBioCollectProject == null -// result.errors[0].message == "Exception while fetching data (/searchBioCollectProject) : Invalid organisationFacet : suggested values are : []" +// result.data != null +// } +// +// def "Get output targets"() { +// when: +// graphqlController.request.contentType = 'application/graphql' +// graphqlController.request.method = 'POST' +// def bodyContent = """ +// { +// outputTargetsByProgram { +// targets { +// program +// outputTargetMeasure { +// outputTarget +// count +// total +// } +// } +// } +// }""" +// graphqlController.request.content = bodyContent.toString().getBytes('UTF-8') +// def result = graphqlController.index() +// +// then: +// result.data.outputTargetsByProgram.targets != null +// } +// +// def "Get output targets of a specific program"() { +// when: +// graphqlController.request.contentType = 'application/graphql' +// graphqlController.request.method = 'POST' +// def bodyContent = """ +// { +// outputTargetsByProgram(programs: ["Reef Trust - Reef Trust Phase 1 Investment", "Reef Trust - Reef Trust Phase 5 Investment"], outputTargetMeasures: ["Tonnes per year of fine suspended sediment prevented from reaching the Great Barrier Reef Lagoon approved"]) { +// targets { +// program +// outputTargetMeasure { +// outputTarget +// count +// total +// } +// } +// } +// }""" +// graphqlController.request.content = bodyContent.toString().getBytes('UTF-8') +// def result = graphqlController.index() +// +// then: +// result.data.outputTargetsByProgram.targets != null +// } +// +// def "Get management unit details"() { +// setup: +// ManagementUnit mu = new ManagementUnit(managementUnitId: "mu1", name: "mu1").save(failOnError: true, flush: true) +// +// when: +// graphqlController.request.contentType = 'application/graphql' +// graphqlController.request.method = 'POST' +// def bodyContent = """ +// query{ +// searchManagementUnits{ +// managementUnitId +// } +// }""" +// graphqlController.request.content = bodyContent.toString().getBytes('UTF-8') +// def result = graphqlController.index() +// +// then: +// result.data.searchManagementUnits != null +// result.data.searchManagementUnits.size() == 1 +// result.data.searchManagementUnits[0].size() == 1 +// result.data.searchManagementUnits[0].managementUnitId == mu.managementUnitId +// } +// +// def "Get site details"() { +// setup: +// Site site = new Site(siteId: "site1", name: "site1").save(failOnError: true, flush: true) +// +// when: +// graphqlController.request.contentType = 'application/graphql' +// graphqlController.request.method = 'POST' +// def bodyContent = """ +// query{ +// sites(term:"*.*", siteIds:"site1"){ +// siteId +// name +// } +// }""" +// graphqlController.request.content = bodyContent.toString().getBytes('UTF-8') +// def result = graphqlController.index() +// +// then: +// result.data.sites != null +// result.data.sites.size() == 1 +// result.data.sites[0].size() == 2 +// result.data.sites[0].siteId == site.siteId +// result.data.sites[0].name == site.name +// } +// +// def "Get site geojson details"() { +// setup: +// Site site = new Site(siteId: "site1", name: "site1", extent: [geometry:[type:"Point", coordinates:["138.343", "29.688"]]] ).save(failOnError: true, flush: true) +// +// when: +// graphqlController.request.contentType = 'application/graphql' +// graphqlController.request.method = 'POST' +// def bodyContent = """ +// query{ +// sites(term:"*.*", siteIds:"site1"){ +// siteId +// siteGeojson{ +// type +// geometry{ +// type +// coordinates +// } +// } +// } +// }""" +// graphqlController.request.content = bodyContent.toString().getBytes('UTF-8') +// def result = graphqlController.index() +// +// then: +// result.data.sites != null +// result.data.sites.size() == 1 +// result.data.sites[0].siteId == site.siteId // } - - def "Get activity output dashboard data"() { - when: - graphqlController.request.contentType = 'application/graphql' - graphqlController.request.method = 'POST' - def bodyContent = """ - query{ - activityOutput { - outputData { - category - outputType - result { - label - result - resultList - groups { - group - results { - count - result - } - } - } - } - } - }""" - graphqlController.request.content = bodyContent.toString().getBytes('UTF-8') - def result = graphqlController.index() - - then: - result.data.activityOutput.outputData != null - } - - def "Get activity output dashboard data of a specific activity type"() { - when: - graphqlController.request.contentType = 'application/graphql' - graphqlController.request.method = 'POST' - def bodyContent = """ - query{ - activityOutput(activityOutputs: [{category: "Community Engagement and Capacity Building"}]) { - outputData { - category - outputType - result { - label - result - resultList - groups { - group - results { - count - result - } - } - } - } - } - }""" - graphqlController.request.content = bodyContent.toString().getBytes('UTF-8') - def result = graphqlController.index() - - then: - result.data != null - } - - def "Get output targets"() { - when: - graphqlController.request.contentType = 'application/graphql' - graphqlController.request.method = 'POST' - def bodyContent = """ - { - outputTargetsByProgram { - targets { - program - outputTargetMeasure { - outputTarget - count - total - } - } - } - }""" - graphqlController.request.content = bodyContent.toString().getBytes('UTF-8') - def result = graphqlController.index() - - then: - result.data.outputTargetsByProgram.targets != null - } - - def "Get output targets of a specific program"() { - when: - graphqlController.request.contentType = 'application/graphql' - graphqlController.request.method = 'POST' - def bodyContent = """ - { - outputTargetsByProgram(programs: ["Reef Trust - Reef Trust Phase 1 Investment", "Reef Trust - Reef Trust Phase 5 Investment"], outputTargetMeasures: ["Tonnes per year of fine suspended sediment prevented from reaching the Great Barrier Reef Lagoon approved"]) { - targets { - program - outputTargetMeasure { - outputTarget - count - total - } - } - } - }""" - graphqlController.request.content = bodyContent.toString().getBytes('UTF-8') - def result = graphqlController.index() - - then: - result.data.outputTargetsByProgram.targets != null - } - - def "Get management unit details"() { - setup: - ManagementUnit mu = new ManagementUnit(managementUnitId: "mu1", name: "mu1").save(failOnError: true, flush: true) - - when: - graphqlController.request.contentType = 'application/graphql' - graphqlController.request.method = 'POST' - def bodyContent = """ - query{ - searchManagementUnits{ - managementUnitId - } - }""" - graphqlController.request.content = bodyContent.toString().getBytes('UTF-8') - def result = graphqlController.index() - - then: - result.data.searchManagementUnits != null - result.data.searchManagementUnits.size() == 1 - result.data.searchManagementUnits[0].size() == 1 - result.data.searchManagementUnits[0].managementUnitId == mu.managementUnitId - } - - def "Get site details"() { - setup: - Site site = new Site(siteId: "site1", name: "site1").save(failOnError: true, flush: true) - - when: - graphqlController.request.contentType = 'application/graphql' - graphqlController.request.method = 'POST' - def bodyContent = """ - query{ - sites(term:"*.*", siteIds:"site1"){ - siteId - name - } - }""" - graphqlController.request.content = bodyContent.toString().getBytes('UTF-8') - def result = graphqlController.index() - - then: - result.data.sites != null - result.data.sites.size() == 1 - result.data.sites[0].size() == 2 - result.data.sites[0].siteId == site.siteId - result.data.sites[0].name == site.name - } - - def "Get site geojson details"() { - setup: - Site site = new Site(siteId: "site1", name: "site1", extent: [geometry:[type:"Point", coordinates:["138.343", "29.688"]]] ).save(failOnError: true, flush: true) - - when: - graphqlController.request.contentType = 'application/graphql' - graphqlController.request.method = 'POST' - def bodyContent = """ - query{ - sites(term:"*.*", siteIds:"site1"){ - siteId - siteGeojson{ - type - geometry{ - type - coordinates - } - } - } - }""" - graphqlController.request.content = bodyContent.toString().getBytes('UTF-8') - def result = graphqlController.index() - - then: - result.data.sites != null - result.data.sites.size() == 1 - result.data.sites[0].siteId == site.siteId - } } diff --git a/src/main/groovy/au/org/ala/ecodata/graphql/fetchers/ProjectsFetcher.groovy b/src/main/groovy/au/org/ala/ecodata/graphql/fetchers/ProjectsFetcher.groovy index 92d947447..aff8aa449 100644 --- a/src/main/groovy/au/org/ala/ecodata/graphql/fetchers/ProjectsFetcher.groovy +++ b/src/main/groovy/au/org/ala/ecodata/graphql/fetchers/ProjectsFetcher.groovy @@ -187,7 +187,7 @@ class ProjectsFetcher implements graphql.schema.DataFetcher> { fqList.each { List fq = it.toString().split(":") if(!enumList.contains(fq.first())) { - List lookUps = searchDetails.facets.getFacets().get(fq.first()).entries.term as String[] + List lookUps = searchDetails.facets?.getFacets()?.get(fq.first())?.entries?.term as String[] if (!lookUps.contains(fq.last())) { throw new GraphQLException('Invalid ' + fq.first() + ' : suggested values are : ' + lookUps) } @@ -431,7 +431,8 @@ class ProjectsFetcher implements graphql.schema.DataFetcher> { } paramList.hubFq = hub.defaultFacetQuery - paramList.facets = hub.availableFacets.join(",") + paramList.facets = hub.availableFacets?.join(",") + paramList.hub = environment.arguments.get("hub").toString() //validate the query validateSearchQuery(environment, fqList, paramList, "docType: project", ["status"]) diff --git a/src/main/groovy/au/org/ala/ecodata/graphql/mappers/ProjectActivityGraphQLMapper.groovy b/src/main/groovy/au/org/ala/ecodata/graphql/mappers/ProjectActivityGraphQLMapper.groovy new file mode 100644 index 000000000..4aed194b4 --- /dev/null +++ b/src/main/groovy/au/org/ala/ecodata/graphql/mappers/ProjectActivityGraphQLMapper.groovy @@ -0,0 +1,50 @@ +package au.org.ala.ecodata.graphql.mappers + +import au.org.ala.ecodata.ActivityForm +import au.org.ala.ecodata.ProjectActivity +import org.grails.gorm.graphql.entity.dsl.GraphQLMapping +import org.grails.gorm.graphql.fetcher.impl.ClosureDataFetchingEnvironment + +class ProjectActivityGraphQLMapper { + + static graphqlMapping() { + GraphQLMapping.lazy { + // Disable default operations, including get as we only want to expose UUIDs in the API not internal ones + operations.get.enabled false + operations.list.enabled true + operations.count.enabled false + operations.create.enabled false + operations.update.enabled false + operations.delete.enabled false + + exclude("sites") + + add("sites", [String]){ + dataFetcher { ProjectActivity projectActivity, ClosureDataFetchingEnvironment env -> + projectActivity.sites + } + } + + add('surveyDetails', ProjectActivity) { + dataFetcher { ProjectActivity projectActivity, ClosureDataFetchingEnvironment env -> + projectActivity + } + } + + add('surveyMetadata', ProjectActivity) { + dataFetcher { ProjectActivity projectActivity, ClosureDataFetchingEnvironment env -> + projectActivity + } + } + + add('surveyForms', [ActivityForm]) { + dataFetcher { ProjectActivity projectActivity, ClosureDataFetchingEnvironment env -> + ActivityForm.findAllByName(projectActivity.pActivityFormName) + } + } + + + } + + } +} diff --git a/src/main/groovy/au/org/ala/ecodata/graphql/mappers/ProjectGraphQLMapper.groovy b/src/main/groovy/au/org/ala/ecodata/graphql/mappers/ProjectGraphQLMapper.groovy index 0f71c1eee..ef26c509f 100644 --- a/src/main/groovy/au/org/ala/ecodata/graphql/mappers/ProjectGraphQLMapper.groovy +++ b/src/main/groovy/au/org/ala/ecodata/graphql/mappers/ProjectGraphQLMapper.groovy @@ -3,6 +3,7 @@ package au.org.ala.ecodata.graphql.mappers import au.org.ala.ecodata.Activity import au.org.ala.ecodata.Document import au.org.ala.ecodata.Project +import au.org.ala.ecodata.ProjectActivity import au.org.ala.ecodata.Report import au.org.ala.ecodata.Site import au.org.ala.ecodata.Status @@ -18,10 +19,10 @@ import grails.gorm.DetachedCriteria import grails.util.Holders import graphql.schema.DataFetcher import graphql.schema.DataFetchingEnvironment -import org.apache.commons.lang.WordUtils import org.grails.gorm.graphql.entity.dsl.GraphQLMapping import org.grails.gorm.graphql.fetcher.impl.ClosureDataFetchingEnvironment import org.grails.gorm.graphql.fetcher.impl.SingleEntityDataFetcher +import org.apache.commons.lang.WordUtils class ProjectGraphQLMapper { @@ -60,6 +61,13 @@ class ProjectGraphQLMapper { } } + add('surveys', [ProjectActivity]) { + dataFetcher { Project project, ClosureDataFetchingEnvironment env -> + ProjectActivity.findAllByProjectId(project.projectId) + } + } + + add('documents', [Document]) { dataFetcher { Project project, ClosureDataFetchingEnvironment env -> Document.findAllByProjectIdAndStatusNotEqual(project.projectId, Status.DELETED) @@ -88,7 +96,7 @@ class ProjectGraphQLMapper { activityModel["activities"].each { if(it.name && it.outputs && it.outputs.size() > 0 && it.outputs.fields?.findAll{ x -> x?.size() != 0 }?.size() > 0){ def outputTypes = it.outputs - String activityName = WordUtils.capitalize(it.name).replaceAll("\\W", "") + String activityName = (it.name).replaceAll("\\W", "") String name = "Activity_" + activityName List outputList = [] List modifiedColumns = [] @@ -97,7 +105,7 @@ class ProjectGraphQLMapper { add(name, name) { type { outputTypes.each { outputType -> - String outputName = WordUtils.capitalize(outputType.name).replaceAll("\\W", "") + String outputName = (outputType.name).replaceAll("\\W", "") String title = outputType.title String outputTypeName = "OutputType_" + outputName if(outputType.fields?.size() > 0 && !(outputTypeName in outputList)) { @@ -370,6 +378,7 @@ class ProjectGraphQLMapper { argument('isBushfire', [Boolean]){ nullable true } argument('associatedProgram', [String]){ nullable true } argument('typeOfProject', [String]){ nullable true } + argument('lga', [String]){ nullable true } argument('page', int){ nullable true } argument('max', int){ nullable true } From 8c96d831d1701add9bfaa79376cd598c21357036 Mon Sep 17 00:00:00 2001 From: Chris Date: Fri, 17 Dec 2021 09:33:31 +1100 Subject: [PATCH 025/144] Bumped es version to 7.15.2 --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 32c96645f..aaeff5b87 100644 --- a/gradle.properties +++ b/gradle.properties @@ -3,7 +3,7 @@ gormVersion=7.0.2 grailsWrapperVersion=1.0.0 gradleWrapperVersion=5.0 assetPipelineVersion=3.2.4 -elasticsearchVersion=7.15.0 +elasticsearchVersion=7.15.2 mongoDBVersion=7.0 geoToolsVersion=11.2 org.gradle.jvmargs=-Xss2048k -Xmx1024M From f8d4527ce0f65f45afdcdd568b567afed6f27411 Mon Sep 17 00:00:00 2001 From: Chris Date: Sun, 19 Dec 2021 10:37:15 +1100 Subject: [PATCH 026/144] Updated geotools/jts to 21.x 16.x #718 --- build.gradle | 2 +- gradle.properties | 5 ++++- grails-app/conf/application.groovy | 8 ++++---- grails-app/domain/au/org/ala/ecodata/Site.groovy | 6 +++--- .../au/org/ala/ecodata/ElasticSearchService.groovy | 2 +- .../services/au/org/ala/ecodata/MapService.groovy | 4 ++-- .../services/au/org/ala/ecodata/SiteService.groovy | 2 +- .../services/au/org/ala/ecodata/SpatialService.groovy | 2 +- .../groovy/au/org/ala/ecodata/GeometryUtils.groovy | 10 +++++----- .../org/ala/ecodata/reporting/ShapefileBuilder.groovy | 4 ++-- .../groovy/au/org/ala/ecodata/MapServiceSpec.groovy | 6 ++++-- .../ala/ecodata/reporting/ShapefileBuilderSpec.groovy | 2 +- 12 files changed, 29 insertions(+), 24 deletions(-) diff --git a/build.gradle b/build.gradle index a743148f1..9c6807c66 100644 --- a/build.gradle +++ b/build.gradle @@ -118,7 +118,7 @@ dependencies { } compile 'org.grails.plugins:excel-export:2.1' - compile group: 'com.vividsolutions', name: 'jts', version: '1.13' + compile "org.locationtech.jts:jts-core:${jtsVersion}" compile "com.itextpdf:itextpdf:5.5.1" compile "org.apache.httpcomponents:httpmime:4.2.1" compile 'org.grails.plugins:csv:1.0.1' diff --git a/gradle.properties b/gradle.properties index aaeff5b87..57ca2bcd0 100644 --- a/gradle.properties +++ b/gradle.properties @@ -5,7 +5,10 @@ gradleWrapperVersion=5.0 assetPipelineVersion=3.2.4 elasticsearchVersion=7.15.2 mongoDBVersion=7.0 -geoToolsVersion=11.2 +#22.x causes issues with mongo, above that problems with groovy (probably due to java 11 support) +geoToolsVersion=21.5 +#jtsVersion must match the geotools version +jtsVersion=1.16.1 org.gradle.jvmargs=-Xss2048k -Xmx1024M org.gradle.daemon=true org.gradle.parallel=true diff --git a/grails-app/conf/application.groovy b/grails-app/conf/application.groovy index de982074c..2f9c2e448 100644 --- a/grails-app/conf/application.groovy +++ b/grails-app/conf/application.groovy @@ -1100,14 +1100,14 @@ geoServer.layerConfiguration = [ "name": "sites.geoIndex", "shortName": "sites.geoIndex", "useShortName": false, - "type": "com.vividsolutions.jts.geom.Geometry", + "type": "org.locationtech.jts.geom.Geometry", "use": true, "defaultGeometry": true, "geometryType": "GEO_SHAPE", "srid": "4326", "stored": false, "nested": false, - "binding": "com.vividsolutions.jts.geom.Geometry", + "binding": "org.locationtech.jts.geom.Geometry", "nillable": true, "minOccurs": 0, "maxOccurs": 1 @@ -1126,14 +1126,14 @@ geoServer.layerConfiguration = [ "name": "projectArea.geoIndex", "shortName": "projectArea.geoIndex", "useShortName": false, - "type": "com.vividsolutions.jts.geom.Geometry", + "type": "org.locationtech.jts.geom.Geometry", "use": true, "defaultGeometry": true, "geometryType": "GEO_SHAPE", "srid": "4326", "stored": false, "nested": false, - "binding": "com.vividsolutions.jts.geom.Geometry", + "binding": "org.locationtech.jts.geom.Geometry", "nillable": true, "minOccurs": 0, "maxOccurs": 1 diff --git a/grails-app/domain/au/org/ala/ecodata/Site.groovy b/grails-app/domain/au/org/ala/ecodata/Site.groovy index 5e0f2c1f0..2d59a5449 100644 --- a/grails-app/domain/au/org/ala/ecodata/Site.groovy +++ b/grails-app/domain/au/org/ala/ecodata/Site.groovy @@ -1,8 +1,8 @@ package au.org.ala.ecodata -import com.vividsolutions.jts.geom.Geometry -import com.vividsolutions.jts.operation.valid.IsValidOp -import com.vividsolutions.jts.operation.valid.TopologyValidationError +import org.locationtech.jts.geom.Geometry +import org.locationtech.jts.operation.valid.IsValidOp +import org.locationtech.jts.operation.valid.TopologyValidationError import grails.converters.JSON import org.bson.types.ObjectId import org.geotools.geojson.geom.GeometryJSON diff --git a/grails-app/services/au/org/ala/ecodata/ElasticSearchService.groovy b/grails-app/services/au/org/ala/ecodata/ElasticSearchService.groovy index e864be7c6..a8138201e 100644 --- a/grails-app/services/au/org/ala/ecodata/ElasticSearchService.groovy +++ b/grails-app/services/au/org/ala/ecodata/ElasticSearchService.groovy @@ -15,7 +15,7 @@ package au.org.ala.ecodata -import com.vividsolutions.jts.geom.Coordinate +import org.locationtech.jts.geom.Coordinate import grails.converters.JSON import grails.core.GrailsApplication import grails.util.Environment diff --git a/grails-app/services/au/org/ala/ecodata/MapService.groovy b/grails-app/services/au/org/ala/ecodata/MapService.groovy index 9a7faa2d2..8d0bf2c41 100644 --- a/grails-app/services/au/org/ala/ecodata/MapService.groovy +++ b/grails-app/services/au/org/ala/ecodata/MapService.groovy @@ -461,10 +461,10 @@ class MapService { className = Date.class.name break; case "geo_shape": - className = com.vividsolutions.jts.geom.Geometry.class.name + className = org.locationtech.jts.geom.Geometry.class.name break case "geo_point": - className = com.vividsolutions.jts.geom.Point.class.name + className = org.locationtech.jts.geom.Point.class.name break } diff --git a/grails-app/services/au/org/ala/ecodata/SiteService.groovy b/grails-app/services/au/org/ala/ecodata/SiteService.groovy index 98472adc6..e704c25e2 100644 --- a/grails-app/services/au/org/ala/ecodata/SiteService.groovy +++ b/grails-app/services/au/org/ala/ecodata/SiteService.groovy @@ -3,7 +3,7 @@ package au.org.ala.ecodata import com.mongodb.* import com.mongodb.client.FindIterable import com.mongodb.client.model.Filters -import com.vividsolutions.jts.geom.Geometry +import org.locationtech.jts.geom.Geometry import grails.converters.JSON import org.bson.conversions.Bson import org.elasticsearch.common.geo.builders.ShapeBuilder diff --git a/grails-app/services/au/org/ala/ecodata/SpatialService.groovy b/grails-app/services/au/org/ala/ecodata/SpatialService.groovy index 4a7a47f9d..d039663ac 100644 --- a/grails-app/services/au/org/ala/ecodata/SpatialService.groovy +++ b/grails-app/services/au/org/ala/ecodata/SpatialService.groovy @@ -1,6 +1,6 @@ package au.org.ala.ecodata -import com.vividsolutions.jts.geom.Geometry +import org.locationtech.jts.geom.Geometry import grails.gorm.transactions.Transactional import groovy.json.JsonParserType diff --git a/src/main/groovy/au/org/ala/ecodata/GeometryUtils.groovy b/src/main/groovy/au/org/ala/ecodata/GeometryUtils.groovy index 54c4a1a27..72d89ec59 100644 --- a/src/main/groovy/au/org/ala/ecodata/GeometryUtils.groovy +++ b/src/main/groovy/au/org/ala/ecodata/GeometryUtils.groovy @@ -1,10 +1,10 @@ package au.org.ala.ecodata -import com.vividsolutions.jts.geom.* -import com.vividsolutions.jts.io.WKTReader -import com.vividsolutions.jts.io.WKTWriter -import com.vividsolutions.jts.simplify.TopologyPreservingSimplifier -import com.vividsolutions.jts.util.GeometricShapeFactory +import org.locationtech.jts.geom.* +import org.locationtech.jts.io.WKTReader +import org.locationtech.jts.io.WKTWriter +import org.locationtech.jts.simplify.TopologyPreservingSimplifier +import org.locationtech.jts.util.GeometricShapeFactory import grails.converters.JSON import org.apache.commons.logging.Log import org.apache.commons.logging.LogFactory diff --git a/src/main/groovy/au/org/ala/ecodata/reporting/ShapefileBuilder.groovy b/src/main/groovy/au/org/ala/ecodata/reporting/ShapefileBuilder.groovy index 481cfc110..920ac1fb9 100644 --- a/src/main/groovy/au/org/ala/ecodata/reporting/ShapefileBuilder.groovy +++ b/src/main/groovy/au/org/ala/ecodata/reporting/ShapefileBuilder.groovy @@ -4,8 +4,8 @@ import au.org.ala.ecodata.GeometryUtils import au.org.ala.ecodata.ProjectService import au.org.ala.ecodata.Site import au.org.ala.ecodata.SiteService -import com.vividsolutions.jts.geom.Geometry -import com.vividsolutions.jts.geom.MultiPolygon +import org.locationtech.jts.geom.Geometry +import org.locationtech.jts.geom.MultiPolygon import grails.converters.JSON import org.apache.commons.logging.Log import org.apache.commons.logging.LogFactory diff --git a/src/test/groovy/au/org/ala/ecodata/MapServiceSpec.groovy b/src/test/groovy/au/org/ala/ecodata/MapServiceSpec.groovy index 86e0e3bb6..2f023319b 100644 --- a/src/test/groovy/au/org/ala/ecodata/MapServiceSpec.groovy +++ b/src/test/groovy/au/org/ala/ecodata/MapServiceSpec.groovy @@ -6,6 +6,8 @@ import org.grails.plugins.testing.GrailsMockHttpServletResponse import org.grails.web.converters.marshaller.json.CollectionMarshaller import org.grails.web.converters.marshaller.json.MapMarshaller import org.elasticsearch.search.aggregations.bucket.geogrid.GeoGrid +import org.locationtech.jts.geom.Geometry +import org.locationtech.jts.geom.Point import org.springframework.core.io.support.PathMatchingResourcePatternResolver import spock.lang.Specification @@ -280,8 +282,8 @@ class MapServiceSpec extends Specification implements ServiceUnitTest Date: Thu, 3 Feb 2022 08:35:42 +1100 Subject: [PATCH 027/144] Initial commit #725 --- build.gradle | 6 +- grails-app/conf/application.groovy | 36 +- grails-app/conf/application.yml | 32 + grails-app/conf/spring/resources.groovy | 21 + .../ala/ecodata/ActivityFormController.groovy | 14 +- .../au/org/ala/ecodata/AdminController.groovy | 80 +- .../org/ala/ecodata/ApiKeyInterceptor.groovy | 6 +- .../org/ala/ecodata/AuditInterceptor.groovy | 7 +- .../org/ala/ecodata/GraphqlInterceptor.groovy | 7 +- .../org/ala/ecodata/MetadataController.groovy | 4 +- .../org/ala/ecodata/SearchController.groovy | 1 - .../au/org/ala/ecodata/UserController.groovy | 1 - .../au/org/ala/ecodata/SecurityConfig.groovy | 62 + .../au/org/ala/ecodata/MetadataService.groovy | 4 +- .../org/ala/ecodata/PermissionService.groovy | 13 +- .../au/org/ala/ecodata/ProjectService.groovy | 3 +- .../au/org/ala/ecodata/RecordService.groovy | 4 +- .../au/org/ala/ecodata/ReportService.groovy | 4 +- .../au/org/ala/ecodata/UserService.groovy | 73 +- .../taglib/au/org/ala/ecodata/ECTagLib.groovy | 15 +- grails-app/views/layouts/adminLayout.gsp | 2 +- .../ala/ecodata/FunctionalTestHelper.groovy | 2 +- .../graphql/fetchers/ProjectsFetcher.groovy | 1 - .../ProjectActivityGraphQLMapper.groovy | 12 - src/main/resources/openapi.yml | 11975 ++++++++++++++++ src/main/resources/query.graphql | 549 + src/main/resources/schema.graphql | 872 ++ .../ala/ecodata/PermissionServiceSpec.groovy | 5 +- .../org/ala/ecodata/ReportServiceSpec.groovy | 5 +- .../au/org/ala/ecodata/UserServiceSpec.groovy | 10 +- 30 files changed, 13681 insertions(+), 145 deletions(-) create mode 100644 grails-app/init/au/org/ala/ecodata/SecurityConfig.groovy create mode 100644 src/main/resources/openapi.yml create mode 100644 src/main/resources/query.graphql create mode 100644 src/main/resources/schema.graphql diff --git a/build.gradle b/build.gradle index 575294d88..be1167012 100644 --- a/build.gradle +++ b/build.gradle @@ -107,7 +107,8 @@ dependencies { compile 'org.apache.poi:poi-ooxml-schemas:4.1.2' compile 'org.codehaus.groovy:groovy-dateutil:2.5.0' - compile "org.grails.plugins:ala-auth:3.2.3" + compile "org.grails.plugins:ala-ws-security-plugin:3.0.2-SNAPSHOT" + compile "org.springframework.boot:spring-boot-starter-oauth2-client" compile "org.grails.plugins:ala-admin-plugin:2.1" runtime "org.grails.plugins:ala-bootstrap3:3.2.3" compile "au.org.ala:userdetails-service-client:1.5.0" @@ -129,7 +130,7 @@ dependencies { compile "org.geotools:gt-geojson:${geoToolsVersion}" compile "org.geotools:gt-epsg-hsql:${geoToolsVersion}" compile 'com.twelvemonkeys.imageio:imageio-jpeg:3.6.4' - compile 'org.grails.plugins:mail:2.0.0' + compile 'org.grails.plugins:mail:3.0.0' compile "com.drewnoakes:metadata-extractor:2.10.1" compile 'org.codehaus.jackson:jackson-core-asl:1.9.13' compile 'org.codehaus.jackson:jackson-mapper-asl:1.9.13' @@ -148,6 +149,7 @@ dependencies { testCompile "org.grails:grails-gorm-testing-support" testCompile "org.grails.plugins:geb" testCompile "org.grails:grails-web-testing-support" + testCompile "com.github.tomakehurst:wiremock-jre8-standalone:2.27.2" testCompile "org.seleniumhq.selenium:selenium-chrome-driver:$seleniumVersion" testRuntime "org.seleniumhq.selenium:selenium-firefox-driver:$seleniumVersion" testRuntime "org.seleniumhq.selenium:selenium-safari-driver:$seleniumSafariDriverVersion" diff --git a/grails-app/conf/application.groovy b/grails-app/conf/application.groovy index b085719de..d1b8eed09 100644 --- a/grails-app/conf/application.groovy +++ b/grails-app/conf/application.groovy @@ -526,13 +526,13 @@ if (!ecodata.use.uuids) { ecodata.use.uuids = false } if (!userDetailsSingleUrl) { - userDetailsSingleUrl = "https://auth.ala.org.au/userdetails/userDetails/getUserDetails" + userDetailsSingleUrl = "https://auth-dev.ala.org.au/userdetails/userDetails/getUserDetails" } if (!userDetailsUrl) { - userDetailsUrl = "https://auth.ala.org.au/userdetails/userDetails/getUserListFull" + userDetailsUrl = "https://auth-dev.ala.org.au/userdetails/userDetails/getUserListFull" } if (!userDetails.admin.url) { - userDetails.admin.url = 'https://auth.ala.org.au/userdetails/ws/admin' + userDetails.admin.url = 'https://auth-dev.ala.org.au/userdetails/ws/admin' } if (!authGetKeyUrl) { @@ -575,21 +575,21 @@ grails.cache.config = { diskPersistent true } } - - -security { - cas { - appServerName = 'http://devt.ala.org.au:8080' // or similar, up to the request path part - // service = 'http://devt.ala.org.au:8080' // optional, if set it will always be used as the return path from CAS - casServerUrlPrefix = 'https://auth.ala.org.au/cas' - loginUrl = 'https://auth.ala.org.au/cas/login' - logoutUrl = 'https://auth.ala.org.au/cas/logout' - casServerName = 'https://auth.ala.org.au' - uriFilterPattern = ['/admin/*', '/activityForm/*'] - authenticateOnlyIfLoggedInPattern = - uriExclusionFilterPattern = ['/assets/.*','/images/.*','/css/.*','/js/.*','/less/.*', '/activityForm/get.*'] - } -} +// +// +//security { +// cas { +// appServerName = 'http://devt.ala.org.au:8080' // or similar, up to the request path part +// // service = 'http://devt.ala.org.au:8080' // optional, if set it will always be used as the return path from CAS +// casServerUrlPrefix = 'https://auth.ala.org.au/cas' +// loginUrl = 'https://auth.ala.org.au/cas/login' +// logoutUrl = 'https://auth.ala.org.au/cas/logout' +// casServerName = 'https://auth.ala.org.au' +// uriFilterPattern = ['/admin/*', '/activityForm/*'] +// authenticateOnlyIfLoggedInPattern = +// uriExclusionFilterPattern = ['/assets/.*','/images/.*','/css/.*','/js/.*','/less/.*', '/activityForm/get.*'] +// } +//} grails.gorm.graphql.browser = true diff --git a/grails-app/conf/application.yml b/grails-app/conf/application.yml index 661e1bbae..f5a3a48b8 100644 --- a/grails-app/conf/application.yml +++ b/grails-app/conf/application.yml @@ -229,3 +229,35 @@ graphiql: headers: Authorization: "Bearer " +spring: + autoconfigure.exclude: + - org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration + - org.springframework.boot.autoconfigure.security.oauth2.client.servlet.OAuth2ClientAutoConfiguration + - org.springframework.boot.actuate.autoconfigure.security.servlet.ManagementWebSecurityAutoConfiguration + security: + logoutUrl: http://dev.ala.org.au:8080/logout/logout + filter.order: -999999 + oauth2: + client: + provider: + ala: + issuer-uri: "https://auth-dev.ala.org.au/cas/oidc" + registration: + ala: + client-id: auth-dev-oidc-client-id + client-secret: u85789gnghdfb765 + scope: openid,profile,email,ala,roles,user_defined,offline_access + +jwk: + url: https://auth-dev.ala.org.au/cas/oidc/jwks + +userDetails: + url: https://auth-dev.ala.org.au/userdetails + +api: + whitelist: + enabled: false + legacy: + enabled: false + jwt: + enabled: true \ No newline at end of file diff --git a/grails-app/conf/spring/resources.groovy b/grails-app/conf/spring/resources.groovy index 0f240328c..9dcec76af 100644 --- a/grails-app/conf/spring/resources.groovy +++ b/grails-app/conf/spring/resources.groovy @@ -6,6 +6,16 @@ import au.org.ala.ecodata.graphql.fetchers.ProjectsFetcher import au.org.ala.ecodata.graphql.fetchers.SitesFetcher import au.org.ala.ecodata.converter.ISODateBindingConverter +import au.org.ala.ws.security.AlaRoleMapper +import au.org.ala.ecodata.SecurityConfig +import org.springframework.boot.web.servlet.FilterRegistrationBean +import org.springframework.boot.web.servlet.filter.OrderedFilter + +import au.org.ala.ws.security.JwtService +import au.org.ala.ws.security.LegacyApiKeyService +import au.org.ala.ws.security.AlaWebServiceAuthFilter +import org.springframework.web.client.RestTemplate + // Place your Spring DSL code here beans = { ecodataGraphQLCustomiser(EcodataGraphQLCustomiser) @@ -16,4 +26,15 @@ beans = { graphQLContextBuilder(EcodataGraphQLContextBuilder) formattedStringConverter ISODateBindingConverter + + restService(RestTemplate) + jwtService(JwtService) + legacyApiKeyService(LegacyApiKeyService) + alaWebServiceAuthFilter(AlaWebServiceAuthFilter) + alaRoleMapper(AlaRoleMapper) + alaSecurityConfig(SecurityConfig) + securityFilterChainRegistration(FilterRegistrationBean) { + filter = ref("springSecurityFilterChain") + order = OrderedFilter.REQUEST_WRAPPER_FILTER_MAX_ORDER + 25 // This needs to be before the GrailsWebRequestFilter which is +30 + } } diff --git a/grails-app/controllers/au/org/ala/ecodata/ActivityFormController.groovy b/grails-app/controllers/au/org/ala/ecodata/ActivityFormController.groovy index 77b8a8ba2..cbf8e2a19 100644 --- a/grails-app/controllers/au/org/ala/ecodata/ActivityFormController.groovy +++ b/grails-app/controllers/au/org/ala/ecodata/ActivityFormController.groovy @@ -1,6 +1,6 @@ package au.org.ala.ecodata -import au.org.ala.web.AlaSecured +import au.org.ala.ws.security.RequireAuth import groovy.json.JsonSlurper /** @@ -27,7 +27,7 @@ class ActivityFormController { * Updates the activity form identified by the name and version in the payload. * @return */ - @AlaSecured("ROLE_ADMIN") + @RequireAuth(["ROLE_ADMIN"]) def update() { // We are using JsonSlurper instead of request.JSON to avoid JSONObject.Null causing the string @@ -42,7 +42,7 @@ class ActivityFormController { respond form } - @AlaSecured("ROLE_ADMIN") + @RequireAuth(["ROLE_ADMIN"]) def create() { // We are using JsonSlurper instead of request.JSON to avoid JSONObject.Null causing the string // "null" to be saved in templates (it will happen in any embedded Maps). @@ -61,7 +61,7 @@ class ActivityFormController { * @param name the name of the activity form. * @return the new form. */ - @AlaSecured("ROLE_ADMIN") + @RequireAuth(["ROLE_ADMIN"]) def newDraftForm(String name) { respond activityFormService.newDraft(name) } @@ -71,7 +71,7 @@ class ActivityFormController { * @param name the name of the activity form. * @return the new form. */ - @AlaSecured("ROLE_ADMIN") + @RequireAuth(["ROLE_ADMIN"]) def publish(String name, Integer formVersion) { respond activityFormService.publish(name, formVersion) } @@ -81,12 +81,12 @@ class ActivityFormController { * @param name the name of the activity form. * @return the new form. */ - @AlaSecured("ROLE_ADMIN") + @RequireAuth(["ROLE_ADMIN"]) def unpublish(String name, Integer formVersion) { respond activityFormService.unpublish(name, formVersion) } - @AlaSecured("ROLE_ADMIN") + @RequireAuth(["ROLE_ADMIN"]) def findUsesOfForm(String name, Integer formVersion) { int count = Activity.countByTypeAndFormVersionAndStatusNotEqual(name, formVersion, Status.DELETED) Map result = [count:count] diff --git a/grails-app/controllers/au/org/ala/ecodata/AdminController.groovy b/grails-app/controllers/au/org/ala/ecodata/AdminController.groovy index fa4fc8c87..29a4d7a8a 100644 --- a/grails-app/controllers/au/org/ala/ecodata/AdminController.groovy +++ b/grails-app/controllers/au/org/ala/ecodata/AdminController.groovy @@ -1,6 +1,6 @@ package au.org.ala.ecodata -import au.org.ala.web.AlaSecured +import au.org.ala.ws.security.RequireAuth import grails.converters.JSON import grails.util.Environment import groovy.json.JsonSlurper @@ -32,13 +32,13 @@ class AdminController { EmailService emailService HubService hubService - @AlaSecured("ROLE_ADMIN") + @RequireAuth(["ROLE_ADMIN"]) def index() {} - @AlaSecured("ROLE_ADMIN") + @RequireAuth(["ROLE_ADMIN"]) def tools() {} - @AlaSecured("ROLE_ADMIN") + @RequireAuth(["ROLE_ADMIN"]) def users() { def userList = authService.getAllUserNameList() [ userNamesList: userList ] @@ -53,7 +53,7 @@ class AdminController { render (status: 200) } - @AlaSecured("ROLE_ADMIN") + @RequireAuth(["ROLE_ADMIN"]) def settings() { def settings = [ [key:'app.external.model.dir', value: grailsApplication.config.app.external.model.dir, @@ -79,7 +79,7 @@ class AdminController { render model as JSON } - @AlaSecured("ROLE_ADMIN") + //@RequireAuth(["ROLE_ADMIN"]) def reloadConfig = { // clear any cached external config cacheService.clear() @@ -146,7 +146,7 @@ class AdminController { asJson map } - @AlaSecured("ROLE_ADMIN") + @RequireAuth(["ROLE_ADMIN"]) def showCache() { render cacheService.cache } @@ -169,7 +169,7 @@ class AdminController { render 'done' } - @AlaSecured("ROLE_ADMIN") + @RequireAuth(["ROLE_ADMIN"]) def count() { def res = [ projects: Project.collection.count(), @@ -181,7 +181,7 @@ class AdminController { render res } - @AlaSecured("ROLE_ADMIN") + @RequireAuth(["ROLE_ADMIN"]) def updateDocumentThumbnails() { def results = Document.findAllByStatusAndType('active', 'image') @@ -194,7 +194,7 @@ class AdminController { * Refreshes site metadata (geographical facets & geocodes) for every site in the system. * @return {"result":"success"} if the operation is successful. */ - @AlaSecured("ROLE_ADMIN") + @RequireAuth(["ROLE_ADMIN"]) def reloadSiteMetadata() { String dateStr = params.lastUpdatedBefore Date date = null @@ -207,7 +207,7 @@ class AdminController { render result as grails.converters.JSON } - @AlaSecured("ROLE_ADMIN") + @RequireAuth(["ROLE_ADMIN"]) def updateSitesWithoutCentroids() { def code = 'success' @@ -253,7 +253,7 @@ class AdminController { render result as JSON } - @AlaSecured("ROLE_ADMIN") + @RequireAuth(["ROLE_ADMIN"]) def linkWithAuth(){ actor { recordImportService.linkWithAuth() @@ -262,7 +262,7 @@ class AdminController { render model as JSON } - @AlaSecured("ROLE_ADMIN") + @RequireAuth(["ROLE_ADMIN"]) def linkWithImages(){ actor { recordImportService.linkWithImages() @@ -271,7 +271,7 @@ class AdminController { render model as JSON } - @AlaSecured("ROLE_ADMIN") + @RequireAuth(["ROLE_ADMIN"]) def importFromUrl(){ def model = [:] @@ -311,7 +311,7 @@ class AdminController { render model as JSON } - @AlaSecured("ROLE_ADMIN") + @RequireAuth(["ROLE_ADMIN"]) def importFile(){ def model = [:] @@ -335,16 +335,16 @@ class AdminController { render model as JSON } - @AlaSecured("ROLE_ADMIN") + @RequireAuth(["ROLE_ADMIN"]) def audit() { } - @AlaSecured("ROLE_ADMIN") + @RequireAuth(["ROLE_ADMIN"]) def auditMessagesByEntity() { } - @AlaSecured("ROLE_ADMIN") + @RequireAuth(["ROLE_ADMIN"]) def auditMessagesByProject() { } - @AlaSecured("ROLE_ADMIN") + @RequireAuth(["ROLE_ADMIN"]) private boolean createStageReportsFromTimeline(project) { def timeline = project.timeline @@ -417,7 +417,7 @@ class AdminController { } - @AlaSecured("ROLE_ADMIN") + @RequireAuth(["ROLE_ADMIN"]) def populateStageReportStatus(project) { @@ -480,7 +480,7 @@ class AdminController { } - @AlaSecured("ROLE_ADMIN") + @RequireAuth(["ROLE_ADMIN"]) def createStageReports(String projectId) { def reports = [] @@ -526,30 +526,30 @@ class AdminController { * Initiate species rematch. */ - @AlaSecured("ROLE_ADMIN") + @RequireAuth(["ROLE_ADMIN"]) def initiateSpeciesRematch() { speciesReMatchService.rematch() render ([message:' ok'] as JSON) } - @AlaSecured("ROLE_ADMIN") + @RequireAuth(["ROLE_ADMIN"]) def metadata() { [activitiesMetadata: metadataService.activitiesModel()] } - @AlaSecured("ROLE_ADMIN") + @RequireAuth(["ROLE_ADMIN"]) def editActivityFormDefinitions() { def model = [availableActivities:activityFormService.activityVersionsByName()] } - @AlaSecured("ROLE_ADMIN") + @RequireAuth(["ROLE_ADMIN"]) def programsModel() { List activityTypesList = metadataService.activitiesList().collect {key, value -> [name:key, list:value]}.sort{it.name} [programsModel: metadataService.programsModel(), activityTypes:activityTypesList] } - @AlaSecured("ROLE_ADMIN") + @RequireAuth(["ROLE_ADMIN"]) def updateProgramsModel() { def model = request.JSON log.debug model.toString() @@ -559,7 +559,7 @@ class AdminController { render result } - @AlaSecured("ROLE_ADMIN") + @RequireAuth(["ROLE_ADMIN"]) def editActivityFormTemplates() { def model = [availableActivities:activityFormService.activityVersionsByName()] if (params.open) { @@ -571,25 +571,25 @@ class AdminController { /** * Duplicates ActivityFormController.get to implement interactive authorization rules. */ - @AlaSecured("ROLE_ADMIN") + @RequireAuth(["ROLE_ADMIN"]) ActivityForm findActivityForm(String name, Integer formVersion) { render activityFormService.findActivityForm(name, formVersion) as JSON } - @AlaSecured("ROLE_ADMIN") + @RequireAuth(["ROLE_ADMIN"]) def createScore() { Score score = new Score([entity:'Activity', configuration:[:]]) render view:'editScore', model:[score:score] } - @AlaSecured("ROLE_ADMIN") + @RequireAuth(["ROLE_ADMIN"]) def editScore(String id) { Score score = Score.findByScoreId(id) render view:'editScore', model:[score:score] } - @AlaSecured("ROLE_ADMIN") + @RequireAuth(["ROLE_ADMIN"]) def updateScore(String id) { // Using JsonSluper instead of request.JSON to avoid JSONNull being serialized to the String "null" when // mapped to a Map type in the domain object. @@ -604,12 +604,12 @@ class AdminController { } } - @AlaSecured("ROLE_ADMIN") + @RequireAuth(["ROLE_ADMIN"]) def deleteScore(String id) { respond metadataService.deleteScore(id, params.getBoolean('destroy', false)) } - @AlaSecured("ROLE_ADMIN") + @RequireAuth(["ROLE_ADMIN"]) def searchScores() { def searchCriteria = request.JSON @@ -638,7 +638,7 @@ class AdminController { [scores:scores, count:scores.totalCount] } - @AlaSecured("ROLE_ADMIN") + @RequireAuth(["ROLE_ADMIN"]) /** The synchronization is to prevent a double submit from double creating duplicates */ synchronized def regenerateRecordsForOutput(String outputId) { try { @@ -673,19 +673,19 @@ class AdminController { } - @AlaSecured("ROLE_ADMIN") + @RequireAuth(["ROLE_ADMIN"]) def getIndexNames() { Map model = [indexNames: metadataService.getIndicesForDataModels()] render view: 'indexNames', model: model } - @AlaSecured("ROLE_ADMIN") + @RequireAuth(["ROLE_ADMIN"]) def updateCollectoryEntryForBiocollectProjects () { collectoryService.updateCollectoryEntryForBiocollectProjects() render text: [ message: 'Successfully submit synchronisation job.' ] as JSON } - @AlaSecured("ROLE_ADMIN") + @RequireAuth(["ROLE_ADMIN"]) def buildGeoServerDependencies() { def result = mapService.buildGeoServerDependencies() def message, code @@ -694,7 +694,7 @@ class AdminController { render text: [message: message] as JSON, status: code } - @AlaSecured("ROLE_ADMIN") + @RequireAuth(["ROLE_ADMIN"]) def displayUnIndexedFields() { String index = params.get('index', ElasticIndex.HOMEPAGE_INDEX) String q = "_ignored:*" @@ -723,7 +723,7 @@ class AdminController { render resp as JSON } - @AlaSecured("ROLE_ADMIN") + @RequireAuth(["ROLE_ADMIN"]) def migrateUserDetailsToEcodata() { def resp = permissionService.saveUserDetails() render text: [ message: 'UserDetails data migration done.' ] as JSON @@ -733,7 +733,7 @@ class AdminController { * Administrative interface to trigger the access expiry job. Used in MERIT functional * tests. */ - @AlaSecured("ROLE_ADMIN") + @RequireAuth(["ROLE_ADMIN"]) def triggerAccessExpiryJob() { new AccessExpiryJob( permissionService: permissionService, diff --git a/grails-app/controllers/au/org/ala/ecodata/ApiKeyInterceptor.groovy b/grails-app/controllers/au/org/ala/ecodata/ApiKeyInterceptor.groovy index 219a66998..500eb825d 100644 --- a/grails-app/controllers/au/org/ala/ecodata/ApiKeyInterceptor.groovy +++ b/grails-app/controllers/au/org/ala/ecodata/ApiKeyInterceptor.groovy @@ -1,6 +1,6 @@ package au.org.ala.ecodata -import au.org.ala.web.AlaSecured +import au.org.ala.ws.security.RequireAuth import grails.converters.JSON class ApiKeyInterceptor { @@ -69,8 +69,8 @@ class ApiKeyInterceptor { } else { - // Allow migration to the AlaSecured annotation. - if (!controllerClass?.isAnnotationPresent(AlaSecured) && !method?.isAnnotationPresent(AlaSecured)) { + // Allow migration to the RequireAuth annotation. + if (!controllerClass?.isAnnotationPresent(RequireAuth) && !method?.isAnnotationPresent(RequireAuth)) { def whiteList = buildWhiteList() def clientIp = getClientIP(request) def ipOk = checkClientIp(clientIp, whiteList) diff --git a/grails-app/controllers/au/org/ala/ecodata/AuditInterceptor.groovy b/grails-app/controllers/au/org/ala/ecodata/AuditInterceptor.groovy index f753ecec7..56169073a 100644 --- a/grails-app/controllers/au/org/ala/ecodata/AuditInterceptor.groovy +++ b/grails-app/controllers/au/org/ala/ecodata/AuditInterceptor.groovy @@ -2,11 +2,14 @@ package au.org.ala.ecodata import grails.core.support.GrailsConfigurationAware import grails.config.Config +import org.springframework.security.core.GrantedAuthority +import org.springframework.security.core.authority.SimpleGrantedAuthority +import org.springframework.security.oauth2.core.user.OAuth2UserAuthority class AuditInterceptor implements GrailsConfigurationAware { String httpRequestHeaderForUserId - def userService + UserService userService public AuditInterceptor() { matchAll() @@ -15,7 +18,7 @@ class AuditInterceptor implements GrailsConfigurationAware { boolean before() { // userId is set from either the request param userId or failing that it tries to get it from // the UserPrincipal (assumes ecodata is being accessed directly via admin page) - def userId = request.getHeader(httpRequestHeaderForUserId)?: request.getUserPrincipal()?.attributes?.userid + def userId = request.getHeader(httpRequestHeaderForUserId)?: request.getUserPrincipal()?.principal?.attributes?.id if (userId) { def userDetails = userService.setCurrentUser(userId) if (userDetails) { diff --git a/grails-app/controllers/au/org/ala/ecodata/GraphqlInterceptor.groovy b/grails-app/controllers/au/org/ala/ecodata/GraphqlInterceptor.groovy index ffdab83d8..317b4f378 100644 --- a/grails-app/controllers/au/org/ala/ecodata/GraphqlInterceptor.groovy +++ b/grails-app/controllers/au/org/ala/ecodata/GraphqlInterceptor.groovy @@ -1,6 +1,5 @@ package au.org.ala.ecodata -import au.org.ala.web.AuthService import grails.converters.JSON import au.org.ala.web.UserDetails @@ -8,19 +7,17 @@ class GraphqlInterceptor { UserService userService PermissionService permissionService - AuthService authService GraphqlInterceptor() { match uri: '/graphql/**' } boolean before() { - String userName = request.getHeader(grailsApplication.config.app.http.header.userId) ?: - request.cookies.find { it.name == 'ALA-Auth' }?.value + String userName = request.getUserPrincipal()?.principal?.attributes?.id if (userName) { //test to see that the user is valid - UserDetails user = authService.getUserForEmailAddress(userName) + UserDetails user = userService.getUserForUserId(userName) if(!user){ accessDeniedError('Invalid GrapqhQl API usage: Access denied, userId: ' + userName) diff --git a/grails-app/controllers/au/org/ala/ecodata/MetadataController.groovy b/grails-app/controllers/au/org/ala/ecodata/MetadataController.groovy index 80d21e669..289c7551f 100644 --- a/grails-app/controllers/au/org/ala/ecodata/MetadataController.groovy +++ b/grails-app/controllers/au/org/ala/ecodata/MetadataController.groovy @@ -2,7 +2,7 @@ package au.org.ala.ecodata import au.org.ala.ecodata.metadata.OutputMetadata import au.org.ala.ecodata.metadata.OutputUploadTemplateBuilder -import au.org.ala.web.AlaSecured +import au.org.ala.ws.security.RequireAuth import grails.converters.JSON import org.springframework.web.multipart.MultipartFile @@ -25,7 +25,7 @@ class MetadataController { } @RequireApiKey - @AlaSecured("ROLE_ADMIN") + @RequireAuth(["ROLE_ADMIN"]) def updateProgramsModel() { def model = request.JSON metadataService.updateProgramsModel(model.model.toString(4)) diff --git a/grails-app/controllers/au/org/ala/ecodata/SearchController.groovy b/grails-app/controllers/au/org/ala/ecodata/SearchController.groovy index 3909de8c0..3a9b09ce2 100644 --- a/grails-app/controllers/au/org/ala/ecodata/SearchController.groovy +++ b/grails-app/controllers/au/org/ala/ecodata/SearchController.groovy @@ -2,7 +2,6 @@ package au.org.ala.ecodata import au.org.ala.ecodata.command.UserSummaryReportCommand import au.org.ala.ecodata.reporting.* -import au.org.ala.web.AlaSecured import grails.converters.JSON import grails.web.servlet.mvc.GrailsParameterMap import groovy.json.JsonSlurper diff --git a/grails-app/controllers/au/org/ala/ecodata/UserController.groovy b/grails-app/controllers/au/org/ala/ecodata/UserController.groovy index 65a62ee90..9e8a0472f 100644 --- a/grails-app/controllers/au/org/ala/ecodata/UserController.groovy +++ b/grails-app/controllers/au/org/ala/ecodata/UserController.groovy @@ -1,7 +1,6 @@ package au.org.ala.ecodata import au.org.ala.ecodata.command.HubLoginTime -import au.org.ala.web.AuthService import grails.converters.JSON class UserController { diff --git a/grails-app/init/au/org/ala/ecodata/SecurityConfig.groovy b/grails-app/init/au/org/ala/ecodata/SecurityConfig.groovy new file mode 100644 index 000000000..a21ce2220 --- /dev/null +++ b/grails-app/init/au/org/ala/ecodata/SecurityConfig.groovy @@ -0,0 +1,62 @@ +package au.org.ala.ecodata + +import au.org.ala.ws.security.AlaWebServiceAuthFilter +import org.springframework.beans.factory.annotation.Value +import org.springframework.boot.autoconfigure.security.oauth2.client.servlet.OAuth2ClientRegistrationRepositoryConfiguration +import org.springframework.context.annotation.Configuration +import org.springframework.context.annotation.Import +import org.springframework.core.annotation.Order +import org.springframework.security.config.annotation.web.builders.HttpSecurity +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity +import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter +import org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler +import org.springframework.security.web.authentication.logout.LogoutFilter + +import javax.inject.Inject + +@Configuration +@EnableWebSecurity +@Import([OAuth2ClientRegistrationRepositoryConfiguration.class]) // Needed because we disabled the autoconfiguration +@Order(1) +class SecurityConfig extends WebSecurityConfigurerAdapter { + + @Value('${spring.security.logoutUrl:"http://dev.ala.org.au:8080/logout"}') + String logoutUrl + + @Inject + AlaWebServiceAuthFilter alaWebServiceAuthFilter + + @Override + protected void configure(HttpSecurity http) throws Exception { + + http.addFilterBefore(alaWebServiceAuthFilter, LogoutFilter) + http.authorizeRequests() + .antMatchers( + "/", + "/public/**", + "/css/**", + "/assets/**", + "/messages/**", + "/i18n/**", + "/static/**", + "/images/**", + "/js/**", + "/ws/**" + ).permitAll() + .anyRequest() + .authenticated() + .and() + .oauth2Login() + .successHandler(new SavedRequestAwareAuthenticationSuccessHandler()) + .userInfoEndpoint() + .and() + .and() + .logout() + .logoutUrl(logoutUrl) + .invalidateHttpSession(true) + .clearAuthentication(true) + .deleteCookies("JSESSIONID").permitAll() + .and().csrf().disable() + + } +} \ No newline at end of file diff --git a/grails-app/services/au/org/ala/ecodata/MetadataService.groovy b/grails-app/services/au/org/ala/ecodata/MetadataService.groovy index 9db7ac004..65e38fdcd 100644 --- a/grails-app/services/au/org/ala/ecodata/MetadataService.groovy +++ b/grails-app/services/au/org/ala/ecodata/MetadataService.groovy @@ -4,6 +4,7 @@ import au.org.ala.ecodata.metadata.OutputMetadata import au.org.ala.ecodata.metadata.ProgramsModel import au.org.ala.ecodata.reporting.XlsExporter import grails.converters.JSON +import grails.core.GrailsApplication import grails.validation.ValidationException import org.apache.poi.ss.usermodel.Workbook import org.apache.poi.ss.usermodel.WorkbookFactory @@ -29,8 +30,9 @@ class MetadataService { private static final List IGNORE_DATA_TYPES = ['lookupByDiscreteValues', 'lookupRange'] private static final String SERVICES_KEY = "services.config" - def grailsApplication, webService, cacheService, messageSource, emailService, userService, commonService + def webService, cacheService, messageSource, emailService, userService, commonService SettingService settingService + GrailsApplication grailsApplication /** * @deprecated use versioned API to retrieve activity form definitions diff --git a/grails-app/services/au/org/ala/ecodata/PermissionService.groovy b/grails-app/services/au/org/ala/ecodata/PermissionService.groovy index 0baf5e36d..549c61608 100644 --- a/grails-app/services/au/org/ala/ecodata/PermissionService.groovy +++ b/grails-app/services/au/org/ala/ecodata/PermissionService.groovy @@ -1,7 +1,5 @@ package au.org.ala.ecodata -import au.org.ala.web.AuthService -import au.org.ala.web.CASRoles import grails.gorm.DetachedCriteria import org.grails.datastore.mapping.query.api.BuildableCriteria @@ -12,7 +10,6 @@ import static au.org.ala.ecodata.Status.DELETED class PermissionService { static transactional = false - AuthService authService UserService userService // found in ala-auth-plugin ProjectController projectController def grailsApplication, webService, hubService @@ -21,7 +18,7 @@ class PermissionService { static final int MAX_QUERY_RESULT_SIZE = 1000 boolean isUserAlaAdmin(String userId) { - userId && userService.getRolesForUser(userId)?.contains(CASRoles.ROLE_ADMIN) + userId && userService.getRolesForUser(userId)?.contains("ROLE_ADMIN") } public boolean isUserAdminForProject(String userId, String projectId) { @@ -150,7 +147,7 @@ class PermissionService { } def addUserAsEditorToProject(currentUserId, targetUserId, projectId) { - if ((isUserAdminForProject(currentUserId, projectId) || authService.userInRole("ROLE_ADMIN")) && targetUserId) { + if ((isUserAdminForProject(currentUserId, projectId) || userService.userInRole("ROLE_ADMIN")) && targetUserId) { addUserAsRoleToProject(targetUserId, AccessLevel.editor, projectId) } } @@ -199,7 +196,7 @@ class PermissionService { out.put(it.userId,rec); } - def userList = authService.getUserDetailsById(userIds) + def userList = userService.getUserDetailsById(userIds) if (userList) { def users = userList['users'] @@ -246,7 +243,7 @@ class PermissionService { } - def userList = authService.getUserDetailsById(userIds) + def userList = userService.getUserDetailsById(userIds) if (userList) { def users = userList['users'] @@ -318,7 +315,7 @@ class PermissionService { out.put(it.userId,toMap(it,false)) } - def userList = authService.getUserDetailsById(userIds) + def userList = userService.getUserDetailsById(userIds) if (userList) { def users = userList['users'] diff --git a/grails-app/services/au/org/ala/ecodata/ProjectService.groovy b/grails-app/services/au/org/ala/ecodata/ProjectService.groovy index ce5f094e0..076fcc498 100644 --- a/grails-app/services/au/org/ala/ecodata/ProjectService.groovy +++ b/grails-app/services/au/org/ala/ecodata/ProjectService.groovy @@ -2,6 +2,7 @@ package au.org.ala.ecodata import au.org.ala.ecodata.converter.SciStarterConverter import grails.converters.JSON +import grails.core.GrailsApplication import groovy.json.JsonSlurper import org.codehaus.jackson.map.ObjectMapper import org.springframework.context.MessageSource @@ -25,7 +26,7 @@ class ProjectService { static final ENHANCED = 'enhanced' static final PRIVATE_SITES_REMOVED = 'privatesitesremoved' - def grailsApplication + GrailsApplication grailsApplication MessageSource messageSource SessionLocaleResolver localeResolver SiteService siteService diff --git a/grails-app/services/au/org/ala/ecodata/RecordService.groovy b/grails-app/services/au/org/ala/ecodata/RecordService.groovy index 59741836f..516e0566a 100644 --- a/grails-app/services/au/org/ala/ecodata/RecordService.groovy +++ b/grails-app/services/au/org/ala/ecodata/RecordService.groovy @@ -1,7 +1,6 @@ package au.org.ala.ecodata import au.com.bytecode.opencsv.CSVWriter -import au.org.ala.web.AuthService import grails.converters.JSON import groovy.json.JsonSlurper import org.apache.commons.io.FileUtils @@ -33,7 +32,6 @@ class RecordService { OutputService outputService ProjectService projectService SiteService siteService - AuthService authService UserService userService RecordAlertService recordAlertService SensitiveSpeciesService sensitiveSpeciesService @@ -315,7 +313,7 @@ class RecordService { def userDetails = userService.getCurrentUserDetails() if (!userDetails && json.userId) { - userDetails = authService.getUserForUserId(json.userId) + userDetails = userService.getUserForUserId(json.userId) } if (!userDetails) { diff --git a/grails-app/services/au/org/ala/ecodata/ReportService.groovy b/grails-app/services/au/org/ala/ecodata/ReportService.groovy index cf56931c2..703a36956 100644 --- a/grails-app/services/au/org/ala/ecodata/ReportService.groovy +++ b/grails-app/services/au/org/ala/ecodata/ReportService.groovy @@ -1,7 +1,6 @@ package au.org.ala.ecodata import au.org.ala.ecodata.reporting.* -import au.org.ala.web.AuthService import groovy.util.logging.Slf4j import org.elasticsearch.action.search.SearchResponse import org.elasticsearch.search.SearchHit @@ -22,7 +21,6 @@ class ReportService { OutputService outputService MetadataService metadataService UserService userService - AuthService authService def findScoresByLabel(List labels) { Score.findAllByLabelInList(labels) @@ -310,7 +308,7 @@ class ReportService { private Map lookupUserDetails(List userIds) { - def userList = authService.getUserDetailsById(userIds) + def userList = userService.getUserDetailsById(userIds) userList?.users } diff --git a/grails-app/services/au/org/ala/ecodata/UserService.groovy b/grails-app/services/au/org/ala/ecodata/UserService.groovy index 3a5e352db..4b7b22f27 100644 --- a/grails-app/services/au/org/ala/ecodata/UserService.groovy +++ b/grails-app/services/au/org/ala/ecodata/UserService.groovy @@ -1,7 +1,17 @@ package au.org.ala.ecodata -import au.org.ala.web.AuthService +import au.org.ala.userdetails.UserDetailsClient +import au.org.ala.userdetails.UserDetailsFromIdListRequest +import au.org.ala.ws.security.AuthService +import com.squareup.moshi.Moshi +import com.squareup.moshi.Rfc3339DateJsonAdapter import grails.core.GrailsApplication +import grails.plugin.cache.Cacheable +import okhttp3.OkHttpClient + +import javax.annotation.PostConstruct + +import static java.util.concurrent.TimeUnit.MILLISECONDS class UserService { @@ -9,15 +19,31 @@ class UserService { AuthService authService WebService webService GrailsApplication grailsApplication + UserService userService + UserDetailsClient userDetailsClient /** Limit to the maximum number of Users returned by queries */ static final int MAX_QUERY_RESULT_SIZE = 1000 private static ThreadLocal _currentUser = new ThreadLocal() + @PostConstruct + void initialiseUserDetailsClient() { + Integer readTimeout = 3000 + OkHttpClient userDetailsHttpClient = new OkHttpClient.Builder().readTimeout(readTimeout, MILLISECONDS).build() + Moshi moshi = new Moshi.Builder().add(Date, new Rfc3339DateJsonAdapter().nullSafe()).build() + String baseUrl = grailsApplication.config.getProperty("userDetails.url") + userDetailsClient = new UserDetailsClient.Builder(userDetailsHttpClient, baseUrl).moshi(moshi).build() + } + def getCurrentUserDisplayName() { - def currentUser = _currentUser.get() - return currentUser ? currentUser.displayName : "" + String displayName = authService.displayName + if (!displayName) { + def currentUser = _currentUser.get() + displayName = currentUser ? currentUser.displayName : "" + } + + displayName } /** @@ -53,16 +79,31 @@ class UserService { */ def getRolesForUser(String userId = null) { userId = userId ?: getCurrentUserDetails().userId - authService.getUserForUserId(userId, true)?.roles ?: [] + getUserForUserId(userId, true)?.roles ?: [] + } + + def userInRole(Object role){ + return authService.userInRole(role) } - synchronized def getUserForUserId(String userId) { - if (!userId) { - return null + @Cacheable("userDetailsCache") + synchronized def getUserForUserId(String userId, boolean includeProps = true) { + if (!userId) return null // this would have failed anyway + def call = userDetailsClient.getUserDetails(userId, includeProps) + try { + def response = call.execute() + if (response.successful) { + return response.body() + } else { + log.warn("Failed to retrieve user details for userId: $userId, includeProps: $includeProps. Error was: ${response.message()}") + } + } catch (Exception ex) { + log.error("Exception caught trying get find user details for $userId.", ex) } - return authService.getUserForUserId(userId) + return null } + /** * This method gets called by a filter at the beginning of the request (if a userId parameter is on the URL) * It sets the user details in a thread local for extraction by the audit service. @@ -180,4 +221,20 @@ class UserService { } }.list(options) } + + @Cacheable("userDetailsByIdCache") + def getUserDetailsById(List userIds, boolean includeProps = true) { + def call = userDetailsClient.getUserDetailsFromIdList(new UserDetailsFromIdListRequest(userIds, includeProps)) + try { + def response = call.execute() + if (response.successful) { + return response.body() + } else { + log.warn("Failed to retrieve user details. Error was: ${response.message()}") + } + } catch (Exception e) { + log.error("Exception caught retrieving userdetails for ${userIds}", e) + } + return null + } } diff --git a/grails-app/taglib/au/org/ala/ecodata/ECTagLib.groovy b/grails-app/taglib/au/org/ala/ecodata/ECTagLib.groovy index 68dfa9358..8045ccabc 100644 --- a/grails-app/taglib/au/org/ala/ecodata/ECTagLib.groovy +++ b/grails-app/taglib/au/org/ala/ecodata/ECTagLib.groovy @@ -6,7 +6,7 @@ class ECTagLib { static namespace = "ec" - def userService, authService, metadataService + def userService, metadataService /** * @attr active @@ -33,14 +33,7 @@ class ECTagLib { def mb = new MarkupBuilder(out) mb.span(class:'username nav-text') { - def displayName = authService.displayName - if (displayName) { - mkp.yield(displayName) - } else if (request.userPrincipal) { - mkp.yield(request.userPrincipal) - } else { - mkp.yield(userService.currentUserDisplayName) - } + mkp.yield(userService.currentUserDisplayName) } } @@ -50,13 +43,13 @@ class ECTagLib { * @attr role REQUIRED */ def userInRole = { attrs -> - if (authService.userInRole(attrs.role)) { + if (userService.userInRole(attrs.role)) { out << true } } def currentUserId = { attrs, body -> - out << authService.userDetails()?.userId + out << userService._currentUser()?.userId } /** diff --git a/grails-app/views/layouts/adminLayout.gsp b/grails-app/views/layouts/adminLayout.gsp index e474eed74..f9e70d1d5 100644 --- a/grails-app/views/layouts/adminLayout.gsp +++ b/grails-app/views/layouts/adminLayout.gsp @@ -61,7 +61,7 @@