Refactor ContactResource into ContactBase and create ContactHistory (#634)

* Create ContactHistory class + table

This is similar to #587, but with contacts instead of hosts.

This also includes a couple cleanups for HostHistoryTest and RegistryLockDaoTest, just making code more proper (we shouldn't be referencing constant revision IDs when using a sequence that is used by multiple classes, and RLDT can extend EntityTest)

Note as well that we set ContactHistory to use the same revision ID sequence as HostHistory.

* Move ContactResource -> ContactBase

* Alter ContactBase and ContactResource
This commit is contained in:
gbrodman 2020-07-06 12:52:16 -04:00 committed by GitHub
parent bd77edb491
commit 43230eee09
14 changed files with 908 additions and 365 deletions

View file

@ -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 <a href="https://tools.ietf.org/html/rfc5733">RFC 5733</a>
* <p>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.
*
* <p>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;
/** Contacts 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;
/** Contacts 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;
/** Contacts 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.
*
* <p>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.
*
* <p>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<PostalInfo> 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 extends ContactBase> 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<T extends ContactBase, B extends Builder<T, B>>
extends EppResource.Builder<T, B> implements BuilderWithTransferData<ContactTransferData, B> {
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.
*
* <p>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();
}
}
}

View file

@ -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.
*
* <p>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<ContactResource> 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<ContactResource> getContactRepoId() {
return contactRepoId;
}
@Override
public Builder asBuilder() {
return new Builder(clone(this));
}
public static class Builder extends HistoryEntry.Builder<ContactHistory, ContactHistory.Builder> {
public Builder() {}
public Builder(ContactHistory instance) {
super(instance);
}
public Builder setContactBase(ContactBase contactBase) {
getInstance().contactBase = contactBase;
return this;
}
public Builder setContactRepoId(VKey<ContactResource> 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;
}
}
}

View file

@ -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;
/**
@ -66,140 +46,12 @@ import org.joda.time.DateTime;
@ExternalMessagingName("contact")
@WithStringVKey
@Access(AccessType.FIELD)
public class ContactResource extends EppResource
implements DatastoreAndSqlEntity, ForeignKeyedEppResource, ResourceWithTransferData {
/**
* Unique identifier for this contact.
*
* <p>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;
/** Contacts 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;
/** Contacts 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;
/** Contacts 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.
*
* <p>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<ContactResource> 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.
*
* <p>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<PostalInfo> 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<ContactResource, Builder>
implements BuilderWithTransferData<ContactTransferData, Builder> {
public static class Builder extends ContactBase.Builder<ContactResource, 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.
*
* <p>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();
}
}
}

View file

@ -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));
}

View file

@ -73,7 +73,7 @@ public class HostHistory extends HistoryEntry {
return this;
}
public Builder setHostResourceId(VKey<HostResource> hostRepoId) {
public Builder setHostRepoId(VKey<HostResource> 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<HostResource>) parent);
getInstance().hostRepoId = VKey.create(HostResource.class, parent.getName(), parent);
return this;
}
}

View file

@ -185,6 +185,10 @@ public class HistoryEntry extends ImmutableObject implements Buildable {
@Transient // domain-specific
Set<DomainTransactionRecord> domainTransactionRecords;
public Long getId() {
return id;
}
public Key<? extends EppResource> getParent() {
return parent;
}

View file

@ -22,6 +22,7 @@
<class>google.registry.model.billing.BillingEvent$Cancellation</class>
<class>google.registry.model.billing.BillingEvent$OneTime</class>
<class>google.registry.model.billing.BillingEvent$Recurring</class>
<class>google.registry.model.contact.ContactHistory</class>
<class>google.registry.model.contact.ContactResource</class>
<class>google.registry.model.domain.DomainBase</class>
<class>google.registry.model.host.HostHistory</class>

View file

@ -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<ContactResource> contactVKey = VKey.createSql(ContactResource.class, "contact1");
ContactResource contactFromDb = jpaTm().transact(() -> jpaTm().load(contactVKey));
ContactHistory contactHistory =
new ContactHistory.Builder()
.setType(HistoryEntry.Type.HOST_CREATE)
.setXmlBytes("<xml></xml>".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());
}
}

View file

@ -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);
});
}

View file

@ -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() {

View file

@ -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,

View file

@ -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);

View file

@ -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);

View file

@ -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: -
--