cdk-rds-sql
Version:
A CDK construct that allows creating roles and databases an on Aurora Serverless Postgresql cluster.
252 lines (209 loc) • 6.47 kB
text/typescript
// reserved Postgres words
import reservedMap from './reserved'
const fmtPattern = {
ident: 'I',
literal: 'L',
string: 's',
}
// convert to Postgres default ISO 8601 format
function formatDate(date:string): string {
date = date.replace('T', ' ')
date = date.replace('Z', '+00')
return date
}
function isReserved(value:string):boolean {
if (reservedMap[value.toUpperCase()]) {
return true
}
return false
}
function arrayToList(useSpace:boolean, array:any[], formatter:(value:any)=>string) {
let sql = ''
sql += useSpace ? ' (' : '('
for (let i = 0; i < array.length; i++) {
sql += (i === 0 ? '' : ', ') + formatter(array[i])
}
sql += ')'
return sql
}
// Ported from PostgreSQL 9.2.4 source code in src/interfaces/libpq/fe-exec.c
export function quoteIdent(value:any): string {
if (value === undefined || value === null) {
throw new Error('SQL identifier cannot be null or undefined')
} else if (value === false) {
return '"f"'
} else if (value === true) {
return '"t"'
} else if (value instanceof Date) {
return '"' + formatDate(value.toISOString()) + '"'
} else if (value instanceof Buffer) {
throw new Error('SQL identifier cannot be a buffer')
} else if (Array.isArray(value) === true) {
const temp: string[] = []
for (let i = 0; i < value.length; i++) {
if (Array.isArray(value[i]) === true) {
throw new Error('Nested array to grouped list conversion is not supported for SQL identifier')
} else {
temp.push(quoteIdent(value[i]))
}
}
return temp.toString()
} else if (value === Object(value)) {
throw new Error('SQL identifier cannot be an object')
}
const ident = value.toString().slice(0) // create copy
// do not quote a valid, unquoted identifier
// https://www.postgresql.org/docs/current/sql-syntax-lexical.html#SQL-SYNTAX-IDENTIFIERS
if (/^[a-zA-Z_][a-zA-Z0-9_$.]*$/.test(ident) === true && isReserved(ident) === false) {
return ident
}
let quoted = '"'
for (let i = 0; i < ident.length; i++) {
const c = ident[i]
if (c === '"') {
quoted += c + c
} else {
quoted += c
}
}
quoted += '"'
return quoted
}
// Ported from PostgreSQL 9.2.4 source code in src/interfaces/libpq/fe-exec.c
export function quoteLiteral(value) {
let literal = ''
let explicitCast: string | null = null
if (value === undefined || value === null) {
return 'NULL'
} else if (typeof value === 'bigint') {
return BigInt(value).toString()
} else if (value === Number.POSITIVE_INFINITY) {
return "'Infinity'"
} else if (value === Number.NEGATIVE_INFINITY) {
return "'-Infinity'"
} else if (Number.isNaN(value)) {
return "'NaN'"
} else if (typeof value === 'number') {//Test must be AFTER other special case number tests
return Number(value).toString()
} else if (value === false) {
return "'f'"
} else if (value === true) {
return "'t'"
} else if (value instanceof Date) {
return "'" + formatDate(value.toISOString()) + "'"
} else if (value instanceof Buffer) {
return "E'\\\\x" + value.toString('hex') + "'"
} else if (Array.isArray(value) === true) {
const temp: string[] = []
for (let i = 0; i < value.length; i++) {
if (Array.isArray(value[i]) === true) {
temp.push(arrayToList(i !== 0, value[i], quoteLiteral))
} else {
temp.push(quoteLiteral(value[i]))
}
}
return temp.toString()
} else if (value === Object(value)) {
explicitCast = 'jsonb'
literal = JSON.stringify(value)
} else {
literal = value.toString().slice(0) // create copy
}
let hasBackslash = false
let quoted = '\''
for (let i = 0; i < literal.length; i++) {
const c = literal[i]
if (c === '\'') {
quoted += c + c
} else if (c === '\\') {
quoted += c + c
hasBackslash = true
} else {
quoted += c
}
}
quoted += '\''
if (hasBackslash === true) {
quoted = 'E' + quoted
}
if (explicitCast) {
quoted += '::' + explicitCast
}
return quoted
}
export function quoteString(value): string {
if (value === undefined || value === null) {
return ''
} else if (value === false) {
return 'f'
} else if (value === true) {
return 't'
} else if (value instanceof Date) {
return formatDate(value.toISOString())
} else if (value instanceof Buffer) {
return '\\x' + value.toString('hex')
} else if (Array.isArray(value) === true) {
const temp: string[] = []
for (let i = 0; i < value.length; i++) {
if (value[i] !== null && value[i] !== undefined) {
if (Array.isArray(value[i]) === true) {
temp.push(arrayToList(i !== 0, value[i], quoteString))
} else {
temp.push(quoteString(value[i]))
}
}
}
return temp.toString()
} else if (value === Object(value)) {
return JSON.stringify(value)
}
return value.toString().slice(0) // return copy
}
export function config(cfg) {
// default
fmtPattern.ident = 'I'
fmtPattern.literal = 'L'
fmtPattern.string = 's'
if (cfg && cfg.pattern) {
if (cfg.pattern.ident) { fmtPattern.ident = cfg.pattern.ident }
if (cfg.pattern.literal) { fmtPattern.literal = cfg.pattern.literal }
if (cfg.pattern.string) { fmtPattern.string = cfg.pattern.string }
}
}
export function formatWithArray(fmt, parameters) {
let index = 0
let params = parameters
let reText = '%(%|(\\d+\\$)?['
reText += fmtPattern.ident
reText += fmtPattern.literal
reText += fmtPattern.string
reText += '])'
const re = new RegExp(reText, 'g')
return fmt.replace(re, function (_, type) {
if (type === '%') {
return '%'
}
let position = index
const tokens = type.split('$')
if (tokens.length > 1) {
position = parseInt(tokens[0]) - 1
type = tokens[1]
}
if (position < 0) {
throw new Error('specified argument 0 but arguments start at 1')
} else if (position > params.length - 1) {
throw new Error('too few arguments')
}
index = position + 1
if (type === fmtPattern.ident) {
return quoteIdent(params[position])
} else if (type === fmtPattern.literal) {
return quoteLiteral(params[position])
} else if (type === fmtPattern.string) {
return quoteString(params[position])
}
})
}
export function format(fmt: string, ...args: any[]): string {
return formatWithArray(fmt, args)
}