mirror of
https://github.com/open-metadata/OpenMetadata.git
synced 2025-12-24 05:58:31 +00:00
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:
parent
d8f8d6beb4
commit
78d71723a0
@ -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) {
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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;
|
||||
}
|
||||
@ -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.
|
||||
*/
|
||||
|
||||
@ -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",
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user