rl-core-api 0.8.0 → 0.10.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.
@@ -20,6 +20,7 @@ export interface FilterFieldDefinition {
20
20
  relation?: FilterRelation;
21
21
  permission?: string;
22
22
  sortable?: boolean;
23
+ sortColumn?: string;
23
24
  filterable?: boolean;
24
25
  }
25
26
  export type FilterCatalog = readonly FilterFieldDefinition[];
@@ -3,6 +3,9 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.resolveCatalogOrder = void 0;
4
4
  const resolveCatalogOrder = (sortBy, sortDir, catalog, alias, fallback) => {
5
5
  const chosen = catalog.find((field) => field.field === sortBy && field.sortable && !field.relation);
6
+ if (chosen?.sortColumn) {
7
+ return { column: chosen.sortColumn, direction: sortDir ?? "DESC" };
8
+ }
6
9
  const column = chosen?.column ??
7
10
  catalog.find((field) => field.field === fallback)?.column ??
8
11
  fallback;
@@ -5,7 +5,7 @@ export declare class QueueAdminController {
5
5
  private readonly queues;
6
6
  constructor(queues: QueueAdminService);
7
7
  counts(): Promise<QueueCounts>;
8
- jobs(state: string, page?: string, limit?: string): Promise<Paginated<QueueJobSummary>>;
8
+ jobs(state: string, page?: string, limit?: string, sortBy?: string, sortDir?: string): Promise<Paginated<QueueJobSummary>>;
9
9
  retry(id: string): Promise<void>;
10
10
  remove(id: string): Promise<void>;
11
11
  }
@@ -28,8 +28,8 @@ let QueueAdminController = class QueueAdminController {
28
28
  counts() {
29
29
  return this.queues.counts();
30
30
  }
31
- jobs(state, page, limit) {
32
- return this.queues.list(toState(state), toPositive(page, 1), Math.min(toPositive(limit, DEFAULT_LIMIT), MAX_LIMIT));
31
+ jobs(state, page, limit, sortBy, sortDir) {
32
+ return this.queues.list(toState(state), toPositive(page, 1), Math.min(toPositive(limit, DEFAULT_LIMIT), MAX_LIMIT), sortBy, sortDir === "ASC" ? "ASC" : "DESC");
33
33
  }
34
34
  retry(id) {
35
35
  return this.queues.retry(id);
@@ -51,12 +51,16 @@ __decorate([
51
51
  (0, common_1.Get)("jobs"),
52
52
  (0, requirePermission_decorator_1.RequirePermission)("queues:read:any"),
53
53
  (0, swagger_1.ApiQuery)({ name: "state", enum: queueAdmin_service_1.LISTABLE_STATES }),
54
+ (0, swagger_1.ApiQuery)({ name: "sortBy", enum: queueAdmin_service_1.SORTABLE_JOB_FIELDS, required: false }),
55
+ (0, swagger_1.ApiQuery)({ name: "sortDir", enum: ["ASC", "DESC"], required: false }),
54
56
  (0, swagger_1.ApiOkResponse)({ type: queueAdmin_response_1.PaginatedQueueJobsResponse }),
55
57
  __param(0, (0, common_1.Query)("state")),
56
58
  __param(1, (0, common_1.Query)("page")),
57
59
  __param(2, (0, common_1.Query)("limit")),
60
+ __param(3, (0, common_1.Query)("sortBy")),
61
+ __param(4, (0, common_1.Query)("sortDir")),
58
62
  __metadata("design:type", Function),
59
- __metadata("design:paramtypes", [String, String, String]),
63
+ __metadata("design:paramtypes", [String, String, String, String, String]),
60
64
  __metadata("design:returntype", Promise)
61
65
  ], QueueAdminController.prototype, "jobs", null);
62
66
  __decorate([
@@ -1,13 +1,16 @@
1
1
  import { QueueCounts, QueueJobSummary } from "./job.interface";
2
2
  import { JobProducer } from "./job.producer";
3
- import { Paginated } from "../utils/pagination.util";
3
+ import { Paginated, SortDir } from "../utils/pagination.util";
4
4
  export declare const LISTABLE_STATES: readonly ["waiting", "active", "completed", "failed", "delayed"];
5
5
  export type ListableState = (typeof LISTABLE_STATES)[number];
6
+ export declare const SORTABLE_JOB_FIELDS: readonly ["name", "attemptsMade", "percent", "createdAt", "failedReason"];
7
+ export type SortableJobField = (typeof SORTABLE_JOB_FIELDS)[number];
8
+ export declare const SORT_SCAN_LIMIT = 1000;
6
9
  export declare class QueueAdminService {
7
10
  private readonly producer;
8
11
  constructor(producer: JobProducer);
9
12
  counts(): Promise<QueueCounts>;
10
- list(state: ListableState, page: number, limit: number): Promise<Paginated<QueueJobSummary>>;
13
+ list(state: ListableState, page: number, limit: number, sortBy?: string, sortDir?: SortDir): Promise<Paginated<QueueJobSummary>>;
11
14
  retry(jobId: string): Promise<void>;
12
15
  remove(jobId: string): Promise<void>;
13
16
  private findJob;
@@ -9,7 +9,7 @@ var __metadata = (this && this.__metadata) || function (k, v) {
9
9
  if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
10
10
  };
11
11
  Object.defineProperty(exports, "__esModule", { value: true });
12
- exports.QueueAdminService = exports.LISTABLE_STATES = void 0;
12
+ exports.QueueAdminService = exports.SORT_SCAN_LIMIT = exports.SORTABLE_JOB_FIELDS = exports.LISTABLE_STATES = void 0;
13
13
  const common_1 = require("@nestjs/common");
14
14
  const job_producer_1 = require("./job.producer");
15
15
  const pagination_util_1 = require("../utils/pagination.util");
@@ -20,6 +20,14 @@ exports.LISTABLE_STATES = [
20
20
  "failed",
21
21
  "delayed",
22
22
  ];
23
+ exports.SORTABLE_JOB_FIELDS = [
24
+ "name",
25
+ "attemptsMade",
26
+ "percent",
27
+ "createdAt",
28
+ "failedReason",
29
+ ];
30
+ exports.SORT_SCAN_LIMIT = 1000;
23
31
  let QueueAdminService = class QueueAdminService {
24
32
  constructor(producer) {
25
33
  this.producer = producer;
@@ -35,15 +43,24 @@ let QueueAdminService = class QueueAdminService {
35
43
  delayed: counts.delayed ?? 0,
36
44
  };
37
45
  }
38
- async list(state, page, limit) {
46
+ async list(state, page, limit, sortBy, sortDir) {
39
47
  const queue = this.producer.requireQueue();
48
+ const field = exports.SORTABLE_JOB_FIELDS.find((name) => name === sortBy);
40
49
  const start = (page - 1) * limit;
50
+ const range = field
51
+ ? [0, exports.SORT_SCAN_LIMIT - 1]
52
+ : [start, start + limit - 1];
41
53
  const [jobs, counts] = await Promise.all([
42
- queue.getJobs([state], start, start + limit - 1),
54
+ queue.getJobs([state], range[0], range[1]),
43
55
  queue.getJobCounts(state),
44
56
  ]);
45
57
  const items = jobs.filter(Boolean).map((job) => this.toSummary(job, state));
46
- return new pagination_util_1.Paginated(items, counts[state] ?? 0, page, limit);
58
+ const total = counts[state] ?? 0;
59
+ if (!field) {
60
+ return new pagination_util_1.Paginated(items, total, page, limit);
61
+ }
62
+ const ordered = [...items].sort(compareBy(field, sortDir ?? "DESC"));
63
+ return new pagination_util_1.Paginated(ordered.slice(start, start + limit), total, page, limit);
47
64
  }
48
65
  async retry(jobId) {
49
66
  const job = await this.findJob(jobId);
@@ -83,3 +100,21 @@ exports.QueueAdminService = QueueAdminService = __decorate([
83
100
  (0, common_1.Injectable)(),
84
101
  __metadata("design:paramtypes", [job_producer_1.JobProducer])
85
102
  ], QueueAdminService);
103
+ const compareBy = (field, direction) => (a, b) => {
104
+ const left = a[field];
105
+ const right = b[field];
106
+ if (left === right) {
107
+ return 0;
108
+ }
109
+ if (left === null) {
110
+ return 1;
111
+ }
112
+ if (right === null) {
113
+ return -1;
114
+ }
115
+ const factor = direction === "ASC" ? 1 : -1;
116
+ const diff = typeof left === "number" && typeof right === "number"
117
+ ? left - right
118
+ : String(left).localeCompare(String(right), "pt-BR");
119
+ return diff * factor;
120
+ };
@@ -20,7 +20,7 @@ let AuditDataChangeRepository = class AuditDataChangeRepository extends typeorm_
20
20
  super(auditDataChange_schema_1.AuditDataChange, dataSource.createEntityManager());
21
21
  }
22
22
  findPaginated(query) {
23
- return (0, paginateWithFilter_util_1.paginateWithFilter)(this.createQueryBuilder("auditDataChange"), {
23
+ return (0, paginateWithFilter_util_1.paginateWithFilter)(this.createQueryBuilder("auditDataChange").leftJoin("users", "trailUser", "trailUser.id = auditDataChange.user_id"), {
24
24
  query,
25
25
  catalog: audit_filters_1.AUDIT_DATA_CHANGES_FILTER_CATALOG,
26
26
  defaultSort: "createdAt",
@@ -20,7 +20,7 @@ let RequestLogRepository = class RequestLogRepository extends typeorm_1.Reposito
20
20
  super(requestLog_schema_1.RequestLog, dataSource.createEntityManager());
21
21
  }
22
22
  findPaginated(query) {
23
- return (0, paginateWithFilter_util_1.paginateWithFilter)(this.createQueryBuilder("requestLog"), {
23
+ return (0, paginateWithFilter_util_1.paginateWithFilter)(this.createQueryBuilder("requestLog").leftJoin("users", "logUser", "logUser.id = requestLog.user_id"), {
24
24
  query,
25
25
  catalog: audit_filters_1.REQUEST_LOGS_FILTER_CATALOG,
26
26
  defaultSort: "createdAt",
@@ -92,6 +92,7 @@ exports.REQUEST_LOGS_FILTER_CATALOG = [
92
92
  label: "Tipo do erro",
93
93
  type: filterFieldType_enum_1.FilterFieldType.Enum,
94
94
  options: optionsFromEnum(ERROR_TYPE_LABELS),
95
+ sortable: true,
95
96
  },
96
97
  {
97
98
  field: "errorCode",
@@ -111,6 +112,15 @@ exports.REQUEST_LOGS_FILTER_CATALOG = [
111
112
  label: "Usuário",
112
113
  type: filterFieldType_enum_1.FilterFieldType.Uuid,
113
114
  },
115
+ {
116
+ field: "user",
117
+ column: "userId",
118
+ label: "Usuário (nome)",
119
+ type: filterFieldType_enum_1.FilterFieldType.Text,
120
+ sortable: true,
121
+ filterable: false,
122
+ sortColumn: "logUser.name",
123
+ },
114
124
  {
115
125
  field: "requestId",
116
126
  column: "requestId",
@@ -154,11 +164,21 @@ exports.AUDIT_DATA_CHANGES_FILTER_CATALOG = [
154
164
  label: "Usuário",
155
165
  type: filterFieldType_enum_1.FilterFieldType.Uuid,
156
166
  },
167
+ {
168
+ field: "user",
169
+ column: "userId",
170
+ label: "Usuário (nome)",
171
+ type: filterFieldType_enum_1.FilterFieldType.Text,
172
+ sortable: true,
173
+ filterable: false,
174
+ sortColumn: "trailUser.name",
175
+ },
157
176
  {
158
177
  field: "ipAddress",
159
178
  column: "ipAddress",
160
179
  label: "IP",
161
180
  type: filterFieldType_enum_1.FilterFieldType.Text,
181
+ sortable: true,
162
182
  },
163
183
  {
164
184
  field: "createdAt",
@@ -228,7 +228,7 @@ let UsersService = class UsersService {
228
228
  Object.assign(user, {
229
229
  name: dto.name ?? user.name,
230
230
  lastName: dto.lastName ?? user.lastName,
231
- phone: dto.phone ?? user.phone,
231
+ phone: dto.phone !== undefined ? dto.phone : user.phone,
232
232
  });
233
233
  await this.userRepo.save(user);
234
234
  return this.findById(userId);
@@ -1,5 +1,5 @@
1
1
  export declare class UpdateProfileCommand {
2
2
  name?: string;
3
3
  lastName?: string;
4
- phone?: string;
4
+ phone?: string | null;
5
5
  }
@@ -11,28 +11,33 @@ var __metadata = (this && this.__metadata) || function (k, v) {
11
11
  Object.defineProperty(exports, "__esModule", { value: true });
12
12
  exports.UpdateProfileCommand = void 0;
13
13
  const swagger_1 = require("@nestjs/swagger");
14
+ const class_transformer_1 = require("class-transformer");
14
15
  const class_validator_1 = require("class-validator");
16
+ const emptyToNull = ({ value }) => typeof value === "string" && value.trim() === "" ? null : value;
15
17
  class UpdateProfileCommand {
16
18
  }
17
19
  exports.UpdateProfileCommand = UpdateProfileCommand;
18
20
  __decorate([
19
21
  (0, swagger_1.ApiProperty)({ required: false }),
20
22
  (0, class_validator_1.IsOptional)(),
21
- (0, class_validator_1.IsString)(),
22
- (0, class_validator_1.Length)(2, 100),
23
+ (0, class_validator_1.IsString)({ message: "Nome inválido" }),
24
+ (0, class_validator_1.Length)(2, 100, { message: "O nome precisa ter entre 2 e 100 caracteres" }),
23
25
  __metadata("design:type", String)
24
26
  ], UpdateProfileCommand.prototype, "name", void 0);
25
27
  __decorate([
26
28
  (0, swagger_1.ApiProperty)({ required: false }),
27
29
  (0, class_validator_1.IsOptional)(),
28
- (0, class_validator_1.IsString)(),
29
- (0, class_validator_1.Length)(2, 100),
30
+ (0, class_validator_1.IsString)({ message: "Sobrenome inválido" }),
31
+ (0, class_validator_1.Length)(2, 100, {
32
+ message: "O sobrenome precisa ter entre 2 e 100 caracteres",
33
+ }),
30
34
  __metadata("design:type", String)
31
35
  ], UpdateProfileCommand.prototype, "lastName", void 0);
32
36
  __decorate([
33
- (0, swagger_1.ApiProperty)({ required: false }),
37
+ (0, swagger_1.ApiProperty)({ required: false, nullable: true }),
34
38
  (0, class_validator_1.IsOptional)(),
35
- (0, class_validator_1.IsString)(),
36
- (0, class_validator_1.Length)(8, 20),
37
- __metadata("design:type", String)
39
+ (0, class_transformer_1.Transform)(emptyToNull),
40
+ (0, class_validator_1.IsString)({ message: "Telefone inválido" }),
41
+ (0, class_validator_1.Length)(8, 20, { message: "O telefone precisa ter entre 8 e 20 caracteres" }),
42
+ __metadata("design:type", Object)
38
43
  ], UpdateProfileCommand.prototype, "phone", void 0);
@@ -37,11 +37,21 @@ exports.USERS_FILTER_CATALOG = [
37
37
  type: filterFieldType_enum_1.FilterFieldType.Boolean,
38
38
  sortable: true,
39
39
  },
40
+ {
41
+ field: "roles",
42
+ column: "id",
43
+ label: "Papéis",
44
+ type: filterFieldType_enum_1.FilterFieldType.Text,
45
+ sortable: true,
46
+ filterable: false,
47
+ sortColumn: "(SELECT MIN(r.name) FROM user_roles ur INNER JOIN roles r ON r.id = ur.role_id WHERE ur.user_id = user.id)",
48
+ },
40
49
  {
41
50
  field: "twoFactorEnabled",
42
51
  column: "twoFactorEnabled",
43
52
  label: "2FA configurado",
44
53
  type: filterFieldType_enum_1.FilterFieldType.Boolean,
54
+ sortable: true,
45
55
  },
46
56
  {
47
57
  field: "createdAt",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rl-core-api",
3
- "version": "0.8.0",
3
+ "version": "0.10.0",
4
4
  "description": "Core NestJS: autenticação com 2FA, RBAC, auditoria, notificações e listagens com filtro dinâmico",
5
5
  "author": "Rodrigo Liberti",
6
6
  "license": "MIT",