@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,917 @@
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 { Graph } from './Graph.js';
25
+ export { AfterBulkCreate, AfterBulkDestroy, AfterBulkRestore, AfterBulkSync, AfterBulkUpdate, AfterConnect, AfterCreate, AfterDefine, AfterDestroy, AfterFind, AfterInit, AfterRestore, AfterSave, AfterSync, AfterUpdate, AfterUpsert, AfterValidate, AllowNull, Association, AutoIncrement, BaseAssociation, BeforeBulkCreate, BeforeBulkDestroy, BeforeBulkRestore, BeforeBulkSync, BeforeBulkUpdate, BeforeConnect, BeforeCount, BeforeCreate, BeforeDefine, BeforeDestroy, BeforeFind, BeforeFindAfterExpandIncludeAll, BeforeFindAfterOptions, BeforeInit, BeforeRestore, BeforeSave, BeforeSync, BeforeUpdate, BeforeUpsert, BeforeValidate, BelongsTo, BelongsToAssociation, BelongsToMany, BelongsToManyAssociation, Column, Comment, Contains, CreatedAt, DataType, Default, DefaultScope, DeletedAt, Equals, ForeignKey, HasAssociation, HasMany, HasOne, INFER_ALIAS_MAP, Index, Is, IsAfter, IsAlpha, IsAlphanumeric, IsArray, IsBefore, IsCreditCard, IsDate, IsDecimal, IsEmail, IsFloat, IsIP, IsIPv4, IsIPv6, IsIn, IsInt, IsLowercase, IsNull, IsNumeric, IsUUID, IsUppercase, IsUrl, Length, Max, Min, Model, Not, NotContains, NotEmpty, NotIn, NotNull, PrimaryKey, Scopes, Table, Unique, UpdatedAt, Validate, ValidationFailed, Validator, addAssociation, addAttribute, addAttributeOptions, addFieldToIndex, addHook, addOptions, addScopeOptions, addScopeOptionsGetter, annotateModelWithIndex, createIndexDecorator, getAssociations, getAssociationsByRelation, getAttributes, getHooks, getIndexes, getModelName, getModels, getOptions, getPreparedAssociationOptions, getScopeOptions, getScopeOptionsGetters, getSequelizeTypeByDesignType, implementHookDecorator, inferDataType, installHooks, isDataType, prepareArgs, prepareOptions, resolveModelGetter, resolveScope, resolveScopes, resolvesDeprecatedScopes, setAssociations, setAttributes, setHooks, setIndexes, setModelName, setOptions, setScopeOptionsGetters, verbose, } from 'sequelize-typescript';
26
+ import { QueryInterface as QueryInterfaceOrigin, } from 'sequelize';
27
+ import { DataType, Model, Sequelize as SequelizeOrigin, } from 'sequelize-typescript';
28
+ import { RX_MATCHER, RX_NAME_MATCHER, } from './decorators/index.js';
29
+ import { query } from './helpers/index.js';
30
+ var sql = query.sql;
31
+ var E = query.E;
32
+ /**
33
+ * Original toJSON method from sequelize's Model class.
34
+ */
35
+ const toJSON = Model.prototype.toJSON;
36
+ const RX_CREATE_VIEW = new RegExp('create\\s+(or\\s+replace\\s+)?' + '(materialized\\s+)?view\\s+(.*?)\\s+as', 'i');
37
+ const RX_SQL_END = /;$/;
38
+ const RX_RETURNING = /returning\s+\*/i;
39
+ const ALIAS_PATH_DELIMITER = '->';
40
+ const castNumber = (value) => +value;
41
+ const NUMBERS_MAP = new Map([
42
+ [DataType.BIGINT.name, castNumber],
43
+ [DataType.NUMBER.name, castNumber],
44
+ [DataType.INTEGER.name, castNumber],
45
+ [DataType.FLOAT.name, castNumber],
46
+ [DataType.REAL.name, castNumber],
47
+ [DataType.DECIMAL.name, castNumber],
48
+ [DataType.MEDIUMINT.name, castNumber],
49
+ [DataType.SMALLINT.name, castNumber],
50
+ [DataType.TINYINT.name, castNumber],
51
+ [DataType.DOUBLE.name, castNumber],
52
+ ]);
53
+ function fixReturningOptions(options) {
54
+ if (options &&
55
+ options.returning &&
56
+ Array.isArray(options.returning) &&
57
+ !options.returning.length) {
58
+ options.returning = false;
59
+ }
60
+ }
61
+ /**
62
+ * Wraps a query interface with view support and the widened `returning` option.
63
+ *
64
+ * @remarks
65
+ * Called once per connection. Every write method is replaced with one that maps an
66
+ * empty `returning` array to `false` first, `createView` and `dropView` are added, and
67
+ * the select-query generator is wrapped so a dynamic view's definition is substituted
68
+ * into the statement — at the top level and through joins.
69
+ *
70
+ * @param queryInterface - Sequelize's own interface, modified in place.
71
+ * @returns The same object, typed with the additions.
72
+ */
73
+ function override(queryInterface) {
74
+ const { insert, upsert, bulkInsert, update, bulkUpdate, bulkDelete, select, increment, rawSelect, queryGenerator, } = queryInterface;
75
+ const del = queryInterface.delete;
76
+ /**
77
+ * Inserts a new record
78
+ */
79
+ queryInterface.insert = function (instance, tableName, values, options) {
80
+ fixReturningOptions(options);
81
+ return insert.call(this, instance, tableName, values, options);
82
+ };
83
+ /**
84
+ * Inserts or Updates a record in the database
85
+ */
86
+ queryInterface.upsert = function (tableName, values, updateValues, model, options) {
87
+ fixReturningOptions(options);
88
+ return upsert.call(this, tableName, values, updateValues, model, options);
89
+ };
90
+ /**
91
+ * Inserts multiple records at once
92
+ */
93
+ queryInterface.bulkInsert = function (tableName, records, options, attributes) {
94
+ fixReturningOptions(options);
95
+ return bulkInsert.call(this, tableName, records, options, attributes);
96
+ };
97
+ /**
98
+ * Updates a row
99
+ */
100
+ queryInterface.update = function (instance, tableName, values, identifier, options) {
101
+ fixReturningOptions(options);
102
+ return update.call(this, instance, tableName, values, identifier, options);
103
+ };
104
+ /**
105
+ * Updates multiple rows at once
106
+ */
107
+ queryInterface.bulkUpdate = function (tableName, values, identifier, options, attributes) {
108
+ fixReturningOptions(options);
109
+ return bulkUpdate.call(this, tableName, values, identifier, options, attributes);
110
+ };
111
+ /**
112
+ * Deletes a row
113
+ */
114
+ queryInterface.delete = function (instance, tableName, identifier, options) {
115
+ fixReturningOptions(options);
116
+ return del.call(this, instance, tableName, identifier, options);
117
+ };
118
+ /**
119
+ * Deletes multiple rows at once
120
+ */
121
+ queryInterface.bulkDelete = function (tableName, identifier, options, model) {
122
+ fixReturningOptions(options);
123
+ return bulkDelete.call(this, tableName, identifier, options, model);
124
+ };
125
+ /**
126
+ * Increments a row value
127
+ */
128
+ queryInterface.increment = function (instance, tableName, values, identifier, options) {
129
+ fixReturningOptions(options);
130
+ return increment.call(this, instance, tableName, values, identifier, options);
131
+ };
132
+ /**
133
+ * Drops view from database
134
+ *
135
+ * @param viewName - view name to drop
136
+ * @param options - drop operation options
137
+ */
138
+ queryInterface.dropView = function (viewName, options = {}) {
139
+ const dropViewSql = `DROP VIEW IF EXISTS "${viewName}"${options.cascade ? ' CASCADE' : ''}`;
140
+ return this.sequelize.query(dropViewSql, this.sequelize.options);
141
+ };
142
+ /**
143
+ * Creates view in a database. Makes sure given view name corresponds to
144
+ * the name inside given create SQL query.
145
+ *
146
+ * @param viewName - view name to create
147
+ * @param viewDefinition - raw sql query to create the view
148
+ */
149
+ queryInterface.createView = function (viewName, viewDefinition) {
150
+ const rx = new RegExp(`\\s*create\\s+(or\\s+replace\\s+)?((temp|temporary)\\s+)?view\\s+"?${viewName}"?\\s+`, 'i');
151
+ if (!rx.test(viewDefinition)) {
152
+ throw new TypeError('Given view definition does not match given view name');
153
+ }
154
+ return this.sequelize.query(viewDefinition, this.sequelize.options);
155
+ };
156
+ /**
157
+ * Returns selected rows
158
+ */
159
+ queryInterface.select = function (model, tableName, options) {
160
+ fixReturningOptions(options);
161
+ return select.call(this, model, tableName, options);
162
+ };
163
+ /**
164
+ * Increments a row value
165
+ */
166
+ queryInterface.increment = function (instance, tableName, values, identifier, options) {
167
+ fixReturningOptions(options);
168
+ return increment.call(this, instance, tableName, values, identifier, options);
169
+ };
170
+ /**
171
+ * Selects raw without parsing the string into an object
172
+ */
173
+ queryInterface.rawSelect = function (tableName, options, attributeSelector, model) {
174
+ fixReturningOptions(options);
175
+ return rawSelect.call(this, tableName, options, attributeSelector, model);
176
+ };
177
+ /**
178
+ * Override queryGenerator behavior for DynamicViews on select queries
179
+ */
180
+ const { selectQuery } = queryGenerator;
181
+ // takes into account dynamic view can be included
182
+ function fixIncludes(options, sqlQuery, parentViewParams, path = '') {
183
+ const model = options.model;
184
+ const modelOptions = ((model || {}).options ||
185
+ {});
186
+ path = path
187
+ ? `${path}${ALIAS_PATH_DELIMITER}${options.as}`
188
+ : options.as || '';
189
+ if (modelOptions.isDynamicView &&
190
+ (options.viewParams || parentViewParams)) {
191
+ const viewParams = Object.assign({}, parentViewParams || {}, options.viewParams || {});
192
+ sqlQuery = sqlQuery.replace(`JOIN "${model.getTableName()}" AS "${path}"`, `JOIN (${model
193
+ .getViewDefinition(viewParams, true)
194
+ .replace(RX_SQL_END, '')}) AS "${path}"`);
195
+ }
196
+ if (options.includeMap) {
197
+ for (const prop of Object.keys(options.includeMap)) {
198
+ sqlQuery = fixIncludes(options.includeMap[prop], sqlQuery, parentViewParams, path);
199
+ }
200
+ }
201
+ return sqlQuery;
202
+ }
203
+ queryGenerator.selectQuery = (tableName, options, model) => {
204
+ const modelOptions = model.options;
205
+ let sqlQuery = selectQuery.call(queryGenerator, tableName, options, model);
206
+ const viewParams = Object.assign({}, modelOptions.viewParams);
207
+ if (modelOptions.isDynamicView && options.viewParams) {
208
+ Object.assign(viewParams, options.viewParams);
209
+ sqlQuery = sqlQuery.replace(`FROM "${tableName}" AS`, `FROM (${model
210
+ .getViewDefinition(viewParams, true)
211
+ .replace(RX_SQL_END, '')}) AS`);
212
+ }
213
+ return fixIncludes(options, sqlQuery, options.viewParams);
214
+ };
215
+ return queryInterface;
216
+ }
217
+ /**
218
+ * Sequelize's own connection class, taught about views, column indices and the
219
+ * widened `returning` option.
220
+ *
221
+ * @remarks
222
+ * Returned by `database()`, which is how a service normally gets one. Four things
223
+ * differ from the class it extends.
224
+ *
225
+ * Views are first-class: a model declared with `View` or `DynamicView` is created as
226
+ * a view after every table exists, dropped as a view, and skipped by the table sync.
227
+ * A dynamic view goes further — its definition carries `@{name}` placeholders, and
228
+ * the select-query generator substitutes them into the statement at query time, so
229
+ * one model can be read with different parameters, including when it is reached
230
+ * through a join.
231
+ *
232
+ * Column indices declared with `ColumnIndex` or `NullableIndex` are created as part
233
+ * of the same sync, which sequelize has no notion of at all.
234
+ *
235
+ * `returning` may name columns rather than being a boolean, both on the write methods
236
+ * and on a raw `query()`.
237
+ *
238
+ * @example
239
+ * ```typescript
240
+ * const orm = database(dbConfig); // a Sequelize, already connected
241
+ *
242
+ * await orm.sync(); // tables, then views, then indices
243
+ * await orm.sync({ withNoViews: true }); // tables and indices only
244
+ * ```
245
+ */
246
+ export class Sequelize extends SequelizeOrigin {
247
+ /**
248
+ * The query interface for this connection, with view support and the widened
249
+ * `returning` option.
250
+ *
251
+ * @remarks
252
+ * Sequelize's own interface is wrapped once, the first time this is called, and
253
+ * the same wrapped object is returned afterwards. The wrapper adds `createView`
254
+ * and `dropView`, makes every write method treat an empty `returning` array as
255
+ * `false`, and teaches the select-query generator to substitute a dynamic view's
256
+ * definition into the statement.
257
+ *
258
+ * @returns The wrapped query interface.
259
+ */
260
+ getQueryInterface() {
261
+ const self = this;
262
+ super.getQueryInterface();
263
+ if (typeof self.queryInterface.dropView !== 'function') {
264
+ self.queryInterface = override(self.queryInterface);
265
+ }
266
+ return self.queryInterface;
267
+ }
268
+ /**
269
+ * Defines a model from an attribute map rather than from a decorated class.
270
+ *
271
+ * @remarks
272
+ * Sequelize's own `define()` builds on its `Model`; this one builds on
273
+ * `BaseModel`, so a model defined this way gets the view handling, the widened
274
+ * `returning` and the serialization of this package. Decorated classes registered
275
+ * through `addModels()` are the usual route, and the one `database()` takes.
276
+ *
277
+ * @param modelName - Name to register the model under.
278
+ * @param attributes - Column definitions, as sequelize's own `define` takes them.
279
+ * @param options - Model options. `modelName` and `sequelize` are filled in.
280
+ * @returns The generated model class.
281
+ */
282
+ define(modelName, attributes, options) {
283
+ const opts = options || {};
284
+ opts.modelName = modelName;
285
+ opts.sequelize = this;
286
+ const model = class extends BaseModel {
287
+ };
288
+ model.init(attributes, opts);
289
+ return model;
290
+ }
291
+ /**
292
+ * Creates every table, then every view, then every column index.
293
+ *
294
+ * @remarks
295
+ * Sequelize's own sync knows about tables only. This one runs it first, then
296
+ * replaces the views declared with `View` or `DynamicView`, then creates the
297
+ * indices declared with `ColumnIndex` or `NullableIndex`. Views come before
298
+ * indices so an index declared on a materialized view has something to attach to,
299
+ * and both come after the tables a view selects from.
300
+ *
301
+ * The three passes used to overlap. The index pass was started but never waited
302
+ * for and its result was discarded, so this resolved while indices were still
303
+ * being created, and a failure in that pass became an unhandled rejection —
304
+ * fatal on any current Node — instead of rejecting here. The options were not
305
+ * forwarded to the view pass either, which left `withoutDrop` doing nothing. All
306
+ * three are fixed.
307
+ *
308
+ * @param options - Sequelize's own sync options, plus `withNoViews` and
309
+ * `withoutDrop`.
310
+ * @returns The connection, once every pass has finished.
311
+ */
312
+ sync(options) {
313
+ const withViews = !(options && options.withNoViews);
314
+ return super.sync(options).then(async (result) => {
315
+ if (withViews) {
316
+ await this.syncViews(options);
317
+ }
318
+ await this.syncIndices(options);
319
+ return result;
320
+ });
321
+ }
322
+ /**
323
+ * Creates the column indices declared across every registered model.
324
+ *
325
+ * @remarks
326
+ * Run by `sync()` once the tables and views are in place. Only models that
327
+ * declare at least one index are visited, and the models are done concurrently.
328
+ *
329
+ * @param options - Passed to each model, which does not currently read it.
330
+ * @returns Resolves once every index of every model exists.
331
+ */
332
+ syncIndices(options) {
333
+ return Promise.all(this.getModelsWithIndices().map(model => model.syncIndices(options)));
334
+ }
335
+ /**
336
+ * Replaces every model that is declared as a view.
337
+ *
338
+ * @remarks
339
+ * Each view is dropped and created again unless `withoutDrop` is set, and the
340
+ * views are done concurrently — so a view that selects from another view has no
341
+ * ordering guarantee, and the drop of the one it depends on would fail anyway.
342
+ * That combination is what `withoutDrop` is for.
343
+ *
344
+ * @param options - `withoutDrop` skips the drop; nothing else is read.
345
+ * @returns Resolves once every view has been replaced.
346
+ */
347
+ syncViews(options) {
348
+ const views = this.getViews();
349
+ return Promise.all(views.map(view => view.syncView(options)));
350
+ }
351
+ /**
352
+ * The registered models that declare at least one column index.
353
+ *
354
+ * @returns Model classes with a non-empty `indices` option, in registration
355
+ * order.
356
+ */
357
+ getModelsWithIndices() {
358
+ const models = [];
359
+ this.modelManager.models.forEach((model) => {
360
+ if (model &&
361
+ model.options &&
362
+ model.options.indices &&
363
+ model.options.indices.length) {
364
+ models.push(model);
365
+ }
366
+ });
367
+ return models;
368
+ }
369
+ /**
370
+ * The registered models that are declared as views.
371
+ *
372
+ * @remarks
373
+ * A model counts as a view once `View` or `DynamicView` has put `treatAsView` in
374
+ * its options, which is the same flag that makes the table sync skip it and
375
+ * `drop()` issue `DROP VIEW`.
376
+ *
377
+ * @returns Model classes flagged as views, in registration order.
378
+ */
379
+ getViews() {
380
+ const views = [];
381
+ this.modelManager.models.forEach((model) => {
382
+ if (model && model.options && model.options.treatAsView) {
383
+ views.push(model);
384
+ }
385
+ });
386
+ return views;
387
+ }
388
+ /**
389
+ * Runs a raw statement, honouring a `returning` column list.
390
+ *
391
+ * @remarks
392
+ * Given a non-empty `returning` array, the `RETURNING *` in the statement is
393
+ * rewritten to name exactly those columns, and every returned model instance
394
+ * remembers the list, so serializing it emits only those properties. Everything
395
+ * else is sequelize's own `query()`.
396
+ *
397
+ * The rewrite is textual and looks for `RETURNING *` specifically, so a statement
398
+ * that already names its columns is left alone — and one that has no `RETURNING`
399
+ * at all is not given one.
400
+ *
401
+ * @param sqlQuery - The statement, or a statement with its bind values.
402
+ * @param options - Query options, where `returning` may be a column list.
403
+ * @returns Whatever sequelize's own `query()` returns for these options.
404
+ */
405
+ query(sqlQuery, options) {
406
+ if (options &&
407
+ Array.isArray(options.returning) &&
408
+ options.returning.length) {
409
+ const sqlText = (typeof sqlQuery === 'string' ? sqlQuery : sqlQuery.query).replace(RX_RETURNING, `RETURNING ${options.returning
410
+ .map(field => `"${field}"`)
411
+ .join(', ')}`);
412
+ if (typeof sqlQuery === 'string') {
413
+ sqlQuery = sqlText;
414
+ }
415
+ else {
416
+ sqlQuery.query = sqlText;
417
+ }
418
+ }
419
+ const original = super.query;
420
+ return original.call(this, sqlQuery, options).then((entities) => {
421
+ if (!(entities && Array.isArray(entities) && entities.length)) {
422
+ return entities;
423
+ }
424
+ for (const entity of entities) {
425
+ // noinspection SuspiciousTypeOfGuard
426
+ if (entity instanceof BaseModel && options) {
427
+ // noinspection TypeScriptUnresolvedVariable
428
+ entity._options.returning = options.returning;
429
+ }
430
+ }
431
+ return entities;
432
+ });
433
+ }
434
+ }
435
+ /**
436
+ * The class every model in an `@imqueue` service extends.
437
+ *
438
+ * @remarks
439
+ * Sequelize's own `Model` with four additions.
440
+ *
441
+ * Views: a model declared with `View` or `DynamicView` is created and dropped as a
442
+ * view, skipped by the table sync, and has its numeric columns cast back to numbers
443
+ * after every finder, since a view returns them as strings.
444
+ *
445
+ * Indices: `ColumnIndex` and `NullableIndex` declarations become `CREATE INDEX`
446
+ * statements at sync time, which plain sequelize cannot express.
447
+ *
448
+ * Serialization: `toJSON()` honours the `returning` column list left behind by the
449
+ * last write, and picks up associated instances that the loaded attributes do not
450
+ * already cover.
451
+ *
452
+ * Associations as a graph: `toGraph()` walks them transitively, so a cycle can be
453
+ * found before a query walks into it.
454
+ *
455
+ * @example
456
+ * ```typescript
457
+ * @Table
458
+ * export class Lead extends BaseModel<Lead> {
459
+ * @PrimaryKey
460
+ * @AutoIncrement
461
+ * @Column(DataType.BIGINT)
462
+ * public readonly id: number;
463
+ *
464
+ * @AllowNull(false)
465
+ * @Column(DataType.STRING)
466
+ * public name: string;
467
+ *
468
+ * @CreatedBy()
469
+ * @Column(DataType.BIGINT)
470
+ * public createdBy: number;
471
+ * }
472
+ * ```
473
+ */
474
+ export class BaseModel extends Model {
475
+ /**
476
+ * Drops this model's relation: `DROP VIEW` for a view, `DROP TABLE` otherwise.
477
+ *
478
+ * @param options - Sequelize's own drop options; `cascade` applies to both.
479
+ * @returns The result of the drop statement.
480
+ */
481
+ static drop(options) {
482
+ const self = this;
483
+ const method = self.options && self.options.treatAsView ? 'dropView' : 'dropTable';
484
+ // noinspection TypeScriptUnresolvedVariable
485
+ return self.QueryInterface[method](self.getTableName(), options);
486
+ }
487
+ /**
488
+ * Creates this model's table, or does nothing when the model is a view.
489
+ *
490
+ * @remarks
491
+ * A view is skipped deliberately: it normally selects from tables that do not
492
+ * exist yet, so views are left to the second pass `Sequelize.sync()` runs once
493
+ * every table is there.
494
+ *
495
+ * @param options - Sequelize's own sync options.
496
+ * @returns Resolves when the table exists, or immediately for a view.
497
+ */
498
+ static sync(options) {
499
+ if (this.options && this.options.treatAsView) {
500
+ // all views skipped until all tables defined
501
+ return Promise.resolve();
502
+ }
503
+ return super.sync(options);
504
+ }
505
+ /**
506
+ * Creates this view, replacing whatever definition is in the database.
507
+ *
508
+ * @remarks
509
+ * Drops the view first unless `withoutDrop` is set. That drop is why a view
510
+ * another view selects from cannot be replaced this way, and why `withoutDrop`
511
+ * exists.
512
+ *
513
+ * Only meaningful on a model declared with `View` or `DynamicView` — anything
514
+ * else has no definition to create.
515
+ *
516
+ * @param options - `withoutDrop` skips the drop.
517
+ * @returns The result of the create statement.
518
+ */
519
+ static syncView(options) {
520
+ const self = this;
521
+ // noinspection TypeScriptUnresolvedVariable
522
+ const queryInterface = self.QueryInterface || self.queryInterface;
523
+ if (options && options.withoutDrop) {
524
+ return queryInterface.createView(self.getTableName(), self.getViewDefinition());
525
+ }
526
+ return queryInterface
527
+ .dropView(self.getTableName())
528
+ .then(() => queryInterface.createView(self.getTableName(), self.getViewDefinition()));
529
+ }
530
+ /**
531
+ * This view's SQL, with any dynamic parameters substituted in.
532
+ *
533
+ * @remarks
534
+ * Parameters are merged in two layers: the defaults given to `DynamicView`, then
535
+ * whatever is passed here. Each `@{name}` placeholder in the definition is
536
+ * replaced with the escaped value, which is what makes it safe to pass a caller's
537
+ * input — though only numbers and strings render as values, and everything else,
538
+ * including a missing parameter, becomes `NULL`.
539
+ *
540
+ * @param viewParams - Values overriding the decorated defaults.
541
+ * @param asQuery - Strips the leading create-view clause, leaving a statement
542
+ * that can be embedded as a subquery. This is how a dynamic view is spliced
543
+ * into a `FROM` or a join.
544
+ * @returns The definition, whitespace-normalised and ending in a semicolon.
545
+ */
546
+ static getViewDefinition(viewParams = {}, asQuery = false) {
547
+ const self = this;
548
+ let viewDef = self.options.viewDefinition || '';
549
+ viewParams = Object.assign({}, self.options.viewParams, viewParams || {});
550
+ if (self.options.isDynamicView) {
551
+ (viewDef.match(RX_MATCHER) || []).forEach(param => {
552
+ // noinspection JSUnusedLocalSymbols
553
+ const [_, name] = param.match(RX_NAME_MATCHER) || ['', ''];
554
+ const RX_PARAM = new RegExp(`@{${name}}`, 'g');
555
+ viewDef = viewDef.replace(RX_PARAM, E(viewParams[name]) + '');
556
+ });
557
+ }
558
+ if (asQuery) {
559
+ viewDef = viewDef.replace(RX_CREATE_VIEW, '');
560
+ }
561
+ return sql(viewDef);
562
+ }
563
+ /**
564
+ * Creates every column index declared on this model.
565
+ *
566
+ * @remarks
567
+ * Reads the declarations left behind by `ColumnIndex` and `NullableIndex` and
568
+ * creates them concurrently. A model that declares none resolves immediately —
569
+ * it used to throw, which made this unsafe to call on anything but a model
570
+ * `Sequelize.getModelsWithIndices()` had already picked out.
571
+ *
572
+ * @param _options - Accepted for symmetry with the other sync methods, not read.
573
+ * @returns Resolves once every index of this model exists.
574
+ */
575
+ static syncIndices(_options) {
576
+ const indices = this.options.indices || [];
577
+ return Promise.all(indices.map((indexOptions, i) => this.syncIndex(indexOptions.column, indexOptions.options, i + 1)));
578
+ }
579
+ /**
580
+ * Builds and runs the statements that create one column index.
581
+ *
582
+ * @remarks
583
+ * Each `ColumnIndexOptions` field maps to a clause of the `CREATE INDEX`. The
584
+ * index is dropped first unless `safe` is set, so the declaration in the code
585
+ * always wins over what is in the database; with `safe`, an existing index is
586
+ * left exactly as it is and only a missing one is created.
587
+ *
588
+ * Five of those options used to be placed where Postgres does not accept them —
589
+ * `USING` ahead of `ON`, and `COLLATE`, the operator class, the sort order and
590
+ * the nulls position after the closing parenthesis rather than inside it — so
591
+ * `method`, `collation`, `opClass`, `order` and `nullsFirst` could not be used at
592
+ * all. `include` was declared and never emitted. Both are fixed, and the reason
593
+ * neither was noticed is the promise bug below: the statement failed, and the
594
+ * rejection went nowhere.
595
+ *
596
+ * The two statements used to be attached to the same already-resolved promise
597
+ * rather than chained onto each other, so they were issued together and this
598
+ * returned before either had run — the create could reach the server ahead of the
599
+ * drop, and a failure of either became an unhandled rejection rather than
600
+ * rejecting here. They now run in order, and the returned promise waits for them.
601
+ *
602
+ * @param column - Column the index is declared on.
603
+ * @param options - The declared index options.
604
+ * @param position - Ordinal used to name the index when `name` is not given,
605
+ * which is what keeps two indices on one column from colliding.
606
+ * @returns Resolves once the index exists.
607
+ */
608
+ static syncIndex(column, options, position) {
609
+ const self = this;
610
+ const indexName = options.name || `${this.getTableName()}_${column}_idx${position}`;
611
+ // noinspection TypeScriptUnresolvedVariable
612
+ const queryInterface = self.QueryInterface || self.queryInterface;
613
+ const concurrently = options.concurrently ? ' CONCURRENTLY' : '';
614
+ // Everything that describes the KEY belongs inside the parentheses, and
615
+ // USING belongs before them: `ON "t" USING btree ("c" COLLATE x DESC)`.
616
+ // Emitted after the closing paren — and USING ahead of ON — Postgres
617
+ // rejects the statement outright, so `method`, `collation`, `opClass`,
618
+ // `order` and `nullsFirst` could not be used at all. It went unnoticed
619
+ // because this method discarded its own rejections.
620
+ const key = [
621
+ options.expression ? `(${options.expression})` : `"${column}"`,
622
+ options.collation ? `COLLATE ${options.collation}` : '',
623
+ options.opClass || '',
624
+ options.order || '',
625
+ options.nullsFirst === true
626
+ ? 'NULLS FIRST'
627
+ : options.nullsFirst === false
628
+ ? 'NULLS LAST'
629
+ : '',
630
+ ]
631
+ .filter(Boolean)
632
+ .join(' ');
633
+ const covered = options.include || [];
634
+ const include = covered.length
635
+ ? ` INCLUDE (${covered.map(name => `"${name}"`).join(', ')})`
636
+ : '';
637
+ let chain = Promise.resolve();
638
+ if (!options.safe) {
639
+ chain = chain.then(() => queryInterface.sequelize.query(`DROP INDEX${concurrently} IF EXISTS "${indexName}"`));
640
+ }
641
+ // noinspection TypeScriptUnresolvedVariable,PointlessBooleanExpressionJS
642
+ return chain.then(() => queryInterface.sequelize.query(`CREATE${options.unique ? ' UNIQUE' : ''} INDEX` +
643
+ `${concurrently}${options.safe ? ' IF NOT EXISTS' : ''}` +
644
+ ` "${indexName}" ON "${this.getTableName()}"` +
645
+ `${options.method ? ` USING ${options.method}` : ''}` +
646
+ ` (${key})${include}` +
647
+ `${options.tablespace
648
+ ? ` TABLESPACE ${options.tablespace}`
649
+ : ''}` +
650
+ `${options.predicate ? ` WHERE ${options.predicate}` : ''}`));
651
+ }
652
+ // Make sure finders executed on views properly map numeric types
653
+ /**
654
+ * Search for multiple instances.
655
+ *
656
+ * @remarks
657
+ * Delegates to Sequelize's own `findAll`, then re-maps numeric columns, which
658
+ * a view returns as strings.
659
+ *
660
+ * @param options - Find options; `viewParams` applies to a dynamic view.
661
+ * @returns The matching instances.
662
+ */
663
+ static findAll(options) {
664
+ const method = super.findAll;
665
+ const original = method.call(this, options);
666
+ if (!this.options.treatAsView) {
667
+ return original;
668
+ }
669
+ return original.then((result) => {
670
+ if (result && !Array.isArray(result)) {
671
+ return result.fixNumbers();
672
+ }
673
+ else if (result) {
674
+ result.map((entity) => entity.fixNumbers());
675
+ }
676
+ return result;
677
+ });
678
+ }
679
+ // noinspection JSAnnotator
680
+ /**
681
+ * Finds one instance by primary key.
682
+ *
683
+ * @remarks
684
+ * Sequelize's own `findByPk` with one addition: on a model declared as a view, the
685
+ * numeric columns of the result are cast back to numbers.
686
+ *
687
+ * @param identifier - Primary key value to look for.
688
+ * @param options - Find options, minus `where`, which the key supplies.
689
+ * @returns The instance, or `null` when no row matches.
690
+ */
691
+ static findByPk(identifier, options) {
692
+ const method = super.findByPk;
693
+ const original = method.call(this, identifier, options);
694
+ if (!this.options.treatAsView) {
695
+ return original;
696
+ }
697
+ return original.then((result) => {
698
+ if (result) {
699
+ result.fixNumbers();
700
+ }
701
+ return result;
702
+ });
703
+ }
704
+ // noinspection JSAnnotator
705
+ /**
706
+ * Finds the first instance matching the options.
707
+ *
708
+ * @remarks
709
+ * Sequelize's own `findOne` with one addition: on a model declared as a view, the
710
+ * numeric columns of the result are cast back to numbers.
711
+ *
712
+ * @param options - Find options; `viewParams` applies to a dynamic view.
713
+ * @returns The instance, or `null` when no row matches.
714
+ */
715
+ static findOne(options) {
716
+ const method = super.findOne;
717
+ const original = method.call(this, options);
718
+ if (!this.options.treatAsView) {
719
+ return original;
720
+ }
721
+ return original.then((result) => {
722
+ if (result) {
723
+ result.fixNumbers();
724
+ }
725
+ return result;
726
+ });
727
+ }
728
+ /**
729
+ * Forgets the `returning` column list, so serializing emits every loaded column
730
+ * again.
731
+ *
732
+ * @remarks
733
+ * A write that named its returning columns leaves that list on the instance, and
734
+ * `toJSON()` honours it from then on. Call this when the same instance is reused
735
+ * for something that should serialize in full.
736
+ *
737
+ * @returns This instance, for chaining.
738
+ */
739
+ restoreSerialization() {
740
+ // noinspection TypeScriptUnresolvedVariable
741
+ delete this._options.returning;
742
+ return this;
743
+ }
744
+ /**
745
+ * Attaches related data to this instance as though it had been joined in.
746
+ *
747
+ * @remarks
748
+ * Sets the property, and when a `returning` list is in force adds the name to it
749
+ * so the property survives serialization. That is the point of it: data fetched
750
+ * by a second query would otherwise be dropped by `toJSON()`.
751
+ *
752
+ * @param name - Property to set, normally an association name.
753
+ * @param data - What to attach: an instance, an array of them, or anything else.
754
+ * @returns This instance, for chaining.
755
+ */
756
+ appendChild(name, data) {
757
+ // noinspection TypeScriptUnresolvedVariable
758
+ const returning = this._options.returning;
759
+ if (returning &&
760
+ Array.isArray(returning) &&
761
+ !~returning.indexOf(name)) {
762
+ returning.push(name);
763
+ }
764
+ this[name] = data;
765
+ return this;
766
+ }
767
+ /**
768
+ * Serializes this instance, honouring the `returning` column list.
769
+ *
770
+ * @remarks
771
+ * Sequelize's own `toJSON()` emits the loaded attributes. This one also picks up
772
+ * an associated instance, or an array of them, that those attributes do not
773
+ * already cover, and then — if the last write named the columns to return — drops
774
+ * every property that was not named.
775
+ *
776
+ * @returns A plain object, ready to send over the wire.
777
+ */
778
+ toJSON() {
779
+ const serialized = toJSON.call(this);
780
+ const props = Object.keys(this);
781
+ // noinspection TypeScriptUnresolvedVariable
782
+ const returning = this._options.returning;
783
+ for (const prop of props) {
784
+ this.verifyProperty(prop, serialized);
785
+ }
786
+ if (Array.isArray(returning)) {
787
+ const serializedProps = Object.keys(serialized);
788
+ for (const prop of serializedProps) {
789
+ if (!~returning.indexOf(prop)) {
790
+ delete serialized[prop];
791
+ }
792
+ }
793
+ }
794
+ return serialized;
795
+ }
796
+ /**
797
+ * Casts this instance's numeric columns back to numbers.
798
+ *
799
+ * @remarks
800
+ * Numeric columns selected through a view arrive as strings, so the finders on
801
+ * this class re-cast them after every select on a view, and this is the method
802
+ * they use. It is public for the cases they do not cover, a raw query being the
803
+ * common one.
804
+ *
805
+ * Only the columns the model declares are looked at, and the instance is changed
806
+ * in place.
807
+ *
808
+ * @returns This instance, for chaining.
809
+ */
810
+ fixNumbers() {
811
+ const model = this.sequelize.models[this.constructor.name];
812
+ const columns = Object.keys(model.rawAttributes);
813
+ for (const column of columns) {
814
+ const value = this[column];
815
+ if (value === undefined || value === null) {
816
+ continue;
817
+ }
818
+ const cast = NUMBERS_MAP.get(model.rawAttributes[column].type.constructor.name);
819
+ if (cast) {
820
+ this[column] = cast(value);
821
+ }
822
+ }
823
+ return this;
824
+ }
825
+ /**
826
+ * Serializes one property that sequelize's own `toJSON()` left out.
827
+ *
828
+ * @param prop - Property to check.
829
+ * @param serialized - Result being built, added to in place.
830
+ */
831
+ verifyProperty(prop, serialized) {
832
+ // add more skipping props if needed...
833
+ if (~['__eagerlyLoadedAssociations'].indexOf(prop)) {
834
+ return;
835
+ }
836
+ const val = this[prop];
837
+ if (!serialized[prop] && val !== this && val instanceof Model) {
838
+ serialized[prop] = toJSON.call(val);
839
+ }
840
+ if (val instanceof Array) {
841
+ this.verifyArray(val, prop, serialized);
842
+ }
843
+ }
844
+ /**
845
+ * Serializes an array property element by element.
846
+ *
847
+ * @param arr - Array to serialize.
848
+ * @param prop - Property it belongs to.
849
+ * @param serialized - Result being built, added to in place.
850
+ */
851
+ verifyArray(arr, prop, serialized) {
852
+ if (!serialized[prop]) {
853
+ serialized[prop] = [];
854
+ }
855
+ for (let i = 0; i < arr.length; i++) {
856
+ const val = this[prop][i];
857
+ if (val instanceof Model) {
858
+ serialized[prop][i] = toJSON.call(val);
859
+ }
860
+ serialized[prop][i] =
861
+ val && val.toJSON
862
+ ? val.toJSON()
863
+ : JSON.parse(JSON.stringify(val));
864
+ }
865
+ }
866
+ /**
867
+ * Builds a graph of this model's associations, following them transitively.
868
+ *
869
+ * @remarks
870
+ * Every association becomes an edge, and a many-to-many contributes two: this
871
+ * model to the through-model, and the through-model to the target. Each target is
872
+ * then walked in turn, and an association already present as an edge is not
873
+ * followed again, which is what stops a cycle from recursing forever.
874
+ *
875
+ * The reason to have it is to find those cycles before a query does — a cycle in
876
+ * the graph is a query that can be asked to include itself.
877
+ *
878
+ * @param graph - Graph to add to. A fresh one by default, and passing the same
879
+ * one is what makes the recursive calls accumulate into a single graph.
880
+ * @returns The graph, so the outermost call can use it.
881
+ */
882
+ static toGraph(graph = new Graph()) {
883
+ if (!graph.hasVertex(this)) {
884
+ graph.addVertex(this);
885
+ }
886
+ for (const field of Object.keys(this.associations)) {
887
+ const relation = this.associations[field];
888
+ const { target, options } = relation;
889
+ const through = options && options.through && options.through.model;
890
+ if (through && graph.hasEdge(this, through)) {
891
+ continue;
892
+ }
893
+ if (through) {
894
+ graph.addEdge(this, through);
895
+ through.toGraph(graph);
896
+ if (target && graph.hasEdge(through, target)) {
897
+ continue;
898
+ }
899
+ if (target && !graph.hasVertex(target)) {
900
+ graph.addEdge(through, target);
901
+ target.toGraph(graph);
902
+ }
903
+ }
904
+ else {
905
+ if (target && graph.hasEdge(this, target)) {
906
+ continue;
907
+ }
908
+ if (target) {
909
+ graph.addEdge(this, target);
910
+ target.toGraph(graph);
911
+ }
912
+ }
913
+ }
914
+ return graph;
915
+ }
916
+ }
917
+ //# sourceMappingURL=BaseModel.js.map