@drax/crud-back 0.34.0 → 0.37.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.
@@ -1,6 +1,6 @@
1
1
  import z from 'zod';
2
2
  import { zodToJsonSchema } from 'zod-to-json-schema';
3
- import { IdParamSchema, DeleteBodyResponseSchema, PaginateQuerySchema, PaginateBodyResponseSchema, FindQuerySchema, SearchQuerySchema, FindByParamSchema, ErrorBodyResponseSchema, ValidationErrorBodyResponseSchema, ExportBodyResponseSchema } from '../index.js';
3
+ import { IdParamSchema, DeleteBodyResponseSchema, PaginateQuerySchema, PaginateBodyResponseSchema, FindQuerySchema, SearchQuerySchema, FindByParamSchema, ErrorBodyResponseSchema, ValidationErrorBodyResponseSchema, ExportBodyResponseSchema, GroupByQuerySchema } from '../index.js';
4
4
  export class CrudSchemaBuilder {
5
5
  constructor(entitySchema, entityCreateSchema, entityUpdateSchema, entityName, target = 'openApi3', tags = []) {
6
6
  this.target = 'openApi3'; //"jsonSchema7" | "jsonSchema2019-09" | "openApi3" | "openAi"
@@ -29,6 +29,11 @@ export class CrudSchemaBuilder {
29
29
  get jsonEntityArraySchema() {
30
30
  return zodToJsonSchema(z.array(this.entitySchema), { target: this.target });
31
31
  }
32
+ get jsonEntityGroupBySchema() {
33
+ return zodToJsonSchema(z.array(z.object({
34
+ count: z.number()
35
+ }).catchall(z.any())), { target: this.target });
36
+ }
32
37
  get jsonExportBodyResponse() {
33
38
  return zodToJsonSchema(ExportBodyResponseSchema, { target: this.target });
34
39
  }
@@ -41,6 +46,9 @@ export class CrudSchemaBuilder {
41
46
  get jsonFindQuerySchema() {
42
47
  return zodToJsonSchema(FindQuerySchema, { target: this.target });
43
48
  }
49
+ get jsonGroupByQuerySchema() {
50
+ return zodToJsonSchema(GroupByQuerySchema, { target: this.target });
51
+ }
44
52
  get jsonSearchQuerySchema() {
45
53
  return zodToJsonSchema(SearchQuerySchema, { target: this.target });
46
54
  }
@@ -127,6 +135,22 @@ export class CrudSchemaBuilder {
127
135
  }
128
136
  };
129
137
  }
138
+ /**
139
+ * Get JSON schema for find entities
140
+ */
141
+ get groupBySchema() {
142
+ return {
143
+ ...(this.getTags),
144
+ query: this.jsonGroupByQuerySchema,
145
+ response: {
146
+ 200: this.jsonEntityGroupBySchema,
147
+ 400: this.jsonErrorBodyResponse,
148
+ 401: this.jsonErrorBodyResponse,
149
+ 403: this.jsonErrorBodyResponse,
150
+ 500: this.jsonErrorBodyResponse
151
+ }
152
+ };
153
+ }
130
154
  /**
131
155
  * Get JSON schema for find entities
132
156
  */
@@ -41,7 +41,9 @@ class AbstractFastifyController extends CommonController {
41
41
  const filters = [];
42
42
  filterArray.forEach((filter) => {
43
43
  const [field, operator, value] = filter.split(";");
44
- filters.push({ field, operator, value });
44
+ if (field && operator && (value !== undefined && value !== '')) {
45
+ filters.push({ field, operator, value });
46
+ }
45
47
  });
46
48
  return filters;
47
49
  }
@@ -330,7 +332,7 @@ class AbstractFastifyController extends CommonController {
330
332
  const search = request.query.search;
331
333
  const filters = this.parseFilters(request.query.filters);
332
334
  this.applyUserAndTenantFilters(filters, request.rbac);
333
- //console.log("FILTERS",filters)
335
+ // console.log("paginate filters",filters)
334
336
  let paginateResult = await this.service.paginate({ page, limit, orderBy, order, search, filters });
335
337
  return paginateResult;
336
338
  }
@@ -380,6 +382,29 @@ class AbstractFastifyController extends CommonController {
380
382
  this.handleError(e, reply);
381
383
  }
382
384
  }
385
+ async groupBy(request, reply) {
386
+ try {
387
+ request.rbac.assertPermission(this.permission.View);
388
+ const fields = request.query.fields ?
389
+ request.query.fields.split(',').map(f => f.trim()).filter(f => f.length > 0) :
390
+ [];
391
+ const dateFormat = request.query.dateFormat ? request.query.dateFormat : 'day';
392
+ if (fields.length === 0) {
393
+ throw new BadRequestError('At least one field is required for grouping');
394
+ }
395
+ const filters = this.parseFilters(request.query.filters);
396
+ this.applyUserAndTenantFilters(filters, request.rbac);
397
+ const result = await this.service.groupBy({ fields, filters, dateFormat });
398
+ // console.log("groupby fields",fields)
399
+ // console.log("groupby dateFormat",dateFormat)
400
+ // console.log("groupby filters",filters)
401
+ // console.log("groupby result",result)
402
+ return result;
403
+ }
404
+ catch (e) {
405
+ this.handleError(e, reply);
406
+ }
407
+ }
383
408
  }
