import { CallExpressionShape } from '@servicenow/sdk-build-core' import type { Diagnostics, ObjectShape } from '@servicenow/sdk-build-core' /** Centralized so every dependentQuestion parent-type rejection site shares one message. */ export function invalidDependentQuestionParentTypeMessage(variableName: string): string { return ( `'dependentQuestion' references '${variableName}' which is not a valid parent type. ` + `Only ReferenceVariable and RequestedForVariable are supported.` ) } /** * Validates that at most one of executionPlan, flow, or workflow is defined. * These fields are mutually exclusive in the FulfillmentProcess type. * Returns true if valid, false if a diagnostic was emitted. */ export function validateFulfillmentProcessExclusivity(arg: ObjectShape, diagnostics: Diagnostics): boolean { const fields = ['executionPlan', 'flow', 'workflow'] as const const defined = fields.filter((f) => { const val = arg.get(f) return val.isDefined() && (!!val.ifRecordId() || !val.ifString()?.isEmpty()) }) if (defined.length > 1) { diagnostics.error( arg, `Only one of executionPlan, flow, or workflow can be specified. Found: ${defined.join(', ')}. Remove the extra fields so that only one fulfillment method is configured.` ) return false } return true } /** * Validates that categories are only provided when catalogs are also provided. * Returns true if valid, false if a diagnostic was emitted. */ export function validateCategoriesRequireCatalogs(arg: ObjectShape, diagnostics: Diagnostics): boolean { const hasCategories = arg.get('categories').isDefined() && (arg.get('categories').ifArray()?.getElements()?.length ?? 0) > 0 const hasCatalogs = arg.get('catalogs').isDefined() && (arg.get('catalogs').ifArray()?.getElements()?.length ?? 0) > 0 if (hasCategories && !hasCatalogs) { diagnostics.error( arg.get('categories'), `'categories' requires 'catalogs' to also be specified. Categories can only be selected after the Catalogs field is populated.` ) return false } return true } /** * Validates that catalogItem and variableSet are not both provided. * When appliesTo is 'set', variableSet should be provided; when 'item', catalogItem should be provided. * Returns true if valid, false if a diagnostic was emitted. */ export function validateCatalogItemVariableSetExclusivity( arg: ObjectShape, diagnostics: Diagnostics, context: string ): boolean { const hasCatalogItem = arg.get('catalogItem').isDefined() && !arg.get('catalogItem').ifString()?.isEmpty() const hasVariableSet = arg.get('variableSet').isDefined() && !arg.get('variableSet').ifString()?.isEmpty() if (hasCatalogItem && hasVariableSet) { diagnostics.error( arg, `${context}: 'catalogItem' and 'variableSet' are mutually exclusive. Specify only one. When appliesTo is 'set', use variableSet; when 'item', use catalogItem.` ) return false } const appliesTo = arg.get('appliesTo')?.ifString()?.getValue() if (appliesTo === 'set' && hasCatalogItem) { diagnostics.error( arg.get('catalogItem'), `${context}: 'catalogItem' should not be specified when appliesTo is 'set'. Use 'variableSet' instead.` ) return false } if (appliesTo === 'item' && hasVariableSet) { diagnostics.error( arg.get('variableSet'), `${context}: 'variableSet' should not be specified when appliesTo is 'item'. Use 'catalogItem' instead.` ) return false } return true } /** * Validates that variableMessage is provided when variableMessageType is set in a UI policy action. * Returns true if valid, false if a diagnostic was emitted. */ export function validateUiPolicyActionMessage(actionObj: ObjectShape, diagnostics: Diagnostics): boolean { const hasMessageType = actionObj.get('variableMessageType').isDefined() && !actionObj.get('variableMessageType').ifString()?.isEmpty() const hasMessage = actionObj.get('variableMessage').isDefined() && !actionObj.get('variableMessage').ifString()?.isEmpty() if (hasMessageType && !hasMessage) { diagnostics.error( actionObj.get('variableMessageType'), `'variableMessage' is required when 'variableMessageType' is specified. Provide a message to display on the field.` ) return false } if (hasMessage && !hasMessageType) { diagnostics.error( actionObj.get('variableMessage'), `'variableMessageType' is required when 'variableMessage' is specified. Provide a message type (info, warning, or error).` ) return false } return true } /** * Validates that 'field' is provided when 'mapToField' is true in a variable config. * Returns true if valid, false if a diagnostic was emitted. */ export function validateMapToFieldRequiresField(config: ObjectShape, diagnostics: Diagnostics): boolean { const mapToField = config.get('mapToField') const isMapToFieldTrue = mapToField.isDefined() && mapToField.ifBoolean()?.getValue() === true const hasField = config.get('field').isDefined() && !config.get('field').ifString()?.isEmpty() if (isMapToFieldTrue && !hasField) { diagnostics.error(mapToField, `'field' is required when 'mapToField' is true. Specify the field to map to.`) return false } return true } /** * Validates that mandatory is not true when hidden or readOnly is true, and vice versa. * Returns true if valid, false if a diagnostic was emitted. */ export function validateMandatoryReadOnlyHidden(config: ObjectShape, diagnostics: Diagnostics): boolean { const isMandatory = config.get('mandatory').isDefined() && config.get('mandatory').ifBoolean()?.getValue() === true const isReadOnly = config.get('readOnly').isDefined() && config.get('readOnly').ifBoolean()?.getValue() === true const isHidden = config.get('hidden').isDefined() && config.get('hidden').ifBoolean()?.getValue() === true if (isMandatory && isReadOnly) { diagnostics.error( config.get('mandatory'), `'mandatory' cannot be true when 'readOnly' is true. A field cannot be both required and read-only.` ) return false } if (isMandatory && isHidden) { diagnostics.error( config.get('mandatory'), `'mandatory' cannot be true when 'hidden' is true. A field cannot be both required and hidden.` ) return false } return true } /** * Validates that selectionRequired is not true when hidden or readOnly is true, and vice versa. * This is specific to Checkbox variables which use selectionRequired instead of mandatory. * Returns true if valid, false if a diagnostic was emitted. */ export function validateSelectionRequiredReadOnlyHidden(config: ObjectShape, diagnostics: Diagnostics): boolean { const isSelectionRequired = config.get('selectionRequired').isDefined() && config.get('selectionRequired').ifBoolean()?.getValue() === true const isReadOnly = config.get('readOnly').isDefined() && config.get('readOnly').ifBoolean()?.getValue() === true const isHidden = config.get('hidden').isDefined() && config.get('hidden').ifBoolean()?.getValue() === true if (isSelectionRequired && isReadOnly) { diagnostics.error( config.get('selectionRequired'), `'selectionRequired' cannot be true when 'readOnly' is true. A checkbox cannot be both required and read-only.` ) return false } if (isSelectionRequired && isHidden) { diagnostics.error( config.get('selectionRequired'), `'selectionRequired' cannot be true when 'hidden' is true. A checkbox cannot be both required and hidden.` ) return false } return true } /** * Validates lookup variable configuration: table-based and choice-based properties are mutually exclusive, * and price fields are not allowed when uniqueValuesOnly is true. * Returns true if valid, false if a diagnostic was emitted. */ export function validateLookupSourceExclusivity(config: ObjectShape, diagnostics: Diagnostics): boolean { const lookupSource = config.get('lookupSource') const lookupSourceValue = lookupSource?.ifString()?.getValue() const hasLookupSource = lookupSource.isDefined() && !lookupSource.ifString()?.isEmpty() const hasLookupFromTable = config.get('lookupFromTable').isDefined() && !config.get('lookupFromTable').ifString()?.isEmpty() const hasLookupValueField = config.get('lookupValueField').isDefined() && !config.get('lookupValueField').ifString()?.isEmpty() const isChoicesSource = hasLookupSource && lookupSourceValue === 'choices' if (isChoicesSource) { if (hasLookupFromTable) { diagnostics.error( config.get('lookupFromTable'), `'lookupFromTable' should not be specified when 'lookupSource' is 'choices'.` ) return false } if (hasLookupValueField) { diagnostics.error( config.get('lookupValueField'), `'lookupValueField' should not be specified when 'lookupSource' is 'choices'.` ) return false } return true } const requiredNode = lookupSource.isDefined() ? lookupSource : config if (!hasLookupFromTable) { diagnostics.error(requiredNode, `'lookupFromTable' is required when 'lookupSource' is not 'choices'.`) return false } if (!hasLookupValueField) { diagnostics.error(requiredNode, `'lookupValueField' is required when 'lookupSource' is not 'choices'.`) return false } return true } /** * Validates that dependentQuestion, when provided as a bare object reference (e.g. a variable const * passed directly), is rejected — only string variable names and dot-walk into a variable set's * `variables` are supported authoring forms. Resolution/rejection of those two forms happens in * resolveDependentQuestion (shape-to-record.ts); this only guards against the unsupported object-ref form. * Returns true if valid, false if a diagnostic error was emitted. */ export function validateDependentQuestionType( config: ObjectShape, variableName: string, diagnostics: Diagnostics ): boolean { const dqShape = config.get('dependentQuestion', false) if (dqShape.isUndefined()) { return true } if (dqShape.isString()) { return true } const dqCallExpr = dqShape.if(CallExpressionShape) if (dqCallExpr) { diagnostics.error( dqShape, `'dependentQuestion' must be a string variable name (e.g. 'requestedFor') or a dot-walk into a ` + `variable set's variables (e.g. employeeSet.variables.requestedFor) referencing '${variableName}'. ` + `A direct reference to a variable const is not supported.` ) return false } return true } /** * Validates reference qualifier fields are mutually exclusive and consistent with useReferenceQualifier. * referenceQualCondition, dynamicRefQual, and referenceQual are mutually exclusive. * Returns true if valid, false if a diagnostic was emitted. */ export function validateReferenceQualifierExclusivity( config: ObjectShape, diagnostics: Diagnostics, context: string ): boolean { const hasRefQualCondition = config.get('referenceQualCondition').isDefined() && !config.get('referenceQualCondition').ifString()?.isEmpty() const hasDynamicRefQual = config.get('dynamicRefQual').isDefined() && !config.get('dynamicRefQual').ifString()?.isEmpty() const hasRefQual = config.get('referenceQual').isDefined() && !config.get('referenceQual').ifString()?.isEmpty() const useRefQualShape = config.get('useReferenceQualifier') const useRefQual = useRefQualShape?.ifString()?.getValue() if ((useRefQual === 'simple' || useRefQualShape.ifString()?.isEmpty()) && !hasRefQualCondition) { diagnostics.error( useRefQualShape, `${context}: 'referenceQualCondition' is required when 'useReferenceQualifier' is 'simple'.` ) return false } if (useRefQual === 'dynamic' && !hasDynamicRefQual) { diagnostics.error( useRefQualShape, `${context}: 'dynamicRefQual' is required when 'useReferenceQualifier' is 'dynamic'.` ) return false } if (useRefQual === 'advanced' && !hasRefQual) { diagnostics.error( useRefQualShape, `${context}: 'referenceQual' is required when 'useReferenceQualifier' is 'advanced'.` ) return false } return true }