@imqueue/pg-sequelize 4.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +50 -0
- package/CONTRIBUTING.md +58 -0
- package/CONTRIBUTION-TERMS.md +79 -0
- package/LICENSE +585 -0
- package/README.md +94 -0
- package/SECURITY.md +41 -0
- package/index.d.ts +86 -0
- package/index.js +87 -0
- package/package.json +75 -0
- package/src/BaseModel.d.ts +695 -0
- package/src/BaseModel.js +917 -0
- package/src/Graph.d.ts +215 -0
- package/src/Graph.js +257 -0
- package/src/decorators/AssociatedWith.d.ts +94 -0
- package/src/decorators/AssociatedWith.js +71 -0
- package/src/decorators/ColumnIndex.d.ts +206 -0
- package/src/decorators/ColumnIndex.js +98 -0
- package/src/decorators/CreatedBy.d.ts +27 -0
- package/src/decorators/CreatedBy.js +84 -0
- package/src/decorators/DeletedBy.d.ts +30 -0
- package/src/decorators/DeletedBy.js +89 -0
- package/src/decorators/DynamicView.d.ts +124 -0
- package/src/decorators/DynamicView.js +113 -0
- package/src/decorators/Emittable.d.ts +39 -0
- package/src/decorators/Emittable.js +42 -0
- package/src/decorators/NullableIndex.d.ts +77 -0
- package/src/decorators/NullableIndex.js +64 -0
- package/src/decorators/UpdatedBy.d.ts +27 -0
- package/src/decorators/UpdatedBy.js +105 -0
- package/src/decorators/View.d.ts +87 -0
- package/src/decorators/View.js +93 -0
- package/src/decorators/index.d.ts +32 -0
- package/src/decorators/index.js +33 -0
- package/src/helpers/index.d.ts +24 -0
- package/src/helpers/index.js +25 -0
- package/src/helpers/js.d.ts +61 -0
- package/src/helpers/js.js +88 -0
- package/src/helpers/query.d.ts +445 -0
- package/src/helpers/query.js +1095 -0
- package/src/index.d.ts +162 -0
- package/src/index.js +223 -0
- package/src/types/DataPage.d.ts +52 -0
- package/src/types/DataPage.js +2 -0
- package/src/types/FieldsInput.d.ts +41 -0
- package/src/types/FieldsInput.js +75 -0
- package/src/types/FilterInput.d.ts +136 -0
- package/src/types/FilterInput.js +291 -0
- package/src/types/JsonObject.d.ts +16 -0
- package/src/types/JsonObject.js +50 -0
- package/src/types/OrderByInput.d.ts +45 -0
- package/src/types/OrderByInput.js +80 -0
- package/src/types/PaginationInput.d.ts +44 -0
- package/src/types/PaginationInput.js +90 -0
- package/src/types/index.d.ts +30 -0
- package/src/types/index.js +31 -0
- package/src/types/ranges/DateRange.d.ts +27 -0
- package/src/types/ranges/DateRange.js +69 -0
- package/src/types/ranges/IRange.d.ts +47 -0
- package/src/types/ranges/IRange.js +2 -0
- package/src/types/ranges/NumericRange.d.ts +19 -0
- package/src/types/ranges/NumericRange.js +61 -0
- package/src/types/ranges/index.d.ts +26 -0
- package/src/types/ranges/index.js +27 -0
|
@@ -0,0 +1,1095 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
* @imqueue/pg-sequelize - Sequelize ORM refines for @imqueue
|
|
3
|
+
*
|
|
4
|
+
* I'm Queue Software Project
|
|
5
|
+
* Copyright (C) 2025 imqueue.com <support@imqueue.com>
|
|
6
|
+
*
|
|
7
|
+
* This program is free software: you can redistribute it and/or modify
|
|
8
|
+
* it under the terms of the GNU General Public License as published by
|
|
9
|
+
* the Free Software Foundation, either version 3 of the License, or
|
|
10
|
+
* (at your option) any later version.
|
|
11
|
+
*
|
|
12
|
+
* This program is distributed in the hope that it will be useful,
|
|
13
|
+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
14
|
+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
15
|
+
* GNU General Public License for more details.
|
|
16
|
+
*
|
|
17
|
+
* You should have received a copy of the GNU General Public License
|
|
18
|
+
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
19
|
+
*
|
|
20
|
+
* If you want to use this code in a closed source (commercial) project, you can
|
|
21
|
+
* purchase a proprietary commercial license. Please contact us at
|
|
22
|
+
* <support@imqueue.com> to get commercial licensing options.
|
|
23
|
+
*/
|
|
24
|
+
import { clearObject, isArray, isObject } from './js.js';
|
|
25
|
+
import { Sequelize as SequelizeLib, Transaction, } from 'sequelize';
|
|
26
|
+
import { Association, Op, } from 'sequelize';
|
|
27
|
+
import { Model } from 'sequelize-typescript';
|
|
28
|
+
import { FieldsInput, FILTER_OPS, FilterInput, OrderByInput, OrderDirection, PaginationInput, } from '../types/index.js';
|
|
29
|
+
/**
|
|
30
|
+
* Turns a caller's serialized query into Sequelize options, and back out as SQL.
|
|
31
|
+
*
|
|
32
|
+
* @remarks
|
|
33
|
+
* The reason this package exists. A GraphQL API receives a filter, a page, an order
|
|
34
|
+
* and the set of fields the query selected — all of it plain JSON, none of it in a
|
|
35
|
+
* shape Sequelize accepts. These helpers translate: `autoQuery` is the one that
|
|
36
|
+
* composes the others, `toWhereOptions` turns a serialized filter into a `where` with
|
|
37
|
+
* the joins its nested parts imply, and `toLimitOptions` and `toOrderOptions` cover
|
|
38
|
+
* paging and ordering.
|
|
39
|
+
*
|
|
40
|
+
* The efficiency comes from the fields map. A column nobody asked for is not selected
|
|
41
|
+
* and a relation nobody reached into is not joined, so the statement narrows as the
|
|
42
|
+
* caller's selection narrows rather than being fixed by the resolver.
|
|
43
|
+
*
|
|
44
|
+
* Below that sit the escape hatches — `sql`, `L` and `E` — for the cases no option
|
|
45
|
+
* object can express.
|
|
46
|
+
*/
|
|
47
|
+
export var query;
|
|
48
|
+
(function (query) {
|
|
49
|
+
const RX_OP = /^\$/;
|
|
50
|
+
const RX_LIKE = /%/;
|
|
51
|
+
const RX_LTE = /^<=/;
|
|
52
|
+
const RX_GTE = /^>=/;
|
|
53
|
+
const RX_LT = /^</;
|
|
54
|
+
const RX_GT = /^>/;
|
|
55
|
+
const RX_EQ = /^=/;
|
|
56
|
+
const RX_RANGE = /Range$/;
|
|
57
|
+
const RX_SPACE = /\s/;
|
|
58
|
+
const RX_SQL_CLEAN = /\s+(;|$)/;
|
|
59
|
+
const RX_SQL_END = /;?$/;
|
|
60
|
+
const RX_SQL_QUOTE = /'/g;
|
|
61
|
+
/**
|
|
62
|
+
* Collapses whitespace outside quoted literals, so a statement fits on one line.
|
|
63
|
+
*
|
|
64
|
+
* @remarks
|
|
65
|
+
* Walks the string tracking whether a single quote is open, so runs of
|
|
66
|
+
* whitespace inside a literal are preserved and only structural whitespace is
|
|
67
|
+
* collapsed. Quote tracking is a simple toggle: it does not understand escaped
|
|
68
|
+
* or doubled quotes, so a literal containing one can throw the parity off.
|
|
69
|
+
*
|
|
70
|
+
* @param input - Statement to normalise.
|
|
71
|
+
* @returns The statement with structural whitespace collapsed to single spaces.
|
|
72
|
+
*/
|
|
73
|
+
function safeSqlSpaceCleanup(input) {
|
|
74
|
+
let output = '';
|
|
75
|
+
let opened = false;
|
|
76
|
+
let space = false;
|
|
77
|
+
for (const char of input) {
|
|
78
|
+
if (!opened && RX_SPACE.test(char)) {
|
|
79
|
+
if (!space) {
|
|
80
|
+
output += ' ';
|
|
81
|
+
}
|
|
82
|
+
space = true;
|
|
83
|
+
}
|
|
84
|
+
else {
|
|
85
|
+
output += char;
|
|
86
|
+
space = false;
|
|
87
|
+
}
|
|
88
|
+
if (char === "'") {
|
|
89
|
+
opened = !opened;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return output;
|
|
93
|
+
}
|
|
94
|
+
query.safeSqlSpaceCleanup = safeSqlSpaceCleanup;
|
|
95
|
+
/**
|
|
96
|
+
* Normalises a SQL string: one-lined, whitespace collapsed, ending in a single
|
|
97
|
+
* semicolon.
|
|
98
|
+
*
|
|
99
|
+
* @remarks
|
|
100
|
+
* Usable as a plain function or as a template tag, the tag form being there so
|
|
101
|
+
* an editor highlights the SQL. It does NOT interpolate: a tag carrying
|
|
102
|
+
* substitutions throws, because the substituted values cannot be reached from
|
|
103
|
+
* here and the statement would come out mangled: tagging
|
|
104
|
+
* `SELECT ... WHERE id = ${id}` used to yield `... id = ,`. Bind parameters are
|
|
105
|
+
* the answer, and they are also the only safe one.
|
|
106
|
+
*
|
|
107
|
+
* Whitespace inside single-quoted literals is preserved; only whitespace
|
|
108
|
+
* outside them is collapsed.
|
|
109
|
+
*
|
|
110
|
+
* @param sqlQuery - A complete statement, or a template with no substitutions.
|
|
111
|
+
* @param values - Template substitutions, which are not supported.
|
|
112
|
+
* @returns The statement on one line, terminated with exactly one `;`.
|
|
113
|
+
* @throws TypeError when used as a template tag with substitutions.
|
|
114
|
+
* @example
|
|
115
|
+
* ```typescript
|
|
116
|
+
* const statement = sql`
|
|
117
|
+
* SELECT id, name
|
|
118
|
+
* FROM "Lead"
|
|
119
|
+
* WHERE status = $1
|
|
120
|
+
* `;
|
|
121
|
+
* // SELECT id, name FROM "Lead" WHERE status = $1;
|
|
122
|
+
* await database().query(statement, { bind: [status] });
|
|
123
|
+
* ```
|
|
124
|
+
*/
|
|
125
|
+
function sql(sqlQuery, ...values) {
|
|
126
|
+
// A tagged template hands the literal parts in as an array, and
|
|
127
|
+
// String() would join them with commas — silently corrupting the
|
|
128
|
+
// statement rather than failing. Refuse instead: every previous caller
|
|
129
|
+
// that interpolated was already producing broken SQL.
|
|
130
|
+
if (values.length && isArray(sqlQuery)) {
|
|
131
|
+
throw new TypeError('query.sql() does not interpolate values: the substitutions ' +
|
|
132
|
+
'cannot be reached and the statement would be mangled. ' +
|
|
133
|
+
'Use bind parameters instead.');
|
|
134
|
+
}
|
|
135
|
+
return safeSqlSpaceCleanup(String(sqlQuery))
|
|
136
|
+
.replace(RX_SQL_CLEAN, '')
|
|
137
|
+
.replace(RX_SQL_END, ';');
|
|
138
|
+
}
|
|
139
|
+
query.sql = sql;
|
|
140
|
+
/**
|
|
141
|
+
* Keeps only the properties a model actually declares.
|
|
142
|
+
*
|
|
143
|
+
* @remarks
|
|
144
|
+
* The filter for input arriving from outside: anything the model does not declare
|
|
145
|
+
* as an attribute is dropped rather than passed to Sequelize, so a caller cannot
|
|
146
|
+
* set a column by sending an unexpected property. Arrays are mapped element by
|
|
147
|
+
* element, and relations are dropped along with everything else — this looks at
|
|
148
|
+
* `rawAttributes` only.
|
|
149
|
+
*
|
|
150
|
+
* @param model - Model whose attributes define what survives.
|
|
151
|
+
* @param input - One object, or an array of them.
|
|
152
|
+
* @param attributes - Attribute names to allow, defaulting to all the model's.
|
|
153
|
+
* @returns A new object (or array) carrying only the allowed properties.
|
|
154
|
+
*/
|
|
155
|
+
query.pureData = (model, input, attributes) => {
|
|
156
|
+
attributes = attributes || Object.keys(model.rawAttributes || {});
|
|
157
|
+
if (isArray(input)) {
|
|
158
|
+
return input.map(inputItem => query.pureData(model, inputItem, attributes));
|
|
159
|
+
}
|
|
160
|
+
return Object.keys(input).reduce((res, prop) => {
|
|
161
|
+
if (~attributes.indexOf(prop)) {
|
|
162
|
+
res[prop] = input[prop];
|
|
163
|
+
}
|
|
164
|
+
return res;
|
|
165
|
+
}, {});
|
|
166
|
+
};
|
|
167
|
+
/**
|
|
168
|
+
* Narrows a requested fields map to the model's own columns.
|
|
169
|
+
*
|
|
170
|
+
* @remarks
|
|
171
|
+
* Relations and unknown names are dropped, and the primary keys are then added
|
|
172
|
+
* back whether or not they were asked for — a deliberate trade so domain logic
|
|
173
|
+
* always has a key to work with, at the cost of returning a column the caller
|
|
174
|
+
* did not request.
|
|
175
|
+
*
|
|
176
|
+
* @param model - Model to narrow against.
|
|
177
|
+
* @param fields - Requested fields map, or a falsy value for "everything".
|
|
178
|
+
* @returns The surviving column names, or `true` meaning no restriction.
|
|
179
|
+
*/
|
|
180
|
+
function pureFields(model, fields) {
|
|
181
|
+
if (!fields) {
|
|
182
|
+
return true;
|
|
183
|
+
}
|
|
184
|
+
const attributes = Object.keys(model.rawAttributes || {});
|
|
185
|
+
const list = Object.keys(Object.keys(fields).reduce((res, prop) => {
|
|
186
|
+
if (~attributes.indexOf(prop)) {
|
|
187
|
+
res[prop] = fields[prop];
|
|
188
|
+
}
|
|
189
|
+
return res;
|
|
190
|
+
}, {}));
|
|
191
|
+
// make sure it contains primary key fields
|
|
192
|
+
// that's a tiny trade-off to make sure we won't loose it for a domain
|
|
193
|
+
// logic to use
|
|
194
|
+
primaryKeys(model).forEach(fieldName => !~list.indexOf(fieldName) && list.push(fieldName));
|
|
195
|
+
return list;
|
|
196
|
+
}
|
|
197
|
+
query.pureFields = pureFields;
|
|
198
|
+
/**
|
|
199
|
+
* Whether a fields map asks for any of the model's relations.
|
|
200
|
+
*
|
|
201
|
+
* @remarks
|
|
202
|
+
* The cheap test for "does this query need joins at all", used to avoid building
|
|
203
|
+
* an `include` when the caller only wants columns.
|
|
204
|
+
*
|
|
205
|
+
* @param model - Model whose associations to check against.
|
|
206
|
+
* @param fields - Requested fields map.
|
|
207
|
+
* @returns `true` when at least one key names an association.
|
|
208
|
+
*/
|
|
209
|
+
function needNesting(model, fields) {
|
|
210
|
+
if (!fields) {
|
|
211
|
+
return false;
|
|
212
|
+
}
|
|
213
|
+
const associations = Object.keys(model.associations || {});
|
|
214
|
+
const properties = Object.keys(fields);
|
|
215
|
+
if (!associations.length) {
|
|
216
|
+
return false;
|
|
217
|
+
}
|
|
218
|
+
return associations.some(name => !!~properties.indexOf(name));
|
|
219
|
+
}
|
|
220
|
+
query.needNesting = needNesting;
|
|
221
|
+
/**
|
|
222
|
+
* Intersects a set of attributes with the names a caller asked for.
|
|
223
|
+
*
|
|
224
|
+
* @remarks
|
|
225
|
+
* With a `model` given, an empty intersection falls back to that model's primary
|
|
226
|
+
* keys rather than to nothing — so a fields list that matches no column selects
|
|
227
|
+
* the keys instead of every column, which is the safer failure but is not what
|
|
228
|
+
* "no matches" might suggest. Without a `model`, an empty result stays empty.
|
|
229
|
+
*
|
|
230
|
+
* @param attributes - Object whose keys are the available names.
|
|
231
|
+
* @param fields - Names the caller asked for.
|
|
232
|
+
* @param model - Model to take primary keys from when nothing matched.
|
|
233
|
+
* @returns The matching names, or the primary keys, or an empty array.
|
|
234
|
+
*/
|
|
235
|
+
function filtered(attributes, fields, model) {
|
|
236
|
+
let filteredAttributes = attributes
|
|
237
|
+
? Object.keys(attributes).filter(attr => ~fields.indexOf(attr))
|
|
238
|
+
: [];
|
|
239
|
+
if (!filteredAttributes.length && model) {
|
|
240
|
+
filteredAttributes = primaryKeys(model);
|
|
241
|
+
}
|
|
242
|
+
return filteredAttributes;
|
|
243
|
+
}
|
|
244
|
+
query.filtered = filtered;
|
|
245
|
+
/**
|
|
246
|
+
* Foreign-key column names on a model for the given relations.
|
|
247
|
+
*
|
|
248
|
+
* @remarks
|
|
249
|
+
* These have to be selected even when the caller did not ask for them, or
|
|
250
|
+
* Sequelize cannot attach the joined rows — which is why `autoQuery` merges them
|
|
251
|
+
* into the attribute list.
|
|
252
|
+
*
|
|
253
|
+
* @param model - Model holding the foreign keys.
|
|
254
|
+
* @param relations - Association names to collect keys for.
|
|
255
|
+
* @returns The foreign-key column names.
|
|
256
|
+
*/
|
|
257
|
+
function foreignKeys(model, relations) {
|
|
258
|
+
const associations = model.associations || {};
|
|
259
|
+
return (relations
|
|
260
|
+
.map((name) => {
|
|
261
|
+
const association = (associations[name] || {});
|
|
262
|
+
if (association.source === model &&
|
|
263
|
+
association.foreignKey &&
|
|
264
|
+
!(association.sourceKey ||
|
|
265
|
+
association.associationType === 'BelongsToMany')) {
|
|
266
|
+
return association.foreignKey;
|
|
267
|
+
}
|
|
268
|
+
return null;
|
|
269
|
+
})
|
|
270
|
+
.filter(idField => idField) || []);
|
|
271
|
+
}
|
|
272
|
+
query.foreignKeys = foreignKeys;
|
|
273
|
+
/**
|
|
274
|
+
* Merges given arrays of scalars making sure they contains unique values
|
|
275
|
+
*
|
|
276
|
+
* @param args - Arrays to merge.
|
|
277
|
+
*/
|
|
278
|
+
function arrayMergeUnique(...args) {
|
|
279
|
+
const result = [];
|
|
280
|
+
for (const arr of args) {
|
|
281
|
+
result.push(...arr);
|
|
282
|
+
}
|
|
283
|
+
return result.filter((item, index) => result.indexOf(item) === index);
|
|
284
|
+
}
|
|
285
|
+
/**
|
|
286
|
+
* Merges query-option fragments into one options object.
|
|
287
|
+
*
|
|
288
|
+
* @remarks
|
|
289
|
+
* Per property: an absent property is taken as-is, arrays are unioned with
|
|
290
|
+
* duplicates dropped, objects are shallow-assigned, and anything else is
|
|
291
|
+
* overwritten by the later value. A fragment whose property type disagrees with
|
|
292
|
+
* what is already there — a scalar where an array sits, say — throws rather than
|
|
293
|
+
* guessing.
|
|
294
|
+
*
|
|
295
|
+
* MUTATES and returns `queryOptions`, so pass a fresh object unless sharing is
|
|
296
|
+
* what you want.
|
|
297
|
+
*
|
|
298
|
+
* @param queryOptions - Target, mutated in place.
|
|
299
|
+
* @param merge - Fragments to merge, in order; falsy ones are skipped.
|
|
300
|
+
* @returns The same `queryOptions` object.
|
|
301
|
+
* @throws TypeError when a fragment's property type conflicts with the target's.
|
|
302
|
+
*/
|
|
303
|
+
function mergeQuery(queryOptions = {}, ...merge) {
|
|
304
|
+
for (const item of merge) {
|
|
305
|
+
if (!item) {
|
|
306
|
+
continue;
|
|
307
|
+
}
|
|
308
|
+
for (const prop of Object.keys(item)) {
|
|
309
|
+
const err = `Given ${prop} option is invalid!`;
|
|
310
|
+
if (typeof queryOptions[prop] === 'undefined') {
|
|
311
|
+
queryOptions[prop] = item[prop];
|
|
312
|
+
continue;
|
|
313
|
+
}
|
|
314
|
+
if (typeof item[prop] === 'undefined') {
|
|
315
|
+
continue;
|
|
316
|
+
}
|
|
317
|
+
if (isArray(queryOptions[prop])) {
|
|
318
|
+
if (!isArray(item[prop])) {
|
|
319
|
+
throw new TypeError(err);
|
|
320
|
+
}
|
|
321
|
+
for (const element of item[prop]) {
|
|
322
|
+
if (!~queryOptions[prop].indexOf(element)) {
|
|
323
|
+
queryOptions[prop].push(element);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
continue;
|
|
327
|
+
}
|
|
328
|
+
if (isObject(queryOptions[prop])) {
|
|
329
|
+
if (!isObject(item[prop])) {
|
|
330
|
+
throw new TypeError(err);
|
|
331
|
+
}
|
|
332
|
+
Object.assign(queryOptions[prop], item[prop]);
|
|
333
|
+
continue;
|
|
334
|
+
}
|
|
335
|
+
queryOptions[prop] = item[prop];
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
return queryOptions;
|
|
339
|
+
}
|
|
340
|
+
query.mergeQuery = mergeQuery;
|
|
341
|
+
/**
|
|
342
|
+
* Builds Sequelize find options from a requested fields map, joins included.
|
|
343
|
+
*
|
|
344
|
+
* @remarks
|
|
345
|
+
* The helper the others compose into. It turns a {@link FieldsInput} into
|
|
346
|
+
* `attributes` plus a nested `include` for every relation the map mentions,
|
|
347
|
+
* recursing into each level, and adds the foreign keys a join needs whether or
|
|
348
|
+
* not they were requested. Its rest parameter then merges the fragments the
|
|
349
|
+
* sibling helpers return, which is the intended shape of a paginated read.
|
|
350
|
+
*
|
|
351
|
+
* A value in the map that is not `false` doubles as a filter for that column, so
|
|
352
|
+
* a fields map can carry a where clause; see {@link FieldsInput} for why presence
|
|
353
|
+
* rather than value is what selects.
|
|
354
|
+
*
|
|
355
|
+
* Two things to know. It MUTATES the `fields` map, adding any column named in a
|
|
356
|
+
* merged `order` that the map omits, so ordering cannot break selection. And
|
|
357
|
+
* `fields` may also be a plain array of names, which selects those columns and
|
|
358
|
+
* builds no joins at all.
|
|
359
|
+
*
|
|
360
|
+
* @param model - Model to build the query for.
|
|
361
|
+
* @param fields - Requested fields map, or an array of column names.
|
|
362
|
+
* @param merge - Option fragments to merge in, typically from the helpers below.
|
|
363
|
+
* @returns Find options, typed as the caller asks.
|
|
364
|
+
* @example
|
|
365
|
+
* ```typescript
|
|
366
|
+
* const where = toWhereOptions(withRangeFilters(filter));
|
|
367
|
+
* const rows = await LeadModel.findAll(autoQuery<FindOptions>(
|
|
368
|
+
* LeadModel,
|
|
369
|
+
* fields,
|
|
370
|
+
* where,
|
|
371
|
+
* toLimitOptions(pageOptions),
|
|
372
|
+
* toOrderOptions(orderBy),
|
|
373
|
+
* ));
|
|
374
|
+
* ```
|
|
375
|
+
*/
|
|
376
|
+
function autoQuery(model, fields, ...merge) {
|
|
377
|
+
const queryOptions = {};
|
|
378
|
+
const { order } = merge.find((item) => item && !!item.order) || {};
|
|
379
|
+
if (order && isArray(order)) {
|
|
380
|
+
// make sure order arg will not break selection
|
|
381
|
+
for (const [field] of order) {
|
|
382
|
+
if (fields && typeof fields[field] === 'undefined') {
|
|
383
|
+
fields[field] = false;
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
if (isArray(fields)) {
|
|
388
|
+
queryOptions.attributes = filtered(model.rawAttributes, fields, model);
|
|
389
|
+
}
|
|
390
|
+
else if (fields) {
|
|
391
|
+
const fieldNames = Object.keys(fields);
|
|
392
|
+
// relations which are requested by a user
|
|
393
|
+
const relations = filtered(model.associations, fieldNames);
|
|
394
|
+
// attributes which are requested by a user
|
|
395
|
+
queryOptions.attributes = arrayMergeUnique(filtered(model.rawAttributes, fieldNames, model), foreignKeys(model, relations));
|
|
396
|
+
// we may want to check if the given field is being filtered
|
|
397
|
+
// and build where clause for it
|
|
398
|
+
Object.assign(queryOptions, toWhereOptions(queryOptions.attributes.reduce((res, attr) => {
|
|
399
|
+
if (fields[attr] !== false) {
|
|
400
|
+
res[attr] = fields[attr];
|
|
401
|
+
}
|
|
402
|
+
return res;
|
|
403
|
+
}, {})));
|
|
404
|
+
if (relations.length) {
|
|
405
|
+
queryOptions.include = [];
|
|
406
|
+
for (const rel of relations) {
|
|
407
|
+
const relModel = model.associations[rel].target;
|
|
408
|
+
// noinspection TypeScriptUnresolvedVariable
|
|
409
|
+
queryOptions.include.push({
|
|
410
|
+
model: relModel,
|
|
411
|
+
as: model.associations[rel].options.as,
|
|
412
|
+
...autoQuery(relModel, fields[rel]),
|
|
413
|
+
});
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
if (merge.length) {
|
|
418
|
+
mergeQuery(queryOptions, ...merge);
|
|
419
|
+
}
|
|
420
|
+
return queryOptions;
|
|
421
|
+
}
|
|
422
|
+
query.autoQuery = autoQuery;
|
|
423
|
+
/**
|
|
424
|
+
* Return names of primary key fields for a given model.
|
|
425
|
+
*
|
|
426
|
+
* @param model - Model to read the keys of.
|
|
427
|
+
* @returns The primary key attribute names, in declaration order.
|
|
428
|
+
*/
|
|
429
|
+
function primaryKeys(model) {
|
|
430
|
+
const fields = model.rawAttributes;
|
|
431
|
+
return Object.keys(fields).filter(name => fields[name].primaryKey);
|
|
432
|
+
}
|
|
433
|
+
query.primaryKeys = primaryKeys;
|
|
434
|
+
/**
|
|
435
|
+
* Returns foreign key map for a given pair of parent model and related
|
|
436
|
+
* model.
|
|
437
|
+
*
|
|
438
|
+
* @param parent - Model on the referenced side.
|
|
439
|
+
* @param model - Model whose foreign keys are wanted.
|
|
440
|
+
*/
|
|
441
|
+
function foreignKeysMap(parent, model) {
|
|
442
|
+
let found = false;
|
|
443
|
+
const map = Object.keys(model.rawAttributes).reduce((fkMap, name) => {
|
|
444
|
+
const relation = model.rawAttributes[name]
|
|
445
|
+
.references;
|
|
446
|
+
if (relation &&
|
|
447
|
+
relation.model === parent.name &&
|
|
448
|
+
relation.key) {
|
|
449
|
+
fkMap[name] = relation.key;
|
|
450
|
+
found = true;
|
|
451
|
+
}
|
|
452
|
+
return fkMap;
|
|
453
|
+
}, {});
|
|
454
|
+
return found ? map : null;
|
|
455
|
+
}
|
|
456
|
+
query.foreignKeysMap = foreignKeysMap;
|
|
457
|
+
/**
|
|
458
|
+
* Prepares input for a given model and builds found relation arguments
|
|
459
|
+
*
|
|
460
|
+
* @param input - Values as the caller sent them.
|
|
461
|
+
* @param relations - Property names that are relations rather than columns.
|
|
462
|
+
* @param model - Model the values belong to.
|
|
463
|
+
* @param fields - Requested fields map, extended as relations are resolved.
|
|
464
|
+
* @param transaction - Transaction the writes run in.
|
|
465
|
+
* @param parent - Already-created parent entity, when there is one.
|
|
466
|
+
*/
|
|
467
|
+
function prepareInput(input, relations, model, fields, transaction, parent) {
|
|
468
|
+
const args = [];
|
|
469
|
+
for (const relation of relations) {
|
|
470
|
+
args.push([
|
|
471
|
+
model.associations[relation].target,
|
|
472
|
+
input[relation],
|
|
473
|
+
fields ? fields[relation] : undefined,
|
|
474
|
+
transaction,
|
|
475
|
+
relation,
|
|
476
|
+
]);
|
|
477
|
+
delete input[relation];
|
|
478
|
+
}
|
|
479
|
+
if (parent) {
|
|
480
|
+
const foreignKey = foreignKeysMap(parent.constructor, model);
|
|
481
|
+
if (foreignKey) {
|
|
482
|
+
Object.keys(foreignKey).forEach(property => {
|
|
483
|
+
if (!input[property]) {
|
|
484
|
+
input[property] = parent[foreignKey[property]];
|
|
485
|
+
}
|
|
486
|
+
});
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
return args;
|
|
490
|
+
}
|
|
491
|
+
/**
|
|
492
|
+
* Recursively creates entity and all it's relations from a given input
|
|
493
|
+
* using a given model.
|
|
494
|
+
*
|
|
495
|
+
* @param model - model class to map entity to
|
|
496
|
+
* @param input - data input object related to a given model
|
|
497
|
+
* @param fields - fields map to return on created entity
|
|
498
|
+
* @param transaction - transaction
|
|
499
|
+
*/
|
|
500
|
+
async function createEntity(model, input, fields, transaction) {
|
|
501
|
+
return await doCreateEntity(model, input, fields, transaction, undefined, undefined, false, !transaction);
|
|
502
|
+
}
|
|
503
|
+
query.createEntity = createEntity;
|
|
504
|
+
/**
|
|
505
|
+
* Recursively creates entity and all it's relations from a given input
|
|
506
|
+
* using a given model.
|
|
507
|
+
*
|
|
508
|
+
* @param model - Model to create.
|
|
509
|
+
* @param input - One entity, or several.
|
|
510
|
+
* @param fields - Requested fields map, deciding what comes back.
|
|
511
|
+
* @param transaction - Transaction to run in; one is opened when absent.
|
|
512
|
+
* @param parentProperty - Property on the parent this entity belongs to.
|
|
513
|
+
* @param noAppend - Skips attaching the result to the parent.
|
|
514
|
+
* @param parent - Parent entity to attach to.
|
|
515
|
+
* @param doCommit - Whether this call owns the transaction and commits it.
|
|
516
|
+
*/
|
|
517
|
+
async function doCreateEntity(model, input, fields, transaction, parentProperty, parent, noAppend = false, doCommit = true) {
|
|
518
|
+
// the package root is imported lazily at call time: a static import
|
|
519
|
+
// here would close a module cycle (index -> helpers -> index) that
|
|
520
|
+
// the synchronous require(esm) path used by CommonJS consumers
|
|
521
|
+
// cannot evaluate (bindings would stay undefined)
|
|
522
|
+
const { database } = await import('../index.js');
|
|
523
|
+
transaction =
|
|
524
|
+
transaction ||
|
|
525
|
+
(await database().transaction({
|
|
526
|
+
autocommit: false,
|
|
527
|
+
}));
|
|
528
|
+
// todo: this could be optimized through bulk operations
|
|
529
|
+
if (isArray(input) && parentProperty && parent) {
|
|
530
|
+
parent.appendChild(parentProperty, await Promise.all(input.map(inputItem => doCreateEntity(model, inputItem, fields, transaction, parentProperty, parent, true, doCommit))));
|
|
531
|
+
return parent;
|
|
532
|
+
}
|
|
533
|
+
if (fields) {
|
|
534
|
+
primaryKeys(model).forEach(name => !fields[name] && (fields[name] = false));
|
|
535
|
+
}
|
|
536
|
+
const fieldNames = Object.keys(input);
|
|
537
|
+
const relationArgs = prepareInput(input, filtered(model.associations, fieldNames), model, fields, transaction, parent);
|
|
538
|
+
const entity = new model(input);
|
|
539
|
+
await entity.save({
|
|
540
|
+
transaction,
|
|
541
|
+
returning: fields
|
|
542
|
+
? filtered(model.rawAttributes, Object.keys(fields), model)
|
|
543
|
+
: true,
|
|
544
|
+
});
|
|
545
|
+
if (!noAppend && parentProperty && parent) {
|
|
546
|
+
parent.appendChild(parentProperty, entity);
|
|
547
|
+
}
|
|
548
|
+
await Promise.all(relationArgs.map(async (args) => {
|
|
549
|
+
args.push(entity);
|
|
550
|
+
await doCreateEntity(...args);
|
|
551
|
+
}));
|
|
552
|
+
if (!parent && doCommit) {
|
|
553
|
+
await transaction.commit();
|
|
554
|
+
}
|
|
555
|
+
return entity;
|
|
556
|
+
}
|
|
557
|
+
/**
|
|
558
|
+
* The counting counterpart of {@link query.autoQuery}, for the same fields and filter.
|
|
559
|
+
*
|
|
560
|
+
* @remarks
|
|
561
|
+
* Builds the same query, then drops `attributes` and counts distinct primary keys
|
|
562
|
+
* instead — `distinct` matters because the joins `autoQuery` adds would otherwise
|
|
563
|
+
* multiply a row once per joined record and inflate the total.
|
|
564
|
+
*
|
|
565
|
+
* Pass it the same `fields` and filter as the data query, or the two disagree.
|
|
566
|
+
*
|
|
567
|
+
* @param model - Model to count rows of.
|
|
568
|
+
* @param fields - The same fields map used for the data query.
|
|
569
|
+
* @param merge - The same filter fragments, minus limit and order.
|
|
570
|
+
* @returns Count options ready for `Model.count()`.
|
|
571
|
+
*/
|
|
572
|
+
function autoCountQuery(model, fields, ...merge) {
|
|
573
|
+
const queryOptions = autoQuery(model, fields, ...merge);
|
|
574
|
+
if (queryOptions.attributes) {
|
|
575
|
+
delete queryOptions.attributes;
|
|
576
|
+
}
|
|
577
|
+
queryOptions.distinct = true;
|
|
578
|
+
queryOptions.col = primaryKeys(model).shift();
|
|
579
|
+
return queryOptions;
|
|
580
|
+
}
|
|
581
|
+
query.autoCountQuery = autoCountQuery;
|
|
582
|
+
/**
|
|
583
|
+
* Builds proper paging options query part
|
|
584
|
+
*
|
|
585
|
+
* @param pageOptions - obtained pagination input
|
|
586
|
+
* from remote
|
|
587
|
+
* @returns pagination part of the query
|
|
588
|
+
*/
|
|
589
|
+
function toLimitOptions(pageOptions) {
|
|
590
|
+
const page = {};
|
|
591
|
+
if (!pageOptions || !+pageOptions.limit) {
|
|
592
|
+
return page;
|
|
593
|
+
}
|
|
594
|
+
page.offset = 0;
|
|
595
|
+
page.limit = 0;
|
|
596
|
+
const count = pageOptions.count || 0;
|
|
597
|
+
if (pageOptions.offset) {
|
|
598
|
+
page.offset = pageOptions.offset;
|
|
599
|
+
}
|
|
600
|
+
if (pageOptions.limit) {
|
|
601
|
+
page.limit = Math.abs(pageOptions.limit);
|
|
602
|
+
}
|
|
603
|
+
if (pageOptions.limit < 0) {
|
|
604
|
+
if (page.offset === 0) {
|
|
605
|
+
page.offset = count - page.limit;
|
|
606
|
+
}
|
|
607
|
+
if (page.offset < 0) {
|
|
608
|
+
page.offset = 0;
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
return page;
|
|
612
|
+
}
|
|
613
|
+
query.toLimitOptions = toLimitOptions;
|
|
614
|
+
/**
|
|
615
|
+
* Ensures order by value is correct or returns default (ASC) if not. This
|
|
616
|
+
* would prevent from any possible injections or errors.
|
|
617
|
+
*
|
|
618
|
+
* @param value - Direction as the caller sent it.
|
|
619
|
+
* @returns `desc` for anything that reads as descending, `asc` otherwise.
|
|
620
|
+
*/
|
|
621
|
+
function toOrderDirection(value) {
|
|
622
|
+
if (String(value).toLocaleLowerCase() === 'desc') {
|
|
623
|
+
return OrderDirection.desc;
|
|
624
|
+
}
|
|
625
|
+
else {
|
|
626
|
+
return OrderDirection.asc;
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
/**
|
|
630
|
+
* Turns a serialized order into Sequelize's `order` option.
|
|
631
|
+
*
|
|
632
|
+
* @remarks
|
|
633
|
+
* One entry per property, in the order the object lists them, so the caller
|
|
634
|
+
* controls precedence. Directions are normalised rather than trusted — anything
|
|
635
|
+
* that does not read as descending becomes ascending, which is what keeps a value
|
|
636
|
+
* off the wire out of the statement. An empty or absent order yields no `order`
|
|
637
|
+
* at all rather than an empty one.
|
|
638
|
+
*
|
|
639
|
+
* @param orderBy - Property-to-direction map from the caller.
|
|
640
|
+
* @returns Options carrying `order`, or empty options.
|
|
641
|
+
*/
|
|
642
|
+
function toOrderOptions(orderBy) {
|
|
643
|
+
const order = {};
|
|
644
|
+
if (!orderBy) {
|
|
645
|
+
return order;
|
|
646
|
+
}
|
|
647
|
+
const fields = Object.keys(orderBy);
|
|
648
|
+
if (!fields.length) {
|
|
649
|
+
return order;
|
|
650
|
+
}
|
|
651
|
+
order.order = [];
|
|
652
|
+
for (const field of fields) {
|
|
653
|
+
order.order.push([
|
|
654
|
+
field,
|
|
655
|
+
toOrderDirection(orderBy[field]),
|
|
656
|
+
]);
|
|
657
|
+
}
|
|
658
|
+
return order;
|
|
659
|
+
}
|
|
660
|
+
query.toOrderOptions = toOrderOptions;
|
|
661
|
+
/**
|
|
662
|
+
* Matches a value, or no value at all.
|
|
663
|
+
*
|
|
664
|
+
* @remarks
|
|
665
|
+
* For the filter that means "these, and the rows where it is not set": the result
|
|
666
|
+
* is an `OR` over `null` and what you pass. Without it a `null` in a where clause
|
|
667
|
+
* compares rather than tests, and matches nothing.
|
|
668
|
+
*
|
|
669
|
+
* @param value - Value, or values, to accept alongside `null`.
|
|
670
|
+
* @returns A where fragment for one column.
|
|
671
|
+
*/
|
|
672
|
+
function orNull(value) {
|
|
673
|
+
if (isArray(value)) {
|
|
674
|
+
return { [Op.or]: [null, ...value] };
|
|
675
|
+
}
|
|
676
|
+
return { [Op.or]: [null, value] };
|
|
677
|
+
}
|
|
678
|
+
query.orNull = orNull;
|
|
679
|
+
/**
|
|
680
|
+
* Rich filters implementation. Actually by doing this we allow outside
|
|
681
|
+
* calls to replicate what sequelize does for us: building rich where
|
|
682
|
+
* clauses.
|
|
683
|
+
*
|
|
684
|
+
* @param filter - Serialized filter from the caller.
|
|
685
|
+
* @returns The `where` clause it describes.
|
|
686
|
+
*/
|
|
687
|
+
function parseFilter(filter) {
|
|
688
|
+
const clause = {};
|
|
689
|
+
if (Object.prototype.toString.call(filter) === '[object Object]') {
|
|
690
|
+
for (const op of Object.keys(filter)) {
|
|
691
|
+
if (FILTER_OPS[op]) {
|
|
692
|
+
clause[FILTER_OPS[op]] = parseFilter(filter[op]);
|
|
693
|
+
}
|
|
694
|
+
else {
|
|
695
|
+
clause[op] = parseFilter(filter[op]);
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
else {
|
|
700
|
+
// that's recursive value reached
|
|
701
|
+
return filter;
|
|
702
|
+
}
|
|
703
|
+
return clause;
|
|
704
|
+
}
|
|
705
|
+
/**
|
|
706
|
+
* This gives us an ability to simulate ILIKE, <, >, <=, >=, = right withing
|
|
707
|
+
* given values in the filter.
|
|
708
|
+
*
|
|
709
|
+
* @param prop - Column or operator name.
|
|
710
|
+
* @param data - Value, or a nested filter.
|
|
711
|
+
*/
|
|
712
|
+
function parseFilterValue(prop, data) {
|
|
713
|
+
const value = { [prop]: data };
|
|
714
|
+
if (typeof data !== 'string') {
|
|
715
|
+
return value;
|
|
716
|
+
}
|
|
717
|
+
if (RX_LIKE.test(data)) {
|
|
718
|
+
value[prop] = { [Op.iLike]: data };
|
|
719
|
+
}
|
|
720
|
+
else if (RX_GTE.test(data)) {
|
|
721
|
+
value[prop] = { [Op.gte]: parseValue(data.replace(RX_GTE, '')) };
|
|
722
|
+
}
|
|
723
|
+
else if (RX_GT.test(data)) {
|
|
724
|
+
value[prop] = { [Op.gt]: parseValue(data.replace(RX_GT, '')) };
|
|
725
|
+
}
|
|
726
|
+
else if (RX_LTE.test(data)) {
|
|
727
|
+
value[prop] = { [Op.lte]: parseValue(data.replace(RX_LTE, '')) };
|
|
728
|
+
}
|
|
729
|
+
else if (RX_LT.test(data)) {
|
|
730
|
+
value[prop] = { [Op.lt]: parseValue(data.replace(RX_LT, '')) };
|
|
731
|
+
}
|
|
732
|
+
else if (RX_EQ.test(data)) {
|
|
733
|
+
value[prop] = { [Op.eq]: parseValue(data.replace(RX_EQ, '')) };
|
|
734
|
+
}
|
|
735
|
+
return value;
|
|
736
|
+
}
|
|
737
|
+
/**
|
|
738
|
+
* Parses a given value
|
|
739
|
+
* @param value - Raw value from the filter.
|
|
740
|
+
* @returns The value as a date when it reads as one, unchanged otherwise.
|
|
741
|
+
*/
|
|
742
|
+
function parseValue(value) {
|
|
743
|
+
try {
|
|
744
|
+
const date = new Date(value);
|
|
745
|
+
if (date.toISOString() === value) {
|
|
746
|
+
return date;
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
catch {
|
|
750
|
+
/* not a date */
|
|
751
|
+
}
|
|
752
|
+
return +value + '' === value ? +value : value;
|
|
753
|
+
}
|
|
754
|
+
/**
|
|
755
|
+
* Turns a serializable filter into Sequelize `where` options.
|
|
756
|
+
*
|
|
757
|
+
* @remarks
|
|
758
|
+
* Each property is dispatched on its shape: a `$`-prefixed key becomes the
|
|
759
|
+
* matching Sequelize operator (see {@link FILTER_OPS}), an object with `start`
|
|
760
|
+
* and `end` becomes a `BETWEEN`, any other object is walked recursively as a
|
|
761
|
+
* nested filter, an array becomes an OR of its values, and a scalar is compared
|
|
762
|
+
* directly.
|
|
763
|
+
*
|
|
764
|
+
* A STRING value is inspected before it is compared, which is convenient and
|
|
765
|
+
* occasionally surprising. A `%` anywhere in it makes the comparison a
|
|
766
|
+
* case-insensitive `ILIKE`, and a leading `<=`, `>=`, `<`, `>` or `=` becomes
|
|
767
|
+
* that operator with the rest as the value, coerced to a number or a `Date` when
|
|
768
|
+
* it parses as one. So `'>=10'` and `'%abc%'` work with no operator key — and a
|
|
769
|
+
* literal value that happens to contain `%`, such as `'50% off'`, becomes a
|
|
770
|
+
* pattern match rather than an equality test. Use an explicit `$eq` where that
|
|
771
|
+
* matters.
|
|
772
|
+
*
|
|
773
|
+
* Empty is treated as absent, not as a contradiction: an empty array is skipped
|
|
774
|
+
* and a single-element array is unwrapped to the value. Given a falsy filter it
|
|
775
|
+
* returns `{}`, which means "no restriction" — so a caller cannot accidentally
|
|
776
|
+
* filter everything out by passing nothing.
|
|
777
|
+
*
|
|
778
|
+
* With `inputType`, a property matching one of that type's own properties is
|
|
779
|
+
* turned into a required `include` on the related model rather than a column
|
|
780
|
+
* comparison, which is how a filter reaches across a relation.
|
|
781
|
+
*
|
|
782
|
+
* @param filter - Filter from a caller. Modified in place as empties are cleared.
|
|
783
|
+
* @param inputType - Constructor describing which properties are relations.
|
|
784
|
+
* @returns Options carrying `where` and, where relations were filtered, `include`.
|
|
785
|
+
*/
|
|
786
|
+
function toWhereOptions(filter, inputType) {
|
|
787
|
+
if (!filter) {
|
|
788
|
+
return {};
|
|
789
|
+
}
|
|
790
|
+
clearObject(filter);
|
|
791
|
+
let inputData = null;
|
|
792
|
+
if (inputType) {
|
|
793
|
+
inputData = new inputType();
|
|
794
|
+
}
|
|
795
|
+
const options = {};
|
|
796
|
+
for (const prop of Object.keys(filter)) {
|
|
797
|
+
let data = filter[prop];
|
|
798
|
+
const inputDataProp = inputData && inputData[prop];
|
|
799
|
+
if (inputData && inputDataProp) {
|
|
800
|
+
const includeData = {
|
|
801
|
+
model: inputDataProp.model,
|
|
802
|
+
required: true,
|
|
803
|
+
...toWhereOptions(withRangeFilters(data), inputDataProp.input),
|
|
804
|
+
};
|
|
805
|
+
// NOTE: If included data contains fields which are empty,
|
|
806
|
+
// it should be deleted
|
|
807
|
+
clearObject(filter);
|
|
808
|
+
options.include = isArray(options.include)
|
|
809
|
+
? options.include.concat(includeData)
|
|
810
|
+
: [includeData];
|
|
811
|
+
continue;
|
|
812
|
+
}
|
|
813
|
+
if (isArray(data)) {
|
|
814
|
+
if (data.length === 0) {
|
|
815
|
+
continue;
|
|
816
|
+
}
|
|
817
|
+
if (data.length === 1) {
|
|
818
|
+
data = data[0];
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
if (data === undefined) {
|
|
822
|
+
continue;
|
|
823
|
+
}
|
|
824
|
+
options.where = options.where || {};
|
|
825
|
+
if (RX_OP.test(prop)) {
|
|
826
|
+
Object.assign(options.where, parseFilter({ [prop]: data }));
|
|
827
|
+
}
|
|
828
|
+
else if (data && data.start && data.end) {
|
|
829
|
+
// range filter
|
|
830
|
+
Object.assign(options.where, {
|
|
831
|
+
[prop]: { [Op.between]: [data.start, data.end] },
|
|
832
|
+
});
|
|
833
|
+
}
|
|
834
|
+
else if (Object.prototype.toString.call(data) === '[object Object]') {
|
|
835
|
+
Object.assign(options.where, { [prop]: parseFilter(data) });
|
|
836
|
+
}
|
|
837
|
+
else if (isArray(data)) {
|
|
838
|
+
Object.assign(options.where, {
|
|
839
|
+
[prop]: buildWhereFromArray(data),
|
|
840
|
+
});
|
|
841
|
+
}
|
|
842
|
+
else {
|
|
843
|
+
Object.assign(options.where, parseFilterValue(prop, data));
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
return options;
|
|
847
|
+
}
|
|
848
|
+
query.toWhereOptions = toWhereOptions;
|
|
849
|
+
/**
|
|
850
|
+
* ORs an array of filter values into one condition.
|
|
851
|
+
*
|
|
852
|
+
* @remarks
|
|
853
|
+
* Plain values are gathered into a single `IN`, while values carrying their own
|
|
854
|
+
* operator prefix become separate conditions, and the two groups are then ORed —
|
|
855
|
+
* so `['a', 'b', '>=10']` becomes `IN (a, b) OR >= 10` rather than three
|
|
856
|
+
* unrelated comparisons.
|
|
857
|
+
*
|
|
858
|
+
* @param data - Values to combine.
|
|
859
|
+
* @returns A `where` fragment for one column.
|
|
860
|
+
*/
|
|
861
|
+
function buildWhereFromArray(data) {
|
|
862
|
+
const ops = [];
|
|
863
|
+
const ins = [];
|
|
864
|
+
for (const value of data) {
|
|
865
|
+
if (RX_LIKE.test(value)) {
|
|
866
|
+
ops.push({ [Op.iLike]: value });
|
|
867
|
+
}
|
|
868
|
+
else if (RX_GTE.test(value)) {
|
|
869
|
+
ops.push({ [Op.gte]: parseValue(value.replace(RX_GTE, '')) });
|
|
870
|
+
}
|
|
871
|
+
else if (RX_GT.test(value)) {
|
|
872
|
+
ops.push({ [Op.gt]: parseValue(value.replace(RX_GT, '')) });
|
|
873
|
+
}
|
|
874
|
+
else if (RX_LTE.test(value)) {
|
|
875
|
+
ops.push({ [Op.lte]: parseValue(value.replace(RX_LTE, '')) });
|
|
876
|
+
}
|
|
877
|
+
else if (RX_LT.test(value)) {
|
|
878
|
+
ops.push({ [Op.lt]: parseValue(value.replace(RX_LT, '')) });
|
|
879
|
+
}
|
|
880
|
+
else if (RX_EQ.test(value)) {
|
|
881
|
+
ops.push({ [Op.eq]: parseValue(value.replace(RX_EQ, '')) });
|
|
882
|
+
}
|
|
883
|
+
else {
|
|
884
|
+
ins.push(value);
|
|
885
|
+
}
|
|
886
|
+
}
|
|
887
|
+
if (!ops.length && ins.length) {
|
|
888
|
+
return { [Op.in]: ins };
|
|
889
|
+
}
|
|
890
|
+
if (ins.length) {
|
|
891
|
+
ops.push({ [Op.in]: ins });
|
|
892
|
+
}
|
|
893
|
+
return { [Op.or]: ops };
|
|
894
|
+
}
|
|
895
|
+
query.buildWhereFromArray = buildWhereFromArray;
|
|
896
|
+
/**
|
|
897
|
+
* Rewrites `<column>Range` filter properties onto the columns they belong to.
|
|
898
|
+
*
|
|
899
|
+
* @remarks
|
|
900
|
+
* The convention that lets a range be filtered over RPC: a caller sends
|
|
901
|
+
* `durationRange: { start, end }` and this moves it to `duration`, where
|
|
902
|
+
* {@link query.toWhereOptions} turns it into a `BETWEEN`. Recognition is strict — the
|
|
903
|
+
* property name must end in `Range` and the value must have exactly the keys
|
|
904
|
+
* `start` and `end`, in either order. Anything else is left untouched and
|
|
905
|
+
* filtered as an ordinary value, and nested objects are walked so a range on a
|
|
906
|
+
* related model works too.
|
|
907
|
+
*
|
|
908
|
+
* Sending both `duration` and `durationRange` throws rather than choosing one.
|
|
909
|
+
*
|
|
910
|
+
* MUTATES and returns the filter it is given.
|
|
911
|
+
*
|
|
912
|
+
* @param filter - Filter to rewrite in place.
|
|
913
|
+
* @returns The same filter, with ranges moved onto their columns.
|
|
914
|
+
*/
|
|
915
|
+
function withRangeFilters(filter) {
|
|
916
|
+
if (!filter) {
|
|
917
|
+
return filter;
|
|
918
|
+
}
|
|
919
|
+
for (const prop of Object.keys(filter)) {
|
|
920
|
+
const col = prop.replace(RX_RANGE, '');
|
|
921
|
+
if (col === prop) {
|
|
922
|
+
// not a range filter
|
|
923
|
+
if (isObject(filter[prop])) {
|
|
924
|
+
withRangeFilters(filter[prop]);
|
|
925
|
+
}
|
|
926
|
+
continue;
|
|
927
|
+
}
|
|
928
|
+
const signature = Object.keys(filter[prop]) + '';
|
|
929
|
+
if (!~['start,end', 'end,start'].indexOf(signature)) {
|
|
930
|
+
continue; // not a range filter signature
|
|
931
|
+
}
|
|
932
|
+
if (filter[col]) {
|
|
933
|
+
throw new TypeError(`Only one of filtering options "${col}" or "${prop}" can be passed as filtering option!`);
|
|
934
|
+
}
|
|
935
|
+
filter[col] = filter[prop];
|
|
936
|
+
delete filter[prop];
|
|
937
|
+
}
|
|
938
|
+
return filter;
|
|
939
|
+
}
|
|
940
|
+
query.withRangeFilters = withRangeFilters;
|
|
941
|
+
/**
|
|
942
|
+
* Finds the include options for a model reached through a chain of includes.
|
|
943
|
+
*
|
|
944
|
+
* @remarks
|
|
945
|
+
* The way to reach into a query that is already built — to add a where clause to a
|
|
946
|
+
* join, say, without rebuilding the whole thing. The path is walked one model at a
|
|
947
|
+
* time, and it is CONSUMED as that happens, so pass a copy if the caller still
|
|
948
|
+
* needs it.
|
|
949
|
+
*
|
|
950
|
+
* @param queryOptions - Query to search.
|
|
951
|
+
* @param path - Models to follow, outermost first.
|
|
952
|
+
* @returns The matching include options, or `null` when the path does not resolve.
|
|
953
|
+
*/
|
|
954
|
+
function getInclude(queryOptions, path) {
|
|
955
|
+
const currentModel = path.shift();
|
|
956
|
+
for (const include of queryOptions.include || []) {
|
|
957
|
+
const model = include.model;
|
|
958
|
+
// noinspection JSIncompatibleTypesComparison
|
|
959
|
+
if (model === currentModel) {
|
|
960
|
+
if (!path.length) {
|
|
961
|
+
return include;
|
|
962
|
+
}
|
|
963
|
+
else {
|
|
964
|
+
return getInclude(include, path);
|
|
965
|
+
}
|
|
966
|
+
}
|
|
967
|
+
}
|
|
968
|
+
return null;
|
|
969
|
+
}
|
|
970
|
+
query.getInclude = getInclude;
|
|
971
|
+
/**
|
|
972
|
+
* Builds a Sequelize literal from a string or a template — the escape hatch for
|
|
973
|
+
* SQL that no query option can express.
|
|
974
|
+
*
|
|
975
|
+
* @remarks
|
|
976
|
+
* A literal is spliced into the statement exactly as given, so nothing about it
|
|
977
|
+
* is parsed, checked or escaped. That is the whole point of it, and the reason to
|
|
978
|
+
* keep each one as small as the job allows: a correlated subquery in a `where`, a
|
|
979
|
+
* window function in an `order`, an operator Sequelize has no name for. Runtime
|
|
980
|
+
* values belong in {@link query.E} rather than in the text.
|
|
981
|
+
*
|
|
982
|
+
* Used as a template tag, the substitutions used to be dropped and the literal
|
|
983
|
+
* parts joined with commas, so the example below produced
|
|
984
|
+
* `(SELECT COUNT(*) FROM "SomeTable" WHERE owner = ,) = 0` — accepted by the
|
|
985
|
+
* template, rejected by Postgres. They are now interpolated in order.
|
|
986
|
+
*
|
|
987
|
+
* {@link query.sql} refuses substitutions rather than interpolating them, and the
|
|
988
|
+
* difference is deliberate: a complete statement can carry bind parameters, so
|
|
989
|
+
* interpolating into one is a choice to avoid. A fragment handed to Sequelize as
|
|
990
|
+
* a literal has no bind channel, which leaves escaping as the only option.
|
|
991
|
+
*
|
|
992
|
+
* @param str - The SQL text, or the literal parts when used as a template tag.
|
|
993
|
+
* @param values - The substitutions, when used as a template tag.
|
|
994
|
+
* @returns The text as a Sequelize literal, ready to use as a query option value.
|
|
995
|
+
* @example
|
|
996
|
+
* ```typescript
|
|
997
|
+
* const owner = 3;
|
|
998
|
+
* const query = {
|
|
999
|
+
* where: L`(SELECT COUNT(*) FROM "SomeTable" WHERE owner = ${E(owner)}) = 0`,
|
|
1000
|
+
* };
|
|
1001
|
+
* ```
|
|
1002
|
+
*/
|
|
1003
|
+
function L(str, ...values) {
|
|
1004
|
+
if (typeof str === 'string') {
|
|
1005
|
+
return SequelizeLib.literal(str);
|
|
1006
|
+
}
|
|
1007
|
+
return SequelizeLib.literal(str.reduce((text, part, i) => text + String(values[i - 1]) + part));
|
|
1008
|
+
}
|
|
1009
|
+
query.L = L;
|
|
1010
|
+
/**
|
|
1011
|
+
* Renders a value as a SQL constant: a number as itself, a string quoted and
|
|
1012
|
+
* escaped, anything else as `NULL`.
|
|
1013
|
+
*
|
|
1014
|
+
* @remarks
|
|
1015
|
+
* The companion to {@link query.L} and the only safe way to get a runtime value
|
|
1016
|
+
* into a literal. Single quotes inside a string are doubled, which is what
|
|
1017
|
+
* Postgres requires — and what this did not do: a value of `O'Brien` came out as
|
|
1018
|
+
* a broken string constant, and a value chosen deliberately came out as SQL. The
|
|
1019
|
+
* same path escapes a dynamic view's parameters, so a view selected with
|
|
1020
|
+
* caller-supplied `viewParams` was open the same way.
|
|
1021
|
+
*
|
|
1022
|
+
* Only numbers and strings render as values. Booleans, dates, objects, `null` and
|
|
1023
|
+
* `undefined` all become `NULL` — so format a date as a string before passing it,
|
|
1024
|
+
* and do not reach for this to render a boolean.
|
|
1025
|
+
*
|
|
1026
|
+
* @param input - Value to render.
|
|
1027
|
+
* @returns The number itself, a quoted and escaped string, or `NULL`.
|
|
1028
|
+
*/
|
|
1029
|
+
function E(input) {
|
|
1030
|
+
if (typeof input === 'number') {
|
|
1031
|
+
return +input;
|
|
1032
|
+
}
|
|
1033
|
+
if (typeof input === 'string') {
|
|
1034
|
+
return `'${input.replace(RX_SQL_QUOTE, "''")}'`;
|
|
1035
|
+
}
|
|
1036
|
+
return 'NULL';
|
|
1037
|
+
}
|
|
1038
|
+
query.E = E;
|
|
1039
|
+
/**
|
|
1040
|
+
* Deletes properties from an object.
|
|
1041
|
+
*
|
|
1042
|
+
* @remarks
|
|
1043
|
+
* MUTATES what it is given and hands it back, so it composes into a call chain.
|
|
1044
|
+
* A falsy argument is returned untouched, which makes it safe on an optional
|
|
1045
|
+
* value.
|
|
1046
|
+
*
|
|
1047
|
+
* @param obj - Object to strip.
|
|
1048
|
+
* @param props - Property names to delete.
|
|
1049
|
+
* @returns The same object.
|
|
1050
|
+
*/
|
|
1051
|
+
function skip(obj, ...props) {
|
|
1052
|
+
if (!obj) {
|
|
1053
|
+
return obj;
|
|
1054
|
+
}
|
|
1055
|
+
for (const prop of props) {
|
|
1056
|
+
delete obj[prop];
|
|
1057
|
+
}
|
|
1058
|
+
return obj;
|
|
1059
|
+
}
|
|
1060
|
+
query.skip = skip;
|
|
1061
|
+
/**
|
|
1062
|
+
* Traverses given query object, lookups for includes matching
|
|
1063
|
+
* the given arguments of include options and overrides those are matching
|
|
1064
|
+
* by model and alias with the provided option.
|
|
1065
|
+
*
|
|
1066
|
+
* @param queryOptions - Query whose includes are to be overridden.
|
|
1067
|
+
* @param options - Include options to apply, matched by model and alias.
|
|
1068
|
+
* @returns The same query options.
|
|
1069
|
+
*/
|
|
1070
|
+
function overrideJoin(queryOptions, ...options) {
|
|
1071
|
+
if (!(queryOptions && queryOptions.include) || !options.length) {
|
|
1072
|
+
return queryOptions;
|
|
1073
|
+
}
|
|
1074
|
+
for (const { model, ...fields } of options) {
|
|
1075
|
+
let found = false;
|
|
1076
|
+
for (const include of queryOptions.include) {
|
|
1077
|
+
const as = fields.as;
|
|
1078
|
+
if (include === model ||
|
|
1079
|
+
(include.model === model && (!as || as === include.as))) {
|
|
1080
|
+
Object.assign(include, fields);
|
|
1081
|
+
found = true;
|
|
1082
|
+
}
|
|
1083
|
+
}
|
|
1084
|
+
if (!found) {
|
|
1085
|
+
queryOptions.include.push({
|
|
1086
|
+
model,
|
|
1087
|
+
...fields,
|
|
1088
|
+
});
|
|
1089
|
+
}
|
|
1090
|
+
}
|
|
1091
|
+
return queryOptions;
|
|
1092
|
+
}
|
|
1093
|
+
query.overrideJoin = overrideJoin;
|
|
1094
|
+
})(query || (query = {}));
|
|
1095
|
+
//# sourceMappingURL=query.js.map
|