Skip to content
This repository was archived by the owner on Sep 4, 2024. It is now read-only.

Commit a1dd46f

Browse files
committed
feat(repository): add sequelize repository base class
exports `SequelizeRepository` to be used in place of `DefaultCrudRepository` in the target loopback 4 project
1 parent 3fcf68f commit a1dd46f

File tree

1 file changed

+382
-0
lines changed

1 file changed

+382
-0
lines changed
Lines changed: 382 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,382 @@
1+
import {
2+
AnyObject,
3+
Count,
4+
DataObject,
5+
DefaultCrudRepository,
6+
Entity,
7+
EntityNotFoundError,
8+
Fields,
9+
Filter,
10+
FilterExcludingWhere,
11+
Operators,
12+
PropertyDefinition,
13+
Where,
14+
} from '@loopback/repository';
15+
import {
16+
Attributes,
17+
DataType,
18+
DataTypes,
19+
FindAttributeOptions,
20+
Identifier,
21+
Model,
22+
ModelAttributeColumnOptions,
23+
ModelAttributes,
24+
ModelStatic,
25+
Op,
26+
Order,
27+
WhereOptions,
28+
} from 'sequelize';
29+
import {MakeNullishOptional} from 'sequelize/types/utils';
30+
import {SequelizeDataSource} from './sequelize.datasource.base';
31+
32+
class SequelizeModel extends Model implements Entity {
33+
getId() {
34+
return null;
35+
}
36+
getIdObject(): Object {
37+
return {};
38+
}
39+
toObject(options?: AnyObject | undefined): Object {
40+
return {};
41+
}
42+
}
43+
44+
const isTruelyObject = (value?: unknown) => {
45+
return typeof value === 'object' && !Array.isArray(value) && value !== null;
46+
};
47+
/**
48+
* @key Operator used in loopback
49+
* @value Equivalent operator in Sequelize
50+
*/
51+
const operatorTranslations: {
52+
[key in Operators]?: symbol;
53+
} = {
54+
eq: Op.eq,
55+
gt: Op.gt,
56+
gte: Op.gte,
57+
lt: Op.lt,
58+
lte: Op.lte,
59+
neq: Op.ne,
60+
between: Op.between,
61+
inq: Op.in,
62+
nin: Op.notIn,
63+
like: Op.like,
64+
nlike: Op.notLike,
65+
ilike: Op.iLike,
66+
nilike: Op.notILike,
67+
regexp: Op.regexp,
68+
and: Op.and,
69+
or: Op.or,
70+
};
71+
72+
export class SequelizeRepository<
73+
T extends Entity,
74+
ID,
75+
Relations extends object = {},
76+
> extends DefaultCrudRepository<T, ID, Relations> {
77+
sequelizeModel: ModelStatic<Model<T>>;
78+
constructor(
79+
public entityClass: typeof Entity & {
80+
prototype: T;
81+
},
82+
public dataSource: SequelizeDataSource,
83+
) {
84+
super(entityClass, dataSource);
85+
console.log('Juggler entity definition', entityClass.definition.properties);
86+
87+
if (this.dataSource.sequelize) {
88+
this.sequelizeModel = this.getSequelizeModel();
89+
}
90+
console.log('this.sequelizeModel', this.sequelizeModel);
91+
}
92+
93+
async create(
94+
entity: MakeNullishOptional<T>,
95+
options?: AnyObject,
96+
): Promise<T> {
97+
console.log('Create entity', entity);
98+
const data = await this.sequelizeModel.create(entity, options);
99+
return data.toJSON();
100+
}
101+
102+
// updateById isn't implemented saperately because the existing one internally
103+
// calls updateAll method which is handled below
104+
105+
async updateAll(
106+
data: DataObject<T>,
107+
where?: Where<T> | undefined,
108+
options?: AnyObject | undefined,
109+
): Promise<Count> {
110+
const [affectedCount] = await this.sequelizeModel.update(
111+
Object.assign({} as AnyObject, data),
112+
{
113+
where: where ? this.buildSequelizeWhere(where) : {},
114+
...options,
115+
},
116+
);
117+
return {count: affectedCount};
118+
}
119+
120+
async find(
121+
filter?: Filter<T>,
122+
options?: AnyObject,
123+
): Promise<(T & Relations)[]> {
124+
const data = await this.sequelizeModel.findAll({
125+
...(filter?.fields
126+
? {attributes: this.buildSequelizeAttributeFilter(filter.fields)}
127+
: {}),
128+
...(filter?.where ? {where: this.buildSequelizeWhere(filter.where)} : {}),
129+
...(filter?.order ? {order: this.buildSequelizeOrder(filter.order)} : {}),
130+
...(filter?.limit ? {limit: filter.limit} : {}),
131+
...(filter?.offset || filter?.skip
132+
? {offset: filter.offset ?? filter.skip}
133+
: {}),
134+
...options,
135+
});
136+
return data.map(entity => {
137+
return entity.toJSON();
138+
});
139+
}
140+
141+
async findById(
142+
id: ID,
143+
filter?: FilterExcludingWhere<T>,
144+
options?: AnyObject,
145+
): Promise<T & Relations> {
146+
const data = await this.sequelizeModel.findByPk(
147+
id as unknown as Identifier,
148+
{
149+
...(filter?.fields
150+
? {attributes: this.buildSequelizeAttributeFilter(filter.fields)}
151+
: {}),
152+
...(filter?.order
153+
? {order: this.buildSequelizeOrder(filter.order)}
154+
: {}),
155+
...(filter?.limit ? {limit: filter.limit} : {}),
156+
...(filter?.offset || filter?.skip
157+
? {offset: filter.offset ?? filter.skip}
158+
: {}),
159+
...options,
160+
},
161+
);
162+
if (!data) {
163+
throw new EntityNotFoundError(this.entityClass, id);
164+
}
165+
// TODO: include relations in object
166+
return data.toJSON() as T & Relations;
167+
}
168+
169+
replaceById(
170+
id: ID,
171+
data: DataObject<T>,
172+
options?: AnyObject | undefined,
173+
): Promise<void> {
174+
const idProp = this.modelClass.definition.idName();
175+
console.log('idProp', idProp);
176+
if (idProp in data) {
177+
delete data[idProp as keyof typeof data];
178+
}
179+
return this.updateById(id, data, options);
180+
}
181+
182+
async deleteAll(
183+
where?: Where<T> | undefined,
184+
options?: AnyObject | undefined,
185+
): Promise<Count> {
186+
const count = await this.sequelizeModel.destroy({
187+
where: where ? this.buildSequelizeWhere(where) : {},
188+
...options,
189+
});
190+
return {count};
191+
}
192+
193+
async deleteById(id: ID, options?: AnyObject | undefined): Promise<void> {
194+
if (id === undefined) {
195+
throw new Error('Invalid Argument: id cannot be undefined');
196+
}
197+
const idProp = this.modelClass.definition.idName();
198+
const where = {} as Where<T>;
199+
(where as AnyObject)[idProp] = id;
200+
const result = await this.deleteAll(where, options);
201+
if (result.count === 0) {
202+
throw new EntityNotFoundError(this.entityClass, id);
203+
}
204+
}
205+
206+
async count(where?: Where<T>, options?: AnyObject): Promise<Count> {
207+
const count = await this.sequelizeModel.count({
208+
...(where ? {where: this.buildSequelizeWhere<T>(where)} : {}),
209+
...options,
210+
});
211+
212+
return {count};
213+
}
214+
215+
private getSequelizeOperator(key: keyof typeof operatorTranslations) {
216+
const sequelizeOperator = operatorTranslations[key];
217+
if (!sequelizeOperator) {
218+
throw Error(`There is no equivalent operator for "${key}" in sequelize.`);
219+
}
220+
return sequelizeOperator;
221+
}
222+
223+
private buildSequelizeAttributeFilter(fields: Fields): FindAttributeOptions {
224+
if (Array.isArray(fields)) {
225+
return fields;
226+
}
227+
const sequelizeFields: FindAttributeOptions = {
228+
include: [],
229+
exclude: [],
230+
};
231+
if (isTruelyObject(fields)) {
232+
for (const key in fields) {
233+
if (fields[key] === true) {
234+
sequelizeFields.include?.push(key);
235+
} else if (fields[key] === false) {
236+
sequelizeFields.exclude?.push(key);
237+
}
238+
}
239+
}
240+
if (
241+
Array.isArray(sequelizeFields.include) &&
242+
sequelizeFields.include.length > 0
243+
) {
244+
delete sequelizeFields.exclude;
245+
return sequelizeFields.include;
246+
}
247+
248+
if (
249+
Array.isArray(sequelizeFields.exclude) &&
250+
sequelizeFields.exclude.length > 0
251+
) {
252+
delete sequelizeFields.include;
253+
}
254+
return sequelizeFields;
255+
}
256+
257+
private buildSequelizeOrder(order: string[] | string): Order {
258+
if (typeof order === 'string') {
259+
const [columnName, orderType] = order.trim().split(' ');
260+
return [[columnName, orderType ?? 'ASC']];
261+
}
262+
return order.map(orderStr => {
263+
const [columnName, orderType] = orderStr.trim().split(' ');
264+
return [columnName, orderType ?? 'ASC'];
265+
});
266+
}
267+
268+
private buildSequelizeWhere<MT extends T>(
269+
where: Where<MT>,
270+
): WhereOptions<MT> {
271+
if (!where) {
272+
return {};
273+
}
274+
275+
const sequelizeWhere: WhereOptions = {};
276+
for (const columnName in where) {
277+
console.log('loop for', columnName, 'sequelizeWhere', sequelizeWhere);
278+
/**
279+
* Handle model attribute conditions like `{ age: { gt: 18 } }`, `{ email: "a@b.c" }`
280+
* Transform Operators - eg. `{ gt: 0, lt: 10 }` to `{ [Op.gt]: 0, [Op.lt]: 10 }`
281+
*/
282+
const conditionValue = <Object | Array<Object> | number | string | null>(
283+
where[columnName as keyof typeof where]
284+
);
285+
if (isTruelyObject(conditionValue)) {
286+
sequelizeWhere[columnName] = {};
287+
for (const lb4Operator of Object.keys(<Object>conditionValue)) {
288+
const sequelizeOperator = this.getSequelizeOperator(
289+
lb4Operator as keyof typeof operatorTranslations,
290+
);
291+
sequelizeWhere[columnName][sequelizeOperator] =
292+
conditionValue![lb4Operator as keyof typeof conditionValue];
293+
294+
console.log(
295+
'build ',
296+
columnName,
297+
sequelizeWhere[columnName][sequelizeOperator],
298+
);
299+
}
300+
} else if (
301+
['and', 'or'].includes(columnName) &&
302+
Array.isArray(conditionValue)
303+
) {
304+
/**
305+
* Eg. {and: [{title: 'My Post'}, {content: 'Hello'}]}
306+
*/
307+
const sequelizeOperator = this.getSequelizeOperator(
308+
columnName as 'and' | 'or',
309+
);
310+
const conditions = conditionValue.map((condition: unknown) => {
311+
return this.buildSequelizeWhere<MT>(condition as Where<MT>);
312+
});
313+
Object.assign(sequelizeWhere, {
314+
[sequelizeOperator]: conditions,
315+
});
316+
} else {
317+
// equals
318+
console.log('equals column name', columnName);
319+
sequelizeWhere[columnName] = {
320+
[Op.eq]: conditionValue,
321+
};
322+
}
323+
}
324+
325+
console.log('build sequelize where', where, '=>', sequelizeWhere);
326+
return sequelizeWhere;
327+
}
328+
329+
private getSequelizeModel() {
330+
if (!this.dataSource.sequelize) {
331+
throw Error(
332+
`The datasource "${this.dataSource.name}" doesn't have sequelize instance bound to it.`,
333+
);
334+
}
335+
console.log('Modelname', this.entityClass.modelName);
336+
337+
if (this.dataSource.sequelize.models[this.entityClass.modelName]) {
338+
console.log('target sequelize returned.');
339+
return this.dataSource.sequelize.models[this.entityClass.modelName];
340+
}
341+
342+
this.dataSource.sequelize.define(
343+
this.entityClass.modelName,
344+
this.getSequelizeModelAttributes(this.entityClass.definition.properties),
345+
{
346+
createdAt: false,
347+
updatedAt: false,
348+
tableName: this.entityClass.modelName.toLowerCase(),
349+
},
350+
);
351+
return this.dataSource.sequelize.models[this.entityClass.modelName];
352+
}
353+
354+
private getSequelizeModelAttributes(definition: {
355+
[name: string]: PropertyDefinition;
356+
}): ModelAttributes<SequelizeModel, Attributes<SequelizeModel>> {
357+
const sequelizeDefinition: ModelAttributes = {};
358+
for (const propName in definition) {
359+
// Set data type
360+
let dataType: DataType = DataTypes.STRING;
361+
if (definition[propName].type === 'Number') {
362+
dataType = DataTypes.NUMBER;
363+
}
364+
const columnOptions: ModelAttributeColumnOptions = {
365+
type: dataType,
366+
};
367+
368+
// set column as `primaryKey` when id is set to true (which is loopback way to define pk)
369+
if (definition[propName].id === true) {
370+
console.log(definition[propName]);
371+
Object.assign(columnOptions, {
372+
primaryKey: true,
373+
autoIncrement: true,
374+
} as typeof columnOptions);
375+
}
376+
console.log('columnOptions for', propName, columnOptions);
377+
sequelizeDefinition[propName] = columnOptions;
378+
}
379+
console.log('definition returned', sequelizeDefinition);
380+
return sequelizeDefinition;
381+
}
382+
}

0 commit comments

Comments
 (0)