Computation Providers
In SolidX, computed fields are fields whose values are automatically derived from other data — for example, totalPrice, fullName, or age. They eliminate the need to store redundant values and ensure consistency across your models.
Computed fields are powered by computation providers, which encapsulate the business logic required to calculate and assign values to these fields.
Providers are triggered automatically when relevant data changes, ensuring that the computed field always reflects the correct value without manual intervention.
How to Configure a Computed Field
Sample Metadata Configuration
Below is an example configuration for a computed field named amountPaid in the paymentCollectionItemDetail model. The field computes the total amount paid based on related payment records.
Example: Configuration for amountPaid computed field
amountPaid computed field{
"name": "paymentCollectionItemDetail",
"displayName": "Payment Collection Item Detail",
"fields": [
{
"name": "amountPaid",
"displayName": "Amount Paid",
"type": "computed",
"ormType": "varchar",
"computedFieldTriggerConfig": [
{
"modelName": "paymentCollectionItemDetail",
"moduleName": "fees-portal",
"operations": [
"after-update",
"after-insert"
]
}
],
"computedFieldValueProvider": "PaymentCollectionItemAmountProvider",
"computedFieldValueProviderCtxt": "{}"
}
]
}
computedFieldValueProviderspecifies the provider class responsible for the computation logic.computedFieldTriggerConfigdefines the entities and operations that trigger the computation.
Supported operations include:
before-updatebefore-insertbefore-deleteafter-updateafter-insertafter-delete
🔸 Before operations trigger preComputeValue() of IEntityPreComputeFieldProvider.
🔸 After operations trigger postComputeAndSaveValue() of IEntityPostComputeFieldProvider.
Note: For before operations,
modelNamemust be the same as the model with the computed field. For after operations, it can be any model, related or unrelated.
You can:
- Access the triggering entity and metadata
- Set the computed value (auto-saved in
preComputeValue) - Execute logic and persist data in
postComputeAndSaveValue
Sample Provider Implementation
Here is a sample implementation of the PaymentCollectionItemAmountProvider:
Example: PaymentCollectionItemAmountProvider Implementation
PaymentCollectionItemAmountProvider Implementationimport { Injectable } from '@nestjs/common';
import { InjectEntityManager } from '@nestjs/typeorm';
import {
ComputedFieldMetadata,
ComputedFieldProvider,
IEntityPostComputeFieldProvider
} from '@solidxai/core';
import { EntityManager } from 'typeorm';
import { PaymentCollectionItemDetail } from '../entities/payment-collection-item-detail.entity';
import { PaymentCollectionItem } from '../entities/payment-collection-item.entity';
@ComputedFieldProvider()
@Injectable()
export class PaymentCollectionItemAmountProvider implements IEntityPostComputeFieldProvider<PaymentCollectionItemDetail, any> {
constructor(
@InjectEntityManager()
private readonly entityManager: EntityManager,
) {}
async postComputeAndSaveValue(
triggerEntity: PaymentCollectionItemDetail,
computedFieldMetadata: ComputedFieldMetadata<any>,
): Promise<void> {
if (!triggerEntity?.paymentCollectionItem?.id) {
console.error('Payment Collection Item Id Missing');
}
const { amountPaid, totalAmountToBePaid, amountPending, status } =
await this.getPaymentCollectionItemAmounts(triggerEntity?.paymentCollectionItem?.id);
await this.entityManager.update(
PaymentCollectionItem,
{ id: triggerEntity?.paymentCollectionItem?.id },
{
amountPaid: String(amountPaid),
amountPending: String(amountPending),
totalAmountToBePaid: String(totalAmountToBePaid),
status: status,
},
);
}
name(): string {
return 'PaymentCollectionItemAmountProvider';
}
help(): string {
return 'Provider used to compute payment collection item amounts based on related details.';
}
private async getPaymentCollectionItemAmounts(itemId: number): Promise<{
amountPaid: number;
totalAmountToBePaid: number;
amountPending: number;
status: string;
}> {
const details = await this.entityManager.find(PaymentCollectionItemDetail, {
where: { paymentCollectionItem: { id: itemId }, paymentStatus: 'Succeeded' },
relations: ['paymentCollectionItem'],
});
const amountPaid = details.reduce(
(sum, detail) => sum + Number(detail.amountPaid || 0),
0,
);
const paymentCollectionItem = details[0]?.paymentCollectionItem;
const totalAmountToBePaid =
Number(paymentCollectionItem?.amountToBePaid || 0) +
Number(paymentCollectionItem?.lateAmountToBePaid || 0);
const amountPending = totalAmountToBePaid - amountPaid;
const status = amountPending > 0 ? 'Partially Paid' : 'Fully Paid';
return { amountPaid, totalAmountToBePaid, amountPending, status };
}
}
Computation Provider Interfaces
To implement a custom computation provider, you need to implement one or both of the following interfaces:
Interfaces to Implement
export interface IEntityPreComputeFieldProvider<TTriggerEntity, TContext, TValue=void> extends IEntityComputedFieldProvider {
preComputeValue(triggerEntity: TTriggerEntity, computedFieldMetadata: ComputedFieldMetadata<TContext>): Promise<TValue>;
}
export interface IEntityPostComputeFieldProvider<TTriggerEntity, TContext> extends IEntityComputedFieldProvider {
postComputeAndSaveValue(triggerEntity: TTriggerEntity, computedFieldMetadata: ComputedFieldMetadata<TContext>): Promise<void>;
}
export interface IEntityComputedFieldProvider {
help(): string;
name(): string;
}
How It Works
Core Mechanism
ComputedEntityFieldSubscriberlistens toinsert,update, anddeleteevents across all entities.- For before operations:
preComputeValue()is called synchronously- Value is set directly on the entity and auto-saved
- For after operations:
postComputeAndSaveValue()is called asynchronously via Background Jobs- Final saving happens via
ComputedFieldEvaluationSubscriber
Note
Computed field configurations are loaded from the database and cached in the Solid Registry at application startup. Any changes require a server restart to take effect.
In-Built Providers
This section describes the built-in computation providers available in SolidX for common use cases. Each provider includes a short description and the corresponding context interface for configuration.
AlphaNumExternalIdComputationProvider
Generates an alphanumeric external ID based on a configurable
pattern.
Useful for creating unique, human-readable identifiers (like invoice
numbers or record codes).
View Context Interface
export interface AlphaNumExternalIdContext {
prefix?: string; // alias -> staticPrefix
length?: number; // i.e default=5 // generated code length
dynamicFieldPrefix?: string; // field name on the entity
}
ConcatEntityComputedFieldProvider
Concatenates multiple entity fields into a single string.
Ideal for building display names, codes, or composite labels derived
from existing fields.
View Context Interface
export interface ConcatComputedFieldContext {
separator: string; // concatenated values separator
// The fields to concatenate
fields: string[]; // supports "city.name" for nested paths (1-level deep)
// Optional: if true, slugify each field value before concatenation
slugify?: boolean;
}
UuidExternalIdEntityComputedFieldProvider
Generates a UUID-based external ID with an optional prefix.
Useful for creating globally unique identifiers for records.
Ideal for entities requiring unique references across distributed
systems.
View Context Interface
export interface UuidExternalIdContext {
prefix?: string; // alias -> staticPrefix
}
NoopsEntityComputedFieldProviderService
A no-operation (noop) provider that does not modify entity fields.
It allows tagging fields as computed without performing redundant
calculations.
This is particularly useful when one provider performs the main computation, and other computed fields are only "placeholders".
Example Use Case
- Suppose you have multiple amount fields:
amount,taxes, andtotalAmount. - You create a computed field
totalAmountlinked to a provider likeCalculateTotalAmountProviderthat performs all computations. - The fields
amountandtaxescan still be tagged as computed but linked to NoopsEntityComputedFieldProviderService, so they don't re-compute independently.
This ensures the logic resides only in one provider while maintaining metadata consistency.