forked from segmentio/pg-escape
-
Notifications
You must be signed in to change notification settings - Fork 0
/
index.js
143 lines (130 loc) · 3.76 KB
/
index.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
const check_arg = (arg, not_null) => {
if (arg === void 0) {
throw new Error('A parameter is undefined');
}
if (arg === null && not_null) {
throw new Error('A parameter is null but should not be');
}
return arg;
};
const reserved_words = require('fs')
.readFileSync(`${__dirname}/reserved.txt`, 'utf8')
.split('\n')
.reduce((map, word) => {
map[word.toLowerCase()] = true;
return map;
}, {});
const coerce_string = x => {
if (x === null) {
return ['NULL', false];
}
switch (typeof x) {
case 'string': return [x, true];
case 'number': return [String(x), false];
case 'boolean': return [String(x).toUpperCase(), false];
default: throw new Error(`Cannot safely coerce value of type ${typeof x} to string`);
}
};
const string = val => coerce_string(check_arg(val, true))[0];
const valid_identifier = id => !reserved_words[id] && /^[a-z_][a-z0-9_$]*$/i.test(id);
const quote_identifier = id => `"${check_arg(id, true).replace(/"/g, '""')}"`;
const ident = val => valid_identifier(check_arg(val, true)) ? val : quote_identifier(val);
const check_string = val => {
if (typeof val !== 'string') {
throw new Error('Argument is not a string');
}
};
/* http://www.postgresql.org/docs/8.3/interactive/sql-syntax-lexical.html#SQL-SYNTAX-DOLLAR-QUOTING */
const dollar_string = val => {
check_arg(val);
if (val === null) {
return 'NULL';
}
check_string(val);
/* Generate random tag for quote markers */
const gen_tag = len => {
let res;
do {
res = Math.random().toString(36).substr(2, len);
} while (/^[a-z]/i.test(res));
return `$${res}$`;
};
let tag = '$$';
/*
* If data string contains the tag, generate a new one and increase the
* tag length (limited by the maximum fractional digits emitted by
* Number.prototype.toString)
*/
let len = 0;
while (val.indexOf(tag) !== -1) {
tag = gen_tag(++len);
}
return `${tag}${val}${tag}`;
};
const quote_string = val => {
check_arg(val);
if (val === null) {
return 'NULL';
}
check_string(val);
const has_backslash = val.indexOf('\\') !== -1;
const prefix = has_backslash ? 'E' : '';
return `${prefix}'${val.replace(/'/g, '\'\'').replace(/\\/g, '\\\\')}'`;
};
const _literal = (val, in_array = false) => {
const literal = x => _literal(x, in_array);
check_arg(val);
if (val === null) {
return 'NULL';
} else if (Array.isArray(val)) {
return `${in_array ? '' : 'ARRAY'}[${val.map(x => _literal(x, true)).join(', ')}]`;
} else if (typeof val === 'object') {
return literal(JSON.stringify(val));
} else {
const [str, quote] = coerce_string(val);
/* Use dollar-quoting for longer strings */
if (!quote) {
return str;
} else if (str.length < 1000) {
return quote_string(str);
} else {
return dollar_string(str);
}
}
};
const literal = x => _literal(x, false);
const format = (fmt, ...args) => {
check_arg(fmt);
if (typeof fmt !== 'string') {
throw new Error('Format string is invalid');
}
let i = 0;
const consume = () => {
const idx = i++;
if (idx === args.length) {
throw new Error('Not enough parameters supplied to query formatter');
}
return check_arg(args[idx]);
};
const res = fmt.replace(/%(.)/g, (_, type) => {
switch (type) {
case '%': return '%';
case 's': return string(consume());
case 'I': return ident(consume());
case 'L': return literal(consume());
case 'Q': return dollar_string(consume());
case 'J': return literal(JSON.stringify(consume()));
default: throw new Error(`Invalid query format specifier: '${type}'`);
}
});
if (i !== args.length) {
throw new Error('Too many parameters supplied to query formatter');
}
return res;
};
module.exports = format;
module.exports.string = string;
module.exports.ident = ident;
module.exports.dollar_string = dollar_string;
module.exports.quote_string = quote_string;
module.exports.literal = literal;