deploy-bbc 1.0.0 → 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.
- package/CLAUDE.md +85 -11
- package/cli/package.json +9 -9
- package/cli/src/cli/index.ts +8 -3
- package/cli/src/helpers/log-next-steps.ts +22 -19
- package/cli/src/helpers/scaffold-project.ts +30 -4
- package/cli/src/templates/base/package.json +2 -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/package.json +2 -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/package.json +2 -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
package/CLAUDE.md
CHANGED
|
@@ -105,20 +105,86 @@ This document outlines the coding conventions and guidelines for the `create-bac
|
|
|
105
105
|
|
|
106
106
|
---
|
|
107
107
|
|
|
108
|
-
## 📦 Type &
|
|
108
|
+
## 📦 Type Naming & Usage
|
|
109
109
|
|
|
110
|
-
### Use `PascalCase` for types,
|
|
110
|
+
### Use `PascalCase` for types, enums, and classes
|
|
111
111
|
|
|
112
112
|
```typescript
|
|
113
|
-
✅
|
|
113
|
+
✅ type CliResults = { }
|
|
114
114
|
✅ type InstallerOptions = { }
|
|
115
115
|
✅ enum AvailablePackages { }
|
|
116
116
|
✅ class ProjectScaffold { }
|
|
117
117
|
|
|
118
|
-
❌
|
|
118
|
+
❌ type cli_results = { }
|
|
119
119
|
❌ type installer_options = { }
|
|
120
120
|
```
|
|
121
121
|
|
|
122
|
+
### ⚠️ ALWAYS use `type` instead of `interface`
|
|
123
|
+
|
|
124
|
+
```typescript
|
|
125
|
+
✅ type CliResults = {
|
|
126
|
+
appName: string;
|
|
127
|
+
flags: {
|
|
128
|
+
noGit: boolean;
|
|
129
|
+
noInstall: boolean;
|
|
130
|
+
};
|
|
131
|
+
packages: string[];
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
✅ type ScaffoldOptions = {
|
|
135
|
+
projectDir: string;
|
|
136
|
+
appName: string;
|
|
137
|
+
packages: string[];
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
❌ interface CliResults {
|
|
141
|
+
appName: string;
|
|
142
|
+
flags: {
|
|
143
|
+
noGit: boolean;
|
|
144
|
+
noInstall: boolean;
|
|
145
|
+
};
|
|
146
|
+
packages: string[];
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
❌ interface ScaffoldOptions {
|
|
150
|
+
projectDir: string;
|
|
151
|
+
appName: string;
|
|
152
|
+
packages: string[];
|
|
153
|
+
}
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
**Why `type` over `interface`?**
|
|
157
|
+
|
|
158
|
+
- **Consistency**: Single way to define object shapes
|
|
159
|
+
- **Flexibility**: Types support unions, intersections, and mapped types more naturally
|
|
160
|
+
- **Composability**: Better for complex type operations and transformations
|
|
161
|
+
- **Simplicity**: One less concept to remember
|
|
162
|
+
- **Modern practice**: Aligns with contemporary TypeScript patterns
|
|
163
|
+
|
|
164
|
+
**Extending types:**
|
|
165
|
+
|
|
166
|
+
```typescript
|
|
167
|
+
✅ type BaseOptions = {
|
|
168
|
+
projectDir: string;
|
|
169
|
+
appName: string;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
✅ type ExtendedOptions = BaseOptions & {
|
|
173
|
+
packages: string[];
|
|
174
|
+
flags: Record<string, boolean>;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
❌ interface BaseOptions {
|
|
178
|
+
projectDir: string;
|
|
179
|
+
appName: string;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
❌ interface ExtendedOptions extends BaseOptions {
|
|
183
|
+
packages: string[];
|
|
184
|
+
flags: Record<string, boolean>;
|
|
185
|
+
}
|
|
186
|
+
```
|
|
187
|
+
|
|
122
188
|
---
|
|
123
189
|
|
|
124
190
|
## 🗂️ Import/Export Conventions
|
|
@@ -163,7 +229,7 @@ import { install_dependencies } from "./install-dependencies.js";
|
|
|
163
229
|
const DEFAULT_PROJECT_DIR = process.cwd();
|
|
164
230
|
const TEMPLATE_BASE_PATH = "../templates/base";
|
|
165
231
|
|
|
166
|
-
|
|
232
|
+
type ScaffoldOptions = {
|
|
167
233
|
projectDir: string;
|
|
168
234
|
appName: string;
|
|
169
235
|
packages: string[];
|
|
@@ -247,7 +313,9 @@ To enforce these conventions, configure ESLint:
|
|
|
247
313
|
"selector": "typeLike",
|
|
248
314
|
"format": ["PascalCase"]
|
|
249
315
|
}
|
|
250
|
-
]
|
|
316
|
+
],
|
|
317
|
+
"@typescript-eslint/consistent-type-definitions": ["error", "type"],
|
|
318
|
+
"@typescript-eslint/no-empty-interface": "error"
|
|
251
319
|
}
|
|
252
320
|
}
|
|
253
321
|
```
|
|
@@ -262,6 +330,7 @@ To enforce these conventions, configure ESLint:
|
|
|
262
330
|
❌ function createProject() { } // camelCase function
|
|
263
331
|
❌ const project_dir = ""; // snake_case variable
|
|
264
332
|
❌ src/CreateProject.ts // PascalCase file
|
|
333
|
+
❌ interface CliResults { } // Using interface
|
|
265
334
|
```
|
|
266
335
|
|
|
267
336
|
### ✅ Do stick to the rules
|
|
@@ -270,6 +339,7 @@ To enforce these conventions, configure ESLint:
|
|
|
270
339
|
✅ function create_project() { } // snake_case function
|
|
271
340
|
✅ const projectDir = ""; // camelCase variable
|
|
272
341
|
✅ src/create-project.ts // kebab-case file
|
|
342
|
+
✅ type CliResults = { } // Using type
|
|
273
343
|
```
|
|
274
344
|
|
|
275
345
|
---
|
|
@@ -283,9 +353,10 @@ To enforce these conventions, configure ESLint:
|
|
|
283
353
|
| **Functions** | `snake_case` | `function create_project()` |
|
|
284
354
|
| **Variables** | `camelCase` | `const projectDir` |
|
|
285
355
|
| **Constants** | `SCREAMING_SNAKE_CASE` | `const MAX_RETRIES` |
|
|
286
|
-
| **Types
|
|
356
|
+
| **Types** | `PascalCase` + `type` keyword | `type CliResults = { }` |
|
|
287
357
|
| **Enums** | `PascalCase` | `enum AvailablePackages` |
|
|
288
358
|
| **Classes** | `PascalCase` | `class ProjectScaffold` |
|
|
359
|
+
| **Object Shapes** | Use `type`, NOT `interface` | `type Config = { }` |
|
|
289
360
|
|
|
290
361
|
---
|
|
291
362
|
|
|
@@ -294,10 +365,11 @@ To enforce these conventions, configure ESLint:
|
|
|
294
365
|
When contributing to this project, please:
|
|
295
366
|
|
|
296
367
|
1. ✅ Follow ALL conventions outlined in this document
|
|
297
|
-
2. ✅
|
|
298
|
-
3. ✅
|
|
299
|
-
4. ✅
|
|
300
|
-
5. ✅
|
|
368
|
+
2. ✅ Use `type` instead of `interface` for all object type definitions
|
|
369
|
+
3. ✅ Run linting before committing: `bun run lint`
|
|
370
|
+
4. ✅ Format code: `bun run format`
|
|
371
|
+
5. ✅ Use descriptive commit messages
|
|
372
|
+
6. ✅ Add JSDoc comments for exported functions
|
|
301
373
|
|
|
302
374
|
---
|
|
303
375
|
|
|
@@ -310,6 +382,7 @@ These conventions are chosen to:
|
|
|
310
382
|
- **Reduce cognitive load** when working with multiple languages
|
|
311
383
|
- **Follow industry standards** where applicable
|
|
312
384
|
- **Enable better tooling support**
|
|
385
|
+
- **Simplify type definitions** by using only `type` declarations
|
|
313
386
|
|
|
314
387
|
---
|
|
315
388
|
|
|
@@ -318,6 +391,7 @@ These conventions are chosen to:
|
|
|
318
391
|
- [Airbnb JavaScript Style Guide](https://github.com/airbnb/javascript)
|
|
319
392
|
- [TypeScript Style Guide](https://google.github.io/styleguide/tsguide.html)
|
|
320
393
|
- [File Naming Conventions](https://github.com/kettanaito/naming-cheatsheet)
|
|
394
|
+
- [Types vs Interfaces](https://www.typescriptlang.org/docs/handbook/2/everyday-types.html#differences-between-type-aliases-and-interfaces)
|
|
321
395
|
|
|
322
396
|
---
|
|
323
397
|
|
package/cli/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "deploy-bbc",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"description": "CLI to bootstrap production-ready backends with Bun (Best Backend Code)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -38,17 +38,17 @@
|
|
|
38
38
|
"url": "https://github.com/aritra69/deploy-bbc/issues"
|
|
39
39
|
},
|
|
40
40
|
"dependencies": {
|
|
41
|
-
"@clack/prompts": "^0.
|
|
42
|
-
"chalk": "^5.
|
|
43
|
-
"commander": "^
|
|
44
|
-
"execa": "^
|
|
45
|
-
"fs-extra": "^11.
|
|
46
|
-
"ora": "^
|
|
41
|
+
"@clack/prompts": "^0.11.0",
|
|
42
|
+
"chalk": "^5.6.2",
|
|
43
|
+
"commander": "^14.0.2",
|
|
44
|
+
"execa": "^9.6.1",
|
|
45
|
+
"fs-extra": "^11.3.3",
|
|
46
|
+
"ora": "^9.1.0"
|
|
47
47
|
},
|
|
48
48
|
"devDependencies": {
|
|
49
49
|
"@types/bun": "latest",
|
|
50
50
|
"@types/fs-extra": "^11.0.4",
|
|
51
|
-
"@types/node": "^
|
|
52
|
-
"typescript": "^5.
|
|
51
|
+
"@types/node": "^25.0.10",
|
|
52
|
+
"typescript": "^5.9.3"
|
|
53
53
|
}
|
|
54
54
|
}
|
package/cli/src/cli/index.ts
CHANGED
|
@@ -100,12 +100,17 @@ export const run_cli = async (): Promise<CliResults> => {
|
|
|
100
100
|
projectName: () =>
|
|
101
101
|
p.text({
|
|
102
102
|
message: "What will your project be called?",
|
|
103
|
-
placeholder: "my-awesome-api",
|
|
103
|
+
placeholder: "my-awesome-api (or . for current directory)",
|
|
104
104
|
defaultValue: cliProvidedName || "my-backend",
|
|
105
105
|
validate: (value) => {
|
|
106
106
|
if (!value) return "Please enter a project name";
|
|
107
|
-
|
|
108
|
-
|
|
107
|
+
// Allow '.' for current directory or paths
|
|
108
|
+
if (value === "." || value.startsWith("./") || value.startsWith("../") || value.startsWith("~/")) {
|
|
109
|
+
return; // Valid path
|
|
110
|
+
}
|
|
111
|
+
// Otherwise check for valid name format
|
|
112
|
+
if (!/^[a-z0-9-_/]+$/i.test(value))
|
|
113
|
+
return "Only letters, numbers, dashes, underscores, and slashes";
|
|
109
114
|
},
|
|
110
115
|
}),
|
|
111
116
|
|
|
@@ -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
|
|
|
@@ -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
|
|
|
@@ -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
|
+
}
|