import { ts, type Shape, type ObjectShape, type Diagnostics, type Compiler, type Record as FluentRecord, } from '@servicenow/sdk-build-core' export function toReference(shape: Shape) { return shape.ifRecord()?.getId() ?? shape.ifString()?.getValue() ?? '' } /** * Reverses an object, swapping keys and values. **reverseObject** should only be used * where the key/values are all known ahead of time and the values are unique. * @example * reverseObject({ a: 'foo', b: 'bar' }) // { foo: 'a', bar: 'b' } */ export function reverseObject(obj: Record) { const reversed = {} as Record for (const key in obj) { const value = obj[key] if (typeof value === 'string') { reversed[value] = key } } return reversed } export function noThrow(action: () => T) { try { return action() } catch (error) { return error as Error } } /** * Regex taken from: https://github.com/oozcitak/xmlbuilder-js/blob/b20136cd1591d0f17ab2f184053c7150150428b2/src/XMLStringifier.coffee#L119C15-L119C127 */ export const INVALID_XML_CHARACTERS = // biome-ignore lint/suspicious/noControlCharactersInRegex: This is intentional /[\0-\x08\x0B\f\x0E-\x1F\uFFFE\uFFFF]|[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?:[^\uD800-\uDBFF]|^)[\uDC00-\uDFFF]/g export function applyPathMappings(path: string, mappings: Record) { if (!mappings) { return path } // TODO: Naive implementation for now that just supports simple patterns. We can explore supporting more complex patterns later. for (const [source, target] of Object.entries(mappings)) { const match = path.match(`${source.replace(/\./, '\\.').replace(/\*+/, '(.*)')}$`)?.[1] if (match !== undefined) { return target.replace(/\*+/, match) } } return path } export function getCallExpressionName(node: ts.CallExpression) { const expression = node.getExpression() if (ts.Node.isIdentifier(expression) || ts.Node.isPropertyAccessExpression(expression)) { return expression.getText() } throw `CallExpression does not have a name: ${expression.getText()}` } export function generateDeprecatedDiagnostics(object: ObjectShape, diagnostics: Diagnostics): void { Object.keys(object.getAliasedKeys()).forEach((key) => { const alias = object.findAliasUsed(key) if (alias) { diagnostics.hint(object.get(key), `The property '${alias}' is deprecated, use '${key}' instead.`) } }) } /** * Helper function to check if a call expression is a require statement */ function isRequire(callExpression: ts.CallExpression): boolean { const expression = callExpression.getExpression() return ts.Node.isIdentifier(expression) && expression.getText() === 'require' } /** * Validates that client-side scripts (Client Scripts and UI Policy scripts) don't contain * import or require statements. These scripts run in the browser and cannot use module imports. * * @param script - The script content to validate * @param compiler - The TypeScript compiler instance * @returns true if the script is valid (no imports/requires), false otherwise */ export function validateClientSideScript(script: string, compiler: Compiler): boolean { const source = compiler.createSourceFile('tmp-script.ts', script) const hasImports = source.getDescendantsOfKind(ts.SyntaxKind.ImportDeclaration).length > 0 const hasRequires = source.getDescendantsOfKind(ts.SyntaxKind.CallExpression).some(isRequire) const isValid = !(hasImports || hasRequires) compiler.removeSourceFile(source) return isValid } /** * Validates a script field, emitting diagnostics for unresolved module references or * inline function expressions (arrow functions or function expressions are not valid * for server-side script fields — a server module import or string literal is required). */ export function validateServerScriptField(shape: Shape, diagnostics: Diagnostics, serverModulesDir: string): void { if (shape.isUnresolved()) { diagnostics.error( shape.getOriginalNode(), `Unable to resolve the reference, ensure the imported module is within the ${serverModulesDir} directory.` ) } else if (shape.ifDefined()) { const kind = shape.getOriginalNode().getKind() if (kind === ts.SyntaxKind.ArrowFunction || kind === ts.SyntaxKind.FunctionExpression) { diagnostics.error( shape, `Inline functions are not valid. Use a named function imported from a server module in '${serverModulesDir}', a string literal, or with Now.include().` ) } } } /** * Shows a diagnostic error when a field expects a valid GUID but receives an invalid value. * * @param inputReceived - The shape containing the invalid value * @param fieldName - Name of the field being validated (e.g., 'annotationId', 'formatterRef') * @param tableName - ServiceNow table name for the expected reference (e.g., 'sys_ui_annotation') * @param diagnostics - Diagnostics collection to add the error to * * @example * if (!isGUID(annotationSysId)) { * showGuidFieldDiagnostic(annotationIdField, 'annotationId', 'sys_ui_annotation', diagnostics) * } */ export const showGuidFieldDiagnostic = ( inputReceived: Shape | undefined, fieldName: string, tableName: string, diagnostics: Diagnostics ) => { if (inputReceived) { const receivedValue = inputReceived.getValue() diagnostics.error( inputReceived, `'${fieldName}' must be a valid GUID or a Record<'${tableName}'>. Received: '${receivedValue}'` ) } } /** * Parses a number from a shape record field with fallback to default value. * Returns undefined if the field doesn't exist and no default is provided. */ export function getFieldAsNumber(shape: FluentRecord, fieldName: string, defaultValue?: number): number | undefined { const rawValue = shape.get(fieldName)?.getValue() if (rawValue === undefined || rawValue === null || rawValue === '') { return defaultValue } const parsed = Number(rawValue) return Number.isNaN(parsed) ? defaultValue : parsed }