vsix 29

import * as fs from 'fs/promises' import * as path from 'path' import * as vscode from 'vscode' type CryptoTarget = { tableName: string plainColumn: string cryptoType: string encColumn: string hashColumn: string javaProperty: string encryptTypeHandler: string hashTypeHandler: string } type ConvertRule = { before: string tobe: string tableMustExist: boolean } type ConvertPlaceholder = { key: string htype?: string camelHtype?: string } type ConvertCaptureResult = { captured: Map targets: CryptoTarget[] } type ResultMapEntry = { kind: 'id' | 'result' column: string property: string typeHandler?: string } type ResultMapDefinition = { id: string type: string entries: ResultMapEntry[] } type MapperItem = { tag: 'insert' | 'update' | 'select' | 'delete' id: string attrs: string text: string } type MapperItemRange = { start: number end: number text: string } const LEGACY_DECRYPT_TARGET_COMMENT_PATTERN = String.raw`/\*\s*복호화 대상\s*\*/` const DECRYPT_TARGET_COMMENT_PATTERN = String.raw`/\* \[KMS-CIPHER\]\s*수정일자:\s*\d{4}-\d{2}-\d{2}\s*수정자\s*:\s*[^*]*\*/` const CRYPTO_TARGET_CHECK_COMMENT = '/* @@@ 암호화 대상 확인 필요 @@@ */' const CONVERT_XML_PATH = 'c:/xml/convert.xml' export function activate(context: vscode.ExtensionContext) { context.subscriptions.push( vscode.commands.registerCommand('cryptoMapper.copyTypeHandlerRecommendation', async () => { try { await applySelectedSelectColumnTypeHandlerRecommendation() } catch (error) { const message = error instanceof Error ? error.message : String(error) vscode.window.showErrorMessage(`Crypto Mapper 추천 생성 실패: ${message}`) } }), vscode.commands.registerCommand('cryptoMapper.copyFullFileTypeHandlerRecommendation', async () => { try { await copyFullFileTypeHandlerRecommendation() } catch (error) { const message = error instanceof Error ? error.message : String(error) vscode.window.showErrorMessage(`Crypto Mapper full XML recommendation failed: ${message}`) } }), vscode.commands.registerCommand('cryptoMapper.markCryptoTargetColumns', async () => { try { await markCryptoTargetColumnsInFullFile() } catch (error) { const message = error instanceof Error ? error.message : String(error) vscode.window.showErrorMessage(`Crypto Mapper target mark failed: ${message}`) } }), vscode.commands.registerCommand('cryptoMapper.markCryptoTargetColumnsInCurrentItem', async () => { try { await markCryptoTargetColumnsInCurrentItem() } catch (error) { const message = error instanceof Error ? error.message : String(error) vscode.window.showErrorMessage(`Crypto Mapper current item target mark failed: ${message}`) } }), vscode.commands.registerCommand('cryptoMapper.applyTypeHandlerRecommendationInCurrentItem', async () => { try { await applyFocusedMapperItemTypeHandlerRecommendation() } catch (error) { const message = error instanceof Error ? error.message : String(error) vscode.window.showErrorMessage(`Crypto Mapper current item conversion failed: ${message}`) } }), vscode.commands.registerCommand('cryptoMapper.applyConvertXmlRulesToSelection', async () => { try { await applyConvertXmlRulesToSelection() } catch (error) { const message = error instanceof Error ? error.message : String(error) vscode.window.showErrorMessage(`Crypto Mapper convert.xml replace failed: ${message}`) } }), ) } export function deactivate() { // no-op } async function applySelectedSelectColumnTypeHandlerRecommendation() { const editor = vscode.window.activeTextEditor if (!editor) throw new Error('No active editor.') if (editor.selection.isEmpty) throw new Error('Ctrl+Shift+4 needs a selected range inside a mapper item.') const originalXml = editor.document.getText() const selectionStart = editor.document.offsetAt(editor.selection.start) const selectionEnd = editor.document.offsetAt(editor.selection.end) const focusedItem = findMapperItemRangeAtOffset(originalXml, selectionStart) if (!focusedItem || selectionEnd > focusedItem.end) { throw new Error('Select a range inside one mapper item, then press Ctrl+Shift+4.') } const mapperItem = parseSelectedMapperItem(focusedItem.text) const workspaceRoot = getWorkspaceRoot(editor.document.uri) const dbIniPath = await findFirstFile(workspaceRoot, 'db.ini') if (!dbIniPath) throw new Error(`db.ini not found under workspace root: ${workspaceRoot}`) const cryptoTargetsPath = await readCryptoTargetsPath(dbIniPath, workspaceRoot) const markerUserId = await readDbIniUserId(dbIniPath) const targets = await readCryptoTargets(cryptoTargetsPath) const date = formatLocalDate(new Date()) const relativeSelectionStart = selectionStart - focusedItem.start const relativeSelectionEnd = selectionEnd - focusedItem.start if (mapperItem.tag !== 'select' || !isRangeInsideSelectColumnRange(focusedItem.text, relativeSelectionStart, relativeSelectionEnd)) { throw new Error('Ctrl+Shift+4 supports only a selected range inside the SELECT column list. Use Ctrl+Shift+1 for the whole mapper item.') } const selectedText = focusedItem.text.slice(relativeSelectionStart, relativeSelectionEnd) const selectedRewrite = rewriteSelectedSelectColumns(focusedItem.text, selectedText, targets, date, markerUserId) if (selectedRewrite.changedTargets.length === 0) { if (selectedRewrite.text !== selectedText) { await editor.edit((editBuilder) => { editBuilder.replace(editor.selection, selectedRewrite.text) }) vscode.window.showInformationMessage('Crypto Mapper selected-column conversion: target check comments removed.') return } vscode.window.showInformationMessage('Crypto Mapper selected-column conversion: no matching crypto target column found in selection.') return } const itemWithSelectionRewrite = [ focusedItem.text.slice(0, relativeSelectionStart), selectedRewrite.text, focusedItem.text.slice(relativeSelectionEnd), ].join('') let desiredResultMap = desiredResultMapDefinitionForTargets(mapperItem, focusedItem.text, originalXml, selectedRewrite.changedTargets) const existingResultMap = findExistingResultMap(originalXml, desiredResultMap) if (!existingResultMap) { desiredResultMap = { ...desiredResultMap, id: uniqueResultMapId(desiredResultMap.id, originalXml), } } const resultMapId = existingResultMap?.id || desiredResultMap.id const updatedItem = applySelectResultMap(itemWithSelectionRewrite, resultMapId) const insertOffset = lineStartOffset(originalXml, focusedItem.start) const itemRange = new vscode.Range( editor.document.positionAt(insertOffset), editor.document.positionAt(focusedItem.end), ) const lineIndent = originalXml.slice(insertOffset, focusedItem.start) const resultMapInsertText = existingResultMap ? '' : `${indentResultMapSnippet(resultMapSnippet(desiredResultMap), lineIndent)}\n\n` await editor.edit((editBuilder) => { editBuilder.replace(itemRange, `${resultMapInsertText}${lineIndent}${updatedItem}`) }) vscode.window.showInformationMessage( `Crypto Mapper selected-column conversion completed. changed columns: ${selectedRewrite.changedTargets.length}, resultMap: ${resultMapId}`, ) } async function applyFocusedMapperItemTypeHandlerRecommendation() { const editor = vscode.window.activeTextEditor if (!editor) throw new Error('No active editor.') const originalXml = editor.document.getText() const focusedItem = findMapperItemRangeAtOffset(originalXml, editor.document.offsetAt(editor.selection.active)) if (!focusedItem) { vscode.window.showInformationMessage('Crypto Mapper current item conversion: cursor is not inside a mapper item.') return } const mapperItem = parseSelectedMapperItem(focusedItem.text) const workspaceRoot = getWorkspaceRoot(editor.document.uri) const dbIniPath = await findFirstFile(workspaceRoot, 'db.ini') if (!dbIniPath) throw new Error(`db.ini not found under workspace root: ${workspaceRoot}`) const cryptoTargetsPath = await readCryptoTargetsPath(dbIniPath, workspaceRoot) const markerUserId = await readDbIniUserId(dbIniPath) const targets = await readCryptoTargets(cryptoTargetsPath) const date = formatLocalDate(new Date()) await applyCurrentMapperItemTypeHandlerRecommendation( editor, originalXml, focusedItem, mapperItem, targets, date, markerUserId, ) } async function applyCurrentMapperItemTypeHandlerRecommendation( editor: vscode.TextEditor, originalXml: string, focusedItem: MapperItemRange, mapperItem: MapperItem, targets: CryptoTarget[], date: string, markerUserId: string, ) { const matchedTargets = targets.filter((target) => matchesTarget(focusedItem.text, target, mapperItem.tag)) if (matchedTargets.length === 0) { vscode.window.showInformationMessage('Crypto Mapper current item conversion: no matching crypto target found in current mapper item.') return } const rewrite = rewriteMapperItemForFullFile(mapperItem, focusedItem.text, originalXml, matchedTargets, date, markerUserId) if (rewrite.afterSql === focusedItem.text && rewrite.supportSnippets.length === 0) { vscode.window.showInformationMessage('Crypto Mapper current item conversion: no rewrite point found in current mapper item.') return } const insertOffset = lineStartOffset(originalXml, focusedItem.start) const itemRange = new vscode.Range( editor.document.positionAt(insertOffset), editor.document.positionAt(focusedItem.end), ) const lineIndent = originalXml.slice(insertOffset, focusedItem.start) const resultMapInsertText = rewrite.supportSnippets.length === 0 ? '' : `${rewrite.supportSnippets.map((snippet) => indentResultMapSnippet(snippet, lineIndent)).join('\n\n')}\n\n` await editor.edit((editBuilder) => { editBuilder.replace(itemRange, `${resultMapInsertText}${lineIndent}${rewrite.afterSql}`) }) vscode.window.showInformationMessage( `Crypto Mapper current item conversion completed. changed targets: ${matchedTargets.length}, resultMaps: ${rewrite.supportSnippets.length}`, ) } async function copyTypeHandlerRecommendation() { const editor = vscode.window.activeTextEditor if (!editor) throw new Error('열려 있는 editor가 없습니다.') if (editor.selection.isEmpty) throw new Error('mapper item 전체를 선택한 뒤 Ctrl+Shift+5를 눌러주세요.') const selectedText = editor.document.getText(editor.selection) const mapperItem = parseSelectedMapperItem(selectedText) const workspaceRoot = getWorkspaceRoot(editor.document.uri) const dbIniPath = await findFirstFile(workspaceRoot, 'db.ini') if (!dbIniPath) throw new Error(`프로젝트 루트 아래에서 db.ini를 찾지 못했습니다: ${workspaceRoot}`) const cryptoTargetsPath = await readCryptoTargetsPath(dbIniPath, workspaceRoot) const markerUserId = await readDbIniUserId(dbIniPath) const targets = await readCryptoTargets(cryptoTargetsPath) const matchedTargets = targets.filter((target) => matchesTarget(selectedText, target, mapperItem.tag)) if (matchedTargets.length === 0) { throw new Error('선택 영역에서 crypto-targets.json의 테이블명과 컬럼명이 함께 경계 일치하는 대상을 찾지 못했습니다.') } const date = formatLocalDate(new Date()) const fullDocumentText = editor.document.getText() const recommendation = buildRecommendation(mapperItem, selectedText, fullDocumentText, matchedTargets, date, markerUserId) await vscode.env.clipboard.writeText(recommendation.clipboardText) vscode.window.showInformationMessage( `Crypto Mapper 추천을 클립보드에 복사했습니다. 대상 ${matchedTargets.length}개, ${recommendation.supportSnippets.length}개 보조 snippet.`, ) } async function copyFullFileTypeHandlerRecommendation() { const editor = vscode.window.activeTextEditor if (!editor) throw new Error('No active editor.') const workspaceRoot = getWorkspaceRoot(editor.document.uri) const dbIniPath = await findFirstFile(workspaceRoot, 'db.ini') if (!dbIniPath) throw new Error(`db.ini not found under workspace root: ${workspaceRoot}`) const cryptoTargetsPath = await readCryptoTargetsPath(dbIniPath, workspaceRoot) const markerUserId = await readDbIniUserId(dbIniPath) const targets = await readCryptoTargets(cryptoTargetsPath) const originalXml = editor.document.getText() const date = formatLocalDate(new Date()) const recommendation = buildFullFileRecommendation(originalXml, targets, date, markerUserId) await vscode.env.clipboard.writeText(recommendation.clipboardText) vscode.window.showInformationMessage( `Crypto Mapper full XML recommendation copied. changed mapper items: ${recommendation.changedItemCount}, resultMaps: ${recommendation.supportSnippets.length}`, ) } async function markCryptoTargetColumnsInFullFile() { const editor = vscode.window.activeTextEditor if (!editor) throw new Error('No active editor.') const workspaceRoot = getWorkspaceRoot(editor.document.uri) const dbIniPath = await findFirstFile(workspaceRoot, 'db.ini') if (!dbIniPath) throw new Error(`db.ini not found under workspace root: ${workspaceRoot}`) const cryptoTargetsPath = await readCryptoTargetsPath(dbIniPath, workspaceRoot) const targets = await readCryptoTargets(cryptoTargetsPath) const originalXml = editor.document.getText() const result = markCryptoTargetColumns(originalXml, targets) if (result.changedCount === 0) { vscode.window.showInformationMessage('Crypto Mapper target mark: no matching mapper item found.') return } const fullRange = new vscode.Range( editor.document.positionAt(0), editor.document.positionAt(originalXml.length), ) await editor.edit((editBuilder) => { editBuilder.replace(fullRange, result.text) }) vscode.window.showInformationMessage(`Crypto Mapper target mark completed. changed columns: ${result.changedCount}`) } async function markCryptoTargetColumnsInCurrentItem() { const editor = vscode.window.activeTextEditor if (!editor) throw new Error('No active editor.') const originalXml = editor.document.getText() const focusedItem = findMapperItemRangeAtOffset(originalXml, editor.document.offsetAt(editor.selection.active)) if (!focusedItem) { vscode.window.showInformationMessage('Crypto Mapper current item target mark: cursor is not inside a mapper item.') return } const workspaceRoot = getWorkspaceRoot(editor.document.uri) const dbIniPath = await findFirstFile(workspaceRoot, 'db.ini') if (!dbIniPath) throw new Error(`db.ini not found under workspace root: ${workspaceRoot}`) const cryptoTargetsPath = await readCryptoTargetsPath(dbIniPath, workspaceRoot) const targets = await readCryptoTargets(cryptoTargetsPath) const result = markCryptoTargetColumnsInMapperItem(focusedItem.text, targets) if (result.changedCount === 0) { vscode.window.showInformationMessage('Crypto Mapper current item target mark: no matching target found in current mapper item.') return } const itemRange = new vscode.Range( editor.document.positionAt(focusedItem.start), editor.document.positionAt(focusedItem.end), ) await editor.edit((editBuilder) => { editBuilder.replace(itemRange, result.text) }) vscode.window.showInformationMessage(`Crypto Mapper current item target mark completed. changed columns: ${result.changedCount}`) } async function applyConvertXmlRulesToSelection() { const editor = vscode.window.activeTextEditor if (!editor) throw new Error('No active editor.') if (editor.selection.isEmpty) throw new Error('Ctrl+Shift+4 convert.xml replace needs a selected range.') const selectedText = editor.document.getText(editor.selection) if (selectedText.includes('작업완료')) { vscode.window.showInformationMessage('Crypto Mapper convert.xml replace skipped: selection already contains 작업완료.') return } const workspaceRoot = getWorkspaceRoot(editor.document.uri) const dbIniPath = await findFirstFile(workspaceRoot, 'db.ini') if (!dbIniPath) throw new Error(`db.ini not found under workspace root: ${workspaceRoot}`) const cryptoTargetsPath = await readCryptoTargetsPath(dbIniPath, workspaceRoot) const markerUserId = await readDbIniUserId(dbIniPath) const targets = await readCryptoTargets(cryptoTargetsPath) const rules = await readConvertRules(CONVERT_XML_PATH) const result = applyConvertRules(selectedText, rules, targets, new Date(), markerUserId) if (result.changedCount === 0) { vscode.window.showInformationMessage('Crypto Mapper convert.xml replace: no matching text found in selection.') return } await editor.edit((editBuilder) => { editBuilder.replace(editor.selection, result.text) }) vscode.window.showInformationMessage(`Crypto Mapper convert.xml replace completed. changed: ${result.changedCount}`) } function getWorkspaceRoot(uri: vscode.Uri) { const folder = vscode.workspace.getWorkspaceFolder(uri) ?? vscode.workspace.workspaceFolders?.[0] if (!folder) throw new Error('VS Code workspace folder가 없습니다.') return folder.uri.fsPath } async function findFirstFile(root: string, fileName: string): Promise { const queue = [root] const ignored = new Set(['.git', 'node_modules', 'dist', 'out', '.next', 'target', 'build']) while (queue.length > 0) { const current = queue.shift()! let entries: Array<{ name: string; isDirectory: () => boolean; isFile: () => boolean }> try { entries = await fs.readdir(current, { withFileTypes: true }) } catch { continue } const file = entries.find((entry) => entry.isFile() && entry.name.toLowerCase() === fileName.toLowerCase()) if (file) return path.join(current, file.name) for (const entry of entries) { if (entry.isDirectory() && !ignored.has(entry.name)) { queue.push(path.join(current, entry.name)) } } } return undefined } async function readCryptoTargetsPath(dbIniPath: string, workspaceRoot: string) { const text = await fs.readFile(dbIniPath, 'utf8') const targetPath = text .split(/\r?\n/) .map((line) => line.trim()) .filter((line) => line && !line.startsWith('#') && !line.startsWith(';')) .map((line) => line.match(/^targetpath\s*=\s*(.+)$/i)?.[1]?.trim()) .find(Boolean) if (!targetPath) throw new Error(`${dbIniPath} 파일에 targetpath 값이 없습니다.`) const unquoted = targetPath.replace(/^['"]|['"]$/g, '') const candidates = path.isAbsolute(unquoted) ? [unquoted] : [path.resolve(path.dirname(dbIniPath), unquoted), path.resolve(workspaceRoot, unquoted)] for (const candidate of candidates) { try { const stat = await fs.stat(candidate) if (stat.isFile()) return candidate if (stat.isDirectory()) return path.join(candidate, 'crypto-targets.json') } catch { // try next } } throw new Error(`targetpath 경로를 찾지 못했습니다: ${unquoted}`) } async function readDbIniUserId(dbIniPath: string) { const text = await fs.readFile(dbIniPath, 'utf8') return text .split(/\r?\n/) .map((line) => line.trim()) .filter((line) => line && !line.startsWith('#') && !line.startsWith(';')) .map((line) => line.match(/^userid\s*=\s*(.+)$/i)?.[1]?.trim()) .find(Boolean) ?.replace(/^['"]|['"]$/g, '') ?? '' } async function readCryptoTargets(filePath: string): Promise { const text = await fs.readFile(filePath, 'utf8') const parsed = JSON.parse(text) as Partial[] if (!Array.isArray(parsed)) throw new Error(`crypto-targets.json 형식이 배열이 아닙니다: ${filePath}`) return parsed .map((target) => ({ tableName: normalizeRequired(target.tableName), plainColumn: normalizeRequired(target.plainColumn), cryptoType: normalizeOptional(target.cryptoType), encColumn: normalizeRequired(target.encColumn), hashColumn: normalizeOptional(target.hashColumn), javaProperty: normalizeOptional(target.javaProperty) || toCamel(normalizeRequired(target.plainColumn)), encryptTypeHandler: normalizeRequired(target.encryptTypeHandler), hashTypeHandler: normalizeOptional(target.hashTypeHandler), })) .filter((target) => target.tableName && target.plainColumn && target.encColumn && target.encryptTypeHandler) } function normalizeRequired(value: unknown) { return String(value ?? '').trim() } async function readConvertRules(filePath: string): Promise { const text = await fs.readFile(filePath, 'utf8') const rules: ConvertRule[] = [] const itemPattern = /]*>([\s\S]*?)<\/item>/gi for (const itemMatch of text.matchAll(itemPattern)) { const itemText = itemMatch[1] const before = tagText(itemText, 'before').trim() const tobe = tagText(itemText, 'tobe').trim() const tableMustExist = equalsIgnoreCase(looseTagText(itemText, 'tablemustexist').trim(), 'Y') if (before && tobe) rules.push({ before, tobe, tableMustExist }) } if (rules.length === 0) throw new Error(`convert.xml has no valid / rules: ${filePath}`) return rules } function tagText(text: string, tagName: string) { const match = text.match(new RegExp(`<${tagName}\\b[^>]*>([\\s\\S]*?)<\\/${tagName}>`, 'i')) return match ? decodeXmlText(match[1]) : '' } function looseTagText(text: string, tagName: string) { const normal = tagText(text, tagName) if (normal) return normal const match = text.match(new RegExp(`<${tagName}\\b[^>]*>\\s*([^<\\r\\n]+)\\s*(?:<${tagName}\\b|$)`, 'i')) return match ? decodeXmlText(match[1]) : '' } function decodeXmlText(text: string) { return text .replace(//g, '$1') .replace(/</g, '<') .replace(/>/g, '>') .replace(/"/g, '"') .replace(/'/g, "'") .replace(/&/g, '&') } function applyConvertRules(text: string, rules: ConvertRule[], targets: CryptoTarget[], date: Date, markerUserId = '') { let next = text let changedCount = 0 for (const rule of rules) { if (next.includes('작업완료')) break const compiled = compileConvertBeforePattern(rule.before, targets) next = next.replace(compiled.pattern, (...args: unknown[]) => { const match = String(args[0]) const captureResult = convertCaptures(args, compiled.placeholders, targets) if (!captureResult) return match if (rule.tableMustExist && !convertTargetsHaveTableInText(captureResult.targets, text)) return match changedCount += 1 return replaceConvertReservedWords(applyConvertTobe(rule.tobe, captureResult.captured), date, markerUserId) }) } return { text: next, changedCount } } function compileConvertBeforePattern(before: string, targets: CryptoTarget[]) { const placeholders: ConvertPlaceholder[] = [] let patternText = '' let lastIndex = 0 const placeholderPattern = /@@\(([^)]+)\)@@/g const placeholderMatches = Array.from(before.matchAll(placeholderPattern)) for (let placeholderIndex = 0; placeholderIndex < placeholderMatches.length; placeholderIndex += 1) { const match = placeholderMatches[placeholderIndex] const token = match[0] const rawKey = match[1] const index = match.index ?? 0 const key = rawKey.trim() const htype = parseConvertPlaceholderHtype(key) const camelHtype = parseConvertPlaceholderCamelHtype(key) const previousFragment = before.slice(lastIndex, index) const nextMatch = placeholderMatches[placeholderIndex + 1] const nextFragment = nextMatch ? before.slice(index + token.length, nextMatch.index ?? index + token.length) : '' const nextHtype = nextMatch ? parseConvertPlaceholderHtype(nextMatch[1].trim()) : undefined const nextCamelHtype = nextMatch ? parseConvertPlaceholderCamelHtype(nextMatch[1].trim()) : undefined patternText += compileConvertRegexFragment(previousFragment) patternText += htype ? convertHtypeColumnPattern(htype, targets, !convertFragmentEndsWithDot(previousFragment)) : camelHtype ? convertCamelHtypePattern(camelHtype, targets) : convertGenericPlaceholderPattern(key, Boolean((nextHtype || nextCamelHtype) && convertFragmentStartsWithDot(nextFragment))) placeholders.push({ key, htype, camelHtype }) lastIndex = index + token.length } patternText += compileConvertRegexFragment(before.slice(lastIndex)) return { pattern: new RegExp(patternText, 'gi'), placeholders, } } function parseConvertPlaceholderHtype(key: string) { return key.match(/^HTYPE\s*:\s*(.+)$/i)?.[1]?.trim() } function parseConvertPlaceholderCamelHtype(key: string) { return key.match(/^CAMELHTP\s*:\s*(.+)$/i)?.[1]?.trim() } function convertHtypeColumnPattern(htype: string, targets: CryptoTarget[], allowAlias: boolean) { const columns = uniqueIgnoreCase( targets .filter((target) => equalsIgnoreCase(target.cryptoType, htype)) .map((target) => target.plainColumn) .filter(Boolean), ) if (columns.length === 0) return '((?=a)b)' const aliasPattern = allowAlias ? String.raw`(?:(?:[A-Za-z_][A-Za-z0-9_]*|"[^"]+"|\[[^\]]+\]|` + '`[^`]+`' + String.raw`)\s*\.\s*)?` : '' const columnAlternation = columns .sort((left, right) => right.length - left.length) .map((column) => escapeRegExp(column)) .join('|') return `(${aliasPattern}(?:${columnAlternation}))` } function convertCamelHtypePattern(htype: string, targets: CryptoTarget[]) { const columns = uniqueIgnoreCase( targets .filter((target) => equalsIgnoreCase(target.cryptoType, htype)) .map((target) => toCamel(target.plainColumn)) .filter(Boolean), ) if (columns.length === 0) return '((?=a)b)' const columnAlternation = columns .sort((left, right) => right.length - left.length) .map((column) => escapeRegExp(column)) .join('|') return `(${columnAlternation})` } function convertGenericPlaceholderPattern(key: string, sqlIdentifierOnly: boolean) { if (/^\d+$/.test(key)) return String.raw`(\S+?)` return sqlIdentifierOnly ? String.raw`([A-Za-z_][A-Za-z0-9_]*|"[^"]+"|\[[^\]]+\]|` + '`[^`]+`' + ')' : '([\\s\\S]*?)' } function convertFragmentStartsWithDot(fragment: string) { return /^\s*(?:\\\.)?\./.test(fragment) } function convertFragmentEndsWithDot(fragment: string) { return /(?:\\\.)?\.\s*$/.test(fragment) } function uniqueIgnoreCase(values: string[]) { const seen = new Set() const unique: string[] = [] for (const value of values) { const key = value.toUpperCase() if (seen.has(key)) continue seen.add(key) unique.push(value) } return unique } function compileConvertRegexFragment(fragment: string) { return makeRegexQuantifiersLazy(makeRegexWhitespaceFlexible(disableRegexCapturingGroups(fragment))) } function makeRegexWhitespaceFlexible(pattern: string) { let next = '' let inCharClass = false let pendingWhitespace = false for (let index = 0; index < pattern.length; index += 1) { const char = pattern[index] const previous = pattern[index - 1] if (char === '[' && previous !== '\\') inCharClass = true if (char === ']' && previous !== '\\') inCharClass = false if (!inCharClass && previous !== '\\' && /\s/.test(char)) { pendingWhitespace = true continue } if (pendingWhitespace) { next += String.raw`\s+` pendingWhitespace = false } next += char } if (pendingWhitespace) next += String.raw`\s+` return next } function disableRegexCapturingGroups(pattern: string) { let next = '' let inCharClass = false for (let index = 0; index < pattern.length; index += 1) { const char = pattern[index] const previous = pattern[index - 1] if (char === '[' && previous !== '\\') inCharClass = true if (char === ']' && previous !== '\\') inCharClass = false if (!inCharClass && char === '(' && previous !== '\\' && pattern[index + 1] !== '?') { next += '(?:' continue } next += char } return next } function makeRegexQuantifiersLazy(pattern: string) { return pattern .replace(/((?:\\.|\.|\[[^\]]+\]|\([^?][\s\S]*?\))[*+])(?![?+{])/g, '$1?') .replace(/(\{(?:\d+,|\d+,\d+)\})(?![?+])/g, '$1?') } function convertCaptures( args: unknown[], placeholders: ConvertPlaceholder[], targets: CryptoTarget[], ): ConvertCaptureResult | undefined { const captured = new Map() const matchedTargets: CryptoTarget[] = [] for (let index = 0; index < placeholders.length; index += 1) { const placeholder = placeholders[index] const value = String(args[index + 1] ?? '') if (placeholder.htype) { const columnName = stripAlias(value) const target = targets.find((item) => ( equalsIgnoreCase(item.cryptoType, placeholder.htype ?? '') && equalsIgnoreCase(item.plainColumn, columnName) )) if (!target) return undefined captured.set(placeholder.key, target.plainColumn) captured.set(`HTYPE:${placeholder.htype}`, target.plainColumn) matchedTargets.push(target) } else if (placeholder.camelHtype) { const camelName = stripAlias(value) const target = targets.find((item) => ( equalsIgnoreCase(item.cryptoType, placeholder.camelHtype ?? '') && equalsIgnoreCase(toCamel(item.plainColumn), camelName) )) if (!target) return undefined const camelColumn = toCamel(target.plainColumn) captured.set(placeholder.key, camelColumn) captured.set(`CAMELHTP:${placeholder.camelHtype}`, camelColumn) matchedTargets.push(target) } else { captured.set(placeholder.key, value) } } return { captured, targets: matchedTargets } } function convertTargetsHaveTableInText(targets: CryptoTarget[], text: string) { if (targets.length === 0) return false return targets.every((target) => includesIgnoreCase(text, target.tableName)) } function applyConvertTobe(tobe: string, captured: Map) { const replaced = tobe.replace(/@@\(([^)]+)\)@@/g, (match: string, rawKey: string) => { const key = rawKey.trim() return captured.get(key) ?? match }) return applyConvertCaseMacros(replaced) } function applyConvertCaseMacros(text: string) { return text.replace(/\b(upper|lower|camel)#\{\s*([^}]*?)\s*\}/gi, (_match: string, command: string, value: string) => { const normalized = value.trim() if (equalsIgnoreCase(command, 'upper')) return normalized.toUpperCase() if (equalsIgnoreCase(command, 'lower')) return normalized.toLowerCase() return toCamel(normalized) }) } function replaceConvertReservedWords(text: string, date: Date, markerUserId = '') { return text .replace(/!!!dateyyyymmddhhmm!!!/gi, formatLocalDateTimeMinute(date)) .replace(/!!!dateyyyymmdd!!!/gi, formatLocalDate(date)) .replace(/!!!userid!!!/gi, markerUserId) } function normalizeOptional(value: unknown) { return String(value ?? '').trim() } function parseSelectedMapperItem(text: string): MapperItem { const match = text.trim().match(/^<(insert|update|select|delete)\b([^>]*)>[\s\S]*<\/\1>$/i) if (!match) { throw new Error(', ,