Of course, to actually test the UX you should create some assets of the new type(s) and test. The simplest way to do this is to make them programmatically accessible — via an SDK.
Each SDK contains a generator package that generates SDK code based on the latest typedefs available in your Atlan instance. However, each new asset type will also have a specific set of minimal attributes required when creating any instances of that type.
This minimal set of attributes is defined for developers to understand and consume through a creator method.
All asset types require a qualifiedName, and this must be unique for all instances of that asset type.
Consider carefully how the qualifiedName should be constructed
Since it must be unique across all instances of that asset type, carefully consider how the qualifiedName should be constructed — and automate or enforce this as much as possible in your creator implementation. While names are often used in the qualifiedName, if the system you are representing does not have unique names at a given level (or these are subject to change without the object itself changing) you may need to use some other string in the qualifiedName to ensure it remains unique.
An example qualifiedName for a dataset named some-dataset, in a generic database connection (that happened to be created on February 13, 2009 at 23:31:30 GMT).
An example qualifiedName for a table named some-table created within that dataset.
An example qualifiedName for a field named some-field created within that table.
From the examples above, you can see all assets require at least the following information at creation (even top-level assets):
typeName — to define the type of asset being created
qualifiedName — which must be unique across all instances of the asset type, since it is used to determine whether to update or create a new instance
connectionQualifiedName — for UX filtering and access control purposes, which has embedded within it a connectorType (which in turn determines which icon to use to represent each instance of an asset)
Now that we know the minimal required attributes for creation, we can define these in a template for each SDK. These templates define only the unique portion of the generated code in each SDK — any standard code will still continue to be generated automatically:
<#macroall>/** * Builds the minimal object necessary to create a CustomDataset. * * @param name of the CustomDataset * @param connectionQualifiedName unique name of the connection through which the spec is accessible * @return the minimal object necessary to create the CustomDataset, as a builder */publicstaticCustomDatasetBuilder<?,?>creator(Stringname,StringconnectionQualifiedName){// (1)returnCustomDataset._internal().guid("-"+ThreadLocalRandom.current().nextLong(0,Long.MAX_VALUE-1))// (2).qualifiedName(connectionQualifiedName+"/"+name).name(name).connectionQualifiedName(connectionQualifiedName).connectorType(Connection.getConnectorTypeFromQualifiedName(connectionQualifiedName));}/** * Builds the minimal object necessary to update a CustomDataset. * * @param qualifiedName of the CustomDataset * @param name of the CustomDataset * @return the minimal request necessary to update the CustomDataset, as a builder */publicstaticCustomDatasetBuilder<?,?>updater(StringqualifiedName,Stringname){// (3)returnCustomDataset._internal().guid("-"+ThreadLocalRandom.current().nextLong(0,Long.MAX_VALUE-1)).qualifiedName(qualifiedName).name(name);}/** * Builds the minimal object necessary to apply an update to a CustomDataset, from a potentially * more-complete CustomDataset object. * * @return the minimal object necessary to update the CustomDataset, as a builder * @throws InvalidRequestException if any of the minimal set of required properties for CustomDataset are not found in the initial object */@OverridepublicCustomDatasetBuilder<?,?>trimToRequired()throwsInvalidRequestException{// (4)validateRequired(TYPE_NAME,Map.of("qualifiedName",this.getQualifiedName(),"name",this.getName()));returnupdater(this.getQualifiedName(),this.getName());}</#macro>
Even though we require 4 attributes to create an CustomDataset, we can derive all of them from just 2 inputs. So to keep the interface as simple as possible, we will only request the 2 inputs we need to derive (automatically) the rest.
Always set the guid to a random, negative integer. This allows the SDK (and Atlan's back-end) to handle referential integrity when multiple inter-related assets are submitted in a single request — even if some of them need to be created.
Also implement an updater() method that takes the minimal set of attributes required to update an asset of this type. In almost all cases this will be qualifiedName and name, but in some very rare cases could require other attributes.
Finally, implement the trimToRequired method to validate that an object of this type has the minimal set of attributes required to be used to update such an asset in Atlan. Like the updater method this will in almost all cases just validate qualifiedName and name are present, but in some very rare cases could validate other attributes.
@classmethod@init_guiddefcreator(cls,*,name:str,connection_qualified_name:str)->CustomDataset:# (1)validate_required_fields(["name","connection_qualified_name"],[name,connection_qualified_name])attributes=CustomDataset.Attributes.create(name=name,connection_qualified_name=connection_qualified_name)returncls(attributes=attributes)@classmethod@init_guiddefcreate(cls,*,name:str,connection_qualified_name:str)->CustomDataset:warn(("This method is deprecated, please use 'creator' ""instead, which offers identical functionality."),DeprecationWarning,stacklevel=2,)returncls.creator(name=name,connection_qualified_name=connection_qualified_name)
Even though we require 4 attributes to create an CustomDataset, we can derive all of them from just 2 inputs. So to keep the interface as simple as possible, we will only request the 2 inputs we need to derive (automatically) the rest.
As stated earlier, the qualifiedName should be a concatenation onto the parent's qualifiedName (in our running example, the parent of a CustomTable is a CustomDataset).
<#macroall>/** * Builds the minimal object necessary to create a CustomTable. * * @param name of the CustomTable * @param customDataset in which the CustomTable should be created, which must have at least * a qualifiedName * @return the minimal request necessary to create the CustomTable, as a builder * @throws InvalidRequestException if the CustomDataset provided is without a qualifiedName */publicstaticCustomTableBuilder<?,?>creator(Stringname,CustomDatasetcustomDataset)throwsInvalidRequestException{// (1)validateRelationship(CustomDataset.TYPE_NAME,Map.of("qualifiedName",customDataset.getQualifiedName()));returncreator(name,customDataset.getQualifiedName()).customDataset(customDataset.trimToReference());// (2)}/** * Builds the minimal object necessary to create an CustomTable. * * @param name unique name of the CustomTable * @param customDatasetQualifiedName unique name of the CustomDataset through which the table is accessible * @return the minimal object necessary to create the CustomTable, as a builder */publicstaticCustomTableBuilder<?,?>creator(Stringname,StringcustomDatasetQualifiedName){// (3)StringconnectionQualifiedName=StringUtils.getParentQualifiedNameFromQualifiedName(customDatasetQualifiedName);returnCustomTable._internal().guid("-"+ThreadLocalRandom.current().nextLong(0,Long.MAX_VALUE-1)).qualifiedName(customDatasetQualifiedName+"/"+name).name(name).customDataset(CustomDataset.refByQualifiedName(customDatasetQualifiedName)).connectionQualifiedName(connectionQualifiedName).connectorType(Connection.getConnectorTypeFromQualifiedName(connectionQualifiedName));}/** * Builds the minimal object necessary to update a CustomTable. * * @param qualifiedName of the CustomTable * @param name of the CustomTable * @return the minimal request necessary to update the CustomTable, as a builder */publicstaticCustomTableBuilder<?,?>updater(StringqualifiedName,Stringname){returnCustomTable._internal().guid("-"+ThreadLocalRandom.current().nextLong(0,Long.MAX_VALUE-1)).qualifiedName(qualifiedName).name(name);}/** * Builds the minimal object necessary to apply an update to a CustomTable, from a potentially * more-complete CustomTable object. * * @return the minimal object necessary to update the CustomTable, as a builder * @throws InvalidRequestException if any of the minimal set of required properties for CustomTable are not found in the initial object */@OverridepublicCustomTableBuilder<?,?>trimToRequired()throwsInvalidRequestException{validateRequired(TYPE_NAME,Map.of("qualifiedName",this.getQualifiedName(),"name",this.getName()));returnupdater(this.getQualifiedName(),this.getName());}</#macro>
For asset types that have a parent asset, you should provide multiple overloaded creator methods. For example, one that takes the parent object itself (and validates the provided object has the minimal set of attributes we require on it) and one that takes only a qualifiedName of the parent asset.
When implementing the method that takes a parent object, always set the relationship to the parent object explicitly and by using the trimToReference() method on the parent object. This ensures that any GUID on the parent object is used to create the reference to the parent object — which ensures that any negative integer present for referential integrity is preferred over a qualifiedName for the parent object. (Which further ensures that you can create both parent and child objects in the same request.)
Typically the fully-parameterized creator() method (with various string parameters) will be the one you call through to from any other overloaded creator() methods, so they all share the same foundational implementation.
@overload@classmethoddefcreator(cls,*,name:str,custom_dataset_qualified_name:str,)->CustomTable:...@overload@classmethoddefcreator(cls,*,name:str,custom_dataset_qualified_name:str,connection_qualified_name:str,)->CustomTable:...@classmethod@init_guiddefcreator(cls,*,name:str,custom_dataset_qualified_name:str,connection_qualified_name:Optional[str]=None,)->CustomTable:validate_required_fields(["name","custom_dataset_qualified_name"],[name,custom_dataset_qualified_name])attributes=CustomTable.Attributes.create(name=name,custom_dataset_qualified_name=custom_dataset_qualified_name,connection_qualified_name=connection_qualified_name,)returncls(attributes=attributes)@classmethod@init_guiddefcreate(cls,*,name:str,custom_dataset_qualified_name:str)->CustomTable:warn(("This method is deprecated, please use 'creator' ""instead, which offers identical functionality."),DeprecationWarning,stacklevel=2,)returncls.creator(name=name,custom_dataset_qualified_name=custom_dataset_qualified_name)
Finally, the CustomField template will be very similar to the CustomTable template.
Illustrates the case where a qualifiedName may contain forward-slashes
Since qualifiedNames are typically constructed using a / as a delimiter, if the system you are representing could actually contain a / in the unique information you are placing into the qualifiedName you need to be sure you pass all information to create the qualifiedName — you will not be able to parse the parent's qualifiedName in these cases.
<#macroall>/** * Builds the minimal object necessary to create a CustomField. * * @param name of the CustomField * @param customTable in which the CustomField should be created, which must have at least * a qualifiedName * @return the minimal request necessary to create the CustomField, as a builder * @throws InvalidRequestException if the CustomTable provided is without a qualifiedName */publicstaticCustomFieldBuilder<?,?>creator(Stringname,CustomTablecustomTable)throwsInvalidRequestException{// (1)Map<String,String>map=newHashMap<>();// (2)map.put("connectionQualifiedName",customTable.getConnectionQualifiedName());map.put("customDatasetName",customTable.getCustomDatasetName());map.put("customDatasetQualifiedName",customTable.getCustomDatasetQualifiedName());map.put("name",customTable.getName());map.put("qualifiedName",customTable.getQualifiedName());validateRelationship(CustomTable.TYPE_NAME,map);returncreator(name,customTable.getConnectionQualifiedName(),customTable.getCustomDatasetName(),customTable.getCustomDatasetQualifiedName(),customTable.getName(),customTable.getQualifiedName()).customTable(customTable.trimToReference());// (3)}/** * Builds the minimal object necessary to create a CustomField. * * @param name unique name of the CustomField * @param customTableQualifiedName unique name of the CustomTable through which the table is accessible * @return the minimal object necessary to create the CustomField, as a builder */publicstaticCustomFieldBuilder<?,?>creator(Stringname,StringcustomTableQualifiedName){// (4)StringcustomTableName=StringUtils.getNameFromQualifiedName(customTableQualifiedName);StringcustomDatasetQualifiedName=StringUtils.getParentQualifiedNameFromQualifiedName(customTableQualifiedName);StringcustomDatasetName=StringUtils.getNameFromQualifiedName(customDatasetQualifiedName);StringconnectionQualifiedName=StringUtils.getParentQualifiedNameFromQualifiedName(customDatasetQualifiedName);returncreator(name,connectionQualifiedName,customDatasetName,customDatasetQualifiedName,customTableName,customTableQualifiedName);}/** * Builds the minimal object necessary to create a CustomField. * * @param name of the CustomField * @param connectionQualifiedName unique name of the connection in which to create the CustomField * @param customDatasetQualifiedName simple name of the CustomDataset in which to create the CustomField * @param customDatasetName unique name of the CustomDataset in which to create the CustomField * @param customTableName simple name of the CustomTable in which to create the CustomField * @param customTableQualifiedName unique name of the CustomTable in which to create the CustomField * @return the minimal request necessary to create the CustomField, as a builder */publicstaticCustomFieldBuilder<?,?>creator(Stringname,StringconnectionQualifiedName,StringcustomDatasetQualifiedName,StringcustomDatasetName,StringcustomTableName,StringcustomTableQualifiedName){AtlanConnectorTypeconnectorType=Connection.getConnectorTypeFromQualifiedName(connectionQualifiedName);returnCustomField._internal().guid("-"+ThreadLocalRandom.current().nextLong(0,Long.MAX_VALUE-1)).name(name).qualifiedName(generateQualifiedName(name,customTableQualifiedName)).connectorType(connectorType).customTableName(customTableName).customTableQualifiedName(customTableQualifiedName).customTable(CustomTable.refByQualifiedName(customTableQualifiedName)).customDatasetName(customDatasetName).customDatasetQualifiedName(customDatasetQualifiedName).connectionQualifiedName(connectionQualifiedName);}/** * Generate a unique CustomField name. * * @param name of the CustomField * @param customTableQualifiedName unique name of the CustomTable in which this CustomField exists * @return a unique name for the CustomField */publicstaticStringgenerateQualifiedName(Stringname,StringcustomTableQualifiedName){// (6)returncustomTableQualifiedName+"/"+name;}/** * Builds the minimal object necessary to update a CustomField. * * @param qualifiedName of the CustomField * @param name of the CustomField * @return the minimal request necessary to update the CustomField, as a builder */publicstaticCustomFieldBuilder<?,?>updater(StringqualifiedName,Stringname){returnCustomField._internal().guid("-"+ThreadLocalRandom.current().nextLong(0,Long.MAX_VALUE-1)).qualifiedName(qualifiedName).name(name);}/** * Builds the minimal object necessary to apply an update to a CustomField, from a potentially * more-complete CustomField object. * * @return the minimal object necessary to update the CustomField, as a builder * @throws InvalidRequestException if any of the minimal set of required properties for CustomField are not found in the initial object */@OverridepublicCustomFieldBuilder<?,?>trimToRequired()throwsInvalidRequestException{validateRequired(TYPE_NAME,Map.of("qualifiedName",this.getQualifiedName(),"name",this.getName()));returnupdater(this.getQualifiedName(),this.getName());}</#macro>
For asset types that have a parent asset, you should provide multiple overloaded creator methods. For example, one that takes the parent object itself (and validates the provided object has the minimal set of attributes we require on it) and one that takes all the qualifiedNames and de-normalized names of the ancestral assets.
When receiving only the parent asset, you will need to validate you have all the other information required on that parent asset (in particular, the full set of de-normalized attributes needed to create this asset).
When implementing the method that takes a parent object, always set the relationship to the parent object explicitly and by using the trimToReference() method on the parent object. This ensures that any GUID on the parent object is used to create the reference to the parent object — which ensures that any negative integer present for referential integrity is preferred over a qualifiedName for the parent object. (Which further ensures that you can create both parent and child objects in the same request.)
You may still want to implement a creator() that parses details from the immediate parent's qualifiedName, if you know you will have control over when you must use the other (because some element of the qualifiedName itself has a / in it).
Typically the fully-parameterized creator() method (with various string parameters) will be the one you call through to from any other overloaded creator() methods, so they all share the same foundational implementation.
You may also want to define a distinct method to generate the qualifiedName for the asset based on a set of defined inputs.
@overload@classmethoddefcreator(cls,*,name:str,custom_table_qualified_name:str,)->CustomField:...@overload@classmethoddefcreator(cls,*,name:str,custom_table_qualified_name:str,custom_table_name:str,custom_dataset_name:str,custom_dataset_qualified_name:str,connection_qualified_name:str,)->CustomField:...@classmethod@init_guiddefcreator(cls,*,name:str,custom_table_qualified_name:str,custom_table_name:Optional[str]=None,custom_dataset_name:Optional[str]=None,custom_dataset_qualified_name:Optional[str]=None,connection_qualified_name:Optional[str]=None,)->CustomField:validate_required_fields(["name","custom_table_qualified_name"],[name,custom_table_qualified_name])attributes=CustomField.Attributes.create(name=name,custom_table_qualified_name=custom_table_qualified_name,custom_table_name=custom_table_name,custom_dataset_name=custom_dataset_name,custom_dataset_qualified_name=custom_dataset_qualified_name,connection_qualified_name=connection_qualified_name,)returncls(attributes=attributes)@classmethod@init_guiddefcreate(cls,*,name:str,custom_table_qualified_name:str)->CustomField:warn(("This method is deprecated, please use 'creator' ""instead, which offers identical functionality."),DeprecationWarning,stacklevel=2,)returncls.creator(name=name,custom_table_qualified_name=custom_table_qualified_name)
Now that the creator method has been defined for each new asset type, you can regenerate the SDK's code to include these new asset types:
Before running any generator scripts, make sure you have configured your environment variables (setting the ATLAN_BASE_URL and ATLAN_API_KEY environment variables is sufficient).
Generate the asset model, enums, and structs in the SDK based on the typedefs present in your Atlan instance:
./gradlewgenModel
If you see failures
The generator also generates unit tests for new asset types, which will use Java reflection to investigate objects like enums. If you see an error during the generation that a given type (struct or enum) is unknown, you may need to simply re-run the generator.
The generated files will be unformatted, so we recommend running Spotless to format the code nicely: