@hed-hog/api-pagination 0.0.3 → 0.0.5

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 (66) hide show
  1. package/.eslintrc.js +9 -9
  2. package/.prettierrc.js +4 -4
  3. package/dist/databases/abstract.database.d.ts +7 -3
  4. package/dist/databases/abstract.database.d.ts.map +1 -1
  5. package/dist/databases/abstract.database.js +42 -1
  6. package/dist/databases/abstract.database.js.map +1 -1
  7. package/dist/pagination.service.d.ts +20 -1
  8. package/dist/pagination.service.d.ts.map +1 -1
  9. package/dist/pagination.service.js +171 -19
  10. package/dist/pagination.service.js.map +1 -1
  11. package/package.json +15 -6
  12. package/src/databases/abstract.database.ts +59 -1
  13. package/src/pagination.service.ts +284 -24
  14. package/tsconfig.json +1 -1
  15. package/tsconfig.production.tsbuildinfo +1 -1
  16. package/src/constants/pagination.constants.d.ts.map +0 -1
  17. package/src/constants/pagination.constants.js +0 -6
  18. package/src/constants/pagination.constants.js.map +0 -1
  19. package/src/databases/abstract.database.d.ts.map +0 -1
  20. package/src/databases/abstract.database.js +0 -655
  21. package/src/databases/abstract.database.js.map +0 -1
  22. package/src/databases/database.d.ts.map +0 -1
  23. package/src/databases/database.factory.d.ts.map +0 -1
  24. package/src/databases/database.factory.js +0 -20
  25. package/src/databases/database.factory.js.map +0 -1
  26. package/src/databases/database.js +0 -9
  27. package/src/databases/database.js.map +0 -1
  28. package/src/databases/index.d.ts.map +0 -1
  29. package/src/databases/index.js +0 -20
  30. package/src/databases/index.js.map +0 -1
  31. package/src/databases/mysql.database.d.ts.map +0 -1
  32. package/src/databases/mysql.database.js +0 -22
  33. package/src/databases/mysql.database.js.map +0 -1
  34. package/src/databases/postgres.database.d.ts.map +0 -1
  35. package/src/databases/postgres.database.js +0 -22
  36. package/src/databases/postgres.database.js.map +0 -1
  37. package/src/decorator/pagination.decorator.d.ts.map +0 -1
  38. package/src/decorator/pagination.decorator.js +0 -58
  39. package/src/decorator/pagination.decorator.js.map +0 -1
  40. package/src/dto/pagination.dto.d.ts.map +0 -1
  41. package/src/dto/pagination.dto.js +0 -58
  42. package/src/dto/pagination.dto.js.map +0 -1
  43. package/src/enums/patination.enums.d.ts.map +0 -1
  44. package/src/enums/patination.enums.js +0 -17
  45. package/src/enums/patination.enums.js.map +0 -1
  46. package/src/index.d.ts.map +0 -1
  47. package/src/index.js +0 -23
  48. package/src/index.js.map +0 -1
  49. package/src/pagination.module.d.ts.map +0 -1
  50. package/src/pagination.module.js +0 -22
  51. package/src/pagination.module.js.map +0 -1
  52. package/src/pagination.service.d.ts.map +0 -1
  53. package/src/pagination.service.js +0 -252
  54. package/src/pagination.service.js.map +0 -1
  55. package/src/types/pagination.types.d.ts.map +0 -1
  56. package/src/types/pagination.types.js +0 -3
  57. package/src/types/pagination.types.js.map +0 -1
  58. package/src/types/query-option.d.ts.map +0 -1
  59. package/src/types/query-option.js +0 -3
  60. package/src/types/query-option.js.map +0 -1
  61. package/src/types/relation-n2n-result.d.ts.map +0 -1
  62. package/src/types/relation-n2n-result.js +0 -3
  63. package/src/types/relation-n2n-result.js.map +0 -1
  64. package/src/types/transaction-queries.d.ts.map +0 -1
  65. package/src/types/transaction-queries.js +0 -3
  66. package/src/types/transaction-queries.js.map +0 -1
@@ -1,11 +1,11 @@
1
1
  import { Connection } from 'mysql2/promise';
