deploy-bbc 1.1.0 → 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/.cspell.json +38 -0
- package/CLAUDE.md +85 -11
- package/README.md +358 -0
- package/cli/package.json +9 -9
- package/cli/src/cli/index.ts +8 -3
- package/cli/src/helpers/generate-docker-compose.ts +1 -1
- package/cli/src/helpers/generate-dockerfile.ts +1 -1
- package/cli/src/helpers/log-next-steps.ts +23 -20
- package/cli/src/helpers/scaffold-project.ts +30 -4
- package/cli/src/installers/auth.ts +1 -1
- package/cli/src/templates/base/.env.example +1 -1
- package/cli/src/templates/base/package.json +2 -1
- package/cli/src/templates/base/src/config/index.ts +1 -1
- package/cli/src/templates/base/src/controllers/user/create-user.controller.ts +84 -0
- package/cli/src/templates/base/src/controllers/user/delete-user.controller.ts +60 -0
- package/cli/src/templates/base/src/controllers/user/get-user.controller.ts +74 -0
- package/cli/src/templates/base/src/controllers/user/get-users.controller.ts +41 -0
- package/cli/src/templates/base/src/controllers/user/update-user.controller.ts +111 -0
- package/cli/src/templates/base/src/models/user.model.ts +83 -0
- package/cli/src/templates/base/src/routes/index.ts +5 -0
- package/cli/src/templates/base/src/routes/user.route.ts +17 -0
- package/cli/src/templates/base/src/types/common/api-response.types.ts +30 -0
- package/cli/src/templates/base/src/types/index.ts +5 -2
- package/cli/src/templates/base/src/types/models/user.types.ts +26 -0
- package/cli/src/templates/base-bun-native/.env.example +1 -1
- package/cli/src/templates/base-bun-native/package.json +2 -1
- package/cli/src/templates/base-bun-native/src/config/index.ts +1 -1
- package/cli/src/templates/base-bun-native/src/controllers/user/create-user.controller.ts +76 -0
- package/cli/src/templates/base-bun-native/src/controllers/user/delete-user.controller.ts +53 -0
- package/cli/src/templates/base-bun-native/src/controllers/user/get-user.controller.ts +67 -0
- package/cli/src/templates/base-bun-native/src/controllers/user/get-users.controller.ts +40 -0
- package/cli/src/templates/base-bun-native/src/controllers/user/update-user.controller.ts +101 -0
- package/cli/src/templates/base-bun-native/src/index.ts +3 -3
- package/cli/src/templates/base-bun-native/src/models/user.model.ts +83 -0
- package/cli/src/templates/base-bun-native/src/routes/index.ts +10 -6
- package/cli/src/templates/base-bun-native/src/routes/user.route.ts +50 -0
- package/cli/src/templates/base-bun-native/src/types/common/api-response.types.ts +30 -0
- package/cli/src/templates/base-bun-native/src/types/index.ts +5 -2
- package/cli/src/templates/base-bun-native/src/types/models/user.types.ts +26 -0
- package/cli/src/templates/base-express/.env.example +1 -1
- package/cli/src/templates/base-express/package.json +2 -1
- package/cli/src/templates/base-express/src/config/index.ts +1 -1
- package/cli/src/templates/base-express/src/controllers/user/create-user.controller.ts +75 -0
- package/cli/src/templates/base-express/src/controllers/user/delete-user.controller.ts +56 -0
- package/cli/src/templates/base-express/src/controllers/user/get-user.controller.ts +70 -0
- package/cli/src/templates/base-express/src/controllers/user/get-users.controller.ts +41 -0
- package/cli/src/templates/base-express/src/controllers/user/update-user.controller.ts +102 -0
- package/cli/src/templates/base-express/src/models/user.model.ts +83 -0
- package/cli/src/templates/base-express/src/routes/index.ts +5 -0
- package/cli/src/templates/base-express/src/routes/user.route.ts +17 -0
- package/cli/src/templates/base-express/src/types/common/api-response.types.ts +30 -0
- package/cli/src/templates/base-express/src/types/index.ts +5 -2
- package/cli/src/templates/base-express/src/types/models/user.types.ts +26 -0
- package/cli/src/utils/parse-name-and-path.ts +18 -2
- package/package.json +3 -3
|
@@ -14,22 +14,31 @@ export function log_next_steps(
|
|
|
14
14
|
options: InstallerOptions,
|
|
15
15
|
_cliResults: CliResults
|
|
16
16
|
): void {
|
|
17
|
-
const { appName, packages } = options;
|
|
17
|
+
const { appName, packages, projectDir } = options;
|
|
18
|
+
|
|
19
|
+
// Check if project was created in current directory
|
|
20
|
+
const isCurrentDirectory = projectDir === process.cwd();
|
|
18
21
|
|
|
19
22
|
console.log("\n" + chalk.bold.green("✨ Project created successfully!"));
|
|
20
23
|
console.log("\n" + chalk.bold("Next steps:"));
|
|
21
24
|
console.log();
|
|
22
25
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
26
|
+
let stepNumber = 1;
|
|
27
|
+
|
|
28
|
+
// Step 1: Navigate to project (skip if current directory)
|
|
29
|
+
if (!isCurrentDirectory) {
|
|
30
|
+
console.log(chalk.cyan(`${stepNumber}.`) + " Navigate to your project:");
|
|
31
|
+
console.log(` ${chalk.gray("cd")} ${appName}`);
|
|
32
|
+
console.log();
|
|
33
|
+
stepNumber++;
|
|
34
|
+
}
|
|
27
35
|
|
|
28
|
-
// Step
|
|
29
|
-
console.log(chalk.cyan(
|
|
36
|
+
// Step: Environment variables
|
|
37
|
+
console.log(chalk.cyan(`${stepNumber}.`) + " Set up environment variables:");
|
|
30
38
|
console.log(` ${chalk.gray("cp")} .env.example .env`);
|
|
31
39
|
console.log(` ${chalk.gray("# Edit .env with your configuration")}`);
|
|
32
40
|
console.log();
|
|
41
|
+
stepNumber++;
|
|
33
42
|
|
|
34
43
|
// Step 3: Docker (if database or redis selected)
|
|
35
44
|
const hasDocker =
|
|
@@ -39,9 +48,10 @@ export function log_next_steps(
|
|
|
39
48
|
packages.includes(AvailablePackages.redis);
|
|
40
49
|
|
|
41
50
|
if (hasDocker) {
|
|
42
|
-
console.log(chalk.cyan(
|
|
51
|
+
console.log(chalk.cyan(`${stepNumber}.`) + " Start Docker services:");
|
|
43
52
|
console.log(` ${chalk.gray("docker-compose up -d")}`);
|
|
44
53
|
console.log();
|
|
54
|
+
stepNumber++;
|
|
45
55
|
}
|
|
46
56
|
|
|
47
57
|
// Step 4: Database migrations (if postgres or mysql)
|
|
@@ -50,21 +60,14 @@ export function log_next_steps(
|
|
|
50
60
|
packages.includes(AvailablePackages.mysql);
|
|
51
61
|
|
|
52
62
|
if (needsMigration) {
|
|
53
|
-
|
|
54
|
-
console.log(chalk.cyan(`${stepNum}.`) + " Run database migrations:");
|
|
63
|
+
console.log(chalk.cyan(`${stepNumber}.`) + " Run database migrations:");
|
|
55
64
|
console.log(` ${chalk.gray("bun run db:migrate")}`);
|
|
56
65
|
console.log();
|
|
66
|
+
stepNumber++;
|
|
57
67
|
}
|
|
58
68
|
|
|
59
|
-
// Step
|
|
60
|
-
|
|
61
|
-
? hasDocker
|
|
62
|
-
? "5"
|
|
63
|
-
: "4"
|
|
64
|
-
: hasDocker
|
|
65
|
-
? "4"
|
|
66
|
-
: "3";
|
|
67
|
-
console.log(chalk.cyan(`${lastStepNum}.`) + " Start the development server:");
|
|
69
|
+
// Step: Start development server
|
|
70
|
+
console.log(chalk.cyan(`${stepNumber}.`) + " Start the development server:");
|
|
68
71
|
console.log(` ${chalk.gray("bun run dev")}`);
|
|
69
72
|
console.log();
|
|
70
73
|
|
|
@@ -74,7 +77,7 @@ export function log_next_steps(
|
|
|
74
77
|
packages.includes(AvailablePackages.scalar)
|
|
75
78
|
) {
|
|
76
79
|
console.log(
|
|
77
|
-
chalk.bold("📚 API Documentation:") + " http://localhost:
|
|
80
|
+
chalk.bold("📚 API Documentation:") + " http://localhost:8000/docs"
|
|
78
81
|
);
|
|
79
82
|
console.log();
|
|
80
83
|
}
|
|
@@ -9,10 +9,14 @@ const __dirname = path.dirname(__filename);
|
|
|
9
9
|
|
|
10
10
|
/**
|
|
11
11
|
* Scaffolds the base project structure by copying template files.
|
|
12
|
-
* Creates the project directory and copies all files from
|
|
12
|
+
* Creates the project directory and copies all files from the selected template.
|
|
13
|
+
*
|
|
14
|
+
* Special handling:
|
|
15
|
+
* - If projectDir is current directory: allows scaffolding but checks for conflicts (src/, package.json)
|
|
16
|
+
* - If projectDir is a new directory: creates it and requires it to be empty
|
|
13
17
|
*
|
|
14
18
|
* @param options - Installer options containing projectDir and other config
|
|
15
|
-
* @throws Error if project directory already exists and is not empty
|
|
19
|
+
* @throws Error if project directory already exists and is not empty, or if conflicts detected in current directory
|
|
16
20
|
*/
|
|
17
21
|
export async function scaffold_project(
|
|
18
22
|
options: InstallerOptions
|
|
@@ -21,8 +25,10 @@ export async function scaffold_project(
|
|
|
21
25
|
const spinner = ora("Scaffolding project...").start();
|
|
22
26
|
|
|
23
27
|
try {
|
|
24
|
-
|
|
25
|
-
|
|
28
|
+
const isCurrentDirectory = projectDir === process.cwd();
|
|
29
|
+
|
|
30
|
+
// Check if directory exists and is not empty (skip for current directory)
|
|
31
|
+
if (!isCurrentDirectory && await fs.pathExists(projectDir)) {
|
|
26
32
|
const files = await fs.readdir(projectDir);
|
|
27
33
|
if (files.length > 0) {
|
|
28
34
|
spinner.fail(`Directory ${projectDir} already exists and is not empty`);
|
|
@@ -32,6 +38,26 @@ export async function scaffold_project(
|
|
|
32
38
|
}
|
|
33
39
|
}
|
|
34
40
|
|
|
41
|
+
// If scaffolding in current directory, check for conflicts
|
|
42
|
+
if (isCurrentDirectory) {
|
|
43
|
+
const conflictingPaths = ["src", "package.json"];
|
|
44
|
+
const conflicts = [];
|
|
45
|
+
|
|
46
|
+
for (const pathToCheck of conflictingPaths) {
|
|
47
|
+
if (await fs.pathExists(path.join(projectDir, pathToCheck))) {
|
|
48
|
+
conflicts.push(pathToCheck);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (conflicts.length > 0) {
|
|
53
|
+
spinner.fail("Cannot scaffold in current directory");
|
|
54
|
+
throw new Error(
|
|
55
|
+
`The following files/folders already exist: ${conflicts.join(", ")}\n` +
|
|
56
|
+
"Please use an empty directory or choose a different location."
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
35
61
|
// Ensure project directory exists
|
|
36
62
|
await fs.ensureDir(projectDir);
|
|
37
63
|
|
|
@@ -76,7 +76,7 @@ async function install_oauth(projectDir: string): Promise<void> {
|
|
|
76
76
|
|
|
77
77
|
await append_env_example(
|
|
78
78
|
projectDir,
|
|
79
|
-
"\n# OAuth Configuration\nOAUTH_CLIENT_ID=your-client-id\nOAUTH_CLIENT_SECRET=your-client-secret\nOAUTH_CALLBACK_URL=http://localhost:
|
|
79
|
+
"\n# OAuth Configuration\nOAUTH_CLIENT_ID=your-client-id\nOAUTH_CLIENT_SECRET=your-client-secret\nOAUTH_CALLBACK_URL=http://localhost:8000/auth/callback\n"
|
|
80
80
|
);
|
|
81
81
|
}
|
|
82
82
|
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import type { Context } from "hono";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { user_model } from "../../models/user.model.js";
|
|
4
|
+
import type { ApiResponse, ApiError } from "../../types/common/api-response.types.js";
|
|
5
|
+
import type { UserResponse } from "../../types/models/user.types.js";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Validation schema
|
|
9
|
+
*/
|
|
10
|
+
const create_user_schema = z.object({
|
|
11
|
+
email: z.string().email("Invalid email address"),
|
|
12
|
+
name: z.string().min(2, "Name must be at least 2 characters").max(100, "Name must not exceed 100 characters"),
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Helper to format user response
|
|
17
|
+
*/
|
|
18
|
+
function format_user_response(user: any): UserResponse {
|
|
19
|
+
return {
|
|
20
|
+
id: user.id,
|
|
21
|
+
email: user.email,
|
|
22
|
+
name: user.name,
|
|
23
|
+
createdAt: user.createdAt.toISOString(),
|
|
24
|
+
updatedAt: user.updatedAt.toISOString(),
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Create a new user
|
|
30
|
+
* POST /users
|
|
31
|
+
*/
|
|
32
|
+
export async function create_user(c: Context) {
|
|
33
|
+
try {
|
|
34
|
+
const body = await c.req.json();
|
|
35
|
+
|
|
36
|
+
// Validate request body
|
|
37
|
+
const validation = create_user_schema.safeParse(body);
|
|
38
|
+
if (!validation.success) {
|
|
39
|
+
return c.json<ApiError>(
|
|
40
|
+
{
|
|
41
|
+
success: false,
|
|
42
|
+
error: "Validation failed",
|
|
43
|
+
details: validation.error.errors.map((err) => ({
|
|
44
|
+
field: err.path.join("."),
|
|
45
|
+
message: err.message,
|
|
46
|
+
})),
|
|
47
|
+
},
|
|
48
|
+
400
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Check if email already exists
|
|
53
|
+
const existing_user = await user_model.find_by_email(validation.data.email);
|
|
54
|
+
if (existing_user) {
|
|
55
|
+
return c.json<ApiError>(
|
|
56
|
+
{
|
|
57
|
+
success: false,
|
|
58
|
+
error: "User with this email already exists",
|
|
59
|
+
},
|
|
60
|
+
409
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Create user
|
|
65
|
+
const user = await user_model.create(validation.data);
|
|
66
|
+
|
|
67
|
+
return c.json<ApiResponse<UserResponse>>(
|
|
68
|
+
{
|
|
69
|
+
success: true,
|
|
70
|
+
data: format_user_response(user),
|
|
71
|
+
message: "User created successfully",
|
|
72
|
+
},
|
|
73
|
+
201
|
|
74
|
+
);
|
|
75
|
+
} catch (error) {
|
|
76
|
+
return c.json<ApiError>(
|
|
77
|
+
{
|
|
78
|
+
success: false,
|
|
79
|
+
error: "Failed to create user",
|
|
80
|
+
},
|
|
81
|
+
500
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import type { Context } from "hono";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { user_model } from "../../models/user.model.js";
|
|
4
|
+
import type { ApiResponse, ApiError } from "../../types/common/api-response.types.js";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Validation schema
|
|
8
|
+
*/
|
|
9
|
+
const user_id_schema = z.string().uuid("Invalid user ID format");
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Delete user by ID
|
|
13
|
+
* DELETE /users/:id
|
|
14
|
+
*/
|
|
15
|
+
export async function delete_user(c: Context) {
|
|
16
|
+
try {
|
|
17
|
+
const id = c.req.param("id");
|
|
18
|
+
|
|
19
|
+
// Validate ID format
|
|
20
|
+
const validation = user_id_schema.safeParse(id);
|
|
21
|
+
if (!validation.success) {
|
|
22
|
+
return c.json<ApiError>(
|
|
23
|
+
{
|
|
24
|
+
success: false,
|
|
25
|
+
error: "Invalid user ID",
|
|
26
|
+
details: validation.error.errors.map((err) => ({
|
|
27
|
+
field: err.path.join("."),
|
|
28
|
+
message: err.message,
|
|
29
|
+
})),
|
|
30
|
+
},
|
|
31
|
+
400
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const deleted = await user_model.delete(id);
|
|
36
|
+
|
|
37
|
+
if (!deleted) {
|
|
38
|
+
return c.json<ApiError>(
|
|
39
|
+
{
|
|
40
|
+
success: false,
|
|
41
|
+
error: "User not found",
|
|
42
|
+
},
|
|
43
|
+
404
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return c.json<ApiResponse>({
|
|
48
|
+
success: true,
|
|
49
|
+
message: "User deleted successfully",
|
|
50
|
+
});
|
|
51
|
+
} catch (error) {
|
|
52
|
+
return c.json<ApiError>(
|
|
53
|
+
{
|
|
54
|
+
success: false,
|
|
55
|
+
error: "Failed to delete user",
|
|
56
|
+
},
|
|
57
|
+
500
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import type { Context } from "hono";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { user_model } from "../../models/user.model.js";
|
|
4
|
+
import type { ApiResponse, ApiError } from "../../types/common/api-response.types.js";
|
|
5
|
+
import type { UserResponse } from "../../types/models/user.types.js";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Validation schema
|
|
9
|
+
*/
|
|
10
|
+
const user_id_schema = z.string().uuid("Invalid user ID format");
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Helper to format user response
|
|
14
|
+
*/
|
|
15
|
+
function format_user_response(user: any): UserResponse {
|
|
16
|
+
return {
|
|
17
|
+
id: user.id,
|
|
18
|
+
email: user.email,
|
|
19
|
+
name: user.name,
|
|
20
|
+
createdAt: user.createdAt.toISOString(),
|
|
21
|
+
updatedAt: user.updatedAt.toISOString(),
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Get user by ID
|
|
27
|
+
* GET /users/:id
|
|
28
|
+
*/
|
|
29
|
+
export async function get_user(c: Context) {
|
|
30
|
+
try {
|
|
31
|
+
const id = c.req.param("id");
|
|
32
|
+
|
|
33
|
+
// Validate ID format
|
|
34
|
+
const validation = user_id_schema.safeParse(id);
|
|
35
|
+
if (!validation.success) {
|
|
36
|
+
return c.json<ApiError>(
|
|
37
|
+
{
|
|
38
|
+
success: false,
|
|
39
|
+
error: "Invalid user ID",
|
|
40
|
+
details: validation.error.errors.map((err) => ({
|
|
41
|
+
field: err.path.join("."),
|
|
42
|
+
message: err.message,
|
|
43
|
+
})),
|
|
44
|
+
},
|
|
45
|
+
400
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const user = await user_model.find_by_id(id);
|
|
50
|
+
|
|
51
|
+
if (!user) {
|
|
52
|
+
return c.json<ApiError>(
|
|
53
|
+
{
|
|
54
|
+
success: false,
|
|
55
|
+
error: "User not found",
|
|
56
|
+
},
|
|
57
|
+
404
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return c.json<ApiResponse<UserResponse>>({
|
|
62
|
+
success: true,
|
|
63
|
+
data: format_user_response(user),
|
|
64
|
+
});
|
|
65
|
+
} catch (error) {
|
|
66
|
+
return c.json<ApiError>(
|
|
67
|
+
{
|
|
68
|
+
success: false,
|
|
69
|
+
error: "Failed to fetch user",
|
|
70
|
+
},
|
|
71
|
+
500
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { Context } from "hono";
|
|
2
|
+
import { user_model } from "../../models/user.model.js";
|
|
3
|
+
import type { ApiResponse, ApiError } from "../../types/common/api-response.types.js";
|
|
4
|
+
import type { UserResponse } from "../../types/models/user.types.js";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Helper to format user response
|
|
8
|
+
*/
|
|
9
|
+
function format_user_response(user: any): UserResponse {
|
|
10
|
+
return {
|
|
11
|
+
id: user.id,
|
|
12
|
+
email: user.email,
|
|
13
|
+
name: user.name,
|
|
14
|
+
createdAt: user.createdAt.toISOString(),
|
|
15
|
+
updatedAt: user.updatedAt.toISOString(),
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Get all users
|
|
21
|
+
* GET /users
|
|
22
|
+
*/
|
|
23
|
+
export async function get_users(c: Context) {
|
|
24
|
+
try {
|
|
25
|
+
const users = await user_model.find_all();
|
|
26
|
+
const formatted_users = users.map(format_user_response);
|
|
27
|
+
|
|
28
|
+
return c.json<ApiResponse<UserResponse[]>>({
|
|
29
|
+
success: true,
|
|
30
|
+
data: formatted_users,
|
|
31
|
+
});
|
|
32
|
+
} catch (error) {
|
|
33
|
+
return c.json<ApiError>(
|
|
34
|
+
{
|
|
35
|
+
success: false,
|
|
36
|
+
error: "Failed to fetch users",
|
|
37
|
+
},
|
|
38
|
+
500
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import type { Context } from "hono";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { user_model } from "../../models/user.model.js";
|
|
4
|
+
import type { ApiResponse, ApiError } from "../../types/common/api-response.types.js";
|
|
5
|
+
import type { UserResponse } from "../../types/models/user.types.js";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Validation schemas
|
|
9
|
+
*/
|
|
10
|
+
const user_id_schema = z.string().uuid("Invalid user ID format");
|
|
11
|
+
const update_user_schema = z.object({
|
|
12
|
+
email: z.string().email("Invalid email address").optional(),
|
|
13
|
+
name: z.string().min(2, "Name must be at least 2 characters").max(100, "Name must not exceed 100 characters").optional(),
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Helper to format user response
|
|
18
|
+
*/
|
|
19
|
+
function format_user_response(user: any): UserResponse {
|
|
20
|
+
return {
|
|
21
|
+
id: user.id,
|
|
22
|
+
email: user.email,
|
|
23
|
+
name: user.name,
|
|
24
|
+
createdAt: user.createdAt.toISOString(),
|
|
25
|
+
updatedAt: user.updatedAt.toISOString(),
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Update user by ID
|
|
31
|
+
* PATCH /users/:id
|
|
32
|
+
*/
|
|
33
|
+
export async function update_user(c: Context) {
|
|
34
|
+
try {
|
|
35
|
+
const id = c.req.param("id");
|
|
36
|
+
const body = await c.req.json();
|
|
37
|
+
|
|
38
|
+
// Validate ID format
|
|
39
|
+
const id_validation = user_id_schema.safeParse(id);
|
|
40
|
+
if (!id_validation.success) {
|
|
41
|
+
return c.json<ApiError>(
|
|
42
|
+
{
|
|
43
|
+
success: false,
|
|
44
|
+
error: "Invalid user ID",
|
|
45
|
+
details: id_validation.error.errors.map((err) => ({
|
|
46
|
+
field: err.path.join("."),
|
|
47
|
+
message: err.message,
|
|
48
|
+
})),
|
|
49
|
+
},
|
|
50
|
+
400
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Validate request body
|
|
55
|
+
const body_validation = update_user_schema.safeParse(body);
|
|
56
|
+
if (!body_validation.success) {
|
|
57
|
+
return c.json<ApiError>(
|
|
58
|
+
{
|
|
59
|
+
success: false,
|
|
60
|
+
error: "Validation failed",
|
|
61
|
+
details: body_validation.error.errors.map((err) => ({
|
|
62
|
+
field: err.path.join("."),
|
|
63
|
+
message: err.message,
|
|
64
|
+
})),
|
|
65
|
+
},
|
|
66
|
+
400
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Check if email is being updated and if it already exists
|
|
71
|
+
if (body_validation.data.email) {
|
|
72
|
+
const existing_user = await user_model.find_by_email(body_validation.data.email);
|
|
73
|
+
if (existing_user && existing_user.id !== id) {
|
|
74
|
+
return c.json<ApiError>(
|
|
75
|
+
{
|
|
76
|
+
success: false,
|
|
77
|
+
error: "User with this email already exists",
|
|
78
|
+
},
|
|
79
|
+
409
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Update user
|
|
85
|
+
const user = await user_model.update(id, body_validation.data);
|
|
86
|
+
|
|
87
|
+
if (!user) {
|
|
88
|
+
return c.json<ApiError>(
|
|
89
|
+
{
|
|
90
|
+
success: false,
|
|
91
|
+
error: "User not found",
|
|
92
|
+
},
|
|
93
|
+
404
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return c.json<ApiResponse<UserResponse>>({
|
|
98
|
+
success: true,
|
|
99
|
+
data: format_user_response(user),
|
|
100
|
+
message: "User updated successfully",
|
|
101
|
+
});
|
|
102
|
+
} catch (error) {
|
|
103
|
+
return c.json<ApiError>(
|
|
104
|
+
{
|
|
105
|
+
success: false,
|
|
106
|
+
error: "Failed to update user",
|
|
107
|
+
},
|
|
108
|
+
500
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import type { User, CreateUserInput, UpdateUserInput } from "../types/models/user.types.js";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* User model
|
|
5
|
+
*
|
|
6
|
+
* This is a simple in-memory implementation.
|
|
7
|
+
* Replace with your database ORM (Drizzle, Prisma, etc.) in production.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
// In-memory storage (for demonstration)
|
|
11
|
+
const users: User[] = [];
|
|
12
|
+
|
|
13
|
+
export const user_model = {
|
|
14
|
+
/**
|
|
15
|
+
* Find all users
|
|
16
|
+
*/
|
|
17
|
+
find_all: async (): Promise<User[]> => {
|
|
18
|
+
return users;
|
|
19
|
+
},
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Find user by ID
|
|
23
|
+
*/
|
|
24
|
+
find_by_id: async (id: string): Promise<User | undefined> => {
|
|
25
|
+
return users.find((user) => user.id === id);
|
|
26
|
+
},
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Find user by email
|
|
30
|
+
*/
|
|
31
|
+
find_by_email: async (email: string): Promise<User | undefined> => {
|
|
32
|
+
return users.find((user) => user.email === email);
|
|
33
|
+
},
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Create a new user
|
|
37
|
+
*/
|
|
38
|
+
create: async (input: CreateUserInput): Promise<User> => {
|
|
39
|
+
const user: User = {
|
|
40
|
+
id: crypto.randomUUID(),
|
|
41
|
+
email: input.email,
|
|
42
|
+
name: input.name,
|
|
43
|
+
createdAt: new Date(),
|
|
44
|
+
updatedAt: new Date(),
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
users.push(user);
|
|
48
|
+
return user;
|
|
49
|
+
},
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Update user by ID
|
|
53
|
+
*/
|
|
54
|
+
update: async (id: string, input: UpdateUserInput): Promise<User | undefined> => {
|
|
55
|
+
const index = users.findIndex((user) => user.id === id);
|
|
56
|
+
|
|
57
|
+
if (index === -1) {
|
|
58
|
+
return undefined;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
users[index] = {
|
|
62
|
+
...users[index],
|
|
63
|
+
...input,
|
|
64
|
+
updatedAt: new Date(),
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
return users[index];
|
|
68
|
+
},
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Delete user by ID
|
|
72
|
+
*/
|
|
73
|
+
delete: async (id: string): Promise<boolean> => {
|
|
74
|
+
const index = users.findIndex((user) => user.id === id);
|
|
75
|
+
|
|
76
|
+
if (index === -1) {
|
|
77
|
+
return false;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
users.splice(index, 1);
|
|
81
|
+
return true;
|
|
82
|
+
},
|
|
83
|
+
};
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { Hono } from "hono";
|
|
2
|
+
import user_route from "./user.route.js";
|
|
2
3
|
|
|
3
4
|
const routes = new Hono();
|
|
4
5
|
|
|
6
|
+
// Root route
|
|
5
7
|
routes.get("/", (c) => {
|
|
6
8
|
return c.json({
|
|
7
9
|
message: "Welcome to your Bun backend!",
|
|
@@ -9,4 +11,7 @@ routes.get("/", (c) => {
|
|
|
9
11
|
});
|
|
10
12
|
});
|
|
11
13
|
|
|
14
|
+
// User routes
|
|
15
|
+
routes.route("/users", user_route);
|
|
16
|
+
|
|
12
17
|
export default routes;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { Hono } from "hono";
|
|
2
|
+
import { get_users } from "../controllers/user/get-users.controller.js";
|
|
3
|
+
import { get_user } from "../controllers/user/get-user.controller.js";
|
|
4
|
+
import { create_user } from "../controllers/user/create-user.controller.js";
|
|
5
|
+
import { update_user } from "../controllers/user/update-user.controller.js";
|
|
6
|
+
import { delete_user } from "../controllers/user/delete-user.controller.js";
|
|
7
|
+
|
|
8
|
+
const user_route = new Hono();
|
|
9
|
+
|
|
10
|
+
// User CRUD routes
|
|
11
|
+
user_route.get("/", get_users);
|
|
12
|
+
user_route.get("/:id", get_user);
|
|
13
|
+
user_route.post("/", create_user);
|
|
14
|
+
user_route.patch("/:id", update_user);
|
|
15
|
+
user_route.delete("/:id", delete_user);
|
|
16
|
+
|
|
17
|
+
export default user_route;
|