384
409
  export default AbstractFastifyController;
385
410
  export { AbstractFastifyController };
package/dist/index.js CHANGED
@@ -10,6 +10,7 @@ import { PaginateBodyResponseSchema, PaginateQuerySchema } from "./schemas/Pagin
10
10
  import { FindQuerySchema } from "./schemas/FindSchema.js";
11
11
  import { SearchQuerySchema } from "./schemas/SearchSchema.js";
12
12
  import { FindByParamSchema } from "./schemas/FindBySchema.js";
13
+ import { GroupByQuerySchema } from "./schemas/GroupBySchema.js";
13
14
  import { ExportBodyResponseSchema } from "./schemas/ExportBodyResponseSchema.js";
14
15
  import { ErrorBodyResponseSchema, ValidationErrorBodyResponseSchema } from "./schemas/ErrorBodyResponseSchema.js";
15
16
  import { CrudSchemaBuilder } from "./builders/CrudSchemaBuilder.js";
@@ -17,6 +18,6 @@ export {
17
18
  //CRUD
18
19
  AbstractMongoRepository, AbstractSqliteRepository, AbstractService, AbstractFastifyController,
19
20
  //Schemas
20
- IdParamSchema, DeleteBodyResponseSchema, PaginateBodyResponseSchema, PaginateQuerySchema, FindQuerySchema, SearchQuerySchema, FindByParamSchema, ErrorBodyResponseSchema, ValidationErrorBodyResponseSchema, ExportBodyResponseSchema,
21
+ IdParamSchema, DeleteBodyResponseSchema, PaginateBodyResponseSchema, PaginateQuerySchema, FindQuerySchema, GroupByQuerySchema, SearchQuerySchema, FindByParamSchema, ErrorBodyResponseSchema, ValidationErrorBodyResponseSchema, ExportBodyResponseSchema,
21
22
  //Builder
22
23
  CrudSchemaBuilder, };
@@ -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);[a-zA-Z0-9_.\-:\., áéíóúÁÉÍÓÚ]*)(?:\|[a-zA-Z0-9_.\-]+;(?:eq|like|ne|in|nin|gt|gte|lt|lte);[a-zA-Z0-9_.\-:\., áéíóúÁÉÍÓÚ]*)*$/;
2
2
  export default QueryFilterRegex;
3
3
  export { QueryFilterRegex };
@@ -215,6 +215,151 @@ class AbstractMongoRepository {
215
215
  const sort = MongooseSort.applySort(orderBy, order);
216
216
  return this._model.find(query).limit(limit).sort(sort).cursor();
217
217
  }
