UNPKG

7.73 kBJavaScriptView Raw
1const QueryBuilder = require('./query/builder');
2const Raw = require('./raw');
3const { transform } = require('lodash');
4
5// Valid values for the `order by` clause generation.
6const orderBys = ['asc', 'desc'];
7
8// Turn this into a lookup map
9const operators = transform(
10 [
11 '=',
12 '<',
13 '>',
14 '<=',
15 '>=',
16 '<>',
17 '!=',
18 'like',
19 'not like',
20 'between',
21 'not between',
22 'ilike',
23 'not ilike',
24 'exists',
25 'not exist',
26 'rlike',
27 'not rlike',
28 'regexp',
29 'not regexp',
30 '&',
31 '|',
32 '^',
33 '<<',
34 '>>',
35 '~',
36 '~*',
37 '!~',
38 '!~*',
39 '#',
40 '&&',
41 '@>',
42 '<@',
43 '||',
44 '&<',
45 '&>',
46 '-|-',
47 '@@',
48 '!!',
49 ['?', '\\?'],
50 ['?|', '\\?|'],
51 ['?&', '\\?&'],
52 ],
53 (result, key) => {
54 if (Array.isArray(key)) {
55 result[key[0]] = key[1];
56 } else {
57 result[key] = key;
58 }
59 },
60 {}
61);
62
63class Formatter {
64 constructor(client, builder) {
65 this.client = client;
66 this.builder = builder;
67 this.bindings = [];
68 }
69
70 // Accepts a string or array of columns to wrap as appropriate.
71 columnize(target) {
72 const columns = Array.isArray(target) ? target : [target];
73 let str = '',
74 i = -1;
75 while (++i < columns.length) {
76 if (i > 0) str += ', ';
77 str += this.wrap(columns[i]);
78 }
79 return str;
80 }
81
82 // Turns a list of values into a list of ?'s, joining them with commas unless
83 // a "joining" value is specified (e.g. ' and ')
84 parameterize(values, notSetValue) {
85 if (typeof values === 'function') return this.parameter(values);
86 values = Array.isArray(values) ? values : [values];
87 let str = '',
88 i = -1;
89 while (++i < values.length) {
90 if (i > 0) str += ', ';
91 str += this.parameter(values[i] === undefined ? notSetValue : values[i]);
92 }
93 return str;
94 }
95
96 // Formats `values` into a parenthesized list of parameters for a `VALUES`
97 // clause.
98 //
99 // [1, 2] -> '(?, ?)'
100 // [[1, 2], [3, 4]] -> '((?, ?), (?, ?))'
101 // knex('table') -> '(select * from "table")'
102 // knex.raw('select ?', 1) -> '(select ?)'
103 //
104 values(values) {
105 if (Array.isArray(values)) {
106 if (Array.isArray(values[0])) {
107 return `(${values
108 .map((value) => `(${this.parameterize(value)})`)
109 .join(', ')})`;
110 }
111 return `(${this.parameterize(values)})`;
112 }
113
114 if (values instanceof Raw) {
115 return `(${this.parameter(values)})`;
116 }
117
118 return this.parameter(values);
119 }
120
121 // Checks whether a value is a function... if it is, we compile it
122 // otherwise we check whether it's a raw
123 parameter(value) {
124 if (typeof value === 'function') {
125 return this.outputQuery(this.compileCallback(value), true);
126 }
127 return this.unwrapRaw(value, true) || '?';
128 }
129
130 unwrapRaw(value, isParameter) {
131 let query;
132 if (value instanceof QueryBuilder) {
133 query = this.client.queryCompiler(value).toSQL();
134 if (query.bindings) {
135 this.bindings = this.bindings.concat(query.bindings);
136 }
137 return this.outputQuery(query, isParameter);
138 }
139 if (value instanceof Raw) {
140 value.client = this.client;
141 if (this.builder._queryContext) {
142 value.queryContext = () => {
143 return this.builder._queryContext;
144 };
145 }
146
147 query = value.toSQL();
148 if (query.bindings) {
149 this.bindings = this.bindings.concat(query.bindings);
150 }
151 return query.sql;
152 }
153 if (isParameter) {
154 this.bindings.push(value);
155 }
156 }
157
158 /**
159 * Creates SQL for a parameter, which might be passed to where() or .with() or
160 * pretty much anywhere in API.
161 *
162 * @param query Callback (for where or complete builder), Raw or QueryBuilder
163 * @param method Optional at least 'select' or 'update' are valid
164 */
165 rawOrFn(value, method) {
166 if (typeof value === 'function') {
167 return this.outputQuery(this.compileCallback(value, method));
168 }
169 return this.unwrapRaw(value) || '';
170 }
171
172 // Puts the appropriate wrapper around a value depending on the database
173 // engine, unless it's a knex.raw value, in which case it's left alone.
174 wrap(value) {
175 const raw = this.unwrapRaw(value);
176 if (raw) return raw;
177 switch (typeof value) {
178 case 'function':
179 return this.outputQuery(this.compileCallback(value), true);
180 case 'object':
181 return this.parseObject(value);
182 case 'number':
183 return value;
184 default:
185 return this.wrapString(value + '');
186 }
187 }
188
189 wrapAsIdentifier(value) {
190 const queryContext = this.builder.queryContext();
191 return this.client.wrapIdentifier((value || '').trim(), queryContext);
192 }
193
194 alias(first, second) {
195 return first + ' as ' + second;
196 }
197
198 operator(value) {
199 const raw = this.unwrapRaw(value);
200 if (raw) return raw;
201 const operator = operators[(value || '').toLowerCase()];
202 if (!operator) {
203 throw new TypeError(`The operator "${value}" is not permitted`);
204 }
205 return operator;
206 }
207
208 // Specify the direction of the ordering.
209 direction(value) {
210 const raw = this.unwrapRaw(value);
211 if (raw) return raw;
212 return orderBys.indexOf((value || '').toLowerCase()) !== -1 ? value : 'asc';
213 }
214
215 // Compiles a callback using the query builder.
216 compileCallback(callback, method) {
217 const { client } = this;
218
219 // Build the callback
220 const builder = client.queryBuilder();
221 callback.call(builder, builder);
222
223 // Compile the callback, using the current formatter (to track all bindings).
224 const compiler = client.queryCompiler(builder);
225 compiler.formatter = this;
226
227 // Return the compiled & parameterized sql.
228 return compiler.toSQL(method || builder._method || 'select');
229 }
230
231 // Ensures the query is aliased if necessary.
232 outputQuery(compiled, isParameter) {
233 let sql = compiled.sql || '';
234 if (sql) {
235 if (
236 (compiled.method === 'select' || compiled.method === 'first') &&
237 (isParameter || compiled.as)
238 ) {
239 sql = `(${sql})`;
240 if (compiled.as) return this.alias(sql, this.wrap(compiled.as));
241 }
242 }
243 return sql;
244 }
245
246 // Key-value notation for alias
247 parseObject(obj) {
248 const ret = [];
249 for (const alias in obj) {
250 const queryOrIdentifier = obj[alias];
251 // Avoids double aliasing for subqueries
252 if (typeof queryOrIdentifier === 'function') {
253 const compiled = this.compileCallback(queryOrIdentifier);
254 compiled.as = alias; // enforces the object's alias
255 ret.push(this.outputQuery(compiled, true));
256 } else if (queryOrIdentifier instanceof QueryBuilder) {
257 ret.push(
258 this.alias(
259 `(${this.wrap(queryOrIdentifier)})`,
260 this.wrapAsIdentifier(alias)
261 )
262 );
263 } else {
264 ret.push(
265 this.alias(this.wrap(queryOrIdentifier), this.wrapAsIdentifier(alias))
266 );
267 }
268 }
269 return ret.join(', ');
270 }
271
272 // Coerce to string to prevent strange errors when it's not a string.
273 wrapString(value) {
274 const asIndex = value.toLowerCase().indexOf(' as ');
275 if (asIndex !== -1) {
276 const first = value.slice(0, asIndex);
277 const second = value.slice(asIndex + 4);
278 return this.alias(this.wrap(first), this.wrapAsIdentifier(second));
279 }
280 const wrapped = [];
281 let i = -1;
282 const segments = value.split('.');
283 while (++i < segments.length) {
284 value = segments[i];
285 if (i === 0 && segments.length > 1) {
286 wrapped.push(this.wrap((value || '').trim()));
287 } else {
288 wrapped.push(this.wrapAsIdentifier(value));
289 }
290 }
291 return wrapped.join('.');
292 }
293}
294
295module.exports = Formatter;