create-aws-project 1.2.1
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/README.md +118 -0
- package/dist/__tests__/generator/replace-tokens.spec.d.ts +1 -0
- package/dist/__tests__/generator/replace-tokens.spec.js +281 -0
- package/dist/__tests__/generator.spec.d.ts +1 -0
- package/dist/__tests__/generator.spec.js +162 -0
- package/dist/__tests__/validation/project-name.spec.d.ts +1 -0
- package/dist/__tests__/validation/project-name.spec.js +57 -0
- package/dist/__tests__/wizard.spec.d.ts +1 -0
- package/dist/__tests__/wizard.spec.js +232 -0
- package/dist/aws/iam.d.ts +75 -0
- package/dist/aws/iam.js +264 -0
- package/dist/aws/organizations.d.ts +79 -0
- package/dist/aws/organizations.js +168 -0
- package/dist/cli.d.ts +4 -0
- package/dist/cli.js +206 -0
- package/dist/commands/setup-github.d.ts +4 -0
- package/dist/commands/setup-github.js +185 -0
- package/dist/generator/copy-file.d.ts +15 -0
- package/dist/generator/copy-file.js +56 -0
- package/dist/generator/generate-project.d.ts +14 -0
- package/dist/generator/generate-project.js +81 -0
- package/dist/generator/index.d.ts +4 -0
- package/dist/generator/index.js +3 -0
- package/dist/generator/replace-tokens.d.ts +29 -0
- package/dist/generator/replace-tokens.js +68 -0
- package/dist/github/secrets.d.ts +109 -0
- package/dist/github/secrets.js +275 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +6 -0
- package/dist/prompts/auth.d.ts +3 -0
- package/dist/prompts/auth.js +23 -0
- package/dist/prompts/aws-config.d.ts +2 -0
- package/dist/prompts/aws-config.js +14 -0
- package/dist/prompts/features.d.ts +2 -0
- package/dist/prompts/features.js +10 -0
- package/dist/prompts/github-setup.d.ts +53 -0
- package/dist/prompts/github-setup.js +208 -0
- package/dist/prompts/org-structure.d.ts +9 -0
- package/dist/prompts/org-structure.js +93 -0
- package/dist/prompts/platforms.d.ts +2 -0
- package/dist/prompts/platforms.js +12 -0
- package/dist/prompts/project-name.d.ts +2 -0
- package/dist/prompts/project-name.js +8 -0
- package/dist/prompts/theme.d.ts +2 -0
- package/dist/prompts/theme.js +14 -0
- package/dist/templates/index.d.ts +4 -0
- package/dist/templates/index.js +2 -0
- package/dist/templates/manifest.d.ts +11 -0
- package/dist/templates/manifest.js +99 -0
- package/dist/templates/tokens.d.ts +39 -0
- package/dist/templates/tokens.js +37 -0
- package/dist/templates/types.d.ts +52 -0
- package/dist/templates/types.js +1 -0
- package/dist/types.d.ts +27 -0
- package/dist/types.js +1 -0
- package/dist/validation/project-name.d.ts +1 -0
- package/dist/validation/project-name.js +12 -0
- package/dist/wizard.d.ts +2 -0
- package/dist/wizard.js +81 -0
- package/package.json +68 -0
- package/templates/.github/actions/build-and-test/action.yml +24 -0
- package/templates/.github/actions/deploy-cdk/action.yml +46 -0
- package/templates/.github/actions/deploy-web/action.yml +72 -0
- package/templates/.github/actions/setup/action.yml +29 -0
- package/templates/.github/pull_request_template.md +15 -0
- package/templates/.github/workflows/deploy-dev.yml +80 -0
- package/templates/.github/workflows/deploy-prod.yml +67 -0
- package/templates/.github/workflows/deploy-stage.yml +77 -0
- package/templates/.github/workflows/pull-request.yml +72 -0
- package/templates/.vscode/extensions.json +7 -0
- package/templates/.vscode/settings.json +67 -0
- package/templates/apps/api/.eslintrc.json +18 -0
- package/templates/apps/api/cdk/app.ts +93 -0
- package/templates/apps/api/cdk/auth/cognito-stack.ts +164 -0
- package/templates/apps/api/cdk/cdk.json +73 -0
- package/templates/apps/api/cdk/deployment-user-stack.ts +187 -0
- package/templates/apps/api/cdk/org-stack.ts +67 -0
- package/templates/apps/api/cdk/static-stack.ts +361 -0
- package/templates/apps/api/cdk/tsconfig.json +39 -0
- package/templates/apps/api/cdk/user-stack.ts +255 -0
- package/templates/apps/api/jest.config.ts +38 -0
- package/templates/apps/api/lambdas.yml +84 -0
- package/templates/apps/api/project.json.template +58 -0
- package/templates/apps/api/src/__tests__/setup.ts +10 -0
- package/templates/apps/api/src/handlers/users/create-user.ts +52 -0
- package/templates/apps/api/src/handlers/users/delete-user.ts +45 -0
- package/templates/apps/api/src/handlers/users/get-me.ts +72 -0
- package/templates/apps/api/src/handlers/users/get-user.ts +45 -0
- package/templates/apps/api/src/handlers/users/get-users.ts +23 -0
- package/templates/apps/api/src/handlers/users/index.ts +17 -0
- package/templates/apps/api/src/handlers/users/update-user.ts +72 -0
- package/templates/apps/api/src/lib/dynamo/dynamo-model.ts +504 -0
- package/templates/apps/api/src/lib/dynamo/index.ts +12 -0
- package/templates/apps/api/src/lib/dynamo/utils.ts +39 -0
- package/templates/apps/api/src/middleware/auth0-auth.ts +97 -0
- package/templates/apps/api/src/middleware/cognito-auth.ts +90 -0
- package/templates/apps/api/src/models/UserModel.ts +109 -0
- package/templates/apps/api/src/schemas/user.schema.ts +44 -0
- package/templates/apps/api/src/services/user-service.ts +108 -0
- package/templates/apps/api/src/utils/auth-context.ts +60 -0
- package/templates/apps/api/src/utils/common/helpers.ts +26 -0
- package/templates/apps/api/src/utils/lambda-handler.ts +148 -0
- package/templates/apps/api/src/utils/response.ts +52 -0
- package/templates/apps/api/src/utils/validator.ts +75 -0
- package/templates/apps/api/tsconfig.app.json +15 -0
- package/templates/apps/api/tsconfig.json +19 -0
- package/templates/apps/api/tsconfig.spec.json +17 -0
- package/templates/apps/mobile/.env.example +5 -0
- package/templates/apps/mobile/.eslintrc.json +33 -0
- package/templates/apps/mobile/app.json +33 -0
- package/templates/apps/mobile/assets/.gitkeep +0 -0
- package/templates/apps/mobile/babel.config.js +19 -0
- package/templates/apps/mobile/index.js +7 -0
- package/templates/apps/mobile/jest.config.ts +22 -0
- package/templates/apps/mobile/metro.config.js +35 -0
- package/templates/apps/mobile/package.json +22 -0
- package/templates/apps/mobile/project.json.template +64 -0
- package/templates/apps/mobile/src/App.tsx +367 -0
- package/templates/apps/mobile/src/__tests__/App.spec.tsx +46 -0
- package/templates/apps/mobile/src/__tests__/store/user-store.spec.ts +156 -0
- package/templates/apps/mobile/src/config/api.ts +16 -0
- package/templates/apps/mobile/src/store/user-store.ts +56 -0
- package/templates/apps/mobile/src/test-setup.ts +10 -0
- package/templates/apps/mobile/tsconfig.json +22 -0
- package/templates/apps/web/.env.example +13 -0
- package/templates/apps/web/.eslintrc.json +26 -0
- package/templates/apps/web/index.html +13 -0
- package/templates/apps/web/jest.config.ts +24 -0
- package/templates/apps/web/package.json +15 -0
- package/templates/apps/web/project.json.template +66 -0
- package/templates/apps/web/src/App.tsx +352 -0
- package/templates/apps/web/src/__mocks__/config/api.ts +41 -0
- package/templates/apps/web/src/__tests__/App.spec.tsx +240 -0
- package/templates/apps/web/src/__tests__/store/user-store.spec.ts +185 -0
- package/templates/apps/web/src/auth/auth0-provider.tsx +103 -0
- package/templates/apps/web/src/auth/cognito-provider.tsx +143 -0
- package/templates/apps/web/src/auth/index.ts +7 -0
- package/templates/apps/web/src/auth/use-auth.ts +16 -0
- package/templates/apps/web/src/config/amplify-config.ts +31 -0
- package/templates/apps/web/src/config/api.ts +38 -0
- package/templates/apps/web/src/config/auth0-config.ts +17 -0
- package/templates/apps/web/src/main.tsx +41 -0
- package/templates/apps/web/src/store/user-store.ts +56 -0
- package/templates/apps/web/src/styles.css +165 -0
- package/templates/apps/web/src/test-setup.ts +1 -0
- package/templates/apps/web/src/theme/index.ts +30 -0
- package/templates/apps/web/src/vite-env.d.ts +19 -0
- package/templates/apps/web/tsconfig.app.json +24 -0
- package/templates/apps/web/tsconfig.json +22 -0
- package/templates/apps/web/tsconfig.spec.json +28 -0
- package/templates/apps/web/vite.config.ts +87 -0
- package/templates/manifest.json +28 -0
- package/templates/packages/api-client/.eslintrc.json +18 -0
- package/templates/packages/api-client/jest.config.ts +13 -0
- package/templates/packages/api-client/package.json +8 -0
- package/templates/packages/api-client/project.json.template +34 -0
- package/templates/packages/api-client/src/__tests__/api-client.spec.ts +408 -0
- package/templates/packages/api-client/src/api-client.ts +201 -0
- package/templates/packages/api-client/src/config.ts +193 -0
- package/templates/packages/api-client/src/index.ts +9 -0
- package/templates/packages/api-client/tsconfig.json +22 -0
- package/templates/packages/api-client/tsconfig.lib.json +11 -0
- package/templates/packages/api-client/tsconfig.spec.json +14 -0
- package/templates/packages/common-types/.eslintrc.json +18 -0
- package/templates/packages/common-types/package.json +6 -0
- package/templates/packages/common-types/project.json.template +26 -0
- package/templates/packages/common-types/src/api.types.ts +24 -0
- package/templates/packages/common-types/src/auth.types.ts +36 -0
- package/templates/packages/common-types/src/common.types.ts +46 -0
- package/templates/packages/common-types/src/index.ts +19 -0
- package/templates/packages/common-types/src/lambda.types.ts +39 -0
- package/templates/packages/common-types/src/user.types.ts +31 -0
- package/templates/packages/common-types/tsconfig.json +19 -0
- package/templates/packages/common-types/tsconfig.lib.json +11 -0
- package/templates/root/.editorconfig +23 -0
- package/templates/root/.nvmrc +1 -0
- package/templates/root/eslint.config.js +61 -0
- package/templates/root/jest.preset.js +16 -0
- package/templates/root/nx.json +29 -0
- package/templates/root/package.json +131 -0
- package/templates/root/tsconfig.base.json +29 -0
|
@@ -0,0 +1,408 @@
|
|
|
1
|
+
import axios from 'axios';
|
|
2
|
+
import { ApiClient, ApiError, createApiClient } from '../api-client';
|
|
3
|
+
import type { User, CreateUserRequest, UpdateUserRequest } from '{{PACKAGE_SCOPE}}/common-types';
|
|
4
|
+
|
|
5
|
+
// Mock axios
|
|
6
|
+
jest.mock('axios');
|
|
7
|
+
const mockedAxios = axios as jest.Mocked<typeof axios>;
|
|
8
|
+
|
|
9
|
+
describe('ApiClient', () => {
|
|
10
|
+
let apiClient: ApiClient;
|
|
11
|
+
const mockAxiosInstance = {
|
|
12
|
+
get: jest.fn(),
|
|
13
|
+
post: jest.fn(),
|
|
14
|
+
put: jest.fn(),
|
|
15
|
+
delete: jest.fn(),
|
|
16
|
+
defaults: {
|
|
17
|
+
headers: {
|
|
18
|
+
common: {} as Record<string, string>,
|
|
19
|
+
},
|
|
20
|
+
baseURL: '',
|
|
21
|
+
},
|
|
22
|
+
interceptors: {
|
|
23
|
+
response: {
|
|
24
|
+
use: jest.fn(),
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
beforeEach(() => {
|
|
30
|
+
jest.clearAllMocks();
|
|
31
|
+
mockedAxios.create.mockReturnValue(mockAxiosInstance as any);
|
|
32
|
+
|
|
33
|
+
apiClient = new ApiClient({
|
|
34
|
+
baseURL: 'https://api.example.com',
|
|
35
|
+
timeout: 5000,
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
describe('constructor', () => {
|
|
40
|
+
it('should create axios instance with correct config', () => {
|
|
41
|
+
expect(mockedAxios.create).toHaveBeenCalledWith({
|
|
42
|
+
baseURL: 'https://api.example.com',
|
|
43
|
+
timeout: 5000,
|
|
44
|
+
headers: {
|
|
45
|
+
'Content-Type': 'application/json',
|
|
46
|
+
},
|
|
47
|
+
withCredentials: false,
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('should create axios instance with custom headers', () => {
|
|
52
|
+
new ApiClient({
|
|
53
|
+
baseURL: 'https://api.example.com',
|
|
54
|
+
headers: {
|
|
55
|
+
'X-Custom-Header': 'value',
|
|
56
|
+
},
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
expect(mockedAxios.create).toHaveBeenCalledWith(
|
|
60
|
+
expect.objectContaining({
|
|
61
|
+
headers: {
|
|
62
|
+
'Content-Type': 'application/json',
|
|
63
|
+
'X-Custom-Header': 'value',
|
|
64
|
+
},
|
|
65
|
+
})
|
|
66
|
+
);
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
describe('createApiClient', () => {
|
|
71
|
+
it('should create and return ApiClient instance', () => {
|
|
72
|
+
const client = createApiClient({ baseURL: 'https://api.example.com' });
|
|
73
|
+
expect(client).toBeInstanceOf(ApiClient);
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
describe('setAuthToken', () => {
|
|
78
|
+
it('should set authorization header', () => {
|
|
79
|
+
apiClient.setAuthToken('test-token');
|
|
80
|
+
expect(mockAxiosInstance.defaults.headers.common['Authorization']).toBe('Bearer test-token');
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
describe('clearAuthToken', () => {
|
|
85
|
+
it('should remove authorization header', () => {
|
|
86
|
+
mockAxiosInstance.defaults.headers.common['Authorization'] = 'Bearer test-token';
|
|
87
|
+
apiClient.clearAuthToken();
|
|
88
|
+
expect(mockAxiosInstance.defaults.headers.common['Authorization']).toBeUndefined();
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
describe('setBaseURL', () => {
|
|
93
|
+
it('should update base URL', () => {
|
|
94
|
+
apiClient.setBaseURL('https://new-api.example.com');
|
|
95
|
+
expect(mockAxiosInstance.defaults.baseURL).toBe('https://new-api.example.com');
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
describe('getUsers', () => {
|
|
100
|
+
it('should fetch all users', async () => {
|
|
101
|
+
const mockUsers: User[] = [
|
|
102
|
+
{ id: '1', email: 'user1@example.com', name: 'User 1', createdAt: '2024-01-01' },
|
|
103
|
+
{ id: '2', email: 'user2@example.com', name: 'User 2', createdAt: '2024-01-02' },
|
|
104
|
+
];
|
|
105
|
+
|
|
106
|
+
mockAxiosInstance.get.mockResolvedValue({
|
|
107
|
+
data: {
|
|
108
|
+
success: true,
|
|
109
|
+
data: mockUsers,
|
|
110
|
+
},
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
const users = await apiClient.getUsers();
|
|
114
|
+
|
|
115
|
+
expect(mockAxiosInstance.get).toHaveBeenCalledWith('/api/users', undefined);
|
|
116
|
+
expect(users).toEqual(mockUsers);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it('should throw error for invalid response format', async () => {
|
|
120
|
+
mockAxiosInstance.get.mockResolvedValue({
|
|
121
|
+
data: {
|
|
122
|
+
success: false,
|
|
123
|
+
},
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
await expect(apiClient.getUsers()).rejects.toThrow('Invalid response format');
|
|
127
|
+
});
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
describe('getUser', () => {
|
|
131
|
+
it('should fetch user by ID', async () => {
|
|
132
|
+
const mockUser: User = {
|
|
133
|
+
id: '1',
|
|
134
|
+
email: 'user@example.com',
|
|
135
|
+
name: 'Test User',
|
|
136
|
+
createdAt: '2024-01-01',
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
mockAxiosInstance.get.mockResolvedValue({
|
|
140
|
+
data: {
|
|
141
|
+
success: true,
|
|
142
|
+
data: mockUser,
|
|
143
|
+
},
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
const user = await apiClient.getUser('1');
|
|
147
|
+
|
|
148
|
+
expect(mockAxiosInstance.get).toHaveBeenCalledWith('/api/users/1', undefined);
|
|
149
|
+
expect(user).toEqual(mockUser);
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it('should throw error for invalid response format', async () => {
|
|
153
|
+
mockAxiosInstance.get.mockResolvedValue({
|
|
154
|
+
data: {
|
|
155
|
+
success: false,
|
|
156
|
+
},
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
await expect(apiClient.getUser('1')).rejects.toThrow('Invalid response format');
|
|
160
|
+
});
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
describe('createUser', () => {
|
|
164
|
+
it('should create a new user', async () => {
|
|
165
|
+
const newUser: CreateUserRequest = {
|
|
166
|
+
email: 'new@example.com',
|
|
167
|
+
name: 'New User',
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
const createdUser: User = {
|
|
171
|
+
id: '3',
|
|
172
|
+
...newUser,
|
|
173
|
+
createdAt: '2024-01-03',
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
mockAxiosInstance.post.mockResolvedValue({
|
|
177
|
+
data: {
|
|
178
|
+
success: true,
|
|
179
|
+
data: createdUser,
|
|
180
|
+
},
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
const user = await apiClient.createUser(newUser);
|
|
184
|
+
|
|
185
|
+
expect(mockAxiosInstance.post).toHaveBeenCalledWith('/api/users', newUser, undefined);
|
|
186
|
+
expect(user).toEqual(createdUser);
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
it('should throw error for invalid response format', async () => {
|
|
190
|
+
mockAxiosInstance.post.mockResolvedValue({
|
|
191
|
+
data: {
|
|
192
|
+
success: false,
|
|
193
|
+
},
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
await expect(
|
|
197
|
+
apiClient.createUser({ email: 'test@example.com', name: 'Test' })
|
|
198
|
+
).rejects.toThrow('Invalid response format');
|
|
199
|
+
});
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
describe('updateUser', () => {
|
|
203
|
+
it('should update an existing user', async () => {
|
|
204
|
+
const updates: UpdateUserRequest = {
|
|
205
|
+
name: 'Updated Name',
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
const updatedUser: User = {
|
|
209
|
+
id: '1',
|
|
210
|
+
email: 'user@example.com',
|
|
211
|
+
name: 'Updated Name',
|
|
212
|
+
createdAt: '2024-01-01',
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
mockAxiosInstance.put.mockResolvedValue({
|
|
216
|
+
data: {
|
|
217
|
+
success: true,
|
|
218
|
+
data: updatedUser,
|
|
219
|
+
},
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
const user = await apiClient.updateUser('1', updates);
|
|
223
|
+
|
|
224
|
+
expect(mockAxiosInstance.put).toHaveBeenCalledWith('/api/users/1', updates, undefined);
|
|
225
|
+
expect(user).toEqual(updatedUser);
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
it('should throw error for invalid response format', async () => {
|
|
229
|
+
mockAxiosInstance.put.mockResolvedValue({
|
|
230
|
+
data: {
|
|
231
|
+
success: false,
|
|
232
|
+
},
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
await expect(apiClient.updateUser('1', { name: 'Test' })).rejects.toThrow(
|
|
236
|
+
'Invalid response format'
|
|
237
|
+
);
|
|
238
|
+
});
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
describe('deleteUser', () => {
|
|
242
|
+
it('should delete a user', async () => {
|
|
243
|
+
mockAxiosInstance.delete.mockResolvedValue({
|
|
244
|
+
data: {
|
|
245
|
+
success: true,
|
|
246
|
+
},
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
await apiClient.deleteUser('1');
|
|
250
|
+
|
|
251
|
+
expect(mockAxiosInstance.delete).toHaveBeenCalledWith('/api/users/1', undefined);
|
|
252
|
+
});
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
describe('getAxiosInstance', () => {
|
|
256
|
+
it('should return the axios instance', () => {
|
|
257
|
+
const instance = apiClient.getAxiosInstance();
|
|
258
|
+
expect(instance).toBe(mockAxiosInstance);
|
|
259
|
+
});
|
|
260
|
+
});
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
describe('Response Interceptor Error Handling', () => {
|
|
264
|
+
let apiClient: ApiClient;
|
|
265
|
+
let successHandler: (response: any) => any;
|
|
266
|
+
let errorHandler: (error: any) => never;
|
|
267
|
+
|
|
268
|
+
beforeEach(() => {
|
|
269
|
+
jest.clearAllMocks();
|
|
270
|
+
|
|
271
|
+
// Capture the handlers when interceptors.response.use is called
|
|
272
|
+
const mockAxiosInstance = {
|
|
273
|
+
get: jest.fn(),
|
|
274
|
+
post: jest.fn(),
|
|
275
|
+
put: jest.fn(),
|
|
276
|
+
delete: jest.fn(),
|
|
277
|
+
defaults: {
|
|
278
|
+
headers: {
|
|
279
|
+
common: {} as Record<string, string>,
|
|
280
|
+
},
|
|
281
|
+
baseURL: '',
|
|
282
|
+
},
|
|
283
|
+
interceptors: {
|
|
284
|
+
response: {
|
|
285
|
+
use: jest.fn((sucHandler, errHandler) => {
|
|
286
|
+
successHandler = sucHandler;
|
|
287
|
+
errorHandler = errHandler;
|
|
288
|
+
}),
|
|
289
|
+
},
|
|
290
|
+
},
|
|
291
|
+
};
|
|
292
|
+
|
|
293
|
+
(axios as jest.Mocked<typeof axios>).create.mockReturnValue(mockAxiosInstance as any);
|
|
294
|
+
apiClient = new ApiClient({ baseURL: 'https://api.example.com' });
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
it('should pass through successful responses', () => {
|
|
298
|
+
const mockResponse = { data: { success: true }, status: 200 };
|
|
299
|
+
const result = successHandler(mockResponse);
|
|
300
|
+
expect(result).toBe(mockResponse);
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
it('should handle server error with API error format', () => {
|
|
304
|
+
const axiosError = {
|
|
305
|
+
response: {
|
|
306
|
+
status: 400,
|
|
307
|
+
data: {
|
|
308
|
+
error: {
|
|
309
|
+
message: 'Validation failed',
|
|
310
|
+
code: 'VALIDATION_ERROR',
|
|
311
|
+
details: { field: 'email' },
|
|
312
|
+
},
|
|
313
|
+
},
|
|
314
|
+
},
|
|
315
|
+
message: 'Request failed',
|
|
316
|
+
};
|
|
317
|
+
|
|
318
|
+
expect(() => errorHandler(axiosError)).toThrow(ApiError);
|
|
319
|
+
try {
|
|
320
|
+
errorHandler(axiosError);
|
|
321
|
+
} catch (e) {
|
|
322
|
+
const error = e as ApiError;
|
|
323
|
+
expect(error.message).toBe('Validation failed');
|
|
324
|
+
expect(error.statusCode).toBe(400);
|
|
325
|
+
expect(error.code).toBe('VALIDATION_ERROR');
|
|
326
|
+
expect(error.details).toEqual({ field: 'email' });
|
|
327
|
+
}
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
it('should handle server error without API error format', () => {
|
|
331
|
+
const axiosError = {
|
|
332
|
+
response: {
|
|
333
|
+
status: 500,
|
|
334
|
+
data: { message: 'Internal server error' },
|
|
335
|
+
},
|
|
336
|
+
message: 'Server error',
|
|
337
|
+
};
|
|
338
|
+
|
|
339
|
+
expect(() => errorHandler(axiosError)).toThrow(ApiError);
|
|
340
|
+
try {
|
|
341
|
+
errorHandler(axiosError);
|
|
342
|
+
} catch (e) {
|
|
343
|
+
const error = e as ApiError;
|
|
344
|
+
expect(error.message).toBe('Server error');
|
|
345
|
+
expect(error.statusCode).toBe(500);
|
|
346
|
+
expect(error.code).toBe('API_ERROR');
|
|
347
|
+
}
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
it('should handle network error (no response)', () => {
|
|
351
|
+
const axiosError = {
|
|
352
|
+
request: {},
|
|
353
|
+
message: 'Network Error',
|
|
354
|
+
};
|
|
355
|
+
|
|
356
|
+
expect(() => errorHandler(axiosError)).toThrow(ApiError);
|
|
357
|
+
try {
|
|
358
|
+
errorHandler(axiosError);
|
|
359
|
+
} catch (e) {
|
|
360
|
+
const error = e as ApiError;
|
|
361
|
+
expect(error.message).toBe('No response from server');
|
|
362
|
+
expect(error.statusCode).toBeUndefined();
|
|
363
|
+
expect(error.code).toBe('NETWORK_ERROR');
|
|
364
|
+
}
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
it('should handle request setup error', () => {
|
|
368
|
+
const axiosError = {
|
|
369
|
+
message: 'Invalid URL',
|
|
370
|
+
};
|
|
371
|
+
|
|
372
|
+
expect(() => errorHandler(axiosError)).toThrow(ApiError);
|
|
373
|
+
try {
|
|
374
|
+
errorHandler(axiosError);
|
|
375
|
+
} catch (e) {
|
|
376
|
+
const error = e as ApiError;
|
|
377
|
+
expect(error.message).toBe('Invalid URL');
|
|
378
|
+
expect(error.statusCode).toBeUndefined();
|
|
379
|
+
expect(error.code).toBe('REQUEST_ERROR');
|
|
380
|
+
}
|
|
381
|
+
});
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
describe('ApiError', () => {
|
|
385
|
+
it('should create error with all properties', () => {
|
|
386
|
+
const error = new ApiError(
|
|
387
|
+
'Test error',
|
|
388
|
+
400,
|
|
389
|
+
'TEST_ERROR',
|
|
390
|
+
{ field: 'email' }
|
|
391
|
+
);
|
|
392
|
+
|
|
393
|
+
expect(error.message).toBe('Test error');
|
|
394
|
+
expect(error.statusCode).toBe(400);
|
|
395
|
+
expect(error.code).toBe('TEST_ERROR');
|
|
396
|
+
expect(error.details).toEqual({ field: 'email' });
|
|
397
|
+
expect(error.name).toBe('ApiError');
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
it('should create error with minimal properties', () => {
|
|
401
|
+
const error = new ApiError('Simple error');
|
|
402
|
+
|
|
403
|
+
expect(error.message).toBe('Simple error');
|
|
404
|
+
expect(error.statusCode).toBeUndefined();
|
|
405
|
+
expect(error.code).toBeUndefined();
|
|
406
|
+
expect(error.details).toBeUndefined();
|
|
407
|
+
});
|
|
408
|
+
});
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
import axios, { type AxiosInstance, type AxiosRequestConfig, AxiosError } from 'axios';
|
|
2
|
+
import type {
|
|
3
|
+
User,
|
|
4
|
+
CreateUserRequest,
|
|
5
|
+
UpdateUserRequest,
|
|
6
|
+
ApiResponse,
|
|
7
|
+
ApiError as ApiErrorType,
|
|
8
|
+
} from '{{PACKAGE_SCOPE}}/common-types';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Configuration options for the API client
|
|
12
|
+
*/
|
|
13
|
+
export interface ApiClientConfig {
|
|
14
|
+
/**
|
|
15
|
+
* Base URL for the API (e.g., 'https://api.example.com' or 'http://localhost:3000')
|
|
16
|
+
*/
|
|
17
|
+
baseURL: string;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Request timeout in milliseconds (default: 30000)
|
|
21
|
+
*/
|
|
22
|
+
timeout?: number;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Additional headers to include in all requests
|
|
26
|
+
*/
|
|
27
|
+
headers?: Record<string, string>;
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Whether to send cookies with requests (default: false)
|
|
31
|
+
*/
|
|
32
|
+
withCredentials?: boolean;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Custom error class for API errors
|
|
37
|
+
*/
|
|
38
|
+
export class ApiError extends Error {
|
|
39
|
+
public readonly statusCode?: number;
|
|
40
|
+
public readonly code?: string;
|
|
41
|
+
public readonly details?: unknown;
|
|
42
|
+
|
|
43
|
+
constructor(message: string, statusCode?: number, code?: string, details?: unknown) {
|
|
44
|
+
super(message);
|
|
45
|
+
this.name = 'ApiError';
|
|
46
|
+
this.statusCode = statusCode;
|
|
47
|
+
this.code = code;
|
|
48
|
+
this.details = details;
|
|
49
|
+
|
|
50
|
+
// Restore prototype chain for instanceof checks
|
|
51
|
+
Object.setPrototypeOf(this, ApiError.prototype);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* API Client for the monorepo
|
|
57
|
+
*
|
|
58
|
+
* Provides type-safe methods for interacting with the backend API.
|
|
59
|
+
*/
|
|
60
|
+
export class ApiClient {
|
|
61
|
+
private client: AxiosInstance;
|
|
62
|
+
|
|
63
|
+
constructor(config: ApiClientConfig) {
|
|
64
|
+
this.client = axios.create({
|
|
65
|
+
baseURL: config.baseURL,
|
|
66
|
+
timeout: config.timeout || 30000,
|
|
67
|
+
headers: {
|
|
68
|
+
'Content-Type': 'application/json',
|
|
69
|
+
...config.headers,
|
|
70
|
+
},
|
|
71
|
+
withCredentials: config.withCredentials || false,
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
// Add response interceptor for error handling
|
|
75
|
+
this.client.interceptors.response.use(
|
|
76
|
+
(response) => response,
|
|
77
|
+
(error: AxiosError) => {
|
|
78
|
+
if (error.response) {
|
|
79
|
+
// Server responded with error status
|
|
80
|
+
const data = error.response.data as ApiResponse<unknown> | ApiErrorType | undefined;
|
|
81
|
+
|
|
82
|
+
if (data && typeof data === 'object' && 'error' in data) {
|
|
83
|
+
const apiError = data.error as ApiErrorType;
|
|
84
|
+
throw new ApiError(
|
|
85
|
+
apiError.message,
|
|
86
|
+
error.response.status,
|
|
87
|
+
apiError.code,
|
|
88
|
+
apiError.details
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
throw new ApiError(
|
|
93
|
+
error.message,
|
|
94
|
+
error.response.status,
|
|
95
|
+
'API_ERROR',
|
|
96
|
+
error.response.data
|
|
97
|
+
);
|
|
98
|
+
} else if (error.request) {
|
|
99
|
+
// Request made but no response received
|
|
100
|
+
throw new ApiError('No response from server', undefined, 'NETWORK_ERROR');
|
|
101
|
+
} else {
|
|
102
|
+
// Something else happened
|
|
103
|
+
throw new ApiError(error.message, undefined, 'REQUEST_ERROR');
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Set authentication token
|
|
111
|
+
*/
|
|
112
|
+
public setAuthToken(token: string): void {
|
|
113
|
+
this.client.defaults.headers.common['Authorization'] = `Bearer ${token}`;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Clear authentication token
|
|
118
|
+
*/
|
|
119
|
+
public clearAuthToken(): void {
|
|
120
|
+
delete this.client.defaults.headers.common['Authorization'];
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Update the base URL
|
|
125
|
+
*/
|
|
126
|
+
public setBaseURL(baseURL: string): void {
|
|
127
|
+
this.client.defaults.baseURL = baseURL;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Get all users
|
|
132
|
+
*/
|
|
133
|
+
public async getUsers(config?: AxiosRequestConfig): Promise<User[]> {
|
|
134
|
+
const response = await this.client.get<ApiResponse<User[]>>('/api/users', config);
|
|
135
|
+
if (response.data.success && response.data.data) {
|
|
136
|
+
return response.data.data;
|
|
137
|
+
}
|
|
138
|
+
throw new ApiError('Invalid response format');
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Get user by ID
|
|
143
|
+
*/
|
|
144
|
+
public async getUser(id: string, config?: AxiosRequestConfig): Promise<User> {
|
|
145
|
+
const response = await this.client.get<ApiResponse<User>>(`/api/users/${id}`, config);
|
|
146
|
+
if (response.data.success && response.data.data) {
|
|
147
|
+
return response.data.data;
|
|
148
|
+
}
|
|
149
|
+
throw new ApiError('Invalid response format');
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Create a new user
|
|
154
|
+
*/
|
|
155
|
+
public async createUser(
|
|
156
|
+
data: CreateUserRequest,
|
|
157
|
+
config?: AxiosRequestConfig
|
|
158
|
+
): Promise<User> {
|
|
159
|
+
const response = await this.client.post<ApiResponse<User>>('/api/users', data, config);
|
|
160
|
+
if (response.data.success && response.data.data) {
|
|
161
|
+
return response.data.data;
|
|
162
|
+
}
|
|
163
|
+
throw new ApiError('Invalid response format');
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Update an existing user
|
|
168
|
+
*/
|
|
169
|
+
public async updateUser(
|
|
170
|
+
id: string,
|
|
171
|
+
data: UpdateUserRequest,
|
|
172
|
+
config?: AxiosRequestConfig
|
|
173
|
+
): Promise<User> {
|
|
174
|
+
const response = await this.client.put<ApiResponse<User>>(`/api/users/${id}`, data, config);
|
|
175
|
+
if (response.data.success && response.data.data) {
|
|
176
|
+
return response.data.data;
|
|
177
|
+
}
|
|
178
|
+
throw new ApiError('Invalid response format');
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Delete a user
|
|
183
|
+
*/
|
|
184
|
+
public async deleteUser(id: string, config?: AxiosRequestConfig): Promise<void> {
|
|
185
|
+
await this.client.delete(`/api/users/${id}`, config);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Get the underlying axios instance for advanced usage
|
|
190
|
+
*/
|
|
191
|
+
public getAxiosInstance(): AxiosInstance {
|
|
192
|
+
return this.client;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Factory function to create an API client
|
|
198
|
+
*/
|
|
199
|
+
export function createApiClient(config: ApiClientConfig): ApiClient {
|
|
200
|
+
return new ApiClient(config);
|
|
201
|
+
}
|