2
2
  import { Client } from 'pg';
3
3
  import { DataSource } from 'typeorm';
4
+ import { EventEmitter } from 'typeorm/platform/PlatformTools';
4
5
  import { QueryOption } from '../types/query-option';
5
6
  import { RelationN2NResult } from '../types/relation-n2n-result';
6
7
  import { TransactionQueries } from '../types/transaction-queries';
7
8
  import { Database } from './database';
8
- import { EventEmitter } from 'typeorm/platform/PlatformTools';
9
9
 
10
10
  export class AbstractDatabase {
11
11
  private client: Client | Connection | null = null;
@@ -827,4 +827,62 @@ export class AbstractDatabase {
827
827
 
828
828
  return result;
829
829
  }
830
+
831
+ /**
832
+ * Executa uma raw query e retorna os resultados
833
+ * Usado para queries parametrizadas com $1, $2, etc.
834
+ */
835
+ async queryRaw(query: string, values?: any[]): Promise<any[]> {
836
+ if (!this.client) {
837
+ await this.getClient();
838
+ }
839
+
840
+ try {
841
+ let result;
842
+
843
+ switch (this.type) {
844
+ case Database.POSTGRES:
845
+ const pgResult = await (this.client as Client).query(query, values);
846
+ result = pgResult.rows;
847
+ break;
848
+
849
+ case Database.MYSQL:
850
+ // Para MySQL, converter $1, $2 para ?
851
+ const mysqlQuery = query.replace(/\$\d+/g, '?');
852
+ const [rows] = await (this.client as Connection).query(mysqlQuery, values);
853
+ result = rows;
854
+ break;
855
+
856
+ default:
857
+ throw new Error(`Unsupported database type: ${this.type}`);
858
+ }
859
+
860
+ if (this.autoClose) {
861
+ await this.client?.end();
862
+ this.client = null;
863
+ }
864
+
865
+ return result;
866
+ } catch (error) {
867
+ console.error({
868
+ error,
869
+ query,
870
+ values,
871
+ });
872
+
873
+ if (this.autoClose && this.client) {
874
+ await this.client.end();
875
+ this.client = null;
876
+ }
877
+
878
+ throw error;
879
+ }
880
+ }
881
+
882
+ /**
883
+ * Escapa um identificador (nome de tabela ou coluna)
884
+ */
885
+ escapeIdentifier(identifier: string): string {
886
+ return this.getColumnNameWithScaping(identifier).replace(/['"]/g, '');
887
+ }
830
888
  }
@@ -10,14 +10,83 @@ import type { FindManyArgs, PaginationParams } from './types/pagination.types';
10
10
 
11
11
  @Injectable()
12
12
  export class PaginationService {
13
- private readonly logger = new Logger(PaginationService.name);
13
+
14
+ private readonly logger = new Logger(PaginationService.name);
14
15
  private db: any = null;
16
+
17
+ async paginatePrismaModel(model: any, options: {
18
+ page?: number;
19
+ pageSize?: number;
20
+ search?: string;
21
+ sortField?: string;
22
+ sortOrder?: 'asc' | 'desc';
23
+ validSortFields?: string[];
24
+ searchFields?: string[];
25
+ where?: any;
26
+ include?: any;
27
+ }) {
28
+
29
+ try {
30
+
31
+ const {
32
+ page = 1,
33
+ pageSize = 10,
34
+ search,
35
+ sortField = 'id',
36
+ sortOrder = 'desc',
37
+ validSortFields = ['id'],
38
+ searchFields = [],
39
+ where: customWhere,
40
+ include,
41
+ } = options;
42
+
43
+ const currentPage = Math.max(Number(page) || 1, 1);
44
+ const limit = Math.max(Number(pageSize) || 10, 1);
45
+ const skip = (currentPage - 1) * limit;
46
+
47
+ let where = customWhere || {};
48
+ if (search && searchFields.length > 0) {
49
+ where = {
50
+ ...where,
51
+ OR: searchFields.map((field) => ({
52
+ [field]: { contains: search, mode: 'insensitive' },
53
+ })),
54
+ };
55
+ }
56
+
57
+ const orderBy =
58
+ sortField && typeof sortField === 'string' && validSortFields.includes(sortField)
59
+ ? { [sortField]: sortOrder === 'asc' ? 'asc' : 'desc' }
60
+ : { id: 'desc' };
61
+
62
+ const [data, total] = await Promise.all([
63
+ model.findMany({ skip, take: limit, where, orderBy, include }),
64
+ model.count({ where }),
65
+ ]);
66
+
67
+ const lastPage = Math.max(1, Math.ceil(total / limit));
68
+
69
+ return {
70
+ total,
71
+ lastPage,
72
+ page: currentPage,
73
+ pageSize: limit,
74
+ data,
75
+ };
76
+
77
+ } catch (error) {
78
+ this.logger.error('Pagination Error:', error);
79
+ throw new BadRequestException(`Failed to paginate: ${error}`);
80
+ }
81
+ }
82
+
15
83
  async paginate<T, M extends any>(
16
84
  model: M,
17
85
  paginationParams: PaginationParams,
18
86
  customQuery?: FindManyArgs<M>,
19
87
  translationKey?: string,
20
88
  ) /*: Promise<PaginatedResult<T>>*/ {
89
+
21
90
  try {
22
91
  if (!model) {
23
92
  throw new BadRequestException('Model is required');
@@ -42,13 +111,22 @@ export class PaginationService {
42
111
  let sortOrderCondition: any = {
43
112
  id: paginationParams.sortOrder || PageOrderDirection.Asc,
44
113
  };
114
+ let needsRawQueryOrdering = false;
115
+ let localeTableName = '';
116
+ let localeSortField = '';
117
+
118
+ this.logger.debug(`Sort field: ${sortField}`);
45
119
 
46
120
  if (sortField) {
47
121
  const invalid = this.isInvalidField(sortField, model);
48
122
  let localeInvalid = false;
123
+
124
+ this.logger.debug(`Field ${sortField} is invalid in main model: ${invalid}`);
125
+
49
126
  if (invalid) {
50
127
  localeInvalid = this.isInvalidLocaleField(sortField, model);
51
-
128
+ this.logger.debug(`Field ${sortField} is invalid in locale model: ${localeInvalid}`);
129
+
52
130
  if (localeInvalid) {
53
131
  this.logger.error(`Invalid field: ${sortField}`);
54
132
  throw new BadRequestException(
@@ -57,9 +135,11 @@ export class PaginationService {
57
135
  ).join(', ')}`,
58
136
  );
59
137
  } else {
60
- sortOrderCondition = {
61
- [`${(model as any).name}_locale`]: { [sortField]: sortOrder },
62
- };
138
+ // Campo existe na tabela locale - precisa usar raw query
139
+ localeTableName = `${(model as any).name}_locale`;
140
+ localeSortField = sortField;
141
+ needsRawQueryOrdering = true;
142
+ this.logger.debug(`Will use raw query ordering for ${localeTableName}.${localeSortField}`);
63
143
  }
64
144
  } else {
65
145
  sortOrderCondition = { [sortField]: sortOrder };
@@ -106,28 +186,46 @@ export class PaginationService {
106
186
  delete (customQuery as any).where.OR;
107
187
  }
108
188
 
109
- const query: any = {
110
- select: selectCondition,
111
- where: (customQuery as any)?.where || {},
112
- orderBy: sortOrderCondition,
113
- take: pageSize,
114
- skip,
115
- };
189
+ // Contar total sempre com Prisma normal
190
+ const total = await (model as any).count({
191
+ where: (customQuery as any)?.where || {}
192
+ });
193
+
194
+ let data: any[];
195
+
196
+ if (needsRawQueryOrdering) {
197
+ // Usar raw query para ordenação por campo de tabela relacionada
198
+ data = await this.paginateWithLocaleOrdering(
199
+ model,
200
+ localeTableName,
201
+ localeSortField,
202
+ sortOrder,
203
+ skip,
204
+ pageSize,
205
+ customQuery,
206
+ );
207
+ } else {
208
+ // Usar Prisma normal
209
+ const query: any = {
210
+ select: selectCondition,
211
+ where: (customQuery as any)?.where || {},
212
+ orderBy: sortOrderCondition,
213
+ take: pageSize,
214
+ skip,
215
+ };
216
+
217
+ if ((customQuery as any)?.include) {
218
+ query.include = (customQuery as any)?.include;
219
+ delete query.select;
220
+ }
116
221
 
117
- if ((customQuery as any)?.include) {
118
- query.include = (customQuery as any)?.include;
119
- delete query.select;
222
+ data = await (model as any).findMany(query);
120
223
  }
121
224
 
122
- let [total, data] = await Promise.all([
123
- (model as any).count({ where: (customQuery as any)?.where || {} }),
124
- (model as any).findMany(query),
125
- //this.query(model, query),
126
- ]);
127
-
128
225
  const lastPage = Math.ceil(total / pageSize);
129
226
 
130
- if (translationKey) {
227
+ if (translationKey !== undefined && translationKey !== null) {
228
+ this.logger.debug(`Applying translations with key: ${translationKey}`);
131
229
  data = data.map((item: any) => {
132
230
  return itemTranslations(translationKey, item);
133
231
  });
@@ -142,15 +240,177 @@ export class PaginationService {
142
240
  next: page < lastPage ? page + 1 : null,
143
241
  data,
144
242
  };
145
- } catch (error) {
243
+ } catch (error: any) {
146
244
  this.logger.error('Pagination Error:', error);
147
245
 
148
246
  if (error instanceof BadRequestException) {
149
247
  throw error;
150
248
  }
151
249
 
152
- throw new BadRequestException(`Failed to paginate: ${error}`);
250
+ throw new BadRequestException(`Failed to paginate: ${error.message || error}`);
251
+ }
252
+ }
253
+
254
+ /**
255
+ * Método auxiliar para paginação com ordenação por campos de tabela locale
256
+ * Usa raw query para ordenação, mas mantém estrutura aninhada do Prisma
257
+ */
258
+ private async paginateWithLocaleOrdering(
259
+ model: any,
260
+ localeTableName: string,
261
+ sortField: string,
262
+ sortOrder: string,
263
+ skip: number,
264
+ take: number,
265
+ customQuery?: any,
266
+ ): Promise<any[]> {
267
+ try {
268
+ const tableName = (model as any).name;
269
+ const db = await this.getDb(model);
270
+
271
+ // Detectar locale code do include (se disponível)
272
+ let localeCode = 'en'; // fallback
273
+ if (customQuery?.include?.[localeTableName]?.where?.locale?.code) {
274
+ localeCode = customQuery.include[localeTableName].where.locale.code;
275
+ }
276
+
277
+ this.logger.debug(`Building raw query for ${tableName} ordered by ${localeTableName}.${sortField}`);
278
+
279
+ // Construir WHERE clause da raw query
280
+ const whereConditions: string[] = [];
281
+ let whereParams: any[] = [];
282
+ let paramIndex = 1;
283
+
284
+ if (customQuery?.where) {
285
+ const { conditions, params, nextIndex } = this.buildWhereClause(
286
+ customQuery.where,
287
+ tableName,
288
+ db,
289
+ paramIndex,
290
+ );
291
+ whereConditions.push(...conditions);
292
+ whereParams.push(...params);
293
+ paramIndex = nextIndex;
294
+ }
295
+
296
+ // Adicionar filtro de locale
297
+ whereConditions.push(`l.code = $${paramIndex}`);
298
+ whereParams.push(localeCode);
299
+ paramIndex++;
300
+
301
+ const whereClause = whereConditions.length > 0
302
+ ? `WHERE ${whereConditions.join(' AND ')}`
303
+ : '';
304
+
305
+ // Query para buscar IDs ordenados
306
+ const orderDirection = sortOrder.toUpperCase() === 'DESC' ? 'DESC' : 'ASC';
307
+ const orderByClause = `ORDER BY tl.${db.escapeIdentifier(sortField)} ${orderDirection}`;
308
+
309
+ const rawQuery = `
310
+ SELECT DISTINCT t.id
311
+ FROM ${db.escapeIdentifier(tableName)} t
312
+ LEFT JOIN ${db.escapeIdentifier(localeTableName)} tl ON t.id = tl.${tableName}_id
313
+ LEFT JOIN locale l ON tl.locale_id = l.id
314
+ ${whereClause}
315
+ ${orderByClause}
316
+ LIMIT $${paramIndex} OFFSET $${paramIndex + 1}
317
+ `;
318
+
319
+ whereParams.push(take, skip);
320
+
321
+ this.logger.debug(`Raw query: ${rawQuery}`);
322
+ this.logger.debug(`Params: ${JSON.stringify(whereParams)}`);
323
+
324
+ // Executar raw query para obter IDs ordenados
325
+ const orderedIds = await db.queryRaw(rawQuery, whereParams);
326
+
327
+ if (!orderedIds || orderedIds.length === 0) {
328
+ return [];
329
+ }
330
+
331
+ const ids = orderedIds.map((row: any) => row.id);
332
+
333
+ this.logger.debug(`Ordered IDs: ${ids.join(', ')}`);
334
+
335
+ // Buscar dados completos com Prisma mantendo estrutura aninhada
336
+ const query: any = {
337
+ where: {
338
+ id: { in: ids },
339
+ ...(customQuery?.where || {}),
340
+ },
341
+ };
342
+
343
+ if (customQuery?.include) {
344
+ query.include = customQuery.include;
345
+ }
346
+
347
+ if (customQuery?.select) {
348
+ query.select = customQuery.select;
349
+ }
350
+
351
+ const data = await model.findMany(query);
352
+
353
+ // Reordenar dados para manter a ordem da raw query
354
+ const orderedData = ids
355
+ .map(id => data.find((item: any) => item.id === id))
356
+ .filter(Boolean);
357
+
358
+ this.logger.debug(`Returning ${orderedData.length} ordered records`);
359
+
360
+ return orderedData;
361
+
362
+ } catch (error: any) {
363
+ this.logger.error(`Error in paginateWithLocaleOrdering: ${error.message}`, error.stack);
364
+ throw new BadRequestException(
365
+ `Failed to paginate with locale ordering: ${error.message}`
366
+ );
367
+ }
368
+ }
369
+
370
+ /**
371
+ * Constrói cláusula WHERE para raw queries
372
+ */
373
+ private buildWhereClause(
374
+ where: any,
375
+ tableName: string,
376
+ db: any,
377
+ startIndex: number = 1,
378
+ ): { conditions: string[]; params: any[]; nextIndex: number } {
379
+ const conditions: string[] = [];
380
+ const params: any[] = [];
381
+ let paramIndex = startIndex;
382
+
383
+ for (const key in where) {
384
+ if (key === 'OR' || key === 'AND') {
385
+ continue; // Simplificação: ignorar OR/AND por enquanto
386
+ }
387
+
388
+ const value = where[key];
389
+
390
+ if (typeof value === 'object' && value !== null) {
391
+ // Operadores especiais do Prisma
392
+ if ('in' in value) {
393
+ const placeholders = value.in.map(() => `$${paramIndex++}`).join(', ');
394
+ conditions.push(`t.${db.escapeIdentifier(key)} IN (${placeholders})`);
395
+ params.push(...value.in);
396
+ } else if ('contains' in value) {
397
+ conditions.push(`t.${db.escapeIdentifier(key)} ILIKE $${paramIndex}`);
398
+ params.push(`%${value.contains}%`);
399
+ paramIndex++;
400
+ } else if ('equals' in value) {
401
+ conditions.push(`t.${db.escapeIdentifier(key)} = $${paramIndex}`);
402
+ params.push(value.equals);
403
+ paramIndex++;
404
+ }
405
+ } else {
406
+ // Valor simples
407
+ conditions.push(`t.${db.escapeIdentifier(key)} = $${paramIndex}`);
408
+ params.push(value);
409
+ paramIndex++;
410
+ }
153
411
  }
412
+
413
+ return { conditions, params, nextIndex: paramIndex };
154
414
  }
155
415
 
156
416
  extractFieldNames(model: Record<string, any>): string[] {
package/tsconfig.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "extends": "@hed-hog/typescript-config/nestjs-library.json",
2
+ "extends": "@hed-hog/typescript-config/library.json",
3
3
  "include": ["src"],
4
4
  "compilerOptions": {
5
5
  "outDir": "dist",