import { generateId, type ValidationLevel, type ValidationState } from './_helpers'; import type { CustomObject } from './custom-object'; import { getExistingManifestContext, type ExistingManifestContextOptions } from './existing-manifest-context'; import type { Field } from './field'; import type { ManifestComponentDeletion } from './manifest-builder'; import { requireStringComponentField } from './_existing-component-fields'; export interface ValidationDeletionIdentifier { ruleId: string; modelRqlName: string; } /** * Initialization properties for {@link Validation}. */ export interface ValidationProps { /** * Display name of this validation rule shown in the UI. * * Required. */ displayName: string; /** * RQL formula that returns `true` when the record is **invalid**. * * Required. Note the violation semantics: a truthy result triggers the error, * not a passing check. Example: `'(end_time__c <= start_time__c)'` fires when * end is not after start. */ rqlFormula: string; /** * User-facing error message shown when the validation fires. * * Required. Pass an empty string for no message. */ errorMessage: string; /** * Whether this rule is active or inactive. * * Required. One of `'ACTIVE'` | `'INACTIVE'`. */ state: ValidationState; /** * Whether the error is shown at the record level or on a specific field. * * Required. One of `'RECORD'` | `'FIELD'`. When `'FIELD'`, also set `fieldRef`. */ level: ValidationLevel; /** * Internal description of this rule (for developer reference only). * * Required. Pass an empty string if unused. */ ruleDescription: string; /** * Stable identifier for this rule. Pass a human-readable slug * (e.g. `'gym_session_end_after_start'`) to keep it stable across regenerations. * * Optional. Auto-generated when omitted. @default generateId('rule') */ ruleId?: string; /** * The field the error is pinned to. Typically paired with `level: 'FIELD'`. * * Optional. */ fieldRef?: Field; } export interface ValidationLoadFromExistingOptions extends ExistingManifestContextOptions { customObjectApiName?: string; } /** * Defines a validation rule and registers it with the manifest. * * A validation rule evaluates an RQL formula on save and blocks the operation * when the formula returns `true`. You would normally define validations after * the fields they reference. * * **Violation semantics:** the formula encodes the *invalid* condition, not the * valid one. `'(end_time__c <= start_time__c)'` fires when end is before or equal * to start — i.e., it is truthy when the record is wrong. * * @example * ```ts * new Validation(sessionObj, { * ruleId: 'gym_session_end_after_start', * displayName: 'End time must be after start', * rqlFormula: '(end_time__c <= start_time__c)', * errorMessage: 'End time must be after the start time.', * ruleDescription: 'Prevents zero-length or inverted intervals', * state: 'ACTIVE', * level: 'FIELD', * fieldRef: sessionEndField, * }); * ``` */ export class Validation { static readonly componentType = 'CUSTOM_OBJECT_VALIDATION_RULE' as const; static toDeletionIdentifier(identifier: ValidationDeletionIdentifier): ManifestComponentDeletion { return { type: Validation.componentType, rule_id: identifier.ruleId, model_rql_name: identifier.modelRqlName, }; } private readonly _ruleId: string; private readonly _customObjectApiName: string; private readonly _displayName: string; private readonly _rqlFormula: string; private readonly _errorMessage: string; private readonly _state: ValidationState; private readonly _level: ValidationLevel; private readonly _ruleDescription: string; private readonly _fieldRef: Field | undefined; /** * @param customObject - The custom object this rule applies to. * @param props - Initialization properties. */ constructor(customObject: CustomObject, props: ValidationProps) { this._ruleId = props.ruleId ?? generateId('rule'); this._customObjectApiName = customObject.getApiName(); this._displayName = props.displayName; this._rqlFormula = props.rqlFormula; this._errorMessage = props.errorMessage; this._state = props.state; this._level = props.level; this._ruleDescription = props.ruleDescription; this._fieldRef = props.fieldRef; customObject._register(this); } /** * Loads this validation rule from the active existing-manifest JSON context. */ static loadFromExisting(ruleId: string, options: ValidationLoadFromExistingOptions = {}): Validation { return getExistingManifestContext(options).loadValidation(ruleId, options); } /** @internal Hydrates a validation rule from existing manifest wire JSON. */ static _fromExistingComponent( customObject: CustomObject, component: Record, resolveField: (apiName: string) => Field, ): Validation { const props: ValidationProps = { ruleId: requireStringComponentField(component, 'rule_id', Validation.componentType), displayName: requireStringComponentField(component, 'display_name', Validation.componentType), rqlFormula: requireStringComponentField(component, 'rql_formula', Validation.componentType), errorMessage: requireStringComponentField(component, 'custom_error_message', Validation.componentType, { allowEmpty: true, }), state: requireStringComponentField(component, 'state', Validation.componentType) as ValidationState, level: requireStringComponentField(component, 'level', Validation.componentType) as ValidationLevel, ruleDescription: requireStringComponentField(component, 'rule_description', Validation.componentType, { allowEmpty: true, }), }; if (component['field_rql_name'] != null) props.fieldRef = resolveField(component['field_rql_name']); return new Validation(customObject, props); } /** * Returns the stable identifier for this validation rule (e.g. `'gym_session_end_after_start'`). */ getRuleId(): string { return this._ruleId; } /** * Returns the api_name of the custom object this validation rule applies to. */ getCustomObjectApiName(): string { return this._customObjectApiName; } /** * Serializes this validation rule to the wire format consumed by the manifest install endpoint. * * @returns A plain object with `type: 'CUSTOM_OBJECT_VALIDATION_RULE'`. * `field_rql_name` is omitted when `fieldRef` was not provided. */ toDict(): Record { const component: Record = { type: Validation.componentType, model_rql_name: this._customObjectApiName, rule_id: this._ruleId, state: this._state, level: this._level, display_name: this._displayName, rule_description: this._ruleDescription, custom_error_message: this._errorMessage, rql_formula: this._rqlFormula, }; if (this._fieldRef != null) component['field_rql_name'] = this._fieldRef.getApiName(); return component; } }