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 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 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 createVKey() { + public VKey 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 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 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: - --