Skip to main content

Overview

Venzia Datalinks is built with extensibility in mind. This guide covers common extension scenarios and how to implement them.

Extension Points

Backend Extensions

Custom Web Scripts

Add new REST API endpoints for custom functionality

Content Model

Define new aspects and properties for different data types

Service Beans

Create Spring beans for business logic

Validators

Implement custom JSON schema validators

Frontend Extensions

Custom Components

Build new UI components for datalink interactions

NgRx Actions

Add state management for custom features

Services

Create Angular services for new functionality

Evaluators

Define rules for showing/hiding features

Adding Custom Web Scripts

Create new REST endpoints to extend backend functionality.

1. Create Web Script Class

package es.venzia.aqua.datalink.webscript;

import es.venzia.aqua.datalink.core.RegisterDataLink;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.json.JSONArray;
import org.json.JSONObject;
import org.springframework.extensions.webscripts.AbstractWebScript;
import org.springframework.extensions.webscripts.Format;
import org.springframework.extensions.webscripts.WebScriptRequest;
import org.springframework.extensions.webscripts.WebScriptResponse;

import java.io.IOException;

public class GetDataLinkByNameWebScript extends AbstractWebScript {
    private static Log logger = LogFactory.getLog(GetDataLinkByNameWebScript.class);
    
    private RegisterDataLink registerDataLink;
    
    public void setRegisterDataLink(RegisterDataLink registerDataLink) {
        this.registerDataLink = registerDataLink;
    }
    
    @Override
    public void execute(WebScriptRequest req, WebScriptResponse res) 
            throws IOException {
        
        String name = req.getParameter("name");
        
        if (name == null || name.isEmpty()) {
            res.setStatus(400);
            res.getWriter().write("{\"error\": \"Missing name parameter\"}");
            return;
        }
        
        JSONArray dataLinks = registerDataLink.getDataLinks();
        JSONObject found = null;
        
        for (int i = 0; i < dataLinks.length(); i++) {
            JSONObject dataLink = dataLinks.getJSONObject(i);
            if (name.equals(dataLink.getString("name"))) {
                found = dataLink;
                break;
            }
        }
        
        if (found != null) {
            res.setContentType(Format.JSON.mimetype());
            res.getWriter().write(found.toString());
        } else {
            res.setStatus(404);
            res.getWriter().write("{\"error\": \"DataLink not found\"}");
        }
        
        res.getWriter().flush();
    }
}

2. Create Descriptor XML

File: src/main/resources/alfresco/extension/templates/webscripts/venzia/datalink/v1/datalink-get.get.desc.xml
<webscript>
    <shortname>Get DataLink by Name</shortname>
    <description>Retrieves a specific datalink configuration by name</description>
    <url>/aqua/datalink/v1/get?name={name}</url>
    <format default="json">argument</format>
    <authentication>user</authentication>
    <transaction allow="readonly">required</transaction>
</webscript>

3. Register Bean

File: src/main/resources/alfresco/module/aqua-datalink/context/webscript-context.xml
<bean id="webscript.venzia.datalink.v1.datalink-get.get"
      class="es.venzia.aqua.datalink.webscript.GetDataLinkByNameWebScript"
      parent="webscript">
    <property name="registerDataLink" ref="registerDataLink" />
</bean>

4. Use from Frontend

export class VenziaDatalinkApiService {
  
  getDataLinkByName(name: string): Observable<any> {
    return from(
      this.executeWebScript(
        'GET', 
        'aqua/datalink/v1/get', 
        { name: name },
        null, 
        'service', 
        null
      )
    ).pipe(
      catchError((err) => this.handleError(err))
    );
  }
}

Adding Custom Aspects and Properties

Extend the content model to support new data types.

1. Define Aspect

File: src/main/resources/alfresco/module/aqua-datalink/model/datalink-model.xml
<aspect name="dlnk:project">
  <title>Project</title>
  <properties>
    <property name="dlnk:project">
      <type>d:text</type>
      <index enabled="true">
        <atomic>true</atomic>
        <stored>false</stored>
        <tokenised>false</tokenised>
      </index>
    </property>
    <!-- Optional: Add metadata properties -->
    <property name="dlnk:projectLinkedDate">
      <type>d:datetime</type>
      <index enabled="true" />
    </property>
    <property name="dlnk:projectLinkedBy">
      <type>d:text</type>
      <index enabled="true" />
    </property>
  </properties>
