schemantic 0.2.1 → 0.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/CHANGELOG.md +196 -159
- package/LICENSE +21 -21
- package/README.md +88 -830
- package/dist/cli/index.d.ts +4 -1
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +186 -62
- package/dist/cli/index.js.map +1 -1
- package/dist/core/configuration.d.ts +6 -0
- package/dist/core/configuration.d.ts.map +1 -0
- package/dist/core/configuration.js +64 -0
- package/dist/core/configuration.js.map +1 -0
- package/dist/core/index.d.ts +1 -0
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js +1 -0
- package/dist/core/index.js.map +1 -1
- package/dist/core/output-writer.d.ts +7 -0
- package/dist/core/output-writer.d.ts.map +1 -0
- package/dist/core/output-writer.js +255 -0
- package/dist/core/output-writer.js.map +1 -0
- package/dist/core/schemantic.d.ts +8 -6
- package/dist/core/schemantic.d.ts.map +1 -1
- package/dist/core/schemantic.js +194 -127
- package/dist/core/schemantic.js.map +1 -1
- package/dist/generators/api-client-generator.d.ts +9 -20
- package/dist/generators/api-client-generator.d.ts.map +1 -1
- package/dist/generators/api-client-generator.js +364 -424
- package/dist/generators/api-client-generator.js.map +1 -1
- package/dist/generators/base.d.ts.map +1 -1
- package/dist/generators/base.js +7 -25
- package/dist/generators/base.js.map +1 -1
- package/dist/generators/enum-generator.d.ts.map +1 -1
- package/dist/generators/enum-generator.js +30 -3
- package/dist/generators/enum-generator.js.map +1 -1
- package/dist/generators/hook-generator.d.ts +3 -17
- package/dist/generators/hook-generator.d.ts.map +1 -1
- package/dist/generators/hook-generator.js +51 -445
- package/dist/generators/hook-generator.js.map +1 -1
- package/dist/generators/naming.d.ts +10 -0
- package/dist/generators/naming.d.ts.map +1 -0
- package/dist/generators/naming.js +331 -0
- package/dist/generators/naming.js.map +1 -0
- package/dist/generators/object-generator.d.ts +0 -4
- package/dist/generators/object-generator.d.ts.map +1 -1
- package/dist/generators/object-generator.js +13 -101
- package/dist/generators/object-generator.js.map +1 -1
- package/dist/generators/primitive-generator.d.ts +1 -13
- package/dist/generators/primitive-generator.d.ts.map +1 -1
- package/dist/generators/primitive-generator.js +11 -76
- package/dist/generators/primitive-generator.js.map +1 -1
- package/dist/generators/projections.d.ts +10 -0
- package/dist/generators/projections.d.ts.map +1 -0
- package/dist/generators/projections.js +65 -0
- package/dist/generators/projections.js.map +1 -0
- package/dist/generators/schema-type.d.ts +23 -0
- package/dist/generators/schema-type.d.ts.map +1 -0
- package/dist/generators/schema-type.js +233 -0
- package/dist/generators/schema-type.js.map +1 -0
- package/dist/parsers/base.d.ts +2 -0
- package/dist/parsers/base.d.ts.map +1 -1
- package/dist/parsers/base.js +12 -2
- package/dist/parsers/base.js.map +1 -1
- package/dist/parsers/factory.d.ts.map +1 -1
- package/dist/parsers/factory.js +2 -1
- package/dist/parsers/factory.js.map +1 -1
- package/dist/parsers/openapi.d.ts +4 -0
- package/dist/parsers/openapi.d.ts.map +1 -1
- package/dist/parsers/openapi.js +107 -20
- package/dist/parsers/openapi.js.map +1 -1
- package/dist/plugins/branded-types.d.ts.map +1 -1
- package/dist/plugins/branded-types.js +231 -365
- package/dist/plugins/branded-types.js.map +1 -1
- package/dist/plugins/builtin.d.ts.map +1 -1
- package/dist/plugins/builtin.js +6 -141
- package/dist/plugins/builtin.js.map +1 -1
- package/dist/plugins/index.d.ts +4 -0
- package/dist/plugins/index.d.ts.map +1 -1
- package/dist/plugins/index.js +17 -0
- package/dist/plugins/index.js.map +1 -1
- package/dist/plugins/loader.d.ts +5 -1
- package/dist/plugins/loader.d.ts.map +1 -1
- package/dist/plugins/loader.js +42 -5
- package/dist/plugins/loader.js.map +1 -1
- package/dist/plugins/manager.d.ts.map +1 -1
- package/dist/plugins/manager.js +7 -7
- package/dist/plugins/manager.js.map +1 -1
- package/dist/plugins/performance-monitoring.d.ts +31 -1
- package/dist/plugins/performance-monitoring.d.ts.map +1 -1
- package/dist/plugins/performance-monitoring.js +146 -207
- package/dist/plugins/performance-monitoring.js.map +1 -1
- package/dist/plugins/request-deduplication.d.ts.map +1 -1
- package/dist/plugins/request-deduplication.js +151 -586
- package/dist/plugins/request-deduplication.js.map +1 -1
- package/dist/plugins/zod-validation.d.ts +23 -15
- package/dist/plugins/zod-validation.d.ts.map +1 -1
- package/dist/plugins/zod-validation.js +390 -268
- package/dist/plugins/zod-validation.js.map +1 -1
- package/dist/types/core.d.ts +20 -4
- package/dist/types/core.d.ts.map +1 -1
- package/dist/types/core.js +2 -1
- package/dist/types/core.js.map +1 -1
- package/dist/types/openapi.d.ts +14 -1
- package/dist/types/openapi.d.ts.map +1 -1
- package/docs/ADVANCED_PLUGINS_IMPLEMENTATION.md +24 -355
- package/docs/ADVANCED_PLUGIN_DEVELOPMENT.md +33 -309
- package/docs/API.md +80 -600
- package/docs/AUDIT.md +49 -0
- package/docs/CONFIGURATION.md +81 -722
- package/docs/FASTAPI_TUTORIAL.md +109 -1497
- package/docs/NAMING_MIGRATION.md +59 -0
- package/docs/PLUGINS.md +49 -687
- package/docs/PLUGIN_DEVELOPMENT.md +53 -720
- package/docs/REAUDIT.md +399 -0
- package/docs/RELEASING.md +88 -0
- package/docs/REMEDIATION.md +58 -0
- package/docs/superpowers/plans/2026-10-10-audit-remediation.md +113 -0
- package/package.json +114 -95
- package/dist/__tests__/setup.d.ts +0 -6
- package/dist/__tests__/setup.d.ts.map +0 -1
- package/dist/__tests__/setup.js +0 -30
- package/dist/__tests__/setup.js.map +0 -1
- package/dist/__tests__/test-config.d.ts +0 -36
- package/dist/__tests__/test-config.d.ts.map +0 -1
- package/dist/__tests__/test-config.js +0 -563
- package/dist/__tests__/test-config.js.map +0 -1
- package/dist/lib/api/api-client.d.ts +0 -71
- package/dist/lib/api/api-client.d.ts.map +0 -1
- package/dist/lib/api/api-client.js +0 -238
- package/dist/lib/api/api-client.js.map +0 -1
- package/dist/lib/api/barrel.d.ts +0 -4
- package/dist/lib/api/barrel.d.ts.map +0 -1
- package/dist/lib/api/barrel.js +0 -26
- package/dist/lib/api/barrel.js.map +0 -1
- package/dist/lib/api/hooks.d.ts +0 -36
- package/dist/lib/api/hooks.d.ts.map +0 -1
- package/dist/lib/api/hooks.js +0 -120
- package/dist/lib/api/hooks.js.map +0 -1
- package/dist/lib/api/index.d.ts +0 -4
- package/dist/lib/api/index.d.ts.map +0 -1
- package/dist/lib/api/index.js +0 -8
- package/dist/lib/api/index.js.map +0 -1
- package/dist/lib/api/types.d.ts +0 -366
- package/dist/lib/api/types.d.ts.map +0 -1
- package/dist/lib/api/types.js +0 -315
- package/dist/lib/api/types.js.map +0 -1
package/docs/FASTAPI_TUTORIAL.md
CHANGED
|
@@ -1,1497 +1,109 @@
|
|
|
1
|
-
# FastAPI
|
|
2
|
-
|
|
3
|
-
This
|
|
4
|
-
|
|
5
|
-
##
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
```
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
# Relationships
|
|
112
|
-
order = relationship("Order", back_populates="items")
|
|
113
|
-
product = relationship("Product", back_populates="order_items")
|
|
114
|
-
```
|
|
115
|
-
|
|
116
|
-
### 1.3 Create Pydantic Schemas
|
|
117
|
-
|
|
118
|
-
Create `backend/schemas.py`:
|
|
119
|
-
|
|
120
|
-
```python
|
|
121
|
-
from pydantic import BaseModel, EmailStr, Field
|
|
122
|
-
from datetime import datetime
|
|
123
|
-
from typing import List, Optional
|
|
124
|
-
from enum import Enum
|
|
125
|
-
|
|
126
|
-
# Enums
|
|
127
|
-
class OrderStatus(str, Enum):
|
|
128
|
-
pending = "pending"
|
|
129
|
-
processing = "processing"
|
|
130
|
-
shipped = "shipped"
|
|
131
|
-
delivered = "delivered"
|
|
132
|
-
cancelled = "cancelled"
|
|
133
|
-
|
|
134
|
-
class ProductCategory(str, Enum):
|
|
135
|
-
electronics = "electronics"
|
|
136
|
-
clothing = "clothing"
|
|
137
|
-
books = "books"
|
|
138
|
-
home = "home"
|
|
139
|
-
sports = "sports"
|
|
140
|
-
|
|
141
|
-
# Base schemas
|
|
142
|
-
class UserBase(BaseModel):
|
|
143
|
-
email: EmailStr = Field(..., description="User's email address")
|
|
144
|
-
username: str = Field(..., min_length=3, max_length=50, description="Unique username")
|
|
145
|
-
full_name: Optional[str] = Field(None, max_length=100, description="User's full name")
|
|
146
|
-
|
|
147
|
-
class UserCreate(UserBase):
|
|
148
|
-
password: str = Field(..., min_length=8, description="User's password (min 8 characters)")
|
|
149
|
-
|
|
150
|
-
class UserUpdate(BaseModel):
|
|
151
|
-
email: Optional[EmailStr] = Field(None, description="Updated email address")
|
|
152
|
-
username: Optional[str] = Field(None, min_length=3, max_length=50, description="Updated username")
|
|
153
|
-
full_name: Optional[str] = Field(None, max_length=100, description="Updated full name")
|
|
154
|
-
is_active: Optional[bool] = Field(None, description="Whether user is active")
|
|
155
|
-
|
|
156
|
-
class User(UserBase):
|
|
157
|
-
id: int = Field(..., description="Unique user identifier")
|
|
158
|
-
is_active: bool = Field(..., description="Whether user is active")
|
|
159
|
-
is_admin: bool = Field(..., description="Whether user has admin privileges")
|
|
160
|
-
created_at: datetime = Field(..., description="User creation timestamp")
|
|
161
|
-
updated_at: datetime = Field(..., description="Last update timestamp")
|
|
162
|
-
|
|
163
|
-
class Config:
|
|
164
|
-
from_attributes = True
|
|
165
|
-
|
|
166
|
-
# Product schemas
|
|
167
|
-
class ProductBase(BaseModel):
|
|
168
|
-
name: str = Field(..., min_length=1, max_length=200, description="Product name")
|
|
169
|
-
description: Optional[str] = Field(None, max_length=1000, description="Product description")
|
|
170
|
-
price: float = Field(..., gt=0, description="Product price (must be positive)")
|
|
171
|
-
category: ProductCategory = Field(..., description="Product category")
|
|
172
|
-
stock_quantity: int = Field(0, ge=0, description="Available stock quantity")
|
|
173
|
-
image_url: Optional[str] = Field(None, description="Product image URL")
|
|
174
|
-
|
|
175
|
-
class ProductCreate(ProductBase):
|
|
176
|
-
pass
|
|
177
|
-
|
|
178
|
-
class ProductUpdate(BaseModel):
|
|
179
|
-
name: Optional[str] = Field(None, min_length=1, max_length=200, description="Updated product name")
|
|
180
|
-
description: Optional[str] = Field(None, max_length=1000, description="Updated description")
|
|
181
|
-
price: Optional[float] = Field(None, gt=0, description="Updated price")
|
|
182
|
-
category: Optional[ProductCategory] = Field(None, description="Updated category")
|
|
183
|
-
stock_quantity: Optional[int] = Field(None, ge=0, description="Updated stock quantity")
|
|
184
|
-
image_url: Optional[str] = Field(None, description="Updated image URL")
|
|
185
|
-
is_active: Optional[bool] = Field(None, description="Whether product is active")
|
|
186
|
-
|
|
187
|
-
class Product(ProductBase):
|
|
188
|
-
id: int = Field(..., description="Unique product identifier")
|
|
189
|
-
is_active: bool = Field(..., description="Whether product is active")
|
|
190
|
-
created_at: datetime = Field(..., description="Product creation timestamp")
|
|
191
|
-
updated_at: datetime = Field(..., description="Last update timestamp")
|
|
192
|
-
|
|
193
|
-
class Config:
|
|
194
|
-
from_attributes = True
|
|
195
|
-
|
|
196
|
-
# Order schemas
|
|
197
|
-
class OrderItemBase(BaseModel):
|
|
198
|
-
product_id: int = Field(..., description="Product identifier")
|
|
199
|
-
quantity: int = Field(..., gt=0, description="Quantity ordered")
|
|
200
|
-
|
|
201
|
-
class OrderItemCreate(OrderItemBase):
|
|
202
|
-
pass
|
|
203
|
-
|
|
204
|
-
class OrderItem(OrderItemBase):
|
|
205
|
-
id: int = Field(..., description="Unique order item identifier")
|
|
206
|
-
unit_price: float = Field(..., description="Price per unit at time of order")
|
|
207
|
-
product: Product = Field(..., description="Product details")
|
|
208
|
-
|
|
209
|
-
class Config:
|
|
210
|
-
from_attributes = True
|
|
211
|
-
|
|
212
|
-
class OrderBase(BaseModel):
|
|
213
|
-
shipping_address: str = Field(..., min_length=10, max_length=500, description="Shipping address")
|
|
214
|
-
|
|
215
|
-
class OrderCreate(OrderBase):
|
|
216
|
-
items: List[OrderItemCreate] = Field(..., min_items=1, description="List of items to order")
|
|
217
|
-
|
|
218
|
-
class OrderUpdate(BaseModel):
|
|
219
|
-
status: Optional[OrderStatus] = Field(None, description="Updated order status")
|
|
220
|
-
shipping_address: Optional[str] = Field(None, min_length=10, max_length=500, description="Updated shipping address")
|
|
221
|
-
|
|
222
|
-
class Order(OrderBase):
|
|
223
|
-
id: int = Field(..., description="Unique order identifier")
|
|
224
|
-
user_id: int = Field(..., description="User who placed the order")
|
|
225
|
-
total_amount: float = Field(..., description="Total order amount")
|
|
226
|
-
status: OrderStatus = Field(..., description="Current order status")
|
|
227
|
-
created_at: datetime = Field(..., description="Order creation timestamp")
|
|
228
|
-
updated_at: datetime = Field(..., description="Last update timestamp")
|
|
229
|
-
items: List[OrderItem] = Field(..., description="List of ordered items")
|
|
230
|
-
user: User = Field(..., description="User details")
|
|
231
|
-
|
|
232
|
-
class Config:
|
|
233
|
-
from_attributes = True
|
|
234
|
-
|
|
235
|
-
# Authentication schemas
|
|
236
|
-
class Token(BaseModel):
|
|
237
|
-
access_token: str = Field(..., description="JWT access token")
|
|
238
|
-
token_type: str = Field("bearer", description="Token type")
|
|
239
|
-
|
|
240
|
-
class TokenData(BaseModel):
|
|
241
|
-
username: Optional[str] = None
|
|
242
|
-
|
|
243
|
-
class LoginRequest(BaseModel):
|
|
244
|
-
username: str = Field(..., description="Username or email")
|
|
245
|
-
password: str = Field(..., description="User password")
|
|
246
|
-
|
|
247
|
-
# API Response schemas
|
|
248
|
-
class APIResponse(BaseModel):
|
|
249
|
-
success: bool = Field(..., description="Whether the operation was successful")
|
|
250
|
-
message: str = Field(..., description="Response message")
|
|
251
|
-
data: Optional[dict] = Field(None, description="Response data")
|
|
252
|
-
|
|
253
|
-
class PaginatedResponse(BaseModel):
|
|
254
|
-
items: List[dict] = Field(..., description="List of items")
|
|
255
|
-
total: int = Field(..., description="Total number of items")
|
|
256
|
-
page: int = Field(..., description="Current page number")
|
|
257
|
-
per_page: int = Field(..., description="Items per page")
|
|
258
|
-
pages: int = Field(..., description="Total number of pages")
|
|
259
|
-
|
|
260
|
-
# Health check schema
|
|
261
|
-
class HealthCheck(BaseModel):
|
|
262
|
-
status: str = Field("healthy", description="Service health status")
|
|
263
|
-
timestamp: datetime = Field(..., description="Health check timestamp")
|
|
264
|
-
version: str = Field("1.0.0", description="API version")
|
|
265
|
-
```
|
|
266
|
-
|
|
267
|
-
### 1.4 Create the FastAPI Application
|
|
268
|
-
|
|
269
|
-
Create `backend/main.py`:
|
|
270
|
-
|
|
271
|
-
```python
|
|
272
|
-
from fastapi import FastAPI, Depends, HTTPException, status
|
|
273
|
-
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
|
|
274
|
-
from fastapi.middleware.cors import CORSMiddleware
|
|
275
|
-
from sqlalchemy.orm import Session
|
|
276
|
-
from typing import List, Optional
|
|
277
|
-
from datetime import datetime, timedelta
|
|
278
|
-
import uvicorn
|
|
279
|
-
|
|
280
|
-
from models import Base, User, Product, Order, OrderItem
|
|
281
|
-
from schemas import (
|
|
282
|
-
User as UserSchema, UserCreate, UserUpdate,
|
|
283
|
-
Product as ProductSchema, ProductCreate, ProductUpdate,
|
|
284
|
-
Order as OrderSchema, OrderCreate, OrderUpdate,
|
|
285
|
-
Token, LoginRequest, HealthCheck, APIResponse, PaginatedResponse,
|
|
286
|
-
OrderStatus, ProductCategory
|
|
287
|
-
)
|
|
288
|
-
from database import engine, get_db
|
|
289
|
-
from auth import authenticate_user, create_access_token, get_current_user, get_password_hash
|
|
290
|
-
|
|
291
|
-
# Create database tables
|
|
292
|
-
Base.metadata.create_all(bind=engine)
|
|
293
|
-
|
|
294
|
-
# FastAPI app configuration
|
|
295
|
-
app = FastAPI(
|
|
296
|
-
title="E-Commerce API",
|
|
297
|
-
description="A comprehensive e-commerce API built with FastAPI and Schemantic",
|
|
298
|
-
version="1.0.0",
|
|
299
|
-
contact={
|
|
300
|
-
"name": "API Support",
|
|
301
|
-
"email": "support@ecommerce-api.com",
|
|
302
|
-
},
|
|
303
|
-
license_info={
|
|
304
|
-
"name": "MIT",
|
|
305
|
-
"url": "https://opensource.org/licenses/MIT",
|
|
306
|
-
},
|
|
307
|
-
)
|
|
308
|
-
|
|
309
|
-
# CORS middleware
|
|
310
|
-
app.add_middleware(
|
|
311
|
-
CORSMiddleware,
|
|
312
|
-
allow_origins=["http://localhost:3000", "http://localhost:5173"], # React dev servers
|
|
313
|
-
allow_credentials=True,
|
|
314
|
-
allow_methods=["*"],
|
|
315
|
-
allow_headers=["*"],
|
|
316
|
-
)
|
|
317
|
-
|
|
318
|
-
# OAuth2 scheme
|
|
319
|
-
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="auth/login")
|
|
320
|
-
|
|
321
|
-
# Health check endpoint
|
|
322
|
-
@app.get("/health", response_model=HealthCheck, tags=["Health"])
|
|
323
|
-
async def health_check():
|
|
324
|
-
"""Check the health status of the API"""
|
|
325
|
-
return HealthCheck(timestamp=datetime.utcnow())
|
|
326
|
-
|
|
327
|
-
# Authentication endpoints
|
|
328
|
-
@app.post("/auth/register", response_model=UserSchema, tags=["Authentication"])
|
|
329
|
-
async def register_user(user: UserCreate, db: Session = Depends(get_db)):
|
|
330
|
-
"""Register a new user account"""
|
|
331
|
-
# Check if user already exists
|
|
332
|
-
db_user = db.query(User).filter(
|
|
333
|
-
(User.email == user.email) | (User.username == user.username)
|
|
334
|
-
).first()
|
|
335
|
-
|
|
336
|
-
if db_user:
|
|
337
|
-
raise HTTPException(
|
|
338
|
-
status_code=400,
|
|
339
|
-
detail="User with this email or username already exists"
|
|
340
|
-
)
|
|
341
|
-
|
|
342
|
-
# Create new user
|
|
343
|
-
hashed_password = get_password_hash(user.password)
|
|
344
|
-
db_user = User(
|
|
345
|
-
email=user.email,
|
|
346
|
-
username=user.username,
|
|
347
|
-
full_name=user.full_name,
|
|
348
|
-
hashed_password=hashed_password
|
|
349
|
-
)
|
|
350
|
-
|
|
351
|
-
db.add(db_user)
|
|
352
|
-
db.commit()
|
|
353
|
-
db.refresh(db_user)
|
|
354
|
-
|
|
355
|
-
return db_user
|
|
356
|
-
|
|
357
|
-
@app.post("/auth/login", response_model=Token, tags=["Authentication"])
|
|
358
|
-
async def login_user(form_data: OAuth2PasswordRequestForm = Depends(), db: Session = Depends(get_db)):
|
|
359
|
-
"""Authenticate user and return access token"""
|
|
360
|
-
user = authenticate_user(db, form_data.username, form_data.password)
|
|
361
|
-
if not user:
|
|
362
|
-
raise HTTPException(
|
|
363
|
-
status_code=status.HTTP_401_UNAUTHORIZED,
|
|
364
|
-
detail="Incorrect username or password",
|
|
365
|
-
headers={"WWW-Authenticate": "Bearer"},
|
|
366
|
-
)
|
|
367
|
-
|
|
368
|
-
access_token = create_access_token(data={"sub": user.username})
|
|
369
|
-
return {"access_token": access_token, "token_type": "bearer"}
|
|
370
|
-
|
|
371
|
-
@app.get("/auth/me", response_model=UserSchema, tags=["Authentication"])
|
|
372
|
-
async def get_current_user_info(current_user: User = Depends(get_current_user)):
|
|
373
|
-
"""Get current authenticated user information"""
|
|
374
|
-
return current_user
|
|
375
|
-
|
|
376
|
-
# User management endpoints
|
|
377
|
-
@app.get("/users", response_model=List[UserSchema], tags=["Users"])
|
|
378
|
-
async def get_users(
|
|
379
|
-
skip: int = 0,
|
|
380
|
-
limit: int = 100,
|
|
381
|
-
current_user: User = Depends(get_current_user),
|
|
382
|
-
db: Session = Depends(get_db)
|
|
383
|
-
):
|
|
384
|
-
"""Get list of users (admin only)"""
|
|
385
|
-
if not current_user.is_admin:
|
|
386
|
-
raise HTTPException(status_code=403, detail="Not enough permissions")
|
|
387
|
-
|
|
388
|
-
users = db.query(User).offset(skip).limit(limit).all()
|
|
389
|
-
return users
|
|
390
|
-
|
|
391
|
-
@app.get("/users/{user_id}", response_model=UserSchema, tags=["Users"])
|
|
392
|
-
async def get_user(
|
|
393
|
-
user_id: int,
|
|
394
|
-
current_user: User = Depends(get_current_user),
|
|
395
|
-
db: Session = Depends(get_db)
|
|
396
|
-
):
|
|
397
|
-
"""Get user by ID"""
|
|
398
|
-
if current_user.id != user_id and not current_user.is_admin:
|
|
399
|
-
raise HTTPException(status_code=403, detail="Not enough permissions")
|
|
400
|
-
|
|
401
|
-
user = db.query(User).filter(User.id == user_id).first()
|
|
402
|
-
if user is None:
|
|
403
|
-
raise HTTPException(status_code=404, detail="User not found")
|
|
404
|
-
|
|
405
|
-
return user
|
|
406
|
-
|
|
407
|
-
@app.put("/users/{user_id}", response_model=UserSchema, tags=["Users"])
|
|
408
|
-
async def update_user(
|
|
409
|
-
user_id: int,
|
|
410
|
-
user_update: UserUpdate,
|
|
411
|
-
current_user: User = Depends(get_current_user),
|
|
412
|
-
db: Session = Depends(get_db)
|
|
413
|
-
):
|
|
414
|
-
"""Update user information"""
|
|
415
|
-
if current_user.id != user_id and not current_user.is_admin:
|
|
416
|
-
raise HTTPException(status_code=403, detail="Not enough permissions")
|
|
417
|
-
|
|
418
|
-
user = db.query(User).filter(User.id == user_id).first()
|
|
419
|
-
if user is None:
|
|
420
|
-
raise HTTPException(status_code=404, detail="User not found")
|
|
421
|
-
|
|
422
|
-
update_data = user_update.dict(exclude_unset=True)
|
|
423
|
-
for field, value in update_data.items():
|
|
424
|
-
setattr(user, field, value)
|
|
425
|
-
|
|
426
|
-
user.updated_at = datetime.utcnow()
|
|
427
|
-
db.commit()
|
|
428
|
-
db.refresh(user)
|
|
429
|
-
|
|
430
|
-
return user
|
|
431
|
-
|
|
432
|
-
# Product endpoints
|
|
433
|
-
@app.get("/products", response_model=List[ProductSchema], tags=["Products"])
|
|
434
|
-
async def get_products(
|
|
435
|
-
skip: int = 0,
|
|
436
|
-
limit: int = 100,
|
|
437
|
-
category: Optional[ProductCategory] = None,
|
|
438
|
-
search: Optional[str] = None,
|
|
439
|
-
db: Session = Depends(get_db)
|
|
440
|
-
):
|
|
441
|
-
"""Get list of products with optional filtering"""
|
|
442
|
-
query = db.query(Product).filter(Product.is_active == True)
|
|
443
|
-
|
|
444
|
-
if category:
|
|
445
|
-
query = query.filter(Product.category == category)
|
|
446
|
-
|
|
447
|
-
if search:
|
|
448
|
-
query = query.filter(Product.name.contains(search))
|
|
449
|
-
|
|
450
|
-
products = query.offset(skip).limit(limit).all()
|
|
451
|
-
return products
|
|
452
|
-
|
|
453
|
-
@app.get("/products/{product_id}", response_model=ProductSchema, tags=["Products"])
|
|
454
|
-
async def get_product(product_id: int, db: Session = Depends(get_db)):
|
|
455
|
-
"""Get product by ID"""
|
|
456
|
-
product = db.query(Product).filter(Product.id == product_id, Product.is_active == True).first()
|
|
457
|
-
if product is None:
|
|
458
|
-
raise HTTPException(status_code=404, detail="Product not found")
|
|
459
|
-
|
|
460
|
-
return product
|
|
461
|
-
|
|
462
|
-
@app.post("/products", response_model=ProductSchema, tags=["Products"])
|
|
463
|
-
async def create_product(
|
|
464
|
-
product: ProductCreate,
|
|
465
|
-
current_user: User = Depends(get_current_user),
|
|
466
|
-
db: Session = Depends(get_db)
|
|
467
|
-
):
|
|
468
|
-
"""Create a new product (admin only)"""
|
|
469
|
-
if not current_user.is_admin:
|
|
470
|
-
raise HTTPException(status_code=403, detail="Not enough permissions")
|
|
471
|
-
|
|
472
|
-
db_product = Product(**product.dict())
|
|
473
|
-
db.add(db_product)
|
|
474
|
-
db.commit()
|
|
475
|
-
db.refresh(db_product)
|
|
476
|
-
|
|
477
|
-
return db_product
|
|
478
|
-
|
|
479
|
-
@app.put("/products/{product_id}", response_model=ProductSchema, tags=["Products"])
|
|
480
|
-
async def update_product(
|
|
481
|
-
product_id: int,
|
|
482
|
-
product_update: ProductUpdate,
|
|
483
|
-
current_user: User = Depends(get_current_user),
|
|
484
|
-
db: Session = Depends(get_db)
|
|
485
|
-
):
|
|
486
|
-
"""Update product (admin only)"""
|
|
487
|
-
if not current_user.is_admin:
|
|
488
|
-
raise HTTPException(status_code=403, detail="Not enough permissions")
|
|
489
|
-
|
|
490
|
-
product = db.query(Product).filter(Product.id == product_id).first()
|
|
491
|
-
if product is None:
|
|
492
|
-
raise HTTPException(status_code=404, detail="Product not found")
|
|
493
|
-
|
|
494
|
-
update_data = product_update.dict(exclude_unset=True)
|
|
495
|
-
for field, value in update_data.items():
|
|
496
|
-
setattr(product, field, value)
|
|
497
|
-
|
|
498
|
-
product.updated_at = datetime.utcnow()
|
|
499
|
-
db.commit()
|
|
500
|
-
db.refresh(product)
|
|
501
|
-
|
|
502
|
-
return product
|
|
503
|
-
|
|
504
|
-
# Order endpoints
|
|
505
|
-
@app.get("/orders", response_model=List[OrderSchema], tags=["Orders"])
|
|
506
|
-
async def get_orders(
|
|
507
|
-
skip: int = 0,
|
|
508
|
-
limit: int = 100,
|
|
509
|
-
status: Optional[OrderStatus] = None,
|
|
510
|
-
current_user: User = Depends(get_current_user),
|
|
511
|
-
db: Session = Depends(get_db)
|
|
512
|
-
):
|
|
513
|
-
"""Get user's orders"""
|
|
514
|
-
query = db.query(Order).filter(Order.user_id == current_user.id)
|
|
515
|
-
|
|
516
|
-
if status:
|
|
517
|
-
query = query.filter(Order.status == status)
|
|
518
|
-
|
|
519
|
-
orders = query.offset(skip).limit(limit).all()
|
|
520
|
-
return orders
|
|
521
|
-
|
|
522
|
-
@app.get("/orders/{order_id}", response_model=OrderSchema, tags=["Orders"])
|
|
523
|
-
async def get_order(
|
|
524
|
-
order_id: int,
|
|
525
|
-
current_user: User = Depends(get_current_user),
|
|
526
|
-
db: Session = Depends(get_db)
|
|
527
|
-
):
|
|
528
|
-
"""Get order by ID"""
|
|
529
|
-
order = db.query(Order).filter(Order.id == order_id).first()
|
|
530
|
-
if order is None:
|
|
531
|
-
raise HTTPException(status_code=404, detail="Order not found")
|
|
532
|
-
|
|
533
|
-
if order.user_id != current_user.id and not current_user.is_admin:
|
|
534
|
-
raise HTTPException(status_code=403, detail="Not enough permissions")
|
|
535
|
-
|
|
536
|
-
return order
|
|
537
|
-
|
|
538
|
-
@app.post("/orders", response_model=OrderSchema, tags=["Orders"])
|
|
539
|
-
async def create_order(
|
|
540
|
-
order: OrderCreate,
|
|
541
|
-
current_user: User = Depends(get_current_user),
|
|
542
|
-
db: Session = Depends(get_db)
|
|
543
|
-
):
|
|
544
|
-
"""Create a new order"""
|
|
545
|
-
total_amount = 0
|
|
546
|
-
order_items = []
|
|
547
|
-
|
|
548
|
-
# Calculate total and validate products
|
|
549
|
-
for item in order.items:
|
|
550
|
-
product = db.query(Product).filter(Product.id == item.product_id, Product.is_active == True).first()
|
|
551
|
-
if not product:
|
|
552
|
-
raise HTTPException(status_code=404, detail=f"Product {item.product_id} not found")
|
|
553
|
-
|
|
554
|
-
if product.stock_quantity < item.quantity:
|
|
555
|
-
raise HTTPException(status_code=400, detail=f"Insufficient stock for product {product.name}")
|
|
556
|
-
|
|
557
|
-
item_total = product.price * item.quantity
|
|
558
|
-
total_amount += item_total
|
|
559
|
-
|
|
560
|
-
order_items.append(OrderItem(
|
|
561
|
-
product_id=item.product_id,
|
|
562
|
-
quantity=item.quantity,
|
|
563
|
-
unit_price=product.price
|
|
564
|
-
))
|
|
565
|
-
|
|
566
|
-
# Create order
|
|
567
|
-
db_order = Order(
|
|
568
|
-
user_id=current_user.id,
|
|
569
|
-
total_amount=total_amount,
|
|
570
|
-
shipping_address=order.shipping_address,
|
|
571
|
-
items=order_items
|
|
572
|
-
)
|
|
573
|
-
|
|
574
|
-
# Update product stock
|
|
575
|
-
for item in order.items:
|
|
576
|
-
product = db.query(Product).filter(Product.id == item.product_id).first()
|
|
577
|
-
product.stock_quantity -= item.quantity
|
|
578
|
-
|
|
579
|
-
db.add(db_order)
|
|
580
|
-
db.commit()
|
|
581
|
-
db.refresh(db_order)
|
|
582
|
-
|
|
583
|
-
return db_order
|
|
584
|
-
|
|
585
|
-
@app.put("/orders/{order_id}", response_model=OrderSchema, tags=["Orders"])
|
|
586
|
-
async def update_order(
|
|
587
|
-
order_id: int,
|
|
588
|
-
order_update: OrderUpdate,
|
|
589
|
-
current_user: User = Depends(get_current_user),
|
|
590
|
-
db: Session = Depends(get_db)
|
|
591
|
-
):
|
|
592
|
-
"""Update order status (admin only for status changes)"""
|
|
593
|
-
order = db.query(Order).filter(Order.id == order_id).first()
|
|
594
|
-
if order is None:
|
|
595
|
-
raise HTTPException(status_code=404, detail="Order not found")
|
|
596
|
-
|
|
597
|
-
# Check permissions
|
|
598
|
-
if order.user_id != current_user.id and not current_user.is_admin:
|
|
599
|
-
raise HTTPException(status_code=403, detail="Not enough permissions")
|
|
600
|
-
|
|
601
|
-
# Only admins can change status
|
|
602
|
-
if order_update.status and not current_user.is_admin:
|
|
603
|
-
raise HTTPException(status_code=403, detail="Only admins can change order status")
|
|
604
|
-
|
|
605
|
-
update_data = order_update.dict(exclude_unset=True)
|
|
606
|
-
for field, value in update_data.items():
|
|
607
|
-
setattr(order, field, value)
|
|
608
|
-
|
|
609
|
-
order.updated_at = datetime.utcnow()
|
|
610
|
-
db.commit()
|
|
611
|
-
db.refresh(order)
|
|
612
|
-
|
|
613
|
-
return order
|
|
614
|
-
|
|
615
|
-
if __name__ == "__main__":
|
|
616
|
-
uvicorn.run(app, host="0.0.0.0", port=8000)
|
|
617
|
-
```
|
|
618
|
-
|
|
619
|
-
### 1.5 Create Database Configuration
|
|
620
|
-
|
|
621
|
-
Create `backend/database.py`:
|
|
622
|
-
|
|
623
|
-
```python
|
|
624
|
-
from sqlalchemy import create_engine
|
|
625
|
-
from sqlalchemy.orm import sessionmaker
|
|
626
|
-
|
|
627
|
-
DATABASE_URL = "sqlite:///./ecommerce.db"
|
|
628
|
-
|
|
629
|
-
engine = create_engine(
|
|
630
|
-
DATABASE_URL, connect_args={"check_same_thread": False}
|
|
631
|
-
)
|
|
632
|
-
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
|
|
633
|
-
|
|
634
|
-
def get_db():
|
|
635
|
-
db = SessionLocal()
|
|
636
|
-
try:
|
|
637
|
-
yield db
|
|
638
|
-
finally:
|
|
639
|
-
db.close()
|
|
640
|
-
```
|
|
641
|
-
|
|
642
|
-
### 1.6 Create Authentication System
|
|
643
|
-
|
|
644
|
-
Create `backend/auth.py`:
|
|
645
|
-
|
|
646
|
-
```python
|
|
647
|
-
from datetime import datetime, timedelta
|
|
648
|
-
from typing import Optional
|
|
649
|
-
from fastapi import Depends, HTTPException, status
|
|
650
|
-
from fastapi.security import OAuth2PasswordBearer
|
|
651
|
-
from jose import JWTError, jwt
|
|
652
|
-
from passlib.context import CryptContext
|
|
653
|
-
from sqlalchemy.orm import Session
|
|
654
|
-
|
|
655
|
-
from models import User
|
|
656
|
-
from schemas import TokenData
|
|
657
|
-
from database import get_db
|
|
658
|
-
|
|
659
|
-
# Configuration
|
|
660
|
-
SECRET_KEY = "your-secret-key-here" # In production, use environment variable
|
|
661
|
-
ALGORITHM = "HS256"
|
|
662
|
-
ACCESS_TOKEN_EXPIRE_MINUTES = 30
|
|
663
|
-
|
|
664
|
-
# Password hashing
|
|
665
|
-
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
|
|
666
|
-
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="auth/login")
|
|
667
|
-
|
|
668
|
-
def verify_password(plain_password, hashed_password):
|
|
669
|
-
return pwd_context.verify(plain_password, hashed_password)
|
|
670
|
-
|
|
671
|
-
def get_password_hash(password):
|
|
672
|
-
return pwd_context.hash(password)
|
|
673
|
-
|
|
674
|
-
def authenticate_user(db: Session, username: str, password: str):
|
|
675
|
-
user = db.query(User).filter(
|
|
676
|
-
(User.username == username) | (User.email == username)
|
|
677
|
-
).first()
|
|
678
|
-
|
|
679
|
-
if not user or not verify_password(password, user.hashed_password):
|
|
680
|
-
return False
|
|
681
|
-
|
|
682
|
-
return user
|
|
683
|
-
|
|
684
|
-
def create_access_token(data: dict, expires_delta: Optional[timedelta] = None):
|
|
685
|
-
to_encode = data.copy()
|
|
686
|
-
if expires_delta:
|
|
687
|
-
expire = datetime.utcnow() + expires_delta
|
|
688
|
-
else:
|
|
689
|
-
expire = datetime.utcnow() + timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
|
|
690
|
-
|
|
691
|
-
to_encode.update({"exp": expire})
|
|
692
|
-
encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
|
|
693
|
-
return encoded_jwt
|
|
694
|
-
|
|
695
|
-
async def get_current_user(token: str = Depends(oauth2_scheme), db: Session = Depends(get_db)):
|
|
696
|
-
credentials_exception = HTTPException(
|
|
697
|
-
status_code=status.HTTP_401_UNAUTHORIZED,
|
|
698
|
-
detail="Could not validate credentials",
|
|
699
|
-
headers={"WWW-Authenticate": "Bearer"},
|
|
700
|
-
)
|
|
701
|
-
|
|
702
|
-
try:
|
|
703
|
-
payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
|
|
704
|
-
username: str = payload.get("sub")
|
|
705
|
-
if username is None:
|
|
706
|
-
raise credentials_exception
|
|
707
|
-
token_data = TokenData(username=username)
|
|
708
|
-
except JWTError:
|
|
709
|
-
raise credentials_exception
|
|
710
|
-
|
|
711
|
-
user = db.query(User).filter(User.username == token_data.username).first()
|
|
712
|
-
if user is None:
|
|
713
|
-
raise credentials_exception
|
|
714
|
-
|
|
715
|
-
return user
|
|
716
|
-
```
|
|
717
|
-
|
|
718
|
-
### 1.7 Run the FastAPI Server
|
|
719
|
-
|
|
720
|
-
```bash
|
|
721
|
-
cd backend
|
|
722
|
-
uvicorn main:app --reload
|
|
723
|
-
```
|
|
724
|
-
|
|
725
|
-
Your API will be available at http://localhost:8000, with interactive docs at http://localhost:8000/docs.
|
|
726
|
-
|
|
727
|
-
## Part 2: Generate TypeScript Types with Schemantic
|
|
728
|
-
|
|
729
|
-
### 2.1 Install Schemantic
|
|
730
|
-
|
|
731
|
-
```bash
|
|
732
|
-
# Go back to project root
|
|
733
|
-
cd ..
|
|
734
|
-
|
|
735
|
-
# Install Schemantic
|
|
736
|
-
npm install -g schemantic
|
|
737
|
-
```
|
|
738
|
-
|
|
739
|
-
### 2.2 Generate Types and API Client
|
|
740
|
-
|
|
741
|
-
```bash
|
|
742
|
-
# Create output directory
|
|
743
|
-
mkdir frontend/src/generated
|
|
744
|
-
|
|
745
|
-
# Generate types and API client with React hooks
|
|
746
|
-
npx schemantic generate \
|
|
747
|
-
--url http://localhost:8000/openapi.json \
|
|
748
|
-
--output ./frontend/src/generated \
|
|
749
|
-
--hooks \
|
|
750
|
-
--naming camelCase \
|
|
751
|
-
# No prefix by default; to add a prefix use --prefix "MyAPI"
|
|
752
|
-
|
|
753
|
-
# Generated files:
|
|
754
|
-
# - types.ts (TypeScript interfaces)
|
|
755
|
-
# - api-client.ts (API client class)
|
|
756
|
-
# - hooks.ts (React hooks)
|
|
757
|
-
# - index.ts (barrel exports)
|
|
758
|
-
```
|
|
759
|
-
|
|
760
|
-
### 2.3 Verify Generated Types
|
|
761
|
-
|
|
762
|
-
Check the generated files:
|
|
763
|
-
|
|
764
|
-
```typescript
|
|
765
|
-
// frontend/src/generated/types.ts
|
|
766
|
-
export interface APIUser {
|
|
767
|
-
id: number;
|
|
768
|
-
email: string;
|
|
769
|
-
username: string;
|
|
770
|
-
fullName?: string;
|
|
771
|
-
isActive: boolean;
|
|
772
|
-
isAdmin: boolean;
|
|
773
|
-
createdAt: string;
|
|
774
|
-
updatedAt: string;
|
|
775
|
-
}
|
|
776
|
-
|
|
777
|
-
export interface APIProduct {
|
|
778
|
-
id: number;
|
|
779
|
-
name: string;
|
|
780
|
-
description?: string;
|
|
781
|
-
price: number;
|
|
782
|
-
category: APIProductCategory;
|
|
783
|
-
stockQuantity: number;
|
|
784
|
-
imageUrl?: string;
|
|
785
|
-
isActive: boolean;
|
|
786
|
-
createdAt: string;
|
|
787
|
-
updatedAt: string;
|
|
788
|
-
}
|
|
789
|
-
|
|
790
|
-
export enum APIProductCategory {
|
|
791
|
-
ELECTRONICS = "electronics",
|
|
792
|
-
CLOTHING = "clothing",
|
|
793
|
-
BOOKS = "books",
|
|
794
|
-
HOME = "home",
|
|
795
|
-
SPORTS = "sports",
|
|
796
|
-
}
|
|
797
|
-
|
|
798
|
-
export enum APIOrderStatus {
|
|
799
|
-
PENDING = "pending",
|
|
800
|
-
PROCESSING = "processing",
|
|
801
|
-
SHIPPED = "shipped",
|
|
802
|
-
DELIVERED = "delivered",
|
|
803
|
-
CANCELLED = "cancelled",
|
|
804
|
-
}
|
|
805
|
-
|
|
806
|
-
// ... many more types
|
|
807
|
-
```
|
|
808
|
-
|
|
809
|
-
## Part 3: React Frontend Setup
|
|
810
|
-
|
|
811
|
-
### 3.1 Create React Application
|
|
812
|
-
|
|
813
|
-
```bash
|
|
814
|
-
# Create React app with TypeScript
|
|
815
|
-
npx create-react-app frontend --template typescript
|
|
816
|
-
cd frontend
|
|
817
|
-
|
|
818
|
-
# Install additional dependencies
|
|
819
|
-
npm install @tanstack/react-query axios react-router-dom @types/react-router-dom
|
|
820
|
-
npm install @mui/material @emotion/react @emotion/styled @mui/icons-material
|
|
821
|
-
```
|
|
822
|
-
|
|
823
|
-
### 3.2 Configure API Client
|
|
824
|
-
|
|
825
|
-
Create `frontend/src/api/client.ts`:
|
|
826
|
-
|
|
827
|
-
```typescript
|
|
828
|
-
import { ECommerceApiClient } from "../generated";
|
|
829
|
-
|
|
830
|
-
// Create API client instance
|
|
831
|
-
export const apiClient = new ECommerceApiClient({
|
|
832
|
-
baseUrl: "http://localhost:8000",
|
|
833
|
-
headers: {
|
|
834
|
-
"Content-Type": "application/json",
|
|
835
|
-
},
|
|
836
|
-
});
|
|
837
|
-
|
|
838
|
-
// Add authentication token to requests
|
|
839
|
-
export const setAuthToken = (token: string) => {
|
|
840
|
-
apiClient.setDefaultHeader("Authorization", `Bearer ${token}`);
|
|
841
|
-
};
|
|
842
|
-
|
|
843
|
-
export const clearAuthToken = () => {
|
|
844
|
-
apiClient.removeDefaultHeader("Authorization");
|
|
845
|
-
};
|
|
846
|
-
```
|
|
847
|
-
|
|
848
|
-
### 3.3 Setup React Query and Hooks
|
|
849
|
-
|
|
850
|
-
Create `frontend/src/hooks/api.ts`:
|
|
851
|
-
|
|
852
|
-
```typescript
|
|
853
|
-
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
|
|
854
|
-
import { apiClient } from "../api/client";
|
|
855
|
-
import { createApiHooks } from "../generated";
|
|
856
|
-
|
|
857
|
-
// Create typed hooks
|
|
858
|
-
export const apiHooks = createApiHooks(apiClient);
|
|
859
|
-
|
|
860
|
-
// Export specific hooks for easy use
|
|
861
|
-
export const {
|
|
862
|
-
// Authentication hooks
|
|
863
|
-
useAuthRegisterMutation,
|
|
864
|
-
useAuthLoginMutation,
|
|
865
|
-
useAuthMeQuery,
|
|
866
|
-
|
|
867
|
-
// User hooks
|
|
868
|
-
useUsersQuery,
|
|
869
|
-
useUsersUserIdQuery,
|
|
870
|
-
useUsersUserIdMutation,
|
|
871
|
-
|
|
872
|
-
// Product hooks
|
|
873
|
-
useProductsQuery,
|
|
874
|
-
useProductsProductIdQuery,
|
|
875
|
-
useProductsMutation,
|
|
876
|
-
useProductsProductIdMutation,
|
|
877
|
-
|
|
878
|
-
// Order hooks
|
|
879
|
-
useOrdersQuery,
|
|
880
|
-
useOrdersOrderIdQuery,
|
|
881
|
-
useOrdersMutation,
|
|
882
|
-
useOrdersOrderIdMutation,
|
|
883
|
-
} = apiHooks;
|
|
884
|
-
|
|
885
|
-
// Custom hooks for common patterns
|
|
886
|
-
export const useAuth = () => {
|
|
887
|
-
const queryClient = useQueryClient();
|
|
888
|
-
|
|
889
|
-
const loginMutation = useAuthLoginMutation({
|
|
890
|
-
onSuccess: (data) => {
|
|
891
|
-
localStorage.setItem("token", data.accessToken);
|
|
892
|
-
queryClient.invalidateQueries(["auth", "me"]);
|
|
893
|
-
},
|
|
894
|
-
});
|
|
895
|
-
|
|
896
|
-
const logout = () => {
|
|
897
|
-
localStorage.removeItem("token");
|
|
898
|
-
queryClient.clear();
|
|
899
|
-
};
|
|
900
|
-
|
|
901
|
-
return {
|
|
902
|
-
login: loginMutation.mutate,
|
|
903
|
-
logout,
|
|
904
|
-
isLoading: loginMutation.isLoading,
|
|
905
|
-
error: loginMutation.error,
|
|
906
|
-
};
|
|
907
|
-
};
|
|
908
|
-
|
|
909
|
-
export const useProducts = (filters?: {
|
|
910
|
-
category?: string;
|
|
911
|
-
search?: string;
|
|
912
|
-
page?: number;
|
|
913
|
-
limit?: number;
|
|
914
|
-
}) => {
|
|
915
|
-
return useProductsQuery({
|
|
916
|
-
query: {
|
|
917
|
-
skip: ((filters?.page || 1) - 1) * (filters?.limit || 20),
|
|
918
|
-
limit: filters?.limit || 20,
|
|
919
|
-
category: filters?.category,
|
|
920
|
-
search: filters?.search,
|
|
921
|
-
},
|
|
922
|
-
});
|
|
923
|
-
};
|
|
924
|
-
|
|
925
|
-
export const useUserOrders = (userId?: number) => {
|
|
926
|
-
return useOrdersQuery({
|
|
927
|
-
enabled: !!userId,
|
|
928
|
-
});
|
|
929
|
-
};
|
|
930
|
-
```
|
|
931
|
-
|
|
932
|
-
### 3.4 Create React Components
|
|
933
|
-
|
|
934
|
-
Create `frontend/src/components/ProductList.tsx`:
|
|
935
|
-
|
|
936
|
-
```typescript
|
|
937
|
-
import React, { useState } from "react";
|
|
938
|
-
import {
|
|
939
|
-
Grid,
|
|
940
|
-
Card,
|
|
941
|
-
CardContent,
|
|
942
|
-
CardMedia,
|
|
943
|
-
Typography,
|
|
944
|
-
Button,
|
|
945
|
-
TextField,
|
|
946
|
-
Select,
|
|
947
|
-
MenuItem,
|
|
948
|
-
FormControl,
|
|
949
|
-
InputLabel,
|
|
950
|
-
Box,
|
|
951
|
-
CircularProgress,
|
|
952
|
-
Alert,
|
|
953
|
-
} from "@mui/material";
|
|
954
|
-
import { APIProduct, APIProductCategory } from "../generated";
|
|
955
|
-
import { useProducts } from "../hooks/api";
|
|
956
|
-
|
|
957
|
-
interface ProductListProps {
|
|
958
|
-
onAddToCart?: (product: APIProduct) => void;
|
|
959
|
-
}
|
|
960
|
-
|
|
961
|
-
export const ProductList: React.FC<ProductListProps> = ({ onAddToCart }) => {
|
|
962
|
-
const [search, setSearch] = useState("");
|
|
963
|
-
const [category, setCategory] = useState<APIProductCategory | "">("");
|
|
964
|
-
const [page, setPage] = useState(1);
|
|
965
|
-
|
|
966
|
-
const {
|
|
967
|
-
data: products,
|
|
968
|
-
isLoading,
|
|
969
|
-
error,
|
|
970
|
-
} = useProducts({
|
|
971
|
-
search: search || undefined,
|
|
972
|
-
category: category || undefined,
|
|
973
|
-
page,
|
|
974
|
-
limit: 12,
|
|
975
|
-
});
|
|
976
|
-
|
|
977
|
-
if (isLoading) {
|
|
978
|
-
return (
|
|
979
|
-
<Box display="flex" justifyContent="center" p={4}>
|
|
980
|
-
<CircularProgress />
|
|
981
|
-
</Box>
|
|
982
|
-
);
|
|
983
|
-
}
|
|
984
|
-
|
|
985
|
-
if (error) {
|
|
986
|
-
return (
|
|
987
|
-
<Alert severity="error">Failed to load products: {error.message}</Alert>
|
|
988
|
-
);
|
|
989
|
-
}
|
|
990
|
-
|
|
991
|
-
return (
|
|
992
|
-
<Box>
|
|
993
|
-
{/* Filters */}
|
|
994
|
-
<Box display="flex" gap={2} mb={3}>
|
|
995
|
-
<TextField
|
|
996
|
-
label="Search products"
|
|
997
|
-
value={search}
|
|
998
|
-
onChange={(e) => setSearch(e.target.value)}
|
|
999
|
-
variant="outlined"
|
|
1000
|
-
size="small"
|
|
1001
|
-
sx={{ minWidth: 200 }}
|
|
1002
|
-
/>
|
|
1003
|
-
|
|
1004
|
-
<FormControl size="small" sx={{ minWidth: 150 }}>
|
|
1005
|
-
<InputLabel>Category</InputLabel>
|
|
1006
|
-
<Select
|
|
1007
|
-
value={category}
|
|
1008
|
-
onChange={(e) => setCategory(e.target.value as APIProductCategory)}
|
|
1009
|
-
label="Category"
|
|
1010
|
-
>
|
|
1011
|
-
<MenuItem value="">All Categories</MenuItem>
|
|
1012
|
-
{Object.values(APIProductCategory).map((cat) => (
|
|
1013
|
-
<MenuItem key={cat} value={cat}>
|
|
1014
|
-
{cat.charAt(0).toUpperCase() + cat.slice(1)}
|
|
1015
|
-
</MenuItem>
|
|
1016
|
-
))}
|
|
1017
|
-
</Select>
|
|
1018
|
-
</FormControl>
|
|
1019
|
-
</Box>
|
|
1020
|
-
|
|
1021
|
-
{/* Product Grid */}
|
|
1022
|
-
<Grid container spacing={3}>
|
|
1023
|
-
{products?.map((product) => (
|
|
1024
|
-
<Grid item xs={12} sm={6} md={4} lg={3} key={product.id}>
|
|
1025
|
-
<Card>
|
|
1026
|
-
{product.imageUrl && (
|
|
1027
|
-
<CardMedia
|
|
1028
|
-
component="img"
|
|
1029
|
-
height="200"
|
|
1030
|
-
image={product.imageUrl}
|
|
1031
|
-
alt={product.name}
|
|
1032
|
-
/>
|
|
1033
|
-
)}
|
|
1034
|
-
|
|
1035
|
-
<CardContent>
|
|
1036
|
-
<Typography variant="h6" component="h2" gutterBottom>
|
|
1037
|
-
{product.name}
|
|
1038
|
-
</Typography>
|
|
1039
|
-
|
|
1040
|
-
<Typography variant="body2" color="text.secondary" paragraph>
|
|
1041
|
-
{product.description}
|
|
1042
|
-
</Typography>
|
|
1043
|
-
|
|
1044
|
-
<Typography variant="h6" color="primary" gutterBottom>
|
|
1045
|
-
${product.price.toFixed(2)}
|
|
1046
|
-
</Typography>
|
|
1047
|
-
|
|
1048
|
-
<Typography variant="body2" color="text.secondary">
|
|
1049
|
-
Stock: {product.stockQuantity}
|
|
1050
|
-
</Typography>
|
|
1051
|
-
|
|
1052
|
-
<Button
|
|
1053
|
-
variant="contained"
|
|
1054
|
-
fullWidth
|
|
1055
|
-
sx={{ mt: 2 }}
|
|
1056
|
-
disabled={product.stockQuantity === 0}
|
|
1057
|
-
onClick={() => onAddToCart?.(product)}
|
|
1058
|
-
>
|
|
1059
|
-
{product.stockQuantity === 0 ? "Out of Stock" : "Add to Cart"}
|
|
1060
|
-
</Button>
|
|
1061
|
-
</CardContent>
|
|
1062
|
-
</Card>
|
|
1063
|
-
</Grid>
|
|
1064
|
-
))}
|
|
1065
|
-
</Grid>
|
|
1066
|
-
</Box>
|
|
1067
|
-
);
|
|
1068
|
-
};
|
|
1069
|
-
```
|
|
1070
|
-
|
|
1071
|
-
Create `frontend/src/components/UserProfile.tsx`:
|
|
1072
|
-
|
|
1073
|
-
```typescript
|
|
1074
|
-
import React from "react";
|
|
1075
|
-
import {
|
|
1076
|
-
Card,
|
|
1077
|
-
CardContent,
|
|
1078
|
-
Typography,
|
|
1079
|
-
Button,
|
|
1080
|
-
Box,
|
|
1081
|
-
Chip,
|
|
1082
|
-
CircularProgress,
|
|
1083
|
-
Alert,
|
|
1084
|
-
} from "@mui/material";
|
|
1085
|
-
import { useAuthMeQuery } from "../hooks/api";
|
|
1086
|
-
|
|
1087
|
-
export const UserProfile: React.FC = () => {
|
|
1088
|
-
const { data: user, isLoading, error } = useAuthMeQuery();
|
|
1089
|
-
|
|
1090
|
-
if (isLoading) {
|
|
1091
|
-
return (
|
|
1092
|
-
<Box display="flex" justifyContent="center" p={4}>
|
|
1093
|
-
<CircularProgress />
|
|
1094
|
-
</Box>
|
|
1095
|
-
);
|
|
1096
|
-
}
|
|
1097
|
-
|
|
1098
|
-
if (error) {
|
|
1099
|
-
return (
|
|
1100
|
-
<Alert severity="error">
|
|
1101
|
-
Failed to load user profile: {error.message}
|
|
1102
|
-
</Alert>
|
|
1103
|
-
);
|
|
1104
|
-
}
|
|
1105
|
-
|
|
1106
|
-
if (!user) {
|
|
1107
|
-
return <Alert severity="info">Please log in to view your profile.</Alert>;
|
|
1108
|
-
}
|
|
1109
|
-
|
|
1110
|
-
return (
|
|
1111
|
-
<Card>
|
|
1112
|
-
<CardContent>
|
|
1113
|
-
<Typography variant="h5" component="h1" gutterBottom>
|
|
1114
|
-
User Profile
|
|
1115
|
-
</Typography>
|
|
1116
|
-
|
|
1117
|
-
<Box display="flex" flexDirection="column" gap={2}>
|
|
1118
|
-
<Box>
|
|
1119
|
-
<Typography variant="subtitle1" fontWeight="bold">
|
|
1120
|
-
Username:
|
|
1121
|
-
</Typography>
|
|
1122
|
-
<Typography>{user.username}</Typography>
|
|
1123
|
-
</Box>
|
|
1124
|
-
|
|
1125
|
-
<Box>
|
|
1126
|
-
<Typography variant="subtitle1" fontWeight="bold">
|
|
1127
|
-
Email:
|
|
1128
|
-
</Typography>
|
|
1129
|
-
<Typography>{user.email}</Typography>
|
|
1130
|
-
</Box>
|
|
1131
|
-
|
|
1132
|
-
{user.fullName && (
|
|
1133
|
-
<Box>
|
|
1134
|
-
<Typography variant="subtitle1" fontWeight="bold">
|
|
1135
|
-
Full Name:
|
|
1136
|
-
</Typography>
|
|
1137
|
-
<Typography>{user.fullName}</Typography>
|
|
1138
|
-
</Box>
|
|
1139
|
-
)}
|
|
1140
|
-
|
|
1141
|
-
<Box>
|
|
1142
|
-
<Typography variant="subtitle1" fontWeight="bold">
|
|
1143
|
-
Status:
|
|
1144
|
-
</Typography>
|
|
1145
|
-
<Box display="flex" gap={1}>
|
|
1146
|
-
<Chip
|
|
1147
|
-
label={user.isActive ? "Active" : "Inactive"}
|
|
1148
|
-
color={user.isActive ? "success" : "error"}
|
|
1149
|
-
size="small"
|
|
1150
|
-
/>
|
|
1151
|
-
{user.isAdmin && (
|
|
1152
|
-
<Chip label="Admin" color="primary" size="small" />
|
|
1153
|
-
)}
|
|
1154
|
-
</Box>
|
|
1155
|
-
</Box>
|
|
1156
|
-
|
|
1157
|
-
<Box>
|
|
1158
|
-
<Typography variant="subtitle1" fontWeight="bold">
|
|
1159
|
-
Member Since:
|
|
1160
|
-
</Typography>
|
|
1161
|
-
<Typography>
|
|
1162
|
-
{new Date(user.createdAt).toLocaleDateString()}
|
|
1163
|
-
</Typography>
|
|
1164
|
-
</Box>
|
|
1165
|
-
</Box>
|
|
1166
|
-
|
|
1167
|
-
<Button
|
|
1168
|
-
variant="outlined"
|
|
1169
|
-
sx={{ mt: 3 }}
|
|
1170
|
-
onClick={() => {
|
|
1171
|
-
// Handle edit profile
|
|
1172
|
-
}}
|
|
1173
|
-
>
|
|
1174
|
-
Edit Profile
|
|
1175
|
-
</Button>
|
|
1176
|
-
</CardContent>
|
|
1177
|
-
</Card>
|
|
1178
|
-
);
|
|
1179
|
-
};
|
|
1180
|
-
```
|
|
1181
|
-
|
|
1182
|
-
Create `frontend/src/components/OrderHistory.tsx`:
|
|
1183
|
-
|
|
1184
|
-
```typescript
|
|
1185
|
-
import React from "react";
|
|
1186
|
-
import {
|
|
1187
|
-
Table,
|
|
1188
|
-
TableBody,
|
|
1189
|
-
TableCell,
|
|
1190
|
-
TableContainer,
|
|
1191
|
-
TableHead,
|
|
1192
|
-
TableRow,
|
|
1193
|
-
Paper,
|
|
1194
|
-
Chip,
|
|
1195
|
-
Typography,
|
|
1196
|
-
Box,
|
|
1197
|
-
CircularProgress,
|
|
1198
|
-
Alert,
|
|
1199
|
-
} from "@mui/material";
|
|
1200
|
-
import { APIOrderStatus } from "../generated";
|
|
1201
|
-
import { useOrdersQuery } from "../hooks/api";
|
|
1202
|
-
|
|
1203
|
-
const getStatusColor = (status: APIOrderStatus) => {
|
|
1204
|
-
switch (status) {
|
|
1205
|
-
case APIOrderStatus.PENDING:
|
|
1206
|
-
return "warning";
|
|
1207
|
-
case APIOrderStatus.PROCESSING:
|
|
1208
|
-
return "info";
|
|
1209
|
-
case APIOrderStatus.SHIPPED:
|
|
1210
|
-
return "primary";
|
|
1211
|
-
case APIOrderStatus.DELIVERED:
|
|
1212
|
-
return "success";
|
|
1213
|
-
case APIOrderStatus.CANCELLED:
|
|
1214
|
-
return "error";
|
|
1215
|
-
default:
|
|
1216
|
-
return "default";
|
|
1217
|
-
}
|
|
1218
|
-
};
|
|
1219
|
-
|
|
1220
|
-
export const OrderHistory: React.FC = () => {
|
|
1221
|
-
const { data: orders, isLoading, error } = useOrdersQuery();
|
|
1222
|
-
|
|
1223
|
-
if (isLoading) {
|
|
1224
|
-
return (
|
|
1225
|
-
<Box display="flex" justifyContent="center" p={4}>
|
|
1226
|
-
<CircularProgress />
|
|
1227
|
-
</Box>
|
|
1228
|
-
);
|
|
1229
|
-
}
|
|
1230
|
-
|
|
1231
|
-
if (error) {
|
|
1232
|
-
return (
|
|
1233
|
-
<Alert severity="error">Failed to load orders: {error.message}</Alert>
|
|
1234
|
-
);
|
|
1235
|
-
}
|
|
1236
|
-
|
|
1237
|
-
if (!orders || orders.length === 0) {
|
|
1238
|
-
return <Alert severity="info">You haven't placed any orders yet.</Alert>;
|
|
1239
|
-
}
|
|
1240
|
-
|
|
1241
|
-
return (
|
|
1242
|
-
<Box>
|
|
1243
|
-
<Typography variant="h5" component="h1" gutterBottom>
|
|
1244
|
-
Order History
|
|
1245
|
-
</Typography>
|
|
1246
|
-
|
|
1247
|
-
<TableContainer component={Paper}>
|
|
1248
|
-
<Table>
|
|
1249
|
-
<TableHead>
|
|
1250
|
-
<TableRow>
|
|
1251
|
-
<TableCell>Order ID</TableCell>
|
|
1252
|
-
<TableCell>Date</TableCell>
|
|
1253
|
-
<TableCell>Items</TableCell>
|
|
1254
|
-
<TableCell>Total</TableCell>
|
|
1255
|
-
<TableCell>Status</TableCell>
|
|
1256
|
-
</TableRow>
|
|
1257
|
-
</TableHead>
|
|
1258
|
-
<TableBody>
|
|
1259
|
-
{orders.map((order) => (
|
|
1260
|
-
<TableRow key={order.id}>
|
|
1261
|
-
<TableCell>#{order.id}</TableCell>
|
|
1262
|
-
<TableCell>
|
|
1263
|
-
{new Date(order.createdAt).toLocaleDateString()}
|
|
1264
|
-
</TableCell>
|
|
1265
|
-
<TableCell>
|
|
1266
|
-
{order.items.length} item{order.items.length !== 1 ? "s" : ""}
|
|
1267
|
-
</TableCell>
|
|
1268
|
-
<TableCell>${order.totalAmount.toFixed(2)}</TableCell>
|
|
1269
|
-
<TableCell>
|
|
1270
|
-
<Chip
|
|
1271
|
-
label={order.status.toUpperCase()}
|
|
1272
|
-
color={getStatusColor(order.status)}
|
|
1273
|
-
size="small"
|
|
1274
|
-
/>
|
|
1275
|
-
</TableCell>
|
|
1276
|
-
</TableRow>
|
|
1277
|
-
))}
|
|
1278
|
-
</TableBody>
|
|
1279
|
-
</Table>
|
|
1280
|
-
</TableContainer>
|
|
1281
|
-
</Box>
|
|
1282
|
-
);
|
|
1283
|
-
};
|
|
1284
|
-
```
|
|
1285
|
-
|
|
1286
|
-
### 3.5 Setup App with React Query
|
|
1287
|
-
|
|
1288
|
-
Update `frontend/src/App.tsx`:
|
|
1289
|
-
|
|
1290
|
-
```typescript
|
|
1291
|
-
import React from "react";
|
|
1292
|
-
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
|
1293
|
-
import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
|
|
1294
|
-
import { ThemeProvider, createTheme, CssBaseline } from "@mui/material";
|
|
1295
|
-
import { BrowserRouter as Router, Routes, Route } from "react-router-dom";
|
|
1296
|
-
import { ProductList } from "./components/ProductList";
|
|
1297
|
-
import { UserProfile } from "./components/UserProfile";
|
|
1298
|
-
import { OrderHistory } from "./components/OrderHistory";
|
|
1299
|
-
|
|
1300
|
-
// Create React Query client
|
|
1301
|
-
const queryClient = new QueryClient({
|
|
1302
|
-
defaultOptions: {
|
|
1303
|
-
queries: {
|
|
1304
|
-
retry: 1,
|
|
1305
|
-
refetchOnWindowFocus: false,
|
|
1306
|
-
},
|
|
1307
|
-
},
|
|
1308
|
-
});
|
|
1309
|
-
|
|
1310
|
-
// Create Material-UI theme
|
|
1311
|
-
const theme = createTheme({
|
|
1312
|
-
palette: {
|
|
1313
|
-
mode: "light",
|
|
1314
|
-
primary: {
|
|
1315
|
-
main: "#1976d2",
|
|
1316
|
-
},
|
|
1317
|
-
secondary: {
|
|
1318
|
-
main: "#dc004e",
|
|
1319
|
-
},
|
|
1320
|
-
},
|
|
1321
|
-
});
|
|
1322
|
-
|
|
1323
|
-
function App() {
|
|
1324
|
-
return (
|
|
1325
|
-
<QueryClientProvider client={queryClient}>
|
|
1326
|
-
<ThemeProvider theme={theme}>
|
|
1327
|
-
<CssBaseline />
|
|
1328
|
-
<Router>
|
|
1329
|
-
<div className="App">
|
|
1330
|
-
<Routes>
|
|
1331
|
-
<Route path="/" element={<ProductList />} />
|
|
1332
|
-
<Route path="/profile" element={<UserProfile />} />
|
|
1333
|
-
<Route path="/orders" element={<OrderHistory />} />
|
|
1334
|
-
</Routes>
|
|
1335
|
-
</div>
|
|
1336
|
-
</Router>
|
|
1337
|
-
<ReactQueryDevtools initialIsOpen={false} />
|
|
1338
|
-
</ThemeProvider>
|
|
1339
|
-
</QueryClientProvider>
|
|
1340
|
-
);
|
|
1341
|
-
}
|
|
1342
|
-
|
|
1343
|
-
export default App;
|
|
1344
|
-
```
|
|
1345
|
-
|
|
1346
|
-
## Part 4: Run the Complete Application
|
|
1347
|
-
|
|
1348
|
-
### 4.1 Start the Backend
|
|
1349
|
-
|
|
1350
|
-
```bash
|
|
1351
|
-
cd backend
|
|
1352
|
-
source venv/bin/activate # On Windows: venv\Scripts\activate
|
|
1353
|
-
uvicorn main:app --reload
|
|
1354
|
-
```
|
|
1355
|
-
|
|
1356
|
-
### 4.2 Start the Frontend
|
|
1357
|
-
|
|
1358
|
-
```bash
|
|
1359
|
-
cd frontend
|
|
1360
|
-
npm start
|
|
1361
|
-
```
|
|
1362
|
-
|
|
1363
|
-
### 4.3 Test the Integration
|
|
1364
|
-
|
|
1365
|
-
1. **Visit the API docs**: http://localhost:8000/docs
|
|
1366
|
-
2. **Visit the React app**: http://localhost:3000
|
|
1367
|
-
3. **Test the type safety**: Try modifying API responses and see TypeScript errors in the frontend
|
|
1368
|
-
|
|
1369
|
-
## Part 5: Advanced Features
|
|
1370
|
-
|
|
1371
|
-
### 5.1 Regenerate Types on API Changes
|
|
1372
|
-
|
|
1373
|
-
Create `scripts/update-types.sh`:
|
|
1374
|
-
|
|
1375
|
-
```bash
|
|
1376
|
-
#!/bin/bash
|
|
1377
|
-
echo "🔄 Updating TypeScript types from API..."
|
|
1378
|
-
|
|
1379
|
-
# Check if API is running
|
|
1380
|
-
if ! curl -s http://localhost:8000/health > /dev/null; then
|
|
1381
|
-
echo "❌ API server is not running. Please start it first."
|
|
1382
|
-
exit 1
|
|
1383
|
-
fi
|
|
1384
|
-
|
|
1385
|
-
# Generate new types
|
|
1386
|
-
npx schemantic generate \
|
|
1387
|
-
--url http://localhost:8000/openapi.json \
|
|
1388
|
-
--output ./frontend/src/generated \
|
|
1389
|
-
--hooks \
|
|
1390
|
-
--naming camelCase \
|
|
1391
|
-
# No prefix by default; to add a prefix use --prefix "MyAPI"
|
|
1392
|
-
|
|
1393
|
-
echo "✅ Types updated successfully!"
|
|
1394
|
-
echo "🏗️ Building frontend to verify types..."
|
|
1395
|
-
|
|
1396
|
-
cd frontend
|
|
1397
|
-
npm run build
|
|
1398
|
-
|
|
1399
|
-
if [ $? -eq 0 ]; then
|
|
1400
|
-
echo "✅ Frontend builds successfully with new types!"
|
|
1401
|
-
else
|
|
1402
|
-
echo "❌ Frontend build failed. Please check for type errors."
|
|
1403
|
-
exit 1
|
|
1404
|
-
fi
|
|
1405
|
-
```
|
|
1406
|
-
|
|
1407
|
-
### 5.2 Add Type-Safe Error Handling
|
|
1408
|
-
|
|
1409
|
-
Create `frontend/src/utils/errors.ts`:
|
|
1410
|
-
|
|
1411
|
-
```typescript
|
|
1412
|
-
import { APIResponse } from "../generated";
|
|
1413
|
-
|
|
1414
|
-
export class APIError extends Error {
|
|
1415
|
-
constructor(message: string, public status: number, public response?: any) {
|
|
1416
|
-
super(message);
|
|
1417
|
-
this.name = "APIError";
|
|
1418
|
-
}
|
|
1419
|
-
}
|
|
1420
|
-
|
|
1421
|
-
export const handleAPIError = (error: any): string => {
|
|
1422
|
-
if (error instanceof APIError) {
|
|
1423
|
-
return `API Error (${error.status}): ${error.message}`;
|
|
1424
|
-
}
|
|
1425
|
-
|
|
1426
|
-
if (error.response?.data?.message) {
|
|
1427
|
-
return error.response.data.message;
|
|
1428
|
-
}
|
|
1429
|
-
|
|
1430
|
-
return error.message || "An unexpected error occurred";
|
|
1431
|
-
};
|
|
1432
|
-
```
|
|
1433
|
-
|
|
1434
|
-
### 5.3 Add Real-time Updates with WebSockets
|
|
1435
|
-
|
|
1436
|
-
The generated types can also be used with WebSocket events:
|
|
1437
|
-
|
|
1438
|
-
```typescript
|
|
1439
|
-
// frontend/src/hooks/useWebSocket.ts
|
|
1440
|
-
import { useEffect, useState } from "react";
|
|
1441
|
-
import { APIOrder } from "../generated";
|
|
1442
|
-
|
|
1443
|
-
export const useOrderUpdates = (userId: number) => {
|
|
1444
|
-
const [orders, setOrders] = useState<APIOrder[]>([]);
|
|
1445
|
-
|
|
1446
|
-
useEffect(() => {
|
|
1447
|
-
const ws = new WebSocket(`ws://localhost:8000/ws/orders/${userId}`);
|
|
1448
|
-
|
|
1449
|
-
ws.onmessage = (event) => {
|
|
1450
|
-
const data = JSON.parse(event.data);
|
|
1451
|
-
|
|
1452
|
-
// Type-safe handling of WebSocket messages
|
|
1453
|
-
if (data.type === "order_update") {
|
|
1454
|
-
const updatedOrder: APIOrder = data.order;
|
|
1455
|
-
setOrders((prev) =>
|
|
1456
|
-
prev.map((order) =>
|
|
1457
|
-
order.id === updatedOrder.id ? updatedOrder : order
|
|
1458
|
-
)
|
|
1459
|
-
);
|
|
1460
|
-
}
|
|
1461
|
-
};
|
|
1462
|
-
|
|
1463
|
-
return () => ws.close();
|
|
1464
|
-
}, [userId]);
|
|
1465
|
-
|
|
1466
|
-
return orders;
|
|
1467
|
-
};
|
|
1468
|
-
```
|
|
1469
|
-
|
|
1470
|
-
## Conclusion
|
|
1471
|
-
|
|
1472
|
-
You now have a complete, type-safe full-stack application with:
|
|
1473
|
-
|
|
1474
|
-
1. ✅ **FastAPI backend** with comprehensive API
|
|
1475
|
-
2. ✅ **Generated TypeScript types** that match your API exactly
|
|
1476
|
-
3. ✅ **Type-safe API client** with error handling
|
|
1477
|
-
4. ✅ **React hooks** for data fetching with React Query
|
|
1478
|
-
5. ✅ **Full type safety** from database to UI
|
|
1479
|
-
6. ✅ **Automatic synchronization** between backend and frontend types
|
|
1480
|
-
|
|
1481
|
-
### Key Benefits
|
|
1482
|
-
|
|
1483
|
-
- **Type Safety**: Catch API integration errors at compile time
|
|
1484
|
-
- **Developer Experience**: Auto-completion and IntelliSense for API calls
|
|
1485
|
-
- **Maintainability**: Types automatically update when API changes
|
|
1486
|
-
- **Performance**: Optimized data fetching with React Query
|
|
1487
|
-
- **Scalability**: Clean separation of concerns and modular architecture
|
|
1488
|
-
|
|
1489
|
-
### Next Steps
|
|
1490
|
-
|
|
1491
|
-
1. **Add authentication persistence** with localStorage
|
|
1492
|
-
2. **Implement real-time features** with WebSockets
|
|
1493
|
-
3. **Add comprehensive error handling** throughout the app
|
|
1494
|
-
4. **Set up automated type generation** in your CI/CD pipeline
|
|
1495
|
-
5. **Add testing** with the generated types for both frontend and backend
|
|
1496
|
-
|
|
1497
|
-
This tutorial demonstrates the power of Schemantic in creating maintainable, type-safe full-stack applications where the frontend and backend always stay in sync!
|
|
1
|
+
# FastAPI tutorial
|
|
2
|
+
|
|
3
|
+
This example uses Node.js 20+, Python with FastAPI/Uvicorn, and a TypeScript consumer. The generated client uses fetch; no React dependency is needed for the default output.
|
|
4
|
+
|
|
5
|
+
## Run the API
|
|
6
|
+
|
|
7
|
+
```sh
|
|
8
|
+
python -m pip install fastapi uvicorn
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Save `main.py`:
|
|
12
|
+
|
|
13
|
+
```python
|
|
14
|
+
from fastapi import FastAPI
|
|
15
|
+
from pydantic import BaseModel
|
|
16
|
+
|
|
17
|
+
app = FastAPI(title="Users API")
|
|
18
|
+
|
|
19
|
+
class User(BaseModel):
|
|
20
|
+
id: int
|
|
21
|
+
display_name: str
|
|
22
|
+
|
|
23
|
+
@app.get("/users/{user_id}", response_model=User, operation_id="getUser")
|
|
24
|
+
def get_user(user_id: int):
|
|
25
|
+
return User(id=user_id, display_name="Example")
|
|
26
|
+
|
|
27
|
+
@app.post("/users", response_model=User, operation_id="createUser")
|
|
28
|
+
def create_user(user: User):
|
|
29
|
+
return user
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
```sh
|
|
33
|
+
python -m uvicorn main:app --host 127.0.0.1 --port 8000
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Generate and use the client
|
|
37
|
+
|
|
38
|
+
From your TypeScript project:
|
|
39
|
+
|
|
40
|
+
```sh
|
|
41
|
+
npm install --save-dev schemantic typescript
|
|
42
|
+
npx schemantic generate --url http://127.0.0.1:8000/openapi.json --output ./src/generated
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
```ts
|
|
46
|
+
import { UsersApiClient as ApiClient } from './generated/index.js';
|
|
47
|
+
|
|
48
|
+
const client = new ApiClient({ baseUrl: 'http://127.0.0.1:8000' });
|
|
49
|
+
const user = await client.getUser(1);
|
|
50
|
+
console.log(user.id, user.displayName);
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
The explicit `operation_id` gives stable operation/method names. The API sends `display_name`; the default application property is `displayName`. Use `propertyNaming: "preserve"` if your application should keep the wire property names. Generated types derive from the live schema, including request/response projections; inspect generated body signatures before constructing a request. Use requestInit headers for per-call overrides. For bearer auth, use `setAuthToken(token)`/`clearAuthToken()`; the client does not perform an OAuth authorization flow.
|
|
54
|
+
|
|
55
|
+
```ts
|
|
56
|
+
import { UsersApiClient as ApiClient } from './generated/index.js';
|
|
57
|
+
|
|
58
|
+
const client = new ApiClient({ baseUrl: 'http://127.0.0.1:8000' });
|
|
59
|
+
const user = await client.getUser(1, { headers: { 'X-Request-ID': 'example' } });
|
|
60
|
+
console.log(user.id, user.displayName);
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## Optional plain React hooks
|
|
64
|
+
|
|
65
|
+
```sh
|
|
66
|
+
npm install react
|
|
67
|
+
npx schemantic generate --url http://127.0.0.1:8000/openapi.json --output ./src/generated --hooks
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
```tsx
|
|
71
|
+
import { UsersApiClient as ApiClient, createApiHooks } from './generated/index.js';
|
|
72
|
+
|
|
73
|
+
const client = new ApiClient({ baseUrl: 'http://127.0.0.1:8000' });
|
|
74
|
+
const hooks = createApiHooks(client);
|
|
75
|
+
|
|
76
|
+
export function UserView() {
|
|
77
|
+
const { data, error, loading, refetch } = hooks.useGetUserQuery({ path: { user_id: 1 } });
|
|
78
|
+
if (loading) return <p>Loading</p>;
|
|
79
|
+
if (error) return <p>{error.message}</p>;
|
|
80
|
+
return <button onClick={() => { void refetch(); }}>{data?.id}</button>;
|
|
81
|
+
}
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Mutation hooks take no callback options. Get `mutate` and `loading` from the hook, then `await mutate({ body: value })` in an event handler with try/catch. Do not pass TanStack Query `enabled`, `onSuccess`, `onError`, or read `isLoading`; these are plain React hooks.
|
|
85
|
+
|
|
86
|
+
## Config and validation
|
|
87
|
+
|
|
88
|
+
```sh
|
|
89
|
+
npx schemantic init --yes
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
Edit `schemantic.config.json`:
|
|
93
|
+
|
|
94
|
+
```json
|
|
95
|
+
{
|
|
96
|
+
"schemaUrl": "http://127.0.0.1:8000/openapi.json",
|
|
97
|
+
"outputDir": "./src/generated",
|
|
98
|
+
"generateHooks": false
|
|
99
|
+
}
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
```sh
|
|
103
|
+
npx schemantic validate --config
|
|
104
|
+
npx schemantic generate --verbose
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
A config file is never replaced implicitly: explicit `init --force` is required. Validation reports supported-schema diagnostics and does not repair files. File watch is available for local JSON snapshots; URL watch is unsupported.
|
|
108
|
+
|
|
109
|
+
For runtime validation, install Zod 4 and enable `zod-validation`, then explicitly call the generated validator helpers. Enabling a plugin does not insert validation/caching/timing into each endpoint automatically. JSON OpenAPI 3.0.x/3.1.x and local refs are supported; YAML, external refs, and OpenAPI 3.2+ are rejected. See [configuration](CONFIGURATION.md) and [plugin contracts](PLUGINS.md).
|