@startsimpli/api 0.5.4 → 0.5.5
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/package.json +1 -1
- package/src/constants/endpoints.ts +4 -0
- package/src/index.ts +4 -0
- package/src/lib/api-client.ts +3 -0
- package/src/lib/fetch-wrapper.ts +25 -5
- package/src/lib/users-api.ts +40 -0
- package/src/types/index.ts +8 -0
- package/src/types/user.ts +31 -0
- package/src/utils/case-transform.ts +44 -0
- package/src/utils/index.ts +2 -0
package/package.json
CHANGED
|
@@ -49,6 +49,10 @@ export const ENDPOINTS = {
|
|
|
49
49
|
FUNNEL_RUN_BY_ID: (runId: string) => `api/v1/funnel-runs/${runId}`,
|
|
50
50
|
FUNNEL_RUN_CANCEL: (runId: string) => `api/v1/funnel-runs/${runId}/cancel`,
|
|
51
51
|
FUNNEL_RUNS_GLOBAL: 'api/v1/funnel-runs',
|
|
52
|
+
// Users
|
|
53
|
+
USER_ME: 'api/v1/users/me',
|
|
54
|
+
USER_CHANGE_PASSWORD: 'api/v1/users/me/change-password',
|
|
55
|
+
|
|
52
56
|
// Companies / Feature flags
|
|
53
57
|
FEATURE_FLAGS: 'api/v1/companies/feature-flags',
|
|
54
58
|
} as const;
|
package/src/index.ts
CHANGED
|
@@ -16,6 +16,8 @@ export { EntitiesApi } from './lib/entities-api';
|
|
|
16
16
|
export { WorkflowsApi } from './lib/workflows-api';
|
|
17
17
|
export { MessagesApi } from './lib/messages-api';
|
|
18
18
|
export type { Message, MessageStatus as MessageApiStatus, MessageRecipient, MessagingChannel as MessagingChannelType } from './lib/messages-api';
|
|
19
|
+
export { UsersApi } from './lib/users-api';
|
|
20
|
+
export type { UserProfile, UpdateProfileRequest, ChangePasswordRequest, ChangePasswordResponse } from './types/user';
|
|
19
21
|
export { FunnelsApi, isFunnelRunConflict, isFunnelValidationError } from './lib/funnels-api';
|
|
20
22
|
export type { FunnelPreviewResult, FunnelRunFilters, FunnelTemplate } from './lib/funnels-api';
|
|
21
23
|
|
|
@@ -126,6 +128,7 @@ import { WorkflowsApi } from './lib/workflows-api';
|
|
|
126
128
|
import { MessagesApi } from './lib/messages-api';
|
|
127
129
|
import { FunnelsApi } from './lib/funnels-api';
|
|
128
130
|
import { FeatureFlagsApi } from './lib/feature-flags';
|
|
131
|
+
import { UsersApi } from './lib/users-api';
|
|
129
132
|
|
|
130
133
|
import type { ApiClientConfig } from './lib/api-client';
|
|
131
134
|
|
|
@@ -145,5 +148,6 @@ export function createStartSimpliApi(config: ApiClientConfig = {}) {
|
|
|
145
148
|
messages: new MessagesApi(client),
|
|
146
149
|
funnels: new FunnelsApi(client),
|
|
147
150
|
featureFlags: new FeatureFlagsApi(client),
|
|
151
|
+
users: new UsersApi(client),
|
|
148
152
|
};
|
|
149
153
|
}
|
package/src/lib/api-client.ts
CHANGED
|
@@ -10,6 +10,8 @@ export interface ApiClientConfig {
|
|
|
10
10
|
getToken?: () => Promise<string | null> | string | null;
|
|
11
11
|
onUnauthorized?: () => void;
|
|
12
12
|
onTokenRefresh?: () => Promise<string | null>;
|
|
13
|
+
/** Auto-convert snake_case↔camelCase on responses/requests. Defaults to true. */
|
|
14
|
+
transformKeys?: boolean;
|
|
13
15
|
}
|
|
14
16
|
|
|
15
17
|
export class ApiClient {
|
|
@@ -24,6 +26,7 @@ export class ApiClient {
|
|
|
24
26
|
getToken: config.getToken,
|
|
25
27
|
onUnauthorized: config.onUnauthorized,
|
|
26
28
|
onTokenRefresh: config.onTokenRefresh,
|
|
29
|
+
transformKeys: config.transformKeys,
|
|
27
30
|
defaultHeaders: {
|
|
28
31
|
'Content-Type': 'application/json',
|
|
29
32
|
'Accept': 'application/json',
|
package/src/lib/fetch-wrapper.ts
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
import type { FetchOptions, HttpMethod } from '../types';
|
|
6
6
|
import { buildUrl, buildQueryString } from '../utils';
|
|
7
7
|
import { ApiException, parseErrorResponse, handleFetchError } from './error-handler';
|
|
8
|
+
import { snakeToCamel, camelToSnake } from '../utils/case-transform';
|
|
8
9
|
|
|
9
10
|
export interface FetchWrapperConfig {
|
|
10
11
|
baseUrl?: string;
|
|
@@ -12,6 +13,12 @@ export interface FetchWrapperConfig {
|
|
|
12
13
|
onUnauthorized?: () => void;
|
|
13
14
|
onTokenRefresh?: () => Promise<string | null>;
|
|
14
15
|
defaultHeaders?: HeadersInit;
|
|
16
|
+
/**
|
|
17
|
+
* Automatically convert response keys from snake_case to camelCase
|
|
18
|
+
* and request body keys from camelCase to snake_case.
|
|
19
|
+
* Defaults to true — set to false for endpoints that need raw keys.
|
|
20
|
+
*/
|
|
21
|
+
transformKeys?: boolean;
|
|
15
22
|
}
|
|
16
23
|
|
|
17
24
|
export class FetchWrapper {
|
|
@@ -131,8 +138,12 @@ export class FetchWrapper {
|
|
|
131
138
|
return undefined as T;
|
|
132
139
|
}
|
|
133
140
|
|
|
134
|
-
// Parse JSON response
|
|
135
|
-
|
|
141
|
+
// Parse JSON response, auto-convert snake_case → camelCase
|
|
142
|
+
const data = await response.json();
|
|
143
|
+
if (this.config.transformKeys !== false) {
|
|
144
|
+
return snakeToCamel(data) as T;
|
|
145
|
+
}
|
|
146
|
+
return data as T;
|
|
136
147
|
} catch (error) {
|
|
137
148
|
handleFetchError(error);
|
|
138
149
|
}
|
|
@@ -155,6 +166,15 @@ export class FetchWrapper {
|
|
|
155
166
|
/**
|
|
156
167
|
* POST request
|
|
157
168
|
*/
|
|
169
|
+
/** Convert request body keys to snake_case if transformKeys is enabled */
|
|
170
|
+
private serializeBody(data: unknown): string | undefined {
|
|
171
|
+
if (data === undefined || data === null) return undefined;
|
|
172
|
+
if (this.config.transformKeys !== false) {
|
|
173
|
+
return JSON.stringify(camelToSnake(data));
|
|
174
|
+
}
|
|
175
|
+
return JSON.stringify(data);
|
|
176
|
+
}
|
|
177
|
+
|
|
158
178
|
async post<T, D = unknown>(
|
|
159
179
|
endpoint: string,
|
|
160
180
|
data?: D,
|
|
@@ -162,7 +182,7 @@ export class FetchWrapper {
|
|
|
162
182
|
): Promise<T> {
|
|
163
183
|
return this.execute<T>('POST', endpoint, {
|
|
164
184
|
...options,
|
|
165
|
-
body:
|
|
185
|
+
body: this.serializeBody(data),
|
|
166
186
|
});
|
|
167
187
|
}
|
|
168
188
|
|
|
@@ -176,7 +196,7 @@ export class FetchWrapper {
|
|
|
176
196
|
): Promise<T> {
|
|
177
197
|
return this.execute<T>('PUT', endpoint, {
|
|
178
198
|
...options,
|
|
179
|
-
body:
|
|
199
|
+
body: this.serializeBody(data),
|
|
180
200
|
});
|
|
181
201
|
}
|
|
182
202
|
|
|
@@ -190,7 +210,7 @@ export class FetchWrapper {
|
|
|
190
210
|
): Promise<T> {
|
|
191
211
|
return this.execute<T>('PATCH', endpoint, {
|
|
192
212
|
...options,
|
|
193
|
-
body:
|
|
213
|
+
body: this.serializeBody(data),
|
|
194
214
|
});
|
|
195
215
|
}
|
|
196
216
|
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Users API wrapper for /api/v1/users/
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type {
|
|
6
|
+
UserProfile,
|
|
7
|
+
UpdateProfileRequest,
|
|
8
|
+
ChangePasswordRequest,
|
|
9
|
+
ChangePasswordResponse,
|
|
10
|
+
} from '../types/user';
|
|
11
|
+
import { ENDPOINTS } from '../constants/endpoints';
|
|
12
|
+
import type { ApiClient } from './api-client';
|
|
13
|
+
|
|
14
|
+
export class UsersApi {
|
|
15
|
+
constructor(private client: ApiClient) {}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Get the current user's profile
|
|
19
|
+
*/
|
|
20
|
+
async getProfile(): Promise<UserProfile> {
|
|
21
|
+
return this.client.fetch.get<UserProfile>(ENDPOINTS.USER_ME);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Update the current user's profile
|
|
26
|
+
*/
|
|
27
|
+
async updateProfile(data: UpdateProfileRequest): Promise<UserProfile> {
|
|
28
|
+
return this.client.fetch.patch<UserProfile>(ENDPOINTS.USER_ME, data);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Change the current user's password
|
|
33
|
+
*/
|
|
34
|
+
async changePassword(data: ChangePasswordRequest): Promise<ChangePasswordResponse> {
|
|
35
|
+
return this.client.fetch.post<ChangePasswordResponse>(
|
|
36
|
+
ENDPOINTS.USER_CHANGE_PASSWORD,
|
|
37
|
+
data
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
}
|
package/src/types/index.ts
CHANGED
|
@@ -81,6 +81,14 @@ export type {
|
|
|
81
81
|
UpdateWorkflowInput,
|
|
82
82
|
} from './workflow';
|
|
83
83
|
|
|
84
|
+
// User types
|
|
85
|
+
export type {
|
|
86
|
+
UserProfile,
|
|
87
|
+
UpdateProfileRequest,
|
|
88
|
+
ChangePasswordRequest,
|
|
89
|
+
ChangePasswordResponse,
|
|
90
|
+
} from './user';
|
|
91
|
+
|
|
84
92
|
// Error types
|
|
85
93
|
export type {
|
|
86
94
|
FieldError,
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* User types for /api/v1/users/ endpoints
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export interface UserProfile {
|
|
6
|
+
id: string;
|
|
7
|
+
email: string;
|
|
8
|
+
first_name: string;
|
|
9
|
+
last_name: string;
|
|
10
|
+
full_name: string;
|
|
11
|
+
is_email_verified: boolean;
|
|
12
|
+
company: string | null;
|
|
13
|
+
is_active: boolean;
|
|
14
|
+
created_at: string;
|
|
15
|
+
updated_at: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface UpdateProfileRequest {
|
|
19
|
+
first_name?: string;
|
|
20
|
+
last_name?: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface ChangePasswordRequest {
|
|
24
|
+
old_password: string;
|
|
25
|
+
new_password: string;
|
|
26
|
+
new_password_confirm: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface ChangePasswordResponse {
|
|
30
|
+
detail: string;
|
|
31
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Recursive key case transformation utilities.
|
|
3
|
+
*
|
|
4
|
+
* Used by FetchWrapper to automatically convert between Django's snake_case
|
|
5
|
+
* and the frontend's camelCase so app code never handles snake_case.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/** Convert a single snake_case string to camelCase */
|
|
9
|
+
function snakeToCamelKey(key: string): string {
|
|
10
|
+
return key.replace(/_([a-z0-9])/g, (_, c) => c.toUpperCase())
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** Convert a single camelCase string to snake_case */
|
|
14
|
+
function camelToSnakeKey(key: string): string {
|
|
15
|
+
return key.replace(/[A-Z]/g, (c) => `_${c.toLowerCase()}`)
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** Recursively convert all object keys from snake_case to camelCase */
|
|
19
|
+
export function snakeToCamel(obj: unknown): unknown {
|
|
20
|
+
if (Array.isArray(obj)) return obj.map(snakeToCamel)
|
|
21
|
+
if (obj !== null && typeof obj === 'object' && !(obj instanceof Date)) {
|
|
22
|
+
return Object.fromEntries(
|
|
23
|
+
Object.entries(obj as Record<string, unknown>).map(([k, v]) => [
|
|
24
|
+
snakeToCamelKey(k),
|
|
25
|
+
snakeToCamel(v),
|
|
26
|
+
])
|
|
27
|
+
)
|
|
28
|
+
}
|
|
29
|
+
return obj
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Recursively convert all object keys from camelCase to snake_case */
|
|
33
|
+
export function camelToSnake(obj: unknown): unknown {
|
|
34
|
+
if (Array.isArray(obj)) return obj.map(camelToSnake)
|
|
35
|
+
if (obj !== null && typeof obj === 'object' && !(obj instanceof Date)) {
|
|
36
|
+
return Object.fromEntries(
|
|
37
|
+
Object.entries(obj as Record<string, unknown>).map(([k, v]) => [
|
|
38
|
+
camelToSnakeKey(k),
|
|
39
|
+
camelToSnake(v),
|
|
40
|
+
])
|
|
41
|
+
)
|
|
42
|
+
}
|
|
43
|
+
return obj
|
|
44
|
+
}
|
package/src/utils/index.ts
CHANGED
|
@@ -17,6 +17,8 @@ export { validateApiResponse } from './validate-response';
|
|
|
17
17
|
export { EntityQueryBuilder } from './entity-query-builder';
|
|
18
18
|
|
|
19
19
|
export { normalizePaginated, isDRFPaginatedResponse } from './drf-transforms';
|
|
20
|
+
|
|
21
|
+
export { snakeToCamel, camelToSnake } from './case-transform';
|
|
20
22
|
export type {
|
|
21
23
|
DRFPaginatedResponse,
|
|
22
24
|
NormalizedPaginatedResponse,
|