@girardmedia/bootspring 3.3.2 → 3.4.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/assets/agents/accessibility-auditor.md +39 -0
- package/assets/agents/api-designer.md +40 -0
- package/assets/agents/auth-implementer.md +64 -0
- package/assets/agents/bug-hunter.md +42 -0
- package/assets/agents/bundle-analyzer.md +40 -0
- package/assets/agents/cache-optimizer.md +55 -0
- package/assets/agents/changelog-writer.md +55 -0
- package/assets/agents/ci-cd-builder.md +40 -0
- package/assets/agents/code-explainer.md +39 -0
- package/assets/agents/code-reviewer.md +39 -0
- package/assets/agents/cost-optimizer.md +57 -0
- package/assets/agents/cron-scheduler.md +51 -0
- package/assets/agents/data-seeder.md +56 -0
- package/assets/agents/database-architect.md +40 -0
- package/assets/agents/dependency-updater.md +40 -0
- package/assets/agents/deploy-checker.md +40 -0
- package/assets/agents/docker-optimizer.md +40 -0
- package/assets/agents/documentation-writer.md +40 -0
- package/assets/agents/email-builder.md +55 -0
- package/assets/agents/env-setup.md +40 -0
- package/assets/agents/error-handler.md +40 -0
- package/assets/agents/eslint-fixer.md +46 -0
- package/assets/agents/feature-flagger.md +69 -0
- package/assets/agents/git-detective.md +39 -0
- package/assets/agents/graphql-builder.md +60 -0
- package/assets/agents/incident-responder.md +59 -0
- package/assets/agents/log-analyzer.md +39 -0
- package/assets/agents/migration-planner.md +41 -0
- package/assets/agents/monorepo-navigator.md +39 -0
- package/assets/agents/nextjs-expert.md +57 -0
- package/assets/agents/notification-builder.md +56 -0
- package/assets/agents/onboarding-guide.md +39 -0
- package/assets/agents/performance-profiler.md +40 -0
- package/assets/agents/prisma-expert.md +57 -0
- package/assets/agents/rate-limiter.md +58 -0
- package/assets/agents/react-expert.md +58 -0
- package/assets/agents/refactorer.md +42 -0
- package/assets/agents/regex-builder.md +46 -0
- package/assets/agents/release-manager.md +40 -0
- package/assets/agents/s3-manager.md +58 -0
- package/assets/agents/schema-validator.md +40 -0
- package/assets/agents/search-builder.md +62 -0
- package/assets/agents/security-auditor.md +39 -0
- package/assets/agents/sitemap-generator.md +53 -0
- package/assets/agents/stripe-integrator.md +59 -0
- package/assets/agents/tailwind-expert.md +55 -0
- package/assets/agents/tech-debt-tracker.md +39 -0
- package/assets/agents/test-writer.md +42 -0
- package/assets/agents/type-fixer.md +45 -0
- package/assets/agents/webhook-builder.md +54 -0
- package/assets/rules/cpp.md +53 -0
- package/assets/rules/css.md +52 -0
- package/assets/rules/go.md +50 -0
- package/assets/rules/html.md +52 -0
- package/assets/rules/java.md +51 -0
- package/assets/rules/kotlin.md +50 -0
- package/assets/rules/php.md +51 -0
- package/assets/rules/python.md +51 -0
- package/assets/rules/ruby.md +51 -0
- package/assets/rules/rust.md +49 -0
- package/assets/rules/shell.md +52 -0
- package/assets/rules/sql.md +49 -0
- package/assets/rules/swift.md +50 -0
- package/assets/rules/typescript.md +52 -0
- package/assets/rules/yaml-json.md +51 -0
- package/assets/skills/accessibility.md +210 -0
- package/assets/skills/agent-patterns.md +387 -0
- package/assets/skills/ai-integration.md +263 -0
- package/assets/skills/animation-patterns.md +224 -0
- package/assets/skills/api-design.md +218 -0
- package/assets/skills/api-gateway.md +341 -0
- package/assets/skills/api-versioning.md +226 -0
- package/assets/skills/astro-patterns.md +233 -0
- package/assets/skills/auth-patterns.md +248 -0
- package/assets/skills/aws-patterns.md +171 -0
- package/assets/skills/background-jobs.md +162 -0
- package/assets/skills/browser-extensions.md +309 -0
- package/assets/skills/caching-patterns.md +253 -0
- package/assets/skills/ci-cd.md +251 -0
- package/assets/skills/cli-development.md +296 -0
- package/assets/skills/code-review.md +185 -0
- package/assets/skills/cron-patterns.md +327 -0
- package/assets/skills/data-fetching.md +231 -0
- package/assets/skills/database-migrations.md +346 -0
- package/assets/skills/database-patterns.md +219 -0
- package/assets/skills/debugging.md +281 -0
- package/assets/skills/design-system.md +289 -0
- package/assets/skills/django-patterns.md +182 -0
- package/assets/skills/docker-patterns.md +235 -0
- package/assets/skills/e2e-testing.md +287 -0
- package/assets/skills/edge-computing.md +268 -0
- package/assets/skills/electron-patterns.md +266 -0
- package/assets/skills/email-templates.md +206 -0
- package/assets/skills/error-handling.md +265 -0
- package/assets/skills/event-driven.md +232 -0
- package/assets/skills/express-patterns.md +239 -0
- package/assets/skills/fastapi-patterns.md +198 -0
- package/assets/skills/feature-flags.md +212 -0
- package/assets/skills/figma-to-code.md +298 -0
- package/assets/skills/file-upload.md +228 -0
- package/assets/skills/forms-patterns.md +264 -0
- package/assets/skills/gcp-patterns.md +189 -0
- package/assets/skills/git-workflow.md +187 -0
- package/assets/skills/golang-patterns.md +185 -0
- package/assets/skills/graphql-patterns.md +244 -0
- package/assets/skills/i18n-patterns.md +172 -0
- package/assets/skills/image-processing.md +350 -0
- package/assets/skills/java-springboot.md +226 -0
- package/assets/skills/kotlin-patterns.md +207 -0
- package/assets/skills/kubernetes-patterns.md +326 -0
- package/assets/skills/laravel-patterns.md +261 -0
- package/assets/skills/llm-fine-tuning.md +335 -0
- package/assets/skills/load-testing.md +303 -0
- package/assets/skills/logging-observability.md +228 -0
- package/assets/skills/markdown-processing.md +318 -0
- package/assets/skills/mcp-server-patterns.md +292 -0
- package/assets/skills/microservices.md +272 -0
- package/assets/skills/migration-patterns.md +239 -0
- package/assets/skills/mongodb-patterns.md +189 -0
- package/assets/skills/monorepo-patterns.md +287 -0
- package/assets/skills/nextjs-app-router.md +237 -0
- package/assets/skills/notification-patterns.md +348 -0
- package/assets/skills/oauth-patterns.md +246 -0
- package/assets/skills/payment-integration.md +222 -0
- package/assets/skills/pdf-generation.md +307 -0
- package/assets/skills/performance-optimization.md +277 -0
- package/assets/skills/php-patterns.md +210 -0
- package/assets/skills/prisma-patterns.md +241 -0
- package/assets/skills/prompt-engineering.md +193 -0
- package/assets/skills/pwa-patterns.md +247 -0
- package/assets/skills/python-patterns.md +158 -0
- package/assets/skills/python-testing.md +172 -0
- package/assets/skills/queue-patterns.md +295 -0
- package/assets/skills/rag-patterns.md +159 -0
- package/assets/skills/rate-limiting.md +319 -0
- package/assets/skills/react-components.md +201 -0
- package/assets/skills/react-native-patterns.md +299 -0
- package/assets/skills/real-time-patterns.md +181 -0
- package/assets/skills/redis-patterns.md +188 -0
- package/assets/skills/refactoring.md +218 -0
- package/assets/skills/regex-patterns.md +191 -0
- package/assets/skills/remix-patterns.md +262 -0
- package/assets/skills/responsive-design.md +199 -0
- package/assets/skills/ruby-rails-patterns.md +178 -0
- package/assets/skills/rust-patterns.md +211 -0
- package/assets/skills/search-patterns.md +227 -0
- package/assets/skills/security-hardening.md +237 -0
- package/assets/skills/seo-patterns.md +179 -0
- package/assets/skills/serverless-patterns.md +223 -0
- package/assets/skills/sql-optimization.md +154 -0
- package/assets/skills/state-management.md +254 -0
- package/assets/skills/storybook-patterns.md +330 -0
- package/assets/skills/svelte-patterns.md +258 -0
- package/assets/skills/swift-patterns.md +227 -0
- package/assets/skills/tailwind-patterns.md +272 -0
- package/assets/skills/tdd-workflow.md +199 -0
- package/assets/skills/terraform-patterns.md +270 -0
- package/assets/skills/testing-react.md +240 -0
- package/assets/skills/testing-vitest.md +232 -0
- package/assets/skills/typescript-strict.md +159 -0
- package/assets/skills/video-processing.md +340 -0
- package/assets/skills/vue-patterns.md +247 -0
- package/assets/skills/web-workers.md +327 -0
- package/assets/skills/webhooks-patterns.md +283 -0
- package/assets/skills/websocket-patterns.md +306 -0
- package/dist/cli/index.js +941 -958
- package/dist/core/index.d.ts +341 -11
- package/dist/core.js +58 -95
- package/dist/mcp/index.d.ts +33 -1
- package/dist/mcp-server.js +177 -255
- package/package.json +4 -1
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: golang-patterns
|
|
3
|
+
description: Go patterns for interfaces, error handling, goroutines, channels, context, testing, and project layout.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Go Patterns
|
|
7
|
+
|
|
8
|
+
## When to Use
|
|
9
|
+
|
|
10
|
+
Apply these patterns when writing Go 1.21+ code. Use this skill for designing
|
|
11
|
+
interfaces, handling errors idiomatically, managing concurrency with goroutines
|
|
12
|
+
and channels, propagating cancellation with context, and structuring projects.
|
|
13
|
+
|
|
14
|
+
## How It Works
|
|
15
|
+
|
|
16
|
+
### Small Interfaces
|
|
17
|
+
|
|
18
|
+
Define interfaces where they are consumed, not where they are implemented. Keep
|
|
19
|
+
interfaces small (1-3 methods). Accept interfaces, return structs.
|
|
20
|
+
|
|
21
|
+
```go
|
|
22
|
+
// In the consumer package, not the provider
|
|
23
|
+
type UserStore interface {
|
|
24
|
+
GetUser(ctx context.Context, id string) (*User, error)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
type Handler struct {
|
|
28
|
+
store UserStore // accepts interface
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
func NewHandler(s UserStore) *Handler {
|
|
32
|
+
return &Handler{store: s} // returns concrete struct
|
|
33
|
+
}
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
### Error Handling
|
|
37
|
+
|
|
38
|
+
Wrap errors with `fmt.Errorf("context: %w", err)` to build an error chain. Define
|
|
39
|
+
sentinel errors for expected conditions. Use `errors.Is` and `errors.As` to inspect.
|
|
40
|
+
|
|
41
|
+
```go
|
|
42
|
+
var ErrNotFound = errors.New("not found")
|
|
43
|
+
|
|
44
|
+
func (s *Store) GetUser(ctx context.Context, id string) (*User, error) {
|
|
45
|
+
row := s.db.QueryRowContext(ctx, "SELECT ... WHERE id = $1", id)
|
|
46
|
+
var u User
|
|
47
|
+
if err := row.Scan(&u.ID, &u.Name); err != nil {
|
|
48
|
+
if errors.Is(err, sql.ErrNoRows) {
|
|
49
|
+
return nil, fmt.Errorf("get user %s: %w", id, ErrNotFound)
|
|
50
|
+
}
|
|
51
|
+
return nil, fmt.Errorf("get user %s: %w", id, err)
|
|
52
|
+
}
|
|
53
|
+
return &u, nil
|
|
54
|
+
}
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
### Goroutines and Channels
|
|
58
|
+
|
|
59
|
+
Always ensure goroutines can exit. Use `errgroup` for fan-out with error propagation.
|
|
60
|
+
Use buffered channels when the producer should not block.
|
|
61
|
+
|
|
62
|
+
```go
|
|
63
|
+
import "golang.org/x/sync/errgroup"
|
|
64
|
+
|
|
65
|
+
func FetchAll(ctx context.Context, urls []string) ([]Response, error) {
|
|
66
|
+
g, ctx := errgroup.WithContext(ctx)
|
|
67
|
+
results := make([]Response, len(urls))
|
|
68
|
+
|
|
69
|
+
for i, url := range urls {
|
|
70
|
+
g.Go(func() error {
|
|
71
|
+
resp, err := fetch(ctx, url)
|
|
72
|
+
if err != nil {
|
|
73
|
+
return err
|
|
74
|
+
}
|
|
75
|
+
results[i] = resp
|
|
76
|
+
return nil
|
|
77
|
+
})
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if err := g.Wait(); err != nil {
|
|
81
|
+
return nil, err
|
|
82
|
+
}
|
|
83
|
+
return results, nil
|
|
84
|
+
}
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
### Context
|
|
88
|
+
|
|
89
|
+
Pass `context.Context` as the first parameter of every function that does I/O or
|
|
90
|
+
may be long-running. Never store context in a struct. Use `context.WithTimeout`
|
|
91
|
+
for deadlines.
|
|
92
|
+
|
|
93
|
+
```go
|
|
94
|
+
func (s *Service) Process(ctx context.Context, id string) error {
|
|
95
|
+
ctx, cancel := context.WithTimeout(ctx, 30*time.Second)
|
|
96
|
+
defer cancel()
|
|
97
|
+
|
|
98
|
+
data, err := s.store.Fetch(ctx, id)
|
|
99
|
+
if err != nil {
|
|
100
|
+
return fmt.Errorf("process %s: %w", id, err)
|
|
101
|
+
}
|
|
102
|
+
return s.transform(ctx, data)
|
|
103
|
+
}
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
### Functional Options
|
|
107
|
+
|
|
108
|
+
Use the functional options pattern for configurable constructors instead of
|
|
109
|
+
large config structs with zero values.
|
|
110
|
+
|
|
111
|
+
```go
|
|
112
|
+
type Server struct {
|
|
113
|
+
port int
|
|
114
|
+
timeout time.Duration
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
type Option func(*Server)
|
|
118
|
+
|
|
119
|
+
func WithPort(p int) Option { return func(s *Server) { s.port = p } }
|
|
120
|
+
func WithTimeout(d time.Duration) Option { return func(s *Server) { s.timeout = d } }
|
|
121
|
+
|
|
122
|
+
func NewServer(opts ...Option) *Server {
|
|
123
|
+
s := &Server{port: 8080, timeout: 30 * time.Second} // defaults
|
|
124
|
+
for _, opt := range opts {
|
|
125
|
+
opt(s)
|
|
126
|
+
}
|
|
127
|
+
return s
|
|
128
|
+
}
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
### Project Layout
|
|
132
|
+
|
|
133
|
+
```
|
|
134
|
+
project/
|
|
135
|
+
cmd/
|
|
136
|
+
server/main.go # entrypoint
|
|
137
|
+
internal/
|
|
138
|
+
user/ # domain package
|
|
139
|
+
user.go
|
|
140
|
+
store.go
|
|
141
|
+
handler.go
|
|
142
|
+
handler_test.go
|
|
143
|
+
pkg/ # public library code (use sparingly)
|
|
144
|
+
go.mod
|
|
145
|
+
go.sum
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
Keep `main.go` minimal: parse config, wire dependencies, start server. All logic
|
|
149
|
+
lives in `internal/` packages.
|
|
150
|
+
|
|
151
|
+
## Examples
|
|
152
|
+
|
|
153
|
+
**Pattern: Table-driven config validation**
|
|
154
|
+
```go
|
|
155
|
+
type rule struct {
|
|
156
|
+
name string
|
|
157
|
+
check func() error
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
func Validate(cfg Config) error {
|
|
161
|
+
rules := []rule{
|
|
162
|
+
{"port range", func() error { if cfg.Port < 1 || cfg.Port > 65535 { return errors.New("invalid port") }; return nil }},
|
|
163
|
+
{"timeout positive", func() error { if cfg.Timeout <= 0 { return errors.New("timeout must be positive") }; return nil }},
|
|
164
|
+
}
|
|
165
|
+
for _, r := range rules {
|
|
166
|
+
if err := r.check(); err != nil {
|
|
167
|
+
return fmt.Errorf("validate %s: %w", r.name, err)
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
return nil
|
|
171
|
+
}
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
## Checklist
|
|
175
|
+
|
|
176
|
+
- [ ] Interfaces defined at the consumer, 1-3 methods each
|
|
177
|
+
- [ ] Errors wrapped with `%w` and context about what failed
|
|
178
|
+
- [ ] Sentinel errors for expected conditions (`ErrNotFound`, `ErrConflict`)
|
|
179
|
+
- [ ] Every goroutine has a clear exit path (context cancellation, done channel)
|
|
180
|
+
- [ ] `errgroup` for fan-out with error propagation
|
|
181
|
+
- [ ] `context.Context` is the first parameter for I/O functions
|
|
182
|
+
- [ ] `defer cancel()` immediately after `context.WithTimeout`
|
|
183
|
+
- [ ] Functional options for constructors with optional configuration
|
|
184
|
+
- [ ] Business logic in `internal/`, entrypoints in `cmd/`
|
|
185
|
+
- [ ] `go vet` and `golangci-lint` pass with zero warnings
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: graphql-patterns
|
|
3
|
+
description: GraphQL patterns for schema design, resolvers, dataloaders, subscriptions, federation, and error handling.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# GraphQL Patterns
|
|
7
|
+
|
|
8
|
+
## When to Use
|
|
9
|
+
|
|
10
|
+
Apply these patterns when designing and implementing GraphQL APIs. Use this skill
|
|
11
|
+
for structuring schemas, writing efficient resolvers, solving N+1 problems with
|
|
12
|
+
DataLoader, implementing real-time subscriptions, federating across services, and
|
|
13
|
+
handling errors gracefully.
|
|
14
|
+
|
|
15
|
+
## How It Works
|
|
16
|
+
|
|
17
|
+
### Schema Design
|
|
18
|
+
|
|
19
|
+
Design schemas around the client's data needs, not your database tables. Use
|
|
20
|
+
clear naming conventions. Prefer object types over primitives for extensibility.
|
|
21
|
+
|
|
22
|
+
```graphql
|
|
23
|
+
type Query {
|
|
24
|
+
project(id: ID!): Project
|
|
25
|
+
projects(filter: ProjectFilter, first: Int = 20, after: String): ProjectConnection!
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
type Mutation {
|
|
29
|
+
createProject(input: CreateProjectInput!): CreateProjectPayload!
|
|
30
|
+
updateProject(id: ID!, input: UpdateProjectInput!): UpdateProjectPayload!
|
|
31
|
+
deleteProject(id: ID!): DeleteProjectPayload!
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
type Project {
|
|
35
|
+
id: ID!
|
|
36
|
+
name: String!
|
|
37
|
+
description: String
|
|
38
|
+
status: ProjectStatus!
|
|
39
|
+
owner: User!
|
|
40
|
+
tasks(first: Int = 10, after: String): TaskConnection!
|
|
41
|
+
createdAt: DateTime!
|
|
42
|
+
updatedAt: DateTime!
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
enum ProjectStatus {
|
|
46
|
+
ACTIVE
|
|
47
|
+
ARCHIVED
|
|
48
|
+
DRAFT
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
input CreateProjectInput {
|
|
52
|
+
name: String!
|
|
53
|
+
description: String
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
type CreateProjectPayload {
|
|
57
|
+
project: Project
|
|
58
|
+
errors: [UserError!]!
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
type UserError {
|
|
62
|
+
field: String
|
|
63
|
+
message: String!
|
|
64
|
+
code: ErrorCode!
|
|
65
|
+
}
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
### Relay-Style Pagination
|
|
69
|
+
|
|
70
|
+
Use cursor-based pagination with `Connection`, `Edge`, and `PageInfo` types. This
|
|
71
|
+
scales better than offset pagination and handles real-time insertions.
|
|
72
|
+
|
|
73
|
+
```graphql
|
|
74
|
+
type ProjectConnection {
|
|
75
|
+
edges: [ProjectEdge!]!
|
|
76
|
+
pageInfo: PageInfo!
|
|
77
|
+
totalCount: Int!
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
type ProjectEdge {
|
|
81
|
+
node: Project!
|
|
82
|
+
cursor: String!
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
type PageInfo {
|
|
86
|
+
hasNextPage: Boolean!
|
|
87
|
+
hasPreviousPage: Boolean!
|
|
88
|
+
startCursor: String
|
|
89
|
+
endCursor: String
|
|
90
|
+
}
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
### Resolvers
|
|
94
|
+
|
|
95
|
+
Keep resolvers thin. Delegate to service/data layer. Return the right types
|
|
96
|
+
for your schema. Use context for auth and shared services.
|
|
97
|
+
|
|
98
|
+
```typescript
|
|
99
|
+
const resolvers = {
|
|
100
|
+
Query: {
|
|
101
|
+
project: async (_, { id }, ctx) => {
|
|
102
|
+
const project = await ctx.services.project.findById(id)
|
|
103
|
+
if (!project) return null
|
|
104
|
+
if (!ctx.user.canView(project)) throw new ForbiddenError('Not authorized')
|
|
105
|
+
return project
|
|
106
|
+
},
|
|
107
|
+
|
|
108
|
+
projects: async (_, { filter, first, after }, ctx) => {
|
|
109
|
+
return ctx.services.project.paginate({ filter, first, after, userId: ctx.user.id })
|
|
110
|
+
},
|
|
111
|
+
},
|
|
112
|
+
|
|
113
|
+
Project: {
|
|
114
|
+
owner: (project, _, ctx) => {
|
|
115
|
+
return ctx.loaders.user.load(project.ownerId)
|
|
116
|
+
},
|
|
117
|
+
|
|
118
|
+
tasks: (project, { first, after }, ctx) => {
|
|
119
|
+
return ctx.services.task.paginateByProject(project.id, { first, after })
|
|
120
|
+
},
|
|
121
|
+
},
|
|
122
|
+
|
|
123
|
+
Mutation: {
|
|
124
|
+
createProject: async (_, { input }, ctx) => {
|
|
125
|
+
try {
|
|
126
|
+
const project = await ctx.services.project.create(input, ctx.user)
|
|
127
|
+
return { project, errors: [] }
|
|
128
|
+
} catch (err) {
|
|
129
|
+
return { project: null, errors: [{ field: 'name', message: err.message, code: 'INVALID_INPUT' }] }
|
|
130
|
+
}
|
|
131
|
+
},
|
|
132
|
+
},
|
|
133
|
+
}
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
### DataLoader for N+1 Prevention
|
|
137
|
+
|
|
138
|
+
DataLoader batches individual loads into a single query per tick. Create a new
|
|
139
|
+
DataLoader per request to avoid stale caches across users.
|
|
140
|
+
|
|
141
|
+
```typescript
|
|
142
|
+
import DataLoader from 'dataloader'
|
|
143
|
+
|
|
144
|
+
function createLoaders(db: Database) {
|
|
145
|
+
return {
|
|
146
|
+
user: new DataLoader<string, User>(async (ids) => {
|
|
147
|
+
const users = await db.user.findMany({ where: { id: { in: [...ids] } } })
|
|
148
|
+
const map = new Map(users.map(u => [u.id, u]))
|
|
149
|
+
return ids.map(id => map.get(id) ?? new Error(`User ${id} not found`))
|
|
150
|
+
}),
|
|
151
|
+
|
|
152
|
+
projectsByOwner: new DataLoader<string, Project[]>(async (ownerIds) => {
|
|
153
|
+
const projects = await db.project.findMany({ where: { ownerId: { in: [...ownerIds] } } })
|
|
154
|
+
const grouped = new Map<string, Project[]>()
|
|
155
|
+
for (const p of projects) {
|
|
156
|
+
const list = grouped.get(p.ownerId) ?? []
|
|
157
|
+
list.push(p)
|
|
158
|
+
grouped.set(p.ownerId, list)
|
|
159
|
+
}
|
|
160
|
+
return ownerIds.map(id => grouped.get(id) ?? [])
|
|
161
|
+
}),
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// In context factory (per request)
|
|
166
|
+
const context = ({ req }) => ({
|
|
167
|
+
user: authenticate(req),
|
|
168
|
+
loaders: createLoaders(db),
|
|
169
|
+
services,
|
|
170
|
+
})
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
### Subscriptions
|
|
174
|
+
|
|
175
|
+
Use subscriptions for real-time updates. Backed by a pub/sub system (Redis,
|
|
176
|
+
in-memory for dev). Filter events server-side.
|
|
177
|
+
|
|
178
|
+
```typescript
|
|
179
|
+
const resolvers = {
|
|
180
|
+
Subscription: {
|
|
181
|
+
taskUpdated: {
|
|
182
|
+
subscribe: (_, { projectId }, ctx) => {
|
|
183
|
+
if (!ctx.user.canView(projectId)) throw new ForbiddenError()
|
|
184
|
+
return ctx.pubsub.asyncIterableIterator(`TASK_UPDATED:${projectId}`)
|
|
185
|
+
},
|
|
186
|
+
},
|
|
187
|
+
},
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// In mutation resolver after updating a task:
|
|
191
|
+
await pubsub.publish(`TASK_UPDATED:${task.projectId}`, { taskUpdated: task })
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
### Federation
|
|
195
|
+
|
|
196
|
+
Split your graph across services. Each service owns its types. Use `@key` for
|
|
197
|
+
entity resolution and `extend` for cross-service references.
|
|
198
|
+
|
|
199
|
+
```graphql
|
|
200
|
+
# Users service
|
|
201
|
+
type User @key(fields: "id") {
|
|
202
|
+
id: ID!
|
|
203
|
+
name: String!
|
|
204
|
+
email: String!
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
# Projects service
|
|
208
|
+
type User @key(fields: "id") {
|
|
209
|
+
id: ID!
|
|
210
|
+
projects: [Project!]!
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
type Project @key(fields: "id") {
|
|
214
|
+
id: ID!
|
|
215
|
+
name: String!
|
|
216
|
+
owner: User!
|
|
217
|
+
}
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
## Examples
|
|
221
|
+
|
|
222
|
+
**Pattern: Query complexity limiting**
|
|
223
|
+
```typescript
|
|
224
|
+
import { createComplexityPlugin } from 'graphql-query-complexity'
|
|
225
|
+
|
|
226
|
+
const complexityPlugin = createComplexityPlugin({
|
|
227
|
+
maximumComplexity: 1000,
|
|
228
|
+
estimators: [fieldExtensionsEstimator(), simpleEstimator({ defaultComplexity: 1 })],
|
|
229
|
+
onComplete: (complexity) => { console.log('Query complexity:', complexity) },
|
|
230
|
+
})
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
## Checklist
|
|
234
|
+
|
|
235
|
+
- [ ] Schema designed around client needs, not database structure
|
|
236
|
+
- [ ] Input types for mutations; payload types with `errors` field for user errors
|
|
237
|
+
- [ ] Cursor-based pagination (`Connection`/`Edge`/`PageInfo`) for all lists
|
|
238
|
+
- [ ] DataLoader created per-request to batch and cache entity lookups
|
|
239
|
+
- [ ] Resolvers are thin: delegate to service/data layer
|
|
240
|
+
- [ ] Subscriptions filter events server-side (not client-side)
|
|
241
|
+
- [ ] Query depth and complexity limits enforced
|
|
242
|
+
- [ ] `@key` directives for federated entity resolution
|
|
243
|
+
- [ ] N+1 queries verified absent via query logging in dev
|
|
244
|
+
- [ ] Error codes in `UserError` type, not raw exception messages
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: i18n-patterns
|
|
3
|
+
description: Internationalization patterns with i18next, ICU message format, RTL layout, Intl API, and locale detection.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Internationalization Patterns
|
|
7
|
+
|
|
8
|
+
## When to Use
|
|
9
|
+
Apply these patterns when your app supports multiple languages or locales -- even if you start with just English. Retrofitting i18n is far more expensive than building it in from the start. This skill covers string externalization, ICU message formatting, plurals, right-to-left layout, date/number formatting with the Intl API, and automatic locale detection.
|
|
10
|
+
|
|
11
|
+
## How It Works
|
|
12
|
+
|
|
13
|
+
### i18next Setup -- Foundation
|
|
14
|
+
|
|
15
|
+
```typescript
|
|
16
|
+
import i18next from "i18next";
|
|
17
|
+
import Backend from "i18next-http-backend";
|
|
18
|
+
import LanguageDetector from "i18next-browser-languagedetector";
|
|
19
|
+
|
|
20
|
+
await i18next
|
|
21
|
+
.use(Backend)
|
|
22
|
+
.use(LanguageDetector)
|
|
23
|
+
.init({
|
|
24
|
+
fallbackLng: "en",
|
|
25
|
+
supportedLngs: ["en", "es", "fr", "de", "ja", "ar"],
|
|
26
|
+
ns: ["common", "auth", "dashboard"],
|
|
27
|
+
defaultNS: "common",
|
|
28
|
+
interpolation: { escapeValue: false },
|
|
29
|
+
detection: {
|
|
30
|
+
order: ["querystring", "cookie", "navigator", "htmlTag"],
|
|
31
|
+
caches: ["cookie"],
|
|
32
|
+
},
|
|
33
|
+
backend: { loadPath: "/locales/{{lng}}/{{ns}}.json" },
|
|
34
|
+
});
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
### Translation Files -- Organize by Namespace
|
|
38
|
+
|
|
39
|
+
```json
|
|
40
|
+
// locales/en/common.json
|
|
41
|
+
{
|
|
42
|
+
"nav": { "home": "Home", "settings": "Settings", "logout": "Log out" },
|
|
43
|
+
"actions": { "save": "Save", "cancel": "Cancel", "delete": "Delete" }
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// locales/es/common.json
|
|
47
|
+
{
|
|
48
|
+
"nav": { "home": "Inicio", "settings": "Configuracion", "logout": "Cerrar sesion" },
|
|
49
|
+
"actions": { "save": "Guardar", "cancel": "Cancelar", "delete": "Eliminar" }
|
|
50
|
+
}
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### ICU Message Format -- Plurals and Gender
|
|
54
|
+
|
|
55
|
+
```json
|
|
56
|
+
{
|
|
57
|
+
"items_count": "{count, plural, =0 {No items} one {# item} other {# items}}",
|
|
58
|
+
"greeting": "{gender, select, male {He} female {She} other {They}} updated their profile."
|
|
59
|
+
}
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
```typescript
|
|
63
|
+
t("items_count", { count: 0 }); // "No items"
|
|
64
|
+
t("items_count", { count: 1 }); // "1 item"
|
|
65
|
+
t("items_count", { count: 42 }); // "42 items"
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
| Language | Plural Categories |
|
|
69
|
+
|----------|------------------|
|
|
70
|
+
| English | one, other |
|
|
71
|
+
| French | one, many, other |
|
|
72
|
+
| Arabic | zero, one, two, few, many, other |
|
|
73
|
+
| Japanese | other (no plurals) |
|
|
74
|
+
|
|
75
|
+
Always use ICU plurals -- never `count === 1 ? "item" : "items"` in code.
|
|
76
|
+
|
|
77
|
+
### React Integration
|
|
78
|
+
|
|
79
|
+
```tsx
|
|
80
|
+
import { useTranslation, Trans } from "react-i18next";
|
|
81
|
+
|
|
82
|
+
function Dashboard() {
|
|
83
|
+
const { t } = useTranslation("dashboard");
|
|
84
|
+
return (
|
|
85
|
+
<div>
|
|
86
|
+
<h1>{t("title")}</h1>
|
|
87
|
+
<p>{t("items_count", { count: orders.length })}</p>
|
|
88
|
+
<Trans i18nKey="welcome_message" t={t}>
|
|
89
|
+
Welcome, <strong>{{ name: user.name }}</strong>!
|
|
90
|
+
Check your <a href="/inbox">inbox</a>.
|
|
91
|
+
</Trans>
|
|
92
|
+
</div>
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
### Date, Number, and Currency Formatting
|
|
98
|
+
|
|
99
|
+
```typescript
|
|
100
|
+
// Dates
|
|
101
|
+
new Intl.DateTimeFormat("de-DE", { dateStyle: "long", timeStyle: "short" })
|
|
102
|
+
.format(new Date()); // "26. Mai 2026, 14:30"
|
|
103
|
+
|
|
104
|
+
// Numbers
|
|
105
|
+
new Intl.NumberFormat("en-US", { notation: "compact", compactDisplay: "short" })
|
|
106
|
+
.format(1_500_000); // "1.5M"
|
|
107
|
+
|
|
108
|
+
// Currency
|
|
109
|
+
new Intl.NumberFormat("ja-JP", { style: "currency", currency: "JPY" })
|
|
110
|
+
.format(1500); // "Y1,500"
|
|
111
|
+
|
|
112
|
+
// Relative time
|
|
113
|
+
new Intl.RelativeTimeFormat("en", { numeric: "auto" })
|
|
114
|
+
.format(-1, "day"); // "yesterday"
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
### RTL (Right-to-Left) Layout
|
|
118
|
+
|
|
119
|
+
```css
|
|
120
|
+
/* Use logical properties -- not left/right */
|
|
121
|
+
.sidebar {
|
|
122
|
+
margin-inline-start: 1rem;
|
|
123
|
+
padding-inline-end: 2rem;
|
|
124
|
+
border-inline-start: 2px solid;
|
|
125
|
+
}
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
```tsx
|
|
129
|
+
function App() {
|
|
130
|
+
const { i18n } = useTranslation();
|
|
131
|
+
const dir = ["ar", "he", "fa"].includes(i18n.language) ? "rtl" : "ltr";
|
|
132
|
+
return (
|
|
133
|
+
<html lang={i18n.language} dir={dir}>
|
|
134
|
+
<body>{/* ... */}</body>
|
|
135
|
+
</html>
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
### Locale Detection Strategy
|
|
141
|
+
|
|
142
|
+
```typescript
|
|
143
|
+
import { match } from "@formatjs/intl-localematcher";
|
|
144
|
+
import Negotiator from "negotiator";
|
|
145
|
+
|
|
146
|
+
// Priority: URL param > cookie > Accept-Language header > default
|
|
147
|
+
function detectLocale(req: Request): string {
|
|
148
|
+
const negotiator = new Negotiator(req);
|
|
149
|
+
const languages = negotiator.languages();
|
|
150
|
+
return match(languages, ["en", "es", "fr", "de", "ja", "ar"], "en");
|
|
151
|
+
}
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
## Examples
|
|
155
|
+
|
|
156
|
+
| Pattern | Purpose | Key API |
|
|
157
|
+
|---------|---------|---------|
|
|
158
|
+
| ICU plurals | Correct grammar per language | i18next-icu |
|
|
159
|
+
| Intl.DateTimeFormat | Locale-aware dates | Built-in |
|
|
160
|
+
| Intl.NumberFormat | Numbers and currency | Built-in |
|
|
161
|
+
| CSS logical properties | RTL-compatible layout | Native CSS |
|
|
162
|
+
| Accept-Language | Server-side locale detection | Negotiator |
|
|
163
|
+
|
|
164
|
+
## Checklist
|
|
165
|
+
- [ ] All user-visible strings externalized into translation files
|
|
166
|
+
- [ ] Plurals use ICU format, not ternary operators
|
|
167
|
+
- [ ] Dates, numbers, and currencies formatted with `Intl` API
|
|
168
|
+
- [ ] CSS uses logical properties (`inline-start` / `inline-end`)
|
|
169
|
+
- [ ] `dir` attribute set on `<html>` based on language direction
|
|
170
|
+
- [ ] Locale detection follows priority: URL > cookie > header > default
|
|
171
|
+
- [ ] Translation keys are namespaced (`auth.login`, not `login`)
|
|
172
|
+
- [ ] No string concatenation for translated sentences
|