rl-core-api 0.6.0 → 0.8.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 (75) hide show
  1. package/README.md +54 -0
  2. package/dist/core/auth/ownershipScope.util.d.ts +10 -0
  3. package/dist/core/auth/ownershipScope.util.js +14 -0
  4. package/dist/core/auth/permissionScope.util.d.ts +2 -0
  5. package/dist/core/auth/permissionScope.util.js +7 -1
  6. package/dist/core/config/env.schema.d.ts +3 -0
  7. package/dist/core/config/env.schema.js +3 -0
  8. package/dist/core/core.module.js +3 -1
  9. package/dist/core/database/migrations/1710000000000-InitialSchema.js +8 -1
  10. package/dist/core/database/migrations/index.js +0 -2
  11. package/dist/core/database/seeds/seedPermissions.d.ts +20 -1
  12. package/dist/core/database/seeds/seedPermissions.js +31 -38
  13. package/dist/core/events/appEvent.enum.d.ts +6 -0
  14. package/dist/core/events/appEvent.enum.js +1 -0
  15. package/dist/core/health/health.controller.d.ts +4 -1
  16. package/dist/core/health/health.controller.js +9 -3
  17. package/dist/core/query/applyOwnershipScope.util.d.ts +7 -0
  18. package/dist/core/query/applyOwnershipScope.util.js +16 -0
  19. package/dist/core/queue/bullConnection.module.d.ts +2 -0
  20. package/dist/core/queue/bullConnection.module.js +27 -0
  21. package/dist/core/queue/job.interface.d.ts +70 -0
  22. package/dist/core/queue/job.interface.js +2 -0
  23. package/dist/core/queue/job.producer.d.ts +15 -0
  24. package/dist/core/queue/job.producer.js +124 -0
  25. package/dist/core/queue/jobEvents.interface.d.ts +4 -0
  26. package/dist/core/queue/jobEvents.interface.js +4 -0
  27. package/dist/core/queue/jobEvents.service.d.ts +12 -0
  28. package/dist/core/queue/jobEvents.service.js +53 -0
  29. package/dist/core/queue/jobStatus.controller.d.ts +8 -0
  30. package/dist/core/queue/jobStatus.controller.js +44 -0
  31. package/dist/core/queue/jobStatus.response.d.ts +20 -0
  32. package/dist/core/queue/jobStatus.response.js +84 -0
  33. package/dist/core/queue/jobWorkerHost.d.ts +17 -0
  34. package/dist/core/queue/jobWorkerHost.js +72 -0
  35. package/dist/core/queue/queue.constants.d.ts +11 -0
  36. package/dist/core/queue/queue.constants.js +18 -0
  37. package/dist/core/queue/queue.module.d.ts +2 -0
  38. package/dist/core/queue/queue.module.js +38 -0
  39. package/dist/core/queue/queueAdmin.controller.d.ts +11 -0
  40. package/dist/core/queue/queueAdmin.controller.js +93 -0
  41. package/dist/core/queue/queueAdmin.response.d.ts +23 -0
  42. package/dist/core/queue/queueAdmin.response.js +87 -0
  43. package/dist/core/queue/queueAdmin.service.d.ts +15 -0
  44. package/dist/core/queue/queueAdmin.service.js +85 -0
  45. package/dist/core/queue/redisConnection.d.ts +2 -0
  46. package/dist/core/queue/redisConnection.js +11 -0
  47. package/dist/core/websocket/baseGateway.d.ts +1 -0
  48. package/dist/core/websocket/baseGateway.js +3 -0
  49. package/dist/features/audit/audit.module.js +1 -0
  50. package/dist/features/audit/presentation/index.d.ts +1 -0
  51. package/dist/features/audit/presentation/index.js +1 -0
  52. package/dist/features/audit/presentation/userPermissionsChanged.listener.d.ts +7 -0
  53. package/dist/features/audit/presentation/userPermissionsChanged.listener.js +43 -0
  54. package/dist/features/notifications/notifications.module.js +1 -1
  55. package/dist/features/notifications/presentation/domainEvents.listener.d.ts +2 -1
  56. package/dist/features/notifications/presentation/domainEvents.listener.js +19 -0
  57. package/dist/features/rbac/domain/rbac.catalog.js +19 -16
  58. package/dist/features/rbac/domain/rbac.service.d.ts +11 -1
  59. package/dist/features/rbac/domain/rbac.service.js +76 -3
  60. package/dist/features/rbac/domain/teamScope.service.d.ts +1 -0
  61. package/dist/features/rbac/domain/teamScope.service.js +10 -4
  62. package/dist/features/rbac/infra/schema/scope.schema.js +1 -1
  63. package/dist/features/rbac/presentation/commands/createScope.command.js +1 -1
  64. package/dist/features/rbac/presentation/commands/setUserPermissions.command.d.ts +3 -0
  65. package/dist/features/rbac/presentation/commands/setUserPermissions.command.js +24 -0
  66. package/dist/features/rbac/presentation/index.d.ts +1 -0
  67. package/dist/features/rbac/presentation/index.js +1 -0
  68. package/dist/features/rbac/presentation/rbac.controller.d.ts +5 -0
  69. package/dist/features/rbac/presentation/rbac.controller.js +57 -0
  70. package/dist/index.d.ts +17 -3
  71. package/dist/index.js +48 -2
  72. package/dist/rlCore.module.js +8 -0
  73. package/package.json +4 -1
  74. package/dist/core/database/migrations/1787270400000-GroupManager.d.ts +0 -6
  75. package/dist/core/database/migrations/1787270400000-GroupManager.js +0 -26
