diff --git a/core/build.gradle.kts b/core/build.gradle.kts index 1a8afeb..b8db30f 100644 --- a/core/build.gradle.kts +++ b/core/build.gradle.kts @@ -58,6 +58,10 @@ micronaut { importMapping.put("EventType", "org.justserve.model.EventType") schemaMapping.put("ProjectLocationType", "org.justserve.model.ProjectLocationType") importMapping.put("ProjectLocationType", "org.justserve.model.ProjectLocationType") + schemaMapping.put("ProjectStatus", "org.justserve.model.ProjectStatus") + importMapping.put("ProjectStatus", "org.justserve.model.ProjectStatus") + schemaMapping.put("TimeZone", "org.justserve.model.TimeZone") + importMapping.put("TimeZone", "org.justserve.model.TimeZone") } } processing { diff --git a/core/src/main/java/org/justserve/client/GraphQLClient.java b/core/src/main/java/org/justserve/client/GraphQLClient.java index 2f268d4..bb4b268 100644 --- a/core/src/main/java/org/justserve/client/GraphQLClient.java +++ b/core/src/main/java/org/justserve/client/GraphQLClient.java @@ -7,6 +7,10 @@ import io.micronaut.http.client.annotation.Client; import io.micronaut.retry.annotation.Retryable; import org.justserve.model.*; +import org.justserve.model.graph.*; + +import java.util.ArrayList; +import java.util.List; @Produces("application/json") @Consumes("application/graphql-response+json; charset=utf-8") @@ -14,6 +18,15 @@ @Client(id = "justserve", path = "/graphql") public interface GraphQLClient { + @Post + GraphQLResponse createEvent(@Body CreateEventMutation request); + + @Post + GraphQLResponse createEvents(@Body CreateEventsMutation request); + + @Post + GraphQLResponse createRecurringEvents(@Body CreateRecurringEventsMutation request); + @Post GraphQLResponse executeAddProjectAttachment(@Body GraphQLAddProjectAttachmentRequest request); @@ -23,9 +36,6 @@ public interface GraphQLClient { @Post GraphQLResponse executeCombinedMutationUpdateProjectAddProjectTag(@Body GraphQLCombinedMutationUpdateProjectAddProjectTagRequest request); - @Post - GraphQLResponse executeCreateEvent(@Body GraphQLCreateEventRequest request); - @Post GraphQLResponse executeCreateProject(@Body GraphQLCreateProjectRequest request); @@ -71,14 +81,6 @@ default GraphQLResponse c return this.executeCombinedMutationUpdateProjectAddProjectTag(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.executeCreateEvent(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(); @@ -128,9 +130,20 @@ default GraphQLResponse updateProjectListing(Gr } 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 }"; + String mutationFormat = "mutation ($projectId: ID!, $logo: String!) {\n updateProject(id: $projectId, modify: { logo: $logo }) {\n %s\n }\n }"; + + List responseFields = new ArrayList<>(); + responseFields.add("id"); + + if (variables.getLogo() != null) { + responseFields.add("logo"); + } + + String fieldsString = String.join("\n", responseFields); + String dynamicQuery = String.format(mutationFormat, fieldsString); + GraphQLUpdateProjectRequest request = new GraphQLUpdateProjectRequest(); - request.setQuery(fixedQuery); + request.setQuery(dynamicQuery); request.setVariables(variables); return this.executeUpdateProject(request); } diff --git a/core/src/main/java/org/justserve/model/CivicGeography.java b/core/src/main/java/org/justserve/model/CivicGeography.java new file mode 100644 index 0000000..ca41cc1 --- /dev/null +++ b/core/src/main/java/org/justserve/model/CivicGeography.java @@ -0,0 +1,17 @@ +package org.justserve.model; + +import io.micronaut.core.annotation.Introspected; +import io.micronaut.serde.annotation.Serdeable; +import lombok.Data; +import lombok.experimental.Accessors; + +/** + * This class is currently a placeholder for a future work. + * This class does nothing. + */ +@Data +@Accessors(chain = true) +@Serdeable +@Introspected +public class CivicGeography { +} diff --git a/core/src/main/java/org/justserve/model/EventType.java b/core/src/main/java/org/justserve/model/EventType.java index 82d42df..9cfae0e 100644 --- a/core/src/main/java/org/justserve/model/EventType.java +++ b/core/src/main/java/org/justserve/model/EventType.java @@ -3,7 +3,7 @@ import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonValue; import io.micronaut.serde.annotation.Serdeable; -import jakarta.annotation.Generated; +import lombok.Generated; import lombok.RequiredArgsConstructor; import java.util.Arrays; @@ -12,16 +12,40 @@ import java.util.stream.Collectors; /** - * Gets or Sets EventType + * Defines the scheduling model for a JustServe project, determining how its + * {@link ProjectEvent}s are structured and displayed. + * + * @author Jonathan Zollinger + * @since 0.1.0 */ @RequiredArgsConstructor @Serdeable -@Generated("io.micronaut.openapi.generator.JavaMicronautClientCodegen") public enum EventType { -// None(0, "None"), + /** + *

Date, Time, and Location

+ * A standard event that occurs at a specific time and place. + */ DTL(1, "DTL"), + + /** + *

Ongoing

+ * An event with no specific time. The start and end dates determine visibility on JustServe + */ Ongoing(2, "ONGOING"), + + /** + *

Recurring

+ * An event that repeats on a regular schedule, such as weekly or monthly. + *

Example: An evening opportunity that occurs every Monday, Wednesday, + * and Friday for three months. + */ Recurring(3, "RECURRING"), + + /** + *

Multiple Date, Time, and Location

+ * A complex event that has multiple, distinct shifts or occurrences. + *

Example: A project with multiple shifts on each Saturday for several weeks. + */ MultipleDTL(4, "MULTIPLE_DTL"); public static final Map VALUE_MAPPING = Map.copyOf(Arrays.stream(values()) @@ -40,8 +64,13 @@ 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. + /** + * Parses the incoming value to either the string or integer value, whichever the server is using. + * + * @param value the incoming value from the server + * @return the event type that matches the incoming value + */ + @Generated //manually placed annotation to tell jacoco coverage report to ignore this @JsonCreator public static EventType fromValue(Object value) { if (value instanceof Number) { @@ -56,4 +85,4 @@ public static EventType fromValue(Object value) { } throw new IllegalArgumentException("Unexpected value '" + value + "' for EventType"); } -} \ No newline at end of file +} diff --git a/core/src/main/java/org/justserve/model/GraphQLRequest.java b/core/src/main/java/org/justserve/model/GraphQLRequest.java deleted file mode 100644 index e9e44f2..0000000 --- a/core/src/main/java/org/justserve/model/GraphQLRequest.java +++ /dev/null @@ -1,18 +0,0 @@ -package org.justserve.model; - -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/model/ProjectEvent.java b/core/src/main/java/org/justserve/model/ProjectEvent.java new file mode 100644 index 0000000..d39eed6 --- /dev/null +++ b/core/src/main/java/org/justserve/model/ProjectEvent.java @@ -0,0 +1,293 @@ +package org.justserve.model; + +import com.fasterxml.jackson.annotation.JsonFormat; +import io.micronaut.core.annotation.Introspected; +import io.micronaut.core.annotation.NonNull; +import io.micronaut.core.annotation.Nullable; +import io.micronaut.serde.annotation.Serdeable; +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.Size; +import lombok.*; +import lombok.experimental.Accessors; +import org.justserve.model.graph.CreateEventMutation; +import org.justserve.model.graph.GraphFields; + +import java.util.Date; +import java.util.List; +import java.util.UUID; + +import static java.lang.Boolean.TRUE; + +/** + *

JustServe Project Event

+ * Valid to use with + *
  • {@link EventType#Ongoing} + *
  • {@link EventType#DTL} + *
  • {@link EventType#MultipleDTL}
+ * (Not valid to use with{@link EventType#Recurring} events + * + *

Creating New Events

+ * Use {@code ProjectEvent.}{@link #builder()} when adding a new event to ensure all + * needed fields are included. This not only checks for required fields, but double checks + * contradictions or invalid combinations are being submitted. + *
Example
+ *
{@code
+ * ProjectEvent newEvent = ProjectEvent.builder()
+ *     .start(startDate)
+ *     .end(endDate)
+ *     .shiftTitle("Morning Shift")
+ *     .build();
+ * }
+ * + *

Updating Existing Events

+ * Use {@code new ProjectEvent()} (without the builder) when updating an event. This + * skips the builder's checks and lets you send partial updates to existing events. + *
Example
+ *
{@code
+ * ProjectEvent partialUpdate = new ProjectEvent()
+ *     .setShiftTitle("Afternoon Shift");
+ * }
+ * + * @author Jonathan Zollinger + * @since 0.1.0 + */ +@EqualsAndHashCode(callSuper = true) +@Data +@Accessors(chain = true) +@NoArgsConstructor +@AllArgsConstructor +@Builder(buildMethodName = "buildInternal") +@Serdeable +@Introspected +public class ProjectEvent extends GraphFields { + @Nullable + @Email + private String contactEmail; + + @Nullable + @Size(max = 139) + private String contactName; + + @Nullable + private String contactPhone; + + /** + *

Whether the project event has been deleted.

+ * Not Usable In: + *
    + *
  • {@link CreateEventMutation}
  • + *
+ */ + @Nullable + private Boolean deleted; + + /** + *

The user who deleted the project event.

+ * See{@link #deletedByNavigation}
+ * Not Usable In: + *
    + *
  • {@link CreateEventMutation}
  • + *
+ */ + @Nullable + private UUID deletedBy; + + /** + *

User who deleted the event.

+ * See{@link #deletedBy}
+ * Not Usable In: + *
    + *
  • {@link CreateEventMutation}
  • + *
+ */ + @Nullable + private User deletedByNavigation; + + /** + *

The date and time the project event was deleted.

+ * Not Usable In: + *
    + *
  • {@link CreateEventMutation}
  • + *
+ */ + @Nullable + @JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", timezone = "UTC") + private Date deletedOn; + + @Nullable + @JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", timezone = "UTC") + private Date end; + + /** + *

The end date and time of the event with timezone offset.

+ * Not Usable In: + *
    + *
  • {@link CreateEventMutation}
  • + *
+ */ + @Nullable + @JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", timezone = "UTC") + private Date endDateTimeOffset; + + /** + *

Indicates if the event capacity has been reached.

+ * Not Usable In: + *
    + *
  • {@link CreateEventMutation}
  • + *
+ */ + @Nullable + private Boolean eventCapReached; + + /** + * Whether a group cap is set for this event. + */ + @NonNull + @Builder.Default + private Boolean groupCap = false; + + /** + * The max number of people which can sign up by one person + */ + @Nullable + private Integer groupLimit; + + /** + *

The unique identifier for the project event.

+ * Not Usable In: + *
    + *
  • {@link CreateEventMutation}
  • + *
+ */ + @Nullable + private UUID id; + + @Nullable + private String locationLink; + + @Nullable + @Size(max = 139) + private String locationName; + + /** + *

The project this event belongs to.

+ * Not Usable In: + *
    + *
  • {@link CreateEventMutation}
  • + *
+ */ + @Nullable + private Project project; + + /** + *

The location of the project event.

+ * Not Usable In: + *
    + *
  • {@link CreateEventMutation}
  • + *
+ */ + @Nullable + private ProjectEventLocation projectEventLocation; + + /** + *

The ID of the project event location.

+ * Not Usable In: + *
    + *
  • {@link CreateEventMutation}
  • + *
+ */ + @Nullable + private UUID projectEventLocationId; + + /** + *

The regions associated with the project event.

+ * Not Usable In: + *
    + *
  • {@link CreateEventMutation}
  • + *
+ */ + @Nullable + private List projectEventRegions; + + /** + *

The ID of the project this event belongs to.

+ * Not Usable In: + *
    + *
  • {@link CreateEventMutation}
  • + *
+ */ + @Nullable + private UUID projectId; + + /** + *

Information about the recurring schedule of the project event.

+ * Not Usable In: + *
    + *
  • {@link CreateEventMutation}
  • + *
+ */ + @Nullable + private ProjectRecurringTime projectRecurringTime; + + @Nullable + private String qrCodeImageLocation; + + /** + *

The date the event is set to renew.

+ * Only Usable In: + *
    + *
  • {@link EventType#MultipleDTL}
  • + *
+ */ + @Nullable + @JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", timezone = "UTC") + private Date renewDate; + + @Nullable + @Size(max = 300) + private String schedule; + + @Nullable + @Size(max = 300) + private String shiftTitle; + + @Nullable + private String specialDirections; + + @Nullable + @JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", timezone = "UTC") + private Date start; + + @Nullable + private ProjectEventStatus status; + + @Nullable + private TimeZone timezone; + + @Nullable + private Integer totalVolunteersNeeded; + + /** + * whether a volunteer cap is set for this event. + */ + @Nullable + private Boolean volunteerCap; + + public static class ProjectEventBuilder { + public ProjectEvent build() { + ProjectEvent event = this.buildInternal(); + + if (event.getEnd() == null || event.getStart() == null) { + throw new IllegalStateException("Events created with the builder must have a start and end date"); + } + if ((TRUE.equals(event.getGroupCap()) && event.getGroupLimit() == null)) { + throw new IllegalStateException("groupLimit cannot be null when groupCap is true"); + } + if (TRUE.equals(event.getVolunteerCap()) && event.getTotalVolunteersNeeded() == null) { + throw new IllegalStateException("totalVolunteersNeeded cannot be null when volunteerCap is true"); + } + + return event; + } + } +} diff --git a/core/src/main/java/org/justserve/model/ProjectEventLocation.java b/core/src/main/java/org/justserve/model/ProjectEventLocation.java new file mode 100644 index 0000000..2b10965 --- /dev/null +++ b/core/src/main/java/org/justserve/model/ProjectEventLocation.java @@ -0,0 +1,17 @@ +package org.justserve.model; + +import io.micronaut.core.annotation.Introspected; +import io.micronaut.serde.annotation.Serdeable; +import lombok.Data; +import lombok.experimental.Accessors; + +/** + * This class is currently a placeholder for a future work. + * This class does nothing. + */ +@Data +@Accessors(chain = true) +@Serdeable +@Introspected +public class ProjectEventLocation { +} diff --git a/core/src/main/java/org/justserve/model/ProjectEventStatus.java b/core/src/main/java/org/justserve/model/ProjectEventStatus.java new file mode 100644 index 0000000..aaed8aa --- /dev/null +++ b/core/src/main/java/org/justserve/model/ProjectEventStatus.java @@ -0,0 +1,64 @@ +package org.justserve.model; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonValue; +import io.micronaut.serde.annotation.Serdeable; +import lombok.Generated; +import lombok.RequiredArgsConstructor; + +import java.util.Arrays; +import java.util.Map; +import java.util.function.Function; +import java.util.stream.Collectors; + +/** + *

Supported Project Event Status

+ *

+ * Project Status available to project events + * Use {@link ProjectStatus} + */ +@RequiredArgsConstructor +@Serdeable +public enum ProjectEventStatus { + ACTIVE(1, "ACTIVE"), + CANCELLED(2, "CANCELLED"), + ON_HOLD(3, "ON_HOLD"); + + 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; + } + + /** + * Parses the incoming value to either the string or integer value, whichever the server is using. + * + * @param value the incoming value from the server + * @return the event type that matches the incoming value + */ + @Generated //manually placed annotation to tell jacoco coverage report to ignore this + @JsonCreator + public static ProjectEventStatus fromValue(Object value) { + if (value instanceof Number) { + int intVal = ((Number) value).intValue(); + for (ProjectEventStatus type : values()) { + if (type.intValue == intVal) return type; + } + } else if (value instanceof String strVal) { + for (ProjectEventStatus type : values()) { + if (type.stringValue.equalsIgnoreCase(strVal)) return type; + } + } + throw new IllegalArgumentException("Unexpected value '" + value + "' for ProjectEventStatus"); + } +} diff --git a/core/src/main/java/org/justserve/model/ProjectLocationType.java b/core/src/main/java/org/justserve/model/ProjectLocationType.java index 049b598..35bcf4d 100644 --- a/core/src/main/java/org/justserve/model/ProjectLocationType.java +++ b/core/src/main/java/org/justserve/model/ProjectLocationType.java @@ -3,7 +3,7 @@ import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonValue; import io.micronaut.serde.annotation.Serdeable; -import jakarta.annotation.Generated; +import lombok.Generated; import lombok.RequiredArgsConstructor; import java.util.Arrays; @@ -11,11 +11,13 @@ import java.util.function.Function; import java.util.stream.Collectors; +/** + *

Supported Location Types for Projects

+ * + */ @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"); @@ -36,6 +38,13 @@ public String getStringValue() { return stringValue; } + /** + * Parses the incoming value to either the string or integer value, whichever the server is using. + * + * @param value the incoming value from the server + * @return the event type that matches the incoming value + */ + @Generated //manually placed annotation to tell jacoco coverage report to ignore this @JsonCreator public static ProjectLocationType fromValue(Object value) { if (value instanceof Number) { diff --git a/core/src/main/java/org/justserve/model/ProjectRecurringTime.java b/core/src/main/java/org/justserve/model/ProjectRecurringTime.java new file mode 100644 index 0000000..3d5782e --- /dev/null +++ b/core/src/main/java/org/justserve/model/ProjectRecurringTime.java @@ -0,0 +1,144 @@ +package org.justserve.model; + +import io.micronaut.core.annotation.Introspected; +import io.micronaut.core.annotation.Nullable; +import io.micronaut.serde.annotation.Serdeable; +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.Size; +import lombok.*; +import lombok.experimental.Accessors; +import org.justserve.model.graph.GraphFields; + +import java.util.List; +import java.util.UUID; + +import static java.lang.Boolean.TRUE; + +/** + *

JustServe Project Recurring Time

+ * Valid to use with + *
    + *
  • {@link EventType#Recurring}
  • + *
+ * + *

Creating New Recurring Events

+ * Use {@code ProjectRecurringTime.}{@link #builder()} when adding a new recurring event to ensure all + * needed fields are included and validated. + * + * @author Jonathan Zollinger + * @since 0.1.0 + */ +@EqualsAndHashCode(callSuper = true) +@Data +@Accessors(chain = true) +@NoArgsConstructor +@AllArgsConstructor +@Builder(buildMethodName = "buildInternal") +@Serdeable +@Introspected +public class ProjectRecurringTime extends GraphFields { + + @Nullable + @Email + private String contactEmail; + + @Nullable + @Size(max = 139) + private String contactName; + + @Nullable + private String contactPhone; + + @Nullable + private String startTime; + + @Nullable + private String endTime; + + @Builder.Default + private Boolean firstWeek = false; + + @Builder.Default + private Boolean secondWeek = false; + + @Builder.Default + private Boolean thirdWeek = false; + + @Builder.Default + private Boolean fourthWeek = false; + + @Builder.Default + private Boolean fifthWeek = false; + + @Builder.Default + private Boolean lastWeek = false; + + @Nullable + private Integer groupLimit; + + + @Nullable + private UUID id; + + @Nullable + private RecurringType recurringType; + + @Nullable + private List daysOfMonth; + + /** + *

The days of the months a recurring event lands on.

+ * For example, a monthly recurring event landing on the 16th of the month: + * + */ + @Nullable + private List recurringDaysOfMonths; + + @Nullable + private UUID projectRecurringId; + + @Nullable + private String specialDirections; + + @Builder.Default + private Boolean volunteersCapped = false; + + @Nullable + private Integer totalVolunteersNeeded; + + @Nullable + private Integer volunteersNeeded; + + @Builder.Default + private Boolean monday = false; + + @Builder.Default + private Boolean tuesday = false; + + @Builder.Default + private Boolean wednesday = false; + + @Builder.Default + private Boolean thursday = false; + + @Builder.Default + private Boolean friday = false; + + @Builder.Default + private Boolean saturday = false; + + @Builder.Default + private Boolean sunday = false; + + + public static class ProjectRecurringTimeBuilder { + public ProjectRecurringTime build() { + ProjectRecurringTime event = this.buildInternal(); + if (TRUE.equals(event.getVolunteersCapped()) && event.getVolunteersNeeded() == null) { + throw new IllegalStateException("volunteersNeeded cannot be null when volunteersCapped is true"); + } + + return event; + } + } +} diff --git a/core/src/main/java/org/justserve/model/ProjectStatus.java b/core/src/main/java/org/justserve/model/ProjectStatus.java new file mode 100644 index 0000000..f4466ee --- /dev/null +++ b/core/src/main/java/org/justserve/model/ProjectStatus.java @@ -0,0 +1,70 @@ +package org.justserve.model; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonValue; +import io.micronaut.serde.annotation.Serdeable; +import lombok.Generated; +import lombok.RequiredArgsConstructor; + +import java.util.Arrays; +import java.util.Map; +import java.util.function.Function; +import java.util.stream.Collectors; + +/** + *

Status for a project.

+ * + * Use{@link ProjectEventStatus} when creating a project events. + * + * @author Jonathan Zollinger + * @since 0.1.0 + */ +@RequiredArgsConstructor +@Serdeable +public enum ProjectStatus { + PUBLISHED(1, "PUBLISHED"), + SUBMITTED(2, "SUBMITTED"), + DRAFT(3, "DRAFT"), + TEMPLATE(4, "TEMPLATE"), + ON_HOLD(5, "ON_HOLD"), + CANCELLED(6, "CANCELLED"), + DECLINED(7, "DECLINED"); + + 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; + } + + /** + * Parses the incoming value to either the string or integer value, whichever the server is using. + * + * @param value the incoming value from the server + * @return the event type that matches the incoming value + */ + @Generated //manually placed annotation to tell jacoco coverage report to ignore this + @JsonCreator + public static ProjectStatus fromValue(Object value) { + if (value instanceof Number) { + int intVal = ((Number) value).intValue(); + for (ProjectStatus type : values()) { + if (type.intValue == intVal) return type; + } + } else if (value instanceof String strVal) { + for (ProjectStatus type : values()) { + if (type.stringValue.equalsIgnoreCase(strVal)) return type; + } + } + throw new IllegalArgumentException("Unexpected value '" + value + "' for ProjectStatus"); + } +} diff --git a/core/src/main/java/org/justserve/model/RecurringType.java b/core/src/main/java/org/justserve/model/RecurringType.java new file mode 100644 index 0000000..5fc1e80 --- /dev/null +++ b/core/src/main/java/org/justserve/model/RecurringType.java @@ -0,0 +1,17 @@ +package org.justserve.model; + +import io.micronaut.core.annotation.Introspected; +import io.micronaut.serde.annotation.Serdeable; + +/** + * JustServe Project Recurring Type. + * + * @author Jonathan Zollinger + * @since 0.1.0 + */ +@Serdeable +@Introspected +public enum RecurringType { + WEEKLY, + MONTHLY +} diff --git a/core/src/main/java/org/justserve/model/TimeZone.java b/core/src/main/java/org/justserve/model/TimeZone.java new file mode 100644 index 0000000..33f95d4 --- /dev/null +++ b/core/src/main/java/org/justserve/model/TimeZone.java @@ -0,0 +1,214 @@ +package org.justserve.model; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonValue; +import io.micronaut.serde.annotation.Serdeable; +import lombok.Generated; +import lombok.RequiredArgsConstructor; + +import java.util.Arrays; +import java.util.Map; +import java.util.function.Function; +import java.util.stream.Collectors; + +/** + *

TimeZones supported in JustServe

+ *
    + *
  • The queryValue field reflects the options in the UI (and what is sent to the server).
  • + *
  • The ResponseValue is the String which is returned from the server
  • + *
+ * + * @author Jonathan Zollinger + * @since 0.1.0 + */ +@RequiredArgsConstructor +@Serdeable +public enum TimeZone { + INTERNATIONAL_DATE_LINE_WEST(1, "(UTC-12:00) International Date Line West", "Dateline Standard Time"), + COORDINATED_UNIVERSAL_TIME_11(2, "(UTC-11:00) Coordinated Universal Time-11", "UTC-11"), + ALEUTIAN_ISLANDS(3, "(UTC-10:00) Aleutian Islands", "Aleutian Standard Time"), + HAWAII(4, "(UTC-10:00) Hawaii", "Hawaiian Standard Time"), + MARQUESAS_ISLANDS(5, "(UTC-09:30) Marquesas Islands", "Marquesas Standard Time"), + ALASKA(6, "(UTC-09:00) Alaska", "Alaskan Standard Time"), + COORDINATED_UNIVERSAL_TIME_09(7, "(UTC-09:00) Coordinated Universal Time-09", "UTC-09"), + BAJA_CALIFORNIA(8, "(UTC-08:00) Baja California", "Pacific Standard Time (Mexico)"), + COORDINATED_UNIVERSAL_TIME_08(9, "(UTC-08:00) Coordinated Universal Time-08", "UTC-08"), + PACIFIC_TIME_US_AND_CANADA(10, "(UTC-08:00) Pacific Time (US & Canada)", "Pacific Standard Time"), + ARIZONA(11, "(UTC-07:00) Arizona", "US Mountain Standard Time"), + LA_PAZ(12, "(UTC-07:00) La Paz", "Mountain Standard Time (Mexico)"), + MOUNTAIN_TIME_US_AND_CANADA(13, "(UTC-07:00) Mountain Time (US & Canada)", "Mountain Standard Time"), + YUKON(14, "(UTC-07:00) Yukon", "Yukon Standard Time"), + CENTRAL_AMERICA(15, "(UTC-06:00) Central America", "Central America Standard Time"), + CENTRAL_TIME_US_AND_CANADA(16, "(UTC-06:00) Central Time (US & Canada)", "Central Standard Time"), + EASTER_ISLAND(17, "(UTC-06:00) Easter Island", "Easter Island Standard Time"), + GUADALAJARA(18, "(UTC-06:00) Guadalajara", "Central Standard Time (Mexico)"), + SASKATCHEWAN(19, "(UTC-06:00) Saskatchewan", "Canada Central Standard Time"), + BOGOTA(20, "(UTC-05:00) Bogota", "SA Pacific Standard Time"), + CHETUMAL(21, "(UTC-05:00) Chetumal", "Eastern Standard Time (Mexico)"), + EASTERN_TIME_US_AND_CANADA(22, "(UTC-05:00) Eastern Time (US & Canada)", "Eastern Standard Time"), + HAITI(23, "(UTC-05:00) Haiti", "Haiti Standard Time"), + HAVANA(24, "(UTC-05:00) Havana", "Cuba Standard Time"), + INDIANA_EAST(25, "(UTC-05:00) Indiana (East)", "US Eastern Standard Time"), + TURKS_AND_CAICOS(26, "(UTC-05:00) Turks and Caicos", "Turks And Caicos Standard Time"), + ASUNCION(27, "(UTC-04:00) Asuncion", "Paraguay Standard Time"), + ATLANTIC_TIME_CANADA(28, "(UTC-04:00) Atlantic Time (Canada)", "Atlantic Standard Time"), + CARACAS(29, "Caracas", "Venezuela Standard Time"), + CUIABA(30, "(UTC-04:00) Cuiaba", "Central Brazilian Standard Time"), + GEORGETOWN(31, "(UTC-04:00) Georgetown", "SA Western Standard Time"), + SANTIAGO(32, "(UTC-04:00) Santiago", "Pacific SA Standard Time"), + NEWFOUNDLAND(33, "(UTC-03:30) Newfoundland", "Newfoundland Standard Time"), + ARAGUAINA(34, "(UTC-03:00) Araguaina", "Tocantins Standard Time"), + BRASILIA(35, "(UTC-03:00) Brasilia", "E. South America Standard Time"), + CAYENNE(36, "(UTC-03:00) Cayenne", "SA Eastern Standard Time"), + CITY_OF_BUENOS_AIRES(37, "(UTC-03:00) City of Buenos Aires", "Argentina Standard Time"), + MONTEVIDEO(38, "(UTC-03:00) Montevideo", "Montevideo Standard Time"), + PUNTA_ARENAS(39, "(UTC-03:00) Punta Arenas", "Magallanes Standard Time"), + SAINT_PIERRE_AND_MIQUELON(40, "(UTC-03:00) Saint Pierre and Miquelon", "Saint Pierre Standard Time"), + SALVADOR(41, "(UTC-03:00) Salvador", "Bahia Standard Time"), + COORDINATED_UNIVERSAL_TIME_02(42, "(UTC-02:00) Coordinated Universal Time-02", "UTC-02"), + GREENLAND(43, "(UTC-02:00) Greenland", "Greenland Standard Time"), + MID_ATLANTIC_OLD(44, "(UTC-02:00) Mid-Atlantic - Old", "Mid-Atlantic Standard Time"), + AZORES(45, "(UTC-01:00) Azores", "Azores Standard Time"), + CABO_VERDE_IS(46, "(UTC-01:00) Cabo Verde Is.", "Cape Verde Standard Time"), + COORDINATED_UNIVERSAL_TIME(47, "(UTC) Coordinated Universal Time", "UTC"), + DUBLIN(48, "(UTC+00:00) Dublin", "GMT Standard Time"), + MONROVIA(49, "(UTC+00:00) Monrovia", "Greenwich Standard Time"), + SAO_TOME(50, "(UTC+00:00) Sao Tome", "Sao Tome Standard Time"), + CASABLANCA(51, "(UTC+01:00) Casablanca", "Morocco Standard Time"), + AMSTERDAM(52, "(UTC+01:00) Amsterdam", "W. Europe Standard Time"), + BELGRADE(53, "(UTC+01:00) Belgrade", "Central Europe Standard Time"), + BRUSSELS(54, "(UTC+01:00) Brussels", "Romance Standard Time"), + SARAJEVO(55, "(UTC+01:00) Sarajevo", "Central European Standard Time"), + WEST_CENTRAL_AFRICA(56, "(UTC+01:00) West Central Africa", "W. Central Africa Standard Time"), + ATHENS(57, "(UTC+02:00) Athens", "GTB Standard Time"), + BEIRUT(58, "(UTC+02:00) Beirut", "Middle East Standard Time"), + CAIRO(59, "(UTC+02:00) Cairo", "Egypt Standard Time"), + CHISINAU(60, "(UTC+02:00) Chisinau", "E. Europe Standard Time"), + GAZA(61, "(UTC+02:00) Gaza", "West Bank Standard Time"), + HARARE(62, "(UTC+02:00) Harare", "South Africa Standard Time"), + HELSINKI(63, "(UTC+02:00) Helsinki", "FLE Standard Time"), + JERUSALEM(64, "(UTC+02:00) Jerusalem", "Israel Standard Time"), + JUBA(65, "(UTC+02:00) Juba", "South Sudan Standard Time"), + KALININGRAD(66, "(UTC+02:00) Kaliningrad", "Kaliningrad Standard Time"), + KHARTOUM(67, "(UTC+02:00) Khartoum", "Sudan Standard Time"), + TRIPOLI(68, "(UTC+02:00) Tripoli", "Libya Standard Time"), + WINDHOEK(69, "(UTC+02:00) Windhoek", "Namibia Standard Time"), + AMMAN(70, "(UTC+03:00) Amman", "Jordan Standard Time"), + BAGHDAD(71, "(UTC+03:00) Baghdad", "Arabic Standard Time"), + DAMASCUS(72, "(UTC+03:00) Damascus", "Syria Standard Time"), + ISTANBUL(73, "(UTC+03:00) Istanbul", "Turkey Standard Time"), + KUWAIT(74, "(UTC+03:00) Kuwait", "Arab Standard Time"), + MINSK(75, "(UTC+03:00) Minsk", "Belarus Standard Time"), + MOSCOW(76, "(UTC+03:00) Moscow", "Russian Standard Time"), + NAIROBI(77, "(UTC+03:00) Nairobi", "E. Africa Standard Time"), + VOLGOGRAD(78, "(UTC+03:00) Volgograd", "Volgograd Standard Time"), + TEHRAN(79, "(UTC+03:30) Tehran", "Iran Standard Time"), + ABU_DHABI(80, "(UTC+04:00) Abu Dhabi", "Arabian Standard Time"), + ASTRAKHAN(81, "(UTC+04:00) Astrakhan", "Astrakhan Standard Time"), + BAKU(82, "(UTC+04:00) Baku", "Azerbaijan Standard Time"), + IZHEVSK(83, "(UTC+04:00) Izhevsk", "Russia Time Zone 3"), + PORT_LOUIS(84, "(UTC+04:00) Port Louis", "Mauritius Standard Time"), + SARATOV(85, "(UTC+04:00) Saratov", "Samara Time"), + TBILISI(86, "(UTC+04:00) Tbilisi", "Georgian Standard Time"), + YEREVAN(87, "(UTC+04:00) Yerevan", "Caucasus Standard Time"), + KABUL(88, "(UTC+04:30) Kabul", "Afghanistan Standard Time"), + ASHGABAT(89, "(UTC+05:00) Ashgabat", "West Asia Standard Time"), + ASTANA(90, "(UTC+05:00) Astana", "Central Asia Standard Time"), + EKATERINBURG(91, "(UTC+05:00) Ekaterinburg", "Ekaterinburg Standard Time"), + ISLAMABAD(92, "(UTC+05:00) Islamabad", "Pakistan Standard Time"), + CHENNAI(93, "(UTC+05:30) Chennai", "India Standard Time"), + SRI_JAYAWARDENEPURA(94, "(UTC+05:30) Sri Jayawardenepura", "Sri Lanka Standard Time"), + KATHMANDU(95, "(UTC+05:45) Kathmandu", "Nepal Standard Time"), + BISHKEK(96, "(UTC+06:00) Bishkek", "Central Asia Standard Time"), + DHAKA(97, "(UTC+06:00) Dhaka", "Bangladesh Standard Time"), + OMSK(98, "(UTC+06:00) Omsk", "Omsk Standard Time"), + YANGON_RANGOON(99, "(UTC+06:30) Yangon (Rangoon)", "Myanmar Standard Time"), + BANGKOK(100, "(UTC+07:00) Bangkok", "SE Asia Standard Time"), + BARNAUL(101, "(UTC+07:00) Barnaul", "Altai Standard Time"), + HOVD(102, "(UTC+07:00) Hovd", "W. Mongolia Standard Time"), + KRASNOYARSK(103, "(UTC+07:00) Krasnoyarsk", "North Asia Standard Time"), + NOVOSIBIRSK(104, "(UTC+07:00) Novosibirsk", "N. Central Asia Standard Time"), + TOMSK(105, "(UTC+07:00) Tomsk", "Tomsk Standard Time"), + BEIJING(106, "(UTC+08:00) Beijing", "China Standard Time"), + IRKUTSK(107, "(UTC+08:00) Irkutsk", "North Asia East Standard Time"), + KUALA_LUMPUR(108, "(UTC+08:00) Kuala Lumpur", "Singapore Standard Time"), + PERTH(109, "(UTC+08:00) Perth", "W. Australia Standard Time"), + TAIPEI(110, "(UTC+08:00) Taipei", "Taipei Standard Time"), + ULAANBAATAR(111, "(UTC+08:00) Ulaanbaatar", "Ulaanbaatar Standard Time"), + EUCLA(112, "(UTC+08:45) Eucla", "Aus Central W. Standard Time"), + CHITA(113, "(UTC+09:00) Chita", "Transbaikal Standard Time"), + OSAKA(114, "(UTC+09:00) Osaka", "Tokyo Standard Time"), + PYONGYANG(115, "(UTC+09:00) Pyongyang", "North Korea Standard Time"), + SEOUL(116, "(UTC+09:00) Seoul", "Korea Standard Time"), + YAKUTSK(117, "(UTC+09:00) Yakutsk", "Yakutsk Standard Time"), + ADELAIDE(118, "(UTC+09:30) Adelaide", "Cen. Australia Standard Time"), + DARWIN(119, "(UTC+09:30) Darwin", "AUS Central Standard Time"), + BRISBANE(120, "(UTC+10:00) Brisbane", "E. Australia Standard Time"), + CANBERRA(121, "(UTC+10:00) Canberra", "AUS Eastern Standard Time"), + GUAM(122, "(UTC+10:00) Guam", "West Pacific Standard Time"), + HOBART(123, "(UTC+10:00) Hobart", "Tasmania Standard Time"), + VLADIVOSTOK(124, "(UTC+10:00) Vladivostok", "Vladivostok Standard Time"), + LORD_HOWE_ISLAND(125, "(UTC+10:30) Lord Howe Island", "Lord Howe Standard Time"), + BOUGAINVILLE_ISLAND(126, "(UTC+11:00) Bougainville Island", "Bougainville Standard Time"), + CHOKURDAKH(127, "(UTC+11:00) Chokurdakh", "Russia Time Zone 10"), + MAGADAN(128, "(UTC+11:00) Magadan", "Magadan Standard Time"), + NORFOLK_ISLAND(129, "(UTC+11:00) Norfolk Island", "Norfolk Standard Time"), + SAKHALIN(130, "(UTC+11:00) Sakhalin", "Sakhalin Standard Time"), + SOLOMON_IS(131, "(UTC+11:00) Solomon Is.", "Central Pacific Standard Time"), + ANADYR(132, "(UTC+12:00) Anadyr", "Russia Time Zone 11"), + AUCKLAND(133, "(UTC+12:00) Auckland", "New Zealand Standard Time"), + COORDINATED_UNIVERSAL_TIME_12(134, "(UTC+12:00) Coordinated Universal Time+12", "UTC+12"), + FIJI(135, "(UTC+12:00) Fiji", "Fiji Standard Time"), + PETROPAVLOVSK_KAMCHATSKY_OLD(136, "(UTC+12:00) Petropavlovsk-Kamchatsky - Old", "Kamchatka Standard Time"), + CHATHAM_ISLANDS(137, "(UTC+12:45) Chatham Islands", "Chatham Islands Standard Time"), + COORDINATED_UNIVERSAL_TIME_13(138, "(UTC+13:00) Coordinated Universal Time+13", "UTC+13"), + NUKU_ALOFA(139, "(UTC+13:00) Nuku'alofa", "Tonga Standard Time"), + SAMOA(140, "(UTC+13:00) Samoa", "Samoa Standard Time"), + KIRITIMATI_ISLAND(141, "(UTC+14:00) Kiritimati Island", "Line Islands Standard Time"); + + public static final Map VALUE_MAPPING = Map.copyOf(Arrays.stream(values()) + .collect(Collectors.toMap(v -> v.intValue, Function.identity()))); + + /** + * This integer value is used for sending + */ + private final Integer intValue; + /** + * This string value is what is used when sending this enum TO the server + */ + private final String queryValue; + /** + * This string value is what is used when receiving this enum FROM the server + */ + private final String responseValue; + + @Override + @JsonValue + public String toString() { + return queryValue; + } + + /** + * Parses the incoming value to either the one of the string or integer value, whichever the server is using. + * + * @param value the incoming value from the server + * @return the event type that matches the incoming value + */ + @Generated //manually placed annotation to tell jacoco coverage report to ignore this + @JsonCreator + public static TimeZone fromValue(Object value) { + if (value instanceof Number) { + int intVal = ((Number) value).intValue(); + for (TimeZone type : values()) { + if (type.intValue == intVal) return type; + } + } else if (value instanceof String strVal) { + for (TimeZone type : values()) { + if (type.queryValue.equalsIgnoreCase(strVal) || type.responseValue.equalsIgnoreCase(strVal)) { + return type; + } + } + } + throw new IllegalArgumentException("Unexpected value '" + value + "' for TimeZone"); + } +} diff --git a/core/src/main/java/org/justserve/model/User.java b/core/src/main/java/org/justserve/model/User.java new file mode 100644 index 0000000..625d0b5 --- /dev/null +++ b/core/src/main/java/org/justserve/model/User.java @@ -0,0 +1,7 @@ +package org.justserve.model; + +import io.micronaut.serde.annotation.Serdeable; + +@Serdeable +public class User { +} diff --git a/core/src/main/java/org/justserve/model/graph/CreateEventMutation.java b/core/src/main/java/org/justserve/model/graph/CreateEventMutation.java new file mode 100644 index 0000000..563df3b --- /dev/null +++ b/core/src/main/java/org/justserve/model/graph/CreateEventMutation.java @@ -0,0 +1,41 @@ +package org.justserve.model.graph; + +import com.fasterxml.jackson.annotation.JsonPropertyOrder; +import io.micronaut.serde.annotation.Serdeable; + +/** + * Data Transfer Object for the {@code createEvent} GraphQL mutation. + * This class dynamically constructs the mutation query based on the non-null fields + * provided in the {@link CreateEventVariables}. + * + * @author Jonathan Zollinger + * @since 0.1.0 + */ +@Serdeable +@JsonPropertyOrder({"query", "variables"}) +public class CreateEventMutation extends GraphMutation { + + public CreateEventMutation(CreateEventVariables variables) { + this.query = """ + mutation createEvent($projectId: ID!, $projectEvent: UpdateProjectEventInput!) { + createEvent( + projectId: $projectId + projectEvent: $projectEvent + ) { + %s + } + } + """; + this.variables = variables; + } + + @Override + public CreateEventVariables getVariables() { + return (CreateEventVariables) super.getVariables(); + } + + @Override + public String getQuery() { + return String.format(query, getVariables().getProjectEvent().getMutationFields()); + } +} diff --git a/core/src/main/java/org/justserve/model/graph/CreateEventVariables.java b/core/src/main/java/org/justserve/model/graph/CreateEventVariables.java new file mode 100644 index 0000000..0c5ce9b --- /dev/null +++ b/core/src/main/java/org/justserve/model/graph/CreateEventVariables.java @@ -0,0 +1,28 @@ +package org.justserve.model.graph; + +import io.micronaut.serde.annotation.Serdeable; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; +import org.justserve.model.ProjectEvent; + +import java.util.UUID; + +/** + * Pojo to serialize the variables object passed with a createEvent mutation. + * + * @author Jonathan Zollinger + * @since 0.1.0 + */ +@EqualsAndHashCode(callSuper = true) +@Serdeable +@Data +@AllArgsConstructor +@NoArgsConstructor +@Accessors(chain = true) +public class CreateEventVariables extends GraphVariables { + private UUID projectId; + private ProjectEvent projectEvent; +} diff --git a/core/src/main/java/org/justserve/model/graph/CreateEventsData.java b/core/src/main/java/org/justserve/model/graph/CreateEventsData.java new file mode 100644 index 0000000..dac2b2e --- /dev/null +++ b/core/src/main/java/org/justserve/model/graph/CreateEventsData.java @@ -0,0 +1,43 @@ +package org.justserve.model.graph; + +import com.fasterxml.jackson.annotation.JsonAnySetter; +import com.fasterxml.jackson.annotation.JsonIgnore; +import io.micronaut.serde.annotation.Serdeable; +import lombok.Data; +import org.justserve.model.ProjectEvent; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * Parses the dynamic response from the createEvents GraphQL mutation. + * + * @author Jonathan Zollinger + * @since 0.1.0 + */ +@Serdeable +@Data +public class CreateEventsData { + + @JsonIgnore + private Map events = new HashMap<>(); + + @JsonAnySetter + public void addEvent(String key, ProjectEvent event) { + if (key != null && key.startsWith("event")) { + events.put(key, event); + } + } + + /** + * Helper to return all the dynamically parsed events as a single list. + * + * @return List of newly created events + */ + @JsonIgnore + public List getEventList() { + return new ArrayList<>(events.values()); + } +} diff --git a/core/src/main/java/org/justserve/model/graph/CreateEventsMutation.java b/core/src/main/java/org/justserve/model/graph/CreateEventsMutation.java new file mode 100644 index 0000000..e328834 --- /dev/null +++ b/core/src/main/java/org/justserve/model/graph/CreateEventsMutation.java @@ -0,0 +1,70 @@ +package org.justserve.model.graph; + +import com.fasterxml.jackson.annotation.JsonPropertyOrder; +import io.micronaut.serde.annotation.Serdeable; + +import java.util.List; +import java.util.stream.IntStream; + +/** + * Data Transfer Object for the {@code createEvents} GraphQL mutation. + * This class dynamically constructs the mutation query based on the + * events provided in the {@link CreateEventsVariables}. + * + * @author Jonathan Zollinger + * @since 0.1.0 + */ +@Serdeable +@JsonPropertyOrder({"query", "variables"}) +public class CreateEventsMutation extends GraphMutation { + + public CreateEventsMutation(CreateEventsVariables variables) { + if (variables.getProjectEvents().isEmpty()) { + throw new IllegalArgumentException("At least one event must be provided"); + } + this.query = buildQuery(variables); + this.variables = variables; + } + + private String buildQuery(CreateEventsVariables variables) { + StringBuilder args = new StringBuilder("$projectId: ID!"); + StringBuilder body = new StringBuilder(); + + List sortedKeys = variables.getProjectEvents().keySet().stream().sorted().toList(); + + IntStream.range(0, sortedKeys.size()).forEach(i -> { + String key = sortedKeys.get(i); + args.append(", $").append(key).append(": UpdateProjectEventInput!"); + body.append(String.format(""" + %s: createEvent( + projectId: $projectId + projectEvent: $%s + ) { + %%s + } + """, "event" + i, key)); + }); + + return String.format(""" + mutation createEvents(%s) { + %s} + """, args, body); + } + + @Override + public CreateEventsVariables getVariables() { + return (CreateEventsVariables) super.getVariables(); + } + + @Override + public String getQuery() { + CreateEventsVariables vars = getVariables(); + + List sortedKeys = vars.getProjectEvents().keySet().stream().sorted().toList(); + + Object[] fieldsArray = sortedKeys.stream().map(sortedKey -> vars.getProjectEvents().get(sortedKey) + .getMutationFields()).toArray(); + + return String.format(query, fieldsArray); + } +} diff --git a/core/src/main/java/org/justserve/model/graph/CreateEventsVariables.java b/core/src/main/java/org/justserve/model/graph/CreateEventsVariables.java new file mode 100644 index 0000000..d42c523 --- /dev/null +++ b/core/src/main/java/org/justserve/model/graph/CreateEventsVariables.java @@ -0,0 +1,46 @@ +package org.justserve.model.graph; + +import com.fasterxml.jackson.annotation.JsonAnyGetter; +import com.fasterxml.jackson.annotation.JsonIgnore; +import io.micronaut.serde.annotation.Serdeable; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.NonNull; +import lombok.experimental.Accessors; +import org.justserve.model.ProjectEvent; + +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; + +/** + * Pojo to serialize the variables object passed with a createEvents mutation. + * + * @author Jonathan Zollinger + * @since 0.1.0 + */ +@EqualsAndHashCode(callSuper = true) +@Serdeable +@Data +@NoArgsConstructor +@Accessors(chain = true) +public class CreateEventsVariables extends GraphVariables { + + private UUID projectId; + + @JsonIgnore + private Map projectEvents = new HashMap<>(); + + public CreateEventsVariables(UUID projectId, @NonNull ProjectEvent... events) { + this.projectId = projectId; + for (int i = 0; i < events.length; i++) { + this.projectEvents.put("projectEvent" + i, events[i]); + } + } + + @JsonAnyGetter + public Map getProjectEvents() { + return projectEvents; + } +} diff --git a/core/src/main/java/org/justserve/model/graph/CreateRecurringEventsData.java b/core/src/main/java/org/justserve/model/graph/CreateRecurringEventsData.java new file mode 100644 index 0000000..41fd297 --- /dev/null +++ b/core/src/main/java/org/justserve/model/graph/CreateRecurringEventsData.java @@ -0,0 +1,43 @@ +package org.justserve.model.graph; + +import com.fasterxml.jackson.annotation.JsonAnySetter; +import com.fasterxml.jackson.annotation.JsonIgnore; +import io.micronaut.serde.annotation.Serdeable; +import lombok.Data; +import org.justserve.model.ProjectRecurringTime; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * Parses the dynamic response from the createRecurringEvents GraphQL mutation. + * + * @author Jonathan Zollinger + * @since 0.1.0 + */ +@Serdeable +@Data +public class CreateRecurringEventsData { + + @JsonIgnore + private Map events = new HashMap<>(); + + @JsonAnySetter + public void addEvent(String key, ProjectRecurringTime event) { + if (key != null && key.startsWith("event")) { + events.put(key, event); + } + } + + /** + * Helper to return all the dynamically parsed events as a single list. + * + * @return List of newly created recurring events + */ + @JsonIgnore + public List getEventList() { + return new ArrayList<>(events.values()); + } +} diff --git a/core/src/main/java/org/justserve/model/graph/CreateRecurringEventsMutation.java b/core/src/main/java/org/justserve/model/graph/CreateRecurringEventsMutation.java new file mode 100644 index 0000000..0c0127b --- /dev/null +++ b/core/src/main/java/org/justserve/model/graph/CreateRecurringEventsMutation.java @@ -0,0 +1,78 @@ +package org.justserve.model.graph; + +import com.fasterxml.jackson.annotation.JsonPropertyOrder; +import io.micronaut.serde.annotation.Serdeable; +import org.justserve.model.ProjectRecurringTime; + +import java.util.List; +import java.util.UUID; +import java.util.stream.IntStream; + +/** + * Data Transfer Object for the {@code createRecurringEvents} GraphQL mutation. + * This class dynamically constructs the mutation query based on the + * events provided in the {@link CreateRecurringEventsVariables}. + * + * @author Jonathan Zollinger + * @since 0.1.0 + */ +@Serdeable +@JsonPropertyOrder({"query", "variables"}) +public class CreateRecurringEventsMutation extends GraphMutation { + + public CreateRecurringEventsMutation(UUID projectId, ProjectRecurringTime... events) { + this(new CreateRecurringEventsVariables(projectId, events)); + } + + public CreateRecurringEventsMutation(CreateRecurringEventsVariables variables) { + if (variables.getProjectEvents() == null || variables.getProjectEvents().isEmpty()) { + throw new IllegalArgumentException("At least one event must be provided"); + } + this.query = buildQuery(variables); + this.variables = variables; + } + + private String buildQuery(CreateRecurringEventsVariables variables) { + StringBuilder args = new StringBuilder("$projectId: ID!"); + StringBuilder body = new StringBuilder(); + + List sortedKeys = variables.getProjectEvents().keySet().stream().sorted().toList(); + + String template = """ + %s: addRecurringTime( + projectId: $projectId + recurringTime: $%s + ) { + %%s + } + """; + + IntStream.range(0, sortedKeys.size()).forEach(i -> { + String key = sortedKeys.get(i); + args.append(", $").append(key).append(": CreateProjectRecurringTimeInput!"); + body.append(String.format(template, "event" + i, key)); + }); + + return String.format(""" + mutation createRecurringEvents(%s) { + %s} + """, args, body); + } + + @Override + public CreateRecurringEventsVariables getVariables() { + return (CreateRecurringEventsVariables) super.getVariables(); + } + + @Override + public String getQuery() { + CreateRecurringEventsVariables vars = getVariables(); + + List sortedKeys = vars.getProjectEvents().keySet().stream().sorted().toList(); + + Object[] fieldsArray = sortedKeys.stream().map(sortedKey -> vars.getProjectEvents().get(sortedKey) + .getMutationFields()).toArray(); + + return String.format(query, fieldsArray); + } +} diff --git a/core/src/main/java/org/justserve/model/graph/CreateRecurringEventsVariables.java b/core/src/main/java/org/justserve/model/graph/CreateRecurringEventsVariables.java new file mode 100644 index 0000000..ebf9337 --- /dev/null +++ b/core/src/main/java/org/justserve/model/graph/CreateRecurringEventsVariables.java @@ -0,0 +1,46 @@ +package org.justserve.model.graph; + +import com.fasterxml.jackson.annotation.JsonAnyGetter; +import com.fasterxml.jackson.annotation.JsonIgnore; +import io.micronaut.serde.annotation.Serdeable; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.NonNull; +import lombok.experimental.Accessors; +import org.justserve.model.ProjectRecurringTime; + +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; + +/** + * Pojo to serialize the variables object passed with a createRecurringEvents mutation. + * + * @author Jonathan Zollinger + * @since 0.1.0 + */ +@EqualsAndHashCode(callSuper = true) +@Serdeable +@Data +@NoArgsConstructor +@Accessors(chain = true) +public class CreateRecurringEventsVariables extends GraphVariables { + + private UUID projectId; + + @JsonIgnore + private Map projectEvents = new HashMap<>(); + + public CreateRecurringEventsVariables(UUID projectId, @NonNull ProjectRecurringTime... events) { + this.projectId = projectId; + for (int i = 0; i < events.length; i++) { + this.projectEvents.put("projectEvent" + i, events[i]); + } + } + + @JsonAnyGetter + public Map getProjectEvents() { + return projectEvents; + } +} diff --git a/core/src/main/java/org/justserve/model/graph/GraphFields.java b/core/src/main/java/org/justserve/model/graph/GraphFields.java new file mode 100644 index 0000000..0d092c1 --- /dev/null +++ b/core/src/main/java/org/justserve/model/graph/GraphFields.java @@ -0,0 +1,52 @@ +package org.justserve.model.graph; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import io.micronaut.core.annotation.Introspected; +import io.micronaut.core.beans.BeanIntrospection; +import io.micronaut.core.beans.BeanIntrospector; +import io.micronaut.core.naming.Named; +import io.micronaut.serde.annotation.Serdeable; + +import java.util.stream.Collectors; + +/** + *

Fields used in a graphql mutation.

+ * These are the fields used in{@link GraphMutation#query} fields. + * + * @author Jonathan Zollinger + * @since 0.1.0 + */ +@Introspected +@Serdeable +public class GraphFields { + + @JsonIgnore + protected String getMutationFields() { + return getFieldsForBean(this); + } + + /** + * Reflection free implementation of querying non-null fields for a bean. + * + * @param bean class which is being queried + * @param class type + * @return all fields that are not null + */ + private static String getFieldsForBean(T bean) { + @SuppressWarnings("unchecked") + Class beanClass = (Class) bean.getClass(); + BeanIntrospection introspection = BeanIntrospector.SHARED.getIntrospection(beanClass); + + return introspection.getBeanProperties().stream() + .filter(prop -> !prop.getName().equals("mutationFields")) + .filter(prop -> { + try { + return prop.get(bean) != null; + } catch (Exception e) { + return false; + } + }) + .map(Named::getName) + .collect(Collectors.joining("\n ")); + } +} diff --git a/core/src/main/java/org/justserve/model/graph/GraphMutation.java b/core/src/main/java/org/justserve/model/graph/GraphMutation.java new file mode 100644 index 0000000..e9afc71 --- /dev/null +++ b/core/src/main/java/org/justserve/model/graph/GraphMutation.java @@ -0,0 +1,80 @@ +package org.justserve.model.graph; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonPropertyOrder; +import io.micronaut.serde.annotation.Serdeable; +import jakarta.validation.constraints.Pattern; +import lombok.Getter; +import org.justserve.client.GraphQLClient; + +/** + * Abstract base class used to serialize the JSON payload sent via the{@link GraphQLClient} + * for GraphQL operations. + * + * @author Jonathan Zollinger + * @since 0.1.0 + */ +@Serdeable +@Getter +@JsonPropertyOrder({"query", "variables"}) +public abstract class GraphMutation { + /** + *

Mutation Query String

formatted to receive a (@code \n) delimited string of variable names.
+ * The query is to include the mutation's signature, as well as its opening and closing curly braces.
+ *
Example:
{@code "mutation ($projectId: ID!, $attachmentId: ID!) {\\n %s\\n}" } + */ + //"^mutation\\b[\\s\\S]*\\{[\\s\\S]*%s[\\s\\S]*\\}$" + @Pattern(regexp = "^mutation.*\\{.*%s.*}.*", message = "Query must begin with 'mutation' and include a '%s' placeholder") + String query; + + @JsonProperty("variables") + GraphVariables variables; + + /** + *

{@summary Gets the query string used in this mutation object.}

+ * Produces the dynamic string used for the mutation. Fields with null values are not included in the query. + *
Example:
+ * {@code getQuery} would return this string if the variables include non-null values for id, projectId, contactEmail, + * contactName, contactPhone, start and end.
+ *
+     * mutation createEvent($projectId: ID!, $projectEvent: UpdateProjectEventInput!) {
+     *      createEvent(
+     *          projectId: $projectId
+     *          projectEvent: $projectEvent
+     *      ) {
+     *          id
+     *          projectId
+     *          contactEmail
+     *          contactName
+     *          contactPhone
+     *          start
+     *          end
+     *      }
+     * }
+     * 
+ *

+ * {@code getQuery} would return this string if the variables did not include non-null values for the contact + * information: + * + *

+     * mutation createEvent($projectId: ID!, $projectEvent: UpdateProjectEventInput!) {
+     *      createEvent(
+     *          projectId: $projectId
+     *          projectEvent: $projectEvent
+     *      ) {
+     *          id
+     *          projectId
+     *          start
+     *          end
+     *      }
+     * }
+     * 
+ * + *

NOTE

+ * Only the value property names values are provided in this part of the query. See{@link GraphFields#getMutationFields()} + * + * @return The entire graphql mutation string. + * + */ + public abstract String getQuery(); +} diff --git a/core/src/main/java/org/justserve/model/GraphQLResponse.java b/core/src/main/java/org/justserve/model/graph/GraphQLResponse.java similarity index 60% rename from core/src/main/java/org/justserve/model/GraphQLResponse.java rename to core/src/main/java/org/justserve/model/graph/GraphQLResponse.java index 8f542a0..d77a1d4 100644 --- a/core/src/main/java/org/justserve/model/GraphQLResponse.java +++ b/core/src/main/java/org/justserve/model/graph/GraphQLResponse.java @@ -1,4 +1,4 @@ -package org.justserve.model; +package org.justserve.model.graph; import io.micronaut.serde.annotation.Serdeable; import lombok.AllArgsConstructor; @@ -7,6 +7,14 @@ import java.util.List; +/** + *

GraphQL Response

+ * A generic wrapper for GraphQL API responses, containing the data payload and any execution errors. + * + * @param Underlying response object + * @author Jonathan Zollinger + * @since 0.1.0 + */ @Serdeable @Data @NoArgsConstructor diff --git a/core/src/main/java/org/justserve/model/graph/GraphVariables.java b/core/src/main/java/org/justserve/model/graph/GraphVariables.java new file mode 100644 index 0000000..92708c7 --- /dev/null +++ b/core/src/main/java/org/justserve/model/graph/GraphVariables.java @@ -0,0 +1,14 @@ +package org.justserve.model.graph; + +import io.micronaut.serde.annotation.Serdeable; + +/** + * Parent class to any variables to be used in a{@link GraphMutation#variables} setting. + * + * @author Jonathan Zollinger + * @since 0.1.0 + */ +@Serdeable +public class GraphVariables { + +} diff --git a/core/src/main/resources/schema.yml b/core/src/main/resources/schema.yml index e75098e..5f21842 100644 --- a/core/src/main/resources/schema.yml +++ b/core/src/main/resources/schema.yml @@ -339,6 +339,8 @@ paths: components: schemas: + TimeZone: + type: string GraphQLCreateEventData: type: object properties: @@ -354,7 +356,7 @@ components: end: { type: string, format: date-time } groupCap: { type: boolean } groupLimit: { type: integer, format: int32 } - timezone: { type: string } + timezone: { $ref: '#/components/schemas/TimeZone' } totalVolunteersNeeded: { type: integer, format: int32 } volunteerCap: { type: boolean } GraphQLSearchOrganizationData: @@ -944,16 +946,17 @@ components: relativityScore: { type: number, format: double } additionalProperties: false ProjectStatus: - type: string - enum: - - None - - Published - - Submitted - - Draft - - Template - - OnHold - - Cancelled - - Declined + type: integer + format: int32 + enum: [ 1, 2, 3, 4, 5, 6, 7 ] + x-enum-varnames: + - PUBLISHED + - SUBMITTED + - DRAFT + - TEMPLATE + - ON_HOLD + - CANCELLED + - DECLINED UserSlimSearchResult: description: | high level information for a given user @@ -2003,10 +2006,16 @@ components: properties: query: { type: string } variables: { $ref: '#/components/schemas/GraphQLCreateEventVariables' } + GraphQLCreateOngoingEventRequest: + type: object + properties: + query: { type: string } + variables: { $ref: '#/components/schemas/GraphQLCreateOngoingEventVariables' } GraphQLCreateEventVariables: type: object properties: - projectId: + id: + description: ID for the overall project type: string format: uuid projectEvent: @@ -2033,11 +2042,39 @@ components: type: string format: date-time timezone: - type: string + $ref: '#/components/schemas/TimeZone' totalVolunteersNeeded: type: integer volunteerCap: type: boolean + GraphQLCreateOngoingEventVariables: + type: object + properties: + id: + description: ID for the overall project + type: string + format: uuid + projectEvent: + type: object + properties: + contactEmail: + type: string + format: email + maxLength: 200 + contactName: + type: string + maxLength: 200 + contactPhone: + type: string + format: phone + maxLength: 139 + schedule: + type: string + maxLength: 100 # will be corrected to 300 + shiftTitle: + type: string + maxLength: 100 # will be corrected to 300 + GraphQLCreateProjectRequest: type: object properties: diff --git a/core/src/test/groovy/org/justserve/GraphQLClientSpec.groovy b/core/src/test/groovy/org/justserve/GraphQLClientSpec.groovy index 02a2896..1706118 100644 --- a/core/src/test/groovy/org/justserve/GraphQLClientSpec.groovy +++ b/core/src/test/groovy/org/justserve/GraphQLClientSpec.groovy @@ -2,12 +2,15 @@ package org.justserve import io.micronaut.test.extensions.spock.annotation.MicronautTest import jakarta.inject.Inject +import net.datafaker.Faker import org.justserve.client.GraphQLClient -import org.justserve.model.EventType -import org.justserve.model.GraphQLCreateProjectVariables -import org.justserve.model.ProjectLocationType +import org.justserve.model.* +import org.justserve.model.graph.* import spock.lang.Shared import spock.lang.Specification +import spock.lang.Unroll + +import java.util.concurrent.TimeUnit @MicronautTest class GraphQLClientSpec extends Specification { @@ -16,6 +19,25 @@ class GraphQLClientSpec extends Specification { @Inject GraphQLClient client + @Shared + Faker faker = new Faker() + + @Shared + Map projectIds = [:] + + + def setupSpec() { + EventType.values().each { type -> + def project = client.createProject(new GraphQLCreateProjectVariables() + .setTitle("Test Project - ${type.name()}") + .setEventType(type) + .setLocationType(ProjectLocationType.SINGLE_LOCATION) + ) + projectIds[type] = project.getData().getCreateProject().getId() + } + } + + @SuppressWarnings("GroovyAssignabilityCheck") void "can create Project with EventType: #eventType, LocationType: #locationType, and Redirect: #redirect"(EventType eventType, ProjectLocationType locationType, String redirect) { given: GraphQLCreateProjectVariables args = new GraphQLCreateProjectVariables() @@ -25,12 +47,324 @@ class GraphQLClientSpec extends Specification { .setRedirect(redirect) when: - client.createProject(args) + def response = client.createProject(args) then: noExceptionThrown() + !response.hasErrors() where: [eventType, locationType, redirect] << [EventType.values(), ProjectLocationType.values(), ["", null, "https://google.com"]].combinations() } + + private ProjectEvent.ProjectEventBuilder baseEventBuilder() { + return ProjectEvent.builder() + .start(Date.from(faker.timeAndDate().future(180, TimeUnit.DAYS))) + .end(Date.from(faker.timeAndDate().future(365, TimeUnit.DAYS))) + } + + @Unroll("can set contact info for #eventType.name() event") + def "can set contact info for #eventType event"() { + given: + def event = baseEventBuilder() + .contactEmail(faker.internet().emailAddress()) + .contactName(faker.name().fullName()) + .contactPhone(faker.phoneNumber().phoneNumber()) + .build() + def vars = new CreateEventVariables().setProjectId(projectIds[eventType]).setProjectEvent(event) + + when: + def response = client.createEvent(new CreateEventMutation(vars)) + + then: + noExceptionThrown() + !response.hasErrors() + + where: + eventType << [EventType.DTL, EventType.Ongoing, EventType.MultipleDTL] + } + + @Unroll("can set dates for #eventType.name() event") + def "can set dates for #eventType event"() { + given: + def event = baseEventBuilder() + .renewDate(Date.from(faker.timeAndDate().future(730, TimeUnit.DAYS))) + .build() + def vars = new CreateEventVariables().setProjectId(projectIds[eventType]).setProjectEvent(event) + + when: + def response = client.createEvent(new CreateEventMutation(vars)) + + then: + noExceptionThrown() + !response.hasErrors() + + where: + eventType << [EventType.MultipleDTL] + } + + @Unroll("can set group info for #eventType.name() event") + def "can set group info for #eventType event"() { + given: + def event = baseEventBuilder() + .groupCap(true) + .groupLimit(10) + .build() + def vars = new CreateEventVariables().setProjectId(projectIds[eventType]).setProjectEvent(event) + + when: + def response = client.createEvent(new CreateEventMutation(vars)) + + then: + noExceptionThrown() + !response.hasErrors() + + where: // error reads "Only a multiple DTL project can have more than one event" + eventType << [/*EventType.DTL,*/ EventType.Ongoing, EventType.MultipleDTL] + } + + @Unroll("can set location info for #eventType.name() event") + def "can set location info for #eventType event"() { + given: + def event = baseEventBuilder() + .locationLink(faker.internet().url()) + .locationName(faker.address().streetAddress()) + .build() + def vars = new CreateEventVariables().setProjectId(projectIds[eventType]).setProjectEvent(event) + + when: + def response = client.createEvent(new CreateEventMutation(vars)) + + then: + noExceptionThrown() + !response.hasErrors() + + where: //error reads "Only a multiple DTL project can have more than one event, StackTrace= at JustServe.Mediators.ProjectEvents.ProjectEventMediator.InternalCreateEvent(Project project, UpdateProjectEvent updateProjectEvent, SecurityContext securityContext) in /src/src/JustServe.Mediators/ProjectEvents/ProjectEventMediator.cs:line 109" + eventType << [EventType.DTL, /*EventType.Ongoing,*/ EventType.MultipleDTL] + } + + @Unroll("can set schedule info for #eventType.name() event") + def "can set schedule info for #eventType event"() { + given: + def schedule = faker.lorem().sentence() + def event = baseEventBuilder() + .schedule(schedule) + .shiftTitle(schedule) + .build() + def vars = new CreateEventVariables().setProjectId(projectIds[eventType]).setProjectEvent(event) + + when: + def response = client.createEvent(new CreateEventMutation(vars)) + + then: + noExceptionThrown() + !response.hasErrors() + + where: + eventType << [EventType.DTL, EventType.Ongoing, EventType.MultipleDTL] + } + + @Unroll("can set volunteer info for #eventType.name() event") + def "can set volunteer info for #eventType event"() { + given: + def event = baseEventBuilder() + .volunteerCap(true) + .totalVolunteersNeeded(20) + .build() + def vars = new CreateEventVariables().setProjectId(projectIds[eventType]).setProjectEvent(event) + + when: + def response = client.createEvent(new CreateEventMutation(vars)) + + then: + noExceptionThrown() + !response.hasErrors() + + where: + eventType << [EventType.DTL, EventType.Ongoing, EventType.MultipleDTL] + } + + @Unroll("can set miscellaneous info for #eventType.name() event") + def "can set miscellaneous info for #eventType event"() { + given: + def event = baseEventBuilder() + .qrCodeImageLocation(faker.internet().url()) + .specialDirections(faker.lorem().paragraph()) + .status(ProjectEventStatus.ACTIVE) + .timezone(TimeZone.ARIZONA) + .build() + def vars = new CreateEventVariables().setProjectId(projectIds[eventType]).setProjectEvent(event) + + when: + def response = client.createEvent(new CreateEventMutation(vars)) + + then: + noExceptionThrown() + !response.hasErrors() + + where: + eventType << [EventType.DTL, EventType.Ongoing, EventType.MultipleDTL] + } + + @Unroll("cannot manually create event for #eventType.name() project") + def "cannot manually create event for invalid project types"() { + given: + def event = baseEventBuilder().build() + def vars = new CreateEventVariables().setProjectId(projectIds[eventType]).setProjectEvent(event) + + when: + def response = client.createEvent(new CreateEventMutation(vars)) + + then: + response.hasErrors() + + where: + eventType << [EventType.Recurring] + } + + @Unroll("can add multiple events only for #eventType.name() projects (shouldFail: #shouldFail)") + def "can add multiple events only for Multi-DTL projects"() { + given: + def firstEvent = baseEventBuilder().build() + def secondEvent = baseEventBuilder().build() + def firstVars = new CreateEventVariables().setProjectId(projectIds[eventType]).setProjectEvent(firstEvent) + def secondVars = new CreateEventVariables().setProjectId(projectIds[eventType]).setProjectEvent(secondEvent) + + when: + client.createEvent(new CreateEventMutation(firstVars)) + def secondResponse = client.createEvent(new CreateEventMutation(secondVars)) + + then: + secondResponse.hasErrors() == shouldFail + + where: + eventType | shouldFail + EventType.DTL | true + EventType.Ongoing | true + EventType.Recurring | true + EventType.MultipleDTL | false + } + + def "can create multiple events at once for Multi-DTL projects using createEvents mutation"() { + given: + def event1 = baseEventBuilder() + .shiftTitle("Morning Shift") + .build() + def event2 = baseEventBuilder() + .shiftTitle("Afternoon Shift") + .build() + def event3 = baseEventBuilder() + .shiftTitle("Evening Shift") + .build() + def vars = new CreateEventsVariables(projectIds[EventType.MultipleDTL], event1, event2, event3) + def mutation = new CreateEventsMutation(vars) + + when: + def response = client.createEvents(mutation) + + then: + noExceptionThrown() + !response.hasErrors() + + and: "the response data should contain the newly created events" + null != response.getData() + } + + def "can create multiple recurring events at once for Recurring projects using createRecurringEvents mutation"() { + given: + def time1 = new ProjectRecurringTime() + .setStartTime("10:00") + .setEndTime("12:00") + .setRecurringType(RecurringType.WEEKLY) + .setMonday(true) + def time2 = new ProjectRecurringTime() + .setStartTime("14:00") + .setEndTime("16:00") + .setRecurringType(RecurringType.MONTHLY) + .setThirdWeek(true) + def vars = new CreateRecurringEventsVariables(projectIds[EventType.Recurring], time1, time2) + def mutation = new CreateRecurringEventsMutation(vars) + + when: + def response = client.createRecurringEvents(mutation) + + then: + noExceptionThrown() + !response.hasErrors() + + and: "the response data should contain the newly created recurring events" + null != response.getData() + } + + def "cannot create event without start and end dates"() { + when: + ProjectEvent.builder().build() + + then: + thrown(IllegalStateException) + } + + def "cannot set groupCap without groupLimit"() { + when: + baseEventBuilder().groupCap(true).build() + + then: + thrown(IllegalStateException) + } + + def "cannot set volunteerCap without totalVolunteersNeeded"() { + when: + baseEventBuilder().volunteerCap(true).build() + + then: + thrown(IllegalStateException) + } + + def "cannot set volunteersCapped without volunteersNeeded on ProjectRecurringTime"() { + when: + ProjectRecurringTime.builder().volunteersCapped(true).build() + + then: + thrown(IllegalStateException) + } + + def "can set all info for ProjectRecurringTime"() { + given: + def time1 = ProjectRecurringTime.builder() + .contactEmail(faker.internet().emailAddress()) + .contactName(faker.name().fullName()) + .contactPhone(faker.phoneNumber().phoneNumber()) + .startTime("10:00") + .endTime("12:00") + .firstWeek(true) + .secondWeek(true) + .thirdWeek(true) + .fourthWeek(true) + .fifthWeek(true) + .lastWeek(true) + .groupLimit(10) + .recurringType(RecurringType.WEEKLY) + .recurringDaysOfMonths([1, 15, 28]) + .specialDirections(faker.lorem().paragraph()) + .volunteersCapped(true) + .volunteersNeeded(5) + .monday(true) + .tuesday(true) + .wednesday(true) + .thursday(true) + .friday(true) + .saturday(true) + .sunday(true) + .build() + + def vars = new CreateRecurringEventsVariables(projectIds[EventType.Recurring], time1) + def mutation = new CreateRecurringEventsMutation(vars) + + when: + def response = client.createRecurringEvents(mutation) + + then: + noExceptionThrown() + !response.hasErrors() + } }