@nocios/crudify-ui 1.3.1 → 1.3.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.
- package/README.md +155 -1108
- package/README_DEPTH.md +1046 -0
- package/package.json +1 -1
- package/MIGRATION-GUIDE.md +0 -312
- package/MIGRATION.md +0 -201
- package/MIGRATION_EXAMPLE.md +0 -538
- package/TECHNICAL_SPECIFICATION.md +0 -344
- package/example-app.tsx +0 -197
- package/mejoras_npm_lib.md +0 -790
- package/tsup.config.ts +0 -9
package/package.json
CHANGED
package/MIGRATION-GUIDE.md
DELETED
|
@@ -1,312 +0,0 @@
|
|
|
1
|
-
# Migration Guide: Refresh Token Pattern Implementation
|
|
2
|
-
|
|
3
|
-
## Overview
|
|
4
|
-
|
|
5
|
-
This guide helps you migrate from the standard JWT authentication to the new **Refresh Token Pattern** implementation in CRUDIFY v1.2.0+.
|
|
6
|
-
|
|
7
|
-
The Refresh Token Pattern addresses security vulnerabilities by:
|
|
8
|
-
- Using short-lived access tokens (15 minutes)
|
|
9
|
-
- Implementing long-lived refresh tokens (7 days)
|
|
10
|
-
- Automatic token refresh before expiration
|
|
11
|
-
- Secure token storage with encryption
|
|
12
|
-
- Session restoration on page reload
|
|
13
|
-
|
|
14
|
-
## Prerequisites
|
|
15
|
-
|
|
16
|
-
Ensure you have the following versions installed:
|
|
17
|
-
- `@nocios/crudify-core` v1.2.0+
|
|
18
|
-
- `@nocios/crudify-ui` v1.2.0+
|
|
19
|
-
|
|
20
|
-
```bash
|
|
21
|
-
npm update @nocios/crudify-core @nocios/crudify-ui
|
|
22
|
-
```
|
|
23
|
-
|
|
24
|
-
## Migration Steps
|
|
25
|
-
|
|
26
|
-
### 1. Update Your Backend (if applicable)
|
|
27
|
-
|
|
28
|
-
If you're using the CRUDIFY backend, ensure you've deployed the refresh token endpoints. The new login response now includes:
|
|
29
|
-
|
|
30
|
-
```typescript
|
|
31
|
-
{
|
|
32
|
-
token: string, // Access token (short-lived)
|
|
33
|
-
refreshToken: string, // Refresh token (long-lived)
|
|
34
|
-
expiresIn: number, // Access token expiration
|
|
35
|
-
refreshExpiresIn: number // Refresh token expiration
|
|
36
|
-
}
|
|
37
|
-
```
|
|
38
|
-
|
|
39
|
-
### 2. Replace SessionProvider Implementation
|
|
40
|
-
|
|
41
|
-
**BEFORE (Old implementation):**
|
|
42
|
-
```tsx
|
|
43
|
-
import { CrudifyDataProvider } from '@nocios/crudify-ui';
|
|
44
|
-
|
|
45
|
-
function App() {
|
|
46
|
-
return (
|
|
47
|
-
<CrudifyDataProvider>
|
|
48
|
-
<YourApp />
|
|
49
|
-
</CrudifyDataProvider>
|
|
50
|
-
);
|
|
51
|
-
}
|
|
52
|
-
```
|
|
53
|
-
|
|
54
|
-
**AFTER (New Refresh Token Pattern):**
|
|
55
|
-
```tsx
|
|
56
|
-
import { SessionProvider } from '@nocios/crudify-ui';
|
|
57
|
-
|
|
58
|
-
function App() {
|
|
59
|
-
return (
|
|
60
|
-
<SessionProvider
|
|
61
|
-
options={{
|
|
62
|
-
autoRestore: true, // Restore session on page reload
|
|
63
|
-
enableLogging: true, // Enable debug logs
|
|
64
|
-
onSessionExpired: () => { // Handle session expiration
|
|
65
|
-
console.log('Session expired');
|
|
66
|
-
// Redirect to login or show modal
|
|
67
|
-
},
|
|
68
|
-
onSessionRestored: (tokens) => {
|
|
69
|
-
console.log('Session restored:', tokens);
|
|
70
|
-
}
|
|
71
|
-
}}
|
|
72
|
-
>
|
|
73
|
-
<YourApp />
|
|
74
|
-
</SessionProvider>
|
|
75
|
-
);
|
|
76
|
-
}
|
|
77
|
-
```
|
|
78
|
-
|
|
79
|
-
### 3. Update Authentication Logic
|
|
80
|
-
|
|
81
|
-
**BEFORE (Manual login handling):**
|
|
82
|
-
```tsx
|
|
83
|
-
import { useCrudifyLogin } from '@nocios/crudify-ui';
|
|
84
|
-
|
|
85
|
-
function LoginForm() {
|
|
86
|
-
const { login, isLoading } = useCrudifyLogin();
|
|
87
|
-
|
|
88
|
-
const handleLogin = async () => {
|
|
89
|
-
const result = await login(email, password);
|
|
90
|
-
// Manual token handling
|
|
91
|
-
};
|
|
92
|
-
}
|
|
93
|
-
```
|
|
94
|
-
|
|
95
|
-
**AFTER (Automatic session management):**
|
|
96
|
-
```tsx
|
|
97
|
-
import { useSessionContext } from '@nocios/crudify-ui';
|
|
98
|
-
|
|
99
|
-
function LoginForm() {
|
|
100
|
-
const { login, isLoading, isAuthenticated, logout } = useSessionContext();
|
|
101
|
-
|
|
102
|
-
const handleLogin = async () => {
|
|
103
|
-
const result = await login(email, password);
|
|
104
|
-
// Session is managed automatically
|
|
105
|
-
if (result.success) {
|
|
106
|
-
// User is now authenticated
|
|
107
|
-
}
|
|
108
|
-
};
|
|
109
|
-
}
|
|
110
|
-
```
|
|
111
|
-
|
|
112
|
-
### 4. Replace Authentication Checks
|
|
113
|
-
|
|
114
|
-
**BEFORE (Manual token checking):**
|
|
115
|
-
```tsx
|
|
116
|
-
import { getCurrentUserEmail, isTokenExpired } from '@nocios/crudify-ui';
|
|
117
|
-
|
|
118
|
-
function ProtectedComponent() {
|
|
119
|
-
const userEmail = getCurrentUserEmail();
|
|
120
|
-
const tokenExpired = isTokenExpired();
|
|
121
|
-
|
|
122
|
-
if (!userEmail || tokenExpired) {
|
|
123
|
-
return <LoginRequired />;
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
return <ProtectedContent />;
|
|
127
|
-
}
|
|
128
|
-
```
|
|
129
|
-
|
|
130
|
-
**AFTER (Using ProtectedRoute):**
|
|
131
|
-
```tsx
|
|
132
|
-
import { ProtectedRoute } from '@nocios/crudify-ui';
|
|
133
|
-
|
|
134
|
-
function ProtectedComponent() {
|
|
135
|
-
return (
|
|
136
|
-
<ProtectedRoute fallback={<LoginRequired />}>
|
|
137
|
-
<ProtectedContent />
|
|
138
|
-
</ProtectedRoute>
|
|
139
|
-
);
|
|
140
|
-
}
|
|
141
|
-
```
|
|
142
|
-
|
|
143
|
-
### 5. Update API Calls
|
|
144
|
-
|
|
145
|
-
The good news is that your existing CRUDIFY API calls **don't need to change**! The automatic refresh mechanism is built into the core library:
|
|
146
|
-
|
|
147
|
-
```tsx
|
|
148
|
-
import { crudify } from '@nocios/crudify-ui';
|
|
149
|
-
|
|
150
|
-
// This automatically handles token refresh if needed
|
|
151
|
-
const result = await crudify.getPermissions();
|
|
152
|
-
```
|
|
153
|
-
|
|
154
|
-
## New Features Available
|
|
155
|
-
|
|
156
|
-
### 1. Session Status Component
|
|
157
|
-
|
|
158
|
-
Display authentication status anywhere in your app:
|
|
159
|
-
|
|
160
|
-
```tsx
|
|
161
|
-
import { SessionStatus } from '@nocios/crudify-ui';
|
|
162
|
-
|
|
163
|
-
function AppHeader() {
|
|
164
|
-
return (
|
|
165
|
-
<AppBar>
|
|
166
|
-
<Toolbar>
|
|
167
|
-
<Typography variant="h6">My App</Typography>
|
|
168
|
-
<SessionStatus /> {/* Shows auth status */}
|
|
169
|
-
</Toolbar>
|
|
170
|
-
</AppBar>
|
|
171
|
-
);
|
|
172
|
-
}
|
|
173
|
-
```
|
|
174
|
-
|
|
175
|
-
### 2. Login Component (Optional)
|
|
176
|
-
|
|
177
|
-
Ready-to-use login component with Material-UI:
|
|
178
|
-
|
|
179
|
-
```tsx
|
|
180
|
-
import { LoginComponent } from '@nocios/crudify-ui';
|
|
181
|
-
|
|
182
|
-
function LoginPage() {
|
|
183
|
-
return <LoginComponent />;
|
|
184
|
-
}
|
|
185
|
-
```
|
|
186
|
-
|
|
187
|
-
### 3. Session Debug Info
|
|
188
|
-
|
|
189
|
-
For development, show detailed session information:
|
|
190
|
-
|
|
191
|
-
```tsx
|
|
192
|
-
import { SessionDebugInfo } from '@nocios/crudify-ui';
|
|
193
|
-
|
|
194
|
-
function App() {
|
|
195
|
-
return (
|
|
196
|
-
<div>
|
|
197
|
-
<YourApp />
|
|
198
|
-
{process.env.NODE_ENV === 'development' && (
|
|
199
|
-
<SessionDebugInfo />
|
|
200
|
-
)}
|
|
201
|
-
</div>
|
|
202
|
-
);
|
|
203
|
-
}
|
|
204
|
-
```
|
|
205
|
-
|
|
206
|
-
### 4. Session Context Hook
|
|
207
|
-
|
|
208
|
-
Access session state from any component:
|
|
209
|
-
|
|
210
|
-
```tsx
|
|
211
|
-
import { useSessionContext } from '@nocios/crudify-ui';
|
|
212
|
-
|
|
213
|
-
function MyComponent() {
|
|
214
|
-
const {
|
|
215
|
-
isAuthenticated,
|
|
216
|
-
isLoading,
|
|
217
|
-
tokens,
|
|
218
|
-
error,
|
|
219
|
-
login,
|
|
220
|
-
logout,
|
|
221
|
-
refreshTokens,
|
|
222
|
-
isExpiringSoon,
|
|
223
|
-
expiresIn
|
|
224
|
-
} = useSessionContext();
|
|
225
|
-
|
|
226
|
-
return (
|
|
227
|
-
<div>
|
|
228
|
-
{isExpiringSoon && (
|
|
229
|
-
<Alert>Token expires in {Math.round(expiresIn / 60000)} minutes</Alert>
|
|
230
|
-
)}
|
|
231
|
-
</div>
|
|
232
|
-
);
|
|
233
|
-
}
|
|
234
|
-
```
|
|
235
|
-
|
|
236
|
-
## Migration Checklist
|
|
237
|
-
|
|
238
|
-
- [ ] Update package versions to v1.2.0+
|
|
239
|
-
- [ ] Replace `CrudifyDataProvider` with `SessionProvider`
|
|
240
|
-
- [ ] Update login logic to use `useSessionContext`
|
|
241
|
-
- [ ] Replace manual auth checks with `ProtectedRoute`
|
|
242
|
-
- [ ] Test automatic token refresh functionality
|
|
243
|
-
- [ ] Test session restoration on page reload
|
|
244
|
-
- [ ] Update logout logic to use session context
|
|
245
|
-
- [ ] Remove manual token management code
|
|
246
|
-
- [ ] Test error handling for expired refresh tokens
|
|
247
|
-
- [ ] Add session debug info for development
|
|
248
|
-
|
|
249
|
-
## Troubleshooting
|
|
250
|
-
|
|
251
|
-
### Issue: "useSessionContext must be used within a SessionProvider"
|
|
252
|
-
**Solution:** Ensure your entire app is wrapped with `SessionProvider`
|
|
253
|
-
|
|
254
|
-
### Issue: Session not restoring on page reload
|
|
255
|
-
**Solution:** Check that `autoRestore: true` is set in SessionProvider options
|
|
256
|
-
|
|
257
|
-
### Issue: Automatic refresh not working
|
|
258
|
-
**Solution:** Verify that your backend returns refresh tokens in the login response
|
|
259
|
-
|
|
260
|
-
### Issue: Tokens not persisting between sessions
|
|
261
|
-
**Solution:** Check browser storage permissions and ensure localStorage is enabled
|
|
262
|
-
|
|
263
|
-
## Example Complete Implementation
|
|
264
|
-
|
|
265
|
-
```tsx
|
|
266
|
-
// App.tsx
|
|
267
|
-
import React from 'react';
|
|
268
|
-
import { SessionProvider, ProtectedRoute } from '@nocios/crudify-ui';
|
|
269
|
-
import { LoginPage } from './pages/LoginPage';
|
|
270
|
-
import { Dashboard } from './pages/Dashboard';
|
|
271
|
-
|
|
272
|
-
function App() {
|
|
273
|
-
return (
|
|
274
|
-
<SessionProvider
|
|
275
|
-
options={{
|
|
276
|
-
autoRestore: true,
|
|
277
|
-
enableLogging: process.env.NODE_ENV === 'development',
|
|
278
|
-
onSessionExpired: () => {
|
|
279
|
-
console.log('Session expired - redirecting to login');
|
|
280
|
-
}
|
|
281
|
-
}}
|
|
282
|
-
>
|
|
283
|
-
<AppContent />
|
|
284
|
-
</SessionProvider>
|
|
285
|
-
);
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
function AppContent() {
|
|
289
|
-
return (
|
|
290
|
-
<div>
|
|
291
|
-
<LoginPage />
|
|
292
|
-
|
|
293
|
-
<ProtectedRoute fallback={null}>
|
|
294
|
-
<Dashboard />
|
|
295
|
-
</ProtectedRoute>
|
|
296
|
-
</div>
|
|
297
|
-
);
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
export default App;
|
|
301
|
-
```
|
|
302
|
-
|
|
303
|
-
## Need Help?
|
|
304
|
-
|
|
305
|
-
If you encounter issues during migration:
|
|
306
|
-
|
|
307
|
-
1. Check the browser console for detailed error messages
|
|
308
|
-
2. Enable logging with `enableLogging: true` in SessionProvider
|
|
309
|
-
3. Use `<SessionDebugInfo />` to inspect session state
|
|
310
|
-
4. Verify your backend is returning refresh tokens properly
|
|
311
|
-
|
|
312
|
-
The new Refresh Token Pattern provides enhanced security and better user experience with automatic session management.
|
package/MIGRATION.md
DELETED
|
@@ -1,201 +0,0 @@
|
|
|
1
|
-
# Guía de Migración - useCrudifyUser v1.2.25+
|
|
2
|
-
|
|
3
|
-
## Cambios Importantes
|
|
4
|
-
|
|
5
|
-
A partir de la versión 1.2.25, el hook `useCrudifyUser` ha sido completamente reestructurado para ser más simple y claro.
|
|
6
|
-
|
|
7
|
-
## Antes (❌ Estructura Anterior)
|
|
8
|
-
|
|
9
|
-
```typescript
|
|
10
|
-
const {
|
|
11
|
-
userEmail,
|
|
12
|
-
userId,
|
|
13
|
-
userIdentifier,
|
|
14
|
-
userProfile,
|
|
15
|
-
profileLoading,
|
|
16
|
-
profileError,
|
|
17
|
-
extendedData,
|
|
18
|
-
refreshProfile,
|
|
19
|
-
clearProfile
|
|
20
|
-
} = useCrudifyUser();
|
|
21
|
-
|
|
22
|
-
// Acceso a datos
|
|
23
|
-
console.log(extendedData.fullProfile);
|
|
24
|
-
console.log(extendedData.displayData);
|
|
25
|
-
console.log(extendedData.totalFields);
|
|
26
|
-
```
|
|
27
|
-
|
|
28
|
-
## Ahora (✅ Nueva Estructura)
|
|
29
|
-
|
|
30
|
-
```typescript
|
|
31
|
-
const {
|
|
32
|
-
user,
|
|
33
|
-
loading,
|
|
34
|
-
error,
|
|
35
|
-
refreshProfile,
|
|
36
|
-
clearProfile
|
|
37
|
-
} = useCrudifyUser();
|
|
38
|
-
|
|
39
|
-
// Acceso a datos
|
|
40
|
-
console.log(user.session); // Datos del JWT token
|
|
41
|
-
console.log(user.data); // Datos de la base de datos
|
|
42
|
-
```
|
|
43
|
-
|
|
44
|
-
## Estructura del objeto `user`
|
|
45
|
-
|
|
46
|
-
```typescript
|
|
47
|
-
interface CrudifyUserData {
|
|
48
|
-
session: JWTPayload | null; // Datos del token JWT
|
|
49
|
-
data: UserProfile | null; // Datos de la base de datos
|
|
50
|
-
}
|
|
51
|
-
```
|
|
52
|
-
|
|
53
|
-
### `user.session` (Datos del JWT)
|
|
54
|
-
- `user.session.sub` - User ID
|
|
55
|
-
- `user.session.email` - Email del usuario
|
|
56
|
-
- `user.session.name` - Nombre del usuario
|
|
57
|
-
- `user.session.exp` - Timestamp de expiración
|
|
58
|
-
- `user.session.iat` - Timestamp de emisión
|
|
59
|
-
- Y todos los demás campos del JWT token
|
|
60
|
-
|
|
61
|
-
### `user.data` (Datos de la Base de Datos)
|
|
62
|
-
- `user.data.id` - ID del perfil en la BD
|
|
63
|
-
- `user.data.email` - Email del perfil
|
|
64
|
-
- `user.data.firstName` - Nombre
|
|
65
|
-
- `user.data.lastName` - Apellido
|
|
66
|
-
- `user.data.role` - Rol del usuario
|
|
67
|
-
- Y todos los demás campos del perfil de usuario
|
|
68
|
-
|
|
69
|
-
## Migraciones Necesarias
|
|
70
|
-
|
|
71
|
-
### 1. Cambiar las destructuraciones
|
|
72
|
-
```typescript
|
|
73
|
-
// ❌ Antes
|
|
74
|
-
const { userProfile, profileLoading, profileError } = useCrudifyUser();
|
|
75
|
-
|
|
76
|
-
// ✅ Ahora
|
|
77
|
-
const { user, loading, error } = useCrudifyUser();
|
|
78
|
-
```
|
|
79
|
-
|
|
80
|
-
### 2. Actualizar acceso a datos de sesión
|
|
81
|
-
```typescript
|
|
82
|
-
// ❌ Antes
|
|
83
|
-
const email = userEmail;
|
|
84
|
-
const userId = userId;
|
|
85
|
-
|
|
86
|
-
// ✅ Ahora
|
|
87
|
-
const email = user.session?.email;
|
|
88
|
-
const userId = user.session?.sub;
|
|
89
|
-
```
|
|
90
|
-
|
|
91
|
-
### 3. Actualizar acceso a datos de perfil
|
|
92
|
-
```typescript
|
|
93
|
-
// ❌ Antes
|
|
94
|
-
const profileData = userProfile;
|
|
95
|
-
const fullName = extendedData.displayData.fullName;
|
|
96
|
-
|
|
97
|
-
// ✅ Ahora
|
|
98
|
-
const profileData = user.data;
|
|
99
|
-
const fullName = user.data?.fullName;
|
|
100
|
-
```
|
|
101
|
-
|
|
102
|
-
### 4. Actualizar estados de carga y error
|
|
103
|
-
```typescript
|
|
104
|
-
// ❌ Antes
|
|
105
|
-
if (profileLoading) return <Loading />;
|
|
106
|
-
if (profileError) return <Error message={profileError} />;
|
|
107
|
-
|
|
108
|
-
// ✅ Ahora
|
|
109
|
-
if (loading) return <Loading />;
|
|
110
|
-
if (error) return <Error message={error} />;
|
|
111
|
-
```
|
|
112
|
-
|
|
113
|
-
## Beneficios de la Nueva Estructura
|
|
114
|
-
|
|
115
|
-
1. **Más Simple**: Solo un objeto `user` con `session` y `data`
|
|
116
|
-
2. **Más Claro**: Es obvio qué viene del JWT (`session`) y qué de la DB (`data`)
|
|
117
|
-
3. **Sin Duplicación**: La información no se repite en múltiples lugares
|
|
118
|
-
4. **Mejor TypeScript**: Tipos más precisos y claros
|
|
119
|
-
5. **Más Fácil de Usar**: Los desarrolladores entienden inmediatamente la estructura
|
|
120
|
-
|
|
121
|
-
## Ejemplo Completo de Migración
|
|
122
|
-
|
|
123
|
-
### Antes
|
|
124
|
-
```tsx
|
|
125
|
-
function UserProfile() {
|
|
126
|
-
const {
|
|
127
|
-
userEmail,
|
|
128
|
-
userProfile,
|
|
129
|
-
profileLoading,
|
|
130
|
-
profileError,
|
|
131
|
-
extendedData,
|
|
132
|
-
refreshProfile
|
|
133
|
-
} = useCrudifyUser();
|
|
134
|
-
|
|
135
|
-
if (profileLoading) return <div>Cargando...</div>;
|
|
136
|
-
if (profileError) return <div>Error: {profileError}</div>;
|
|
137
|
-
|
|
138
|
-
return (
|
|
139
|
-
<div>
|
|
140
|
-
<h1>Welcome {userProfile?.fullName || userEmail}</h1>
|
|
141
|
-
<p>Total fields: {extendedData.totalFields}</p>
|
|
142
|
-
<pre>{JSON.stringify(extendedData.displayData, null, 2)}</pre>
|
|
143
|
-
<button onClick={refreshProfile}>Refresh</button>
|
|
144
|
-
</div>
|
|
145
|
-
);
|
|
146
|
-
}
|
|
147
|
-
```
|
|
148
|
-
|
|
149
|
-
### Después
|
|
150
|
-
```tsx
|
|
151
|
-
function UserProfile() {
|
|
152
|
-
const {
|
|
153
|
-
user,
|
|
154
|
-
loading,
|
|
155
|
-
error,
|
|
156
|
-
refreshProfile
|
|
157
|
-
} = useCrudifyUser();
|
|
158
|
-
|
|
159
|
-
if (loading) return <div>Cargando...</div>;
|
|
160
|
-
if (error) return <div>Error: {error}</div>;
|
|
161
|
-
|
|
162
|
-
return (
|
|
163
|
-
<div>
|
|
164
|
-
<h1>Welcome {user.data?.fullName || user.session?.email}</h1>
|
|
165
|
-
<h2>Datos de Sesión:</h2>
|
|
166
|
-
<pre>{JSON.stringify(user.session, null, 2)}</pre>
|
|
167
|
-
<h2>Datos de Perfil:</h2>
|
|
168
|
-
<pre>{JSON.stringify(user.data, null, 2)}</pre>
|
|
169
|
-
<button onClick={refreshProfile}>Refresh</button>
|
|
170
|
-
</div>
|
|
171
|
-
);
|
|
172
|
-
}
|
|
173
|
-
```
|
|
174
|
-
|
|
175
|
-
## Retrocompatibilidad
|
|
176
|
-
|
|
177
|
-
Para mantener compatibilidad temporal, puedes crear un wrapper:
|
|
178
|
-
|
|
179
|
-
```typescript
|
|
180
|
-
// wrapper temporal para compatibilidad
|
|
181
|
-
export const useLegacyUserProfile = () => {
|
|
182
|
-
const { user, loading, error, refreshProfile, clearProfile } = useCrudifyUser();
|
|
183
|
-
|
|
184
|
-
return {
|
|
185
|
-
userEmail: user.session?.email || null,
|
|
186
|
-
userId: user.session?.sub || null,
|
|
187
|
-
userProfile: user.data,
|
|
188
|
-
profileLoading: loading,
|
|
189
|
-
profileError: error,
|
|
190
|
-
extendedData: {
|
|
191
|
-
fullProfile: user.data,
|
|
192
|
-
displayData: user.data,
|
|
193
|
-
totalFields: user.data ? Object.keys(user.data).length : 0
|
|
194
|
-
},
|
|
195
|
-
refreshProfile,
|
|
196
|
-
clearProfile
|
|
197
|
-
};
|
|
198
|
-
};
|
|
199
|
-
```
|
|
200
|
-
|
|
201
|
-
**Nota**: Este wrapper es solo para facilitar la migración. Se recomienda migrar completamente a la nueva estructura.
|