@qazuor/claude-code-config 0.1.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/LICENSE +21 -0
- package/README.md +1248 -0
- package/dist/bin.cjs +11886 -0
- package/dist/bin.cjs.map +1 -0
- package/dist/bin.d.cts +1 -0
- package/dist/bin.d.ts +1 -0
- package/dist/bin.js +11869 -0
- package/dist/bin.js.map +1 -0
- package/dist/index.cjs +3887 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +1325 -0
- package/dist/index.d.ts +1325 -0
- package/dist/index.js +3835 -0
- package/dist/index.js.map +1 -0
- package/package.json +86 -0
- package/templates/.log/notifications.log +1775 -0
- package/templates/agents/README.md +164 -0
- package/templates/agents/_registry.json +443 -0
- package/templates/agents/design/content-writer.md +353 -0
- package/templates/agents/design/ux-ui-designer.md +382 -0
- package/templates/agents/engineering/astro-engineer.md +293 -0
- package/templates/agents/engineering/db-drizzle-engineer.md +360 -0
- package/templates/agents/engineering/express-engineer.md +316 -0
- package/templates/agents/engineering/fastify-engineer.md +399 -0
- package/templates/agents/engineering/hono-engineer.md +263 -0
- package/templates/agents/engineering/mongoose-engineer.md +473 -0
- package/templates/agents/engineering/nestjs-engineer.md +429 -0
- package/templates/agents/engineering/nextjs-engineer.md +451 -0
- package/templates/agents/engineering/node-typescript-engineer.md +347 -0
- package/templates/agents/engineering/prisma-engineer.md +432 -0
- package/templates/agents/engineering/react-senior-dev.md +394 -0
- package/templates/agents/engineering/tanstack-start-engineer.md +447 -0
- package/templates/agents/engineering/tech-lead.md +269 -0
- package/templates/agents/product/product-functional.md +329 -0
- package/templates/agents/product/product-technical.md +578 -0
- package/templates/agents/quality/debugger.md +514 -0
- package/templates/agents/quality/qa-engineer.md +390 -0
- package/templates/agents/specialized/enrichment-agent.md +277 -0
- package/templates/agents/specialized/i18n-specialist.md +322 -0
- package/templates/agents/specialized/seo-ai-specialist.md +387 -0
- package/templates/agents/specialized/tech-writer.md +300 -0
- package/templates/code-style/.editorconfig +27 -0
- package/templates/code-style/.prettierignore +25 -0
- package/templates/code-style/.prettierrc +12 -0
- package/templates/code-style/biome.json +78 -0
- package/templates/code-style/commitlint.config.js +44 -0
- package/templates/commands/README.md +175 -0
- package/templates/commands/_registry.json +420 -0
- package/templates/commands/add-new-entity.md +211 -0
- package/templates/commands/audit/accessibility-audit.md +360 -0
- package/templates/commands/audit/performance-audit.md +290 -0
- package/templates/commands/audit/security-audit.md +231 -0
- package/templates/commands/code-check.md +127 -0
- package/templates/commands/five-why.md +225 -0
- package/templates/commands/formatting/format-markdown.md +197 -0
- package/templates/commands/git/commit.md +247 -0
- package/templates/commands/meta/create-agent.md +257 -0
- package/templates/commands/meta/create-command.md +312 -0
- package/templates/commands/meta/create-skill.md +321 -0
- package/templates/commands/meta/help.md +318 -0
- package/templates/commands/planning/check-completed-tasks.md +224 -0
- package/templates/commands/planning/cleanup-issues.md +248 -0
- package/templates/commands/planning/planning-cleanup.md +251 -0
- package/templates/commands/planning/sync-planning-github.md +133 -0
- package/templates/commands/planning/sync-todos-github.md +203 -0
- package/templates/commands/quality-check.md +211 -0
- package/templates/commands/run-tests.md +159 -0
- package/templates/commands/start-feature-plan.md +232 -0
- package/templates/commands/start-refactor-plan.md +244 -0
- package/templates/commands/sync-planning.md +176 -0
- package/templates/commands/update-docs.md +242 -0
- package/templates/docs/CHECKPOINT-SYSTEM.md +504 -0
- package/templates/docs/INDEX.md +677 -0
- package/templates/docs/RECOMMENDED-HOOKS.md +415 -0
- package/templates/docs/_registry.json +329 -0
- package/templates/docs/diagrams/README.md +220 -0
- package/templates/docs/diagrams/agent-hierarchy.mmd +55 -0
- package/templates/docs/diagrams/documentation-map.mmd +61 -0
- package/templates/docs/diagrams/tools-relationship.mmd +55 -0
- package/templates/docs/diagrams/workflow-decision-tree.mmd +38 -0
- package/templates/docs/doc-sync.md +533 -0
- package/templates/docs/examples/end-to-end-workflow.md +1505 -0
- package/templates/docs/glossary.md +495 -0
- package/templates/docs/guides/mockup-prompt-engineering.md +644 -0
- package/templates/docs/guides/mockup-setup.md +737 -0
- package/templates/docs/learnings/README.md +250 -0
- package/templates/docs/learnings/common-architectural-patterns.md +123 -0
- package/templates/docs/learnings/common-mistakes-to-avoid.md +149 -0
- package/templates/docs/learnings/markdown-formatting-standards.md +104 -0
- package/templates/docs/learnings/monorepo-command-execution.md +64 -0
- package/templates/docs/learnings/optimization-tips.md +146 -0
- package/templates/docs/learnings/planning-linear-sync-workflow.md +70 -0
- package/templates/docs/learnings/shell-compatibility-fish.md +46 -0
- package/templates/docs/learnings/test-organization-structure.md +68 -0
- package/templates/docs/mcp-installation.md +613 -0
- package/templates/docs/mcp-servers.md +989 -0
- package/templates/docs/notification-installation.md +570 -0
- package/templates/docs/quick-start.md +354 -0
- package/templates/docs/standards/architecture-patterns.md +1064 -0
- package/templates/docs/standards/atomic-commits.md +513 -0
- package/templates/docs/standards/code-standards.md +993 -0
- package/templates/docs/standards/design-standards.md +656 -0
- package/templates/docs/standards/documentation-standards.md +1160 -0
- package/templates/docs/standards/testing-standards.md +969 -0
- package/templates/docs/system-maintenance.md +604 -0
- package/templates/docs/templates/PDR-template.md +561 -0
- package/templates/docs/templates/TODOs-template.md +534 -0
- package/templates/docs/templates/tech-analysis-template.md +800 -0
- package/templates/docs/workflows/README.md +519 -0
- package/templates/docs/workflows/atomic-task-protocol.md +955 -0
- package/templates/docs/workflows/decision-tree.md +482 -0
- package/templates/docs/workflows/edge-cases.md +856 -0
- package/templates/docs/workflows/phase-1-planning.md +957 -0
- package/templates/docs/workflows/phase-2-implementation.md +896 -0
- package/templates/docs/workflows/phase-3-validation.md +792 -0
- package/templates/docs/workflows/phase-4-finalization.md +927 -0
- package/templates/docs/workflows/quick-fix-protocol.md +505 -0
- package/templates/docs/workflows/task-atomization.md +537 -0
- package/templates/docs/workflows/task-completion-protocol.md +448 -0
- package/templates/hooks/on-notification.sh +28 -0
- package/templates/schemas/checkpoint.schema.json +97 -0
- package/templates/schemas/code-registry.schema.json +84 -0
- package/templates/schemas/pdr.schema.json +314 -0
- package/templates/schemas/problems.schema.json +55 -0
- package/templates/schemas/tech-analysis.schema.json +404 -0
- package/templates/schemas/telemetry.schema.json +298 -0
- package/templates/schemas/todos.schema.json +234 -0
- package/templates/schemas/workflows.schema.json +69 -0
- package/templates/scripts/add-changelogs.sh +105 -0
- package/templates/scripts/generate-code-registry.ts +270 -0
- package/templates/scripts/health-check.sh +343 -0
- package/templates/scripts/sync-registry.sh +40 -0
- package/templates/scripts/telemetry-report.ts +36 -0
- package/templates/scripts/validate-docs.sh +224 -0
- package/templates/scripts/validate-registry.sh +225 -0
- package/templates/scripts/validate-schemas.ts +283 -0
- package/templates/scripts/validate-structure.sh +165 -0
- package/templates/scripts/worktree-cleanup.sh +81 -0
- package/templates/scripts/worktree-create.sh +63 -0
- package/templates/sessions/planning/.gitkeep +0 -0
- package/templates/sessions/planning/archived/.gitkeep +0 -0
- package/templates/settings.json +202 -0
- package/templates/settings.local.json +138 -0
- package/templates/skills/README.md +197 -0
- package/templates/skills/_registry.json +473 -0
- package/templates/skills/audit/accessibility-audit.md +309 -0
- package/templates/skills/audit/performance-audit.md +257 -0
- package/templates/skills/audit/security-audit.md +217 -0
- package/templates/skills/auth/nextauth-patterns.md +308 -0
- package/templates/skills/brand-guidelines.md +240 -0
- package/templates/skills/documentation/markdown-formatter.md +302 -0
- package/templates/skills/git/git-commit-helper.md +321 -0
- package/templates/skills/i18n/i18n-patterns.md +251 -0
- package/templates/skills/patterns/error-handling-patterns.md +242 -0
- package/templates/skills/patterns/tdd-methodology.md +342 -0
- package/templates/skills/qa/qa-criteria-validator.md +383 -0
- package/templates/skills/qa/web-app-testing.md +398 -0
- package/templates/skills/react/react-hook-form-patterns.md +359 -0
- package/templates/skills/state/redux-toolkit-patterns.md +272 -0
- package/templates/skills/state/tanstack-query-patterns.md +299 -0
- package/templates/skills/state/zustand-patterns.md +301 -0
- package/templates/skills/tech/mermaid-diagram-specialist.md +195 -0
- package/templates/skills/tech/shadcn-specialist.md +252 -0
- package/templates/skills/tech/vercel-specialist.md +297 -0
- package/templates/skills/testing/api-app-testing.md +254 -0
- package/templates/skills/testing/performance-testing.md +275 -0
- package/templates/skills/testing/security-testing.md +348 -0
- package/templates/skills/utils/add-memory.md +295 -0
- package/templates/skills/utils/json-data-auditor.md +283 -0
- package/templates/skills/utils/pdf-creator-editor.md +342 -0
- package/templates/tools/format-markdown.sh +185 -0
|
@@ -0,0 +1,1064 @@
|
|
|
1
|
+
# Architecture Patterns
|
|
2
|
+
|
|
3
|
+
This document defines the architectural patterns and design principles.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Table of Contents
|
|
8
|
+
|
|
9
|
+
<!-- markdownlint-disable MD051 -->
|
|
10
|
+
|
|
11
|
+
1. [Layer Architecture](#layer-architecture)
|
|
12
|
+
2. [Entity Creation Flow](#entity-creation-flow)
|
|
13
|
+
3. [Base Classes & Patterns](#base-classes--patterns)
|
|
14
|
+
4. [Database Patterns](#database-patterns)
|
|
15
|
+
5. [Service Patterns](#service-patterns)
|
|
16
|
+
6. [API Patterns](#api-patterns)
|
|
17
|
+
7. [Frontend Patterns](#frontend-patterns)
|
|
18
|
+
8. [Authentication & Authorization](#authentication--authorization)
|
|
19
|
+
9. [Error Handling](#error-handling)
|
|
20
|
+
10. [Validation Flow](#validation-flow)
|
|
21
|
+
|
|
22
|
+
<!-- markdownlint-enable MD051 -->
|
|
23
|
+
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
## Layer Architecture
|
|
27
|
+
|
|
28
|
+
### Bottom-Up Layers
|
|
29
|
+
|
|
30
|
+
```text
|
|
31
|
+
┌─────────────────────────────────────────┐
|
|
32
|
+
│ Frontend Layer │
|
|
33
|
+
│ (Astro, React, TanStack) │
|
|
34
|
+
│ - UI Components │
|
|
35
|
+
│ - State Management │
|
|
36
|
+
│ - Forms & Validation │
|
|
37
|
+
└─────────────────┬───────────────────────┘
|
|
38
|
+
│ HTTP/REST
|
|
39
|
+
┌─────────────────▼───────────────────────┐
|
|
40
|
+
│ API Layer │
|
|
41
|
+
│ (Hono Routes) │
|
|
42
|
+
│ - Request/Response Handling │
|
|
43
|
+
│ - Middleware (auth, validation) │
|
|
44
|
+
│ - Route Factories │
|
|
45
|
+
└─────────────────┬───────────────────────┘
|
|
46
|
+
│ Function Calls
|
|
47
|
+
┌─────────────────▼───────────────────────┐
|
|
48
|
+
│ Service Layer │
|
|
49
|
+
│ (Business Logic) │
|
|
50
|
+
│ - Services extending BaseCrudService │
|
|
51
|
+
│ - Business Rules & Validation │
|
|
52
|
+
│ - Transactions │
|
|
53
|
+
└─────────────────┬───────────────────────┘
|
|
54
|
+
│ ORM Calls
|
|
55
|
+
┌─────────────────▼───────────────────────┐
|
|
56
|
+
│ Database Layer │
|
|
57
|
+
│ (Drizzle ORM + PostgreSQL) │
|
|
58
|
+
│ - Models extending BaseModel │
|
|
59
|
+
│ - Database Schemas │
|
|
60
|
+
│ - Migrations │
|
|
61
|
+
└─────────────────────────────────────────┘
|
|
62
|
+
```text
|
|
63
|
+
|
|
64
|
+
### Layer Responsibilities
|
|
65
|
+
|
|
66
|
+
**Database Layer** (`@repo/db`)
|
|
67
|
+
|
|
68
|
+
- Define Drizzle schemas
|
|
69
|
+
- Implement models (extend `BaseModel`)
|
|
70
|
+
- Handle database queries
|
|
71
|
+
- Manage migrations
|
|
72
|
+
|
|
73
|
+
**Service Layer** (`@repo/service-core`)
|
|
74
|
+
|
|
75
|
+
- Implement business logic
|
|
76
|
+
- Enforce business rules
|
|
77
|
+
- Handle transactions
|
|
78
|
+
- Coordinate between models
|
|
79
|
+
|
|
80
|
+
**API Layer** (`apps/api`)
|
|
81
|
+
|
|
82
|
+
- Handle HTTP requests/responses
|
|
83
|
+
- Validate input (Zod)
|
|
84
|
+
- Authenticate/authorize requests
|
|
85
|
+
- Call service methods
|
|
86
|
+
|
|
87
|
+
**Frontend Layer** (`apps/web`, `apps/admin`)
|
|
88
|
+
|
|
89
|
+
- Render UI
|
|
90
|
+
- Handle user interactions
|
|
91
|
+
- Manage client state
|
|
92
|
+
- Call API endpoints
|
|
93
|
+
|
|
94
|
+
---
|
|
95
|
+
|
|
96
|
+
## Entity Creation Flow
|
|
97
|
+
|
|
98
|
+
### Complete Order (Never Skip Steps)
|
|
99
|
+
|
|
100
|
+
When creating a new entity, follow this exact order:
|
|
101
|
+
|
|
102
|
+
```text
|
|
103
|
+
|
|
104
|
+
1. Zod Schemas (@repo/schemas)
|
|
105
|
+
|
|
106
|
+
↓
|
|
107
|
+
|
|
108
|
+
2. Types via z.infer<typeof schema>
|
|
109
|
+
|
|
110
|
+
↓
|
|
111
|
+
|
|
112
|
+
3. Drizzle Schema (@repo/db/schemas)
|
|
113
|
+
|
|
114
|
+
↓
|
|
115
|
+
|
|
116
|
+
4. Model extending BaseModel (@repo/db/models)
|
|
117
|
+
|
|
118
|
+
↓
|
|
119
|
+
|
|
120
|
+
5. Service extending BaseCrudService (@repo/service-core)
|
|
121
|
+
|
|
122
|
+
↓
|
|
123
|
+
|
|
124
|
+
6. API Routes using factory pattern (apps/api/routes)
|
|
125
|
+
|
|
126
|
+
↓
|
|
127
|
+
|
|
128
|
+
7. Frontend Components (apps/web or apps/admin)
|
|
129
|
+
|
|
130
|
+
```text
|
|
131
|
+
|
|
132
|
+
### Step-by-Step Example: Entity Entity
|
|
133
|
+
|
|
134
|
+
#### Step 1: Zod Schemas
|
|
135
|
+
|
|
136
|
+
```typescript
|
|
137
|
+
// packages/schemas/src/entity/entity.schema.ts
|
|
138
|
+
|
|
139
|
+
import { z } from 'zod';
|
|
140
|
+
|
|
141
|
+
export const entitySchema = z.object({
|
|
142
|
+
id: z.string().uuid(),
|
|
143
|
+
name: z.string().min(1).max(200),
|
|
144
|
+
type: z.enum(['house', 'apartment', 'room']),
|
|
145
|
+
capacity: z.number().int().positive().max(50),
|
|
146
|
+
pricePerNight: z.number().positive(),
|
|
147
|
+
hostId: z.string().uuid(),
|
|
148
|
+
createdAt: z.date(),
|
|
149
|
+
updatedAt: z.date(),
|
|
150
|
+
deletedAt: z.date().nullable(),
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
export const createEntitySchema = entitySchema.omit({
|
|
154
|
+
id: true,
|
|
155
|
+
createdAt: true,
|
|
156
|
+
updatedAt: true,
|
|
157
|
+
deletedAt: true,
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
export const updateEntitySchema = createEntitySchema.partial();
|
|
161
|
+
|
|
162
|
+
export const searchEntitySchema = z.object({
|
|
163
|
+
q: z.string().optional(),
|
|
164
|
+
type: z.enum(['house', 'apartment', 'room']).optional(),
|
|
165
|
+
minCapacity: z.number().optional(),
|
|
166
|
+
maxPrice: z.number().optional(),
|
|
167
|
+
page: z.number().default(1),
|
|
168
|
+
pageSize: z.number().default(20),
|
|
169
|
+
});
|
|
170
|
+
```text
|
|
171
|
+
|
|
172
|
+
#### Step 2: Infer Types
|
|
173
|
+
|
|
174
|
+
```typescript
|
|
175
|
+
// In the same file or separate types file
|
|
176
|
+
export type Entity = z.infer<typeof entitySchema>;
|
|
177
|
+
export type CreateEntityInput = z.infer<typeof createEntitySchema>;
|
|
178
|
+
export type UpdateEntityInput = z.infer<typeof updateEntitySchema>;
|
|
179
|
+
export type SearchEntityInput = z.infer<typeof searchEntitySchema>;
|
|
180
|
+
```text
|
|
181
|
+
|
|
182
|
+
#### Step 3: Drizzle Schema
|
|
183
|
+
|
|
184
|
+
```typescript
|
|
185
|
+
// packages/db/src/schemas/entity/entity.schema.ts
|
|
186
|
+
|
|
187
|
+
import { pgTable, uuid, varchar, integer, numeric, timestamp } from 'drizzle-orm/pg-core';
|
|
188
|
+
import { users } from '../user/user.schema';
|
|
189
|
+
|
|
190
|
+
export const entitys = pgTable('entitys', {
|
|
191
|
+
id: uuid('id').primaryKey().defaultRandom(),
|
|
192
|
+
name: varchar('name', { length: 200 }).notNull(),
|
|
193
|
+
type: varchar('type', { length: 50 }).notNull(),
|
|
194
|
+
capacity: integer('capacity').notNull(),
|
|
195
|
+
pricePerNight: numeric('price_per_night', { precision: 10, scale: 2 }).notNull(),
|
|
196
|
+
hostId: uuid('host_id').references(() => users.id).notNull(),
|
|
197
|
+
createdAt: timestamp('created_at').defaultNow().notNull(),
|
|
198
|
+
updatedAt: timestamp('updated_at').defaultNow().notNull(),
|
|
199
|
+
deletedAt: timestamp('deleted_at'),
|
|
200
|
+
});
|
|
201
|
+
```text
|
|
202
|
+
|
|
203
|
+
#### Step 4: Model
|
|
204
|
+
|
|
205
|
+
```typescript
|
|
206
|
+
// packages/db/src/models/entity.model.ts
|
|
207
|
+
|
|
208
|
+
import { BaseModel } from './base.model';
|
|
209
|
+
import { entitys } from '../schemas/entity/entity.schema';
|
|
210
|
+
import type { Entity } from '@repo/schemas';
|
|
211
|
+
import { eq, and, isNull, ilike, sql } from 'drizzle-orm';
|
|
212
|
+
|
|
213
|
+
export class EntityModel extends BaseModel<Entity> {
|
|
214
|
+
protected table = entitys;
|
|
215
|
+
protected entityName = 'Entity';
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
|
|
219
|
+
* Find entitys with text search
|
|
220
|
+
|
|
221
|
+
*
|
|
222
|
+
|
|
223
|
+
* @param options - Search options
|
|
224
|
+
* @returns List of entitys matching search
|
|
225
|
+
|
|
226
|
+
*/
|
|
227
|
+
async findAll(options?: {
|
|
228
|
+
search?: { q?: string };
|
|
229
|
+
page?: number;
|
|
230
|
+
pageSize?: number;
|
|
231
|
+
}) {
|
|
232
|
+
const { search, page = 1, pageSize = 20 } = options ?? {};
|
|
233
|
+
|
|
234
|
+
const conditions = [isNull(this.table.deletedAt)];
|
|
235
|
+
|
|
236
|
+
if (search?.q) {
|
|
237
|
+
conditions.push(
|
|
238
|
+
ilike(this.table.name, `%${search.q}%`)
|
|
239
|
+
);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const offset = (page - 1) * pageSize;
|
|
243
|
+
|
|
244
|
+
const items = await this.db
|
|
245
|
+
.select()
|
|
246
|
+
.from(this.table)
|
|
247
|
+
.where(and(...conditions))
|
|
248
|
+
.limit(pageSize)
|
|
249
|
+
.offset(offset);
|
|
250
|
+
|
|
251
|
+
const [{ count }] = await this.db
|
|
252
|
+
.select({ count: sql<number>`count(*)` })
|
|
253
|
+
.from(this.table)
|
|
254
|
+
.where(and(...conditions));
|
|
255
|
+
|
|
256
|
+
return {
|
|
257
|
+
items,
|
|
258
|
+
pagination: {
|
|
259
|
+
page,
|
|
260
|
+
pageSize,
|
|
261
|
+
total: Number(count),
|
|
262
|
+
totalPages: Math.ceil(Number(count) / pageSize),
|
|
263
|
+
},
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
```text
|
|
268
|
+
|
|
269
|
+
#### Step 5: Service
|
|
270
|
+
|
|
271
|
+
```typescript
|
|
272
|
+
// packages/service-core/src/services/entity/entity.service.ts
|
|
273
|
+
|
|
274
|
+
import { BaseCrudService } from '../base-crud.service';
|
|
275
|
+
import { EntityModel } from '@repo/db';
|
|
276
|
+
import type { ServiceContext } from '../../types';
|
|
277
|
+
import type {
|
|
278
|
+
Entity,
|
|
279
|
+
CreateEntityInput,
|
|
280
|
+
UpdateEntityInput,
|
|
281
|
+
SearchEntityInput,
|
|
282
|
+
} from '@repo/schemas';
|
|
283
|
+
|
|
284
|
+
export class EntityService extends BaseCrudService<
|
|
285
|
+
Entity,
|
|
286
|
+
EntityModel,
|
|
287
|
+
CreateEntityInput,
|
|
288
|
+
UpdateEntityInput,
|
|
289
|
+
SearchEntityInput
|
|
290
|
+
> {
|
|
291
|
+
constructor(ctx: ServiceContext, model?: EntityModel) {
|
|
292
|
+
super(ctx, model ?? new EntityModel(ctx.db));
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
|
|
297
|
+
* Create entity with additional business logic
|
|
298
|
+
|
|
299
|
+
*/
|
|
300
|
+
async create({ input, user }: { input: CreateEntityInput; user: User }) {
|
|
301
|
+
return this.runWithLoggingAndValidation(
|
|
302
|
+
'create',
|
|
303
|
+
{ input, user },
|
|
304
|
+
async () => {
|
|
305
|
+
// Business rule: Only hosts can create entitys
|
|
306
|
+
if (user.role !== 'host') {
|
|
307
|
+
throw new ServiceError(
|
|
308
|
+
'Only hosts can create entitys',
|
|
309
|
+
ServiceErrorCode.FORBIDDEN
|
|
310
|
+
);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// Business rule: Set hostId to current user
|
|
314
|
+
const entityData = {
|
|
315
|
+
...input,
|
|
316
|
+
hostId: user.id,
|
|
317
|
+
};
|
|
318
|
+
|
|
319
|
+
return super.create({ input: entityData, user });
|
|
320
|
+
}
|
|
321
|
+
);
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
```text
|
|
325
|
+
|
|
326
|
+
#### Step 6: API Routes
|
|
327
|
+
|
|
328
|
+
```typescript
|
|
329
|
+
// apps/api/src/routes/entity/index.ts
|
|
330
|
+
|
|
331
|
+
import { Hono } from 'hono';
|
|
332
|
+
import { createCRUDRoute, createListRoute } from '../../factories/route-factory';
|
|
333
|
+
import { EntityService } from '@repo/service-core';
|
|
334
|
+
import {
|
|
335
|
+
createEntitySchema,
|
|
336
|
+
updateEntitySchema,
|
|
337
|
+
searchEntitySchema,
|
|
338
|
+
} from '@repo/schemas';
|
|
339
|
+
|
|
340
|
+
const app = new Hono();
|
|
341
|
+
|
|
342
|
+
// Public list route (no auth required)
|
|
343
|
+
const listRoute = createListRoute({
|
|
344
|
+
service: EntityService,
|
|
345
|
+
schema: searchEntitySchema,
|
|
346
|
+
options: {
|
|
347
|
+
skipAuth: true,
|
|
348
|
+
},
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
// Protected CRUD routes
|
|
352
|
+
const crudRoute = createCRUDRoute({
|
|
353
|
+
service: EntityService,
|
|
354
|
+
schemas: {
|
|
355
|
+
create: createEntitySchema,
|
|
356
|
+
update: updateEntitySchema,
|
|
357
|
+
search: searchEntitySchema,
|
|
358
|
+
},
|
|
359
|
+
options: {
|
|
360
|
+
skipAuth: false,
|
|
361
|
+
permissions: ['entity:write'],
|
|
362
|
+
},
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
app.route('/', listRoute); // GET /entitys (public)
|
|
366
|
+
app.route('/', crudRoute); // POST, PUT, DELETE (protected)
|
|
367
|
+
|
|
368
|
+
export default app;
|
|
369
|
+
```text
|
|
370
|
+
|
|
371
|
+
#### Step 7: Frontend Components
|
|
372
|
+
|
|
373
|
+
```typescript
|
|
374
|
+
// apps/web/src/components/entity/EntityList.tsx
|
|
375
|
+
|
|
376
|
+
import { useQuery } from '@tanstack/react-query';
|
|
377
|
+
import { EntityCard } from './EntityCard';
|
|
378
|
+
import type { SearchEntityInput } from '@repo/schemas';
|
|
379
|
+
|
|
380
|
+
const fetchEntitys = async (params: SearchEntityInput) => {
|
|
381
|
+
const res = await fetch(`/api/entitys?${new URLSearchParams(params)}`);
|
|
382
|
+
return res.json();
|
|
383
|
+
};
|
|
384
|
+
|
|
385
|
+
export const EntityList = () => {
|
|
386
|
+
const { data, isLoading } = useQuery({
|
|
387
|
+
queryKey: ['entitys'],
|
|
388
|
+
queryFn: () => fetchEntitys({ page: 1, pageSize: 20 }),
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
if (isLoading) return <div>Loading...</div>;
|
|
392
|
+
|
|
393
|
+
return (
|
|
394
|
+
<div className="grid grid-cols-3 gap-4">
|
|
395
|
+
{data.items.map((entity) => (
|
|
396
|
+
<EntityCard key={entity.id} entity={entity} />
|
|
397
|
+
))}
|
|
398
|
+
</div>
|
|
399
|
+
);
|
|
400
|
+
};
|
|
401
|
+
```text
|
|
402
|
+
|
|
403
|
+
---
|
|
404
|
+
|
|
405
|
+
## Base Classes & Patterns
|
|
406
|
+
|
|
407
|
+
### BaseModel Pattern
|
|
408
|
+
|
|
409
|
+
**All models MUST extend BaseModel:**
|
|
410
|
+
|
|
411
|
+
```typescript
|
|
412
|
+
import { BaseModel } from './base.model';
|
|
413
|
+
import type { YourEntity } from '@repo/schemas';
|
|
414
|
+
|
|
415
|
+
export class YourEntityModel extends BaseModel<YourEntity> {
|
|
416
|
+
protected table = yourEntitiesTable;
|
|
417
|
+
protected entityName = 'YourEntity';
|
|
418
|
+
|
|
419
|
+
// Override methods as needed
|
|
420
|
+
async findAll(options?: SearchOptions) {
|
|
421
|
+
// Custom implementation
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
```text
|
|
425
|
+
|
|
426
|
+
**BaseModel provides:**
|
|
427
|
+
|
|
428
|
+
- `findById(id)` - Find by primary key
|
|
429
|
+
- `findAll(options)` - List with pagination
|
|
430
|
+
- `create(data)` - Insert record
|
|
431
|
+
- `update(id, data)` - Update record
|
|
432
|
+
- `softDelete(id)` - Soft delete (set deletedAt)
|
|
433
|
+
- `restore(id)` - Restore soft deleted
|
|
434
|
+
- `hardDelete(id)` - Permanent delete
|
|
435
|
+
|
|
436
|
+
### BaseCrudService Pattern
|
|
437
|
+
|
|
438
|
+
**All services MUST extend BaseCrudService:**
|
|
439
|
+
|
|
440
|
+
```typescript
|
|
441
|
+
import { BaseCrudService } from '../base-crud.service';
|
|
442
|
+
|
|
443
|
+
export class YourService extends BaseCrudService<
|
|
444
|
+
Entity,
|
|
445
|
+
EntityModel,
|
|
446
|
+
CreateSchema,
|
|
447
|
+
UpdateSchema,
|
|
448
|
+
SearchSchema
|
|
449
|
+
> {
|
|
450
|
+
constructor(ctx: ServiceContext, model?: EntityModel) {
|
|
451
|
+
super(ctx, model ?? new EntityModel(ctx.db));
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
// Override or add methods
|
|
455
|
+
async customMethod({ input }: { input: CustomInput }) {
|
|
456
|
+
return this.runWithLoggingAndValidation(
|
|
457
|
+
'customMethod',
|
|
458
|
+
{ input },
|
|
459
|
+
async () => {
|
|
460
|
+
// Implementation
|
|
461
|
+
}
|
|
462
|
+
);
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
```text
|
|
466
|
+
|
|
467
|
+
**BaseCrudService provides:**
|
|
468
|
+
|
|
469
|
+
- `findById({ id })` - Get single record
|
|
470
|
+
- `findAll({ search, page, pageSize })` - List with pagination
|
|
471
|
+
- `create({ input, user })` - Create record
|
|
472
|
+
- `update({ id, input, user })` - Update record
|
|
473
|
+
- `delete({ id, user })` - Soft delete record
|
|
474
|
+
- `runWithLoggingAndValidation()` - Wrapper for logging/validation
|
|
475
|
+
|
|
476
|
+
---
|
|
477
|
+
|
|
478
|
+
## Database Patterns
|
|
479
|
+
|
|
480
|
+
### Soft Delete Pattern
|
|
481
|
+
|
|
482
|
+
**Default behavior: Soft delete**
|
|
483
|
+
|
|
484
|
+
```typescript
|
|
485
|
+
// Soft delete (sets deletedAt)
|
|
486
|
+
await model.softDelete(id);
|
|
487
|
+
|
|
488
|
+
// Restore
|
|
489
|
+
await model.restore(id);
|
|
490
|
+
|
|
491
|
+
// Hard delete (permanent)
|
|
492
|
+
await model.hardDelete(id);
|
|
493
|
+
```text
|
|
494
|
+
|
|
495
|
+
### Pagination Pattern
|
|
496
|
+
|
|
497
|
+
```typescript
|
|
498
|
+
// Request pagination
|
|
499
|
+
const result = await model.findAll({
|
|
500
|
+
page: 1,
|
|
501
|
+
pageSize: 20,
|
|
502
|
+
orderBy: { field: 'createdAt', direction: 'desc' },
|
|
503
|
+
});
|
|
504
|
+
|
|
505
|
+
// Response format
|
|
506
|
+
type PaginatedResult<T> = {
|
|
507
|
+
items: T[];
|
|
508
|
+
pagination: {
|
|
509
|
+
page: number;
|
|
510
|
+
pageSize: number;
|
|
511
|
+
total: number;
|
|
512
|
+
totalPages: number;
|
|
513
|
+
};
|
|
514
|
+
};
|
|
515
|
+
```text
|
|
516
|
+
|
|
517
|
+
### Transaction Pattern
|
|
518
|
+
|
|
519
|
+
```typescript
|
|
520
|
+
// Use transactions for multi-step operations
|
|
521
|
+
await db.transaction(async (trx) => {
|
|
522
|
+
const entity = await trx
|
|
523
|
+
.insert(entitys)
|
|
524
|
+
.values(data)
|
|
525
|
+
.returning();
|
|
526
|
+
|
|
527
|
+
await trx
|
|
528
|
+
.insert(entityImages)
|
|
529
|
+
.values(images.map(img => ({ ...img, entityId: entity.id })));
|
|
530
|
+
|
|
531
|
+
return entity;
|
|
532
|
+
});
|
|
533
|
+
```text
|
|
534
|
+
|
|
535
|
+
### Query Optimization
|
|
536
|
+
|
|
537
|
+
```typescript
|
|
538
|
+
// Use indexes for frequent queries
|
|
539
|
+
CREATE INDEX idx_entitys_host_id ON entitys(host_id);
|
|
540
|
+
CREATE INDEX idx_entitys_type ON entitys(type);
|
|
541
|
+
CREATE INDEX idx_entitys_created_at ON entitys(created_at DESC);
|
|
542
|
+
|
|
543
|
+
// Composite index for common filter combinations
|
|
544
|
+
CREATE INDEX idx_entitys_type_capacity ON entitys(type, capacity);
|
|
545
|
+
```text
|
|
546
|
+
|
|
547
|
+
---
|
|
548
|
+
|
|
549
|
+
## Service Patterns
|
|
550
|
+
|
|
551
|
+
### Business Logic Encapsulation
|
|
552
|
+
|
|
553
|
+
```typescript
|
|
554
|
+
export class BookingService extends BaseCrudService<...> {
|
|
555
|
+
/**
|
|
556
|
+
|
|
557
|
+
* Create booking with business rules
|
|
558
|
+
|
|
559
|
+
*/
|
|
560
|
+
async create({ input, user }: CreateParams) {
|
|
561
|
+
return this.runWithLoggingAndValidation(
|
|
562
|
+
'create',
|
|
563
|
+
{ input, user },
|
|
564
|
+
async () => {
|
|
565
|
+
// Business rule 1: Check availability
|
|
566
|
+
const isAvailable = await this.checkAvailability(
|
|
567
|
+
input.entityId,
|
|
568
|
+
input.checkIn,
|
|
569
|
+
input.checkOut
|
|
570
|
+
);
|
|
571
|
+
|
|
572
|
+
if (!isAvailable) {
|
|
573
|
+
throw new ServiceError(
|
|
574
|
+
'Entity not available for selected dates',
|
|
575
|
+
ServiceErrorCode.CONFLICT
|
|
576
|
+
);
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
// Business rule 2: Calculate total price
|
|
580
|
+
const entity = await this.entityModel.findById(
|
|
581
|
+
input.entityId
|
|
582
|
+
);
|
|
583
|
+
const nights = this.calculateNights(input.checkIn, input.checkOut);
|
|
584
|
+
const totalPrice = entity.pricePerNight * nights;
|
|
585
|
+
|
|
586
|
+
// Create booking
|
|
587
|
+
const booking = await this.model.create({
|
|
588
|
+
...input,
|
|
589
|
+
totalPrice,
|
|
590
|
+
status: 'pending',
|
|
591
|
+
userId: user.id,
|
|
592
|
+
});
|
|
593
|
+
|
|
594
|
+
// Business rule 3: Send confirmation email
|
|
595
|
+
await this.emailService.sendBookingConfirmation(booking);
|
|
596
|
+
|
|
597
|
+
return { booking };
|
|
598
|
+
}
|
|
599
|
+
);
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
```text
|
|
603
|
+
|
|
604
|
+
### Service Composition
|
|
605
|
+
|
|
606
|
+
```typescript
|
|
607
|
+
export class EntityService extends BaseCrudService<...> {
|
|
608
|
+
private imageService: ImageService;
|
|
609
|
+
private amenityService: AmenityService;
|
|
610
|
+
|
|
611
|
+
constructor(
|
|
612
|
+
ctx: ServiceContext,
|
|
613
|
+
model?: EntityModel,
|
|
614
|
+
imageService?: ImageService,
|
|
615
|
+
amenityService?: AmenityService
|
|
616
|
+
) {
|
|
617
|
+
super(ctx, model ?? new EntityModel(ctx.db));
|
|
618
|
+
this.imageService = imageService ?? new ImageService(ctx);
|
|
619
|
+
this.amenityService = amenityService ?? new AmenityService(ctx);
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
async create({ input, user }: CreateParams) {
|
|
623
|
+
return this.db.transaction(async (trx) => {
|
|
624
|
+
// Create entity
|
|
625
|
+
const entity = await this.model.create(input);
|
|
626
|
+
|
|
627
|
+
// Add images
|
|
628
|
+
if (input.images) {
|
|
629
|
+
await this.imageService.createMultiple({
|
|
630
|
+
entityId: entity.id,
|
|
631
|
+
images: input.images,
|
|
632
|
+
});
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
// Add amenities
|
|
636
|
+
if (input.amenityIds) {
|
|
637
|
+
await this.amenityService.linkToEntity({
|
|
638
|
+
entityId: entity.id,
|
|
639
|
+
amenityIds: input.amenityIds,
|
|
640
|
+
});
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
return { entity };
|
|
644
|
+
});
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
```text
|
|
648
|
+
|
|
649
|
+
---
|
|
650
|
+
|
|
651
|
+
## API Patterns
|
|
652
|
+
|
|
653
|
+
### Route Factory Pattern
|
|
654
|
+
|
|
655
|
+
**Always use factory functions:**
|
|
656
|
+
|
|
657
|
+
```typescript
|
|
658
|
+
// Simple list route (GET only)
|
|
659
|
+
const listRoute = createListRoute({
|
|
660
|
+
service: YourService,
|
|
661
|
+
schema: searchSchema,
|
|
662
|
+
options: {
|
|
663
|
+
skipAuth: true, // Public endpoint
|
|
664
|
+
},
|
|
665
|
+
});
|
|
666
|
+
|
|
667
|
+
// Full CRUD routes
|
|
668
|
+
const crudRoute = createCRUDRoute({
|
|
669
|
+
service: YourService,
|
|
670
|
+
schemas: {
|
|
671
|
+
create: createSchema,
|
|
672
|
+
update: updateSchema,
|
|
673
|
+
search: searchSchema,
|
|
674
|
+
},
|
|
675
|
+
options: {
|
|
676
|
+
skipAuth: false, // Protected endpoints
|
|
677
|
+
permissions: ['entity:write'],
|
|
678
|
+
},
|
|
679
|
+
});
|
|
680
|
+
|
|
681
|
+
// Custom route
|
|
682
|
+
const customRoute = createSimpleRoute({
|
|
683
|
+
method: 'post',
|
|
684
|
+
path: '/custom-action',
|
|
685
|
+
schema: customSchema,
|
|
686
|
+
handler: async (c) => {
|
|
687
|
+
const input = c.req.valid('json');
|
|
688
|
+
// Custom logic
|
|
689
|
+
return c.json({ success: true, data: result });
|
|
690
|
+
},
|
|
691
|
+
options: {
|
|
692
|
+
skipAuth: false,
|
|
693
|
+
permissions: ['custom:action'],
|
|
694
|
+
},
|
|
695
|
+
});
|
|
696
|
+
```text
|
|
697
|
+
|
|
698
|
+
### Middleware Chain
|
|
699
|
+
|
|
700
|
+
```typescript
|
|
701
|
+
// Order matters
|
|
702
|
+
app.post(
|
|
703
|
+
'/entitys',
|
|
704
|
+
// 1. CORS (if needed)
|
|
705
|
+
cors(),
|
|
706
|
+
// 2. Authentication
|
|
707
|
+
authenticate(),
|
|
708
|
+
// 3. Authorization
|
|
709
|
+
requirePermissions(['entity:write']),
|
|
710
|
+
// 4. Validation
|
|
711
|
+
zValidator('json', createEntitySchema),
|
|
712
|
+
// 5. Rate limiting
|
|
713
|
+
rateLimit({ max: 10, window: '1m' }),
|
|
714
|
+
// 6. Handler
|
|
715
|
+
async (c) => {
|
|
716
|
+
const input = c.req.valid('json');
|
|
717
|
+
const user = c.get('user');
|
|
718
|
+
// ...
|
|
719
|
+
}
|
|
720
|
+
);
|
|
721
|
+
```text
|
|
722
|
+
|
|
723
|
+
### Error Response Format
|
|
724
|
+
|
|
725
|
+
```typescript
|
|
726
|
+
// Success
|
|
727
|
+
type SuccessResponse<T> = {
|
|
728
|
+
success: true;
|
|
729
|
+
data: T;
|
|
730
|
+
};
|
|
731
|
+
|
|
732
|
+
// Error
|
|
733
|
+
type ErrorResponse = {
|
|
734
|
+
success: false;
|
|
735
|
+
error: {
|
|
736
|
+
code: string;
|
|
737
|
+
message: string;
|
|
738
|
+
details?: unknown;
|
|
739
|
+
};
|
|
740
|
+
};
|
|
741
|
+
|
|
742
|
+
// In handler
|
|
743
|
+
try {
|
|
744
|
+
const result = await service.method({ input });
|
|
745
|
+
return c.json({ success: true, data: result });
|
|
746
|
+
} catch (error) {
|
|
747
|
+
if (error instanceof ServiceError) {
|
|
748
|
+
return c.json(
|
|
749
|
+
{
|
|
750
|
+
success: false,
|
|
751
|
+
error: {
|
|
752
|
+
code: error.code,
|
|
753
|
+
message: error.message,
|
|
754
|
+
},
|
|
755
|
+
},
|
|
756
|
+
error.statusCode
|
|
757
|
+
);
|
|
758
|
+
}
|
|
759
|
+
throw error;
|
|
760
|
+
}
|
|
761
|
+
```text
|
|
762
|
+
|
|
763
|
+
---
|
|
764
|
+
|
|
765
|
+
## Frontend Patterns
|
|
766
|
+
|
|
767
|
+
### Query Key Factory
|
|
768
|
+
|
|
769
|
+
```typescript
|
|
770
|
+
// Define query keys centrally
|
|
771
|
+
export const entityKeys = {
|
|
772
|
+
all: ['entitys'] as const,
|
|
773
|
+
lists: () => [...entityKeys.all, 'list'] as const,
|
|
774
|
+
list: (filters: Filters) => [...entityKeys.lists(), filters] as const,
|
|
775
|
+
details: () => [...entityKeys.all, 'detail'] as const,
|
|
776
|
+
detail: (id: string) => [...entityKeys.details(), id] as const,
|
|
777
|
+
};
|
|
778
|
+
```text
|
|
779
|
+
|
|
780
|
+
### Query Hooks
|
|
781
|
+
|
|
782
|
+
```typescript
|
|
783
|
+
// Fetch single item
|
|
784
|
+
export const useEntity = (id: string) => {
|
|
785
|
+
return useQuery({
|
|
786
|
+
queryKey: entityKeys.detail(id),
|
|
787
|
+
queryFn: () => fetchEntity(id),
|
|
788
|
+
enabled: !!id,
|
|
789
|
+
});
|
|
790
|
+
};
|
|
791
|
+
|
|
792
|
+
// Fetch list
|
|
793
|
+
export const useEntitys = (filters: Filters) => {
|
|
794
|
+
return useQuery({
|
|
795
|
+
queryKey: entityKeys.list(filters),
|
|
796
|
+
queryFn: () => fetchEntitys(filters),
|
|
797
|
+
});
|
|
798
|
+
};
|
|
799
|
+
```text
|
|
800
|
+
|
|
801
|
+
### Mutation Hooks
|
|
802
|
+
|
|
803
|
+
```typescript
|
|
804
|
+
export const useCreateEntity = () => {
|
|
805
|
+
const queryClient = useQueryClient();
|
|
806
|
+
|
|
807
|
+
return useMutation({
|
|
808
|
+
mutationFn: createEntity,
|
|
809
|
+
onSuccess: () => {
|
|
810
|
+
// Invalidate lists
|
|
811
|
+
queryClient.invalidateQueries({
|
|
812
|
+
queryKey: entityKeys.lists()
|
|
813
|
+
});
|
|
814
|
+
},
|
|
815
|
+
});
|
|
816
|
+
};
|
|
817
|
+
```text
|
|
818
|
+
|
|
819
|
+
### Component Patterns
|
|
820
|
+
|
|
821
|
+
```typescript
|
|
822
|
+
// Container/Presenter pattern
|
|
823
|
+
export const EntityListContainer = () => {
|
|
824
|
+
const { data, isLoading, error } = useEntitys({ page: 1 });
|
|
825
|
+
|
|
826
|
+
if (isLoading) return <LoadingSpinner />;
|
|
827
|
+
if (error) return <ErrorMessage error={error} />;
|
|
828
|
+
|
|
829
|
+
return <EntityListPresenter entitys={data.items} />;
|
|
830
|
+
};
|
|
831
|
+
|
|
832
|
+
// Pure presenter component
|
|
833
|
+
type EntityListPresenterProps = {
|
|
834
|
+
entitys: Entity[];
|
|
835
|
+
};
|
|
836
|
+
|
|
837
|
+
export const EntityListPresenter = ({
|
|
838
|
+
entitys
|
|
839
|
+
}: EntityListPresenterProps) => {
|
|
840
|
+
return (
|
|
841
|
+
<div className="grid grid-cols-3 gap-4">
|
|
842
|
+
{entitys.map((entity) => (
|
|
843
|
+
<EntityCard key={entity.id} entity={entity} />
|
|
844
|
+
))}
|
|
845
|
+
</div>
|
|
846
|
+
);
|
|
847
|
+
};
|
|
848
|
+
```text
|
|
849
|
+
|
|
850
|
+
---
|
|
851
|
+
|
|
852
|
+
## Authentication & Authorization
|
|
853
|
+
|
|
854
|
+
### Get Actor from Context
|
|
855
|
+
|
|
856
|
+
```typescript
|
|
857
|
+
import { getActorFromContext } from '../utils/auth';
|
|
858
|
+
|
|
859
|
+
const handler = async (c: Context) => {
|
|
860
|
+
const actor = await getActorFromContext(c);
|
|
861
|
+
|
|
862
|
+
// Actor has: role, permissions, userId, etc.
|
|
863
|
+
console.log(actor.role); // 'admin' | 'host' | 'guest'
|
|
864
|
+
console.log(actor.permissions); // ['entity:write', ...]
|
|
865
|
+
};
|
|
866
|
+
```text
|
|
867
|
+
|
|
868
|
+
### Role-Based Access
|
|
869
|
+
|
|
870
|
+
```typescript
|
|
871
|
+
// Check role
|
|
872
|
+
if (actor.role !== 'admin') {
|
|
873
|
+
return c.json(
|
|
874
|
+
{ success: false, error: { code: 'FORBIDDEN', message: 'Admin only' } },
|
|
875
|
+
403
|
|
876
|
+
);
|
|
877
|
+
}
|
|
878
|
+
```text
|
|
879
|
+
|
|
880
|
+
### Permission-Based Access
|
|
881
|
+
|
|
882
|
+
```typescript
|
|
883
|
+
// Check permission
|
|
884
|
+
if (!actor.permissions.includes('entity:delete')) {
|
|
885
|
+
return c.json(
|
|
886
|
+
{ success: false, error: { code: 'FORBIDDEN', message: 'Permission denied' } },
|
|
887
|
+
403
|
|
888
|
+
);
|
|
889
|
+
}
|
|
890
|
+
```text
|
|
891
|
+
|
|
892
|
+
### Public Endpoints
|
|
893
|
+
|
|
894
|
+
```typescript
|
|
895
|
+
// Use skipAuth option in route factory
|
|
896
|
+
const publicRoute = createListRoute({
|
|
897
|
+
service: EntityService,
|
|
898
|
+
schema: searchSchema,
|
|
899
|
+
options: {
|
|
900
|
+
skipAuth: true, // No authentication required
|
|
901
|
+
},
|
|
902
|
+
});
|
|
903
|
+
```text
|
|
904
|
+
|
|
905
|
+
---
|
|
906
|
+
|
|
907
|
+
## Error Handling
|
|
908
|
+
|
|
909
|
+
### ServiceError Usage
|
|
910
|
+
|
|
911
|
+
```typescript
|
|
912
|
+
import { ServiceError, ServiceErrorCode } from '@repo/service-core';
|
|
913
|
+
|
|
914
|
+
// Not found
|
|
915
|
+
throw new ServiceError(
|
|
916
|
+
'Entity not found',
|
|
917
|
+
ServiceErrorCode.NOT_FOUND,
|
|
918
|
+
{ entityId: id }
|
|
919
|
+
);
|
|
920
|
+
|
|
921
|
+
// Validation error
|
|
922
|
+
throw new ServiceError(
|
|
923
|
+
'Invalid input data',
|
|
924
|
+
ServiceErrorCode.VALIDATION_ERROR,
|
|
925
|
+
{ errors: validationErrors }
|
|
926
|
+
);
|
|
927
|
+
|
|
928
|
+
// Forbidden
|
|
929
|
+
throw new ServiceError(
|
|
930
|
+
'You do not have permission to perform this action',
|
|
931
|
+
ServiceErrorCode.FORBIDDEN
|
|
932
|
+
);
|
|
933
|
+
|
|
934
|
+
// Conflict
|
|
935
|
+
throw new ServiceError(
|
|
936
|
+
'Email already in use',
|
|
937
|
+
ServiceErrorCode.CONFLICT,
|
|
938
|
+
{ email }
|
|
939
|
+
);
|
|
940
|
+
```text
|
|
941
|
+
|
|
942
|
+
### Global Error Handler
|
|
943
|
+
|
|
944
|
+
```typescript
|
|
945
|
+
// In app setup
|
|
946
|
+
app.onError((err, c) => {
|
|
947
|
+
if (err instanceof ServiceError) {
|
|
948
|
+
return c.json(
|
|
949
|
+
{
|
|
950
|
+
success: false,
|
|
951
|
+
error: {
|
|
952
|
+
code: err.code,
|
|
953
|
+
message: err.message,
|
|
954
|
+
},
|
|
955
|
+
},
|
|
956
|
+
err.statusCode
|
|
957
|
+
);
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
// Log unexpected errors
|
|
961
|
+
logger.error('Unexpected error', { error: err });
|
|
962
|
+
|
|
963
|
+
return c.json(
|
|
964
|
+
{
|
|
965
|
+
success: false,
|
|
966
|
+
error: {
|
|
967
|
+
code: 'INTERNAL_ERROR',
|
|
968
|
+
message: 'An unexpected error occurred',
|
|
969
|
+
},
|
|
970
|
+
},
|
|
971
|
+
500
|
|
972
|
+
);
|
|
973
|
+
});
|
|
974
|
+
```text
|
|
975
|
+
|
|
976
|
+
---
|
|
977
|
+
|
|
978
|
+
## Validation Flow
|
|
979
|
+
|
|
980
|
+
### Client → Server Validation
|
|
981
|
+
|
|
982
|
+
```text
|
|
983
|
+
|
|
984
|
+
1. User Input
|
|
985
|
+
|
|
986
|
+
↓
|
|
987
|
+
|
|
988
|
+
2. Client-Side Validation (Zod schema)
|
|
989
|
+
- Immediate feedback
|
|
990
|
+
- User-friendly errors
|
|
991
|
+
|
|
992
|
+
↓
|
|
993
|
+
|
|
994
|
+
3. API Request
|
|
995
|
+
|
|
996
|
+
↓
|
|
997
|
+
|
|
998
|
+
4. Server-Side Validation (Zod middleware)
|
|
999
|
+
- zValidator('json', schema)
|
|
1000
|
+
- Security layer
|
|
1001
|
+
|
|
1002
|
+
↓
|
|
1003
|
+
|
|
1004
|
+
5. Business Logic Validation (Service)
|
|
1005
|
+
- Business rules
|
|
1006
|
+
- Database constraints
|
|
1007
|
+
|
|
1008
|
+
↓
|
|
1009
|
+
|
|
1010
|
+
6. Database Constraints
|
|
1011
|
+
- Final safety net
|
|
1012
|
+
|
|
1013
|
+
```text
|
|
1014
|
+
|
|
1015
|
+
### Example
|
|
1016
|
+
|
|
1017
|
+
```typescript
|
|
1018
|
+
// 1. Client-side (React component)
|
|
1019
|
+
const { register, handleSubmit, formState: { errors } } = useForm({
|
|
1020
|
+
resolver: zodResolver(createEntitySchema),
|
|
1021
|
+
});
|
|
1022
|
+
|
|
1023
|
+
// 2. API route (Server-side)
|
|
1024
|
+
app.post(
|
|
1025
|
+
'/entitys',
|
|
1026
|
+
zValidator('json', createEntitySchema), // Validates again
|
|
1027
|
+
async (c) => {
|
|
1028
|
+
const input = c.req.valid('json'); // Validated and typed
|
|
1029
|
+
// ...
|
|
1030
|
+
}
|
|
1031
|
+
);
|
|
1032
|
+
|
|
1033
|
+
// 3. Service (Business logic)
|
|
1034
|
+
async create({ input, user }) {
|
|
1035
|
+
// Additional business validation
|
|
1036
|
+
if (input.capacity > 100) {
|
|
1037
|
+
throw new ServiceError('Capacity too high', ServiceErrorCode.VALIDATION_ERROR);
|
|
1038
|
+
}
|
|
1039
|
+
// ...
|
|
1040
|
+
}
|
|
1041
|
+
```text
|
|
1042
|
+
|
|
1043
|
+
---
|
|
1044
|
+
|
|
1045
|
+
## Summary Checklist
|
|
1046
|
+
|
|
1047
|
+
When implementing a new feature, ensure:
|
|
1048
|
+
|
|
1049
|
+
- [ ] Follows layer architecture (Database → Service → API → Frontend)
|
|
1050
|
+
- [ ] Entity created in correct order (Zod → Types → Drizzle → Model → Service → API)
|
|
1051
|
+
- [ ] Models extend BaseModel
|
|
1052
|
+
- [ ] Services extend BaseCrudService
|
|
1053
|
+
- [ ] API routes use factory pattern
|
|
1054
|
+
- [ ] Proper authentication/authorization
|
|
1055
|
+
- [ ] Soft delete pattern used
|
|
1056
|
+
- [ ] Transactions for multi-step operations
|
|
1057
|
+
- [ ] Validation at all layers
|
|
1058
|
+
- [ ] Consistent error handling
|
|
1059
|
+
- [ ] TanStack Query for frontend state
|
|
1060
|
+
|
|
1061
|
+
---
|
|
1062
|
+
|
|
1063
|
+
**These patterns are mandatory. Deviations must be discussed and approved.**
|
|
1064
|
+
|