@dv.nghiem/flowdeck 0.2.3 → 0.3.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/README.md +24 -41
- package/dist/hooks/memory-hook.d.ts +21 -0
- package/dist/hooks/memory-hook.d.ts.map +1 -0
- package/dist/hooks/orchestrator-guard-hook.d.ts +2 -1
- package/dist/hooks/orchestrator-guard-hook.d.ts.map +1 -1
- package/dist/hooks/todo-hook.d.ts +1 -7
- package/dist/hooks/todo-hook.d.ts.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +649 -310
- package/dist/services/memory-store.d.ts +40 -0
- package/dist/services/memory-store.d.ts.map +1 -0
- package/dist/services/telemetry.d.ts +1 -1
- package/dist/services/telemetry.d.ts.map +1 -1
- package/dist/tools/memory-search.d.ts +3 -0
- package/dist/tools/memory-search.d.ts.map +1 -0
- package/docs/commands/fd-doctor.md +21 -0
- package/docs/commands/fd-quick.md +33 -0
- package/docs/commands/fd-reflect.md +23 -0
- package/docs/commands/fd-status.md +31 -0
- package/docs/commands/fd-translate-intent.md +17 -0
- package/docs/commands.md +209 -271
- package/docs/configuration.md +2 -1
- package/docs/index.md +22 -28
- package/docs/memory.md +69 -0
- package/docs/quick-start.md +1 -1
- package/docs/workflows.md +72 -320
- package/package.json +1 -2
- package/src/commands/fd-deploy-check.md +189 -34
- package/src/commands/fd-discuss.md +44 -6
- package/src/commands/fd-fix-bug.md +47 -20
- package/src/commands/fd-map-codebase.md +66 -18
- package/src/commands/fd-multi-repo.md +130 -6
- package/src/commands/fd-new-feature.md +164 -21
- package/src/commands/fd-new-project.md +14 -1
- package/src/commands/fd-plan.md +66 -44
- package/src/commands/fd-quick.md +60 -0
- package/src/commands/fd-reflect.md +41 -2
- package/src/commands/fd-status.md +84 -0
- package/src/commands/fd-write-docs.md +55 -23
- package/src/rules/README.md +8 -7
- package/src/skills/agent-harness-construction/SKILL.md +227 -0
- package/src/skills/api-design/SKILL.md +5 -0
- package/src/skills/backend-patterns/SKILL.md +105 -0
- package/src/skills/clean-architecture/SKILL.md +85 -0
- package/src/skills/cqrs/SKILL.md +230 -0
- package/src/skills/ddd-architecture/SKILL.md +104 -0
- package/src/skills/django-patterns/SKILL.md +304 -0
- package/src/skills/django-tdd/SKILL.md +297 -0
- package/src/skills/event-driven-architecture/SKILL.md +152 -0
- package/src/skills/frontend-pattern/SKILL.md +159 -0
- package/src/skills/hexagonal-architecture/SKILL.md +80 -0
- package/src/skills/layered-architecture/SKILL.md +64 -0
- package/src/skills/postgres-patterns/SKILL.md +74 -0
- package/src/skills/python-patterns/SKILL.md +5 -0
- package/src/skills/saga-architecture/SKILL.md +113 -0
- package/dist/tools/run-parallel.d.ts +0 -4
- package/dist/tools/run-parallel.d.ts.map +0 -1
- package/docs/command-migration.md +0 -175
- package/docs/commands/fd-analyze-change.md +0 -107
- package/docs/commands/fd-dashboard.md +0 -11
- package/docs/commands/fd-evaluate-risk.md +0 -134
- package/docs/commands/fd-guarded-edit.md +0 -105
- package/docs/commands/fd-progress.md +0 -11
- package/docs/commands/fd-review-code.md +0 -29
- package/docs/commands/fd-roadmap.md +0 -10
- package/docs/commands/fd-settings.md +0 -10
- package/docs/parallel-execution.md +0 -227
- package/src/commands/fd-analyze-change.md +0 -57
- package/src/commands/fd-approve.md +0 -64
- package/src/commands/fd-blast-radius.md +0 -49
- package/src/commands/fd-dashboard.md +0 -57
- package/src/commands/fd-evaluate-risk.md +0 -62
- package/src/commands/fd-guarded-edit.md +0 -69
- package/src/commands/fd-impact-radar.md +0 -51
- package/src/commands/fd-learn.md +0 -36
- package/src/commands/fd-progress.md +0 -50
- package/src/commands/fd-regression-predict.md +0 -57
- package/src/commands/fd-review-code.md +0 -62
- package/src/commands/fd-review-route.md +0 -54
- package/src/commands/fd-roadmap.md +0 -46
- package/src/commands/fd-settings.md +0 -57
- package/src/commands/fd-test-gap.md +0 -54
- package/src/commands/fd-volatility-map.md +0 -64
- package/src/commands/fd-workspace-status.md +0 -34
- package/src/skills/parallel-execute/SKILL.md +0 -92
- package/src/workflows/debug-flow.md +0 -119
- package/src/workflows/deploy-check-flow.md +0 -98
- package/src/workflows/discuss-flow.md +0 -97
- package/src/workflows/execute-flow.md +0 -233
- package/src/workflows/execute-phase.md +0 -145
- package/src/workflows/fix-bug-flow.md +0 -210
- package/src/workflows/map-codebase-flow.md +0 -92
- package/src/workflows/multi-repo-flow.md +0 -226
- package/src/workflows/parallel-execution-flow.md +0 -236
- package/src/workflows/plan-flow.md +0 -126
- package/src/workflows/plan-phase.md +0 -101
- package/src/workflows/refactor-flow.md +0 -122
- package/src/workflows/review-code-flow.md +0 -105
- package/src/workflows/spec-driven-flow.md +0 -43
- package/src/workflows/write-docs-flow.md +0 -95
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: django-tdd
|
|
3
|
+
description: Test-driven development patterns for Django covering pytest, Django TestCase, factory_boy, test client, and best practices for testing Django applications.
|
|
4
|
+
origin: FlowDeck
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Django TDD Skill
|
|
8
|
+
|
|
9
|
+
Test-driven development workflow for Django applications. Covers pytest fixtures, Django TestCase, factory_boy, and testing patterns.
|
|
10
|
+
|
|
11
|
+
## When to Activate
|
|
12
|
+
|
|
13
|
+
Activate when:
|
|
14
|
+
- Writing new Django features using TDD workflow
|
|
15
|
+
- Adding tests to existing Django applications
|
|
16
|
+
- Debugging test failures in Django projects
|
|
17
|
+
- Setting up testing infrastructure for Django apps
|
|
18
|
+
|
|
19
|
+
## TDD Workflow
|
|
20
|
+
|
|
21
|
+
1. Write a failing test (RED)
|
|
22
|
+
2. Run the test - it should fail
|
|
23
|
+
3. Write minimal implementation (GREEN)
|
|
24
|
+
4. Run tests - they should pass
|
|
25
|
+
5. Refactor (IMPROVE)
|
|
26
|
+
6. Verify coverage
|
|
27
|
+
|
|
28
|
+
## Test Setup
|
|
29
|
+
|
|
30
|
+
### Using pytest with Django
|
|
31
|
+
|
|
32
|
+
```python
|
|
33
|
+
# conftest.py
|
|
34
|
+
import pytest
|
|
35
|
+
import django
|
|
36
|
+
from django.conf import settings
|
|
37
|
+
|
|
38
|
+
@pytest.fixture(scope="session")
|
|
39
|
+
def django_db_setup():
|
|
40
|
+
settings.DATABASES["default"] = {
|
|
41
|
+
"ENGINE": "django.db.backends.sqlite3",
|
|
42
|
+
"NAME": ":memory:",
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
@pytest.fixture
|
|
46
|
+
def api_client():
|
|
47
|
+
from rest_framework.test import APIClient
|
|
48
|
+
return APIClient()
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
### Django TestCase
|
|
52
|
+
|
|
53
|
+
```python
|
|
54
|
+
from django.test import TestCase, Client
|
|
55
|
+
from django.urls import reverse
|
|
56
|
+
from django.contrib.auth import get_user_model
|
|
57
|
+
from .models import Article, Author
|
|
58
|
+
|
|
59
|
+
User = get_user_model()
|
|
60
|
+
|
|
61
|
+
class ArticleViewTest(TestCase):
|
|
62
|
+
def setUp(self):
|
|
63
|
+
self.client = Client()
|
|
64
|
+
self.user = User.objects.create_user(
|
|
65
|
+
email='test@example.com',
|
|
66
|
+
password='testpass123'
|
|
67
|
+
)
|
|
68
|
+
self.author = Author.objects.create(name='Author', email='a@test.com')
|
|
69
|
+
self.article = Article.objects.create(
|
|
70
|
+
title='Test',
|
|
71
|
+
content='Content',
|
|
72
|
+
author=self.author,
|
|
73
|
+
status='published'
|
|
74
|
+
)
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
## Testing Views
|
|
78
|
+
|
|
79
|
+
### List View Tests
|
|
80
|
+
|
|
81
|
+
```python
|
|
82
|
+
def test_article_list_view(self):
|
|
83
|
+
response = self.client.get(reverse('article-list'))
|
|
84
|
+
self.assertEqual(response.status_code, 200)
|
|
85
|
+
self.assertContains(response, 'Test')
|
|
86
|
+
self.assertTemplateUsed(response, 'articles/list.html')
|
|
87
|
+
self.assertEqual(len(response.context['articles']), 1)
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
### Detail View Tests
|
|
91
|
+
|
|
92
|
+
```python
|
|
93
|
+
def test_article_detail_view(self):
|
|
94
|
+
response = self.client.get(
|
|
95
|
+
reverse('article-detail', kwargs={'pk': self.article.pk})
|
|
96
|
+
)
|
|
97
|
+
self.assertEqual(response.status_code, 200)
|
|
98
|
+
self.assertContains(response, self.article.title)
|
|
99
|
+
|
|
100
|
+
def test_article_detail_not_found(self):
|
|
101
|
+
response = self.client.get(
|
|
102
|
+
reverse('article-detail', kwargs={'pk': 9999})
|
|
103
|
+
)
|
|
104
|
+
self.assertEqual(response.status_code, 404)
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
### Authentication Tests
|
|
108
|
+
|
|
109
|
+
```python
|
|
110
|
+
def test_create_article_requires_login(self):
|
|
111
|
+
response = self.client.get(reverse('article-create'))
|
|
112
|
+
self.assertRedirects(response, '/accounts/login/?next=/articles/create/')
|
|
113
|
+
|
|
114
|
+
def test_create_article_authenticated(self):
|
|
115
|
+
self.client.login(email='test@example.com', password='testpass123')
|
|
116
|
+
response = self.client.post(reverse('article-create'), {
|
|
117
|
+
'title': 'New Article',
|
|
118
|
+
'content': 'New content',
|
|
119
|
+
'status': 'draft',
|
|
120
|
+
})
|
|
121
|
+
self.assertEqual(response.status_code, 302)
|
|
122
|
+
self.assertTrue(Article.objects.filter(title='New Article').exists())
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
### JSON API Tests
|
|
126
|
+
|
|
127
|
+
```python
|
|
128
|
+
def test_json_response(self):
|
|
129
|
+
response = self.client.get(
|
|
130
|
+
reverse('api-articles'),
|
|
131
|
+
content_type='application/json'
|
|
132
|
+
)
|
|
133
|
+
self.assertEqual(response.status_code, 200)
|
|
134
|
+
data = response.json()
|
|
135
|
+
self.assertIn('articles', data)
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
## Testing Models
|
|
139
|
+
|
|
140
|
+
```python
|
|
141
|
+
def test_article_creation(self):
|
|
142
|
+
article = Article.objects.create(
|
|
143
|
+
title='Test Article',
|
|
144
|
+
content='Test content',
|
|
145
|
+
author=self.author,
|
|
146
|
+
status='draft'
|
|
147
|
+
)
|
|
148
|
+
self.assertEqual(article.title, 'Test Article')
|
|
149
|
+
self.assertEqual(article.status, 'draft')
|
|
150
|
+
self.assertEqual(str(article), 'Test Article')
|
|
151
|
+
|
|
152
|
+
def test_article_ordering(self):
|
|
153
|
+
Article.objects.create(title='Second', author=self.author)
|
|
154
|
+
Article.objects.create(title='First', author=self.author)
|
|
155
|
+
articles = list(Article.objects.all())
|
|
156
|
+
self.assertEqual(articles[0].title, 'First')
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
## Factory Boy Fixtures
|
|
160
|
+
|
|
161
|
+
### Defining Factories
|
|
162
|
+
|
|
163
|
+
```python
|
|
164
|
+
# factories.py
|
|
165
|
+
import factory
|
|
166
|
+
from factory.django import DjangoModelFactory
|
|
167
|
+
from .models import Author, Article
|
|
168
|
+
|
|
169
|
+
class AuthorFactory(DjangoModelFactory):
|
|
170
|
+
class Meta:
|
|
171
|
+
model = Author
|
|
172
|
+
|
|
173
|
+
name = factory.Sequence(lambda n: f"Author {n}")
|
|
174
|
+
email = factory.LazyAttribute(lambda obj: f"{obj.name.replace(' ', '')}@example.com")
|
|
175
|
+
|
|
176
|
+
class ArticleFactory(DjangoModelFactory):
|
|
177
|
+
class Meta:
|
|
178
|
+
model = Article
|
|
179
|
+
|
|
180
|
+
title = factory.Sequence(lambda n: f"Article {n}")
|
|
181
|
+
content = "Test content"
|
|
182
|
+
author = factory.SubFactory(AuthorFactory)
|
|
183
|
+
status = 'draft'
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
### Using Factories in Tests
|
|
187
|
+
|
|
188
|
+
```python
|
|
189
|
+
from .factories import AuthorFactory, ArticleFactory
|
|
190
|
+
|
|
191
|
+
def test_article_with_factory(self):
|
|
192
|
+
author = AuthorFactory(name="Jane Doe")
|
|
193
|
+
article = ArticleFactory(title="Test", author=author)
|
|
194
|
+
assert article.author.name == "Jane Doe"
|
|
195
|
+
assert article.title == "Test"
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
## Pytest Fixtures
|
|
199
|
+
|
|
200
|
+
### Basic Fixture
|
|
201
|
+
|
|
202
|
+
```python
|
|
203
|
+
import pytest
|
|
204
|
+
|
|
205
|
+
@pytest.fixture
|
|
206
|
+
def sample_data():
|
|
207
|
+
return {"name": "test", "value": 42}
|
|
208
|
+
|
|
209
|
+
def test_sample_data(sample_data):
|
|
210
|
+
assert sample_data["name"] == "test"
|
|
211
|
+
assert sample_data["value"] == 42
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
### Fixture with Teardown
|
|
215
|
+
|
|
216
|
+
```python
|
|
217
|
+
import tempfile
|
|
218
|
+
import os
|
|
219
|
+
|
|
220
|
+
@pytest.fixture
|
|
221
|
+
def temp_file():
|
|
222
|
+
fd, path = tempfile.mkstemp()
|
|
223
|
+
os.write(fd, b"test content")
|
|
224
|
+
os.close(fd)
|
|
225
|
+
yield path
|
|
226
|
+
os.unlink(path)
|
|
227
|
+
|
|
228
|
+
def test_temp_file(temp_file):
|
|
229
|
+
assert os.path.exists(temp_file)
|
|
230
|
+
with open(temp_file) as f:
|
|
231
|
+
assert f.read() == "test content"
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
### Parametrized Fixtures
|
|
235
|
+
|
|
236
|
+
```python
|
|
237
|
+
@pytest.fixture(params=["mysql", "postgresql", "sqlite"])
|
|
238
|
+
def database_type(request):
|
|
239
|
+
return request.param
|
|
240
|
+
|
|
241
|
+
def test_database_type(database_type):
|
|
242
|
+
assert database_type in ["mysql", "postgresql", "sqlite"]
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
## Common Patterns
|
|
246
|
+
|
|
247
|
+
### Testing Forms
|
|
248
|
+
|
|
249
|
+
```python
|
|
250
|
+
def test_form_valid(self):
|
|
251
|
+
form_data = {
|
|
252
|
+
'title': 'New Article',
|
|
253
|
+
'content': 'Content',
|
|
254
|
+
'author': self.author.pk,
|
|
255
|
+
'status': 'draft',
|
|
256
|
+
}
|
|
257
|
+
form = ArticleForm(data=form_data)
|
|
258
|
+
self.assertTrue(form.is_valid())
|
|
259
|
+
|
|
260
|
+
def test_form_invalid(self):
|
|
261
|
+
form_data = {'title': '', 'content': 'Content'}
|
|
262
|
+
form = ArticleForm(data=form_data)
|
|
263
|
+
self.assertFalse(form.is_valid())
|
|
264
|
+
self.assertIn('title', form.errors)
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
### Testing Middleware
|
|
268
|
+
|
|
269
|
+
```python
|
|
270
|
+
def test_middleware_process_request(self):
|
|
271
|
+
response = self.client.get('/articles/')
|
|
272
|
+
self.assertEqual(response.status_code, 200)
|
|
273
|
+
# Check middleware added expected headers or behavior
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
### Testing Signals
|
|
277
|
+
|
|
278
|
+
```python
|
|
279
|
+
def test_signal_on_save(self):
|
|
280
|
+
article = ArticleFactory()
|
|
281
|
+
# Verify signal handlers executed (e.g., notifications sent)
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
## Coverage Verification
|
|
285
|
+
|
|
286
|
+
```bash
|
|
287
|
+
# Run with coverage
|
|
288
|
+
pytest --cov=myapp --cov-report=html
|
|
289
|
+
|
|
290
|
+
# Minimum 80% coverage required
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
## Related Skills
|
|
294
|
+
|
|
295
|
+
- django-patterns
|
|
296
|
+
- python-patterns
|
|
297
|
+
- tdd-workflow
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
# Event-Driven Architecture
|
|
2
|
+
|
|
3
|
+
## When to Activate
|
|
4
|
+
|
|
5
|
+
Activate when:
|
|
6
|
+
- Designing or implementing message-based communication between services
|
|
7
|
+
- Building systems that require asynchronous processing
|
|
8
|
+
- Decoupling producers from consumers in distributed systems
|
|
9
|
+
- Implementing event sourcing or audit trails
|
|
10
|
+
- Setting up webhooks, message queues, or pub/sub patterns
|
|
11
|
+
|
|
12
|
+
## Steps
|
|
13
|
+
|
|
14
|
+
### 1. Identify Event Boundaries
|
|
15
|
+
|
|
16
|
+
Define what constitutes an "event" in your system:
|
|
17
|
+
- Events are **facts** about something that happened (past tense: `OrderPlaced`, `PaymentProcessed`)
|
|
18
|
+
- Commands are **requests** for an action (present tense: `PlaceOrder`, `ProcessPayment`)
|
|
19
|
+
- Events should be **immutable** once emitted
|
|
20
|
+
|
|
21
|
+
### 2. Choose the Right Messaging Pattern
|
|
22
|
+
|
|
23
|
+
| Pattern | Use Case | Examples |
|
|
24
|
+
|---------|----------|----------|
|
|
25
|
+
| Pub/Sub | One-to-many notification | Notifications, audit logs |
|
|
26
|
+
| Message Queue | Point-to-point processing | Order processing, email sending |
|
|
27
|
+
| Event Streaming | Durable, replayable event log | Event sourcing, analytics |
|
|
28
|
+
| Webhooks | External system integration | HTTP callbacks |
|
|
29
|
+
|
|
30
|
+
### 3. Design Event Schema
|
|
31
|
+
|
|
32
|
+
```typescript
|
|
33
|
+
interface Event<T> {
|
|
34
|
+
id: string; // Unique event identifier (UUID)
|
|
35
|
+
type: string; // Event type (e.g., "ORDER_PLACED")
|
|
36
|
+
version: string; // Schema version for evolution
|
|
37
|
+
timestamp: string; // ISO 8601 timestamp
|
|
38
|
+
source: string; // Origin service name
|
|
39
|
+
data: T; // Event payload
|
|
40
|
+
metadata?: Record<string, unknown>; // Optional tracing/correlation
|
|
41
|
+
}
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
### 4. Handle Eventual Consistency
|
|
45
|
+
|
|
46
|
+
- Design consumers to be **idempotent** (safe to process twice)
|
|
47
|
+
- Use **correlation IDs** to track event chains
|
|
48
|
+
- Implement **dead letter queues** for failed processing
|
|
49
|
+
- Set **retry policies** with exponential backoff
|
|
50
|
+
|
|
51
|
+
### 5. Ensure Durability
|
|
52
|
+
|
|
53
|
+
- Use persistent message storage (not in-memory)
|
|
54
|
+
- Acknowledge messages only after successful processing
|
|
55
|
+
- Implement **at-least-once** delivery semantics
|
|
56
|
+
|
|
57
|
+
## Examples
|
|
58
|
+
|
|
59
|
+
### TypeScript Event Emitter
|
|
60
|
+
|
|
61
|
+
```typescript
|
|
62
|
+
interface OrderEvent {
|
|
63
|
+
orderId: string;
|
|
64
|
+
customerId: string;
|
|
65
|
+
total: number;
|
|
66
|
+
items: OrderItem[];
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
class OrderEventPublisher {
|
|
70
|
+
private emitter: EventEmitter;
|
|
71
|
+
|
|
72
|
+
async publishOrderPlaced(event: OrderEvent): Promise<void> {
|
|
73
|
+
const message: Event<OrderEvent> = {
|
|
74
|
+
id: crypto.randomUUID(),
|
|
75
|
+
type: 'ORDER_PLACED',
|
|
76
|
+
version: '1.0',
|
|
77
|
+
timestamp: new Date().toISOString(),
|
|
78
|
+
source: 'order-service',
|
|
79
|
+
data: event,
|
|
80
|
+
metadata: {
|
|
81
|
+
correlationId: event.orderId,
|
|
82
|
+
partitionKey: event.customerId
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
await this.messageBroker.publish('orders.placed', message);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
### Message Consumer with Retry
|
|
92
|
+
|
|
93
|
+
```typescript
|
|
94
|
+
class OrderEventConsumer {
|
|
95
|
+
async handleOrderPlaced(event: Event<OrderEvent>): Promise<void> {
|
|
96
|
+
try {
|
|
97
|
+
// Idempotent processing
|
|
98
|
+
const existingOrder = await this.orderRepo.findById(event.data.orderId);
|
|
99
|
+
if (existingOrder) {
|
|
100
|
+
logger.info('Order already processed, skipping', { orderId: event.data.orderId });
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
await this.orderService.processOrder(event.data);
|
|
105
|
+
await this.messageBroker.ack(event.id);
|
|
106
|
+
} catch (error) {
|
|
107
|
+
if (error instanceof TransientError) {
|
|
108
|
+
// Requeue with delay for retry
|
|
109
|
+
await this.messageBroker.requeue(event.id, { delay: 5000 });
|
|
110
|
+
} else {
|
|
111
|
+
// Send to dead letter queue
|
|
112
|
+
await this.messageBroker.sendToDlq(event, error);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
### Event Schema Registry
|
|
120
|
+
|
|
121
|
+
```typescript
|
|
122
|
+
// contracts/order-events.ts
|
|
123
|
+
export const OrderPlacedEventSchema = {
|
|
124
|
+
type: 'object',
|
|
125
|
+
required: ['orderId', 'customerId', 'total', 'items'],
|
|
126
|
+
properties: {
|
|
127
|
+
orderId: { type: 'string', format: 'uuid' },
|
|
128
|
+
customerId: { type: 'string', format: 'uuid' },
|
|
129
|
+
total: { type: 'number', minimum: 0 },
|
|
130
|
+
items: {
|
|
131
|
+
type: 'array',
|
|
132
|
+
items: {
|
|
133
|
+
type: 'object',
|
|
134
|
+
required: ['productId', 'quantity', 'price'],
|
|
135
|
+
properties: {
|
|
136
|
+
productId: { type: 'string' },
|
|
137
|
+
quantity: { type: 'number', minimum: 1 },
|
|
138
|
+
price: { type: 'number', minimum: 0 }
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
};
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
## Related Skills
|
|
147
|
+
|
|
148
|
+
- api-design
|
|
149
|
+
- backend-patterns
|
|
150
|
+
- cqrs
|
|
151
|
+
- event-sourcing
|
|
152
|
+
- message-queues
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: frontend-pattern
|
|
3
|
+
description: Frontend development patterns — component composition, state management, URL-as-state, data fetching, and animation best practices for React/TypeScript web applications
|
|
4
|
+
origin: FlowDeck
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Frontend Pattern Skill
|
|
8
|
+
|
|
9
|
+
Implements maintainable, performant frontend patterns using React and TypeScript.
|
|
10
|
+
|
|
11
|
+
## When to Activate
|
|
12
|
+
|
|
13
|
+
Activate when:
|
|
14
|
+
- Building new UI components
|
|
15
|
+
- Setting up state management
|
|
16
|
+
- Implementing data fetching
|
|
17
|
+
- Adding animations or transitions
|
|
18
|
+
- Structuring a new feature module
|
|
19
|
+
|
|
20
|
+
## Component Patterns
|
|
21
|
+
|
|
22
|
+
### Compound Components
|
|
23
|
+
|
|
24
|
+
Use compound components when related UI shares state and interaction semantics:
|
|
25
|
+
|
|
26
|
+
```tsx
|
|
27
|
+
<Tabs defaultValue="overview">
|
|
28
|
+
<Tabs.List>
|
|
29
|
+
<Tabs.Trigger value="overview">Overview</Tabs.Trigger>
|
|
30
|
+
<Tabs.Trigger value="settings">Settings</Tabs.Trigger>
|
|
31
|
+
</Tabs.List>
|
|
32
|
+
<Tabs.Content value="overview">...</Tabs.Content>
|
|
33
|
+
<Tabs.Content value="settings">...</Tabs.Content>
|
|
34
|
+
</Tabs>
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
- Parent owns state via `useState` or context
|
|
38
|
+
- Children consume via context — no prop drilling
|
|
39
|
+
- Keeps keyboard handling, ARIA, and focus logic in the headless layer
|
|
40
|
+
|
|
41
|
+
### Container / Presentational Split
|
|
42
|
+
|
|
43
|
+
```tsx
|
|
44
|
+
// Container — owns data loading and side effects
|
|
45
|
+
function UserProfileContainer({ userId }: { userId: string }) {
|
|
46
|
+
const { data, isLoading } = useUser(userId);
|
|
47
|
+
if (isLoading) return <Skeleton />;
|
|
48
|
+
return <UserProfileView user={data} />;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Presentational — receives props, renders UI
|
|
52
|
+
function UserProfileView({ user }: { user: User }) {
|
|
53
|
+
return (
|
|
54
|
+
<div>
|
|
55
|
+
<Avatar src={user.avatar} />
|
|
56
|
+
<h1>{user.name}</h1>
|
|
57
|
+
</div>
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## State Management
|
|
63
|
+
|
|
64
|
+
| Concern | Tooling |
|
|
65
|
+
|---------|---------|
|
|
66
|
+
| Server state | TanStack Query, SWR, tRPC |
|
|
67
|
+
| Client state | Zustand, Jotai, signals |
|
|
68
|
+
| URL state | search params, route segments |
|
|
69
|
+
| Form state | React Hook Form or equivalent |
|
|
70
|
+
|
|
71
|
+
**Do not duplicate server state into client stores.** Derive values instead of storing redundant computed state.
|
|
72
|
+
|
|
73
|
+
## URL As State
|
|
74
|
+
|
|
75
|
+
Persist shareable, bookmarkable state in the URL:
|
|
76
|
+
|
|
77
|
+
```tsx
|
|
78
|
+
// Good: filters, sort, pagination in URL
|
|
79
|
+
const [searchParams, setSearchParams] = useSearchParams();
|
|
80
|
+
const filter = searchParams.get('filter') ?? 'all';
|
|
81
|
+
|
|
82
|
+
// Usage
|
|
83
|
+
<button onClick={() => setSearchParams({ filter: 'active' })}>
|
|
84
|
+
Active
|
|
85
|
+
</button>
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
## Data Fetching Patterns
|
|
89
|
+
|
|
90
|
+
### Stale-While-Revalidate
|
|
91
|
+
|
|
92
|
+
Return cached data immediately, revalidate in background:
|
|
93
|
+
|
|
94
|
+
```tsx
|
|
95
|
+
const { data } = useQuery({
|
|
96
|
+
queryKey: ['users', userId],
|
|
97
|
+
queryFn: () => fetchUser(userId),
|
|
98
|
+
staleTime: 5 * 60 * 1000, // 5 minutes
|
|
99
|
+
});
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
### Optimistic Updates
|
|
103
|
+
|
|
104
|
+
```tsx
|
|
105
|
+
const mutation = useMutation({
|
|
106
|
+
mutationFn: updateUser,
|
|
107
|
+
onMutate: async (newData) => {
|
|
108
|
+
await queryClient.cancelQueries({ queryKey: ['user', newData.id] });
|
|
109
|
+
const previous = queryClient.getQueryData(['user', newData.id]);
|
|
110
|
+
queryClient.setQueryData(['user', newData.id], newData);
|
|
111
|
+
return { previous };
|
|
112
|
+
},
|
|
113
|
+
onError: (err, newData, context) => {
|
|
114
|
+
queryClient.setQueryData(['user', newData.id], context.previous);
|
|
115
|
+
},
|
|
116
|
+
});
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
## CSS Custom Properties
|
|
120
|
+
|
|
121
|
+
Define design tokens as CSS variables — do not hardcode values:
|
|
122
|
+
|
|
123
|
+
```css
|
|
124
|
+
:root {
|
|
125
|
+
--color-surface: oklch(98% 0 0);
|
|
126
|
+
--color-text: oklch(18% 0 0);
|
|
127
|
+
--color-accent: oklch(68% 0.21 250);
|
|
128
|
+
|
|
129
|
+
--text-base: clamp(1rem, 0.92rem + 0.4vw, 1.125rem);
|
|
130
|
+
--space-section: clamp(4rem, 3rem + 5vw, 10rem);
|
|
131
|
+
|
|
132
|
+
--duration-fast: 150ms;
|
|
133
|
+
--duration-normal: 300ms;
|
|
134
|
+
--ease-out-expo: cubic-bezier(0.16, 1, 0.3, 1);
|
|
135
|
+
}
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
## Animation Guidelines
|
|
139
|
+
|
|
140
|
+
Use compositor-friendly properties only:
|
|
141
|
+
|
|
142
|
+
```
|
|
143
|
+
✅ transform, opacity, clip-path, filter
|
|
144
|
+
❌ width, height, top, left, margin, padding, border, font-size
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
```tsx
|
|
148
|
+
// Good
|
|
149
|
+
const style = { opacity: isVisible ? 1 : 0, transform: `translateY(${isVisible ? 0 : 20}px)` };
|
|
150
|
+
|
|
151
|
+
// Bad
|
|
152
|
+
const style = { height: isVisible ? 'auto' : 0 };
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
## Related Skills
|
|
156
|
+
|
|
157
|
+
- [code-review](code-review) — Review frontend code for quality
|
|
158
|
+
- [security-scan](security-scan) — Check for XSS and injection vulnerabilities
|
|
159
|
+
- [test-coverage](test-coverage) — Ensure UI component tests exist
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# hexagonal-architecture
|
|
2
|
+
|
|
3
|
+
## When to Activate
|
|
4
|
+
When building applications that must remain flexible to changing external systems (databases, APIs, UI frameworks) and need to support multiple entry points (ports) for the same business logic.
|
|
5
|
+
|
|
6
|
+
## Steps
|
|
7
|
+
1. **Identify the core domain** - Isolate the pure business logic that makes no references to infrastructure.
|
|
8
|
+
2. **Define inbound ports** - Create interfaces (ports) for primary/ driving actors (UI, API controllers) that trigger application logic.
|
|
9
|
+
3. **Define outbound ports** - Create interfaces (ports) for secondary/ driven actors (databases, external services) that the domain calls.
|
|
10
|
+
4. **Implement primary adapters** - Create adapters for inbound traffic (REST controllers, GraphQL resolvers, CLI commands).
|
|
11
|
+
5. **Implement secondary adapters** - Create adapters for outbound traffic (Postgres repositories, Redis caches, email gateways).
|
|
12
|
+
6. **Ensure domain has no external dependencies** - The domain layer should compile and run with no imports from adapters.
|
|
13
|
+
7. **Wire via dependency injection** - Connect adapters to ports at application startup.
|
|
14
|
+
|
|
15
|
+
## Examples
|
|
16
|
+
```typescript
|
|
17
|
+
// Domain Core - Pure business logic, no infrastructure dependencies
|
|
18
|
+
class Transfer {
|
|
19
|
+
constructor(
|
|
20
|
+
public readonly fromAccountId: string,
|
|
21
|
+
public readonly toAccountId: string,
|
|
22
|
+
public readonly amount: number
|
|
23
|
+
) {}
|
|
24
|
+
|
|
25
|
+
execute(accounts: Map<string, Account>): TransferResult {
|
|
26
|
+
const from = accounts.get(this.fromAccountId)
|
|
27
|
+
const to = accounts.get(this.toAccountId)
|
|
28
|
+
|
|
29
|
+
if (!from || !to) {
|
|
30
|
+
return TransferResult.failed('Account not found')
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (!from.canDebit(this.amount)) {
|
|
34
|
+
return TransferResult.failed('Insufficient funds')
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
from.debit(this.amount)
|
|
38
|
+
to.credit(this.amount)
|
|
39
|
+
|
|
40
|
+
return TransferResult.success()
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Inbound Port (Primary Port) - Interface for driving operations
|
|
45
|
+
interface TransferUseCase {
|
|
46
|
+
execute(transfer: Transfer): TransferResult
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Outbound Port (Secondary Port) - Interface for driven operations
|
|
50
|
+
interface AccountRepository {
|
|
51
|
+
findById(id: string): Promise<Account | null>
|
|
52
|
+
save(account: Account): Promise<void>
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
interface EventBus {
|
|
56
|
+
publish(event: DomainEvent): Promise<void>
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Primary Adapter - REST API
|
|
60
|
+
class TransferController implements TransferUseCase {
|
|
61
|
+
constructor(private readonly accounts: AccountRepository) {}
|
|
62
|
+
|
|
63
|
+
async execute(transfer: Transfer): Promise<TransferResult> {
|
|
64
|
+
const allAccounts = await this.accounts.findById(transfer.fromAccountId)
|
|
65
|
+
// ... handle via injected port
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Secondary Adapter - PostgreSQL implementation
|
|
70
|
+
class PostgresAccountRepository implements AccountRepository {
|
|
71
|
+
constructor(private readonly db: Database) {}
|
|
72
|
+
// ... implementation
|
|
73
|
+
}
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## Related Skills
|
|
77
|
+
- clean-architecture
|
|
78
|
+
- layered-architecture
|
|
79
|
+
- ddd-architecture
|
|
80
|
+
- backend-patterns
|