218
+ async groupBy({ fields = [], filters = [], dateFormat = 'day' }) {
219
+ const query = {};
220
+ MongooseQueryFilter.applyFilters(query, filters);
221
+ // Obtener el schema para identificar campos de referencia y fechas
222
+ const schema = this._model.schema;
223
+ // Construir el objeto de agrupación dinámicamente
224
+ const groupId = {};
225
+ const lookupStages = [];
226
+ const finalProjectFields = { count: 1, _id: 0 };
227
+ const refFields = new Set();
228
+ const dateFields = new Set();
229
+ // Función para obtener el formato de fecha según el nivel de granularidad
230
+ const getDateFormat = (field, format) => {
231
+ const formats = {
232
+ 'year': {
233
+ $dateFromParts: {
234
+ year: { $year: `$${field}` },
235
+ month: 1,
236
+ day: 1
237
+ }
238
+ },
239
+ 'month': {
240
+ $dateFromParts: {
241
+ year: { $year: `$${field}` },
242
+ month: { $month: `$${field}` },
243
+ day: 1
244
+ }
245
+ },
246
+ 'day': {
247
+ $dateFromParts: {
248
+ year: { $year: `$${field}` },
249
+ month: { $month: `$${field}` },
250
+ day: { $dayOfMonth: `$${field}` }
251
+ }
252
+ },
253
+ 'hour': {
254
+ $dateFromParts: {
255
+ year: { $year: `$${field}` },
256
+ month: { $month: `$${field}` },
257
+ day: { $dayOfMonth: `$${field}` },
258
+ hour: { $hour: `$${field}` }
259
+ }
260
+ },
261
+ 'minute': {
262
+ $dateFromParts: {
263
+ year: { $year: `$${field}` },
264
+ month: { $month: `$${field}` },
265
+ day: { $dayOfMonth: `$${field}` },
266
+ hour: { $hour: `$${field}` },
267
+ minute: { $minute: `$${field}` }
268
+ }
269
+ },
270
+ 'second': {
271
+ $dateFromParts: {
272
+ year: { $year: `$${field}` },
273
+ month: { $month: `$${field}` },
274
+ day: { $dayOfMonth: `$${field}` },
275
+ hour: { $hour: `$${field}` },
276
+ minute: { $minute: `$${field}` },
277
+ second: { $second: `$${field}` }
278
+ }
279
+ }
280
+ };
281
+ return formats[format] || formats['day'];
282
+ };
283
+ fields.forEach(field => {
284
+ const schemaPath = schema.path(field);
285
+ // Verificar si el campo es de tipo Date
286
+ if (schemaPath && schemaPath.instance === 'Date') {
287
+ dateFields.add(field);
288
+ groupId[field] = getDateFormat(field, dateFormat);
289
+ }
290
+ // Verificar si el campo es una referencia
291
+ else if (schemaPath && schemaPath.options && schemaPath.options.ref) {
292
+ const refModel = schemaPath.options.ref;
293
+ const fieldName = field;
294
+ refFields.add(field);
295
+ // Obtener el modelo referenciado y su nombre de colección real
296
+ const refModelInstance = mongoose.model(refModel);
297
+ const collectionName = refModelInstance.collection.name;
298
+ // Determinar el campo local correcto según si es un solo campo o múltiples
299
+ const localField = fields.length === 1 ? '_id' : `_id.${fieldName}`;
300
+ lookupStages.push({
301
+ $lookup: {
302
+ from: collectionName,
303
+ localField: localField,
304
+ foreignField: '_id',
305
+ as: `${fieldName}_populated`
306
+ }
307
+ });
308
+ // Unwind para convertir el array en objeto único
309
+ lookupStages.push({
310
+ $unwind: {
311
+ path: `$${fieldName}_populated`,
312
+ preserveNullAndEmptyArrays: true
313
+ }
314
+ });
315
+ // En la proyección final, usar el objeto poblado
316
+ finalProjectFields[field] = `$${fieldName}_populated`;
317
+ groupId[field] = `$${field}`;
318
+ }
319
+ else {
320
+ // Si no es una referencia ni fecha, usar el valor directo
321
+ groupId[field] = `$${field}`;
322
+ }
323
+ });
324
+ // Construir la proyección final para campos de fecha
325
+ fields.forEach(field => {
326
+ if (dateFields.has(field)) {
327
+ if (fields.length === 1) {
328
+ finalProjectFields[field] = `$_id`;
329
+ }
330
+ else {
331
+ finalProjectFields[field] = `$_id.${field}`;
332
+ }
333
+ }
334
+ else if (!refFields.has(field)) {
335
+ if (fields.length === 1) {
336
+ finalProjectFields[field] = `$_id`;
337
+ }
338
+ else {
339
+ finalProjectFields[field] = `$_id.${field}`;
340
+ }
341
+ }
342
+ });
343
+ const pipeline = [
344
+ { $match: query },
345
+ {
346
+ $group: {
347
+ _id: fields.length === 1 ? (dateFields.has(fields[0]) ? getDateFormat(fields[0], dateFormat) : `$${fields[0]}`) : groupId,
348
+ count: { $sum: 1 }
349
+ }
350
+ }
351
+ ];
352
+ // Solo agregar lookups si hay campos de referencia
353
+ if (lookupStages.length > 0) {
354
+ pipeline.push(...lookupStages);
355
+ }
356
+ pipeline.push({
357
+ $project: finalProjectFields
358
+ }, { $sort: { count: -1 } });
359
+ console.log("pipeline", JSON.stringify(pipeline, null, 2));
360
+ const result = await this._model.aggregate(pipeline).exec();
361
+ return result;
362
+ }
218
363
  }
219
364
  export default AbstractMongoRepository;
220
365
  export { AbstractMongoRepository };
