diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/jdbi3/DataContractRepository.java b/openmetadata-service/src/main/java/org/openmetadata/service/jdbi3/DataContractRepository.java index 2d3bbb858d5..f169d96df3b 100644 --- a/openmetadata-service/src/main/java/org/openmetadata/service/jdbi3/DataContractRepository.java +++ b/openmetadata-service/src/main/java/org/openmetadata/service/jdbi3/DataContractRepository.java @@ -136,6 +136,8 @@ public class DataContractRepository extends EntityRepository { 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 { 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 { .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) { diff --git a/openmetadata-service/src/test/java/org/openmetadata/service/resources/data/DataContractResourceTest.java b/openmetadata-service/src/test/java/org/openmetadata/service/resources/data/DataContractResourceTest.java index 6cf3ba75213..a820028011a 100644 --- a/openmetadata-service/src/test/java/org/openmetadata/service/resources/data/DataContractResourceTest.java +++ b/openmetadata-service/src/test/java/org/openmetadata/service/resources/data/DataContractResourceTest.java @@ -1757,6 +1757,138 @@ public class DataContractResourceTest extends EntityResourceTest 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 diff --git a/openmetadata-spec/src/main/resources/json/schema/entity/data/dataContract.json b/openmetadata-spec/src/main/resources/json/schema/entity/data/dataContract.json index 072f0032b0d..4a021902196 100644 --- a/openmetadata-spec/src/main/resources/json/schema/entity/data/dataContract.json +++ b/openmetadata-spec/src/main/resources/json/schema/entity/data/dataContract.json @@ -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" diff --git a/openmetadata-ui/src/main/resources/ui/src/generated/api/events/createNotificationTemplate.ts b/openmetadata-ui/src/main/resources/ui/src/generated/api/events/createNotificationTemplate.ts new file mode 100644 index 00000000000..bb6f8e4176c --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/generated/api/events/createNotificationTemplate.ts @@ -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; +} diff --git a/openmetadata-ui/src/main/resources/ui/src/generated/entity/data/dataContract.ts b/openmetadata-ui/src/main/resources/ui/src/generated/entity/data/dataContract.ts index 81c31e8db7c..c9a3ab9c000 100644 --- a/openmetadata-ui/src/main/resources/ui/src/generated/entity/data/dataContract.ts +++ b/openmetadata-ui/src/main/resources/ui/src/generated/entity/data/dataContract.ts @@ -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. */ diff --git a/openmetadata-ui/src/main/resources/ui/src/generated/entity/events/notificationTemplate.ts b/openmetadata-ui/src/main/resources/ui/src/generated/entity/events/notificationTemplate.ts new file mode 100644 index 00000000000..2d1d73ae653 --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/generated/entity/events/notificationTemplate.ts @@ -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", +}