/
sequelize.repository.base.ts
1320 lines (1195 loc) · 40.6 KB
/
sequelize.repository.base.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
// Copyright LoopBack contributors 2022. All Rights Reserved.
// Node module: @loopback/sequelize
// This file is licensed under the MIT License.
// License text available at https://opensource.org/licenses/MIT
import {
AnyObject,
BelongsToAccessor,
BelongsToDefinition,
Command,
Count,
DataObject,
Entity,
EntityCrudRepository,
EntityNotFoundError,
Fields,
Filter,
FilterExcludingWhere,
Getter,
HasManyDefinition,
HasManyRepositoryFactory,
HasManyThroughRepositoryFactory,
HasOneDefinition,
HasOneRepositoryFactory,
Inclusion,
InclusionFilter,
InclusionResolver,
RelationType as LoopbackRelationType,
NamedParameters,
Options,
PositionalParameters,
PropertyDefinition,
ReferencesManyAccessor,
ReferencesManyDefinition,
Where,
createBelongsToAccessor,
createHasManyRepositoryFactory,
createHasManyThroughRepositoryFactory,
createHasOneRepositoryFactory,
createReferencesManyAccessor,
} from '@loopback/repository';
import debugFactory from 'debug';
import {
Attributes,
DataType,
DataTypes,
FindAttributeOptions,
Identifier,
Includeable,
Model,
ModelAttributeColumnOptions,
ModelAttributes,
ModelStatic,
Op,
Order,
SyncOptions,
Transaction,
TransactionOptions,
WhereOptions,
} from 'sequelize';
import {MakeNullishOptional} from 'sequelize/types/utils';
import {operatorTranslations} from './operator-translation';
import {SequelizeDataSource} from './sequelize.datasource.base';
import {SequelizeModel} from './sequelize.model';
import {isTruelyObject} from './utils';
const debug = debugFactory('loopback:sequelize:repository');
const debugModelBuilder = debugFactory('loopback:sequelize:modelbuilder');
/**
* Sequelize implementation of CRUD repository to be used with default loopback entities
* and SequelizeDataSource for SQL Databases
*/
export class SequelizeCrudRepository<
T extends Entity,
ID,
Relations extends object = {},
> implements EntityCrudRepository<T, ID, Relations>
{
constructor(
public entityClass: typeof Entity & {
prototype: T;
},
public dataSource: SequelizeDataSource,
) {
if (this.dataSource.sequelize) {
this.sequelizeModel = this.getSequelizeModel();
}
}
/**
* Default `order` filter style if only column name is specified
*/
readonly DEFAULT_ORDER_STYLE = 'ASC';
/**
* Object keys used in models for set database specific settings.
* Example: In model property definition one can use postgresql dataType as float
* `{
* type: 'number',
* postgresql: {
* dataType: 'float',
* precision: 20,
* scale: 4,
* },
* }`
*
* This array of keys is used while building model definition for sequelize.
*/
readonly DB_SPECIFIC_SETTINGS_KEYS = [
'postgresql',
'mysql',
'sqlite3',
] as const;
public readonly inclusionResolvers: Map<
string,
InclusionResolver<T, Entity>
> = new Map();
/**
* Sequelize Model Instance created from the model definition received from the `entityClass`
*/
public sequelizeModel: ModelStatic<Model<T>>;
async create(entity: DataObject<T>, options?: AnyObject): Promise<T> {
let err = null;
const data = await this.sequelizeModel
.create(entity as MakeNullishOptional<T>, options)
.catch(error => {
err = error;
});
if (!data) {
throw new Error(err ?? 'Something went wrong');
}
return new this.entityClass(this.excludeHiddenProps(data.toJSON())) as T;
}
async createAll(
entities: DataObject<T>[],
options?: AnyObject,
): Promise<T[]> {
const models = await this.sequelizeModel.bulkCreate(
entities as MakeNullishOptional<T>[],
options,
);
return this.toEntities(models);
}
exists(id: ID, _options?: AnyObject): Promise<boolean> {
return new Promise((resolve, reject) => {
this.sequelizeModel
.findByPk(id as unknown as Identifier)
.then(value => {
resolve(!!value);
})
.catch(err => {
reject(err);
});
});
}
async save(entity: T, options?: AnyObject): Promise<T> {
const id = this.entityClass.getIdOf(entity);
if (id == null) {
return this.create(entity, options);
} else {
await this.replaceById(id, entity, options);
return new this.entityClass(entity.toObject()) as T;
}
}
update(entity: T, options?: AnyObject): Promise<void> {
return this.updateById(entity.getId(), entity, options);
}
async updateById(
id: ID,
data: DataObject<T>,
options?: AnyObject,
): Promise<void> {
if (id === undefined) {
throw new Error('Invalid Argument: id cannot be undefined');
}
const idProp = this.entityClass.definition.idProperties()[0];
const where = {} as Where<T>;
(where as AnyObject)[idProp] = id;
const result = await this.updateAll(data, where, options);
if (result.count === 0) {
throw new EntityNotFoundError(this.entityClass, id);
}
}
async updateAll(
data: DataObject<T>,
where?: Where<T>,
options?: AnyObject,
): Promise<Count> {
const [affectedCount] = await this.sequelizeModel.update(
Object.assign({} as AnyObject, data),
{
where: this.buildSequelizeWhere(where),
...options,
},
);
return {count: affectedCount};
}
async delete(entity: T, options?: AnyObject): Promise<void> {
return this.deleteById(entity.getId(), options);
}
async find(
filter?: Filter<T>,
options?: AnyObject,
): Promise<(T & Relations)[]> {
const data = await this.sequelizeModel
.findAll({
include: this.buildSequelizeIncludeFilter(filter?.include),
where: this.buildSequelizeWhere(filter?.where),
attributes: this.buildSequelizeAttributeFilter(filter?.fields),
order: this.buildSequelizeOrder(filter?.order),
limit: filter?.limit,
offset: filter?.offset ?? filter?.skip,
...options,
})
.catch(err => {
debug('findAll() error:', err);
throw new Error(err);
});
return this.includeReferencesIfRequested(
data,
this.entityClass,
filter?.include,
);
}
async findOne(
filter?: Filter<T>,
options?: AnyObject,
): Promise<(T & Relations) | null> {
const data = await this.sequelizeModel
.findOne({
include: this.buildSequelizeIncludeFilter(filter?.include),
where: this.buildSequelizeWhere(filter?.where),
attributes: this.buildSequelizeAttributeFilter(filter?.fields),
order: this.buildSequelizeOrder(filter?.order),
offset: filter?.offset ?? filter?.skip,
...options,
})
.catch(err => {
debug('findOne() error:', err);
throw new Error(err);
});
if (data === null) {
return Promise.resolve(null);
}
const resolved = await this.includeReferencesIfRequested(
[data],
this.entityClass,
filter?.include,
);
return resolved[0];
}
async findById(
id: ID,
filter?: FilterExcludingWhere<T>,
options?: AnyObject,
): Promise<T & Relations> {
const data = await this.sequelizeModel.findByPk(
id as unknown as Identifier,
{
order: this.buildSequelizeOrder(filter?.order),
attributes: this.buildSequelizeAttributeFilter(filter?.fields),
include: this.buildSequelizeIncludeFilter(filter?.include),
limit: filter?.limit,
offset: filter?.offset ?? filter?.skip,
...options,
},
);
if (!data) {
throw new EntityNotFoundError(this.entityClass, id);
}
const resolved = await this.includeReferencesIfRequested(
[data],
this.entityClass,
filter?.include,
);
return resolved[0];
}
async replaceById(
id: ID,
data: DataObject<T>,
options?: AnyObject | undefined,
): Promise<void> {
const idProp = this.entityClass.definition.idProperties()[0];
if (idProp in data) {
delete data[idProp as keyof typeof data];
}
await this.updateById(id, data, options);
}
async deleteAll(
where?: Where<T> | undefined,
options?: AnyObject | undefined,
): Promise<Count> {
const count = await this.sequelizeModel.destroy({
where: this.buildSequelizeWhere(where),
...options,
});
return {count};
}
async deleteById(id: ID, options?: AnyObject | undefined): Promise<void> {
const idProp = this.entityClass.definition.idProperties()[0];
if (id === undefined) {
throw new Error(`Invalid Argument: ${idProp} cannot be undefined`);
}
const where = {} as Where<T>;
(where as AnyObject)[idProp] = id;
const count = await this.sequelizeModel.destroy({
where: this.buildSequelizeWhere(where),
...options,
});
if (count === 0) {
throw new EntityNotFoundError(this.entityClass, id);
}
}
async count(where?: Where<T>, options?: AnyObject): Promise<Count> {
const count = await this.sequelizeModel.count({
where: this.buildSequelizeWhere<T>(where),
...options,
});
return {count};
}
/**
* Execute a SQL command.
*
* **WARNING:** In general, it is always better to perform database actions
* through repository methods. Directly executing SQL may lead to unexpected
* results, corrupted data, security vulnerabilities and other issues.
*
* @example
*
* ```ts
* // MySQL
* const result = await repo.execute(
* 'SELECT * FROM Products WHERE size > ?',
* [42]
* );
*
* // PostgreSQL
* const result = await repo.execute(
* 'SELECT * FROM Products WHERE size > $1',
* [42]
* );
* ```
*
* @param command A parameterized SQL command or query.
* @param parameters List of parameter values to use.
* @param options Additional options, for example `transaction`.
* @returns A promise which resolves to the command output. The output type (data structure) is database specific and
* often depends on the command executed.
*/
async execute(
command: Command,
parameters?: NamedParameters | PositionalParameters,
options?: Options,
): Promise<AnyObject> {
return this.dataSource.execute(command, parameters, options);
}
protected toEntities(models: Model<T, T>[]): T[] {
return models.map(m => new this.entityClass(m.toJSON()) as T);
}
/**
* Get Sequelize Operator
* @param key Name of the operator used in loopback eg. lt
* @returns Equivalent operator symbol if available in Sequelize eg `Op.lt`
*/
protected getSequelizeOperator(key: keyof typeof operatorTranslations) {
const sequelizeOperator = operatorTranslations[key];
if (!sequelizeOperator) {
throw Error(`There is no equivalent operator for "${key}" in sequelize.`);
}
return sequelizeOperator;
}
/**
* Get Sequelize `attributes` filter value from `fields` of loopback.
* @param fields Loopback styles `fields` options. eg. `["name", "age"]`, `{ id: false }`
* @returns Sequelize Compatible Object/Array based on the fields provided. eg. `{ "exclude": ["id"] }`
*/
protected buildSequelizeAttributeFilter(
fields?: Fields,
): FindAttributeOptions | undefined {
if (fields === undefined) {
return undefined;
}
if (Array.isArray(fields)) {
// Both (sequelize and loopback filters) consider array as "only columns to include"
return fields;
}
const sequelizeFields: FindAttributeOptions = {
include: [],
exclude: [],
};
// Push column having `false` values in `exclude` key and columns
// having `true` in `include` key
if (isTruelyObject(fields)) {
for (const key in fields) {
if (fields[key] === true) {
sequelizeFields.include?.push(key);
} else if (fields[key] === false) {
sequelizeFields.exclude?.push(key);
}
}
}
if (
Array.isArray(sequelizeFields.include) &&
sequelizeFields.include.length > 0
) {
delete sequelizeFields.exclude;
return sequelizeFields.include;
}
if (
Array.isArray(sequelizeFields.exclude) &&
sequelizeFields.exclude.length > 0
) {
delete sequelizeFields.include;
}
return sequelizeFields;
}
/**
* Get Sequelize Order filter value from loopback style order value
* @param order Sorting order in loopback style filter. eg. `title ASC`, `["id DESC", "age ASC"]`
* @returns Sequelize compatible order filter value
*/
protected buildSequelizeOrder(order?: string[] | string): Order | undefined {
if (order === undefined) {
return undefined;
}
if (typeof order === 'string') {
const [columnName, orderType] = order.trim().split(' ');
return [[columnName, orderType ?? this.DEFAULT_ORDER_STYLE]];
}
return order.map(orderStr => {
const [columnName, orderType] = orderStr.trim().split(' ');
return [columnName, orderType ?? this.DEFAULT_ORDER_STYLE];
});
}
/**
* Build Sequelize compatible `include` filter
* @param inclusionFilters - loopback style `where` condition
* @param sourceModel - sequelize model instance
* @returns Sequelize compatible `Includeable` array
*/
protected buildSequelizeIncludeFilter(
inclusionFilters?: Array<InclusionFilter & {required?: boolean}>,
sourceModel?: ModelStatic<Model<T>>,
): Includeable[] {
if (!inclusionFilters || inclusionFilters.length === 0) {
return [];
}
if (!sourceModel) {
sourceModel = this.sequelizeModel;
}
const includable: Includeable[] = [];
for (const filter of inclusionFilters) {
if (typeof filter === 'string') {
if (filter in sourceModel.associations) {
includable.push(filter);
} else {
debug(
`Relation '${filter}' is not available in sequelize model associations. If it's referencesMany relation it will fallback to loopback inclusion resolver.`,
);
}
} else if (typeof filter === 'object') {
if (!(filter.relation in sourceModel.associations)) {
debug(
`Relation '${filter.relation}' is not available in sequelize model associations. If it's referencesMany relation it will fallback to loopback inclusion resolver.`,
);
continue;
}
const targetAssociation = sourceModel.associations[filter.relation];
includable.push({
model: targetAssociation.target,
/**
* Exclude through model data from response to be backward compatible
* with loopback response style for hasMany through relation.
* Does not work with sqlite3
*/
...(targetAssociation.associationType === 'BelongsToMany' &&
targetAssociation.isMultiAssociation
? {through: {attributes: []}}
: {}),
where: this.buildSequelizeWhere(filter.scope?.where),
limit: filter.scope?.totalLimit ?? filter.scope?.limit,
attributes: this.buildSequelizeAttributeFilter(filter.scope?.fields),
include: this.buildSequelizeIncludeFilter(
filter.scope?.include,
targetAssociation.target,
),
order: this.buildSequelizeOrder(filter.scope?.order),
as: filter.relation,
/**
* If true, uses an inner join, which means that the parent model will only be loaded if it has any matching children.
*/
required: !!filter.required,
/**
* saperate: true is required for `order` and `limit` filter to work, it runs include in saperate queries
*/
separate:
!!filter.scope?.order ||
!!(filter.scope?.totalLimit ?? filter.scope?.limit),
});
}
}
return includable;
}
/**
* Build Sequelize compatible where condition object
* @param where loopback style `where` condition
* @returns Sequelize compatible where options to be used in queries
*/
protected buildSequelizeWhere<MT extends T>(
where?: Where<MT>,
): WhereOptions<MT> {
if (!where) {
return {};
}
const sequelizeWhere: WhereOptions = {};
/**
* Handle model attribute conditions like `{ age: { gt: 18 } }`, `{ email: "a@b.c" }`
* Transform Operators - eg. `{ gt: 0, lt: 10 }` to `{ [Op.gt]: 0, [Op.lt]: 10 }`
*/
for (const columnName in where) {
const conditionValue = <Object | Array<Object> | number | string | null>(
where[columnName as keyof typeof where]
);
if (isTruelyObject(conditionValue)) {
sequelizeWhere[columnName] = {};
for (const lb4Operator of Object.keys(<Object>conditionValue)) {
const sequelizeOperator = this.getSequelizeOperator(
lb4Operator as keyof typeof operatorTranslations,
);
sequelizeWhere[columnName][sequelizeOperator] =
conditionValue![lb4Operator as keyof typeof conditionValue];
}
} else if (
['and', 'or'].includes(columnName) &&
Array.isArray(conditionValue)
) {
/**
* Eg. {and: [{title: 'My Post'}, {content: 'Hello'}]}
*/
const sequelizeOperator = this.getSequelizeOperator(
columnName as 'and' | 'or',
);
const conditions = conditionValue.map((condition: unknown) => {
return this.buildSequelizeWhere<MT>(condition as Where<MT>);
});
Object.assign(sequelizeWhere, {
[sequelizeOperator]: conditions,
});
} else {
// Equals
sequelizeWhere[columnName] = {
[Op.eq]: conditionValue,
};
}
}
return sequelizeWhere;
}
/**
* Get Sequelize Model
* @returns Sequelize Model Instance based on the definitions from `entityClass`
*/
public getSequelizeModel(entityClass = this.entityClass) {
if (!this.dataSource.sequelize) {
throw Error(
`The datasource "${this.dataSource.name}" doesn't have sequelize instance bound to it.`,
);
}
if (this.dataSource.sequelize.models[entityClass.modelName]) {
// Model Already Defined by Sequelize before
return this.dataSource.sequelize.models[entityClass.modelName];
}
// TODO: Make it more flexible, check support of all possible definition props
const sourceModel = this.dataSource.sequelize.define(
entityClass.modelName,
this.getSequelizeModelAttributes(entityClass.definition.properties),
{
timestamps: false,
tableName: this.getTableName(entityClass),
freezeTableName: true,
},
);
// Setup associations
for (const key in entityClass.definition.relations) {
const targetModel = this.getSequelizeModel(
entityClass.definition.relations[key].target(),
);
debugModelBuilder(
`Setting up relation`,
entityClass.definition.relations[key],
);
if (
entityClass.definition.relations[key].type ===
LoopbackRelationType.belongsTo
) {
const foreignKey = (
entityClass.definition.relations[key] as BelongsToDefinition
).keyTo;
sourceModel.belongsTo(targetModel, {
foreignKey: {name: foreignKey},
// Which client will pass on in loopback style include filter, eg. `include: ["thisName"]`
as: entityClass.definition.relations[key].name,
});
} else if (
entityClass.definition.relations[key].type ===
LoopbackRelationType.hasOne
) {
const foreignKey = (
entityClass.definition.relations[key] as HasOneDefinition
).keyTo;
sourceModel.hasOne(targetModel, {
foreignKey: foreignKey,
as: entityClass.definition.relations[key].name,
});
} else if (
entityClass.definition.relations[key].type ===
LoopbackRelationType.hasMany
) {
const relationDefinition = entityClass.definition.relations[
key
] as HasManyDefinition;
const through = relationDefinition.through;
const foreignKey = relationDefinition.keyTo;
if (through) {
const keyTo = through.keyTo;
const keyFrom = through.keyFrom;
// Setup hasMany through
const throughModel = this.getSequelizeModel(through.model());
sourceModel.belongsToMany(targetModel, {
through: {model: throughModel},
otherKey: keyTo,
foreignKey: keyFrom,
as: entityClass.definition.relations[key].name,
});
} else {
sourceModel.hasMany(targetModel, {
foreignKey: foreignKey,
as: entityClass.definition.relations[key].name,
});
}
}
}
debugModelBuilder(
'Table name supplied to sequelize'.concat(
`"${entityClass.modelName.toLowerCase()}"`,
),
);
return sourceModel;
}
/**
* This function retrieves the table name associated with a given entity class.
* Different loopback connectors have different conventions for picking up table names,
* unless the name is specified in the @model decorator.
*
* The function follows the following cases to determine the table name:
* - It checks if the name property is specified in the @model decorator and uses it. (this takes precedence over all other cases)
* - If the dialect of the dataSource is PostgreSQL, it uses the lowercased version of the model class name.
* - If the dialect is MySQL or any other dialect, it uses the default model class name.
* @param {Entity} entityClass - The entity class for which the table name is being retrieved.
* @returns {string} - The table name associated with the entity class. Which is used when performing the query.
*/
getTableName(entityClass = this.entityClass) {
let tableName = entityClass.name; // model class name
if (entityClass.definition.name !== tableName) {
// name is specified in decorator
tableName = entityClass.definition.name;
} else if (this.dataSource.sequelizeConfig.dialect === 'postgres') {
// postgres is being used and name is not specified in @model decorator
tableName = entityClass.modelName.toLowerCase();
}
return tableName;
}
/**
* Run CREATE TABLE query for the target sequelize model, Useful for quick testing
* @param options Sequelize Sync Options
*/
async syncSequelizeModel(options: SyncOptions = {}) {
if (!this.dataSource.sequelize) {
throw new Error(
'Sequelize instance is not attached to the datasource yet.',
);
}
await this.dataSource.sequelize.authenticate();
await this.dataSource.sequelize.models[this.entityClass.modelName].sync(
options,
);
}
/**
* Run CREATE TABLE query for the all sequelize models, Useful for quick testing
* @param options Sequelize Sync Options
*/
async syncLoadedSequelizeModels(options: SyncOptions = {}) {
await this.dataSource.sequelize?.sync(options);
}
/**
* Get Sequelize Model Attributes
* @param definition property definition received from loopback entityClass eg. `{ id: { type: "Number", id: true } }`
* @returns model attributes supported in sequelize model definiotion
*
* TODO: Verify all possible loopback types https://loopback.io/doc/en/lb4/LoopBack-types.html
*/
protected getSequelizeModelAttributes(definition: {
[name: string]: PropertyDefinition;
}): ModelAttributes<SequelizeModel, Attributes<SequelizeModel>> {
debugModelBuilder('loopback model definition', definition);
const sequelizeDefinition: ModelAttributes = {};
for (const propName in definition) {
// Set data type, defaults to `DataTypes.STRING`
let dataType: DataType = DataTypes.STRING;
const isString =
definition[propName].type === String ||
['String', 'string'].includes(definition[propName].type.toString());
if (
definition[propName].type === Number ||
['Number', 'number'].includes(definition[propName].type.toString())
) {
dataType = DataTypes.INTEGER;
// handle float
for (const dbKey of this.DB_SPECIFIC_SETTINGS_KEYS) {
if (!(dbKey in definition[propName])) {
continue;
}
const dbSpecificSetting = definition[propName][dbKey] as {
dataType: string;
};
if (
['double precision', 'float', 'real'].includes(
dbSpecificSetting.dataType,
)
) {
// TODO: Handle precision
dataType = DataTypes.FLOAT;
}
}
}
if (
definition[propName].type === Boolean ||
['Boolean', 'boolean'].includes(definition[propName].type.toString())
) {
dataType = DataTypes.BOOLEAN;
}
if (
definition[propName].type === Array ||
['Array', 'array'].includes(definition[propName].type.toString())
) {
// Postgres only
dataType = DataTypes.ARRAY(DataTypes.INTEGER);
}
if (
definition[propName].type === Object ||
['object', 'Object'].includes(definition[propName].type.toString())
) {
// Postgres only, JSON dataType
dataType = DataTypes.JSON;
}
if (
definition[propName].type === Date ||
['date', 'Date'].includes(definition[propName].type.toString())
) {
dataType = DataTypes.DATE;
}
if (dataType === DataTypes.STRING && !isString) {
throw Error(
`Unhandled DataType "${definition[
propName
].type.toString()}" for column "${propName}" in sequelize extension`,
);
}
const columnOptions: ModelAttributeColumnOptions = {
type: dataType,
...('default' in definition[propName]
? {defaultValue: definition[propName].default}
: {}),
};
// Set column as `primaryKey` when id is set to true (which is loopback way to define primary key)
if (definition[propName].id === true) {
if (columnOptions.type === DataTypes.NUMBER) {
columnOptions.type = DataTypes.INTEGER;
}
Object.assign(columnOptions, {
primaryKey: true,
/**
* `autoIncrement` needs to be true even if DataType is not INTEGER else it will pass the ID in the query set to NULL.
*/
autoIncrement: !!definition[propName].generated,
} as typeof columnOptions);
}
// TODO: Get the column name casing using actual methods / conventions used in different sql connectors for loopback
columnOptions.field =
definition[propName]['name'] ?? propName.toLowerCase();
sequelizeDefinition[propName] = columnOptions;
}
debugModelBuilder('Sequelize model definition', sequelizeDefinition);
return sequelizeDefinition;
}
/**
* Remove hidden properties specified in model from response body. (See: https://github.com/sourcefuse/loopback4-sequelize/issues/3)
* @param entity normalized entity. You can use `entity.toJSON()`'s value
* @returns normalized entity excluding the hiddenProperties
*/
protected excludeHiddenProps(entity: T & Relations): T & Relations {
const hiddenProps = this.entityClass.definition.settings.hiddenProperties;
if (!hiddenProps) {
return entity;
}
for (const propertyName of hiddenProps as Array<keyof typeof entity>) {
delete entity[propertyName];
}
return entity;
}
/**
* Include related entities of `@referencesMany` relation
*
* referencesMany relation is NOT handled by `sequelizeModel.findAll` as it doesn't have any direct alternative to it,
* so to include relation data of referencesMany, we're manually fetching related data requested
*
* @param parentEntities source table data
* @param filter actual payload passed in request
* @param parentEntityClass loopback entity class for the parent entity
* @returns entities with related models in them
*/
protected async includeReferencesIfRequested(
parentEntities: Model<T, T>[],
parentEntityClass: typeof Entity,
inclusionFilters?: InclusionFilter[],
): Promise<(T & Relations)[]> {
if (!parentEntityClass) {
parentEntityClass = this.entityClass;
}
/**
* All columns names defined in model with `@referencesMany`
*/
const allReferencesColumns: string[] = [];
for (const key in parentEntityClass.definition.relations) {
if (
parentEntityClass.definition.relations[key].type ===
LoopbackRelationType.referencesMany
) {
const loopbackRelationObject = parentEntityClass.definition.relations[
key
] as ReferencesManyDefinition;
if (loopbackRelationObject.keyFrom) {
allReferencesColumns.push(loopbackRelationObject.keyFrom);
}
}
}
// Validate data type of items in any column having references
// For eg. convert ["1", "2"] into [1, 2] if `itemType` specified is `number[]`
const normalizedParentEntities = parentEntities.map(entity => {
const data = entity.toJSON();
for (const columnName in data) {
if (!allReferencesColumns.includes(columnName)) {
// Column is not the one used for referencesMany relation. Eg. "programmingLanguageIds"
continue;
}
const columnDefinition =
parentEntityClass.definition.properties[columnName];
if (
columnDefinition.type !== Array ||
!Array.isArray(data[columnName])
) {
// Column type or data received is not array, wrong configuration/data
continue;
}
// Loop over all references in array received
const items = data[columnName] as unknown as Array<String | Number>;
for (let itemIndex = 0; itemIndex < items.length; itemIndex++) {
if (
columnDefinition.itemType === Number &&
typeof items[itemIndex] === 'string'
) {
items[itemIndex] = parseInt(items[itemIndex] as string);
}
}
data[columnName] = items as unknown as T[Extract<keyof T, string>];
}
return data;
});
// Requested inclusions of referencesMany relation
const referencesManyInclusions: Array<{
/**
* Target Include filter entry
*/
filter: Inclusion;
/**
* Loopback relation definition
*/
definition: ReferencesManyDefinition;
/**
* Distinct foreignKey values of child model
* example: [1, 2, 4, 8]
*/
keys: Array<T[]>;
}> = [];
for (let includeFilter of inclusionFilters ?? []) {
if (typeof includeFilter === 'string') {
includeFilter = {relation: includeFilter} as Inclusion;