@startsimpli/api 0.5.3 → 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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@startsimpli/api",
3
- "version": "0.5.3",
3
+ "version": "0.5.5",
4
4
  "description": "Type-safe Django REST API client for StartSimpli apps",
5
5
  "main": "./src/index.ts",
6
6
  "types": "./src/index.ts",
@@ -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
  }
@@ -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',
@@ -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
- return await response.json();
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: data ? JSON.stringify(data) : undefined,
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: data ? JSON.stringify(data) : undefined,
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: data ? JSON.stringify(data) : undefined,
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
+ }
@@ -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
+ }
@@ -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,