</aspect>
File: datalink-project.json
{
  "name": "projects",
  "title": "Company Projects",
  "description": "Link documents to project records",
  "aspectName": "dlnk:project",
  "aspectPropertyName": "dlnk:project",
  "order": 30,
  "connectorRest": {
    "url": "http://localhost:3005/api/v1/private/projects/search",
    "authentication": {
      "type": "basic",
      "username": "apiuser",
      "password": "secret"
    },
    "searchParam": "query"
  },
  "columns": [
    {
      "primaryKey": true,
      "name": "project_id",
      "label": "Project ID",
      "type": "text"
    },
    {
      "primaryKey": false,
      "name": "project_name",
      "label": "Project Name",
      "type": "text"
    },
    {
      "primaryKey": false,
      "name": "status",
      "label": "Status",
      "type": "text"
    },
    {
      "primaryKey": false,
      "name": "start_date",
      "label": "Start Date",
      "type": "date",
      "format": "yyyy-MM-dd"
    }
  ]
}
The order property determines tab order in the UI. Lower numbers appear first.

Adding Custom Services

Create reusable Angular services for common operations.
import { Injectable } from '@angular/core';
import { Observable, of } from 'rxjs';
import { map } from 'rxjs/operators';
import { VenziaDatalinkApiService } from './venzia-datalink-api.service';

@Injectable({
  providedIn: 'root'
})
export class VenziaDatalinkValidationService {
  
  constructor(private datalinkApi: VenziaDatalinkApiService) {}
  
  /**
   * Validates that a node has a specific datalink aspect
   */
  hasDataLinkAspect(
    nodeProperties: any, 
    aspectName: string
  ): boolean {
    return nodeProperties && 
           nodeProperties.hasOwnProperty(aspectName);
  }
  
  /**
   * Checks if a datalink has any linked records
   */
  hasLinkedRecords(
    nodeProperties: any, 
    aspectPropertyName: string
  ): boolean {
    if (!this.hasDataLinkAspect(nodeProperties, aspectPropertyName)) {
      return false;
    }
    
    try {
      const data = JSON.parse(nodeProperties[aspectPropertyName]);
      return Array.isArray(data) && data.length > 0;
    } catch {
      return false;
    }
  }
  
  /**
   * Gets the count of linked records
   */
  getLinkedRecordCount(
    nodeProperties: any, 
    aspectPropertyName: string
  ): number {
    if (!this.hasLinkedRecords(nodeProperties, aspectPropertyName)) {
      return 0;
    }
    
    try {
      const data = JSON.parse(nodeProperties[aspectPropertyName]);
      return data.length;
    } catch {
      return 0;
    }
  }
  
  /**
   * Validates datalink configuration structure
   */
  validateDataLinkConfig(config: any): Observable<boolean> {
    const required = [
      'name', 'aspectName', 'aspectPropertyName', 
      'connectorRest', 'order', 'columns'
    ];
    
    for (const field of required) {
      if (!config.hasOwnProperty(field)) {
        return of(false);
      }
    }
    
    // Validate connector
    if (!config.connectorRest.url || 
        !config.connectorRest.authentication) {
      return of(false);
    }
    
    // Validate columns
    if (!Array.isArray(config.columns) || config.columns.length === 0) {
      return of(false);
    }
    
    const hasPrimaryKey = config.columns.some(
      (col: any) => col.primaryKey === true
    );
    
    return of(hasPrimaryKey);
  }
}

Using the Service

export class CustomComponent {
  constructor(
    private validationService: VenziaDatalinkValidationService
  ) {}
  
  checkNode(node: Node) {
    if (this.validationService.hasLinkedRecords(
      node.properties, 
      'dlnk:employee'
    )) {
      const count = this.validationService.getLinkedRecordCount(
        node.properties, 
        'dlnk:employee'
      );
      console.log(`Node has ${count} linked employees`);
    }
  }
}

Adding Custom Components

Create reusable UI components for datalink features.
import { Component, Input, OnInit } from '@angular/core';
import { CommonModule } from '@angular/common';
import { MatBadgeModule } from '@angular/material/badge';
import { MatIconModule } from '@angular/material/icon';
import { VenziaDatalinkValidationService } from '../../services/venzia-datalink-validation.service';

@Component({
  selector: 'aqua-datalink-badge',
  standalone: true,
  imports: [CommonModule, MatBadgeModule, MatIconModule],
  template: `
    <div class="datalink-badge" *ngIf="hasLinks">
      <mat-icon [matBadge]="linkCount" matBadgeColor="accent">
        link
      </mat-icon>
    </div>
  `,
  styles: [`
    .datalink-badge {
      display: inline-flex;
      align-items: center;
    }
  `]
})
export class DatalinkBadgeComponent implements OnInit {
  @Input() nodeProperties: any;
  @Input() aspectPropertyName: string;
  