package/README.md CHANGED
@@ -61,6 +61,7 @@ migrations do core vêm no pacote — você nunca copia nem reescreve nenhuma.
61
61
  | **Auditoria** | trilha de alterações de dados, log de requisições e log de erros, mantidos indefinidamente (sem retenção automática) |
62
62
  | **Notificações** | WebSocket com handshake por cookie ou Bearer, escopo resolvido na leitura |
63
63
  | **Listagens** | paginação, ordenação e filtro dinâmico com catálogo por listagem (`$AND`/`$OR` aninhados, 17 operadores) |
64
+ | **Fila** | processamento em segundo plano com progresso em tempo real (BullMQ + Redis), opcional por ambiente |
64
65
  | **Infra** | config validada com Zod, mailer, agendador, rate-limit, CSRF, filtro global de exceções, logger Winston |
65
66
 
66
67
  ## Configuração
@@ -69,6 +70,59 @@ O pacote **não traz `.env`** — ele declara o que precisa e quebra no boot se
69
70
  faltar. Cada projeto tem os valores dele (banco próprio, segredo JWT próprio).
70
71
  Comece pelo `.env.example` do repositório.
71
72
 
73
+ ### Fila de processamento (opcional)
74
+
75
+ Sem `REDIS_HOST` a aplicação sobe **sem fila e sem erro** — é o padrão.
76
+ Declarado, o Redis passa a ser obrigatório: a subida falha se ele não responder,
77
+ em vez de a falha aparecer no primeiro job.
78
+
79
+ ```env
80
+ REDIS_HOST=localhost
81
+ REDIS_PORT=6379
82
+ REDIS_PASSWORD=redis
83
+ REDIS_PREFIX=controlx # um Redis para vários projetos: o prefixo os separa
84
+ ```
85
+
86
+ Concorrência, tentativas, backoff e retenção não são variáveis de ambiente —
87
+ são constantes em `queue.constants.ts`. São decisão de arquitetura da fila, e um
88
+ número desses ajustado no `.env` de produção sem ninguém revisar é como se
89
+ descobre, tarde, por que os jobs empilharam.
90
+
91
+ O processador é sempre do projeto — o core não conhece o que a feature faz:
92
+
93
+ ```ts
94
+ @Processor(JOBS_QUEUE, { concurrency: JOB_CONCURRENCY, autorun: false })
95
+ export class FunkosImportProcessor extends JobWorkerHost<Payload, Summary> {
96
+ constructor(
97
+ jobEvents: JobEventsService,
98
+ private readonly imports: FunkosImportService,
99
+ ) {
100
+ super(jobEvents);
101
+ }
102
+
103
+ async handle(job: JobContext<Payload>): Promise<Summary> {
104
+ return this.imports.run(job.payload, (done, total) => job.report(done, total));
105
+ }
106
+ }
107
+ ```
108
+
109
+ Registre a classe como provider do módulo da feature, **atrás do
110
+ `isRedisConfigured()`** — sem Redis não há conexão para o worker abrir:
111
+
112
+ ```ts
113
+ @Module({ providers: isRedisConfigured() ? [FunkosImportProcessor] : [] })
114
+ export class FunkosModule {}
115
+ ```
116
+
117
+ Na rota, `jobs.enqueue(name, payload, { ownerId })` devolve o `jobId` na hora. O
118
+ progresso vai para a sala do dono pela **mesma** conexão de socket das
119
+ notificações (`job:progress`, `job:completed`, `job:failed`), e `GET /jobs/:id`
120
+ responde a quem perdeu o socket.
121
+
122
+ Quem opera vê tudo pela tela de **Filas** (`QueuesScreen`, no `rl-core-front`):
123
+ contagem por estado, o motivo cru da falha, reprocessar e remover — com
124
+ `queues:read:any` e `queues:manage:any`.
125
+
72
126
  ### Identidade do sistema
