django-cfg 1.4.10__py3-none-any.whl → 1.4.11__py3-none-any.whl
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.
- django_cfg/apps/agents/management/commands/create_agent.py +1 -1
- django_cfg/apps/agents/management/commands/orchestrator_status.py +3 -3
- django_cfg/apps/newsletter/serializers.py +40 -3
- django_cfg/apps/newsletter/views/campaigns.py +12 -3
- django_cfg/apps/newsletter/views/emails.py +14 -3
- django_cfg/apps/newsletter/views/subscriptions.py +12 -2
- django_cfg/apps/payments/views/api/currencies.py +49 -6
- django_cfg/apps/payments/views/api/webhooks.py +72 -7
- django_cfg/apps/payments/views/overview/serializers.py +34 -1
- django_cfg/apps/payments/views/overview/views.py +2 -1
- django_cfg/apps/payments/views/serializers/payments.py +6 -6
- django_cfg/apps/urls.py +106 -45
- django_cfg/core/base/config_model.py +2 -2
- django_cfg/core/constants.py +1 -1
- django_cfg/core/generation/integration_generators/__init__.py +1 -1
- django_cfg/core/generation/integration_generators/api.py +72 -49
- django_cfg/core/integration/display/startup.py +30 -22
- django_cfg/core/integration/url_integration.py +15 -16
- django_cfg/dashboard/sections/documentation.py +391 -0
- django_cfg/management/commands/check_endpoints.py +11 -160
- django_cfg/management/commands/check_settings.py +13 -348
- django_cfg/management/commands/clear_constance.py +13 -201
- django_cfg/management/commands/create_token.py +13 -321
- django_cfg/management/commands/generate_clients.py +23 -0
- django_cfg/management/commands/list_urls.py +13 -306
- django_cfg/management/commands/migrate_all.py +13 -126
- django_cfg/management/commands/migrator.py +13 -396
- django_cfg/management/commands/rundramatiq.py +15 -247
- django_cfg/management/commands/rundramatiq_simulator.py +12 -429
- django_cfg/management/commands/runserver_ngrok.py +15 -160
- django_cfg/management/commands/script.py +12 -488
- django_cfg/management/commands/show_config.py +12 -215
- django_cfg/management/commands/show_urls.py +12 -342
- django_cfg/management/commands/superuser.py +15 -295
- django_cfg/management/commands/task_clear.py +14 -217
- django_cfg/management/commands/task_status.py +13 -248
- django_cfg/management/commands/test_email.py +15 -86
- django_cfg/management/commands/test_telegram.py +14 -61
- django_cfg/management/commands/test_twilio.py +15 -105
- django_cfg/management/commands/tree.py +13 -383
- django_cfg/management/commands/validate_openapi.py +10 -0
- django_cfg/middleware/README.md +1 -1
- django_cfg/middleware/user_activity.py +3 -3
- django_cfg/models/__init__.py +2 -2
- django_cfg/models/api/drf/spectacular.py +6 -6
- django_cfg/models/django/__init__.py +2 -2
- django_cfg/models/django/openapi.py +238 -0
- django_cfg/modules/django_admin/management/__init__.py +0 -0
- django_cfg/modules/django_admin/management/commands/__init__.py +0 -0
- django_cfg/modules/django_admin/management/commands/check_endpoints.py +169 -0
- django_cfg/modules/django_admin/management/commands/check_settings.py +355 -0
- django_cfg/modules/django_admin/management/commands/clear_constance.py +208 -0
- django_cfg/modules/django_admin/management/commands/create_token.py +328 -0
- django_cfg/modules/django_admin/management/commands/list_urls.py +313 -0
- django_cfg/modules/django_admin/management/commands/migrate_all.py +133 -0
- django_cfg/modules/django_admin/management/commands/migrator.py +403 -0
- django_cfg/modules/django_admin/management/commands/script.py +496 -0
- django_cfg/modules/django_admin/management/commands/show_config.py +225 -0
- django_cfg/modules/django_admin/management/commands/show_urls.py +361 -0
- django_cfg/modules/django_admin/management/commands/superuser.py +302 -0
- django_cfg/modules/django_admin/management/commands/tree.py +390 -0
- django_cfg/modules/django_client/__init__.py +20 -0
- django_cfg/modules/django_client/apps.py +35 -0
- django_cfg/modules/django_client/core/__init__.py +56 -0
- django_cfg/modules/django_client/core/archive/__init__.py +11 -0
- django_cfg/modules/django_client/core/archive/manager.py +134 -0
- django_cfg/modules/django_client/core/cli/__init__.py +12 -0
- django_cfg/modules/django_client/core/cli/main.py +235 -0
- django_cfg/modules/django_client/core/config/__init__.py +18 -0
- django_cfg/modules/django_client/core/config/config.py +188 -0
- django_cfg/modules/django_client/core/config/group.py +101 -0
- django_cfg/modules/django_client/core/config/service.py +209 -0
- django_cfg/modules/django_client/core/generator/__init__.py +115 -0
- django_cfg/modules/django_client/core/generator/base.py +767 -0
- django_cfg/modules/django_client/core/generator/python.py +751 -0
- django_cfg/modules/django_client/core/generator/templates/python/__init__.py.jinja +9 -0
- django_cfg/modules/django_client/core/generator/templates/python/api_wrapper.py.jinja +130 -0
- django_cfg/modules/django_client/core/generator/templates/python/app_init.py.jinja +6 -0
- django_cfg/modules/django_client/core/generator/templates/python/client/app_client.py.jinja +18 -0
- django_cfg/modules/django_client/core/generator/templates/python/client/flat_client.py.jinja +38 -0
- django_cfg/modules/django_client/core/generator/templates/python/client/main_client.py.jinja +50 -0
- django_cfg/modules/django_client/core/generator/templates/python/client/main_client_file.py.jinja +13 -0
- django_cfg/modules/django_client/core/generator/templates/python/client/operation_method.py.jinja +7 -0
- django_cfg/modules/django_client/core/generator/templates/python/client/sub_client.py.jinja +11 -0
- django_cfg/modules/django_client/core/generator/templates/python/client_file.py.jinja +13 -0
- django_cfg/modules/django_client/core/generator/templates/python/main_init.py.jinja +50 -0
- django_cfg/modules/django_client/core/generator/templates/python/models/app_models.py.jinja +17 -0
- django_cfg/modules/django_client/core/generator/templates/python/models/enum_class.py.jinja +15 -0
- django_cfg/modules/django_client/core/generator/templates/python/models/enums.py.jinja +8 -0
- django_cfg/modules/django_client/core/generator/templates/python/models/models.py.jinja +17 -0
- django_cfg/modules/django_client/core/generator/templates/python/models/schema_class.py.jinja +19 -0
- django_cfg/modules/django_client/core/generator/templates/python/utils/logger.py.jinja +255 -0
- django_cfg/modules/django_client/core/generator/templates/python/utils/schema.py.jinja +12 -0
- django_cfg/modules/django_client/core/generator/templates/typescript/app_index.ts.jinja +2 -0
- django_cfg/modules/django_client/core/generator/templates/typescript/client/app_client.ts.jinja +18 -0
- django_cfg/modules/django_client/core/generator/templates/typescript/client/client.ts.jinja +327 -0
- django_cfg/modules/django_client/core/generator/templates/typescript/client/flat_client.ts.jinja +109 -0
- django_cfg/modules/django_client/core/generator/templates/typescript/client/main_client_file.ts.jinja +9 -0
- django_cfg/modules/django_client/core/generator/templates/typescript/client/operation.ts.jinja +61 -0
- django_cfg/modules/django_client/core/generator/templates/typescript/client/sub_client.ts.jinja +15 -0
- django_cfg/modules/django_client/core/generator/templates/typescript/client_file.ts.jinja +9 -0
- django_cfg/modules/django_client/core/generator/templates/typescript/index.ts.jinja +5 -0
- django_cfg/modules/django_client/core/generator/templates/typescript/main_index.ts.jinja +206 -0
- django_cfg/modules/django_client/core/generator/templates/typescript/models/app_models.ts.jinja +8 -0
- django_cfg/modules/django_client/core/generator/templates/typescript/models/enums.ts.jinja +4 -0
- django_cfg/modules/django_client/core/generator/templates/typescript/models/models.ts.jinja +8 -0
- django_cfg/modules/django_client/core/generator/templates/typescript/utils/errors.ts.jinja +114 -0
- django_cfg/modules/django_client/core/generator/templates/typescript/utils/http.ts.jinja +98 -0
- django_cfg/modules/django_client/core/generator/templates/typescript/utils/logger.ts.jinja +251 -0
- django_cfg/modules/django_client/core/generator/templates/typescript/utils/schema.ts.jinja +7 -0
- django_cfg/modules/django_client/core/generator/templates/typescript/utils/storage.ts.jinja +114 -0
- django_cfg/modules/django_client/core/generator/typescript.py +872 -0
- django_cfg/modules/django_client/core/groups/__init__.py +13 -0
- django_cfg/modules/django_client/core/groups/detector.py +178 -0
- django_cfg/modules/django_client/core/groups/manager.py +314 -0
- django_cfg/modules/django_client/core/ir/__init__.py +57 -0
- django_cfg/modules/django_client/core/ir/context.py +387 -0
- django_cfg/modules/django_client/core/ir/operation.py +518 -0
- django_cfg/modules/django_client/core/ir/schema.py +353 -0
- django_cfg/modules/django_client/core/parser/__init__.py +74 -0
- django_cfg/modules/django_client/core/parser/base.py +648 -0
- django_cfg/modules/django_client/core/parser/models/__init__.py +74 -0
- django_cfg/modules/django_client/core/parser/models/base.py +212 -0
- django_cfg/modules/django_client/core/parser/models/components.py +160 -0
- django_cfg/modules/django_client/core/parser/models/openapi.py +203 -0
- django_cfg/modules/django_client/core/parser/models/operation.py +207 -0
- django_cfg/modules/django_client/core/parser/models/schema.py +266 -0
- django_cfg/modules/django_client/core/parser/openapi30.py +56 -0
- django_cfg/modules/django_client/core/parser/openapi31.py +64 -0
- django_cfg/modules/django_client/core/validation/__init__.py +22 -0
- django_cfg/modules/django_client/core/validation/checker.py +134 -0
- django_cfg/modules/django_client/core/validation/fixer.py +216 -0
- django_cfg/modules/django_client/core/validation/reporter.py +480 -0
- django_cfg/modules/django_client/core/validation/rules/__init__.py +11 -0
- django_cfg/modules/django_client/core/validation/rules/base.py +96 -0
- django_cfg/modules/django_client/core/validation/rules/type_hints.py +288 -0
- django_cfg/modules/django_client/core/validation/safety.py +266 -0
- django_cfg/modules/django_client/management/__init__.py +3 -0
- django_cfg/modules/django_client/management/commands/__init__.py +3 -0
- django_cfg/modules/django_client/management/commands/generate_client.py +422 -0
- django_cfg/modules/django_client/management/commands/validate_openapi.py +343 -0
- django_cfg/modules/django_client/spectacular/__init__.py +9 -0
- django_cfg/modules/django_client/spectacular/enum_naming.py +192 -0
- django_cfg/modules/django_client/urls.py +72 -0
- django_cfg/modules/django_email/management/__init__.py +0 -0
- django_cfg/modules/django_email/management/commands/__init__.py +0 -0
- django_cfg/modules/django_email/management/commands/test_email.py +93 -0
- django_cfg/modules/django_logging/django_logger.py +6 -6
- django_cfg/modules/django_ngrok/management/__init__.py +0 -0
- django_cfg/modules/django_ngrok/management/commands/__init__.py +0 -0
- django_cfg/modules/django_ngrok/management/commands/runserver_ngrok.py +167 -0
- django_cfg/modules/django_tasks/management/__init__.py +0 -0
- django_cfg/modules/django_tasks/management/commands/__init__.py +0 -0
- django_cfg/modules/django_tasks/management/commands/rundramatiq.py +254 -0
- django_cfg/modules/django_tasks/management/commands/rundramatiq_simulator.py +437 -0
- django_cfg/modules/django_tasks/management/commands/task_clear.py +226 -0
- django_cfg/modules/django_tasks/management/commands/task_status.py +257 -0
- django_cfg/modules/django_telegram/management/__init__.py +0 -0
- django_cfg/modules/django_telegram/management/commands/__init__.py +0 -0
- django_cfg/modules/django_telegram/management/commands/test_telegram.py +68 -0
- django_cfg/modules/django_twilio/management/__init__.py +0 -0
- django_cfg/modules/django_twilio/management/commands/__init__.py +0 -0
- django_cfg/modules/django_twilio/management/commands/test_twilio.py +112 -0
- django_cfg/modules/django_unfold/callbacks/main.py +16 -5
- django_cfg/modules/django_unfold/callbacks/revolution.py +41 -36
- django_cfg/pyproject.toml +2 -6
- django_cfg/registry/third_party.py +5 -7
- django_cfg/routing/callbacks.py +1 -1
- django_cfg/static/admin/css/prose-unfold.css +666 -0
- django_cfg/templates/admin/index.html +8 -0
- django_cfg/templates/admin/index_new.html +13 -0
- django_cfg/templates/admin/layouts/dashboard_with_tabs.html +15 -3
- django_cfg/templates/admin/sections/documentation_section.html +172 -0
- django_cfg/templates/admin/snippets/tabs/documentation_tab.html +231 -0
- {django_cfg-1.4.10.dist-info → django_cfg-1.4.11.dist-info}/METADATA +2 -2
- {django_cfg-1.4.10.dist-info → django_cfg-1.4.11.dist-info}/RECORD +180 -59
- django_cfg/management/commands/generate.py +0 -107
- /django_cfg/models/django/{revolution.py → revolution_legacy.py} +0 -0
- {django_cfg-1.4.10.dist-info → django_cfg-1.4.11.dist-info}/WHEEL +0 -0
- {django_cfg-1.4.10.dist-info → django_cfg-1.4.11.dist-info}/entry_points.txt +0 -0
- {django_cfg-1.4.10.dist-info → django_cfg-1.4.11.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,206 @@
|
|
1
|
+
/**
|
2
|
+
* {{ api_title }} - API Client with JWT Management
|
3
|
+
*
|
4
|
+
* Usage:
|
5
|
+
* ```typescript
|
6
|
+
* import { API } from './api';
|
7
|
+
*
|
8
|
+
* const api = new API('https://api.example.com');
|
9
|
+
*
|
10
|
+
* // Set JWT token
|
11
|
+
* api.setToken('your-jwt-token', 'refresh-token');
|
12
|
+
*
|
13
|
+
* // Use API
|
14
|
+
* const posts = await api.posts.list();
|
15
|
+
* const user = await api.users.retrieve(1);
|
16
|
+
*
|
17
|
+
* // Check authentication
|
18
|
+
* if (api.isAuthenticated()) {
|
19
|
+
* // ...
|
20
|
+
* }
|
21
|
+
*
|
22
|
+
* // Custom storage (for Electron/Node.js)
|
23
|
+
* import { MemoryStorageAdapter } from './storage';
|
24
|
+
* const api = new API('https://api.example.com', {
|
25
|
+
* storage: new MemoryStorageAdapter()
|
26
|
+
* });
|
27
|
+
*
|
28
|
+
* // Get OpenAPI schema
|
29
|
+
* const schema = api.getSchema();
|
30
|
+
* ```
|
31
|
+
*/
|
32
|
+
|
33
|
+
import { APIClient } from "./client";
|
34
|
+
import { OPENAPI_SCHEMA } from "./schema";
|
35
|
+
import {
|
36
|
+
StorageAdapter,
|
37
|
+
LocalStorageAdapter,
|
38
|
+
CookieStorageAdapter,
|
39
|
+
MemoryStorageAdapter
|
40
|
+
} from "./storage";
|
41
|
+
{% for tag in tags %}
|
42
|
+
export * as {{ tag.class_name }}Types from "./{{ tag.slug }}/models";
|
43
|
+
{% endfor %}
|
44
|
+
{% if has_enums %}
|
45
|
+
export * as Enums from "./enums";
|
46
|
+
{% endif %}
|
47
|
+
|
48
|
+
// Re-export storage adapters for convenience
|
49
|
+
export {
|
50
|
+
StorageAdapter,
|
51
|
+
LocalStorageAdapter,
|
52
|
+
CookieStorageAdapter,
|
53
|
+
MemoryStorageAdapter
|
54
|
+
};
|
55
|
+
|
56
|
+
export const TOKEN_KEY = "auth_token";
|
57
|
+
export const REFRESH_TOKEN_KEY = "refresh_token";
|
58
|
+
|
59
|
+
export interface APIOptions {
|
60
|
+
/** Custom storage adapter (defaults to LocalStorageAdapter) */
|
61
|
+
storage?: StorageAdapter;
|
62
|
+
}
|
63
|
+
|
64
|
+
export class API {
|
65
|
+
private baseUrl: string;
|
66
|
+
private _client!: APIClient;
|
67
|
+
private _token: string | null = null;
|
68
|
+
private _refreshToken: string | null = null;
|
69
|
+
private storage: StorageAdapter;
|
70
|
+
|
71
|
+
// Sub-clients
|
72
|
+
{% for tag in tags %}
|
73
|
+
public {{ tag.property }}!: APIClient['{{ tag.property }}'];
|
74
|
+
{% endfor %}
|
75
|
+
|
76
|
+
constructor(baseUrl: string, options?: APIOptions) {
|
77
|
+
this.baseUrl = baseUrl;
|
78
|
+
this.storage = options?.storage || new LocalStorageAdapter();
|
79
|
+
this._loadTokensFromStorage();
|
80
|
+
this._initClients();
|
81
|
+
}
|
82
|
+
|
83
|
+
private _loadTokensFromStorage(): void {
|
84
|
+
this._token = this.storage.getItem(TOKEN_KEY);
|
85
|
+
this._refreshToken = this.storage.getItem(REFRESH_TOKEN_KEY);
|
86
|
+
}
|
87
|
+
|
88
|
+
private _initClients(): void {
|
89
|
+
this._client = new APIClient(this.baseUrl);
|
90
|
+
|
91
|
+
// Inject Authorization header if token exists
|
92
|
+
if (this._token) {
|
93
|
+
this._injectAuthHeader();
|
94
|
+
}
|
95
|
+
|
96
|
+
// Proxy sub-clients
|
97
|
+
{% for tag in tags %}
|
98
|
+
this.{{ tag.property }} = this._client.{{ tag.property }};
|
99
|
+
{% endfor %}
|
100
|
+
}
|
101
|
+
|
102
|
+
private _injectAuthHeader(): void {
|
103
|
+
// Override request method to inject auth header
|
104
|
+
const originalRequest = this._client.request.bind(this._client);
|
105
|
+
this._client.request = async <T>(
|
106
|
+
method: string,
|
107
|
+
path: string,
|
108
|
+
options?: { params?: Record<string, any>; body?: any }
|
109
|
+
): Promise<T> => {
|
110
|
+
const headers: Record<string, string> = {};
|
111
|
+
|
112
|
+
if (this._token) {
|
113
|
+
headers['Authorization'] = `Bearer ${this._token}`;
|
114
|
+
}
|
115
|
+
|
116
|
+
// Merge with existing options
|
117
|
+
const mergedOptions = {
|
118
|
+
...options,
|
119
|
+
headers: {
|
120
|
+
...(options as any)?.headers,
|
121
|
+
...headers,
|
122
|
+
},
|
123
|
+
};
|
124
|
+
|
125
|
+
return originalRequest(method, path, mergedOptions);
|
126
|
+
};
|
127
|
+
}
|
128
|
+
|
129
|
+
/**
|
130
|
+
* Get current JWT token
|
131
|
+
*/
|
132
|
+
getToken(): string | null {
|
133
|
+
return this.storage.getItem(TOKEN_KEY);
|
134
|
+
}
|
135
|
+
|
136
|
+
/**
|
137
|
+
* Get current refresh token
|
138
|
+
*/
|
139
|
+
getRefreshToken(): string | null {
|
140
|
+
return this.storage.getItem(REFRESH_TOKEN_KEY);
|
141
|
+
}
|
142
|
+
|
143
|
+
/**
|
144
|
+
* Set JWT token and refresh token
|
145
|
+
* @param token - JWT access token
|
146
|
+
* @param refreshToken - JWT refresh token (optional)
|
147
|
+
*/
|
148
|
+
setToken(token: string, refreshToken?: string): void {
|
149
|
+
this._token = token;
|
150
|
+
this.storage.setItem(TOKEN_KEY, token);
|
151
|
+
|
152
|
+
if (refreshToken) {
|
153
|
+
this._refreshToken = refreshToken;
|
154
|
+
this.storage.setItem(REFRESH_TOKEN_KEY, refreshToken);
|
155
|
+
}
|
156
|
+
|
157
|
+
// Reinitialize clients with new token
|
158
|
+
this._initClients();
|
159
|
+
}
|
160
|
+
|
161
|
+
/**
|
162
|
+
* Clear all tokens
|
163
|
+
*/
|
164
|
+
clearTokens(): void {
|
165
|
+
this._token = null;
|
166
|
+
this._refreshToken = null;
|
167
|
+
this.storage.removeItem(TOKEN_KEY);
|
168
|
+
this.storage.removeItem(REFRESH_TOKEN_KEY);
|
169
|
+
|
170
|
+
// Reinitialize clients without token
|
171
|
+
this._initClients();
|
172
|
+
}
|
173
|
+
|
174
|
+
/**
|
175
|
+
* Check if user is authenticated
|
176
|
+
*/
|
177
|
+
isAuthenticated(): boolean {
|
178
|
+
return !!this.getToken();
|
179
|
+
}
|
180
|
+
|
181
|
+
/**
|
182
|
+
* Update base URL and reinitialize clients
|
183
|
+
* @param url - New base URL
|
184
|
+
*/
|
185
|
+
setBaseUrl(url: string): void {
|
186
|
+
this.baseUrl = url;
|
187
|
+
this._initClients();
|
188
|
+
}
|
189
|
+
|
190
|
+
/**
|
191
|
+
* Get current base URL
|
192
|
+
*/
|
193
|
+
getBaseUrl(): string {
|
194
|
+
return this.baseUrl;
|
195
|
+
}
|
196
|
+
|
197
|
+
/**
|
198
|
+
* Get OpenAPI schema
|
199
|
+
* @returns Complete OpenAPI specification for this API
|
200
|
+
*/
|
201
|
+
getSchema(): any {
|
202
|
+
return OPENAPI_SCHEMA;
|
203
|
+
}
|
204
|
+
}
|
205
|
+
|
206
|
+
export default API;
|
@@ -0,0 +1,114 @@
|
|
1
|
+
/**
|
2
|
+
* API Error Classes
|
3
|
+
*
|
4
|
+
* Typed error classes with Django REST Framework support.
|
5
|
+
*/
|
6
|
+
|
7
|
+
/**
|
8
|
+
* HTTP API Error with DRF field-specific validation errors.
|
9
|
+
*
|
10
|
+
* Usage:
|
11
|
+
* ```typescript
|
12
|
+
* try {
|
13
|
+
* await api.users.create(userData);
|
14
|
+
* } catch (error) {
|
15
|
+
* if (error instanceof APIError) {
|
16
|
+
* if (error.isValidationError) {
|
17
|
+
* console.log('Field errors:', error.fieldErrors);
|
18
|
+
* // { "email": ["Email already exists"], "username": ["Required"] }
|
19
|
+
* }
|
20
|
+
* }
|
21
|
+
* }
|
22
|
+
* ```
|
23
|
+
*/
|
24
|
+
export class APIError extends Error {
|
25
|
+
constructor(
|
26
|
+
public statusCode: number,
|
27
|
+
public statusText: string,
|
28
|
+
public response: any,
|
29
|
+
public url: string,
|
30
|
+
message?: string
|
31
|
+
) {
|
32
|
+
super(message || `HTTP ${statusCode}: ${statusText}`);
|
33
|
+
this.name = 'APIError';
|
34
|
+
}
|
35
|
+
|
36
|
+
/**
|
37
|
+
* Get error details from response.
|
38
|
+
* DRF typically returns: { "detail": "Error message" } or { "field": ["error1", "error2"] }
|
39
|
+
*/
|
40
|
+
get details(): Record<string, any> | null {
|
41
|
+
if (typeof this.response === 'object' && this.response !== null) {
|
42
|
+
return this.response;
|
43
|
+
}
|
44
|
+
return null;
|
45
|
+
}
|
46
|
+
|
47
|
+
/**
|
48
|
+
* Get field-specific validation errors from DRF.
|
49
|
+
* Returns: { "field_name": ["error1", "error2"], ... }
|
50
|
+
*/
|
51
|
+
get fieldErrors(): Record<string, string[]> | null {
|
52
|
+
const details = this.details;
|
53
|
+
if (!details) return null;
|
54
|
+
|
55
|
+
// DRF typically returns: { "field": ["error1", "error2"] }
|
56
|
+
const fieldErrors: Record<string, string[]> = {};
|
57
|
+
for (const [key, value] of Object.entries(details)) {
|
58
|
+
if (Array.isArray(value)) {
|
59
|
+
fieldErrors[key] = value;
|
60
|
+
}
|
61
|
+
}
|
62
|
+
|
63
|
+
return Object.keys(fieldErrors).length > 0 ? fieldErrors : null;
|
64
|
+
}
|
65
|
+
|
66
|
+
/**
|
67
|
+
* Get single error message from DRF.
|
68
|
+
* Checks for "detail", "message", or first field error.
|
69
|
+
*/
|
70
|
+
get errorMessage(): string {
|
71
|
+
const details = this.details;
|
72
|
+
if (!details) return this.message;
|
73
|
+
|
74
|
+
// Check for "detail" field (common in DRF)
|
75
|
+
if (details.detail) {
|
76
|
+
return Array.isArray(details.detail) ? details.detail.join(', ') : String(details.detail);
|
77
|
+
}
|
78
|
+
|
79
|
+
// Check for "message" field
|
80
|
+
if (details.message) {
|
81
|
+
return String(details.message);
|
82
|
+
}
|
83
|
+
|
84
|
+
// Return first field error
|
85
|
+
const fieldErrors = this.fieldErrors;
|
86
|
+
if (fieldErrors) {
|
87
|
+
const firstField = Object.keys(fieldErrors)[0];
|
88
|
+
return `${firstField}: ${fieldErrors[firstField].join(', ')}`;
|
89
|
+
}
|
90
|
+
|
91
|
+
return this.message;
|
92
|
+
}
|
93
|
+
|
94
|
+
// Helper methods for common HTTP status codes
|
95
|
+
get isValidationError(): boolean { return this.statusCode === 400; }
|
96
|
+
get isAuthError(): boolean { return this.statusCode === 401; }
|
97
|
+
get isPermissionError(): boolean { return this.statusCode === 403; }
|
98
|
+
get isNotFoundError(): boolean { return this.statusCode === 404; }
|
99
|
+
get isServerError(): boolean { return this.statusCode >= 500 && this.statusCode < 600; }
|
100
|
+
}
|
101
|
+
|
102
|
+
/**
|
103
|
+
* Network Error (connection failed, timeout, etc.)
|
104
|
+
*/
|
105
|
+
export class NetworkError extends Error {
|
106
|
+
constructor(
|
107
|
+
message: string,
|
108
|
+
public url: string,
|
109
|
+
public originalError?: Error
|
110
|
+
) {
|
111
|
+
super(message);
|
112
|
+
this.name = 'NetworkError';
|
113
|
+
}
|
114
|
+
}
|
@@ -0,0 +1,98 @@
|
|
1
|
+
/**
|
2
|
+
* HTTP Client Adapter Pattern
|
3
|
+
*
|
4
|
+
* Allows switching between fetch/axios/httpx without changing generated code.
|
5
|
+
* Provides unified interface for making HTTP requests.
|
6
|
+
*/
|
7
|
+
|
8
|
+
export interface HttpRequest {
|
9
|
+
method: string;
|
10
|
+
url: string;
|
11
|
+
headers?: Record<string, string>;
|
12
|
+
body?: any;
|
13
|
+
params?: Record<string, any>;
|
14
|
+
/** FormData for file uploads (multipart/form-data) */
|
15
|
+
formData?: FormData;
|
16
|
+
}
|
17
|
+
|
18
|
+
export interface HttpResponse<T = any> {
|
19
|
+
data: T;
|
20
|
+
status: number;
|
21
|
+
statusText: string;
|
22
|
+
headers: Record<string, string>;
|
23
|
+
}
|
24
|
+
|
25
|
+
/**
|
26
|
+
* HTTP Client Adapter Interface.
|
27
|
+
* Implement this to use custom HTTP clients (axios, httpx, etc.)
|
28
|
+
*/
|
29
|
+
export interface HttpClientAdapter {
|
30
|
+
request<T = any>(request: HttpRequest): Promise<HttpResponse<T>>;
|
31
|
+
}
|
32
|
+
|
33
|
+
/**
|
34
|
+
* Default Fetch API adapter.
|
35
|
+
* Uses native browser fetch() with proper error handling.
|
36
|
+
*/
|
37
|
+
export class FetchAdapter implements HttpClientAdapter {
|
38
|
+
async request<T = any>(request: HttpRequest): Promise<HttpResponse<T>> {
|
39
|
+
const { method, url, headers, body, params, formData } = request;
|
40
|
+
|
41
|
+
// Build URL with query params
|
42
|
+
const finalUrl = new URL(url);
|
43
|
+
if (params) {
|
44
|
+
Object.entries(params).forEach(([key, value]) => {
|
45
|
+
if (value !== null && value !== undefined) {
|
46
|
+
finalUrl.searchParams.append(key, String(value));
|
47
|
+
}
|
48
|
+
});
|
49
|
+
}
|
50
|
+
|
51
|
+
// Build headers
|
52
|
+
const finalHeaders: Record<string, string> = { ...headers };
|
53
|
+
|
54
|
+
// Determine body and content-type
|
55
|
+
let requestBody: string | FormData | undefined;
|
56
|
+
|
57
|
+
if (formData) {
|
58
|
+
// For multipart/form-data, let browser set Content-Type with boundary
|
59
|
+
requestBody = formData;
|
60
|
+
// Don't set Content-Type - browser will set it with boundary
|
61
|
+
} else if (body) {
|
62
|
+
// JSON request
|
63
|
+
finalHeaders['Content-Type'] = 'application/json';
|
64
|
+
requestBody = JSON.stringify(body);
|
65
|
+
}
|
66
|
+
|
67
|
+
// Make request
|
68
|
+
const response = await fetch(finalUrl.toString(), {
|
69
|
+
method,
|
70
|
+
headers: finalHeaders,
|
71
|
+
body: requestBody,
|
72
|
+
credentials: 'include', // Include Django session cookies
|
73
|
+
});
|
74
|
+
|
75
|
+
// Parse response
|
76
|
+
let data: any = null;
|
77
|
+
const contentType = response.headers.get('content-type');
|
78
|
+
|
79
|
+
if (response.status !== 204 && contentType?.includes('application/json')) {
|
80
|
+
data = await response.json();
|
81
|
+
} else if (response.status !== 204) {
|
82
|
+
data = await response.text();
|
83
|
+
}
|
84
|
+
|
85
|
+
// Convert Headers to plain object
|
86
|
+
const responseHeaders: Record<string, string> = {};
|
87
|
+
response.headers.forEach((value, key) => {
|
88
|
+
responseHeaders[key] = value;
|
89
|
+
});
|
90
|
+
|
91
|
+
return {
|
92
|
+
data,
|
93
|
+
status: response.status,
|
94
|
+
statusText: response.statusText,
|
95
|
+
headers: responseHeaders,
|
96
|
+
};
|
97
|
+
}
|
98
|
+
}
|
@@ -0,0 +1,251 @@
|
|
1
|
+
/**
|
2
|
+
* API Logger with Consola
|
3
|
+
* Beautiful console logging for API requests and responses
|
4
|
+
*
|
5
|
+
* Installation:
|
6
|
+
* npm install consola
|
7
|
+
*/
|
8
|
+
|
9
|
+
import { type ConsolaInstance, createConsola } from 'consola';
|
10
|
+
|
11
|
+
/**
|
12
|
+
* Request log data
|
13
|
+
*/
|
14
|
+
export interface RequestLog {
|
15
|
+
method: string;
|
16
|
+
url: string;
|
17
|
+
headers?: Record<string, string>;
|
18
|
+
body?: any;
|
19
|
+
timestamp: number;
|
20
|
+
}
|
21
|
+
|
22
|
+
/**
|
23
|
+
* Response log data
|
24
|
+
*/
|
25
|
+
export interface ResponseLog {
|
26
|
+
status: number;
|
27
|
+
statusText: string;
|
28
|
+
data?: any;
|
29
|
+
duration: number;
|
30
|
+
timestamp: number;
|
31
|
+
}
|
32
|
+
|
33
|
+
/**
|
34
|
+
* Error log data
|
35
|
+
*/
|
36
|
+
export interface ErrorLog {
|
37
|
+
message: string;
|
38
|
+
statusCode?: number;
|
39
|
+
fieldErrors?: Record<string, string[]>;
|
40
|
+
duration: number;
|
41
|
+
timestamp: number;
|
42
|
+
}
|
43
|
+
|
44
|
+
/**
|
45
|
+
* Logger configuration
|
46
|
+
*/
|
47
|
+
export interface LoggerConfig {
|
48
|
+
/** Enable logging */
|
49
|
+
enabled: boolean;
|
50
|
+
/** Log requests */
|
51
|
+
logRequests: boolean;
|
52
|
+
/** Log responses */
|
53
|
+
logResponses: boolean;
|
54
|
+
/** Log errors */
|
55
|
+
logErrors: boolean;
|
56
|
+
/** Log request/response bodies */
|
57
|
+
logBodies: boolean;
|
58
|
+
/** Log headers (excluding sensitive ones) */
|
59
|
+
logHeaders: boolean;
|
60
|
+
/** Custom consola instance */
|
61
|
+
consola?: ConsolaInstance;
|
62
|
+
}
|
63
|
+
|
64
|
+
/**
|
65
|
+
* Default logger configuration
|
66
|
+
*/
|
67
|
+
const DEFAULT_CONFIG: LoggerConfig = {
|
68
|
+
enabled: process.env.NODE_ENV !== 'production',
|
69
|
+
logRequests: true,
|
70
|
+
logResponses: true,
|
71
|
+
logErrors: true,
|
72
|
+
logBodies: true,
|
73
|
+
logHeaders: false,
|
74
|
+
};
|
75
|
+
|
76
|
+
/**
|
77
|
+
* Sensitive header names to filter out
|
78
|
+
*/
|
79
|
+
const SENSITIVE_HEADERS = [
|
80
|
+
'authorization',
|
81
|
+
'cookie',
|
82
|
+
'set-cookie',
|
83
|
+
'x-api-key',
|
84
|
+
'x-csrf-token',
|
85
|
+
];
|
86
|
+
|
87
|
+
/**
|
88
|
+
* API Logger class
|
89
|
+
*/
|
90
|
+
export class APILogger {
|
91
|
+
private config: LoggerConfig;
|
92
|
+
private consola: ConsolaInstance;
|
93
|
+
|
94
|
+
constructor(config: Partial<LoggerConfig> = {}) {
|
95
|
+
this.config = { ...DEFAULT_CONFIG, ...config };
|
96
|
+
this.consola = config.consola || createConsola({
|
97
|
+
level: this.config.enabled ? 4 : 0,
|
98
|
+
});
|
99
|
+
}
|
100
|
+
|
101
|
+
/**
|
102
|
+
* Enable logging
|
103
|
+
*/
|
104
|
+
enable(): void {
|
105
|
+
this.config.enabled = true;
|
106
|
+
}
|
107
|
+
|
108
|
+
/**
|
109
|
+
* Disable logging
|
110
|
+
*/
|
111
|
+
disable(): void {
|
112
|
+
this.config.enabled = false;
|
113
|
+
}
|
114
|
+
|
115
|
+
/**
|
116
|
+
* Update configuration
|
117
|
+
*/
|
118
|
+
setConfig(config: Partial<LoggerConfig>): void {
|
119
|
+
this.config = { ...this.config, ...config };
|
120
|
+
}
|
121
|
+
|
122
|
+
/**
|
123
|
+
* Filter sensitive headers
|
124
|
+
*/
|
125
|
+
private filterHeaders(headers?: Record<string, string>): Record<string, string> {
|
126
|
+
if (!headers) return {};
|
127
|
+
|
128
|
+
const filtered: Record<string, string> = {};
|
129
|
+
Object.keys(headers).forEach((key) => {
|
130
|
+
const lowerKey = key.toLowerCase();
|
131
|
+
if (SENSITIVE_HEADERS.includes(lowerKey)) {
|
132
|
+
filtered[key] = '***';
|
133
|
+
} else {
|
134
|
+
filtered[key] = headers[key];
|
135
|
+
}
|
136
|
+
});
|
137
|
+
|
138
|
+
return filtered;
|
139
|
+
}
|
140
|
+
|
141
|
+
/**
|
142
|
+
* Log request
|
143
|
+
*/
|
144
|
+
logRequest(request: RequestLog): void {
|
145
|
+
if (!this.config.enabled || !this.config.logRequests) return;
|
146
|
+
|
147
|
+
const { method, url, headers, body } = request;
|
148
|
+
|
149
|
+
this.consola.start(`${method} ${url}`);
|
150
|
+
|
151
|
+
if (this.config.logHeaders && headers) {
|
152
|
+
this.consola.debug('Headers:', this.filterHeaders(headers));
|
153
|
+
}
|
154
|
+
|
155
|
+
if (this.config.logBodies && body) {
|
156
|
+
this.consola.debug('Body:', body);
|
157
|
+
}
|
158
|
+
}
|
159
|
+
|
160
|
+
/**
|
161
|
+
* Log response
|
162
|
+
*/
|
163
|
+
logResponse(request: RequestLog, response: ResponseLog): void {
|
164
|
+
if (!this.config.enabled || !this.config.logResponses) return;
|
165
|
+
|
166
|
+
const { method, url } = request;
|
167
|
+
const { status, statusText, data, duration } = response;
|
168
|
+
|
169
|
+
const statusColor = status >= 500 ? 'red'
|
170
|
+
: status >= 400 ? 'yellow'
|
171
|
+
: status >= 300 ? 'cyan'
|
172
|
+
: 'green';
|
173
|
+
|
174
|
+
this.consola.success(
|
175
|
+
`${method} ${url} ${status} ${statusText} (${duration}ms)`
|
176
|
+
);
|
177
|
+
|
178
|
+
if (this.config.logBodies && data) {
|
179
|
+
this.consola.debug('Response:', data);
|
180
|
+
}
|
181
|
+
}
|
182
|
+
|
183
|
+
/**
|
184
|
+
* Log error
|
185
|
+
*/
|
186
|
+
logError(request: RequestLog, error: ErrorLog): void {
|
187
|
+
if (!this.config.enabled || !this.config.logErrors) return;
|
188
|
+
|
189
|
+
const { method, url } = request;
|
190
|
+
const { message, statusCode, fieldErrors, duration } = error;
|
191
|
+
|
192
|
+
this.consola.error(
|
193
|
+
`${method} ${url} ${statusCode || 'Network'} Error (${duration}ms)`
|
194
|
+
);
|
195
|
+
|
196
|
+
this.consola.error('Message:', message);
|
197
|
+
|
198
|
+
if (fieldErrors && Object.keys(fieldErrors).length > 0) {
|
199
|
+
this.consola.error('Field Errors:');
|
200
|
+
Object.entries(fieldErrors).forEach(([field, errors]) => {
|
201
|
+
errors.forEach((err) => {
|
202
|
+
this.consola.error(` • ${field}: ${err}`);
|
203
|
+
});
|
204
|
+
});
|
205
|
+
}
|
206
|
+
}
|
207
|
+
|
208
|
+
/**
|
209
|
+
* Log general info
|
210
|
+
*/
|
211
|
+
info(message: string, ...args: any[]): void {
|
212
|
+
if (!this.config.enabled) return;
|
213
|
+
this.consola.info(message, ...args);
|
214
|
+
}
|
215
|
+
|
216
|
+
/**
|
217
|
+
* Log warning
|
218
|
+
*/
|
219
|
+
warn(message: string, ...args: any[]): void {
|
220
|
+
if (!this.config.enabled) return;
|
221
|
+
this.consola.warn(message, ...args);
|
222
|
+
}
|
223
|
+
|
224
|
+
/**
|
225
|
+
* Log debug
|
226
|
+
*/
|
227
|
+
debug(message: string, ...args: any[]): void {
|
228
|
+
if (!this.config.enabled) return;
|
229
|
+
this.consola.debug(message, ...args);
|
230
|
+
}
|
231
|
+
|
232
|
+
/**
|
233
|
+
* Log success
|
234
|
+
*/
|
235
|
+
success(message: string, ...args: any[]): void {
|
236
|
+
if (!this.config.enabled) return;
|
237
|
+
this.consola.success(message, ...args);
|
238
|
+
}
|
239
|
+
|
240
|
+
/**
|
241
|
+
* Create a sub-logger with prefix
|
242
|
+
*/
|
243
|
+
withTag(tag: string): ConsolaInstance {
|
244
|
+
return this.consola.withTag(tag);
|
245
|
+
}
|
246
|
+
}
|
247
|
+
|
248
|
+
/**
|
249
|
+
* Default logger instance
|
250
|
+
*/
|
251
|
+
export const defaultLogger = new APILogger();
|