  hasLinks = false;
  linkCount = 0;
  
  constructor(
    private validationService: VenziaDatalinkValidationService
  ) {}
  
  ngOnInit() {
    this.hasLinks = this.validationService.hasLinkedRecords(
      this.nodeProperties, 
      this.aspectPropertyName
    );
    
    if (this.hasLinks) {
      this.linkCount = this.validationService.getLinkedRecordCount(
        this.nodeProperties, 
        this.aspectPropertyName
      );
    }
  }
}

Using the Component

<aqua-datalink-badge
  [nodeProperties]="node.entry.properties"
  [aspectPropertyName]="'dlnk:employee'">
</aqua-datalink-badge>

Adding Extension Evaluators

Control when features are visible based on node context.

1. Create Evaluator Rule

File: src/lib/rules/datalink.rules.ts
import { RuleContext } from '@alfresco/adf-extensions';

/**
 * Checks if node can have datalinks applied
 */
export function canDatalink(context: RuleContext): boolean {
  if (!context.selection || context.selection.isEmpty) {
    return false;
  }
  
  const node = context.selection.first;
  
  // Only files can have datalinks
  if (!node.entry.isFile) {
    return false;
  }
  
  // Node must have properties
  if (!node.entry.properties) {
    return false;
  }
  
  return true;
}

/**
 * Checks if node has any datalinks applied
 */
export function hasDatalinks(context: RuleContext): boolean {
  if (!canDatalink(context)) {
    return false;
  }
  
  const node = context.selection.first;
  const properties = node.entry.properties;
  
  // Check for any dlnk: properties
  return Object.keys(properties).some(key => key.startsWith('dlnk:'));
}

/**
 * Checks if node has specific datalink aspect
 */
export function hasEmployeeDatalink(context: RuleContext): boolean {
  if (!canDatalink(context)) {
    return false;
  }
  
  const node = context.selection.first;
  return node.entry.properties.hasOwnProperty('dlnk:employee');
}

2. Register Evaluators

@NgModule({
  // ...
})
export class VenziaDatalinkModule {
  constructor(
    translation: TranslationService, 
    extensions: ExtensionService
  ) {
    // Register evaluators
    extensions.setEvaluators({
      'datalink.canDatalink': rules.canDatalink,
      'datalink.hasDatalinks': rules.hasDatalinks,
      'datalink.hasEmployeeDatalink': rules.hasEmployeeDatalink
    });
  }
}

3. Use in Extension Configuration

File: venzia-datalink.plugin.json
{
  "$schema": "../../extension.schema.json",
  "$version": "1.0.0",
  "$name": "venzia-datalink",
  
  "actions": [
    {
      "id": "datalink.edit",
      "type": "DATALINK_OPEN",
      "payload": "$eval(context.selection.first)"
    }
  ],
  
  "features": {
    "contextMenu": [
      {
        "id": "app.context.datalink",
        "title": "Edit Datalink",
        "icon": "link",
        "actions": {
          "click": "datalink.edit"
        },
        "rules": {
          "visible": "datalink.canDatalink"
        }
      },
      {
        "id": "app.context.datalink.view",
        "title": "View Datalinks",
        "icon": "visibility",
        "actions": {
          "click": "datalink.edit"
        },
        "rules": {
          "visible": "datalink.hasDatalinks"
        }
      }
    ]
  }
}

Adding NgRx Actions and Effects

Extend state management with custom actions.

1. Define Actions

import { Action } from '@ngrx/store';
import { NodeEntry } from '@alfresco/js-api';

export enum DatalinkActionTypes {
  ValidateDatalinks = 'VALIDATE_DATALINKS',
  ValidateDatalinksSuccess = 'VALIDATE_DATALINKS_SUCCESS',
  ValidateDatalinksError = 'VALIDATE_DATALINKS_ERROR',
  ExportDatalinks = 'EXPORT_DATALINKS',
  ExportDatalinksSuccess = 'EXPORT_DATALINKS_SUCCESS'
}

export class ValidateDatalinksAction implements Action {
  readonly type = DatalinkActionTypes.ValidateDatalinks;
  constructor(public payload: NodeEntry) {}
}

export class ValidateDatalinksSuccessAction implements Action {
  readonly type = DatalinkActionTypes.ValidateDatalinksSuccess;
  constructor(public payload: any) {}
}

export class ExportDatalinksAction implements Action {
  readonly type = DatalinkActionTypes.ExportDatalinks;
  constructor(public payload: { nodeId: string; format: string }) {}
}

2. Create Effects

