@drax/crud-back 2.9.0 → 3.0.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 (68) hide show
  1. package/dist/controllers/AbstractFastifyController.js +48 -25
  2. package/dist/regexs/QueryFilterRegex.js +1 -1
  3. package/dist/repository/AbstractMongoRepository.js +19 -15
  4. package/dist/repository/AbstractSqliteRepository.js +142 -60
  5. package/dist/services/AbstractService.js +9 -6
  6. package/package.json +8 -7
  7. package/src/controllers/AbstractFastifyController.ts +57 -29
  8. package/src/regexs/QueryFilterRegex.ts +1 -1
  9. package/src/repository/AbstractMongoRepository.ts +25 -16
  10. package/src/repository/AbstractSqliteRepository.ts +204 -66
  11. package/src/services/AbstractService.ts +12 -6
  12. package/test/_mocks/MockRepository.ts +1 -1
  13. package/test/controllers/PersonController.test.ts +547 -0
  14. package/test/people/controllers/CountryController.ts +40 -0
  15. package/test/people/controllers/LanguageController.ts +29 -0
  16. package/test/people/controllers/PersonController.ts +29 -0
  17. package/test/people/factory/services/CountryServiceFactory.ts +41 -0
  18. package/test/people/factory/services/LanguageServiceFactory.ts +41 -0
  19. package/test/people/factory/services/PersonServiceFactory.ts +41 -0
  20. package/test/people/interfaces/ICountry.ts +28 -0
  21. package/test/people/interfaces/ICountryRepository.ts +11 -0
  22. package/test/people/interfaces/ILanguage.ts +32 -0
  23. package/test/people/interfaces/ILanguageRepository.ts +11 -0
  24. package/test/people/interfaces/IPerson.ts +58 -0
  25. package/test/people/interfaces/IPersonRepository.ts +11 -0
  26. package/test/people/models/CountryModel.ts +38 -0
  27. package/test/people/models/LanguageModel.ts +40 -0
  28. package/test/people/models/PersonModel.ts +61 -0
  29. package/test/people/permissions/CountryPermissions.ts +14 -0
  30. package/test/people/permissions/LanguagePermissions.ts +14 -0
  31. package/test/people/permissions/PersonPermissions.ts +18 -0
  32. package/test/people/repository/mongo/CountryMongoRepository.ts +22 -0
  33. package/test/people/repository/mongo/LanguageMongoRepository.ts +22 -0
  34. package/test/people/repository/mongo/PersonMongoRepository.ts +22 -0
  35. package/test/people/repository/sqlite/CountrySqliteRepository.ts +33 -0
  36. package/test/people/repository/sqlite/LanguageSqliteRepository.ts +37 -0
  37. package/test/people/repository/sqlite/PersonSqliteRepository.ts +42 -0
  38. package/test/people/routes/CountryRoutes.ts +34 -0
  39. package/test/people/routes/LanguageRoutes.ts +34 -0
  40. package/test/people/routes/PersonRoutes.ts +40 -0
  41. package/test/people/schemas/CountrySchema.ts +22 -0
  42. package/test/people/schemas/LanguageSchema.ts +22 -0
  43. package/test/people/schemas/PersonSchema.ts +42 -0
  44. package/test/people/services/CountryService.ts +20 -0
  45. package/test/people/services/LanguageService.ts +20 -0
  46. package/test/people/services/PersonService.ts +20 -0
  47. package/test/services/AbstractService.test.ts +11 -7
  48. package/test/setup/MongoInMemory.ts +56 -0
  49. package/test/setup/TestSetup.ts +344 -0
  50. package/test/setup/data/admin-role.ts +13 -0
  51. package/test/setup/data/basic-user.ts +14 -0
  52. package/test/setup/data/one-tenant.ts +6 -0
  53. package/test/setup/data/restricted-role.ts +16 -0
  54. package/test/setup/data/root-user.ts +14 -0
  55. package/test/setup/data/tenant-one-user.ts +13 -0
  56. package/test/setup/data/tenant-two-user.ts +14 -0
  57. package/test/setup/data/two-tenant.ts +6 -0
  58. package/test/workers/ExportCsvWorker.test.ts +1 -1
  59. package/tsconfig.json +2 -1
  60. package/tsconfig.tsbuildinfo +1 -1
  61. package/types/controllers/AbstractFastifyController.d.ts.map +1 -1
  62. package/types/regexs/QueryFilterRegex.d.ts.map +1 -1
  63. package/types/repository/AbstractMongoRepository.d.ts +3 -3
  64. package/types/repository/AbstractMongoRepository.d.ts.map +1 -1
  65. package/types/repository/AbstractSqliteRepository.d.ts +6 -5
  66. package/types/repository/AbstractSqliteRepository.d.ts.map +1 -1
  67. package/types/services/AbstractService.d.ts +4 -2
  68. package/types/services/AbstractService.d.ts.map +1 -1
