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,395 @@
|
|
|
1
|
+
# Sequelize Skill
|
|
2
|
+
|
|
3
|
+
## Configuration
|
|
4
|
+
\`\`\`typescript
|
|
5
|
+
import { Sequelize } from 'sequelize';
|
|
6
|
+
|
|
7
|
+
const sequelize = new Sequelize(process.env.DATABASE_URL!, {
|
|
8
|
+
dialect: 'postgres',
|
|
9
|
+
logging: process.env.NODE_ENV === 'development' ? console.log : false,
|
|
10
|
+
pool: {
|
|
11
|
+
max: 10,
|
|
12
|
+
min: 0,
|
|
13
|
+
acquire: 30000,
|
|
14
|
+
idle: 10000,
|
|
15
|
+
},
|
|
16
|
+
define: {
|
|
17
|
+
timestamps: true, // Adds createdAt, updatedAt
|
|
18
|
+
underscored: true, // Uses snake_case column names
|
|
19
|
+
paranoid: true, // Soft deletes (deletedAt)
|
|
20
|
+
},
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
// Test connection
|
|
24
|
+
await sequelize.authenticate();
|
|
25
|
+
\`\`\`
|
|
26
|
+
|
|
27
|
+
## Model Definition (TypeScript)
|
|
28
|
+
\`\`\`typescript
|
|
29
|
+
import {
|
|
30
|
+
Model, DataTypes, InferAttributes, InferCreationAttributes,
|
|
31
|
+
CreationOptional, ForeignKey, NonAttribute, Association
|
|
32
|
+
} from 'sequelize';
|
|
33
|
+
|
|
34
|
+
class User extends Model<InferAttributes<User>, InferCreationAttributes<User>> {
|
|
35
|
+
declare id: CreationOptional<number>;
|
|
36
|
+
declare email: string;
|
|
37
|
+
declare name: string;
|
|
38
|
+
declare password: string;
|
|
39
|
+
declare role: 'user' | 'admin';
|
|
40
|
+
declare createdAt: CreationOptional<Date>;
|
|
41
|
+
declare updatedAt: CreationOptional<Date>;
|
|
42
|
+
declare deletedAt: CreationOptional<Date | null>;
|
|
43
|
+
|
|
44
|
+
// Associations
|
|
45
|
+
declare posts?: NonAttribute<Post[]>;
|
|
46
|
+
declare static associations: {
|
|
47
|
+
posts: Association<User, Post>;
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
// Instance methods
|
|
51
|
+
async comparePassword(candidate: string): Promise<boolean> {
|
|
52
|
+
return bcrypt.compare(candidate, this.password);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
User.init({
|
|
57
|
+
id: {
|
|
58
|
+
type: DataTypes.INTEGER,
|
|
59
|
+
autoIncrement: true,
|
|
60
|
+
primaryKey: true,
|
|
61
|
+
},
|
|
62
|
+
email: {
|
|
63
|
+
type: DataTypes.STRING(255),
|
|
64
|
+
allowNull: false,
|
|
65
|
+
unique: true,
|
|
66
|
+
validate: {
|
|
67
|
+
isEmail: true,
|
|
68
|
+
notEmpty: true,
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
name: {
|
|
72
|
+
type: DataTypes.STRING(100),
|
|
73
|
+
allowNull: false,
|
|
74
|
+
},
|
|
75
|
+
password: {
|
|
76
|
+
type: DataTypes.STRING,
|
|
77
|
+
allowNull: false,
|
|
78
|
+
},
|
|
79
|
+
role: {
|
|
80
|
+
type: DataTypes.ENUM('user', 'admin'),
|
|
81
|
+
defaultValue: 'user',
|
|
82
|
+
},
|
|
83
|
+
createdAt: DataTypes.DATE,
|
|
84
|
+
updatedAt: DataTypes.DATE,
|
|
85
|
+
deletedAt: DataTypes.DATE,
|
|
86
|
+
}, {
|
|
87
|
+
sequelize,
|
|
88
|
+
tableName: 'users',
|
|
89
|
+
indexes: [
|
|
90
|
+
{ fields: ['email'] },
|
|
91
|
+
{ fields: ['role', 'created_at'] },
|
|
92
|
+
],
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
class Post extends Model<InferAttributes<Post>, InferCreationAttributes<Post>> {
|
|
96
|
+
declare id: CreationOptional<number>;
|
|
97
|
+
declare title: string;
|
|
98
|
+
declare content: string | null;
|
|
99
|
+
declare published: boolean;
|
|
100
|
+
declare authorId: ForeignKey<User['id']>;
|
|
101
|
+
declare author?: NonAttribute<User>;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
Post.init({
|
|
105
|
+
id: {
|
|
106
|
+
type: DataTypes.INTEGER,
|
|
107
|
+
autoIncrement: true,
|
|
108
|
+
primaryKey: true,
|
|
109
|
+
},
|
|
110
|
+
title: {
|
|
111
|
+
type: DataTypes.STRING,
|
|
112
|
+
allowNull: false,
|
|
113
|
+
},
|
|
114
|
+
content: DataTypes.TEXT,
|
|
115
|
+
published: {
|
|
116
|
+
type: DataTypes.BOOLEAN,
|
|
117
|
+
defaultValue: false,
|
|
118
|
+
},
|
|
119
|
+
}, { sequelize, tableName: 'posts' });
|
|
120
|
+
\`\`\`
|
|
121
|
+
|
|
122
|
+
## Associations
|
|
123
|
+
\`\`\`typescript
|
|
124
|
+
// One-to-Many
|
|
125
|
+
User.hasMany(Post, {
|
|
126
|
+
foreignKey: 'authorId',
|
|
127
|
+
as: 'posts',
|
|
128
|
+
onDelete: 'CASCADE',
|
|
129
|
+
});
|
|
130
|
+
Post.belongsTo(User, {
|
|
131
|
+
foreignKey: 'authorId',
|
|
132
|
+
as: 'author',
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
// Many-to-Many
|
|
136
|
+
User.belongsToMany(Role, {
|
|
137
|
+
through: 'user_roles',
|
|
138
|
+
as: 'roles',
|
|
139
|
+
});
|
|
140
|
+
Role.belongsToMany(User, {
|
|
141
|
+
through: 'user_roles',
|
|
142
|
+
as: 'users',
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
// One-to-One
|
|
146
|
+
User.hasOne(Profile, {
|
|
147
|
+
foreignKey: 'userId',
|
|
148
|
+
as: 'profile',
|
|
149
|
+
});
|
|
150
|
+
Profile.belongsTo(User, {
|
|
151
|
+
foreignKey: 'userId',
|
|
152
|
+
as: 'user',
|
|
153
|
+
});
|
|
154
|
+
\`\`\`
|
|
155
|
+
|
|
156
|
+
## Query Patterns
|
|
157
|
+
\`\`\`typescript
|
|
158
|
+
import { Op } from 'sequelize';
|
|
159
|
+
|
|
160
|
+
// Find operations
|
|
161
|
+
const user = await User.findByPk(id);
|
|
162
|
+
const user = await User.findOne({ where: { email } });
|
|
163
|
+
const users = await User.findAll({ where: { role: 'user' } });
|
|
164
|
+
|
|
165
|
+
// With eager loading
|
|
166
|
+
const users = await User.findAll({
|
|
167
|
+
where: { role: 'user' },
|
|
168
|
+
include: [{
|
|
169
|
+
model: Post,
|
|
170
|
+
as: 'posts',
|
|
171
|
+
where: { published: true },
|
|
172
|
+
required: false, // LEFT JOIN
|
|
173
|
+
}],
|
|
174
|
+
order: [['createdAt', 'DESC']],
|
|
175
|
+
limit: 20,
|
|
176
|
+
offset: 0,
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
// Complex conditions with operators
|
|
180
|
+
const users = await User.findAll({
|
|
181
|
+
where: {
|
|
182
|
+
[Op.and]: [
|
|
183
|
+
{ role: 'user' },
|
|
184
|
+
{ createdAt: { [Op.gte]: new Date('2024-01-01') } },
|
|
185
|
+
{
|
|
186
|
+
[Op.or]: [
|
|
187
|
+
{ email: { [Op.like]: '%@company.com' } },
|
|
188
|
+
{ name: { [Op.startsWith]: 'Admin' } },
|
|
189
|
+
],
|
|
190
|
+
},
|
|
191
|
+
],
|
|
192
|
+
},
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
// Count and exists
|
|
196
|
+
const count = await User.count({ where: { role: 'user' } });
|
|
197
|
+
const exists = await User.findOne({ where: { email } }) !== null;
|
|
198
|
+
|
|
199
|
+
// Select specific attributes
|
|
200
|
+
const users = await User.findAll({
|
|
201
|
+
attributes: ['id', 'email', 'name'],
|
|
202
|
+
// Or exclude: attributes: { exclude: ['password'] }
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
// Aggregations
|
|
206
|
+
const stats = await User.findAll({
|
|
207
|
+
attributes: [
|
|
208
|
+
'role',
|
|
209
|
+
[sequelize.fn('COUNT', sequelize.col('id')), 'count'],
|
|
210
|
+
],
|
|
211
|
+
group: ['role'],
|
|
212
|
+
});
|
|
213
|
+
\`\`\`
|
|
214
|
+
|
|
215
|
+
## Create, Update, Delete
|
|
216
|
+
\`\`\`typescript
|
|
217
|
+
// Create
|
|
218
|
+
const user = await User.create({
|
|
219
|
+
email: 'user@example.com',
|
|
220
|
+
name: 'John',
|
|
221
|
+
password: await bcrypt.hash('password', 10),
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
// Bulk create
|
|
225
|
+
const users = await User.bulkCreate([
|
|
226
|
+
{ email: 'user1@example.com', name: 'User 1' },
|
|
227
|
+
{ email: 'user2@example.com', name: 'User 2' },
|
|
228
|
+
], { validate: true });
|
|
229
|
+
|
|
230
|
+
// Update
|
|
231
|
+
await user.update({ name: 'Jane' });
|
|
232
|
+
|
|
233
|
+
// Or bulk update
|
|
234
|
+
const [affectedCount] = await User.update(
|
|
235
|
+
{ role: 'admin' },
|
|
236
|
+
{ where: { id: userId } }
|
|
237
|
+
);
|
|
238
|
+
|
|
239
|
+
// Upsert
|
|
240
|
+
const [user, created] = await User.upsert({
|
|
241
|
+
email: 'user@example.com',
|
|
242
|
+
name: 'Updated Name',
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
// Delete (soft delete if paranoid: true)
|
|
246
|
+
await user.destroy();
|
|
247
|
+
|
|
248
|
+
// Hard delete
|
|
249
|
+
await user.destroy({ force: true });
|
|
250
|
+
|
|
251
|
+
// Restore soft-deleted
|
|
252
|
+
await user.restore();
|
|
253
|
+
\`\`\`
|
|
254
|
+
|
|
255
|
+
## Scopes
|
|
256
|
+
\`\`\`typescript
|
|
257
|
+
class User extends Model {
|
|
258
|
+
static associate(models) {
|
|
259
|
+
// Define associations
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
User.init({ /* ... */ }, {
|
|
264
|
+
sequelize,
|
|
265
|
+
scopes: {
|
|
266
|
+
active: {
|
|
267
|
+
where: { deletedAt: null },
|
|
268
|
+
},
|
|
269
|
+
admins: {
|
|
270
|
+
where: { role: 'admin' },
|
|
271
|
+
},
|
|
272
|
+
withPosts: {
|
|
273
|
+
include: [{ model: Post, as: 'posts' }],
|
|
274
|
+
},
|
|
275
|
+
recent(days: number) {
|
|
276
|
+
return {
|
|
277
|
+
where: {
|
|
278
|
+
createdAt: {
|
|
279
|
+
[Op.gte]: new Date(Date.now() - days * 24 * 60 * 60 * 1000),
|
|
280
|
+
},
|
|
281
|
+
},
|
|
282
|
+
};
|
|
283
|
+
},
|
|
284
|
+
},
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
// Usage
|
|
288
|
+
const activeAdmins = await User.scope(['active', 'admins']).findAll();
|
|
289
|
+
const recentUsers = await User.scope({ method: ['recent', 7] }).findAll();
|
|
290
|
+
\`\`\`
|
|
291
|
+
|
|
292
|
+
## Transactions
|
|
293
|
+
\`\`\`typescript
|
|
294
|
+
// Managed transaction (auto commit/rollback)
|
|
295
|
+
const result = await sequelize.transaction(async (t) => {
|
|
296
|
+
const user = await User.create({ email, name }, { transaction: t });
|
|
297
|
+
|
|
298
|
+
await Post.create({
|
|
299
|
+
title: 'Welcome Post',
|
|
300
|
+
authorId: user.id,
|
|
301
|
+
}, { transaction: t });
|
|
302
|
+
|
|
303
|
+
return user;
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
// Unmanaged transaction
|
|
307
|
+
const t = await sequelize.transaction();
|
|
308
|
+
try {
|
|
309
|
+
const user = await User.create({ email, name }, { transaction: t });
|
|
310
|
+
await Post.create({ title, authorId: user.id }, { transaction: t });
|
|
311
|
+
await t.commit();
|
|
312
|
+
} catch (error) {
|
|
313
|
+
await t.rollback();
|
|
314
|
+
throw error;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// Transaction isolation levels
|
|
318
|
+
await sequelize.transaction({
|
|
319
|
+
isolationLevel: Transaction.ISOLATION_LEVELS.SERIALIZABLE,
|
|
320
|
+
}, async (t) => {
|
|
321
|
+
// ...
|
|
322
|
+
});
|
|
323
|
+
\`\`\`
|
|
324
|
+
|
|
325
|
+
## Hooks
|
|
326
|
+
\`\`\`typescript
|
|
327
|
+
User.init({ /* ... */ }, {
|
|
328
|
+
sequelize,
|
|
329
|
+
hooks: {
|
|
330
|
+
beforeCreate: async (user) => {
|
|
331
|
+
if (user.password) {
|
|
332
|
+
user.password = await bcrypt.hash(user.password, 10);
|
|
333
|
+
}
|
|
334
|
+
},
|
|
335
|
+
beforeUpdate: async (user) => {
|
|
336
|
+
if (user.changed('password')) {
|
|
337
|
+
user.password = await bcrypt.hash(user.password, 10);
|
|
338
|
+
}
|
|
339
|
+
},
|
|
340
|
+
afterDestroy: async (user) => {
|
|
341
|
+
await Post.destroy({ where: { authorId: user.id } });
|
|
342
|
+
},
|
|
343
|
+
},
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
// Or add hooks after
|
|
347
|
+
User.addHook('beforeValidate', 'trimFields', (user) => {
|
|
348
|
+
if (user.email) user.email = user.email.trim().toLowerCase();
|
|
349
|
+
});
|
|
350
|
+
\`\`\`
|
|
351
|
+
|
|
352
|
+
## Migrations
|
|
353
|
+
\`\`\`bash
|
|
354
|
+
# Create migration
|
|
355
|
+
npx sequelize-cli migration:generate --name add-user-role
|
|
356
|
+
|
|
357
|
+
# Run migrations
|
|
358
|
+
npx sequelize-cli db:migrate
|
|
359
|
+
|
|
360
|
+
# Rollback
|
|
361
|
+
npx sequelize-cli db:migrate:undo
|
|
362
|
+
\`\`\`
|
|
363
|
+
|
|
364
|
+
\`\`\`typescript
|
|
365
|
+
// migrations/20240101000000-add-user-role.js
|
|
366
|
+
module.exports = {
|
|
367
|
+
async up(queryInterface, Sequelize) {
|
|
368
|
+
await queryInterface.addColumn('users', 'role', {
|
|
369
|
+
type: Sequelize.ENUM('user', 'admin'),
|
|
370
|
+
defaultValue: 'user',
|
|
371
|
+
});
|
|
372
|
+
await queryInterface.addIndex('users', ['role']);
|
|
373
|
+
},
|
|
374
|
+
|
|
375
|
+
async down(queryInterface) {
|
|
376
|
+
await queryInterface.removeColumn('users', 'role');
|
|
377
|
+
},
|
|
378
|
+
};
|
|
379
|
+
\`\`\`
|
|
380
|
+
|
|
381
|
+
## ❌ DON'T
|
|
382
|
+
- Use \`sync({ force: true })\` in production
|
|
383
|
+
- Forget to handle N+1 queries (use include/eager loading)
|
|
384
|
+
- Use \`findAll()\` without pagination on large tables
|
|
385
|
+
- Put business logic in models
|
|
386
|
+
- Skip transactions for multi-model operations
|
|
387
|
+
|
|
388
|
+
## ✅ DO
|
|
389
|
+
- Use migrations for schema changes
|
|
390
|
+
- Use TypeScript with proper typing
|
|
391
|
+
- Use scopes for reusable query logic
|
|
392
|
+
- Use transactions for related operations
|
|
393
|
+
- Use paranoid mode for soft deletes
|
|
394
|
+
- Define proper associations with aliases
|
|
395
|
+
- Use hooks for side effects
|