import { Injectable } from '@angular/core';
import { Actions, createEffect, ofType } from '@ngrx/effects';
import { map, catchError, switchMap } from 'rxjs/operators';
import { of } from 'rxjs';

@Injectable()
export class DatalinkValidationEffects {
  
  constructor(
    private actions$: Actions,
    private validationService: VenziaDatalinkValidationService,
    private notificationService: NotificationService
  ) {}
  
  validateDatalinks$ = createEffect(() =>
    this.actions$.pipe(
      ofType<ValidateDatalinksAction>(
        DatalinkActionTypes.ValidateDatalinks
      ),
      switchMap((action) => {
        const node = action.payload;
        const properties = node.entry.properties;
        
        // Validate all datalink aspects
        const results = Object.keys(properties)
          .filter(key => key.startsWith('dlnk:'))
          .map(key => ({
            aspect: key,
            valid: this.validationService.hasLinkedRecords(
              properties, key
            )
          }));
        
        return of(new ValidateDatalinksSuccessAction(results));
      }),
      catchError((error) => {
        this.notificationService.showError(
          'Failed to validate datalinks'
        );
        return of({ type: DatalinkActionTypes.ValidateDatalinksError });
      })
    )
  );
}

3. Dispatch Actions

export class CustomComponent {
  constructor(private store: Store<AppStore>) {}
  
  validateNode(node: NodeEntry) {
    this.store.dispatch(new ValidateDatalinksAction(node));
  }
}

Custom Column Types

Extend the display logic for custom data types.

Backend: Schema Extension

"columns": [
  {
    "name": "status",
    "label": "Status",
    "type": "status",
    "format": "badge"
  },
  {
    "name": "priority",
    "label": "Priority",
    "type": "number",
    "format": "star-rating"
  }
]

Frontend: Custom Cell Renderer

import { Component } from '@angular/core';

@Component({
  selector: 'aqua-status-cell',
  template: `
    <span class="status-badge" [ngClass]="getStatusClass()">
      {{ value }}
    </span>
  `,
  styles: [`
    .status-badge {
      padding: 4px 8px;
      border-radius: 4px;
      font-size: 12px;
      font-weight: 500;
    }
    .status-active { background: #4caf50; color: white; }
    .status-pending { background: #ff9800; color: white; }
    .status-closed { background: #9e9e9e; color: white; }
  `]
})
export class StatusCellComponent {
  value: string;
  
  getStatusClass(): string {
    return `status-${this.value.toLowerCase()}`;
  }
}

Best Practices

  • Follow Angular and Alfresco coding standards
  • Use dependency injection for loose coupling
  • Implement proper error handling
  • Add TypeScript types for type safety
  • Write unit tests for services and components
  • Document custom APIs and components
  • Use semantic versioning for extensions
  • Keep backend and frontend in sync

Testing Extensions

Backend Unit Test

import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnitRunner;
import static org.mockito.Mockito.*;
import static org.junit.Assert.*;

@RunWith(MockitoJUnitRunner.class)
public class GetDataLinkByNameWebScriptTest {
    
    @Mock
    private RegisterDataLink registerDataLink;
    
    @InjectMocks
    private GetDataLinkByNameWebScript webScript;
    
    @Test
    public void testGetDataLinkByName() throws Exception {
        // Test implementation
    }
}

Frontend Unit Test

import { TestBed } from '@angular/core/testing';
import { VenziaDatalinkValidationService } from './venzia-datalink-validation.service';

describe('VenziaDatalinkValidationService', () => {
  let service: VenziaDatalinkValidationService;
  
  beforeEach(() => {
    TestBed.configureTestingModule({});
    service = TestBed.inject(VenziaDatalinkValidationService);
  });
  
  it('should detect linked records', () => {
    const properties = {
      'dlnk:employee': '[{"emp_no":"001","name":"John"}]'
    };
    
    const result = service.hasLinkedRecords(properties, 'dlnk:employee');
    expect(result).toBe(true);
  });
  
  it('should count linked records', () => {
    const properties = {
      'dlnk:employee': '[{"emp_no":"001"},{"emp_no":"002"}]'
    };
    
    const count = service.getLinkedRecordCount(properties, 'dlnk:employee');
    expect(count).toBe(2);
  });
});

Deployment

1

Build Backend

cd venzia-datalink
mvn clean install
2

Build Frontend

cd app
npm run build venzia-datalink
3

Deploy AMP

Copy generated AMP to Alfresco and restart
4

Deploy Frontend

Build and deploy Alfresco Content App with extension
Always test extensions in a development environment before deploying to production.

Build docs developers (and LLMs) love