@@ -0,0 +1,7 @@
1
+ import z from "zod";
2
+ import QueryFilterRegex from "../regexs/QueryFilterRegex.js";
3
+ const GroupByQuerySchema = z.object({
4
+ fields: z.array(z.string()).min(1).max(10),
5
+ filters: z.string().regex(QueryFilterRegex).optional().describe("Format: field;operator;value|field;operator;value|..."),
6
+ });
7
+ export { GroupByQuerySchema };
@@ -200,6 +200,9 @@ class AbstractService {
200
200
  throw e;
201
201
  }
202
202
  }
203
+ async groupBy({ fields = [], filters = [], dateFormat = 'day' }) {
204
+ return await this._repository.groupBy({ fields, filters, dateFormat });
205
+ }
203
206
  async export({ format = 'JSON', headers = [], headersTranslate = [], separator = ';', fileName = 'export', orderBy = '', order = false, search = '', filters = [] }, destinationPath) {
204
207
  try {
205
208
  let cursor;
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "publishConfig": {
4
4
  "access": "public"
5
5
  },
6
- "version": "0.34.0",
6
+ "version": "0.37.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": "^0.34.0",
26
- "@drax/common-share": "^0.34.0",
27
- "@drax/identity-share": "^0.34.0",
28
- "@drax/media-back": "^0.34.0",
25
+ "@drax/common-back": "^0.37.0",
26
+ "@drax/common-share": "^0.37.0",
27
+ "@drax/identity-share": "^0.37.0",
28
+ "@drax/media-back": "^0.37.0",
29
29
  "@graphql-tools/load-files": "^7.0.0",
30
30
  "@graphql-tools/merge": "^9.0.4",
31
31
  "mongoose": "^8.6.3",
@@ -45,5 +45,5 @@
45
45
  "tsc-alias": "^1.8.10",
46
46
  "typescript": "^5.6.2"
47
47
  },
48
- "gitHead": "3a121099fcdd0814fd232d90aeac0b2086e2e625"
48
+ "gitHead": "0bca48d4b686fd9536a78d84f5befe6801238000"
49
49
  }
@@ -11,7 +11,8 @@ import {
11
11
  FindByParamSchema,
12
12
  ErrorBodyResponseSchema,
13
13
  ValidationErrorBodyResponseSchema,
14
- ExportBodyResponseSchema
14
+ ExportBodyResponseSchema,
15
+ GroupByQuerySchema
15
16
  } from '../index.js';
16
17
 
17
18
  export class CrudSchemaBuilder<T extends z.ZodObject<z.ZodRawShape>, TCreate extends z.ZodObject<z.ZodRawShape>, TUpdate extends z.ZodObject<z.ZodRawShape>> {
@@ -54,6 +55,17 @@ export class CrudSchemaBuilder<T extends z.ZodObject<z.ZodRawShape>, TCreate ext
54
55
  return zodToJsonSchema(z.array(this.entitySchema), {target: this.target})
55
56
  }
56
57
 
58
+ get jsonEntityGroupBySchema() {
59
+ return zodToJsonSchema(
60
+ z.array(
61
+ z.object({
62
+ count: z.number()
63
+ }).catchall(z.any())
64
+ ),
65
+ {target: this.target}
66
+ )
67
+ }
68
+
57
69
  get jsonExportBodyResponse() {
58
70
  return zodToJsonSchema(ExportBodyResponseSchema, {target: this.target})
59
71
  }
@@ -70,6 +82,10 @@ export class CrudSchemaBuilder<T extends z.ZodObject<z.ZodRawShape>, TCreate ext
70
82
  return zodToJsonSchema(FindQuerySchema, {target: this.target})
71
83
  }
72
84
 
85
+ get jsonGroupByQuerySchema(){
86
+ return zodToJsonSchema(GroupByQuerySchema, {target: this.target})
87
+ }
88
+
73
89
  get jsonSearchQuerySchema(){
74
90
  return zodToJsonSchema(SearchQuerySchema, {target: this.target})
75
91
  }
@@ -166,6 +182,23 @@ export class CrudSchemaBuilder<T extends z.ZodObject<z.ZodRawShape>, TCreate ext
166
182
  }
167
183
  }
168
184
 
185
+ /**
186
+ * Get JSON schema for find entities
187
+ */
188
+ get groupBySchema() {
189
+ return {
190
+ ...(this.getTags),
191
+ query: this.jsonGroupByQuerySchema,
192
+ response: {
193
+ 200: this.jsonEntityGroupBySchema,
194
+ 400: this.jsonErrorBodyResponse,
195
+ 401: this.jsonErrorBodyResponse,
196
+ 403: this.jsonErrorBodyResponse,
197
+ 500: this.jsonErrorBodyResponse
198
+ }
199
+ }
200
+ }
201
+
169
202
  /**
170
203
  * Get JSON schema for find entities
171
204
  */
