@loopback/sequelize 0.1.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 (87) hide show
  1. package/LICENSE +25 -0
  2. package/README.md +194 -0
  3. package/dist/.sandbox/6646miobBk/application.js +15 -0
  4. package/dist/.sandbox/6646miobBk/controllers/book-category.controller.js +41 -0
  5. package/dist/.sandbox/6646miobBk/controllers/book.controller.js +210 -0
  6. package/dist/.sandbox/6646miobBk/controllers/category.controller.js +198 -0
  7. package/dist/.sandbox/6646miobBk/controllers/developer.controller.js +177 -0
  8. package/dist/.sandbox/6646miobBk/controllers/doctor-patient.controller.js +112 -0
  9. package/dist/.sandbox/6646miobBk/controllers/doctor.controller.js +177 -0
  10. package/dist/.sandbox/6646miobBk/controllers/index.js +20 -0
  11. package/dist/.sandbox/6646miobBk/controllers/patient.controller.js +165 -0
  12. package/dist/.sandbox/6646miobBk/controllers/programming-languange.controller.js +204 -0
  13. package/dist/.sandbox/6646miobBk/controllers/test.controller.base.js +25 -0
  14. package/dist/.sandbox/6646miobBk/controllers/todo-list-todo.controller.js +113 -0
  15. package/dist/.sandbox/6646miobBk/controllers/todo-list.controller.js +177 -0
  16. package/dist/.sandbox/6646miobBk/controllers/todo-todo-list.controller.js +41 -0
  17. package/dist/.sandbox/6646miobBk/controllers/todo.controller.js +177 -0
  18. package/dist/.sandbox/6646miobBk/controllers/user-todo-list.controller.js +113 -0
  19. package/dist/.sandbox/6646miobBk/controllers/user.controller.js +210 -0
  20. package/dist/.sandbox/6646miobBk/datasources/db.datasource.js +28 -0
  21. package/dist/.sandbox/6646miobBk/models/appointment.model.js +37 -0
  22. package/dist/.sandbox/6646miobBk/models/book.model.js +48 -0
  23. package/dist/.sandbox/6646miobBk/models/category.model.js +32 -0
  24. package/dist/.sandbox/6646miobBk/models/developer.model.js +40 -0
  25. package/dist/.sandbox/6646miobBk/models/doctor.model.js +44 -0
  26. package/dist/.sandbox/6646miobBk/models/index.js +15 -0
  27. package/dist/.sandbox/6646miobBk/models/patient.model.js +32 -0
  28. package/dist/.sandbox/6646miobBk/models/programming-language.model.js +32 -0
  29. package/dist/.sandbox/6646miobBk/models/todo-list.model.js +45 -0
  30. package/dist/.sandbox/6646miobBk/models/todo.model.js +44 -0
  31. package/dist/.sandbox/6646miobBk/models/user.model.js +85 -0
  32. package/dist/.sandbox/6646miobBk/repositories/appointment.repository.js +20 -0
  33. package/dist/.sandbox/6646miobBk/repositories/book.repository.js +25 -0
  34. package/dist/.sandbox/6646miobBk/repositories/category.repository.js +20 -0
  35. package/dist/.sandbox/6646miobBk/repositories/developer.repository.js +25 -0
  36. package/dist/.sandbox/6646miobBk/repositories/doctor.repository.js +27 -0
  37. package/dist/.sandbox/6646miobBk/repositories/index.js +15 -0
  38. package/dist/.sandbox/6646miobBk/repositories/patient.repository.js +20 -0
  39. package/dist/.sandbox/6646miobBk/repositories/programming-language.repository.js +20 -0
  40. package/dist/.sandbox/6646miobBk/repositories/todo-list.repository.js +25 -0
  41. package/dist/.sandbox/6646miobBk/repositories/todo.repository.js +25 -0
  42. package/dist/.sandbox/6646miobBk/repositories/user.repository.js +29 -0
  43. package/dist/component.d.ts +7 -0
  44. package/dist/component.js +30 -0
  45. package/dist/component.js.map +1 -0
  46. package/dist/index.d.ts +4 -0
  47. package/dist/index.js +12 -0
  48. package/dist/index.js.map +1 -0
  49. package/dist/keys.d.ts +8 -0
  50. package/dist/keys.js +16 -0
  51. package/dist/keys.js.map +1 -0
  52. package/dist/sequelize/connector-mapping.d.ts +9 -0
  53. package/dist/sequelize/connector-mapping.js +19 -0
  54. package/dist/sequelize/connector-mapping.js.map +1 -0
  55. package/dist/sequelize/index.d.ts +2 -0
  56. package/dist/sequelize/index.js +10 -0
  57. package/dist/sequelize/index.js.map +1 -0
  58. package/dist/sequelize/operator-translation.d.ts +8 -0
  59. package/dist/sequelize/operator-translation.js +31 -0
  60. package/dist/sequelize/operator-translation.js.map +1 -0
  61. package/dist/sequelize/sequelize.datasource.base.d.ts +23 -0
  62. package/dist/sequelize/sequelize.datasource.base.js +60 -0
  63. package/dist/sequelize/sequelize.datasource.base.js.map +1 -0
  64. package/dist/sequelize/sequelize.model.d.ts +7 -0
  65. package/dist/sequelize/sequelize.model.js +24 -0
  66. package/dist/sequelize/sequelize.model.js.map +1 -0
  67. package/dist/sequelize/sequelize.repository.base.d.ts +231 -0
  68. package/dist/sequelize/sequelize.repository.base.js +835 -0
  69. package/dist/sequelize/sequelize.repository.base.js.map +1 -0
  70. package/dist/sequelize/utils.d.ts +6 -0
  71. package/dist/sequelize/utils.js +17 -0
  72. package/dist/sequelize/utils.js.map +1 -0
  73. package/dist/types.d.ts +9 -0
  74. package/dist/types.js +14 -0
  75. package/dist/types.js.map +1 -0
  76. package/package.json +64 -0
  77. package/src/component.ts +34 -0
  78. package/src/index.ts +9 -0
  79. package/src/keys.ts +16 -0
  80. package/src/sequelize/connector-mapping.ts +26 -0
  81. package/src/sequelize/index.ts +7 -0
  82. package/src/sequelize/operator-translation.ts +32 -0
  83. package/src/sequelize/sequelize.datasource.base.ts +81 -0
  84. package/src/sequelize/sequelize.model.ts +22 -0
  85. package/src/sequelize/sequelize.repository.base.ts +1246 -0
  86. package/src/sequelize/utils.ts +13 -0
  87. package/src/types.ts +19 -0