@@ -1,4 +1,4 @@
1
- import { CommonConfig, DraxConfig, LimitError, NotFoundError, BadRequestError, CommonController } from "@drax/common-back";
1
+ import { CommonConfig, DraxConfig, LimitError, NotFoundError, BadRequestError, CommonController, setNestedValue } from "@drax/common-back";
2
2
  import { join } from "path";
3
3
  import QueryFilterRegex from "../regexs/QueryFilterRegex.js";
4
4
  import CrudEventEmitter from "../events/CrudEventEmitter.js";
@@ -43,7 +43,7 @@ class AbstractFastifyController extends CommonController {
43
43
  const filters = [];
44
44
  filterArray.forEach((filter) => {
45
45
  const [field, operator, value] = filter.split(";");
46
- if (field && operator && (value !== undefined && value !== '')) {
46
+ if (field && operator && (operator === 'empty' || (value !== undefined && value !== ''))) {
47
47
  filters.push({ field, operator, value });
48
48
  }
49
49
  });
@@ -72,15 +72,26 @@ class AbstractFastifyController extends CommonController {
72
72
  }
73
73
  }
74
74
  assertTenant(item, rbac) {
75
- if (this.tenantAssert) {
76
- const itemTenantId = item[this.tenantField]?._id ? item[this.tenantField]._id.toString() : null;
77
- rbac.assertTenantId(itemTenantId);
75
+ //Si tenantAssert esta habilitado y si ademas el usuario pertenece a un tenant
76
+ if (this.tenantAssert && rbac.hasTenant) {
77
+ //Si esta populado
78
+ if (item[this.tenantField]?._id) {
79
+ rbac.assertTenantId(item[this.tenantField]._id.toString()); //
80
+ //Si esta crudo
81
+ }
82
+ else if (item[this.tenantField]) {
83
+ rbac.assertTenantId(item[this.tenantField].toString());
84
+ }
78
85
  }
79
86
  }
80
87
  assertUser(item, rbac) {
81
88
  if (this.userAssert) {
82
- const itemUserId = item[this.userField]?._id ? item[this.userField]._id.toString() : null;
83
- rbac.assertUserId(itemUserId);
89
+ if (item[this.userField]?._id) {
90
+ rbac.assertUserId(item[this.userField]._id.toString());
91
+ }
92
+ else if (item[this.userField]) {
93
+ rbac.assertUserId(item[this.userField].toString());
94
+ }
84
95
  }
85
96
  }
86
97
  assertUserAndTenant(item, rbac) {
@@ -89,10 +100,10 @@ class AbstractFastifyController extends CommonController {
89
100
  }
90
101
  applyUserAndTenantSetters(payload, rbac) {
91
102
  if (this.tenantSetter && rbac.tenantId) {
92
- payload[this.tenantField] = rbac.tenantId;
103
+ setNestedValue(payload, this.tenantField, rbac.tenantId);
93
104
  }
94
105
  if (this.userSetter && rbac.userId) {
95
- payload[this.userField] = rbac.userId;
106
+ setNestedValue(payload, this.userField, rbac.userId);
96
107
  }
97
108
  }
98
109
  extractRequestData(request) {
@@ -212,17 +223,21 @@ class AbstractFastifyController extends CommonController {
212
223
  const id = request.params.id;
213
224
  const payload = request.body;
214
225
  let preItem = await this.service.findById(id);
226
+ if (!preItem) {
227
+ reply.statusCode = 404;
228
+ reply.send({ error: 'NOT_FOUND' });
229
+ }
215
230
  if (!request.rbac.hasSomePermission([this.permission.All, this.permission.UpdateAll])) {
216
- if (!preItem) {
217
- reply.statusCode = 404;
218
- reply.send({ error: 'NOT_FOUND' });
219
- }
231
+ //Si assertUser habilitado y si el usuario no tiene UpdateAll/All, solo puede modificar sus propios registros
220
232
  this.assertUser(preItem, request.rbac);
221
233
  }
222
- //Definido el tenant/user en el create no debe modificarse en un update
234
+ //Si assertTenant habilitado y si usuario tiene tenant, solo puede modificar registros de su tenant
235
+ this.assertTenant(preItem, request.rbac);
236
+ //Definido el tenant en create no debe modificarse en un update
223
237
  if (this.tenantSetter) {
224
238
  delete payload[this.tenantField];
225
239
  }
240
+ //Definido el user en create no debe modificarse en un update
226
241
  if (this.userSetter) {
227
242
  delete payload[this.userField];
228
243
  }
@@ -255,17 +270,21 @@ class AbstractFastifyController extends CommonController {
255
270
  const id = request.params.id;
256
271
  const payload = request.body;
257
272
  let preItem = await this.service.findById(id);
273
+ if (!preItem) {
274
+ reply.statusCode = 404;
275
+ reply.send({ error: 'NOT_FOUND' });
276
+ }
258
277
  if (!request.rbac.hasSomePermission([this.permission.All, this.permission.UpdateAll])) {
259
- if (!preItem) {
260
- reply.statusCode = 404;
261
- reply.send({ error: 'NOT_FOUND' });
262
- }
278
+ //Si assertUser habilitado y si el usuario no tiene UpdateAll/All, solo puede modificar sus propios registros
263
279
  this.assertUser(preItem, request.rbac);
264
280
  }
265
- //Definido el tenant/user en el create no debe modificarse en un update
281
+ //Si assertTenant habilitado y si usuario tiene tenant, solo puede modificar registros de su tenant
282
+ this.assertTenant(preItem, request.rbac);
283
+ //Definido el tenant en el create no debe modificarse en un update
266
284
  if (this.tenantSetter) {
267
285
  delete payload[this.tenantField];
268
286
  }
287
+ //Definido el user en el create no debe modificarse en un update
269
288
  if (this.userSetter) {
270
289
  delete payload[this.userField];
271
290
  }
@@ -404,10 +423,12 @@ class AbstractFastifyController extends CommonController {
404
423
  const limit = this.defaultLimit;
405
424
  const field = request.params.field;
406
425
  const value = request.params.value;
407
- let items = await this.service.findBy(field, value, limit);
408
- for (let item of items) {
409
- this.assertUserAndTenant(item, request.rbac);
410
- }
426
+ let filters = [];
427
+ this.applyUserAndTenantFilters(filters, request.rbac);
428
+ let items = await this.service.findBy(field, value, limit, filters);
429
+ // for (let item of items) {
430
+ // this.assertUserAndTenant(item, request.rbac)
431
+ // }
411
432
  return items;
412
433
  }
413
434
  catch (e) {
@@ -423,8 +444,10 @@ class AbstractFastifyController extends CommonController {
423
444
  }
424
445
  const field = request.params.field;
425
446
  const value = request.params.value;
426
- let item = await this.service.findOneBy(field, value);
427
- this.assertUserAndTenant(item, request.rbac);
447
+ let filters = [];
448
+ this.applyUserAndTenantFilters(filters, request.rbac);
449
+ let item = await this.service.findOneBy(field, value, filters);
450
+ // this.assertUserAndTenant(item, request.rbac);
428
451
  return item;
429
452
  }
430
453
  catch (e) {
@@ -1,3 +1,3 @@
1
- const QueryFilterRegex = /^(?:[a-zA-Z0-9_.\-]+;(?:eq|like|ne|in|nin|gt|gte|lt|lte);[a-zA-Z0-9_.\-:\., áéíóúÁÉÍÓÚ]*)(?:\|[a-zA-Z0-9_.\-]+;(?:eq|like|ne|in|nin|gt|gte|lt|lte);[a-zA-Z0-9_.\-:\., áéíóúÁÉÍÓÚ]*)*$/;
1
+ const QueryFilterRegex = /^(?:[a-zA-Z0-9_.\-]+;(?:eq|like|ne|in|nin|gt|gte|lt|lte|empty);[a-zA-Z0-9_.\-:\., áéíóúÁÉÍÓÚ]*)(?:\|[a-zA-Z0-9_.\-]+;(?:eq|like|ne|in|nin|gt|gte|lt|lte|empty);[a-zA-Z0-9_.\-:\., áéíóúÁÉÍÓÚ]*)*$/;
2
2
  export default QueryFilterRegex;
3
3
  export { QueryFilterRegex };
@@ -89,28 +89,32 @@ class AbstractMongoRepository {
89
89
  .exec();
90
90
  return item;
91
91
  }
92
- async findByIds(ids) {
92
+ async findByIds(ids, filters = []) {
93
93
  ids.map(id => this.assertId(id));
94
+ const query = { _id: { $in: ids } };
95
+ MongooseQueryFilter.applyFilters(query, filters, this._model);
94
96
  const items = await this._model
95
- .find({ _id: { $in: ids } })
97
+ .find(query)
96
98
  .populate(this._populateFields)
97
99
  .lean(this._lean)
98
100
  .exec();
99
101
  return items;
100
102
  }
101
- async findOneBy(field, value) {
102
- const filter = { [field]: value };
103
+ async findOneBy(field, value, filters = []) {
104
+ const query = { [field]: value };
105
+ MongooseQueryFilter.applyFilters(query, filters, this._model);
103
106
  const item = await this._model
104
- .findOne(filter)
107
+ .findOne(query)
105
108
  .populate(this._populateFields)
106
109
  .lean(this._lean)
107
110
  .exec();
108
111
  return item;
109
112
  }
110
- async findBy(field, value, limit = 0) {
111
- const filter = { [field]: value };
113
+ async findBy(field, value, limit = 0, filters = []) {
114
+ const query = { [field]: value };
115
+ MongooseQueryFilter.applyFilters(query, filters, this._model);
112
116
  const items = await this._model
113
- .find(filter)
117
+ .find(query)
114
118
  .limit(limit)
115
119
  .populate(this._populateFields)
116
120
  .lean(this._lean)
@@ -133,7 +137,7 @@ class AbstractMongoRepository {
133
137
  else if (value) {
134
138
  query['$or'] = this._searchFields.map(field => ({ [field]: new RegExp(value.toString(), 'i') }));
135
139
  }
136
- MongooseQueryFilter.applyFilters(query, filters);
140
+ MongooseQueryFilter.applyFilters(query, filters, this._model);
137
141
  const items = await this._model
138
142
  .find(query)
139
143
  .limit(limit)
@@ -152,7 +156,7 @@ class AbstractMongoRepository {
152
156
  query['$or'] = this._searchFields.map(field => ({ [field]: new RegExp(search.toString(), 'i') }));
153
157
  }
154
158
  }
155
- MongooseQueryFilter.applyFilters(query, filters);
159
+ MongooseQueryFilter.applyFilters(query, filters, this._model);
156
160
  // console.log("Paginate Query", query)
157
161
  const sort = MongooseSort.applySort(orderBy, order);
158
162
  const populate = this._populateFields;
@@ -176,7 +180,7 @@ class AbstractMongoRepository {
176
180
  query['$or'] = this._searchFields.map(field => ({ [field]: new RegExp(search.toString(), 'i') }));
177
181
  }
178
182
  }
179
- MongooseQueryFilter.applyFilters(query, filters);
183
+ MongooseQueryFilter.applyFilters(query, filters, this._model);
180
184
  const item = this._model
181
185
  .findOne(query)
182
186
  .populate(this._populateFields)
@@ -194,7 +198,7 @@ class AbstractMongoRepository {
194
198
  query['$or'] = this._searchFields.map(field => ({ [field]: new RegExp(search.toString(), 'i') }));
195
199
  }
196
200
  }
197
- MongooseQueryFilter.applyFilters(query, filters);
201
+ MongooseQueryFilter.applyFilters(query, filters, this._model);
198
202
  const sort = MongooseSort.applySort(orderBy, order);
199
203
  const items = await this._model
200
204
  .find(query)
@@ -212,14 +216,14 @@ class AbstractMongoRepository {
212
216
  { name: new RegExp(search, 'i') },
213
217
  ];
214
218
  }
215
- MongooseQueryFilter.applyFilters(query, filters);
219
+ MongooseQueryFilter.applyFilters(query, filters, this._model);
216
220
  const sort = MongooseSort.applySort(orderBy, order);
217
221
  return this._model.find(query).limit(limit).sort(sort).cursor();
218
222
  }
219
223
  async groupBy({ fields = [], filters = [], dateFormat = 'day' }) {
220
224
  const query = {};
221
- MongooseQueryFilter.applyFilters(query, filters);
222
- // console.log("groupBy Query", query)
225
+ MongooseQueryFilter.applyFilters(query, filters, this._model);
226
+ // console.log("groupBy Query", query)
223
227
  // Obtener el schema para identificar campos de referencia y fechas
224
228
  const schema = this._model.schema;
225
229
  // Construir el objeto de agrupación dinámicamente
@@ -132,41 +132,61 @@ class AbstractSqliteRepository {
132
132
  async paginate({ page = 1, limit = 5, orderBy = '', order = 'desc', search = '', filters = [] }) {
133
133
  const offset = page > 1 ? (page - 1) * limit : 0;
134
134
  let where = "";
135
+ let params = [];
136
+ // SEARCH
135
137
  if (search && this.searchFields.length > 0) {
136
- where = ` WHERE ${this.searchFields.map(field => `${field} LIKE '%${search}%'`).join(" OR ")}`;
138
+ const searchConditions = this.searchFields
139
+ .map(field => `${field} LIKE ?`)
140
+ .join(" OR ");
141
+ where = ` WHERE (${searchConditions})`;
142
+ params.push(...this.searchFields.map(() => `%${search}%`));
137
143
  }
144
+ // FILTERS
138
145
  if (filters.length > 0) {
139
- where = SqlQueryFilter.applyFilters(where, filters);
146
+ const result = SqlQueryFilter.applyFilters(where, filters);
147
+ where = result.where;
148
+ params.push(...result.params);
140
149
  }
141
150
  const sort = SqlSort.applySort(orderBy, order);
142
- const rCount = this.db.prepare(`SELECT COUNT(*) as count
143
- FROM ${this.tableName} ${where}`).get();
144
- const items = this.db.prepare(`SELECT *
145
- FROM ${this.tableName} ${where} ${sort} LIMIT ?
146
- OFFSET ? `).all([limit, offset]);
151
+ // COUNT
152
+ const rCount = this.db
153
+ .prepare(`SELECT COUNT(*) as count FROM ${this.tableName} ${where}`)
154
+ .get(params);
155
+ // DATA
156
+ const items = this.db
157
+ .prepare(`SELECT * FROM ${this.tableName} ${where} ${sort} LIMIT ? OFFSET ?`)
158
+ .all([...params, limit, offset]);
147
159
  for (const item of items) {
148
160
  await this.decorate(item);
149
161
  }
150
162
  return {
151
- page: page,
152
- limit: limit,
163
+ page,
164
+ limit,
153
165
  total: rCount.count,
154
- items: items
166
+ items
155
167
  };
156
168
  }
157
169
  async find({ limit = 5, orderBy = '', order = 'desc', search = '', filters = [] }) {
158
170
  let where = "";
171
+ let params = [];
172
+ // SEARCH
159
173
  if (search && this.searchFields.length > 0) {
160
- where = ` WHERE ${this.searchFields.map(field => `${field} LIKE '%${search}%'`).join(" OR ")}`;
174
+ const searchConditions = this.searchFields
175
+ .map(field => `${field} LIKE ?`)
176
+ .join(" OR ");
177
+ where = ` WHERE (${searchConditions})`;
178
+ params.push(...this.searchFields.map(() => `%${search}%`));
161
179
  }
180
+ // FILTERS
162
181
  if (filters.length > 0) {
163
- where = SqlQueryFilter.applyFilters(where, filters);
182
+ const result = SqlQueryFilter.applyFilters(where, filters);
183
+ where = result.where;
184
+ params.push(...result.params);
164
185
  }
165
186
  const sort = SqlSort.applySort(orderBy, order);
166
- const rCount = this.db.prepare(`SELECT COUNT(*) as count
167
- FROM ${this.tableName} ${where}`).get();
168
- const items = this.db.prepare(`SELECT *
169
- FROM ${this.tableName} ${where} ${sort} LIMIT ? `).all([limit]);
187
+ const items = this.db
188
+ .prepare(`SELECT * FROM ${this.tableName} ${where} ${sort} LIMIT ?`)
189
+ .all([...params, limit]);
170
190
  for (const item of items) {
171
191
  await this.decorate(item);
172
192
  }
@@ -180,13 +200,19 @@ class AbstractSqliteRepository {
180
200
  }
181
201
  return items;
182
202
  }
183
- async search(value, limit = 1000) {
203
+ async search(value, limit = 1000, filters = []) {
184
204
  let where = "";
205
+ let params = [];
185
206
  if (value && this.searchFields.length > 0) {
186
- where = ` WHERE ${this.searchFields.map(field => `${field} LIKE '%${value}%'`).join(" OR ")}`;
207
+ const searchConditions = this.searchFields
208
+ .map(field => `${field} LIKE ?`)
209
+ .join(" OR ");
210
+ where = ` WHERE (${searchConditions})`;
211
+ params.push(...this.searchFields.map(() => `%${value}%`));
187
212
  }
188
- const items = this.db.prepare(`SELECT *
189
- FROM ${this.tableName} ${where} LIMIT ${limit}`).all();
213
+ const items = this.db
214
+ .prepare(`SELECT * FROM ${this.tableName} ${where} LIMIT ?`)
215
+ .all([...params, limit]);
190
216
  for (const item of items) {
191
217
  await this.decorate(item);
192
218
  }
@@ -199,32 +225,73 @@ class AbstractSqliteRepository {
199
225
  await this.decorate(item);
200
226
  return item;
201
227
  }
202
- async findBy(field, value, limit = 0) {
203
- const items = this.db.prepare(`SELECT *
204
- FROM ${this.tableName}
205
- WHERE ${field} = ? LIMIT ${limit}`).all(value);
228
+ async findByIds(ids, filters = []) {
229
+ const inPlaceholders = ids.map(() => '?').join(',');
230
+ let where = `WHERE ID IN(${inPlaceholders})`;
231
+ let params = [ids];
232
+ if (filters.length > 0) {
233
+ const result = SqlQueryFilter.applyFilters(where, filters);
234
+ where = result.where;
235
+ params.push(...result.params);
236
+ }
237
+ const items = this.db
238
+ .prepare(`SELECT * FROM ${this.tableName} ${where}`)
239
+ .all(params);
206
240
  for (const item of items) {
207
241
  await this.decorate(item);
208
242
  }
209
243
  return items;
210
244
  }
211
- async findOneBy(field, value) {
212
- const item = this.db.prepare(`SELECT *
213
- FROM ${this.tableName}
214
- WHERE ${field} = ?`).get(value);
245
+ async findBy(field, value, limit = 0, filters = []) {
246
+ let where = `WHERE ${field} = ?`;
247
+ let params = [value];
248
+ if (filters.length > 0) {
249
+ const result = SqlQueryFilter.applyFilters(where, filters);
250
+ where = result.where;
251
+ params.push(...result.params);
252
+ }
253
+ const items = this.db
254
+ .prepare(`SELECT * FROM ${this.tableName} ${where} LIMIT ?`)
255
+ .all([...params, limit]);
256
+ for (const item of items) {
257
+ await this.decorate(item);
258
+ }
259
+ return items;
260
+ }
261
+ async findOneBy(field, value, filters = []) {
262
+ let where = `WHERE ${field} = ?`;
263
+ let params = [value];
264
+ if (filters.length > 0) {
265
+ const result = SqlQueryFilter.applyFilters(where, filters);
266
+ where = result.where;
267
+ params.push(...result.params);
268
+ }
269
+ const item = this.db
270
+ .prepare(`SELECT * FROM ${this.tableName} ${where} LIMIT 1`)
271
+ .get(params);
215
272
  await this.decorate(item);
216
273
  return item;
217
274
  }
218
275
  async findOne({ search = '', filters = [] }) {
219
276
  let where = "";
277
+ let params = [];
278
+ // SEARCH
220
279
  if (search && this.searchFields.length > 0) {
221
- where = ` WHERE ${this.searchFields.map(field => `${field} LIKE '%${search}%'`).join(" OR ")}`;
280
+ const searchConditions = this.searchFields
281
+ .map(field => `${field} LIKE ?`)
282
+ .join(" OR ");
283
+ where = ` WHERE (${searchConditions})`;
284
+ params.push(...this.searchFields.map(() => `%${search}%`));
222
285
  }
286
+ // FILTERS
223
287
  if (filters.length > 0) {
224
- where = SqlQueryFilter.applyFilters(where, filters);
288
+ const result = SqlQueryFilter.applyFilters(where, filters);
289
+ where = result.where;
290
+ params.push(...result.params);
225
291
  }
226
- const item = this.db.prepare(`SELECT *
227
- FROM ${this.tableName} ${where} LIMIT 1`).get();
292
+ const item = this.db
293
+ .prepare(`SELECT * FROM ${this.tableName} ${where} LIMIT 1`)
294
+ .get(params);
228
295
  if (item) {
229
296
  await this.decorate(item);
230
297
  }
@@ -234,12 +301,13 @@ class AbstractSqliteRepository {
234
301
  if (fields.length === 0) {
235
302
  throw new Error("At least one field is required for groupBy");
236
303
  }
237
- // Construir la cláusula WHERE con los filtros
238
304
  let where = "";
305
+ let params = [];
239
306
  if (filters.length > 0) {
240
- where = SqlQueryFilter.applyFilters(where, filters);
307
+ const result = SqlQueryFilter.applyFilters(where, filters);
308
+ where = result.where;
309
+ params.push(...result.params);
241
310
  }
242
- // Función para obtener el formato de fecha según SQLite
243
311
  const getDateFormatSQL = (field, format) => {
244
312
  const formats = {
245
313
  'year': `strftime('%Y', ${field})`,
@@ -251,39 +319,53 @@ class AbstractSqliteRepository {
251
319
  };
252
320
  return formats[format] || formats['day'];
253
321
  };
254
- // Determinar si cada campo es de fecha
255
322
  const isDateField = (field) => {
256
323
  const tableField = this.tableFields.find(tf => tf.name === field);
257
- return tableField ? tableField.type === 'TEXT' && (field.includes('Date') || field.includes('date')) : false;
324
+ return tableField
325
+ ? tableField.type === 'TEXT' && (field.includes('Date') || field.includes('date'))
326
+ : false;
258
327
  };
259
- // Construir los campos SELECT con formato de fecha si aplica
260
- const selectFields = fields.map(field => {
261
- if (isDateField(field)) {
262
- return `${getDateFormatSQL(field, dateFormat)} as ${field}`;
328
+ const isNumericField = (field) => {
329
+ const tableField = this.tableFields.find(tf => tf.name === field);
330
+ if (!tableField)
331
+ return false;
332
+ const type = tableField.type.toUpperCase();
333
+ return ['INTEGER', 'REAL', 'NUMERIC'].includes(type);
334
+ };
335
+ const selectParts = [];
336
+ const groupParts = [];
337
+ for (const field of fields) {
338
+ const tableField = this.tableFields.find(tf => tf.name === field);
339
+ if (!tableField) {
340
+ throw new Error(`Invalid field ${field}`);
341
+ }
342
+ if (isNumericField(field)) {
343
+ selectParts.push(`SUM(${field}) as ${field}`);
344
+ continue;
263
345
  }
264
- return field;
265
- }).join(', ');
266
- // Construir la cláusula GROUP BY
267
- const groupByFields = fields.map(field => {
268
346
  if (isDateField(field)) {
269
- return getDateFormatSQL(field, dateFormat);
347
+ const formatted = getDateFormatSQL(field, dateFormat);
348
+ selectParts.push(`${formatted} as ${field}`);
349
+ groupParts.push(formatted);
350
+ continue;
270
351
  }
271
- return field;
272
- }).join(', ');
273
- // Construir y ejecutar la query
352
+ selectParts.push(field);
353
+ groupParts.push(field);
354
+ }
355
+ const selectFields = selectParts.join(", ");
356
+ const groupByFields = groupParts.join(", ");
274
357
  const query = `
275
- SELECT ${selectFields}, COUNT(*) as count
276
- FROM ${this.tableName}
277
- ${where}
278
- GROUP BY ${groupByFields}
279
- ORDER BY count DESC
280
- `;
358
+ SELECT ${selectFields}, COUNT(*) as count
359
+ FROM ${this.tableName}
360
+ ${where}
361
+ ${groupByFields ? `GROUP BY ${groupByFields}` : ''}
362
+ ORDER BY count DESC
363
+ `;
281
364
  try {
282
- const result = this.db.prepare(query).all();
283
- // Decorar los items si tienen campos de población
284
- for (const item of result) {
285
- await this.decorate(item);
286
- }
365
+ const result = this.db.prepare(query).all(params);
366
+ // for (const item of result) {
367
+ // await this.decorate(item)
368
+ // }
287
369
  return result;
288
370
  }
289
371
  catch (e) {
@@ -110,9 +110,12 @@ class AbstractService {
110
110
  }
111
111
  async updatePartial(id, data) {
112
112
  data = await this.validateInputUpdatePartial(data);
113
+ if (this.transformUpdatePartial) {
114
+ data = await this.transformUpdatePartial(data);
115
+ }
113
116
  let item = await this._repository.updatePartial(id, data);
114
- if (this.onUpdated) {
115
- await this.onUpdated(item);
117
+ if (this.onUpdatedPartial) {
118
+ await this.onUpdatedPartial(item);
116
119
  }
117
120
  item = await this.validateOutput(item);
118
121
  return item;
@@ -155,9 +158,9 @@ class AbstractService {
155
158
  throw e;
156
159
  }
157
160
  }
158
- async findOneBy(field, value) {
161
+ async findOneBy(field, value, filters = []) {
159
162
  try {
160
- let item = await this._repository.findOneBy(field, value);
163
+ let item = await this._repository.findOneBy(field, value, filters);
161
164
  if (item && this.transformRead) {
162
165
  item = await this.transformRead(item);
163
166
  }
@@ -211,9 +214,9 @@ class AbstractService {
211
214
  throw e;
212
215
  }
213
216
  }
214
- async findBy(field, value, limit = 1000) {
217
+ async findBy(field, value, limit = 1000, filters = []) {
215
218
  try {
216
- let items = await this._repository.findBy(field, value, limit);
219
+ let items = await this._repository.findBy(field, value, limit, filters);
217
220
  if (this.transformRead) {
218
221
  items = await Promise.all(items.map(item => this.transformRead(item)));
219
222
  }
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "publishConfig": {
4
4
  "access": "public"
5
5
  },
6
- "version": "2.9.0",
6
+ "version": "3.0.0",
7
7
  "description": "Crud utils across modules",
8
8
  "main": "dist/index.js",
9
9
  "types": "types/index.d.ts",
@@ -22,10 +22,10 @@
22
22
  "author": "Cristian Incarnato & Drax Team",
23
23
  "license": "ISC",
24
24
  "dependencies": {
25
- "@drax/common-back": "^2.8.0",
26
- "@drax/common-share": "^2.0.0",
27
- "@drax/identity-share": "^2.0.0",
28
- "@drax/media-back": "^2.9.0",
25
+ "@drax/common-back": "^3.0.0",
26
+ "@drax/common-share": "^3.0.0",
27
+ "@drax/identity-share": "^3.0.0",
28
+ "@drax/media-back": "^3.0.0",
29
29
  "@graphql-tools/load-files": "^7.0.0",
30
30
  "@graphql-tools/merge": "^9.0.4",
31
31
  "mongoose": "^8.23.0",
@@ -44,7 +44,8 @@
44
44
  "nodemon": "^3.1.0",
45
45
  "ts-node": "^10.9.2",
46
46
  "tsc-alias": "^1.8.10",
47
- "typescript": "^5.9.3"
47
+ "typescript": "^5.9.3",
48
+ "vitest": "^3.2.4"
48
49
  },
49
- "gitHead": "2bc9b59a45c762bd32403dce14db2693be34dcc7"
50
+ "gitHead": "63ae718b24ea25ae80b1a9a5dfb84a3abbb95199"
50
51
  }