@@ -39,6 +39,8 @@ type CustomRequest = FastifyRequest<{
39
39
  headersTranslate?: string
40
40
  separator?: string
41
41
  fileName?: string
42
+ fields?: string
43
+ dateFormat?: 'year' | 'month' | 'day' | 'hour' | 'minute' | 'second'
42
44
  }
43
45
  }>
44
46
 
@@ -97,7 +99,11 @@ class AbstractFastifyController<T, C, U> extends CommonController {
97
99
  const filters: IDraxFieldFilter[] = []
98
100
  filterArray.forEach((filter) => {
99
101
  const [field, operator, value] = filter.split(";")
100
- filters.push({field, operator, value})
102
+
103
+ if(field && operator && (value !== undefined && value !== '') ) {
104
+ filters.push({field, operator, value})
105
+ }
106
+
101
107
  })
102
108
  return filters
103
109
  } catch (e) {
@@ -449,7 +455,7 @@ class AbstractFastifyController<T, C, U> extends CommonController {
449
455
  const filters: IDraxFieldFilter[] = this.parseFilters(request.query.filters)
450
456
  this.applyUserAndTenantFilters(filters, request.rbac);
451
457
 
452
- //console.log("FILTERS",filters)
458
+ // console.log("paginate filters",filters)
453
459
 
454
460
  let paginateResult = await this.service.paginate({page, limit, orderBy, order, search, filters})
455
461
  return paginateResult
@@ -509,6 +515,35 @@ class AbstractFastifyController<T, C, U> extends CommonController {
509
515
  this.handleError(e, reply)
510
516
  }
511
517
  }
518
+
519
+ async groupBy(request: CustomRequest, reply: FastifyReply) {
520
+ try {
521
+ request.rbac.assertPermission(this.permission.View)
522
+
523
+ const fields: string[] = request.query.fields ?
524
+ request.query.fields.split(',').map(f => f.trim()).filter(f => f.length > 0) :
525
+ []
526
+
527
+ const dateFormat = request.query.dateFormat ? request.query.dateFormat : 'day'
528
+
529
+ if (fields.length === 0) {
530
+ throw new BadRequestError('At least one field is required for grouping')
531
+ }
532
+
533
+ const filters: IDraxFieldFilter[] = this.parseFilters(request.query.filters)
534
+ this.applyUserAndTenantFilters(filters, request.rbac)
535
+
536
+
537
+ const result = await this.service.groupBy({fields, filters, dateFormat})
538
+ // console.log("groupby fields",fields)
539
+ // console.log("groupby dateFormat",dateFormat)
540
+ // console.log("groupby filters",filters)
541
+ // console.log("groupby result",result)
542
+ return result
543
+ } catch (e) {
544
+ this.handleError(e, reply)
545
+ }
546
+ }
512
547
  }
513
548
 
514
549
  export default AbstractFastifyController;
package/src/index.ts CHANGED
@@ -12,6 +12,7 @@ import {PaginateBodyResponseSchema, PaginateQuerySchema} from "./schemas/Paginat
12
12
  import {FindQuerySchema} from "./schemas/FindSchema.js"
13
13
  import {SearchQuerySchema} from "./schemas/SearchSchema.js"
14
14
  import {FindByParamSchema} from "./schemas/FindBySchema.js"
15
+ import {GroupByQuerySchema} from "./schemas/GroupBySchema.js"
15
16
  import {ExportBodyResponseSchema} from "./schemas/ExportBodyResponseSchema.js"
16
17
  import {ErrorBodyResponseSchema, ValidationErrorBodyResponseSchema} from "./schemas/ErrorBodyResponseSchema.js"
17
18
  import {CrudSchemaBuilder} from "./builders/CrudSchemaBuilder.js";
@@ -35,6 +36,7 @@ export {
35
36
  PaginateBodyResponseSchema,
36
37
  PaginateQuerySchema,
37
38
  FindQuerySchema,
39
+ GroupByQuerySchema,
38
40
  SearchQuerySchema,
39
41
  FindByParamSchema,
40
42
  ErrorBodyResponseSchema,
@@ -1,4 +1,4 @@
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);[a-zA-Z0-9_.\-:\., áéíóúÁÉÍÓÚ]*)(?:\|[a-zA-Z0-9_.\-]+;(?:eq|like|ne|in|nin|gt|gte|lt|lte);[a-zA-Z0-9_.\-:\., áéíóúÁÉÍÓÚ]*)*$/
2
2
 
3
3
  export default QueryFilterRegex
4
4
  export {QueryFilterRegex}