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.
- 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/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,382 @@
|
|
|
1
|
+
# SeaORM Skill
|
|
2
|
+
|
|
3
|
+
## Setup and Migrations
|
|
4
|
+
\`\`\`bash
|
|
5
|
+
# Install sea-orm-cli
|
|
6
|
+
cargo install sea-orm-cli
|
|
7
|
+
|
|
8
|
+
# Initialize migration directory
|
|
9
|
+
sea-orm-cli migrate init
|
|
10
|
+
|
|
11
|
+
# Create migration
|
|
12
|
+
sea-orm-cli migrate generate create_users
|
|
13
|
+
|
|
14
|
+
# Run migrations
|
|
15
|
+
sea-orm-cli migrate up
|
|
16
|
+
|
|
17
|
+
# Generate entities from database
|
|
18
|
+
sea-orm-cli generate entity -o src/entities --with-serde both
|
|
19
|
+
\`\`\`
|
|
20
|
+
|
|
21
|
+
## Entity Definition
|
|
22
|
+
\`\`\`rust
|
|
23
|
+
// src/entities/user.rs
|
|
24
|
+
use sea_orm::entity::prelude::*;
|
|
25
|
+
use serde::{Deserialize, Serialize};
|
|
26
|
+
|
|
27
|
+
#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel, Serialize, Deserialize)]
|
|
28
|
+
#[sea_orm(table_name = "users")]
|
|
29
|
+
pub struct Model {
|
|
30
|
+
#[sea_orm(primary_key, auto_increment = false)]
|
|
31
|
+
pub id: Uuid,
|
|
32
|
+
#[sea_orm(unique)]
|
|
33
|
+
pub email: String,
|
|
34
|
+
pub name: String,
|
|
35
|
+
#[serde(skip_serializing)]
|
|
36
|
+
pub password_hash: String,
|
|
37
|
+
pub is_active: bool,
|
|
38
|
+
pub created_at: DateTimeUtc,
|
|
39
|
+
pub updated_at: DateTimeUtc,
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
|
43
|
+
pub enum Relation {
|
|
44
|
+
#[sea_orm(has_many = "super::post::Entity")]
|
|
45
|
+
Posts,
|
|
46
|
+
#[sea_orm(has_one = "super::profile::Entity")]
|
|
47
|
+
Profile,
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
impl Related<super::post::Entity> for Entity {
|
|
51
|
+
fn to() -> RelationDef {
|
|
52
|
+
Relation::Posts.def()
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
impl Related<super::profile::Entity> for Entity {
|
|
57
|
+
fn to() -> RelationDef {
|
|
58
|
+
Relation::Profile.def()
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
impl ActiveModelBehavior for ActiveModel {}
|
|
63
|
+
|
|
64
|
+
// src/entities/post.rs
|
|
65
|
+
#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel, Serialize, Deserialize)]
|
|
66
|
+
#[sea_orm(table_name = "posts")]
|
|
67
|
+
pub struct Model {
|
|
68
|
+
#[sea_orm(primary_key, auto_increment = false)]
|
|
69
|
+
pub id: Uuid,
|
|
70
|
+
pub title: String,
|
|
71
|
+
#[sea_orm(column_type = "Text")]
|
|
72
|
+
pub content: String,
|
|
73
|
+
pub published: bool,
|
|
74
|
+
pub author_id: Uuid,
|
|
75
|
+
pub created_at: DateTimeUtc,
|
|
76
|
+
pub updated_at: DateTimeUtc,
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
|
80
|
+
pub enum Relation {
|
|
81
|
+
#[sea_orm(
|
|
82
|
+
belongs_to = "super::user::Entity",
|
|
83
|
+
from = "Column::AuthorId",
|
|
84
|
+
to = "super::user::Column::Id"
|
|
85
|
+
)]
|
|
86
|
+
Author,
|
|
87
|
+
#[sea_orm(has_many = "super::comment::Entity")]
|
|
88
|
+
Comments,
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
impl Related<super::user::Entity> for Entity {
|
|
92
|
+
fn to() -> RelationDef {
|
|
93
|
+
Relation::Author.def()
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
\`\`\`
|
|
97
|
+
|
|
98
|
+
## Database Connection
|
|
99
|
+
\`\`\`rust
|
|
100
|
+
use sea_orm::{Database, DatabaseConnection, ConnectOptions};
|
|
101
|
+
use std::time::Duration;
|
|
102
|
+
|
|
103
|
+
pub async fn create_connection() -> Result<DatabaseConnection, DbErr> {
|
|
104
|
+
let database_url = std::env::var("DATABASE_URL")
|
|
105
|
+
.expect("DATABASE_URL must be set");
|
|
106
|
+
|
|
107
|
+
let mut opts = ConnectOptions::new(database_url);
|
|
108
|
+
opts.max_connections(100)
|
|
109
|
+
.min_connections(5)
|
|
110
|
+
.connect_timeout(Duration::from_secs(10))
|
|
111
|
+
.idle_timeout(Duration::from_secs(300))
|
|
112
|
+
.sqlx_logging(true)
|
|
113
|
+
.sqlx_logging_level(log::LevelFilter::Debug);
|
|
114
|
+
|
|
115
|
+
Database::connect(opts).await
|
|
116
|
+
}
|
|
117
|
+
\`\`\`
|
|
118
|
+
|
|
119
|
+
## CRUD Operations
|
|
120
|
+
\`\`\`rust
|
|
121
|
+
use sea_orm::*;
|
|
122
|
+
use crate::entities::{user, post};
|
|
123
|
+
use crate::entities::prelude::{User, Post};
|
|
124
|
+
|
|
125
|
+
pub struct UserRepository {
|
|
126
|
+
db: DatabaseConnection,
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
impl UserRepository {
|
|
130
|
+
pub fn new(db: DatabaseConnection) -> Self {
|
|
131
|
+
Self { db }
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Create
|
|
135
|
+
pub async fn create(&self, email: &str, name: &str, password: &str) -> Result<user::Model, AppError> {
|
|
136
|
+
let password_hash = hash_password(password)?;
|
|
137
|
+
|
|
138
|
+
let new_user = user::ActiveModel {
|
|
139
|
+
id: Set(Uuid::new_v4()),
|
|
140
|
+
email: Set(email.to_string()),
|
|
141
|
+
name: Set(name.to_string()),
|
|
142
|
+
password_hash: Set(password_hash),
|
|
143
|
+
is_active: Set(true),
|
|
144
|
+
created_at: Set(Utc::now()),
|
|
145
|
+
updated_at: Set(Utc::now()),
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
new_user.insert(&self.db).await.map_err(AppError::from)
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Find by ID
|
|
152
|
+
pub async fn find_by_id(&self, id: Uuid) -> Result<Option<user::Model>, AppError> {
|
|
153
|
+
User::find_by_id(id)
|
|
154
|
+
.one(&self.db)
|
|
155
|
+
.await
|
|
156
|
+
.map_err(AppError::from)
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Find by email
|
|
160
|
+
pub async fn find_by_email(&self, email: &str) -> Result<Option<user::Model>, AppError> {
|
|
161
|
+
User::find()
|
|
162
|
+
.filter(user::Column::Email.eq(email))
|
|
163
|
+
.one(&self.db)
|
|
164
|
+
.await
|
|
165
|
+
.map_err(AppError::from)
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// List with pagination
|
|
169
|
+
pub async fn list(&self, page: u64, per_page: u64) -> Result<(Vec<user::Model>, u64), AppError> {
|
|
170
|
+
let paginator = User::find()
|
|
171
|
+
.filter(user::Column::IsActive.eq(true))
|
|
172
|
+
.order_by_desc(user::Column::CreatedAt)
|
|
173
|
+
.paginate(&self.db, per_page);
|
|
174
|
+
|
|
175
|
+
let total = paginator.num_items().await?;
|
|
176
|
+
let users = paginator.fetch_page(page - 1).await?;
|
|
177
|
+
|
|
178
|
+
Ok((users, total))
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Update
|
|
182
|
+
pub async fn update(&self, id: Uuid, name: Option<String>, email: Option<String>) -> Result<user::Model, AppError> {
|
|
183
|
+
let user = User::find_by_id(id)
|
|
184
|
+
.one(&self.db)
|
|
185
|
+
.await?
|
|
186
|
+
.ok_or(AppError::NotFound)?;
|
|
187
|
+
|
|
188
|
+
let mut active: user::ActiveModel = user.into();
|
|
189
|
+
|
|
190
|
+
if let Some(n) = name {
|
|
191
|
+
active.name = Set(n);
|
|
192
|
+
}
|
|
193
|
+
if let Some(e) = email {
|
|
194
|
+
active.email = Set(e);
|
|
195
|
+
}
|
|
196
|
+
active.updated_at = Set(Utc::now());
|
|
197
|
+
|
|
198
|
+
active.update(&self.db).await.map_err(AppError::from)
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Delete
|
|
202
|
+
pub async fn delete(&self, id: Uuid) -> Result<DeleteResult, AppError> {
|
|
203
|
+
User::delete_by_id(id)
|
|
204
|
+
.exec(&self.db)
|
|
205
|
+
.await
|
|
206
|
+
.map_err(AppError::from)
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
\`\`\`
|
|
210
|
+
|
|
211
|
+
## Relationships and Eager Loading
|
|
212
|
+
\`\`\`rust
|
|
213
|
+
// Find user with posts
|
|
214
|
+
pub async fn find_user_with_posts(&self, id: Uuid) -> Result<Option<(user::Model, Vec<post::Model>)>, AppError> {
|
|
215
|
+
User::find_by_id(id)
|
|
216
|
+
.find_with_related(Post)
|
|
217
|
+
.all(&self.db)
|
|
218
|
+
.await
|
|
219
|
+
.map(|results| results.into_iter().next())
|
|
220
|
+
.map_err(AppError::from)
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// Find posts with authors
|
|
224
|
+
pub async fn find_posts_with_authors(&self) -> Result<Vec<(post::Model, Option<user::Model>)>, AppError> {
|
|
225
|
+
Post::find()
|
|
226
|
+
.filter(post::Column::Published.eq(true))
|
|
227
|
+
.find_also_related(User)
|
|
228
|
+
.order_by_desc(post::Column::CreatedAt)
|
|
229
|
+
.all(&self.db)
|
|
230
|
+
.await
|
|
231
|
+
.map_err(AppError::from)
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// Custom select with partial fields
|
|
235
|
+
#[derive(FromQueryResult, Serialize)]
|
|
236
|
+
pub struct UserSummary {
|
|
237
|
+
pub id: Uuid,
|
|
238
|
+
pub email: String,
|
|
239
|
+
pub name: String,
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
pub async fn list_summaries(&self) -> Result<Vec<UserSummary>, AppError> {
|
|
243
|
+
User::find()
|
|
244
|
+
.select_only()
|
|
245
|
+
.column(user::Column::Id)
|
|
246
|
+
.column(user::Column::Email)
|
|
247
|
+
.column(user::Column::Name)
|
|
248
|
+
.filter(user::Column::IsActive.eq(true))
|
|
249
|
+
.into_model::<UserSummary>()
|
|
250
|
+
.all(&self.db)
|
|
251
|
+
.await
|
|
252
|
+
.map_err(AppError::from)
|
|
253
|
+
}
|
|
254
|
+
\`\`\`
|
|
255
|
+
|
|
256
|
+
## Complex Queries
|
|
257
|
+
\`\`\`rust
|
|
258
|
+
use sea_orm::{QueryOrder, QuerySelect, Condition};
|
|
259
|
+
|
|
260
|
+
// Complex filtering
|
|
261
|
+
pub async fn search_users(
|
|
262
|
+
&self,
|
|
263
|
+
query: Option<&str>,
|
|
264
|
+
is_active: Option<bool>,
|
|
265
|
+
page: u64,
|
|
266
|
+
per_page: u64,
|
|
267
|
+
) -> Result<Vec<user::Model>, AppError> {
|
|
268
|
+
let mut condition = Condition::all();
|
|
269
|
+
|
|
270
|
+
if let Some(q) = query {
|
|
271
|
+
condition = condition.add(
|
|
272
|
+
Condition::any()
|
|
273
|
+
.add(user::Column::Name.contains(q))
|
|
274
|
+
.add(user::Column::Email.contains(q))
|
|
275
|
+
);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
if let Some(active) = is_active {
|
|
279
|
+
condition = condition.add(user::Column::IsActive.eq(active));
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
User::find()
|
|
283
|
+
.filter(condition)
|
|
284
|
+
.order_by_desc(user::Column::CreatedAt)
|
|
285
|
+
.offset((page - 1) * per_page)
|
|
286
|
+
.limit(per_page)
|
|
287
|
+
.all(&self.db)
|
|
288
|
+
.await
|
|
289
|
+
.map_err(AppError::from)
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// Aggregate query with raw SQL
|
|
293
|
+
use sea_orm::{FromQueryResult, Statement};
|
|
294
|
+
|
|
295
|
+
#[derive(FromQueryResult)]
|
|
296
|
+
pub struct UserStats {
|
|
297
|
+
pub user_id: Uuid,
|
|
298
|
+
pub post_count: i64,
|
|
299
|
+
pub comment_count: i64,
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
pub async fn get_user_stats(&self, user_id: Uuid) -> Result<UserStats, AppError> {
|
|
303
|
+
UserStats::find_by_statement(Statement::from_sql_and_values(
|
|
304
|
+
DbBackend::Postgres,
|
|
305
|
+
r#"
|
|
306
|
+
SELECT
|
|
307
|
+
u.id as user_id,
|
|
308
|
+
COUNT(DISTINCT p.id) as post_count,
|
|
309
|
+
COUNT(DISTINCT c.id) as comment_count
|
|
310
|
+
FROM users u
|
|
311
|
+
LEFT JOIN posts p ON p.author_id = u.id
|
|
312
|
+
LEFT JOIN comments c ON c.user_id = u.id
|
|
313
|
+
WHERE u.id = $1
|
|
314
|
+
GROUP BY u.id
|
|
315
|
+
"#,
|
|
316
|
+
[user_id.into()],
|
|
317
|
+
))
|
|
318
|
+
.one(&self.db)
|
|
319
|
+
.await?
|
|
320
|
+
.ok_or(AppError::NotFound)
|
|
321
|
+
}
|
|
322
|
+
\`\`\`
|
|
323
|
+
|
|
324
|
+
## Transactions
|
|
325
|
+
\`\`\`rust
|
|
326
|
+
use sea_orm::TransactionTrait;
|
|
327
|
+
|
|
328
|
+
pub async fn create_user_with_profile(
|
|
329
|
+
&self,
|
|
330
|
+
email: &str,
|
|
331
|
+
name: &str,
|
|
332
|
+
password: &str,
|
|
333
|
+
bio: &str,
|
|
334
|
+
) -> Result<(user::Model, profile::Model), AppError> {
|
|
335
|
+
self.db.transaction::<_, (user::Model, profile::Model), AppError>(|txn| {
|
|
336
|
+
Box::pin(async move {
|
|
337
|
+
// Create user
|
|
338
|
+
let user = user::ActiveModel {
|
|
339
|
+
id: Set(Uuid::new_v4()),
|
|
340
|
+
email: Set(email.to_string()),
|
|
341
|
+
name: Set(name.to_string()),
|
|
342
|
+
password_hash: Set(hash_password(password)?),
|
|
343
|
+
is_active: Set(true),
|
|
344
|
+
created_at: Set(Utc::now()),
|
|
345
|
+
updated_at: Set(Utc::now()),
|
|
346
|
+
}
|
|
347
|
+
.insert(txn)
|
|
348
|
+
.await?;
|
|
349
|
+
|
|
350
|
+
// Create profile
|
|
351
|
+
let profile = profile::ActiveModel {
|
|
352
|
+
id: Set(Uuid::new_v4()),
|
|
353
|
+
user_id: Set(user.id),
|
|
354
|
+
bio: Set(bio.to_string()),
|
|
355
|
+
}
|
|
356
|
+
.insert(txn)
|
|
357
|
+
.await?;
|
|
358
|
+
|
|
359
|
+
Ok((user, profile))
|
|
360
|
+
})
|
|
361
|
+
})
|
|
362
|
+
.await
|
|
363
|
+
.map_err(|e| match e {
|
|
364
|
+
TransactionError::Connection(e) => AppError::Database(e),
|
|
365
|
+
TransactionError::Transaction(e) => e,
|
|
366
|
+
})
|
|
367
|
+
}
|
|
368
|
+
\`\`\`
|
|
369
|
+
|
|
370
|
+
## ✅ DO
|
|
371
|
+
- Use \`Set()\` to set values in ActiveModel
|
|
372
|
+
- Use \`NotSet\` for optional fields in updates
|
|
373
|
+
- Use \`.paginate()\` for pagination
|
|
374
|
+
- Use \`find_with_related()\` for eager loading
|
|
375
|
+
- Use transactions for multi-step operations
|
|
376
|
+
- Use \`FromQueryResult\` for custom query results
|
|
377
|
+
|
|
378
|
+
## ❌ DON'T
|
|
379
|
+
- Don't forget to derive \`DeriveEntityModel\` for entities
|
|
380
|
+
- Don't use \`.unwrap()\` - handle \`DbErr\` properly
|
|
381
|
+
- Don't load relationships separately when you can use \`find_with_related\`
|
|
382
|
+
- Don't forget to define \`Relation\` enum for relationships
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
# Security Skill
|
|
2
|
+
|
|
3
|
+
## Input Validation
|
|
4
|
+
\`\`\`typescript
|
|
5
|
+
// ✅ ALWAYS validate with Zod
|
|
6
|
+
import { z } from 'zod';
|
|
7
|
+
|
|
8
|
+
const userSchema = z.object({
|
|
9
|
+
email: z.string().email().max(255),
|
|
10
|
+
name: z.string().min(1).max(100).trim(),
|
|
11
|
+
age: z.number().int().min(0).max(150).optional()
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
const result = userSchema.safeParse(input);
|
|
15
|
+
if (!result.success) throw new ValidationError(result.error);
|
|
16
|
+
\`\`\`
|
|
17
|
+
|
|
18
|
+
## SQL Injection Prevention
|
|
19
|
+
\`\`\`typescript
|
|
20
|
+
// ❌ NEVER: String concatenation
|
|
21
|
+
db.query(\`SELECT * FROM users WHERE id = '\${userId}'\`);
|
|
22
|
+
|
|
23
|
+
// ✅ ALWAYS: Parameterized queries or ORM
|
|
24
|
+
await prisma.user.findUnique({ where: { id: userId } });
|
|
25
|
+
\`\`\`
|
|
26
|
+
|
|
27
|
+
## XSS Prevention
|
|
28
|
+
\`\`\`tsx
|
|
29
|
+
// ❌ NEVER with user content
|
|
30
|
+
<div dangerouslySetInnerHTML={{ __html: userInput }} />
|
|
31
|
+
|
|
32
|
+
// ✅ React auto-escapes
|
|
33
|
+
<div>{userInput}</div>
|
|
34
|
+
|
|
35
|
+
// ✅ If HTML needed, sanitize
|
|
36
|
+
import DOMPurify from 'dompurify';
|
|
37
|
+
<div dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(content) }} />
|
|
38
|
+
\`\`\`
|
|
39
|
+
|
|
40
|
+
## CORS Configuration
|
|
41
|
+
\`\`\`typescript
|
|
42
|
+
// Express with cors middleware
|
|
43
|
+
import cors from 'cors';
|
|
44
|
+
|
|
45
|
+
// ❌ DON'T: Allow all origins in production
|
|
46
|
+
app.use(cors()); // Allows everything
|
|
47
|
+
|
|
48
|
+
// ✅ DO: Whitelist specific origins
|
|
49
|
+
app.use(cors({
|
|
50
|
+
origin: ['https://myapp.com', 'https://admin.myapp.com'],
|
|
51
|
+
methods: ['GET', 'POST', 'PUT', 'DELETE'],
|
|
52
|
+
allowedHeaders: ['Content-Type', 'Authorization'],
|
|
53
|
+
credentials: true, // If using cookies
|
|
54
|
+
maxAge: 86400 // Cache preflight for 24 hours
|
|
55
|
+
}));
|
|
56
|
+
\`\`\`
|
|
57
|
+
|
|
58
|
+
## Secure Headers (Helmet.js)
|
|
59
|
+
\`\`\`typescript
|
|
60
|
+
import helmet from 'helmet';
|
|
61
|
+
|
|
62
|
+
app.use(helmet()); // Applies all defaults
|
|
63
|
+
|
|
64
|
+
// Or configure individually:
|
|
65
|
+
app.use(helmet({
|
|
66
|
+
contentSecurityPolicy: {
|
|
67
|
+
directives: {
|
|
68
|
+
defaultSrc: ["'self'"],
|
|
69
|
+
scriptSrc: ["'self'", "'unsafe-inline'"], // Avoid if possible
|
|
70
|
+
styleSrc: ["'self'", "'unsafe-inline'"],
|
|
71
|
+
imgSrc: ["'self'", "data:", "https:"],
|
|
72
|
+
connectSrc: ["'self'", "https://api.myapp.com"],
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
hsts: { maxAge: 31536000, includeSubDomains: true },
|
|
76
|
+
frameguard: { action: 'deny' }, // Prevent clickjacking
|
|
77
|
+
}));
|
|
78
|
+
\`\`\`
|
|
79
|
+
|
|
80
|
+
## Rate Limiting
|
|
81
|
+
\`\`\`typescript
|
|
82
|
+
import rateLimit from 'express-rate-limit';
|
|
83
|
+
|
|
84
|
+
// General API rate limiting
|
|
85
|
+
const apiLimiter = rateLimit({
|
|
86
|
+
windowMs: 15 * 60 * 1000, // 15 minutes
|
|
87
|
+
max: 100, // 100 requests per window
|
|
88
|
+
message: { error: 'Too many requests, try again later' },
|
|
89
|
+
standardHeaders: true, // Return rate limit info in headers
|
|
90
|
+
});
|
|
91
|
+
app.use('/api/', apiLimiter);
|
|
92
|
+
|
|
93
|
+
// Stricter limit for auth endpoints
|
|
94
|
+
const authLimiter = rateLimit({
|
|
95
|
+
windowMs: 60 * 60 * 1000, // 1 hour
|
|
96
|
+
max: 5, // 5 attempts per hour
|
|
97
|
+
skipSuccessfulRequests: true, // Don't count successful logins
|
|
98
|
+
});
|
|
99
|
+
app.use('/api/auth/login', authLimiter);
|
|
100
|
+
\`\`\`
|
|
101
|
+
|
|
102
|
+
## CSRF Protection
|
|
103
|
+
\`\`\`typescript
|
|
104
|
+
// For traditional forms (not needed for JWT-only APIs)
|
|
105
|
+
import csrf from 'csurf';
|
|
106
|
+
|
|
107
|
+
app.use(csrf({ cookie: true }));
|
|
108
|
+
|
|
109
|
+
// Include token in forms
|
|
110
|
+
app.get('/form', (req, res) => {
|
|
111
|
+
res.render('form', { csrfToken: req.csrfToken() });
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
// In HTML: <input type="hidden" name="_csrf" value="{{csrfToken}}">
|
|
115
|
+
\`\`\`
|
|
116
|
+
|
|
117
|
+
## Authentication Patterns
|
|
118
|
+
\`\`\`typescript
|
|
119
|
+
// ✅ Password hashing (12+ rounds)
|
|
120
|
+
const hash = await bcrypt.hash(password, 12);
|
|
121
|
+
|
|
122
|
+
// ✅ JWT with short expiry + refresh tokens
|
|
123
|
+
const accessToken = jwt.sign({ userId }, ACCESS_SECRET, { expiresIn: '15m' });
|
|
124
|
+
const refreshToken = jwt.sign({ userId }, REFRESH_SECRET, { expiresIn: '7d' });
|
|
125
|
+
|
|
126
|
+
// ✅ Secure cookie settings
|
|
127
|
+
res.cookie('refreshToken', refreshToken, {
|
|
128
|
+
httpOnly: true, // Not accessible via JavaScript
|
|
129
|
+
secure: true, // HTTPS only
|
|
130
|
+
sameSite: 'strict', // CSRF protection
|
|
131
|
+
maxAge: 7 * 24 * 60 * 60 * 1000 // 7 days
|
|
132
|
+
});
|
|
133
|
+
\`\`\`
|
|
134
|
+
|
|
135
|
+
## Environment Variables
|
|
136
|
+
\`\`\`typescript
|
|
137
|
+
// ✅ Use a validation library
|
|
138
|
+
import { z } from 'zod';
|
|
139
|
+
|
|
140
|
+
const envSchema = z.object({
|
|
141
|
+
DATABASE_URL: z.string().url(),
|
|
142
|
+
JWT_SECRET: z.string().min(32),
|
|
143
|
+
NODE_ENV: z.enum(['development', 'production', 'test']),
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
// Validate at startup - fail fast if missing
|
|
147
|
+
export const env = envSchema.parse(process.env);
|
|
148
|
+
\`\`\`
|
|
149
|
+
|
|
150
|
+
## ❌ DON'T
|
|
151
|
+
- Store passwords in plain text
|
|
152
|
+
- Expose stack traces in production
|
|
153
|
+
- Trust client-side validation alone
|
|
154
|
+
- Store secrets in code
|
|
155
|
+
- Use * for CORS origin in production
|
|
156
|
+
- Skip rate limiting on auth endpoints
|
|
157
|
+
- Log sensitive data (passwords, tokens)
|
|
158
|
+
|
|
159
|
+
## ✅ DO
|
|
160
|
+
- Validate ALL input server-side
|
|
161
|
+
- Use environment variables for secrets
|
|
162
|
+
- Implement rate limiting
|
|
163
|
+
- Use HTTPS everywhere
|
|
164
|
+
- Keep dependencies updated
|
|
165
|
+
- Use Helmet for secure headers
|
|
166
|
+
- Configure CORS properly
|
|
167
|
+
- Validate env vars at startup
|