@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.
Files changed (63) hide show
  1. package/CHANGELOG.md +50 -0
  2. package/CONTRIBUTING.md +58 -0
  3. package/CONTRIBUTION-TERMS.md +79 -0
  4. package/LICENSE +585 -0
  5. package/README.md +94 -0
  6. package/SECURITY.md +41 -0
  7. package/index.d.ts +86 -0
  8. package/index.js +87 -0
  9. package/package.json +75 -0
  10. package/src/BaseModel.d.ts +695 -0
  11. package/src/BaseModel.js +917 -0
  12. package/src/Graph.d.ts +215 -0
  13. package/src/Graph.js +257 -0
  14. package/src/decorators/AssociatedWith.d.ts +94 -0
  15. package/src/decorators/AssociatedWith.js +71 -0
  16. package/src/decorators/ColumnIndex.d.ts +206 -0
  17. package/src/decorators/ColumnIndex.js +98 -0
  18. package/src/decorators/CreatedBy.d.ts +27 -0
  19. package/src/decorators/CreatedBy.js +84 -0
  20. package/src/decorators/DeletedBy.d.ts +30 -0
  21. package/src/decorators/DeletedBy.js +89 -0
  22. package/src/decorators/DynamicView.d.ts +124 -0
  23. package/src/decorators/DynamicView.js +113 -0
  24. package/src/decorators/Emittable.d.ts +39 -0
  25. package/src/decorators/Emittable.js +42 -0
  26. package/src/decorators/NullableIndex.d.ts +77 -0
  27. package/src/decorators/NullableIndex.js +64 -0
  28. package/src/decorators/UpdatedBy.d.ts +27 -0
  29. package/src/decorators/UpdatedBy.js +105 -0
  30. package/src/decorators/View.d.ts +87 -0
  31. package/src/decorators/View.js +93 -0
  32. package/src/decorators/index.d.ts +32 -0
  33. package/src/decorators/index.js +33 -0
  34. package/src/helpers/index.d.ts +24 -0
  35. package/src/helpers/index.js +25 -0
  36. package/src/helpers/js.d.ts +61 -0
  37. package/src/helpers/js.js +88 -0
  38. package/src/helpers/query.d.ts +445 -0
  39. package/src/helpers/query.js +1095 -0
  40. package/src/index.d.ts +162 -0
  41. package/src/index.js +223 -0
  42. package/src/types/DataPage.d.ts +52 -0
  43. package/src/types/DataPage.js +2 -0
  44. package/src/types/FieldsInput.d.ts +41 -0
  45. package/src/types/FieldsInput.js +75 -0
  46. package/src/types/FilterInput.d.ts +136 -0
  47. package/src/types/FilterInput.js +291 -0
  48. package/src/types/JsonObject.d.ts +16 -0
  49. package/src/types/JsonObject.js +50 -0
  50. package/src/types/OrderByInput.d.ts +45 -0
  51. package/src/types/OrderByInput.js +80 -0
  52. package/src/types/PaginationInput.d.ts +44 -0
  53. package/src/types/PaginationInput.js +90 -0
  54. package/src/types/index.d.ts +30 -0
  55. package/src/types/index.js +31 -0
  56. package/src/types/ranges/DateRange.d.ts +27 -0
  57. package/src/types/ranges/DateRange.js +69 -0
  58. package/src/types/ranges/IRange.d.ts +47 -0
  59. package/src/types/ranges/IRange.js +2 -0
  60. package/src/types/ranges/NumericRange.d.ts +19 -0
  61. package/src/types/ranges/NumericRange.js +61 -0
  62. package/src/types/ranges/index.d.ts +26 -0
  63. 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