mythix-orm-sql-base 1.9.6 → 1.10.1

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/.biblorc.js ADDED
@@ -0,0 +1,30 @@
1
+ 'use strict';
2
+
3
+ /* global __dirname */
4
+
5
+ const Path = require('path');
6
+
7
+ module.exports = {
8
+ rootDir: __dirname,
9
+ inputDir: Path.resolve(__dirname),
10
+ outputDir: Path.resolve(__dirname, '..', 'mythix-orm-sql-base.wiki'),
11
+ files: [
12
+ {
13
+ include: /\/lib\/.*\.js$/,
14
+ parser: 'typescript',
15
+ compiler: 'typescript',
16
+ },
17
+ {
18
+ include: /\/docs\/.*\.md$/,
19
+ parser: 'markdown',
20
+ compiler: 'markdown',
21
+ },
22
+ ],
23
+ exclude: [
24
+ /node_modules|\/spec\//
25
+ ],
26
+ generatorOptions: {
27
+ repositoryURL: 'https://github.com/th317erd/mythix-orm-sql-base',
28
+ baseURL: './',
29
+ },
30
+ };
package/README.md CHANGED
@@ -1,7 +1,5 @@
1
1
  # mythix-orm-sql-base
2
2
 
3
- SQL base support for Mythix ORM
3
+ SQL base support for [Mythix ORM](https://www.npmjs.com/package/mythix-orm).
4
4
 
5
- Mythix ORM aims to replace Sequelize and the few other terrible solutions that the poor destitute Node community has to work with. Mythix ORM is not yet quite ready for prime time however, so please check back soon!
6
-
7
- This module isn't intended to be used by itself. It is a support module for other SQL-based database drivers.
5
+ This module isn't intended to be used by itself. It is a support module for other SQL database drivers.
package/docs/Home.md ADDED
@@ -0,0 +1,3 @@
1
+ SQL base support for [Mythix ORM](https://www.npmjs.com/package/mythix-orm).
2
+
3
+ This module isn't intended to be used by itself. It is a support module for other SQL database drivers.
@@ -1,5 +1,6 @@
1
1
  'use strict';
2
2
 
3
+ const SqlString = require('sqlstring');
3
4
  const Nife = require('nife');
4
5
  const UUID = require('uuid');
5
6
  const {
@@ -13,11 +14,88 @@ const {
13
14
  const SQLQueryGeneratorBase = require('./sql-query-generator-base');
14
15
 
15
16
  const SAVE_POINT_NAME_CHARS = [ 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P' ];
16
- const MODEL_RELATIONS = Symbol.for('_mythixModelRelations');
17
-
17
+ const MODEL_RELATIONS = Symbol.for('@_mythix/orm-sql-base/SQLConnectionBase/ModelRelations');
18
+
19
+ /// `SQLConnectionBase` is a support class for all other
20
+ /// SQL connection drivers built for Mythix ORM. It isn't
21
+ /// intended to be used on its own, but rather to add SQL
22
+ /// support to all other Mythix ORM SQL connections.
23
+ ///
24
+ /// Extends: [ConnectionBase](https://github.com/th317erd/mythix-orm/wiki/ConnectionBase)
18
25
  class SQLConnectionBase extends ConnectionBase {
19
26
  static DefaultQueryGenerator = SQLQueryGeneratorBase;
20
27
 
28
+ /// The low-level DB interface for escaping a
29
+ /// value. By default this function uses the
30
+ /// [sqlstring](https://www.npmjs.com/package/sqlstring)
31
+ /// module to escape values. However, the `escape`
32
+ /// method for whatever database the connection is
33
+ /// using should be used instead of this. This is
34
+ /// a "default implementation" that is meant as a
35
+ /// fallback when a connection doesn't provide its
36
+ /// own, but each connection should provide its own
37
+ /// when it is able.
38
+ ///
39
+ /// Note:
40
+ /// This method escapes "values" that are given in
41
+ /// the underlying query language of the database.
42
+ /// To escape identifiers, use the [ConnectionBase._escapeID](https://github.com/th317erd/mythix-orm/wiki/ConnectionBase#method-_escapeID)
43
+ /// instead.
44
+ ///
45
+ /// Return: string
46
+ /// The value provided, escaped for the specific
47
+ /// underlying database driver.
48
+ ///
49
+ /// Arguments:
50
+ /// value: any
51
+ /// The value to escape. This could be a number, a boolean,
52
+ /// a string, or anything else that can be provided to your
53
+ /// specific database.
54
+ _escape(value) {
55
+ if (Nife.instanceOf(value, 'string'))
56
+ return `'${value.replace(/'/g, '\'\'')}'`;
57
+
58
+ return SqlString.escape(value);
59
+ }
60
+
61
+ /// Low-level database method for escaping an identifier.
62
+ /// Each database driver should provide its own version of
63
+ /// this method. This is the "default" method Mythix ORM
64
+ /// provides as a "fallback" to database drivers that don't
65
+ /// supply their own.
66
+ ///
67
+ /// It works by first stripping all quotes (single `'`, double `"`, and backtick `` ` ``)
68
+ /// from the provided `value`. After this, it will split on the period (dot) character
69
+ /// `.`, and then will map each resulting part through [sqlstring](https://www.npmjs.com/package/sqlstring)
70
+ /// `escapeId` method, finally re-joining the parts with a period `.` character.
71
+ ///
72
+ /// The extra processing is to allow for already escaped identifiers to not be double-escaped.
73
+ ///
74
+ /// Return: string
75
+ /// The provided identifier, escaped for the underlying database.
76
+ ///
77
+ /// Arguments:
78
+ /// value: string
79
+ /// The identifier to escape.
80
+ _escapeID(value) {
81
+ let parts = value.replace(/['"`]/g, '').split(/\.+/g);
82
+ return parts.map((part) => SqlString.escapeId(part).replace(/^`/, '"').replace(/`$/, '"')).join('.');
83
+ }
84
+
85
+ /// Prepare an array of values for use in an `IN` statement.
86
+ /// This method will flatten the provided array, and then
87
+ /// will filter out all non-primitive values from the array.
88
+ /// Only `null`, `boolean`, `number`, `bigint`, and `string`
89
+ /// values will remain in the resulting array. The array is
90
+ /// also reduced to only unique values, with duplicates removed.
91
+ ///
92
+ /// Arguments:
93
+ /// array: Array<any>
94
+ /// The array to flatten, filter, and remove duplicates from.
95
+ ///
96
+ /// Return: Array<any>
97
+ /// Return a copy of the array provided, after being flattened, filtered,
98
+ /// and duplicates removed.
21
99
  prepareArrayValuesForSQL(_array) {
22
100
  let array = Nife.arrayFlatten(_array);
23
101
 
@@ -37,6 +115,13 @@ class SQLConnectionBase extends ConnectionBase {
37
115
  return Nife.uniq(array);
38
116
  }
39
117
 
118
+ /// Generate a `SAVEPOINT` name for use in sub-transactions.
119
+ /// This will generate a UUID v4 ID, and then mutate it so
120
+ /// that the name is SQL-safe. Finally, it will add an `SP`
121
+ /// prefix to the `SAVEPOINT` name.
122
+ ///
123
+ /// Return: string
124
+ /// A random SQL-safe `SAVEPOINT` name.
40
125
  generateSavePointName() {
41
126
  let id = UUID.v4();
42
127
 
@@ -48,6 +133,26 @@ class SQLConnectionBase extends ConnectionBase {
48
133
  return `SP${id}`;
49
134
  }
50
135
 
136
+ /// Given a `Map` or an `Array` from a projection field-set, find all matching
137
+ /// projected fields through this connection, and return
138
+ /// them as an array.
139
+ ///
140
+ /// Arguments:
141
+ /// projectionFieldMap: Map<string, string> | Array<string>
142
+ /// A set of projected fields, as a `Map` or an `Array`. A `Map` type will usually be generated
143
+ /// by <see>SQLQueryGeneratorBase.getProjectedFields</see>. The `Map` is expected
144
+ /// to have fully qualified field names and expanded literal strings as keys, with
145
+ /// the projected database fields (or literals) in database format for values. As
146
+ /// an `Array` type, this should be a list of fully qualified field names, or expanded
147
+ /// literals as values. An `Array` of field names is generally used when the `columns`
148
+ /// from the database result are passed into this method.
149
+ ///
150
+ /// Return: Array<[Field](https://github.com/th317erd/mythix-orm/wiki/Field) | string>
151
+ /// All fields found from the projection `Map`. Any field that wasn't able to be
152
+ /// found on this `connection` will instead just be returned as its raw `string` form.
153
+ /// Any raw strings returned are likely literals, and so couldn't be matched to fields.
154
+ /// However, they may not always be literals, but instead may be a custom field or field
155
+ /// alias that the user requested on the projection.
51
156
  findAllFieldsFromFieldProjectionMap(projectionFieldMap) {
52
157
  let fullFieldNames = (Array.isArray(projectionFieldMap)) ? projectionFieldMap : Array.from(projectionFieldMap.keys());
53
158
 
@@ -64,6 +169,26 @@ class SQLConnectionBase extends ConnectionBase {
64
169
  }).filter(Boolean);
65
170
  }
66
171
 
172
+ /// This method will take a `{ rows: Array<any>; columns: Array<string>; }` result from
173
+ /// a `SELECT` statement, and build a map of model data and sub-model data
174
+ /// to be later turned into model instances by <see>SQLConnectionBase.buildModelsFromModelDataMap</see>.
175
+ ///
176
+ /// Arguments:
177
+ /// queryEngine: [QueryEngine](https://github.com/th317erd/mythix-orm/wiki/QueryEngine)
178
+ /// The query engine that was used to generate the `SELECT` statement that returned these results.
179
+ /// result: { rows: Array<any>; columns: Array<string>; }
180
+ /// The results as returned by the database, used to build the data map to later construct model instances.
181
+ ///
182
+ /// Return: { [key: string]: Array<object> }
183
+ /// Return `result` from the database, mapped to the models that were projected. Each key in
184
+ /// this object will be a model name, and each value an array of model attributes. The model
185
+ /// attributes in each array of models represents a model instance that will be created later
186
+ /// using these attributes. There is a special key `Symbol.for('@_mythix/orm-sql-base/SQLConnectionBase/ModelRelations')`
187
+ /// that can be present on any object of model attributes (a model). This special key--if present--will be
188
+ /// another mapped object that represents "sub-models" that are "owned" by the model that has this
189
+ /// special key. This is used when projecting and loading "related models" during a load operation.
190
+ ///
191
+ /// See: SQLConnectionBase.buildModelsFromModelDataMap
67
192
  buildModelDataMapFromSelectResults(queryEngine, result) {
68
193
  if (!result)
69
194
  return {};
@@ -249,11 +374,26 @@ class SQLConnectionBase extends ConnectionBase {
249
374
  return modelData;
250
375
  }
251
376
 
252
- // eslint-disable-next-line no-unused-vars
253
- async enableForeignKeyConstraints(enable) {
254
- throw new Error(`${this.constructor.name}::enableForeignKeyConstraints: This operation is not supported for this connection type.`);
255
- }
256
-
377
+ /// Take the result from a <see>SQLConnectionBase.buildModelDataMapFromSelectResults</see> call
378
+ /// and instantiate all models defined by the model attribute map.
379
+ ///
380
+ /// Arguments:
381
+ /// queryEngine: [QueryEngine](https://github.com/th317erd/mythix-orm/wiki/QueryEngine)
382
+ /// The query engine that was used to generate the `SELECT` statement that returned these results.
383
+ /// modelDataMap: { [key: string]: Array<object> }
384
+ /// The result from a call to <see>SQLConnectionBase.buildModelDataMapFromSelectResults</see>.
385
+ /// callback?: (RootModel: class [Model](https://github.com/th317erd/mythix-orm/wiki/Model), model: [Model](https://github.com/th317erd/mythix-orm/wiki/Model)) => [Model](https://github.com/th317erd/mythix-orm/wiki/Model)
386
+ /// A callback that is called for each *root model* instance created from the provided `modelDataMap`.
387
+ /// Sub-models, or other models projected for the operation will not be passed through this callback.
388
+ /// Only instances of the "root model" (or "target model") of the query will be passed through this
389
+ /// callback. This callback **must** return the original model instance provided to it, or an equivalent
390
+ /// model instance (possibly the same instance modified by the callback).
391
+ ///
392
+ /// Return: Array<[Model](https://github.com/th317erd/mythix-orm/wiki/Model)>
393
+ /// An array of all fully-instantiated "root models" returned from the `SELECT` query, including
394
+ /// any "sub-models" that were loaded along-side them. All models will be marked "clean".
395
+ ///
396
+ /// See: SQLConnectionBase.buildModelDataMapFromSelectResults
257
397
  buildModelsFromModelDataMap(queryEngine, modelDataMap, callback) {
258
398
  if (Nife.isEmpty(modelDataMap))
259
399
  return [];
@@ -305,6 +445,26 @@ class SQLConnectionBase extends ConnectionBase {
305
445
  return rootModels;
306
446
  }
307
447
 
448
+ /// Take the `{ rows: Array<any>; columns: Array<string>; }` result from
449
+ /// the database for a `RETURNING` statement, and update the effected models
450
+ /// with the results.
451
+ ///
452
+ /// This is used by `UPDATE` and `INSERT` statements to sync model attributes
453
+ /// with the database update/insert operation that just took place.
454
+ ///
455
+ /// This will also set all models provided to the call as "persisted".
456
+ ///
457
+ /// Arguments:
458
+ /// Model: class [Model](https://github.com/th317erd/mythix-orm/wiki/Model)
459
+ /// The model class of the array of `storedModels` that are being operated upon.
460
+ /// storedModels: Array<[Model](https://github.com/th317erd/mythix-orm/wiki/Model)>
461
+ /// The models that were just persisted.
462
+ /// results: { rows: Array<any>; columns: Array<string>; }
463
+ /// The raw results from the database, used to update all persisted models.
464
+ ///
465
+ /// Return: Array<[Model](https://github.com/th317erd/mythix-orm/wiki/Model)>
466
+ /// Return `storedModels`, with each model being updated with the returned results
467
+ /// from the database, and each model marked as "persisted".
308
468
  updateModelsFromResults(Model, storedModels, results) {
309
469
  let {
310
470
  rows,
@@ -328,6 +488,21 @@ class SQLConnectionBase extends ConnectionBase {
328
488
  return storedModels;
329
489
  }
330
490
 
491
+ /// Parse the database-specific return value for an
492
+ /// `UPDATE` or `DELETE` operation to retrieve the
493
+ /// number of rows that were updated or deleted.
494
+ ///
495
+ /// Note:
496
+ /// Each database driver should overload this method to
497
+ /// properly parse the results of an `UPDATE` or `DELETE`
498
+ /// statement to return the number of rows affected.
499
+ ///
500
+ /// Arguments:
501
+ /// queryResult: any
502
+ /// The raw database response for an `UPDATE` or `DELETE` statement.
503
+ ///
504
+ /// Return: number
505
+ /// The number of rows that were affected by the operation.
331
506
  getUpdateOrDeleteChangeCount(queryResult) {
332
507
  if (!queryResult)
333
508
  return 0;
@@ -343,20 +518,68 @@ class SQLConnectionBase extends ConnectionBase {
343
518
 
344
519
  // --------------------------------------------- //
345
520
 
521
+ /// For databases that support it, enable or disable foreign key constraints.
522
+ ///
523
+ /// Arguments:
524
+ /// enable: boolean
525
+ /// If `true`, enable foreign key constraints in the underlying database, otherwise
526
+ /// disable foreign key constraints in the underlying database.
527
+ // eslint-disable-next-line no-unused-vars
528
+ async enableForeignKeyConstraints(enable) {
529
+ throw new Error(`${this.constructor.name}::enableForeignKeyConstraints: This operation is not supported for this connection type.`);
530
+ }
531
+
532
+ /// Drop the table/bucket defined by `Model`.
533
+ ///
534
+ /// Note:
535
+ /// Mythix ORM always refers to data-spaces as "tables", even though
536
+ /// they might actually be "buckets" for example for no-SQL databases.
537
+ ///
538
+ /// Arguments:
539
+ /// Model: class [Model](https://github.com/th317erd/mythix-orm/wiki/Model)
540
+ /// The model that defines the table to be dropped.
541
+ /// options?: object
542
+ /// Though these options can be database specific, they are commonly just
543
+ /// the following options.
544
+ /// | Option | Type | Default Value | Description |
545
+ /// | ------ | ---- | ------------- | ----------- |
546
+ /// | `ifExists` | `boolean` | `false` | Add an `IF EXISTS` clause to the drop table statement. |
547
+ /// | `cascade` | `boolean` | `true` | Add a `CASCADE` clause to the drop table statement (destroying rows in other tables defined by foreign key constraints). |
548
+ ///
549
+ /// Return: undefined
550
+ /// This method returns nothing.
346
551
  async dropTable(Model, options) {
347
552
  let queryGenerator = this.getQueryGenerator();
348
553
  let createTableSQL = queryGenerator.generateDropTableStatement(Model, options);
349
554
 
350
555
  // Drop table
351
- return await this.query(createTableSQL, options);
556
+ await this.query(createTableSQL, options);
352
557
  }
353
558
 
559
+ /// Create the table/bucket defined by `Model`.
560
+ ///
561
+ /// Note:
562
+ /// Mythix ORM always refers to data-spaces as "tables", even though
563
+ /// they might actually be "buckets" for example for no-SQL databases.
564
+ ///
565
+ /// Arguments:
566
+ /// Model: class [Model](https://github.com/th317erd/mythix-orm/wiki/Model)
567
+ /// The model that defines the table to be created.
568
+ /// options?: object
569
+ /// Though these options can be database specific, they are commonly just
570
+ /// the following options.
571
+ /// | Option | Type | Default Value | Description |
572
+ /// | ------ | ---- | ------------- | ----------- |
573
+ /// | `ifNotExists` | `boolean` | `false` | Add an `IF NOT EXISTS` clause to the create table statement. |
574
+ ///
575
+ /// Return: undefined
576
+ /// This method returns nothing.
354
577
  async createTable(Model, options) {
355
578
  let queryGenerator = this.getQueryGenerator();
356
579
  let createTableSQL = queryGenerator.generateCreateTableStatement(Model, options);
357
580
 
358
581
  // Create table
359
- let result = await this.query(createTableSQL, options);
582
+ await this.query(createTableSQL, options);
360
583
 
361
584
  // Create indexes and constraints
362
585
  let trailingStatements = Nife.toArray(queryGenerator.generateCreateTableStatementOuterTail(Model, options)).filter(Boolean);
@@ -366,8 +589,6 @@ class SQLConnectionBase extends ConnectionBase {
366
589
  await this.query(trailingStatement, options);
367
590
  }
368
591
  }
369
-
370
- return result;
371
592
  }
372
593
 
373
594
  async insert(Model, models, _options) {
@@ -558,6 +779,19 @@ class SQLConnectionBase extends ConnectionBase {
558
779
  return this.getUpdateOrDeleteChangeCount(await this.query(sqlStr, options));
559
780
  }
560
781
 
782
+ /// Convert the raw `{ rows: Array<any>; columns: Array<string>; }` results
783
+ /// from a database into an array of mapped objects.
784
+ ///
785
+ /// This method simply takes the rows and columns reported by the database
786
+ /// for a `SELECT` operation, and maps them to objects, where each key is
787
+ /// a column name, and each value is a column from one of the returned rows.
788
+ ///
789
+ /// Arguments:
790
+ /// result: { rows: Array<any>; columns: Array<string>; }
791
+ /// The raw results as returned by the database.
792
+ ///
793
+ /// Return: Array<object>
794
+ /// The raw results from the database, with each row mapped to an object.
561
795
  queryResultRowsToRawData(result) {
562
796
  if (!result)
563
797
  return [];
@@ -614,6 +848,10 @@ class SQLConnectionBase extends ConnectionBase {
614
848
 
615
849
  let batchSize = options.batchSize || 500;
616
850
  let startIndex = queryContext.offset || 0;
851
+ let limit = queryContext.limit;
852
+
853
+ if (Nife.instanceOf(limit, 'number') && isFinite(limit) && limit < batchSize)
854
+ batchSize = limit;
617
855
 
618
856
  while (true) {
619
857
  let query = queryEngine.clone().LIMIT(batchSize).OFFSET(startIndex);
@@ -55,7 +55,6 @@ declare class SQLQueryGeneratorBase extends QueryGeneratorBase {
55
55
  public getEscapedTableName(modelOrField: ModelClass | Field, options?: GetEscapedTableNameNameOptions): string;
56
56
  public getEscapedProjectionName(Model: ModelClass | null | undefined, field: Field, options?: GetEscapedProjectionNameOptions): string;
57
57
  public getEscapedModelFields(Model: ModelClass, options?: GetEscapedModelFieldsOptions): { [key: string]: string };
58
- public isFieldIdentifier(value: string): boolean;
59
58
  public getProjectedFields(queryEngine: QueryEngine, options?: GenericObject, asMap?: false | undefined): Array<string>;
60
59
  public getProjectedFields(queryEngine: QueryEngine, options?: GenericObject, asMap?: true): Map<string, string>;
61
60
 
@@ -67,21 +66,13 @@ declare class SQLQueryGeneratorBase extends QueryGeneratorBase {
67
66
  ): JoinTableInfo;
68
67
 
69
68
  public prepareArrayValuesForSQL(array: Array<any>): Array<any>;
70
- public parseFieldProjection(value: string, getRawField: boolean): string | Field | undefined;
71
- public parseFieldProjectionToFieldMap(selectStatement: string): Map<string, Field | string>;
72
69
 
73
70
  public generateSelectQueryFieldProjection(
74
71
  queryEngine: QueryEngine,
75
72
  options?: GenericObject,
76
- asMap?: false | undefined,
73
+ projectedFields?: Map<string, string>,
77
74
  ): Array<string>;
78
75
 
79
- public generateSelectQueryFieldProjection(
80
- queryEngine: QueryEngine,
81
- options?: GenericObject,
82
- asMap?: true,
83
- ): Map<string, string>;
84
-
85
76
  public generateSelectQueryOperatorFromQueryEngineOperator(
86
77
  queryPart: GenericObject,
87
78
  operator: string | LiteralBase,
@@ -174,7 +165,7 @@ declare class SQLQueryGeneratorBase extends QueryGeneratorBase {
174
165
 
175
166
  public generateInsertStatementTail(
176
167
  Model: ModelClass,
177
- model: Model,
168
+ models: Model | Array<Model> | PreparedModels,
178
169
  options: GenericObject,
179
170
  context: {
180
171
  escapedTableName: string,
@@ -225,9 +216,9 @@ declare class SQLQueryGeneratorBase extends QueryGeneratorBase {
225
216
  public generateAddColumnStatement(field: Field, options?: GenericObject): string;
226
217
 
227
218
  public _collectRemoteReturningFields(Model: ModelClass): Array<string>;
228
- public _collectReturningFields(
219
+ public generateReturningClause(
229
220
  Model: ModelClass,
230
- model: Model,
221
+ models: Model | Array<Model> | PreparedModels,
231
222
  options: GenericObject,
232
223
  context: {
233
224
  escapedTableName: string,