@djangocfg/ext-newsletter 1.0.9 → 1.0.10
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/dist/config.cjs +1 -1
- package/dist/config.js +1 -1
- package/dist/hooks.cjs +46 -11
- package/dist/hooks.js +47 -12
- package/dist/index.cjs +46 -11
- package/dist/index.d.cts +40 -17
- package/dist/index.d.ts +40 -17
- package/dist/index.js +47 -12
- package/package.json +5 -5
- package/src/api/generated/ext_newsletter/CLAUDE.md +1 -8
- package/src/api/generated/ext_newsletter/_utils/schemas/BulkEmailRequest.schema.ts +1 -1
- package/src/api/generated/ext_newsletter/_utils/schemas/NewsletterCampaign.schema.ts +1 -1
- package/src/api/generated/ext_newsletter/_utils/schemas/NewsletterCampaignRequest.schema.ts +1 -1
- package/src/api/generated/ext_newsletter/_utils/schemas/PatchedNewsletterCampaignRequest.schema.ts +1 -1
- package/src/api/generated/ext_newsletter/api-instance.ts +61 -13
- package/src/api/generated/ext_newsletter/client.ts +23 -2
- package/src/api/generated/ext_newsletter/http.ts +8 -2
- package/src/api/generated/ext_newsletter/index.ts +3 -1
- package/src/api/index.ts +6 -1
|
@@ -1,29 +1,37 @@
|
|
|
1
1
|
// Auto-generated by DjangoCFG - see CLAUDE.md
|
|
2
2
|
/**
|
|
3
|
-
* Global API Instance - Singleton configuration
|
|
3
|
+
* Global API Instance - Singleton configuration with auto-configuration support
|
|
4
4
|
*
|
|
5
|
-
* This module provides a global API instance that
|
|
6
|
-
*
|
|
5
|
+
* This module provides a global API instance that auto-configures from
|
|
6
|
+
* environment variables or can be configured manually.
|
|
7
7
|
*
|
|
8
|
-
*
|
|
8
|
+
* AUTO-CONFIGURATION (recommended):
|
|
9
|
+
* Set one of these environment variables and the API will auto-configure:
|
|
10
|
+
* - NEXT_PUBLIC_API_URL (Next.js)
|
|
11
|
+
* - VITE_API_URL (Vite)
|
|
12
|
+
* - REACT_APP_API_URL (Create React App)
|
|
13
|
+
* - API_URL (generic)
|
|
14
|
+
*
|
|
15
|
+
* Then just use fetchers and hooks directly:
|
|
16
|
+
* ```typescript
|
|
17
|
+
* import { getUsers } from './_utils/fetchers'
|
|
18
|
+
* const users = await getUsers({ page: 1 })
|
|
19
|
+
* ```
|
|
20
|
+
*
|
|
21
|
+
* MANUAL CONFIGURATION:
|
|
9
22
|
* ```typescript
|
|
10
|
-
* // Configure once (e.g., in your app entry point)
|
|
11
23
|
* import { configureAPI } from './api-instance'
|
|
12
24
|
*
|
|
13
25
|
* configureAPI({
|
|
14
26
|
* baseUrl: 'https://api.example.com',
|
|
15
27
|
* token: 'your-jwt-token'
|
|
16
28
|
* })
|
|
17
|
-
*
|
|
18
|
-
* // Then use fetchers and hooks anywhere without configuration
|
|
19
|
-
* import { getUsers } from './fetchers'
|
|
20
|
-
* const users = await getUsers({ page: 1 })
|
|
21
29
|
* ```
|
|
22
30
|
*
|
|
23
31
|
* For SSR or multiple instances:
|
|
24
32
|
* ```typescript
|
|
25
33
|
* import { API } from './index'
|
|
26
|
-
* import { getUsers } from './fetchers'
|
|
34
|
+
* import { getUsers } from './_utils/fetchers'
|
|
27
35
|
*
|
|
28
36
|
* const api = new API('https://api.example.com')
|
|
29
37
|
* const users = await getUsers({ page: 1 }, api)
|
|
@@ -33,27 +41,67 @@
|
|
|
33
41
|
import { API, type APIOptions } from './index'
|
|
34
42
|
|
|
35
43
|
let globalAPI: API | null = null
|
|
44
|
+
let autoConfigAttempted = false
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Auto-configure from environment variable if available (Next.js pattern)
|
|
48
|
+
* This allows hooks and fetchers to work without explicit configureAPI() call
|
|
49
|
+
*
|
|
50
|
+
* Supported environment variables:
|
|
51
|
+
* - NEXT_PUBLIC_API_URL (Next.js)
|
|
52
|
+
* - VITE_API_URL (Vite)
|
|
53
|
+
* - REACT_APP_API_URL (Create React App)
|
|
54
|
+
* - API_URL (generic)
|
|
55
|
+
*/
|
|
56
|
+
function tryAutoConfigureFromEnv(): void {
|
|
57
|
+
// Only attempt once
|
|
58
|
+
if (autoConfigAttempted) return
|
|
59
|
+
autoConfigAttempted = true
|
|
60
|
+
|
|
61
|
+
// Skip if already configured
|
|
62
|
+
if (globalAPI) return
|
|
63
|
+
|
|
64
|
+
// Skip if process is not available (pure browser without bundler)
|
|
65
|
+
if (typeof process === 'undefined' || !process.env) return
|
|
66
|
+
|
|
67
|
+
// Try different environment variable patterns
|
|
68
|
+
const baseUrl =
|
|
69
|
+
process.env.NEXT_PUBLIC_API_URL ||
|
|
70
|
+
process.env.VITE_API_URL ||
|
|
71
|
+
process.env.REACT_APP_API_URL ||
|
|
72
|
+
process.env.API_URL
|
|
73
|
+
|
|
74
|
+
if (baseUrl) {
|
|
75
|
+
globalAPI = new API(baseUrl)
|
|
76
|
+
}
|
|
77
|
+
}
|
|
36
78
|
|
|
37
79
|
/**
|
|
38
80
|
* Get the global API instance
|
|
39
|
-
*
|
|
81
|
+
* Auto-configures from environment variables on first call if not manually configured.
|
|
82
|
+
* @throws Error if API is not configured and no env variable is set
|
|
40
83
|
*/
|
|
41
84
|
export function getAPIInstance(): API {
|
|
85
|
+
// Try auto-configuration on first access (lazy initialization)
|
|
86
|
+
tryAutoConfigureFromEnv()
|
|
87
|
+
|
|
42
88
|
if (!globalAPI) {
|
|
43
89
|
throw new Error(
|
|
44
90
|
'API not configured. Call configureAPI() with your base URL before using fetchers or hooks.\n\n' +
|
|
45
91
|
'Example:\n' +
|
|
46
92
|
' import { configureAPI } from "./api-instance"\n' +
|
|
47
|
-
' configureAPI({ baseUrl: "https://api.example.com" })'
|
|
93
|
+
' configureAPI({ baseUrl: "https://api.example.com" })\n\n' +
|
|
94
|
+
'Or set environment variable: NEXT_PUBLIC_API_URL, VITE_API_URL, or REACT_APP_API_URL'
|
|
48
95
|
)
|
|
49
96
|
}
|
|
50
97
|
return globalAPI
|
|
51
98
|
}
|
|
52
99
|
|
|
53
100
|
/**
|
|
54
|
-
* Check if API is configured
|
|
101
|
+
* Check if API is configured (or can be auto-configured)
|
|
55
102
|
*/
|
|
56
103
|
export function isAPIConfigured(): boolean {
|
|
104
|
+
tryAutoConfigureFromEnv()
|
|
57
105
|
return globalAPI !== null
|
|
58
106
|
}
|
|
59
107
|
|
|
@@ -31,6 +31,7 @@ export class APIClient {
|
|
|
31
31
|
private httpClient: HttpClientAdapter;
|
|
32
32
|
private logger: APILogger | null = null;
|
|
33
33
|
private retryConfig: RetryConfig | null = null;
|
|
34
|
+
private tokenGetter: (() => string | null) | null = null;
|
|
34
35
|
|
|
35
36
|
// Sub-clients
|
|
36
37
|
public ext_newsletter_bulk_email: ExtNewsletterBulkEmail;
|
|
@@ -47,10 +48,12 @@ export class APIClient {
|
|
|
47
48
|
httpClient?: HttpClientAdapter;
|
|
48
49
|
loggerConfig?: Partial<LoggerConfig>;
|
|
49
50
|
retryConfig?: RetryConfig;
|
|
51
|
+
tokenGetter?: () => string | null;
|
|
50
52
|
}
|
|
51
53
|
) {
|
|
52
54
|
this.baseUrl = baseUrl.replace(/\/$/, '');
|
|
53
55
|
this.httpClient = options?.httpClient || new FetchAdapter();
|
|
56
|
+
this.tokenGetter = options?.tokenGetter || null;
|
|
54
57
|
|
|
55
58
|
// Initialize logger if config provided
|
|
56
59
|
if (options?.loggerConfig !== undefined) {
|
|
@@ -87,6 +90,21 @@ export class APIClient {
|
|
|
87
90
|
return null;
|
|
88
91
|
}
|
|
89
92
|
|
|
93
|
+
/**
|
|
94
|
+
* Get the base URL for building streaming/download URLs.
|
|
95
|
+
*/
|
|
96
|
+
getBaseUrl(): string {
|
|
97
|
+
return this.baseUrl;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Get JWT token for URL authentication (used in streaming endpoints).
|
|
102
|
+
* Returns null if no token getter is configured or no token is available.
|
|
103
|
+
*/
|
|
104
|
+
getToken(): string | null {
|
|
105
|
+
return this.tokenGetter ? this.tokenGetter() : null;
|
|
106
|
+
}
|
|
107
|
+
|
|
90
108
|
/**
|
|
91
109
|
* Make HTTP request with Django CSRF and session handling.
|
|
92
110
|
* Automatically retries on network errors and 5xx server errors.
|
|
@@ -98,6 +116,7 @@ export class APIClient {
|
|
|
98
116
|
params?: Record<string, any>;
|
|
99
117
|
body?: any;
|
|
100
118
|
formData?: FormData;
|
|
119
|
+
binaryBody?: Blob | ArrayBuffer;
|
|
101
120
|
headers?: Record<string, string>;
|
|
102
121
|
}
|
|
103
122
|
): Promise<T> {
|
|
@@ -134,6 +153,7 @@ export class APIClient {
|
|
|
134
153
|
params?: Record<string, any>;
|
|
135
154
|
body?: any;
|
|
136
155
|
formData?: FormData;
|
|
156
|
+
binaryBody?: Blob | ArrayBuffer;
|
|
137
157
|
headers?: Record<string, string>;
|
|
138
158
|
}
|
|
139
159
|
): Promise<T> {
|
|
@@ -147,8 +167,8 @@ export class APIClient {
|
|
|
147
167
|
...(options?.headers || {})
|
|
148
168
|
};
|
|
149
169
|
|
|
150
|
-
// Don't set Content-Type for FormData (browser will set it with boundary)
|
|
151
|
-
if (!options?.formData && !headers['Content-Type']) {
|
|
170
|
+
// Don't set Content-Type for FormData/binaryBody (browser will set it with boundary)
|
|
171
|
+
if (!options?.formData && !options?.binaryBody && !headers['Content-Type']) {
|
|
152
172
|
headers['Content-Type'] = 'application/json';
|
|
153
173
|
}
|
|
154
174
|
|
|
@@ -175,6 +195,7 @@ export class APIClient {
|
|
|
175
195
|
params: options?.params,
|
|
176
196
|
body: options?.body,
|
|
177
197
|
formData: options?.formData,
|
|
198
|
+
binaryBody: options?.binaryBody,
|
|
178
199
|
});
|
|
179
200
|
|
|
180
201
|
const duration = Date.now() - startTime;
|
|
@@ -14,6 +14,8 @@ export interface HttpRequest {
|
|
|
14
14
|
params?: Record<string, any>;
|
|
15
15
|
/** FormData for file uploads (multipart/form-data) */
|
|
16
16
|
formData?: FormData;
|
|
17
|
+
/** Binary data for octet-stream uploads */
|
|
18
|
+
binaryBody?: Blob | ArrayBuffer;
|
|
17
19
|
}
|
|
18
20
|
|
|
19
21
|
export interface HttpResponse<T = any> {
|
|
@@ -37,7 +39,7 @@ export interface HttpClientAdapter {
|
|
|
37
39
|
*/
|
|
38
40
|
export class FetchAdapter implements HttpClientAdapter {
|
|
39
41
|
async request<T = any>(request: HttpRequest): Promise<HttpResponse<T>> {
|
|
40
|
-
const { method, url, headers, body, params, formData } = request;
|
|
42
|
+
const { method, url, headers, body, params, formData, binaryBody } = request;
|
|
41
43
|
|
|
42
44
|
// Build URL with query params
|
|
43
45
|
let finalUrl = url;
|
|
@@ -58,12 +60,16 @@ export class FetchAdapter implements HttpClientAdapter {
|
|
|
58
60
|
const finalHeaders: Record<string, string> = { ...headers };
|
|
59
61
|
|
|
60
62
|
// Determine body and content-type
|
|
61
|
-
let requestBody: string | FormData | undefined;
|
|
63
|
+
let requestBody: string | FormData | Blob | ArrayBuffer | undefined;
|
|
62
64
|
|
|
63
65
|
if (formData) {
|
|
64
66
|
// For multipart/form-data, let browser set Content-Type with boundary
|
|
65
67
|
requestBody = formData;
|
|
66
68
|
// Don't set Content-Type - browser will set it with boundary
|
|
69
|
+
} else if (binaryBody) {
|
|
70
|
+
// Binary upload (application/octet-stream)
|
|
71
|
+
finalHeaders['Content-Type'] = 'application/octet-stream';
|
|
72
|
+
requestBody = binaryBody;
|
|
67
73
|
} else if (body) {
|
|
68
74
|
// JSON request
|
|
69
75
|
finalHeaders['Content-Type'] = 'application/json';
|
|
@@ -163,10 +163,11 @@ export class API {
|
|
|
163
163
|
|
|
164
164
|
this._loadTokensFromStorage();
|
|
165
165
|
|
|
166
|
-
// Initialize APIClient
|
|
166
|
+
// Initialize APIClient with token getter for URL authentication
|
|
167
167
|
this._client = new APIClient(this.baseUrl, {
|
|
168
168
|
retryConfig: this.options?.retryConfig,
|
|
169
169
|
loggerConfig: this.options?.loggerConfig,
|
|
170
|
+
tokenGetter: () => this.getToken(),
|
|
170
171
|
});
|
|
171
172
|
|
|
172
173
|
// Always inject auth header wrapper (reads token dynamically from storage)
|
|
@@ -191,6 +192,7 @@ export class API {
|
|
|
191
192
|
this._client = new APIClient(this.baseUrl, {
|
|
192
193
|
retryConfig: this.options?.retryConfig,
|
|
193
194
|
loggerConfig: this.options?.loggerConfig,
|
|
195
|
+
tokenGetter: () => this.getToken(),
|
|
194
196
|
});
|
|
195
197
|
|
|
196
198
|
// Always inject auth header wrapper (reads token dynamically from storage)
|
package/src/api/index.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { createExtensionAPI } from '@djangocfg/ext-base/api';
|
|
1
|
+
import { createExtensionAPI, initializeExtensionAPI } from '@djangocfg/ext-base/api';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Newsletter Extension API
|
|
@@ -6,5 +6,10 @@ import { createExtensionAPI } from '@djangocfg/ext-base/api';
|
|
|
6
6
|
* Pre-configured API instance with shared authentication
|
|
7
7
|
*/
|
|
8
8
|
import { API } from './generated/ext_newsletter';
|
|
9
|
+
import { configureAPI } from './generated/ext_newsletter/api-instance';
|
|
9
10
|
|
|
11
|
+
// Initialize global API singleton for hooks and fetchers
|
|
12
|
+
initializeExtensionAPI(configureAPI);
|
|
13
|
+
|
|
14
|
+
// Create instance for direct usage
|
|
10
15
|
export const apiNewsletter = createExtensionAPI(API);
|