From 09fc040f4a447861d863b3099f54de53a17beccc Mon Sep 17 00:00:00 2001 From: jonathan zollinger Date: Fri, 6 Mar 2026 10:33:31 -0700 Subject: [PATCH 1/8] feat: add graph post endpoint and pojos --- core/src/main/resources/schema.yml | 219 +++++++++++++++++++++++++++++ 1 file changed, 219 insertions(+) diff --git a/core/src/main/resources/schema.yml b/core/src/main/resources/schema.yml index 0122c27..50db567 100644 --- a/core/src/main/resources/schema.yml +++ b/core/src/main/resources/schema.yml @@ -12,6 +12,23 @@ info: - Organization - Project paths: + /graphql: + post: + tags: [ GraphQL ] + description: GraphQL API endpoint + operationId: graphqlEndpoint + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/GraphQLRequest' + responses: + '200': + description: OK + content: + application/graphql-response+json; charset=utf-8: + schema: + $ref: '#/components/schemas/GraphQLResponse' /api/v1/images: post: tags: [ Image ] @@ -339,6 +356,208 @@ paths: components: schemas: + GraphQLRequest: + type: object + properties: + query: { type: string } + variables: { type: object } + GraphQLResponse: + type: object + properties: + data: + type: object + oneOf: + - $ref: '#/components/schemas/GraphQLCreateEventData' + - $ref: '#/components/schemas/GraphQLSearchOrganizationData' + - $ref: '#/components/schemas/GraphQLUpdateProjectData' + - $ref: '#/components/schemas/GraphQLSetProjectLocationData' + - $ref: '#/components/schemas/GraphQLAddProjectAttachmentData' + - $ref: '#/components/schemas/GraphQLAddProjectOrganizationData' + - $ref: '#/components/schemas/GraphQLCombinedMutationUpdateProjectAddProjectTagData' + - $ref: '#/components/schemas/GraphQLCreateProjectData' + - $ref: '#/components/schemas/GraphQLPublishProjectData' + - $ref: '#/components/schemas/GraphQLUpdateProjectAttachmentData' + - $ref: '#/components/schemas/GraphQLUpdateProjectListingData' + errors: + type: array + items: + type: object + GraphQLCreateEventData: + type: object + properties: + createEvent: + type: object + properties: + id: { type: string, format: uuid } + projectId: { type: string, format: uuid } + contactEmail: { type: string } + contactName: { type: string } + contactPhone: { type: string } + start: { type: string, format: date-time } + end: { type: string, format: date-time } + groupCap: { type: boolean } + groupLimit: { type: integer, format: int32 } + timezone: { type: string } + totalVolunteersNeeded: { type: integer, format: int32 } + volunteerCap: { type: boolean } + GraphQLSearchOrganizationData: + type: object + properties: + adminOrganizationSearchByTitle: + type: array + items: + type: object + properties: + id: { type: string, format: uuid } + name: { type: string } + logo: { type: string } + description: { type: string } + contactName: { type: string } + contactPhone: { type: string } + contactEmail: { type: string } + url: { type: string } + location: + type: object + properties: + displayCity: { type: string } + displayState: { type: string } + GraphQLUpdateProjectData: + type: object + properties: + updateProject: + type: object + properties: + id: { type: string, format: uuid } + logo: { type: string } + GraphQLSetProjectLocationData: + type: object + properties: + setProjectLocation: + type: object + properties: + displayAddress: { type: string } + displayAddress2: { type: string, nullable: true } + displayCity: { type: string } + displayCountry: { type: string } + displayCountryCode: { type: string } + displayCounty: { type: string } + displayNeighborhood: { type: string, nullable: true } + displayPostalCode: { type: string } + displayState: { type: string } + id: { type: string, format: uuid } + latitude: { type: number, format: double } + locationDetails: { type: string, nullable: true } + locationName: { type: string } + longitude: { type: number, format: double } + maxLatitude: { type: number, format: double } + maxLongitude: { type: number, format: double } + minLatitude: { type: number, format: double } + minLongitude: { type: number, format: double } + timezone: { type: string } + civicGeography: + type: object + properties: + state: + type: object + properties: + code: { type: string, nullable: true } + churchGeography: + type: object + properties: + areaUnitId: { type: string } + ccUnitId: { type: string } + missionUnitId: { type: string } + stakeUnitId: { type: string } + GraphQLAddProjectAttachmentData: + type: object + properties: + addProjectAttachment: + type: object + properties: + attachmentId: { type: string, format: uuid } + GraphQLAddProjectOrganizationData: + type: object + properties: + addProjectOrganization: + type: object + properties: + id: { type: string, format: uuid } + organizations: + type: array + items: + type: object + properties: + id: { type: string, format: uuid } + name: { type: string } + GraphQLCombinedMutationUpdateProjectAddProjectTagData: + type: object + properties: + updateProject: + type: object + properties: + id: { type: string, format: uuid } + wheelchairAccessible: { type: boolean } + itemDonations: { type: boolean } + indoors: { type: boolean } + longDescription: { type: string } + shortDescription: { type: string } + sponsorUserId: { type: string, format: uuid } + groupProjects: { type: boolean } + additionalProperties: + type: object + properties: + id: { type: string, format: uuid } + tags: + type: array + items: + type: object + properties: + id: { type: integer, format: int32 } + tagType: { type: string } + tagTypeId: { type: integer, format: int32 } + translations: + type: array + items: + type: object + properties: + description: { type: string, nullable: true } + label: { type: string } + languageId: { type: integer, format: int32 } + GraphQLCreateProjectData: + type: object + properties: + createProject: + type: object + properties: + id: { type: string, format: uuid } + title: { type: string } + typeId: { type: integer, format: int32 } + locationTypeId: { type: integer, format: int32 } + externalVolunteerUrl: { type: string, nullable: true } + statusId: { type: integer, format: int32 } + GraphQLPublishProjectData: + type: object + properties: + publishProject: + type: object + properties: + id: { type: string, format: uuid } + statusId: { type: integer, format: int32 } + GraphQLUpdateProjectAttachmentData: + type: object + properties: + updateProjectAttachment: + type: object + properties: + attachmentId: { type: string, format: uuid } + GraphQLUpdateProjectListingData: + type: object + properties: + updateProjectListing: + type: object + properties: + id: { type: string, format: uuid } + unlisted: { type: boolean } BoundaryUpdateRequest: type: object properties: From b81c66172591b0fd4f54e956409086c123e265b2 Mon Sep 17 00:00:00 2001 From: jonathan zollinger Date: Fri, 6 Mar 2026 10:33:48 -0700 Subject: [PATCH 2/8] feat: add event type enum --- core/src/main/resources/schema.yml | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/core/src/main/resources/schema.yml b/core/src/main/resources/schema.yml index 50db567..e2be48a 100644 --- a/core/src/main/resources/schema.yml +++ b/core/src/main/resources/schema.yml @@ -1142,6 +1142,16 @@ components: deletedOn: { type: string, format: date-time, nullable: true } deletedBy: { type: string, format: uuid, nullable: true } additionalProperties: false + EventType: + type: integer + format: int32 + enum: [ 0, 1, 2, 3, 4 ] + x-enum-varnames: + - "None" + - "DTL" + - "Ongoing" + - "Recurring" + - "MultipleDTL" OrgRepresentative: type: object properties: From 5d307f187b98555dde9dfd36a4f519424e40e483 Mon Sep 17 00:00:00 2001 From: jonathan zollinger Date: Fri, 6 Mar 2026 11:49:17 -0700 Subject: [PATCH 3/8] feat: add several project creation methods --- .../org/justserve/client/GraphQLClient.java | 137 +++++++++ .../org/justserve/client/GraphQLRequest.java | 18 ++ .../org/justserve/client/GraphQLResponse.java | 23 ++ core/src/main/resources/schema.yml | 269 +++++++++++++++--- .../org/justserve/GraphQLClientSpec.groovy | 20 ++ 5 files changed, 424 insertions(+), 43 deletions(-) create mode 100644 core/src/main/java/org/justserve/client/GraphQLClient.java create mode 100644 core/src/main/java/org/justserve/client/GraphQLRequest.java create mode 100644 core/src/main/java/org/justserve/client/GraphQLResponse.java create mode 100644 core/src/test/groovy/org/justserve/GraphQLClientSpec.groovy diff --git a/core/src/main/java/org/justserve/client/GraphQLClient.java b/core/src/main/java/org/justserve/client/GraphQLClient.java new file mode 100644 index 0000000..4b0d794 --- /dev/null +++ b/core/src/main/java/org/justserve/client/GraphQLClient.java @@ -0,0 +1,137 @@ +package org.justserve.client; + +import io.micronaut.http.annotation.Body; +import io.micronaut.http.annotation.Consumes; +import io.micronaut.http.annotation.Post; +import io.micronaut.http.annotation.Produces; +import io.micronaut.http.client.annotation.Client; +import io.micronaut.retry.annotation.Retryable; +import org.justserve.model.*; + +@Produces("application/json") +@Consumes("application/graphql-response+json; charset=utf-8") +@Retryable +@Client("/graphql") +public interface GraphQLClient { + + @Post + GraphQLResponse addProjectAttachment(@Body GraphQLAddProjectAttachmentRequest request); + + @Post + GraphQLResponse addProjectOrganization(@Body GraphQLAddProjectOrganizationRequest request); + + @Post + GraphQLResponse combinedMutationUpdateProjectAddProjectTag(@Body GraphQLCombinedMutationUpdateProjectAddProjectTagRequest request); + + @Post + GraphQLResponse createEvent(@Body GraphQLCreateEventRequest request); + + @Post + GraphQLResponse createProject(@Body GraphQLCreateProjectRequest request); + + @Post + GraphQLResponse publishProject(@Body GraphQLPublishProjectRequest request); + + @Post + GraphQLResponse searchOrganization(@Body GraphQLSearchOrganizationRequest request); + + @Post + GraphQLResponse setProjectLocation(@Body GraphQLSetProjectLocationRequest request); + + @Post + GraphQLResponse updateProjectAttachment(@Body GraphQLUpdateProjectAttachmentRequest request); + + @Post + GraphQLResponse updateProjectListing(@Body GraphQLUpdateProjectListingRequest request); + + @Post + GraphQLResponse updateProject(@Body GraphQLUpdateProjectRequest request); + + default GraphQLResponse addProjectAttachment(GraphQLAddProjectAttachmentVariables variables) { + String fixedQuery = "mutation ($projectId: ID!, $attachmentId: ID!) {\n addProjectAttachment(projectId: $projectId, attachmentId: $attachmentId) {\n attachmentId\n }\n }"; + GraphQLAddProjectAttachmentRequest request = new GraphQLAddProjectAttachmentRequest(); + request.setQuery(fixedQuery); + request.setVariables(variables); + return this.addProjectAttachment(request); + } + + default GraphQLResponse addProjectOrganization(GraphQLAddProjectOrganizationVariables variables) { + String fixedQuery = "mutation addProjectOrganization($organizationId: ID!, $projectId: ID!) {\n addProjectOrganization(organizationId: $organizationId, projectId: $projectId) {\n id\n organizations {\n id\n name\n }\n }\n }"; + GraphQLAddProjectOrganizationRequest request = new GraphQLAddProjectOrganizationRequest(); + request.setQuery(fixedQuery); + request.setVariables(variables); + return this.addProjectOrganization(request); + } + + default GraphQLResponse combinedMutationUpdateProjectAddProjectTag(GraphQLCombinedMutationUpdateProjectAddProjectTagVariables variables) { + String fixedQuery = "mutation combinedMutation($projectId: ID!, $modify: UpdateProjectInput!) {\n updateProject(\n id: $projectId,\n modify: $modify\n ) {\n id\n wheelchairAccessible\n itemDonations\n indoors\n longDescription\n shortDescription\n sponsorUserId\n groupProjects\n }\n\n skill0: addProjectTag(\n projectId: $projectId\n tagId: 31\n ) {\n id\n tags {\n id\n tagType\n tagTypeId\n translations(languageId: 1) {\n description\n label\n languageId\n }\n }\n }\nskill1: addProjectTag(\n projectId: $projectId\n tagId: 46\n ) {\n id\n tags {\n id\n tagType\n tagTypeId\n translations(languageId: 1) {\n description\n label\n languageId\n }\n }\n }\n\n interest0: addProjectTag(\n projectId: $projectId\n tagId: 11\n ) {\n id\n tags {\n id\n tagType\n tagTypeId\n translations(languageId: 1) {\n description\n label\n languageId\n }\n }\n }\ninterest1: addProjectTag(\n projectId: $projectId\n tagId: 26\n ) {\n id\n tags {\n id\n tagType\n tagTypeId\n translations(languageId: 1) {\n description\n label\n languageId\n }\n }\n }\n }"; + GraphQLCombinedMutationUpdateProjectAddProjectTagRequest request = new GraphQLCombinedMutationUpdateProjectAddProjectTagRequest(); + request.setQuery(fixedQuery); + request.setVariables(variables); + return this.combinedMutationUpdateProjectAddProjectTag(request); + } + + default GraphQLResponse createEvent(GraphQLCreateEventVariables variables) { + String fixedQuery = "mutation createEvent($projectId: ID!, $projectEvent: UpdateProjectEventInput!) {\n createEvent(\n projectId: $projectId\n projectEvent: $projectEvent\n ) {\n id\n projectId\n contactEmail\n contactName\n contactPhone\n start\n end\n groupCap\n groupLimit\n timezone\n totalVolunteersNeeded\n volunteerCap\n }\n }"; + GraphQLCreateEventRequest request = new GraphQLCreateEventRequest(); + request.setQuery(fixedQuery); + request.setVariables(variables); + return this.createEvent(request); + } + + default GraphQLResponse createProject(GraphQLCreateProjectVariables variables) { + String fixedQuery = "mutation createProject($title: String!, $eventType: ProjectType!, $locationType: ProjectLocationType!, $redirect: String) {\n createProject(\n title: $title\n eventType: $eventType\n locationType: $locationType\n redirect: $redirect\n ) {\n id\n title\n typeId\n locationTypeId\n externalVolunteerUrl\n statusId\n }\n }"; + GraphQLCreateProjectRequest request = new GraphQLCreateProjectRequest(); + request.setQuery(fixedQuery); + request.setVariables(variables); + return this.createProject(request); + } + + default GraphQLResponse publishProject(GraphQLPublishProjectVariables variables) { + String fixedQuery = "mutation ($projectId: ID!){\n publishProject(projectId: $projectId) {\n id\n statusId\n }\n }"; + GraphQLPublishProjectRequest request = new GraphQLPublishProjectRequest(); + request.setQuery(fixedQuery); + request.setVariables(variables); + return this.publishProject(request); + } + + default GraphQLResponse searchOrganization(GraphQLSearchOrganizationVariables variables) { + String fixedQuery = "\n query organization(\n $searchTerm: String!\n $includeAll: Boolean\n $activeOnly: Boolean\n ) {\n adminOrganizationSearchByTitle(\n activeOnly: $activeOnly\n includeAll: $includeAll\n title: $searchTerm\n ) {\n id\n name\n logo\n description\n contactName\n contactPhone\n contactEmail\n url\n location {\n displayCity\n displayState\n }\n }\n }\n "; + GraphQLSearchOrganizationRequest request = new GraphQLSearchOrganizationRequest(); + request.setQuery(fixedQuery); + request.setVariables(variables); + return this.searchOrganization(request); + } + + default GraphQLResponse setProjectLocation(GraphQLSetProjectLocationVariables variables) { + String fixedQuery = "mutation setProjectLocation($projectId: ID!, $location: String, $locationData: LocationDataInput) {\n setProjectLocation(\n projectId: $projectId\n location: $location\n locationData: $locationData\n ) {\n displayAddress\n displayAddress2\n displayCity\n displayCountry\n displayCountryCode\n displayCounty\n displayNeighborhood\n displayPostalCode\n displayState\n id\n latitude\n locationDetails\n locationName\n longitude\n maxLatitude\n maxLongitude\n minLatitude\n minLongitude\n timezone\n civicGeography {\n state {\n code\n }\n }\n churchGeography {\n areaUnitId\n ccUnitId\n missionUnitId\n stakeUnitId\n }\n }\n }"; + GraphQLSetProjectLocationRequest request = new GraphQLSetProjectLocationRequest(); + request.setQuery(fixedQuery); + request.setVariables(variables); + return this.setProjectLocation(request); + } + + default GraphQLResponse updateProjectAttachment(GraphQLUpdateProjectAttachmentVariables variables) { + String fixedQuery = "mutation ($attachmentId: ID!, $title: String!, $description: String!) {\n updateProjectAttachment(attachmentId: $attachmentId, title: $title, description: $description) {\n attachmentId\n }\n }"; + GraphQLUpdateProjectAttachmentRequest request = new GraphQLUpdateProjectAttachmentRequest(); + request.setQuery(fixedQuery); + request.setVariables(variables); + return this.updateProjectAttachment(request); + } + + default GraphQLResponse updateProjectListing(GraphQLUpdateProjectListingVariables variables) { + String fixedQuery = "mutation listing ($projectId: ID!, $unlisted: Boolean!) {\n updateProjectListing(projectId: $projectId, unlisted: $unlisted) {\n id\n unlisted\n }\n }"; + GraphQLUpdateProjectListingRequest request = new GraphQLUpdateProjectListingRequest(); + request.setQuery(fixedQuery); + request.setVariables(variables); + return this.updateProjectListing(request); + } + + default GraphQLResponse updateProject(GraphQLUpdateProjectVariables variables) { + String fixedQuery = "mutation ($projectId: ID!, $logo: String!) {\n updateProject(id: $projectId, modify: { logo: $logo }) {\n id\n logo\n }\n }"; + GraphQLUpdateProjectRequest request = new GraphQLUpdateProjectRequest(); + request.setQuery(fixedQuery); + request.setVariables(variables); + return this.updateProject(request); + } +} diff --git a/core/src/main/java/org/justserve/client/GraphQLRequest.java b/core/src/main/java/org/justserve/client/GraphQLRequest.java new file mode 100644 index 0000000..10c2452 --- /dev/null +++ b/core/src/main/java/org/justserve/client/GraphQLRequest.java @@ -0,0 +1,18 @@ +package org.justserve.client; + +import io.micronaut.serde.annotation.Serdeable; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Serdeable +@Data +@NoArgsConstructor +@AllArgsConstructor +public class GraphQLRequest { + + private String query; + + // This handles dynamic variable maps/objects + private Object variables; +} diff --git a/core/src/main/java/org/justserve/client/GraphQLResponse.java b/core/src/main/java/org/justserve/client/GraphQLResponse.java new file mode 100644 index 0000000..31acf3c --- /dev/null +++ b/core/src/main/java/org/justserve/client/GraphQLResponse.java @@ -0,0 +1,23 @@ +package org.justserve.client; + +import io.micronaut.serde.annotation.Serdeable; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; + +@Serdeable +@Data +@NoArgsConstructor +@AllArgsConstructor +public class GraphQLResponse { + + private T data; + + private List errors; + + public boolean hasErrors() { + return errors != null && !errors.isEmpty(); + } +} diff --git a/core/src/main/resources/schema.yml b/core/src/main/resources/schema.yml index e2be48a..7b4b6e8 100644 --- a/core/src/main/resources/schema.yml +++ b/core/src/main/resources/schema.yml @@ -12,23 +12,6 @@ info: - Organization - Project paths: - /graphql: - post: - tags: [ GraphQL ] - description: GraphQL API endpoint - operationId: graphqlEndpoint - requestBody: - content: - application/json: - schema: - $ref: '#/components/schemas/GraphQLRequest' - responses: - '200': - description: OK - content: - application/graphql-response+json; charset=utf-8: - schema: - $ref: '#/components/schemas/GraphQLResponse' /api/v1/images: post: tags: [ Image ] @@ -356,32 +339,7 @@ paths: components: schemas: - GraphQLRequest: - type: object - properties: - query: { type: string } - variables: { type: object } - GraphQLResponse: - type: object - properties: - data: - type: object - oneOf: - - $ref: '#/components/schemas/GraphQLCreateEventData' - - $ref: '#/components/schemas/GraphQLSearchOrganizationData' - - $ref: '#/components/schemas/GraphQLUpdateProjectData' - - $ref: '#/components/schemas/GraphQLSetProjectLocationData' - - $ref: '#/components/schemas/GraphQLAddProjectAttachmentData' - - $ref: '#/components/schemas/GraphQLAddProjectOrganizationData' - - $ref: '#/components/schemas/GraphQLCombinedMutationUpdateProjectAddProjectTagData' - - $ref: '#/components/schemas/GraphQLCreateProjectData' - - $ref: '#/components/schemas/GraphQLPublishProjectData' - - $ref: '#/components/schemas/GraphQLUpdateProjectAttachmentData' - - $ref: '#/components/schemas/GraphQLUpdateProjectListingData' - errors: - type: array - items: - type: object + GraphQLCreateEventData: type: object properties: @@ -1988,3 +1946,228 @@ components: linked: { type: boolean } logo: { type: string, nullable: true } additionalProperties: false + GraphQLAddProjectAttachmentRequest: + type: object + properties: + query: { type: string } + variables: { $ref: '#/components/schemas/GraphQLAddProjectAttachmentVariables' } + GraphQLAddProjectAttachmentVariables: + type: object + properties: + projectId: + type: string + format: uuid + attachmentId: + type: string + format: uuid + GraphQLAddProjectOrganizationRequest: + type: object + properties: + query: { type: string } + variables: { $ref: '#/components/schemas/GraphQLAddProjectOrganizationVariables' } + GraphQLAddProjectOrganizationVariables: + type: object + properties: + organizationId: + type: string + format: uuid + projectId: + type: string + format: uuid + GraphQLCombinedMutationUpdateProjectAddProjectTagRequest: + type: object + properties: + query: { type: string } + variables: { $ref: '#/components/schemas/GraphQLCombinedMutationUpdateProjectAddProjectTagVariables' } + GraphQLCombinedMutationUpdateProjectAddProjectTagVariables: + type: object + properties: + projectId: + type: string + format: uuid + modify: + type: object + properties: + indoors: + type: boolean + longDescription: + type: string + shortDescription: + type: string + sponsorUserId: + type: string + format: uuid + suitableAllAges: + type: boolean + groupProjects: + type: boolean + itemDonations: + type: boolean + wheelchairAccessible: + type: boolean + sponsorType: + type: string + GraphQLCreateEventRequest: + type: object + properties: + query: { type: string } + variables: { $ref: '#/components/schemas/GraphQLCreateEventVariables' } + GraphQLCreateEventVariables: + type: object + properties: + projectId: + type: string + format: uuid + projectEvent: + type: object + properties: + contactEmail: + type: string + contactName: + type: string + contactPhone: + type: string + end: + type: string + groupCap: + type: boolean + groupLimit: + type: integer + shiftTitle: + type: string + start: + type: string + timezone: + type: string + totalVolunteersNeeded: + type: integer + volunteerCap: + type: boolean + GraphQLCreateProjectRequest: + type: object + properties: + query: { type: string } + variables: { $ref: '#/components/schemas/GraphQLCreateProjectVariables' } + GraphQLCreateProjectVariables: + type: object + properties: + title: + type: string + eventType: + type: string + locationType: + type: string + redirect: + type: string + GraphQLPublishProjectRequest: + type: object + properties: + query: { type: string } + variables: { $ref: '#/components/schemas/GraphQLPublishProjectVariables' } + GraphQLPublishProjectVariables: + type: object + properties: + projectId: + type: string + format: uuid + GraphQLSearchOrganizationRequest: + type: object + properties: + query: { type: string } + variables: { $ref: '#/components/schemas/GraphQLSearchOrganizationVariables' } + GraphQLSearchOrganizationVariables: + type: object + properties: + searchTerm: + type: string + includeAll: + type: boolean + GraphQLSetProjectLocationRequest: + type: object + properties: + query: { type: string } + variables: { $ref: '#/components/schemas/GraphQLSetProjectLocationVariables' } + GraphQLSetProjectLocationVariables: + type: object + properties: + projectId: + type: string + format: uuid + location: + type: string + locationData: + type: object + properties: + country: + type: string + countryCode: + type: string + state: + type: string + city: + type: string + county: + type: string + postal: + type: string + neighborhood: + type: string + latitude: + type: number + longitude: + type: number + address: + type: string + areaId: + type: string + ccId: + type: string + missionId: + type: string + stakeId: + type: string + locationDetails: + type: string + locationName: + type: string + GraphQLUpdateProjectAttachmentRequest: + type: object + properties: + query: { type: string } + variables: { $ref: '#/components/schemas/GraphQLUpdateProjectAttachmentVariables' } + GraphQLUpdateProjectAttachmentVariables: + type: object + properties: + attachmentId: + type: string + format: uuid + title: + type: string + description: + type: string + GraphQLUpdateProjectListingRequest: + type: object + properties: + query: { type: string } + variables: { $ref: '#/components/schemas/GraphQLUpdateProjectListingVariables' } + GraphQLUpdateProjectListingVariables: + type: object + properties: + projectId: + type: string + format: uuid + unlisted: + type: boolean + GraphQLUpdateProjectRequest: + type: object + properties: + query: { type: string } + variables: { $ref: '#/components/schemas/GraphQLUpdateProjectVariables' } + GraphQLUpdateProjectVariables: + type: object + properties: + projectId: + type: string + format: uuid + logo: + type: string diff --git a/core/src/test/groovy/org/justserve/GraphQLClientSpec.groovy b/core/src/test/groovy/org/justserve/GraphQLClientSpec.groovy new file mode 100644 index 0000000..e8f7d4b --- /dev/null +++ b/core/src/test/groovy/org/justserve/GraphQLClientSpec.groovy @@ -0,0 +1,20 @@ +package org.justserve + +import jakarta.inject.Inject +import org.justserve.client.GraphQLClient +import org.justserve.client.GraphQLRequest +import org.justserve.model.GraphQLCreateProjectRequest +import spock.lang.Shared +import spock.lang.Specification + +class GraphQLClientSpec extends Specification{ + + @Shared + @Inject + private GraphQLClient client; + + void "simple test"(){ + GraphQLRequest request = new GraphQLCreateProjectRequest() + client.createProject() + } +} From b032ca99950cd071a2659faa63f4545b67f29f5d Mon Sep 17 00:00:00 2001 From: jonathan zollinger Date: Fri, 6 Mar 2026 14:57:15 -0700 Subject: [PATCH 4/8] feat: add helper methods for graph calls --- .../org/justserve/client/GraphQLClient.java | 44 +++++++++---------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/core/src/main/java/org/justserve/client/GraphQLClient.java b/core/src/main/java/org/justserve/client/GraphQLClient.java index 4b0d794..4babe42 100644 --- a/core/src/main/java/org/justserve/client/GraphQLClient.java +++ b/core/src/main/java/org/justserve/client/GraphQLClient.java @@ -15,44 +15,44 @@ public interface GraphQLClient { @Post - GraphQLResponse addProjectAttachment(@Body GraphQLAddProjectAttachmentRequest request); + GraphQLResponse executeAddProjectAttachment(@Body GraphQLAddProjectAttachmentRequest request); @Post - GraphQLResponse addProjectOrganization(@Body GraphQLAddProjectOrganizationRequest request); + GraphQLResponse executeAddProjectOrganization(@Body GraphQLAddProjectOrganizationRequest request); @Post - GraphQLResponse combinedMutationUpdateProjectAddProjectTag(@Body GraphQLCombinedMutationUpdateProjectAddProjectTagRequest request); + GraphQLResponse executeCombinedMutationUpdateProjectAddProjectTag(@Body GraphQLCombinedMutationUpdateProjectAddProjectTagRequest request); @Post - GraphQLResponse createEvent(@Body GraphQLCreateEventRequest request); + GraphQLResponse executeCreateEvent(@Body GraphQLCreateEventRequest request); @Post - GraphQLResponse createProject(@Body GraphQLCreateProjectRequest request); + GraphQLResponse executeCreateProject(@Body GraphQLCreateProjectRequest request); @Post - GraphQLResponse publishProject(@Body GraphQLPublishProjectRequest request); + GraphQLResponse executePublishProject(@Body GraphQLPublishProjectRequest request); @Post - GraphQLResponse searchOrganization(@Body GraphQLSearchOrganizationRequest request); + GraphQLResponse executeSearchOrganization(@Body GraphQLSearchOrganizationRequest request); @Post - GraphQLResponse setProjectLocation(@Body GraphQLSetProjectLocationRequest request); + GraphQLResponse executeSetProjectLocation(@Body GraphQLSetProjectLocationRequest request); @Post - GraphQLResponse updateProjectAttachment(@Body GraphQLUpdateProjectAttachmentRequest request); + GraphQLResponse executeUpdateProjectAttachment(@Body GraphQLUpdateProjectAttachmentRequest request); @Post - GraphQLResponse updateProjectListing(@Body GraphQLUpdateProjectListingRequest request); + GraphQLResponse executeUpdateProjectListing(@Body GraphQLUpdateProjectListingRequest request); @Post - GraphQLResponse updateProject(@Body GraphQLUpdateProjectRequest request); + GraphQLResponse executeUpdateProject(@Body GraphQLUpdateProjectRequest request); default GraphQLResponse addProjectAttachment(GraphQLAddProjectAttachmentVariables variables) { String fixedQuery = "mutation ($projectId: ID!, $attachmentId: ID!) {\n addProjectAttachment(projectId: $projectId, attachmentId: $attachmentId) {\n attachmentId\n }\n }"; GraphQLAddProjectAttachmentRequest request = new GraphQLAddProjectAttachmentRequest(); request.setQuery(fixedQuery); request.setVariables(variables); - return this.addProjectAttachment(request); + return this.executeAddProjectAttachment(request); } default GraphQLResponse addProjectOrganization(GraphQLAddProjectOrganizationVariables variables) { @@ -60,7 +60,7 @@ default GraphQLResponse addProjectOrganizatio GraphQLAddProjectOrganizationRequest request = new GraphQLAddProjectOrganizationRequest(); request.setQuery(fixedQuery); request.setVariables(variables); - return this.addProjectOrganization(request); + return this.executeAddProjectOrganization(request); } default GraphQLResponse combinedMutationUpdateProjectAddProjectTag(GraphQLCombinedMutationUpdateProjectAddProjectTagVariables variables) { @@ -68,7 +68,7 @@ default GraphQLResponse c GraphQLCombinedMutationUpdateProjectAddProjectTagRequest request = new GraphQLCombinedMutationUpdateProjectAddProjectTagRequest(); request.setQuery(fixedQuery); request.setVariables(variables); - return this.combinedMutationUpdateProjectAddProjectTag(request); + return this.executeCombinedMutationUpdateProjectAddProjectTag(request); } default GraphQLResponse createEvent(GraphQLCreateEventVariables variables) { @@ -76,7 +76,7 @@ default GraphQLResponse createEvent(GraphQLCreateEventVa GraphQLCreateEventRequest request = new GraphQLCreateEventRequest(); request.setQuery(fixedQuery); request.setVariables(variables); - return this.createEvent(request); + return this.executeCreateEvent(request); } default GraphQLResponse createProject(GraphQLCreateProjectVariables variables) { @@ -84,7 +84,7 @@ default GraphQLResponse createProject(GraphQLCreatePro GraphQLCreateProjectRequest request = new GraphQLCreateProjectRequest(); request.setQuery(fixedQuery); request.setVariables(variables); - return this.createProject(request); + return this.executeCreateProject(request); } default GraphQLResponse publishProject(GraphQLPublishProjectVariables variables) { @@ -92,7 +92,7 @@ default GraphQLResponse publishProject(GraphQLPublish GraphQLPublishProjectRequest request = new GraphQLPublishProjectRequest(); request.setQuery(fixedQuery); request.setVariables(variables); - return this.publishProject(request); + return this.executePublishProject(request); } default GraphQLResponse searchOrganization(GraphQLSearchOrganizationVariables variables) { @@ -100,7 +100,7 @@ default GraphQLResponse searchOrganization(GraphQ GraphQLSearchOrganizationRequest request = new GraphQLSearchOrganizationRequest(); request.setQuery(fixedQuery); request.setVariables(variables); - return this.searchOrganization(request); + return this.executeSearchOrganization(request); } default GraphQLResponse setProjectLocation(GraphQLSetProjectLocationVariables variables) { @@ -108,7 +108,7 @@ default GraphQLResponse setProjectLocation(GraphQ GraphQLSetProjectLocationRequest request = new GraphQLSetProjectLocationRequest(); request.setQuery(fixedQuery); request.setVariables(variables); - return this.setProjectLocation(request); + return this.executeSetProjectLocation(request); } default GraphQLResponse updateProjectAttachment(GraphQLUpdateProjectAttachmentVariables variables) { @@ -116,7 +116,7 @@ default GraphQLResponse updateProjectAttachm GraphQLUpdateProjectAttachmentRequest request = new GraphQLUpdateProjectAttachmentRequest(); request.setQuery(fixedQuery); request.setVariables(variables); - return this.updateProjectAttachment(request); + return this.executeUpdateProjectAttachment(request); } default GraphQLResponse updateProjectListing(GraphQLUpdateProjectListingVariables variables) { @@ -124,7 +124,7 @@ default GraphQLResponse updateProjectListing(Gr GraphQLUpdateProjectListingRequest request = new GraphQLUpdateProjectListingRequest(); request.setQuery(fixedQuery); request.setVariables(variables); - return this.updateProjectListing(request); + return this.executeUpdateProjectListing(request); } default GraphQLResponse updateProject(GraphQLUpdateProjectVariables variables) { @@ -132,6 +132,6 @@ default GraphQLResponse updateProject(GraphQLUpdatePro GraphQLUpdateProjectRequest request = new GraphQLUpdateProjectRequest(); request.setQuery(fixedQuery); request.setVariables(variables); - return this.updateProject(request); + return this.executeUpdateProject(request); } } From 5131169e1a87459e0d95b7f89f93a64e0a3f6cff Mon Sep 17 00:00:00 2001 From: jonathan zollinger Date: Mon, 9 Mar 2026 12:20:37 -0600 Subject: [PATCH 5/8] fix: work with graphql response types --- .../org/justserve/client/GraphQLClient.java | 33 ++++++++++++------- core/src/main/resources/application.yml | 4 +++ 2 files changed, 26 insertions(+), 11 deletions(-) diff --git a/core/src/main/java/org/justserve/client/GraphQLClient.java b/core/src/main/java/org/justserve/client/GraphQLClient.java index 4babe42..50b716f 100644 --- a/core/src/main/java/org/justserve/client/GraphQLClient.java +++ b/core/src/main/java/org/justserve/client/GraphQLClient.java @@ -14,37 +14,48 @@ @Client("/graphql") public interface GraphQLClient { - @Post + @Post("/graphql") + @Consumes("application/graphql-response+json") GraphQLResponse executeAddProjectAttachment(@Body GraphQLAddProjectAttachmentRequest request); - @Post + @Post("/graphql") + @Consumes("application/graphql-response+json") GraphQLResponse executeAddProjectOrganization(@Body GraphQLAddProjectOrganizationRequest request); - @Post + @Post("/graphql") + @Consumes("application/graphql-response+json") GraphQLResponse executeCombinedMutationUpdateProjectAddProjectTag(@Body GraphQLCombinedMutationUpdateProjectAddProjectTagRequest request); - @Post + @Post("/graphql") + @Consumes("application/graphql-response+json") GraphQLResponse executeCreateEvent(@Body GraphQLCreateEventRequest request); - @Post + @Post("/graphql") + @Consumes("application/graphql-response+json") GraphQLResponse executeCreateProject(@Body GraphQLCreateProjectRequest request); - @Post + @Post("/graphql") + @Consumes("application/graphql-response+json") GraphQLResponse executePublishProject(@Body GraphQLPublishProjectRequest request); - @Post + @Post("/graphql") + @Consumes("application/graphql-response+json") GraphQLResponse executeSearchOrganization(@Body GraphQLSearchOrganizationRequest request); - @Post + @Post("/graphql") + @Consumes("application/graphql-response+json") GraphQLResponse executeSetProjectLocation(@Body GraphQLSetProjectLocationRequest request); - @Post + @Post("/graphql") + @Consumes("application/graphql-response+json") GraphQLResponse executeUpdateProjectAttachment(@Body GraphQLUpdateProjectAttachmentRequest request); - @Post + @Post("/graphql") + @Consumes("application/graphql-response+json") GraphQLResponse executeUpdateProjectListing(@Body GraphQLUpdateProjectListingRequest request); - @Post + @Post("/graphql") + @Consumes("application/graphql-response+json") GraphQLResponse executeUpdateProject(@Body GraphQLUpdateProjectRequest request); default GraphQLResponse addProjectAttachment(GraphQLAddProjectAttachmentVariables variables) { diff --git a/core/src/main/resources/application.yml b/core/src/main/resources/application.yml index 308c445..9bb0298 100644 --- a/core/src/main/resources/application.yml +++ b/core/src/main/resources/application.yml @@ -14,6 +14,10 @@ micronaut: enabled: true acquire-timeout: 30s max-connections: 100 + codec: + json: + additional-types: + - application/graphql-response+json justserve: token: ${:i-need-to-be-defined} jackson: From 8151b5a3552b851038948b1e8dfbbeefd1c17822 Mon Sep 17 00:00:00 2001 From: jonathan zollinger Date: Mon, 9 Mar 2026 12:22:19 -0600 Subject: [PATCH 6/8] fix: serialize event and location types --- core/build.gradle.kts | 5 ++ .../java/org/justserve/model/EventType.java | 59 +++++++++++++++++++ .../{client => model}/GraphQLRequest.java | 2 +- .../{client => model}/GraphQLResponse.java | 2 +- .../justserve/model/ProjectLocationType.java | 53 +++++++++++++++++ core/src/main/resources/schema.yml | 14 +---- 6 files changed, 121 insertions(+), 14 deletions(-) create mode 100644 core/src/main/java/org/justserve/model/EventType.java rename core/src/main/java/org/justserve/{client => model}/GraphQLRequest.java (91%) rename core/src/main/java/org/justserve/{client => model}/GraphQLResponse.java (93%) create mode 100644 core/src/main/java/org/justserve/model/ProjectLocationType.java diff --git a/core/build.gradle.kts b/core/build.gradle.kts index 5012338..1a8afeb 100644 --- a/core/build.gradle.kts +++ b/core/build.gradle.kts @@ -53,6 +53,11 @@ micronaut { apiNameSuffix = "Client" alwaysUseGenerateHttpResponse = true additionalProperties.put("retryable", "true") +// https://github.com/micronaut-projects/micronaut-openapi/discussions/1783 + schemaMapping.put("EventType", "org.justserve.model.EventType") + importMapping.put("EventType", "org.justserve.model.EventType") + schemaMapping.put("ProjectLocationType", "org.justserve.model.ProjectLocationType") + importMapping.put("ProjectLocationType", "org.justserve.model.ProjectLocationType") } } processing { diff --git a/core/src/main/java/org/justserve/model/EventType.java b/core/src/main/java/org/justserve/model/EventType.java new file mode 100644 index 0000000..82d42df --- /dev/null +++ b/core/src/main/java/org/justserve/model/EventType.java @@ -0,0 +1,59 @@ +package org.justserve.model; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonValue; +import io.micronaut.serde.annotation.Serdeable; +import jakarta.annotation.Generated; +import lombok.RequiredArgsConstructor; + +import java.util.Arrays; +import java.util.Map; +import java.util.function.Function; +import java.util.stream.Collectors; + +/** + * Gets or Sets EventType + */ +@RequiredArgsConstructor +@Serdeable +@Generated("io.micronaut.openapi.generator.JavaMicronautClientCodegen") +public enum EventType { +// None(0, "None"), + DTL(1, "DTL"), + Ongoing(2, "ONGOING"), + Recurring(3, "RECURRING"), + MultipleDTL(4, "MULTIPLE_DTL"); + + public static final Map VALUE_MAPPING = Map.copyOf(Arrays.stream(values()) + .collect(Collectors.toMap(v -> v.intValue, Function.identity()))); + + private final Integer intValue; + private final String stringValue; + + @Override + public String toString() { + return String.valueOf(intValue); + } + + @JsonValue + public String getStringValue() { + return stringValue; + } + + // 2. RECEIVING (Response): This catches the incoming data. + // It can handle the Integer '1' from GraphQL, or even a String if a REST endpoint sends one. + @JsonCreator + public static EventType fromValue(Object value) { + if (value instanceof Number) { + int intVal = ((Number) value).intValue(); + for (EventType type : values()) { + if (type.intValue == intVal) return type; + } + } else if (value instanceof String strVal) { + for (EventType type : values()) { + if (type.stringValue.equalsIgnoreCase(strVal)) return type; + } + } + throw new IllegalArgumentException("Unexpected value '" + value + "' for EventType"); + } +} \ No newline at end of file diff --git a/core/src/main/java/org/justserve/client/GraphQLRequest.java b/core/src/main/java/org/justserve/model/GraphQLRequest.java similarity index 91% rename from core/src/main/java/org/justserve/client/GraphQLRequest.java rename to core/src/main/java/org/justserve/model/GraphQLRequest.java index 10c2452..e9e44f2 100644 --- a/core/src/main/java/org/justserve/client/GraphQLRequest.java +++ b/core/src/main/java/org/justserve/model/GraphQLRequest.java @@ -1,4 +1,4 @@ -package org.justserve.client; +package org.justserve.model; import io.micronaut.serde.annotation.Serdeable; import lombok.AllArgsConstructor; diff --git a/core/src/main/java/org/justserve/client/GraphQLResponse.java b/core/src/main/java/org/justserve/model/GraphQLResponse.java similarity index 93% rename from core/src/main/java/org/justserve/client/GraphQLResponse.java rename to core/src/main/java/org/justserve/model/GraphQLResponse.java index 31acf3c..8f542a0 100644 --- a/core/src/main/java/org/justserve/client/GraphQLResponse.java +++ b/core/src/main/java/org/justserve/model/GraphQLResponse.java @@ -1,4 +1,4 @@ -package org.justserve.client; +package org.justserve.model; import io.micronaut.serde.annotation.Serdeable; import lombok.AllArgsConstructor; diff --git a/core/src/main/java/org/justserve/model/ProjectLocationType.java b/core/src/main/java/org/justserve/model/ProjectLocationType.java new file mode 100644 index 0000000..049b598 --- /dev/null +++ b/core/src/main/java/org/justserve/model/ProjectLocationType.java @@ -0,0 +1,53 @@ +package org.justserve.model; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonValue; +import io.micronaut.serde.annotation.Serdeable; +import jakarta.annotation.Generated; +import lombok.RequiredArgsConstructor; + +import java.util.Arrays; +import java.util.Map; +import java.util.function.Function; +import java.util.stream.Collectors; + +@RequiredArgsConstructor +@Serdeable +@Generated("io.micronaut.openapi.generator.JavaMicronautClientCodegen") +public enum ProjectLocationType { +// NONE(0, "NONE"), + SINGLE_LOCATION(1, "SINGLE_LOCATION"), + REGIONAL(3, "REGIONAL"), + REMOTE(4, "REMOTE"); + + public static final Map VALUE_MAPPING = Map.copyOf(Arrays.stream(values()) + .collect(Collectors.toMap(v -> v.intValue, Function.identity()))); + + private final Integer intValue; + private final String stringValue; + + @Override + public String toString() { + return String.valueOf(intValue); + } + + @JsonValue + public String getStringValue() { + return stringValue; + } + + @JsonCreator + public static ProjectLocationType fromValue(Object value) { + if (value instanceof Number) { + int intVal = ((Number) value).intValue(); + for (ProjectLocationType type : values()) { + if (type.intValue == intVal) return type; + } + } else if (value instanceof String strVal) { + for (ProjectLocationType type : values()) { + if (type.stringValue.equalsIgnoreCase(strVal)) return type; + } + } + throw new IllegalArgumentException("Unexpected value '" + value + "' for ProjectLocationType"); + } +} diff --git a/core/src/main/resources/schema.yml b/core/src/main/resources/schema.yml index 7b4b6e8..ae4333b 100644 --- a/core/src/main/resources/schema.yml +++ b/core/src/main/resources/schema.yml @@ -1100,16 +1100,6 @@ components: deletedOn: { type: string, format: date-time, nullable: true } deletedBy: { type: string, format: uuid, nullable: true } additionalProperties: false - EventType: - type: integer - format: int32 - enum: [ 0, 1, 2, 3, 4 ] - x-enum-varnames: - - "None" - - "DTL" - - "Ongoing" - - "Recurring" - - "MultipleDTL" OrgRepresentative: type: object properties: @@ -2054,9 +2044,9 @@ components: title: type: string eventType: - type: string + $ref: "EventType" locationType: - type: string + $ref: "ProjectLocationType" redirect: type: string GraphQLPublishProjectRequest: From 4bcefa145d29e3c0227c0c2578cfe8243a6e7045 Mon Sep 17 00:00:00 2001 From: jonathan zollinger Date: Mon, 9 Mar 2026 12:23:25 -0600 Subject: [PATCH 7/8] fix: specify client for graphql call --- core/src/main/java/org/justserve/client/GraphQLClient.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/src/main/java/org/justserve/client/GraphQLClient.java b/core/src/main/java/org/justserve/client/GraphQLClient.java index 50b716f..d96cad8 100644 --- a/core/src/main/java/org/justserve/client/GraphQLClient.java +++ b/core/src/main/java/org/justserve/client/GraphQLClient.java @@ -11,7 +11,7 @@ @Produces("application/json") @Consumes("application/graphql-response+json; charset=utf-8") @Retryable -@Client("/graphql") +@Client("justserve") public interface GraphQLClient { @Post("/graphql") From 571f1b7b5f1ca095909dad68cf309231805a2bdb Mon Sep 17 00:00:00 2001 From: jonathan zollinger Date: Mon, 9 Mar 2026 12:27:12 -0600 Subject: [PATCH 8/8] test: add createProject test for graph client --- .../org/justserve/GraphQLClientSpec.groovy | 30 ++++++++++++++----- 1 file changed, 23 insertions(+), 7 deletions(-) diff --git a/core/src/test/groovy/org/justserve/GraphQLClientSpec.groovy b/core/src/test/groovy/org/justserve/GraphQLClientSpec.groovy index e8f7d4b..02a2896 100644 --- a/core/src/test/groovy/org/justserve/GraphQLClientSpec.groovy +++ b/core/src/test/groovy/org/justserve/GraphQLClientSpec.groovy @@ -1,20 +1,36 @@ package org.justserve +import io.micronaut.test.extensions.spock.annotation.MicronautTest import jakarta.inject.Inject import org.justserve.client.GraphQLClient -import org.justserve.client.GraphQLRequest -import org.justserve.model.GraphQLCreateProjectRequest +import org.justserve.model.EventType +import org.justserve.model.GraphQLCreateProjectVariables +import org.justserve.model.ProjectLocationType import spock.lang.Shared import spock.lang.Specification -class GraphQLClientSpec extends Specification{ +@MicronautTest +class GraphQLClientSpec extends Specification { @Shared @Inject - private GraphQLClient client; + GraphQLClient client - void "simple test"(){ - GraphQLRequest request = new GraphQLCreateProjectRequest() - client.createProject() + void "can create Project with EventType: #eventType, LocationType: #locationType, and Redirect: #redirect"(EventType eventType, ProjectLocationType locationType, String redirect) { + given: + GraphQLCreateProjectVariables args = new GraphQLCreateProjectVariables() + .setEventType(eventType) + .setLocationType(locationType) + .setTitle("this is my title") + .setRedirect(redirect) + + when: + client.createProject(args) + + then: + noExceptionThrown() + + where: + [eventType, locationType, redirect] << [EventType.values(), ProjectLocationType.values(), ["", null, "https://google.com"]].combinations() } }