autoworkflow 3.1.5 → 3.6.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 +26 -0
- 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/post-edit.sh +190 -17
- package/.claude/hooks/pre-edit.sh +221 -0
- package/.claude/hooks/session-check.sh +90 -0
- package/.claude/settings.json +56 -6
- package/.claude/settings.local.json +5 -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 +163 -52
- package/package.json +1 -1
- package/system/triggers.md +256 -17
|
@@ -0,0 +1,392 @@
|
|
|
1
|
+
# Diesel ORM Skill
|
|
2
|
+
|
|
3
|
+
## Setup and Configuration
|
|
4
|
+
\`\`\`bash
|
|
5
|
+
# Install diesel CLI
|
|
6
|
+
cargo install diesel_cli --no-default-features --features postgres
|
|
7
|
+
|
|
8
|
+
# Initialize diesel
|
|
9
|
+
diesel setup
|
|
10
|
+
|
|
11
|
+
# Create migration
|
|
12
|
+
diesel migration generate create_users
|
|
13
|
+
|
|
14
|
+
# Run migrations
|
|
15
|
+
diesel migration run
|
|
16
|
+
|
|
17
|
+
# Revert migration
|
|
18
|
+
diesel migration revert
|
|
19
|
+
\`\`\`
|
|
20
|
+
|
|
21
|
+
## Schema Definition (schema.rs - auto-generated)
|
|
22
|
+
\`\`\`rust
|
|
23
|
+
// src/schema.rs - Generated by diesel migration run
|
|
24
|
+
diesel::table! {
|
|
25
|
+
users (id) {
|
|
26
|
+
id -> Uuid,
|
|
27
|
+
email -> Varchar,
|
|
28
|
+
name -> Varchar,
|
|
29
|
+
password_hash -> Varchar,
|
|
30
|
+
is_active -> Bool,
|
|
31
|
+
created_at -> Timestamptz,
|
|
32
|
+
updated_at -> Timestamptz,
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
diesel::table! {
|
|
37
|
+
posts (id) {
|
|
38
|
+
id -> Uuid,
|
|
39
|
+
title -> Varchar,
|
|
40
|
+
content -> Text,
|
|
41
|
+
published -> Bool,
|
|
42
|
+
author_id -> Uuid,
|
|
43
|
+
created_at -> Timestamptz,
|
|
44
|
+
updated_at -> Timestamptz,
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
diesel::table! {
|
|
49
|
+
comments (id) {
|
|
50
|
+
id -> Uuid,
|
|
51
|
+
post_id -> Uuid,
|
|
52
|
+
user_id -> Uuid,
|
|
53
|
+
content -> Text,
|
|
54
|
+
created_at -> Timestamptz,
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
diesel::joinable!(posts -> users (author_id));
|
|
59
|
+
diesel::joinable!(comments -> posts (post_id));
|
|
60
|
+
diesel::joinable!(comments -> users (user_id));
|
|
61
|
+
|
|
62
|
+
diesel::allow_tables_to_appear_in_same_query!(
|
|
63
|
+
users,
|
|
64
|
+
posts,
|
|
65
|
+
comments,
|
|
66
|
+
);
|
|
67
|
+
\`\`\`
|
|
68
|
+
|
|
69
|
+
## Model Definitions
|
|
70
|
+
\`\`\`rust
|
|
71
|
+
use diesel::prelude::*;
|
|
72
|
+
use chrono::{DateTime, Utc};
|
|
73
|
+
use serde::{Deserialize, Serialize};
|
|
74
|
+
use uuid::Uuid;
|
|
75
|
+
|
|
76
|
+
// Queryable - for SELECT queries
|
|
77
|
+
#[derive(Debug, Clone, Queryable, Selectable, Serialize)]
|
|
78
|
+
#[diesel(table_name = crate::schema::users)]
|
|
79
|
+
pub struct User {
|
|
80
|
+
pub id: Uuid,
|
|
81
|
+
pub email: String,
|
|
82
|
+
pub name: String,
|
|
83
|
+
#[serde(skip_serializing)]
|
|
84
|
+
pub password_hash: String,
|
|
85
|
+
pub is_active: bool,
|
|
86
|
+
pub created_at: DateTime<Utc>,
|
|
87
|
+
pub updated_at: DateTime<Utc>,
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Insertable - for INSERT queries
|
|
91
|
+
#[derive(Debug, Insertable)]
|
|
92
|
+
#[diesel(table_name = crate::schema::users)]
|
|
93
|
+
pub struct NewUser {
|
|
94
|
+
pub id: Uuid,
|
|
95
|
+
pub email: String,
|
|
96
|
+
pub name: String,
|
|
97
|
+
pub password_hash: String,
|
|
98
|
+
pub is_active: bool,
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
impl NewUser {
|
|
102
|
+
pub fn new(email: &str, name: &str, password: &str) -> Result<Self, AppError> {
|
|
103
|
+
let password_hash = hash_password(password)?;
|
|
104
|
+
Ok(Self {
|
|
105
|
+
id: Uuid::new_v4(),
|
|
106
|
+
email: email.to_string(),
|
|
107
|
+
name: name.to_string(),
|
|
108
|
+
password_hash,
|
|
109
|
+
is_active: true,
|
|
110
|
+
})
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// AsChangeset - for UPDATE queries
|
|
115
|
+
#[derive(Debug, AsChangeset)]
|
|
116
|
+
#[diesel(table_name = crate::schema::users)]
|
|
117
|
+
pub struct UpdateUser {
|
|
118
|
+
pub name: Option<String>,
|
|
119
|
+
pub email: Option<String>,
|
|
120
|
+
pub is_active: Option<bool>,
|
|
121
|
+
pub updated_at: DateTime<Utc>,
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Associations
|
|
125
|
+
#[derive(Debug, Queryable, Selectable, Associations, Serialize)]
|
|
126
|
+
#[diesel(table_name = crate::schema::posts)]
|
|
127
|
+
#[diesel(belongs_to(User, foreign_key = author_id))]
|
|
128
|
+
pub struct Post {
|
|
129
|
+
pub id: Uuid,
|
|
130
|
+
pub title: String,
|
|
131
|
+
pub content: String,
|
|
132
|
+
pub published: bool,
|
|
133
|
+
pub author_id: Uuid,
|
|
134
|
+
pub created_at: DateTime<Utc>,
|
|
135
|
+
pub updated_at: DateTime<Utc>,
|
|
136
|
+
}
|
|
137
|
+
\`\`\`
|
|
138
|
+
|
|
139
|
+
## Connection Pool
|
|
140
|
+
\`\`\`rust
|
|
141
|
+
use diesel::pg::PgConnection;
|
|
142
|
+
use diesel::r2d2::{self, ConnectionManager, Pool, PooledConnection};
|
|
143
|
+
use std::env;
|
|
144
|
+
|
|
145
|
+
pub type DbPool = Pool<ConnectionManager<PgConnection>>;
|
|
146
|
+
pub type DbConnection = PooledConnection<ConnectionManager<PgConnection>>;
|
|
147
|
+
|
|
148
|
+
pub fn create_pool() -> DbPool {
|
|
149
|
+
let database_url = env::var("DATABASE_URL")
|
|
150
|
+
.expect("DATABASE_URL must be set");
|
|
151
|
+
|
|
152
|
+
let manager = ConnectionManager::<PgConnection>::new(database_url);
|
|
153
|
+
|
|
154
|
+
Pool::builder()
|
|
155
|
+
.max_size(10)
|
|
156
|
+
.min_idle(Some(2))
|
|
157
|
+
.build(manager)
|
|
158
|
+
.expect("Failed to create pool")
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Get connection from pool
|
|
162
|
+
pub fn get_conn(pool: &DbPool) -> Result<DbConnection, AppError> {
|
|
163
|
+
pool.get().map_err(|e| AppError::Database(e.to_string()))
|
|
164
|
+
}
|
|
165
|
+
\`\`\`
|
|
166
|
+
|
|
167
|
+
## CRUD Operations
|
|
168
|
+
\`\`\`rust
|
|
169
|
+
use diesel::prelude::*;
|
|
170
|
+
use crate::schema::users::dsl::*;
|
|
171
|
+
|
|
172
|
+
pub struct UserRepository;
|
|
173
|
+
|
|
174
|
+
impl UserRepository {
|
|
175
|
+
// Create
|
|
176
|
+
pub fn create(conn: &mut PgConnection, new_user: NewUser) -> Result<User, AppError> {
|
|
177
|
+
diesel::insert_into(users)
|
|
178
|
+
.values(&new_user)
|
|
179
|
+
.returning(User::as_returning())
|
|
180
|
+
.get_result(conn)
|
|
181
|
+
.map_err(|e| AppError::Database(e.to_string()))
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Read by ID
|
|
185
|
+
pub fn find_by_id(conn: &mut PgConnection, user_id: Uuid) -> Result<Option<User>, AppError> {
|
|
186
|
+
users
|
|
187
|
+
.filter(id.eq(user_id))
|
|
188
|
+
.select(User::as_select())
|
|
189
|
+
.first(conn)
|
|
190
|
+
.optional()
|
|
191
|
+
.map_err(|e| AppError::Database(e.to_string()))
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Read by email
|
|
195
|
+
pub fn find_by_email(conn: &mut PgConnection, user_email: &str) -> Result<Option<User>, AppError> {
|
|
196
|
+
users
|
|
197
|
+
.filter(email.eq(user_email))
|
|
198
|
+
.select(User::as_select())
|
|
199
|
+
.first(conn)
|
|
200
|
+
.optional()
|
|
201
|
+
.map_err(|e| AppError::Database(e.to_string()))
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// List with pagination
|
|
205
|
+
pub fn list(
|
|
206
|
+
conn: &mut PgConnection,
|
|
207
|
+
page: i64,
|
|
208
|
+
per_page: i64,
|
|
209
|
+
) -> Result<(Vec<User>, i64), AppError> {
|
|
210
|
+
let offset = (page - 1) * per_page;
|
|
211
|
+
|
|
212
|
+
let results = users
|
|
213
|
+
.filter(is_active.eq(true))
|
|
214
|
+
.order(created_at.desc())
|
|
215
|
+
.offset(offset)
|
|
216
|
+
.limit(per_page)
|
|
217
|
+
.select(User::as_select())
|
|
218
|
+
.load(conn)
|
|
219
|
+
.map_err(|e| AppError::Database(e.to_string()))?;
|
|
220
|
+
|
|
221
|
+
let total: i64 = users
|
|
222
|
+
.filter(is_active.eq(true))
|
|
223
|
+
.count()
|
|
224
|
+
.get_result(conn)
|
|
225
|
+
.map_err(|e| AppError::Database(e.to_string()))?;
|
|
226
|
+
|
|
227
|
+
Ok((results, total))
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// Update
|
|
231
|
+
pub fn update(
|
|
232
|
+
conn: &mut PgConnection,
|
|
233
|
+
user_id: Uuid,
|
|
234
|
+
changeset: UpdateUser,
|
|
235
|
+
) -> Result<User, AppError> {
|
|
236
|
+
diesel::update(users.filter(id.eq(user_id)))
|
|
237
|
+
.set(&changeset)
|
|
238
|
+
.returning(User::as_returning())
|
|
239
|
+
.get_result(conn)
|
|
240
|
+
.map_err(|e| AppError::Database(e.to_string()))
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// Delete
|
|
244
|
+
pub fn delete(conn: &mut PgConnection, user_id: Uuid) -> Result<usize, AppError> {
|
|
245
|
+
diesel::delete(users.filter(id.eq(user_id)))
|
|
246
|
+
.execute(conn)
|
|
247
|
+
.map_err(|e| AppError::Database(e.to_string()))
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
\`\`\`
|
|
251
|
+
|
|
252
|
+
## Complex Queries
|
|
253
|
+
\`\`\`rust
|
|
254
|
+
use diesel::prelude::*;
|
|
255
|
+
use crate::schema::{users, posts, comments};
|
|
256
|
+
|
|
257
|
+
// Join queries
|
|
258
|
+
pub fn get_user_with_posts(
|
|
259
|
+
conn: &mut PgConnection,
|
|
260
|
+
user_id: Uuid,
|
|
261
|
+
) -> Result<(User, Vec<Post>), AppError> {
|
|
262
|
+
let user = users::table
|
|
263
|
+
.filter(users::id.eq(user_id))
|
|
264
|
+
.select(User::as_select())
|
|
265
|
+
.first(conn)?;
|
|
266
|
+
|
|
267
|
+
let user_posts = Post::belonging_to(&user)
|
|
268
|
+
.select(Post::as_select())
|
|
269
|
+
.load(conn)?;
|
|
270
|
+
|
|
271
|
+
Ok((user, user_posts))
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// Load posts with authors
|
|
275
|
+
pub fn get_posts_with_authors(
|
|
276
|
+
conn: &mut PgConnection,
|
|
277
|
+
) -> Result<Vec<(Post, User)>, AppError> {
|
|
278
|
+
posts::table
|
|
279
|
+
.inner_join(users::table)
|
|
280
|
+
.filter(posts::published.eq(true))
|
|
281
|
+
.order(posts::created_at.desc())
|
|
282
|
+
.select((Post::as_select(), User::as_select()))
|
|
283
|
+
.load(conn)
|
|
284
|
+
.map_err(|e| AppError::Database(e.to_string()))
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// Aggregate query
|
|
288
|
+
pub fn get_post_stats(conn: &mut PgConnection, post_id: Uuid) -> Result<PostStats, AppError> {
|
|
289
|
+
use diesel::dsl::count;
|
|
290
|
+
|
|
291
|
+
let comment_count: i64 = comments::table
|
|
292
|
+
.filter(comments::post_id.eq(post_id))
|
|
293
|
+
.select(count(comments::id))
|
|
294
|
+
.first(conn)?;
|
|
295
|
+
|
|
296
|
+
Ok(PostStats { comment_count })
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// Raw SQL when needed
|
|
300
|
+
use diesel::sql_query;
|
|
301
|
+
use diesel::sql_types::{Uuid as SqlUuid, BigInt};
|
|
302
|
+
|
|
303
|
+
#[derive(QueryableByName, Debug)]
|
|
304
|
+
pub struct UserStats {
|
|
305
|
+
#[diesel(sql_type = SqlUuid)]
|
|
306
|
+
pub user_id: Uuid,
|
|
307
|
+
#[diesel(sql_type = BigInt)]
|
|
308
|
+
pub post_count: i64,
|
|
309
|
+
#[diesel(sql_type = BigInt)]
|
|
310
|
+
pub comment_count: i64,
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
pub fn get_user_stats(conn: &mut PgConnection, user_id: Uuid) -> Result<UserStats, AppError> {
|
|
314
|
+
sql_query(r#"
|
|
315
|
+
SELECT
|
|
316
|
+
u.id as user_id,
|
|
317
|
+
COUNT(DISTINCT p.id) as post_count,
|
|
318
|
+
COUNT(DISTINCT c.id) as comment_count
|
|
319
|
+
FROM users u
|
|
320
|
+
LEFT JOIN posts p ON p.author_id = u.id
|
|
321
|
+
LEFT JOIN comments c ON c.user_id = u.id
|
|
322
|
+
WHERE u.id = $1
|
|
323
|
+
GROUP BY u.id
|
|
324
|
+
"#)
|
|
325
|
+
.bind::<SqlUuid, _>(user_id)
|
|
326
|
+
.get_result(conn)
|
|
327
|
+
.map_err(|e| AppError::Database(e.to_string()))
|
|
328
|
+
}
|
|
329
|
+
\`\`\`
|
|
330
|
+
|
|
331
|
+
## Transactions
|
|
332
|
+
\`\`\`rust
|
|
333
|
+
use diesel::Connection;
|
|
334
|
+
|
|
335
|
+
pub fn create_user_with_profile(
|
|
336
|
+
conn: &mut PgConnection,
|
|
337
|
+
new_user: NewUser,
|
|
338
|
+
new_profile: NewProfile,
|
|
339
|
+
) -> Result<(User, Profile), AppError> {
|
|
340
|
+
conn.transaction(|conn| {
|
|
341
|
+
let user = diesel::insert_into(users::table)
|
|
342
|
+
.values(&new_user)
|
|
343
|
+
.returning(User::as_returning())
|
|
344
|
+
.get_result(conn)?;
|
|
345
|
+
|
|
346
|
+
let profile = diesel::insert_into(profiles::table)
|
|
347
|
+
.values(&NewProfile { user_id: user.id, ..new_profile })
|
|
348
|
+
.returning(Profile::as_returning())
|
|
349
|
+
.get_result(conn)?;
|
|
350
|
+
|
|
351
|
+
Ok((user, profile))
|
|
352
|
+
})
|
|
353
|
+
.map_err(|e: diesel::result::Error| AppError::Database(e.to_string()))
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// Nested transaction with savepoints
|
|
357
|
+
pub fn complex_operation(conn: &mut PgConnection) -> Result<(), AppError> {
|
|
358
|
+
conn.transaction(|conn| {
|
|
359
|
+
// First operation
|
|
360
|
+
create_user(conn, user1)?;
|
|
361
|
+
|
|
362
|
+
// Savepoint for nested transaction
|
|
363
|
+
let result = conn.transaction(|conn| {
|
|
364
|
+
create_user(conn, user2)
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
// Handle nested transaction failure
|
|
368
|
+
if let Err(e) = result {
|
|
369
|
+
tracing::warn!("Nested transaction failed: {:?}", e);
|
|
370
|
+
// Outer transaction continues
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
Ok(())
|
|
374
|
+
})
|
|
375
|
+
}
|
|
376
|
+
\`\`\`
|
|
377
|
+
|
|
378
|
+
## ✅ DO
|
|
379
|
+
- Use \`Queryable\` and \`Selectable\` for SELECT
|
|
380
|
+
- Use \`Insertable\` for INSERT
|
|
381
|
+
- Use \`AsChangeset\` for UPDATE
|
|
382
|
+
- Use \`#[diesel(belongs_to(...))]\` for associations
|
|
383
|
+
- Use connection pools (r2d2) in production
|
|
384
|
+
- Use transactions for multi-step operations
|
|
385
|
+
- Use \`.optional()\` for queries that may return no results
|
|
386
|
+
|
|
387
|
+
## ❌ DON'T
|
|
388
|
+
- Don't use \`.get_result()\` when you don't need the result
|
|
389
|
+
- Don't forget to run \`diesel migration run\` after schema changes
|
|
390
|
+
- Don't manually write schema.rs (it's auto-generated)
|
|
391
|
+
- Don't use \`.first()\` without \`.optional()\` unless you're sure it exists
|
|
392
|
+
- Don't hold connections longer than necessary
|
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
# Django Skill
|
|
2
|
+
|
|
3
|
+
## Project Structure
|
|
4
|
+
\`\`\`
|
|
5
|
+
myproject/
|
|
6
|
+
├── manage.py
|
|
7
|
+
├── config/ # Project settings
|
|
8
|
+
│ ├── __init__.py
|
|
9
|
+
│ ├── settings/
|
|
10
|
+
│ │ ├── base.py
|
|
11
|
+
│ │ ├── development.py
|
|
12
|
+
│ │ └── production.py
|
|
13
|
+
│ ├── urls.py
|
|
14
|
+
│ └── wsgi.py
|
|
15
|
+
└── apps/
|
|
16
|
+
└── users/
|
|
17
|
+
├── __init__.py
|
|
18
|
+
├── admin.py
|
|
19
|
+
├── apps.py
|
|
20
|
+
├── models.py
|
|
21
|
+
├── views.py
|
|
22
|
+
├── serializers.py # DRF
|
|
23
|
+
├── urls.py
|
|
24
|
+
├── signals.py
|
|
25
|
+
├── tasks.py # Celery
|
|
26
|
+
└── tests/
|
|
27
|
+
├── test_models.py
|
|
28
|
+
└── test_views.py
|
|
29
|
+
\`\`\`
|
|
30
|
+
|
|
31
|
+
## Models
|
|
32
|
+
\`\`\`python
|
|
33
|
+
from django.db import models
|
|
34
|
+
from django.contrib.auth.models import AbstractUser
|
|
35
|
+
|
|
36
|
+
class User(AbstractUser):
|
|
37
|
+
email = models.EmailField(unique=True)
|
|
38
|
+
bio = models.TextField(blank=True)
|
|
39
|
+
avatar = models.ImageField(upload_to='avatars/', null=True, blank=True)
|
|
40
|
+
created_at = models.DateTimeField(auto_now_add=True)
|
|
41
|
+
updated_at = models.DateTimeField(auto_now=True)
|
|
42
|
+
|
|
43
|
+
USERNAME_FIELD = 'email'
|
|
44
|
+
REQUIRED_FIELDS = ['username']
|
|
45
|
+
|
|
46
|
+
class Meta:
|
|
47
|
+
ordering = ['-created_at']
|
|
48
|
+
indexes = [
|
|
49
|
+
models.Index(fields=['email']),
|
|
50
|
+
models.Index(fields=['-created_at']),
|
|
51
|
+
]
|
|
52
|
+
|
|
53
|
+
def __str__(self):
|
|
54
|
+
return self.email
|
|
55
|
+
|
|
56
|
+
@property
|
|
57
|
+
def full_name(self):
|
|
58
|
+
return f"{self.first_name} {self.last_name}".strip()
|
|
59
|
+
|
|
60
|
+
class Post(models.Model):
|
|
61
|
+
author = models.ForeignKey(User, on_delete=models.CASCADE, related_name='posts')
|
|
62
|
+
title = models.CharField(max_length=200)
|
|
63
|
+
content = models.TextField()
|
|
64
|
+
published = models.BooleanField(default=False)
|
|
65
|
+
created_at = models.DateTimeField(auto_now_add=True)
|
|
66
|
+
|
|
67
|
+
class Meta:
|
|
68
|
+
ordering = ['-created_at']
|
|
69
|
+
|
|
70
|
+
# Manager for common queries
|
|
71
|
+
objects = models.Manager()
|
|
72
|
+
|
|
73
|
+
class PublishedManager(models.Manager):
|
|
74
|
+
def get_queryset(self):
|
|
75
|
+
return super().get_queryset().filter(published=True)
|
|
76
|
+
|
|
77
|
+
published_objects = PublishedManager()
|
|
78
|
+
\`\`\`
|
|
79
|
+
|
|
80
|
+
## Django REST Framework
|
|
81
|
+
\`\`\`python
|
|
82
|
+
# serializers.py
|
|
83
|
+
from rest_framework import serializers
|
|
84
|
+
|
|
85
|
+
class UserSerializer(serializers.ModelSerializer):
|
|
86
|
+
full_name = serializers.ReadOnlyField()
|
|
87
|
+
posts_count = serializers.SerializerMethodField()
|
|
88
|
+
|
|
89
|
+
class Meta:
|
|
90
|
+
model = User
|
|
91
|
+
fields = ['id', 'email', 'username', 'full_name', 'posts_count', 'created_at']
|
|
92
|
+
read_only_fields = ['id', 'created_at']
|
|
93
|
+
|
|
94
|
+
def get_posts_count(self, obj):
|
|
95
|
+
return obj.posts.count()
|
|
96
|
+
|
|
97
|
+
class UserCreateSerializer(serializers.ModelSerializer):
|
|
98
|
+
password = serializers.CharField(write_only=True, min_length=8)
|
|
99
|
+
|
|
100
|
+
class Meta:
|
|
101
|
+
model = User
|
|
102
|
+
fields = ['email', 'username', 'password']
|
|
103
|
+
|
|
104
|
+
def create(self, validated_data):
|
|
105
|
+
return User.objects.create_user(**validated_data)
|
|
106
|
+
|
|
107
|
+
# views.py
|
|
108
|
+
from rest_framework import viewsets, permissions, status
|
|
109
|
+
from rest_framework.decorators import action
|
|
110
|
+
from rest_framework.response import Response
|
|
111
|
+
|
|
112
|
+
class UserViewSet(viewsets.ModelViewSet):
|
|
113
|
+
queryset = User.objects.all()
|
|
114
|
+
permission_classes = [permissions.IsAuthenticated]
|
|
115
|
+
|
|
116
|
+
def get_serializer_class(self):
|
|
117
|
+
if self.action == 'create':
|
|
118
|
+
return UserCreateSerializer
|
|
119
|
+
return UserSerializer
|
|
120
|
+
|
|
121
|
+
def get_queryset(self):
|
|
122
|
+
# Optimize queries
|
|
123
|
+
return User.objects.prefetch_related('posts').all()
|
|
124
|
+
|
|
125
|
+
@action(detail=False, methods=['get'])
|
|
126
|
+
def me(self, request):
|
|
127
|
+
serializer = self.get_serializer(request.user)
|
|
128
|
+
return Response(serializer.data)
|
|
129
|
+
|
|
130
|
+
@action(detail=True, methods=['post'])
|
|
131
|
+
def follow(self, request, pk=None):
|
|
132
|
+
user = self.get_object()
|
|
133
|
+
request.user.following.add(user)
|
|
134
|
+
return Response({'status': 'following'})
|
|
135
|
+
|
|
136
|
+
# urls.py
|
|
137
|
+
from rest_framework.routers import DefaultRouter
|
|
138
|
+
|
|
139
|
+
router = DefaultRouter()
|
|
140
|
+
router.register('users', UserViewSet)
|
|
141
|
+
|
|
142
|
+
urlpatterns = router.urls
|
|
143
|
+
\`\`\`
|
|
144
|
+
|
|
145
|
+
## Views (Class-Based)
|
|
146
|
+
\`\`\`python
|
|
147
|
+
from django.views.generic import ListView, DetailView, CreateView
|
|
148
|
+
from django.contrib.auth.mixins import LoginRequiredMixin
|
|
149
|
+
|
|
150
|
+
class PostListView(ListView):
|
|
151
|
+
model = Post
|
|
152
|
+
template_name = 'posts/list.html'
|
|
153
|
+
context_object_name = 'posts'
|
|
154
|
+
paginate_by = 10
|
|
155
|
+
|
|
156
|
+
def get_queryset(self):
|
|
157
|
+
return Post.published_objects.select_related('author')
|
|
158
|
+
|
|
159
|
+
class PostDetailView(DetailView):
|
|
160
|
+
model = Post
|
|
161
|
+
template_name = 'posts/detail.html'
|
|
162
|
+
|
|
163
|
+
class PostCreateView(LoginRequiredMixin, CreateView):
|
|
164
|
+
model = Post
|
|
165
|
+
fields = ['title', 'content']
|
|
166
|
+
success_url = '/posts/'
|
|
167
|
+
|
|
168
|
+
def form_valid(self, form):
|
|
169
|
+
form.instance.author = self.request.user
|
|
170
|
+
return super().form_valid(form)
|
|
171
|
+
\`\`\`
|
|
172
|
+
|
|
173
|
+
## Signals
|
|
174
|
+
\`\`\`python
|
|
175
|
+
# signals.py
|
|
176
|
+
from django.db.models.signals import post_save, pre_delete
|
|
177
|
+
from django.dispatch import receiver
|
|
178
|
+
from django.core.mail import send_mail
|
|
179
|
+
|
|
180
|
+
@receiver(post_save, sender=User)
|
|
181
|
+
def send_welcome_email(sender, instance, created, **kwargs):
|
|
182
|
+
if created:
|
|
183
|
+
send_mail(
|
|
184
|
+
'Welcome!',
|
|
185
|
+
'Thanks for signing up.',
|
|
186
|
+
'noreply@example.com',
|
|
187
|
+
[instance.email],
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
@receiver(pre_delete, sender=User)
|
|
191
|
+
def cleanup_user_data(sender, instance, **kwargs):
|
|
192
|
+
# Clean up related data
|
|
193
|
+
instance.posts.all().delete()
|
|
194
|
+
|
|
195
|
+
# apps.py - Register signals
|
|
196
|
+
class UsersConfig(AppConfig):
|
|
197
|
+
name = 'apps.users'
|
|
198
|
+
|
|
199
|
+
def ready(self):
|
|
200
|
+
import apps.users.signals # noqa
|
|
201
|
+
\`\`\`
|
|
202
|
+
|
|
203
|
+
## Middleware
|
|
204
|
+
\`\`\`python
|
|
205
|
+
# middleware.py
|
|
206
|
+
import time
|
|
207
|
+
import logging
|
|
208
|
+
|
|
209
|
+
logger = logging.getLogger(__name__)
|
|
210
|
+
|
|
211
|
+
class RequestTimingMiddleware:
|
|
212
|
+
def __init__(self, get_response):
|
|
213
|
+
self.get_response = get_response
|
|
214
|
+
|
|
215
|
+
def __call__(self, request):
|
|
216
|
+
start_time = time.time()
|
|
217
|
+
response = self.get_response(request)
|
|
218
|
+
duration = time.time() - start_time
|
|
219
|
+
logger.info(f"{request.method} {request.path} - {duration:.2f}s")
|
|
220
|
+
return response
|
|
221
|
+
|
|
222
|
+
class CurrentUserMiddleware:
|
|
223
|
+
def __init__(self, get_response):
|
|
224
|
+
self.get_response = get_response
|
|
225
|
+
|
|
226
|
+
def __call__(self, request):
|
|
227
|
+
# Make user available in thread-local
|
|
228
|
+
from threading import local
|
|
229
|
+
_thread_locals = local()
|
|
230
|
+
_thread_locals.user = request.user
|
|
231
|
+
return self.get_response(request)
|
|
232
|
+
\`\`\`
|
|
233
|
+
|
|
234
|
+
## Admin Customization
|
|
235
|
+
\`\`\`python
|
|
236
|
+
from django.contrib import admin
|
|
237
|
+
from django.contrib.auth.admin import UserAdmin as BaseUserAdmin
|
|
238
|
+
|
|
239
|
+
@admin.register(User)
|
|
240
|
+
class UserAdmin(BaseUserAdmin):
|
|
241
|
+
list_display = ['email', 'username', 'is_active', 'created_at']
|
|
242
|
+
list_filter = ['is_active', 'is_staff', 'created_at']
|
|
243
|
+
search_fields = ['email', 'username']
|
|
244
|
+
ordering = ['-created_at']
|
|
245
|
+
|
|
246
|
+
fieldsets = BaseUserAdmin.fieldsets + (
|
|
247
|
+
('Profile', {'fields': ('bio', 'avatar')}),
|
|
248
|
+
)
|
|
249
|
+
|
|
250
|
+
@admin.register(Post)
|
|
251
|
+
class PostAdmin(admin.ModelAdmin):
|
|
252
|
+
list_display = ['title', 'author', 'published', 'created_at']
|
|
253
|
+
list_filter = ['published', 'created_at']
|
|
254
|
+
search_fields = ['title', 'content']
|
|
255
|
+
raw_id_fields = ['author']
|
|
256
|
+
date_hierarchy = 'created_at'
|
|
257
|
+
|
|
258
|
+
actions = ['publish_posts']
|
|
259
|
+
|
|
260
|
+
@admin.action(description='Publish selected posts')
|
|
261
|
+
def publish_posts(self, request, queryset):
|
|
262
|
+
queryset.update(published=True)
|
|
263
|
+
\`\`\`
|
|
264
|
+
|
|
265
|
+
## Query Optimization
|
|
266
|
+
\`\`\`python
|
|
267
|
+
# ❌ N+1 Problem
|
|
268
|
+
posts = Post.objects.all()
|
|
269
|
+
for post in posts:
|
|
270
|
+
print(post.author.name) # Query per post!
|
|
271
|
+
|
|
272
|
+
# ✅ select_related (ForeignKey, OneToOne)
|
|
273
|
+
posts = Post.objects.select_related('author').all()
|
|
274
|
+
|
|
275
|
+
# ✅ prefetch_related (ManyToMany, reverse FK)
|
|
276
|
+
users = User.objects.prefetch_related('posts').all()
|
|
277
|
+
|
|
278
|
+
# ✅ Only select needed fields
|
|
279
|
+
users = User.objects.only('id', 'email').all()
|
|
280
|
+
users = User.objects.values('id', 'email')
|
|
281
|
+
|
|
282
|
+
# ✅ Aggregate queries
|
|
283
|
+
from django.db.models import Count, Avg
|
|
284
|
+
User.objects.annotate(post_count=Count('posts'))
|
|
285
|
+
\`\`\`
|
|
286
|
+
|
|
287
|
+
## ❌ DON'T
|
|
288
|
+
- Put business logic in views
|
|
289
|
+
- Use raw SQL without parameterization
|
|
290
|
+
- Forget migrations after model changes
|
|
291
|
+
- Skip select_related/prefetch_related
|
|
292
|
+
- Use synchronous code in async views
|
|
293
|
+
|
|
294
|
+
## ✅ DO
|
|
295
|
+
- Fat models, thin views
|
|
296
|
+
- Use select_related/prefetch_related
|
|
297
|
+
- Use Django REST Framework for APIs
|
|
298
|
+
- Use signals for decoupled logic
|
|
299
|
+
- Customize admin for easy management
|
|
300
|
+
- Use class-based views for reusability
|
|
301
|
+
- Write tests for models and views
|