Building Amplication Plugins - Techniques and Examples
Learn how to develop Amplication plugins with practical techniques and code examples.
Amplication plugins allow developers to extend and customize the code generation process to fit specific application requirements. By creating plugins, developers can modify generated code, introduce new functionalities, and streamline development workflows. This guide provides detailed explanations of essential plugin development techniques, along with practical code examples, to help you build effective Amplication plugins.
Plugins often need to access resource metadata, such as entity definitions, properties, and settings. This information is essential for making informed modifications to the generated code. The context object provides access to all relevant resource data, enabling plugins to dynamically adapt based on project configurations.
Retrieving resource properties.
Copy
const resourceInfo = context.resourceInfo;//Check if resource info is available - this should always be availableif (!resourceInfo) { throw new Error("Resource info is not available");}//Get the name of the resourceconst resourceName = resourceInfo.name;//resource catalog propertiesconst resourceCatalogProperties = (resourceInfo.properties || {}) as Record< string, string>;//Get the value of a property with the key DOMAINconst domain = resourceCatalogProperties["DOMAIN"];//Resource blueprint propertiesconst resourceSetting = (context.resourceSettings?.properties || {}) as Record< string, string>;//Get the value of a property with the key REGIONconst region = resourceSetting["REGION"];
Copy the content of a folder and replace placeholders
Amplication plugins can generate files dynamically by leveraging static template files and replacing placeholders with project-specific values. This approach ensures consistency and reduces manual code duplication.
The functions importStaticFilesWithReplacements and importStaticFiles from context.utils allow developers to import entire directories of template files into the generated code. Placeholders within these files can be replaced with dynamic values, making it easy to customize the generated output.
A list of text values that will be used to replace simple text in the files
and file paths of the loaded files.
Import Static Files With Replacements
Copy
import { IFile, blueprintTypes } from "@amplication/code-gen-types";import { pascalCase } from "pascal-case";import { resolve } from "path";import { CodeBlock } from "@amplication/csharp-ast";//convert the solution file to a AST so it can be used on other plugins as ASTexport async function copyStaticFiles( context: blueprintTypes.DsgContext): Promise<void> { const params = {} as Record<string, string>; //prepare some placeholders for the static files params.SERVICE_DISPLAY_NAME = context.resourceInfo?.name || "Service Name"; params.SERVICE_NAME = pascalCase(params.SERVICE_DISPLAY_NAME); const resourceInfo = context.resourceInfo; if (!resourceInfo) { throw new Error("Resource info is not available"); } //resource catalog properties const resourceCatalogProperties = (resourceInfo.properties || {}) as Record< string, string >; //Resource blueprint properties const resourceSetting = (context.resourceSettings?.properties || {}) as Record<string, string>; //Add all catalog and resource settings to the placeholders //replacement is done on all files and paths in a format {{key}} const placeholders = { ...params, ...resourceCatalogProperties, ...resourceSetting, }; //Add the service name to the placeholders //replacement is done on all files and paths as a simple string replacement const stringReplacements = { TemplateServiceName: params.SERVICE_NAME, }; // read the static files from the static folder const staticPath = resolve(__dirname, "./static"); const files = await context.utils.importStaticFilesWithReplacements( staticPath, ".", placeholders, stringReplacements ); //convert all files to CodeBlock and add them to the context for (const file of files.getAll()) { const codeBlock: IFile<CodeBlock> = { path: file.path, code: new CodeBlock({ code: file.code, }), }; context.files.set(codeBlock); }}
Resources in Amplication can be linked to other related resources. Accessing and managing these relationships is essential for generating code that takes into account dependencies between different resources.
For example, when generating deployment files for a Kubernetes cluster, a plugin might need to retrieve information from all related services to correctly configure the deployment specifications. By accessing related resources, plugins can ensure that generated files reflect the entire system architecture and include all necessary dependencies.
Retrieving related resources
Copy
import { blueprintTypes, DSGResourceData } from "@amplication/code-gen-types";// Relation keys for the related resources based on the blueprint relation configurationexport enum EnumRelationKey { ProjectMetadata = "PROJECT_METADATA", Deployment = "DEPLOYMENT", Service = "SERVICE",}export function getRelatedResources( context: blueprintTypes.DsgContext, relationKey: EnumRelationKey,): DSGResourceData[] { const relation = context.relations?.find( (relation) => relation.relationKey === relationKey, ); if (!relation) { context.logger.error( `Related resource not found for relation key: ${relationKey}`, ); return []; } const relatedResources = context.otherResources?.filter( (resource) => resource.resourceInfo && relation.relatedResources.includes(resource.resourceInfo.id), ); if (!relatedResources) { context.logger.error( `Related resources not found for relation key: ${relationKey}`, ); return []; } return relatedResources;}
Plugins can define configurable settings that influence their behavior. These settings allow users to customize how the plugin functions without modifying its core code.
By reading and applying plugin settings, developers can create more flexible and reusable plugins that adapt to different project requirements.
The FileMap class is the primary interface for managing files within a plugin. It allows plugin developers to add, edit, or remove files from the generated code. Any modifications made through FileMap directly impact the final generated output.
The FileMap is also a shared resource across different events and plugins. Files that were added in previous events or by other plugins are available in the map and can be read or altered. This enables collaboration between multiple plugins and ensures consistency in the generated project structure.
To make any changes to the generated code, plugin developers must use FileMap, ensuring a structured and maintainable approach to file management.
const path = `./src/${serviceName}.sln`; //get the file from the context const slnFile = context.files.get(path); if (!slnFile) { throw new Error(`File ${path} not found`); } const slnFileContent = slnFile.code.toString(); //parse the solution file into an AST const solution = new Solution(); solution.parse(slnFileContent); slnFile.code = solution; //overwrite the existing file with the updated AST context.files.set(slnFile);