autoworkflow 3.1.5 → 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.
Files changed (123) hide show
  1. package/.claude/commands/analyze.md +19 -0
  2. package/.claude/commands/audit.md +26 -0
  3. package/.claude/commands/build.md +39 -0
  4. package/.claude/commands/commit.md +25 -0
  5. package/.claude/commands/fix.md +23 -0
  6. package/.claude/commands/plan.md +18 -0
  7. package/.claude/commands/suggest.md +23 -0
  8. package/.claude/commands/verify.md +18 -0
  9. package/.claude/hooks/post-bash-router.sh +20 -0
  10. package/.claude/hooks/post-commit.sh +140 -0
  11. package/.claude/hooks/pre-edit.sh +129 -0
  12. package/.claude/hooks/session-check.sh +79 -0
  13. package/.claude/settings.json +40 -6
  14. package/.claude/settings.local.json +3 -1
  15. package/.claude/skills/actix.md +337 -0
  16. package/.claude/skills/alembic.md +504 -0
  17. package/.claude/skills/angular.md +237 -0
  18. package/.claude/skills/api-design.md +187 -0
  19. package/.claude/skills/aspnet-core.md +377 -0
  20. package/.claude/skills/astro.md +245 -0
  21. package/.claude/skills/auth-clerk.md +327 -0
  22. package/.claude/skills/auth-firebase.md +367 -0
  23. package/.claude/skills/auth-nextauth.md +359 -0
  24. package/.claude/skills/auth-supabase.md +368 -0
  25. package/.claude/skills/axum.md +386 -0
  26. package/.claude/skills/blazor.md +456 -0
  27. package/.claude/skills/chi.md +348 -0
  28. package/.claude/skills/code-review.md +133 -0
  29. package/.claude/skills/csharp.md +296 -0
  30. package/.claude/skills/css-modules.md +325 -0
  31. package/.claude/skills/cypress.md +343 -0
  32. package/.claude/skills/debugging.md +133 -0
  33. package/.claude/skills/diesel.md +392 -0
  34. package/.claude/skills/django.md +301 -0
  35. package/.claude/skills/docker.md +319 -0
  36. package/.claude/skills/doctrine.md +473 -0
  37. package/.claude/skills/documentation.md +182 -0
  38. package/.claude/skills/dotnet.md +409 -0
  39. package/.claude/skills/drizzle.md +293 -0
  40. package/.claude/skills/echo.md +321 -0
  41. package/.claude/skills/eloquent.md +256 -0
  42. package/.claude/skills/emotion.md +426 -0
  43. package/.claude/skills/entity-framework.md +370 -0
  44. package/.claude/skills/express.md +316 -0
  45. package/.claude/skills/fastapi.md +329 -0
  46. package/.claude/skills/fastify.md +299 -0
  47. package/.claude/skills/fiber.md +315 -0
  48. package/.claude/skills/flask.md +322 -0
  49. package/.claude/skills/gin.md +342 -0
  50. package/.claude/skills/git.md +116 -0
  51. package/.claude/skills/github-actions.md +353 -0
  52. package/.claude/skills/go.md +377 -0
  53. package/.claude/skills/gorm.md +409 -0
  54. package/.claude/skills/graphql.md +478 -0
  55. package/.claude/skills/hibernate.md +379 -0
  56. package/.claude/skills/hono.md +306 -0
  57. package/.claude/skills/java.md +400 -0
  58. package/.claude/skills/jest.md +313 -0
  59. package/.claude/skills/jpa.md +282 -0
  60. package/.claude/skills/kotlin.md +347 -0
  61. package/.claude/skills/kubernetes.md +363 -0
  62. package/.claude/skills/laravel.md +414 -0
  63. package/.claude/skills/mcp-browser.md +320 -0
  64. package/.claude/skills/mcp-database.md +219 -0
  65. package/.claude/skills/mcp-fetch.md +241 -0
  66. package/.claude/skills/mcp-filesystem.md +204 -0
  67. package/.claude/skills/mcp-github.md +217 -0
  68. package/.claude/skills/mcp-memory.md +240 -0
  69. package/.claude/skills/mcp-search.md +218 -0
  70. package/.claude/skills/mcp-slack.md +262 -0
  71. package/.claude/skills/micronaut.md +388 -0
  72. package/.claude/skills/mongodb.md +319 -0
  73. package/.claude/skills/mongoose.md +355 -0
  74. package/.claude/skills/mysql.md +281 -0
  75. package/.claude/skills/nestjs.md +335 -0
  76. package/.claude/skills/nextjs-app-router.md +260 -0
  77. package/.claude/skills/nextjs-pages.md +172 -0
  78. package/.claude/skills/nuxt.md +202 -0
  79. package/.claude/skills/openapi.md +489 -0
  80. package/.claude/skills/performance.md +199 -0
  81. package/.claude/skills/php.md +398 -0
  82. package/.claude/skills/playwright.md +371 -0
  83. package/.claude/skills/postgresql.md +257 -0
  84. package/.claude/skills/prisma.md +293 -0
  85. package/.claude/skills/pydantic.md +304 -0
  86. package/.claude/skills/pytest.md +313 -0
  87. package/.claude/skills/python.md +272 -0
  88. package/.claude/skills/quarkus.md +377 -0
  89. package/.claude/skills/react.md +230 -0
  90. package/.claude/skills/redis.md +391 -0
  91. package/.claude/skills/refactoring.md +143 -0
  92. package/.claude/skills/remix.md +246 -0
  93. package/.claude/skills/rest-api.md +490 -0
  94. package/.claude/skills/rocket.md +366 -0
  95. package/.claude/skills/rust.md +341 -0
  96. package/.claude/skills/sass.md +380 -0
  97. package/.claude/skills/sea-orm.md +382 -0
  98. package/.claude/skills/security.md +167 -0
  99. package/.claude/skills/sequelize.md +395 -0
  100. package/.claude/skills/spring-boot.md +416 -0
  101. package/.claude/skills/sqlalchemy.md +269 -0
  102. package/.claude/skills/sqlx-rust.md +408 -0
  103. package/.claude/skills/state-jotai.md +346 -0
  104. package/.claude/skills/state-mobx.md +353 -0
  105. package/.claude/skills/state-pinia.md +431 -0
  106. package/.claude/skills/state-redux.md +337 -0
  107. package/.claude/skills/state-tanstack-query.md +434 -0
  108. package/.claude/skills/state-zustand.md +340 -0
  109. package/.claude/skills/styled-components.md +403 -0
  110. package/.claude/skills/svelte.md +238 -0
  111. package/.claude/skills/sveltekit.md +207 -0
  112. package/.claude/skills/symfony.md +437 -0
  113. package/.claude/skills/tailwind.md +279 -0
  114. package/.claude/skills/terraform.md +394 -0
  115. package/.claude/skills/testing-library.md +371 -0
  116. package/.claude/skills/trpc.md +426 -0
  117. package/.claude/skills/typeorm.md +368 -0
  118. package/.claude/skills/vitest.md +330 -0
  119. package/.claude/skills/vue.md +202 -0
  120. package/.claude/skills/warp.md +365 -0
  121. package/README.md +135 -52
  122. package/package.json +1 -1
  123. package/system/triggers.md +152 -11
