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,379 @@
|
|
|
1
|
+
# Hibernate Skill
|
|
2
|
+
|
|
3
|
+
## Entity Definition
|
|
4
|
+
\`\`\`java
|
|
5
|
+
@Entity
|
|
6
|
+
@Table(name = "users", indexes = {
|
|
7
|
+
@Index(name = "idx_users_email", columnList = "email")
|
|
8
|
+
})
|
|
9
|
+
public class User {
|
|
10
|
+
|
|
11
|
+
@Id
|
|
12
|
+
@GeneratedValue(strategy = GenerationType.UUID)
|
|
13
|
+
private String id;
|
|
14
|
+
|
|
15
|
+
@Column(unique = true, nullable = false, length = 255)
|
|
16
|
+
private String email;
|
|
17
|
+
|
|
18
|
+
@Column(nullable = false, length = 100)
|
|
19
|
+
private String name;
|
|
20
|
+
|
|
21
|
+
@Column(name = "password_hash", nullable = false)
|
|
22
|
+
private String passwordHash;
|
|
23
|
+
|
|
24
|
+
@Column(name = "is_active")
|
|
25
|
+
private boolean isActive = true;
|
|
26
|
+
|
|
27
|
+
@CreationTimestamp
|
|
28
|
+
@Column(name = "created_at", updatable = false)
|
|
29
|
+
private Instant createdAt;
|
|
30
|
+
|
|
31
|
+
@UpdateTimestamp
|
|
32
|
+
@Column(name = "updated_at")
|
|
33
|
+
private Instant updatedAt;
|
|
34
|
+
|
|
35
|
+
// Relationships
|
|
36
|
+
@OneToOne(mappedBy = "user", cascade = CascadeType.ALL, orphanRemoval = true)
|
|
37
|
+
private Profile profile;
|
|
38
|
+
|
|
39
|
+
@OneToMany(mappedBy = "author", cascade = CascadeType.ALL, orphanRemoval = true)
|
|
40
|
+
@OrderBy("createdAt DESC")
|
|
41
|
+
private List<Post> posts = new ArrayList<>();
|
|
42
|
+
|
|
43
|
+
@ManyToMany
|
|
44
|
+
@JoinTable(
|
|
45
|
+
name = "user_roles",
|
|
46
|
+
joinColumns = @JoinColumn(name = "user_id"),
|
|
47
|
+
inverseJoinColumns = @JoinColumn(name = "role_id")
|
|
48
|
+
)
|
|
49
|
+
private Set<Role> roles = new HashSet<>();
|
|
50
|
+
|
|
51
|
+
// Helper methods for bidirectional relationships
|
|
52
|
+
public void addPost(Post post) {
|
|
53
|
+
posts.add(post);
|
|
54
|
+
post.setAuthor(this);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
public void removePost(Post post) {
|
|
58
|
+
posts.remove(post);
|
|
59
|
+
post.setAuthor(null);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
public void addRole(Role role) {
|
|
63
|
+
roles.add(role);
|
|
64
|
+
role.getUsers().add(this);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Getters and setters...
|
|
68
|
+
|
|
69
|
+
@Override
|
|
70
|
+
public boolean equals(Object o) {
|
|
71
|
+
if (this == o) return true;
|
|
72
|
+
if (!(o instanceof User user)) return false;
|
|
73
|
+
return id != null && id.equals(user.id);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
@Override
|
|
77
|
+
public int hashCode() {
|
|
78
|
+
return getClass().hashCode();
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
@Entity
|
|
83
|
+
@Table(name = "posts")
|
|
84
|
+
public class Post {
|
|
85
|
+
|
|
86
|
+
@Id
|
|
87
|
+
@GeneratedValue(strategy = GenerationType.UUID)
|
|
88
|
+
private String id;
|
|
89
|
+
|
|
90
|
+
@Column(nullable = false)
|
|
91
|
+
private String title;
|
|
92
|
+
|
|
93
|
+
@Column(columnDefinition = "TEXT")
|
|
94
|
+
private String content;
|
|
95
|
+
|
|
96
|
+
private boolean published = false;
|
|
97
|
+
|
|
98
|
+
@ManyToOne(fetch = FetchType.LAZY)
|
|
99
|
+
@JoinColumn(name = "author_id")
|
|
100
|
+
private User author;
|
|
101
|
+
|
|
102
|
+
@ManyToMany
|
|
103
|
+
@JoinTable(
|
|
104
|
+
name = "post_tags",
|
|
105
|
+
joinColumns = @JoinColumn(name = "post_id"),
|
|
106
|
+
inverseJoinColumns = @JoinColumn(name = "tag_id")
|
|
107
|
+
)
|
|
108
|
+
private Set<Tag> tags = new HashSet<>();
|
|
109
|
+
|
|
110
|
+
@CreationTimestamp
|
|
111
|
+
@Column(name = "created_at", updatable = false)
|
|
112
|
+
private Instant createdAt;
|
|
113
|
+
}
|
|
114
|
+
\`\`\`
|
|
115
|
+
|
|
116
|
+
## JPQL Queries
|
|
117
|
+
\`\`\`java
|
|
118
|
+
@Repository
|
|
119
|
+
public interface UserRepository extends JpaRepository<User, String> {
|
|
120
|
+
|
|
121
|
+
// Derived query methods
|
|
122
|
+
Optional<User> findByEmail(String email);
|
|
123
|
+
|
|
124
|
+
List<User> findByIsActiveTrue();
|
|
125
|
+
|
|
126
|
+
boolean existsByEmail(String email);
|
|
127
|
+
|
|
128
|
+
// JPQL queries
|
|
129
|
+
@Query("SELECT u FROM User u WHERE u.isActive = true ORDER BY u.createdAt DESC")
|
|
130
|
+
List<User> findActiveUsers();
|
|
131
|
+
|
|
132
|
+
@Query("SELECT u FROM User u JOIN FETCH u.roles WHERE u.id = :id")
|
|
133
|
+
Optional<User> findByIdWithRoles(@Param("id") String id);
|
|
134
|
+
|
|
135
|
+
@Query("SELECT u FROM User u LEFT JOIN FETCH u.posts WHERE u.id = :id")
|
|
136
|
+
Optional<User> findByIdWithPosts(@Param("id") String id);
|
|
137
|
+
|
|
138
|
+
// Pagination
|
|
139
|
+
@Query("SELECT u FROM User u WHERE u.isActive = true")
|
|
140
|
+
Page<User> findActiveUsersPaged(Pageable pageable);
|
|
141
|
+
|
|
142
|
+
// Modifying queries
|
|
143
|
+
@Modifying
|
|
144
|
+
@Query("UPDATE User u SET u.isActive = false WHERE u.id = :id")
|
|
145
|
+
int deactivateUser(@Param("id") String id);
|
|
146
|
+
|
|
147
|
+
@Modifying
|
|
148
|
+
@Query("DELETE FROM User u WHERE u.isActive = false AND u.createdAt < :date")
|
|
149
|
+
int deleteInactiveOlderThan(@Param("date") Instant date);
|
|
150
|
+
}
|
|
151
|
+
\`\`\`
|
|
152
|
+
|
|
153
|
+
## Criteria API (Dynamic Queries)
|
|
154
|
+
\`\`\`java
|
|
155
|
+
@Repository
|
|
156
|
+
public class UserSearchRepository {
|
|
157
|
+
|
|
158
|
+
@PersistenceContext
|
|
159
|
+
private EntityManager em;
|
|
160
|
+
|
|
161
|
+
public List<User> search(UserSearchCriteria criteria) {
|
|
162
|
+
CriteriaBuilder cb = em.getCriteriaBuilder();
|
|
163
|
+
CriteriaQuery<User> cq = cb.createQuery(User.class);
|
|
164
|
+
Root<User> user = cq.from(User.class);
|
|
165
|
+
|
|
166
|
+
List<Predicate> predicates = new ArrayList<>();
|
|
167
|
+
|
|
168
|
+
if (criteria.getEmail() != null) {
|
|
169
|
+
predicates.add(cb.like(
|
|
170
|
+
cb.lower(user.get("email")),
|
|
171
|
+
"%" + criteria.getEmail().toLowerCase() + "%"
|
|
172
|
+
));
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if (criteria.getName() != null) {
|
|
176
|
+
predicates.add(cb.like(
|
|
177
|
+
cb.lower(user.get("name")),
|
|
178
|
+
"%" + criteria.getName().toLowerCase() + "%"
|
|
179
|
+
));
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if (criteria.getIsActive() != null) {
|
|
183
|
+
predicates.add(cb.equal(user.get("isActive"), criteria.getIsActive()));
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
if (criteria.getCreatedAfter() != null) {
|
|
187
|
+
predicates.add(cb.greaterThan(user.get("createdAt"), criteria.getCreatedAfter()));
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
cq.where(predicates.toArray(new Predicate[0]));
|
|
191
|
+
cq.orderBy(cb.desc(user.get("createdAt")));
|
|
192
|
+
|
|
193
|
+
return em.createQuery(cq)
|
|
194
|
+
.setFirstResult(criteria.getOffset())
|
|
195
|
+
.setMaxResults(criteria.getLimit())
|
|
196
|
+
.getResultList();
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// With Specification (Spring Data JPA)
|
|
201
|
+
public class UserSpecifications {
|
|
202
|
+
|
|
203
|
+
public static Specification<User> emailContains(String email) {
|
|
204
|
+
return (root, query, cb) -> email == null ? null :
|
|
205
|
+
cb.like(cb.lower(root.get("email")), "%" + email.toLowerCase() + "%");
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
public static Specification<User> isActive(Boolean active) {
|
|
209
|
+
return (root, query, cb) -> active == null ? null :
|
|
210
|
+
cb.equal(root.get("isActive"), active);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
public static Specification<User> createdAfter(Instant date) {
|
|
214
|
+
return (root, query, cb) -> date == null ? null :
|
|
215
|
+
cb.greaterThan(root.get("createdAt"), date);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// Usage
|
|
220
|
+
List<User> users = userRepository.findAll(
|
|
221
|
+
Specification.where(emailContains(email))
|
|
222
|
+
.and(isActive(true))
|
|
223
|
+
.and(createdAfter(lastWeek))
|
|
224
|
+
);
|
|
225
|
+
\`\`\`
|
|
226
|
+
|
|
227
|
+
## Entity Graphs (Solving N+1)
|
|
228
|
+
\`\`\`java
|
|
229
|
+
@Entity
|
|
230
|
+
@NamedEntityGraph(
|
|
231
|
+
name = "User.withPostsAndRoles",
|
|
232
|
+
attributeNodes = {
|
|
233
|
+
@NamedAttributeNode("posts"),
|
|
234
|
+
@NamedAttributeNode("roles")
|
|
235
|
+
}
|
|
236
|
+
)
|
|
237
|
+
public class User { ... }
|
|
238
|
+
|
|
239
|
+
// Using in repository
|
|
240
|
+
@EntityGraph(attributePaths = {"posts", "roles"})
|
|
241
|
+
Optional<User> findWithDetailsById(String id);
|
|
242
|
+
|
|
243
|
+
// Or with JPQL
|
|
244
|
+
@Query("SELECT u FROM User u")
|
|
245
|
+
@EntityGraph(attributePaths = {"posts"})
|
|
246
|
+
List<User> findAllWithPosts();
|
|
247
|
+
|
|
248
|
+
// Programmatic entity graph
|
|
249
|
+
public User findWithGraph(String id) {
|
|
250
|
+
EntityGraph<User> graph = em.createEntityGraph(User.class);
|
|
251
|
+
graph.addAttributeNodes("posts", "roles");
|
|
252
|
+
|
|
253
|
+
Map<String, Object> hints = new HashMap<>();
|
|
254
|
+
hints.put("jakarta.persistence.fetchgraph", graph);
|
|
255
|
+
|
|
256
|
+
return em.find(User.class, id, hints);
|
|
257
|
+
}
|
|
258
|
+
\`\`\`
|
|
259
|
+
|
|
260
|
+
## Transactions and Session Management
|
|
261
|
+
\`\`\`java
|
|
262
|
+
@Service
|
|
263
|
+
@Transactional(readOnly = true)
|
|
264
|
+
public class UserService {
|
|
265
|
+
|
|
266
|
+
private final UserRepository userRepository;
|
|
267
|
+
private final EntityManager em;
|
|
268
|
+
|
|
269
|
+
@Transactional
|
|
270
|
+
public User create(CreateUserRequest request) {
|
|
271
|
+
User user = new User();
|
|
272
|
+
user.setEmail(request.email());
|
|
273
|
+
user.setName(request.name());
|
|
274
|
+
return userRepository.save(user);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
@Transactional
|
|
278
|
+
public void updateWithManualFlush(String id, String name) {
|
|
279
|
+
User user = userRepository.findById(id).orElseThrow();
|
|
280
|
+
user.setName(name);
|
|
281
|
+
// Explicit flush if needed before external call
|
|
282
|
+
em.flush();
|
|
283
|
+
// Call external service...
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
@Transactional(propagation = Propagation.REQUIRES_NEW)
|
|
287
|
+
public void auditAction(String action) {
|
|
288
|
+
// Runs in separate transaction
|
|
289
|
+
// Commits even if parent transaction rolls back
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
@Transactional(rollbackFor = Exception.class)
|
|
293
|
+
public void complexOperation() {
|
|
294
|
+
// Rolls back on any exception, not just RuntimeException
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
\`\`\`
|
|
298
|
+
|
|
299
|
+
## Caching
|
|
300
|
+
\`\`\`java
|
|
301
|
+
// Entity with second-level cache
|
|
302
|
+
@Entity
|
|
303
|
+
@Cache(usage = CacheConcurrencyStrategy.READ_WRITE)
|
|
304
|
+
public class User {
|
|
305
|
+
// ...
|
|
306
|
+
|
|
307
|
+
@Cache(usage = CacheConcurrencyStrategy.READ_WRITE)
|
|
308
|
+
@OneToMany(mappedBy = "author")
|
|
309
|
+
private List<Post> posts;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// Query cache
|
|
313
|
+
@QueryHints(@QueryHint(name = "org.hibernate.cacheable", value = "true"))
|
|
314
|
+
List<User> findByIsActiveTrue();
|
|
315
|
+
|
|
316
|
+
// Configuration (application.properties)
|
|
317
|
+
spring.jpa.properties.hibernate.cache.use_second_level_cache=true
|
|
318
|
+
spring.jpa.properties.hibernate.cache.use_query_cache=true
|
|
319
|
+
spring.jpa.properties.hibernate.cache.region.factory_class=org.hibernate.cache.jcache.JCacheRegionFactory
|
|
320
|
+
spring.jpa.properties.hibernate.javax.cache.provider=org.ehcache.jsr107.EhcacheCachingProvider
|
|
321
|
+
|
|
322
|
+
// Cache eviction
|
|
323
|
+
@CacheEvict(value = "users", key = "#id")
|
|
324
|
+
public void delete(String id) {
|
|
325
|
+
userRepository.deleteById(id);
|
|
326
|
+
}
|
|
327
|
+
\`\`\`
|
|
328
|
+
|
|
329
|
+
## Batch Operations
|
|
330
|
+
\`\`\`java
|
|
331
|
+
// Batch inserts - configure in properties
|
|
332
|
+
spring.jpa.properties.hibernate.jdbc.batch_size=50
|
|
333
|
+
spring.jpa.properties.hibernate.order_inserts=true
|
|
334
|
+
spring.jpa.properties.hibernate.order_updates=true
|
|
335
|
+
|
|
336
|
+
// Bulk update
|
|
337
|
+
@Modifying
|
|
338
|
+
@Query("UPDATE User u SET u.isActive = false WHERE u.createdAt < :date")
|
|
339
|
+
int bulkDeactivate(@Param("date") Instant date);
|
|
340
|
+
|
|
341
|
+
// Batch processing large datasets
|
|
342
|
+
@Transactional
|
|
343
|
+
public void processAllUsers() {
|
|
344
|
+
int batchSize = 100;
|
|
345
|
+
int offset = 0;
|
|
346
|
+
List<User> batch;
|
|
347
|
+
|
|
348
|
+
do {
|
|
349
|
+
batch = em.createQuery("SELECT u FROM User u", User.class)
|
|
350
|
+
.setFirstResult(offset)
|
|
351
|
+
.setMaxResults(batchSize)
|
|
352
|
+
.getResultList();
|
|
353
|
+
|
|
354
|
+
for (User user : batch) {
|
|
355
|
+
processUser(user);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
em.flush();
|
|
359
|
+
em.clear(); // Clear persistence context to avoid memory issues
|
|
360
|
+
offset += batchSize;
|
|
361
|
+
} while (!batch.isEmpty());
|
|
362
|
+
}
|
|
363
|
+
\`\`\`
|
|
364
|
+
|
|
365
|
+
## ✅ DO
|
|
366
|
+
- Use \`FetchType.LAZY\` for all relationships (default for @OneToMany/@ManyToMany)
|
|
367
|
+
- Use \`@EntityGraph\` or \`JOIN FETCH\` to solve N+1 problems
|
|
368
|
+
- Override \`equals()\` and \`hashCode()\` using business key or ID
|
|
369
|
+
- Use helper methods for bidirectional relationships
|
|
370
|
+
- Use \`@Transactional(readOnly = true)\` for read operations
|
|
371
|
+
- Clear persistence context (\`em.clear()\`) when processing large batches
|
|
372
|
+
|
|
373
|
+
## ❌ DON'T
|
|
374
|
+
- Don't use \`FetchType.EAGER\` - causes performance issues
|
|
375
|
+
- Don't use \`@Data\` from Lombok on entities (breaks equals/hashCode)
|
|
376
|
+
- Don't call getters on lazy relationships outside transaction
|
|
377
|
+
- Don't forget \`orphanRemoval = true\` when needed
|
|
378
|
+
- Don't use \`CascadeType.ALL\` without understanding implications
|
|
379
|
+
- Don't compare entities with \`==\` (use \`equals()\`)
|
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
# Hono Skill
|
|
2
|
+
|
|
3
|
+
## Project Structure
|
|
4
|
+
\`\`\`
|
|
5
|
+
src/
|
|
6
|
+
├── index.ts # Entry point
|
|
7
|
+
├── app.ts # Hono app factory
|
|
8
|
+
├── middleware/
|
|
9
|
+
│ ├── auth.ts # JWT auth
|
|
10
|
+
│ └── logger.ts # Request logging
|
|
11
|
+
├── routes/
|
|
12
|
+
│ ├── index.ts # Route aggregator
|
|
13
|
+
│ └── users.ts # User routes
|
|
14
|
+
└── lib/
|
|
15
|
+
├── db.ts # Database client
|
|
16
|
+
└── validators.ts # Zod schemas
|
|
17
|
+
\`\`\`
|
|
18
|
+
|
|
19
|
+
## App Setup
|
|
20
|
+
\`\`\`typescript
|
|
21
|
+
import { Hono } from 'hono';
|
|
22
|
+
import { cors } from 'hono/cors';
|
|
23
|
+
import { logger } from 'hono/logger';
|
|
24
|
+
import { secureHeaders } from 'hono/secure-headers';
|
|
25
|
+
import { prettyJSON } from 'hono/pretty-json';
|
|
26
|
+
|
|
27
|
+
import { usersApp } from './routes/users';
|
|
28
|
+
import { authMiddleware } from './middleware/auth';
|
|
29
|
+
import { errorHandler } from './middleware/error';
|
|
30
|
+
|
|
31
|
+
// Type-safe environment variables
|
|
32
|
+
type Bindings = {
|
|
33
|
+
DATABASE_URL: string;
|
|
34
|
+
JWT_SECRET: string;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const app = new Hono<{ Bindings: Bindings }>();
|
|
38
|
+
|
|
39
|
+
// Global middleware
|
|
40
|
+
app.use('*', logger());
|
|
41
|
+
app.use('*', secureHeaders());
|
|
42
|
+
app.use('*', cors({ origin: '*' }));
|
|
43
|
+
app.use('*', prettyJSON());
|
|
44
|
+
|
|
45
|
+
// Error handler
|
|
46
|
+
app.onError(errorHandler);
|
|
47
|
+
|
|
48
|
+
// Routes
|
|
49
|
+
app.route('/api/users', usersApp);
|
|
50
|
+
|
|
51
|
+
// Health check
|
|
52
|
+
app.get('/health', (c) => c.json({ status: 'ok' }));
|
|
53
|
+
|
|
54
|
+
export default app;
|
|
55
|
+
\`\`\`
|
|
56
|
+
|
|
57
|
+
## Routes with Validation
|
|
58
|
+
\`\`\`typescript
|
|
59
|
+
// routes/users.ts
|
|
60
|
+
import { Hono } from 'hono';
|
|
61
|
+
import { zValidator } from '@hono/zod-validator';
|
|
62
|
+
import { z } from 'zod';
|
|
63
|
+
|
|
64
|
+
// Schemas
|
|
65
|
+
const createUserSchema = z.object({
|
|
66
|
+
email: z.string().email(),
|
|
67
|
+
name: z.string().min(1),
|
|
68
|
+
password: z.string().min(8),
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
const updateUserSchema = createUserSchema.partial();
|
|
72
|
+
|
|
73
|
+
const paramsSchema = z.object({
|
|
74
|
+
id: z.string().uuid(),
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
const querySchema = z.object({
|
|
78
|
+
page: z.coerce.number().min(1).default(1),
|
|
79
|
+
limit: z.coerce.number().min(1).max(100).default(10),
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
// Type inference
|
|
83
|
+
type CreateUser = z.infer<typeof createUserSchema>;
|
|
84
|
+
|
|
85
|
+
export const usersApp = new Hono()
|
|
86
|
+
// GET /users
|
|
87
|
+
.get('/', zValidator('query', querySchema), async (c) => {
|
|
88
|
+
const { page, limit } = c.req.valid('query');
|
|
89
|
+
const users = await db.user.findMany({
|
|
90
|
+
skip: (page - 1) * limit,
|
|
91
|
+
take: limit,
|
|
92
|
+
});
|
|
93
|
+
return c.json({ users, page, limit });
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
// GET /users/:id
|
|
97
|
+
.get('/:id', zValidator('param', paramsSchema), async (c) => {
|
|
98
|
+
const { id } = c.req.valid('param');
|
|
99
|
+
const user = await db.user.findUnique({ where: { id } });
|
|
100
|
+
|
|
101
|
+
if (!user) {
|
|
102
|
+
return c.json({ error: 'User not found' }, 404);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return c.json(user);
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
// POST /users
|
|
109
|
+
.post('/', zValidator('json', createUserSchema), async (c) => {
|
|
110
|
+
const data = c.req.valid('json');
|
|
111
|
+
const user = await db.user.create({ data });
|
|
112
|
+
return c.json(user, 201);
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
// PATCH /users/:id
|
|
116
|
+
.patch(
|
|
117
|
+
'/:id',
|
|
118
|
+
zValidator('param', paramsSchema),
|
|
119
|
+
zValidator('json', updateUserSchema),
|
|
120
|
+
async (c) => {
|
|
121
|
+
const { id } = c.req.valid('param');
|
|
122
|
+
const data = c.req.valid('json');
|
|
123
|
+
const user = await db.user.update({ where: { id }, data });
|
|
124
|
+
return c.json(user);
|
|
125
|
+
}
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
// DELETE /users/:id
|
|
129
|
+
.delete('/:id', zValidator('param', paramsSchema), async (c) => {
|
|
130
|
+
const { id } = c.req.valid('param');
|
|
131
|
+
await db.user.delete({ where: { id } });
|
|
132
|
+
return c.body(null, 204);
|
|
133
|
+
});
|
|
134
|
+
\`\`\`
|
|
135
|
+
|
|
136
|
+
## JWT Authentication
|
|
137
|
+
\`\`\`typescript
|
|
138
|
+
// middleware/auth.ts
|
|
139
|
+
import { jwt } from 'hono/jwt';
|
|
140
|
+
import { createMiddleware } from 'hono/factory';
|
|
141
|
+
import type { Context, Next } from 'hono';
|
|
142
|
+
|
|
143
|
+
// Type-safe user in context
|
|
144
|
+
type Variables = {
|
|
145
|
+
user: { id: string; email: string };
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
// JWT middleware
|
|
149
|
+
export const authMiddleware = jwt({
|
|
150
|
+
secret: (c) => c.env.JWT_SECRET,
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
// Custom auth with user loading
|
|
154
|
+
export const requireAuth = createMiddleware<{ Variables: Variables }>(
|
|
155
|
+
async (c, next) => {
|
|
156
|
+
const authHeader = c.req.header('Authorization');
|
|
157
|
+
|
|
158
|
+
if (!authHeader?.startsWith('Bearer ')) {
|
|
159
|
+
return c.json({ error: 'Unauthorized' }, 401);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const token = authHeader.split(' ')[1];
|
|
163
|
+
|
|
164
|
+
try {
|
|
165
|
+
const payload = await verifyToken(token, c.env.JWT_SECRET);
|
|
166
|
+
c.set('user', payload);
|
|
167
|
+
await next();
|
|
168
|
+
} catch {
|
|
169
|
+
return c.json({ error: 'Invalid token' }, 401);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
);
|
|
173
|
+
|
|
174
|
+
// Usage
|
|
175
|
+
app.get('/me', requireAuth, (c) => {
|
|
176
|
+
const user = c.get('user');
|
|
177
|
+
return c.json(user);
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
// Protected route group
|
|
181
|
+
const protectedRoutes = new Hono()
|
|
182
|
+
.use('*', requireAuth)
|
|
183
|
+
.get('/profile', (c) => c.json(c.get('user')))
|
|
184
|
+
.get('/settings', (c) => c.json({ theme: 'dark' }));
|
|
185
|
+
|
|
186
|
+
app.route('/api', protectedRoutes);
|
|
187
|
+
\`\`\`
|
|
188
|
+
|
|
189
|
+
## Error Handling
|
|
190
|
+
\`\`\`typescript
|
|
191
|
+
// middleware/error.ts
|
|
192
|
+
import { HTTPException } from 'hono/http-exception';
|
|
193
|
+
import type { ErrorHandler } from 'hono';
|
|
194
|
+
|
|
195
|
+
export const errorHandler: ErrorHandler = (err, c) => {
|
|
196
|
+
console.error(err);
|
|
197
|
+
|
|
198
|
+
if (err instanceof HTTPException) {
|
|
199
|
+
return c.json({ error: err.message }, err.status);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// Zod validation errors
|
|
203
|
+
if (err.name === 'ZodError') {
|
|
204
|
+
return c.json({
|
|
205
|
+
error: 'Validation failed',
|
|
206
|
+
details: err.errors,
|
|
207
|
+
}, 400);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
return c.json({ error: 'Internal server error' }, 500);
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
// Throwing HTTP errors
|
|
214
|
+
import { HTTPException } from 'hono/http-exception';
|
|
215
|
+
|
|
216
|
+
app.get('/users/:id', async (c) => {
|
|
217
|
+
const user = await db.user.findUnique({ where: { id } });
|
|
218
|
+
|
|
219
|
+
if (!user) {
|
|
220
|
+
throw new HTTPException(404, { message: 'User not found' });
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
return c.json(user);
|
|
224
|
+
});
|
|
225
|
+
\`\`\`
|
|
226
|
+
|
|
227
|
+
## Middleware Patterns
|
|
228
|
+
\`\`\`typescript
|
|
229
|
+
// Rate limiting
|
|
230
|
+
import { rateLimiter } from 'hono-rate-limiter';
|
|
231
|
+
|
|
232
|
+
app.use(
|
|
233
|
+
'/api/*',
|
|
234
|
+
rateLimiter({
|
|
235
|
+
windowMs: 15 * 60 * 1000, // 15 minutes
|
|
236
|
+
limit: 100,
|
|
237
|
+
keyGenerator: (c) => c.req.header('CF-Connecting-IP') ?? 'anonymous',
|
|
238
|
+
})
|
|
239
|
+
);
|
|
240
|
+
|
|
241
|
+
// Request timing
|
|
242
|
+
app.use('*', async (c, next) => {
|
|
243
|
+
const start = Date.now();
|
|
244
|
+
await next();
|
|
245
|
+
const duration = Date.now() - start;
|
|
246
|
+
c.header('X-Response-Time', \`\${duration}ms\`);
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
// CORS with options
|
|
250
|
+
import { cors } from 'hono/cors';
|
|
251
|
+
|
|
252
|
+
app.use('/api/*', cors({
|
|
253
|
+
origin: ['https://example.com'],
|
|
254
|
+
allowMethods: ['GET', 'POST', 'PUT', 'DELETE'],
|
|
255
|
+
allowHeaders: ['Content-Type', 'Authorization'],
|
|
256
|
+
exposeHeaders: ['X-Request-Id'],
|
|
257
|
+
credentials: true,
|
|
258
|
+
}));
|
|
259
|
+
\`\`\`
|
|
260
|
+
|
|
261
|
+
## Edge Runtime (Cloudflare Workers)
|
|
262
|
+
\`\`\`typescript
|
|
263
|
+
// wrangler.toml
|
|
264
|
+
// name = "my-api"
|
|
265
|
+
// main = "src/index.ts"
|
|
266
|
+
// compatibility_date = "2024-01-01"
|
|
267
|
+
|
|
268
|
+
// [vars]
|
|
269
|
+
// ENVIRONMENT = "production"
|
|
270
|
+
|
|
271
|
+
// [[d1_databases]]
|
|
272
|
+
// binding = "DB"
|
|
273
|
+
// database_name = "my-db"
|
|
274
|
+
|
|
275
|
+
import { Hono } from 'hono';
|
|
276
|
+
|
|
277
|
+
type Bindings = {
|
|
278
|
+
DB: D1Database;
|
|
279
|
+
ENVIRONMENT: string;
|
|
280
|
+
};
|
|
281
|
+
|
|
282
|
+
const app = new Hono<{ Bindings: Bindings }>();
|
|
283
|
+
|
|
284
|
+
app.get('/users', async (c) => {
|
|
285
|
+
const { results } = await c.env.DB
|
|
286
|
+
.prepare('SELECT * FROM users')
|
|
287
|
+
.all();
|
|
288
|
+
return c.json(results);
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
export default app;
|
|
292
|
+
\`\`\`
|
|
293
|
+
|
|
294
|
+
## ❌ DON'T
|
|
295
|
+
- Skip input validation
|
|
296
|
+
- Expose stack traces in production
|
|
297
|
+
- Forget error handling middleware
|
|
298
|
+
- Block the event loop with sync operations
|
|
299
|
+
|
|
300
|
+
## ✅ DO
|
|
301
|
+
- Use Zod for validation (@hono/zod-validator)
|
|
302
|
+
- Use middleware for cross-cutting concerns
|
|
303
|
+
- Use typed context (Bindings, Variables)
|
|
304
|
+
- Deploy to edge (Cloudflare, Vercel, Deno)
|
|
305
|
+
- Handle errors with HTTPException
|
|
306
|
+
- Use route groups for organization
|