import { CallExpressionShape, Plugin } from '@servicenow/sdk-build-core' import { NowIdShape } from '../now-id-plugin' const SYS_RETRY_POLICY = 'sys_retry_policy' const DEFAULT_CONNECTION_TYPE = 'http_retry_conditions' const DEFAULT_RETRY_STRATEGY = 'fixed_time_interval' const MAX_ELAPSED_TIME_SECONDS = 86400 const DEFAULT_RESTRICT_TO = 'http_method,status_code,error,response_body,response_headers' export const RetryPolicyPlugin = Plugin.create({ name: 'RetryPolicyPlugin', records: { [SYS_RETRY_POLICY]: { async toShape(record) { const restrictToRaw = record.get('restrict_to')?.ifString()?.getValue() const restrictToArray = restrictToRaw === undefined || restrictToRaw === DEFAULT_RESTRICT_TO ? undefined : restrictToRaw === '' ? [] : restrictToRaw .split(',') .map((s) => s.trim()) .filter(Boolean) return { success: true, value: new CallExpressionShape({ source: record, callee: 'RetryPolicy', args: [ record.transform(({ $ }) => ({ $id: $.val(NowIdShape.from(record)), name: $.def(''), connectionType: $.from('connection_type').def(DEFAULT_CONNECTION_TYPE), retryStrategy: $.from('retry_strategy').def(DEFAULT_RETRY_STRATEGY), count: $.map((v) => v.ifString()?.ifNotEmpty()?.toNumber()), interval: $.map((v) => v.ifString()?.ifNotEmpty()?.toNumber()), maxElapsedTime: $.from('max_elapsed_time').map((v) => v.ifString()?.ifNotEmpty()?.toNumber() ), condition: $.def(''), restrictTo: $.val(restrictToArray), protectionPolicy: $.from('sys_policy').def(''), })), ], }), } }, }, }, shapes: [ { shape: CallExpressionShape, fileTypes: ['fluent'], async toRecord(callExpression, { factory, diagnostics }) { if (callExpression.getCallee() !== 'RetryPolicy') { return { success: false } } const arg = callExpression.getArgument(0).asObject() // Runtime validation for maxElapsedTime upper bound (TypeScript cannot enforce numeric ranges) const maxElapsedTimeShape = arg.get('maxElapsedTime') const maxElapsedTime = maxElapsedTimeShape.ifNumber()?.getValue() if (maxElapsedTime !== undefined && maxElapsedTime > MAX_ELAPSED_TIME_SECONDS) { diagnostics.error( maxElapsedTimeShape, `maxElapsedTime must not exceed ${MAX_ELAPSED_TIME_SECONDS} seconds (24 hours). Received: ${maxElapsedTime}.` ) } const countShape = arg.get('count') const count = countShape.ifNumber()?.getValue() if (count !== undefined && !Number.isInteger(count)) { diagnostics.error(countShape, `count must be an integer. Received: ${count}.`) } const intervalShape = arg.get('interval') const interval = intervalShape.ifNumber()?.getValue() if (interval !== undefined && !Number.isInteger(interval)) { diagnostics.error(intervalShape, `interval must be an integer. Received: ${interval}.`) } // Convert restrictTo string[] → comma-separated string for the DB const restrictToShape = arg.get('restrictTo') const restrictToArrayShape = restrictToShape.ifArray() const restrictToCsv = restrictToArrayShape ? restrictToArrayShape .getElements() .map((el) => el.ifString()?.getValue() ?? '') .filter(Boolean) .join(',') : DEFAULT_RESTRICT_TO // Validate condition only references fields in restrictTo const allowedFields = (restrictToCsv || DEFAULT_RESTRICT_TO).split(',') const conditionShape = arg.get('condition') const conditionValue = conditionShape.ifString()?.ifNotEmpty()?.getValue() if (conditionValue) { for (const part of conditionValue.split('^')) { const fieldPart = part.startsWith('OR') ? part.slice(2) : part if (!allowedFields.some((field) => fieldPart.startsWith(field))) { const fieldName = fieldPart.match(/^[a-z_]+/)?.[0] ?? fieldPart diagnostics.error( conditionShape, `Condition references field '${fieldName}' which is not in restrictTo. Allowed fields: ${allowedFields.join(', ')}.` ) break } } } const record = await factory.createRecord({ source: callExpression, table: SYS_RETRY_POLICY, explicitId: arg.get('$id'), properties: arg.transform(({ $ }) => ({ name: $.def(''), connection_type: $.from('connectionType').def(DEFAULT_CONNECTION_TYPE), retry_strategy: $.from('retryStrategy').def(DEFAULT_RETRY_STRATEGY), count: $, interval: $, max_elapsed_time: $.from('maxElapsedTime'), condition: $.def(''), restrict_to: $.val(restrictToCsv), sys_policy: $.from('protectionPolicy').def(''), })), }) return { success: true, value: record } }, }, ], })