73
127
 
74
128
  O core não tem nome próprio. `APP_NAME` é o do projeto, e dele saem o título do
@@ -0,0 +1,10 @@
1
+ import { Actor } from "./actor.interface";
2
+ export type OwnershipScope = {
3
+ kind: "all";
4
+ } | {
5
+ kind: "own";
6
+ userId: string;
7
+ } | {
8
+ kind: "none";
9
+ };
10
+ export declare const resolveOwnershipScope: (actor: Actor, action: string) => OwnershipScope;
@@ -0,0 +1,14 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.resolveOwnershipScope = void 0;
4
+ const permissionScope_util_1 = require("./permissionScope.util");
5
+ const resolveOwnershipScope = (actor, action) => {
6
+ if (actor.permissions.includes(`${action}:${permissionScope_util_1.SCOPE_ANY}`)) {
7
+ return { kind: "all" };
8
+ }
9
+ if (actor.permissions.includes(`${action}:${permissionScope_util_1.SCOPE_OWN}`)) {
10
+ return { kind: "own", userId: actor.id };
11
+ }
12
+ return { kind: "none" };
13
+ };
14
+ exports.resolveOwnershipScope = resolveOwnershipScope;
@@ -1,3 +1,5 @@
1
1
  export declare const SCOPE_ANY = "any";
2
2
  export declare const SCOPE_TEAM = "team";
3
+ export declare const SCOPE_OWN = "own";
3
4
  export declare const anyOrTeam: (action: string) => [string, string];
5
+ export declare const anyOrOwn: (action: string) => [string, string];
@@ -1,10 +1,16 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.anyOrTeam = exports.SCOPE_TEAM = exports.SCOPE_ANY = void 0;
3
+ exports.anyOrOwn = exports.anyOrTeam = exports.SCOPE_OWN = exports.SCOPE_TEAM = exports.SCOPE_ANY = void 0;
4
4
  exports.SCOPE_ANY = "any";
5
5
  exports.SCOPE_TEAM = "team";
6
+ exports.SCOPE_OWN = "own";
6
7
  const anyOrTeam = (action) => [
7
8
  `${action}:${exports.SCOPE_ANY}`,
8
9
  `${action}:${exports.SCOPE_TEAM}`,
9
10
  ];
10
11
  exports.anyOrTeam = anyOrTeam;