@@ -0,0 +1,1246 @@
1
+ // Copyright LoopBack contributors 2022. All Rights Reserved.
2
+ // Node module: @loopback/sequelize
3
+ // This file is licensed under the MIT License.
4
+ // License text available at https://opensource.org/licenses/MIT
5
+
6
+ import {
7
+ AnyObject,
8
+ BelongsToAccessor,
9
+ BelongsToDefinition,
10
+ Count,
11
+ createBelongsToAccessor,
12
+ createHasManyRepositoryFactory,
13
+ createHasManyThroughRepositoryFactory,
14
+ createHasOneRepositoryFactory,
15
+ createReferencesManyAccessor,
16
+ DataObject,
17
+ Entity,
18
+ EntityCrudRepository,
19
+ EntityNotFoundError,
20
+ Fields,
21
+ Filter,
22
+ FilterExcludingWhere,
23
+ Getter,
24
+ HasManyDefinition,
25
+ HasManyRepositoryFactory,
26
+ HasManyThroughRepositoryFactory,
27
+ HasOneDefinition,
28
+ HasOneRepositoryFactory,
29
+ Inclusion,
30
+ InclusionFilter,
31
+ InclusionResolver,
32
+ PositionalParameters,
33
+ PropertyDefinition,
34
+ ReferencesManyAccessor,
35
+ ReferencesManyDefinition,
36
+ RelationType as LoopbackRelationType,
37
+ Where,
38
+ } from '@loopback/repository';
39
+ import debugFactory from 'debug';
40
+ import {
41
+ Attributes,
42
+ DataType,
43
+ DataTypes,
44
+ FindAttributeOptions,
45
+ Identifier,
46
+ Includeable,
47
+ Model,
48
+ ModelAttributeColumnOptions,
49
+ ModelAttributes,
50
+ ModelStatic,
51
+ Op,
52
+ Order,
53
+ SyncOptions,
54
+ WhereOptions,
55
+ } from 'sequelize';
56
+ import {MakeNullishOptional} from 'sequelize/types/utils';
57
+ import {operatorTranslations} from './operator-translation';
58
+ import {SequelizeDataSource} from './sequelize.datasource.base';
59
+ import {SequelizeModel} from './sequelize.model';
60
+ import {isTruelyObject} from './utils';
61
+
62
+ const debug = debugFactory('loopback:sequelize:repository');
63
+ const debugModelBuilder = debugFactory('loopback:sequelize:modelbuilder');
64
+
65
+ /**
66
+ * Sequelize implementation of CRUD repository to be used with default loopback entities
67
+ * and SequelizeDataSource for SQL Databases
68
+ */
69
+ export class SequelizeCrudRepository<
70
+ T extends Entity,
71
+ ID,
72
+ Relations extends object = {},
73
+ > implements EntityCrudRepository<T, ID, Relations>
74
+ {
75
+ constructor(
76
+ public entityClass: typeof Entity & {
77
+ prototype: T;
78
+ },
79
+ public dataSource: SequelizeDataSource,
80
+ ) {
81
+ if (this.dataSource.sequelize) {
82
+ this.sequelizeModel = this.getSequelizeModel();
83
+ }
84
+ }
85
+ /**
86
+ * Default `order` filter style if only column name is specified
87
+ */
88
+ readonly DEFAULT_ORDER_STYLE = 'ASC';
89
+
90
+ /**
91
+ * Object keys used in models for set database specific settings.
92
+ * Example: In model property definition one can use postgresql dataType as float
93
+ * {
94
+ * type: 'number',
95
+ * postgresql: {
96
+ * dataType: 'float',
97
+ * precision: 20,
98
+ * scale: 4,
99
+ * },
100
+ * }
101
+ *
102
+ * This array of keys is used while building model definition for sequelize.
103
+ */
104
+ readonly DB_SPECIFIC_SETTINGS_KEYS = [
105
+ 'postgresql',
106
+ 'mysql',
107
+ 'sqlite3',
108
+ ] as const;
109
+
110
+ public readonly inclusionResolvers: Map<
111
+ string,
112
+ InclusionResolver<T, Entity>
113
+ > = new Map();
114
+
115
+ /**
116
+ * Sequelize Model Instance created from the model definition received from the `entityClass`
117
+ */
118
+ public sequelizeModel: ModelStatic<Model<T>>;
119
+
120
+ async create(entity: DataObject<T>, options?: AnyObject): Promise<T> {
121
+ let err = null;
122
+ const data = await this.sequelizeModel
123
+ .create(entity as MakeNullishOptional<T>, options)
124
+ .catch(error => {
125
+ console.error(error);
126
+ err = error;
127
+ });
128
+
129
+ if (!data) {
130
+ throw new Error(err ?? 'Something went wrong');
131
+ }
132
+ return new this.entityClass(this.excludeHiddenProps(data.toJSON())) as T;
133
+ }
134
+
135
+ async createAll(
136
+ entities: DataObject<T>[],
137
+ options?: AnyObject,
138
+ ): Promise<T[]> {
139
+ const models = await this.sequelizeModel.bulkCreate(
140
+ entities as MakeNullishOptional<T>[],
141
+ options,
142
+ );
143
+
144
+ return this.toEntities(models);
145
+ }
146
+
147
+ exists(id: ID, _options?: AnyObject): Promise<boolean> {
148
+ return new Promise((resolve, reject) => {
149
+ this.sequelizeModel
150
+ .findByPk(id as unknown as Identifier)
151
+ .then(value => {
152
+ resolve(!!value);
153
+ })
154
+ .catch(err => {
155
+ reject(err);
156
+ });
157
+ });
158
+ }
159
+
160
+ async save(entity: T, options?: AnyObject): Promise<T> {
161
+ const id = this.entityClass.getIdOf(entity);
162
+ if (id == null) {
163
+ return this.create(entity, options);
164
+ } else {
165
+ await this.replaceById(id, entity, options);
166
+ return new this.entityClass(entity.toObject()) as T;
167
+ }
168
+ }
169
+
170
+ update(entity: T, options?: AnyObject): Promise<void> {
171
+ return this.updateById(entity.getId(), entity, options);
172
+ }
173
+
174
+ async updateById(
175
+ id: ID,
176
+ data: DataObject<T>,
177
+ options?: AnyObject,
178
+ ): Promise<void> {
179
+ if (id === undefined) {
180
+ throw new Error('Invalid Argument: id cannot be undefined');
181
+ }
182
+ const idProp = this.entityClass.definition.idProperties()[0];
183
+ const where = {} as Where<T>;
184
+ (where as AnyObject)[idProp] = id;
185
+ const result = await this.updateAll(data, where, options);
186
+ if (result.count === 0) {
187
+ throw new EntityNotFoundError(this.entityClass, id);
188
+ }
189
+ }
190
+
191
+ async updateAll(
192
+ data: DataObject<T>,
193
+ where?: Where<T>,
194
+ options?: AnyObject,
195
+ ): Promise<Count> {
196
+ const [affectedCount] = await this.sequelizeModel.update(
197
+ Object.assign({} as AnyObject, data),
198
+ {
199
+ where: this.buildSequelizeWhere(where),
200
+ ...options,
201
+ },
202
+ );
203
+ return {count: affectedCount};
204
+ }
205
+
206
+ async delete(entity: T, options?: AnyObject): Promise<void> {
207
+ return this.deleteById(entity.getId(), options);
208
+ }
209
+
210
+ async find(
211
+ filter?: Filter<T>,
212
+ options?: AnyObject,
213
+ ): Promise<(T & Relations)[]> {
214
+ const data = await this.sequelizeModel
215
+ .findAll({
216
+ include: this.buildSequelizeIncludeFilter(filter?.include),
217
+ where: this.buildSequelizeWhere(filter?.where),
218
+ attributes: this.buildSequelizeAttributeFilter(filter?.fields),
219
+ order: this.buildSequelizeOrder(filter?.order),
220
+ limit: filter?.limit,
221
+ offset: filter?.offset ?? filter?.skip,
222
+ ...options,
223
+ })
224
+ .catch(err => {
225
+ debug('findAll() error:', err);
226
+ throw new Error(err);
227
+ });
228
+
229
+ return this.includeReferencesIfRequested(
230
+ data,
231
+ this.entityClass,
232
+ filter?.include,
233
+ );
234
+ }
235
+
236
+ async findOne(
237
+ filter?: Filter<T>,
238
+ options?: AnyObject,
239
+ ): Promise<(T & Relations) | null> {
240
+ const data = await this.sequelizeModel
241
+ .findOne({
242
+ include: this.buildSequelizeIncludeFilter(filter?.include),
243
+ where: this.buildSequelizeWhere(filter?.where),
244
+ attributes: this.buildSequelizeAttributeFilter(filter?.fields),
245
+ order: this.buildSequelizeOrder(filter?.order),
246
+ offset: filter?.offset ?? filter?.skip,
247
+ ...options,
248
+ })
249
+ .catch(err => {
250
+ debug('findOne() error:', err);
251
+ throw new Error(err);
252
+ });
253
+
254
+ if (data === null) {
255
+ return Promise.resolve(null);
256
+ }
257
+
258
+ const resolved = await this.includeReferencesIfRequested(
259
+ [data],
260
+ this.entityClass,
261
+ filter?.include,
262
+ );
263
+
264
+ return resolved[0];
265
+ }
266
+
267
+ async findById(
268
+ id: ID,
269
+ filter?: FilterExcludingWhere<T>,
270
+ options?: AnyObject,
271
+ ): Promise<T & Relations> {
272
+ const data = await this.sequelizeModel.findByPk(
273
+ id as unknown as Identifier,
274
+ {
275
+ order: this.buildSequelizeOrder(filter?.order),
276
+ attributes: this.buildSequelizeAttributeFilter(filter?.fields),
277
+ include: this.buildSequelizeIncludeFilter(filter?.include),
278
+ limit: filter?.limit,
279
+ offset: filter?.offset ?? filter?.skip,
280
+ ...options,
281
+ },
282
+ );
283
+ if (!data) {
284
+ throw new EntityNotFoundError(this.entityClass, id);
285
+ }
286
+
287
+ const resolved = await this.includeReferencesIfRequested(
288
+ [data],
289
+ this.entityClass,
290
+ filter?.include,
291
+ );
292
+
293
+ return resolved[0];
294
+ }
295
+
296
+ async replaceById(
297
+ id: ID,
298
+ data: DataObject<T>,
299
+ options?: AnyObject | undefined,
300
+ ): Promise<void> {
301
+ const idProp = this.entityClass.definition.idProperties()[0];
302
+ if (idProp in data) {
303
+ delete data[idProp as keyof typeof data];
304
+ }
305
+
306
+ await this.updateById(id, data, options);
307
+ }
308
+
309
+ async deleteAll(
310
+ where?: Where<T> | undefined,
311
+ options?: AnyObject | undefined,
312
+ ): Promise<Count> {
313
+ const count = await this.sequelizeModel.destroy({
314
+ where: this.buildSequelizeWhere(where),
315
+ ...options,
316
+ });
317
+ return {count};
318
+ }
319
+
320
+ async deleteById(id: ID, options?: AnyObject | undefined): Promise<void> {
321
+ const idProp = this.entityClass.definition.idProperties()[0];
322
+
323
+ if (id === undefined) {
324
+ throw new Error(`Invalid Argument: ${idProp} cannot be undefined`);
325
+ }
326
+
327
+ const where = {} as Where<T>;
328
+ (where as AnyObject)[idProp] = id;
329
+ const count = await this.sequelizeModel.destroy({
330
+ where: this.buildSequelizeWhere(where),
331
+ ...options,
332
+ });
333
+
334
+ if (count === 0) {
335
+ throw new EntityNotFoundError(this.entityClass, id);
336
+ }
337
+ }
338
+
339
+ async count(where?: Where<T>, options?: AnyObject): Promise<Count> {
340
+ const count = await this.sequelizeModel.count({
341
+ where: this.buildSequelizeWhere<T>(where),
342
+ ...options,
343
+ });
344
+
345
+ return {count};
346
+ }
347
+
348
+ async execute(..._args: PositionalParameters): Promise<AnyObject> {
349
+ throw new Error(
350
+ 'RAW Query execution is currently NOT supported for Sequelize CRUD Repository.',
351
+ );
352
+ }
353
+
354
+ protected toEntities(models: Model<T, T>[]): T[] {
355
+ return models.map(m => new this.entityClass(m.toJSON()) as T);
356
+ }
357
+
358
+ /**
359
+ * Get Sequelize Operator
360
+ * @param key Name of the operator used in loopback eg. lt
361
+ * @returns Equivalent operator symbol if available in Sequelize eg `Op.lt`
362
+ */
363
+ protected getSequelizeOperator(key: keyof typeof operatorTranslations) {
364
+ const sequelizeOperator = operatorTranslations[key];
365
+ if (!sequelizeOperator) {
366
+ throw Error(`There is no equivalent operator for "${key}" in sequelize.`);
367
+ }
368
+ return sequelizeOperator;
369
+ }
370
+
371
+ /**
372
+ * Get Sequelize `attributes` filter value from `fields` of loopback.
373
+ * @param fields Loopback styles `fields` options. eg. `["name", "age"]`, `{ id: false }`
374
+ * @returns Sequelize Compatible Object/Array based on the fields provided. eg. `{ "exclude": ["id"] }`
375
+ */
376
+ protected buildSequelizeAttributeFilter(
377
+ fields?: Fields,
378
+ ): FindAttributeOptions | undefined {
379
+ if (fields === undefined) {
380
+ return undefined;
381
+ }
382
+
383
+ if (Array.isArray(fields)) {
384
+ // Both (sequelize and loopback filters) consider array as "only columns to include"
385
+ return fields;
386
+ }
387
+
388
+ const sequelizeFields: FindAttributeOptions = {
389
+ include: [],
390
+ exclude: [],
391
+ };
392
+
393
+ // Push column having `false` values in `exclude` key and columns
394
+ // having `true` in `include` key
395
+ if (isTruelyObject(fields)) {
396
+ for (const key in fields) {
397
+ if (fields[key] === true) {
398
+ sequelizeFields.include?.push(key);
399
+ } else if (fields[key] === false) {
400
+ sequelizeFields.exclude?.push(key);
401
+ }
402
+ }
403
+ }
404
+
405
+ if (
406
+ Array.isArray(sequelizeFields.include) &&
407
+ sequelizeFields.include.length > 0
408
+ ) {
409
+ delete sequelizeFields.exclude;
410
+ return sequelizeFields.include;
411
+ }
412
+
413
+ if (
414
+ Array.isArray(sequelizeFields.exclude) &&
415
+ sequelizeFields.exclude.length > 0
416
+ ) {
417
+ delete sequelizeFields.include;
418
+ }
419
+ return sequelizeFields;
420
+ }
421
+
422
+ /**
423
+ * Get Sequelize Order filter value from loopback style order value
424
+ * @param order Sorting order in loopback style filter. eg. `title ASC`, `["id DESC", "age ASC"]`
425
+ * @returns Sequelize compatible order filter value
426
+ */
427
+ protected buildSequelizeOrder(order?: string[] | string): Order | undefined {
428
+ if (order === undefined) {
429
+ return undefined;
430
+ }
431
+
432
+ if (typeof order === 'string') {
433
+ const [columnName, orderType] = order.trim().split(' ');
434
+ return [[columnName, orderType ?? this.DEFAULT_ORDER_STYLE]];
435
+ }
436
+
437
+ return order.map(orderStr => {
438
+ const [columnName, orderType] = orderStr.trim().split(' ');
439
+ return [columnName, orderType ?? this.DEFAULT_ORDER_STYLE];
440
+ });
441
+ }
442
+
443
+ /**
444
+ * Build Sequelize compatible `include` filter
445
+ * @param inclusionFilters - loopback style `where` condition
446
+ * @param sourceModel - sequelize model instance
447
+ * @returns Sequelize compatible `Includeable` array
448
+ */
449
+ protected buildSequelizeIncludeFilter(
450
+ inclusionFilters?: Array<InclusionFilter & {required?: boolean}>,
451
+ sourceModel?: ModelStatic<Model<T>>,
452
+ ): Includeable[] {
453
+ if (!inclusionFilters || inclusionFilters.length === 0) {
454
+ return [];
455
+ }
456
+
457
+ if (!sourceModel) {
458
+ sourceModel = this.sequelizeModel;
459
+ }
460
+
461
+ const includable: Includeable[] = [];
462
+
463
+ for (const filter of inclusionFilters) {
464
+ if (typeof filter === 'string') {
465
+ if (filter in sourceModel.associations) {
466
+ includable.push(filter);
467
+ } else {
468
+ debug(
469
+ `Relation '${filter}' is not available in sequelize model associations. If it's referencesMany relation it will fallback to loopback inclusion resolver.`,
470
+ );
471
+ }
472
+ } else if (typeof filter === 'object') {
473
+ if (!(filter.relation in sourceModel.associations)) {
474
+ debug(
475
+ `Relation '${filter.relation}' is not available in sequelize model associations. If it's referencesMany relation it will fallback to loopback inclusion resolver.`,
476
+ );
477
+ continue;
478
+ }
479
+
480
+ const targetAssociation = sourceModel.associations[filter.relation];
481
+
482
+ includable.push({
483
+ model: targetAssociation.target,
484
+ /**
485
+ * Exclude through model data from response to be backward compatible
486
+ * with loopback response style for hasMany through relation.
487
+ * Does not work with sqlite3
488
+ */
489
+ ...(targetAssociation.associationType === 'BelongsToMany' &&
490
+ targetAssociation.isMultiAssociation
491
+ ? {through: {attributes: []}}
492
+ : {}),
493
+
494
+ where: this.buildSequelizeWhere(filter.scope?.where),
495
+ limit: filter.scope?.totalLimit ?? filter.scope?.limit,
496
+ attributes: this.buildSequelizeAttributeFilter(filter.scope?.fields),
497
+ include: this.buildSequelizeIncludeFilter(
498
+ filter.scope?.include,
499
+ targetAssociation.target,
500
+ ),
501
+ order: this.buildSequelizeOrder(filter.scope?.order),
502
+ as: filter.relation,
503
+
504
+ /**
505
+ * If true, uses an inner join, which means that the parent model will only be loaded if it has any matching children.
506
+ */
507
+ required: !!filter.required,
508
+
509
+ /**
510
+ * saperate: true is required for `order` and `limit` filter to work, it runs include in saperate queries
511
+ */
512
+ separate:
513
+ !!filter.scope?.order ||
514
+ !!(filter.scope?.totalLimit ?? filter.scope?.limit),
515
+ });
516
+ }
517
+ }
518
+
519
+ return includable;
520
+ }
521
+
522
+ /**
523
+ * Build Sequelize compatible where condition object
524
+ * @param where loopback style `where` condition
525
+ * @returns Sequelize compatible where options to be used in queries
526
+ */
527
+ protected buildSequelizeWhere<MT extends T>(
528
+ where?: Where<MT>,
529
+ ): WhereOptions<MT> {
530
+ if (!where) {
531
+ return {};
532
+ }
533
+
534
+ const sequelizeWhere: WhereOptions = {};
535
+
536
+ /**
537
+ * Handle model attribute conditions like `{ age: { gt: 18 } }`, `{ email: "a@b.c" }`
538
+ * Transform Operators - eg. `{ gt: 0, lt: 10 }` to `{ [Op.gt]: 0, [Op.lt]: 10 }`
539
+ */
540
+ for (const columnName in where) {
541
+ const conditionValue = <Object | Array<Object> | number | string | null>(
542
+ where[columnName as keyof typeof where]
543
+ );
544
+
545
+ if (isTruelyObject(conditionValue)) {
546
+ sequelizeWhere[columnName] = {};
547
+
548
+ for (const lb4Operator of Object.keys(<Object>conditionValue)) {
549
+ const sequelizeOperator = this.getSequelizeOperator(
550
+ lb4Operator as keyof typeof operatorTranslations,
551
+ );
552
+ sequelizeWhere[columnName][sequelizeOperator] =
553
+ conditionValue![lb4Operator as keyof typeof conditionValue];
554
+ }
555
+ } else if (
556
+ ['and', 'or'].includes(columnName) &&
557
+ Array.isArray(conditionValue)
558
+ ) {
559
+ /**
560
+ * Eg. {and: [{title: 'My Post'}, {content: 'Hello'}]}
561
+ */
562
+ const sequelizeOperator = this.getSequelizeOperator(
563
+ columnName as 'and' | 'or',
564
+ );
565
+ const conditions = conditionValue.map((condition: unknown) => {
566
+ return this.buildSequelizeWhere<MT>(condition as Where<MT>);
567
+ });
568
+ Object.assign(sequelizeWhere, {
569
+ [sequelizeOperator]: conditions,
570
+ });
571
+ } else {
572
+ // Equals
573
+ sequelizeWhere[columnName] = {
574
+ [Op.eq]: conditionValue,
575
+ };
576
+ }
577
+ }
578
+
579
+ return sequelizeWhere;
580
+ }
581
+
582
+ /**
583
+ * Get Sequelize Model
584
+ * @returns Sequelize Model Instance based on the definitions from `entityClass`
585
+ */
586
+ public getSequelizeModel(entityClass = this.entityClass) {
587
+ if (!this.dataSource.sequelize) {
588
+ throw Error(
589
+ `The datasource "${this.dataSource.name}" doesn't have sequelize instance bound to it.`,
590
+ );
591
+ }
592
+
593
+ if (this.dataSource.sequelize.models[entityClass.modelName]) {
594
+ // Model Already Defined by Sequelize before
595
+ return this.dataSource.sequelize.models[entityClass.modelName];
596
+ }
597
+
598
+ // TODO: Make it more flexible, check support of all possible definition props
599
+ const sourceModel = this.dataSource.sequelize.define(
600
+ entityClass.modelName,
601
+ this.getSequelizeModelAttributes(entityClass.definition.properties),
602
+ {
603
+ timestamps: false,
604
+ tableName: entityClass.modelName.toLowerCase(),
605
+ freezeTableName: true,
606
+ },
607
+ );
608
+
609
+ // Setup associations
610
+ for (const key in entityClass.definition.relations) {
611
+ const targetModel = this.getSequelizeModel(
612
+ entityClass.definition.relations[key].target(),
613
+ );
614
+
615
+ debugModelBuilder(
616
+ `Setting up relation`,
617
+ entityClass.definition.relations[key],
618
+ );
619
+
620
+ if (
621
+ entityClass.definition.relations[key].type ===
622
+ LoopbackRelationType.belongsTo
623
+ ) {
624
+ const foreignKey = (
625
+ entityClass.definition.relations[key] as BelongsToDefinition
626
+ ).keyTo;
627
+ sourceModel.belongsTo(targetModel, {
628
+ foreignKey: {name: foreignKey},
629
+
630
+ // Which client will pass on in loopback style include filter, eg. `include: ["thisName"]`
631
+ as: entityClass.definition.relations[key].name,
632
+ });
633
+ } else if (
634
+ entityClass.definition.relations[key].type ===
635
+ LoopbackRelationType.hasOne
636
+ ) {
637
+ const foreignKey = (
638
+ entityClass.definition.relations[key] as HasOneDefinition
639
+ ).keyTo;
640
+
641
+ sourceModel.hasOne(targetModel, {
642
+ foreignKey: foreignKey,
643
+ as: entityClass.definition.relations[key].name,
644
+ });
645
+ } else if (
646
+ entityClass.definition.relations[key].type ===
647
+ LoopbackRelationType.hasMany
648
+ ) {
649
+ const relationDefinition = entityClass.definition.relations[
650
+ key
651
+ ] as HasManyDefinition;
652
+ const through = relationDefinition.through;
653
+ const foreignKey = relationDefinition.keyTo;
654
+ if (through) {
655
+ const keyTo = through.keyTo;
656
+ const keyFrom = through.keyFrom;
657
+ // Setup hasMany through
658
+ const throughModel = this.getSequelizeModel(through.model());
659
+
660
+ sourceModel.belongsToMany(targetModel, {
661
+ through: {model: throughModel},
662
+ otherKey: keyTo,
663
+ foreignKey: keyFrom,
664
+ as: entityClass.definition.relations[key].name,
665
+ });
666
+ } else {
667
+ sourceModel.hasMany(targetModel, {
668
+ foreignKey: foreignKey,
669
+ as: entityClass.definition.relations[key].name,
670
+ });
671
+ }
672
+ }
673
+ }
674
+
675
+ debugModelBuilder(
676
+ 'Table name supplied to sequelize'.concat(
677
+ `"${entityClass.modelName.toLowerCase()}"`,
678
+ ),
679
+ );
680
+
681
+ return sourceModel;
682
+ }
683
+
684
+ /**
685
+ * Run CREATE TABLE query for the target sequelize model, Useful for quick testing
686
+ * @param options Sequelize Sync Options
687
+ */
688
+ async syncSequelizeModel(options: SyncOptions = {}) {
689
+ await this.dataSource.sequelize?.models[this.entityClass.modelName]
690
+ .sync(options)
691
+ .catch(console.error);
692
+ }
693
+ /**
694
+ * Run CREATE TABLE query for the all sequelize models, Useful for quick testing
695
+ * @param options Sequelize Sync Options
696
+ */
697
+ async syncLoadedSequelizeModels(options: SyncOptions = {}) {
698
+ await this.dataSource.sequelize?.sync(options).catch(console.error);
699
+ }
700
+
701
+ /**
702
+ * Get Sequelize Model Attributes
703
+ * @param definition property definition received from loopback entityClass eg. `{ id: { type: "Number", id: true } }`
704
+ * @returns model attributes supported in sequelize model definiotion
705
+ *
706
+ * TODO: Verify all possible loopback types https://loopback.io/doc/en/lb4/LoopBack-types.html
707
+ */
708
+ protected getSequelizeModelAttributes(definition: {
709
+ [name: string]: PropertyDefinition;
710
+ }): ModelAttributes<SequelizeModel, Attributes<SequelizeModel>> {
711
+ debugModelBuilder('loopback model definition', definition);
712
+
713
+ const sequelizeDefinition: ModelAttributes = {};
714
+
715
+ for (const propName in definition) {
716
+ // Set data type, defaults to `DataTypes.STRING`
717
+ let dataType: DataType = DataTypes.STRING;
718
+
719
+ const isString =
720
+ definition[propName].type === String ||
721
+ ['String', 'string'].includes(definition[propName].type.toString());
722
+
723
+ if (
724
+ definition[propName].type === Number ||
725
+ ['Number', 'number'].includes(definition[propName].type.toString())
726
+ ) {
727
+ dataType = DataTypes.NUMBER;
728
+
729
+ // handle float
730
+ for (const dbKey of this.DB_SPECIFIC_SETTINGS_KEYS) {
731
+ if (!(dbKey in definition[propName])) {
732
+ continue;
733
+ }
734
+
735
+ const dbSpecificSetting = definition[propName][dbKey] as {
736
+ dataType: string;
737
+ };
738
+
739
+ if (
740
+ ['double precision', 'float', 'real'].includes(
741
+ dbSpecificSetting.dataType,
742
+ )
743
+ ) {
744
+ // TODO: Handle precision
745
+ dataType = DataTypes.FLOAT;
746
+ }
747
+ }
748
+ }
749
+
750
+ if (
751
+ definition[propName].type === Boolean ||
752
+ ['Boolean', 'boolean'].includes(definition[propName].type.toString())
753
+ ) {
754
+ dataType = DataTypes.BOOLEAN;
755
+ }
756
+
757
+ if (
758
+ definition[propName].type === Array ||
759
+ ['Array', 'array'].includes(definition[propName].type.toString())
760
+ ) {
761
+ // Postgres only
762
+ dataType = DataTypes.ARRAY(DataTypes.INTEGER);
763
+ }
764
+
765
+ if (
766
+ definition[propName].type === Object ||
767
+ ['object', 'Object'].includes(definition[propName].type.toString())
768
+ ) {
769
+ // Postgres only, JSON dataType
770
+ dataType = DataTypes.JSON;
771
+ }
772
+
773
+ if (
774
+ definition[propName].type === Date ||
775
+ ['date', 'Date'].includes(definition[propName].type.toString())
776
+ ) {
777
+ dataType = DataTypes.DATE;
778
+ }
779
+
780
+ if (dataType === DataTypes.STRING && !isString) {
781
+ throw Error(
782
+ `Unhandled DataType "${definition[
783
+ propName
784
+ ].type.toString()}" for column "${propName}" in sequelize extension`,
785
+ );
786
+ }
787
+
788
+ const columnOptions: ModelAttributeColumnOptions = {
789
+ type: dataType,
790
+ ...('default' in definition[propName]
791
+ ? {defaultValue: definition[propName].default}
792
+ : {}),
793
+ };
794
+
795
+ // Set column as `primaryKey` when id is set to true (which is loopback way to define primary key)
796
+ if (definition[propName].id === true) {
797
+ if (columnOptions.type === DataTypes.NUMBER) {
798
+ columnOptions.type = DataTypes.INTEGER;
799
+ }
800
+ Object.assign(columnOptions, {
801
+ primaryKey: true,
802
+ /**
803
+ * `autoIncrement` needs to be true even if DataType is not INTEGER else it will pass the ID in the query set to NULL.
804
+ */
805
+ autoIncrement: !!definition[propName].generated,
806
+ } as typeof columnOptions);
807
+ }
808
+
809
+ // TODO: Get the column name casing using actual methods / conventions used in different sql connectors for loopback
810
+ columnOptions.field =
811
+ definition[propName]['name'] ?? propName.toLowerCase();
812
+
813
+ sequelizeDefinition[propName] = columnOptions;
814
+ }
815
+
816
+ debugModelBuilder('Sequelize model definition', sequelizeDefinition);
817
+ return sequelizeDefinition;
818
+ }
819
+
820
+ /**
821
+ * Remove hidden properties specified in model from response body. (See: https://github.com/sourcefuse/loopback4-sequelize/issues/3)
822
+ * @param entity normalized entity. You can use `entity.toJSON()`'s value
823
+ * @returns normalized entity excluding the hiddenProperties
824
+ */
825
+ protected excludeHiddenProps(entity: T & Relations): T & Relations {
826
+ const hiddenProps = this.entityClass.definition.settings.hiddenProperties;
827
+ if (!hiddenProps) {
828
+ return entity;
829
+ }
830
+
831
+ for (const propertyName of hiddenProps as Array<keyof typeof entity>) {
832
+ delete entity[propertyName];
833
+ }
834
+
835
+ return entity;
836
+ }
837
+
838
+ /**
839
+ * Include related entities of `@referencesMany` relation
840
+ *
841
+ * referencesMany relation is NOT handled by `sequelizeModel.findAll` as it doesn't have any direct alternative to it,
842
+ * so to include relation data of referencesMany, we're manually fetching related data requested
843
+ *
844
+ * @param parentEntities source table data
845
+ * @param filter actual payload passed in request
846
+ * @param parentEntityClass loopback entity class for the parent entity
847
+ * @returns entities with related models in them
848
+ */
849
+ protected async includeReferencesIfRequested(
850
+ parentEntities: Model<T, T>[],
851
+ parentEntityClass: typeof Entity,
852
+ inclusionFilters?: InclusionFilter[],
853
+ ): Promise<(T & Relations)[]> {
854
+ if (!parentEntityClass) {
855
+ parentEntityClass = this.entityClass;
856
+ }
857
+ /**
858
+ * All columns names defined in model with `@referencesMany`
859
+ */
860
+ const allReferencesColumns: string[] = [];
861
+ for (const key in parentEntityClass.definition.relations) {
862
+ if (
863
+ parentEntityClass.definition.relations[key].type ===
864
+ LoopbackRelationType.referencesMany
865
+ ) {
866
+ const loopbackRelationObject = parentEntityClass.definition.relations[
867
+ key
868
+ ] as ReferencesManyDefinition;
869
+ if (loopbackRelationObject.keyFrom) {
870
+ allReferencesColumns.push(loopbackRelationObject.keyFrom);
871
+ }
872
+ }
873
+ }
874
+
875
+ // Validate data type of items in any column having references
876
+ // For eg. convert ["1", "2"] into [1, 2] if `itemType` specified is `number[]`
877
+ const normalizedParentEntities = parentEntities.map(entity => {
878
+ const data = entity.toJSON();
879
+ for (const columnName in data) {
880
+ if (!allReferencesColumns.includes(columnName)) {
881
+ // Column is not the one used for referencesMany relation. Eg. "programmingLanguageIds"
882
+ continue;
883
+ }
884
+
885
+ const columnDefinition =
886
+ parentEntityClass.definition.properties[columnName];
887
+ if (
888
+ columnDefinition.type !== Array ||
889
+ !Array.isArray(data[columnName])
890
+ ) {
891
+ // Column type or data received is not array, wrong configuration/data
892
+ continue;
893
+ }
894
+
895
+ // Loop over all references in array received
896
+ const items = data[columnName] as unknown as Array<String | Number>;
897
+
898
+ for (let itemIndex = 0; itemIndex < items.length; itemIndex++) {
899
+ if (
900
+ columnDefinition.itemType === Number &&
901
+ typeof items[itemIndex] === 'string'
902
+ ) {
903
+ items[itemIndex] = parseInt(items[itemIndex] as string);
904
+ }
905
+ }
906
+
907
+ data[columnName] = items as unknown as T[Extract<keyof T, string>];
908
+ }
909
+
910
+ return data;
911
+ });
912
+
913
+ // Requested inclusions of referencesMany relation
914
+ const referencesManyInclusions: Array<{
915
+ /**
916
+ * Target Include filter entry
917
+ */
918
+ filter: Inclusion;
919
+ /**
920
+ * Loopback relation definition
921
+ */
922
+ definition: ReferencesManyDefinition;
923
+ /**
924
+ * Distinct foreignKey values of child model
925
+ * example: [1, 2, 4, 8]
926
+ */
927
+ keys: Array<T[]>;
928
+ }> = [];
929
+
930
+ for (let includeFilter of inclusionFilters ?? []) {
931
+ if (typeof includeFilter === 'string') {
932
+ includeFilter = {relation: includeFilter} as Inclusion;
933
+ }
934
+ const relationName = includeFilter.relation;
935
+ const relation = parentEntityClass.definition.relations[relationName];
936
+ if (relation.type === LoopbackRelationType.referencesMany) {
937
+ referencesManyInclusions.push({
938
+ filter: includeFilter,
939
+ definition: relation as ReferencesManyDefinition,
940
+ keys: [],
941
+ });
942
+ }
943
+ }
944
+
945
+ if (referencesManyInclusions.length === 0) {
946
+ const entityClasses = normalizedParentEntities.map(
947
+ e => new parentEntityClass(e),
948
+ );
949
+ return entityClasses as (T & Relations)[];
950
+ }
951
+
952
+ for (const relation of referencesManyInclusions) {
953
+ normalizedParentEntities.forEach(entity => {
954
+ if (!relation.definition.keyFrom) {
955
+ return;
956
+ }
957
+
958
+ const columnValue = entity[relation.definition.keyFrom as keyof T];
959
+
960
+ if (Array.isArray(columnValue)) {
961
+ relation.keys.push(...columnValue);
962
+ } else if (typeof columnValue === 'string' && columnValue.length > 0) {
963
+ relation.keys.push(
964
+ ...((columnValue as string).split(',') as unknown as Array<T[]>),
965
+ );
966
+ } else {
967
+ // column value holding references keys isn't an array nor a string
968
+ debug(
969
+ `Column "${
970
+ relation.definition.keyFrom
971
+ }"'s value holding references keys isn't an array for ${JSON.stringify(
972
+ entity,
973
+ )}, Can't fetch related models.`,
974
+ );
975
+ }
976
+ });
977
+ relation.keys = [...new Set(relation.keys)];
978
+
979
+ const foreignKey =
980
+ relation.definition.keyTo ??
981
+ relation.definition.target().definition.idProperties()[0];
982
+
983
+ // Strictly include primary key in attributes
984
+ const attributesToFetch = this.buildSequelizeAttributeFilter(
985
+ relation.filter.scope?.fields,
986
+ );
987
+ let includeForeignKeyInResponse = false;
988
+ if (attributesToFetch !== undefined) {
989
+ if (Array.isArray(attributesToFetch)) {
990
+ if (attributesToFetch.includes(foreignKey)) {
991
+ includeForeignKeyInResponse = true;
992
+ } else {
993
+ attributesToFetch.push(foreignKey);
994
+ }
995
+ } else if (Array.isArray(attributesToFetch.include)) {
996
+ if (attributesToFetch.include.includes(foreignKey)) {
997
+ includeForeignKeyInResponse = true;
998
+ } else {
999
+ attributesToFetch.include.push(foreignKey);
1000
+ }
1001
+ }
1002
+ } else {
1003
+ includeForeignKeyInResponse = true;
1004
+ }
1005
+
1006
+ const targetLoopbackModel = relation.definition.target();
1007
+ const targetSequelizeModel = this.getSequelizeModel(targetLoopbackModel);
1008
+ const sequelizeData = await targetSequelizeModel.findAll({
1009
+ where: {
1010
+ // eg. id: { [Op.in]: [1,2,4,8] }
1011
+ [foreignKey]: {
1012
+ [Op.in]: relation.keys,
1013
+ },
1014
+ ...this.buildSequelizeWhere(relation.filter.scope?.where),
1015
+ },
1016
+ attributes: attributesToFetch,
1017
+ include: this.buildSequelizeIncludeFilter(
1018
+ relation.filter.scope?.include,
1019
+ targetSequelizeModel,
1020
+ ),
1021
+ order: this.buildSequelizeOrder(relation.filter.scope?.order),
1022
+ limit:
1023
+ relation.filter.scope?.totalLimit ?? relation.filter.scope?.limit,
1024
+ offset: relation.filter.scope?.offset ?? relation.filter.scope?.skip,
1025
+ });
1026
+
1027
+ const childModelData = await this.includeReferencesIfRequested(
1028
+ sequelizeData,
1029
+ targetLoopbackModel,
1030
+ relation.filter.scope?.include,
1031
+ );
1032
+
1033
+ normalizedParentEntities.forEach(entity => {
1034
+ const foreignKeys = entity[relation.definition.keyFrom as keyof T];
1035
+ const filteredChildModels = childModelData.filter(childModel => {
1036
+ if (Array.isArray(foreignKeys)) {
1037
+ return foreignKeys?.includes(
1038
+ childModel[foreignKey as keyof typeof childModel],
1039
+ );
1040
+ } else {
1041
+ return true;
1042
+ }
1043
+ });
1044
+ Object.assign(entity, {
1045
+ [relation.definition.name]: filteredChildModels.map(
1046
+ filteredChildModel => {
1047
+ const safeCopy = {...filteredChildModel};
1048
+ if (includeForeignKeyInResponse === false) {
1049
+ delete safeCopy[foreignKey as keyof typeof safeCopy];
1050
+ }
1051
+ return safeCopy;
1052
+ },
1053
+ ),
1054
+ });
1055
+ return new parentEntityClass(entity) as T & Relations;
1056
+ });
1057
+ }
1058
+
1059
+ return normalizedParentEntities as (T & Relations)[];
1060
+ }
1061
+
1062
+ /**
1063
+ * Register an inclusion resolver for the related model name.
1064
+ *
1065
+ * @param relationName - Name of the relation defined on the source model
1066
+ * @param resolver - Resolver function for getting related model entities
1067
+ */
1068
+ registerInclusionResolver(
1069
+ relationName: string,
1070
+ resolver: InclusionResolver<T, Entity>,
1071
+ ) {
1072
+ this.inclusionResolvers.set(relationName, resolver);
1073
+ }
1074
+
1075
+ /**
1076
+ * Function to create a constrained relation repository factory
1077
+ *
1078
+ * @example
1079
+ * ```ts
1080
+ * class CustomerRepository extends SequelizeCrudRepository<
1081
+ * Customer,
1082
+ * typeof Customer.prototype.id,
1083
+ * CustomerRelations
1084
+ * > {
1085
+ * public readonly orders: HasManyRepositoryFactory<Order, typeof Customer.prototype.id>;
1086
+ *
1087
+ * constructor(
1088
+ * protected db: SequelizeDataSource,
1089
+ * orderRepository: EntityCrudRepository<Order, typeof Order.prototype.id>,
1090
+ * ) {
1091
+ * super(Customer, db);
1092
+ * this.orders = this.createHasManyRepositoryFactoryFor(
1093
+ * 'orders',
1094
+ * orderRepository,
1095
+ * );
1096
+ * }
1097
+ * }
1098
+ * ```
1099
+ *
1100
+ * @param relationName - Name of the relation defined on the source model
1101
+ * @param targetRepo - Target repository instance
1102
+ */
1103
+ protected createHasManyRepositoryFactoryFor<
1104
+ Target extends Entity,
1105
+ TargetID,
1106
+ ForeignKeyType,
1107
+ >(
1108
+ relationName: string,
1109
+ targetRepositoryGetter: Getter<EntityCrudRepository<Target, TargetID>>,
1110
+ ): HasManyRepositoryFactory<Target, ForeignKeyType> {
1111
+ const meta = this.entityClass.definition.relations[relationName];
1112
+ return createHasManyRepositoryFactory<Target, TargetID, ForeignKeyType>(
1113
+ meta as HasManyDefinition,
1114
+ targetRepositoryGetter,
1115
+ );
1116
+ }
1117
+
1118
+ /**
1119
+ * Function to create a belongs to accessor
1120
+ *
1121
+ * @param relationName - Name of the relation defined on the source model
1122
+ * @param targetRepo - Target repository instance
1123
+ */
1124
+ protected createBelongsToAccessorFor<Target extends Entity, TargetId>(
1125
+ relationName: string,
1126
+ targetRepositoryGetter:
1127
+ | Getter<EntityCrudRepository<Target, TargetId>>
1128
+ | {
1129
+ [repoType: string]: Getter<EntityCrudRepository<Target, TargetId>>;
1130
+ },
1131
+ ): BelongsToAccessor<Target, ID> {
1132
+ const meta = this.entityClass.definition.relations[relationName];
1133
+ return createBelongsToAccessor<Target, TargetId, T, ID>(
1134
+ meta as BelongsToDefinition,
1135
+ targetRepositoryGetter,
1136
+ this,
1137
+ );
1138
+ }
1139
+
1140
+ /**
1141
+ * Function to create a constrained hasOne relation repository factory
1142
+ *
1143
+ * @param relationName - Name of the relation defined on the source model
1144
+ * @param targetRepo - Target repository instance
1145
+ */
1146
+ protected createHasOneRepositoryFactoryFor<
1147
+ Target extends Entity,
1148
+ TargetID,
1149
+ ForeignKeyType,
1150
+ >(
1151
+ relationName: string,
1152
+ targetRepositoryGetter:
1153
+ | Getter<EntityCrudRepository<Target, TargetID>>
1154
+ | {
1155
+ [repoType: string]: Getter<EntityCrudRepository<Target, TargetID>>;
1156
+ },
1157
+ ): HasOneRepositoryFactory<Target, ForeignKeyType> {
1158
+ const meta = this.entityClass.definition.relations[relationName];
1159
+ return createHasOneRepositoryFactory<Target, TargetID, ForeignKeyType>(
1160
+ meta as HasOneDefinition,
1161
+ targetRepositoryGetter,
1162
+ );
1163
+ }
1164
+
1165
+ /**
1166
+ * Function to create a constrained hasManyThrough relation repository factory
1167
+ *
1168
+ * @example
1169
+ * ```ts
1170
+ * class CustomerRepository extends SequelizeCrudRepository<
1171
+ * Customer,
1172
+ * typeof Customer.prototype.id,
1173
+ * CustomerRelations
1174
+ * > {
1175
+ * public readonly cartItems: HasManyRepositoryFactory<CartItem, typeof Customer.prototype.id>;
1176
+ *
1177
+ * constructor(
1178
+ * protected db: SequelizeDataSource,
1179
+ * cartItemRepository: EntityCrudRepository<CartItem, typeof, CartItem.prototype.id>,
1180
+ * throughRepository: EntityCrudRepository<Through, typeof Through.prototype.id>,
1181
+ * ) {
1182
+ * super(Customer, db);
1183
+ * this.cartItems = this.createHasManyThroughRepositoryFactoryFor(
1184
+ * 'cartItems',
1185
+ * cartItemRepository,
1186
+ * );
1187
+ * }
1188
+ * }
1189
+ * ```
1190
+ *
1191
+ * @param relationName - Name of the relation defined on the source model
1192
+ * @param targetRepo - Target repository instance
1193
+ * @param throughRepo - Through repository instance
1194
+ */
1195
+ protected createHasManyThroughRepositoryFactoryFor<
1196
+ Target extends Entity,
1197
+ TargetID,
1198
+ Through extends Entity,
1199
+ ThroughID,
1200
+ ForeignKeyType,
1201
+ >(
1202
+ relationName: string,
1203
+ targetRepositoryGetter:
1204
+ | Getter<EntityCrudRepository<Target, TargetID>>
1205
+ | {
1206
+ [repoType: string]: Getter<EntityCrudRepository<Target, TargetID>>;
1207
+ },
1208
+ throughRepositoryGetter: Getter<EntityCrudRepository<Through, ThroughID>>,
1209
+ ): HasManyThroughRepositoryFactory<
1210
+ Target,
1211
+ TargetID,
1212
+ Through,
1213
+ ForeignKeyType
1214
+ > {
1215
+ const meta = this.entityClass.definition.relations[relationName];
1216
+ return createHasManyThroughRepositoryFactory<
1217
+ Target,
1218
+ TargetID,
1219
+ Through,
1220
+ ThroughID,
1221
+ ForeignKeyType
1222
+ >(
1223
+ meta as HasManyDefinition,
1224
+ targetRepositoryGetter,
1225
+ throughRepositoryGetter,
1226
+ );
1227
+ }
1228
+
1229
+ /**
1230
+ * Function to create a references many accessor
1231
+ *
1232
+ * @param relationName - Name of the relation defined on the source model
1233
+ * @param targetRepo - Target repository instance
1234
+ */
1235
+ protected createReferencesManyAccessorFor<Target extends Entity, TargetId>(
1236
+ relationName: string,
1237
+ targetRepoGetter: Getter<EntityCrudRepository<Target, TargetId>>,
1238
+ ): ReferencesManyAccessor<Target, ID> {
1239
+ const meta = this.entityClass.definition.relations[relationName];
1240
+ return createReferencesManyAccessor<Target, TargetId, T, ID>(
1241
+ meta as ReferencesManyDefinition,
1242
+ targetRepoGetter,
1243
+ this,
1244
+ );
1245
+ }
1246
+ }