Add createdBy, createdAt for dataContracts (#23427)

* Add createdBy, createdAt for dataContracts

* Update generated TypeScript types

* Preserve createdBy, createdAt, createdBy marked as string

* Set createdBy, createdAt during prepare method

* Update generated TypeScript types

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: Ashish Gupta <ashish@getcollate.io>
This commit is contained in:
Ram Narayan Balaji 2025-09-22 14:19:05 +05:30 committed by GitHub
parent d8f8d6beb4
commit 78d71723a0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 412 additions and 2 deletions

View File

@ -136,6 +136,8 @@ public class DataContractRepository extends EntityRepository<DataContract> {
if (!update) {
validateEntityReference(entityRef);
dataContract.setCreatedAt(dataContract.getUpdatedAt());
dataContract.setCreatedBy(dataContract.getUpdatedBy());
}
// Validate schema fields and throw exception if there are failures
@ -936,6 +938,9 @@ public class DataContractRepository extends EntityRepository<DataContract> {
updateSchema(original, updated);
updateQualityExpectations(original, updated);
updateSemantics(original, updated);
// Preserve immutable creation fields
updated.setCreatedAt(original.getCreatedAt());
updated.setCreatedBy(original.getCreatedBy());
}
private void updateSchema(DataContract original, DataContract updated) {
@ -1048,8 +1053,8 @@ public class DataContractRepository extends EntityRepository<DataContract> {
.withId(original.getId())
.withName(original.getName())
.withFullyQualifiedName(original.getFullyQualifiedName())
.withUpdatedAt(original.getUpdatedAt())
.withUpdatedBy(original.getUpdatedBy());
.withCreatedAt(original.getCreatedAt())
.withCreatedBy(original.getCreatedBy());
}
private void validateEntityReference(EntityReference entity) {

View File

@ -1757,6 +1757,138 @@ public class DataContractResourceTest extends EntityResourceTest<DataContract, C
assertThrows(HttpResponseException.class, () -> getDataContract(created.getId(), null));
}
@Test
@Execution(ExecutionMode.CONCURRENT)
void testCreatedByAndCreatedAtFieldsOnCreation(TestInfo test) throws IOException {
Table table = createUniqueTable(test.getDisplayName());
CreateDataContract create = createDataContractRequest(test.getDisplayName(), table);
long beforeCreation = System.currentTimeMillis();
DataContract created = createDataContract(create);
long afterCreation = System.currentTimeMillis();
// Verify createdAt is set and within reasonable bounds
assertNotNull(created.getCreatedAt());
assertTrue(created.getCreatedAt() >= beforeCreation);
assertTrue(created.getCreatedAt() <= afterCreation);
// Verify createdBy is always set
assertNotNull(created.getCreatedBy());
assertEquals("admin", created.getCreatedBy());
// Get with fields parameter to ensure fields are persisted
DataContract retrieved = getDataContract(created.getId(), "createdBy,createdAt");
assertNotNull(retrieved.getCreatedAt());
assertNotNull(retrieved.getCreatedBy());
assertEquals(created.getCreatedAt(), retrieved.getCreatedAt());
assertEquals(created.getCreatedBy(), retrieved.getCreatedBy());
}
@Test
@Execution(ExecutionMode.CONCURRENT)
void testCreatedByAndCreatedAtPreservedOnUpdate(TestInfo test) throws IOException {
Table table = createUniqueTable(test.getDisplayName());
CreateDataContract create = createDataContractRequest(test.getDisplayName(), table);
DataContract created = createDataContract(create);
// Store original creation metadata
Long originalCreatedAt = created.getCreatedAt();
String originalCreatedBy = created.getCreatedBy();
// Update the contract
create.withEntityStatus(EntityStatus.APPROVED).withDescription("Updated description");
DataContract updated = updateDataContract(create);
// Verify creation fields are preserved
assertNotNull(updated.getCreatedAt());
assertNotNull(updated.getCreatedBy());
assertEquals(originalCreatedAt, updated.getCreatedAt());
assertEquals(originalCreatedBy, updated.getCreatedBy());
// Verify updatedAt exists
assertNotNull(updated.getUpdatedAt());
}
@Test
@Execution(ExecutionMode.CONCURRENT)
void testCreatedByAndCreatedAtPreservedOnPatch(TestInfo test) throws IOException {
Table table = createUniqueTable(test.getDisplayName());
CreateDataContract create = createDataContractRequest(test.getDisplayName(), table);
DataContract created = createDataContract(create);
String originalJson = JsonUtils.pojoToJson(created);
// Store original creation metadata
Long originalCreatedAt = created.getCreatedAt();
String originalCreatedBy = created.getCreatedBy();
// Apply patch
created.setEntityStatus(EntityStatus.APPROVED);
created.setDescription("Patched description");
DataContract patched = patchDataContract(created.getId(), originalJson, created);
// Verify creation fields are preserved
assertEquals(originalCreatedAt, patched.getCreatedAt());
assertEquals(originalCreatedBy, patched.getCreatedBy());
// Verify the patch was applied
assertEquals(EntityStatus.APPROVED, patched.getEntityStatus());
assertEquals("Patched description", patched.getDescription());
// Verify updatedAt is different from createdAt (updatedAt should be newer or equal)
assertNotNull(patched.getUpdatedAt());
assertTrue(patched.getUpdatedAt() >= patched.getCreatedAt());
}
@Test
@Execution(ExecutionMode.CONCURRENT)
void testCreatedByAndCreatedAtInGetByEntityId(TestInfo test) throws IOException {
Table table = createUniqueTable(test.getDisplayName());
CreateDataContract create = createDataContractRequest(test.getDisplayName(), table);
DataContract created = createDataContract(create);
// Get by entity ID with fields parameter
DataContract retrieved =
getDataContractByEntityId(
table.getId(), org.openmetadata.service.Entity.TABLE, "createdBy,createdAt");
// Verify creation fields are returned
assertNotNull(retrieved.getCreatedAt());
assertNotNull(retrieved.getCreatedBy());
assertEquals(created.getCreatedAt(), retrieved.getCreatedAt());
assertEquals(created.getCreatedBy(), retrieved.getCreatedBy());
// Get by entity ID without fields parameter should still return creation fields (they're audit
// fields)
DataContract retrievedNoFields =
getDataContractByEntityId(table.getId(), org.openmetadata.service.Entity.TABLE);
assertNotNull(retrievedNoFields.getCreatedBy());
assertNotNull(retrievedNoFields.getCreatedAt());
}
@Test
@Execution(ExecutionMode.CONCURRENT)
void testCreatedByAndCreatedAtInGetByName(TestInfo test) throws IOException {
Table table = createUniqueTable(test.getDisplayName());
CreateDataContract create = createDataContractRequest(test.getDisplayName(), table);
DataContract created = createDataContract(create);
// Get by name with fields parameter
DataContract retrieved =
getDataContractByName(created.getFullyQualifiedName(), "createdBy,createdAt");
// Verify creation fields are returned
assertNotNull(retrieved.getCreatedAt());
assertEquals(created.getCreatedAt(), retrieved.getCreatedAt());
// createdBy should always be set
assertNotNull(created.getCreatedBy());
assertNotNull(retrieved.getCreatedBy());
assertEquals(created.getCreatedBy(), retrieved.getCreatedBy());
}
// ===================== Business Logic Tests =====================
@Test

View File

@ -202,6 +202,14 @@
"description": "User who made the update.",
"type": "string"
},
"createdAt": {
"description": "Timestamp in Unix epoch time milliseconds corresponding to when the data contract was created.",
"$ref": "../../type/basic.json#/definitions/timestamp"
},
"createdBy": {
"description": "User or Bot who created the data contract.",
"type": "string"
},
"href": {
"description": "Link to this data contract resource.",
"$ref": "../../type/basic.json#/definitions/href"

View File

@ -0,0 +1,98 @@
/*
* Copyright 2025 Collate.
* 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 request for Notification Template
*/
export interface CreateNotificationTemplate {
/**
* Description of this notification template
*/
description?: string;
/**
* Display name for this notification template
*/
displayName?: string;
/**
* Fully qualified names of the domains the template belongs to
*/
domains?: string[];
/**
* Name that uniquely identifies this notification template (e.g., 'entity_change',
* 'test_change')
*/
name: string;
/**
* Owners of this template
*/
owners?: EntityReference[];
/**
* Handlebars template content for rendering notifications
*/
templateBody: string;
}
/**
* Owners of this template
*
* This schema defines the EntityReferenceList type used for referencing an entity.
* EntityReference is used for capturing relationships from one entity to another. For
* example, a table has an attribute called database of type EntityReference that captures
* the relationship of a table `belongs to a` database.
*
* This schema defines the EntityReference type used for referencing an entity.
* EntityReference is used for capturing relationships from one entity to another. For
* example, a table has an attribute called database of type EntityReference that captures
* the relationship of a table `belongs to a` database.
*/
export interface EntityReference {
/**
* If true the entity referred to has been soft-deleted.
*/
deleted?: boolean;
/**
* Optional description of entity.
*/
description?: string;
/**
* Display Name that identifies this entity.
*/
displayName?: string;
/**
* Fully qualified name of the entity instance. For entities such as tables, databases
* fullyQualifiedName is returned in this field. For entities that don't have name hierarchy
* such as `user` and `team` this will be same as the `name` field.
*/
fullyQualifiedName?: string;
/**
* Link to the entity resource.
*/
href?: string;
/**
* Unique identifier that identifies an entity instance.
*/
id: string;
/**
* If true the relationship indicated by this entity reference is inherited from the parent
* entity.
*/
inherited?: boolean;
/**
* Name of the entity instance.
*/
name?: string;
/**
* Entity type/class name - Examples: `database`, `table`, `metrics`, `databaseService`,
* `dashboardService`...
*/
type: string;
}

View File

@ -22,6 +22,15 @@ export interface DataContract {
* History of updates to the data contract.
*/
contractUpdates?: ContractUpdate[];
/**
* Timestamp in Unix epoch time milliseconds corresponding to when the data contract was
* created.
*/
createdAt?: number;
/**
* User or Bot who created the data contract.
*/
createdBy?: string;
/**
* When `true` indicates the entity has been soft deleted.
*/

View File

@ -0,0 +1,158 @@
/*
* Copyright 2025 Collate.
* 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.
*/
/**
* A NotificationTemplate defines the default formatting template for notifications of a
* specific entity type.
*/
export interface NotificationTemplate {
/**
* Change that lead to this version of the template.
*/
changeDescription?: ChangeDescription;
/**
* When `true` indicates the template has been soft deleted.
*/
deleted?: boolean;
/**
* Description of the template purpose and usage.
*/
description?: string;
/**
* Display Name that identifies this template.
*/
displayName?: string;
/**
* Fully qualified name for the template.
*/
fullyQualifiedName?: string;
/**
* Link to this template resource.
*/
href?: string;
/**
* Unique identifier of this template instance.
*/
id: string;
/**
* Change that lead to this version of the entity.
*/
incrementalChangeDescription?: ChangeDescription;
/**
* Name for the notification template (e.g., 'Default Table Template', 'Custom Pipeline
* Alerts').
*/
name: string;
/**
* Provider of the template. System templates are pre-loaded and cannot be deleted. User
* templates are created by users and can be deleted.
*/
provider?: ProviderType;
/**
* Handlebars HTML template body with placeholders.
*/
templateBody: string;
/**
* Last update time corresponding to the new version of the template.
*/
updatedAt?: number;
/**
* User who made the update.
*/
updatedBy?: string;
/**
* Metadata version of the template.
*/
version?: number;
}
/**
* Change that lead to this version of the template.
*
* Description of the change.
*
* Change that lead to this version of the entity.
*/
export interface ChangeDescription {
changeSummary?: { [key: string]: ChangeSummary };
/**
* Names of fields added during the version changes.
*/
fieldsAdded?: FieldChange[];
/**
* Fields deleted during the version changes with old value before deleted.
*/
fieldsDeleted?: FieldChange[];
/**
* Fields modified during the version changes with old and new values.
*/
fieldsUpdated?: FieldChange[];
/**
* When a change did not result in change, this could be same as the current version.
*/
previousVersion?: number;
}
export interface ChangeSummary {
changedAt?: number;
/**
* Name of the user or bot who made this change
*/
changedBy?: string;
changeSource?: ChangeSource;
[property: string]: any;
}
/**
* The source of the change. This will change based on the context of the change (example:
* manual vs programmatic)
*/
export enum ChangeSource {
Automated = "Automated",
Derived = "Derived",
Ingested = "Ingested",
Manual = "Manual",
Propagated = "Propagated",
Suggested = "Suggested",
}
export interface FieldChange {
/**
* Name of the entity field that changed.
*/
name?: string;
/**
* New value of the field. Note that this is a JSON string and use the corresponding field
* type to deserialize it.
*/
newValue?: any;
/**
* Previous value of the field. Note that this is a JSON string and use the corresponding
* field type to deserialize it.
*/
oldValue?: any;
}
/**
* Provider of the template. System templates are pre-loaded and cannot be deleted. User
* templates are created by users and can be deleted.
*
* Type of provider of an entity. Some entities are provided by the `system`. Some are
* entities created and provided by the `user`. Typically `system` provide entities can't be
* deleted and can only be disabled. Some apps such as AutoPilot create entities with
* `automation` provider type. These entities can be deleted by the user.
*/
export enum ProviderType {
Automation = "automation",
System = "system",
User = "user",
}