@drax/crud-back 0.36.0 → 0.37.2

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
  */
@@ -332,7 +332,7 @@ class AbstractFastifyController extends CommonController {
332
332
  const search = request.query.search;
333
333
  const filters = this.parseFilters(request.query.filters);
334
334
  this.applyUserAndTenantFilters(filters, request.rbac);
335
- //console.log("FILTERS",filters)
335
+ // console.log("paginate filters",filters)
336
336
  let paginateResult = await this.service.paginate({ page, limit, orderBy, order, search, filters });
337
337
  return paginateResult;
338
338
  }
@@ -382,6 +382,29 @@ class AbstractFastifyController extends CommonController {
382
382
  this.handleError(e, reply);
383
383
  }
384
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
+ }
385
408
  }
386
409
  export default AbstractFastifyController;
387
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, };
@@ -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.36.0",
6
+ "version": "0.37.2",
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.36.0",
26
- "@drax/common-share": "^0.36.0",
27
- "@drax/identity-share": "^0.36.0",
28
- "@drax/media-back": "^0.36.0",
25
+ "@drax/common-back": "^0.37.2",
26
+ "@drax/common-share": "^0.37.0",
27
+ "@drax/identity-share": "^0.37.0",
28
+ "@drax/media-back": "^0.37.2",
29
29
  "@graphql-tools/load-files": "^7.0.0",
30
30
  "@graphql-tools/merge": "^9.0.4",
31
31
  "mongoose": "^8.6.3",
@@ -33,7 +33,7 @@
33
33
  "mongoose-paginate-v2": "^1.8.3"
34
34
  },
35
35
  "peerDependencies": {
36
- "dayjs": "^1.11.13",
36
+ "dayjs": "^1.11.19",
37
37
  "mongoose-paginate-v2": "^1.8.3"
38
38
  },
39
39
  "devDependencies": {
@@ -45,5 +45,5 @@
45
45
  "tsc-alias": "^1.8.10",
46
46
  "typescript": "^5.6.2"
47
47
  },
48
- "gitHead": "096f17a9a7f6e6969b8367a978137e090916f16d"
48
+ "gitHead": "85833fa79a0d1df3899c13a3a47ed3e7e3fe0448"
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
 
@@ -453,7 +455,7 @@ class AbstractFastifyController<T, C, U> extends CommonController {
453
455
  const filters: IDraxFieldFilter[] = this.parseFilters(request.query.filters)
454
456
  this.applyUserAndTenantFilters(filters, request.rbac);
455
457
 
456
- //console.log("FILTERS",filters)
458
+ // console.log("paginate filters",filters)
457
459
 
458
460
  let paginateResult = await this.service.paginate({page, limit, orderBy, order, search, filters})
459
461
  return paginateResult
@@ -513,6 +515,35 @@ class AbstractFastifyController<T, C, U> extends CommonController {
513
515
  this.handleError(e, reply)
514
516
  }
515
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
+ }
516
547
  }
517
548
 
518
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,