|
class SafeValue<T> { |
|
#value: T |
|
constructor(value: T) { |
|
this.#value = value |
|
} |
|
|
|
toString() { |
|
return `${this.#value}` |
|
} |
|
} |
|
|
|
/** Get leading space count */ |
|
function getLeadingSpaceCount(text: string): number { |
|
const match = text.match(/^([ ]*)/) |
|
return match?.at(1)?.length ?? 0 |
|
} |
|
/** Get last line ending space count */ |
|
function getLastLineEndSpaceCount(text: string): number { |
|
const match = text.match(/.*\n?([ ]*)$/) |
|
return match?.at(1)?.length ?? 0 |
|
} |
|
|
|
type LinePadding = { spaceCount: number, padFirstLine: boolean } |
|
class SafeSql { |
|
#value: string |
|
constructor(value: string) { |
|
this.#value = value |
|
} |
|
|
|
#normalize({ spaceCount, padFirstLine }: LinePadding) { |
|
const lines = this.#value.split('\n') |
|
// Remove empty lines |
|
.filter(line => line.trim() != '') |
|
|
|
const minLeadingSpaces = Math.min(...lines.map(getLeadingSpaceCount)) |
|
|
|
return lines |
|
// Pad lines if needed, (for nested text) |
|
.map((line, idx) => ((idx > 0 || padFirstLine) ? ' '.repeat(spaceCount) : '') + line) |
|
// Remove global left padding |
|
.map(line => line.slice(minLeadingSpaces)) |
|
.join('\n') |
|
} |
|
|
|
toString(pad?: LinePadding) { |
|
return this.#normalize(pad ?? { spaceCount: 0, padFirstLine: false }) |
|
} |
|
} |
|
|
|
function safe<T>(value: T): SafeValue<T> { |
|
return new SafeValue<T>(value) |
|
} |
|
function singleQuote(value: string): SafeValue<string> { |
|
return new SafeValue('\'' + value.replaceAll(`'`, `\\'`) + '\'') |
|
} |
|
function doubleQuote(value: string): SafeValue<string> { |
|
return new SafeValue('"' + safe(value.replaceAll(`"`, `\\"`)) + '"') |
|
} |
|
|
|
function escape(value: unknown): string { |
|
if (value == null) |
|
return 'NULL' |
|
switch (typeof value) { |
|
case 'boolean': |
|
return value ? 'TRUE' : 'FALSE' |
|
case 'number': |
|
return `${value}` |
|
case 'bigint': |
|
return `'${value}'` |
|
case 'object': |
|
return Array.isArray(value) |
|
? value.map(a => escape(a)).join(', ') |
|
: singleQuote(JSON.stringify(value)).toString() |
|
case 'string': |
|
return singleQuote(value).toString() |
|
case 'undefined': |
|
return 'NULL' |
|
default: |
|
throw new Error(`Unknown type: ${typeof value}`) |
|
} |
|
}; |
|
|
|
type ISqlParams = boolean | number | string | undefined | null | object | SafeValue<unknown> | SafeSql |
|
function sql(strings: TemplateStringsArray, ...values: ISqlParams[]) { |
|
let str = '' |
|
for (const [i, s] of strings.entries()) { |
|
str += s |
|
if (i < values.length) { |
|
const value = values[i] |
|
if (value instanceof SafeValue) |
|
str += value.toString() |
|
else if (value instanceof SafeSql) |
|
str += value.toString({ |
|
spaceCount: getLastLineEndSpaceCount(str), |
|
padFirstLine: false, // Cause of parent template has it |
|
}) |
|
else if (Array.isArray(value) && value.every(v => v instanceof SafeSql)) |
|
str += new SafeSql(value.join('\n')).toString({ |
|
spaceCount: getLastLineEndSpaceCount(str), |
|
padFirstLine: false, // Cause of parent template has it |
|
}) |
|
else |
|
str += escape(value) |
|
} |
|
} |
|
return new SafeSql(str) |
|
} |
|
|
|
sql.escape = escape |
|
sql.safe = safe |
|
sql.singleQuote = singleQuote |
|
sql.doubleQuote = doubleQuote |
|
|
|
export { sql } |