@gzl10/nexus-backend 0.1.4
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.
- package/README.md +80 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +30 -0
- package/dist/cli.js.map +1 -0
- package/dist/index.d.ts +561 -0
- package/dist/index.js +2267 -0
- package/dist/index.js.map +1 -0
- package/package.json +97 -0
package/README.md
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# nexus-backend
|
|
2
|
+
|
|
3
|
+
> **Warning**: This project is currently in testing/experimental phase. Use at your own risk.
|
|
4
|
+
|
|
5
|
+
Backend as a Service (BaaS) with Express 5, Knex and CASL. Ready to use as an npm library.
|
|
6
|
+
|
|
7
|
+
**Repository**: [https://gitlab.gzl10.com/oss/nexus-backend](https://gitlab.gzl10.com/oss/nexus-backend)
|
|
8
|
+
|
|
9
|
+
## Installation
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
pnpm add nexus-backend
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Usage
|
|
16
|
+
|
|
17
|
+
```typescript
|
|
18
|
+
import { start, stop, nexusEvents } from 'nexus-backend'
|
|
19
|
+
|
|
20
|
+
// Start server
|
|
21
|
+
await start({
|
|
22
|
+
port: 3000,
|
|
23
|
+
jwt: { secret: 'your-secret-at-least-32-characters' },
|
|
24
|
+
database: { url: 'file:./data.db' }
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
// Listen to CRUD events
|
|
28
|
+
nexusEvents.on('db.users.created', ({ data }) => {
|
|
29
|
+
console.log('User created:', data)
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
// Stop server
|
|
33
|
+
await stop()
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Configuration
|
|
37
|
+
|
|
38
|
+
| Option | Description | Default |
|
|
39
|
+
| -------------------- | ------------------------ | ----------------------- |
|
|
40
|
+
| `port` | Server port | `3000` |
|
|
41
|
+
| `host` | Server host | `0.0.0.0` |
|
|
42
|
+
| `database.url` | Database URL | `file:./dev.db` |
|
|
43
|
+
| `jwt.secret` | JWT secret (min 32 chars)| Required |
|
|
44
|
+
| `jwt.accessExpires` | Access token expiration | `15m` |
|
|
45
|
+
| `jwt.refreshExpires` | Refresh token expiration | `7d` |
|
|
46
|
+
| `cors.origin` | Allowed CORS origin | `http://localhost:3001` |
|
|
47
|
+
|
|
48
|
+
### Supported Databases
|
|
49
|
+
|
|
50
|
+
- **SQLite**: `file:./data.db`
|
|
51
|
+
- **PostgreSQL**: `postgresql://user:pass@host:5432/db`
|
|
52
|
+
- **MySQL**: `mysql://user:pass@host:3306/db`
|
|
53
|
+
|
|
54
|
+
## API Endpoints
|
|
55
|
+
|
|
56
|
+
- `POST /api/v1/auth/login` - Login
|
|
57
|
+
- `POST /api/v1/auth/register` - Register
|
|
58
|
+
- `POST /api/v1/auth/refresh` - Refresh token
|
|
59
|
+
- `GET /api/v1/users` - List users
|
|
60
|
+
- `GET /api/v1/posts` - List posts
|
|
61
|
+
- `GET /api/health` - Health check
|
|
62
|
+
|
|
63
|
+
## Development
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
pnpm dev # Server with hot reload
|
|
67
|
+
pnpm build # Production build
|
|
68
|
+
pnpm typecheck # Type check
|
|
69
|
+
pnpm lint # Linting
|
|
70
|
+
pnpm db:migrate # Run migrations
|
|
71
|
+
pnpm db:seed # Initial seed
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## Support
|
|
75
|
+
|
|
76
|
+
<a href="https://www.buymeacoffee.com/gzl10g" target="_blank"><img src="https://cdn.buymeacoffee.com/buttons/v2/default-yellow.png" alt="Buy Me A Coffee" style="height: 60px !important;width: 217px !important;" ></a>
|
|
77
|
+
|
|
78
|
+
## License
|
|
79
|
+
|
|
80
|
+
MIT
|
package/dist/cli.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/cli.ts
|
|
4
|
+
import { existsSync } from "fs";
|
|
5
|
+
import { Command } from "commander";
|
|
6
|
+
import { consola } from "consola";
|
|
7
|
+
if (existsSync(".env")) {
|
|
8
|
+
process.loadEnvFile(".env");
|
|
9
|
+
}
|
|
10
|
+
var program = new Command();
|
|
11
|
+
program.name("nexus").description("Nexus Backend CLI").version("0.1.0");
|
|
12
|
+
program.command("ui").description("Open UI in browser").option("-p, --port <port>", "Nexus backend port").action(async (options) => {
|
|
13
|
+
const port = parseInt(options.port || process.env["PORT"] || "3000", 10);
|
|
14
|
+
const baseUrl = (process.env["BACKEND_URL"] || `http://localhost:${port}`).replace(/\/$/, "");
|
|
15
|
+
const url = `${baseUrl}/ui`;
|
|
16
|
+
try {
|
|
17
|
+
const res = await fetch(`${baseUrl}/health`);
|
|
18
|
+
if (!res.ok) throw new Error();
|
|
19
|
+
} catch {
|
|
20
|
+
consola.error(`Nexus no est\xE1 corriendo en ${baseUrl}`);
|
|
21
|
+
consola.info("Inicia el servidor primero con: pnpm dev");
|
|
22
|
+
process.exit(1);
|
|
23
|
+
}
|
|
24
|
+
const { exec } = await import("child_process");
|
|
25
|
+
const cmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
|
|
26
|
+
exec(`${cmd} ${url}`);
|
|
27
|
+
consola.success(`Abriendo: ${url}`);
|
|
28
|
+
});
|
|
29
|
+
program.parse();
|
|
30
|
+
//# sourceMappingURL=cli.js.map
|
package/dist/cli.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/cli.ts"],"sourcesContent":["#!/usr/bin/env node\nimport { existsSync } from 'fs'\nimport { Command } from 'commander'\nimport { consola } from 'consola'\n\n// Cargar .env si existe (Node 20+)\nif (existsSync('.env')) {\n process.loadEnvFile('.env')\n}\n\nconst program = new Command()\n\nprogram\n .name('nexus')\n .description('Nexus Backend CLI')\n .version('0.1.0')\n\nprogram\n .command('ui')\n .description('Open UI in browser')\n .option('-p, --port <port>', 'Nexus backend port')\n .action(async (options) => {\n const port = parseInt(options.port || process.env['PORT'] || '3000', 10)\n const baseUrl = (process.env['BACKEND_URL'] || `http://localhost:${port}`).replace(/\\/$/, '')\n const url = `${baseUrl}/ui`\n\n // Verificar que Nexus está corriendo\n try {\n const res = await fetch(`${baseUrl}/health`)\n if (!res.ok) throw new Error()\n } catch {\n consola.error(`Nexus no está corriendo en ${baseUrl}`)\n consola.info('Inicia el servidor primero con: pnpm dev')\n process.exit(1)\n }\n\n // Abrir navegador\n const { exec } = await import('child_process')\n const cmd = process.platform === 'darwin' ? 'open'\n : process.platform === 'win32' ? 'start'\n : 'xdg-open'\n\n exec(`${cmd} ${url}`)\n consola.success(`Abriendo: ${url}`)\n })\n\nprogram.parse()\n"],"mappings":";;;AACA,SAAS,kBAAkB;AAC3B,SAAS,eAAe;AACxB,SAAS,eAAe;AAGxB,IAAI,WAAW,MAAM,GAAG;AACtB,UAAQ,YAAY,MAAM;AAC5B;AAEA,IAAM,UAAU,IAAI,QAAQ;AAE5B,QACG,KAAK,OAAO,EACZ,YAAY,mBAAmB,EAC/B,QAAQ,OAAO;AAElB,QACG,QAAQ,IAAI,EACZ,YAAY,oBAAoB,EAChC,OAAO,qBAAqB,oBAAoB,EAChD,OAAO,OAAO,YAAY;AACzB,QAAM,OAAO,SAAS,QAAQ,QAAQ,QAAQ,IAAI,MAAM,KAAK,QAAQ,EAAE;AACvE,QAAM,WAAW,QAAQ,IAAI,aAAa,KAAK,oBAAoB,IAAI,IAAI,QAAQ,OAAO,EAAE;AAC5F,QAAM,MAAM,GAAG,OAAO;AAGtB,MAAI;AACF,UAAM,MAAM,MAAM,MAAM,GAAG,OAAO,SAAS;AAC3C,QAAI,CAAC,IAAI,GAAI,OAAM,IAAI,MAAM;AAAA,EAC/B,QAAQ;AACN,YAAQ,MAAM,iCAA8B,OAAO,EAAE;AACrD,YAAQ,KAAK,0CAA0C;AACvD,YAAQ,KAAK,CAAC;AAAA,EAChB;AAGA,QAAM,EAAE,KAAK,IAAI,MAAM,OAAO,eAAe;AAC7C,QAAM,MAAM,QAAQ,aAAa,WAAW,SAChC,QAAQ,aAAa,UAAU,UAC/B;AAEZ,OAAK,GAAG,GAAG,IAAI,GAAG,EAAE;AACpB,UAAQ,QAAQ,aAAa,GAAG,EAAE;AACpC,CAAC;AAEH,QAAQ,MAAM;","names":[]}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,561 @@
|
|
|
1
|
+
import http from 'node:http';
|
|
2
|
+
import * as express_serve_static_core from 'express-serve-static-core';
|
|
3
|
+
import knex, { Knex } from 'knex';
|
|
4
|
+
import * as _casl_ability from '@casl/ability';
|
|
5
|
+
import { MongoAbility, RawRuleOf } from '@casl/ability';
|
|
6
|
+
import { Router, RequestHandler, Request } from 'express';
|
|
7
|
+
export { CookieOptions, NextFunction, Request, RequestHandler, Response, Router } from 'express';
|
|
8
|
+
import { Logger } from 'pino';
|
|
9
|
+
import { ZodSchema } from 'zod';
|
|
10
|
+
import pkg from 'eventemitter2';
|
|
11
|
+
|
|
12
|
+
interface SmtpConfig {
|
|
13
|
+
host: string;
|
|
14
|
+
port: number;
|
|
15
|
+
secure: boolean;
|
|
16
|
+
auth?: {
|
|
17
|
+
user: string;
|
|
18
|
+
pass: string;
|
|
19
|
+
};
|
|
20
|
+
from: string;
|
|
21
|
+
}
|
|
22
|
+
interface NexusConfig {
|
|
23
|
+
port?: number;
|
|
24
|
+
host?: string;
|
|
25
|
+
homePath?: string;
|
|
26
|
+
cors?: {
|
|
27
|
+
origin?: string | string[];
|
|
28
|
+
};
|
|
29
|
+
database?: {
|
|
30
|
+
url?: string;
|
|
31
|
+
};
|
|
32
|
+
jwt?: {
|
|
33
|
+
secret?: string;
|
|
34
|
+
accessExpires?: string;
|
|
35
|
+
refreshExpires?: string;
|
|
36
|
+
};
|
|
37
|
+
admin?: {
|
|
38
|
+
email?: string;
|
|
39
|
+
password?: string;
|
|
40
|
+
};
|
|
41
|
+
smtp?: Partial<SmtpConfig>;
|
|
42
|
+
}
|
|
43
|
+
interface ResolvedConfig {
|
|
44
|
+
nodeEnv: 'development' | 'production' | 'test';
|
|
45
|
+
port: number;
|
|
46
|
+
host: string;
|
|
47
|
+
homePath: string;
|
|
48
|
+
corsOrigin: string;
|
|
49
|
+
databaseUrl: string;
|
|
50
|
+
jwtSecret: string;
|
|
51
|
+
jwtAccessExpires: string;
|
|
52
|
+
jwtRefreshExpires: string;
|
|
53
|
+
adminEmail?: string;
|
|
54
|
+
adminPassword?: string;
|
|
55
|
+
smtp: SmtpConfig;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
declare function start(config?: NexusConfig): Promise<http.Server>;
|
|
59
|
+
declare function stop(): Promise<void>;
|
|
60
|
+
declare function restart(config?: NexusConfig): Promise<http.Server>;
|
|
61
|
+
declare function isRunning(): boolean;
|
|
62
|
+
|
|
63
|
+
declare function createApp(): express_serve_static_core.Express;
|
|
64
|
+
|
|
65
|
+
declare const db: knex.Knex<any, any[]>;
|
|
66
|
+
declare function getDatabaseType(): 'sqlite' | 'postgresql' | 'mysql';
|
|
67
|
+
declare function destroyDb(): Promise<void>;
|
|
68
|
+
|
|
69
|
+
declare function getConfig(): ResolvedConfig;
|
|
70
|
+
|
|
71
|
+
interface MailAction {
|
|
72
|
+
label: string;
|
|
73
|
+
url: string;
|
|
74
|
+
}
|
|
75
|
+
interface SendMailOptions {
|
|
76
|
+
to: string | string[];
|
|
77
|
+
subject: string;
|
|
78
|
+
title?: string;
|
|
79
|
+
message?: string;
|
|
80
|
+
actions?: MailAction[];
|
|
81
|
+
html?: string;
|
|
82
|
+
text?: string;
|
|
83
|
+
from?: string;
|
|
84
|
+
replyTo?: string;
|
|
85
|
+
logoUrl?: string;
|
|
86
|
+
attachments?: Array<{
|
|
87
|
+
filename: string;
|
|
88
|
+
content: Buffer | string;
|
|
89
|
+
contentType?: string;
|
|
90
|
+
}>;
|
|
91
|
+
}
|
|
92
|
+
interface SendMailResult {
|
|
93
|
+
messageId: string;
|
|
94
|
+
accepted: string[];
|
|
95
|
+
rejected: string[];
|
|
96
|
+
}
|
|
97
|
+
declare class MailService {
|
|
98
|
+
private transporter;
|
|
99
|
+
private defaultFrom;
|
|
100
|
+
private defaultLogoUrl;
|
|
101
|
+
private logger;
|
|
102
|
+
private template;
|
|
103
|
+
constructor(config: SmtpConfig, logger: Logger);
|
|
104
|
+
send(options: SendMailOptions): Promise<SendMailResult | null>;
|
|
105
|
+
private renderTemplate;
|
|
106
|
+
private processConditionalBlock;
|
|
107
|
+
verify(): Promise<boolean>;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
declare class AppError extends Error {
|
|
111
|
+
readonly statusCode: number;
|
|
112
|
+
constructor(message: string, statusCode?: number);
|
|
113
|
+
}
|
|
114
|
+
declare class NotFoundError extends AppError {
|
|
115
|
+
constructor(resource?: string);
|
|
116
|
+
}
|
|
117
|
+
declare class UnauthorizedError extends AppError {
|
|
118
|
+
constructor(message?: string);
|
|
119
|
+
}
|
|
120
|
+
declare class ForbiddenError extends AppError {
|
|
121
|
+
constructor(message?: string);
|
|
122
|
+
}
|
|
123
|
+
declare class ConflictError extends AppError {
|
|
124
|
+
constructor(message?: string);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
interface Role {
|
|
128
|
+
id: string;
|
|
129
|
+
name: string;
|
|
130
|
+
description: string | null;
|
|
131
|
+
is_system: boolean;
|
|
132
|
+
created_at: Date;
|
|
133
|
+
updated_at: Date;
|
|
134
|
+
created_by: string | null;
|
|
135
|
+
updated_by: string | null;
|
|
136
|
+
}
|
|
137
|
+
interface RolePermission {
|
|
138
|
+
id: string;
|
|
139
|
+
role_id: string;
|
|
140
|
+
action: string;
|
|
141
|
+
subject: string;
|
|
142
|
+
conditions: Record<string, unknown> | null;
|
|
143
|
+
fields: string[] | null;
|
|
144
|
+
inverted: boolean;
|
|
145
|
+
created_at: Date;
|
|
146
|
+
updated_at: Date;
|
|
147
|
+
created_by: string | null;
|
|
148
|
+
updated_by: string | null;
|
|
149
|
+
}
|
|
150
|
+
/** Role con permisos incluidos */
|
|
151
|
+
interface RoleWithPermissions extends Role {
|
|
152
|
+
permissions: RolePermission[];
|
|
153
|
+
}
|
|
154
|
+
/** Role con conteo de usuarios */
|
|
155
|
+
interface RoleWithCounts extends Role {
|
|
156
|
+
permissions_count: number;
|
|
157
|
+
users_count: number;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
interface User {
|
|
161
|
+
id: string;
|
|
162
|
+
email: string;
|
|
163
|
+
password: string;
|
|
164
|
+
name: string;
|
|
165
|
+
role_id: string;
|
|
166
|
+
metadata: Record<string, unknown> | null;
|
|
167
|
+
created_at: Date;
|
|
168
|
+
updated_at: Date;
|
|
169
|
+
created_by: string | null;
|
|
170
|
+
updated_by: string | null;
|
|
171
|
+
}
|
|
172
|
+
/** User sin password para respuestas API */
|
|
173
|
+
type UserWithoutPassword = Omit<User, 'password'>;
|
|
174
|
+
/** User con rol incluido */
|
|
175
|
+
interface UserWithRole extends UserWithoutPassword {
|
|
176
|
+
role: Role;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Registro de subjects CASL.
|
|
181
|
+
* Los módulos core definen sus subjects aquí.
|
|
182
|
+
* Los plugins pueden extender via declaration merging:
|
|
183
|
+
*
|
|
184
|
+
* @example
|
|
185
|
+
* ```typescript
|
|
186
|
+
* // my-plugin/src/types/abilities.d.ts
|
|
187
|
+
* declare module '@g10/nexus-backend' {
|
|
188
|
+
* interface SubjectRegistry {
|
|
189
|
+
* MyCustomEntity: true
|
|
190
|
+
* }
|
|
191
|
+
* }
|
|
192
|
+
* ```
|
|
193
|
+
*/
|
|
194
|
+
interface SubjectRegistry {
|
|
195
|
+
UsrUser: true;
|
|
196
|
+
RolRole: true;
|
|
197
|
+
RolRolePermission: true;
|
|
198
|
+
all: true;
|
|
199
|
+
}
|
|
200
|
+
/** Subjects como strings (derivado del registro, extensible) */
|
|
201
|
+
type SubjectStrings = keyof SubjectRegistry;
|
|
202
|
+
type Actions = 'manage' | 'create' | 'read' | 'update' | 'delete';
|
|
203
|
+
/** Subjects válidos: strings del registro + instancias de objetos */
|
|
204
|
+
type Subjects = SubjectStrings | User | UserWithoutPassword | Role;
|
|
205
|
+
type AppAbility = MongoAbility<[Actions, Subjects]>;
|
|
206
|
+
|
|
207
|
+
declare const EventEmitter2: typeof pkg.EventEmitter2;
|
|
208
|
+
interface DbEventPayload {
|
|
209
|
+
table: string;
|
|
210
|
+
action: 'created' | 'updated' | 'deleted';
|
|
211
|
+
data: unknown;
|
|
212
|
+
timestamp: Date;
|
|
213
|
+
}
|
|
214
|
+
interface NexusEvents {
|
|
215
|
+
'server.starting': {
|
|
216
|
+
port: number;
|
|
217
|
+
host: string;
|
|
218
|
+
};
|
|
219
|
+
'server.started': {
|
|
220
|
+
port: number;
|
|
221
|
+
host: string;
|
|
222
|
+
};
|
|
223
|
+
'server.stopping': undefined;
|
|
224
|
+
'server.stopped': undefined;
|
|
225
|
+
'server.restarting': undefined;
|
|
226
|
+
'db.connected': {
|
|
227
|
+
type: 'sqlite' | 'postgresql' | 'mysql';
|
|
228
|
+
};
|
|
229
|
+
'db.disconnected': undefined;
|
|
230
|
+
[key: `db.${string}.created`]: DbEventPayload;
|
|
231
|
+
[key: `db.${string}.updated`]: DbEventPayload;
|
|
232
|
+
[key: `db.${string}.deleted`]: DbEventPayload;
|
|
233
|
+
'auth.login': {
|
|
234
|
+
userId: string;
|
|
235
|
+
email: string;
|
|
236
|
+
};
|
|
237
|
+
'auth.logout': {
|
|
238
|
+
userId: string;
|
|
239
|
+
};
|
|
240
|
+
'auth.refresh': {
|
|
241
|
+
userId: string;
|
|
242
|
+
};
|
|
243
|
+
'auth.failed': {
|
|
244
|
+
email: string;
|
|
245
|
+
reason: string;
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
type NexusEventName = keyof NexusEvents;
|
|
249
|
+
type NexusEventPayload<T extends NexusEventName> = NexusEvents[T];
|
|
250
|
+
declare class TypedEventEmitter extends EventEmitter2 {
|
|
251
|
+
emitEvent<T extends NexusEventName>(event: T, ...args: NexusEvents[T] extends undefined ? [] : [NexusEvents[T]]): boolean;
|
|
252
|
+
onEvent<T extends NexusEventName>(event: T, listener: NexusEvents[T] extends undefined ? () => void : (payload: NexusEvents[T]) => void): this;
|
|
253
|
+
onceEvent<T extends NexusEventName>(event: T, listener: NexusEvents[T] extends undefined ? () => void : (payload: NexusEvents[T]) => void): this;
|
|
254
|
+
offEvent<T extends NexusEventName>(event: T, listener: NexusEvents[T] extends undefined ? () => void : (payload: NexusEvents[T]) => void): this;
|
|
255
|
+
}
|
|
256
|
+
declare const nexusEvents: TypedEventEmitter;
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Request autenticado con usuario y abilities CASL
|
|
260
|
+
* Los módulos usan este tipo en lugar de importar de ../../types/
|
|
261
|
+
*/
|
|
262
|
+
interface AuthRequest extends Request {
|
|
263
|
+
user: User;
|
|
264
|
+
ability: AppAbility;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Helpers para migraciones de base de datos
|
|
269
|
+
*/
|
|
270
|
+
interface MigrationHelpers {
|
|
271
|
+
/** Añade created_at y updated_at */
|
|
272
|
+
addTimestamps: (table: Knex.CreateTableBuilder, db: Knex) => void;
|
|
273
|
+
/** Añade created_by y updated_by si no existen */
|
|
274
|
+
addAuditFieldsIfMissing: (db: Knex, tableName: string) => Promise<void>;
|
|
275
|
+
/** Añade columna si no existe (idempotente). Retorna true si se añadió */
|
|
276
|
+
addColumnIfMissing: (db: Knex, tableName: string, columnName: string, columnBuilder: (table: Knex.AlterTableBuilder) => void) => Promise<boolean>;
|
|
277
|
+
}
|
|
278
|
+
/**
|
|
279
|
+
* Opciones para el middleware de validación Zod
|
|
280
|
+
*/
|
|
281
|
+
interface ValidateOptions {
|
|
282
|
+
body?: ZodSchema;
|
|
283
|
+
query?: ZodSchema;
|
|
284
|
+
params?: ZodSchema;
|
|
285
|
+
}
|
|
286
|
+
/**
|
|
287
|
+
* Middlewares disponibles en el contexto
|
|
288
|
+
* Incluye validate (genérico) y middlewares registrados por módulos
|
|
289
|
+
*/
|
|
290
|
+
interface ModuleMiddlewares {
|
|
291
|
+
/** Validación con Zod */
|
|
292
|
+
validate: (schemas: ValidateOptions) => RequestHandler;
|
|
293
|
+
/** Middlewares registrados dinámicamente por módulos */
|
|
294
|
+
[key: string]: RequestHandler | ((...args: any[]) => RequestHandler) | undefined;
|
|
295
|
+
}
|
|
296
|
+
/**
|
|
297
|
+
* Errores disponibles en el contexto
|
|
298
|
+
* Los módulos usan ctx.errors en lugar de importar directamente
|
|
299
|
+
*/
|
|
300
|
+
interface ModuleErrors {
|
|
301
|
+
AppError: typeof AppError;
|
|
302
|
+
NotFoundError: typeof NotFoundError;
|
|
303
|
+
UnauthorizedError: typeof UnauthorizedError;
|
|
304
|
+
ForbiddenError: typeof ForbiddenError;
|
|
305
|
+
ConflictError: typeof ConflictError;
|
|
306
|
+
}
|
|
307
|
+
/**
|
|
308
|
+
* Funciones de abilities CASL disponibles en el contexto
|
|
309
|
+
*/
|
|
310
|
+
interface ModuleAbilities {
|
|
311
|
+
/** Construye abilities desde permisos de BD */
|
|
312
|
+
defineAbilityFor: (user: User, permissions: RolePermission[]) => AppAbility;
|
|
313
|
+
/** Serializa abilities para enviar al frontend */
|
|
314
|
+
packRules: (ability: AppAbility) => RawRuleOf<AppAbility>[];
|
|
315
|
+
/** Wrapper para verificar permisos contra instancias */
|
|
316
|
+
subject: typeof _casl_ability.subject;
|
|
317
|
+
/** Error de CASL para throwUnlessCan */
|
|
318
|
+
ForbiddenError: typeof _casl_ability.ForbiddenError;
|
|
319
|
+
}
|
|
320
|
+
/**
|
|
321
|
+
* Contexto inyectado a los módulos
|
|
322
|
+
* Contiene todas las herramientas necesarias para operar
|
|
323
|
+
*/
|
|
324
|
+
interface ModuleContext {
|
|
325
|
+
/** Conexión a base de datos */
|
|
326
|
+
db: Knex;
|
|
327
|
+
/** Logger */
|
|
328
|
+
logger: Logger;
|
|
329
|
+
/** Generar ID único */
|
|
330
|
+
generateId: () => string;
|
|
331
|
+
/** Tipo de base de datos */
|
|
332
|
+
dbType: 'sqlite' | 'postgresql' | 'mysql';
|
|
333
|
+
/** Helpers para migraciones */
|
|
334
|
+
helpers: MigrationHelpers;
|
|
335
|
+
/** Crear un router Express */
|
|
336
|
+
createRouter: () => Router;
|
|
337
|
+
/** Middlewares disponibles (validate + registrados por módulos) */
|
|
338
|
+
middleware: ModuleMiddlewares;
|
|
339
|
+
/** Registrar middleware para uso de otros módulos */
|
|
340
|
+
registerMiddleware: (name: string, handler: RequestHandler) => void;
|
|
341
|
+
/** Configuración resuelta de la aplicación */
|
|
342
|
+
config: ResolvedConfig;
|
|
343
|
+
/** Errores para lanzar en módulos (evita imports directos) */
|
|
344
|
+
errors: ModuleErrors;
|
|
345
|
+
/** Funciones de abilities CASL (evita imports directos) */
|
|
346
|
+
abilities: ModuleAbilities;
|
|
347
|
+
/** Event emitter para comunicación entre módulos */
|
|
348
|
+
events: TypedEventEmitter;
|
|
349
|
+
/** Servicio de email SMTP */
|
|
350
|
+
mail: MailService;
|
|
351
|
+
}
|
|
352
|
+
/**
|
|
353
|
+
* Requisitos para activar un módulo
|
|
354
|
+
*/
|
|
355
|
+
interface ModuleRequirements {
|
|
356
|
+
/** Variables de entorno requeridas */
|
|
357
|
+
env?: string[];
|
|
358
|
+
/** Módulos que deben estar activos */
|
|
359
|
+
modules?: string[];
|
|
360
|
+
}
|
|
361
|
+
/**
|
|
362
|
+
* Tipo de campo para formularios dinámicos
|
|
363
|
+
*/
|
|
364
|
+
type FormFieldType = 'text' | 'email' | 'password' | 'number' | 'textarea' | 'markdown' | 'select' | 'checkbox' | 'date' | 'datetime';
|
|
365
|
+
/**
|
|
366
|
+
* JSON Schema para validación de campos (subset serializable)
|
|
367
|
+
* @see https://json-schema.org/understanding-json-schema/
|
|
368
|
+
*/
|
|
369
|
+
interface FieldValidation {
|
|
370
|
+
/** Tipo JSON Schema */
|
|
371
|
+
type?: 'string' | 'number' | 'integer' | 'boolean';
|
|
372
|
+
/** Longitud mínima (strings) o valor mínimo (numbers) */
|
|
373
|
+
minLength?: number;
|
|
374
|
+
maxLength?: number;
|
|
375
|
+
minimum?: number;
|
|
376
|
+
maximum?: number;
|
|
377
|
+
/** Regex pattern para strings */
|
|
378
|
+
pattern?: string;
|
|
379
|
+
/** Formatos predefinidos: email, uri, date, date-time, uuid */
|
|
380
|
+
format?: 'email' | 'uri' | 'date' | 'date-time' | 'uuid';
|
|
381
|
+
/** Mensaje de error personalizado */
|
|
382
|
+
errorMessage?: string;
|
|
383
|
+
}
|
|
384
|
+
/**
|
|
385
|
+
* Configuración de un campo de formulario
|
|
386
|
+
*/
|
|
387
|
+
interface FormField {
|
|
388
|
+
/** Etiqueta del campo */
|
|
389
|
+
label: string;
|
|
390
|
+
/** Tipo de input */
|
|
391
|
+
type: FormFieldType;
|
|
392
|
+
/** Campo requerido (default: false) */
|
|
393
|
+
required?: boolean;
|
|
394
|
+
/** Placeholder del input */
|
|
395
|
+
placeholder?: string;
|
|
396
|
+
/** Solo mostrar en creación, no en edición (ej: password) */
|
|
397
|
+
createOnly?: boolean;
|
|
398
|
+
/** Campo deshabilitado */
|
|
399
|
+
disabled?: boolean;
|
|
400
|
+
/** Campo oculto */
|
|
401
|
+
hidden?: boolean;
|
|
402
|
+
/** Endpoint para cargar opciones (para type: 'select') */
|
|
403
|
+
optionsEndpoint?: string;
|
|
404
|
+
/** Campo a usar como value en opciones (default: 'id') */
|
|
405
|
+
optionValue?: string;
|
|
406
|
+
/** Campo a usar como label en opciones (default: 'name') */
|
|
407
|
+
optionLabel?: string;
|
|
408
|
+
/** Validación JSON Schema (serializable) */
|
|
409
|
+
validation?: FieldValidation;
|
|
410
|
+
}
|
|
411
|
+
/**
|
|
412
|
+
* Tipo de visualización de lista
|
|
413
|
+
*/
|
|
414
|
+
type ListType = 'table' | 'list' | 'grid' | 'masonry';
|
|
415
|
+
/**
|
|
416
|
+
* Configuración de una entidad/tabla del módulo para UI CRUD
|
|
417
|
+
*/
|
|
418
|
+
interface ModuleEntity {
|
|
419
|
+
/** Nombre del subject CASL (debe coincidir con la tabla) */
|
|
420
|
+
name: string;
|
|
421
|
+
/** Nombre para mostrar en UI */
|
|
422
|
+
label: string;
|
|
423
|
+
/** Campos a mostrar en la tabla: { field: 'Label' } */
|
|
424
|
+
listFields: Record<string, string>;
|
|
425
|
+
/** Campos del formulario: { field: FormField } */
|
|
426
|
+
formFields?: Record<string, FormField>;
|
|
427
|
+
/** Campo usado como título/label en la UI */
|
|
428
|
+
labelField: string;
|
|
429
|
+
/** Prefijo de ruta si es diferente al del módulo */
|
|
430
|
+
routePrefix?: string;
|
|
431
|
+
/** Modo de edición: 'modal' (default) o 'page' para formularios grandes */
|
|
432
|
+
editMode?: 'modal' | 'page';
|
|
433
|
+
/** Tipo de visualización: 'table' (default), 'list', 'grid', 'masonry' */
|
|
434
|
+
listType?: ListType;
|
|
435
|
+
}
|
|
436
|
+
/**
|
|
437
|
+
* Manifest de un módulo Nexus
|
|
438
|
+
*
|
|
439
|
+
* Define la estructura y capacidades de un módulo para auto-discovery.
|
|
440
|
+
* Usado por el sistema de plugins y extensiones.
|
|
441
|
+
*/
|
|
442
|
+
interface ModuleManifest {
|
|
443
|
+
/** Identificador único del módulo (ej: 'users', 'posts') */
|
|
444
|
+
name: string;
|
|
445
|
+
/** Código único del módulo (ej: 'USR', 'PST') - para referencias cortas */
|
|
446
|
+
code: string;
|
|
447
|
+
/** Nombre para mostrar en UI */
|
|
448
|
+
label: string;
|
|
449
|
+
/** Icono del módulo (nombre de @vicons/ionicons5 o ruta a png/svg) */
|
|
450
|
+
icon?: string;
|
|
451
|
+
/** Descripción del módulo */
|
|
452
|
+
description?: string;
|
|
453
|
+
/** Versión del módulo (semver) */
|
|
454
|
+
version?: string;
|
|
455
|
+
/** Tipo de módulo: 'core' | 'plugin' | 'auth-plugin' | 'custom' */
|
|
456
|
+
type?: 'core' | 'plugin' | 'auth-plugin' | 'custom';
|
|
457
|
+
/** Categoría del módulo para agrupar en sidebar (vacío = raíz) */
|
|
458
|
+
category?: string;
|
|
459
|
+
/** Dependencias de otros módulos (para orden de migración/rutas) */
|
|
460
|
+
dependencies?: string[];
|
|
461
|
+
/** Requisitos para activar el módulo */
|
|
462
|
+
required?: ModuleRequirements;
|
|
463
|
+
/** Función de migración de base de datos */
|
|
464
|
+
migrate?: (ctx: ModuleContext) => Promise<void>;
|
|
465
|
+
/** Función de seed de datos iniciales */
|
|
466
|
+
seed?: (ctx: ModuleContext) => Promise<void>;
|
|
467
|
+
/** Inicialización del módulo (registrar middlewares, etc.) - se ejecuta antes de routes */
|
|
468
|
+
init?: (ctx: ModuleContext) => void;
|
|
469
|
+
/** Factory de rutas del módulo (recibe contexto) */
|
|
470
|
+
routes?: (ctx: ModuleContext) => Router;
|
|
471
|
+
/** Prefijo de rutas (default: /{name}) */
|
|
472
|
+
routePrefix?: string;
|
|
473
|
+
/** Subjects CASL que expone el módulo */
|
|
474
|
+
subjects?: string[];
|
|
475
|
+
/** Entidades/tablas del módulo con config CRUD para UI */
|
|
476
|
+
entities?: ModuleEntity[];
|
|
477
|
+
}
|
|
478
|
+
/**
|
|
479
|
+
* Manifest de un plugin Nexus
|
|
480
|
+
*
|
|
481
|
+
* Agrupa múltiples módulos relacionados en un solo paquete instalable.
|
|
482
|
+
* Los plugins se distribuyen como paquetes npm con peerDependency en @gzl10/nexus-backend.
|
|
483
|
+
*/
|
|
484
|
+
interface PluginManifest {
|
|
485
|
+
/** Nombre del plugin (normalmente el nombre del paquete npm) */
|
|
486
|
+
name: string;
|
|
487
|
+
/** Versión del plugin (semver) */
|
|
488
|
+
version: string;
|
|
489
|
+
/** Descripción del plugin */
|
|
490
|
+
description: string;
|
|
491
|
+
/** Módulos incluidos en el plugin */
|
|
492
|
+
modules: ModuleManifest[];
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
/**
|
|
496
|
+
* Ordenar módulos por dependencias (topological sort)
|
|
497
|
+
* Garantiza que un módulo se procese después de sus dependencias
|
|
498
|
+
*/
|
|
499
|
+
declare function getOrderedModules(): ModuleManifest[];
|
|
500
|
+
/**
|
|
501
|
+
* Registrar módulo dinámicamente (para plugins)
|
|
502
|
+
*/
|
|
503
|
+
declare function registerModule(mod: ModuleManifest): void;
|
|
504
|
+
/**
|
|
505
|
+
* Obtener todos los módulos registrados
|
|
506
|
+
*/
|
|
507
|
+
declare function getModules(): ModuleManifest[];
|
|
508
|
+
/**
|
|
509
|
+
* Obtener módulo por nombre
|
|
510
|
+
*/
|
|
511
|
+
declare function getModule(name: string): ModuleManifest | undefined;
|
|
512
|
+
/**
|
|
513
|
+
* Obtener todos los subjects registrados en módulos
|
|
514
|
+
* Incluye 'all' que siempre está disponible
|
|
515
|
+
*/
|
|
516
|
+
declare function getRegisteredSubjects(): string[];
|
|
517
|
+
/**
|
|
518
|
+
* Validar que un subject existe en algún módulo registrado
|
|
519
|
+
*/
|
|
520
|
+
declare function isValidSubject(subject: string): boolean;
|
|
521
|
+
|
|
522
|
+
interface PaginationParams {
|
|
523
|
+
page: number;
|
|
524
|
+
limit: number;
|
|
525
|
+
}
|
|
526
|
+
interface PaginatedResult<T> {
|
|
527
|
+
items: T[];
|
|
528
|
+
total: number;
|
|
529
|
+
page: number;
|
|
530
|
+
limit: number;
|
|
531
|
+
totalPages: number;
|
|
532
|
+
hasNext: boolean;
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
interface RefreshToken {
|
|
536
|
+
id: string;
|
|
537
|
+
token: string;
|
|
538
|
+
user_id: string;
|
|
539
|
+
expires_at: Date;
|
|
540
|
+
created_at: Date;
|
|
541
|
+
}
|
|
542
|
+
interface JwtPayload {
|
|
543
|
+
userId: string;
|
|
544
|
+
email: string;
|
|
545
|
+
roleId: string;
|
|
546
|
+
}
|
|
547
|
+
interface TokenPair {
|
|
548
|
+
accessToken: string;
|
|
549
|
+
refreshToken: string;
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
/**
|
|
553
|
+
* Construye abilities CASL desde permisos de BD.
|
|
554
|
+
* @param user - Usuario autenticado
|
|
555
|
+
* @param permissions - Permisos del rol del usuario (cargados desde BD)
|
|
556
|
+
*/
|
|
557
|
+
declare function defineAbilityFor(user: User, permissions: RolePermission[]): AppAbility;
|
|
558
|
+
declare function packRules(ability: AppAbility): RawRuleOf<AppAbility>[];
|
|
559
|
+
declare function unpackRules(rules: RawRuleOf<AppAbility>[]): AppAbility;
|
|
560
|
+
|
|
561
|
+
export { type Actions, type AppAbility, type AuthRequest, type DbEventPayload, type JwtPayload, type MigrationHelpers, type ModuleAbilities, type ModuleContext, type ModuleEntity, type ModuleErrors, type ModuleManifest, type ModuleMiddlewares, type ModuleRequirements, type NexusConfig, type NexusEventName, type NexusEventPayload, type NexusEvents, type PaginatedResult, type PaginationParams, type PluginManifest, type RefreshToken, type ResolvedConfig, type Role, type RolePermission, type RoleWithCounts, type RoleWithPermissions, type SubjectRegistry, type SubjectStrings, type Subjects, type TokenPair, type User, type UserWithRole, type UserWithoutPassword, type ValidateOptions, createApp, db, defineAbilityFor, destroyDb, getConfig, getDatabaseType, getModule, getModules, getOrderedModules, getRegisteredSubjects, isRunning, isValidSubject, nexusEvents, packRules, registerModule, restart, start, stop, unpackRules };
|