Computation Providers
Learn how to extend the backend with custom computation providers.
Overview
In SolidX, a computed field is a field whose value is always derived from other data - never set manually. Common examples include totalPrice (summed from line items), fullName (concatenated from first and last name), or amountPaid (aggregated from child payment records).
A computation provider is the TypeScript class that implements the derivation logic. You register it once, and SolidX calls it automatically whenever the relevant data changes - ensuring computed values stay consistent without any manual intervention.
Mental Model
Mental Model
SolidX can derive field values at runtime, but the derivation logic must be explicitly modeled and registered.
- The computed field metadata declares which field is derived and which events should trigger recomputation.
- The computation provider contains the runtime logic that produces or persists the derived value.
- Use before operations when the value depends only on the triggering entity inside the current transaction.
- Use after operations when the value depends on related records or requires additional writes after persistence.
This page is the bridge between computed-field metadata and the backend logic that keeps derived values consistent at runtime.
Before vs. After
The trigger timing determines which interface your provider implements and how the value gets saved:
| Timing | Operations | Method called | How it runs | When to use |
|---|---|---|---|---|
| Before | before-insert, before-update, before-delete | preComputeValue() | Synchronously, in the same transaction | Value depends only on the triggering entity |
| After | after-insert, after-update, after-delete | postComputeAndSaveValue() | Asynchronously, via a background job | Value depends on related records or requires additional writes |
For before-* triggers, modelName in the trigger config must match the model that owns the computed field.
For after-* triggers, modelName can be any model - related or otherwise.
Configuration
Step 1 - Define the field in metadata
Add the field to your model's metadata with "type": "computed" and specify:
computedFieldValueProvider- the name of the provider class to invokecomputedFieldTriggerConfig- which entity events trigger the computationcomputedFieldValueProviderCtxt- a JSON string passed as context to the provider
Example scenario: paymentCollectionItemDetail records represent individual payment transactions. When a detail is inserted or updated (i.e., a payment comes in), the parent paymentCollectionItem needs its aggregate totals recalculated - amountPaid, amountPending, totalAmountToBePaid, and status. Since this involves writing to a related parent entity, after-* operations are the right choice here.
Example: amountPaid computed field on paymentCollectionItemDetail
{
"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": "{}"
}
]
}Computed field configurations are loaded from the database and cached in the Solid Registry at startup. Any changes require a server restart to take effect.
Step 2 - Implement the provider
Implement IEntityPreComputeFieldProvider for before operations, or IEntityPostComputeFieldProvider for after operations. Decorate the class with @ComputedFieldProvider().
In this example, PaymentCollectionItemAmountProvider implements IEntityPostComputeFieldProvider because it needs to query all related detail records and write aggregated totals back to the parent - work that can only happen after the triggering detail has been saved.
Example: PaymentCollectionItemAmountProvider
import { 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 'Computes 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 };
}
}Step 3 - Register the provider
Register the provider as a NestJS provider in the module it belongs to.
// fees-portal.module.ts
@Module({
providers: [PaymentCollectionItemAmountProvider],
})Provider Interfaces
SolidX provides two interfaces for computation providers. The key difference is in who performs the save and when the logic runs.
IEntityPreComputeFieldProvider - before operations
export interface IEntityPreComputeFieldProvider<TTriggerEntity, TContext, TValue = void>
extends IEntityComputedFieldProvider {
preComputeValue(
triggerEntity: TTriggerEntity,
computedFieldMetadata: ComputedFieldMetadata<TContext>
): Promise<TValue>;
}- Runs synchronously before the entity is saved, inside the same database transaction.
- Your implementation sets the computed value directly on
triggerEntity(e.g.,triggerEntity.fullName = ...). Because the entity is saved immediately after, those mutations are persisted automatically - noentityManagercall needed. - Use this when the computation depends only on the triggering entity's own fields.
- The
modelNameincomputedFieldTriggerConfigmust be the same model that owns the computed field.
IEntityPostComputeFieldProvider - after operations
export interface IEntityPostComputeFieldProvider<TTriggerEntity, TContext>
extends IEntityComputedFieldProvider {
postComputeAndSaveValue(
triggerEntity: TTriggerEntity,
computedFieldMetadata: ComputedFieldMetadata<TContext>
): Promise<void>;
}- Runs asynchronously after the entity is saved, via a background job.
- Returns
void- you are responsible for all persistence. The platform does not set or save any field value on your behalf. - Use this when the computation needs to query related records, or when the result must be written to a different entity (like rolling up child totals to a parent).
- The
modelNameincomputedFieldTriggerConfigcan be any model - it does not need to be the model that owns the computed field.
Shared base interface
Both interfaces extend IEntityComputedFieldProvider, which requires two methods:
export interface IEntityComputedFieldProvider {
name(): string; // Must exactly match computedFieldValueProvider in the field metadata
help(): string; // Human-readable description of the provider and expected context shape
}How It Works
ComputedEntityFieldSubscriberlistens toinsert,update, anddeleteevents across all entities.- On a before event -
preComputeValue()runs synchronously. The returned value is set on the entity and persisted in the same transaction. - On an after event -
postComputeAndSaveValue()is queued as a background job and runs asynchronously. TheComputedFieldEvaluationSubscriberhandles persistence once the job completes.
Built-in Providers
SolidX ships with several providers for common patterns - generating IDs, concatenating fields, and no-op placeholders. See Built-in Computation Providers for the full reference with configuration examples and sample outputs.

