phos 1.0.3 → 1.2.0

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.
Files changed (38) hide show
  1. package/README.md +3 -1
  2. package/package.json +1 -1
  3. package/src/templates/backend/elysia/src/api/user_api.ts +313 -0
  4. package/src/templates/backend/elysia/src/function/helper.ts +111 -0
  5. package/src/templates/backend/elysia/src/service/user_service.ts +109 -0
  6. package/src/templates/backend/elysia/src/sql/user_sql.ts +191 -0
  7. package/src/templates/backend/elysia/src/types/user_type.ts +54 -0
  8. package/src/templates/backend/fastapi/src/api/user_api.py +163 -0
  9. package/src/templates/backend/fastapi/src/function/__init__.py +0 -0
  10. package/src/templates/backend/fastapi/src/function/helper.py +107 -0
  11. package/src/templates/backend/fastapi/src/service/user_service.py +94 -0
  12. package/src/templates/backend/fastapi/src/sql/user_sql.py +197 -0
  13. package/src/templates/backend/fastapi/src/types/user_type.py +64 -0
  14. package/src/templates/frontend/vue/.editorconfig +8 -0
  15. package/src/templates/frontend/vue/.gitattributes +1 -0
  16. package/src/templates/frontend/vue/.oxlintrc.json +10 -0
  17. package/src/templates/frontend/vue/.prettierrc.json +6 -0
  18. package/src/templates/frontend/vue/.vscode/extensions.json +11 -0
  19. package/src/templates/frontend/vue/README.md +73 -0
  20. package/src/templates/frontend/vue/e2e/tsconfig.json +4 -0
  21. package/src/templates/frontend/vue/e2e/vue.spec.ts +8 -0
  22. package/src/templates/frontend/vue/env.d.ts +1 -0
  23. package/src/templates/frontend/vue/eslint.config.ts +38 -0
  24. package/src/templates/frontend/vue/index.html +13 -0
  25. package/src/templates/frontend/vue/package.json +54 -0
  26. package/src/templates/frontend/vue/playwright.config.ts +110 -0
  27. package/src/templates/frontend/vue/public/favicon.ico +0 -0
  28. package/src/templates/frontend/vue/src/App.vue +11 -0
  29. package/src/templates/frontend/vue/src/__tests__/App.spec.ts +11 -0
  30. package/src/templates/frontend/vue/src/main.ts +12 -0
  31. package/src/templates/frontend/vue/src/router/index.ts +8 -0
  32. package/src/templates/frontend/vue/src/stores/counter.ts +12 -0
  33. package/src/templates/frontend/vue/tsconfig.app.json +12 -0
  34. package/src/templates/frontend/vue/tsconfig.json +14 -0
  35. package/src/templates/frontend/vue/tsconfig.node.json +19 -0
  36. package/src/templates/frontend/vue/tsconfig.vitest.json +11 -0
  37. package/src/templates/frontend/vue/vite.config.ts +20 -0
  38. package/src/templates/frontend/vue/vitest.config.ts +14 -0
