Unverified Commit efc3c447 authored by Martin Lowe's avatar Martin Lowe 🇨🇦 Committed by GitHub
Browse files

Add Quartz scheduler to clean up stale form entries. (#313)

* Add Quartz scheduler to clean up stale form entries.

* Update scheduled DB clean to use java time lib rather than util time

* Update default to be 60d for db stale inactive clean up
parent da772198
......@@ -8,17 +8,15 @@
<properties>
<eclipse-api-version>0.2-SNAPSHOT</eclipse-api-version>
<surefire-plugin.version>2.22.1</surefire-plugin.version>
<compiler-plugin.version>3.8.1</compiler-plugin.version>
<maven.compiler.target>11</maven.compiler.target>
<maven.compiler.source>11</maven.compiler.source>
<quarkus.platform.artifact-id>quarkus-universe-bom</quarkus.platform.artifact-id>
<quarkus.platform.group-id>io.quarkus</quarkus.platform.group-id>
<quarkus.platform.version>1.13.7.Final</quarkus.platform.version>
<maven.compiler.source>11</maven.compiler.source>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<project.reporting.outputEncoding>${project.build.sourceEncoding}</project.reporting.outputEncoding>
<maven.compiler.parameters>true</maven.compiler.parameters>
<quarkus-plugin.version>1.13.7.Final</quarkus-plugin.version>
<compiler-plugin.version>3.8.1</compiler-plugin.version>
<quarkus.platform.group-id>io.quarkus</quarkus.platform.group-id>
<openhtml.version>1.0.9</openhtml.version>
</properties>
<repositories>
......@@ -90,6 +88,10 @@
<groupId>io.quarkus</groupId>
<artifactId>quarkus-mailer</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-quartz</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-resteasy-qute</artifactId>
......
......@@ -18,6 +18,7 @@ import java.util.Date;
import java.util.List;
import java.util.Optional;
import java.util.Random;
import java.util.concurrent.ThreadLocalRandom;
import javax.annotation.PostConstruct;
import javax.enterprise.event.Observes;
......@@ -34,6 +35,7 @@ import org.eclipsefoundation.react.dto.Contact;
import org.eclipsefoundation.react.dto.FormOrganization;
import org.eclipsefoundation.react.dto.FormWorkingGroup;
import org.eclipsefoundation.react.dto.MembershipForm;
import org.eclipsefoundation.react.helper.TimeHelper;
import org.eclipsefoundation.react.namespace.ContactTypes;
import org.eclipsefoundation.react.namespace.FormState;
import org.eclipsefoundation.react.namespace.OrganizationTypes;
......@@ -89,8 +91,9 @@ public class DataLoader {
mf.setRegistrationCountry("CA");
mf.setVatNumber(RandomStringUtils.randomNumeric(10));
mf.setPurchaseOrderRequired(Math.random() > 0.5 ? "yes" : "no");
mf.setDateCreated(Math.random() > 0.5 ? System.currentTimeMillis() + r.nextInt(10000)
: System.currentTimeMillis() - r.nextInt(10000));
mf.setDateCreated(
TimeHelper.getMillis() - ThreadLocalRandom.current().nextLong(config.getDaysOut().toMillis()));
mf.setDateUpdated(mf.getDateCreated());
mf.setState(FormState.INPROGRESS);
forms.add(mf);
}
......@@ -108,9 +111,9 @@ public class DataLoader {
o.setLegalName(RandomStringUtils.randomAlphabetic(4, 10));
o.setTwitterHandle(RandomStringUtils.randomAlphabetic(4, 10));
o.setAggregateRevenue(RandomStringUtils.randomNumeric(5, 10));
o.setEmployeeCount(RandomStringUtils.randomNumeric(5, 10));
o.setEmployeeCount(RandomStringUtils.randomNumeric(5, 10));
o.setOrganizationType(OrganizationTypes.OTHER);
Address a = new Address();
Address a = new Address();
a.setCity(RandomStringUtils.randomAlphabetic(4, 10));
a.setCountry(RandomStringUtils.randomAlphabetic(4, 10));
a.setPostalCode(RandomStringUtils.randomAlphabetic(4, 10));
......
......@@ -11,12 +11,13 @@
*/
package org.eclipsefoundation.react.bootstrap;
import java.time.Duration;
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import io.quarkus.arc.config.ConfigProperties;
/**
......@@ -35,6 +36,7 @@ public class DataLoaderConfig {
private List<String> workingGroups = Arrays.asList("internet-things-iot", "jakarta-ee", "cloud-tools-development");
private List<String> membershipLevels = Arrays.asList("strategic", "contributing", "associate", "committer");
private List<String> participationLevels = Arrays.asList("platinum", "gold", "silver", "associate");
private Duration daysOut = Duration.of(60, ChronoUnit.DAYS);
/**
* @return the dataLoaderEnabled
......@@ -133,4 +135,18 @@ public class DataLoaderConfig {
public void setParticipationLevels(List<String> participationLevels) {
this.participationLevels = new ArrayList<>(participationLevels);
}
/**
* @return the daysOut
*/
public Duration getDaysOut() {
return daysOut;
}
/**
* @param daysOut the daysOut to set
*/
public void setDaysOut(Duration daysOut) {
this.daysOut = daysOut;
}
}
\ No newline at end of file
......@@ -11,6 +11,7 @@
*/
package org.eclipsefoundation.react.dto;
import java.util.List;
import java.util.Objects;
import javax.inject.Inject;
......@@ -31,6 +32,7 @@ import org.eclipsefoundation.persistence.dto.filter.DtoFilter;
import org.eclipsefoundation.persistence.model.DtoTable;
import org.eclipsefoundation.persistence.model.ParameterizedSQLStatement;
import org.eclipsefoundation.persistence.model.ParameterizedSQLStatementBuilder;
import org.eclipsefoundation.react.namespace.MembershipFormAPIParameterNames;
import org.hibernate.annotations.GenericGenerator;
/**
......@@ -184,6 +186,12 @@ public class Address extends BareNode implements TargetedClone<Address> {
new ParameterizedSQLStatement.Clause(TABLE.getAlias() + ".id = ?", new Object[] { id }));
}
}
// form IDs check
List<String> formIds = params.get(MembershipFormAPIParameterNames.FORM_IDS.getName());
if (formIds != null) {
stmt.addClause(new ParameterizedSQLStatement.Clause(TABLE.getAlias() + ".organization.form.id IN ?",
new Object[] { formIds }));
}
return stmt;
}
......
......@@ -11,6 +11,7 @@
*/
package org.eclipsefoundation.react.dto;
import java.util.List;
import java.util.Objects;
import javax.inject.Inject;
......@@ -196,12 +197,18 @@ public class Contact extends BareNode implements TargetedClone<Contact> {
new ParameterizedSQLStatement.Clause(TABLE.getAlias() + ".id = ?", new Object[] { id }));
}
}
// user ID check
// form ID check
String formId = params.getFirst(MembershipFormAPIParameterNames.FORM_ID.getName());
if (formId != null) {
stmt.addClause(new ParameterizedSQLStatement.Clause(TABLE.getAlias() + ".form.id = ?",
new Object[] { formId }));
}
// form IDs check
List<String> formIds = params.get(MembershipFormAPIParameterNames.FORM_IDS.getName());
if (formIds != null) {
stmt.addClause(new ParameterizedSQLStatement.Clause(TABLE.getAlias() + ".form.id IN ?",
new Object[] { formIds }));
}
return stmt;
}
......
......@@ -11,6 +11,7 @@
*/
package org.eclipsefoundation.react.dto;
import java.util.List;
import java.util.Objects;
import javax.inject.Inject;
......@@ -240,12 +241,18 @@ public class FormOrganization extends BareNode implements TargetedClone<FormOrga
new ParameterizedSQLStatement.Clause(TABLE.getAlias() + ".id = ?", new Object[] { id }));
}
}
// user ID check
// form ID check
String formId = params.getFirst(MembershipFormAPIParameterNames.FORM_ID.getName());
if (formId != null) {
stmt.addClause(new ParameterizedSQLStatement.Clause(TABLE.getAlias() + ".form.id = ?",
new Object[] { formId }));
}
// form IDs check
List<String> formIds = params.get(MembershipFormAPIParameterNames.FORM_IDS.getName());
if (formIds != null) {
stmt.addClause(new ParameterizedSQLStatement.Clause(TABLE.getAlias() + ".form.id IN ?",
new Object[] { formIds }));
}
return stmt;
}
......
......@@ -12,6 +12,7 @@
package org.eclipsefoundation.react.dto;
import java.util.Date;
import java.util.List;
import java.util.Objects;
import javax.inject.Inject;
......@@ -40,8 +41,7 @@ import org.hibernate.annotations.GenericGenerator;
import com.fasterxml.jackson.annotation.JsonProperty;
/**
* Represents a prospective Working Group relationship with the current
* organization (based on the form)
* Represents a prospective Working Group relationship with the current organization (based on the form)
*
* @author Martin Lowe
*/
......@@ -224,12 +224,18 @@ public class FormWorkingGroup extends BareNode implements TargetedClone<FormWork
new ParameterizedSQLStatement.Clause(TABLE.getAlias() + ".id = ?", new Object[] { id }));
}
}
// user ID check
// form ID check
String formId = params.getFirst(MembershipFormAPIParameterNames.FORM_ID.getName());
if (formId != null) {
stmt.addClause(new ParameterizedSQLStatement.Clause(TABLE.getAlias() + ".form.id = ?",
new Object[] { formId }));
}
// form IDs check
List<String> formIds = params.get(MembershipFormAPIParameterNames.FORM_IDS.getName());
if (formIds != null) {
stmt.addClause(new ParameterizedSQLStatement.Clause(TABLE.getAlias() + ".form.id IN ?",
new Object[] { formIds }));
}
return stmt;
}
......
......@@ -24,6 +24,7 @@ import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
import javax.ws.rs.core.MultivaluedMap;
import org.apache.commons.lang3.StringUtils;
import org.eclipsefoundation.core.namespace.DefaultUrlParameterNames;
import org.eclipsefoundation.persistence.dto.BareNode;
import org.eclipsefoundation.persistence.dto.filter.DtoFilter;
......@@ -56,6 +57,8 @@ public class MembershipForm extends BareNode implements TargetedClone<Membership
@SortableField
private Long dateCreated;
@SortableField
private Long dateUpdated;
@SortableField
private Long dateSubmitted;
@NotNull(message = "The form state cannot be null")
private FormState state;
......@@ -136,6 +139,20 @@ public class MembershipForm extends BareNode implements TargetedClone<Membership
this.dateCreated = dateCreated;
}
/**
* @return the dateUpdated
*/
public Long getDateUpdated() {
return dateUpdated;
}
/**
* @param dateUpdated the dateUpdated to set
*/
public void setDateUpdated(Long dateUpdated) {
this.dateUpdated = dateUpdated;
}
public Long getDateSubmitted() {
return this.dateSubmitted;
}
......@@ -176,7 +193,7 @@ public class MembershipForm extends BareNode implements TargetedClone<Membership
final int prime = 31;
int result = super.hashCode();
result = prime * result + Objects.hash(id, membershipLevel, signingAuthority, userID, vatNumber,
registrationCountry, purchaseOrderRequired, dateCreated, dateSubmitted, state);
registrationCountry, purchaseOrderRequired, dateCreated, dateUpdated, dateSubmitted, state);
return result;
}
......@@ -194,7 +211,8 @@ public class MembershipForm extends BareNode implements TargetedClone<Membership
&& Objects.equals(vatNumber, other.vatNumber) && Objects.equals(dateCreated, other.dateCreated)
&& Objects.equals(registrationCountry, other.registrationCountry)
&& Objects.equals(purchaseOrderRequired, other.purchaseOrderRequired)
&& Objects.equals(state, other.state) && Objects.equals(dateSubmitted, other.dateSubmitted);
&& Objects.equals(state, other.state) && Objects.equals(dateSubmitted, other.dateSubmitted)
&& Objects.equals(dateUpdated, other.dateUpdated);
}
@Override
......@@ -252,6 +270,13 @@ public class MembershipForm extends BareNode implements TargetedClone<Membership
stmt.addClause(new ParameterizedSQLStatement.Clause(TABLE.getAlias() + ".state = ?",
new Object[] { FormState.valueOf(formState.toUpperCase()) }));
}
String dateCreated = params
.getFirst(MembershipFormAPIParameterNames.BEFORE_DATE_UPDATED_IN_MILLIS.getName());
if (dateCreated != null && StringUtils.isNumeric(dateCreated)) {
stmt.addClause(new ParameterizedSQLStatement.Clause(TABLE.getAlias() + ".dateUpdated < ?",
new Object[] { Long.parseLong(dateCreated) }));
}
return stmt;
}
......
package org.eclipsefoundation.react.helper;
import java.time.ZoneOffset;
import java.time.ZonedDateTime;
/**
* Centralize the retrieval of date/time objects to synchronize usage of timezones across potentially varied
* environments.
*
* @author Martin Lowe
*
*/
public final class TimeHelper {
/**
* Return the current instant as a datetime object zoned to UTC.
*
* @return the current localdatetime in UTC
*/
public static ZonedDateTime now() {
return ZonedDateTime.now(ZoneOffset.UTC);
}
/**
* Gets the current epoch milli according to UTC
*
* @return the current epoch milli in UTC
*/
public static long getMillis() {
return now().toInstant().toEpochMilli();
}
/**
* Returns the epoch milli of the time passed with UTC assumed.
*
* @param time the time object to retrieve epoch millis from
* @return the epoch millis of the passed time object in UTC
*/
public static long getMillis(ZonedDateTime time) {
return time.toInstant().toEpochMilli();
}
private TimeHelper() {
}
}
......@@ -13,10 +13,12 @@ import org.eclipsefoundation.core.namespace.UrlParameterNamespace;
public final class MembershipFormAPIParameterNames implements UrlParameterNamespace {
public static final UrlParameter USER_ID = new UrlParameter("userID");
public static final UrlParameter FORM_ID = new UrlParameter("formID");
public static final UrlParameter FORM_IDS = new UrlParameter("formIDs");
public static final UrlParameter FORM_STATE = new UrlParameter("state");
public static final UrlParameter BEFORE_DATE_UPDATED_IN_MILLIS = new UrlParameter("beforeDateUpdatedMillis");
private static final List<UrlParameter> params = Collections
.unmodifiableList(Arrays.asList(USER_ID, FORM_ID, FORM_STATE));
.unmodifiableList(Arrays.asList(USER_ID, FORM_ID, FORM_IDS, FORM_STATE, BEFORE_DATE_UPDATED_IN_MILLIS));
@Override
public List<UrlParameter> getParameters() {
......
......@@ -43,6 +43,7 @@ import org.eclipsefoundation.react.dto.FormOrganization;
import org.eclipsefoundation.react.dto.FormWorkingGroup;
import org.eclipsefoundation.react.dto.MembershipForm;
import org.eclipsefoundation.react.dto.ValidationGroups.Completion;
import org.eclipsefoundation.react.helper.TimeHelper;
import org.eclipsefoundation.react.model.ConstraintViolationWrapFactory;
import org.eclipsefoundation.react.model.ConstraintViolationWrapFactory.ConstraintViolationWrap;
import org.eclipsefoundation.react.model.MailerData;
......@@ -117,7 +118,8 @@ public class MembershipFormResource extends AbstractRESTResource {
@POST
public List<MembershipForm> create(MembershipForm mem) {
mem.setUserID(ident.getPrincipal().getName());
mem.setDateCreated(System.currentTimeMillis());
mem.setDateCreated(TimeHelper.getMillis());
mem.setDateUpdated(mem.getDateCreated());
return dao.add(new RDBMSQuery<>(wrap, filters.get(MembershipForm.class)), Arrays.asList(mem));
}
......@@ -134,6 +136,7 @@ public class MembershipFormResource extends AbstractRESTResource {
return r;
}
mem.setUserID(ident.getPrincipal().getName());
mem.setDateUpdated(TimeHelper.getMillis());
// need to fetch ref to use attached entity
MembershipForm ref = mem.cloneTo(dao.getReference(formID, MembershipForm.class));
return Response.ok(dao.add(new RDBMSQuery<>(wrap, filters.get(MembershipForm.class)), Arrays.asList(ref)))
......@@ -211,7 +214,8 @@ public class MembershipFormResource extends AbstractRESTResource {
mailer.sendToMembershipTeam(data);
// update the state and push the update
mf.setDateSubmitted(System.currentTimeMillis());
mf.setDateSubmitted(TimeHelper.getMillis());
mf.setDateUpdated(mf.getDateSubmitted());
mf.setState(FormState.SUBMITTED);
return Response.ok(dao.add(new RDBMSQuery<>(wrap, filters.get(MembershipForm.class)), Arrays.asList(mf)))
.build();
......
......@@ -41,7 +41,7 @@ import org.slf4j.LoggerFactory;
@ApplicationScoped
public class DefaultOrganizationsService implements OrganizationsService {
public static final Logger LOGGER = LoggerFactory.getLogger(DefaultOrganizationsService.class);
@RestClient
@Inject
OrganizationAPI orgAPI;
......
package org.eclipsefoundation.react.tasks;
import java.time.Duration;
import java.time.ZonedDateTime;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
import javax.enterprise.context.ApplicationScoped;
import javax.inject.Inject;
import javax.ws.rs.core.MultivaluedMap;
import org.eclipse.microprofile.config.inject.ConfigProperty;
import org.eclipsefoundation.core.model.RequestWrapper;
import org.eclipsefoundation.core.namespace.DefaultUrlParameterNames;
import org.eclipsefoundation.persistence.dao.PersistenceDao;
import org.eclipsefoundation.persistence.dto.BareNode;
import org.eclipsefoundation.persistence.model.RDBMSQuery;
import org.eclipsefoundation.persistence.service.FilterService;
import org.eclipsefoundation.react.dto.Contact;
import org.eclipsefoundation.react.dto.FormOrganization;
import org.eclipsefoundation.react.dto.FormWorkingGroup;
import org.eclipsefoundation.react.dto.MembershipForm;
import org.eclipsefoundation.react.helper.TimeHelper;
import org.eclipsefoundation.react.namespace.FormState;
import org.eclipsefoundation.react.namespace.MembershipFormAPIParameterNames;
import org.jboss.resteasy.specimpl.MultivaluedMapImpl;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import io.quarkus.scheduler.Scheduled;
/**
* Schedules a task everyday to batch cleanup documents that are older than the given maxage duration.
*
* @author Martin Lowe
*
*/
@ApplicationScoped
public class ScheduledDBCleanTask {
public static final Logger LOGGER = LoggerFactory.getLogger(ScheduledDBCleanTask.class);
@ConfigProperty(name = "eclipse.scheduled.membership.enabled", defaultValue = "true")
boolean enabled;
@ConfigProperty(name = "eclipse.scheduled.membership.max-age", defaultValue = "P60D")
Duration maxAgeBeforeDeletion;
@Inject
PersistenceDao dao;
@Inject
FilterService filters;
/**
* Schedule the task every day from start up to clean up unused form entries.
*/
@Scheduled(every = "P1D")
void schedule() {
if (enabled) {
ZonedDateTime maxAge = TimeHelper.now().minus(maxAgeBeforeDeletion);
LOGGER.info("Checking for database entries updated before {}", maxAge);
// create parameter map for inprogress documents older than the configured period
MultivaluedMap<String, String> params = new MultivaluedMapImpl<>();
params.add(MembershipFormAPIParameterNames.BEFORE_DATE_UPDATED_IN_MILLIS.getName(), Long.toString(TimeHelper.getMillis(maxAge)));
params.add(MembershipFormAPIParameterNames.FORM_STATE.getName(), FormState.INPROGRESS.name());
// generate the query to get expired documents
RDBMSQuery<MembershipForm> initialQuery = new RDBMSQuery<>(new RequestWrapper(), filters.get(MembershipForm.class),
params);
initialQuery.setRoot(false);
// get the expired form objects
long size = dao.count(initialQuery);
List<MembershipForm> forms = new ArrayList<>();
int count = 0;
while (forms.size() <= size) {
// update the query to get the next page
params.add(DefaultUrlParameterNames.PAGE.getName(), Integer.toString(++count));
RDBMSQuery<MembershipForm> q = new RDBMSQuery<>(new RequestWrapper(), filters.get(MembershipForm.class), params);
q.setRoot(false);
forms.addAll(dao.get(q));
}
// build batch parameters to delete old documents
MultivaluedMap<String, String> formFKParams = new MultivaluedMapImpl<>();
formFKParams.addAll(MembershipFormAPIParameterNames.FORM_IDS.getName(),
forms.stream().map(MembershipForm::getId).collect(Collectors.toList()));
// log useful information about removed entries
LOGGER.info("Removing {} form entries from the database", size);
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("Entries being removed {}",
formFKParams.get(MembershipFormAPIParameterNames.FORM_IDS.getName()));
}
// delete the downstream entities in bulk
dao.delete(generateQuery(formFKParams, FormWorkingGroup.class));
dao.delete(generateQuery(formFKParams, Contact.class));
dao.delete(generateQuery(formFKParams, FormOrganization.class));
// delete the forms last
dao.delete(initialQuery);
} else {
LOGGER.warn("DB clean scheduled task not run as task has been disabled through configuration");
}
}
private <T extends BareNode> RDBMSQuery<T> generateQuery(MultivaluedMap<String, String> params, Class<T> type) {
RDBMSQuery<T> out = new RDBMSQuery<>(new RequestWrapper(), filters.get(type), params);
out.setRoot(false);
return out;
}
}
......@@ -59,5 +59,8 @@ quarkus.oidc.authentication.java-script-auto-redirect=false
## Optional dev settings
#%dev.quarkus.hibernate-orm.database.generation=drop-and-create
#%dev.eclipse.dataloader.enabled=true
#%dev.eclipse.dataloader.form-count=2500
#%dev.quarkus.mailer.mock=false
#%dev.quarkus.log.category."org.eclipsefoundation".level=DEBUG
#%dev.eclipse.scheduled.membership.enabled=false
......@@ -2,6 +2,7 @@ DROP TABLE IF EXISTS Address, FormWorkingGroup, Contact, FormOrganization, Membe
CREATE TABLE `MembershipForm` (
`id` varchar(255) NOT NULL,
`dateCreated` bigint(20) DEFAULT NULL,
`dateUpdated` bigint(20) DEFAULT NULL,
`dateSubmitted` bigint(20) DEFAULT NULL,
`membershipLevel` varchar(255) DEFAULT NULL,
`purchaseOrderRequired` varchar(255) DEFAULT NULL,
......@@ -23,7 +24,7 @@ CREATE TABLE `Contact` (
`form_id` varchar(255) DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `FK2g2hji3xsvakv5blqilnol5ad` (`form_id`),
CONSTRAINT `FK2g2hji3xsvakv5blqilnol5ad` FOREIGN KEY (`form_id`) REFERENCES `MembershipForm` (`id`)
CONSTRAINT `FK2g2hji3xsvakv5blqilnol5ad` FOREIGN KEY (`form_id`) REFERENCES `MembershipForm` (`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=latin1;
CREATE TABLE `FormOrganization` (
......@@ -36,7 +37,7 @@ CREATE TABLE `FormOrganization` (
`form_id` varchar(255) DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `UK_27vg3uhbmy3ev7ote4fjd4evl` (`form_id`),
CONSTRAINT `FKib65ho89l5rvfgqql7wq15oxv` FOREIGN KEY (`form_id`) REFERENCES `MembershipForm` (`id`)
CONSTRAINT `FKib65ho89l5rvfgqql7wq15oxv` FOREIGN KEY (`form_id`) REFERENCES `MembershipForm` (`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=latin1;
CREATE TABLE `FormWorkingGroup` (
......@@ -49,8 +50,8 @@ CREATE TABLE `FormWorkingGroup` (
PRIMARY KEY (`id`),
UNIQUE KEY `UK_1gt3qh8yu1jpky54o38igvfrq` (`contact_id`),
KEY `UK_2qrha8ti59jcxnvi2h8b9d2u8` (`form_id`),
CONSTRAINT `FK8mhoi37sufequy7nnn19811rv` FOREIGN KEY (`form_id`) REFERENCES `MembershipForm` (`id`),
CONSTRAINT `FKmnoygtwlsvh31vb4volo8odct` FOREIGN KEY (`contact_id`) REFERENCES `Contact` (`id`)
CONSTRAINT `FK8mhoi37sufequy7nnn19811rv` FOREIGN KEY (`form_id`) REFERENCES `MembershipForm` (`id`) ON DELETE CASCADE,
CONSTRAINT `FKmnoygtwlsvh31vb4volo8odct` FOREIGN KEY (`contact_id`) REFERENCES `Contact` (`id`) ON DELETE SET NULL
) ENGINE=InnoDB DEFAULT CHARSET=latin1;
CREATE TABLE `Address` (
......@@ -63,5 +64,5 @@ CREATE TABLE `Address` (
`organization_id` varchar(255) DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `UK_i4vgutrsl3ve37hc8xx7vvslf` (`organization_id`),
CONSTRAINT `FKok1ylu26qjwvmfwgxlllbkj8l` FOREIGN KEY (`organization_id`) REFERENCES `FormOrganization` (`id`)
CONSTRAINT `FKok1ylu26qjwvmfwgxlllbkj8l` FOREIGN KEY (`organization_id`) REFERENCES `FormOrganization` (`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=latin1;
package org.eclipsefoundation.react.helper;
import java.time.Duration;