Skip to main content

Shared Services

The SharedModule is decorated with @Global(), making all its exported services available to every module without explicit imports. These services handle cross-cutting concerns like audit logging, activity feeds, document management, and record-level access control.

Overview

ServicePurpose
AuditServiceImmutable audit trail for all mutations
ActivityServiceActivity feed entries (calls, emails, meetings, etc.)
NotesServiceUser notes attached to any entity
DocumentsServiceDocument/file management per entity
DataAccessServiceGenerates record-level access SQL WHERE clauses
RecordTeamServiceManages team members assigned to individual records
FieldValidationServiceValidates field values against configured rules
TableColumnsServiceColumn definitions available per module
TablePreferencesServiceUser-specific column visibility, widths, and sort
ModuleSettingsServiceKey-value JSONB settings per module

Using Shared Services

Because SharedModule is @Global(), simply inject services via constructor — no need to add SharedModule to your module's imports:

@Injectable()
export class ProposalsService {
constructor(
private readonly dataSource: DataSource,
private readonly auditService: AuditService, // Auto-available
private readonly activityService: ActivityService, // Auto-available
private readonly dataAccessService: DataAccessService, // Auto-available
) {}
}

AuditService

Creates immutable audit log entries in "${schema}".audit_logs. Called after every create, update, and delete operation.

Method Signature

async log(schemaName: string, entry: {
entityType: string; // Table/entity name: 'leads', 'contacts', etc.
entityId: string; // Record UUID
action: string; // 'create' | 'update' | 'delete'
changes: object; // { before: {...}, after: {...} } for updates
newValues: object; // Current state of the record
performedBy: string; // User ID who performed the action
}): Promise<void>

Usage

// After creating a record
await this.auditService.log(schemaName, {
entityType: 'leads',
entityId: newLead.id,
action: 'create',
changes: {},
newValues: newLead,
performedBy: userId,
});

// After updating a record
await this.auditService.log(schemaName, {
entityType: 'leads',
entityId: id,
action: 'update',
changes: { before: oldValues, after: newValues },
newValues: updatedRow,
performedBy: userId,
});

// After soft-deleting a record
await this.auditService.log(schemaName, {
entityType: 'leads',
entityId: id,
action: 'delete',
changes: {},
newValues: {},
performedBy: userId,
});
warning

Audit logging is mandatory for all create, update, and delete operations. Skipping it breaks compliance and traceability requirements.

ActivityService

Creates activity feed entries tied to any entity. Activities represent business events (phone calls, emails sent, meetings, stage changes).

Method Signature

async create(schemaName: string, activity: {
entityType: string; // 'leads', 'contacts', 'opportunities'
entityId: string; // Record UUID
activityType: string; // 'call', 'email', 'meeting', 'note', 'stage_change', etc.
title: string; // Human-readable description
description?: string; // Optional detailed description
performedBy: string; // User ID
}): Promise<any>

Usage

await this.activityService.create(schemaName, {
entityType: 'leads',
entityId: leadId,
activityType: 'stage_change',
title: `Stage changed from "${oldStage}" to "${newStage}"`,
performedBy: userId,
});

await this.activityService.create(schemaName, {
entityType: 'contacts',
entityId: contactId,
activityType: 'email',
title: 'Follow-up email sent',
description: 'Sent proposal follow-up email',
performedBy: userId,
});

NotesService

CRUD operations for user notes attached to any entity type.

Methods

// Create a note
async create(
schemaName: string,
entityType: string,
entityId: string,
content: string,
userId: string,
): Promise<any>

// Find notes for an entity
async findByEntity(
schemaName: string,
entityType: string,
entityId: string,
): Promise<any[]>

Usage

// Create
const note = await this.notesService.create(
schemaName, 'leads', leadId, 'Client is interested in premium plan', userId,
);

// List
const notes = await this.notesService.findByEntity(schemaName, 'leads', leadId);

DocumentsService

Manages documents (files) associated with any entity.

Methods

// Find documents for an entity
async findByEntity(
schemaName: string,
entityType: string,
entityId: string,
): Promise<any[]>

// Associate a document with an entity
async linkToEntity(
schemaName: string,
documentId: string,
entityType: string,
entityId: string,
): Promise<void>

DataAccessService

Generates SQL WHERE clauses based on the user's record access level. This is the core of record-level security.

Method Signature

async buildWhereClause(
schemaName: string,
user: JwtPayload,
module: string,
alias?: string,
): Promise<{ clause: string; params: any[] }>

Usage

async findAll(schemaName: string, user: JwtPayload) {
const { clause, params } = await this.dataAccessService.buildWhereClause(
schemaName, user, 'leads', 'l',
);

const rows = await this.dataSource.query(
`SELECT l.* FROM "${schemaName}".leads l
WHERE l.deleted_at IS NULL ${clause}
ORDER BY l.created_at DESC`,
params,
);

return rows.map(this.formatRow);
}
tip

The alias parameter should match the table alias in your SQL query. If your query uses FROM leads l, pass 'l' as the alias. If no alias is used, omit the parameter.

RecordTeamService

Manages which users are assigned as team members on individual records (not to be confused with organizational teams).

Methods

// Add a team member to a record
async addMember(
schemaName: string,
entityType: string,
entityId: string,
userId: string,
role?: string,
): Promise<any>

// Remove a team member
async removeMember(
schemaName: string,
entityType: string,
entityId: string,
userId: string,
): Promise<void>

// Get all team members for a record
async getTeamMembers(
schemaName: string,
entityType: string,
entityId: string,
): Promise<any[]>

Usage

// Add a team member to a lead
await this.recordTeamService.addMember(
schemaName, 'leads', leadId, userId, 'sales_rep',
);

// Get team members
const team = await this.recordTeamService.getTeamMembers(
schemaName, 'leads', leadId,
);

FieldValidationService

Validates field values against rules configured in the admin panel (required fields, regex patterns, min/max values).

Method Signature

async validate(
schemaName: string,
module: string,
data: Record<string, any>,
): Promise<{ valid: boolean; errors: string[] }>

Usage

const { valid, errors } = await this.fieldValidationService.validate(
schemaName, 'leads', body,
);

if (!valid) {
throw new BadRequestException({ message: 'Validation failed', errors });
}

TableColumnsService

Returns the available columns for a module (used by the data table component).

async getColumns(schemaName: string, module: string): Promise<ColumnDefinition[]>

TablePreferencesService

Manages user-specific table preferences (which columns are visible, column widths, sort order).

async getPreferences(schemaName: string, userId: string, module: string): Promise<TablePreference>
async savePreferences(schemaName: string, userId: string, module: string, prefs: any): Promise<void>

ModuleSettingsService

Stores and retrieves key-value settings per module using JSONB in the module_settings table.

async getSetting(schemaName: string, module: string, key: string): Promise<any>
async setSetting(schemaName: string, module: string, key: string, value: any): Promise<void>
async getAllSettings(schemaName: string, module: string): Promise<Record<string, any>>

Usage

// Get a setting
const autoAssign = await this.moduleSettingsService.getSetting(
schemaName, 'leads', 'auto_assign_enabled',
);

// Set a setting
await this.moduleSettingsService.setSetting(
schemaName, 'leads', 'auto_assign_enabled', true,
);
Storage Format

Settings are stored as JSONB with a setting_key and setting_value column, allowing any JSON-serializable value to be stored without schema changes.