12
+ const anyOrOwn = (action) => [
13
+ `${action}:${exports.SCOPE_ANY}`,
14
+ `${action}:${exports.SCOPE_OWN}`,
15
+ ];
16
+ exports.anyOrOwn = anyOrOwn;
@@ -57,6 +57,9 @@ export declare const envSchema: z.ZodObject<{
57
57
  PASSWORD_RESET_TOKEN_EXPIRY: z.ZodDefault<z.ZodCoercedNumber<unknown>>;
58
58
  FIRST_ACCESS_TOKEN_EXPIRY: z.ZodDefault<z.ZodCoercedNumber<unknown>>;
59
59
  PASSWORD_MAX_AGE_DAYS: z.ZodDefault<z.ZodCoercedNumber<unknown>>;
60
+ REDIS_HOST: z.ZodOptional<z.ZodString>;
61
+ REDIS_PORT: z.ZodDefault<z.ZodCoercedNumber<unknown>>;
62
+ REDIS_PASSWORD: z.ZodOptional<z.ZodString>;
60
63
  TOKEN_CLEANUP_CRON: z.ZodDefault<z.ZodString>;
61
64
  AUDIT_SLOW_REQUEST_MS: z.ZodDefault<z.ZodCoercedNumber<unknown>>;
62
65
  AUDIT_IP_SALT: z.ZodOptional<z.ZodString>;
@@ -68,6 +68,9 @@ exports.envSchema = zod_1.z
68
68
  .positive()
69
69
  .default(1440),
70
70
  PASSWORD_MAX_AGE_DAYS: zod_1.z.coerce.number().int().positive().default(90),
71
+ REDIS_HOST: zod_1.z.string().min(1).optional(),
72
+ REDIS_PORT: zod_1.z.coerce.number().int().positive().default(6379),
73
+ REDIS_PASSWORD: zod_1.z.string().optional(),
71
74
  TOKEN_CLEANUP_CRON: zod_1.z.string().default("0 */6 * * *"),
72
75
  AUDIT_SLOW_REQUEST_MS: zod_1.z.coerce.number().int().positive().default(1000),
73
76
  AUDIT_IP_SALT: zod_1.z.string().optional(),
@@ -21,6 +21,7 @@ const config_module_1 = require("./config/config.module");
21
21
  const env_service_1 = require("./config/env.service");
22
22
  const health_controller_1 = require("./health/health.controller");
23
23
  const mailer_module_1 = require("./mailer/mailer.module");
24
+ const queue_module_1 = require("./queue/queue.module");
24
25
  let CoreModule = CoreModule_1 = class CoreModule {
25
26
  static forRoot(options) {
26
27
  return {
@@ -28,6 +29,7 @@ let CoreModule = CoreModule_1 = class CoreModule {
28
29
  imports: [
29
30
  config_module_1.ConfigModule.forRoot({ validateEnv: options.validateEnv }),
30
31
  mailer_module_1.MailerModule,
32
+ queue_module_1.QueueModule,
31
33
  schedule_1.ScheduleModule.forRoot(),
32
34
  event_emitter_1.EventEmitterModule.forRoot(),
33
35
  typeorm_1.TypeOrmModule.forRootAsync({
@@ -63,7 +65,7 @@ let CoreModule = CoreModule_1 = class CoreModule {
63
65
  { provide: core_1.APP_GUARD, useClass: csrf_guard_1.CsrfGuard },
64
66
  { provide: core_1.APP_FILTER, useClass: allExceptions_filter_1.AllExceptionsFilter },
65
67
  ],
66
- exports: [mailer_module_1.MailerModule],
68
+ exports: [mailer_module_1.MailerModule, queue_module_1.QueueModule],
67
69
  };
68
70
  }
69
71
  };
@@ -133,11 +133,18 @@ class InitialSchema1710000000000 {
133
133
  description varchar(255) NULL,
134
134
  type varchar(50) NOT NULL DEFAULT 'team',
135
135
  parent_id char(36) NULL,
136
+ manager_id char(36) NULL,
136
137
  is_active tinyint NOT NULL DEFAULT 1,
137
138
  ${ts},
138
139
  PRIMARY KEY (id),
139
140
  KEY IDX_groups_parent (parent_id),
140
- CONSTRAINT FK_groups_parent FOREIGN KEY (parent_id) REFERENCES \`groups\` (id)
141
+ KEY IDX_groups_manager (manager_id),
142
+ CONSTRAINT FK_groups_parent FOREIGN KEY (parent_id) REFERENCES \`groups\` (id),
143
+ -- Quem manda no grupo, base do escopo \`team\`. \`ON DELETE SET NULL\` de
144
+ -- propósito: apagar a pessoa não pode apagar o grupo junto — o time
145
+ -- continua existindo e fica sem gerente até alguém assumir.
146
+ CONSTRAINT FK_groups_manager FOREIGN KEY (manager_id)
147
+ REFERENCES users (id) ON DELETE SET NULL
141
148
  ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
142
149
  `);
143
150
  await queryRunner.query(`
@@ -2,8 +2,6 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.coreMigrations = void 0;
4
4
  const _1710000000000_InitialSchema_1 = require("./1710000000000-InitialSchema");
5
- const _1787270400000_GroupManager_1 = require("./1787270400000-GroupManager");
6
5
  exports.coreMigrations = [
7
6
  _1710000000000_InitialSchema_1.InitialSchema1710000000000,
8
- _1787270400000_GroupManager_1.GroupManager1787270400000,
9
7
  ];
@@ -1,2 +1,21 @@
1
1
  import { DataSource } from "typeorm";
2
- export declare function seedPermissions(ds: DataSource): Promise<void>;
2
+ interface CatalogEntry {
3
+ name: string;
4
+ description: string;
5
+ }
6
+ export interface RbacCatalogExtension {
7
+ resources?: CatalogEntry[];
8
+ actions?: CatalogEntry[];
9
+ scopes?: CatalogEntry[];
10
+ permissions?: {
11
+ code: string;
12
+ description: string;
13
+ }[];
14
+ roles?: {
15
+ name: string;
16
+ description: string;
17
+ permissions: string[] | "*";
18
+ }[];
19
+ }
20
+ export declare function seedPermissions(ds: DataSource, extension?: RbacCatalogExtension): Promise<void>;
21
+ export {};
@@ -7,47 +7,40 @@ const permission_schema_1 = require("../../../features/rbac/infra/schema/permiss
7
7
  const resource_schema_1 = require("../../../features/rbac/infra/schema/resource.schema");
8
8
  const role_schema_1 = require("../../../features/rbac/infra/schema/role.schema");
9
9
  const scope_schema_1 = require("../../../features/rbac/infra/schema/scope.schema");
10
- async function seedPermissions(ds) {
11
- const resourceRepo = ds.getRepository(resource_schema_1.Resource);
12
- const actionRepo = ds.getRepository(action_schema_1.Action);
13
- const scopeRepo = ds.getRepository(scope_schema_1.Scope);
14
- const permissionRepo = ds.getRepository(permission_schema_1.Permission);
15
- const roleRepo = ds.getRepository(role_schema_1.Role);
16
- const resources = new Map();
17
- for (const r of rbac_catalog_1.RESOURCES) {
18
- let entity = await resourceRepo.findOne({
19
- where: { name: r.name },
20
- withDeleted: true,
21
- });
22
- if (!entity) {
23
- entity = await resourceRepo.save(resourceRepo.create(r));
24
- }
25
- resources.set(r.name, entity);
26
- }
27
- const actions = new Map();
28
- for (const a of rbac_catalog_1.ACTIONS) {
29
- let entity = await actionRepo.findOne({
30
- where: { name: a.name },
10
+ async function seedCatalog(repo, entries) {
11
+ const byName = new Map();
12
+ for (const entry of entries) {
13
+ let row = await repo.findOne({
14
+ where: { name: entry.name },
31
15
  withDeleted: true,
32
16
  });
33
- if (!entity) {
34
- entity = await actionRepo.save(actionRepo.create(a));
17
+ if (!row) {
18
+ row = await repo.save(repo.create(entry));
35
19
  }
36
- actions.set(a.name, entity);
37
- }
38
- const scopes = new Map();
39
- for (const s of rbac_catalog_1.SCOPES) {
40
- let entity = await scopeRepo.findOne({
41
- where: { name: s.name },
42
- withDeleted: true,
43
- });
44
- if (!entity) {
45
- entity = await scopeRepo.save(scopeRepo.create(s));
20
+ else if (row.description !== entry.description) {
21
+ row.description = entry.description;
22
+ row = await repo.save(row);
46
23
  }
47
- scopes.set(s.name, entity);
24
+ byName.set(entry.name, row);
48
25
  }
26
+ return byName;
27
+ }
28
+ async function seedPermissions(ds, extension = {}) {
29
+ const allResources = [...rbac_catalog_1.RESOURCES, ...(extension.resources ?? [])];
30
+ const allActions = [...rbac_catalog_1.ACTIONS, ...(extension.actions ?? [])];
31
+ const allScopes = [...rbac_catalog_1.SCOPES, ...(extension.scopes ?? [])];
32
+ const allPermissions = [...rbac_catalog_1.PERMISSIONS, ...(extension.permissions ?? [])];
33
+ const allRoles = [...rbac_catalog_1.ROLES, ...(extension.roles ?? [])];
34
+ const resourceRepo = ds.getRepository(resource_schema_1.Resource);
35
+ const actionRepo = ds.getRepository(action_schema_1.Action);
36
+ const scopeRepo = ds.getRepository(scope_schema_1.Scope);
37
+ const permissionRepo = ds.getRepository(permission_schema_1.Permission);
38
+ const roleRepo = ds.getRepository(role_schema_1.Role);
39
+ const resources = await seedCatalog(resourceRepo, allResources);
40
+ const actions = await seedCatalog(actionRepo, allActions);
41
+ const scopes = await seedCatalog(scopeRepo, allScopes);
49
42
  const permissions = new Map();
50
- for (const p of rbac_catalog_1.PERMISSIONS) {
43
+ for (const p of allPermissions) {
51
44
  let entity = await permissionRepo.findOne({
52
45
  where: { code: p.code },
53
46
  withDeleted: true,
@@ -70,7 +63,7 @@ async function seedPermissions(ds) {
70
63
  }
71
64
  permissions.set(p.code, entity);
72
65
  }
73
- for (const r of rbac_catalog_1.ROLES) {
66
+ for (const r of allRoles) {
74
67
  let role = await roleRepo.findOne({
75
68
  where: { name: r.name },
76
69
  relations: { permissions: true },
@@ -83,11 +76,11 @@ async function seedPermissions(ds) {
83
76
  permissions: [],
84
77
  });
85
78
  }
86
- const codes = r.permissions === "*" ? rbac_catalog_1.PERMISSIONS.map((p) => p.code) : r.permissions;
79
+ const codes = r.permissions === "*" ? allPermissions.map((p) => p.code) : r.permissions;
87
80
  role.permissions = codes
88
81
  .map((code) => permissions.get(code))
89
82
  .filter(Boolean);
90
83
  await roleRepo.save(role);
91
84
  }
92
- console.log(`[seed] RBAC: ${resources.size} resources, ${actions.size} actions, ${scopes.size} scopes, ${permissions.size} permissions, ${rbac_catalog_1.ROLES.length} roles`);
85
+ console.log(`[seed] RBAC: ${resources.size} resources, ${actions.size} actions, ${scopes.size} scopes, ${permissions.size} permissions, ${allRoles.length} roles`);
93
86
  }
@@ -2,6 +2,7 @@ export declare enum AppEvent {
2
2
  USER_LOCKED = "user.locked",
3
3
  USER_TWO_FACTOR_RESET = "user.two-factor-reset",
4
4
  USER_ROLES_CHANGED = "user.roles-changed",
5
+ USER_PERMISSIONS_CHANGED = "user.permissions-changed",
5
6
  USER_PASSWORD_RESET_REQUESTED = "user.password-reset-requested",
6
7
  USER_PASSWORD_EXPIRING = "user.password-expiring",
7
8
  SYSTEM_CLEANUP_FAILED = "system.cleanup-failed",
@@ -20,6 +21,11 @@ export interface UserRolesChangedEvent {
20
21
  roleNames: string[];
21
22
  previousRoleNames: string[];
22
23
  }
24
+ export interface UserPermissionsChangedEvent {
25
+ userId: string;
26
+ codes: string[];
27
+ previousCodes: string[];
28
+ }
23
29
  export interface UserPasswordResetRequestedEvent {
24
30
  userId: string;
25
31
  }
@@ -6,6 +6,7 @@ var AppEvent;
6
6
  AppEvent["USER_LOCKED"] = "user.locked";
7
7
  AppEvent["USER_TWO_FACTOR_RESET"] = "user.two-factor-reset";
8
8
  AppEvent["USER_ROLES_CHANGED"] = "user.roles-changed";
9
+ AppEvent["USER_PERMISSIONS_CHANGED"] = "user.permissions-changed";
9
10
  AppEvent["USER_PASSWORD_RESET_REQUESTED"] = "user.password-reset-requested";
10
11
  AppEvent["USER_PASSWORD_EXPIRING"] = "user.password-expiring";
11
12
  AppEvent["SYSTEM_CLEANUP_FAILED"] = "system.cleanup-failed";
@@ -1,11 +1,14 @@
1
1
  import { DataSource } from "typeorm";
2
+ import { JobProducer } from "../queue/job.producer";
2
3
  export declare class HealthController {
3
4
  private readonly ds;
5
+ private readonly jobs;
4
6
  private readonly logger;
5
- constructor(ds: DataSource);
7
+ constructor(ds: DataSource, jobs: JobProducer);
6
8
  check(): Promise<{
7
9
  status: string;
8
10
  database: string;
11
+ queue: string;
9
12
  timestamp: string;
10
13
  }>;
11
14
  }
@@ -19,9 +19,11 @@ const swagger_1 = require("@nestjs/swagger");
19
19
  const typeorm_1 = require("@nestjs/typeorm");
20
20
  const typeorm_2 = require("typeorm");
21
21
  const public_decorator_1 = require("../auth/decorators/public.decorator");
22
+ const job_producer_1 = require("../queue/job.producer");
22
23
  let HealthController = HealthController_1 = class HealthController {
23
- constructor(ds) {
24
+ constructor(ds, jobs) {
24
25
  this.ds = ds;
26
+ this.jobs = jobs;
25
27
  this.logger = new common_1.Logger(HealthController_1.name);
26
28
  }
27
29
  async check() {
@@ -33,9 +35,12 @@ let HealthController = HealthController_1 = class HealthController {
33
35
  this.logger.error(`Healthcheck do banco falhou: ${err.message}`);
34
36
  database = "down";
35
37
  }
38
+ const queue = await this.jobs.redisStatus();
39
+ const healthy = database === "up" && queue !== "down";
36
40
  return {
37
- status: database === "up" ? "ok" : "degraded",
41
+ status: healthy ? "ok" : "degraded",
38
42
  database,
43
+ queue,
39
44
  timestamp: new Date().toISOString(),
40
45
  };
41
46
  }
@@ -52,5 +57,6 @@ exports.HealthController = HealthController = HealthController_1 = __decorate([
52
57
  (0, swagger_1.ApiTags)("Health"),
53
58
  (0, common_1.Controller)({ path: "health", version: "1" }),
54
59
  __param(0, (0, typeorm_1.InjectDataSource)()),
55
- __metadata("design:paramtypes", [typeorm_2.DataSource])
60
+ __metadata("design:paramtypes", [typeorm_2.DataSource,
61
+ job_producer_1.JobProducer])
56
62
  ], HealthController);
@@ -0,0 +1,7 @@
1
+ import { ObjectLiteral, SelectQueryBuilder } from "typeorm";
2
+ import { OwnershipScope } from "../auth/ownershipScope.util";
3
+ export interface OwnershipScopeOptions {
4
+ alias: string;
5
+ column?: string;
6
+ }
7
+ export declare const applyOwnershipScope: <T extends ObjectLiteral>(qb: SelectQueryBuilder<T>, scope: OwnershipScope, options: OwnershipScopeOptions) => SelectQueryBuilder<T>;
@@ -0,0 +1,16 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.applyOwnershipScope = void 0;
4
+ const applyOwnershipScope = (qb, scope, options) => {
5
+ const { alias, column = "ownerId" } = options;
6
+ if (scope.kind === "all") {
7
+ return qb;
8
+ }
9
+ if (scope.kind === "none") {
10
+ return qb.andWhere("1 = 0");
11
+ }
12
+ return qb.andWhere(`${alias}.${column} = :ownershipScopeUserId`, {
13
+ ownershipScopeUserId: scope.userId,
14
+ });
15
+ };
16
+ exports.applyOwnershipScope = applyOwnershipScope;
@@ -0,0 +1,2 @@
1
+ export declare class BullConnectionModule {
2
+ }
@@ -0,0 +1,27 @@
1
+ "use strict";
2
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
3
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
4
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
5
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
6
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
7
+ };
8
+ Object.defineProperty(exports, "__esModule", { value: true });
9
+ exports.BullConnectionModule = void 0;
10
+ const bullmq_1 = require("@nestjs/bullmq");
11
+ const common_1 = require("@nestjs/common");
12
+ const queue_constants_1 = require("./queue.constants");
13
+ const redisConnection_1 = require("./redisConnection");
14
+ let BullConnectionModule = class BullConnectionModule {
15
+ };
16
+ exports.BullConnectionModule = BullConnectionModule;
17
+ exports.BullConnectionModule = BullConnectionModule = __decorate([
18
+ (0, common_1.Module)({
19
+ imports: [
20
+ bullmq_1.BullModule.forRoot({
21
+ connection: (0, redisConnection_1.buildRedisOptions)(),
22
+ prefix: (0, queue_constants_1.queuePrefix)(),
23
+ }),
24
+ ],
25
+ exports: [bullmq_1.BullModule],
26
+ })
27
+ ], BullConnectionModule);
@@ -0,0 +1,70 @@
1
+ export type JobState = "waiting" | "active" | "completed" | "failed" | "delayed" | "unknown";
2
+ export interface JobRef {
3
+ jobId: string;
4
+ name: string;
5
+ }
6
+ export interface JobProgress {
7
+ jobId: string;
8
+ name: string;
9
+ processed: number;
10
+ total: number;
11
+ percent: number;
12
+ message?: string;
13
+ }
14
+ export interface JobCompleted<TSummary = unknown> {
15
+ jobId: string;
16
+ name: string;
17
+ summary: TSummary;
18
+ }
19
+ export interface JobFailed {
20
+ jobId: string;
21
+ name: string;
22
+ errorCode: string | null;
23
+ message: string;
24
+ }
25
+ export interface JobStatus<TSummary = unknown> {
26
+ id: string;
27
+ name: string;
28
+ state: JobState;
29
+ progress: Omit<JobProgress, "jobId" | "name"> | null;
30
+ summary: TSummary | null;
31
+ error: {
32
+ code: string | null;
33
+ message: string;
34
+ } | null;
35
+ createdAt: string;
36
+ finishedAt: string | null;
37
+ }
38
+ export interface EnqueueOptions {
39
+ ownerId: string;
40
+ jobId?: string;
41
+ attempts?: number;
42
+ }
43
+ export interface QueueJobSummary {
44
+ id: string;
45
+ name: string;
46
+ state: JobState;
47
+ ownerId: string | null;
48
+ attemptsMade: number;
49
+ percent: number | null;
50
+ failedReason: string | null;
51
+ createdAt: string;
52
+ finishedAt: string | null;
53
+ }
54
+ export interface QueueCounts {
55
+ waiting: number;
56
+ active: number;
57
+ completed: number;
58
+ failed: number;
59
+ delayed: number;
60
+ }
61
+ export interface JobContext<TPayload = unknown> {
62
+ jobId: string;
63
+ ownerId: string;
64
+ payload: TPayload;
65
+ report: (processed: number, total: number, message?: string) => Promise<void>;
66
+ }
67
+ export interface JobEnvelope<TPayload = unknown> {
68
+ ownerId: string;
69
+ payload: TPayload;
70
+ }
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,15 @@
1
+ import { OnApplicationBootstrap } from "@nestjs/common";
2
+ import { Queue } from "bullmq";
3
+ import { EnqueueOptions, JobEnvelope, JobRef, JobStatus } from "./job.interface";
4
+ export declare class JobProducer implements OnApplicationBootstrap {
5
+ private readonly queue?;
6
+ private readonly logger;
7
+ constructor(queue?: Queue<JobEnvelope> | undefined);
8
+ onApplicationBootstrap(): Promise<void>;
9
+ isAvailable(): boolean;
10
+ enqueue<TPayload>(name: string, payload: TPayload, options: EnqueueOptions): Promise<JobRef>;
11
+ status<TSummary>(jobId: string, ownerId: string): Promise<JobStatus<TSummary>>;
12
+ redisStatus(): Promise<"up" | "down" | "off">;
13
+ requireQueue(): Queue<JobEnvelope>;
14
+ private assertRedisReachable;
15
+ }