@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.
@@ -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 can be configured once
6
- * and used throughout your application.
5
+ * This module provides a global API instance that auto-configures from
6
+ * environment variables or can be configured manually.
7
7
  *
8
- * Usage:
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
- * @throws Error if API is not configured
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);