@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.
- package/LICENSE +25 -0
- package/README.md +194 -0
- package/dist/.sandbox/6646miobBk/application.js +15 -0
- package/dist/.sandbox/6646miobBk/controllers/book-category.controller.js +41 -0
- package/dist/.sandbox/6646miobBk/controllers/book.controller.js +210 -0
- package/dist/.sandbox/6646miobBk/controllers/category.controller.js +198 -0
- package/dist/.sandbox/6646miobBk/controllers/developer.controller.js +177 -0
- package/dist/.sandbox/6646miobBk/controllers/doctor-patient.controller.js +112 -0
- package/dist/.sandbox/6646miobBk/controllers/doctor.controller.js +177 -0
- package/dist/.sandbox/6646miobBk/controllers/index.js +20 -0
- package/dist/.sandbox/6646miobBk/controllers/patient.controller.js +165 -0
- package/dist/.sandbox/6646miobBk/controllers/programming-languange.controller.js +204 -0
- package/dist/.sandbox/6646miobBk/controllers/test.controller.base.js +25 -0
- package/dist/.sandbox/6646miobBk/controllers/todo-list-todo.controller.js +113 -0
- package/dist/.sandbox/6646miobBk/controllers/todo-list.controller.js +177 -0
- package/dist/.sandbox/6646miobBk/controllers/todo-todo-list.controller.js +41 -0
- package/dist/.sandbox/6646miobBk/controllers/todo.controller.js +177 -0
- package/dist/.sandbox/6646miobBk/controllers/user-todo-list.controller.js +113 -0
- package/dist/.sandbox/6646miobBk/controllers/user.controller.js +210 -0
- package/dist/.sandbox/6646miobBk/datasources/db.datasource.js +28 -0
- package/dist/.sandbox/6646miobBk/models/appointment.model.js +37 -0
- package/dist/.sandbox/6646miobBk/models/book.model.js +48 -0
- package/dist/.sandbox/6646miobBk/models/category.model.js +32 -0
- package/dist/.sandbox/6646miobBk/models/developer.model.js +40 -0
- package/dist/.sandbox/6646miobBk/models/doctor.model.js +44 -0
- package/dist/.sandbox/6646miobBk/models/index.js +15 -0
- package/dist/.sandbox/6646miobBk/models/patient.model.js +32 -0
- package/dist/.sandbox/6646miobBk/models/programming-language.model.js +32 -0
- package/dist/.sandbox/6646miobBk/models/todo-list.model.js +45 -0
- package/dist/.sandbox/6646miobBk/models/todo.model.js +44 -0
- package/dist/.sandbox/6646miobBk/models/user.model.js +85 -0
- package/dist/.sandbox/6646miobBk/repositories/appointment.repository.js +20 -0
- package/dist/.sandbox/6646miobBk/repositories/book.repository.js +25 -0
- package/dist/.sandbox/6646miobBk/repositories/category.repository.js +20 -0
- package/dist/.sandbox/6646miobBk/repositories/developer.repository.js +25 -0
- package/dist/.sandbox/6646miobBk/repositories/doctor.repository.js +27 -0
- package/dist/.sandbox/6646miobBk/repositories/index.js +15 -0
- package/dist/.sandbox/6646miobBk/repositories/patient.repository.js +20 -0
- package/dist/.sandbox/6646miobBk/repositories/programming-language.repository.js +20 -0
- package/dist/.sandbox/6646miobBk/repositories/todo-list.repository.js +25 -0
- package/dist/.sandbox/6646miobBk/repositories/todo.repository.js +25 -0
- package/dist/.sandbox/6646miobBk/repositories/user.repository.js +29 -0
- package/dist/component.d.ts +7 -0
- package/dist/component.js +30 -0
- package/dist/component.js.map +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +12 -0
- package/dist/index.js.map +1 -0
- package/dist/keys.d.ts +8 -0
- package/dist/keys.js +16 -0
- package/dist/keys.js.map +1 -0
- package/dist/sequelize/connector-mapping.d.ts +9 -0
- package/dist/sequelize/connector-mapping.js +19 -0
- package/dist/sequelize/connector-mapping.js.map +1 -0
- package/dist/sequelize/index.d.ts +2 -0
- package/dist/sequelize/index.js +10 -0
- package/dist/sequelize/index.js.map +1 -0
- package/dist/sequelize/operator-translation.d.ts +8 -0
- package/dist/sequelize/operator-translation.js +31 -0
- package/dist/sequelize/operator-translation.js.map +1 -0
- package/dist/sequelize/sequelize.datasource.base.d.ts +23 -0
- package/dist/sequelize/sequelize.datasource.base.js +60 -0
- package/dist/sequelize/sequelize.datasource.base.js.map +1 -0
- package/dist/sequelize/sequelize.model.d.ts +7 -0
- package/dist/sequelize/sequelize.model.js +24 -0
- package/dist/sequelize/sequelize.model.js.map +1 -0
- package/dist/sequelize/sequelize.repository.base.d.ts +231 -0
- package/dist/sequelize/sequelize.repository.base.js +835 -0
- package/dist/sequelize/sequelize.repository.base.js.map +1 -0
- package/dist/sequelize/utils.d.ts +6 -0
- package/dist/sequelize/utils.js +17 -0
- package/dist/sequelize/utils.js.map +1 -0
- package/dist/types.d.ts +9 -0
- package/dist/types.js +14 -0
- package/dist/types.js.map +1 -0
- package/package.json +64 -0
- package/src/component.ts +34 -0
- package/src/index.ts +9 -0
- package/src/keys.ts +16 -0
- package/src/sequelize/connector-mapping.ts +26 -0
- package/src/sequelize/index.ts +7 -0
- package/src/sequelize/operator-translation.ts +32 -0
- package/src/sequelize/sequelize.datasource.base.ts +81 -0
- package/src/sequelize/sequelize.model.ts +22 -0
- package/src/sequelize/sequelize.repository.base.ts +1246 -0
- package/src/sequelize/utils.ts +13 -0
- 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
|
+
}
|