package/README.md CHANGED
@@ -10,7 +10,7 @@ Full-stack interactive project generator CLI
10
10
  [![Node.js Version](https://img.shields.io/node/v/phos)](https://nodejs.org)
11
11
  [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg)](http://makeapullrequest.com)
12
12
 
13
- **Version 1.0.3 - Production Release** 🎉
13
+ **Version 1.2.0 - Production Release** 🎉
14
14
 
15
15
  [View on npmjs.com](https://www.npmjs.com/package/phos)
16
16
 
@@ -278,6 +278,8 @@ Contributions are welcome! Please feel free to submit a Pull Request.
278
278
 
279
279
  Phos follows [semantic versioning](https://semver.org/).
280
280
 
281
+ - **1.2.0** - Minor release (backend API architecture)
282
+ - **1.1.0** - Minor release (new features, non-breaking changes)
281
283
  - **1.0.3** - Patch release (template path fix)
282
284
  - **1.0.2** - Patch release (template missing fix)
283
285
  - **1.0.1** - Patch release (bug fixes)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "phos",
3
- "version": "1.0.3",
3
+ "version": "1.2.0",
4
4
  "description": "Full-stack interactive project generator CLI - Phos",
5
5
  "type": "module",
6
6
  "bin": {
@@ -0,0 +1,313 @@
1
+ import { Elysia, t } from "elysia";
2
+ import { userService } from "../service/user_service";
3
+ import { validateString, validateCreateUserDto, validateUpdateUserDto } from "../function/helper";
4
+ import type { ApiResponse, UserResponse } from "../types/user_type";
5
+
6
+ export const userApi = new Elysia({ prefix: "/users" })
7
+ .get(
8
+ "/",
9
+ async () => {
10
+ try {
11
+ const users = await userService.getAllUsers();
12
+ return {
13
+ success: true,
14
+ data: users,
15
+ message: "Users retrieved successfully",
16
+ } as ApiResponse<UserResponse[]>;
17
+ } catch (error) {
18
+ throw {
19
+ success: false,
20
+ error: error instanceof Error ? error.message : String(error),
21
+ } as ApiResponse<never>;
22
+ }
23
+ },
24
+ {
25
+ detail: {
26
+ tags: ["Users"],
27
+ summary: "Get all users",
28
+ description: "Retrieve all active users from the database",
29
+ },
30
+ }
31
+ )
32
+
33
+ .get(
34
+ "/:id",
35
+ async ({ params, set }) => {
36
+ try {
37
+ const id = parseInt(params.id);
38
+ if (isNaN(id) || id <= 0) {
39
+ set.status = 400;
40
+ throw new Error("Invalid user ID");
41
+ }
42
+
43
+ const user = await userService.getUserById(id);
44
+ if (!user) {
45
+ set.status = 404;
46
+ throw new Error("User not found");
47
+ }
48
+
49
+ return {
50
+ success: true,
51
+ data: user,
52
+ message: "User retrieved successfully",
53
+ } as ApiResponse<UserResponse>;
54
+ } catch (error) {
55
+ throw {
56
+ success: false,
57
+ error: error instanceof Error ? error.message : String(error),
58
+ } as ApiResponse<never>;
59
+ }
60
+ },
61
+ {
62
+ params: t.Object({
63
+ id: t.String(),
64
+ }),
65
+ detail: {
66
+ tags: ["Users"],
67
+ summary: "Get user by ID",
68
+ description: "Retrieve a specific user by their ID",
69
+ },
70
+ }
71
+ )
72
+
73
+ .get(
74
+ "/search/:keyword",
75
+ async ({ params }) => {
76
+ try {
77
+ const keyword = params.keyword;
78
+ if (!validateString(keyword, 1, 100)) {
79
+ throw new Error("Search keyword must be between 1 and 100 characters");
80
+ }
81
+
82
+ const users = await userService.searchUsers(keyword);
83
+ return {
84
+ success: true,
85
+ data: users,
86
+ message: "Users retrieved successfully",
87
+ } as ApiResponse<UserResponse[]>;
88
+ } catch (error) {
89
+ throw {
90
+ success: false,
91
+ error: error instanceof Error ? error.message : String(error),
92
+ } as ApiResponse<never>;
93
+ }
94
+ },
95
+ {
96
+ params: t.Object({
97
+ keyword: t.String(),
98
+ }),
99
+ detail: {
100
+ tags: ["Users"],
101
+ summary: "Search users",
102
+ description: "Search for users by email, username, or full name",
103
+ },
104
+ }
105
+ )
106
+
107
+ .post(
108
+ "/",
109
+ async ({ body, set }) => {
110
+ try {
111
+ const validatedData = validateCreateUserDto(body);
112
+ const user = await userService.createUser(validatedData);
113
+ set.status = 201;
114
+ return {
115
+ success: true,
116
+ data: user,
117
+ message: "User created successfully",
118
+ } as ApiResponse<UserResponse>;
119
+ } catch (error) {
120
+ const errorMessage = error instanceof Error ? error.message : String(error);
121
+ if (errorMessage.includes("already exists") || errorMessage.includes("already taken")) {
122
+ set.status = 409;
123
+ } else if (errorMessage.includes("Validation error")) {
124
+ set.status = 400;
125
+ } else {
126
+ set.status = 400;
127
+ }
128
+ throw {
129
+ success: false,
130
+ error: errorMessage,
131
+ } as ApiResponse<never>;
132
+ }
133
+ },
134
+ {
135
+ body: t.Object({
136
+ email: t.String(),
137
+ username: t.String(),
138
+ full_name: t.String(),
139
+ password: t.String(),
140
+ avatar_url: t.Optional(t.String()),
141
+ bio: t.Optional(t.String()),
142
+ role: t.Optional(t.String()),
143
+ }),
144
+ detail: {
145
+ tags: ["Users"],
146
+ summary: "Create a new user",
147
+ description: "Create a new user with the provided data",
148
+ },
149
+ }
150
+ )
151
+
152
+ .put(
153
+ "/:id",
154
+ async ({ params, body, set }) => {
155
+ try {
156
+ const id = parseInt(params.id);
157
+ if (isNaN(id) || id <= 0) {
158
+ set.status = 400;
159
+ throw new Error("Invalid user ID");
160
+ }
161
+
162
+ const validatedData = validateUpdateUserDto(body);
163
+ const user = await userService.updateUser(id, validatedData);
164
+ if (!user) {
165
+ set.status = 404;
166
+ throw new Error("User not found");
167
+ }
168
+
169
+ return {
170
+ success: true,
171
+ data: user,
172
+ message: "User updated successfully",
173
+ } as ApiResponse<UserResponse>;
174
+ } catch (error) {
175
+ const errorMessage = error instanceof Error ? error.message : String(error);
176
+ if (errorMessage.includes("already in use") || errorMessage.includes("already taken")) {
177
+ set.status = 409;
178
+ } else if (errorMessage.includes("not found")) {
179
+ set.status = 404;
180
+ } else if (errorMessage.includes("Validation error")) {
181
+ set.status = 400;
182
+ } else {
183
+ set.status = 400;
184
+ }
185
+ throw {
186
+ success: false,
187
+ error: errorMessage,
188
+ } as ApiResponse<never>;
189
+ }
190
+ },
191
+ {
192
+ params: t.Object({
193
+ id: t.String(),
194
+ }),
195
+ body: t.Object({
196
+ email: t.Optional(t.String()),
197
+ username: t.Optional(t.String()),
198
+ full_name: t.Optional(t.String()),
199
+ password: t.Optional(t.String()),
200
+ avatar_url: t.Optional(t.String()),
201
+ bio: t.Optional(t.String()),
202
+ role: t.Optional(t.String()),
203
+ is_active: t.Optional(t.Boolean()),
204
+ }),
205
+ detail: {
206
+ tags: ["Users"],
207
+ summary: "Update a user",
208
+ description: "Update an existing user's information",
209
+ },
210
+ }
211
+ )
212
+
213
+ .delete(
214
+ "/:id",
215
+ async ({ params, set }) => {
216
+ try {
217
+ const id = parseInt(params.id);
218
+ if (isNaN(id) || id <= 0) {
219
+ set.status = 400;
220
+ throw new Error("Invalid user ID");
221
+ }
222
+
223
+ const deleted = await userService.deleteUser(id);
224
+ if (!deleted) {
225
+ set.status = 404;
226
+ throw new Error("User not found");
227
+ }
228
+
229
+ return {
230
+ success: true,
231
+ message: "User deleted successfully",
232
+ } as ApiResponse<never>;
233
+ } catch (error) {
234
+ const errorMessage = error instanceof Error ? error.message : String(error);
235
+ if (errorMessage.includes("not found")) {
236
+ set.status = 404;
237
+ } else {
238
+ set.status = 400;
239
+ }
240
+ throw {
241
+ success: false,
242
+ error: errorMessage,
243
+ } as ApiResponse<never>;
244
+ }
245
+ },
246
+ {
247
+ params: t.Object({
248
+ id: t.String(),
249
+ }),
250
+ detail: {
251
+ tags: ["Users"],
252
+ summary: "Delete a user",
253
+ description: "Permanently delete a user from the database",
254
+ },
255
+ }
256
+ )
257
+
258
+ .post(
259
+ "/:id/soft-delete",
260
+ async ({ params, set }) => {
261
+ try {
262
+ const id = parseInt(params.id);
263
+ if (isNaN(id) || id <= 0) {
264
+ set.status = 400;
265
+ throw new Error("Invalid user ID");
266
+ }
267
+
268
+ const user = await userService.softDeleteUser(id);
269
+ if (!user) {
270
+ set.status = 404;
271
+ throw new Error("User not found");
272
+ }
273
+
274
+ return {
275
+ success: true,
276
+ data: user,
277
+ message: "User soft deleted successfully",
278
+ } as ApiResponse<UserResponse>;
279
+ } catch (error) {
280
+ const errorMessage = error instanceof Error ? error.message : String(error);
281
+ if (errorMessage.includes("not found")) {
282
+ set.status = 404;
283
+ } else {
284
+ set.status = 400;
285
+ }
286
+ throw {
287
+ success: false,
288
+ error: errorMessage,
289
+ } as ApiResponse<never>;
290
+ }
291
+ },
292
+ {
293
+ params: t.Object({
294
+ id: t.String(),
295
+ }),
296
+ detail: {
297
+ tags: ["Users"],
298
+ summary: "Soft delete a user",
299
+ description: "Deactivate a user without permanently deleting them",
300
+ },
301
+ }
302
+ )
303
+
304
+ .onError(({ error, set }) => {
305
+ if (error && typeof error === "object" && "success" in error) {
306
+ return error;
307
+ }
308
+ set.status = 500;
309
+ return {
310
+ success: false,
311
+ error: error instanceof Error ? error.message : "Internal server error",
312
+ } as ApiResponse<never>;
313
+ });
@@ -0,0 +1,111 @@
1
+ import type { CreateUserDto, UpdateUserDto } from "../types/user_type";
2
+
3
+ export function validateEmail(email: string): boolean {
4
+ const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
5
+ return emailRegex.test(email);
6
+ }
7
+
8
+ export function validateString(value: any, minLength: number = 1, maxLength: number = 255): boolean {
9
+ return typeof value === "string" && value.length >= minLength && value.length <= maxLength;
10
+ }
11
+
12
+ export function validateBoolean(value: any): boolean {
13
+ return typeof value === "boolean";
14
+ }
15
+
16
+ export function validateCreateUserDto(data: any): CreateUserDto {
17
+ const errors: string[] = [];
18
+
19
+ if (!validateEmail(data.email)) {
20
+ errors.push("Invalid email format");
21
+ }
22
+
23
+ if (!validateString(data.username, 3, 100)) {
24
+ errors.push("Username must be between 3 and 100 characters");
25
+ }
26
+
27
+ if (!validateString(data.full_name, 1, 255)) {
28
+ errors.push("Full name must be between 1 and 255 characters");
29
+ }
30
+
31
+ if (!validateString(data.password, 6, 255)) {
32
+ errors.push("Password must be at least 6 characters");
33
+ }
34
+
35
+ if (data.avatar_url !== undefined && data.avatar_url !== null && !validateString(data.avatar_url, 1, 1000)) {
36
+ errors.push("Avatar URL must be a valid string");
37
+ }
38
+
39
+ if (data.bio !== undefined && data.bio !== null && !validateString(data.bio, 1, 1000)) {
40
+ errors.push("Bio must be a valid string");
41
+ }
42
+
43
+ if (data.role !== undefined && data.role !== null && !validateString(data.role, 1, 50)) {
44
+ errors.push("Role must be a valid string");
45
+ }
46
+
47
+ if (errors.length > 0) {
48
+ throw new Error(`Validation error: ${errors.join(", ")}`);
49
+ }
50
+
51
+ return {
52
+ email: data.email,
53
+ username: data.username,
54
+ full_name: data.full_name,
55
+ password: data.password,
56
+ avatar_url: data.avatar_url ?? null,
57
+ bio: data.bio ?? null,
58
+ role: data.role ?? "user",
59
+ };
60
+ }
61
+
62
+ export function validateUpdateUserDto(data: any): UpdateUserDto {
63
+ const errors: string[] = [];
64
+
65
+ if (data.email !== undefined && !validateEmail(data.email)) {
66
+ errors.push("Invalid email format");
67
+ }
68
+
69
+ if (data.username !== undefined && !validateString(data.username, 3, 100)) {
70
+ errors.push("Username must be between 3 and 100 characters");
71
+ }
72
+
73
+ if (data.full_name !== undefined && !validateString(data.full_name, 1, 255)) {
74
+ errors.push("Full name must be between 1 and 255 characters");
75
+ }
76
+
77
+ if (data.password !== undefined && !validateString(data.password, 6, 255)) {
78
+ errors.push("Password must be at least 6 characters");
79
+ }
80
+
81
+ if (data.avatar_url !== undefined && !validateString(data.avatar_url, 1, 1000) && data.avatar_url !== null) {
82
+ errors.push("Avatar URL must be a valid string or null");
83
+ }
84
+
85
+ if (data.bio !== undefined && !validateString(data.bio, 1, 1000) && data.bio !== null) {
86
+ errors.push("Bio must be a valid string or null");
87
+ }
88
+
89
+ if (data.role !== undefined && !validateString(data.role, 1, 50)) {
90
+ errors.push("Role must be a valid string");
91
+ }
92
+
93
+ if (data.is_active !== undefined && !validateBoolean(data.is_active)) {
94
+ errors.push("is_active must be a boolean");
95
+ }
96
+
97
+ if (errors.length > 0) {
98
+ throw new Error(`Validation error: ${errors.join(", ")}`);
99
+ }
100
+
101
+ return {
102
+ email: data.email,
103
+ username: data.username,
104
+ full_name: data.full_name,
105
+ password: data.password,
106
+ avatar_url: data.avatar_url,
107
+ bio: data.bio,
108
+ role: data.role,
109
+ is_active: data.is_active,
110
+ };
111
+ }
@@ -0,0 +1,109 @@
1
+ import * as UserSQL from "../sql/user_sql";
2
+ import type { User, CreateUserDto, UpdateUserDto, UserResponse } from "../types/user_type";
3
+
4
+ export class UserService {
5
+ async getAllUsers(): Promise<UserResponse[]> {
6
+ const users = await UserSQL.getUsers();
7
+ return users.map(this.mapToResponse);
8
+ }
9
+
10
+ async getUserById(id: number): Promise<UserResponse | null> {
11
+ const user = await UserSQL.getUserById(id);
12
+ if (!user) return null;
13
+ return this.mapToResponse(user);
14
+ }
15
+
16
+ async getUserByEmail(email: string): Promise<User | null> {
17
+ return await UserSQL.getUserByEmail(email);
18
+ }
19
+
20
+ async getUserByUsername(username: string): Promise<User | null> {
21
+ return await UserSQL.getUserByUsername(username);
22
+ }
23
+
24
+ async createUser(data: CreateUserDto): Promise<UserResponse> {
25
+ const existingUser = await UserSQL.getUserByEmail(data.email);
26
+ if (existingUser) {
27
+ throw new Error("User with this email already exists");
28
+ }
29
+
30
+ const existingUsername = await UserSQL.getUserByUsername(data.username);
31
+ if (existingUsername) {
32
+ throw new Error("Username already taken");
33
+ }
34
+
35
+ const user = await UserSQL.createUser(data);
36
+ return this.mapToResponse(user);
37
+ }
38
+
39
+ async updateUser(id: number, data: UpdateUserDto): Promise<UserResponse | null> {
40
+ const existingUser = await UserSQL.getUserById(id);
41
+ if (!existingUser) {
42
+ throw new Error("User not found");
43
+ }
44
+
45
+ if (data.email) {
46
+ const emailUser = await UserSQL.getUserByEmail(data.email);
47
+ if (emailUser && emailUser.id !== id) {
48
+ throw new Error("Email already in use");
49
+ }
50
+ }
51
+
52
+ if (data.username) {
53
+ const usernameUser = await UserSQL.getUserByUsername(data.username);
54
+ if (usernameUser && usernameUser.id !== id) {
55
+ throw new Error("Username already taken");
56
+ }
57
+ }
58
+
59
+ const filteredData = Object.fromEntries(
60
+ Object.entries(data).filter(([_, value]) => value !== undefined)
61
+ ) as UpdateUserDto;
62
+
63
+ const user = await UserSQL.updateUser(id, filteredData);
64
+ if (!user) return null;
65
+ return this.mapToResponse(user);
66
+ }
67
+
68
+ async deleteUser(id: number): Promise<boolean> {
69
+ const existingUser = await UserSQL.getUserById(id);
70
+ if (!existingUser) {
71
+ throw new Error("User not found");
72
+ }
73
+
74
+ return await UserSQL.deleteUser(id);
75
+ }
76
+
77
+ async softDeleteUser(id: number): Promise<UserResponse | null> {
78
+ const existingUser = await UserSQL.getUserById(id);
79
+ if (!existingUser) {
80
+ throw new Error("User not found");
81
+ }
82
+
83
+ const user = await UserSQL.softDeleteUser(id);
84
+ if (!user) return null;
85
+ return this.mapToResponse(user);
86
+ }
87
+
88
+ async searchUsers(keyword: string): Promise<UserResponse[]> {
89
+ const users = await UserSQL.searchUsers(keyword);
90
+ return users.map(this.mapToResponse);
91
+ }
92
+
93
+ private mapToResponse(user: User): UserResponse {
94
+ return {
95
+ id: user.id,
96
+ email: user.email,
97
+ username: user.username,
98
+ full_name: user.full_name,
99
+ avatar_url: user.avatar_url,
100
+ bio: user.bio,
101
+ role: user.role,
102
+ is_active: user.is_active,
103
+ created_at: user.created_at,
104
+ updated_at: user.updated_at,
105
+ };
106
+ }
107
+ }
108
+
109
+ export const userService = new UserService();