autoworkflow 3.1.4 → 3.5.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/commands/analyze.md +19 -0
- package/.claude/commands/audit.md +174 -11
- package/.claude/commands/build.md +39 -0
- package/.claude/commands/commit.md +25 -0
- package/.claude/commands/fix.md +23 -0
- package/.claude/commands/plan.md +18 -0
- package/.claude/commands/suggest.md +23 -0
- package/.claude/commands/verify.md +18 -0
- package/.claude/hooks/post-bash-router.sh +20 -0
- package/.claude/hooks/post-commit.sh +140 -0
- package/.claude/hooks/pre-edit.sh +129 -0
- package/.claude/hooks/session-check.sh +79 -0
- package/.claude/settings.json +40 -6
- package/.claude/settings.local.json +3 -1
- package/.claude/skills/actix.md +337 -0
- package/.claude/skills/alembic.md +504 -0
- package/.claude/skills/angular.md +237 -0
- package/.claude/skills/api-design.md +187 -0
- package/.claude/skills/aspnet-core.md +377 -0
- package/.claude/skills/astro.md +245 -0
- package/.claude/skills/auth-clerk.md +327 -0
- package/.claude/skills/auth-firebase.md +367 -0
- package/.claude/skills/auth-nextauth.md +359 -0
- package/.claude/skills/auth-supabase.md +368 -0
- package/.claude/skills/axum.md +386 -0
- package/.claude/skills/blazor.md +456 -0
- package/.claude/skills/chi.md +348 -0
- package/.claude/skills/code-review.md +133 -0
- package/.claude/skills/csharp.md +296 -0
- package/.claude/skills/css-modules.md +325 -0
- package/.claude/skills/cypress.md +343 -0
- package/.claude/skills/debugging.md +133 -0
- package/.claude/skills/diesel.md +392 -0
- package/.claude/skills/django.md +301 -0
- package/.claude/skills/docker.md +319 -0
- package/.claude/skills/doctrine.md +473 -0
- package/.claude/skills/documentation.md +182 -0
- package/.claude/skills/dotnet.md +409 -0
- package/.claude/skills/drizzle.md +293 -0
- package/.claude/skills/echo.md +321 -0
- package/.claude/skills/eloquent.md +256 -0
- package/.claude/skills/emotion.md +426 -0
- package/.claude/skills/entity-framework.md +370 -0
- package/.claude/skills/express.md +316 -0
- package/.claude/skills/fastapi.md +329 -0
- package/.claude/skills/fastify.md +299 -0
- package/.claude/skills/fiber.md +315 -0
- package/.claude/skills/flask.md +322 -0
- package/.claude/skills/gin.md +342 -0
- package/.claude/skills/git.md +116 -0
- package/.claude/skills/github-actions.md +353 -0
- package/.claude/skills/go.md +377 -0
- package/.claude/skills/gorm.md +409 -0
- package/.claude/skills/graphql.md +478 -0
- package/.claude/skills/hibernate.md +379 -0
- package/.claude/skills/hono.md +306 -0
- package/.claude/skills/java.md +400 -0
- package/.claude/skills/jest.md +313 -0
- package/.claude/skills/jpa.md +282 -0
- package/.claude/skills/kotlin.md +347 -0
- package/.claude/skills/kubernetes.md +363 -0
- package/.claude/skills/laravel.md +414 -0
- package/.claude/skills/mcp-browser.md +320 -0
- package/.claude/skills/mcp-database.md +219 -0
- package/.claude/skills/mcp-fetch.md +241 -0
- package/.claude/skills/mcp-filesystem.md +204 -0
- package/.claude/skills/mcp-github.md +217 -0
- package/.claude/skills/mcp-memory.md +240 -0
- package/.claude/skills/mcp-search.md +218 -0
- package/.claude/skills/mcp-slack.md +262 -0
- package/.claude/skills/micronaut.md +388 -0
- package/.claude/skills/mongodb.md +319 -0
- package/.claude/skills/mongoose.md +355 -0
- package/.claude/skills/mysql.md +281 -0
- package/.claude/skills/nestjs.md +335 -0
- package/.claude/skills/nextjs-app-router.md +260 -0
- package/.claude/skills/nextjs-pages.md +172 -0
- package/.claude/skills/nuxt.md +202 -0
- package/.claude/skills/openapi.md +489 -0
- package/.claude/skills/performance.md +199 -0
- package/.claude/skills/php.md +398 -0
- package/.claude/skills/playwright.md +371 -0
- package/.claude/skills/postgresql.md +257 -0
- package/.claude/skills/prisma.md +293 -0
- package/.claude/skills/pydantic.md +304 -0
- package/.claude/skills/pytest.md +313 -0
- package/.claude/skills/python.md +272 -0
- package/.claude/skills/quarkus.md +377 -0
- package/.claude/skills/react.md +230 -0
- package/.claude/skills/redis.md +391 -0
- package/.claude/skills/refactoring.md +143 -0
- package/.claude/skills/remix.md +246 -0
- package/.claude/skills/rest-api.md +490 -0
- package/.claude/skills/rocket.md +366 -0
- package/.claude/skills/rust.md +341 -0
- package/.claude/skills/sass.md +380 -0
- package/.claude/skills/sea-orm.md +382 -0
- package/.claude/skills/security.md +167 -0
- package/.claude/skills/sequelize.md +395 -0
- package/.claude/skills/spring-boot.md +416 -0
- package/.claude/skills/sqlalchemy.md +269 -0
- package/.claude/skills/sqlx-rust.md +408 -0
- package/.claude/skills/state-jotai.md +346 -0
- package/.claude/skills/state-mobx.md +353 -0
- package/.claude/skills/state-pinia.md +431 -0
- package/.claude/skills/state-redux.md +337 -0
- package/.claude/skills/state-tanstack-query.md +434 -0
- package/.claude/skills/state-zustand.md +340 -0
- package/.claude/skills/styled-components.md +403 -0
- package/.claude/skills/svelte.md +238 -0
- package/.claude/skills/sveltekit.md +207 -0
- package/.claude/skills/symfony.md +437 -0
- package/.claude/skills/tailwind.md +279 -0
- package/.claude/skills/terraform.md +394 -0
- package/.claude/skills/testing-library.md +371 -0
- package/.claude/skills/trpc.md +426 -0
- package/.claude/skills/typeorm.md +368 -0
- package/.claude/skills/vitest.md +330 -0
- package/.claude/skills/vue.md +202 -0
- package/.claude/skills/warp.md +365 -0
- package/README.md +135 -52
- package/package.json +1 -1
- package/system/triggers.md +152 -11
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
# MySQL Skill
|
|
2
|
+
|
|
3
|
+
## Schema Design
|
|
4
|
+
\`\`\`sql
|
|
5
|
+
-- Table with constraints (MySQL 8.0+)
|
|
6
|
+
CREATE TABLE users (
|
|
7
|
+
id BINARY(16) PRIMARY KEY DEFAULT (UUID_TO_BIN(UUID())),
|
|
8
|
+
email VARCHAR(255) NOT NULL,
|
|
9
|
+
name VARCHAR(100) NOT NULL,
|
|
10
|
+
password_hash VARCHAR(255) NOT NULL,
|
|
11
|
+
role ENUM('user', 'admin', 'moderator') DEFAULT 'user',
|
|
12
|
+
settings JSON DEFAULT '{}',
|
|
13
|
+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
14
|
+
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
|
15
|
+
deleted_at TIMESTAMP NULL,
|
|
16
|
+
UNIQUE KEY uk_email (email),
|
|
17
|
+
INDEX idx_role_created (role, created_at)
|
|
18
|
+
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
|
19
|
+
|
|
20
|
+
-- Foreign key with cascade
|
|
21
|
+
CREATE TABLE posts (
|
|
22
|
+
id BINARY(16) PRIMARY KEY DEFAULT (UUID_TO_BIN(UUID())),
|
|
23
|
+
title VARCHAR(255) NOT NULL,
|
|
24
|
+
content TEXT,
|
|
25
|
+
published BOOLEAN DEFAULT FALSE,
|
|
26
|
+
author_id BINARY(16) NOT NULL,
|
|
27
|
+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
28
|
+
INDEX idx_author (author_id),
|
|
29
|
+
INDEX idx_published_created (published, created_at),
|
|
30
|
+
FOREIGN KEY (author_id) REFERENCES users(id) ON DELETE CASCADE
|
|
31
|
+
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
|
32
|
+
|
|
33
|
+
-- Junction table for many-to-many
|
|
34
|
+
CREATE TABLE post_tags (
|
|
35
|
+
post_id BINARY(16) NOT NULL,
|
|
36
|
+
tag_id BINARY(16) NOT NULL,
|
|
37
|
+
PRIMARY KEY (post_id, tag_id),
|
|
38
|
+
FOREIGN KEY (post_id) REFERENCES posts(id) ON DELETE CASCADE,
|
|
39
|
+
FOREIGN KEY (tag_id) REFERENCES tags(id) ON DELETE CASCADE
|
|
40
|
+
) ENGINE=InnoDB;
|
|
41
|
+
\`\`\`
|
|
42
|
+
|
|
43
|
+
## Indexes
|
|
44
|
+
\`\`\`sql
|
|
45
|
+
-- B-tree index (default)
|
|
46
|
+
CREATE INDEX idx_users_email ON users(email);
|
|
47
|
+
|
|
48
|
+
-- Compound index (leftmost prefix rule applies)
|
|
49
|
+
CREATE INDEX idx_posts_author_created ON posts(author_id, created_at DESC);
|
|
50
|
+
|
|
51
|
+
-- Covering index (MySQL 8.0+ functional indexes)
|
|
52
|
+
CREATE INDEX idx_email_lower ON users((LOWER(email)));
|
|
53
|
+
|
|
54
|
+
-- Full-text index
|
|
55
|
+
ALTER TABLE posts ADD FULLTEXT INDEX ft_content (title, content);
|
|
56
|
+
|
|
57
|
+
-- Prefix index for long columns
|
|
58
|
+
CREATE INDEX idx_content_prefix ON posts(content(100));
|
|
59
|
+
|
|
60
|
+
-- Invisible index (for testing removal impact)
|
|
61
|
+
ALTER TABLE users ALTER INDEX idx_role INVISIBLE;
|
|
62
|
+
-- Later: ALTER TABLE users ALTER INDEX idx_role VISIBLE;
|
|
63
|
+
\`\`\`
|
|
64
|
+
|
|
65
|
+
## Common Queries
|
|
66
|
+
\`\`\`sql
|
|
67
|
+
-- Pagination (offset-based)
|
|
68
|
+
SELECT * FROM posts
|
|
69
|
+
WHERE published = TRUE
|
|
70
|
+
ORDER BY created_at DESC
|
|
71
|
+
LIMIT 20 OFFSET 40;
|
|
72
|
+
|
|
73
|
+
-- Pagination (cursor-based, more efficient)
|
|
74
|
+
SELECT * FROM posts
|
|
75
|
+
WHERE published = TRUE AND created_at < '2024-01-15 10:00:00'
|
|
76
|
+
ORDER BY created_at DESC
|
|
77
|
+
LIMIT 20;
|
|
78
|
+
|
|
79
|
+
-- Upsert (INSERT ... ON DUPLICATE KEY)
|
|
80
|
+
INSERT INTO users (email, name, updated_at)
|
|
81
|
+
VALUES ('user@example.com', 'John', NOW())
|
|
82
|
+
ON DUPLICATE KEY UPDATE
|
|
83
|
+
name = VALUES(name),
|
|
84
|
+
updated_at = NOW();
|
|
85
|
+
|
|
86
|
+
-- Insert ignore (skip on conflict)
|
|
87
|
+
INSERT IGNORE INTO users (email, name) VALUES ('user@example.com', 'John');
|
|
88
|
+
|
|
89
|
+
-- Replace (delete + insert on conflict)
|
|
90
|
+
REPLACE INTO users (email, name) VALUES ('user@example.com', 'New Name');
|
|
91
|
+
\`\`\`
|
|
92
|
+
|
|
93
|
+
## JSON Operations (MySQL 8.0+)
|
|
94
|
+
\`\`\`sql
|
|
95
|
+
-- Query JSON field
|
|
96
|
+
SELECT * FROM users WHERE JSON_EXTRACT(settings, '$.theme') = 'dark';
|
|
97
|
+
-- Shorthand: settings->>'$.theme' = 'dark'
|
|
98
|
+
|
|
99
|
+
SELECT * FROM users WHERE JSON_CONTAINS(settings, '"premium"', '$.tags');
|
|
100
|
+
|
|
101
|
+
-- Update JSON field
|
|
102
|
+
UPDATE users
|
|
103
|
+
SET settings = JSON_SET(settings, '$.theme', 'light')
|
|
104
|
+
WHERE id = ?;
|
|
105
|
+
|
|
106
|
+
-- Remove JSON key
|
|
107
|
+
UPDATE users
|
|
108
|
+
SET settings = JSON_REMOVE(settings, '$.oldKey')
|
|
109
|
+
WHERE id = ?;
|
|
110
|
+
|
|
111
|
+
-- Merge JSON objects
|
|
112
|
+
UPDATE users
|
|
113
|
+
SET settings = JSON_MERGE_PATCH(settings, '{"newKey": "value"}')
|
|
114
|
+
WHERE id = ?;
|
|
115
|
+
|
|
116
|
+
-- Index JSON values (generated column)
|
|
117
|
+
ALTER TABLE users
|
|
118
|
+
ADD COLUMN theme VARCHAR(50) GENERATED ALWAYS AS (settings->>'$.theme') STORED,
|
|
119
|
+
ADD INDEX idx_theme (theme);
|
|
120
|
+
\`\`\`
|
|
121
|
+
|
|
122
|
+
## CTEs & Window Functions (MySQL 8.0+)
|
|
123
|
+
\`\`\`sql
|
|
124
|
+
-- CTE for readability
|
|
125
|
+
WITH active_users AS (
|
|
126
|
+
SELECT * FROM users WHERE deleted_at IS NULL
|
|
127
|
+
)
|
|
128
|
+
SELECT u.*, COUNT(p.id) as post_count
|
|
129
|
+
FROM active_users u
|
|
130
|
+
LEFT JOIN posts p ON u.id = p.author_id
|
|
131
|
+
GROUP BY u.id;
|
|
132
|
+
|
|
133
|
+
-- Recursive CTE (hierarchical data)
|
|
134
|
+
WITH RECURSIVE category_tree AS (
|
|
135
|
+
SELECT id, name, parent_id, 0 as depth
|
|
136
|
+
FROM categories
|
|
137
|
+
WHERE parent_id IS NULL
|
|
138
|
+
|
|
139
|
+
UNION ALL
|
|
140
|
+
|
|
141
|
+
SELECT c.id, c.name, c.parent_id, ct.depth + 1
|
|
142
|
+
FROM categories c
|
|
143
|
+
JOIN category_tree ct ON c.parent_id = ct.id
|
|
144
|
+
)
|
|
145
|
+
SELECT * FROM category_tree ORDER BY depth, name;
|
|
146
|
+
|
|
147
|
+
-- Window functions
|
|
148
|
+
SELECT
|
|
149
|
+
author_id,
|
|
150
|
+
title,
|
|
151
|
+
created_at,
|
|
152
|
+
ROW_NUMBER() OVER (PARTITION BY author_id ORDER BY created_at DESC) as post_rank,
|
|
153
|
+
LAG(title) OVER (ORDER BY created_at) as prev_title
|
|
154
|
+
FROM posts;
|
|
155
|
+
\`\`\`
|
|
156
|
+
|
|
157
|
+
## Full-Text Search
|
|
158
|
+
\`\`\`sql
|
|
159
|
+
-- Natural language mode (default)
|
|
160
|
+
SELECT *, MATCH(title, content) AGAINST('search terms') as relevance
|
|
161
|
+
FROM posts
|
|
162
|
+
WHERE MATCH(title, content) AGAINST('search terms')
|
|
163
|
+
ORDER BY relevance DESC;
|
|
164
|
+
|
|
165
|
+
-- Boolean mode (more control)
|
|
166
|
+
SELECT * FROM posts
|
|
167
|
+
WHERE MATCH(title, content) AGAINST('+required -excluded "exact phrase"' IN BOOLEAN MODE);
|
|
168
|
+
|
|
169
|
+
-- Query expansion (find related)
|
|
170
|
+
SELECT * FROM posts
|
|
171
|
+
WHERE MATCH(title, content) AGAINST('database' WITH QUERY EXPANSION);
|
|
172
|
+
\`\`\`
|
|
173
|
+
|
|
174
|
+
## Query Optimization
|
|
175
|
+
\`\`\`sql
|
|
176
|
+
-- Analyze query plan
|
|
177
|
+
EXPLAIN SELECT * FROM users WHERE email = 'test@example.com';
|
|
178
|
+
|
|
179
|
+
-- Extended explain with more details
|
|
180
|
+
EXPLAIN ANALYZE SELECT * FROM posts WHERE author_id = ?;
|
|
181
|
+
|
|
182
|
+
-- Update table statistics
|
|
183
|
+
ANALYZE TABLE users;
|
|
184
|
+
|
|
185
|
+
-- Show index usage
|
|
186
|
+
SHOW INDEX FROM users;
|
|
187
|
+
|
|
188
|
+
-- Check slow queries (enable slow_query_log)
|
|
189
|
+
SELECT * FROM mysql.slow_log ORDER BY start_time DESC LIMIT 10;
|
|
190
|
+
|
|
191
|
+
-- Profile query execution
|
|
192
|
+
SET profiling = 1;
|
|
193
|
+
SELECT * FROM users WHERE role = 'admin';
|
|
194
|
+
SHOW PROFILES;
|
|
195
|
+
SHOW PROFILE FOR QUERY 1;
|
|
196
|
+
\`\`\`
|
|
197
|
+
|
|
198
|
+
## Transactions & Locking
|
|
199
|
+
\`\`\`sql
|
|
200
|
+
-- Transaction with isolation level
|
|
201
|
+
SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;
|
|
202
|
+
START TRANSACTION;
|
|
203
|
+
-- ... operations ...
|
|
204
|
+
COMMIT;
|
|
205
|
+
-- Or ROLLBACK;
|
|
206
|
+
|
|
207
|
+
-- Row-level lock (InnoDB)
|
|
208
|
+
SELECT * FROM accounts WHERE id = ? FOR UPDATE;
|
|
209
|
+
-- Other transactions wait
|
|
210
|
+
|
|
211
|
+
-- Shared lock (read lock)
|
|
212
|
+
SELECT * FROM accounts WHERE id = ? FOR SHARE;
|
|
213
|
+
|
|
214
|
+
-- Named lock (application-level)
|
|
215
|
+
SELECT GET_LOCK('resource_name', 10); -- 10 second timeout
|
|
216
|
+
-- ... do work ...
|
|
217
|
+
SELECT RELEASE_LOCK('resource_name');
|
|
218
|
+
\`\`\`
|
|
219
|
+
|
|
220
|
+
## Prepared Statements (Node.js example)
|
|
221
|
+
\`\`\`typescript
|
|
222
|
+
import mysql from 'mysql2/promise';
|
|
223
|
+
|
|
224
|
+
const pool = mysql.createPool({
|
|
225
|
+
host: process.env.DB_HOST,
|
|
226
|
+
user: process.env.DB_USER,
|
|
227
|
+
password: process.env.DB_PASSWORD,
|
|
228
|
+
database: process.env.DB_NAME,
|
|
229
|
+
waitForConnections: true,
|
|
230
|
+
connectionLimit: 10,
|
|
231
|
+
queueLimit: 0,
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
// Parameterized query (prevents SQL injection)
|
|
235
|
+
const [rows] = await pool.execute(
|
|
236
|
+
'SELECT * FROM users WHERE email = ? AND role = ?',
|
|
237
|
+
[email, 'user']
|
|
238
|
+
);
|
|
239
|
+
|
|
240
|
+
// Named placeholders
|
|
241
|
+
const [rows] = await pool.execute(
|
|
242
|
+
'SELECT * FROM users WHERE email = :email',
|
|
243
|
+
{ email: 'user@example.com' }
|
|
244
|
+
);
|
|
245
|
+
\`\`\`
|
|
246
|
+
|
|
247
|
+
## Performance Tips
|
|
248
|
+
\`\`\`sql
|
|
249
|
+
-- Check InnoDB status
|
|
250
|
+
SHOW ENGINE INNODB STATUS;
|
|
251
|
+
|
|
252
|
+
-- Buffer pool hit ratio (should be > 99%)
|
|
253
|
+
SHOW GLOBAL STATUS LIKE 'Innodb_buffer_pool_read%';
|
|
254
|
+
|
|
255
|
+
-- Table statistics
|
|
256
|
+
SELECT
|
|
257
|
+
table_name,
|
|
258
|
+
table_rows,
|
|
259
|
+
data_length / 1024 / 1024 as data_mb,
|
|
260
|
+
index_length / 1024 / 1024 as index_mb
|
|
261
|
+
FROM information_schema.tables
|
|
262
|
+
WHERE table_schema = DATABASE();
|
|
263
|
+
\`\`\`
|
|
264
|
+
|
|
265
|
+
## ❌ DON'T
|
|
266
|
+
- Use MyISAM for transactional tables
|
|
267
|
+
- Store UUIDs as VARCHAR (use BINARY(16))
|
|
268
|
+
- Use SELECT * in production
|
|
269
|
+
- Create indexes on every column
|
|
270
|
+
- Use OFFSET for deep pagination
|
|
271
|
+
- Skip parameterized queries (SQL injection risk)
|
|
272
|
+
|
|
273
|
+
## ✅ DO
|
|
274
|
+
- Use InnoDB for all tables
|
|
275
|
+
- Use utf8mb4 charset for proper Unicode support
|
|
276
|
+
- Create compound indexes matching query patterns
|
|
277
|
+
- Use prepared statements for all queries
|
|
278
|
+
- Monitor slow query log regularly
|
|
279
|
+
- Use connection pooling
|
|
280
|
+
- Prefer cursor-based pagination
|
|
281
|
+
- Use JSON columns for flexible schemas (MySQL 8.0+)
|
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
# NestJS Skill
|
|
2
|
+
|
|
3
|
+
## Project Structure
|
|
4
|
+
\`\`\`
|
|
5
|
+
src/
|
|
6
|
+
├── main.ts # Bootstrap
|
|
7
|
+
├── app.module.ts # Root module
|
|
8
|
+
├── common/
|
|
9
|
+
│ ├── decorators/ # Custom decorators
|
|
10
|
+
│ ├── guards/ # Auth guards
|
|
11
|
+
│ ├── interceptors/ # Response transforms
|
|
12
|
+
│ ├── filters/ # Exception filters
|
|
13
|
+
│ └── pipes/ # Validation pipes
|
|
14
|
+
└── modules/
|
|
15
|
+
└── users/
|
|
16
|
+
├── users.module.ts
|
|
17
|
+
├── users.controller.ts
|
|
18
|
+
├── users.service.ts
|
|
19
|
+
├── dto/
|
|
20
|
+
│ ├── create-user.dto.ts
|
|
21
|
+
│ └── update-user.dto.ts
|
|
22
|
+
└── entities/
|
|
23
|
+
└── user.entity.ts
|
|
24
|
+
\`\`\`
|
|
25
|
+
|
|
26
|
+
## Module & Controller
|
|
27
|
+
\`\`\`typescript
|
|
28
|
+
// users.module.ts
|
|
29
|
+
@Module({
|
|
30
|
+
imports: [TypeOrmModule.forFeature([User])],
|
|
31
|
+
controllers: [UsersController],
|
|
32
|
+
providers: [UsersService],
|
|
33
|
+
exports: [UsersService],
|
|
34
|
+
})
|
|
35
|
+
export class UsersModule {}
|
|
36
|
+
|
|
37
|
+
// users.controller.ts
|
|
38
|
+
@Controller('users')
|
|
39
|
+
@UseGuards(JwtAuthGuard)
|
|
40
|
+
export class UsersController {
|
|
41
|
+
constructor(private readonly usersService: UsersService) {}
|
|
42
|
+
|
|
43
|
+
@Get()
|
|
44
|
+
findAll(@Query() query: PaginationDto) {
|
|
45
|
+
return this.usersService.findAll(query);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
@Get(':id')
|
|
49
|
+
findOne(@Param('id', ParseUUIDPipe) id: string) {
|
|
50
|
+
return this.usersService.findOne(id);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
@Post()
|
|
54
|
+
@HttpCode(HttpStatus.CREATED)
|
|
55
|
+
create(@Body() createUserDto: CreateUserDto) {
|
|
56
|
+
return this.usersService.create(createUserDto);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
@Patch(':id')
|
|
60
|
+
update(
|
|
61
|
+
@Param('id', ParseUUIDPipe) id: string,
|
|
62
|
+
@Body() updateUserDto: UpdateUserDto,
|
|
63
|
+
) {
|
|
64
|
+
return this.usersService.update(id, updateUserDto);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
@Delete(':id')
|
|
68
|
+
@HttpCode(HttpStatus.NO_CONTENT)
|
|
69
|
+
remove(@Param('id', ParseUUIDPipe) id: string) {
|
|
70
|
+
return this.usersService.remove(id);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
\`\`\`
|
|
74
|
+
|
|
75
|
+
## DTOs with Validation
|
|
76
|
+
\`\`\`typescript
|
|
77
|
+
// dto/create-user.dto.ts
|
|
78
|
+
import { IsEmail, IsString, MinLength, IsOptional } from 'class-validator';
|
|
79
|
+
|
|
80
|
+
export class CreateUserDto {
|
|
81
|
+
@IsEmail()
|
|
82
|
+
email: string;
|
|
83
|
+
|
|
84
|
+
@IsString()
|
|
85
|
+
@MinLength(1)
|
|
86
|
+
name: string;
|
|
87
|
+
|
|
88
|
+
@IsString()
|
|
89
|
+
@MinLength(8)
|
|
90
|
+
password: string;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// dto/update-user.dto.ts
|
|
94
|
+
import { PartialType } from '@nestjs/mapped-types';
|
|
95
|
+
|
|
96
|
+
export class UpdateUserDto extends PartialType(CreateUserDto) {}
|
|
97
|
+
|
|
98
|
+
// Enable global validation in main.ts
|
|
99
|
+
app.useGlobalPipes(new ValidationPipe({
|
|
100
|
+
whitelist: true, // Strip unknown properties
|
|
101
|
+
forbidNonWhitelisted: true, // Throw on unknown properties
|
|
102
|
+
transform: true, // Auto-transform types
|
|
103
|
+
}));
|
|
104
|
+
\`\`\`
|
|
105
|
+
|
|
106
|
+
## Guards
|
|
107
|
+
\`\`\`typescript
|
|
108
|
+
// guards/jwt-auth.guard.ts
|
|
109
|
+
@Injectable()
|
|
110
|
+
export class JwtAuthGuard implements CanActivate {
|
|
111
|
+
constructor(private jwtService: JwtService) {}
|
|
112
|
+
|
|
113
|
+
async canActivate(context: ExecutionContext): Promise<boolean> {
|
|
114
|
+
const request = context.switchToHttp().getRequest();
|
|
115
|
+
const token = this.extractToken(request);
|
|
116
|
+
|
|
117
|
+
if (!token) {
|
|
118
|
+
throw new UnauthorizedException();
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
try {
|
|
122
|
+
const payload = await this.jwtService.verifyAsync(token);
|
|
123
|
+
request.user = payload;
|
|
124
|
+
} catch {
|
|
125
|
+
throw new UnauthorizedException();
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return true;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
private extractToken(request: Request): string | undefined {
|
|
132
|
+
const [type, token] = request.headers.authorization?.split(' ') ?? [];
|
|
133
|
+
return type === 'Bearer' ? token : undefined;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// guards/roles.guard.ts
|
|
138
|
+
@Injectable()
|
|
139
|
+
export class RolesGuard implements CanActivate {
|
|
140
|
+
constructor(private reflector: Reflector) {}
|
|
141
|
+
|
|
142
|
+
canActivate(context: ExecutionContext): boolean {
|
|
143
|
+
const requiredRoles = this.reflector.getAllAndOverride<Role[]>(ROLES_KEY, [
|
|
144
|
+
context.getHandler(),
|
|
145
|
+
context.getClass(),
|
|
146
|
+
]);
|
|
147
|
+
|
|
148
|
+
if (!requiredRoles) return true;
|
|
149
|
+
|
|
150
|
+
const { user } = context.switchToHttp().getRequest();
|
|
151
|
+
return requiredRoles.some((role) => user.roles?.includes(role));
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Usage
|
|
156
|
+
@Roles(Role.Admin)
|
|
157
|
+
@UseGuards(JwtAuthGuard, RolesGuard)
|
|
158
|
+
@Delete(':id')
|
|
159
|
+
remove(@Param('id') id: string) {}
|
|
160
|
+
\`\`\`
|
|
161
|
+
|
|
162
|
+
## Custom Decorators
|
|
163
|
+
\`\`\`typescript
|
|
164
|
+
// decorators/current-user.decorator.ts
|
|
165
|
+
export const CurrentUser = createParamDecorator(
|
|
166
|
+
(data: keyof User | undefined, ctx: ExecutionContext) => {
|
|
167
|
+
const request = ctx.switchToHttp().getRequest();
|
|
168
|
+
const user = request.user;
|
|
169
|
+
return data ? user?.[data] : user;
|
|
170
|
+
},
|
|
171
|
+
);
|
|
172
|
+
|
|
173
|
+
// decorators/roles.decorator.ts
|
|
174
|
+
export const ROLES_KEY = 'roles';
|
|
175
|
+
export const Roles = (...roles: Role[]) => SetMetadata(ROLES_KEY, roles);
|
|
176
|
+
|
|
177
|
+
// Usage
|
|
178
|
+
@Get('me')
|
|
179
|
+
getProfile(@CurrentUser() user: User) {
|
|
180
|
+
return user;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
@Get('email')
|
|
184
|
+
getEmail(@CurrentUser('email') email: string) {
|
|
185
|
+
return { email };
|
|
186
|
+
}
|
|
187
|
+
\`\`\`
|
|
188
|
+
|
|
189
|
+
## Exception Filters
|
|
190
|
+
\`\`\`typescript
|
|
191
|
+
// filters/http-exception.filter.ts
|
|
192
|
+
@Catch(HttpException)
|
|
193
|
+
export class HttpExceptionFilter implements ExceptionFilter {
|
|
194
|
+
catch(exception: HttpException, host: ArgumentsHost) {
|
|
195
|
+
const ctx = host.switchToHttp();
|
|
196
|
+
const response = ctx.getResponse<Response>();
|
|
197
|
+
const request = ctx.getRequest<Request>();
|
|
198
|
+
const status = exception.getStatus();
|
|
199
|
+
const exceptionResponse = exception.getResponse();
|
|
200
|
+
|
|
201
|
+
response.status(status).json({
|
|
202
|
+
statusCode: status,
|
|
203
|
+
timestamp: new Date().toISOString(),
|
|
204
|
+
path: request.url,
|
|
205
|
+
message: typeof exceptionResponse === 'string'
|
|
206
|
+
? exceptionResponse
|
|
207
|
+
: (exceptionResponse as any).message,
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Apply globally in main.ts
|
|
213
|
+
app.useGlobalFilters(new HttpExceptionFilter());
|
|
214
|
+
\`\`\`
|
|
215
|
+
|
|
216
|
+
## Interceptors
|
|
217
|
+
\`\`\`typescript
|
|
218
|
+
// interceptors/transform.interceptor.ts
|
|
219
|
+
@Injectable()
|
|
220
|
+
export class TransformInterceptor<T> implements NestInterceptor<T, Response<T>> {
|
|
221
|
+
intercept(context: ExecutionContext, next: CallHandler): Observable<Response<T>> {
|
|
222
|
+
return next.handle().pipe(
|
|
223
|
+
map((data) => ({
|
|
224
|
+
success: true,
|
|
225
|
+
data,
|
|
226
|
+
timestamp: new Date().toISOString(),
|
|
227
|
+
})),
|
|
228
|
+
);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// interceptors/logging.interceptor.ts
|
|
233
|
+
@Injectable()
|
|
234
|
+
export class LoggingInterceptor implements NestInterceptor {
|
|
235
|
+
private readonly logger = new Logger(LoggingInterceptor.name);
|
|
236
|
+
|
|
237
|
+
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
|
|
238
|
+
const request = context.switchToHttp().getRequest();
|
|
239
|
+
const { method, url } = request;
|
|
240
|
+
const now = Date.now();
|
|
241
|
+
|
|
242
|
+
return next.handle().pipe(
|
|
243
|
+
tap(() => {
|
|
244
|
+
this.logger.log(\`\${method} \${url} - \${Date.now() - now}ms\`);
|
|
245
|
+
}),
|
|
246
|
+
);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
\`\`\`
|
|
250
|
+
|
|
251
|
+
## Config Module
|
|
252
|
+
\`\`\`typescript
|
|
253
|
+
// config/configuration.ts
|
|
254
|
+
export default () => ({
|
|
255
|
+
port: parseInt(process.env.PORT, 10) || 3000,
|
|
256
|
+
database: {
|
|
257
|
+
host: process.env.DB_HOST,
|
|
258
|
+
port: parseInt(process.env.DB_PORT, 10) || 5432,
|
|
259
|
+
},
|
|
260
|
+
jwt: {
|
|
261
|
+
secret: process.env.JWT_SECRET,
|
|
262
|
+
expiresIn: '1h',
|
|
263
|
+
},
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
// app.module.ts
|
|
267
|
+
@Module({
|
|
268
|
+
imports: [
|
|
269
|
+
ConfigModule.forRoot({
|
|
270
|
+
isGlobal: true,
|
|
271
|
+
load: [configuration],
|
|
272
|
+
}),
|
|
273
|
+
],
|
|
274
|
+
})
|
|
275
|
+
export class AppModule {}
|
|
276
|
+
|
|
277
|
+
// Usage
|
|
278
|
+
@Injectable()
|
|
279
|
+
export class AuthService {
|
|
280
|
+
constructor(private configService: ConfigService) {}
|
|
281
|
+
|
|
282
|
+
getJwtSecret() {
|
|
283
|
+
return this.configService.get<string>('jwt.secret');
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
\`\`\`
|
|
287
|
+
|
|
288
|
+
## Testing
|
|
289
|
+
\`\`\`typescript
|
|
290
|
+
describe('UsersController', () => {
|
|
291
|
+
let controller: UsersController;
|
|
292
|
+
let service: UsersService;
|
|
293
|
+
|
|
294
|
+
beforeEach(async () => {
|
|
295
|
+
const module: TestingModule = await Test.createTestingModule({
|
|
296
|
+
controllers: [UsersController],
|
|
297
|
+
providers: [
|
|
298
|
+
{
|
|
299
|
+
provide: UsersService,
|
|
300
|
+
useValue: {
|
|
301
|
+
findAll: jest.fn(),
|
|
302
|
+
findOne: jest.fn(),
|
|
303
|
+
create: jest.fn(),
|
|
304
|
+
},
|
|
305
|
+
},
|
|
306
|
+
],
|
|
307
|
+
}).compile();
|
|
308
|
+
|
|
309
|
+
controller = module.get<UsersController>(UsersController);
|
|
310
|
+
service = module.get<UsersService>(UsersService);
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
it('should return all users', async () => {
|
|
314
|
+
const result = [{ id: '1', name: 'Test' }];
|
|
315
|
+
jest.spyOn(service, 'findAll').mockResolvedValue(result);
|
|
316
|
+
|
|
317
|
+
expect(await controller.findAll({})).toBe(result);
|
|
318
|
+
});
|
|
319
|
+
});
|
|
320
|
+
\`\`\`
|
|
321
|
+
|
|
322
|
+
## ❌ DON'T
|
|
323
|
+
- Put business logic in controllers
|
|
324
|
+
- Skip validation on DTOs
|
|
325
|
+
- Use any for types
|
|
326
|
+
- Forget to handle exceptions
|
|
327
|
+
|
|
328
|
+
## ✅ DO
|
|
329
|
+
- Use DTOs for validation
|
|
330
|
+
- Use dependency injection
|
|
331
|
+
- Organize by feature modules
|
|
332
|
+
- Use guards for authentication
|
|
333
|
+
- Use interceptors for response transforms
|
|
334
|
+
- Use exception filters for error handling
|
|
335
|
+
- Write unit tests for services
|