@@ -0,0 +1,416 @@
1
+ # Spring Boot Skill
2
+
3
+ ## Project Structure
4
+ \`\`\`
5
+ src/main/java/com/example/
6
+ ├── Application.java
7
+ ├── config/
8
+ │ ├── SecurityConfig.java
9
+ │ └── WebConfig.java
10
+ ├── controller/
11
+ │ └── UserController.java
12
+ ├── service/
13
+ │ ├── UserService.java
14
+ │ └── impl/UserServiceImpl.java
15
+ ├── repository/
16
+ │ └── UserRepository.java
17
+ ├── entity/
18
+ │ └── User.java
19
+ ├── dto/
20
+ │ ├── CreateUserRequest.java
21
+ │ └── UserResponse.java
22
+ └── exception/
23
+ ├── GlobalExceptionHandler.java
24
+ └── NotFoundException.java
25
+ \`\`\`
26
+
27
+ ## Controller Layer
28
+ \`\`\`java
29
+ @RestController
30
+ @RequestMapping("/api/v1/users")
31
+ @RequiredArgsConstructor // Lombok for constructor injection
32
+ public class UserController {
33
+ private final UserService userService;
34
+
35
+ @GetMapping
36
+ public ResponseEntity<Page<UserResponse>> listUsers(
37
+ @RequestParam(defaultValue = "0") int page,
38
+ @RequestParam(defaultValue = "20") int size) {
39
+ Page<UserResponse> users = userService.findAll(PageRequest.of(page, size));
40
+ return ResponseEntity.ok(users);
41
+ }
42
+
43
+ @GetMapping("/{id}")
44
+ public ResponseEntity<UserResponse> getUser(@PathVariable String id) {
45
+ return ResponseEntity.ok(userService.findById(id));
46
+ }
47
+
48
+ @PostMapping
49
+ public ResponseEntity<UserResponse> createUser(
50
+ @Valid @RequestBody CreateUserRequest request) {
51
+ UserResponse user = userService.create(request);
52
+ URI location = ServletUriComponentsBuilder
53
+ .fromCurrentRequest()
54
+ .path("/{id}")
55
+ .buildAndExpand(user.id())
56
+ .toUri();
57
+ return ResponseEntity.created(location).body(user);
58
+ }
59
+
60
+ @PutMapping("/{id}")
61
+ public ResponseEntity<UserResponse> updateUser(
62
+ @PathVariable String id,
63
+ @Valid @RequestBody UpdateUserRequest request) {
64
+ return ResponseEntity.ok(userService.update(id, request));
65
+ }
66
+
67
+ @DeleteMapping("/{id}")
68
+ public ResponseEntity<Void> deleteUser(@PathVariable String id) {
69
+ userService.delete(id);
70
+ return ResponseEntity.noContent().build();
71
+ }
72
+ }
73
+ \`\`\`
74
+
75
+ ## Service Layer
76
+ \`\`\`java
77
+ public interface UserService {
78
+ Page<UserResponse> findAll(Pageable pageable);
79
+ UserResponse findById(String id);
80
+ UserResponse create(CreateUserRequest request);
81
+ UserResponse update(String id, UpdateUserRequest request);
82
+ void delete(String id);
83
+ }
84
+
85
+ @Service
86
+ @RequiredArgsConstructor
87
+ @Transactional(readOnly = true)
88
+ public class UserServiceImpl implements UserService {
89
+ private final UserRepository userRepository;
90
+ private final PasswordEncoder passwordEncoder;
91
+
92
+ @Override
93
+ public Page<UserResponse> findAll(Pageable pageable) {
94
+ return userRepository.findAll(pageable)
95
+ .map(UserResponse::from);
96
+ }
97
+
98
+ @Override
99
+ public UserResponse findById(String id) {
100
+ return userRepository.findById(id)
101
+ .map(UserResponse::from)
102
+ .orElseThrow(() -> new NotFoundException("User not found: " + id));
103
+ }
104
+
105
+ @Override
106
+ @Transactional
107
+ public UserResponse create(CreateUserRequest request) {
108
+ if (userRepository.existsByEmail(request.email())) {
109
+ throw new ConflictException("Email already exists");
110
+ }
111
+
112
+ User user = User.builder()
113
+ .email(request.email())
114
+ .name(request.name())
115
+ .passwordHash(passwordEncoder.encode(request.password()))
116
+ .isActive(true)
117
+ .build();
118
+
119
+ return UserResponse.from(userRepository.save(user));
120
+ }
121
+
122
+ @Override
123
+ @Transactional
124
+ public UserResponse update(String id, UpdateUserRequest request) {
125
+ User user = userRepository.findById(id)
126
+ .orElseThrow(() -> new NotFoundException("User not found: " + id));
127
+
128
+ if (request.name() != null) {
129
+ user.setName(request.name());
130
+ }
131
+ if (request.email() != null) {
132
+ user.setEmail(request.email());
133
+ }
134
+
135
+ return UserResponse.from(userRepository.save(user));
136
+ }
137
+
138
+ @Override
139
+ @Transactional
140
+ public void delete(String id) {
141
+ if (!userRepository.existsById(id)) {
142
+ throw new NotFoundException("User not found: " + id);
143
+ }
144
+ userRepository.deleteById(id);
145
+ }
146
+ }
147
+ \`\`\`
148
+
149
+ ## DTOs with Validation
150
+ \`\`\`java
151
+ public record CreateUserRequest(
152
+ @NotBlank @Email String email,
153
+ @NotBlank @Size(min = 2, max = 100) String name,
154
+ @NotBlank @Size(min = 8) String password
155
+ ) {}
156
+
157
+ public record UpdateUserRequest(
158
+ @Email String email,
159
+ @Size(min = 2, max = 100) String name
160
+ ) {}
161
+
162
+ public record UserResponse(
163
+ String id,
164
+ String email,
165
+ String name,
166
+ boolean isActive,
167
+ Instant createdAt
168
+ ) {
169
+ public static UserResponse from(User user) {
170
+ return new UserResponse(
171
+ user.getId(),
172
+ user.getEmail(),
173
+ user.getName(),
174
+ user.isActive(),
175
+ user.getCreatedAt()
176
+ );
177
+ }
178
+ }
179
+ \`\`\`
180
+
181
+ ## Global Exception Handling
182
+ \`\`\`java
183
+ @RestControllerAdvice
184
+ @Slf4j
185
+ public class GlobalExceptionHandler {
186
+
187
+ @ExceptionHandler(NotFoundException.class)
188
+ public ResponseEntity<ErrorResponse> handleNotFound(NotFoundException ex) {
189
+ return ResponseEntity.status(HttpStatus.NOT_FOUND)
190
+ .body(new ErrorResponse("NOT_FOUND", ex.getMessage()));
191
+ }
192
+
193
+ @ExceptionHandler(ConflictException.class)
194
+ public ResponseEntity<ErrorResponse> handleConflict(ConflictException ex) {
195
+ return ResponseEntity.status(HttpStatus.CONFLICT)
196
+ .body(new ErrorResponse("CONFLICT", ex.getMessage()));
197
+ }
198
+
199
+ @ExceptionHandler(MethodArgumentNotValidException.class)
200
+ public ResponseEntity<ValidationErrorResponse> handleValidation(
201
+ MethodArgumentNotValidException ex) {
202
+ Map<String, String> errors = ex.getBindingResult()
203
+ .getFieldErrors()
204
+ .stream()
205
+ .collect(Collectors.toMap(
206
+ FieldError::getField,
207
+ e -> e.getDefaultMessage() != null ? e.getDefaultMessage() : "Invalid value"
208
+ ));
209
+
210
+ return ResponseEntity.badRequest()
211
+ .body(new ValidationErrorResponse("VALIDATION_ERROR", "Validation failed", errors));
212
+ }
213
+
214
+ @ExceptionHandler(Exception.class)
215
+ public ResponseEntity<ErrorResponse> handleGeneral(Exception ex) {
216
+ log.error("Unexpected error", ex);
217
+ return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
218
+ .body(new ErrorResponse("INTERNAL_ERROR", "An unexpected error occurred"));
219
+ }
220
+ }
221
+
222
+ public record ErrorResponse(String code, String message) {}
223
+
224
+ public record ValidationErrorResponse(
225
+ String code,
226
+ String message,
227
+ Map<String, String> errors
228
+ ) {}
229
+ \`\`\`
230
+
231
+ ## Configuration
232
+ \`\`\`java
233
+ // application.yml
234
+ spring:
235
+ datasource:
236
+ url: \${DATABASE_URL}
237
+ username: \${DATABASE_USER}
238
+ password: \${DATABASE_PASSWORD}
239
+ jpa:
240
+ hibernate:
241
+ ddl-auto: validate
242
+ open-in-view: false
243
+ jackson:
244
+ serialization:
245
+ write-dates-as-timestamps: false
246
+
247
+ // AppConfig.java
248
+ @Configuration
249
+ public class AppConfig {
250
+ @Bean
251
+ public PasswordEncoder passwordEncoder() {
252
+ return new BCryptPasswordEncoder();
253
+ }
254
+ }
255
+ \`\`\`
256
+
257
+ ## Security Configuration
258
+ \`\`\`java
259
+ @Configuration
260
+ @EnableWebSecurity
261
+ @RequiredArgsConstructor
262
+ public class SecurityConfig {
263
+ private final JwtAuthenticationFilter jwtFilter;
264
+
265
+ @Bean
266
+ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
267
+ return http
268
+ .csrf(csrf -> csrf.disable())
269
+ .sessionManagement(session ->
270
+ session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
271
+ .authorizeHttpRequests(auth -> auth
272
+ .requestMatchers("/api/v1/auth/**").permitAll()
273
+ .requestMatchers("/actuator/health").permitAll()
274
+ .anyRequest().authenticated()
275
+ )
276
+ .addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class)
277
+ .build();
278
+ }
279
+ }
280
+
281
+ @Component
282
+ @RequiredArgsConstructor
283
+ public class JwtAuthenticationFilter extends OncePerRequestFilter {
284
+ private final JwtService jwtService;
285
+ private final UserDetailsService userDetailsService;
286
+
287
+ @Override
288
+ protected void doFilterInternal(
289
+ HttpServletRequest request,
290
+ HttpServletResponse response,
291
+ FilterChain filterChain) throws ServletException, IOException {
292
+
293
+ String authHeader = request.getHeader("Authorization");
294
+ if (authHeader == null || !authHeader.startsWith("Bearer ")) {
295
+ filterChain.doFilter(request, response);
296
+ return;
297
+ }
298
+
299
+ String token = authHeader.substring(7);
300
+ String username = jwtService.extractUsername(token);
301
+
302
+ if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
303
+ UserDetails userDetails = userDetailsService.loadUserByUsername(username);
304
+ if (jwtService.isTokenValid(token, userDetails)) {
305
+ var authToken = new UsernamePasswordAuthenticationToken(
306
+ userDetails, null, userDetails.getAuthorities());
307
+ SecurityContextHolder.getContext().setAuthentication(authToken);
308
+ }
309
+ }
310
+
311
+ filterChain.doFilter(request, response);
312
+ }
313
+ }
314
+ \`\`\`
315
+
316
+ ## Testing
317
+ \`\`\`java
318
+ @WebMvcTest(UserController.class)
319
+ class UserControllerTest {
320
+ @Autowired
321
+ private MockMvc mockMvc;
322
+
323
+ @MockBean
324
+ private UserService userService;
325
+
326
+ @Autowired
327
+ private ObjectMapper objectMapper;
328
+
329
+ @Test
330
+ void getUser_whenExists_returnsUser() throws Exception {
331
+ var response = new UserResponse("1", "test@example.com", "Test", true, Instant.now());
332
+ when(userService.findById("1")).thenReturn(response);
333
+
334
+ mockMvc.perform(get("/api/v1/users/1"))
335
+ .andExpect(status().isOk())
336
+ .andExpect(jsonPath("$.email").value("test@example.com"));
337
+ }
338
+
339
+ @Test
340
+ void getUser_whenNotExists_returns404() throws Exception {
341
+ when(userService.findById("999"))
342
+ .thenThrow(new NotFoundException("User not found"));
343
+
344
+ mockMvc.perform(get("/api/v1/users/999"))
345
+ .andExpect(status().isNotFound());
346
+ }
347
+
348
+ @Test
349
+ void createUser_withValidData_returns201() throws Exception {
350
+ var request = new CreateUserRequest("test@example.com", "Test", "password123");
351
+ var response = new UserResponse("1", "test@example.com", "Test", true, Instant.now());
352
+ when(userService.create(any())).thenReturn(response);
353
+
354
+ mockMvc.perform(post("/api/v1/users")
355
+ .contentType(MediaType.APPLICATION_JSON)
356
+ .content(objectMapper.writeValueAsString(request)))
357
+ .andExpect(status().isCreated())
358
+ .andExpect(header().exists("Location"));
359
+ }
360
+
361
+ @Test
362
+ void createUser_withInvalidEmail_returns400() throws Exception {
363
+ var request = Map.of("email", "invalid", "name", "Test", "password", "password123");
364
+
365
+ mockMvc.perform(post("/api/v1/users")
366
+ .contentType(MediaType.APPLICATION_JSON)
367
+ .content(objectMapper.writeValueAsString(request)))
368
+ .andExpect(status().isBadRequest())
369
+ .andExpect(jsonPath("$.errors.email").exists());
370
+ }
371
+ }
372
+
373
+ @SpringBootTest
374
+ @AutoConfigureMockMvc
375
+ class UserIntegrationTest {
376
+ @Autowired
377
+ private MockMvc mockMvc;
378
+
379
+ @Autowired
380
+ private UserRepository userRepository;
381
+
382
+ @BeforeEach
383
+ void setUp() {
384
+ userRepository.deleteAll();
385
+ }
386
+
387
+ @Test
388
+ void fullUserLifecycle() throws Exception {
389
+ // Create
390
+ mockMvc.perform(post("/api/v1/users")
391
+ .contentType(MediaType.APPLICATION_JSON)
392
+ .content("{\\"email\\":\\"test@example.com\\",\\"name\\":\\"Test\\",\\"password\\":\\"password123\\"}"))
393
+ .andExpect(status().isCreated());
394
+
395
+ // Verify in database
396
+ assertThat(userRepository.findByEmail("test@example.com")).isPresent();
397
+ }
398
+ }
399
+ \`\`\`
400
+
401
+ ## ✅ DO
402
+ - Use constructor injection (add @RequiredArgsConstructor with Lombok)
403
+ - Use records for DTOs
404
+ - Use @Transactional at service layer
405
+ - Use @Valid for request validation
406
+ - Return ResponseEntity for explicit status control
407
+ - Use @RestControllerAdvice for global exception handling
408
+ - Set \`spring.jpa.open-in-view=false\`
409
+
410
+ ## ❌ DON'T
411
+ - Don't inject repositories directly into controllers
412
+ - Don't expose entities in API responses (use DTOs)
413
+ - Don't catch exceptions in controllers (use @RestControllerAdvice)
414
+ - Don't use field injection (@Autowired on fields)
415
+ - Don't return null - throw exceptions or use Optional
416
+ - Don't put business logic in controllers
@@ -0,0 +1,269 @@
1
+ # SQLAlchemy Skill
2
+
3
+ ## SQLAlchemy 2.0 Model Definition
4
+ \`\`\`python
5
+ from datetime import datetime
6
+ from sqlalchemy import String, ForeignKey, func
7
+ from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship
8
+
9
+ class Base(DeclarativeBase):
10
+ pass
11
+
12
+ class User(Base):
13
+ __tablename__ = "users"
14
+
15
+ # Mapped columns with type annotations
16
+ id: Mapped[int] = mapped_column(primary_key=True)
17
+ email: Mapped[str] = mapped_column(String(255), unique=True, index=True)
18
+ name: Mapped[str] = mapped_column(String(100))
19
+ is_active: Mapped[bool] = mapped_column(default=True)
20
+ created_at: Mapped[datetime] = mapped_column(server_default=func.now())
21
+
22
+ # Relationships
23
+ posts: Mapped[list["Post"]] = relationship(back_populates="author", cascade="all, delete-orphan")
24
+
25
+ def __repr__(self) -> str:
26
+ return f"User(id={self.id}, email={self.email!r})"
27
+
28
+ class Post(Base):
29
+ __tablename__ = "posts"
30
+
31
+ id: Mapped[int] = mapped_column(primary_key=True)
32
+ title: Mapped[str] = mapped_column(String(200))
33
+ content: Mapped[str]
34
+ author_id: Mapped[int] = mapped_column(ForeignKey("users.id"))
35
+
36
+ author: Mapped["User"] = relationship(back_populates="posts")
37
+ \`\`\`
38
+
39
+ ## Async Engine & Session
40
+ \`\`\`python
41
+ from sqlalchemy.ext.asyncio import create_async_engine, async_sessionmaker, AsyncSession
42
+
43
+ # Create async engine
44
+ engine = create_async_engine(
45
+ "postgresql+asyncpg://user:pass@localhost/db",
46
+ echo=True, # Log SQL
47
+ pool_size=5,
48
+ max_overflow=10,
49
+ )
50
+
51
+ # Session factory
52
+ async_session = async_sessionmaker(
53
+ engine,
54
+ class_=AsyncSession,
55
+ expire_on_commit=False,
56
+ )
57
+
58
+ # Dependency for FastAPI
59
+ async def get_db() -> AsyncGenerator[AsyncSession, None]:
60
+ async with async_session() as session:
61
+ try:
62
+ yield session
63
+ await session.commit()
64
+ except Exception:
65
+ await session.rollback()
66
+ raise
67
+ finally:
68
+ await session.close()
69
+ \`\`\`
70
+
71
+ ## Async Queries (SQLAlchemy 2.0)
72
+ \`\`\`python
73
+ from sqlalchemy import select
74
+ from sqlalchemy.orm import selectinload, joinedload
75
+
76
+ # Select with type hints
77
+ async def get_user(session: AsyncSession, user_id: int) -> User | None:
78
+ result = await session.execute(
79
+ select(User).where(User.id == user_id)
80
+ )
81
+ return result.scalar_one_or_none()
82
+
83
+ # Select all with filtering
84
+ async def get_active_users(session: AsyncSession) -> list[User]:
85
+ result = await session.execute(
86
+ select(User)
87
+ .where(User.is_active == True)
88
+ .order_by(User.created_at.desc())
89
+ )
90
+ return list(result.scalars().all())
91
+
92
+ # Eager loading relationships
93
+ async def get_user_with_posts(session: AsyncSession, user_id: int) -> User | None:
94
+ result = await session.execute(
95
+ select(User)
96
+ .options(selectinload(User.posts)) # Async-safe loading
97
+ .where(User.id == user_id)
98
+ )
99
+ return result.scalar_one_or_none()
100
+
101
+ # Pagination
102
+ async def get_users_paginated(
103
+ session: AsyncSession,
104
+ page: int = 1,
105
+ per_page: int = 10,
106
+ ) -> list[User]:
107
+ result = await session.execute(
108
+ select(User)
109
+ .offset((page - 1) * per_page)
110
+ .limit(per_page)
111
+ )
112
+ return list(result.scalars().all())
113
+ \`\`\`
114
+
115
+ ## CRUD Operations
116
+ \`\`\`python
117
+ # Create
118
+ async def create_user(session: AsyncSession, email: str, name: str) -> User:
119
+ user = User(email=email, name=name)
120
+ session.add(user)
121
+ await session.flush() # Get ID without committing
122
+ return user
123
+
124
+ # Update
125
+ async def update_user(session: AsyncSession, user_id: int, **data) -> User | None:
126
+ user = await get_user(session, user_id)
127
+ if user:
128
+ for key, value in data.items():
129
+ setattr(user, key, value)
130
+ await session.flush()
131
+ return user
132
+
133
+ # Delete
134
+ async def delete_user(session: AsyncSession, user_id: int) -> bool:
135
+ user = await get_user(session, user_id)
136
+ if user:
137
+ await session.delete(user)
138
+ return True
139
+ return False
140
+
141
+ # Bulk insert
142
+ async def create_users_bulk(session: AsyncSession, users_data: list[dict]) -> None:
143
+ session.add_all([User(**data) for data in users_data])
144
+ await session.flush()
145
+ \`\`\`
146
+
147
+ ## Transactions
148
+ \`\`\`python
149
+ from sqlalchemy.exc import IntegrityError
150
+
151
+ # Explicit transaction
152
+ async def transfer_posts(session: AsyncSession, from_id: int, to_id: int):
153
+ async with session.begin(): # Auto-commit or rollback
154
+ from_user = await get_user(session, from_id)
155
+ to_user = await get_user(session, to_id)
156
+
157
+ for post in from_user.posts:
158
+ post.author_id = to_id
159
+
160
+ # Changes committed when exiting context
161
+
162
+ # Savepoints for partial rollbacks
163
+ async def create_with_fallback(session: AsyncSession, data: dict):
164
+ try:
165
+ async with session.begin_nested(): # Savepoint
166
+ user = User(**data)
167
+ session.add(user)
168
+ await session.flush()
169
+ except IntegrityError:
170
+ # Rollback to savepoint, outer transaction continues
171
+ pass
172
+ \`\`\`
173
+
174
+ ## Complex Queries
175
+ \`\`\`python
176
+ from sqlalchemy import func, and_, or_, case
177
+
178
+ # Aggregation
179
+ async def get_post_counts(session: AsyncSession):
180
+ result = await session.execute(
181
+ select(User.id, User.name, func.count(Post.id).label('post_count'))
182
+ .join(Post, isouter=True)
183
+ .group_by(User.id)
184
+ .having(func.count(Post.id) > 0)
185
+ )
186
+ return result.all()
187
+
188
+ # Subqueries
189
+ async def get_users_with_recent_posts(session: AsyncSession):
190
+ subq = (
191
+ select(Post.author_id)
192
+ .where(Post.created_at > func.now() - timedelta(days=7))
193
+ .distinct()
194
+ .subquery()
195
+ )
196
+
197
+ result = await session.execute(
198
+ select(User).where(User.id.in_(select(subq)))
199
+ )
200
+ return result.scalars().all()
201
+
202
+ # Case expressions
203
+ async def get_users_by_activity(session: AsyncSession):
204
+ result = await session.execute(
205
+ select(
206
+ User,
207
+ case(
208
+ (func.count(Post.id) > 10, 'active'),
209
+ (func.count(Post.id) > 0, 'moderate'),
210
+ else_='inactive'
211
+ ).label('activity')
212
+ )
213
+ .join(Post, isouter=True)
214
+ .group_by(User.id)
215
+ )
216
+ return result.all()
217
+ \`\`\`
218
+
219
+ ## Relationships In-Depth
220
+ \`\`\`python
221
+ # Many-to-Many
222
+ user_roles = Table(
223
+ 'user_roles',
224
+ Base.metadata,
225
+ Column('user_id', ForeignKey('users.id'), primary_key=True),
226
+ Column('role_id', ForeignKey('roles.id'), primary_key=True),
227
+ )
228
+
229
+ class Role(Base):
230
+ __tablename__ = 'roles'
231
+ id: Mapped[int] = mapped_column(primary_key=True)
232
+ name: Mapped[str] = mapped_column(String(50), unique=True)
233
+ users: Mapped[list["User"]] = relationship(secondary=user_roles, back_populates="roles")
234
+
235
+ class User(Base):
236
+ # ... other fields
237
+ roles: Mapped[list["Role"]] = relationship(secondary=user_roles, back_populates="users")
238
+
239
+ # Self-referential (followers)
240
+ followers = Table(
241
+ 'followers',
242
+ Base.metadata,
243
+ Column('follower_id', ForeignKey('users.id'), primary_key=True),
244
+ Column('followed_id', ForeignKey('users.id'), primary_key=True),
245
+ )
246
+
247
+ class User(Base):
248
+ # ... other fields
249
+ following: Mapped[list["User"]] = relationship(
250
+ secondary=followers,
251
+ primaryjoin=id == followers.c.follower_id,
252
+ secondaryjoin=id == followers.c.followed_id,
253
+ backref="followers",
254
+ )
255
+ \`\`\`
256
+
257
+ ## ❌ DON'T
258
+ - Use session.query() (legacy, use select())
259
+ - Forget eager loading (N+1 problem)
260
+ - Use sync operations in async code
261
+ - Commit inside functions (let caller decide)
262
+
263
+ ## ✅ DO
264
+ - Use SQLAlchemy 2.0 style (Mapped, select)
265
+ - Use async for web applications
266
+ - Use selectinload for async relationship loading
267
+ - Use transactions for multi-step operations
268
+ - Use indices on frequently queried columns
269
+ - Use connection pooling