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
}
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
}
const DECRYPT_TARGET_COMMENT = '/* 복호화 대상 */'
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 copyTypeHandlerRecommendation()
} 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.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 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 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 targets = await readCryptoTargets(cryptoTargetsPath)
const rules = await readConvertRules(CONVERT_XML_PATH)
const result = applyConvertRules(selectedText, rules, targets, new Date())
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) {
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)
})
}
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 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
patternText += compileConvertRegexFragment(previousFragment)
patternText += htype
? convertHtypeColumnPattern(htype, targets, !convertFragmentEndsWithDot(previousFragment))
: convertGenericPlaceholderPattern(key, Boolean(nextHtype && convertFragmentStartsWithDot(nextFragment)))
placeholders.push({ key, htype })
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 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 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 {
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) {
return text
.replace(/!!!dateyyyymmddhhmm!!!/gi, formatLocalDateTimeMinute(date))
.replace(/!!!dateyyyymmdd!!!/gi, formatLocalDate(date))
}
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(', , , 중 하나의 XML 태그 전체를 선택해야 합니다.')
}
return {
tag: match[1].toLowerCase() as MapperItem['tag'],
attrs: match[2],
id: attrValue(match[2], 'id') || 'anonymousSql',
text,
}
}
function buildRecommendation(
mapperItem: MapperItem,
selectedText: string,
mapperXml: string,
matchedTargets: CryptoTarget[],
date: string,
markerUserId: string,
) {
const tag = mapperItem.tag.toUpperCase()
const warnings: string[] = []
let afterSql = selectedText
let supportSnippets: string[] = []
if (tag === 'INSERT') {
afterSql = matchedTargets.reduce((next, target) => addInsertEncryption(next, target, date), afterSql)
} else if (tag === 'UPDATE') {
afterSql = matchedTargets.reduce((next, target) => addUpdateEncryption(next, target, date), afterSql)
} else if (tag === 'SELECT') {
afterSql = addSelectEncryption(selectedText, matchedTargets, date, markerUserId)
if (hasDecryptTargetComment(afterSql)) {
const desiredResultMap = desiredResultMapDefinition(mapperItem, selectedText, mapperXml, matchedTargets)
const existingResultMap = findExistingResultMap(mapperXml, desiredResultMap)
const resultMapId = existingResultMap?.id || desiredResultMap.id
afterSql = applySelectResultMap(afterSql, resultMapId)
if (!existingResultMap) {
supportSnippets = [resultMapSnippet(desiredResultMap)]
warnings.push(`resultMap 추가 추천: ${desiredResultMap.id}`)
} else {
warnings.push(`기존 resultMap 재사용: ${existingResultMap.id}`)
}
}
} else {
afterSql = matchedTargets.reduce((next, target) => addWhereHashRewrite(next, target, date, markerUserId), afterSql)
}
if (afterSql === selectedText && supportSnippets.length === 0) {
throw new Error('대상은 찾았지만 적용 가능한 치환 지점을 찾지 못했습니다.')
}
const header = [
``,
].filter(Boolean).join('\n')
const clipboardText = [header, afterSql.trim(), ...supportSnippets.map((snippet) => snippet.trim())].join('\n\n')
return { clipboardText, supportSnippets }
}
function buildFullFileRecommendation(mapperXml: string, targets: CryptoTarget[], date: string, markerUserId: string) {
const itemPattern = /<(insert|update|select|delete)\b([^>]*)>[\s\S]*?<\/\1>/gi
const supportSnippets: string[] = []
const chunks: string[] = []
let changedItemCount = 0
let lastIndex = 0
let mapperXmlForLookup = mapperXml
for (const match of mapperXml.matchAll(itemPattern)) {
const itemText = match[0]
const index = match.index ?? 0
chunks.push(mapperXml.slice(lastIndex, index))
const mapperItem = parseSelectedMapperItem(itemText)
const matchedTargets = targets.filter((target) => matchesTarget(itemText, target, mapperItem.tag))
if (matchedTargets.length === 0) {
chunks.push(itemText)
lastIndex = index + itemText.length
continue
}
const rewrite = rewriteMapperItemForFullFile(mapperItem, itemText, mapperXmlForLookup, matchedTargets, date, markerUserId)
chunks.push(rewrite.afterSql)
if (rewrite.afterSql !== itemText || rewrite.supportSnippets.length > 0) changedItemCount += 1
for (const snippet of rewrite.supportSnippets) {
if (appendUniqueResultMapSnippet(supportSnippets, mapperXmlForLookup, snippet)) {
mapperXmlForLookup += `\n${snippet}`
}
}
lastIndex = index + itemText.length
}
chunks.push(mapperXml.slice(lastIndex))
if (changedItemCount === 0) {
throw new Error('No mapper item matched crypto targets or no rewrite point was found.')
}
const rewrittenXml = insertResultMapsAtMapperTop(chunks.join(''), supportSnippets)
return {
clipboardText: rewrittenXml,
changedItemCount,
supportSnippets,
}
}
function markCryptoTargetColumns(mapperXml: string, targets: CryptoTarget[]) {
const itemPattern = /<(insert|update|select|delete)\b([^>]*)>[\s\S]*?<\/\1>/gi
const chunks: string[] = []
let changedCount = 0
let lastIndex = 0
for (const match of mapperXml.matchAll(itemPattern)) {
const itemText = match[0]
const index = match.index ?? 0
chunks.push(mapperXml.slice(lastIndex, index))
let nextItemText = itemText
const matchedTargets = targets.filter((target) => hasBoundaryToken(itemText, target.tableName) && hasBoundaryToken(itemText, target.plainColumn))
for (const target of matchedTargets) {
const result = markCryptoTargetColumnInItem(nextItemText, target)
nextItemText = result.text
changedCount += result.changedCount
}
chunks.push(nextItemText)
lastIndex = index + itemText.length
}
chunks.push(mapperXml.slice(lastIndex))
return { text: chunks.join(''), changedCount }
}
function markCryptoTargetColumnInItem(itemText: string, target: CryptoTarget) {
const commentRanges = commentTokenRanges(itemText)
let changedCount = 0
const pattern = new RegExp(
`(^|[^A-Za-z0-9_\\.])((?:[A-Za-z_][A-Za-z0-9_]*\\s*\\.\\s*)?${escapeRegExp(target.plainColumn)})(?=$|[^A-Za-z0-9_])`,
'gi',
)
const text = itemText.replace(pattern, (match: string, prefix: string, column: string, offset: number) => {
const columnStart = offset + prefix.length
const afterMatch = itemText.slice(offset + match.length)
if (isInsideRanges(columnStart, commentRanges)) return match
if (new RegExp(`^\\s*${escapeRegExp(CRYPTO_TARGET_CHECK_COMMENT)}`).test(afterMatch)) return match
changedCount += 1
return `${prefix}${column} ${CRYPTO_TARGET_CHECK_COMMENT}`
})
return { text, changedCount }
}
function commentTokenRanges(text: string) {
const ranges: Array<{ start: number; end: number }> = []
const commentPattern = /|\/\*[\s\S]*?\*\//g
for (const match of text.matchAll(commentPattern)) {
const start = match.index ?? 0
ranges.push({ start, end: start + match[0].length })
}
return ranges
}
function isInsideRanges(index: number, ranges: Array<{ start: number; end: number }>) {
return ranges.some((range) => index >= range.start && index < range.end)
}
function rewriteMapperItemForFullFile(
mapperItem: MapperItem,
itemText: string,
mapperXml: string,
matchedTargets: CryptoTarget[],
date: string,
markerUserId: string,
) {
const tag = mapperItem.tag.toUpperCase()
let afterSql = itemText
let supportSnippets: string[] = []
if (tag === 'INSERT') {
afterSql = matchedTargets.reduce((next, target) => addInsertEncryption(next, target, date), afterSql)
} else if (tag === 'UPDATE') {
afterSql = matchedTargets.reduce((next, target) => addUpdateEncryption(next, target, date), afterSql)
} else if (tag === 'SELECT') {
afterSql = addSelectEncryption(itemText, matchedTargets, date, markerUserId)
if (hasDecryptTargetComment(afterSql)) {
let desiredResultMap = desiredResultMapDefinition(mapperItem, itemText, mapperXml, matchedTargets)
const existingResultMap = findExistingResultMap(mapperXml, desiredResultMap)
if (!existingResultMap) {
desiredResultMap = {
...desiredResultMap,
id: uniqueResultMapId(desiredResultMap.id, mapperXml),
}
supportSnippets = [resultMapSnippet(desiredResultMap)]
}
afterSql = applySelectResultMap(afterSql, existingResultMap?.id || desiredResultMap.id)
}
} else {
afterSql = matchedTargets.reduce((next, target) => addWhereHashRewrite(next, target, date, markerUserId), afterSql)
}
return { afterSql, supportSnippets }
}
function appendUniqueResultMapSnippet(snippets: string[], mapperXml: string, snippet: string) {
const id = attrValue(snippet.match(/^]*)>/i)?.[1] ?? '', 'id')
if (!id) return false
if (parseResultMaps(mapperXml).some((resultMap) => resultMap.id === id)) return false
if (snippets.some((item) => attrValue(item.match(/^]*)>/i)?.[1] ?? '', 'id') === id)) return false
snippets.push(snippet)
return true
}
function insertResultMapsAtMapperTop(mapperXml: string, supportSnippets: string[]) {
if (supportSnippets.length === 0) return mapperXml
const block = supportSnippets.map((snippet) => indentResultMapSnippet(snippet, ' ')).join('\n\n')
return mapperXml.replace(/(]*>\s*)/i, `$1\n${block}\n\n`)
}
function indentResultMapSnippet(snippet: string, indent: string) {
return snippet
.split(/\r?\n/)
.map((line) => `${indent}${line}`)
.join('\n')
}
function uniqueResultMapId(baseId: string, mapperXml: string) {
const existingIds = new Set(parseResultMaps(mapperXml).map((resultMap) => resultMap.id))
if (!existingIds.has(baseId)) return baseId
for (let index = 2; index < 1000; index += 1) {
const candidate = `${baseId}${index}`
if (!existingIds.has(candidate)) return candidate
}
return `${baseId}${Date.now()}`
}
function addInsertEncryption(xmlText: string, target: CryptoTarget, date: string) {
if (hasBoundaryToken(xmlText, target.encColumn) && (!target.hashColumn || hasBoundaryToken(xmlText, target.hashColumn))) {
return xmlText
}
const valuesAt = xmlText.search(/\bVALUES\b/i)
if (valuesAt < 0) return xmlText
const head = xmlText.slice(0, valuesAt)
const tail = xmlText.slice(valuesAt)
const columnLine = new RegExp(`^(\\s*)${escapeRegExp(target.plainColumn)}(\\s*,?)\\s*$`, 'mi')
const bindLine = new RegExp(`^(\\s*)#\\{\\s*${escapeRegExp(target.javaProperty)}\\s*\\}(\\s*,?)\\s*$`, 'mi')
const columns = [target.plainColumn, target.encColumn, target.hashColumn].filter(Boolean)
const binds = [
`#{${target.javaProperty}}`,
withComment(typeHandlerBind(target.javaProperty, target.encryptTypeHandler), date),
target.hashColumn ? withComment(typeHandlerBind(target.javaProperty, target.hashTypeHandler), date) : '',
].filter(Boolean)
const nextHead = head.replace(columnLine, (_match, indent: string, comma: string) => {
return columns.map((column, index) => {
const suffix = index < columns.length - 1 || comma ? ',' : ''
return `${indent}${column}${suffix}`
}).join('\n')
})
const nextTail = tail.replace(bindLine, (_match, indent: string, comma: string) => {
return binds.map((bind, index) => {
const suffix = index < binds.length - 1 || comma ? ',' : ''
return `${indent}${bind}${suffix}`
}).join('\n')
})
if (nextHead !== head && nextTail !== tail) return nextHead + nextTail
return replaceFirstBoundary(head, target.plainColumn, columns.join(',\n '))
+ replaceFirstBoundary(tail, `#{${target.javaProperty}}`, binds.join(',\n '))
}
function addUpdateEncryption(xmlText: string, target: CryptoTarget, date: string) {
if (hasBoundaryToken(xmlText, target.encColumn) && (!target.hashColumn || hasBoundaryToken(xmlText, target.hashColumn))) {
return xmlText
}
const plainAssign = new RegExp(
`(^\\s*${escapeRegExp(target.plainColumn)}\\s*=\\s*#\\{\\s*${escapeRegExp(target.javaProperty)}\\s*\\}\\s*,?)(?:\\s*)?`,
'mi',
)
return xmlText.replace(plainAssign, (match, plainAssignment: string) => {
const indent = plainAssignment.match(/^\s*/)?.[0] ?? ''
const additions = [
hasBoundaryToken(xmlText, target.encColumn) ? '' : `${indent}${target.encColumn} = ${withComment(typeHandlerBind(target.javaProperty, target.encryptTypeHandler), date)},`,
target.hashColumn && !hasBoundaryToken(xmlText, target.hashColumn)
? `${indent}${target.hashColumn} = ${withComment(typeHandlerBind(target.javaProperty, target.hashTypeHandler), date)},`
: '',
].filter(Boolean)
if (additions.length === 0) return match
const plainAssignmentWithComma = /,\s*$/.test(plainAssignment) ? plainAssignment : `${plainAssignment},`
return [plainAssignmentWithComma, ...additions].join('\n')
})
}
function addSelectEncryption(xmlText: string, targets: CryptoTarget[], date: string, markerUserId: string) {
let next = xmlText
for (const target of targets) {
const tableAlias = findTableAlias(next, target.tableName)
const aliasPattern = new RegExp(
`\\b${escapeRegExp(target.encColumn)}\\s+AS\\s+${escapeRegExp(target.plainColumn)}\\b(?:\\s*/\\*\\s*복호화 대상\\s*\\*/)?`,
'i',
)
if (!aliasPattern.test(next)) {
next = replaceFirstSelectColumn(next, target, tableAlias)
} else {
next = next.replace(aliasPattern, (match) => {
return /\/\*\s*복호화 대상\s*\*\//.test(match) ? match : `${match} ${DECRYPT_TARGET_COMMENT}`
})
}
next = addWhereHashRewrite(next, target, date, markerUserId)
}
return next
}
function applySelectResultMap(xmlText: string, resultMapId: string) {
return xmlText.replace(/^(\s*)]*)>/i, (_match, indent: string, attrs: string) => {
const nextAttrs = attrs
.replace(/\s+resultType\s*=\s*['"][^'"]*['"]/i, '')
.replace(/\s+resultMap\s*=\s*['"][^'"]*['"]/i, '')
.trimEnd()
return `${indent}`
})
}
function hasDecryptTargetComment(xmlText: string) {
return xmlText.includes(DECRYPT_TARGET_COMMENT)
}
function selectDecryptAlias(target: CryptoTarget, tableAlias: string) {
const qualifier = tableAlias ? `${tableAlias}.` : ''
return `NVL(${qualifier}${target.encColumn}, ${qualifier}${target.plainColumn}) AS ${target.plainColumn} ${DECRYPT_TARGET_COMMENT}`
}
function selectDecryptAliasPattern(target: CryptoTarget) {
const qualifier = String.raw`(?:[A-Za-z_][A-Za-z0-9_]*\s*\.\s*)?`
return new RegExp(
String.raw`\bNVL\s*\(\s*${qualifier}${escapeRegExp(target.encColumn)}\s*,\s*${qualifier}${escapeRegExp(target.plainColumn)}\s*\)\s+AS\s+${escapeRegExp(target.plainColumn)}\b`,
'i',
)
}
function replaceFirstSelectColumn(xmlText: string, target: CryptoTarget, tableAlias: string) {
if (selectDecryptAliasPattern(target).test(xmlText)) return xmlText
const columnRange = selectColumnRange(xmlText)
if (!columnRange) return xmlText
const { columnsStart, columnsEnd } = columnRange
const before = xmlText.slice(0, columnsStart)
const columns = xmlText.slice(columnsStart, columnsEnd)
const after = xmlText.slice(columnsEnd)
const columnPattern = selectPlainColumnPattern(target)
const columnItems = splitTopLevelCsvItems(columns)
for (const item of columnItems) {
if (!isTargetSelectColumnItem(item.text, target)) continue
const replacedItem = item.text.replace(columnPattern, (_match, prefix: string, column: string) => {
const columnAlias = column.match(/^([A-Za-z_][A-Za-z0-9_]*)\s*\./)?.[1] ?? ''
return `${prefix}${selectDecryptAlias(target, columnAlias || tableAlias)}`
})
return `${before}${columns.slice(0, item.start)}${replacedItem}${columns.slice(item.end)}${after}`
}
return xmlText
}
function selectPlainColumnPattern(target: CryptoTarget) {
return new RegExp(
`(^|[^A-Za-z0-9_\\.])((?:[A-Za-z_][A-Za-z0-9_]*\\s*\\.\\s*)?${escapeRegExp(target.plainColumn)})(?=$|[^A-Za-z0-9_])`,
'i',
)
}
function isTargetSelectColumnItem(itemText: string, target: CryptoTarget) {
const column = stripAlias(itemText)
return equalsIgnoreCase(column, target.plainColumn) || equalsIgnoreCase(column, target.encColumn)
}
function findTableAlias(xmlText: string, tableName: string) {
const fromMatch = xmlText.match(/\bfrom\b([\s\S]*?)(?:\bwhere\b|\bgroup\s+by\b|\border\s+by\b|\bhaving\b|\bunion\b|<\/select>|$)/i)
if (!fromMatch) return ''
const tablePattern = new RegExp(
`\\b${escapeRegExp(tableName)}\\b\\s+(?:AS\\s+)?([A-Za-z_][A-Za-z0-9_]*)\\b`,
'i',
)
const alias = fromMatch[1].match(tablePattern)?.[1] ?? ''
return alias && !/^(where|inner|left|right|full|cross|join|on|group|order|having|union)$/i.test(alias) ? alias : ''
}
function addWhereHashRewrite(xmlText: string, target: CryptoTarget, date: string, markerUserId = '') {
if (!target.hashColumn || !target.hashTypeHandler) return xmlText
const whereMatch = xmlText.match(/\bwhere\b/i)
if (!whereMatch || whereMatch.index === undefined) return xmlText
const beforeWhereBody = xmlText.slice(0, whereMatch.index + whereMatch[0].length)
const whereBody = xmlText.slice(whereMatch.index + whereMatch[0].length)
const tableAlias = findTableAlias(xmlText, target.tableName)
const qualifier = String.raw`(?:(?[A-Za-z_][A-Za-z0-9_]*)\s*\.\s*)?`
const column = escapeRegExp(target.plainColumn)
const property = escapeRegExp(target.javaProperty)
const parameter = String.raw`#\{\s*${property}\s*\}`
const equalsPattern = new RegExp(String.raw`${qualifier}\b${column}\b\s*=\s*${parameter}`, 'i')
const likePattern = new RegExp(
String.raw`${qualifier}\b${column}\b\s+LIKE\s*'%'\s*\|\|\s*${parameter}\s*\|\|\s*'%'`,
'i',
)
const replaceCondition = (match: string, alias?: string) => {
const conditionAlias = alias || tableAlias
return hashWhereFallbackCondition(target, conditionAlias, date, markerUserId)
}
if (equalsPattern.test(whereBody)) {
return `${beforeWhereBody}${whereBody.replace(equalsPattern, replaceCondition)}`
}
if (likePattern.test(whereBody)) {
return `${beforeWhereBody}${whereBody.replace(likePattern, replaceCondition)}`
}
return xmlText
}
function hashWhereFallbackCondition(target: CryptoTarget, alias: string, date: string, markerUserId: string) {
const qualifier = alias ? `${alias}.` : ''
const hashColumn = `${qualifier}${target.hashColumn}`
const plainColumn = `${qualifier}${target.plainColumn}`
const marker = cipherMarkerComment(date, markerUserId)
return [
`(${hashColumn} IS NOT NULL AND ${hashColumn} = ${typeHandlerBind(target.javaProperty, target.hashTypeHandler)})`,
` OR (${hashColumn} IS NULL AND UPPER(${plainColumn}) = UPPER(#{${target.javaProperty}})) ${marker}`,
].join('\n')
}
function cipherMarkerComment(date: string, markerUserId: string) {
const userIdPart = markerUserId ? ` ${markerUserId}` : ''
return `/* DS_PI_META_NET CIPHER_${date.replace(/-/g, '')}${userIdPart} */`
}
function desiredResultMapDefinition(
mapperItem: MapperItem,
selectedText: string,
mapperXml: string,
targets: CryptoTarget[],
): ResultMapDefinition {
const existingResultMapId = attrValue(mapperItem.attrs, 'resultMap')
const existingResultMap = existingResultMapId ? parseResultMaps(mapperXml).find((item) => item.id === existingResultMapId) : undefined
const type = attrValue(mapperItem.attrs, 'resultType')
|| existingResultMap?.type
|| inferDtoTypeFromMapperNamespace(mapperXml)
|| 'java.util.Map'
return {
id: resultMapId(mapperItem, selectedText, targets),
type,
entries: resultMapEntries(selectedText, targets),
}
}
function resultMapEntries(selectedText: string, targets: CryptoTarget[]) {
const entries: ResultMapEntry[] = []
const seen = new Set()
for (const column of selectedColumns(selectedText)) {
const target = targets.find((item) => equalsIgnoreCase(item.plainColumn, column) || equalsIgnoreCase(item.encColumn, column))
if (!target) continue
const property = target.javaProperty
const normalizedColumn = target.plainColumn
const key = `${normalizedColumn.toUpperCase()}|${property}`
if (seen.has(key)) continue
seen.add(key)
entries.push({
kind: entries.length === 0 && /_ID$/i.test(normalizedColumn) ? 'id' : 'result',
column: normalizedColumn,
property,
typeHandler: fqcn(target.encryptTypeHandler),
})
}
return entries
}
function selectedColumns(selectedText: string) {
const columnRange = selectColumnRange(selectedText)
if (!columnRange) return []
return splitTopLevelCsv(selectedText.slice(columnRange.columnsStart, columnRange.columnsEnd))
.map((item) => stripAlias(item))
.filter((item) => /^[A-Za-z_][A-Za-z0-9_]*$/.test(item))
}
function selectColumnRange(xmlText: string) {
const openTag = xmlText.match(/]*>/i)
const bodyStart = openTag && openTag.index !== undefined ? openTag.index + openTag[0].length : 0
const closeTag = xmlText.match(/<\/select\s*>/i)
const bodyEnd = closeTag && closeTag.index !== undefined ? closeTag.index : xmlText.length
const body = xmlText.slice(bodyStart, bodyEnd)
const selectIndex = findTopLevelSqlKeyword(body, 'select', 0)
if (selectIndex < 0) return undefined
const fromIndex = findTopLevelSqlKeyword(body, 'from', selectIndex + 'select'.length)
if (fromIndex < 0) return undefined
return {
columnsStart: bodyStart + selectIndex + 'select'.length,
columnsEnd: bodyStart + fromIndex,
}
}
function findTopLevelSqlKeyword(text: string, keyword: string, start: number) {
let quote = ''
let depth = 0
for (let index = start; index < text.length; index += 1) {
const char = text[index]
const next = text[index + 1]
if (quote) {
if (char === quote) {
if (quote === "'" && next === "'") {
index += 1
} else {
quote = ''
}
}
continue
}
if (char === '-' && next === '-') {
index = skipLineComment(text, index + 2)
continue
}
if (char === '/' && next === '*') {
index = skipBlockComment(text, index + 2)
continue
}
if (char === "'" || char === '"') {
quote = char
continue
}
if (char === '(') {
depth += 1
continue
}
if (char === ')') {
depth = Math.max(0, depth - 1)
continue
}
if (depth === 0 && matchesSqlKeywordAt(text, keyword, index)) return index
}
return -1
}
function matchesSqlKeywordAt(text: string, keyword: string, index: number) {
return text.toLowerCase().startsWith(keyword.toLowerCase(), index)
&& isSqlKeywordBoundary(text, index, keyword.length)
}
function isSqlKeywordBoundary(text: string, index: number, length: number) {
const before = text[index - 1] ?? ''
const after = text[index + length] ?? ''
return !/[A-Za-z0-9_]/.test(before) && !/[A-Za-z0-9_]/.test(after)
}
function skipLineComment(text: string, start: number) {
const end = text.indexOf('\n', start)
return end < 0 ? text.length : end
}
function skipBlockComment(text: string, start: number) {
const end = text.indexOf('*/', start)
return end < 0 ? text.length : end + 1
}
function splitTopLevelCsv(text: string) {
return splitTopLevelCsvItems(text).map((item) => item.text.trim())
}
function splitTopLevelCsvItems(text: string) {
const items: Array<{ text: string; start: number; end: number }> = []
let current = ''
let quote = false
let depth = 0
let itemStart = 0
for (let index = 0; index < text.length; index += 1) {
const char = text[index]
const next = text[index + 1]
if (char === "'") {
current += char
if (quote && next === "'") {
current += next
index += 1
} else {
quote = !quote
}
continue
}
if (!quote && char === '(') depth += 1
if (!quote && char === ')') depth = Math.max(0, depth - 1)
if (!quote && depth === 0 && char === ',') {
items.push({ text: current, start: itemStart, end: index })
current = ''
itemStart = index + 1
continue
}
current += char
}
if (current.trim()) items.push({ text: current, start: itemStart, end: text.length })
return items
}
function stripAlias(value: string): string {
const normalized = value
.replace(//g, '')
.replace(/\/\*[\s\S]*?\*\//g, '')
.trim()
const explicitAlias = normalized.match(/\s+AS\s+([A-Za-z_][A-Za-z0-9_]*)$/i)
if (explicitAlias) return explicitAlias[1]
const implicitAlias = normalized.match(/^((?:[A-Za-z_][A-Za-z0-9_]*\s*\.\s*)?[A-Za-z_][A-Za-z0-9_]*)\s+[A-Za-z_][A-Za-z0-9_]*$/i)
if (implicitAlias) return stripAlias(implicitAlias[1])
return normalized
.replace(/^.*\.([A-Za-z_][A-Za-z0-9_]*)$/, '$1')
.trim()
}
function resultMapId(mapperItem: MapperItem, selectedText: string, targets: CryptoTarget[]) {
const existing = attrValue(mapperItem.attrs, 'resultMap')
if (existing) return existing
const dtoResultMapId = resultMapIdFromDtoResultType(attrValue(mapperItem.attrs, 'resultType'))
if (dtoResultMapId) return dtoResultMapId
const sqlPart = toPascal(mapperItem.id.replace(/^(select|get|find|query|list)/i, '')) || toPascal(mapperItem.id)
const tablePart = toPascal(commonTableName(targets))
const suffix = /result$/i.test(sqlPart) ? 'Map' : 'ResultMap'
const id = `${lowerFirst(sqlPart || tablePart)}${tablePart && !sqlPart.includes(tablePart) ? tablePart : ''}Crypto${suffix}`
return id.replace(/^[A-Z]/, (char) => char.toLowerCase())
}
function resultMapIdFromDtoResultType(resultType: string) {
if (!resultType) return ''
const dtoName = resultType.split('.').pop() ?? ''
return /^[A-Za-z_][A-Za-z0-9_]*Dto$/.test(dtoName) ? `${dtoName}Result` : ''
}
function commonTableName(targets: CryptoTarget[]) {
const table = targets[0]?.tableName || 'Crypto'
return table.replace(/^TB_/i, '')
}
function parseResultMaps(mapperXml: string): ResultMapDefinition[] {
const resultMaps: ResultMapDefinition[] = []
const resultMapPattern = /]*)>([\s\S]*?)<\/resultMap>/gi
for (const match of mapperXml.matchAll(resultMapPattern)) {
const attrs = match[1]
const body = match[2]
const entries: ResultMapEntry[] = []
for (const entryMatch of body.matchAll(/<(id|result)\b([^/>]*?)\/?>/gi)) {
const entryAttrs = entryMatch[2]
const column = attrValue(entryAttrs, 'column')
const property = attrValue(entryAttrs, 'property')
if (!column || !property) continue
entries.push({
kind: entryMatch[1].toLowerCase() === 'id' ? 'id' : 'result',
column,
property,
typeHandler: attrValue(entryAttrs, 'typeHandler') || undefined,
})
}
resultMaps.push({
id: attrValue(attrs, 'id'),
type: attrValue(attrs, 'type'),
entries,
})
}
return resultMaps
}
function findExistingResultMap(mapperXml: string, desired: ResultMapDefinition) {
return parseResultMaps(mapperXml).find((resultMap) => sameResultMapDefinition(resultMap, desired))
}
function sameResultMapDefinition(left: ResultMapDefinition, right: ResultMapDefinition) {
if (left.type !== right.type) return false
const leftEntries = left.entries.map(resultMapEntryKey).sort()
const rightEntries = right.entries.map(resultMapEntryKey).sort()
return leftEntries.length === rightEntries.length && leftEntries.every((entry, index) => entry === rightEntries[index])
}
function resultMapEntryKey(entry: ResultMapEntry) {
return [
entry.kind,
entry.column.toUpperCase(),
entry.property,
fqcn(entry.typeHandler).toLowerCase(),
].join('|')
}
function resultMapSnippet(definition: ResultMapDefinition) {
const lines = [
``,
...definition.entries.map((entry) => {
const typeHandler = entry.typeHandler ? ` typeHandler="${fqcn(entry.typeHandler)}"` : ''
return ` <${entry.kind} column="${entry.column}" property="${entry.property}"${typeHandler}/>`
}),
` `,
]
return lines.join('\n')
}
function inferDtoTypeFromMapperNamespace(mapperXml: string) {
const namespace = mapperXml.match(/]*\bnamespace\s*=\s*['"]([^'"]+)['"]/i)?.[1]
if (!namespace) return ''
if (/Mapper$/i.test(namespace)) return namespace.replace(/Mapper$/i, 'Dto')
return `${namespace}Dto`
}
function matchesTarget(text: string, target: CryptoTarget, tag?: MapperItem['tag']) {
if (!hasBoundaryToken(text, target.tableName)) return false
if (tag === 'select') return hasSelectRewritePoint(text, target)
return hasBoundaryToken(text, target.plainColumn)
|| hasBoundaryToken(text, target.encColumn)
|| Boolean(target.hashColumn && hasBoundaryToken(text, target.hashColumn))
}
function hasSelectRewritePoint(text: string, target: CryptoTarget) {
return selectedColumns(text).some((column) => equalsIgnoreCase(column, target.plainColumn) || equalsIgnoreCase(column, target.encColumn))
|| hasWhereRewritePoint(text, target)
}
function hasWhereRewritePoint(text: string, target: CryptoTarget) {
const whereMatch = text.match(/\bwhere\b/i)
if (!whereMatch || whereMatch.index === undefined) return false
const whereBody = text.slice(whereMatch.index + whereMatch[0].length)
const qualifier = String.raw`(?:[A-Za-z_][A-Za-z0-9_]*\s*\.\s*)?`
const column = escapeRegExp(target.plainColumn)
const property = escapeRegExp(target.javaProperty)
const parameter = String.raw`#\{\s*${property}\s*\}`
const equalsPattern = new RegExp(String.raw`${qualifier}\b${column}\b\s*=\s*${parameter}`, 'i')
const likePattern = new RegExp(
String.raw`${qualifier}\b${column}\b\s+LIKE\s*'%'\s*\|\|\s*${parameter}\s*\|\|\s*'%'`,
'i',
)
return equalsPattern.test(whereBody) || likePattern.test(whereBody)
}
function hasBoundaryToken(text: string, token: string) {
return token ? boundaryPattern(token).test(text) : false
}
function replaceFirstBoundary(text: string, token: string, replacement: string) {
const pattern = new RegExp(`(^|[^A-Za-z0-9_])(${escapeRegExp(token)})(?=$|[^A-Za-z0-9_])`, 'i')
return text.replace(pattern, (_match, prefix: string) => `${prefix}${replacement}`)
}
function boundaryPattern(token: string) {
return new RegExp(`(^|[^A-Za-z0-9_])${escapeRegExp(token)}(?=$|[^A-Za-z0-9_])`, 'i')
}
function attrValue(text: string, name: string) {
return text.match(new RegExp(`\\b${name}\\s*=\\s*['"]([^'"]+)['"]`, 'i'))?.[1] ?? ''
}
function typeHandlerBind(property: string, handler?: string) {
const typeHandler = fqcn(handler)
return typeHandler ? `#{${property}, typeHandler=${typeHandler}}` : `#{${property}}`
}
function fqcn(handler?: string) {
if (!handler) return ''
return handler.includes('.') ? handler : `${handler}`
}
function withComment(text: string, date: string) {
return `${text} `
}
function toCamel(value: string) {
return value
.toLowerCase()
.replace(/_([a-z0-9])/g, (_match, char: string) => char.toUpperCase())
}
function toPascal(value: string) {
const words = value
.replace(/^TB_/i, '')
.replace(/([a-z0-9])([A-Z])/g, '$1_$2')
.split(/[^A-Za-z0-9]+|_/)
.filter(Boolean)
return words.map((word) => word.slice(0, 1).toUpperCase() + word.slice(1).toLowerCase()).join('')
}
function lowerFirst(value: string) {
return value.replace(/^./, (char) => char.toLowerCase())
}
function equalsIgnoreCase(left: string, right: string) {
return left.toUpperCase() === right.toUpperCase()
}
function includesIgnoreCase(text: string, value: string) {
return value ? text.toUpperCase().includes(value.toUpperCase()) : false
}
function escapeRegExp(value: string) {
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
}
function formatLocalDate(date: Date) {
const year = date.getFullYear()
const month = String(date.getMonth() + 1).padStart(2, '0')
const day = String(date.getDate()).padStart(2, '0')
return `${year}-${month}-${day}`
}
function formatLocalDateTimeMinute(date: Date) {
const hours = String(date.getHours()).padStart(2, '0')
const minutes = String(date.getMinutes()).padStart(2, '0')
return `${formatLocalDate(date)} ${hours}:${minutes}`
}
댓글
댓글 쓰기