diff --git a/core/src/main/java/google/registry/model/contact/ContactBase.java b/core/src/main/java/google/registry/model/contact/ContactBase.java
new file mode 100644
index 000000000..5f8d70180
--- /dev/null
+++ b/core/src/main/java/google/registry/model/contact/ContactBase.java
@@ -0,0 +1,398 @@
+// Copyright 2020 The Nomulus Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package google.registry.model.contact;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.collect.ImmutableList.toImmutableList;
+import static google.registry.model.EppResourceUtils.projectResourceOntoBuilderAtTime;
+
+import com.google.common.collect.ImmutableList;
+import com.googlecode.objectify.Key;
+import com.googlecode.objectify.annotation.IgnoreSave;
+import com.googlecode.objectify.annotation.Index;
+import com.googlecode.objectify.condition.IfNull;
+import google.registry.model.EppResource;
+import google.registry.model.EppResource.ResourceWithTransferData;
+import google.registry.model.transfer.ContactTransferData;
+import google.registry.persistence.VKey;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.stream.Stream;
+import javax.persistence.Access;
+import javax.persistence.AccessType;
+import javax.persistence.AttributeOverride;
+import javax.persistence.AttributeOverrides;
+import javax.persistence.Column;
+import javax.persistence.Embeddable;
+import javax.persistence.Embedded;
+import javax.persistence.MappedSuperclass;
+import javax.xml.bind.annotation.XmlElement;
+import org.joda.time.DateTime;
+
+/**
+ * A persistable contact resource including mutable and non-mutable fields.
+ *
+ * @see RFC 5733
+ *
This class deliberately does not include an {@link javax.persistence.Id} so that any
+ * foreign-keyed fields can refer to the proper parent entity's ID, whether we're storing this
+ * in the DB itself or as part of another entity
+ */
+@MappedSuperclass
+@Embeddable
+@Access(AccessType.FIELD)
+public class ContactBase extends EppResource implements ResourceWithTransferData {
+
+ /**
+ * Unique identifier for this contact.
+ *
+ *
This is only unique in the sense that for any given lifetime specified as the time range
+ * from (creationTime, deletionTime) there can only be one contact in Datastore with this id.
+ * However, there can be many contacts with the same id and non-overlapping lifetimes.
+ */
+ String contactId;
+
+ /**
+ * Localized postal info for the contact. All contained values must be representable in the 7-bit
+ * US-ASCII character set. Personal info; cleared by {@link ContactResource.Builder#wipeOut}.
+ */
+ @IgnoreSave(IfNull.class)
+ @Embedded
+ @AttributeOverrides({
+ @AttributeOverride(name = "name", column = @Column(name = "addr_local_name")),
+ @AttributeOverride(name = "org", column = @Column(name = "addr_local_org")),
+ @AttributeOverride(name = "type", column = @Column(name = "addr_local_type")),
+ @AttributeOverride(
+ name = "address.streetLine1",
+ column = @Column(name = "addr_local_street_line1")),
+ @AttributeOverride(
+ name = "address.streetLine2",
+ column = @Column(name = "addr_local_street_line2")),
+ @AttributeOverride(
+ name = "address.streetLine3",
+ column = @Column(name = "addr_local_street_line3")),
+ @AttributeOverride(name = "address.city", column = @Column(name = "addr_local_city")),
+ @AttributeOverride(name = "address.state", column = @Column(name = "addr_local_state")),
+ @AttributeOverride(name = "address.zip", column = @Column(name = "addr_local_zip")),
+ @AttributeOverride(
+ name = "address.countryCode",
+ column = @Column(name = "addr_local_country_code"))
+ })
+ PostalInfo localizedPostalInfo;
+
+ /**
+ * Internationalized postal info for the contact. Personal info; cleared by {@link
+ * ContactResource.Builder#wipeOut}.
+ */
+ @IgnoreSave(IfNull.class)
+ @Embedded
+ @AttributeOverrides({
+ @AttributeOverride(name = "name", column = @Column(name = "addr_i18n_name")),
+ @AttributeOverride(name = "org", column = @Column(name = "addr_i18n_org")),
+ @AttributeOverride(name = "type", column = @Column(name = "addr_i18n_type")),
+ @AttributeOverride(
+ name = "address.streetLine1",
+ column = @Column(name = "addr_i18n_street_line1")),
+ @AttributeOverride(
+ name = "address.streetLine2",
+ column = @Column(name = "addr_i18n_street_line2")),
+ @AttributeOverride(
+ name = "address.streetLine3",
+ column = @Column(name = "addr_i18n_street_line3")),
+ @AttributeOverride(name = "address.city", column = @Column(name = "addr_i18n_city")),
+ @AttributeOverride(name = "address.state", column = @Column(name = "addr_i18n_state")),
+ @AttributeOverride(name = "address.zip", column = @Column(name = "addr_i18n_zip")),
+ @AttributeOverride(
+ name = "address.countryCode",
+ column = @Column(name = "addr_i18n_country_code"))
+ })
+ PostalInfo internationalizedPostalInfo;
+
+ /**
+ * Contact name used for name searches. This is set automatically to be the internationalized
+ * postal name, or if null, the localized postal name, or if that is null as well, null. Personal
+ * info; cleared by {@link ContactResource.Builder#wipeOut}.
+ */
+ @Index String searchName;
+
+ /** Contact’s voice number. Personal info; cleared by {@link ContactResource.Builder#wipeOut}. */
+ @IgnoreSave(IfNull.class)
+ @Embedded
+ @AttributeOverrides({
+ @AttributeOverride(name = "phoneNumber", column = @Column(name = "voice_phone_number")),
+ @AttributeOverride(name = "extension", column = @Column(name = "voice_phone_extension")),
+ })
+ ContactPhoneNumber voice;
+
+ /** Contact’s fax number. Personal info; cleared by {@link ContactResource.Builder#wipeOut}. */
+ @IgnoreSave(IfNull.class)
+ @Embedded
+ @AttributeOverrides({
+ @AttributeOverride(name = "phoneNumber", column = @Column(name = "fax_phone_number")),
+ @AttributeOverride(name = "extension", column = @Column(name = "fax_phone_extension")),
+ })
+ ContactPhoneNumber fax;
+
+ /** Contact’s email address. Personal info; cleared by {@link ContactResource.Builder#wipeOut}. */
+ @IgnoreSave(IfNull.class)
+ String email;
+
+ /** Authorization info (aka transfer secret) of the contact. */
+ @Embedded
+ @AttributeOverrides({
+ @AttributeOverride(name = "pw.value", column = @Column(name = "auth_info_value")),
+ @AttributeOverride(name = "pw.repoId", column = @Column(name = "auth_info_repo_id")),
+ })
+ ContactAuthInfo authInfo;
+
+ /** Data about any pending or past transfers on this contact. */
+ ContactTransferData transferData;
+
+ /**
+ * The time that this resource was last transferred.
+ *
+ *
Can be null if the resource has never been transferred.
+ */
+ DateTime lastTransferTime;
+
+ // If any new fields are added which contain personal information, make sure they are cleared by
+ // the wipeOut() function, so that data is not kept around for deleted contacts.
+
+ /** Disclosure policy. */
+ @Embedded
+ @AttributeOverrides({
+ @AttributeOverride(name = "name", column = @Column(name = "disclose_types_name")),
+ @AttributeOverride(name = "org", column = @Column(name = "disclose_types_org")),
+ @AttributeOverride(name = "addr", column = @Column(name = "disclose_types_addr")),
+ @AttributeOverride(name = "flag", column = @Column(name = "disclose_mode_flag")),
+ @AttributeOverride(name = "voice.marked", column = @Column(name = "disclose_show_voice")),
+ @AttributeOverride(name = "fax.marked", column = @Column(name = "disclose_show_fax")),
+ @AttributeOverride(name = "email.marked", column = @Column(name = "disclose_show_email"))
+ })
+ Disclose disclose;
+
+ @Override
+ public VKey extends ContactBase> createVKey() {
+ // TODO(mmuller): create symmetric keys if we can ever reload both sides.
+ return VKey.create(ContactBase.class, getRepoId(), Key.create(this));
+ }
+
+ public String getContactId() {
+ return contactId;
+ }
+
+ public PostalInfo getLocalizedPostalInfo() {
+ return localizedPostalInfo;
+ }
+
+ public PostalInfo getInternationalizedPostalInfo() {
+ return internationalizedPostalInfo;
+ }
+
+ public String getSearchName() {
+ return searchName;
+ }
+
+ public ContactPhoneNumber getVoiceNumber() {
+ return voice;
+ }
+
+ public ContactPhoneNumber getFaxNumber() {
+ return fax;
+ }
+
+ public String getEmailAddress() {
+ return email;
+ }
+
+ public ContactAuthInfo getAuthInfo() {
+ return authInfo;
+ }
+
+ public Disclose getDisclose() {
+ return disclose;
+ }
+
+ public final String getCurrentSponsorClientId() {
+ return getPersistedCurrentSponsorClientId();
+ }
+
+ @Override
+ public final ContactTransferData getTransferData() {
+ return Optional.ofNullable(transferData).orElse(ContactTransferData.EMPTY);
+ }
+
+ @Override
+ public DateTime getLastTransferTime() {
+ return lastTransferTime;
+ }
+
+ @Override
+ public String getForeignKey() {
+ return contactId;
+ }
+
+ /**
+ * Postal info for the contact.
+ *
+ *
The XML marshalling expects the {@link PostalInfo} objects in a list, but we can't actually
+ * persist them to Datastore that way because Objectify can't handle collections of embedded
+ * objects that themselves contain collections, and there's a list of streets inside. This method
+ * transforms the persisted format to the XML format for marshalling.
+ */
+ @XmlElement(name = "postalInfo")
+ public ImmutableList getPostalInfosAsList() {
+ return Stream.of(localizedPostalInfo, internationalizedPostalInfo)
+ .filter(Objects::nonNull)
+ .collect(toImmutableList());
+ }
+
+ @Override
+ public ContactBase cloneProjectedAtTime(DateTime now) {
+ return cloneContactProjectedAtTime(this, now);
+ }
+
+ /**
+ * Clones the contact (or subclass). A separate static method so that we can pass in and return a
+ * T without the compiler complaining.
+ */
+ protected static T cloneContactProjectedAtTime(T contact, DateTime now) {
+ Builder builder = contact.asBuilder();
+ projectResourceOntoBuilderAtTime(contact, builder, now);
+ return (T) builder.build();
+ }
+
+ @Override
+ public Builder asBuilder() {
+ return new Builder<>(clone(this));
+ }
+
+ /** A builder for constructing {@link ContactResource}, since it is immutable. */
+ public static class Builder>
+ extends EppResource.Builder implements BuilderWithTransferData {
+
+ public Builder() {}
+
+ protected Builder(T instance) {
+ super(instance);
+ }
+
+ public B setContactId(String contactId) {
+ getInstance().contactId = contactId;
+ return thisCastToDerived();
+ }
+
+ public B setLocalizedPostalInfo(PostalInfo localizedPostalInfo) {
+ checkArgument(
+ localizedPostalInfo == null
+ || PostalInfo.Type.LOCALIZED.equals(localizedPostalInfo.getType()));
+ getInstance().localizedPostalInfo = localizedPostalInfo;
+ return thisCastToDerived();
+ }
+
+ public B setInternationalizedPostalInfo(PostalInfo internationalizedPostalInfo) {
+ checkArgument(
+ internationalizedPostalInfo == null
+ || PostalInfo.Type.INTERNATIONALIZED.equals(internationalizedPostalInfo.getType()));
+ getInstance().internationalizedPostalInfo = internationalizedPostalInfo;
+ return thisCastToDerived();
+ }
+
+ public B overlayLocalizedPostalInfo(PostalInfo localizedPostalInfo) {
+ return setLocalizedPostalInfo(
+ getInstance().localizedPostalInfo == null
+ ? localizedPostalInfo
+ : getInstance().localizedPostalInfo.overlay(localizedPostalInfo));
+ }
+
+ public B overlayInternationalizedPostalInfo(PostalInfo internationalizedPostalInfo) {
+ return setInternationalizedPostalInfo(
+ getInstance().internationalizedPostalInfo == null
+ ? internationalizedPostalInfo
+ : getInstance().internationalizedPostalInfo.overlay(internationalizedPostalInfo));
+ }
+
+ public B setVoiceNumber(ContactPhoneNumber voiceNumber) {
+ getInstance().voice = voiceNumber;
+ return thisCastToDerived();
+ }
+
+ public B setFaxNumber(ContactPhoneNumber faxNumber) {
+ getInstance().fax = faxNumber;
+ return thisCastToDerived();
+ }
+
+ public B setEmailAddress(String emailAddress) {
+ getInstance().email = emailAddress;
+ return thisCastToDerived();
+ }
+
+ public B setAuthInfo(ContactAuthInfo authInfo) {
+ getInstance().authInfo = authInfo;
+ return thisCastToDerived();
+ }
+
+ public B setDisclose(Disclose disclose) {
+ getInstance().disclose = disclose;
+ return thisCastToDerived();
+ }
+
+ @Override
+ public B setTransferData(ContactTransferData transferData) {
+ getInstance().transferData = transferData;
+ return thisCastToDerived();
+ }
+
+ @Override
+ public B setLastTransferTime(DateTime lastTransferTime) {
+ getInstance().lastTransferTime = lastTransferTime;
+ return thisCastToDerived();
+ }
+
+ /**
+ * Remove all personally identifying information about a contact.
+ *
+ * This should be used when deleting a contact so that the soft-deleted entity doesn't
+ * contain information that the registrant requested to be deleted.
+ */
+ public B wipeOut() {
+ setEmailAddress(null);
+ setFaxNumber(null);
+ setInternationalizedPostalInfo(null);
+ setLocalizedPostalInfo(null);
+ setVoiceNumber(null);
+ return thisCastToDerived();
+ }
+
+ @Override
+ public T build() {
+ T instance = getInstance();
+ // If TransferData is totally empty, set it to null.
+ if (ContactTransferData.EMPTY.equals(instance.transferData)) {
+ setTransferData(null);
+ }
+ // Set the searchName using the internationalized and localized postal info names.
+ if ((instance.internationalizedPostalInfo != null)
+ && (instance.internationalizedPostalInfo.getName() != null)) {
+ instance.searchName = instance.internationalizedPostalInfo.getName();
+ } else if ((instance.localizedPostalInfo != null)
+ && (instance.localizedPostalInfo.getName() != null)) {
+ instance.searchName = instance.localizedPostalInfo.getName();
+ } else {
+ instance.searchName = null;
+ }
+ return super.build();
+ }
+ }
+}
diff --git a/core/src/main/java/google/registry/model/contact/ContactHistory.java b/core/src/main/java/google/registry/model/contact/ContactHistory.java
new file mode 100644
index 000000000..9e42943b2
--- /dev/null
+++ b/core/src/main/java/google/registry/model/contact/ContactHistory.java
@@ -0,0 +1,88 @@
+// Copyright 2020 The Nomulus Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package google.registry.model.contact;
+
+import com.googlecode.objectify.Key;
+import google.registry.model.EppResource;
+import google.registry.model.reporting.HistoryEntry;
+import google.registry.persistence.VKey;
+import javax.persistence.Column;
+import javax.persistence.Entity;
+
+/**
+ * A persisted history entry representing an EPP modification to a contact.
+ *
+ *
In addition to the general history fields (e.g. action time, registrar ID) we also persist a
+ * copy of the host entity at this point in time. We persist a raw {@link ContactBase} so that the
+ * foreign-keyed fields in that class can refer to this object.
+ */
+@Entity
+@javax.persistence.Table(
+ indexes = {
+ @javax.persistence.Index(columnList = "creationTime"),
+ @javax.persistence.Index(columnList = "historyRegistrarId"),
+ @javax.persistence.Index(columnList = "historyType"),
+ @javax.persistence.Index(columnList = "historyModificationTime")
+ })
+public class ContactHistory extends HistoryEntry {
+ // Store ContactBase instead of ContactResource so we don't pick up its @Id
+ ContactBase contactBase;
+
+ @Column(nullable = false)
+ VKey contactRepoId;
+
+ /** The state of the {@link ContactBase} object at this point in time. */
+ public ContactBase getContactBase() {
+ return contactBase;
+ }
+
+ /** The key to the {@link ContactResource} this is based off of. */
+ public VKey getContactRepoId() {
+ return contactRepoId;
+ }
+
+ @Override
+ public Builder asBuilder() {
+ return new Builder(clone(this));
+ }
+
+ public static class Builder extends HistoryEntry.Builder {
+
+ public Builder() {}
+
+ public Builder(ContactHistory instance) {
+ super(instance);
+ }
+
+ public Builder setContactBase(ContactBase contactBase) {
+ getInstance().contactBase = contactBase;
+ return this;
+ }
+
+ public Builder setContactRepoId(VKey contactRepoId) {
+ getInstance().contactRepoId = contactRepoId;
+ contactRepoId.maybeGetOfyKey().ifPresent(parent -> getInstance().parent = parent);
+ return this;
+ }
+
+ // We can remove this once all HistoryEntries are converted to History objects
+ @Override
+ public Builder setParent(Key extends EppResource> parent) {
+ super.setParent(parent);
+ getInstance().contactRepoId = VKey.create(ContactResource.class, parent.getName(), parent);
+ return this;
+ }
+ }
+}
diff --git a/core/src/main/java/google/registry/model/contact/ContactResource.java b/core/src/main/java/google/registry/model/contact/ContactResource.java
index e050fb02e..bd13f8139 100644
--- a/core/src/main/java/google/registry/model/contact/ContactResource.java
+++ b/core/src/main/java/google/registry/model/contact/ContactResource.java
@@ -14,36 +14,16 @@
package google.registry.model.contact;
-import static com.google.common.base.Preconditions.checkArgument;
-import static com.google.common.collect.ImmutableList.toImmutableList;
-import static google.registry.model.EppResourceUtils.projectResourceOntoBuilderAtTime;
-
-import com.google.common.collect.ImmutableList;
import com.googlecode.objectify.Key;
import com.googlecode.objectify.annotation.Entity;
-import com.googlecode.objectify.annotation.IgnoreSave;
-import com.googlecode.objectify.annotation.Index;
-import com.googlecode.objectify.condition.IfNull;
-import google.registry.model.EppResource;
import google.registry.model.EppResource.ForeignKeyedEppResource;
-import google.registry.model.EppResource.ResourceWithTransferData;
import google.registry.model.annotations.ExternalMessagingName;
import google.registry.model.annotations.ReportedOn;
-import google.registry.model.contact.PostalInfo.Type;
-import google.registry.model.transfer.ContactTransferData;
import google.registry.persistence.VKey;
import google.registry.persistence.WithStringVKey;
import google.registry.schema.replay.DatastoreAndSqlEntity;
-import java.util.Objects;
-import java.util.Optional;
-import java.util.stream.Stream;
import javax.persistence.Access;
import javax.persistence.AccessType;
-import javax.persistence.AttributeOverride;
-import javax.persistence.AttributeOverrides;
-import javax.persistence.Column;
-import javax.persistence.Embedded;
-import javax.xml.bind.annotation.XmlElement;
import org.joda.time.DateTime;
/**
@@ -57,149 +37,21 @@ import org.joda.time.DateTime;
@javax.persistence.Table(
name = "Contact",
indexes = {
- @javax.persistence.Index(columnList = "creationTime"),
- @javax.persistence.Index(columnList = "currentSponsorRegistrarId"),
- @javax.persistence.Index(columnList = "deletionTime"),
- @javax.persistence.Index(columnList = "contactId", unique = true),
- @javax.persistence.Index(columnList = "searchName")
+ @javax.persistence.Index(columnList = "creationTime"),
+ @javax.persistence.Index(columnList = "currentSponsorRegistrarId"),
+ @javax.persistence.Index(columnList = "deletionTime"),
+ @javax.persistence.Index(columnList = "contactId", unique = true),
+ @javax.persistence.Index(columnList = "searchName")
})
@ExternalMessagingName("contact")
@WithStringVKey
@Access(AccessType.FIELD)
-public class ContactResource extends EppResource
- implements DatastoreAndSqlEntity, ForeignKeyedEppResource, ResourceWithTransferData {
-
- /**
- * Unique identifier for this contact.
- *
- * This is only unique in the sense that for any given lifetime specified as the time range
- * from (creationTime, deletionTime) there can only be one contact in Datastore with this id.
- * However, there can be many contacts with the same id and non-overlapping lifetimes.
- */
- String contactId;
-
- /**
- * Localized postal info for the contact. All contained values must be representable in the 7-bit
- * US-ASCII character set. Personal info; cleared by {@link Builder#wipeOut}.
- */
- @IgnoreSave(IfNull.class)
- @Embedded
- @AttributeOverrides({
- @AttributeOverride(name = "name", column = @Column(name = "addr_local_name")),
- @AttributeOverride(name = "org", column = @Column(name = "addr_local_org")),
- @AttributeOverride(name = "type", column = @Column(name = "addr_local_type")),
- @AttributeOverride(
- name = "address.streetLine1",
- column = @Column(name = "addr_local_street_line1")),
- @AttributeOverride(
- name = "address.streetLine2",
- column = @Column(name = "addr_local_street_line2")),
- @AttributeOverride(
- name = "address.streetLine3",
- column = @Column(name = "addr_local_street_line3")),
- @AttributeOverride(name = "address.city", column = @Column(name = "addr_local_city")),
- @AttributeOverride(name = "address.state", column = @Column(name = "addr_local_state")),
- @AttributeOverride(name = "address.zip", column = @Column(name = "addr_local_zip")),
- @AttributeOverride(
- name = "address.countryCode",
- column = @Column(name = "addr_local_country_code"))
- })
- PostalInfo localizedPostalInfo;
-
- /**
- * Internationalized postal info for the contact. Personal info; cleared by {@link
- * Builder#wipeOut}.
- */
- @IgnoreSave(IfNull.class)
- @Embedded
- @AttributeOverrides({
- @AttributeOverride(name = "name", column = @Column(name = "addr_i18n_name")),
- @AttributeOverride(name = "org", column = @Column(name = "addr_i18n_org")),
- @AttributeOverride(name = "type", column = @Column(name = "addr_i18n_type")),
- @AttributeOverride(
- name = "address.streetLine1",
- column = @Column(name = "addr_i18n_street_line1")),
- @AttributeOverride(
- name = "address.streetLine2",
- column = @Column(name = "addr_i18n_street_line2")),
- @AttributeOverride(
- name = "address.streetLine3",
- column = @Column(name = "addr_i18n_street_line3")),
- @AttributeOverride(name = "address.city", column = @Column(name = "addr_i18n_city")),
- @AttributeOverride(name = "address.state", column = @Column(name = "addr_i18n_state")),
- @AttributeOverride(name = "address.zip", column = @Column(name = "addr_i18n_zip")),
- @AttributeOverride(
- name = "address.countryCode",
- column = @Column(name = "addr_i18n_country_code"))
- })
- PostalInfo internationalizedPostalInfo;
-
- /**
- * Contact name used for name searches. This is set automatically to be the internationalized
- * postal name, or if null, the localized postal name, or if that is null as well, null. Personal
- * info; cleared by {@link Builder#wipeOut}.
- */
- @Index
- String searchName;
-
- /** Contact’s voice number. Personal info; cleared by {@link Builder#wipeOut}. */
- @IgnoreSave(IfNull.class)
- @Embedded
- @AttributeOverrides({
- @AttributeOverride(name = "phoneNumber", column = @Column(name = "voice_phone_number")),
- @AttributeOverride(name = "extension", column = @Column(name = "voice_phone_extension")),
- })
- ContactPhoneNumber voice;
-
- /** Contact’s fax number. Personal info; cleared by {@link Builder#wipeOut}. */
- @IgnoreSave(IfNull.class)
- @Embedded
- @AttributeOverrides({
- @AttributeOverride(name = "phoneNumber", column = @Column(name = "fax_phone_number")),
- @AttributeOverride(name = "extension", column = @Column(name = "fax_phone_extension")),
- })
- ContactPhoneNumber fax;
-
- /** Contact’s email address. Personal info; cleared by {@link Builder#wipeOut}. */
- @IgnoreSave(IfNull.class)
- String email;
-
- /** Authorization info (aka transfer secret) of the contact. */
- @Embedded
- @AttributeOverrides({
- @AttributeOverride(name = "pw.value", column = @Column(name = "auth_info_value")),
- @AttributeOverride(name = "pw.repoId", column = @Column(name = "auth_info_repo_id")),
- })
- ContactAuthInfo authInfo;
-
- /** Data about any pending or past transfers on this contact. */
- ContactTransferData transferData;
-
- /**
- * The time that this resource was last transferred.
- *
- *
Can be null if the resource has never been transferred.
- */
- DateTime lastTransferTime;
-
- // If any new fields are added which contain personal information, make sure they are cleared by
- // the wipeOut() function, so that data is not kept around for deleted contacts.
-
- /** Disclosure policy. */
- @Embedded
- @AttributeOverrides({
- @AttributeOverride(name = "name", column = @Column(name = "disclose_types_name")),
- @AttributeOverride(name = "org", column = @Column(name = "disclose_types_org")),
- @AttributeOverride(name = "addr", column = @Column(name = "disclose_types_addr")),
- @AttributeOverride(name = "flag", column = @Column(name = "disclose_mode_flag")),
- @AttributeOverride(name = "voice.marked", column = @Column(name = "disclose_show_voice")),
- @AttributeOverride(name = "fax.marked", column = @Column(name = "disclose_show_fax")),
- @AttributeOverride(name = "email.marked", column = @Column(name = "disclose_show_email"))
- })
- Disclose disclose;
+public class ContactResource extends ContactBase
+ implements DatastoreAndSqlEntity, ForeignKeyedEppResource {
@Override
public VKey createVKey() {
+ // TODO(mmuller): create symmetric keys if we can ever reload both sides.
return VKey.create(ContactResource.class, getRepoId(), Key.create(this));
}
@@ -210,81 +62,9 @@ public class ContactResource extends EppResource
return super.getRepoId();
}
- public String getContactId() {
- return contactId;
- }
-
- public PostalInfo getLocalizedPostalInfo() {
- return localizedPostalInfo;
- }
-
- public PostalInfo getInternationalizedPostalInfo() {
- return internationalizedPostalInfo;
- }
-
- public String getSearchName() {
- return searchName;
- }
-
- public ContactPhoneNumber getVoiceNumber() {
- return voice;
- }
-
- public ContactPhoneNumber getFaxNumber() {
- return fax;
- }
-
- public String getEmailAddress() {
- return email;
- }
-
- public ContactAuthInfo getAuthInfo() {
- return authInfo;
- }
-
- public Disclose getDisclose() {
- return disclose;
- }
-
- public final String getCurrentSponsorClientId() {
- return getPersistedCurrentSponsorClientId();
- }
-
- @Override
- public ContactTransferData getTransferData() {
- return Optional.ofNullable(transferData).orElse(ContactTransferData.EMPTY);
- }
-
- @Override
- public DateTime getLastTransferTime() {
- return lastTransferTime;
- }
-
- @Override
- public String getForeignKey() {
- return contactId;
- }
-
- /**
- * Postal info for the contact.
- *
- * The XML marshalling expects the {@link PostalInfo} objects in a list, but we can't actually
- * persist them to Datastore that way because Objectify can't handle collections of embedded
- * objects that themselves contain collections, and there's a list of streets inside. This method
- * transforms the persisted format to the XML format for marshalling.
- */
- @XmlElement(name = "postalInfo")
- public ImmutableList getPostalInfosAsList() {
- return Stream.of(localizedPostalInfo, internationalizedPostalInfo)
- .filter(Objects::nonNull)
- .collect(toImmutableList());
- }
-
@Override
public ContactResource cloneProjectedAtTime(DateTime now) {
- Builder builder = this.asBuilder();
- projectResourceOntoBuilderAtTime(this, builder, now);
- return builder.build();
+ return ContactBase.cloneContactProjectedAtTime(this, now);
}
@Override
@@ -293,116 +73,12 @@ public class ContactResource extends EppResource
}
/** A builder for constructing {@link ContactResource}, since it is immutable. */
- public static class Builder extends EppResource.Builder
- implements BuilderWithTransferData {
+ public static class Builder extends ContactBase.Builder {
public Builder() {}
private Builder(ContactResource instance) {
super(instance);
}
-
- public Builder setContactId(String contactId) {
- getInstance().contactId = contactId;
- return this;
- }
-
- public Builder setLocalizedPostalInfo(PostalInfo localizedPostalInfo) {
- checkArgument(localizedPostalInfo == null
- || Type.LOCALIZED.equals(localizedPostalInfo.getType()));
- getInstance().localizedPostalInfo = localizedPostalInfo;
- return this;
- }
-
- public Builder setInternationalizedPostalInfo(PostalInfo internationalizedPostalInfo) {
- checkArgument(internationalizedPostalInfo == null
- || Type.INTERNATIONALIZED.equals(internationalizedPostalInfo.getType()));
- getInstance().internationalizedPostalInfo = internationalizedPostalInfo;
- return this;
- }
-
- public Builder overlayLocalizedPostalInfo(PostalInfo localizedPostalInfo) {
- return setLocalizedPostalInfo(getInstance().localizedPostalInfo == null
- ? localizedPostalInfo
- : getInstance().localizedPostalInfo.overlay(localizedPostalInfo));
- }
-
- public Builder overlayInternationalizedPostalInfo(PostalInfo internationalizedPostalInfo) {
- return setInternationalizedPostalInfo(getInstance().internationalizedPostalInfo == null
- ? internationalizedPostalInfo
- : getInstance().internationalizedPostalInfo.overlay(internationalizedPostalInfo));
- }
-
- public Builder setVoiceNumber(ContactPhoneNumber voiceNumber) {
- getInstance().voice = voiceNumber;
- return this;
- }
-
- public Builder setFaxNumber(ContactPhoneNumber faxNumber) {
- getInstance().fax = faxNumber;
- return this;
- }
-
- public Builder setEmailAddress(String emailAddress) {
- getInstance().email = emailAddress;
- return this;
- }
-
- public Builder setAuthInfo(ContactAuthInfo authInfo) {
- getInstance().authInfo = authInfo;
- return this;
- }
-
- public Builder setDisclose(Disclose disclose) {
- getInstance().disclose = disclose;
- return this;
- }
-
- @Override
- public Builder setTransferData(ContactTransferData transferData) {
- getInstance().transferData = transferData;
- return this;
- }
-
- @Override
- public Builder setLastTransferTime(DateTime lastTransferTime) {
- getInstance().lastTransferTime = lastTransferTime;
- return thisCastToDerived();
- }
-
- /**
- * Remove all personally identifying information about a contact.
- *
- * This should be used when deleting a contact so that the soft-deleted entity doesn't
- * contain information that the registrant requested to be deleted.
- */
- public Builder wipeOut() {
- setEmailAddress(null);
- setFaxNumber(null);
- setInternationalizedPostalInfo(null);
- setLocalizedPostalInfo(null);
- setVoiceNumber(null);
- return this;
- }
-
- @Override
- public ContactResource build() {
- ContactResource instance = getInstance();
- // If TransferData is totally empty, set it to null.
- if (ContactTransferData.EMPTY.equals(instance.transferData)) {
- setTransferData(null);
- }
- // Set the searchName using the internationalized and localized postal info names.
- if ((instance.internationalizedPostalInfo != null)
- && (instance.internationalizedPostalInfo.getName() != null)) {
- instance.searchName = instance.internationalizedPostalInfo.getName();
- } else if ((instance.localizedPostalInfo != null)
- && (instance.localizedPostalInfo.getName() != null)) {
- instance.searchName = instance.localizedPostalInfo.getName();
- } else {
- instance.searchName = null;
- }
- return super.build();
- }
}
}
diff --git a/core/src/main/java/google/registry/model/host/HostBase.java b/core/src/main/java/google/registry/model/host/HostBase.java
index 5f10d95d9..a0cb2459b 100644
--- a/core/src/main/java/google/registry/model/host/HostBase.java
+++ b/core/src/main/java/google/registry/model/host/HostBase.java
@@ -125,7 +125,7 @@ public class HostBase extends EppResource {
}
@Override
- public VKey extends EppResource> createVKey() {
+ public VKey extends HostBase> createVKey() {
return VKey.create(HostBase.class, getRepoId(), Key.create(this));
}
diff --git a/core/src/main/java/google/registry/model/host/HostHistory.java b/core/src/main/java/google/registry/model/host/HostHistory.java
index 3507bc247..e1587246d 100644
--- a/core/src/main/java/google/registry/model/host/HostHistory.java
+++ b/core/src/main/java/google/registry/model/host/HostHistory.java
@@ -73,7 +73,7 @@ public class HostHistory extends HistoryEntry {
return this;
}
- public Builder setHostResourceId(VKey hostRepoId) {
+ public Builder setHostRepoId(VKey hostRepoId) {
getInstance().hostRepoId = hostRepoId;
hostRepoId.maybeGetOfyKey().ifPresent(parent -> getInstance().parent = parent);
return this;
@@ -83,8 +83,7 @@ public class HostHistory extends HistoryEntry {
@Override
public Builder setParent(Key extends EppResource> parent) {
super.setParent(parent);
- getInstance().hostRepoId =
- VKey.create(HostResource.class, parent.getName(), (Key) parent);
+ getInstance().hostRepoId = VKey.create(HostResource.class, parent.getName(), parent);
return this;
}
}
diff --git a/core/src/main/java/google/registry/model/reporting/HistoryEntry.java b/core/src/main/java/google/registry/model/reporting/HistoryEntry.java
index d096733cb..1201a9b3b 100644
--- a/core/src/main/java/google/registry/model/reporting/HistoryEntry.java
+++ b/core/src/main/java/google/registry/model/reporting/HistoryEntry.java
@@ -185,6 +185,10 @@ public class HistoryEntry extends ImmutableObject implements Buildable {
@Transient // domain-specific
Set domainTransactionRecords;
+ public Long getId() {
+ return id;
+ }
+
public Key extends EppResource> getParent() {
return parent;
}
diff --git a/core/src/main/resources/META-INF/persistence.xml b/core/src/main/resources/META-INF/persistence.xml
index 764081281..65b97aefd 100644
--- a/core/src/main/resources/META-INF/persistence.xml
+++ b/core/src/main/resources/META-INF/persistence.xml
@@ -22,6 +22,7 @@
google.registry.model.billing.BillingEvent$Cancellation
google.registry.model.billing.BillingEvent$OneTime
google.registry.model.billing.BillingEvent$Recurring
+ google.registry.model.contact.ContactHistory
google.registry.model.contact.ContactResource
google.registry.model.domain.DomainBase
google.registry.model.host.HostHistory
diff --git a/core/src/test/java/google/registry/model/history/ContactHistoryTest.java b/core/src/test/java/google/registry/model/history/ContactHistoryTest.java
new file mode 100644
index 000000000..75bdea225
--- /dev/null
+++ b/core/src/test/java/google/registry/model/history/ContactHistoryTest.java
@@ -0,0 +1,87 @@
+// Copyright 2017 The Nomulus Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package google.registry.model.history;
+
+import static com.google.common.truth.Truth.assertThat;
+import static google.registry.persistence.transaction.TransactionManagerFactory.jpaTm;
+import static google.registry.testing.SqlHelper.saveRegistrar;
+import static java.nio.charset.StandardCharsets.UTF_8;
+
+import google.registry.model.EntityTestCase;
+import google.registry.model.contact.ContactHistory;
+import google.registry.model.contact.ContactResource;
+import google.registry.model.eppcommon.Trid;
+import google.registry.model.reporting.HistoryEntry;
+import google.registry.model.transfer.ContactTransferData;
+import google.registry.persistence.VKey;
+import org.junit.jupiter.api.Test;
+
+/** Tests for {@link ContactHistory}. */
+public class ContactHistoryTest extends EntityTestCase {
+
+ public ContactHistoryTest() {
+ super(true);
+ }
+
+ @Test
+ public void testPersistence() {
+ saveRegistrar("registrar1");
+
+ ContactResource contact =
+ new ContactResource.Builder()
+ .setRepoId("contact1")
+ .setContactId("contactId")
+ .setCreationClientId("registrar1")
+ .setPersistedCurrentSponsorClientId("registrar1")
+ .setTransferData(new ContactTransferData.Builder().build())
+ .build();
+
+ jpaTm().transact(() -> jpaTm().saveNew(contact));
+ VKey contactVKey = VKey.createSql(ContactResource.class, "contact1");
+ ContactResource contactFromDb = jpaTm().transact(() -> jpaTm().load(contactVKey));
+ ContactHistory contactHistory =
+ new ContactHistory.Builder()
+ .setType(HistoryEntry.Type.HOST_CREATE)
+ .setXmlBytes("".getBytes(UTF_8))
+ .setModificationTime(fakeClock.nowUtc())
+ .setClientId("registrar1")
+ .setTrid(Trid.create("ABC-123", "server-trid"))
+ .setBySuperuser(false)
+ .setReason("reason")
+ .setRequestedByRegistrar(true)
+ .setContactBase(contactFromDb)
+ .setContactRepoId(contactVKey)
+ .build();
+ jpaTm().transact(() -> jpaTm().saveNew(contactHistory));
+ jpaTm()
+ .transact(
+ () -> {
+ ContactHistory fromDatabase = jpaTm().load(VKey.createSql(ContactHistory.class, 1L));
+ assertContactHistoriesEqual(fromDatabase, contactHistory);
+ });
+ }
+
+ private void assertContactHistoriesEqual(ContactHistory one, ContactHistory two) {
+ // enough of the fields get changed during serialization that we can't depend on .equals()
+ assertThat(one.getClientId()).isEqualTo(two.getClientId());
+ assertThat(one.getContactRepoId()).isEqualTo(two.getContactRepoId());
+ assertThat(one.getBySuperuser()).isEqualTo(two.getBySuperuser());
+ assertThat(one.getRequestedByRegistrar()).isEqualTo(two.getRequestedByRegistrar());
+ assertThat(one.getReason()).isEqualTo(two.getReason());
+ assertThat(one.getTrid()).isEqualTo(two.getTrid());
+ assertThat(one.getType()).isEqualTo(two.getType());
+ assertThat(one.getContactBase().getContactId()).isEqualTo(two.getContactBase().getContactId());
+ }
+}
diff --git a/core/src/test/java/google/registry/model/history/HostHistoryTest.java b/core/src/test/java/google/registry/model/history/HostHistoryTest.java
index bafb04149..9ce044254 100644
--- a/core/src/test/java/google/registry/model/history/HostHistoryTest.java
+++ b/core/src/test/java/google/registry/model/history/HostHistoryTest.java
@@ -61,13 +61,14 @@ public class HostHistoryTest extends EntityTestCase {
.setReason("reason")
.setRequestedByRegistrar(true)
.setHostBase(hostFromDb)
- .setHostResourceId(hostVKey)
+ .setHostRepoId(hostVKey)
.build();
jpaTm().transact(() -> jpaTm().saveNew(hostHistory));
jpaTm()
.transact(
() -> {
- HostHistory fromDatabase = jpaTm().load(VKey.createSql(HostHistory.class, 1L));
+ HostHistory fromDatabase =
+ jpaTm().load(VKey.createSql(HostHistory.class, hostHistory.getId()));
assertHostHistoriesEqual(fromDatabase, hostHistory);
});
}
diff --git a/core/src/test/java/google/registry/model/registry/RegistryLockDaoTest.java b/core/src/test/java/google/registry/model/registry/RegistryLockDaoTest.java
index 0fccfae81..0df2a5805 100644
--- a/core/src/test/java/google/registry/model/registry/RegistryLockDaoTest.java
+++ b/core/src/test/java/google/registry/model/registry/RegistryLockDaoTest.java
@@ -26,26 +26,18 @@ import static google.registry.testing.SqlHelper.getRegistryLocksByRegistrarId;
import static google.registry.testing.SqlHelper.saveRegistryLock;
import static org.junit.Assert.assertThrows;
+import google.registry.model.EntityTestCase;
import google.registry.schema.domain.RegistryLock;
-import google.registry.testing.AppEngineRule;
-import google.registry.testing.FakeClock;
import java.util.Optional;
import org.joda.time.Duration;
import org.junit.jupiter.api.Test;
-import org.junit.jupiter.api.extension.RegisterExtension;
/** Unit tests for {@link RegistryLockDao}. */
-public final class RegistryLockDaoTest {
+public final class RegistryLockDaoTest extends EntityTestCase {
- private final FakeClock fakeClock = new FakeClock();
-
- @RegisterExtension
- public final AppEngineRule appEngine =
- AppEngineRule.builder()
- .withDatastoreAndCloudSql()
- .enableJpaEntityCoverageCheck(true)
- .withClock(fakeClock)
- .build();
+ public RegistryLockDaoTest() {
+ super(true);
+ }
@Test
public void testSaveAndLoad_success() {
diff --git a/core/src/test/java/google/registry/schema/integration/SqlIntegrationTestSuite.java b/core/src/test/java/google/registry/schema/integration/SqlIntegrationTestSuite.java
index 9ce5aed9b..d582e8584 100644
--- a/core/src/test/java/google/registry/schema/integration/SqlIntegrationTestSuite.java
+++ b/core/src/test/java/google/registry/schema/integration/SqlIntegrationTestSuite.java
@@ -19,6 +19,7 @@ import static com.google.common.truth.Truth.assert_;
import google.registry.model.billing.BillingEventTest;
import google.registry.model.contact.ContactResourceTest;
import google.registry.model.domain.DomainBaseSqlTest;
+import google.registry.model.history.ContactHistoryTest;
import google.registry.model.history.HostHistoryTest;
import google.registry.model.poll.PollMessageTest;
import google.registry.model.registry.RegistryLockDaoTest;
@@ -74,6 +75,7 @@ import org.junit.runner.RunWith;
BeforeSuiteTest.class,
BillingEventTest.class,
ClaimsListDaoTest.class,
+ ContactHistoryTest.class,
ContactResourceTest.class,
CursorDaoTest.class,
DomainBaseSqlTest.class,
diff --git a/db/src/main/resources/sql/flyway/V38__create_contact_history.sql b/db/src/main/resources/sql/flyway/V38__create_contact_history.sql
new file mode 100644
index 000000000..9187126a2
--- /dev/null
+++ b/db/src/main/resources/sql/flyway/V38__create_contact_history.sql
@@ -0,0 +1,99 @@
+-- Copyright 2020 The Nomulus Authors. All Rights Reserved.
+--
+-- Licensed under the Apache License, Version 2.0 (the "License");
+-- you may not use this file except in compliance with the License.
+-- You may obtain a copy of the License at
+--
+-- http://www.apache.org/licenses/LICENSE-2.0
+--
+-- Unless required by applicable law or agreed to in writing, software
+-- distributed under the License is distributed on an "AS IS" BASIS,
+-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+-- See the License for the specific language governing permissions and
+-- limitations under the License.
+
+CREATE TABLE "ContactHistory" (
+ history_revision_id int8 NOT NULL,
+ history_by_superuser boolean NOT NULL,
+ history_registrar_id text,
+ history_modification_time timestamptz NOT NULL,
+ history_reason text NOT NULL,
+ history_requested_by_registrar boolean NOT NULL,
+ history_client_transaction_id text,
+ history_server_transaction_id text,
+ history_type text NOT NULL,
+ history_xml_bytes bytea NOT NULL,
+ auth_info_repo_id text,
+ auth_info_value text,
+ contact_id text,
+ disclose_types_addr text[],
+ disclose_show_email boolean,
+ disclose_show_fax boolean,
+ disclose_mode_flag boolean,
+ disclose_types_name text[],
+ disclose_types_org text[],
+ disclose_show_voice boolean,
+ email text,
+ fax_phone_extension text,
+ fax_phone_number text,
+ addr_i18n_city text,
+ addr_i18n_country_code text,
+ addr_i18n_state text,
+ addr_i18n_street_line1 text,
+ addr_i18n_street_line2 text,
+ addr_i18n_street_line3 text,
+ addr_i18n_zip text,
+ addr_i18n_name text,
+ addr_i18n_org text,
+ addr_i18n_type text,
+ last_transfer_time timestamptz,
+ addr_local_city text,
+ addr_local_country_code text,
+ addr_local_state text,
+ addr_local_street_line1 text,
+ addr_local_street_line2 text,
+ addr_local_street_line3 text,
+ addr_local_zip text,
+ addr_local_name text,
+ addr_local_org text,
+ addr_local_type text,
+ search_name text,
+ transfer_gaining_poll_message_id int8,
+ transfer_losing_poll_message_id int8,
+ transfer_client_txn_id text,
+ transfer_server_txn_id text,
+ transfer_gaining_registrar_id text,
+ transfer_losing_registrar_id text,
+ transfer_pending_expiration_time timestamptz,
+ transfer_request_time timestamptz,
+ transfer_status text,
+ voice_phone_extension text,
+ voice_phone_number text,
+ creation_registrar_id text NOT NULL,
+ creation_time timestamptz NOT NULL,
+ current_sponsor_registrar_id text NOT NULL,
+ deletion_time timestamptz,
+ last_epp_update_registrar_id text,
+ last_epp_update_time timestamptz,
+ statuses text[],
+ contact_repo_id text NOT NULL,
+ primary key (history_revision_id)
+);
+
+create index IDXo1xdtpij2yryh0skxe9v91sep on "ContactHistory" (creation_time);
+create index IDXhp33wybmb6tbpr1bq7ttwk8je on "ContactHistory" (history_registrar_id);
+create index IDX9q53px6r302ftgisqifmc6put on "ContactHistory" (history_type);
+create index IDXsudwswtwqnfnx2o1hx4s0k0g5 on "ContactHistory" (history_modification_time);
+
+ALTER TABLE IF EXISTS "ContactHistory"
+ ADD CONSTRAINT fk_contact_history_registrar_id
+ FOREIGN KEY (history_registrar_id)
+ REFERENCES "Registrar";
+
+ALTER TABLE IF EXISTS "ContactHistory"
+ ADD CONSTRAINT fk_contact_history_contact_repo_id
+ FOREIGN KEY (contact_repo_id)
+ REFERENCES "Contact";
+
+ALTER TABLE ONLY public."ContactHistory" ALTER COLUMN history_revision_id
+ SET DEFAULT nextval('public."history_id_sequence"'::regclass);
diff --git a/db/src/main/resources/sql/schema/db-schema.sql.generated b/db/src/main/resources/sql/schema/db-schema.sql.generated
index a1a2c8b34..d48ad7541 100644
--- a/db/src/main/resources/sql/schema/db-schema.sql.generated
+++ b/db/src/main/resources/sql/schema/db-schema.sql.generated
@@ -133,6 +133,74 @@ create sequence history_id_sequence start 1 increment 1;
primary key (repo_id)
);
+ create table "ContactHistory" (
+ history_revision_id int8 not null,
+ history_by_superuser boolean not null,
+ history_registrar_id text,
+ history_modification_time timestamptz not null,
+ history_reason text not null,
+ history_requested_by_registrar boolean not null,
+ history_client_transaction_id text,
+ history_server_transaction_id text,
+ history_type text not null,
+ history_xml_bytes bytea not null,
+ auth_info_repo_id text,
+ auth_info_value text,
+ contact_id text,
+ disclose_types_addr text[],
+ disclose_show_email boolean,
+ disclose_show_fax boolean,
+ disclose_mode_flag boolean,
+ disclose_types_name text[],
+ disclose_types_org text[],
+ disclose_show_voice boolean,
+ email text,
+ fax_phone_extension text,
+ fax_phone_number text,
+ addr_i18n_city text,
+ addr_i18n_country_code text,
+ addr_i18n_state text,
+ addr_i18n_street_line1 text,
+ addr_i18n_street_line2 text,
+ addr_i18n_street_line3 text,
+ addr_i18n_zip text,
+ addr_i18n_name text,
+ addr_i18n_org text,
+ addr_i18n_type text,
+ last_transfer_time timestamptz,
+ addr_local_city text,
+ addr_local_country_code text,
+ addr_local_state text,
+ addr_local_street_line1 text,
+ addr_local_street_line2 text,
+ addr_local_street_line3 text,
+ addr_local_zip text,
+ addr_local_name text,
+ addr_local_org text,
+ addr_local_type text,
+ search_name text,
+ transfer_gaining_poll_message_id int8,
+ transfer_losing_poll_message_id int8,
+ transfer_client_txn_id text,
+ transfer_server_txn_id text,
+ transfer_gaining_registrar_id text,
+ transfer_losing_registrar_id text,
+ transfer_pending_expiration_time timestamptz,
+ transfer_request_time timestamptz,
+ transfer_status text,
+ voice_phone_extension text,
+ voice_phone_number text,
+ creation_registrar_id text not null,
+ creation_time timestamptz not null,
+ current_sponsor_registrar_id text not null,
+ deletion_time timestamptz,
+ last_epp_update_registrar_id text,
+ last_epp_update_time timestamptz,
+ statuses text[],
+ contact_repo_id text not null,
+ primary key (history_revision_id)
+ );
+
create table "Cursor" (
scope text not null,
type text not null,
@@ -437,6 +505,10 @@ create index IDX1p3esngcwwu6hstyua6itn6ff on "Contact" (search_name);
alter table if exists "Contact"
add constraint UKoqd7n4hbx86hvlgkilq75olas unique (contact_id);
+create index IDXo1xdtpij2yryh0skxe9v91sep on "ContactHistory" (creation_time);
+create index IDXhp33wybmb6tbpr1bq7ttwk8je on "ContactHistory" (history_registrar_id);
+create index IDX9q53px6r302ftgisqifmc6put on "ContactHistory" (history_type);
+create index IDXsudwswtwqnfnx2o1hx4s0k0g5 on "ContactHistory" (history_modification_time);
create index IDX8nr0ke9mrrx4ewj6pd2ag4rmr on "Domain" (creation_time);
create index IDXhsjqiy2lyobfymplb28nm74lm on "Domain" (current_sponsor_registrar_id);
create index IDX5mnf0wn20tno4b9do88j61klr on "Domain" (deletion_time);
diff --git a/db/src/main/resources/sql/schema/nomulus.golden.sql b/db/src/main/resources/sql/schema/nomulus.golden.sql
index 1aabca016..ac2b0e529 100644
--- a/db/src/main/resources/sql/schema/nomulus.golden.sql
+++ b/db/src/main/resources/sql/schema/nomulus.golden.sql
@@ -254,6 +254,90 @@ CREATE TABLE public."Contact" (
);
+--
+-- Name: history_id_sequence; Type: SEQUENCE; Schema: public; Owner: -
+--
+
+CREATE SEQUENCE public.history_id_sequence
+ START WITH 1
+ INCREMENT BY 1
+ NO MINVALUE
+ NO MAXVALUE
+ CACHE 1;
+
+
+--
+-- Name: ContactHistory; Type: TABLE; Schema: public; Owner: -
+--
+
+CREATE TABLE public."ContactHistory" (
+ history_revision_id bigint DEFAULT nextval('public.history_id_sequence'::regclass) NOT NULL,
+ history_by_superuser boolean NOT NULL,
+ history_registrar_id text,
+ history_modification_time timestamp with time zone NOT NULL,
+ history_reason text NOT NULL,
+ history_requested_by_registrar boolean NOT NULL,
+ history_client_transaction_id text,
+ history_server_transaction_id text,
+ history_type text NOT NULL,
+ history_xml_bytes bytea NOT NULL,
+ auth_info_repo_id text,
+ auth_info_value text,
+ contact_id text,
+ disclose_types_addr text[],
+ disclose_show_email boolean,
+ disclose_show_fax boolean,
+ disclose_mode_flag boolean,
+ disclose_types_name text[],
+ disclose_types_org text[],
+ disclose_show_voice boolean,
+ email text,
+ fax_phone_extension text,
+ fax_phone_number text,
+ addr_i18n_city text,
+ addr_i18n_country_code text,
+ addr_i18n_state text,
+ addr_i18n_street_line1 text,
+ addr_i18n_street_line2 text,
+ addr_i18n_street_line3 text,
+ addr_i18n_zip text,
+ addr_i18n_name text,
+ addr_i18n_org text,
+ addr_i18n_type text,
+ last_transfer_time timestamp with time zone,
+ addr_local_city text,
+ addr_local_country_code text,
+ addr_local_state text,
+ addr_local_street_line1 text,
+ addr_local_street_line2 text,
+ addr_local_street_line3 text,
+ addr_local_zip text,
+ addr_local_name text,
+ addr_local_org text,
+ addr_local_type text,
+ search_name text,
+ transfer_gaining_poll_message_id bigint,
+ transfer_losing_poll_message_id bigint,
+ transfer_client_txn_id text,
+ transfer_server_txn_id text,
+ transfer_gaining_registrar_id text,
+ transfer_losing_registrar_id text,
+ transfer_pending_expiration_time timestamp with time zone,
+ transfer_request_time timestamp with time zone,
+ transfer_status text,
+ voice_phone_extension text,
+ voice_phone_number text,
+ creation_registrar_id text NOT NULL,
+ creation_time timestamp with time zone NOT NULL,
+ current_sponsor_registrar_id text NOT NULL,
+ deletion_time timestamp with time zone,
+ last_epp_update_registrar_id text,
+ last_epp_update_time timestamp with time zone,
+ statuses text[],
+ contact_repo_id text NOT NULL
+);
+
+
--
-- Name: Cursor; Type: TABLE; Schema: public; Owner: -
--
@@ -325,18 +409,6 @@ CREATE TABLE public."DomainHost" (
);
---
--- Name: history_id_sequence; Type: SEQUENCE; Schema: public; Owner: -
---
-
-CREATE SEQUENCE public.history_id_sequence
- START WITH 1
- INCREMENT BY 1
- NO MINVALUE
- NO MAXVALUE
- CACHE 1;
-
-
--
-- Name: HostHistory; Type: TABLE; Schema: public; Owner: -
--
@@ -793,6 +865,14 @@ ALTER TABLE ONLY public."ClaimsList"
ADD CONSTRAINT "ClaimsList_pkey" PRIMARY KEY (revision_id);
+--
+-- Name: ContactHistory ContactHistory_pkey; Type: CONSTRAINT; Schema: public; Owner: -
+--
+
+ALTER TABLE ONLY public."ContactHistory"
+ ADD CONSTRAINT "ContactHistory_pkey" PRIMARY KEY (history_revision_id);
+
+
--
-- Name: Contact Contact_pkey; Type: CONSTRAINT; Schema: public; Owner: -
--
@@ -1013,6 +1093,13 @@ CREATE INDEX idx73l103vc5900ig3p4odf0cngt ON public."BillingEvent" USING btree (
CREATE INDEX idx8nr0ke9mrrx4ewj6pd2ag4rmr ON public."Domain" USING btree (creation_time);
+--
+-- Name: idx9q53px6r302ftgisqifmc6put; Type: INDEX; Schema: public; Owner: -
+--
+
+CREATE INDEX idx9q53px6r302ftgisqifmc6put ON public."ContactHistory" USING btree (history_type);
+
+
--
-- Name: idx_registry_lock_registrar_id; Type: INDEX; Schema: public; Owner: -
--
@@ -1069,6 +1156,13 @@ CREATE INDEX idxfg2nnjlujxo6cb9fha971bq2n ON public."HostHistory" USING btree (c
CREATE INDEX idxhmv411mdqo5ibn4vy7ykxpmlv ON public."BillingEvent" USING btree (allocation_token_id);
+--
+-- Name: idxhp33wybmb6tbpr1bq7ttwk8je; Type: INDEX; Schema: public; Owner: -
+--
+
+CREATE INDEX idxhp33wybmb6tbpr1bq7ttwk8je ON public."ContactHistory" USING btree (history_registrar_id);
+
+
--
-- Name: idxj77pfwhui9f0i7wjq6lmibovj; Type: INDEX; Schema: public; Owner: -
--
@@ -1111,6 +1205,13 @@ CREATE INDEX idxn1f711wicdnooa2mqb7g1m55o ON public."Contact" USING btree (delet
CREATE INDEX idxn898pb9mwcg359cdwvolb11ck ON public."BillingRecurrence" USING btree (registrar_id);
+--
+-- Name: idxo1xdtpij2yryh0skxe9v91sep; Type: INDEX; Schema: public; Owner: -
+--
+
+CREATE INDEX idxo1xdtpij2yryh0skxe9v91sep ON public."ContactHistory" USING btree (creation_time);
+
+
--
-- Name: idxp3usbtvk0v1m14i5tdp4xnxgc; Type: INDEX; Schema: public; Owner: -
--
@@ -1139,6 +1240,13 @@ CREATE INDEX idxqa3g92jc17e8dtiaviy4fet4x ON public."BillingCancellation" USING
CREATE INDEX idxrwl38wwkli1j7gkvtywi9jokq ON public."Domain" USING btree (tld);
+--
+-- Name: idxsudwswtwqnfnx2o1hx4s0k0g5; Type: INDEX; Schema: public; Owner: -
+--
+
+CREATE INDEX idxsudwswtwqnfnx2o1hx4s0k0g5 ON public."ContactHistory" USING btree (history_modification_time);
+
+
--
-- Name: premiumlist_name_idx; Type: INDEX; Schema: public; Owner: -
--
@@ -1299,6 +1407,22 @@ ALTER TABLE ONLY public."BillingRecurrence"
ADD CONSTRAINT fk_billing_recurrence_registrar_id FOREIGN KEY (registrar_id) REFERENCES public."Registrar"(registrar_id);
+--
+-- Name: ContactHistory fk_contact_history_contact_repo_id; Type: FK CONSTRAINT; Schema: public; Owner: -
+--
+
+ALTER TABLE ONLY public."ContactHistory"
+ ADD CONSTRAINT fk_contact_history_contact_repo_id FOREIGN KEY (contact_repo_id) REFERENCES public."Contact"(repo_id);
+
+
+--
+-- Name: ContactHistory fk_contact_history_registrar_id; Type: FK CONSTRAINT; Schema: public; Owner: -
+--
+
+ALTER TABLE ONLY public."ContactHistory"
+ ADD CONSTRAINT fk_contact_history_registrar_id FOREIGN KEY (history_registrar_id) REFERENCES public."Registrar"(registrar_id);
+
+
--
-- Name: Contact fk_contact_transfer_gaining_registrar_id; Type: FK CONSTRAINT; Schema: public; Owner: -
--